排序算法学习整理
排序,定义上来说就是重新排列表中元素,令表中的元素满足按关键字有序的过程。
排序的稳定性:一个待排序表中有两个元素a和b,它们两个具有一样的关键字key。在排序前,a在b前面;若使用某个排序算法后,a仍然在b前面,则这种排序是稳定的。
在实际操作中,有时候待排序的数据过大,或者其他的一些原因,要排序的数据并不都在内存中,这样的排序就叫做外部排序;反之都在内存内排序,称为内部排序。
按照大致的操作方式,常见的排序算法可以被分为以下几类:插入排序、交换排序、选择排序、归并排序和技术排序五类。插入排序中主要存在以插入方式进行排序的操作,交换排序则是以交换两个元素来进行排序,归并和基数较为特殊。
在评价一个算法前,有这些点需要进行关注:
1)元素的初始位置;不同的算法对元素初始顺序敏感度不同,有的算法不关注初始位置,不管元素怎么排它都要按规定比较、移动,有的排序算法则更适合处理已经差不多有序的表。
2)元素的个数;
3)算法的空间复杂度和时间复杂度 ;
4)算法是否稳定。有的算法在计算的时候会改变元素已有的相对顺序。
5)元素的存储结构。有的算法可适应顺序存储和链表,有的算法不适用于链表。
6)元素的排序过程。有的算法每次必能排出部分有序的队列,有的算法则要到最后才能得到有序表。
此处均默认从小到大排列,堆取小根堆
1.插入排序
基本思想:把一个要排序的元素插入到已经排序好的部分。可以分为直接插入、折半插入和希尔排序。
1)直接插入算法:在一开始便将第一个元素视为有序,从第二个开始,逐个向前对比,找到合适的位置就插入。
举例:表中前3个元素已经排列有序,排列第四个元素时,从第3个开始对比,若小于第三个数,则插入到第三个元素前;若大于,继续向前对比;直到插入到最前端。
在使用这种算法时,因为插入的时候其他数据需要腾位置,所以需要常数个辅助空间,空间复杂度o(1)。时间取平均状态,时间复杂度o(n2),最好情况下每个元素已经有序,只比较一次,不用排列,时间复杂度只有o(n)。最坏情况元素逆序。
2)折半插入排序
基本思想:普通的插入每次要把待排序元素依次和每个已排序元素对比,折半排序则是以折半的形式和已排序元素做对比。相比于直接插入,它减少了元素对比的次数,元素移动的次数并未改变,时间复杂度还是o(n2)。
3)希尔排序
基本思想:直插排序对已经有序的序列处理速度更快,对其进行修改,即得到了希尔排序,又叫缩小增量排序。具体操作:取一个步长d,以此步长将整个表分为多个小表,例如步长为5,那么第1、6、11个数为一组,第2、7、12个为一组。在组内进行直接插入排序。经历完一轮后称为一趟排序。在一趟排序完成后,选择更小的步长e<d,继续进行排序,直到步长等于1。即所有的元素都在一组,再进行一次直插排序。
在空间上,同插入排序,只需要常数个辅助空间,空间复杂度o(1)。时间上则较为复杂,它的时间复杂度依赖于增量,某个范围内时间复杂度为o(n1.3),最坏情况下为o(n2)。在稳定性上,由于相同关键字可能会被分到不同子表,所以顺序也可能被打乱,希尔排序是不稳定排序。此外, 希尔排序只适用于顺序存储。
在做题考试中,可能会遇到给出表,认定已经进行了n趟排序,要求指出这是什么类型算法的情况,要注意,直接插入和折半插入,每次都能产生一个有序元素。
--------------------------
2.交换排序
基本思想:根据两个元素关键字的比较来直接交换两个元素的位置。
1)冒泡排序
基本思想:从前往后(或从后往前)依次对比两个元素之间的关键字并交换位置。第1个和第2个比,第2个和第3个比。。。。这样便称为一趟排序。下一趟排序从第2个元素开始,继续互相比较。。。每次完成这样的一趟排序,都能有一个最小/最大元素被排到正确位置。因为元素会像气泡一样逐渐上升,因此称为冒泡排序。是比较简单常见的排序。并且它是一种稳定排序。
在交换过程中需要一个辅助空间做临时存储,空间复杂度o(1)。对元素初始顺序不敏感。如果在算法里加一个标志位,用于判断本趟排序是否有元素被移动;当遍历一次后标志位还是未移动,那么就说明已经有序。在标志位帮助下,最好情况时时间复杂度为o(n)。平均情况和最坏情况下时间复杂度都是o(n2)。
2)快速排序
基本思想:
快速排序基于分治算法。在整个待排序表中,先选择一个元素作为基准(一般选首元素)并在首尾各放置一个指针i j,用一个辅助空间储存基准。此时首元素的位置就相当于一个可用的“空位”。
在进行排序时,j先向前移动,遇到一个小于基准的数,就将基准和指针j指向元素交换;接下来再移动指针i,遇到一个比基准大的数,再交换基准和指针i指向元素。再移动指针j....如此往复,直到i和j碰头。称为完成了一趟排序。
初始: 5是基准
5 15 x x x x x x x 3
i j
首先,--j向前移动,3比5小,5和3交换位置,j前移。 (5可以取出来存储,把基准置空也可)
3 15 x x x x x x x 5
i j
i++后移,15比5大,交换基准和15.
3 5 x x x x x x x 15
i j
快速排序被称为快速排序,是因为在进行一趟排序后,一个表就会被分为两个子表。在下一趟排序中,可以同时对两个子表进行排序。直到最后每个子表只剩下一个元素,即有序。
快速排序的算法可以被写为递归的形式,需要借用一个栈来存储递归信息,最好和平均情况下空间复杂度为0(log2n),最坏情况为o(n)。在时间上,未排序表中元素的顺序会影响性能,最糟糕的情况就是每次切出来n个元素和0个元素的表,这也就意味着快速排序不适合处理已经有序和逆序的表,最坏情况下,时间复杂度为o(n2),最好情况和平均情况下,时间复杂度为o(nlog2n)。
同时,快速排序在排序过程中不会生成有序表,但是每一趟排序后,基准元素都会被放到正确的位置。
------------------------------
3.选择排序
基本思想:每一趟都在未排序元素中选取一个最小值放入有序部分,需要做n-1趟。
1)简单选择排序
思路:直接进行选择。空间复杂度0(1),时间复杂度o(n2),不随着元素顺序改变。最好情况下,数据已经有序,只对比,不用移动。并且元素比较的次数和元素初始顺序无关。由于每次要选择一个元素,所以元素间顺序可能改变,属于不稳定排序。
2)堆排序
基本思路:将表视为一个完全二叉树,那么这张表的n/2处一定就是叶节点和非叶节点的分界线。将这个表按要求建立成小根堆,每次堆顶所要求的当前最小元素。每次取出要求元素后继续处理,就能获得所需的有序表。
(如图所示为初始堆的建立,最后的元素变红表示不再参与排序,可视为取出)
对于堆排序,有两个问题需要处理:一开始的初始堆如何建立?在插入、去除元素后如何重新排序?
初始建堆时,自下而上,从右向左(看序号就能理解,其实就是从小到大),从最后一个树开始对比。若父节点大于子节点,交换两者。然后继续处理右侧、上方的树。值得注意的是,在构建初始堆时,修改非底层子树的节点,可能会破坏底部已经排序好的树,因此在处理非底层子树时,还要把已经处理过的子树再检查一遍。
除过初始建堆,堆排序也支持加入和删除元素。插入或删除元素一般在堆的末尾,取出一般是在堆顶。在这里需要注意的是,建立初始堆和插入元素的时间复杂度不同,插入元素时只需要调整新元素直到根节点的这一条子树,比较次数 最多为log2n(向下取整)。
对于堆排序,如果需要排序所有元素,一般会把已排序部分放在表的末尾。在空间上只需要常数个辅助空间,空间复杂度为o(1)。建堆时间为o(n),还需要n-1次排序对其进行调整(堆排序一开始只保证堆顶满足需求),每次调整为o(n),所以堆排序在最好/最坏/平均下时间复杂度为o(nlog2n)。此外,堆排序可能会把后面的同关键字元素排到前面,所以它也是一种不稳定算法。
------------------------------
4.归并排序
基本思想:将多个小的有序表合并为一个完整的有序表。
操作:待排序表有n个元素,可以视为n个长度为1的有序表。两两归并,直到完全合并。这种行为称为2路归并排序。
值得一提的是,在对两个小有序表进行排序时,采用了类似“车轮战”的比较方法:先把它们都复制到辅助数组B中(足够大)。记录两个小表的长度和表头。每次从两个小表中各取出一个元素,小的元素写入原表,大的继续保留,下一个元素继续来比,直到一个小表完全比完,剩下的直接放回原表,这样就完成了两个子表的合并(不是一趟,仅仅是两个子表)。这个过程也用到了分治的思想。
在合并子表时,辅助空间也为n,所以空间复杂度o(n)。每趟归并都会将n个表合并为n/2个表,用时o(n),总共要log2n次归并,所以时间复杂度为o(nlog2n)。此外,它是稳定算法。
初始: 49 38 65 97 76 13 27 7组
一趟: 38 49 65 97 13 76 27 4组
二趟: 38 49 65 97 13 27 76 2组
三趟: 13 27 38 49 65 76 97 1组
-------------------------------------
5.基数排序
基本思想:与人的想法较为接近,直接对比某一位的值。例如:43的十位是4,那么肯定大于38.
在实际排列时,先从最低位开始排列,一直排到最高位。在升高的过程中,已经排列好的相对位置不改变。
空间效率:需要r个队头指针和队尾指针,并且会反复利用,因此空间复杂度为o(r)。在时间上,需要d趟分配和收集,分配需要o(n),收集需要o(r)。所以基数排序的时间复杂度为o(d(n+r))。并且它与序列的初始位置无关。是稳定排序。
(如图,即为将10个数按照个位值排列的结果)
(注:分配是指把元素挂在0 1 2 3等指针后,收集是指把这些元素重新组合为表)
---------------------------------------------------------------
各种排序算法间的对比:
直插排序、选择排序、冒泡和希尔排序都只需要常数个辅助空间用于元素的转移,快速排序中由于存在递归,所以需要一个栈来暂存数据,一般情况下为o(log2n),最坏情况下为o(n)。对于归并排序,它需要大量空间用于两个子表的合并,空间复杂度为0(n)。
各种排序算法的性质
算法类型 | 时间复杂度 | 空间复杂度 | 稳定性 | ||
最好 | 平均 | 最坏 | |||
直接插入 | o(n) | o(n2) | o(n2) | o(1) | 稳定 |
折半插入 | o(n) | o(n2) | o(n2) | o(1) | 稳定 |
希尔排序 | 难以认定 | o(1) | 不稳定 | ||
简单选择 | o(n2) | o(n2) | o(n2) | o(1) | 稳定 |
快速排序 | o(nlog2n) | o(nlog2n) | o(n2) | o(log2n)-o(n) | 不稳定 |
冒泡排序 | o(n) | o(n2) | o(n2) | o(1) | 稳定 |
堆排序 | o(nlog2n) | o(nlog2n) | o(nlog2n) | o(1) | 不稳定 |
2路归并 | o(nlog2n) | o(nlog2n) | o(nlog2n) | o(n) | 不稳定 |
基数排序 | o(d(r+n)) | o(d(r+n)) | o(d(r+n)) | o(r) | 稳定 |
排序趟数不受元素初始顺序影响的算法:直插排序、简单选择排序、基数排序 (交换类算法都会受影响)
元素的移动次数不受元素初始顺序影响的算法:基数排序
交换类排序(如冒泡)如何受元素位置的影响:在算法中加入标志位,如果某趟排序后标志位还是提示无移动,那就说明已经有序,故影响了实际排序次数。