排序算法概述

1,414 阅读3分钟

1 概述

  在计算机科学与数学中,一个排序算法(英语:Sorting algorithm)是一种能将一串资料依照特定排序方式进行排列的一种算法。最常用到的排序方式是数值顺序以及字典顺序。有效的排序算法在一些算法(例如搜索算法与合并算法)中是重要的,如此这些算法才能得到正确解答。排序算法也用在处理文字资料以及产生人类可读的输出结果。基本上,排序算法的输出必须遵守下列两个原则:

  (1) 输出结果为递增序列(递增是针对所需的排序顺序而言);

  (2) 输出结果是原输入的一种排列、或是重组;

  虽然排序算法是一个简单的问题,但是从计算机科学发展以来,在此问题上已经有大量的研究。举例而言,冒泡排序在1956年就已经被研究。虽然大部分人认为这是一个已经被解决的问题,有用的新算法仍在不断的被发明。(例子:图书馆排序在2004年被发表)。

2 分类

  在计算机科学所使用的排序算法通常依以下标准分类:

  (1) 计算的时间复杂度(最差、平均、和最好性能),依据列表(list)的大小(n)。一般而言,好的性能是O(n log n)(大O符号),坏的性能是O(n^{2})。对于一个排序理想的性能是O(n),但平均而言不可能达到。基于比较的排序算法对大多数输入而言至少需要O(n log n);

  (2) 内存使用量(以及其他电脑资源的使用);

  (3) 稳定性:稳定排序算法会让原本有相等键值的纪录维持相对次序。也就是如果一个排序算法是稳定的,当有两个相等键值的纪录R和S,且在原本的列表中R出现在S之前,在排序过的列表中R也将会是在S之前;

  (4) 排序的方法:插入、交换、选择、合并等等;

3 时间复杂度

3.1 概述

  时间复杂度,即执行当前算法所消耗的时间,通常用大O表示。

  以以下代码为例:

for(i=1; i<=n; ++i) {
   j = i;
   j++;
}

  我们来计算该代码的时间复杂度:

  (1) 假设每行代码的执行时间是一样的,我们用一颗粒时间来表示;

  (2) 第一行,“for(i=1; i<=n; ++i) {”会被执行n次,因此执行时间是n个颗粒时间;

  (3) 第二行,"j = i"会被执行n次,因此执行时间是n个颗粒时间;

  (4) 第三行,“j++”会被执行n次,因此执行时间是n个颗粒时间;

  (5) 第四行符号,暂时忽略;

  (6) 于是,总执行时间为n+n+n=3n次;

  而时间复杂度的计算有个规则是:将常量忽略。因此可以认为,以上代码的执行时间趋势是n次,注意,是“时间趋势”而不是真实的执行时间。

  最终得到以上代码的时间复杂度:T(n) = O(n)。

3.2 计算步骤和基本原则

  时间复杂度的计算一般遵循以下几个步骤:

  (1) 找出算法中的基本语句:算法中执行次数最多的语句就是基本语句,通常是最内层循环的循环体;

  (2) 计算基本语句的执行次数的数量级:只需计算基本语句执行次数的数量级,即只要保证函数中的最高次幂正确即可,可以忽略所有低次幂和最高次幂的系数。这样能够简化算法分析,使注意力集中在最重要的一点上:增长率;

  (3) 用大Ο表示算法的时间性能:将基本语句执行次数的数量级放入大Ο记号中;

  而用大O表示法,有几个原则:

  (1) 用常数1取代运行时间中的所有加法常数;

  (2) 只保留时间函数中的最高阶项;

  (3) 如果最高阶项存在,则省去最高阶项前面的系数,即,忽略常量;

  时间复杂度一般分为常数阶、对数阶、线性阶、线性对数阶、平方阶等。

3.3 常数阶O(1)

  消耗时间并不随着某个变量的增长而增长,如以下代码所示:

int i = 1;
int j = 2;
int k = 1 + 2;

  无论最终代码有多少行,因不会随着某个变量的增长而增长,因此认为时间复杂度为O(1)。

3.4 线性阶O(n)

  如以下代码所示:

for(i = 0; i < n; i++) {
   j = i;
   j++;
}

  执行时间由n决定,n有多大,执行时间就是多长,因此其时间复杂度为O(n)。

3.5 对数阶O(log n)

  如以下代码所示:

int i = 1;
while(i < n) {
    i = i * 2;
}

  基本语句为“i = i * 2”,假设运行x次后,i大于等于n,循环退出,即2的x次方大于等于n,那么x应为log2^n,因此时间复杂度为O(log n),称为对数阶时间复杂度。

3.6 线性对数阶O(n log n)

  如以下代码所示:

for (int m = 1; m < n; m++) {
   int i = 1;
   while (i <= n) {
      i = i * 2;
   }
}

  基本语句为“i = i * 2”,它运行的次数为n * log2^n次,用大O表示法表示为O(n log n),称为线性对数阶。

3.7 平方阶O(n^2)

  如以下代码所示:

int k = 0;
for (int i = 0; i < n; i++) {
   for (int j = 0; j < n; j++) {
      k++;
   }
}

  基本语句为“k++”,它运行的次数为n的2次方次,用大O表示法表示为,称为平方阶。

3.8 其他

  除以上时间复杂度为,还有O(m + n)时间复杂度(参考线性阶,即由两个不同的变量决定时间复杂度),O(n^3)、O(n^k)等其他时间复杂度算法,计算方式与以上时间复杂度算法保持一致。

4 空间复杂度

  整体上,时间复杂度与空间复杂度的算法类似,也是用大O表示,所以也会有O(1)复杂度,O(n)复杂度等,同样是看新建的空间与变量的关系,例如O(1)空间复杂度:

int i = 1;
int j = 2;
int k = 1 + 2;

  O(n)空间复杂度:

int j = 0;
int[] m = new int[n];
for (int i = 1; i <= n; ++i) {
   j = i;
   j++;
}

5 排序算法及其时空复杂度

  作者对排序算法进行了收集,并使用一句话对各种排序算法进行了描述,现总结如下:

排序算法描述时间复杂度
冒泡排序从第一个元素开始,相邻比较,把较大的数值替换到后面,直到最后一个元素,然后再从第一个元素开始,相邻比较,把较大的数值替换到后面,直到倒数第二个元素,依此类推O(n^2)
选择排序找到数组中的最小值放在第一位,次小的放在第二位,依此类推O(n^2)
插入排序假设第一个元素是有序数组,把第二个元素放在这个“有序数组”的正确位置,然后第一、二个元素变成有序数组,把第三个元素放到这个有序数组的正确位置,依此类推O(n^2)
希尔排序把数组分成length/2组,然后各组各自排序,然后把数组分成length/2/2组,然后各组各自排序,然后继续分成length/2/2/2组进行各自排序,直到length/2/2.../2=1O(n^1.3~2)
归并排序把数组分成平分成两组,再把分成的两组再各分成两组总共四组,再把分成的四组再各分成两组总共8组,当小分组中只有1个元素时开始对小分组排序,排序完成后合入父分组中再排序,直到合成的分组为原始分组时,排序完成O(n log n)
快速排序选定最后一个元素作为分界值,迭代数组,把小于分界值的往前排,大于分界值的往后排,使得分界值前的元素小于分界值,分界值后的元素大于等于分界值,然后把分界值左右分成两个小数组,重复分界处理,分界完后再拆分成小数组,直到拆无可拆时,数据已有序O(n log n)
堆排序先把数组分成一个堆(平衡二叉树),从最后一个树叶开始(最下一层、最右边),把较大的元素往树枝移,移完一轮后,把第一个元素跟最后一个元素替换,使得最后一个元素为数组中的最大值;然后,把最后一个元素排除,重复前面步骤,直到迭代到第一个元素O(n log n)
计数排序建一个空数组,数组长度可选为数组的最大值+1,或者最大值-最小值+1,然后迭代数组,把数组的值作为新数组的索引,计算每个元素出现的次数,最后迭代新数组,依序把新数组的索引写入原始数组,即可得到有序数组O(n + k)
桶排序将数组按一定的规则,分到多个桶里面,然后对桶里面的小数组排序,最后再组合即可得到有序数组;分桶的规则可以是元素除以10的n次方、2的n次方、(数组的最大值-最小值)/2等,分桶后会把较小的值放前面的桶,较大的值放后面的桶;桶内排序可以用其他排序算法,也可以继续使用桶排序O(n + k)
基数排序先找出数组中最长的数(因为会包含正数、负数,所以是最长而不是最大),然后先按个位,分成十个小数组,把这十个小数组合并,接下来按十位,分成十个小数组,再合并,依此类推,最后得到有序数组O(nk)
鸡尾酒排序找到数组中的最小值和最大值,将最小值放第一位、最大值放最后一位,再找数组中其余元素中的最小值、最大值,把最小值放第二位,最大值放倒数第二位,依此类推O(n^2)
二叉树排序先取出第一个元素放在树的根上,然后取第二个元素,小于根元素往左移,大于根元素往右移,然后取第三个元素、第四个元素等等,按照小于往左,大于往右的原则,把数组放入树中,然后使用中序遍历法(左->中->右)遍历树,放入数组中O(n log n)
侏儒排序将指针从第二个元素开始,令其与前一个元素比较:若大于前一个元素,则直接把指针往后移一位,继续比较;若小于前一个元素,则相互交换位置,并把指针往前移一位,继续比较O(n^2)
图书馆排序从第一位开始,先把第一位插入一个新的length为3的数组中,使其前后都为0,然后把第二位元素使用二分查找法,插入到对应的位置,再把空位去除;接着处理第三位元素,把第一、二位元素插入新数组中,使新数组的前、后都为空位,然后把第三位元素插入相应的位置,再去除空位,依此类推,可得到有序数组O(n log n)
梳排序与希尔排序实现方式一致,只是分组的时候不再是length/2,而是(int)(length/1.3)O(n^1.3)
耐心排序先将数组按分成多个子数组,再把这些子数组聚合成一个数组,最后进行插入排序。拆分成子数组的规则为:把第一个元素放入一个新子数组中,然后取出第二个元素作为比较元素,依序巡视所有子数组,若比较元素小于已有子数组的最后一个元素,则扩展子数组将之插入,若巡视完所有子数组都未找到最后一个元素大于比较元素,则新建一个子数组用于存放比较元素O(n log n + k)