忙了一个月考试和课设,今天写点排序
今天主要内容是在排序中比较难的快排
一、快速排序
首先来说快速排序的定义:通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据比另一部分的所有数据要小,再按这种方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,使整个数据变成有序序列。
快速排序是冒泡排序的改进。
快速排序一般有以下几种方法:
hoare法:
我们有一组待排序的数据,在其中选一个关键字key出来,一般是选数据的头数字或者尾数字
举个例子,用以下数据来进行hoare法排序
6 1 2 7 9 3 4 5 10 8
首先选key为6,设置begin和end两个指针,分别指向数据列的头和尾,接着end从右边出发,找比key小的数字,找到5,然后begin开始从左走,找比6大的数字,一直走到7,然后5和7互换,然后end继续从7向左出发继续找小,找到4,begin再出发,找到9,互换,end再出发找到3,begin再出发,此时就会和end相遇,交换key和begin end相遇的值,这样,单趟快速排序就完成了,接下来,key的左边的值比key小,key右边值比它大,再想办法让key左边区间有序,key的右边区间有序,整体就有序了。
其实上面这段话描述完,应当有两个疑问,当key为头的时候,为什么一定要右边先出发呢?为什么begin和end相遇的那个数字一定比key小呢?其实这是一个问题,因为begin和end相遇只有两种情况,一种是begin碰到end。另一种是end碰到begin
begin碰到end:end先走,end停在了比key小的位置上,如果此时begin后面的都比key小,那么begin就来到了end的位置停止了,而end就在比key小的位置上。
end碰到begin:end先走,end停在了比key小的位置上,begin再走,begin停在了比key大的位置上,将begin和end处的值交换,end再走,假设end前面的都比key小,那么end来到了begin的位置就停止了,此时相遇,此时相遇点处的值比key小了,为什么呢?因为前面begin处的值与end处的值发生了交换,而end处的值是小于key的。
接下来,我们就该考虑,如何让左右区间分别有序呢?
那就是递归
代码图如下
//单趟排序
int PartSort(int *a,int begin,int end)
{
int keyi = begin;//选定一个关键字的下标
while (begin < end)
{
//右边先走 右边先找比keyi小的
while (begin < end && a[end] >= a[keyi])//这里要注意=的添加,不然就错了
{
end--;
}
//此时end是比keyi小的那个数的下标
//左边找比keyi大的
while (begin < end && a[begin] <= a[keyi])
{
begin++;
}
//此时begin是比keyi大的那个数的下标
Swap(&a[begin], &a[end]);//交换
//这里的目的是让左边是比关键字小的,右边是比关键字大的
}
//此时begin和end相遇了
Swap(&a[begin], &a[keyi]);//交换相遇的地方和关键字的位置
return begin;//返回关键字的位置
}
//递归
void QuickSort(int* a, int begin, int end)
{
if (begin >= end)//如果区间不存在
{
return;
}
int keyi = PartSort(a, begin, end);
QuickSort(a, begin, keyi - 1);
QuickSort(a, keyi + 1, end);
}
快排时间复杂度
这个需要分类讨论:
1.理想情况下,单趟排序后key已经出于中间,那么递归深度为层数次,也就是logN,而每一层的时间复杂度都是N,所以总体复杂度就是相乘
2.但大部分情况下都是不理想的,比如取个不好点的情况,数据有序,不论升序降序,如果想重新排一遍,那么一定每次有一边的区间都是空的,那么递归深度就到了N次,而每一层都需要N次,所以时间复杂度就square了。
所以这里可以提出一个优化的方案
随机选key或者是三数取中,实际中三数取中应用比较广泛。
下面实现三数取中
int GetMidIndex(int* a, int begin, int end)
{
int mid = (begin + end) / 2;
if (a[begin] < a[mid])
{
if (a[mid] < a[end])
{
return mid;
}
else if (a[begin] < a[end])
{
return end;
}
else
{
return begin;
}
}
else
{
if (a[mid] > a[end])
{
return mid;
}
else if (a[begin] < a[end])
{
return begin;
}
else
{
return end;
}
}
}
三数取中之后就保证了不会出现上面说的只有一边有数的情况,所以基本就是最优了。
接下来介绍快排的另一种方法,思想都是一样的,方法大同小异
挖坑法
将最左边(右边也可以)的值用key变量保存起来,此时最左边(右边)形成一个坑位,然后end开始走(如果最右边找的key,那么begin先走),end找比关键字小的,找到后将坑位填充,然后坑位变为end,然后begin开始走,begin找比关键字大的,找到后将坑位填充,然后坑位变为begin,直到begin和end相遇,此时begin和end都是坑位,将之前保存的关键字的值填到坑里面
这种方法和hoare法其实差不多
int PartSort2(int* a, int begin, int end)
{
int hole = begin;
int key=a[begin];
while(begin<end)
{
//让右边先走找比key小的值,填到坑里
while(begin<end && a[end]>=key)
{
end--;
}
//填到坑
a[hole]=a[end];
//更新坑
hole=end;
//左边走,找比key大的值,填到坑
while(begin<end && a[begin]<=key)
{
begin++;
}
//填到坑
a[hole]=a[left];
//更新坑
hole=begin;
}
//相遇
a[hole]=key;
return hole;
}
最后介绍一种快排法,
前后指针法
这种方法可以避开比较多的麻烦
前后指针法核心思想:
1.cur往前走,找比key小的数据
2.找到比key小的数据以后,停下来,prev++
3.交换prev和cur指向位置的值
4.直到cur走到数组的结尾,将key与prev交换
这里我们选取第一个数为key,具体过程看下面动图
cur还没遇到比key大的值前,prev紧跟着cur,cur遇到比key大的值后,prev和cur之间间隔了一段比key大的数据,所以当cur找到小于关键字的值时,prev++,将cur和prev交换,实际上就是将小于关键字的值换到左边,大于关键字的值换到了右边
具体代码如下:
int PartSort3(int* a, int begin, int end)
{
int prev = begin;
int cur = begin + 1;
int keyi = begin;
// 加入三数取中的优化
int midi = GetMidIndex(a, begin, end);
Swap(&a[keyi], &a[midi]);
while (cur <= end)
{
// cur位置的之小于keyi位置值
if (a[cur] < a[keyi] && ++prev != cur)
Swap(&a[prev], &a[cur]);
++cur;
}
Swap(&a[prev], &a[keyi]);
keyi = prev;
return keyi;
}
以上三种就是常见的快排方法,但都存在一个致命的缺点,就是递归,一旦递归,就涉及到性能和栈溢出的问题,所以下面再写以下快速排序的非递归思想
非递归思路:
这里我们借用数据结构的栈来实现
依次把需要单趟排的区间入栈,依次取栈里面的区间出来单趟排,再把需要处理的子区间入栈
还是用上面的数据举例子
6 1 2 7 9 3 4 5 10 8
此时begin是0,end是9,首先将9和0入栈,当栈不为空的时候,令left等于0,令right等于9,分别出栈,然后用设定好的left和right对a进行一次快速排序,排完之后就是3 1 2 5 4 6 9 7 10 8,此时begin也就是keyi指向6,值为5,而end指向3,值为0,接下里就判断左右子区间的存在性,然后把子区间入栈,再快排,最后直到入完栈。
void QuickSortNonR(int *a,int n)
{
ST 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 keyi = PartSort1(a,left,right);
//[left,keyi-1] keyi [keyi+1,right]
if(keyi+1<right)//如果存在右序列(右序列需要大于1)
{
StackPush(&st,right);
StackPush(&st,keyi+1);
}
if(keyi-1>left)//如果存在左序列(左序列需要大于1)
{
StackPush(&st,keyi-1);
StackPush(&st,left);
}
}
StackDestory(&st);
}
在快速排序中, 这个非递归其实就是目标堆栈是一个不断 入栈-出栈 的过程,在出栈的过程中,就对数据进行处理,没有必要再最后一次性处理。将递归的方式变为出入栈的方式,本质是一样的。