字节跳动面试看这一篇就够了

背景

突然想整理下各个大厂的面经,做一个整理,在牛客网上看了一下23年4月到24年1月以来字节后段的面经,做了一个简单的整理。
持续更新中。。。。

网络相关

TCP相关

这里给出两篇比较好的文章:
https://blog.csdn.net/yangtianle1/article/details/129064594
https://blog.csdn.net/hyg0811/article/details/102366854
本文tcp部分的主要内容也是来自于这两篇文章。

TCP三次握手过程&四次挥手过程

总体流程图
字节跳动面试看这一篇就够了_第1张图片

三次握手:
刚开始客户端处于 Closed 的状态,服务端处于 Listen 状态。
进行三次握手:

  • 第一次握手:客户端给服务端发一个 SYN 报文,并指明客户端的初始化序列号 ISN。此时客户端处于 SYN_SENT 状态。首部的同步位SYN=1,初始序号seq=x,SYN=1的报文段不能携带数据,但要消耗掉一个序号。
  • 第二次握手:服务器收到客户端的 SYN 报文之后,会以自己的 SYN 报文作为应答,并且也是指定了自己的初始化序列号 ISN(s)。同时会把客户端的 ISN + 1 作为ACK 的值,表示自己已经收到了客户端的 SYN,此时服务器处于 SYN_RCVD 的状态。 在确认报文段中SYN=1,ACK=1,确认号ack=x+1,初始序号seq=y。
  • 第三次握手:客户端收到 SYN 报文之后,会发送一个 ACK 报文,当然,也是一样把服务器的 ISN + 1 作为 ACK 的值,表示已经收到了服务端的 SYN 报文,此时客户端处于 ESTABLISHED 状态。服务器收到 ACK 报文之后,也处于 ESTABLISHED 状态,此时,双方已建立起了连接。 确认报文段ACK=1,确认号ack=y+1,序号seq=x+1(初始为seq=x,第二个报文段所以要+1),ACK报文段可以携带数据,不携带数据则不消耗序号。
    四次挥手:
    终止一个连接要经过四次挥手。这由TCP的半关闭造成的。TCP提供了连接的一端在结束它的发送后还能接收来自另一端数据的能力。
    刚开始双方都处于ESTABLISHED 状态,假如是客户端先发起关闭请求。四次挥手的过程如下:
  • 第一次挥手:客户端发送一个 FIN 报文,报文中会指定一个序列号。此时客户端处于 FIN_WAIT1 状态。即发出连接释放报文段(FIN=1,序号seq=x+2),并停止再发送数据,主动关闭TCP连接,进入FIN_WAIT1(终止等待1)状态,等待服务端的确认。
  • 第二次挥手:服务端收到 FIN 之后,会发送 ACK 报文,且把客户端的序列号值 +1 作为 ACK 报文的序列号值,表明已经收到客户端的报文了,此时服务端处于 CLOSE_WAIT 状态。 即服务端收到连接释放报文段后即发出确认报文段(ACK=1,确认号ack=x+3),服务端进入CLOSE_WAIT(关闭等待)状态,此时的TCP处于半关闭状态,客户端到服务端的连接释放。客户端收到服务端的确认后,进入FIN_WAIT2(终止等待2)状态,等待服务端发出的连接释放报文段。
  • 第三次挥手:如果服务端也想断开连接了,和客户端的第一次挥手一样,发给 FIN 报文,且指定一个序列号。此时服务端处于 LAST_ACK 的状态。 即服务端没有要向客户端发出的数据,服务端发出连接释放报文段(FIN=1,ACK=1,序号seq=y+1),服务端进入LAST_ACK(最后确认)状态,等待客户端的确认。
  • 第四次挥手:客户端收到 FIN 之后,一样发送一个 ACK 报文作为应答,且把服务端的序列号值 +1 作为自己 ACK 报文的序列号值,此时客户端处于 TIME_WAIT 状态。需要过一阵子以确保服务端收到自己的 ACK 报文之后才会进入 CLOSED 状态,服务端收到 ACK 报文之后,就处于关闭连接了,处于 CLOSED 状态。 即客户端收到服务端的连接释放报文段后,对此发出确认报文段(ACK=1,ack=y+2),客户端进入TIME_WAIT(时间等待)状态。此时TCP未释放掉,需要经过时间等待计时器设置的时间2MSL后,客户端才进入CLOSED状态。
    数据传送:
    仔细观察流程图,可以发现,在起始建联时,双方互换了起始序列号,以客户端发起的请求为例,发起的请求序列号假设为x,那么服务端恢复的ack值就会是ack+1,正好对应了下一次客户端请求需要设置的序列号值。
    建联的SYN请求和断联的FIN请求,也符合这个规则,只是分别在SYN和FIN位置为1.

为什么需要三次握手,而不是两次

  • 第一次握手:客户端发送网络包,服务端收到了。
    • 这样服务端就能得出结论:客户端的发送能力、服务端的接收能力是正常的。
  • 第二次握手:服务端发包,客户端收到了。
    • 这样客户端就能得出结论:服务端的接收、发送能力,客户端的接收、发送能力是正常的。不过此时服务器并不能确认客户端的接收能力是否正常。
  • 第三次握手:客户端发包,服务端收到了。
    • 这样服务端就能得出结论:客户端的接收、发送能力正常,服务器自己的发送、接收能力也正常。

TCP三次握手变2次会有什么问题

  1. 客户端第一次发出连接请求,因为一些网络原因长时间滞留,
  2. 此时因为一直没有接到服务端确认应答,发出第二次连接请求,
  3. 第二次连接请求被服务端处理并建立了连接,当数据传输结束后关闭连接。
  4. 第一次因为网络原因长时间滞留的第一次的请求,终于到达了服务端
  5. 服务端处理后又会建立连接,但是此时客户端已经结束任务了,
  6. 所以客户端会忽略服务端的确认,也不会进行数据传输,
  7. 所以服务端会一直等待客户端传输数据,造成资源浪费

ISN(Initial Sequence Number)是固定的吗

客户端和服务端交换 ISN是三次握手中重要的环节,告诉对方数据是如何按照序号组装的,为了防止攻击者猜出后续的确认号,ISN是随时间变化

三次握手过程中可以携带数据吗

第一次、第二次握手不可以携带数据,第三次可以携带数据
防止攻击者在重复在第一次握手时在SYN报文中塞入大量数据,让服务器花费大量资源来接收处理这些报文。
第三次可以携带数据,是因为此时客户端已经是处于ESTABLISHED(连接)状态了,对于客户端来说已经建立连接了。

SYN(洪泛)攻击是什么

服务器端的资源分配是在二次握手时分配的,而客户端的资源是在完成三次握手时分配的
所以客户端在短时间内伪造大量不存在的ip地址,不断向server发送syn包,server回复确认包并等待client确认时,由于源地址不存在,所以server不断重发最后超时
这些伪造的syn包长时间占用未连接队列,导致正常的SYN请求因为队列满而被丢弃,从而引起网络拥塞甚至系统瘫痪

检测 SYN 攻击非常的方便,当你在服务器上看到大量的半连接状态时,特别是源IP地址是随机的,基本上可以断定这是一次SYN攻击。在 Linux/Unix 上可以使用系统自带的 netstat 命令来检测 SYN 攻击。

防御SYN 攻击

  • 缩短超时(SYN Timeout)时间
  • 增加最大半连接数
  • 过滤网关防护
  • SYN cookies技术

