0%

计网

概念

MTU:网络中一次可以传输的最大数据包的大小
MSS:TCP中除去TCP和IP的头部,能够传输的最大的数据量
SDU,Service Data Unit,服务数据单元。
ISP 是 Internet Service Provider 的缩写,翻译为互联网服务提供商。
协议
Telnet 客户端登录协议,属于应用层
TLD 是 Top-Level Domain 的缩写,翻译为顶级域名
GBN (Go-Back-N) 退回
SR 选择性重传
mask 子网掩码
SDN 是指软件定义网络(Software-Defined Networking)
OSPF(Open Shortest Path First)是一种开放式的链路状态路由协议,用于计算机网络中的动态路由。
Secure sockets layer (SSL)

MSL:报文最大生存时间
TTL:经过的路由跳数

序列号:在建立连接时由内核生成的随机数作为其初始值,通过 SYN 报文传给接收端主机,每发送一次数据,就「累加」一次该「数据字节数」的大小。用来解决网络包乱序问题。
确认号:指下一次「期望」收到的数据的序列号,发送端收到接收方发来的 ACK 确认报文以后,就可以认为在这个序号以前的数据都已经被正常接收。用来解决丢包的问题。
控制位: 用来标识 TCP 报文是什么类型的报文,比如是 SYN 报文、数据报文、ACK 报文,FIN 报文等。

面试常问

  • UDP是面向报文的:发送方不会对报文进行拆分,所以一个报文就是一个完整的消息,接收方使用队列来区分不同报文
  • TCP是面向字节流的:粘包问题:TCP报文会拆分成多个,取决于发送窗口、拥塞窗口以及当前发送缓冲区的大小等条件,所以不容易知道一个用户消息的边界
    • 控制位是单独的留出来的几个位置
    • TCP面向字节流的,发送的是一个个连续的字节,而UDP是面向数据报的,一个UDP数据报就是一个完整的消息
    • 重传机制:1.超时重传,重传时间最好略大于包的往返时间,2.快速重传3.SACK重传,选择性确认 TCP 头部「选项」字段里加一个 SACK 的东西,它可以将已收到的数据的信息发送给「发送方」,这样发送方就可以知道哪些数据收到了,哪些数据没收到,知道了这些信息,就可以只重传丢失的数据。4. Duplicate SACK,只告诉对方有哪些数据被重复接受了
    • 滑动窗口:无需等待确认应答,而可以继续发送数据的最大值
    • 拥塞窗口 cwnd是发送方维护的一个的状态变量,它会根据网络的拥塞程度动态变化的。 网络没出现拥塞,cwnd增加,出现拥塞cwnd减少
  • HTTP:最常使用的是HTTP/1.1
    • 优化 HTTP/1.1
      1. 减少请求次数:1.使用客户端缓存,2.减少重定向次数,3.合并请求,4.延迟发送,只发送一部分内容
      2. 减少响应数据大小:使用压缩方式
    1. Keep-Alive:用于保持长连接,keepalive,TCP中的保活机制,HTTP1.1默认是开启的,在请求头中使用Connection: Keep-Alive
    2. 强制缓存:服务器直接要求使用浏览器缓存的数据, 协商缓存:服务器会对比缓存过期时间,决定是否过期,返回给浏览器决定是否使用浏览器的缓存
    3. 队头阻塞:服务端在处理请求时耗时较长导致后面的请求无法即使发送,解决方式 :使用管道:客户端可以直接发送多个请求,服务端根据发送顺序来解决,但是管道技术基本 没人使用
    • 演进过程:
      1. 1.1 使用长连接改善短链接的性能开销,使用管道进传输,改善了队头阻塞。缺点是:header未压缩,只能压缩body,只能从客户端开始发送请求,没有请求优先级
      2. 2 头部压缩,当同时发送多个请求,如果请求头是一样或者相似的就把重复发部分消除,基于HTTPS的,头部全部使用二进制格式取代原来的纯文本,使用Stream,一个Stream可以包含多个Message,Message中可以包含多个Frame,Frame中包含Headers和Body,Stream都跑在同一个TCP上,客户端收到后会根据相同的Stream ID 有序组装成HTTP消息,Stream流用于多路复用TCP
        • 缺点是:由于TCP的是基于字节流的,必须保证收到的字节数据是完整且恋雪的才能将数据返回给应用层
      3. HTTP/3 使用UDP协议来解决响应的队头阻塞。
        1. 使用基于UDP的QUIC协议,保证类似的可靠传输
        2. QUIC 有自己的一套机制可以保证传输的可靠性的。当某个流发生丢包时,只会阻塞这个流,其他流不会受到影响,因此不存在队头阻塞问题。这与 HTTP/2 不同,HTTP/2 只要某个流中的数据包丢失了,其他流也会因此受影响。
        3. 当某个流丢包时,只会阻塞这个流,其他流不会阻塞,不存在队头阻塞问题
        4. RSA算法不支持前向加密,只要获取浏览器的私钥,即可破解
  • HTTPS:
    1. 使用混合加密:通信建立前使用非对称加密来交换会话密钥, 通信过程中使用对称加密的会话密钥来进行通信
    2. 公钥运算出的结果,只有使用私钥来进行逆运算得到结果。 发送时,发送者使用接收者的公钥,公钥是可知的,解决了密钥交换的问题,接收者使用自己的私钥进行解密。
    3. 公钥加密,私钥解密:保证传输过程中内容的加密
    4. 私钥加密,公钥解密:保证消息不会被篡改。—>数字签名算法
    5. 使用权威机构CA(数字证书认证机构)来保存数字证书:个人信息+公钥+数字签名
  • TCP
    • 三次握手的过程
      1. 客户端发送带有SYN(同步序列)SEQ(序列号) = x 标志的数据包,客户端进入SYN_SEND状态
      2. 服务端发送带有SYN(SEQ = y) + ACK (ACK = x + 1) 标志的数据包,进入SYN_RECV状态
      3. 客户端发送 带有ACK(ACK = y + 1) 标志的数据包,二者进入ESTABLISHED状态
      • 为什么要第二次传回SYN 用于表明所收到的是对应的客户端发送的信号
    • 四次挥手:
      1. 客户端发送FIN(SEQ = x) ,进入FIN-WAIT-1状态
      2. 服务端发送ACK(ACK=x+1) 进入CLOSE-WAIT状态,客户端进入FIN-WAIT-2
      3. 服务端发送FIN(SEQ=y) 请求关闭,进入LAST-ACK
      4. 客户端发送,ACK(ACK=y+1) ,客户端进入TIME-WAIT,服务端接收后进入CLOSE,客户端等待一段时间之后如果没有收到回复,就关闭客户端。
    • 可不可以把ACK和FIN合并起来变成三次挥手: 不行,因为服务器收到断开请求时,可能还有一些数据没有传输完成,所以先发送ACK表明收到请求,等待所有数据发送完成之后发送FIN断开连接
  • IP
    • DNS:域名解析:
    • DHCP(动态主机配置协议)是一个网络协议,用于自动分配 IP 地址和其他相关配置信息给网络中的设备。这使得设备可以在加入网络时自动获取网络配置,无需手动设置。
    • ARP工作原理:广播问询,单播响应
      • 一个局域网中
        1. 维护一个ARP表 <IP,MAC,TTL>
        2. 查询ARP表,如果不存在,则构造一个ARP查询分组,将其广播到局域网中,MAC地址为广播地址,然后希望收到的是IP地址
        3. 设备接收后查询是否为自己的IP,如果不是则丢弃,如果是,则构造ARP响应分组,发送给查询主机,并在自己的表中构造一条查询主机的IP-MAC映射表,使用的是单播,不再广播
        4. 查询的主机接受后,将其加入ARP表中
      • 不在一个局域网中:通过路由器转发查询
    • NAT:同一个场景下将私有IP转化为共有IP地址
    • ICMP:互联网控制报文协议,确认Ip包是否
      • ping:基于ICMP协议工作的,使用的ICMP类型中0和8,也就是回送应答和会送请求,根据这个来判断是否到达IP地址,如果路由器中间没有找到对应的接收端IP就会往发送端IP发送ICMP报文,8是源主机向目标主机的发送的请求,0是目标主机的回应
      • 各种本地IP代指的区别:
        1. localhost 默认就是同于127.0.0.1,但是可以修改,是属于域名
        2. 0.0.0.0 IPv4中是无效地址,代指的是广播,监听本地的0.0.0.0时,代表的监听本机上所有的IPv4地址
        3. 127.0.0.1是回环地址
    • IGMP:Internet组管理协议,工作哎主机和最后一跳的路由之间。
    • 为何断网了也能ping通127.0.0.1? 因为会把消息交给本地网卡,本地网卡直接把消息发送到本机接受到的消息链表中并触发软中断,由内核线程来传递给上层应用程序

IP

分类

TCP

如何解决粘包问题

找到消息的边界就能解决这个问题

  1. 固定长度
  2. 特殊字符作为边界
  3. 自定义消息结构

重传机制

重传是由序列号和确认应答来控制的

  1. 超时重传,TCP的超时重传策略是间隔时间每次都设置为先前值的两倍,如果出现两次超时,说明网络环境差,不会重传了
  2. 快速重传:当收到三个相同ACK报文时,说明出现了丢包,会重传丢失的报文。

[滑动窗口](4.2 TCP 重传、滑动窗口、流量控制、拥塞控制 | 小林coding (xiaolincoding.com))

TCP头中有一个Window字段,代表滑动窗口大小。表示接受方告诉对方自己还有多少缓冲区可以接受数据。

发送方窗口

HTTP

基本概念

HTTP是一个在计算机世界中专门在亮点之间传输文字、图片等超文本的约定与规范

常见的状态码

  1. 1xx 是一种中间状态,实际使用较少
  2. 2xx 表示服务器成功处理了请求
    • 200 OK
    • 204 No Content 与200基本相同,但是响应头没有body数据
    • 206 Partial Content表示响应返回的body数据并不是资源的全部。
  3. 3xx 重定向
    • 301 永久重定向
    • 302 Found 临时重定向,注意301和302都会在响应头中使用Location字段知名后续需要跳转的URL,浏览器会自动重定向到新的URL
    • 304 资源未修改,可以使用缓存资源
  4. 4xx 客户端发送的报文有误
    • 400 请求有误,笼统的错误
    • 403 服务器禁止访问资源
    • 404 服务器不存在或未找到
  5. 5xx 客户端请求报文正确,但是服务器处理时,内部出现了错误,属于服务端错误码
    • 500 笼统错误
    • 501 客户端请求功能不支持,敬请期待
    • 502 网关错误
    • 503 服务器繁忙,无法咱是无法响应客户端。

HTTP缓存

  1. 强制缓存:浏览器判断缓存没过期就强制使用本地缓存。使用的是Cache-Control和Expires,第一个是相对时间,第二个是绝对时间。第一个优先级更高。
    • 浏览器第一次请求时,会加上这个过期时间
    • 再次请求时,根据请求时间和这个过期时间进行比较,来判断是否过期,并且更新这个时间
  2. 协商缓存: 与服务器协商之后,通过协商结果来判断是否使用本地缓存。

不同版本的HTTP特点

HTTP: 80
HTTPS: 443

HTTP缓存

Cache-Control(相对时间)优先级高于Expires(绝对时间)
强制缓存是浏览器中的缓存没过期就强制使用缓存,协商缓存是每次与服务器协商是否过期。

HTTP的迭代和对比

HTTP1.1

已经实现了长连接和管道网络传输(不需要一问一答,可以连续发送请求)
解决了 请求的队头阻塞,但是没解决响应的队头阻塞。
头部冗长,未压缩,请求只能由客户端开始

优点和缺点

双刃剑:无状态、明文传输
缺点:不安全

  • 无状态可以使得浏览器不需要额外记忆HTTP的状态,缺点是处理有关联性的操作时会很麻烦
    • 解决方式就是使用Cookie
  • 明文传输:方便调试的同时不安全
  • 不安全:1. 明文传输2.不验证通信双方的身份3.不能证明报文的完整性
    • 解决方式使用HTTPS

性能(长连接)

  1. 相比较于1.0,1.1使用了长连接,一次TCP连接发起多次请求
  2. 可以使用管道通信,只要请求发送出去后,不需要等待响应结果即可发送下一个请求
    解决了请求的队头阻塞,但是没有解决响应的队头阻塞,同时不是默认使用的

    队头阻塞是指当一个请求因为某种原因被阻塞,会导致后面排队的所有请求都一同被阻塞
    进步:

  3. 长连接
  4. 支持管道网络传输
    缺点:
  5. Header部分未经压缩就发送,延迟大,只能压缩Body部分
  6. 会出现队头阻塞:服务端响应慢,导致后续的请求不能及时发送
  7. 请求只能从客户端开始

HTTP/2

进步:

  1. HTTP/2是基于HTTPS,安全有保证
  2. 队头压缩:如果同时发出多个请求,他们的请求头是一样的或者是相似的,就会消除重复的部分,原理是客户端和服务端同时维护一张头信息表,所有字段都会存在里面来进行索引
  3. 二进制格式:报文使用二进制格式,而不是使用纯文本的格式
  4. 并发传输,多路复用,一条TCP连接包含多个Stream,StreamID来区分,不同Stream的帧是可以乱序发送的
  5. 服务器可以主动推送资源
    虽然在HTTP层解决了队头阻塞,但是TCP是字节流协议,导致必须满足一个一个字节才能够读取数据,从而降低效率,一旦丢包,必须进行TCP的重传,就会导致效率降低。

HTTP/3

使用UDP来进行传输

QUIC协议

QUIC协议也实现了Stream的概念,从而某个流发生丢包时,只会则色这个流,而不会阻塞其他流,从而不会存在队头阻塞。
QUIC是google设计的一个基于UDP的网络传输协议,旨在替代TCP协议以提供更快的连接建立和数据传输速度。

  1. 快速建立连接:使用基于TLS的安全连接,同时将连接的建立与TLS握手合并,从而减少了连接所需要的往返时间。
  2. 多路复用:允许在单个连接上同时进行多个独立的数据流,从避免了TCP连接中的队头阻塞
  3. 零RTT握手:QUIC支持零往返时间握手,允许客户端在第一次连接时发送数据,无需等待握手完成,进一步减少了连接建立时间。
  4. 动态调整拥塞控制:使用更先进的拥塞控制算法,能够动态的根据网络条件调增数据传输速率。
  5. 错误恢复:QUIC内置了一些错误恢复机制,包括快速重传和前向错误纠正,能够在发生丢包或网络拥塞时更快的恢复数据传输。

HTTP/2 是⼀个应⽤层协议,是 HTTP/1.1 的后继版本,旨在提⾼ Web ⻚⾯加载速度和性能。 HTTP/2
在传输层使⽤了⼆进制分帧,头部压缩,多路复⽤等技术,以减少延迟和提⾼效率。 HTTP/2 不是⼀个
替代传输层协议,⽽是在传输层上实现的 HTTP 协议的增强版本。

HTTPS

HTTPS是在TCP和HTTP之间加入了SSL/TLS安全协议

核心

  1. 混合加密:对称加密和非对称加密的混合加密方式,通信建立之前使用的是非对称加密,建立之后通信使用的对称加密,对称加密只需要使用一个密钥,所以更加高效。
  2. 摘要算法 + 数字签名:通过摘要算法获得哈希值,来判断报文是否被修改。而数字签名则能保证通信双方的身份
  3. 数字证书:可信的第三方来保证双方身份,主要是CA通过私钥加密双方的公钥数字签名,然后CA的公钥是公开的,之后另一方通过CA的公钥解密获得对方的公钥,之后对方使用私钥加密,就可以使用公钥进行解密了或者是加密通信。

非对称加密

  1. 公钥加密,私钥解密:保证内容的安全,只有私钥可以解密内容
  2. 私钥加密,公钥解密:保证消息不可冒充,因为私钥是不可以泄漏的,验证双方身份,也就是数字签名算法|525

流程(RSA算法为例)

  1. 三次握手建立TCP连接
  2. 客户端发送ClientHello请求,携带客户端支持的TLS版本信息和客户端随机数Client Random,支持的密码套件列表(比如RSA算法)
  3. ServerHello响应:确定TLS版本,浏览器不支持就断开,服务器随机数Server Random, 使用的加密算法,服务器的数字证书
  4. 客户端使用CA的公钥确定证书的真实性,之后取出公钥,加密报文发送:1. 随机数 pre-master key 2.加密通信算法改变通知,表示之后都将使用会话密钥(对称加密)进行通信,客户端握手结束通知,把之前所有的内容进行摘要,给服务器进行校验
  5. 双方使用这三个随机数和加密算法生成会话密钥
  6. 服务器最后1.加密算法改变 2.握手结束,生成摘要供客户端校验

HTTPS抓包/代理人

抓包工具的原理就是往系统受信任的根证书列表导入抓包工具生成的证书,这个证书会被浏览器新人,也就是转包工具给自己建立了一个CA。
如何解决中间人攻击:使用HTTPS双向认证,服务器也对客户端的认证信息进行验证。

计网常问

一个请求整个网络的处理

  1. 浏览器解析URL生成HTTP消息
  2. 通过DNS解析获得IP地址
  3. 之后将HTTP的传输工作通过调用Socket库交给操作系统的协议栈
  4. TCP简历链接需要三次握手,保证双方都有发送和接收的能力
  5. 建立连接之后,TCP报文中的数据部分就是存放HTTP头部+数据,组装好TCP报文之后,交给下面的网络层处理
  6. IP协议里需要源地址和目的地址IP,存在多个网卡时,需要根据路由表规则。
  7. 生成IP头部之后,需要在前面加上MAC头部,接收方的MAC地址通过ARP协议获得,并且放入ARP缓存
  8. 网卡驱动获得网络包,将其复制到网卡内的缓存区中,从开头加上包头和起始帧分节符,末尾加上用于校验错误的帧校验序列。之后网卡将包转为电信号,通过网线发送
  9. 电信号到达网线接口,交换机里的模块进行接收。之后将电信号转化为数字信号。FCS校验没问题后放入缓冲区,之后查询MAC地址,如果找不到,就发除了源端口的所有端口。
  10. 电信号到达⽹线接⼝部分,路由器中的模块会将电信号转成数字信号, FCS 进⾏错误校验没问题后确认接收⽅MAC地址,然后去掉MAC头部,查询路由表判断转发⽬标,如果⽹关为空则 IP 头部中的接收⽅ IP 地址就是要转发到的⽬标地址
  11. 知道对⽅的 IP 地址之后,接下来需要通过 ARP 协议根据 IP 地址查询 MAC 地址,加上MAC头部,发送⽹络包,通过交换机到达下⼀个路由器
  12. 最后数据包抵达了服务器

IPv6优点

  1. 更⼤的地址空间:IPv6将地址⻓度从IPv4的32位扩展到了128位,理论上可以分配⼤约3.4x10^38个唯⼀的IP地址。
  2. 简化的报⽂格式:IPv6头部格式更为简洁,减少了处理的复杂性,提⾼了路由效率。
  3. 改进的服务质量(QoS):IPv6⽀持更好的流量分类和优先级处理,有助于提供更加可靠的服务质量。
  4. 内建的安全机制:IPv6原⽣⽀持IPsec(⽹络安全协议),为数据传输提供了端到端的加密和认证。
  5. ⾃动配置能⼒:IPv6⽀持有状态和⽆状态的地址⾃动配置(SLAAC),简化了⽹络设备的配置和管理

BGP、OSPF协议原理

边界⽹关协议(Border Gateway Protocol,简称BGP)和开放最短路径优先协议(Open Shortest Path
First,简称OSPF)是世界上最流⾏的两种基于标准的动态路由协议。

三握四挥

TCP和UDP

|525

  1. 序列号:建立连接时,由计算机生成的随机数作为初始值,通过SYN包传给接收端主机,每发送一次,就累加一次该数据字节数的大小,解决网络包乱序问题。
  2. 确认应答号:下一次期望收到数据的序列号。发送端收到这个之后可以认为在这之前的数据都被正常接收,用于解决丢包问题。
  3. 控制位为1时:
    • ACK:确认应答变为有效,TCP规定除了最初建立连接时的SYN包之外必须设为1
    • RST:标识连接异常必须强制断开连接
    • SYN:表示希望建立连接
    • FIN:今后不会再有数据发送,希望断开连接时,表示今后不再有数据发送,希望断开连接。当通信结束希望断开连接时,通信双方可以交换FIN位为1的TCP段。

SYN攻击

攻击者短时间伪造不同的IP地址的SYN报文,沾满服务端的办理按揭队列,导致客户端无法和服务端建立连接。

解决方案:

  1. 增大netdev_max_backlog:网卡接收数据包的数据大于内核处理速度时保存数据包的队列长度
  2. 增大TCP半连接队列:
    • 增大net.ipv4.tcp_max_syn_backlog
    • 增大listen()函数中的backlog
    • 增大net.core.somaxconn
  3. 开启net.ipv4.tcp_syncookies,就可以在使用SYN半连接的情况下成功建立连接。|550
  • 减少SYN+ACK重传次数,tcp_synack_retries内核参数
  • Anti-DDoS系统拦截客⼾端发送的SYN报⽂,代替服务器向客⼾端发送SYN-ACK报⽂,如果客⼾端不应答,则认为该客⼾端为虚假源;如果客⼾端应答,则Anti-DDoS系统认为该客⼾端为真实源,并将其IP地址加⼊⽩名单,在⼀段时间允许该源发送的所有SYN报⽂通过,也不做代答。

滑动窗口

滑动窗口机制允许发送方在等待接收方确认之前发送多个数据包,从而提高数据传输效率。
为了解决一发一答的效率问题。窗口的实现实际上是操作系统开辟一个缓存空间,发送方主机在等到确认应答返回之前,必须在缓冲区中保留已发送的数据,如果按期收到确认应答,这个数据就可以从缓冲区删除。

慢启动

当连接开始时,以指数速率增加发送速率,直到第一次报文丢失事件发生为止。
初始:拥塞窗口值cwnd = 1 MSS
每RTT倍增cwnd
每收到一个ACK,增加cwnd
初始速率很低,但是以指数增加

阻塞控制

  1. 慢启动:发送方每收到一个ACK,拥塞窗口cwnd大小指数增加
  2. 拥塞避免算法:当cwnd超过慢启动门限ssthresh就会进入拥塞避免算法:每个 RTT(往返时间)增加 1 个 MSS,而不是每个 ACK 增加 1 个 MSS。
  3. 拥塞发生:发生了 超时重传时,sshtresh设置为cwnd/2,cwnd恢复为初始化值,发生快速重传是,cwnd =cwnd/2,ssthresh=cwnd并进入快速恢复算法。
  4. 快速恢复

DNS

主机名到IP地址的转换
主机别名:一个主机可以有一个规范主机名和多个主机别名。
邮件服务器别名:负载分配:DNS实现冗余服务器,一个IP地址集合可以对应同一个规范主机名。

DNS客⼾端设置使⽤的DNS服务器⼀般都是递归服务器,它负责全权处理客⼾端的DNS查询请求,直
到返回最终结果。⽽DNS服务器之间⼀般采⽤迭代查询⽅式。

DNS劫持

通过某些手段获得对某域名的解析控制权,修改此域名的解析结果,导致对该域名的访问由原IP转为修改后的指定IP,记过就是对特定网站不能访问/访问的是假网站

防止:限制对DNS的访问、设置较小的TTL值,定期修改域名管理系统的账号,使用支持DNSSEC的注册商,使用可靠的DNS服务商。

DNSSEC(Domain Name System Security Extensions,域名系统安全扩展)是一组用于保护 DNS(域名系统)信息安全的协议和技术。它通过数字签名验证 DNS 数据的真实性和完整性,防止 DNS 缓存投毒和其他类型的攻击。

网络核心

