常见的各种排序
算法复杂度
快速排序
1.原理
假设我们现在对“6 1 2 7 9 3 4 5 10 8”这个10个数进行排序。首先在这个序列中随便找一个数作为基准数。为了方便,就让第一个数6作为基准数吧。接下来,需要将这个序列中所有比基准数大的数放在6的右边,比基准数小的数放在6的左边,类似下面这种排列:
3 1 2 5 4 6 9 7 10 8
在初始状态下,数字6在序列的第1位。我们的目标是将6挪到序列中间的某个位置,假设这个位置是k。现在就需要寻找这个k,并且以第k位为分界点,左边的数都小于等于6,右边的数都大于等于6,递归对左右两个区间进行同样排序即可。想一想,你有办法可以做到这点吗?这就是快速排序所解决的问题。
快速排序是C.R.A.Hoare于1962年提出的一种划分交换排序。它采用了一种分治的策略,通常称其为分治法(Divide-and-ConquerMethod)。它的平均时间复杂度为O(nlogn),最坏时间复杂度为O(n^2).
从图中我们可以看到:
left指针,right指针,base参照数。
其实思想是蛮简单的,就是通过第一遍的遍历(让left和right指针重合)来找到数组的切割点。
第一步:首先我们从数组的left位置取出该数(20)作为基准(base)参照物。(如果是选取随机的,则找到随机的哨兵之后,将它与第一个元素交换,开始普通的快排)
第二步:从数组的right位置向前找,一直找到比(base)小的数,如果找到,将此数赋给left位置(也就是将10赋给20),此时数组为:10,40,50,10,60, left和right指针分别为前后的10。
第三步:从数组的left位置向后找,一直找到比(base)大的数,如果找到,将此数赋给right的位置(也就是40赋给10),此时数组为:10,40,50,40,60, left和right指针分别为前后的40。
第四步:重复“第二,第三“步骤,直到left和right指针重合,最后将(base)放到40的位置, 此时数组值为:10,20,50,40,60,至此完成一次排序。
第五步:此时20已经潜入到数组的内部,20的左侧一组数都比20小,20的右侧作为一组数都比20大, 以20为切入点对左右两边数按照"第一,第二,第三,第四"步骤进行,最终快排大功告成。
2.算法实现
//快速排序,随机选取哨兵放前面void QuickSort(int* h, int left, int right){ if(h==NULL) return; if(left>=right) return; //防止有序队列导致快速排序效率降低 srand((unsigned)time(NULL)); int len=right-left; int kindex=rand()%(len+1)+left; Swap(h[left],h[kindex]); int key=h[left],i=left,j=right; while(i { while(h[j]>=key && i if(i while(h[i] if(i } h[i]=key; //QuickSort(&h[left],0,i-1); //QuickSort(&h[j+1],0,right-j-1); QuickSort(h,left,i-1); QuickSort(h,j+1,right);}
冒泡排序
1.原理
冒泡排序在扫描过程中两两比较相邻记录,如果反序则交换,最终,最大记录就被“沉到”了序列的最后一个位置,第二遍扫描将第二大记录“沉到”了倒数第二个位置,重复上述操作,直到n-1 遍扫描后,整个序列就排好序了。
2.算法实现
//冒泡排序void BubbleSort(int* h, size_t len){ if(h==NULL) return; if(len<=1) return; //i是次数,j是具体下标 for(int i=0;i for(int j=0;j if(h[j]>h[j+1]) Swap(h[j],h[j+1]); return;}
选择排序
1.原理
选择排序也是一种简单直观的排序算法。它的工作原理很容易理解:初始时在序列中找到最小(大)元素,放到序列的起始位置作为已排序序列;然后,再从剩余未排序元素中继续寻找最小(大)元素,放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。
注意选择排序与冒泡排序的区别:冒泡排序通过依次交换相邻两个顺序不合法的元素位置,从而将当前最小(大)元素放到合适的位置;而选择排序每遍历一次都记住了当前最小(大)元素的位置,最后仅需一次交换操作即可将其放到合适的位置。
2.算法实现
//选择排序void SelectionSort(int* h, size_t len){ if(h==NULL) return; if(len<=1) return; int minindex,i,j; //i是次数,也即排好的个数;j是继续排 for(i=0;i { minindex=i; for(j=i+1;j { if(h[j] } Swap(h[i],h[minindex]); } return;}
插入排序
1.原理
直接插入排序(straight insertion sort),有时也简称为插入排序(insertion sort),是减治法的一种典型应用。其基本思想如下:
对于一个数组A[0,n]的排序问题,假设认为数组在A[0,n-1]排序的问题已经解决了。
考虑A[n]的值,从右向左扫描有序数组A[0,n-1],直到第一个小于等于A[n]的元素,将A[n]插在这个元素的后面。
很显然,基于增量法的思想在解决这个问题上拥有更高的效率。
直接插入排序对于最坏情况(严格递减的数组),需要比较和移位的次数为n(n-1)/2;对于最好的情况(严格递增的数组),需要比较的次数是n-1,需要移位的次数是0。当然,对于最好和最坏的研究其实没有太大的意义,因为实际情况下,一般不会出现如此极端的情况。然而,直接插入排序对于基本有序的数组,会体现出良好的性能,这一特性,也给了它进一步优化的可能性。(希尔排序)。直接插入排序的时间复杂度是O(n^2),空间复杂度是O(1),同时也是稳定排序。
下面用一个具体的场景,直观地体会一下直接插入排序的过程:
场景:
现有一个无序数组,共7个数:89 45 54 29 90 34 68。
使用直接插入排序法,对这个数组进行升序排序。
89 45 54 29 90 34 68
45 89 54 29 90 34 68
45 54 89 29 90 34 68
29 45 54 89 90 34 68
29 45 54 89 90 34 68
29 34 45 54 89 90 68
29 34 45 54 68 89 90
2.算法实现
//插入排序void InsertSort(int* h, size_t len){ if(h==NULL) return; if(len<=1) return; int i,j; //i是次数,也即排好的个数;j是继续排 for(i=1;i for(j=i;j>0;--j) if(h[j]-1]) Swap(h[j],h[j else break; return;}
归并排序
1.原理
归并排序(MERGE-SORT)是利用归并的思想实现的排序方法,该算法采用经典的分治(divide-and-conquer)策略(分治法将问题分(divide)成一些小的问题然后递归求解,而治(conquer)的阶段则将分的阶段得到的各答案"修补"在一起,即分而治之)。
分而治之
2.算法实现
//归并排序void MergeArray(int* arr, size_t left, size_t mid, size_t right, int* temp){ if(arr==NULL) return; size_t i=left,j=mid+1,k=0; while(i<=mid && j<=right) { if(arr[i]<=arr[j]) { temp[k++]=arr[i++]; continue; } temp[k++]=arr[j++]; } while(i<=mid) temp[k++]=arr[i++]; while(j<=right) temp[k++]=arr[j++]; memcpy(&arr[left],temp,k*sizeof(int)); return;}void MMergeSort(int* arr, size_t left, size_t right, int* temp){ if(left { size_t mid=(left+right)/2; MMergeSort(arr, left, mid, temp); MMergeSort(arr, mid+1,right, temp); MergeArray(arr,left, mid, right, temp); }}void MergeSort(int* h, size_t len){ if(h==NULL) return; if(len<=1) return; int* temp=(int*)calloc(len,sizeof(int)); MMergeSort(h, 0, len-1, temp); memcpy(h,temp,sizeof(int)*len); free(temp); return;}
希尔排序
1.原理
希尔排序是希尔(Donald Shell)于1959年提出的一种排序算法。希尔排序也是一种插入排序,它是简单插入排序经过改进之后的一个更高效的版本,也称为缩小增量排序,同时该算法是冲破O(n2)的第一批算法之一。本文会以图解的方式详细介绍希尔排序的基本思想及其代码实现。
基本思想
希尔排序是把记录按下标的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至1时,整个文件恰被分成一组,算法便终止。
简单插入排序很循规蹈矩,不管数组分布是怎么样的,依然一步一步的对元素进行比较,移动,插入,比如[5,4,3,2,1,0]这种倒序序列,数组末端的0要回到首位置很是费劲,比较和移动元素均需n-1次。而希尔排序在数组中采用跳跃式分组的策略,通过某个增量将数组元素划分为若干组,然后分组进行插入排序,随后逐步缩小增量,继续按组进行插入排序操作,直至增量为1。希尔排序通过这种策略使得整个数组在初始阶段达到从宏观上看基本有序,小的基本在前,大的基本在后。然后缩小增量,到增量为1时,其实多数情况下只需微调即可,不会涉及过多的数据移动。
我们来看下希尔排序的基本步骤,在此我们选择增量gap=length/2,缩小增量继续以gap = gap/2的方式,这种增量选择我们可以用一个序列来表示,{n/2,(n/2)/2...1},称为增量序列。希尔排序的增量序列的选择与证明是个数学难题,我们选择的这个增量序列是比较常用的,也是希尔建议的增量,称为希尔增量,但其实这个增量序列不是最优的。此处我们做示例使用希尔增量。
2.算法实现
//希尔排序void ShellSort(int* h, size_t len){ if(h==NULL) return; if(len<=1) return; for(int div=len/2;div>=1;div/=2) for(int k=0;k
for(int i=div+k;i
for(int j=i;j>k;j-=div)
if(h[j]
else break;
return;
}
堆排序
1.原理
堆排序实际上是利用堆的性质来进行排序的,要知道堆排序的原理我们首先一定要知道什么是堆。 堆的定义: 堆实际上是一棵完全二叉树。 堆满足两个性质: 1、堆的每一个父节点都大于(或小于)其子节点; 2、堆的每个左子树和右子树也是一个堆。 堆的分类: 堆分为两类: 1、最大堆(大顶堆):堆的每个父节点都大于其孩子节点; 2、最小堆(小顶堆):堆的每个父节点都小于其孩子节点;
堆的存储: 一般都用数组来表示堆,i结点的父结点下标就为(i – 1) / 2。它的左右子结点下标分别为2 * i + 1和2 * i + 2。如下图所示:
堆排序: 由上面的介绍我们可以看出堆的第一个元素要么是最大值(大顶堆),要么是最小值(小顶堆),这样在排序的时候(假设共n个节点),直接将第一个元素和最后一个元素进行交换,然后从第一个元素开始进行向下调整至第n-1个元素。所以,如果需要升序,就建一个大堆,需要降序,就建一个小堆。 堆排序的步骤分为三步: 1、建堆(升序建大堆,降序建小堆); 2、交换数据; 3、向下调整。 假设我们现在要对数组arr[]={8,5,0,3,7,1,2}进行排序(降序): 首先要先建小堆:
堆建好了下来就要开始排序了:
现在这个数组就已经是有序的了。
2.算法实现
//堆排序/*大顶堆sort之后,数组为从小到大排序*///====调整=====void AdjustHeap(int* h, int node, int len) //----node为需要调整的结点编号,从0开始编号;len为堆长度{ int index=node; int child=2*index+1; //左孩子,第一个节点编号为0 while(child<len) { //右子树 if(child+1<len && h[child]1]) { child++; } if(h[index]>=h[child]) break; Swap(h[index],h[child]); index=child; child=2*index+1; }}//====建堆=====void MakeHeap(int* h, int len){ for(int i=len/2;i>=0;--i) { AdjustHeap(h, i, len); }}//====排序=====void HeapSort(int* h, int len){ MakeHeap(h, len); for(int i=len-1;i>=0;--i) { Swap(h[i],h[0]); AdjustHeap(h, 0, i); }}