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