数据结构排序算法

来源:互联网 发布:线性映射及其矩阵表示 编辑:程序博客网 时间:2024/05/29 08:35
http://blog.csdn.net/anzelin_ruc/article/details/9294459

冒泡排序:

重复地走访过要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来。走访数列的工作是重复地进行直到不再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端,故名。

算法步骤:

1.比较相邻的元素。如果第一个比第二个大,就交换他们两个。
2.对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。对所有元素在一趟比较之后,最后的元素应该就是最大的数。

3.针对除最后已排好序的所有的元素重复以上步骤1和2,直到没有任何一对数字需要比较。

时间复杂度:

若文件的初始状态是正序的,一趟扫描即可完成排序。所需的关键字比较次数和记录移动次数均达到最小值:C(min) = n-1, M(min) = 0。所以,冒泡排序最好的时间复杂度为:o(n)。

若初始文件是反序的,需要进行趟排序。每趟排序要进行次关键字的比较(1≤i≤n-1),且每次比较都必须移动记录三次来达到交换记录位置。在这种情况下,比较和移动次数均达到最大值:C(max) = n(n-1)/2 = o(n^2) M(max) =  3n(n-1)/2 = o(n^2)。所以,冒泡排序的最坏时间复杂度:o(n^2)

综上,因此冒泡排序总的平均时间复杂度为:o(n^2)。

空间复杂度:

该算法只有在进行数据交换时最多需要一个临时变量,因此空间复杂度为o(1)。

算法稳定性:

冒泡排序就是把小的元素往前调或者把大的元素往后调。比较是相邻的两个元素比较,交换也发生在这两个元素之间。所以,如果两个元素相等,则不需要进行交换;如果两个相等的元素没有相邻,那么即使通过前面的两两交换把两个相邻起来,这时候也不会交换,因此相同元素的前后顺序不会发生改变,冒泡排序是一种稳定排序算法。


[cpp] view plaincopyprint?
  1. #include <stdio.h>  
  2.   
  3. void Swap(int *a,int *b)  
  4. {  
  5.     int tmp = *a;  
  6.     *a = *b;  
  7.     *b = tmp;  
  8. }  
  9.   
  10. void BubbleSort(int arr[],int len)  
  11. {  
  12.     /*需要n-1趟排序*/  
  13.     for (int i = 0; i < len - 1; ++i)  
  14.     {  
  15.         for (int j = 0; j < len -1 - i; ++j)  
  16.         {  
  17.             if (arr[j] > arr[j+1])  
  18.             {  
  19.                 swap(&arr[j],&arr[j+1]);  
  20.             }  
  21.         }  
  22.     }  
  23. }  
  24.   
  25. int main(int argc, char const *argv[])  
  26. {  
  27.     int arr[10] = {9,8,7,6,5,4,3,2,1,0};  
  28.     int len = sizeof(arr)/sizeof(int);  
  29.   
  30.     for (int i = 0; i < len; ++i)  
  31.     {  
  32.         printf("%d ",arr[i]);  
  33.     }  
  34.     printf("\n");  
  35.   
  36.     BubbleSort(arr,len);  
  37.   
  38.     for (int i = 0; i < len; ++i)  
  39.     {  
  40.         printf("%d ", arr[i]);  
  41.     }  
  42.     printf("\n");  
  43.     return 0;  
  44. }  

直接插入排序:

有一个已经有序的数据序列,要求在这个已经排好的数据序列中插入一个数,但要求插入后此数据序列仍然有序。

算法步骤:

1.从第一个元素开始,该元素可以认为已经被排序。
2. 取出下一个元素,在已经排序的元素序列中从后向前扫描。
3. 如果该元素(已排序)大于新元素,将该元素移到下一位置。
4. 重复步骤3,直到找到已排序的元素小于或者等于新元素的位置。
5. 将新元素插入到下一位置中。
6. 重复步骤2。

时间复杂度:

如果目标是把n个元素的序列升序排列,那么采用插入排序存在最好情况和最坏情况。最好情况就是,序列已经是升序排列了,在这种情况下,需要进行的比较操作需(n-1)次即可。最坏情况就是,序列是降序排列,那么此时需要进行的比较共有n(n-1)/2次。插入排序的赋值操作是比较操作的次数加上 (n-1)次。平均来说插入排序算法的时间复杂度为O(n^2)。因而,插入排序不适合对于数据量比较大的排序应用。但是,如果需要排序的数据量很小,例如,量级小于千,那么插入排序还是一个不错的选择。

