常见排序算法
冒泡排序
冒泡排序是一种交换排序,基本思想是:两两比较相邻记录的关键字,如果反序则交换,直到没有反序的记录为止。
算法步骤
- 比较相邻的元素。如果第一个比第二个大,就交换他们两个。
- 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大的数。
- 针对所有的元素重复以上的步骤,除了最后一个。
- 持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。
动图演示
———— 动图来自《菜鸟教程》
说明:绿色表示当前正在比较的两个相邻元素;橘黄色表示已排完序的元素,不再参与后续的比较
代码实现(JS)
1 | var arr = [3, 44, 38, 5, 47, 15, 36, 26, 27, 2, 46, 4, 19, 50, 48]; |
代码与图片配合食用更加~
选择排序
选择排序是一种简单直观的排序算法,无论什么数据进去都是 O(n²) 的时间复杂度。所以用到它的时候,数据规模越小越好。唯一的好处可能就是不占用额外的内存空间了吧。
算法步骤
- 首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置。
- 再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。
- 重复第2步,直到所有元素均排序完毕。
动图演示
代码实现(JS)
1 | var arr = [3, 44, 38, 5, 47, 15, 36, 26, 27, 2, 46, 4, 19, 50, 48]; |
配图:
插入排序
插入排序的原理应该是最容易理解的,因为只要你打过扑克牌就应该能秒懂。插入排序是一种最简单直观的排序算法,它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。
算法步骤
- 从第一个元素开始,该元素可以认为已经被排序;
- 取出下一个元素,在已经排序的元素序列中从后向前扫描;
- 如果该元素(已排序)大于新元素,将该元素移到下一位置;
- 重复步骤3,直到找到已排序的元素小于或者等于新元素的位置;
- 将新元素插入到该位置后;
- 重复步骤2~5。
动图演示
代码实现(JS)
1 | var arr = [3, 44, 38, 5, 47, 15, 36, 26, 27, 2, 46, 4, 19, 50, 48]; |
希尔排序
希尔排序也是一种插入排序,它是简单插入排序的一个改进版,也称为缩小增量排序,同时该算法是冲破O(n2)的第一批算法之一。本文会以图解的方式详细介绍希尔排序的基本思想及其代码实现。
希尔排序是把记录按下标的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至1时,整个文件恰被分成一组,算法便终止。
算法步骤(示例)
在此我们选择增量 gap=数组长度length/2
,缩小增量继续以 gap = gap/2
的方式,这种增量选择我们可以用一个序列来表示:{n/2,(n/2)/2...1}
,称为增量序列。当然,希尔排序的增量序列有很多种,这里采用的是比较常用的一种作为示例:
在上面这幅图中:
初始时,有一个大小为 10 的无序序列。
在第一趟排序中,我们设 gap1 = 10 / 2 = 5,即相隔距离为 5 的元素组成一组,可以分为 5 组。接下来,按照直接插入排序的方法对每个组进行排序。
在第二趟排序中,我们把上次的 gap 缩小一半,即 gap2 = gap1 / 2 = 2 (取整)。这样每相隔距离为 2 的元素组成一组,可以分为 2 组。按照直接插入排序的方法对每个组进行排序。
在第三趟排序中,再次把 gap 缩小一半,即gap3 = gap2 / 2 = 1。 这样相隔距离为 1 的元素组成一组,即只有一组。按照直接插入排序的方法对每个组进行排序。此时,排序已经结束。
注意:图中有两个相等数值的元素 5 和 5 。我们可以清楚的看到,在排序过程中,两个元素位置交换了。
所以,希尔排序是不稳定的算法。
代码演示(JS)
1 | var arr = [3, 44, 38, 5, 47, 15, 36, 26, 27, 2, 46, 4, 19, 50, 48]; |
归并排序
归并排序是利用归并的思想实现的排序方法,该算法采用经典的分治策略(分治法将问题分成一些小的问题然后递归求解,而治的阶段则将分的阶段得到的各答案”修补”在一起,即分而治之)。
可以看到这种结构很像一棵完全二叉树,下面的归并排序代码将会采用递归去实现(你也可采用迭代的方式去实现)。分阶段可以理解为就是递归拆分子序列的过程,递归深度为logn。
算法步骤
- 申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列
- 设定两个指针,最初位置分别为两个已经排序序列的起始位置
- 比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置
- 重复步骤 3 直到某一指针达到序列尾
- 将另一序列剩下的所有元素直接复制到合并序列尾
图片演示
图片来自博客《图解排序算法之归并排序》
代码实现
1 | var arr = [3, 44, 38, 5, 47, 15, 36, 26, 27, 2, 46, 4, 19, 50, 48]; |
快速排序
快速排序的基本思想是:通过一趟排序将待排记录分割成独立的两部分,其中一部分记录的元素均比另一部分记录的元素小,继而再分别对这两部分记录递归的进行同样的排序操作。
算法步骤
- 把数组中第一个元素当做一个基准值,称为“基准”(Pivot)
- 重新排序数列,把所有比基准值小的元素摆放在基准前面,所有比基准值大元素摆放在基准后面。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作;
- 递归地把小于基准值元素的子数列和大于基准值元素的子数列排序;
分区(Partition)思路
首先把未排序数组的第一个(最左边)元素设置为基准,把它的位置叫做 l
:
然后依次向后查看所有元素,在查看过程中,不断的调整后面元素的位置,使得后面的元素分为两部分,一部分都小于 v ;一部分都大于 v 。
我们用 j
来不断记录这两部分的分割线位置。而 e 就是我们要判断的下一个元素,用索引 i
来表示,i
会遍历每一个元素,来看该如何调整这个元素。
在下图中,我们用 arr[l+1 ... j]
来表示小于 v 的橙色部分,用 arr[j+1 ... i-1]
来表示大于 v 的紫色部分。
接下来就要看如何来调整下一个元素 e 的位置。分情况讨论:
当 e 大于 v 时,我们直接让 e 融入大于 v 的部分,并让 i++
当 e 小于 v 时,我们让 e 和大于 v 部分的第一个元素交换位置。
把 e 融入到小于 v 的部分。此时就需要让分隔线的索引位置 j++
,相应的,索引 i
的位置也要 i++
以便查看下一个元素。
以这样的步骤我们就能遍历完整个数组,如下图所示
现在还差最后一个步骤,那就是把基准值 v ,与小于 v 部分的最后一个元素交换位置。此刻 v 左边都小于它,v 右边都大于它,而 j
指向的就是基准值所在的位置。这样我们就完成了分区(Partition)操作。
代码实现
1 | var arr = [3, 44, 38, 5, 47, 15, 36, 26, 27, 2, 46, 4, 19, 50, 48]; |
计数排序
计数排序不是基于比较的排序算法,其核心在于将输入的数据值转化为键存储在额外开辟的数组空间中。 作为一种线性时间复杂度的排序,计数排序要求输入的数据必须是有确定范围的整数。
算法步骤
- 找出待排序的数组中最大和最小的元素;
- 统计数组中每个值为 i 的元素出现的次数,存入数组 Count 的第 i 项;
- 对所有的计数累加(从 Count 中的第一个元素开始,每一项和前一项相加);
- 反向填充目标数组:将每个元素 i 放在新数组的第 Count(i) 项,每放一个元素就将 Count(i) 减去1。
代码实现
1 | var arr = [3, 44, 38, 5, 47, 15, 36, 26, 27, 2, 46, 4, 19, 50, 48]; |