【学习笔记】数据结构(十一)

外部排序

文章目录

  • 外部排序
    • 11.1 外存信息的存取
    • 11.2 外部排序的方法
    • 11.3 多路平衡归并的实现 - 增加k
    • 11.4 置换-选择排序 - 减少m
    • 11.5 最佳归并树

外部排序 指的是大文件的排序,即待排序的记录存储在外存储器 上,在排序过程中需进行多次的内、外存之间的交换。

11.1 外存信息的存取

计算机一般有 两种存储器: 内存储器(主存)和外存储器(辅存)。

  • 内存的信息可随机存取,且存取速度快,但价格贵、容量小。

  • 外存储器包括磁带和磁盘(或磁鼓),前者为顺序存取的设备,后者为随机存取的设备。

1、磁带信息的存取

磁带是薄薄涂上一层磁性材料的一条窄带。

磁带不是连续运转的设备,而是一种启停设备(启停时间约为 5 毫秒),它可以根据读 /写的需要随时启动和停止。由千读/写信息应在旋转稳定时进行,而磁带从静止状态启 动后,要经过一个加速的过程才能达到稳定状态;反之,在读/写结束后,从运动状态到完 全停止,要经过一个减速的过程。因此,在磁带上相邻两组字符组(记录)之间要留一空白 区,叫做 间隙 IRG(lnter Record Gap)

在每次写信息时, 将若干个字符组合并成一块后一次写入磁带。 于是,每个字符组间就没有 IRG,而变成 块间的间隙 IBG(lnter Block Gap) , 从而可以减少IRG 的数目,提高磁带的利用率,块的长度大于 IBG 的长度。

在这里插入图片描述

成块还可减少I/O操作。 因为一次1/0操作可把整个物理块都读到内存缓冲区中. 然后再从缓冲区中取出所需要的信息(一个字符组)。每当要读一个字符组时,首先要查 缓冲区中是否已有,若有,则不必执行I/O操作,直接从缓冲区读取即可。

在磁带上 读写一块信息所需的时间 由两部分组成: TI/O = ta + n · tw

  • ta 为延迟时间,读/写头到达传输信息所在物理块起始位置所需时间;
  • tw 为传输一 个字符的时间。

缺点:

由于磁带是 顺序存取 的设备,则读/写信息之前先要进行顺序查找,并且当读/写头位于磁带尾端,而要读的信息在磁带始端时,尚需使磁带倒转运动。这是顺序存取设备的主要缺点,它使检索和修改信息很不方便。

2、磁盘信息的存取

磁盘是一种 直接存取 的存储设备(DASO)。它的容量大、速度快,存 取速度比磁带快得多。

磁盘可以是单片的,也可以由若干盘片组成盘组。每一片上有两个面。以 6 片盘组为例,由于最顶上和最低下盘片的外侧面不存信息,所以总共只有10个面可用来保存信 息。盘片装在一个主轴上,并绕主轴高速旋转,当磁道在读/写头下通过时,便可以进行信息的读/写。

把磁盘分为 固定头盘和活动头盘

  • 固定头盘的每 一道上都有独立的磁头,它是固定不动的,专负责读/写某一 道上的信息。
  • 活动头盘的磁头是可移动的。盘组也是可变的。一个 面上只有一个磁头,它可以从该面上的一道移动到另一道。 磁头装在一个动臂上,不同面上的磁头是同时移动的,并处 于同一圆柱面上。各个面上半径相同的磁道组成一个圆柱 面,圆柱面的个数就是盘片面上的磁道数。

在这里插入图片描述

在磁盘上标明一个具体信息必须用一个三维地址:柱面号、盘面号、块号

  • 柱面号确定读/写头的径向运动,
  • 块号确定信息在盘片圆圈上的位置。

在磁盘上读写一块信息步骤:

  • 首先必须找柱面,移动臂使磁头移 动到所需柱面上(称为定位或寻查);
  • 然后等待要访问的信息转到磁头之下;
  • 最后,读/写所需信息。

