浅谈排序——冒泡、桶排序、快速排序、选择排序

最近开始带下一届的学弟学妹们打ACM,有很多以前没在意过的基础算法,在这里写一下,就当是复习+备课吧,希望到时候不要讲不明白,今天看一下简单的排序算法,(今天的算法都是基于升序排列写的,降序同理)先从桶排开始:

桶排序,嗯是一个简单的算法,“把垃圾扔垃圾桶需要几步?”——“两步,第一步,拿着垃圾;第二步,把垃圾放桶里”,所谓的桶排序,跟上述过程其实是一样的,我们用数组模拟一下,假设我们有5 4 3 2 4 1这6个元素,我们用一个数组bucket[6]来表示桶,则可以画如下示意图:

也就是,用数组对应下标表示元素的值,数组的某一个值表示该元素的个数 ,这样我们只需要遍历整个数组,如果当前遍历到的位置有值,就取出元素,知道全部取完为止,就像下图:

那么代码就很简单了,直接给出:

//桶排序;
#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;

int main()
{
    int a[100],bucket[1000000],n;
    while(cin>>n)
    {
        int _max = -1,_min = 0x3f3f3f3f;
        memset(bucket,0,sizeof bucket);     //清空桶;
        for(int i = 0; i < n; i++)
        {
            cin>>a[i];
            bucket[a[i]]++;
            _max = max(_max,a[i]);
            _min = min(_min,a[i]);
        }
        for(int i = _min; i <= _max; i++)   //输出排序后的序列;
        {
            while(bucket[i]--)
            {
                cout<<i<<" ";
            }
        }
        cout<<endl;
    }
    return 0;
}

可是桶排序有限制啊,首先,我们必须知道桶的大小——也就是待排序序列元素的取值范围范围过大可能会内存不够!其次,不能出现负数,因为数组下标没有负数,最后,不能有小数,因为不存在bucket[1.5]这样的东东,所以尽管复杂度是O(n)级别的,我们却不能每次都愉快地使用它,所以我们介绍一下冒泡——暴力排序!

冒泡排序嘛,就是每次找一个优先级最低的数,归并到一组的最前面,称为有序组,而另一组就是无序组了,当所有无序组的数归并到有序组里面的时候,排序就完成了!怎么样是不是很简单?下面我们还是画图模拟一下:

那么每一次归并,又发生了什么呢?以第一次归并为例:

怎么样?从无序组到有序组,9是不是向冒泡一样往上浮呢?这就是此算法名称的由来 ,冒泡算法其实是,一共进行n-1趟,每一趟从无序组中选择优先级最高的数加到有序组里去,这样估算的时间复杂度为O(n^2),可以标记是否进行了交换来优化此算法,比如上图其实执行八趟就完成了排序,但是其实没有必要,因为优化不了多少,而且我们有快排啊...先把冒泡的代码给一下:

//冒泡排序;
#include<iostream>
#include<algorithm>
using namespace std;

int main()
{
    int a[100],n;
    while(cin>>n)
    {
        for(int i = 0; i < n; i++)
            cin>>a[i];
        for(int i = 0; i < n-1; i++)    //n-1趟比较;
            for(int j = 0; j < n-i-1; j++)    //每趟只用从无序组找就行了,所以只有n-i-1次比较;
            {
                if(a[j] > a[j+1])
                    swap(a[j],a[j+1]);
            }
        for(int i = 0; i < n; i++)
            cout<<a[i]<<" ";
        cout<<endl;
    }
    return 0;
}

然后就是快速排序了,都说了是快速排序,肯定是有什么过人之处了吧,对的,它能够在O(nlogn)的复杂度完成排序,虽然相较桶排慢但是没有任何限制,不管是什么类型什么范围,统统都可以搞定,而且在大数据的条件下,快排比冒泡的优势可不是一星半点!让我们一起来看看快速排序的思想:

第一步,确定基准数,我们取序列第一个元素为基准数;

第二步,两个指针i,j,i指向第一个元素,j指向最后一个元素;

