快速排序详解C++实现
注:要学会快速排序,前提要明白分治的思想
1.分治介绍
分治算法(divide and conquer)的核心思想就是将原问题划分成n个规模较小,并且结构与原问题相似的子问题,递归地解决这些子问题,然后再合并其结果,就得到原问题的解。要区分开分治和递归,分治是处理问题的思想,递归是一种编程技巧。分治一般都比较适合用用递归来实现。这里的快排就是体现分治思想的一个例子,类似的归并排序以及汉诺塔等等都能较好的应用到分治的思想。
2.使用分治的思想解决排序问题
在快排中如何使用分治将是学习快排的第一步。
2.1哨兵划分
快速排序的核心操作是“哨兵划分”,其目标是:选择数组中的某个元素作为“基准数”,将所有小于基准数的元素移到其左侧,而大于基准数的元素移到其右侧。
这里按照升序进行,对一个无序数组进行排序,我们这里取数组第一个元素作为基准,并且i和j分别指向数组的头部和尾部,i的使命是找到比基准大的数,j的使命是找到比基准小的数,当他们各自找到时便互相交换,直到i和j相遇,两者指向同一个数字,此时将该位置的数据与基准做交换。
注:将所有小于基准数的元素移到其左侧,而大于基准数的元素移到其右侧。因此i找到小于等于基准的数不做处理,继续保留在左侧,j同理
最开始i指向最左侧,j指向最右侧,取2为基准,j寻找小于2的数,i寻找大于2的数
此时j不断向左移动找到了小于基准的数字0,j完成使命
i开始向右移动寻找大于基准的数,并找到4
此时,i所在位置元素和j所在位置元素交换
完成交换后,j继续向左走,走到指向1的位置停下
i继续向右走,此时i与j相遇
相遇位置元素和基准进行交换
经过一次“哨兵划分”之后,实现了基准左边的数小于等于基准,基准右边的数大于等于基准。
注:这里可以发现,我是让j先走的,原因是j先走,可以保证相遇位置的数小于基准,感兴趣可以试试i先走结果又当如何
哨兵划分完成后,原数组被划分成三部分:左子数组、基准数、右子数组,且满足“左子数组任意元素 ≤ 基准数 ≤ 右子数组任意元素”。因此,我们接下来只需对这两个子数组进行排序。此时的子数组和我们前面讨论的数组具有相同的结构,只不过是数组更短了。因此哨兵划分的实质是将一个较长数组的排序问题简化为两个较短数组的排序问题,这边是前面分治思想的应用。实现则为地柜方式实现
/*
*哨兵划分
*pram
*return 基准索引
*/
int partiton(vector<int>& nums, int left, int right)
{
int i = left;
int j = right;
while(i<j)
{
//j先动,寻找小于left的数
while(i<j && nums[j]>=nums[left])
{
j--;
}
//i寻找大于left的数
while(i<j && nums[i]<=nums[left])
{
j++;
}
swap(&nums[i],&nums[j]);
}
swap(&left,&nums[i]);
return i;
}
/* 快速排序 */
void quickSort(vector<int> &nums, int left, int right) {
// 子数组长度为 1 时终止递归
if (left >= right)
return;
// 哨兵划分
int pivot = partition(nums, left, right);
// 递归左子数组、右子数组
quickSort(nums, left, pivot - 1);
quickSort(nums, pivot + 1, right);
}
介绍完毕,上述写法叫左右指针法,一般情况下效率确实高,确实快,但是对极端情况下会退化为冒泡排序,如对5,4,3,2,1
进行升序排列,基准选择为5,j找小于5的数字,以此j在数据为1处停留,i找寻大于5的找不到,知道i , j相遇,此时交换,变为1,4,3,2,5
,这显然就是最坏情况下的时间复杂度的状况。经过一轮之后,待排序列由n变为n-1,效率低下。
根据这种极端情况,我们可以发现,核心问题在于基准数选的不好,如果当前所取的基准值恰好是当前序列中最小的数或者最大的数,那么我们的划分位置必然是首或尾的位置。因此对快排优化的核心想法就落到了如何取到更合理的基准数,我们希望,这个数处于数列的中位数,这样经过一次划分,我可以达到左右子列的数据数量基本一致,避免一侧子列数据量很大或很小,故提出三数取中的方法取基准值,取序列中的最左边,中间,以及最右边三个数,取这三个数的中位数元素作为基准值。这样可以更大程度上避免划分位置不好的情况。此方法代码,只需将第一种方法的left换成上述的中位数