Javascript实现8大排序算法

510 阅读6分钟

内部排序

插入排序

在要排序的一组元素,假设前面n-1(n>=2)个元素已经是排好顺序的,先要把第n个元素插入到前面的有序序列,使得这n元素也是排好顺序的。如此反复循环,直到所有元素排好顺序。(PS:首次认为第1个元素已经拍好顺序)

插入排序在小规模数据和基本有序数据上最高效。

function insertSort(arr) {
  for (let i = 1; i < arr.length; i++) {
    // 将待插入元素提取出来
    let temp = arr[i];
    let j = i - 1;
    for (; j >= 0; j--) {
      if (arr[j] > temp) {
        // 插入元素小于比较元素,比较元素则向后移动一位
        arr[j + 1] = arr[j];
      } else {
        // 否则,结束移位
        break;
      }
    }
    //将插入元素插入正确位置
    arr[j + 1] = temp;
  }
  return arr;
}

希尔排序

先将要排序的一组元素按某个增量gaparr.length/2gap为要排序的个数)分成若干组,每组中记录的下标相差gap。对每组中全部元素进行直接插入排序,然后再用一个较小的增量(gap/2)对它进行分组,在每组中再进行直接插入排序。当增量减到1时,进行直接插入排序后,排序完成。

具体看这篇文章,采用漫画的形式,更好理解

function shellSort(arr) {
  let gap = arr.length;

  while (gap > 1) {
    gap = Math.floor(gap / 2);

    for (let i = 0; i < gap; i++) {
      // 分为gap组
      let tempArr = [];
      for (let j = i; j < arr.length; j += gap) {
        tempArr.push(arr[j]);
      }
      // 使用插入排序有序化
      tempArr = insertSort(tempArr);

      for (let j = i, k = 0; j < arr.length; j += gap, k++) {
        arr[j] = tempArr[k];
      }
    }
  }

  return arr;
}

直接选择排序

每次选择待排序的元素中最小的值,放置在序列的首位。过程如下:

初始数组: [8, 5, 2, 6, 9, 3, 1, 4, 0, 7] 第1趟排序后:0, [5, 2, 6, 9, 3, 1, 4, 8, 7] 第2趟排序后:0, 1, [2, 6, 9, 3, 5, 4, 8, 7] 第3趟排序后:0, 1, 2, [6, 9, 3, 5, 4, 8, 7] 第4趟排序后:0, 1, 2, 3, [9, 6, 5, 4, 8, 7] 第5趟排序后:0, 1, 2, 3, 4, [6, 5, 9, 8, 7] 第6趟排序后:0, 1, 2, 3, 4, 5, [6, 9, 8, 7] 第7趟排序后:0, 1, 2, 3, 4, 5, 6, [9, 8, 7] 第8趟排序后:0, 1, 2, 3, 4, 5, 6, 7, [8, 9] 第9趟排序后:0, 1, 2, 3, 4, 5, 6, 7, 8, [9] 最终结果: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

function selectSort(arr) {
  for (let i = 0; i < arr.length; i++) {
    let min = arr[i];
    let minIndex = i;

    for (let j = i + 1; j < arr.length; j++) {
      if (arr[j] < min) {
        // 找到最小值,并标注最小值索引,方便后续与元素arr[i]交换位置
        min = arr[j];
        minIndex = j;
      }
    }

    arr[minIndex] = arr[i];
    arr[i] = min;
  }

  return arr;
}

堆排序

堆分为"最大堆"和"最小堆"。最大堆通常被用来进行"升序"排序,而最小堆通常被用来进行"降序"排序。

由于我们的算法主要讲升序,它分为两步:

  1. 初始化堆:将数列a[1...n]构造成最大堆。
  2. 交换数据:将a[1]和a[n]交换,使a[n]是a[1...n]中的最大值;然后将a[1...n-1]重新调整为最大堆。 接着,将a[1]和a[n-1]交换,使a[n-1]是a[1...n-1]中的最大值;然后将a[1...n-2]重新调整为最大值。 依次类推,直到整个数列都是有序的。

因为涉及到完全二叉树的概念,查看这里详细了解

