快速排序:
主要思路:
选取一个基准数,把小于基准数的元素调整到基准数的左边,大于基准数的元素调整到基准数的右边。然后对基准数左边和右边的序列重复上述操作,直到整个序列变成有序的。
快排分割函数:
快速排序的关键过程就是在找到基准数位置时进行的这一系列元素的移动。可以把这个过程单独构成一个函数。
-
对于一个大小为size的数组arr,令L = 0,R = size - 1,选取基准数 val = arr[L]
-
从R开始往前找第一个 < val 的值,放在L的地方,L++
-
从L开始往后找第一个 > val 的值,放在R的位置,R--
-
重复上述过程,直到L = R,将val放在L和R指向的位置
对于其左侧右侧的两段序列分别再次使用快排分割函数,直到无法再次分割。
用一个数组来模拟这个过程:
18 37 82 97 4 35 83 42 11 52
L R
先使基准数等于左起第一个元素val = 18。从右起向左遍历数组,直到找到小于val的数字,覆盖掉L标记的元素。
11 37 82 97 4 35 83 42 11 52
L R
这时再从左向右找到第一个大于val的元素,用这个数字将R标记的元素覆盖。
11 37 82 97 4 35 83 42 37 52
L R
重复这个过程,直到L = R。
11 4 82 97 82 35 83 42 37 52
L R
11 4 82 97 82 35 83 42 37 52
LR
L与R标记同一个元素时,这个位置的左侧都是比val小的值,右侧都是比val大的值,在有序数组中这里就是val的位置。将val的值赋给这个位置的元素。
11 4 18 97 82 35 83 42 37 52
LR
之后再对两侧分别进行上面的分割就可以得到一个有序数组。
而对于数组整体,快排可以得到下面的树形递归结构:
11 4 ++18++ 97 82 35 83 42 37 52
/ \
4 ++11++ 52 82 35 83 42 37 ++97++
/ /
4 37 42 35 ++52++ 83 82
/ \
35 ++37++ 42 82 ++83++
/ \ /
35 42 82
最后将树按照从左到右的顺序拿出来:
4 11 18 35 37 42 52 82 83 97
参照上面的过程编写代码:
可以看一下下面的代码分析,对每个函数进行了详细介绍。
cpp
#include <iostream>
#include <random>
#include <ctime>
//用递归实现快速排序
//快排分割函数
int Partation(int arr[], int l, int r)
{
int val = arr[l];
while (l < r)
{
//需要从右侧找到第一个比val小的数
while (l < r && arr[r] >= val)
r--;
//l == r时不执行后面的l++和赋值操作
if (l < r)
{
//放到左边
arr[l] = arr[r];
l++;
}
//然后从左往右找到第一个比val大的数
while (l < r && arr[l] <= val)
l++;
//确保标记不会越界,l在r左侧
if (l < r)
{
//放到右边
arr[r] = arr[l];
r--;
}
}
//退出时l = r
arr[l] = val;
return l;
}
//快排递归函数
void QuickSort(int arr[], int l, int r) //重载
{
if (l >= r)
return;
int pos = Partation(arr, l, r);
QuickSort(arr, l, pos - 1);
QuickSort(arr, pos + 1, r);
}
//快排普通接口
void QuickSort(int arr[], int size)
{
return QuickSort(arr, 0, size - 1);
}
int main()
{
int arr[10];
srand(time(NULL));
for (int i = 0; i < 10; i++)
{
arr[i] = rand() % 100;
std::cout << arr[i] << " ";
}
QuickSort(arr, 10);
//换行输出排序后数组
std::cout << std::endl << "Array Sorted: " << std::endl;
for (int i = 0; i < 10; i++)
{
std::cout << arr[i] << " ";
}
}
代码分析:
这里还是用随机数来生成数组,并且将数组以指针形式传给函数,所以需要同时传数组大小。
cpp
int main()
{
int arr[10];
srand(time(NULL));
for (int i = 0; i < 10; i++)
{
arr[i] = rand() % 100;
std::cout << arr[i] << " ";
}
QuickSort(arr, 10);
//换行输出排序后数组
std::cout << std::endl << "Array Sorted: " << std::endl;
for (int i = 0; i < 10; i++)
{
std::cout << arr[i] << " ";
}
}
由于使用递归方法二分处理数组,每次递归都需要给出处理数组的范围,这里重载QuickSort方法,在递归函数中给出快排范围的左下标和右下标。
cpp
//快排普通接口
void QuickSort(int arr[], int size)
{
return QuickSort(arr, 0, size - 1);
}
在递归当中,重要的是返回的位置,当L == R时说明快排函数只处理一个数值,可以直接返回。
可以先将快排分割函数抽象成一个接口,只得到分割点的位置,之后再去完善内部。从分割点左右再分别进行下一层递归。
cpp
//快排递归函数
void QuickSort(int arr[], int l, int r) //重载
{
if (l >= r)
return;
int pos = Partation(arr, l, r);
QuickSort(arr, l, pos - 1);
QuickSort(arr, pos + 1, r);
}
快排分割函数可以参考一开始的描述来编写。先将左侧第一个数作为基准数保存。在L < R的时候需要重复从左侧右侧找小于和大于基准数的数字,所以使用while循环。循环退出时,应该令L == R,此时将val赋值给arr[l],并返回L给出分割点。
再看while内部的情况。先从右侧找到第一个比val小的数,所以使用while (l < r && arr[r] >= val)。再从左侧找到第一个比val大的数,使用while (l < r && arr [l] <= val)。循环体中分别R--; L++,在找到各自的目标值后,如果l < r 仍成立,则将小的数放到左边或将大的数放到右边,这个if (l < r)可以防止l == r时执行后面的l++; r--;导致越界,以及arr[l] = arr[r]自己赋值给自己的重复操作。
至于为什么在这里while条件还要加上l < r,试想数组2 6 8 9,l = 0,r = 3,循环会执行到r = -1才结束,此时虽然能正常处理后续的操作,但是有数组越界的风险;而对于第二个l++的循环,循环执行到l == r时,如果此处的数字依然满足<= val,l就会继续减到大于r,则退出循环后无法将基准数赋给正确的位置,也无法返回正确的分割点。
cpp
int Partation(int arr[], int l, int r)
{
int val = arr[l];
while (l < r)
{
//需要从右侧找到第一个比val小的数
while (l < r && arr[r] >= val)
r--;
//l == r时不执行后面的l++和赋值操作
if (l < r)
{
//放到左边
arr[l] = arr[r];
l++;
}
//然后从左往右找到第一个比val大的数
while (l < r && arr[l] <= val)
l++;
//确保标记不会越界,l在r左侧
if (l < r)
{
//放到右边
arr[r] = arr[l];
r--;
}
}
//退出时l = r
arr[l] = val;
return l;
}
性能指标:
在理想情况下,快排的递归层数就是上面的快排二叉树的层数,树的层高为log n。
每层都需要基准数与其他所有元素进行比较调整,这个过程总的时间复杂度为。
快排平均&最好时间复杂度 :
由于快排使用递归实现,每次调用函数都需要开辟栈帧,快排递归深度越深,占用栈内存越大,递归层数为log n。
快排平均&最好空间复杂度 :
最坏情况下,比如已经排序好的数组,递归层数为n。
++1++ 2 3 4
\
++2++ 3 4
\
++3++ 4
\
4
最坏时间复杂度 :
最坏空间复杂度 :
在来回左右移动的过程中很容易使相等元素位置发生变化。
比如5 7① 7② 3 2这个数列:第一次排序:2 7① 7② 3 2,第二次排序:2 7 7② 3 7①,此时第一个7已经到了最后一位,相对位置发生了变动。第三次排序:2 3 7② 3 7①,第四次排序:2 3 7 7② 7①。最后将基准数放在中间:2 3 5 7② 7①,两个7的顺序发生了改变。
稳定性:不稳定。
快速排序优化:
优化一:
对于有序的数组,快速排序由于递归结构,效率反而很低。而当数据趋于有序时,插入排序的效率最高。我们可以用插入排序来优化快速排序。
快排随着递归层数的增加,序列越来越短且越来越有序,可以在序列长度减小到一定值后采用插入排序。
cpp
//快排递归函数
void QuickSort(int arr[], int l, int r) //重载
{
if (l >= r)
return;
//优化一:当序列元素个数小到指定数量,采用插入排序
if (r - l + 1 <= 50)
{
InsertSort(arr, l, r);
return;
}
int pos = Partation(arr, l, r);
QuickSort(arr, l, pos - 1);
QuickSort(arr, pos + 1, r);
}
优化二:
为了让快排的递归逻辑趋近于平衡的二叉树,从而达到log n的递归深度,采用 "三数取中" 法,找合适的基准数。
"三数取中" 是从序列第一个数L,最后一个数R,以及中间的数MID = (L + R) / 2这三个数字中取中间一个数为基准数。选好基准数后可以把基准数跟L位置的数字进行交换。
cpp
//快排分割函数
int Partation(int arr[], int l, int r)
{
//三数取中法选择基准数,令基准数与arr[l]交换
int mid = (l + r) / 2;
int val = 0;
if ((arr[l] <= arr[r] && arr[l] >= arr[mid]) || (arr[l] >= arr[r] && arr[l] <= arr[mid]))
val = arr[l];
else if ((arr[mid] <= arr[r] && arr[mid] >= arr[l]) || (arr[mid] >= arr[r] && arr[mid] <= arr[l]))
{
val = arr[mid];
arr[mid] = arr[l];
arr[l] = val;
}
else
{
val = arr[r];
arr[r] = arr[l];
arr[l] = val;
}
while (l < r)
{
//需要从右侧找到第一个比val小的数
while (l < r && arr[r] >= val)
r--;
//l == r时不执行后面的l++和赋值操作
if (l < r)
{
//放到左边
arr[l] = arr[r];
l++;
}
//然后从左往右找到第一个比val大的数
while (l < r && arr[l] <= val)
l++;
//确保标记不会越界,l在r左侧
if (l < r)
{
//放到右边
arr[r] = arr[l];
r--;
}
}
//退出时l = r
arr[l] = val;
return l;
}