空间复杂度:

直接插入排序只有需要一个临时变量存储将要插入的数据,因此空间复杂度为o(1)。

算法稳定性:

直接插入排序是在一个已经有序的小序列的基础上,一次插入一个元素。当然,刚开始这个有序的小序列只有1个元素,就是第一个元素。比较是从有序序列的末尾开始,也就是想要插入的元素和已经有序的最大者开始比起,如果比它大则直接插入在其后面,否则一直往前找直到找到它该插入的位置。如果碰见一个和插入元素相等的,那么插入元素把想插入的元素放在相等元素的后面。所以,相等元素的前后顺序没有改变,从原无序序列出去的顺序就是排好序后的顺序,所以插入排序是稳定的。

[cpp] view plaincopyprint?
  1. #include <stdio.h>  
  2.   
  3. void InsertSort(int arr[],int n)  
  4. {  
  5.     for (int i = 1; i < n; ++i)  
  6.     {  
  7.         int tmp = arr[i];  
  8.         int j;  
  9.         for (j = i; j > 0 && arr[j-1] > tmp ; --j)  
  10.         {  
  11.             arr[j] = arr[j-1];  
  12.         }  
  13.         arr[j] = tmp;  
  14.     }  
  15. }  
  16.   
  17. int main(int argc, char const *argv[])  
  18. {  
  19.     int arr[10] = {9,8,7,6,5,4,3,2,1,0};  
  20.     int len = sizeof(arr)/sizeof(int);  
  21.     for (int i = 0; i < len; ++i)  
  22.     {  
  23.         printf("%d ", arr[i]);  
  24.     }  
  25.     printf("\n");  
  26.       
  27.     InsertSort(arr,len);  
  28.       
  29.     for (int i = 0; i < len; ++i)  
  30.     {  
  31.         printf("%d ", arr[i]);  
  32.     }  
  33.     printf("\n");  
  34.     return 0;  
  35. }  

希尔排序:

希尔排序(Shell Sort)是插入排序的一种。是针对直接插入排序算法的改进。该方法又称缩小增量排序,因DL.Shell于1959年提出而得名。

算法步骤:

先取一个小于n的整数d1作为第一个增量,把文件的全部记录分成d1个组。所有距离为d1的倍数的记录放在同一个组中。先在各组内进行直接插入排序;然后,取第二个增量d2<d1重复上述的分组和排序,直至所取的增量dt=1(dt<dt-l<…<d2<d1),即所有记录放在同一组中进行直接插入排序为止。

时间复杂度:

希尔排序的时间复杂度与增量序列的选取有关,例如希尔增量时间复杂度为O(n^2),而Hibbard增量的希尔排序的时间复杂度为O(N^(5/4)),但是现今仍然没有人能找出希尔排序的精确下界。

空间复杂度:

希尔排序只有需要一个临时变量存储将要插入的数据,因此空间复杂度为o(1)。

算法稳定性:

由于进行了多次直接插入排序,我们知道一次插入排序是稳定的,不会改变相同元素的相对顺序,但在不同的插入排序过程中,相同的元素可能在各自的插入排序中移动,最后其稳定性就会被打乱,所以希尔排序是不稳定的。

