Sorting Algorithms: Mastering the Fundamentals in JavaScript

Nile BitsNile Bits
10 min read

An essential idea in software engineering and computer science is sorting algorithms. They are necessary to enable effective searching, retrieval, and data manipulation as well as meaningful data organization. Any developer that works with JavaScript—a language that is frequently used for web development—must understand sorting algorithms. With a particular emphasis on JavaScript implementation, this essay seeks to offer a thorough grasp of sorting algorithms.

Understanding Sorting Algorithms

Algorithms for sorting lists or arrays are processes that put the items in a specific order, usually lexicographical or numerical. Sorting algorithms come in a variety of forms, each having advantages and disadvantages. Selecting the appropriate algorithm for a given issue or dataset requires an understanding of these algorithms.

Why Sorting Algorithms Matter

Sorting is a common operation in programming. Whether you are managing databases, processing data for machine learning, or simply organizing a list of names, sorting algorithms come into play. Efficient sorting can save time and computational resources, making your applications faster and more responsive.

Categories of Sorting Algorithms

Sorting algorithms can be broadly classified into two categories:

  1. Comparison-based Sorting Algorithms: These algorithms determine the order of elements by comparing them. Examples include Bubble Sort, Selection Sort, Insertion Sort, Merge Sort, Quick Sort, and Heap Sort.

  2. Non-comparison-based Sorting Algorithms: These algorithms do not compare elements directly. Instead, they use other techniques to sort data. Examples include Counting Sort, Radix Sort, and Bucket Sort.

Common Sorting Algorithms in JavaScript

1. Bubble Sort

One of the most basic sorting algorithms is bubble sort. It runs over the list again and again, comparing next components and swapping them if they are out of order. Until the list is sorted, this procedure is repeated.

Implementation

function bubbleSort(arr) {
    let n = arr.length;
    for (let i = 0; i < n - 1; i++) {
        for (let j = 0; j < n - 1 - i; j++) {
            if (arr[j] > arr[j + 1]) {
                // Swap arr[j] and arr[j + 1]
                let temp = arr[j];
                arr[j] = arr[j + 1];
                arr[j + 1] = temp;
            }
        }
    }
    return arr;
}

let array = [64, 34, 25, 12, 22, 11, 90];
console.log(bubbleSort(array));

2. Selection Sort

Selection Sort divides the input list into two parts: the sorted part and the unsorted part. It repeatedly selects the smallest element from the unsorted part and moves it to the sorted part.

Implementation

function selectionSort(arr) {
    let n = arr.length;
    for (let i = 0; i < n - 1; i++) {
        let minIndex = i;
        for (let j = i + 1; j < n; j++) {
            if (arr[j] < arr[minIndex]) {
                minIndex = j;
            }
        }
        if (minIndex !== i) {
            // Swap arr[i] and arr[minIndex]
            let temp = arr[i];
            arr[i] = arr[minIndex];
            arr[minIndex] = temp;
        }
    }
    return arr;
}

let array2 = [64, 25, 12, 22, 11];
console.log(selectionSort(array2));

3. Insertion Sort

Insertion Sort builds the sorted array one item at a time. It picks the next element from the unsorted part and inserts it into its correct position in the sorted part.

Implementation

function insertionSort(arr) {
    let n = arr.length;
    for (let i = 1; i < n; i++) {
        let key = arr[i];
        let j = i - 1;
        while (j >= 0 && arr[j] > key) {
            arr[j + 1] = arr[j];
            j--;
        }
        arr[j + 1] = key;
    }
    return arr;
}

let array3 = [12, 11, 13, 5, 6];
console.log(insertionSort(array3));

4. Merge Sort

Merge Sort is a divide-and-conquer algorithm. It divides the array into halves, sorts each half recursively, and then merges the sorted halves.

Implementation

function mergeSort(arr) {
    if (arr.length <= 1) {
        return arr;
    }

    const mid = Math.floor(arr.length / 2);
    const left = arr.slice(0, mid);
    const right = arr.slice(mid);

    return merge(mergeSort(left), mergeSort(right));
}

function merge(left, right) {
    let result = [];
    let leftIndex = 0;
    let rightIndex = 0;

    while (leftIndex < left.length && rightIndex < right.length) {
        if (left[leftIndex] < right[rightIndex]) {
            result.push(left[leftIndex]);
            leftIndex++;
        } else {
            result.push(right[rightIndex]);
            rightIndex++;
        }
    }

    return result.concat(left.slice(leftIndex)).concat(right.slice(rightIndex));
}

let array4 = [38, 27, 43, 3, 9, 82, 10];
console.log(mergeSort(array4));

5. Quick Sort

