3种简单排序


1、简单选择排序
  简单选择排序是最简单直观的一种算法,基本思想为每一趟从待排序的数据元素中选择最小(或最大)的一个元素
作为首元素,直到所有元素排完为止,简单选择排序是不稳定排序。
  在算法实现时,每一趟确定最小元素的时候会通过不断地比较交换来使得首位置为当前最小,交换是个比较耗时的
操作。其实我们很容易发现,在还未完全确定当前最小元素之前,这些交换都是无意义的。
    我们可以通过设置一个变量min,每一次比较仅存储较小元素的数组下标,当轮循环结束之后,那这个变量存储的就是
当前最小元素的下标,此时再执行交换操作即可。

void select_sort()
{
    for (int i = 0; i < n; i ++ )
    {
        int k = i;
        for (int j = i+1; j < n; j ++ )
        {
            if (a[j] < a[k])
                k = j;
        }
        swap(a[i], a[k]);
    }

}


时间复杂度分析:
  简单选择排序通过上面优化之后,无论数组原始排列如何,比较次数是不变的;
对于交换操作,在最好情况下也就是数组完全有序的时候,无需任何交换移动;
在最差情况下,也就是数组倒序的时候,交换次数为n-1次。
    综合下来,时间复杂度为O(n²)
2、冒泡排序
  冒泡排序的基本思想是,对相邻的元素进行两两比较,顺序相反则进行交换。
这样,每一趟会将最小或最大的元素“浮”到顶端,最终达到完全有序。
    图示:

11.png


    在冒泡排序的过程中,如果某一趟执行完毕,没有做任何一次交换操作,
比如数组[5,4,1,2,3],执行了两次冒泡,也就是两次外循环之后,分别将5和4调整到最终位置[1,2,3,4,5]。
此时,再执行第三次循环后,一次交换都没有做,这就说明剩下的序列已经是有序的,排序操作也就可以完成了。 
代码实现:
 

void bubble_sort()
{
    for (int i = n-1; i >= 1; i -- )
    {
        bool flag = true;
        for (int j = 1; j <= i; j ++ )
            if (a[j-1] > a[j])
            {
                swap(a[j-1], a[j]);
                flag = false;
            }
        if (flag) return;
    }
}


时间复杂度分析:
  根据上面这种冒泡实现,若原数组本身就是有序的(这是最好情况),仅需n-1次比较就可完成;
若是倒序,比较次数为 n-1+n-2+...+1=n(n-1)/2,交换次数和比较次数等值。所以,其时间复杂度依然为O(n²)。
   综合来看,冒泡排序性能还还是稍差于上面那种选择排序的。
3、直接插入排序
   基本思想是:每一步将一个待排序的记录,插入到前面已经排好序的有序序列中去,直到插完所有元素为止。

12.png


代码实现:
    

void insert_sort()
{
    for (int i = 1; i < n; i ++ )
    {
        int x = a[i];
        int j = i-1;

        while (j >= 0 && x < a[j])
        {
            a[j+1] = a[j];
            j -- ;
        }
        a[j+1] = x;
    }
}


时间复杂度分析:
  简单插入排序在最好情况下,需要比较n-1次,无需交换元素,时间复杂度为O(n);
在最坏情况下,时间复杂度依然为O(n²)。
    但是在数组元素随机排列的情况下,插入排序还是要优于上面两种排序的。
注:
    以上排序算法是最基本的三种算法(简单选择,冒泡,插入),这三种排序算法的时间复杂度均为O(n²)。
二、希尔排序
    希尔排序是希尔(Donald Shell)于1959年提出的一种排序算法。
    希尔排序也是一种插入排序,它是简单插入排序经过改进之后的一个更高效的版本,也称为缩小增量排序,
同时该算法是冲破O(n²)的第一批算法之一。
基本思想
  希尔排序是把记录按下标的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,
每组包含的关键词越来越多,当增量减至1时,整个文件恰被分成一组,算法便终止。
  简单插入排序很循规蹈矩,不管数组分布是怎么样的,依然一步一步的对元素进行比较,移动,插入,
比如[5,4,3,2,1,0]这种倒序序列,数组末端的0要回到首位置很是费劲,比较和移动元素均需n-1次。
    而希尔排序在数组中采用跳跃式分组的策略,通过某个增量将数组元素划分为若干组,然后分组进行插入排序,
随后逐步缩小增量,继续按组进行插入排序操作,直至增量为1。
    希尔排序通过这种策略使得整个数组在初始阶段达到从宏观上看基本有序,小的基本在前,大的基本在后。
