本文记录了学习慕课网课程【新版】计算机基础,计算机组成原理+操作系统+网络时的计算机组成原理篇的笔记,方便查阅复习使用
一、概述篇
1.1 计算机的发展历史
1)计算机发展的四个阶段
①第一个阶段:电子管计算机
背景:第二次世界大战是电子管计算机产生的催化剂,当时英国为了解密德国海军的密文而诞生的
特点:集成度小、空间占用大;功耗高,运行速度慢;操作复杂,更换程序需要接线
②第二个阶段:晶体管计算机
背景:贝尔实验室的三个科学家发明了晶体管
特点:集成度相对较高,空间占用相对小;功耗相对较低,运行速度较快; 操作相对简单,交互更加方便
③第三阶段:集成电路计算机
背景:德州仪器的工程师发明了集成电路(IC)
特点:计算机变得更小;功耗变得更低;计算速度变得更快
④第四阶段:超大规模集成电路计算机
特点:一个芯片集成了上百万的晶体管;速度更快,体积更小,价格更低,更能被大众接受; 用途丰富:文本处理、表格处理、高交互的游戏与应用
2)微型计算机的发展历史
摩尔定律:集成电路的性能,每18-24个月就会提升一倍
1.2 计算机的分类
1)超级计算机
- 功能最强、运算速度最快、存储容量最大的计算机
- 多用于国家高科技领域和尖端技术研究
- 标记他们运算速度的单位是TFlop/s(1TFlop/s=每秒一万亿次浮点计算)
例子:Intel® Core™ i7-6700K CPU @ 4.00GHz: 44.87 GFlop/s(44.87 GFlop/s = 0.04487TFlop/s )
由此可见我们常用计算机的运算速度和超级计算机是有很大差距的
2)大型计算机
- 又称大型机、大型主机、主机等
- 具有高性能,可处理大量数据与复杂的运算
- 在大型机市场领域,IBM占据着很大的份额
3)迷你计算机(服务器)
- 也称为小型机,普通服务器
- 不需要特殊的空调场所
- 具备不错的算力,可以完成较复杂的运算
普通服务器已经代替了传统的大型机,成为大规模企业计算的中枢
4)工作站
- 高端的通用微型计算机,提供比个人计算机更强大的性能
- 类似于普通台式电脑,体积较大,但性能强劲
5)微型计算机
- 又称为个人计算机,是最普通的一类计算机
- 麻雀虽小、五脏俱全,从构成的本质上来讲,个人计算机与前面的分类无异
1.3 计算机的体系与结构
1)冯诺依曼体系
指将程序指令和数据一起存储的计算机设计概念结构
特点:
- 能够把需要的程序和数据送至计算机中
- 能够长期记忆程序、数据、中间结果及最终运算结果的能力
- 能够具备算术、逻辑运算和数据传送等数据加工处理的能力
- 能够按照要求将处理结果输出给用户
瓶颈:
- CPU和存储器速率之间的问题无法调和
2)现代计算机结构
- 现代计算机在冯诺依曼体系结构基础上进行修改
- 在CPU内部增加高速缓存解决CPU与存储设备之间的性能差异问题
1.4 计算机的层次与编程语言
1)程序翻译与程序解释
①程序翻译
②程序解释
区别:
- 计算机执行的指令都是L0
- 翻译过程生成新的L0程序,解释过程不生成新的L0程序
- 解释过程由L0编写的解释器去解释L1程序
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
2)计算机的层次与编程语言
①硬件逻辑层
- 门、触发器等逻辑电路组成
- 属于电子工程的领域
②微程序机器层
- 编程语言是微指令集
- 微指令所组成的微程序直接交由硬件执行
③传统机器层
- 编程语言是CPU指令集(机器指令:一条机器指令对应一个微程序,一个微程序对应一组微指令)
- 编程语言和硬件是直接相关
- 不同架构的CPU使用不同的CPU指令集
④操作系统层
- 向上提供了简易的操作界面
- 向下对接了指令系统,管理硬件资源
- 操作系统层是在软件和硬件之间的适配层
⑤汇编语言层
- 编程语言是汇编语言
- 汇编语言可以翻译成可直接执行的机器语言
- 完成翻译的过程的程序就是汇编器
⑥高级语言层
- 编程语言为广大程序员所接受的高级语言
- 高级语言的类别非常多,有几百种
- 常见的高级语言有:Python、Java、C/C++、Golang等
⑦应用层
- 满足计算机针对某种用途而专门设计
1.5 计算机的速度单位
1)容量单位
- 在物理层面,高低电平记录信息
- 理论上只认识0/1两种状态(0/1称为bit(比特位))
- 0/1能够表示的内容太少了,需要更大的容量表示方法
1G内存,可以存储多少字节的数据?可以存储多少比特数据?
1G = 1024^3Bytes = 1024^3*8bits
为什么网上买的移动硬盘500G,格式化之后就只剩465G了?
硬盘商一般用10进位标记容量
𝟓𝟎𝟎 ∗ 𝟏𝟎𝟎𝟎^3 / 1024^3 约等于 465
2)速度单位
- 网络速度
- CPU频率
①网络速度
为什么电信拉的100M光纤,测试峰值速度只有12M每秒?
网络常用单位为(Mbps)
100M/s = 100Mbps = 100Mbit/s
100Mbit/s=(100/8)MB/s=12.5MB/s
②CPU速度
- CPU的速度一般体现为CPU的时钟频率
- CPU的时钟频率的单位一般是赫兹(Hz)
- 主流CPU的时钟频率都在2GHz以上
- Hz其实就是秒分之一
- 并不是描述计算机领域所专有的单位,它是每秒中的周期性变动重复次数的计量
1.6 计算机的字符与编码集
1)字符编码集的历史
①ASCII码
-
使用7个bits(𝟑𝟑 + 𝟗𝟓 = 𝟏𝟐𝟖 = 𝟐 的7次方)就可以完全表示ASCII码,包含95个可打印字符和33个不可打印字符(包括控制字符)
-
然而很多应用或者国家中的符号都无法表示,所以第一次对ASCII码进行扩充,7bits => 8bits
②Extended ASCII码
- 常见数学运算符
- 带音标的欧洲字符
- 其他常用符、表格符等
2)中文编码集
①GBK编码集
- 《信息交换用汉字编码字符集——基本集》
- 一共收录了 7445 个字符
- 包括 6763 个汉字和 682 个其它符号
②GBK编码集
- 《汉字内码扩展规范》
- 向下兼容GB2312,向上支持国际ISO标准
- 收录了21003个汉字,支持全部中日韩汉字
③Unicode编码集
- 统一码、万国码、单一码,兼容全球的字符集
- Unicode定义了世界通用的符号集,UTF-*实现了编码
- UTF-8以字节为单位对Unicode进行编码
二、组成篇
2.1 计算机的总线
1)总线的概述
- 提供了对外连接的接口
- 不同设备可以通过USB接口进行连接
- 连接的标准,促使外围设备接口的统一,解决不同设备之间的通信问题
我们常见的USB即为通用串行总线,定义了统一接口,不然不同设备需要不同接口,我们的电脑为此需要设计很多接口,增加复杂度,所以定义了通用的USB接口
分散连接:
如果没有总线的话,输入设备需要分别连接计算机的各个组件才能进行工作,很复杂
总线连接:
有了总线之后,输入设备只需通过接口连接到总线上,由总线和计算机组件通信即可,无需分别连接计算机各个组件,大大降低了设计的复杂度
2)总线的分类
①片内总线
- 芯片内部的总线,高集成度芯片内部的信息传输线
- 寄存器与寄存器之间
- 寄存器与控制器、运算器之间
②系统总线
- 系统总线又分为:数据总线、地址总线、控制总线
- CPU、主内存、IO设备、各组件之间的信息传输线
数据总线:
- 双向传输各个部件的数据信息
- 数据总线的位数(总线宽度)是数据总线的重要参数,一般与CPU位数相同(32位、64位)
地址总线:
- 指定源数据或目的数据在内存中的地址
- 地址总线的位数与存储单元有关,地址总线位数=n,则寻址范围:0~2的n次方
控制总线:
- 控制总线是用来发出各种控制信号的传输线
- 控制信号经由控制总线从一个组件发给另外一个组件
- 控制总线可以监视不同组件之间的状态(就绪/未就绪)
3)总线的仲裁
以下图片中,红色双箭头线均指总线
为什么需要总线仲裁?
当主存需要和硬盘、IO设备进行通信时,硬盘和IO设备都发起了使用总线的请求,此时总线无法决定给谁使用,所以需要进行仲裁,决定其中一个组件使用;为了解决总线使用权的冲突问题,有以下几种方法
仲裁的方法:
①链式查询
设备1、设备2、设备3通过仲裁控制线连接仲裁控制器,当设备1和设备2同时发起总线使用请求时,仲裁控制器收到请求后会发送一个同意的信号,由于设备1、设备2、设备3之间通过链式连接传递信号,会优先经过设备1,设备1就可以使用总线了,而设备2则不可以;如果只有设备2需要使用总线,则信号经过设备1时,由于设备1不使用,此时信号会传递给设备2,设备2就可以使用总线了
-
好处:电路复杂度低,仲裁方式简单
-
坏处:优先级低的设备难以获得总线使用权
-
坏处:对电路故障敏感
②计时器定时查询
- 仲裁控制器对设备编号并使用计数器累计计数
- 接收到仲裁信号后,往所有设备发出计数值
- 计数值与设备编号一致则获得总线使用权
当设备2需要使用总线时,会发送请求到仲裁控制器,仲裁控制器收到请求后向所有设备发出计数值,此时由于计数值为1,不会有设备使用总线;接着仲裁控制器使用计数器对计数值加一在发送出去,此时计数值为2和设备2的编号一致,则设备2获得了总线使用权
③独立请求
- 每个设备均有总线独立连接仲裁器
- 设备可单独向仲裁器发送请求和接收请求
- 当同时收到多个请求信号,仲裁器有权按优先级分配使用权
- 好处:响应速度快,优先顺序可动态改变
- 坏处:设备连线多,总线控制复杂
2.2 计算机的输入输出设备
1)常见的输入输出设备
2)输入输出接口的通用设计
有了输入输出设备,现在存在以下问题:
- 如何向设备发送数据?
- 怎么知道设备有没有被占用?
- 怎么读取数据?
- 设备是否连接?
- 设备是否已经启动?
而数据线、状态线、命令线、设备选择线就是解决这些问题的设计
①数据线
-
是I/O设备与主机之间进行数据交换的传送线
-
单向传输数据线
-
双向传输数据线
②状态线
- IO设备状态向主机报告的信号线
- 查询设备是否已经正常连接并就绪
- 查询设备是否已经被占用
③命令线
- CPU向设备发送命令的信号线
- 发送读写信号
- 发送启动停止信号
④设备选择线
- 主机选择I/O设备进行操作的信号线
- 对连在总线上的设备进行选择
3)CPU与IO设备的通信
①程序中断
- 当外围IO设备就绪时,向CPU发出中断信号
- CPU有专门的电路响应中断信号
- 提供低速设备通知CPU的一种异步的方式
- CPU可以高速运转同时兼顾低速设备的响应
②DMA(直接存储器访问)
- DMA直接连接主存与IO设备
- DMA工作时不需要CPU的参与
- 硬盘、外置显卡
2.3 计算机存储器概览
1)存储器的分类
- 按存储介质分分类
- 按存取方式分类
2)存储器的层次结构
①缓存-主存层次
- 原理:局部性原理,局部性原理是指CPU访问存储器时,无论是存取指令 还是存取数据,所访问的存储单元都趋于聚集在一个 较小的连续区域中。
- 实现:在CPU与主存之间增加一层速度快(容量小)的Cache
- 目的:解决主存速度不足的问题
②主存-辅存层次
- 原理:局部性原理
- 实现:主存之外增加辅助存储器(磁盘、SD卡、U盘等)
- 目的:解决主存容量不足的问题
2.4 计算机的主存储器与辅助存储器
1)主存储器——内存
-
RAM(随机存取存储器:Random Access Memory)
-
RAM通过电容存储数据,必须隔一段时间刷新一次
-
如果掉电,那么一段时间后将丢失所有数据
CPU通过地址总线和数据总线与内存连接
- 对于 32 位操作系统,可以支持 4 G B 内存 ( 2 32 = 4 ∗ 2 30 = 4 G B ) 对于 64 位操作系统,可以支持 2 34 G B 内存 ( 2 64 = 2 34 ∗ 2 30 = 2 34 G B ) 对于32位操作系统,可以支持4GB内存(2^{32} = 4*2^{30} = 4GB) \\ 对于64位操作系统,可以支持2^{34}GB内存(2^{64} = 2^{34}*2^{30} = 2^{34}GB) 对于32位操作系统,可以支持4GB内存(232=4∗230=4GB)对于64位操作系统,可以支持234GB内存(264=234∗230=234GB)
2)辅助存储器——磁盘
-
表面是可磁化的硬磁特性材料
-
移动磁头径向运动读取磁道信息
磁盘的调度算法:
前提:磁头在磁道4,磁头方向向外,现读取磁道:1 4 2 3 1 5
①先来先服务算法
- 按顺序访问进程的磁道读写需求
结果:1->4->2->3->1->5
②最短寻道时间优先
- 向距离最近的磁道读写需求
结果:4->5->3->2->1->1
③扫描算法(电梯算法)
- 每次只往一个方向移动
- 到达一个方向需要服务的尽头再反方向移动
结果:4->3->2->1->1->5
④循环扫描算法
- 只往一个方向读取,要么由内向外,要么由外向内
结果:4->5->1->1->2->3
2.5 计算机的高速缓存
1)高速缓存的工作原理
①字与字块
- 字:是指存放在一个存储单元中的二进制代码组合
- 字块:存储在连续的存储单元中而被看作是一个单元的一组字
- 字的地址包含两个部分,前m位指定字块的地址,后b位指定字在字块中的地址
例子:
假设主存用户空间容量为4G,字块大小为4M,字长为32位,则对于字地址中的块地址m和块内地址b的位数,至少应该是多少?
4𝐺 = 4096𝑀
字块数:4096 ÷ 4 = 1024
字块地址m:log2 1024 = 10
块内字数: 4𝑀 ÷ 32𝑏𝑖𝑡 = 1048576
块内地址b: log2 1048576 = 20
所以𝑚 ≥ 10 b ≥ 20
- 缓存的存储的逻辑结构类似,速度更快,但容量较小
②命中率
- CPU需要的数据在缓存里,可以直接取
- CPU需要的数据不在缓存里,需要去主存拿
- 因此CPU每次能从缓存里取到数据的概率高低叫命中率
命中率是衡量缓存的重要性能指标,理论上CPU每次都能从高速缓存取数据的时候,命中率为1
命中率计算公式:
访问主存次数:
N
m
访问
C
a
c
h
e
次数:
N
c
命中率:
h
=
N
c
N
c
+
N
m
访问主存次数:N_m\\ 访问Cache次数:N_c\\ 命中率: h = \frac{N_c}{N_c + N_m}
访问主存次数:Nm访问Cache次数:Nc命中率:h=Nc+NmNc
访问效率计算公式:
访问效率:
e
命中率:
h
访问主存时间:
t
m
访问缓存时间:
t
c
访问
C
a
c
h
e
−
主存系统平均时间:
t
a
=
h
t
c
+
(
1
−
h
)
t
m
e
=
t
c
t
a
=
t
c
h
t
c
+
(
1
−
h
)
t
m
访问效率: e\\ 命中率:h\\ 访问主存时间:t_m \qquad 访问缓存时间:t_c\\ 访问Cache-主存系统平均时间:t_a = ht_c + (1 − h)t_m\\ e = \frac{t_c}{t_a} = \frac{t_c}{ht_c + (1-h)t_m}
访问效率:e命中率:h访问主存时间:tm访问缓存时间:tc访问Cache−主存系统平均时间:ta=htc+(1−h)tme=tatc=htc+(1−h)tmtc
例子:
1.假设CPU在执行某段程序时,共访问了Cache命中2000次,访问主存50次,已知Cache的存取时间为50ns,主存的存取时间为200ns,求Cache主存系统的命中率、访问效率和平均访问时间。
h = 2000 / (2000 + 50) = 0.97
e = 50 / (0.97 * 50 + (1-0.97) * 200) = 0.917 = 91.7%
平均访问时间 = 0.97 ∗ 50 + 1 − 0.97 200 = 54.5𝑛s
2)高速缓存的替换策略
- 缓存由于存储量较小,因此当存储量不够时需要性能良好的缓存替换策略,把一些数据替换成新的数据
①随机算法
- 随机替换某个字块
②先进先出算法(FIFO)
- 把高速缓存看做是一个先进先出的队列
- 优先替换最先进入队列的字块,如图队列头部位1,尾部位8
③最不经常使用算法(LFU)
- 优先淘汰最不经常使用的字块
- 需要额外的空间记录字块的使用频率
④最近最少使用算法(LRU)
- 优先淘汰一段时间内没有使用的字块
- 有多种实现方法,一般使用双向链表
- 把当前访问节点置于链表前面(保证链表头部节点是最近使用的)
2.6 计算机的指令系统
1)机器指令的形式
-
机器指令主要由两部分组成:操作码、地址码
-
操作码指明指令所要完成的操作
-
操作码的位数反映了机器的操作种类
-
地址码直接给出操作数或者操作数的地址,分三地址指令、二地址指令和一地址指令
①三地址指令
- 通常是地址1和地址2运算后将结果保存到地址3中
②二地址指令
- 通常是地址1和地址2运算后将结果保存到地址1或地址2中
③一地址指令
- 第一个为地址1对自己做一些操作将结果保存到地址1中
- 第二个为地址1做一些默认行为,比如自增操作
④零地址指令
- 在机器指令中无地址码
- 空操作、停机操作、中断返回操作等
2)机器指令的操作类型
①数据传输
- 寄存器之间、寄存器与存储单元、存储单元之间传送
- 数据读写、交换地址数据、清零置一等操作
②算术逻辑操作
- 操作数之间的加减乘除运算
- 操作数的与或非等逻辑位运算
③移位操作
- 数据左移(乘2)、数据右移(除2)
- 完成数据在算术逻辑单元的必要操作
④控制指令
- 等待指令、停机指令、空操作指令、中断指令等
3)机器指令的寻址方式
1.指令寻址
指令寻址分为顺序寻址、跳跃寻址。如图,当程序顺序执行101到105程序为顺序寻址,其中105地址所在的指令为跳跃寻址,跳回到102地址执行指令
2.数据寻址
①立即寻址
- 指令直接获得操作数
- 无需访问存储器
②直接寻址
- 直接给出操作数在主存的地址
- 寻找操作数简单,无需计算数据地址
③间接寻址
- 指令地址码给出的是操作数地址的地址
- 需要访问一次或多次主存来获取操作数
区别:
寻址方式 | 优点 | 缺点 |
---|---|---|
立即寻址 | 速度快 | 地址码位数限制操作数表示范围 |
直接寻址 | 寻找操作数简单 | 地址码位数限制操作数寻址范围 |
间接寻址 | 操作数寻址范围大 | 速度较慢 |
2.7 计算机的控制器
①程序计数器
- 程序计数器用来存储下一条指令的地址
- 循环从程序计数器中拿出指令
- 当指令被拿出时,指向下一条指令
②时序发生器
-
电气工程领域,用于发送时序脉冲
-
CPU依据不同的时序脉冲有节奏的进行工作
③指令译码器
- 指令译码器是控制器的主要部件之一
- 计算机指令由操作码和地址码组成
- 翻译操作码对应的操作以及控制传输地址码对应的数据
④指令寄存器
- 指令寄存器也是控制器的主要部件之一
- 从主存或高速缓存取计算机指令
⑤主存地址寄存器
- 保存当前CPU正要访问的内存单元的地址
⑥主存数据寄存器
- 保存当前CPU正要读或写的主存数据
⑦通用寄存器
- 用于暂时存放或传送数据或指令
- 可保存ALU的运算中间结果
- 容量比一般专用寄存器要大
2.8 计算机的运算器
①数据缓冲器
- 分为输入缓冲和输出缓冲
- 输入缓冲暂时存放外设送过来的数据
- 输出缓冲暂时存放送往外设的数据
②ALU
- ALU:算术逻辑单元,是运算器的主要组成
- 可以进行常见的位运算(左右移、与或非等)
- 可以进行算术运算(加减乘除等)
③状态字寄存器
- 存放运算状态(条件码、进位、溢出、结果正负等)
- 存放运算控制信息(调试跟踪标记位、允许中断位等)
④通用寄存器
- 用于暂时存放或传送数据或指令
- 可保存ALU的运算中间结果
- 容量比一般专用寄存器要大
2.9 计算机指令执行的过程
1)指令执行过程
相关视频
取指令–>分析指令–>执行指令
如图数据缓存、指令缓存为高速缓存部分,左边为运算器部分,右边为控制器部分,它们之间通过片内总线连接在一起。
- 取指令阶段:程序计数器(PC)提供将要执行的指令的内存地址,然后从内存中读取指令并存入指令寄存器(IR)。
- 指令译码阶段:指令译码器(ID)从指令寄存器中取出指令,分析其操作码(opcode),以确定指令的类型和操作,同时,准备好操作数。
- 执行指令阶段:根据操作码的指示,**控制器(CU)发出控制信号,指挥算术逻辑单元(ALU)**或其他处理单元执行指定的操作,这个阶段可能包括数据读取、运算或修改寄存器等内容。
- **形成下一条指令地址:**在指令执行期间或之后,程序计数器根据指令的长度和是否发生跳转更新为下一条指令的地址,准备下一轮的取指令循环。
存在问题:
- CPU的综合利用率并不高,取指令、分析指令时不能执行指令,执行指令时不能取指令、分析指令;运算器和控制器不能同时工作
2)CPU的流水线设计
- 类似工厂的装配线
- 工厂的装配线使得多个产品可以同时被加工
- 在同一个时刻,不同产品均位于不同的加工阶段
**普通串行工作:**取指令 分析指令 执行指令 取指令 分析指令 执行指令
**流水线工作:**取指令 分析指令 执行指令
取指令 分析指令 执行指令
- 流水线效率,当m很大时,流水线执行指令的效率大概是串行执行指令的3倍
串行执行
m
条指令:
T
1
=
3
t
∗
m
串行执行m条指令: T_1 = 3t * m
串行执行m条指令:T1=3t∗m
流水线执行
m
条指令:
T
2
=
t
∗
(
m
+
2
)
流水线执行m条指令: T_2 = t*(m+2)
流水线执行m条指令:T2=t∗(m+2)
流水线效率:
H
=
T
2
T
1
=
t
∗
(
m
+
2
)
3
t
∗
m
=
1
3
+
1
3
m
流水线效率: H = \frac{T_2}{T_1} = \frac{t*(m+2)}{3t*m} = \frac{1}{3} + \frac{1}{3m}
流水线效率:H=T1T2=3t∗mt∗(m+2)=31+3m1
2.10 CPU访问存储器的详细过程
TLB(页表缓冲:Translation Lookaside Buffer)
- CPU的一种缓存:页表缓存、转址旁路缓存
- 加快从虚拟地址映射到物理地址的转译速度
- 类似编程里面的Map、Dict等数据结构
内存页
- 将进程逻辑空间等分成若干大小的页面
- 相应的把物理内存空间分成与页面大小的物理块
- 以页面为单位把进程空间装进物理内存中分散的物理块
CPU访问存储器的详细过程
2.11 总线架构与总线控制流程
1)单总线架构
- 早期计算机采用分散连接的方式
- 为了便捷地接入设备,引入了总线设计的结构
- 总线是连接多个部件的信息传输线,是各个部件共享的传输介质
- 由于所有数据都经过该总线,所以随着计算机发展,单总线极易成为瓶颈
- 多总线结构才是符合现代计算机要求的架构
2)多总线架构
- 多总线结构的目的是为了提高计算机的性能
- 不同传输速率的设备分类接入不同的总线(片内总线、系统总线、通信总线等)
以存储器为核心的双总线结构:
多层总线结构:
3)总线判优控制
- 由于总线在一个时间,只能有一个设备使用,所以总线的使用权需要进行控制。
- 总线的使用权控制称为总线的判优控制。
三、计算篇
3.1 进制运算的基础
1)进制概述
进制的定义:
- 进位制是一种记数方式,亦称进位计数法或位值计数法
- 有限种数字符号来表示无限的数值
- 使用的数字符号的数目称为这种进位制的基数或底数
常见的进制:
- 二十进制:玛雅文明的玛雅数字、因努伊特的因努伊特数字
- 六十进制:时间、坐标、角度等量化数据
- 十六进制:[0-9]和A、B、C、D、E、F
- 八进制、二进制
计算机喜欢二进制,但是二进制表达太长了;使用大进制位可以解决这个问题,比如八进制、十六进制满足2的n次方的要求,可以解决表达太长问题
二进制: 1024=0b1000000000
八进制: 1024=0o2000
十六进制: 1024=0x400
2)二进制运算的基础
正整数N,基数为r
N
=
d
n
−
1
d
n
−
2
⋅
⋅
⋅
d
1
d
0
=
d
n
−
1
r
n
−
1
+
d
n
−
2
r
n
−
2
+
⋅
⋅
⋅
+
d
1
r
+
d
0
N
=
1024
N
=
1
∗
1
0
3
+
2
∗
1
0
1
+
4
N
=
1000000000
N
=
1
∗
2
10
\,\,\,N = d_{n-1}d_{n-2}···d_1d_0\\ \qquad\qquad\qquad\qquad\qquad= d_{n-1}r^{n-1} + d_{n-2}r^{n-2} + ··· + d_1r + d_0\\ \qquad\qquad\qquad N = 1024\qquad\qquad\qquad N = 1 * 10^3 + 2 * 10^1 + 4\\ N = 1000000000\qquad\,\,\,\,\,\,\, N = 1 * 2^{10}\\
N=dn−1dn−2⋅⋅⋅d1d0=dn−1rn−1+dn−2rn−2+⋅⋅⋅+d1r+d0N=1024N=1∗103+2∗101+4N=1000000000N=1∗210
①(整数)二进制转换十进制:按权展开法
N
=
(
01100101
)
=
1
∗
2
6
+
1
∗
2
5
+
1
∗
2
2
+
1
=
101
N
=
(
11101101
)
=
1
∗
2
7
+
1
∗
2
6
+
1
∗
2
5
+
1
∗
2
3
+
1
∗
2
2
+
1
=
237
N = (01100101) = 1 * 2^6 + 1 * 2^5 + 1 * 2^2 + 1 = 101\\ \qquad\qquad\qquad\,\,\,\,\,\,\, N = (11101101) = 1 * 2^7 + 1 * 2^6 + 1 * 2^5 + 1 * 2^3 + 1 * 2^2 + 1 = 237
N=(01100101)=1∗26+1∗25+1∗22+1=101N=(11101101)=1∗27+1∗26+1∗25+1∗23+1∗22+1=237
②(整数)十进制转换二进制:重复相除法
重复除以2 | 得商 | 取余数 |
---|---|---|
101/2 | 50 | 1 |
50/2 | 25 | 0 |
25/2 | 12 | 1 |
12/2 | 6 | 0 |
6/2 | 3 | 0 |
3/2 | 1 | 1 |
1/2 | 0 | 1 |
余数从下往上取得到N = 1100101
验证:
N
=
(
1100101
)
=
1
∗
2
6
+
1
∗
2
5
+
1
∗
2
2
+
1
=
101
N = (1100101) = 1 * 2^6 + 1 * 2^5 + 1 * 2^2 + 1 = 101
N=(1100101)=1∗26+1∗25+1∗22+1=101
③(小数)二进制转换十进制:按权展开法
N
=
(
0.11001
)
=
1
∗
2
−
1
+
1
∗
2
−
2
+
1
∗
2
−
5
=
0.78125
=
25
32
N
=
(
0.01011
)
=
1
∗
2
−
2
+
1
∗
2
−
4
+
1
∗
2
−
5
=
0.34375
=
11
32
N = (0.11001) = 1 * 2^{-1} + 1 * 2^{-2} + 1 * 2^{-5} = 0.78125 = \frac{25}{32}\\ N = (0.01011) = 1 * 2^{-2} + 1 * 2^{-4} + 1 * 2^{-5} = 0.34375 = \frac{11}{32}\\
N=(0.11001)=1∗2−1+1∗2−2+1∗2−5=0.78125=3225N=(0.01011)=1∗2−2+1∗2−4+1∗2−5=0.34375=3211
④(小数)十进制转换二进制:重复相乘法
重复乘以2 | 得积 | 取1 |
---|---|---|
25/32 | 50/32=1+9/16 | 1 |
9/16 | 18/16 = 1+1/8 | 1 |
1/8 | 1/4=0+1/4 | 0 |
1/4 | 1/2=0+1/2 | 0 |
1/2 | 1=1+0 | 1 |
最后一列从上往下取得N=0.11001
验证:
N
=
(
0.11001
)
=
1
∗
2
−
1
+
1
∗
2
−
2
+
1
∗
2
−
5
=
0.78125
=
25
32
N = (0.11001) = 1 * 2^{-1} + 1 * 2^{-2} + 1 * 2^{-5} = 0.78125 = \frac{25}{32}
N=(0.11001)=1∗2−1+1∗2−2+1∗2−5=0.78125=3225
3.2 二进制的原码表示法
在我们日常生活中,通常+表示正数,-表示负数;那在计算机中要怎么表示正负数呢?由于正负是两种状态,而计算机是用二进制表示数据,所以在原码表示法中,0表示正数,1表示负数。
定义:
- 使用0表示正数、1表示负数
- 规定符号位位于数值第一位
- 表达简单明了,是人类最容易理解的表示法
缺点:
- 0有两种表示方法:00、10,有歧义
- 原码进行运算非常复杂,特别是两个操作数符号不同的时候需要判断两个操作数绝对值大小,使用绝对值大的数减去绝对值小的数,对于符号值,以绝对值大的为准
由于原码进行运算太复杂,因此我们需要一种不同符号操作数更加简单的运算方法,能够使用正数代替负数的方法,让加法操作代替减法操作,从而消除减法。
3.3 二进制的补码表示法
①引进补码的目的:
- 减法运算复杂,希望找到使用正数替代负数的方法
- 使用加法代替减法操作,从而消除减法
- 但是在计算补码的过程中,还是使用了减法!!
②定义:
x
=
{
x
2
n
>
x
≥
0
2
n
+
1
+
x
0
>
x
≥
−
2
n
x= \begin{cases} x&\text2^n>x≥0\\ 2^{n+1}+x&\text0>x≥-2^n \end{cases}
x={x2n+1+x2n>x≥00>x≥−2n
③例子:
例子1:n=4,x=13,计算x的二进制原码和补码
原码:x=0,1101
补码:x=0,1101
例子2:例子2:x=-13,计算x的二进制原码和补码
原码:x=1,1101
补码:x=2^(4+1) − 13 = 100000 − 1101 = 10011
3.4 二进制的反码表示法
①引进反码的目的:
- 找出原码和补码之间的规律,消除转换过程中的减法
②定义:
x
=
{
x
2
n
>
x
≥
0
(
2
n
+
1
−
1
)
+
x
0
>
x
≥
−
2
n
x= \begin{cases} x&\text2^n>x≥0\\ (2^{n+1}-1)+x&\text0>x≥-2^n \end{cases}
x={x(2n+1−1)+x2n>x≥00>x≥−2n
③例子:
x=-13,计算x的二进制原码和反码
原码:x=1,1101
反码:x = (2^(4+1)−1) − 13 = 011111 − 1101 = 10010
④规律:
- 负数的反码等于原码除符号位外按位取反
- 负数的补码等于反码+1
例子:
x=-7,计算x的二进制原码和反码和补码
原码:x=1,0111 反码:x=1,1000 补码:x=1,1001
x=-9,计算x的二进制原码和反码和补码
原码:x=1,1001 反码:x=1,0110 补码:x=1,0111
补码能避免0的歧义原因:
如果把0看成正数
原码:00000000
反码:01111111
补码:00000000
看成负数
原码:10000000
反码:11111111
补码:00000000
正数0和负数0的补码表示都一样
3.5 小数的补码
定义:
x
=
{
x
1
>
x
≥
0
2
+
x
0
>
x
≥
−
1
(
m
o
d
2
)
x= \begin{cases} x&\text1>x≥0\\ 2+x&\text0>x≥-1 (mod\,\,2) \end{cases}
x={x2+x1>x≥00>x≥−1(mod2)
规律和整数一样,负数都是原码取反加一,正数和原码一样
3.6 定点数与浮点数
1)定点数的表示方法
- 小数点固定在某个位置的数称之为定点数
- 当不是纯小数和纯整数时,需要乘以比例因子以满足定点数保存格式(10.01 ,101.1)
数值 | 符号位 | 数值位 |
---|---|---|
0.1011 | 0 | 1011 |
-0.1011 | 1 | 1011 |
1011 | 0 | 1011 |
-1011 | 1 | 1011 |
2)浮点数的表示方法
- 计算机处理的很大程度上不是纯小数或纯整数
- 数据范围很大,定点数难以表达
①浮点数的表示格式
- 其中阶码和位数都用二进制表示
例子:
11.0101
=
0.110101
∗
2
10
11.0101
=
0.0110101
∗
2
11
11.0101 = 0.110101 * 2^{10}\\ \,\,\,11.0101 = 0.0110101 * 2^{11}
11.0101=0.110101∗21011.0101=0.0110101∗211
阶码符号位 | 阶码数值位 | 尾数符号位 | 尾数数值位(8位) |
---|---|---|---|
0 | 10 | 0 | 11010100 |
0 | 11 | 0 | 01101010 |
②浮点数的表示范围
- 阶码范围
- 尾数范围(一个正数范围,一个负数范围)
- 上溢和下溢
- 单精度浮点数:使用4字节、32位来表达浮点数(float)
- 双精度浮点数: 使用8字节、64位来表达浮点数(double)
③浮点数的规格化
- 尾数规定使用纯小数
- 尾数最高位必须是1
正确规格化:
11.0101
=
0.110101
∗
2
10
11.0101 = 0.110101 * 2^{10}
11.0101=0.110101∗210
错误规格化:
11.0101
=
0.0110101
∗
2
11
11.0101
=
0.00110101
∗
2
100
11.0101
=
1.10101
∗
2
1
\quad\,\, 11.0101 = 0.0110101 * 2^{11}\\ \qquad\, 11.0101 = 0.00110101 * 2^{100}\\ 11.0101 = 1.10101 * 2^{1}\\
11.0101=0.0110101∗21111.0101=0.00110101∗210011.0101=1.10101∗21
例子1:设浮点数字长为16位,阶码为5位,尾数为11位,将十进制数 13/128表示 为二进制浮点数。
例子2:设浮点数字长为16位,阶码为5位,尾数为11位,将十进制数−54表 示为二进制浮点数。
3)定点数与浮点数的对比
- 当定点数与浮点数位数相同时,浮点数表示的范围更大
- 当浮点数尾数为规格化数时,浮点数的精度更高
- 浮点数运算包含阶码和尾数,浮点数的运算更为复杂
- 浮点数在数的表示范围、精度、溢出处理、编程等方面均优于定点数
- 浮点数在数的运算规则、运算速度、硬件成本方面不如定点数
3.7 定点数的加减法运算
1)定点数加法
-
整数加法
A [ 补 ] + B [ 补 ] = [ A + B ] [ 补 ] ( m o d 2 n + 1 ) A[补] + B[补] = [A+B][补](mod2^{n+1}) A[补]+B[补]=[A+B][补](mod2n+1) -
小数加法
A [ 补 ] + B [ 补 ] = [ A + B ] [ 补 ] ( m o d 2 ) A[补] + B[补] = [A+B][补](mod2) A[补]+B[补]=[A+B][补](mod2)
- 数值位与符号位一同运算,并将符号位产生的进位自然丢掉
例子:
1.A=-110010, B=001101,求A+B
逗号前面的数字为符号位
A[补] = 1,001110
B 补 = B[原] = 0,001101
A[补] + B[补] = (A + B) [补] =1,011011
将上面的结果除了符号位取反再加一等于结果
A + B = −100101
2.A=-0.1010010, B=0.0110100,求A+B
A[补] = 1,1.0101110
B[补] = B[原] = 0,0.0110100 1,1.0101110
+ 0,0.0110100
1,1.1100010
A[补] + B[补] = (A + B)[补] =1,1.1100010
A + B =-0.0011110
4.A=-10010000, B=-11010000,求A+B
A[补] = 1, 01110000
B[补] = 1, 00110000
A[补] + B[补] = 0,10100000
1,01110000
+ 1,00110000
------------
1 0,10100000
其中进位1(模2^(n+1))舍去,由于符号位为0,所以不用取反加一
A + B = 10100000 = 160
但是A = −144 B = −208,两个负数为什么加起来等于正数呢,原来是发生了溢出
2)判断溢出的方法
双符号位判断法
- 单符号位表示变成双符号位:0=>00,1=>11
- 双符号位产生的进位丢弃
- 结果的双符号位不同则表示溢出
拿刚才的例子演示:
先将两个补码的单符号位变成双符号位再相加,符号位变成了110,多余的符号位进位舍去,符号位变成了10,两位数不一样,所以发生了溢出
3)定点数减法
- 整数减法
A [ 补 ] − B [ 补 ] = A + ( − B ) [ 补 ] ( m o d 2 n + 1 ) A[补] - B[补] = A + (-B)[补](mod2^{n+1}) A[补]−B[补]=A+(−B)[补](mod2n+1)
- 小数减法
A [ 补 ] − B [ 补 ] = A + ( − B ) [ 补 ] ( m o d 2 ) A[补] - B[补] = A + (-B)[补](mod2) A[补]−B[补]=A+(−B)[补](mod2)
- 其中-B[补]等于B[补]连同符号位按位取反,末位加一,比如B[补] = 1,0010101 (−B) [补] = 0,1101011
例子5:A=11001000, B=-00110100,求A-B
A[补] = A[原] = 0,11001000
B[补] = 1,11001100
(−B)[补] = 0,00110100
00,11001000
+ 00,00110100
-------------
00,11111100
A + (−B)[补] = 0,11111100
A − B = 111111100
3.8 浮点数的加减法运算
**步骤:**对阶–>尾数求和–>尾数规格化–>舍入–>溢出判断
1)对阶
- 浮点数尾数运算简单
- 浮点数位数实际小数位与阶码有关
- 阶码按小阶看齐大阶的原则
- 対阶的目的是使得两个浮点数阶码一致,使得尾数可以进行运算
2)尾数求和
- 使用补码进行运算
- 减法运算转化为加法运算:A - B = A + (-B)
3)尾数规格化
-
对补码进行规格化需要判断两种情况:S>0和S<0
-
S[补] = 00.1xxxxxx(𝑆 > 0)
-
S[补] = 11.0xxxxxx(𝑆 < 0)
-
符号位与最高位不一致,如果不满足此格式,需要进行左移,同时阶码相应变化,以满足规格化
尾数规格化(右移)
-
一般情况下都是左移
-
双符号位不一致下需要右移(相当于定点运算的溢出情况)
-
右移的话则需要进行舍入操作
4)舍入
- “0舍1入”法(二进制的四舍五入),即如果右移后最后一位为1的话,得将移位后的数据加一,如果为0就不需要加一
如下两个例子:
5)溢出判断
- 定点运算双符号位不一致为溢出
- 浮点运算尾数双符号位不一致不算溢出,因为尾数双符号位可以进行右规
- 浮点运算主要通过阶码的双符号位判断是否溢出,如果规格化后,阶码双符号位不一致,则认为是溢出
例子:
- 第一步:对阶
- 第二步:尾数求和
- 第三步:规格化
- 第四步:舍入
- 第五步:溢出判断
6)总结
3.9 浮点数的乘除法运算
- 乘法:阶码相加,尾数求积
x = S x ∗ r j x y = S y ∗ r j y x ∗ y = ( S x ∗ S y ) ∗ r j x + j y x = S_x * r^{j_x}\\ y = S_y * r^{j_y}\\ x * y = (S_x * S_y) * r^{j_x+j_y} x=Sx∗rjxy=Sy∗rjyx∗y=(Sx∗Sy)∗rjx+jy
- 除法:阶码相减,尾数求商
x = S x ∗ r j x y = S y ∗ r j y x / y = ( S x / S y ) ∗ r j x − j y x = S_x * r^{j_x}\\ y = S_y * r^{j_y}\\ x / y = (S_x / S_y) * r^{j_x-j_y} x=Sx∗rjxy=Sy∗rjyx/y=(Sx/Sy)∗rjx−jy
步骤:
阶码运算 --> 尾数运算 --> 尾数规格化 --> 舍入 --> 溢出判断
3.10 IEEE754标准详解
1)IEEE754标准简介
电气与电子工程师协会(Institute of Electrical and Electronics Engineers),简称lEEE,总部位于美国纽约,是一个国际性的电子技术与信息科学工程师的协会,也是全球最大的非营利性专业技术学会。
- IEEE754:IEEE二进制浮点数算术标准,是20世纪80年代以来最广泛使用的浮点数运算标准
- 为许多CPU与浮点运算器所采用。这个标准定义了表示浮点数的格式(包括负零-0)与反常值(denormal number),一些特殊数值((无穷(lnf)与非数值(NaN)),以及这些数值的“浮点数运算符”。
- 它也指明了四种数值舍入规则和五种例外状况(包括例外发生的时机与处理方式)
2)IEEE754数据表示方式
-
定点数和浮点数,通过小数点是否在固定的位置来判断
-
浮点数在数的表示范围、精度、溢出处理、编程等方面均优于定点数
-
浮点数在数的运算规则、运算速度、硬件成本方面不如定点数
-
单精度浮点数:使用4字节、32位来表达浮点数(float)
-
双精度浮点数:使用8字节、64位来表达浮点数(double)
偏正值:
偏正值为实际的指数大小与一个固定值(32位的情况是127)的和单精度的指数部分是-126+127加上偏移值127,指数值的大小从1254(0和255是特殊值)
- 阶码的值可能为正也可能为负,如果采用补码表示的话,全体符号位S和Exp自身的符号位将导致不能简单的进行大小比较
- 阶码部分>0: 补码=原码
非归约数:
如果浮点数的指数部分的编码值是0,分数部分非零,那么这个浮点数将被称为非规约形式的浮点数。一般是某个数字相当接近零时才会使用非规约型式来表示。
3)IEEE754数据表示方式
四、原理实践
本章将实现双向链表的代码,并在此基础上实现先进先出算法、最近最少使用算法和最不经常使用算法。
4.1 双向链表的原理与实践
- 可以快速找到一个节点的下一个节点
- 可以快速找到一个节点的上一个节点
- 可以快速去掉链表中的某一个节点
节点类
/**
* 节点类
*
* @param <T>
*/
public class Node<T> {
// 键
private T key;
// 值
private T value;
// 前驱节点
private Node prev;
// 后继节点
private Node next;
public Node(T key, T value){
this.key = key;
this.value = value;
this.prev = null;
this.next = null;
}
public String toString(){
return String.format("{key: %s, value: %s}", this.key, this.value);
}
public T getKey() {
return key;
}
public void setKey(T key) {
this.key = key;
}
public T getValue() {
return value;
}
public void setValue(T value) {
this.value = value;
}
public Node getPrev() {
return prev;
}
public void setPrev(Node prev) {
this.prev = prev;
}
public Node getNext() {
return next;
}
public void setNext(Node next) {
this.next = next;
}
}
双向链表类
/**
* 双向链表类
*/
public class DoubleLinkList {
// 链表容量
private long capacity;
// 头节点
private Node head;
// 尾节点
private Node tail;
// 节点个数
private long size;
public DoubleLinkList(){
this.capacity = Long.MAX_VALUE;
this.size = 0;
}
public DoubleLinkList(long capacity){
this.capacity = capacity;
this.size = 0;
}
/**
* # 从头部添加
* def __add_head(self, node):
* if not self.head:
* self.head = node
* self.tail = node
* self.head.next = None
* self.head.prev = None
* else:
* node.next = self.head
* self.head.prev = node
* self.head = node
* self.head.prev = None
* self.size += 1
* return node
*/
private Node addHead(Node node){
// 如果头部为空,头节点和尾节点取当前节点
if (this.head == null){
node.setPrev(null);
node.setNext(null);
this.head = node;
this.tail = node;
} else {
// 头节点不空,当前节点的后继节点指向头节点,头节点的前驱节点指向当前节点,将头节点移到当前节点,并置空前驱节点
node.setNext(this.head);
this.head.setPrev(node);
this.head = node;
this.head.setPrev(null);
}
this.size += 1;
return node;
}
/**
* # 从尾部添加
* def __add_tail(self, node):
* if not self.tail:
* self.tail = node
* self.head = node
* self.tail.next = None
* self.tail.prev = None
* else:
* self.tail.next = node
* node.prev = self.tail
* self.tail = node
* self.tail.next = None
* self.size += 1
* return node
*/
private Node addTail(Node node){
// 尾节点为空,头尾节点取当前节点
if(this.tail == null){
node.setPrev(null);
node.setNext(null);
this.head = node;
this.tail = node;
} else {
// 尾节点不空,尾节点的后继节点指向当前节点,当前节点的前驱节点指向尾节点,将尾节点移到当前节点,并置空后继节点
this.tail.setNext(node);
node.setPrev(this.tail);
this.tail = node;
this.tail.setNext(null);
}
this.size += 1;
return node;
}
/**
* # 从头部删除
* def __del_head(self):
* if not self.head:
* return
* node = self.head
* if self.head.next:
* self.head.next.prev = None
* self.head = self.head.next
* else:
* self.head = self.tail = None
* self.size -= 1
* return node
*/
private Node delHead(){
if (this.head == null){
return null;
}
Node node = this.head;
// 头节点的后继节点不空,将后继节点的前驱节点置空,并将头节点移到后继节点
if (this.head.getNext() != null){
this.head.getNext().setPrev(null);
this.head = this.head.getNext();
} else {
this.head = null;
this.tail = null;
}
this.size -= 1;
return node;
}
/**
* # 从尾部删除
* def __del_tail(self):
* if not self.tail:
* return
* node = self.tail
* if node.prev:
* self.tail = node.prev
* self.tail.next = None
* else:
* self.tail = self.head = None
* self.size -= 1
* return node
*/
private Node delTail(){
if (this.tail == null){
return null;
}
Node node = this.tail;
// 尾节点的前驱节点不空,将尾节点移到前驱节点,将其后继节点置空,
if (node.getPrev() != null){
this.tail = node.getPrev();
this.tail.setNext(null);
} else {
this.tail = null;
this.head = null;
}
this.size -= 1;
return node;
}
/**
* # 任意节点删除
* def __remove(self, node):
* # 如果node=None, 默认删除尾部节点
* if not node:
* node = self.tail
* if node == self.tail:
* self.__del_tail()
* elif node == self.head:
* self.__del_head()
* else:
* node.prev.next = node.next
* node.next.prev = node.prev
* self.size -= 1
* return node
*/
private Node _remove(Node node) {
if (node == null){
node = this.tail;
}
if (node == this.head){
return this.delHead();
}
if (node == this.tail){
return this.delTail();
}
node.getPrev().setNext(node.getNext());
node.getNext().setPrev(node.getPrev());
this.size -= 1;
return node;
}
// 弹出头节点
public Node pop(){
return this.delHead();
}
// 从尾节点加入节点
public Node append(Node node){
return this.addTail(node);
}
// 从头节点加入节点
public Node appendFront(Node node){
return this.addHead(node);
}
// 删除尾节点
public Node remove(){
return this._remove(null);
}
// 删除指定节点
public Node remove(Node node){
return this._remove(node);
}
public String toString(){
Node node = this.head;
StringBuffer buffer = new StringBuffer();
long tmp = 6;
while(node != null && tmp > 0){
buffer.append(node.toString());
node = node.getNext();
if(node != null){
buffer.append("->");
}
tmp -= 1;
}
return buffer.toString();
}
public long getCapacity() {
return capacity;
}
public void setCapacity(long capacity) {
this.capacity = capacity;
}
public Node getHead() {
return head;
}
测试类
public class Test {
private static void testDoubleLinkList(){
DoubleLinkList list = new DoubleLinkList();
int size = 10;
List<Node<Integer>> nodeList = new ArrayList<>();
for(int i = 0; i < size; i ++){
Node<Integer> node = new Node<>(i, i*i);
nodeList.add(node);
}
list.append(nodeList.get(0));
System.out.println(list.toString());
list.append(nodeList.get(1));
System.out.println(list.toString());
list.append(nodeList.get(2));
System.out.println(list.toString());
list.pop();
System.out.println(list.toString());
list.appendFront(nodeList.get(3));
System.out.println(list.toString());
list.remove();
System.out.println(list.toString());
list.remove(nodeList.get(1));
System.out.println(list.toString());
}
// private static void testFIFOCache(){
// FIFOCache<Integer> fifoCache = new FIFOCache<>(2);
// fifoCache.put(1, 1);
// fifoCache.put(2, 2);
// fifoCache.put(3, 3);
// fifoCache.put(4, 4);
// System.out.println(fifoCache.get(1));
// System.out.println(fifoCache.toString());
// fifoCache.put(5, 5);
// System.out.println(fifoCache.get(1));
// System.out.println(fifoCache.toString());
// fifoCache.put(2, 20);
// System.out.println(fifoCache.toString());
// }
//
// private static void testLRUCache(){
// LRUCache<Integer> lruCache = new LRUCache<>(4);
// lruCache.put(1, 1);
// lruCache.put(2, 2);
// lruCache.put(3, 3);
// lruCache.put(4, 4);
// System.out.println(lruCache.toString());
// System.out.println(lruCache.get(1));
// System.out.println(lruCache.toString());
// lruCache.put(5, 5);
// System.out.println(lruCache.toString());
// System.out.println(lruCache.get(1));
// System.out.println(lruCache.toString());
// lruCache.put(2, 20);
// System.out.println(lruCache.toString());
// }
//
// private static void testLFUCache(){
// LFUCache<Integer> lfuCache = new LFUCache<>(4);
// lfuCache.put(1, 1);
// lfuCache.put(2, 2);
// lfuCache.put(3, 3);
// lfuCache.put(4, 4);
// System.out.println(lfuCache.toString());
// System.out.println(lfuCache.get(1));
// System.out.println(lfuCache.toString());
// lfuCache.put(5, 5);
// System.out.println(lfuCache.toString());
// System.out.println(lfuCache.get(1));
// System.out.println(lfuCache.toString());
// lfuCache.put(2, 20);
// System.out.println(lfuCache.toString());
// lfuCache.get(2);
// System.out.println(lfuCache.toString());
// lfuCache.get(2);
// System.out.println(lfuCache.toString());
// }
public static void main(String []args){
testDoubleLinkList();
// testFIFOCache();
// testLRUCache();
// testLFUCache();
}
}
为了后续算法的实现,需提供一个基类(缓存类)
public abstract class BaseCache<T> {
protected Map<T, Node> map;
protected DoubleLinkList list;
public BaseCache(){
this.map = new HashMap<>();
this.list = new DoubleLinkList();
}
public BaseCache(long capacity){
this.map = new HashMap<>();
this.list = new DoubleLinkList(capacity);
}
abstract public T get(T key);
abstract public void put(T key, T value);
public String toString(){
return this.list.toString();
}
}
4.2 先进先出算法(FIFO)
- 把高速缓存看做是一个先进先出的队列
- 优先替换最先进入队列的字块
实现类
/**
* 先进先出算法
*
* @param <T>
*/
public class FIFOCache<T> extends BaseCache<T> {
public FIFOCache(long capacity){
super(capacity);
}
/**
* def get(self, key):
* """
* :type key: int
* :rtype: int
* """
* if key not in self.map:
* return -1
* else:
* node = self.map.get(key)
* return node.value
*/
public T get(T key){
if (! this.map.containsKey(key)){
return null;
}
Node node = this.map.get(key);
T value = (T)node.getValue();
return value;
}
/**
* def put(self, key, value):
* """
* :type key: int
* :type value: int
* :rtype: None
* """
* if self.capacity == 0:
* return
*
* if key in self.map:
* node = self.map.get(key)
* node.value = value
* self.list.remove(node)
* self.list.append(node)
* else:
* if self.size == self.capacity:
* node = self.list.pop()
* del self.map[node.key]
* self.size -= 1
*
* node = Node(key, value)
* self.list.append(node)
* self.map[key] = node
* self.size += 1
*/
public void put(T key, T value){
if (this.list.getCapacity() == 0){
return;
}
// 说明当前节点在链表中,先删除该节点,再重新插入尾部
if (this.map.containsKey(key)){
Node node = this.map.get(key);
this.list.remove(node);
node.setValue(value);
System.out.println("========"+this.list.toString());
this.list.append(node);
System.out.println("========"+this.list.toString());
} else {
if (this.list.getSize() == this.list.getCapacity()){
Node node = this.list.pop();
this.map.remove(node.getKey());
}
Node node = new Node(key, value);
this.list.append(node);
this.map.put(key, node);
}
}
}
4.3 最近最少使用算法 (LRU)
-
优先淘汰一段时间内没有使用的字块
-
优先淘汰一段时间内没有使用的字块
-
把当前访问节点置于链表前面(保证链表头部节点是最近使用的)
实现类
/**
* 最近最少使用算法
*
* @param <T>
*/
public class LRUCache<T> extends BaseCache<T> {
public LRUCache(long capacity){
super(capacity);
}
/**
* def get(self, key):
* """
* :type key: int
* :rtype: int
* """
* if key in self.map.keys():
* node = self.map[key]
* self.list.remove(node)
* self.list.append_front(node)
* return node.value
* else:
* return -1
*/
public T get(T key){
if(this.map.containsKey(key)){
Node node = this.map.get(key);
this.list.remove(node);
this.list.appendFront(node);
return (T)node.getValue();
} else {
return null;
}
}
/**
* def put(self, key, value):
* """
* :type key: int
* :type value: int
* :rtype: None
* """
* if key in self.map.keys():
* old_node = self.map[key]
* self.list.remove(old_node)
* new_node = old_node
* new_node.value = value
* self.list.append_front(new_node)
* else:
* new_node = Node(key, value)
* if self.list.size >= self.list.capacity:
* node = self.list.remove()
* self.map.pop(node.key)
*
* node = self.list.append_front(new_node)
* self.map[key] = node
*/
public void put(T key, T value){
if (this.map.containsKey(key)){
Node oldNode = this.map.get(key);
this.list.remove(oldNode);
Node newNode = oldNode;
newNode.setValue(value);
this.list.appendFront(newNode);
} else {
Node newNode = new Node(key, value);
if (this.list.getSize() >= this.list.getCapacity()){
Node node = this.list.remove();
this.map.remove(node.getKey());
}
Node node = this.list.appendFront(newNode);
this.map.put(key, node);
}
}
}
4.4 最不经常使用算法(LFU)
- 优先淘汰最不经常使用的字块
- 需要额外的空间记录字块的使用频率
当有相同频率的节点时该怎么淘汰?
使用一个map缓存对应频率的双向链表,当链表容量满时,淘汰最低频率的双向链表的头节点,即可解决上述问题
实现类
/**
* 最不经常使用算法
*
* @param <T>
*/
public class LFUCache<T> extends BaseCache<T> {
class LFUNode<T> extends Node<T> {
// 使用频率
private int freq;
public LFUNode(T key, T value) {
super(key, value);
this.freq = 0;
}
}
// 频率链表映射关系
private Map<Integer, DoubleLinkList> freqMap;
// 节点个数
private int size;
// 容量
private long capacity;
public LFUCache(long capacity){
super();
this.size = 0;
this.capacity = capacity;
this.freqMap = new HashMap<>();
}
/**
* 更新节点使用频率
* def update_freq(self, node):
* freq = node.freq
* node = self.freq_map[freq].remove(node)
* if self.freq_map[freq].size == 0:
* del self.freq_map[freq]
* freq += 1
* node.freq = freq
* if freq not in self.freq_map:
* self.freq_map[freq] = DoubleLinkedList()
* self.freq_map[freq].append(node)
*/
private void updateFreq(LFUNode node){
int freq = node.freq;
// 原频率链表删除当前节点
node = (LFUNode)this.freqMap.get(freq).remove(node);
if(this.freqMap.get(freq).getSize() == 0){
this.freqMap.remove(freq);
}
freq += 1;
node.freq = freq;
if (! this.freqMap.containsKey(freq)){
this.freqMap.put(freq, new DoubleLinkList());
}
// 往下一个频率链表尾部增加节点
this.freqMap.get(freq).append(node);
}
/**
* 获取节点值
* def get(self, key):
* """
* :type key: int
* :rtype: int
* """
* if key not in self.map:
* return -1
* else:
* node = self.map.get(key)
* self.update_freq(node)
* return node.value
*/
public T get(T key){
if(! this.map.containsKey(key)){
return null;
}
LFUNode node = (LFUNode)this.map.get(key);
this.updateFreq(node);
return (T)node.getValue();
}
/**
* 插入节点
* def put(self, key, value):
* """
* :type key: int
* :type value: int
* :rtype: None
* """
* if self.capacity == 0:
* return
*
* if key in self.map:
* node = self.map.get(key)
* node.value = value
* self.update_freq(node)
* else:
* if self.size == self.capacity:
* min_freq = min(self.freq_map)
* node = self.freq_map[min_freq].pop()
* del self.map[node.key]
* self.size -= 1
* node = LFUNode(key, value)
* node.freq = 1
* self.map[key] = node
* if 1 not in self.freq_map:
* self.freq_map[1] = DoubleLinkedList()
* node = self.freq_map[1].append(node)
* self.size += 1
*/
public void put(T key, T value){
if (this.capacity == 0){
return;
}
if (this.map.containsKey(key)){
LFUNode node = (LFUNode)this.map.get(key);
node.setValue(value);
this.updateFreq(node);
} else {
if (this.size == this.capacity){
Set<Integer> set = this.freqMap.keySet();
Object[] obj = set.toArray();
Arrays.sort(obj);
int minFreq = (Integer)obj[0];
Node node = this.freqMap.get(minFreq).pop();
this.map.remove(node.getKey());
this.size -= 1;
}
LFUNode node = new LFUNode(key, value);
node.freq = 1;
this.map.put(key, node);
if (! this.freqMap.containsKey(node.freq)){
this.freqMap.put(node.freq, new DoubleLinkList());
}
this.freqMap.get(node.freq).append(node);
this.size += 1;
}
}
public String toString(){
StringBuffer buffer = new StringBuffer();
buffer.append("**********************\r\n");
for(int key: this.freqMap.keySet()){
buffer.append(String.format("%d: %s", key, this.freqMap.get(key)));
buffer.append("\r\n");
}
return buffer.toString().trim();
}
}