98. 链表排序

描述

在 O(nlogn) 时间复杂度和常数级的空间复杂度下给链表排序

样例

给出 1->3->2->null,给它排序变成 1->2->3->null.

挑战

分别用归并排序和快速排序做一遍。

PS

常数级复杂度即 O(1) 的复杂度只用一个额外变量

知识点

  1. quick sort:
    平均时间复杂度 O(nlogn),如果每次选取的切割点都是链表端点,通过 O(1) 的操作使 O(n) 的问题变为 O(n - 1),最坏情况时间复杂度是 O(n^2),空间复杂度 O(1)
  2. Merge sort
    时间复杂度一直是 O(nlogn),空间复杂度 O(n)
  3. heap sort
    时间复杂度 O(n),空间复杂度 O(1)

区别

无论是快排还是归并排序都需要用递归,但快排是先整体有序后局部有序,而归并排序是先局部有序再整体有序

代码

/**
 * Definition for ListNode.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode(int val) {
 *         this.val = val;
 *         this.next = null;
 *     }
 * }
 */
  1. Merge Sort
public class Solution {            
    private ListNode findMiddle(ListNode head) {
        ListNode slow = head, fast = head.next;
        while (fast != null && fast.next != null) {
            fast = fast.next.next;
            slow = slow.next;
        }
        return slow;
    }    

    // 合并两个链表
    private ListNode merge(ListNode head1, ListNode head2) {
        ListNode dummy = new ListNode(0);
        ListNode tail = dummy;

        while (head1 != null && head2 != null) {
            if (head1.val < head2.val) {
                tail.next = head1;
                head1 = head1.next;
            } else {
                tail.next = head2;
                head2 = head2.next;
            }
            tail = tail.next;
        }

        if (head1 != null) {
            tail.next = head1;
        } else {
            tail.next = head2;
        }

        return dummy.next;
    }

    /* 用二分的方式不断地递归细化链表,直到链表变成只有两个结点,
     * 然后用 merge 排个序,再一层层返回递归,
     * 值得说明的是每退出一层递归都有两个排好序的链表,
     * 每一层递归都要 merge 名为 left 和 right 的两个链表
     */
    public ListNode sortList(ListNode head) {
        if (head == null || head.next == null) {
            return head;
        }

        ListNode mid = findMiddle(head);

        ListNode right = sortList(mid.next);
        // 保证 left 是 head 到 mid 的链表,不然 left 还是从 head 开始的整个链表
        mid.next = null;
        ListNode left = sortList(head);

        return merge(left, right);
    }
}

  1. Quick Sort 1
    链表快排并不是采用两根指针,一根指针从头到尾
public class Solution {
    public ListNode sortList(ListNode head) {
        if (head == null || head.next == null) {
            return head;
        }

        ListNode mid = findMedian(head); // O(n)

        // 定义每一部分的头指针
        ListNode leftDummy = new ListNode(0), leftTail = leftDummy;
        ListNode rightDummy = new ListNode(0), rightTail = rightDummy;
        ListNode middleDummy = new ListNode(0), middleTail = middleDummy;

        // 快排交换结点的模板
        while (head != null) {
            if (head.val < mid.val) {
                leftTail.next = head;
                leftTail = head;
            } else if (head.val > mid.val) {
                rightTail.next = head;
                rightTail = head;
            } else {
                middleTail.next = head;
                middleTail = head;
            }
            head = head.next;
        }

        // 确保每一轮切分后,新的部分到切分点结束,这个地方若不写,则会出现超时,因为不写 null 指针,每一轮递归问题的规模不会下降
        leftTail.next = null;
        middleTail.next = null;
        rightTail.next = null;

        // 递归
        ListNode left = sortList(leftDummy.next);
        ListNode right = sortList(rightDummy.next);

        return concat(left, middleDummy.next, right);
    }

    private ListNode findMedian(ListNode head) {
        ListNode slow = head, fast = head.next;
        // 比较有意思的是,fast.next != null 写到 fast != null 前面会出现空指针溢出错误
        while (fast != null && fast.next != null) {
            slow = slow.next;
            fast = fast.next.next;
        }
        return slow;
    }

    // 把三部分链表拼起来
    private ListNode concat(ListNode left, ListNode middle, ListNode right) {
        ListNode dummy = new ListNode(0), tail = dummy;

        tail.next = left; tail = getTail(tail);
        // 下一个 tail 是 left 的最后一个非空结点,形参的 middle 对应的传入值是middleDummy.next
        tail.next = middle; tail = getTail(tail);
        // 下一个 tail 是 middle 的最后一个非空结点
        tail.next = right;
        // 可写可不写的一句
        tail = getTail(tail);

        return dummy.next;
    }