然后缩小增量,到增量为1时,其实多数情况下只需微调即可,不会涉及过多的数据移动。
  我们来看下希尔排序的基本步骤,在此我们选择增量gap=length/2,缩小增量继续以gap=gap/2的方式,
这种增量选择我们可以用一个序列来表示,{n/2,(n/2)/2...1},称为增量序列。
    希尔排序的增量序列的选择与证明是个数学难题,我们选择的这个增量序列是比较常用的,也是希尔建议的增量,
称为希尔增量,但其实这个增量序列不是最优的。此处我们做示例使用希尔增量。

13.png


    在希尔排序的理解时,我们倾向于对于每一个分组,逐组进行处理,
但在代码实现中,我们可以不用这么按部就班地处理完一组再调转回来处理下一组(这样还得加个for循环去处理分组)
    比如[5,4,3,2,1,0] ,首次增量设gap=length/2=3,则为3组[5,2],[4,1],[3,0],实现时不用循环按组处理,
我们可以从第gap个元素开始,逐个跨组处理。
    同时,在插入数据时,可以采用元素交换法寻找最终位置,也可以采用数组元素移动法寻觅。
    希尔排序的代码比较简单,如下:

 

void shell_sort()
{
    for (int gap = n >> 1; gap; gap >>= 1)
    {
        for (int i = gap; i < n; i ++ )
        {
            int x = a[i];
            int j;
            for (j = i; j >= gap && a[j-gap] > x; j -= gap)
                a[j] = a[j-gap];
            a[j] = x;
        }
    }
}


时间复杂度分析:
  希尔排序中对于增量序列的选择十分重要,直接影响到希尔排序的性能。
  我们上面选择的增量序列{n/2,(n/2)/2...1}(希尔增量),其最坏时间复杂度依然为O(n²),一些经过优化的增量
序列如Hibbard,经过复杂证明可使得最坏时间复杂度提升为O(n的3/2)。
三、堆排序
    堆排序是利用堆这种数据结构而设计的一种排序算法,堆排序是一种选择排序。
它的最坏,最好,平均时间复杂度均为O(nlogn),它也是不稳定排序。
首先简单了解下堆结构:
  堆是具有以下性质的完全二叉树:每个结点的值都大于或等于其左右孩子结点的值,称为大顶堆;
或者每个结点的值都小于或等于其左右孩子结点的值,称为小顶堆。
  如下图:

14.png


    同时,我们对堆中的结点按层进行编号,将这种逻辑结构映射到数组中就是下面这个样子:

15.png


该数组从逻辑上讲就是一个堆结构,我们用简单的公式来描述一下堆的定义就是:
    大顶堆:arr[i] >= arr[2i+1] && arr[i] >= arr[2i+2]  
    小顶堆:arr[i] <= arr[2i+1] && arr[i] <= arr[2i+2]  
堆排序基本思想及步骤
  堆排序的基本思想是:将待排序序列构造成一个大顶堆,此时,整个序列的最大值就是堆顶的根节点。
将其与末尾元素进行交换,此时末尾就为最大值。然后将剩余n-1个元素重新构造成一个堆,这样会得到n个元素的
次小值。如此反复执行,便能得到一个有序序列。
步骤一:构造初始堆。
        将给定无序序列构造成一个大顶堆(一般升序采用大顶堆,降序采用小顶堆)。
  a.假设给定无序序列结构如下:

16.png


    b.此时我们从最后一个非叶子结点开始(叶结点自然不用调整,第一个非叶子结点arr.length/2-1=5/2-1=1,
也就是下面的6结点),从左至右,从下至上进行调整。

17.png


    c.找到第二个非叶节点4,由于[4,9,8]中9元素最大,4和9交换。

18.png


    这时,交换导致了子根[4,5,6]结构混乱,继续调整,[4,5,6]中6最大,交换4和6。

19.png


    此时,我们就将一个无需序列构造成了一个大顶堆。
步骤二 将堆顶元素与末尾元素进行交换,使末尾元素最大。然后继续调整堆,再将堆顶元素与末尾元素交换,
       得到第二大元素。如此反复进行交换、重建、交换。
    a.将堆顶元素9和末尾元素4进行交换:

20.png


    b.重新调整结构,使其继续满足堆定义:

21.png


    c.再将堆顶元素8与末尾元素5进行交换,得到第二大元素8

22.png


    d.后续过程,继续进行调整,交换,如此反复进行,最终使得整个序列有序。

23.png


简单总结下堆排序的基本思路:
  a.将无需序列构建成一个堆,根据升序降序需求选择大顶堆或小顶堆;
  b.将堆顶元素与末尾元素交换,将最大元素"沉"到数组末端;
  c.重新调整结构,使其满足堆定义,然后继续交换堆顶元素与当前末尾元素,反复执行调整+交换步骤,
