数据结构 排序算法
数据结构可视化 https://www.cs.usfca.edu/~galles/visualization/Algorithms.html
在待排序的文件中,若存在多个关键字相同的记录,经过排序后这些具有相同关键字的记录之间的相对次序保持不变,该排序方法是稳定的;若具有相同关键字的记录之间的相对次序发生改变,则称这种排序方法是不稳定的。即所有相等的数经过某种排序方法后,仍能保持它们在排序之前的相对次序,则说这种排序算法是稳定的,反之,就是不稳定的。
稳定的排序算法如下表所示:
稳定的排序 |
时间复杂度 |
空间复杂度 |
冒泡排序(bubble sort) |
最差、平均都是O(n^2),最好是O(n) |
1 |
插入排序(insertion sort) |
最差、平均都是O(n^2),最好是O(n) |
1 |
归并排序(merge sort) |
最差、平均、最好都是O(n log n) |
O(n) |
桶排序(bucket sort) |
O(n) |
O(k) |
基数排序(Radix sort) |
O(dn)(d是常数) |
O(n) |
二叉树排序(Binary tree sort) |
O(n log n) |
O(n) |
不稳定的排序算法如下表所示:
不稳定的排序 |
时间复杂度 |
空间复杂度 |
选择排序(selection sort) |
最差、平均都是O(n^2) |
1 |
希尔排序(shell sort) |
O(n log n) |
1 |
堆排序(heapsort) |
最差、平均、最好都是O(n log n) |
1 |
快速排序(quicksort) |
平均是O(n log n),最差是O(n^2) |
O(log n) |
一、冒泡排序
冒泡排序(BubbleSort)的基本概念是:依次比较相邻的两个数,将小数放在前面,大数放在后面。即在第一趟:首先比较第1个和第2个数,将小数放前,大数放后。然后比较第2个数和第3个数,将小数放前,大数放后,如此继续,直至比较最后两个数,将小数放前,大数放后。至此第一趟结束,将最大的数放到了最后。在第二趟:仍从第一对数开始比较(因为可能由于第2个数和第3个数的交换,使得第1个数不再小于第2个数),将小数放前,大数放后,一直比较到倒数第二个数(倒数第一的位置上已经是最大的),第二趟结束,在倒数第二的位置上得到一个新的最大数(其实在整个数列中是第二大的数)。如此下去,重复以上过程,直至最终完成排序。
代码实现如下:
二、插入排序
插入排序的基本思想是每步将一个待排序的记录按其排序码值的大小,插到前面已经排好的文件中的适当位置,直到全部插入完为止。插入排序方法主要有直接插入排序和希尔排序。
直接插入排序具体算法描述如下:
1. 从第一个元素开始,该元素可以认为已经被排序
2. 取出下一个元素,在已经排序的元素序列中从后向前扫描
3. 如果该元素(已排序)大于新元素,将该元素移到下一位置
4. 重复步骤3,直到找到已排序的元素小于或者等于新元素的位置
5. 将新元素插入到下一位置中
6. 重复步骤2
伪码描述如下:
三、归并排序
归并排序是将两个或两个以上的有序子表合并成一个新的有序表。初始时,把含有n个结点的待排序序列看作由n个长度都为1的有序子表组成,将它们依次两两归并得到长度为2的若干有序子表,再对它们两两合并。直到得到长度为n的有序表,排序结束。
归并操作的工作原理如下:
1、申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列
2、设定两个指针,最初位置分别为两个已经排序序列的起始位置
3、比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置
4、重复步骤3直到某一指针达到序列尾
5、将另一序列剩下的所有元素直接复制到合并序列尾
代码实现如下:
四、桶排序
桶排序的基本思想就是把区间[0,1)划分成n个相同大小的子区间,或称桶,然后将n个输入数分布到各个桶中去。因为输入数均匀分布在[0,1)上,所以一般不会有很多数落在 一个桶中的情况。为得到结果,先对各个桶中的数进行排序,然后按次序把各桶中的元素列出来即可。
在桶排序算法的代码中,假设输入是个含n个元素的数组A,且每个元素满足0≤ A[i]<1。另外还需要一个辅助数组B[O..n-1]来存放链表实现的桶,并假设可以用某种机制来维护这些表。
我的理解是:桶排序相当于一个N路的归并排序,首先将输入按均匀分布分到N个桶中,每一个桶都用一个链表来维护,并用插入排序对每个桶(也就是每一路)进行排序,最后将N个有序桶合并成一个,即得最终的排序结果。
伪码实现如下:
五、基数排序
设单关键字的每个分量的取值范围均是C0<=Kj<=Crd-1(0<=j<=rd),可能的取值个数rd称为基数.基数的选择和关键字的分解因关键字的类型而异.
(1)若关键字是十进制整数,则按个、十等位进行分解,基数rd=10,C0=0,C9=9,d为最长整数的位数.
(2)若关键字是小写的英文字符串,则rd=26,C0='a',C25='z',d为最长字符串的长度.
基数排序的基本思想是:从低位到高位依次对待排序的关键码进行分配和收集,经过d趟分配和收集,就可以得到一个有序序列.
基数排序从低位到高位进行,使得最后一次计数排序完成后,数组有序。其原理在于对于待排序的数据,整体权重未知的情况下,先按权重小的因子排序,然后按权重大的因子排序。例如比较时间,先按日排序,再按月排序,最后按年排序,仅需排序三次。但是如果先排序高位就没这么简单了。基数排序源于老式穿孔机,排序器每次只能看到一个列,很多教科书上的基数排序都是对数值排序,数值的大小是已知的,与老式穿孔机不同。将数值按位拆分再排序,是无聊并自找麻烦的事。算法的目的是找到最佳解决问题的方案,而不是把简单的事搞的更复杂。基数排序更适合用于对时间、字符串等这些整体权值未知的数据进行排序。
我的理解是:基数排序算法中,数据可分解为d个因子,每个因子对排序结果都有影响(即权重),先按权重小的因子进行排序,后按权重大的因子进行排序,所有因子排序完即得结果。如时间可分解为三个因子:日、月、年,先按日对时间排序,再按月对时间排序,最后按年对时间进行排序,即可。注意:每一趟按因子进行的排序都必须是稳定的!
伪码实现如下:
六、二叉树排序
二叉排序树(Binary Sort Tree)又称二叉查找树。它或者是一棵空树;或者是具有下列性质的二叉树:
(1)若左子树不空,则左子树上所有结点的值均小于它的根结点的值;
(2)若右子树不空,则右子树上所有结点的值均大于它的根结点的值;
(3)左、右子树也分别为二叉排序树;
我的理解是:二叉树排序,即先建一个二叉排序树,然后中序遍历,即得到一个从小到大的排序结果。
插入结点:
1、首先执行查找算法,找出被插结点的父亲结点。
2、判断被插结点是其父亲结点的左、右儿子。将被插结点作为叶子结点插入。
3、若二叉树为空。则首先单独生成根结点。
PS:新插入的结点总是叶子结点。依次插入数据即完成建树。
伪码实现如下:
删除结点:
将结点z从二叉排序树中删除,分三种情况讨论:
1、如果结点z没有子女,则修改其父结点p[z],使NIL为其子女;
2、如果结点z只有一个子女,则可以通过在其子结点与父结点间建立一条链来删除z;
3、如果结点z只有两个子女,先删除z的后继y(它没有左子女),再用y的内容来替代z的内容。
PS:某一结点x的后继即具有大于key[x]中的关键字中最小者的那个结点,即中序遍历顺序下的后继
伪码实现如下:
七、选择排序
选择排序的基本思想是每一趟从待排序的数据元素中选出最小(或最大)的一个元素,顺序放在已排好序的数列的最后,直到全部待排序的数据元素排完。选择排序中主要使用直接选择排序和堆排序。
直接选择排序的过程是:首先在所有记录中选出序码最小的记录,把它与第1个记录交换,然后在其余的记录内选出排序码最小的记录,与第2个记录交换......依次类推,直到所有记录排完为止。
代码实现如下:
希尔(Shell)排序的基本思想是:先取一个小于n的整数d1作为第一个增量把文件的全部记录分成d1个组。所有距离为d1的倍数的记录放在同一个组中。先在各组内进行直接插入排序;然后,取得第二个增量d2<d1重复上述的分组和排序,直至所取的增量di=1,即所有记录放在同一组中进行直接插入排序为止。该方法实质上是一种分组插入方法。
一般取d1=n/2,di+1=di/2。如果结果为偶数,则加1,保证di为奇数。
代码实现如下:
九、堆排序
堆的定义:n个关键字序列Kl,K2,…,Kn称为(Heap),当且仅当该序列满足如下性质(简称为堆性质):
(1) ki≤K2i 且 ki≤K2i+1
或(2)Ki≥K2i 且 ki≥K2i+1(1≤i≤ n)
若将此序列所存储的向量R[1..n]看作是一棵完全二叉树的存储结构,则堆实质上是满足如下性质的完全二叉树:树中任一非叶结点的关键字均不大于(或不小于)其左右孩子(若存在)结点的关键字。
根结点(堆顶)的关键字是堆里所有结点关键字中最小者,称为小根堆;根结点的关键字是堆里所有结点关键字中最大者,称为大根堆。
用大根堆排序的基本思想如下:
1、先将初始文件R[1..n]建成一个大根堆,此堆为初始的无序区
2、再将关键字最大的记录R[1](即堆顶)和无序区的最后一个记录R[n]交换,由此得到新的无序区R[1..n-1]和有序区R[n],且满足R[1..n-1].keys≤R[n].key
3、由于交换后新的根R[1]可能违反堆性质,故应将当前无序区R[1..n-1]调整为堆。然后再次将R[1..n-1]中关键字最大的记录R[1]和该区间的最后一个记录R[n-1]交换,由此得到新的无序区R[1..n-2]和有序区R[n-1..n],且仍满足关系R[1..n-2].keys≤R[n-1..n].keys,同样要将R[1..n-2]调整为堆。
……
直到无序区只有一个元素为止。
伪码实现如下:
十、快速排序
快速排序采用了一种分治的策略,通常称其为分治法,其基本思想是:将原问题分解为若干个规模更小但结构与原问题相似的子问题。递归地解这些子问题,然后将这些子问题的解组合为原问题的解。
快速排序的具体过程如下:
第一步,在待排序的n个记录中任取一个记录,以该记录的排序码为准,将所有记录分成两组,第1组各记录的排序码都小于等于该排序码,第2组各记录的排序码都大于该排序码,并把该记录排在这两组中间。
第二步,采用同样的方法,对左边的组和右边的组进行排序,直到所有记录都排到相应的位置为止。
代码实现如下: