排序2-冒泡排序与快速排序(递归加非递归讲解)
Posted 无聊的马岭头
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了排序2-冒泡排序与快速排序(递归加非递归讲解)相关的知识,希望对你有一定的参考价值。
前言
一、冒泡排序
冒泡排序:是一种交换排序,其基本思想是,两两比较相邻记录的关键字,如果反序则交换,直到没有反序的记录就为止。
当然,我们今天的主角是后面要介绍的快排,我相信朋友们对冒泡已经很熟悉了,我就简单带过一下。
代码:
//冒泡排序:两个两个比较
void BubblSort(int *arr,int n)
{
for (int end = n; end > 0; end--)
{
int flag=0;//优化,防止已经在有序的情况下,还来做无意义的比较
for (int i = 1; i < end; i++)
{
if (arr[i]>arr[i - 1])
{
Swap(&arr[i], &arr[i - 1]);
flag=1;
}
}
if(flag==0)
break;
}
}
总结:
- 时间复杂度:O(N2)
- 空间复杂度:O(1)
- 稳定性:稳定
二、快速排序
快排的基本思想:通过一趟排序将待排记录分割成独立的两个部分,其中一部分记录的关键字均比另一部分记录的关键字小,则可分别对这两部分记录继续进行排序,以达到整个序列有序的目的。
怎样来分割成这两个部分呢?
- 在待排序中找一个关键字(key)来分割,我们一般选最左边或者最右边.
1.左右指针法
- 定义两个变量来记录下标,分别为left=0、right=n
- 右边的找比key小的值,左边的找比key大的值
- 其细节是,先移动right来找小,找到后,再来移动left来找大,然后交换,当left>=right的时候,停止,再交换left和key上的值。(第一趟分割完成)
- 然后递归
我们在写代码的时候,应该先分析成两个部分
1.先写第一趟分割,
2.然后写多趟。
代码:
void Swap(int *p, int *q)
{
int temp = *p;
*p = *q;
*q = temp;
}
void PartSort1(int* arr, int begin, int end)
{
if (begin >= end)
return;
int left = begin,right=end;
int keyi = left;//上面介绍中key的下标
while (left < right)
{
//找小
while (left<right&&arr[right]>=arr[keyi])
{
right--;
}
//找大
while (left<right&&arr[left]<=arr[keyi])
{
left++;
}
//交换
Swap(&arr[left], &arr[right]);
}
Swap(&arr[keyi], &arr[left]);
int meeti = left;//相遇点
//分割后两个部分的区间[begin,keyi-1][keyi+1,end]
//递归
PartSort1(arr, begin, meeti - 1);
PartSort1(arr, meeti+1,end);
}
2.挖坑法
挖坑法和上面的左右指针法有点类似。
- 找最左边关键字key并记录,arr[0]为一个坑,定义两个变量记录下标,left=0、right=n
- 右边先找小,找到后,把小的值填到arr[0]中,而找到的那个小的值其所在的位置就为一个新坑
- 然后左边再找大,找到后,把大的值放到新坑中,自己的位置就变成了一个新坑,依此往复。
- 当left>=right时,停止,然后把key放到新坑中(第一趟分割完了)
- 递归
代码:
//挖坑法,快排
void PartSort4(int* arr, int begin, int end)
{
if (begin >= end)
return;
int key = arr[begin];
int left = begin;
int right = end;
while (left < right)
{
//找小
while (left < right&&arr[right]>=key)
{
--right;
}
//放到左边的坑位中,右边形成新的坑位
arr[left] = arr[right];
//找大
while (left< right&&arr[left]<=key)
{
left++;
}
//放到右边的坑位中,左边形成新的坑位
arr[right] = arr[left];
}
arr[left] = key;//把key放入新坑
int meeti = left;
//分割后两个部分的区间[begin,meeti-1][meeti+1,end]
//递归
PartSort4(arr, begin, meeti - 1);
PartSort4(arr, meeti+1, end);
}
3.前后指针法
- 由标题题意就知道,定义两个变量来记录下标,且下标是前后关系,left=0、right=left+1.
- 别忘了,我们是来分割的,所以我们还要定义一个关键字(key),取最左边的值为关键字,并保存下标为keyi.
- arr[right]<arr[left],left++,交换,然后right++(这里我在代码中做了一下优化)
- 直到right>n时,才停止,然后交换arr[left]和arr[keyi](第一趟分割完成)
- 递归(在递归中做了一些优化:当数据超大时,一直递归有可能会栈溢出,我们可以排序了一段时间后,直接用插入排序)
代码:
void PartSort3(int* arr, int begin, int end)
{
if (begin >= end)
return;
if (end - begin > 17)//优化
{
int left = begin;
int right = left + 1;
int keyi = begin;
while (right <= end)
{
if (arr[right] < arr[keyi] && ++left != right)
{
Swap(&arr[right], &arr[left]);
}
++right;
}
Swap(&arr[left], &arr[keyi]);
keyi = left;//这时候left的位置就是分割的地方
//分割后两个部分的区间[begin,keyi-1][keyi+1,end]
//递归
PartSort2(arr, begin, keyi - 1);
PartSort2(arr, keyi + 1, end);
}
else
{
InsertSort(arr + begin, end - begin + 1);
}
}
4.快排的优化(三数取中)
看完上面三种方法后,我们应该已经发现了,当一个数组中是有序的情况下,我们每次分割只得到比上一次小一个记录的子序列,注意另一个为空,这时候我们的时间复杂度是O(N2)
所以,我们可以三数取中来优化
- 找下标为0的值,和下标为n的值,和下标为(n+0)/2的值,用它们的中位数来当key
代码:
//三数取中,找中间值
int GetMidIndex(int *arr,int left,int right)
{
int mid = (left + right) >> 1;
if (arr[left] > arr[mid])
{
if (arr[mid] > arr[right])
return mid;
else if (arr[left] < arr[right])
return left;
else
return right;
}
else//arr[left]<=arr[mid]
{
if (arr[left]>arr[right])
return left;
else if (arr[mid] < arr[right])
return mid;
else
return right;
}
}
5.迭代实现(非递归)
这可是我们的大餐啊,都说快排用递归实现,那我们如何用非递归实现呢?
这就得我们用栈或者队列来实现了
- 栈是来存储我们要进行分割排序的这段区间的下标的
- 把我们要进行区间的下标放入栈中,然后再放入,再取出。(每一次放入的都不同)
- 每取出一段区间,我们都对这段区间来进行单趟排序
- 然后把分割成两部分的区间下标,再放入栈中,等下次来取
- 判断结束的标志是栈中是否还有未处理的区间
- 而判断区间下标是否需要放入栈中的条件是,其区间中元素的个数是否大于1
- 排完后,销毁栈
代码:
//非递归快排,迭代
void QuickSortNonR(int* a, int left, int right)
{
//创建栈
Stack st;
//初始化栈
StackInit(&st, 10);
//先入大区间
if (left < right)
{
StackPush(&st, right);
StackPush(&st, left);
}
//栈不为空,说明还有没处理的区间
while (StackEmpty(&st) != 0)//终止条件
{
left = StackTop(&st);
StackPop(&st);
right = StackTop(&st);
StackPop(&st);
//快排单趟排序
int div = PartSort5(a, left, right);//这个函数是进行单趟排序的
// [left div-1]
// 把大于1个数的区间继续入栈
if (left < div - 1)//判断是否还需要接着排序的条件
{
StackPush(&st, div - 1);
StackPush(&st, left);
}
// [div+1, right]
if (div + 1 < right)
{
StackPush(&st, right);
StackPush(&st, div + 1);
}
}
StackDestroy(&st);//最后销毁栈
}
总结:
- 快速排序整体的综合性能和使用场景都是比较好的,所以才敢叫快速排序
- 时间复杂度:O(N*logN)
- 空间复杂度:O(logN)
- 稳定性:不稳定
总结
有问题的地方欢迎指出,共同学习,我们下期见。
以上是关于排序2-冒泡排序与快速排序(递归加非递归讲解)的主要内容,如果未能解决你的问题,请参考以下文章
java快速排序引起的StackOverflowError异常
8种面试经典!排序详解--选择,插入,希尔,冒泡,堆排,3种快排,快排非递归,归并,归并非递归,计数(图+C语言代码+时间复杂度)
8种面试经典排序详解--选择,插入,希尔,冒泡,堆排,3种快排及非递归,归并及非递归,计数(图+C语言代码+时间复杂度)
8种面试经典排序详解--选择,插入,希尔,冒泡,堆排,3种快排及非递归,归并及非递归,计数(图+C语言代码+时间复杂度)