概念:路由器的网状网络
提问: 数据怎么通过网络进行传输?

  • 电路电路交换: 不可共享资源,会造成资源浪费,不适合计算机之间的通讯,计算机通信的特点: 突发性,耗时短
  • 分组交换: 存储 – 转发 , 数据转发过程中使用所有的资源,而不是使用一部分pieces,会将所有的的分组都存储之后再进行转发,方便共享的实现

应用层

  1. 应用架构
    • CS体系 客户-服务器:
      • 服务器: 一直运行,固定ip和周知的端口号,扩展性差
      • 客户端: 主动与服务器通信,可能是动态ip,不直接与其他客户端通信
    • P2P 每个端都可以作为服务器,点对点
    • 混合体 c/s + P2P
  2. TCP socket :对于面向连接服务(TCP) 的应用而言,socket是四元组的一个具有本地意义的标示,相当于一个记录特定会话的指针,只需要用socket就可以指定这个应用
    • socket 其实是应用层和传输层之间的,使得允许应用能发起通信,与其他主机上的应用进程进行通信
    • 4元组 源IP 源port 目标IP 目标port
    • 唯一指定了一个会话
    • 应用使用这个标示,与远程的应用进程进行通讯
    • 不必在每一个报文中都指定这四元组
    • udp 只提供源主机的ip 和 port
  3. WebSocket 工作过程:
    1. 客户端发送一个HTTP请求,包含升级字段
    2. 服务器接收之后,进行协议升级,如果支持,会返回一个101状态码,包含一些对应响应头
    3. 进行双向通信,数据以帧的方式传送。
    4. 其中一方发送一个关闭帧,二者关闭TCP连接
    5. 通过心跳机制保证WebSocket的稳定性和活跃性
  4. UDP socket
    UDP 两个进程之间的通信之前不需要建立连接,每个报文独立传输,前后报文可能给不同的分布式进程
    udp socket 记录本IP 本port 但是传输报文时,需要提供对方ip, port ,接收报文时传输层需要上传对方IP port
    二元组: 源IP 源port

TCP 服务:

  • 可靠的传输服务

  • 流量控制:发送方不会淹

没接受方

  • 拥塞控制:当网络出现拥

塞时,能抑制发送方

  • 不能提供的服务:时间保证、最小吞吐保证和安全

  • 面向连接:要求在客户端进程和服务器进程之间建立连接

UDP 服务:

  • 不可靠数据传输

  • 不提供的服务:可靠,流量控制、拥塞控制、时间、带宽保证、建立连接
    UDP存在的必要性

  • 能够区分不同的进程,而IP服务不能

  • 在IP提供的主机到主机端到端功能的基础上,区分了主机的

应用进程

  • 无需建立连接,省去了建立连接时间,适合事务性的

应用

  • 不做可靠性的工作,例如检错重发,适合那些对实时

性要求比较高而对正确性要求不高的应用

  • 因为为了实现可靠性(准确性、保序等),必须付出时间代

价(检错重发)

  • 没有拥塞控制和流量控制,应用能够按照设定的速度

发送数据

  • 而在TCP上面的应用,应用发送数据的速度和主机向网络发送

的实际速度是不一致的,因为有流量控制和拥塞控制

  • 安全TCP
    TCP和UDP都没有加密,明文传输
    使用SSL协议来实现加密,在TCP的基础上实现,提供加密的TCP,私密性,数据完整性,段堆到端的鉴别
    SSL socket API 应用通过API将铭文交给socket,SSL将其加密
    URL : 访问协议 + 用户名 + 口令字 + 端口等

常见的应用层协议

HTTP面试题

  • 状态码:

    1. 200
    2. 204 No Content 响应头没有body数据
    3. 206  Partial Content 表示返回的body数据不是资源的全部,只是一部分
    4. 301 Moved Permanently 永久重定向,请求的资源已经不存在的
    5. 302: Found: 表示临时重定向,301,302都会在响应头中使用字段Location指明后续要跳转URL,浏览器会自动重定向到新的URL
    6. 304:Not Modified 告诉客户端可以接着使用缓存资源
    7. 400Bad Request 报文有错
    8. 403 Forbidden 禁止访问
    9. 404 Not Found 资源不存在或者没找到
    10. 500 Internal Server Error 服务器内部错误
    11. 501 Not Implemented 客户端请求还不支持
    12. 502 Bad Gateway 通常是服务器作为网关或代理时返回的错误码,表示服务器自身工作正常,访问后端服务器发生了错误。
    13. 03 Service Unavailable 服务器忙,暂时无法响应
  • 缓存技术:把请求-响应的数据存到本地,下一次直接都本地数据,不需要等待服务器的想用了

    1. 强制缓存
      • Cache-Control, 是一个相对时间;
      • Expires,是一个绝对时间;
    2. 协商缓存
      • 在强制缓存未命中时,服务器第一次请求资源时,会在Response头部加上ETag唯一表示
      • 当浏览器再次请求访问服务器中的资源时,先检查缓存是否过期,如果没有过期直接使用本地缓存,如果过期了会在Request头部上加上If-None-Match紫萼段
      • 服务器再次收到请求之后,会根据请求中的If-None-Match值是否与当前请求的资源生成的唯一标识比较
        • 如果值相等,返回304 Not Modified 不会返回资源
        • 如果不相等,则返回 200 状态码和返回资源,并在 Response 头部加上新的 ETag 唯一标识;
      • 浏览器收到304响应码,会从本地加载资源否则更新资源。
  • HTTP/1.1 的优点:简单灵活、应用广泛

    • 缺点是:无状态一方面可以不需要服务器使用额外的资源来记录状态信息
    • 坏处是:进行关联性操作时,需要每次都验证一次身份
    • 解决方法:使用Cookie
  • HTTP:超文本传输协议 HTTP默认80 HTTPS默认 443

    • 流程: 1. 客户发起一个与服务器的TCP连接,(建立socket) 2. 服务器接受TCP 连接 3. 浏览器和web服务器之间交换HTTP报文 4. TCP连接关闭
    • HTTP 是无状态的连接,不会维护任何和客户有关的信息,这时候就需要websocket了
    • HTTP/1.1 之后默认使用持久连接,保证了多个u第项可以在一个TCP连接上传输 ,非持久连接下载多个文件需要及案例多个TCP连接
      响应时间
      往返时间 RTT round - trip - time: 一个小的分组从客户端到服务器,再回到客户端的时间,传输时间忽略不计
      响应时间为: 2RTT + 传输时间
      1. 一个RTT 用来发起TCP请求
      2. 一个用来HTTP请求和等待响应
      3. 文件传输时间
      持久HTTP : 一个TCP连接建立之后,不会断开然后进行多个HTTP请求
      服务器在发送响应之后仍然保持TCP连接
      在相同客户端和服务器之间的后续请求和响应报文通过相同的连接进行传送
      客户端,在遇到一个引用对象的时候就可以尽快的发送该对象的请求,
      1. 每个对象要两个RTT
      2. 操作系统必须为每一个TCP连接分配资源,但是浏览器通常并行打开TPC连接,以获得引用对象
      两种方式:
      1. 非流线方式的持久HTTP,客户端只能在前一个HTTP请求响应之后才能发送新的请求,每个引用对象花费一个RTT
      2. 流水线方式:客户端遇到一个引用对象(一个小的ui所指的资源)之后就立即产生一个请
      3. 所有(小)的引用对象只花费一个RTT是有可能的
      HTTP 报文格式 : 请求行 + 请求头 + (request body)
      tips : PUT 请求是将实体对象中文件载到URL指定的路径(一般是更新资源)
      POST 不需要在URL 中指定资源位置,一般用于创建资源
      • 缓存:命中率(h): 百分之多少的请求可以在缓存中满足
      • 接入链路的利用率: (1-h) * 请求速率 / 带宽
  • FTP 文件传输协议(基于TCP)
    ftp服务器端口号默认为 21,需要建立两个TCP连接,一个用于控制,一个用于传输

  • SMIT 电子邮件的邮件传输协议 (默认端口25)
    用于上传邮件,和HTTP的区别,HTTP的每个对象封装在各自的响应报文中,SMIT可以将多个 引用对象封装在一个报文中
    报文格式:
    HEAD
    To :
    From :
    Subject:
    BODY:
    报文

    • MIME 多媒体邮件扩展
    • POP 邮局访问协议 用户确认身份(代理 <–>服务器)并下载
      • POP3 不保留会话状态 本地管理文件夹
    • IMAP Internet 邮件访问协议,保留用户状态 远程管理文件夹
  • DNS Domain Name System 建立IP 地址和 对应域名之间的映射
    DNS默认默认端口是53
    主要思路:分层,基于域的命名机制,在若干分布式的数据库上完成转换
    也可以做到负载均衡
    域名结构:使用层次树状结构来进行命名
    域名结构:从本域开始往上直至树根,域严格遵循组织界限,而不是物理网络
    DNS 记录格式
    RP格式: (name,value,type , ttl)
    type=A 时, name 为主机 value 为IP
    =CNAME Name 为规范名字的别名
    =NS Name 为域名 (foo.com) value 为该域名的权威服务器的域名
    =MX Value 为name对应邮件服务器名字

    • 应用调用 解析器(resolver)
    • 解析器作为客户 向Name Server发出查询报文(封装在UDP段中)
    • Name Server返回响应报文(name/ip)
    • 本地名字服务器 Local Name Server 起到代理的作用,将查询转发到DNS服务器
      • 不严格属于层次结构,每个ISP 都有一个本地DNS,优先去本地DNS服务器中查询
      • 递归查询:本地LNS 无,直接 去找权威服务器,然后从权威服务器往下开始查询,只需要向一个服务器去请求,然后由这个服务器去查询或者去向其他服务器来进行查找,最终由这个服务器来返回结果
      • 迭代查询:转发到服务器,如果这个服务器没有就告诉发起请求的服务器要去查询下个一个服务器,由请求服务器去接着请求其他服务器,最终由能查询到的服务器来返回结果,就可以降低根服务器的负荷了
    • 缓存,一旦名字服务器得到了一个映射,就将该映射缓存起来,根服务器一般在本地服务器中缓存着,使用TTL (Time to Live)
    • 攻击DNS的方法:
      • DDoS攻击:对根服务器进行流量轰炸,发送大量的ping
      • 向TLD(权威,顶级域名)攻击
      • 重定向攻击:
  • CDN 内容分发网络:在CDN节点中存储内容的多个拷贝,用户请求重定向到最近的一个CDN节点

传输层

提供服务:为运行在不同主机上应用程序提供逻辑通信
数据元为报文段
与网络层的区别:网络层提供不同主机之间的逻辑通信,传输层提供的应用程序之间的通讯
tips:TCP和UDP都不提供延时保证和带宽保证,都支持多路复用和解复用,TCP额外提供拥塞控制和流量控制,以及建立连接

  • 多路复用/解复用:多路复用指的是许多个信号或数据流共享同一物理通信通道,解复用指的是根据报文段的头背部信息中的IP地址和端口号将接受的报文段发给正确的socket
  • 为何要有UDP :
    1. 不建立连接(会增加延时)
    2. 简单:在发送端和接收端没有连接状态
    3. 报文段的头部很小(开销小)
    4. 无阻塞控制和流量控制可以保证UDP尽快的发送报文段,应用-> 传输速率 = 主机 -> 网络的速率
    5. UDP也会进行校验,但是只是通过校验和的方式来检测是否遭到篡改
  • RDT 是一种模型: RDT(Reliable Data Transfer)是一种可靠的数据传输协议,用于在不可靠的通信信道上实现可靠的数据传输。
    1. RDT 1.0(停等协议):相信信道可靠
      • 发送方只发送一次数据,不进行重传。
      • 接收方只接受一次数据,不进行重传请求。
      • 适用于理想化的通信信道,不考虑错误和丢失。
    2. RDT 2.0(回退N协议): 相信会出现bits errors
      • 使用checksum 来进行错误检验
      • 引入了有限状态自动机,来切换来指定发送者和接收者
      • 发送方发送数据帧,并等待接收方的确认帧。
      • 接收方接收数据帧,发送确认帧。
      • 如果发送方未收到确认帧,它将重传数据帧。
      • 接收方可能收到重复的数据帧,但通过带有序号的数据帧来排除重复。
      • 问题:如果ACK或者NCK传错了,就会重复
    3. RDT 3.0
      • 机制:在超过合理时间之后进行重传
        • 如果package(或ACK)只是被延迟了:
        • 重传将会导致数据重复,但利用序列号已经可以处理这个问题
        • 接收方必须指明被正确接收的序列号
    • 需要一个倒计数定时器
      1. 停等协议:发送方发送一个分组,然后等待接收方的应答
      2. ACK NAK NAK 是negative ACK
  • TCP:
    • 点对点:一个发送方,一个接收方
    • 可靠的、按顺序的字节流:没有报文边界
    • 管道化:TCP拥塞控制和流量控制设置窗口的大小
    • 面向连接:交换数据之前,通过握手来初始化双方的状态变量
    • 流量控制:发送方不会淹没接收方
    • tcp报文
    • 序号:报文段首字节在字节流的编号
    • 确认号:期望从另一方收到的下一个字节的序号
    • TCP超时时间= EstimatedRTT + 安全边界时间
    • 。。。。待续

网络层

DU 为 数据包

  • 服务:在发送和接收主机之间传送段
  • 功能:
    • 转发:将分组从路由器的输入接口转发到合适的输出接口上
    • 路由:使用路由算法来决定分组从发送主机到目标接收主机的路径
  • 数据平面:转发
  • 控制平面:路径
  • 路由器的结构:
    • 路由:运行路由选择算法生成路由表
    • 转发:从输入到输出链路交换数据报,根据路由表进行分组的转发
  • 查表方式:
    • 最长前缀匹配
  • 交换结构:
    • 通过内存交换,在cpu的直接控制下进行交换
    • 使用总线进行交换: 数据报通过共享总线进行转发,交换速度受限于总线带宽
    • 使用互联网络进行交换
  • 调度机制: 调度指的是选择下一个要通过链路交换的分组
    • FIFO
    • 优先权调度
    • 轮询
  • IP
    • IP地址是对主机或者路由器的接口进行编址
    • 接口指的是 主机/路由器 和 物理链路的连接处
    • 关系是1 : 1
    • 子网
      1. 一个子网内的节点,他们IP地址的高位部分都相同,这些节点叫做子网
      2. 无需路由器介入,子网内的个主机在物理上是可以直接打到
      3. 将子网掩码转为二进制,则为1的部分代表着IP地址中的这一部分是网络中的地址,为0的部分是标识子网中的主机
    • 分类:根据第一个8位bit来进行分类
      • A 类 最高位固定为0:也就是 1 - 126 7 位网络 24 位主机
      • B : 10 128 - 191 14位 网络 16主机
      • C : 110 192 - 223 21位 网络 8位主机
      • D : 224 - 239 用于多播
      • E : 240 - 255 保留用于实验和研究目的
    • CIDR Classless InterDomain Routing 无类域间路由
      • 也就是 a.b.c.d/x x是子网掩码(mask)
    • NAT 网络地址转换
      • 将私有网络中的内部IP映射到公共网络中的单个IP地址
      1. 节省IP地址:NAT允许多个内部设备共享一个公共IP地址,因此可以延长IPv4地址池的使用寿命。

      2. 增强网络安全性:因为内部设备的私有IP地址不直接暴露在互联网上,NAT提供了一层基本的安全性,可以隐藏内部网络结构。

      3. 简化网络管理:NAT可以使网络管理员更轻松地管理多台设备,而无需为每个设备分配唯一的公共IP地址。

  • IPv6
    • IPv4和IPv6通信方式:隧道 Tunneling, 在IPv4路由器之间传输的IPv4报文中携带PIv6的报文
  • SDN
  • OpenFlow
  • route 路由,按照某种指标找到一条从源节点到目标节点的较好路径
    • 路由算法分为 全局和 分布式
    • 全局:
      • 所有边都拥有完整(所有的拓扑)的拓扑和边的代价的信息
      • link state LS 链路状态路由选择算法
    • 分布式:
      • 路由器只知道与它物理连接关系的邻居路由器和到响应邻居路由器的代价
      • 迭代的与邻居路由器交换路由信息,计算路由信息
      • distance vector DV算法 距离矢量路由选择算法
  • RIP(Routing Information Protocol)是一种用于计算机网络中的距离矢量路由协议
  • OSP
  • DNS 主机向要访问->向DNS查询IP地址->所查询的DNS服务器未知要查询的IP地址->向根域名服务器查询->根域名服务器收录了这个地址 ->返回地址给客户端->客户端建立通信
    网络层解决一个两个网络之间的问题,链路层要解决点对点传输的问题
  • ARP:以目标IP地址为线索,来定位下一个应该接受数据分包的网络设备的MAC地址,沟通IP和MAC地址 **IP->MAC **数据元为 frame 帧,帧的头部时使用MAC地址来标示源和目的地
    • ARP记录一个<IP,MAC,TTL> 的表,TTL是生存周期
  • RARP MAC->IP
    实现是在适配器上实现的,例如以太网卡
  • WAN 广域网 网络形式采用点到点链路
  • LAN 局域网 一般采用多点连接的方式
  • 奇偶校验
    • 单bit奇偶校验只能检测单个bit级别的错误,不能纠错
    • 二维奇偶校验可以检测和纠正单个bit错误
    • checksum
    • CRC 循环冗余校验

网络接口层:等价数据链路层,使用mac地址

  • NIC的驱动程序

  • NIC 是网络适配器,也就是网卡

  • PPP也属于数据链路层

物理层

OSI参考模型 有七个分层

自上而下 每一层的功能和作用由协议规定,协议的内容是规范

  1. 应用层

    • 针对特定应用的协议
  2. 表示层

    • 设备固有数据格式和网络标准数据格式 比如:接受不同表现形式的信息
  3. 会话层

    • 负责通信管理,负责建立连接和断开,管理传输层以下的分层
  4. 传输层

    • 管理两个节点之间的数据传输,负责可靠传输
  5. 网络层

    • 路由选择与地址管理
  6. 数据链路层

    • 互连设备之间传送,和 识别数据帧
  7. 物理层

    • 界定连接器和网线的规格,比特流和电子信号转换

网络安全

  • 对称加密:发送方和接收方的密钥相同
  • 公开密钥加密:发送方使用接收方的公钥进行加密,接收方使用自己的私钥进行解密
  • 数字签名:

国外的那些课的实验是可以写在简历上的比如 15445
苍穹外卖项目 + 服务器部署

海明码:https://www.cnblogs.com/godoforange/p/12003676.html
https://www.bilibili.com/video/BV1t4411e7LH?p=38&spm_id_from=pageDriver&vd_source=603b67fa519e58ee792d9e969a3ad8a1

基本名词解释

PC 程序计数器
IR 指令寄存器,存放当前正在执行的指令,
MAR
CU 控制单元,为控制器的核心部件,其功能是产生微操作命令序列。
ALU:Arithmetic Logic Unit,算术逻辑运算单元,为运算器的核心部件,其功能是进行算术、逻辑运算。
ACC:Accumulator,累加器,是运算器中既能存放运算前的操作数,又能存放运算结果的寄存器。
MQ:Multiplier-Quotient Register,乘商寄存器,乘法运算时存放乘数、除法时存放商的寄存器。
MAR:Memory Address Register,存储器地址寄存器,在主存中用来存放欲访问的存储单元的地址。
MDR:Memory Data Register,存储器数据缓冲寄存器,在主存中用来存放从某单元读出、或要写入某存储单元的数据。
I/O:Input/Output equipment,输入/输出设备,为输入设备和输出设备的总称,用于计算机内部和外界信息的转换与传送。

MIPS:Million Instruction Per Second,每秒执行百万条指令数,为计算机运算速度指标的一种计量单位。

CPI:执行一条指令所需的时钟周期(机器主频的倒数)。

FLOPS:浮点运算次数每秒。

主存储器

  • 汉明码
  1. 汉明码要添加 检测位要满足 2 ^k >= n + k +1 k位检测位 , n为总位数
  2. 检测位放在 2 ^i 次方处 i = 0 , 1 , ……

进制字母表示

十进制数用D表示,二进制用B表示,十六进制数用H表示,八进制用O表示。

Cache

命中率: 访问成功次数 / 总访问次数
命中率与Cache 的容量和块长有关
访问效率 = 访问Cache 的时间 / 平均访问时间 * 100 %
e 访问效率 , h 命中率, 访问Cache 时间为tc 访问主存时间为tm
则 e = tc / h * tc + ( 1 - h) * t * 100%
访问时间 / 命中的时间 + 未命中的时间

  • 读写操作
    • 写 : 1. 写直达法 : 写时同时写入Cache和主存
      1. 写回法: 只写入Cache 不写入内存,当Cache数据被替换出去的时候才写会主存,会导致Cache和主存的不一致
  • 地址映射
    1. 直接映射 Cache任意一块可以放在
    2. 全相联映射 主存任何一块可以放在Cache任意一块中

DMA

  1. DMA和 CPU交替访问
    CPU工作周期 C1 专供DMA访存,C2专攻CPU访存
  • 功能:
    1. 向CPU申请DMA传送
    2. 处理总线控制权的转交
    3. 管理系统总线,控制数据传输
    4. 确定数据传送的首地址和长度,修正传送过程中的数据和长度
    5. DMA传送结束时,给出操作完成的信号

浮点数: 阶码组成为 阶符 + 数值部分 尾数由数符和数值组成
阶码是用二进制来表示的比如
2 ^15
就是 2 ^1111
即可,所以要用四位二进制来表示

  • 规格化
    基数为
    2 要求尾数最高位为 1 2 ^1
    4 最高2位为1 2 ^2

8 最高3位为 1 2 ^3

左规,数据左移,尾数变大
小数转二进制,直接把分子写成二进制,然后根据分母是2的多少次方,移动小数点就行了

  • IEEE 754标准
    数符 + 阶码(含阶符) 尾数
    尾数使用规格化表示,非 0 的有效位最高位为1

  • 运算
    补码 左移时,后面加0 , 右移时前面加 1
    定点数运算:

  1. 补码加减运算,直接 A + B mod 2 ^n+1
  2. 小数 就mod 2

指令系统

  • 指令格式 : 操作码 + 地址码+寻址方式
    指令字长分为可变和固定字长
    RISC 精简指令系统
    长度可变的指令:操作码分散在指令字的不同字段中
  • 地址码
  1. 四地址 op + A1 + A2 + A3 + A4 分别是第一操作数地址,第二操作数地址,结果地址,下一条指令地址 (A1) OP(A2) –> A3
  2. 三地址 OP A1 A2 A3
  3. 二地址 OP A1 A2 (A1) OP (A2) –> A1 或者 (A1) OP(A2) -> A2
  4. 一地址 OP A1 (ACC) OP (A1) -> A1 ACC暂存
  5. 零地址,对ACC中的数据进行操作
    可以使用寄存器地址来替代指令地址字段,因为寄存器很少,占用的位数少
  • 指令寻址
    顺序 PC + 1 -> PC
    跳跃 由转移指令给出
    数据寻址: 操作码 + 寻址特征+ 形式地址A
    形式地址 是指令字中的地址,有效地址是操作数的真实地址
  1. 立即寻址: 形式地址A就是操作数,OP # A # 为立寻址标志 A 使用补码
  2. 直接寻址,EA = A 有效地址由形式地址给出,无偏移量
  3. 隐含寻址 间接寻址 有效地址由形式地址简介提供,A提供的是EA 的地址,需要去寻址,EA 的地址才是指向真实的数据
  4. 寄存器寻址,EA = Ri 有效地址为寄存器的编号
  5. 寄存器间接寻址: EA = (Ri) 有效地址在寄存器中
  • 基址寻址
    1. 使用专用寄存器作为基址寄存器
      EA = (BR) + A BR 为基址寄存器,也就是 物理地址 = 逻辑地址 + 基址地址