在磁盘上读写一块信息所需的时间 由 3 部分组成: TI/O = tseek + tla + twm

  • tseek 为寻查时间(seek time) , 即读/写头定位的时间;
  • tla 为等待时间(latency time), 即等待信息块的初始位置旋转到读写头下的时间;
  • twm 为传输时间(transmission time)

内外存之间的数据交换原理:

在这里插入图片描述

11.2 外部排序的方法

外部排序基本上由两个相对独立的阶段组成:

  • 按可用内存大小,将外存上含 n 个记录的文件分成若干长度为 l 的子文件或段(segment),依次读入内存并利用有效的内 部排序方法对它们进行排序,并将排序后得到的有序子文件重新写入外存,通常称这些有序子文件为归并段或顺串(run);
  • 对这些归并段进行逐趟归并,使归并段(有序的子文件)逐渐由小至大,直至得到整个有序文件为止

在这里插入图片描述

外部排序所需总的时间=内部排序(产生初始归并段)所需的时间(m * tIS) + 外存信息读写的时间(d * tIO) + 内部归并所需的时间(s * utmg

  • tIS:为得到一个初始归并段进行内部排序所需时间的均值;
  • m:为经过内部排序之后得到的初始归并段的个数I
  • tIO:是进行一次外存 读/写时间的均值;取决于所用的外存设备;
  • d:为总的读/写次数
  • utmg:对u 个记录进行内部归并所需时间;
  • s :为归并的趟数;

tIO 比 tmg大得多,所以提高外排的效率应主要 着眼于减少外存信息读写的次数d

假设在每个物理块可以容纳 200个记录,则每一趟归并需进行 50 次“读” 和 50 次“写"’

利用 2-路归并:4 趟 * 100 + 100 内部排序 = 500 次的读/写。

利用 5-路归并:2 趟 * 100 + 100 内部排序 = 300 次的读/写。

可见,对同一文件而言,进行外排时所需读/写外存的次数和归并的趟数 s成正比。

对m个初始归并段进行k-路平衡归并时,归并的趟数 s = ⌊ logkm ⌋ —— 可见,若增加 K 或减少m 便能减少s

若能增加初始归并段的长度则可减少初始归并段数量m

例子:

在这里插入图片描述

11.3 多路平衡归并的实现 - 增加k

每一趟从m个归并段得到 ⌈m/k⌉ 归并段。这种归并方法称为 k-路平衡归并

使用k路平衡归并策略,选出一个最小元素需要对比关键字(k-1)次,所以增加k会导致内部归并所需时间增加

eg:8路平衡归并,从公企归并段中选出一个最小元素需要对比关键字7次

在这里插入图片描述

“败者树"(Tree of Loser):可视为一棵完全二叉树)(根节点上多了一个结点)。k个叶结点分别是当前参加比较的元素,非叶子结点用来记忆左右子树中的“失败者”,而让胜者往上继续进行比较,一直到根结点。目的- 使k元素的关键字对比次数变少

在这里插入图片描述

#define _CRT_SECURE_NO_WARNINGS 1  // 禁用 Visual Studio 的安全警告(例如 scanf 的安全问题)
#include <stdio.h>
#include <stdlib.h>
#include <limits.h>

// 定义状态码,用于表示函数执行的成功或失败
#define OK 1
#define OVERFLOW -2
#define ERROR -1

// 栈的初始大小
#define STACK_INIT_SIZE 4

// 定义归并段的数量
#define K 5

// 定义最大键值和最小键值
#define MAXKEY INT_MAX  // 表示归并段结束的特殊键值
#define MINKEY INT_MIN  // 用于初始化败者树的虚拟键值

// 定义状态类型
typedef int Status;

// 定义栈的结构体
typedef struct {
    int* base;      // 栈的基地址(栈底)
    int* top;       // 栈顶指针
    int stacksize;  // 栈的当前容量
} SqStack;

// 定义键值类型
typedef int KeyType;

// 定义败者树,大小为 K
typedef int LoserTree[K];

// 定义外部节点结构体
typedef struct {
    KeyType key;  // 外部节点的键值
} ExNode, External[K + 1];  // 外部节点数组,大小为 K + 1(用于败者树)

/**
 * 初始化栈
 * @param S 栈的指针
 * @return 成功返回 OK,失败返回 OVERFLOW
 */
Status InitStack(SqStack* S) {
    S->base = (int*)malloc(STACK_INIT_SIZE * sizeof(int));  // 分配栈的初始内存
    if (!S->base)
        exit(OVERFLOW);  // 内存分配失败,退出程序
    S->top = S->base;  // 初始化栈顶指针为栈底
    S->stacksize = STACK_INIT_SIZE;  // 初始化栈的容量
    return OK;  // 初始化成功
}

/**
 * 向栈中压入元素
 * @param S 栈的指针
 * @param e 要压入的元素
 * @return 成功返回 OK,失败返回 OVERFLOW
 */
Status Push(SqStack* S, int e) {
    // 如果栈已满,则需要扩容
    if (S->top - S->base >= S->stacksize) {
        int* new_base = (int*)realloc(S->base, (S->stacksize + STACK_INIT_SIZE) * sizeof(int));  // 重新分配内存
        if (!new_base) {
            return OVERFLOW;  // 内存分配失败,返回错误
        }
        S->base = new_base;  // 更新栈底指针
        S->stacksize += STACK_INIT_SIZE;  // 更新栈的容量
    }
    *S->top++ = e;  // 将元素压入栈顶,并移动栈顶指针
    return OK;  // 成功返回 OK
}

/**
 * 从栈中弹出元素
 * @param S 栈的指针
 * @param e 用于存储弹出元素的变量
 * @return 成功返回 OK,失败返回 ERROR
 */
Status Pop(SqStack* S, int* e) {
    if (S->top == S->base) {
        return ERROR;  // 如果栈为空,返回错误
    }
    *e = *(--S->top);  // 弹出栈顶元素,并移动栈顶指针
    return OK;  // 成功返回 OK
}

/**
 * 调整败者树的某个节点
 * @param ls 败者树
 * @param b 外部节点数组
 * @param s 当前要调整的节点索引
 */
void Adjust(LoserTree ls, External b, int s) {
    int t, tmp;
    // 计算当前节点的父节点索引
    t = (s + K) / 2;  // 公式:(s + K) / 2,用于找到父节点索引
    // 从当前节点向上调整到根节点
    while (t > 0) {
        // 如果当前节点的值大于父节点的值
        if (b[s].key > b[ls[t]].key) {
            tmp = s;  // 交换当前节点和父节点
            s = ls[t];
            ls[t] = tmp;
        }
        t = t / 2;  // 继续向上调整
    }
    ls[0] = s;  // 更新胜者节点(ls[0] 始终存储当前胜者)
}

/**
 * 创建败者树
 * @param ls 败者树
 * @param b 外部节点数组
 */
void CreateLoserTree(LoserTree ls, External b) {
    int i;
    // 初始化外部节点的虚拟节点(用于处理特殊情况)
    b[K].key = MINKEY;  // 第 K 个外部节点的键值设置为 MINKEY
    // 初始化败者树的叶子节点(归并段的索引)
    for (i = 0; i < K; i++) {
        ls[i] = K;  // 初始化败者树的叶子节点为虚拟节点
    }
    // 从最后一个叶子节点开始调整败者树
    for (i = K - 1; i >= 0; i--) {
        Adjust(ls, b, i);  // 调整败者树
    }
}

/**
 * K 路归并
 * @param ls 败者树
 * @param b 外部节点数组
 * @param S 栈数组
 */
void K_merge(LoserTree ls, External b, SqStack* S) {
    int i, q;
    // 从每个栈中弹出第一个元素作为初始值
    for (i = 0; i < K; i++) {
        Pop(&S[i], &b[i].key);  // 从第 i 个栈中弹出第一个元素
    }
    // 创建败者树
    CreateLoserTree(ls, b);
    // 当胜者节点的值不为 MAXKEY 时,继续归并
    while (b[ls[0]].key != MAXKEY) {
        q = ls[0];  // 获取当前胜者节点(ls[0] 始终存储胜者索引)
        printf("%d ", b[q].key);  // 输出胜者节点的值
        // 从胜者节点的栈中弹出下一个元素
        Pop(&S[q], &b[q].key);
        // 调整败者树
        Adjust(ls, b, q);
    }
    printf("\n");  // 输出换行符
}

/**
 * 主函数
 */
void main() {
    int i, j;
    int e;
    SqStack S[K];  // 每个归并段对应一个栈
    LoserTree ls;  // 败者树
    External b;  // 外部节点数组
    // 初始化每个归并段的栈
    for (i = 0; i < K; i++) {
        InitStack(&S[i]);  // 初始化第 i 个栈
        Push(&S[i], MAXKEY);  // 将 MAXKEY 压入栈底,表示归并结束
        // 输入 3 个元素
        for (j = 0; j < 3; j++) {
            printf("S[%d] push: ", i);
            scanf("%d", &e);  // 从标准输入读取一个整数
            Push(&S[i], e);  // 将读取的整数压入栈中
        }
    }
    // 执行 K 路归并
    K_merge(ls, b, S);
}

在这里插入图片描述

11.4 置换-选择排序 - 减少m

m是外部文件经过内部排序之后 得到的初始归并段的个数,

m = ⌈n/l⌉, 其中 n 为外部文件中的记录数,l 为初始归并段中的记录数

注意事项:

  • 当归并段为空时,选择工作区内最小数
  • 当归并段不为空时,选择工作区内【大于归并段最大值的-归并段最后一个值】最小数
  • 不能被放进归并段时,需重新创建一个归并段

在这里插入图片描述

11.5 最佳归并树

2路归并

在这里插入图片描述

多路归并

在这里插入图片描述

补充虚段

k路最佳归并树:

  • 一定是一棵严格的 k叉树
  • 只有度为0和度为k的结点

在这里插入图片描述

添加虚段的数量:

初始归并段数量 + 虚段数量 = n0 = 度为0的结点数

度为k的结点数 = nk

① 总结点数 = n = n0 + nk

② 叉的数量 = 分支结点 nk * 每个分支结点发出的分叉数 k = 除根节点外的其他结点都被一个分叉连着 n - 1 即 nk * k = n - 1

① 和 ② => n0 =(k-1)nk + 1 => nk = (n0 - 1) / (k - 1) 如果是“严格k叉树”一定能除得尽

① 若(初始归并段数量 -1) % (k-1)=0,说明刚好可以构成严格k叉树,此时不需要添加虚段

② 若(初始归并段数量 -1) % (k-1) = u ≠ 0,则需要添加虚段(k-1) - u个

在这里插入图片描述

例题:设外存上有120个初始归并段,进行12路归并时,为实现最佳归并,需要补充的虚段个数是()
A.1 B.2 C.3 D.4

(120 - 1)% (12 - 1) = 9; 则补充 (12 - 1) - 9 = 2

例题:已知三叉树T中6个叶结点的权分别是2,3,4,5,6,7,T的带权(外部)路径长度最小的是()
A.27 B.46 C.54 D.56

(6 - 1)% (3 - 1) = 1 ≠ 0; 需要补充的虚段个数是1;则变成【0,2,3,4,5,6,7】,WPLmin = (2+3)*3 + (4+5)*2 + (6+7)*1 = 15 + 18 + 13 = 46

0
2
3
4
5
6
7
5
14
27

参考:

教材:严蔚敏《数据结构》(C语言版).pdf

视频:

外部排序

败者树

置换选择排序

博客:

多路平衡归并败者树(Tree of Loser)算法

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/953010.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

《跟我学Spring Boot开发》系列文章索引❤(2025.01.09更新)

章节文章名备注第1节Spring Boot&#xff08;1&#xff09;基于Eclipse搭建Spring Boot开发环境环境搭建第2节Spring Boot&#xff08;2&#xff09;解决Maven下载依赖缓慢的问题给火车头提提速第3节Spring Boot&#xff08;3&#xff09;教你手工搭建Spring Boot项目纯手工玩法…

【Linux笔记】Day1

基于韩顺平老师课程记录&#xff1a; https://www.bilibili.com/video/BV1Sv411r7vd 安装CentOS 给CentOS手动分区 分为三个区&#xff1a; boot分区&#xff08;给1G就行&#xff09; 交换分区&#xff08;和内存相关&#xff0c;这里和虚拟机的内存2G一致&#xff09; …

【网络】:网络编程套接字

目录 源IP地址和目的IP地址 源MAC地址和目的MAC地址 源端口号和目的端口号 端口号 VS 进程ID TCP协议和UDP协议 网络字节序 字符串IP和整数IP相互转换 查看当前网络的状态 socket编程接口 socket常见API 创建套接字 绑定端口号 发送数据 接收数据 sockaddr结构…

使用 Multer 上传图片到阿里云 OSS

文件上传到哪里更好&#xff1f; 上传到服务器本地 上传到服务器本地&#xff0c;这种方法在现今商业项目中&#xff0c;几乎已经见不到了。因为服务器带宽&#xff0c;磁盘 IO 都是非常有限的。将文件上传和读取放在自己服务器上&#xff0c;并不是明智的选择。 上传到云储存…

【端云一体化】云函数的使用

前言 为丰富HarmonyOS对云端开发的支持、实现端云联动&#xff0c;DevEco Studio以Cloud Foundation Kit&#xff08;云开发服务&#xff09;为底座、在传统的“端开发”基础上新增“云开发”能力&#xff0c;开发者在创建工程时选择合适的云开发工程模板&#xff0c;即可在De…

YARN 架构组件及原理

一、YARN 体系架构 YARN&#xff08;Yet Another Resource Negotiator&#xff0c;另一种资源协调者&#xff09; 是 Hadoop 2.0 中的资源管理系统&#xff0c;它的基本设计思想是将 MRv1 中的 JobTracker拆分成了两个独立的服务 &#xff1a;一个全局的资源管理器 ResourceMa…

C# GDI+的DrawString无法绘制Tab键的现象

【啰嗦2句】 现在用C#的人很少了吧&#xff1f;GDI更少了吧&#xff1f;所以这个问题估计也冷门。没关系&#xff0c;分享给特定需要的人也不错。 【问题现象】 工作中开发了一个报告编辑器&#xff0c;实现图文排版等功能&#xff0c;用着没什么问题&#xff0c;直到有一天…

最近在盘gitlab.0.先review了一下docker

# 正文 本猿所在产品的代码是保存到了一个本地gitlab实例上&#xff0c;实例是别的同事搭建的。最近又又又想了解一下&#xff0c;而且已经盘了一些了&#xff0c;所以写写记录一下。因为这个事儿没太多的进度压力&#xff0c;索性写到哪儿算哪儿&#xff0c;只要是新了解到的…

春秋云镜——initial

初步认识内网渗透流程 thinkphp外网打点 打开环境后尝试登陆无果&#xff0c;用fscan扫一下看看 fscan.exe -h 39.99.224.87 发现是think PHP漏洞 补充&#xff1a; fscan&#xff1a;一款内网综合扫描工具&#xff0c;方便一键自动化、全方位漏扫扫描。支持主机存活探测、端…

【C++】string的关系运算与比较分析

博客主页&#xff1a; [小ᶻ☡꙳ᵃⁱᵍᶜ꙳] 本文专栏: C 文章目录 &#x1f4af;前言&#x1f4af;基础知识&#xff1a;C 中的 string 关系运算器1. 关系运算器概述2. 字符串比较的本质 &#x1f4af;代码解析与扩展代码例一&#xff1a;相等比较代码解析输出 代码例二&a…

Qt C++读写NFC标签NDEF网址URI

本示例使用的发卡器&#xff1a;https://item.taobao.com/item.htm?spma21dvs.23580594.0.0.1d292c1biFgjSs&ftt&id615391857885 #include "mainwindow.h" #include "ui_mainwindow.h" #include <QDebug> #include "QLibrary" …

NVIDIA Clara平台助力医学影像处理:编程案例与实践探索(上)

一、引言 1.1 研究背景与意义 在现代医学领域,医学影像技术已然成为疾病诊断、治疗方案制定以及疗效评估的关键支柱。从早期的 X 射线成像,到如今的计算机断层扫描(CT)、磁共振成像(MRI)、正电子发射断层扫描(PET)等先进技术,医学影像为医生提供了直观、精准的人体内…

【硬件介绍】Type-C接口详解

一、Type-C接口概述 Type-C接口特点&#xff1a;以其独特的扁头设计和无需区分正反两面的便捷性而广受欢迎。这种设计大大提高了用户的使用体验&#xff0c;避免了传统USB接口需要多次尝试才能正确插入的问题。Type-C接口内部结构&#xff1a;内部上下两排引脚的设计虽然可能不…

【数据结构】第1天之Java中的数据结构

前言 众所周知&#xff0c;程序数据结构算法&#xff0c;可见数据结构的重要性。 在Java中&#xff0c;数据结构通常指的是Java集合框架中的类和接口。 Java集合框架提供了一套标准的数据结构&#xff0c;例如列表、集合、映射表等&#xff0c;以及相应的实现类。 今天要分享的…

Open FPV VTX开源之默认MAVLink设置

Open FPV VTX开源之默认MAVLink设置 1. 源由2. 准备3. 连接4. 安装5. 配置6. 测试6.1 启动wfb-ng服务6.2 启动wfb-ng监测6.3 启动QGroundControl6.4 观察测试结果 7. 总结8. 参考资料9. 补充9.1 telemetry_tx异常9.2 DEBUG串口部分乱码9.3 PixelPilot软件问题9.4 偶尔启动卡住 …

Spring Boot 和微服务:快速入门指南

&#x1f496; 欢迎来到我的博客&#xff01; 非常高兴能在这里与您相遇。在这里&#xff0c;您不仅能获得有趣的技术分享&#xff0c;还能感受到轻松愉快的氛围。无论您是编程新手&#xff0c;还是资深开发者&#xff0c;都能在这里找到属于您的知识宝藏&#xff0c;学习和成长…

Redis 为什么要引入 Pipeline机制?

在 Redis 中有一种 Pipeline&#xff08;管道&#xff09;机制&#xff0c;其目的是提高数据传输效率和吞吐量。那么&#xff0c;Pipeline是如何工作的&#xff1f;它又是如何提高性能的&#xff1f;Pipeline有什么优缺点&#xff1f;我们该如何使用 Pipeline&#xff1f; 1、…

Cesium小知识:粒子系统的参数详解

Cesium 的粒子系统通过 ParticleSystem 类提供了一套丰富的参数来控制粒子的生成、行为和外观。以下是这些参数的详细说明,帮助你更好地理解和使用 Cesium 的粒子系统。 基本参数 image (String) - 粒子图像的URL路径。这个图像是每个粒子在渲染时使用的纹理。 startColor (Co…

【数据结构-堆】力扣1834. 单线程 CPU

给你一个二维数组 tasks &#xff0c;用于表示 n​​​​​​ 项从 0 到 n - 1 编号的任务。其中 tasks[i] [enqueueTimei, processingTimei] 意味着第 i​​​​​​​​​​ 项任务将会于 enqueueTimei 时进入任务队列&#xff0c;需要 processingTimei 的时长完成执行。 现…

OSPF - 2、3类LSA(Network-LSA、NetWork-Sunmmary-LSA)

前篇博客有对常用LSA的总结 2类LSA&#xff08;Network-LSA&#xff09; DR产生泛洪范围为本区域 作用:  描述MA网络拓扑信息和网络信息&#xff0c;拓扑信息主要描述当前MA网络中伪节点连接着哪几台路由。网络信息描述当前网络的 掩码和DR接口IP地址。 影响邻居建立中说到…