挥手为什么需要四次

服务端收到客户端的SYN连接请求报文后,可以直接发送SYN+ACK报文。其中ACK报文是用来应答的,SYN报文是用来同步的
但是关闭连接时,当服务端收到FIN报文时,很可能并不会立即关闭SOCKET,所以只能先回复一个ACK报文,只有等服务端所有的报文都发送完了,才能发送FIN报文,因此不能一起发送。故需要四次挥手。

2MSL等待状态

TIME_WAIT状态也称为2MSL等待状态。每个具体TCP实现必须选择一个报文段最大生存时间MSL(Maximum Segment Lifetime),它是任何报文段被丢弃前在网络内的最长时间。这个时间是有限的,因为TCP报文段以IP数据报在网络内传输,而IP数据报文则有限制其生存时间的TTL字段。

对一个具体实现所给定的MSL值,处理的原则是:当TCP执行一个主动关闭,并发回最后一个ACK,该连接必须在TIME_WAIT状态停留的时间为2倍的MSL。这样可让TCP再次发送最后的ACK以防这个ACK丢失(另一端超时并重发最后的FIN)。

这种2MSL等待的另一个结果是这个TCP连接在2MSL等待期间,定义这个连接的插口(客户的IP地址和端口号,服务器的IP地址和端口号)不能再被使用。这个连接只能在2MSL结束后才能再被使用。

四次挥手释放连接时,等待2MSL的意义

MSL是Maximum Segment Lifetime的英文缩写,可译为“最长报文段寿命”,它是任何报文在网络上存在的最长时间,超过这个时间报文将被丢弃。

  • 保证客户端发送的最后一个ACK报文段能够到达服务端

    • 保证客户端发送的最后一个ACK报文段能够到达服务器,确保服务器也能正常关闭。
    • 因为这个ACK有可能丢失,从而导致处在LAST-ACK状态的服务器收不到对FIN-ACK的确认报文。
    • 服务器会超时重传这个FIN-ACK,接着客户端再重传一次确认,重新启动时间等待计时器。 最后客户端和服务器都能正常的关闭。
    • 假设客户端不等待2MSL,而是在发送完ACK之后直接释放关闭,一但这个ACK丢失的话,服务器就无法正常的进入关闭连接状态。
  • 防止“已失效的连接请求报文段”出现在本连接中

    • 客户端在发送完最后一个ACK报文段后,再经过2MSL,就可以使本连接持续的时间内所产生的所有报文段都从网络中消失,使下一个新的连接中不会出现这种旧的连接请求报文段。

TIME_WAIT状态需要经过2MSL才能返回到CLOSE状态

理论上,四个报文都发送完毕,就可以直接进入CLOSE状态了,但是可能网络是不可靠的,有可能最后一个ACK丢失。所以TIME_WAIT状态就是用来重发可能丢失的ACK报文。

TCP的flag有哪些

  • SYN:同步标志位。用于建立一个新的TCP连接。通常在连接的初始阶段使用,用于协商初始序列号等参数。
  • FIN:结束标志位。用于终止一个TCP连接。通常在连接的关闭阶段使用,通知对方不再发送数据。
  • ACK:确认标志位。用于指示TCP报文中的确认号字段是否有效。如果ACK标志被设置,那么确认号字段包含了期望的下一个序列号。通常用于确认已经收到了数据。
  • URG:紧急标志位。用于指示数据中包含紧急数据。与紧急指针字段一起使用,通常用于通知接收方应该优先处理这部分数据。
  • PSH:推送标志位。用于告知接收方在接收到这个TCP报文后立即将数据传递给应用层,而不需要等待缓冲区满了再传递。
  • RST:重置连接标志位。用于终止一个TCP连接。通常用于处理异常情况,例如连接中断或无效连接尝试。

socket连接一个未启动的服务器端口会产生什么情况

当你尝试使用Socket连接到一个未启动的服务器端口时,会发生连接失败的情况。这通常会导致一个错误码,通常是"Connection Refused"(连接被拒绝)

## 滑动窗口的作用

实现单次发送多条数据,极大的提高性能

拥塞控制过程

当TCP开始启动的时候,慢启动阈值等于窗口最大值;
在每次超时重发的时候,慢启动阈值会变成原来的一半,同时拥塞窗口置回1;

tcp如何实现一个可靠通信

  • 三次握手
  • 四次挥手
  • 确认应答机制
  • 超时重传机制
  • 连接管理机制
  • 流量控制
  • 拥塞控制

TCP和UDP的区别

1、TCP面向连接(如打电话要先拨号建立连接);UDP是无连接的,即发送数据之前不需要建立连接

2、TCP提供可靠的服务。通过TCP连接传送的数据,无差错,不丢失,不重复,且按序到达;UDP尽最大努力交付,即不保证可靠交付

3、TCP面向字节流,实际上是TCP把数据看成一连串无结构的字节流;UDP是面向报文的

UDP没有拥塞控制,因此网络出现拥塞不会使源主机的发送速率降低(对实时应用很有用,如IP电话,实时视频会议等)

4、每一条TCP连接只能是点到点的;UDP支持一对一,一对多,多对一和多对多的交互通信

5、TCP首部开销20字节;UDP的首部开销小,只有8个字节
6、TCP的逻辑通信信道是全双工的可靠信道,UDP则是不可靠信道

HTTP相关

HTTP(超文本传输协议)是一种应用层通信协议,承载于TCP协议之上。它允许将超文本标记语言(HTML)文档从Web服务器传送到客户端的浏览器。

介绍下 https 原理。

  • HTTP是一个应用层协议,由请求和响应构成,是一个标准的客户端服务器模型。
  • HTTP是一个无状态的协议。HTTP协议通常承载于TCP协议之上,HTTPS就是也承载于TLS或SSL协议层之上。
  • HTTP由请求和响应构成,是一个标准的客户端服务器模型(B/S)。
  • HTTP协议永远都是客户端发起请求,服务器回送响应。
  • HTTP协议是一个双向协议:我们在上网冲浪时,浏览器是请求方A,百度网站就是应答方B。双方约定用HTTP协议来通信,于是浏览器把请求数据发送给网站,网站再把一些数据返回给浏览器,最后由浏览器渲染在屏幕,就可以看到图片、视频了。

HTTPS协议是一种基于传输层安全协议(TLS)的安全协议,用于保护互联网通信安全。HTTPS协议基于传输层安全协议(TLS)协议,并将HTTP协议封装在TLS协议之上,以实现网络通信安全。具体来说,HTTPS协议的实现包含以下步骤:

https是怎么交互来实现“安全”的(HTTPS安全通信原理)

HTTP的不安全性
HTTP作为应用层协议,在客户端与服务端之间的通信传输采用的是明文传输,这就导致了其通信如果被第三方截取,那么通信的内容也就完全暴露了,甚至第三方截取后可以对通信内容进行篡改发给服务端,更有甚者,如钓鱼网站,从一开始可能就完全冒充了服务端来与客户端进行通信。这一系列的安全问题,证明了HTTP不够“可靠”,亟需一种解决方案来让HTTP变得安全可靠。这个解决方案,就是HTTPS

什么是HTTPS
其实我们只需要在TCP/IP五层模型中传输层和应用层中间在添加一个所谓的安全层,由安全层对应用层的数据进行安全加解密处理,就可以保证传输的可靠性了。HTTPS其实就是在应用层协议HTTP和传输层协议TCP/IP之间添加了一个SSL/TLS层,SSL即Secure Sockets Layer 安全套接层,TLS即Transport Layer Security 传输安全层,TLS是SSL的前身,因此统称为SSL/TLS层,其作用就是提供私密性,信息完整性和身份认证

