当前位置: 首页 > news >正文

数据结构:排序

📌目录

  • 📊 一,排序的基本概念与方法概述
    • (一)基本概念
    • (二)内部排序方法的分类
    • (三)待排序记录的存储方式
      • 1. 顺序存储结构
      • 2. 链式存储结构
    • (四)排序算法效率的评价指标
      • 1. 时间复杂度
      • 2. 空间复杂度
      • 3. 稳定性与实用性
  • 🔍 二,插入排序
    • (一)直接插入排序
      • 核心思路
      • 示例(升序排序)
      • 代码实现(顺序存储,哨兵优化)
      • 性能分析
    • (二)折半插入排序
      • 核心思路
      • 代码实现
      • 性能分析
    • (三)希尔排序
      • 核心思想
      • 示例(增量序列5→3→1,升序)
      • 代码实现(增量序列d = d/2)
      • 性能分析
  • 🔄 三,交换排序
    • (一)冒泡排序
      • 核心思路
      • 示例(升序排序)
      • 代码实现(优化版:提前终止)
      • 性能分析
    • (二)快速排序
      • 核心思路
      • 分区操作(Hoare法)
      • 示例(升序排序,基准选第一个元素)
      • 代码实现(Hoare分区法)
      • 性能分析
  • 🔍 四,选择排序
    • (一)简单选择排序
      • 核心思路
      • 示例(升序排序)
      • 代码实现(顺序存储)
      • 性能分析
    • (二)树形选择排序
      • 核心思想
      • 示例(升序排序,元素`[49,38,65,97,76,13,27,49]`)
      • 性能分析
    • (三)堆排序
      • 1. 堆的定义与性质
      • 2. 核心思路(以大根堆实现升序排序为例)
      • 3. 核心操作:堆调整(Heapify)
      • 4. 代码实现
      • 5. 性能分析
  • 🔗 五,归并排序
    • 核心思想
      • 示例(升序排序,序列`[49,38,65,97,76,13,27,49]`)
    • 核心操作:合并两个有序子序列
    • 代码实现(二路归并排序)
    • 性能分析
  • 🔢 六,基数排序
    • (一)多关键字的排序
      • 两种排序方式
    • (二)链式基数排序
      • 核心概念
      • 核心步骤(以十进制整数排序为例,基数`r=10`,位数`d=3`)
      • 示例(排序序列`[329, 457, 657, 839, 436, 720, 355]`,`d=3`,`r=10`)
      • 代码实现(十进制整数链式基数排序)
      • 性能分析
  • 📥 七,外部排序
    • (一)外部排序的基本方法
      • 1. 基本流程(以磁盘排序为例)
      • 2. 示例(内存容量`m=4`,待排序数据`[9,3,1,7,4,2,8,6,5]`)
      • 3. 关键指标:归并趟数
    • (二)多路平衡归并的实现
      • 1. 败者树的结构与原理
        • 核心步骤
      • 2. 示例(`t=3`路归并,归并段`Run1: [1,4,7]`、`Run2: [2,5,8]`、`Run3: [3,6,9]`)
      • 3. 败者树的优势
      • 4. 归并路数`t`的限制
    • (三)置换-选择排序
      • 1. 核心思想
      • 2. 示例(内存容量`m=3`,待排序数据`[5,3,7,6,2,8,1,4]`)
      • 3. 优势与特点
    • (四)最佳归并树
      • 1. 归并代价的定义
      • 2. 最佳归并树的原理
      • 3. 示例(`t=3`路归并,初始归并段长度`[2,3,5,7,11,13,17]`,`k=7`)
      • 4. 虚归并段的添加规则
  • 📝 章结
    • 1. 排序算法的分类与核心特征
    • 2. 关键算法的适用场景


📊 一,排序的基本概念与方法概述

在这里插入图片描述

排序是数据处理中最基础且核心的操作,通过按特定规则(如数值大小、字母顺序)重新排列数据,为高效查询、统计等后续操作奠定基础。从手机通讯录的按姓名排序,到数据库查询结果的按时间排序,排序算法的应用无处不在。

(一)基本概念

  • 排序(Sorting):将一组无序的数据元素(记录),按照某个关键字(可直接比较的属性,如学号、成绩)重新排列为有序序列(升序或降序)的过程。
  • 关键字(Key):用于确定数据元素排列顺序的属性,分为:
    • 主关键字:唯一标识数据元素(如身份证号),排序后元素位置唯一;
    • 次关键字:非唯一标识(如成绩),排序后可能存在多个元素关键字相同(称为“相等元素”)。
  • 稳定排序与不稳定排序
    • 稳定排序:若排序前两个相等元素的相对位置,在排序后保持不变(如“学生A(成绩80,序号1)”和“学生B(成绩80,序号2)”,排序后仍为A在前、B在后);
    • 不稳定排序:相等元素的相对位置可能改变。
  • 内排序与外排序
    • 内部排序(简称“内排”):待排序数据全部存放在内存中,排序过程不依赖外部存储;
    • 外部排序(简称“外排”):待排序数据量过大,无法全部放入内存,需借助磁盘等外部存储设备完成排序。本文聚焦内部排序

(二)内部排序方法的分类

根据排序过程中核心操作的不同,内部排序可分为五大类:

排序类别核心思想典型算法
插入排序将待排序元素逐个插入到已排序序列的合适位置,逐步扩大有序序列范围直接插入、折半插入、希尔排序
交换排序通过比较相邻元素,若不符合顺序则交换位置,直至所有元素有序冒泡排序、快速排序
选择排序每次从无序序列中选出关键字最小(或最大)的元素,放入有序序列的末尾简单选择、堆排序
归并排序将序列逐步拆分为子序列,分别排序后再合并为有序序列二路归并排序
基数排序按关键字的各位(如个位、十位)依次排序,无需比较关键字大小LSD基数排序、MSD基数排序

(三)待排序记录的存储方式

内部排序中,待排序数据通常采用以下两种存储结构:

1. 顺序存储结构

用数组存储记录,通过下标访问和修改元素,操作直观高效。

#define MAXSIZE 100  // 最大记录数
typedef int KeyType;  // 关键字类型(假设为整数)// 记录结构
typedef struct {KeyType key;      // 关键字// 其他数据项(如姓名、年龄等)
} RecordType;// 顺序表(存储待排序记录)
typedef struct {RecordType r[MAXSIZE + 1];  // r[0]可作为哨兵或临时存储int length;                 // 实际记录数
} SqList;

优势:随机访问效率高(O(1)),适合大多数排序算法;
劣势:插入、删除元素需移动大量数据(O(n))。

2. 链式存储结构

用链表存储记录,通过指针连接节点,无需连续内存空间。

// 链表节点结构
typedef struct Node {KeyType key;              // 关键字struct Node *next;        // 指向下一节点的指针// 其他数据项
} Node, *LinkList;

优势:插入、删除元素只需修改指针,无需移动数据;
劣势:无法随机访问,仅适合部分排序算法(如链表插入排序)。

(四)排序算法效率的评价指标

衡量排序算法性能的核心指标包括时间复杂度空间复杂度,同时需考虑算法的稳定性实用性(如代码复杂度、对数据的适应性)。