第三步,先从j开始找,找到第一个小于基准数的元素停止移动,否则j不断向前移动;

第四步,从i开始找,找到第一个大于基准数的元素停止移动并与刚刚找到的j位置的数交换,否则不断向后移动;

第五步,若第三、四步中,i与j重合,立即停止寻找并让基准数与该位置上的数交换;

第六步,基准数交换后,对于基准数左、右两边的区间,重复第一至五步即可完成排序

快速排序的思想其实就是对于一个基准数,将比它小的数放在左边,比它大的数放右边,最后将它归位(放中间)即可,基准数归位后,无论其左、右区间如何变动,它的位置不受影响,因此可以分区间进行归位,每次把一个大区间化为两个小区间处理,有点像二分是吧?但其实准确的说是分治的思想,当每个区间长度都是1时,排序完成,代码如下:

//快速排序;
#include<iostream>
#include<algorithm>
using namespace std;

void Quick_sort(int a[],int left,int right) //快速排序;
{
    if(left > right)    return;
    int i = left,j = right;
    int base = a[left];     //记录基准数;
    while(i != j)
    {
        while(i < j && a[j] >= base)    //先从基准数的对面开始找;
            j--;
        while(i < j && a[i] <= base)    //再从另一边找;
            i++;
        if(i < j)               //在i和j未相遇时;
            swap(a[i],a[j]);    //交换两个数;
    }
    swap(a[left],a[i]);         //将基准数归位;
    Quick_sort(a,left,i-1);     //处理左区间;
    Quick_sort(a,i+1,right);    //处理右区间;
}

int main()
{
    int a[100],n;
    while(cin>>n)
    {
        for(int i = 0; i < n; i++)
            cin>>a[i];
        Quick_sort(a,0,n-1);
        for(int i = 0; i < n; i++)
            cout<<a[i]<<" ";
        cout<<endl;
    }
    return 0;
}

为什么一定要先从基准数对面的方向开始找呢?严格证明我证明不了,但是可以举反例,比如下面这组数据:

6 1 2 7 9,若先从左边(基准数所在的一边)开始找:

基准数为6时,执行顺序:6 1 2 7 9 -> 7 1 2 6 9,此时基准数已归位,但是基准数两边的数并不满足左边的均小于等于基准数,右边的均大于等于基准数的性质,这样的排序将永远无法得到正确结果!因此一定要从基准数对面先开始找!

刚刚写的时候忘了,现在补一下选择排序算法

所谓选择排序,其实也是一种非常简单的排序算法,它将序列分成两组,有序组和无序组,开始有序组长度为0,每次从无序组中选择一个优先级最高的元素,直接插入有序组最后面,依次执行直到所有元素都插入了有序组,算法结束,这个算法可以看成是冒泡算法的另一种形式,冒泡是两两比较最后无序组中优先级最低的元素插入有序组的最前面,而选择排序是,无序组中优先级最高的元素插入有序组的最后面,所以选择排序的复杂度也是O(n^2),选择排序代码如下:

//选择排序;
#include<algorithm>
#include<iostream>
using namespace std;

int main()
{
    int n,a[100];
    while(cin>>n)
    {
        for(int i = 0; i < n; i++)
            cin>>a[i];
        for(int i = 0; i < n-1; i++)
        {
            int _min = i;              //标记这次应该插入的位置;
            for(int j = i+1; j < n; j++)    //找最小值;
            {
                if(a[_min] > a[j])
                    _min = j;
            }
            if(_min != i)               //找到最小值,就插入有序组后面;
                swap(a[_min],a[i]);     //否则说明该位置恰好处于有序组,插入位置应该在其后面;
        }
        for(int i = 0; i < n; i++)
            cout<<a[i]<<" ";
        cout<<endl;
    }
    return 0;
}

还有个堆排序貌似没写,时间复杂度为O(nlogn),感觉并没有快排用得频繁而且复杂度也只是和快排一样优秀,也许以后再写吧,先睡了,狗命要紧。。。

猜你喜欢

转载自blog.csdn.net/AAMahone/article/details/82830105