前端常用算法 - 排序

153 阅读6分钟

排序

排序方法平均时间复杂度最坏时间复杂度最好时间复杂度空间复杂度是否为稳定排序
冒泡排序O(n²)O(n²)O(n)O(1)
快速排序O(nlogn)O(n²)O(logn)O(logn)
选择排序O(n²)O(n²)O(n²)O(1)
插入排序O(n²)O(n²)O(n)O(1)
希尔排序O(nlogn)O(n²)O(n)O(1)
归并排序O(nlogn)O(nlogn)O(nlogn)O(n)
堆排序O(nlogn)O(nlogn)O(nlogn)O(1)
基数排序O(nk)O(nk)O(nk)O(n)

如果数组中存在两个相等的数字,排序过程中 这两个数字的先后顺序如果不会发生变化 就叫做稳定的排序反之叫做不稳定

冒泡排序

遍历数组,依次比较两个相邻的元素,如果顺序错误(不符合我们需要的顺序),就把他们交换过来,直到没有可交换的元素为止。
每一趟只能确定将一个数归位。
排序过程图解

冒泡排序.gif

  • 【简单的未经过优化的代码】
function bubbleSort(arr) {
    for (let i = 0; i < arr.length - 1; i++) {
        // 冒泡排序每一趟遍历都会将一个数归位
        for (let j = 0; j < arr.length - 1 - i; j++) {
            if (arr[j] > arr[j + 1]) {
                [arr[j], arr[j + 1]] = [arr[j + 1], arr[j]];
            }
        }
    }
    return arr;
}
  • 【第一步优化:外层循环优化】设置flag变量,记录当前循环的过程中是否有变量发生了交换,若没有发生交换,则说明数组已经有序,就不需要继续执行。
function bubbleSort(arr) {
    for (let i = 0; i < arr.length - 1; i++) {
        let flag = false;
        // 冒泡排序每一趟遍历都会将一个数归位
        for (let j = 0; j < arr.length - 1 - i; j++) {
            if (arr[j] > arr[j + 1]) {
                [arr[j], arr[j + 1]] = [arr[j + 1], arr[j]];
                flag = true;
            }
        }
        if (!flag) {
            return arr;
        }
    }
    return arr;
}
  • 【第二步优化:内层循环优化】记录当前循环最后一次元素交换的位置,该位置以后的序列都是已经有序的序列,在下一轮中无需继续进行参与比较。
function bubbleSort(arr) {
    // 初始状态设置最后一个交换元素的元素为数组的最后一个元素
    let lastIndex = arr.length - 1;
    while (lastIndex) {
        let flag = false, pos = lastIndex;
        for (let j = 0; j < pos; j++) {
            if (arr[j] > arr[j + 1]) {
                [arr[j], arr[j + 1]] = [arr[j + 1], arr[j]];
                flag = true;
                lastIndex = j; // 记录该循环最后一个交换的元素 => j+1位置的元素有序
            }
        }
        if (!flag) {
            return arr;
        }
    }
    return arr;
}

优化后的冒泡排序,当排序数组为需要顺序的有序数组时,为最好的时间复杂度为 O(n)。
冒泡排序的平均时间复杂度为 O(n²) ,最坏时间复杂度为 O(n²) ,空间复杂度为 O(1) ,是稳定排序

快速排序

通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。 详细参考图解 排序过程图解

quickSort.gif

function partition(arr, left ,right) {
    let pivot = arr[left];
    while (left < right) {
        // 必须从右侧开始移动
        while (arr[right] >= pivot && left < right) {
            right--;
        }
        arr[left] = arr[right];
        while (arr[left] < pivot && left < right) {
            left++;
        }
        arr[right] = arr[left];
        // [arr[left], arr[right]] = [arr[right], arr[left]];
    }
    // 退出循环时 left === right
    arr[left] = pivot;
    return left;
}

function quickSort(arr, left, right) {
    if (left > right || arr.length < 2) {
        return arr;
    }
    let index = partition(arr, left, right);
    quickSort(arr, left, index - 1);
    quickSort(arr, index + 1, right);
    return arr;
}

快速排序的平均时间复杂度为 O(nlogn) ,最坏时间复杂度为 O(n²) ,最好时间复杂度为 O(logn),空间复杂度为 O(logn) ,不是稳定排序

选择排序

每一趟从待排序的数据元素中选择最小(或最大)的一个元素作为首元素,直到所有元素排完为止。
不断地比较交换元素耗时较多,可设置变量存储最小(或最大)元素的下标,一次遍历结束后再进行元素交换。 排序过程图解

插入排序.gif

function selectionSort(arr) {
    for (let i = 0; i < arr.length; i++) {
        let minIndex = i;
        for (let j = i + 1; j < arr.length; j++) {
            if (arr[minIndex] > arr[j]) {
                minIndex = j;
            }
        }
        [arr[i], arr[minIndex]] = [arr[minIndex], arr[i]]
    }
    return arr;
}

选择排序不管数组是否有序,时间复杂度都为 O(n²)。
选择排序的平均时间复杂度为 O(n²) ,最坏时间复杂度为 O(n²) ,空间复杂度为 O(1) ,不是稳定排序

插入排序

每一步将一个待排序的记录,插入到前面已经排好序的有序序列中去,直到插完所有元素为止。
排序过程图解

插入排序.gif

