一次搞定常用排序算法-JS版

423 阅读5分钟

排序总结1.png

排序算法

稳定性:排序后 2 个相等元素的顺序和排序之前它们的顺序相同,即排序前后相等元素相对顺序不变

1.冒泡排序(了解!)

时间复杂度:O(n^2)

比较相邻的元素。如果第一个比第二个大,就交换他们两个。

对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大的数。

针对所有的元素重复以上的步骤,除了最后一个。

持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。

冒泡排序.gif

function bubbleSort(arr) {
  for (let i = 0; i < arr.length - 1; i++) {
    for (let j = 0; j < arr.length - i - 1; j++) {
      if (arr[j] > arr[j + 1]) { // 相邻元素两两对比
        let temp = arr[j + 1];  // 元素交换
        arr[j + 1] = arr[j];
        arr[j] = temp;
      }
    }
  }
  return arr;
}

let arr = [3, 2, 5, 7, 6, 1, 8, 4];
console.log(bubbleSort(arr));

改进版

设置标志位flag,如果发生了交换flag设置为true;如果没有交换就设置为false。

这样当一轮比较结束后如果flag仍为false,即:这一轮没有发生交换,说明数据的顺序已经排好,没有必要继续进行下去。

function bubbleSortBetter(arr) {
  for (let i = 0; i < arr.length - 1; i++) {
    let flag = false;
    for (let j = 0; j < arr.length - i - 1; j++) {
      if (arr[j] > arr[j + 1]) {
        let temp = arr[j + 1];
        arr[j + 1] = arr[j];
        arr[j] = temp;
        flag = true;
      }
    }
    if (!flag) break;
  }
  return arr;
}

let arr = [3, 2, 5, 7, 6, 1, 8, 4];
console.log(bubbleSortBetter(arr));

2.插入排序(熟悉!)

时间复杂度:O(n^2)

原理:通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。

将第一待排序序列第一个元素看做一个有序序列,把第二个元素到最后一个元素当成是未排序序列。

从头到尾依次扫描未排序序列,将扫描到的每个元素插入有序序列的适当位置。(如果待插入的元素与有序序列中的某个元素相等,则将待插入元素插入到相等元素的后面。)

插入排序.gif

function insertSort(arr) {
  let len = arr.length;
  for (let i = 1; i < len; i++) {
    let cur = arr[i];
    let preIndex = i - 1;
    while (preIndex >= 0 && arr[preIndex] > cur) {
      arr[preIndex + 1] = arr[preIndex];
      preIndex--;
    }
    arr[preIndex + 1] = cur;
  }
  return arr;
}

let arr = [3, 2, 5, 7, 6, 1, 8, 4];
console.log(insertSort(arr));

3.选择排序-不稳定(了解!)

时间复杂度:O(n^2)

首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置

再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。

重复第二步,直到所有元素均排序完毕。

选择排序.gif

function selectSort(arr) {
  let len = arr.length;
  for (let i = 0; i < len - 1; i++) {
    let minIndex = i;
    for (let j = i + 1; j < len; j++) {
      if (arr[j] < arr[minIndex]) { //寻找最小的数
        minIndex = j; //寻找最小数的索引保存
      }
    }
    let temp = arr[i];
    arr[i] = arr[minIndex];
    arr[minIndex] = temp;
  }
  return arr;
}

let arr = [3, 2, 5, 7, 6, 1, 8, 4];
console.log(selectSort(arr));

4.希尔排序-不稳定(了解!)

缩小增量排序-改进版插入排序

时间复杂度:O(n^1.3-2),看选择的增量公式

希尔排序是基于插入排序的以下两点性质而提出改进方法的:

  • 插入排序在对几乎已经排好序的数据操作时,效率高,即可以达到线性排序的效率;
  • 但插入排序一般来说是低效的,因为插入排序每次只能将数据移动一位;

希尔排序的基本思想是:先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,待整个序列中的记录“基本有序”时,再对全体记录进行依次直接插入排序。

通过某个增量将数组元素划分为若干组,然后分组进行插入排序,随后逐步缩小增量,继续按组进行插入排序操作,直至增量为1。直接对整个数组进行直接插入排序。

增量公式: gap = gap / 2 或 gap = gap / 3 + 1

希尔排序.png

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

let arr = [3, 2, 5, 7, 6, 1, 8, 4];
console.log(shellSort(arr));

5.归并排序(重点!)

时间复杂度:O(n log n)

空间复杂度:O(n)

归并排序(Merge sort)是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。

作为一种典型的分而治之思想的算法应用,归并排序的实现由两种方法:

  • 自上而下的递归(所有递归的方法都可以用迭代重写,所以就有了第 2 种方法);
  • 自下而上的迭代;

和选择排序一样,归并排序的性能不受输入数据的影响,但表现比选择排序好的多,因为始终都是 O(nlogn) 的时间复杂度。代价是需要额外的内存空间。

1.申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列;

2.设定两个指针,最初位置分别为两个已经排序序列的起始位置;

3.比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置;

4.重复步骤 3 直到某一指针达到序列尾;

5.将另一序列剩下的所有元素直接复制到合并序列尾。

归并排序图解.png

合并有序子序列.png

归并排序.gif

function mergeSort(arr) {
  let len = arr.length;
  if (len < 2) {
    return arr;
  }
  let mid = Math.floor(len / 2),
    left = arr.slice(0, mid),
    right = arr.slice(mid);
  return merge(mergeSort(left), mergeSort(right));
}

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

let arr = [3, 2, 5, 7, 6, 1, 8, 4];
console.log(mergeSort(arr));

