快速排序(非递归)

简介: 快速排序(非递归)

 前面的三个版本的快速排序都是以递归的方式写的,但是我们都知道,递归虽好,但是递归的深度是不易太深的,因为栈区的内存是有限的,递归深度太深必然会栈溢出,导致程序崩溃,所以,我们有必要学会如何把快排的递归改为非递归,接下来就跟大家聊一聊快排的非递归该如何实现。


       从前面的几种快排递归的写法(hoare版本,挖坑法,前后指针法)中能看出,它们每完成一趟快速排序之后都会返回一个keyi的位置,a[keyi]的左边的值都比a[keyi]小(或者等于),右边的值都比a[keyi]的大。然后下一趟快排就是以keyi为分界线,分成左右两边的子区间再进行同样的快排操作的。也就是说这里的排序能一直递归下去的重点是递归的时候能够把数组不断地划分为更小的区间,由此可知想要用非递归的方式模拟实现递归排序的重点是每一趟快排结束得到keyi,keyi对数组的分割的两个子区间需要被保存下来,等到下一趟进行排序的时候就可以用保存好的子区间作为子数组排序的边界了,这样一来就能完成快排的非递归。


       那怎么保存排序过程中产生的子区间呢?毫无疑问,这里最适合用的是栈存储子区间。其实仔细思考一下你会发现,这个的思想是不是跟二叉树的前序遍历的思想是一样的,先排好一个数,再排好这个数的左区间,然后再排好这个数的右区间,最后使整个数组有序,而二叉树的前序遍历就是dfs(深度遍历)的类型,dfs类型最适合用栈数据结构辅助解题,所以这里用栈来存储是最适合的。


       那我们如何用栈保存数组排序时产生的子区间呢?


大概思路是:刚开始时,先把数组的左右下标入栈,然后开始取栈内的数据作为排序子数组的区间值,那么每走一趟快排都会返回一个keyi,如果keyi-1比begin大,那证明这个区间里面至少还有两个值,那就把begin和keyi-1入栈,否则就证明只有一个值或者没有值了,不需要入栈;如果keyi+1小于end,那就把这keyi+1和end入栈,否则不入,等到下一次循环的时候就在栈中取出两个数,这两个数就是需要排序的子区间,进行排序,以此类推,直到最后栈中没有了数就证明数组已经排完了。


       参考代码及注释:

