经典排序算法的实现、理解、总结(C++)


还在不断更新中…

1 快速排序

1.1 基本思想

快速排序分为两步

  1. 划分:选取一个基准数,将小于该数的划分在其左边,大于该数的划分在其右边(基准数通常采用左边界值,理论上可以是数组中的任何一个数);
  2. 递归:将基准数的左边和右边部分采用第一步的方法进行相同的操作(递归边界为左半部分或者右半部分只剩下一个数的时候就返回)

1.2 快速排序实例

百度百科给的快速排序实例挺好的:
https://baike.baidu.com/item/%E5%BF%AB%E9%80%9F%E6%8E%92%E5%BA%8F%E7%AE%97%E6%B3%95/369842?fr=aladdin

1.3 快速排序实现

#include <iostream>
using namespace std;
const int N=1e6+10;
int d[N];
void quick_sort(int d[],int l,int r)
{
    if(l>=r) return;
    int i=l-1,j=r+1,p=d[l+r>>1];//此处选取的中间值是数组中间的那个数 
    //划分
    while(i<j)
    {
        do i++; while(d[i]<p);
        do j--; while(d[j]>p);
        if(i<j) swap(d[i],d[j]);
    }
    //排左边
    quick_sort(d,l,j);
    //排右边
    quick_sort(d,j+1,r);
}
int main()
{
    int n;
    cin>>n;
    for(int i=0;i<n;i++) cin>>d[i];
    quick_sort(d,0,n-1);
    for(int j=0;j<n;j++) cout<<d[j]<<" ";
    return 0;
}

在这里插入图片描述
快排实现的一些bug讨论

1.4 快排速排序应用——寻找第K大的数

题目描述
给定一个长度为 n 的整数数列,以及一个整数 k,请用快速选择算法求出数列从小到大排序后的第 k 个数。

输入格式
第一行包含两个整数 n 和 k。
第二行包含 n 个整数(所有整数均在 1∼109 范围内),表示整数数列。
输出格式
输出一个整数,表示数列的第 k 小数。
数据范围
1≤n≤100000,
1≤k≤n
输入样例:
5 3
2 4 1 5 3
输出样例:
3

题目分析:关于这道题,需要寻找第k大的数。基本想法就是先对数组排序,然后对数组进行遍历,再寻找第k大的,即排好序后,再找,有没有一边排一边找的方法呢?这就可以对应到快排了,快排每次都会划分,如果划分点的位置恰好是我们找的位置,岂不妙哉。而且我们可以只选择包含要找的第k大的数那一部分进行排序查找,丢掉另一半。

#include <iostream>
using namespace std;
const int N=1e6+20;
int d[N];
int n,k;
int quick_sort(int d[],int l,int r,int k)
{
    if(l>=r) return d[l];
    int i=l-1,j=r+1,p=d[l+r>>1];
    while(i<j)
    {
        do i++; while(d[i]<p);
        do j--; while(d[j]>p);
        if(i<j) swap(d[i],d[j]);
    }
    int par=j-l+1;
    if(k>par) return quick_sort(d,j+1,r,k-par);
    return quick_sort(d,l,j,k);
}
int main()
{
    cin>>n>>k;
    for(int i=0;i<n;i++) cin>>d[i];
    cout<<quick_sort(d,0,n-1, k)<<endl;
}

2 归并排序

2.1 基本思想

归并排序的思想分为三步:
1.划分:通常取中间值;
2.排序:分别排好左边部分,再排好右边部分;
3.合并:将排好的左右部分进行合并

2.2 归并排序实例

在这里插入图片描述

2.3 归并排序实现

#include<iostream>
using namespace std;
const int N=1e6+10;
int n,d[N],tmp[N];
void mergesort(int d[],int l,int r)
{
    //递归边界
    if(l>=r) reuturn;
    //划分成两份
    int mid=l+r>>1;
    //左边排序
    mergesort(d,l,mid);
    //右边排序
    mergesort(d,mid+1,r);
    //合并
    int i=l,j=mid+1,k=0;
    while(i<=mid&&j<=r)
    {
        //始终保存当前最小
        if(d[i]<d[j]) tmp[k++]=d[i++];
        else tmp[k++]=d[j++];
    }
    //甚于数据
    while(i<=mid)   tmp[k++]=d[i++];
    while(j<=r) tmp[k++]=d[j++];
}
int main()
{
    cin>>n;
    for(int i=0;i<n;i++) cin>>d[i];
    mergesort(d,0,n-1);
    for(int i=0;i<n;i++) cout<<d[i]<<" ";
    return 0;
}

实现的一些问题:
归并排序要注意边界值的问题,可能会导致无限划分,即

//左边排序
mergesort(d,l,mid);
//右边排序
mergesort(d,mid+1,r);

这里的左边界和右边界确定以及mid的取值
无限划分例子
当l=3,r=4,mid=(3+4)/2=3(向下取整)
依据

mergesort(d,l,mid-1);
mergesort(d,mid,r);

mergesort(d,3,3);
mergesort(d,3,4);

