QuickSort on Singly Linked List
Linked lists are common data structures used in many algorithms and applications. They allow efficient insertion and deletion of elements, unlike static arrays. However, sorting linked lists efficiently presents unique challenges compared to regular arrays. QuickSort is one of the most popular sorting algorithms for arrays, known for its fast average performance. This article will discuss how QuickSort can be adapted to sort a singly linked list in O(n log n) time complexity.
Sorting a linked list efficiently is important for many use cases. While languages like C provide the Qsort library function for sorting arrays, linked lists require a custom sorting approach. QuickSort is an optimal algorithm for sorting arrays, but implementing it on linked lists takes some modification. We will go through a stepbystep guide on applying the QuickSort algorithm to sort a single linked list in place. We will cover details like selecting a pivot, partitioning around it, recursively sorting sublists, and joining sorted sublists back together. With the right pivot selection and care during partitioning, QuickSort can achieve its fast average case runtime of O(n log n) even for linked lists.
What is QuickSort?
QuickSort is a divideandconquer sorting algorithm that selects a 'pivot' element from the array and partitions the other elements into two subarrays according to whether they are less than or greater than the pivot.
The key steps of QuickSort are:
Pivot Selection
Choosing the pivot is important as it impacts the partitioning and runtime. Some common pivot selection methods:
 First element  Simple but can cause worstcase O(n^2) on already sorted arrays.
 The last element is simple but has the same issue as above.
 The median of first, middle and last elements  Adds O(n) cost but improves the chance of balanced partitions.
 Random element  Good guarantee of O(n log n) runtime. Need random number generator.
Partitioning Around the Pivot
It is the core step of QuickSort. It rearranges elements across the pivot index. A typical partitioning routine:
 Start left (l) and right (r) index variables at the start and end.
 Loop while l is still less than r:
 Increment l till we find an element greater than the pivot.
 Decrement r till we find an element less than the pivot.
 If l is still <= r, swap elements at indexes l and r.
 Finally, swap the pivot with the element at index r.
It partitions the array into elements less than the pivot from 0 to r1, pivot at r, and greater than the pivot from r+1 to end.
Recursive Sorting
With each partition call, the algorithm recursively operates on smaller subsets:
 Recursively call QuickSort on the left subset from index 0 to r1.
 Recursively call QuickSort on the right subset from r+1 to end.
Base cases are empty or singleelement subsets that don't need sorting.
The recursion tree has a depth of O(log n).
Time and Space Complexity
 Time Complexity: O(n log n) average case. O(n^2) worst case.
 Space Complexity: O(log n) stack space for recursive calls.
Optimizations
Some optimizations to improve performance:
 Switch to Insertion Sort for small arrays.
 Pick a random pivot to avoid the worst case.
 Tail call elimination or iteration instead of recursion.
Properties of Quick Sort
Here are some key properties and characteristics of the QuickSort algorithm:
 Comparisonbased sort  QuickSort compares elements and swaps them into positions based on the comparisons. This allows it to work with any data type that can be compared.
 Divide and Conquer  QuickSort uses a divideandconquer approach to break the problem into smaller subproblems. This recursive decomposition is key to its efficiency.
 O(n log n) time complexity  The average case runtime of QuickSort is O(n log n). This is optimal for comparisonbased sorting. It's faster than algorithms like insertion sort and merge sort.
 O(n^2) worst case  In the worst case, where the pivot causes very uneven partitions, QuickSort can degrade to O(n^2) time. It is rare and can be avoided by random pivot selection.
 Cache friendly  QuickSort exhibits good locality of reference as it operates on subsets of data partitioned next to each other. It makes good use of fast cache memory.
 Inplace  QuickSort sorts the array in place, requiring only a small auxiliary stack for recursion. No extra storage is needed for manipulating elements.
 Unstable sort  Being an inplace sort, the relative order of equal elements is not preserved by QuickSort. This makes it unsuitable for some applications.
 Not adaptive  QuickSort always divides problems into base cases, even if the array is partially sorted. Adaptive algorithms like merge sort handle these cases better.
 Hard to parallelize  The recursive nature and dependence on pivots make QuickSort challenging to parallelize efficiently across multiple CPU cores.
So, in summary, QuickSort provides fast average case performance for sorting while efficiently utilizing memory caches. However, its instability and lack of adaptiveness must be considered for various use cases.
Advantages of using QuickSort
Speed:
 QuickSort has an optimal O(n log n) time complexity on average. This makes it faster than insertion sort, selection sort, bubble sort, etc., which have quadratic time complexities.
 It is also faster than heapsort and merges sort, which has O(n log n) worstcase time, but constant factors slow them down in practice. QuickSort leverages partitioning for faster practical performance.
Memory Usage:
 QuickSort sorts are in place, requiring only O(log n) space for recursion. Algorithms like merge sort require O(n) extra space for copying data.
 It has better cache performance by operating on data organized into partitions. This minimizes slow memory access.
Simplicity:
 QuickSort is simpler to implement than merge sort, which requires maintaining extra arrays/lists.
 The inplace partitioning routine with single loop and swaps is easy to code.
 It is a textbook example of an elegant divideandconquer recursive algorithm.
General Purpose:
 Unlike radix sort, which depends on keys, it works for all data types that can be compared.
 Not limited to just numbers. Can sort strings, structures, custom objects, etc.
Adaptability:
 Various optimizations, like pivot selection, inplace partitioning, etc., allow QuickSort to be adapted for different situations.
 It can be modified to have stable behaviour if element order needs preservation.
So, in summary, QuickSort's optimal speed, efficient memory usage, simplicity of implementation and adaptability give it significant advantages over other sorting algorithms for a wide range of use cases.
Python Implementation of QuickSort on Singly Linked Lists
Output:
Explanation
 The Node class represents each node having data and next pointer.
 partition(head, tail) handles partitioning the list around the pivot:
 Check for an empty list or a single node. Return head in that case.
 Select the first node (head) as the pivot element.
 Initialize left and right markers to head and head. next.
 Loop through list till right != tail.next:
 If the right.data is less than the pivot, swap the right.data with the left.data to place it before the pivot.
 Advance left and right pointers.
 Finally, swap the pivot with the value at the left marker.
 Return the left index, which divides the list into lower and higher partitions.
 quickSort(head, tail) recursively sorts the list:
 Base cases  empty or single node list.
 Call partition() to divide the list into two parts.
 Recursively call quickSort on the left partition from start to pivot index returned by partition.
 Recursively call quickSort on the right partition from the pivot. next to the end.
 printList() prints the linked list data.
 The main section creates a linked list, calls quickSort on the entire list, and prints the sorted result.
So, in summary, it partitions the list in place around a pivot, recursively sorts the sublists, and iterates this process to sort the complete list. The key aspect is efficiently partitioning without extra space.
Conclusion
In this article, we explored implementing the QuickSort algorithm to efficiently sort singly linked lists in average O(n log n) time. The key ideas included selecting a good pivot, partitioning the list around the pivot by rearranging node pointers, and applying divideandconquer recursion on the sublists. With an appropriate pivot scheme and careful list manipulation, QuickSort can match its excellent performance on arrays when applied to linked lists. It allows sorting without the overhead of data movement or extra storage. QuickSort on linked lists demonstrates the flexibility of the algorithm across data structures.
