🥰作者: FlashRider
🌏专栏: 初阶数据结构
🍖知识概要:探讨八大排序的时空复杂度与稳定性,讲解快排和归并的非递归实现。
目录
快速排序-非递归
快速排序的递归过程实际上可以看作二叉树,我们利用先序遍历或者层序遍历(栈或队列)来实现非递归即可。
这里直接用栈实现,对于一趟快排结束后,我们要排他的左右区间,所以我们把区间入栈就行了,但要注意入栈出栈的顺序,不要把区间的端点弄反了。
void QuickSortNonR(int* a, int begin, int end)//栈实现
{
Stack st;
StackInit(&st);
StackPush(&st, end);
StackPush(&st, begin);
while(!StackEmpty(&st))
{
int left = StackTop(&st);
StackPop(&st);
int right = StackTop(&st);
StackPop(&st);
int key = left;
int i = left, j = right;
//此处用i j快排一趟 key随关键值下标改变而改变
while(i < j)
{
while(i < j && a[j] >= a[key]) j--;
while(i< j&& a[i] <= a[key]) i++;
Swap(a[i], a[j]);
}
Swap(a[i], a[key]);
key = i;//修正key
if(left < key - 1)//子区间入栈 可以下面的if交换顺序 不重要
//只要Push的时候的两个区间左右下标都对应前面的left right即可。
{
StackPush(&st, key - 1);
StackPush(&st, left);
}
if(key + 1 < right)
{
StackPush(&st, right);
StackPush(&st, key + 1);
}
}
StackDestroy(&st);
}
归并排序-非递归
归并排序因为要先划分区间,所以过程是二叉树的后序遍历,因此用栈和队列不好去实现,所以我们选择用步长来不断分割区间,控制好边界条件,模拟非递归。
因为gap是不断×2的,所以对于一些序列长度来说划分的区间可能会越界,因此越界后要修正边界。
oid MergeSortNonR(int* a, int n)
{
int gap = 1;
int* tmp = (int*)malloc(sizeof(int) * n);
while(gap < n)
{
for(int i = 0; i < n; i += 2*gap)
{
//分为[i, i + gap - 1] [i+gap, i + 2*gap - 1]
int l1 = i, r1 = i+gap-1;
int l2 = i+gap, r2 = i+2*gap-1;
int k = l1;
//越界修正边界
if(r1 >= n)
{
r1 = n - 1;
l2 = n;
r2 = n - 1;
}
else if(l2 >= n)
{
l2 = n;
r2 = n - 1;
}
else if(r2 >= n)
r2 = n - 1;
while(l1 <= r1 && l2 <= r2)
{
if(a[l1] < a[l2])
{
tmp[k++] = a[l1];
l1++;
}
else
{
tmp[k++] = a[l2];
l2++;
}
}
while(l1 <= r1) tmp[k++] = a[l1++];
while(l2 <= r2) tmp[k++] = a[l2++];
}
memcpy(a, tmp, n * sizeof(int));
gap *= 2;
}
free(tmp);
}
八大排序分析
时间复杂度:时间复杂度简单来说就是对一个算法所用的时间量级的表示,比如一个算法,你输入n个数据,他要大概循环n次,执行n次关键语句,那它的时间复杂度就是O(n)。
空间复杂度:空间复杂度是算法需要的额外空间的表示,比如归并n个数据,他需要开一个大小为n的临时数组,因此空间复杂度就是O(n)。
稳定性:排序算法的稳定性简单来说就是对于两个相等的数据,排序前后他们的相对顺序不变。
比如 1 3 1 2 排序后 1 1 2 3。红包依然在橙色前面。
排序算法 | 时间复杂度 | 空间复杂度 | 稳定性 | 分析 |
插入排序 | O(N)~O(N^2) | O(1) | 稳定 | 对于有序序列插入排序的时间最短,接近O(N),对于完全倒序的序列时间复杂度为O(N^2)。因为是一个一个数据往前比较插入,所以相同数据不会交换,是稳定的。 |
希尔排序 | 平均O(N^1.3) | O(1) | 不稳定 | 只需要记住平均复杂度就行,详细分析比较复杂,可以去查其他资料。因为是按照步长插排的,所以不一定稳定。 |
选择排序 | O(N^2) | O(1) | 不稳定 | 每次选择最大最小为n次循环,要选择n次,所以一共是n*n=n^2的时间复杂度,因为选择完后直接是按位置交换,而不考虑交换的时候谁大谁小,所以不稳定。 |
堆排序 | O(NlogN) | O(1) | 不稳定 | 原序列建堆调整为logn,遍历为n,所以时间复杂度为nlogn。 |
冒泡排序 | O(N)~O(N^2) | O(1) | 稳定 | 相邻比较交换,所以稳定。优化之后,对于有序序列,遍历一次发现没有交换就会break,所以最好是O(N),最坏O(N^2)。 |
快速排序 | O(NlogN)~O(N^2) | O(logN) | 不稳定 | 递归需要开辟函数栈帧,非递归也需要栈或队列模拟,所以空间复杂度是logN。对于完全有序的序列来说快排时间复杂度会降低到N^2,此时可以使用取mid为key来优化。 |
归并排序 | O(NlogN) | O(N) | 稳定 | 归并需要有临时数组,所以空间复杂度O(N)。因为是两个序列比较,按大小顺序加入到临时数组中,所以是稳定的。 |