HTTP协议的流程?单指应用层的流程(输入url到渲染的总流程)。

  • 建立连接:客户端(通常是Web浏览器)向服务器发起连接请求。这个连接通常是基于TCP/IP协议的。客户端使用标准端口80(HTTP)或443(HTTPS)发起连接。
  • 请求:客户端向服务器发送HTTP请求。这个请求通常包括以下内容:
    • HTTP方法(GET、POST、PUT、DELETE等):定义对资源的操作。
    • URL(Uniform Resource Locator):指定要访问的资源的地址。
    • 头部信息:包括请求的主机、客户端的信息、接受的数据格式等。
    • 消息体(对于POST请求):如果需要向服务器发送数据,数据将包含在请求消息体中。
  • 服务器处理:服务器接收到HTTP请求后,会根据请求中的信息和服务器上的资源来处理请求。这可能涉及到从数据库检索数据、生成动态内容、访问文件系统等。
  • 响应:服务器会生成HTTP响应,该响应包括以下内容:
    • 状态码:指示请求是否成功,或是否发生了错误。
    • 头部信息:包括响应的日期、服务器信息、响应的数据格式等。
    • 消息体:包含实际的响应数据,例如HTML文档、图像、文本等。
  • 传输数据:服务器将HTTP响应发送回客户端,通常是作为HTTP响应包。这是通过之前建立的TCP连接来完成的。
  • 渲染页面:客户端接收到HTTP响应后,会解析响应的数据并在Web浏览器中呈现。这可能包括渲染HTML、JavaScript执行、加载图像和其他媒体等。
  • 断开连接:一旦响应数据传输完毕,连接可以被断开,尤其对于非持久性连接。客户端和服务器都可以选择关闭连接,或者在需要时保持连接以获取其他资源。

HTTP请求头部有哪些字段

  • HTTP方法(GET、POST、PUT、DELETE等):定义对资源的操作。
  • URL(Uniform Resource Locator):指定要访问的资源的地址。
  • 头部信息:包括请求的主机、客户端的信息、接受的数据格式等。
  • 消息体(对于POST请求):如果需要向服务器发送数据,数据将包含在请求消息体中。

HTTP响应头部有哪些字段

  • 状态码:指示请求是否成功,或是否发生了错误。
  • 头部信息:包括响应的日期、服务器信息、响应的数据格式等。
  • 消息体:包含实际的响应数据,例如HTML文档、图像、文本等。

介绍各个状态码的作用

  • 1xx:(Informational) 信息性状态码,表示正在处理。
  • 2xx:(Success) 成功状态码,表示请求正常。200:请求被成功处理。
  • 3xx:(Redirection) 重定向状态码,表示客户端需要进行附加操作。
  • 4xx:(Client Error) 客户端错误状态码,表示服务器无法处理请求。
  • 5xx:(Server Error) 服务器错误状态码,表示服务器处理请求出错。

简单说一下HTTP的GET和POST

相同:
GET 请求和 POST 请求底层都是基于 TCP/IP 协议实现的,使用二者中的任意一个,都可以实现客户端和服务器端的双向交互。
区别:
1、get主要用于获取数据(也可以修改),post主要用于修改数据
2、get将参数拼接到url上,长度也会被限制,post将参数写入到请求正文内,无参数大小限制。
3、get参数在url里明文传递,不安全
4、GET请求会被浏览器主动缓存,比如常见的CSS,JS,HTML请求都会被缓存,如果下次传输的数据相同,那么他们就会返回缓存中的内容,以求更快的展示所需要的数据。

如何实现免密登录

session和cookie的区别

1、数据存放位置不同:cookie数据存放在客户的浏览器上,session数据放在服务器上。

2、安全程度不同:cookie不是很安全,别人可以分析存放在本地的COOKIE并进行COOKIE欺骗,考虑到安全应当使用session。

3、性能使用程度不同:session会在一定时间内保存在服务器上。当访问增多,会比较占用你服务器的性能,考虑到减轻服务器性能方面,应当使用cookie。

4、数据存储大小不同:单个cookie保存的数据不能超过4K,很多浏览器都限制一个站点最多保存20个cookie,而session则存储与服务端,浏览器对其没有限制。

osi七层模型

七层网络模型

  • 物理层:物理层,包括:EIA/TIARS-232、EIA/TIARS-449、V.35、RJ-45等。
  • 数据链路层:提供介质访问和链路管理,SDLC、HDLC、PPP、STP、帧中继等。
  • 网络层:IP选址和路由选择,包含:IP、IPX、RIP、OSPF等。
  • 传输层:建立、管理和维护端到端的连接,包含:TCP、UDP、SPX等。
  • 会话层:建立、管理和维护会话
  • 表示层:数据格式转换、数据加密
  • 应用层:为应用程序提供服务,包含:Telnet、FTP、HTTP、SNMP等。

tcp和udp哪层

tcp和udp属于传输层协议

数据链路层协议

SDLC、HDLC、PPP、STP、帧中继等。

nginx在osi哪一层

Nginx是七层负载均衡

LVS在osi哪一层

lvs是四层负载均衡

负载均衡在哪一层

二层负载均衡
负载均衡服务器对外提供一个VIP(虚IP),集群中不同的机器采用相同IP地址,但是机器的MAC地址不一样。当负载均衡服务器接受到请求之后,通过改写报文的目标MAC地址的方式将请求转发到目标机器实现负载均衡。

三层负载均衡
和二层负载均衡类似负载均衡服务器对外依然提供一个VIP(虚IP),但是集群中不同的机器采用不同的IP地址。当负载均衡服务器接受到请求之后,根据不同的负载均衡算法,通过IP将请求转发至不同的真实服务器

四层负载均衡:
四层负载均衡只,建立一次TCP连接,工作在OSI模型的传输层,由于在传输层,只有TCP/UDP协议,这两种协议中除了包含源IP、目标IP以外,还包含源端口号及目的端口号,基于IP+端口的负载均衡。通过发布三层的IP地址(VIP),然后加四层的端口号,来决定哪些流量需要做负载均衡,对需要处理的流量进行NAT处理,通过修改数据包的地址信息(IP+端口号)将流量转发到应用服务器。并记录下这个TCP或者UDP的流量是由哪台服务器处理的,后续这个连接的所有流量都同样转发到同一台服务器处理;

七层负载均衡:
负载均衡器与客户端及后端的服务器会分别建立一个TCP连接。即两次TCP连接。就是在四层的基础上(没有四层是绝对不可能有七层的),再考虑应用层的特征,比如同一个Web服务器的负载均衡,除了根据VIP加80端口辨别是否需要处理的流量,还可根据七层的URL、浏览器类别、语言来决定是否要进行负载均衡。举个例子,如果你的Web服务器分成两组,一组是中文语言的,一组是英文语言的,那么七层负载均衡就可以当用户来访问你的域名时,自动辨别用户语言,然后选择对应的语言服务器组进行负载均衡处理。七层就是基于URL等应用层信息的负载均衡

实现负载均衡的几种方式

HTTP重定向负载均衡
浏览器向HTTP重定向服务器发送请求后, 该服务器将一台真实的服务器地址写入HTTP响应头的location, 向浏览器返回一个重定向响应. 浏览器自动重新请求新的URL, 完成自动跳转.

