排序算法总结

本文详细介绍了排序算法,包括传统排序算法如冒泡排序,以及对数时间复杂度的快速排序。重点讲解了快速排序的原理、时间复杂度分析和随机化改进,还提到了线性时间复杂度的计数排序和桶排序。这些排序算法在不同的场景下有不同的效率和适用性。

排序算法总结

传统排序算法

传统排序算法其时间复杂度一般为 O ( n 2 ) O(n^2) O(n2)。原理简单实现简单。

冒泡排序

时间复杂度额外空间复杂度稳定
O ( n 2 ) O(n^2) O(n2) O ( 1 ) O(1) O(1)Yes
for(int i = n - 1; i > 0; i--)
	for(int j = 0; j < i; j++)
		if(p[j] > p[j + 1])
			swap(p[j], p[j + 1])

即每次把最大的元素交换上去,需要交换 n − 1 n-1 n1次。

性质:

  • 有多少个逆序对就有多少次交换被执行。
  • 不完全冒泡(指仅仅进行 k k k次冒泡),逆序对数小于等于 k k k的将归零,而大于 k k k的将减去 k k k,每一趟冒泡,总是把当前元素前面最大的交换过去,因此每个元素的逆序数将减一。P6186

对数时间排序算法

对数时间排序算法是基于序理论最优的排序算法,已经被证明不存在基于序理论并且小于对数时间的排序算法,其时间复杂度基本为 O ( n log ⁡ n ) O(n \log n) O(nlogn)

快速排序

快速排序是一种基于归并排序的排序算法,对比归并排序,其没有归并的过程,属于自然归并,因其中掺杂随机化算法,因此只讨论期望时间复杂度。

时间复杂度额外空间复杂度稳定
期 望 O ( n log ⁡ n ) 期望O(n \log n) O(nlogn) O ( n ) O(n) O(n)No
过程

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

  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;
}

线性时间排序算法

线性时间排序算法主要使用了数字的性质,其时间复杂度为 O ( n ) O(n) O(n),可以对值域小的数字进行排序,其一般不稳定。

计数排序

时间复杂度额外空间复杂度稳定
O ( n ) O(n) O(n) O ( n ) O(n) O(n)No

适合元素范围较小的数组排序,扫描一次数组,并把相应的元素计数器+1,然后按照计数器的数量输出计数器的值即可。

计数排序是桶排序的特例应用。

P1271

#include <bits/stdc++.h>
using namespace std;

typedef long long ll;

#define FR freopen("in.txt", "r", stdin)

ll arr[1005];

int main()
{
    int n, m;
    scanf("%d %d", &n, &m);
    for (int i = 0; i < m; i++)
    {
        int id;
        scanf("%d", &id);
        arr[id]++;
    }

    for (int i = 1; i <= n; i++)
    {
        for (int c = 0; c < arr[i]; c++)
        {
            printf("%d ", i);
        }
    }
    return 0;
}

桶排序

时间复杂度额外空间复杂度稳定
O ( n σ ( n ) ) O(n \sigma(n)) O(nσ(n)) O ( n σ ( n ) ) O(n \sigma(n)) O(nσ(n))No

桶排序的概念比较广泛,桶排序的一般思想是将元素放入一个个的桶中,然后在桶内排序,然后将桶排序,类似于捆绑法。 σ ( n ) \sigma(n) σ(n)取决于桶内排序算法

特别的,我们有单桶排序,即一个桶只有单个元素,适用于检查元素完整性。

P1152

#include <bits/stdc++.h>

using namespace std;

#define FR freopen("in.txt", "r", stdin)
#define FW freopen("out.txt", "w", stdout)

bool vis[1001];

int main()
{
    int n;
    scanf("%d", &n);
    int prv = 0;
    for (int i = 0; i < n; i++)
    {
        int val;
        scanf("%d", &val);
        if (abs(val - prv) <= 1000)
            vis[abs(val - prv)] = true;
        prv = val;
    }

    for (int i = 1; i <= n - 1; i++)
    {
        if (!vis[i])
        {
            printf("Not jolly");
            return 0;
        }
    }
    printf("Jolly");
    return 0;
}
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值