突然想整理下各个大厂的面经,做一个整理,在牛客网上看了一下23年4月到24年1月以来字节后段的面经,做了一个简单的整理。
持续更新中。。。。
这里给出两篇比较好的文章:
https://blog.csdn.net/yangtianle1/article/details/129064594
https://blog.csdn.net/hyg0811/article/details/102366854
本文tcp部分的主要内容也是来自于这两篇文章。
三次握手:
刚开始客户端处于 Closed 的状态,服务端处于 Listen 状态。
进行三次握手:
客户端和服务端交换 ISN是三次握手中重要的环节,告诉对方数据是如何按照序号组装的,为了防止攻击者猜出后续的确认号,ISN是随时间变化
第一次、第二次握手不可以携带数据,第三次可以携带数据
防止攻击者在重复在第一次握手时在SYN报文中塞入大量数据,让服务器花费大量资源来接收处理这些报文。
第三次可以携带数据,是因为此时客户端已经是处于ESTABLISHED(连接)状态了,对于客户端来说已经建立连接了。
服务器端的资源分配是在二次握手时分配的,而客户端的资源是在完成三次握手时分配的
所以客户端在短时间内伪造大量不存在的ip地址,不断向server发送syn包,server回复确认包并等待client确认时,由于源地址不存在,所以server不断重发最后超时
这些伪造的syn包长时间占用未连接队列,导致正常的SYN请求因为队列满而被丢弃,从而引起网络拥塞甚至系统瘫痪
检测 SYN 攻击非常的方便,当你在服务器上看到大量的半连接状态时,特别是源IP地址是随机的,基本上可以断定这是一次SYN攻击。在 Linux/Unix 上可以使用系统自带的 netstat 命令来检测 SYN 攻击。
服务端收到客户端的SYN连接请求报文后,可以直接发送SYN+ACK报文。其中ACK报文是用来应答的,SYN报文是用来同步的
但是关闭连接时,当服务端收到FIN报文时,很可能并不会立即关闭SOCKET,所以只能先回复一个ACK报文,只有等服务端所有的报文都发送完了,才能发送FIN报文,因此不能一起发送。故需要四次挥手。
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结束后才能再被使用。
MSL是Maximum Segment Lifetime的英文缩写,可译为“最长报文段寿命”,它是任何报文在网络上存在的最长时间,超过这个时间报文将被丢弃。
保证客户端发送的最后一个ACK报文段能够到达服务端
防止“已失效的连接请求报文段”出现在本连接中
理论上,四个报文都发送完毕,就可以直接进入CLOSE状态了,但是可能网络是不可靠的,有可能最后一个ACK丢失。所以TIME_WAIT状态就是用来重发可能丢失的ACK报文。
当你尝试使用Socket连接到一个未启动的服务器端口时,会发生连接失败的情况。这通常会导致一个错误码,通常是"Connection Refused"(连接被拒绝)
实现单次发送多条数据,极大的提高性能
当TCP开始启动的时候,慢启动阈值等于窗口最大值;
在每次超时重发的时候,慢启动阈值会变成原来的一半,同时拥塞窗口置回1;
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(超文本传输协议)是一种应用层通信协议,承载于TCP协议之上。它允许将超文本标记语言(HTML)文档从Web服务器传送到客户端的浏览器。
HTTPS协议是一种基于传输层安全协议(TLS)的安全协议,用于保护互联网通信安全。HTTPS协议基于传输层安全协议(TLS)协议,并将HTTP协议封装在TLS协议之上,以实现网络通信安全。具体来说,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层,其作用就是提供私密性,信息完整性和身份认证
相同:
GET 请求和 POST 请求底层都是基于 TCP/IP 协议实现的,使用二者中的任意一个,都可以实现客户端和服务器端的双向交互。
区别:
1、get主要用于获取数据(也可以修改),post主要用于修改数据
2、get将参数拼接到url上,长度也会被限制,post将参数写入到请求正文内,无参数大小限制。
3、get参数在url里明文传递,不安全
4、GET请求会被浏览器主动缓存,比如常见的CSS,JS,HTML请求都会被缓存,如果下次传输的数据相同,那么他们就会返回缓存中的内容,以求更快的展示所需要的数据。
1、数据存放位置不同:cookie数据存放在客户的浏览器上,session数据放在服务器上。
2、安全程度不同:cookie不是很安全,别人可以分析存放在本地的COOKIE并进行COOKIE欺骗,考虑到安全应当使用session。
3、性能使用程度不同:session会在一定时间内保存在服务器上。当访问增多,会比较占用你服务器的性能,考虑到减轻服务器性能方面,应当使用cookie。
4、数据存储大小不同:单个cookie保存的数据不能超过4K,很多浏览器都限制一个站点最多保存20个cookie,而session则存储与服务端,浏览器对其没有限制。
tcp和udp属于传输层协议
SDLC、HDLC、PPP、STP、帧中继等。
Nginx是七层负载均衡
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, 网络的处理能力决定如何分发请求. 根据后端服务器的响应时间来分配请求.
区别有:用途不同。安全性不同。目的不同。代理不同。服务对象不同。功能不同。
Go使用协程(goroutines)和通道(channels)来实现并发编程。
groutine能拥有强大的并发实现是通过GPM调度模型实现,下面就来解释下goroutine的调度模型。
每个用户线程对应多个内核空间线程,同时也可以一个内核空间线程对应多个用户空间线程。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的优势在于上下文切换在完全用户态进行,无需像线程一样频繁在用户态与内核态之间切换,节约了资源消耗。
是以runtime中实现的底层同步机制(cas、atomic、spinlock、sem)为基础的。
1 cas(Compare And Swap)和原子运算是其他同步机制的基础
2 自旋锁(spinlock)
3 信号量
协程(coroutine)是一种比线程更轻量级的并发执行单元,它的切换开销和内存栈的占用大小都比线程要小。
只要内存足够,在一个线程中可有上万个或更多的协程。
在网络编程应用中,采用协程后的业务代码比那些采用异步或事件回调方式的代码更好维护。
使用协程的业务逻辑代码表面看上去是同步执行的,编写这些代码时思维是连贯的,更符合人类的思维习惯;而采用异步和回调方式后,业务逻辑代码被分割(分拆)在多个回调方法中,开发人员的思维需要切换,是跳跃的,这样显然容易出错,并且一些场景下还要设计事件或会话上下文数据结构,来保存那些需要在多个回调方法间共享的状态数据。
从语言层面原生支持协程,在函数或者方法前面加 go关键字就可创建一个协程。
协程是在线程中执行的,在Golang中使用环境变量GOMAXPROCS来控制协程使用的线程数,它的缺省值是1,在1.5版本中改为cpu核数。也可在代码中调用
协程拥有自己的寄存器上下文和栈。协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈。因此,协程能保留上一次调用时的状态(即所有局部状态的一个特定组合),每次过程重入时,就相当于进入上一次调用的状态。这个过程完全由程序控制,不需要内核进行调度。
协程与线程的关系如下图所示:
资源控制和重用:通过使用协程池,可以控制并发任务的数量,避免资源被过度占用。协程池中预先创建的goroutine可以被重复使用,而不需要频繁地创建和销毁,从而降低了系统开销。
提高性能和吞吐量:协程池能够有效地管理并发任务的执行,通过合理调度和分配任务,可以最大限度地利用系统资源,提高并发性能和吞吐量。通过避免创建大量的goroutine和减少上下文切换,可以减少系统负载,提高处理能力。
控制并发度和资源限制:使用协程池可以限制并发任务的数量,确保系统资源不会被过度占用。可以根据系统的处理能力和资源限制,设置适当的协程池大小,避免资源耗尽和系统崩溃。
避免竞态条件:在多个goroutine并发执行时,如果它们之间共享某些资源,可能会出现竞态条件。使用协程池可以通过限制并发的数量,避免过多的goroutine同时访问共享资源,从而减少竞态条件的发生。
简化并发编程:使用协程池可以将任务的调度和管理逻辑与具体的任务逻辑分离开来,使得并发编程变得更加简单和直观。开发人员只需要关注具体的任务实现,而不需要手动管理goroutine的创建和销毁,以及任务的调度和排队。
变量存储的是一个地址,这个地址存储最终的值。内存通常在堆上分配,当没有任何变量引用这个地址时,该地址对应的数据空间就成为一个垃圾,通过GC回收。
变量直接存储的值,内存通常在栈中分配。
make用于内建类型(map、slice 和channel)的内存分配。new用于各种类型的内存分配
内建函数new本质上说跟其它语言中的同名函数功能一样:new(T)分配了零值填充的T类型的内存空间,并且返回其地址,即一个*T类型的值。用Go的术语说,它返回了一个指针,指向新分配的类型T的零值。
内建函数make(T, args)与new(T)有着不同的功能,make只能创建slice、map和channel,并且返回一个有初始值(非零)的T类型,而不是*T。本质来讲,导致这三个类型有所不同的原因是指向数据结构的引用在使用前必须被初始化。
例如,一个slice,是一个包含指向数据(内部array)的指针、长度和容量的三项描述符;在这些项目被初始化之前,slice为nil。对于slice、map和channel来说,make初始化了内部的数据结构,填充适当的值。
应该是10个0还有一个1,因为make会初始化10len为0,append的话是从后面追加
CSP(Communicating Sequential Processes,通信顺序进程)并发模型倡导使用通信的手段来进行共享内存,继而实现多个线程之间的通信。这也是 Golang 倡导使用的并发模型,通过 Channel 来使用。
CSP 有两个核心概念:
go channel 关闭后,读取该 channel 永远不会阻塞,且只会输出对应类型的零值。
nil 可能也是需要 channel传输的值之一,通常我们无法通过判断是否为类型的零值确定 channel 是否关闭。所以为了避免输出无意义的值,我们需要一种合理的方式判断 channel 是否关闭。golang 官方为我们提供了两种方式。
如果发生重复关闭、关闭后发送等问题,会造成 channel panic。那么如何优雅地关闭 channel,是我们关心的一个问题。
这里给出涉及到的比较好的文章:
https://blog.csdn.net/weixin_39725844/article/details/111200870
在MySQL数据库中,B树和B+树是常用的索引结构,用于加快数据库的查询速度。它们都是多叉树结构,但在一些方面有一些不同之处。
B树
B树是一种平衡的多叉树结构,其中每个节点可以拥有多个子节点。B树的特点包括:
B+树
B+树也是一种平衡的多叉树结构,与B树相似,但在一些方面有所不同。B+树的特点包括:
这里涉及到B树和B+树的区别,因为B树的每个节点都存储数据,并且B树的节点大小与磁盘页大小相同,每次读取磁盘页时可能会读取一些不需要的数据。
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最长回文子序列
给你一个字符串 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 最长连续序列
给定一个未排序的整数数组 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. 最长公共子序列
给定两个字符串 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
案例伪代码:
案例里对应的表格示意图:
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. 反转链表
给你单链表的头节点 head ,请你反转链表,并返回反转后的链表。
输入:head = [1,2,3,4,5]
输出:[5,4,3,2,1]
输入: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
给你单链表的头指针 head 和两个整数 left 和 right ,其中 left <= right 。请你反转从位置 left 到位置 right 的链表节点,返回 反转后的链表 。
输入: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