快速排序(非递归)

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

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


       从前面的几种快排递归的写法(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;
}
相关文章
|
Linux 编译器 C语言
Linux应用开发基础知识——Makefile 的使用(二)
Linux应用开发基础知识——Makefile 的使用(二)
811 0
Linux应用开发基础知识——Makefile 的使用(二)
|
8月前
|
存储 机器学习/深度学习 算法
基于A星算法的无人机三维路径规划算法研究(Mattlab代码实现)
基于A星算法的无人机三维路径规划算法研究(Mattlab代码实现)
486 1
|
9月前
|
供应链 JavaScript API
深度分析电子元件API接口,用Python脚本实现
电子元件API为电子制造、研发及供应链提供元件查询、库存、价格、供应商及技术文档等核心功能,支持采购决策与研发选型。主流平台包括国际的Digikey、Mouser及国内的立创商城、华强电子网,接口设计各有差异但功能逻辑一致。
|
11月前
|
人工智能 前端开发 搜索推荐
LangGraph实战教程:构建会思考、能记忆、可人工干预的多智能体AI系统
本文介绍了使用LangGraph和LangSmith构建企业级多智能体AI系统的完整流程。从简单的ReAct智能体开始,逐步扩展至包含身份验证、人工干预、长期内存管理和性能评估的复杂架构。文章详细讲解了状态管理、工具集成、条件流程控制等关键技术,并对比了监督者架构与群体架构的优劣。通过系统化的方法,展示了如何构建可靠、可扩展的AI系统,为现代AI应用开发提供了坚实基础。*作者:Fareed Khan*
2609 0
LangGraph实战教程:构建会思考、能记忆、可人工干预的多智能体AI系统
|
存储 安全 固态存储
《深入理解数据库事务:掌握ACID特性的奥秘》
事务是数据库操作中确保数据一致性和完整性的核心机制,其ACID特性(原子性、一致性、隔离性、持久性)是关键保障。原子性确保操作“全有或全无”,避免部分执行导致的数据不一致;一致性维护业务逻辑和约束规则,使数据始终处于有效状态;隔离性通过并发控制技术防止多个事务互相干扰;持久性则保证事务提交后数据永久保存,即使系统故障也能恢复。以银行转账为例,事务将扣款与存款视为一个整体,任何失败均回滚,确保资金安全。掌握ACID特性对开发高效可靠的数据库系统至关重要。
556 1
|
JSON 人工智能 算法
探索LLM推理全阶段的JSON格式输出限制方法
文章详细讨论了如何确保大型语言模型(LLMs)输出结构化的JSON格式,这对于提高数据处理的自动化程度和系统的互操作性至关重要。
3387 52
|
存储 Ubuntu Docker
Docker从入门到精通:Docker pull命令学习
了解Docker镜像下载方法!使用`docker pull`命令从[Docker Hub](https://hub.docker.com/)获取镜像。基本语法是`docker pull NAME[:TAG]`。例如,拉取Python最新镜像的命令是`docker pull python`或`docker pull python:latest`。可选参数包括`-a`(拉取所有标签)和`--quiet`(只显示进度条)。拉取后,用`docker images`检查镜像是否成功存储。开始你的容器化之旅吧!
|
机器学习/深度学习 人工智能 自然语言处理
AI第三极之争:生成式人工智能(GAI)认证如何重塑青年竞争力与时代担当
本文探讨了AI时代青年面临的机遇与挑战,强调跨学科知识、创新思维及伦理意识的重要性,并提出GAI认证对提升青年就业竞争力的意义。通过加强教育衔接、校企合作及政策支持,助力青年在AI领域成长,成为推动社会进步的重要力量。
|
缓存 C语言
glibc函数malloc的工作原理
glibc函数malloc的工作原理
322 0
|
人工智能 搜索推荐 数据挖掘
AI教育的评估方法有哪些?
【6月更文挑战第2天】AI教育的评估方法有哪些?
883 2

热门文章

最新文章