缺点:浏览器需要两次请求服务器才能完成一次访问, 性能较差. 重定向服务器自身的负载较大, 可能成为性能瓶颈.

DNS负载均衡
DNS负责提供域名解析服务, 当访问某个站点时, 需要通过DNS服务器获取域名指向的IP地址. DNS会完成域名到IP地址的映射. 这样的映射也可以是一对多的.可以在DNS服务器中配置域名到多个真实IP地址的映射.
DNS服务器便充当了负载均衡调度器, 每次请求会根据负载均衡算法返回不同的IP地址. 将用户请求分发到多台机器上.

反向代理负载均衡
反向代理服务器处于Web服务器前面, 可以将请求根据负载均衡算法转发到不同的Web服务器上. Web服务器处理完后之后再返回给代理服务器, 代理服务器返回给用户. 反向代理负载均衡服务器工作在应用层. 比如Nginx

IP负载均衡
在网络层通过修改请求目标地址进行负载均衡.

数据包到达负载均衡服务器后, 负载均衡服务器在操作系统内核进程获取网络数据包. 修改数据包的IP地址, 将响应转发到真实服务器, 在响应完成之后, 将响应数据包回到负载均衡服务器. 将源IP地址修改为自身IP地址. 然后返回给用户.

数据链路层负载均衡(直接路由)
直接路由使工作在第二层. 通过修改数据包的MAC地址, 将数据包转发到实际的服务器上. 负载均衡服务器将请求的数据的目的MAC地址修改为真实服务器的地址, 由于Web服务器集群的虚拟IP地址和负载均衡服务器的IP地址相同, 数据可以正常传输到达真实MAC地址的服务器. 处理完后之后发送响应数据到网站网关服务器, 网关服务器直接将请求发送到用户.

负载均衡算法

静态负载均衡算法:
轮询, 权重, IP哈希, URL哈希.

动态负载均衡算法:
动态负载均衡算法: 根据CPU, IO, 网络的处理能力决定如何分发请求. 根据后端服务器的响应时间来分配请求.

如何访问DNS服务器(访问顺序)

  • 第一步:浏览器会检查缓存中有没有这个域名对应的解析过的IP地址,如果有,该解析过程将会结束。浏览器缓存域名也是有限制的,包括缓存的时间、大小,可以通过 TTL 属性来设置。
  • 第二步:如果用户的浏览器缓存中没有,操作系统会先检查自己本地的hosts文件是否有这个网址映射关系,如果有,就先调用这个IP地址映射,完成域名解析。
  • 第三步:如果hosts里没有这个域名的映射,则查找本地 DNS 解析器缓存,是否有这个网址映射关系,如果有,直接返回,完成域名解析。
  • 第四步:如果hosts与本地 DNS 解析器缓存都没有相应的网址映射关系,首先会找TCP/IP参数中设置的首选 DNS 服务器,在此我们叫它本地 DNS 服务器,此服务器收到查询时,如果要查询的域名,包含在本地配置区域资源中,则返回解析结果给客户机,完成域名解析,此解析具有权威性。
  • 第五步:如果要查询的域名,不由本地DNS服务器区域解析,但该服务器已缓存了此网址映射关系,则调用这个IP地址映射,完成域名解析,此解析不具有权威性。
  • 第六步:如果本地 DNS 服务器本地区域文件与缓存解析都失效,则根据本地 DNS 服务器的设置(是否设置转发器)进行查询,如果未用转发模式,本地 DNS 就把请求发至13台根 DNS,根 DNS 服务器收到请求后会判断这个域名(.com)是谁来授权管理,并会返回一个负责该顶级域名服务器的一个IP。
  • 本地 DNS 服务器收到IP信息后,将会联系负责 .com 域的这台服务器。这台负责 .com 域的服务器收到请求后,如果自己无法解析,它就会找一个管理 .com 域的下一级 DNS 服务器地址给本地 DNS 服务器。当本地 DNS 服务器收到这个地址后,就会找域名服务器,重复上面的动作,进行查询,直至找到域名对应的主机。
  • 第七步:如果用的是转发模式,此 DNS 服务器就会把请求转发至上一级 DNS 服务器,由上一级服务器进行解析,上一级服务器如果不能解析,或找根 DNS 或把转请求转至上上级,以此循环。不管是本地 DNS 服务器用的是转发,还是根提示,最后都是把结果返回给本地 DNS 服务器,由此 DNS 服务器再返回给客户机。

网络代理,网络代理正向和反向区别

区别有:用途不同。安全性不同。目的不同。代理不同。服务对象不同。功能不同。

  • 用途不同。正向代理的典型用途是为在防火墙内的局域网客户端提供访问Internet的途径。正向代理还可以使用缓冲特性减少网络使用率。反向代理的典型用途是将防火墙后面的服务器提供给Internet用户访问。
  • 安全性不同。正向代理允许客户端通过它访问任意网站并且隐藏客户端自身,因此你必须采取安全措施以确保仅为经过授权的客户端提供服务。反向代理对外都是透明的,访问者并不知道自己访问的是一个代理。
  • 目的不同。正向代理实际代理的是客户端。反向代理代理的是目标服务器。
  • 代理不同。正向代理是客户端架构,而反向代理是服务器架构。
    服务对象不同。正向代理中,服务器不知道真正的用户是谁。反向代理中,用户不知道真正的服务器是谁。
  • 功能不同。正向代理主要用来解决访问问题。反向代理主要用于解决负载均衡、安全防护,但二者都能提高访问速度。

go

go怎么实现并发

Go使用协程(goroutines)和通道(channels)来实现并发编程。

  • 协程(Goroutines):
    协程是Go中的轻量级线程,由Go运行时管理。与传统线程相比,协程的创建和销毁成本很低,因此可以轻松创建数千个协程。
    使用go关键字可以启动一个新的协程。例如:go someFunction()。
    协程运行在相同的地址空间中,因此它们可以共享数据,并且不需要显式的锁定来保护共享状态。
  • 通道(Channels):
    通道是一种用于在协程之间传递数据的机制,它提供了一种同步的方式,确保数据在发送和接收之间正确地同步。
    通道使用make函数创建:ch := make(chan int)。
    发送数据到通道:ch <- data。
    从通道接收数据:data := <-ch。
    通道还可以用于关闭通信:close(ch)。
  • 选择语句(Select Statement):
    • 选择语句用于在多个通道操作中选择一个可以执行的操作。
    • 它使您可以编写非阻塞的代码,从而可以同时处理多个通道。
    • 示例:select {
      case msg1 := <-ch1:
      fmt.Println(“Received”, msg1)
      case ch2 <- data:
      fmt.Println(“Sent”, data)
      }
  • 互斥锁(Mutex):
    • Go提供了互斥锁来保护共享资源免受并发访问的影响。可以使用sync包中的Mutex类型来创建锁。
    • 示例:var mu sync.Mutex
      mu.Lock()
      // 访问共享资源
      mu.Unlock()
  • 条件变量(Cond):
    • 条件变量用于在多个协程之间进行条件等待。可以使用sync包中的Cond类型来创建条件变量。
    • 示例:var mu sync.Mutex
      cond := sync.NewCond(&mu)
      // 等待条件满足
      cond.Wait()
  • 原子操作:Go还提供了原子操作,允许在不使用互斥锁的情况下执行特定操作。sync/atomic包包含了原子操作的实现。
  • 并发模式:Go支持多种并发模式,包括生产者-消费者模式、工作池模式、扇出-扇入模式等。这些模式可以帮助您组织和管理并发代码。
  • 并发安全(Concurrency Safety):Go鼓励编写并发安全的代码,以避免竞态条件和数据竞争。使用通道和互斥锁来确保数据的正确同步。
  • 并行编程:Go还支持并行编程,允许将工作分配给多个处理器核心,以加速计算密集型任务。runtime包提供了控制并行度的功能。