[cpp] view plaincopyprint?
  1. #include <stdio.h>  
  2.   
  3.   
  4. void ShellSort1(int arr[],int n)  
  5. {  
  6.     /*步长为gap,每次排序后gap减半,知道gap =1 */  
  7.     for (int gap = n/2; gap > 0; gap /= 2)  
  8.     {  
  9.         /*对各组进行排序*/  
  10.         for (int i = gap; i < n; ++i)  
  11.         {  
  12.             int j;  
  13.             int tmp = arr[i];  
  14.             for (j = i; j >= gap && arr[j - gap] > tmp; j -= gap)  
  15.             {  
  16.                 arr[j] = arr[j - gap];  
  17.             }  
  18.             arr[j] = tmp;  
  19.         }  
  20.     }  
  21.   
  22.   
  23. }  
  24.   
  25.   
  26. void ShellSort2(int arr[],int n)  
  27. {  
  28.     /*步长为gap,每次排序后gap减半,知道gap =1 */  
  29.     for (int gap = n/2; gap > 0; gap /= 2)  
  30.     {  
  31.         /*对各组进行排序*/  
  32.         for (int i = gap; i < n; ++i)  
  33.         {  
  34.             int j;  
  35.             int tmp = arr[i];  
  36.             for (j = i; j >= gap && arr[j -gap] > tmp; j -= gap)  
  37.             {  
  38.                 arr[j] = arr[j - gap];  
  39.                 arr[j -gap] = tmp ;  
  40.             }  
  41.               
  42.         }  
  43.     }  
  44.   
  45.   
  46. }  
  47.   
  48.   
  49. int main(int argc, char const *argv[])  
  50. {  
  51.     int arr[10] = {9,8,7,6,5,4,3,2,1,0};  
  52.     int len = sizeof(arr)/sizeof(int);  
  53.     for (int i = 0; i < len; ++i)  
  54.     {  
  55.         printf("%d ", arr[i]);  
  56.     }  
  57.     printf("\n");  
  58.   
  59.   
  60.     ShellSort1(arr,len);  
  61.   
  62.   
  63.     for (int i = 0; i < len; ++i)  
  64.     {  
  65.         printf("%d ", arr[i]);  
  66.     }  
  67.     printf("\n");  
  68.   
  69.   
  70.     return 0;  
  71. }  


简单选择排序:

每一趟从待排序的数据元素中选出最小(或最大)的一个元素,顺序放在已排好序的数列的最后,直到全部待排序的数据元素排完。 

算法步骤:

n个记录的文件的直接选择排序可经过n-1趟直接选择排序得到有序结果:
1.初始状态:无序区为R[1..n],有序区为空。
2.第1趟排序在无序区R[1..n]中选出关键字最小的记录R[k],将它与无序区的第1个记录R[1]交换,使R[1..1]和R[2..n]分别变为记录个数增加1个的新有序区和记录个数减少1个的新无序区。

3.第i趟排序开始时,当前有序区和无序区分别为R[1..i-1]和R(i..n)。该趟排序从当前无序区中选出关键字最小的记录 R[k],将它与无序区的第1个记录R交换,使R[1..i]和R分别变为记录个数增加1个的新有序区和记录个数减少1个的新无序区。
这样,n个记录的文件的直接选择排序可经过n-1趟直接选择排序得到有序结果。

时间复杂度:

选择排序的交换操作介于 0 和 (n - 1) 次之间。选择排序的比较操作为 n (n - 1) / 2 次之间。选择排序的赋值操作介于 0 和 3 (n - 1) 次之间。 比较次数O(n^2),比较次数与关键字的初始状态无关,总的比较次数N=(n-1)+(n-2)+...+1=n*(n-1)/2。交换次数O(n),最好情况是,已经有序,交换0次;最坏情况是,逆序,交换n-1次。因此简单选择排序的时间复杂度是O(n^2)。

空间复杂度:

这里需要一个额外的空间存储当前临时的最小值,因此空间复杂度为O(1)。

算法稳定性:

选择排序是给每个位置选择当前元素最小的,比如给第一个位置选择最小的,在剩余元素里面给第二个元素选择第二小的,依次类推,直到第n-1个元素,第n个元素不用选择了,因为只剩下它一个最大的元素了。那么,在一趟选择,如果当前元素比一个元素小,而该小的元素又出现在一个和当前元素相等的元素后面,那么交换后稳定性就被破坏了。比较拗口,举个例子,序列5 8 5 2 9,我们知道第一遍选择第1个元素5会和2交换,那么原序列中2个5的相对前后顺序就被破坏了,所以选择排序不是一个稳定的排序算法。

[cpp] view plaincopyprint?
  1. #include <stdio.h>  
  2.   
  3. void swap(int *a,int *b)  
  4. {  
  5.     int tmp = *a;  
  6.     *a = *b;  
  7.     *b = tmp;  
  8. }  
  9.   
  10. void SimpleSelectSort(int arr[],int len)  
  11. {  
  12.       
  13.     for (int i = 0; i < len; ++i)  
  14.     {  
  15.         int min_index = i;  
  16.         for (int j = i + 1; j < len ; ++j)  
  17.         {  
  18.             if (arr[j] < arr[min_index])  
  19.             {  
  20.                 min_index = j;  
  21.             }  
  22.         }  
  23.         if (min_index != i)  
  24.         {  
  25.             swap(&arr[i],&arr[min_index]);  
  26.         }  
  27.     }  
  28. }  
  29.   
  30.   
  31. int main(int argc, char const *argv[])  
  32. {  
  33.     int arr[10] = {9,8,7,6,5,4,3,2,1,0};  
  34.     int len = sizeof(arr)/sizeof(int);  
  35.   
  36.     for (int i = 0; i < len; ++i)  
  37.     {  
  38.         printf("%d ", arr[i]);  
  39.     }  
  40.     printf("\n");  
  41.   
  42.     SimpleSelectSort(arr,len);  
  43.   
  44.     for (int i = 0; i < len; ++i)  
  45.     {  
  46.         printf("%d ", arr[i]);  
  47.     }  
  48.     printf("\n");  
  49.     return 0;  
  50. }  