CPU 组成

  • 寄存器
    1. 通用寄存器 : 存放操作数,可作为某中寻址方式的专用寄存器
    2. 数据寄存器:存放操作数,两个寄存器拼接放双倍字长的数据
    3. 地址寄存器: 存放地址
    4. 条件码寄存器: 存放条件码,可作为程序分支的依据,正负,0,溢出等
    5. 控制和状态寄存器:
      • 控制寄存器 PC -> MAR -> M -> MDR -> IR
      • PC用户可见
      • 状态寄存器 存放条件码
      • PSW 存放程序状态字
  • 指令周期 : 取出并执行一条指令所需的全部时间
  • 包括 取指,分析 => 取指周期 执行=>执行周期

流水线

  • 吞吐率: 单位时间内流水线所完成执行或输出结果的数量
  • 最大吞吐率: Tpmax = 1 / Δt Δt为m段流水线各段时间
  • 加速比 :
    • 使用流水线的方式完成n条指令在m段流水线上共需 T = m * t + (n-1) * t
    • 使用等效的非流水线共需: T = nmt
    • 加速比等于 不使用 / 使用

简单的增删改查练习

数据库

## 服务器
给nginx设置负载均衡即可
## 具体编码
1. BeanUtils的使用
2. DigestUtils md5加密



- 一些借鉴
jwt拦截器,md5加密,全局错误处理,ThreadLocal 进行线程内部传递变量

PageHelper分页
```java
//设置分页和分页大小
    PageHelper.startPage(employeePageQueryDTO.getPage(), employeePageQueryDTO.getPageSize());
//进行查询
    Page<Employee> page = employeeMapper.pageQuery(employeePageQueryDTO);//后续定义

xml中的mapper示例:

<select id="pageQuery" resultType="com.sky.entity.Employee">
        select * from employee
        <where>
            <if test="name != null and name != ''">
                and name like concat('%',#{name},'%')
            </if>
        </where>
        order by create_time desc
    </select>

对于时间格式的全局管理,在mvcconfig中

protected void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
    log.info("扩展消息转换器...");
    //创建一个消息转换器对象
    MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
    //需要为消息转换器设置一个对象转换器,对象转换器可以将Java对象序列化为json数据
    converter.setObjectMapper(new JacksonObjectMapper());
    //将自己的消息转化器加入容器中
    converters.add(0,converter);
}
  • aop

  • Apache POI 来进行文件格式转换,导出为excel

  • Apache ECharts 可视化图表

  • Spring Task 定时任务

  • HttpClient
    HttpClient的核心API:

  • HttpClient:Http客户端对象类型,使用该类型对象可发起Http请求。

  • HttpClients:可认为是构建器,可创建HttpClient对象。

  • CloseableHttpClient:实现类,实现了HttpClient接口。

  • HttpGet:Get方式请求类型。

  • HttpPost:Post方式请求类型。

HttpClient发送请求步骤:

  • 创建HttpClient对象

  • 创建Http请求对象

  • 调用HttpClient的execute方法发送请求

需求分析

学生管理系统

一.功能要求

  • 添加学生功能:姓名、学号、性别、出生年月日。(学号自动生成且唯一)

  • 添加学生成绩功能:每个人都有数学、Java、英语、体育四门课,可分课程输入成绩。

  • 根据学生学号查找学生成绩功能:在界面上显示姓名、学号和成绩,学号不存在的能给出提示信息。

  • 根据学生姓名(支持模糊匹配)查找学生成绩功能:并在界面上显示姓名、学号和成绩,如果有多个相同姓名学生存在,一起显示出来,姓名不存在的给出提示信息。

  • 支持对单个学生各科成绩画出柱状分布图。

  • 学生信息的修改与删除功能:不能修改学号。

  • 生成学生学习情况报表功能:报表包含学号、姓名、各科目成绩及对应的该科目班级平均值,总成绩以及班级总成绩平均值,并将该排序结果输出至excel文件。

数据库设计

  1. admin
    id
    username 
    password
  2. student
    id (学号自动生成且唯一)
    name
    birthday
  3. score
    id
    Math
    Java
    English
    PE
    sql设计
CREATE DATABASE management;
USE management;
CREATE TABLE admin(
    Id INT AUTO_INCREMENT PRIMARY KEY,
    Username char(20) NOT NULL,
    Password char(20) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

CREATE TABLE student(
    Id INT AUTO_INCREMENT PRIMARY KEY,
    StudentId char(10) NOT NULL UNIQUE,
    StudentName char(20) NOT NULL,
    Birthday date NOT NULL
);

CREATE TABLE score(
    Id INT AUTO_INCREMENT PRIMARY KEY,
    Math DOUBLE NOT NULL,
    Java DOUBLE NOT NULL,
    English DOUBLE NOT NULL,
    PE DOUBLE NOT NULL
);

ps: 使用方式 mysql -u username -p < xxx.sql ,之后输入密码即可

xml配置
druid
knife4j生成接口文档

  • 部署
    服务器配置:
    mysql
    将服务器上的mysql配置文件修改
    vim /etc/mysql/mysql.conf.d/mysqld.cnf
    将bind-address 的127.0.0.1 改为  0.0.0.0 
    服务器开放端口 3306
    redis
    1.打开redis的配置文件“redis.conf”。

2.将“bind 127.0.0.1”注释掉。

3.将“protected-mode yes”改成“protected-mode no”。

4.添加以下一行代码。

daemonize no

复制

5.重启redis服务即可
sudo service redis restart

配置

knif4j注意设置好扫描的包

开发

  1. common module
    1. 返回类封装Result
    2. pageHelper所需的分页参数

苍穹外卖项目

仓库: 已完成 https://github.com/qiuEly/sky-take-out

知识

  1. 当前端传输的数据与实体类差距比较大时使用DTO来封装
  2. 数据传输对象(DTO)(Data Transfer Object),是一种设计模式之间传输数据的软件应用系统。数据传输目标往往是数据访问对象从数据库中检索数据。数据传输对象与数据交互对象或数据访问对象之间的差异是一个以不具有任何行为除了存储和检索的数据(访问和存取器)。
  3. ThreadLocal 不是一个线程,但是可以用于保存Thread的一个变量,相当于设置线程内部的变量,可以用来存token和cookie
  4. mybatis-plus 分页插件的使用,并且配合queryWrapper来封装自定义查询
  5. 使用配置jackson和java格式互相转化器
  6. ‘ALTER TABLE table_name AUTO_INCREMENT = value;’ 记得删除没有用的数据之后并且让value大于当前的行数
  7. mp 实现递增需要在配置文件中加上 ‘mybatis-plus:
    global-config:
    db-config:
    id-type: auto’
  8. 自定义注解:
    1. @Target 注解

      @Target注解:

      • 作用:@Target 注解用于指定可以将注解应用到的元素类型。它决定了注解可以用于标记哪些程序元素,例如类、方法、字段等。
      • 参数:@Target 的参数是一个 ElementType 枚举数组,你可以在其中指定一个或多个目标元素类型。对于 @ComponentScan 来说,@Target(ElementType.TYPE) 表示该注解可以用于标记类。
    2. @Retention 注解

      • 作用:@Retention 注解用于指定注解在编译后是否保留到运行时,并且是否可以通过反射访问注解。有三个可能的 RetentionPolicy 值:SOURCECLASSRUNTIME
      • 参数:@Retention 的参数是一个 RetentionPolicy 枚举值。@Retention(RetentionPolicy.RUNTIME) 表示注解会在运行时保留,并可以通过反射访问。
    3. @Documented 注解

      • 作用:@Documented 注解用于指示该注解应该包含在生成的文档中。如果你想要将注解的信息包含在 Java 文档中,可以使用 @Documented 注解。
      • 参数:@Documented 注解没有参数,它只是一个标记注解,用于指示文档工具要包括注解信息。
  9. 阿里云oss使用,主要使用的是spring 中的MultipartFile
  10. Redis的使用
  11. 通过给RestController设置标识名来防止依赖注入无法识别相同名称的Bean
  12. vivo50

    HttpClient的核心API:

  • HttpClient:Http客户端对象类型,使用该类型对象可发起Http请求。

  • HttpClients:可认为是构建器,可创建HttpClient对象。

  • CloseableHttpClient:实现类,实现了HttpClient接口。

  • HttpGet:Get方式请求类型。

  • HttpPost:Post方式请求类型。

HttpClient发送请求步骤:

  • 创建HttpClient对象

  • 创建Http请求对象

  • 调用HttpClient的execute方法发送请求
    用例:

    package com.sky.test;
    
    import org.apache.http.HttpEntity;
    import org.apache.http.client.methods.CloseableHttpResponse;
    import org.apache.http.client.methods.HttpGet;
    import org.apache.http.impl.client.CloseableHttpClient;
    import org.apache.http.impl.client.HttpClients;
    import org.apache.http.util.EntityUtils;
    import org.junit.jupiter.api.Test;
    import org.springframework.boot.test.context.SpringBootTest;
    
    @SpringBootTest
    public class HttpClientTest {
    
        /**
         * 测试通过httpclient发送GET方式的请求
         */
        @Test
        public void testGET() throws Exception{
            //创建httpclient对象
            CloseableHttpClient httpClient = HttpClients.createDefault();
    
            //创建请求对象
            HttpGet httpGet = new HttpGet("http://localhost:8080/user/shop/status");
    
            //发送请求,接受响应结果
            CloseableHttpResponse response = httpClient.execute(httpGet);
    
            //获取服务端返回的状态码
            int statusCode = response.getStatusLine().getStatusCode();
            System.out.println("服务端返回的状态码为:" + statusCode);
    
            HttpEntity entity = response.getEntity();
            String body = EntityUtils.toString(entity);
            System.out.println("服务端返回的数据为:" + body);
    
            //关闭资源
            response.close();
            httpClient.close();
        }
    }
  1. spring cache
    在SpringCache中提供了很多缓存操作的注解,常见的是以下的几个:
注解 说明
@EnableCaching 开启缓存注解功能,通常加在启动类上
@Cacheable 在方法执行前先查询缓存中是否有数据,如果有数据,则直接返回缓存数据;如果没有缓存数据,调用方法并将方法返回值放到缓存中
@CachePut 将方法的返回值放到缓存中
@CacheEvict 将一条或多条数据从缓存中删除

在spring boot项目中,使用缓存技术只需在项目中导入相关缓存技术的依赖包,并在启动类上使用@EnableCaching开启缓存支持即可。

  1. spring task 定时任务
  2. websocket
    WebSocket 是基于 TCP 的一种新的网络协议。它实现了浏览器与服务器全双工通信——浏览器和服务器只需要完成一次握手,两者之间就可以创建持久性的连接, 并进行双向数据传输。

HTTP协议和WebSocket协议对比:

  • HTTP是短连接

  • WebSocket是长连接

  • HTTP通信是单向的,基于请求响应模式

  • WebSocket支持双向通信

  • HTTP和WebSocket底层都是TCP连接

  1. Apache ECharts
  2. Apache POI
  3. 部署!!!
  1. 当springboot项目不识别application配置文件时,右键resource -> Mark Directory as > Resources Root

修改建议Todo:

  1. 密码加密 使用 security + jwt ,前端保存session
  2. 微信小程序开发 + 前端 vue
  3. docker环境下使用

需求

  1. admin 界面来管理

项目介绍

一次自己blog手动搭建记录
技术栈:

  • 前端:vue
  • 后端: express redis

HTML

  • head中的
    • meta的使用,description可以用来记录文章主要内容,方便搜索引擎使用
    • <link rel="icon" href="./favicon.ico"> 引入ico

Vue

文件结构:

  • api 存放封装了Ajax的请求文件
  • components 公共组件存放目录
  • views 视图组件
  • App.vue 主组件,入口文件
  • main.ts 项目的路口文件
  • router.ts路由文件

打包部署在nginx上

使用npm run build 进行打包,将dist包放在/usr/share/nginx/html
即可,然后root路径要写全
本地使用vue,基于脚手架安装

npm create vue@latest
cd xxx
npm install
npm run dev
  • 基础语法:
  • v-bind:xxx 绑定语法,可简写为:xxx 如果绑定的是多个值,比如是一个json数组,那么可以把xxx省略留下 v-bind: = aaa
  • v-on 监听事件 简写为 @xxx 动态绑定事件 @[事件列表] = "事件函数"
  • <template> 不会被渲染,一般作为v-if根
  • 第三个参数表示位置索引:
    <li v-for="(value, key, index) in myObject">
      {{ index }}. {{ key }}: {{ value }}
    </li>
  • setup是在建立渲染之前就会调用
  • 想要使用变量就必须要把变量return 出来即可
  • 使用语法糖可以简化代码,不需要一个个return了
    <script setup>  
       const msg = 'Hello Vite + Vue 3!'  
    </script>  
    <template>  
      <div id="app">  
        <h1>{{msg}}</h1>  
      </div></template>
  • 使用reactive函数来返回一个响应式对象
    <script setup>  
       import {reactive} from "vue";  
       const state = reactive({  
         msg: "hell world"  
       })  
       const setState = ()=>{  
         state.msg = "hello vite"  
       }  
    </script>  
    <template>  
      {{state.msg}}  
      <button @click="setState">click</button>  
    </template>
  • 使用ref来返回一个简单响应式对象
    <script setup>  
        import {ref} from "vue";  
        const msg = ref(0);  
    </script>  
    <template>  
      <div id="app">  
        <button @click="msg++">count is: {{ msg }}</button>  
      </div></template>
  • computed对象函数,里面传进去计算逻辑
    <script setup>  
    import {computed, ref} from "vue";  
      const list = ref([1,2,3,4,5]);  
      const filterlist = computed(()=>{  
        return list.value.filter(item=>item>3)  
      })  
    </script>  
    <template>  
      <div>    <ul>      <li v-for="item in filterlist" :key="item">{{item}}</li>  
        </ul>  </div></template>
  • watch 监听数据的变化,如果数据变化就执行回调函数,剩下两个参数,immediate 控制立刻执行,deep开启深度监听
    • 监听一个
      	<script setup>  
      import {ref, watch} from "vue";  
      const count = ref(0);  
      watch(count,(newCount, oldCount) => {  
        console.log(`new count is: ${newCount}, old count is: ${oldCount}`)  
      })  
      const increment = () => {  
        count.value++;  
      }  
      </script>  
      <template>  
      <button @click="increment">count is: {{count}}</button>  
      </template>
    • 监听多个数据,只需要把参数化成数组即可
      watch([count, name], ([newCount, newName],[oldCount,oldName])=>{
        console.log(`count或者name变化了,[newCount, newName],[oldCount,oldName])
      })
  • immediate 在创建时立刻出发
    watch(count, (newValue, oldValue)=>{
       console.log(`count发生了变化,老值为${oldValue},新值为${newValue}`)
     },{
       immediate: true
     })
  • deep通过watch监听的ref对象是浅层监听的,直接修改嵌套的对象属性是不会回调的,但是开启之后就可以回调了
    <script setup>
      // 1. 导入watch
      import { ref, watch } from 'vue'
      const state = ref({ count: 0 })
      // 2. 监听对象state
      watch(state, ()=>{
        console.log('数据变化了')
      })
      const changeStateByCount = ()=>{
        // 直接修改不会引发回调执行
        state.value.count++
      }
    </script>
    
    <script setup>
      // 1. 导入watch
      import { ref, watch } from 'vue'
      const state = ref({ count: 0 })
      // 2. 监听对象state 并开启deep
      watch(state, ()=>{
        console.log('数据变化了')
      },{deep:true})
      const changeStateByCount = ()=>{
        // 此时修改可以触发回调
        state.value.count++
      }
    </script>
    
  • 父组件传值给子组件: 1.引入子组件,使用子组件并绑定子组件中props中的属性 2. 子组件使用defineProps来接受父组件的传值
    父组件
    <script setup>  
    import son from './components/money.vue'  
    </script>  
      
    <template>  
      <son  message="hello world"/>  
    </template>
    <!--子组件-->
    <script setup>  
    const props =  defineProps({  
      message : String  
    })  
    </script>  
    <template>  
      {{message}}  
    </template>
  • 子组件传值给父组件: 1.子组件通过defineEmits来生成emit方法 2.子组件使用emit定义事件,并传递参数 3.父组件使用绑定子组件的事件,并绑定自己的函数,定义自己的函数使用传递的值
    子组件
    <script setup>  
    const props =  defineProps({  
      message : String  
    })  
    const emit = defineEmits(['say']) //事件列表  
    const hh = ()=>{  
      emit('say','hello world')  
    }  
    </script>  
    <template>  
      {{message}}  
      <button @click="hh">click</button>  
    </template>
    父组件
    <script setup>  
    import son from './components/money.vue'  
    const func = (msg,num)=>{  
      console.log(msg,num)  
    }  
    </script>  
    <template>  
      <son  messoage="hello world" @say="func"/>  
    </template>
  • 模板使用 新建一个ref 然后在 html 中的ref来进行绑定这个ref即可获得dom元素,但是会在onmounted之后才能访问
  • 父组件默认不会获得子组件的dom因为有setup 所以可以使用
    defineExpose({
    	需要暴漏的属性和方法名或者一个匿名函数
    })
  • 通过provide和inject来跨层传递
    顶层
    provide('key',value) value可以是函数等
    底层
    const value = inject('key')
  • v-text 来更新文本内容,v-html来更新html元素

Express

支持前后端分离的简单框架

npm install express-generator -g
express --no-view server 新建项目
cd server
npm install
ET DEBUG = server:* & npm start 开启服务
user www-data;
worker_processes auto;
pid /run/nginx.pid;
include /etc/nginx/modules-enabled/*.conf;

events {
        worker_connections 768;
        # multi_accept on;


        ##自定义服务列表

}
http {
        ##

        sendfile on;
        tcp_nopush on;
        types_hash_max_size 2048;
        # server_tokens off;

        # server_names_hash_bucket_size 64;
        # server_name_in_redirect off;

        include /etc/nginx/mime.types;
        default_type application/octet-stream;.
         ssl_protocols TLSv1 TLSv1.1 TLSv1.2 TLSv1.3; # Dropping SSLv3, ref: POODLE
        ssl_prefer_server_ciphers on;

        ##
        # Logging Settings
        ##

        access_log /var/log/nginx/access.log;
        error_log /var/log/nginx/error.log;

        ##
        # Gzip Settings
        ##

        gzip on;

        # gzip_vary on;
        # gzip_proxied any;
        # gzip_comp_level 6;
        # gzip_buffers 16 8k;
        # gzip_http_version 1.1;
        # gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;

        ##
        # Virtual Host Configs
        ##
          include /etc/nginx/conf.d/*.conf;
        include /etc/nginx/sites-enabled/*;



Node.js

  • 进度条

    const ProgressBar = require('progress')  
    const bar = new ProgressBar(':bar',{total:100})  
    const timer =  setInterval(()=>{  
        bar.tick()  
        if (bar.complete){  
            clearInterval(timer)  
        }  
    },100)
  • 文件操作 require(‘fs’)

  • 网络开发

    const net = require('net')  
    const server = net.createServer((c)=>{  
        console.log('客户端已连接')  
        c.on('end',()=>{  
            console.log('断开连接')  
        })  
    })  
    server.on('error',(err)=>{  
        throw err  
    })  
    server.listen(8124,()=>{  
        console.log('服务器已启动')  
    })

    on来绑定事件

  • udp 使用dgram模块

  • WebSocket 使用 ws模块,是对socket的具体实现

  • socket.io框架

  • 常用api

  • express框架

  • koa框架

  • mongoose

软件工程

描述软件需求的方法:

功能层次模型:一般来讲就是系统的功能图,模块分布图等描述整个系统的功能的分布和功能的层次结构;

数据流模型:就是以数据流为着眼点的分析方法得到的模型,主要通过数据在整个系统的流动情况来确定系统的主要功能主线和流程;

控制流模型:通过了解和界定系统中控制线,通过控制流的走向和控制的对象来确定系统的功能分布和控制与被控制的关系;

结构化分析(SA) 方法是一种面向数据流的需求分析方法,它适用于分析大型数据处理系统。结构化分析方法的基本思想是自顶向下逐层分解,这样做可以把一个大问题分解成若干个小问题,经过多次逐层分解,每个最底层的问题都是足够简单、容易解决的,这个过程就是分解的过程。

结构化方法的分析结果由数据流图DFD、数据词典和加工逻辑说明几个部分组成。其中,DFD的基本成分有数据流(data flow)、加工(process)、文件(file)和源/宿(source/sink)。

结构化设计(SD) 方法是一种面向数据流的设计方法,它可以与SA方法衔接。

结构化设计采用结构图(SC) 来描述程序的结构。其基本成分有模块、调用和输入/输出数据。

结构图:

 

条件调用                        循环调用

   在需求分析阶段用SA方法产生了数据流图(DFD) 。面向数据流的设计可以方便的将DFD转换成程序结构图。DFD从系统的输入数据流到系统的输出数据流的一连串连续变换形成一条信息流。DFD的信息流大体可分为两种类型:变换流和事务流。与之对应的也存在两种分析,变换分析和事务分析。变换分析是从变换流型的DFD导出程序结构图,而事务分析则是从事务流行型的DFD导出程序结构图。

SD方法的具体设计步骤为:

Ø         复查并精化数据流图

Ø         确定DFD的信息流类型

Ø         根据信息流类型分别将变换流或事务流转换成程序结构图

Ø         根据软件设计的原则对程序结构图作改进

  • 测试

白盒测试是根据程序的内部逻辑来设计测试用例,常用的技术是逻辑覆盖,即考察用例测试数据运行被测程序时对程序逻辑的覆盖程度。主要的覆盖标准有6种:

黑盒测试

黑盒测试时根据规格说明所规定的功能来设计测试用例,它不考虑程序的内部结构和处理过程。常用的黑盒测试技术有:

Ø         等价类划分

Ø         边值划分

Ø         错误猜测

Springboot练习

通过atguigu的今日头条项目进行项目驱动式学习
前端已准备

后端开发

要求:后端使用springboot整合mybatis和springmvc来进行简单的增删改查

  1. 导入依赖:
    • springboot启动包,springboot-web项目启动包,mybatis插件,数据库配置启动器springboot-starter-jdbc,druid启动器,mysql驱动类,lombok,aop,test,打包插件
  2. 编写配置类:
    mybatis的配置类可以使用yaml格式或者是properties格式的文件,推荐使用yaml格式的文件,有分层的效果
    [[Tools#yaml|查看Tools中的yaml]]
    菜鸟教程
    具体配置
    # server配置
    server:
      port: 8080
      servlet:
        context-path: / #默认的根路径
    
    # 连接池配置
    spring:
      datasource:
        type: com.alibaba.druid.pool.DruidDataSource
        druid:
          url: jdbc:mysql:///sm_db1
          username: root
          password: root
          driver-class-name: com.mysql.cj.jdbc.Driver
    
    # mybatis-plus的配置
    mybatis-plus:
      type-aliases-package: com.atguigu.pojo
      global-config:
        db-config:
          logic-delete-field: isDeleted  #全局逻辑删除
          id-type: auto #直接使用springboot来进行配置,就不需要再加上这个注解了
          table-prefix: news_ # 设置表的前缀
  3. druid兼容文件
    文件名:
    org.springframework.boot.autoconfigure.AutoConfiguration.imports
    内容:
    com.alibaba.druid.spring.boot3.autoconfigure.DruidDataSourceAutoConfigure
  4. 编写启动类main
    疑问: 什么是乐观锁和悲观锁
    配置使用的插件 [[Java#^f13de1]]
    教程
    @SpringBootApplication
    @MapperScan("com.atguigu.mapper")
    public class Main {
    
        public static void main(String[] args) {
            SpringApplication.run(Main.class,args);
        }
    
        //配置mybatis-plus插件
        @Bean
        public MybatisPlusInterceptor mybatisPlusInterceptor() {
            MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
            interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); //分页
            interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());  //乐观锁
            interceptor.addInnerInterceptor(new BlockAttackInnerInterceptor());  //防全局修改和删除
            return interceptor;
        }
    
    }
    
  5. 工具类封装:主要是统一返回结果的类
    结果封装类
/**
 * 全局统一返回结果类
 */