GMP可以没有P吗

字节跳动面试看这一篇就够了_第2张图片
groutine能拥有强大的并发实现是通过GPM调度模型实现,下面就来解释下goroutine的调度模型。

M:N模型

每个用户线程对应多个内核空间线程,同时也可以一个内核空间线程对应多个用户空间线程。Go采用这种模型,使用多个内核线程管理多个goroutine。这样结合了以上两种模型的优点,但缺点就是调度的复杂性。

基本概念

  • M:M是对内核级线程的封装,数量对应真实的CPU数,一个M就是一个线程,goroutine就是跑在M之上的;

  • G:代表一个goroutine,它有自己的栈,用于调度。

  • P:P全称是Processor,处理器,它的主要用途就是用来执行goroutine的。每个Processor对象都拥有一个LRQ(Local Run Queue),未分配的Goroutine对象保存在**GRQ(Global Run Queue )**中,等待分配给某一个P的LRQ中,每个LRQ里面包含若干个用户创建的Goroutine对象。
      Golang采用的是M:N线程模型,更详细的说他是一个两级线程模型,但它对系统线程(内核级线程)进行了封装,暴露了一个轻量级的协程goroutine(用户级线程)供用户使用,而用户级线程到内核级线程的调度由golang的runtime负责,调度逻辑对外透明。goroutine的优势在于上下文切换在完全用户态进行,无需像线程一样频繁在用户态与内核态之间切换,节约了资源消耗。

mutex底层实现

是以runtime中实现的底层同步机制(cas、atomic、spinlock、sem)为基础的。

  • 1 cas(Compare And Swap)和原子运算是其他同步机制的基础

    • 原子操作:指那些不能够被打断的操作被称为原子操作,当有一个CPU在访问这块内容addr时,其他CPU就不能访问。
    • CAS:比较及交换,其实也属于原子操作,但它是非阻塞的,所以在被操作值被频繁变更的情况下,CAS操作并不那么容易成功,不得不利用for循环以进行多次尝试。
  • 2 自旋锁(spinlock)

    • 自旋锁是指当一个线程在获取锁的时候,如果锁已经被其他线程获取,那么该线程将循环等待,然后不断地判断是否能够被成功获取,知直到获取到锁才会退出循环。获取锁的线程一直处于活跃状态 Golang中的自旋锁用来实现其他类型的锁,与互斥锁类似,不同点在于,它不是通过休眠来使进程阻塞,而是在获得锁之前一直处于活跃状态(自旋)。
  • 3 信号量

    • 实现休眠和唤醒协程的一种方式。

为什么多协程和多线程相比多协程效率更好

协程(coroutine)是一种比线程更轻量级的并发执行单元,它的切换开销和内存栈的占用大小都比线程要小。

只要内存足够,在一个线程中可有上万个或更多的协程。

在网络编程应用中,采用协程后的业务代码比那些采用异步或事件回调方式的代码更好维护。

使用协程的业务逻辑代码表面看上去是同步执行的,编写这些代码时思维是连贯的,更符合人类的思维习惯;而采用异步和回调方式后,业务逻辑代码被分割(分拆)在多个回调方法中,开发人员的思维需要切换,是跳跃的,这样显然容易出错,并且一些场景下还要设计事件或会话上下文数据结构,来保存那些需要在多个回调方法间共享的状态数据。

从语言层面原生支持协程,在函数或者方法前面加 go关键字就可创建一个协程。

协程是在线程中执行的,在Golang中使用环境变量GOMAXPROCS来控制协程使用的线程数,它的缺省值是1,在1.5版本中改为cpu核数。也可在代码中调用

线程、协程、进程区别

概念

  • 进程: 进程是一个具有一定独立功能的程序关于某个数据集合上的一次运行活动,是系统资源分配和独立运行的最小单位
  • 线程: 线程是进程的一个执行单元,是任务调度和系统执行的最小单位
  • 协程: 协程是一种用户态的轻量级线程,协程的调度完全由用户控制。

进程与线程的区别

  • 根本区别:
    • 进程是操作系统资源分配和独立运行的最小单位;
    • 线程是任务调度和系统执行的最小单位。
  • 地址空间区别:
    • 每个进程都有独立的地址空间,一个进程崩溃不影响其它进程;
    • 一个进程中的多个线程共享该 进程的地址空间,一个线程的非法操作会使整个进程崩溃。
  • 上下文切换开销区别:
    • 每个进程有独立的代码和数据空间,进程之间上下文切换开销较大;
    • 线程组共享代码和数据空间,线程之间切换的开销较小。# 数据库相关

协程

协程拥有自己的寄存器上下文和栈。协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈。因此,协程能保留上一次调用时的状态(即所有局部状态的一个特定组合),每次过程重入时,就相当于进入上一次调用的状态。这个过程完全由程序控制,不需要内核进行调度。
协程与线程的关系如下图所示:
字节跳动面试看这一篇就够了_第3张图片

golang协程池, 作用是什么

  • 资源控制和重用:通过使用协程池,可以控制并发任务的数量,避免资源被过度占用。协程池中预先创建的goroutine可以被重复使用,而不需要频繁地创建和销毁,从而降低了系统开销。

  • 提高性能和吞吐量:协程池能够有效地管理并发任务的执行,通过合理调度和分配任务,可以最大限度地利用系统资源,提高并发性能和吞吐量。通过避免创建大量的goroutine和减少上下文切换,可以减少系统负载,提高处理能力。

  • 控制并发度和资源限制:使用协程池可以限制并发任务的数量,确保系统资源不会被过度占用。可以根据系统的处理能力和资源限制,设置适当的协程池大小,避免资源耗尽和系统崩溃。

  • 避免竞态条件:在多个goroutine并发执行时,如果它们之间共享某些资源,可能会出现竞态条件。使用协程池可以通过限制并发的数量,避免过多的goroutine同时访问共享资源,从而减少竞态条件的发生。

  • 简化并发编程:使用协程池可以将任务的调度和管理逻辑与具体的任务逻辑分离开来,使得并发编程变得更加简单和直观。开发人员只需要关注具体的任务实现,而不需要手动管理goroutine的创建和销毁,以及任务的调度和排队。

golang中哪些是引用类型,哪些是值类型

引用类型

变量存储的是一个地址,这个地址存储最终的值。内存通常在堆上分配,当没有任何变量引用这个地址时,该地址对应的数据空间就成为一个垃圾,通过GC回收。

  • 指针
  • slice
  • map
  • chan
  • interface
  • func

值类型

变量直接存储的值,内存通常在栈中分配。

  • int
  • float
  • bool
  • string
  • 数组
  • struct

make与new的区别

make用于内建类型(map、slice 和channel)的内存分配。new用于各种类型的内存分配

new返回指针

内建函数new本质上说跟其它语言中的同名函数功能一样:new(T)分配了零值填充的T类型的内存空间,并且返回其地址,即一个*T类型的值。用Go的术语说,它返回了一个指针,指向新分配的类型T的零值。

