常见的十大排序算法详解

常见的十大排序算法详解

性能对比

在这里插入图片描述

名词解释

  • n: 数据规模
  • k:“桶”的个数
  • In-place: 占用常数内存,不占用额外内存
  • Out-place: 占用额外内存
  • 稳定性:排序后2个相等键值的顺序和排序之前它们的顺序相同

测试数组:arr: [2, 48, 4, 19, 27, 5, 14, 36, 38, 44, 3, 46, 47, 50, 26]

冒泡排序

算法思想:

  1. 从数组头部开始,不断比较相邻2个元素的大小,将较大的往后移,直到数组末尾。经过一轮比较即可找到最大元素,且将之放在了数组末尾。
  2. 第二轮比较仍从数组头部开始,直到数组倒数第二个元素,找打第二大的元素,且将其放在数组倒数第二位置。
  3. 依此类推,进行n-1次递归,即可确认所有元素的大小位置。

在这里插入图片描述

//冒泡排序
export const bubbleSort = (arr) => {
    let length = arr.length;
    for (let i = 0; i < length; i++) {//第一轮轮询
        let flag = false;       //设置个标识,用于如果数组已经排序成功则,不需要在轮询优化
        for (let j = 0; j < length - i - 1; j++) {//之后每次轮询长度-1
            if (arr[j] > arr[j + 1]) {//比较相邻2个元素,将较大的元素向后移(相邻元素位置交换)
                let temp = arr[j];
                arr[j] = arr[j + 1];
                arr[j + 1] = temp;
                flag = true;        //如果还存在大小比较则继续轮询
            }
        }
        if (!flag) { //如果本轮不存在大小互换则表示已经排序成功,不需要再比较了
            break;
        }
    }
    return arr;
}

冒泡排序在数组已经排序完成后依旧会继续轮询,比较相邻元素大小,直到n*(n-1)次轮询全部执行,为了避免不必要的浪费,上面代码添加了flag,区分数组是否排序完成,排序完成则不继续遍历。

选择排序

算法思想:

  1. 从数组头部开始,假设第一个元素即为最小元素,不断与最小元素比较,如果当前元素比之前的元素更小则记录最小元素下标,直到数组末尾。此时已经找到了最小元素的下标,将其与第一个元素调换位置即可。
  2. 第二轮比较,从数组的第二个元素开始,直到数组末尾,找到第二小的元素下标,和数组第二位置元素互换位置。
  3. 依此类推,进行n-1次递归,即可确认所有元素的大小位置。

在这里插入图片描述

//选择排序排序
export const selectionSort = (arr) => {
    let length = arr.length;
    for (let i = 0; i < length; i++) {//第一轮轮询
        let minIndex = i;           //设置最小元素下标为当前元素下标
        for (let j = i + 1; j < length; j++) {//将当前元素和之后所有元素进行比较
            if (arr[j] < arr[minIndex]) {//比较相邻2个元素,将较大的元素向后移(相邻元素位置交换)
                minIndex = j;
            }
        }

        //找到最小元素下标后,将其和当前遍历的第一个元素位置互换
        let temp = arr[i];
        arr[i] = arr[minIndex];
        arr[minIndex] = temp;
    }
    return arr;
}

选择排序不稳定,如果存在相同的值的情况下就存在该问题,例如:[6,6,2],第一次排序就将第一个6和2互换了位置,即第一个6在第二个6的后面。

选择排序复杂度较高,无法像冒泡排序样优化,因为每次都遍历了剩余数组,所有可以同时寻找最大和最小值,这样即可节约遍历次数。

优化后算法

//选择排序排序
export const selectionSort = (arr) => {
    let length = arr.length;
    let maxPos = length - 1;          //最大元素数组位置
    for (let i = 0; i < length; i++) {//第一轮轮询
        let minIndex = i;           //设置最小元素下标为当前元素下标
        let maxIndex = maxPos;           //设置最大元素下标为当前元素下标
        for (let j = i + 1; j < maxPos; j++) {//将当前元素和之后所有元素进行比较
            if (arr[j] < arr[minIndex]) {//比较相邻2个元素,将较大的元素向后移(相邻元素位置交换)
                minIndex = j;
            }

            if (arr[j] > arr[maxIndex]) {
                maxIndex = j;
            }
        }

        //找到最小元素下标后,将其和当前遍历的第一个元素位置互换
        let minTemp = arr[i];
        arr[i] = arr[minIndex];
        arr[minIndex] = minTemp;

        //找到最小元素下标后,将其和当前遍历的最后一个元素位置互换
        let maxTemp = arr[maxPos];
        arr[maxPos] = arr[maxIndex];
        arr[maxIndex] = maxTemp;
        maxPos--;

        if (minIndex >= maxPos) {//如果最小元素下标和最大元素下标重合,则表示排序完成
            break;
        }
    }
    return arr;
}