1. 时间复杂度

主要取决于排序过程中比较次数(关键字间的比较操作)和移动次数(元素位置的调整操作),通常按最坏情况、平均情况和最好情况分析:

  • 最坏情况:待排序序列与目标顺序完全相反(如升序排序序列为降序);
  • 最好情况:待排序序列已为目标顺序;
  • 平均情况:随机分布的待排序序列。

常见时间复杂度等级:O(n²)(简单排序,如冒泡、直接插入)、O(n log n)(高效排序,如快速、堆排序)、O(n)(线性排序,如基数排序,仅适用于特定场景)。

2. 空间复杂度

指排序过程中额外占用的内存空间(不包括待排序数据本身):

  • 原地排序:空间复杂度为O(1),仅需少量临时变量(如直接插入、冒泡、堆排序);
  • 非原地排序:需额外占用O(n)或更多空间(如归并排序需O(n)辅助空间,基数排序需O®(r为基数)辅助空间)。

3. 稳定性与实用性

  • 稳定性:在需要保持相等元素原始相对位置的场景(如按成绩排序后需保留录入顺序),稳定排序更适用;
  • 实用性:简单排序(如直接插入、冒泡)代码简洁,适合小规模数据;高效排序(如快速、堆排序)代码较复杂,但适合大规模数据;基数排序虽时间复杂度低,但仅适用于关键字可分解的场景(如整数、字符串)。

🔍 二,插入排序

插入排序的核心思想是“逐步构建有序序列”:将待排序序列分为“有序区”和“无序区”,每次从无序区取出一个元素,插入到有序区的合适位置,直至无序区为空。

(一)直接插入排序

直接插入排序是最简单的插入排序算法,适合小规模数据或基本有序的数据。

核心思路

  1. 初始时,有序区为序列的第一个元素(r[1],假设数组从1开始),无序区为r[2]~r[n];
  2. 对无序区的每个元素r[i](i从2到n):
    • 将r[i]暂存到临时变量(或利用r[0]作为“哨兵”,避免数组越界判断);
    • 从有序区的末尾(r[i-1])开始向前比较,若r[j] > r[i](升序),则将r[j]后移一位;
    • 找到插入位置j+1,将r[i]放入该位置;
  3. 重复步骤2,直至i=n。

示例(升序排序)

待排序序列:[49, 38, 65, 97, 76, 13, 27, 49]

  • i=2(r[2]=38):有序区[49],38<49,插入到49前 → 有序区[38, 49];
  • i=3(r[3]=65):65>49,直接插入末尾 → 有序区[38, 49, 65];
  • 以此类推,最终得到有序序列[13, 27, 38, 49, 49, 65, 76, 97]。

代码实现(顺序存储,哨兵优化)

