【数据结构与算法】:非递归实现快速排序、归并排序

简介: 上篇文章我们详细讲解了递归版本的快速排序,本篇我们来探究非递归实现快速排序和归并排序

1.非递归实现快速排序

快速排序的非递归实现主要依赖于栈(stack)来模拟递归过程中的函数调用栈。递归版本的快速排序通过递归调用自身来处理子数组,而非递归版本则通过手动管理一个栈来跟踪接下来需要排序的子数组的边界


那么怎样通过栈来实现排序的过程呢?


思路如下:


使用栈实现快速排序是对递归版本的模拟。在递归的快速排序中,函数调用栈隐式地保存了每次递归调用的状态。但是在非递归的实现中,你需要显式地使用一个辅助栈来保存子数组的边界


以下是具体步骤和栈的操作过程:


初始化辅助栈:

创建一个空栈。栈用于保存每个待排序子数组的起始索引(begin)和结束索引(end)。


开始排序:

将整个数组的起始和结束索引作为一对入栈。这对应于最初的排序问题。


迭代处理:

在栈非空时,重复下面的步骤:


弹出一对索引(即栈顶元素)来指定当前要处理的子数组。

选择子数组的一个元素作为枢轴(pivot)进行分区(可以是第一个元素,也可以通过其他方法选择,下面我们还是用三数取中)。

进行分区操作,这会将子数组划分为比枢轴小的左侧部分和比枢轴大的右侧部分,同时确定枢轴元素的最终位置。

处理子数组:

分区操作完成后,如果枢轴元素左侧的子数组(如果存在)有超过一个元素,则将其起始和结束索引作为一对入栈。同样,如果右侧的子数组(如果存在)也有超过一个元素,也将其索引入栈


循环:

继续迭代该过程,直到栈为空,此时所有的子数组都已经被正确排序。


所以主要思路就两个:


分区

单趟排序

1.1 提取单趟排序

我们上篇文章讲到递归排序的多种方法,这里我们可以取其中的一种提取出单趟排序:


int Getmidi(int* a, int begin, int end)
{
  int midi = (begin + end) / 2;
  if (a[begin] < a[midi])
  {
  if (a[midi] < a[end])
    return midi;
  else if (a[begin] > a[end])
    return begin;
  else
    return end;
  }
  else
  {
  if (a[midi] > a[end])
    return midi;
  else if (a[end] < a[begin])
    return end;
  else
    return begin;
  }
}
void QuickSortHole(int* arr, int begin, int end) {
  if (begin >= end) {
  return;
  }
  int midi = Getmidi(arr, begin, end);
  Swap(&arr[midi], &arr[begin]);
  int key = arr[begin]; 
  int left = begin;
  int right = end;
  while (left < right) {
  while (left < right && arr[right] >= key) {
    right--;
  }
  arr[left] = arr[right];
  while (left < right && arr[left] <= key) {
    left++;
  }
  arr[right] = arr[left];
  }
  arr[left] = key; 
  QuickSortHole(arr, begin, left - 1);
  QuickSortHole(arr, left + 1, end);
}

接下来完成单趟排序函数:


int singlePassQuickSort(int* arr, int begin, int end) 
{
  if (begin >= end) {
  return;
  }
  // 选择枢轴元素
  int midi = Getmidi(arr, begin, end);
  Swap(&arr[midi], &arr[begin]);
  int key = arr[begin];  // 挖第一个坑
  int left = begin;  // 初始化左指针
  int right = end;   // 初始化右指针
  // 进行分区操作
  while (left < right) {
  // 从右向左找小于key的元素,放到左边的坑中
  while (left < right && arr[right] >= key) {
    right--;
  }
  arr[left] = arr[right];
  // 从左向右找大于key的元素,放到右边的坑中
  while (left < right && arr[left] <= key) {
    left++;
  }
  arr[right] = arr[left];
  }
  // 将枢轴元素放入最后的坑中
  arr[left] = key;
  // 函数可以返回枢轴元素的位置,若需要进一步的迭代过程
  return left;
}


