各种排序算法的分析与实现
2016-04-11 13:42
155 查看
由于最近在找工作的过程中遇见了很多考察各种排序算法的问题,而我对此却不是非常熟悉,所以花了一些时间做了一个非常简单的总结。
一.直接插入排序(straight insertion sort)
直接插入排序算法是稳定的排序算法,时间复杂度为
O(n^2)。
思想为:假设待排序的数组存放在a[1...n]中,初始时,a[1]自成一个有序区,无序区为a[2...n]。从i=2到i=n为止,依次将a[i]插入到当前有序区a[1...i-1]中去,生成n个记录的有序区。
排序方法
1.简单方法
首先在当前有序区R[1..i-1]中查找R[i]的正确插入位置k(1≤k≤i-1);然后将R[k..i-1]中的记录均后移一个位置,腾出k位置上的空间插入R[i]。
注意:若R[i]的关键字大于等于R[1..i-1]中所有记录的关键字,则R[i]就是插入原位置。
2.改进的方法
一种查找比较操作和记录移动操作交替地进行的方法。具体做法:
将待插入记录R[i]的关键字从右向左依次与有序区中记录R[j](j=i-1,i-2,…,1)的关键字进行比较:
① 若R[j]的关键字大于R[i]的关键字,则将R[j]后移一个位置;
②若R[j]的关键字小于或等于R[i]的关键字,则查找过程结束,j+1即为R[i]的插入位置。
关键字比R[i]的关键字大的记录均已后移,所以j+1的位置已经腾空,只要将R[i]直接插入此位置即可完成一趟直接插入排序。
二.希尔排序(Shell Sort)
希尔排序(Shell Sort)是插入排序的一种。也称缩小增量排序,是直接插入排序算法的一种更高效的改进版本。希尔排序是非稳定排序算法。希尔排序时间复杂度的下界是n*log2n。
希尔排序是按照不同步长对元素进行插入排序,当刚开始元素很无序的时候,步长最大,所以插入排序的元素个数很少,速度很快;当元素基本有序了,步长很小,插入排序对于有序的序列效率很高。所以,希尔排序的时间复杂度会比o(n^2)好一些。
算法思想:先取一个小于n的整数d1作为第一个增量,把文件的全部记录分组。所有距离为d1的倍数的记录放在同一个组中。先在各组内进行直接插入排序;然后,取第二个增量d2<d1重复上述的分组和排序,直至所取的增量dt
= 1,即所有记录放在同一组中进行直接插入排序为止。
该方法实质上是一种分组插入方法。
一般的初次取序列的一半为增量,以后每次减半,直到增量为1。
三.冒泡排序(Bubble Sort)
冒泡排序(Bubble Sort),是一种较简单的稳定的排序算法,时间复杂度为
O(n^2)。
思想:(从小到大排序)存在n个不同大小的气泡,由底至上地把较轻的气泡逐步地向上升,这样经过遍历一次后,最轻的气泡就会被上升到顶(下标为0),然后再从底至上地这样升,循环直至n个气泡大小有序。
在冒泡排序中,最重要的思想是两两比较,将两者较小的升上去。
四.快速排序(Quick Sort)
快速排序(Quick sort)是对冒泡排序的一种改进。不稳定。它的平均时间复杂度为O(nlgn)。
快速排序的时间主要耗费在划分操作上,对长度为k的区间进行划分,共需k-1次关键字的比较。
最坏情况是每次划分选取的基准都是当前无序区中关键字最小(或最大)的记录,划分的结果是基准左边的子区间为空(或右边的子区间为空),而划分所得的另一个非空的子区间中记录数目,仅仅比划分前的无序区中记录个数减少一个。时间复杂度为O(n*n)
在最好情况下,每次划分所取的基准都是当前无序区的"中值"记录,划分的结果是基准的左、右两个无序子区间的长度大致相等。总的关键字比较次数:O(nlgn)
尽管快速排序的最坏时间为O(n^2),但就平均性能而言,它是基于关键字比较的内部排序算法中速度最快者,快速排序亦因此而得名。
思想:设要排序的数组是A[0]……A[N-1],首先任意选取一个数据(通常选用数组的第一个数)作为关键数据,然后将所有比它小的数都放到它前面,所有比它大的数都放到它后面,这个过程称为一趟快速排序。
一趟快速排序的算法是:
1)设置两个变量i、j,排序开始的时候:i=0,j=N-1;
2)以第一个数组元素作为关键数据,赋值给key,即key=A[0];
3)从j开始向前搜索,即由后开始向前搜索(j--),找到第一个小于key的值A[j],将A[j]和A[i]互换;
4)从i开始向后搜索,即由前开始向后搜索(i++),找到第一个大于key的A[i],将A[i]和A[j]互换;
5)重复第3、4步,直到i = j
;
五.直接选择排序(Straight Select Sorting )
直接选择排序(Straight Select Sorting)
是一种较简单的不稳定的排序算法,时间复杂度为
O(n^2)。
思想:第一次从R[0]~R[n-1]中选取最小值,与R[0]交换,第二次从R[1]~R[n-1]中选取最小值,与R[1]交换,....,第i次从R[i-1]~R[n-1]中选取最小值,与R[i-1]交换,.....,第n-1次从R[n-2]~R[n-1]中选取最小值,与R[n-2]交换,总共通过n-1次,得到一个按排序码从小到大排列的有序序列。
六.堆排序(Heap Sort)
平均性能 :O(N*logN),它是不稳定的排序方法。
其他性能 :由于建初始堆所需的比较次数较多,所以堆排序不适宜于记录数较少的文件。 堆排序是就地排序,辅助空间为O(1).
小顶堆:所有子节点都大于等于其父节点;
大顶堆:所有子节点都小于等于其父节点。
若将此序列所存储的向量A[1...n]看为一颗完全二叉树的存储结构,那么堆实质上是满足一下性质的完全二叉树:树中任一非叶子结点的关键字均不大于(或不小于)其左右子节点的关键字。
因此堆排序是一种树形选择排序。
堆排序利用了大根堆(或小根堆)堆顶记录的关键字最大(或最小)这一特征,使得在当前无序区中选取最大(或最小)关键字的记录变得简单。
用大根堆排序的基本思想
① 先将初始文件R[1..n]建成一个大根堆,此堆为初始的无序区
② 再将关键字最大的记录R[1](即堆顶)和无序区的最后一个记录R
交换,由此得到新的无序区R[1..n-1]和有序区R
,且满足R[1..n-1].keys≤R
.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)大根堆排序算法的基本操作:
①建堆,建堆是不断调整堆的过程,从len/2处开始调整,一直到第一个节点,此处len是堆中元素的个数。建堆的过程是线性的过程,从len/2到0处一直调用调整堆的过程,相当于o(h1)+o(h2)…+o(hlen/2)
其中h表示节点的深度,len/2表示节点的个数,这是一个求和的过程,结果是线性的O(n)。
PS:从len/2开始调整的原因是,从最后一个非叶子节点处开始调整,它的左右叶子节点已经是堆了。每次调整一个节点之前,保证它的左右叶子一定是一个堆。
②调整堆:调整堆在构建堆的过程中会用到,而且在堆排序过程中也会用到。利用的思想是比较节点i和它的孩子节点left(i),right(i),选出三者最大(或者最小)者,如果最大(小)值不是节点i而是它的一个孩子节点,那便交换节点i和该节点,然后再调用调整堆过程,这是一个递归的过程。调整堆的过程时间复杂度与堆的深度有关系,是lgn的操作,因为是沿着深度方向进行调整的。
③堆排序:堆排序是利用上面的两个过程来进行的。首先是根据元素构建堆。然后将堆的根节点取出(一般是与最后一个节点进行交换),将前面len-1个节点继续进行堆调整的过程,然后再将根节点取出,这样一直到所有节点都取出。堆排序过程的时间复杂度是O(nlgn)。因为建堆的时间复杂度是O(n)(调用一次);调整堆的时间复杂度是lgn,调用了n-1次,所以堆排序的时间复杂度是O(nlgn) 。
调整堆(Heapify)函数思想方法:
每趟排序开始前,A[1...i]是以A[1]为根的堆,在A[1]与A[i]交换之后,新的无序区A[1...i-1]中只有A[1]的值发生了变化,故除A[1]可能违反堆的性质外,其余任何节点为根的子树均是堆。因此调整区间A[low...high]时,只须调整以A[low]为根的树就可以了。建立堆也是这样的一个过程。
七.基数排序(Radix Sort)
基数排序法是属于稳定性的排序,其时间复杂度为O
(nlog(r)m),其中r为所采取的基数,而m为堆数,在某些时候,基数排序法的效率高于其它的稳定性排序法。
原理类似桶排序,这里总是需要10个桶,多次使用
首先以个位数的值进行装桶,即个位数为1则放入1号桶,为9则放入9号桶,暂时忽视十位数
例如
待排序数组[62,14,59,88,16]简单点五个数字
分配10个桶,桶编号为0-9,以个位数数字为桶编号依次入桶,变成下边这样
| 0 | 0 | 62 | 0 | 14 | 0 | 16 | 0 | 88 | 59 |
| 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |桶编号
将桶里的数字顺序取出来,
输出结果:[62,14,16,88,59]
再次入桶,不过这次以十位数的数字为准,进入相应的桶,变成下边这样:
由于前边做了个位数的排序,所以当十位数相等时,个位数字是由小到大的顺序入桶的,就是说,入完桶还是有序
| 0 | 14,16 | 0 | 0 | 0 | 59 | 62 | 0 | 88 | 0 |
| 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |桶编号
因为没有大过100的数字,没有百位数,所以到这排序完毕,顺序取出即可
最后输出结果:[14,16,59,62,88].
既然我们可以从最低位到最高位进行如此的分配收集,那么是否可以由最高位到最低位依次操作呢? 答案是完全可以的。
基于两种不同的排序顺序,我们将基数排序分为最低位优先LSD(Least
significant digital)或最高位优先MSD(Most significant digital),
LSD的排序方式由数值的最右边(低位)开始,而MSD则相反,由数值的最左边(高位)开始。
注意一点:LSD的基数排序适用于位数少的数列,如果位数多的话,使用MSD的效率会比较好。
MSD的方式与LSD相反,是由高位数为基底开始进行分配,但在分配之后并不马上合并回一个数组中,而是在每个“桶子”中建立“子桶”,将每个桶子中的数值按照下一数位的值分配到“子桶”中。
在进行完最低位数的分配后再合并回单一的数组中。
八.归并排序(Merge Sort)
时间复杂度为O(nlogn)
这是该算法中最好、最坏和平均的时间性能。空间复杂度为
O(n)。归并排序比较占用内存,但却是一种效率高且稳定的算法。
归并排序是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divide
and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。
首先考虑下如何将将二个有序数列合并。这个非常简单,只要从比较二个数列的第一个数,谁小就先取谁,取了后就在对应数列中删除这个数。然后再进行比较,如果有数列为空,那直接将另一个数列的数据依次取出即可。
一次归并操作的代码
可以看出合并有序数列的效率是比较高的,可以达到O(n)。
解决了上面的合并有序数列问题,再来看归并排序,其的基本思路就是将数组分成二组A,B,如果这二组组内的数据都是有序的,那么就可以很方便的将这二组数据进行排序。如何让这二组组内数据有序了?
可以将A,B组各自再分成二组。依次类推,当分出来的小组只有一个数据时,可以认为这个小组组内已经达到了有序,然后再合并相邻的二个小组就可以了。这样通过先递归的分解数列,再合并数列就完成了归并排序。
一.直接插入排序(straight insertion sort)
直接插入排序算法是稳定的排序算法,时间复杂度为
O(n^2)。
思想为:假设待排序的数组存放在a[1...n]中,初始时,a[1]自成一个有序区,无序区为a[2...n]。从i=2到i=n为止,依次将a[i]插入到当前有序区a[1...i-1]中去,生成n个记录的有序区。
排序方法
1.简单方法
首先在当前有序区R[1..i-1]中查找R[i]的正确插入位置k(1≤k≤i-1);然后将R[k..i-1]中的记录均后移一个位置,腾出k位置上的空间插入R[i]。
注意:若R[i]的关键字大于等于R[1..i-1]中所有记录的关键字,则R[i]就是插入原位置。
2.改进的方法
一种查找比较操作和记录移动操作交替地进行的方法。具体做法:
将待插入记录R[i]的关键字从右向左依次与有序区中记录R[j](j=i-1,i-2,…,1)的关键字进行比较:
① 若R[j]的关键字大于R[i]的关键字,则将R[j]后移一个位置;
②若R[j]的关键字小于或等于R[i]的关键字,则查找过程结束,j+1即为R[i]的插入位置。
关键字比R[i]的关键字大的记录均已后移,所以j+1的位置已经腾空,只要将R[i]直接插入此位置即可完成一趟直接插入排序。
void insert_sort(int a[],int n) { for(int i = 0 ; i < n ; i++) { int temp = a[i] ; int j ; for(j = i-1 ; j >= 0 && temp < a[j] ; j--) { a[j+1] = a[j] ; } a[j+1] = temp ; } }
二.希尔排序(Shell Sort)
希尔排序(Shell Sort)是插入排序的一种。也称缩小增量排序,是直接插入排序算法的一种更高效的改进版本。希尔排序是非稳定排序算法。希尔排序时间复杂度的下界是n*log2n。
希尔排序是按照不同步长对元素进行插入排序,当刚开始元素很无序的时候,步长最大,所以插入排序的元素个数很少,速度很快;当元素基本有序了,步长很小,插入排序对于有序的序列效率很高。所以,希尔排序的时间复杂度会比o(n^2)好一些。
算法思想:先取一个小于n的整数d1作为第一个增量,把文件的全部记录分组。所有距离为d1的倍数的记录放在同一个组中。先在各组内进行直接插入排序;然后,取第二个增量d2<d1重复上述的分组和排序,直至所取的增量dt
= 1,即所有记录放在同一组中进行直接插入排序为止。
该方法实质上是一种分组插入方法。
一般的初次取序列的一半为增量,以后每次减半,直到增量为1。
void shell_sort(int a[],int n) { int h,i,j,temp ; for(h = n/2 ; h > 0 ; h/=2) //控制增量 { for(i = h ; h < n ; h++) //这个for循环就是前面的直接插入排序 { temp = a[i] ; for(j = i - h ; j >= 0 && temp < a[j] ; j-=h) { a[j+h] = a[j] ; } a[j+h] = temp ; } } }
三.冒泡排序(Bubble Sort)
冒泡排序(Bubble Sort),是一种较简单的稳定的排序算法,时间复杂度为
O(n^2)。
思想:(从小到大排序)存在n个不同大小的气泡,由底至上地把较轻的气泡逐步地向上升,这样经过遍历一次后,最轻的气泡就会被上升到顶(下标为0),然后再从底至上地这样升,循环直至n个气泡大小有序。
在冒泡排序中,最重要的思想是两两比较,将两者较小的升上去。
void bubble_sort(int a[],int n) { int exchange = 0 ; //用于记录每次扫描时是否发生交换 for(int i = 0 ; i < n-1 ; i++) //进行n-1趟扫描 { exchange = 0 ; for(int j = n-1 ; j >= i ; j--) //从后往前交换,这样最小值每次都到开头的位置 { if(a[j]>a[j+1]) { int temp = a[j] ; a[j] = a[j+1] ; a[j+1] = temp ; exchange = 1 ; //假如一趟扫描中至少有一组值发生了交换,那么就说明还没有序 } } if(exchange != 1) break; } }
四.快速排序(Quick Sort)
快速排序(Quick sort)是对冒泡排序的一种改进。不稳定。它的平均时间复杂度为O(nlgn)。
快速排序的时间主要耗费在划分操作上,对长度为k的区间进行划分,共需k-1次关键字的比较。
最坏情况是每次划分选取的基准都是当前无序区中关键字最小(或最大)的记录,划分的结果是基准左边的子区间为空(或右边的子区间为空),而划分所得的另一个非空的子区间中记录数目,仅仅比划分前的无序区中记录个数减少一个。时间复杂度为O(n*n)
在最好情况下,每次划分所取的基准都是当前无序区的"中值"记录,划分的结果是基准的左、右两个无序子区间的长度大致相等。总的关键字比较次数:O(nlgn)
尽管快速排序的最坏时间为O(n^2),但就平均性能而言,它是基于关键字比较的内部排序算法中速度最快者,快速排序亦因此而得名。
思想:设要排序的数组是A[0]……A[N-1],首先任意选取一个数据(通常选用数组的第一个数)作为关键数据,然后将所有比它小的数都放到它前面,所有比它大的数都放到它后面,这个过程称为一趟快速排序。
一趟快速排序的算法是:
1)设置两个变量i、j,排序开始的时候:i=0,j=N-1;
2)以第一个数组元素作为关键数据,赋值给key,即key=A[0];
3)从j开始向前搜索,即由后开始向前搜索(j--),找到第一个小于key的值A[j],将A[j]和A[i]互换;
4)从i开始向后搜索,即由前开始向后搜索(i++),找到第一个大于key的A[i],将A[i]和A[j]互换;
5)重复第3、4步,直到i = j
;
void quick_sort(int a[],int low,int high) { int i,j,pivot; //pivot基准 if(low < high) //传进来进行一次判断 { pivot = a[low] ; i = low ; j = high ; while(i<j) { while(i<j && a[j] >= pivot) j--; if(i<j) a[i++] = a[j] ; //将比pivot小的元素移到低端 while(i<j && a[i] <= pivot) i++; if(i<j) a[j--] = a[i] ; //将比pivot大的元素移到高端 } a[i] = pivot ; //pivot移到最终位置 quick_sort(a,low,i-1); //左区间递归排序 quick_sort(a,i+1,high); //右区间递归排序 } }
五.直接选择排序(Straight Select Sorting )
直接选择排序(Straight Select Sorting)
是一种较简单的不稳定的排序算法,时间复杂度为
O(n^2)。
思想:第一次从R[0]~R[n-1]中选取最小值,与R[0]交换,第二次从R[1]~R[n-1]中选取最小值,与R[1]交换,....,第i次从R[i-1]~R[n-1]中选取最小值,与R[i-1]交换,.....,第n-1次从R[n-2]~R[n-1]中选取最小值,与R[n-2]交换,总共通过n-1次,得到一个按排序码从小到大排列的有序序列。
void select_sort(int a[],int n) { for(int i = 0 ; i < n ; i++) //进行n-1次遍历 { int x = a[i] ; //每次遍历前x和l的初始设置 int l = i ; for(int j = i ; j < n ; j++) //遍历从i位置到数组的尾部 { if (a[j]<x) { x = a[j] ; //x记录最小值 l = j ; //l记录最小值的位置 } } a[l] = a[i] ; //把最小元素与a[i]进行交换 a[i] = x ; } }
六.堆排序(Heap Sort)
平均性能 :O(N*logN),它是不稳定的排序方法。
其他性能 :由于建初始堆所需的比较次数较多,所以堆排序不适宜于记录数较少的文件。 堆排序是就地排序,辅助空间为O(1).
小顶堆:所有子节点都大于等于其父节点;
大顶堆:所有子节点都小于等于其父节点。
若将此序列所存储的向量A[1...n]看为一颗完全二叉树的存储结构,那么堆实质上是满足一下性质的完全二叉树:树中任一非叶子结点的关键字均不大于(或不小于)其左右子节点的关键字。
因此堆排序是一种树形选择排序。
堆排序利用了大根堆(或小根堆)堆顶记录的关键字最大(或最小)这一特征,使得在当前无序区中选取最大(或最小)关键字的记录变得简单。
用大根堆排序的基本思想
① 先将初始文件R[1..n]建成一个大根堆,此堆为初始的无序区
② 再将关键字最大的记录R[1](即堆顶)和无序区的最后一个记录R
交换,由此得到新的无序区R[1..n-1]和有序区R
,且满足R[1..n-1].keys≤R
.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)大根堆排序算法的基本操作:
①建堆,建堆是不断调整堆的过程,从len/2处开始调整,一直到第一个节点,此处len是堆中元素的个数。建堆的过程是线性的过程,从len/2到0处一直调用调整堆的过程,相当于o(h1)+o(h2)…+o(hlen/2)
其中h表示节点的深度,len/2表示节点的个数,这是一个求和的过程,结果是线性的O(n)。
PS:从len/2开始调整的原因是,从最后一个非叶子节点处开始调整,它的左右叶子节点已经是堆了。每次调整一个节点之前,保证它的左右叶子一定是一个堆。
②调整堆:调整堆在构建堆的过程中会用到,而且在堆排序过程中也会用到。利用的思想是比较节点i和它的孩子节点left(i),right(i),选出三者最大(或者最小)者,如果最大(小)值不是节点i而是它的一个孩子节点,那便交换节点i和该节点,然后再调用调整堆过程,这是一个递归的过程。调整堆的过程时间复杂度与堆的深度有关系,是lgn的操作,因为是沿着深度方向进行调整的。
③堆排序:堆排序是利用上面的两个过程来进行的。首先是根据元素构建堆。然后将堆的根节点取出(一般是与最后一个节点进行交换),将前面len-1个节点继续进行堆调整的过程,然后再将根节点取出,这样一直到所有节点都取出。堆排序过程的时间复杂度是O(nlgn)。因为建堆的时间复杂度是O(n)(调用一次);调整堆的时间复杂度是lgn,调用了n-1次,所以堆排序的时间复杂度是O(nlgn) 。
调整堆(Heapify)函数思想方法:
每趟排序开始前,A[1...i]是以A[1]为根的堆,在A[1]与A[i]交换之后,新的无序区A[1...i-1]中只有A[1]的值发生了变化,故除A[1]可能违反堆的性质外,其余任何节点为根的子树均是堆。因此调整区间A[low...high]时,只须调整以A[low]为根的树就可以了。建立堆也是这样的一个过程。
int heapSize = 0 ; //堆大小,值为数组的长度 //返回右子节点的索引 int Left(int index){ return ((index<<1) + 1 ) ; } //返回左子节点的索引 int Right(int index){ return ((index<<1) + 2 ) ; } //交换a,b的值,指针操作 void swap(int *a,int *b){int temp = *a ; *a = *b ; *b = temp ;} //a[index]与其左右子树进行递归对比 void maxHeapify(int a[],int index) { int largest = index ; //最大数 int left = Left(index) ; //左子树索引 int right = Right(index) ; //右子树索引 if((left<=heapSize) && (a[left]>a[largest])) largest = left ; if((right<=heapSize) && (a[right]>a[largest])) largest = right ; //把largest与堆顶的左右子节点比较,取最大值 //此时largest为堆顶,左子节点,右子节点的最大值 if(largest!=index) { //如果堆顶不是最大值,那么交换并递归调整堆 swap(&a[index],&a[largest]); maxHeapify(a,largest); } } //初始化堆,将数组中的每一个元素放到合适的位置 //完成之后,堆顶元素为数组的最大值 void buildMaxheap(int a[],int n) { heapSize = n ; //堆大小为数组长度 for (int i = (n>>1) ; i>= 0 ; i--) { //从最后一个非叶子节点开始调整,因为所有的叶子结点已经为堆了 maxHeapify(a,i); } } void heap_sort(int a[],int n) { //初始化堆 buildMaxheap(a,n-1); //传进去的是数组长度 for(int i = (n-1) ; i>= 1; i--) { //堆顶元素a[0](数组的最大值)被置换到数组的尾部a[i] swap(&a[0],&a[i]); heapSize--; //从堆中移除该元素 maxHeapify(a,0);//每次调整都是从堆顶开始的,因为除堆顶外其他的节点都满足堆的性质 } }
七.基数排序(Radix Sort)
基数排序法是属于稳定性的排序,其时间复杂度为O
(nlog(r)m),其中r为所采取的基数,而m为堆数,在某些时候,基数排序法的效率高于其它的稳定性排序法。
原理类似桶排序,这里总是需要10个桶,多次使用
首先以个位数的值进行装桶,即个位数为1则放入1号桶,为9则放入9号桶,暂时忽视十位数
例如
待排序数组[62,14,59,88,16]简单点五个数字
分配10个桶,桶编号为0-9,以个位数数字为桶编号依次入桶,变成下边这样
| 0 | 0 | 62 | 0 | 14 | 0 | 16 | 0 | 88 | 59 |
| 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |桶编号
将桶里的数字顺序取出来,
输出结果:[62,14,16,88,59]
再次入桶,不过这次以十位数的数字为准,进入相应的桶,变成下边这样:
由于前边做了个位数的排序,所以当十位数相等时,个位数字是由小到大的顺序入桶的,就是说,入完桶还是有序
| 0 | 14,16 | 0 | 0 | 0 | 59 | 62 | 0 | 88 | 0 |
| 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |桶编号
因为没有大过100的数字,没有百位数,所以到这排序完毕,顺序取出即可
最后输出结果:[14,16,59,62,88].
既然我们可以从最低位到最高位进行如此的分配收集,那么是否可以由最高位到最低位依次操作呢? 答案是完全可以的。
基于两种不同的排序顺序,我们将基数排序分为最低位优先LSD(Least
significant digital)或最高位优先MSD(Most significant digital),
LSD的排序方式由数值的最右边(低位)开始,而MSD则相反,由数值的最左边(高位)开始。
注意一点:LSD的基数排序适用于位数少的数列,如果位数多的话,使用MSD的效率会比较好。
MSD的方式与LSD相反,是由高位数为基底开始进行分配,但在分配之后并不马上合并回一个数组中,而是在每个“桶子”中建立“子桶”,将每个桶子中的数值按照下一数位的值分配到“子桶”中。
在进行完最低位数的分配后再合并回单一的数组中。
int find_max(int a[],int n) { int maxx = a[0] ; for(int i = 1 ; i < n ; i++) { if(a[i]>maxx)maxx = a[i] ; } return maxx ; } //返回数字n的位数 int digit_num(int n) { int digit = 0 ; do { ++digit ; n/=10 ; } while (n!=0); return digit ; } //返回数组num的第k位数字 int kth_digit(int num,int k) { num/=pow(10,k) ; return num%10 ; } //对长度为n的数组进行基数排序 void radix_sort(int a[],int n) { int *temp[10] ; //指针数组,每一个数组表示一个箱子 int count[10] = {0,0,0,0,0,0,0,0,0,0} ; //用于存储每个箱子装的元素个数 int maxx = find_max(a,n); //取得数组中的最大值 int maxDigit = digit_num(maxx) ; //取得maxx的位数 for(int i = 0 ; i < 10 ; i++) { temp[i] = new int ; //使得每一个箱子可以装下n的元素 memset(temp[i],0,sizeof(int)*n) ; } for(int i = 0 ; i < maxDigit ; i++) { memset(count,0,sizeof(count)) ; // 将count清空 for(int j = 0 ; j < n ; j++) { int x = kth_digit(a[j],i) ; //将数据按照位数的值放入数组中 temp[x][count[x]] = a[j] ; count[x]++ ; //此箱子里面装入的数字加1 } int num = 0 ; for(int j = 0 ; j < 10 ; j++) { for(int k = 0 ; k < count[j] ; k++) { a[num++] = temp[j][k] ; } } } }
八.归并排序(Merge Sort)
时间复杂度为O(nlogn)
这是该算法中最好、最坏和平均的时间性能。空间复杂度为
O(n)。归并排序比较占用内存,但却是一种效率高且稳定的算法。
归并排序是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divide
and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。
首先考虑下如何将将二个有序数列合并。这个非常简单,只要从比较二个数列的第一个数,谁小就先取谁,取了后就在对应数列中删除这个数。然后再进行比较,如果有数列为空,那直接将另一个数列的数据依次取出即可。
一次归并操作的代码
//将有序数组a[]和b[]合并到c[]中 void MemeryArray(int a[], int n, int b[], int m, int c[]) { int i, j, k; i = j = k = 0; while (i < n && j < m) { if (a[i] < b[j]) c[k++] = a[i++]; else c[k++] = b[j++]; } while (i < n) c[k++] = a[i++]; while (j < m) c[k++] = b[j++]; }
可以看出合并有序数列的效率是比较高的,可以达到O(n)。
解决了上面的合并有序数列问题,再来看归并排序,其的基本思路就是将数组分成二组A,B,如果这二组组内的数据都是有序的,那么就可以很方便的将这二组数据进行排序。如何让这二组组内数据有序了?
可以将A,B组各自再分成二组。依次类推,当分出来的小组只有一个数据时,可以认为这个小组组内已经达到了有序,然后再合并相邻的二个小组就可以了。这样通过先递归的分解数列,再合并数列就完成了归并排序。
//将结果填入临时数组然后将临时数组拷贝到原始数组 //将有二个有序数列a[first...mid]和a[mid...last]合并。 void Merge(int a[],int temp[],int first,int middle,int last) { int i = first , j = middle+1 ; int n = middle , m = last ; int k = 0 ; while(i<=n&&j<=m) { if(a[i]<=a[j]) { temp[k++]=a[i++]; } else temp[k++]=a[j++]; } while(i<=n) temp[k++]=a[i++]; while(j<=m) temp[k++]=a[j++]; for(i = 0 ; i < k ; i++) { a[first+i]=temp[i] ; } } void msort(int a[],int temp[],int low,int high) { if(low>=high) return ; //结束条件 int middle = (low+high)/2; //分裂点 msort(a,temp,low,middle); msort(a,temp,middle+1,high); Merge(a,temp,low,middle,high); //组合,将两个有序区间组合成一个 } void merge_sort(int a[],int n) { int *temp = new int ; // 分配临时数组空间 if(temp!=NULL) { msort(a,temp,0,n-1) ; //调用msort归并排序 delete []temp ; //释放临时空间的地址 } }
相关文章推荐
- kafka源码解析之十四TopicConfigManager
- 重新学习运维知识的决心书
- 「产品经理」和「功能经理」的区别
- 误删 libc.so.6的解决方法(开机启动不了)
- js-DOM、BOM相关方法及属性的应用
- java.net.BindException: Cannot assign requested address: bind
- kafka源码解析之十三KafkaHealthcheck
- 控制符号的可见性
- 求集合的全排列
- 少女花海自拍撞亡:自拍PK火车速度,没有赢家
- UVM:8.4.3 用factory 机制创建实例的接口
- 前端学习笔录--1--HTML篇
- java project获取项目配置文件内容
- 使用apktool工具遇到could not decode arsc file的解决办法
- RelativeLayout各个属性
- @import导入外部样式表与link链入外部样式表的区别
- C# 类型实例化的语法糖--unity下诡异结果
- C语言实现队循FIFO缓冲区-《30天自制操作系统》
- 《java编程思想》14章类型信息 读书笔记
- tableView中deselectRowAtIndexPath的作用 TableView didSelectRowAtIndexPath 不执行