数据结构-排序
创始人
2025-05-30 12:07:47
0

排序

基本概念和排序方法概述

  • 排序:将一组杂乱无章的数据按一定规律顺次排列起来。即,将无序序列排成一个有序序列(由小到大或由大到小)的运算。

    • 如果参加排序的数据结点包含多个数据域,那么排序往往是针对其中某个域而言。
  • 排序的应用非常广泛

    • 软件中直接应用
    • 程序中间接应用
      • 二分法查找
      • 最短路径、最小生成树
  • 排序方法的分类

    • 按数据存储介质:内部排序和外部排序

      • 内部排序:数据量不大、数据在内存,无序内外存交换数据

      • 外部排序:数据量较大、数据在外存(文件排序)

        ​ 外部排序时,要将数据分批调入内存来排序,中间结果还要及时放入外存,显然外部排序要复杂得多。

    • 比较器个数:串行排序和并行排序

      • 串行排序单处理机(同一时刻比较一对元素)
      • 并行排序多处理机(同一时刻比较多对元素)
    • 主要操作:比较排序和基数排序

      • 比较排序:用比较的方法

        插入排序、交换排序、选择排序、归并排序

      • 基数排序:不比较元素的大小,仅仅根据元素本身的取值确定其有序位置

    • 辅助空间:原地排序和非原地排序

      • 原地排序:辅助空间用量为O(1)的排序方法(所占的辅助存储空间与参加排序的数据量大小无关
      • 非原地排序:辅助空间用量超过O(1)的排序方法
    • 稳定性:稳定排序和非稳定排序

      • 稳定排序:能够使任何数值相等的元素,排序以后相对次序不变
      • 非稳定性排序:不是稳定排序的方法
      • 排序的稳定性只对结构类型数据排序有意义
      • 排序方法是否稳定,并不能衡量一个排序算法的优劣
    • 自然性:自然排序和非自然排序

      • 自然排序:输入数据越有序,排序的速度越快的排序方法
      • 非自然排序:不是自然排序的方法

    • 按排序依据原则

      • 插入排序:直接插入排序、折半插入排序、希尔排序
      • 交换排序:冒泡排序、快速排序
      • 选择排序:简单选择排序、堆排序
      • 归并排序:2-路归并排序
      • 基数排序
    • 按排序所需工作量

      • 简单的排序方法:T(n)=O(n^2)
      • 先进的排序方法:T(n)=O(nlogn)
      • 基数排序:T(n)=O(d.n)
  • 存储结构——记录序列以顺序表存储

    #define MAXSIZE 20		//设记录不超过20个
    typedef int KeyType;	//设关键字为整型量(int型)typedef struct{		//定义每个记录KeyType key;	//关键字InfoType otherinfo;	//其它数据项
    }RedType;	//Record Typetypedef struct{				//定义顺序表的结构RedType r[MAXSIZE+1];	//存储顺序表的向量//r[0]一般作哨兵或缓冲区int length;				//顺序表的长度
    }SqList;
    

插入排序

基本思想:每步将一个待排序的对象,按其关键码大小,插入到前面已经排好序的一组对象的适当位置上,直到对象全部插入为止。

