do while和while之间
一、挖坑分治法
int AdjustArray(int s[], int l, int r) //返回调整后基准数的位置
{
int i = l, j = r;
int x = s[l]; //s[l]即s[i]就是第一个坑
while (i < j)
{
// 从右向左找小于x的数来填s[i]
while(i < j && s[j] >= x)
j--;
if(i < j)
{
s[i] = s[j]; //将s[j]填到s[i]中,s[j]就形成了一个新的坑
i++;
}
// 从左向右找大于或等于x的数来填s[j]
while(i < j && s[i] < x)
i++;
if(i < j)
{
s[j] = s[i]; //将s[i]填到s[j]中,s[i]就形成了一个新的坑
j--;
}
}
//退出时,i等于j。将x填到这个坑中。
s[i] = x;
return i;
}
void quick_sort1(int s[], int l, int r)
{
if (l < r)
{
int i = AdjustArray(s, l, r);//先成挖坑填数法调整s[]
quick_sort1(s, l, i - 1); // 递归调用
quick_sort1(s, i + 1, r);
}
}
简洁之后
//快速排序
void quick_sort(int s[], int l, int r)
{
if (l < r)
{
//Swap(s[l], s[(l + r) / 2]); //将中间的这个数和第一个数交换 参见注1
int i = l, j = r, x = s[l];
while (i < j)
{
while(i < j && s[j] >= x) // 从右向左找第一个小于x的数
j--;
if(i < j)
s[i++] = s[j];
while(i < j && s[i] < x) // 从左向右找第一个大于等于x的数
i++;
if(i < j)
s[j--] = s[i];
}
s[i] = x;
quick_sort(s, l, i - 1); // 递归调用
quick_sort(s, i + 1, r);
}
}
二、交换分治法
#include <iostream>
using namespace std;
const int N = 1000010;
int q[N];
void quick_sort(int q[], int l, int r)
{
if (l >= r) return;
int i = l - 1, j = r + 1, x = q[l + r >> 1];
while (i < j)
{
do i ++ ; while (q[i] < x);
do j -- ; while (q[j] > x);
if (i < j) swap(q[i], q[j]);
}
quick_sort(q, l, j);
quick_sort(q, j + 1, r);
}
注意的是:
- do while和while的区别--do while可以剩去循环之前(也可以理解为循环之后)的i++,j--;
- 挖坑排序和交换排序需要注意的每次循环之前都要使i++,j--;不然的话会陷入死循环。。。。深刻的记忆啊
- 递归调用,用递归来实现分治法
- 交换法的递归调用必须要使用j来区分区间,不可以使用i
- 挖坑法的递归调用可以使用i或者j,但是必须是i-1,i+1,或者是j-1,j+1;
- 挖坑法如果把基准选择到中间的话需要设置标志index来定位目前坑在哪里,如果基准在第一位的话,可以设置index,也可以省略,坑位直接就是i,i走到哪里坑就在哪里。
- 交换法在使用i还是j分割的时候,最好使用j来分割,我以为i和j是一样的,后来发现不一样。因为会又i和j相等和j大于i的情况。所以对i来说是有两种情况的,需要分情况来写递归的参数
if(i>j){ quick_sort(q,l,i-1,n); quick_sort(q,i,r,n+1); }else{ quick_sort(q,l,j,n); quick_sort(q,j+1,r,n+1); }
对j来说不需要分j一直是大于i的,所以选择j的话,可以不需要分情况来改变递归参数。
quick_sort(q,l,j,n);
quick_sort(q,j+1,r,n+1);
还要注意到的是i可能会到i负数,所以最好不要是用i-1;来分隔
总结:
- 将基准放在中间比放在头要稍快一些。
- 通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。
- 快速排序的时间复杂度计算---快速排序涉及到递归调用,所以该算法的时间复杂度还需要从递归算法的复杂度开始说起; 递归算法的时间复杂度公式:T[n] = aT[n/b] + f(n) ;
- 快速排序的速度并不稳定,最快是nlogN,最慢是n平方
对比:
void quick_sort(int q[],int l,int r){
if(l>=r){
return;
}
int i=l,j=r,x=q[l+r>>1];
while(i<j){
while(q[i]<x){
++i;
}
while(q[j]>x){
--j;
}
if(i<j) {
swap(q[i],q[j]);
i++;
j--;
}
}
quick_sort(q,l,j);
quick_sort(q,j+1,r);
}
void kkk(int q[],int l,int r){
int i=l-1;
int j=r+1;
int x=q[l+r>>1];
if(l<r){
while(i<j){
i++;
while(q[i]<x){
i++;
}
j--;
while(q[j]>x){
j--;
}
if(i<j){
swap(q[i],q[j]);
}
}
kkk(q,l,j);
kkk(q,j+1,r);
}
}
对比上方两幅图:
我以为把i++;j--位置放前面和后面没有什么。
最后发现如果放在后面的话i和j会多加1;。。。。
最优情况下时间复杂度
快速排序最优的情况就是每一次取到的元素都刚好平分整个数组(很显然我上面的不是);
此时的时间复杂度公式则为:T[n] = 2T[n/2] + f(n);T[n/2]为平分后的子数组的时间复杂度,f[n] 为平分这个数组时所花的时间;
下面来推算下,在最优的情况下快速排序时间复杂度的计算(用迭代法):
T[n] = 2T[n/2] + n ----------------第一次递归
令:n = n/2 = 2 { 2 T[n/4] + (n/2) } + n ----------------第二次递归
= 2^2 T[ n/ (2^2) ] + 2n
令:n = n/(2^2) = 2^2 { 2 T[n/ (2^3) ] + n/(2^2)} + 2n ----------------第三次递归
= 2^3 T[ n/ (2^3) ] + 3n
......................................................................................
令:n = n/( 2^(m-1) ) = 2^m T[1] + mn ----------------第m次递归(m次后结束)
当最后平分的不能再平分时,也就是说把公式一直往下跌倒,到最后得到T[1]时,说明这个公式已经迭代完了(T[1]是常量了)。
得到:T[n/ (2^m) ] = T[1] ===>> n = 2^m ====>> m = logn;
T[n] = 2^m T[1] + mn ;其中m = logn;
T[n] = 2^(logn) T[1] + nlogn = n T[1] + nlogn = n + nlogn ;其中n为元素个数
又因为当n >= 2时:nlogn >= n (也就是logn > 1),所以取后面的 nlogn;
综上所述:快速排序最优的情况下时间复杂度为:O( nlogn )
最差情况下时间复杂度
最差的情况就是每一次取到的元素就是数组中最小/最大的,这种情况其实就是冒泡排序了(每一次都排好一个元素的顺序)
这种情况时间复杂度就好计算了,就是冒泡排序的时间复杂度:T[n] = n * (n-1) = n^2 + n;
综上所述:快速排序最差的情况下时间复杂度为:O( n^2 )
平均时间复杂度
快速排序的平均时间复杂度也是:O(nlogn)
空间复杂度
其实这个空间复杂度不太好计算,因为有的人使用的是非就地排序,那样就不好计算了(因为有的人用到了辅助数组,所以这就要计算到你的元素个数了);我就分析下就地快速排序的空间复杂度吧;
首先就地快速排序使用的空间是O(1)的,也就是个常数级;而真正消耗空间的就是递归调用了,因为每次递归就要保持一些数据;
最优的情况下空间复杂度为:O(logn) ;每一次都平分数组的情况
最差的情况下空间复杂度为:O( n ) ;退化为冒泡排序的情况