Reference: LeetCode Difficulty: Medium Slow/Fast Pointers
My Post: Java Mergesort for Sort List (Recursion & Iteration)
Problem
Sort a linked list in $O(N\log{N})$ time using constant space complexity.
Example:
1 2 3 4 5 Input: 4 ->2 ->1 ->3 Output: 1 ->2 ->3 ->4 Input: 4 ->2 ->1 ->3 ->5 Output: 1 ->2 ->3 ->4 ->5
Analysis Note: Be careful of linked list generation.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 public ListNode naiveSort (ListNode head) { if (head == null || head.next == null ) { return head; } List<ListNode> array = new ArrayList <>(); ListNode p = head; while (p != null ) { array.add(p); p = p.next; } Comparator<ListNode> comp = (ListNode o1, ListNode o2) -> (o1.val - o2.val); array.sort(comp); ListNode dummy = new ListNode (-1 ); ListNode prev = dummy; for (int i = 0 ; i < array.size(); ++i) { prev.next = array.get(i); prev = prev.next; } prev.next = null ; return dummy.next; }
Time: $O(N + N\log{N} + N) = O(N\log{N})$Space: $O(N)$
Mergesort + Slow/Fast Pointers Recursion Note:
It seems bad to use right-leaning splitInHalf, because you can’t split the list (set the previous node of mid to null).
Since we need to set mid.next to null, we can do right mergeSort first. Clean code.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 public ListNode sortList (ListNode head) { return mergesort(head); } private ListNode mergesort (ListNode head) { if (head == null || head.next == null ) { return head; } ListNode[] splitResult = splitInHalf(head); ListNode left = mergesort(splitResult[0 ]); ListNode right = mergesort(splitResult[1 ]); return mergeList(left, right); } private ListNode mergeList (ListNode head1, ListNode head2) { ListNode dummy = new ListNode (-1 ); ListNode prev = dummy; while (head1 != null && head2 != null ) { if (head1.val < head2.val) { prev.next = head1; head1 = head1.next; } else { prev.next = head2; head2 = head2.next; } prev = prev.next; } if (head1 != null ) prev.next = head1; if (head2 != null ) prev.next = head2; return dummy.next; } private ListNode[] splitInHalf(ListNode head) { if (head == null ) { return new ListNode [] { null , null }; } ListNode slow = head; ListNode fast = head; while (fast.next != null && fast.next.next != null ) { slow = slow.next; fast = fast.next.next; } ListNode[] result = new ListNode [] { head, slow.next }; slow.next = null ; return result; }
Time: $O(N\log{N})$Space: $O(\log{N})$
Iteration Each time we double n.
split(head, n) that splits input head into [n nodes, the rest].
We need to handle a case that n is larger than the number of available nodes.
mergeList(n1, n2) returns [head, tail] of the merged list.
Note: In each loop, p should be initialized with dummy.next rather than head, since head might be changing.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 public ListNode sortList (ListNode head) { if (head == null || head.next == null ) { return head; } int len = getLength(head); ListNode dummy = new ListNode (-1 ); dummy.next = head; for (int n = 1 ; n < len; n *= 2 ) { ListNode p = dummy.next; ListNode prev = dummy; while (p != null ) { ListNode[] result = split(p, n); ListNode left = result[0 ]; ListNode rest = result[1 ]; result = split(rest, n); ListNode right = result[0 ]; rest = result[1 ]; result = mergeList(left, right); prev.next = result[0 ]; prev = result[1 ]; p = rest; } } return dummy.next; }
Helper functions:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 private ListNode[] split(ListNode head, int n) { if (head == null || head.next == null ) { return new ListNode [] { head, null }; } ListNode p = head; while (n > 1 && p.next != null ) { p = p.next; --n; } ListNode rest = p.next; p.next = null ; return new ListNode [] { head, rest }; } private ListNode[] mergeList(ListNode n1, ListNode n2) { ListNode dummy = new ListNode (-1 ); ListNode prev = dummy; while (n1 != null && n2 != null ) { if (n1.val < n2.val) { prev.next = n1; n1 = n1.next; } else { prev.next = n2; n2 = n2.next; } prev = prev.next; } prev.next = (n1 != null ) ? n1 : n2; while (prev.next != null ) prev = prev.next; return new ListNode [] { dummy.next, prev }; } private int getLength (ListNode x) { ... }
Tips for Slow/Fast Pointers Note: Use two pointers to find the middle node in the list. But there are two cases: left-leaning and right-leaning.
The difference lies in the while condition.
Left-leaning: while (fast.next != null && fast.next.next != null) You can see this as a stricter constraint
Right-leaning: while (fast != null && fast.next != null)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 private ListNode findMid (ListNode x) { if (x == null || x.next == null ) { return x; } ListNode slow = x; ListNode fast = x; while (fast.next != null && fast.next.next != null ) { slow = slow.next; fast = fast.next.next; } return slow; } ```java private ListNode findMid (ListNode x) { if (x == null || x.next == null ) { return x; } ListNode slow = x; ListNode fast = x; while (fast != null && fast.next != null ) { slow = slow.next; fast = fast.next.next; } return slow; }