void Swap(int* a, int* b)
{
  int tmp = *a;
  *a = *b;
  *b = tmp;
}
void PrintArr(int* a, int n)
{
  int i = 0;
  for (i = 0; i < n; i++)
  {
    printf("%d ", a[i]);
  }
  printf("\n");
}
//三数取中
int GetMidNumi(int* a, int left, int right)
{
  int mid = (left + right) / 2;
  if (a[left] > a[mid])
  {
    if (a[mid] > a[right])
    {
      return mid;
    }
    //来到这里证明a[mid]最小,那么a[left]和a[right]
    //谁小谁就是中间的那个数
    else if (a[left] < a[right])
    {
      return left;
    }
    else
    {
      return right;
    }
  }
  else//a[left]<=a[mid]
  {
    if (a[mid] < a[right])
    {
      return mid;
    }
    //来到这里证明a[mid]最大,那么a[left]和a[right]
    //谁小谁就是中间的那个数
    else if (a[left] < a[right])
    {
      return left;
    }
    else
    {
      return right;
    }
  }
}
int PartSort3(int* a, int left, int right)
{
  //三数取中
  int mid = GetMidNumi(a, left, right);
  //如果left就是取到的数,那么也就没有必要交换了
  if (mid != left)
  {
    Swap(&a[left], &a[mid]);
  }
  int keyi = left;
  int prev = left;
  int cur = prev + 1;
  while (cur <= right)//由于这里是左闭右闭区间,所以需要取等号
  {
    //这里是逻辑与操作,只有第一个条件成立,第二个条件才会参与判断
    //所以后面的++prev是在a[cur]<a[keyi]的条件下再走的,如果++prev
    //后prev==cur,证明它们的位置是一样的,也就没有必要交换了,只有等
    //prev和cur错开才需要交换
    if (a[cur] < a[keyi] && (++prev != cur))
    {
      Swap(&a[cur], &a[prev]);
    }
    //无论上面是否需要交换,cur都需要++
    cur++;
  }
  //最后prev的位置就是a[keyi]的最终的位置,交换prev和keyi对应的值
  Swap(&a[prev], &a[keyi]);
  //交换后a[prev]的值比前面的值大,比后面的值小,这里就是分界点,返回这个prev做递归的边界
  return prev;
}
void QuickSortNonR(int* a, int left, int right)
{
  //定义一个栈
  stack<int> st;
  //先把大数组的需要排序的区间push到栈中
  //这里是先push区间的左边,在push区间的右边
  //所以拿的时候先拿到的是区间的右边,再拿到区间的左边
  st.push(left);
  st.push(right);
  while (!st.empty())
  {
    //由于是先如区间的左边,再入区间的右边
    //所以拿的时候先拿右边,再拿左边
    int end = st.top();
    st.pop();
    int begin = st.top();
    st.pop();
    //用前后指针法完成每一趟快排
    //如果对这里的一趟快排的细节
    //有疑问,可以转移到上一篇文章
    int keyi = PartSort3(a, begin, end);
    //如果返回的keyi-1>begin,说明小区间至少还有两个
    //元素,需要入栈,等循环回去继续排序
    if (begin < keyi - 1)
    {
      st.push(begin);
      st.push(keyi - 1);
    }
    //如果返回的keyi+1<end,说明小区间至少还有两个
    //元素,需要入栈,等循环回去继续排序
    if (keyi + 1 < end)
    {
      st.push(keyi + 1);
      st.push(end);
    }
  }
  //当栈为空时,说明全部元素都已经有序,那么数组也就有序了
}
int main()
{
  int a[] = { 6,6,6,6,1,2,7,9,3,4,5,10,8 };
  int sz = sizeof(a) / sizeof(a[0]);
  QuickSortNonR(a, 0, sz - 1);
  PrintArr(a, sz);
  return 0;
}
相关文章
|
Shell 分布式数据库
shell脚本中if判断‘-a‘ - ‘-z‘含义
shell脚本中if判断‘-a‘ - ‘-z‘含义
313 0
|
前端开发 机器人 API
前端大模型入门(一):用 js+langchain 构建基于 LLM 的应用
本文介绍了大语言模型(LLM)的HTTP API流式调用机制及其在前端的实现方法。通过流式调用,服务器可以逐步发送生成的文本内容,前端则实时处理并展示这些数据块,从而提升用户体验和实时性。文章详细讲解了如何使用`fetch`发起流式请求、处理响应流数据、逐步更新界面、处理中断和错误,以及优化用户交互。流式调用特别适用于聊天机器人、搜索建议等应用场景,能够显著减少用户的等待时间,增强交互性。
3707 2
|
1月前
|
编解码 资源调度 物联网
正交时频空间(OTFS)调制技术:理论基础与性能分析
正交时频空间(OTFS)调制技术在延迟-多普勒域进行信号设计,有效应对高多普勒、短包传输等5G挑战。相比传统OFDM,OTFS通过全时频分集和信道硬化,显著提升高速移动场景下的鲁棒性与分集增益,仿真显示其在BLER性能上可获得3-4dB SNR增益,尤其适用于车联网、物联网等应用场景。
503 0
正交时频空间(OTFS)调制技术:理论基础与性能分析
|
11月前
|
弹性计算 自然语言处理 数据库
通过阿里云Milvus和LangChain快速构建LLM问答系统
本文介绍如何通过整合阿里云Milvus、阿里云DashScope Embedding模型与阿里云PAI(EAS)模型服务,构建一个由LLM(大型语言模型)驱动的问题解答应用,并着重演示了如何搭建基于这些技术的RAG对话系统。
|
消息中间件 存储 监控
实战Linux I/O多路复用:借助epoll,单线程高效管理10,000+并发连接
本文介绍了如何使用Linux的I/O多路复用技术`epoll`来高效管理超过10,000个并发连接。`epoll`允许单线程监控大量文件描述符,显著提高了资源利用率。文章详细阐述了`epoll`的几个关键接口,包括`epoll_create`、`epoll_ctl`和`epoll_wait`,以及它们在处理并发连接中的作用。此外,还探讨了`epoll`在高并发TCP服务场景的应用,展示了如何通过`epoll`和线程/协程池来构建服务框架。
1297 113
|
数据可视化 机器人 Python
实例8:机器人的空间描述和变换仿真
本文是关于机器人空间描述和变换的仿真实验教程,通过Python编程和可视化学习,介绍了刚体的平动和转动、位姿描述、坐标变换等基础知识,并提供了具体的实验步骤和代码实现。实验目的是让读者通过编程实践,了解和掌握空间变换的数学原理和操作方法。
291 2
实例8:机器人的空间描述和变换仿真
|
机器学习/深度学习 人工智能 数据挖掘
【机器学习】贝叶斯统计中,“先验概率”和“后验概率”的区别?
【5月更文挑战第11天】【机器学习】贝叶斯统计中,“先验概率”和“后验概率”的区别?
|
Android开发
Android基础知识:什么是Fragment?与Activity的区别是什么?
Android基础知识:什么是Fragment?与Activity的区别是什么?
2740 54
|
存储 缓存 前端开发
毕业设计|SpringBoot+Vue的前后端分离个人博客系统
毕业设计|SpringBoot+Vue的前后端分离个人博客系统
293 0
|
机器学习/深度学习 自然语言处理 计算机视觉
Claude 3系列包含Haiku(低)、Sonnet(中)和Opus(高)三个模型
Claude 3系列包含Haiku(低)、Sonnet(中)和Opus(高)三个模型
1025 7