概述

排序算法 思路 时间复杂度 空间复杂度 稳定性
冒泡排序 两两比较,将较大(或较小)的元素换到后面,每轮比较后数组后面都是排序好的 O(n^2) O(1) 稳定
插入排序 将待插入的元素依次与前面已排序的元素对比,插入到正确的位置 O(n^2) O(1) 稳定
归并排序 分治,排序,合并 O(nlogn) O(n) 稳定
快速排序 基准元素,小于基准元素放左边,大于基准元素放右边,递归 最好O(nlogn)、最坏O(n^2) O(logn) 不稳定
拓扑排序 有向图、无环、理清依赖关系、广度优先搜索或深度优先搜索

1.基本的排序算法

  • 冒泡排序(BubbleSort)
  • 插入排序(InsertionSort)

2.常考的排序算法

  • 归并排序(MergeSort)
  • 快速排序(QuickSort)
  • 拓扑排序(Topological Sort)

3.其他排序算法

  • 堆排序(Heap Sort)
  • 桶排序(Bucket Sort)

注意:

  • 冒泡排序和插入排序是最基础的,面试官有时候喜欢拿它们来考察你的基础知识,并且看看你能不能快速地写出没有bug的代码。
  • 归并排序、快速排序和拓扑排序的思想是解决绝大部分涉及排序问题的关键。
  • 堆排序和桶排序,本节课不作深入研究,但有时间的话一定要看看,尤其是桶排序,在一定的场合中(例如知道所有元素出现的范围时),能在线性的时间复杂度里解决战斗,掌握好它的解题思想能开阔解题思路。

冒泡排序

  • 基本思想

给定一个数组,我们把数组里的元素通通倒入到水池中,这些元素将通过相互之间的比较,按照大小顺序一个一个地像气泡一样浮出水面。

  • 实现

每一轮,从杂乱无章的数组头部开始,每两个元素比较大小并进行交换,直到这一轮当中最大或最小的元素被放置在数组的尾部,然后不断地重复这个过程,直到所有元素都排好位置。其中,核心操作就是元素相互比较。

  • 代码实现
var arr = [2, 1, 7, 9, 5, 8];
// 冒泡排序  O(n^2)  稳定算法
// 在冒泡排序中,经过每一轮的排序处理后,数组后端的数是排好序的
// 每一轮,从数组头部开始,比较两个元素,将大的换到后面,则这一轮结束后就将最大的元素换到了数组尾部
// 如果hasChange===flase,说明上一轮未发生位置交换,已经排序好了,就不需要下一轮了
bubbleSort = function (arr) {
  var hasChange = true;
  for (var i = 0; i < arr.length - 1 && hasChange; i++) {
    hasChange = false;
    for (var j = 0; j < arr.length - 1 - i; j++) {
      if (arr[j] > arr[j + 1]) {
        [arr[j], arr[j + 1]] = [arr[j + 1], arr[j]];
        hasChange = true;
      }
    }
  }
  return arr;
};
bubbleSort(arr); // [1,2,5,7,8,9]
  • 空间复杂度: 由于在整个排序的过程中,是直接在给定的数组里面进行元素的两两交换,所以空间复杂度是 O(1)。
  • 时间复杂度:冒泡排序的时间复杂度是 O(n^2)。它是一种稳定的排序算法。(稳定是指如果数组里两个相等的数,那么排序前后这两个相等的数的相对位置保持不变。)

插入排序

  • 基本思想

不断地将尚未排好序的数插入到已经排好序的部分。

  • 特点

在冒泡排序中,经过每一轮的排序处理后,数组后端的数是排好序的;而对于插入排序来说,经过每一轮的排序处理后,数组前端的数都是排好序的。

  • 代码实现
function InsertionSort(arr) {
  var len = arr.length;
  // 将数组的第一个元素当作是已经排序好的,从第二个元素,即 i 从 1 开始遍历数组
  for (var i = 1; i < len; i++) {
    var temp = arr[i]; // 存 待插入的元素
    for (var j = i; j > 0; j--) {
      // 将待插入的元素与他前面的进行比较,如果比前面的小,就将前面的后移
      if (temp >= arr[j - 1]) {
        break; // 当前考察的数大于前一个数,证明有序,退出循环
      } else {
        arr[j] = arr[j - 1]; // 将前一个数复制到后一个数上
      }
    }
    // 内循环结束,j所指向的位置就是temp值插入的位置
    arr[j] = temp; // 找到考察的数应处于的位置
  }
  console.log(arr);

  return arr;
}
InsertionSort(arr); //  [1, 2, 5, 7, 8, 9]
  • 时间复杂度:

和冒泡排序一样,插入排序的时间复杂度是 O(n2),并且它也是一种稳定的排序算法。

  • 空间复杂度

假设数组的元素个数是 n,由于在整个排序的过程中,是直接在给定的数组里面进行元素的两两交换,空间复杂度是 O(1)。