public class Result<T> { //T是要使用的泛型,要在这里声明
    // 返回码
    private Integer code;
    // 返回消息
    private String message;
    // 返回数据
    private T data;
    public Result(){}
    // 返回数据
    //泛型方法,要将 要使用的泛型在返回类型之前进行声明
    protected static <T> Result<T> build(T data) {
        Result<T> result = new Result<T>();
        if (data != null)
            result.setData(data);
        return result;
    }
    public static <T> Result<T> build(T body, Integer code, String message) {
        Result<T> result = build(body);
        result.setCode(code);
        result.setMessage(message);
        return result;
    }
    public static <T> Result<T> build(T body, ResultCodeEnum resultCodeEnum) {
        Result<T> result = build(body);
        result.setCode(resultCodeEnum.getCode());
        result.setMessage(resultCodeEnum.getMessage());
        return result;
    }
    /**
     * 操作成功
     * @param data  baseCategory1List
     * @param <T>
     * @return
     */
    public static<T> Result<T> ok(T data){
        Result<T> result = build(data);
        return build(data, ResultCodeEnum.SUCCESS);
    }
    public Result<T> message(String msg){
        this.setMessage(msg);
        return this;
    }
    public Result<T> code(Integer code){
        this.setCode(code);
        return this;
    }
    public Integer getCode() {
        return code;
    }
    public void setCode(Integer code) {
        this.code = code;
    }
    public String getMessage() {
        return message;
    }
    public void setMessage(String message) {
        this.message = message;
    }
    public T getData() {
        return data;
    }
    public void setData(T data) {
        this.data = data;
    }
}

解决枚举类
枚举类可以使用 常量(具体的常量内容来进行枚举)

/**
 * 统一返回结果状态信息类
 *
 */
public enum ResultCodeEnum {

    SUCCESS(200,"success"),
    USERNAME_ERROR(501,"usernameError"),
    PASSWORD_ERROR(503,"passwordError"),
    NOTLOGIN(504,"notLogin"),
    USERNAME_USED(505,"userNameUsed");

    private Integer code;
    private String message;
    private ResultCodeEnum(Integer code, String message) {
        this.code = code;
        this.message = message;
    }
    public Integer getCode() {
        return code;
    }
    public String getMessage() {
        return message;
    }
}

MD5加密工具类

import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;

