这篇文章讲的很好了, 我只是重新归纳总结一下
排序算法术语
- 稳定 :如果a原本在b前面,而a=b,排序之后a仍然在b的前面;
- 不稳定 :如果a原本在b的前面,而a=b,排序之后a可能会出现在b的后面;
- 内排序 :所有排序操作都在内存中完成;
- 外排序 :由于数据太大,因此把数据放在磁盘中,而排序通过磁盘和内存的数据传输才能进行;
常见排序算法对比
比较排序和非比较排序的特点
比较排序
常见的快速排序、归并排序、堆排序、冒泡排序等属于比较排序。
在排序的最终结果里,元素之间的次序依赖于它们之间的比较。每个数都必须和其他数进行比较,才能确定自己的位置 。
在冒泡排序之类的排序中,问题规模为n,又因为需要比较n次,所以平均时间复杂度为O(n²)。在归并排序、快速排序之类的排序中,问题规模通过分治法消减为logN次,所以时间复杂度平均O(nlogn)。
优势
比较排序的优势是,适用于各种规模的数据,也不在乎数据的分布,都能进行排序。可以说,比较排序适用于一切需要排序的情况。
非比较排序
计数排序、基数排序、桶排序则属于非比较排序。
非比较排序是通过确定每个元素之前,应该有多少个元素来排序。针对数组arr,计算arr[i]之前有多少个元素,则唯一确定了arr[i]在排序后数组中的位置。
非比较排序只要确定每个元素之前的已有的元素个数即可,所有一次遍历即可解决。算法时间复杂度O(n)。
优势
非比较排序时间复杂度底,但由于非比较排序需要占用空间来确定唯一位置。所以对数据规模和数据分布有一定的要求。
冒泡排序
冒泡排序 是一种简单的排序算法。它重复地走访过要排序的数列,一次比较两个元素,如果它们的顺序错误就把它们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端。
算法步骤
- 一次遍历, 完成一个数字的排序
- 从开头开始, 位置1和位置2比较, 大的元素交换到后一个位置, 然后位置2和位置3比较, 同样大的元素交换到后一个位置, 一次类推
- 第一次遍历, 最大的数就会再最后, 第二次遍历, 第二大的数就在倒数第二位.
- 重复遍历, 直到排序完成
选择排序
选择排序(Selection-sort) 是一种简单直观的排序算法。它的工作原理:首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。一次过程中, 比较多次, 但是只会交换一次.
算法步骤
- 如果是需要升序排列的话
- 假设第一个元素是最小的, 然后向后遍历数组, 遇见比第一个元素小的, 就将当前的索引变成小的元素, 就是有一个指针指向的是最小的元素, 然后继续后面的元素比较, 直到最后, 然后将当前指针对应的最小元素和第一个数字进行交换. 过程中发生了多次比较, 只有一次交换
- 然后再从第二个元素开始, 寻找到最小的元素, 和第二个位置的元素进行交换.
- 逐一假设, 直到排序完成.
插入排序
插入排序(Insertion-Sort) 的算法描述是一种简单直观的排序算法。它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。插入排序在实现上,通常采用in-place排序(即只需用到O(1)的额外空间的排序),因而在从后向前扫描过程中,需要反复把已排序元素逐步向后挪位,为最新元素提供插入空间。
算法步骤
- 假设第一个元素是已经排序了的
- 从第二个元素开始, 当前元素之前的元素都是已经排序完成了的, 将当前元素在有序的部分中, 从后往前比较, 每次比较完毕, 就可以决定这个有序元素是否有必要移动, 而不是等到最后再一起移动.
- 到第三个元素, 前两个元素就是有序的, 重复步骤2
- 遍历到数组最后, 排序完毕.
二分法 + 插入排序
插入排序在有序部分寻找位置的时候, 没必要一个个去比较, 可以通过二分法查找正确的位置, 虽然不会减少元素移动的次数, 但是比较的次数会减少.
希尔排序
希尔排序是希尔(Donald Shell) 于1959年提出的一种排序算法。希尔排序也是一种插入排序,它是简单插入排序经过改进之后的一个更高效的版本,也称为缩小增量排序,同时该算法是冲破O(n2)的第一批算法之一。它与插入排序的不同之处在于,它会优先比较距离较远的元素。希尔排序又叫缩小增量排序。
希尔排序是把记录按下表的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至1时,整个文件恰被分成一组,算法便终止。
算法步骤
(1) 选择初始增量, 以及增量的变化规则, 这个增量的选择是一个数学难题, 通常选择初始增量gap = 数组长度 / 2, 每次增量的变化规则 gap = gap / 2;
(2) gap为多少, 就代表多少个元素分成一组, 具体分成几组不限制, 在选择哪些元素为一组的时候, 跳跃间隔2进行选择, 例如位置在0, 2, 4的为一组
(3) 每一个组的元素, 按照插入排序进行一次调整. 当前gap下 所有元素调整完毕后, gap = gap / 2;
(4) 重新分组, 然后对每一组进行插入排序.
(5) 当gap = 1, 所有元素进行一次插入排序, 这个时候, 数组中的元素其实已经大部分都有序了, 需要调整的元素个数非常少, 这也是希尔排序前面那些步骤的原因.
归并排序
和选择排序一样,归并排序的性能不受输入数据的影响,但表现比选择排序好的多,因为始终都是O(n log n)的时间复杂度。代价是需要额外的内存空间。
归并排序 是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。归并排序是一种稳定的排序方法。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为2-路归并。
算法步骤
(1) 将原始数组不断对半拆分, 直到拆分成只有一个元素的时候, 开始合并
(2) 参与合并的两个数组一定是有序的, 按照有序数组合并的方式进行合并
(3) 最后返回的就是有序数组
快速排序
快速排序 的基本思想:通过一趟排序将待排记录分隔成独立的两部分,其中一部分记录的关键字均比另一部分的关键字小,则可分别对这两部分记录继续进行排序,以达到整个序列有序。
算法步骤
(1) 总体思想来说, 是选择一个基准值, 然后将数组分成两个部分, 左边的数组里的值, 全部小于基准, 右边数组里的值全部大于基准, 基准所在的位置就是正确的位置, 然后对于左右两个数组, 重新选择基准值, 重复这一过程, 直到数组大小为1.
(2) 可以选择数组中任一个数为基准值, 通常选择数组中的第一个元素作为基准值.
(3) 设置两个指针left和right, 分别指向数组的第二个位置和数组的最后位置. 然后右边指针先动(这样保证最后指针相遇的位置的元素, 一定是一个比基准元素小的元素, 这样可以顺利的进行交换, 如果左指针先动, 那么相遇的元素是一个比基准元素大的), 右指针不断比较, 一直移动, 直到遇见一个比基准元素小的数字, 然后右指针停止, 左指针移动, 左指针在遇到右边指针之前, 如果遇见了一个比基准大的数, 左右指针交换元素.
(4) 元素交换完毕后, 右指针继续移动, 直到左右指针相遇, 相遇位置的元素和基准元素完成交换, 就完成了一轮快排.
(5) 基准元素分割开的左右两个数组重复1-4步骤.
堆排序
堆排序(Heapsort) 是指利用堆这种数据结构所设计的一种排序算法。堆积是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。
算法步骤
(1) 先将无序的元素, 构建成堆, 假设为大顶堆
(2) 此时堆顶的元素为最大值, 将堆顶的元素与堆的最后一个元素交换, 触发一次堆的调整.
(3) 然后将堆顶的元素与倒数第二个元素交换, 触发一次堆的调整.
(4) 从后往前遍历, 直到最后一个元素, 不用交换了, 此时数组已经有序.
计数排序
计数排序 的核心在于将输入的数据值转化为键存储在额外开辟的数组空间中。 作为一种线性时间复杂度的排序,计数排序要求输入的数据必须是有确定范围的整数。
计数排序(Counting sort) 是一种稳定的排序算法。计数排序使用一个额外的数组C,其中第i个元素是待排序数组A中值等于i的元素的个数。然后根据数组C来将A中的元素排到正确的位置。
因为元素的值会作为新数组的索引, 因此它只能对整数进行排序。
算法步骤
(1) 新建一个数组temp, 数组的大小由无序数组中元素最大值和最小值决定, 如果元素最大值为10000, 最小值为100, 那么就需要开辟一个(10000 - 100)大小的数组
(2) 遍历一遍无序数组, 每个元素的值作为temp数组的索引, 出现一次, temp[元素值] += 1, 遍历完原数组, 排序也就完成了
(3) 遍历一遍temp数组, 如果数组值不为0, 就将该元素按顺序添加到待排序数组中, 出现了几次, 就放几个.
桶排序
桶排序 是计数排序的升级版。计数排序如果因为原始数据的分布很不均匀的话, 会浪费大量的空间.
它利用了函数的映射关系,将待排序数组中的每个数, 通过某种函数关系的映射放到一个桶中, 桶排序算法的高效与否的关键就在于这个映射函数的确定。
假设输入数据服从均匀分布,将数据分到有限数量的桶里,每个桶再分别排序(有可能再使用别的排序算法或是以递归方式继续使用桶排序进行排序. 这样相比于计数排序, 不会浪费空间.
算法步骤
(1) 确定函数关系, 例如对原始数据对每个数字除10, 决定在哪个桶, 例如9在桶0, 15在桶1, 99在桶9, 桶几就代表了在桶数组的哪个索引处, 每个索引处可以用一个链表来保存该桶里的所有数据
(2) 桶和桶之间的顺序性已经排序好了, 例如桶1中的数一定全部大于桶0.
(3) 只需要对每个桶里对数字进行排序, 可以采用任何一种排序方法.
(4) 每个桶里的元素排序完毕后, 再将所有桶的结果合并, 就可以得到排序结果.
基数排序
基数排序也是非比较的排序算法,对每一位进行排序,从最低位开始排序,复杂度为O(kn),为数组长度,k为数组中的数的最大的位数;
基数排序是按照低位先排序,然后收集;再按照高位排序,然后再收集;依次类推,直到最高位。有时候有些属性是有优先级顺序的,先按低优先级排序,再按高优先级排序。最后的次序就是高优先级高的在前,高优先级相同的低优先级高的在前。基数排序基于分别排序,分别收集,所以是稳定的。
算法步骤
(1) 基数排序和你当前待排序数组中, 数字的位数有关, 如果是整数的话, 遍历一遍待排序数组, 得到最大值, 最大值的位数, 就是基数排序的轮数. 假设最大数的位数是2;
(2) 每一位数字的可能性都为0 - 9, 因此创建一个大小为10的数组temp. 每个数组的位置上放一个队列结构
(3) 从个位开始, 计算所有待排序元素的个位值是多少, 添加进数组相应位置的队列中.
(4) 原数组遍历完毕后, 遍历temp数组, 将队列中的元素按顺序弹出, 构成了一个新的数组.
(5) 然后从10位开始, 计算所有待排序元素的十位值是多少, 添加进数组相应位置的队列中.
(6) 将temp数组中, 所有队列中的元素依次弹出.
(7) 按照最大数的位数, 完成两轮后, 排序完毕