冒泡排序:
重复地走访过要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来。走访数列的工作是重复地进行直到不再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端,故名。
算法步骤:
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)。
算法稳定性:
冒泡排序就是把小的元素往前调或者把大的元素往后调。比较是相邻的两个元素比较,交换也发生在这两个元素之间。所以,如果两个元素相等,则不需要进行交换;如果两个相等的元素没有相邻,那么即使通过前面的两两交换把两个相邻起来,这时候也不会交换,因此相同元素的前后顺序不会发生改变,冒泡排序是一种稳定排序算法。
- #include <stdio.h>
- void Swap(int *a,int *b)
- {
- int tmp = *a;
- *a = *b;
- *b = tmp;
- }
- void BubbleSort(int arr[],int len)
- {
- /*需要n-1趟排序*/
- for (int i = 0; i < len - 1; ++i)
- {
- for (int j = 0; j < len -1 - i; ++j)
- {
- if (arr[j] > arr[j+1])
- {
- swap(&arr[j],&arr[j+1]);
- }
- }
- }
- }
- int main(int argc, char const *argv[])
- {
- int arr[10] = {9,8,7,6,5,4,3,2,1,0};
- int len = sizeof(arr)/sizeof(int);
- for (int i = 0; i < len; ++i)
- {
- printf("%d ",arr[i]);
- }
- printf("\n");
- BubbleSort(arr,len);
- for (int i = 0; i < len; ++i)
- {
- printf("%d ", arr[i]);
- }
- printf("\n");
- return 0;
- }
直接插入排序:
有一个已经有序的数据序列,要求在这个已经排好的数据序列中插入一个数,但要求插入后此数据序列仍然有序。
算法步骤:
1.从第一个元素开始,该元素可以认为已经被排序。
2. 取出下一个元素,在已经排序的元素序列中从后向前扫描。
3. 如果该元素(已排序)大于新元素,将该元素移到下一位置。
4. 重复步骤3,直到找到已排序的元素小于或者等于新元素的位置。
5. 将新元素插入到下一位置中。
6. 重复步骤2。
时间复杂度:
如果目标是把n个元素的序列升序排列,那么采用插入排序存在最好情况和最坏情况。最好情况就是,序列已经是升序排列了,在这种情况下,需要进行的比较操作需(n-1)次即可。最坏情况就是,序列是降序排列,那么此时需要进行的比较共有n(n-1)/2次。插入排序的赋值操作是比较操作的次数加上 (n-1)次。平均来说插入排序算法的时间复杂度为O(n^2)。因而,插入排序不适合对于数据量比较大的排序应用。但是,如果需要排序的数据量很小,例如,量级小于千,那么插入排序还是一个不错的选择。
空间复杂度:
直接插入排序只有需要一个临时变量存储将要插入的数据,因此空间复杂度为o(1)。
算法稳定性:
直接插入排序是在一个已经有序的小序列的基础上,一次插入一个元素。当然,刚开始这个有序的小序列只有1个元素,就是第一个元素。比较是从有序序列的末尾开始,也就是想要插入的元素和已经有序的最大者开始比起,如果比它大则直接插入在其后面,否则一直往前找直到找到它该插入的位置。如果碰见一个和插入元素相等的,那么插入元素把想插入的元素放在相等元素的后面。所以,相等元素的前后顺序没有改变,从原无序序列出去的顺序就是排好序后的顺序,所以插入排序是稳定的。
- #include <stdio.h>
- void InsertSort(int arr[],int n)
- {
- for (int i = 1; i < n; ++i)
- {
- int tmp = arr[i];
- int j;
- for (j = i; j > 0 && arr[j-1] > tmp ; --j)
- {
- arr[j] = arr[j-1];
- }
- arr[j] = tmp;
- }
- }
- int main(int argc, char const *argv[])
- {
- int arr[10] = {9,8,7,6,5,4,3,2,1,0};
- int len = sizeof(arr)/sizeof(int);
- for (int i = 0; i < len; ++i)
- {
- printf("%d ", arr[i]);
- }
- printf("\n");
- InsertSort(arr,len);
- for (int i = 0; i < len; ++i)
- {
- printf("%d ", arr[i]);
- }
- printf("\n");
- return 0;
- }
希尔排序:
希尔排序(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)。
算法稳定性:
由于进行了多次直接插入排序,我们知道一次插入排序是稳定的,不会改变相同元素的相对顺序,但在不同的插入排序过程中,相同的元素可能在各自的插入排序中移动,最后其稳定性就会被打乱,所以希尔排序是不稳定的。
- #include <stdio.h>
- void ShellSort1(int arr[],int n)
- {
- /*步长为gap,每次排序后gap减半,知道gap =1 */
- for (int gap = n/2; gap > 0; gap /= 2)
- {
- /*对各组进行排序*/
- for (int i = gap; i < n; ++i)
- {
- int j;
- int tmp = arr[i];
- for (j = i; j >= gap && arr[j - gap] > tmp; j -= gap)
- {
- arr[j] = arr[j - gap];
- }
- arr[j] = tmp;
- }
- }
- }
- void ShellSort2(int arr[],int n)
- {
- /*步长为gap,每次排序后gap减半,知道gap =1 */
- for (int gap = n/2; gap > 0; gap /= 2)
- {
- /*对各组进行排序*/
- for (int i = gap; i < n; ++i)
- {
- int j;
- int tmp = arr[i];
- for (j = i; j >= gap && arr[j -gap] > tmp; j -= gap)
- {
- arr[j] = arr[j - gap];
- arr[j -gap] = tmp ;
- }
- }
- }
- }
- int main(int argc, char const *argv[])
- {
- int arr[10] = {9,8,7,6,5,4,3,2,1,0};
- int len = sizeof(arr)/sizeof(int);
- for (int i = 0; i < len; ++i)
- {
- printf("%d ", arr[i]);
- }
- printf("\n");
- ShellSort1(arr,len);
- for (int i = 0; i < len; ++i)
- {
- printf("%d ", arr[i]);
- }
- printf("\n");
- return 0;
- }
简单选择排序:
每一趟从待排序的数据元素中选出最小(或最大)的一个元素,顺序放在已排好序的数列的最后,直到全部待排序的数据元素排完。
算法步骤:
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趟直接选择排序得到有序结果。
时间复杂度:
空间复杂度:
算法稳定性:
- #include <stdio.h>
- void swap(int *a,int *b)
- {
- int tmp = *a;
- *a = *b;
- *b = tmp;
- }
- void SimpleSelectSort(int arr[],int len)
- {
- for (int i = 0; i < len; ++i)
- {
- int min_index = i;
- for (int j = i + 1; j < len ; ++j)
- {
- if (arr[j] < arr[min_index])
- {
- min_index = j;
- }
- }
- if (min_index != i)
- {
- swap(&arr[i],&arr[min_index]);
- }
- }
- }
- int main(int argc, char const *argv[])
- {
- int arr[10] = {9,8,7,6,5,4,3,2,1,0};
- int len = sizeof(arr)/sizeof(int);
- for (int i = 0; i < len; ++i)
- {
- printf("%d ", arr[i]);
- }
- printf("\n");
- SimpleSelectSort(arr,len);
- for (int i = 0; i < len; ++i)
- {
- printf("%d ", arr[i]);
- }
- printf("\n");
- return 0;
- }
堆排序:
堆排序(Heapsort)是指利用堆这种数据结构所设计的一种排序算法,可以利用数组的特点快速定位指定索引的元素。
算法步骤:
时间复杂度:
空间复杂度:
算法稳定性:
- #include <stdio.h>
- void swap(int *a,int *b)
- {
- int tmp = *a;
- *a = *b;
- *b = tmp;
- }
- /*堆调整*/
- void HeapAdjust(int *arr,int i,int size){
- int lchild = 2*i+1; //节点i的左子节点
- int rchild = 2*(i+1); //节点i的右子节点
- int max = i;
- if (i <= size/2) //只对非叶节点进行调整
- {
- if (lchild < size && arr[lchild] > arr[max])
- {
- max = lchild;
- }
- if (rchild < size && arr[rchild] > arr[max])
- {
- max = rchild;
- }
- if (max != i)
- {
- swap(&arr[i],&arr[max]);
- HeapAdjust(arr,max,size);//对调整过的max节点重新进行堆调整
- }
- }
- }
- /*建立无序大顶堆*/
- void BulidHeap(int *arr,int size)
- {
- for (int i = size/2; i >= 0; --i)
- {
- HeapAdjust(arr,i,size);
- }
- }
- /*堆排序*/
- void HeapSort(int *arr, int size)
- {
- BulidHeap(arr,size);
- for (int i = size - 1; i > 0; --i)
- {
- swap(&arr[0], &arr[i]);
- HeapAdjust(arr,0,i);
- }
- }
- int main(int argc, char const *argv[])
- {
- int size = 10;
- int arr[10] = {9,8,7,6,5,4,3,2,1,0};
- for (int i = 0; i < size; ++i)
- {
- printf("%d ",arr[i]);
- }
- printf("\n");
- HeapSort(arr, size);
- for (int i = 0; i < size; ++i)
- {
- printf("%d ",arr[i]);
- }
- printf("\n");
- return 0;
- }
归并排序:
归并(Merge)排序法是将两个(或两个以上)有序表合并成一个新的有序表,即把待排序序列分为若干个子序列,每个子序列是有序的。然后再把有序子序列合并为整体有序序列的排序算法。归并排序是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。它将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为2-路归并。
算法步骤:
时间复杂度:
空间复杂度:
算法稳定性:
- #include <stdio.h>
- int arr[10] = {9,8,7,6,5,4,3,2,1,0};
- int tmp[10];
- void Merge(int begin,int middle,int end)
- {
- int i = begin;
- int j = middle + 1;
- int k = begin;
- while(i <= middle && j <= end)
- {
- if (arr[i] <= arr[j] )
- {
- tmp[k++] = arr[i++];
- }
- else
- {
- tmp[k++] = arr[j++];
- }
- }
- while(i <= middle)
- {
- tmp[k++] = arr[i++];
- }
- while(j <= end)
- {
- tmp[k++] = arr[j++];
- }
- for (k = begin; k <= end; ++k)
- {
- arr[k] = tmp[k];
- }
- }
- void MergeSort(int begin,int end)
- {
- if (begin < end)
- {
- int middle = (begin + end) / 2;
- MergeSort(begin,middle);
- MergeSort(middle + 1,end);
- Merge(begin,middle,end);
- }
- }
- int main(int argc, char const *argv[])
- {
- int len = sizeof(arr)/sizeof(int);
- for (int i = 0; i < len; ++i)
- {
- printf("%d ",arr[i] );
- }
- printf("\n");
- MergeSort(0,len - 1);
- for (int i = 0; i < len; ++i)
- {
- printf("%d ",arr[i] );
- }
- printf("\n");
- return 0;
- }
快速排序:
快速排序(Quicksort)是对冒泡排序的一种改进。由C. A. R. Hoare在1962年提出。它的基本思想是:通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。
算法步骤:
A[0]
|
A[1]
|
A[2]
|
A[3]
|
A[4]
|
A[5]
|
A[6]
|
49
|
38
|
65
|
97
|
76
|
13
|
27
|
时间复杂度:
空间复杂度:
算法稳定性:
快速排序不稳定,且是在关键元素和某个元素发生交换时导致稳定性被破坏,比如序列5 3 3 4 3 8 9 10 11, 现在中枢元素5和3(第5个元素,下标从1开始计)交换就会破坏元素3的稳定性。
- #include <stdio.h>
- #include <string.h>
- #include <malloc.h>
- void swap(int *a,int *b)
- {
- int tmp;
- tmp = *a;
- *a = *b;
- *b = tmp;
- }
- int partition(int *arr,int low,int high)
- {
- int pivot = arr[low];
- while(low < high)
- {
- while(low < high && arr[high] >= pivot)
- {
- high--;
- }
- if (low < high)
- {
- swap(&arr[low],&arr[high]);
- low++;
- }
- while(low < high && arr[low] <= pivot)
- {
- low++;
- }
- if(low < high)
- {
- swap(&arr[low],&arr[high]);
- high--;
- }
- }
- return low;
- }
- void quickSort(int *arr,int low,int high)
- {
- int pivotpos;
- if (low < high)
- {
- pivotpos = partition(arr,low,high);
- quickSort(arr,low,pivotpos-1);
- quickSort(arr,pivotpos+1,high);
- }
- }
- int main(int argc, char const *argv[])
- {
- int n ;
- printf("please input the length of arr:\n");
- scanf("%d",&n);
- //int *arr = (int*)malloc(n * sizeof(int));
- int arr[n];
- printf("please input %d numbers for each elements\n", n);
- for(int i = 0; i < n; i++)
- {
- scanf("%d",&arr[i]);
- }
- quickSort(arr,0,n-1);
- for (int i = 0; i < n; ++i)
- {
- printf("%d ",arr[i]);
- }
- printf("\n");
- return 0;
- }
以上就是基本的数据结构排序算法,欢迎补充和讨论。
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,都填1000,101分的有1200人,于是从1000到2019,都填入101……如图:
很显然,如果分数不是从100到900的整数,而是从0到2亿,那就要分配2亿个桶了,这是不可能的,所以桶排序有其局限性,适合元素值集合并不大的情况。
8、基数排序(Radix Sort)
基数排序是对桶排序的一种改进,这种改进是让“桶排序”适合于更大的元素值集合的情况,而不是提高性能。它的思想是这样的,比如数值的集合是8位整数,我们很难创建一亿个桶,于是我们先对这些数的个位进行类似桶排序的排序(下文且称作“类桶排序”吧),然后再对这些数的十位进行类桶排序,再就是百位……一共做8次,当然,我说的是思路,实际上我们通常并不这么干,因为C++的位移运算速度是比较快,所以我们通常以“字节”为单位进行桶排序。但下图为了画图方便,我是以半字节(4 bit)为单位进行类桶排序的,因为字节为单位进行桶排得画256个桶,有点难画,如图:
基数排序适合数值分布较广的情况,但由于需要额外分配一个跟原始数组一样大的暂存空间,它的处理也是有局限性的,对于元素数量巨大的原始数组而言,空间开销较大。性能上由于要多次“类桶排序”,所以不如桶排序。但它的复杂度跟桶排序一样,也是Ο(n),虽然它用了多次循环,但却没有循环嵌套。
9、性能分析和总结
先不分析复杂度为Ο(n)的算法,因为速度太快,而且有些条件限制,我们先分析前六种算法,即:冒泡,直接插入,二分插入,直接选择,快速排序和改进型快速排序。
我的分析过程并不复杂,尝试产生一个随机数数组,数值范围是0到7FFF,这正好可以用C++的随机函数rand()产生随机数来填充数组,然后尝试不同长度的数组,同一种长度的数组尝试10次,以此得出平均值,避免过多波动,最后用Excel对结果进行分析,OK,上图了。
最差的一眼就看出来了,是冒泡,直接插入和直接选择旗鼓相当,但我更偏向于使用直接选择,因为思路简单,需要移动的元素相对较少,况且速度还稍微快一点呢,从图中看,二分插入的速度比直接插入有了较大的提升,但代码稍微长了一点点。
令人感到比较意外的是快速排序,3万点以内的快速排序所消耗的时间几乎可以忽略不计,速度之快,令人振奋,而改进型快速排序的线跟快速排序重合,因此不画出来。看来要对快速排序进行单独分析,我加大了数组元素的数目,从5万到150万,画出下图:
可以看到,即便到了150万点,两种快速排序也仅需差不多半秒钟就完成了,实在快,改进型快速排序性能确实有微略提高,但并不明显,从图中也能看出来,是不是我设置的最小快速排序元素数目不太合适?但我尝试了好几个值都相差无几。
最后看线性复杂度的排序,速度非常惊人,我从40万测试到1200万,结果如图:
可见稍微调整下算法,速度可以得到质的飞升,而不是我们以前所认为的那样:再快也不会比冒泡法快多少啊?
我最后制作一张表,比较一下这些排序法:
还有一个最后:附上我的代码。