【数据结构与算法】data structures & algorithms 第六章:各类常见的排序算法

数据结构与算法系列文章目录

【数据结构与算法】data structures & algorithms 第一章:复杂度分析
【数据结构与算法】data structures & algorithms 第二章:基本概念
【数据结构与算法】data structures & algorithms 第三章:线性数据结构
【数据结构与算法】data structures & algorithms 第四章:树的数据结构
【数据结构与算法】data structures & algorithms 第五章:图的数据结构
【数据结构与算法】data structures & algorithms 第六章:各类常见的排序算法
【数据结构与算法】data structures & algorithms 第七章:散列表算法的初步运用
【数据结构与算法】data structures & algorithms 第八章:红黑树的理解与使用



一、简单排序

  • 前提
    void X_Sort (ElementType A[], int N)
    • 大多数情况下,讨论从小到大的整数排序;
    • N是正整数,表示元素的个数;
    • 只讨论基于比较的排序(>=<有定义);
    • 只讨论内部排序;
    • 稳定性:任意两个相等的数据,排序前后的相对位置不发生改变
    • 没有任何一种排序是任何情况下都表示最好的;

1、冒泡排序

  • 每次遍历比较,将最大的元素移动到数组后面;
void Bubble_Sort(ElementType A[], int N)
{
    for (int P = N - 1; p >= 0; --P)
    {
        flag = 0;
        for (int i = 0; i < P; ++i)
        {
            if (A[i] > A[i + 1])
            {
                Swap(A[i], A[i + 1]);
                flag = 1;
            }
        }
        if (flag == 0) break;  //全程无交换就退出循环
    }
}
  • 最好情况:顺序 T   =   O ( N ) T\ =\ O(N) T = O(N)
    最坏情况:逆序 T   =   O ( N 2 ) T\ =\ O(N^2) T = O(N2)

2、插入排序

  • 每次遍历地拿出一个元素,与其前面的元素比较大小,放入合适的位置
void Insertion_Sort(ElementType A[], int N)
{
    for (int p = 1; p < N; ++p)
    {
        int tmp = A[p];
        for (int i = p; i > 0 && A[i - 1] > tmp; --i)
            A[i] = A[i - 1];  //移出空位;
        A[i] = tmp;  //将该元素放入空位;
    }
}
  • 最好情况:顺序 T   =   O ( N ) T\ =\ O(N) T = O(N)
    最坏情况:逆序 T   =   O ( N 2 ) T\ =\ O(N^2) T = O(N2)

3、时间复杂度下界

  • 对于下标 i   <   j i\ <\ j i < j,如果 A [ i ]   >   A [ j ] A[i]\ >\ A[j] A[i] > A[j],则称 ( i ,   j ) (i,\ j) (i, j)是一对逆序对ub(inversion)
  • 交换2个相邻元素正好消去1个逆序对
  • 插入排序: T ( M , I )   =   O ( N + I ) T(M,I)\ =\ O(N+I) T(M,I) = O(N+I)
    • 如果顺序基本有序,则插入排序简短且高效
  • 定理:任意N个不同元素组成的序列平均具有 N ( N   −   1 ) / 4 N(N\ -\ 1)/4 N(N  1)/4个序列对
  • 定理:任何仅以交换相邻两元素来排序的算法,其平均时间复杂度为 Ω ( N 2 ) \Omega(N^2) Ω(N2)
  • 这意味着:要提高算法效率,我们必须
    • 每次不止1个逆序对;
    • 每次交换相隔较远的2个元素;

4、希尔排序

  • 希尔排序,也称为递减增量排序算法,是插入排序的一种更高效的改进版本,但它是非稳定排序算法

    • 思想:先将整个待排序的记录序列分割成为若干子序列,分别进行直接插入排序,待整个序列中的记录基本有序时,再对全体记录进行依次直接插入排序
  • 定义增量序列 D M   >   D M − 1   > ⋯ > D 1   =   1 D_M\ >\ D_{M-1}\ >\cdots>D_1\ =\ 1 DM > DM1 >>D1 = 1;最后一次排序必须是间隔为1的排序;

  • 对每个 D k D_k Dk进行” D k − D_k- Dk间隔“排序 ( k = M , M − 1 , ⋯   , 1 ) (k=M,M-1,\cdots,1) (k=M,M1,,1)

  • 注意:” D k D_k Dk-间隔“有序的序列,在执行” D k − 1 D_{k-1} Dk1-间隔“排序后,仍然是” D k D_k Dk-间隔“有序的;

  • 原始希尔排序用到的增量序列
    D M   =   ⌊ N / 2 ⌋ ,   D k   =   ⌊ D k + 1 / 2 ⌋ D_M\ =\ \lfloor N/2 \rfloor,\ D_k\ =\ \lfloor D_{k+1}/2 \rfloor DM = N/2, Dk = Dk+1/2

