几种经典排序算法的实现

197 阅读5分钟

概述

最近项目里面用到了排序相关功能,多数情况下,js用数组内置的sort函数就可以了,这里顺便总结下常见的几种排序算法js的实现,也算是对经典排序算法的更加深入理解。

对比各种排序算法

20210926135144534.png

实现

注:文章实现的排序方法全部以升序排序。

插入排序

思路分析

  1. 从数组第二个元素开始,当前位置和前面位置比较,当小于前一个位置,进行交换,当前指针前移。
  2. 重复1的步骤,继续比较前面的位置和前面的前面的数值。
  3. i++,重复以上步骤,直到i=n-1结束,排序完成

动图演示:

ebb2f96563014c0e95b2d57f4dbce51f.gif

代码实现:

    function insertSort(arr) {
      let len = arr.length;
      for (let i = 1; i < len; i++) {
        let prev = now = i - 1;
        while (now >= 0) {
          if (arr[i] < arr[now]) {
            prev = now;
            now --;
          } else {
            break;
          }
        }
        if (prev >= 0) {
          swap(arr, i, prev)
        }
      }
      function swap(arr, i, j) {
        let temp = arr[i];
        arr[i] = arr[j];
        arr[j] = temp;
      }
      return arr;
    }

希尔排序(插入排序的升级版)

思路分析

  1. 在此我们选择增量gap=length/2,缩小增量继续以gap = gap/2的方式
  2. 这种增量选择我们可以用一个序列来表示,{n/2,(n/2)/2...1},称为增量序列。
  3. 对增量步骤的数值进行插入排序,知道最后gap为0

动图演示:

1258817-20190420100142756-421005088.gif

代码实现:

   function shellSort(arr) {
            let len = arr.length;
            // 定义初始
            let gap = Math.floor(len / 2);
            while (gap > 0) {
                for (let i = gap; i < len; i += gap) {
                    let now = i;
                    let prev = now - gap;
                    while (prev >= 0) {
                        if (arr[now] < arr[prev]) {
                            swap(arr, now, prev);
                            now -= gap;
                            prev -= gap;
                        } else {
                            break;
                        }
                    }
                }
                gap = Math.floor(gap / 2);
            }
            function swap(arr, i, j) {
                let temp = arr[i];
                arr[i] = arr[j];
                arr[j] = temp;
            }
            return arr;
        }

选择排序

思路分析

  1. 选择排序在开始的时候,先扫描整个列表,以找到列表中的最小元素,然后将这个元素与第一个元素进行交换。这样最小元素就放到它的最终位置上。
  2. 然后,从第二个元素开始扫描,找到n-1个元素中的最小元素,然后再与第二个元素进行交换。
  3. 以此类推,直到第n-1个元素(如果前n-1个元素都已在最终位置,则最后一个元素也将在最终位置上)。

动图演示:

d350fd68e1124bbabeeb57ba32f266b4.gif

代码实现:

    function selectSort(arr) {
      let len = arr.length;
      for (let i = 0; i < len; i++) {
        let minIndex = i;
          for (let j = i + 1; j < len; j++) {
              if(arr[j] < arr[minIndex]) {
                minIndex = j;
              }
          }
          swap(arr, i, minIndex)
      }
      function swap(arr, i, j) {
        let temp = arr[i];
        arr[i] = arr[j];
        arr[j] = temp;
      }
    }

冒泡排序

思路分析

  1. 相邻两个数两两相比,n[i]跟n[j+1]比,如果n[i]>n[j+1],则将连个数进行交换
  2. j++, 重复以上步骤,第一趟结束后,最大数就会被确定在最后一位,这就是冒泡排序又称大(小)数沉底
  3. i++,重复以上步骤,直到i=n-1结束,排序完成

动图演示:

1258817-20190325093445247-432584102.gif

代码实现:

   function bubbleSort(arr) {
      let len = arr.length;
      for (let i = len - 1; i >= 0; i--) {
        for (let j = 0; j <= i; j++) {
          if (arr[j] > arr[j + 1]) {
            swap(arr, j, j + 1);
          }
        }
      }
      function swap(arr, i, j) {
        let temp = arr[i];
        arr[i] = arr[j];
        arr[j] = temp;
      }
      return arr;
    }

快速排序

思路分析

  1. 从数列中挑出一个元素,作为基准值,这里实现选择第一个元素;
  2. 重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。
  3. 递归地把小于基准值元素的子数列和大于基准值元素的子数列排序,最后拼接(回溯)。

动图演示:

v2-c411339b79f92499dcb7b5f304c826f4_b.gif

代码实现:

    function quickSort(arr) {
      if (!arr.length) return [];
      let leftArr = [];
      let rightArr = [];
      let cur = arr[0];
      for (let i = 1; i < arr.length; i++) {
        if(arr[i] > cur) {
          rightArr.push(arr[i])
        }else{
          leftArr.push(arr[i])
        }
      }
      return [...quickSort(leftArr),cur,...quickSort(rightArr)]
    }

堆排序(分为大顶堆和小顶堆)

思路分析

  1. 堆它是选择排序的一种,它是通过堆来进行选择数据。(升序要建大堆,降序建小堆)
  2. 首先是创建堆,即把待排序的序列转换成堆的形式。
  3. 然后将根节点与最后一个节点交换。
  4. 交换之后会打乱堆的规律,需要对前(n-1)个节点进行调整,使之重新成为堆。
  5. 接下来交换根节点与倒数第二个节点 …… 重复上述调整,直到序列有序为止。(建堆、交换、调整)

动图演示:

1d19be374f004da0bf6be5be6e32fe6e.gif

代码实现:

 // 堆排序(大顶堆)
    function bigHeapSort(arr) {
      let len = arr.length;
      // 大顶堆序列化
      function heapify(arr, len, index) {
        let max = index;
        let leftNodeIndex = index * 2 + 1;
        let rightNodeIndex = index * 2 + 2;
        if (leftNodeIndex < len && arr[leftNodeIndex] > arr[max]) {
          max = leftNodeIndex;
        }
        if(rightNodeIndex < len && arr[rightNodeIndex] > arr[max]) {
          max = rightNodeIndex;
        }
        // 父节点比两个子节点任意一个小,进行交换,递归维护大顶堆的特性
        if (max != index) {
          swap(arr, index, max);
          heapify(arr, len, max);
        }
      }
      // 构建大顶堆
      for (let i = Math.floor((len - 1) / 2); i >= 0; i--) {
        heapify(arr, len, i);
      }
      // 每次和后面一个元素交换,这样堆顶的元素就跑到后面去了
      for (let j = len - 1; j > 0; j--) {
        swap(arr, j, 0);
        heapify(arr, j, 0);
      }
      function swap(arr, i, j) {
        let temp = arr[i];
        arr[i] = arr[j];
        arr[j] = temp;
      }
      return arr;
    }
    // 堆排序(小顶堆)
    function smallHeapSort(arr) {
      let len = arr.length;
      // 大顶堆序列化
      function heapify(arr, len, index) {
        let min = index;
        let leftNodeIndex = index * 2 + 1;
        let rightNodeIndex = index * 2 + 2;
        if (leftNodeIndex < len && arr[leftNodeIndex] < arr[min]) {
          min = leftNodeIndex;
        }
        if(rightNodeIndex < len && arr[rightNodeIndex] < arr[min]) {
          min = rightNodeIndex;
        }
        // 父节点比两个子节点任意一个小,进行交换,递归维护大顶堆的特性
        if (min != index) {
          swap(arr, index, min);
          heapify(arr, len, min);
        }
      }
      // 构建小顶堆
      for (let i = Math.floor((len - 1) / 2); i >= 0; i--) {
        heapify(arr, len, i);
      }
      // 每次和后面一个元素交换,这样堆顶的元素就跑到后面去了
      for (let j = len - 1; j > 0; j--) {
        swap(arr, j, 0);
        heapify(arr, j, 0);
      }
      function swap(arr, i, j) {
        let temp = arr[i];
        arr[i] = arr[j];
        arr[j] = temp;
      }
      return arr;
    }

归并排序

思路分析

  1. 思路和快速排序类似,采用分而治之思想,递归回溯
  2. 和快速排序不同在于,归并排序需要递归处理子问题的有序数组进行合并,最终返回
  3. 归并排序以空间换时间

动图演示:

radixSort.gif

代码实现:

     function mergeSort(arr) {
      // 递归函数
      function merge(arr) {
        if (arr.length <= 1) return arr;
        let left = 0;
        let middle = Math.floor((arr.length - 1) / 2);
        let right = arr.length;
        let leftArr = merge(arr.slice(left, middle + 1));
        let rightArr = merge(arr.slice(middle + 1, right));
        // 分而治之,进行回溯合并两个有序数据
        return helper(leftArr, rightArr);
      }
      // 用于排列两个有序数组
      function helper(arr1, arr2) {
        let res = [];
        let i = 0;
        let j = 0;
        while (i < arr1.length && j < arr2.length) {
          if (arr1[i] <= arr2[j]) {
            res.push(arr1[i]);
            i++;
          } else {
            res.push(arr2[j]);
            j++;
          }
        }
        if (i != arr1.length) {
          for (let m = i; m < arr1.length; m++) {
            res.push(arr1[m])
          }
        }
        if (j != arr2.length) {
          for (let n = j; n < arr2.length; n++) {
            res.push(arr2[n])
          }
        }
        return res;
      }
      return merge(arr);
    }

