【考研·数据结构】 内部排序算法 小结

本篇目录

前言

一、总览

二、分类讨论

1.插入排序

(1)直接插入排序

(2)折半插入排序

(3)希尔排序

(4)插入排序小结

2.交换排序

(1)冒泡排序

(2)快速排序

3.选择排序

(1)简单选择排序

(2)堆排序

4.2路归并排序

5.基数排序

总结


前言

本篇总结了各类内部排序算法的特点,适用于考研复习。


一、总览

先来一张总表,如图。


二、分类讨论

1.插入排序

(1)直接插入排序

最基本的是直接插入排序,逐个与相邻元素进行比较,边比较边移动,直到找到正确位置,就插入。每一趟排序会产生一个有序子表,但是不能保证子表中的这些元素就是处于最终位置上,因为可能他们中间还要插入别的元素。因为比较都发生在相邻元素之间,边比较边移动,当发现某个位置的元素≤哨兵(待插入元素)的值时,就确定了插入位置。因此,即便目标插入位置的前一个元素是关键字与哨兵的关键字相等,也不会导致乱序,也就是不会改变两个相等的关键字的先后顺序,即算法具有稳定性。

这种方法中规中矩,简单易懂。如果原表基本有序,那么算法的效率就可以接近于线性,最坏的情况就是O(n^2)。空间方面,插入排序都是原地进行的。总的来说,这样的算法适用于元素基本有序、数据量较小的情况。

(2)折半插入排序

折半插入排序和希尔排序则是对直接插入排序进行的优化。

既然直接插入排序产生的是有序子表,那么就可以将查找插入位置这一步改为折半查找,这样就能提高“比较”的效率,因为折半查找利用了“分而治之”的思想,这一步的效率就能达到O(log n)。折半查找是先完成“比较”这一步,之后再移动元素,“比较”的效率提高,但移动次数不会改变。因此总的时间复杂度不改变。折半插入排序同样适用于元素基本有序、数据量较小的情况。

关于稳定性。在折半查找的时候,同样是根据某个位置的元素≤哨兵(待插入元素)的值时,就确定了插入位置。这里的关键在于符号是≤而不是<。无论是直接排序还是折半插入排序,都是稳定的。

(3)希尔排序

而希尔排序,将指定间隔量的元素抽取为子表,对每个子表进行直接插入排序,最后对整个表(这时已基本有序)进行一次直接插入排序。考虑到两个具有关键字相等的元素,可能被抽取到不同子表中,并在子表中分别参与直接排序,因而这两个元素的先后顺序无法保证不变,因此这种算法是不稳定的。

希尔排序算法的效率依赖于间隔量的选择。在空间上倒是与前两种插入排序相同,都是原地算法。

(4)插入排序小结

插入排序中规中矩,是老老实实排序的算法。三种排序算法都是原地排序,空间复杂度为O(1)。在最坏情况下,时间复杂度都是O(n^2) ,并且算法的效率与元素的初始状态有关,若初始时元素基本有序,那么效率就较高。虽然在时间复杂度上不算突出,但是插入排序的原理简单易懂。它们都适用于数据量较小、元素基本有序的情况。

2.交换排序

(1)冒泡排序

冒泡排序的名字实在是很形象了。它的基本原理是选取一个元素,通过不断与相邻元素的比较、交换,最终将无序表中的最值元素滚动到最左端(或者最右端)。一趟排序可以固定一个元素到最终位置上,产生有序子表。从另一个角度想,这种实现的原理可以看做是将直接插入排序的操作顺序做了修改,但是没有改变比较和移动的次数。(只不过直接插入排序是优先将选定的元素插入到有序表中,而冒泡排序是优先将最值元素插入到有序表中,但他们都同样是通过原地与相邻元素逐个进行比较、移动来进行排序的。)因此冒泡排序算法的时空复杂度与直接插入排序完全一致,且都是稳定算法,算法效率都与元素初始状态有关(当元素基本有序时,效率更高)。

(2)快速排序

如何能做到“快速”呢?选取一个元素(通常是首个元素)作为枢轴,并把它的值暂存起来,这样对于无序表来说,相当于0号位有一个空位(因为0号位元素的值已经被另外保存,那么0号位就可以保存其它数据了),之后通过逐个填空的方法将无序表分为左小右大两个子表(这两个子表本身也是无序的),最后剩下的一个空位就是枢轴元素要填补的位置。每一趟排序可以确定枢轴元素处于最终位置。

显然,枢轴的选择十分重要,如果能选择接近中位数的元素作为枢轴,那么在划分子表这一步就形成了有效的“分而治之”,这一步的时间效率就能达到O(log n)。如果枢轴元素选择的不好,选了最小值或者最大值,那就没有“分而治之”的效果,这一步的效率就只能退化为O(n)了。最后,因为一共有n个元素,所以也就是需要n次选取枢轴的过程,这一步的时间效率显然是O(n)。因此总的时间复杂度,在好的情况下能达到O(nlog n),坏的情况下就是O(n^2)了。这个好坏主要依赖于枢轴的选择方法。如果元素基本有序,那么选首个元素作为枢轴就很容易选到最值(因此如果元素基本有序就最好考虑另外的枢轴选取方法),否则,一般来说,快速排序的平均时间复杂度还是O(nlog n),这一点是内部排序中性能最好的了。

因为它要用到递归,就要额外占用一点栈空间了。因此空间复杂度为O(log n),前边的算法都是原地排序的。就当是拿空间换时间了吧,毕竟这个算法的名字叫“快速排序”,肯定是把时间效率放在第一位。果然算法都是各有优劣的。

