思想
通过一趟排序将待排记录分割成独立的两部分,其中一部分记录的关键字均比另一部分记录的关键字小,则可分别对这两部分记录继续进行排序,以达到整个序列有序的目的。
partition函数
分割函数,快排的核心,思想就是在数组中找一个数作为中间值,把数组中比它小的放左边,大的放右边。这个函数有两种写法,单指针版和双指针版。
核心思想: 每次都取数组的第一个元素作为比较标准(哨兵元素),凡是大于这个哨兵元素的都放在它的右边,凡是小于这个哨兵元素的都放在它的左边;
-
以数组的第一个元素为哨兵元素,让其他元素和它比较大小;(记住这时候第一个元素位置是空的,因为里面的值被作为哨兵元素保存起来了)
-
开始从数组尾部**(很重要)**往前循环得到一个小于哨兵元素的元素A ,把该元素A 放到第一个元素位置(也就是哨兵元素位置上,因为哨兵元素位置是空的,这时候要记住 元素A 的位置是空的了)
-
开始从数组头部往后循环得到一个大于哨兵元素的 元素B ,把该 元素B 放在上一步中移出的 元素A 的位置上;
-
依次循环上面2、3步,直到左右指针相遇,完成一次数组分割(此时左右两边分别是大于和小于基准的元素),然后将相遇的索引处 置为 哨兵。
//双指针方法
int quick_sort(vector<int> &nums,int left,int right){
int tmp = nums[left];
while(left<right){
while (left<right&&nums[right]>tmp) right--;
nums[left] = nums[right];
while (left<right&&nums[left]<=tmp) left++;
nums[right] = nums[left];
}
nums[left] = tmp;
return left;
}
递归与非递归
- 递归就是: 对当前数组进行一次分割后,继续递归对 左半部分和有半部分 进行相同的递归操作,知道数组大小减为1停止;
- 非递归:使用一个栈保存 左右两边的下标,然后不断进行 进出栈 来获取左右边界,然后对每个 子数组 进行排序。
// 递归版本
// 结束:数组只有一个元素时,有序,结束
// 返回值: 已经有序的 子数组
// 处理函数:分割函数,对每次递归的子数组 进行分割排序
void dfs(vector<int> &nums,int left,int right){
if(left >= right) return ;
int k = quick_sort(nums,left,right);
dfs(nums,left,k-1);
dfs(nums,k+1,right);
}
// 非递归版本 保存 左右边界索引信息
vector<int> Solution::sortArray(vector<int> &nums) {
int len = nums.size();
typedef pair<int,int> P;
stack<P> ms;
ms.push(P(0,len-1));
while(!ms.empty()){
int left = ms.top().first;
int right = ms.top().second;
ms.pop();
if(left >= right) continue;
int k = quick_sort(nums,left,right);
ms.push(P(left,k-1));
ms.push(P(k+1,right));
}
稳定性: 因为在快速排序的时候,即使待排序元素可基数相等也需要移动待排序元素的位置使得有序,所以快速排序是不稳定的。
4_ 0 3 4^ 2 5 --> 2 0 3 4
关于递归转循环需要知道的事情
通过归并排序和快速排序非递归实现的讲解,似乎将其转化为循环是一个更佳的做法,其实不然,它只适用于特定的场景。
关于这种方法,需要有如下的认知:
- 递归的代码,在很多时候比循环的代码更加容易理解。
- 递归转循环,在效率上并没有提高。相反,由于增加了构造堆栈模型的过程,其消耗的时间更多。
- 只有当递归的层数过多,而导致 StackOverFlow 的问题出现,才考虑使用递归转循环的方法。
- 可以通过调整 JVM 参数,来达到扩充堆栈空间的目的,但是一般不推荐这么做,因为这个影响是整体的。
- 从代码的角度,如果循环能够解决问题,那么就使用循环;如果递归能解决问题,那么就使用递归,没有必要特意去做两者的转换。