归并排序( Merge Sort)

  • 基本思想

核心是分治,就是把一个复杂的问题分成两个或多个相同或相似的子问题,然后把子问题分成更小的子问题,直到子问题可以简单的直接求解,最原问题的解就是子问题解的合并。归并排序将分治的思想体现得淋漓尽致。

  • 实现

一开始先把数组从中间划分成两个子数组,一直递归地把子数组划分成更小的子数组,直到子数组里面只有一个元素,才开始排序。
排序的方法就是按照大小顺序合并两个元素,接着依次按照递归的返回顺序,不断地合并排好序的子数组,直到最后把整个数组的顺序排好。

  • 代码实现
// 归并排序
function mergeSort(arr) {
  debugger
  var len = arr.length;
  if (len < 2) {
    return arr;
  }
  // 首先将无序数组划分为两个数组
  var mid = Math.floor(len / 2);
  var left = arr.slice(0, mid);
  var right = arr.slice(mid, len);
  return merge(mergeSort(left), mergeSort(right));
}

// 合并,依次将小的放进新数组
function merge(left, right) {
  var result = [];
  while (left.length > 0 && right.length > 0) {
    if (left[0] < right[0]) {
      result.push(left.shift(0));
    } else {
      result.push(right.shift(0));
    }
  }
  while (left.length > 0) {
    result.push(left.shift(0));
  }
  while (right.length > 0) {
    result.push(right.shift(0));
  }
  return result;
}
console.log(mergeSort(arr)); //  [1, 2, 5, 7, 8, 9]
  • 空间复杂度:由于合并n个元素需要分配一个大小为n的额外数组,合并完成之后,这个数组的空间就会被释放,所以算法的空间复杂度就是O(n)。归并排序也是稳定的排序算法。
  • 时间复杂度:归并算法是一个不断递归的过程。

举例:数组的元素个数是n,时间复杂度是T(n)的函数。

解法:把这个规模为n的问题分成两个规模分别为η/2的子问题,每个子问题的时间复杂度就是T(n/2),那么两个子问题的复杂度就是2×T(n/2)。当两个子问题都得到了解决,即两个子数组都排好了序,需要将它们合并,一共有n个元素,每次都要迸行最多n-1次的比较,所以合并的复杂度是o(n)。由此我们得到了递归复杂度公式:T(n)=2×T(n/2)+O(n)。

对于公式求解,不断地把一个规模为η的问题分解成规模为η2的问题,一直分解到规模大小为1。

如果n等于2,只需要分一次;如果η等于4,需要分2次。这里的次数是按照规模大小的变化分类的以此类推,对于规模为η的问题,一共要进行log(η)层的大小切分。在每一层里,我们都要进行合并,所涉及到的元素其实就是数组里的所有元素,因此,每一层的合并复杂度都是O(n),所以整体的复杂度就是 o(nlogn)

建议:归并算法的思想很重要,其中对两个有序数组合并的操作,在很多面试题里都有用到,建议大家一定要把这个算法练熟。

快速排序( Quick Sort)

  • 基本思想:快速排序也采用了分治的思想。
  • 实现:取一个基准元素,比这个元素小的放到左边数组,比这个元素大的放到右边数组,然后递归地排序两个子数组,然后把排序好的左数组、基准元素、排序好的右数组合并。
  • 代码实现
// 快速排序
function quickSort(arr) {
  if (arr.length < 2) {
    return arr;
  }
  var p = arr[0]; // 用第一个元素作为基准值,比它小的放到左边数组,比它大的放到右边数组
  var left = [];
  var right = [];
  for (var i = 1; i < arr.length; i++) {
    if (arr[i] <= p) {
      left.push(arr[i]);
    } else {
      right.push(arr[i]);
    }
  }
  return [...quickSort(left), p, ...quickSort(right)];
}
console.log(quickSort(arr)); // [1, 2, 5, 7, 8, 9]
  • 时间复杂度: 最好情况O(nlogn),最坏情况O(n^2)

拓扑排序( Topological Sort)

  • 基本思想和前面介绍的几种排序不同,拓扑排序应用的场合不再是一个简单的数组,而是研究图论里面顶点和顶点连线之间的性质。拓扑排序就是要将这些顶点按照相连的性质进行排序。

要能实现拓扑排序,得有几个前提
1.图必须是有向图
2.图里面没有环拓扑排序一般用来理清具有依赖关系的任务。

举例:假设有三门课程A、B、C,如果想要学习课程C就必须先把课程B学完,要学习课程B,还得先学习课程A,所以得出课程的学习顺序应该是A->B->C
实现
1.将问题用一个有向无环图(DAG, Directed Acyclic Graph)进行抽象表达,定义出哪些是图的顶点,顶点之间如何互相关联。
2.可以利用广度优先搜索或深度优先搜索来进行拓扑排序。


MandyShen
166 声望21 粉丝