function heapify(arr, i, len) {
 // 堆调整
 // 完全二叉树特性:左边子节点位置 = 当前父节点的两倍 + 1,右边子节点位置 = 当前父节点的两倍 + 2
 let left = 2 * i + 1;
 let right = 2 * i + 2;
 let largest = i; // 默认最大值是父级节点

 if (left < len && arr[left] > arr[largest]) {
   // 如果有左子节点,且左子节点元素大于父级元素
   largest = left;
 }

 if (right < len && arr[right] > arr[largest]) {
   // 如果有左子节点,且左子节点元素大于父级元素
   largest = right;
 }

 if (largest !== i) {
   // 最大元素不是父级节点,则交换
   [arr[i], arr[largest]] = [arr[largest], arr[i]];
   heapify(arr, largest, len);
 }
}

function heapSort(arr) {
 // 建立最大堆
 let len = arr.length;

 for (let i = Math.floor(len / 2); i >= 0; i--) {
   heapify(arr, i, len);
 }

 for (let i = arr.length - 1; i > 0; i--) {
   // 先把大根堆顶部最大值放在数组末尾
   [arr[0], arr[i]] = [arr[i], arr[0]];
   len--;
   heapify(arr, 0, len);
 }
 return arr;
}

冒泡排序

在要排序的一组元素中,对当前还未排好序的范围内的全部元素,自上而下对相邻的两个元素依次进行比较和交换,让较大的元素往下沉,较小的往上冒。

function bubbleSort(arr) {
  for (let i = 0; i < arr.length; i++) {
    // 每次比较时都已经有i个元素到最后去了,所以j < 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;
}

快速排序

选择一个基准元素(通常选择第一个元素),通过一趟排序将要排序的数据分割成独立的两部分,一部分的所有元素小于基准元素,另外一部分的所有元素大于等于基准元素。再用递归的方式继续切分。

function quickSort(arr) {
  if (arr.length <= 1) {
    // 切个到最小了,不需要比较
    return arr;
  }

  const baseVal = arr.shift(); // 取第一个作为基准元素
  const lessArr = [];
  const moreArr = [];

  for (const val of arr) {
    if (val < baseVal) {
      lessArr.push(val);
    } else {
      moreArr.push(val);
    }
  }

  return quickSort(lessArr).concat([baseVal], quickSort(moreArr));
}

归并排序

将两个有序表合并成一个新的有序表,即把待排序序列分为若干个子序列,每个子序列是有序的。然后再把有序子序列合并为整体有序序列。即先划分为两个部分,最后进行合并。过程如下:

初始数组: [1, 5, 4, 6, 3, 2, 0] 第1次归并后:[1, 5], [4, 6], [2, 3], [0] 第2次归并后:[1, 4, 5, 6], [0, 2, 3] 第3次归并后:[1, 2, 3, 4, 5, 6, 7]

function merge(leftArr, rightArr) {
  const retArr = [];

  while (leftArr.length > 0 && rightArr.length > 0) {
    // 拿出两个有序序列中最小的放到新的有序序列中
    if (leftArr[0] < rightArr[0]) {
      retArr.push(leftArr.shift());
    } else {
      retArr.push(rightArr.shift());
    }
  }
  // leftArr和rightArr只有一个还有剩余的最大元素组
  return retArr.concat(leftArr, rightArr);
}

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

  // 拆分成两个序列
  const mid = Math.floor(arr.length / 2);
  const leftArr = arr.slice(0, mid);
  const rightArr = arr.slice(mid);

  return merge(mergeSort(leftArr), mergeSort(rightArr))
}

基数排序

基数排序是一种非比较型整数排序算法,其原理是将数据按位数切割成不同的数字,然后按每个位数分别比较。

在类似对百万级的电话号码进行排序的问题上,使用基数排序效率较高。流程如下:

radixSort.gif

function radixSort(arr) {
  // 求出最大数位长度,比如1就是1位,10就是2位
  const maxDigit = Math.floor(Math.log10(Math.max.apply(Math, arr)));

  for (let i = 0, dev = 1, mod = 10; i <= maxDigit; i++ , dev *= 10, mod *= 10) {
    const counter = [];

    for (let j = 0; j < arr.length; j++) {
      // 求出元素在当前数位的值,比如50在十分位(第二分位)上的值是5
      let bucket = parseInt((arr[j] % mod) / dev);
      if (counter[bucket] == null) {
        counter[bucket] = [];
      }
      counter[bucket].push(arr[j]);
    }

    let index = 0;
    counter.forEach(bucketArr => {
      bucketArr.forEach(val => {
        arr[index++] = val;
      });
    });
  }
  return arr;
}

各算法时间复杂度、空间复杂度与稳定性

算法比较.png