一、常见的十种排序算法:
冒泡排序、选择排序、插入排序、归并排序、快速排序、希尔排序、堆排序、计数排序、桶排序、基数排序
1.【知识框架】
补充:
内部排序:整个排序过程完全在内存中进行。
外部排序:由于待排序记录数据量太大,内存无法容纳全部数据,需要借助外部存储。
二、排序方法
插入排序
•直接插入排序
1.算法思想
从待排序的第二个元素开始,向下扫描列表,比较这个目标值target与arr[i-1]、arr[i-2]的大小,依次类推。如果target的值小于或等于每一个元素值,那么每个元素都会向右滑动一个位置,一旦确定了正确位置j,目标值target(即原始的arr[i])就会被复制到这个位置。(例如:整理桥牌时,我们会将每一张牌插入到一个已经有序的序列的适当位置。为了给要插入的元素腾出空间,需要我们将已经有序的序列元素都向右移动一位)
2.算法实现
3.插入排序伪代码
1 void InsertSort (ElemType A[], int n)
2 {
3 int i,j;
4 for(i=2;i<=n;i++)
5 if(A[i].key<A[i-1].key)
6 {
7 A[0]=A[i];
8 for(j=i-1;A[0].key<A[j].key;--j)
9 A[j+i]=A[j];
10 A[j+i]=A[0];
11 }
12 }
4.稳定性
由于每次插入元素时总是从后往前先比较在移动,所以不会出现相同元素相对位置,发生变化的情况即直接插入排序是一个稳定的排序方法
5.时间复杂度:O(n²)
•希尔排序(缩小增量排序)
1.算法提出:
为解决插入排序每次只能将数据移动一位,在数组较大且基本无序的情况下性能出现的恶化所以引入其算法。
2.算法思想:
先取一个小于n的步长d1把表中全部记录分成d1个组,所有距离为d1的倍数的记录放在同一个组中,在各组中进行直接插入排序:然后取第二个步长d2<d1,重复上 述过程,直到所取到的d1=1,即所有记录已放在同一组中,再进行直接插入排序,由于此时已经具有较好的局部有序性,故可以很快得到最终结果。到目前为 止,尚未求得一个最好的增量序列,希尔提出的方法是d1=n/2,di+1=⌈di/2⌉ , 并且最后一个增量等于 1。
3.算法实现:
补充:操作原理时间复杂度与选取的增量序列有关且所取增量序列的函数介于O(N*logN)和O(n²)之间增量序列有很多种取法,但是使增量序列中的值没有除1之外的公因子,并且增量序列中最后一个值必须为1。
4.希尔排序伪代码
1 void ShellSort (ElemType A[],int n){
2 //对顺序表作希尔插入排序,基本算法和直接插入排序相比,做了以下修改:
3 //1.前后记录位置的增量是dk,不是1
4 //2.r[0]只是暂时存储单元,不是哨兵,当j<=0时,插入位置已到
5 for(dk=n/2;dk>=1,dk=dk/2) //步长变化
6 for(i=dk+1;i<=n;++i)
7 if(A[i].key<A[i-dk].key){ //需将A[i]插入有序增量子表
8 A[0]=A[i];
9 for(j=i-dk;j>0&&A[0].key<A[j].key;j-=dk)
10 A[j+dk]=A[j]; //记录后移,查找插入位置
11 A[j+dk]=A[0]; //插入
12 }//if
13 }
5.稳定性
当相同关键字的记录被划分到不同的子表时,可能会改变它们之间的相对次序,因此,希尔排序是一种不稳定的排序方法。例如,表L=[3,2,2].经过一趟排序后,L=[2,2,3],最终排序序列也是L=[2,2,3],显然2与2的相对次序已经发生了变化。
6.时间复杂度:O(N*logN)
选择排序
•简单选择排序
1.算法思想
Step1:将待排序数组分为有序和无序两组(初始情况下有序组为空)。
Step2:从左向右扫描无序组,找出最小的元素,将其放置在无序组的第一个位置。至此有序组++,无序组–;
Step3:重复Step2,直至无序组只剩下一个元素。
2.算法实现
3.选择排序伪代码
1 void ShellSort (ElemType A[],int n){
2 //对表A作简单的选择排序,A[]从0开始放元素
3 for(i=0;i<=n-1;i++){ //一共进行n-1趟
4 min=i; //记录最小元素位置
5 for(j=i+1;j<n;j++) //在A[i...n-1]中选择最小的元素
6 if(A[j]<A[min])
7 min=j; //更新最小元素位置
8 if(min!=i) swap(A[i],A[min]); //在第n个元素交换
9
10 }
11 }
4.稳定性
选择排序的时间复杂度为O(n²),由于每次选择仅考虑某一位置上的数据情况,可能会破坏之前数据的相对位置,因此它是一种不稳定的排序方法。 例如:序列 [9,9,1]第一次就将第一个[9]与[1]交换,导致第一个9挪动到第二个9后面。
5.时间复杂度: O(n²)
补充:简单选择排序的比较次数与序列的初始排序无关。假设待排序的序列有 n个元素,选择排序的赋值操作介于0和3(n - 1次之间; 则比较次数永远都是n(n-1)/2; 而移动次数(即:交换操作)与序列的初始排序有关,介于 0 和 (n - 1) 次之间。当序列正序时,移动次数最少,为 0。当序列反序时,移动次数最多,为n - 1 次;逆序交换n/2次。选择排序的移动次数比冒泡排序少多了,由于交换所需CPU时间比 比较所需的CPU时间多,n值较小时,选择排序比冒泡排序快。
•堆排序
1.引入概念
堆是一棵顺序存储的完全二叉树。其中每个结点的关键字都不大于其孩子结点的关键字,这样的堆称为小根堆。
其中每个结点的关键字都不小于其孩子结点的关键字,这样的堆称为大根堆。
举例来说,对于n个元素的序列{R0, R1, … , Rn}当且仅当满足下列关系之一时,称之为堆:
2.算法思想
堆排序(Heapsort)是指利用堆积树(堆)这种数据结构所设计的一种排序算法,它是选择排序的一种。可以利用数组的特点快速定位指定索引的元素。堆分为大根堆和小根堆,是完全二叉树。大根堆的要求是每个节点的值都不大于其父节点的值,即A[PARENT[i]] >= A[i]。在数组的非降序排序中,需要使用的就是大根堆,因为根据大根堆的要求可知,最大的值一定在堆顶。
如图:
3.算法实现(大顶堆)
注:若n为数组的个数,那么就从2/n开始,依次循环。
4.堆排序伪代码
下面是建立大根堆的算法:
1 void BuildMaxHeap (ElemType A[], int len) {
2 for(int i=len/2;i>0;i--) //从i=[n/2]~1,反复调整堆
3 AdjustDown(A, i, len) ;
4
5 }
6 void AdjustDown (ElemType A[], int k, int len) {
7 //函数AdjustDown将元素k向下进行调整
8 A[0]=A[k] ; //A[O]暂存
9 for (i=2*k;i<=len;i*=2){ //沿 key较大的子结点向下筛选
10 if (i<len&&A[i] <A[i+1])
11 i++;
12 if(A[0]>=A[i]) break; // 筛选结束
13 else{
14 A[k]=A[i]; //将A[i]调整到双亲结点上
15 k=i ; //修改k值,以便继续向下筛选
16 }
17 }//for
18 A[k]=A[0]; //被筛选结点的值放入最终位置
19 }
下面是堆排序算法:
1 void HeapSort (ElemType A[],int len) {
2 BuildMaxHeap (A,len) ; //初始建堆
3 for(i=len;i>1;i--){ //n-1 趟的交换和建堆过程
4 Swap (A[i],A[1]) ; / /输出堆顶元素(和堆底元素交换)
5 AdjustDown (A, 1, i-1) ;} //整理,把剩余的i-1个元素整理成堆
6 }//for
7 }
下面是向上调整堆的算法:
1 void AdjustUp (ElemType A[], int k) {
2 //参数k为向上调整的结点,也为堆的元素个数
3 A[0]=A[k] ;
4 int i=k/2; //若结点值大于双亲结点, 则将双亲结点向下调,并继续向上比较
5 while (i>0&&A[i]<A[0]) { //循环跳出条件
6 A[k]=A[i]; //双亲结点下调
7 k=i;
8 i=k/2; //继续向上比较
9 }//while
10 A[k]=A[0] ; //复制到最终位置
11 }
5.稳定性
堆排序是一种不稳定的排序方法。因为在堆的调整过程中,关键字进行比较和交换所走的是该结点到叶子结点的一条路径,因此对于相同的关键字就可能出现排在后面的关键字被交换到前面来的。
6.时间复杂度:O(N*logN)。
交换排序
•冒泡排序
1.算法思想
冒泡排序是一种交换排序,它的实现原理是:两两比较相邻的记录值,如果反序则交换,直到没有反序的记录为准。对数组中的各数据,依次比较相邻的两元素的大小。如果前面的数据大于后面的数据,就交换这两个数据。经过第一轮的多次比较排序后,变可把最小的数据排好。再用同样的方法把剩下的数据逐个进行比较,最后便可按照从小到大的顺序排好数组各数据的顺序。
2.算法实现
- 冒泡排序伪代码
1 void BubbleSort (ElemType A[],int n){
2 //用冒泡排序将序列A中的元素按从小到大排列
3 for(i=0;i<n-1;i++){
4 flag=false; //标示本趟冒泡是否发生交换标志
5 for(j=n-1;j>i;j--) //一趟冒泡过程
6 if(A[j-i].key>A[j].key){ //若为逆序
7 swap(A[j-1],A[j]); //交换
8 flag=true;
9 }
10 if(flag==false)
11 return; //本趟遍历没有发生交换,说明已经有序
12 }
13 }
4.稳定性
冒泡排序是把小的元素往前调或者把大的元素往后调。比较是相邻的两个元素比较,交换也发生在这两个元素之间。相同元素的前后顺序并没有改变,冒泡排序是一种稳定排序算法。
5.时间复杂度: O(n²)
补充:若文件的初始状态是正序的,一趟扫描即可完成排序。所需的关键字比较次数C和记录移动次数M均达到最小值:Cmin = N - 1, Mmin = 0。所以,冒泡排序最好时间复杂度为O(N)。若初始文件是反序的,需要进行 N -1 趟排序。每趟排序要进行 N - i 次关键字的比较(1 ≤ i ≤ N - 1),且每次比较都必须移动记录三次来达到交换记录位置。在这种情况下,比较和移动次数均达到最大值:Cmax = N(N-1)/2 = O(N^2) Mmax = 3N(N-1)/2 = O(N2)冒泡排序的最坏时间复杂度为O(N2)。因此,冒泡排序的平均时间复杂度为O(N^2)。当数据越接近正序时,冒泡排序性能越好。
•快速排序
1.基本概念
快速排序为应用最多的排序算法,因为快速二字而闻名。快速排序和归并排序一样,采用的都是分治思想。快速排序可以分为:单路快速排序,双路快速排序,三路快速排序,他们区别在于选取几个指针来对数组进行遍历 。
1.算法思想
快速排序是对冒泡排序的一种改进。其基本思想是基于分治法的:在待排序表L[`1…n]中任取一个元素 pivot作为基准,通过一趟排序表将待排序表划分为独立的两部分
L[1…k-1]和L[k+1…n],使得L[1…k-1]中所有元素小子pivot, L[k+1…n]中所有元素大于或等于pivot,则pivot放在了其最终位置L(k)上,这个过程称作一趟快速排序。 而后分别递归地对两个子表重复上述过程,直至每部分内只有一个元素或空为止,即所有元素放在了其最终位置上。
2.算法实现
注:后找小前找大
- 快速排序伪代码
首先假设划分算法已知,记为Partition(),返回的是上述中的k,注意到L(k)已经在最终的位置,所以可以先对表进行划分,而后对两个表调用同样的排序操作。因此可以递归地调用快速排序算法进行排序,具体的程序结构如下:
1 void QuickSort (ElemType A[],int low,int high){
2
3 if (low<high) { //递归跳出的条件
4 //Partition()就是划分操作,将表low-high划分为满足上述条件的两个子表
5 int pivotpos-=Partition(A, low,high); //划分//依次对两个子表进行递归排序
6 QuickSort MB (A, low,pivotpos-1);
7 QuickSort (A, pivotpos+1,high) ;
8 }
9 }
从上面的代码也 不难看出快速排序算法的关键在于划分操作,同时快速排序算法的性能也主据要取决于划分操作的好坏。假设每次总是以当前表中第一个元素作为枢轴值 (基准)对表进行划分,则必须将表中比枢轴值大的元素向右移动,比枢轴值小的元素向左移动,使得一趟Partition()操作之后,表中的元素被枢轴值一分为二。
4.稳定性
快速排序有两个方向,左边的i下标一直往右走,当a[i] <= a[center_index],其中center_index是中枢元素的数f组下标,一般取为数组第0个元素。而右边的j下标一直往左走,当a[j] > a[center_index]。如果i和j都走不动了,i <= j, 交换a[i]和a[j],重复上面的过程,直到i>j。 交换a[j]和a[center_index],完成一趟快速排序。在中枢元素和a[j]交换的时候,很有可能把前面的元素的稳定性打乱,比如序列为 5 3 3 4 3 8 9 10 11, 现在中枢元素5和3(第5个元素,下标从1开始计)交换就会把元素3的稳定性打乱,所以快速排序是一个不稳定的排序算法,不稳定发生在中枢元素和a[j] 交换的时刻。
5.时间平均复杂度: O(nlog2n)。
基数排序(桶排序)
1.基本概念
基数排序(Radix Sort)属于分配式排序,又称"桶子法"(Bucket Sort或Bin Sort),将要排序的元素分配到某些"桶"中,以达到排序的作用。基数排序属于稳定的排序,其时间复杂度为nlog®m (其中r为的采取的基数,m为堆数),基数排序的效率有时候高于其它比较性排序。
2.算法思想
- 根据输入建立适当个数的桶,每个桶可以存放某个范围内的元素;
2.将落在特定范围内的所有元素放入对应的桶中;
3.对每个非空的桶中元素进行排序,可以选择通用的排序方法,比如插入、快排;
4.按照划分的范围顺序,将桶中的元素依次取出。排序完成。 - 算法实现
数组: int arr[] = {278,109,63,930,589,184,505,269,8,83};
个位分配,个位收集,先进后出。
十位分配,十位收集,先进后出。
百位分配,百位收集,先进后出。
4.稳定性
基数排序是按照低位先排序,然后收集;再按照高位排序,然后再收集;依次类推,直到最高位。有时候有些属性是有优先级顺序的,先按低优先级排序,再按高优 先级排序,最后的次序就是高优先级高的在前,高优先级相同的低优先级高的在前。基数排序基于分别排序,分别收集,所以其是稳定的排序算法。
5.时间平均复杂度:O(d(n+r))
归并排序(二路归并排序)
1.基本概念
归并排序是通过“归并”操作完成排序的,将两个或者多个有序子表归并成一个子表。归并排序是“分治法”的一个非常典型的应用,同事它也是递归算法的一个好的实例。它将问题分成一些小的问题然后递归求解,而治就是把分阶段的答案拼起来。
2.算法思想
将一个大小为 n 的数组排序。归并排序算法的排序步骤是:
1.将所有的数字放入一个无序的堆。
2.将堆分成两部分,现在你有两个无序的堆。
3.持续将无序的堆拆分,直到无法再拆分为止,你将得到 n 个堆,每一个堆中有一个数字。
4.现在开始将这些堆按照一定顺序按对合并。每一次合并过程中,将堆中的数字放入有序的队列。这一点很容易实现,因为每一个独立的堆中的内容都是有序的。
3.算法实现
数组: int arr[] = {3,6,1,7,9,4,5,8,2}
二路归并排序的过程如图所示:
4.稳定性
归并排序的空间复杂度O(n)。另外,归并排序中归并的算法并不会将相同关键字的元素改变相对次序,所以归并排序是稳定的。
5.时间平均复杂度:O(nlog2n)。
后续未完…