目录:
三数取中
Hoare法(左右指针法)
挖坑法
前后指针法
用栈实现非递归快排
特性总结
基本思想
快速排序是指在待排序列中选取一个基准(key值),然后把不比该基准大的放在基准的前面,把不比该基准小的放在基准的后面,这样待排序列就被基准划分成了两个序列,对这两个序列再递归进行这样的操作,直到基准两边没有或者只有一个数字,最终就完成了排序。快排主要用到的算法思想是分治思想。常用的快排方法有:Hoare法(左右指针法)、挖坑法、前后指针法。
对下面的数据进行排序:
三数取中:
首先我们需要选定一个 key值,通常情况下是直接用 arr[begin] 或者 arr[end] 作为 key值的。但是在一些特殊情况下还不是很好,因为选取的 key值可能偏大或者偏小。因此可以用三数取中法选择 key值,判断 begin、end、middle的大小,从上图中可以看到 begin的值适中,适合作为 key值。则交换 begin 和 end 所在位置的元素,然后选取 arr[end]作为key值。
选定好 key值以后,就可以开始进行快速排序的步骤了。后面介绍的Hoare法(左右指针法)、挖坑法、前后指针法三种方法都是要先用到三数取中。
过程分析
Hoare法(左右指针法)
我们来对三数取中后的数据进行排序:
- 从 begin开始依次向后找,直到找到比 key大的元素停止;从 end开始依次向前找,直到找到比 key小的元素停止
- 交换 begin 和 end 所在位置的元素
- begin继续依次向后找,直到找到比 key大的元素停止;end继续依次向前找,直到找到比 key小的元素停止
- 继续交换 begin 和 end 所在位置的元素
- begin继续依次向后找,直到找到比 key大的元素停止;end继续依次向前找,直到找到比 key小的元素停止
- 继续交换 begin 和 end 所在位置的元素
- begin继续依次向后找,此时 begin和 end相遇,结束 begin和 end的遍历交换。只有当 begin < end时才可以继续遍历。
- 把begin所在位置的元素和 key值交换
从上图可以发现 key左边的元素都是小于等于6的,key右边的元素都是大于6的,这样我们就完成了第一次预排序。然后再分别对左区间和右区间的元素重新用三数取中法选定 key值,重复上述步骤,就完成了快速排序。
代码实现:
void Swap(int* x, int* y)
{
int tmp = *x;
*x = *y;
*y = tmp;
}
int GetMiddleKey(int* arr, int begin, int end)
{
int middle = (begin + end) / 2;
if (arr[begin] > arr[middle])
{
if (arr[middle] > arr[end])
{
return middle;
}
else if(arr[begin] > arr[end]) //arr[middle] < arr[end]
{
return end;
}
else
{
return begin;
}
}
else //arr[begin] < arr[middle]
{
if (arr[end] > arr[middle])
{
return middle;
}
else if (arr[begin] > arr[end]) //arr[end] < arr[middle]
{
return end;
}
else
{
return begin;
}
}
}
//Hoare法(左右指针法)
int Hoare(int* arr, int begin, int end)
{
//三数取中
int middle_key = GetMiddleKey(arr, begin, end);
Swap(&arr[middle_key], &arr[end]);
//依据key进行排序确定子区间
int key = arr[end];
int keyi = end;
while (begin < end)
{
while (begin < end && arr[begin] <= key)
{
++begin;
}
while (begin < end && arr[end] >= key)
{
--end;
}
Swap(&arr[begin], &arr[end]);
}
Swap(&arr[begin], &arr[keyi]);
return begin;
}
void InQuickSort(int* arr, int begin, int end)
{
if (begin >= end)
{
return;
}
int key = Hoare(arr, begin, end);
InQuickSort(arr, begin, key - 1); //左区间
InQuickSort(arr, key + 1, end); //右区间
}
void QuickSort(int* arr, int n)
{
int begin = 0;
int end = n - 1;
InQuickSort(arr, begin, end);
}
测试用例:
void Test()
{
int arr[] = { 6, 1, 2, 7, 9, 3, 4, 5, 10, 8 };
int length = sizeof(arr) / sizeof(arr[0]);
QuickSort(arr, length);
for (int i = 0; i < length; i++)
{
printf("%d ", arr[i]);
}
printf("\n");
}
挖坑法
同样我们还是来对三数取中后的数据进行排序:
- 选定key值为arr[end],且让 end所在位置为坑
- 从 begin开始依次向后找,直到找到比 key大的元素停止,将 begin所在的位置和坑交换
- 从 end开始依次向前找,直到找到比 key小的元素停止,将 end所在的位置和坑交换
- begin继续依次向后找,直到找到比 key大的元素停止,将 begin所在的位置和坑交换
- end继续依次向前找,直到找到比 key小的元素停止,将 end所在的位置和坑交换
- begin继续依次向后找,直到找到比 key大的元素停止,将 begin所在的位置和坑交换
- end继续依次向前找,直到找到比 key小的元素停止,将 end所在的位置和坑交换
- begin继续依次向后找,此时 begin和 end相遇,结束遍历交换。只有当 begin < end时才可以继续遍历。
- 结束遍历交换后,将 key值填入到坑中
从上图可以发现 key左边的元素都是小于等于6的,key右边的元素都是大于6的,这样我们就完成了第一次预排序。然后再分别对左区间和右区间的元素重新用三数取中法选定 key值和坑,重复上述步骤,就完成了快速排序。
代码实现:
void Swap(int* x, int* y)
{
int tmp = *x;
*x = *y;
*y = tmp;
}
int GetMiddleKey(int* arr, int begin, int end)
{
int middle = (begin + end) / 2;
if (arr[begin] < arr[middle])
{
if (arr[end] > arr[middle])
{
return middle;
}
else if (arr[begin] > arr[end]) //arr[end] < arr[middle]
{
return begin;
}
else
{
return end;
}
}
else //arr[begin] > arr[middle]
{
if (arr[middle] > arr[end])
{
return middle;
}
else if (arr[begin] > arr[end])
{
return end;
}
else
{
return begin;
}
}
}
int DigPitMethod(int* arr, int begin, int end)
{
int key_middle = GetMiddleKey(arr, begin, end);
Swap(&arr[key_middle], &arr[end]);
int key = arr[end];
while (begin < end)
{
while (begin < end && arr[begin] <= key)
{
++begin;
}
arr[end] = arr[begin];
while (begin < end && arr[end] >= key)
{
--end;
}
arr[begin] = arr[end];
}
arr[begin] = key;
return begin;
}
void InQuickSort(int* arr, int begin, int end)
{
if (begin >= end)
{
return;
}
int key = DigPitMethod(arr, begin, end);
InQuickSort(arr, begin, key - 1);
InQuickSort(arr, key + 1, end);
}
void QuickSort(int* arr, int n)
{
int begin = 0;
int end = n - 1;
InQuickSort(arr, begin, end);
}
测试用例:
void Test()
{
int arr[] = { 6, 1, 2, 7, 9, 3, 4, 5, 10, 8 };
int length = sizeof(arr) / sizeof(arr[0]);
QuickSort(arr, length);
for (int i = 0; i < length; i++)
{
printf("%d ", arr[i]);
}
printf("\n");
}
前后指针法
同样我们还是来对三数取中后的数据进行排序:
整体思路:cur初始位置是begin,prev初始位置是 begin-1。cur先依次向后遍历找比 key小的元素,当 cur < key时,++prev,然后交换 prev 和 cur所在位置的元素。重复前面的步骤,直到 cur遇到 key时终止。
- cur开始找比 key小的元素,找到后 ++prev,然后交换 prev 和 cur所在位置的元素
- cur继续找比 key小的元素,找到后 ++prev,然后交换 prev 和 cur所在位置的元素
- cur继续找比 key小的元素,找到后 ++prev,然后交换 prev 和 cur所在位置的元素
- cur继续找比 key小的元素,找到后 ++prev,然后交换 prev 和 cur所在位置的元素
- cur继续找比 key小的元素,找到后 ++prev,然后交换 prev 和 cur所在位置的元素
- cur继续找比 key小的元素,此时 cur和 key相遇,然后 ++prev,交换 prev和 key所在位置的元素
通过对上图观察可以发现左区间都是小于等于6的元素,右区间都是大于6的元素。然后再对左区间和右区间分别重复上述步骤排序。
代码实现:
void Swap(int* x, int* y)
{
int tmp = *x;
*x = *y;
*y = tmp;
}
int GetMiddleKey(int* arr, int begin, int end)
{
int middle = (begin + end) / 2;
if (arr[begin] < arr[middle])
{
if (arr[end] > arr[middle])
{
return middle;
}
else if (arr[begin] > arr[end]) //arr[end] < arr[middle]
{
return begin;
}
else
{
return end;
}
}
else //arr[begin] > arr[middle]
{
if (arr[middle] > arr[end])
{
return middle;
}
else if (arr[begin] > arr[end])
{
return end;
}
else
{
return begin;
}
}
}
//前后指针法
int PrevCurMethod(int* arr, int begin, int end)
{
//三数取中
int key_middle = GetMiddleKey(arr, begin, end);
Swap(&arr[key_middle], &arr[end]);
int key = arr[end];
int cur = begin;
int prev = begin - 1;
while (cur < end)
{
if (arr[cur] < key && ++prev != cur)
{
Swap(&arr[prev], &arr[cur]);
}
++cur;
}
++prev;
Swap(&arr[prev], &arr[end]);
return prev;
}
void InQuickSort(int* arr, int begin, int end)
{
if (begin >= end)
{
return;
}
int key = PrevCurMethod(arr, begin, end);
InQuickSort(arr, begin, key - 1);
InQuickSort(arr, key + 1, end);
}
void QuickSort(int* arr, int n)
{
int begin = 0;
int end = n - 1;
InQuickSort(arr, begin, end);
}
测试用例:
void Test()
{
int arr[] = { 6, 1, 2, 7, 9, 3, 4, 5, 10, 8 };
int length = sizeof(arr) / sizeof(arr[0]);
QuickSort(arr, length);
for (int i = 0; i < length; i++)
{
printf("%d ", arr[i]);
}
printf("\n");
}
用栈实现非递归快排(C++实现)
#include <iostream>
#include <stack>
using namespace std;
void Swap(int* x, int* y)
{
int tmp = *x;
*x = *y;
*y = tmp;
}
int GetMiddleKey(int* arr, int begin, int end)
{
int middle = (begin + end) / 2;
if (arr[begin] < arr[middle])
{
if (arr[end] > arr[middle])
{
return middle;
}
else if (arr[begin] > arr[end]) //arr[end] < arr[middle]
{
return begin;
}
else
{
return end;
}
}
else //arr[begin] > arr[middle]
{
if (arr[middle] > arr[end])
{
return middle;
}
else if (arr[begin] > arr[end])
{
return end;
}
else
{
return begin;
}
}
}
//前后指针法
int PrevCurMethod(int* arr, int begin, int end)
{
//三数取中
int key_middle = GetMiddleKey(arr, begin, end);
Swap(&arr[key_middle], &arr[end]);
int key = arr[end];
int cur = begin;
int prev = begin - 1;
while (cur < end)
{
if (arr[cur] < key && ++prev != cur)
{
Swap(&arr[prev], &arr[cur]);
}
++cur;
}
++prev;
Swap(&arr[prev], &arr[end]);
return prev;
}
//用栈实现非递归快排
void QuickSortNonR(int* arr, int begin, int end)
{
stack<int> st;
st.push(begin);
st.push(end);
while (!st.empty())
{
int right = st.top();
st.pop();
int left = st.top();
st.pop();
int key = PrevCurMethod(arr, left, right);
if (left < key - 1)
{
st.push(left);
st.push(key - 1);
}
if (key + 1 < right)
{
st.push(key + 1);
st.push(right);
}
}
}
void QuickSort(int* arr, int n)
{
int begin = 0;
int end = n - 1;
QuickSortNonR(arr, begin, end);
}
测试用例:
void Test()
{
int arr[] = { 6, 1, 2, 7, 9, 3, 4, 5, 10, 8 };
int length = sizeof(arr) / sizeof(arr[0]);
QuickSort(arr, length);
for (int i = 0; i < length; i++)
{
printf("%d ", arr[i]);
}
printf("\n");
}
特性总结
- 快速排序整体的综合性能和使用场景都是比较好的,所以才敢叫快速排序
- 时间复杂度:O(N*logN)
- 空间复杂度:O(logN),真正消耗空间的是递归调用,因为每次递归就要保持一些数据
- 稳定性:不稳定