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 step-by-step 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.

QuickSort on Singly Linked List

What is QuickSort?

QuickSort is a divide-and-conquer sorting algorithm that selects a 'pivot' element from the array and partitions the other elements into two sub-arrays 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 worst-case 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 r-1, 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 r-1.
  • Recursively call QuickSort on the right subset from r+1 to end.

Base cases are empty or single-element 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:

  • Comparison-based 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 divide-and-conquer approach to break the problem into smaller sub-problems. 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 comparison-based 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.
  • In-place - 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 in-place 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) worst-case 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 in-place partitioning routine with single loop and swaps is easy to code.
  • It is a textbook example of an elegant divide-and-conquer 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, in-place 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:

QuickSort on Singly Linked List

Explanation

  1. The Node class represents each node having data and next pointer.
  2. 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.
  3. 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.
  4. printList() prints the linked list data.
  5. 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 divide-and-conquer 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.