一、快速排序简述
1、快速排序是对冒泡排序的一种改进;
2、快速排序是由C.A.RHoare于1962年提出的;
3、快速排序的基本思想
(1)先找到一个基准值;
(2)将所有比基准值小的数放在基准值的左边区域,所有比基准值大的数放在基准值的右边区域,等于基准值的数可放在左右任一区域;
(3)对左右区域按照上述方法分别进行排序,整个快速排序可以递归进行,直到数据有序为止。
二、三种快排算法
1、左右指针法
(1)算法描
第一步:定义两个指针left和right分别指向数组的第一个 元素和最后一个元素,并且定义最后一个元素为基准值K;
第二步:left从左向右走,寻找比K大的数A
right从右向左走,寻找比K小的数B
当A和B不相等时将A、B进行交换
然后left继续向右走,right继续向左走
第三步:当left等于right时,将left指向的数赋给K
再将left返回
(2)举例说明
(3)代码实现
//左右指针法
int PartSort1(int* a, int left, int right)
{
assert(a);
int key = a[right];
while (left < right)
{
//left指针找比key大的数
while (left < right && a[left] <= key)
{
++left;
}
//right指针找比key小的数
while (left<right && a[right]>=key)
{
--right;
}
//当left指向数跟right指向的数不相同时进行交换
if (a[left] != a[right])
{
swap(a[left], a[right]);
}
}
//当left等于right时,将left指向的值和标准值进行交换
swap(a[left], key);
return left;
}
2、挖坑法
(1)算法描述
第一步:定义两个指针left和right分别指向待排序数组的第一个元素和最后一个元素,并将待排序数组中的最后一个元素key所在的位置作为“坑”;
第二步:left指针向右走,寻找比key大的值,找到之后,将该数据填入坑中,并将此时left所指的位置作为坑;
第三步:right向左走,寻找比key小的值,找到之后将该数据填入坑中,并将此时right所指向的位置作为坑;
第四步:当left等于right时,将数据key填入坑中,至此第一趟排序结束;
第五步:对左右区间进行递归排序,直到待排序数组有序为止。
(2)举例说明
(3)代码实现
//挖坑法
int PartSort2(int* a, int left, int right)
{
assert(a);
int key = a[right];
int pit = right;
while (left < right)
{
//指针left向右找比key大的数
while (left < right && a[left] <= key)
{
++left;
}
//找到之后填坑,并制造新坑
a[pit] = a[left];
pit = left;
//指针right向左找比key小的数
while (left < right && a[right] >=key)
{
--right;
}
//找到之后填坑,并制造新坑
a[pit] = a[right];
pit = right;
}
a[pit] = key;
return left;
}
3、前后指针法
(1)算法描述
第一步:定义两个指针prev和cur分别指向数组的最左端和最左端的前一个位置,并定义数组最右端的值为key值;
第二步:cur向右走,寻找小于key的值,找到了之后与++prev位置上的值进行交换;如果没有找到小于key的值,则一直向右走,直到cur=right时,将a[cur]与a[++prev]进行交换;
第三步:递归进行,直到数组有序为止。
(2)举例说明
(3)代码实现
//前后指针法
int PartSort3(int* a, int left, int right)
{
int key = a[right];
int cur = left;
int prev = cur - 1;
while (cur != right)
{
if (a[cur] < key && a[++prev] != a[cur])
{
swap(a[prev], a[cur]);
}
++cur;
}
swap(a[++prev], a[cur]);
return prev;
}
三、快速排序的性能分析
1、时间复杂度
最优情况:如果对8个无序的数进行快排,那么需要递归3次,递归的每一次都要处理n个数,因此此时递归的时间复杂度为O(n*logn);
最差情况:如果对8个顺序或者逆序的数进行快排,会得到一个只比上次划分少一个的子序列,因此需要递归n-1次,所以此时的时间复杂度为O(n^2);
综上所述,平均情况下,快排的时间复杂度为O(n*logn)
2、空间复杂度
最好情况:需要递归logn次,因此空间复杂度为O(logn);
最坏情况:需要递归n-1次,因此时间复杂度为O(n);
综上所述,平均情况下,快排的时间复杂度为0(logn)
3、稳定性
由于在快排的过程中,关键字的比较和交换是跳跃进行的,因此快排不稳定。
四、快排的优化
1、三数取中法
(1)问题引入
经过上文的描述,我们知道快速排序的效率还是比较高的,但是当我们选择的基准数接近最大or最小,或者待排序数列是顺序或者逆序的时候,效率就比较低了,为了解决这一问题,我们在选择基准数的时候可以采取三数取中法。
(2)算法描述
选取该组数中的第一个数、中间一个数、和最后一个数,选择这三个数中居中的一个数作为基准值。
(3)代码实现
//三数取中
int GetMidIndex(int *a, int left, int right)
{
int mid = left -((left - right) >> 1);
if (a[left] < a[mid])
{
if (a[mid] < a[right])
{
return mid;
}
else if (a[left] < a[right])
{
return right;
}
else
{
return left;
}
}
else //(a[left] >= a[mid])
{
if (a[mid] > a[right])
{
return mid;
}
else if (a[right] < a[left])
{
return right;
}
else
{
return left;
}
}
}
2、小区间优化
(1)问题引入
当待排序元素较少时,直接插入排序比快排的效率要高,因此在这种情况下采用直接插入排序。
(2)直接插入排序图示(以升序为例)
(3)代码实现
void InsertSort(int *a, size_t n)
{
assert(a);
for (int i = 1; i < n; ++i)
{
int tmp = a[i];
int j = i - 1;
for (; j >= 0; --j)
{
if (a[j]>tmp)
{
a[j + 1] = a[j];
}
else
break;
}
a[j + 1] = tmp;
}
3、非递归实现快排
(1)问题引入
当待排序的数据较多时递归的次数也随增多,会不停的创建栈帧,因此我们借助栈用非递归来实现快排。
(2)代码实现
//非递归实现快排
void QuickSortNOR(int *a, int left, int right)
{
assert(a);
stack<int> s;
s.push(right);
s.push(left);
while (!s.empty())
{
int start = s.top();
s.pop();
int end = s.top();
s.pop();
int div = PartSort3(a, start, end);
if (start < div - 1)
{
s.push(div - 1);
s.push(left);
}
if (end > div + 1)
{
s.push(end);
s.push(div + 1);
}
}
}
下面给出快排的完整代码
#include <algorithm>
#include <vector>
#include <stack>
#include <assert.h>
#include <iostream>
using namespace std;
//三数取中
int GetMidIndex(int *a, int left, int right)
{
int mid = left -((left - right) >> 1);
if (a[left] < a[mid])
{
if (a[mid] < a[right])
{
return mid;
}
else if (a[left] < a[right])
{
return right;
}
else
{
return left;
}
}
else //(a[left] >= a[mid])
{
if (a[mid] > a[right])
{
return mid;
}
else if (a[right] < a[left])
{
return right;
}
else
{
return left;
}
}
}
//左右指针法
int PartSort1(int* a, int left, int right)
{
assert(a);
int key = GetMidIndex(a, left, right);
while (left < right)
{
//left指针找比key大的数
while (left < right && a[left] <a[key])
{
++left;
}
//right指针找比key小的数
while (left<right && a[right]>a[key])
{
--right;
}
//当left指向数跟right指向的数不相同时进行交换
if (a[left] != a[right])
{
swap(a[left], a[right]);
}
}
//当left等于right时,将left指向的值和标准值进行交换
swap(a[left], a[key]);
return left;
}
//挖坑法
int PartSort2(int* a, int left, int right)
{
assert(a);
int key = a[right];
int pit = right;
while (left < right)
{
//指针left向右找比key大的数
while (left < right && a[left] <= key)
{
++left;
}
//找到之后填坑,并制造新坑
a[pit] = a[left];
pit = left;
//指针right向左找比key小的数
while (left < right && a[right] >=key)
{
--right;
}
//找到之后填坑,并制造新坑
a[pit] = a[right];
pit = right;
}
a[pit] = key;
return left;
}
//前后指针法
int PartSort3(int* a, int left, int right)
{
int key = a[right];
int cur = left;
int prev = cur - 1;
while (cur != right)
{
if (a[cur] < key && a[++prev] != a[cur])
{
swap(a[prev], a[cur]);
}
++cur;
}
swap(a[++prev], a[cur]);
return prev;
}
//递归实现快排
void QuickSort(int* a, int left, int right)
{
assert(a);
if (left < right)
{
int div = PartSort3(a, left, right);
QuickSort(a, left, div - 1);
QuickSort(a, div+1, right);
}
}
//非递归实现快排
void QuickSortNOR(int *a, int left, int right)
{
assert(a);
stack<int> s;
s.push(right);
s.push(left);
while (!s.empty())
{
int start = s.top();
s.pop();
int end = s.top();
s.pop();
int div = PartSort3(a, start, end);
if (start < div - 1)
{
s.push(div - 1);
s.push(left);
}
if (end > div + 1)
{
s.push(end);
s.push(div + 1);
}
}
}
测试代码
#include "Sort.h"
#include <assert.h>
#include <iostream>
using namespace std;
int main()
{
int a[] = { 7, 9, 8, 4, 6, 1, 2, 3,7,10,11,5,20,21,27,13,19,16,12 };
size_t size = sizeof(a) / sizeof(a[0]);
QuickSortNOR(a, 0, size - 1);
PrintArr(a, size);
return 0;
}
测试结果