10. 堆,优先队列与堆排序
堆
我们将一棵树变得更加特别,即限制其必须为完整的,即每一个树的层级都是满的,除了最后一层。
然后,我们联想生活中的常见的堆型结构:稻草堆,其特征是上小下大,那么,我们就可以定义数据结构中堆的特性:具有一定的偏序关系(优先级),且根节点的值总是该偏序关系的一端。
堆的节点关系
堆在内存中存储为一个 array 的形式,假设当前节点的 index 为 i (假设根节点 id = 1):
父节点 i / 2
左子节点 2i
右子节点 2i + 1
是否为叶节点 return (2 * i > n)
大顶堆的实现
fixUp()
当我们向一个堆进行插入元素操作,那么我们需要保证堆的特性,即根节点的值总是最大的。我们可以通过将新插入的元素与其父节点进行比较,如果新插入的元素大于其父节点,那么我们就将新插入的元素与其父节点进行交换,直到新插入的元素不再大于其父节点。
这个操作的结果是 将新的堆变成符合 父节点大于子节点的属性,且根节点一定是顺序最高的
fixDown()
当我们删除堆顶的元素的时候,我们需要将下面的元素逐个选择大的向上填充,那么我们就需要进行 fixDown() 操作, ...
Untitled
内存指令集 memory instructions
word 单词
表示几个字节的组合,在 lc2k 以及 arm 中,一个 word 通常是 4 个字节 32 位
有些地方写的 double word 就表示 8 字节
注意这里的 word 并不等于 char 之类的字符命令,只是一个长度单位,也可以用于指代长度 4 字节的 int
可寻址性 addressable
lc2k 指令集架构下,word 是可寻址的,也就是说,每个地址会指向内存中某个特定的 word,同时这也要求在 lc2k 的内存中 指令必须每 4 字节对齐一次,或者说PC 等指针在内存中的移动长度单位是 4 字节
移动 1 字节(比如 1 char)是非法的
相比较下, ARM 架构是 byte addressable,即每个地址指向一个字节,每次移动的单位也是1字节,可以逐个遍历一个字符串的每一个 char,更可以遍历每个 int (一次性读取 4 个字节)
LEGv8 内存指令代码
与 lc2k 指令一样,基于 base + displacement 的寻址方式,base为一个寄存器,displacement 为 ...
8. STL 的使用
std::move()
将参数转换为右值引用,在调用移动构造函数或移动赋值操作符时可以进行资源的转移而非拷贝,从而提高效率
std::swap()
实现
123456template<typename T>void swap(T& a, T& b) { T temp = std::move(a); a = std::move(b); b = std::move(temp);}
功能
可以交换两个动态数组,但是实际上是交换两个数组的指针,操作量非常小,效率非常高
也常常用于所谓需要 “copy” 的地方,特别是需要硬拷贝的类的赋值
一般类的赋值涉及指针的时候,需要额外 new 一个新的指针进行等于,且在判断是否 new 的时候需要判断输入对象和执行对象不相同(相同的话在析构的时候会将当前的数组指针释放且会重复申请同一个数值的多个指针)
使用这个 swap 函数进行 就不需要担心这个问题
STL 依赖的特殊关键字
explicit
阻止编译器隐藏转换数据类型,比如
123explicit FeetInches(int a); ...
9. 图的最短路径
问题背景阐述以及分类
在一个有权有向图(weighted directed graph)中,我们想知道从一个节点到其他某一个节点的路径的权和,注意权重可能是负数,我们这里假设图本身没有形成环
那么问题会被分成两类
Single-Source Shortest Paths (SSSP): 单个源出发到达某一节点的最短路径
All-pairs Shortest Paths (ASAP): 任意一对节点之间的最短路径
两则引理
引理1
如果从 sss 到 ttt 的最短路径经过 vvv 节点,那么 sss 到 vvv 和 vvv 到 ttt 均为最短路径
引理2
如果图中没有负权回路,那么从 sss 到 ttt 的最短路径中不会存在回路
递归法解决 SSSP 问题
我们可以得到递归公式:
dist(s,t)=minv∈neighbors(s){dist(s,v)+w(v,t)}dist(s, t) = \min_{v \in \text{neighbors}(s)} \{dist(s, v) + w(v, t)\}
dist(s,t)=v∈neighbors(s)min{dist ...
STM32 时钟树概念及配置思路
我们为什么需要时钟?
在一般芯片电路中,光信号的传播速度非常快,可以不计入时间,而电信号如果经过了芯片或者门电路,就会遇到延迟,如果这个延时不能很好的校对清楚,那么就会出现在二者时间差内产生奇怪的输出信号
因此我们在每一个输入到芯片的电路中添加一个时间管理器,即按照一定周期输出方波,芯片内部捕获上升沿,也就是说只有在电路信号在时钟达到上升沿的时候才会将信号传入处理器内部
同时,不同的外设装置例如串口,I2C总线等都有自己的时钟来判断是否要继续发送,这些时钟也需要被统一管理,因此我们需要一个时钟树来管理这些时钟
先进高性能总线 Advanced High-performance Bus AHB
这是 STM32 的内部总线,主要用于连接 CPU 和内部存储器,以及连接外设总线,其时钟线称为 HCLK
HCLK 与 cpu、内存、dma 的时间总线直接相连,因此频率相等
在处理器内部会存在一个 SystemTick 计时器用来给程序提供时间基准,一般是 72 MHz
分频器
一般是对频率做除法,比如我们将 HCLK 分频为 1/2,那么我们就可以得到 HCLK/2 的频率
SystemTi ...
l2_Tip
array 和 linked list 的对比
array 在 cpp 中还可以细分为 deque, vector 等
我们从几个方面来对比 array 和 linked list 的处理问题方面的优劣:
获取任意元素:array O(1), linked list O(n)
插入(任意位置)和append(末尾位置): array O(n), linked list 如果没有尾指针则 O(n), 有尾指针则 O(1),insert 是 O(n) 因为要先从 head 指针走到 index 位置再更新指针
额外尺寸: array 需要额外的三个指针,分别指向 beginning, current end 以及 max end, 而 linked list 只需要一个指针指向 head,也可能有一个指向尾巴的 tail 指针
内存占用:array 会浪费一定的内存,且如果实际空间非常小还会需要重新分配内存,linked list 则是动态分配内存,不会浪费内存
如何给 stack 排序?
其实我们不难想到 汉诺埃塔 问题,但是这个问题中我们用到了三个 stack,但是有没有可能只用一 ...
2. Multiplier
如果使用 lc-2k 指令集完成一个乘法操作,要求复杂度是 O(logn)O(\log n)O(logn) 要怎么做?
问题分析
lc-2k 指令集中涉及计算的部分只有 add 和 nor 两个指令
对于一个十进制的乘法操作,我们一般的做法是 列竖式,即将两个数的每一位相乘,然后将结果相加
但是在二进制中,我们应该想到的是将被乘数(较小的)写成二进制,看每一位是否为1,如果是1,那么即乘数的 cand*2^i ,然后将所有的结果相加
Bit Mask 实现
因为要找到每一位的值是否为 1, 那么我们必然需要一个能计算 每一位逻辑的工具,在 lc-2k 中, 只能用 nor 实现
将一个数字和 0xFFFF 进行 nor 操作,就会得到全 0, 但是如果我们将 0xFFFF 的第 k 位变成 0, 再进行 nor 操作,如果返回为 0, 说明原数字k位为1, 否则为 0
所以我们需要一个常量寄存器存储全1数, 即补码 -1; 我们还需要一个 每次乘 2 的bit mask 寄存器,初始值为 -1 (这里思考的时候不要转到 机器码空间,否则用补码进行计算有点逆天,我们直接用实际效果进行反 ...
4. 从 C 语言到 汇编指令
位数扩展
有符号数的符号位(most significant number)为 1 时,扩展时高位补 1
有符号数的符号位(most significant number) 为 0 时,扩展时高位补 0
无符号数的扩展时高位补 0
寄存器知道存储数值是有符号还是无符号的吗
答案是不知道,寄存器只会存储数值,不会存储数值的符号,符号是由汇编码的不同操作指令来决定的,比如 LDURSW 就是有符号数,LDUR 就是无符号数
例子
比如将一般的 int 进行 bit shift, 那么 1 >> 2 = 0, 而 -1 >> 2 = -1
数字存储顺序
Endian 这个词出自Jonathan Swift书写的《格列佛游记》。这本书根据将鸡蛋敲开的方法不同将所有的人分为两类,从圆头开始将鸡蛋敲开的人被归为Big Endian,从尖头开始将鸡蛋敲开的人被归为Littile Endian(这句话最为形象)。小人国的内战就源于吃鸡蛋时是究竟从大头(Big-Endian)敲开还是从小头(Little-Endian)敲开
注:存储顺序是内存的,寄存器没有这个说法!
一般来 ...
2. lc-2k 指令集架构
硬件结构
32 位 处理器: 指令长度是 32 位 , 寄存器大小是 32 位
32 位处理器的关键特征是其寄存器、数据总线和地址总线的宽度为 32 位。这意味着它可以处理 32 位的整数,并且通常可以寻址的最大内存空间为 4 GB。
在 32 位处理器中,寄存器和数据路径通常也是 32 位的,所以处理器可以在一次操作中处理 32 位的数据
其指令长度也是 32 位(这里的32 没有特指王楚钦)
8 个寄存器,其中 reg 0 一般只会被赋值 0
内存寻址范围 4GB 即 65536 words
指令形式
寄存器型指令结构
31 - 25
24 - 22
21 - 19
18 - 16
15 - 3
2 - 0
unused
opcode
regA
regB
unused
destR
立即数型指令结构
31 - 25
24 - 22
21 - 19
18 - 16
15 - 0
unused
opcode
regA
regB
offset
J 类型指令 (jalr)
31 - 25
24 - 22
21 - 19
18 - 16 ...
3. ARM 指令集架构
相比于前一节课讲的 lc2k isa, arm 架构的效果会好很多
ISA 的分类
RISC 架构,Reduced Instruction Set Computing
指令数量更少,编码的指令长度是一致的,硬件设备相对更加简单,程序更大,手写会更加困难,但是当前非常常用
应用: lc2k, risc-v, arm (包括其子集 LEG)
CISC 架构,Complex Instruction Set Computing
用更多、更复杂的指令集,编码的指令的长度是不一定的,所以硬件更复杂,程序的长度更短,手写更方便,现在比较少用了
应用: x86
指令及其分类
算术指令 Arithmetic
格式:3 operand fields, 一般会将 destination 寄存器放在第一位
ADD X1, X2, X3: 将 X2 + X3 的结果转存到 X1 寄存器中
这个和 lc2k 是反的
只能操作于
ARM 指令集存储形式
| opcode | Rm | shamt | Rn | Rd | 存储了一个指令集
其中 Rm 表示第二个 register operand, Rn 表 ...
