【面试】高频题七种排序算法js实现

255 阅读3分钟

面试当中,排序算法经常出现,遇到过的有冒泡排序,快速排序,快排的出现频率是非常高的,但是现场写代码的话很容易出错,今天再把常见排序算法实现总结一下,分别是以下七种:

  1. 冒泡排序
  2. 选择排序
  3. 插入排序
  4. 希尔排序:插入排序的进化版本
  5. 归并排序:递归
  6. 快速排序: 面试高频题
  7. 堆排序:求最大的k个数的算法

先脑图总结一波:

7大排序.png

冒泡🫧排序

需要n-1趟排序,每次将一个最大或者最小的元素冒泡到最后。

function bubbleSort(arr) {
    const len = arr.length;
    // 遍历n-1次
    for (let i = 0; i < len - 1; i++) {
        // 比较范围是0-j,j-n是已经排序好的数据
        for (let j = 0; j < len - i - 1; j++) {
            // 从0开始的话和后面的元素比较
            if (arr[j] > arr[j + 1]) {
                [arr[j], arr[j + 1]] = [arr[j + 1], arr[j]];
            }
        }
    }
    return arr;
}

arr = [3,44,38,5,47,15,36,26,27,2,46,4,19,50,48];
bubbleSort(arr);

选择排序

n-1趟排序,每趟从无序区中选择最小的记录,和有序区最后一个i做交换。

function selectionSort(arr) {
    const len = arr.length;
    for (let i = 0; i < len - 1; i++) {
        let min = arr[i];
        let minIndex = i;
        for (let j = i + 1; j < len; j++) {
            if (arr[j] < min) {
                min = arr[j];
                minIndex = j;
            }
        }
        [arr[i], arr[minIndex]] = [arr[minIndex], arr[i]];
    }
    return arr;
}
arr = [3,44,38,5,47,15,36,26,27,2,46,4,19,50,48];
selectionSort(arr);

插入排序

假设第一个元素是排好序的,然后后面每个元素不断往前插入,像摆扑克牌一样, 所以要找到当前元素在前面已排序数组中的位置,然后交换过去。

function insertSort(arr) {
    for (let i = 1; i < arr.length; i++) {
        let tmp = arr[i];
        let j = i - 1;
        while (j >= 0 && arr[j] > tmp) {
            arr[j + 1] = arr[j];
            j--;
        }
        arr[j + 1] = tmp;
    }
    return arr;
}
arr = [3,44,38,5,47,15,36,26,27,2,46,4,19,50,48];
insertSort(arr);

希尔排序

插入排序的进化版,将元素分为若干个子序列分别进行直接插入排序,gap为一个序列,不断降低为1,为1后即为排完序的序列。gap的选择,一般选择n/2,不断除以2,直到1。

function shellSort(arr) {
    let len = arr.length;
    let gap = Math.floor(len / 2);
    for (gap; gap > 0; gap = Math.floor(gap / 2)) {
        for (let i = gap; i < len; i++) {
            let temp = arr[i];
            let j;
            for (j = i - gap; j >= 0 && arr[j] > temp; j -= gap) {
                arr[j + gap] = arr[j];
            }
            arr[j + gap] = temp;
        }
    }
    return arr;
}

arr = [3,44,38,5,47,15,36,26,27,2,46,4,19,50,48];
shellSort(arr);

归并排序

把长度为n的输入序列非为2个长度为n/2的子序列,对两个子序列分别采用归并排序,将两个排序好的子序列合并为最终的排序序列。

function mergeSort(arr) {
    const len = arr.length;
    if (len < 2) {
        return arr;
    }
    const middle = Math.floor(len / 2);
    return merge(mergeSort(arr.slice(0, middle)), mergeSort(arr.slice(middle)))
}

function merge(left, right) {
    let result = [];
    let i = 0;
    let j = 0;
    while (i < left.length && j < right.length) {
        if (left[i] < right[j]) {
            result.push(left[i++]);
        } else {
            result.push(right[j++]);
        }
    }
    while (i < left.length) {
        result.push(left[i++]);
    }
    while (j < right.length) {
        result.push(right[j++]);
    }
    return result;
}

arr = [3,44,38,5,47,15,36,26,27,2,46,4,19,50,48];
mergeSort(arr);

快速排序

思路