注意!“mergesort(d,3,4)”这一句就出现了无限划分.造成这样的根本原因是mid计算的时候采用向下取整,导致了mid=(l+r)/2的时候mid可能取到l,则mergesort(d,mid,r)就和原来的mergesort(d,l,r)是同一个,而出现了无限划分
依据

mergesort(d,l,mid);
mergesort(d,mid+1,r);

mergesort(d,3,3);
mergesort(d,4,4);

这样就不会出现无限划分,了mid=(l+r)/2的时候mid取到了l的时候,mergesort(d,mid+1,r)保证了另一半的左边界不再是l。
所以为了避免出现上述边界问题,背模板,不瞎写(手动狗头)

2.4 归并排序应用——求逆序对数目

题目描述
给定一个长度为 n 的整数数列,请你计算数列中的逆序对的数量。
逆序对的定义如下:对于数列的第 i 个和第 j 个元素,如果满足 i<j 且 a[i]>a[j],则其为一个逆序对;否则不是。

输入格式
第一行包含整数 n,表示数列的长度。
第二行包含 n 个整数,表示整个数列。
输出格式
输出一个整数,表示逆序对的个数。
数据范围
1≤n≤100000
输入样例:
6
2 3 4 5 6 1
输出样例:
5

解题思路:暴力解法总能求,两个while循环就可以了,时间复杂度为o(n 2 ^2 2),但是这里的话可以采用分治的思想。
例子:
3 6 2 4 2 1 9
将数据分成两半分别为A:3 6 2 4和B:2 1 9,划分点位置mid=(l+r)/2=(0+6)/2=3
对于数据n i _i i和n j _j j构成逆序,分别有三种情况:
1.n 1 _1 1和n 2 _2 2都出现在A部分;
2.n 1 _1 1和n 2 _2 2都出现在B部分;
3.n 1 _1 1和n 2 _2 2分别出现在A、B部分;
因此逆序数目=A内部的逆序数目+B内部的逆序数目+由A和B两部分数字构成的逆序对的数目
上述是不是看着很熟悉,没错这不就是归并排序的思想吗。那么是否需要在求解的过程中将数据排序,还是可以套用归并排序的思想呢?且看看对数据排序的好处,以及排序后是否会对结果造成影响:
1.A和B内部的逆序数目不会受到各自的影响;
2.假设A中的数a 1 _1 1和B中的数b 2 _2 2构成逆序对,a 1 _1 1原来在A里面处在位置“2”,经过对A排序后变成了位置“1”,b 2 _2 2原来在B里面处在位置“1”,经过对B排序后变成了位置“2”,a 1 _1 1和B中的数b 2 _2 2还是构成了逆序对,因为a 1 _1 1位于b 2 _2 2前面且比b 2 _2 2大,这一点不会因为排序而改变。且A和B内部数分别不会因为排序后而使得,A内部的数跑到B后面去了,或者改变大小了。横跨A和B两部分的逆序数对不会因排序而改变
3.排序对于求逆序对有什么好处呢?在计算由A和B两部分数字构成的逆序对的数目其实就是一个归并的过程,如果暴力的进行逐一比较则时间复杂度为O(n 2 ^2 2),但是如果A部分和B部分都事先排好序了会如何呢?请看下面
A:2 3 4 6 B:1 2 9
指针i指向A中的2,指针j指向B中的1
对于2比1大,那么2后面的数一定都与1构成逆序对,因此2后面的数不需要再与1进行比较,逆序对的数目为mid-l+1=4,必有4个数和1构成逆序对;j++,i指向了2,j指向了2,不构成逆序对;i++(也可以j++取决于你怎么设计),i指向了3,j指向了2,3后面的数一定都与2构成逆序对4+(3-1+1)=7;j++,i指向了3,j指向了9,不构成逆序对,i++,i指向了4,j指向了9,不构成逆序对,i++,i指向了6,j指向了9,不构成逆序对;结束,7+3(A中逆序对)+1(B中逆序对)=11
这个过程可以实现了若A和B部分事先有序,则O(n)的复杂度得到了求解由A和B两部分数字构成的逆序对的数目,而且该归并过程可以配合归并排序的归并过程。

#include <iostream>
using namespace std;
const int N=1e6+10;
int a[N],tmp[N],n;
long long mergesort(int a[],int l,int r)
{
    if(l>=r) return 0;
    int mid=(l+r)/2;
    long long num=mergesort(a,l,mid)+mergesort(a,mid+1,r);//避免溢出需要用long long
    int i=l,j=mid+1,k=0;
    while(i<=mid&&j<=r)//计算分别位于两部分的数字所构成的逆序对
    {
        if(a[i]<=a[j]) tmp[k++]=a[i++];
        else//出现逆序
        {
            tmp[k++]=a[j++];
            num+=mid-i+1;//i后面的数字肯定都和a[j]构成逆序
        }
    }
    //剩余数字的处理
    while(i<=mid)
    {
        tmp[k++]=a[i++];
    }
    while(j<=r)
    {
        tmp[k++]=a[j++];
    }
    for(int i=l;i<=r;i++) a[i]=tmp[i-l];
    return num;
}
int main()
{
    cin>>n;
    for(int i=0;i<n;i++) cin>>a[i];
    cout<<mergesort(a,0,n-1);
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值