【第四课】冒泡排序,快速排序(acwing-785)(含思路详解)!!!

 橙色表示步骤 黄色表示重点 紫色表疑问 佳文推荐用好看的蓝色

目录

冒泡排序 

快速排序 

死循环问题:

基准元素的选择:

递归时间复杂度:

空间暴力


冒泡排序 

因为之前学过冒泡排序,在没接触快速排序算法之前这道题我就用冒泡做了。

#include <iostream>
using namespace std;
const int N = 1e5 + 10;
int a[N], n;
void bubble(int a[], int n)
{
    int flag = 1;
    for (int i = 0; i < n; i++) // 有n个数需要排序,所以需要n趟
    {
        for (int j = 0; j < n - i - 1; j++) // 针对每一趟,都要进行n-i-1次比较
        {
            if (a[j] > a[j + 1])
            {
                int tmp = a[j];
                a[j] = a[j + 1];
                a[j + 1] = tmp;
                flag = 0;
            }
        }
        if (flag == 1)
            break; // 倘若一个循环下来都没有交换数据说明本来这个序列就是有序的
    }
}
int main()
{
    cin >> n;
    for (int i = 0; i < n; i++)
    {
        cin >> a[i];
    }
    bubble(a, n);
    for (int i = 0; i < n; i++)
    {
        cout << a[i] << " ";
    }
    return 0;
}

简单说一下冒泡排序的思想:就是有n个元素就进行n趟,每一趟 从第一个元素从前到后把每两个相邻的元素进行大小比较符合条件(升/降)就进行交换顺序的过程。

比如这道题是要从小到大排序,那么代码中的外层循环就是需要遍历完数组的趟数(那自然是几个数排序就是几趟),内层循环就是实现相邻两个数的比较了,比较的次数应该是n-i-1,这是因为有n个数的时候,相邻元素的比较次数只有n-1次(想一个例子,5个数的时候,相邻两个元素比较的话,是不是只有4次比较?),然而每经过一趟就有一个数已经排好序了,所以需要再减去 i (已经经历的趟数)。

注:这里我多在循环开始前定义一个变量flag的目的是:在每一趟有数据进行交换的时候更改flag的值,倘若某一趟过去之后没有数据进行交换,说明整个序列已经是有序的了,这时flag值就不会被更改,就不需要再进行冒泡排序的后续步骤,直接跳出循环。

冒泡排序的时间复杂度为 O(n^2)。

快速排序 

为了寻找更为高效的排序算法,出现了快速排序。

快速排序的思想:①把已知区间(数组)分成两段,②让<x的数在x的左边,>x的数在x的右边,③继续不断地把已知的这两段区间分别 分成两个子区间 [这里是递归思想] 重复即可

注意:当已知区间中只有一个元素(l=r)或者区间非法(l>r)时,直接return

由于我们还是需要不断地修改左右边界来进行划分两段的操作,所以仍然需要定义左右边界 l r。

①把已知区间分成两段 分的原则就是找出一个数记为x作为基准(这个数可以是a[l] a[r] a[mid] 还可以是数组中随机一个数 这四种选择方式)

②让<x的数在x的左边,>x的数在x的右边如何实现这个操作 就是我们快排算法的关键:定义两个变量 i j 表示数组下标,让i 从数组左边开始寻找<x的数,每找到一个之后就停下;j 从数组右边开始寻找>x的数,每找到一个之后停下。

这个操作代码如何实现呢?想让 i 不断地从左向右寻找,让 r 不断地从右向左寻找,每次比较完之后+1或-1即可,通过后置++/--实现。由于我们希望在第一次执行循环时就能够正确地移动(后置++/--) i 和 j 指针(数组下标实质意思就是指针),因此我们需要将它们的初始值设为 l-1 和 r+1

当 i j 并未相遇:即在满足i<j,且i j 又因为满足判断条件停了下来,就将这两个数进行交换。

交换之后继续重复这个步骤进行判断,直到 i 不满足<j 时停止(由此得到循环出口条件)

③继续不断地把已知的这两段区间分别 分成两个子区间 [这里是递归思想] 重复即可注意区间的边界。i j 来作为循环出口,那么两个子区间的边界就通过 i j 的值来展现,当传递左边区间的右边界时,用 i 表示就是 i-1,用 j 表示是 j ;当传递右边区间的左边界时,用 i 表示就是 i,用 j 表示是 j +1

死循环问题:

倘若把数组第一个元素作为基准,就不能在传递子区间边界的时候使用 i 的方式传递。

可视化:以x=a[l]为基准,且quicksort(a,l,i-1),会出现什么问题?一开始 i 指针就要停下不移动(即 i = l ),对于只有两个元素的数组来说,接下来就会退出循环,执行递归,那么递归时就要传递 l i-1,发生了什么,如果 i=l 那么 i-1<l 左边界大于右边界???直接return了;而右边的递归用了quicksort(a,i,r) i=l 导致区间并未改变,造成死循环。


同理当把数组中最后一个元素作为基准,就不能在传递子区间边界的时候使用 j 的方式传递。

可视化:以x=a[r]为基准,且 quicksort(a,l,j),一开始 j 指针就要停下不移动(j=r),对于只有两个元素的数组,执行递归左半边的时候,quicksort(a,l,j) j=r 造成区间未改变,死循环;递归右半边的时候,quicksort(a,j+1,r) j=r,左边界>右边界,直接return了。