make返回初始化后的(非零)值。

内建函数make(T, args)与new(T)有着不同的功能,make只能创建slice、map和channel,并且返回一个有初始值(非零)的T类型,而不是*T。本质来讲,导致这三个类型有所不同的原因是指向数据结构的引用在使用前必须被初始化。
例如,一个slice,是一个包含指向数据(内部array)的指针、长度和容量的三项描述符;在这些项目被初始化之前,slice为nil。对于slice、map和channel来说,make初始化了内部的数据结构,填充适当的值

make一个len为10的int切片,再append一个1进去,会打印什么情况

应该是10个0还有一个1,因为make会初始化10len为0,append的话是从后面追加

CSP是怎么样的

CSP(Communicating Sequential Processes,通信顺序进程)并发模型倡导使用通信的手段来进行共享内存,继而实现多个线程之间的通信。这也是 Golang 倡导使用的并发模型,通过 Channel 来使用。

CSP 有两个核心概念:

  • 并发实体:在 Golang 中就是 Goroutine,它们相互独立,且并发执行;
  • 通道(Channel):并发实体之间使用 Channel 发送信息。

使用 Go 通道时,常见问题和注意事项

channel 为什么阻塞了?

  • go channel 如果没有设置缓冲队列,无论读取还是写入,都会阻塞

什么情况下关闭 channel 会造成 panic ?

  • 未初始化时关闭
  • 重复关闭
  • 关闭后发送
  • 发送时关闭。

有没有必要关闭 channel?不关闭又如何?

  • channel 的发送次数等于接收次数
    发送者 go routine 和接收者 go routine 分别都会在发送或接收结束时结束各自的 go routine。ch 会由于没有代码使用被垃圾收集器回收。因此这种情况下,不关闭 channel,没有任何副作用。
  • channel 的发送次数大于/小于接收次数
    channel 的发送次数小于接收次数时,接收者 go routine 由于等待发送者发送一直阻塞。因此接收者 go routine 一直未退出,ch 也由于一直被接收者使用无法被垃圾回收。未退出的 go routine 和未被回收的 channel 都造成了内存泄漏的问题。
    因此,在发送者与接收者一对一的情况下,只要我们确保发送者或接收者不会阻塞,不关闭 channel 是可行的。在我们无法准确判断 channel 的发送次数和接收次数时,我们应该在合适的时机关闭 channel。那么如何判断 channel 是否关闭呢?

如何判断 channel 是否关闭?

go channel 关闭后,读取该 channel 永远不会阻塞,且只会输出对应类型的零值。

nil 可能也是需要 channel传输的值之一,通常我们无法通过判断是否为类型的零值确定 channel 是否关闭。所以为了避免输出无意义的值,我们需要一种合理的方式判断 channel 是否关闭。golang 官方为我们提供了两种方式。

  • 使用 channel 的多重返回值(如 err, ok := <-errCh )
  • 解决方案二:使用 for range 简化语法

如何优雅地关闭 channel ?

如果发生重复关闭、关闭后发送等问题,会造成 channel panic。那么如何优雅地关闭 channel,是我们关心的一个问题。

  • 一个发送者,一个接收者:发送者关闭 channel,接收者使用 select 或 for range 判断 channel 是否关闭。
  • 一个发送者,多个接收者:发送者关闭 channel,同上。
  • 多个发送者,一个接收者:接收者接收完毕后,使用专用的 stop channel 关闭;发送者使用 select 监听 stop channel 是否关闭。
  • 多个发送者,多个接收者:任意一方使用专用的 stop channel 关闭;发送者、接收者都使用 select 监听 stop channel 是否关闭。

java 和Go 你怎么看,有啥特点和区别

  • 1、函数重载:
    Go上不允许函数重载,必须具有方法和函数的唯一名称。java允许函数重载。
  • 2、速度:
    go的速度比java快,它在不使用虚拟机的情况下编译其代码,直接编译成二进制文件。
  • 3、多态:
    Java默认允许多态。而Go没有。
  • 4、路由配置:
    Go语言使用HTTP协议进行路由配置
    java使用Akka.routing.ConsistentHashingRouter和Akka.routing.ScatterGatherFirstCompletedRouter进行路由配置。
  • 5、可扩展性:
    Go代码可以自动扩展到多个核心;而,Java并不总是具有足够的可扩展性。
  • 6、类型系统:
    JAVA中有两套完全独立的类型系统,一套是值类型系统,byte、int、boolean、char、double另一套是以object类型为根的对象类型系统,Integer,HashMap等。值类型系统希望用object类型引用,则需要装箱。
    go语言中多数类型都是值语义,甚至包括一些复合类型如数组(array),结构体(struct)等,并且这些类型都可以有方法。我们可以给任何类型增加新方法。同时Go语言可以通过&获得一个对象的引用如 var b=&a
  • 7、对象传递:
    Java中对象的方法会有隐藏的this指针传递
    而Go语言中面向对象只是换了一种语法形式来表达,没有隐藏的this指针,即方法施加的目标显示传递,没有被隐藏。
  • 8、初始化:
    Java中有默认的构造函数或者用户自定义的构造函数
    Go中不需要,可以自定义一个普通的函数。
  • 9、继承:
    Java的继承通过extends关键字完成,不支持多继承。
    Go语言的继承通过匿名组合完成:基类以Struct的方式定义,子类只需要把基类作为成员放在子类的定义中,并且可以通过调整基类成员的位置改变内存布局,支持多继承。
  • 10、接口:
    java中的接口作为不同组件中的契约存在,是强制的。
    而GO语言中采用的是非侵入式接口,一个类只需要实现接口要求的所有函数,那我们就说该类实现了该接口。

数据库

mysql数据库

这里给出涉及到的比较好的文章:
https://blog.csdn.net/weixin_39725844/article/details/111200870

B+树,B树与B+树区别

在MySQL数据库中,B树和B+树是常用的索引结构,用于加快数据库的查询速度。它们都是多叉树结构,但在一些方面有一些不同之处。

B树
B树是一种平衡的多叉树结构,其中每个节点可以拥有多个子节点。B树的特点包括:

  • 每个节点可以存储多个键值对,而不仅仅是一个键值对。这使得B树可以在一个节点中存储更多的数据,减少了磁盘I/O次数。
  • B树的所有叶子节点都在同一层。这使得B树可以快速定位到指定键值,提高了查询的效率。
  • B树的每个节点都会保存一部分键值对,并按照键值的大小进行排序。这样可以在查询时使用二分查找算法,减少查找的时间复杂度。
  • B树的高度相对较低,因为每个节点可以存储多个键值对。这使得B树的查询速度相对较快。

B+树
B+树也是一种平衡的多叉树结构,与B树相似,但在一些方面有所不同。B+树的特点包括:

  • B+树的非叶子节点只存储键值,而不存储数据。数据只存在于叶子节点中。这样可以减少非叶子节点的存储空间,提高了B+树的存储效率。
  • B+树的所有叶子节点使用链表连接起来,形成一个有序的链表。这使得B+树可以进行范围查询,并且可以很方便地进行顺序访问。
  • B+树的叶子节点之间没有指针,只有叶子节点与非叶子节点之间有指针。这样可以减少B+树的内存占用,提高了查询的效率。
  • B+树的高度相对较高,因为每个节点只存储键值,而不存储数据。这使得B+树的查询速度相对较慢,但对于大量数据的范围查询和顺序访问非常高效。

存相同的数据哪个树高

