目录
前言
本章博客用于持续记录 LeetCode 网站做题思路,不定期更新。内容主要包括栈、链表和队列。另外也包括一些经典如汉诺塔问题,还有字符串、求和之类的题目。
链表
环形链表
题目:给定一个链表,判断链表中是否有环。
如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。
思路一:哈希表
建立一个哈希表,依次遍历整个链表,并将其加入哈希表中。若遍历过程中某元素的相同元素个数大于1,即出现环。
若遍历过程中出现空节点,则无环。
思路二:快慢指针
链表有环的一个特性:快指针最终会追上慢指针。
若最终快指针指向空节点,则无环。
平凡解:头节点指针为空或头节点的next指针为空
环形链表 II
题目:给定一个链表,返回链表开始入环的第一个节点。 如果链表无环,则返回 null。
说明:不允许修改给定的链表。
思路一:哈希表
遍历链表,判断该节点是否在哈希表内:在则返回该节点,否则继续遍历。若最后访问至空指针,则返回 nullptr 。
思路二:快慢指针(fast, low)
注意:快慢指针都从头部开始移动
假设 fast 绕环n圈后和 low 相遇
- d ( f a s t ) = a + n ( b + c ) + b = a + ( n + 1 ) b + n c d(fast) = a+n(b+c)+b = a+(n+1)b+nc d(fast)=a+n(b+c)+b=a+(n+1)b+nc
- d ( l o w ) = a + b d(low) = a + b d(low)=a+b
任何时刻:
d
(
f
a
s
t
)
=
2
∗
d
(
l
o
w
)
d(fast) = 2 * d(low)
d(fast)=2∗d(low)
得出关系式:
a
=
c
+
(
n
−
1
)
∗
(
b
+
c
)
a = c + (n-1) * (b + c)
a=c+(n−1)∗(b+c)
即从相遇点 到 入环点 的距离等于 头部 到 入环点
因此找到入环节点只需在头部设置结果指针res。slow 向后移动 直到和cur 相遇,返回该相遇节点。
两两交换链表中的节点
题目:给定一个链表,两两交换其中相邻的节点,并返回交换后的链表。
注意:你不能只是单纯的改变节点内部的值,而是需要实际的进行节点交换。
思路:虚拟头节点
使用虚拟头节点统一所有操作。用临时节点 t m p tmp tmp 指向虚拟头节点,交换节点为临时节点后两个节点。
- 当待交换节点不足两个,停止操作。
- 交换操作:
- 设置前后指针
f
n
t
,
b
a
c
k
fnt, back
fnt,back 指向待交换节点
- t m p . n e x t = b a c k tmp.next = back tmp.next=back
- f n t . n e x t = b a c k . n e x t fnt.next = back.next fnt.next=back.next
- b a c k . n e x t = f n t back.next = fnt back.next=fnt
- t m p tmp tmp 指针后移两个节点,循环上述交换操作
链表倒数第k节点
题目:输入一个链表,输出该链表中倒数第k个节点。
思路一:
- 递归函数外设置计数器,递归函数内设置指针用于保存应该返回的节点
- 向下递归到尾节点
- 每次回退使计数器加一并返回当前节点
- 当计数器等于k时,用指针记录当前节点并返回
- 往后每次回退都接收结果指针,直到最后返回该结果
思路二:
使用双指针,让快指针先走k步,然后快慢指针同时遍历,当快指针遍历完成时,慢指针指向的位置就是倒数第k个节点。
删除链表倒数第k个节点
题目:给定一个链表,删除链表的倒数第 n 个节点,并且返回链表的头结点。
思路:添加虚拟头节点,方便将表头和其他节点元素的删除操作统一起来。此后只需要寻找链表的倒数第 k + 1 k+1 k+1个节点,然后即可删除第 k k k 个节点。寻找方式可参考上一题。
链表相交
题目:找到两个单链表相交的起始节点
思路:
使用两个指针node1,node2分别从两个链表头开始遍历链表,若任何一个指针指向空,则从另外一个链表头开始遍历,直到指针相同。
这个思路妙在两个指针最终必然会相同(走过的距离均等于两个链表长度和)。
反转链表
题目:定义一个函数,输入一个链表的头节点,反转该链表并输出反转后链表的头节点。
思路一:递归(分治思想)
递归基:该节点为空或者下一个节点为空(不需要转置)返回该节点
递归操作:
- 将返回值设置为反转链表的头节点( t a i l = R e v e r s e ( h e a d − > n e x t ) tail = Reverse(head->next) tail=Reverse(head−>next)),即整个链表的尾节点
- 反转操作: h e a d − > n e x t − > n e x t = h e a d head->next->next = head head−>next−>next=head, h e a d − > n e x t = n u l l p t r head->next = nullptr head−>next=nullptr。注意将头节点的下一个节点指针置空,防止循环。
- 返回 “头节点”
思路二:双指针
初始化:指针 c u r cur cur,指针 p r e pre pre 指向头节点
循环操作:当 p r e pre pre 不为空
- 先保存 p r e pre pre 的前一个节点
- 令 p r e − > n e x t = c u r pre->next = cur pre−>next=cur
- p r e , c u r pre,cur pre,cur 都沿着链表后移一个节点
反转链表 II
题目:反转从位置 m 到 n 的链表。请使用一趟扫描完成反转。
说明:
1 ≤ m ≤ n ≤ 链表长度
思路:
回文链表
题目:判断一个链表是否为回文链表。
思路一:
类似括号匹配,使用栈的思想,将链表前半段转化为栈。若当前节点的值或者下一个节点的值和栈顶元素相同,则开始匹配。若成功配对则出栈。最后栈空则是回文链表。
注意在配对过程未结束而栈已空则直接返回false。
思路二:
建立一个新的反转的链表,然后逐节点进行匹配。时间和空间复杂均为O(n)
思路三:
若企图将空间复杂度降至O(1),则只需要将链表后半段进行反转。
确定中点的方法可以使用快慢指针。
两数相加
题目:给出两个非空的链表用来表示两个非负的整数。其中,它们各自的位数是按照逆序的方式存储的,并且它们的每个节点只能存储 一位 数字。将这两个数相加起来,返回一个新的链表来表示它们的和。
思路:
从头到尾扫描记录该位下的值和进位。注意要分类讨论:两个链表相加,只剩一个链表。注意判断最高位是否有进位。
注:官方题解中,可认为长度短的链表的后面有若干个0,即不用分类。
栈
用两个栈实现队列
题目:实现它的两个函数 appendTail 和 deleteHead ,分别完成在队列尾部插入整数和在队列头部删除整数的功能。(若队列中没有元素,deleteHead 操作返回 -1 )
思路:
确定插入栈和弹出栈。
插入栈:插入操作直接push入栈顶
弹出栈:弹出操作需要分类讨论:
- 弹出栈非空,直接弹出栈顶元素
- 若插入栈非空,倒入弹出栈中,再弹出栈顶元素
- 否则返回-1
时间复杂度分析:删除操作,每个元素只会至多被插入和弹出 stack2 一次,因此均摊下来每个元素被删除的时间复杂度仍为 O(1)。
包含min函数的栈、栈的最小值
题目:定义栈的数据结构,请在该类型中实现一个能够得到栈的最小元素的 min 函数在该栈中,调用 min、push 及 pop 的时间复杂度都是 O(1)。
思路一:
遍历法,每次寻找min值都遍历整个栈,这个栈用数组实现。
时间复杂度为O(n)。
思路二:
使用辅助栈,在每次入栈前检查是否小于等于最小的元素,若是则入栈。
以上操作可以得到一个逆序栈,每次查询栈内最小值只需要返回逆序栈顶即可。
每次出栈需要判断是否弹出的是最小值,以此判断是否需要弹出逆序栈顶元素。
栈排序
题目:对栈进行排序使最小元素位于栈顶。最多只能使用一个其他的临时栈存放数据,但不得将元素复制到别的数据结构(如数组)中。
思路:
类似插入排序,每当入栈元素大于栈顶元素时,则弹出当前栈顶元素到辅助栈中。当入栈元素按顺序入栈后,再将辅助栈中的元素弹回栈中。时间复杂度为O(n)。
栈混洗/栈序列
题目:输入两个整数序列,第一个序列表示栈的压入顺序,请判断第二个序列是否为该栈的弹出顺序。假设压入栈的所有数字均不相等。
思路:
用栈进行模拟pop操作。输入序列依次入栈,并根据输出序列判断栈顶元素是否弹出,最后判断栈是否为空。
用队列实现栈
题目:使用队列实现栈的下列操作:
push(x) – 元素 x 入栈
pop() – 移除栈顶元素
top() – 获取栈顶元素
empty() – 返回栈是否为空
思路一:
使用双队列:q1,q2
push(x):直接在q1入队
pop():若q1非空,则只留下最后一个,其他元素按顺序入队q2。若q2非空,同样只留下最后一个,其余元素按顺序入队q1。注意记录下该值,弹出后再返回结果值。
top():直接返回队列的尾元素
empty():若q1和q2都空则返回true
上述思路优势在于不用两个队列之间反复倒,算法效率较高,但是并未完全按照栈的特性进行元素的排列,即应满足队列前端的元素为栈顶元素。
思路二:
按照上述分析,只用队列q1按照栈的次序保存元素。因此只需在每次入“栈”时,元素入队q2。再将q1中的所有元素依次放入q2中。最后将q2中的所有元素重新放回q1中。(最后一步可以省略,即不严格区分辅助栈,使用判断语句辅助实现即可)。
注:leetcode官网解法最后一步是swap两个栈,但题目中并未允许使用这种操作。
该算法的缺点是每次入栈操作次数为3n+2,相比于思路一仅出栈操作次数2n-1,效率较低。但是符合栈元素排列的规范,思路比较容易接受。
思路三:
使用一个队列。每次第n+1个元素“入栈”时,将队列前n个元素按顺序重新出队再入队。
入栈操作次数为2n+1
队列
斐波那契数
题目:写一个函数,输入 n ,求斐波那契(Fibonacci)数列的第 n 项。斐波那契数列的定义如下:
F(0) = 0, F(1) = 1
F(N) = F(N - 1) + F(N - 2), 其中 N > 1.
思路:
使用队列解决,当n > 1时,记录队首元素并出队,再和队首元素相加并入队。且该记录值为结果值。
队列的最大值
题目:请定义一个队列并实现函数 max_value 得到队列里的最大值,要求函数max_value、push_back 和 pop_front 的均摊时间复杂度都是O(1)。
若队列为空,pop_front 和 max_value 需要返回 -1
思路:
类似栈的最小值这道题目。
设计循环双端队列
题目:设计实现双端队列。
思路:使用数组实现。额外使用一个空间来避免判空和判满条件重合。
规定头指针指向头元素,尾指针指向尾元素的后一个位置(因此额外使用空间)。
头插入:头指针先移动,再放置元素。
尾插入:尾指针先放置元素,再移动
判空:头尾指针位置相同
判满:尾指针的下一个位置是头指针
注意:
- 插入和删除都需要先判断数组是否可操作(是否满或空)。
- 实现循环队列注意取模,特别是头部插入元素、获取尾元素和删除尾元素需要注意负数取模问题。
经典问题
汉诺塔
题目:在经典汉诺塔问题中,有 3 根柱子及 N 个不同大小的穿孔圆盘,盘子可以滑入任意一根柱子。一开始,所有盘子自上而下按升序依次套在第一根柱子上(即每一个盘子只能放在更大的盘子上面)。移动圆盘时受到以下限制:
(1) 每次只能移动一个盘子;
(2) 盘子只能从柱子顶端滑出移到下一根柱子;
(3) 盘子只能叠在比它大的盘子上
思路:
分治递归法。
- 递归基:当只有一个盘子,从A移动到C
- 借助C将n-1个规模得盘子移动到B
- 将A中得盘子移动到C
- 借助A将B中的 n-1个盘子移动到C
其他
四数之和
题目:给定一个包含 n 个整数的数组 nums 和一个目标值 target,判断 nums 中是否存在四个元素 a,b,c 和 d ,使得 a + b + c + d 的值与 target 相等?找出所有满足条件且不重复的四元组。
注意:
答案中不可以包含重复的四元组。
思路:双指针法
- 首先按照升序排序。
- 双重循环枚举前两个数,下标分别为 i, j。将左指针 lp 设为 j + 1,右指针设为数组的最后一个元素,即 size - 1。每次计算四个数的和:
- 如果大于目标值,则右指针左移
- 小于目标值,左指针右移
- 如果等于,将该序列加入结果数组。且将左指针右移,右指针左移至不同的数
- 左指针小于右指针则循环
颜色分类
题目:给定一个包含红色、白色和蓝色,一共 n 个元素的数组,原地对它们进行排序,使得相同颜色的元素相邻,并按照红色、白色、蓝色顺序排列。
此题中,我们使用整数 0、 1 和 2 分别表示红色、白色和蓝色。
注意:
不能使用代码库中的排序函数来解决这道题。
思路一:暴力求解
遍历数组分别得到三种颜色的元素数量,再按照顺序放回数组即可。
思路二:双指针(红指针p1在头,蓝指针p2在尾)
遍历数组,当遍历位置大于p2时停止。
- 找到0则和红指针指向的元素交换,p1右移。
- 找到2则和蓝指针指向的元素交换,p2左移。
注意找到2时要确保交换过来的元素不能为2。
字符串的排列
题目:输入一个字符串,打印出该字符串中字符的所有排列。你可以以任意顺序返回这个字符串数组,但里面不能有重复元素。
思路:回溯法(交换分割)
递归基:当递推深度等于字符串大小时,将该字符串记录,停止递推并返回。
从当前深度开始遍历所有可选的字符:
- 选择一个字符和当前字符交换,即固定当前位置一个字符,分割出其余仍可选择的字符。
- 递推至下一个深度
- 撤销选择:将选择的字符再次交换
和为s的连续子序列
题目:输入一个正整数 target ,输出所有和为 target 的连续正整数序列(至少含有两个数)。
序列内的数字由小到大排列,不同序列按照首个数字从小到大排列。
思路:使用双指针(左指针p1,右指针p2)
连续区间整数和数学公式: s u m = ( p 1 + p 2 ) ∗ ( p 2 − p 1 + 1 ) 2 sum = \frac{(p1 + p2) * (p2 - p1 + 1)}{2} sum=2(p1+p2)∗(p2−p1+1)
从最小区间开始搜索,即p1 = 1, p2 = 2。并且比较 sum 和 target
- 等于,将该序列记录,左指针右移
- 小于,右指针右移
- 大于,左指针右移
时间复杂度O(target)