1.2 用栈实现的具体思路

以下面这串数组为例:


首先建立一个栈,将整个数组的起始和结束索引作为一对入栈


弹出一对索引(即栈顶元素)来指定当前要处理的子数组:这里即弹出0 9索引

找到枢轴6进行一次单趟排序:


针对这个数组:


6 3 4 9 5 8 7 2 1 10


我们使用“三数取中”法选择枢轴。起始位置的元素为6,结束位置的元素为10,中间位置的元素为5。在这三个元素中,6为中间大小的值,因此选择6作为枢轴。因为枢轴已经在第一个位置,我们可以直接开始单趟排序。


现在,开始单趟排序:


枢轴值为6。

从右向左扫描,找到第一个小于6的数1。

从左向右扫描,找到第一个大于6的数9。

交换这两个元素。

继续进行上述步骤,直到左右指针相遇。

经过单趟排序后:


6 3 4 1 5 2 7 8 9 10


接下来需要将枢轴6放置到合适的位置。我们知道,最终左指针和右指针会停在第一个大于或等于枢轴值6的位置。在这个例子中,左右指针会停在7上。现在我们将6与左指针指向的位置的数交换:


5 3 4 1 2 6 7 8 9 10

1

现在枢轴值6处于正确的位置,其左侧所有的元素都小于或等于6,右侧所有的元素都大于或等于6。


分区操作完成后,如果枢轴元素左侧的子数组(如果存在)有超过一个元素,则将其起始和结束索引作为一对入栈。同样,如果右侧的子数组(如果存在)也有超过一个元素,也将其索引入栈


我们接下来完成这个入栈过程:让两个子数组的索引入栈


接着取0 4索引进行单趟排序并不断分区,分割的索引继续压栈,继续迭代该过程,直到栈为空,此时所有的子数组都已经被正确排序


1.3 代码实现

这里我们调用之前的栈的代码,基本声明如下:


1. typedef int STDataType;
2. 
3. 
4. typedef struct Stack
5. {
6.  STDataType* a;
7.  int top;
8.  int capacity;
9. }ST; 
10. 
11. void StackInit(ST* ps);
12. // 入栈
13. void StackPush(ST* ps, STDataType x);
14. // 出栈
15. void StackPop(ST* ps);
16. // 获取栈顶元素
17. STDataType StackTop(ST* ps);
18. // 获取栈中有效元素个数
19. int StackSize(ST* ps);
20. // 检测栈是否为空,如果为空返回非零结果,如果不为空返回0 
21. bool StackEmpty(ST* ps);
22. // 销毁栈
23. void StackDestroy(ST* ps);

我们接下来完成排序代码,首先建栈,初始化,并完成第一个压栈过程:


ST s;
StackInit(&s);
StackPush(&s, end);
StackPush(&s, begin);


实现一次单趟排序:


1. int left = StackTop(&s);
2. StackPop(&s);
3. 
4. int right = StackTop(&s);
5. StackPop(&s);
6. 
7. int keyi = singlePassQuickSort(a, left, right);

注意这里我们先压入end,那么我们先出的就是begin,用left首先获取begin,再pop掉获取end


接着判断keyi左右是否还有子数组

