九大排序算法联系与分析

九大排序算法设计与分析

一切问题的解决都有其相应的方法论做指导。作为程序设计的基本问题,排序算法经过长期的发展已经有了较为成熟的解决方案。排序算法看似基本但是其中却蕴含着算法设计的基本思想。各个排序算法不是孤立的,他们是在科学的理论框架下的产物。本文以排序类算法为媒介,通过学习排序类算法,了解和掌握分析问题与解决问题的基本思路。
编程论到极致,核心非代码,即思想

  • 排序定义:

排序是计算机内经常进行的一种操作,其目的是将一组“无序”的记录序列调整为“有序”的记录序列。分内部排序和外部排序,若整个排序过程不需要访问外存便能完成,则称此类排序问题为内部排序。反之,若参加排序的记录数量很大,整个序列的排序过程不可能在内存中完成,则称此类排序问题为外部排序。内部排序的过程是一个逐步扩大记录的有序序列长度的过程。 – 百度百科

  • 基本排序分类

    排序分类

存储结构设计

#define MAXSIZE  20     //待排顺序表最大长度
typedef  int  KeyType;    // 关键字类型为整数类型
typedef int InfoType;
typedef  struct {
    
    
    KeyType   key;             // 排序关键字
    InfoType  otherinfo;  // 其它数据项
} RcdType;  // 记录类型
typedef  struct {
    
    
    RcdType    r[MAXSIZE+1];   //r[0]闲置,做哨兵
    int length;         //记录个数
} SqList;  //顺序存储,主要排序,很少增删

插入类排序

  1. 直接插入排序

直接插入排序(Straight Insertion Sort)是一种最简单的排序方法,其基本操作是将一条记录插入到已排好的有序表中,从而得到一个新的、记录数量增1的有序表。
——百度百科

思想:将无序块首元素与左侧元素由后向前逐个比较,边比较边后移; 最后定位到一个大于等于待插入元素的记录左侧,填入即可。
直接插入排序

void InsertionSort ( SqList &L) {
    
    
    int i, j;
    for (i = 2; i <= L.length; ++i) {
    
    
        if(L.r[i].key < L.r[i-1].key) {
    
       //判断是否需要进行记录后移
            L.r[0] = L.r[i];  //备份无序块首元素,即待插入记录
            for (j = i - 1; L.r[j].key > L.r[0].key; --j)
                L.r[j] = L.r[j - 1];  // 大记录逐个后移
            L.r[j] = L.r[0];  //填入终止位右侧
        }
    }
}

算法分析

空间复杂度O(1), 就地排序。
时间复杂度:最好:O(n),元素有序。最坏:O(n^2),元素逆序。
稳定性:稳定。
算法分析:就本算法而言,影响时间复杂度有两个方面

  • 已排序列中相对最小项的查找
  • 对未排序列的处理过于暴力,时间损耗过大
思想引入

对未排序列而言,由于其元素排布毫无规律,对他们的处理只能由前到后逐一处理。但是,对于已排序列,其是有序的,排布是有规律的,因此运用搜索算法可以显著的降低对已排序列中对应最小项的查找时间而不用从后往前依次比对。最常用的有序序列搜索算法为折半查找法。

折半法查找法

二分查找也称折半查找(Binary Search),它是一种效率较高的查找方法。但是,折半查找要求线性表必须采用顺序存储结构,而且表中元素按关键字有序排列。
——百度百科

思想:

每次和中间元素比较,如果中间元素小于或等于待插入记录,则所寻找位置在右半区间,low变为mid+1;如果中间元素大,则将high变为mid-1【high右侧均大于待插入记录】其中:

mid = (low + high) / 2, 找到位置后,移动元素时,不必再判断大小,从而节省了时间
折半插入排序1
折半插入排序2
折半插入排序3

  1. 折半插入排序

折半插入排序(binary insertion sort)是对插入排序算法的一种改进,由于排序算法过程中,就是不断的依次将元素插入前面已排好序的序列中。由于前半部分为已排好序的数列,这样我们不用按顺序依次寻找插入点,可以采用折半查找的方法来加快寻找插入点的速度。
——百度百科

思路:

直接插入排序的改进版,通过折半查找的方法,减少 已排序列 中相对最小项搜索时间,从而在平均效率上优于直接插入排序。