@Component
public final class MD5Util {
    public static String encrypt(String strSrc) {
        try {
            char hexChars[] = { '0', '1', '2', '3', '4', '5', '6', '7', '8',
                    '9', 'a', 'b', 'c', 'd', 'e', 'f' };
            byte[] bytes = strSrc.getBytes();
//获得m5的实例
            MessageDigest md = MessageDigest.getInstance("MD5");
            md.update(bytes);
            bytes = md.digest();
            int j = bytes.length;
            char[] chars = new char[j * 2];
            int k = 0;
            for (int i = 0; i < bytes.length; i++) {
                byte b = bytes[i];
                chars[k++] = hexChars[b >>> 4 & 0xf];
                chars[k++] = hexChars[b & 0xf];
            }
            return new String(chars);
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
            throw new RuntimeException("MD5加密出错!!+" + e);
        }
    }
}
  1. 使用mybatisX插件,选中表之后逆向工程生成实体类和接口(注意自己补充和删减一些注释)
    @Data
    public class User implements Serializable {
        
        @TableId //主键
        private Integer uid;
    
        private String username;
    
        private String userPwd;
    
        private String nickName;
    
        @Version //版本
        private Integer version;
    
        @TableLogic //逻辑删除
        private Integer isDeleted;
    
        private static final long serialVersionUID = 1L;
    }
    补充:
    [[Java#^86b436||逻辑删除]]
  2. 使用jwt来生成[[Web学习#Token是一种令牌,用来识别访问人员的|Token]]
  3. JSON Web Token JWT由三部分组成: header(头部).payload(载荷).signature(签名)
    1. 导入依赖
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.1</version>
</dependency>

<dependency>
    <groupId>javax.xml.bind</groupId>
    <artifactId>jaxb-api</artifactId>
    <version>2.3.0</version>
</dependency>
  1. 编写配置

    application.yaml

    #jwt配置
    jwt:
      token:
        tokenExpiration: 120 #有效时间,单位分钟
        tokenSignKey: headline123456  #当前程序签名秘钥 自定义
  2. 导入工具类

    封装jwt技术工具类

    package com.atguigu.utils;
    
    import com.alibaba.druid.util.StringUtils;
    import io.jsonwebtoken.*;
    import lombok.Data;
    import org.springframework.boot.context.properties.ConfigurationProperties;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.stereotype.Component;
    
    import java.util.Date;
    
    @Data
    @Component
    @ConfigurationProperties(prefix = "jwt.token") //使用这个就可以省略前缀,如果后面的变量名和配置中相同的话就可以自动装配而不用手动装配了
    public class JwtHelper {
    
        private  long tokenExpiration; //有效时间,单位毫秒 1000毫秒 == 1秒
        private  String tokenSignKey;  //当前程序签名秘钥
    
        //生成token字符串
        public  String createToken(Long userId) {
            System.out.println("tokenExpiration = " + tokenExpiration);
            System.out.println("tokenSignKey = " + tokenSignKey);
            String token = Jwts.builder()
    
                    .setSubject("YYGH-USER")
                    .setExpiration(new Date(System.currentTimeMillis() + tokenExpiration*1000*60)) //单位分钟
                    .claim("userId", userId)
                    .signWith(SignatureAlgorithm.HS512, tokenSignKey)
                    .compressWith(CompressionCodecs.GZIP)
                    .compact();
            return token;
        }
    
        //从token字符串获取userid
        public  Long getUserId(String token) {
            if(StringUtils.isEmpty(token)) return null;
            Jws<Claims> claimsJws = Jwts.parser().setSigningKey(tokenSignKey).parseClaimsJws(token);
            Claims claims = claimsJws.getBody();
            Integer userId = (Integer)claims.get("userId");
            return userId.longValue();
        }
    
    
    
        //判断token是否有效
        public  boolean isExpiration(String token){
            try {
                boolean isExpire = Jwts.parser()
                        .setSigningKey(tokenSignKey)
                        .parseClaimsJws(token)
                        .getBody()
                        .getExpiration().before(new Date());
                //没有过期,有效,返回false
                return isExpire;
            }catch(Exception e) {
                //过期出现异常,返回true
                return true;
            }
        }
    }
    
  3. 编写controller
    知识点:跨域: 例如从不同的服务器或域名获取信息。

时间格式解决

timeformat

JAVA 快写

最短路与具体的路径记录问题

TreeMap

TreeMap<Integer, Integer> map = new TreeMap<>();
map.getOrDefault(key,0) 获得这个数,如果为空,返回默认值,可以自己设定
k = map.higherKey(k); 获得下一个顺序的键

把toCharArray向右移动一位

 a = (' '+ re.readLine()).toCharArray();

a ~ z 之间偏移量确认

 a[i] = (char)('a' + ((a[i] + offset - 'a')%26));

看到唯一解,且是难以做的图形题,可以考虑转化为方程

https://www.lanqiao.cn/problems/17160/learning/?contest_id=179

卡特兰数问题 :h[n]=C[2n,n]−C [2n,n−1] (n=0,1,2,…) 组合数C不解释了; C是组合数

  1. 出栈顺序问题 假设有N个数字依次入栈:1,2,3,…,n,试问有多少种出栈顺序?这里为表述简便,下文用+1表示一个元素入栈,用-1表示一个元素出栈
  2. 问题描述:有n对()括号,试问可以组成多少种合法正确的括号序列?

树是无向边

Java日期

LocalDate d1 = LocalDate.parse(s,java.time.format.DateTimeFormatter.BASIC_ISO_DATE);
		s = reader.readLine();
		LocalDate d2 = LocalDate.parse(s,java.time.format.DateTimeFormatter.BASIC_ISO_DATE);
		long diff = ChronoUnit.DAYS.between(d1, d2);
		System.out.println(Math.abs(diff)+1);
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(System.out));
writer.flush() 才会写

JAVA更快更强的读入和写 https://www.luogu.com.cn/problem/P2367

 StreamTokenizer reader = new StreamTokenizer(new BufferedReader(new InputStreamReader(System.in)));

reader.nextToken();
n = (int) reader.nval;

取余数的小技巧

(x + n) % n 可以保证不会出现负数

先思考再做题,前几个题大概率时模拟题,所以别急着用算法

常用算法

next_permutation(a,a+n); 对数组的前n个数进行全排列,并存储在这个数组中
char a[n][n]
puts(a[i]) 可以直接输出一行,如果不是最后一行还会输出换行符
求 q^0 + q^1 + ..... + q^n
long long t = 1;
  while(n--){
    //秦九zhao算法
    t = (t *q + 1) %mod;
} 

整数划分https://ac.nowcoder.com/acm/problem/252724

accumulate 求和

快乐的模板:

#include <bits/stdc++.h>  
#define ll long long   
#define pii pair<int,int>  
using namespace std;  
void solve(){  
    return ;  
}  
int main(){  
    ios::sync_with_stdio(false);  
    cin.tie(nullptr);  
    int T;  
    cin >> T;  
    while (T--) solve();  
    return 0;  
}  
​```


## 小TIPS:

做题思路:

1. 从小数据,小范围推大范围
    
2. 划分,以及反证,如果要求全部满足一个性质,那么只要有部分不满足我们已经推出来的条件即可不满足所有性质
    

> 1. 数组和字符串比较字典序是可以直接用大于号小于号比较的
>     
> 2. 字典序是指在ASCII码中出现的顺序所以 也就是 a b c 0 1 2 3 ABC 等z
>     
> 3. vector <> 可以直接赋值
>     

## 牛顿迭代法:

求平方根

例子: f(x)=m,可转化为 g(x)=f(x)-m=0;

迭代公式:_x_n+1 = _x_n − _g_ (_x_n)/ _g_ ′ (_x_n)

例题:[力扣(LeetCode)官网 - 全球极客挚爱的技术成长平台](https://leetcode.cn/problems/sqrtx/submissions/431189596/)
```c++
//f (x) = x2 − a = 0  
int mySqrt(int a) {  
long x = a;  
while (x * x > a) {  
x = (x + a / x) / 2;  
}  
return x;  
}

stl和一些内置函数

accumulate(num.begin(),num.end(),0); //第三个参数是初始化要返回的东西

排序

快速排序模板 根据数来分治

先找数字中的中位数,然后递归
注意: while中先递归的左边,那么最后递归的时候就要以j来为界限,递归的顺序无所谓

void quick_sort(int q[], int l, int r)
{
    if (l >= r) return;
    int x = q[rand()% (r-l+1)+l]; // 随机取
    int i = l - 1, j = r + 1; 
    while (i < j) {
        do i++; while (
        q[i] < x);
        do j--; while (q[j] > x);
        if (i < j) swap(q[i], q[j]);    //如果i与j没有相遇,就交换一下
    }

    quick_sort(q, l, j);    //递归处理左右两边
    quick_sort(q, j + 1, r);
}

归并排序模板 分治 根据中间两个数为分界线

  1. 确定分界点, mid=(l+r)/2

  2. 递归排序 left,right

  3. 归并 合二为一

模板


void msort(int a[], int l, int r) {  
    if (l >= r) return;  
    //确定分界  
    int mid = l + r >> 1;  
    //递归  
    msort(a, l, mid); msort(a, mid + 1, r);  
    //归并  
    int k = 0, i = l, j = mid + 1;  
      
    while (i <=mid && j <= r) {//左右比较,小的放在辅助数组里,直到有一个指针到达边界  
        if (a[i] <= a[j]) tmp[k++] = a[i++];  
        else tmp[k++] = a[j++];  
    }  
    //这里继续把另一个没到边界的指针赋值给辅助数组  
    while (i <= mid) tmp[k++] = a[i++];  
    while (j <= r)tmp[k++] = a[j++];  
    //最后把辅助数组的元素还回去  
    for (int i = l, j = 0; i <= r; i++, j++) a[i] = tmp[j];  
}

二分法 二分要保证有解

整数二分

一分为二,一边满足性质,一半不满足,可以来用来寻找性质的边界

两种模板:一种去检查满足的一半,另一种去检查不满足性质的的一半

考虑边界是否会包括进去

  1. 先写出 mid=r+l>>1

  2. 二分要检查的性质

  3. 画图考虑,直线图

  4. 思考mid是否会包含

  5. 考虑不存在条件时

l = mid + 1 时,输出的是 L , r = mid -1 时 输出的是r

注意死循环,男左女右,查找从右侧往左的时候mid 要 + 1,否则不+1

l + (r - l + 1 >> 1); 
​
更好的二分模板
while(l<=r){
	mid = (l + r) >> 1;
	if(mid < r) r = mid - 1;
	else  mid = l + 1;
}

浮点数二分 不要处理边界

思路:通过mid来判断,答案落在缩小的区间内,只要近似值

bool check(double x) {/* ... */} // 检查x是否满足某种性质  
  
double bsearch_3(double l, double r)  
{  
    const double eps = 1e-6;   // eps 表示精度,取决于题目对精度的要求  
    while (r - l > eps)  
    {  
        double mid = (l + r) / 2;  
        if (check(mid)) r = mid;  
        else l = mid;  
    }  
    //或者直接不管精度,直接循环几百次  
    return l;  
}

高精度

加法: 注意要把A设为位数更大的那一个,因为最后的位数取决于位数大的那一个,使用vector容器可以更方便的进行计算和进位和确定位数

  1. 逆序存数的每一位

  2. 从低位开始计算,之后计算进位

  3. 加完之后检查最后一位是否还有进位

  4. 返回数字

vector<int> add(vector<int> &A, vector<int> &B)  
{  
    if (A.size() < B.size()) return add(B, A);  
  
    vector<int> C;  
    int t = 0;  
    for (int i = 0; i < A.size(); i ++ )  
    {  
        t += A[i];  
        if (i < B.size()) t += B[i];  
        C.push_back(t % 10);  
        t /= 10;  
    }  
  
    if (t) C.push_back(1);  
    return C;  
}

减法

和加法基本一致,只要变进位为借位即可

// C = A - B, 满足A >= B, A >= 0, B >= 0

vector<int> sub(vector<int> &A, vector<int> &B)  
{  
    vector<int> C;  
    for (int i = 0, t = 0; i < A.size(); i ++ )  
    {  
        t = A[i] - t;  
        if (i < B.size()) t -= B[i];  
        C.push_back((t + 10) % 10);  
        if (t < 0) t = 1;  
        else t = 0;  
    }  
  
    while (C.size() > 1 && C.back() == 0) C.pop_back();  
    return C;  
}

乘法

vector<int> mul(vector<int>&A,int b){  
   vector<int>C;  
   int t=0;  
   for(int i=0;i<A.size()||t;i++){//出现进位  
      if(i<A.size()) t+=A[i]*b;  
      C.push_back(t%10);  
      t/=10;  
   }  
   while(C.size()>1&&C.back()==0) C.pop_back();  
   //去除前导零  
   return C;  
}
### 除法

vector<int> div(vector<int>&A,int b){  
   vector<int>C;  
   int t=0;  
   for(int i=0;i<A.size();i++){  
      t=t*10+A[i];  
      C.push_back(t/b);  
      t %= b;  
   }  
   //这里是清除前置零,不是后置零
   reverse(C.begin(),C.end());  
   while(C.size()>1&&C.back()==0) C.pop_back();  
   return C;  
}

前缀和和差分

构造差分可以用一个空数组,一直执行插入操作即可

前缀和一般初始化为0到n但是只用1到n

二维

S[i, j] = 第i行j列格子左上部分所有元素的和
以(x1, y1)为左上角,(x2, y2)为右下角的子矩阵的和为:
S[x2, y2] - S[x1 - 1, y2] - S[x2, y1 - 1] + S[x1 - 1, y1 - 1]

二维差分: 差分的前缀和就是原数组

给以(x1, y1)为左上角,(x2, y2)为右下角的子矩阵中的所有元素加上c:
S[x1, y1] += c, S[x2 + 1, y1] -= c, S[x1, y2 + 1] -= c, S[x2 + 1, y2 + 1] += c//注意是x2+1,y2+1不是x2,y2

双指针

int x;  
int * p1 = &x; // 指针可以被修改,值也可以被修改  
const int * p2 = &x; // 指针可以被修改,值不可以被修改(const int)  
int * const p3 = &x; // 指针不可以被修改(* const),值可以被修改  
const int * const p4 = &x; // 指针不可以被修改,值也不可以被修改

for (int i = 0, j = 0; i < n; i ++ )  
{  
    while (j < i && check(i, j)) j ++ ;  
  
    // 具体问题的逻辑  
}  

常见问题分类:
(1) 对于一个序列,用两个指针维护一段区间
(2) 对于两个序列,维护某种次序,比如归并排序中合并两个有序序列的操作

Floyd判圈法 龟兔赛跑法 用于判断链表有无环和求出环的长度

两个指针fast slow 都从起始位置出发,fast 一次走2步,slow一次走1步,如果能相遇,则存在环

计算环的长度

让其中一个指针停在环的起点不动,另一个一步一步向前走并记录步数,再次相遇时步数即为环的长度。

寻找环的起点

其中一个指针在环的起点不动,另一个放到起点,两个指针同时一步一步移动,则两指针将会在循环节的起点相遇。

142. 环形链表 II - 力扣(Leetcode)

/**

  • Definition for singly-linked list.
  • struct ListNode {
  • int val;  
    
  • ListNode *next;  
    
  • ListNode(int x) : val(x), next(NULL) {}  
    
  • };
    */
    class Solution {
    public:
    ListNode *detectCycle(ListNode *head) {
    ListNode *fast=head,*slow=head;
    do{
    if(!fast||!fast->next) return NULL;//如果能到达末尾,则不存在环
    fast=fast->next->next;
    slow=slow->next;
    }while(fast!=slow);
    fast=head;
    while(fast!=slow){
    fast=fast->next;
    slow=slow->next;
    }
    return slow;
    }
    };

位运算

思路:

n的第k位是什么? n>>k&1 右移k位与1与得到是0就是0,反之就是1

  1. 先把要判断的位置移到最左边

  2. 判断

lowbit

解释:cpp的负数使用的补码表示的所以,-x就等于 ~x+1 反码+1

用法:树状数组和求1的个数



import java.util.Scanner;
// 1:无需package
// 2: 类名必须Main, 不可修改

public class Main {
    static int N = (int) 1e5 + 10;
    static long[] tree = new long[N];
    static int n ;
    static int lowbit(int x) {
    	return x & (-x);
    }
    static void update(int i ,int x) {
    	for(int pos = i ; pos <= n; pos += lowbit(pos)) {
    		tree[pos] += x;
    	}
    }
    static long query(int x) {
    	long sum =0;
    	for(int pos = x; pos > 0; pos-=lowbit(pos)) {
    		sum +=tree[pos];
    	}
    	return sum;
    }
    static long ask(int a,int b) {
    	return query(b) - query(a-1);
    }
    public static void main(String[] args) {
        Scanner scan = new Scanner(System.in);
        n = scan.nextInt();
        int m =scan.nextInt();
        for(int i = 1; i <= n; i ++) {
        	int x = scan.nextInt();
        	update(i,x);
        }
        while(m-- > 0) {
        	int k = scan.nextInt();
        	int a = scan.nextInt();
        	int b = scan.nextInt();
        	if(k == 0) {
        		System.out.println(ask(a,b));
        	} else {
        		update(a, b);
        	}
        }
        scan.close();
    }
}

树状数组:对差分和前缀和的利用:

使用情况:

  1. 数组不变求区间和

  2. 多次修改某个区间,求区间和

  3. 将某个区间变为同一个数求区间和

  4. 多次修改区间,寻找定点值

模板:

说明:lowbit()奇妙用法…..

#define low
```bit(x) (-x)&x  
///或者 int lowbit(int x) {return -x&x;}  
int tree[length];//树状数组,长度和原数组相等  
//区间修改  
void update(int x,int val)  
{  
    while(x){  
        tree[x]+=val;  
        x+=lowbit(x);  
    }  
}  
//区间求和  
int sum(int l,int r){  
    int ans=0;  
    while(r){  
        ans+=tree[r];  
        r-=lowbit(r);  
    }  
    l--;//  
    while(l){  
        ans-=tree[l];  
        l-=lowbit(l);  
    }  
    return ans;  
}

使用方式:

  1. 正常的顶点修改,区间求和直接用

  2. 区间修改,求单独一个数 P3368 【模板】树状数组 2 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

    • 树状数组是保存的每一个下标为位置的前缀和

    • 想要求指定位置的数值需要用到差分,差分的前缀和就是每一个位置的数据大小

    • 修改时只需要修改 update(l,val),update(r+1,-val)

    • 初始化时,要插入的是差分

      #include <iostream>  
      #include <algorithm>  
      #define ll long long  
      #define lowbit(x) (x&(-x))  
      using namespace std;  
      const int mx = 10e5 + 5;  
      int t[mx], m, n;  
      int add(int x, int k) {  
      	while (x <= n) {  
      		t[x] += k;  
      		x += lowbit(x);  
      	}  
      }  
      int query(int x){  
      	ll ans=0;  
      	while(x){  
      		ans+=t[x];  
      		x-=lowbit(x);  
      	}  
      	return ans;  
      }  
      //用差分来的前缀和来表示每一位置上的数字,  
      //第一个数字之后,每次把差分加入,再求前缀和就能得到每一个位置上的数字是多少了  
      //修改时只需要修改x和y+1两个位置的差分,但是我们用的tree是前缀和,所以依然要用把和lowbit有关的都修改 了   
      int main() {  
      	cin >> n >> m;  
      	int cf=0;   
      	for (int i = 1; i <= n; i++) {  
      		int num;  
      		scanf("%d", &num);  
      		add(i,num-cf);  
      		cf=num;  
      	}  
      	  
      	for (int i = 1; i <= m; i++) {  
      		int ch, x, y,k;  
      		scanf("%d", &ch);  
      		if (ch == 1){  
      			scanf("%d %d %d", &x, &y, &k);  
      			add(x,k);  
      			add(y+1,-k);  
      		}  
      		else {  
      			int s;  
      			scanf("%d", &s);  
      			printf("%d\n", query(s));  
      		}  
      	}  
      	return 0;  
      }  
      
  3. 用指定数据替换某个数据,然后求和 307. 区域和检索 - 数组可修改 - 力扣(Leetcode)

    • 更换数据也是用到了差分,新的数据-原数据==要更新的val

    • 然后更新之后,原数组指定位置也要更新,方便下次修改同一位置

    • 其他正常食用即可

      class NumArray {  
          int []t;  
          int n;  
          int []nums;  
          int lowbit(int x){return x&(-x);}  
          public NumArray(int[] nums) {  
              this.nums=nums;  
              n=nums.length;  
              t=new int[n+1];  
              int i=1;  
              for (int num:nums  
                   ) {  
                  add(i++,num);  
              }  
          }  
          void add(int index,int val){  
              while(index<=n){  
                  t[index]+=val;  
                  index+=lowbit(index);  
              }  
          }  
          public void update(int index, int val) {  
              add(index+1,val-nums[index]);  
              nums[index]=val;  
          }  
            
          public int sumRange(int left, int right) {  
              int ans=0;  
              right++;  
              while(right>0){  
                  ans+=t[right];  
                  right-=lowbit(right);  
              }  
        
              while(left>0){  
                  ans-=t[left];  
                  left-=lowbit(left);  
              }  
              return ans;  
          }  
      }  
        
      /**  
       * Your NumArray object will be instantiated and called as such:  
       * NumArray obj = new NumArray(nums);  
       * obj.update(index,val);  
       * int param_2 = obj.sumRange(left,right);  
       */

求1的个数

#include <algorithm>  
#include <vector>  
#include <iostream>  
#define lowbit(x) (-x)&x  
using namespace std;  
int main(){  
    int n,ans=0;  
    cin>>n;  
    while(n){  
        ans++;  
        n-=lowbit(n);  
    }  
    cout<<ans<<endl;  
    return 0;  
}

离散化

unique 返回的是下下标
vector<int> alls; // 存储所有待离散化的值  
sort(alls.begin(), alls.end()); // 将所有值排序  
alls.erase(unique(alls.begin(), alls.end()), alls.end());   // 去掉重复元素  
//配合erase 即可把放在后面的重复元素删除  
/*  
该函数的作用是“去除”容器或者数组中相邻元素的重复出现的元素  
(1) 这里的去除并非真正意义的erase,而是将重复的元素放到容器的末尾,返回值是去重之后的尾地址。   
(2) unique针对的是相邻元素,所以对于顺序顺序错乱的数组成员,或者容器成员,需要先进行排序,可以调用std::sort()函数  
// 二分求出x对应的离散化的值*/  
int find(int x) // 找到第一个大于等于x的位置  
{  
    int l = 0, r = alls.size() - 1;  
    while (l < r)  
    {  
        int mid = l + r >> 1;  
        if (alls[mid] >= x) r = mid;  
        else l = mid + 1;  
    }  
    return r + 1; // 映射到1, 2, ...n  
}

区间和并

每次维护一个右端点

// 将所有存在交集的区间合并  
void merge(vector<PII> &segs)  
{  
    vector<PII> res;  
  
    sort(segs.begin(), segs.end());//根据first进行排序,默认的就是这样的不需要进行自定义  
  
    int st = -2e9, ed = -2e9;  
    for (auto seg : segs)  
        if (ed < seg.first)  
        {  
            //当起点的值大于右端点的时候,一段区间结束,可以继续下一段区间了  
            if (st != -2e9) res.push_back({st, ed});  
            st = seg.first, ed = seg.second;  
        }  g
        else ed = max(ed, seg.second);//如果左没大于右端点,那么右端点每次更新完为最大值  
  
    if (st != -2e9) res.push_back({st, ed});  
    segs = res;  
}

数组模拟链表

单链表


int head,e[N],ne[N],idx;  
void init(){  
    head=-1;  
    idx=0;  
}  
//头插  
//head 也是指针,e[idx] 新节点  
void add_head(int x){  
    e[idx]=x;//插入数据  
    ne[idx]=head;//idx 当前的位置的指针指向head指向的位置 -1  
    head=idx;//head 指针指向idx   
    idx++;  
}  
//插入任意位置  
void insert_linkedlist(int k,int x){  
    e[idx]=x;//建立新节点  
    ne[idx]=ne[k];//新节点指向k的下一个节点  
    ne[k]=ne[idx]; //k指向idx这个结点  
    idx++;  
}  
void delete_linkedlist(int k){  
    ne[k]=ne[ne[k]];  
}  
int main(){  
    int k,x,m;  
    char op;  
    cin>>m;  
    init();  
    while(m--){  
        cin>>op;  
        if(op=='h'){  
            cin>>x;  
            add_head(x);  
        }  
        else if(op=='d'){  
            cin>>k;  
            delete_linkedlist(k-1);  
        }  
        else {  
            cin>>k>>x;  
            insert_linkedlist(k-1,x);  
        }  
    }  
    for(int i=head;i!=-1;i=ne[i]){cout<<e[i]<<' ';}  
    return 0;  
}

双链表

// e[]表示节点的值,l[]表示节点的左指针,r[]表示节点的右指针,idx表示当前用到了哪个节点  
int e[N], l[N], r[N], idx;  
  
// 初始化  
void init()  
{  
    //0是左端点,1是右端点  
    r[0] = 1, l[1] = 0;  
    idx = 2;  
}  
  
// 在节点a的右边插入一个数x  
void insert(int a, int x)  
{  
    e[idx] = x;  
    l[idx] = a, r[idx] = r[a];  
    l[r[a]] = idx, r[a] = idx ++ ;  
}  
  
// 删除节点a  
void remove(int a)  
{  
    l[r[a]] = l[a];  
    r[l[a]] = r[a];  
}  

// tt表示栈顶  
int stk[N], tt = 0;  
  
// 向栈顶插入一个数  
stk[ ++ tt] = x;  
  
// 从栈顶弹出一个数  
tt -- ;  
  
// 栈顶的值  
stk[tt];  
  
// 判断栈是否为空,如果 tt > 0,则表示不为空  
if (tt > 0)  
{  
  
}

单调栈

注意这个题目要的是结果的下标不是具体的数据

用栈暴力模拟一遍,然后再考虑哪些元素没有用处,就可以排除

P5788 【模板】单调栈 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

#include<iostream>  
#include <cstdio>  
#define ll long long   
using namespace std;  
const ll N=3*1000000+1;  
ll stk[N],a[N],ans[N];  
int tt=0;  
int main(){  
    int n;  
    cin>>n;  
    for(int i=1;i<=n;i++){  
        scanf("%lld",&a[i]);  
    }  
    for(int i=n;i>0;i--){  
        while(tt!=0&&a[i]>=a[stk[tt]]) tt--;  
        ans[i]= tt==0?0:stk[tt];  
        stk[++tt]=i;  
    }  
    for(int i=1;i<=n;i++)printf("%lld ",ans[i]);  
    return 0;  
}

队列

// hh 表示队头,tt表示队尾  
int q[N], hh = 0, tt = -1;  
  
// 向队尾插入一个数  
q[ ++ tt] = x;  
  
// 从队头弹出一个数  
hh ++ ;  
  
// 队头的值  
q[hh];  
  
// 判断队列是否为空,如果 hh <= tt,则表示不为空  
if (hh <= tt)  
{  
  
}

单调队列 (好东西)

几个点:

  • 初始化时,hh=0,tt=-1 使得队列为空

  • 比较的是队尾元素与当前元素

  • 注意队列长度为0时不要输出

常见模型:找出滑动窗口中的最大值/最小值

int hh = 0, tt = -1;  
for (int i = 0; i < n; i ++ )  
{  
    while (hh <= tt && check_out(q[hh])) hh ++ ;  // 判断队头是否滑出窗口  
    while (hh <= tt && check(q[tt], i)) tt -- ;  
    q[ ++ tt] = i;  
}

例题

P1886 滑动窗口 /【模板】单调队列 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

#include<iostream>  
#include <cstdio>  
#define ll long long   
using namespace std;  
const int N=1000000+2;  
int n,k,a[N],q[N],ans,hh,tt;  
  
int main(){  
    cin>>n>>k;  
    for(int i=0;i<n;i++){  
        scanf("%d",&a[i]);  
    }  
    //队列存的是下标  
    //最小值  
    hh=0;tt=-1;//目的是让队列初始化为空  
    for(int i=0;i<n;i++){  
        //判断队列是否为空  
        if(hh<=tt&&i-k+1>q[hh]) hh++;  
        //目的是把最小的元素放在队头  
        while(hh<=tt&&a[q[tt]]>=a[i]) tt--;//从队尾删除,因为经过我们的处理,已经是严格单调递增的了,所以如果第一个都大于这个元素的话,那么后面几个都大于,所以要删除  
        q[++tt]=i;  
        if(i>=k-1)  
        printf("%d ",a[q[hh]]);  
    }  
    cout<<endl;  
    //最大值  
      hh=0;tt=-1;  
    for(int i=0;i<n;i++){  
        //判断队列是否为空  
        if(hh<=tt&&i-k+1>q[hh]) hh++;  
        //目的是把最大的元素放在队头  
        while(hh<=tt&&a[q[tt]]<=a[i]) tt--;  
        q[++tt]=i;  
        if(i>=k-1)  
        printf("%d ",a[q[hh]]);  
    }  
      
    return 0;  
}

KMP

对称的 ,以j这个点为中点的前后缀是相同的,所以可以直接变成next[ j ]
image-20230511171657614

s从1开始,p从0开始

// s[]是长文本,p[]是模式串,n是s的长度,m是p的长度  
求模式串的Next数组:ne[1]=0  ,一开始就错了肯定从零开始
for (int i = 2, j = 0; i <= m; i ++ )  
{  
    while (j && p[i] != p[j + 1]) j = ne[j];  
    if (p[i] == p[j + 1]) j ++ ;  
    ne[i] = j;  
}  
  
// 匹配  
for (int i = 1, j = 0; i <= n; i ++ )  
{  
    while (j && s[i] != p[j + 1]) j = ne[j];  
    if (s[i] == p[j + 1]) j ++ ;  
    if (j == m)  
    {  
        j = ne[j];  
        // 匹配成功后的逻辑  
    }  
}

例题

P3375 【模板】KMP字符串匹配 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)


#include <iostream>  
#include <cstring>  
#define N 1000010  
using namespace std;  
char s[N],p[N];  
int ls,lp,ne[N];  
int main(){  
    cin>>s+1>>p+1;  
    ls=strlen(s+1);  
    lp=strlen(p+1);  
    for(int i=2,j=0;i<=lp;i++){  
        while(j&&p[i]!=p[j+1]) j=ne[j];  
        if(p[j+1]==p[i]) j++;  
        ne[i]=j;  
    }  
     for(int i=1,j=0;i<=ls;i++){  
        while(j&&s[i]!=p[j+1]) j=ne[j];  
        if(s[i]==p[j+1]) j++;  
        if(j==lp){  
            cout<<i-lp+1<<endl;  
            j=ne[j];  
        }  
    }  
    for(int i=1;i<=lp;i++){  
        cout<<ne[i]<<" ";  
    }  
    return 0;  
}

Trie树 高效存储和查找字符串

集合的数据结构

将字符串分解为一个一个单独的字符然后存储,然后查询这个字符串是否出现过,

出现过几次

更全面 的映射

int getnum(char x){  
    if(x>='A'&&x<='Z')  
        return x-'A';  
    else if(x>='a'&&x<='z')  
        return x -'a'+26;  
    else  
        return x-'0'+52;  
} 

#include<iostream>  
#include <algorithm>  
#include <cstdio>  
using namespace std;  
const int N=100010;  
int son[N][26],cnt[N],idx;//子子节点的个数,只包含26个 小写字母 cnt是以这个字母为结尾的单词出现了福哦少个  
//idx 当前用到哪了  
//插入操作  
char str[N];  
void insert(char str[]){  
    int p=0;//当前的结点  
    for(int i=0;str[i];i++){  
        int u=str[i]-'a';//将26个小写字母映射为数字  
        if(!son[p][u]) son[p][u]=++idx;  
        p=son[p][u];  
    }  
    cnt[p]++;  
}  
int query(char str[]){  
    int p=0;  
    for(int i=0;str[i];i++){  
        int u=str[i]-'a';  
        if(!son[p][u]) return 0;  
        p=son[p][u];  
    }  
    return cnt[p];  
}  
int main(){  
    int n;  
    cin>>n;  
    while(n--){  
       char op[2];  
       cin>>op>>str;  
       if(op[0]=='i') insert(str);  
       else cout<<query(str)<<endl;  
    }  
    return 0;  
}

P8306 【模板】字典树 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

#include<iostream>  
#include <algorithm>  
#include <cstdio>  
#include <cstring>  
using namespace std;  
const int N=3000005;  
int son[N][65],cnt[N],idx;//子子节点的个数,只包含26个 小写字母 cnt是以这个字母位借位的单词出现了福哦少个  
//idx 当前用到哪了  
//插入操作  
 int n,m,t;  
char str[N];  
int hashs(char x){  
    if(x>='A'&&x<='Z')  
        return x-'A';  
    else if(x>='a'&&x<='z')  
        return x-'a'+26;  
    else  
        return x-'0'+52;  
}  
void insert(char str[]){  
    int p=0;//当前的结点  
    int l=strlen(str);  
    for(int i=0;i<l;i++){  
        int u=hashs(str[i]);//将26个小写字母映射为数字  
        if(!son[p][u]) son[p][u]=++idx;  
        p=son[p][u];  
        cnt[p]++;  
    }  
      
}  
int query(char str[]){  
    int p=0;//当前的结点  
    int l=strlen(str);  
    for(int i=0;i<l;i++){  
        int u =hashs(str[i]);  
        if(!son[p][u]) return 0;  
        p=son[p][u];  
    }  
    return cnt[p];  
}  
int main(){  
     
    cin>>t;  
    while(t--){  
          for(int i=0;i<=idx;i++){  
            for(int j=0;j<=122;j++){  
                son[i][j]=0;  
            }  
          }  
          for(int i=0;i<=idx;i++)  
            cnt[i]=0;  
        idx=0;  
       scanf("%d%d",&n,&m);  
        while(n--){  
            scanf("%s",str);  
            insert(str);  
        }  
        while(m--){  
             scanf("%s",str);  
            printf("%d\n",query(str));  
        }  
    }  
    return 0;  
}

并查集

用法:

某些点或者数据是否处于一个连通块中

  1. 将两个集合合并

  2. 询问两个元素是否在一个集合中

基本原理:每个集合用一个树来表示,树根的编号就是整个集合的编号,

每一个结点表示他的父节点p[x] 表示x的父节点

  • 判断树根: if(p[x]==x)

  • 如何集合的编号: while(p[x]!=x) x=p[x];

  • 如何合并两个集合直接让其中一个的根节点的父节点为另一个集合的根节点就行

  • 如何优化,查询一次后,将将所经过的路径的父节点全都修改为根节点

#include<iostream>  
#include <algorithm>  
#include <cstdio>  
#include <cstring>  
using namespace std;  
const int N=10010;  
int p[N];  
//初始每个点都是一个单独的集合  
void init(int n){  
    for(int i=1;i<=n;i++){  
        p[i]=i;  
    }  
}  
int find(int x){  
    if(p[x]!=x) p[x]=find(p[x]);  
    return p[x];  
}  
  
int main(){  
    int n,m;  
    cin>>n>>m;  
    init(n);  
    char op[2];  
    while(m--){  
        int a,b;  
        cin>>op>>a>>b;  
        //合并两个集合,路径压缩  
        if(op[0]=='i') p[find(a)]=find(b);//让a的父节点等于b的父节点,即可合并  
        else {  
            //查询  
            if(find(a)==find(b)) cout<<"yes"<<endl;  
            else cout<<"no"<<endl;  
        }  
    }  
    return 0;  
}

P3367 【模板】并查集 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

维护点的数量:

#include<iostream>  
#include <algorithm>  
#include <cstdio>  
#include <cstring>  
using namespace std;  
const int N=10010;  
int p[N],sizes[N];//每个集合中点的数量  
//初始每个点都是一个单独的集合  
void init(int n){  
    for(int i=1;i<=n;i++){  
        p[i]=i;  
        sizes[i]=1;  
    }  
}  
int find(int x){  
    if(p[x]!=x) p[x]=find(p[x]);  
    return p[x];  
}  
  
int main(){  
    int n,m;  
    cin>>n>>m;  
    init(n);  
    int op;  
    while(m--){  
        int a,b;  
        cin>>op;  
        //合并两个集合,路径压缩  
        if(op==1){  
            cin>>a>>b;  
            if(find(a)==find(b)) continue;  
            sizes[find(b)]+=sizes[find(a)];  
            p[find(a)]=find(b);//让a的父节点等于b的父节点,即可合并  
      
        }  
        else if(op==2){  
            //查询  
            cin>>a>>b;  
            if(find(a)==find(b)) cout<<"Y"<<endl;  
            else cout<<"N"<<endl;  
        }  
        else {  
            //询问某个集合中点的数量  
            cin>>n;  
            cout<<sizes[find(a)]<<endl;  
        }  
    }  
    return 0;  
}

堆 只能保证堆顶是最值,保证不了左右两边的大小关系

操作:down 和up 把元素向下或向上走,使用的是一维数组,x的左儿子2x,右儿子2x+1

size 表示数组的最后一个位置

  1. 插入一个元素: heap[++size]=x up(size)

  2. 求最小值 heap[1]

  3. 删除最小值 数组尾部好删除,所以用最后一个元素覆盖数组的头,然后执行down,

    再删除尾部,head[k]=heap[size];size–; down(k)||up(k)

  4. 修改 heap[k]=k; down(k)||up(k);

// h[N]存储堆中的值, h[1]是堆顶,x的左儿子是2x, 右儿子是2x + 1  
// ph[k]存储第k个插入的点在堆中的位置  
// hp[k]存储堆中下标是k的点是第几个插入的  
int h[N], ph[N], hp[N], size;  
  
// 交换两个点,及其映射关系  
void heap_swap(int a, int b)  
{  
    swap(ph[hp[a]],ph[hp[b]]);  
    swap(hp[a], hp[b]);  
    swap(h[a], h[b]);  
}  
  
void down(int u)  
{  
    int t = u;  
    //查找到三个结点中的最小值  
    if (u * 2 <= size && h[u * 2] < h[t]) t = u * 2;  
    if (u * 2 + 1 <= size && h[u * 2 + 1] < h[t]) t = u * 2 + 1;  
    if (u != t)  
    {  
        heap_swap(u, t);  
        down(t);  
    }  
}  
  
void up(int u)  
{  
    while (u / 2 && h[u] < h[u / 2])//父节点存在且当前结点小于父节点   
    {  
        heap_swap(u, u / 2);  
        u >>= 1;//下一个父节点  
    }  
}  
  
// O(n)建堆  
for (int i = n / 2; i; i -- ) down(i);

P3378 【模板】堆 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

#include <bits/stdc++.h>  
using namespace std;  
const int N=1000001;  
int h[N],s;  
void down(int u){  
    int t=u;  
    if(u*2<=s&&h[u*2]<h[t]) t=u*2;  
    if(u*2+1<=s&&h[u*2+1]<h[t]) t=u*2+1;  
    if(u!=t){  
        swap(h[u],h[t]);  
        down(t);  
    }  
}  
void up(int u){  
    while(u/2&&h[u]<h[u/2]){  
        swap(h[u],h[u/2]);  
        u>>=1;  
    }  
}  
int main(){  
    int n,op;  
    cin>>n;  
    for(int i=n/2;i;i--){  
        down(i);  
    }  
    while(n--){  
        scanf("%d",&op);  
        if(op==1) {  
            int x;  
            scanf("%d",&x);  
            h[++s]=x;  
            up(s);  
        }  
        else if(op==2){  
            printf("%d\n",h[1]);  
        }  
        else {  
            //最后一个换到第一个  
            swap(h[1],h[s]);  
            s--;//删除最后一个  
            down(1);  
        }  
    }  
  
}
			

HashTable

删除的话打个标记
(1) 拉链法

int h[N], e[N], ne[N], idx;  
  
// 向哈希表中插入一个数  
void insert(int x)  
{  
    int k = (x % N + N) % N;  
    e[idx] = x;  
    ne[idx] = h[k];  
    h[k] = idx ++ ;  
}  
  
// 在哈希表中查询某个数是否存在  
bool find(int x)  
{  
    int k = (x % N + N) % N;  
    for (int i = h[k]; i != -1; i = ne[i])  
        if (e[i] == x)  
            return true;  
  
    return false;  
}  

开放寻址法,遇到冲突的话直接往后找没用的节点

数组要开比原来数据范围大2~3倍

//只要开一个h数组就可以了,不需要e和ne了,找一个不在数据范围内的数据来表示当前位置为空
(2) 开放寻址法

   int h[N];  
const int null =xxx;  
   // 如果x在哈希表中,返回x的下标;如果x不在哈希表中,返回x应该插入的位置  
   int find(int x)  
   {  
       int t = (x % N + N) % N;  
       while (h[t] != null && h[t] != x)  
       {  
           t ++ ;  
           if (t == N) t = 0;  
       }  
       return t;  
   }

字符串哈希

快速判断两个字符串是否相等

核心思想:将字符串看成P进制数,P的经验值是131或13331,取这两个值的冲突概率低
小技巧:取模的数用2^64,这样直接用unsigned long long存储,溢出的结果就是取模的结果

typedef unsigned long long ULL;  
ULL h[N], p[N]; // h[k]存储字符串前k个字母的哈希值, p[k]存储 P^k mod 2^64  
  
// 初始化  
p[0] = 1;  
for (int i = 1; i <= n; i ++ )  
{  
    h[i] = h[i - 1] * P + str[i];  
    p[i] = p[i - 1] * P;//P存储的是每一位的基数值  
}  
  
// 计算子串 str[l ~ r] 的哈希值  
ULL get(int l, int r)  
{  
    return h[r] - h[l - 1] * p[r - l + 1];  
}

STL常用

vector, 变长数组,倍增的思想  优化思路:减少申请空间的次数  
    size()  返回元素个数  
    empty()  返回是否为空  
    clear()  清空  
    front()/back()  
    push_back()/pop_back()  
    begin()/end()  
    []  
    支持比较运算,按字典序  
    vector<int> a(1,2),b(3,4);  
printf(a<b) == 0  
​  
pair<int, int>  
    first, 第一个元素  
    second, 第二个元素  
    支持比较运算,以first为第一关键字,以second为第二关键字(字典序)  
​  
string,字符串  
    size()/length()  返回字符串长度  
    empty()  
    clear()  
    substr(起始下标,(子串长度))  返回子串  
    c_str()  返回字符串所在字符数组的起始地址  
​  
queue, 队列  
    size()  
    empty()  
    push()  向队尾插入一个元素  
    front()  返回队头元素  
    back()  返回队尾元素  
    pop()  弹出队头元素  
​  
priority_queue, 优先队列,默认是大根堆  
    size()  
    empty()  
    push()  插入一个元素  
    top()  返回堆顶元素  
    pop()  弹出堆顶元素  
    定义成小根堆的方式:priority_queue<int, vector<int>, greater<int>> q;  
​  
stack, 栈  
    size()  
    empty()  
    push()  向栈顶插入一个元素  
    top()  返回栈顶元素  
    pop()  弹出栈顶元素  
​  
deque, 双端队列  
    size()  
    empty()  
    clear()  
    front()/back()  
    push_back()/pop_back()  
    push_front()/pop_front()  
    begin()/end()  
    []  
​  
set, map, multiset, multimap, 基于平衡二叉树(红黑树),动态维护有序序列  
    size()  
    empty()  
    clear()  
    begin()/end()  
    ++, -- 返回前驱和后继,时间复杂度 O(logn)  
​  
    set/multiset  
        insert()  插入一个数  
        find()  查找一个数  
        count()  返回某一个数的个数  
        erase()  
            (1) 输入是一个数x,删除所有x   O(k + logn)  
            (2) 输入一个迭代器,删除这个迭代器  
        lower_bound()/upper_bound()  
            lower_bound(x)  返回大于等于x的最小的数的迭代器  
            upper_bound(x)  返回大于x的最小的数的迭代器  
    map/multimap  
        insert()  插入的数是一个pair  
        erase()  输入的参数是pair或者迭代器  
        find()  
        []  注意multimap不支持此操作。 时间复杂度是 O(logn)  
        lower_bound()/upper_bound()  
​  
unordered_set, unordered_map, unordered_multiset, unordered_multimap, 哈希表  
    和上面类似,增删改查的时间复杂度是 O(1)  
    不支持 lower_bound()/upper_bound(), 迭代器的++,--  
​  
bitset, 圧位 省空间      
    bitset<10000> s;  //< >里面是个数, 可以用来替代bool 数组  
    //以下操作都支持  
    ~, &, |, ^  
    >>, <<  
    ==, !=  
    []  
​  
    count()  返回有多少个1  
​  
    any()  判断是否至少有一个1  
    none()  判断是否全为0  
​  
    set()  把所有位置成1  
    set(k, v)  将第k位变成v  
    reset()  把所有位变成0  
    flip()  等价于~  
    flip(k) 把第k位取反  
​

图论背思路

BFS和DFS

非常完美的一道dfs + 并查集 https://www.luogu.com.cn/problem/P1127

还原现场很重要

static boolean dfs(int start,int num) {
		if(num == n) return true;
		//开始搜索
		for (int i = 0 ; i < n; i ++) {
			//找start这个开头的string
			if(node.get(i).charAt(0) - 'a' == start && st[i] == 0) {
				//标记
				st[i] = 1;
				ans.add(i);
				if(dfs(node.get(i).charAt(node.get(i).length() - 1)-'a',num+1)) return true;
				//还原现场
				st[i] = 0;
				ans.remove(ans.size() - 1);
			}
			
		}
		return false;
	}

DFS 回溯的时候记得回复现场

邻接矩阵: p[ a] [ b ] a -> b 适合稠密图

邻接表: 稀疏图

和哈希表思路一样

// 对于每个点k,开一个单链表,存储k所有可以走到的点。h[k]存储这个单链表的头结点  
int h[N], e[N], ne[N], idx; //e是终点end  
  
// 添加一条边a->b  
void add(int a, int b)  
{  
    e[idx] = b, ne[idx] = h[a], h[a] = idx ++ ;  
}   
// 初始化  
idx = 0;  
memset(h, -1, sizeof h);  
//遍历图  
for(int i = h[t] ; i != -1 ; i = ne[i] )

## 另一种使用结构体的邻接表存法

int idx=0,n;  
int h[N] , dis[N] , vis[N];  
struct Edge{  
    int ne,to,dis;  
}ed[N];  
//添加, 从 1 开始  
void add(int a,int b ,int c){  
    ed[++idx].ne = h[a];  
    ed[idx].to = b;  
    ed[idx].dis = c;  
    h[a] = idx;  
}
//遍历图  
for(int i = h[t] ; i ; i = ed[i].ne)

BFS 可用于解决权值相等的最短路径问题

拓扑排序

必须是又向无环

排完后,所有的起点都在终点之前

  1. 统计每一个节点的入度和出度

  2. 每一次将入读相同的点放入queue

  3. 枚举队头的出边,删掉 出边,这条边的终点的入度-1

  4. 如果某个点的入度为0 放入队列

bool topsort()  
{  
    int hh = 0, tt = -1;  
  
    // d[i] 存储点i的入度  
    for (int i = 1; i <= n; i ++ )  
        if (!d[i])  
            q[ ++ tt] = i;//把每个入度为0的点加入队列  
  
    while (hh <= tt)  
    {  
        int t = q[hh ++ ];//取出队头  
		//从队头开始找路径  
        for (int i = h[t]; i != -1; i = ne[i])  
        {  
            int j = e[i];  
            if (-- d[j] == 0)  
                q[ ++ tt] = j;  
        }  
    }  
  
    // 如果所有点都入队了,说明存在拓扑序列;否则不存在拓扑序列。  
    return tt == n - 1;  
}

最短路问题:

_分层最短路_,适用于乘车路线和免费次数。

飞机路线

单源最短路 一个点./.到其他所有点的最短路

  • 所有边的权都是正数

    1. 朴素Dijkstra O(n^2) n为点的数量 稠密图 边很多 外部迭代n-1 次
      int g[N][N];  // 存储每条边  权值  
      int dist[N];  // 存储1号点到每个点的最短距离  
      bool st[N];   // 存储每个点的最短路是否已经确定  
        
      // 求1号点到n号点的最短路,如果不存在则返回-1  
      int dijkstra()  
      {  
          memset(dist, 0x3f, sizeof dist);  
          dist[1] = 0;  
        
          for (int i = 0; i < n - 1; i ++ )//迭代n-1 次,因为上来选中了一个点  
          {  
              int t = -1;     // 在还未确定最短路的点中,寻找距离最小的点  
              for (int j = 1; j <= n; j ++ )  
                  if (!st[j] && (t == -1 || dist[t] > dist[j]))  
                      t = j;  
        
              // 用t更新其他点的距离  
              for (int j = 1; j <= n; j ++ )  
                  dist[j] = min(dist[j], dist[t] + g[t][j]);  
        
              st[t] = true;  
          }  
        
          if (dist[n] == 0x3f3f3f3f) return -1;  
          return dist[n];  
      }
    2. 堆优化版的 O(mlogn) 稀疏图
      typedef pair<int, int> PII;  
        
      int n;      // 点的数量  
      int h[N], w[N], e[N], ne[N], idx;       // 邻接表存储所有边  
      int dist[N];        // 存储所有点到1号点的距离  
      bool st[N];     // 存储每个点的最短距离是否已确定  
        
      // 求1号点到n号点的最短距离,如果不存在,则返回-1  
      int dijkstra()  
      {  
          memset(dist, 0x3f, sizeof dist);  
          dist[1] = 0;  
          priority_queue<PII, vector<PII>, greater<PII>> heap;  
          heap.push({0, 1});      // first存储距离,second存储节点编号  
        
          while (heap.size())  
          {  
              auto t = heap.top();  
              heap.pop();  
        
              int ver = t.second, distance = t.first;  
        
              if (st[ver]) continue;  
              st[ver] = true;  
        
              for (int i = h[ver]; i != -1; i = ne[i])  
              {  
                  int j = e[i];  
                  if (dist[j] > distance + w[i])  
                  {  
                      dist[j] = distance + w[i];  
                      heap.push({dist[j], j});  
                  }  
              }  
          }  
        
          if (dist[n] == 0x3f3f3f3f) return -1;  
          return dist[n];  
      }
  • 存在负权边

    1. Bellman -Ford O(nm) 奇妙的存图方式 无负权回路 经过路径有次数限制的话只能用这个了,外面限制的是经过i的点的个数,然后每次遍历边即可。

      int n, m;       // n表示点数,m表示边数  
      int dist[N];        // dist[x]存储1到x的最短路距离  
        
      struct Edge     // 边,a表示出点,b表示入点,w表示边的权重  
      {  
          int a, b, w;  
      }edges[M];  
        
      // 求1到n的最短路距离,如果无法从1走到n,则返回-1。  
      int bellman_ford()  
      {  
          memset(dist, 0x3f, sizeof dist);  
          dist[1] = 0;  
      	//不需要进行收录顶点  
          // 如果第n次迭代仍然会松弛三角不等式,就说明存在一条长度是n+1的最短路径,由抽屉原理,路径中至少存在两个相同的点,说明图中存在负权回路。  
          for (int i = 0; i < n; i ++ )//这个n是指的是最多不经过 多少次经过同一条边  
          {  
              for (int j = 0; j < m; j ++ )  
              {  
                  int a = edges[j].a, b = edges[j].b, w = edges[j].w;  
                  if (dist[b] > dist[a] + w)  
                      dist[b] = dist[a] + w;  
              }  
          }  
        
          if (dist[n] > 0x3f3f3f3f / 2) return -1;  
          return dist[n];  
      }
    2. SPFA 一般: O(m) 最坏O(nm) 不存在负权环才能使用 99%都没有负环比较好用

      优化思路:只有更新过点才对后面的点更新有影响

      要从 1 开始存比较好 ,e 是end 也就是一条边的终点

      int n;      // 总点数  
      int h[N], w[N], e[N], ne[N], idx;       // 邻接表存储所有边  
      int dist[N];        // 存储每个点到1号点的最短距离  
      bool st[N];     // 存储每个点是否在队列中  
        
        
      // 求x号点到n号点的最短路距离,如果从1号点无法走到n号点则返回-1  
      int spfa(int x)  
      {  
          memset(dist, 0x3f, sizeof dist);//初始化要根据题目来  
            
          dist[x] = 0;  
      /*或者  
      for(int i = 1 ; i <= n ; i ++){  
              dis[i] = INT_MAX;  
          }*/	  
          queue<int> q;  
          q.push(x);  
        
          while (q.size())//不为空,即为还有更新的点  
          {  
              auto t = q.front();  
              q.pop();  
        
              st[t] = false;//这里不要忘记  
             //遍历所以能到达的顶点,进行更新  
              for (int i = h[t]; i != -1; i = ne[i])  
              {  
                  int j = e[i];  
                  if (dist[j] > dist[t] + w[i])  
                  {  
                      dist[j] = dist[t] + w[i];  
                      if (!st[j])     // 如果队列中已存在j,则不需要将j重复插入  
                      {  
                          q.push(j);//这里是j  
                          st[j] = true;//这里是j  
                      }  
                  }  
              }  
          }  
        
          if (dist[n] == 0x3f3f3f3f) return -1;  
          return dist[n];  
      }  
      //初始化和存图  
      void add(int a, int b , int c){  
          w[idx] = c;  
          ne[idx] = h[a];   
          en[idx] =  b;    
          h[a] = idx++;  
      }  
      void init(){  
          idx = 1;  
          memset(h , -1 ,sizeof h);  
      }

      模板题:

      P3371 【模板】单源最短路径(弱化版) - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

==判断有无负环 用cnt 来记录当前最短路的边数

例题: 负环路

int n;      // 总点数  
int h[N], w[N], e[N], ne[N], idx;       // 邻接表存储所有边  
int dist[N], cnt[N];        // dist[x]存储1号点到x的最短距离,cnt[x]存储1到x的最短路中经过的点数  
bool st[N];     // 存储每个点是否在队列中  
  
// 如果存在负环,则返回true,否则返回false。  
bool spfa()  
{  
    // 不需要初始化dist数组  
    // 原理:如果某条最短路径上有n个点(除了自己),那么加上自己之后一共有n+1个点,由抽屉原理一定有两个点相同,所以存在环。  
  
    queue<int> q;  
    for (int i = 1; i <= n; i ++ )  
    {  
        q.push(i);  
        st[i] = true;  
    }  
  
    while (q.size())  
    {  
        auto t = q.front();  
        q.pop();  
  
        st[t] = false;  
  
        for (int i = h[t]; i != -1; i = ne[i])  
        {  
            int j = e[i];  
            if (dist[j] > dist[t] + w[i])  
            {  
                dist[j] = dist[t] + w[i];  
                cnt[j] = cnt[t] + 1;  
                if (cnt[j] >= n) return true;       // 如果从1号点到x的最短路中包含至少n个点(不包括自己),则说明存在环  
                if (!st[j])  
                {  
                    q.push(j);  
                    st[j] = true;  
                }  
            }  
        }  
    }  
  
    return false;  
}

多源汇最短路 起点终点都不确定

Floyd O(n^3)

初始化:  
    for (int i = 1; i <= n; i ++ )  
        for (int j = 1; j <= n; j ++ )  
            if (i == j) d[i][j] = 0;  
            else d[i][j] = INF;  
​  
// 算法结束后,d[a][b]表示a到b的最短距离  
void floyd()  
{  
    for (int k = 1; k <= n; k ++ )  
        for (int i = 1; i <= n; i ++ )  
            for (int j = 1; j <= n; j ++ )  
                d[i][j] = min(d[i][j], d[i][k] + d[k][j]);// i经过k 点到达j   
}

最小生成树

普利姆算法 Prim 思路和Dijsktra算法相似 外部迭代 n 次,因为没有提前选中一个点

迭代n次因为没有提前选中一个点 枚举所有点

  1. 朴素Prim算法 稠密图 每次找到未收录的距离最近的点,收录并进行更新其他点到集合的距离

  2. 找这个点是否与集合内部相连

  3. 某个点到这个集合的距离为某个点到这个集合当中的点的距离最短的边

    int n;      // n表示点数  
    int g[N][N];        // 邻接矩阵,存储所有边  
    int dist[N];        // 存储其他点到当前最小生成树的距离  
    bool st[N];     // 存储每个点是否已经在生成树中  
    ​  
    ​  
    // 如果图不连通,则返回INF(值是0x3f3f3f3f), 否则返回最小生成树的树边权重之和  
    int prim()  
    {  
        memset(dist, 0x3f, sizeof dist);  
    ​  
        int res = 0;  
        for (int i = 0; i < n; i ++ )  
        {  
            int t = -1;  
            for (int j = 1; j <= n; j ++ )  
                if (!st[j] && (t == -1 || dist[t] > dist[j]))  
                    t = j;  
    ​  
            if (i && dist[t] == INF) return INF;  
    ​  
            if (i) res += dist[t];  
            st[t] = true;  
    ​  
            for (int j = 1; j <= n; j ++ ) dist[j] = min(dist[j], g[t][j]);  
            //不是相加  
        }  
    ​  
        return res;  
    }
  4. 堆优化版的Prim 稀疏图 不常用

克鲁斯卡尔算法 Kruskal 先对边进行排序 稀疏图 可以用并查集 枚举所有边

java版本:

package bronya;
import java.io.*;
import java.math.BigInteger;
import java.util.*;


class Edge {
	int a , b , w;
	public Edge(int a , int b , int w) {
		this.a = a;
		this.b = b;
		this.w = w;
	}
}

public class Main{

	static int N = (int) ((int) 2 * 1e5 + 10);
	static int n , m ;
	static int[] p = new int[N];
	static Edge[] ed = new Edge[N];
	static long ans = 0;
	static int find(int x) {
		if(x != p[x]) p[x] = find(p[x]);
		return p[x];
	}
	static boolean check() {
		for(int i = 1; i <= n ;  i ++) {
			p[i] = i;
		}
		Arrays.sort(ed,0,m,(e1,e2)->e1.w - e2.w);
		int cnt = 0;
		for(int i = 0 ; i < m ; i ++) {
			int a = ed[i].a;
			int b = ed[i].b;
			int w = ed[i].w;
			a = find(a);
			b = find(b);
			if(a != b) {
				p[a] = b;
				ans += w;
				cnt ++;
			}
		}
		if(cnt < n - 1) return false;
		else return true;
	}
	public static void main(String[] args) throws IOException {
		BufferedReader re = new BufferedReader(new InputStreamReader(System.in));
		String[] s = re.readLine().split(" ");
		n = Integer.parseInt(s[0]);
		m = Integer.parseInt(s[1]);
		for(int i = 0 ;  i < m ; i ++) {
			s = re.readLine().split(" ");
			int a = Integer.parseInt(s[0]);
			int b = Integer.parseInt(s[1]);
			int w = Integer.parseInt(s[2]);
			ed[i] = new Edge(a, b, w);
		}
		boolean f = check();
		if(f) System.out.println(ans);
		else System.out.println("orz");
}
}

P3366 【模板】最小生成树 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

  1. 所有边按权重从小到大排序

  2. 枚举每条边a,b权重c if a,b不连通, 将这条边加入集合中

int n, m;       // n是点数,m是边数  
int p[N];       // 并查集的父节点数组  
  
struct Edge     // 存储边  
{  
    int a, b, w;  
  
    bool operator< (const Edge &W)const // 重载了 <   
    {  
        return w < W.w;  
    }  
}edges[M];  
  
int find(int x)     // 并查集核心操作  
{  
    if (p[x] != x) p[x] = find(p[x]);  
    return p[x];  
}  
  
int kruskal()  
{  
    sort(edges, edges + m);  
  
    for (int i = 1; i <= n; i ++ ) p[i] = i;    // 初始化并查集  
  
    int res = 0, cnt = 0;  
    for (int i = 0; i < m; i ++ )  
    {  
        int a = edges[i].a, b = edges[i].b, w = edges[i].w;  
  
        a = find(a), b = find(b);  
        if (a != b)     // 如果两个连通块不连通,则将这两个连通块合并  
        {  
            p[a] = b;  
            res += w;  
            cnt ++ ;  
        }  
    }  
  
    if (cnt < n - 1) return INF;  
    return res;  
}

二分图 当且仅当图中没有奇数环

染色法 O(n+m) 判断是否是二分图

int n;      // n表示点数  
int h[N], e[M], ne[M], idx;     // 邻接表存储图  
int color[N];       // 表示每个点的颜色,-1表示未染色,0表示白色,1表示黑色  
  
// 参数:u表示当前节点,c表示当前点的颜色  
bool dfs(int u, int c)  
{  
    color[u] = c;  
    for (int i = h[u]; i != -1; i = ne[i])  
    {  
        int j = e[i];  
        if (color[j] == -1)//未染色  
        {  
            if (!dfs(j, !c)) return false;//比如两种颜色, 0,1表示,那么这里就可以用 3- c,也就是用另一种颜色去染色  
        }  
        else if (color[j] == c) return false;  
    }  
  
    return true;  
}  
  
bool check()  
{  
    memset(color, -1, sizeof color);  
    bool flag = true;  
    //枚举所有点,去染色  
    for (int i = 1; i <= n; i ++ )  
        if (color[i] == -1)  
            if (!dfs(i, 0))  
            {  
                flag = false;  
                break;  
            }  
    return flag;  
}

匈牙利算法 O(mn) 实际运行时间一般小于这个值 稠密图不适合用邻接表,推荐使用临界矩阵

int n1, n2;     // n1表示第一个集合中的点数,n2表示第二个集合中的点数  
int h[N], e[M], ne[M], idx;     // 邻接表存储所有边,匈牙利算法中只会用到从第一个集合指向第二个集合的边,所以这里只用存一个方向的边  
int match[N];       // 存储第二个集合中的每个点当前匹配的第一个集合中的点是哪个  
bool st[N];     // 表示第二个集合中的每个点是否已经被遍历过  
​  
bool find(int x)  
{  
    for (int i = h[x]; i != -1; i = ne[i])  
    {  
        int j = e[i];  
        if (!st[j])  
        {  
            st[j] = true;  
            if (match[j] == 0 || find(match[j]))//第二个集合的点未匹配,或者是可以为已经 匹配的第一个集合中的点找到别的集合二中的点  
            {  
                match[j] = x;  
                return true;  
            }  
        }  
    }  
​  
    return false;  
}  
​  
// 求最大匹配数,依次枚举第一个集合中的每个点能否匹配第二个集合中的点  
int res = 0;  
for (int i = 1; i <= n1; i ++ )  
{  
    memset(st, false, sizeof st);  
    if (find(i)) res ++ ;  
}

例题:P3386 【模板】二分图最大匹配 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

邻接表的写法(稠密图会超时)

#include <bits/stdc++.h>  
using namespace std;  
const int N  = 505;  
int n1,n2,ed;//需要两个集合  
int h[N] , ne[N] , e[N] , idx;  
int match[N] ;  
bool vis[N];  
void init(){  
    memset(h,-1,sizeof h);  
    idx = 0;  
}  
void add(int a, int b){  
    e[idx] = b ;  
    ne[idx] = h[a];  
    h[a] = idx++;  
}  
bool find(int x){  
    for (int i = h[x] ; i != -1 ; i =ne[i]){  
        int j = e[i];  
        if(!vis[j]){  
            vis[j] = true;  
            if(match[j] == 0 || find(match[j])){  
                //如果终边的点未匹配,或者是可以为以匹配的起点找到另一个终点  
                match[j] = x;  
                return true;  
            }  
        }  
    }  
    return false;  
}  
int main(){  
    ios::sync_with_stdio(false);  
    cin.tie(nullptr);  
    cin >> n1 >> n2 >>ed;  
    init();  
    int u ,v;  
    for (int i = 1 ; i <= ed ; i++){  
        cin >> u >> v;  
        if(v <= n2){  
            add(u , v);  
        }  
         
    }  
    int ans = 0;  
    for (int i = 1 ; i <= n1 ; i++){  
        memset(vis, false ,sizeof vis);  
        if(find(i)) ans++;  
    }  
    cout << ans <<endl ;  
    return 0;  
}

邻接矩阵的写法:

#include <bits/stdc++.h>  
using namespace std;  
const int N  = 505;  
int n1,n2,ed;//需要两个集合  
bool a[N][N];  
int match[N];  
bool vis[N];  
bool find(int x){  
    //枚举终边  
    for (int i = 1 ; i <= n2 ; i ++){  
        if(!vis[i] && a[x][i]){  
            vis[i] = true;  
            if(match[i] == 0 || find(match[i])){  
                //如果终边的点未匹配,或者是可以为以匹配的起点找到另一个终点  
                match[i] = x;  
                return true;  
            }  
        }  
    }  
    return false;  
}  
int main(){  
    ios::sync_with_stdio(false);  
    cin.tie(nullptr);  
    cin >> n1 >> n2 >>ed;  
    int u ,v;  
    for (int i = 1 ; i <= ed ; i++){  
        cin >> u >> v;  
        if(v <= n2){  
            a[u][v] = 1;  
        }  
         
    }  
    int ans = 0;  
    for (int i = 1 ; i <= n1 ; i++){  
        ans+=find(i); // 这里不一样哦  
        memset(vis, false ,sizeof vis);  
    }  
    cout << ans <<endl ;  
    return 0;  
}

数学知识

[!NOTE]

当看见 0 的个数时考虑将结果分成 2 , 5 的个数,并且,末尾的0的个数一定是由5的个数决定的

E-Kevin喜欢零(困难版本)_牛客小白月赛73 (nowcoder.com)

#include <bits/stdc++.h>  
​  
void solve() {#include <bits/stdc++.h>  
​  
void solve() {  
    int n, k;  
    std::cin >> n >> k;  
​  
    int64_t ans = 0;  
    std::unordered_map<int, std::vector<int>> tp, fp;  
    //tp 记录5的个数,fp记录2的个数  
    //两个是互补的  
    tp[0].push_back(0);  
    fp[0].push_back(0);  
​  
    for (int i = 0, t = 0, f = 0; i < n; i++) {  
        int x;  
        std::cin >> x;  
        for (; x && x % 2 == 0; x /= 2, t++) {  
        }  
        for (; x && x % 5 == 0; x /= 5, f++) {  
        }  
        ans += std::max(  
            //需要 2^k   * 5^k 即可满足 10^k 所以是动态规划  
            //每次计算出当前数据含有的2 和  5 的数量 和仍然需要 k  - 当前数量   这一行的去更新  
            //因为同一个状态有可能对应多个数据, 所以只要找到第一个满足能凑出另一个2或者5的即可   
            //map不能使用upper_bound 但是 内部的vector 是可以使用的  
            std::upper_bound(tp[t - k].begin(), tp[t - k].end(), f - k) - tp[t - k].begin(),  
            std::upper_bound(fp[f - k].begin(), fp[f - k].end(), t - k) - fp[f - k].begin()  
        );  
        tp[t].push_back(f);  
        fp[f].push_back(t);  
    }  
​  
    std::cout << ans << "\n";  
}  
​  
int main() {  
    std::ios::sync_with_stdio(false);  
    std::cin.tie(nullptr);  
​  
    int t;  
    std::cin >> t;  
​  
    while (t--) {  
        solve();  
    }  
​  
    return 0;  
}  
​  
    int n, k;  
    std::cin >> n >> k;  
​  
    int64_t ans = 0;  
    std::unordered_map<int, std::vector<int>> tp, fp;  
    //tp 记录5的个数,fp记录2的个数  
    //两个是互补的  
    tp[0].push_back(0);  
    fp[0].push_back(0);  
​  
    for (int i = 0, t = 0, f = 0; i < n; i++) {  
        int x;  
        std::cin >> x;  
        for (; x && x % 2 == 0; x /= 2, t++) {  
        }  
        for (; x && x % 5 == 0; x /= 5, f++) {  
        }  
        ans += std::max(  
            //需要 2^k   * 5^k 即可满足 10^k 所以是动态规划  
            //每次计算出当前数据含有的2 和  5 的数量 和仍然需要 k  - 当前数量   这一行的去更新  
            //因为同一个状态有可能对应多个数据, 所以只要找到第一个满足能凑出另一个2或者5的即可   
            std::upper_bound(tp[t - k].begin(), tp[t - k].end(), f - k) - tp[t - k].begin(),  
            std::upper_bound(fp[f - k].begin(), fp[f - k].end(), t - k) - fp[f - k].begin()  
        );  
        tp[t].push_back(f);  
        fp[f].push_back(t);  
    }  
​  
    std::cout << ans << "\n";  
}  
​  
int main() {  
    std::ios::sync_with_stdio(false);  
    std::cin.tie(nullptr);  
​  
    int t;  
    std::cin >> t;  
​  
    while (t--) {  
        solve();  
    }  
​  
    return 0;  
}  
​

数论

  1. 质数

    • 试除法判定质数,只枚举 d*d<=n 即可`

      for(int i=2;i<=n/i;i++){  
      //不要担心	数据超过int  
      }
    • 试除法分解质因子,质因数 ,求约数

      • 从小到大枚举所有的约数,n中最多只存在一个大于风雨根号n的质因子
            void divide(int x)  
            {  
                for (int i = 2; i <= x / i; i ++ )  
                    if (x % i == 0)  
                    {  
                        int s = 0;  
                        while (x % i == 0) x /= i, s ++ ;  
                        cout << i << ' ' << s << endl;  
                    }  
                if (x > 1) cout << x << ' ' << 1 << endl;  
                cout << endl;  
            }
            
```            

# 埃氏筛法: 枚举所有数据,然后把每个数的倍数筛去,留下的就是质数 **思想比较好** 思想太好了
``` cpp
	   void get_primes(int n){  
        	for(int i=2;i<=n;i++){  
        		if(st[i]) continue;  
                //删除所有质数的倍数  
        		primes[cnt++]=i;  
        		for(int j=i+i;j<=n;j+=i){  
        			st[j]=true;  
        		}  
        	}	  
        }

线性筛法: 如果质数,把这个数加入集合中 最常用

int primes[N], cnt;     // primes[]存储所有素数  
bool st[N];         // st[x]存储x是否被筛掉  
  
void get_primes(int n)  
{  
    for (int i = 2; i <= n; i ++ )  
    {  
        if (!st[i]) primes[cnt ++ ] = i;  
        //枚举已有的质数,删除它的倍数  
        for (int j = 0; primes[j] <= n / i; j ++ )  
        {  
            st[primes[j] * i] = true;  
            if (i % primes[j] == 0) break;  
        }  
    }  
}  
  1. 约数

    • 试除法求所有约数

      • 只枚举较小的约数,较大的约数可以直接算出
vector<int> get(int n){  
	vector<int> ans;  
	for(int i=1;i<=n/i;i++){  
		if(n%i==0){  
			//从小到大枚举所有约数,并把n/i 得到的约数加入即可  
			ans.push_back(i);  
			if(i!=n/i) ans.push_back(n/i);  
		}  
	}  
	sort(ans.begin(),ans.end());  
	return ans;  
}
  • 约数个数,约数之和
            如果 N = p1^c1 * p2^c2 * ... *pk^ck  
            约数个数: (c1 + 1) * (c2 + 1) * ... * (ck + 1)  
            约数之和: (p1^0 + p1^1 + ... + p1^c1) * ... * (pk^0 + pk^1 + ... + pk^ck
    #include <bits/stdc++.h>
    using namespace std;
    typedef unsigned long long ULL;
    const int mod  = 1e9 + 7;
    int main() {
        int n ;
        cin >> n;
        unordered_map<int,int> primes;
        while(n--) {
            int x;
            scanf ("%d", &x);
            for (int i = 2 ; i <= x / i ; i ++ ) {
                while(x % i == 0) {
                    primes[i] ++;
                    x /= i;
                }
            }
            if (x > 1)  primes[x]++;
        }
        ULL cnts = 1,res = 1;
        for (auto [prime,cnt]:primes)  {
            cnts = (cnts * (cnt + 1)) % mod;
            //求和
            ULL t = 1;
            while(cnt--){
                //秦九zhao算法
                t = (t *prime + 1) %mod;
            } 
            res = res * t % mod;
        }
        cout << cnts << " " << res;
        return 0;
    }
  1. 欧几里得算法,辗转相除法,求最大公约数

    • GCD(a,b) = =GCD(a.amodb)
              int gcd(int a, int b)  
              {  
                  return b ? gcd(b, a % b) : a;  
              }
              
      ```     
          - LCM 最小公倍数 将两个数相乘再除以最大公因数即可得到最小公倍数
          ```cpp
              int lcm(int a,int b){  
              	return a*b/gcd(a,b);  
              }
              
    • 求ax+by=gcd(a,b)
              int xGCD(int a, int b, int &x, int &y) {  
                  if (!b) {//b = 0 时  
                      x = 1, y = 0;  
                      return a;  
                  }  
                  int x1, y10, gcd = xGCD(b, a % b, x1, y1);  
                  x = y1, y = x1 - (a / b) * y1;  
                  return gcd;  
              }
              
      ```     
      ### 欧拉函数:小于x的整数中与x互质的数的个数]
              
      求出单个数的欧拉函数  
      f(n) = `n *(1 - 1 / p1) * (1 - 1 / p2a) ......`
      ```cpp
              int phi(int x){  
              	int res=x;  
              	for(int i=2;i<=x/i;i++){  
              		if(x%i==0){  
              			res=res/i*(i-1);  
              			while(x%i==0) x/=i;  
              		}  
              	}  
              	if(x>1) res=res/x*(x-1);  
              	return res;  
              }
              

筛法:求出1-n每个数的欧拉函数,在线性筛法的模板中加上三行

int primes[N], cnt;     // primes[]存储所有素数  
int euler[N];           // 存储每个数的欧拉函数  
bool st[N];         // st[x]存储x是否被筛掉  
  
  
void get_eulers(int n)  
{  
    euler[1] = 1;  
    for (int i = 2; i <= n; i ++ )  
    {  
        if (!st[i])  
        {  
            primes[cnt ++ ] = i;  
            euler[i] = i - 1;  
        }  
        for (int j = 0; primes[j] <= n / i; j ++ )  
        {  
            int t = primes[j] * i;  
            st[t] = true;  
            if (i % primes[j] == 0)  
            {  
                euler[t] = euler[i] * primes[j];  
                break;  
            }  
            euler[t] = euler[i] * (primes[j] - 1);  
        }  
    }  
}

欧拉定理

a ^f(n) % n = 1
a和n互质

快速幂:

        把指数转为2进制就可以看出来有哪些含有了
        
        预处理时每一个结果都是前一个的平方再mod
        
        求 m^k mod p,时间复杂度 O(logk)。  
        ​  
        int qmi(int m, int k, int p)  
        {  
            int res = 1 % p, t = m;  
            while (k)  
            {  
                if (k&1) res = res * t % p;  
                t = t * t % p;  
                k >>= 1;  
            }  
            return res;  
        }
        
    
```      
### 快速幂求逆元
```cpp
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
int quick_mi(int a,int b,int p) {
    int res = 1 % p;
    while(b) {
        if(b&1) res = (LL) res*a %p;
        b >>= 1;
        a = (LL) a * a % p;
    }
    return res;
}
int main() {
    int n;
    scanf ("%d", &n);
    while(n--) {
        int a,p;
        scanf("%d%d",&a,&p);
        if(a % p) printf("%d\n",quick_mi(a,p-2,p));
        else puts("impossible");
    }
}

扩展欧几里得算法(不会)

    // 求x, y,使得ax + by = gcd(a, b)  
    int exgcd(int a, int b, int &x, int &y)  
    {  
        if (!b)  
        {  
            x = 1; y = 0;  
            return a;  
        }  
        int d = exgcd(b, a % b, y, x);  
        y -= (a/b) * x;  
        return d;  
    }
```  
#### 高斯消元求解方程组的解
 ```cpp
	 #include <bits/stdc++.h>  
    using namespace std;  
    const int N=100;  
    int n;  
    const double eps=1e-6;  
    double a[N][N];  
    // a[N][N]是增广矩阵  
    // a[N][N]是增广矩阵  
    int gauss()  
    {  
        int c, r;  
        for (c = 0, r = 0; c < n; c ++ )  
        {  
            int t = r;  
            for (int i = r; i < n; i ++ )   // 找到绝对值最大的行  
                if (fabs(a[i][c]) > fabs(a[t][c]))  
                    t = i;if (fabs(a[t][c]) < eps) continue;for (int i = c; i <= n; i ++ ) swap(a[t][i], a[r][i]);      // 将绝对值最大的行换到最顶端  
            for (int i = n; i >= c; i -- ) a[r][i] /= a[r][c];      // 将当前行的首位变成1  
            for (int i = r + 1; i < n; i ++ )       // 用当前行将下面所有的列消成0  
                if (fabs(a[i][c]) > eps)  
                    for (int j = n; j >= c; j -- )  
                        a[i][j] -= a[r][j] * a[i][c];  
    ​  
            r ++ ;  
        }if (r < n)  
        {  
            for (int i = r; i < n; i ++ )  
                if (fabs(a[i][n]) > eps)  
                    return 2; // 无解  
            return 1; // 有无穷多组解  
        }for (int i = n - 1; i >= 0; i -- )  
            for (int j = i + 1; j < n; j ++ )  
                a[i][n] -= a[i][j] * a[j][n];return 0; // 有唯一解  
    }  
    int main(){  
        cin>>n;  
        for(int i=0;i<n;i++)  
            for(int j=0;j<n+1;j++){  
                cin>>a[i][j];  
            }int t=gauss();  
        if(t==0){  
            for(int i=0;i<n;i++){  
                printf("%.2lf ",a[i][n]);  
            }  
        }  
        else if(t==2) cout<<"无解"<<endl;  
        else cout<<"有无数组解"<<endl;  
        return 0;  
    }
    

递推求组合数,dp法 适合询问次数>10w

// c[a][b] 表示从a个苹果中选b个的方案数  
for (int i = 0; i < N; i ++ )  
    for (int j = 0; j <= i; j ++ )  
        if (!j) c[i][j] = 1;  
        else c[i][j] = (c[i - 1][j] + c[i - 1][j - 1]) % mod;

通过预处理逆元的方式求组合数 询问1w

    首先预处理出所有阶乘取模的余数fact[N],以及所有阶乘取模的逆元infact[N]  
    如果取模的数是质数,可以用费马小定理求逆元  
    int qmi(int a, int k, int p)    // 快速幂模板  
    {  
        int res = 1;  
        while (k)  
        {  
            if (k & 1) res = (LL)res * a % p;  
            a = (LL)a * a % p;  
            k >>= 1;  
        }  
        return res;  
    }// 预处理阶乘的余数和阶乘逆元的余数  
    fact[0] = infact[0] = 1;  
    for (int i = 1; i < N; i ++ )  
    {  
        fact[i] = (LL)fact[i - 1] * i % mod;  
        infact[i] = (LL)infact[i - 1] * qmi(i, mod - 2, mod) % mod;  
    }  
    ​
```    
### Lucas定理求组合数 20次询问以下
    
![image-20230519193526425](file://D:/typora%E7%94%A8%E5%9B%BE/Screenshots/image-20230519193526425.png?lastModify=1694256669)    
```cpp
      //若p是质数,则对于任意整数 1 <= m <= n,有:  
      //  C(n, m) = C(n % p, m % p) * C(n / p, m / p) (mod p)  int qmi(int a, int k, int p)  // 快速幂模板  
    {  
        int res = 1 % p;  
        while (k)  
        {  
            if (k & 1) res = (LL)res * a % p;  
            a = (LL)a * a % p;  
            k >>= 1;  
        }  
        return res;  
    }int C(int a, int b, int p)  // 通过定理求组合数C(a, b)  
    {  
        if (a < b) return 0;  
    ​  
        LL x = 1, y = 1;  // x是分子,y是分母  
        for (int i = a, j = 1; j <= b; i --, j ++ )  
        {  
            x = (LL)x * i % p;  
            y = (LL) y * j % p;  
        }return x * (LL)qmi(y, p - 2, p) % p;  
    }int lucas(LL a, LL b, int p)  
    {  
        if (a < p && b < p) return C(a, b, p);  
        return (LL)C(a % p, b % p, p) * lucas(a / p, b / p, p) % p;  
    }
    

image-20230519193254443

逆元:

可以在快速幂中求出

也就是a的p-2次方的

在模为素数p的情况下,有费马小定理 a^(p-1)=1(mod p) 那么a^(p-2)=a^-1(mod p) 也就是说a的逆元为a^(p-2)

分解质因数法求组合数

 

    当我们需要求出组合数的真实值,而非对某个数的余数时,分解质因数的方式比较好用:  
        1. 筛法求出范围内的所有质数  
        2. 通过 C(a, b) = a! / b! / (a - b)! 这个公式求出每个质因子的次数。 n! 中p的次数是 n / p + n / p^2 + n / p^3 + ...  
        3. 用高精度乘法将所有质因子相乘  
      
    int primes[N], cnt;     // 存储所有质数  
    int sum[N];     // 存储每个质数的次数  
    bool st[N];     // 存储每个数是否已被筛掉

    void get_primes(int n)      // 线性筛法求素数  {for (int i = 2; i <= n; i ++ ){if (!st[i]) primes[cnt ++ ] = i;for (int j = 0; primes[j] <= n / i; j ++ ){  
•                    st[primes[j] * i] = true;if (i % primes[j] == 0) break;}}}  
​  
​  
•          
•        int get(int n, int p)       // 求n!中的次数  {int res = 0;while (n){  
•                res += n / p;  
•                n /= p;}return res;}  
​  
​  
•          
•        vector<int> mul(vector<int> a, int b)       // 高精度乘低精度模板  {  
•            vector<int> c;int t = 0;for (int i = 0; i < a.size(); i ++ ){  
•                t += a[i] * b;  
•                c.push_back(t % 10);  
•                t /= 10;}  
•          
•            while (t){  
•                c.push_back(t % 10);  
•                t /= 10;}  
•          
•            return c;}get_primes(a);  // 预处理范围内的所有质数  
      
    for (int i = 0; i < cnt; i ++ )     // 求每个质因数的次数  
    {  
        int p = primes[i];  
        sum[i] = get(a, p) - get(b, p) - get(a - b, p);  
    }  
      
    vector<int> res;  
    res.push_back(1);  
      
    for (int i = 0; i < cnt; i ++ )     // 用高精度乘法将所有质因子相乘  
        for (int j = 0; j < sum[i]; j ++ )  
            res = mul(res, primes[i]);  

