[路飞] 排序链表

165 阅读3分钟

记录 1 道算法题

排序链表

leetcode-cn.com/problems/so…


因为是单向链表,所以不会像数组那么方便,我们可以把他拆成多个链表,然后进行归并,例如有序链表的归并。从而实现对整个链表的排序。分为了递归和循环两种写法。

例如我们对长度是 1 的链表进行合并,我们得到 n 个长度 2 的链表,这 n 个链表是能实现升序排列的,当对 n 个长度为 2 的升序链表进行合并时,合成了 n 个长度为 4 的升序链表,以此类推,归并能够完成数组的排序。问题是如何进行拆分。

  1. 递归

简单的方法是利用递归,递归会维护一个栈,从上到下,跟分治很像,第一层从中点切开,然后递归左右两个子链表,最后递归的终止条件就是链表无法再进行拆分。即长度为 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 + 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
                }
            }
        }
    }