堆排序:

堆排序(Heapsort)是指利用堆这种数据结构所设计的一种排序算法,可以利用数组的特点快速定位指定索引的元素。

算法步骤:

堆排序利用了大根堆(或小根堆)堆顶记录的关键字最大(或最小)这一特征,使得在当前无序区中选取最大(或最小)关键字的记录变得简单。 (1)用大根堆排序的基本思想 :
① 先将初始文件R[1..n]建成一个大根堆,此堆为初始的无序区 。
② 再将关键字最大的记录R[1](即堆顶)和无序区的最后一个记录R[n]交换,由此得到新的无序区R[1..n-1]和有序区R[n],且满足R[1..n-1].keys≤R[n].key 。
③由于交换后新的根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]调整为堆。 …… 直到无序区只有一个元素为止。 
(2)大根堆排序算法的基本操作:
 ① 初始化操作:将R[1..n]构造为初始堆。
 ② 每一趟排序的基本操作:将当前无序区的堆顶记录R[1]和该区间的最后一个记录交换,然后将新的无序区调整为堆(亦称重建堆)。
 注意:
 ①只需做n-1趟排序,选出较大的n-1个关键字即可以使得文件递增有序。 
②用小根堆排序与利用大根堆类似,只不过其排序结果是递减有序的。堆排序和直接选择排序相反:在任何时刻堆排序中无序区总是在有序区之前,且有序区是在原向量的尾部由后往前逐步扩大至整个向量为止。

时间复杂度:

堆排序的时间,主要由建立初始堆和反复重建堆这两部分的时间开销构成。堆排序的最坏时间复杂度为O(nlogn)。堆序的平均性能较接近于最坏性能。由于建初始堆所需的比较次数较多,所以堆排序不适宜于记录数较少的文件

空间复杂度:

和上面算法一样,堆排序是就地排序,辅助空间为O(1)。

算法稳定性:

堆排序是不稳定的排序方法。
[cpp] view plaincopyprint?
  1. #include <stdio.h>  
  2. void swap(int *a,int *b)  
  3. {  
  4.     int tmp = *a;  
  5.     *a = *b;  
  6.     *b = tmp;  
  7. }  
  8. /*堆调整*/  
  9. void HeapAdjust(int *arr,int i,int size){  
  10.     int lchild = 2*i+1;    //节点i的左子节点  
  11.     int rchild = 2*(i+1);  //节点i的右子节点  
  12.     int max = i;             
  13.     if (i <= size/2)       //只对非叶节点进行调整  
  14.     {  
  15.         if (lchild < size && arr[lchild] > arr[max])  
  16.         {  
  17.             max = lchild;  
  18.         }  
  19.         if (rchild < size && arr[rchild] > arr[max])  
  20.         {  
  21.             max = rchild;  
  22.         }  
  23.   
  24.   
  25.         if (max != i)  
  26.         {  
  27.             swap(&arr[i],&arr[max]);  
  28.             HeapAdjust(arr,max,size);//对调整过的max节点重新进行堆调整  
  29.         }  
  30.     }  
  31.   
  32.   
  33. }  
  34. /*建立无序大顶堆*/  
  35. void BulidHeap(int *arr,int size)  
  36. {  
  37.     for (int i = size/2; i >= 0; --i)  
  38.     {  
  39.         HeapAdjust(arr,i,size);  
  40.     }  
  41. }  
  42.   
  43.   
  44. /*堆排序*/  
  45. void HeapSort(int *arr, int size)  
  46. {  
  47.     BulidHeap(arr,size);  
  48.     for (int i = size - 1; i > 0; --i)  
  49.     {  
  50.         swap(&arr[0], &arr[i]);  
  51.         HeapAdjust(arr,0,i);  
  52.     }  
  53. }  
  54.   
  55.   
  56. int main(int argc, char const *argv[])  
  57. {  
  58.     int size = 10;  
  59.     int arr[10] = {9,8,7,6,5,4,3,2,1,0};  
  60.     for (int i = 0; i < size; ++i)  
  61.     {  
  62.         printf("%d ",arr[i]);  
  63.     }  
  64.     printf("\n");  
  65.     HeapSort(arr, size);  
  66.   
  67.   
  68.     for (int i = 0; i < size; ++i)  
  69.     {  
  70.         printf("%d ",arr[i]);  
  71.     }  
  72.     printf("\n");  
  73.     return 0;  
  74. }  