// 第一遍写的时候其实没理解死循环的核心。现在改一下。这位博主Xin_Hack 的关于acwing-785的题解文章,对死循环问题讲的非常详细(我都看懂了)感兴趣的可以去看一下。(另外我还看了他的786的题解文章,也是讲的非常详细。好喜欢这样的大佬qaq)

基准元素的选择:

我们通常会选择数组的中间元素作为基准元素。这样可以保证算法的时间复杂度为 O(nlogn)

这时因为选中间元素每次都尽可能保证每次划分都能将数组分成两个长度相等的子数组:由于每个子数组的长度都减半,所以每次递归调用都会将问题规模减半。这意味着算法需要进行 logn 次递归调用才能将问题规模减小到 1。

可以通过反向解释一下原因:如果快速排序算法每次都不能将数组分成两个长度相等的子数组,那么算法的时间复杂度可能会退化为 O(n^2)

举例:

一个长度为 8 的数组 a = [8, 7, 6, 5, 4, 3, 2, 1],选定右边界a[r]为基准元素。(解释为什么不能将数组分成两个长度相等的子数组时,算法的时间复杂度可能会退化为 O(n^2)?)

移动指针 i 和 j。由于恰好所有元素都大于基准元素,所以指针 i 不会移动;指针 j 向左移动,直到找到第一个小于等于基准元素的元素 a[0] = 8。此时,i = 0,j = 0。划分数组。由于 i = j = 0,所以不需要交换任何元素。此时,数组被划分成两部分:quicksort(a,l,j)左边部分为空,quicksort(a,j+1,r)右边部分为 [8, 7, 6, 5, 4, 3, 2, 1]。递归排序。对左边部分进行快速排序(此时为空),对右边部分 [8, 7, 6, 5, 4, 3, 2, 1] 进行快速排序。这时候我们发现每一次都只排好了一个元素,总时间复杂度退化为了O(n^2)。得证。

Xin_Hack 博主还总结了选择中间元素作为基准元素的两种不同情况。感兴趣可以看一下,所提到的内容都在(Acwing 785.快速排序)他的这篇文章中。

代码如下

#include <iostream>
#include <algorithm>
using namespace std;
const int N = 1e5 + 10;
int a[N], n;
void quicksort(int a[], int l, int r)
{
    if (l >= r)
        return;
    int x = a[l], i = l - 1, j = r + 1;
    while (i < j)
    {
        do
        {
            i++;
        } while (a[i] < x);
        do
        {
            j--;
        } while (a[j] > x);
        if (i < j)
            swap(a[i], a[j]);//引头文件algorithm
    }
    quicksort(a, l, j);
    quicksort(a, j + 1, r);
}
int main()
{
    cin >> n;
    for (int i = 0; i < n; i++)
    {
        cin >> a[i];
    }
    int l = 0, r = n - 1;
    quicksort(a, l, r);
    for (int i = 0; i < n; i++)
    {
        cout << a[i] << " ";
    }
    return 0;
}

递归时间复杂度:

在快速排序的每一轮中,都需要选取一个基准元素,然后将待排序序列中小于基准元素的元素放在基准元素的左边,大于基准元素的元素放在基准元素的右边。这一步骤需要O(n)的时间复杂度。

由于函数中包含递归。我们就要补充一下,递归函数计算时间复杂度的方法:

有点复杂。。。实在是暂时不太想了解了(doge) 


空间暴力

另外再提一嘴在acwing视频课里有提到一种浪费空间的一种简单的思路。

创建两个数组,一个数组p 放<x的数,一个数组q 放>x的数,然后再将 p q 数组合并到 a 数组,实现排序。

第一次将p q分好之后,其实还是要对p q这两个数组分别通过递归再多次重复才能实现排序的效果的,我第一次写的时候因为这个写错了。但是接下来又不知道怎么向我们熟悉的那样(因为感觉快排是这样比较熟悉)通过修改 p q ,将p q 中的元素都排好序这样。。。然后在定义的函数中实现排序,在主函数中实现将p q合并。但本蒟蒻真的不知道怎么写了,看好久了。。。(哭)

下面这是bing给出的写法,并没有按我想的那样实现。

#include<iostream>
using namespace std;
const int N=1e5+10;
int a[N],p[N],q[N];
int n;
void mysort(int a[],int l,int r)
{
    if(l>=r)return;
    int x=a[l];
    int j=0,k=0;
    for(int i=l+1;i<=r;i++)//l+1是为了把基准数字排除出去
    {
        if(a[i]<=x)p[j++]=a[i];
        else q[k++]=a[i];
    }
    for(int i=0;i<j;i++)
    {
        a[l+i]=p[i];//为了避免元素错误覆盖,应该从l+i开始放回到a中
    }
    a[l+j]=x;//把基准元素插到二者之间
    for(int i=0;i<k;i++)
    {
        a[l+j+1+i]=q[i];
    }
    mysort(a,l,l+j-1);
    mysort(a,l+j+1,r);
}
int main()
{
    cin>>n;
    for(int i=0;i<n;i++)
    {
        cin>>a[i];
    }
    int l=0,r=n-1;
    mysort(a,l,r);
    
    for(int i=0;i<n;i++)
    {
        cout<<a[i]<<" ";
    }
    return 0;
}

如果有大佬可以实现的话或者说 不能实现的原因的话,求指点(哭)!!!


ok了,就先写到这里了。感觉这篇文章写的时候状态不太好(哭)

有问题的话欢迎指出,非常感谢!!

也欢迎交流建议哦。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值