类似对半查找算法,将数组分为了2部分,最小元素和最大元素依次向中靠拢,当下标交叉时即表示排序完成。

插入排序

算法思想:

  1. 假设数组第一个元素已经排好序了。
  2. 从数组第二个元素开始,与他前一个元素进行比较,假设当前元素要插入的位置为前一个元素的下标,比较插入位置的元素和当前元素的值,如果小于插入位置的元素,则将插入位置的元素向后移动一位。
  3. 在第2步比较的插入位置,继续向前一个元素比较,如果当前元素的值依旧小,则继续移动插入位置的元素后移一位。
  4. 依次内推,直到找到排序元素的插入位置的前一个元素下标位置为止。
  5. 将排序元素赋值给插入位置即可。

在这里插入图片描述

//插入排序
export const insertSort = (arr) => {
    let length = arr.length;
    for (let i = 1; i < length; i++) {
        let prevIndex = i - 1;    //当前元素的前一个元素下标
        let current = arr[i];   //当前元素值
        while (prevIndex >= 0 && arr[prevIndex] > current) {//如果下标存在,且前一个元素大于当前元素
            arr[prevIndex + 1] = arr[prevIndex];        //将大于当前元素的值,依次向后移动一位
            prevIndex--;                                //继续跟前一个元素比较
        }
        //此时已经获取了当前元素应该插入位置的前一个元素下标,所以prevIndex + 1就是当前元素的插入下标
        arr[prevIndex + 1] = current;                   
    }
    return arr;
}

优化方式和选择排序类似,对半插入,将数组分为最小值和最大值同时插入,可节约一半时间消耗。

希尔排序

希尔排序又叫缩小增量排序,是简单插入排序的改进版。简单插入排序是依次和前一个元素比较,至少移动和比较均需n-1次,希尔排序采用分组跳跃式比较,逐步缩小增量大小,直至增量为1。

算法思想:

1.将数据按一定的增量分组,在组内进行插入排序。

2.缩小增量大小,继续进行组内插入排序。

3.直到增量为1,则进入最后一次的比较。

在这里插入图片描述

//希尔排序
export const shellSort = (arr) => {
    let length = arr.length;
    let gap = 0;    //增量大小
    for (gap = length / 3; gap > 0; gap = Math.floor(gap / 2)) {//每次遍历增量减半
        //内部进行插入排序
        for (let i = gap; i < length; i++) {//跨度增量进行比较
            let temp = arr[i];
            let j = i - gap;        //跨度坐标
            for (j; j > 0 && arr[j] > temp; j -= gap) {//相同跨度的元素,进行值交换
                arr[j + gap] = arr[j]
            }
            arr[j + gap] = temp;    //非相同跨度的元素值还原
        }
    }
    return arr;
}

需注意:增量只能为整数,最小增量为1

因为是跳跃式比较,所以不稳定。

归并排序

