前端面试和笔试中被问到最多的算法可能就是各类排序算法了,算法并不难,平时常常用到,但不少时候不多会去认真考虑算法优劣性和适应场景,真正一个一个去分析也须要花很多时时间,因此趁年底有空,不如再复习一遍排序算法。javascript
全部排序算法读者可自行尝试coding,想看源码戳这里。此文配合源码体验更佳!html
一个算法语句总的执行次数是关于问题规模N的某个函数,记为f(N),N称为问题的规模。语句总的执行次数记为T(N),当N不断变化时,T(N)也在变化,算法执行次数的增加速率和f(N)的增加速率相同。前端
则有T(N) = O(f(N)),这称做算法的渐进时间复杂度,简称时间复杂度。java
最坏时间复杂度
最坏状况下的时间复杂度称最坏时间复杂度,通常不特别说明,讨论的时间复杂度均是最坏状况下的时间复杂度。这样作的缘由是:最坏状况下的时间复杂度是算法在任何输入实例上运行时间的上界,这就保证了算法的运行时间不会比任何更长。 git
平均时间复杂度
平均时间复杂度是指全部可能的输入实例均以等几率出现的状况下,算法的指望运行时间,设每种状况的出现的几率为pi,平均时间复杂度则为sum(pi*f(n)) 。github
最好时间复杂度
最理想状况下的时间复杂度称最好时间复杂度。面试
空间复杂度(Space Complexity)
是对一个算法在运行过程当中临时占用存储空间大小的量度,记作S(n)=O(f(n))。算法
稳定
的算法在排序的过程当中不会改变元素彼此的位置的相对次序,反之不稳定
的排序算法常常会改变这个次序,这是咱们不肯意看到的。shell
咱们在使用排序算法或者选择排序算法时,更但愿这个次序不会改变,更加稳定,因此排序算法的稳定性,是一个特别重要的参数衡量指标依据。segmentfault
内排序
:全部排序操做都在内存中完成,适用于数据规模不是特别大的状况;外排序
:因为数据太大,所以把数据放在磁盘中,而排序经过磁盘和内存的数据传输才能进行;
对算法原理很熟悉的同窗能够直接记忆这张表。
图片名词解释:
冒泡排序是一种简单的排序算法。它重复地走访过要排序的数列,一次比较两个相邻元素,若是它们的顺序错误就把它们交换过来。走访数列的工做是重复地进行直到没有再须要交换,也就是说该数列已经排序完成。这个算法的名字由来是由于越小的元素会经由交换慢慢“浮”到数列的顶端。
具体算法描述以下:
let bubbleSort = arr => { for (let i = 0; i < arr.length; i++) { for (let j = 0; j < arr.length - i - 1; j++) { if (arr[j] > arr[j + 1]) { let temp = arr[j]; arr[j] = arr[j + 1]; arr[j + 1] = temp; } } } return arr; };
平均时间复杂度: T(n) = O(n²)
最坏时间复杂度: T(n) = O(n²)
:当输入的数据是反序时
最好时间复杂度: T(n) = O(n)
:当输入的数据已经有序时,只需遍历一遍用于确认数据已有序。
空间复杂度: O(1)
稳定性: 稳定
选择排序(Selection-sort)是一种简单直观的排序算法。它的工做原理:首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,而后,再从剩余未排序元素中继续寻找最小(大)元素,而后放到已排序序列的末尾。以此类推,直到全部元素均排序完毕。
具体算法描述以下:
n个记录的直接选择排序可通过n-1趟直接选择排序获得有序结果。具体算法描述以下:
let selectSort = arr => { for (let i = 0; i < arr.length; i++) { Let min = arr[i]; // 初始化最小值为第一个元素 let index = i; // 最小值下标 for (let j = i + 1; j < arr.length; j++) { if (arr[j] < min) { // 发现更小的数则交换位置 min = arr[j]; index = j; } } // 将当前趟最小值移动至其最终位置 let temp = arr[i]; arr[i] = min; arr[index] = temp; } };
选择排序是时间复杂度表现最稳定的排序算法之一,不管什么数据进去都是O(n²) 的时间复杂度…..因此用到它的时候,数据规模越小越好。这也是通常人想到最多的简单算法,简单粗暴。
平均时间复杂度: T(n) = O(n²)
最坏时间复杂度: T(n) = O(n²)
最好时间复杂度: T(n) = O(n²)
空间复杂度: O(1)
稳定性: 不稳定
性能表现实在太稳定了,通常改进思路能够从空间换时间角度切入,减小比较次数。
插入排序(insertion-Sort)的算法描述是一种简单直观的排序算法。它的工做原理是经过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。插入排序在实现上,一般采用in-place排序(即只需用到O(1)的额外空间的排序),于是在从后向前扫描过程当中,须要反复把已排序元素逐步向后挪位,为最新元素提供插入空间。
若是你会打扑克牌,你在抓牌整理牌时,已经无心识地用到了插入排序。
通常来讲,插入排序都采用in-place在数组上实现。具体算法描述以下:
let insertSort = arr => { for (let i = 1; i < arr.length; i++) { let key = arr[i]; let j = i - 1; while (j >= 0 && arr[j] > key) { arr[j + 1] = arr[j]; j--; } arr[j + 1] = key; } return arr; };
平均时间复杂度: T(n) = O(n²)
最坏时间复杂度: T(n) = O(n²)
:输入数组按降序排列(彻底逆序)
最好时间复杂度: T(n) = O(n)
:输入数组按升序排列(基本有序)
空间复杂度: O(1)
稳定性:稳定
1959年Shell发明; 第一个突破O(n²)的排序算法;是简单插入排序的改进版;它与插入排序的不一样之处在于,它会优先比较距离较远的元素。希尔排序又叫缩小增量排序
该方法实质上是一种分组插入方法,希尔排序是基于插入排序的如下两点性质而提出改进方法的:
先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,具体算法描述:
1. 选择一个增量序列t1,t2,…,tk,其中ti>tj,tk=1; 2. 按增量序列个数k,对序列进行k 趟排序; 3. 每趟排序,根据对应的增量ti,将待排序列分割成若干长度为m 的子序列,分别对各子表进行直接插入排序。仅增量因子为1 时,整个序列做为一个表来处理,表长度即为整个序列的长度。
let shellSort = function (arr) { let len = arr.length, temp, gap = 1; // 动态定义间隔序列 while (gap < len / 5) { gap = gap * 5 + 1; } for (gap; gap > 0; gap = Math.floor(gap / 5)) { for (let i = gap; i < len; i++) { temp = arr[i]; for (let j = i - gap; j >= 0 && arr[j] > temp; j -= gap) { arr[j + gap] = arr[j]; } arr[j + gap] = temp; } } return arr; }
T(n) = O(n^1.5)
T(n) = O(nlog²n)
O(1)
不稳定
,因为屡次插入排序,咱们知道一次插入排序是稳定的,不会改变相同元素的相对顺序,但在不一样的插入排序过程当中,相同的元素可能在各自的插入排序中移动,最后其稳定性就会被打乱,因此shell排序是不稳定的。Shell排序的执行时间依赖于增量序列,好的增量序列的共同特征:
① 最后一个增量必须为1;
② 应该尽可能避免序列中的值(尤为是相邻的值)互为倍数的状况。
有人经过大量的实验,给出了较好的结果:当n较大时,比较和移动的次数约在nl.25到1.6n1.25之间。
可是Shell排序的时间性能显然优于直接插入排序,希尔排序的时间性能优于直接插入排序的缘由:
所以,希尔排序在效率上较直接插入排序有较大的改进。
不须要大量的辅助空间,和归并排序同样容易实现。希尔排序是基于插入排序的一种算法, 在此算法基础之上增长了一个新的特性,提升了效率。希尔排序没有快速排序算法快 O(n(logn)),所以中等大小规模表现良好,对规模很是大的数据排序不是最优选择。可是比O(n²)复杂度的算法快得多。而且希尔排序很是容易实现,算法代码短而简单。 此外,希尔算法在最坏的状况下和平均状况下执行效率相差不是不少,与此同时快速排序在最坏的状况下执行的效率会很是差。专家们提倡,几乎任何排序工做在开始时均可以用希尔排序,若在实际使用中证实它不够快,再改为快速排序这样更高级的排序算法
和选择排序同样,归并排序的性能不受输入数据的影响,但表现比选择排序好的多,由于始终都是O(n log^n)的时间复杂度。代价是须要额外的内存空间。
归并排序是创建在归并操做上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个很是典型的应用。归并排序是一种稳定的排序方法。将已有序的子序列合并,获得彻底有序的序列;即先使每一个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为2-路归并。
归并排序算法思想:分而治之:
let merge = (left, right) => { let result = []; while (left.length && right.length) { if (left[0] <= right[0]) { result.push(left.shift()); } else { result.push(right.shift()); } } while (left.length) { result.push(left.shift()); } while (right.length) { result.push(right.shift()); } return result; } //采用自上而下的递归方法 let mergeSort = arr => { let len = arr.length; if (len < 2) { return arr; } let middle = Math.floor(len / 2), left = arr.slice(0, middle), right = arr.slice(middle); return merge(mergeSort(left), mergeSort(right)); }
T(n) = O(nlogn)
T(n) = O(nlogn)
T(n) = O(n)
O(n)
,归并排序须要一个与原数组相同长度的数组作辅助来排序稳定
快速排序的名字起的是简单粗暴,由于一听到这个名字你就知道它存在的意义,就是快,并且效率高! 它是处理大数据最快的排序算法之一了。
快速排序的基本思想:经过一趟排序将待排记录分隔成独立的两部分,其中一部分记录的关键字均比另外一部分的关键字小,则可分别对这两部分记录继续进行排序,以达到整个序列有序。
快速排序使用分治法来把一个串(list)分为两个子串(sub-lists)。具体算法描述以下:
方法一:
let quickSort = (array, left, right) => { if (Array.isArray(array) && typeof left === 'number' && typeof right === 'number') { if (left < right) { let x = array[right], i = left - 1, temp; for (let j = left; j <= right; j++) { if (array[j] <= x) { i++; temp = array[i]; array[i] = array[j]; array[j] = temp; } } quickSort(array, left, i - 1); quickSort(array, i + 1, right); } return array; } else { return 'array is not an Array or left or right is not a number!'; } }
方法二
let quickSort2 = arr => { if (arr.length < 2) { return arr; } let pivotindex = Math.floor(arr.length / 2); let pivot = arr.splice(pivotindex, 1)[0]; let left = []; let right = []; for (let i = 0; i < arr.length; i++) { if (arr[i] < pivot) { left.push(arr[i]); } else { right.push(arr[i]); } } return quickSort2(left).concat([pivot], quickSort2(right)); };
T(n) = O(nlogn)
,快速排序最优的状况就是每一次取到的元素都恰好平分整个数组T(n) = O(n²)
,最差的状况就是每一次取到的元素就是数组中最小/最大的,这种状况其实就是冒泡排序了(每一次都排好一个元素的顺序)T(n) = O(nlogn)
不稳定
改进思路:改进选取枢轴的方法
《算法导论(第二版)》
) P111 第九章中位数和顺序统计学:在平均状况下,任何顺序统计量(特别是中位数)均可以在线性时间内获得。其余改进思路:
堆排序能够说是一种利用堆的概念来排序的选择排序。
堆排序(Heapsort)是指利用堆这种数据结构所设计的一种排序算法。堆积是一个近似彻底二叉树的结构,并同时知足堆积的性质:即子结点的键值或索引老是小于(或者大于)它的父节点。
具体算法描述以下:
/*方法说明:堆排序 @param array 待排序数组*/ function heapSort(array) { console.time('堆排序耗时'); if (Object.prototype.toString.call(array).slice(8, -1) === 'Array') { //建堆 var heapSize = array.length, temp; for (var I = Math.floor(heapSize / 2) - 1; I >= 0; I—) { heapify(array, I, heapSize); } //堆排序 for (var j = heapSize - 1; j >= 1; j—) { temp = array[0]; array[0] = array[j]; array[j] = temp; heapify(array, 0, —heapSize); } console.timeEnd('堆排序耗时'); return array; } else { return 'array is not an Array!'; } } /*方法说明:维护堆的性质 @param arr 数组 @param x 数组下标 @param len 堆大小*/ function heapify(arr, x, len) { if (Object.prototype.toString.call(arr).slice(8, -1) === 'Array' && typeof x === 'number') { var l = 2 * x + 1, r = 2 * x + 2, largest = x, temp; if (l < len && arr[l] > arr[largest]) { largest = l; } if (r < len && arr[r] > arr[largest]) { largest = r; } if (largest != x) { temp = arr[x]; arr[x] = arr[largest]; arr[largest] = temp; heapify(arr, largest, len); } } else { return 'arr is not an Array or x is not a number!'; } }
调堆:O(h)
建堆:O(n)
循环调堆:O(nlogn)
总运行时间T(n) = O(nlogn) + O(n) = O(nlogn)。对于堆排序的最好状况与最坏状况的运行时间,由于最坏与最好的输入都只是影响建堆的运行时间O(1)或者O(n),而在整体时间中占重要比例的是循环调堆的过程,即O(nlogn) + O(1) =O(nlogn) + O(n) = O(nlogn)。所以最好或者最坏状况下,堆排序的运行时间都是O(nlogn)。并且堆排序仍是 原地算法(in-place algorithm) 。
T(n) = O(nlogn)
T(n) = O(nlogn)
T(n) = O(nlogn)
O(1)
不稳定
文章最后再对七大经典排序算法性能分析作一次小结,加深记忆。
稳定的排序:冒泡排序,插入排序,归并排序
不稳定的排序:选择排序,堆排序,快速排序,希尔排序
平均时间复杂度T(n) = O(nlogn)
:希尔排序,归并排序,快速排序,堆排序
平均时间复杂度T(n) = O(n²)
:冒泡排序,简单选择排序,插入排序
最好时间复杂度T(n) = O(n)
:冒泡排序,插入排序
最好时间复杂度T(n) = O(nlogn)
:归并排序,快速排序,堆排序
最好时间复杂度T(n) = O(n²)
:简单选择排序
最坏时间复杂度T(n) = O(nlogn)
:归并排序,堆排序
最坏时间复杂度T(n) = O(n²)
:冒泡排序,简单选择排序,插入排序,快速排序
空间复杂度O(1)
:冒泡排序,简单选择排序,插入排序,希尔排序,堆排序
空间复杂度O(n)
:归并排序
空间复杂度O(nlogn)
:快速排序
推荐阅读:
【专题:JavaScript进阶之路】
TCP三次握手和四次挥手
AJAX原理及常见面试题
ES6 尾调用和尾递归
JavaScript之函数柯理化
浅谈 MVC 和 MVVM 模型
我是Cloudy,年轻的前端攻城狮一枚,爱专研,爱技术,爱分享。
我的笔记,整理不易,感谢关注、阅读、点赞和收藏。
文章有任何问题欢迎你们指出,也欢迎你们一块儿交流各类技术问题!
最后,Base上海,正在寻找新的工做机会中(^-^),提早祝你们新年快乐!