归并排序:

归并(Merge)排法是将两个(或两个以上)有序表合并成一个新的有序表,即把待排序序列分为若干个子序列,每个子序列是有序的。然后再把有序子序列合并为整体有序序列的排序算法。归并排序是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为2-路归并。

算法步骤:

归并操作的工作原理如下:第一步:申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列第二步:设定两个指针,最初位置分别为两个已经排序序列的起始位置第三步:比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置重复步骤3直到某一指针达到序列尾将另一序列剩下的所有元素直接复制到合并序列尾。
例如:
如 设有数列{6,202,100,301,38,8,1} 
初始状态:[6] [202] [100] [301] [38] [8] [1]      比较次数 
i=1   [6 202 ] [ 100 301] [ 8 38] [ 1 ]3
i=2[ 6 100 202 301 ] [ 1 8 38 ]
i=3 [ 1 6 8 38 100 202 301 ]4
 总计: 11次

时间复杂度:

将参加排序的初始序列分成长度为1的子序列进行第一趟排序,得到 n / 2 个长度为 2 的各自有序的子序列(若n为奇数,还会存在一个最后元素的子序列),再调用排序函数进行第二趟排序,得到 n / 4 个长度为 4 的各自有序的子序列, 第 i 趟排序就是两两归并长度为 2^(i-1) 的子序列得到 n / (2^i) 长度为 2^i 的子序列,直到最后只剩一个长度为n的子序列。由此看出,一共需要 log2n 趟排序,每一趟排序的时间复杂度是 O(n), 由此可知 该算法的总的时间复杂度是是 O(n log2n)。

空间复杂度:

归并排序算法需要和原数据规模一样大的辅助空间 ,因此其空间复杂度是 O(n)。

算法稳定性:

归并(Merge)排序法是一个稳定的排序算法。

[cpp] view plaincopyprint?
  1. #include <stdio.h>  
  2.   
  3. int arr[10] = {9,8,7,6,5,4,3,2,1,0};  
  4. int tmp[10];   
  5.   
  6. void Merge(int begin,int middle,int end)  
  7. {  
  8.     int i = begin;  
  9.     int j = middle + 1;  
  10.     int k = begin;  
  11.       
  12.     while(i <= middle && j <= end)  
  13.     {  
  14.         if (arr[i] <= arr[j] )  
  15.         {  
  16.             tmp[k++] = arr[i++];  
  17.         }  
  18.         else  
  19.         {  
  20.             tmp[k++] = arr[j++];  
  21.         }  
  22.     }  
  23.   
  24.     while(i <= middle)  
  25.     {  
  26.         tmp[k++] = arr[i++];  
  27.     }  
  28.   
  29.     while(j <= end)  
  30.     {  
  31.         tmp[k++] = arr[j++];  
  32.     }  
  33.   
  34.     for (k = begin; k <= end; ++k)  
  35.     {  
  36.         arr[k] = tmp[k];  
  37.     }  
  38. }  
  39.   
  40. void MergeSort(int begin,int end)  
  41. {  
  42.     if (begin < end)  
  43.     {  
  44.         int middle = (begin + end) / 2;  
  45.         MergeSort(begin,middle);  
  46.         MergeSort(middle + 1,end);  
  47.         Merge(begin,middle,end);  
  48.     }  
  49. }  
  50.   
  51.   
  52. int main(int argc, char const *argv[])  
  53. {  
  54.     int len = sizeof(arr)/sizeof(int);  
  55.     for (int i = 0; i < len; ++i)  
  56.     {  
  57.         printf("%d ",arr[i] );  
  58.     }  
  59.     printf("\n");  
  60.       
  61.     MergeSort(0,len - 1);  
  62.       
  63.     for (int i = 0; i < len; ++i)  
  64.     {  
  65.         printf("%d ",arr[i] );  
  66.     }  
  67.     printf("\n");  
  68.     return 0;  
  69. }  