直到整个序列有序。
时间复杂度分析:
    堆排序是一种选择排序,整体主要由构建初始堆+交换堆顶元素和末尾元素并重建堆两部分组成。
其中构建初始堆经推导复杂度为O(n),在交换并重建堆的过程中,需交换n-1次,而重建堆的过程中,
根据完全二叉树的性质,[log2(n-1),log2(n-2)...1]逐步递减,近似为nlogn。
    所以堆排序时间复杂度一般认为就是O(nlogn)级。

void down(int u)
{
    int t = u;
    if (u<<1 <= n && h[u<<1] < h[t]) t = u<<1;
    if ((u<<1|1) <= n && h[u<<1|1] < h[t]) t = u<<1|1;
    if (u != t)
    {
        swap(h[u], h[t]);
        down(t);
    }
}

int main()
{
    for (int i = 1; i <= n; i ++ ) cin >> h[i];
    for (int i = n/2; i; i -- ) down(i);
    while (true)
    {
        if (!n) break;
        cout << h[1] << ' ';
        h[1] = h[n];
        n -- ;
        down(1);
    }
    return 0;
}


四、归并排序
基本思想
  归并排序(merge-sort)是利用归并的思想实现的排序方法,该算法采用经典的分治策略
(分治法将问题分(divide)成一些小的问题然后递归求解,而治(conquer)的阶段则将分的阶段得到的各答案"修补"
在一起,即分而治之)。
分而治之

24.png


    可以看到这种结构很像一棵完全二叉树,本文的归并排序我们采用递归去实现(也可采用迭代的方式去实现)。
分阶段可以理解为就是递归拆分子序列的过程,递归深度为log2n。
合并相邻有序子序列
  再来看看治阶段,我们需要将两个已经有序的子序列合并成一个有序序列,比如上图中的最后一次合并,
要将[4,5,7,8]和[1,2,3,6]两个已经有序的子序列,合并为最终序列[1,2,3,4,5,6,7,8],
来看下实现步骤。

25.png

代码实现:

void merge_sort(int q[], int l, int r)
{
    int tmp[100006];
    if (l >= r) return;

    int mid = l + r >> 1;
    merge_sort(q, l, mid);
    merge_sort(q, mid + 1, r);

    int k = 0, i = l, j = mid + 1;
    while (i <= mid && j <= r)
        if (q[i] <= q[j]) tmp[k ++ ] = q[i ++ ];
        else tmp[k ++ ] = q[j ++ ];

    while (i <= mid) tmp[k ++ ] = q[i ++ ];
    while (j <= r) tmp[k ++ ] = q[j ++ ];

    for (i = l, j = 0; i <= r; i ++, j ++ ) q[i] = tmp[j];
}


时间复杂度分析:
    归并排序是稳定排序,它也是一种十分高效的排序,能利用完全二叉树特性的排序一般性能都不会太差。
java中Arrays.sort()采用了一种名为TimSort的排序算法,就是归并排序的优化版本。
    从上文的图中可看出,每次合并操作的平均时间复杂度为O(n),而完全二叉树的深度为|log2n|。
总的平均时间复杂度为O(nlogn)。
    而且,归并排序的最好,最坏,平均时间复杂度均为O(nlogn)。
五、快速排序
    快速排序由C. A. R. Hoare在1962年提出。
    它的基本思想是:通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的
所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个
数据变成有序序列。
基本步骤之三数取中:
  在快排的过程中,每一次我们要取一个元素作为枢纽值,以这个数字来将序列划分为两部分。
  在此我们采用三数取中法,也就是取左端、中间、右端三个数,然后进行排序,将中间数作为枢纽值。

27.png

28.png

29.png


代码实现:

void quick_sort(int l, int r)
{
    if (l >= r) return ;

    int x = a[l+r>>1], i = l-1, j = r+1;
    while (i < j)
    {
        while (a[++ i] < x);
        while (a[-- j] > x);
        if (i < j) swap(a[i], a[j]);
    }
    sort(l, j), sort(j+1, r);
}


时间复杂度分析:
    快速排序是一种交换类的排序,它同样是分治法的经典体现。
在一趟排序中将待排序的序列分割成两组,其中一部分记录的关键字均小于另一部分。
然后分别对这两组继续进行排序,以使整个序列有序。在分割的过程中,枢纽值的选择至关重要,
本文采取了三位取中法,可以很大程度上避免分组"一边倒"的情况。
    快速排序平均时间复杂度也为O(nlogn)级。

作者:我吃西红柿
链接:https://www.acwing.com/solution/content/47347/
来源:AcWing
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

  • 8
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值