记录 1 道算法题
排序链表
因为是单向链表,所以不会像数组那么方便,我们可以把他拆成多个链表,然后进行归并,例如有序链表的归并。从而实现对整个链表的排序。分为了递归和循环两种写法。
例如我们对长度是 1 的链表进行合并,我们得到 n 个长度 2 的链表,这 n 个链表是能实现升序排列的,当对 n 个长度为 2 的升序链表进行合并时,合成了 n 个长度为 4 的升序链表,以此类推,归并能够完成数组的排序。问题是如何进行拆分。
- 递归
简单的方法是利用递归,递归会维护一个栈,从上到下,跟分治很像,第一层从中点切开,然后递归左右两个子链表,最后递归的终止条件就是链表无法再进行拆分。即长度为 1 或者 null。null 是因为链表长度不一定是 2 的 n 次方。
function sortList(head, tail) {
// null 或者长度为 1 时
if (head == null) return head
// if (head.next == tail) {
// head.next = null
// return head
// }
if (head.next == null) {
return head
}
let fast = head
let slow = head
let prevSlow = head
let flag = false
// 找中点用快慢指针
while(fast != null) {
fast = fast.next
// 为了断开链表,需要得到中间的前一个节点,
// 如果不使用这种方法的话,可以在递归时进行判断,
// 增加一个参数,尾节点,如果是等于尾节点就赋值 null
if (flag) prevSlow = slow
slow = slow.next
if (fast != null) {
fast = fast.next
}
}
// 截断
prevSlow.next = null
// 递归
return merge(sortList(head), sortList(slow))
}
function merge(node1, node2) {
const head = new ListNode()
let curr = head
// 比较链表的头部,将小的一个插入链表,直到有一边先结束,因为不等长
while(node1 != null, node2 != null) {
if (node1.val < node2.val) {
curr = curr.next = node1
const t = node1.next
node1.next = null
node1 = t
} else {
curr = curr.next = node2
const t = node2.next
node2.next = null
node2 = t
}
}
if (node1 != null) {
curr.next = node1
} else if (node2 != null) {
curr.next = node2
}
return head.next
}
- 循环
循环的版本是一种由下至上的一种思路,循环会准备一个新的链表头,然后根据子链表的长度,将链表的节点拆下来进行分组合并,添加到新链表上。然后再从新链表中把节点按照新的子链表长度拆下来进行分组合并。所以实现了从 1 + 1, 2 + 2 一直递增的归并。这种方法需要知道链表的长度,因为循环需要停止。
简单的表示就是
3 2 5 3 9 8 6 1
子链表长度是指长度为 n 的链表被分到一组
子链表长度 1 时的分组: [3,2] [5,3] [9,8] [6,1] 4组
合并后添加到链表上: 2 3 3 5 8 9 1 6
子链表长度 2 时的分组: [2,3,3,5] [8,9,1,6] 2组
合并后添加到链表上: 2 3 3 5 1 6 8 9
子链表长度 4 时的分组: [2,3,3,5,1,6,8,9] 1组
合并后添加到链表上: 1 2 3 3 5 6 8 9
完成排序
function sortList(head) {
let node = head
let len = 0
while(node != null) {
node = node.next
len++
}
let mergedNode = new ListNode(null, head)
// i 就是子链表的长度,一开始是 1
for(let i = 1; i < len; i <<= 2) {
let head = mergedNode
let curr = mergedNode.next
// 通过 while 完成对每个子链表的截取
while(curr != null) {
let list1 = curr
// 根据子链表长度拿节点,拿完后curr 是第二个子链表的开头
// 第一个子链表不至于会 null,保证可以斩断,但是下一个可能是null
for(let j = 1; j < i && curr.next != null; j++) {
curr = curr.next
}
// 斩断
const temp = curr.next
curr.next = null
let list2 = curr = temp
// 经过了斩断移动到下一节点,可能是 null,
// 也要保证可以第二次斩断,
// 链表的长度不一定是 2 的 n 次方,curr 移动到最后一个节点的时候
for(let j = 1; j < i && curr != null && curr.next != null; j++) {
curr = curr.next
}
// 截断
// 最后一次就不用斩断
if (curr != null) {
const temp = curr.next
curr.next = null
curr = temp
}
// 对一组的两个子链表进行合并
// merge 和上面一样的操作
head.next = merge(list1, list2)
// 有多组,所以要指向最后一个节点
while(head.next != null) {
head = head.next
}
}
}
}