数据结构之排序

Posted blknemo

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了数据结构之排序相关的知识,希望对你有一定的参考价值。

内部排序

分类 排序算法 改进思路 最好情况 平均时间复杂度 最坏情况 空间复杂度 稳定性
插入排序 直接插入排序 基本排序方法 O(n) O(\(n^2\)) O(\(n^2\)) O(1) 稳定
折半插入排序 确定有序序列的插入位置 O(\(nlog_2n\)) O(\(n^2\)) O(\(n^2\)) O(1) 稳定
希尔排序 利用直接插入的最好情况 O(\(n^1.3\)) NA O(\(n^2\)) O(1) 不稳定
交换排序 冒泡排序 基本排序方法 O(n) O(\(n^2\)) O(\(n^2\)) O(1) 稳定
快速排序 交换不相邻元素 O(\(nlog_2n\)) O(\(nlog_2n\)) O(\(n^2\)) O(\(nlog_2n\)) 不稳定
选择排序 简单选择排序 基本排序方法 O(\(n^2\)) O(\(n^2\)) O(\(n^2\)) O(1) 不稳定
堆排序 将待排序列转化为完全二叉树 O(\(nlog_2n\)) O(\(nlog_2n\)) O(\(nlog_2n\)) O(1) 不稳定
归并排序 2路归并排序 合并每个有序序列 O(\(nlog_2n\)) O(\(nlog_2n\)) O(\(nlog_2n\)) O(n) 稳定
基数排序 基数排序 O(d(n+r)) O(d(n+r)) O(d(n+r)) O(r) 稳定

插入排序

直接插入排序

有序序列L[1...i-1] L(i) 无序序列L[i+1...n]
  • 基本思想:
    可以将L(2)~L(n)依次插入到前面已排好序的子序列中,初始假定L[1]是一个已经排好序的子序列。
    1. 查找出L(i)在L[1...i-1]中的插入位置k。
    2. 将L[k...i-1]中的所有元素全部后移一个位置。
    3. 将L(i)复制到L(k)。
  • 具体实现:
void InsertSort(ElemType A[], int n) 
    int i, j;
    for(i = 2; i <= n; i++)        //依次将A[2]~A[n]插入到前面已排序序列
        if(A[i].data < A[i-1].data)    //若A[i]的关键码小于有序序列的最后一个元素(有序序列中的最大值)(即其前驱),需将A[i]插入有序表;大于有序序列的最后一个元素,则不用从头插入,并入有序序列(本来就排在有序序列之后),成为有序序列新的最大值(最后一个)
            A[0] = A[i];        //复制为哨兵,A[0]不存放元素
            for(j = i - 1; A[0].data < A[j].data; --j)     //从后往前查找待插入位置
                A[j+1] = A[j];      //向后挪位
            
            A[j+1] = A[0];      //复制到插入位置
        
    
  • 性能分析:
    • 空间效率:O(1)
      仅使用了常数个辅助单元。

    • 时间效率:O(\(n^2\))
      在排序过程中,向有序子表中逐个插入元素的操作进行了n-1趟,每趟操作都分比较关键字和移动元素,而比较次数移动元素取决于待排序表的初始状态。
      • 最好情况:O(n) (顺序)
      • 最坏情况:O(\(n^2\)) (逆序)
      • 平均情况:O(\(n^2\))
    • 稳定性:稳定
      由于每次插入元素时,总是从后向前先比较在移动,所以不会出现相同元素相对位置发生变化的情况。

    • 适用性:
      适用于顺序存储链式存储的线性表。
      适用于基本有序的排序表和数据量不大的排序表。
      为链式存储时,可以从前往后查找指定元素的位置。

      大部分排序算法都仅适用于顺序存储的线性表。

折半插入排序

  • 基本思想:
    将比较和移动操作分离,即先折半查找出元素的待插入位置,然后统一地移动待插入位置之后的所有元素。

  • 具体实现:
