软考中级 软件设计师 上午考试内容笔记(个人向)Part.3
软考上午考试内容
7. 网络安全
-
威胁名称 描述 恶意攻击(ARP) 所截获的合法通信数据拷贝,出于非法的目的而被重新发送。 拒绝服务(DOS) 对信息或其它资源的合法访问被无条件地阻止。 窃听 用户可利用合法或非法的手段窃取系统中的信息资源和敏感信息。例如对通信线路中传输的信号进行搭线监听,或者利用通信设备在工作过程中产生的电磁泄露截取有用信息等。 业务分析 通过对系统进行长期监听,利用统计分析方法对诸如通信频度、通信信息流向、通信总量的变化等参数进行研究,从而发现有价值的信息和规律。 信息泄露 信息被泄露或透露给某个非授权的实体。 破坏信息的完整性 数据被非授权地进行增删、修改或破坏而受到损失。 非授权访问 某一资源被某个非授权的人、或以非授权的方式使用。 欺骗 通过欺骗通信系统(或用户)达到非法用户冒充成为合法用户,或者将权限小的用户冒充为权限大的用户的目的。黑客大多是采用欺骗进行攻击。 旁路控制 攻击者利用系统的安全缺陷或安全性上的脆弱之处获得非授权的权利或特权。例如,攻击者通过各种攻击手段发现原本应保密,但是却又暴露出来的一些系统“特性”。利用这些“特性”,攻击者可以绕过防线守卫者入侵系统的内部。 授权侵犯 被授权以某一目的使用某一系统或资源的某个人,却将此权限用于其他非授权的目的,也称作“内部攻击”。 特洛伊木马 软件中含有一个察觉不出的或者无害的程序段,当它被执行时,会破坏用户的安全。 陷阱门 在某个系统或某个部件中设置了“机关”,使得当提供特定的输入数据时,允许违反安全策略。 抵赖 这是一种来自用户的攻击,比如:否认自己曾经发布过的某条消息,伪造一份对方来信等。 - 一些常见的介绍:
- 口令入侵
- 先得到目标主机的某位合法用户的账号,再进行合法用户的口令的破译;
- 再使用合法用户的账号和口令登录到目标主机,实施攻击活动;
- 放置木马
- 主要目的是远程控制计算机
- 分为:服务器端(
Server
)、客户端(client
); -
- 【服务器端】:将木马代码放置在服务器上,当用户访问服务器上的文件时,服务器端会自动将木马代码发送到用户的计算机上,然后运行木马代码,从而实现对用户的计算机的远程控制。
- 【客户端】:将木马代码放置在用户的计算机上,当用户访问服务器上的文件时,客户端会自动将木马代码发送到服务器上,然后运行木马代码,从而实现对服务器的远程控制。
- DoS 攻击
- 使计算机或者网络无法提供正常的服务;
- 常见的有:【1】网络带宽攻击 、【2】连通性攻击;
- 端口扫描
- 网络监听(
抓包、爬虫、Sniffer
) - 欺骗攻击(
Web欺骗、ARP欺骗、DNS欺骗、IP欺骗
)
- 口令入侵
7.1 防火墙技术
- 包过滤型防火墙
- 主要工作在网络层;
- 应用代理网关防火墙
- 彻底切断内外网的直接通信,两者的交互和访问必须经过防火墙进行;
- 状态检测技术防火墙
- 结合了前两者的优点;
- 典型的防火墙结构:通常是由过滤路由器和代理服务器组成;
- 典型的防火墙体系结构包括包过滤路由器、双宿主主机、被屏蔽主机、被屏蔽子网:
- 包过滤路由器
原理: 通过对网络数据包的头部信息(如源IP、目标IP、端口号、协议类型等)进行检查和过滤,决定是否允许数据包通过。
特点: 简单、易于配置,但安全性较低,容易受到源路由攻击等。
适用场景: 小型网络或作为网络安全的第一道防线。 - 双宿主主机
原理: 一台主机同时连接内部网络和外部网络,通过设置不同的网络接口和访问控制列表来实现对网络流量的过滤。
特点: 配置相对复杂,但功能较全面。
适用场景: 需要对网络流量进行细粒度控制的场景。 - 被屏蔽主机
原理: 在内部网络和外部网络之间设置一个屏蔽路由器,屏蔽路由器只进行数据包的过滤,不提供其他服务。内部网络中的主机通过堡垒主机来访问外部网络。
特点: 安全性较高,但灵活性较差。
适用场景: 对安全性要求较高的网络环境。 - 被屏蔽子网
原理: 在被屏蔽主机基础上,将堡垒主机放在一个单独的子网中,这个子网称为屏蔽子网。屏蔽子网与外部网络通过屏蔽路由器连接。
特点: 安全性更高,可以更好地保护内部网络。
适用场景: 对安全性要求极高的网络环境,如金融机构、政府部门等。
- 包过滤路由器
7.2 加密与数字签名
-
数据加密的基本思想就是将信息转换成只有授权人才能读懂的形态;
-
加密的基本原理:
- 明文:原始数据;
- 暗文:被加密后的数据;
- 密钥:用于加密和解密数据的信息;
- 加密算法:使用密钥实现明文、暗文之间转换的数学办法;
-
加密方式/密码体系:
- 对称加密: 加密和解密使用同一个密钥。
- 优点: 加解密速度快。
- 缺点: 密钥的传输和管理是个难题。
- 常见算法: AES、DES、3DES、RC-5、IDEA算法、TDEA算法;
3DES
有两个56位的密钥K1,K2
。它的加解密过程如下:- 加密: K 1 加密 → K 2 解密 → K 1 加密 K1加密\rightarrow K2解密 \rightarrow K1加密 K1加密→K2解密→K1加密
- 解密: K 1 解密 → K 2 加密 → K 1 解密 K1解密\rightarrow K2加密 \rightarrow K1解密 K1解密→K2加密→K1解密
- 非对称加密: 使用一对密钥,公钥用于加密,私钥用于解密。
- 优点: 密钥分发相对安全。
- 缺点: 加解密速度较慢。
- 常见算法: RSA(
计算量大,难破解
)、ECC(椭圆曲线算法
)、Elgamal、D-H、Rabin、背包算法;
- 对称加密: 加密和解密使用同一个密钥。
-
数字签名是用于确认发送者身份和消息完整性的一个加密的信息摘要;
-
数字签名应该满足:
- 接收者能够核实发送者;
- 发送者事后不能抵赖对报文的签名;
- 接受者不能伪造对报文的签名;
-
数字签名最常用的实现方式是 公钥密码体制 和 单向散列函数算法(
MD5、SHA
) 的组合基础上; -
各个网络层次的安全保障:
7.3 多媒体
- 种类
感觉媒体、表示媒体、显示媒体(
表现媒体
)、存储媒体、传输媒体; - 计算问题
- 图像容量计算:
条件 示例 已知像素、位数 位数是 x x x,像素是 y × z y\times z y×z,则总容量为 x × y × z x\times y\times z x×y×z 已知像素、色数 色数是 x x x,像素是 y × z y\times z y×z,则总容量为 ( l o g 2 ( x ) / 8 ) × y × z (log_{2}(x)/8)\times y\times z (log2(x)/8)×y×z - 音频容量计算:
容量 = 采样频率( H z ) × 量化 / 采样位数(位) × 声道数 ÷ 8 容量 = 采样频率(Hz)\times 量化 / 采样位数(位) \times 声道数 \div 8 容量=采样频率(Hz)×量化/采样位数(位)×声道数÷8(注意双声道这个坑
) - 视频容量计算:
容量 = 每帧图像容量( b y t e ) × 每秒帧数 × 时间 + 音频容量 × 时间 容量 = 每帧图像容量(byte)\times 每秒帧数 \times 时间 + 音频容量 \times 时间 容量=每帧图像容量(byte)×每秒帧数×时间+音频容量×时间
- 图像容量计算:
8. 操作系统
- 是介于软件层与硬件层之间的抽象层,用于控制和调配计算机系统资源;
- 它是计算机系统中最基本的系统软件;
- 操作系统的特征:并发
(在一段时间内,多个任务同时进行)
、共享(互斥共享、同时共享)
、虚拟(具体上来说是将一个物理上的实体转换成很多逻辑上的实体,分为空分复用【虚拟存储】、时分复用【虚拟处理器】两种)
、异步(一个任务的执行不阻塞其他任务)
; - OS的发展与分类:
- 分时操作系统;
- 实时操作系统(
实时进行变化
); - 网络操作系统;
- 分布式操作系统(
扁平化
); - PC端;
8.1 进程管理
- 进程的五种形态:
- 创建态
- OS为进程分配内存资源,初始化PCB(
process control block
);
- OS为进程分配内存资源,初始化PCB(
- 就绪态
- 进程创建完成后,且具有运行条件(
有空闲的CPU
),进入就绪态,等待CPU调度(就绪队列
);
- 进程创建完成后,且具有运行条件(
- 运行态
- 进程被调度到CPU上,开始执行;
- 阻塞态
- 进程运行过程中因为种种原因需要等待事件发生,或者进程运行时间过长时间片到,为了整体运行的效率,将此进程下CPU,进入阻塞态;
- 终止态
- 执行
exit
系统调用,请求操作系统终止该进程,使进程进入“终止态”; - 在终止态中,OS会回收进程的各项资源和PCB,当其功能结束后,这个进程就荡然无存了;
- 执行
- 创建态
- 简单来说,异步就是各做各的,互不干扰;同步就是按照一定的顺序,前后进程有所依赖。所以同步又叫直接制约关系。“同步”的反义词是“异步”;
- 互斥指对资源的排他性占有,它是“共享”的反义词。事实上,进程的“并发”需要“共享”的支持。
- 若某个资源在一段时间内只允许一个进程使用,则可称之为 临界资源;
- 对临界资源的访问,必须互斥的进行;
- 进程互斥机制:
显然,进入区和退出区都只是为了实现互斥,只有临界区才真正对临界资源进行访问。do { entry_methods(); // 进入区,负责检查进程是否可以访问临界区。如果可以访问后,设置特定标志符给进程“上锁”,以实现互斥 critical_methods(); // 临界区,负责访问临界数据 exit_methods(); // 退出区,负责解除“上锁”,恢复特定标志符 reminder_methods(); //剩余区,负责收尾等其他处理 }while(1)
- 进程互斥的调度:进程互斥机制
- 空闲让进;
- 忙则等待;
- 有限等待:对于请求访问的进程,其等待时间应该为有限的;
- 让权等待:进程不能进入临界区,应立即释放CPU,防止进程忙等待;
8.1.1 信号量机制
- 用户进程可以使用OS提供的原语来对信号量进行操作,从而很方便地实现进程互斥、进程同步;
- 信号量可以用于描述OS中某种资源的数量。常见的形式是: 一个信号量 = 一种资源信号量的值 = 这种资源的剩余数量 一个信号量 = 一种资源信号量的值 = 这种资源的剩余数量 一个信号量=一种资源信号量的值=这种资源的剩余数量事实上,如果信号量的值小于0,则说明有进程在等待这种资源
- 原语是一种特殊的程序段,其中包含代码块只能被一次性执行,不能被打断;
- 假设一对原语是
wait(S)
和signal(S)
,也常写作P(S)
和V(S)
,即常见的P、V操作; - 具体执行上看:
P(S)
:申请一个资源S。当资源不够的时候,就需要阻塞等待,最后$ S - 1$;V(s)
:释放一个资源S,即 S + 1 S+1 S+1。当有进程在等待时,可以顺便唤醒一个进程;
-
信号量实现同步
semaphore s = 0;// 初始化信号量,置0 P1() { 代码1; 代码2; V(s); // 释放资源,s + 1,唤醒一个进程 代码3; } P2() { P(s); // 申请资源,s - 1,若S<0,则阻塞等待 代码4; 代码5; 代码6; } 无论进程P1先执行还是进程P2先执行,最终的宏观的运行方式都是先执行P1(),通过V(s)释放一个资源让s = 1, 再使P2()依靠P(s)获取一个资源执行剩余的代码,也就是P1() --> P2()。
-
信号量实现前驱操作
- 是前者的扩张版,即不断通过 V ( s ) − P ( s ) 桥 V(s)-P(s)桥 V(s)−P(s)桥的方式实现一对进程之间的同步,从而实现全部进程节点执行的有序化;
-
死锁
- 出现死锁的条件(
四个缺一不可
):- 使用临界资源,需要互斥;
- 进程资源未使用完前不能强制剥夺;
- 进程保持自己的一个资源不放,又对其他进程占有的资源提出请求,造成阻塞(
吃着碗里的看着锅里的
) - 出现循环等待;
- 计算最少信息量的方法:将将有富余,这里体现了 “银行家算法”,即 总资源数 ≥ 进程数 × ( 每个进程的最大需求 − 1 ) + 1 总资源数≥进程数×(每个进程的最大需求−1)+1 总资源数≥进程数×(每个进程的最大需求−1)+1
- 出现死锁的条件(
8.1.2 银行家算法
- 核心思想:找出一个安全序列,使得系统能处于安全状态,让每个进程都能顺利完成;
- 系统处于安全状态,就一定不会发生死锁; 与此相对的,当系统处于不安全状态时,也未必会发生死锁。但发生了死锁一定是在不安全状态;
- 对于进程与资源量,需要关注可用资源量、最大需求量、已分配资源数;
- 具体步骤是:
- 还需资源量 = 最大需求量 − 已分配资源量 还需资源量 = 最大需求量 - 已分配资源量 还需资源量=最大需求量−已分配资源量
- 剩余资源量 = 可用资源数 − 已分配资源数 剩余资源量 = 可用资源数 - 已分配资源数 剩余资源量=可用资源数−已分配资源数
- 对比还需资源数和剩余资源数(
主要是价格公道
),找出 还需资源数 < 剩余资源数 还需资源数 < 剩余资源数 还需资源数<剩余资源数的进程,让它执行; - 当执行完毕后,原进程占有的资源释放,数目记入剩余资源量并刷新;
- 由此便开始了不断重复上述过程,直到所有进程都执行完毕,系统处于得到安全序列。该安全序列表示系统状态是安全的;
8.2 存储管理
- OS负责内存空间的分配和回收;
- OS使用虚拟内存技术从逻辑上对内存空间进行扩充(
例如60G的游戏能在16G的内存上打开运行
); - 内存的分配和回收分为:
- 连续分配·管理方式(
局部性原理
)- 单一连续分配;
- 固定连续分配;
- 动态连续分配
- 首次适应算法;
- 最佳适应算法;
- 最坏适应算法;
- Next-Fit算法/邻近适应算法;
- 非连续分配·管理方式
- 连续分配·管理方式(
- 内存管理目标:提高内存利用率,减少碎片的产生
8.2.1 首次适应算法
- 实现思路:从低地址开始查找,找到第一个能满足大小的空闲分区;
- 实现方法:将空闲分区按照递增的次序排列,每次分配内存时顺序查找空闲分区链/表,直到找到第一个满足要求的后填入;
8.2.2 最佳适应算法
- 实现思路:从小空间到大空间进行查找,为了日后能保留大空间的内容范围,优先从小的空闲区开始使用;
- 实现方法:将空闲分区按照空间的大小递增排列,每次分配内存时顺序查找空闲分区链/表,直到找到第一个满足大小要求的后填入;
- 算法问题:人为制造许多碎片,难以利用;
8.2.3 最坏适应算法
- 实现思路:与上述相反,从大到小进行查找,为了分配后剩余的空闲区不会太小便于下次分配,优先从大的空闲区开始使用;
- 实现方法:将空闲分区按照空间大小的递减顺序排列,每次分配内存时顺序查找空闲分区链/表,直到找到第一个满足大小的后填入;
8.2.4 Next-Fit算法/邻近适应算法
- 在首次适应算法的基础上做出的改进,使得装入在地址上变得均匀;
- 实现思路:每次分配内存时,从上次分配的位置开始查找向高地址顺序查找,直到找到第一个满足大小的空闲分区;
- 实现方法:将空闲分区按照地址递增的顺序排成一个循环链表,每次分配内存都从上次查找结束的位置开始查找空闲分区链/表,直到找到大小上满足要求的第一个空闲分区;
- 几种存储管理模式:
- 分页存储管理;
- 分块存储管理;
- 分段存储管理;
8.2.5 分页存储管理
高级程序语言使用逻辑地址,但在程序的运行状态中和内存中,使用的是物理地址。
- 将辅存和内容中划分成相同大小的存储单元,称为页。每一个页都有地址,通过页表,既标注了在内存中的地址,也标注了在辅存中的地址;
- 在辅存中的地址是 页号 + 页内地址 页号 + 页内地址 页号+页内地址,通过页表的 页号 : 块号,页内地址 = 块内地址 页号 : 块号 ,页内地址 = 块内地址 页号:块号,页内地址=块内地址,与在内存中的地址 块号 + 块内地址 块号 + 块内地址 块号+块内地址相联系;
- 其中(物理)块号又被叫为页帧号;
- 上述思路通过地址映射机构实现;
-
- 优点:利用率高、碎片小,分配/管理思路清晰;
- 缺点:增加了系统开销,容易产生内部碎片且可能产生抖动1现象;
8.2.6 分段存储管理
- 与上边类似,但区别在于每一个存储单元的大小是不一致的,是可变的,这个大小被称为段长;
- 最主要的区别发生在段表:
- 段表没有段号,只有段长 + 基址;
- 段长是一个容纳所有大小情况的二进制数,基址与偏移量一样;
- 地址转换也是靠控制寄存器实现;
-
- 优点:允许多道程序共享内存,各段程序修改互不干扰;
- 缺点:内存利用率低下,容易产生外部碎片;
8.2.7 段页式存储管理
- 前两者的缝合怪;
- 地址构成较为复杂,为:
段号S 段内页号P 页内地址W - 实际上是一个将段长单位化成页长的组合,用一个段包含不同数量的页来动态调整段长;
- 实现方式是段表寄存器 → \rightarrow → 段表 → \rightarrow → 页表 → \rightarrow →主存;
-
- 优点:空间浪费小、存储共享容易、存储保护容易、能动态连接;
- 缺点:复杂性和开销增加,需要的硬件以及占用的内容也有所增加,执行速度大大下降;
- 以下介绍【页面置换算法】,用于处理内存被装满的情况:
- 最佳置换算法(
OPT
); - 先进先出置换算法(
FIFO
); - 最近最久未使用置换算法(
LRU
); - 时钟置换算法(
Clock
);
- 最佳置换算法(
- 它常用于决定要将那一个页面换出内存;
8.2.8 最佳置换算法
- 淘汰方式:置换掉以后永不使用,或者不再被使用时间最长的页面;
当Cache访问内存中没有找到相应的数据,被称为缺页,要从辅存中调入补充。 缺页率 = 缺页次数 总请求次数 缺页率 = \frac{缺页次数}{总请求次数} 缺页率=总请求次数缺页次数
8.2.9 先进先出置换算法
- 淘汰方式:每次选择的页面都是最早进入内存的页面;
8.2.10 最近最久未使用置换算法(LRU
)
- 淘汰方式:最近最久未使用的页面被淘汰,也就是距上一次请求最远的页面;
8.3 文件管理
- 文件的介绍:
- 定义:一组有意义的集合;
- 属性:
文件名.标识符
、类型
、路径
、大小
、修改日期
等; - 结构:
- 物理结构:文件如何存放在外存中;
- 目录结构:文件之间如何被组织起来;
- 逻辑结构:文件在内部是如何组织、编排的;
- 文件管理:增删改查、替换、管理空闲块;
- 功能提供:文件保护、文件共享;
8.3.1 文件目录
- 主要依靠 文件控制块(FCB) 实现,它是OS为了管理文件而设置的数据结构;
- FCB是文件存在的标志,它记录了OS管理文件所需要的全部信息;
- 目录结构分为:
- 一级目录结构;
- 二级目录结构;
- 多级目录/树形目录结构
- 绝对路径:从盘符开始的路径;
- 相对路径:从当前目录下开始的路径;
8.3.2 文件结构
- 分为物理结构(
分配方式
)和逻辑结构(存储格式
); - 逻辑结构分为:
- 无结构的流式文件(
stream
); - 有结构的记录式文件;
- 无结构的流式文件(
- 物理结构分为:连续分配、链接分配和索引分配;
8.3.2.1 索引分配
- 本方式通过设置【索引结点】的方式进行查找;
- 索引方式包括:
- 直接索引(
索引结点-->物理盘块
); - 二级间接索引;
- 三级间接索引;
- ……
- 直接索引(
- 存储结构是 磁盘索引块(物理块号) → 磁盘数据块(逻辑块号) 磁盘索引块(物理块号)\rightarrow 磁盘数据块(逻辑块号) 磁盘索引块(物理块号)→磁盘数据块(逻辑块号) ,其中索引块/数据块中包含很多的地址项,其数目等于 磁盘索引块 / 磁盘数据块大小 地址项大小 \frac{磁盘索引块/磁盘数据块大小}{地址项大小} 地址项大小磁盘索引块/磁盘数据块大小
8.3.2.2 空闲存储空间的管理
重点在位示图法;
- 概念介绍:
- 空闲区表
- 对OS上的所有空闲区建立的一张用于管理空闲空间的表;
- 位示图
- 在外存上建立一张位示图,用于记录文件存储器的使用情况;
- 每一位都对应文件存储器上的一个物理块(
等于是第x号物理块对应位示图中地址是x的逻辑块
),取值0和1分别表示空闲和占有;
- 空闲块链
- 每个空闲物理块中都有一个指向下一个的
next
指针,将所有空闲物理块串起来成一个链表,链表的head
指针放在文件管理器的特定位置(如管理块中
);
- 每个空闲物理块中都有一个指向下一个的
- 成组链接法
- 将空闲物理块分组,每一组的第一个空闲物理块登记下下一组的物理盘块号和空闲块总数;
- 空闲区表
- 如何使用位示图确定物理块的使用情况:
- 已知系统字长为 x x x位;
- 已知物理块编号从0开始,此时是 y y y;
- 则该物理块的使用情况在位示图的第 y x \frac{y}{x} xy 个字上被描述;
8.3.2.3 I/O 设备
- 即输出/输入设备;
8.3.2.3.1 I/O控制方式
好的,这里为你将表格转换成 Markdown 格式,并对部分内容进行优化,使其更清晰易懂:
完成一次I/O的过程 | CPU干预 | 每次I/O的数据传输单位 | 数据流向 | |
---|---|---|---|---|
程序直接控制方式 | CPU发出I/O命令后需要不断重复询问 | 极高 | 字 | 设备→CPU→内存 内存→CPU→设备 |
中断驱动方式 | CPU发出命令后可以去做其他事,本轮I/O完成后设备控制器发出中断信号 | 较高 | 字 | 设备→CPU→内存 内存→CPU→设备 |
DMA方式 | CPU发出命令后可以去做其他事,本轮I/O完成后DMA控制器发出中断信号 | 中等 | 块 | 设备→内存 内存→CPU |
通道控制方式 | CPU发出命令后可以去做其他事,通道会自行执行通道程序来完成I/O操作,完成后通道会向CPU发出中断信号 | 较低 | 一组块 | 设备→内存 内存→CPU |
9. 算法与数据结构
- 逻辑关系: n 个数据项 → m 个数据元素 → 一则数据 n个数据项\rightarrow m个数据元素 \rightarrow 一则数据 n个数据项→m个数据元素→一则数据;
- 数据结构是数据元素的集合;
- 逻辑结构:
- 集合;
- 线性结构;
- 树状结构;
- 网状/图状结构;
- 物理结构:
- 顺序存储;
- 链式存储;
- 索引存储;
- 散列存储(
Hash
);
- 算法的五个特性:
有穷性、确定性、可行性、输出、输入 有穷性、确定性、可行性、输出、输入 有穷性、确定性、可行性、输出、输入 - 效率度量(空间/时间):
- 空间复杂度:用 O ( ∗ ) O(*) O(∗)来表示,用于衡量算法在运行的过程中对空间的最大临时占有是多少;
- 时间复杂度:也用 O ( ∗ ) O(*) O(∗)来表示,用于衡量算法在运行过程中对时间的最大消耗是多少;
- 在递归的函数调用中,内存开销是很大的,基本等于递归调用的深度规模,而时间复杂度则与递归次数有关;
9.1 线性表
- 分为:顺序表、链表;
- 顺序表的随机读取能力强,但插入删除操作较为复杂;
- 链表的插入和删除操作较为容易,但是只能顺序读取,访问能力和查找能力较弱;
- 链表分为:
- 单链表(
head、next
); - 循环链表(
head、next。常用于循环队列、循环堆栈中
); - 双向链表;
- 单链表(
- 链表的实现(
以c++为例
):// domain 数据结构生成 struct Node{ int data; Node* next; }; class LinkList{ public: Node* headNode = new Node(); Node* head = headNode; headNode->next = NULL; Node* tail = headNode; // 头插法插入元素 void insertNodeByHead(int data){ Node* newNode = new Node(); newNode->data = data; newNode->next = head->next; head->next = newNode; } // 尾插法插入元素 void insertNodeByTail(int data){ Node* newNode = new Node(); newNode->data = data; newNode->next = tail->next; tail->next = newNode; tail = newNode; } // 查找元素 int findNodeByValue(int value){ Node* p = head->next; int k = 0; while(p != null){ if(p->data == data) { return k; } else { p = p->next; k++; } } if (k == 0) { cout << "未找到元素" << endl; } } // 删除元素 void removeByValue(int value){ Node* p = head->next; int k = 0; while (p != null){ if (p->data == value){ Node* q = p->next; p->next = p->next->next; delete q; k++; break; } } if (k != 0) cout << "未找到可删除项" << endl; } // 修改元素 void ModifyByValue(int k, int newValue){ Node* p = head; for (int i = 0; i < k; i++){ p = p->next; } p->data = newValue; return; } }
9.1.1 堆&栈
-
线性表表示: L = ( a 1 , a 2 , . . . , a i , a i + 1 , . . . , a n ) L = (a_1,a_2,...,a_i,a_{i+1},...,a_n) L=(a1,a2,...,ai,ai+1,...,an)
-
栈的数据结构构成:单进单出,先进后出、栈顶指针;
-
队列的数据结构构成:表头、表尾指针、尾进头出、先进先出;
-
循环队列的数据结构构成:表头、表尾指针
队空条件: h e a d = t a i l 队空条件:head = tail 队空条件:head=tail 队满条件: ( t a i l + 1 ) % M a x S i z e = h e a d 队满条件:(tail+1)\%MaxSize = head 队满条件:(tail+1)%MaxSize=head
9.1.2 字符串
- 空串是任何字符串的子串;
- 对串的基本操作有:
- 串的连接;
- 串的修改;
- 求串长;
- 求子串的位置;
- 串的比较;
- 串的实现:
- 顺序表:定长存储数组;
- 链表:块链;
- 求字串的位置
- 即字串的定位操作,又叫串的模式匹配。在这个过程中,字串又叫模式串;
9.1.3 广义表
- 深度:看有多少层嵌套;
- 长度:看含有多少个一级元素;
9.1.4 二叉树
-
叶子节点、根节点;
-
父节点、子节点;
-
遍历:
- 先序遍历:根、左、右;
- 中序遍历:左、根、右;
- 后序遍历:左、右、根;
-
树转二叉树:
- 找出每一个子树内的左子树节点;
- 以左子树结点为父节点,从左到右依次连接同层的节点;
- 倒转树结构,得到二叉树;
-
查找二叉树
- 左孩子结点的数值 < 根节点的数值 < 右孩子结点的数值;
-
霍夫曼树
- 构造最小/最优的带权路径长度的二叉树;
-
带权路径长度
=
∑
n
i
=
1
λ
i
l
i
带权路径长度 = \underset{i=1}{\overset{n}\sum} \lambda_il_i
带权路径长度=i=1∑nλili
其中 l i l_i li为第i个叶子结点的权值, i i i为叶子结点的编号, l i l_i li是第i个叶子结点的路径长度;
- 构造方法:
- 从权值序列中选出最小的两个,相加得到新的结点权值和新结点;
- 将新的结点权值加回到权值序列中,重复上述操作,直到只剩一个最终权值,得到根节点和最优路径长度的值;
-
线索二叉树
- 利用多出来的指针空间,实现二叉树的逆向追溯/查找;
- 左孩子指针指向当前结点访问的上一个地址,右孩子指针指向当前结点访问的下一个地址;
-
平衡二叉树
- 任意结点的左右子树深度相差不超过1,故平衡度只能为-1、0、1;
9.2 图
- 存储结构:
- 邻接表;
- 实现步骤:
- 将每一个顶点的邻接顶点用链式表示出来;
- 用一个数组来顺序存储上边的链表头指针(
也就是最早的一批用来编历的
);
- 实现步骤:
- 邻接矩阵
- 用一个 n × n n\times n n×n的矩阵来存储含有 n n n个结点的图的边信息;
- 其矩阵元素 R i j R_{ij} Rij被定义为: R i j = { 1 若顶点 i 到顶点 j 有邻接边 0 若顶点 i 到顶点 j 无邻接边 R_{ij} = \begin{cases}1 \quad若顶点i到顶点j有邻接边 \\\\ 0 \quad若顶点i到顶点j无邻接边\end{cases} Rij=⎩ ⎨ ⎧1若顶点i到顶点j有邻接边0若顶点i到顶点j无邻接边
- 邻接表;
9.2.1 图的遍历算法
- 即深度和广度;
- 深度优先遍历算法(
DFS
)- 首先访问出发顶点;
- 从出发顶点开始,依次搜索出发顶点的任何一个联结点;
- 若被搜索到的结点从来未被访问过,则在该结点继续执行深度优先算法;
有点类似于二叉树的先序遍历。
- 广度优先遍历算法(
BFS
)- 首先访问出发顶点;
- 从出发顶点开始,依次访问出发顶点的所有未访问节点;
- 再依次访问与ii.邻接的未访问的顶点;
类似于二叉树的层次遍历;
9.2.2 图的拓扑排序
- 用有向边表示活动之间开始的先后关系;
- 采用这种有向边的有向图,用于表示活动网络,简称为AOV网络;
- 求出一个有向图的拓扑排序的方法:
- 从出发顶点开始,将其加入排序序列;
- 将出发顶点从图中删去,选择下一个入读为0的结点作为出发顶点,重复上述过程;
- 当所有点都已经被添加到序列中,则已求得一种拓扑排序;
9.2.3 最小生成树
9.2.3.1 Prim算法
适用于图是无向边带有权值的情况;
- 主要实现思路:分两块,第一块是纳入图的点集 P P P,第二块是纳入图的边集 S S S;
- 步骤:
- 随机选择一个顶点加入到最小生成树中;
- 创建一个集合,用于存储已经加入到最小生成树中的顶点;
- 找到与当前最小生成树中顶点相连的所有边中,权值最小的边;
- 将这条边的另一个端点加入到最小生成树中;
- 重复步骤
iii.
和iv.
,直到所有顶点都被加入到最小生成树中
9.2.3.2 Kruskal算法
属于贪心算法的一种;
-
初始化:
- 将图中的每条边按照权重从小到大排序。
- 创建一个包含n个集合的并查集,每个集合初始时只包含一个顶点。
-
迭代:
- 从权重最小的边开始,依次检查每条边。
- 如果这条边的两个端点属于不同的集合,则将这两个集合合并,并将这条边加入到最小生成树中(
如果出现两个端点属于同一个集合,则说明出现环路,不应该采纳
)。 - 重复步骤2,直到所有顶点都在同一个集合中,即形成了一个连通图。
- 实现思路:先连接图中权值最小的边,再避免出现环路;
9.3 查找
-
二分查找
- 仅适用于有序的顺序表,如果无序的话,需要先排序;
- 需要依靠
low
、high
和mid
指针进行遍历,其中 m i d = ⌊ h i g h + l o w 2 ⌋ mid = \lfloor \frac{high + low}{2} \rfloor mid=⌊2high+low⌋ -
- 如果mid的值等于要查找的值,则返回mid;
- 如果
mid
的值小于要查找的值,则将low
指针移动到mid+1
的位置; - 如果
mid
的值大于要查找的值,则将high
指针移动到mid-1
的位置;
-
分块查找
- 需要一个包含区间内最大值的索引表,先根据索引表的最大值筛选出数据所处的小组,再在小组内进行查找;
- 这类分块的特点是块内无序,快间有序;
-
散列查找
- 其中的哈希算法常采用 取余/% 的运算;
- 当通过哈希算法确定的位置已经存有一个元素时,我们称这种情况为冲突。为解决冲突引入以下方法:
-
开放地址法
- 线性探测
- 当冲突发生时,逐个向后直至找到下一个空位;
- 新位置 = (冲突位置 + 固定步长 ) % 哈希表大小 新位置 = (冲突位置 + 固定步长)\quad\% \quad哈希表大小 新位置=(冲突位置+固定步长)%哈希表大小
- 但是这样会造成“堆积”的问题;
- 二次探测
- 发生冲突后,步长按照探测次数的平方生成;
- 新位置 = (当前位置 + i 2 ) % 哈希表大小 新位置 = (当前位置 + i^2)\quad\%\quad哈希表大小 新位置=(当前位置+i2)%哈希表大小
- 会导致存在一些空位无法被检测到;
- 双重散列
- 第二个步长有第二个Hash函数决定,每一次发生冲突都会使用一个新的Hash函数来计算其步长;
- 新位置 = ( 当前位置 + i ∗ h 2 ( 关键字 ) ) % 哈希表大小 新位置 = (当前位置 + i * h2(关键字))\quad\%\quad哈希表大小 新位置=(当前位置+i∗h2(关键字))%哈希表大小 其中h2(关键字)选择很多,常见的例如: h 2 ( v a l u e ) = 1 + ( k e y % ( 表大小 − 1 ) ) h2(value) = 1 + (key\%(表大小 - 1)) h2(value)=1+(key%(表大小−1))
- 线性探测
-
链地址法
- 原地变成链表,冲突了就添加到链表的末尾;
-
9.4 排序
- 可以分为:
- 内部排序(
仅在内存中进行的排序
)和外部排序(需要外存/辅存辅助进行的排序
); - 稳定排序和不稳定排序(
在排序过程中是否会改变相同值元素相对顺序
);
- 内部排序(
9.4.1 直接插入排序
- 将序列中的第一项设置成关键字,其余部分较关键字插入到包含关键字的已排好的子序列中;
9.4.2 希尔排序
- 前者的优化,按照步长,将隔着k个距离的元素视作同一子集,对其采用直接插入排序;
- 区别在于步长,本算法是动态步长,通过不断缩小步长,换得全面重整的效果,直至步长为1;
- 常规的缩小步长的方法,是取半下降;
9.4.3 冒泡排序
- 从前往后/从前往后,两两比较相邻元素的值,若为逆序(
前者比后者大
),则交换两者,直至序列完全有序; - 上述结束一趟冒泡排序,仅仅是将最大的数放置在序列的末尾,接着下一趟冒泡是第二大的数,依此类推;
9.4.4 快速排序
- 步骤:
- 先选择序列第一个元素作为枢轴(
pivot
); - 通过一趟排序,让大于等于枢轴的元素,小于枢轴的元素分别放置在枢轴元素的两侧,这称之为一次“划分”;
- 递归地对左右两侧的序列重复上述步骤进行划分,直至每一部分都只有一个元素或者为空,即所有元素都放在了其最终位置上;
- 先选择序列第一个元素作为枢轴(
9.4.5 简单选择排序
- 每一趟都在待排序的元素中选择最小的加入到有序子序列;
9.4.6 堆排序
- 首先要区分【大顶堆】和【小顶堆】:
- 父节点是最小的数,是小顶堆;
- 父节点是最大的数,是大顶堆;
- 通过将序列变成二叉树,再对二叉树的每一个父节点进行堆排序递归,最后会得到一个有序的序列,区别在于大顶堆是升序,小顶堆是降序;
9.4.7 归并排序
- 将排序分成若干个部分,对每个部分进行排序,再将部分两两合并,进行微排;
- 重复上述步骤,直至部分完全合并为一个有序序列;
9.4.8 基数排序
- 核心思想:多关键字排序;
- 按照个位、十位、……的顺序进行排序,最后得到一个有序的序列;
- 它是稳定的算法,对于一定范围的整数排序,它的效率非常高;
专题
以下是该表格的 Markdown 版本:
类别 | 排序方法 | 时间复杂度 - 平均情况 | 时间复杂度 - 最坏情况 | 空间复杂度(辅助存储) | 稳定性 |
---|---|---|---|---|---|
插入排序 | 直接插入排序 | O(n²) | O(n²) | O(1) | 稳定 |
插入排序 | 希尔排序 | O( n 1.3 n^{1.3} n1.3) | O(n²) | O(1) | 不稳定 |
选择排序 | 直接选择排序 | O(n²) | O(n²) | O(1) | 不稳定 |
选择排序 | 堆排序 | O(nlog₂n) | O(nlog₂n) | O(1) | 不稳定 |
交换排序 | 冒泡排序 | O(n²) | O(n²) | O(1) | 稳定 |
交换排序 | 快速排序 | O(nlog₂n) | O(n²) | O(log₂n) | 不稳定 |
归并排序 | O(nlog₂n) | O(nlog₂n) | O(n) | 稳定 | |
基数排序 | O(d(r+n)) | O(d(r+n)) | O(r+n) | 稳定 |
- 总而言之,言而总之:
- 如果需要稳定性且数据量大:选择 归并排序,尤其在需要保持数据相对位置的场景。
- 如果不需要稳定性、追求效率:选择 快速排序,因为它在平均情况下效率高,适合对大规模数据进行快速排序。
- 对于小规模数据,可以考虑插入排序,它在小数据集上效率好且稳定
9.5 算法分析
- 分治法
- 将规模较大、较为复杂的问题分解成若干个小规模的子问题,这些子问题要与原问题形式相同,从而递归地解决它们(
首先要求是没有疏漏的,且各个子问题之间是相互独立的
),最后合并得到原问题的解; - 递归的思想就是在运行的过程中调用自己,且一定要有结束递归的条件;
- 记忆:子问题相似 + 子问题相互独立;
- 将规模较大、较为复杂的问题分解成若干个小规模的子问题,这些子问题要与原问题形式相同,从而递归地解决它们(
- 动态规划法
- 将复杂的原问题分解成若干子问题,先求解子问题,再合并子问题的解,最终得到原问题的解;
- 与前者的区别在于,经分解得到的子问题一般不是互相独立的;
- 通常用于求解某种具有最优性质的答案,同时也是整体最优的(
可能有很多个
); - 一般为了确定整体最优,动态规划法需要开辟内存空间存储备选解到表中、而且每次查表的时间为常数(
不是常数就是递归了
)
- 贪心算法
- 其谋求局部最优的效果,并不从整体情况加以考虑;
- 一般可以快速得到满意的解,虽然不是最优的,但也能解决部分最优化问题;
- 回溯法
内存的抖动现象(
Memory Thrashing
)通常是指在计算机系统中,由于频繁的内存分配和释放,导致系统的内存频繁变化,影响性能甚至引发卡顿的问题。其通常由以下原因产生:- 频繁的对象创建和销毁;
- 内存碎片;
- 垃圾回收;
- 大对象分配;