Welcome to this insightful session, where our aim is to master the complexities of the illustrious applications of sorting algorithms. Today's voyage links two problems: "Find the K-th Ordinal Statistic in a List" and "Count the Number of Inversions in a List." These problems mirror practical scenarios, and the efficient techniques used to solve them present valuable demonstrations of the application of sorting algorithms. By solving these two problems, we'll see how Quick Sort and Merge Sort knowledge applies here and helps provide efficient implementations for both questions.
Let's dive into these captivating problems!
Our first problem presents a list of integers and the number k
. The challenge is finding the k
-th smallest element in that given list. To further elucidate, for k = 1
, you are seeking to find the smallest element; if k = 2
, you're searching for the second smallest element, and so on. By the conclusion of this lesson, you'll be highly skilled at performing this task!
A primary instinctive solution could involve iteratively identifying and discarding the smallest element from the list until you reach the k
-th smallest element. While it sounds straightforward, this approach, unfortunately, incurs high time complexity due to the repetitive scans of the list to locate the smallest element. This solution has an complexity.
Another straightforward solution is just to sort the input array and return the k
-th element:
Go1import "sort" 2 3func kthSmallestNaive(inputArray []int, k int) int { 4 sort.Ints(inputArray) 5 return inputArray[k-1] 6}
This approach has complexity and is as simple as two lines of code. However, there are more efficient approaches than this, as there is an solution (for the average case) to this problem, using Quick Sort techniques we covered earlier in this course.
The Quick Sort algorithm, a splendid application of divide and conquer, can solve this problem efficiently. By selecting the right pivot for partitioning, the input list is divided into two: a left partition, which contains elements less than the pivot, and a right partition, which includes elements greater than the pivot.
In Go, arrays and slices are useful for managing groups of data. We must handle their positions carefully to avoid mistakes. We also use recursive functions, which call themselves, to solve problems with these groups. In recursion, if a pivot’s position matches k
, we’ve found our answer. If the pivot is smaller than k
, we focus on the right side; otherwise, we look at the left side.
We will implement the solution bit by bit, starting with the partition
function:
Go1func partition(arr []int, start, end int) int { 2 pivot := arr[end] 3 i := start - 1 4 5 // Rearrange elements based on pivot 6 for j := start; j < end; j++ { 7 if arr[j] <= pivot { 8 i++ 9 // Swap elements to place element <= pivot on the left 10 arr[i], arr[j] = arr[j], arr[i] 11 } 12 } 13 // Place pivot in its correct position 14 arr[i+1], arr[end] = arr[end], arr[i+1] 15 return i+1 // return pivot position 16}
The partition
function rearranges the elements in the array segment between start
and end
using the last element as the pivot. Elements less than or equal to the pivot are moved to the left, while elements greater than the pivot are on the right. The final position of the pivot is returned, indicating the division between left and right partitions.
Next we will delve into the kthSmallest
function:
Go1func kthSmallest(arr []int, start, end, k int) int { 2 if k > 0 && k <= end-start+1 { 3 pos := partition(arr, start, end) // Partition the array 4 // Check if pivot position is the k-1 index 5 if pos-start == k-1 { 6 return arr[pos] // Found the k-th smallest element 7 } 8 // Search left subarray 9 if pos-start > k-1 { 10 return kthSmallest(arr, start, pos-1, k) 11 } 12 // Search right subarray 13 return kthSmallest(arr, pos+1, end, k-pos+start-1) 14 } 15 return math.MaxInt // Return max int if no valid k-th smallest found 16}
The kthSmallest
function uses the partition
function to find the position of the pivot. It recursively searches the appropriate partition for the k-th smallest element. If the pivot's position aligns with the (k-1)
index, the k-th smallest element has been found. Otherwise, it continues the search in the left or right subarray based on the pivot's position.
Finally, the findKthSmallest
function:
Go1func findKthSmallest(numbers []int, k int) (int, error) { 2 if numbers == nil || len(numbers) < k { 3 return math.MinInt, fmt.Errorf("invalid input") 4 } 5 return kthSmallest(numbers, 0, len(numbers)-1, k), nil 6}
The findKthSmallest
function serves as the main interface, validating the input list and its size relative to k
. It initiates the recursive search for the k-th smallest element by calling the kthSmallest
function, ensuring robust handling of edge cases such as invalid input data.
Our second problem entails a list of integers; your task is to deduce the number of inversions in the list.
An inversion is a pair of elements where the larger element appears before the smaller one. In other words, if we have two indices i
and j
, where i < j
and the element at position i
is greater than the element at position j
(numbers[i] > numbers[j]
), we have an inversion.
For example, for numbers = {4, 2, 1, 3}
, there are four inversions: (4, 2)
, (4, 1)
, (4, 3)
, and (2, 1)
.
Again we will build our solution little by little. We will first create a supporting structure that we will use in the solution:
Go1type Result struct { 2 sorted []int // slice to store the sorted version of the array 3 inversions int // count of inversions found 4}
This structure will hold sorted slices as well as the count of inversions found.
Next, we will write the mergeAndCountInversions
function:
Go1// Merges subarrays while counting inversions. 2func mergeAndCountInversions(left, right []int) Result { 3 merged := make([]int, 0, len(left)+len(right)) 4 i, j, inversions := 0, 0, 0 5 6 for i < len(left) && j < len(right) { 7 if left[i] <= right[j] { 8 merged = append(merged, left[i]) // Append left element 9 i++ 10 } else { 11 merged = append(merged, right[j]) // Append right element 12 j++ 13 inversions += len(left) - i // Count inversions 14 } 15 } 16 // Append remaining elements 17 merged = append(merged, left[i:]...) 18 merged = append(merged, right[j:]...) 19 20 return Result{sorted: merged, inversions: inversions} 21}
The mergeAndCountInversions
function merges two sorted subarrays while counting inversions. During the merge, if an element in the right subarray is smaller than an element in the left, every remaining element in the left subarray is an inversion with the right element. This function returns both the merged array and the count of inversions, maintaining an efficient complexity like the standard merge process.
Finally, we get to the countInversions
function:
Go1func countInversions(arr []int) Result { 2 if len(arr) <= 1 { 3 return Result{sorted: arr, inversions: 0} 4 } 5 middle := len(arr) / 2 6 left := countInversions(arr[:middle]) 7 right := countInversions(arr[middle:]) 8 result := mergeAndCountInversions(left.sorted, right.sorted) 9 return Result{sorted: result.sorted, inversions: left.inversions + right.inversions + result.inversions} 10}
The countInversions
function applies a modified merge sort to count inversions. It recursively splits the array into halves and sorts each while counting inversions in subarrays. Upon merging, it accumulates the inversions found via mergeAndCountInversions
along with those identified individually in the left and right subarrays. This approach efficiently calculates the total number of inversions, maintaining the efficient complexity characteristic of merge sort.
During today's lesson, we thoroughly inspected the advanced applications of Quick Sort and Merge Sort algorithms through the dissection of two exciting problems. We went from recognizing the issues, proposing naive methods, progressing towards efficient approaches, and executing the solutions in Go.
You're now ready to flex those coding muscles! We have covered a substantial amount of theory and strongly advocate that real-world application solidifies the lessons learned. Be prepared for the forthcoming exercises, where you'll apply the logic imbibed in this session to similar problems. Let's get hands-on!