对快速排序又恨又爱,因为面试很容易遇到,但是如果要在面试中写出一个没错的代码,还得花点时间,很容易在边界上出问题。思路的话容易理解,选择枢纽元,没趟将小于枢纽元的放在左边,大于枢纽元的放在右边。然后不断递归。如果用双指针的写法话,容易出错的点有:

  1. 枢纽元的选取
    • 第一个元素:最简单,要是面试就这样写吧,简单不易错。
    • 随机选择的话,注意生成随机index的方法,要注意把left加回去 Math.floor(Math.random() * (right - left + 1)) + left
    • 三数中值法: 是算法导论上推荐的写法,left,middle、right,排好序,middle放到left+1的位置。图解排序算法之快速排序—三数取中法
  2. 选择完枢纽元后,放到left位置,i从left+1,j从right开始。找大于枢纽元的位置和小于枢纽元的位置时,left可以等于right,这样最后i指向的是比枢纽元大的位置,j指向的是比枢纽元小的元素的位置。因为枢纽元放在了最左边,所以一趟处理完后要和j进行交换。就是说放到left位置的枢纽元和j最后的位置交换。
  3. 递归子数组的范围处理好,要按照同样的规则处理,要是都是左闭右开都左闭右开,要是都左闭右闭,都左闭右闭,保持统一即可。
function quickSort(arr, left, right) {
    if (right <= left) {
        return arr;
    }
    // 随机选择枢纽元:易错点
    const pivotIndex = Math.floor(Math.random() * (right - left + 1)) + left;
    const pivot = arr[pivotIndex];
    // 交换枢纽元
    [arr[left], arr[pivotIndex]] = [arr[pivotIndex], arr[left]];
    let i = left + 1;
    let j = right;
    while (i < j) {
        // i最后指向比它大的元素
        while (i <= j && arr[i] < pivot) {
            i++;
        }
        // j指向比它小的元素,因为枢纽元在第一位,所以要和比它小的元素替换
        while (j >= i && arr[j] > pivot) {
            j--;
        }
        if (i < j) {
            [arr[i], arr[j]] = [arr[j], arr[i]];
        }
    }
    if (arr[j] < pivot) {
        [arr[j], arr[left]] = [arr[left], arr[j]];
    }
    quickSort(arr, 0, j - 1);
    quickSort(arr, j + 1, right);
    return arr;
}
arr = [3,44,38,5,47,15,36,26,27,2,46,4,19,50,48];
quickSort(arr, 0, arr.length - 1);

另一种写法

如果说以上按照索引的写法容易出错,那另一种避开索引的写法就容易多了,就是不传入left和right,写的时候直接将小于枢纽元的数组收集成left,大于枢纽元的数组收集成right,然后对left和right分别就行快速排序,但是要记住的是要返回排序后的数组。

function quickSort2(arr) {
    if (arr.length < 2) {
        return arr;
    }
    const pivot = arr[0];
    const left = [];
    const right = [];
    for (let i = 1; i < arr.length; i++) {
        if (arr[i] < pivot) {
            right.push(arr[i]);
        } else {
            left.push(arr[i]);
        }
    }
    return [...quickSort2(right), pivot, ...quickSort2(left)];
}

arr = [3,44,38,5,47,15,36,26,27,2,46,4,19,50,48];
quickSort2(arr);

堆排序

  1. 建大顶堆
  2. 不断交换堆顶元素和最后一个元素
  3. 交换后将数组重新调整为大顶堆。
function heapSort(arr) {
    buildHeap(arr);
    for (let j = arr.length - 1; j > 0; j--) {
        swap(arr, 0, j);
        // 这里需要调整的堆的大小要减小,所以heapify需要加上size参数,否则范围不对
        heapify(arr, 0, j);
    }
    return arr;
}

function buildHeap(arr) {
    const index = Math.floor(arr.length / 2) - 1;
    // 从第一个非叶子节点开始调整堆
    for (let i = index; i >= 0; i--) {
        heapify(arr, i, arr.length);
    }
    return arr;
}

// 调整以index为根节点的子树为大顶堆
function heapify(arr, index, size) {
    const len = size;
    let maxIndex = index;
    const leftChild = maxIndex * 2 + 1;
    const rightChild = leftChild + 1;
    if (leftChild < len && arr[leftChild] > arr[index]) {
        maxIndex = leftChild;
    }
    if (rightChild < len && arr[rightChild] > arr[maxIndex]) {
        maxIndex = rightChild;
    }
    if (index !== maxIndex) {
        swap(arr, index, maxIndex);
        heapify(arr, maxIndex, size);
    }
}

var arr=[91,60,96,13,35,65,46,65,10,30,20,31,77,81,22];
heapSort(arr);

总结

终于写完了,完结撒花,其他的还有计数排序、桶排序、基数排序,等以后有时间再研究吧,这几个问到的很少😄。

参考文章:

十大经典排序算法总结(JavaScript描述): 很棒的文章,算法的图过程很生动。