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

前言

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

快速排序

快速排序是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++有关的知识分享,敬请期待!

相关推荐
小张成长计划..9 分钟前
双向链表的实现
数据结构·链表
s1533516 分钟前
数据结构之顺序表,链表,栈,队列
数据结构·数据库
Wo3Shi4七32 分钟前
双向队列
数据结构·算法·go
Wo3Shi4七36 分钟前
列表
数据结构·算法·go
Wo3Shi4七41 分钟前
链表
数据结构·算法·go
Wo3Shi4七1 小时前
数组
数据结构·算法·go
CoovallyAIHub1 小时前
YOLOv13都来了,目标检测还卷得动吗?别急,还有这些新方向!
深度学习·算法·计算机视觉
北方有星辰zz1 小时前
数据结构:栈
java·开发语言·数据结构
zl_dfq1 小时前
数据结构之 【树的简介】(树的(相关)概念、二叉树的概念、部分性质、满二叉树、完全二叉树)
数据结构
转转技术团队2 小时前
边学边做:图片识别技术的学习与应用
后端·算法