快排
分治:
- 分解:将数组A[p,r]划分成两个(可能为空)的子数组A[p,q-1]和A[q+1,r],使得A[p,q-1]
中的每一个元素都小于等于A[q],A[q]也小于等于A[p+1,r]中的每个元素 - 解决:递归调用快排,对两个子数组进行排序
- 合并:因为是基于地址的排序,所以不需要进行合并操作,原数组已经有序
partition函数:
先贴出代码
int partition(int *a, int p, int r) {
int x = a[r];
int i = p - 1;
for (int j = p; j < r; j++)
if (a[j] <= x) {
++i;
swap(a[i], a[j]);
}
swap(a[i + 1], a[r]);
return i + 1;
}
在for循环中,每个部分都满足如下性质:
对任意数组下标k,有:
- 若p <= k <= i, 则A[k] <= x
- 若i+1 <= k <= j-1, 则A[k] > x
- 若k = r, 则A[k] = x
重点理解一下partition函数的作用:
在partition中完成一趟划分,选取一个基准元素(pivot element),将小于它的放到它的左边,将大于它的放到他的右边,最后返回基准的位置。
数组被划分成四个部分(如下图)
- 浅阴影部分的元素都在划分的第一部分,其值都不大于x
- 深阴影部分的元素都在划分的第二部分,其值都大于x
- 无阴影的部分则是还未划分入这两个部分中的任意一个
- 最后一个元素为基准(主元)
![这里写图片描述](https://i-blog.csdnimg.cn/blog_migrate/181ea551bfad464c634ef3d30fbbccaf.png)
过程:以图中的数组为例
- 初始化变量,基准元素x为最右边的4,i=p-1,j=p,数组中的元素均为被放到前两个部分中的任意一个(i为较小部分的最后一个位置,j为较大部分的最后一个位置的下一个位置,j也表示当前待加入到前两个部分的元素下标)
- 对于第一个元素2,2<4,则i++,将2与他本身进行交换,2放入到元素值较小的部分
- 对于第二个元素8,8>4,则直接j++。7同理。8与7放入到元素较大的部分
- 对于第四个元素1,1<4,则将A[j] = 1与第一个大于基准的元素A[i+1] = 8进行交换(即swap(A[j], A[++i])),数值较小的部分规模增加。1进入较小的部分,8还在较大的部分,只是i与j的值都加了一,表示两部分的规模都变大了
- 对于第五个元素3,3<4将3与第一个大于基准的元素交换,即交换7与3的位置(swap(A[j], A[++i])),3进入较小部分
- 剩余元素都大于4,直接j++,将其放入较大的部分即可
- 最后一个基准元素4,将它与第一个大于基准的元素交换,即交换A[i+1]与A[r],这样基准元素就放到了两个部分之间
综上:如果A[j]>x,需要做的只是将j的值加1。如果A[j]<=x,则将下标 i 加1,并交换A[i]与A[j],再将 j 的值加1。最后将主元A[r]与最左边的大于x的元素A[i+1]进行交换,即可将基准元素移动到正确的位置上。函数最终返回基准元素的下标
partition在子数组A[p,r]上的时间复杂度为O(n), n=r-p+1
快排
void quicksort(int *a, int p, int r) {
if (p < r) {
int q = partition(a, p, r);
quicksort(a, p, q - 1);
quicksort(a, q + 1, r);
}
}
每次递归,先进行一趟划分,然后选出基准元素,再分别对基准左右两侧的子数组递归调用快排即可
时间复杂度分析:
最好情况划分:partition得到的两个子数组的规模都(约)等于n/2,在这种情况下,算法的时间复杂度为:T(n) = 2T(n/2) + O(n),最终求得T(n) = O(nlogn)
最坏情况划分:每次划分得到的两个子问题都分别包含了n-1个元素和0个元素,则
T(n) = T(n-1) + T(0) + O(n) = T(n-1) + O(n)
最终求得T(n) = O(n^2)
平均情况:利用递归树求得平均时间复杂度为O(nlogn)
快排的随机化版本
基准元素的选取直接影响了整个算法的时间性能。可以在算法中引入随机性,从而使得算法对于所有的输入都能获得较好的期望性能
在A[p,r]中随机选取一个元素作为主元,再将A[r]与随机选取出的元素进行交换
通过对序列p-r的随机抽样,可以保证基准元素x=A[r]是等概率地从子数组的r-p+1个元素中选取的。因为主元素是随机选取的,所以期望的平均情况下,对输入数组的划分也是比较均衡的
int partition(int *a, int p, int r) {
int x = a[r];
int i = p - 1;
for (int j = p; j < r; j++)
if (a[j] <= x) {
++i;
swap(a[i], a[j]);
}
swap(a[i + 1], a[r]);
return i + 1;
}
int randomPartition(int *a, int p, int r) {
int i = p + rand() % (r - p + 1);
swap(a[r], a[i]);
return partition(a, p, r);
}
void randoomQuicksort(int *a, int p, int r) {
if (p < r) {
int q = randomPartition(a, p, r);
randoomQuicksort(a, p, q - 1);
randoomQuicksort(a, q + 1, r);
}
}
期望运行时间T(n) = O(nlgn)
方法二:
双向扫描
void quickSort(int a[], int l, int r) {
if (l < r) {
int p = partition(a, l, r);
quickSort(a, l, p - 1);
quickSort(a, p + 1, r);
}
}
int partition(int a[], int p, int r) {
int i = p, j = r + 1;
int t = a[p];
while (1) {
/*从左边开始找第一个比t大的,从右边开始找第一个比t小的,然后交换两个元素*/
while (a[++i] < t && i < r);
while (a[--j] > t && j);
if (i >= j) //如果i>j,则表示已经有序,不需要交换
//如果i == j,也无交换的必要
break;
swap(a[i], a[j]);
}
//交换基准
a[p] = a[j];
a[j] = t;
return j;
}
或
void quickSort(int s[], int left, int right) {
if (left< right) {
int begin = left, end = right, x = s[left];
while (begin < end) {
while (begin < end && s[end] >= x) // 从右向左找第一个小于x的数
end--;
if (begin < end)
s[begin++] = s[end];
while (begin < end && s[begin]< x) // 从左向右找第一个大于等于x的数
begin++;
if (begin < end)
s[end--] = s[begin];
}
s[begin] = x;
quickSort(s, left, begin - 1); // 递归调用
quickSort(s, begin + 1, right);
}
}
此外还有“三数取中划分”,留作以后进行补充