Skip to content
dsa450for five year olds

Linked List · 23 of 36

Sort a k sorted Doubly Linked list

Sort a two way chain where every box is at most k places from where it belongs, by keeping only the next k + 1 boxes in a small pile that always hands you the smallest.

MediumHeap of size kMust do
Simple way
time O(n log n)
room O(n)
Clever way
time O(n log k)
room O(k)

The problem, simply

You get a doubly linked list (each box has next and prev arrows) and a number k. The chain is almost sorted: every box is at most k places away from the spot it would have in the sorted chain.

Sort the chain from small to big, fix every next and prev arrow, and give back the new head. Use the "almost sorted" hint to be faster than a normal sort.

A story to remember it

Picture it

doubly linked list
null36212568head

The chain 3 6 2 12 56 8 with k = 2. Box 2 belongs at the front, and it is exactly 2 places away from there.

k
2
  • looking now

Examples

A normal case

In
3 <-> 6 <-> 2 <-> 12 <-> 56 <-> 8, k = 2
Out
2 <-> 3 <-> 6 <-> 8 <-> 12 <-> 56

Every box moves at most 2 places.

Neighbors swapped

In
2 <-> 1 <-> 3 <-> 5 <-> 4 <-> 6, k = 1
Out
1 <-> 2 <-> 3 <-> 4 <-> 5 <-> 6

With k = 1, a box is at most one place off, so the pile only needs 2 boxes.

Already sorted

In
1 <-> 2 <-> 3, k = 0
Out
1 <-> 2 <-> 3

k = 0 means every box is already in place. A pile of one box hands them out in the same order.

k as big as the chain

In
5 <-> 4 <-> 3 <-> 2 <-> 1, k = 4
Out
1 <-> 2 <-> 3 <-> 4 <-> 5

The pile holds everything, so this becomes a full heap sort.

Way 1: the simple way

Ignore the hint. Copy all the numbers out, sort them, and write them back.

  1. Walk the chain and put every number in a list.
  2. Sort the list.
  3. Walk the chain again and write the sorted numbers into the boxes.
doubly linked list
null36212568head
arraysorted copy
01234523681256

All six numbers copied out and sorted. They will be written back into the same boxes from the head.

  • done
java
import java.util.*;

class Solution {
    public Node sortKSorted(Node head, int k) {
        List<Integer> nums = new ArrayList<>();
        for (Node cur = head; cur != null; cur = cur.next) nums.add(cur.data);
        Collections.sort(nums);                       // a normal full sort
        int i = 0;
        for (Node cur = head; cur != null; cur = cur.next) cur.data = nums.get(i++);
        return head;
    }
}

How long it takes: the sort is O(n log n). Extra room: the copy of all numbers, O(n). It throws away the hint that the chain is almost sorted.

Way 2: the clever way

The smallest box of the whole chain must be among the first k + 1 boxes, because it is at most k places from the front. The same is true for every next smallest box. So keep a small pile, a min heap, that always holds the next k + 1 boxes and gives back the smallest one fast.

  1. Put the first k + 1 boxes into the min heap.
  2. Take the smallest box out and hook it to the end of the sorted chain, fixing both next and prev.
  3. If boxes are left in the input, put the next one into the heap.
  4. Repeat until the heap is empty. Set the last box's next to null.

A pile of 3 boxes always gives the next smallest

01 / 13

heapmin heap
empty heap
doubly linked listnot read yet
null36212568curr
linked listsorted so far
null

k is 2, so the pile will hold k + 1 = 3 boxes. Nothing is read yet.

k
2
heap size
0
  • looking now
  • done
  • answer
  • just added
java
import java.util.*;

class Solution {
    public Node sortKSorted(Node head, int k) {
        if (head == null) return null;
        PriorityQueue<Node> pile = new PriorityQueue<>((a, b) -> Integer.compare(a.data, b.data));
        Node curr = head;
        for (int i = 0; i <= k && curr != null; i++) {   // the first k + 1 boxes
            pile.offer(curr);
            curr = curr.next;
        }
        Node newHead = null, tail = null;
        while (!pile.isEmpty()) {
            Node small = pile.poll();                      // the next smallest box
            if (newHead == null) { newHead = small; small.prev = null; }
            else { tail.next = small; small.prev = tail; } // hook both arrows
            tail = small;
            if (curr != null) {                            // keep the pile full
                pile.offer(curr);
                curr = curr.next;
            }
        }
        tail.next = null;
        return newHead;
    }
}

Reading curr.next is safe even while we rewire, because we only change the arrows of boxes that already left the pile, and curr is always ahead of them. How long it takes: each of the n boxes goes into a pile of at most k + 1 boxes and comes out once, and each of those costs O(log k), so O(n log k). Extra room: the pile, O(k).

Another way: insertion sort with the back arrows

A doubly list can walk backwards, so insertion sort fits it well. Take each box and walk back with prev until you find a smaller number, then move the box there. Since every box is at most k places off, each walk back is at most k steps. This takes O(n * k) time and O(1) room, which beats the heap when k is tiny, like 1 or 2.

java
class Solution {
    public Node sortKSorted(Node head, int k) {
        if (head == null) return null;
        Node cur = head.next;
        while (cur != null) {
            Node next = cur.next;
            Node spot = cur.prev;
            while (spot != null && spot.data > cur.data) spot = spot.prev;   // at most k steps
            if (spot != cur.prev) {
                cur.prev.next = cur.next;                  // unhook cur
                if (cur.next != null) cur.next.prev = cur.prev;
                if (spot == null) {                        // cur becomes the new head
                    cur.prev = null; cur.next = head; head.prev = cur; head = cur;
                } else {                                   // put cur right after spot
                    cur.next = spot.next; cur.prev = spot;
                    spot.next.prev = cur; spot.next = cur;
                }
            }
            cur = next;
        }
        return head;
    }
}

Watch out for

  • Fix both arrows when hooking a box: tail.next = small and small.prev = tail. The new head needs prev = null and the last box needs next = null.
  • (a, b) -> a.data - b.data can overflow for very large or very negative numbers. Use Integer.compare.
  • Equal numbers: the heap is not stable, so equal boxes can swap places. The insertion sort way keeps them in order.

What interviewers ask next

  • Why does this work? The box that belongs at position i sits somewhere between i - k and i + k. When we fill position i, every box up to i + k has been read, so it is in the pile or already placed.
  • Merge k sorted lists (LeetCode 23). Same pile, but it holds the front box of each list.
  • Sort a nearly sorted array. The same pile of k + 1, writing back into the array.
  • Heap or insertion sort? Heap is O(n log k), insertion sort is O(n k) with no extra room. For k of 1 or 2 insertion sort wins; for larger k the heap wins.
  • What if k is unknown? You cannot use the window trick safely. Fall back to merge sort on the list, O(n log n).

Remember this

Grown-up words: the pile that always hands out the smallest is a min heap or priority queue, the boxes are nodes with prev and next pointers, "every box at most k places off" is a k-sorted list, and walking back to place each box is insertion sort.