【C/排序算法】:快速排序和归并排序的非递归实现

简介: 【C/排序算法】:快速排序和归并排序的非递归实现

1. 递归实现的缺陷

在以前的文章中我们把快速排序和归并排序的递归实现方式进行了介绍,但是在校招面试和在企业的日常开发过程中,仅掌握递归方法是不够的,因为递归也有它的缺陷。

我们知道在函数调用过程中会在内存中建立栈帧,栈帧的建立是会消耗空间的。而递归最致命的缺陷就是:在极端情况下,当栈帧的深度太深时,栈空间不够用,就会导致栈溢出!

1.1 栈溢出的例子

可以举一个简单的例子来证明存在栈溢出的情况。

比如:我们用递归实现 1 + 2 + 3 + …… + n 的求和。

#define _CRT_SECURE_NO_WARNINGS 
#include <stdio.h>
int func(int n)
{
  return n == 1 ? 1 : n + func(n - 1);
}
int main()
{
  int n = 0;
  scanf("%d", &n);
  int sum = func(n);
  printf("%d\n", sum);
  return 0;
}

当输入的 n = 10000 时 ,调试结果如下:

2. 递归改非递归的实现方式

通常来说,递归改非递归有两种方式:

1. 直接改成循环(迭代)

2. 借助数据结构的栈模拟

3. 快速排序的非递归 — 使用栈

1.首先先来观察快排的递归实现(三种方法均可,这里用的"前后"指针法 ):

通过观察我们发现,每次递归调用传过去的是一个数组一个区间,数组不用多说,这个区间就是我们的突破点。

也就是说我们要想一个方法来拿到每左右子区间,再对它们分别进行排序,这样才能模拟出递归的过程。那该如何做呢?借助数据结构的栈。

2.非递归的代码实现:

注意:由于C语言没有栈函数的库,所以这里使用的栈要提前准备好,实现栈的过程这里不再介绍。若想了解请前往我的主页。

void Swap(int* p1, int* p2)
{
  int tmp = *p1;
  *p1 = *p2;
  *p2 = tmp;
}
void QuickSortNonR(int* arr, int n)
{
  ST st;
  StackInit(&st);
  //先入右,后入左
  StackPush(&st, n - 1);
  StackPush(&st, 0);
  while (!StackEmpty(&st))
  {
    //先出左边界
    int left = StackTop(&st);
    StackPop(&st);
    //后出右边界
    int right = StackTop(&st);
    StackPop(&st);
    
    //单趟
    int keyi = left;
    int prev = left;
    int cur = left + 1;
    while (cur <= right)
    {
      //++prev != cur是指当cur和prev重合时不用多于的交换
      if (arr[cur] < arr[keyi] && ++prev != cur)
      {
        Swap(&arr[cur], &arr[prev]);
      }
      cur++;
    }
    
    Swap(&arr[keyi], &arr[prev]);
    keyi = prev;
    
    //说明这个区间还有两个及其以上的值
    if (keyi + 1 < right)
    {
      StackPush(&st, right);
      StackPush(&st, keyi + 1);
    }
    if (left < keyi - 1)
    {
      StackPush(&st, keyi - 1);
      StackPush(&st, left);
    }
  }
  StackDestory(&st);
}

排序结果如下:

3.步骤总结:

3.1.首先要先把数组 最右端最左端 入栈,这样就有了一个初始区间,然后开始循环。

3.2.取出栈顶的两个数据,分别赋给 leftright注意在这之后要pop掉取出的数据

3.3.再使用前后指针法走完一趟后就得到了keyi

3.4.然后数组就被 keyIndex 分成了两个子区间,分别是:

左区间:[left,keyi -1]

右区间:[keyi +1,right]

3.5.由于我们一般是先排左区间,再排右区间,根据栈的后进先出特性,所以要先入右区间

分别将左区间和右区间入栈,注意这里要判断

keyi + 1 < right

left < keyi - 1

否则会出现数组访问越界或是死循环的情况。

3.6.循环结束后,销毁栈。

4.总结一下

最后我们要知道的是,快排的非递归并不会使性能受到破坏,它的时间复杂度也是O(N*logN),它的效率依旧极高。使用非递归的主要原因就是防止溢出。

4. 归并排序的非递归 — 使用循环

首先我们知道归并排序的思想是将两组有序的数据合成一组有序的数据。

1. 循环步骤如下:

1.1 那么我们会这样想,当第一组只有1个数据,第二组也只有1个数据时,我们认为这两个数是有序的,把它们一一归并到一个临时数组中,此时临时数组里的2个数据就有序了。

1.2.当第一组有2个有序数据,第二组也有2个有序数据时,把它们两两归并到一个临时数组中此时临时数组里的4个数据就有序了。

1.3.当第一组有3个有序数据,第二组也有3个有序数据时,把它们三三归并到一个临时数组中,此时临时数组里的6个数据就有序了。

1.4 重复上述步骤……直到全部数据有序为止。

2.循环图解如下:

我们假设 gap 为每一组的数据个数,这里最难的是如何控制每一组的范围,即那个区间的边界

假设循环的初始值从 i = 0 开始,则两组的范围可以分别表示为:

第一组:[ i , i + gap - 1 ]

第二组:[ i + gap , i + 2gap - 1 ]

每组1个数据,一一归,归完后临时数组里的2个数据就有序了;

每组2个数据,二二归,归完后临时数组里的4个数据就有序了;

每组4个数据,四四归,归完后临时数组里的8个数据就有序了;

2. 非递归的代码实现:

这里有两种边界修正问题需要特别注意:

(1)在归并过程中右半区间可能不存在