Another divide-and-conquer algorithm is Quick Sort. The array is divided into two parts, with items fewer than the pivot on one side and elements bigger than the pivot on the other, when a pivot element is chosen. The halves are then sorted recursively.

Implementation

function quickSort(arr) {
    if (arr.length <= 1) {
        return arr;
    }

    const pivot = arr[arr.length - 1];
    const left = [];
    const right = [];

    for (let i = 0; i < arr.length - 1; i++) {
        if (arr[i] < pivot) {
            left.push(arr[i]);
        } else {
            right.push(arr[i]);
        }
    }

    return [...quickSort(left), pivot, ...quickSort(right)];
}

let array5 = [10, 7, 8, 9, 1, 5];
console.log(quickSort(array5));

6. Heap Sort

Heap Sort is a comparison-based algorithm that uses a binary heap data structure. It divides the array into a sorted and an unsorted region and iteratively shrinks the unsorted region by extracting the largest element and moving it to the sorted region.

Implementation

function heapSort(arr) {
    let n = arr.length;

    // Build heap (rearrange array)
    for (let i = Math.floor(n / 2) - 1; i >= 0; i--) {
        heapify(arr, n, i);
    }

    // One by one extract an element from heap
    for (let i = n - 1; i > 0; i--) {
        // Move current root to end
        let temp = arr[0];
        arr[0] = arr[i];
        arr[i] = temp;

        // Call max heapify on the reduced heap
        heapify(arr, i, 0);
    }

    return arr;
}

function heapify(arr, n, i) {
    let largest = i;
    let left = 2 * i + 1;
    let right = 2 * i + 2;

    if (left < n && arr[left] > arr[largest]) {
        largest = left;
    }

    if (right < n && arr[right] > arr[largest]) {
        largest = right;
    }

    if (largest !== i) {
        let swap = arr[i];
        arr[i] = arr[largest];
        arr[largest] = swap;

        heapify(arr, n, largest);
    }
}

let array6 = [12, 11, 13, 5, 6, 7];
console.log(heapSort(array6));

7. Counting Sort

Counting Sort is a non-comparison-based sorting algorithm. It works by counting the number of occurrences of each distinct element in the array and then using this information to place the elements in the correct position.

Implementation

function countingSort(arr, maxValue) {
    let count = new Array(maxValue + 1).fill(0);
    let sortedArray = new Array(arr.length);

    // Count the number of occurrences of each value
    for (let i = 0; i < arr.length; i++) {
        count[arr[i]]++;
    }

    // Modify count array such that each element at each index 
    // stores the sum of previous counts.
    for (let i = 1; i <= maxValue; i++) {
        count[i] += count[i - 1];
    }

    // Build the sorted array
    for (let i = arr.length - 1; i >= 0; i--) {
        sortedArray[count[arr[i]] - 1] = arr[i];
        count[arr[i]]--;
    }

    return sortedArray;
}

let array7 = [4, 2, 2, 8, 3, 3, 1];
console.log(countingSort(array7, Math.max(...array7)));

8. Radix Sort

Radix Sort is another non-comparison-based algorithm. It processes each digit of the numbers and sorts them by individual digits, starting from the least significant digit to the most significant digit.

Implementation

function radixSort(arr) {
    const max = Math.max(...arr);
    let digit = 1;

    while ((max / digit) >= 1) {
        arr = countingSortByDigit(arr, digit);
        digit *= 10;
    }

    return arr;
}

function countingSortByDigit(arr, digit) {
    let count = new Array(10).fill(0);
    let sortedArray = new Array(arr.length);

    // Count the occurrences of each digit
    for (let i = 0; i < arr.length; i++) {
        let digitIndex = Math.floor((arr[i] / digit) % 10);
        count[digitIndex]++;
    }

    // Transform count to position of each digit in sorted array
    for (let i = 1; i < 10; i++) {
        count[i] += count[i - 1];
    }

    // Build the sorted array
    for (let i = arr.length - 1; i >= 0; i--) {
        let digitIndex = Math.floor((arr[i] / digit) % 10);
        sortedArray[count[digitIndex] - 1] = arr[i];
        count[digitIndex]--;
    }

    return sortedArray;
}

let array8 = [170, 45, 75, 90, 802, 24, 2, 66];
console.log(radixSort(array8));

9. Bucket Sort

Bucket Sort works by distributing the elements of an array into a number of buckets. Each bucket is then sorted individually, either using a different sorting algorithm or by recursively applying the bucket sort.

Implementation