关于稳定性。通过模拟算法过程可知,每一次移动后表中空位的值都会发生变化,这个空位是从两边逐渐向最终枢轴位置靠近的,因此,若有两个关键字相等的元素,它们在被移动时无法保证相对位置不变,因此算法不具有稳定性。

3.选择排序

(1)简单选择排序

这个方法听起来有点暴力排序的感觉:选取无序表的首位,将这个元素逐个与表中的其它元素比较,如果有哪个元素更小(或者大),就把它交换过来。这样,一趟排序后就能确定最小值元素在最终位置上。说它暴力,是因为不管元素的初始状态如何,这个算法都会按照既定策略把无序表中所有元素比较个遍,只要符合要求就交换(所以稳定性就被破坏了)。因此时间复杂度只能是O(n^2)。虽然有点暴力,但是这个方法简单易懂,而且它只是在表的内部暴力一下,并没有占用表之外的空间(空间复杂度为O(1) )。

(2)堆排序

这个算法运用了完全二叉树的概念,个人觉得有一种整洁(也可能是洁癖吧)的气质。别的算法都是对着数据挨个进行处理,它不一样,不管数据量有多大(也无论元素的初始状态如何),这个算法都先将所有数据在线性的效率内组织成一棵有规律的完全二叉树(大根堆或者小根堆)。这有点像一个人一进屋子就先把所有的东西都排列得整整齐齐,而且排列得还有内在逻辑性。

初始时数组(存储结构)中已经有一些乱序的数据。构造初始堆的方法是,从最后一个结点开始向上筛选交换,如果应当与父结点交换元素那么就交换。对每一个结点都要考虑两个问题,①是否需要向上交换,②该结点若是刚被更改过,那么该结点与其它子结点是否需要交换,如果需要,就下沉一层进行处理。逐级向上,直到根结点。

之后是输出步骤。每输出一次堆顶的元素,这堆(二叉树)就乱了,于是算法就立马重新整理好,又变成有规律的完全二叉树。不管数据量有多大,这算法都绝不会偷懒。而且,它整理得还很快,居然在整理这一步能达到O(log n)的效率,实在是整理能手了。(这是因为有“二叉树”这个好工具!)

整理的方法是,当输出堆顶元素后,将最后一个元素放到堆顶位置,并逐级向下筛选交换,直到重新满足堆的性质。

总体而言,这个堆排序算法的时间复杂度是O(nlog n),是厉害的算法了。还有,这个算法也不占多余的空间,原地排序。数据量大也无所畏惧,这个算法就适合关键字多的表。它与上边的算法不一样,它不产生有序子表、不稳定,但是,它就是能高效地完成排序,优秀!

4.2路归并排序

这个算法属于“不积跬步无以至千里”的类型,或者说有点类似“2048”那种合并游戏,怪不得叫“归并”排序。首先需要O(n)的额外空间,利用“分而治之”的思想,每两个元素(分属于两个有序表)进行排序,归并成一个含有2个元素的有序表。之后继续向上抽象,两两合并,最终合并成一整个的有序表。因为有二分法的思想在,所以它每次归并的时间复杂度都是O(log n),一共需要的归并次数为O(n),因此总的时间复杂度始终是O(nlog n)。换句话说,排序步骤是固定的,排序效率不会因元素初始状态而改变。

关于稳定性。这个算法比较踏实,通过点滴积累最终完成整个表的排序,不像前边有的算法那样大手笔,会将元素移动很远的距离。这样小心谨慎的算法,自然是稳定的。

一般来说,会在外部排序中使用多路归并排序,这种一般是要排序的数据量太大以致于无法全部加载进内存的情况。

5.基数排序

这真是一个神奇的算法,不基于对关键字的直接比较。基数排序算法根据关键字每一位单独进行一趟排序,每一趟排序都进行分配、收集,之后进行下一趟排序。稳定性是自然要保证的,不然之前的几趟排序就白做了。这个算法需要一些额外的空间来存放队列,时间上与其它算法没法直接比较。


总结

1.空间复杂度。在不考虑基数排序的前提下,除了快速排序(用到递归,空间O(log n)、2路归并排序(需要额外O(n)空间存放第二路的表)这两种算法之外,其它算法都是原地排序的。

2.时间复杂度。同样在不考虑基数排序的前提下,有三种算法(快速排序、2路归并排序、堆排序)的平均时间复杂度为O(n log n),(这三种都利用了“分而治之”的思想因此能在其中一步达到O(log n)的效率)表现突出;其它算法都是O(n^2),在数据量较小时也可以接受。可以理解为,普通算法耗时不占地,快速排序和2路归并排序拿空间换时间,堆排序时空效率都优秀。

3.稳定性。只有5个算法是稳定的,它们就是 “鲁班泡茶机”,即路(2路归并排序)、半(折半插入)、泡(冒泡排序)、插(直接插入排序)、基(基数排序)。 (鲁班是爱发明的能工巧匠,发明的工具都稳定耐用,就让我想像一下他发明一款泡茶机的样子吧,顺道记住稳定的算法。)

4.适用情况。插入排序(无论哪种插入法)适合数据量较小,基本有序的表;堆排序适合关键字多的表。

5.效率与初始状态的关系。三种插入排序、冒泡排序、快速排序,效率与初始状态有关。或许可以反过来说,选择排序、堆排序、2路归并排序、基数排序,各自都有自己固定的独门策略,无论初始状态如何,只管套用策略,排序就完成了。

猜你喜欢

转载自blog.csdn.net/Dr_Cheeze/article/details/128002143