void shell_sort(elementType A[], int N)
{
    //希尔增量排序
    for (int D = N/2; D > 0; D /= 2)
    {
        //插入排序
        for (int P = D; P < N; ++P)
        {
            int tmp = A[P];
            for (int i = P; i >= D && A[i - D] > tmp; i -= D)
            {
                A[i] = A[i - D];
            }
            A[i] = tmp;
        }
    }
}
  • 最坏情况: T   =   Θ ( N 2 ) T\ =\ \Theta (N^2) T = Θ(N2)

  • 增量元素如果不互质,则小增量可能根本不起作用;

在这里插入图片描述

  • 更多增量序列
    • Hibbard增量序列
      • D k   =   2 k − 1 D_k\ =\ 2^k-1 Dk = 2k1 ——相邻元素互质
      • 最坏情况: T   =   Θ ( N 3 / 2 ) T\ =\ \Theta (N^{3/2}) T = Θ(N3/2)
      • 猜想: T a v g   =   O ( N 5 / 4 ) T_{avg}\ =\ O(N^{5/4}) Tavg = O(N5/4)
    • Sedgewick增量序列
      • { 1 , 5 , 19 , 41 , 109 , ⋯   } \{ 1,5,19,41,109, \cdots \} {1,5,19,41,109,}
        —— 9 × 4 i − 9 × 2 i + 1 9\times 4^i-9\times 2^i + 1 9×4i9×2i+1 4 i − 3 × 2 i + 1 4^i - 3\times 2^i + 1 4i3×2i+1
      • 猜想: T a v g   =   O ( N 7 / 6 ) T_{avg}\ =\ O(N^{7/6}) Tavg = O(N7/6) T w o r s t   =   O ( N 4 / 3 ) T_{worst}\ =\ O(N^{4/3}) Tworst = O(N4/3)

5、堆排序

  • 利用这种数据结构,所设计的排序算法;有两种排序方法:

    • 最小堆:每个结点的值都小于或等于其子结点的值,在算法中用于降序排列
    • 最大堆:每个结点的值都大于或等于其子结点的值,在算法中用于升序排列
  • 最小堆的方式

    • 时间复杂度: T ( N )   =   O ( N log ⁡ N ) T(N)\ =\ O(N\log N) T(N) = O(NlogN)
    • 注意的是,这里需要额外 O ( N ) O(N) O(N)空间,并且复制元素需要时间;
void Heap_Sort(elementType A[], int N)
{
    buildMinHeap(A);  //O(N)
    for (int i = 0; i < N; ++i)
    {
        tmpA[i] = deleteMin(A);  //O(logN)
    }
    for (int i  = 0; i < N; ++i)
    {
        A[i] = tmpA[i];
    }
}
  • 最大堆的方式
    • 定理:堆排序处理N个不同元素的随机排列的平均比较次数是 2 N log ⁡ N   −   O ( N log ⁡ log ⁡ N ) 2N\log N\ -\ O(N\log \log N) 2NlogN  O(NloglogN)
    • 虽然堆排序给出最佳平均时间复杂度,但实际效果不如用Sedgewick增量序列的希尔排序;
void Heap_Sort(elementType A[], int N)
{
    for (int i = N / 2 - 1; i >= 0; --i)
    {
        PrecDown(A, i, N);
    }
    for (int i = N - 1; i > 0; --i)
    {
        Swap(&A[0], &A[i]);
        PrecDown(A, 0, i);
    }
}

6、归并排序

6.1、核心:有序子列的归并

  • 对两个有序的子列进行归并,成为一个更大的有序子列
  • 归并的时间复杂度为: T ( N )   =   O ( N ) T(N)\ =\ O(N) T(N) = O(N)
  • 核心程序 Merge()