动态规划DP

思路:

  1. 状态表示:需要几维的动态规划

    • 集合 所有选法

      1. 所有选法

      2. 条件

        • 只从前i个物品中选择

        • 总体积<=j

    • 属性 最大值?最小值?……

  2. 状态计算:怎么得到结果

01背包 每个物品只能用一次

一维优化;

///f[N][N] 表示前i个物品,j的容量下的最大值  
进行压缩,因为每次更新i是时只用到了i-1 这个位置的数据,所以可以使用滚动数组,实现每次i-1到i的更新;  
所以编成  f[N] 表示j的背包容量下的最大值;  
j=0的结果都为0 所以可以跳过;i=0的结果也都为0,同时,如果当前的背包容量不足以把当前物品装入也不需要进行更新了,所以小于当前背包容量的就不要考虑了;  
不能更新也就是f[i][j]=f[i-1][j] 所以可以直接去掉,变为  f[j]=f[j]  
for(int i=1;i<=n;i++){//遍览物品  
    for(int j=m;j>=v[i];j--)//m为最大背包容量  
        f[j]=max(f[j],f[j-v[i]+w[i]]);  
}

多状态dp

示例:
魔法背包
修路
魔法阵
存在多个状态,其中每一个状态内的转移是正常转移的,而状态之间是需要满足条件进行转移的。

