快速排序(三)

时间空间复杂度

我们来分析一下快速排序法的性能。快速排序的时间性能取决于快速排序递归的深度,可以用递归树来描述递归算法的执行情况。如图9‐9‐7所示,它是{50,10,90,30, 70,40,80,60,20}在快速排序过程中的递归过程。由于我们的第一个关键字是50,正好是待排序的序列的中间值,因此递归树是平衡的,此时性能也比较好。

 
图9-9-7
在最优情况下,Partition每次都划分得很均匀,如果排序n个关键字,其递归树的深度就为.log2n.+1(.x.表示不大于x的最大整数),即仅需递归log2n次,需要时间为T(n)的话,第一次Partiation应该是需要对整个数组扫描一遍,做n次比较。然后,获得的枢轴将数组一分为二,那么各自还需要T(n/2)的时间(注意是最好情况,所以平分两半)。于是不断地划分下去,我们就有了下面的不等式推断。
 
 
  1. T(n)≤2T(n/2) +n,T(1)=0  
  2. T(n)≤2(2T(n/4)+n/2) +n=4T(n/4)+2n  
  3. T(n)≤4(2T(n/8)+n/4) +2n=8T(n/8)+3n  
  4. ……  
  5. T(n)≤nT(1)+(log2n)×nO(nlogn) 

也就是说,在最优的情况下,快速排序算法的时间复杂度为O(nlogn)。

在最坏的情况下,待排序的序列为正序或者逆序,每次划分只得到一个比上一次划分少一个记录的子序列,注意另一个为空。如果递归树画出来,它就是一棵斜树。此时需要执行n‐1次递归调用,且第i次划分需要经过n‐i次关键字的比较才能找到第i个记录,也就是枢轴的位置,因此比较次数为 ,最终其时间复杂度为O(n2)。

平均的情况,设枢轴的关键字应该在第k的位置(1≤k≤n),那么:

 

由数学归纳法可证明,其数量级为O(nlogn)。

就空间复杂度来说,主要是递归造成的栈空间的使用,最好情况,递归树的深度为log2n,其空间复杂度也就为O(logn),最坏情况,需要进行n‐1递归调用,其空间复杂度为O(n),平均情况,空间复杂度也为O(logn)。

可惜的是,由于关键字的比较和交换是跳跃进行的,因此,快速排序是一种不稳定的排序方法。


快速排序原理

版本一:

原数组: 2 1 7 8 3 4 5

1.选中最后一个元素 5 为判断标准, 此时 bengin = 0 ; end = 6; i = -1; j 从0 到5

2. j = 0 : 判断A[0]小于5, i此时为0, 交换A[0]自身.

    2 1 7 8 3 4 5

3. j = 1 : 判断A[1]小于5, i此时为1, 交换A[1]自身.

    2 1 7 8 3 4 5

4. j = 2 : 判断A[2]大于5, i此时为1, 没有交换行为.

    2 1 7 8 3 4 5

5. j = 3 : 判断A[3]大于5, i此时为1, 没有交换行为.

    2 1 7 8 3 4 5

6. j = 4 : 判断A[4]小于5, i此时为2, 交换A[2]和A[4], 即交换7和3;

    2 1 3 8 7 4 5

7. j = 5 : 判断A[5]小于5, i此时为3, 交换A[3]和A[5], 即交换8和4;

    2 1 3 4 7 8 5

8. 完成循环, 交换A[3+1]和A[6];

    2 1 3 4 5 8 7

此时数组分成 {2, 1, 3, 4}, {5}, {8,7} 三个部分.

 

Note: 由Step 2, 3 可以看出, 当开始的元素小于基准值时, 自身被迫与自身交换. 可以做简化至 判断 j 和 i当前是否相等 决定是否进行交换. 不过这事实上不会影响排序的效率.


快速排序算法的关键是PARTITION过程,它对A[p..r]进行就地重排:

PARTITION(A, p, r)
1  x ← A[r]         //以最后一个元素,A[r]为主元
2  i ← p - 1
3  for j ← p to r - 1    //注,j从p指向的是r-1,不是r。
4       do if A[j] ≤ x
5             then i ← i + 1
6                  exchange A[i] <-> A[j]
7  exchange A[i + 1] <-> A[r]    //最后,交换主元
8  return i + 1

然后,对整个数组进行递归排序:

QUICKSORT(A, p, r)
1 if p < r
2    then q ← PARTITION(A, p, r)   //关键
3         QUICKSORT(A, p, q - 1)
4         QUICKSORT(A, q + 1, r)


代码:

/*#include<iostream>//该快速排序是以最后元素为主元
//比较之后相交换


using namespace std;


void swap(int &a,int &b)
{
int temp;
temp=a;
a=b;
b=temp;
}
int partition(int qs[],int f,int l)
{
int i,j,r;
r=qs[l];
i=f-1;
for(j=f;j<=l;j++)
{
if(qs[j]<=r)
{
++i;
swap(qs[i],qs[j]);
}
}
//swap(qs[i+1],qs[l]);
return i;
}
void quicksort(int qs[],int f,int l)
{
int q;
if(f<l)       //这个条件很重要 否则递归会一直进行下去
{
q=partition(qs,f,l);
   quicksort(qs,f,q-1);
   quicksort(qs,q+1,l);
}
}
int main()
{
int n,i;
int *qs=new int[n];
cin>>n;
for(i=0;i<n;i++)
{
cin>>qs[i];
}
quicksort(qs,0,n-1);
for(i=0;i<n;i++)
{
cout<<qs[i]<<"  ";
}
cout<<endl;
delete[] qs;
return 0;
}*/


版本二:

快速排序是C.R.A.Hoare1962年提出的一种划分交换排序。它采用了一种分治的策略,通常称其为分治法(Divide-and-ConquerMethod)

该方法的基本思想是:

1.先从数列中取出一个数作为基准数。

2.分区过程,将比这个数大的数全放到它的右边,小于或等于它的数全放到它的左边。

3.再对左右区间重复第二步,直到各区间只有一个数。

 

虽然快速排序称为分治法,但分治法这三个字显然无法很好的概括快速排序的全部步骤。因此我的对快速排序作了进一步的说明:挖坑填数+分治法:

先来看实例吧,定义下面再给出(最好能用自己的话来总结定义,这样对实现代码会有帮助)。

 

以一个数组作为示例,取区间第一个数为基准数。

0

1

2

3

4

5

6

7

8

9

72

6

57

88

60

42

83

73

48

85

初始时,i = 0;  j = 9;   X = a[i] = 72

由于已经将a[0]中的数保存到X中,可以理解成在数组a[0]上挖了个坑,可以将其它数据填充到这来。

j开始向前找一个比X小或等于X的数。当j=8,符合条件,将a[8]挖出再填到上一个坑a[0]中。a[0]=a[8]; i++;  这样一个坑a[0]就被搞定了,但又形成了一个新坑a[8],这怎么办了?简单,再找数字来填a[8]这个坑。这次从i开始向后找一个大于X的数,当i=3,符合条件,将a[3]挖出再填到上一个坑中a[8]=a[3]; j--;

 

数组变为:

0

1

2

3

4

5

6

7

8

9

48

6

57

88

60

42

83

73

88

85

 i = 3;   j = 7;   X=72

再重复上面的步骤,先从后向前找,再从前向后找

j开始向前找,当j=5,符合条件,将a[5]挖出填到上一个坑中,a[3] = a[5]; i++;

i开始向后找,当i=5时,由于i==j退出。

此时,i = j = 5,而a[5]刚好又是上次挖的坑,因此将X填入a[5]

 

数组变为:

0

1

2

3

4

5

6

7

8

9

48

6

57

42

60

72

83

73

88

85

可以看出a[5]前面的数字都小于它,a[5]后面的数字都大于它。因此再对a[0…4]a[6…9]这二个子区间重复上述步骤就可以了。

 

 

对挖坑填数进行总结

1.i =L; j = R; 将基准数挖出形成第一个坑a[i]

2.j--由后向前找比它小的数,找到后挖出此数填前一个坑a[i]中。

3.i++由前向后找比它大的数,找到后也挖出此数填到前一个坑a[j]中。

4.再重复执行23二步,直到i==j,将基准数填入a[i]中。


代码:

#include<iostream>//该快速排序是以第一个元素为主元
//左右同时扫描


using namespace std;
void quicksort(int qs[],int f,int l)
{
   if(f<l)

int i,j,k;
    i=f;
    j=l;
    k=qs[i];
    if(i<j)  
   {
   while(i<j)
   {
   while(i<j && qs[j]>=k)
   j--;
   if(i<j) qs[i++]=qs[j];//从右向左找第一个小于k的数
   
   while(i<j && qs[i]<k)
   i++;
   if(i<j) qs[j--]=qs[i];//从左向右找第一个大于k的数
   }
   }
    qs[i]=k;
quicksort(qs,f,i-1);
quicksort(qs,i+1,l);
}
}
int main()
{
    int i,n;
int *qs=new int[n];
cin>>n;
for(i=0;i<n;i++)
{
cin>>qs[i];
}
quicksort(qs,0,n-1);
for(i=0;i<n;i++)
{
cout<<qs[i]<<"  ";
}
cout<<endl;
delete[] qs;
return 0;
}


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
数取中法是一种改进的快速排序算法中的一种优化方法。该方法通过在待排序数组的左端、右端和中间选择个元素,并将这个元素进行排序,将中间值作为枢纽元(pivot)来进行分割。这样可以尽量避免在有序数组中选择极值作为枢轴,从而提高快速排序的效率。 下面是一种C语言实现的快速排序数取中法的代码示例: ```c #include <stdio.h> void Swap(int *a, int *b) { int temp = *a; *a = *b; *b = temp; } void InsertionSort(int *arr, int left, int right) { for(int i = left + 1; i <= right; i++) { int key = arr[i]; int j = i - 1; while(j >= left && arr[j] > key) { arr[j + 1] = arr[j]; j--; } arr[j + 1] = key; } } int Median3(int *arr, int left, int right) { int center = (left + right) / 2; if(arr[left] > arr[center]) { Swap(&arr[left], &arr[center]); } if(arr[left] > arr[right]) { Swap(&arr[left], &arr[right]); } if(arr[center] > arr[right]) { Swap(&arr[center], &arr[right]); } Swap(&arr[center], &arr[right - 1]); return arr[right - 1];} void QuickSort(int *arr, int left, int right) { const int Cutoff = 5; // 当待排序的元素个数小于等于Cutoff时,使用插入排序 if(left + Cutoff <= right) { int pivot = Median3(arr, left, right); int i = left; int j = right - 1; while(1) { while(arr[++i] < pivot); while(arr[--j] > pivot); if(i < j) { Swap(&arr[i], &arr[j]); } else { break; } } Swap(&arr[i], &arr[right - 1]); // 将枢轴放到正确的位置 QuickSort(arr, left, i - 1); QuickSort(arr, i + 1, right); } else { InsertionSort(arr, left, right); } } int main() { int arr[] = {5, 2, 1, 4, 3, 6, 9, 8, 7, 0}; QuickSort(arr, 0, 9); for(int i = 0; i < 10; i++) { printf("%d ", arr[i]); } return 0; } ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值