void InsertSort(ElemType A[], int n) 
    int i, j, low, high, mid;
    for(i = 2; i <= n; i++)        //依次将A[2]~A[n]插入前面的已知排序序列
        A[0] = A[i];        //将A[i]暂存到A[0]
        low = 1;            //设置折半查找的范围
        high = i-1;         
        while(low <= high)     //折半查找(默认递增有序)
            mid = (low + high) / 2;     //取中间点
            if(A[mid].data > A[0].data)    //查找左半边
                high = mid - 1;
            else          //查找右半边
                low = mid + 1;
            
            //查找结束后,high<low,k在high和high+1之间,所以顶替high+1位置,把high+1后面的元素后移
            for(j=i-1; j>=high+1; --j) 
                A[j+1] = A[j];      //统一后移元素,空出插入位置
            
            A[high+1] = A[0];       //插入
        
    
  • 性能分析:
    • 空间效率:O(1)
      仅使用了常数个辅助单元。

    • 时间效率:O(\(n^2\))
      仅减少了比较元素的次数,约为O(\(nlog_2n\)),该比较次数与待排序表的初始状态无关,仅取决于表中元素的个数n;而元素移动次数并未改变,他依赖于待排序表的初始状态。
      • 最好情况:O(\(nlog_2n\)) (顺序)
      • 最坏情况:O(\(n^2\)) (逆序)
      • 平均情况:O(\(n^2\))
    • 稳定性:稳定

    • 适用性:适用于顺序存储的线性表

希尔排序

  • 基本思想:
    先将待排序表分割成若干形如L[i, i+d, i+2d, ..., i+kd]的“特殊”子表,分别进行直接插入排序,当整个表中的元素已呈“基本有序”时,再对全体记录进行一次直接插入排序

  • 具体实现:
//对顺序表做希尔插入排序,本算法和直接插入排序相比,做了以下修改:
//1. 前后记录的位置增量是dk,不是1
//2. A[0]只是暂存单元,不是哨兵,当j<=0时,插入位置已到
void ShellSort(ElemType A[], int n) 
    for(dk=n/2; dk>=1; dk=dk/2)        //步长变化
        for(i=dk+1; i<=n; ++i) 
            if(A[i].data < A[i-dk].data)       //需将A[i]插入有序增量子表
                A[0] = A[i];        //暂存在A[0]
                for(j=i-dk; j>0 && A[0].data<A[j].data; j-=dk) 
                    A[j+dk] = A[j];     //记录后移,寻找插入的位置
                
                A[j+dk] = A[0];     //插入
            
        
    
  • 性能分析:
    • 空间效率:O(1)
      仅使用了常数个辅助单元
    • 时间效率:
      由于希尔排序的时间复杂度依赖于增量序列的函数,这设计数学上尚未解决的难题,所以其时间复杂度分析比较困难。
      • 最好情况:O(\(n^1.3\)) (当n在某个特定范围时)
      • 最坏情况:O(\(n^2\))
      • 平均情况:NA
    • 稳定性:不稳定
      但相同关键字的记录被划分到不同的子表时,可能会改变它们之间的相对次序。

    • 适用性:仅适用于线性表为顺序存储的情况。

交换排序

冒泡排序

  • 基本思想:
    假设待排序表长为n,从后往前(或从前往后)两两比较相邻元素的值,若为逆序(即A[i-1] > A[i]),则交换它们,知道序列比较完。称之为一趟冒泡,即将最小的元素交换到待排序列的第一个位置(关键字最小的元素如气泡一般逐渐往上“漂浮”直至“水面”)。下一趟冒泡时,前一趟确定的最小元素不在参与比较,待排序列减少一个元素。这样最多做n-1趟冒泡就能把所有元素排好序。

  • 具体实现:
//用冒泡排序法将序列A中的元素按从小到大排列
void BubbleSort(ElemType A[], int n) 
    for(i=0; i<n-1; i++)   //趟数
        flag = false;       //表示本趟冒泡是否发生交换的标志
        for(j=n-1; j>i; j--)       //一趟冒泡过程
            if(A[j-1].data > A[j].data)    //若为逆序
                swap(A[j-1], A[j]);     //交换
                flag = true;
            
        
        if(flag == false)      //本趟遍历后没有发生交换(前面一个元素均小于后面一个),说明表已经有序
            return;
        
    
  • 性能分析:
    • 空间效率:O(1)
      仅使用了常数个辅助单元
    • 时间效率:O(\(n^2\))
      当初始序列有序时,显然第一趟冒泡后没有元素交换(前均小于后),从而直接跳出循环,比较次数为n-1,移动次数为0。
      • 最好情况:O(n) (顺序)
      • 最坏情况:O(\(n^2\)) (逆序)
      • 平均情况:O(\(n^2\))
    • 稳定性:稳定
      i>j且A[i].data==A[j].data时,不会交换两个元素

    • 适用性:适用于线性表

快速排序

小L[1...k-1] L(k) 大L[k+1...n]
  • 基本思想:
    基于分治法,在待排序表L[1...n]中任取一个元素pivot(枢轴)作为基准,通过一趟排序将待排表划分为两个独立的两部分L[1...k-1]和L[k+1...n],使得L[1...k-1]中所有元素小于pivot,L[k+1...n]中的所有元素大于等于pivot,则pivot放在了其排序后的最终位置L(k)上,这个称为一趟快速排序。而后分别递归地对两个子表重复上述过程,直至每个部分内只有一个元素或空为止,即所有元素放在了其最终位置上。

  • 具体实现:
//划分算法(一趟排序过程)
int Partition(ElemType A[], int low, int high) 
    ElemType pivot = A[low];    //将当前表中第一个元素设为枢轴值,对表进行划分
    while(low < high)      
        while(low<high && A[high]>=pivot)      //当high中的值已经比pivot大,则不移动
            --high;
        
        //循环结束时,high中的元素比pivot小
        A[low] = A[high];       //将小值移动到左端
        while(low<high && A[low]<=pivot)       //当low中的值已经比pivot小,则不移动
            ++low;
        
        //循环结束时,low中的元素比pivot大
        A[high] = A[low];       //将大值移动到右端
    
    //循环结束时,low==high,已经是pivot的最终位置
    A[low] = pivot;     //放入最终位置
    return low;     //返回存放枢轴的最终位置


void QuickSort(ElemType A[], int low, int high) 
    if(low >= high)    //不能划分了,递归出口
        return;
    
    int pivotpos = Partition(A, low, high);     //划分
    QuickSort(A, low, pivotpos-1);      //依次对两个子表进行递归排序划分
    QuickSort(A, pivotpos+1, high);
  • 性能分析:
    • 空间效率:O(\(log_2n\))
      递归需要借助一个递归工作栈来保存每层递归调用的必要信息,其容量应与递归调用的最大深度一致
      • 最好情况:?\(log_2(n+1)\)?
      • 最坏情况:O(n)
      • 平均情况:O(\(log_2n\))
    • 时间效率:O(\(nlog_2n\))
      快速排序的运行时间与划分是否对称有关,而后者又与具体使用的划分算法有关。
      • 最好情况:O(\(nlog_2n\)) (对称,即大于、小于枢轴的个数相等)
      • 最坏情况:O(\(n^2\)) (基本有序基本逆序)
      • 平均情况:O(\(nlog_2n\))
    • 稳定性:不稳定
      在划分算法中,若右端区间有两个关键字相同,且均小于基准值的记录,则在交换到左区间后,它们的相对位置会发生变化。

    • 适用性:适用于顺序存储的线性表

选择排序

简单选择排序

  • 基本思想:
    假设排序表为L[1...n],第i趟排序即从L[i...n]中选择关键字最小的元素与L(i)交换,每一趟排序可以确定一个元素的最终位置,这样经过n-1趟排序就可使得整个排序表有序。

  • 具体实现:
//对表A做简单选择排序,A[]从0开始存放元素
void SelectSort(ElemType A[], int n) 
    for(i=0; i<n-1; i++)       //一共进行n-1趟
        min = i;                //记录最小元素的位置
        for(j=i+1; j<n; j++)   //在A[i...n-1]中选择最小的元素
            if(A[j] < A[min])  //更新最小元素位置
                min = j;
            
            if(min != i)       //与第i个位置交换
                swap(A[i], A[min]);
            
        
    
  • 性能分析:
    • 空间效率:O(1)
      仅使用常数个辅助单元

    • 时间效率:O(\(n^2\))
      元素间比较次数与序列的初始状态无关,始终是n(n-1)/2次。
      • 最好情况:O(\(n^2\))
      • 最坏情况:O(\(n^2\))
      • 平均情况:O(\(n^2\))
    • 稳定性:不稳定
      在第i趟找到最小元素后,和第i个元素交换,可能会导致第i个元素与其含有相同关键字元素的相对位置发生改变。

    • 适用性:适用于顺序存储的线性表

堆排序

  • 基本思想:

  • 具体实现:

  • 性能分析:
    • 空间效率:O(1)
      仅使用了常数个辅助单元。

    • 时间效率:O(\(nlog_2n\))
      建堆时间为O(n),之后又n-1次向下
      • 最好情况:O(\(nlog_2n\))
      • 最坏情况:O(\(nlog_2n\))
      • 平均情况:O(\(nlog_2n\))
    • 稳定性:不稳定
      进行筛选时,有可能把后面相同关键字的元素调整到前面。

    • 适用性:线性表

归并排序

  • 基本思想:
    “归并”的含义是将两个或两个以上的有序表组合成一个新的有序表。
    假定待排序表含有n个记录,则可将其视为n个有序的子表,每个子表的长度为1,然后两两归并,得到\(?n/2?\)个长度为2或1的有序表;再两两归并……如此重复,知道合并成一个长度为n的有序表位置,这种排序方法称为2路归并排序

    递归形式的2路归并排序算法是基于分治的。
    分解:将含有n个元素的待排序表分成各含\(n/2\)个元素的子表,采用2路归并排序算法对两个子表递归地进行排序。
    合并:合并两个已排序的子表得到排序结果。

  • 具体实现:

  • 性能分析:
    • 空间效率:O(n)
      Merge()操作中,辅助空间刚好占用n个单元

    • 时间效率:O(\(nlog_2n\))
      每趟归并的时间复杂度为O(n),共需进行?log_2n?趟归并
      • 最好情况:O(\(nlog_2n\))
      • 最坏情况:O(\(nlog_2n\))
      • 平均情况:O(\(nlog_2n\))
    • 稳定性:稳定
      由于Merge()操作不会改变相同关键字记录的相对次序

    • 适用性:顺序存储的线性表

基数排序

  • 基本思想:
    将每个位数排好序,由低到高(由高到低)

  • 具体实现:

  • 性能分析:
    • 空间效率:O(r)
      一趟排序需要的辅助空间为r(r个队列),但以后的排序中会重复使用这些队列

    • 时间效率:O(d(n+r))
      基数排序需要进行d趟分配和收集,一趟分配需要O(n),一趟收集需要O(r)
      • 最好情况:O(d(n+r))
      • 最坏情况:O(d(n+r))
      • 平均情况:O(d(n+r))
    • 稳定性:稳定
      对于基数排序算法而言,按位排序是必须是稳定的,所以保证了基数排序的稳定性

    • 适用性:顺序存储的线性表

以上是关于数据结构之排序的主要内容,如果未能解决你的问题,请参考以下文章

数据结构之排序算法

SDUT 3403 数据结构实验之排序六:希尔排序

SDUT 3403 数据结构实验之排序六:希尔排序

数据结构实验之排序八:快速排序

数据结构之排序

数据结构之排序