function bucketSort(arr, bucketSize = 5) {
    if (arr.length === 0) {
        return arr;
    }

    // Determine minimum and maximum values
    let minValue = Math.min(...arr);
    let maxValue = Math.max(...arr);

    // Initialize buckets
    let bucketCount = Math.floor((maxValue - minValue) / bucketSize) + 1;
    let buckets = new Array(bucketCount).fill().map(() => []);

    // Distribute input array values into buckets
    for (let i = 0; i < arr.length; i++) {
        let bucketIndex = Math.floor((arr[i] - minValue) / bucketSize);
        buckets[bucketIndex].push(arr[i]);
    }

    // Sort buckets and concatenate results
    arr = [];
    for (let i = 0; i < buckets.length; i++) {
        if (buckets[i].length > 0) {
            insertionSort(buckets[i]); // Using insertion sort to sort individual buckets
            arr = arr.concat(buckets[i]);
        }
    }

    return arr;
}

let array9 = [29, 25, 3, 49, 9, 37, 21, 43];
console.log(bucketSort(array9));

Performance Analysis of Sorting Algorithms

Understanding the performance characteristics of different sorting algorithms is crucial for selecting the right one for your specific use case. The performance of sorting algorithms is typically measured in terms of time complexity and space complexity.

Time Complexity

  • Bubble Sort: O(n^2)

  • Selection Sort: O(n^2)

  • Insertion Sort: O(n^2), but O(n) when the array is nearly sorted

  • Merge Sort: O(n log n)

  • Quick Sort: O(n log n) on average, O(n^2) in the worst case

  • Heap Sort: O(n log n)

  • Counting Sort: O(n + k), where k is the range of the input

  • Radix Sort: O(nk), where k is the number of digits in the largest number

  • Bucket Sort: O(n + k), wherek is the number of buckets

Space Complexity

  • BubbleSort: O(1)

  • Selection Sort: O(1)

  • Insertion Sort: O(1)

  • Merge Sort: O(n)

  • Quick Sort: O(logn) (due to recursion)

  • Heap Sort: O(1)

  • Counting Sort: O(k)

  • Radix Sort: O(n + k)

  • Bucket Sort: O(n + k)

Stability of Sorting Algorithms

A sorting algorithm is stable if it maintains the relative order of records with equal keys (i.e., values). Stability is important in certain applications where the original order of equal elements needs to be preserved.

  • Stable Algorithms: Bubble Sort, Insertion Sort, Merge Sort, Counting Sort, Radix Sort, Bucket Sort

  • Unstable Algorithms: Selection Sort, Quick Sort, Heap Sort

Choosing the Right Sorting Algorithm

The choice of sorting algorithm depends on several factors, including the size of the input array, the range of input values, the need for stability, and the expected distribution of values.

Small Arrays

For small arrays, simple algorithms like Insertion Sort and Bubble Sort are often efficient due to their low overhead. Although they have a time complexity of O(n^2), their performance can be competitive with more complex algorithms for small datasets.

Large Arrays

For larger arrays, algorithms with better time complexity such as Merge Sort, Quick Sort, and Heap Sort are more appropriate. Merge Sort and Heap Sort offer guaranteed O(n log n) performance, while Quick Sort is typically faster in practice due to smaller constant factors, despite its worst-case O(n^2) time complexity.

Nearly Sorted Arrays

For arrays that are already nearly sorted, Insertion Sort can be particularly efficient, with a best-case time complexity of O(n).

Arrays with a Small Range of Values

When the range of values is small compared to the number of elements, Counting Sort and Radix Sort can be very efficient. Counting Sort is particularly useful when the range of the input values (k) is not significantly larger than the number of elements (n).

Arrays Requiring Stability

When stability is a requirement, choose from stable algorithms such as Merge Sort, Bubble Sort, and Insertion Sort. Radix Sort and Counting Sort are also stable and efficient for suitable datasets.

Conclusion

Gaining an understanding of sorting algorithms is crucial for every JavaScript developer. An extensive review of several sorting algorithms, their JavaScript implementations, and their performance attributes has been given in this article. Knowing the advantages and disadvantages of each algorithm will help you choose the best sorting method for the various situations.

Sorting algorithms are not just theoretical constructs; they have practical applications in numerous fields, from data analysis and machine learning to web development and database management. By practicing these algorithms and implementing them in real-world projects, you can deepen your understanding and improve your problem-solving skills as a developer.

Remember, the key to mastering sorting algorithms lies in practice and experimentation. Try implementing these algorithms on your own, experiment with different datasets, and analyze their performance. With time and practice, you will gain a deeper appreciation for the elegance and efficiency of sorting algorithms in JavaScript.

0
Subscribe to my newsletter

Read articles from Nile Bits directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Nile Bits
Nile Bits

Nile Bits is a software company, focusing on outsourcing software development and custom software solutions. Our outsourcing software services and solutions are designed with a focus on secure, scalable, expandable and reliable business systems. Via our low cost, high quality and reliable outsourcing software services, we provide to our clients value for money and therefore client satisfaction.