void BinInsertionSort (SqList &L) {
    
    
    int i, j, low, high, mid;
    i = j = low = high = mid = 0;  // 变量初始化
    for (i = 2; i <= L.length; ++i) {
    
    
        low = 1;
        high = i-1;
        while (low <= high) {
    
      // 折半查找。寻找插入的位置
            mid = (low + high) / 2;
            if(L.r[mid].key < L.r[i].key) low = mid + 1;
            else high = mid - 1;
        }
        L.r[0] = L.r[i];  //备份无序块首元素,即待插入记录
        for ( j = i - 1; j >= high; --j) // 只移动位置,不比较大小
            L.r[j+1] = L.r[j];  // 大记录逐个后移
        L.r[high] = L.r[0];  // // 插入

    }
}
算法分析

空间复杂度:O(1),就地排序。
时间复杂度:最好:O(n),最坏:O(n^2),序列正序时O(NlogN).
稳定性:稳定。
算法分析

  • 平均情况下,折半插入时间复杂度低,当元素有序时,直接插入法最快。
  • 折半查找只是减少了已排序序列的查找时间,对未排序列没有进行改进,本质上对排序效率影有限。因此,需要一种新的插入排序思想
思想引入

就排序问题,可以用递归思想进行处理。
就简单插入排序与折半插入排序而言,将其进行递归化处理,可以看到两者的递归深度为n- 1,即每次递归操作只对未排序列的一个元素进行处理,费时费力。因此,减小递归深度使其尽快达到递归边界是减少时间复杂度的最有效手段。这里需要的思想为递归中的基本思想,分治法。

分治法

分治法可以通俗的解释为:把一片领土分解,分解为若干块小部分,然后一块块地占领征服,被分解的可以是不同的政治派别或是其他什么,然后让他们彼此异化。

分治法的精髓:
分–将问题分解为规模更小的子问题;
治–将这些规模更小的子问题逐个击破;
合–将已解决的子问题合并,最终得出“母”问题的解;
——百度百科

就简单插入与插入排序而言,可以认为其分治的子问题为各个元素。减少递归深度问题转化为对待排序列进行分治的问题。前面分析得出,顺序序列的时间复杂度最低,为O(n),这是排序的最理想情况,在这里我们的方法是,整体对待整个序列,先使序列相对有序,最后整体有序。

  1. 希尔排序(缩小增量排序)

希尔排序(Shell’s Sort)是插入排序的一种又称“缩小增量排序”(Diminishing Increment Sort),是直接插入排序算法的一种更高效的改进版本。希尔排序是非稳定排序算法。该方法因D.L.Shell于1959年提出而得名。
希尔排序是把记录按下标的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至1时,整个文件恰被分成一组,算法便终止。
——百度百科

思路:

先取一个小于n的整数d1作为第一个增量,把序列的全部记录分组。所有距离为d1的倍数的记录放在同一个组中。先在各组内进行直接插入排序;然后,取第二个增量d2<d1重复上述的分组和排序,直至所取的增量为1即所有记录放在同一组中进行直接插入排序为止。
设本例增量序列为5-3-1。希尔排序
在这里插入图片描述

void ShellInsert ( SqList &L, int dk ) {
    
    
   int i, j;
   for (i = dk + 1; i <= L.length; ++i) {
    
    //从第dk+1个元素开始子序列内插入排序
       if (L.r[i].key < L.r[i - dk].key) {
    
       //子序列内前一个不再是i-1,而是i-dk
           L.r[0] = L.r[i];        // 备份待插入元素
           for (j = i; (L.r[j-dk].key > L.r[0].key) && j - dk > 0; j -= dk)
               L.r[j] = L.r[j - dk];  //子序列内后移一位不再是j+1,而是j+dk
           L.r[j] = L.r[0];
       }
   }
}

void  ShellSort(SqList &L)
{
    
    
   /*按增量序列dlta[0…t-1]对顺序表L作Shell排序,假设规定增量序列为5,3,1*/
   int dlta[3] = {
    
    5, 3, 1};
   int t = 3;
   for(int k = 0; k < t; ++k)
       ShellInsert(L, dlta[k]);
}
算法分析:

空间复杂度:O(1),就地排序
时间复杂度:

  • 受增量序列和初始状态影响
  • 最优增量序列尚待确定
  • 统计表明平均性能O(n^1.3)

稳定性:不稳定
通过分治法的思想缩小了递归深度,在整体上减少了时间复杂度。至此,插入类排序走上了成熟。希尔排序便是当前插入类排序的最终版。

思想引入

除了比较类排序,还有一类方法容易想到,就是交换,以交换为主体的交换类排序便应运而生。

交换类排序

  1. 冒泡排序

冒泡排序(Bubble Sort),是一种计算机科学领域的较简单的排序算法。
它重复地走访过要排序的元素列,依次比较两个相邻的元素,如果顺序(如从大到小、首字母从从Z到A)错误就把他们交换过来。走访元素的工作是重复地进行直到没有相邻元素需要交换,也就是说该元素列已经排序完成。
这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端(升序或降序排列),就如同碳酸饮料中二氧化碳的气泡最终会上浮到顶端一样,故名“冒泡排序”。
——百度百科

思路

  1. 比较相邻的元素。如果第一个比第二个大,就交换他们两个。
  2. 对每一对相邻元素做同样的工作,从开始第一对到结尾的最后一对。在这一点,最后的元素应该会是最大的数。
  3. 针对所有的元素重复以上的步骤,除了最后一个。
  4. 持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。

在这里插入图片描述

void BubbleSort (SqList &L) {
    
    
    int  i, j;
    for(i = 1; i < L.length; ++i)  // 外循环
    {
    
    
        for(j = 1; j <= L.length - i; ++j)  // 内循环
        {
    
    
            if(L.r[j].key > L.r[j+1].key)  // 交换顺序
            {
    
    
                RcdType temp = L.r[j];
                L.r[j] = L.r[j+1];
                L.r[j+1] = temp;
            }
        }
    }
}
算法分析:

空间复杂度:O(1),就地排序

时间复杂度:最好O(n);最坏O(n^2)
稳定性:稳定
冒泡排序一次循环仅使一个元素移到最终位置,用递归思想来说,递归深度过深。因此减少递归深度,尽快到达递归边界成为减小交换类排序时间复杂度的必要手段。

思想引入

就冒泡排序而言,每次从序列左边开始进行交换,序列右边弃之不用,浪费了很多节省时间的机会,因此充分利用序列左右两边成为交换类排序的突破口。由希尔排序可知,运用分治法,可以先使序列基本有序,最终进行整体排序可以大大节省时间。因此,递归与分治成为交换类排序的解决之道。

  1. 快速排序

快速排序(Quicksort)是对冒泡排序的一种改进。
快速排序由C. A. R. Hoare在1960年提出。它的基本思想是:通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列
——百度百科

思路
  • 用最左侧元素作“枢轴”,存入“哨所”
  • 设low和high指向两端
  • high向左移动,一旦遇到小于枢轴的元素,则将其移动到左侧,放入low指向的位置;
  • low向右移动,一旦遇到大于枢轴的元素,则将其移动到右侧,放入high指向的位置;
  • high和low从两侧交替向中间移动,直至low=high. 左侧均小于等于枢轴,右侧均大于等于枢轴,将枢轴记录填入重叠位置则完成划分,本次递归完成,返回low或high.
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
int Partion(SqList &L, int low, int high)
{
    
    
    RcdType temp = L.r[low];  // 记录哨兵
    while(low < high)
    {
    
    
        for(; L.r[high].key > temp.key && low < high; --high){
    
    }  // 从右侧开始
        if(low < high) L.r[low++] = L.r[high];  // 发现小于哨兵的,与low交换
        for(; L.r[low].key < temp.key && low < high; ++low){
    
    }  // 从左侧开始
        if(low < high) L.r[high--] = L.r[low];  // 发现大于哨兵的,与high交换
    }
    // 在high=low处,填入哨兵。此时,哨兵以左全比哨兵小。哨兵以右全比哨兵大,序列部分有序
    L.r[low] = temp;  
    return low;
}

void QuickSort(SqList&L, int low, int high)
{
    
    
    int l = Partion(L, low, high);  // 对序列进行一次排序
    // 递归进行,直至序列整体有序
    if(low < high)
    {
    
    
        QuickSort(L, low, l - 1); 
        QuickSort(L, l + 1, high);
    }
}
算法分析

