0x05 排序

0x05 排序

在程序设计中,经常使用到以下这些排序算法,这里把它们分成三类:

1.选择排序、插入排序、冒泡排序

2.堆排序、归并排序、快速排序

3.桶排序、计数排序、基数排序

前两类是基于比较的算法,第一类排序算法的时间复杂度是 O ( n 2 ) O(n^2) O(n2),第二类排序算法的时间复杂度是 O ( n l o g n ) O(nlogn) O(nlogn)

第三类算法换了一种思路,它们不直接比较大小,而是对被排序的数值采取按位划分、分类映射等处理方式,其时间复杂度不仅与n有关,还与数值的大小范围m有关。

1.离散化

排序算法的第一个应用是离散化。通俗来讲,“离散化”就是把无穷大集合中的若干个元素映射为有限集合以便于统计的方法

例如在很多情况下,问题的定义范围在整数集合 z \mathbb{z} z ,但是只涉及其中m个有限数值,并且与数值的绝对大小无关(只把这些数值作为代表,或只与它们的相对顺序有关)。此时,我们就可以把整数集合 z \mathbb{z} z 中的这m个整数与1~m建立映射关系。如果有一个时间、空间复杂度与数值范围 z \mathbb{z} z 的大小有关的算法,在离散化后,该算法的时间、空间复杂度就降低为与m有关。

具体来说,假设问题涉及int范围内的n个整数a[1]~a[n],这n个整数可能有重复,去重以后含有m个整数。我们要把每个整数a[i]用一个1~ m之间的整数代替,并且保持大小顺序不变,即例如如果a[i]小于a[j],则代替a[i]的整数也小于代替a[j]的整数。

void discrete() // 离散化
{
    sort(a + 1, a + n + 1);
    for (int i = 1; i <= n; ++i)
    {
        if (i == 1 || a[i] != a[i - 1])
            b[++m] = a[i];
    }
}

int query(int x) // 查询x映射为1~m之间的哪个整数
{
    return lower_bound(b + 1, b + m + 1, x) - b;
}

离散化的步骤为:原始数据排序,去重建立新的映射索引,然后可以通过lower_bound获得每个数新的映射索引。

2.中位数

动态维护中位数:依次读入一个整数数列,每当已经读入的整数个数为奇数时,输入已读入的整数构成的序列的中位数。

“对顶堆”的在线做法(读入的同时即时计算答案)。

为了动态维护中位数,我们可以建立两个二叉堆:一个大根堆,一个小根堆。在依次读入这个整数序列的过程中,设当前序列长度为M,我们始终保持:

1.序列中从小到大排序排名为 1 ∼ M / 2 1\sim M/2 1M/2的整数储存在大根堆中;

2.序列中从小到大排序排名为 M / 2 + 1 ∼ M M/2+1\sim M M/2+1M的整数储存在小根堆中。

任何时候,如果一个堆中元素个数过多,打破了这个性质,就取出该堆的堆顶,放入另一个堆中。序列的中位数就是小根堆的堆顶。

读入一个新的数值X,如果X小于中位数,放入大根堆,反之,放入小根堆。

“链表+Hash”的离线做法(完成所有读入后进行计算然后再统一输出)将在0x13节中讲解。

3.第k大数

快速排序在每一轮挑选一个基准元素,并让其他比它大的元素移动到数列一边,比它小的元素移动到数列的另一边,从而把数列拆解成了两个部分

void quick_sort(int a[],int i,int j)
{
    if(i>=j)
        return;
   	int val=a[i];
    int l=i,r=j;
    while(l!=r)
    {
        while(a[r]>=val&&l<r)
            r--;
        while(a[l]<=val&&l<r)
            l++;
        if(l<r)
            swap(a[l],a[r]);
	}
    a[i]=a[l];
    a[l]=val;
    quick_sort(a,i,l-1);
    quick_sort(a,l+1,j);
}

时间复杂度O(nlogn)

求第k大数思路:

快速排序在每一轮挑选一个基准元素,并让其他比它大的元素移动到数列一边,比它小的元素移动到数列的另一边,从而把数列拆解成了两个部分。实际上我们每次在选取基准值后,可以统计出大于基准值的数的数量cnt,如果k<=cnt,我们就在左半段寻找第k大数;如果k>cnt,就在右半段寻找第k-cnt大数。然后不断递归求解。平均时间复杂度n+n/2+n/4+n/8+...+1=O(n)

int findk(int a[],int i,int j,int k)
{
   	int val=a[i];
    int l=i,r=j;
    while(l!=r)
    {
        while(a[r]<=val&&l<r)
            r--;
        while(a[l]>=val&&l<r)
            l++;
        if(l<r)
            swap(a[l],a[r]);
	}
    a[i]=a[l];
    a[l]=val;
    if(k==l)
        return a[l];
    else if(k<l)
        return findk(a,i,l-1,k);
   	else
        return findk(a,l+1,j,k);
}

4.逆序对

对于一个序列a,若 i < j i<j i<j a [ i ] > a [ j ] a[i]>a[j] a[i]>a[j],则称 a [ i ] a[i] a[i] a [ j ] a[j] a[j]构成逆序对。使用归并排序可以在 O ( n l o g n ) O(nlogn) O(nlogn)的时间里求出一个长度为n的序列里逆序对的个数。

归并排序思路:每次把序列二分,递归对左右两边进行排序,然后合并两个有序序列。

void merge(int a[],int b[],int l,int m,int r)
{
    int i=l,j=m+1,k=l;
    while(i<=m&&j<=r)
    {
        if(a[i]>a[j])
            b[k++]=a[j++];
        else
            b[k++]=a[i++];
    }
    while(i<=m)
        b[k++]=a[i++];
    while(j<=r)
        b[k++]=a[j++];
   	for(i=l;i<=r;++i)
        a[i]=b[i];
}
void mergesort(int a[],int b[],int l,int r)
{
    if(l<r)
    {
        int m=(l+r)/2;
        mergesort(a,b,l,m);
        mergesort(a,b,m+1,r);
        merge(a,b,l,m,r);
	}
}

时间复杂度O(nlogn)

合并两个有序序列a[l~m]a[m+1~r],采用两个指针ij分别对两者进行扫描,不断比较两个指针所指向数值a[i]a[j]的数值大小,将小的加入到排序的结果数组中。若小的是a[j],则a[i~m]都比a[j]大,它们都会与a[j]组成逆序对,可以顺便统计到答案上。时间复杂度O(nlogn)

int ans=0;
void merge(int a[],int b[],int l,int m,int r)
{
    int i=l,j=m+1,k=l;
    while(i<=m&&j<=r)
    {
        if(a[i]>a[j])
            b[k++]=a[j++],ans+=m-i+1;
        else
            b[k++]=a[i++];
    }
    while(i<=m)
        b[k++]=a[i++];
    while(j<=r)
        b[k++]=a[j++];
   	for(i=l;i<=r;++i)
        a[i]=b[i];
}
void mergesort(int a[],int b[],int l,int r)
{
    if(l<r)
    {
        int m=(l+r)/2;
        mergesort(a,b,l,m);
        mergesort(a,b,m+1,r);
        merge(a,b,l,m,r);
	}
}

求逆序对的方法还有树状数组,我们将在后续的章节中讲解树状数组的应用。

实际应用:

(1)仅通过交换数组中相邻两个数使得数组变成升序排列,最小的交换次数为数组的逆序对数。

(2)奇数码问题中,比如八数码(3*3的棋盘),两个局面可以相互变换到,当且仅当两个局面下网格的数写成一行n*n-1个元素的序列后(不考虑空格),逆序对个数的奇偶性相同。该结论很容易证明:当空格左右移动时,写成的序列显然不变,上下移动时,相当于某个数和它后(前)边的n-1个数交换了位置,因为n-1是偶数,所以逆序对变化也是偶数。

(3)偶数码问题中,两个局面可以相互变换到,当且仅当两个局面下网格的数写成序列后,“逆序对数之差”和“两个局面下空格所在的行数之差”奇偶性相同。

  • 24
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

谷神星ceres

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值