[{"content":" BE BEST 思维导图 数据结构与算法总览 文章列表 如何学习算法 数据结构 线性表 - 数组、链表 线性表 - 栈、队列 树 — 二叉树、递归树 树 — 红黑树、堆 树 — B+树 图 图的搜索 基于图的其它算法 字符串匹配 字符串相似度 常见类型 排序 O(n^2)：冒泡排序、插入排序、选择排序 O(nlogn)：归并排序、快速排序、堆排序 O(n)：桶排序、计数排序、基数排序 排序算法对比 查找 过滤 相似 基本算法思想 常见应用 索引底层的数据结构 Redis 常用数据类型对应的数据结构 搜索引擎 Disruptor 内存消息队列 鉴权与限流 短网址服务 打赏 请我喝杯奶茶吧～\n","date":"2023-08-24T13:28:17+08:00","permalink":"https://boxtsecond.github.io/en/2023/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E4%B8%8E%E7%AE%97%E6%B3%95%E6%80%BB%E8%A7%88/","title":"数据结构与算法总览"},{"content":"本文概览 VLAN Virtual Local Area Network，即虚拟局域网，用于将局域网的物理拓扑与逻辑拓扑解耦。将一个物理的局域网在逻辑上划分成多个广播域，VLAN 内的网络设备可以直接通信，跨 VLAN 不能直接通信，必须经由路由器等路由设备,从而将广播报文限制在单个广播域内。\nVLAN 出现的背景 随着以太网的迅猛发展，网络接入设备呈指数级增长。而以太网在节点间传输数据基于共享物理介质，局域网中的网络设备越多，冲突的概率就会越高，性能会显著下降。其次，广播帧传递的范围太大，导致局域网的带宽资源、网络设备处理广播帧的 CPU 资源等资源的无谓消耗，影响整个局域网的性能。除此之外，在企业管理中，随着人员的变动，添加新设备时常常需要重新布线。而且通常会将不同部门划分不同的网络，以提高安全性。 虽然路由器也可以分割广播域，但由于路由器设备的价格较贵，而且分割的局域网个数受限于路由器的端口，而路由器本身的端口数量较少。在这种环境下，用于在交换机（特指二层交换机）上分割局域网的 VLAN 技术应运而生。\n总结一下局域网物理拓扑的缺点：\n网络设备越多，冲突概率越高，导致性能下降 广播风暴消耗资源，影响局域网的整体性能 扩展性低，无法灵活的分割网络 VLAN 的作用 VLAN 的出现解决了局域网物理拓扑带来的限制，其作用说白了只有一点，按需分割广播域。将一个物理网络划分成多个逻辑网络，可以理解为将一台真实的交换机在逻辑上分为了多个交换机。\nVLAN 数据帧格式 重新复习一遍传统以太网帧的格式的解析 传统以太网帧 IEEE 802.1Q 标准协议1的 VLAN 数据帧格式，是在传统以太网帧的目的 MAC 地址和源 MAC 地址字段之后，协议类型字段之前加入 4 个字节的 VLAN 标签（VLAN Tag 简称 Tag），用于标识数据帧所属的 VLAN。下图中的红色部分表示 VLAN 标签。\nVLAN 帧 以下仅对 VLAN 标签作出解释，其他字段的含义可查看传统以太网帧的格式\nTPID：Tag protocol identifier 标签协议标识符，表示数据帧类型，0x8100 表示 IEEE 802.1Q 帧，长度为 2 个字节 TCI：Tag control information，包含了数据帧控制信息，同样由 2 个字节组成 PCP（PRI）：Priority code point，表示数据帧的优先级。之前的名称为 PRI，具体含义没变。PCP 占 3 bits，所以取值范围为 0 ～ 8，值越大表示优先级越高。如果发生拥塞，交换机首先发送优先级最高的数据包 DEI（CFI）：Drop eligible indicator 丢弃资格标识符，表示 MAC 地址封装是否符合规范格式，之前的名称为 CFI。DEI 占 1 bit，0 表示 MAC 地址以标准格式进行封装 VID：VLAN identifier，表示帧所属的 VLAN。一般情况下在 TCI 中，只关心 VID。VID 占 12 bits，取值范围为 0 ～ 4095，总计可以标识 4096 个 VLAN 在一个 VLAN 网络中，数据帧由两种类型，有标记帧（tagged frame）和无标记帧（untagged frame）。交换设备的端口按照处理数据帧的能力，分为 Access 接口、Trunk 接口和 Hybrid 接口。每个接口都可以配置一个默认的 VLAN ID，用于为 untagged frame 添加 VLAN Tag。称为缺省 VLAN ID，即 PVID（Port Default VLAN ID）。\n接口类型 作用 可处理的数据帧 Access 连接终端设备，如计算机或 IP 电话 单个 VLAN 的数据帧，即 untagged frame。 Trunk 连接交换机之间或交换机与路由器之间 多个 VLAN 的数据帧，通过 VLAN 标签区分。即可同时收发 tagged、untagged frame。 untagged frame 仅存在于发送数据帧时剥离 VLAN 标签，转发给特定 VLAN Hybrid 结合 Access 和 Trunk 的功能，可连接终端设备，同时处理多个 VLAN 单个或多个 VLAN 的数据帧，根据配置确定 VLAN 的优缺点 优点 按需分割广播域 减少广播流量 提高安全性 简化管理成本 缺点 VLAN ID 仅有 4096 个，在大型网络中不够用 虽然可以从逻辑上划分成多个网络，但真实的数据帧无法脱离物理网络，即全部是 underlay 网络，会带来以下问题 交换机需要学习转发多个 VLAN 的数据帧，而交换机的内存有限且比较宝贵。当交换机 MAC 地址表不足以支持存储网络中的所有设备时，未存储 MAC 地址的设备的数据帧的转发，需要泛洪 不同的 VLAN ID 通常会分配不同的 IP 地址段，当业务集中在某个 IP 段内时，会导致网络资源分配不均 若局域网内使用 STP/RSTP 协议避免环路，则整个网络中的多个 VLAN 仅构造一颗树 VLAN 通信过程解析 为了更好的理解 VLAN 的实际通信过程，我们将分别从 VLAN 内通信、跨 VLAN 通信两个方面来详细描述及实践，其中又分别包括了同设备和跨设备两种类型。\nVLAN 内通信 同设备 同设备同 VLAN 同设备同 VLAN 内通信，交换机与设备的连接如上图所示。设备 A 想要与设备 B 通信。\n同设备 VLAN 内抓包实践 gns3 网络拓扑及配置\nGNS3 按照图中所示分别配置 A、B 的 IP 地址为 10.1.1.10、10.1.1.20\n配置 A 配置 B A：ip 10.1.1.10/24 10.1.1.1 B：ip 10.1.1.20/24 10.1.1.1 跨设备 跨设备 VLAN 内抓包实践 跨 VLAN 通信 同设备 同设备跨 VLAN 抓包实践 跨设备 跨设备跨 VLAN 抓包实践 VLAN 的应用 除了 IEEE 802.1Q 还有 ISL，两者都可在数据帧中附加 VLAN 信息。唯一的区别是 ISL 是 Cisco 产品独有的，仅能在 Cisco 网络设备间使用。\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n","date":"2023-11-07T18:25:28+08:00","permalink":"https://boxtsecond.github.io/en/2023/%E5%BF%AB%E9%80%9F%E7%90%86%E8%A7%A3%E7%BD%91%E7%BB%9C%E5%8D%8F%E8%AE%AE%E5%85%ADvlan/","title":"快速理解网络协议（六）VLAN"},{"content":"本文概览 本文概览 为了更好地理解物理链路环路对网络的影响，需要大致了解以太网交换机自学习和转发帧的过程。后文中的“交换机”均表示以太网二层交换机。\n交换机自学习和转发帧的过程 交换机 Switch 是一种网络设备，用于在局域网内连接多个网络设备，接收其发送的数据帧。基于目标 MAC 地址决定转发或丢弃数据帧，以及数据帧从哪个端口转发出去。交换机有多个端口，每个端口都被隔离成自己的碰撞域，即连接某个端口的网络设备独占整个带宽。\n交换机在网络中肩负着至关重要的角色，它以一种即插即用的方式，实现了网络设备之间的高效数据传输。在交换机刚启动时，其内部的帧交换表是空的。随着网络中各设备之间的通信，交换机通过自学习算法逐渐建立起帧交换表。\n交换机接收某个端口的数据帧，通过数据帧的源 MAC 地址，就可以知道哪些设备连接此端口，记录下来用于转发数据帧时，根据目标 MAC 地址选择对应的端口 交换机根据目标 MAC 地址决定丢弃数据帧、转发数据帧及转发端口 如果不知道目标 MAC 地址对应的转发端口，则将数据帧转发给除接收端口外的全部端口 如果明确知道目标 MAC 地址对应的转发端口，则仅转发数据帧至对应端口 如果目标 MAC 地址和源端口一致，则丢弃此数据帧 下面我们来举例说明交换机的自学习和帧转发过程，网络拓扑如下图所示。假设各设备知道网络中其它设备的 MAC 地址（无需进行 ARP）。\n网络拓扑 交换机 1 的端口 1、2、3 分别连接了网络设备 A、B、C，交换机 2 的端口 4 连接网络设备 E。\n交换机 1 的端口 4 和交换机 2 的端口 2 相连。\n自学习过程 自学习 设备 A 发送数据帧至设备 C，交换机 1 的端口 1 接收到此数据帧 交换机 1 将数据帧的源 MAC 地址和接收端口 1 记录到自己的帧交换表中，这个记录的行为就是交换机的自学习 转发帧过程 交换机 1 自学习之后，根据目标 MAC 地址和交换机的帧交换表，决定转发或丢弃数据帧。其中包括三种情况，泛洪、明确转发和丢弃。\n泛洪 泛洪 交换机 1 记录源 MAC 地址和接收端口的映射关系到帧交换表后，开始转发数据帧 交换机 1 在帧交换表中查找数据帧的目标 MAC 地址，发现没有匹配项，交换机 1 向除端口 1 以外的全部端口广播这个帧，即泛洪 设备 B、E 收到此数据帧，发现目标 MAC 地址不是自己，丢弃此数据帧 设备 C 收到此数据帧，发现目标 MAC 地址是自己，接收此数据帧 转发数据帧 转发 设备 C 发送数据帧至设备 A，交换机 1 的端口 3 接收到此数据帧 交换机 1 将数据帧的源 MAC 地址和接收端口记录到自己的帧交换表中 交换机 1 在帧交换表中查找数据帧的目标 MAC 地址，成功获取到目标 MAC 地址对应的端口 1，将数据帧仅转发至端口 1 设备 A 收到此数据帧，发现目标 MAC 地址是自己，接收此数据帧 丢弃数据帧 丢弃 设备 A 发送数据帧，设备 D 和交换机 1 都可以收到此数据帧 交换机 1 将数据帧的源 MAC 地址和接收端口记录到自己的帧交换表中 交换机 1 在帧交换表中查找数据帧的目标 MAC 地址，成功获取到目标 MAC 地址对应的端口 1，发现端口 1 和源端口一致，丢弃此数据帧 STP 协议 Spanning Tree Protocol，即 STP 协议，用于为以太网构建无环路的逻辑拓扑。\nSTP 出现的背景 单点故障 如图所示，设备 A、B、C 互相通信需要通过交换机 1、2、3。当交换机 1 出现故障时，设备 A 无法与 B、C 通信，此种现象称之为单点故障。为了解决单点故障的问题，提高网络的可靠性，需要在物理上冗余备份链路，如下图所示。交换机 1 和交换机 3 之间添加了一条冗余链路，当交换机 1 与交换机 2 间的链路出现故障时，设备 A 仍旧可以通过交换机 3 与 B、C 通信。\n冗余备份 根据交换机自学习和转发帧的过程的解析，当网络物理拓扑中存在环路时，会带来以下问题\n交换机自学习过程中，帧交换表震荡，MAC 地址表项不断被破坏和变化\nMAC 地址震荡 设备 A 发送一个广播包 交换机 1 的端口 2 收到广播包，根据数据帧的源 MAC 地址添加端口映射关系，并转发数据帧到端口 1、3 交换机 2 和交换机 3 同理，将数据帧转发给其他端口 交换机 1 的端口 3 收到此数据帧，源 MAC 地址仍旧为设备 A 的 MAC 地址，而对应端口变成了 3 数据帧在环路中不断转发，即广播风暴，消耗大量的网络资源，影响网络中的其他数据帧的转发性能，甚至无法转发\n在环路中的设备收到重复的数据帧，同样也会消耗设备资源\nSTP 协议的作用 STP 协议通过阻塞交换机端口，令其不再转发数据帧，使得环路被裁剪成一颗树，达到**在逻辑上消除环路的目的。**在链路发生故障后，激活备份链路，及时恢复网络连通性。\n消除环路：允许网络物理拓扑中存在备份链路，消除逻辑拓扑上的二层环路 备份链路：发生故障后使用备份链路，及时恢复网络，提高网络的可靠性 STP 协议过程解析 在解析 STP 协议的具体过程前，先来了解一下 STP 协议中的重要组成部分。\nSTP 协议的重要概念 根桥 Root Bridge，是整个 STP 网络的逻辑中心，一般会选择性能高的交换设备作为根桥。根桥没有根端口，根桥的全部端口都是指定端口。 根端口 Root Port，交换机上距离根桥路径最短的端口，每个交换机上最多有一个根端口，负责向根桥方向转发数据。 指定端口 Designated Port，交换机向所连网段转发配置消息的端口，每个网段只能有一个指定端口。 阻塞端口 Blocked Port，如果一个端口既不是根端口又不是指定端口，这个端口将被阻塞。被阻塞的端口仅接受 BPDU 消息，不会接受或发送其他非 BPDU 的数据帧。 BPDU Bridge Protocol Data Units，STP 生成树过程中所传递的配置消息。 STP 协议过程解析 选举根桥，根据根桥 ID 选举 根桥 ID（BID）：桥优先级（默认 32769，且必须为 4096 的倍数） + 桥 MAC 非根桥交换机选举根端口（RP），按照以下顺序选举 根路径成本最低 对端 BID（Bridge ID）最小 对端 PID（Port ID）最小 本端 PID 最小 每个网段选举指定端口（DP），根桥的所有端口都是指定端口，非根桥按照以下顺序选举 根路径成本最低 对端 BID 最小 对端 PID 最小 阻塞既不是根端口也不是指定端口的端口 STP 协议实践 gns3 网络拓扑及配置\nGNS3 按照图中所示 BridgeID 分别配置交换机的优先级\npriority 配置 进入配置模式 configure terminal 配置优先级 spanning-tree vlan 1 priority 4096 exit 查看是否配置成功 show spanning-tree 重复上述命令，配置其他交换机 按照此拓扑推理并验证\nSwitch A，优先级 4096\n推理 Switch A 在整个网络中优先级最高，为根桥，所有端口都为指定端口 验证 Switch A Switch B，优先级 20480\n推理 e0/0 与根桥的根路径成本最低，e0/0 为 RP e1/0 与 Switch E 相连比较，e1/0 为 DP 验证 Switch B Switch C，优先级 8192\n推理 同 Switch B，e1/1 为 RP，e2/0、e3/0 为 DP 验证 Switch C Switch D，优先级 40960\n推理 同 Switch B，e2/2 为 RP 验证 Switch D Switch E，优先级 8192\n推理 同 Switch B，e0/0 为 RP，e1/1 为 DP 验证 Switch E Switch F，优先级 12288\n推理 同 Switch B，e0/0 为 RP e1/1 与 Switch E 相连比较，e1/1 优先级更低，被阻塞 验证 Switch F Switch G，优先级 16384\n推理 同 Switch B，e0/0 为 RP 验证 Switch G 关闭 Switch B，查看 Switch F 的变化 关闭 Switch B 查看 Switch F Switch F 可以看到 Switch F 原先被阻塞的 e1/1 端口，由于网络拓扑的改变更新为 DP Switch A 感知到网络拓扑发生变化后，向 Switch C、D 发送 BPDU 报文，通知 Switch C、D 把 MAC 地址表项的老化时间由默认的 300 秒修改为 Forward Delay 的时间（默认为15秒） Switch C 继续向 Switch F 发送 BPDU 报文，Swith F 重新计算自己的 DP、RP 最多等待15秒之后，Switch F 开始 MAC 表项的学习及转发操作 STP 协议的缺点 由上可知，当网络中的某个设备发生故障不可用时，需要等待所有的网络设备接收和传播 BPDU，重新选举根桥、根端口、指定端口和阻塞端口，收敛速度慢。而且在网络设备端口状态的转换过程中，数据流量会被阻塞，可能会加剧延时。除此之外，STP 协议对局域网仅生成一棵树，对于所有 VLAN 都使用相同的拓扑。这意味着在多个 VLAN 环境中，可能存在不必要的阻塞端口。如果需要不同的拓扑或特定的 VLAN 配置，可能需要手动配置。\n总结一下，STP 协议的缺点有以下两条\n收敛速度慢 不支持 VLAN RSTP、MSTP 协议 针对 STP 协议缺点的改进，出现了 RSTP 和 MSTP 协议\nRSTP 协议 RSTP 是 STP 的改进版本，它的主要目标是加速 STP 的收敛速度。\nRSTP 引入了两种端口状态：指定端口（Designated Port）和替代端口（Alternate Port）。 RSTP 使用不同的 BPDU 格式和机制，以快速传播拓扑变化。 RSTP 的收敛速度较快，通常可以在数秒内响应拓扑变化，而不是 STP 的30秒或更长时间。 MSTP 协议 MSTP 是STP的另一种改进版本，旨在提供更大的灵活性，特别是在多 VLAN 环境下。\nMSTP 引入了多个实例（Instance），每个实例可以关联到一个或多个 VLAN。 MSTP 允许管理员为每个实例配置独立的树状拓扑，支持不同 VLAN，从而提高网络的灵活性。 MSTP 使用单一 BPDU 格式，但在每个实例中维护独立的树状拓扑，因此可以减少网络开销。 STP vs RSTP vs MSTP 特征 STP（Spanning Tree Protocol） RSTP（Rapid Spanning Tree Protocol） MSTP（Multiple Spanning Tree Protocol） 防止环路 是 是 是 选举根桥和根端口 是 是 是 收敛速度 慢 较快 较快 支持多树状拓扑实例 否 否 是 带宽利用 较低 较高 较高 配置复杂性 低 低 较高 适用性 单一 VLAN 环境 单一 VLAN 环境 多 VLAN 环境 设备支持 大多数网络设备支持 大多数网络设备支持 需要支持 MSTP 的设备 ","date":"2023-10-30T11:50:47+08:00","permalink":"https://boxtsecond.github.io/en/2023/%E5%BF%AB%E9%80%9F%E7%90%86%E8%A7%A3%E7%BD%91%E7%BB%9C%E5%8D%8F%E8%AE%AE%E4%BA%94stp-%E5%8D%8F%E8%AE%AE/","title":"快速理解网络协议（五）STP 协议"},{"content":"本文概览 本文概览 ARP 协议过程解析 Address Resolution Protocol 地址解析协议，工作在 MAC 层，用于获得已知 IP 地址的 MAC 地址。\n同一子网中 ARP 假设 Client A 和 Client D 在同一个子网中，Client A 要向 Client D 发送一个 IP 数据包（IP Packet）\nClient A 查看自己的 ARP 缓存表，判断是否包含 Client D 对应的 ARP 表项。如果有，则直接利用 ARP 缓存表中的 MAC 地址，对 IP 数据包进行帧封装，发送数据帧给 Client D\n若 Client A 的 ARP 缓存表中，没有包含 Client D 的信息，则 Client A 先将此数据帧缓存，以广播的形式发送 ARP 请求报文，交换机收到广播包时，会原样复制该包到除源端口外的所有端口，具体包格式如下图\nARP Request Packet dMAC 目标 MAC 地址 ff:ff:ff:ff:ff:ff ，即广播包 sMAC 源 MAC 地址，即 Client A 的 MAC 地址 type 数据帧类型为 ARP ARP 数据报文内容 1 HType（Hardware Type）硬件类型，标识链路层协议，当为 1 时表示以太网 PType（Protocol Type）协议类型，标识网络层协议，当为 0x0800 时表示 IPv4 协议 HLen（Hardware Length），硬件地址长度，以太网的长度为 6 PLen（Protocol Length），协议地址长度，IPv4 的长度为 4 Opcode 操作码，1 表示 ARP 请求（“Request”） Sender MAC、Sender IP，为 Client A 的 MAC 地址 和 IP 地址 Target MAC 为 00:00:00:00:00:00，说明此时 Client A 还不知道 Client D 的 MAC 地址 Target IP 为 Client D 的 IP 地址 Client D 比较自己的 IP 地址和 ARP 请求报文中的 Target IP 地址，若二者相同，将 ARP 请求报文中的 Sender MAC、Sender IP 地址信息存入自己的 ARP 缓存表中，以单播的形式返回 ARP 响应报文 （Client C 也会将 Client A 的 IP、MAC 地址缓存在自己的 ARP 缓存表中，但并不响应 ARP 请求）。\nARP Reply Packet dMAC 目标 MAC 地址，即 Client A 的 MAC 地址 sMAC 源 MAC 地址，即 Client D 的 MAC 地址 type 数据帧类型为 ARP ARP 数据报文内容 HType、PType、HLen、PLen 与 ARP 包请求相似 Opcode 为 “Reply”，即 ARP 响应包 Sender MAC、Sender IP，为 Client D 的 MAC 地址 和 IP 地址 Target MAC、Target IP， 为 Client A 的 MAC 地址 和 IP 地址 同一子网内 ARP 抓包实践 申请在同一个 VPC 下的 2 台云主机，内网地址分别为 10.23.139.22、10.23.197.59\n登陆到 10.23.139.22，使用命令 arp -n 查看已经存在的 arp 缓存表项。\narp -n 如果这里发现已经有了 10.23.197.59 的缓存，使用命令 arp -d 10.23.197.59 删掉缓存，删完之后再 check 下，如果没有的话就继续下一步。\narp -d 使用 tcpdump -i eth0 arp -w arp.pcap 命令开始抓包\n再开一个 tab，还是登陆到 10.23.139.22，执行命令 ping 10.23.197.59，对 10.23.197.59 发送一个 ICMP 报文\n结束抓包，将 arp.pcap 下载到本地，用 wireshark 打开 arp.pcap\nARP Request\nARP Request 可以看到 ARP 报文中的目标 MAC 地址是 00:00:00:00:00:00，而外层的目标 MAC 地址是 ff:ff:ff:ff:ff:ff，即广播 ARP Reply\nARP Reply ARP Reply 报文中，由于已经知道目标 MAC 地址，直接以单播的形式返回 跨网段 ARP 假设 Client A 和 Client D 不在同一个子网中，Client A 要向 Client D 发送一个 IP 数据包（IP Packet），还会像在同一子网内那样发送 ARP 报文吗？答案是否定的，这里要分两种情况，Client A 本身是否存在网关。由于之前已经解析过 ARP 报文中的具体字段，之后的 ARP 报文我们就只关注于字段的值而不再赘述字段含义。\n普通 ARP（跨子网有网关） Client A 和 Client D 不在同一个子网中且 Client A 存在网关\nClient A 首先发现目标 IP 是跨网段的，并且 Client A 此时有网关，那么 Client A 就会发送 ARP 报文去查找网关的 MAC 地址，而不是跨网段的目标 IP 的 MAC 地址（本地无网关 MAC 地址缓存） Client A 以广播的形式发送 ARP 请求报文 网关以单播的形式返回 ARP 响应报文 跨子网有网关的 ARP 抓包实践 gns3 网络拓扑及配置\nGNS3 路由器配置地址 192.168.1.1 和 10.10.10.1\nRouter 配置 进入配置模式 configure terminal 配置接口 int e0/0 ip add 192.168.1.1 255.255.255.0 no shut exit e1/0 与 e0/0 相同 Client D-ARP 配置地址 10.10.10.20\nClient D-ARP 配置 ip 10.10.10.20/24 10.10.10.1 Client A 配置地址 192.168.1.100 和 网关地址 192.168.1.1\nClient A 配置 注意这里的 Client A 是由路由器模拟的，为了方便后续的 proxy arp 实验 进入配置模式 configure terminal 配置接口 int e0/0 ip add 192.168.1.100 255.255.255.0 no shut exit 关闭路由功能 no ip routing 设置默认网关 ip default-gateway 192.168.1.1 退出配置模式后，检查下是否配置成功 show ip route 开始抓包\n在 Client A 上 ping 10.10.10.20 ping 10.10.10.20 ARP Request\nARP Request 仍旧是广播的形式 可以看到 Target IP 是 192.168.1.1 即网关的地址，而不是 10.10.10.20 ARP Reply\nARP Reply 网关单播回复 ARP Reply，附上自己的 MAC 地址 Proxy ARP（跨子网无网关） Client A 和 Client D 不在同一个子网中且 Client A 不存在网关\n先来看看什么是 Proxy ARP 当局域网内部主机发起跨网段的 ARP 请求，网关设备开启 ARP 代理功能且知道目标的路由信息时，会使用自身的 MAC 地址回复该请求 这里有两个非常重要的点，一个是 Proxy ARP 需要网关设备支持且开启 ARP 代理功能，这说明 Proxy ARP 依赖且受限于网关设备，因此必定会来可扩展性差和不可靠的缺点。另一个是网关设备会用自身的 MAC 地址回复 ARP 请求报文。 Proxy ARP 和普通 ARP 的请求路径类似，唯一不同的是 ARP 请求报文中的 Target IP Address 中的值是目标 IP（而不是网关 IP） 跨子网无网关的 ARP 抓包实践 gns3 网络拓扑及配置\nGNS3 路由器和上述相同\nClient D-ProxyARP 类似，配置地址换为 10.10.10.10\nClient A 这里要关闭路由功能（上述已关闭）且去掉默认网关\nClient A 配置 no ip default-gateway 开始抓包\n在 Client A 上 ping 10.10.10.10\nping 10.10.10.10 ARP Request\nARP Request 仍旧是广播的形式 可以看到 Target IP 是 10.10.10.10 即目标地址，而不是网关地址 ARP Reply\nARP Reply 仍旧是网关单播回复 ARP Reply，附上自己的 MAC 地址 ARP 缓存 ARP 动态表项 由 ARP 协议通过 ARP 报文自动生成和维护，可以被 ARP 静态表项覆盖，可以被 ARP 报文更新，可以过期\nARP 静态表项 手动配置和维护，不会被 ARP 动态表项覆盖，不会被 ARP 报文更新，不会过期\nGratuitous ARP（免费 ARP） Gratuitous ARP 是一种特殊的 ARP 请求。当主机启动时，发送一个 Gratuitous ARP 请求，即请求自己的 IP 地址和 MAC 地址，即目标 IP 地址为 自己的 IP 地址。\nGratuitous ARP 的作用\n以广播的形式发送自己的 IP 地址 和 MAC 地址，在网络中宣告自己的信息，可以是宣告新添加的主机或者更新自己的 MAC 地址 检测 IP 地址冲突，若收到了 ARP 响应报文，则说明网络中已存在使用该 IP 地址的主机 ARP 欺骗及防御 由于 ARP 请求不会验证 ARP 响应报文，若此时有主机在收到广播 ARP 请求报文后伪造 ARP 响应报文，则可以欺骗发送方，伪造 IP 地址 - MAC 地址 的映射关系 劫持通信内容 广播攻击 丢弃通信，拒绝服务 防御措施 建立正确的 ARP 映射关系、检测并过滤伪造的 ARP 报文，保证经过其转发的 ARP报文正确合法 抑制短时间内大量 ARP 报文的冲击 ARP 常见命令 查看 arp 表项 arp -a/ arp -e 删除 arp 表项 arp -d，例：arp -i eth1 -d 192.168.60.1 添加 arp 表项 arp -s，例：arp -s IP MAC-ADDRESS 更多：arp -h RARP 协议 Reverse Address Resolution Protocol 反向地址转换协议，用于获得已知 MAC 地址的 IP 地址。\n一般在主机刚接入网络时，通过本地 MAC 地址来发送 RARP 请求，如果局域网内有 RARP Server 且 Server 上存在关于此 MAC 地址的映射 IP，则会返回 RARP Reply 响应，此时主机就获取了 IP 地址。\n需要 RARP 服务器，一般用于无法使用 DHCP 或没有任何输入接口的小型嵌入式设备 主机以广播的形式发送 RARP 请求包，声明自己的 MAC 地址，并请求分配一个 IP 地址 RARP 服务器收到 RARP 请求包后，检查 RARP 列表，查找该 MAC 地址对应的 IP 地址 若存在，则返回 RARP 响应包，成功分配 IP 地址 若不存在，则不做任何响应，分配 IP 地址失败 更多参数值查看：https://www.iana.org/assignments/arp-parameters/arp-parameters.xhtml#arp-parameters-3\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n","date":"2023-09-28T10:22:17+08:00","permalink":"https://boxtsecond.github.io/en/2023/%E5%BF%AB%E9%80%9F%E7%90%86%E8%A7%A3%E7%BD%91%E7%BB%9C%E5%8D%8F%E8%AE%AE%E5%9B%9Barprarp-%E5%8D%8F%E8%AE%AE/","title":"快速理解网络协议（四）ARP、RARP 协议"},{"content":"深入浅出 LVS 负载均衡系列：\n深入浅出 LVS 负载均衡（一）NAT、FULLNAT 模型原理 两台计算机如何在互联网中通信 LVS 负载均衡 通过修改数据包的「源 IP 地址」或 「目标 IP 地址」 NAT 模式 FULLNAT 模式 深入浅出 LVS 负载均衡（二）DR、TUN 模型原理 LVS 负载均衡 通过修改数据包的「目标 MAC 地址」 DR 模式 通过二次封装数据包的「IP 报文」 TUN 模式 深入浅出 LVS 负载均衡（三）实操 NAT、TUNNEL 模型、深入浅出 LVS 负载均衡（四）实操 DR 模型、Keepalived DR 模型的高可用 使用 UCloud 云主机实操各个模型 使用 Keepalived 实现 LVS-DR 的高可用 LVS（Linux Virtual Server）是一个虚拟服务器集群系统。工作在 OSI 模型的传输层，即四层负载均衡。LVS 本身实现了 NAT、DR、TUN 模型，这些模型仅做数据包的转发，而不会与客户端建立连接，成本低效率高。FULLNAT 基于 NAT 实现，LVS 本身不支持，需要额外对内核打补丁后才能使用。\n本系列按照负载均衡器对数据包的处理方式分类，从计算机间通信的角度出发，浅谈 NAT、FULLNAT、DR、TUN 模型的实现原理。\n之前介绍了 LVS 负载均衡 NAT、FULLNAT、DR、TUN 模型的实现原理。现在来动手实践一下～\n实验环境 LVS 目前已经是 Linux 内核中的一部分，在内核中的模块叫做 ipvs，支持 NAT、DR、TUNNEL 模型。用户不能直接操作 ipvs 模块，需要安装交互软件 ipvsadm，使用 ipvsadm 和 ipvs 进行交互。\n我使用 4 台 UCloud 云主机来搭建实验环境，创建云主机的时候选择分时购买，更划算一点。\n实验机器及环境 4 台 UCloud 云主机，CentOS 7.9 64位，1 核 1 G，需要注意一下防火墙规则，我选择的是【Web服务器推荐】，开放 22、3389、80、443 的端口号，这个可以自行配置 两台 Real Server：RS01、RS02，一台负载均衡服务器：LB01 RS01：10.23.190.76、RS02：10.23.122.152、LB01：10.23.21.184、LB02：10.23.115.100 VIP：10.23.88.247 RS01、RS02 安装 httpd，快速启动 http 服务器，且配置不同的请求响应 LB01、LB02 安装 ipvsadm、keepalived，并启动 ipvsadm、keepalived 实验机器展示 实验机器 DR 模式实操 回顾一下 DR 模式的特点\nDR DR 模式仅修改数据包的「目标 MAC 地址」，只有请求数据包需要经过负载均衡器，所以 DR 模式不支持对端口的转换 真实服务器和负载均衡器必须在同一个网段，且真实服务器的默认网关不能是负载均衡器 真实服务器的 lo 接口上需要配置 VIP 的 IP 地址，且真实服务器需要更改 ARP 协议，“隐藏” lo 接口上的 VIP 前置准备工作和 NAT 模式的一样，这里就不赘述了。\n开始配置 DR 模式。\nRS01、RS02\n修改 ARP 协议 echo 1 \u0026gt; /proc/sys/net/ipv4/conf/lo/arp_ignore echo 1 \u0026gt; /proc/sys/net/ipv4/conf/lo/arp_ignore echo 2 \u0026gt; /proc/sys/net/ipv4/conf/lo/arp_announce echo 2 \u0026gt; /proc/sys/net/ipv4/conf/all/arp_announce 在 lo 接口上配置 VIP ifconfig lo:0 10.23.21.184 netmask 255.255.255.255 验证配置 ifconfig ifconfig LB01\n配置路由入口规则 ipvsadm -A -t 10.23.21.184:80 -s rr 配置路由出口规则 ipvsadm -a -t 10.23.21.184:80 -r 10.23.190.76 -g -w 1 ipvsadm -a -t 10.23.21.184:80 -r 10.23.122.152 -g -w 1 如果 VIP 和本机地址不一致，要配置 VIP（我这里就不用了） 配置 VIP ifconfig eth0:0 VIP/24 验证配置 ipvsadm -ln ipvsadm ifconfig ifconfig 配置完成，现在我们来验证下 DR 模式下的负载均衡。\n发现直接在本地请求 LB01 的外网 IP 地址时，一直处于等待状态，最终报错：Operation timed out。\n我们先来看下 LB01 有没有正确的收到连接请求\n查看 LB01 记录的连接信息：ipvsadm -Lnc ipvsadm 可以看到 LB01 正确的收到了连接请求，并且转发给了 RS02。接下来我们登陆到 RS02 上，检查 RS02 是否接收到了数据包。\n查看 RS02 上的 TCP 连接信息：netstat -natp | grep 10.23.21.184 netstat RS02 收到了数据包，并且也发出了返回的数据包，返回数据包的 IP 地址和端口号也和发出的一致。所以可以合理地猜测，问题出在由 RS02 直接返回数据包给客户端的过程中。那么只有两种情况，RS02 无法连接到客户端或者客户端拒绝接收这个数据包。\n检查 RS02 是否能正常连接到客户端\nping 106.75.220.2 ping RS02 和客户端可以正常请求访问。那么应该是客户端拒绝接收了这个数据包，抓包来看下，客户端是否有收到这个数据包。\n再次请求 LB01，并查看客户端和 LB01 交互的数据包\ntcpdump host 106.75.253.112 -nne tcpdump 发现只有发出的数据包，而没有收到的数据包。现在情况是：RS02 发出了数据包，但是客户端却没收到。那只有一种可能，就是云主机的 EIP 转发数据包的时候，由于某种条件限制，扔掉了这个数据包。如果是这样的话，在内网环境中应该是可以正常访问的。我们再申请一台在相同网段的云主机，验证一下。\n果然是可以正常访问的，后来和官方交流之后也证实了这一点。（我猜测应该是出于对安全的考虑，所有进出的数据包，IP 地址 和 MAC 地址必须和本机一致，否则数据包会被丢弃。）\n到此实验配置完成，验证也完成～\nKeepalived 实现 DR 模型的高可用性实操 在成功搭建 DR 模型之后，不由得思考这么一个问题，如果负载均衡服务器宕机了怎么办？负载均衡服务器承载着客户端对服务端的所有请求路由，如果一旦宕机，影响的是整个系统不可用。所以我们需要一些措施来保证负载均衡的高可用性。\n最简单的办法就是将单点部署的负载均衡服务器变成多点部署。如果当前使用的节点出现问题，迅速地切换到另一个节点上，这样就可以保证系统的整个可用性。那么，现在负载均衡服务器单点故障的问题就转换成多点部署的切换问题。\n我们先来看看解决多点部署的切换问题，需要什么条件？首先我们需要发现问题，即需要不断地检查当前节点是否正常，如果当前节点不正常的话，需要快速地切换到其他的节点上。keepalived 就是这样工作的。\n我们来实际操作一下～\nRS01、RS02 和 DR 模型实操类似，只不过我们要重新申请一个 VIP：10.23.88.247，绑定在 RS01、RS02 的 lo:0 上 ifconfig lo:0 10.23.88.247 netmask 255.255.255.255 broadcast 10.23.88.247 up LB01、LB02\n安装 ipvsadm、keepalived\nyum install ipvsadm keepalived -y 修改 keepalived 配置文件\ncp keepalived.conf keepalived.conf.bak ! Configuration File for keepalived vrrp_instance VI_1 { state MASTER // 备节点：BACKUP interface eth0 virtual_router_id 51 priority 100 // 备节点：50 advert_int 1 authentication { auth_type PASS auth_pass 1111 } virtual_ipaddress { 10.23.88.247 dev eth0 } } virtual_server 10.23.88.247 80 { delay_loop 6 lb_algo rr lb_kind DR nat_mask 255.255.0.0 persistence_timeout 0 protocol TCP real_server 10.23.190.76 80 { weight 1 HTTP_GET { url { path / status_code 200 } connect_timeout 3 nb_get_retry 3 delay_before_retry 3 } } real_server 10.23.122.152 80 { weight 1 HTTP_GET { url { path / status_code 200 } connect_timeout 3 nb_get_retry 3 delay_before_retry 3 } } } 重新启动 keepalived systemctl restart keepalived 验证 keepalived DR 模型\n验证 可以正常访问到两台服务器，接下来把 LB01 的 keepalived 停掉，继续访问 VIP。\n验证 还是可以正常访问，VIP 漂到了 LB02 上。使用 ipvsadm -lnc 查看具体连接信息。\n实验完成，差不多断断续续的用了 4.5 小时，包括一些额外的排查时间，共计花费不到 5 块～\n","date":"2023-09-05T20:52:03+08:00","permalink":"https://boxtsecond.github.io/en/2023/%E6%B7%B1%E5%85%A5%E6%B5%85%E5%87%BA-lvs-%E8%B4%9F%E8%BD%BD%E5%9D%87%E8%A1%A1%E5%9B%9B%E5%AE%9E%E6%93%8D-dr-%E6%A8%A1%E5%9E%8Bkeepalived-dr-%E6%A8%A1%E5%9E%8B%E7%9A%84%E9%AB%98%E5%8F%AF%E7%94%A8/","title":"深入浅出 LVS 负载均衡（四）实操 DR 模型、Keepalived DR 模型的高可用"},{"content":"深入浅出 LVS 负载均衡系列：\n深入浅出 LVS 负载均衡（一）NAT、FULLNAT 模型原理 两台计算机如何在互联网中通信 LVS 负载均衡 通过修改数据包的「源 IP 地址」或 「目标 IP 地址」 NAT 模式 FULLNAT 模式 深入浅出 LVS 负载均衡（二）DR、TUN 模型原理 LVS 负载均衡 通过修改数据包的「目标 MAC 地址」 DR 模式 通过二次封装数据包的「IP 报文」 TUN 模式 深入浅出 LVS 负载均衡（三）实操 NAT、TUNNEL 模型、深入浅出 LVS 负载均衡（四）实操 DR 模型、Keepalived DR 模型的高可用 使用 UCloud 云主机实操各个模型 使用 Keepalived 实现 LVS-DR 的高可用 LVS（Linux Virtual Server）是一个虚拟服务器集群系统。工作在 OSI 模型的传输层，即四层负载均衡。LVS 本身实现了 NAT、DR、TUN 模型，这些模型仅做数据包的转发，而不会与客户端建立连接，成本低效率高。FULLNAT 基于 NAT 实现，LVS 本身不支持，需要额外对内核打补丁后才能使用。\n📌 本系列按照负载均衡器对数据包的处理方式分类，从计算机间通信的角度出发，浅谈 NAT、FULLNAT、DR、TUN 模型的实现原理。 之前介绍了 LVS 负载均衡 NAT、FULLNAT、DR、TUN 模型的实现原理。现在来动手实践一下～\n实验环境 LVS 目前已经是 Linux 内核中的一部分，在内核中的模块叫做 ipvs，支持 NAT、DR、TUNNEL 模型。用户不能直接操作 ipvs 模块，需要安装交互软件 ipvsadm，使用 ipvsadm 和 ipvs 进行交互。\n我使用 3 台 UCloud 云主机来搭建实验环境，创建云主机的时候选择分时购买，更划算一点。\n实验机器及环境 3 台 UCloud 云主机，CentOS 7.9 64位，1 核 1 G，需要注意一下防火墙规则，我选择的是【Web服务器推荐】，开放 22、3389、80、443 的端口号，这个可以自行配置 两台 Real Server：RS01、RS02，一台负载均衡服务器：LB01 RS01：10.23.190.76、RS02：10.23.122.152、LB01：10.23.21.184 RS01、RS02 安装 httpd，快速启动 http 服务器，且配置不同的请求响应 LB01 安装 ipvsadm，并启动 ipvsadm 实验机器展示 实验机器 NAT 模式实操 回顾一下 NAT 模式的特点\nNAT NAT 模式修改数据包的「目标 IP 地址」或 「源 IP 地址」，所有的请求数据包、响应数据包都要经过负载均衡器，所以 NAT 模式支持对端口的转换 真实服务器的默认网关是负载均衡器，所以真实服务器和负载均衡器必须在同一个网段 实操开始，首先我们要做一些前置的准备工作，即把该安装的软件和服务，安装和启动起来。\nRS01、RS02 安装 httpd，快速启动 http 服务\nyum install httpd -y \u0026amp;\u0026amp; service httpd start echo \u0026ldquo;Hello From RS01/RS02\u0026rdquo; \u0026gt; /var/www/html/index.html 验证：curl 0.0.0.0，返回对应的信息 curl 0.0.0.0 LB01 安装 ipvsadm，并启动 ipvsadm\nyum install ipvsadm \u0026amp;\u0026amp; ipvsadm \u0026ndash;save\u0026gt; /etc/sysconfig/ipvsadm \u0026amp;\u0026amp; service ipvsadm start\n看到下图就表示成功启动了 ipvsadm ipvsadm 做完这些前置准备工作之后，接下来我们来对照 NAT 模式来配置具体的负载规则。\nRS01、RS02\n设置默认网关为 DIP，即 LB01 的内网 IP - 10.23.21.184 查看 RS01、RS02 当前的默认网关\nroute -n route -n 可以看到当前的默认网关是 10.23.0.1\n设置默认网关为 10.23.21.184\nroute add default gw 10.23.21.184\n输入命令并按下回车之后，会很长时间没有响应，是正常的。我们等它连接断掉之后，再通过 LB01 登陆到 RS01、RS02 上 删除之前的默认网关\nroute del default gw 10.23.0.1 LB01\n配置路由入口规则，使用 -A 参数\n因为实验使用的是云主机，而云主机的 EIP 或者说外网 IP 地址，本身就是通过 NAT 方式映射到了绑定的云主机上，所以不能将 EIP 当作 VIP 绑定端口。这里就直接将内网 IP 当作 DIP 使用。 ipvsadm -A -t 10.23.21.184:8000 -s rr 配置路由入口规则，使用 -a 参数\nipvsadm -a -t 10.23.21.184:8000 -r 10.23.190.76:80 -m ipvsadm -a -t 10.23.21.184:8000 -r 10.23.122.152:80 -m 验证配置\nipvsadm -ln 开启路由转发\necho 1 \u0026gt;/proc/sys/net/ipv4/ip_forward 这里大概解释下对 ipvsadm 的配置信息\n-A 添加一条新的虚拟服务器记录，即添加一台新的虚拟服务器 -a 添加一条新的真实服务器记录，即在虚拟服务器中添加一台真实服务器 -t 真实服务器提供的是 TCP 服务 -s 负载均衡使用的调度算法，rr 表示轮询 -w 设置权重 -r 指定真实服务器 -m 指定 LVS 使用 NAT 模式 -g 指定 LVS 使用 DR 模式 -i 指定 LVS 使用 TUNNEL 模式 可以看到，我们配置的是使用 NAT 模式，调度算法为轮询。\n到这里我们的配置就全部完成了，接下来让我们验证一下 LB01 能不能按照预期负载到 RS01、RS02 。使用浏览器直接打开 LB01 的外网 IP 地址。\n由于浏览器的缓存机制，在短时间内刷新的时候，返回可能不会发生变化。可以使用 curl 更准确的查看。\n验证 验证成功～\nTUNNEL 模式实操 回顾一下 TUNNEL 模式的特点\nTUN TUNNEL 模式不改变原数据包，而是在原数据包上新增一层 IP 首部信息。所以 TUNNEl 模式不支持对端口的转换，且真实服务器必须能够支持解析两层 IP 首部信息 真实服务器和负载均衡器可以不在同一个网段中 真实服务器需要更改 ARP 协议，“隐藏” lo 接口上的 VIP TUNNEL 模式和其他模式有点不同，不能像之前那样直接使用 VIP 来充当 DIP。所以我们需要额外一个 DIP：10.23.21.180。\n开始配置具体的负载规则～\nRS01、RS02\n安装 ipip 模块\nmodprobe ipip 验证 ipip 模块是否加载成功\nlsmod | grep ipip\nipip 修改 ARP 协议\necho 1 \u0026gt; /proc/sys/net/ipv4/conf/tunl0/arp_ignore echo 2 \u0026gt; /proc/sys/net/ipv4/conf/tunl0/arp_announce echo 1 \u0026gt; /proc/sys/net/ipv4/conf/all/arp_ignore echo 2 \u0026gt; /proc/sys/net/ipv4/conf/all/arp_announce rp_filter 表示是否开启对数据包源地址的校验，这里我们直接关闭校验即可。\necho 0 \u0026gt; /proc/sys/net/ipv4/conf/tunl0/rp_filter echo 0 \u0026gt; /proc/sys/net/ipv4/conf/all/rp_filter 配置 DIP\nifconfig tunl0 10.23.21.180 broadcast 10.23.21.180 netmask 255.255.255.255 up route add -host 10.23.21.180 tunl0 验证配置\nifconfig ifconfig route -n route -n LB01\n配置路由入口规则 ipvsadm -A -t 10.23.21.180:80 -s wrr 配置路由出口规则，由于在不同的网段，这里需要配置 RS01、RS02 的外网地址 ipvsadm -a -t 10.23.21.180:80 -r 10.23.190.76 -i -w 1 ipvsadm -a -t 10.23.21.180:80 -r 10.23.122.152 -i -w 1 配置 DIP 安装 ipip 模块 modprobe ipip ifconfig tunl0 10.23.21.180 broadcast 10.23.21.180 netmask 255.255.255.255 up route add -host 10.23.21.180 tunl0 验证配置 ipvsadm -ln ipvsadm route -n route -n 配置完成，我们再申请一台云主机来验证实操结果。\nDIP 是我们虚拟出来的 IP 地址，所以实际在网络中是找不到的，我们需要先手动将访问 DIP 的路由，访问到 LB01 上。\nroute add -host 10.23.21.180 gw 10.23.21.184 验证一下 route -n route -n 现在让我们来验证一下 TUNNEL 模型是否成功。\n验证 验证成功～\n下篇继续 DR 模型的实操，及使用 Keepalived 实现 DR 模型的高可用。\n","date":"2023-09-05T18:52:03+08:00","permalink":"https://boxtsecond.github.io/en/2023/%E6%B7%B1%E5%85%A5%E6%B5%85%E5%87%BA-lvs-%E8%B4%9F%E8%BD%BD%E5%9D%87%E8%A1%A1%E4%B8%89%E5%AE%9E%E6%93%8D-nattunnel-%E6%A8%A1%E5%9E%8B/","title":"深入浅出 LVS 负载均衡（三）实操 NAT、TUNNEL 模型"},{"content":"深入浅出 LVS 负载均衡系列：\n深入浅出 LVS 负载均衡（一）NAT、FULLNAT 模型原理 两台计算机如何在互联网中通信 LVS 负载均衡 通过修改数据包的「源 IP 地址」或「目标 IP 地址」 NAT 模式 FULLNAT 模式 深入浅出 LVS 负载均衡（二）DR、TUN 模型原理 LVS 负载均衡 通过修改数据包的「目标 MAC 地址」 DR 模式 通过二次封装数据包的「IP 报文」 TUN 模式 深入浅出 LVS 负载均衡（三）实操 NAT、DR 模型、深入浅出 LVS 负载均衡（四）实操 TUNNEL 模型、Keepalived DR 模型的高可用 使用 UCloud 云主机实操各个模型 使用 Keepalived 实现 LVS-DR 的高可用 LVS（Linux Virtual Server）是一个虚拟服务器集群系统。工作在 OSI 模型的传输层，即四层负载均衡。LVS 本身实现了 NAT、DR、TUN 模型，这些模型仅做数据包的转发，而不会与客户端建立连接，成本低效率高。FULLNAT 基于 NAT 实现，LVS 本身不支持，需要额外对内核打补丁后才能使用。\n📌 本系列按照负载均衡器对数据包的处理方式分类，从计算机间通信的角度出发，浅谈 NAT、FULLNAT、DR、TUN 模型的实现原理。 上篇从计算机间的通信说起，知道通信必要的六要素是 源 IP 地址、端口号、源 MAC 地址，目标 IP 地址、端口号、目标 MAC 地址。其中，端口号标志了在应用层的两个具体应用信息，即快递的具体发送人和接收人，IP 地址表示在网络层上两个端点的地址，即快递的发出地址和收货地址，MAC 地址表示在数据链路层上节点间的地址，即快递传送中的各个驿站的地址。\n在了解 LVS 的 NAT、FULLNAT 模型对数据包的修改方式以及它们的缺点之后，站在 NAT 模型的肩膀上，怎样才能更好地优化负载均衡器？\n在 NAT 和 FULLNAT 模式中，不管是请求数据包还是响应数据包，都要经过负载均衡器。但是响应数据包一般要比请求数据包大很多，这可能会成为系统的瓶颈。如果能够将请求数据包转发到真实服务器之后，响应数据包由真实服务器直接返回，这样对负载均衡器的压力就小很多。这种模式又应该如何实现呢？\nLVS 负载均衡 DR 模式 如果真的能够由真实服务器直接响应客户端，而不经过负载均衡器。那么这个由负载均衡器直接响应回客户端的数据包需要满足什么条件，才能被客户端正常接收？\n真实服务器发出的数据包，在客户端接收到的时候，一定要匹配得上从客户端发出的数据包。如果不匹配的话，客户端收到响应数据包后会直接将数据包丢弃。\n📌 客户端发出的请求数据包：CIP ➡️ VIP，则收到的响应数据包一定是 VIP ➡️ CIP。 💡做个小思考，为什么没有带上 MAC 地址？后文有解释～\n但是 VIP 已经绑定在负载均衡器上，真实服务器也有多个，在连通的网络里，如何能让多个真实服务器和负载均衡器的 VIP 地址相同？并且真实服务器的 VIP 不能被其他设备访问的到。也就是说在每台真实服务器上的 VIP 地址，只能它们自己知道“我悄悄藏着 VIP”，别的设备压根不知道。\n这里不得不提另一个非常神奇的 IP 地址 127.0.0.1/0.0.0.0。仔细想一下，127.0.0.1 不就是每台设备上都相同，“悄悄藏着”的 IP 地址吗，除了自己的其他设备都没办法访问。这个神奇的 IP 地址，叫做 Loopback 接口。它是一种纯软件性质的虚拟接口，当有请求数据包送达到 lo 接口，那么 lo 接口会将这个数据包直接 “掉头”，返回给这个设备本身，这就是“环回”接口的由来。所以，如果将 VIP 绑定在 lo 接口上，是不是就可以完成“只有自己知道这个 VIP，别的设备不知道”呢？\n将 VIP 绑定在 lo 接口上，其实只完成了一半，只是做到了在多台真实服务器上绑定相同的 VIP 地址。还记得上篇文章中所讲的 ARP 协议吗，ARP 协议会采集在当前局域网中，除了自己之外的其他主机的 MAC 地址和 IP 地址的映射关系。而 VIP 是一个不能被别的设备采集到的地址，所以我们要对真实服务器的 ARP 协议做一些修改，让 VIP 不会被其他设备采集到。很显然，这不但要修改设备收到 ARP 请求之后的响应（arp_ignore 参数)，也要修改设备向外通告 ARP 的请求 （arp_announce 参数）。\n📌 arp_ignore：定义接收 ARP 请求时的响应级别 0：响应任意网卡上接收到的对本机 IP 地址的 ARP 请求（包括环回网卡），不论目的 IP 地址是否在接收网卡上 1：只响应目的 IP 地址为接收网卡地址的 ARP 请求 2：只响应目的 IP 地址为接收网卡地址的 ARP 请求，且 ARP 请求的源 IP 地址必须和接收网卡的地址在同网段 📌 arp_announce：定义将自己地址向外通告时的通告级别 0：允许任意网卡上的任意地址向外通告 1：尽量仅向目标网络通告与其网络匹配的地址 2：仅向与本地接口上地址匹配的网络进行通告 可以看到，arp_ignore 为 1 并且 arp_announce 为 1 时，lo 接口上绑定的 VIP 可以被藏在本机上，只有自己知道。\nDR 红色表示发出的数据包 绿色表示返回的数据包 黄色表示负载均衡器修改的内容 虚线表示经过 N 个下一跳，即可以不在同一局域网内 实线表示只能 “跳跃一次”，即必须在同一局域网内\n当计算机发出一个请求的数据包到达负载均衡器后，负载均衡器修改请求数据包的 { 目标MAC 地址 } 为 真实服务器的 MAC 地址，其余信息不变。负载均衡器和真实服务器必须在同一局域网内，否则负载均衡器无法替换请求数据包的 { 目标MAC 地址 } 为 真实服务器的 MAC 地址 真实服务器收到请求的数据包，发现自己有一个 “隐藏“ 的 VIP，于是接收这个数据包，并交由对应的上层应用处理 处理完成后，将响应数据包直接返回给客户端。此时在真实服务器上查看 TCP 连接为：VIP ➡️ CIP 总结一下 DR 模式的特点： 仅修改请求数据包的「目标 MAC 地址」，作用在数据链路层。因此负载均衡器必须和真实服务器在同一局域网内，且不能对端口进行转发 真实服务器中的 VIP，只能被自己 “看见”，其他设备不可知。因此 VIP 必须绑定在真实服务器的 lo 网卡上，并且不允许将此网卡信息经过 ARP 协议对外通告 请求的数据包经过负载均衡器后，直接由真实服务器返回给客户端，响应数据包不需要再经过负载均衡器 TUN 模式 除了直接修改请求数据包的目标 MAC 地址，做一次 MAC 地址欺骗之外，还有没有其他方式能够将响应数据包由真实服务器直接返回给客户端呢？看看 VPN 是怎么能够支持你远程办公的吧～\n我们已经讨论过，如果真实服务器直接将响应数据包返回给客户端，那么真实服务器必须有一个 “隐藏” 的 VIP，即配置在 lo 网卡上并且不允许此 VIP 通过 ARP 协议对外通告。\nTUN 红色表示发出的数据包 绿色表示返回的数据包 黄色表示负载均衡器修改的内容 虚线表示经过 N 个下一跳，即可以不在同一局域网内 实线表示只能 “跳跃一次”，即必须在同一局域网内\n当计算机发出一个请求的数据包到达负载均衡器后，负载均衡器不改变源数据包，而是在源数据包上新增一层 IP 首部 { 分发 IP、端口号、MAC 地址 } ➡️ { 真实服务器 IP、端口号、MAC 地址 } 真实服务器收到请求的数据包后，将最外层封装的 IP 首部去掉，发现还有一层 IP 首部，并且目标 IP 地址能够和 lo 上的地址匹配，于是收下请求数据包，并交由对应的上层应用处理 处理完成后，将响应数据包直接返回给客户端。此时在真实服务器上查看 TCP 连接为：VIP ➡️ CIP 总结一下 TUN 模式的特点：\n不改变请求数据包，而是在请求数据包上新增一层 IP 首部信息。因此负载均衡器不能对端口进行转发，但可以和真实服务器不在同一局域网内，且真实服务器需要支持能够解析两层 IP 首部信息，即需要支持“IP Tunneling”或“IP Encapsulation”协议 真实服务器中的 VIP，只能被自己 “看见”，其他设备不可知。因此 VIP 必须绑定在真实服务器的 lo 网卡上，并且不允许将此网卡信息经过 ARP 协议对外通告 请求的数据包经过负载均衡器后，直接由真实服务器返回给客户端，响应数据包不需要再经过负载均衡器 在 DR 和 TUN 模式中，负载均衡器只转发了请求数据包，响应数据包不经过负载均衡器，而是直接返回给客户端。解决了在通常情况下响应数据包比请求数据包大，如果请求和响应数据包都经过负载均衡器，在高并发下可能成为系统瓶颈的问题。\n根据我们的推导过程，可以轻易地得出各种模式的特点和它们要解决的问题。\nNAT 模式，通过修改数据包的「目标 IP 地址」和 「源 IP 地址」，将请求负载到多台真实服务器，是四层负载均衡模式中最基础的负载方式。因为它是对 IP 地址层面的修改，作用在网络层，所以可以对端口进行映射。真实服务器返回的响应数据包必须经过负载均衡器，所以要求真实服务器的默认网关是负载均衡器。 FULLNAT 模式，是对 NAT 模式的一种演进。通过同时修改「源 IP 地址」和「目标 IP 地址」，突破 NAT 模式中真实服务器的默认网关必须是负载均衡器的限制。但是由于同时修改了「源 IP 地址」和「目标 IP 地址」，真实服务器建立的真实连接和客户端毫无关系，所以会丢失客户端的信息。 DR 模式，是对 NAT 模式的另一种演进。由于真实请求中响应数据包比请求数据包大很多的特点，在高并发下会成为系统的瓶颈，于是将响应数据包直接由真实服务器返回给客户端。使用 MAC 地址欺骗来达到此目的，作用于数据链路层，所以不能对端口映射。并且在真实服务器上，必须有一个仅自己可见的 “隐藏” VIP。在网络中，真实的 VIP 绑定在负载均衡器上，用来接收客户端的真实请求数据包。而 “隐藏” 的 VIP 绑定在真实服务器的 lo 接口上，用来欺骗自己可以正常接收目标地址是 VIP 的数据包。所以真实服务器和负载均衡器必须在同一局域网中。 TUN 模式，是对 DR 模式的一种演进。突破 DR 模式中真实服务器和负载均衡器必须在同一局域网中的限制。TUN 模式不会对源数据包进行修改，而是在源数据包上额外新增一条 IP 首部信息，所以不能对端口映射，且要求真实服务器必须能够卸载掉两层 IP 首部信息。 💡 回顾之前的小思考题：为什么在说真实服务器能够正常接收负载均衡器转发的数据包的必要条件时，没有带上 MAC 地址？\n📌 在网络通信部分介绍 ARP 协议和下一跳机制时，我们提到数据包在网络中的转发和快递传送中的驿站类似，每一次数据包的发送，会不断地找到 “下一个目的地” 的 MAC 地址。所以，但凡涉及到物理端口的变迁，都涉及到源 MAC 地址的变化，这个变化是 IP 通信的特性，而并不是负载均衡的特点。 在对负载均衡的各个模式有一定的了解之后，下一篇我们来具体实践和配置～\n","date":"2023-09-05T16:52:03+08:00","permalink":"https://boxtsecond.github.io/en/2023/%E6%B7%B1%E5%85%A5%E6%B5%85%E5%87%BA-lvs-%E8%B4%9F%E8%BD%BD%E5%9D%87%E8%A1%A1%E4%BA%8Cdrtun-%E6%A8%A1%E5%9E%8B%E5%8E%9F%E7%90%86/","title":"深入浅出 LVS 负载均衡（二）DR、TUN 模型原理"},{"content":"深入浅出 LVS 负载均衡系列：\n深入浅出 LVS 负载均衡（一）NAT、FULLNAT 模型原理\n两台计算机如何在互联网中通信 LVS 负载均衡 通过修改数据包的「源 IP 地址」或「目标 IP 地址」 NAT 模式 FULLNAT 模式 深入浅出 LVS 负载均衡（二）DR、TUN 模型原理\nLVS 负载均衡 通过修改数据包的「目标 MAC 地址」 DR 模式 通过二次封装数据包的「IP 报文」 TUN 模式 深入浅出 LVS 负载均衡（三）实操 NAT、DR 模型、深入浅出 LVS 负载均衡（四）实操 TUNNEL 模型、Keepalived DR 模型的高可用\n使用 UCloud 云主机实操各个模型 使用 Keepalived 实现 LVS-DR 的高可用 LVS（Linux Virtual Server）是一个虚拟服务器集群系统。工作在 OSI 模型的传输层，即四层负载均衡。LVS 本身实现了 NAT、DR、TUN 模型，这些模型仅做数据包的转发，而不会与客户端建立连接，成本低效率高。FULLNAT 基于 NAT 实现，LVS 本身不支持，需要额外对内核打补丁后才能使用。\n📌 本系列按照负载均衡器对数据包的处理方式分类，从计算机间通信的角度出发，浅谈 NAT、FULLNAT、DR、TUN 模型的实现原理。 两台计算机如何在互联网中通信 在了解 LVS 负载均衡之前，先要搞清楚两台计算机如何在互联网中通信。茫茫互联网中，两台计算机如何才能找到对方？\n我们先来看看，快递是如何被快递小哥完美地送到你手上的。根据你的地址，快递先送到你所在的省市区，接着在当前省市区找到你的门牌号，最后根据门牌号和姓名，再亲手把快递交给你。\n两台计算机在互联网中的通信也是如此。首先需要知道双方的 IP 地址，即省市区，其次需要知道双方的 MAC 地址，即门牌号。MAC 地址标志着唯一的计算机。在同一台计算机上，可能有多个不同的服务，如何能像快递小哥按照姓名找到你一样，在计算机上找到对应的服务呢？没错，就是按照端口号。\n这样，通信中每台计算机需要提供的信息就很清晰了，即 IP 地址、MAC 地址、端口号。总结一下，通信必需的六个要素就是，源 IP 地址、端口号、源 MAC 地址，目标 IP 地址、端口号、目标 MAC 地址。\n通信 假设计算机 A 和计算机 B 在上述的网络拓扑图（不在同一局域网）中。可以很清晰地看到 计算机 A 和 计算机 B 通信需要五个步骤，其中 ①② 和 ④⑤ 的原理相同。现在我们来看看具体的每个步骤在计算机的世界中是如何实现的。\n首先 A 和 B 的 IP 地址和端口号是已知的，即一个数据包从哪来要发往哪去。所以现在的问题是：A 如何能知道 B 的 MAC 地址？\n最简单的方式就是 A 保存网络中全部设备的 MAC 地址，在发送时查询一下，这样就能得到 B 的 MAC 地址。但是网络中的设备有几十亿个，还在不断地增长，显然这种方式是不可取的。如果按照快递发送过程中，在每个驿站之间进行转移，这样只需要知道该发往的下一目的地在哪里，最终也能将快递成功地交到你的手上。在实际网络中发送数据包也是这样，现在的目标就是确定 “下一个目的地” 的 MAC 地址。\n步骤 ①②：发送数据包至网关\nA 不知道 B 的 MAC 地址，且 A 和 B 也不在同一个局域网中。这时 A 会根据 ARP 协议先发出一个广播包，即目标 MAC 地址是 FF:FF:FF:FF:FF:FF 的数据包。当 交换机1 收到这个广播包之后，会将这个数据包转发给其他端口，并且记录 A 的 MAC 地址和交换机端口之间的映射关系，后续再看到这个 MAC 地址，就知道该从哪个端口转发出去。当 路由器1 收到广播包后，会将自己的 MAC 地址返回。A 接收到返回后，就知道 “下一个目的地” 的 MAC 地址了。A 重新发送数据包，将目标 MAC 地址填写为 路由器1 的 MAC 地址，并在本地的缓存表中记录返回的 MAC 地址。\n💡 查看当前设备缓存表里已存的 MAC 地址：arp -a 📌 ARP 协议的目的就是找到“下一个目的地”的 MAC 地址，即 IP 地址和 MAC 地址之间的映射关系。 当两台设备同处于一个局域网时，“下一个目的地” 就是目标设备的 MAC 地址，当两台设备不处于同一个局域网时，“下一个目的地” 就是网关的 MAC 地址。 步骤 ③：网关间跳转\n经过步骤 ①②，此时 路由器1 收到的数据包为 { A_IP、PORT、A_MAC } ➡ { B_IP、PORT、路由器1_MAC } 。收到数据包后，路由器1 查看自己的路由表，如下图所示。\n💡 查看当前设备设置的路由表：route -n route -n 路由器1 会将 B_IP 与 路由表中每条记录的子网掩码（Genmask）做按位与运算。如果得到的结果和目的网络（Destination）相同，那么 “下一个目的地“ 的 MAC 地址，就是配置的网关（Gateway）的 MAC 地址，这种找到相邻网络的方式叫做 “下一跳机制”。如果网关的地址为 0.0.0.0，说明可以在局域网中直接通信，不需要 “下一跳”。至此，再次找到了 “下一个目的地” 的 MAC 地址，即 路由器2 的 MAC 地址。此时 路由器2 收到的数据包为 { A_IP、PORT、路由器1_MAC } ➡ { B_IP、PORT、路由器2_MAC } 。步骤 ④⑤ 和 ①② 的原理相同，在这里就不赘述了。\n📌 下一跳的目的就是找到“下一个目的地”，即下一步该到达哪里，侧重 “路线” 的选择，并由此获取到对应的 MAC 地址，继续传送数据包。 总结一下： 使用 ARP 协议找到网关出口或同一局域网内设备的 MAC 地址 按照路由表的每条规则和 目标 IP 地址做按位与运算，找到相邻网关入口的 MAC 地址 “下一个目的地” 和当前地址都仅仅相邻一步，且每次 “跳跃” 后的源 MAC 地址 和 目标 MAC 地址都会发生对应的替换 数据包中，IP 地址指明起点终点，MAC 地址指明跳跃的节点，端口号指明对应的应用服务 当然，光是找到对方还不够，还需要一个约定的交流方式，平时我们所熟知的各种协议，都是计算机「约定的交流方式」。\nLVS 负载均衡 随着使用互联网的设备不断增长，服务端对应接收到的 HTTP 请求更是呈指数型的增长。当一台服务器无法承载非常大的请求量时，使用多台服务器来分摊请求。将请求分摊给多台服务器的行为，就称之为负载均衡。\n从网络中计算机通信的角度，而非使用更上层的应用（如 Nginx）出发，搭建四层负载均衡器后，数据包的发送链路为：CIP ➡ VIP ➡ DIP ➡ RIP，即 客户端 IP ➡ 虚拟 IP ➡ 分发 IP ➡ 真实服务器 IP。对于客户端来说，只需要知道请求到达的地址是 VIP，不需要考虑负载，即 CIP ➡ VIP 是固定的。\n所以负载均衡器要做的事情，就是将 CIP 发送到 VIP 的数据包，经由 DIP 转发给 RIP，服务响应后再将响应的数据包返回给 CIP。\nNAT 模式 NAT 红色表示发出的数据包 绿色表示返回的数据包 黄色表示负载均衡器修改的内容 虚线表示经过 N 个下一跳，即可以不在同一局域网内 实线表示只能 “跳跃一次”，即必须在同一局域网内\n当计算机发出一个请求的数据包到达负载均衡器后，负载均衡器将发送数据包的 { 目标 IP 地址、端口号、目标 MAC 地址 } 转换为 { 某台真实服务器的 IP 地址、真实服务的端口号、真实服务器的 MAC 地址 } ，其余信息不变。这种只转换数据包的目标设备信息，而不修改数据包的源设备信息，称之为 DNAT，即目标网络地址转换。 真实服务器收到请求的数据包，返回响应的数据包：{ 某台真实服务器的 IP 地址、真实服务的端口号、真实服务器的 MAC 地址 } ➡️ { 原始请求的 IP 地址、端口号、跳跃的 MAC 地址 } 。所以此时在服务器上查看 TCP 连接为：CIP ➡️ RIP。 真实服务器返回的数据包的 “下一个目的地” 必须是负载均衡器。如果返回数据包直接返回给客户端，客户端发现返回数据包的源设备信息和发出数据包的目标设备信息不一致，将会丢弃返回数据包。所以真实服务器的默认网关必须是 DIP，保证返回数据包的 “下一个目的地” 是负载均衡器。 当返回的数据包到达负载均衡器后，负载均衡器将返回数据包的 { 原始请求的 IP 地址、端口号、跳跃的 MAC 地址 } 转换为原始请求的 { 目标 IP 地址、端口号、目标 MAC 地址 } 。这种只转换数据包的源设备信息，而不修改数据包的目标设备信息，称之为 SNAT，即源网络地址转换。 负载均衡器返回数据包给客户端。 总结一下 NAT 模式的特点： 修改数据包的「源 IP 地址」或 「目标 IP 地址」，可以对端口进行转发 真实服务器的默认网关必须是负载均衡器，所以真实服务器和负载均衡器必须在同一个局域网内 所有的请求数据包、响应数据包都要经过负载均衡器 FULLNAT 模式 NAT 模式中，负载均衡器和真实服务器必须在同一局域网内，但在实际的开发过程中，真实服务器可能分布在不同的网段，甚至不同的城市。如何能将 NAT 模式应用在真实服务器分布在不同网段的场景下？\nFULLNAT 红色表示发出的数据包 绿色表示返回的数据包 黄色表示负载均衡器修改的内容 虚线表示经过 N 个下一跳，即可以不在同一局域网内 实线表示只能 “跳跃一次”，即必须在同一局域网\n当计算机发出一个请求的数据包到达负载均衡器后，负载均衡器会对请求数据包同时做 SNAT 和 DNAT，将请求数据包修改为：{ 均衡出口 IP 地址、端口号、负载均衡器的 MAC 地址 } ➡️ { 某台真实服务器的 IP 地址、真实服务的端口号、真实服务器的 MAC 地址 }。 这样负载均衡器就可以独立的和真实服务器进行数据包的传送 真实服务器收到请求的数据包，返回响应的数据包：{ 某台真实服务器的 IP 地址、真实服务的端口号、真实服务器的 MAC 地址 } ➡️ { 负载均衡器的 IP 地址、端口号、负载均衡器的 MAC 地址 } 。此时在真实服务器上查看 TCP 连接为：DIP ➡️ RIP。 当返回的数据包到达负载均衡器后，负载均衡器将返回数据包再次同时做 DNAT 和 SNAT。 负载均衡器返回数据包给客户端。 总结一下FULL NAT 模式的特点： 同时修改数据包的「源 IP 地址」和「目标 IP 地址」，可以对端口进行转发 负载均衡器不需要以网关的形式存在，即负载均衡器可以和真实服务器在不同的网络中 真实服务器最终建立的连接是 DIP ➡️ RIP，无法获取真实客户端的 IP 地址 所有的请求数据包、响应数据包都要经过负载均衡器 LVS 本身不支持 FULLNAT 模式，需要额外对内核打补丁后才能使用。\n可以看到在 NAT 和 FULLNAT 模式中，不管是请求数据包还是响应数据包，都要经过负载均衡器。但是响应数据包一般要比请求数据包大很多，这可能会成为系统的瓶颈。如果能够将请求数据包转发到真实服务器之后，响应数据包由真实服务器直接返回，这样对负载均衡器的压力就小很多。这种模式又该如何实现？\n下篇继续～\n","date":"2023-09-05T14:52:03+08:00","permalink":"https://boxtsecond.github.io/en/2023/%E6%B7%B1%E5%85%A5%E6%B5%85%E5%87%BA-lvs-%E8%B4%9F%E8%BD%BD%E5%9D%87%E8%A1%A1%E4%B8%80natfullnat-%E6%A8%A1%E5%9E%8B%E5%8E%9F%E7%90%86/","title":"深入浅出 LVS 负载均衡（一）NAT、FULLNAT 模型原理"},{"content":"短网址服务 生成短网址，将原始的长网址转化为短网址 访问短网址，当用户点击短网址时，重定向到原始网址 短网址服务 生成短网址 哈希算法 使用哈希算法，将长网址转化成一个固定长度的哈希值 哈希算法的选择，不需要考虑反向解密，只需要关心哈希算法的计算和冲突概率，选用 MurmurHash 算法，可以得到 32 bits 或 128 bits 长度的哈希值 如何让短网址更短 将 MurmurHash 算法得到的哈希值，转化为更高进制的哈希值，如 62 进制，即包含了 0～9、a～z、A～Z 的 62 个字符 解决哈希冲突 当新的原始网址需要生成短网址的时候，先通过哈希算法得到短网址 在存储数据库中查找是否已经存在此短网址，如果存在，且原始网址一样，则返回短网址，如果存在，且原始网址不一样，可以将原始网址后拼接一个特殊字符串，再计算哈希值，如果仍旧冲突，则换一个拼接字符串继续尝试 优化 可以借助数据库的唯一索引，来减少从数据库中获取原始网址的次数，当出现唯一索引冲突时，再去执行“查询、写入”的过程 使用布隆过滤器来判断短网址是否有冲突 ID 生成器 通过维护一个 ID 自增生成器，生成原始网址对应的自增 ID，再转化为 62 进制，并将短网址和对应的原始网址存储在数据库中 优化 相同的原始网址可能会对应不同的短网址\n不做处理 当需要生成短网址时，现在数据库中查找原始网址是否已经存在，如果存在则直接返回之前生成的短网址，在数据库中对原始网址、短网址都添加索引 提高 ID 生成器的性能\n利用数据库的自增字段 ID 生成器添加多个前置发号器，给多个前置发号器发送 ID 号码，当接收到短网址生成请求时，选择某一个前置发号器来生成号码 ID 生成器 多个 ID 生成器同时服务，每个 ID 生成器按照一定的规则生成 ID 号码\nID 生成器 ","date":"2023-08-24T13:17:41+08:00","permalink":"https://boxtsecond.github.io/en/2023/%E5%B8%B8%E8%A7%81%E5%BA%94%E7%94%A8%E5%85%AD%E7%9F%AD%E7%BD%91%E5%9D%80%E6%9C%8D%E5%8A%A1/","title":"常见应用（六）短网址服务"},{"content":"鉴权 1.精确匹配规则 - 使用散列表存储，不同应用对应的不同规则关系 - 当需要匹配时 - 暴力匹配：可以将请求 URL 和字符串数组中逐一匹配，使用字符串匹配算法 KMP、BM、BF 等 - 快速匹配：将所有规则按照字符串大小排序，存储成一个有序数组，当要匹配时，使用二分查找，在有序数组中匹配 精确匹配规则 前缀匹配规则\n使用散列表存储，不同应用对应的不同规则关系\n当需要匹配时\n暴力匹配 快速匹配：使用 Trie 树来存储所有字符串规则，Trie 树中的每个节点不是存储单个字符串，而是存储接口被“/”分割之后的子目录，并且将每个节点的子节点存储成有序数组，在匹配的过程中，可以利用二分查找算法，决定下一个节点 前缀匹配规则 前缀匹配规则 模糊匹配规则\n使用散列表存储，不同应用对应的不同规则关系\n当需要匹配时\n暴力匹配：使用回溯的思想来匹配通配符 快速匹配：将包含通配符和不包含通配符的规则分开处理，不包含通配符的规则，存成有序数组或 Trie 树，剩余的包含通配符的规则，存储在数组中。当收到请求时，先在不包含通配符的有序数组或 Trie 树中匹配，如果可以匹配成功，则返回，如果不能匹配，则继续在通配符规则中匹配 模糊匹配规则 限流 固定时间窗口限流算法\n思想\n选定一个时间起点，之后收到请求时将计数器加一 根据限流规则，如果在当前时间窗口内，累计访问次数超过限流值，则拒绝后续的访问请求 当进入下一个时间窗口时，计数器清零重新计数 固定时间窗口 缺点\n限流策略过于粗略，无法应对两个时间窗口临界时间内的突发流量 缺点 滑动时间窗口限流算法\n假设限流的规则为，在任意 1s 内，接口的请求次数都不能大于 K 次\n维护一个 K + 1 的循环队列，用来记录 1s 内到来的请求\n当收到新的请求时，将与这个新请求的时间间隔超过 1s 的请求，从队列里删除 查看循环队列中是否有空闲位置，如果有的话，把新请求存储在队尾位置，如果没有，说明这 1s 内的请求次数已经达到了 K 次，拒绝此次请求 滑动时间窗口 更多的平滑算法\n令牌桶算法 漏桶算法 https://mp.weixin.qq.com/s?__biz=MzI4MTY5NTk4Ng==\u0026mid=2247488993\u0026idx=1\u0026sn=4b9d5deedd0e626c456744f04b499bbb\u0026source=41#wechat_redirect ","date":"2023-08-24T13:09:34+08:00","permalink":"https://boxtsecond.github.io/en/2023/%E5%B8%B8%E8%A7%81%E5%BA%94%E7%94%A8%E4%BA%94%E9%89%B4%E6%9D%83%E4%B8%8E%E9%99%90%E6%B5%81/","title":"常见应用（五）鉴权与限流"},{"content":"Disruptor 内存消息队列 Disruptor 内存消息队列是线程之间用于消息传递的队列\n消息队列的实现 “生产者 - 消费者模型”，需要先进先出，选择队列作为底层的数据结构\n基于链表实现的无界队列 优点，快速动态扩容 缺点，占用内存多 基于数组实现的有界队列循环队列 优点，限制队列的大小，限制内存的持续增长，防止 Out of Memory 错误 缺点，搬移数据时性能变差，使用循环队列解决此问题 选用顺序队列中的循环队列 加锁的“生产者 - 消费者模型”\n普通的“生产者 - 消费者模型”会出现的问题 多个生产者并发地往队列中写入数据，可能会出现互相覆盖的情况 多个消费者并发地从队列中消费数据，可能会出现重复读取的情况 解决上述问题 加锁，同一时间只允许一个线程执行，将并行改为串行 串行会导致多个生产者同一时间生产数据时，效率的下降，可以使用 CAS（Compare And Swap）优化 无锁的“生产者 - 消费者模型”，Disruptor 使用的方式\n生产者生产数据\n在队列中添加数据前，申请可用空闲存储空间，批量地申请连续的 n 个（n \u0026gt;= 1）存储单元 申请到 n 个存储单元之后，后续往队列中添加元素时，就可以不用加锁了 申请存储单元的过程是需要加锁的\n消费者消费数据\n申请一批连续可读的 n 个存储单元 申请到 n 个存储单元之后，后续的读取操作就可以不用加锁了 申请连续可读的存储单元是需要加锁的\n当多个生产者申请了多块连续的存储单元时，若前一个存储单元没有完全写入数据，在这个存储单元的后续存储单元，无法被消费者读取\ndisruptor 源码地址： https://github.com/LMAX-Exchange/disruptor\n","date":"2023-08-24T13:06:42+08:00","permalink":"https://boxtsecond.github.io/en/2023/%E5%B8%B8%E8%A7%81%E5%BA%94%E7%94%A8%E5%9B%9Bdisruptor-%E5%86%85%E5%AD%98%E6%B6%88%E6%81%AF%E9%98%9F%E5%88%97/","title":"常见应用（四）Disruptor 内存消息队列"},{"content":"搜索引擎主要构成部分 搜索 通过爬虫爬取网页\n分析 将网页内容抽取、分词，构建临时索引，计算 PageRank\n索引 通过分析阶段构建的临时索引，构建倒排索引\n查询 根据倒排索引获取相关网页，计算网页排名，返回查询结果\n搜集 实现要点 将整个互联网当作一个非常庞大的有向图，把每一个页面当作一个顶点，如果某个页面包含另一个页面的链接，则在这两个页面（顶点）之间，连出一条有向边 利用图的遍历算法（广度、深度优先搜索算法），来遍历整个互联网中的网页 采用广度优先搜索算法 先爬取一些知名（权重较高）的网页，作为种子网页链接，放入到队列中 技术细节 待爬取网页链接文件 links.bin 将 links.bin 作为爬虫待爬取页面的队列，即广度优先搜索中的队列 每次从 links.bin 中取出网页链接爬取对应的内容 将爬取网页内容中的链接存储到 links.bin 中，实现断点续爬 网页判重文件 bloom_filter.bin 使用布隆过滤器，来对已经爬取过的网页进行判重处理 定时地将布隆过滤器存储在磁盘上，写入 bloom_filter.bin 中，实现断点续爬不会重复爬取 原始网页存储文件 doc_raw.bin 将多个网页存储在一个文件中，每个网页之间用标识符分割，方便后续读取\n![网页存储](post/algorithm/img/usage_search_engine_save.jpg) 对每个文件的大小做限制，当文件大小超过限制时，创建新的文件 网页链接及其编号的对应文件 doc_id.bin\n对每个网页分配一个唯一的 ID，可以按照网页被爬取的先后顺序，从小到大依次编号 links.bin、bloom_filter.bin 供爬虫使用，doc_raw.bin、doc_id.bin 为搜集阶段的成果，供后续使用\n分析 抽取网页文本信息 根据 HTML 标签抽取网页中的文本信息 去掉不需要的标签，如 JavaScript 代码、、、 标签中的内容 去掉所有的 HTML 标签 使用 AC 自动机多模式匹配算法，在整个网页的字符串中，一次性的查找多个关键词 分词并创建临时索引 确定分词的规则，如 基于字典（词库）和规则的分词方法\n字典中包含大量常用的词语（可以直接从网上 download） 基于词库，采取最长匹配规则，对文本进行分词 可以将词库中的单词构建成 Trie 树 构建临时索引 tem_index.bin\n分词之后，得到网页对应的一组单词列表，将网页和对应的单词列表之间的对应关系，写入到一个临时索引文件中，其中单词列表使用单词编号（为了节省存储空间）\n![t.jpg](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/f78f3f3c-7445-4745-8348-7cab85f5c29f/t.jpg) 将单词编号和单词的对应关系，存储到文件 term_id.bin 中\n使用给网页编号类似的方法给单词编号 使用散列表来记录已经编过号的单词 tmp_index.bin、term_id.bin 供后续构建索引使用\n索引 使用多路归并排序的方式，将分析阶段产生的临时索引，构建成倒排索引 （Inverted Index） index.bin\n对临时索引，按照单词编号的大小进行排序，由于临时索引的文件大小一般都很大，所以使用“归并排序”的处理思想，先把临时索引文件分割成多个小文件，在对每个小文件独立排序，最后合并在一起（实际应用中可以直接使用 MapReduce 处理）\n临时索引文件排序后，相同的单词被排列到了一起，顺序地遍历排好序的临时索引文件，将每个单词对应的网页编号列表存储在倒排索引文件中 index.bin\n临时索引 倒排索引中记录了每个单词以及包含它的网页列表\n倒排索引 生成每个单词编号在倒排索引文件中的偏移位置 term_offset.bin，快速地查找某个单词编号在倒排索引中存储的位置\n偏移位置 index.bin、term_offset.bin 供后续查询使用\n查询 经过上面三个阶段之后，得到以下文件 doc_id.bin，记录网页链接和编号之间的对应关系 term_id.bin，记录单词和编号之间的对应关系 index.bin，倒排索引文件，记录每个单词编号及对应包含它的网页编号列表 term_offset.bin，记录每个单词编号在倒排索引中的偏移位置 将 doc_id.bin、term_id.bin、term_offset.bin 文件中的内容，以散列表的结构加载在内存中 当用户输入搜索文本时，先对用户的输入文本进行分词处理，得到 k 个单词 在 term_id.bin 对应的散列表中查找这 k 个单词对应的单词编号，k_ids 在 term_offset.bin 对应的散列表中查找 k_ids 对应在倒排索引文件中的偏移位置，k_offsets 在 index.bin 文件中，查找 k 个单词对应的包含它们的网页编号列表，k_web_ids 使用散列表统计 k_web_ids 每个网页编号出现的次数，次数越多说明包含越多的用户查询单词 按照出现次数的倒序，在 doc_id.bin 中查找对应的网页链接，分页展示 相关算法 计算网页权重，PageRank 算法， https://zh.wikipedia.org/wiki/PageRank 计算查询结果排名，tf-idf 模型， https://zh.wikipedia.org/wiki/Tf-idf ","date":"2023-08-24T12:58:25+08:00","permalink":"https://boxtsecond.github.io/en/2023/%E5%B8%B8%E8%A7%81%E5%BA%94%E7%94%A8%E4%B8%89%E6%90%9C%E7%B4%A2%E5%BC%95%E6%93%8E/","title":"常见应用（三）搜索引擎"},{"content":"Redis 常用的数据类型 列表 list 当列表中保存的单个数据小于 64 字节，且列表中数据个数少于 512 个，将会使用压缩列表，否则使用的是双向循环链表 压缩列表 ziplist，Redis 自己设计的一种数据结构 类似于数组，通过一片连续的内存空间来存储数据，允许存储的数据大小不同\n压缩列表 压缩列表相比于数组，节省内存，而且能够支持不同类型数据的存储\n双向循环链表 Redis 中会额外定义一个 list 结构体，包含链表的首、尾指针、长度等信息，使用起来十分方便 字典 hash 当字典中柏村的键和值的大小都小于 64 字节，且键值对的个数小于 512 个，将会使用压缩列表，否则使用的是散列表 散列表 MurmurHash2 使用 MurmurHash2 哈希算法作为哈希函数，运行速度快、随机性好 出现哈希冲突时，使用链表法解决 支持散列表的动态扩容、缩容 当装载因子大于 1 时，触发扩容，将散列表扩大为原来的 2 倍左右，具体的值通过计算得到 当装载因子小于 0.1 时，触发缩容，将散列表缩小为原来的 1/2 左右，具体的值也是通过计算得到的 参考 https://github.com/antirez/redis/blob/unstable/src/dict.c 在扩容、缩容的过程中，需要做大量的数据搬移和哈希值的重新计算，会比较耗时，所以 Redis 使用渐进式扩容缩容策略，将数据的搬移分批进行 集合 set 当存储的数据都是整数，且存储的数据元素个数不超过 512 个时，将会使用有序数组，否则使用的是散列表\n有序集合 sortedset 当存储的所有数据的大小都小于 64 字节，且元素个数少于 128 个时，将会使用压缩列表，否则使用的是跳表\nRedis 的数据持久化（数据结构的持久化） 清除原有的存储结构，只将数据存储到磁盘中。当需要从磁盘读取到内存中时，再重新将数据组织成原来的数据结构 从磁盘读取到内存中，将数据组织成原来的数据结构，可能会消耗不少的时间 保留原来的数据格式，将数据按照原有的格式存储在磁盘中 ","date":"2023-08-24T12:55:04+08:00","permalink":"https://boxtsecond.github.io/en/2023/%E5%B8%B8%E8%A7%81%E5%BA%94%E7%94%A8%E4%BA%8Credis-%E5%B8%B8%E7%94%A8%E6%95%B0%E6%8D%AE%E7%B1%BB%E5%9E%8B%E5%AF%B9%E5%BA%94%E7%9A%84%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/","title":"常见应用（二）Redis 常用数据类型对应的数据结构"},{"content":"为什么需要索引 实际软件开发中，需求的本质是对数据的存储和计算 数据的存储，需要的是“数据结构” 数据的计算，需要的是“算法” 对数据存储的需求 增删改查，以及提高这些操作的执行效率 节省存储空间 “索引”的需求分析 功能性需求 构建索引的原始数据的格式 结构化数据，如 MySQL 中的数据 非结构化数据，如搜索引擎中的网页等，需要做预处理，提取出查询关键词，对关键词构建索引 操作数据的类型 静态数据，构建索引时只需要考虑数据查询时的效率 动态数据，构建索引时，需要考虑数据查询、数据更新（动态的更新索引）的效率 数据存储的位置 内存，查询数据的速度快，但是存储的数据量有限 硬盘，查询数据的速度慢，支持大量数据的存储 内存 + 硬盘 数据的查找方式 单值查找，根据某个关键词等于某个值的数据查找 区间查找，查找关键词处于某个区间值内的数据 单关键词查找 多关键词组合查找 如果是结构化数据，如 MySQL，可以针对多个关键词的组合建立联合索引 如果是非结构化数据，如 网页内容，可以对多个关键词分别建立索引，在查询的时候取交集、并集等，计算出多个关键词组合的查询结果 非功能性需求 索引不能消耗过多的存储空间，或者远超过原始数据，否则本末倒置 在考虑索引的查询效率的同时，也要考虑到索引的维护成本 构建索引常用的数据结构 散列表 增删改查的时间复杂度为 O（1） 常用于键值数据库中，如 Redis、Memcache 常构建在内存中 红黑树 数据的插入、删除、查找的时间复杂度为 O（logn） 常构建在内存中 Ext 文件系统中，对磁盘块的索引，使用的是红黑树 B+ 树 B+ 树需要操作的磁盘 IO 更少 常构建在硬盘上 大部分关系型数据库的索引都是用 B+ 树来实现的，如 MySQL、Oracle 跳表 通过调整索引结点个数和数据个数之间的比例，可以很好地平衡索引对内存的消耗及查询效率 常构建在内存中 Redis 中，有序集合的底层实现，使用的是跳表 辅助类数据结构，快速判断数据是否在索引中 位图 布隆过滤器，利用“不存在的一定不存在”规则 ","date":"2023-08-24T12:50:44+08:00","permalink":"https://boxtsecond.github.io/en/2023/%E5%B8%B8%E8%A7%81%E5%BA%94%E7%94%A8%E4%B8%80%E7%B4%A2%E5%BC%95%E5%BA%95%E5%B1%82%E7%9A%84%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/","title":"常见应用（一）索引底层的数据结构"},{"content":"贪心算法 Greedy Algorithm 适用场景明确包含期望值和限制值 一组数据中定义了限制值和期望值，求满足限制值的情况下，令期望值最大 确定是否可以使用贪心算法 每次选择当前情况下，在对限制值同等贡献量的情况下，对期望值贡献最大 初步验证贪心算法求得的结果是否是最优 尝试举几个例子 不能使用贪心算法的问题的特征 前面的选择，会影响后面的选择，例如 地图的最短路径 贪心算法的常见示例 霍夫曼编码 Huffman Coding 给字符进行编码，字符出现的频率越高，则编码后的符号越短 任何字符的编码都不是另一个的前缀 解压缩时，尽可能长地读取二进制串 按照字符出现的频率，放入优先级队列中从小到大排列，若频率相同则按照出现次序排列 构建霍夫曼树 每次从队列中取出前两个元素（最小的两个元素），将它们的频率相加后，放入优先级队列中，仍旧保证优先级队列是有序的 根据霍夫曼树进行编码 给霍夫曼树中的所有左链接 0，右链接 1 从根结点开始，依次记录所有字母的编码 Prim 最小生成树算法、Kruskal 最小生成树算法 Dijkstra 单源最短路径算法 分糖果 每次从剩下的孩子中找到需求最小的孩子 从剩下的糖果中找到能满足他的最小的糖果 钱币找零 如果钱币的面额分布较为均匀，如 100、50、20，则可以使用贪心算法。每次找出最大面额的钱币支付 如果钱币的面额分布不均匀，如 100、99、1，则贪心算法不再适用，需要使用动态规划 6.区间覆盖 n 个区间的最左端点为 lmin，最右端点为 rmax n 个区间按照起始端点从小到大排序 每次选择左端点和已经覆盖的区间不重合，右端点尽量小的 分治算法 Divide and Conquer 原问题可以分解成一系列的子问题，且原问题与分解成的小问题有相同的模式 原问题分解成的子问题可以独立求解，子问题之间没有相关性 具有分解终止条件，在问题足够小时，可以直接求解 可以将子问题合并成原问题，合并操作的复杂度不高 分治算法-递归操作 分解，将原问题分解成一系列子问题 解决，递归地求解各个子问题，若子问题足够小，则直接求解 合并，将子问题的结果合并成原问题 分治算法的常见示例 一组数据的有序对个数或者逆序对个数 将数组分成前后两段 A1、A2，则数组的逆序对个数等于 A1 的逆序对个数 K1 与 A2 的逆序对个数 K2、A1 A2 的逆序对个数 K3 之和，即 K1 + K2 + K3 借助归并排序，当 A1、A2 两个有序数组合并时，找出 A1 中比 A2 大的元素个数 最后将结果全部相加，得到总的逆序对个数 求平面内的最近点对 矩阵计算 回溯算法 Backtracking Algorithm 枚举搜索的思想，有规律地枚举所有可能的解\n回溯算法的常见示例 八皇后 0 - 1 背包 正则表达式 图的着色 旅行商问题 数独 全排列 动态规划 Dynamic Programming 适用场景 用于求解最优问题，如最大值、最小值等 思路 把问题分解为多个阶段，每个阶段对应一个决策 记录每个阶段可达的状态集合（去掉重复的） 通过当前阶段的状态集合，来推导下一个阶段的状态集合，动态地向前推进 动态规划适用的问题 动态规划适合解决的问题的模型（一个模型），即多阶段决策最优解模型 解决问题的过程中，需要经历多个决策阶段 每个决策阶段都对应着一组状态 求解最优解时，可以经过某组决策序列来找到 三个特征 最优子结构 问题的最优解包含子问题的最优解，即可以通过子问题的最优解，推导出问题的最优解 后面阶段的状态可以通过前面阶段的状态推导出来 无后效性 推到后面阶段的状态时，只需要关心前面阶段的状态值，而不需要关心这个状态值是怎么得到的 某阶段的状态确定后，不受后面阶段的决策影响 重复子问题 不同的决策序列，到达某个相同的阶段时，可能会产生重复的状态 动态规划解决思路 状态转移表法 先建立一个状态表（一般都是二维的，如果问题的状态比较复杂的话，可能是多维） 状态表的每个状态包含三个变量，行、列、数组值 根据决策的过程，从前往后递推地填充状态表 状态转移方程法 递归 + 缓存、迭代递推 找到最优子结构 推导出状态转移方程 动态规划 和 回溯算法 的关系 根据回溯算法画出递归树 如果存在子问题，则可以考虑是否能用动态规划实现 如果不存在子问题，则回溯算法就是最优解 并行算法 并行处理的常见示例 并行排序 处理思想：先将数据进行分片，然后并行处理 并行执行归并排序，先随意地对数据分片，排序之后再合并 并行执行快速排序，先对数据按照大小划分区间，然后再排序，排完序后不需要合并 并行查找 使用散列表存储一定量的数据，当需要对散列表动态扩容时，会耗费很多不必要的内存 将数据随机分割成 k 份，每份中的数据只有原来的 1/k，针对这 k 个小数据集合分别构建散列表 当某个散列表的装载因子过大时，可以单独对这个小散列表进行扩容，这样不仅从内存的利用率或扩容的执行效率上，都比只使用一个大的散列表高效 并行匹配字符串 将一个大文本分割成 k 个小文本，并行地在这些小文本中查找关键词 在相邻的两个小文本中，将前一个小文本的结尾取 m 个字符，后一个小文本的开始取 m 个字符，在这个 2m 的字符串中再查找一遍需要匹配的字符串 并行搜索 并行化广度优先搜索算法 广度优先搜索是一种逐层搜索的搜索策略，可以基于当前结点，启动多个线程并行地搜索下一个层的顶点 使用两个队列 A、B，多个线程并行地处理队列 A 中的顶点，并将扩展得到的顶点存储在队列 B 中。队列 A 中的顶点都遍历过之后，队列 A 被清空。再遍历队列 B 中的顶点，将扩展得到的顶点存储在队列 A 中。A、B 循环使用。 ","date":"2023-08-24T12:40:26+08:00","permalink":"https://boxtsecond.github.io/en/2023/%E5%B8%B8%E8%A7%81%E7%AE%97%E6%B3%95%E7%B1%BB%E5%9E%8B%E4%B9%9D%E5%9F%BA%E6%9C%AC%E7%AE%97%E6%B3%95%E6%80%9D%E6%83%B3/","title":"常见算法类型（九）基本算法思想"},{"content":"字符串相似度 字符串相似度\n推荐系统 Recommendation System 解析推荐系统 找到偏好相似的用户，推荐他们喜欢的内容 找到特征相似的内容，推荐这些内容 基于相似用户推荐 需要有当前用户喜爱歌曲的数据，根据用户间的“口味”相似度来进行推荐 定义“口味”相似 遍历所有的用户，对比每个用户和你共同喜爱的歌曲个数，并且设置一个阈值，当通过喜爱的歌曲个数超过这个阈值，则认为这个用户和你的“口味”相似 将和你“口味”相似的用户喜爱且你没有听过的歌曲，推荐给你 定义喜爱程度 给每个行为定义一个得分，得分越高表示喜爱程度越高\n定义得分 使用欧几里得距离来计算两个用户之间的相似度\n使用向量表示每个用户对所有歌曲的喜爱程度 计算两个向量之间的欧几里得距离，度量两个用户的“口味”相似程度 计算向量距离 基于相似歌曲推荐 根据当前用户喜欢的歌曲，找到相似的歌曲来进行推荐 定义歌曲“相似” 对歌曲定义一些特征项，基于歌曲特征项向量的距离来比表示两首歌曲相似度，缺点是需要人工标记出每首歌的特征项，数据量太大且有很高的主观性\n找到喜欢听的“相似”人群，如果两首歌曲的喜爱人群相似，则说明这两首歌比较相似\n遍历所有的歌曲，将每个用户的打分作为向量，计算出歌曲的相似度\n歌曲的相似度 ","date":"2023-08-24T12:33:23+08:00","permalink":"https://boxtsecond.github.io/en/2023/%E5%B8%B8%E8%A7%81%E7%AE%97%E6%B3%95%E7%B1%BB%E5%9E%8B%E5%85%AB%E7%9B%B8%E4%BC%BC/","title":"常见算法类型（八）相似"},{"content":"过滤器 过滤重复的数据 位图 布隆过滤器 过滤、拦截数据（如 短信、邮件等） 基于黑名单的过滤器 基于规则的过滤器 基于概率统计的过滤器 布隆过滤器 Bloom Filter 位图 Bit Map 位图是一种“特殊”的散列表，使用一个二进制位（bit）来表示一个布尔类型，通过位运算来实现对 bit 的使用 位图通过数组下标来定位数据，访问效率高，需要的内存小 1 Byte = 8 bit， 1 KB = 1024 B，1 MB = 1024 KB，1 GB = 1024 MB 位图与布隆过滤器 位图适合数据个数和数据范围的差值不大的情况 当差值过大时，位图需要的存储空间可能超过使用散列表存储的空间 若通过哈希函数对数字处理，使得数字落在一个合适的数据范围内，则可能出现哈希冲突 布隆过滤器 布隆过滤器是基于位图的改进，为了处理数据个数和数据范围的差值过大的情况 布隆过滤器存在误判，当判断不存在时一定不存在，而判断存在时可能存在，也可能不存在 布隆过滤器一般不用删除数据，如果要支持删除操作，可以额外使用其他的数据结构用来记录删除的数据 布隆过滤器 Bloom Filter 使用 K 个哈希函数，对同一个数字分别使用这 K 个哈希函数求哈希值，分别为 X1、X2、X3 \u0026hellip;. Xk，将求得的这 K 个哈希值的下标都置为 true，用 K 个二进制位来表示一个数字的存在 查询数字是否存在时，使用同样的 K 个哈希函数求哈希值，通过这 K 个哈希值对应位图中的数值是否都为 true，如果都是 true 则说明这个数字存在，如果其中有任意一个不为 true，则说明这个数字不存在 布隆过滤器会出现误判的情况，但只会对存在的情况发生误判。如果某个数字经过布隆过滤器判断不存在，则这个数字一定不存在，而某个数字经过布隆过滤器判断为存在，则这个数字可能存在，也可能不存在 降低布隆过滤器误判概率的方法 增加哈希函数的个数 增加位图的大小和存储数字的个数之间的比例 在数据个数与位图大小的比例超过一定阈值的时候，可以重新申请一个新的位图。当需要判断某个数据是否存在时，需要查看多个位图 布隆过滤器 vs 散列表 布隆过滤器使用多个哈希函数对同一个数据进行处理，CPU 只需要从内存中读取一次，进行多次哈希运算，属于 CPU 密集型操作 散列表中可能存在散列冲突，当出现散列冲突时，需要读取散列值相同的多个数据，并跟原数据进行对比，属于内存密集型操作 理论上来看，布隆过滤器比散列表更加快速 布隆过滤器的应用 判断是否存在 爬虫网页的去重 快速判断某个数字是否存在于数据中 统计大型网站的每日 UV 数，对重复访问的用户去重 过滤、拦截器 基于黑名单的过滤器 动态维护一个发送方信息的黑名单，即判断是否为黑名单的数据来源，如骚扰电话号码和垃圾短信发送号码、IP 地址等 若黑名单的数据量不大，可以使用散列表、二叉树等动态数据结构存储 若黑名单的数据量很大 使用布隆过滤器来减少所占用内存的大小，缺点是可能存在误判 将黑名单存储在服务器上，手机端将需要检查的号码发送给服务器端，完全不需要占用手机内存，缺点是必须联网工作，且网络延迟会降低处理速度 基于规则的过滤器 通过内容判断是否为垃圾信息，基于人工制定的规则 内容包含特殊单词 发送号码为群发号码 包含回拨的联系方式，如网页链接、QQ、微信等 格式花哨、内容很长，包含表情、图片、网页链接等 符合已知的垃圾内容的模版 结合多条规则综合来判断是否为垃圾信息 符合 N 条以上 每个规则对应不同的得分，总得分超过一定数值 基于概率统计的方法判断是否为垃圾信息 有大量的样本数据，且样本数据已经做好了标记，是否为垃圾信息 对样本信息进行分词处理，去掉“的、和、是”等没有意义的停用词（Stop Words），得到 n 个不同的单词 针对每个单词，统计此单词出现在有多少个垃圾信息中，即此单词出现在垃圾信息中的概率 如果此单词出现在垃圾信息中的概率远大于出现在非垃圾短信中的概率，则标记此单词为“垃圾单词” 缺点 人工制定的规则受人思维方式的限制，规则简单、局限 垃圾内容发送方可能会针对这些规则，精心设计内容，绕过这些规则的拦截 基于概率统计的过滤器 基于朴素贝叶斯算法 Native Bayesian Classification\n朴素贝叶斯算法 使用概率来表示一个内容是垃圾信息的可信程度，将内容抽象成一组计算机可识别且可计算的特征项，用这一组特征项来代替内容本身\n概率 利用朴素贝叶斯公式计算出概率\n独立事件发生的概率计算公式：P(A*B) = P(A)*P(B)\n如果事件 A 和事件 B 是独立事件，两者的发生没有相关性，事件 A 发生的概率 P(A) 等于 p1，事件 B 发生的概率 P(B) 等于 p2，那两个同时发生的概率 P(A*B) 就等于 P(A)*P(B)\n计算概率 计算概率 计算同时包含 W1，W2，W3，…，Wn， 这 n 个单词的内容，是垃圾信息的概率 P1 和非垃圾信息的概率 P2，利用 P1 和 P2 的比值来判断此内容是否为垃圾信息\nP（W1，W2，W3，…，Wn 同时出现在一条短信中） 在计算 P1 和 P2 的比值中抵消了\n计算概率 实际应用 可以结合上述三种不同的过滤拦截方式的结果，对同一个内容处理，如果三者都表明是垃圾信息，则过滤拦截 不断地调整策略 准确率，是否会把不是垃圾的短信错判为垃圾短信 召回率，是否能把所有的垃圾短信都找到 ","date":"2023-08-24T12:17:11+08:00","permalink":"https://boxtsecond.github.io/en/2023/%E5%B8%B8%E8%A7%81%E7%AE%97%E6%B3%95%E7%B1%BB%E5%9E%8B%E4%B8%83%E8%BF%87%E6%BB%A4/","title":"常见算法类型（七）过滤"},{"content":"二分查找 思想说明 针对有序数据集合的查找算法，依赖于顺序表结构，即数组 每次通过和区间的中间元素对比，将待查找的区间缩小为之前的一半，直到找到要查找的元素，或区间被缩小至 0 分析 时间复杂度 O（log(n)） 代码实操注意事项 循环退出条件 low \u0026lt;= high mid 的取值，如果 low 和 high 比较大的话，两者之和可能会溢出，改进为：low + ( high - low ) / 2，更进一步，将性能优化到极致 low + (( high - low) \u0026raquo; 1) low 和 high 的更新，low = mid + 1，high = mid - 1 可以使用循环和递归两种实现方式 应用场景 只能应用在数据是通过顺序表来存储的数据结构上 只能应用在有序数据，且插入、删除操作不频繁，一次排序多次查找的场景 数据量的要求 数据量太小不适合二分查找，直接顺序遍历就足够，但如果数据之间的比较操作非常耗时，使用二分查找尽可能地减少比较次数，例如 数组中存储的都是长度超过 300 的字符串 数据量太大也不适合二分查找，二分查找的底层需要依赖数组这种数据结构，而数组为了支持随机访问的特性，要求内存空间连续 非常适合用在“近似”查找问题 凡是用二分查找能解决的，绝大部分更倾向于用散列表或者二叉树查找 常见的二分查找变形问题 查找第一个值等于给定值的元素 当 a[mid] \u0026gt; value，high = mid - 1 当 a[mid] \u0026lt; value，low = mid + 1 当 a[mid] == value 如果 mid == 0，或者 a[mid - 1] != value，则 mid 就是第一个等于给定值的元素 如果不是，则继续 high = mid - 1，要找的元素一定在 [ low，mid - 1] 中 查找最后一个值等于给定值的元素 当 a[mid] \u0026gt; value，high = mid - 1 当 a[mid] \u0026lt; value，low = mid + 1 当 a[mid] == value 如果 mid == n - 1，或者 a[mid + 1] != value，则 mid 就是最后一个值等于给定值的元素 如果不是，则继续 low = mid + 1，要找的元素一定在 [ mid + 1， high ] 中 查找第一个大于等于给定值的元素 当 a[mid] \u0026gt;= value 如果 mid == 0，或者 a[mid + 1] \u0026lt; value，则 mid 就是第一个大于等于给定值的元素 如果不是，则继续 high = mid - 1，要找的元素一定在 [ low，mid - 1 ] 中 当 a[mid] \u0026lt; value，low = mid + 1 查找最后一个小于等于给定值的元素 当 a[mid] \u0026lt;= value 如果 mid == n - 1，或者 a[mid + 1] \u0026gt; value，则 mid 就是最后一个小于等于给定值的元素 如果不是，则继续 low = mid + 1，要找的元素一定在 [ mid + 1，high ] 中 当 a[mid] \u0026gt; value，high = mid - 1 跳表 思想说明 将原始有序链表每 n 个结点就提取一个结点到上层索引，添加多层索引 链表 ➕ 多级索引的结构，就是跳表 分析 查找、插入、删除操作，时间复杂度均为 O（log(n)） 查找时间复杂度 假设每 m 个结点抽出一个结点作为上一级索引的结点 第一级索引的结点个数大约为 n / m，第二级索引的结点个数 n / m，第 k 级索引的结点个数是 n / （）， 假设 h 级索引，最高级索引有 m 个结点， n / （） = m，，整个跳表的高度就是 时间复杂度 = 每层需要遍历的结点数 x 跳表的高度 = O（m * ） = O（log(n)） 空间复杂度 O（n） 假设每 m 个结点抽出一个结点作为上一级索引的结点 索引结点的总和就是 n / m + n / + n / + \u0026hellip;.+ + m 在实际的软件开发中，原始链表中存储的有可能是很大的对象，而索引结点只需要存储关键值和几个指针，并不需要存储对象，所以当对象比索引结点大很多时，索引占用的额外空间就可以忽略 索引动态更新 当不停地往跳表中插入数据是，如果不更新索引，就有可能出现某 2 个索引结点之间数据非常多的情况 通过一个随机函数，来决定将这个结点插入到哪几级索引中，譬如随机函数生成了值 k，就将这个结点添加到 第 1 级至第 k 级 这 k 级索引中 应用场景 Redis 中的有序集合，使用跳表、散列表来实现 使得【按照区间查找数据】功能，可以在时间复杂度 O（log(n)）定位区间的起点，然后在原始链表中顺序往后遍历就可以了 跳表更为灵活，可以通过改变索引构建策略，有效平衡执行效率和内存消耗 跳表的代码实现更加容易 散列表 思想说明 散列表是一种数组的扩展，用的是数组支持按照下表随机访问数据的特性 关键字 / 键 key 标识数据 散列函数（Hash 函数） 将数据转化为数组下标的映射方法 散列值 由散列函数计算得到的值 装载因子（load factor） 表示空位的多少 散列表的装载因子 = 填入表中的元素个数 / 散列表的长度 装载因子越大，说明空闲位置越少，冲突越多 散列函数设计的基本要求 散列函数计算得到的散列值是一个非负整数 如果 key1 = key2，那么 hash( key1 ) == hash( key2 ) 如果 key1 ≠ key2，那么 hash( key1 ) ≠ hash( key2 ) 在真实情况下，要想找到一个不同的 key 对应的散列值都不一样的散列函数，几乎是不可能的，存在散列冲突 由于数组的存储空间有限，也会加大散列冲突的概率 常见的解决散列冲突的办法 开放寻址法 线性探测（Linear Probing）\n插入：如果出现了散列冲突，重新探测一个空闲位置，将其插入 查找：通过散列函数求出要查找元素的键值对应的散列值，比较数组中下标为散列值的元素和要查找的元素，如果相等，则说明找到了，否则就顺序往后依次查找，如果遍历到数组中的空闲位置还没有找到，则说明查找的元素不存在散列表中 删除：将删除的元素标记为 deleted，当线性探测查找时，遇到标记为 deleted 的空间，并不是停下来，而是继续往下探测 最好时间复杂度 O（1），最坏时间复杂度 O（n） 二次探测（Quadratic Probing）\n类似线性探测，线性探测每次探测的步长是 1，hash( key ) + 0，hash( key ) + 1，，hash( key ) + 2 二次探测每次探测的步长是原来的平方，hash( key ) + 0，hash( key ) + 1^2，hash( key ) + 2^2 双重散列（Double Hashing）\n使用一组散列函数，先用第一个散列函数，如果计算得到的存储位置已经被占用，再用第二个散列函数，依此类推，直到找到空闲的存储位置 链表法 在散列表中，每个“桶（bucket）”或者“槽（slot）”会对应一条链表，所有散列值相同的元素放在相同槽位对应的链表中 插入：通过散列函数计算出对应的散列槽位，将其插入到对应链表中，时间复杂度为 O（1） 查找、删除：通过散列函数计算得出对应的散列槽位，遍历链表查找或删除，时间复杂度跟链表的长度 k 成正比，O（k），对于散列比较均匀的散列函数来说，k = n / m，n 为散列表中数据的个数，m 为散列表中“槽”的个数 设计工业级散列表 设计要求 避免在散列冲突的情况下，性能急剧下降 能抵抗散列碰撞攻击 散列函数的设计 散列函数的设计不能太复杂 散列函数生成的值要尽可能随机并且均匀分布 需要综合考虑各种因素，如 关键字的长度、特点、分布、散列表的大小等 散列函数的常见设计：数据分析法、直接寻址法、平方取中法、折叠法、随机数法 等 装载因子，动态扩容策略 装载因子越大，说明散列表中元素越多，空闲位置越少，散列冲突的概率越大 对于没有频繁插入和删除的静态数据集合，根据数据的特点、分布等，很容易设计出极少冲突的散列函数 对于动态散列表，数据集合是频繁变动的，当装载因子过大时，可以进行动态扩容，重新申请一个更大的散列表，将数据搬移到新散列表中 插入操作，最好时间复杂为 O（1），最坏时间复杂度为 O（n），均摊时间复杂度为 O（1） 如果对空间消耗比较敏感，可以在装载因子小于某个值后，启动动态缩容 如果对效率比较敏感，可以容忍多消耗一点内存空间，就不需要缩容 装载因子阈值的设置要权衡时间、空间复杂度 内存空间不紧张，对执行效率要就很高，可以降低装载因子的阈值 内存空间紧张，对执行效率要求不高，可以增加装载因子的阈值，甚至可以大于 1 如何避免低效地扩容 当极个别非常慢的插入操作（扩容并搬移数据）不能被容忍时，“一次性”扩容机制不能满足要求 可以将扩容操作穿插在插入操作的过程中，分批完成 当装载因子达到阈值时，只申请新空间，并不将老的数据搬移至新散列表中 当有新数据要插入时，将新数据插入新散列表中，并且从老的散列表中拿出一个数据放入到新散列表中 对于查询操作，为了兼容新、老散列表中的数据，先从新散列表中查找，如果没有找到，再去老的散列表中查找 如何选择冲突解决方法 开放寻址法 优点 散列表中的数据都存储在数组中，可以有效地利用 CPU 缓存加快查询速度 序列化简单 缺点 删除数据时，需要特殊标记已经删除掉的数据 所有的数据都存储在一个数组中，冲突的代价更高 在使用开放寻址法解决冲突的散列表中，装载因子的上限不能太大，更浪费内存空间 适合场景 数据量比较小、装载因子小 例如 Java 中的 ThreadLocalMap 链表法 优点 对大装载因子的容忍度更高 可以对链表法中的链表改造为其他更为高效的动态数据结构，如 跳表、红黑树，即便出现散列冲突，在极端情况下，所有的数据都散列到一个桶内，最终退化成的散列表的查询时间为 O（logn），有效地避免了散列碰撞攻击 缺点 链表需要存储指针，对于比较小的对象的存储，比较消耗内存 链表中的节点是零散分布在内存中的，不是连续的，对 CPU 缓存不友好，如果是存储大对象的话，指针的内存消耗可以忽略不计 适用场景 存储大对象、大数据量，更为灵活，可以支持更多的优化策略 工业级散列表 Java HashMap 分析 初始大小 默认是 16，如果事先知道数据量的大概范围，可以通过修改默认初始值，减少动态扩容的次数，提高 HashMap 的性能 装载因子和动态扩容 最大装载因子默认是 0.75，当 HashMap 中元素个数超过 0.75 x capacity，就会启动扩容，每次扩容都会扩容原来的两倍 散列冲突解决方法 采用链表法来解决冲突 当链表长度太长（默认超过 8）时，链表就转换成红黑树 散列函数 简单高效、分布均匀 特性 支持快速的查询、插入、删除操作 内存占用合理，不能浪费过多的内存空间 性能稳定，极端情况下，散列表的性能也不会退化到无法接受的情况 散列表和链表的组合使用 LRU 缓存淘汰算法（Least Recently Used） 借助散列表，将 LRU 缓存淘汰算法的时间复杂度降低为 O（1） 使用双向链表存储数据，链表中每个节点处理存储数据 data、前驱指针 prev、后继指针 next、散列表的拉链指针 hnext，前驱和后继指针是为了将节点串在双向链表中，hnext 指针是为了将结点串在散列表的拉链中 查找数据：在散列表中查找，当找到数据后，将它移动到双向链表的尾部 删除数据：查找数据并将结点删除 添加数据：先看下数据是否存在缓存中，如果已经存在，则将它移动到双向链表的尾部，如果不在其中，查看缓存是否已经满了，如果满了，则将双向链表的头结点删除，再将数据放到链表的尾部，如果没有满，则直接将数据放到链表的尾部 Redis 有序集合 按照分值将成员对象组织成跳表的结构 按照键值构建一个散列表 Java LinkedHashMap 通过双向链表和散列表两种数据结构组合实现 LinkedHashMap 中的 “Linked”实际上指的是双向链表，并非用链表法解决散列冲突 散列表和链表经常一起使用的原因 散列表的数据结构支持非常高效的插入、删除、查找操作，但是无法支持按照某种顺序快速地遍历数据 为了能够按照顺序遍历散列表中的数据时，将散列表和链表（或者跳表）结合在一起使用 哈希算法 将任意长度的二进制值串映射为固定长度的二进制值串，其中映射的规则就是哈希算法，通过原始数据映射之后得到的二进制值串就是哈希值 哈希算法的要求 从哈希值不能反向推导出原始数据，即单向 对输入数据非常敏感，哪怕原始数据的改动微小，最后得到的哈希值也大不相同 散列冲突的概率要很小 哈希算法的执行效率要尽量高效，针对较长的文本，也能快速地计算出哈希值 哈希算法的应用 安全加密 MD5（Message-Digest Algorithm，MD5 信息摘要算法） SHA（Secure Hash Algorithm，安全散列算法） DES（Data Encryption Standard，数据加密标准） AES（Advanced Encryption Standard，高级加密标准） 着重注意点 单向推导，不能根据哈希值反向推导出原始数据 散列冲突的概率很小 唯一标识 图片的信息摘要，将图片的二进制码串的开头取一百个字节，中间一百个字节，结尾再取一百个字节，将这三百个字节通过哈希算法（例如 MD5），得到一个哈希字符串，用它作为图片的唯一标识 数据校验 下载的文件块校验，对文件块分别取哈希值，并且保存在种子文件中。当文件块下载完成之后，可以通过相同的哈希算法，对下载好的文件块逐一求哈希值，跟种子文件中保存的哈希值对比 散列函数 散列函数更注重数据能否均匀地散列在各个槽中和散列函数执行的效率、性能 对于散列冲突，使用开放寻址法或者链表法解决 负载均衡 会话粘滞的负载均衡算法 在同一个客户端上，再一次会话中的所有请求都路由到同一个服务器上 简单粗暴法 维护一张映射关系表，客户端 IP 地址或者会话 ID 与服务器编号的映射关系 弊端 客户端很多，映射表会很大，浪费内存空间 客户端上下线、服务器扩容缩容都会导致映射失效，维护映射表的成本会增大 哈希算法 对客户端 IP 地址或者会话 ID 计算哈希值 将取得的哈希值与服务器列表的大小进行取模运算，得到应该被路由到的服务器编号 将同一个 IP 过来的所有请求，都路由到同一个后端服务器上 数据分片 如何统计“搜索关键字” 难点 搜索日志很大，没办法放在一台机器的内存中 如果只用一台机器处理，处理时间会很长 解决方法（MapReduce） 对数据进行分片，采用 n 台机器处理 从搜索日志中依次读出每个搜索关键词，通过哈希函数计算哈希值，对 n 取模，最终得到的值，就是被分配到的机器编号 哈希值相同的搜索关键词被分配到了同一台机器上 如何快速判断图片是否在图库中 难点 图片的数量达到一定规模后，没办法在单台机器上构建散列表 解决方法 对数据进行分片，采用 n 台机器处理，每台机器只维护某部分图片对应的散列表 从图库中读取一个图片，计算唯一标识，与 n 求余取模，得到对应要分配的机器编号，然后将图片的唯一标识和图片路径发送到对应的机器上构建散列表 当要判断图片是否在图库中时，首先通过哈希算法，计算图片的唯一标识，然后与 n 求余取模，到对应机器上构建的散列表中查找 分布式存储 使用数据分片的思想，即通过哈希算法对数据取哈希值，然后对机器个数取模，这个最终值就是应该存储的缓存机器编号 考虑到数据会持续增多，当需要扩容时，需要一种方法，在新加入一台机器后，不需要做大量的数据搬移 使用一致性哈希算，n 台机器，数据的哈希值范围是 [0, MAX]。将整个范围划分为 m 个小区间，m 远大于 n，每个机器负责 m / n 个小区间。这样当有新机器加入时，将某几个小区间的数据，从原来的机器上搬移到新的机器中。 参考资料 https://en.wikipedia.org/wiki/Consistent_hashing http://www.zsythink.net/archives/1182 ","date":"2023-08-24T12:06:40+08:00","permalink":"https://boxtsecond.github.io/en/2023/%E5%B8%B8%E8%A7%81%E7%AE%97%E6%B3%95%E7%B1%BB%E5%9E%8B%E5%85%AD%E6%9F%A5%E6%89%BE/","title":"常见算法类型（六）查找"},{"content":"各个排序算法间的比较 排序算法 如何优化快速排序 快速排序的最坏时间复杂度是 O（n^2），出现最坏时间复杂度的原因是分区点选择的不够合理 最理想的分区点：被分区点分开的两个分区中，数据的数量差不多 比较常见的分区算法 三数取中法 取区间的首、尾、中间 三个数的中间值作为分区点 如果排序的数组比较大，“三数取中法”可能不够，需要“五数取中”或者“十数取中” 随机法 每次从要排序的区间中，随机选择一个元素作为分区点 快速排序使用递归实现，要警惕堆栈溢出 限制递归深度，一旦递归过深，停止递归 在堆上模拟实现一个函数调用栈，手动模拟递归压栈、出栈的过程 ","date":"2023-08-24T12:02:21+08:00","permalink":"https://boxtsecond.github.io/en/2023/%E5%B8%B8%E8%A7%81%E7%AE%97%E6%B3%95%E7%B1%BB%E5%9E%8B%E4%BA%94%E6%8E%92%E5%BA%8F%E7%AE%97%E6%B3%95%E5%AF%B9%E6%AF%94/","title":"常见算法类型（五）排序算法对比"},{"content":"桶排序 Bucket Sort 思想说明 桶排序只包含一种操作，元素的移动，不涉及元素之间的比较操作 将要排序的数据分到几个有序的桶里，每个桶里的数据单独进行排序 桶内排序完成后（归并排序），把每个桶里的数据按照顺序依次取出 分析 空间复杂度 O（n），不是原地排序算法 最好时间复杂度 O（n），最坏时间复杂度 O（nlog(n)） 是稳定的排序算法 应用场景分析 排序的数据需要很容易的划分成 m 个桶 桶和桶之间有天然的大小关系 数据在各个桶之间的分布比较均匀 比较适合用在外部排序中，因数据量较大，内存有限而无法全部加载在内存中 计数排序 Counting Sort 思想说明 计数排序只包含一种操作，元素的移动，不涉及元素之间的比较操作 排序的数据划分成 m 个桶，每个数据对应的值为一个桶，每个桶中存放的是这个值对应的数据个数 将桶中存放的数据个数依次相加，得到位置数组 创建和原数据大小一致的数组，从后到前遍历原数据，将数据对应的值的桶中存放的数据个数取出（x），放在创建数组 x-1 的位置上，并将桶中的值 - 1 分析 空间复杂度 O（n），不是原地排序算法 时间复杂度 O（n） 是稳定的排序算法（从后到前遍历原数据） 应用场景分析 排序的数据范围比较小 只能给非负整数排序，如果要排序的数据是其他类型，要在不改变相对大小的情况下，转化为非负整数 基数排序 Radix Sort 思想说明 将要排序的数据按“位”分割 从后往前的按照“位”来排序数据，排序数据的算法必须是稳定的 分析 空间复杂度 O（1），是原地排序算法 最好时间复杂度 O（n）（“位”次桶排序或计数排序） 是稳定的排序算法 应用场景分析 要排序的数据可以分割出独立的“位”来比较， “位”之间有递进的关系，若 a 数据的高位比 b 数据的大，则剩下的低位不用比较 每一“位”的数据范围不能过大，要可以用线性排序算法来排序 ","date":"2023-08-24T11:59:04+08:00","permalink":"https://boxtsecond.github.io/en/2023/%E5%B8%B8%E8%A7%81%E7%AE%97%E6%B3%95%E7%B1%BB%E5%9E%8B%E5%9B%9B%E6%8E%92%E5%BA%8F-on%E6%A1%B6%E6%8E%92%E5%BA%8F%E8%AE%A1%E6%95%B0%E6%8E%92%E5%BA%8F%E5%9F%BA%E6%95%B0%E6%8E%92%E5%BA%8F/","title":"常见算法类型（四）排序 O(n)：桶排序、计数排序、基数排序"},{"content":"归并排序（Merge Sort） 归并排序 思想说明 归并排序包含两种操作，一种是元素的比较，一种是元素的移动 使用分治思想，分而治之，将一个大问题分解成小的字问题来解决 将数组从中间分成前后两部分，并对前后两部分分别排序 不断重复 b，直到数组分解完成（归） 将排好序的两部分合并在一起（并） 分析 空间复杂度 O（n log(n)），不是原地排序算法 最好、最坏、平均时间复杂度均为 O（n log(n)） 不是稳定的排序算法 快速排序（Quick Sort） 快速排序 思想说明 快速排序包含两种操作，一种是元素的比较，一种是元素的移动 选择数组中的任意一个元素作为 pivot（分区点） 遍历数组，将小于 pivot 的元素放在左边，将大于 pivot 的元素放在右边，pivot 放在中间，此时数组分成了三个部分 不断重复 2，直到区间缩小为 1 分析 空间复杂度 O（1），是原地排序算法 最好时间复杂度 O（n log(n)），最坏时间复杂度 O（n^2）（概率很小），平均时间复杂度 O（n log(n)） 是稳定的排序算法 归并 vs 快排 堆排序 堆 Heap\n","date":"2023-08-24T11:31:06+08:00","permalink":"https://boxtsecond.github.io/en/2023/%E5%B8%B8%E8%A7%81%E7%AE%97%E6%B3%95%E7%B1%BB%E5%9E%8B%E4%B8%89%E6%8E%92%E5%BA%8F-onlogn%E5%BD%92%E5%B9%B6%E6%8E%92%E5%BA%8F%E5%BF%AB%E9%80%9F%E6%8E%92%E5%BA%8F%E5%A0%86%E6%8E%92%E5%BA%8F/","title":"常见算法类型（三）排序 O(nlogn)：归并排序、快速排序、堆排序"},{"content":"冒泡排序 Bubble Sort 冒泡排序 思想说明\n冒泡排序中包含两种操作，一种是元素的比较，一种是元素的移动 冒泡排序只会操作相邻的两个数据，每次冒泡排序操作都会对相邻的两个元素进行比较，若不满足大小关系要求，则互换 一次冒泡至少会让一个元素移动到它应该在的位置，重复 n 次，就完成了 n 个数据的排序工作 分析\n空间复杂度 O（1），是原地排序算法 最好时间复杂度 O（n），最坏时间复杂度 O（n^2），平均时间复杂度 O（n^2） 是稳定的排序算法 插入排序 Insertion Sort 插入排序 思想说明\n插入排序包含两种操作，一种是元素的比较，一种是元素的移动 将数组中的数据分为两个区间，已排序区和未排序区，动态地往有序集合中添加数据 取未排序区间中的元素，在已排序区间中找到合适的插入位置将其插入，并保证已排序区间数据一直有序 分析\n空间复杂度 O（1），是原地排序算法 最好时间复杂度 O（n），最坏时间复杂度 O（n^2），平均时间复杂度 O（n^2） 是稳定的排序算法 选择排序 Selection Sort 选择排序 思想说明 选择排序包含两种操作，一种是元素的比较，一种是元素的移动 将数组中的数据，分为已排序区间和未排序区间 每次从未排序区间中找到最小的元素，放在已排序区间的末尾 分析 空间复杂度 O（1），是原地排序算法 最好时间复杂度 O（n^2），最坏时间复杂度 O（n^2），平均时间复杂度 O（n^2） 不是稳定的排序算法 ","date":"2023-08-24T11:25:44+08:00","permalink":"https://boxtsecond.github.io/en/2023/%E5%B8%B8%E8%A7%81%E7%AE%97%E6%B3%95%E7%B1%BB%E5%9E%8B%E4%BA%8C%E6%8E%92%E5%BA%8F-on2%E5%86%92%E6%B3%A1%E6%8E%92%E5%BA%8F%E6%8F%92%E5%85%A5%E6%8E%92%E5%BA%8F%E9%80%89%E6%8B%A9%E6%8E%92%E5%BA%8F/","title":"常见算法类型（二）排序 O(n^2)：冒泡排序、插入排序、选择排序"},{"content":"如何分析一个排序算法 排序算法的执行效率 最好情况、最坏情况、平均情况时间复杂度 时间复杂度的系数、常数、低阶 比较次数交换（或移动）次数 排序算法的内存消耗 原地排序，特指空间复杂度是 O（1）的排序算法 排序算法的稳定性 如果待排序的序列中存在值相等的元素，经过排序之后，相等元素之间原有的先后顺序不变，则此排序算法是稳定的 平均情况时间复杂度的计算 有序度 数组中具有有序关系的元素对的个数\n有序度 完全有序的数组的有序度叫做满有序度，n*(n-1)/2，如 1，2，3，4，5\n逆序度 逆序度 = 满有序度 - 有序度 排序算法 O(n^2)：冒泡排序、插入排序、选择排序\nO(nlogn)：归并排序、快速排序、堆排序\nO(n)：桶排序、计数排序、基数排序\n排序算法对比\nTopological Sorting 拓扑排序\n","date":"2023-08-24T10:25:22+08:00","permalink":"https://boxtsecond.github.io/en/2023/%E5%B8%B8%E8%A7%81%E7%AE%97%E6%B3%95%E7%B1%BB%E5%9E%8B%E4%B8%80%E6%8E%92%E5%BA%8F/","title":"常见算法类型（一）排序"},{"content":"字符串的相似度 编辑距离（Edit Distance） 将一个字符串转化成另一个字符串，需要的最少编辑次数（增加、删除、替换 一个字符） 编辑距离越大，说明两个字符串的相似程度越小，对于两个完全相同的字符串，它们的编辑距离为 0 编辑距离的常见计算方式 莱文斯坦距离（Levenshtein Distance） 允许 添加、删除、替换 字符 表示两个字符串差异的大小 最长公共子串长度（Longest Common Substring Length） 允许 添加、删除 字符 表示两个字符串相似程度的大小 计算莱文斯坦距离 如果 a[i] == b[j]，则继续匹配 a[i+1] 和 b[j+1]\n如果 a[i] != b[j]\n删除 a[i]，匹配 a[i+1] 和 b[j] 删除 b[j]，匹配 a[i] 和 b[j+1] 在 a[i] 前添加一个和 b[j] 相同的字符，匹配 a[i] 和 b[j+1] 在 b[j] 前添加一个和 a[i] 相同的字符，匹配 a[i+1] 和 b[j] 将 a[i] 替换成 b[j]，或 b[j] 替换成 a[i] ，匹配 a[i+1] 和 b[j+1] 状态转移方程法\n状态 （i，j）可以通过（i-1，j）、（i，j-1）、（i-1，j-1）三个状态中的任意一个转移过来 a[i] == b[j]，min_edist（i，j）=min（min_edist（i-1，j）+1，min_edist（i-1，j-1）+1，min_edist（i，j-1）+1） a[i] != b[j]，min_edist（i，j）=min（min_edist（i-1，j）+1，min_edist（i-1，j-1），min_edist（i，j-1）+1） 状态转移表法\n状态转移表法 计算最长公共子串长度 如果 a[i] == b[j]，则继续匹配 a[i+1] 和 b[j+1] 如果 a[i] != b[j] 删除 a[i]，匹配 a[i+1] 和 b[j] 删除 b[j]，匹配 a[i] 和 b[j+1] 在 a[i] 前添加一个和 b[j] 相同的字符，匹配 a[i] 和 b[j+1] 在 b[j] 前添加一个和 a[i] 相同的字符，匹配 a[i+1] 和 b[j] 状态转移方程法 状态 （i，j）可以通过（i-1，j）、（i，j-1）、（i-1，j-1）三个状态中的任意一个转移过来 a[i] == b[j]，max_edist（i，j）=max（max_edist（i-1，j），max_edist（i-1，j-1）+1，max_max（i，j-1）） a[i] != b[j]，max_edist（i，j）=max（max_edist（i-1，j），max_edist（i-1，j-1），max_edist（i，j-1）） 拼写纠错的优化 取出编辑距离最小的 TOP 10，根据其他参数决策出选择哪个单词作为拼写纠错单词。如 根据搜索热门程度。 使用多种编辑距离计算方法，分别求编辑距离最小的 TOP 10，然后求交集。 统计用户的搜索日志，得到最常被拼错的单词列表，以及对应的拼写正确的单词。之后在拼写纠错时，先到这个单词列表中查找。 针对每个用户都维护此用户的常用搜索关键词。当用户输入错误时，有现在常用的搜索关键词列表中计算编辑距离，查找编辑距离最小的单词。 ","date":"2023-08-24T10:20:14+08:00","permalink":"https://boxtsecond.github.io/en/2023/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E5%8D%81%E5%AD%97%E7%AC%A6%E4%B8%B2%E7%9B%B8%E4%BC%BC%E5%BA%A6/","title":"数据结构（十）字符串相似度"},{"content":"Brute Force 暴力匹配算法/朴素匹配算法 思想 在主串中，匹配模式串。若主串的长度是 n，模式串的长度是 m，则 n \u0026gt;= m。主串可以按照模式串的长度 m，得出 n - m + 1 个子串。 将主串中起始位置为 0，长度为 m 的子串 x，与模式串的每一位字符依次比较。若相等则继续，不相等则进行下一步。 若不匹配，则将主串中的起始位置加 1。循坏执行上一步，直到主串的起始位置大于 n - m + 1 或 匹配成功。 时间复杂度：O（n*m） 优点 思路简单，代码易实现，维护简单，是最常见的处理小规模的字符串匹配算法 在实际应用中，时间复杂度往往远低于 O（n*m） Rabin-Karp 思想 在 BF 算法上进行优化，利用哈希算法减少匹配时的时间复杂度，但是匹配的次数没有减少。 将主串中得出的 n - m + 1 个子串分别求哈希值。依次和模式串的哈希值比较。 有技巧地计算子串的哈希值 假设要匹配的字符串的字符集中包含 k 个字符，使用 k 进制来表示一个子串。\n将代表子串的 k 进制数字转换成十进制。\n在相邻的两个子串的哈希值计算公式存在以下关系：\nrabin-karp B = （A - 第一位的 k 进制值） * k + 最后一位的 k 进制值 第一位的 k 进制 = （km-1）* 此字符表示的 k 进制的数字 km 可以提前算好存储在数组中，下标为 m 的值为 km 若可能存在哈希冲突，则将哈希值相等的子串和模式串再一次进行比较 时间复杂度：O（n），当可能存在大量的哈希冲突时，则退化为 O（n*m） 缺点 可能会有散列冲突 Boyer-Moore BM 算法 目的 尽可能地一次性跳过多个字符，从后往前的比较字符，用来减少 BF（暴力匹配）算法的匹配次数，以提升算法的效率 思想 坏字符规则\n坏字符规则 从后往前的将模式串和主串进行匹配 找到主串“坏字符”对应下标 若存在不匹配的字符，将主串中对应的字符标记为“坏字符”，并记录“坏字符”的位置为 si 找到模式串中另一个与主串中“坏字符”相等的字符 在模式串中查找是否存在另一个与主串中的“坏字符”相等的字符 若模式串中存在一个或多个与主串中“坏字符”相等的字符，则记录下最后出现的位置为 xi（以免跳过太多字符，导致错过可以正确匹配的字符），跳过 si - xi 个字符 若不存在，则直接跳过模式串长度个字符 注意事项 si - xi 有可能出现负数的情况 例如：主串为 aaaaaa，模式串为 baa，当比较模式串中的 b 时，主串中“坏字符”的位置为 0，模式串中的位置为 2（取最后一个），则跳过字符的个数为 0 - 2 = -2 为了避免出现跳跃负数个字符，还需要“好后缀规则” 好后缀规则\n从后往前的匹配模式串和主串\n找到主串中的“好后缀”{u}\n某个不匹配字符之后的字符串，就是“好后缀”{u} 找到模式串中另一个与主串中“好后缀”相等的字符串\n在模式串中查找是否存在与主串中“好后缀”相等的字符串\n如果存在一个或多个与主串中“好后缀”相等的字符串，则找到最后一个相等的字符串 {u*}，将 {u*} 跳跃对应到 {u} 的位置\n好后缀规则 如果模式串中不存在与“好后缀”相等的字符串，则需要判断匹配到的好后缀中，在模式串中是否存在相等的前缀子串\n如果模式串中存在后缀子串和前缀子串相等的字符串，则跳跃到前缀子串最长能匹配到的位置\n如果模式串中不存在后缀子串和前缀子串相等的字符串，则跳跃模式串长度个字符\n好后缀规则 注意事项\n当坏字符规则和好后缀规则同时存在时，可判断这两种情况下得出的跳跃字符个数，选择较大的跳过。可以避免坏字符规则出现负数的情况。 当内存条件受限时，可只采用好后缀规则。 应用 文本中的查找功能 linux grep 的功能 BM 算法的代码分析 坏字符规则 需要找到“坏字符”在模式串中最后出现的位置，即 xi 每次将“坏字符”在模式串中进行顺序查找（效率太低） 使用散列表存储模式串每个字符出现的最后位置，可以使用数组或者对象，默认值是 -1，即在模式串中没有此字符 好后缀规则 需要找到“好后缀”在模式串中，除本身外最后出现的位置\n使用 suffix 数组，数组的下标表示“好后缀”的长度，数组中的值表示除本身外最后出现的位置\n好后缀 需要找到与模式串的前缀子串与模式串后缀子串重合的位置\n使用 prefix 数组，数组的下标表示后缀子串的长度，数组中的值表示是否存在后缀子串与前缀子串相等的字符串\n好后缀 如果 suffix 对应后缀子串长度的值不为 -1，则表示模式串中存在（除本身之外）与“好后缀”相等的字符串，则将模式串中最后出现的“好后缀”字符串对应到主串中的“好后缀”字符串\n若主串中“坏字符”位置为 j，模式串的长度为 m 主串中“好后缀”字符串的起始位置为 j + 1，“好后缀”长度为 k，k = m - j - 1 需要移动的位数为：j + 1 - suffix[k] 如果 suffix 对应后缀子串长度的值为 -1，即模式串中不存在（除本身之外）与“好后缀”相等的字符串，则判断 prefix 数组中，下标为 [ 0～好后缀长度 ] 的值\n倒序查找下标为 [ 0～好后缀长度 ]，找到符合条件最长的前缀子串 如果 prefix 对应后缀子串长度的值为 false，则表示模式串的前缀子串不存在和后缀子串重合的字符串，则移动的位数为：模式串的长度 m 如果 prefix 对应后缀子串长度的值为 true，即模式串的前缀子串存在和后缀子串重合的字符串，则移动的位数为：模式串的长度 m - prefix 的下标 如何填充 suffix 和 prefix 数组\n模式串的长度为 m，则模式串为 B [ 0，m - 1 ] 将 B [ 0，m - 1 ] 分别与 B [ 0，i ] 求公共后缀子串，i 的取值为 0 ～ m - 2 如果公共后缀子串的长度为 k，则 suffix[k] = j，j 表示公共后缀子串的起始位置 如果 j = 0，则表示模式串的后缀子串和前缀子串有重合，prefix[k] = true 好后缀 参考资料 http://www.cs.jhu.edu/~langmea/resources/lecture_notes/boyer_moore.pdf\nKnuth Morris Pratt KMP 算法 目的\n尽可能地一次性跳过多个字符，从前往后的比较字符，用来减少 BF（暴力匹配）算法的匹配次数，以提升算法的效率 在已经匹配的前缀子串 A 中，看看 A 的后缀子串有没有和 A 的前缀子串重合，如果有的话，重合的部分可以不用比较 思想\n从前往后的将模式串与主串进行匹配 当出现不能匹配的“坏字符”时，将之前可以匹配到的字符串称为“好前缀” 如果“好前缀”中的后缀子串和模式串的前缀子串有重合，则可以跳过重合的部分，继续依次比较后面的字符。将“好前缀”后缀子串中最长可匹配模式串的前缀子串，称为“最长可匹配后缀子串”，对应的前缀子串称为“最长可匹配前缀子串” kmp kmp KMP 算法的代码分析 求模式串中前缀子串的“最长可匹配前缀子串”\n使用 next 数组（失效函数），数组的下标代表模式串的前缀子串的长度，数组中的值为这个前缀子串对应的“最长可匹配前缀子串”的结尾字符下标（或这个前缀子串的“最长可匹配前缀子串”的长度 - 1）\nkmp 利用动态规划来求 next 数组\n若 next[i - 1] = k - 1，即 B[0，i - 1] 对应的“最长可匹配好前缀”为 B[0，k - 1] 当 B[i] == B[k] 时，则 next[i] = next[i - 1] + 1，即 next[i] = k 当 B[i] != B[k] 时 假设 B[i - 1] 存在一个“可匹配好后缀”（不是“最长可匹配好后缀”）为 B[x，i - 1]，则对应的“可匹配前缀子串”为 B[0，i - 1 - x]。如果 B[i] == B[i - x]，则说明 B[x，i] 是 B[0，i] 的“最长可匹配后缀子串” 如果不存在 B[i] == B[i - x]，则说明 next[i] = -1，即不存在“最长可匹配前缀子串” 将模式串与主串依次匹配，当出现“坏字符”时，获取对应“好前缀”的 next 数组（失效函数）的值 n，主串的匹配位置不变，将模式串移动 n 位，继续匹配\nTrie 树（字典树） 一组字符串复用公共前缀后，所构成的树形结构，当单个字符串结束时，需要将节点标识为结束（下图中的红色节点）\ntrie 适用场景\n在确定的一组字符集中，频繁查找或匹配不同的字符串，如 关键字检测 在确定的一组字符集中，查找或匹配符合前缀的字符串，如 搜索框自动补全 字符集要求\n字符集中，字符串的公共前缀重合较多 Trie 树 的操作 构建 Trie 树 根结点不存放任何字符，仅作为开始节点 根据字符集中的字符串，存储为多叉树，相同公共前缀的字符串拥有相同的父节点 存储为多叉树的方法有很多，如 有序数组、跳表、红黑树等 将字符串的最后一个字符，标识为结束节点，结束节点可以是叶子结点，也可以是任意一个父节点 在 Trie 树中查找字符串 当要查找的字符串的最后一个字符在 Trie 树中标识为结束节点时，则匹配成功，否则为匹配失败 Trie 树的优缺点 优点 在已确定的字符集中，且字符集中的字符串的公共前缀较多。适合频繁的查找能否匹配成功，复杂度为 O（k），k 为要查找的字符串的长度 适合查找前缀匹配的字符串 缺点 每个节点需要维护子节点的指针，数据块不连续，对缓存不友好，内存消耗大 改进 Trie 树，使其内存消耗减少时，效率降低，且编码难度增大，不如直接使用红黑树 如 缩点优化 trie AC 自动机（Aho-Corasick） 适用场景 适用于于多模式串匹配，即在一个主串中查找多个模式串 目的 类似于 KMP 算法，尽可能地一次性跳过多个字符，用于减少匹配次数 一次性地寻找多个模式串 AC 自动机的本质就是在 Trie 树上添加类似 KMP 的 next 数组，只不过 AC 自动机的 next 数组是构建在 Trie 树上 AC 自动机的操作 构建 Trie 树\n根结点不存放任何字符，仅作为开始节点 根据字符集中的字符串，存储为多叉树，相同公共前缀的字符串拥有相同的父节点 存储为多叉树的方法有很多，如 有序数组、跳表、红黑树等 将字符串的最后一个字符，标识为结束节点，结束节点可以是叶子结点，也可以是任意一个父节点 在结束节点上记录模式串的长度，匹配时使用 在 Trie 树上构建失败指针\n失败指针的意义\n输入的字符与当前结点的所有孩子结点都不匹配时，AC 自动机的状态应该转移到的状态 从根结点到当前结点所组成的字符串 与 整个 Trie 树 中的所有前缀 的最长公共部分 根结点的失败指针为 NULL，指向自己，按层遍历整个 Trie 树\n如果当前结点对应的字符和当前结点父节点指向的失败指针的孩子结点对应的字符一致，则当前结点的失败指针指向当前结点父节点的失败指针的孩子结点\np.fail == q \u0026amp;\u0026amp; pc == qc，则 pc.fail = qc ac 如果当前结点对应的字符和当前结点父节点指向的失败指针的孩子结点对应的字符不一致，比较当前结点对应的字符和父节点的失败指针的失败指针，直到找到失败指针的孩子结点对应的字符与之相等，如果不存在，则当前结点的失败指针指向根结点\np.fail = q \u0026amp;\u0026amp; pc != q.children，则令 q = q.fail，继续比较 ac 在 AC 自动机中匹配主串\n当前结点的指针从根结点开始，主串字符从 0 开始匹配 匹配当前结点的孩子结点和当前主串字符 如果相等，则判断当前结点和当前结点的失败指针指向的结点，是否是字符串的结束字符 如果是，将匹配到的字符保存在结果集中 如果不是，当前结点指向孩子结点，继续匹配主串字符 如果不相等，则判断当前结点的失败指针是否指向 NULL 如果是，则令当前结点的指针指向根结点，继续匹配主串字符 如果不是，则令当前结点的指针指向当前结点失败指针的结点，继续匹配失败指针的孩子结点和当前主串字符 ","date":"2023-08-23T23:19:41+08:00","permalink":"https://boxtsecond.github.io/en/2023/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E4%B9%9D%E5%AD%97%E7%AC%A6%E4%B8%B2%E5%8C%B9%E9%85%8D/","title":"数据结构（九）字符串匹配"},{"content":"拓扑排序 Topological Sorting 当一组数据间存在两两依赖关系，并且数据间不存在循环依赖的关系。如果要对这组数据进行排序，则可以使用拓扑排序 拓扑排序是基于有向无环图的算法 需要通过局部顺序来推导出全局顺序的，使用拓扑排序来实现 拓扑排序的实现方式 Kahn 算法\n如果 s 先于 t 执行，则添加一条 从 s 指向 t 的边 若某个顶点的入度为 0，说明没有任何一个顶点需要先于此顶点执行，则此顶点可以先执行 在图中找出入度为 0 的顶点，输出并在图中删除此顶点，也就是把这个顶点可达的顶点的入度都 - 1 若输出的顶点个数少于图中的顶点个数，并且图中还存在入度不为 0 的顶点，则说明此图中存在环 DFS 深度优先遍历算法\n构造逆邻接表，如果 s 先于 t 执行，则添加一条 从 t 指向 s 的边 递归的处理每个顶点，先输出这个顶点可到达的顶点后，再输入此顶点 Dijkstra 最短路径算法 单源最短路径算法，即 求一个顶点到另一个顶点的最短路径 Dijkstra 是基于有向有权图 Dijkstra 的时间复杂度为 O（E * logV），E 表示边的个数，V 表示顶点个数 Dijkstra 算法的实现 使用邻接表（邻接矩阵）来表示有向有权图 每个结点包括起始顶点 s 、结束顶点 t 、权重 w，即 s -\u0026gt; t 使用一个数组来表示从顶点 s 到此顶点 n 的最小距离 每个结点包括起始顶点 s 、当前顶点 n 、s 到 n 的最小距离 dist 初始起点顶点的 dist 为 0，其余顶点的初始 dist 的值为无限大 根据 dist 构造一个小顶堆，每次优先获取 dist 最小的顶点 初始 dist 小顶堆只有起始顶点，遍历其余顶点时不断更新 dist 小顶堆 dist 小顶堆中存放的是已经遍历过且计算出 dist 的顶点 使用一个数组来表示此顶点是否已经被遍历过 如果已经遍历过，且发现到达此顶点的更短路径，则更新 dist 的小顶堆 如果没遍历过，将此顶点加入到 dist 的小顶堆中 使用一个数组来表示最短路径，记录每个顶点的前驱顶点 Dijkstra 的优化 当在较大的地图上，计算两点之间的最短路径时 如果两点间的距离较近，可以在整个大地图中划分出一个小的区块，这个小区块可以覆盖这两个点，但是区域不会太大 如果两点间的距离较远，可以把两点所在的大区域当作一个点，先计算两个大区域的路线，然后再细化每个阶段的小路线 计算两点间所用最少时间 将边的权重改为经过这段路所需要的时间 计算两点之间所经过最少红绿灯 将每条边的权重值改为 1，或者直接使用广度优先搜索算法 Dijkstra 算法的变形和应用 背景\n单词经过翻译系统后，得到单词对应的释义及分数，分数代表翻译的可信程度。将句子中的单词翻译后相加，可以得到整句的翻译分数。希望求得翻译得分的 TOP k Dijkstra 实现\n回溯算法 穷举所有的排列组合情况，选取得分最高的前 k 个翻译结果 时间复杂度为 O（mn），m 表示每个单词的平均可选翻译个数，n 表示单词数 Dijkstra 算法 首先可以得到得分最高的一个组合，基于此组合来进行扩展，将每个单词的翻译分别替换成下一个得分最高的翻译。假设为 a0b0c0，扩展后为：a1b0c0、a0b1c0、a0b0c1 每次从优先级队列（大顶堆）中取出 ","date":"2023-08-23T22:45:41+08:00","permalink":"https://boxtsecond.github.io/en/2023/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E5%85%AB%E5%9F%BA%E4%BA%8E%E5%9B%BE%E7%9A%84%E5%85%B6%E5%AE%83%E7%AE%97%E6%B3%95/","title":"数据结构（八）基于图的其它算法"},{"content":"应用背景 广度、深度优先搜索，都是应用于图的搜索算法，是一种暴力搜索的算法。 其他应用于图的搜索算法 A* IDA* 广度优先搜索 BFS（Breadth First Search） 主要目的：在图中寻找两个顶点间的路径 思想： 类似于“地毯式搜索”，主要包含两个步骤 不断地访问顶点 将“路径”记录下来 访问顶点 将顶点的相邻顶点放入队列中，作为下一个需要访问的顶点。 如果此顶点已经被访问过，则继续下一个访问下一个需要访问的顶点。 记录“路径” “路径”，即将每个顶点作为相邻顶点的“上一步”记录下来 最终的路径为从终点开始，依次根据“上一步”找到起点 当找到终点时停止访问顶点 代码实现要点 visited 数组：记录顶点是否被访问 queue 数组：记录需要被访问的顶点 prev 数组：记录相邻顶点的“上一步”为访问顶点 复杂度分析 时间复杂度 每个顶点最多被访问一次，每个边也最多被访问一次 O（V+E），V 为顶点的个数，E 为边的个数 在连通图中，E 一定会大于等于 V - 1 O（E） 空间复杂度 使用邻接表记录顶点的相邻顶点 O（V） 深度优先搜索 DFS（Depth First Search） 主要目的：在图中寻找两个顶点间的路径 思想： 回漱思想，类似于“走迷宫”，当发现“无路可走”时，返回上一个“岔路口”，主要包含两个步骤 不断地访问顶点 将“路径”记录下来 访问顶点 不断地“深入”访问相邻顶点，直到顶点无相邻顶点或者顶点的相邻顶点已经被访问过 当找到终点时停止访问顶点 记录“路径” “路径”，即将每个顶点作为相邻顶点的“上一步”记录下来 最终的路径为从终点开始，依次根据“上一步”找到起点 代码实现要点 使用递归来实现 visited 数组：记录顶点是否被访问 prev 数组：记录相邻顶点的“上一步”为访问顶点 found 布尔值：记录是否找到终点 复杂度分析 时间复杂度 O（E） 每个顶点最多被访问两次，每个边最多被访问两次 空间复杂度 O（V） 双向广度优先搜索 双向广度优先搜索 求两个结点 a、b 的最短路径\n从 a 出发，进行广度搜索，记录下 a 的所有一度结点 a1，查看 b 是否出现在集合 a1 中，如果有，则停止 从 b 出发，进行广度搜索，记录下 b 的所有一度结点 b1，查看 a、a1 是否出现在 b 和 b1 的并集中 重复上述两步，直到找到 a 和 b 的好友的交集 c，则 a、b 的最短通路长为 a 到 c 的通路长度 + b 到 c 的通路长度 启发式搜索算法 Heuristically Search Algorithm A* 搜索算法属于启发式搜索算法（Heuristically Search Algorithm），是对 Dijkstra 算法的优化和改造 启发式搜索算法利用估价函数，避免“跑偏”，贪心地朝着最有可能到达终点的方向前进。启发式搜索算法找出的路径不一定是最短路径，但可以更加快速地找到最短路线 A* 搜索算法 A Star Search Algorithm 遍历顶点时，从起点到这个顶点的路径及路径的长度是确定的，记作 g（i），i 表示顶点编号 通过启发函数（Heuristic Function），即使用一个估算值来判断从这个顶点到终点的路径长度，避免“跑偏”，记作 h（i） 可以使用这个顶点到终点之间的直线距离，即欧几里得距离，近似地估计路径长度 欧几里得距离涉及到比较耗时的开根号计算，可以使用更加简洁的曼哈顿距离（Manhattan Distance），只有简单的加减计算 使用估价函数（Evaluation Function），来综合判断该优先遍历哪个顶点，记作 f（i）。譬如 f（i）= g（i）+ h（i） A* 搜索算法和 Dijkstra 算法的区别 优先级队列构建的方式不同 A* 搜索算法是根据估价函数（f（i））来构建优先级队列 Dijkstra 算法是根据从起点到当前顶点的路径长度（g（i））来构建优先级队列 A* 搜索算法在更新顶点的路径长度（g（i））时，也会同步更新估价函数的值（f（i）） 循环结束的条件不同 A* 搜索算法遍历到终点时就结束 Dijkstra 算法在终点出优先级队列时结束 A* 搜索算法找到的路径不是最短路径，Dijkstra 算法找到的路径是最短路径 ","date":"2023-08-23T22:39:41+08:00","permalink":"https://boxtsecond.github.io/en/2023/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E4%B8%83%E5%9B%BE%E7%9A%84%E6%90%9C%E7%B4%A2/","title":"数据结构（七）图的搜索"},{"content":"图 Graph 相关概念 顶点 Vertex：图中的元素 边 Edge：图中的一个顶点与任意其他顶点建立的连接关系 度 Degree：与顶点相连接的边的条数 入度 In-degree：顶点的入度，表示有多少条边指向这个顶点 出度 Out-degree：顶点的出度，表示有多少条边是以这个顶点为起点指向其他顶点 有向图：图中的边带有方向 无向图：图中的边不带有方向 带权图：图中的每条边都有一个权重 weight 图的存储方式 邻接矩阵 Adjacency Matrix\n邻接矩阵 使用二维数组来存储 无向图 A 如果顶点 i 与顶点 j 之间有边，就将 A[i][j] 和 A[j][i] 标记为1 其余标记为 0 有向图 A 如果顶点 i 与顶点 j 之间，有一条箭头从顶点 i 指向顶点 j 的边，就将 A[i][j] 标记为 1 其余标记为 0 带权图 在数组中存储相应的权重 优点： 简单直观 获取顶点的关系时，非常的高效 方便计算，可以将很多图的运算转换成矩阵之间的运算 缺点：比较浪费存储空间 邻接表 Adjacency List\n邻接表 每个顶点对应一条链表，链表中存储的是与这个顶点相连接的其他顶点 链表的存储方式对缓存不太友好，可以将链表替换成其他更加高效的数据结构，如平衡二叉查找树、跳表、有序动态数组等 优点：比较节省存储空间 缺点：链表无法支持快速查找，可以使用跳表、红黑树、有序动态数组、散列表代替链表 图的应用 社交网络的好友关系\n使用一个邻接表来存储好友关系的有向图，可以快速查找到某个用户关注了哪些用户，即此用户的关注列表 使用一个逆邻接表来存储好友关系，可以快速查找某个用户被哪些用户关注，即此用户的粉丝列表 使用链表的邻接表不能快速地判断两个用户之间的好友关系，可以将链表改为红黑树、跳表、有序动态数组、散列表中的一种 若是需要按照用户名称的首字母排序，分也来获取用户的粉丝列表或关注列表，则适合使用跳表。跳表的插入、删除、查找的时间复杂度为 O（logn），空间复杂度为 O（n），跳表本身的数据也是有序的。 如果数据太多，可以使用哈希算法等数据分片的方法，将邻接表存储在不同的机器上。或者利用外部存储，即数据库。 ","date":"2023-08-23T22:33:41+08:00","permalink":"https://boxtsecond.github.io/en/2023/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E5%85%AD%E5%9B%BE/","title":"数据结构（六）图"},{"content":"树 Tree 相关概念 父节点、子节点、兄弟节点、根节点、叶子节点（叶节点） 节点的高度（Height） 节点到叶子节点的最长路径（边数），从最底层开始计数，计数的起点是 0 树的高度 = 根节点的高度 节点的深度（Depth） 根节点到当前节点所经历的边的个数，从根节点开始度量，计数起点是 0 节点的层数（Level） 节点的深度 + 1 B+ 树 B+ Tree B+ 树 是通过 B 树（二叉查找树）演化而来，目的是在二叉查找树中能够按照区间来查找数据 B+ 树中的非叶子节点不存储数据，只存储索引，而 B 树中的节点存储数据 B+ 树中的叶子节点使用链表串联起来，链表中的数据是有序的，可以按区间查找，而 B 树中的叶子节点不需要链表串联 B+ 树的根节点一般放在内存中，其他节点存储在磁盘中 B+ 树中的每个节点中子节点的个数不能超过 m 个，即 m叉树，也不能小于 m/2（除根节点外） B+ 树的实现 B+ 树的非叶子节点存储索引，存储在磁盘中 内存的访问速度是纳米级别的，磁盘的访问速度是毫秒级别的，读取同样大小的数据，从磁盘中读取花费的时间是从内存中读取的上万倍，甚至几十万倍 每个节点的读取操作，都对应一次磁盘 IO 操作 树的高度 = 每次查询数据时磁盘 IO 操作的最大次数 尽量减少磁盘 IO 操作，也就是尽量降低树的高度 B+ 树的叶子节点存储的是 k 个数据行的键值、地址信息、当前节点在链表中的前驱节点的地址、当前节点在链表中的后继节点的地址，k 的取值由事先计算得到 B+ 树是一个 m 叉树，m 的取值由事先计算得到 k、m 的计算方式 操作系统按照页（PAGE_SIZE）的方式读取内存、磁盘中的数据，一页的大小通常为 4KB，可以通过 getconf PAGE_SIZE 命令查看 当读取的数据量超过一页时，会触发多次 IO 操作 k、m 的取值则根据每个节点的大小 \u0026lt;= 页的大小，且尽可能的大 B+ 树的操作 查询\n按照主键的值在树中进行查找，当查找到叶子节点后 若查找主键 ，则根据叶子节点中的地址信息，获取到具体数据 若查找范围区间，则在链表中继续查找，直到不满足条件（达到 limit 限制、超过范围区间），获取到符合区间值的所有数据 插入\nB+ 树中，m 值是由事先计算得出，即每个节点最多只能有 m 个子节点 当插入数据时同时更新索引，插入后的节点个数超过 m 个，则需要对索引进行更新分裂操作 将当前节点分裂成两个节点，若当前节点分裂之后，父节点的子节点个数超过 m 个，则继续向上分裂，直至父节点的子节点个数 \u0026lt; m b+树 删除\n删除某个数据同时删除索引，当删除后的节点个数小于 m/2 时，则需要对索引进行更新合并操作 当前节点的个数小于 m/2，将当前节点和它相邻的兄弟节点合并 若合并之后的节点个数超过 m，则执行插入的“分裂”操作 b+树 ","date":"2023-08-23T22:25:43+08:00","permalink":"https://boxtsecond.github.io/en/2023/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E4%BA%94%E6%A0%91-b-%E6%A0%91/","title":"数据结构（五）树 — B+树"},{"content":"树 Tree 相关概念 父节点、子节点、兄弟节点、根节点、叶子节点（叶节点） 节点的高度（Height） 节点到叶子节点的最长路径（边数），从最底层开始计数，计数的起点是 0 树的高度 = 根节点的高度 节点的深度（Depth） 根节点到当前节点所经历的边的个数，从根节点开始度量，计数起点是 0 节点的层数（Level） 节点的深度 + 1 平衡二叉查找树 定义：二叉树中任意一个节点的左右子树的高度相差不能大于 1。 目的：解决普通二叉查找树在频繁的插入、删除等动态更新的情况下，出现时间复杂度退化的问题。 常见的平衡二叉查找树，AVL 树、Splay Tree（伸展树）、Treap（树堆） 红黑树 Red-Black Tree 简称 R-B Tree，是一种不严格的平衡二叉查找树。是为了解决普通二叉查找树在数据更新的过程中，复杂度退化的问题而产生的。 红黑树的高度近似 log2n，是近似平衡，插入、删除、查找操作的时间复杂度都是 O（logn）。 在红黑树中的节点，一类被标记为黑色，一类被标记为红色。 红黑树的要求 根节点是黑色的 每个叶子节点都是黑色的空节点，也就是说，叶子节点不存储数据（为了简化红黑树的代码实现） 任何相邻的节点都不能同时为红色，也就是说，红色节点是被黑色节点隔开的 每个节点，从该节点到达其可达叶子节点的所有路径，都包含相同数目的黑色节点 用红黑树的高度是否比较稳定地趋近 log2n 来证明红黑树是否是近似平衡的。 左旋（rotate left）、右旋（rotate right） 左旋 rotate left，围绕某个节点的左旋 右旋 rotate right，围绕某个节点的右旋 红黑树的实现 插入操作 插入的节点必须是红色的，新插入的节点都是放在叶子节点上 如果插入节点的父节点是黑色的，不需要做其他操作 如果插入节点是根节点，直接将节点的颜色置为黑色 其他情况则需要进入红黑树的平衡调整过程 CASE 1：如果关注节点是 a，它的叔叔节点 d 是红色\ncase 1 将关注节点 a 的父节点 b、叔叔节点 d 的颜色都设置成黑色 将关注节点 a 的祖父节点 c 的颜色设置成红色 关注节点变成 a 的祖父节点 c 跳到 CASE 2 或 CASE 3 CASE 2：如果关注节点是 a，它的叔叔节点 d 是黑色，关注节点是其父节点 b 的右子节点\ncase 2 关注节点变成节点 a 的父节点 b 围绕新的关注节点 b 左旋 跳到 CASE 3 CASE 3：如果关注节点是 a，它的叔叔节点 d 是黑色，关注节点 a 是其父节点 b 的左子节点\ncase 3 围绕关注节点 a 的祖父节点 c 右旋 将关注节点 a 的父节点 b、兄弟节点 c 的颜色互换 删除操作 针对删除节点的初步调整 CASE 1：如果要删除的节点是 a，它只有一个子节点 b\ncase 1 删除节点 a，并且把节点 b 替换道节点 a 的位置 节点 a 只能是黑色，节点 b 也只能是红色 CASE 2：如果要删除的节点 a 有两个非空子节点，并且它的后继节点就是节点 a 的右子节点 c\ncase 2 如果节点 a 的后继节点就是右子节点 c，那么右子节点 c 肯定没有左子树。将节点 a 删除，并且将节点 c 替换到节点 a 的位置 把节点 c 的颜色设置为跟节点 a 相同的颜色 如果节点 c 是黑色，将节点 c 的右子节点 d 多加一个黑色 将关注节点变成节点 d CASE 3：如果要删除的是节点 a，它有两个非空子节点，并且节点 a 的后继节点不是右节点\ncase 3 找到后继节点 d，并将它删除，删除后继节点 d 的过程参照 CASE 1 将节点 a 替换成后继节点 d 把节点 d 的颜色设置为跟节点 a 相同的颜色 如果节点 d 是黑色，将节点 d 的右子节点 c 多加一个黑色 关注节点变成节点 c 针对关注节点进行二次调整，为了让红黑树中不存在相邻的红色节点 CASE 1：如果关注节点是 a，它的兄弟节点 c 是红色的\ncase 1 围绕关注节点 a 的父节点 b 左旋 关注节点 a 的父节点 b 和祖父节点 c 交换颜色 关注节点不变 CASE 2：如果关注节点是 a，它的兄弟节点 c 是黑色的，并且节点 c 的左右子节点 d、e 都是黑色的\ncase 2 将关注节点 a 的兄弟节点 c 的颜色变成红色 从关注节点 a 中去掉一个黑色，这个时候节点 a 就是单纯的红色或黑色 给关注节点 a 的父节点 b 添加一个黑色节点 关注节点从 a 变成其父节点 b CASE 3：如果关注节点是 a，它的兄弟节点 c 是黑色，c 的左子节点 d 是红色，c 的右子节点 e 是黑色\ncase 3 围绕关注节点 a 的兄弟节点 c 右旋 节点 c 和节点 d 交换颜色 关注节点不变 跳转到 CASE 4 CASE 4：如果关注节点 a 的兄弟节点 c 是黑色的，并且 c 的右子节点是红色的\ncase 4 围绕关注节点 a 的父节点 b 左旋 将关注节点 a 的兄弟节点 c 的颜色，跟关注节点 a 的父节点 b 设置成相同的颜色 将关注节点 a 的父节点 b 的颜色设置为黑色 从关注节点 a 中去掉一个黑色，节点 a 就变成了单纯的红色或者黑色 将关注节点 a 的叔叔节点 e 设置为黑色 参考资料 https://blog.csdn.net/fei33423/article/details/79132930 https://blog.csdn.net/abcdef314159/article/details/77193888 堆 Heap 堆是一种特殊的树，是一个完全二叉树 堆中每一个节点的值都必须大于等于（或小于等于）其子树中每个节点的值，大顶堆、小顶堆 堆的实现 堆是一个完全二叉树，使用数组来存储，节省存储空间。直接通过数组的下标，找到对应节点的左右子节点和父节点 堆的操作 插入元素（堆化），时间复杂度 O（logn） 从下往上的堆化，遇到不满足大小条件的就交换父子节点 从上往下的堆化，遇到不满足大小条件的就交换父子节点 删除堆顶元素 从上往下的堆化，时间复杂度 O（logn） 将最后一个数字放在堆顶，遇到不满足大小条件的就交换父子节点 基于堆实现排序 建堆，时间复杂度，O（n） 将数组原地建成一个堆，直接在原数组上操作 方式 从前往后处理数组数据，从下往上的堆化。不断地在堆中插入元素，对堆进行插入操作 从后往前处理数组数据，从上往下的堆化。从非叶子节点开始，依次堆化。 对于完全二叉树来说，下标从 n/2 + 1 到 n 的都是叶子节点 排序，时间复杂度，O（nlogn） 堆顶（数组中的第一个元素）就是最大的元素 删除堆顶元素，将堆顶和最后一个元素交换，最大元素放在下标为 n 的位置 将剩下的 n - 1 个元素重新构建成堆 重复上述操作 堆排序整体的时间复杂度是O（nlogn），不是稳定的排序算法 为什么快速排序比堆排序性能好（时间复杂度同样为O(nlogn)） 堆排序数据访问的方式没有快速排序友好 快速排序的数据是顺序访问的，而堆排序是跳着访问的，对 CPU 缓存不友好 对于同样的数据，在排序过程中，堆排序算法的数据交换次数要多于快速排序 堆的应用 堆排序 求 topK 流里面的中值 流里面的中位数 优先级队列 优先级队列 思路：数据的出队顺序按照优先级高的先出队 实现 一个堆就可以看作是一个优先级队列 在优先级队列中，执行入队操作，就相当于往堆中插入一个元素。执行出队操作，相当于取出堆顶元素。 应用场景 合并有序小文件 假设有 100 个小文件，每个文件的大小是 100 MB，每个文件中存储的都是有序的字符串。将这 100 个小文件合并成一个大文件 使用数组的实现 从这 100 个文件中，各取第一个字符串，放入数组中，比较大小，把最小的字符串放入合并后的大文件中，并将它从数组中删除。 假设这个最小的字符串来自 13.txt，就从这个文件中取下一个字符串，放入数组中。重新比较大小，选择最小的放入合并后的大文件，将它从数组中删除。 重复上述步骤，直至所有文件中的数据都放入到大文件为止。 使用优先队列的实现 和使用数组的方法类似，只不过将从文件中取出的字符串放在小顶堆中。 删除堆顶元素，即最小的元素。然后再从堆顶元素所在的文件当中取出下一个字符串，放入堆中。 重复上述步骤，直至所有文件中的数据都放入到大文件为止。 高性能定时器 假设定时器中维护了很多定时任务，每个任务都设定了一个要出发执行的时间点。 简单粗暴的实现 每隔一个很小的单位时间（如 1 秒）扫描一遍任务列表，检查是否有任务达到设定的执行时间。如果达到了，就执行任务。 使用堆的实现 按照任务设定的执行时间，将这些任务存储在优先队列中，队列首部（小顶堆的堆顶）存储的是最先执行的任务。 将队列首部任务的执行时间与当前时间相减，得到时间间隔 T。即从当前开始，第一个任务执行需要等待的时间。 时间 T 过去后，定时器取优先级队列中队列首部的执行任务。 移除队列首部任务，然后再计算新的队列首部任务的执行时间点与当前时间点的差值。 求 Top K 静态数据集合 例：如何在包含 n 个数据的数组中，查找前 K 大的数据 使用小顶堆，维护一个大小为 K 的小顶堆 实现：顺序遍历数组，从数组中取出数据与堆顶元素比较，如果比堆顶元素大，则把堆顶元素删除，并将这个数据插入到堆中。如果比堆顶元素小，则不做任何处理，继续遍历数组。 时间复杂度 遍历数组：O（n） 堆化：O（logK） 所以总共是 O（nlogK） 动态数据集合 例：数据集合有两个操作，一个是添加数据，一个是查询当前 K 大数据 一直维护一个 K 大小的小顶堆 实现：当有数据被添加到集合中时，与堆顶元素比较大小。如果比堆顶元素大，则把堆顶元素删除，并将这个元素插入堆中。如果比堆顶元素小，则不做处理。 利用堆求中位数 静态数据集合 先排序，取下标 n / 2 的数据 动态数据集合 维护两个堆，一个大顶堆，一个小顶堆 大顶堆中存储前半部分数据，小顶堆中存储后半部分数据，且小顶堆中的数据都大于大顶堆中的数据 中位数就是大顶堆的堆顶元素 实现： n 个数据，如果 n 是偶数，按照从小到大排序，前 n / 2 个数据存储在大顶堆中，后 n / 2 个数据存储在小顶堆中。如果 n 是奇数，大顶堆存储 n / 2 + 1 个数据，小顶堆中存储 n / 2 个数据 如果新加入的数据小于等于大顶堆的堆顶元素，将这个数据插入到大顶堆，否则，将这个数据插入到小顶堆 当不满足大顶堆和小顶堆的数据个数时，通过将一个堆中不停地将堆顶元素移动到另一个堆，来满足数据个数的要求 延伸：如何快速求 n% 的数据，例：如何快速求接口的 99% 响应时间 使用两个堆，一个大顶堆，一个小顶堆。大顶堆中保存前 n% 的数据，小顶堆中保存 1 - n% 的数据。大顶堆堆顶的数据就是要求的数据。 每次插入一个数据的时候，判断这个数据跟大顶堆堆顶数据和小顶堆堆顶数据的大小关系。如果新插入的数据比大顶堆的堆顶数据小，则插入大顶堆。如果新插入的数据比小顶堆的堆顶数据大，则插入小顶堆。 每次插入数据后，需要重新计算大顶堆和小顶堆的数据格式，看看是否还满足要求。如果不满足，就将一个堆中的数据移动到另一个堆中，直到满足要求。 ","date":"2023-08-23T22:22:42+08:00","permalink":"https://boxtsecond.github.io/en/2023/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E5%9B%9B%E6%A0%91-%E7%BA%A2%E9%BB%91%E6%A0%91%E5%A0%86/","title":"数据结构（四）树 — 红黑树、堆"},{"content":"树 Tree 相关概念 父节点、子节点、兄弟节点、根节点、叶子节点（叶节点） 节点的高度（Height） 节点到叶子节点的最长路径（边数），从最底层开始计数，计数的起点是 0 树的高度 = 根节点的高度 节点的深度（Depth） 根节点到当前节点所经历的边的个数，从根节点开始度量，计数起点是 0 节点的层数（Level） 节点的深度 + 1 二叉树 Binary Tree 二叉树 每个节点最多有两个子节点，分别是左子节点和右子节点 满二叉树 叶子节点全都在最底层，除了叶子节点之外，每个节点都有左右两个子节点 满二叉树的高度大约是 log2(n) 完全二叉树 叶子节点都在最底下两层，最后一层的叶子节点都靠左排列，并且除了最后一层，其他层的节点个数都要达到最大 完全二叉树的高度是 log2(n) 向下取整 存储二叉树 基于指针或引用的二叉链式存储法 每个节点有三个字段，分别存储数据、左右子节点的指针 基于数组的顺序存储法 根节点存储在下标为 1 的位置 若节点存储在数组中下标为 i 的位置，左子节点存储在下标 2 * i 的位置，右子节点存储在下标 2 * i + 1 的位置 遍历二叉树 前序遍历 对于树中的任意节点，先打印这个节点，再打印它的左子树，最后打印它的右子树 中序遍历 对于树中的任意节点，先打印它的左子树，再打印它本身，最后打印它的右子树 后序遍历 对于树中的任意节点，先打印它的左子树，再打印它的右子树，最后打印它本身 递归公式 前序遍历的递推公式：preOrder(r) = print r-\u0026gt;preOrder(r-\u0026gt;left)-\u0026gt;preOrder(r-\u0026gt;right) 中序遍历的递推公式：inOrder(r) = inOrder(r-\u0026gt;left)-\u0026gt;print r-\u0026gt;inOrder(r-\u0026gt;right) 后序遍历的递推公式：postOrder(r) = postOrder(r-\u0026gt;left)-\u0026gt;postOrder(r-\u0026gt;right)-\u0026gt;print r 二叉查找树 / 二叉搜索树（Binary Search Tree） 前提是一颗完全二叉树 ，在树中的任意一个节点，其左子树中的每个节点的值，都小于这个节点的值，而右子树节点的值都大于这个节点的值 查找操作 先取根节点，如果等于要查找的数据，则返回，若查找的数据比根节点的值小，则在左子树中递归查找，若查找的数据比根节点的值大，则在右子树中递归查找 插入操作 如果要插入的数据比节点的数据大 节点的右子树为空，直接将新数据插入到右子节点的位置 节点的右子树不为空，继续递归遍历右子树 如果要插入的数据比节点的数据小 节点的左子树为空，直接将新数据插入到左子节点的位置 节点的左子树不为空，继续递归遍历左子树 删除操作 如果要删除的节点没有子节点 直接将父节点中指向要删除节点的指针置为 null 如果要删除的节点只有一个子节点（左子节点或右子节点） 更新父节点中指向要删除节点的指针，让它指向要删除节点的子节点 如果要删除的节点有两个子节点 找到这个节点的右子树中的最小节点，替换到要删除的节点上，删除这个最小节点 或者找到这个节点的左子树中的最大节点，替换到要删除的节点上 取巧的删除操作 单纯地将要删除的节点标记为“已删除”，没有真正地从树中将这个节点去掉，比较浪费内存空间 快速地查找最大节点和最小节点、前驱节点和后继节点 中序遍历二叉查找树，可以输出有序的数据序列，时间复杂度是 O(n) 时间复杂度跟树的高度成正比，也就是 O(height) 支持重复数据的二叉查找树 二叉查找树中的每个节点不仅仅存储一个数据，通过链表和支持动态扩容的数据等数据结构，把值相同的数据都存储在同一个节点上 二叉查找树中的每个节点只存储一个数据，插入相同值时，将要插入的数据放到这个节点的右子树，把这个新插入的数据当作是大于这个节点的值来处理 二叉查找树的优势（与散列表相比） 散列表中的数据是无序存储的，而二叉查找树的中序遍历可以在 O（n）的时间复杂度内，输入有序的数据 散列表扩容耗时多，而且遇到散列冲突时性能不稳定，平衡二叉查找树的性能稳定，时间复杂度为 O（logn） 由于哈希冲突，散列表的查找速度不一定比平衡二叉查找树的效率高 散列表的构造复杂，需要考虑到散列函数的设计、冲突解决办法、扩容、缩容等，而平衡二叉查找树只需要考虑平衡性的问题 为了避免过多的散列冲突，散列表装载因子不能过大，会浪费一定的存储空间 递归树 递归的思想是，将大问题分解为小问题来求解，然后再将小问题分解为小小问题。这样一层一层地分解，直到问题的数据规模被分解得足够小，不用继续递归分解为止。如果把这个一层一层的分解过程画成图，它其实就是一棵树，这棵树就是递归树。\n借助递归树分析递归算法的时间复杂度 分析快速排序的时间复杂度\n快排递归树 假设每次分区后，两个分区的大小比例为 1 ： k，递推公式可以写成 T（n）= T（n / 10）+ T（9n / 10） 每层分区操作所遍历的个数之和是 n（固定） 当分区不能均匀的一分为二时，递归树不是满二叉树，那么递归树的高度介于最短路径（每次都是 1 / 10）和最长路径（每次都是 9 / 10）之间，所以时间复杂度介于 nlog10（n）和 nlog9/10（n）之间，时间复杂度仍然是 O（nlog（n）） 分析斐波那契数列的时间复杂度\n斐波那契数列 f（n） = f（n - 1） + f（n - 2），每次数据规模都是 -1 或者 -2，叶子结点的数据规模是 1 或者 2。 最长路径是 n（每次走 1 的数据），最短路径是 n / 2（每次走 2 的数据） 合并操作需要一次加法运算，第一层的时间消耗是 1，第二层是 2，第三层是 4。如果是最长路径，则总时间消耗是 1 + 2 + 4 + \u0026hellip; + 2n + 1。如果是最短路径，则总消耗时间是 1 + 2 + \u0026hellip; + 2n/2 + 1。 时间复杂度介于 O（2n）和 O（2n/2）之间 分析全排列的时间复杂度\n把 n 个数据的所有排列都找出来，如果确定了最后一位数据，就变成了求解剩下 n - 1 个数据的排列问题。所以 n 个数据的排列问题，可以分解成 n 个 n - 1 个数据的排列的子问题。 f(1,2,\u0026hellip;n) = {最后一位是1, f(n-1)} + {最后一位是2, f(n-1)} +\u0026hellip;+{最后一位是n, f(n-1)}。 ","date":"2023-08-23T22:19:41+08:00","permalink":"https://boxtsecond.github.io/en/2023/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E4%B8%89%E6%A0%91-%E4%BA%8C%E5%8F%89%E6%A0%91%E9%80%92%E5%BD%92%E6%A0%91/","title":"数据结构（三）树 — 二叉树、递归树"},{"content":"线性表 每个线性表上的数据最多只有前和后两个方向，如 数组、链表、队列、栈 等 非线性表，数据之间不是简单的前后关系，如 二叉树、堆、图 等 栈 Stack “栈”数据结构的特点 后进者先出，先进者后出（类似一堆盘子） 是一种“操作受限”的线性表（只有 入栈 push、出栈 pop 的操作） 栈的实现，空间复杂度 O（1） 顺序栈，用数组实现的栈 出栈操作，时间复杂度 O（1） 入栈操作，最好情况时间复杂度O（1），最差情况时间复杂度 O（n），均摊时间复杂度 O（1） 链式栈，用链表实现的栈 出栈、入栈操作，时间复杂度 O（1） 栈的实际应用 函数调用栈 操作系统给每个线程分配一块独立的内存空间，以栈的结构来存储函数调用时的临时变量 每进入一个函数，将临时变量作为一个栈帧入栈，当被调用函数执行完成后，将这个函数对应的栈帧出栈 表达式求值 使用两个栈来分别保存，操作数和运算符 从左向右遍历表达式，当遇到数字时，直接压入操作数栈，遇到运算符时，与运算符的栈顶元素进行比较 当运算符栈顶的运算符优先级高时，将当前运算符压入栈，当运算符栈顶的运算符优先级低或相同时，从运算符栈中取栈顶运算符，从操作数栈的栈顶取两个操作数，进行计算，并将计算后的结果压入操作数栈 持续比较，直到结束 检验括号匹配 从左到右依次扫描字符串，用栈保存未匹配的括号（如，( [ { 等） 当扫描到需要匹配的括号（如，) ] } 等），则从栈顶取出一个括号，若匹配，则继续，若不匹配，则说明为非法格式 当所有的括号都扫描完成之后，如果栈为空，则说明字符串为合法格式，否则，说明有未匹配的括号，则为非法格式 浏览器的前进后退功能 使用两个栈 X Y 把首次浏览的页面一次压入栈 X 点击后退时，依次从栈 X 中出栈，并将出栈的数据放入栈 Y，若栈 X 为空，则说明无法后退 点击前进时，依次从栈 Y 中出栈，并将出栈的数据放入栈 X，若栈 Y 为空，则说明无法前进 当跳转到新的页面时，则需要清空栈 Y 使用双向链表，使用 prev 和 next 来跳转 内存中的堆栈 内存中的堆栈和数据结构中的堆栈不是一个概念，内存中的堆栈是真实存在的物理区，数据结构中的堆栈是抽象的数据存储结构 内存空间在逻辑上分为三部分：代码区，数据静态区和动态数据区，动态数据区分为 栈区 和 堆区 代码区：存储方法体的二进制代码，高级调度（作业调度），中级调度（内存调度），低级调度（进程调度）控制代码区执行代码的切换 静态数据区：存储全局变量、静态变量、常量，常量包括 final 修饰的常量和 String 常量，系统自动分配和回收 栈区：存储运行方法的形参、局部变量、返回值，由系统自动分配和回收 堆区：new 一个对象的引用或地址存储在栈区，指向该对象存储在堆区中的真实数据 堆和栈的区别和联系 堆和栈都是指内存空间，不同的是，堆是按需申请、动态分配，并且需要手动释放。栈是由系统自动分配，自动释放，当申请内存超过可用内存时，将报异常提示栈溢出 栈的内存空间是连续的，堆的内存空间则不是。当收到申请空间的请求时，遍历操作系统中记录空闲内存地址的链表，寻找第一个空间大于所申请空间的堆结点，并将此结点从空闲结点链表中删除 队列 Queue “队列”数据结构的特点 先进者先出（类似于排队） 是一种“操作受限”的线性表（只有 入队 enqueue、出队 dequeue 操作） 队列的实现 顺序队列，用数组实现的队列 有两个指针，分别指向 队列头、队列尾 入队操作，时间复杂度 O（1） 由于数组的特性，当队列需要调整长度时，需要数据搬移操作 出队操作，均摊复杂度 O（1） 每次出队，需要删除数组的第一个元素，若仍旧需要使队列头指向数组的第一个元素，则需要数据搬移，时间复杂度为 O（n） 优化：在队列无空闲空间时，触发一次数据的搬移操作 链式队列，用链表实现的队列 有两个指针，分别指向队列头、队列尾 入队操作，时间复杂度 O（1） 由于链表的特性，可以实现长度无限的队列 出队操作，时间复杂度 O（1） 循环队列，队列头、队列尾相连结的队列，一般用数组实现 双指针 有两个指针，分别指向队列头、队列尾 入队操作，时间复杂度 O（1） 避免了顺序队列的数据搬移工作，循环入队 出队操作，时间复杂度 O（1） 注意事项 确定好队空和队满的判定条件，队空 head == tail、队满 （tail + 1）% n = head （head + tail + 1 = n） 循坏时，头尾指针的位置要取模 头指针 + 队列长度 入队操作，时间复杂度O（1） 出队操纵，时间复杂度O（1） 注意事项 确定好队空和队满的判定条件，队空 count == head、队满 count == limit 循环时，头指针的位置要取模，head = ( head + 1 ) % limit，尾指针，tail = ( head + count ) % limit 双端循环队列，可以分别从队列头、队列尾执行出队、入队操作 在队列中预留一个位置，使用两个指针，分别指向队列头、队列尾的下一个位置 注意事项： 确定好队空和队满的判定条件，队空 head == tail、队满 ( tail + 1 ) % limit == head 循环时，头尾指针的位置要取模，head = ( head + limit - 1 ) % limit，tail = ( tail + 1 ) % limit 头指针先取模，再赋值，尾指针先赋值，再取模 阻塞队列 在队列的基础上增加阻塞操作 在队列为空时，从队列头取数据会被阻塞，在队列满时，入队操作被阻塞 生产者 - 消费者模型 并发队列 线程安全的队列，同一时刻仅允许一个入队或出队操作 可以使用基于数组的循环队列，利用 CAS 原子操作 入队操作，比较入队前和入队时的 tail，若无变化，则允许入队 出队操作，比较出队前和出队时的 head，若无变化，则允许出队 ","date":"2023-08-23T22:12:18+08:00","permalink":"https://boxtsecond.github.io/en/2023/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E4%BA%8C%E7%BA%BF%E6%80%A7%E8%A1%A8-%E6%A0%88%E9%98%9F%E5%88%97/","title":"数据结构（二）线性表 — 栈、队列"},{"content":"线性表 每个线性表上的数据最多只有前和后两个方向，如 数组、链表、队列、栈 等 非线性表，数据之间不是简单的前后关系，如 二叉树、堆、图 等 数组 Array 线性表数据结构 连续的内存空间和相同类型的数据 利：“随机访问” 的前提条件 弊：删除、插入操作变得低效，为保证连续性，需要做大量的数据搬移工作 寻址公式：a[i]_address = base_address + i * data_type_size（为什么数组下标以 0 开始，下标意味着“偏移（offset）”） 改进数组的插入、删除操作 改进插入操作，插入第 k 个位置的元素 普通：k～n 的元素顺序后移一位 改进：插入第 k 个位置，并直接将第 k 位的数据搬移到数组元素的最后 改进删除操作，删除第 k 个位置的元素 普通：k～n 的元素顺序前移一位 改进： 将多次删除操作集中在一起执行，减少删除操作导致的数据搬移。每次的删除操作并不是真正地删除并搬移数据，而是记录数据已经被删除，当数组没有更多空间存储数据时，触发一次真正的删除操作。（JVM 标记清除垃圾回收算法） 将最后一位的数据搬移至 k 数组的访问越界问题 访问数组的本质是访问一段连续内存，只要数组通过偏移计算得到的内存地址是可用的，程序就可能不会报任何的错误 数组的访问越界问题，和编译器分配内存和字节对齐有关 容器和数组的选择 容器可以将很多数组操作的细节封装起来，支持动态扩容 Java ArrayList，在创建的时候事先指定数据大小 Java ArrayList 无法存储基本类型，若特别关注性能或只使用基本类型，可以选用数组 表示多维数组时，使用数组更加直观 链表 Linked List 常见的链表结构 单链表 单链表 每个结点都有存储数据和记录下一个结点的地址（后继指针 next） 第一个结点，称作头结点，最后一个结点，称作尾结点，尾结点的 next 指向一个空地址 NULL 单链表操作 循环链表 循环链表.jpg 循环链表是一种特殊的单链表，尾结点指针只想链表的头结点 要处理的数据具有环形结构特点，如 约瑟夫问题 双向链表 双向链表 每个结点有一个后继指针 next 指向后面的结点，有一个前驱指针 prev 指向前面的结点，头结点的 prev、尾结点的 next 指向 NULL 比单链表占用更多的内存空间 双向循环链表 双向循环链表 双向链表 + 循环链表 每个结点有一个后继指针 next 指向后面的结点，有一个前驱指针 prev 指向前面的结点，头结点的 prev 指向 尾结点，尾结点的 next 指向 头结点 链表的删除操作 删除结点中“值等于某个给定值”的结点，时间复杂度：O（n） 从头结点开始，依次遍历对比，直到找到值等于给定值的结点 删除结点操作 删除给定指针指向的结点 单链表，时间复杂度：O（n） 获取要删除结点的前驱结点，从头结点开始依次遍历 删除结点操作 双向链表，时间复杂度：O（1） 直接获取前驱结点 删除结点操作 有序链表 对于一个有序链表，双向链表的按值查询，可以通过记录上次查找的位置 p，每次查询时，根据要查找的值与 p 的大小关系，决定往前还是往后查找，平均只需要查找一半的数据\n数组 VS 链表 数组的储存空间是连续的，可以借助 CPU 的缓存机制，预读数组中的数据，链表则不行 数组的大小是固定的，可能会出现“内存不足”、“重新申请并拷贝操作”（费时），链表没有限制，天然地支持动态扩容 链表需要储存指针，使用的内存比数组多 对链表进行频繁的插入、删除操作，会导致频繁的内存申请和释放，容易造成内存碎片（Java 可能会导致频繁 GC） 链表的应用场景 LRU 缓存淘汰算法（Least Recently Used 最近最少使用） 有序单链表，越靠近尾部的结点是越早之前访问的 有新数据被访问时，从头结点依次遍历 如果数据已经被缓存在链表中了，找到原本此结点的位置，删除，插入到链表的头部 如果数据未被缓存，并且缓存未满，则直接插入到链表的头部 如果数据未被缓存，并且缓存已满，则删除链表的尾结点，并将数据插入到链表的头部 判断一个字符串是否是回文字符串 快慢指针法 链表代码实现的技巧 理解指针或引用的含义 指针：将某个变量赋值给指针，实际上就是将这个变量的地址赋值给指针，指针中存储了这个变量的内存地址，指向了这个变量，通过指针就能找到这个变量 警惕指针丢失和内存泄漏 插入结点时，注意操作顺序 删除结点时，注意内存泄漏问题（手动释放内存空间） 利用哨兵简化难度 针对链表的插入、删除操作，需要对插入第一个结点和删除最后一个结点的情况进行特殊处理 有哨兵结点的链表叫 带头链表，没有哨兵结点的链表叫作 不带头链表 带头链表 留意边界条件处理 链表为空 链表只包含一个结点 链表只包含两个结点 代码逻辑处理头结点和尾结点 举例画图，辅助思考 多写多练！！！ 链表代码练习 单链表反转 链表中环的检测 使用快慢指针，时间复杂度O(n)，空间复杂度O(1)。快指针步长两步，慢指针步长一步，若会相遇，则为有环，若快指针遍历完链表，则为无环 使用集合，时间复杂度O(n)，空间复杂度O(n)。依次遍历链表，若存在相同结点，则为有环，若遍历完链表，则为无环 两个有序的链表合并 新增头结点，依次比较两个链表的结点，顺序在头结点后连接 删除链表倒数第 n 个结点 先使用快慢指针找到倒数第 n 个结点 若无第 n 个结点，则删除第一个结点或不执行删除操作 删除第 n 个结点 求链表的中间结点 使用快慢指针，当快指针指向尾结点时，慢指针指向中间结点 ","date":"2023-08-23T19:02:18+08:00","permalink":"https://boxtsecond.github.io/en/2023/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E4%B8%80%E7%BA%BF%E6%80%A7%E8%A1%A8-%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8/","title":"数据结构（一）线性表 — 数组、链表"},{"content":"学习的步骤 这个东西是什么，重点是什么 为什么需要学这个东西 怎么学这个东西 数据结构与算法 是什么 数据结构：一组数据的存储结构 算法：操作数据的一组方法 数据结构为算法服务，算法要作用在特定的数据结构之上 重点是什么 复杂度分析 10 个数据结构：数组、链表、栈、队列、散列表、二叉树、堆、跳表、图、Trie 树 10 个算法：递归、排序、二分查找、搜索、哈希算法、贪心算法、分治算法、回溯算法、动态规划、字符串匹配算法 为什么要学 修炼自己的内功、锻炼自己的逻辑思维能力 成为 top 程序员 怎么学 边学边练，适度刷题（python、java、c++、js） 多思考、多互动 坚持学会、掌握，练习 反复三遍 衡量算法的优劣 \u0026ndash; 复杂度分析 时间复杂度分析，衡量执行算法消耗的时间 空间复杂度分析，衡量执行算法消耗的存储空间 为什么要复杂度分析 与普通的事后统计法相比较 测试结果非常依赖测试环境 测试结果受数据规模的影响很大 需要一种衡量标准来描述算法的优劣，并且跟测试数据、测试环境无关 怎么做复杂度分析 时间复杂度分析 执行算法的时间随数据规模增长的变化趋势 T(n) = O(f(n)) T(n) 代码的执行时间 f(n) 代码的执行次数总和 只关注循环执行次数最多的一段代码 加法法则：总复杂度等于量级最大的那段代码的复杂度 T(n)=T1(n)+T2(n)=max(O(f(n)), O(g(n))) =O(max(f(n), g(n))) 乘法法则：嵌套代码的复杂度等于嵌套内外代码复杂度的乘积 T(n)=T1(n)*T2(n)=O(f(n))*O(g(n))=O(f(n)*g(n)) 常见的时间复杂度 多项式量级 O(1)，代码中不存在循环、递归语句 O(logn)、O(nlogn)，对数阶时间复杂度，以步长做循环 O(m+n)、O(m*n)，两个数据的规模决定时间复杂度 非多项式量级：O(2n) 和 O(n!) 复杂度量级 空间复杂度分析 算法的存储空间与数据规模之间的增长关系 常见的空间复杂度：O(1)、O(n)、O(n ^ 2) 除了原本的数据存储空间外，算法运行需要额外的存储空间 更全面的复杂度分析 最好情况时间复杂度（best case time complexity）➡️ 最理想的情况 最坏情况时间复杂度（worst case time complexity）➡️ 最糟糕的情况 平均情况时间复杂度（average case time complexity） 加权平均时间复杂度、期望时间复杂度 均摊时间复杂度（amortized time complexity） 有一定的前后时序关系 大部分情况下时间复杂度都相同，个别情况不同 一般情况下，均摊时间复杂度等于最好情况时间复杂度 是一种特殊的平均时间复杂度 解决方案的思考过程 定义清楚问题 调研问题 对模糊的需求进行假设，限定要解决的问题的范围 理解隐藏需求 需求一般可以分为功能性需求和非功能性需求 功能性需求一般来讲是和业务逻辑紧密相关的 非功能性需求包括安全、性能、用户体验等 尝试用学过的数据结构解决这个问题 尝试对比多种数据结构 如果不能直接使用基本数据结构解决，尝试改造数据结构，可以结合多个数据结构的不同特点，使用多个数据结构尝试解决 计算时间、空间复杂度，包括对内存、磁盘的访问 合理地选择数据结构和算法 熟知每种数据结构和算法的功能、特点、时间空间复杂度 时间、空间复杂度不能跟性能划等号 复杂度不是执行时间和内存消耗的精确值，会忽略低阶、常数、系数 处理小规模数据时，代码的执行时间有时不跟时间复杂度成正比 对于处理不同问题的不同算法，复杂度没有可比性 算法的选择，一定要根据数据规模来抉择。数据规模小的时候，不必选择高级算法，而是选择简单、易维护、易实现的算法 解决问题的重点在于对需求的调研、理解，理清楚要处理数据的特征与访问方式 区别对待 IO 密集、内存密集、计算密集 数据在磁盘上，代码的性能瓶颈可能在磁盘 IO，要尽可能地减少磁盘 IO 的次数 数据在内存中，判断代码是内存密集型还是 CPU 密集型 CPU 密集型，CPU 计算耗时占大部分，在选择数据结构和算法的时候，要尽量减少逻辑计算的复杂度，如 用位运算代替加减乘除等 内存密集型，内存数据的读取耗时占大部分，可以考虑是否能减少数据的读取量，数据是否在内存中连续存储，是否能利用 CPU 缓存预读 使用现有算法，避免重复造轮子 不要漫无目的地过度优化 学习书单 《大话数据结构》 《算法图解》 《数据结构和算法分析》 《剑指 offer》 《编程珠玑》 《编程之美》 《算法》 《算法导论》 《计算机程序设计艺术》 《算法帝国》 《数学之美》 《算法之美》 ","date":"2023-08-23T18:44:08+08:00","permalink":"https://boxtsecond.github.io/en/2023/%E5%A6%82%E4%BD%95%E5%AD%A6%E4%B9%A0%E7%AE%97%E6%B3%95/","title":"如何学习算法"},{"content":"本文概览 本文概览 物理层 Physical Layer 虽然 TCP/IP 模型不涉及硬件设备所在的物理层，但还是在这里简单介绍下物理层。1\n物理层，顾名思义，表示实际的物理链接。物理层利用物理传输介质为通信的两端建立链接，实现比特流的传输，如铜线、光缆或无线通道，保证比特流正确的传输到对端。常见设备包括中继器、集线器等。其中集线器 Hub，完全在物理层工作，会将自己收到的每一个字节，都复制到其他端口上去，即广播模式。\nMAC 层 Link Layer 链路层，又称 MAC 层。MAC 的全称为 Medium Access Control，即媒体访问控制，主要有两个功能，寻址和控制。\n寻址，即如何找到数据帧接收方 控制，即谁先发、谁后发，发送出现问题如何处理 解决“控制”问题 在以太网中，两个节点间传输的数据最终都会转换成电信号，在物理传输介质上发送出去。物理传输介质肯定是共享的，在发送数据时需要考虑谁先发、谁后发、同时发送时怎么避免冲突、出现问题怎么处理等。这就是多路访问问题，即如何协调多个发送和接收节点对单一共享广播信道的访问。 计算机网络使用多路访问协议来规范它们在共享的广播信道上的传输行为。\n多路访问协议 我们可以先来思考一下，理想情况下在共享信道上期望怎么传输数据。\n如果只有一个节点需要发送数据，那么我们期望它可以以整个信道的速率 R 发送数据 如果有 M 个节点需要同时发送数据，那么我们期望它可以以信道的传输速率 R / M来发送数据 规划车道 ➡️ 信道划分协议 Channel Partitioning Protocol\n采用多路复用技术，将链路、网络资源（如带宽）划分为“资源片”，将“资源片”分配给各路通信，每路通信独占其分配到的资源片进行通信。这样的划分方式，可以满足理想情况 2。\n依次排队 ➡️ 轮转协议 Taking-turns Protocol\n多个节点轮流发送数据。当只有一个结点活跃时，以信道的全部发送速率 R 发送；当有M个结点活跃时，每个活跃结点的吞吐量接近 R/M。完全满足理想情况 1，几乎满足理想情况 2。\n随机应变 ➡️ 随机访问协议 Random Access Protocol\n节点总是以信道的全部速率发送数据，传输时可能且允许出现冲突。当检测到冲突时通过延时重传等方式恢复，这也是以太网使用的方式。 完全满足理想情况 1，几乎满足理想情况 2。\n在轮转协议和随机访问协议中，好像都可以满足理想情况。为什么以太网使用的是随机访问协议而不是轮转协议？主要有以下三个原因\n网络架构更简单，所有设备都可以同时发送和接收数据，不需要等待其他设备的轮转时机 更加灵活和高效，随机访问协议允许多个设备同时发送数据，不需要进行时间调度或协调 可扩展性高，新设备可以随时连接到网络，并立即开始发送和接收数据，不需要调度或配置 解决“寻址”问题 MAC 地址是指网络设备的硬件地址，是由网络设备的制造商烧录在设备的网卡中的一个全球唯一的地址，如果一台设备有多个网卡，则每个网卡都需要并会有一个唯一的 MAC 地址。MAC 地址共 48 位（6个字节），以十六进制表示。\n当数据传输时，源设备会根据目标设备的 MAC 地址 “寻址” ，确保数据能够被正确地传送到目标设备。当设备接收到数据后，如果发现目的地址与本地 MAC 地址不一致，则会丢弃，只有真正的目标设备才会接收并处理收到的数据。\nMAC 地址是一个局域网（LAN）范围内的地址，它只在局域网中起作用，不能直接进行跨网络通信。在局域网中，可以通过交换机（Switch）来完成数据的传输。交换机通过学习和记录连接到它的设备的 MAC 地址，根据 MAC 地址来决定数据的转发。而在互联网中，数据通常是通过路由器（Router）等设备进行转发，而不是根据 MAC 地址进行直接传输。\n获取 MAC 地址 在网络通信中，使用 MAC 地址直接进行通信是不可行的，我们借助 IP 地址作为中间桥梁来实现通信。在局域网中，知道了要发送的 IP 地址，该如何获得目标设备的 MAC 地址呢？\n每个设备的网卡的 MAC 地址都是固定的，我们可以手动一个个添加局域网中其他设备的 MAC 地址，随着局域网中的主机越来越多，这种方式难以为继。我们需要自动获取、更新和维护各个设备的 MAC 地址，这就是 Address Resolution Protocol。\nARP 协议，工作在 MAC 层，用于获得已知 IP 地址的 MAC 地址。而手动配置 MAC 地址的方式，一般是出于安全考虑，为了防止非法用户访问，由网络管理员手动在 MAC 地址表中添加合法用户的 MAC 地址表项，当手动配置的数量比较大时，难以人工维护，可以使用端口安全功能实现 MAC 地址和接口的动态绑定。\n具体的 ARP 协议内容，查看快速理解网络协议（四）ARP、RARP 协议。\nMAC 层的传输单位 Frame 以太网帧 Ethernet frame 是 MAC 层传输数据的基本单位。其中 MAC 头部包含 目标 MAC 地址、源 MAC 地址 和 类型。类型标识了传输数据的封装协议2。常见的类型有，0x0800 表示 IPv4 数据报，0x0806 表示 ARP 数据报，0x86DD 表示 IPv6 数据报3。\nframe 目标 MAC 地址：表示数据帧应该被发送到哪个网络设备，由 6 个字节组成 源 MAC 地址：表示数据帧是从哪个网络设备发送的，同样由 6 个字节组成 类型：表示数据帧类型，0800 表示 IP 数据包，0806 表示 ARP 数据包 数据：实际的数据内容，长度可变 CRC：Cyclic Redundancy Check 循环冗余检测，通过 XOR 异或的算法，计算整个数据帧在发送的过程中是否出现了错误 小测验 在物理层工作的常见设备有哪些？ 在 MAC 层工作的常见设备有哪些？ MAC 层传输数据的基本是什么？它的头的格式是什么？每个字段的含义和作用是什么？ 如何通过 IP 地址获取 MAC 地址？ 有些文章描述 TCP/IP 模型有五层，其中包括了物理层 Physical Layer。但在 RFC 1122 中未指定物理层，我还是倾向于四层。详情可参考：https://datatracker.ietf.org/doc/html/rfc1122#page-8\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n以太网II帧：https://en.wikipedia.org/wiki/Ethernet_frame#Ethernet_II\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n更多类型含义：https://en.wikipedia.org/wiki/EtherType#Values\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n","date":"2023-08-23T17:16:32+08:00","permalink":"https://boxtsecond.github.io/en/2023/%E5%BF%AB%E9%80%9F%E7%90%86%E8%A7%A3%E7%BD%91%E7%BB%9C%E5%8D%8F%E8%AE%AE%E4%B8%89mac-%E5%B1%82/","title":"快速理解网络协议（三）MAC 层"},{"content":"本文概览 快速理解网络协议（一），基础知识的补充\n本文概览 IP 地址 IP 地址中的 “IP” 代表Internet Protocol（互联网协议），IP 地址是在 IP 协议下使用的地址，IP 地址有三个主要功能：标识设备或网络、定位和选择路由。由于 IPv4 是第一个架构中的主要版本，所以在没有特殊说明的时候，IP 地址一般指的是 IPv4 地址。\nIPv4 地址 一个 IPv4 地址被 “.” 分割成 4 个部分，每个部分 8 个 bit，总共 32 位，使用十进制表示。在设计之初，IPv4 地址被分成 5 类，如下图所示，我们平时接触到的基本是 ABC 三类。一个 IPv4 地址被分为两部分，网络号 + 主机号，这种地址分类的方式叫做分类网络。\nIPv4地址分类 在分类网络中，A、B、C 三类的 IPv4 地址的数量分割一点都不合理，范围跨度太大了。随着互联网的快速发展，CIDR 取代了分类网络。\nCIDR （Classless Inter-Domain Routing ）无类别域间路由，旨在重新划分地址空间，将地址块可以较为自由合理的分配给用户。\nCIDR（“/” 后带着数字这种表示形式，“/” 后）将 32 位的 IP 地址一分为二，网络号 + 主机号，例如 100.100.100.2/24，前 24 位是网络号，后 8 位是主机号 网络号全为 1 的就是子网掩码，主机号全为 1 的就是广播地址 将子网掩码和 IP 地址按位计算 AND，可以得到网络号，即可以判断一个 IP 地址是不是在当前子网内 示例： 100.100.100.2/24\n子网掩码：255.255.255.0 广播地址：100.100.100.255，所有 100.100.100 网络里面的机器都可以收到 100.100.100.20 与子网掩码做按位与运算，得到 100.100.100.0，即为网络号 易错计算： 求 16.158.165.91/22 的网络号、第一个地址、子网掩码和广播地址\n16.158.165.91/22 → 16.158. 101001/01. 01011011 网络号：16.158. 101001/00.0 → 16.158.164.0 第一个地址：16.158.164.1 子网掩码：255.255.252.0 广播地址：16.158.167.255 常见特殊 IPv4 地址 127.0.0.1 表示本机地址 0.0.0.0 有两种含义，默认路由地址和通配符地址。默认路由地址我们之后再说。通配符地址指的是，若进程监听了通配符地址，那么进程监听了本机上的所有 IP 地址 IPv6 地址 由于互联网的快速兴起，导致 IPv4 地址很快就不够用了，于是出现了 IPv6 地址。一个 IPv6 地址被 “:” 分为 8 个部分，每个部分以 4 位十六进制方式表示，总共 128 位。比如：2001:0db8:85a3:08d3:1319:8a2e:0370:734。\nIPv6 地址和 IPv4 地址相似，也使用网络号 + 主机号的形式划分。一个 IPv4 地址可以很容易的转换成一个 IPv6 地址，如果一个地址是 IPv4 地址，可以直接表示成，::ffff:IPv4地址，比如 ::ffff:192.168.89.9，这种格式叫做**IPv4映射地址。**而::1 类似于 IPv4 中的 127.0.0.1。\nIP 地址的功能 标识设备或网络、定位，提供设备在网络中的位置信息 选择路由，在网络传输中，通过 IP 地址选择路由 MAC 地址 和 IP 地址相似，由于此地址作用于 MAC 层，所以称之为 MAC 地址。MAC 地址是指网络设备的硬件地址，是由网络设备的制造商烧录在设备的网卡中的一个全球唯一的地址。类比现实生活的话，IP 地址更像是精确到门牌号的地址信息，而 MAC 地址则是拥有唯一身份证号的“人”。\nIP 地址 和 MAC 地址的区别 区别 IP地址 MAC地址 地址类型 逻辑地址 物理地址 分配方式 由运营商分配 由设备制造商烧录到设备的网卡中 作用范围 全球互联网上的通信 局域网内通信 主要功能 标识设备和选择路由 唯一标识设备 查看 IP 地址、MAC 地址 Linux 下使用 ip addr 或 ifconfig，Windows 下使用 ipconfig\nifconfig “1” 表示 MAC 地址，是一个网卡的物理地址，使用十六进制，6个 byte表示 “2” 表示 net_device flags，网络设备的状态标识 BROADCAST 表示此网卡有广播地址，可以发送广播包 MULTICAST 表示此网卡可以发送多播包 UP 表示此网卡处于启动状态 LOWER_UP 表示 L1 启动，即网线已插入 mtu 1500 表示最大传输单元 MTU 为 1500，是以太网的默认值，即正文部分不允许超过 1500 个字节 qdisc，表示 queueing discipline 排队规则，内核通过网络接口发送数据包时，需要按照 qdisc 配置的规则把数据包加入队列 mq，是一个虚拟 qdisc，目的是为网络设备的每个硬件队列创建一个 pfifo_fast 队列 pfifo，不对数据包做任何处理，先入先出 pfifo_fast，数据包按照服务类型 TOS Type Of Service（ IP 头中的一个字段）分配到不同的波段中，每个波段对应的优先级不同 小测验 如何判断一个 IP 地址是否在子网内？ IP 地址是逻辑地址还是物理地址？它在网络中的作用是什么？ IP 地址和 MAC 地址的区别？ MAC 地址是逻辑地址还是物理地址？它在网络中的作用是什么？ ","date":"2023-08-23T14:52:03+08:00","permalink":"https://boxtsecond.github.io/en/2023/%E5%BF%AB%E9%80%9F%E7%90%86%E8%A7%A3%E7%BD%91%E7%BB%9C%E5%8D%8F%E8%AE%AE%E4%B8%80-ip-%E5%9C%B0%E5%9D%80mac-%E5%9C%B0%E5%9D%80/","title":"快速理解网络协议（一） IP 地址、MAC 地址"},{"content":"本文概览 本文概览 TCP/IP 协议模型 TCP/IP 协议模型 我们从下往上依次介绍这些层级，有一个很重要的原因是，在网络中传输的数据包，会按照这些层级来封装报文，一个数据包只可能有下层没上层，绝不会有上层没下层（这句话要牢牢记住）。\n层级 作用 在此层的协议 链路层 负责节点之间数据的传输控制和寻址 ATM, PPP 等 网络层 负责数据包的路由和转发 IP, ICMP, OSPF 等 传输层 负责数据端到端之间的具体传输行为控制 TCP, UDP 等 应用层 负责不同应用使用的通信协议的数据封装 HTTP, SMTP, FTP, DNS, SSH 等 链路层 链路层负责节点之间数据的传输控制和寻址，重点在于控制和寻址。控制，即谁先发，谁后发，发送时出现问题如何处理；寻址，即如何找到数据包的接收方。\n解决 “控制” 问题\n网络中的链路分为两种类型，点对点链路 和 广播链路。点对点链路，由链路一端的单个发送方和链路另一端的单个接收方组成，如 点对点协议（PPP，Point-to-Point Protocol）、高级数据链路控制（HDLC，High-level Data Link Control）。广播链路，能够让多个发送和接收节点都连接到相同的、单一的、共享的广播信道上。很明显，在点对点链路中，由于只有两个节点参与通信，因此不需要进行数据传输的控制，“控制“ 主要解决的问题是如何协调多个发送和接收节点对单一共享广播信道的访问，即**多路访问问题。**为了解决多路访问问题，于是出现了多路访问控制协议，Multiple Access Control 简称 MAC。\n常见的多路访问协议\n信道划分协议 Channel Partitioning Protocol 采用多路复用技术，将链路、网络资源（如带宽）划分为“资源片”，将“资源片”分配给各路通信，每路通信独占其分配到的资源片进行通信 轮转协议 Taking-turns Protocol 多个节点轮流发送数据 当只有一个结点活跃时，以信道的全部发送速率 R 发送；当有M个结点活跃时，每个活跃结点的吞吐量接近 R/M 随机访问协议 Random Access Protocol 以信道的全部速率发送数据，检测到冲突时通过延时重传等方式恢复 以太网使用的方式 解决 “寻址” 问题\n这里要用到一个物理地址，叫作链路层地址，或 MAC 地址。MAC 地址用于在网络中唯一标示一个网卡（并不是设备），一台设备若有一或多个网卡，则每个网卡都需要并会有一个唯一的 MAC 地址。\n在此层的头部一般被称为 MAC 头。具体格式如下图橙色部分\nMAC 头 目标 MAC 地址：表示数据包应该被发送到哪个网络设备，由 6 个字节组成 源 MAC 地址：表示数据包是从哪个网络设备发送的，同样由 6 个字节组成 类型：表示数据包类型，0800 表示 IP 数据包，0806 表示 ARP 数据包 数据：实际的数据内容，长度可变 CRC：Cyclic Redundancy Check 循环冗余检测，通过 XOR 异或的算法，计算整个数据包在发送的过程中是否出现了错误 网络层 网络层负责数据包的路由和转发，重点在于路由，即选择合适的路径转发到目标主机。\n网络层使用 IP 协议来提供基本的数据传输服务，包括数据包的路由、分组和转发等功能。IP 协议使用 IP 地址唯一标识网络中的设备。\nIP 地址主要有两个作用\n定位，即提供设备网络中的“位置”信息 路由，即数据包选择什么 “路径” 到达目标设备 在此层的头部一般被称为 IP 头。IPv4 头具体格式如图所示\nIPv4 头 IPv4 头\n版本：指定 IP 协议的版本，4 表示 IPv4，占用 4 bits 首部长度 IHL：表示 IP 头的长度，占用 4 bits 区分服务：用于标识网络中不同类型的数据流，只有在使用区分服务时，此字段才有作用，占用 6 bits 显示拥塞通告：可选功能，在两端都支持且底层网络支持时可被使用，占用 2 bits 全长：IP 数据报总长度，包括 IP 首部和 IP 数据部分的长度，占用 16 bits 标识符：唯一标识一个 IP 数据报的所有分片，占用 16 bits 标志：用于控制和识别报文的分片，占用 3 bits 分片偏移：用于标识每个分片相对于原始报文开头的偏移量，占用 13 bits 生存时间：用于防止 IP 数据报在网络中无限循环，作为跳数计数器，每经过一个路由都会减 1，为0时，将丢弃该数据报，占用 8 bits 协议：表示 IP 数据报中使用的上层协议类型，例如 TCP、UDP 等，占用 8 bits 首部校验和：用于检验 IP 首部（不包括数据部分）在传输过程中是否有错，占用 16 bits 源地址：IP 数据报的源地址 目的地址：IP 数据报的目的地址 选项：其他附加信息，首部长度必须是 32 的倍数，若不满足则填充 EOL（0x00），直至满足 传输层 传输层负责数据端到端之间的具体传输行为控制，重点在于端到端和传送控制。端到端，即将数据具体送达至哪个程序，传送控制，即如何将数据送达到目的地。\n传输层上有两个最重要也最常见的协议，TCP 和 UDP，用于端到端的数据传输控制。这两块的内容比较复杂和重要，后面会专门拿出来单独说明，在此就先不赘述了。\n应用层 应用层负责不同应用使用的通信协议的数据封装，重点在于通信协议的数据封装，即在通信时按照哪种格式如何封装数据。\n应用层上的协议是和我们日常开发息息相关的，比如程序使用 HTTP 协议发送一个 HTTP 请求、使用 SMTP 协议向客户发送一封邮件等等。在应用层上，每个协议的封装和解析是协议特有的，HTTP 协议的数据不能被 SMTP 协议解析。\n分层的原因 任何复杂、大型的工程都需要分层来实现模块化，用来降低不同模块之间耦合度，提高模块之间的可替代性和整个工程的复杂度。分层带来的优点有以下几个方面：\n降低不同层级、模块之间的耦合度，提高网络的灵活性。每层的功能和任务清晰且明确，可以分别设计和实现，使得单一协议或模块的开发、维护、升级变得更加容易。 提高相同层级内模块的可替代性。比如 DNS 协议，既可以使用 TCP 协议又可以使用 UDP 协议作为传输层协议。 更低的层级可以统一的向上提供服务，比如传输层的 TCP 协议可以为应用层的 HTTP、FTP 协议等提供服务。 促进了层级和模块的标准化，使得网络在技术和商业上更加开放和透明。 在日常的开发设计中，我们也要将这种分层的思想融入到自己开发的系统中。\n模块化或分层：将系统按照不同的功能和职责进行分层，使系统的各个部分职责清晰、功能单一、易于维护和扩展。 标准化：遵循接口规范，对不同层之间的数据传输进行约束和规范，从而提高系统的稳定性、可靠性和可维护性。 可替代性：通过定义接口和协议来约束不同层之间的数据传输，降低模块间的耦合度，提高模块的可替代性。 小测验 TCP/IP 协议模型有几层，它们的作用分别是什么？ 在网络中的数据包，有没有可能有 TCP 头，但是没有 IP 头？如果有的话，举例说明。 在网络中的数据包，有没有可能有 TCP 头，但是没有 HTTP 头？如果有的话，举例说明。 ","date":"2023-08-23T14:52:03+08:00","permalink":"https://boxtsecond.github.io/en/2023/%E5%BF%AB%E9%80%9F%E7%90%86%E8%A7%A3%E7%BD%91%E7%BB%9C%E5%8D%8F%E8%AE%AE%E4%BA%8Ctcp/ip-%E5%8D%8F%E8%AE%AE%E6%A8%A1%E5%9E%8B/","title":"快速理解网络协议（二）TCP/IP 协议模型"}]