6.快速排序-不稳定(重点!)

时间复杂度:O(n log n)

空间复杂度:O(log n)

快速排序通常明显比其他同为Ο(n log2n) 排序算法更快。快速排序应该算是在冒泡排序基础上的递归分治法。

快速排序使用 分治法 来把一个串(list)分为两个子串(sub-lists)。具体算法描述如下:

  • 从数列中挑出一个元素,称为 “基准”(pivot);
  • 重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作;
  • 递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序。

(通俗解释)挖坑填数+分治法:

1.i =L; j = R; 将基准数挖出形成第一个坑a[i]。

2.j--由后向前找比它小的数,找到后挖出此数填前一个坑a[i]中。同时i++

3.i++由前向后找比它大的数,找到后也挖出此数填到前一个坑a[j]中。同时j--

4.再重复执行2,3二步,直到i==j,将基准数填入a[i]中。

快速排序图解.jpg

快排优化:

注: 在待排数组有序或基本有序的情况下,选择使用固定基准影响快排的效率。为了解决数组基本有序的问题,可以采用随机基准的方式来化解这一问题。排序分治形成的二叉树会非常不平衡,退化成接近链表。

if (l < r) {
    let ranIndex = l + Math.floor(Math.random() * (r - l))
    let temp = arr[l]
    arr[l] = arr[ranIndex]
    arr[ranIndex] = temp
}

完整代码:

function quickSort(arr, l, r) {
  //去掉特殊情况,可不写
  //if(arr === null || arr.length === 0) return
  if (l < r) {
    let mid = partition(arr, l, r);
    quickSort(arr, l, mid - 1);
    quickSort(arr, mid + 1, r);
  }
  return arr;
}

function partition(arr, l, r) {
  //随机选择基准,解决数组基本有序的问题
  if (l < r) {
      let ranIndex = l + Math.floor(Math.random() * (r - l))
      let temp = arr[l]
      arr[l] = arr[ranIndex]
      arr[ranIndex] = temp
  }
  
  let pivot = arr[l];
  while (l < r) {
    while (l < r && arr[r] > pivot) r--;
    if (l < r) {
      arr[l] = arr[r];
      l++;
    }
    while (l < r && arr[l] <= pivot) l++;
    if (l < r) {
      arr[r] = arr[l];
      r--;
    }
  }
  arr[l] = pivot;
  return l;
}

let arr = [3, 2, 5, 7, 6, 1, 8, 4];
console.log(quickSort(arr, 0, arr.length - 1));

7.堆排序-不稳定(重点!)

时间复杂度:O(n log n)

堆排序(Heapsort)是指利用堆这种数据结构所设计的一种排序算法。堆积是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。堆排序其实也是一种选择排序,是一种树形选择排序。堆排序为不稳定排序,不适合记录较少的排序。堆排序可以说是一种利用堆的概念来排序的选择排序。分为两种方法:

  1. 大顶堆:每个节点的值都大于或等于其子节点的值,在堆排序算法中用于升序排列;

  2. 小顶堆:每个节点的值都小于或等于其子节点的值,在堆排序算法中用于降序排列;

堆结构.png

堆的存储: 一般用数组来表示堆,下标为 i (i > 1)的结点的父结点下标为(i - 1) / 2;其父节点左右子结点分别为 (2i + 1)、(2i + 2)。最后一个父节点为len / 2 - 1

基本思想:

利用大顶堆(小顶堆)堆顶记录的是最大关键字(最小关键字)这一特性,使得每次从无序中选择最大记录(最小记录)变得简单。

① 将待排序的序列构造成一个最大堆,此时序列的最大值为根节点 ② 依次将根节点与待排序序列的最后一个元素交换 ③ 再维护从根节点到该元素的前一个节点为最大堆,如此往复,最终得到一个递增序列

详细分步解析.jpg

堆排序动图.gif

let len;
function heapSort(arr) {
  len = arr.length;
  if (len < 1) return arr;
  //构建一个最大堆
  buildMaxHeap(arr);
  //循环将堆首位(最大值)与末位交换,然后在重新调整最大堆
  //此处写arr.length,因为n是控制堆元素个数,会改变
  for (let i = arr.length - 1; i >= 0; i--) {
    swap(arr, 0, i);
    len--;
    heapify(arr, 0);
  }
  return arr;
}

function buildMaxHeap(arr) {
  //初始化,i从最后一个父节点开始调整,最后一个父节点为len/2-1
  for (let i = Math.floor(len / 2) - 1; i >= 0; i--) {
  //for (let i = Math.floor(len / 2 - 1); i >= 0; i--) {
    heapify(arr, i);
  }
}

function heapify(arr, i) {
  let l = 2 * i + 1,
    r = 2 * i + 2,
    largest = i;
  //如果有左子树,且左子树大于父节点,则将最大指针指向左子树
  if (l < len && arr[l] > arr[largest]) {
    largest = l;
  }
  //如果有右子树,且右子树大于父节点,则将最大指针指向右子树
  if (r < len && arr[r] > arr[largest]) {
    largest = r;
  }
  //如果父节点不是最大值,则将父节点与最大值交换,并且递归调整与父节点交换的位置。
  if (largest !== i) {
    swap(arr, i, largest);
    heapify(arr, largest);
  }
}

function swap(arr, i, j) {
  let temp = arr[j];
  arr[j] = arr[i];
  arr[i] = temp;
}

let arr = [3, 2, 5, 7, 6, 1, 8, 4];
console.log(heapSort(arr));

参考文章:

1、排序动图

2、希尔排序

3、归并排序

4、快速排序

5、堆排序