6

I was asked to write a function that takes 3 unsorted linked lists and returns one single sorted linked list that combines all three lists. What is the best way you can think of?

I don't really have restrictions of memory, but what would you do with/without memory restrictions?

Jonathan Leffler
  • 730,956
  • 141
  • 904
  • 1,278
Awesomenuts
  • 61
  • 1
  • 1
  • 2
  • 1
    Added the homework tag. How do you have to sort 'em anyway? Alphabetically, from smallest to greatest..? – BlackBear Aug 23 '11 at 22:12
  • 10
    join the 3 lists together (tail of list 1 -> head list 2, etc..), then you've only got 1 list and reduces to a simple sort function. – Marc B Aug 23 '11 at 22:12
  • What sorting algorithms do you know? – Beta Aug 23 '11 at 22:13
  • If the sort needs to be a linked list sort, then a bottom up merge sort using a small array of pointers to nodes is fastest. [wiki example](https://en.wikipedia.org/wiki/Merge_sort#Bottom-up_implementation_using_lists). Since nodes are merged into the internal array one at a time, the 3 lists can be handled separately or concatenated into a single list before merging into the internal array. The internal array is then merged to form a single sorted list. – rcgldr Dec 22 '17 at 16:18

5 Answers5

40

One option would be to use merge sort on all three of the linked lists, then use one final merge step to merge them together into an overall sorted list.

Unlike most O(n log n) sorting algorithms, merge sort can run efficiently on linked lists. At a high-level, the intuition behind merge sort on a linked list is as follows:

  1. As a base case, if the list has zero or one elements, it's already sorted.
  2. Otherwise:
    1. Split the list into two lists of roughly equal size, perhaps by moving odd elements into one list and even elements into the other.
    2. Recursively use merge sort to sort those lists.
    3. Apply a merge step to combine those lists into one sorted list.

The merge algorithm on linked lists is really beautiful. The pseudocode works roughly like this:

  1. Initialize an empty linked list holding the result.
  2. As long as both lists aren't empty:
    1. If the first element of the first list is less than the first element of the second list, move it to the back of the result list.
    2. Otherwise, move the first element of the second list to the back of the result list.
  3. Now that exactly one list is empty, move all the elements from the second list to the back of the result list.

This can be made to run in O(n) time, so the overall complexity of the merge sort is O(n log n).

Once you've sorted all three lists independently, you can apply the merge algorithm to combine the three lists into one final sorted list. Alternatively, you could consider concatenating together all three linked lists, then using a giant merge sort pass to sort all of the lists at the same time. There's no clear "right way" to do this; it's really up to you.

The above algorithm runs in Θ(n log n) time. It also uses only Θ(log n) memory, since it allocates no new linked list cells and just needs space in each stack frame to store pointers to the various lists. Since the recursion depth is Θ(log n), the memory usage is Θ(log n) as well.


Another O(n log n) sort that you can implement on linked lists is a modification of quicksort. Although the linked list version of quicksort is fast (still O(n log n) expected), it isn't nearly as fast as the in-place version that works on arrays due to the lack of locality effects from array elements being stored contiguously. However, it's a very beautiful algorithm as applied to lists.

The intuition behind quicksort is as follows:

  1. If you have a zero- or one-element list, the list is sorted.
  2. Otherwise:
    1. Choose some element of the list to use as a pivot.
    2. Split the list into three groups - elements less than the pivot, elements equal to the pivot, and elements greater than the pivot.
    3. Recursively sort the smaller and greater elements.
    4. Concatenate the three lists as smaller, then equal, then greater to get back the overall sorted list.

One of the nice aspects of the linked-list version of quicksort is that the partitioning step is substantially easier than in the array case. After you've chosen a pivot (details a bit later), you can do the partitioning step by creating three empty lists for the less-than, equal-to, and greater-than lists, then doing a linear scan over the original linked list. You can then append/prepend each linked list node to the linked list corresponding to the original bucket.

The one challenge in getting this working is picking a good pivot element. It's well known that quicksort can degenerate to O(n2) time if the choice of pivot is bad, but it is also known that if you pick a pivot element at random the runtime is O(n log n) with high probability. In an array this is easy (just pick a random array index), but in the linked list case is trickier. The easiest way to do this is to pick a random number between 0 and the length of the list, then choose that element of the list in O(n) time. Alternatively, there are some pretty cool methods for picking an element at random out of a linked list; one such algorithm is described here.


If you want a simpler algorithm that needs only O(1) space, you can also consider using insertion sort to sort the linked lists. While insertion sort is easier to implement, it runs in O(n2) time in the worst case (though it also has O(n) best-case behavior), so it's probably not a good choice unless you specifically want to avoid merge sort.

The idea behind the insertion sort algorithm is as follows:

  1. Initialize an empty linked list holding the result.
  2. For each of the three linked lists:
    1. While that linked list isn't empty:
      1. Scan across the result list to find the location where the first element of this linked list belongs.
      2. Insert the element at that location.

Another O(n2) sorting algorithm that can be adapted for linked lists is selection sort. This can be implemented very easily (assuming you have a doubly-linked list) by using this algorithm:

  1. Initialize an empty list holding the result.
  2. While the input list is not empty:
    1. Scan across the linked list looking for the smallest remaining element.
    2. Remove that element from the linked list.
    3. Append that element to the result list.

This also runs in O(n2) time and uses only O(1) space, but in practice it's slower than insertion sort; in particular, it always runs in Θ(n2) time.


Depending on how the linked lists are structured, you might be able to get away with some extremely awesome hacks. In particular, if you are given doubly-linked lists, then you have space for two pointers in each of your linked list cells. Given that, you can reinterpret the meaning of those pointers to do some pretty ridiculous sorting tricks.

As a simple example, let's see how we could implement tree sort using the linked list cells. The idea is as follows. When the linked list cells are stored in a linked list, the next and previous pointers have their original meaning. However, our goal will be to iteratively pull the linked list cells out of the linked list, then reinterpret them as nodes a in binary search tree, where the next pointer means "right subtree" and the previous pointer means "left subtree." If you're allowed to do this, here's a really cool way to implement tree sort:

  1. Create a new pointer to a linked list cell that will serve as the pointer to the root of the tree.
  2. For each element of the doubly-linked list:
    1. Remove that cell from the linked list.
    2. Treating that cell as a BST node, insert the node into the binary search tree.
  3. Do an in-order walk of the BST. Whenever you visit a node, remove it from the BST and insert it back into the doubly-linked list.

This runs in best-case O(n log n) time and worst-case O(n2). In terms of memory usage, the first two steps require only O(1) memory, since we're recycling space from the older pointers. The last step can be done in O(1) space as well using some particularly clever algorithms.

You could also consider implementing heap sort this way as well, though it's a bit tricky.


Hope this helps!

templatetypedef
  • 362,284
  • 104
  • 897
  • 1,065
  • That's kind of what I was getting at, but you put it a lot better than I did! :O) – Richard Baxter Aug 23 '11 at 22:32
  • Unless I'm mistaken, qsort works well/okay when the linked list is doubly linked. Might want to point that out. – Thomas Eding Aug 23 '11 at 23:19
  • @trinithis- Yes, that's true. I was mostly pointing out that quicksort doesn't get the huge performance win from locality that causes it to outperform all the other O(n log n) sorts when dealing with the array case. – templatetypedef Aug 23 '11 at 23:24
  • can you please explain the time complexity of the merge step? – CodeYogi Jul 28 '16 at 12:22
  • @CodeYogi Merging two sorted linked lists, like merging two sorted arrays, can be done in time O(n), where n is the total number of elements in the lists. You just need to store tail pointers. – templatetypedef Jul 28 '16 at 13:35
  • Wiki article for merge sort for linked list now includes a faster [bottom up merge sort for linked list](http://en.wikipedia.org/wiki/Merge_sort#Bottom-up_implementation_using_lists) that uses a small (25 to 32) array of pointers (or references) to nodes. This is about twice as fast as a top down merge sort that has to repeated scan sub-lists in order to split them. In this case, all 3 lists could be merged into the array (since this part is done one node at a time), then the array merged to form a single list. – rcgldr Nov 09 '16 at 18:45
0

If the 3 lists were individually sorted the problem would be simple, but as they aren't it's a little more tricky.

I would write a function that takes a sorted list and an unsorted list as parameters, goes through each item of the unsorted list and adds it in the correct position in the sorted list in turn until there are no items left in the unsorted list.

Then simply create a forth "empty" list which by the very nature of being empty is "sorted" and then call your method three times with each of the unsorted lists.

Converting the lists to arrays may make things a little more efficient in terms of being able to use more advanced sort techniques, but the cost of converting to an array has to be considered and balanced against the size of the original lists.

Richard Baxter
  • 1,317
  • 1
  • 15
  • 24
0

I was thinking that you can apply quick sort. It is almost same as merge sort, only difference is that you first split and then merge, where whit quicksort you first "merge" and then you make split. If you look little different is mergesort quicksort in opposite direction

mergesort:

split -> recursion -> merge

quicksort:

umnerge (opposite of merge) -> recursion -> join (opposite of split)

Luka Rahne
  • 10,336
  • 3
  • 34
  • 56
-1

The mergesort algorithm described in the popular post by @templatetypedef does not work in O(n lg n). Because a linked list is not random access, step 2.1 Split the list into two lists of roughly equal size actually means an overall algorithm of O(n^2 log n) to sort the list. Just think about it a bit.

Here is a link that uses mergesort to sort a linked list by first reading the elements into an array -- http://www.geekviewpoint.com/java/singly_linked_list/sort.

kasavbere
  • 5,873
  • 14
  • 49
  • 72
  • 3
    As I mentioned in my comment on your answer to another question, this is not true. The linear work required to make the split does not increase the asymptotic runtime because there is already linear work being done in the merge step. The recurrence relation is exactly the same. Historically, merge sort was invented to work on tape drives (which function similarly to linked lists in many regards) and improved over the naive O(n^2) sorts, which is why it was used. – templatetypedef Jan 11 '13 at 03:49
-7

There are no effecient sorting algorithms for linked lists. make an array, sort, and relink.

ddyer
  • 1,792
  • 19
  • 26
  • 4
    Err...mergesort works just fine. The only trick is figuring out how to divide the list efficiently. For instance: http://www.chiark.greenend.org.uk/~sgtatham/algorithms/listsort.html . – dmckee --- ex-moderator kitten Aug 23 '11 at 22:23
  • @dmckee - Merge sort only works if the lists are already sorted -- in this case the 3 list are originally un-sorted, so step 1 would be to sort the linked lists, and then merge join them -- if memory is not an issue, then creating an array of pointers, sorting the pointers and then create a new linked list would be more efficient. – Soren Aug 23 '11 at 22:30
  • 1
    Soren: mergesort can be used *on* the unsorted lists, *then* the merge function/method/tool can be used to combine them. – dmckee --- ex-moderator kitten Aug 23 '11 at 22:33
  • 1
    -1: I use mergesort all the time in my Haskell programs. It comes under the guise `Data.List.sort`. It runs efficiently in O(nlogn). – Thomas Eding Aug 23 '11 at 22:44
  • 1
    BTW: This would be a good time to get a [disciplined badge](http://stackoverflow.com/badges/37/disciplined). – dmckee --- ex-moderator kitten Aug 24 '11 at 20:48
  • Starting with unsorted lists, any sort based on lists is O(n) – ddyer Sep 20 '11 at 07:09