这里涉及到B树和B+树的区别,因为B树的每个节点都存储数据,并且B树的节点大小与磁盘页大小相同,每次读取磁盘页时可能会读取一些不需要的数据。

redis数据库

算法

287找重复的数

287. 寻找重复数

题目

给定一个包含 n + 1 个整数的数组 nums ,其数字都在 [1, n] 范围内(包括 1 和 n),可知至少存在一个重复的整数。

假设 nums 只有 一个重复的整数 ,返回 这个重复的数 。

你设计的解决方案必须 不修改 数组 nums 且只用常量级 O(1) 的额外空间。

示例 1:
输入:nums = [1,3,4,2,2]
输出:2

示例 2:
输入:nums = [3,1,3,4,2]
输出:3

提示:

1 <= n <= 105
nums.length == n + 1
1 <= nums[i] <= n
nums 中 只有一个整数 出现 两次或多次 ,其余整数均只出现 一次

解题

官方题解代码:

func findDuplicate(nums []int) int {
	lo, hi := 1, len(nums)-1
	for lo < hi {
		mid := (lo + hi) >> 1
		count := 0
		for i := 0; i < len(nums); i++ {
			if nums[i] <= mid {
				count++
			}
		}
		if count > mid {
			hi = mid
		} else {
			lo = mid + 1
		}
	}
	return lo
}

一篇非常好的题解:https://leetcode.cn/problems/find-the-duplicate-number/solutions/262703/zhe-ge-shu-zu-you-dian-te-shu-suo-yi-ke-yi-yong-ku

516最长回文子序列

516最长回文子序列

题目

给你一个字符串 s ,找出其中最长的回文子序列,并返回该序列的长度。

子序列定义为:不改变剩余字符顺序的情况下,删除某些字符或者不删除任何字符形成的一个序列。

示例 1:
输入:s = “bbbab”
输出:4
解释:一个可能的最长回文子序列为 “bbbb” 。

示例 2:
输入:s = “cbbd”
输出:2
解释:一个可能的最长回文子序列为 “bb” 。

提示:

1 <= s.length <= 1000
s 仅由小写英文字母组成

题目分析

官方题解代码:

func longestPalindromeSubseq(s string) int {
    n := len(s)
    dp := make([][]int, n)
    for i := range dp {
        dp[i] = make([]int, n)
    }
    for i := n - 1; i >= 0; i-- {
        dp[i][i] = 1
        for j := i + 1; j < n; j++ {
            if s[i] == s[j] {
                dp[i][j] = dp[i+1][j-1] + 2
            } else {
                dp[i][j] = max(dp[i+1][j], dp[i][j-1])
            }
        }
    }
    return dp[0][n-1]
}

func max(a, b int) int {
    if a > b {
        return a
    }
    return b
}

一篇非常好的题解:https://leetcode.cn/problems/longest-palindromic-subsequence/solutions/67456/zi-xu-lie-wen-ti-tong-yong-si-lu-zui-chang-hui-wen

128 最长连续序列

128 最长连续序列

题目

给定一个未排序的整数数组 nums ,找出数字连续的最长序列(不要求序列元素在原数组中连续)的长度。

请你设计并实现时间复杂度为 O(n) 的算法解决此问题。

示例 1:
输入:nums = [100,4,200,1,3,2]
输出:4
解释:最长数字连续序列是 [1, 2, 3, 4]。它的长度为 4。

示例 2:
输入:nums = [0,3,7,2,5,8,4,6,0,1]
输出:9

提示:
0 <= nums.length <= 105
-109 <= nums[i] <= 109

题解分析

官方题解代码:

func longestConsecutive(nums []int) int {
    numSet := map[int]bool{}
    for _, num := range nums {
        numSet[num] = true
    }
    longestStreak := 0
    for num := range numSet {
        if !numSet[num-1] {
            currentNum := num
            currentStreak := 1
            for numSet[currentNum+1] {
                currentNum++
                currentStreak++
            }
            if longestStreak < currentStreak {
                longestStreak = currentStreak
            }
        }
    }
    return longestStreak
}

加一下注释:

func longestConsecutive(nums []int) int {
	// 创建一个哈希集合,用于存储数组中的数字,这里是乱序的set
	numSet := make(map[int]bool)
	for _, num := range nums {
		numSet[num] = true
	}
	// 记录最长连续序列的长度
	longestStreak := 0
	// 遍历哈希集合中的每个数字
	for num := range numSet {
		// 如果当前数字的前一个数字不在哈希集合中,则当前数字是连续序列的开头,set中可能同时存在多个序列,例如1、2、3、4是第一个序列,7、8、9、10、11、12是第二个序列,这里就会分别找到两个序列
		if !numSet[num-1] {
			currentNum := num
			currentStreak := 1
			// 继续递增当前数字,直到连续序列结束
			for numSet[currentNum+1] {
				currentNum++
				currentStreak++
			}
			// 更新最长连续序列的长度
			if currentStreak > longestStreak {
				longestStreak = currentStreak
			}
		}
	}
	// 返回最长连续序列的长度
	return longestStreak
}

LCR 095. 最长公共子序列

LCR 095. 最长公共子序列

题目

给定两个字符串 text1 和 text2,返回这两个字符串的最长 公共子序列 的长度。如果不存在 公共子序列 ,返回 0 。

一个字符串的 子序列 是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。

例如,“ace” 是 “abcde” 的子序列,但 “aec” 不是 “abcde” 的子序列。
两个字符串的 公共子序列 是这两个字符串所共同拥有的子序列。

示例 1:
输入:text1 = “abcde”, text2 = “ace”
输出:3
解释:最长公共子序列是 “ace” ,它的长度为 3 。

示例 2:
输入:text1 = “abc”, text2 = “abc”
输出:3
解释:最长公共子序列是 “abc” ,它的长度为 3 。

示例 3:
输入:text1 = “abc”, text2 = “def”
输出:0
解释:两个字符串没有公共子序列,返回 0 。

提示:

1 <= text1.length, text2.length <= 1000
text1 和 text2 仅由小写英文字符组成。

题目分析

官方题解代码:

func longestCommonSubsequence(text1, text2 string) int {
    m, n := len(text1), len(text2)
    dp := make([][]int, m+1)
    for i := range dp {
        dp[i] = make([]int, n+1)
    }
    for i, c1 := range text1 {
        for j, c2 := range text2 {
            if c1 == c2 {
                dp[i+1][j+1] = dp[i][j] + 1
            } else {
                dp[i+1][j+1] = max(dp[i][j+1], dp[i+1][j])
            }
        }
    }
    return dp[m][n]
}

func max(a, b int) int {
    if a > b {
        return a
    }
    return b
}

这里找到一个B站讲解的十分透彻的案例:最长公共子序列 LCS
案例伪代码:
字节跳动面试看这一篇就够了_第4张图片案例里对应的表格示意图:
字节跳动面试看这一篇就够了_第5张图片

三数之和

15. 三数之和

题目

给你一个整数数组 nums ,判断是否存在三元组 [nums[i], nums[j], nums[k]] 满足 i != j、i != k 且 j != k ,同时还满足 nums[i] + nums[j] + nums[k] == 0 。请你返回所有和为 0 且不重复的三元组。

注意:答案中不可以包含重复的三元组。

