转发请注明出处:https://www.cnblogs.com/fnlingnzb-learner/p/9374732.html
最近在研究一些经常用到的东西想把它们做一个汇总,想了想用到最多的应该是排序算法,所以对排序算法做了个总结,并自己用C++实现了一下。
一、算法概述
0.1 算法分类
十种常见排序算法可以分为两大类:
非线性时间比较类排序:通过比较来决定元素间的相对次序,由于其时间复杂度不能突破O(nlogn),因此称为非线性时间比较类排序。
线性时间非比较类排序:不通过比较来决定元素间的相对次序,它可以突破基于比较排序的时间下界,以线性时间运行,因此称为线性时间非比较类排序。
0.2 算法复杂度
0.3 相关概念
稳定:如果a原本在b前面,而a=b,排序之后a仍然在b的前面。
不稳定:如果a原本在b的前面,而a=b,排序之后 a 可能会出现在 b 的后面。
时间复杂度:对排序数据的总的操作次数。反映当n变化时,操作次数呈现什么规律。
空间复杂度:是指算法在计算机内执行时所需存储空间的度量,它也是数据规模n的函数。
二、快速排序
假设我们现在对“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为切入点对左右两边数按照"第一,第二,第三,第四"步骤进行,最终快排大功告成。
快速排序代码如下:
1 //快速排序,随机选取哨兵放前面 2 void QuickSort(int* h, int left, int right) 3 { 4 if(h==NULL) return; 5 if(left>=right) return; 6 7 //防止有序队列导致快速排序效率降低 8 srand((unsigned)time(NULL)); 9 int len=right-left; 10 int kindex=rand()%(len+1)+left; 11 Swap(h[left],h[kindex]); 12 13 int key=h[left],i=left,j=right; 14 while(i<j) 15 { 16 while(h[j]>=key && i<j) --j; 17 if(i<j) h[i]=h[j]; 18 while(h[i]<key && i<j) ++i; 19 if(i<j) h[j]=h[i]; 20 } 21 22 h[i]=key; 23 24 //QuickSort(&h[left],0,i-1); 25 //QuickSort(&h[j+1],0,right-j-1); 26 27 QuickSort(h,left,i-1); 28 QuickSort(h,j+1,right); 29 }
三、冒泡排序
1.原理
冒泡排序在扫描过程中两两比较相邻记录,如果反序则交换,最终,最大记录就被“沉到”了序列的最后一个位置,第二遍扫描将第二大记录“沉到”了倒数第二个位置,重复上述操作,直到n-1 遍扫描后,整个序列就排好序了。
2.算法实现
1 //冒泡排序 2 void BubbleSort(int* h, size_t len) 3 { 4 if(h==NULL) return; 5 if(len<=1) return; 6 //i是次数,j是具体下标 7 for(int i=0;i<len-1;++i) 8 for(int j=0;j<len-1-i;++j) 9 if(h[j]>h[j+1]) 10 Swap(h[j],h[j+1]); 11 12 return; 13 }
四、选择排序
选择排序也是一种简单直观的排序算法。它的工作原理很容易理解:初始时在序列中找到最小(大)元素,放到序列的起始位置作为已排序序列;然后,再从剩余未排序元素中继续寻找最小(大)元素,放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。
注意选择排序与冒泡排序的区别:冒泡排序通过依次交换相邻两个顺序不合法的元素位置,从而将当前最小(大)元素放到合适的位置;而选择排序每遍历一次都记住了当前最小(大)元素的位置,最后仅需一次交换操作即可将其放到合适的位置。
算法实现:
1 //选择排序 2 void SelectionSort(int* h, size_t len) 3 { 4 if(h==NULL) return; 5 if(len<=1) return; 6 7 int minindex,i,j; 8 //i是次数,也即排好的个数;j是继续排 9 for(i=0;i<len-1;++i) 10 { 11 minindex=i; 12 for(j=i+1;j<len;++j) 13 { 14 if(h[j]<h[minindex]) minindex=j; 15 } 16 Swap(h[i],h[minindex]); 17 } 18 19 return; 20 }
五、插入排序
直接插入排序(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
算法实现:
1 //插入排序 2 void InsertSort(int* h, size_t len) 3 { 4 if(h==NULL) return; 5 if(len<=1) return; 6 7 int i,j;