力扣刷题总结
前言
力扣刷题套路(剑指offer)总结。
第 1 天、 栈与队列(简单)
- 判断两个出栈的元素是否相等,要用
.equals()
方法。因为Java 代码中,由于 Stack 中存储的是 int 的包装类 Integer ,因此需要使用 equals() 代替 == 来比较值是否相等。 - 栈的两种定义方法:
- 直接使用Java自带的栈类:
Stack<Integer> stack = new Stack<>();
- 使用链表
LinkList<Interger> stack = new LinkedList<>();
模拟。为了不弄混还是用第一种方法好。
- 直接使用Java自带的栈类:
- 队列是先入先出,栈是先入后出。
- 队列与栈的常用方法:
- 队列:
方法名 | 说明 |
---|---|
offer()/add() | 入队。区别:一些队列有大小限制,因此如果想在一个满的队列中加入一个新项,多出的项就会被拒绝。这时新的 offer 方法就可以起作用了。它不是对调用 add() 方法抛出一个 unchecked 异常,而只是得到由 offer() 返回的 false。 |
poll()/remove() | remove() 和 poll() 方法都是从队列中删除第一个元素。remove() 的行为与 Collection 接口的版本相似, 但是新的 poll() 方法在用空集合调用时不是抛出异常,只是返回 null。因此新的方法更适合容易出现异常条件的情况。 |
peek()/element() | element() 和 peek() 用于在队列的头部查询元素。与 remove() 方法类似,在队列为空时, element() 抛出一个异常,而 peek() 返回 null。 |
- 栈:
方法名 | 说明 |
---|---|
push() | 压栈 |
pop() | 弹栈 |
empty() | 是否为空 |
peek() | 返回栈顶元素 |
题目详解与代码实现:力扣刷题day1
1.1 用两个栈实现队列
题目链接:用两个栈实现队列
要求:用两个栈实现一个队列。队列的声明如下,请实现它的两个函数 appendTail
和 deleteHead
,分别完成在队列尾部插入整数和在队列头部删除整数的功能。(若队列中没有元素,deleteHead
操作返回 -1 )
思路:一个栈负责入队,一个栈负责出队。出队的栈实现了对入队栈的反转。
- 当对前需要入队的元素执行
appendTail
(入队)时,把这些元素压入栈1。 - 当对当前队列第一个元素执行
deleteHead
(出队)时- 若栈2为空,则先把栈1的所有元素压入栈2,然后对栈2执行
pop
,利用栈2进行反转。 - 若栈2不为空,则直接对栈2执行
pop
。
- 若栈2为空,则先把栈1的所有元素压入栈2,然后对栈2执行
1.2 包含min函数的栈
题目链接:包含min函数的栈
要求:定义栈的数据结构,请在该类型中实现一个能够得到栈的最小元素的 min
函数在该栈中,调用 min
、push
、top
及 pop
的时间复杂度都是 。
思路:需求不难,难就难在如何让时间复杂度为。可以维护一个非严格最小的辅助栈2,保证栈2的顶端元素储存的是栈1每次push
后的当前最小值。典型的空间换时间思想。
push
函数的设计:
- 当栈1执行入栈的
push
操作时,判断栈2是否为空。- 如果栈2为空,则将这个值也
push
入栈2. - 如果栈2不为空,则将这个值和栈2顶端的值
peek
进行比较。- 如果这个值大于
peek
,则不执行任何操作。 - 如果这个值小于等于
peek
,则将这个值入栈2。
- 如果这个值大于
- 如果栈2为空,则将这个值也
pop
函数的设计:
- 当栈1执行
pop
操作时,判断这个值和栈2顶端的值peek
是否相同。- 如果相同,则栈1和栈2的顶端元素分别出栈。
- 如果不同,则栈1的顶端元素出栈。
min
函数的设计:
- 判断栈2是否为空。
- 若栈2为空,返回{}。
- 若栈2不为空,返回栈2的顶端元素
peek
。
top
函数的设计:
返回栈1的顶端元素。
第 2 天、 链表(简单)
题目详解与代码实现:力扣刷题day2
链表这块一般都是定义指针指向头结点,然后慢慢指针右移去遍历。
遍历复杂链表,本质上就是一个图了。可以使用递归+哈希表的方法进行遍历。
2.1 从尾到头打印链表
题目链接:从尾到头打印链表
要求:输入一个链表的头节点,从尾到头反过来返回每个节点的值(用数组返回),这里的头节点是有值的。
思路:使用栈来实现反序打印。
- 定义一个指针,指向头结点。
- 如果当前指针指向的节点不为
null
,则将这个节点压入栈中,指针右移。 - 重复步骤2,直到遍历所有的链表。
- 将栈中的元素依次
pop
出来,将值保存在数组中。 - 打印数组。
2.2 反转链表
题目链接:反转链表
要求:定义一个函数,输入一个链表的头节点,反转该链表并输出反转后链表的头节点。
思路:经典头插法。数据结构学习笔记 02 链表–腾讯面试题-单链表反转
-
现有一个链表:
flowchart LR Head-->A-->B-->C -
定义一个辅助的头结点
reverseHead
flowchart LR Head-->A-->B-->C reverseHead -
从头到尾遍历原来的链表,每遍历一个节点,就将其取出,放在辅助头结点所在的新链表的最前端。
flowchart LR Head-->B-->C reverseHead-->A
- 让head节点指向reverseHead链表中的第一个节点,并将reverseHead指向null
- 结果
2.3 复杂链表的复制
题目链接:复杂链表的复制
要求:请实现 copyRandomList 函数,复制一个复杂链表。在复杂链表中,每个节点除了有一个 next 指针指向下一个节点,还有一个 random 指针指向链表中的任意节点或者 null。
思路:使用哈希表存储已经遍历到的节点,使用递归进行遍历。哈希表中,键为节点,代表了节点的身份证,值也为节点,但对值的next
和random
做了赋值,使值之间相互连线。
- 引入当前结点。
- 如果当前节点为空
null
,return null
。这里是递归边界! - 如果当前节点在哈希表中的键集中找不到:
- 将当前节点和一个以当前节点的值创建的辅助节点填入哈希表中。
- 递归当前节点的
next
节点headNew.next = copyRandomList(head.next)
。 next
递归完后,递归当前节点的random
节点headNew.random = copyRandomList(head.random)
。
- 如果走到这,说明当前节点在哈希表的键集中找得到,直接返回这个节点键对应的值
return cachedNode.get(head)
- 例如对于上述复杂的链表,在执行完
next
递归后(带箭头的实线表示next):
- 在执行完
random
递归后(圆头的实线表示random
):
第 3 天、 字符串(简单)
题目详解与代码实现:力扣刷题记录 day3 字符串
字符串没啥说的,主要是调用一些常见的String
方法和StringBuilder
方法,charAt
也经常用到。
3.1 替换空格
题目链接:剑指 Offer 05. 替换空格
要求:请实现一个函数,把字符串 s
中的每个空格替换成"%20"。
最好思路:最简单的方法,直接调用String的replace("target","modified")
方法即可。
其他思路:
- 用
charAt
遍历,如果相等就替换。(字符char是可以直接使用"=="来进行比较的,底层就是数字。) - **双指针法,极致解法。**如果不让调库函数最优解是双指针。
- 先扩充String空间
- 定义左指针指向原始字符串的最后一个位置和右指针指向扩充空间后的最后一个位置。
- 左右指针向左遍历。如果左指针指向了空格,那右指针分别填入"0",“2”,"%"并右移。
- 如果左指针没指向空格,就把左指针的值赋值给右指针。
3.2 左旋转字符串
要求: 字符串的左旋转操作是把字符串前面的若干个字符转移到字符串的尾部。请定义一个函数实现字符串左旋转操作的功能。比如,输入字符串"abcdefg"和数字2,该函数将返回左旋转两位得到的结果"cdefgab"。
最好思路:最简单的方法,拼接子字符串subString(目标索引,总字符串长度)+subString(0,目标索引)
。一行搞定。
其他思路:利用charAt
对字符串往一个StringBuilder
里切片,最后输出这个StringBuilder.toString
。
第 4 天、 查找算法(简单)
题目详解与代码实现:力扣刷题记录 day 4 查找算法(简单)
Java中的字典 Dictionary
类已经过时了,想要实现用哈希表就行了。
主要的就是考察的二分法。
如果是排好序的数组,二分法很好用!(二分法可以用递归或者循环来写,最好是循环。)
4.1 数组中的重复数字
要求:找出数组中重复的数字。
在一个长度为 n 的数组 nums 里的所有数字都在 0~n-1 的范围内。数组中某些数字是重复的,但不知道有几个数字重复了,也不知道每个数字重复了几次。请找出数组中任意一个重复的数字。
最好思路:利用哈希表(这里只需要判断数字是否相同,不需要判断别的,用hashset
)储存数字,如果有重复的就return
。
其他思路:
- 暴力遍历:不太行,时间复杂度高。
- 利用
hashmap
,更麻烦一点,时间复杂度也更高,可以但没必要。 - 原地交换位置
4.2 在排序数组中查找数字 I
题目链接:剑指 Offer 53 - I. 在排序数组中查找数字 I
要求:统计一个数字在排序数组中出现的次数。
最好思路:二分法。
start
是0,end
是length-1
,mid
为二者中间。- 比较
mid
和要找的数字的值。- 如果
mid
比要找的数字大,说明要找的数字在mid
的前面,即end = mid - 1
- 如果
mid
比要找的数字小,说明要找的数字在mid
的后面,即start = mid + 1
- 如果
mid
和要找的数字相等,向前和向后统计和该数字相同的数字出现的频数。
- 如果
- 当
start<=end
时结束循环,因为比较的是[start,end]
这样的闭区间。 - 返回频数。
4.3 0~n-1中缺失的数字
题目链接:剑指 Offer 53 - II. 0~n-1中缺失的数字
要求:一个长度为n-1的递增排序数组中的所有数字都是唯一的,并且每个数字都在范围0~n-1之内。在范围0~n-1内的n个数字中有且只有一个数字不在该数组中,请找出这个数字。
最好思路:二分法。缺失的数字将数组分为左子数组和右子数组,只需要查找右子数组的第一个元素即可。
start
是0,end
是length-1
,mid
为二者中间。- 比较
mid
和mid
索引处应该出现的值nums[mid]
。- 如果
mid
等于nums[mid]
,说明还没缺呢,缺失的数字在mid
的后面,即start = mid + 1
- 不存在
mid
大于num[mid]
的情况,不用考虑。 - 如果
mid
小于num[mid]
,说明已经缺了,缺失的数字在mid
的前面,即end = mid - 1
- 如果
- 当
start<=end
时结束循环,因为比较的是[start,end]
这样的闭区间。注意结束循环的时候,start
指针和end
指针是在重合之后的下一次循环结束的。也就是要么end
左移了,要么start
右移了。无论是哪种情况,start
指针始终是在end
指针的后一位。 start
指针处本来应该出现的值,就是缺失的值。
个人理解:
- 如果
mid
指针落入左边的子组,由于没有缺失,mid
指针会因为向后二分查找而向右靠。 - 如果
mid
指针落入右边的子组,由于缺失了数字,mid
指针会因为向前二分查找而向前靠。 - 最终,
mid
指针一定会停在左子组和右子组的交界处。 - 这里真正要找的值即不在左子组,也不再右子组,是一个缺失的值。因此可以用
start = mid + 1
和end = mid - 1
。
其他思路:也是二分法,当mid
指针落入右边子组时,看一下mid
指针前面的元素正不正常,不正常就返回。这个思路不好。
第 5 天、查找算法(中等)
题目详解与代码实现:力扣刷题记录 day5 查找方法 - 2
二分法进阶需要注意的点:
- 循环条件是
start<=end
还是start<end
?这个和end
索引是否是闭区间有关。个人喜欢start<=end
。 - 当
start
指针和end
指针重合时,如果使用start<=end
,仍然会进入一次循环,此时应该注意返回值的变化。 - 到底是
right = middle
呢,还是要right = middle - 1
呢?这个也和end
索引是否是闭区间有关。同时,也要注意观察是否真的当前节点middle
是不符合条件的节点,随机应变。
5.1 二维数组中的查找
要求:在一个 n * m 的二维数组中,每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺序排序。请完成一个高效的函数,输入这样的一个二维数组和一个整数,判断数组中是否含有该整数。
最好思路:从矩阵的右上角来看,把这个矩阵看成是一个排序二叉树。这个值的下面的都是比这个值大的,这个值左边都是比这个值小的。
- 考虑特殊情况矩阵为空矩阵
{{}}
或者{}
时,直接返回false
。 - 将矩阵的右上角元素看成是根节点。
- 在矩阵的索引范围内,比较要找的值和这个根节点的值
- 如果要找的值比这个根节点的值大,检查这个根节点的下面的节点
- 如果要找的值比这个根节点的值小,检查这个根节点的左边的值。
- 如果正好相等,直接
return true
- 如果走到这里,那就是找遍了也没找到,直接返回
false
。
其他思路:暴力。
5.2 旋转数组的最小数字
要求:把一个数组最开始的若干个元素搬到数组的末尾,我们称之为数组的旋转。
给你一个可能存在 重复 元素值的数组 numbers ,它原来是一个升序排列的数组,并按上述情形进行了一次旋转。请返回旋转数组的最小元素。例如,数组 [3,4,5,1,2] 为 [1,2,3,4,5] 的一次旋转,该数组的最小值为1。
最好思路:二分查找,和4.3题类似。不同的地方在于,这个数组是可以有相同的元素的,因此相比于4.3题,稍微复杂。可以把旋转后的数组近似的看成一个反比例函数。左子组都是逐渐上升的,右子组都是逐渐下降的。
start
是0,end
是length-1
,mid
为二者中间。想要找的元素为右子组的第一个元素。- 比较
nums[mid]
和nums[end]
。- 如果
nums[mid]
大于nums[end]
,说明nums[mid]
在y轴左边,也就是左子组。因此,要找的点肯定在nums[mid]
的右边。因为nums[mid]
肯定在左子组,一定不是右子组的第一个元素,因此包括nums[mid]
在内的nums[mid]
左边的所有的元素都可以忽略了。start = mid + 1;
- 如果
nums[mid]
小于nums[end]
,说明nums[mid]
在y轴右边,也就是右子组。此时,要找的元素可能就是nums[mid]
,也可能在nums[mid]
之前。因此nums[mid]
左边的所有的元素都可以忽略,而nums[mid]
本身还不能忽略。end = mid;
- 如果
nums[mid]
等于nums[end]
,此时循环还在继续,证明start
指针和end
指针还未重合,二者之间还有一定距离。此时由于数组含有重复元素,不好说这两个指针中间的mid
是不是指向的最小的,没准还有还有更小的。为了让循环继续,就武断的将end
指针向左移,继续循环过程。nums[end]
和nums[mid]
值相同,而end
一定在mid
的右侧,和mid
相比,end
一定不是右子组的第一个元素。所以这样做无可厚非。
- 如果
- 当
start=end
时,指针重合处一定是右子组的第一个元素。当start``mid``end
三个指针重合后,仍然会进行一次循环,此时nums[mid]
等于nums[end]
,end
指针会再左移一次。所以end
指针最终循环结束后是不准的。 - 所以最终应该返回
start
指针指向的元素,而不是end
。
5.3 第一个只出现一次的字符
要求:在字符串 s 中找出第一个只出现一次的字符。如果没有,返回一个单空格。 s 只包含小写字母。
最好思路:出现“重复”这种词,第一反应就是用哈希表来做。
- 考虑特殊情况,如果输入的字符串是"",返回一个空字符串。
- 如果输入的字符串不是"",使用charAt遍历字符串:
- 如果在哈希表键集中没有这个字符,就把这个字符存在哈希表的键中,值设置为0。
- 如果哈希下表键集中有这个字符,就把哈希表中这个字符的值设置为1。
- 使用charAt再次遍历这个字符串,如果哈希表中这个字符的键对应的值为0,则返回。否则继续执行。
- 遍历整个字符串后还没找到值是0的键,返回空的字符串。
第 6 天、搜索与回溯算法(简单)
队列使用LinkedList
来定义:Queue<TreeNode> nodeQueue = new LinkedList<>();
注意: 层序遍历是按层来,一层一层的遍历节点。
而:
DLR–前序遍历(根在前,从左往右,一棵树的根永远在左子树前面,左子树又永远在右子树前面 )
LDR–中序遍历(根在中,从左往右,一棵树的左子树永远在根前面,根永远在右子树前面)
LRD–后序遍历(根在后,从左往右,一棵树的左子树永远在右子树前面,右子树永远在根前面)
实现前、中、后序遍历是用递归好一点,实现层序遍历理论基础还是BFS,还是队列好一点。
题目详情与代码实现:搜索与回溯算法(简单)
6.1 从上到下打印二叉树(I)
要求:从上到下打印出二叉树的每个节点,同一层的节点按照从左到右的顺序打印。
最好思路:前序遍历的话就是BFS,BFS一般用队列来实现。
- 将根节点入队。
- 循环,循环结束的条件为队列为空。根节点出队,查找根节点的左子节点和右子节点
- 如果左子节点和右子节点存在,将左子节点和右子节点入队。
- 如果左子节点和右子节点不存在,抵达退出循环的边界条件,返回null。
- 使用一个列表来接收出队的值。
6.2 从上到下打印二叉树(II)
题目链接:剑指 Offer 32 - II. 从上到下打印二叉树 II
要求:从上到下按层打印二叉树,同一层的节点按从左到右的顺序打印,每一层打印到一行。和上题类似,但是上题是打印到一个列表里(例如:{1, 2, 3, 4, 5}
)。但是这道题是要在列表里实现分层,例如:{{1}, {2, 3}, {4, 5}
。
最好思路:和上题类似,最精髓的地方是使用queue.size()
来控制层数。
- 将根节点入队。
- 循环,循环结束的条件为队列为空。
- 新定义一个内层的tempList,用来接收每一次内层循环打印的节点。
- 定义一个内层循环,循环的次数为
queue.size()
,也就是当前层数下offer
进队列的所有节点。循环的写法为:for (int i = nodeQueue.size(); i > 0; i--){}
。巧妙的地方在于直接将nodeQueue.size()
赋值给i
将这个固定值保留了。不能写成for(int i = 0; i < nodeQueue.size();i++){}
,这样写nodeQueue.size()
的值就是时刻变化的。- 节点出队,查找节点的左子节点和右子节点
- 如果左子节点和右子节点存在,将左子节点和右子节点入队。
- 如果左子节点和右子节点不存在,抵达退出循环的边界条件,返回null。
- 完成一次内层循环后,用tempList接收这些节点。
- 使用FinalList来接受tempList这个列表。
return
FinalList。
6.3 从上到下打印二叉树(III)
题目链接:剑指 Offer 32 - III. 从上到下打印二叉树 III
要求:请实现一个函数按照之字形顺序打印二叉树,即第一行按照从左到右的顺序打印,第二层按照从右到左的顺序打印,第三行再按照从左到右的顺序打印,其他行以此类推。
最好思路:和上题类似,仍然定义一个内层循环,来控制层数。不同的地方就是定义一个count
用来判定是奇数层数还是偶数层数。奇数层数就使用Collection.reverse
翻转tempList,实现反向打印。
第 7 天、搜索与回溯算法(简单)
递归:主要是要定义好递归出口,每次递归的逻辑,递归函数返回值及其参数这三个要素。
题目详情与代码实现:力扣刷题记录 day7 搜索与回溯算法(简单)
7.1 树的子结构
题目链接:剑指 Offer 26. 树的子结构
要求:输入两棵二叉树A和B,判断B是不是A的子结构。(约定空树不是任意一个树的子结构)。B是A的子结构, 即 A中有出现和B相同的结构和节点值。
最好思路:递归嵌套递归解法:由于没有要求逐层打印,就不需要BFS,也不需要队列了,直接使用递归嵌套递归前序遍历即可。
recur(A, B) 函数
:
- 终止条件:
- 当节点 B 为空:说明树 B 已匹配完成(越过叶子节点),因此返回
true
; - 当节点 A 为空:说明已经越过树 A 叶子节点,即匹配失败,返回
false
; - 当节点 A 和 B 的值不同:说明匹配失败,返回
false
;
- 当节点 B 为空:说明树 B 已匹配完成(越过叶子节点),因此返回
- 返回值:
- 判断 A 和 B 的左子节点是否相等,即
recur(A.left, B.left)
; - 判断 A 和 B 的右子节点是否相等,即
recur(A.right, B.right)
;
- 判断 A 和 B 的左子节点是否相等,即
isSubStructure(A, B) 函数
:
- 特例处理: 当 树 A 为空 或 树 B 为空 时,直接返回
false
; - 返回值: 若树 B 是树 A 的子结构,则必满足以下三种情况之一,因此用或
||
连接;- 以 节点 A 为根节点的子树 包含树 B ,对应
recur(A, B)
; - 树 B 是 树 A 左子树 的子结构,对应
isSubStructure(A.left, B)
; - 树 B是 树 A 右子树 的子结构,对应
isSubStructure(A.right, B)
;
- 以 节点 A 为根节点的子树 包含树 B ,对应
代码赏析:
1 | public static boolean isSubStructure(TreeNode A, TreeNode B) { |
其他思路:用队列层序遍历,时间复杂度很高。
7.2 二叉树的镜像
题目链接:[剑指 Offer 27. 二叉树的镜像](https://leetcode-cn.com/problems/shu-de-zi-jie-gou-lcof/)
要求:请完成一个函数,输入一个二叉树,该函数输出它的镜像。
最好思路 :还是递归。
- 现有一二叉树如下所示
- 保存根节点4的左子节点2,递归其右子节点7,并准备赋值给左子节点。
- 保存节点7的左子节点6,递归其右子节点9,并准备赋值给左子节点。
- 保存节点9的左子节点null,递归其右子节点null,并准备赋值给左子节点。
- 已经遍历到叶子节点之下了,抵达递归出口,返回null,开始遍历之前储存的左子节点,也返回null。回到节点9。
- 此时,将节点9赋值给其父节点的左子节点,并开始遍历其父节点的右子节点(之前储存的节点6)
- 节点6的遍历过程和上述相同。就这样一直递归,直到整个树完成镜像翻转。
7.3 对称的二叉树
题目链接:[剑指 Offer 28. 对称的二叉树](https://leetcode-cn.com/problems/shu-de-zi-jie-gou-lcof/)
要求:请实现一个函数,用来判断一棵二叉树是不是对称的。如果一棵二叉树和它的镜像一样,那么它是对称的。
最好思路 :还是递归。
isSymmetric(root)
:
-
特例处理: 若根节点 root 为空,则直接返回
true
。 -
返回值: 即
recur(root.left, root.right)
;
recur(L, R)
:
终止条件:
-
当 L 和 R 同时越过叶节点: 此树从顶至底的节点都对称,因此返回
true
; -
当 L 或 R 中只有一个越过叶节点: 此树不对称,因此返回
false
; -
当节点 L 值不等于节点 R 值: 此树不对称,因此返回
false
;
递推工作:
-
判断两节点
L.left
和R.right
是否对称,即recur(L.left, R.right)
; -
判断两节点
L.right
和R.left
是否对称,即recur(L.right, R.left)
; -
返回值: 两对节点都对称时,才是对称树,因此用与逻辑符
&&
连接。 -
本质上,可以看成是对一棵树进行“前左右”和“前右左”两次严格与非严格的前序遍历。
其他思路:先镜像,再逐点比较。