计数排序(最简单)

思路分析 1、找出待排序的数组中最大和最小的元素
2、统计数组中每个值为i的元素出现的次数,存入数组C的第i项
3、对所有的计数累加(从C中的第一个元素开始,每一项和前一项相加)
4、反向填充目标数组:将每个元素i放在新数组的第C(i)项,每放一个元素就将C(i)减去1

动图演示:

20200210211952977-1.gif

代码实现:

  function countSort(arr) {
            let res = [];
            let len = arr.length;
            let list = new Array(len);
            for (let i = 0; i < len; i++) {
                list[arr[i]] = list[arr[i]] != undefined ? list[arr[i]] + 1 : 1;
            }
            for (let j = 0; j < list.length; j++) {
                if (list[j]) {
                    let count = list[j];
                    while (count > 0) {
                        res.push(j)
                        count--;
                    }
                }
            }
            return res;
        }

桶排序

思路分析

  1. 创建桶
  2. 将元素放入桶(桶可以使用链表或者数组)
  3. 每个桶里的元素,做好排序
  4. 遍历所有桶,放入到结果数组中

动图演示:

a3a32f00af904de0a60c37e9fd278330.gif

代码实现:

    function bucketsort(arr) {
      // 计算最大值,最小值
      let max = min = arr[0];
      let len = arr.length;
      for (let i = 0; i < len; i++) {
        max = Math.max(max, arr[i])
        min = Math.min(min, arr[i])
      }
      // 计算桶的数量
      // [3, 1, 7, 10, 2, 3, 6, 5, 4, 2];
      let bucketNum = Math.floor((max - min) / arr.length) + 1;
      // 通过二维数组保存桶元素
      let bucketList = new Array(bucketNum).fill(0).map(item => []);
      // 将数组元素放到对应的桶当中
      for (let j = 0; j < len; j++) {
        // 计算当前元素对应桶的索引位置,将当前元素放到正确的桶当中
        let bucketIndex = Math.floor((arr[j] - min) / len);
        bucketList[bucketIndex].push(arr[j]);
      }
      // 对桶中的元素进行排序
      for (let k = 0; k < bucketList.length; k++) {
         bubbleSort( bucketList[k])
      }
      // 拆分输出
      return bucketList.flat(1);
    }

基数排序

思路分析

  1. 基数排序(Radix Sort)是桶排序的扩展,它的基本思想是:将整数按位数切割成不同的数字,然后按每个位数分别比较。
  2. 将所有待比较数值统一为同样的数位长度,数位较短的数前面补零。
  3. 然后,从最低位开始,依次进行一次排序
  4. 这样从最低位排序一直到最高位排序完成以后, 数列就变成一个有序序列。
  5. ps:基数排序只可以应用于整数。

动图演示:

849589-20171015232453668-1397662527.gif

代码实现:

  function radixSort(arr) {
      // 初始化桶
      let radixList = new Array(10).fill(0).map(item => []);
      // 结果
      let res = arr;
      // 最大值
      let max = Math.max(...arr);
      // 最大值的位数
      let maxLen = max.toString().length;
      // 数组长度
      let len = arr.length;
      // 初始索引
      let init = 1;
      while (init <= maxLen) {
        // 重置桶中数据
        radixList = new Array(10).fill(0).map(item => []);
        // 依次对个十百各个数位进行重新排列
        for (let i = 0; i < len; i++) {
          let currentDigitalString = res[i].toString();
          let currentDigitalStringLen = currentDigitalString.length;
          let currentDigitalNumber = currentDigitalStringLen >= init ? currentDigitalString[currentDigitalStringLen - init] * 1 : 0;
          radixList[currentDigitalNumber].push(res[i]);
        }
        // 从新排列数组
        res = [];
        for (let i = 0; i < radixList.length; i++) {
          let currentLen = radixList[i].length;
          if (currentLen) {
            for (let j = 0; j < currentLen; j++) {
              res.push(radixList[i][j])
            }
          }
        }
        init++;
      }
      return res;
    }