空间复杂度:平均空间复杂度O( log ⁡ 2 N \log_2N log2N),最坏空间复杂度O(N)
时间复杂度:平均时间复杂度O(N* log ⁡ 2 N \log_2N log2N),最坏时间复杂度O(N^2)
稳定度:不稳定
快速排序可以说对冒泡排序作出了极大的改进,其着眼于整体,利用递归与分治思想,将N^2级转化为对数级,由于其优良的性能,目前应用较多。但是因为其运用了递归,所以会开辟隐式函数栈,故其空间复杂度较高。

思想引入

快速排序递归与分治思想引入,极大的改进了冒泡排序的效率,但是这是在交换类排序的基础上进行的改进,那么有没有一种算法,是基于分治思想直接得出的呢?有。

归并类排序

归并排序

归并排序(MERGE-SORT)是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。归并排序是一种稳定的排序方法。
——百度百科

思路

运用分治思想,对待排序列分而治之,将序列一分为二,在将子序列一分为二,直到每一个子序列仅为一个元素,再对每个子序列顺序插入,反向进行,直至对整个序列顺序插入。本算法在递归中进行,因此看起来麻烦,写起来比较简单。

在这里插入图片描述

void Merge(SqList &L, int left, int right, int mid) //合并两组数字
{
    
    
    int j, i = 0;  // 循环,计数用
    int first = left;
    int second = mid + 1; // first, second为待插入的两个数组
    RcdType temp[right - left + 1];  // 用来存排序数的临时数组,长度为两组数组元素个数总和
    while(first <= mid && second <= right)  // 当两组数字都未全部移入临时数组
    {
    
    
        if(L.r[first].key > L.r[second].key)  //  如果first所指数大于second所指的数
            temp[i++] = L.r[second++];  // 将second所指的数移入临时数组,first右移
        else
            temp[i++] = L.r[first++];
    }
    while(first <= mid)  // 当第一个数组中还有元素未移入临时数组
        temp[i++] = L.r[first++];
    while(second <= right)
        temp[i++] = L.r[second++];
    i = 0;
    for(j = left; j <= right; ++j)  // 将临时数组的元素移入原数组
        L.r[j] = temp[i++];
}

void MergeSort(SqList &L, int left, int right)//归并排序
{
    
    
    if(left < right - 1) //如果该组数字个数大于2
    {
    
    
        int mid = (left + right) / 2;
        MergeSort(L, left, mid);
        MergeSort(L,mid + 1, right); //分割
        Merge(L, left, right, mid); //合并
    }
    else if(left == right - 1) //如果该组数字个数等于2
    {
    
    
        if(L.r[left].key > L.r[right].key) //如果前一个数字比后一个数字大,交换
        {
    
    
            RcdType z = L.r[left];
            L.r[left] = L.r[right];
            L.r[right] = z;
        }
    }
    //如果该组数字只剩一个,不用处理
}

算法分析:

空间复杂度:O(n),递归中调用了函数隐式栈且Merge时开辟了内存存储元素
时间复杂度:O(N* log ⁡ 2 N \log_2N log2N)
归并排序为运用分治法最为彻底的算法,空间复杂度较高,但时间复杂度低。
其实以上算法或多或少用到了贪心和动态规划的思想。那么有没有一种算法是贪心或动态规划的集中表现呢,有。

贪心算法

贪心算法(又称贪婪算法)是指,在对问题求解时,总是做出在当前看来是最好的选择。也就是说,不从整体最优上加以考虑,他所做出的是在某种意义上的局部最优解。
贪心算法不是对所有问题都能得到整体最优解,关键是贪心策略的选择,选择的贪心策略必须具备无后效性,即某个状态以前的过程不会影响以后的状态,只与当前状态有关。
——百度百科

应用贪心算法需要具备的基本条件:

  1. 可行性:必须满足问题的约束条件
  2. 局部最优:他是当前步骤中所有可行选择中最佳的局部选择
  3. 不可更改:即选择一旦做出,在算法的后面步骤就不可改变
动态规划