这时直接跳出循环,把剩下的那一个数据直接拷贝进数组即可。

比如在一一归并时:

(2)在归并过程中右半区间可能算多了

此时要把右半区间的右边界修改为数组最后一个元素的下标。

//归并排序的非递归
void MergeSortNonR(int* a, int sz)
{
  int* tmp = (int*)malloc(sizeof(int) *sz);
  int gap = 1; // 每组数据个数
  while (gap < sz)
  {
    for (int i = 0; i < sz; i += 2 * gap)
    {
      // [i, i+gap-1] [i+gap,i+2*gap-1]
      int begin1 = i, end1 = i + gap - 1; //确定闭区间的位置,要-1
      int begin2 = i + gap, end2 = i + 2 * gap - 1;
      // 归并过程中右半区间可能就不存在
      if (end1 >= sz || begin2 >= sz)
        break;
      // 归并过程中右半区间算多了, 修正一下
      if (end2 >= sz)
      {
        end2 = sz - 1;
      }
      
            //归并的过程
      int index = i;
      
      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++];
      }
        // 归一部分,拷一部分,没归的就不拷
      memecpy(arr + j, tmp + j, sizeof(int) * (end2 - j + 1);
    }
    
    gap *= 2;
  }
  free(tmp);
  tmp = NULL
}

排序结果如下:

3.总结一下

归并排序的非递归的时间复杂度也是O(N*logN),它与递归的性能也差不多。

目录
相关文章
|
1天前
|
搜索推荐 算法 Java
Java数据结构与算法:排序算法之归并排序
Java数据结构与算法:排序算法之归并排序
|
1天前
|
搜索推荐 算法 Java
Java数据结构与算法:排序算法之快速排序
Java数据结构与算法:排序算法之快速排序
|
7天前
|
搜索推荐 算法 Java
Java中的快速排序、归并排序和堆排序是常见的排序算法。
【6月更文挑战第21天】Java中的快速排序、归并排序和堆排序是常见的排序算法。快速排序采用分治,以基准元素划分数组并递归排序;归并排序同样分治,先分割再合并有序子数组;堆排序通过构建堆来排序,保持堆性质并交换堆顶元素。每种算法各有优劣:快排平均高效,最坏O(n²);归并稳定O(n log n)但需额外空间;堆排序O(n log n)且原地排序,但不稳定。
19 3
|
4天前
|
搜索推荐 算法
【C/排序算法】:归并排序和计数排序
【C/排序算法】:归并排序和计数排序
9 0
|
4天前
|
机器学习/深度学习 搜索推荐 算法
【C/排序算法】:快速排序和冒泡排序
【C/排序算法】:快速排序和冒泡排序
8 0
|
4天前
|
搜索推荐
归并排序算法总结
归并排序算法总结
|
3天前
|
机器学习/深度学习 自然语言处理 算法
m基于深度学习的OFDM+QPSK链路信道估计和均衡算法误码率matlab仿真,对比LS,MMSE及LMMSE传统算法
**摘要:** 升级版MATLAB仿真对比了深度学习与LS、MMSE、LMMSE的OFDM信道估计算法,新增自动样本生成、复杂度分析及抗频偏性能评估。深度学习在无线通信中,尤其在OFDM的信道估计问题上展现潜力,解决了传统方法的局限。程序涉及信道估计器设计,深度学习模型通过学习导频信息估计信道响应,适应频域变化。核心代码展示了信号处理流程,包括编码、调制、信道模拟、降噪、信道估计和解调。
23 8
|
5天前
|
算法
基于GA遗传优化的混合发电系统优化配置算法matlab仿真
**摘要:** 该研究利用遗传算法(GA)对混合发电系统进行优化配置,旨在最小化风能、太阳能及电池储能的成本并提升系统性能。MATLAB 2022a用于实现这一算法。仿真结果展示了一系列图表,包括总成本随代数变化、最佳适应度随代数变化,以及不同数据的分布情况,如负荷、风速、太阳辐射、弃电、缺电和电池状态等。此外,代码示例展示了如何运用GA求解,并绘制了发电单元的功率输出和年变化。该系统原理基于GA的自然选择和遗传原理,通过染色体编码、初始种群生成、适应度函数、选择、交叉和变异操作来寻找最优容量配置,以平衡成本、效率和可靠性。
|
6天前
|
机器学习/深度学习 算法
基于鲸鱼优化的knn分类特征选择算法matlab仿真
**基于WOA的KNN特征选择算法摘要** 该研究提出了一种融合鲸鱼优化算法(WOA)与K近邻(KNN)分类器的特征选择方法,旨在提升KNN的分类精度。在MATLAB2022a中实现,WOA负责优化特征子集,通过模拟鲸鱼捕食行为的螺旋式和包围策略搜索最佳特征。KNN则用于评估特征子集的性能。算法流程包括WOA参数初始化、特征二进制编码、适应度函数定义(以分类准确率为基准)、WOA迭代搜索及最优解输出。该方法有效地结合了启发式搜索与机器学习,优化特征选择,提高分类性能。
|
1天前
|
机器学习/深度学习 存储 算法
基于SFLA算法的神经网络优化matlab仿真
**摘要:** 使用MATLAB2022a,基于SFLA算法优化神经网络,降低训练误差。程序创建12个神经元的前馈网络,训练后计算性能。SFLA算法寻找最优权重和偏置,更新网络并展示训练与测试集的预测效果,以及误差对比。SFLA融合蛙跳与遗传算法,通过迭代和局部全局搜索改善网络性能。通过调整算法参数和与其他优化算法结合,可进一步提升模型预测精度。