if (left < keyi - 1)
{
  StackPush(&s, keyi - 1);
  StackPush(&s, left);
}
if (keyi + 1
{
  StackPush(&s, right);
  StackPush(&s, keyi+1);
}


将此过程不断循环即为整个过程,总代码如下:


void Quicksortst(int* a, int begin, int end)
{
  ST s;
  StackInit(&s);
  StackPush(&s, end);
  StackPush(&s, begin);
  while (!StackEmpty(&s))
  {
  int left = StackTop(&s);
  StackPop(&s);
  int right = StackTop(&s);
  StackPop(&s);
  int keyi = singlePassQuickSort(a, left, right);
  if (left < keyi - 1)
  {
    StackPush(&s, keyi - 1);
    StackPush(&s, left);
  }
  if (keyi + 1
  {
    StackPush(&s, right);
    StackPush(&s, keyi+1);
  }
  }
  StackDestroy(&s);
}


这里思想跟递归其实是差不多的,也是一次取一组进行排序,递归寻找每个区间


2.归并排序

假如我们已经有了两个已经排序好的数组,我们如何让他们并为一个有序的数组呢?


我们的做法就是用两个索引进行比较,然后插入一个新的数组完成排序,这就是归并排序的基础思路


那如果左右不是两个排序好的数组呢?


下面是归并排序的算法步骤:


递归分解数组:如果数组的长度大于1,首先将数组分解成两个部分。通常这是通过将数组从中间切分为大致相等的两个子数组


递归排序子数组:递归地对这两个子数组进行归并排序,直到每个子数组只包含一个元素或为空,这意味着它自然已经排序好


合并排序好的子数组:将两个排序好的子数组合并成一个排序好的数组。这通常通过设置两个指针分别指向两个子数组的开始,比较它们指向的元素,并将较小的元素放入一个新的数组中,然后移动指针。重复此过程,直到所有元素都被合并进新数组


所以我们得需要递归来实现这一过程,首先声明函数并建造新的数组:


void MergeSort(int* a, int n)
{
  int* tmp =(int *) malloc(sizeof(int) * n);
  if (tmp == NULL)
  {
  perror("malloc fail");
  return;
  }
  free(tmp);
}

由于我们不能每次开辟一遍数组,我们这里就需要一个子函数来完成递归过程:


void _MergrSort(int* a, int begin, int end, int* tmp);

首先,不断递归将数组分解:

接着获取分解的两个数组的各自的首端到尾端的索引:
1. int begin1 = begin, end1 = mid;
2. int begin2 = mid + 1, end2 = end;
令要插入到数组tmp的起点为begin处:
1. int begin1 = begin, end1 = mid;
2. int begin2 = mid + 1, end2 = end;
3. int i = begin;
4. 
接下来遍历两个数组,无论谁先走完都跳出循环
1. 
2. while (begin1 <= end1 && begin2 <= end2)
3. {
4.  if (a[begin1] < a[begin2])
5.  {
6.   tmp[i] = a[begin1];
7.   i++;
8.   begin1++;
9.  }
10.   else
11.   {
12.   tmp[i] = a[begin2];
13.   i++;
14.   begin2++;
15.   }
16. }
这时会有一方没有遍历完,按照顺序插入到新数组中即可
1. while (begin1 <= end1)
2. {
3.  tmp[i] = a[begin1];
4.  begin1++;
5.  i++;
6. }
7. while (begin2<= end2)
8. {
9.  tmp[i] = a[begin2];
10.   begin2++;
11.   i++;
12. }
插入到新数组后,我们拷贝到原数组中即完成了一次排序
1.  memcpy(a+begin,tmp+begin,sizeof(int )*(end-begin+1));
完整代码如下:
1. 
2. void _MergrSort(int* a, int begin, int end, int* tmp)
3. {
4.  int mid = (begin + end) / 2;
5. 
6.  if (begin >= end)
7.  {
8.   return;
9.  }
10.   _MergrSort(a, begin, mid, tmp);
11.   _MergrSort(a, mid+1, end, tmp);
12.   int begin1 = begin, end1 = mid;
13.   int begin2 = mid + 1, end2 = end;
14.   int i = begin;
15.   while (begin1 <= end1 && begin2 <= end2)
16.   {
17.   if (a[begin1] < a[begin2])
18.   {
19.     tmp[i] = a[begin1];
20.     i++;
21.     begin1++;
22.   }
23.   else
24.   {
25.     tmp[i] = a[begin2];
26.     i++;
27.     begin2++;
28.   }
29.   }
30.   while (begin1 <= end1)
31.   {
32.   tmp[i] = a[begin1];
33.   begin1++;
34.   i++;
35.   }
36.   while (begin2<= end2)
37.   {
38.   tmp[i] = a[begin2];
39.   begin2++;
40.   i++;
41.   }
42.   memcpy(a+begin,tmp+begin,sizeof(int )*(end-begin+1));
43. }
44. void MergeSort(int* a, int n)
45. {
46.   int* tmp =(int *) malloc(sizeof(int) * n);
47.   if (tmp == NULL)
48.   {
49.   perror("malloc fail");
50.   return;
51.   }
52.   _MergrSort(a, 0, n - 1, tmp);
53.   free(tmp);
54. }


排序好的左半部分和右半部分接着被合并。为此,使用了两个游标begin1和begin2,它们分别指向两个子数组的起始位置,然后比较两个子数组当前元素,将较小的元素拷贝到tmp数组中。这个过程继续直到两个子数组都被完全合并

在所有元素都被合并到tmp数组之后,使用memcpy将排序好的部分拷贝回原数组a。这个地方注意memcpy的第三个参数,它是sizeof(int)*(end - begin + 1),表示拷贝的总大小,单位是字节

begin和end变量在这里表示待排序和合并的数组部分的起止索引

本节内容到此结束!感谢大家阅读!


相关文章
|
9天前
|
搜索推荐 C语言
【排序算法】快速排序升级版--三路快排详解 + 实现(c语言)
本文介绍了快速排序的升级版——三路快排。传统快速排序在处理大量相同元素时效率较低,而三路快排通过将数组分为三部分(小于、等于、大于基准值)来优化这一问题。文章详细讲解了三路快排的实现步骤,并提供了完整的代码示例。
32 4
|
1月前
|
算法 搜索推荐 Shell
数据结构与算法学习十二:希尔排序、快速排序(递归、好理解)、归并排序(递归、难理解)
这篇文章介绍了希尔排序、快速排序和归并排序三种排序算法的基本概念、实现思路、代码实现及其测试结果。
20 1
|
1月前
|
算法
蓝桥杯宝藏排序 | 数据结构 | 快速排序 归并排序
蓝桥杯宝藏排序 | 数据结构 | 快速排序 归并排序
|
1月前
|
搜索推荐 Java Go
深入了解快速排序算法
深入了解快速排序算法
32 2
|
1月前
|
存储 搜索推荐 算法
【排序算法(二)】——冒泡排序、快速排序和归并排序—>深层解析
【排序算法(二)】——冒泡排序、快速排序和归并排序—>深层解析
|
1月前
|
存储 算法 搜索推荐
算法进阶之路:Python 归并排序深度剖析,让数据排序变得艺术起来!
算法进阶之路:Python 归并排序深度剖析,让数据排序变得艺术起来!
72 0
|
1月前
|
算法 Python
Python算法编程:冒泡排序、选择排序、快速排序
Python算法编程:冒泡排序、选择排序、快速排序
|
1月前
|
搜索推荐 Java Go
深入了解归并排序算法
深入了解归并排序算法
20 0
|
25天前
|
算法 安全 数据安全/隐私保护
基于game-based算法的动态频谱访问matlab仿真
本算法展示了在认知无线电网络中,通过游戏理论优化动态频谱访问,提高频谱利用率和物理层安全性。程序运行效果包括负载因子、传输功率、信噪比对用户效用和保密率的影响分析。软件版本:Matlab 2022a。完整代码包含详细中文注释和操作视频。
|
10天前
|
算法 数据挖掘 数据安全/隐私保护
基于FCM模糊聚类算法的图像分割matlab仿真
本项目展示了基于模糊C均值(FCM)算法的图像分割技术。算法运行效果良好,无水印。使用MATLAB 2022a开发,提供完整代码及中文注释,附带操作步骤视频。FCM算法通过隶属度矩阵和聚类中心矩阵实现图像分割,适用于灰度和彩色图像,广泛应用于医学影像、遥感图像等领域。