动态规划算法通常用于求解具有某种最优性质的问题。在这类问题中,可能会有许多可行解。每一个解都对应于一个值,我们希望找到具有最优值的解。动态规划算法与分治法类似,其基本思想也是将待求解问题分解成若干个子问题,先求解子问题,然后从这些子问题的解得到原问题的解。与分治法不同的是,适合于用动态规划求解的问题,经分解得到子问题往往不是互相独立的。若用分治法来解这类问题,则分解得到的子问题数目太多,有些子问题被重复计算了很多次。如果我们能够保存已解决的子问题的答案,而在需要时再找出已求得的答案,这样就可以避免大量的重复计算,节省时间。我们可以用一个表来记录所有已解的子问题的答案。不管该子问题以后是否被用到,只要它被计算过,就将其结果填入表中。这就是动态规划法的基本思路。具体的动态规划算法多种多样,但它们具有相同的填表格式。
——百度百科

选择类排序

  1. 简单选择排序

简单选择排序是指一种排序算法,在简单选择排序过程中,所需移动记录的次数比较少。最好情况下,即待排序记录初始状态就已经是正序排列了,则不需要移动记录。
方法是设所排序序列的记录个数为n。i取1,2,…,n-1,从所有n-i+1个记录(Ri,Ri+1,…,Rn)中找出排序码最小的记录,与第i个记录交换。执行n-1趟 后就完成了记录序列的排序。
——百度百科

思路:

每次选取未排序列中最小(大)的元素与第i个元素进行交换,执行完n-1趟后,排序完成。

void SelectSort(SqList &L)
{
    
    
    int sub = 0;
    for( int i = 1;i <= L.length; ++i)
    {
    
    
        sub = i;
        int j = i;
        for(; j <= L.length; ++j)
        {
    
    
            if(L.r[i].key > L.r[j].key) sub = j;
        }
        RcdType temp = L.r[sub];
        L.r[sub] = L.r[i];
        L.r[i] = temp;
    }
}
算法分析:

空间复杂度:O(N),就地排序
时间复杂度:O(N^2)
稳定性:稳定
就简单选择排序,其每次选择待排序列的最小(大)项,是贪心算法的最集中体现。
但是,该算法仅仅运用了贪心算法,时间复杂度过高属于基本简单算法的范畴,动态规划在选择排序中作用不大,因此急需一种新的思想填充进来解决选择排序的时间复杂度问题。
我们知道一切方法都是相互联系的,从排序问题引出了递归思想,运用动态规划和贪心算法实现了排序。在已排序列中通过查找算法中的折半查找法减少了查找对应最小项的时间。因此,普遍联系成为了算法设计的一项很重要的思想。这时,第一个找到突破口的是弗洛伊德。
弗洛伊德将排序问题同二叉树联系起来,通过堆这种特殊的数据结构对序列进行特殊处理,大大减小的排序的时间复杂度。

堆的概念:

堆(heap)是计算机科学中一类特殊的数据结构的统称。堆通常是一个可以被看做一棵树的数组对象。堆总是满足下列性质:
堆中某个节点的值总是不大于或不小于其父节点的值;
堆总是一棵完全二叉树。
将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆。常见的堆有二叉堆、斐波那契堆等。
堆是非线性数据结构,相当于一维数组,有两个直接后继。
堆的定义如下:n个元素的序列{k1,k2,ki,…,kn}当且仅当满足下关系时,称之为堆。
(ki <= k2i,ki <= k2i+1)或者(ki >= k2i,ki >= k2i+1), (i = 1,2,3,4…n/2)
若将和此次序列对应的一维数组(即以一维数组作此序列的存储结构)看成是一个完全二叉树,则堆的含义表明,完全二叉树中所有非终端结点的值均不大于(或不小于)其左、右孩子结点的值。由此,若序列{k1,k2,…,kn}是堆,则堆顶元素(或完全二叉树的根)必为序列中n个元素的最小值(或最大值)。
——百度百科

  1. 堆排序

堆排序(英语:Heapsort)是指利用堆这种数据结构所设计的一种排序算法。堆是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点、
一个序列,设它对应某完全二叉树的层序序列,若该二叉树任意一个节点均比其左右孩子节点大(小),则称该序列为大(小)顶堆
——百度百科

思路:

初始大顶堆:由未排序列初始一棵完全二叉树
最大堆调整(Max Heapify):将堆的末端子节点作调整,使得子节点永远小于父节点
创建最大堆(Build Max Heap):将堆中的所有数据重新排序
堆排序(HeapSort):移除位在第一个数据的根节点,并做最大堆调整的递归运算
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

注意:

堆排序相比之前几个排序,较为复杂,需多多思考。如果看不懂的话,可以考虑先去B站看一下视频,大体了解之后再回来阅读。
初始建堆过程比较复杂,在堆排序时先考虑调整建堆,当调整建堆完成后再思考初始大顶堆就比较简单了。
堆实际上是完全二叉树的特殊形式。因此,实际上对线性表进行堆排序时并不会真的建一棵二叉树,而是通过公式计算节点相应的父节点和子节点。

// 交换函数
void Swap(int tree[], int i, int j){
    
    
    int temp = tree[i];
    tree[i] = tree[j];
    tree[j] = temp;
}

// 调整大顶堆
void Heapify(int tree[], int nodes, int index){
    
    
    // 定义递归出口
    if(index > nodes)
        return;
    // 计算左右子节点
    int c_1 = index * 2;
    int c_2 = index * 2 + 1;
    // 寻找最大值
    int max = index;
    if(c_1 < nodes && tree[c_1] > tree[max])
        max = c_1;
    if(c_2 < nodes && tree[c_2] > tree[max])
        max = c_2;
    // 如果进行了改变,递归对其子树进行heapify
    if(max != index)
    {
    
    
        Swap(tree, max, index);
        Heapify(tree, nodes, max);
    }
}

// 建立大顶堆
void BuildHeap(int tree[], int last_node){
    
    
    int parent = last_node / 2;
    for(int i = parent; i > 0; --i){
    
    
        Heapify(tree, last_node, i);
    }
}

// 堆排序
void HeapSort(int tree[], int nodes){
    
    
    BuildHeap(tree, nodes);
    for(int i = nodes; i > 0; --i){
    
    
        Swap(tree, i, 1);
        Heapify(tree, i, 1);
    }
}
算法分析:

空间复杂度:O(1),就地排序
时间复杂度:O(N* log ⁡ 2 N \log_2N log2N)
就堆排序可以看出,前期建堆是比较耗费时间的,但是后期排序相比其他算法非常节省时间。因此有些情况下,以退为进不失为一种很好的解决问题策略。普遍联系是解决问题的突破口。

思想引入

就排序问题看来,无非是通过比较和交换,实现元素从无序变为有序。上面介绍了交换类排序与比较类排序,看起来排序问题已经解决完了,但是真的是这样吗。以上算法的时间限制主要是比较和移动,移动是不可避免的但是比较是必须的吗?可不可以不比较直接排序?

基数排序

基数排序

基数排序(radix sort)属于“分配式排序”(distribution sort),又称“桶子法”(bucket sort)或bin sort,顾名思义,它是透过键值的部份资讯,将要排序的元素分配至某些“桶”中,藉以达到排序的作用,基数排序法是属于稳定性的排序,其时间复杂度为O (nlog®m),其中r为所采取的基数,而m为堆数,在某些时候,基数排序法的效率高于其它的稳定性排序法。
——百度百科

思想:

基数排序属于分配排序的一种,其不在着眼于元素的大小,而是将元素的值作为咨询的一部分。类似与查找算法中的哈希查找法。

基数排序是由计数排序改善而来的,基数排序将整数或者字符串切分不同的数字或字符,然后按照低位先排序收集,接着高位排序收集,依次类推直到最高位。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

int GetNumInPos(int num, int pos)  // 关键值分配函数
{
    
    
    int temp = 1;
    for (int i = 0; i < pos - 1; i++)
        temp *= 10;
    return (num / temp) % 10;
}

