数据结构学习记录(四)
排序
一、知识要点
1、选择排序
-
简单选择排序
-
思想:在未排序的数组中选出一个最大值或最小值与序列首位元素交换,然后在剩下未排序序列再选出最大值或最小值与第二位元素交换,依次类推,直到排序完成
-
typedef int ElementType; //太简单了我就不写注释了 void SSSort(ElementType A[], int N) { int i, j, min; for(i = 0; i < N - 1; i++) { min = i; for(j = i + 1; j < N; j++) { if(A[min] >A[j]) min = j; } int t; t = A[i]; A[i] = A[min]; A[min] = t; } }
-
简单选择排序无论在什么情况下,都需要比较N*(N - 1) / 2次,故其时间复杂度为O(N2)。
-
-
堆排序
-
思想:利用堆输出堆顶元素,或最大值或最小值,其余元素会重新生成堆,继续输出堆顶元素,重复此过程,直到全部元素都已输出。
-
要得到一个从小到大的序列,想要不另外设置数组来存栈顶元素,可以先把原数组调整为最大堆,然后把堆顶和堆尾元素 i 调换,重新调整最大堆,再把堆顶的i - 1调换,重复此过程,直到i = 0。
-
void HeapSort(ElementType A[], int N) { //堆排序 int i; //生成最大堆 for(i = N / 2 - 1; i >= 0; i--) { PercDown(A, i, N); } //删除堆顶,调整最大堆 for(i = N - 1; i > 0; i--) { int t; t = A[i]; A[i] = A[0]; A[0] = t; //调整堆 PercDown(A, 0, i); //将堆容量缩小 } } //将数组以A[p]为根的子堆调整为最大堆 void PercDown(ElementType A[], int p, int N) { int Parent, Child; ElementType X; X = A[p]; for(Parent = p; (Parent*2 + 1) < N; Parent = Child) { Child = Parent * 2 + 1; if(Child != N - 1 && A[Child] < A[Child + 1]) Child++; if(A[Child] < X) break; //说明找到了合适位置 else A[Parent] = A[Child]; } A[Parent] = X; }
-
2、插入排序
-
简单插入排序
-
思想:将序列分为排好序和未排序两个部分,初始时,已排序序列只有第一个元素,此后将未排序序列的元素逐一插入到已排序序列中,直到未排序序列中元素数为0。
-
#include<stdio.h> typedef int ElementType; void InsertSort(ElementType A[], int N) { int p, i; ElementType Tmp; //从未排序序列第一个元素开始,也就是下标为1的元素开始 for(p = 1; p < N; p++) { Tmp = A[p]; for(i = p; i > 0 && A[i - 1] > Tmp; i--) //如果该元素小于左边元素 //交换两个元素 A[i] = A[i - 1]; A[i] = Tmp; //放进合适位置 } }
-
该排序有两个嵌套的for循环,所以时间复杂度为O(N2),最快的情况下只执行第一次for循环,时间复杂度为O(N)。
-
简单选择排序是稳定的排序,因为两个数值相同的元素不会交换位置。
-
-
希尔排序
-
思想:将待排序的一组元素按一定间隔分为若干个序列,分别进入插入排序,开始时设置的间隔可以为N / 2,在每轮排序中将间隔逐渐除二,直到间隔为一,最后一步就是简单插入排序。
-
这过程感觉有点像画画,先排好整体的,再逐步细化……
-
//插入排序的希尔排序 typedef int ElementType; void ShellSort(ElementType arr[], int size) { int i; //gap表示希尔增量 //希尔增量逐渐缩小 for(int gap = size / 2; gap >= 1; gap /= 2) { //从i到N - gap - 1每次进行插入排序 for(i = 0; i < size - gap; i++) { //进行插入排序 int tmp = arr[i + gap]; int j; for(j = i + gap; j >= gap && arr[j - gap] > tmp; j -= gap) { arr[j] = arr[j - gap]; } arr[j] = tmp; } } }
-
希尔排序的复杂度和简单插入排序一样,也是O(N2),不过其平均复杂度在O(N2)和O(N)之间,一般情况下比简单插入排序效率高。
-
3、交换排序
-
冒泡排序
- 思想:对元素个数为N的待排序列进行排序时,共进行N - 1次循环。在第k次循环中,对从1到N - k个元素从前往后进行比较,每次比较相邻的两个元素,若前一个元素大于(或小于)另一个元素,则两者互换位置,否则保持不变,i指向下一个元素。
-
typedef int ElementType; void BubbleSort(ElementType arr[], int N) { //冒泡排序,C语言学过,就不写注释了 int i, j; bool flag; for(i = N - 2; i >= 0; i--) { flag = false; for(j = 0; j <= i; j++) { if(arr[j] > arr[j + 1]) { ElementType t = arr[j]; arr[j] = arr[j + 1]; arr[j + 1] = t; flag = true; } } if(flag == true) break; } }
- 时间复杂度为O(N2),在序列已经是排好序的情况下,因为有flag标记,只要进行O(N)次就可以跳出循环。
-
快速排序
-
思想:快速排序采用分治法,将问题的规模缩小,然后再分别进行处理。
- 我们从原序列选择一个主元,将比主元大的元素从右向左放置,比主元小的元素从左向右放置,然后递归快排左右元素。
-
//用快速排序,找一个好的主元是很重要的,可以很好的提高排序效率 //可以将A[low]、A[high]、A[(low+high) / 2]三者关键字的中值作为主元 //在序列长度低到某个值时,快速排序的效率很低,可以在低长度序列用简单插排来排序 //快速排序 void QuickSort(vector<ElementType> &A, int N) { //统一接口 QSort(A, 0, N - 1); } //真快速排序 //从序列的范围left到right进行快排 void QSort(vector<ElementType> &A, int left, int right) { int Cutoff = 30, low, Hight; //Pivot是主元基准序列号,Cutoff是判断是否切换排序的序列长度 ElementType Pivot; //如果元素充分多,进入快排 if(Cutoff <= right - left) { //选择基准 Pivot = Median(A, left, right); low = left; Hight = right - 1; //此时基准在这段序列的Hight处 //将序列比基准小的放右边,比基准大的放左边 while(1) { while (A[++low] < Pivot); while (A[--Hight] > Pivot); if(low < Hight) //到这一步说明此时A[low]的值大于主元,A[Hight]的值小于主元,将两元素调换 swap(A[low], A[Hight]); else break;//这一步说明该序列元素已比较完毕,此时A[low]的值大于主元,A[Hight]的值小于主元 } //将A[low]和主元调换,使主元回到正确位置 swap(A[low], A[right - 1]); //分别递归解决左边序列和右边序列 QSort(A, left, low - 1); QSort(A, low + 1, right); } //如果序列长度小于阈值,用插入排序 else { InsertSort(A, left, right); } } //选择主元函数 ElementType Median(vector<ElementType> &A, int left, int right) { int center = (left + right) / 2; //判断三个序列代表的值的大小,给他们安排位置 if(A[left] > A[center]) swap(A[left], A[center]); if(A[left] > A[right]) swap(A[left], A[right]); if(A[center] > A[right]) swap(A[center], A[right]); //此时A[left] < A[center] < A[right],将A[center]元素与A[right - 1]交换 swap(A[center], A[right - 1]); //只需要考虑A[left + 1]……A[right-2] //返回主元 return A[right - 1]; } //插排 void InsertSort(vector<ElementType> &A, int left, int right) { int i, p; ElementType Tmp; //从未排序序列第一个元素开始,也就是下标为1的元素开始 for(p = left + 1; p <= right; p++) { Tmp = A[p]; for(i = p; i > 0 && A[i - 1] > Tmp; i--) //如果该元素小于左边元素 //交换两个元素 A[i] = A[i - 1]; A[i] = Tmp; //放进合适位置 } }
-
一些复杂的证明显示,快速排序的平均复杂度是O(Nlog2N),但最坏的情况下,它的复杂度就接近冒泡排序了。
-
4、归并排序
-
思想:将两个已排序的子序列合并成一个有序序列。归并排序可以用分治法去自上而下的去理解,就是将原序列分成两个等长的子序列,再递归地排序这两个子序列,最后合并成一个完整地有序序列。
-
//归并排序 // 将原序列和排序序列,以及需要归并的两个子序列的起点位序和右序列的终点传入函数 void Merge(vector<ElementType>& A, vector<ElementType>& TmpA, int L, int R, int Rend) { //归并两个有序序列 int Lend, Tmp; //左序列终点的位序 Lend = R - 1; //排序序列的起始位置 Tmp = L; int left = L; int right = Rend; //比较合并 while (L <= Lend && R <= Rend) { if (A[L] <= A[R]) TmpA[Tmp++] = A[L++]; else TmpA[Tmp++] = A[R++]; } //将多余的序列放进排序序列里 while (L <= Lend) TmpA[Tmp++] = A[L++]; while (R <= Rend) TmpA[Tmp++] = A[R++]; //最后将TmpA复制回A for (int i = left; i <= right; i++) { A[i] = TmpA[i]; } } //真·归并排序 //将原序列和排序序列以及他们的左位序和右位序传入函数 void MSort(vector<ElementType>& A, vector<ElementType> &TmpA, int left, int right) { //核心递归排序函数 int center; if (left < right) { //将序列分为左右两半 center = (left + right) / 2; //递归解决左边子序列 MSort(A, TmpA, left, center); //递归解决右边子序列 MSort(A, TmpA, center + 1, right); //合并两个子序列 Merge(A, TmpA, left, center + 1, right); } } //这是一个用来调整接口的函数 void MergeSort(vector<ElementType>& A) { //创建一个空数组用来存归并后的元素 vector<ElementType> TmpA(A.size()); //真·归并排序 MSort(A, TmpA, 0, A.size() - 1); //清空tmp数组 TmpA.clear(); }
-
相对于快速排序和堆排序,归并排序虽然耗费更多的额外空间,但整体的排序过程是很稳定的。
-
在实际应用中,开辟大块的额外空间并且将两个数组来回赋值是很耗时的,所以一般不把它用于内部排序,但他是外部排序很有用的工具。
5、基数排序
-
桶排序
- 如果已知N个关键字的取值范围是在0~M - 1之间,而M比N小的多,那么可以对关键字的每个可能取值建立M个桶,将每个关键字放入相应的桶中,然后按桶的顺序收集一遍就自然有序了。这就是桶排序。
-
基数排序
- 基数排序可看作是桶排序的推广,它所考虑的待排记录包含不止一个关键字。
- 对于一般有k个关键字的情况,基数排序通常有两种方法:主位优先法和次位优先法。
- 主位优先法:先为最主位关键字建立桶,将每个关键字放入桶中,再按照次位关键字进行排序,依次反复,直到将他们全部顺序收集。
- 次位优先法:先为最次位关键字建立桶,将每个关键字放入桶中,再按照上位关键字进行排序,依次反复,直到最主位关键字都排好序。
- 主位优先法基本是分治法的思路,将序列分割成子列后,分别排序再合并结果;而次位优先法是将排序过程分解成了分配和收集这两个相对简单的步骤,不需要分割子列序列,所以一般情况下次位优先法效率更高。
-
单关键字的基数分解
-
由上面可知基数排序是对有多关键字的对象进行排序,其实可以将单个整型关键字按某种基数分解为多关键字,再进行排序。比如865可以根据基数10将其分解为8、6、5这三个关键字,其中8是最主位关键字,6是次位关键字,5是最次位关键字。
-
//指定基数 #define Redix 10 //指定最多关键字 #define MaxDigit 4 //根据基数取数字相应的位 int GetDigit(ElementType X, int D) { int d, i; for (i = 1; i <= D; i++) { d = X % Redix; X = X / Redix; } return d; } //次位优先基数排序 void LSDRadixSort(vector<ElementType> &A) { int Di; //创建十个桶 vector<vector<ElementType>> Bucket(Redix); //开始排序 for (int D = 1; D <= MaxDigit; D++) //对数据的每一位循环处理 { //分配 for (int i = 0; i < A.size(); i++) //将每个数字按基数分配到各个桶中 { Di = GetDigit(A[i], D); //获得当前位数字 //放入桶中 Bucket[Di].push_back(A[i]); } //收集 int j = A.size() - 1; for (int i = Redix - 1; i >= 0; i--) { //将桶中元素按顺序放入A中 while (!Bucket[i].empty()) { A[j] = Bucket[i].back(); Bucket[i].pop_back(); j--; } } } }
-
二、感想
排序还算简单,最近还学了C++stl库,书上的一长串代码可可以压缩很多,写得贼舒服