// 直接插入排序(升序)
void InsertSort(SqList *L) {int i, j;for (i = 2; i <= L->length; i++) {  // 无序区从第2个元素开始if (L->r[i].key < L->r[i-1].key) {  // 若当前元素小于前一个,需插入L->r[0] = L->r[i];  // r[0]作为哨兵,暂存当前元素for (j = i-1; L->r[j].key > L->r[0].key; j--) {L->r[j+1] = L->r[j];  // 元素后移}L->r[j+1] = L->r[0];  // 插入到合适位置}}
}

性能分析

  • 时间复杂度
    • 最好情况(已有序):只需比较n-1次,无需移动元素 → O(n);
    • 最坏情况(逆序):比较次数O(n²),移动次数O(n²) → O(n²);
    • 平均情况:O(n²)。
  • 空间复杂度:O(1)(仅用r[0]和临时变量),原地排序;
  • 稳定性:稳定(相等元素不会后移,相对位置不变)。

(二)折半插入排序

直接插入排序在有序区查找插入位置时采用“顺序查找”,效率较低。折半插入排序通过“折半查找”(二分查找)优化插入位置的查找过程,减少比较次数。

核心思路

  1. 有序区和无序区的划分与直接插入排序一致;
  2. 对无序区元素r[i],在有序区r[1]~r[i-1]中通过折半查找确定插入位置;
  3. 将插入位置后的元素后移,放入r[i]。

代码实现

// 折半插入排序(升序)
void BInsertSort(SqList *L) {int i, j, low, high, mid;for (i = 2; i <= L->length; i++) {L->r[0] = L->r[i];  // 哨兵暂存当前元素low = 1; high = i-1;// 折半查找插入位置while (low <= high) {mid = (low + high) / 2;if (L->r[0].key < L->r[mid].key) {high = mid - 1;  // 插入位置在左半区} else {low = mid + 1;   // 插入位置在右半区}}// 元素后移(从high+1到i-1)for (j = i-1; j >= high+1; j--) {L->r[j+1] = L->r[j];}L->r[high+1] = L->r[0];  // 插入}
}

性能分析

  • 时间复杂度
    • 查找插入位置的时间从O(n)优化为O(log n),但元素移动次数仍为O(n²);
    • 总时间复杂度仍为O(n²)(移动操作占主导),但实际效率优于直接插入排序。
  • 空间复杂度:O(1),原地排序;
  • 稳定性:稳定(折半查找不改变相等元素的相对位置)。

(三)希尔排序

希尔排序(又称“缩小增量排序”)是对直接插入排序的进一步优化,通过“分组插入排序”减少元素移动距离,适合大规模数据。

核心思想

  1. 选择一个增量序列(如d₁, d₂, …, dₖ,其中d₁ > d₂ > … > dₖ=1);
  2. 按增量dᵢ将序列分为dᵢ组,每组包含n/dᵢ个元素(元素下标相差dᵢ);
  3. 对每组分别执行直接插入排序;
  4. 减小增量dᵢ(如dᵢ = dᵢ/2),重复步骤2-3,直至dᵢ=1(此时整个序列为一组,执行最后一次直接插入排序)。

示例(增量序列5→3→1,升序)

待排序序列:[49, 38, 65, 97, 76, 13, 27, 49, 55, 4]

  • d=5:分为5组[49,13]、[38,27]、[65,49]、[97,55]、[76,4],每组排序后 → [13,27,49,55,4,49,38,65,97,76];
  • d=3:分为3组[13,55,38,97]、[27,4,65,76]、[49,49],每组排序后 → [13,4,49,38,27,49,55,65,97,76];
  • d=1:对整个序列执行直接插入排序 → [4,13,27,38,49,49,55,65,76,97]。

代码实现(增量序列d = d/2)

// 希尔排序(升序)
void ShellSort(SqList *L) {int i, j, d;d = L->length / 2;  // 初始增量while (d >= 1) {// 对每组执行直接插入排序for (i = d+1; i <= L->length; i++) {if (L->r[i].key < L->r[i-d].key) {  // 当前元素小于组内前一个元素L->r[0] = L->r[i];  // 哨兵暂存for (j = i-d; j > 0 && L->r[j].key > L->r[0].key; j -= d) {L->r[j+d] = L->r[j];  // 组内元素后移}L->r[j+d] = L->r[0];  // 插入}}d /= 2;  // 缩小增量}
}

性能分析

  • 时间复杂度:依赖增量序列的选择,难以精确计算,通常介于O(n¹.³)(增量序列为dᵢ = dᵢ/2)和O(n²)(最坏情况)之间,平均性能接近O(n log n);
  • 空间复杂度:O(1),原地排序;
  • 稳定性:不稳定(分组排序可能导致相等元素分到不同组,相对位置改变);
  • 优势:对大规模数据的排序效率远高于直接插入和折半插入排序。

🔄 三,交换排序

交换排序的核心思想是“通过比较和交换元素位置”逐步消除逆序(逆序指关键字大小与目标顺序相反的元素对),直至序列有序。

(一)冒泡排序

冒泡排序是最简单的交换排序算法,通过相邻元素的反复比较和交换,将关键字最大(或最小)的元素逐步“冒泡”到序列末尾。

核心思路

  1. 从序列头部开始,依次比较相邻元素r[j]r[j+1]j从1到n-i+1i为排序轮次);
  2. r[j].key > r[j+1].key(升序),则交换两者位置;
  3. 每轮排序后,无序区中最大的元素“冒泡”到有序区的末尾;
  4. 重复步骤1-3,直至某轮排序未发生任何交换(序列已有序)。

示例(升序排序)

待排序序列:[49, 38, 65, 97, 76, 13, 27, 49]

  • 第1轮:比较并交换 → [38,49,65,76,13,27,49,97](97冒泡到末尾);
  • 第2轮:比较并交换 → [38,49,65,13,27,49,76,97](76冒泡到倒数第二位);
  • 第3轮:比较并交换 → [38,49,13,27,49,65,76,97](65冒泡到倒数第三位);
  • 直至第6轮,序列已有序([13,27,38,49,49,65,76,97]),无需继续交换。

代码实现(优化版:提前终止)

// 冒泡排序(升序,优化版)
void BubbleSort(SqList *L) {int i, j;bool flag = true;  // 标记本轮是否发生交换,优化无交换时提前终止for (i = 1; i <= L->length - 1 && flag; i++) {  // i为排序轮次flag = false;  // 初始化为“无交换”// 每轮遍历无序区(前L->length - i + 1个元素)for (j = 1; j <= L->length - i; j++) {if (L->r[j].key > L->r[j+1].key) {  // 逆序则交换RecordType temp = L->r[j];L->r[j] = L->r[j+1];L->r[j+1] = temp;flag = true;  // 标记“有交换”}}}
}

性能分析

  • 时间复杂度
    • 最好情况(已有序):仅需遍历1轮,无交换操作,比较次数n-1O(n)
    • 最坏情况(逆序):需遍历n-1轮,比较次数n(n-1)/2,交换次数n(n-1)/2O(n²)
    • 平均情况:O(n²)
  • 空间复杂度:仅用1个临时变量存储交换元素 → O(1),原地排序;
  • 稳定性:稳定(相邻元素交换时,相等元素不会改变相对位置);
  • 适用场景:小规模数据或基本有序的数据,代码简洁易实现。

(二)快速排序

快速排序(Quick Sort)由Hoare于1962年提出,是“分治法”的经典应用。其核心是通过“基准元素”将序列划分为两部分,左部分元素均小于基准,右部分均大于基准,再递归处理两部分,最终实现整体有序。

核心思路

  1. 选择基准:从序列中选择一个元素作为基准(通常选第一个、最后一个或中间元素);
  2. 分区操作:将序列划分为左、右两个子序列,左子序列所有元素≤基准,右子序列所有元素≥基准,基准元素放在最终位置(称为“枢轴”);
  3. 递归排序:对左、右子序列分别重复步骤1-2,直至子序列长度为1(已有序)。

分区操作(Hoare法)

以第一个元素为基准,通过双指针交替移动实现分区:

  • 初始化左指针low(指向序列头部)、右指针high(指向序列尾部);
  • 右指针向左移动,找到第一个≤基准的元素,赋值给low位置;
  • 左指针向右移动,找到第一个≥基准的元素,赋值给high位置;
  • 重复上述两步,直至low = high,将基准元素放入该位置,完成分区。

示例(升序排序,基准选第一个元素)

待排序序列:[49, 38, 65, 97, 76, 13, 27, 49]

  1. 基准=49,low=1high=8
    • high左移,找到27(≤49),赋值给low=1[27, 38, 65, 97, 76, 13, 27, 49]
    • low右移,找到65(≥49),赋值给high=8[27, 38, 65, 97, 76, 13, 65, 49]
    • 重复移动,最终low=high=6,基准49放入位置6 → 分区结果:[27,38,13,49,76,97,65,49](左:[27,38,13],基准:49,右:[76,97,65,49]);
  2. 递归处理左子序列[27,38,13]和右子序列[76,97,65,49],最终得到有序序列:[13,27,38,49,49,65,76,97]

代码实现(Hoare分区法)

// 分区操作:返回基准元素最终位置
int Partition(SqList *L, int low, int high) {L->r[0] = L->r[low];  // 基准元素存入r[0](哨兵)KeyType pivot = L->r[low].key;  // 基准值while (low < high) {// 右指针左移,找≤基准的元素while (low < high && L->r[high].key >= pivot) {high--;}L->r[low] = L->r[high];  // 赋值给左指针位置// 左指针右移,找≥基准的元素while (low < high && L->r[low].key <= pivot) {low++;}L->r[high] = L->r[low];  // 赋值给右指针位置}L->r[low] = L->r[0];  // 基准元素放入最终位置return low;  // 返回基准位置
}// 快速排序递归函数
void QSort(SqList *L, int low, int high) {if (low < high) {  // 子序列长度≥2时才需排序int pivotpos = Partition(L, low, high);  // 分区,得到基准位置QSort(L, low, pivotpos - 1);  // 排序左子序列QSort(L, pivotpos + 1, high);  // 排序右子序列}
}// 快速排序入口函数
void QuickSort(SqList *L) {QSort(L, 1, L->length);
}

性能分析

  • 时间复杂度
    • 最好情况(每次分区后基准位于序列中间,子序列长度均衡):递归深度log n,每轮比较次数nO(n log n)
    • 最坏情况(序列已有序或逆序,每次分区后子序列长度为n-10):递归深度n,比较次数n(n-1)/2O(n²)(可通过随机选择基准优化);
    • 平均情况:O(n log n),是实际应用中最快的内部排序算法之一。
  • 空间复杂度:递归调用需占用栈空间,栈深度取决于递归深度 → 最好情况O(log n),最坏情况O(n)(可通过尾递归优化或非递归实现降为O(log n));
  • 稳定性:不稳定(分区操作中,基准元素可能与相等元素交换,改变相对位置);
  • 适用场景:大规模数据排序,尤其是随机分布的数据,实际效率优于堆排序和归并排序。

🔍 四,选择排序

选择排序的核心思想是“每次从无序区中选出关键字最小(或最大)的元素,将其放入有序区的末尾”,通过“选择”而非“频繁交换”逐步构建有序序列,减少不必要的元素移动操作。

(一)简单选择排序

简单选择排序(Simple Selection Sort)是选择排序中最基础的算法,逻辑直观易懂,适合小规模数据或对交换操作成本敏感的场景。

核心思路

  1. 分区划分:将待排序序列分为“有序区”(初始为空)和“无序区”(初始为整个序列);
  2. 查找最小值:遍历无序区,找到关键字最小的元素,记录其位置;
  3. 交换元素:将找到的最小元素与无序区的第一个元素交换位置,此时该元素进入有序区;
  4. 迭代完成:重复步骤2-3,直至无序区仅剩1个元素(已自然有序)。

示例(升序排序)

待排序序列:[49, 38, 65, 97, 76, 13, 27, 49]

  • 第1轮:无序区[49,38,65,97,76,13,27,49],最小元素为13(下标6),与无序区第一个元素49交换 → 有序区[13],无序区[38,65,97,76,49,27,49]
  • 第2轮:无序区最小元素为27(下标6),与38交换 → 有序区[13,27],无序区[65,97,76,49,38,49]
  • 第3轮:无序区最小元素为38(下标5),与65交换 → 有序区[13,27,38],无序区[97,76,49,65,49]
  • 依次迭代,最终得到有序序列:[13,27,38,49,49,65,76,97]

代码实现(顺序存储)

// 简单选择排序(升序)
void SelectSort(SqList *L) {int i, j, min_idx;  // min_idx存储无序区最小元素的下标// i为有序区末尾下标,无序区从i开始for (i = 1; i <= L->length - 1; i++) {min_idx = i;  // 初始假设无序区第一个元素为最小// 遍历无序区,找到真正的最小元素for (j = i + 1; j <= L->length; j++) {if (L->r[j].key < L->r[min_idx].key) {min_idx = j;  // 更新最小元素下标}}// 若最小元素不是无序区第一个,交换位置if (min_idx != i) {RecordType temp = L->r[i];L->r[i] = L->r[min_idx];L->r[min_idx] = temp;}}
}

性能分析

  • 时间复杂度
    • 无论序列是否有序,均需遍历n-1轮,每轮遍历无序区需n-i次比较,总比较次数为n(n-1)/2O(n²)
    • 交换次数:最好情况(已有序)为0次,最坏情况(逆序)为n-1次,远少于冒泡排序(最坏n(n-1)/2次交换)。
  • 空间复杂度:仅需1个临时变量存储交换元素 → O(1),属于原地排序。
  • 稳定性:不稳定。例如序列[2, 2, 1],第一轮最小元素1与第一个2交换,导致两个2的相对位置从“前2后2”变为“后2前2”。
  • 适用场景:小规模数据排序,或元素体积较大(如包含大量附加信息)、交换成本高的场景(因交换次数少)。

(二)树形选择排序

树形选择排序(Tree Selection Sort)又称“锦标赛排序”,通过构建二叉树(胜者树)高效筛选无序区中的最小元素,减少重复比较次数,是堆排序的理论基础。

核心思想

  1. 构建胜者树:将无序区所有元素作为二叉树的叶子节点,每个非叶子节点的值为其两个子节点中的较小值(称为“胜者”),根节点即为无序区的最小元素;
  2. 输出最小元素:将根节点(最小元素)放入有序区;
  3. 更新胜者树:将叶子节点中已输出的最小元素替换为“无穷大”(表示已淘汰),从该叶子节点向上回溯,更新所有父节点(重新比较子节点,选出新胜者),使树重新满足胜者树性质;
  4. 迭代筛选:重复步骤2-3,直至所有叶子节点均被替换为无穷大,有序区即为最终结果。

示例(升序排序,元素[49,38,65,97,76,13,27,49]

  1. 构建胜者树
    • 叶子节点:[49,38,65,97,76,13,27,49]
    • 非叶子节点(从下到上):[38,65,13,27,38,13,13](根节点为13,即最小元素);
  2. 输出与更新
    • 输出13,将对应叶子节点设为∞,回溯更新父节点,新根节点为27;
    • 输出27,对应叶子节点设为∞,更新后根节点为38;
    • 依次迭代,最终输出序列:[13,27,38,49,49,65,76,97]

性能分析

  • 时间复杂度
    • 构建胜者树:需n-1次比较(二叉树非叶子节点数为n-1);
    • 每次更新与输出:需log n次比较(树的深度为log n),共需n-1次更新;
    • 总时间复杂度:O(n log n)
  • 空间复杂度:需额外存储胜者树的非叶子节点(共n-1个) → O(n)
  • 稳定性:稳定。通过“左子节点优先”规则(若两子节点值相等,选择左子节点作为胜者),可保持相等元素的原始相对位置。
  • 局限:额外空间开销较大(需存储整棵胜者树),且对“无穷大”的处理可能引入边界问题,实际应用中较少直接使用,更多作为堆排序的理论铺垫。

(三)堆排序

堆排序(Heap Sort)由J.W.J. Williams于1964年提出,是对树形选择排序的优化。通过构建“堆”(一种特殊的完全二叉树),利用堆的性质高效筛选最小/最大元素,避免了树形选择排序的额外空间浪费。

1. 堆的定义与性质

堆是一棵完全二叉树,满足以下两种性质之一:

  • 大根堆:每个父节点的关键字 ≥ 其左右子节点的关键字(根节点为序列最大值);
  • 小根堆:每个父节点的关键字 ≤ 其左右子节点的关键字(根节点为序列最小值)。

完全二叉树的下标规律(假设堆用数组存储,根节点下标为1):

  • 下标为i的节点,左子节点下标为2i,右子节点下标为2i+1
  • 下标为i的节点,父节点下标为⌊i/2⌋(向下取整)。

示例(大根堆)

        97 (i=1)/    \76(i=2)  65(i=3)/  \     /  \
49(i=4)38(i=5)13(i=6)27(i=7)/
49(i=8)

2. 核心思路(以大根堆实现升序排序为例)

升序排序需利用大根堆(每次提取最大值放入序列末尾),步骤如下:

  1. 构建初始大根堆:将无序序列调整为大根堆(确保所有父节点 ≥ 子节点);
  2. 提取堆顶元素:将堆顶(最大值)与堆尾元素交换,此时最大值进入“有序区”(堆尾);
  3. 调整堆结构:将剩余元素(除有序区外)重新调整为大根堆(因交换后堆顶可能破坏堆性质);
  4. 迭代完成:重复步骤2-3,直至堆中仅剩1个元素(有序区覆盖整个序列)。

3. 核心操作:堆调整(Heapify)

堆调整是堆排序的关键,用于将一棵完全二叉树的某一子树调整为堆。以大根堆为例,操作步骤:

  • 输入:堆数组L、待调整子树的根节点下标i、堆的有效长度len(排除已进入有序区的元素);
  • 暂存根节点i的值,避免被覆盖;
  • 从根节点的左子节点2i开始,找到该子树中关键字最大的节点(比较左、右子节点,取较大值);
  • 若根节点值 < 最大子节点值,将最大子节点值上移至根节点位置,更新根节点下标为最大子节点下标;
  • 重复上述步骤,直至根节点值 ≥ 其所有子节点值,将暂存的根节点值放入最终位置。

4. 代码实现

// 堆调整:将以i为根的子树调整为大根堆(len为堆的有效长度)
void HeapAdjust(SqList *L, int i, int len) {RecordType temp = L->r[i];  // 暂存根节点// j从i的左子节点开始,沿较大子节点向下遍历for (int j = 2 * i; j <= len; j *= 2) {// 若右子节点存在且大于左子节点,j指向右子节点if (j < len && L->r[j].key < L->r[j+1].key) {j++;}// 若根节点值≥最大子节点值,无需继续调整if (temp.key >= L->r[j].key) {break;}// 最大子节点上移至父节点位置L->r[i] = L->r[j];i = j;  // 更新i为当前子节点位置,继续向下调整}L->r[i] = temp;  // 将暂存的根节点放入最终位置
}// 堆排序(升序,基于大根堆)
void HeapSort(SqList *L) {int i;// 步骤1:构建初始大根堆(从最后一个非叶子节点开始调整)for (i = L->length / 2; i >= 1; i--) {HeapAdjust(L, i, L->length);}// 步骤2:提取堆顶元素并调整堆for (i = L->length; i > 1; i--) {// 堆顶(最大值)与堆尾元素交换,最大值进入有序区RecordType temp = L->r[1];L->r[1] = L->r[i];L->r[i] = temp;// 调整剩余元素为大根堆(有效长度为i-1)HeapAdjust(L, 1, i - 1);}
}

5. 性能分析

  • 时间复杂度
    • 构建初始堆:需调整⌊n/2⌋个非叶子节点,每个节点的调整深度不超过log n,总时间O(n)
    • 迭代调整堆:共n-1轮,每轮调整深度log n,总时间O(n log n)
    • 整体时间复杂度:O(n log n)(与快速排序平均时间复杂度相同)。
  • 空间复杂度:仅需1个临时变量存储交换元素 → O(1),原地排序。
  • 稳定性:不稳定。例如大根堆[5,5,3],第一次交换后堆变为[3,5,5],两个5的相对位置改变。
  • 适用场景:大规模数据排序,尤其是需要“原地排序”(内存有限)或需频繁提取最大/最小值的场景(如优先队列)。堆排序的最坏时间复杂度仍为O(n log n),稳定性优于快速排序(快速排序最坏O(n²)),但实际应用中因缓存命中率较低,平均速度略逊于快速排序。

🔗 五,归并排序

归并排序(Merge Sort)是“分治法”(Divide and Conquer)的经典应用,核心思想是“先拆分、后合并”——将无序序列逐步拆分为子序列,分别排序后再合并为有序序列。其优势在于稳定的时间复杂度和稳定性,适合大规模数据排序,尤其适用于外部排序(数据无法全部放入内存)。

核心思想

  1. 拆分(Divide):将待排序序列从中间拆分为两个长度近似相等的子序列,递归拆分每个子序列,直至子序列长度为1(长度为1的序列天然有序);
  2. 合并(Conquer):将两个有序子序列合并为一个有序序列,重复合并过程,直至所有子序列合并为完整的有序序列。

示例(升序排序,序列[49,38,65,97,76,13,27,49]

  1. 拆分过程
    [49,38,65,97,76,13,27,49][49,38,65,97][76,13,27,49] → 继续拆分至8个长度为1的子序列:[49][38][65][97][76][13][27][49]
  2. 合并过程
    • 第1轮合并:[38,49][65,97][13,76][27,49]
    • 第2轮合并:[38,49,65,97][13,27,49,76]
    • 第3轮合并:[13,27,38,49,49,65,76,97](最终有序序列)。

核心操作:合并两个有序子序列

合并操作是归并排序的关键,需借助一个临时数组存储合并结果,步骤如下:

  1. 设两个有序子序列的起始下标为i(左子序列,范围low~mid)和j(右子序列,范围mid+1~high),临时数组起始下标为k
  2. 比较r[i].keyr[j].key,将较小的元素放入临时数组temp[k],并移动对应下标(i++j++)和k++
  3. 当其中一个子序列遍历完毕后,将另一个子序列的剩余元素直接复制到临时数组;
  4. 将临时数组的内容复制回原数组的low~high范围,完成合并。

代码实现(二路归并排序)

二路归并排序是最常用的归并排序方式,每次合并两个子序列:

// 合并两个有序子序列:r[low..mid] 和 r[mid+1..high]
void Merge(SqList *L, int low, int mid, int high) {RecordType temp[MAXSIZE];  // 临时数组int i = low, j = mid + 1, k = 0;  // i左子序列指针,j右子序列指针,k临时数组指针// 合并两个子序列while (i <= mid && j <= high) {if (L->r[i].key <= L->r[j].key) {temp[k++] = L->r[i++];  // 左子序列元素更小,放入临时数组} else {temp[k++] = L->r[j++];  // 右子序列元素更小,放入临时数组}}// 复制左子序列剩余元素while (i <= mid) {temp[k++] = L->r[i++];}// 复制右子序列剩余元素while (j <= high) {temp[k++] = L->r[j++];}// 将临时数组内容复制回原数组for (i = low, k = 0; i <= high; i++, k++) {L->r[i] = temp[k];}
}// 递归拆分并合并
void MergeSortRec(SqList *L, int low, int high) {if (low < high) {  // 子序列长度≥2时才需拆分int mid = (low + high) / 2;  // 中间位置MergeSortRec(L, low, mid);    // 拆分左子序列并排序MergeSortRec(L, mid + 1, high);  // 拆分右子序列并排序Merge(L, low, mid, high);     // 合并两个有序子序列}
}// 归并排序入口函数
void MergeSort(SqList *L) {MergeSortRec(L, 1, L->length);  // 假设数组从1开始存储
}

性能分析

  • 时间复杂度
    • 拆分过程:递归深度为log n(每次拆分后子序列长度减半);
    • 合并过程:每轮合并需遍历所有元素,时间为O(n)
    • 总时间复杂度:O(n log n)(无论最好、最坏还是平均情况,时间复杂度均稳定为O(n log n))。
  • 空间复杂度:需额外存储临时数组(大小为n) → O(n)(非原地排序)。
  • 稳定性:稳定。合并时若两元素关键字相等,优先选择左子序列元素,保持原始相对位置。
  • 适用场景
    • 大规模数据排序(时间复杂度稳定,不受数据分布影响);
    • 外部排序(可分批次读取数据到内存,逐步合并);
    • 需保持相等元素相对位置的场景(如按成绩排序后需保留录入顺序)。

🔢 六,基数排序

基数排序(Radix Sort)是一种“非比较类排序算法”,无需通过关键字比较实现排序,而是通过“按关键字的各位(如个位、十位、百位)依次排序”完成整体有序。其核心是利用“多关键字排序”思想,适合关键字可分解为多个独立位(如整数、字符串)的场景。

(一)多关键字的排序

多关键字排序是基数排序的理论基础,指对包含多个关键字(如“学号”由“年级”“班级”“序号”组成,“字符串”由多个字符组成)的记录进行排序,需遵循“优先级规则”(先按高优先级关键字排序,再按低优先级关键字排序)。

两种排序方式

  1. 最高位优先(MSD,Most Significant Digit First)

    • 先按最高优先级关键字排序,将序列划分为若干子序列;
    • 对每个子序列按次高优先级关键字排序,继续划分子序列;
    • 依次处理至最低优先级关键字,最终合并所有子序列。
    • 示例:对学号20230105(年级2023、班级01、序号05)排序,先按年级分组,再按班级分组,最后按序号排序。
  2. 最低位优先(LSD,Least Significant Digit First)

    • 先按最低优先级关键字排序;
    • 再按次低优先级关键字排序(此时低优先级已有序,高优先级排序不会破坏低优先级的顺序);
    • 依次处理至最高优先级关键字,直接得到整体有序序列。
    • 示例:对整数329、457、657、839、436、720、355排序,先按个位排序,再按十位排序,最后按百位排序。

基数排序采用LSD方式,无需分组和合并,通过“桶排序”实现每一位的排序,效率更高。

(二)链式基数排序

基数排序通常采用链式存储结构(避免元素移动,提高效率),称为“链式基数排序”。其核心是通过“分配”(按当前位关键字将节点放入对应桶)和“收集”(将桶中节点按顺序串联)两个步骤,依次处理关键字的每一位。

核心概念

  • 基数(r):关键字每一位的可能取值范围,如十进制数的基数r=10(0-9),英文字母的基数r=26(A-Z);
  • 位数(d):关键字的总位数,如三位数的d=3,长度为5的字符串的d=5
  • 桶(Bucket):共r个桶,对应基数的每个取值(如十进制数的桶编号为0-9),每个桶存储当前位关键字等于桶编号的节点。

核心步骤(以十进制整数排序为例,基数r=10,位数d=3

  1. 初始化:将待排序序列用链表串联,设置r个桶(每个桶为链表头节点);
  2. 按最低位(个位)排序
    • 分配:遍历链表,按节点关键字的个位数字,将节点插入对应桶的尾部;
    • 收集:从桶0到桶9依次遍历,将每个桶中的节点串联,形成新的链表;
  3. 按次低位(十位)排序:重复“分配-收集”步骤,按关键字的十位数字排序;
  4. 按最高位(百位)排序:重复“分配-收集”步骤,按关键字的百位数字排序;
  5. 结束:处理完所有位数后,链表即为有序序列。

示例(排序序列[329, 457, 657, 839, 436, 720, 355]d=3r=10

  1. 初始链表329 → 457 → 657 → 839 → 436 → 720 → 355
  2. 按个位排序(分配-收集)
    • 分配:桶0(720)、桶5(355)、桶6(436)、桶7(457,657)、桶9(329,839);
    • 收集:720 → 355 → 436 → 457 → 657 → 329 → 839
  3. 按十位排序(分配-收集)
    • 分配:桶2(720,329)、桶3(436,839)、桶5(355,457,657);
    • 收集:720 → 329 → 436 → 839 → 355 → 457 → 657
  4. 按百位排序(分配-收集)
    • 分配:桶3(329,355)、桶4(436,457)、桶6(657)、桶7(720)、桶8(839);
    • 收集:329 → 355 → 436 → 457 → 657 → 720 → 839(最终有序序列)。

代码实现(十进制整数链式基数排序)

// 链表节点结构
typedef struct Node {KeyType key;          // 关键字(假设为整数)struct Node *next;    // 指向下一节点
} Node, *LinkList;// 找到关键字的第k位数字(k=1为个位,k=2为十位,...)
int GetDigit(KeyType key, int k) {int digit = 0;for (int i = 0; i < k; i++) {digit = key % 10;key /= 10;}return digit;
}// 链式基数排序(r=10,d为关键字最大位数)
void RadixSort(LinkList *head, int d) {LinkList bucket[10];  // 10个桶(0-9),每个桶存储链表头节点LinkList tail[10];    // 每个桶的尾节点指针(用于快速插入)LinkList p, q;int i, k, digit;// 初始化链表(确保链表有头节点,方便操作)*head = (LinkList)malloc(sizeof(Node));(*head)->next = NULL;// 假设此处已通过输入构建待排序链表(略去输入代码)// 按每一位排序(从低位到高位,共d轮)for (k = 1; k <= d; k++) {// 初始化桶(头节点指向空,尾节点指向头节点)for (i = 0; i < 10; i++) {bucket[i] = (LinkList)malloc(sizeof(Node));bucket[i]->next = NULL;tail[i] = bucket[i];}// 分配:遍历链表,将节点放入对应桶p = (*head)->next;  // p指向待排序节点while (p != NULL) {digit = GetDigit(p->key, k);  // 获取第k位数字// 将p插入对应桶的尾部tail[digit]->next = p;tail[digit] = p;// 继续遍历下一个节点p = p->next;}// 收集:将桶中节点串联为新链表(*head)->next = NULL;q = (*head);  // q指向新链表的尾节点for (i = 0; i < 10; i++) {if (bucket[i]->next != NULL) {  // 桶非空q->next = bucket[i]->next;  // 连接桶的节点q = tail[i];                // 更新新链表尾节点}free(bucket[i]);  // 释放桶的头节点}q->next = NULL;  // 新链表尾节点置空}
}

性能分析

  • 时间复杂度

    • 每一轮“分配-收集”需遍历所有n个节点,共需d轮(d为关键字位数);
    • 总时间复杂度:O(d(n + r))r为基数)。
    • d为常数(如整数的位数固定)、r不随n增大时,时间复杂度可视为O(n),是线性时间排序算法。
  • 空间复杂度

    • 需额外存储r个桶的头节点和尾节点,以及链表指针 → r为基数,通常为常数,如r=10r=26)。
  • 稳定性:稳定。分配时节点按顺序插入桶的尾部,收集时按桶顺序串联,相等关键字的节点相对位置不变。

  • 适用场景

    • 关键字可分解为固定位数(如整数、日期、固定长度字符串);
    • 数据量较大且关键字位数较少(如手机号、身份证号排序);
    • 需稳定排序且不适合比较类排序的场景(如关键字无法直接比较大小,但可按位划分)。
  • 局限:不适用于关键字位数不确定或位数极多的场景(如超长字符串),此时d过大,时间复杂度会显著上升。

📥 七,外部排序

当待排序数据量远超内存容量(如GB级甚至TB级数据),无法将所有数据一次性载入内存进行内部排序时,需借助磁盘、磁带等外部存储设备完成排序,这类排序方式称为外部排序。外部排序的核心是“分治策略”,通过“内存与外存的数据交互”,将大规模数据拆分为可处理的小块,逐步合并为有序序列。

(一)外部排序的基本方法

外部排序的典型流程分为**“生成初始归并段”** 和**“多趟归并排序”** 两个阶段,核心是通过“内存排序+外存归并”实现整体有序。

1. 基本流程(以磁盘排序为例)

假设内存最多可容纳m个数据,待排序数据存储在磁盘文件Input中,最终有序数据存入Output

  • 阶段1:生成初始归并段(顺串)

    1. Input读取m个数据到内存,用内部排序(如快速排序、堆排序)将其排序,形成一个初始归并段(Run)
    2. 将排序后的初始归并段写入磁盘临时文件(如Run1Run2、…、Runk);
    3. 重复步骤1-2,直至Input中所有数据均被处理为k个初始归并段。
  • 阶段2:多趟归并排序

    1. 每次从磁盘读取t个(t为归并路数)初始归并段到内存的t个输入缓冲区,同时分配1个输出缓冲区;
    2. 用“多路归并算法”(如败者树)合并t个有序归并段,将合并后的有序数据写入输出缓冲区;
    3. 当输出缓冲区满时,将数据写入磁盘新的临时归并段(如Run1'Run2'、…);
    4. 重复步骤1-3,每趟归并将k个归并段合并为⌈k/t⌉个新归并段,直至最终合并为1个有序归并段,写入Output

2. 示例(内存容量m=4,待排序数据[9,3,1,7,4,2,8,6,5]

  • 阶段1:生成初始归并段

    • 第1次读入[9,3,1,7],排序后生成Run1: [1,3,7,9]
    • 第2次读入[4,2,8,6],排序后生成Run2: [2,4,6,8]
    • 第3次读入[5],生成Run3: [5]
    • 共得到3个初始归并段:Run1Run2Run3
  • 阶段2:2路归并(t=2

    • 第1趟归并:合并Run1Run2Run1': [1,2,3,4,6,7,8,9],剩余Run3
    • 第2趟归并:合并Run1'Run3Output: [1,2,3,4,5,6,7,8,9]

3. 关键指标:归并趟数

归并趟数直接影响磁盘I/O次数(外部排序的主要时间开销),计算公式为:
归并趟数 s = ⌈log_t k⌉

  • t:归并路数(每次合并的归并段数量);
  • k:初始归并段数量。

示例k=8t=2时,s=32^3=8);t=4时,s=24^2=8)。可见,增加归并路数t可减少归并趟数,从而降低磁盘I/O次数。

(二)多路平衡归并的实现

多路平衡归并的核心是高效合并t个有序归并段,关键在于解决“如何快速从t个归并段的当前元素中选出最小值”的问题。常用实现方式为败者树(Loser Tree),相比直接比较(时间复杂度O(t)),败者树可将选最小值的时间优化至O(log t)

1. 败者树的结构与原理

败者树是一棵完全二叉树,叶子节点存储t个归并段的当前元素(称为“选手”),非叶子节点存储“比赛败者”(即两个子节点中值较大的元素对应的归并段编号),根节点存储“比赛胜者”(即值最小的元素对应的归并段编号)。

核心步骤
  • 初始化:将每个归并段的第一个元素作为叶子节点,两两比较,非叶子节点记录败者,根节点记录胜者(最小值所在归并段);
  • 选最小值:根节点对应的归并段的当前元素即为最小值,将其输出;
  • 更新败者树:从输出最小值的归并段读取下一个元素(若归并段已空,设为无穷大),替换对应的叶子节点,自下而上重新比较,更新非叶子节点的败者信息,直至根节点重新确定新的胜者;
  • 重复选值与更新:直至所有归并段均为空(所有元素输出完毕)。

2. 示例(t=3路归并,归并段Run1: [1,4,7]Run2: [2,5,8]Run3: [3,6,9]

  • 初始化败者树
    叶子节点:Run1(1)Run2(2)Run3(3)
    非叶子节点:比较12(败者2)、与3(败者3);
    根节点:胜者1Run1),输出1
  • 更新与输出
    Run1下一个元素为4,替换叶子节点后重新比较,根节点胜者2Run2),输出2
    依次类推,最终输出序列:[1,2,3,4,5,6,7,8,9]

3. 败者树的优势

  • 时间复杂度:每次选最小值和更新树的时间为O(log t)t路归并n个元素的总时间为O(n log t)
  • 减少比较次数:相比直接比较(每次选最小值需t-1次比较),败者树每次更新仅需log t次比较,尤其适合t较大的场景(如t=100时,log t≈7,远小于99)。

4. 归并路数t的限制

理论上,t越大,归并趟数越少,磁盘I/O次数越少,但t受限于内存容量:

  • 内存需为每个归并段分配一个输入缓冲区(存储当前待比较的元素),t越大,所需缓冲区数量越多;
  • t超过内存可支持的缓冲区数量,需频繁换入换出缓冲区数据,反而增加I/O开销。

(三)置换-选择排序

初始归并段的长度取决于内存容量(内存最多容纳m个数据,初始归并段长度最大为m)。置换-选择排序(Replacement-Selection Sort) 可突破这一限制,生成长度远超m的“长归并段”,从而减少初始归并段数量k,降低归并趟数。

1. 核心思想

利用“堆”在内存中动态维护一个“当前归并段”,通过“置换”(用外存新数据替换堆中输出的元素)和“选择”(选堆顶元素输出),使归并段长度最大化:

  • 步骤1:构建初始堆
    从外存读取m个数据到内存,构建一个最小堆(堆顶为当前最小值),此时堆中元素构成“当前归并段”。
  • 步骤2:输出与置换
    1. 输出堆顶元素(当前归并段的下一个元素);
    2. 从外存读取一个新数据x
      • x ≥ 刚输出的元素(符合当前归并段的有序性),则将x放入堆顶,重新调整堆;
      • x < 刚输出的元素(无法加入当前归并段),则将x暂存到内存的“备用区”;
  • 步骤3:切换归并段
    当堆顶元素为无穷大(表示当前归并段所有元素已输出),结束当前归并段,将备用区的元素构建新堆,开始下一个归并段;
  • 步骤4:重复
    直至外存所有数据处理完毕,生成k个长归并段。

2. 示例(内存容量m=3,待排序数据[5,3,7,6,2,8,1,4]

  • 初始堆:读取[5,3,7],构建最小堆[3,5,7],当前归并段开始;
  • 输出与置换
    • 输出3,读入66≥3),堆调整为[5,6,7]
    • 输出5,读入22<5),放入备用区,堆调整为[6,7,∞]
    • 输出6,读入88≥6),堆调整为[7,8,∞]
    • 输出7,读入11<7),放入备用区,堆调整为[8,∞,∞]
    • 输出8,读入44<8),放入备用区,堆调整为[∞,∞,∞]
  • 切换归并段:当前归并段输出完毕([3,5,6,7,8]),备用区[2,1,4]构建新堆[1,2,4],继续输出得到第二个归并段[1,2,4]
  • 最终归并段[3,5,6,7,8](长度5>3)、[1,2,4](长度3),共2个归并段(若用普通内部排序,需3个归并段)。

