一、递归实现快速排序
快速排序简称快排,快速排序是Hoare于1962年提出的一种二叉树结构的交换排序方法,其基本思想为:
任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止。
根据上面的快速排序思想我们就有了三个步骤来实现从小到大的序列:
1.选择基准:从要排序的序列中选择一个元素做为基准(也叫关键字)。
2.单趟排序: 通过单趟排序将关键字置于一个已经排序好的位置,并且把序列分成两个子序列。左序列的元素都小于关键字,右序列的元素都大于关键字。
3.重复上面两个过程,再次对子序列进行分割,直到子序列为空或者只有一个元素为止。
而快速排序中最关键的部分就是第二步的单趟排序,我们有以下三种方法来实现:
1.左右哨兵法
可是实现了快速排序的单趟排序有什么用呢?我们要对一整个序列进行排序才行呀!
这个就是快速排序的分治思想了,我们排完一趟就能把一个元素给固定在排好的位置,所以我们就不断分割序列进行排序,直到序列不能分割为止。
代码实现如下:
//交换函数
void Swap(int* px, int* py)
{
int tmp = *px;
*px = *py;
*py = tmp;
}
//左右哨兵法
int Partion1(int* p, int left, int right)
{
//选取最左边为key
int keyi = left;
while (left < right)
{
//先从末端完前面找比key小的元素,找到就停止
while (left < right && p[right] >= p[keyi])
{
right--;
}
//再从前面找比key大的元素,找到就停止
while (left < right && p[left] <= p[keyi])
{
left++;
}
//把找到的元素交换,这样比key小的就在左边,比key大的就在右边
Swap(&p[right], &p[left]);
}
//到这里left==right,left或者right和key交换,这样key就排好序了
Swap(&p[keyi], &p[right]);
//返回已经排好序元素的下标
return right;
}
//快速排序
void QuickSort(int* p, int left, int right)
{
if (left > right)
{
return;
}
//快速排序的单趟排序
int keyi = Partion1(p, left, right);
QuickSort(p, left, keyi - 1);
QuickSort(p, keyi + 1, right);
}
//类似于二叉树的前序遍历,
//已经排好一个元素再对这个元素的左边区间和右边区间排序
2.挖坑法
我们对快速排序的单趟排序有一种方法和前面类似,不够这个比左右哨兵法有意思一点,这个方法就是挖坑法,图片介绍如下:
代码实现如下:
//挖坑法
int Partion2(int* p, int left, int right)
{
//选择最左边为坑pivot
int pivot = left;
int key = p[left];
while (left < right)
{
//右边找小,放到左边的坑中
while (left < right && p[right] >= key)
{
right--;
}
p[pivot] = p[right];
pivot = right; //更新坑的位置变为右边
//左边找大,放到右边的坑中
while (left < right && p[left] <= key)
{
left++;
}
p[pivot] = p[left];
pivot = left;//更新坑的位置变为左边
}
//走到这里,left==right==pivot
//所以我们把前面保存的key放到坑中,坑中的元素就排序好了
//最后再返回排好序元素的下标
p[pivot] = key;
return pivot;
}
void QuickSort(int* p, int left, int right)
{
if (left > right)
{
return;
}
//快速排序的单趟排序
int keyi = Partion2(p, left, right);
QuickSort(p, left, keyi - 1);
QuickSort(p, keyi + 1, right);
}
3.前后指针法
前后指针法和前面两种方法有点不同,它的思想的是把大的元素往后面翻,把小的元素往前面翻,用两个指针prev和cur来维护。这种方法比前面两种方法要好控制范围和边界,我非常推荐掌握种方法,图片分析如下:
代码实现如下:
//交换函数
void Swap(int* px, int* py)
{
int tmp = *px;
*px = *py;
*py = tmp;
}
//前后指针法
int Partion3(int* p, int left, int right)
{
//选择最左边为基准
int keyi = left;
int prev = left;
int cur = left + 1;
while (cur <= right)
{
if (p[cur] < p[keyi])
{
prev++;
Swap(&p[prev], &p[cur]);
}
cur++;
}
//走到这里,cur已经遍历完序列了
//最后把prev位置的元素和key交换即可
//这样prev位置的元素就排好序了,返回prev下标
Swap(&p[keyi], &p[prev]);
return prev;
}
void QuickSort(int* p, int left, int right)
{
if (left > right)
{
return;
}
//快速排序的单趟排序
int keyi = Partion3(p, left, right);
QuickSort(p, left, keyi - 1);
QuickSort(p, keyi + 1, right);
}
二、对快速排序的优化
快速排序有什么缺点呢?难道在什么情况下都是排序最快的吗?
答案当然是否定的,在有序数组种排序反而是最慢的,
因为快速排序是把数列按一个枢纽值分成两部分分别排序,所以效率高。但是若原数据为有序,并且选择的枢纽值为第一个数时,那在分块时会将一个第一个数前面的数(也就是没有)分为一块,将除第一个数的所有数分成了另一块。这样一来,每一次分块都只减少了一个值,而每次分块的时间为O(N),所以总时间为O(N^2)。
并且递归的深度是非常深的,有可能造成栈溢出,所以我们有以下的优化:
1.三数取中法
当我们不知道序列是否有序,我们选择基准不再是最左边或者最右边,而是选择最左边、中间、最右边的三个数的中间数为基准,这样就能在一定程度上优化快速排序,代码实现如下:
//三数取中函数,返回中间数的下标
int GetMidNum(int* p, int left, int right)
{
//int midi=left+(right-left)/2;
int midi = (left + right) / 2;
if (p[left] > p[midi])
{
if (p[right] > p[left])
{
return left;
}
else if (p[midi] > p[right])
{
return midi;
}
else
{
return right;
}
}
else //p[midi]>p[left]
{
if (p[right] > p[midi])
{
return midi;
}
else if (p[left] > p[right])
{
return left;
}
else
{
return right;
}
}
}
//前后指针法
int Partion3(int* p, int left, int right)
{
//三数取中法,取到最左边、中间、最右边三个数的中间数
int midi = GetMidNum(p, left, right);
//然后把中间数调到最左边,这样下面的选择基准不受影响
//仍然可以选择最左边为key
Swap(&p[left], &p[midi]);
//选择最左边为基准
int keyi = left;
int prev = left;
int cur = left + 1;
while (cur <= right)
{
if (p[cur] < p[keyi])
{
prev++;
Swap(&p[prev], &p[cur]);
}
cur++;
}
//走到这里,cur已经遍历完序列了
//最后把prev位置的元素和key交换即可
//这样prev位置的元素就排好序了,返回prev下标
Swap(&p[keyi], &p[prev]);
return prev;
}
2.减少递归的深度
我们知道递归的深度越深,在栈中占用的内存就越多,当递归太深时会造成栈溢出,所以当序列的个数小于一定程度时我们就不再递归,用插入排序来排序,减少递归深度。
代码实现如下:
//插入排序
void InsertSort(int* p, int n)
{
int i = 0;
for (i = 0; i < n - 1; i++)
{
//end为被插入数组的边界
int end = i;
//x为要插入的元素
int x = p[end + 1];
while (end >= 0)
{
//比x大就往后面挪动数据
if (p[end] > x)
{
p[end + 1] = p[end];
end--;
}
//比x小就退出
else
{
break;
}
}
//在这里插入元素x
p[end + 1] = x;
}
}
//对快速排序递归深度的优化
void QuickSort(int* p, int left, int right)
{
if (left > right)
{
return;
}
//减少快速排序递归的深度
//当序列区间小于一定个数后就用插入排序
//就不再递归下去
if (right - left + 1 < 5)
{
InsertSort(p + left, right - left + 1);
}
else
{
int keyi = Partion3(p, left, right);
QuickSort(p, left, keyi - 1);
QuickSort(p, keyi + 1, right);
}
}
三、非递归实现快速排序
可是递归终究还是递归,当排序的数据量超级大的时候,栈还是有可能造成溢出,所以能不能用非递归来实现快速排序呢?
答案是能的,不够有点麻烦,因为要用到数据结构中的栈结构;而C语言中没有栈,我们要自己实现一个栈出来才行,而在我前面有写过栈和队列的实现方式,在这篇博文中有写,点击跳转即可,【数据结构中栈的实现】
所以下面我直接引用栈的接口即可,代码实现如下:
//快速排序非递归
void QuickSortNonR(int* p, int left,int right)
{
if (left >= right)
{
return;
}
ST st;
StackInit(&st); //初始化栈
StackPush(&st, left);//入左边界
StackPush(&st, right);//入右边界
//当栈为空时就不再排序,就已经排序好了
//当栈不为空就继续排序
while (!StackEmpty(&st))
{
//取出左右边界
int end = StackTop(&st);
StackPop(&st);
int begin = StackTop(&st);
StackPop(&st);
//对区间进行排序
int keyi = Partion3(p, begin, end);
//如果区间符号右边界大于左边界就入栈
if (begin < keyi - 1)
{
StackPush(&st, begin);
StackPush(&st, keyi - 1);
}
if (keyi + 1 < end)
{
StackPush(&st, keyi + 1);
StackPush(&st, end);
}
}
}