快速排序是 Hoare 于1962年提出的一种二叉树结构的交换排序方法,其基本思想为:任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止。
将区间按照基准值划分为左右两半部分的常见方式有以下三种:
int BaseNumber(int array[], int begin, int end) {
int mid = begin + ((end - begin) >> 1); // 采用位运算效率高
if (array[begin] > array[mid]) { // 逻辑判断取出中位数的下标,当然排序及其它方法均可
if (array[begin] > array[end]) {
if (array[mid] > array[end]) {
return mid;
}
else {
return end;
}
}
else {
return begin;
}
}
else {
if (array[mid] > array[end]) {
if (array[begin] > array[end]) {
return begin;
}
else {
return end;
}
}
else {
return mid;
}
}
}
三次异或进行两整数交换
void Swap(int *x, int *y) {
//int tmp = *x;
//*x = *y;
//*y = tmp;
*x = ((*x) ^ (*y));
*y = ((*x) ^ (*y));
*x = ((*x) ^ (*y));
}
hoare法要点:
key
,设置两个指针 begin
、end
begin
从前往后移动遇到比基准 key
大的停止end
从后往前移动遇到比基准 key
小的停止begin
和 end
所对应的数据交换begin==end
int QuickSort1(int array[], int begin, int end) {
// 找基准
// 由于每次规划取到最大值或最小值的概率都非常高,
// 这样容易使树变成单支树,所以采用三数取中法来降低取到最值的概率
int index = BaseNumber(array, begin, end); // 基准值在数组中的下标
if (index != end) {
Swap(&array[index], &array[end]); // 将基准值与最后一个数字进行值交换
}
// 基准值
int key = array[end];
// 基准值的下标
int k = end;
// 两个指针、begin从0开始,end从size-1开始
while (begin != end) {
// begin向后移动,找比基准值大的元素,且begin不能大于end
// 如果array[begin]比key小,则++begin
while (array[begin] <= key && (begin < end)) {
++begin;
}
// end向前移动,找比基准值key小的元素,且end不能小于begin
// 如果array[end]比key大,则--end
while (array[end] >= key && (begin < end)) {
--end;
}
// 如果下标begin和下标end不相等,则交换所对应的数组元素值
if (begin != end) {
Swap(&array[begin], &array[end]);
}
}
// 如果begin的最终位置就是基准的位置则不用交换
if (begin != k) {
// 将基准值挪到相应位置上
Swap(&array[begin], &array[k]);
}
return begin;
}
采用hoare法
讲解时的图片作为测试用例:
测试数据:int array[] = { 0,5,4,9,3,6,8,7,1,2 };
其中选取的三数为0,3,2
,故基准值为 2,划分后的区间应为0,1,2,9,3,6,8,7,5,4
返回begin = 2
,且基准值左边全部小于基准值,右边全部大于基准值。
挖坑法要点:
key
begin
、end
begin
从左边开始找比关键字大的元素将其入坑,begin
所在位置变为坑end
从右边开始找比关键字小的元素将其入坑,end
所在位置变为坑begin
和 end
所对应的数据交换begin==end
将标记的第一个元素入坑int QuickSort2(int array[], int begin, int end) {
// 依旧三数取中法确定基准
int index = BaseNumber(array, begin, end);
if (index != end) {
Swap(&array[index], &array[end]);
}
// 第一个坑
int key = array[end];
int k = end;
while (begin != end) {
// begin从左边开始找比关键字大的元素将其入坑
// begin所在位置变为坑
while (array[begin] <= key && begin < end) {
++begin;
}
if (begin != end) {
array[end] = array[begin];
--end;
}
// end从右开始找比关键字小的元素将其入begin坑
while (array[end] >= key && begin < end) {
--end;
}
if (begin != end) {
array[begin] = array[end];
++begin;
}
}
if (begin != k) {
array[begin] = key;
}
return begin;
}
采用挖坑法
讲解时的图片作为测试用例:
测试数据:int array[] = { 0,5,4,9,3,6,8,7,1,2 };
其中选取的三数为0,3,2
,故基准值为 2,划分后的区间应为0,1,2,9,3,6,8,7,4,5
返回begin = 2
,且基准值左边全部小于基准值,右边全部大于基准值。
仔细观察可以发现,得到的区间划分与 hoare
不一样了,提示:最后一位数字~
前后指针法:
key
cur
在待排序序列的最左侧perv=cur-1
arr[cur] ,则 prev
和 cur
一起向后移动
arr[cur]>key
,则 prev
停止,cur
向后移动,直至遇到 arr[cur],cur
停止
cur
与 prev + 1
对应的元素交换,++cur
cur
超过待排序序列的长度,返回 prev
prev
左边就是比基准值小的序列,右边就是比基准值大的序列
int QuickSort3(int array[], int begin, int end) {
int index = BaseNumber(array, begin, end);
int cur = begin, prev = begin - 1;
if (index != end) {
Swap(&array[index], &array[end]);
}
int key = array[end];
// cur不能超过序列长度
while (cur <= end) {
if (array[cur] <= key && ++prev != cur) {
Swap(&array[cur], &array[prev]);
}
++cur;
}
return prev;
}
采用前后指针法
讲解时的图片作为测试用例:
测试数据:int array[] = { 0,5,4,9,3,6,8,7,1,2 };
其中选取的三数为0,3,2
,故基准值为 2,划分后的区间应为0,1,2,9,3,6,8,7,5,4
返回begin = 2
,且基准值左边全部小于基准值,右边全部大于基准值。
在此可见与 挖坑法 产生的区间方法又有一点点区别,提示:最后一位数字。
若待排序的序列较长,使用递归的方法一层一层调用极易造成栈溢出,由于当待排序的序列长度逐渐减小时,元素已经接近有序,使用插入排序会更加快捷,算是快速排序的一个优化点。
// 快速排序(递增)
void QuickSort(int array[], int left, int right) {
/*
// 由于快速排序是递归调用,容易产生栈溢出
// 但是快速排序排到最后元素也接近有序,则采用插入排序
if (right - left < 2) {
InsertSort(array + left, right - left);
}
*/
// 数据较小时,直接判断即可
if (left == right) {
return; // 区间内只有一个数
}
if (left > right) {
return; // 区间内没有数
}
// 基准值是array[right]
int pos;
// 仅修改QuickSort1,1,2,3即可完成测试
pos = QuickSort1(array, left, right - 1);
QuickSort(array, 0, pos); // 快速排序基准值左侧
QuickSort(array, pos + 1, right); //快速排序基准值右侧
}
测试数据:int array[] = { 3, 9, 1, 4, 2, 8, 2, 7, 5, 3, 6, 11, 9, 4, 2, 5, 0, 6 };
快速排序的递归实现在之前都已经叙述清楚了,但若是待排序的数量非常大且杂乱无章,每层循环都使用递归调用,会很容易造成栈溢出,所以可以将快速排序设计为非递归实现来避免这个问题。
总结一下递归实现快速排序算法重点:
若想把递归的快速排序改成非递归的循环最重要的是将基准值右侧区间的下标记住,排序完左边序列之后排序右边序列。
步骤如下:
size
入栈size
后进栈,所以 size
先出栈。定义一个 right
接收栈顶元素,出栈操作、定义一个 left
接收栈顶元素,出栈操作。这个确实不好叙述,但是只要对着下面的代码,拿个纸笔一画清楚无比,确实是鬼才思想~~
在这为了复习下数据结构,还模拟实现了一个栈~~
// 快排非递归实现
typedef struct Stack {
int *data;
int size;
}stack;
void InitStack(stack *s) {
int *data = (int*)malloc(20 * sizeof(int));
if (data == NULL) {
assert(0);
return;
}
s->data = data;
s->size = 0;
}
void PushStack(stack *s, int d) {
assert(s);
if (s->size > 20) {
return;
}
else {
s->data[s->size++] = d;
}
}
void PopStack(stack *s) {
assert(s);
if (s->size == 0) {
return;
}
else {
s->size--;
}
}
int TopStack(stack *s) {
assert(s);
return s->data[s->size - 1];
}
int EmptyStack(stack *s) {
assert(s);
return s->size == 0;
}
void QuickSortStack(int array[], int size) {
stack s;
int pos, left = 0, right = 0;
InitStack(&s);
PushStack(&s, 0);
PushStack(&s, size - 1);
while (!EmptyStack(&s)) {
right = TopStack(&s);
PopStack(&s);
left = TopStack(&s);
PopStack(&s);
if (left >= right) {
continue;
}
else {
// 注意在此的 right 传参
pos = QuickSort1(array, left, right);
//先快排基准左侧,则先将后侧的下标入栈
if ((right - left) > pos + 1) {
PushStack(&s, pos + 1);
PushStack(&s, right - left);
}
if (pos > 0) {
PushStack(&s, 0);
PushStack(&s, pos - 1);
}
}
}
}
Somehow, everything just work. 你不知道他怎么想的,但他就是想到了
测试数据:int array[] = { 3, 9, 1, 4, 2, 8, 2, 7, 5, 3, 6, 11, 9, 4, 2, 5, 0, 6 };
上述实现已经很是完美了,在找以往资料的时候发现以前写的一个简单的快排,没考虑三数取中,用的 stl
中的stack
实现,这个写起来快,也算是个精炼版吧:
#include
void Swap(int *a, int *b) {
int t = *a; *a = *b; *b = t;
}
// 快速排序
// 不能保证 left 一定是 0
int Partition_1(int array[], int left, int right) {
int begin = left; // 不要写成 begin = 0;
int end = right; // end 不能是 right - 1,反例 { 1,2, 3, 4 }
while (begin < end) {
// 基准值在右边,先走左边
// 否则反例 { 1, 7, 8, 4 }
// array[begin] 和 array[right] 比较必须有 ==
// 反例 { 1, 1, 1 }
while (begin < end && array[begin] <= array[right]) {
begin++;
}
// 意味着 array[begin] > array[right]
while (begin < end && array[end] >= array[right]) {
end--;
}
// 意味着 array[end] < array[right]
Swap(array + begin, array + end);
}
// 意味着区间被分成 3 份,分别是 { 小 , 大, 基准值 }
Swap(array + begin, array + right);
// 返回当前基准值所在下标
return begin;
}
int Partition_2(int array[], int left, int right) {
int begin = left;
int end = right;
int pivot = array[right];
while (begin < end) {
while (begin < end && array[begin] <= pivot) {
begin++;
}
array[end] = array[begin];
while (begin < end && array[end] >= pivot) {
end--;
}
array[begin] = array[end];
}
array[begin] = pivot;
return begin;
}
int Partition_3(int array[], int left, int right) {
int d = left;
for (int i = left; i < right; i++) {
if (array[i] < array[right]) {
Swap(array + i, array + d);
d++;
}
}
Swap(array + d, array + right);
return d;
}
// array[left, right]
void __QuickSort(int array[], int left, int right) {
if (left == right) {
// 区间内只有一个数
return;
}
if (left > right) {
// 区间内没有数
return;
}
// 基准值是 array[right]
int div; // 用来保存最终基准值所在的下标
div = Partition_1(array, left, right); // 遍历 array[left, right]
// 把小的放左,大的放右
// 返回最后基准值所在的下标
// 区间被分成
// [left, div - 1] 比基准值小 *
// [div, div] 基准值 已经在最终位置
// [div + 1, right] 比基准值大 *
__QuickSort(array, left, div - 1);
__QuickSort(array, div + 1, right);
}
#include // 栈的头文件
void QuickSortNor(int array[], int size) {
std::stack<int> stack;
stack.push(size - 1); // right
stack.push(0); // left
while (!stack.empty()) {
int left = stack.top(); stack.pop();
int right = stack.top(); stack.pop();
if (left >= right) {
continue;
}
else {
int d = Partition_2(array, left, right);
// [d + 1, right]
stack.push(right);
stack.push(d + 1);
// [left, d - 1]
stack.push(d - 1);
stack.push(left);
}
}
}
void QuickSort(int array[], int size) {
__QuickSort(array, 0, size - 1);
}
上面程序写这篇博文的时候测过了,通过了,有些注释还是值得细品的~。
快速排序整体的综合性能和使用场景都是比较好的,所以才敢叫快速排序
时间复杂度
空间复杂度
排序稳定性