快速排序
过程
与归并排序类似,快速排序也运用了分治的思想。
- 分解:将数组 A [ l … r ] A[l \ldots r] A[l…r]分解成两个部分 A [ l … m i d − 1 ] A[l \ldots mid-1] A[l…mid−1]和 A [ m i d + 1 … r ] A[mid+1 \ldots r] A[mid+1…r](全文默认为全闭下标集合)。并且我们要求 A [ l … m i d − 1 ] A[l \ldots mid-1] A[l…mid−1]中的元素,都小于等于 A [ m i d ] A[mid] A[mid], A [ m i d + 1 … r ] A[mid+1 \ldots r] A[mid+1…r]都大于 A [ m i d ] A[mid] A[mid](边界条件不严格,可以根据情况调换)。这个过程的名字叫做分区。
- 解决:通过递归调用,解决两个区间 A [ l … m i d − 1 ] A[l \ldots mid-1] A[l…mid−1]和 A [ m i d + 1 … r ] A[mid+1 \ldots r] A[mid+1…r]的子问题。
- 合并:因为左右两边都是有序的,并且左区间的元素都小于右区间的元素,因此整个数组就是有序的,因此不需要合并问题。
下面的伪代码实现快速排序:
QUICKSORT(A,l,r):
if l < r:
q = PARTITION(A,l,r)
QUICKSORT(A,l,q - 1)
QUICKSORT(A,q + 1,r)
分区
其中 P A R T I T I O N PARTITION PARTITION函数是对数组进行划分,他实现了对数组的重新排序。交换法是实现这个函数的最常用的方法。
PARTITION(A,l,r):
x = A[r]
i = l - 1
for j = p to r - 1
if A[j] <= x
exchange(A[++i],A[j])
exchange(A[++i],A[r])
return i
其中, P A R T I T I O N PARTITION PARTITION函数总是选择 A [ r ] A[r] A[r]作为 A A A的主元,有时也叫哨兵元素。
朴素快速排序的时间复杂度分析
在朴素快速排序中,我们总是选择数组最后一个元素作为主元元素,但是,这样真的最优吗?
考虑特例 A = 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 A = {1,2,3,4,5,6,7,8} A=1,2,3,4,5,6,7,8,第一趟,我们将数组都扫描一遍,递归解决 1 , 2 , 3 , 4 , 5 , 6 , 7 {1,2,3,4,5,6,7} 1,2,3,4,5,6,7,并且依次解决 1 , 2 , 3 , 4 , 5 , 6 {1,2,3,4,5,6} 1,2,3,4,5,6,一直到 1 {1} 1为止,我们发现,每次分割的区间都是一个区间为全满,另一个为空区间,这么做就是退化成插入排序,时间复杂度为 O ( n 2 ) O(n^2) O(n2),不是我们所期望的时间复杂度。
随机化快速排序
根据上面的分析,我们选择主元元素的时候,可以考虑随机的从数组中挑选元素作为主元。因此,我们可以修改分区函数:
RANDOMIZED-PARTITION(A,l,r):
i = RANDOM(l,r)
exchange(A[i],A[r])
PARTITION(A,l,r)
新的排序方法改为调用 R A N D O M I Z E D − P A R T I T I O N RANDOMIZED-PARTITION RANDOMIZED−PARTITION函数即可。
随机化快速排序的时间复杂度分析
参考《算法导论》第7章,这里给出结论。
随机化快速排序的期望时间复杂度为 O ( n log n ) O(n \log n) O(nlogn)
代码实现
朴素排序:
int partition(int a[], int l, int r)
{
int x = a[r];
int p = l - 1;
for (int i = l; i <= r - 1; i++)
{
if (a[i] <= x)
{
swap(a[i], a[++p]);
}
}
swap(a[r], a[++p]);
return p;
}
void quicksort(int a[], int l, int r)
{
if (l == r)
return;
int p = partition(a, l, r);
quicksort(a, l, p - 1);
quicksort(a, p + 1, r);
}
随机化:
int randomizedPartition(int a[], int l, int r)
{
int k = rand() % (r - l + 1) + l;
swap(a[k], a[r]);
return partition(a, l, r);
}
int partition(int a[], int l, int r)
{
int x = a[r];
int p = l - 1;
for (int i = l; i <= r - 1; i++)
{
if (a[i] <= x)
{
swap(a[i], a[++p]);
}
}
swap(a[r], a[++p]);
return p;
}
void quicksort(int a[], int l, int r)
{
if (l == r)
return;
int p = randomizedPartition(a, l, r);
quicksort(a, l, p - 1);
quicksort(a, p + 1, r);
}
例题
虽然OI中几乎很少让你写快速排序的模板,但是快速排序的思想是很重要的。
寻找第K小的元素,采用快速排序的思想,如果分区大于要求元素,那么继续分治下去;如果分区小于要求元素,那么就需要在另外的那个区域选择,并缩小K的范围。
总之,就是按照分区的情况,合理选择范围。
#include <bits/stdc++.h>
using namespace std;
#define FR freopen("in.txt", "r", stdin)
#define FW freopen("out.txt", "w", stdout)
typedef long long ll;
ll arr[5000005];
int n, k;
int partition(int l, int r)
{
ll x = arr[r];
int j = l - 1;
for (int i = l; i < r; i++)
{
if (arr[i] <= x)
{
swap(arr[i], arr[++j]);
}
}
swap(arr[r], arr[++j]);
return j;
}
ll kth(int l, int r, int k)
{
int p = partition(l, r);
if (p - l == k)
{
return arr[p];
}
else if (p - l > k)
{
return kth(l, p - 1, k);
}
else if (p - l < k)
{
return kth(p + 1, r, k - (p - l) - 1);
}
}
int main()
{
scanf("%d %d", &n, &k);
for (int i = 0; i < n; i++)
{
scanf("%lld", arr + i);
}
printf("%lld", kth(0, n - 1, k));
return 0;
}
求第K小元素的扩展版本,求第1-K小的元素,仍然是拓扑排序,注意处理两个子区间的时候,注意分区位置 p p p不能包含在左区间中,否则就会出现死循环。
#include <bits/stdc++.h>
using namespace std;
#define FR freopen("in.txt", "r", stdin)
#define FW freopen("out.txt", "w", stdout)
typedef long long ll;
int n, k;
struct Entry
{
int id;
int ch;
int ma;
int en;
bool operator<(const Entry &o) const
{
if (ch + ma + en == o.ch + o.ma + o.en)
if (ch == o.ch)
return id < o.id;
else
return ch > o.ch;
else
return ch + ma + en > o.ch + o.ma + o.en;
}
} e[305];
int partition(int l, int r)
{
Entry en = e[r];
int j = l - 1;
for (int i = l; i < r; i++)
{
if (e[i] < en)
swap(e[i], e[++j]);
}
swap(e[++j], e[r]);
return j;
}
void akth(int l, int r, int k)
{
if (l > r || k == 0)
return;
int p = partition(l, r);
if (p - l >= k)
{
akth(l, p - 1, k);
}
else
{
akth(l, p - 1, p - l);
printf("%d %d\n", e[p].id, e[p].ch + e[p].en + e[p].ma);
akth(p + 1, r, k - p + l - 1);
}
}
int main()
{
scanf("%d", &n);
for (int i = 0; i < n; i++)
{
e[i].id = i + 1;
scanf("%d %d %d", &e[i].ch, &e[i].ma, &e[i].en);
}
akth(0, n - 1, 5);
return 0;
}