    // 一个接一个地找到链表中的非空结点,遍历它们,然后返回每一部分最后一个非空结点
    private ListNode getTail(ListNode head) {
        if (head == null) {
           return null;
        } 

        while (head.next != null) {
            head = head.next;
        }
        return head;
    }
}

最后这个地方如果这么写,括号里写 left,middle,right 和写成 tail 实际上差了一个结点,若这么写,
比如:输入2 -> -1 -> 0 -> null,left == null,tail = getTail(left) 会返回 null 使得 tail 指向 null,空指针后面就没办法继续连接 middle 和 right 部分了;但如果是 tail = getTail(tail),tail 是 dummy 结点,沿 tail.next = left 计算时尽管 left 等于空仍旧返回 dummy (非空结点),仍可以继续进行; middle 和 right 为空的情形也是同理,这种写法保证了连接过程中不会出现连接 null 的错误

98. 链表排序_第1张图片
错误
  1. Quick Sort 2

相比于另一种快排省略了连接的过程,两种快排的区别主要在于右边切分部分的头结点位置,本算法在链表最结尾,另一种算法在分割好的右链表的开始结点

// 用pair来代表左右两个链表的分割点
class Pair {
    public ListNode first, second; 
    public Pair(ListNode first, ListNode second) {
        this.first = first;
        this.second = second;
    }
}

public class Solution {
    /**
     * @param head: The head of linked list.
     * @return: You should return the head of the sorted linked list,
                    using constant space complexity.
     */
    public ListNode sortList(ListNode head) {
        if (head == null || head.next == null) {
            return head;
        }

        ListNode mid = findMedian(head); // O(n)
        // 注意这个地方时mid.val
        Pair pair = partition(head, mid.val); // O(n)

        // 递归传入的参数一定得是个变化的值,若是固定值,问题规模不见减小,会出现超时
        ListNode left = sortList(pair.first);
        ListNode right = sortList(pair.second);
        // 把拆开的两行代码重新连起来
        getTail(left).next = right; // O(n)

        return left;
    }

    // 1->2->3 return 2
    // 1->2 return 1
    private ListNode findMedian(ListNode head) {
        ListNode slow = head, fast = head.next;
        // fast == null 代表只有一个结点,fast.next == null 代表只有两个结点,这种情况中值直接返回 slow 就可以了
        while (fast != null && fast.next != null) {
            slow = slow.next;
            fast = fast.next.next;
        }
        return slow;
    }

    // < value in the left, > value in the right
    private Pair partition(ListNode head, int value) {
        ListNode leftDummy = new ListNode(0), leftTail = leftDummy;
        ListNode rightDummy = new ListNode(0), rightTail = rightDummy;
        ListNode equalDummy = new ListNode(0), equalTail = equalDummy;

        // 遍历链表将链表分为 3 部分
        while (head != null) {
            if (head.val < value) {
                leftTail.next = head;
                leftTail = head;
            } else if (head.val > value) {
                rightTail.next = head;
                rightTail = head;
            } else {
                equalTail.next = head;
                equalTail = head;
            }
            // 忘记这句 head 不继续往下移动就会出现超时
            head = head.next;
        }

        leftTail.next = null;
        rightTail.next = null;
        equalTail.next = null;

        // 只有链表中结点值全部一样时才会出现左边没结点,右边也没结点,这时找到 equal 的中间结点,左右各一半
        if (leftDummy.next == null && rightDummy.next == null) {
            ListNode mid = findMedian(equalDummy.next);
            leftDummy.next = equalDummy.next;
            rightDummy.next = mid.next;
            mid.next = null;
        // 之前选取的分割点是链表中最小的点,左边结点少,把中间结点全分给左边
        // 尽量保证快速排序的时间平衡性
        } else if (leftDummy.next == null) {
            leftTail.next = equalDummy.next;
        // 之前选取的分割点是链表中最大的点,右边结点少,把中间结点全分给右边
        // 尽量保证快速排序的时间平衡性
        } else {
            rightTail.next = equalDummy.next;
        }

        // 尤其是在递归中,参数对应的值随时可变,要 deep copy 一下
        return new Pair(leftDummy.next, rightDummy.next);
    }

    private ListNode getTail(ListNode head) {
        if (head == null) {
           return null;
        } 
        // head.next == null 表明遍历到了链表最后一个结点,while 循环确保不返回null
        while (head.next != null) {
            head = head.next;
        }
        return head;
    }
}

你可能感兴趣的:(98. 链表排序)