快速排序:

快速排序(Quicksort)是对冒泡排序的一种改进。由C. A. R. Hoare在1962年提出。它的基本思想是:通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。

算法步骤:

一趟快速排序的算法是:
 1)设置两个变量i、j,排序开始的时候:i=0,j=N-1;
 2)以第一个数组元素作为关键数据,赋值给key,即key=A[0];
 3)从j开始向前搜索,即由后开始向前搜索(j -- ),找到第一个小于key的值A[j],A[i]与A[j]交换; 
4)从i开始向后搜索,即由前开始向后搜索(i ++ ),找到第一个大于key的A[i],A[i]与A[j]交换; 
5)重复第3、4、5步,直到 I=J; (3,4步是在程序中没找到时候j=j-1,i=i+1,直至找到为止。找到并交换的时候i, j指针位置不变。另外当i=j这过程一定正好是i+或j-完成的最后令循环结束。
例如:
待排序的数组A的值分别是:(初始关键数据:key=49) 注意关键key永远不变,永远是和key进行比较,无论在什么位置,最后的目的就是把key放在中间,小的放前面大的放后面。
A[0]
A[1]
A[2]
A[3]
A[4]
A[5]
A[6]
49
38
65
97
76
13
27
进行第一次交换后:27 38 65 97 76 13 49
( 按照算法的第三步从后面开始找,此时:J=6)
进行第二次交换后:27 38 49 97 76 13 65
( 按照算法的第四步从前面开始找>key的值,65>49,两者交换,此时:I=2 )
进行第三次交换后:27 38 13 97 76 49 65
( 按照算法的第五步将又一次执行算法的第三步从后开始找
进行第四次交换后:27 38 13 49 76 97 65
( 按照算法的第四步从前面开始找大于key的值,97>49,两者交换,此时:I=3,J=5 )
此时再执行第三和四步的时候就发现i=J=4,从而结束一趟快速排序,那么经过一趟快速排序之后的结果是:27 38 13 49 76 97 65,即所有大于key49的数全部在49的后面,所有小于key(49)的数全部在key(49)的前面。

时间复杂度:

快速排序每次将待排序数组分为两个部分,在理想状况下,每一次都将待排序数组划分成等长两个部分,则需要logn次划分。而在最坏情况下,即数组已经有序或大致有序的情况下,每次划分只能减少一个元素,快速排序将不幸退化为冒泡排序,所以快速排序时间复杂度下界为O(nlogn),最坏情况为O(n^2)。在实际应用中,快速排序的平均时间复杂度为O(nlogn)

空间复杂度:

快速排序需要一个额外的空间存储关键元素(pivot element),因此空间复杂度为O(1)。

算法稳定性:

快速排序不稳定,且是在关键元素和某个元素发生交换时导致稳定性被破坏,比如序列5 3 3 4 3 8 9 10 11, 现在中枢元素5和3(第5个元素,下标从1开始计)交换就会破坏元素3的稳定性。

[cpp] view plaincopyprint?
  1. #include <stdio.h>  
  2. #include <string.h>  
  3. #include <malloc.h>  
  4.   
  5. void swap(int *a,int *b)  
  6. {  
  7.     int tmp;  
  8.     tmp = *a;  
  9.     *a = *b;  
  10.     *b = tmp;  
  11. }  
  12.   
  13. int partition(int *arr,int low,int high)  
  14. {  
  15.     int pivot = arr[low];  
  16.     while(low < high)  
  17.     {  
  18.         while(low < high && arr[high] >= pivot)  
  19.         {  
  20.             high--;  
  21.         }  
  22.         if (low < high)  
  23.         {  
  24.             swap(&arr[low],&arr[high]);  
  25.             low++;  
  26.         }  
  27.         while(low < high && arr[low] <= pivot)  
  28.         {  
  29.             low++;  
  30.         }  
  31.         if(low < high)  
  32.         {  
  33.             swap(&arr[low],&arr[high]);  
  34.             high--;  
  35.         }  
  36.     }  
  37.   
  38.     return low;  
  39.   
  40. }  
  41. void quickSort(int *arr,int low,int high)  
  42. {  
  43.     int pivotpos;  
  44.     if (low < high)  
  45.     {  
  46.         pivotpos = partition(arr,low,high);  
  47.         quickSort(arr,low,pivotpos-1);  
  48.         quickSort(arr,pivotpos+1,high);  
  49.     }  
  50. }  
  51.   
  52.   
  53. int main(int argc, char const *argv[])  
  54. {  
  55.     int n ;  
  56.     printf("please input the length of arr:\n");  
  57.     scanf("%d",&n);  
  58.     //int *arr  = (int*)malloc(n * sizeof(int));  
  59.     int arr[n];  
  60.     printf("please input  %d numbers for each elements\n", n);  
  61.     for(int i = 0; i < n; i++)  
  62.     {  
  63.         scanf("%d",&arr[i]);  
  64.           
  65.     }  
  66.     quickSort(arr,0,n-1);  
  67.     for (int i = 0; i < n; ++i)  
  68.     {  
  69.         printf("%d ",arr[i]);  
  70.     }  
  71.     printf("\n");  
  72.     return 0;  
  73. }  

以上就是基本的数据结构排序算法,欢迎补充和讨论。




http://www.cppblog.com/guogangj/archive/2009/11/13/100876.html

这可能是最有趣的一节。排序的考题,在各大公司的笔试里最喜欢出了,但我看多数考得都很简单,通常懂得冒泡排序就差不多了,确实,我在刚学数据机构时候,觉得冒泡排序真的很精妙,我怎么就想不出呢?呵呵,其实冒泡通常是效率最差的排序算法,差多少?请看本文,你一定不会后悔的。

1、冒泡排序Bubbler Sort

前面刚说了冒泡排序的坏话,但冒泡排序也有其优点,那就是好理解,稳定,再就是空间复杂度低,不需要额外开辟数组元素的临时保存控件,当然了,编写起来也容易。

其算法很简单,就是比较数组相邻的两个值,把大的像泡泡一样“冒”到数组后面去,一共要执行N的平方除以2这么多次的比较和交换的操作(N为数组元素),其复杂度为Ο(n²),如图:

 

 2、直接插入排序Straight Insertion Sort

冒泡法对于已经排好序的部分(上图中,数组显示为白色底色的部分)是不再访问的,插入排序却要,因为它的方法就是从未排序的部分中取出一个元素,插入到已经排好序的部分去,插入的位置我是从后往前找的,这样可以使得如果数组本身是有序(顺序)的话,速度会非常之快,不过反过来,数组本身是逆序的话,速度也就非常之慢了,如图:

3、二分插入排序(Binary Insertion Sort

这是对直接插入排序的改进,由于已排好序的部分是有序的,所以我们就能使用二分查找法确定我们的插入位置,而不是一个个找,除了这点,它跟插入排序没什么区别,至于二分查找法见我前面的文章(本系列文章的第四篇)。图跟上图没什么差别,差别在于插入位置的确定而已,性能却能因此得到不少改善。(性能分析后面会提到)

4、直接选择排序(Straight Selection Sort

这是我在学数据结构前,自己能够想得出来的排序法,思路很简单,用打擂台的方式,找出最大的一个元素,和末尾的元素交换,然后再从头开始,查找第1个到第N-1个元素中最大的一个,和第N-1个元素交换……其实差不多就是冒泡法的思想,但整个过程中需要移动的元素比冒泡法要少,因此性能是比冒泡法优秀的。看图:

5、快速排序(Quick Sort

快速排序是非常优秀的排序算法,初学者可能觉得有点难理解,其实它是一种“分而治之”的思想,把大的拆分为小的,小的再拆分为更小的,所以你一会儿从代码中就能很清楚地看到,用了递归。如图:

其中要选择一个轴值,这个轴值在理想的情况下就是中轴,中轴起的作用就是让其左边的元素比它小,它右边的元素不小于它。(我用了“不小于”而不是“大于”是考虑到元素数值会有重复的情况,在代码中也能看出来,如果把“>=”运算符换成“>”,将会出问题)当然,如果中轴选得不好,选了个最大元素或者最小元素,那情况就比较糟糕,我选轴值的办法是取出第一个元素,中间的元素和最后一个元素,然后从这三个元素中选中间值,这已经可以应付绝大多数情况。

6、改进型快速排序(Improved Quick Sort

快速排序的缺点是使用了递归,如果数据量很大,大量的递归调用会不会导致性能下降呢?我想应该会的,所以我打算作这么种优化,考虑到数据量很小的情况下,直接选择排序和快速排序的性能相差无几,那当递归到子数组元素数目小于30的时候,我就是用直接选择排序,这样会不会提高一点性能呢?我后面分析。排序过程可以参考前面两个图,我就不另外画了。

7、桶排序(Bucket Sort

这是迄今为止最快的一种排序法,其时间复杂度仅为Ο(n),也就是线性复杂度!不可思议吧?但它是有条件的。举个例子:一年的全国高考考生人数为500万,分数使用标准分,最低100,最高900,没有小数,你把这500万元素的数组排个序。我们抓住了这么个非常特殊的条件,就能在毫秒级内完成这500万的排序,那就是:最低100,最高900,没有小数,那一共可出现的分数可能有多少种呢?一共有900-100+1=801,那么多种,想想看,有没有什么“投机取巧”的办法?方法就是创建801个“桶”,从头到尾遍历一次数组,对不同的分数给不同的“桶”加料,比如有个考生考了500分,那么就给500分的那个桶(下标为500-100)加1,完成后遍历一下这个桶数组,按照桶值,填充原数组,100分的有1000人,于是从0填到999,都填1000101分的有1200人,于是从10002019,都填入101……如图:

很显然,如果分数不是从100900的整数,而是从02亿,那就要分配2亿个桶了,这是不可能的,所以桶排序有其局限性,适合元素值集合并不大的情况。

8、基数排序(Radix Sort

基数排序是对桶排序的一种改进,这种改进是让“桶排序”适合于更大的元素值集合的情况,而不是提高性能。它的思想是这样的,比如数值的集合是8位整数,我们很难创建一亿个桶,于是我们先对这些数的个位进行类似桶排序的排序(下文且称作“类桶排序”吧),然后再对这些数的十位进行类桶排序,再就是百位……一共做8次,当然,我说的是思路,实际上我们通常并不这么干,因为C++的位移运算速度是比较快,所以我们通常以“字节”为单位进行桶排序。但下图为了画图方便,我是以半字节(4 bit)为单位进行类桶排序的,因为字节为单位进行桶排得画256个桶,有点难画,如图:

基数排序适合数值分布较广的情况,但由于需要额外分配一个跟原始数组一样大的暂存空间,它的处理也是有局限性的,对于元素数量巨大的原始数组而言,空间开销较大。性能上由于要多次“类桶排序”,所以不如桶排序。但它的复杂度跟桶排序一样,也是Ο(n),虽然它用了多次循环,但却没有循环嵌套。

9、性能分析和总结

先不分析复杂度为Ο(n)的算法,因为速度太快,而且有些条件限制,我们先分析前六种算法,即:冒泡,直接插入,二分插入,直接选择,快速排序和改进型快速排序。

我的分析过程并不复杂,尝试产生一个随机数数组,数值范围是07FFF,这正好可以用C++的随机函数rand()产生随机数来填充数组,然后尝试不同长度的数组,同一种长度的数组尝试10次,以此得出平均值,避免过多波动,最后用Excel对结果进行分析,OK,上图了。

最差的一眼就看出来了,是冒泡,直接插入和直接选择旗鼓相当,但我更偏向于使用直接选择,因为思路简单,需要移动的元素相对较少,况且速度还稍微快一点呢,从图中看,二分插入的速度比直接插入有了较大的提升,但代码稍微长了一点点。

令人感到比较意外的是快速排序,3万点以内的快速排序所消耗的时间几乎可以忽略不计,速度之快,令人振奋,而改进型快速排序的线跟快速排序重合,因此不画出来。看来要对快速排序进行单独分析,我加大了数组元素的数目,从5万到150万,画出下图:

可以看到,即便到了150万点,两种快速排序也仅需差不多半秒钟就完成了,实在快,改进型快速排序性能确实有微略提高,但并不明显,从图中也能看出来,是不是我设置的最小快速排序元素数目不太合适?但我尝试了好几个值都相差无几。

最后看线性复杂度的排序,速度非常惊人,我从40万测试到1200万,结果如图:

可见稍微调整下算法,速度可以得到质的飞升,而不是我们以前所认为的那样:再快也不会比冒泡法快多少啊?

我最后制作一张表,比较一下这些排序法:

还有一个最后:附上我的代码。


0 0
原创粉丝点击