即边插入边排序,保证子序列中随时都是排好序的

  • 基本操作:有序插入

    • 在有序序列中插入一个元素,保持序列有序,有序长度不断增加。
    • 起初,a[0]是长度为1的子序列。然后,逐一将a[1]至a[n-1]插入到有序子序列中。
    • 在插入a[i]前,数组a的前半段(a[0] ~ a[i-1])是有序段,后半段(a[i] ~ a[n-1])是停留于输入次序的“无序段”。
    • 插入a[i]使a[0] ~ a[i-1]有序,也就是**要为a[i]找到有序位置j(0≤j≤i),将a[i]插入在a[j]的位置上。
  • 插入排序的种类

    • 顺序法定位插入位置——直接插入排序

      1. 复制插入元素 x=a[i];
      2. 记录后移,查找插入位置 for(j=i-1;j>=0&&x
      3. 插入到正确位置 a[j+1]=x;

      使用“哨兵”

      1. 复制为哨兵 L.r[0]=L.r[i];
      2. 记录后移,查找插入位置 for(j=i-1;L.r[0].key
      3. 插入到正确位置 L.r[j+1]=L.r[0];

      void InsertSort(SqList &L){int i,j;for(i=2;i<=L.length;++i){if(L.r[i].key	//若“<”,需将L.r[i]插入有序子表L.r[0]=L.r[i];				//复制为哨兵for(j=i-1;L.r[0].keyL.r[j+1]=L.r[j];		//记录后移}L.r[j+1]=L.r[0];			//插入到正确位置}}
      }
      

      时间复杂度结论

      • 原始数据越接近有序,排序速度越快
      • 最坏情况下(输入数据是逆序的)Tw(n)=O(n^2)
      • 平均情况下,耗时差不多是最坏情况的一半 Te(n)=O(n^2)
      • 要提高查找速度
        • 减少元素的比较次数
        • 减少元素的移动次数
    • 二分法定位插入位置——二分插入排序

      void BInsertSort(SqList &L){for(i=2;i<=L.length;++i){	//依次插入第2~第n个元素L.r[0]=L.r[i];	//当前插入元素存到"哨兵"位置low=1;	high=i-1;	//采用二分查找法查找插入位置while(low<=high){mid=(low+high)/2;if(L.r[0].key=high+1;--j) L.r[j+1]=L.r[j];	//移动元素L.r[high+1]=L.r[0];	//插入到正确位置}
      }
      

      时间复杂度为O(n^2)

      空间复杂度为O(1)

      是一种稳定的排序方法

    • 缩小增量多遍插入排序——希尔排序

      基本思想:先将整个待排记录序列分割成若干子序列,分别进行直接插入排序,待整个序列中的记录“基本有序”时,再对全体记录进行一次直接插入排序

      思路:

      1. 定义增量序列Dk:DM>DM-1>…>D1=1
      2. 对每个DK进行“DK-间隔”插入排序(k=M,M-1,…,1)

      特点:

      • 一次移动,移动位置较大,跳跃式地接近排序后的最终位置
      • 最后一次只需要少量移动
      • 增量序列必须是递减的,最后一个必须是1
      • 增量序列应该是互质的
      void ShellSort(Sqlist &L,int dlta[],int t){//主程序//按增量序列dlta[0..t-1]对顺序表L作希尔排序。for(k=0;k//其中某一趟的排序操作//对顺序表L进行一趟增量为dk的Shell排序,dk为步长因子for(i=dk+1;i<=L.length;++i){r[0]=r[i];for(j=i-dk;j>0&&(r[0].key

      算法分析:

      • Hibbard增量序列
        • Dk=2^(k-1)——相邻元素互质
        • 最坏情况:T_worst=O(n^(3/2))
        • 猜想:T_avg=O(n^(5/4))
      • Sedgewick增量序列
        • {1,5,19,41,109,…}——9 * 4 ^ i - 9 * 2 ^ i + 1或4 ^ i - 3 * 2 ^ i + 1
        • 猜想:T_avg=O(n^(7/6)) T_worst=O(n^(4/3))
      • 时间复杂度是n和d的函数:O(n^1.25) ~ O(1.6n^1.25)——经验公式
      • 空间复杂度为O(1)
      • 是一种不稳定的排序方法

      如何选择最佳d序列,目前尚未解决

      • 最后一个增量值必须为1,无除了1之外的公因子
      • 不宜在链式存储结构上实现

交换排序

**基本思想:**两两比较,如果发生逆序则交换,直到所有记录都排好序为止。

常见的交换排序方法:冒泡排序O(n^2)、快速排序O(nlog_2 n)

冒泡排序——基于简单交换思想

基本思想:每趟不断将记录两两比较,并按“前小后大”规则交换

void bubble_sort(SqList &L){int m,i,j;	RedType x;	//交换时临时存储for(m=1;m<=n-1;m++){	//总共需要mfor(j=1;j<=n-m;j++){if(L.r[j].key>L.r[j+1].key){//发生逆序x=L.r[j];L.r[j]=L.r[j+1];L.r[j+1]=x;//交换}}}
}

**优点:**每趟结束时,不仅能挤出一个最大值到最后面位置,还能同时部分理顺其他元素;

若某躺排序结束后已理顺,则可结束算法

改进:

void bubble_sort(SqList &L){int m,i,j,flag=1;RedType x;//flag作为是否有交换的标记for(m=1;m<=n-1&&flag==1;m++){flag=0;for(j=1;j<=n-m;j++){if(L.r[j].key>L.r[j+1],key){flag=1;//发生交换,flag置为1,若本趟没发生交换,flag保持为0x=L.r[j];L.r[j]=L.r[j+1];L.r[j+1]=x;}}}
}

算法评价

  • 冒泡排序最好时间复杂度是O(n)
  • 冒泡排序最坏时间复杂度为O(n^2)
  • 冒泡排序平均时间复杂度为O(n^2)
  • 冒泡排序中增加一个辅助空间temp,辅助空间为S(n)=O(1)
  • 冒泡排序是稳定

快速排序——改进的交换排序

基本思想:

  • 任取一个元素为中心
  • 所有比它的元素一律前放,比它的元素一律后放,形成左右两个子表
  • 对各子表重新选择中心元素并依此规则调整
  • 直到每个子表的元素只剩一个

通过一趟排序,将待排序记录分割成独立的两部分,其中一部分记录的关键字均比另一部分记录的关键字小,则可分别对这两部分记录进行排序,以达到整个序列有序

void main(){QSort(L,1,L.length);
}void QSort(SqList &L,int low,int high){if(lowpivotloc=Partition(L,low,high);//将L.r[low...high]一分为二,pivotloc为枢轴元素排好序的位置QSort(L,low,pivotloc-1);//对低子表递归排序QSort(L,pivotloc+1,high);//对高子表递归排序}
}int Partition(SqList &L,int low,int high){L.r[0]=L.r[low];privotkey=L.r[low].key;while(lowwhile(low=pivokey)	--high;L.r[low]=L.r[high];while(low

算法分析

时间复杂度

平均计算时间是O(nlog_2 n)

  • Qsort(): O(log_2 n)
  • Partition(): O(n)

实验结果表明:就平均计算时间而言,快速排序是我们所讨论的所有内排序方法中最好的一个。

空间复杂度

快速排序不是原地排序

由于程序中使用了递归,需要递归调用栈的支持,而栈的长度取决于递归调用的深度。(即使不用递归,也需要用用户栈)

  • 在平均情况下:需要**O(logn)**的栈空间
  • 最坏情况下:栈空间可达O(n)。

稳定性

快速排序是一种不稳定的排序方法

自然性

快速排序不适于对原本有序或基本有序的记录序列进行排序。

划分元素的选取是影响时间性能的关键

输入数据次序越乱,所选划分元素值的随机性越好,排序速度越快,快速排序不是自然排序方法

改变划分元素的选取方法,至多只能改变算法平均情况下的时间性能,无法改变最坏情况下的时间性能。即最坏情况下,快速排序的时间复杂性总是O(n^2)

选择排序

简单选择排序

**基本思想:**在待排序的数据中选出最大(小)的元素放在其最终的位置。

基本操作:

  1. 首先通过n-1次关键字比较,从n个记录中找出关键字最小的记录,将它与第一个记录交换
  2. 再通过n-2次比较,从剩余的n-1个记录中找出关键字次小的记录,将它与第二个记录交换
  3. 重复上述操作,共进行n-1趟排序后,排序结束
void SelectSort(SqList &K){for(i=1;ik=i;for(j=i+1;j<=L.length;j++)if(L.r[j].keyL.r[k];//交换}
}

时间复杂度

  • 记录移动次数

    • 最好情况:0
    • 最坏情况:3(n-1)
  • 比较次数:无论待排序列处于什么状态,选择排序所需进行的“比较”次数都相同
    ∑i=1n−1(n−i)=n2(n−1)\sum_{i=1}^{n-1}(n-i)=\frac{n}{2}(n-1) i=1∑n−1​(n−i)=2n​(n−1)

稳定性

  • 简单选择排序是不稳定排序

堆排序

堆的定义:
若n个元素的序列{a1,a2,...,an}满足{ai≤a2iai≤a2i+1或{ai≥a2iai≥a2i+1则分别称该序列{a1,a2,...,an}为小根堆和大根堆若n个元素的序列\{a_1,a_2,...,a_n\}满足\\ \begin{cases} a_i≤a_{2i} \\ a_i≤a_{2i+1} \end{cases} \quad 或 \begin{cases} a_i≥a_{2i} \\ a_i≥a_{2i+1} \end{cases}\\ 则分别称该序列\{a_1,a_2,...,a_n\}为小根堆和大根堆 若n个元素的序列{a1​,a2​,...,an​}满足{ai​≤a2i​ai​≤a2i+1​​或{ai​≥a2i​ai​≥a2i+1​​则分别称该序列{a1​,a2​,...,an​}为小根堆和大根堆
从堆的定义可以看出,堆实质是满足如下性质的完全二叉树二叉树中任一非叶子结点均小于(大于)它的孩子结点

若在输出堆顶的最小值(最大值)后,使得剩余n-1个元素的序列重新建成一个堆,则得到n个元素的次小值(次大值)…如此反复,便能得到一个有序序列,这个过程称之为堆排序

堆的调整

小根堆:

  1. 输出堆顶元素之后,以堆中最后一个元素替代之
  2. 然后将根结点值与左、右子树的根结点值进行比较,并与其中小者进行交换
  3. 重复上述操作,直至叶子结点,将得到新的堆,称这个从堆顶至叶子的调整过程为“筛选
void HeapAdjust(elem R[],int s,int m){/*已知R[s...m]中记录的关键字除R[s]之外均满足堆的定义,本函数调整R[s]的关键字,使R[s...m]成为一个大根堆*/rc=R[s];for(j=2*s;j<=m;j*=2){//沿key较大的孩子结点向下筛选if(j=R[j])break;R[s]=R[j];	s=j;//rc应插入在位置s上}R[s]=rc;//插入
}

堆的建立

单结点的二叉树是堆;

在完全二叉树中所有以叶子结点(序号i>n/2)为根的子树是堆。

这样,我们只需依次将以序号为n/2,n/2-1,…,1的结点为根的子树均调整为堆即可。

将初始无序的R[1]到R[n]建成一个小根堆,可用以下语句实现:

for(i=n/2;i>=1;i--)HeapAdjust(R,i,n);

由以上分析知:

若对一个无序序列减建堆,然后输出根;重复该过程就可以由一个无序序列输出有序序列。

实质上,堆排序就是利用完全二叉树中父结点与孩子结点之间的内在关系来排序的。

void HeapSort(elem R[]){int i;for(i=n/2;i>=1;i--)HeapAdjust(R,i,n);//建初始堆for(i=n;i>1;i--){//进行n-1趟排序Swap(R[1],R[i]);//根与最后一个元素交换HeapAdjust(R,1,i-1);//对R[1]到R[i-1]重新建堆}
}

算法性能分析

  • 初始堆化所需时间不超过O(n)
  • 排序阶段(不含初始堆化)
    • 一次重新堆化所需时间不超过O(logn)
    • n-1次循环所需时间不超过O(nlogn)

Tw(n)=O(n)+O(nlogn)=O(nlogn)

堆排序的时间主要耗费在建初始堆和调整建新堆时进行的反复筛选上。堆排序在最坏情况下,其时间复杂度也为O(nlog_2 n),这是堆排序的最大优点。无论待排序列中的记录是正序还是逆序排列,都不会使堆排序处于”最好“或”最坏“的状态。

另外,堆排序仅需一个记录大小供交换用的辅助存储空间。

然而堆排序是一种不稳定的排序方法,它不适用于待排序记录个数n较少的情况,但对于n较大的文件还是很有效的。

归并排序

基本思想:将两个或两个以上的有序子序列”归并“为一个有序序列。

在内部排序中,通常采用的是2-路归并排序

即:将两个位置相邻的有序子序列R[l…m]和R[m+1…n]归并为一个有序序列R[l…n]

算法分析

  • 时间效率:O(nlog2n)

  • 空间效率:O(n)

    因为需要一个与原始序列同样大小的辅助序列(R1)。这正是此算法的缺点。

  • 稳定性:稳定

基数排序

基本思想:分配+收集

也叫桶排序箱排序:设置若干个箱子,将关键字为k的记录放入第k个箱子,然后在按序号将非空的连接。

基数排序:数字是有范围的,均由0-9这十个数字组成,则只需设置十个箱子,相继按个、十、百…进行排序。

算法分析

  • 时间效率:O(k * (n+m))

    k:关键字个数,

    m:关键字取值范围为m个值

  • 空间效率:O(n+m)

  • 稳定性:稳定

各种排序方法的综合比较

  1. 时间性能

    1. 按平均的时间性能来分,有三类排序方法:

      • 时间复杂度为O(nlogn)的方法有:

        快速排序、堆排序和归并排序,其中以快速排序为最好;

      • 时间复杂度为O(n^2)的方法有:

        直接插入排序、冒泡排序和简单选择排序,其中以直接插入为最好,特别是对那些对关键字近似有序的记录序列尤为如此;

      • 时间复杂度为O(n)的方法只有:基数排序。

    2. 当待排记录序列按关键字顺序有序时,直接插入排序和冒泡排序能达到O(n)的时间复杂度;而对于快速排序而言,这是最不好的情况,此时的时间性能退化为O(n^2),因此是应该尽量避免的情况。

    3. 简单选择排序、堆排序和归并排序的时间性能不随记录序列中关键字的分布而改变。

  2. 空间性能

    指的是排序过程中所需的辅助空间大小

    1. 所有的简单排序方法(包括:直接插入、冒泡和简单选择)和堆排序的空间复杂度为O(1)
    2. 快速排序为O(logn),为栈所需的辅助空间
    3. 归并排序所需辅助空间最多,其空间复杂度为O(n)
    4. 链式基数排序需附设队列首尾指针,则空间复杂度为O(rd)
  3. 排序方法的稳定性能

    • 稳定的排序方法指的是,对于两个关键字相等的记录,它们在序列中的相对位置,在排序之前和经过排序之后,没有改变。
    • 当对多关键字的记录序列进行LSD方法排序时,必须采用稳定的排序方法。
    • 对于不稳定的排序方法,只要能举出一个实例说明即可。
    • 快速排序和堆排序是不稳定的排序方法。
  4. 关于“排序方法的时间复杂度的下限”

    • 除基数排序外,其它方法都是基于”比较关键字“进行排序的排序方法,可以证明,这类排序法可能达到的最快时间复杂度为O(nlogn)。

      (基数排序不是基于”比较关键字“的排序方法,所以它不受这个限制)。

    • 可以用一棵判定树来描述这类基于”比较关键字“进行排序的排序方法。

相关内容

热门资讯

linux入门---制作进度条 了解缓冲区 我们首先来看看下面的操作: 我们首先创建了一个文件并在这个文件里面添加了...
C++ 机房预约系统(六):学... 8、 学生模块 8.1 学生子菜单、登录和注销 实现步骤: 在Student.cpp的...
JAVA多线程知识整理 Java多线程基础 线程的创建和启动 继承Thread类来创建并启动 自定义Thread类的子类&#...
【洛谷 P1090】[NOIP... [NOIP2004 提高组] 合并果子 / [USACO06NOV] Fence Repair G ...
国民技术LPUART介绍 低功耗通用异步接收器(LPUART) 简介 低功耗通用异步收发器...
城乡供水一体化平台-助力乡村振... 城乡供水一体化管理系统建设方案 城乡供水一体化管理系统是运用云计算、大数据等信息化手段࿰...
程序的循环结构和random库...   第三个参数就是步长     引入文件时记得指明字符格式,否则读入不了 ...
中国版ChatGPT在哪些方面... 目录 一、中国巨大的市场需求 二、中国企业加速创新 三、中国的人工智能发展 四、企业愿景的推进 五、...
报名开启 | 共赴一场 Flu... 2023 年 1 月 25 日,Flutter Forward 大会在肯尼亚首都内罗毕...
汇编00-MASM 和 Vis... Qt源码解析 索引 汇编逆向--- MASM 和 Visual Studio入门 前提知识ÿ...
【简陋Web应用3】实现人脸比... 文章目录🍉 前情提要🌷 效果演示🥝 实现过程1. u...
前缀和与对数器与二分法 1. 前缀和 假设有一个数组,我们想大量频繁的去访问L到R这个区间的和,...
windows安装JDK步骤 一、 下载JDK安装包 下载地址:https://www.oracle.com/jav...
分治法实现合并排序(归并排序)... 🎊【数据结构与算法】专题正在持续更新中,各种数据结构的创建原理与运用✨...
在linux上安装配置node... 目录前言1,关于nodejs2,配置环境变量3,总结 前言...
Linux学习之端口、网络协议... 端口:设备与外界通讯交流的出口 网络协议:   网络协议是指计算机通信网...
Linux内核进程管理并发同步... 并发同步并发 是指在某一时间段内能够处理多个任务的能力,而 并行 是指同一时间能够处理...
opencv学习-HOG LO... 目录1. HOG(Histogram of Oriented Gradients,方向梯度直方图)1...
EEG微状态的功能意义 导读大脑的瞬时全局功能状态反映在其电场结构上。聚类分析方法一致地提取了四种头表面脑电场结构ÿ...
【Unity 手写PBR】Bu... 写在前面 前期积累: GAMES101作业7提高-实现微表面模型你需要了解的知识 【技...