手撕排序算法5:快速排序非递归版本
一.算法剖析
1.非递归版本的设计
递归版本的缺陷:
1.效率低(早期),目前效率并不低
2.极端情况下(当递归的深度太深时)会导致栈溢出
递归深度太深,程序是没错的,但是栈的空间不够用
递归改非递归
1.直接改循环(简单)
2.借助数据结构栈模拟递归过程(复杂一点)
非递归的效率并没有递归的效率的很大的提升,只不过是防止了栈溢出
在这里我们使用数据结构中的栈来帮助我们实现非递归,改循环的话非常困难
因为C语言没有标准模板库,所以我们必须自己实现栈
栈写好之后,所以我们只需要知道怎么用就行,所以在这里只把头文件给大家了
Stack.h
typedef int STDataType;
typedef struct Stack
{
STDataType* a;
int top;
int capacity;
}ST;
//初始化栈
void StackInit(ST* ps);
//销毁栈
void StackDestroy(ST* ps);
//入栈(压栈)
void StackPush(ST* ps, STDataType x);
//出栈
void StackPop(ST* ps);
//取栈顶元素
STDataType StackTop(ST* ps);
//求栈中数据个数
int StackSize(ST* ps);
//判断栈是否为空
bool StackEmpty(ST* ps);
2.在这里栈需要存什么,怎么利用
1.因为我们在这里使用数据结构的栈去替代函数栈帧来解决问题
那么我们就要让数据结构栈实现函数栈帧的功能
2.函数栈帧是用来存放局部变量的,所以我们要用栈来存放对应的局部变量
3.那么在这里的函数栈帧所存放的局部变量是什么呢?
大家可以看一下这张图片
下面我们来看一下栈的存放过程
二.代码实现
void print(int a[], int sz)
{
for (int i = 0; i < sz; i++)
{
printf("%d ", a[i]);
}
}
int PartSort(int* a, int left, int right)//前后指针法
{
int index = GetMidIndex(a, left, right);//三数取中优化版本
Swap(&a[left], &a[index]);
int keyi = left;
int prev = left;
int cur = left + 1;
while (cur <= right)
{
if (a[cur] < a[keyi])
{
++prev;
Swap(&a[cur], &a[prev]);
}
++cur;
}
Swap(&a[keyi], &a[prev]);
return prev;
}
void QuickSortNonR(int* a, int n)
{
ST st;
StackInit(&st);
StackPush(&st, n - 1);//栈是后进先出,要想后续先拿区间左侧,就要先入区间右侧,这里先入区间右侧
StackPush(&st, 0);
while (!StackEmpty(&st))
{
int left = StackTop(&st);//取区间左侧
StackPop(&st);
int right = StackTop(&st);
StackPop(&st);
int KeyIndex = 0;
KeyIndex = PartSort(a, left, right);
//[left,KeyIndex-1] KeyIndex [KeyIndex+1,right]
if (KeyIndex + 1 < right)//当右区间长度为1时不许入栈,因为已经长度为1的区间本来就是有序的
{
StackPush(&st, right);
StackPush(&st, KeyIndex + 1);
}
if (KeyIndex - 1 > left)
{
StackPush(&st, KeyIndex - 1);
StackPush(&st, left);
}
}
StackDestroy(&st);
}
从这里我们就可以看出用数据结构栈去模拟递归实现也类似于递归实现
也就是说它本质上还是递归的思路和实际的操作步骤
唯一不同的就是递归是将局部变量开辟在栈帧上.
而数据结构的栈是在堆上开辟的
这个非递归也可以通过队列来实现,只不过这个题用队列来实现的话,过程就不想递归了,而是一层一层地去进行的那趟排序,类似于二叉树的层序遍历
而用栈模拟跟递归一样都类似于二叉树的先序遍历
非递归的效率并没有递归的效率的很大的提升,只不过是防止了栈溢出
稳定性
快速排序不具有稳定性
因为我们一般只会左边找大,右边找小再去交换,相等的数我们根本就不管
三.计数排序(属于非比较排序)
1.基本思想
1.利用数组的下标来映射,来统计每个数出现的次数
数组大小为原数据当中最大的值+1,
因为要能够映射最大的那个元素
要统计完以后使用次数就可以排序了
<<剑指offer>>中出现过
但是如果我们遇到的数据是这样的呢?
100 101 102 101 109 105
我们发现,原数组只有6个数,但是最大值缺为109,
那么我们如果申请出一个能容纳110个整数的数组的话是不是就有些大材小用了,
所以我们称上面的映射叫做绝对映射
而我们最好使用下面介绍的相对映射
与绝对映射的不同点在于:
1.我们只需要申请一个数组,这个数组的大小为max-min+1即可
2.绝对映射的下标位置是num,而相对映射的下标位置是num-min
3.绝对映射是还原下标,而相对映射是还原下标+min
下面我们来实现一下代码(相对映射的版本)
2.代码实现
void CountSort2(int* a, int n)
{
int max = a[0];
int min = a[0];
int i = 0;
for (i = 0; i < n; i++)
{
if (a[i] > max)
{
max = a[i];
}
if (a[i] < min)
{
min = a[i];
}
}
int range = max - min + 1;//用range为相对映射
int* tmp = (int*)malloc(sizeof(int) * range);
if (tmp == NULL)
{
printf("malloc fail\n");
exit(-1);
}
//1.遍历初始化为0
//for (i = 0; i <= max; i++)
//{
// tmp[i] = 0;
//}
//2.用memset来初始化为0
memset(tmp, 0, sizeof(int) * range);
//统计次数
for (i = 0; i < n; i++)
{
tmp[a[i] - min]++;//-min为相对映射
}
//开始排序
int j = 0;
for (i = 0; i <= max; i++)
{
int count = tmp[i];
int num = i;
while (count--)
{
a[j] = num + min;//+min为相对映射
j++;
}
}
}
3.时间复杂度
时间复杂度:O(N+范围)
说明它适用于范围集中一组的整形数据排序
空间复杂度:O(范围)
不过计数排序只适用于整数排序
思想很巧,但是适用范围具有局限性
以上就是手撕排序算法的快排非递归版本和计数排序的讲解,希望能对大家有所帮助