排序——快速排序

快速排序

过程

与归并排序类似,快速排序也运用了分治的思想。

  1. 分解:将数组 A [ l … r ] A[l \ldots r] A[lr]分解成两个部分 A [ l … m i d − 1 ] A[l \ldots mid-1] A[lmid1] A [ m i d + 1 … r ] A[mid+1 \ldots r] A[mid+1r](全文默认为全闭下标集合)。并且我们要求 A [ l … m i d − 1 ] A[l \ldots mid-1] A[lmid1]中的元素,都小于等于 A [ m i d ] A[mid] A[mid] A [ m i d + 1 … r ] A[mid+1 \ldots r] A[mid+1r]都大于 A [ m i d ] A[mid] A[mid](边界条件不严格,可以根据情况调换)。这个过程的名字叫做分区
  2. 解决:通过递归调用,解决两个区间 A [ l … m i d − 1 ] A[l \ldots mid-1] A[lmid1] A [ m i d + 1 … r ] A[mid+1 \ldots r] A[mid+1r]的子问题。
  3. 合并:因为左右两边都是有序的,并且左区间的元素都小于右区间的元素,因此整个数组就是有序的,因此不需要合并问题。

下面的伪代码实现快速排序:

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 RANDOMIZEDPARTITION函数即可。

随机化快速排序的时间复杂度分析

参考《算法导论》第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中几乎很少让你写快速排序的模板,但是快速排序的思想是很重要的。

P1923

寻找第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;
}

P1093

求第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;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值