《算法》系列 知识整理(C++描述)
算法学习历程
- 排序算法
- 查找算法
- 图
- 字符串问题
- 智能算法
学习目录
-
排序算法
- 初级排序算法
- 归并排序
- 快速排序
- 优先队列
- 排序算法的应用
本文主要内容
本文主要针对快速排序算法的思想、实现、复杂度分析、算法改进几个方面展开。
各位看官可以根据需求,从任意一部分开始看起。
快速排序
算法思想
快速排序的基本思想为:将一个数组分成两个子数组 ,保证其中一个子数组的所有元素均小于另一个数组的最小元素。然后对两个数组独立地排序,当两个数组都有序时,整个数组便有序了。
我们在这里比较一下归并排序和快速排序:
1、归并排序将数组分成两个子数组,分别进行排序,再将有序的子数组归并得到有序的整个数组。
2、快速排序将数组以一种规定的方式切分为两个子数组,再分别进行排序,两个子数组均有序后,整个数组便有序。
我们看到:
1、两者均用到递归的思想,且“分别对两个数组进行排序 ”体现了递归的思想。
2、两个算法对于数组的操作,一个是“归并”,一个是“切分”,且前者发生在递归前,后者发生在递归后。
接着来看快速排序的思想,在上面的比较中,我们知道了实现快速排序的关键点在于如何划分数组,在划分好数组后,只需要递归调用函数自身即可。
划分这一操作,使得数组有以下性质:
1、对于数组中的某个元素,其位置在划分完成后就定了下来;
2、对于该元素左侧的所有元素,均小于等于该元素;
3、对于该元素右侧的所有元素,均大于等于该元素。
也就是说,在进行一次这样的划分后,就会有一个元素的位置确定了下来,我们只需要递归调用这个函数,就可以完成所有元素的排序。
算法的实现
我们沿着刚刚提到的划分操作的性质,来逐步实现这样的划分操作。
第一条提到:对于数组中的某个元素,其位置在划分后就定了下来。
那么,我们要怎样来确定这个“某个元素”呢?
倘若我们直接去遍历一遍数组,想直接得到满足上述三条性质的元素,那是很难实现的。
那么我们不妨随机选取一个元素,将其作为将要被排定的元素,在经过对数组的一定操作后,使其满足条件。
那为了方便起见,也不妨直接选取数组首元素作为这个将要被排定的元素,我们记为a[lo]。
接下来只需要想办法,把小于a[lo]的元素都放在a[lo]的左侧,大于a[lo]的元素都放在a[lo]的右侧。
现在来看如何实现:(非最终操作,只是作为引导开端,后面会进行一些优化)
1、设两个指针i、j,分别指向数组的两端a[lo]、a[hi]。
2、使指针i从左往右移动,寻找大于等于a[lo]的元素,当遇到第一个符合要求的元素时便停下来。
3、使指针j从右向左自动,寻找小于等于a[lo]的元素,当遇到第一个符合要求的元素时便停下来。
4、如果此时i在j的左侧,即数组还没有被扫描完毕,则将i、j处的元素交换,回到步骤2,继续扫描。
5、如果此时i与j已经相遇(i>=j),证明数组已经被扫描完毕。现在就要把a[lo]放到i与j相遇的地方来,那么我们是放到a[i]处还是a[j]处呢?
答:a[j]处。由于i处的元素是大于等于a[lo]的,当a[i]>a[lo],若我们交换a[i]和a[lo],那么就会有一个大于a[lo]的元素去了最左端,便不符合我们的目的了。而a[j]处是小于等于a[lo]的元素,交换后符合我们的目的。
将a[lo]与a[j]交换后,一轮划分完毕,此时a[j]处是已排定的元素,其左侧均小于它,右侧均大于它。
来看一下切分的轨迹,有助于理解:
记v=a[lo]=K
a[ ]
i j 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
初始 0 16 K R A T E L E P U I M Q C X O S
扫描 1 12 K
R
i
\frac{R}{i}
iR A T E L E P U I M Q
C
j
\frac{C}{j}
jC X O S
交换 1 12 K
C
i
\frac{C}{i}
iC A T E L E P U I M Q
R
j
\frac{R}{j}
jR X O S
扫描 3 9 K C A
T
i
\frac{T}{i }
iT E L E P U
I
j
\frac{I}{j}
jI M Q R X O S
交换 3 9 K C A
I
i
\frac{I}{i}
iI E L E P U
T
j
\frac{T}{j}
jT M Q R X O S
扫描 5 6 K C A I E
L
i
\frac{L}{i}
iL
E
j
\frac{E}{j}
jE P U T M Q R X O S
交换 5 6 K C A I E
E
i
\frac{E}{i}
iE
L
j
\frac{L}{j}
jL P U T M Q R X O S
扫描 6 5 K C A I E
E
j
\frac{E}{j}
jE
L
i
\frac{L}{i}
iL P U T M Q R X O S
最后一次
交换 5 6 E C A I E
K
j
\frac{K}{j}
jK
L
i
\frac{L}{i}
iL P U T M Q R X O S
不得不说 画轨迹图可真累。。。
代码先不贴,咱们先分析以下复杂度,然后对这个算法进行一番优化,最后再贴代码。
算法复杂度分析
来分析快速排序需要的比较次数。
老规矩,先说明结论,再分析:
将长度为N的无重复数组排序,快速排序平均需要2NlnN次比较。
记CN为将N个不同元素进行排序所需要的比较次数。
1、C0=C1=0;
2、当N>1时:
根据上面的分析,我们可以得治,快速排序分为三大部分:划分、左数组递归、右数组递归,计算比较次数时,分别计算后相加即可。
划分中的比较次数:在划分过程中,我们对数组遍历了一遍,遍历每个元素时,都需要和a[lo]进行一次比较,此时有N次比较,而当i与j相遇时,两个指针对同一个元素都进行了比较,因此总的比较次数为N+1;
左、右数组递归:左右两个子数组的长度可能为0、1、2、……、N-1中的任何一个值(对应地,另一个数组为N-1、……、2、1、0),因此,我们考虑平均情况,左右两个数组地比较次数分别为:
(C0+C1+……+CN-2+CN-1)/N和(CN-1+CN-2+……+C0)/N
因此:
CN=N+1+(C0+C1+……+CN-2+CN-1)/N+(CN-1+CN-2+……+C0)/N
下面即是一些数学归纳:
等式两边同乘N,整理后得:
NCN=N(N+1)+2(C0+C1+……+CN-2+CN-1)……………………①
令N=N-1,得:
(N-1)CN-1=N(N-1)+2(C0+C1+……+CN-2)……………………②
①-②得:
NCN-(N-1)CN-1=2N+2CN-1
整理得:
NCN=(N+1)CN-1+2N
两边同除以N得:
CN=
N
+
1
N
\frac{N+1}{N}
NN+1CN-1+2
即得到了CN和CN-1之间得关系。
利用数学归纳法,可得:
CN=
N
+
1
N
\frac{N+1}{N}
NN+1CN-1+2
=
N
+
1
N
\frac{N+1}{N}
NN+1(
N
N
−
1
\frac{N}{N-1}
N−1NCN-2+2)+2
=
N
+
1
N
−
1
\frac{N+1}{N-1}
N−1N+1CN-2+2
N
+
1
N
\frac{N+1}{N}
NN+1+2
=……
=2(N+1)(
1
3
\frac{1}{3}
31+
1
4
\frac{1}{4}
41+……+
1
N
+
1
\frac{1}{N+1}
N+11)
~2NlnN
这是我们讨论的平均情况,但在最坏得情况下,快速排序会回退到 N ² 2 \frac{N²}{2} 2N²次比较,但通过下面的一种改进方式——随机打乱数组,可以有效避免这种情况的出现。
算法改进
1、最简单直白的一点改进为:初始时,我们没有必要将a[i]与a[lo]进行比较,因此i可以从lo+1开始;
2、为了避免出现a[lo]的选择受所输入的数组的影响,可以考虑在输入数组后,将其随机打乱,再定a[lo],以便消除对输入的依赖。
3、当数组较小时,切换成插入排序;(当数组较小时,快速排序比插入排序要慢,因其在小数组中也要递归调用自己。)
4、三取样切分(对于有大量重复元素出现时,效率非常高)
我们设置三个指针,将整个数组划分为四部分(其中三部分是确定的,有一部分未确定,因此称为“三取样”),三个指针分别记:it、i、gt,使得:
①a[lo…it-1]均小于v
②a[it…i-1]均等于v
③a[i…gt]未确定
④a[gt+1…hi]均大于v
采取以下三个操作来实现上述的划分:
①若a[i]<v,交换a[it]和a[i],再it++,i++;
②若a[i]>v,交换a[gt]和a[i],再gt–;
③若a[i]==v,执行i++;
最后,我们贴上改进后的普通快速排序和三向切分快速排序的C++代码:
首先是普通快速排序:
void exch(int *a,int i,int j)
{
int t=a[i];
a[i]=a[j];
a[j]=t;
}
int Partition(int *a,int low,int high)
{
int i=low,j=high+1;
int v=a[low];
while(true)
{
//j=high+1、+=i和--j这样的设计避免了a[i]与a[lo]的冗余比较
while(a[++i]<v);
while(a[--j]>v);
if(i>=j)
break;
exch(a,i,j);
}
exch(a,low,j);
return j;
}
void Quick_sort(int *a,int low,int high)
{
if(high<=low)
return;
int j=Partition(a,low,high);
Quick_sort(a,low,j-1);
Quick_sort(a,j+1,high);
}
接下来是三向切分快速排序的代码:
void exch(int *a,int i,int j)
{
int t=a[i];
a[i]=a[j];
a[j]=t;
}
void Quick3way_sort(int *a,int low,int high)
{
if(high<=low)
return;
int lt=low,i=low+1,gt=high;
int v=a[low];
while(i<=gt)
{
if(a[i]<v)
exch(a,lt++,i++);
else if(a[i]>v)
exch(a,i,gt--);
else i++;
}
Quick3way_sort(a,low,lt-1);
Quick3way_sort(a,gt+1,high);
}
有关快速排序的内容到这里就结束了,初次整理,避免不了有错误,欢迎指正。:)
如果喜欢,还可以观看本系列其他文章呐~