void Merge (elementType A[], elementType tmpA[], int L, int R, int rightEnd)
{
    //L为左边起始位置,R为右边起始位置,rightEnd为右边终点位置
    int leftEnd = R - 1;  //左边终点位置,假设左右两列紧挨着
    int tmp = L;  //结果数组的初始位置
    numElements = rightEnd - L + 1;
    while (L <= leftEnd && R <= rightEnd)
    {
        if (A[L] <= A[R]) tmpA[tmp++] = A[L++];
        else tmpA[tmp++] = A[R++];
    }
    while (L <= leftEnd) tmpA[tmp++] = A[L++];
    while (R <= rightEnd) tmpA[tmp++] = A[R++];
    for (int i = 0; i < numElements; ++i, --rightEnd) A[rightEnd] = tmpA[rightEnd];
}

6.2、递归算法

  • 采用递归的方式实现归并排序,该算法是稳定的
void MSort (elementType A[], elementType tmpA[], int L, int rightEnd)
{
    int center;
    if (L < rightEnd)
    {
        center = (L + rightEnd) / 2;
        MSort (A, tmpA, L, center);
        MSort (A, tmpA, center + 1, rightEnd);
        Merge (A, tmpA, L, center + 1, rightEnd);
    }
}
  • 采用分而治之的方法,若整个子列的时间为 T ( N ) T(N) T(N),则左右两个子列的时间为 T ( N / 2 ) T(N/2) T(N/2)
    故时间复杂度为 T ( N ) = T ( N / 2 ) + T ( N / 2 ) + O ( N ) = O ( N log ⁡ N ) T(N) = T(N/2) + T(N/2) + O(N) = O(N \log N) T(N)=T(N/2)+T(N/2)+O(N)=O(NlogN)

  • 算法格式要求,函数名首字母大写,形参两个,一为原数据域,二为元素个数

  • 统一函数接口

void Merge_sort (elementType A[], int N)
{
    elementType* tmpA = new elementType[N];
    if (tmpA != NULL)
    {
        MSort (A, tmpA, 0, N - 1);
        delete[] tmpA;
    }
    else Error("空间不足");
}
  • 声明临时数组 tmpA是在最外层进行的,如果放在 Merge中进行,将会进行多余的new操作和delete操作

  • 在最外层声明

在这里插入图片描述

  • 在Merge中声明

在这里插入图片描述

6.3、非递归算法

  • 非递归的方式实现归并排序,是稳定的;
    每每一对进行归并排序,最终分为左右子列进行归并排序,额外空间复杂度为 O ( N ) O(N) O(N)

在这里插入图片描述

void Merge_pass (elementType A[], elementType tmpA[], int N, int length)
{
    //length为当前有序子列的长度
    for (int i = 0; i <= N - 2 * length; i += 2 * length)
        Merge1 (A, tmpA, i, i + length, i + 2 * length - 1);
    //归并最后2个子列
    if (i + length < N)
        Merge1 (A, tmpA, i , i + length, N - 1);  //将A中元素归并到tmpA中
    //最后只剩1个子列的情况
    else
        for (int j = i; j < N; ++j) tmpA[j] = A[j];
}
  • 统一函数接口
void Merge_sort (elementType A[], int N)
{
    elementType* tmpA = new elementType[N];
    if (tmpA != NULL)
    {
        int length = 1;
        while (length < N)
        {
            Merge_pass (A, tmpA, N, length);
            length *= 2;
            Merge_pass (tmpA, A, N, length);  //来回操作,并保证最后的结果是保存在A中
            length *= 2;
        }
        delete[] tmpA;
    }
    else Error("空间不足");
}

7、快速排序

  • 快排采取分而治之的策略,每次将子列分为左右子列;
  • 快排算法最好的情况是,每次正好中分,时间复杂度为 T ( N ) = O ( N log ⁡ N ) T(N)=O(N \log N) T(N)=O(NlogN)
void QuickSort (elementType A[], int N)
{
    pivot = 从A[]中选一个主元;
    将S = { A[] \ pivot } 分成2个独立子集:
        A1 = { a in S | a <= pivot } 和
        A2 = { a in S | a >= pivot };
    A[] = QucikSort (A1, N1) and
                     {pivot} and
               QuickSort (A2, N2);
}
  • 选主元
    主元选得好,快排才会快;
    假设 p i v o t = A [ 0 ] pivot = A[0] pivot=A[0],则时间复杂度为 T ( N ) = O ( N 2 ) T(N) = O(N^2) T(N)=O(N2)

  • 如果使用 r a n d ( ) rand() rand()选取 p i v o t pivot pivot在时间消耗上是不划算的

  • 取头、中、尾的中位数

    • 也可以不局限于三个数的中位数,可以是5、7等
