快速排序(非递归)

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

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


       从前面的几种快排递归的写法(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‘含义
327 0
|
2月前
|
编解码 资源调度 物联网
正交时频空间(OTFS)调制技术:理论基础与性能分析
正交时频空间(OTFS)调制技术在延迟-多普勒域进行信号设计,有效应对高多普勒、短包传输等5G挑战。相比传统OFDM,OTFS通过全时频分集和信道硬化,显著提升高速移动场景下的鲁棒性与分集增益,仿真显示其在BLER性能上可获得3-4dB SNR增益,尤其适用于车联网、物联网等应用场景。
670 0
正交时频空间(OTFS)调制技术:理论基础与性能分析
|
3月前
|
存储 机器学习/深度学习 算法
基于A星算法的无人机三维路径规划算法研究(Mattlab代码实现)
基于A星算法的无人机三维路径规划算法研究(Mattlab代码实现)
295 1
|
Linux 数据库 iOS开发
CrossOver 25.1.0 for macOS & Linux - 领先的 Wine 解决方案
CrossOver 25.1.0 for macOS & Linux - 领先的 Wine 解决方案
473 0
|
8月前
|
存储 安全 固态存储
《深入理解数据库事务:掌握ACID特性的奥秘》
事务是数据库操作中确保数据一致性和完整性的核心机制,其ACID特性(原子性、一致性、隔离性、持久性)是关键保障。原子性确保操作“全有或全无”,避免部分执行导致的数据不一致;一致性维护业务逻辑和约束规则,使数据始终处于有效状态;隔离性通过并发控制技术防止多个事务互相干扰;持久性则保证事务提交后数据永久保存,即使系统故障也能恢复。以银行转账为例,事务将扣款与存款视为一个整体,任何失败均回滚,确保资金安全。掌握ACID特性对开发高效可靠的数据库系统至关重要。
400 1
|
9月前
|
机器学习/深度学习 人工智能 自然语言处理
AI第三极之争:生成式人工智能(GAI)认证如何重塑青年竞争力与时代担当
本文探讨了AI时代青年面临的机遇与挑战,强调跨学科知识、创新思维及伦理意识的重要性,并提出GAI认证对提升青年就业竞争力的意义。通过加强教育衔接、校企合作及政策支持,助力青年在AI领域成长,成为推动社会进步的重要力量。
|
安全 架构师 中间件
5个人如何1年交付了120+项目?分享我在阿里云做交付的工作手记
谨以此文,分享一些我加入阿里云后,我和我所在团队的成长经历。这里既有我个人如何从程序员成长为一个技术经理,也有我的团队如何把事情越做越大的过程和思考,希望能够帮到有需要的人。
5个人如何1年交付了120+项目?分享我在阿里云做交付的工作手记
|
机器学习/深度学习 人工智能 数据挖掘
【机器学习】贝叶斯统计中,“先验概率”和“后验概率”的区别?
【5月更文挑战第11天】【机器学习】贝叶斯统计中,“先验概率”和“后验概率”的区别?
|
存储 关系型数据库 MySQL
MySQL 分区表
MySQL 分区表
229 4
|
Android开发
Android基础知识:什么是Fragment?与Activity的区别是什么?
Android基础知识:什么是Fragment?与Activity的区别是什么?
2873 54

热门文章

最新文章