3. 优势与特点

  • 生成的归并段长度平均为2m(在随机数据下),最坏情况仍为m(如数据逆序);
  • 减少初始归并段数量k,从而减少归并趟数和磁盘I/O次数;
  • 仅需1个堆和1个备用区,内存开销与普通内部排序相当。

(四)最佳归并树

多趟归并中,磁盘I/O次数取决于归并趟数和每趟归并的数据量。最佳归并树(Optimal Merge Tree) 是一种基于哈夫曼树的归并方案,通过优化归并段的合并顺序,使“总归并代价”(即磁盘I/O总数据量)最小。

1. 归并代价的定义

归并k个长度分别为l1, l2, ..., lk的归并段,总归并代价为所有归并过程中处理的数据量之和。对于t路归并:

  • 合并t个归并段的代价为l1 + l2 + ... + lt(需读取所有数据并写入合并结果);
  • 多趟归并的总代价为各趟归并代价之和。

2. 最佳归并树的原理

最佳归并树将每个初始归并段视为“叶子节点”,叶子节点的权值为归并段长度,t路归并对应构建“t叉哈夫曼树”:

  • 核心思想:每次选择t个权值最小的归并段合并,使每次合并的代价最小,从而总代价最小;
  • 与哈夫曼树的区别:哈夫曼树为二叉树(t=2),最佳归并树为t叉树,需满足“非叶子节点的子节点数均为t”,若初始归并段数量k不满足(k-1) mod (t-1) = 0,需添加权值为0的“虚归并段”(不产生实际I/O代价)。