elementType Median3 (elementType A[], int left, int right)
{
    int center = (left + right) / 2;
    if (A[left] > A[center])
        Swap (&A[left], &A[center]);
    if (A[left] > A[right])
        Swap (&A[left] > A[right]);
    if (A[center] > A[right])
        Swap (A[center], A[right]);
    // 现在,A[left] <= A[center] <= A[right]
    Swap (&A[center], &A[right - 1]);  //将pivot放到右边倒数第二个位置
    //只需考虑 A[left + 1] 到 A[right - 2] 的元素
    return A[right - 1];
}
  • 子集划分
    A [ l e f t + 1 ] 到 A [ r i g h t − 2 ] A[left + 1]到A[right - 2] A[left+1]A[right2]中,假设有 i i i指向 A [ l e f t + 1 ] A[left + 1] A[left+1],有 j j j指向 A [ r i g h t − 2 ] A[right - 2] A[right2],而主元 p i v o t = A [ r i g h t − 1 ] pivot = A[right - 1] pivot=A[right1]

    • 先比较 i i i指向的元素与主元 p i v o t pivot pivot,如果比 p i v o t pivot pivot小,则 i = i + 1 i = i + 1 i=i+1向右指向下一个元素,反之若大于 p i v o t pivot pivot,则停下;
    • 后比较 j j j指向的元素与主元 p i v o t pivot pivot,如果比 p i v o t pivot pivot大,则 j = j − 1 j = j - 1 j=j1向左指向上一个元素,反之若小于 p i v o t pivot pivot,则停下;
    • i 与 j i与j ij都停下时,先判断 j − i > 0 j - i > 0 ji>0是否成立,若成立,则交换此时 i i i j j j指向的元素,并继续上述两步操作;反之若不成立,则跳出循环;
    • 此时在循环外,交换 p i v o t pivot pivot i i i指向的元素,至此,主元右边的元素都大于它,主元左边的元素都小于它;
  • 注意的是,当有元素正好等于 p i v o t pivot pivot,选择停下来进行交换;看起来费时间,但是这样能保证主元停在子集的中间位置;如若不这么选择,那么主元可能在两段位置,则快排的速度反而降下来;

  • 小规模数据的处理

    • 快速排序的问题
      • 用递归方法产生的问题;
      • 对小规模的数据(例如N不到100)可能好不如插入排序快;
    • 解决方案
      • 当递归的数据规模充分小,则停止递归,直接调用简单排序(例如插入排序);
      • 在程序中定义一个 c u t o f f cutoff cutoff的阈值,阈值设置的不同,程序的效率也不同;
  • 算法实现

void QuickSort (elementType A[], int left, int right)
{
    int cutoff = 100;
    if (cutoff <= (right - left))
    {
        elementType pivot = Median3 (A, left, right);
        int i = left;
        int j = right - 1;
        for (;;)
        {
            while (A[++i] < pivot) {};
            while (A[--j] > pivot) {};
            if (i < j)
                Swap (&A[i], &A[j]);
            else break;
        }
        Swap (&A[i], &A[right - 1]);
        QuickSort (A, left, i - 1);
        QucikSort (A, i + 1, right);
    }
    else
        Insertion_Sort (A + left, right - left + 1);
}
  • 统一函数接口
void Quick_Sort (elementType A[], int N)
{
    QucikSort (A, 0, N - 1);
}

8、表排序

  • 间接排序
    • 定义一个指针数组作为“表”(table)
      排序的时候,不改变元素的位置,通过该表table的值来进行排序;

在这里插入图片描述

如果仅要求按顺序输出,则输出:

A [ t a b l e [ 0 ] ] ,   A [ t a b l e [ 1 ] ] ,   ⋯   ,   A [ t a b l e [ N − 1 ] ] A[table[0]],\ A[table[1]],\ \cdots,\ A[table[N - 1]] A[table[0]], A[table[1]], , A[table[N1]]

  • 物理排序

    • N个数字的排列由若干个独立的环组成
      在这里插入图片描述

      • 可以得到 table值为3、5、1、0为一个环;2为一个环;7、4、6为一个环;
    • 进行排序时,

      • 先将f的元素放入Temp临时空间,并记录此时A的下标 t m p I n d e x = 0 tmpIndex = 0 tmpIndex=0,$ t a b l e = 3 指 向 A [ 3 ] = a table = 3 指向 A[3] = a table=3A[3]=a,将a的元素放入 A [ 0 ] A[0] A[0]的位置;
      • t a b l e = 1 指 向 A [ 1 ] = d table = 1指向A[1] = d table=1A[1]=d,将 d的元素放入 A [ 3 ] A[3] A[3]的位置;
      • t a b l e = 5 指 向 A [ 5 ] = b table = 5指向A[5] = b table=5A[5]=b,将 b的元素放入 A [ 1 ] A[1] A[1]的位置;
      • t a b l e = 0 指 向 A [ 0 ] table = 0指向A[0] table=0A[0],因为满足 t a b l e = t m p I n d e x table = tmpIndex table=tmpIndex,判定当前的环已经结束了;
      • 开始寻找新的环,直到没有环;
  • 复杂度分析

    • 最好情况:初始即有序
    • 最坏情况:
      • ⌊ N / 2 ⌋ \lfloor N / 2 \rfloor N/2个环,每个环包含2个元素
      • 需要 ⌊ 3 N / 2 ⌋ \lfloor 3 N / 2 \rfloor 3N/2次元素移动
    • 时间复杂度为 T = O ( m N ) T = O(mN) T=O(mN) m m m是每个 A A A元素的复制时间;

9、基数排序

  • 前言

9.1、桶排序(Bucket Sort)

  • 基本思路:

    • 设有 K K K个桶,先扫描序列求出最大值 M a x V MaxV MaxV和最小值 M i n V MinV MinV,则把将区间 [ M i n V ,   M a x V ] [MinV,\ MaxV] [MinV, MaxV]均匀划分为 K K K个区间,每个区间就是一个桶;而后将序列中的元素分配到对应的桶中;
    • 选择任意一种排序算法,对每个桶进行排序;
    • 最后将各个桶中的元素合并成一个大的有序序列;
  • 注意

    • 桶越多,时间效率就越高,但是所占空间就越大;
    • 这是稳定的算法
  • 假设有 N N N个学生,成绩是0到100之间的整数(于是有 M = 101 M=101 M=101个不同的成绩值),这里就可以采用桶排序

在这里插入图片描述

void BucketSort (elementType A[], int N)
{
    count[] 初始化;
    while (读入1个学生成绩grade)
        将该生插入count[grade]链表;
    对每条链表进行排序;
    for (int i = 0; i < M; ++i)
    {
        //链表不为空则输出链表
        if (count[i])
            输出整个count[i]链表;
    }
}
  • 时间复杂度为 T ( N , M ) = O ( M + N ) T(N,M) = O(M + N) T(N,M)=O(M+N)

9.2、计数排序(Counting Sort)

  • 基本思想:
    • 找出待排序的数组中最大和最小的元素;
    • 统计数组中每个值为 i i i的元素出现的次数,存入数组 C C C的第 i i i项;
    • 对所有的计数累加(从 C C C中的第一个元素开始,每一项和前一项相加);
    • 方向填充目标数组:将每个元素 i i i放在新数组的第 C ( i ) C(i) C(i)项,每放一个元素就将 C ( i ) C(i) C(i)减去1;
  • 注意:
    • 这是稳定的算法

9.3、基数排序(Radix Sort)

  • 基本思想:

    • 将整数按位数切割成不同的数字,然后按个位数分别比较;基数排序的方式可以采用 L S D ( L e a s t   S i g n i f i c a n t   D i g i t a l ) LSD(Least\ Significant\ Digital) LSDLeast Significant Digital M S D ( M o s t   S i g n i f i c a n t   D i g i t a l ) MSD(Most \ Significant\ Digital) MSDMost Significant Digital L S D LSD LSD的排序方式由键值的最右值边开始,而 M S D MSD MSD则相反,由键值的最左边开始;
    • M S D MSD MSD:先从高位开始进行排序,在每个关键字上,可采用计数排序;
    • L S D LSD LSD:先从地位开始进行排序,在每个关键字上,可采用桶排序;
  • 案例

在这里插入图片描述

二、部分排序算法的比较

在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值