(C)涉及C中的常见内部排序。
1 内部排序
排序:将数据或者记录的任意序列重新排列成按照某个关键字的某种顺序的序列的 过程。
排序分类:按照被排序列的数量可分为内部排序和外部排序。
外部排序:如因被排序列的数量过大时,在排序过程中不得不访问外存的排序过程。
内部排序:全部被排元素在排序过程中都被存储在随机存储器(内存)中。
稳定排序:不改变被排序列两个相等元素相对顺序的排序。
2 排序分类
属插入排序的“shell排序”,“快速排序”,属选择排序的“堆排序”,“归并排序”等。
2.1 shell排序
shell排序是在直接插入排序之上的一种改进。避序列中元素的移动,以规定步长(step>=1)在序列中不断地进行插入操作。
直接插入排序的过程:用两层循环实现,外层循环指向已有序列的末位。若有序序列末位跟下一位不符排序规则,则进入内层循环从有序序列的末位开始往后移一位,直到移到的元素跟“下一位”符合排序规则为止。最初的有序序列可设为序列的第一个元素。
Shell为改进直接插入排序算法复杂度,以选取步长、再缩小步长的方式来排序。故而shell排序又称为“缩小增量排序”。
(1) 排序过程
如给序列[5,2,1,3,1,4]以重小到大的排序,采用起始步长为3,每次减少1步的shell排序法的过程为:
1:shell排序过程
需要认识到的是
- Shell排序为不稳定排序。
- 每进行一次插入时需要进行彻底,每符合插入的条件都会有插入操作发生。如步长为3时,红色1插入到2的位置,若序列后面还有元素,则红色1将会继续往前插入(或者2继续往后插入)。
(2)代码实现
//Shell Insert
void ShellInsert( INT a[N], INT s )
{
int i, j, k;
INT len;
INT temp;
temp = 0;
len = N;
if( s < 1 || s >= len){
return ;
}
for(i = 0; i + s < len; ++i){
//Save big one
if( MAX(a[i], a[i+s]) ){
temp = a[i];
//Continue insert backward until temp less than current element
for( j = i + s; j < len && MAX(temp, a[j]); j += s){
a[j-s] = a[j];
}
//Insert temp in this location
a[j-s] = temp;
}
}
print(a);
}
//Shell sort,
//Call ShellInsert function to complete Sheel sort
void ShellSort( INT a[N], INT n )
{
int i;
for(i = n; i > 0; --i){
ShellInsert(a, i);
}
}
排序原理
-
从序列当前元素开始,将当前元素保存给temp,比较和当前元素相差s-1个元素的的元素。
-
若两个元素之间符合排序要求,则当前元素变为下一个元素。若两个元素不符合排序要求,则将与当前元素相差s-1的元素a[s]插入到当前位置。再跳到与当前元素相差2(s-1)的元素a[2s]那里,再与temp比较,若两者序列符合要求,则temp元素插入到a[s]元素位置,否则a[2s]元素插入到a[s]原有的位置。继续往后比较直到超出排序序列的长度或者两者满足序列要求为止。
排序关键
-
体会准排序思想。
-
认准当前元素与比较元素,避免其它元素干扰。
-
编写具通用性的子函数,这样的子函数最好具有“只需要多次组合调用子函数就可以完成整个目标”的特点。
算法复杂度:增量的选取将决定不同的复杂度,增量序列有各种取法,但应该注意增量序列中的值没有除1之外的公因子,并且最后一个增量必须为1[摘自《数据结构.严蔚敏 吴伟民》]。
2.2快速排序
快速排序采取的方略是交换。以交换来排序最简单的是气泡排序。快速排序是对气泡排序的一种改进。
气泡排序的过程为:两层循环实现。内层循环以外层循环所指元素为开始元素,比较当前元素及下一元素,若不符合序列要求则交换两元素,然后内层循环的索引值加1后重复两元素的比较直到到元素末尾。内层循环执行完一次就找到最大或者最小的元素放在指定位置。直到外层循环溢出。
快速排序的过程为:选取序列中的某个元素作为参考值,以此参考值通过一次排序将序列分成两部分。一部分都比参考值小,另一部分都比参考值大。继续对每个部分的序列重复以上操作,直到整个序列被分成由单个序列组成为止。
(1)排序过程
同shell排序法一样,可以先探索每次进行快速排序的通用方法。这个通用的方法就是将序列分成部分的序列,一部分比选取的关键值小,另一部分全都比关键值大。当将整个序列被划分为许多个部分,每个部分都是两个元素或者一个元素时,序列就有序了。以下从小到大排列
(2)代码实现
[1]一趟快速排序
此一趟排序可以对整个数组进行一趟快速排序,也可以指定数组中的某一部分进行一趟快速排序。
//Part a sort to tow partion
//l is the first index of the array
//h is the max index of the array
//Retrun the l or the h
int PartSort( INT4 a[], int l, int h )
{
if( IS_NEGATIVE(l) || IS_NEGATIVE(h) || MAX(l, h) ){
return ;
}
INT4 temp;
//Because not know the length of the a,
//Choose the first element be the key value
temp = a[l];
while( l < h){
//Check elements in back
while(h > l && a[h] >= temp){
--h;
}
//The back element which less than the key value
a[l] = a[h];
while(l < h && a[l] <= temp){
++l;
}
a[h] = a[l];
}
//l = h
a[l] = temp;
return l;
}
[2]以一趟快速排序为基础的递归快速排序
递归函数虽代码简洁却不实用。叫人难以将递归代码彻底理解。编写递归代码往往是根据对象的规律来进行总结。对于快速排序来说,它具有这样的特点:将l - h的数列分为左右两个序列,再将l – l(经分离后的l值)(序列元素为奇数还是偶数都是如此)的序列分成左右两个序列,然后再将l(经分离后的l值) – h分为左右两个序列,左右两个子序列继续符合这样子的排序规律……直到左右两个序列的个数为2或者1时为止。故而可以在函数中递归的分,叹一言难尽。
//Shell recursive
void QuickRecursive( INT4 a[], int l, int h)
{
if( IS_NEGATIVE(l) || IS_NEGATIVE(h) || MAX(l, h) ){
return ;
}
int m;
if( l < h ){
//Part array
m = PartSort(a, l, h);
//Left recursive
QuickRecursive(a, l, m - 1);
//Right recursive
QuickRecursive(a, m + 1, h);
}
}
[3]非递归快速排序
如果要编写非递归的排序函数,则就要抓住整个排序的原理。将每次排序的过程用循环语句和判断语句表示出来。这时,编写的代码可以按照递归的思维将递归代码全部编写出来,也可以在不违背快速排序的原理之上编写代码。实际上是序列各位置(下标)用一定的策略来排序,进而操作了序列。
//Shell not recursive sort
void QuickNotRecursive(int a[], int l, int h)
{
int index[100] = {h, l};
int i;
int m;
//Left index
i = 1;
//Right array left only one element
while( index[i] < h ){//!(index[i] >= index[i-1] && i == 1)
//Part the left array
m = PartSort(a, index[i], index[i-1]);
//Judge the part left arry is not only one element
if(m - 1 > index[i]){
++i;
index[i] = index[i-1];
index[i-1] = m - 1;
//Only one element
}else if(index[i] == index[i-1]){
index[i--] = 0;
index[i] += 2;
//Do not need sort if only one element between two m
}else if(index[i] + 1 == index[i-1] && a[index[i]] < a[index[i-1]]){
index[i--] = 0;
//Left array is noly one element
//Return up floor
}else{
index[i] = m + 1;
}
}
}
实现此函数的关键点:
-
index数组后一个元素跟前一个元素分别对应的是父(子)序列最小和最大下标的关系。函数依据这两个下标值对子序列进行一趟快速排序。每经一趟快速排序得到的下标值m根据不同的情况进行处理。
-
如果m的左序列不止一个元素则将m作为新序列的最大下标,再进行一趟快速排序。即以左序列优先的特点找到序列中的第一个元素。如上图中的步骤2。
-
如果m的左序列只有一个元素则以m右序列的第一个下标作为新序列的最小下标值[步骤2],以上一躺快速排序得到的下标作为新序列的最大下标,再进行一次快速排序。如上图的步骤4。
-
只要得出序列的第一个元素之后,就会出现最小下标和最大下标相等的情况,且此等情况还可能进入死循环,就算不进入死循环也要做进入上一趟排序的右序列的处理。如步骤8。此时做出的处理是将当前不会在使用的最小坐标清楚,并进入上一趟快速排序的有序列。如步骤9。
-
还有一种比较隐藏的情形是新序列的最小下标与最大下标只相差1的情况,这种新序列只有两个元素的情况下可判断序列是否有序。是一种减少函数执行步骤的手段。如步骤4。
-
如果m不符合上述任何的条件,则直接进入右序列。对某个右序列进行以上相同的操作。如步骤3进入步骤4,步骤8进入步骤9。
-
整个操作完毕的条件就是最右边的序列只剩下原序列的最后一个元素。
index数组后一个元素跟前一个元素分别对应的是父(子)序列最小和最大下标的关系。函数依据这两个下标值对子序列进行一趟快速排序。每经一趟快速排序得到的下标值m根据不同的情况进行处理。
如果m的左序列不止一个元素则将m作为新序列的最大下标,再进行一趟快速排序。即以左序列优先的特点找到序列中的第一个元素。如上图中的步骤2。
如果m的左序列只有一个元素则以m右序列的第一个下标作为新序列的最小下标值[步骤2],以上一躺快速排序得到的下标作为新序列的最大下标,再进行一次快速排序。如上图的步骤4。
只要得出序列的第一个元素之后,就会出现最小下标和最大下标相等的情况,且此等情况还可能进入死循环,就算不进入死循环也要做进入上一趟排序的右序列的处理。如步骤8。此时做出的处理是将当前不会在使用的最小坐标清楚,并进入上一趟快速排序的有序列。如步骤9。
还有一种比较隐藏的情形是新序列的最小下标与最大下标只相差1的情况,这种新序列只有两个元素的情况下可判断序列是否有序。是一种减少函数执行步骤的手段。如步骤4。
如果m不符合上述任何的条件,则直接进入右序列。对某个右序列进行以上相同的操作。如步骤3进入步骤4,步骤8进入步骤9。
整个操作完毕的条件就是最右边的序列只剩下原序列的最后一个元素。
以下是将[3, 1, 2, 4, 5, 1]经递归和非递归排序的结果:
misskissc@DeskTop:~/C/SortALG$./a.out TheRecursive method is: 1: 1 1 2 3 5 4 2: 1 1 2 3 5 4 3: 1 1 2 3 5 4 4: 1 1 2 3 5 4 5: 1 1 2 3 4 5 6: 1 1 2 3 4 5 Not theRecursive method is: 7: 1 1 2 3 5 4 8: 1 1 2 3 5 4 9: 1 1 2 3 5 4 10: 1 12 3 5 4 11: 1 12 3 5 4 12: 1 12 3 4 5 misskissc@DeskTop:~/C/SortALG$ |
都经过了6个步骤。
另外我个人觉得,只要实现了快速排序的过程,不就是非要遵循递归法的过程。如可以将得到的左子序列和右子序列平等的进行排序。
2.3堆排序
堆排序是对基本选择排序的一个升级。每次操作3个元素。堆排序适合元素比较多的场合。
对一个序列采取堆排序的过程为:
-
将序列对应为一个完全二叉树。
-
将此完全二叉树建成一个大堆或者小堆。
-
将根元素与最后一个元素交换。此时只有对顶元素搅坏了堆序,故而只需从堆顶元素起对n – 1个元素再进行建堆。
-
重复3)直到只剩最后一个元素。
关键:序列到完全二叉树的对应。当前节点一定要遍历到最后的那一个节点。
建堆与堆筛选的一个区别是起始元素不同。
(1)建堆
以数组a[] = [3, 1, 2, 4, 5, 1]为例。
[1]建堆过程
1)首先以下标的先后顺序将其看成一个完全二叉树
2)建堆,以小堆为例
从完全二叉树的最后一个非终端节点[n / 2 - 1]开始,至根节点结束。在每个节点处和自己较小的一个孩子交换,然后再去此节点的下一个节点。
在第3步之后,不管根元素时和哪一个节点交换了值。被交换的那个节点还要继续判断是否要和较小的一个元素交换。如根元素和右边子结点交换后还要继续和子节点交换值。
[2]代码
由于堆筛选就是不断的进行堆建立的操作,所以对堆建立过程编写一个通用性的函数,以便供堆筛选调用。
//a is the list
//c is the node
//l is the length of the list
//Heap construct: continue change two elements
void HeapAdjust(int a[], int c, int l)
{
int temp;
int i;
//Save current node value
temp = a[c];
//To child node of a[c]
//i = i * 2 + 1 means node left child element
for( i = 2 * c + 1; i < l; i = i * 2 + 1 ){
//Choose the less one between two children
if( i < l - 1 && MY_MAX(a[i], a[i+1]) ){
++i;
}
if( MY_MAX(a[c], a[i]) ){
//Change value with the less one child
a[c] = a[i];
//New node
c = i;
}
//temp value should be here
a[c] = temp;
}
}
(2)堆筛选
堆筛选是在堆建立之后,将根元素和最后一个元素交换后,再进行n – 1个元素建堆操作。
//a is the list
//len is the length of the a
//HeapSort: sort the list by heap method
void HeapSort(int a[], int len)
{
int i;
//Firstly, construct the a list, from a[n / 2 -1]
begin
for( i = len / 2 - 1; i >= 0; --i){
HeapAdjust(a, i, len);
}
//Secondly, construct i - 1 heap until here is noly
one element
for( i = len - 1; i > 0; --i){
//Get root element
swap(&a[0], &a[i]);
//Reconstruct the heap
HeapAdjust(a, 0, i);
}
}
2.4归并排序
归并排序:将两个或两个以上的有序表组合成一个新的序列。
2-路归并:设某序列含有n个记录,则可将此序列看成是由n个有序的单个子序列构成的序列。然后两两归并得到长度[ n/2]个长度为2或1的有序子序列,再两两归并…,直到得到一个长度为n的有序序列为止。
(1)合并两个有序序列的程序实现
将源序列分别有序的两部分合并为一个整体有序的序列,可以借助于另外一个存储空间来实现。
//Sort s[l..m] and s[m+1..n] to d[]
void merge( int s[], int d[], int l, int m, int n)
{
int i, j;
i = l;
j = m + 1;
for(; l <= m && j <=n; ++i){
if( !MY_MAX(s[l], s[j]) ){
d[i] = s[l++];
}else{
d[i] = s[j++];
}
}
//The rest elements
for(; j <= n; ++j){
d[i++] = s[j];
}
for(; l <= m; ++l){
d[i++] = s[l];
}
}
(2) 2路-归并法的递归程序实现
//Merge recursive
//Sort s[l...n] to d[l..n]
void MergeRecusionSort(int s[], int d[], int l, int n)
{
int t[N]={0};
int m;
if( l == n){
d[l] = s[l];
}else{
m = (l + n) / 2;
MergeRecusionSort(s, t, l, m);
MergeRecusionSort(s, t, m + 1, n);
merge(t, d, l, m, n);
}
}
3总结
(1) shell排序
-
本质为插入排序,两比较的元素不再相差1,改进直接插入排序元素的移动。也可无直接交换的操作。
-
以步长不断的插入。经一趟shell排序后缩小步长,直至1。
-
时间复杂度可看着n^(3/2),不稳定排序。
-
不存在递归到非递归函数的编写。
(2)快速排序
-
快速排序本质为交换排序(冒泡排序),以随机选取序列中的某个元素为,将序列调整为一边比参考元素都小和都大的序列。然后再以参考序列左序列优先以相同的方法进行调整。可无直接交换的操作。
-
时间复杂度O(nlogn),不稳定排序。
-
将递归转化为非递归的关键在于理解递归法对序列下标的操作过程。
(3)堆排序
-
堆排序的实质是选择排序,每次在以当前元素为根节点的二叉树之上选出最大或最小的元素。建堆时以最后一个非叶子节点[n/2]往上遍历,堆筛选时从根节点开始。建堆或者堆筛选得到根元素和最后一个元素交换,作为选出来的最大或者最小的元素。
-
时间复杂度O(nlogn),不稳定排序。
-
暂无递归到非递归算法的转化。
(4)归并排序
-
归并是将多个有序序列合并为一个有序序列。不仅有2-路归并算法。一趟2-路归并算法比较简单,只需要另外一个跟源序列相同大小的存储空间。
-
时间复杂度时间复杂度O(nlogn),稳定排序。
-
2-路归并递归法还是挺有意思的。分析了一下递归过程,只要总结出对各元素操作的规律,编写非递归的函数应该是留点汗水就可以编写出来。这种转换可以自立门户来作笔记。因为可能还有n-路归并法。
这些算法应用于实际时需要解决最坏情况下的排序过程。避免不必要的排序。不然运行时间跟一般算法一样长,达不到算法的改进效果。
读《数据结构》。
Note Over。