Lesson 7
TypeScript Adventures in Sorting Algorithms: Quick Sort and Merge Sort Applications
Introduction

Ready for an adventure in sorting algorithms? We will solve two fun problems: "Find the K-th Ordinal Number in a List" and "Count the Number of Flips in a List". These tasks portray situations where we need clever system design. Let's employ quick sort and merge sort to find efficient solutions. Buckle up!

Problem 1: Finding K-th Number in an Array

Imagine an array of numbers and a number k. Your mission is to discover the k-th smallest number in that array. k starts from 1, so when k = 1, we seek the smallest number; when k = 2, we want the second smallest, and onward.

Problem 1: Simple Solutions

The first solution might involve scanning and shrinking the array by removing the smallest number until you reach the k-th smallest. But this method, while straightforward, has a time complexity of O(n2)O(n^2) due to continuous array rewriting.

An efficient plan might be to sort the array and then directly select the k-th number:

TypeScript
1function findKthNumber(inputArray: number[], k: number): number { 2 inputArray.sort((a, b) => a - b); // Sort array ascending 3 return inputArray[k - 1]; // Return k-th element 4}

This method has a better time complexity — O(nlogn)O(n \log n). But can we do even better? Quick sort thinks so.

Problem 1: Quick Sort to the Rescue

Quick sort can provide an optimal solution. We’ll divide the array into two parts using a pivot: the left side contains numbers less than the pivot, while the right side has all greater numbers.

If the pivot's position equals k, that's our answer! If not, we repeat the process on the necessary partition.

Problem 1: Building the Solution – Partition

It's coding time! Let’s make a function for partitioning in TypeScript.

TypeScript
1function partition(arr: number[], low: number, high: number): number { 2 const pivot = arr[low]; // Select pivot 3 let i = low; // Index of smaller element 4 5 for (let j = low + 1; j <= high; j++) { 6 if (arr[j] <= pivot) { 7 i++; 8 [arr[i], arr[j]] = [arr[j], arr[i]]; // Swap elements 9 } 10 } 11 12 [arr[i], arr[low]] = [arr[low], arr[i]]; // Swap pivot 13 return i; // Return pivot index 14}
Problem 1: Building the Solution – Main Logic

Now we use our partition function in the main logic. If the pivot's position equals k, return the pivot. Otherwise, check the appropriate partition!

TypeScript
1function findKthSmallest(numbers: number[], k: number): number { 2 if (!numbers || numbers.length < k) return Number.MIN_SAFE_INTEGER; 3 return kthSmallest(numbers, 0, numbers.length - 1, k); 4} 5 6function kthSmallest(arr: number[], start: number, end: number, k: number): number { 7 if (k > 0 && k <= end - start + 1) { 8 const pos = partition(arr, start, end); // Get partition index 9 if (pos - start === k - 1) { 10 return arr[pos]; // K-th element found 11 } 12 if (pos - start > k - 1) { 13 return kthSmallest(arr, start, pos - 1, k); // Search left part 14 } 15 return kthSmallest(arr, pos + 1, end, k - pos + start - 1); // Search right part 16 } 17 return Number.MAX_SAFE_INTEGER; 18} 19 20console.log(findKthSmallest([1, 7, 2, 4, 2, 1, 6], 5)); // Prints 4

Number.MIN_SAFE_INTEGER is returned by findKthSmallest if the input array numbers is empty or has fewer elements than k. This could represent a case where the k smallest value doesn't exist.

Number.MAX_SAFE_INTEGER is used in the kthSmallest function if k is either less than 1 or greater than the length of the portion of the array being considered. This could mean that there's an error in the k parameter passed, or the array doesn't have enough elements.

These extreme values are used because they are unlikely to be a valid result in any normal situation, making it easy to spot when something has gone wrong. It's a convention to return values that clearly indicate error situations, aiding in debugging and error handling.

Problem 2: Counting Flips in an Array

Problem two presents an array; your challenge is counting the flips or inversions. An inversion is a pair of numbers with the higher one coming first.

For instance, in numbers = [4, 2, 1, 3], there are four inversions: (4, 2), (4, 1), (4, 3), and (2, 1).

Problem 2: Using Merge Sort

Merge sort can efficiently solve this. It sorts an array in O(nlogn)O(n \log n) by splitting it, sorting the halves, and merging them. During this process, we can count the inversions.

When merging, a smaller number on the right than a number on the left reveals an inversion. And it's not just one: there are as many inversions as the remaining numbers on the left.

Problem 2: Building the Solution – Helper Function

We begin with a mergeAndCount helper function in TypeScript:

TypeScript
1function mergeAndCount(arr1: number[], arr2: number[]): [number[], number] { 2 let i = 0; 3 let j = 0; 4 const merged: number[] = []; 5 let inversions = 0; 6 7 while (i < arr1.length || j < arr2.length) { 8 if (i === arr1.length) { 9 merged.push(arr2[j]); // Add remaining right 10 j++; 11 } else if (j === arr2.length) { 12 merged.push(arr1[i]); // Add remaining left 13 i++; 14 } else if (arr1[i] <= arr2[j]) { 15 merged.push(arr1[i]); // Add smaller left 16 i++; 17 } else { 18 merged.push(arr2[j]); // Add smaller right 19 j++; 20 inversions += (arr1.length - i); // Count inversions 21 } 22 } 23 return [merged, inversions]; 24}

The mergeAndCount function merges two sorted arrays, arr1 and arr2, while counting inversions, which occur when an element from arr2 is smaller than the remaining elements in arr1. It returns the merged array and the inversion count.

Problem 2: Building the Solution – Main Function

Finally, we develop countInversions, aided by mergeAndCount:

TypeScript
1function countInversions(arr: number[]): [number[], number] { 2 if (arr.length === 1) return [arr, 0]; // Base case 3 4 const middle = Math.floor(arr.length / 2); 5 const [left, leftInversions] = countInversions(arr.slice(0, middle)); 6 const [right, rightInversions] = countInversions(arr.slice(middle)); 7 const [merged, mergeInversions] = mergeAndCount(left, right); 8 9 return [merged, leftInversions + rightInversions + mergeInversions]; // Total inversions 10} 11 12console.log(countInversions([4, 2, 1, 3])); // Prints [ [ 1, 2, 3, 4 ], 4 ]

The countInversions function recursively splits an array into halves, sorts them, and merges the sorted halves back while counting inversions using the mergeAndCount helper. It returns a tuple containing the sorted array and the total number of inversions, effectively combining the counting and sorting processes into one efficient operation.

Conclusion

We've dived into quick sort and merge sort, applying them to two exciting tasks, starting from simple solutions to more efficient ones and building them in TypeScript. Now it's your turn! By employing TypeScript's type safety features, you can ensure that your algorithms are not only efficient but reliable and maintainable as well. Enjoy writing clean, type-safe code with TypeScript!

Enjoy this lesson? Now it's time to practice with Cosmo!
Practice is how you turn knowledge into actual skills.