function insertionSort(arr) {
    const len = arr.length;
    let preIndex, current;
    for (let i = 1; i < len; i++) {
        preIndex = i - 1;
        current = arr[i];
        // 将比当前要插入元素大的元素依次后移
        while(preIndex >= 0 && arr[preIndex] > current) {
            arr[preIndex + 1] = arr[preIndex];
            preIndex--;
        }
        arr[preIndex + 1] = current;
    }
    return arr;
}

当排序序列为已排序序列时,为最好的时间复杂度 O(n)。插入排序的平均时间复杂度为 O(n²) ,最坏时间复杂度为 O(n²) ,空间复杂度为 O(1) ,是稳定排序

希尔排序

先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,待整个序列中的记录"基本有序"时,再对全体记录进行依次直接插入排序。
排序过程图示

希尔排序.gif

function shellSort(arr) {
    const len = arr.length;
    for (let gap = len >> 1; gap >= 1; gap = gap >> 1) {
        // 对每个分组使用插入排序
        for (let i = gap; i < len; i++) {
            let current = arr[i];
            let preIndex = i - gap;
            while (preIndex >= 0 && arr[preIndex] > current) {
                arr[preIndex + gap] = arr[preIndex];
                preIndex -= gap;
            }
            arr[preIndex + gap] = current;
        }
    }
    return arr;
}

希尔排序的平均时间复杂度为 O(nlogn) ,最坏时间复杂度为 O(n²) ,空间复杂度为 O(1) ,不是稳定排序

归并排序

排序过程图示

归并排序.gif

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) {
    const answer = [];
    while (left.length && right.length) {
        if (left[0] < right[0]) {
            answer.push(left.shift());
        } else {
            answer.push(right.shift());
        }
    }
    while (left.length) {
        answer.push(left.shift());
    }
    while (right.length) {
        answer.push(right.shift());
    }
    return answer;
}

归并排序的时间复杂度不管在什么情况下都为O(nlogn),故归并排序平均时间复杂度为 O(nlogn) ,最坏时间复杂度为 O(nlogn) ,空间复杂度为 O(n) ,是稳定排序

堆排序

堆排序是指利用堆这种数据结构所设计的一种排序算法。
堆:左右节点的值总是小于(或者大于)它的父节点。
排序过程图示

  • 创建一个堆(以大根堆为例)
  • 把堆首(最大值)和堆尾互换
  • 把堆的尺寸缩小 1,并重新构建堆;
  • 重复步骤 2,直到堆的尺寸为 1

堆排序.gif

// 从第i个节点向下检查建立大根堆
function buildHeap(arr, i, length) {
    let temp = arr[i]; // 当前父节点
    for (let j = 2 * i + 1; j < length; j = 2 * j + 1) {
        temp = arr[i];
        // 若右节点存在,j指向两个子节点中较大的值
        if (j + 1 < length && arr[j] < arr[j + 1]) {
            j++;
        }
        if (temp < arr[j]) {
            // 父节点比子节点小, 交换父子节点, 并保存节点编号
            [arr[i], arr[j]] = [arr[j], arr[i]];
            i = j;
        } else {
            break;
        }
    }
}

// 堆排序
function heapSort(arr) {
    // 初始化大顶堆,从第一个非叶子结点开始
    for (let i = Math.floor(arr.length / 2 - 1); i >= 0; i--) {
        buildHeap(arr, i, arr.length);
    }
    // 排序,每一次for循环找出一个当前最大值,数组长度减一
    for (let i = Math.floor(arr.length - 1); i > 0; i--) {
        [arr[i], arr[0]] = [arr[0], arr[i]]; // 根节点与最后一个节点交换
        buildHeap(arr, 0, i); 
    }
    return arr;
}

堆排序的时间复杂度在不管什么情况下都是 O(nlogn),空间复杂度为 O(1) ,不是稳定排序

基数排序

将整数按位数切割成不同的数字,然后按每个位数分别比较。过程:将所有待比较数值(正整数)统一为同样的数位长度,数位较短的数前面补零。然后,从最低位开始,依次进行一次排序。这样从最低位排序一直到最高位排序完成以后,数列就变成一个有序序列。
排序过程图示

基数排序.gif

function radixSort(arr) {
    // 找到数组中最大的数并获取其对应的长度
    let maxValue = Math.max(...arr);
    let maxLen = (maxValue + '').length;

    // 初始化桶
    let bucket = new Array(10).fill(0).map(() => []);
    // 最大的数有几位就需要循环几次
    for (let i = 0; i < maxLen; i++) {
        // 按照当前位的值将数加入不同的bucket中
        for (let j = 0; j < arr.length; j++) {
            let cur = arr[j] + '';
            if (cur.length < i + 1) {
                bucket[0].push(arr[j]);
            } else {
                bucket[cur[cur.length - 1 - i] - '0'].push(arr[j]);
            }
        }
        arr = [];
        // 清空桶中的数据,得到一次排序的结果arr
        for (let i = 0; i < 10; i++) {
            for (let j = 0; j < bucket[i].length; j++) {
                arr.push(bucket[i][j]);
            }
            bucket[i] = [];
        }
    }
    return arr;
}

基数排序的平均时间复杂度为 O(nk),k 为最大元素的长度,最坏时间复杂度为 O(nk),空间复杂度为 O(n) ,是稳定排序