算法思想

  1. 先将排序数组从中间一分为2,拆成2个数组(L1、R1)。

  2. 继续对拆分的数据进行拆分,L1拆分为L11、L12两数组,R1拆分为R11、R12两数组。

  3. 直到将排序数组拆分为每个数组只有一个元素为止。(这个过程称之为递归有序

  4. 将拆分后的有序数组依次排序,然后再合并回来,例如:将L11排序、L12排序,然后合并为L1。

  5. 新定义一个数组作为排序数组用,从递归后的数组依次排序合并,直到合并L1、R1为止。

    合并的原理是左边数组的第一个元素和右边数组的第一个比较,比较成功后则去掉成功元素,继续比较第一个元素,直到某个数组元素为空,再将另一个数组的剩余元素添加到最后即可。

在这里插入图片描述

//归并排序
export const mergeSort = (arr) => {
    let length = arr.length;
    if (length < 2) {
        return arr;
    }
    let midIndex = Math.floor(length / 2);
    let leftArr = arr.slice(0, midIndex);
    let rightArr = arr.slice(midIndex);
    return merge(mergeSort(leftArr), mergeSort(rightArr));
}

const merge = (leftArr, rightArr) => {
    let sortArr = [];
    while (leftArr.length && rightArr.length) {
        //每次只需比较两个数组的第一个元素即可
        if (leftArr[0] <= rightArr[0]) {
            sortArr.push(leftArr.shift());
        } else {
            sortArr.push(rightArr.shift());
        }
    }

    //如果比较完成后左侧仍剩余数据则直接添加到数组末尾
    while (leftArr.length) {
        sortArr.push(leftArr.shift());
    }

    //如果比较完成后右侧仍剩余数据则直接添加到数组末尾
    while (rightArr.length) {
        sortArr.push(rightArr.shift());
    }

    return sortArr;
}

快速排序

算法思想

  1. 从数列中随机选择一个数值作为基数。(一般以数组第一个元素为基数)
  2. 先从右向左找,将小于基数的值放在基数左边,找到后再从左往右找将大于基数的值放在基数右边。
  3. 当第一遍循环完成后数据以基数为分界线,左边都是小于基数的,右边都是大于基数的。
  4. 重置基数,对基数左侧和右侧分别递归上诉步骤,直到排序完成。

在这里插入图片描述

/**
 * 快速排序
 * arr:排序数组
 * startIndex:数组起始位置
 * endIndex:数组结束位置
 */
export const quickSort = (arr, startIndex, endIndex) => {
    if (startIndex < endIndex) {
        let i = startIndex;
        let j = endIndex;
        let base = arr[i];  //基数
        while (i < j) {
            //从右向左找第一个小于基数的数
            while (i < j && arr[j] > base) {
                j--;
            }
            if (i < j) {//找到第一个小于基数的数,则调换他们的位置
                arr[i] = arr[j];
                i++;
            }
            //从左往右找,第一个大于基数的值
            while (i < j && arr[i] < base) {
                i++;
            }
            if (i < j) {//找到第一个大于基数的数,则调换他们的位置
                arr[j] = arr[i];
                j--;
            }
        }

        //当第一轮遍历完成后,将基数值放在当前查找的重叠坐标
        arr[i] = base;
        //对以基数坐标切分的左右半边分别递归排序,直到完成排序
        quickSort(arr, startIndex, i - 1);
        quickSort(arr, i + 1, endIndex);
    }
}

堆排序

  • 堆:一种类似完全二叉树数据结构。
  • 大顶堆:每个节点的值都大于或等于其子节点的值,在堆排序算法中用于升序排列;
  • 小顶堆:每个节点的值都小于或等于其子节点的值,在堆排序算法中用于降序排列;
  • 完全二叉树: 除了最后一层之外的其他每一层都被完全填充,并且所有结点都保持向左对齐

算法思想

  1. 将排序数组组成一个堆结构。
  2. 将堆结构转化为一个大顶堆,根据大顶堆特性,当前堆顶元素即为最大元素。
  3. 将堆顶元素和最后一个元素互换,然后重新对剩余节点构建大顶堆。
  4. 重复第3步骤,每次大顶堆的构建都会获得当前节点的最大值,这样既可会的一个正向排序的数组。
    在这里插入图片描述
//数组中2元素交换位置
const swap = (arr, i, j) => {
    let temp = arr[i];
    arr[i] = arr[j];
    arr[j] = temp;
}

//堆排序
export const heapSort = (arr) => {
    //第一次大堆顶处理后可以拿到数组最大值
    let size = arr.length;
    buildMapHeap(arr, size);
    for (let i = size - 1; i >= 0; i--) {
        //将最大元素放到数组末尾
        swap(arr, 0, i);
        //数组元素减少最后一个
        size--;
        //继续对剩余元素进行大堆顶排序
        heapify(arr, 0, size);
    }

    return arr;
}

//构建大顶堆
const buildMapHeap = (arr, size) => {
    let lastHeapIndex = Math.floor(size / 2); //最后一个节点的父元素下标
    for (let i = lastHeapIndex; i >= 0; i--) {
        heapify(arr, i, size);
    }
}

//跳转堆为大顶堆
const heapify = (arr, index, size) => {
    let left = 2 * index + 1;   //最后叶子节点的左节点坐标
    let right = 2 * index + 2;  //最后叶子节点的右节点坐标
    let lastParentIndex = index;    //最后叶子节点的父节点坐标

    //如果左叶子节点大于父节点,变更父节点坐标
    if (left < size && arr[left] > arr[lastParentIndex]) {
        lastParentIndex = left;
    }

    //如果右叶子节点大于父节点,变更父节点坐标
    if (right < size && arr[right] > arr[lastParentIndex]) {
        lastParentIndex = right;
    }

    //如果当前节点坐标变动过,则交换元素位置,将最大值放在堆顶
    if (lastParentIndex != index) {
        swap(arr, index, lastParentIndex);
        //对剩余元素进行大堆顶处理
        heapify(arr, lastParentIndex, size);
    }
}

计数排序

算法思想

  1. 查找数组的最大值和最小值,并申请max-min+1的额外数组空间(Array[max-min+1])。

  2. 将待排序集合记录到申请的额外数组中,记录坐标:index=value-min,并统计每个值的出现次数。

    例如本例中max=50,min=2,所以会申请一个长度为49的数组

    出现次数111111111111111
    新下标04621725312343642144454824
    数据值24841927514363844346475026
  3. 将额外数组依次展开,即可得到排序后的数组。

在这里插入图片描述

//计数排序
export const countingSort = (arr) => {
    let max = Math.max.apply(null, arr);
    let min = Math.min.apply(null, arr);
    let countArr = new Array(max - min + 1);
    let arrLen = arr.length;
    let countLen = max - min + 1;
    let resultArr = [];

    //统计每个元素出现个数
    for (let i = 0; i < arrLen; i++) {
        let countNum = countArr[arr[i] - min] || 0;
        countArr[arr[i] - min] = countNum + 1;
    }

    //将计数项逐项相加,为了让重复元素可以遍历读取
    let sum = 0
    for (let j = 0; j < countLen; j++) {
        let countValue = countArr[j] || 0;
        sum = sum + countValue;
        countArr[j] = sum;
    }

    //展开计数数组
    for (let n = arrLen - 1; n >= 0; n--) {
        //计数下标
        let countIndex = arr[n] - min;
        //根据存在值的计数数组移至新的结果数组
        resultArr[countArr[countIndex] - 1] = arr[n];
        //因为方便重复数据展开做了相加操作,所以每拿到一个结果,统计个数应减少一个
        countArr[countIndex]--;
    }

    return resultArr;
}

桶排序

算法思想

  1. 查找数组的最大值和最小值。

  2. 设定间隔大小,将桶分区管理,并确定应该申请的桶数。

    桶数:max/bucketSize - min/bucketSize+1。

  3. 遍历数组将,所有元素放入分区桶中。

  4. 对每个桶进行排序。

  5. 将分区桶合并还原为排序数组。
    在这里插入图片描述

本例将数据分为5个桶排序合并。

//桶排序
export const bucketSort = (arr) => {
    let max = Math.max.apply(null, arr);
    let min = Math.min.apply(null, arr);
    let bucketSize = 5;      //默认将数组划分为5个区域
    //重新计算划分区域,即桶数
    let bucketCount = Math.floor((max - min) / bucketSize) + 1;
    let bucketList = new Array(bucketCount);

    //初始化每个桶区域数组为空,避免其他数据污染
    for (let i = 0; i < bucketList.length; i++) {
        bucketList[i] = []
    }

    //将元素放入桶中
    for (let j = 0; j < arr.length; j++) {
        //计算元素值对应的桶坐标
        let bucketIndex = Math.floor((arr[j] - min) / bucketSize);
        //将元素放入对应的桶中
        bucketList[bucketIndex].push(arr[j]);
    }

    let bucketArr = [];
    //对每个桶内数据进行排序,然后合并数组
    for (let i = 0; i < bucketList.length; i++) {
        //桶内使用插入排序
        insertSort(bucketList[i]);
        //依次遍历桶,将桶数据平铺为有序数组
        for (let j = 0; j < bucketList[i].length; j++) {
            bucketArr.push(bucketList[i][j]);
        }
    }

    return bucketArr;
}

基数排序

算法思想

  1. 查找数组的最大值,计算出最大位数。
  2. 将所有元素按位数分配到对应的桶中(每个位数分为[0,10)个桶)。
  3. 将桶元素合并,生成该位数上的新数组。
  4. 对新数组进行下一位数的排序,依次重复2、3、4步骤。

简单来说就是先对元素的个位数分组排序,然后对十位数分组排序,依次类推…
在这里插入图片描述

//基数排序
export const radixSort = (arr) => {
    let max = Math.max.apply(null, arr);
    let dight = 1;  //从个位开始排序,十位值为10,百位100,依次类推
    let mod = 10;   //用于求余,获取某位数上的值
    let maxDight = max.toString().length;   //数据最大位数
    let bucketList = [];


    //遍历统计每个位数的元素
    for (let i = 0; i < maxDight; i++ , dight *= 10, mod *= 10) {
        for (let j = 0; j < arr.length; j++) {
            //某一位数上的桶坐标,例如112,在十位的数字为1,(112%100)/10
            let bucketIndex = Math.floor((arr[j] % mod) / dight);
            //初始化桶为空数组
            if (bucketList[bucketIndex] == null || bucketList[bucketIndex] == undefined) {
                bucketList[bucketIndex] = [];
            }
            //将数据放入桶中
            bucketList[bucketIndex].push(arr[j]);
        }

        //重组数据
        let pos = 0;
        for (let n = 0; n < bucketList.length; n++) {
            if (bucketList[n] != null && bucketList[n] != undefined) {
                //遍历每个桶重组数据
                for (let m = 0; m < bucketList[n].length; m++) {
                    arr[pos++] = bucketList[n][m];;
                }
            }
        }
        //重组桶数组为空数组
        bucketList = [];
    }
    return arr;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值