俗话说金三银四 金九银十,立刻又到了求职跳槽的黄金季。可是今年的这种大环境下,前端岗位的竞争势必比往日更加激烈。javascript
在现在的面试过程当中,算法是经常被考察的知识点,而排序做为算法中比较基础的部分,被面试官要求当场手写几种排序算法也不算是过度的要求。html
因此最近将十种常见的排序算法整理以下,并附上一些常见的优化方法以及一些对应的leetcode(传送门) 题目,建议你们能够申请个帐号刷起来,毕竟看明白了跟可以写出来而且经过LeetCode全部的 case 是两码事😂,但愿能够对刚接触算法以及最近须要参加面试的小伙伴有一点帮助。前端
毕竟手里有粮 内心不慌(逃~java
想看源码戳这里,读者能够 Clone 下来本地跑一下。BTW,文章配合源码体验更棒哦~~~git
最后,限于我的能力,如过在阅读过程当中遇到问题或有更好的优化方法,能够:github
我都会看到并处理,欢迎Star,点赞,您的支持是我写做最大的动力。面试
排序算法的稳定性: 排序先后两个相等的数相对位置不变,则算法稳定。算法
时间复杂度: 简单的理解为一个算法执行所耗费的时间,通常使用大O符号表示法,详细解释见时间复杂度api
空间复杂度: 运行完一个程序所需内存的大小。数组
常见算法的复杂度(图片来源于网络)
/** * 按照正序比较并交换数组中的两项 * * @param {Array} ary * @param {*} x * @param {*} y */ function swap(ary, x, y) { if (x === y) return var temp = ary[x] ary[x] = ary[y] ary[y] = temp } 复制代码
冒泡排序是一种简单的排序算法。它重复地走访过要排序的数列,一次比较两个元素,若是他们的顺序错误就把他们交换过来。走访数列的工做是重复地进行直到没有再须要交换,也就是说该数列已经排序完成。这个算法的名字由来是由于越小的元素会经由交换慢慢“浮”到数列的顶端。
算法步骤: 假设咱们最终须要的是依次递增的有序数组
function bubbleSort1(ary) { var l = ary.length for (var i = 0; i < l-1; i++) { for (var j = 0; j <= l-2; j++) { if (ary[j] > ary[j + 1]) { swap(ary, j, j + 1) } } } return ary } 复制代码
优化: 上述排序对于一个长度为 n 的数组排序须要进行 n * n 次排序。(内外两层循环次数都是 n ) 能够预见到的是,每进行一轮冒泡,从数组末尾起有序部分长度就会加一,这就意味着数组末尾的有序数组进行比较的操做是无用的。
改进后的算法以下:
function bubbleSort2(ary) { var l = ary.length for (var i = l - 1; i >= 0; i--) { // 优化的部分 arr[i]及以后的部分都是有序的 for (var j = 0; j < i; j++) { if (ary[j] > ary[j + 1]) { swap(ary, j, j + 1) } } } return ary } 复制代码
优化点:对于一些比较极限状况的处理,举一个比较极限的例子,假如给定的数组已是有序数组了,那么 bubbleSort1 和 bubbleSort2 仍是傻傻的去走完预约的次数 分别为 n*n 和 n!。 固然这种状况并不容易遇到,可是在排序的后段部分很容易遇到的是,理论上应该是未排序的部分其实已是有序的了,咱们须要对这种状况进行甄别并处理。 引入一个 swapedFlag ,若是在排序的上一步没有进入内层循环,那么代表剩余元素都是有序的,排序完成。
优化后的代码以下:
/** * 冒泡排序 优化 * * @param {Array} ary * @returns */ function bubbleSort3(ary) { var l = ary.length var swapedFlag for (var i = l - 1; i >= 0; i--) { swapedFlag = false for (var j = 0; j < i; j++) { if (ary[j] > ary[j + 1]) { swapedFlag = true swap(ary, j, j + 1) } } if (!swapedFlag) { break } } return ary } 复制代码
选择排序是先在数据中找出最大或最小的元素,放到序列的起始;而后再从余下的数据中继续寻找最大或最小的元素,依次放到排序序列中,直到全部数据样本排序完成。 复杂度分析:很显然,选择排序也是一个费时的排序算法,不管什么数据,都须要O(n*n) 的时间复杂度,不适宜大量数据的排序。
算法步骤: 初始状态为n的无序区(数组)可通过n-1趟直接选择排序获得有序结果
function selectSort(ary) { var l = ary.length var minPos for (var i = 0; i < l - 1; i++) { minPos = i for (var j = i + 1; j < l; j++) { if (ary[j] - ary[minPos] < 0) { minPos = j } } swap(ary, i, minPos) } return ary } 复制代码
插入排序是先将待排序序列的第一个元素看作一个有序序列,把第二个元素到最后一个元素当成是未排序序列;而后从头至尾依次扫描未排序序列,将扫描到的每一个元素插入有序序列的适当位置,直到全部数据都完成排序;若是待插入的元素与有序序列中的某个元素相等,则将待插入元素插入到相等元素的后面。
算法步骤:
function insertionSort1(arr) { var l = arr.length; var preIndex, current; for (var i = 1; i < l; i++) { preIndex = i - 1; current = arr[i]; while (preIndex >= 0 && arr[preIndex] > current) { arr[preIndex + 1] = arr[preIndex]; preIndex--; } arr[preIndex + 1] = current; } return arr; } 复制代码
优化思路:
简单介绍下二分法: 二分查找法,是一种在有序数组中查找某一特定元素的搜索算法。搜素过程从数组的中间元素开始,若是中间元素正好是要查找的元素,则搜素过程结束;若是某一特定元素大于或者小于中间元素,则在数组大于或小于中间元素的那一半中查找,并且跟开始同样从中间元素开始比较。若是在某一步骤数组为空,则表明找不到。这种搜索算法每一次比较都使搜索范围缩小一半。
注: 准备面试的同窗可以理解并记忆如下一种便可,排序二叉树和链表的实现限于篇幅就不细说,准备之后写数据结构时再详细介绍,本篇介绍下使用二分法优化拆入排序的思路:
/** * 插入排序 * * @param {*} ary * @returns {Arrray} 排序完成的数组 */ function insertSort2(ary) { return ary.reduce(insert, []) } /** * 使用二分法完成查找插值位置,并完成插值操做。 * 时间复杂度 logN * @param {*} sortAry 有序数组部分 * @param {*} val * @returns */ function insert(sortAry, val) { var l = sortAry.length if (l == 0) { sortAry.push(val) return sortAry } var i = 0, j = l, mid //先判断是否为极端值 if (val < sortAry[i]) { return sortAry.unshift(val), sortAry } if (val >= sortAry[l - 1]) { return sortAry.push(val), sortAry } while (i < j) { mid = ((j + i) / 2) | 0 //结束条件 等价于j - i ==1 if (i == mid) { break } if (val < sortAry[mid]) { j = mid } if (val == sortAry[mid]) { i = mid break } //结束条件 统一c处理对外输出i if (val > sortAry[mid]) { i = mid } } var midArray = [val] var lastArray = sortAry.slice(i + 1) sortAry = sortAry .slice(0, i + 1) .concat(midArray) .concat(lastArray) return sortAry } 复制代码
归并排序是利用归并的思想实现的排序方法,该算法采用经典的分治(divide-and-conquer)策略(分治法将问题分(divide)成一些小的问题而后递归求解,而治(conquer)的阶段则将分的阶段获得的各答案"修补"在一块儿,即分而治之)。
稳定性分析:归并排序严格遵循从左到右或从右到左的顺序合并子数据序列, 它不会改变相同数据之间的相对顺序, 所以归并排序是一种稳定的排序算法.
算法步骤:
// 采用自上而下的递归方法 function mergeSort(ary) { if (ary.length < 2) { return ary.slice() } var mid = Math.floor(ary.length / 2) var left = mergeSort(ary.slice(0, mid)) var right = mergeSort(ary.slice(mid)) var result = [] while (left.length && right.length) { if (left[0] <= right[0]) { result.push(left.shift()) } else { result.push(right.shift()) } } result.push(...left, ...right) return result } 复制代码
堆排序是指利用堆这种数据结构所设计的一种排序算法。堆积结构具备以下特色:即子结点的键值老是小于(或者大于)它的父节点,据此可分为如下两类:
算法步骤:
/** * 聚堆:将数组中的某一项做为堆顶,调整为最大堆。 * 把在堆顶位置的一个可能不是堆,但左右子树都是堆的树调整成堆。 * * @param {*} ary 待排序数组 * @param {*} topIndex 当前处理的堆的堆顶 * @param {*} [endIndex=ary.length - 1] 数组的末尾边界 */ function reheap(ary, topIndex, endIndex = ary.length - 1) { if (topIndex > endIndex) { return } var largestIndex = topIndex var leftIndex = topIndex * 2 + 1 var rightIndex = topIndex * 2 + 2 if (leftIndex <= endIndex && ary[leftIndex] > ary[largestIndex]) { largestIndex = leftIndex } if (rightIndex <= endIndex && ary[rightIndex] > ary[largestIndex]) { largestIndex = rightIndex } if (largestIndex != topIndex) { swap(ary, largestIndex, topIndex) reheap(ary, largestIndex, endIndex) } } /** * 将数组调整为最大堆结构 * * @param {*} ary * @returns */ function heapify(ary) { for (var i = ary.length - 1; i >= 0; i--) { reheap(ary, i) } return ary } /** * 堆排序 * * @param {*} ary * @returns */ function heapSort(ary) { heapify(ary) for (var i = ary.length - 1; i >= 1; i--) { swap(ary, 0, i) reheap(ary, 0, i - 1) } return ary } 复制代码
快速排序使用分治法策略来把一个数组分为两个子数组。首先从数组中挑出一个元素,并将这个元素称为「基准」,英文pivot。从新排序数组,全部比基准值小的元素摆放在基准前面,全部比基准值大的元素摆在基准后面(相同的数能够到任何一边)。在这个分区结束以后,该基准就处于数组的中间位置。这个称为分区(partition)操做。以后,在子序列中继续重复这个方法,直到最后整个数据序列排序完成。
注意: 在 js 中实现快排中最耗费时间的就是交换,本例子中哨兵的元素是随机取得的,而上面动图中老是的取数组中的第一个值做为哨兵(pivot),那么考虑一种极限状况,在 [9,8,7,6,5,4,3,2,1] 重中例子中使用就地排序就算法复杂度就会变成 n*n。 本例中的哨兵是从数组中随机抽取的,我的认为比取首元素的方案更优。
应用: 取前K大元素、求中位数 、leetcode
嗯,先整一个粗暴版本稍微了解下快排的基本思路:
//快排粗暴版本 function quickSort1(ary) { if (ary.length < 2) { return ary.slice() } var pivot = ary[Math.floor(Math.random() * ary.length)] var left = [] var middle = [] var right = [] for (var i = 0; i < ary.length; i++) { var val = ary[i] if (val < pivot) { left.push(val) } if (val === pivot) { middle.push(val) } if (val > pivot) { right.push(val) } } return quickSort1(left).concat(middle, quickSort(right)) } 复制代码
这个是推荐掌握的,很重要(敲黑板)
算法步骤
function quickSort2(ary, comparator = (a, b) => a - b) { return partition(ary, comparator) } function partition(ary, comparator, start = 0, end = ary.length - 1, ) { if (start >= end) { return } var pivotIndex = Math.floor(Math.random() * (end - start + 1) + start) var pivot = ary[pivotIndex] swap(ary, pivotIndex, end) for (var i = start - 1, j = start; j < end; j++) { if (comparator(ary[j], pivot) < 0) { i++ swap(ary, i, j) } } swap(ary, i + 1, end) partition(ary, comparator, start, i) partition(ary, comparator, i + 2, end) return ary } 复制代码