完全背包 每件物品有无限个

01背包:从f[i-1]转过来 f[i,h]=max(f[i-1,j],f[i-1,j-v]+w[i]) //优化后:f[j]=max(f[j],f[v-v[i]]+w[i])

完全背包:从f[i]转移 f[i,j]=max(f[i-1,j],f[i,j-v[i]]+w[i])

每次更新:k是每个物品可以有多少个,i是前i个物品,j是当前背包容量

f[i][j]=max(f[i][j],f[i-j][j-v[i]*k]+w[i]*k

优化思路:

image-20230521084412853

for (int i = 1; i <= n ; i++) {
        for (int j = 1 ; j <= m ; j ++) {
            // for (int k = 0 ; k * v[i] <= j ; k ++) {
            //     f[i][j] = max(f[i][j],f[i - 1][j - k *v[i]] + k * w[i]);
            // }
            //f[i][j] = max(f[i-1][j],f[i-1][j-v]+w,f[i-1][j-2v] + 2w , ......)
            //f[i][j-v] = max(        ,f[i-1][j-v],f[i-1][j-2v]+2w,....)
            //错位相减
            // f[i][j] - f[i][j-v] = w
            // f[i][j] = f[i][j-v] + w
            //所以 
            //f[i][j] = max(f[i-1][j],f[i][j-v] + w)
            //根据递推,每一个max后面那个数都等于上一个的f[i][j-v] + w
            //前提是要大于v[i]等式才成立
            f[i][j] = f[i-1][j];//前一个转移
            if (j >= v[i]) f[i][j] = max(f[i][j],f[i][j - v[i]]+w[i]);
        }
    }

一维优化:删去一维

//f[i][j]=f[i-1][j];  f[i]==f[i-1] 直接删去  //if(j>=v[i]){ 只有当前背包容量大于当前的物品时才能装入,采用从v[i]遍历背包容量  
for(int i=1;i<=n;i++)  
    for(int j=v[i],j<=m;j++)  
    //f[i,j]=max(f[i-1,j],f[i,j-v[i]]+w[i])  
        f[j]=max(f[j],f[j-v[i]]+w[i]);  
//}  

01和完全背包区别:

背包容量的遍历不同

01:

for(int j=m;j>=v[i];j–)//m为最大背包容量
f[j]=max(f[j],f[j-v[i]+w[i]]);

完全:

for(int j=v[i],j<=m;j++)
f[j]=max(f[j],f[j-v[i]]+w[i]);

多重背包问题 优化

每个物品有个数限制,但不是无限

  1. 状态表示:f[ i ] [ j ]

    • 集合

    • 属性

  2. 状态计算:

    f[i][j]=max(f[i-1][j-v[i]*k]+w[i]*k) k有范围

    优化:将数量打包,比如打包成1个物品一起,2个物品一起……

    之后用01背包做即可

#include <bits/stdc++.h>  
using namespace std;  
const int N = 1001;  
int f[N],v[N],w[N];  
int main(){  
    int n,m,cnt=0;  
    cin>>n>>m;  
    for(int i=1;i<=n;i++){  
        int a,b,s;//容量,价值  
        cin>>a>>b>>s;  
        int k=1;//用来打包  
        while(k<=s){  
            cnt++;  
            v[cnt]=a*k;  
            w[cnt]=b*k;  
            s-=k;  
            k*=2;  
        }  
        if(s>0){  
            cnt++;  
            v[cnt]=a*s;  
            w[cnt]=b*s;  
        }  
    }  
    for(int i=1;i<=cnt;i++){  
        for(int j=m;j>=v[i];j--)  
        f[j]=max(f[j],f[j-v[i]]+w[i]);  
    }  
    return 0;  
}

分组背包问题

线性DP

递归方程有线性关系,具有求的先后顺序

具体有各种子序列

建议看leetcode101 里面的比较好

数位统计DP

给出两个数字 a, b

统计a到b 中每一位 的0~9的出现次数

思路:分情况讨论 + 前缀和思想

image-20230524164849922

include <iostream>  
# include <cmath>  
using namespace std;  
  
int dgt(int n) // 计算整数n有多少位  
{  
    int res = 0;  
    while (n) ++ res, n /= 10;  
    return res;  
}  
  
int cnt(int n, int i) // 计算从1到n的整数中数字i出现多少次   
{  
    int res = 0, d = dgt(n);  
    for (int j = 1; j <= d; j ++) // 从右到左第j位上数字i出现多少次,所有位上的次数加起来就是i出现的总次数  
    {  
        // l和r是第j位左边和右边的整数 (视频中的abc和efg); dj是第j位的数字  
        int p = pow(10, j - 1), l = n / p / 10, r = n % p, dj = n / p % 10;  
        // 计算第j位左边的整数小于l (视频中xxx = 000 ~ abc - 1)的情况  
        if (i) res += l * p;   
        if (!i && l) res += (l - 1) * p; // 如果i = 0, 左边高位不能全为0(视频中xxx = 001 ~ abc - 1),并且&&l表示这时i也不能在最高位出现。  
        // 计算第j位左边的整数等于l (视频中xxx = abc)的情况  
        if ( (dj > i) && (i || l) ) res += p;  //(i || l)表示i=0时,i不能出现在最高位(即l不能为0),因为这种数是不存在的  
        if ( (dj == i) && (i || l) ) res += r + 1;  //(i || l)表示i=0时,i不能出现在最高位(即l不能为0),因为这种数是不存在的  
    }  
    return res;  
}  
  
int main()  
{  
    int a, b;  
    while (cin >> a >> b , a)  
    {  
        if (a > b) swap(a, b);  
        for (int i = 0; i <= 9; ++ i) cout << cnt(b, i) - cnt(a - 1, i) << ' ';  
        cout << endl;  
    }  
    return 0;  
}

数位DP(重要)

DP时间复杂度:状态个数*转移个数

相当于往空里填数字

知识点1:mask集合

集合和数字的替换,使用二进制转化集合来实现某些数字不选择

例: 10011 从高到低依次代表者 4 3 2 1 0 这几个数字选不选,1表示选择,那么,

集合mask >> d & 1 d为这个数字,进行这样的运算就可以判断mask 对应的d数字这个位置上是1还是0

同理 mask|(1<<d) 将1移位到mask上 代表d这个数字的位置,进行或运算,即可将d加入集合中

模板:

将问题转化为填数字受限问题:

递归中的变量:

i 当前下标 当 i == 最大长度时,根据是否满足条件返回 0 或 1

mask 集合 代表着一种状态的记录,比如某位填了哪些数字,或者截止到上一位已经积累的某个要求满足条件的数据是多少,这个一般是变化的

is_limit 前一个位置是否收到原来的数字的限制:123 第一个填1 了后面一定受限,同时如果前面受限了,那么后面所有的都受限

is_num 前一个位置是否填数字,如果前一个没填数字,那么这一位无法填0了,如果填了可以填任意数字,当0对题目没影响时,可以不用这个东西

模板公式:

  1. 求出最大长度和memo,记忆数组,并初始化为-1 memo数组的一维是长度,二维是能包含所有枚举的最大长度,具体问题具体分析

  2. 递归函数

    • 结束递归条件i == m 返回是否满足 满足为1 满足为0

    • 记忆化剪枝:当is_limit 和 is_num 只有一个为真时,后面的数字也是可以任意填的,所以可以剪枝

    • !is_limit && is_num && memo[i][mask] != -1 return memo[i][mask]

    • 设出res = 0 即为我们要求的答案

    • (可能不存在这种情况)这一位不填数字:res = (i+1 , mask不改变, false , false)

    • 求出这一位数字可以填写的上下界:根据is_limit来求 up = is_limit ? s[i] - '0' : 9

    • 枚举这一位数字,进行递归 (条件判断不一定需要)

    • for (int d = 初始 ; d <= up ; d++) if(当前这个数字没使用) 把这个数字加入mask

    • 循环内:res = (i+1 , mask 的改变 , is_limit && d == up , is_num的变化)

    • 当is_limit 和 is_num 只有一个为真时将答案加入memo中 if(!is_limit && is_num) memo[i][mask] = res

    • 返回res

例题:

  1. 1012. 至少有 1 位重复的数字 - 力扣(Leetcode)
class Solution {  
public:  
    int numDupDigitsAtMostN(int n) {  
        //将n转化为字符串,方便枚举每一位置上的数字  
        auto s =to_string(n);//使用auto 防止爆int  
        //记忆化数组,当dp到相同的情况时直接可以使用以前的的出来的  
        //第一维为长度  
        //第二位代表可以选择的数字有哪些  
        //这题从高到低每一位代表着 9876543210 所以需要移动到第11位,才能出现10个数字都选择的情况  
        int m = s.length(),memo[m][1 << 10];  
        memset(memo, -1 ,sizeof memo);//-1表示没有计算过这种情况  
        //递归函数  
        function<int(int,int,bool,bool)> f = [&] (int i ,int mask, bool is_limit , bool is_num) -> int {  
            //i 下标  
            //mask 是记录当前已经选择数字的集合  
            //is_limit 代表当前位置是否受到n这个数字的制约,比如不能超过某个数字  
            //is_num 代表前一位是否填数字了,这个是用来判断0是否可填的,如果0可不可填都无所谓就可以不使用这个了  
            if (i == m) //如果dp到了最后一个位置了,要返回是否得到合法数字了  
                 return is_num;//合法数字一定为true,因为长度到了前一个一定要填数字的  
            //如果没收到限制或者没收到前一个的填数字的限制,后面可以任意填了,所以必定会有很小重复性的,所以可以直接返回  
            if (!is_limit && is_num &&memo[i][mask] != -1) return memo[i][mask];  
            int res = 0;  
            if (!is_num){//前一个数字没填,这个数字当然也可以不填  
                res = f(i + 1 , mask , false , false);  
                //继续递归  
            }  
            //如果当前数字没收到限制,那么当然可以继续任意填  
            //如果收到限制了,那么最多只能填当前这一位置上的数字  
            int up = is_limit ? s[i] - '0' : 9;  
            for (int d = 1 - is_num ; d <= up ; ++d)  
                //枚举可以填入的数据,前一位没填数字 ,那么这一位只能从1开始,否则可以从0开始,那么可以填0,如果没被限制,那么要小心是否是第一位了,所以从1开始  
                //当d == up 时,  
                if ((mask >> d & 1) == 0) // d 不在 mask 中  
                //d != up 时,所有的后面的位置都不会受限  
                //d == up 时,如果前一位已经受限了,那么后面还会接着受限  
                    res += f(i + 1, mask | (1 << d), is_limit && d == up, true);     
              
            if (!is_limit && is_num)  
            //如果没收到限制或者没收到前一个的填数字的限制,后面可以任意填了  
                memo[i][mask] = res;  
            return res;  
        };  
        return n - f(0 , 0 , true , false);//第一个数字肯定受限了  
    }  
};
2. [6396. 统计整数数目 - 力扣(Leetcode)](https://leetcode.cn/problems/count-of-integers/description/) 一种变形,好好理解
    class Solution {  
    public:  
        const int mod = 1e9 + 7;  
        int cmp(string s , int min_sum , int max_sum){  
            //memo数组最大有 n个9 或者是被迫的这个最大数字 记得 + 1  
            int n = s.length() , memo[n][min(9*n,max_sum) + 1];  
            memset(memo , -1 , sizeof memo);  
            //因为0无影响,所以is_num可以不要了  
            //sum 就是mask  
            function <int(int ,int ,bool)> f = [&] (int i , int sum , bool is_limit) -> int {  
            //1.非法情况  
            if (sum > max_sum) return 0;//结束递归,sum是一直增加的,后面无法减小的  
            if (i == n) return sum >= min_sum;//结束递归的时候如果数字合法且满足  
            //少了一个is_num,只要后面不受限制,那么,后面一定重复  
            if (!is_limit && memo[i][sum] != -1) return memo[i][sum];  
            int res = 0 ; //计算所要的答案,也就是计数  
            int up = is_limit ? s[i] - '0' : 9;//上界  
            for (int d = 0 ; d <= up ; d++){//枚举位置上的数字  
                //d == up的时候,如果前面受限了,后面继续受限,如果前面没受限,那么后面也不受限  
                res = (res + f(i + 1 , sum + d  , is_limit && d == up)) % mod;  
            }  
            if (!is_limit) memo[i][sum] = res ;  //与上面保持一致  
            return res ;//返回答案  
            };  
            return f(0 , 0 ,true);//初始状态下,一定受限  
        }  
        int count(string num1, string num2, int min_sum, int max_sum) {  
            //转化:  
            /*  
                计算 <= num2 的合法数字a和 <= num2 的合法数字b 答案就等于 a - b  
                可以直接计算 <= num1 的合法数字,最后单独判定 num1是否合法  
                套模板: mask 在这里指的是各位数字之和  
                递归结束条件:  
                sum > max_sum 直接返回0 ,不成立,因为sum不能减小,所以继续递归下去也没有用  
                递归到就结束的时候,如果 sum >= min_sum 那么就是满足的,可以直接返回1了  
                前导零对和没有影响,所以isnum可以不用  
                最后:取模运算  
                (a+b)mod m = ((a mod m) + (b mod m )) mod m  
                (a*b)mode m = ((a mod m) * (b mod m )) mod m  
            */  
            int ans = cmp(num2 , min_sum ,max_sum) - cmp(num1 , min_sum ,max_sum);  
            int sum = 0;  
            //最后一个num1单独判断,因为上面计算的是 num1 < x <= num2的,和前缀和一样  
            for (auto c : num1) sum += c - '0';  
            ans += min_sum <= sum && sum <= max_sum ;  
            return ans % mod;  
        }  
    };

状压DP 状态压缩+动态规划 利用二进制把状态记录成二进制数

291 91

树形DP 各层选择取最大值

将状态分为当前节点选择和不选择

285. 没有上司的舞会 - AcWing题库


#include <bits/stdc++.h>  
using namespace std;  
const int N = 6010;  
int happy[N];  
int n;  
int e[N]/*e存的是编号1*/ , ne[N] , idx ,h[N] , dp[N][N];  
bool fa[N];  
/*  
    状态定义:dp[i][1] dp[i][0] 当前节点选不选  
    状态转移 : dp[i][0] = sum(子树)   子树有可以分为选与不选  取最大值  
                dp[i][1] = sum(下一层子树不选)    
    属性: 最大值  
   
*/  
void init(){  
    idx = 1;  
    memset(h, -1 , sizeof h);  
}  
void add(int a, int b){  
    e[idx] = b;  
    ne[idx] = h[a];  
    h[a] = idx++;  
}  
void dfs(int u){  
        //如果当前的节点要选择的话,要初始化数据  
        dp[u][1] = happy[u];  
        //遍历u的子树  
        for(int i = h[u] ; i != -1 ; i = ne[i]){  
            int j = e[i];  
            //递归到最低层,实现一层层的求和  
            dfs(j);  
            //当前不选,则是子树的最大值  
            dp[u][0] += max(dp[j][0] , dp[j][1]);  
            //当前选择,那么就是下一层子树不选  
            dp[u][1] += dp[j][0];  
        }  
}  
int main(){  
    scanf("%d" ,&n);  
    for(int i = 1 ; i <= n ; i++){  
        scanf("%d",&happy[i]);  
    }  
    init();  
    for(int i = 0 ; i < n-1 ; i++){  
        int a , b ;  
        scanf("%d%d",&a,&b);  
        fa[a] =true;  
        add(b,a);  
    }  
    int root = 1;  
    while(fa[root]) root++;  
    dfs(root);  
    printf("%d",max(dp[root][0],dp[root][1]));  
    return 0;  
}

状态机:

通过表示出状态的转换方式即可自动得到答案

DP行/列问题

6456. 矩阵中严格递增的单元格数 - 力扣(Leetcode)

例题:股票买卖问题

含有冷却时间需要用四个状态,正常两个即可,具体问题具体分析

  1. 714. 买卖股票的最佳时机含手续费 - 力扣(Leetcode)

  2. 309. 最佳买卖股票时机含冷冻期 - 力扣(Leetcode)

通过stoi 和隔板法实现枚举每一个数字的任意子子串

stoi substr to_string 的妙用

6441. 求一个整数的惩罚数 - 力扣(LeetCode)

子列和/字串问题:

  1. 求任意子列的乘积最大 6393. 一个小组的最大实力值 - 力扣(Leetcode)

  2. 一个字符串匹配另一个字典求最大匹配长度问题:6394. 字符串中的额外字符 - 力扣(Leetcode)

  3. 反转01得到相等字符串问题:6455. 使所有字符相等的最小成本 - 力扣(Leetcode)

暴力枚举法

使用于组合的数量少,但是需要找到最合适的组合的题目
例题
3*3

面试常问:

基础

  • 概念辨别:
    • JDK Java Development Kit 是Java的开发工具包,是Java的一个SDK
    • JRE Java Runtime Environment Java运行环境
    • SDK Software Development Kit 软件开发工具包
    • Java9 之后部分jdk和jre了
    • JIT 运行时编译,当JIT编译器第一次编译之后,会把字节码对应的机器码保存下来,下次可以直接使用,如果是热点代码就使用JIT进行编译启动,如果不是热点数据,就使用解释器来执行。
    • AOT:在程序执行前将其编译成机器码,属于静态编译。适合云原生场景
  • 基础数据类型:
    • == 和 equals : == 比较的是内存地址是否一致,equals比较的是对象的内容是否相等,Object类中没有区别,Striing,Integer等就有区别了
    • 基本数据类型:byte 8bit ,short 16 ,int 32 ,long 64,float 32 ,double 64,boolean 1,char , 注意char的默认值是 \u0000 也就是表示null的字符
    • StringBuffer可以看作是线程安全的StringBuilder
    • Map和Set:
      • 为什么HashMap的长度(桶的数量是2的幂次方),因为可以优化哈希值的分布,哈希值与长度-1进行位运算而不是取模,可以加快效率,也能分布更均匀,方便扩容
      • HashSet的底层是是用来一个HashMap,key为set的元素,value为一个固定的Object对象
      • HashMap的查询:底层实现数组+链表 1.8之后多了红黑树
        1. 没有哈希冲突,O(1)
        2. 有冲突,就会把冲突的键放在同一个桶的链表中,需要查询链表O(n);
        3. Java8之后,当桶中的数据达到一定规模就会转为红黑树,O(logn)
        • put方法:
          1. 判断key对数组table,是否为null,否则执行resize进行扩容(初始化)
          2. 根据key计算hash,得到数组索引
          3. table[i] == null 直接添加
          4. 不成立:
            • 判断table[i] 的首个元素是否和key一样,如果相同直接覆盖value
            • table[i] 是否为treeNode ,也就是是否是红黑树,如果是直接在树种插入键值对
            • 遍历table[i] 在尾部插入数据,如果长度大于8转为红黑树
          5. 判断实际数量是否超过了最大容量*0.75,如果超过进行扩容
        • 如何扩容:
          • 每次到达数组长度* 0.75时扩容,,每次扩容长度是原来最大容量的两倍
          • 扩容之后要把老数组移到新数组钟
        • 寻址算法:
          • 计算出key的hashCode,然后在这个值右移16位后的二进制及逆行按位异或运算,得到的hash
      • HashSet如何比较是否重复:根据hashcode来比较,如果相等,那么再调用equals方法来比较
    • 字节码:JVM可以理解的代码就是字节码,也就是.class文件
      • Java代码先经过编译生成字节码,之后由java解释器来解释执行
      • 面向对象三大特点:封装、继承、多态
      • 比较对象比较的是内存地址,而equals()没有重写时,也是比较的地址
      • 序列化:将数据结构或者对象转换成二进制字节流的过程
      • 包装类型的常量池:包装类型保存了一定范围内的所有的实例,可以减少内存的使用
    • 引用类型:
      1. 强引用:使用new
      2. 软引用:只有这个方式时,内存不足时会被回收
      3. 弱引用:只有这个方式是,内存重组也会被回收
      4. 虚引用:无法通过他获取对象
  • IO
    • BIO同步阻塞IO一直等待内核把数据拷贝到用户空间
    • NIO非阻塞IO,可以看作多路复用IO
    • AIO异步IO
    • NIO非阻塞IO Netty使用

多态的原理

动态绑定,运行时才将方法调用和方法实现关联起来。

静态方法为什么不能调用非静态成员

静态方法在类加载时就会分配内存,而非静态成员属于实例对象,所以调用不到,属于非法操作

字符串常量池

对于编译期可以确定值的字符串,也就是常量字符串 ,jvm 会将其存入字符串常量池。并且,字符串常量拼接得到的字符串常量在编译阶段就已经被存放字符串常量池,这个得益于编译器的优化。
字符串使用 final 关键字声明之后,可以让编译器当做常量来处理。

String为什么不可变

  1. String中的byte数组被private和final修饰,并且没有具体的方法暴露
  2. String对象使用final修饰,不可被继承,保证不会被子类破坏不可变性

== 和 equals区别

== :
基本数据类型是比较的值,对象比较的是地址
equals:
没重写的话比较的是地址,String是专门重写过的,比较的是具体的值是否相同

三种拷贝方式的区别

  1. 引用拷贝:不同的引用指向相同的对象
  2. 浅拷贝:外部对象是new 出来的新对象,内部对象仍然是指向一个
  3. 深拷贝:外部对象和内部对象都是新new 出来的

concurrenthashmap和hashmap和hashtable的区别

HashMap线程不安全
HashTable和concurrentHashMap是线程安全的HashMap,HashTable上锁时,会锁住整个表,而ConcurrentHashMap只会锁对应的段,使用的是分段锁。

序列化

SerialVersionUID

即使被static修饰,也会被序列化进二进制流中,用来判断序列化对象的版本一致性。

代理

代理模式是使用代理对象来替代真实对象,从而在不修改原目标的前提下提供额外的功能操作,拓展对象功能。

动态代理和静态代理的区别

  1. 静态代理在编译阶段就将接口、实现类、代理类都变成一个个实际的class文件。对于每一个被代理的对象都需要单独写一个代理类,非常不方便
  2. 动态代理:是在运行时动态生成字节码,并加载到JVM中。核心是 InvocationHandler 接口和 Proxy 类

JDK和CGLIB的区别

JDK是面向接口的,而CGLib是通过直接吗底层继承要代理的类来实现的,底层是asm

Spring AOP的底层实现主要基于动态代理模式。具体来说,有两种主要的实现方式:JDK 动态代理和 CGLIB 动态代理。Spring AOP 在运行时会根据目标对象的类型和配置来选择使用 JDK 动态代理还是 CGLIB 动态代理。如果目标对象实现了接口,并且没有强制要求使用 CGLIB 代理,Spring 会优先使用 JDK 动态代理。如果目标对象没有实现接口,或者通过配置强制使用 CGLIB 代理,那么 Spring 会使用 CGLIB 动态代理来实现 AOP。

BigDecimal工具类

import java.math.BigDecimal;
import java.math.RoundingMode;

/**
 * 简化BigDecimal计算的小工具类
 */
public class BigDecimalUtil {

    /**
     * 默认除法运算精度
     */
    private static final int DEF_DIV_SCALE = 10;

    private BigDecimalUtil() {
    }

    /**
     * 提供精确的加法运算。
     *
     * @param v1 被加数
     * @param v2 加数
     * @return 两个参数的和
     */
    public static double add(double v1, double v2) {
        BigDecimal b1 = BigDecimal.valueOf(v1);
        BigDecimal b2 = BigDecimal.valueOf(v2);
        return b1.add(b2).doubleValue();
    }

    /**
     * 提供精确的减法运算。
     *
     * @param v1 被减数
     * @param v2 减数
     * @return 两个参数的差
     */
    public static double subtract(double v1, double v2) {
        BigDecimal b1 = BigDecimal.valueOf(v1);
        BigDecimal b2 = BigDecimal.valueOf(v2);
        return b1.subtract(b2).doubleValue();
    }

    /**
     * 提供精确的乘法运算。
     *
     * @param v1 被乘数
     * @param v2 乘数
     * @return 两个参数的积
     */
    public static double multiply(double v1, double v2) {
        BigDecimal b1 = BigDecimal.valueOf(v1);
        BigDecimal b2 = BigDecimal.valueOf(v2);
        return b1.multiply(b2).doubleValue();
    }

    /**
     * 提供(相对)精确的除法运算,当发生除不尽的情况时,精确到
     * 小数点以后10位,以后的数字四舍五入。
     *
     * @param v1 被除数
     * @param v2 除数
     * @return 两个参数的商
     */
    public static double divide(double v1, double v2) {
        return divide(v1, v2, DEF_DIV_SCALE);
    }

    /**
     * 提供(相对)精确的除法运算。当发生除不尽的情况时,由scale参数指
     * 定精度,以后的数字四舍五入。
     *
     * @param v1    被除数
     * @param v2    除数
     * @param scale 表示表示需要精确到小数点以后几位。
     * @return 两个参数的商
     */
    public static double divide(double v1, double v2, int scale) {
        if (scale < 0) {
            throw new IllegalArgumentException(
                    "The scale must be a positive integer or zero");
        }
        BigDecimal b1 = BigDecimal.valueOf(v1);
        BigDecimal b2 = BigDecimal.valueOf(v2);
        return b1.divide(b2, scale, RoundingMode.HALF_EVEN).doubleValue();
    }

    /**
     * 提供精确的小数位四舍五入处理。
     *
     * @param v     需要四舍五入的数字
     * @param scale 小数点后保留几位
     * @return 四舍五入后的结果
     */
    public static double round(double v, int scale) {
        if (scale < 0) {
            throw new IllegalArgumentException(
                    "The scale must be a positive integer or zero");
        }
        BigDecimal b = BigDecimal.valueOf(v);
        BigDecimal one = new BigDecimal("1");
        return b.divide(one, scale, RoundingMode.HALF_UP).doubleValue();
    }

    /**
     * 提供精确的类型转换(Float)
     *
     * @param v 需要被转换的数字
     * @return 返回转换结果
     */
    public static float convertToFloat(double v) {
        BigDecimal b = new BigDecimal(v);
        return b.floatValue();
    }

    /**
     * 提供精确的类型转换(Int)不进行四舍五入
     *
     * @param v 需要被转换的数字
     * @return 返回转换结果
     */
    public static int convertsToInt(double v) {
        BigDecimal b = new BigDecimal(v);
        return b.intValue();
    }

    /**
     * 提供精确的类型转换(Long)
     *
     * @param v 需要被转换的数字
     * @return 返回转换结果
     */
    public static long convertsToLong(double v) {
        BigDecimal b = new BigDecimal(v);
        return b.longValue();
    }

    /**
     * 返回两个数中大的一个值
     *
     * @param v1 需要被对比的第一个数
     * @param v2 需要被对比的第二个数
     * @return 返回两个数中大的一个值
     */
    public static double returnMax(double v1, double v2) {
        BigDecimal b1 = new BigDecimal(v1);
        BigDecimal b2 = new BigDecimal(v2);
        return b1.max(b2).doubleValue();
    }

    /**
     * 返回两个数中小的一个值
     *
     * @param v1 需要被对比的第一个数
     * @param v2 需要被对比的第二个数
     * @return 返回两个数中小的一个值
     */
    public static double returnMin(double v1, double v2) {
        BigDecimal b1 = new BigDecimal(v1);
        BigDecimal b2 = new BigDecimal(v2);
        return b1.min(b2).doubleValue();
    }

    /**
     * 精确对比两个数字
     *
     * @param v1 需要被对比的第一个数
     * @param v2 需要被对比的第二个数
     * @return 如果两个数一样则返回0,如果第一个数比第二个数大则返回1,反之返回-1
     */
    public static int compareTo(double v1, double v2) {
        BigDecimal b1 = BigDecimal.valueOf(v1);
        BigDecimal b2 = BigDecimal.valueOf(v2);
        return b1.compareTo(b2);
    }

}

UnSafe类

提供一些很底层的操作,JUC中经常使用。
核心功能:

  1. 内存操作
  2. [[多线程#内存屏障|内存屏障]]
  3. 对象操作
  4. 数据操作
  5. CAS操作
  6. 线程调度
  7. Class操作
  8. 系统信息

并发相关

线程池

线程池的原理

初始化一个线程池,指定线程池的大小,也就是线程池中的线程数量
然后每次向线程池中提交任务,线程池中的线程循环从任务队列中去除任务并且执行
如果一个线程执行完一个任务之后,就会回到线程池,而不是销毁

线程池通过循环利用线程,而不是销毁避免了频繁创建和销毁线程池的开销

线程池中关于时间的参数起什么作用

  1. 空闲线程存活时间:当线程中的线程数量超过核心线程数量时,这些额外的线程在超过空闲线程存活时间就会被中止
  2. 任务超时时间:执行任务执行的最大时间,如果
  • 多线程:
    • 线程不安全的集合:
      • ArrayList,HashMap,HashSet,LinkedList等
        • ArrayList的实现:
          • add方法:确保数组在已使用长度(size) + 1之后能够存下洗一个数据
          • 计算数组的容量,如果当前数组已使用的长度+1后的大于当前数组的长度,使用grow方法扩容,扩大约1.5倍
          • 确保新增的数据有地方存储之后奖新元素加到位于size的位置上
          • 返回bool值
        • ArrayList list=new ArrayList(10)中的list 不会扩容
        • 使用asList后,原数组修改会改变新生成的List,因为他们最终指向的都是一个内存地址
        • 如何处理linkedlist和arraylist的线程不安全:
          1. 优先在方法内使用,定义为局部变量
          2. 使用synchornizedList来替换ConcurrentLinkedQueue
      • 使用toArray后,修改List的内容,数组的内容不会改变
      • Vector和ArrayList的区别:Vector是线程安全的,大部分方法是同步到,性能上ArrayList好一点,以为不需要同步,扩容的时候,A增加50%,V怎加100%
      • ConcurrentHashMap如何保证线程安全:
        1. 使用CAS(Compare and Swap),每次更新时,对比内存位置的值与预期的原值相同则更新,否则不更新,这是一种无锁的操作
        2. 使用synchorinized,ConcurrentHashMap的每个桶(也即是哈希表种的链表或者红黑树),都可以当作一个锁,线程访问时,锁住这个桶,而不是锁住整个哈希表
    • 6种状态:
    • synchronized:
      • 作用:把证同一时刻只能有一个线程来执行该段代码,保证线程的同步
      • 原理:使用了JVM种的监视器锁monitor,每个对象都有一个内治所,当线程调用synchronized方法时,就获得了这个锁,
      • JDK1.6 之后的优化:引入了偏向锁、和轻量级锁,逐步升级到重量级锁
      • synchronized和volatile的区别:volatile只能保证线程的可见性,当一个线程修改了这个变量的值,其他线程立刻可见这个修改
      • 并发编程的三个重要特性:原子性(全做or全部做),可见性(共享变量可见),有序性(按照先后顺序)
      • ThreadLocal原理:维护一个ThreadLocalMap,key为ThreadLocal对,值为变量
        • 内存泄漏:key是弱引用,value是强引用,所以外部没有使用强引用时,ThreadLocal会被回收,但是value会继续使用内存,可以使用ThreadLocal.remove()来解决这个问题
  • JUC:
    • Java Util Concurrent是并发编程的一个工具包,常用:
      • Executor框架
      • ConcurrentHashMap并发集合
      • CountDownLatch同步工具
      • Locks 比synchronized更灵活的锁机制
      • 原子变量:AtomicInteger
      • 并发工具类:ForkJoinPool
    • volatile关键字:保证变量的可见性,要求每次使用它时都需要从主存中重新读取,但不能保证原子性
    • 单例模式示例代码:
      public class Singleton {
      
          private volatile static Singleton uniqueInstance;
      
          private Singleton() {
          }
      
          public  static Singleton getUniqueInstance() {
             //先判断对象是否已经实例过,没有实例化过才进入加锁代码
              if (uniqueInstance == null) {
                  //类对象加锁
                  synchronized (Singleton.class) {
                      if (uniqueInstance == null) {
                          uniqueInstance = new Singleton();
                      }
                  }
              }
              return uniqueInstance;
          }
      }
      

集合相关

Set

  • HashSet底层使用的是HashMap
  • LinkedHashSet通过LinkedHashMap实现的
  • TreeSet:红黑树(自平衡的排序二叉树)实现

List

ArrayList是动态数组,可以扩容、缩容,同时可以使用泛型来保证,线程不安全,适用于频繁的查找工作。可添加null值
Vector是老实现,底层使用Object [ ]存储,线程安全

Queue

Map

HashMap

JDK1.8之前由数组+链表组成。JDK1.8之后当链表长度大于阈值,会转化为红黑树(当前数组的长度小于64时,会先进行数组扩容,而不是转化为红黑树)
LinkedHashMap:继承HashMap,增加了 一条双向链表。
JDK1.7时,HashMap在多线程环境下,扩容操作使用的是头插法,导致链表中的节点指向错误的位置。JDK1.8使用的是为尾插法,但是有可能会出现数据覆盖的问题,并发环境下推荐使用ConcurrentHashMap

Queue

PriorityQueue

使用二叉堆实现,底层使用可变长的数据来存储数据,非线程安全,不知处NULL值和不可排序的对象,默认是小顶堆

BlockingQueue

阻塞队列