数据结构初阶之排序(下)

前言

上一期内容中我们了解了基本排序中的插入与选择排序,今天我将为大家带来剩下的几种排序算法

快速排序

快速排序是Hoare于1962年提出的⼀种⼆叉树结构的交换排序⽅法,其基本思想为:任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两⼦序列,左⼦序列中所有元素均⼩于基准值,右⼦序列中所有元素均⼤于基准值,然后最左右⼦序列重复该过程,直到所有元素都排列在相应位置上为⽌。

快速排序有着许多种实现方式,今天我们就hoare、挖坑法以及lomuto前后指针法来对其进行递归实现,同时还将运用数据结构中的栈来实现快速排序的非递归实现方法。

快速排序实现的主框架

cpp 复制代码
//快速排序 
void QuickSort(int* a, int left, int right) 
{
 if (left >= right) {
 return;
 }
 //_QuickSort⽤于按照基准值将区间[left,right)中的元素进⾏划分 
 int meet = _QuickSort(a, left, right);
 QuickSort(a, left, meet - 1);
 QuickSort(a, meet + 1, right);
}

将区间中的元素进⾏划分的 _QuickSort⽅法主要有以下⼏种实现⽅式:

我们可以将这些方法看做一个找基准值的过程:

hoare版本

思路:

1)创建左右指针,确定基准值

2)从右向左找出⽐基准值⼩的数据,从左向右找出⽐基准值⼤的数据,左右指针数据交换,进⼊下次循环
问题1:为什么跳出循环后right位置的值⼀定不⼤于key?

当 left > right 时,即right⾛到left的左侧,⽽left扫描过的数据均不⼤于key,因此right此时指向的数据⼀定不⼤于key

如图:

问题2:为什么left和right指定的数据和key值相等时也要交换?

相等的值参与交换确实有⼀些额外消耗。实际还有各种复杂的场景,假设数组中的数据⼤量重复时,⽆法进⾏有效的分割排序。

如图:

代码的实现
cpp 复制代码
int _QuickSort(int* a, int left, int right) 
{
 int begin = left;
 int end = right;
 int keyi = left;
 ++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--]);
 }
 }
 swap(&a[keyi], &a[right]);
 return right;
}

挖坑法

思路:

创建左右指针。⾸先从右向左找出⽐基准⼩的数据,找到后⽴即放⼊左边坑中,当前位置变为新的"坑",然后从左向右找出⽐基准⼤的数据,找到后⽴即放⼊右边坑中,当前位置变为新的"坑",结束循环后将最开始存储的分界值放⼊当前的"坑"中,返回当前"坑"下标(即分界值下标)

我们通过一张图来了解:

代码的实现
cpp 复制代码
int _QuickSort(int* a, int left, int right) 
{
 int mid = a[left];
 int hole = left;
 int key = a[hole];
 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;
}

lomuto前后指针

思路:

创建前后指针,从左往右找⽐基准值⼩的进⾏交换,使得⼩的都排在基准值的左边。

如图:

代码的实现
cpp 复制代码
int _QuickSort(int* a, int left, int right) 
{
 int prev = left, cur = left + 1;
 int key = left;
 while (cur <= right)
 {
 if (a[cur] < a[key] && ++prev != cur) 
 {
 swap(&a[cur], &a[prev]);
 }
 ++cur;
 }
 swap(&a[key], &a[prev]);
 
 return prev;
}

快速排序的特征

  1. 时间复杂度:O(nlogn)

  2. 空间复杂度:O(logn)


快速排序的非递归版本

⾮递归版本的快速排序需要借助数据结构:

根据栈结构先进后出的原则,我们先将待排序数组的末尾元素先入栈,头元素后入栈。

随后分别取栈顶与栈底元素,利用循环来实现递归操作。

代码的实现

cpp 复制代码
void QuickSortNonR(int* a, int left, int right)
{
 ST st;
 STInit(&st);
 STPush(&st, right);
 STPush(&st, left);
 while (!STEmpty(&st))
 {
 int begin = STTop(&st);
 STPop(&st);
 int end = STTop(&st);
 STPop(&st);
 // 单趟 
 int keyi = begin;
 int prev = begin;
 int cur = begin + 1;
 while (cur <= end)
 {
 if (a[cur] < a[keyi] && ++prev != cur)
 Swap(&a[prev], &a[cur]);
 ++cur;
 }
 Swap(&a[keyi], &a[prev]);
 keyi = prev;
 // [begin, keyi-1] keyi [keyi+1, end] 
 if (keyi + 1 < end)
 {
 STPush(&st, end);
 STPush(&st, keyi + 1);
 }
 if (begin < keyi-1)
 {
 STPush(&st, keyi-1);
 STPush(&st, begin);
 }
 }
 STDestroy(&st);
}

归并排序

归并排序的思想:

归并排序(MERGE-SORT)是建⽴在归并操作上的⼀种有效的排序算法,该算法是采⽤分治法(Divide and Conquer)的⼀个⾮常典型的应⽤。将已有序 的⼦序列合并,得到完全有序的序列;即先使每个⼦序列有序,再使⼦序列段间有序。若将两个有序表合并成⼀个有序表,称为⼆路归并。归并排序核⼼步骤(如下图):