//基数排序  pDataArray 无序数组;iDataNum为无序数据个数
void RadixSort(SqList &L, int bit)
{
    
    
    int bits = 10;
    RcdType *radixArrays[bits];    //分别为0~9的序列空间,创立二维数组
    for (int i = 0; i < bits; i++)
    {
    
    
        // 每一位数字分配后分配数组大小的空间,防止所有元素改位数字相同
        radixArrays[i] = (RcdType*)malloc(sizeof(RcdType) * (L.length + 1));
        radixArrays[i][0].key = 0;    //index为0处的key值记录这组数据的个数
    }
    for (int pos = 1; pos <= bits; ++pos)
    {
    
    
        for (int i = 0; i <= L.length; ++i)    //分配过程
        {
    
    
            int num = GetNumInPos(L.r[i].key, pos);
            int index = ++radixArrays[num][0].key;
            radixArrays[num][index] = L.r[i];
        }

        for (int i = 0, j =0; i < bits; i++)    //收集
        {
    
    
            for (int k = 1; k <= radixArrays[i][0].key; k++)
                L.r[j++] = radixArrays[i][k];
            radixArrays[i][0].key = 0;    //复位
        }
    }
}
算法分析:

空间复杂度:O(10N)
时间复杂度:O(Length
N)
稳定性:稳定
可以看出。该算法空间复杂度较高,其实该算法在链式存储上应用最佳时间复杂度O(n),空间复杂度O(radix),运用基数排序在链式存储结构上极大的的优化了排序算法的时间复杂度。由此可见突破常规思维对于问题的解决是多么重要。

总结

  • 直接插入排序正序时最好时间复杂度O(n),逆序最坏O(n2),平均O(n2),空间复杂度O(1);稳定;原始序列基本有序时该方法好

  • 折半插入排序逆序最坏O(n2),正序时O(NLogN) , S(n)=O(1);稳定

  • 希尔排序(缩小增量排序):平均时间复杂度O(n1.4),S(n)=O(1);不稳定

  • 冒泡排序(改进的)正序时间复杂度最好O(n),逆序最坏O(n2),平均O(n2),S(n)=O(1); 稳定. 注意思考如何改进?

  • 快速排序平均时间复杂度O(nlogn),平均性能最优,正序或逆序最坏O(n2), 有辅助栈,空间复杂度最坏O(n),平均O(logn);不稳定. 枢轴改进

  • 选择排序复杂度T(n)=O(n2),原本有序无序均如此,S(n)=O(1);稳定

  • 堆排序T(n)=O(nlogn),S(n)=O(1);不稳定(因为间隔着比和移动)

  • 归并排序最好最坏复杂度为O(nlogn),空间复杂度O(n),稳定

  • 链式基数排序最好最坏时间复杂度为O(d*(n+ rd )),空间O(rd),稳定

  • 内部排序方法分类:复杂度O(n2)的简单排序方法,O(nlogn)的高效排序方法(比较法的理论下界),O(d*(n+rd))的基数排序方法.

  • 各排序方法各有优缺点,具体选择时考虑稳定性、记录多少、原始数据是否基本有序、关键字大小等因素。

  • 直接插入排序适合基本有序、n值较小时

  • 基数排序适合关键字较短、n较大、要求稳定时;

  • 快速排序适合n大、不要求稳定、分布随机时;

  • 堆排序适合n大、关键字分布可能有序、不要求稳定;

  • 归并排序适合n大、关键字分布可能有序、要求稳定且内存空间充裕时;

  • 如果只选择最小的前几个元素,则简单选择排序优先

  • 理论上可证明基于比较的排序方法可能达到的最快的时间复杂度为O(nlogn);基数排序不是基于“比较”

  • 为避免顺序存储时大量移动记录的时间开销,可考虑用链表作为存储结构
    可用链式存储结构进行的排序算法为:直接插入排序、归并排序(非递归)、基数排序
    不宜采用链表作为存储结构的折半插入排序、希尔排序、快速排序、堆排序

  • 当n较大时适宜选用的排序算法:
    (1)分布随机,稳定性不做要求,则采用快速排序
    (2)内存允许,要求排序稳定时,则采用归并排序
    (3)可能会出现正序或逆序,稳定性不做要求,则采用堆排序或归并排序

  • 当n较小时适宜选用的排序算法:
    (1)基本有序,则采用直接插入排序
    (2)分布随机,则采用简单选择排序,若排序码不接近逆序,也可以采用直接插入排序

本文是对排序算法的整理与自己的理解,如发现错误,欢迎指正。

猜你喜欢

转载自blog.csdn.net/qq_44733706/article/details/103755754
今日推荐