快速排序
- 快速排序是Hoare于1962年提出的一种二叉树结构的交换排序方法.
- 基本思想:任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,
- 特点:左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值。
- 结束条件:最左右子序列重复该过程,直到所有元素都排列在相应位置上为止。
快速排序的流程
- 这里假设以数组最右面的值做基准值
- 通过上图我们可以看到,我可以通过递归的思想不断的将大区间分成小区间.
- 那么其终止条件是什么呢?
- 其实我们可以发现当区间不可再分的时候,递归也就无法再继续下去了.
- 那么区间不可再分的条件是什么呢?
- 那就是当区间大小为0 或者 区间大小为1的时候(既left >= right 的时候)
递归的代码实现
void _QuickSort(int *array,int left,int right){
//终止条件:size==0 || size==1
//left ==right 区间内还剩一个数
//left > right 区间内没有数
if(left>=right){
return ;
}
int div;//比基准值小的放其左边,大的放到后面去,基准值所在的下标
div=Partion(array,left,right);//遍历array[left,right]区间,把比其小的放左面,大的放右面
_QuickSort(array,left,div-1);//分治解决左边的小区间
_QuickSort(array,div+1,right);//分治解决右边的小区间
}
【那么问题来了,非递归的方式可不可以实现这个过程呢?】
- 当然可以了,我们可以利用栈的后进先出 思想来完成.也可以通过队列 先进先出的思想来完成.
- 具体步骤如下
- 通过上述递归的方式我们发现,要完成排序,我们要先拿到区间两端数值的下标,再进行排序
说明:先拿到左边区间两端的下标值还是右边区间两端的下标值都可以,但是必须是同一区间两端的下标值
使用栈的思想拿到区间两端下标的实现方式
特点:后进先出
void _QuickSort(int* array,int left,int right){
stack<int> s;
s.push(right);
s.push(left);
while(!s.empty()){
int _left=s.top();
s.pop();
int _right=s.top();
s.pop();
if(_left>=_right){
continue;
}
int div=Partition(array,_left,_right);
//div+1 right
s.push(_right);
s.push(div+1);
//_left,div -1
s.push(div-1);
s.push(_left);
}
}
通过队列拿到区间两端下标值的实现方式
特点:先进先出
void _QuickSort(int *array,int left,int right){
queue<int> q;
q.push(left);
q.push(right);
while(!q.empty()){
int _left=q.front();
q.pop();
int _right=q.front();
q.pop();
if(_left>=_right){
continue;
}
int div=Partition(array,_left,_right);
//[left,div-1]
q.push(_left);
q.push(div-1);
//[div+1,_right]
q.push(div+1);
q.push(_right);
}
}
将区间按照基准值划分为左右两半部分的常见方法
- 函数Pation的作用说明
- 1.将区间分成比基准值大的一部分与比基准值小的一部分
- 2.返回选定的基准值
【说明:下面方式都是将区间最右边的数作为基准值】
1.hoare版本
- 算法思想
- 1.标记区间中需要排序的开始和结束位置
- 2.从两端开始遍历整个排序区间,将两端比基准值小的值和比基准值大的值进行交换,重复该步骤
- 3.最后将标准值(待排序区间的最右边)与begin停止处的值交换
- 4.返回选定的标准值
int Partion(int* array,int left,int right){
int begin=left;
int end=right; //不能从right-1开始
while(begin<end){
while(begin<end && array[begin]<=array[right]){
begin++;
//在遍历过程中对大于基准值的元素进行定位
//此处的array[begin]<=array[right]中的"=" 必须存在
//如果不存在的话会造成基准值右边区间的死循环
}
while(begin<end && array[end]>=array[right]){
end--;
//在遍历过程中对小于基准值的元素进行定位
}
if(begin!=end){
Swap(array+begin,array+end);
}//当begin和end两个数相同的时候,两者指向相同的值,所以不需要交换
}
Swap(array+begin,array+right);
//返回当前基准值所在位置,
return begin;
}
2.挖坑法版本
- 算法思想
- 1.标记区间中需要排序的开始和结束位置
- 2.保存基准值(避免填坑造成的数据覆盖,导致标准值丢失),挖出填坑的空间
- 3.从待排序区间前端开始找比标准值大的数,去填右边区间的坑
- 4.从待排序区间后端开始找比基准值小的数,去填左边区间的坑
- 5.用标准值去填最后一个坑
- 6.返回标准值
- 代码实现
int Partion_wakeng(int *array,int left,int right){
int begin=left;
int end=right;
int flag=array[right]; //保存基准值,给挖坑目标腾空间
while(begin<end){
while(begin<end && array[begin]<= flag){
begin++;
}
//右侧坑
array[end]=array[begin];
while(begin<end && array[end] >= flag ){
end--;
}
//左侧坑
array[begin]=array[end];
}
array[begin]=flag;
return begin;
}
3.前后标兵法
- 算法思想
- 1.使用标兵指向数组前端,
- 2.遍历整个数组,将标兵指向的数据元素和符合条件的元素进行交换,重复该步骤
- 3.最后交换标兵和标准值
- 4.返回标准值
int Partion_Point(int *array,int left,int right){
int d=left;
for(int i=left;i<right;i++){
if(array[i]<array[right]){
if(d!=i){
Swap(array+d,array+i);
}
d++;
}
}
Swap(array+d,array+right);
return d;
}
我的实现方式
//挖坑法测试
//自己实现的版本
//在遍历过程中,将基准值和不符合条件的值进行交换
//并不断更新标准值的下标数据
int Partion_wakeng(int *array,int left,int right){
int begin=left;
int end=right;
int flag=right; //保存基准值,用来挖坑腾空间
while(begin<end){
while(begin<end && array[begin]<= array[flag]){
begin++;
}
if(array[begin]>array[flag]){
Swap(array+begin,array+flag);
flag=begin;
}
while(begin<end && array[end] >= array[flag] ){
end--;
}
if(array[end]<array[flag]){
Swap(array+end,array+flag);
flag=end;
}
}
Swap(array+begin,array+flag);
return begin;
}
总结
特性总结
- 1.时间复杂度:O(N*lonN)
- 2.空间复杂度:O(logN)
- 3.稳定性:不稳定
个人快排思想:(个人理解)
- 1.选择一个基准值,遍历整个排序数组.把比基准值小的放到基准值前面,把比基准值大的放到基准值后面.多次排序(递归、堆、栈都可以实现)后使得数组变的有序。
- 难点:基准值的选择和元素与基准值比较后的交换.