在生活中有很多排序的需求,这让我们不得不熟悉排序,并将它们优化的更好,让使用者感到更加便捷。
1.排序的概念及其运用
1.1排序的概念
排序:所谓排序,就是使一串记录,按照其中的某个或某些关键字的大小,递增或递减的排列起来的操作。 稳定性:假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次 序保持不变,即在原序列中,r[i]=r[j],且r[i]在r[j]之前,而在排序后的序列中,r[i]仍在r[j]之前,则称这种排 序算法是稳定的;否则称为不稳定的。 内部排序:数据元素全部放在内存中的排序。 外部排序:数据元素太多不能同时放在内存中,根据排序过程的要求不能在内外存之间移动数据的排序。
1.2排序运用:
根据商品的销量,价格等的排序就可以让顾客更好的选择,可见排序算法的重要性
1.3 常见的排序算法
有七大排序,直接插入排序,选择排序,希尔排序,冒泡排序,堆排序,快速排序,归并排序。
但是它们的时间复杂度和空间复杂度却不相同,让我们一起学习他们吧
首先要介绍一个可以比较他们性能的接口:
// 测试排序的性能对比
void TestOP()
{
srand(time(0));
const int N = 10000000;
int* a1 = (int*)malloc(sizeof(int) * N);
int* a2 = (int*)malloc(sizeof(int) * N);
int* a3 = (int*)malloc(sizeof(int) * N);
int* a4 = (int*)malloc(sizeof(int) * N);
int* a5 = (int*)malloc(sizeof(int) * N);
int* a6 = (int*)malloc(sizeof(int) * N);
int* a7 = (int*)malloc(sizeof(int) * N);
for (int i = 0; i < N; ++i)
{
a1[i] = rand();
a2[i] = a1[i];
a3[i] = a1[i];
a4[i] = a1[i];
a5[i] = a1[i];
a6[i] = a1[i];
a7[i] = a1[i];
}
int begin1 = clock();
//InsertSort(a1, N);
int end1 = clock();
int begin2 = clock();
ShellSort(a2, N);
int end2 = clock();
int begin3 = clock();
//SelectSort(a3, N);
int end3 = clock();
int begin4 = clock();
HeapSort(a4, N);
int end4 = clock();
int begin5 = clock();
QuickSortNonR(a5, 0, N - 1);
int end5 = clock();
int begin6 = clock();
//MergeSort(a6, N);
int end6 = clock();
int begin7 = clock();
//BubbleSort(a6, N);
int end7 = clock();
printf("InsertSort:%d\n", end1 - begin1);
printf("ShellSort:%d\n", end2 - begin2);
printf("SelectSort:%d\n", end3 - begin3);
printf("HeapSort:%d\n", end4 - begin4);
printf("QuickSort:%d\n", end5 - begin5);
printf("MergeSort:%d\n", end6 - begin6);
printf("BubbleSort:%d\n", end7 - begin7);
free(a1);
free(a2);
free(a3);
free(a4);
free(a5);
free(a6);
free(a7);
}
它们可以分别检测出每个排序的排序时间,能定量的比较结果。
插入排序:直接插入排序是一种简单的插入排序法,其基本思想是:把待排序的记录按其关键码值的大小逐个插入到一个已经排好序的有序序列中,直到所有的记录插入完为 止,得到一个新的有序序列 。
实际中我们玩扑克牌时,就用了插入排序的思想:
直接插入排序: 当插入第i(i>=1)个元素时,前面的array[0],array[1],…,array[i-1]已经排好序,此时用array[i]的排序码与 array[i-1],array[i-2],…的排序码顺序进行比较,找到插入位置即将array[i]插入,原来位置上的元素顺序后移
下面是代码实现:
// 插入排序 //最好的时间复杂度是O(N) //最坏的是O(N^2) void InsertSort(int* a, int n) { //给定一组数,一个一个插入,大的插入在小的后面 for (int i = 0; i < n - 1; i++)//n-1是防止越界 { //一次插入排序 int end = i; int tmp = a[end + 1]; while (end >= 0) { if (a[end] > tmp) { //交换 Swap(&a[end], &a[end + 1]); end--; } else { break; } } a[end + 1] = tmp; } }
希尔排序( 缩小增量排序 )
希尔排序法又称缩小增量法。希尔排序法的基本思想是:先选定一个整数,把待排序文件中所有记录分成个 组,所有距离为的记录分在同一组内,并对每一组内的记录进行排序。然后,取,重复上述分组和排序的工 作。当到达=1时,所有记录在统一组内排好序。
代码实现:
// 希尔排序 //时间复杂度大概是O(N^1.3)很快 void ShellSort(int* a, int n) { //先取距离是gap的数,先排好,这样能保证数据整体有序,然后执行插入排序 //预排序+插入排序 int gap = n; while (gap > 1)//gap等于1的时候就是直接插入排序 { gap = gap / 3 + 1; //这里gap = gap / 2也可以 for (int i = 0; i < n - gap; i++)//多次希尔排序同时进行 { int end = i; int tmp = a[end + gap]; while (end >= 0) { if (a[end] > tmp) { Swap(&a[end], &a[end + gap]); end -= gap; } else { break; } } a[end + gap] = tmp; } } }
希尔排序的特性总结:
1. 希尔排序是对直接插入排序的优化。
2. 当gap > 1时都是预排序,目的是让数组更接近于有序。当gap == 1时,数组已经接近有序的了,这样就 会很快。这样整体而言,可以达到优化的效果。我们实现后可以进行性能测试的对比。
3. 希尔排序的时间复杂度不好计算,因为gap的取值方法很多,导致很难去计算,因此在好些书中给出的 希尔排序的时间复杂度都不固定。
选择排序
基本思想: 每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到全部待排序的 数据元素排完 。
// 选择排序 //时间复杂度是O(N^2) void SelectSort(int* a, int n) { //一次遍历找出最大的和最小的,分别排在最前和最后 int begin = 0; int end = n - 1; while (begin < end) { int maxi = begin; int mini = begin; for (int i = begin + 1; i <=end; i++) { if (a[i] > a[maxi]) { maxi = i; } if (a[i] < a[mini]) { mini = i; } } //交换 Swap(&a[mini], &a[begin]); //防止最大的数在最前面的情况,mini交换了最大的数而maxi还在begin 的位置 if (maxi == begin) { maxi = mini; } Swap(&a[maxi], &a[end]); begin++; end--; } }
选择排序效率很低,一般都不适用。
堆排序
堆排序(Heapsort)是指利用堆积树(堆)这种数据结构所设计的一种排序算法,它是选择排序的一种。它是 通过堆来进行选择数据。需要注意的是排升序要建大堆,排降序建小堆。对于堆排序,在之前的二叉树的堆那里已经实现过了,这里就不多介绍,要提的就是,排升序建大堆的因为我们排序的时候是将堆顶的数据和最后一个数据互换来实现的,所以一开始最大的数据反而应该在堆顶,排降序建小堆也是这个道理。
// 堆排序 void AdjustDwon(int* a, int n, int parent) { int minchild = 2 * parent + 1; while (minchild < n) { //如果假设失败 if (minchild + 1 < n && a[minchild + 1] > a[minchild]) { minchild++; } if (a[minchild] > a[parent]) { Swap(&a[minchild], &a[parent]); //迭代 parent = minchild; minchild = 2 * parent + 1; } else { break; } } } //时间复杂度是O(N^logN) void HeapSort(int* a, int n) { //升序排大堆,然后把最顶的数据和最后的数据互换再向下调整 for (int i = (n - 2) / 2;i>=0; i--)//找最后节点的父节点 { AdjustDwon(a, n, i); } int i = 1; while (i < n) { Swap(&a[0], &a[n - i]); AdjustDwon(a, n - i, 0); i++; } }
堆排序的特性总结:
1. 堆排序使用堆来选数,效率就高了很多。
2. 时间复杂度:O(N*logN)
交换排序 基本思想:所谓交换,就是根据序列中两个记录键值的比较结果来对换这两个记录在序列中的位置,交换排 序的特点是:将键值较大的记录向序列的尾部移动,键值较小的记录向序列的前部移动。
冒泡排序:冒泡排序实现非常简单,这里就不多介绍;
// 冒泡排序 //时间复杂度是O(N^2) void BubbleSort(int* a, int n) { //前后比较,大的在前就交换一下顺序 for (int i = 0; i < n -1; i++) { //一趟冒泡排序 int change = 0;//在有序的时候可以保证时间复杂度为O(1) for (int j = 1; j < n - i; j++) { if (a[j-1] > a[j]) { Swap(&a[j], &a[j -1]); change = 1; } } if (change == 0) { break; } } }
快速排序:
快速排序是Hoare于1962年提出的一种二叉树结构的交换排序方法,其基本思想为:任取待排序元素序列中 的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右 子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止。
其中实现单次快速排序的方法有3种,这里一一介绍:
第一种就是hoare版本:
![]()
最后的结果:
代码实现:
// 快速排序hoare版本 int PartSort1(int* a, int left, int right) { //三数取中 int mid = GetMidIndex(a, left, right); Swap(&a[mid], &a[left]); //假设keyi在最左边,那么右边的先走 int keyi = left; while (left < right) { //右边的找小 while (left < right && a[right] > a[keyi]) { right--; } //左边的找大 while (left < right && a[left] < a[keyi]) { left++; } if (left<right) { Swap(&a[left], &a[right]); } } int meeti = left; Swap(&a[left], &a[keyi]); return meeti; }
其中的三数取中是更好的优化,因为在数据有序的时候,我们就会发现快速排序的效率会很低,所以就写个三数取中解决这个问题:
int GetMidIndex(int*a, int left, int right) { //最开始,中间,最后找出一个中间的数 int mid = left + (right - left) / 2; //int mid = (left + right) / 2; if (a[left] < a[mid]) { if (a[right] > a[mid]) { return mid; } //此时的mid为最大,比较left和right else if(a[right]>a[left]) { return right; } else { return left; } } //a[left]>a[mid] else { if (a[left] < a[right]) { return left; } else if (a[right] > a[mid]) { return right; } else { return mid; } } }
快速排序单次的第二个版本:挖坑法
这个方法很好理解,我个人认为这是三种方法中最好写,也是最容易记得住的方法
R找小,L找大,R找到小了以后就把数填到坑里,然后自己的位置变成新的坑位,最后把key填到坑位就完成了
代码实现:
// 快速排序挖坑法 int PartSort2(int* a, int left, int right) { //三数取中 int mid = GetMidIndex(a, left, right); Swap(&a[mid], &a[left]); int hole = left; int key = a[left]; while (left < right) { //右边找小 while (left<right && a[right]>key) { right--; } a[hole] = a[right]; hole = right; //左边找大 while (left < right && a[left] < key) { left++; } a[hole] = a[left]; hole = left; } a[hole] = key; return hole; }
快速排序单次的第二个版本:前后指针法
我们先定义两个变量,一个cur,一个prev,cur找比key小的数,prev就找比key大的数,找到就互换,这样能够保证比key大的数都在prev和cur之间,最后都在右边
最后结果:
// 快速排序前后指针法 int PartSort3(int* a, int left, int right) { //前面的指针找小,两个指针相差的就是比key要大的数,最后比key大的数都换到右边 //三数取中 int mid = GetMidIndex(a, left, right); Swap(&a[mid], &a[left]); int cur, prev,keyi; keyi = prev = left; cur = prev + 1; while (cur<=right) { if (a[cur] < a[keyi] && ++prev != cur) Swap(&a[cur], &a[prev]); ++cur; } Swap(&a[keyi], &a[prev]); return prev; }
快速排序的递归版本:
在写好了一次快速排序之后,我们就要上手对全部数据的排序,在我们排好一次以后,那个数的位置就确定了,这时我们就可以以这个数为中心,把它分成两个区间[left,keyi-1]和[keyi+1,right],其中keyi的位置就是前一次排好数的位置,这样就不难想到利用递归的思想去解决这个问题:
void QuickSort(int* a, int left, int right) { if (left >= right) { return; } int keyi = PartSort3(a, left, right); //有三个区间[left,keyi-1],keyi,[keyi+1,right] QuickSort(a, left, keyi - 1); QuickSort(a, keyi + 1, right); }
快速排序的非递归版本:利用数据结构栈的性质去模拟递归
我们都知道栈空间的大小是很小的,这样就大大限制了快排的使用,这时候我们就可以通过改变递归版本来更好的优化快排:
在之前的学习中我们已经学会了用顺序表实现了数据结构栈,而我们今天要利用数据结构栈只需要包含一下头文件即可。
这就是模拟递归的部分过程,这跟递归的思想其实是一样的
下面是代码实现:
// 快速排序 非递归实现 //时间复杂度为O(N*logN) void QuickSortNonR(int* a, int begin, int end) { Stack st; StackInit(&st); StackPush(&st, begin); StackPush(&st, end); while (!StackEmpty(&st)) { int right = Stacktop(&st); StackPop(&st); int left = Stacktop(&st); StackPop(&st); int keyi = PartSort3(a, left, right); // [left, keyi-1] keyi [keyi+1,right] if (keyi + 1 < right) { StackPush(&st, keyi + 1); StackPush(&st, right); } if (left < keyi - 1) { StackPush(&st, left); StackPush(&st, keyi - 1); } } StackDestroy(&st); }
最后要记得对栈进行释放,内存泄漏是个比较严重的问题
以上就是本期的全部内容,喜欢的小伙伴多多支持