示例 1:
输入:nums = [-1,0,1,2,-1,-4]
输出:[[-1,-1,2],[-1,0,1]]
解释:
nums[0] + nums[1] + nums[2] = (-1) + 0 + 1 = 0 。
nums[1] + nums[2] + nums[4] = 0 + 1 + (-1) = 0 。
nums[0] + nums[3] + nums[4] = (-1) + 2 + (-1) = 0 。
不同的三元组是 [-1,0,1] 和 [-1,-1,2] 。
注意,输出的顺序和三元组的顺序并不重要。

示例 2:
输入:nums = [0,1,1]
输出:[]
解释:唯一可能的三元组和不为 0 。

示例 3:
输入:nums = [0,0,0]
输出:[[0,0,0]]
解释:唯一可能的三元组和为 0 。

提示:
3 <= nums.length <= 3000
-105 <= nums[i] <= 105

题目分析

官方题解代码:

func threeSum(nums []int) [][]int {
    n := len(nums)
    sort.Ints(nums)
    ans := make([][]int, 0)
 
    // 枚举 a
    for first := 0; first < n; first++ {
        // 需要和上一次枚举的数不相同
        if first > 0 && nums[first] == nums[first - 1] {
            continue
        }
        // c 对应的指针初始指向数组的最右端
        third := n - 1
        target := -1 * nums[first]
        // 枚举 b
        for second := first + 1; second < n; second++ {
            // 需要和上一次枚举的数不相同
            if second > first + 1 && nums[second] == nums[second - 1] {
                continue
            }
            // 需要保证 b 的指针在 c 的指针的左侧
            for second < third && nums[second] + nums[third] > target {
                third--
            }
            // 如果指针重合,随着 b 后续的增加
            // 就不会有满足 a+b+c=0 并且 b
            if second == third {
                break
            }
            if nums[second] + nums[third] == target {
                ans = append(ans, []int{nums[first], nums[second], nums[third]})
            }
        }
    }
    return ans
}

官方题解:https://leetcode.cn/problems/3sum/solutions/284681/san-shu-zhi-he-by-leetcode-solution
这个整体比较好理解,有几个需要注意的点:
1、枚举a和b时都需要注意不能和上一次枚举的数不同
2、需要保证b的指针一直在c的左侧

206. 反转链表

206. 反转链表

题目

给你单链表的头节点 head ,请你反转链表,并返回反转后的链表。

示例 1:
字节跳动面试看这一篇就够了_第6张图片

输入:head = [1,2,3,4,5]
输出:[5,4,3,2,1]

示例 2:
字节跳动面试看这一篇就够了_第7张图片

输入:head = [1,2]
输出:[2,1]

示例 3:
输入:head = []
输出:[]

提示:

链表中节点的数目范围是 [0, 5000]
-5000 <= Node.val <= 5000

题目解析

官方题解代码:

func reverseList(head *ListNode) *ListNode {
    var prev *ListNode
    curr := head
    for curr != nil {
        next := curr.Next
        curr.Next = prev
        prev = curr
        curr = next
    }
    return prev
}

92. 反转链表 II

92. 反转链表 II

题目

给你单链表的头指针 head 和两个整数 left 和 right ,其中 left <= right 。请你反转从位置 left 到位置 right 的链表节点,返回 反转后的链表 。

示例 1:
字节跳动面试看这一篇就够了_第8张图片

输入:head = [1,2,3,4,5], left = 2, right = 4
输出:[1,4,3,2,5]

示例 2:
输入:head = [5], left = 1, right = 1
输出:[5]

提示:
链表中节点数目为 n
1 <= n <= 500
-500 <= Node.val <= 500
1 <= left <= right <= n

题目分析

官方题解代码:

func reverseLinkedList(head *ListNode) {
    var pre *ListNode
    cur := head
    for cur != nil {
        next := cur.Next
        cur.Next = pre
        pre = cur
        cur = next
    }
}

func reverseBetween(head *ListNode, left, right int) *ListNode {
    // 因为头节点有可能发生变化,使用虚拟头节点可以避免复杂的分类讨论
    dummyNode := &ListNode{Val: -1}
    dummyNode.Next = head

    pre := dummyNode
    // 第 1 步:从虚拟头节点走 left - 1 步,来到 left 节点的前一个节点
    // 建议写在 for 循环里,语义清晰
    for i := 0; i < left-1; i++ {
        pre = pre.Next
    }

    // 第 2 步:从 pre 再走 right - left + 1 步,来到 right 节点
    rightNode := pre
    for i := 0; i < right-left+1; i++ {
        rightNode = rightNode.Next
    }

    // 第 3 步:切断出一个子链表(截取链表)
    leftNode := pre.Next
    curr := rightNode.Next

    // 注意:切断链接
    pre.Next = nil
    rightNode.Next = nil

    // 第 4 步:同第 206 题,反转链表的子区间
    reverseLinkedList(leftNode)

    // 第 5 步:接回到原来的链表中
    pre.Next = rightNode
    leftNode.Next = curr
    return dummyNode.Next
}

面经链接

https://www.nowcoder.com/discuss/566306341396951040?sourceSSR=enterprise
https://www.nowcoder.com/discuss/569116932234829824?sourceSSR=enterprise
https://www.nowcoder.com/discuss/572339408033124352?sourceSSR=enterprise
https://www.nowcoder.com/discuss/563096059170316288?sourceSSR=enterprise
https://www.nowcoder.com/discuss/561666823398121472?sourceSSR=enterprise
https://www.nowcoder.com/discuss/559376234627497984?sourceSSR=enterprise
https://www.nowcoder.com/discuss/555904859770314752?sourceSSR=enterprise
https://www.nowcoder.com/discuss/554371366380650496?sourceSSR=enterprise
https://www.nowcoder.com/discuss/550639195967000576?sourceSSR=enterprise
https://www.nowcoder.com/discuss/551074669743378432?sourceSSR=enterprise
https://www.nowcoder.com/discuss/544981012556607488?sourceSSR=enterprise
https://www.nowcoder.com/discuss/542697256915415040?sourceSSR=enterprise
https://www.nowcoder.com/discuss/537645679745757184?sourceSSR=enterprise
https://www.nowcoder.com/discuss/536351021895901184?sourceSSR=enterprise
https://www.nowcoder.com/discuss/536236744748826624?sourceSSR=enterprise
https://www.nowcoder.com/discuss/534130180848095232?sourceSSR=enterprise
https://www.nowcoder.com/discuss/532303076607197184?sourceSSR=enterprise
https://www.nowcoder.com/discuss/531123350265937920?sourceSSR=enterprise
https://www.nowcoder.com/discuss/527828062470144000?sourceSSR=enterprise
https://www.nowcoder.com/discuss/516012580041682944?sourceSSR=enterprise
https://www.nowcoder.com/discuss/513289737344389120?sourceSSR=enterprise
https://www.nowcoder.com/discuss/511213837740269568?sourceSSR=enterprise
https://www.nowcoder.com/discuss/492623452088651776?sourceSSR=enterprise
https://www.nowcoder.com/discuss/491932734969958400?sourceSSR=enterprise
https://www.nowcoder.com/discuss/488469089342533632?sourceSSR=enterprise
https://www.nowcoder.com/discuss/486648762086019072?sourceSSR=enterprise
https://www.nowcoder.com/discuss/478026030490402816?sourceSSR=enterprise
https://www.nowcoder.com/discuss/475259205662806016?sourceSSR=enterprise
https://www.nowcoder.com/discuss/471469172959080448?sourceSSR=enterprise

你可能感兴趣的:(面经,面试,字节跳动)