转载至:https://www.cnblogs.com/fnlingnzb-learner/p/9374732.html
一、算法概述
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
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 if(h==NULL)return;
4 if(len<=1)return ;
5 //i是次数,j是具体下标
6 for(int i=0;i
9 Swap(h[j],h[j+1])
10 return ;
11 }
四、选择排序
选择排序也是一种简单直观的排序算法,它的工作原理很容易理解;初始时在序列中找到最小(大)元素,放到序列的起始位置作为已排序序列;然后,再从剩余未排序元素中继续寻找最小(大)元素,放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。
注意选择排序与冒泡排序的区别:冒泡排序通过依次交换相邻两个顺序不合法的元素位置,从而将当前最小(大)元素放到合适的位置;而选择排序每遍历一次都记住了当前最小(大)元素的位置,最后仅需一次交换操作即可将其放到合适的位置。
选择排序不稳:
举个例子,序列5 8 5 2 9, 我们知道第一遍选择第1个元素5会和2交换,那么原序列中2个5的相对前后顺序就被破坏了,所以选择排序不是一个稳定的排序算法
算法实现:
1 //选择排序
2 void SelectionSort(int* h,size_t len){
3 if(h==nullptr) return;
4 if(len<=1) return;
5
6 int minindex,i,j;
7 //i是次数,也即排好的个数;j是继续排
8 for(i=0;i<len-1;++i){
9 minindex=i;
10 for(j=i+1;i<len;++j){
11 if(h[j]<h[minindex]) minindex=j;
12 }
13 Swap(h[i],h[minindex]);
14 }
15 return;
16 }
五、插入排序
直接插入排序(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
算法实现:
//插入排序
void InsertSort(int* h,size_t len){
if(h==nullptr)return;
if(len<=1)return;
int i,j;
//i是次数,也即排好的个数;j是继续排
for(i=1;i<len;++i)
for(j=i;j>0;--j)
if(h\[j\]<h\[j-1\])Swap(h\[j\],h\[j-1\]);
else break;
return ;
}
六、归并排序
归并排序(MERGE-SORT)是利用归并的思想实现的排序方法,该算法采用经典的分治(divide-and-conquer)策略(分治法将问题分(divide)成一些小的问题然后递归求解,而治(conquer)的阶段则将分的阶段得到的各答案"修补"在一起,即分而治之)。
分而治之
可以看到这种结构很像一棵完全二叉树,本文的归并排序我们采用递归去实现(也可采用迭代的方式去实现)。分阶段可以理解为就是递归拆分子序列的过程,递归深度为log2n。
再来看看治阶段,我们需要将两个已经有序的子序列合并成一个有序序列,比如上图中的最后一次合并,要将[4,5,7,8]和[1,2,3,6]两个已经有序的子序列,合并为最终序列[1,2,3,4,5,6,7,8],来看下实现步骤。
算法实现:
1 //合并两个序列
2 void mergeArray(int arr[],int first,int mid,int last,int temp[]){
3 int i=first;
4 int j=mid+1;
5 int m=mid;
6 int n=last;
7 int k=0;
8 while(i<=m && j<=n){
9 if(arr[i]<=arr[j])
10 temp[k++]=arr[i++];
11 else
12 temp[k++]=arr[j++];
13 }
14 while(i<=m)
15 temp[k++]=arr[i++];
16 while(j<=n)
17 temp[k++]=arr[j++];
18 for(i=0;i<k;i++)
19 arr[first+i]=temp[i];
20 }
21
22 void mySort(int arr[],int first,int last,int temp[]){
23 if(first<last){
24 int mid=(first+last)/2;
25 mySort(arr,first,mid,temp);
26 mySort(arr,mid+1,last,temp);
27 mergeArray(arr,first,mid,last,temp);
28 }
29 }
30 bool mergeSort(int arr[],int len){
31 int *p=new int[len];
32 if(nullptr==p)
33 return false;
34 mySort(arr,0,len-1,p);
35 delete[] p;
36 return true;
37 }
七、希尔排序
希尔排序是希尔(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},称为增量序列。希尔排序的增量序列的选择与证明是个数学难题,我们选择的这个增量序列是比较常用的,也是希尔建议的增量,称为希尔增量,但其实这个增量序列不是最优的。此处我们做示例使用希尔增量。
算法实现:
1 //希尔排序
2 void ShellSort(int* h,size_t len){
3 if(h==nullptr)return;
4 if(len<=1)return;
5
6 for(int div=len/2;div>=1;div/=2)
7 for(int k=0;k
八、堆排序
堆排序实际上是利用堆的性质来进行排序的,要知道堆排序的原理我们首先一定要知道什么是堆。
堆的定义:
堆实际上是一棵完全二叉树。
堆满足两个性质:
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}进行排序(降序):
首先要先建小堆:
堆建好了下来就要开始排序了:
现在这个数组就已经是有序的了。
算法实现:
1 //调整堆
2 void adjust_heap(int* a,int node,int size){
3 int left=2*node+1;
4 int right=2*node+2;
5 int max=node;
6 if(left
7 max=left;
8 }
9 if(right
10 max=right;
11 }
12 if(max!=node){
13 swap(a[max],a[node]);
14 adjust_heap(a,max,size);
15 }
16 }
17 //堆排序
18 void heap_sort(int* a,int len){
19 for(int i=len/2;i>=0;--i)
20 adjust_heap(a,i,len);
21 for(int i=len-1;i>=0;i--){
22 swap(a[0],a[i]); //将当前最大的放置到数组末尾
23 adjust_heap(a,0,i); //将未完成排序的部分继续进行堆排序
24 }
25 }
九、基数排序
基数排序与本系列前面讲解的七种排序方法都不同,它不需要比较关键字的大小。
它是根据关键字中各位的值,通过对排序的N个元素进行若干趟“分配”与“收集”来实现排序的。
1.LSD(低位到高位的排序)
不妨通过一个具体的实例来展示一下,基数排序是如何进行的。
设有一个初始序列为: R {50, 123, 543, 187, 49, 30, 0, 2, 11, 100}。
我们知道,任何一个阿拉伯数,它的各个位数上的基数都是以0~9来表示的。
所以我们不妨把0~9视为10个桶。
我们先根据序列的个位数的数字来进行分类,将其分到指定的桶中。例如:R[0] = 50,个位数上是0,将这个数存入编号为0的桶中。
分类后,我们在从各个桶中,将这些数按照从编号0到编号9的顺序依次将所有数取出来。
这时,得到的序列就是个位数上呈递增趋势的序列。
按照个位数排序: {50, 30, 0, 100, 11, 2, 123, 543, 187, 49}。
接下来,可以对十位数、百位数也按照这种方法进行排序,最后就能得到排序完成的序列。
1 int maxbit(int data[],int n){
2 int d=1;//保存最大的位数
3 int p=10;
4 for(int i=0;i
6 p*=10;
7 ++d;
8 }
9 }
10 return d;
11 }
12
13 void radixsort(int data[],int n){//基数排序
14 int d=maxbit(data,n);
15 int tmp[n];
16 int count[10];//计数器
17 int i,j,k;
18 int radix=1;
19 for(i=1;i<=d;++i){
20 for(j=0;j<10;++j)
21 count[j]=0;//每次分配前清空计数器
22 for(j=0;i
29 k=(data[j]/radix)%10;
30 tmp[count[k]-1]=data[j];
31 count[k]--;
32 }
33 for(j=0;j<n;++j)//将临时数组的内容复制到data中
34 data[j]=tmp[j];
35 radix=radix*10;
36 }
37 }
手机扫一扫
移动阅读更方便
你可能感兴趣的文章