利用递归分别将待排序的数组二等分割成n份,每次取一半,随后就分割后的两两数组进行合并操作(先比大小后放置)

代码的实现

cpp 复制代码
void _MergeSort(int* a, int left, int right, int* tmp) 
{
 if (left >= right) 
 {
 return;
 }
 int mid = (right + left) / 2;
 //[left,mid] [mid+1,right]
 _MergeSort(a, left, mid, tmp);
 _MergeSort(a, mid + 1, right, tmp);
 
 int begin1 = left, end1 = mid;
 int begin2 = mid + 1, end2 = right;
 int index = begin1;
//合并两个有序数组为⼀个数组 
 while (begin1 <= end1 && begin2 <= end2)
 {
 if (a[begin1] < a[begin2]) 
 {
 tmp[index++] = a[begin1++];
 }
 else 
 {
 tmp[index++] = a[begin2++];
 }
 }
 while (begin1 <= end1)
 {
 tmp[index++] = a[begin1++];
 }
 while (begin2 <= end2)
 {
 tmp[index++] = a[begin2++];
 }
 for (int i = left; i <= right; i++)
 {
 a[i] = tmp[i];
 }
}
void MergeSort(int* a, int n) 
{
 int* tmp = new int[n];
 _MergeSort(a, 0, n - 1, tmp);
 delete[] tmp;
}

归并排序的特征

  1. 时间复杂度:O(nlogn)

  2. 空间复杂度:O(n)


测试代码

通过以下代码,我们可以得出各种排序算法的时间效率。

cpp 复制代码
// 测试排序的性能对⽐  
void TestOP() 
{ 
 srand(time(0)); 
 const int N = 100000; 
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(); 
 QuickSort(a5, 0, N-1); 
 int end5 = clock(); 
 
 int begin6 = clock(); 
 MergeSort(a6, N); 
 int end6 = clock(); 
 
 int begin7 = clock(); 
 BubbleSort(a7, 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); 
} 

非比较排序

学习了以上的代码,那么有没有不通过比较大小就能实现的排序算法呢?有!

计数排序

计数排序⼜称为鸽巢原理,是对哈希直接定址法的变形应⽤。

操作步骤:

1)统计相同元素出现次数

2)根据统计的结果将序列回收到原来的序列中

通过两张图片来了解一下:

为了避免空间的无端浪费,我们将遍历待排数组中的最大最小值,后得出其间的差值,用其差值来申请空间大小以达到排序的效果。

代码的实现

cpp 复制代码
void CountSort(int* a, int n)
{
 int min = a[0], max = a[0];
 for (int i = 1; i < n; i++)
 {
 if (a[i] > max)
 max = a[i];
 if (a[i] < min)
 min = a[i];
 }
 int range = max - min + 1;
 int* count = (int*)malloc(sizeof(int) * range);
 if (count == NULL)
 {
 perror("malloc fail");
 return;
 }
memset(count, 0, sizeof(int) * range);
 // 统计次数 
 for (int i = 0; i < n; i++)
 {
 count[a[i] - min]++;`
 }
 // 排序 
 int j = 0;
 for (int i = 0; i < range; i++)
 {
 while (count[i]--)
 {
 a[j++] = i + min;
 }
 }
}

计数排序的特征

计数排序在数据范围集中时,效率很⾼,但是适⽤范围及场景有限。

时间复杂度:O(N + range)

空间复杂度:O(range)

稳定性:稳定

*计数排序仅适用于整数的排序


排序算法的稳定性及其复杂度综合分析

稳定性:假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的 相对次序保持不变,即在原序列中,r[i]=r[j],且r[i]在r[j]之前,⽽在排序后的序列中,r[i]仍在r[j]之 前,则称这种排序算法是稳定的;否则称为不稳定的。


以上便是数据结构初阶的全部内容,感谢各位的支持!后面我将为大家带来C++有关的知识分享,敬请期待!

相关推荐
xiaoshiguang33 小时前
LeetCode:222.完全二叉树节点的数量
算法·leetcode
爱吃西瓜的小菜鸡3 小时前
【C语言】判断回文
c语言·学习·算法
别NULL3 小时前
机试题——疯长的草
数据结构·c++·算法
TT哇3 小时前
*【每日一题 提高题】[蓝桥杯 2022 国 A] 选素数
java·算法·蓝桥杯
ZSYP-S4 小时前
Day 15:Spring 框架基础
java·开发语言·数据结构·后端·spring
yuanbenshidiaos4 小时前
C++----------函数的调用机制
java·c++·算法
唐叔在学习4 小时前
【唐叔学算法】第21天:超越比较-计数排序、桶排序与基数排序的Java实践及性能剖析
数据结构·算法·排序算法
ALISHENGYA5 小时前
全国青少年信息学奥林匹克竞赛(信奥赛)备考实战之分支结构(switch语句)
数据结构·算法
chengooooooo5 小时前
代码随想录训练营第二十七天| 贪心理论基础 455.分发饼干 376. 摆动序列 53. 最大子序和
算法·leetcode·职场和发展
jackiendsc5 小时前
Java的垃圾回收机制介绍、工作原理、算法及分析调优
java·开发语言·算法