3. 示例(t=3路归并,初始归并段长度[2,3,5,7,11,13,17]k=7

  • 步骤1:判断是否需添加虚归并段
    (k-1) mod (t-1) = (7-1) mod 2 = 0,无需添加虚归并段。
  • 步骤2:构建3叉哈夫曼树
    1. 选择最小3个归并段[2,3,5]合并,代价2+3+5=10,生成新归并段[10]
    2. 选择[7,10,11]合并,代价7+10+11=28,生成新归并段[28]
    3. 选择[13,17,28]合并,代价13+17+28=58,生成最终归并段;
  • 总归并代价10 + 28 + 58 = 96(若按其他顺序合并,总代价会更大,如先合并大归并段,总代价可能超过100)。

4. 虚归并段的添加规则

(k-1) mod (t-1) = r ≠ 0时,需添加t-1 - r个权值为0的虚归并段,使(k + (t-1 - r) - 1) mod (t-1) = 0,确保树的所有非叶子节点均有t个子节点。

示例t=3k=5(5-1) mod 2 = 0(无需添加);k=6(6-1) mod 2 = 1,需添加2-1=1个虚归并段。

📝 章结

排序算法是数据处理的基础工具,其设计与选择需结合数据规模、存储环境、稳定性需求等因素,本章内容可总结为以下核心要点:

1. 排序算法的分类与核心特征

  • 内部排序:数据全部在内存中处理,按核心思想分为插入排序(直接插入、希尔)、交换排序(冒泡、快速)、选择排序(简单选择、堆)、归并排序、基数排序。

    • 时间复杂度:O(n²)(简单排序,适合小规模数据)、O(n log n)(高效排序,适合大规模数据)、O(n)(线性排序,仅适用于关键字可分解场景);
    • 空间复杂度:原地排序(O(1),如堆排序、快速排序)、非原地排序(O(n),如归并排序);
    • 稳定性:稳定排序(归并、基数、直接插入)可保持相等元素相对位置,不稳定排序(快速、堆、简单选择)则可能改变。
  • 外部排序:数据需在内存与外存间交互,核心流程为“生成初始归并段→多趟归并”,通过败者树(优化多路归并效率)、置换-选择排序(生成长归并段)、最佳归并树(最小化I/O代价)提升性能。

2. 关键算法的适用场景

算法时间复杂度(平均)空间复杂度稳定性适用场景
快速排序O(n log n)O(log n)不稳定大规模随机数据,内存充足
堆排序O(n log n)O(1)不稳定大规模数据,需原地排序
归并排序O(n log n)O(n)稳定大规模数据,需稳定排序或外部排序
基数排序O(d(n+r))O(r)稳定关键字可分解(如整数、字符串),数据量大

文章转载自:

http://STvA991m.kfLpf.cn
http://PDL76CdW.kfLpf.cn
http://XjWeR6qV.kfLpf.cn
http://twgjufCP.kfLpf.cn
http://DTyrqr0S.kfLpf.cn
http://vFp0seg2.kfLpf.cn
http://82oPuaJC.kfLpf.cn
http://kAV8wIki.kfLpf.cn
http://ikA9GicU.kfLpf.cn
http://rjSsOSL0.kfLpf.cn
http://nZWXDZ8C.kfLpf.cn
http://Bq1dp1iP.kfLpf.cn
http://o503PiaS.kfLpf.cn
http://ySwjsfTw.kfLpf.cn
http://7reSPX5d.kfLpf.cn
http://hgMx2emJ.kfLpf.cn
http://TTQhhU5b.kfLpf.cn
http://GkWLtJAc.kfLpf.cn
http://8PFWFczM.kfLpf.cn
http://7iB9JY4K.kfLpf.cn
http://ajLWztZo.kfLpf.cn
http://yANww3kj.kfLpf.cn
http://p2HlRK1O.kfLpf.cn
http://f0WmhTvg.kfLpf.cn
http://jwSzTqUo.kfLpf.cn
http://cMxJR8SW.kfLpf.cn
http://sHaAnnhy.kfLpf.cn
http://yncdTfyl.kfLpf.cn
http://alriJwuy.kfLpf.cn
http://uM1eeFsN.kfLpf.cn
http://www.dtcms.com/a/368895.html

相关文章:

  • mac清除浏览器缓存,超实用的3款热门浏览器清除缓存教程
  • 残差连接与归一化结合应用
  • 【知识点讲解】模型扩展法则(Scaling Law)与计算最优模型全面解析:从入门到前沿
  • MySQL锁篇-锁类型
  • LINUX_Ubunto学习《2》_shell指令学习、gitee
  • FastGPT源码解析 Agent知识库管理维护使用详解
  • MATLAB 2023a深度学习工具箱全面解析:从CNN、RNN、GAN到YOLO与U-Net,涵盖模型解释、迁移学习、时间序列预测与图像生成的完整实战指南
  • 均匀圆形阵抗干扰MATLAB仿真实录与特点解读
  • 《深入理解双向链表:增删改查及销毁操作》
  • 属性关键字
  • 【Linux基础】Linux系统管理:MBR分区实践详细操作指南
  • 国产化FPGA开发板:2050-基于JFMK50T4(XC7A50T)的核心板
  • 时隔4年麒麟重新登场!华为这8.8英寸新「手机」给我看麻了
  • 敏感词过滤这么玩?自定义注解 + DFA 算法,优雅又高效!
  • RPC内核细节(转载)
  • 如何将 Android 设备的系统底层日志(如内核日志、系统服务日志等)拷贝到 Windows 本地
  • Vue美化文字链接(超链接)
  • 中囯移动电视盒子(魔百和)B860AV2.1-A2和CM311-5-zg刷机手记
  • TCP/IP函数——sendmsg
  • Linux网络自定义协议与序列化
  • 人工智能机器学习——聚类
  • docker-compose跨节点部署Elasticsearch 9.X集群
  • Qt控件:Item Views/Widgets
  • 轻量高效:Miniserve文件共享神器
  • Netty从0到1系列之JDK零拷贝技术
  • 从无图到轻图,大模型时代,图商的新角逐
  • 【物种分布模型】R语言物种气候生态位动态量化与分布特征模拟——气候生态位动态检验、质心转移可视化、适生区预测等
  • 盟接之桥说制造:在安全、确定与及时之间,构建品质、交期与反应速度的动态平衡
  • 【Android】SQLite使用——增删查改
  • DJANGO后端服务启动报错及解决