快速排序非递归实现
无论什么算法,用递归肯定存在栈溢出的风险,需要转化为非递归。如快排,快排进行非递归变化的其实是处理区间,且都是在同一数组上进行操作。因此可节省函数栈的开销将区间的代表数值仅栈。
关于快排的非递归细节:
将数组的两端下标先入栈,再取出来进行一趟排序;后分割为2个区间,每个区间都有2个端点值,再将他们压栈,顺序不影响。后重复一样的操作。
也就是每次从栈里面拿出一段区间,单趟分割处理左、右子区间入栈。
当两个数不构成区间(left>=right)时停止本次操作。
例如一种区间划分:

也可以使用队列。但只有栈才能模拟递归的效果,队列是处理抽象树一层一层的对象。
两种分别代表深度优先(dfs)和广度优先(bfs)的思路,在不同场景有不同用途。
Linux线程32位的一个栈只有8MB空间,足够日常使用,但极端情况下递归深度太深扛不住。 8 M B = 8 × 1024 × 1024 b y t e 8MB=8\times1024\times1024\ byte 8MB=8×1024×1024 byte。
堆则是以GB为单位,空间相对比原生栈大。所以无论是windows还是Linux,向堆区申请空间,可以申请数量可观的内存。
此外,快排的每次递归相当于二叉树,这里的实现方式又是将每个区间的端点值入栈,
原则上只要在快排的递归深度 l o g 2 ( n + 1 ) log_2(n+1) log2(n+1)的基础上乘2即可满足快排对栈容量的全部需求。
但经过测试还有没发现的不确定因素使得空间复杂度乘2依旧不够开销,所以乘3,
但其实就算乘3,开辟的空间数也非常小,随着n的增大到一定的量,依旧会越界访问。但如果栈的空间开到n,则不会出现这种问题。
参考程序:
//快排的非递归实现
void quickSortNonR(Datatype* a, int left, int right) {
int* st = (int*)malloc(
sizeof(int) * 200//floor(log(right - left + 1) / log(2)) * 3
);
//栈开200个空间后就能处理平时99%的数据排列
if (st == NULL) {
printf("栈空间申请失败\n");
return;
}
int top = 0;//一个栈顶变量和一个数组构成的简易栈
st[top++] = right;
st[top++] = left;
while (top != 0) {
int l = st[top - 1];
--top;
int r = st[top - 1];
--top;
int keyi = partSortDPoint(a, l, r);//单趟排序
if (keyi + 1 < r) {
st[top++] = r;//右边先入栈
st[top++] = keyi + 1;
}
if (l < keyi - 1) {
st[top++] = keyi - 1;//左边后入栈
st[top++] = l;
}
}
free(st);
}
关于这里的栈空间的大小:空间的大小取决于快速排序的空间复杂度。
快排的排序过程可看成一个二叉树,而区间划分的深度也就是二叉树的高度,所以栈空间的大小开到 [ l o g 2 n ] [log_2n] [log2n]就足够大部分情况使用,这个非递归快排因为区间分开存储,所以需要乘2。最坏的情况是数据在排序之前便已有序,或每次都选最值作为基准值,此时需要为每个数据开一个栈空间,这个时候空间复杂度便来到 O ( n ) O(n) O(n)。
因为对数的特性,当我们的栈开到200个单位时,通过普通计算器理论能排序的数据已经来到 1 0 30 10^{30} 1030。因为 l o g 2 1 0 30 = 30 l o g 2 10 ≈ 99.66 log_2{10^{30}}=30log_2{10}\approx99.66 log21030=30log210≈99.66,也就是说, 1 0 30 10^{30} 1030个数据进行快排时的递归深度最多有100层,如果是存储区间的端点值的话则需要200层。
实际测试过在栈开出200个空间的情况下完成了 1 0 9 10^9 109个数据的排序。这样庞大的数据一般放在文件中,排序在文件中的数据用外排序。
但无论怎么样,开辟的空间为 ( r i g h t − l e f t ) ∗ 2 (right-left)*2 (right−left)∗2个单位时,快速排序都能通过。
测试用的程序:
#include<stdio.h>
#include<stdlib.h>
#include<time.h>
#include<stdlib.h>
#include<stdbool.h>
#include<math.h>
typedef int Datatype;
int sortJudg(Datatype* a, int n) {//判断数组是否升序
int i = 0;
for (i = 0; i < n - 1; i++) {
if (a[i] > a[i + 1])
break;
}
return i == n - 1;
}
void Swap(Datatype* a, Datatype* b) {
int tmp = *a;
*a = *b;
*b = tmp;
}
int partSortHoare(Datatype* a, int left, int right) {
int keyi = left;//这里规定keyi记录下标, key记录值。
while (left < right) {//相遇时结束循环
// 右边找小
while (left < right && a[right] >= a[keyi])//利用c语言的短路特性去判断
--right;
// 左边找大
while (left < right && a[left] <= a[keyi])//这里还有问题
++left;
Swap(&a[left], &a[right]);
}
Swap(&a[keyi], &a[left]);//相遇了就交换
return left;
}
int partSortHole(Datatype* a, int left, int right) {
int key = a[left];
int hole = left;//坑
while (left < right) {
// 右边找小
while (left < right && a[right] >= key)
--right;
a[hole] = a[right];
hole = right;
// 左边找大
while (left < right && a[left] <= key)
++left;
a[hole] = a[left];
hole = left;
}
a[hole] = key;
return hole;
}
int partSortDPoint(Datatype* a, int left, int right) {
int prev = left;
int cur = left + 1;
int keyi = left;
while (cur <= right) {
if (a[cur] < a[keyi] && ++prev != cur)
Swap(&a[prev], &a[cur]);
++cur;
}
Swap(&a[prev], &a[keyi]);
return prev;
}
//快排的非递归实现
void quickSortNonR(Datatype* a, int left, int right) {
int* st = (int*)malloc(
sizeof(int) * 200//floor(log(right - left + 1) / log(2)) * 3
);
if (st == NULL) {
printf("栈空间申请失败\n");
return;
}
int top = 0;//一个栈顶变量和一个数组构成的简易栈
st[top++] = right;
st[top++] = left;
while (top != 0) {
int l = st[top - 1];
--top;
int r = st[top - 1];
--top;
int keyi = partSortDPoint(a, l, r);//单趟排序,三个版本都可以
if (keyi + 1 < r) {
st[top++] = r;//右边先入栈
st[top++] = keyi + 1;
}
if (l < keyi - 1) {
st[top++] = keyi - 1;//左边后入栈
st[top++] = l;
}
}
free(st);
}
void f() {
srand((size_t)time(0));
#define NUM 1000000000
Datatype* a = (Datatype*)malloc(sizeof(Datatype) * NUM);
if (a == NULL) {
printf("数组空间申请失败\n");
return;
}
int i = 0;
for (i = 0; i < NUM; i++) {
a[i] = rand()*rand() + 1;//rand的返回值最大只有32767(0x7fff),于是相乘使数据范围扩大减少重复数据
}
quickSortNonR(a, 0,NUM-1);
printf("%d\n", sortJudg(a, NUM));//判断数据是否升序
free(a);
}
int main() {
f();
return 0;
}
实际实现时也可以使用链式栈,可以在计算机能承受的范围内不用考虑栈溢出的情况。
快速排序的特性总结
-
快速排序整体的综合性能和使用场景都是比较好的,所以才敢叫快速排序
-
时间复杂度: O ( N ∗ l o g N ) O(N*logN) O(N∗logN)
当快排的
key选中位数时,第一轮的单趟是处理 n n n个数,第二轮的两轮递归总共处理 n n n个数,
依次类推,最后一次每次递归处理1个数,总共处理的层数为 l o g 2 ( N + 1 ) log_2(N+1) log2(N+1), N N N为递归申请的函数调用次数(这种情况和二分十分接近)。
所以最好的情况为 n l o g 2 ( N + 1 ) nlog_2(N+1) nlog2(N+1),即每层处理 n n n个数据,总共有 [ l o g 2 ( N + 1 ) ] [log_2(N+1)] [log2(N+1)]层。
而每次递归又存在一个
key,最后一层递归相当于每个数都当选一轮key,因为之前每轮递归选出一个中位数排好,所以每层实际上都是接近 n n n次处理。所以每个数都当一次中位数,也就有 n = 2 h − 1 n=2^h-1 n=2h−1, h = l o g 2 ( n + 1 ) h=log_2(n+1) h=log2(n+1),
即每次递归选1个数据排好,剩下的递归再选各自的中间数,每个数都当选1次中间数,将每次递归当成二叉树的结点,n就是结点数。
所以 N N N可近似看做 n n n,
时间复杂度为 O ( n l o g n ) O(nlogn) O(nlogn)。
当数组有序时,快排效率最低,是 O ( n 2 ) O(n^2) O(n2)。
若数组有序,每次选
key时都是选第1个(按hoare版本思路分析),因为左区间和右区间肯定有一个不存在,每轮选
key的时候相当于缩小排序的范围,这点和冒泡排序十分相似。之后递归时,因为每次都是一个区间不存在,另一个区间都是上一层递归处理的数据数少1,进行多次排列后,每层递归的排列次数为 n , n − 1 , n − 2 , ⋯ , 3 , 2 , 1 n,n-1,n-2,\cdots,3,2,1 n,n−1,n−2,⋯,3,2,1,这是等差数列,求和的话排列次数还是 n 2 n^2 n2。
如何解决?
- 随机数选key,即不再只选固定位为key。
- 三路划分。
而且部分情况还需要两个方案一起用。
尽管快排对这种有序或接近有序的数据十分乏力,但对全是随机数的数据依旧有可观的效率。
- 空间复杂度: O ( l o g N ) O(logN) O(logN),最坏情况为 O ( N ) O(N) O(N)。
函数栈最多申请 h h h层,而 n = 2 h − 1 n=2^h-1 n=2h−1,所以 h = l o g 2 ( n + 1 ) h=log_2(n+1) h=log2(n+1),空间复杂度即为 O ( l o g n ) O(logn) O(logn)。
最坏的情况是数据在排序之前便已有序,或每次都选最值作为基准值,此时需要为每个数据开一个栈空间,这个时候空间复杂度便来到 O ( n ) O(n) O(n)。
- 稳定性:不稳定
因为快排存在左、右区间的数交换,所以不稳定。
例如这个例子:
[6,6,3],快排无论哪种方案,,只要key为6,排列结果可能为[3,6,6],原本两个6的相对位置发生了变化。除了举例,还可以通过定义存储数据和自己原来的位置的自定义变量,通过对这种自定义类型的数组进行排序,来观察该排序算法是否稳定。代码参考之前的排序算法。

5004

被折叠的 条评论
为什么被折叠?



