- 知识点
- 热题 HOT 100
- 简单
- 1. 两数之和">1. 两数之和
- 136. 只出现一次的数字">136. 只出现一次的数字
- 21. 合并两个有序链表">21. 合并两个有序链表
- 206. 反转链表">206. 反转链表
- 461. 汉明距离">461. 汉明距离
- 160. 相交链表">160. 相交链表
- 69. 多数元素">69. 多数元素
- 448. 找到所有数组中消失的数字">448. 找到所有数组中消失的数字
- 234. 回文链表">234. 回文链表
- 617. 合并二叉树">617. 合并二叉树
- 226. 翻转二叉树">226. 翻转二叉树
- 104. 二叉树的最大深度">104. 二叉树的最大深度
- 283. 移动零">283. 移动零
- 101. 对称二叉树">101. 对称二叉树
- 155. 最小栈">155. 最小栈
- 121. 买卖股票的最佳时机">121. 买卖股票的最佳时机
- 53. 最大子序和">53. 最大子序和
- 543. 二叉树的直径">543. 二叉树的直径
- 70. 爬楼梯">70. 爬楼梯
- 141. 环形链表">141. 环形链表
- 20. 有效的括号">20. 有效的括号
- 700. 二叉搜索树中的搜索">700. 二叉搜索树中的搜索
- 783. 二叉搜索树节点最小距离">783. 二叉搜索树节点最小距离
- 530. 二叉搜索树的最小绝对差">530. 二叉搜索树的最小绝对差
- 501. 二叉搜索树中的众数">501. 二叉搜索树中的众数
- 938. 二叉搜索树的范围和">938. 二叉搜索树的范围和
- 中等
- 15. 三数之和">15. 三数之和
- 338. 比特位计数">338. 比特位计数
- 46. 全排列">46. 全排列
- 94. 二叉树的中序遍历">94. 二叉树的中序遍历
- 102. 二叉树的层序遍历">102. 二叉树的层序遍历
- 105. 从前序与中序遍历序列构造二叉树">105. 从前序与中序遍历序列构造二叉树
- 106. 从中序与后序遍历序列构造二叉树">106. 从中序与后序遍历序列构造二叉树
- 19. 删除链表的倒数第 N 个结点">19. 删除链表的倒数第 N 个结点
- 114. 二叉树展开为链表">114. 二叉树展开为链表
- 124. 二叉树中的最大路径和">124. 二叉树中的最大路径和
- 142. 环形链表 II">142. 环形链表 II
- 647. 回文子串">647. 回文子串
- 5. 最长回文子串">5. 最长回文子串
- 1669. 合并两个链表">1669. 合并两个链表
- 143. 重排链表">143. 重排链表
- 1143. 最长公共子序列">1143. 最长公共子序列
- 322. 零钱兑换">322. 零钱兑换
- 300. 最长递增子序列">300. 最长递增子序列
- 437. 路径总和 III">437. 路径总和 III
- 62. 不同路径">62. 不同路径
- 64. 最小路径和">64. 最小路径和
- 116. 填充每个节点的下一个右侧节点指针">116. 填充每个节点的下一个右侧节点指针
- 654. 最大二叉树">654. 最大二叉树
- 652. 寻找重复的子树">652. 寻找重复的子树
- 297. 二叉树的序列化与反序列化">297. 二叉树的序列化与反序列化
- 230. 二叉搜索树中第K小的元素">230. 二叉搜索树中第K小的元素
- 538. 把二叉搜索树转换为累加树">538. 把二叉搜索树转换为累加树
- 1038. 把二叉搜索树转换为累加树">1038. 把二叉搜索树转换为累加树
- 98. 验证二叉搜索树">98. 验证二叉搜索树
- 701. 二叉搜索树中的插入操作">701. 二叉搜索树中的插入操作
- 450. 删除二叉搜索树中的节点">450. 删除二叉搜索树中的节点
- 96. 不同的二叉搜索树">96. 不同的二叉搜索树
- 95. 不同的二叉搜索树 II">95. 不同的二叉搜索树 II
- 148. 排序链表">148. 排序链表
- 困难
- 简单
- 系列问题
- 一、买卖股票
- 二、双指针技巧(快慢指针、左右子针、滑动窗口)
- 2Sum 3Sum 4Sum 问题(快慢双指针)
- 剑指 Offer 57. 和为s的两个数字)">两数之和I(剑指 Offer 57. 和为s的两个数字)
- 两数之和ii
- 15. 三数之和">15. 三数之和
- 18. 四数之和">18. 四数之和
- 1两数之和(补充的:这里是返回角标,不是返回数值,所以解法与上面是不同的)">1两数之和(补充的:这里是返回角标,不是返回数值,所以解法与上面是不同的)
- 三、滑动窗口算法(双指针的高级应用部分)
- 四、BFS算法框架
- 五、区间相关问题
- 六、单调栈
- 七、随机读取元素相关
知识点
1.关于取模运算:
(a + b) % p = (a % p + b % p) % p
(a - b) % p = (a % p - b % p) % p
(a b) % p = (a % p b % p) % p
热题 HOT 100
简单
1. 两数之和
https://leetcode-cn.com/problems/two-sum/solution/liang-shu-zhi-he-by-leetcode-solution/
给定一个整数数组 nums
和一个整数目标值 target
,请你在该数组中找出 和为目标值 的那 两个 整数,并返回它们的数组下标。
你可以假设每种输入只会对应一个答案。但是,数组中同一个元素不能使用两遍。
你可以按任意顺序返回答案。
示例 1:
输入:nums = [2,7,11,15], target = 9
输出:[0,1]
解释:因为 nums[0] + nums[1] == 9 ,返回 [0, 1] 。
class Solution {
public int[] twoSum(int[] nums, int target) {
HashMap<Integer,Integer> map=new HashMap<>();
for(int i=0;i<nums.length;i++){
if(map.containsKey(target-nums[i]))
return new int[]{map.get(target-nums[i]),i};
map.put(nums[i],i);
}
return new int[0];
}
}
136. 只出现一次的数字
https://leetcode-cn.com/problems/single-number/solution/zhi-chu-xian-yi-ci-de-shu-zi-by-leetcode-solution/
给定一个非空整数数组,除了某个元素只出现一次以外,其余每个元素均出现两次。找出那个只出现了一次的元素。
说明:
你的算法应该具有线性时间复杂度。 你可以不使用额外空间来实现吗?
示例 1:
输入: [2,2,1]
输出:1
class Solution {
public int singleNumber(int[] nums) {
int x=0;
for(int num:nums){
x^=num;
}
return x;
}
}
21. 合并两个有序链表
https://leetcode-cn.com/problems/merge-two-sorted-lists/solution/he-bing-liang-ge-you-xu-lian-biao-by-leetcode-solu/
将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。
示例 1:
输入:l1 = [1,2,4], l2 = [1,3,4]
输出:[1,1,2,3,4,4]
class Solution {
public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
ListNode preHead=new ListNode(-1);
ListNode newHead=preHead;
while(l1!=null&&l2!=null){
if(l1.val<=l2.val){
newHead.next=l1;
l1=l1.next;
}else{
newHead.next=l2;
l2=l2.next;
}
newHead=newHead.next;
}
newHead.next=(l1==null)?l2:l1;
return preHead.next;
}
}
206. 反转链表
https://leetcode-cn.com/problems/reverse-linked-list/solution/fan-zhuan-lian-biao-by-leetcode-solution-d1k2/
反转一个单链表。
示例:
输入: 1->2->3->4->5->NULL
输出: 5->4->3->2->1->NULL
进阶:你可以迭代或递归地反转链表。你能否用两种方法解决这道题?
class Solution {
public ListNode reverseList(ListNode head) {
if(head==null) return null;
ListNode preHead=null;
ListNode curHead=head;
while(curHead!=null){
ListNode next=curHead.next;//保存下一个节点
curHead.next=preHead;
preHead=curHead;
curHead=next;
}
return preHead;
}
}
461. 汉明距离
https://leetcode-cn.com/problems/hamming-distance/solution/
两个整数之间的汉明距离指的是这两个数字对应二进制位不同的位置的数目。
给出两个整数 x
和 y
,计算它们之间的汉明距离。
注意:
0 ≤ x
, y
< 231.
示例:
输入: x = 1, y = 4
输出: 2
解释:
1 (0 0 0 1)
4 (0 1 0 0)
↑ ↑
上面的箭头指出了对应二进制位不同的位置。
class Solution {
public int hammingDistance(int x, int y) {
int temp=x^y;
int count=0;
while(temp!=0){
count++;
temp&=(temp-1);
}
return count;
}
}
160. 相交链表
https://leetcode-cn.com/problems/intersection-of-two-linked-lists/solution/xiang-jiao-lian-biao-by-leetcode/
编写一个程序,找到两个单链表相交的起始节点。
如下面的两个链表:
在节点 c1 开始相交。
public class Solution {
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
ListNode hA=headA;
ListNode hB=headB;
while(hA!=hB){
hA=(hA==null)?headB:hA.next;
hB=(hB==null)?headA:hB.next;
}
return hA;
}
}
69. 多数元素
https://leetcode-cn.com/problems/majority-element/solution/duo-shu-yuan-su-by-leetcode-solution/
给定一个大小为 n 的数组,找到其中的多数元素。多数元素是指在数组中出现次数 大于 ⌊ n/2 ⌋
的元素。
你可以假设数组是非空的,并且给定的数组总是存在多数元素。
示例 :
输入:[2,2,1,1,1,2,2]
输出:2
进阶:
尝试设计时间复杂度为 O(n)、空间复杂度为 O(1) 的算法解决此问题。
class Solution {
public int majorityElement(int[] nums) {
int votes=0,x=0;
for(int num:nums){
if(votes==0)
x=num;
votes+=(x==num)?1:-1;
}
return x;
}
}
448. 找到所有数组中消失的数字
https://leetcode-cn.com/problems/find-all-numbers-disappeared-in-an-array/solution/zhao-dao-suo-you-shu-zu-zhong-xiao-shi-d-mabl/
给定一个范围在 1 ≤ a[i] ≤ n ( n = 数组大小 ) 的 整型数组,数组中的元素一些出现了两次,另一些只出现一次。
找到所有在 [1, n] 范围之间没有出现在数组中的数字。
您能在不使用额外空间且时间复杂度为O(n)的情况下完成这个任务吗? 你可以假定返回的数组不算在额外空间内。
示例:
输入:
[4,3,2,7,8,2,3,1]
输出:
[5,6]class Solution {
public List<Integer> findDisappearedNumbers(int[] nums) {
int len=nums.length;
for(int num:nums){
int i=(num-1)%len;
nums[i]+=len;
}
List<Integer> res=new ArrayList<>();
for(int i=0;i<len;i++){
if(nums[i]<=len){
res.add(i+1);
}
}
return res;
}
}
234. 回文链表
https://leetcode-cn.com/problems/palindrome-linked-list/solution/hui-wen-lian-biao-by-leetcode-solution/
请判断一个链表是否为回文链表。
示例 1:
输入: 1->2
输出: false
示例 2:
输入: 1->2->2->1
输出: true
进阶:
你能否用 O(n) 时间复杂度和 O(1) 空间复杂度解决此题?class Solution {
public boolean isPalindrome(ListNode head) {
if(head==null) return true;
//找到中点
ListNode halfNode=halfNode(head);
//从中点开始反转了的链表
ListNode reverseNode=reverseList(halfNode.next);
//判断是否为回文
ListNode p1=head;
ListNode p2=reverseNode;
boolean result=true;
while(result&&p2!=null){
if(p1.val!=p2.val){
result=false;
}
p1=p1.next;
p2=p2.next;
}
//还原被反转了的链表
reverseNode=reverseList(reverseNode);
return result;
}
//找到链表的中点:快慢指针找到中点
private ListNode halfNode(ListNode head){
ListNode slow=head;
ListNode fast=head;
while(fast.next!=null&&fast.next.next!=null){
slow=slow.next;
fast=fast.next.next;
}
return slow;
}
//反转链表的函数
private ListNode reverseList(ListNode head){
ListNode pre=null;
ListNode cur=head;
while(cur!=null){
ListNode nex=cur.next;
cur.next=pre;
pre=cur;
cur=nex;
}//跳出时cur==null
return pre;
}
}
或
//利用链表的后序遍历(递归),但是增加了O(N)的时间复杂度
class Solution{
ListNode left;
public boolean isPalindrome(ListNode head) {
left=head;
return traverse(head);
}
private boolean traverse(ListNode head) {//利用链表的后序遍历
if(head==null) return true;
boolean res=traverse(head.next);
res=res&&(left.val==head.val);
left=left.next;
return res;
}
}
617. 合并二叉树
https://leetcode-cn.com/problems/merge-two-binary-trees/solution/he-bing-er-cha-shu-by-leetcode-solution/
给定两个二叉树,想象当你将它们中的一个覆盖到另一个上时,两个二叉树的一些节点便会重叠。
你需要将他们合并为一个新的二叉树。合并的规则是如果两个节点重叠,那么将他们的值相加作为节点合并后的新值,否则不为 NULL 的节点将直接作为新二叉树的节点。
示例 1:
输入:
Tree 1 Tree 2
1 2
/ \ / \
3 2 1 3
/ \ \
5 4 7
输出:
合并后的树:
3
/ \
4 5
/ \ \
5 4 7
注意: 合并必须从两个树的根节点开始。class Solution {
public TreeNode mergeTrees(TreeNode root1, TreeNode root2) {
if(root1==null){
return root2;
}
if(root2==null){
return root1;
}
TreeNode mergeNode=new TreeNode(root1.val+root2.val);
mergeNode.left=mergeTrees(root1.left,root2.left);
mergeNode.right=mergeTrees(root1.right,root2.right);
return mergeNode;
}
}
226. 翻转二叉树
https://leetcode-cn.com/problems/invert-binary-tree/solution/fan-zhuan-er-cha-shu-by-leetcode-solution/
翻转一棵二叉树。
示例:
输入:
4
/ \
2 7
/ \ / \
1 3 6 9
输出:
4
/ \
7 2
/ \ / \
9 6 3 1class Solution {
public TreeNode invertTree(TreeNode root) {// 将整棵树的节点翻转
if (root == null) {// base case
return null;
}
/**** 前序遍历位置 ****/
// root 节点需要交换它的左右子节点
TreeNode tmp = root.left;
root.left = root.right;
root.right = tmp;
// 让左右子节点继续翻转它们的子节点
invertTree(root.left);
invertTree(root.right);
return root;
}
}
104. 二叉树的最大深度
https://leetcode-cn.com/problems/maximum-depth-of-binary-tree/solution/er-cha-shu-de-zui-da-shen-du-by-leetcode-solution/
给定一个二叉树,找出其最大深度。
二叉树的深度为根节点到最远叶子节点的最长路径上的节点数。
说明: 叶子节点是指没有子节点的节点。
示例:
给定二叉树[3,9,20,null,null,15,7]
,
3
/ \
9 20
/ \
15 7
返回它的最大深度 3 。class Solution {
public int maxDepth(TreeNode root) {
if(root==null) return 0;
int left=maxDepth(root.left);
int right=maxDepth(root.right);
return Math.max(left+1,right+1);
}
}
283. 移动零
https://leetcode-cn.com/problems/move-zeroes/solution/yi-dong-ling-by-leetcode-solution/
给定一个数组nums
,编写一个函数将所有0
移动到数组的末尾,同时保持非零元素的相对顺序。
示例:
输入:[0,1,0,3,12]
输出:[1,3,12,0,0]
说明:
- 必须在原数组上操作,不能拷贝额外的数组。
- 尽量减少操作次数。
class Solution {
public void moveZeroes(int[] nums) {
int n = nums.length, left = 0, right = 0;
while (right < n) {
//right遇到0可以跳过,到挨着的下一个非0数
//left遇到0不能跳过,直接指向这个0
//这样right和left交换,就能把非0移动到最后
//此方法不适合奇数与偶数的交换
if (nums[right] != 0) {
swap(nums, left, right);
left++;
}
right++;
}
}
public void swap(int[] nums, int left, int right) {
int temp = nums[left];
nums[left] = nums[right];
nums[right] = temp;
}
}
101. 对称二叉树
https://leetcode-cn.com/problems/symmetric-tree/solution/dui-cheng-er-cha-shu-by-leetcode-solution/
给定一个二叉树,检查它是否是镜像对称的。
例如,二叉树[1,2,2,3,4,4,3]
是对称的。
1
/ \
2 2
/ \ / \
3 4 4 3
但是下面这个[1,2,2,null,3,null,3]
则不是镜像对称的:
1
/ \
2 2
\ \
3 3
进阶:
你可以运用递归和迭代两种方法解决这个问题吗?//递归
class Solution {
public boolean isSymmetric(TreeNode root) {
return temp(root,root);
}
private boolean temp(TreeNode node1,TreeNode node2){
if(node1==null&&node2==null) return true;
if(node1==null||node2==null) return false;
return (node1.val==node2.val)&&temp(node1.left,node2.right)&&temp(node1.right,node2.left);
}
}
//迭代
class Solution {
public boolean isSymmetric(TreeNode root) {
return check(root, root);
}
public boolean check(TreeNode u, TreeNode v) {
Queue<TreeNode> q = new LinkedList<TreeNode>();
q.offer(u);
q.offer(v);
while (!q.isEmpty()) {
u = q.poll();
v = q.poll();
if (u == null && v == null) {
continue;
}
if ((u == null || v == null) || (u.val != v.val)) {
return false;
}
q.offer(u.left);
q.offer(v.right);
q.offer(u.right);
q.offer(v.left);
}
return true;
}
}
155. 最小栈
https://leetcode-cn.com/problems/min-stack/solution/zui-xiao-zhan-by-leetcode-solution/
设计一个支持push
,pop
,top
操作,并能在常数时间内检索到最小元素的栈。
push(x)
—— 将元素 x 推入栈中。pop()
—— 删除栈顶的元素。top()
—— 获取栈顶元素。getMin()
—— 检索栈中的最小元素。
示例:
输入:
[“MinStack”,”push”,”push”,”push”,”getMin”,”pop”,”top”,”getMin”]
[[],[-2],[0],[-3],[],[],[],[]]
输出:
[null,null,null,null,-3,null,0,-2]
解释:
MinStack minStack = new MinStack();
minStack.push(-2);
minStack.push(0);
minStack.push(-3);
minStack.getMin(); —> 返回 -3.
minStack.pop();
minStack.top(); —> 返回 0.
minStack.getMin(); —> 返回 -2.
提示:
pop
、top
和getMin
操作总是在 非空栈 上调用。//方式1
class MinStack {
LinkedList<Integer> xStack;
LinkedList<Integer> minStack;
public MinStack() {
xStack = new LinkedList<>();
minStack = new LinkedList<>();
minStack.push(Integer.MAX_VALUE);//添加这个数的目的是为了第一个添加的时候,要比较峰值和当前要添加进来的值的大小时,不会出现空指针异常
}
public void push(int x) {
xStack.push(x);
minStack.push(Math.min(minStack.peek(), x));
}
public void pop() {
xStack.pop();
minStack.pop();
}
public int top() {
return xStack.peek();
}
public int getMin() {
return minStack.peek();
}
}
//方式2
class MinStack {
Stack<Integer> stoStk;
Stack<Integer> minStk;
public MinStack() {
stoStk=new Stack<>();
minStk=new Stack<>();
}
public void push(int x) {
stoStk.add(x);
if( minStk.empty() || minStk.peek()>=x)
minStk.add(x);
}
public void pop() {
if(stoStk.pop().equals(minStk.peek()))
minStk.pop();
}
public int top() {
return stoStk.peek();
}
public int getMin() {
return minStk.peek();
}
}
121. 买卖股票的最佳时机
https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock/solution/121-mai-mai-gu-piao-de-zui-jia-shi-ji-by-leetcode-/
给定一个数组prices
,它的第i
个元素prices[i]
表示一支给定股票第i
天的价格。
你只能选择 某一天 买入这只股票,并选择在 未来的某一个不同的日子 卖出该股票。设计一个算法来计算你所能获取的最大利润。
返回你可以从这笔交易中获取的最大利润。如果你不能获取任何利润,返回0
。
示例 1:
输入:[7,1,5,3,6,4]
输出:5
解释:在第 2 天(股票价格 = 1)的时候买入,在第 5 天(股票价格 = 6)的时候卖出,最大利润 = 6-1 = 5 。
注意利润不能是 7-1 = 6, 因为卖出价格需要大于买入价格;同时,你不能在买入前卖出股票。
示例 2:
输入:prices = [7,6,4,3,1]
输出:0
解释:在这种情况下, 没有交易完成, 所以最大利润为 0。
class Solution {
public int maxProfit(int[] prices) {
int minValue=Integer.MAX_VALUE;
int maxDiff=0;
for(int i=0;i<prices.length;i++){
if(prices[i]<minValue) {
minValue=prices[i];
}else if(prices[i]-minValue>maxDiff){
maxDiff=prices[i]-minValue;
}
}
return maxDiff;
}
}
53. 最大子序和
https://leetcode-cn.com/problems/maximum-subarray/solution/zui-da-zi-xu-he-by-leetcode-solution/
给定一个整数数组 nums
,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
示例 1:
输入:nums = [-2,1,-3,4,-1,2,1,-5,4]
输出:6
解释:连续子数组 [4,-1,2,1] 的和最大,为 6 。
//方法一:动态规划
class Solution {
public int maxSubArray(int[] nums) {
int maxSum=nums[0];
int Fi1=nums[0];
for(int i=1;i<nums.length;i++){
Fi1=(Fi1+nums[i])>nums[i]?(Fi1+nums[i]):nums[i];
maxSum=maxSum>Fi1?maxSum:Fi1;
}
return maxSum;
}
}
//
543. 二叉树的直径
https://leetcode-cn.com/problems/diameter-of-binary-tree/solution/er-cha-shu-de-zhi-jing-by-leetcode-solution/
给定一棵二叉树,你需要计算它的直径长度。一棵二叉树的直径长度是任意两个结点路径长度中的最大值。这条路径可能穿过也可能不穿过根结点。
示例 :
给定二叉树
1
/ \
2 3
/ \
4 5
返回 3, 它的长度是路径 [4,2,1,3] 或者 [5,2,1,3]。
注意:两结点之间的路径长度是以它们之间边的数目表示。
class Solution {
int ans;
public int diameterOfBinaryTree(TreeNode root) {
ans = 1;//ans记录该路径经过节点数的最大值
depth(root);
return ans - 1;
}
public int depth(TreeNode node) {
if (node == null) {
return 0; // 访问到空节点了,返回0
}
int L = depth(node.left); // 左儿子为根的子树的深度
int R = depth(node.right); // 右儿子为根的子树的深度
ans = Math.max(ans, L+R+1); // 计算d_node即L+R+1 并更新ans
return Math.max(L, R) + 1; // 返回该节点为根的子树的深度
}
}
70. 爬楼梯
https://leetcode-cn.com/problems/climbing-stairs/solution/pa-lou-ti-by-leetcode-solution/
假设你正在爬楼梯。需要 n 阶你才能到达楼顶。
每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?
注意:给定 n 是一个正整数。
示例 1:
输入: 2
输出: 2
解释: 有两种方法可以爬到楼顶。
1. 1 阶 + 1 阶
2. 2 阶
示例 2:
输入: 3
输出: 3
解释: 有三种方法可以爬到楼顶。
1. 1 阶 + 1 阶 + 1 阶
2. 1 阶 + 2 阶
3. 2 阶 + 1 阶
class Solution {
public int climbStairs(int n) {
int f1=0,f2=1,temp=f1+f2;
for(int i=1;i<=n;i++){
temp=f1+f2;
f1=f2;
f2=temp;
}
return f2;
}
}
141. 环形链表
https://leetcode-cn.com/problems/linked-list-cycle/solution/huan-xing-lian-biao-by-leetcode-solution/
给定一个链表,判断链表中是否有环。
如果链表中有某个节点,可以通过连续跟踪 next
指针再次到达,则链表中存在环。 为了表示给定链表中的环,我们使用整数 pos
来表示链表尾连接到链表中的位置(索引从 0 开始)。 如果 pos
是 -1
,则在该链表中没有环。注意:**pos**
不作为参数进行传递,仅仅是为了标识链表的实际情况。
如果链表中存在环,则返回 true
。 否则,返回 false
。
进阶:
你能用 O(1)(即,常量)内存解决此问题吗?
示例 1:
输入:head = [3,2,0,-4], pos = 1
输出:true
解释:链表中有一个环,其尾部连接到第二个节点。
示例 2:
输入:head = [1,2], pos = 0
输出:true
解释:链表中有一个环,其尾部连接到第一个节点。
示例 3:
输入:head = [1], pos = -1
输出:false
解释:链表中没有环。
提示:
- 链表中节点的数目范围是
[0, 104]
-105 <= Node.val <= 105
pos
为-1
或者链表中的一个 有效索引 。public class Solution {
public boolean hasCycle(ListNode head) {
ListNode fast=head;//快指针
ListNode low=head;//慢指针
while(fast!=null&&fast.next!=null){//如果不是环形链表,则在不满足这两个条件中的任意一个条件时跳出
fast=fast.next.next;//此处不会空指针
low=low.next;
if(fast==low) break;//是环形链表时跳出
}
if(fast==null||fast.next==null) return false;//判断跳出循环的条件时哪个
return true;//到此处则一定有环存在
}
}
20. 有效的括号
https://leetcode-cn.com/problems/valid-parentheses/solution/you-xiao-de-gua-hao-by-leetcode-solution/
给定一个只包括'('
,')'
,'{'
,'}'
,'['
,']'
的字符串s
,判断字符串是否有效。
有效字符串需满足:
- 左括号必须用相同类型的右括号闭合。
左括号必须以正确的顺序闭合。
示例 1:
输入:s = “()”
输出:true
示例 2:
输入:s = “()[]{}”
输出:true
示例 3:
输入:s = “(]”
输出:false
示例 4:
输入:s = “([)]”
输出:false
示例 5:
输入:s = “{[]}”
输出:true
提示:
1 <= s.length <= 104
s
仅由括号'()[]{}'
组成class Solution {
public boolean isValid(String s) {
int len=s.length();
if(len%2==1) return false;
HashMap<Character,Character> map=new HashMap<>();
map.put(')','(');
map.put(']','[');
map.put('}','{');
Stack<Character> stack=new Stack<>();
for(int i=0;i<len;i++){
char ch=s.charAt(i);
if(map.containsKey(ch)){
if(stack.isEmpty()|| stack.peek()!=map.get(ch)){
return false;
}
stack.pop();
}else{
stack.push(ch);
}
}
return stack.isEmpty();
}
}
700. 二叉搜索树中的搜索
给定二叉搜索树(BST)的根节点和一个值。 你需要在BST中找到节点值等于给定值的节点。 返回以该节点为根的子树。 如果节点不存在,则返回 NULL。
例如,
给定二叉搜索树:
4
/ \
2 7
/ \
1 3
和值: 2
你应该返回如下子树:
2
/ \
1 3
在上述示例中,如果要找的值是5,但因为没有节点值为5,我们应该返回NULL。class Solution{
public TreeNode searchBST(TreeNode root, int val) {
if(root==null)
return null;
if(root.val==val){// 找到目标,做点什么
return root;//返回找到的节点
}
if(root.val<val){
return searchBST(root.right, val);
}
if(root.val>val){
return searchBST(root.left, val);
}
return null;//在整棵树中都没有找到目标值
}
}
或
//这只是两种值的不同的返回方式而已
class Solution {
TreeNode res=null;
public TreeNode searchBST(TreeNode root, int val) {
dfs(root,val);
return res;
}
private void dfs(TreeNode root, int val) {
if(root==null)
return;
if(root.val==val){
res=root;
}
if(root.val<val){
dfs(root.right, val);
}
if(root.val>val){
dfs(root.left, val);
}
}
}
783. 二叉搜索树节点最小距离
530. 二叉搜索树的最小绝对差
给你一个二叉搜索树的根节点root,返回树中任意两不同节点值之间的最小差值。
注意:本题与 530:https://leetcode-cn.com/problems/minimum-absolute-difference-in-bst/相同
示例 1:
输入:root = [4,2,6,1,3]
输出:1class Solution {
int preVal=-1;
int minusVal=Integer.MAX_VALUE;
public int minDiffInBST(TreeNode root) {
dfs(root);
return minusVal;
}
private void dfs(TreeNode node){
if(node==null)
return;
dfs(node.left);
if(preVal!=-1){
minusVal=Math.min(Math.abs(node.val-preVal), minusVal);
preVal=node.val;
}else{
preVal=node.val;
}
dfs(node.right);
}
}
501. 二叉搜索树中的众数
给定一个有相同值的二叉搜索树(BST),找出 BST 中的所有众数(出现频率最高的元素)。
假定 BST 有如下定义:结点左子树中所含结点的值小于等于当前结点的值
- 结点右子树中所含结点的值大于等于当前结点的值
- 左子树和右子树都是二叉搜索树
例如:给定 BST[1,null,2,2],
1
\
2
/
2
返回[2].
提示:如果众数超过1个,不需考虑输出顺序
进阶:你可以不使用额外的空间吗?(假设由递归产生的隐式调用栈的开销不被计算在内)
class Solution {
List<Integer> res=new ArrayList<>();
int base,count,maxCount;
public int[] findMode(TreeNode root) {
dfs(root);
int[] result=new int[res.size()];
for(int i=0;i<result.length;i++){
result[i]=res.get(i);
}
return result;
}
private void dfs(TreeNode root){
if(root==null)
return;
dfs(root.left);
update(root.val);
dfs(root.right);
}
private void update(int x) {
if(base==x){
count++;
}else{
base=x;
count=1;
}
if(count==maxCount){
res.add(x);
}
if(count>maxCount){
maxCount=count;
res.clear();
res.add(x);
}
}
}
938. 二叉搜索树的范围和
给定二叉搜索树的根结点 root,返回值位于范围[low, high]之间的所有结点的值的和。
示例 1:
输入:root = [10,5,15,3,7,null,18], low = 7, high = 15
输出:32
class Solution {
public int rangeSumBST(TreeNode root, int low, int high) {
if(root==null)
return 0;
if(high<root.val){//root 节点的值大于high,无需考虑右子树
return rangeSumBST(root.left, low, high);
}
if(low>root.val){//root 节点的值小于 low,无需考虑左子树
return rangeSumBST(root.right, low, high);
}
//root 节点的值在 [low,high] 范围内,考虑上这三者的和
return root.val+rangeSumBST(root.left, low, high)+rangeSumBST(root.right, low, high);
}
}
中等
15. 三数之和
https://leetcode-cn.com/problems/3sum/solution/san-shu-zhi-he-by-leetcode-solution/
给你一个包含n个整数的数组 nums,判断 nums 中是否存在三个元素a,b,c ,使得 _a + b + c =_0 ?请你找出所有和为0且不重复的三元组。
注意:答案中不可以包含重复的三元组。
示例 1:
输入:nums = [-1,0,1,2,-1,-4]输出:[[-1,-1,2],[-1,0,1]]
示例 2:
输入:nums = []输出:[]
示例 3:
输入:nums = [0]输出:[]
提示:
- 0 <= nums.length <= 3000
-105<= nums[i] <= 105
class Solution {
public List<List<Integer>> threeSum(int[] nums) {
Arrays.sort(nums);//对数组排序后再查找
int len=nums.length;
List<List<Integer>> list=new ArrayList<>();
for (int first=0;first<len;first++) {// 枚举 first
if (first>0&&nums[first]==nums[first-1]) {//为了不重复,下一次枚举的first需要和上一次枚举的数不相同
continue;
}
int third=len-1;// third对应的指针初始指向数组的最右端
for (int second=first+1;second<len;second++) {//枚举second
if (second>first+1&&nums[second]==nums[second-1]) {//为了不重复,下一次枚举的second需要和上一次枚举的数不相同
continue;
}
//让这个循环while退出可能有两种情况,下面依次判断
while (second<third&&nums[second]+nums[third]>-nums[first]) {//需要保证second的指针在third的指针的左侧
third--;//因为是排序后的数,一定能满足 first<=second<=third
}
// 如果指针重合,随着second后续的增加,
// 就不会有满足 first+second+third=0 并且second<third的third了,可以退出循环
if (second==third) {
break;
}
//这是循环退出的第二种情况,如果满足条件,则添加
if (nums[second]+nums[third]==-nums[first]) {
list.add(Arrays.asList(nums[first],nums[second],nums[third]));
}
//走到这里说明是nums[second]+nums[third]<-nums[first],则让second++
}
}
return list;
}
}
338. 比特位计数
https://leetcode-cn.com/problems/counting-bits/solution/bi-te-wei-ji-shu-by-leetcode-solution-0t1i/
给定一个非负整数 num。对于 0 ≤ i ≤ num范围中的每个数字 i ,计算其二进制数中的 1 的数目并将它们作为数组返回。
示例 1:
输入: 2输出: [0,1,1]
示例 2:
输入: 5输出: [0,1,1,2,1,2]
进阶:给出时间复杂度为O(n*sizeof(integer))的解答非常容易。但你可以在线性时间O(n)内用一趟扫描做到吗?
- 要求算法的空间复杂度为O(n)。
你能进一步完善解法吗?要求在C++或任何其他语言中不使用任何内置函数(如 C++ 中的 __builtin_popcount)来执行此操作。
//方法二:动态规划——最高有效位
class Solution {
public int[] countBits(int num) {
int[] bits = new int[num + 1];
int highBit = 0;
for (int i = 1; i <= num; i++) {
if ((i & (i - 1)) == 0) {
highBit = i;
}
bits[i] = bits[i - highBit] + 1;
}
return bits;
}
}
//方法三:动态规划——最低有效位
class Solution {
public int[] countBits(int num) {
int[] bits = new int[num + 1];
for (int i = 1; i <= num; i++) {
bits[i] = bits[i >> 1] + (i & 1);
}
return bits;
}
}
//方法四:动态规划——最低设置位
class Solution {
public int[] countBits(int num) {
int[] bits = new int[num + 1];
for (int i = 1; i <= num; i++) {
bits[i] = bits[i & (i - 1)] + 1;
}
return bits;
}
}
46. 全排列
给定一个没有重复数字的序列,返回其所有可能的全排列。
示例:
输入: [1,2,3]输出: ```java class Solution { public List- > permute(int[] nums) {
List
- > res=new ArrayList<>();
List
path=new ArrayList<>(); dfs(nums,0,path,res); return res; } private void dfs(int[] nums, int index, List
path, List - > res) {
if(path.size()== nums.length){
res.add(new ArrayList<>(path));
return;
} for(int i=0; i<nums.length;i++){
if(path.contains(nums[i])) continue;
path.add(nums[i]);
dfs(nums, index+1, path, res);
path.remove(path.size()-1);
} }
}
<a name="urbYu"></a>
#### [22. 括号生成](https://leetcode-cn.com/problems/generate-parentheses/)
数字 n 代表生成括号的对数,请你设计一个函数,用于能够生成所有可能的并且 有效的 括号组合。<br />示例 1:<br />输入:n = 3<br />输出:["((()))","(()())","(())()","()(())","()()()"]<br />示例 2:<br />输入:n = 1<br />输出:["()"]<br />提示:<br />1 <= n <= 8
```java
class Solution {
public List<String> generateParenthesis(int n) {
List<String> res=new ArrayList<>();
if(n<1) return res;
dfs(n,"",res,0,0);
return res;
}
private void dfs(int n,String path,List<String> res,int open,int close){
if(close>open||open>n)
return;
if(2*n==path.length()){
res.add(path);
return;
}
dfs(n, path+"(", res,open+1,close);
dfs(n, path+")", res,open,close+1);
}
}
或
class Solution {
public List<String> generateParenthesis(int n) {
List<String> res=new ArrayList<>();
if(n<1) return res;
dfs(n,"",res,0,0);
return res;
}
private void dfs(int n,String path,List<String> res,int open,int close){
if(2*n==path.length()){
res.add(path);
return;
}
if(open<n)
dfs(n, path+"(", res,open+1,close);
if(close<open)
dfs(n, path+")", res,open,close+1);
}
}
94. 二叉树的中序遍历
给定一个二叉树的根节点root,返回它的中序 遍历。
class Solution {
public List<Integer> inorderTraversal(TreeNode root) {
List<Integer> res=new ArrayList<>();
Stack<TreeNode> stack=new Stack<>();
TreeNode node=root;
while (node!=null||!stack.isEmpty()){
if(node!=null){
stack.add(node);
node=node.left;
}else{
node = stack.pop();
res.add(node.val);
node=node.right;
}
}
return res;
}
}
或
class Solution {
public List<Integer> inorderTraversal(TreeNode root) {
List<Integer> res=new ArrayList<>();
Stack<TreeNode> stack=new Stack<>();
TreeNode node=root;
while (node!=null||!stack.isEmpty()){
while (node!=null){
stack.add(node);
node=node.left;
}
node = stack.pop();
res.add(node.val);
node=node.right;
}
return res;
}
}
102. 二叉树的层序遍历
给你一个二叉树,请你返回其按层序遍历得到的节点值。 (即逐层地,从左到右访问所有节点)。
class Solution {
public List<List<Integer>> levelOrder(TreeNode root) {
List<List<Integer>> res=new ArrayList<>();
if(root==null) return res;
LinkedList<TreeNode> nodesList=new LinkedList<>();
nodesList.add(root);
while(!nodesList.isEmpty()){
List<Integer> temp=new ArrayList<>();
for(int i=nodesList.size();i>0;i--){
TreeNode node = nodesList.removeFirst();
temp.add(node.val);
if(node.left!=null)
nodesList.add(node.left);
if(node.right!=null)
nodesList.add(node.right);
}
res.add(new ArrayList<>(temp));
}
return res;
}
}
105. 从前序与中序遍历序列构造二叉树
根据一棵树的前序遍历与中序遍历构造二叉树。
注意:你可以假设树中没有重复的元素。
例如,给出
前序遍历 preorder = [3,9,20,15,7]
class Solution {
public TreeNode buildTree(int[] preorder,int[] inorder) {
HashMap<Integer,Integer> map=new HashMap<>();
for(int i=0;i<inorder.length;i++){
map.put(inorder[i],i);
}
return build(preorder, 0, preorder.length-1,inorder,0,inorder.length-1,map);
}
private TreeNode build(int[] preorder,int preLeft,int preRight,
int[] inorder,int inLeft,int inRight,
HashMap<Integer,Integer> map){
if(preLeft>preRight||inLeft>inRight)
return null;
// root 节点对应的值就是前序遍历数组的第一个元素
TreeNode root=new TreeNode(preorder[preLeft]);
int len=map.get(preorder[preLeft])-inLeft;// rootVal 在中序遍历数组中的索引
// 递归构造左右子树
root.left=build(preorder,preLeft+1,preLeft+len,inorder,inLeft,inLeft+len-1,map);
root.right=build(preorder,preLeft+len+1,preRight, inorder, inLeft+len+1, inRight,map);
return root;
}
}
106. 从中序与后序遍历序列构造二叉树
难度中等496
根据一棵树的中序遍历与后序遍历构造二叉树。
注意:你可以假设树中没有重复的元素。
例如,给出
中序遍历 inorder = [9,3,15,20,7]
后序遍历 postorder = [9,15,7,20,3]
返回如下的二叉树:
3
/ \
9 20
/ \
15 7
class Solution {
public TreeNode buildTree(int[] inorder, int[] postorder) {
HashMap<Integer,Integer> map=new HashMap<>();
for(int i=0;i<inorder.length;i++){
map.put(inorder[i],i);
}
return build(inorder,0,inorder.length-1,postorder,0,postorder.length-1,map);
}
private TreeNode build(int[] inorder,int inLeft,int inRight,
int[] postorder,int postLeft,int postRight,
HashMap<Integer,Integer> map){
if(inLeft>inRight||postLeft>postRight)
return null;
TreeNode root=new TreeNode(postorder[postRight]);
int len=map.get(postorder[postRight])-inLeft;
root.left=build(inorder, inLeft, inLeft+len-1, postorder, postLeft, postLeft+len-1, map);
root.right=build(inorder, inLeft+len+1, inRight, postorder, postLeft+len, postRight-1, map);
return root;
}
}
19. 删除链表的倒数第 N 个结点
给你一个链表,删除链表的倒数第 n个结点,并且返回链表的头结点。
进阶:你能尝试使用一趟扫描实现吗?
class Solution {
public ListNode removeNthFromEnd(ListNode head, int n) {
ListNode temp=new ListNode(0,head);
ListNode pre=head;
ListNode after=temp;
while(n>0){
pre=pre.next;
n--;
}
while(pre!=null){
pre=pre.next;
after=after.next;
}
after.next=after.next.next;
return temp.next;
}
}
114. 二叉树展开为链表
给你二叉树的根结点root,请你将它展开为一个单链表:
- 展开后的单链表应该同样使用TreeNode,其中right子指针指向链表中下一个结点,而左子指针始终为null。
- 展开后的单链表应该与二叉树先序遍历顺序相同。
示例 1:
输入:root = [1,2,5,3,4,null,6]输出:[1,null,2,null,3,null,4,null,5,null,6]
class Solution {
// 定义:将以 root 为根的树拉平为链表
public void flatten(TreeNode root) {
// base case
if (root == null) return;
flatten(root.left);
flatten(root.right);
/**** 后序遍历位置 ****/
// 1、左右子树已经被拉平成一条链表
TreeNode left = root.left;
TreeNode right = root.right;
// 2、将左子树作为右子树
root.left = null;
root.right = left;
// 3、将原先的右子树接到当前右子树的末端
TreeNode p = root;
while (p.right != null) {
p = p.right;
}
p.right = right;
}
}
124. 二叉树中的最大路径和
路径被定义为一条从树中任意节点出发,沿父节点-子节点连接,达到任意节点的序列。同一个节点在一条路径序列中至多出现一次。该路径至少包含一个节点,且不一定经过根节点。
路径和是路径中各节点值的总和。
给你一个二叉树的根节点root,返回其最大路径和。
class Solution {
int ans=Integer.MIN_VALUE;
public int maxPathSum(TreeNode root) {
dfs(root);
return ans;
}
private int dfs(TreeNode node) {//找出每个节点最大路径
if(node==null) return 0;
int left=Math.max(0, dfs(node.left));
int right=Math.max(0, dfs(node.right));
ans=Math.max(ans,left+right+node.val);
return Math.max(left, right)+node.val;
}
}
142. 环形链表 II
给定一个链表,返回链表开始入环的第一个节点。 如果链表无环,则返回 null。
为了表示给定链表中的环,我们使用整数pos来表示链表尾连接到链表中的位置(索引从 0 开始)。 如果pos是-1,则在该链表中没有环。注意,pos仅仅是用于标识环的情况,并不会作为参数传递到函数中。
说明:不允许修改给定的链表。
进阶:
- 你是否可以使用O(1)空间解决此题?
示例 1:
输入:head = [3,2,0,-4], pos = 1输出:返回索引为 1 的链表节点解释:链表中有一个环,其尾部连接到第二个节点。
public class Solution {
public ListNode detectCycle(ListNode head) {
ListNode fast=head;
ListNode slow=head;
while (fast!=null&&fast.next!=null){
fast=fast.next.next;
slow=slow.next;
if(fast==slow){
break;
}
}
if(fast==null||fast.next==null)
return null;
fast=head;
while (fast!=slow){
slow=slow.next;
fast=fast.next;
}
return fast;
}
}
647. 回文子串
给定一个字符串,你的任务是计算这个字符串中有多少个回文子串。
具有不同开始位置或结束位置的子串,即使是由相同的字符组成,也会被视作不同的子串。
示例 1:
输入:“abc”输出:3解释:三个回文子串: “a”, “b”, “c”
class Solution {
public int countSubstrings(String s) {
int[] res=new int[1];
for(int i=0;i<s.length();i++){
palindrome(s,i,i,res);
palindrome(s,i,i+1,res);
}
return res[0];
}
private void palindrome(String s,int i,int j,int[] res) {
while(i>=0&&j<s.length()&&s.charAt(i)==s.charAt(j)){
i--;
j++;
res[0]++;
}
}
}
5. 最长回文子串
https://mp.weixin.qq.com/s?__biz=MzAxODQxMDM0Mw==&mid=2247484471&idx=1&sn=7c26d04a1f035770920d31377a1ebd42&chksm=9bd7fa3faca07329189e9e8b51e1a665166946b66b8e8978299ba96d5f2c0d3eafa7db08b681&mpshare=1&scene=23&srcid=0502skarllDchCXKriEMLVWp&sharer_sharetime=1619940584540&sharer_shareid=371aa2900d94d479c7e5122b7ae18619#rd
给你一个字符串s,找到s中最长的回文子串。
示例 1:
输入:s = “babad”输出:“bab”解释:“aba” 同样是符合题意的答案。
class Solution {
public String longestPalindrome(String s) {
String res="";
for(int i=0;i<s.length();i++){
String result1=palindrome(s,i,i);//截取到的字符串是奇数个
String result2=palindrome(s,i,i+1);//截取到的字符串是偶数个
//获取到最大的回文字符串的长度
res=res.length()>result1.length()?res:result1;
res=res.length()>result2.length()?res:result2;
}
return res;
}
private String palindrome(String s,int i,int j) {
while (i>=0&&j<s.length()&&s.charAt(i)==s.charAt(j)){//防止索引越界
//从中间向两边扩展的情况
i--;
j++;
}//跳出这个循环时,要么是 i 刚越界,要么是 j 刚越界,要么是 s.charAt(i)!=s.charAt(j)
//无论是 i 越界还是 s.charAt(i)!=s.charAt(j),都应该从 i+1 位置处开始截取字符串
//无论是 j 越界还是 s.charAt(i)!=s.charAt(j),都应该从 j-1 位置处开始截取字符串
// 但 substring() 方法是左开右闭,则截取到 j 位置处
return s.substring(i+1,j);
}
}
1669. 合并两个链表
给你两个链表 list1和 list2 ,它们包含的元素分别为 n个和 m个。
请你将 list1 中第 a 个节点到第 b 个节点删除,并将list2 接在被删除节点的位置。
下图中蓝色边和节点展示了操作后的结果:
请你返回结果链表的头指针。
示例 1:
class Solution {
public ListNode mergeInBetween(ListNode list1, int a, int b, ListNode list2) {
ListNode temp1=list1;
ListNode temp2=list1;
while(a-->1){
temp1=temp1.next;
}
while(b-->=0){
temp2=temp2.next;
}
temp1.next=list2;
while(temp1.next!=null){
temp1=temp1.next;
}
temp1.next=temp2;
return list1;
}
}
143. 重排链表
给定一个单链表 L:L_0→_L_1→…→_Ln-1→L_n ,将其重新排列后变为:_L_0→_Ln→L_1→_Ln-1→L_2→_Ln-2→…
你不能只是单纯的改变节点内部的值,而是需要实际的进行节点交换。
示例 1:
给定链表 1->2->3->4, 重新排列为 1->4->2->3.
示例 2:
给定链表 1->2->3->4->5, 重新排列为 1->5->2->4->3.
//注意到目标链表即为将原链表的左半端和反转后的右半端合并后的结果。
class Solution {
public void reorderList(ListNode head) {
ListNode halfNode=halfNode(head);
ListNode l1=head;
ListNode l2=halfNode.next;
halfNode.next=null;//将链表分为两段
l2 = reverseList(l2);
mergeList(l1, l2);
}
private ListNode reverseList(ListNode node) {
ListNode pre=null;
ListNode cur=node;
while (cur!=null){
ListNode temp=cur.next;
cur.next=pre;
pre=cur;
cur=temp;
}
return pre;
}
private ListNode halfNode(ListNode head) {
ListNode fast=head;
ListNode slow=head;
while(fast!=null&&fast.next!=null){
fast=fast.next.next;
slow=slow.next;
}
return slow;
}
private ListNode mergeList(ListNode node1,ListNode node2){
ListNode temp1;
ListNode temp2;
while(node1!=null&&node2!=null){
temp1=node1.next;
temp2=node2.next;
node1.next=node2;
node1=temp1;
node2.next=node1;
node2=temp2;
}
return node1;
}
}
1143. 最长公共子序列
给定两个字符串 text1和 text2,返回这两个字符串的最长公共子序列的长度。如果不存在公共子序列,返回0。
一个字符串的 子序列是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。
- 例如,”ace”是”abcde”的子序列,但”aec”不是”abcde”的子序列。
两个字符串的公共子序列是这两个字符串所共同拥有的子序列。
//动态规划
class Solution {
public int longestCommonSubsequence(String text1, String text2) {
int len1=text1.length();
int len2=text2.length();
int[][] dp=new int[len1+1][len2+1];
for(int i=1;i<=len1;i++){
for(int j=1;j<=len2;j++){
if(text1.charAt(i-1)==text2.charAt(j-1)){
dp[i][j]=dp[i-1][j-1]+1;
}else{
dp[i][j]=Math.max(dp[i-1][j],dp[i][j-1],dp[i-1][j-1]);
}
}
}
return dp[len1][len2];
}
}
//动态规划是有以下的递归方式优化而得,递归会超时
class Solution{
public int longestCommonSubsequence(String text1, String text2) {
int len1=text1.length();
int len2=text2.length();
int[][] dp=new int[len1][len2];
return dfs(dp,len1-1,len2-1,text1,text2);
}
private int dfs(int[][] dp,int i,int j,String text1,String text2){
if(i==-1||j==-1)
return 0;
if(text1.charAt(i)==text2.charAt(j)){
return dfs(dp,i-1,j-1,text1,text2)+1;
}else{
return Math.max(dfs(dp,i-1,j,text1,text2),dfs(dp,i,j-1,text1,text2));
}
}
}
322. 零钱兑换
难度中等1248
给定不同面额的硬币coins和一个总金额amount。编写一个函数来计算可以凑成总金额所需的最少的硬币个数。如果没有任何一种硬币组合能组成总金额,返回 -1。
你可以认为每种硬币的数量是无限的。
示例 1:
输入:coins = [1, 2, 5], amount = 11
输出:3
解释:11 = 5 + 5 + 1
//动态规划
class Solution {
public int coinChange(int[] coins, int amount) {
int[] dp=new int[amount+1];
Arrays.fill(dp, amount+1);
dp[0]=0;
for(int i=1;i<=amount;i++){
for(int coin:coins){// 内层 for 在求所有子问题 + 1 的最小值
if(i-coin<0) continue;
dp[i]=Math.min(dp[i],1+dp[i-coin]);
}
}
return dp[amount]==amount+1?-1:dp[amount];
}
}
//带备忘录的递归,拥有O(NK)的时间复杂度
class Solution {
public int coinChange(int[] coins, int amount) {
int[] memory=new int[amount+1];
Arrays.fill(memory, -2);//初始化备忘录为-2
return help(coins, amount,memory);
}
private int help(int[] coins, int amount,int[] memory) {
if (amount==0) return 0;
if (memory[amount]!=-2) return memory[amount];
int res=Integer.MAX_VALUE;
for (int coin:coins) {
//金额不可达
if(amount-coin<0) continue;
int sub=help(coins,amount-coin,memory);
//子问题无解
if(sub==-1) continue;
res=Math.min(res,sub+1);
}
//记录每个节点的答案
memory[amount]=(res==Integer.MAX_VALUE)?-1:res;
return memory[amount];
}
}
//纯递归,拥有O(N^K*K)的超高的时间复杂度,K是硬币的币种(向下递归成为树结构来查看)
class Solution {
public int coinChange(int[] coins, int amount) {
if(amount==0) return 0;
int res=Integer.MAX_VALUE;
for(int coin:coins){
//金额不可达
if(amount-coin<0) continue;//如果当前节点不能凑整,则直接计算当前节点的同一层的相邻节点
int sub=coinChange(coins, amount-coin);//计算当前这个节点的能凑整的最小硬币数
//子问题无解
if(sub==-1) continue;//如果这个节点不能凑整,则跳过当前节点,计算当前节点的同一层的相邻节点
res=Math.min(res,sub+1);//比较得到每个节点的支路能凑整的最小硬币数,这个硬币数得加上当前节点的这个硬币
}
return(res==Integer.MAX_VALUE)?-1:res;//如果某个节点的所有支路都没法凑整(即在for循环里一直执行的是 if(amount-coin<0) continue; 这句话),则返回-1
}
}
300. 最长递增子序列
给你一个整数数组nums,找到其中最长严格递增子序列的长度。
子序列是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7]是数组[0,3,1,6,2,2,7]的子序列。
示例 1:
输入:nums = [10,9,2,5,3,7,101,18]
输出:4
解释:最长递增子序列是 [2,3,7,101],因此长度为 4 。
//动态规划
class Solution {
public int lengthOfLIS(int[] nums) {
int[] dp=new int[nums.length];
Arrays.fill(dp, 1);//极端情况下,子序列至少有1个
int res=Integer.MIN_VALUE;
for(int i=0;i<nums.length;i++){//分别是以0-num.length长度结尾的字符串
for(int j=0;j<i;j++){//从0-i之间找能构成的最长子序列
if(nums[j]<nums[i]){
dp[i]=Math.max(dp[i],dp[j]+1);//每次都找0-i的字符串的最长升序子序列,必须要把num[i]这个字符串包进去,以num[i]结尾
}
}
res=Math.max(res, dp[i]);//找最长的子序列
}
return res;
}
}
437. 路径总和 III
给定一个二叉树,它的每个结点都存放着一个整数值。
找出路径和等于给定数值的路径总数。
路径不需要从根节点开始,也不需要在叶子节点结束,但是路径方向必须是向下的(只能从父节点到子节点)。
二叉树不超过1000个节点,且节点数值范围是 [-1000000,1000000] 的整数。
示例:
root = [10,5,-3,3,2,null,11,3,-2,null,1], sum = 8
10
/ \
5 -3
/ \ \
3 2 11
/ \ \
3 -2 1
返回 3。和等于 8 的路径有:
1. 5 -> 3
2. 5 -> 2 -> 1
3. -3 -> 11
class Solution {
public int pathSum(TreeNode root, int targetSum) {//遍历二叉树的每一个节点,计算每个节点能有多少条路径
if(root==null) //递归结束条件
return 0;
int rootCount=count(root,targetSum);// 自己为开头的路径数
int leftCount=pathSum(root.left, targetSum);// 左边路径总数(相信他能算出来)
int rightCount=pathSum(root.right, targetSum);// 右边路径总数(相信他能算出来)
return rootCount+leftCount+rightCount;
}
private int count(TreeNode root, int targetSum) {//计算每一个节点能有多少条路径
if(root==null)
return 0;
int self=(root.val==targetSum)?1:0;// 我自己能不能独当一面,作为一条单独的路径呢?
int left=count(root.left, targetSum-root.val); // 左边的小老弟,你那边能凑几个 sum - node.val 呀?
int right=count(root.right, targetSum-root.val);// 右边的小老弟,你那边能凑几个 sum - node.val 呀?
return self+left+right;// 我这能凑这么多个
}
}
62. 不同路径
一个机器人位于一个m x n网格的左上角 (起始点在下图中标记为 “Start” )。
机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish” )。
问总共有多少条不同的路径?
示例 1:
输入:m = 3, n = 7
输出:28
class Solution {
public int uniquePaths(int m, int n) {
int[][] dp=new int[m][n];
for(int i=0;i<m;i++){
for(int j=0;j<n;j++){
if(i==0||j==0){
dp[i][j]=1;
}else{
dp[i][j]=dp[i-1][j]+dp[i][j-1];
}
}
}
return dp[m-1][n-1];
}
}
64. 最小路径和
给定一个包含非负整数的m x n 网格 grid,请找出一条从左上角到右下角的路径,使得路径上的数字总和为最小。
说明:每次只能向下或者向右移动一步。
示例 1:
输入:grid = [[1,3,1],[1,5,1],[4,2,1]]
输出:7
解释:因为路径 1→3→1→1→1 的总和最小。
class Solution {
public int minPathSum(int[][] grid) {
int m=grid.length;
int n=grid[0].length;
int[][] dp=new int[m][n];
dp[0][0]=grid[0][0];
for(int i=0;i<m;i++){
for(int j=0;j<n;j++){
if(i==0&&j==0){
continue;
}else if(j==0){
dp[i][0]=dp[i-1][0]+grid[i][0];
}else if(i==0){
dp[0][j]=dp[0][j-1]+grid[0][j];
}else{
dp[i][j]=Math.min(dp[i-1][j]+grid[i][j],dp[i][j-1]+grid[i][j]);
}
}
}
return dp[m-1][n-1];
}
}
116. 填充每个节点的下一个右侧节点指针
给定一个 完美二叉树 ,其所有叶子节点都在同一层,每个父节点都有两个子节点。二叉树定义如下:
struct Node {
int val;
Node left;
Node right;
Node next;
}
填充它的每个 next 指针,让这个指针指向其下一个右侧节点。如果找不到下一个右侧节点,则将 next 指针设置为NULL。
初始状态下,所有 next 指针都被设置为NULL。
*进阶:
- 你只能使用常量级额外空间。
- 使用递归解题也符合要求,本题中递归程序占用的栈空间不算做额外的空间复杂度。
示例:
输入:root = [1,2,3,4,5,6,7]
输出:[1,#,2,3,#,4,5,6,7,#]
解释:给定二叉树如图 A 所示,你的函数应该填充它的每个 next 指针,以指向其下一个右侧节点,如图 B 所示。序列化的输出按层序遍历排列,同一层节点由 next 指针连接,’#’ 标志着每一层的结束。
class Solution {
public Node connect(Node root) {
if(root==null) return null;
connectTwoNode(root.left,root.right);
return root;
}
// 定义:输入两个节点,将它俩连接起来
private void connectTwoNode(Node node1, Node node2) {
if (node1 == null || node2 == null) {
return;
}
/**** 前序遍历位置 ****/
// 将传入的两个节点连接
node1.next = node2;
// 连接相同父节点的两个子节点
connectTwoNode(node1.left, node1.right);
connectTwoNode(node2.left, node2.right);
// 连接跨越父节点的两个子节点
connectTwoNode(node1.right, node2.left);
}
}
654. 最大二叉树
给定一个不含重复元素的整数数组nums。一个以此数组直接递归构建的最大二叉树定义如下:
- 二叉树的根是数组nums中的最大元素。
- 左子树是通过数组中最大值左边部分递归构造出的最大二叉树。
- 右子树是通过数组中最大值右边部分递归构造出的最大二叉树。
返回有给定数组nums构建的最大二叉树。
示例 1:
输入:nums = [3,2,1,6,0,5]
输出:[6,3,5,null,2,0,null,null,1]
class Solution {
public TreeNode constructMaximumBinaryTree(int[] nums) {
return dfs(nums,0,nums.length-1);
}
private TreeNode dfs(int[] nums,int left,int right){
if(left>right)
return null;
int max=Integer.MIN_VALUE;
int maxIndex=Integer.MIN_VALUE;
// 找到数组中的最大值和对应的索引
for(int i=left;i<=right;i++){
if(nums[i]>max){
max=nums[i];
maxIndex=i;
}
}
TreeNode root=new TreeNode(nums[maxIndex]);
// 递归调用构造左右子树
root.left=dfs(nums, left,maxIndex-1);
root.right=dfs(nums, maxIndex+1,right);
return root;
}
}
652. 寻找重复的子树
给定一棵二叉树,返回所有重复的子树。对于同一类的重复子树,你只需要返回其中任意一棵的根结点即可。
两棵树重复是指它们具有相同的结构以及相同的结点值。
示例 1:
1
/ \
2 3
/ / \
4 2 4
/
4
下面是两个重复的子树:
2
/
4
和
4
因此,你需要以列表的形式返回上述重复子树的根结点。
class Solution {
public List<TreeNode> findDuplicateSubtrees(TreeNode root) {
HashMap<String,Integer> memory=new HashMap<>();//记录所有子树以及出现的次数
List<TreeNode> res=new ArrayList<>();//记录重复的子树根节点
dfs(root,memory,res);
return res;
}
private String dfs(TreeNode root,HashMap<String,Integer> memory,List<TreeNode> res) {
if(root==null)
return "#";
String left=dfs(root.left,memory,res);
String right=dfs(root.right,memory,res);
String subTree=left+","+right+","+root.val;
int freq = memory.getOrDefault(subTree, 0);
if(freq==1){// 多次重复也只会被加入结果集一次
res.add(root);
}
memory.put(subTree,freq+1);//给子树对应的出现次数加一
return subTree;
}
}
297. 二叉树的序列化与反序列化
序列化是将一个数据结构或者对象转换为连续的比特位的操作,进而可以将转换后的数据存储在一个文件或者内存中,同时也可以通过网络传输到另一个计算机环境,采取相反方式重构得到原数据。
请设计一个算法来实现二叉树的序列化与反序列化。这里不限定你的序列 / 反序列化算法执行逻辑,你只需要保证一个二叉树可以被序列化为一个字符串并且将这个字符串反序列化为原始的树结构。
提示:输入输出格式与 LeetCode 目前使用的方式一致,详情请参阅 LeetCode 序列化二叉树的格式。你并非必须采取这种方式,你也可以采用其他的方法解决这个问题。
示例 1:
输入:root = [1,2,3,null,null,4,5]
输出:[1,2,3,null,null,4,5]
//前序遍历
public class Codec {
String NULL="#";// 代表 null 空指针的字符
String SEP=",";// 代表分隔符的字符
public String serialize(TreeNode root) {
StringBuilder sb=new StringBuilder();
serialize(root,sb);
return sb.toString();
}
private void serialize(TreeNode root, StringBuilder sb) {
if(root==null){
sb.append(NULL).append(SEP);
return;
}
sb.append(root.val).append(SEP);//前序遍历代码
serialize(root.left,sb);
serialize(root.right,sb);
}
public TreeNode deserialize(String data) {
LinkedList<String> nodes=new LinkedList<>();
for(String node:data.split(SEP)){
nodes.addLast(node);
}
return deserialize(nodes);
}
private TreeNode deserialize(LinkedList<String> nodes) {
if(nodes.isEmpty())
return null;
// 列表最左侧就是根节点,从前往后取出元素
String node = nodes.removeFirst();
if(NULL.equals(node))
return null;
TreeNode root=new TreeNode(Integer.parseInt(node));
//先构造左子树,后构造右子树
root.left=deserialize(nodes);
root.right=deserialize(nodes);
return root;
}
}
//后序遍历
public class Codec {
String NULL="#";// 代表 null 空指针的字符
String SEP=",";// 代表分隔符的字符
public String serialize(TreeNode root) {
StringBuilder sb=new StringBuilder();
serialize(root,sb);
return sb.toString();
}
private void serialize(TreeNode root, StringBuilder sb) {
if(root==null){
sb.append(NULL).append(SEP);
return;
}
serialize(root.left,sb);
serialize(root.right,sb);
sb.append(root.val).append(SEP);//后序遍历位置
}
public TreeNode deserialize(String data) {
LinkedList<String> nodes=new LinkedList<>();
for(String node:data.split(SEP)){
nodes.addLast(node);
}
return deserialize(nodes);
}
private TreeNode deserialize(LinkedList<String> nodes) {
if(nodes.isEmpty())
return null;
// 列表最右侧就是根节点,从后往前取出元素
String node = nodes.removeLast();
if(NULL.equals(node))
return null;
TreeNode root=new TreeNode(Integer.parseInt(node));
// 先构造右子树,后构造左子树
root.right=deserialize(nodes);
root.left=deserialize(nodes);
return root;
}
}
//中序遍历
//中序遍历的方式行不通,序列化方法 serialize 依然容易,但无法实现反序列化方法 deserialize。
//层序遍历
public class Codec {
String NULL="#";// 代表 null 空指针的字符
String SEP=",";// 代表分隔符的字符
public String serialize(TreeNode root) {
if(root==null)
return "";
Queue<TreeNode> queue=new LinkedList<>();
StringBuilder sb=new StringBuilder();
// 初始化队列,将 root 加入队列
queue.add(root);
while(!queue.isEmpty()){
TreeNode node = queue.poll();
if(node==null){
sb.append(NULL).append(SEP);
continue;
}
sb.append(node.val).append(SEP);
queue.add(node.left);
queue.add(node.right);
}
return sb.toString();
}
public TreeNode deserialize(String data) {
if(data.isEmpty()) return null;
String[] nodes = data.split(SEP);
Queue<TreeNode> queue=new LinkedList<>();
// 第一个元素就是 root 的值
TreeNode root=new TreeNode(Integer.parseInt(nodes[0]));
// 队列 queue 记录父节点,将 root 加入队列
queue.add(root);
for(int i=1;i<nodes.length;){
// 队列中存的都是父节点
TreeNode node = queue.poll();
// 父节点对应的左侧子节点的值
String left=nodes[i++];
if(!NULL.equals(left)){
node.left=new TreeNode(Integer.parseInt(left));
queue.add(node.left);
}else{
node.left=null;
}
// 父节点对应的右侧子节点的值
String right=nodes[i++];
if(!NULL.equals(right)){
node.right=new TreeNode(Integer.parseInt(right));
queue.add(node.right);
}else{
node.right=null;
}
}
return root;
}
/**或:下面使用的是while循环搭配i++的反序列化二叉树
public TreeNode deserialize(String data) {
if(data.isEmpty()) return null;
String[] nodes = data.split(SEP);
Queue<TreeNode> queue=new LinkedList<>();
// 第一个元素就是 root 的值
int i=0;
TreeNode root=new TreeNode(Integer.parseInt(nodes[i++]));
// 队列 queue 记录父节点,将 root 加入队列
queue.add(root);
while (!queue.isEmpty()){
// 队列中存的都是父节点
TreeNode node = queue.poll();
// 父节点对应的左侧子节点的值
String left=nodes[i++];
if(!NULL.equals(left)){
node.left=new TreeNode(Integer.parseInt(left));
queue.add(node.left);
}else{
node.left=null;
}
// 父节点对应的右侧子节点的值
String right=nodes[i++];
if(!NULL.equals(right)){
node.right=new TreeNode(Integer.parseInt(right));
queue.add(node.right);
}else{
node.right=null;
}
}
return root;
}
*/
}
230. 二叉搜索树中第K小的元素
给定一个二叉搜索树的根节点root,和一个整数k,请你设计一个算法查找其中第 k个最小元素(从 1 开始计数)。
示例 1:
输入:root = [3,1,4,null,2], k = 1
输出:1
class Solution {
int count=0;// 记录当前元素的排名
int res=0;// 记录结果
public int kthSmallest(TreeNode root, int k) {
dfs(root, k);
return res;
}
private void dfs(TreeNode root, int k){
if(root==null)
return;
dfs(root.left, k);
if(k==count){ // 找到第 k 小的元素
res=root.val;
return;
}
dfs(root.right, k);
}
}
538. 把二叉搜索树转换为累加树
1038. 把二叉搜索树转换为累加树
给出二叉搜索树的根节点,该树的节点值各不相同,请你将其转换为累加树(Greater Sum Tree),使每个节点node 的新值等于原树中大于或等于 node.val 的值之和。
提醒一下,二叉搜索树满足下列约束条件:
- 节点的左子树仅包含键小于节点键的节点。
- 节点的右子树仅包含键大于节点键的节点。
- 左右子树也必须是二叉搜索树。
示例 1:
输入:[4,1,6,0,2,5,7,null,null,null,3,null,null,null,8]
输出:[30,36,21,36,35,26,15,null,null,null,33,null,null,null,8]
class Solution {
public TreeNode bstToGst(TreeNode root) {
dfs(root);
return root;
}
int sum=0;// 记录累加和
private void dfs(TreeNode root){
if(root==null){
return;
}
dfs(root.right); // 先递归遍历右子树
// 中序遍历代码位置
sum+=root.val;// 维护累加和
root.val=sum; // 将 BST 转化成累加树
dfs(root.left);// 后递归遍历左子树
}
}
98. 验证二叉搜索树
给定一个二叉树,判断其是否是一个有效的二叉搜索树。
假设一个二叉搜索树具有如下特征:
- 节点的左子树只包含小于当前节点的数。
- 节点的右子树只包含大于当前节点的数。
- 所有左子树和右子树自身必须也是二叉搜索树。
示例 1:
输入:
2
/ \
1 3
输出: true
class Solution {
public boolean isValidBST(TreeNode root) {
return isValidBST(root, null, null);
}
private boolean isValidBST(TreeNode root,TreeNode min,TreeNode max) {
if(root==null)
return true;
// 若 root.val 不符合 max 和 min 的限制,说明不是合法 BST
if(min!=null&&root.val<=min.val) return false;
if(max!=null&&root.val>=max.val) return false;
// 限定左子树的最大值是 root.val,右子树的最小值是 root.val
return isValidBST(root.left,min,root)&&isValidBST(root.right,root,max);
}
}
701. 二叉搜索树中的插入操作
给定二叉搜索树(BST)的根节点和要插入树中的值,将值插入二叉搜索树。 返回插入后二叉搜索树的根节点。 输入数据保证,新值和原始二叉搜索树中的任意节点值都不同。
注意,可能存在多种有效的插入方式,只要树在插入后仍保持为二叉搜索树即可。 你可以返回任意有效的结果。
示例 1:
输入:root = [4,2,7,1,3], val = 5
输出:[4,2,7,1,3,5]
解释:另一个满足题目要求可以通过的树是:
class Solution{
//因为涉及到要返回TreeNode类型,就要对递归调用的返回值进行接收。所以选择边递归边进行插入
public TreeNode insertIntoBST(TreeNode root, int val) {
if(root==null)// 找到空位置插入新节点
return new TreeNode(val);
// if (root.val == val)// BST 中一般不会插入已存在元素
if(root.val>val){
root.left=insertIntoBST(root.left, val);
}
if(root.val<val){
root.right=insertIntoBST(root.right, val);
}
return root;
}
}
450. 删除二叉搜索树中的节点
给定一个二叉搜索树的根节点root和一个值key,删除二叉搜索树中的 key 对应的节点,并保证二叉搜索树的性质不变。返回二叉搜索树(有可能被更新)的根节点的引用。
一般来说,删除节点可分为两个步骤:
- 首先找到需要删除的节点;
- 如果找到了,删除它。
说明:要求算法时间复杂度为 O(h),h 为树的高度。
示例:
root = [5,3,6,2,4,null,7]
key = 3
5
/ \
3 6
/ \ \
2 4 7
给定需要删除的节点值是 3,所以我们首先找到 3 这个节点,然后删除它。
一个正确的答案是 [5,4,6,2,null,null,7], 如下图所示。
5
/ \
4 6
/ \
2 7
另一个正确答案是 [5,2,6,null,4,null,7]。
5
/ \
2 6
\ \
4 7
class Solution {
public TreeNode deleteNode(TreeNode root, int key) {
if(root==null)
return null;
if(root.val==key){
if(root.right==null) return root.left;
if(root.left==null) return root.right;
//找到右子树的最小节点
TreeNode minNode=getMin(root.right);
// 把 root 改成 minNode
root.val=minNode.val;
// 转而去删除 minNode
root.right=deleteNode(root.right, minNode.val);
}
if(root.val>key){
root.left=deleteNode(root.left, key);
}
if(root.val<key){
root.right=deleteNode(root.right, key);
}
return root;
}
private TreeNode getMin(TreeNode root) {
// BST 最左边的就是最小的
while(root.left!=null)
root=root.left;
return root;
}
}
96. 不同的二叉搜索树
给你一个整数n,求恰由n个节点组成且节点值从1到n互不相同的二叉搜索树有多少种?返回满足题意的二叉搜索树的种数。
示例 1:
输入:n = 3
输出:5
class Solution {
public int numTrees(int n) {
int[][] memo=new int[n+1][n+1];
//计算闭区间 [1, n] 组成的 BST 个数
return count(1,n,memo);
}
//计算闭区间 [lo, hi] 组成的 BST 个数
private int count(int lo,int hi,int[][] memo) {
if(lo>hi)
return 1;
if(memo[lo][hi]!=0)
return memo[lo][hi];
int res=0;
for(int i=lo;i<=hi;i++){
// i 的值作为根节点 root
int left=count(lo,i-1,memo);
int right=count(i+1,hi,memo);
res+=left*right;// 左右子树的组合数乘积是 BST 的总数
}
memo[lo][hi]=res;
return res;
}
}
95. 不同的二叉搜索树 II
给定一个整数n,生成所有由 1 … n为节点所组成的二叉搜索树。
示例:
输入:3
输出:
[
[1,null,3,2],
[3,2,null,1],
[3,1,null,null,2],
[2,1,3],
[1,null,2,null,3]
]
解释:
以上的输出对应以下 5 种不同结构的二叉搜索树:
1 3 3 2 1
\ / / / \ \
3 2 1 1 3 2
/ / \ \
2 1 2 3
提示:
0 <= n <= 8
class Solution {
public List<TreeNode> generateTrees(int n) {
if (n == 0) return new ArrayList<>();
return build(1,n);
}
private List<TreeNode> build(int lo, int hi) {
List<TreeNode> res=new ArrayList<>();
if(lo>hi){
res.add(null);
return res;
}
// 1、穷举 root 节点的所有可能。
for(int i=lo;i<=hi;i++){
//2、递归构造出左右子树的所有合法 BST。
List<TreeNode> leftNodes=build(lo, i-1);
List<TreeNode> rightNodes=build(i+1, hi);
// 3、给 root 节点穷举所有左右子树的组合。
for(TreeNode left:leftNodes){
for(TreeNode right:rightNodes){
TreeNode root=new TreeNode(i); // i 作为根节点 root 的值
root.left=left;
root.right=right;
res.add(root);
}
}
}
return res;
}
}
148. 排序链表
给你链表的头结点 head ,请将其按升序排列并返回排序后的链表。
进阶:你可以在 O(n log n)时间复杂度和常数级空间复杂度下,对链表进行排序吗?
示例 1:
输入:head = [4,2,1,3]
输出:[1,2,3,4]
困难
32. 最长有效括号
给你一个只包含’(‘ 和’)’ 的字符串,找出最长有效(格式正确且连续)括号子串的长度。
示例 1:
输入:s = “(()”输出:2解释:最长有效括号子串是 “()”
//动规
class Solution {
public int longestValidParentheses(String s) {
if(s.length()<1) return 0;
char[] charArray = s.toCharArray();
int[] dp=new int[s.length()];
int max=0;
for(int i=1;i<charArray.length;i++){
if(charArray[i]==')'){
if(charArray[i-1]=='('){
dp[i]=(i>=2?dp[i-2]:0)+2;
}else if(i-1-dp[i-1]>=0&&s.charAt(i-1-dp[i-1])=='('){
dp[i]=dp[i-1]+(i>=dp[i-1]+2?dp[i-dp[i-1]-2]:0)+2;
}
}
max=Math.max(max, dp[i]);
}
return max;
}
}
或
//栈
class Solution {
public int longestValidParentheses(String s) {
Stack<Integer> stack=new Stack<>();
stack.push(-1);//用于初始化,便于后续求差
int max=0;
for(int i=0;i<s.length();i++){
if(s.charAt(i)=='('){
stack.push(i);
}else{
stack.pop();
if(stack.isEmpty()) {
stack.push(i);//不断更新没有被匹配的右括号的位置,也就是不断扔掉右括号
}
else {
max=Math.max(max, i-stack.peek());
}
}
}
return max;
}
}
或
//利用两个计数器left 和 right
class Solution {
public int longestValidParentheses(String s) {
int open=0,close=0,max=0;
for(int i=0;i<s.length();i++){
char ch=s.charAt(i);
if(ch=='(')
open++;
else
close++;
if(open==close)
max=Math.max(max, open*2);
else if(close>open)
open=close=0;
}
open=close=0;
for(int i=s.length()-1;i>=0;i--){
char ch=s.charAt(i);
if(ch=='(')
open++;
else
close++;
if(open==close)
max=Math.max(max, open*2);
else if(open>close)
open=close=0;
}
return max;
}
}
25. K 个一组翻转链表
给你一个链表,每 k 个节点一组进行翻转,请你返回翻转后的链表。
k 是一个正整数,它的值小于或等于链表的长度。
如果节点总数不是 k 的整数倍,那么请将最后剩余的节点保持原有顺序。
进阶:
- 你可以设计一个只使用常数额外空间的算法来解决此问题吗?
- 你不能只是单纯的改变节点内部的值,而是需要实际进行节点交换。
示例 1:
输入:head = [1,2,3,4,5], k = 2输出:[2,1,4,3,5]
//迭代反转链表+递归分解子问题
class Solution {
public ListNode reverseKGroup(ListNode head, int k) {//这个还是用的递归
if(head==null) return null;
ListNode a=head,b=head;
for(int i=0;i<k;i++){ // 区间 [a, b) 包含 k 个待反转元素
if(b==null) return head;//不足 k 个,不需要反转,base case
b=b.next;
}
ListNode reverseNode = reverse(a, b);//此处利用迭代反转链表,反转前 k 个元素
a.next=reverseKGroup(b,k);//递归分解为子问题,//递归反转后续链表并连接起来
return reverseNode;
}
//迭代反转链表,反转区间 [a, b) 的元素,注意是左闭右开
private ListNode reverse(ListNode a,ListNode b){//迭代反转链表
ListNode pre=null;
ListNode cur=a;
while (a!=b){//while 终止条件要注意
ListNode temp=a.next;
a.next=pre;
pre=a;
a=temp;
}
return pre;//返回反转后的头结点
}
}
或
//递归反转链表+递归分解子问题
class Solution {
public ListNode reverseKGroup(ListNode head, int k) {//这个还是用的递归
if(head==null) return null;
ListNode a=head,b=head;
for(int i=0;i<k;i++){
if(b==null) return head;
b=b.next;
}
ListNode newNode = traverse(a, k);
a.next=reverseKGroup(b,k);//递归分解为子问题
return newNode;
}
ListNode after;
private ListNode traverse(ListNode head,int m) {//递归反转链表的前n个节点
if(m==1){
after=head.next;
return head;
}
ListNode last=traverse(head.next,m-1);
head.next.next=head;
head.next=after;
return last;
}
}
72. 编辑距离
给你两个单词 word1 和 word2,请你计算出将 word1 转换成 word2 所使用的最少操作数 。
你可以对一个单词进行如下三种操作:
插入一个字符
删除一个字符
替换一个字符
示例 1:
输入:word1 = “horse”, word2 = “ros”
输出:3
解释:
horse -> rorse (将 ‘h’ 替换为 ‘r’)
rorse -> rose (删除 ‘r’)
rose -> ros (删除 ‘e’)
//动态规划(最优解)
class Solution {
public int minDistance(String word1, String word2) {
int len1=word1.length();
int len2=word2.length();
int[][] dp=new int[len1+1][len2+1];//dp[i][j]代表修改s1[0..i-1]到s2[0...j-1]需要的次数
for(int i=1;i<=len1;i++){//行改变,代表从 s1[0..i-1]到成为 空串 需要删除的次数
dp[i][0]=i;
}
for(int j=1;j<=len2;j++){//列改变,代表从 空串 到 s2[0..j-1] 需要增加的次数
dp[0][j]=j;
}
for(int i=1;i<=len1;i++){
for(int j=1;j<=len2;j++){
if(word1.charAt(i-1)==word2.charAt(j-1)){
dp[i][j]=dp[i-1][j-1];
}else{
dp[i][j]=min(1+dp[i-1][j-1],1+dp[i-1][j],1+dp[i][j-1]);
}
}
}
return dp[len1][len2];
}
private int min(int a,int b,int c){
return Math.min(Math.min(a, b), c);
}
}
//带备忘录的递归
class Solution {
public int minDistance(String word1, String word2) {
int[][] memory=new int[word1.length()][word2.length()];
return dp(word1.length()-1,word2.length()-1,word1,word2,memory);
}
private int dp(int i, int j, String word1, String word2,int[][] memory) {
if(i==-1) return 1+j;
if(j==-1) return 1+i;
if(memory[i][j]!=0) return memory[i][j];
if(word1.charAt(i)==word2.charAt(j)){
memory[i][j]=dp(i-1, j-1, word1, word2,memory);
}else{
memory[i][j]=min(dp(i,j-1,word1,word2,memory)+1,
dp(i-1,j,word1,word2,memory)+1,
dp(i-1,j-1,word1,word2,memory)+1);
}
return memory[i][j];
}
private int min(int a,int b,int c){
return Math.min(Math.min(a, b), c);
}
}
//递归:时间复杂度太大
class Solution {
public int minDistance(String word1, String word2) {
return dp(word1.length()-1,word2.length()-1,word1,word2);//从两个字符串的末尾开始比较
}
private int dp(int i, int j, String word1, String word2) {
if(i==-1) return 1+j;//s1先遍历到头部,则把s2剩余的都插入到s1的头部
if(j==-1) return 1+i;//s2先遍历到头部,则把s1剩余的全部都删除掉
if(word1.charAt(i)==word2.charAt(j)){
return dp(i-1, j-1, word1, word2);//直接跳过,啥都不做,操作数不变
}else{//求删除、替换和插入三者最小的,这三种都会导致操作数+1
return Math.min(Math.min(dp(i,j-1,word1,word2)+1,dp(i-1,j,word1,word2)+1),
dp(i-1,j-1,word1,word2)+1);
}
}
}
72.编辑距离拓展
计算最小编辑距离的路径是怎样的:即从s1变成s2针对s1字符的具体操作步骤
例如:都是按字符串的索引顺序进行操作的
从 horse 到 ros ,horse需要经过:替换 h ,跳过 o ,删除 r ,跳过 o,删除s
路径为 [3, 4, 2, 4, 2]
从 ros 到 horse,ros需要经过:替换 h ,跳过 o ,插入 r ,跳过 s,插入e
路径为 [3, 4, 1, 4, 1]
1代表插入; 2代表删除; 3代表替换; 4代表啥都不做
class Soluton{
public Node[][] minDistance(String word1, String word2) {
int len1=word1.length();
int len2=word2.length();
Node[][] dp=new Node[len1+1][len2+1];
for(int i=0;i<dp.length;i++){
for(int j=0;j<dp[0].length;j++){
dp[i][j]=new Node();
}
}
for(int i=1;i<=len1;i++){
dp[i][0].val=i;
dp[i][0].choice=2;//删除
}
for(int j=1;j<=len2;j++){
dp[0][j].val=j;
dp[0][j].choice=1;//插入
}
for(int i=1;i<=len1;i++){
for(int j=1;j<=len2;j++){
if(word1.charAt(i-1)==word2.charAt(j-1)){//跳过,啥也不做
dp[i][j].val=dp[i-1][j-1].val;
dp[i][j].choice=4;
}else{
dp[i][j].val=min(1+dp[i-1][j-1].val,1+dp[i-1][j].val,1+dp[i][j-1].val);
if(dp[i][j].val==1+dp[i-1][j-1].val){//替换
dp[i][j].choice=3;
}else if(dp[i][j].val==1+dp[i-1][j].val){//删除i
dp[i][j].choice=2;
}else{//插入
dp[i][j].choice=1;
}
}
}
}
return dp;
}
private int min(int a,int b,int c){
return Math.min(Math.min(a, b), c);
}
private void path(Node[][] res,int len1, int len2,ArrayList<Integer> path) {
while (len1>0&&len2>0){
int choice=res[len1][len2].choice;
path.add(0,choice);
if(choice==1){
len2--;
}else if(choice==2){
len1--;
}else if(choice==3||choice==4){
len1--;
len2--;
}
}
}
}
class Node {
int val=0;
/** 1代表插入; 2代表删除; 3代表替换; 4代表啥都不做 */
int choice=0;
}
10. 正则表达式匹配
给你一个字符串 s 和一个字符规律 p,请你来实现一个支持’.’ 和 ‘*’ 的正则表达式匹配。
- ‘.’匹配任意单个字符
- ‘*’匹配零个或多个前面的那一个元素
所谓匹配,是要涵盖 整个 字符串 s的,而不是部分字符串。
示例 1:
输入:s = “aa” p = “a”
输出:false
解释:“a” 无法匹配 “aa” 整个字符串。
//带备忘录的递归1
class Solution{
public boolean isMatch(String s,String p) {
boolean[][] memory=new boolean[s.length()][p.length()];
return isMatch(0, 0, s, p, memory);
}
public boolean isMatch(int i,int j,String s,String p,boolean[][] memory) {
if(j==p.length())
return i==s.length();
if(i<s.length()&&j<p.length()&&memory[i][j])
return memory[i][j];
boolean firstMatch=(i<s.length())&&(s.charAt(i)==p.charAt(j)||p.charAt(j)=='.');
boolean res;
if(p.length()>=j+2&&p.charAt(j+1)=='*'){
res=isMatch(i, j+2,s,p,memory) ||(firstMatch&&isMatch(i+1,j,s,p,memory));
}else{
res=firstMatch&&isMatch(i+1,j+1,s,p,memory);
}
if(i<s.length()&&j<p.length())
memory[i][j]=res;
return res;
}
}
//带备忘录的递归2
class Solution {
HashMap<String,Boolean> map=new HashMap<>();
public boolean isMatch(String s, String p) {
int m=s.length(),n=p.length();
return isMatch(s,0,p,0);
}
private boolean isMatch(String s,int i,String p,int j){
if(j==p.length()){
return i==s.length();
}
if(i==s.length()){
if((p.length()-j)%2==1){//全都是*x*x的形式
return false;
}
for(;j+1<p.length();j+=2){
if(p.charAt(j+1)!='*'){
return false;
}
}
return true;
}
String key=i+","+j;
if(map.containsKey(key)){
return map.get(key);
}
boolean res;
if(s.charAt(i)==p.charAt(j)||p.charAt(j)=='.'){//匹配
if(j+1<p.length()&&p.charAt(j+1)=='*') {//匹配0次或多次
res=isMatch(s, i,p,j+2)||isMatch(s, i+1,p,j);
}else{
res=isMatch(s, i+1,p,j+1);
}
}else{//不匹配
if(j+1<p.length()&&p.charAt(j+1)=='*'){//只能匹配0次
res=isMatch(s, i,p,j+2);
}else{
res=false;
}
}
map.put(key, res);
return res;
}
}
或
//动态规划
class Solution{
public boolean isMatch2(String s, String p) {
int m=s.length()+1;
int n=p.length()+1;
boolean[][] dp=new boolean[m][n];
dp[0][0]=true;
for(int i=2;i<n;i+=2){//判断矩阵首行的比较结果,同样首列的结果也是固定的
dp[0][i]=dp[0][i-2]&&p.charAt(i-1)=='*';
}
for(int i=1;i<m;i++){ //因为*前面至少要有一个数,所以在p串中至少在第二位,所以 j-2 不会越界,同样的, i-1 也不会越界
for(int j=1;j<n;j++){
if(p.charAt(j-1)=='*'){
dp[i][j]=(dp[i][j-2])||(dp[i-1][j]&&s.charAt(i-1)==p.charAt(j-2))||(dp[i-1][j]&&p.charAt(j-2)=='.');
}else{
dp[i][j]=(dp[i-1][j-1]&&s.charAt(i-1)==p.charAt(j-1))||(dp[i-1][j-1]&&p.charAt(j-1)=='.');
}
}
}
return dp[m-1][n-1];
}
}
或
//递归版的,还未优化
class Solution{
public boolean isMatch(String s, String p) {
if(p.length()==0)
return s.length()==0;
boolean firstMatch=(s.length()!=0)&&(p.charAt(0)==s.charAt(0)||p.charAt(0)=='.');
if(p.length()>=2&&p.charAt(1)=='*'){
return isMatch(s, p.substring(2)) ||
(firstMatch&&isMatch(s.substring(1),p));
}
return firstMatch&&isMatch(s.substring(1),p.substring(1));
}
}
1373. 二叉搜索子树的最大键值和
给你一棵以 root 为根的 二叉树 ,请你返回任意 二叉搜索子树的最大键值和。
二叉搜索树的定义如下:
- 任意节点的左子树中的键值都 小于 此节点的键值。
- 任意节点的右子树中的键值都大于 此节点的键值。
- 任意节点的左子树和右子树都是二叉搜索树。
示例 1:
输入:root = [1,4,3,2,4,2,5,null,null,null,null,null,null,4,6]
输出:20
解释:键值为 3 的子树是和最大的二叉搜索树。
class Solution{
// 全局变量,记录 BST 最大节点之和
int maxNum;
public int maxSumBST(TreeNode root) {
traverse(root);
return maxNum;
}
//0:是否是bst 1:最小值 2:最大值 3:树的和
// 函数返回 int[]{ isBST, min, max, sum}
private int[] traverse(TreeNode root) {
if(root==null){
return new int[]{1,Integer.MIN_VALUE,Integer.MAX_VALUE,0};
}
// 递归计算左右子树
int[] left=traverse(root.left);
int[] right=traverse(root.right);
int[] res=new int[4];
// 这个 if 在判断以 root 为根的二叉树是不是 BST
if(left[0]==1&&right[0]==1&&root.val>left[1]&&root.val<right[2]){
// 以 root 为根的二叉树是 BST
res[0]=1;
res[1]=Math.max(right[1], root.val);// 计算以 root 为根的这棵 BST 的最大值
res[2]=Math.min(left[2], root.val); // 计算以 root 为根的这棵 BST 的最小值
res[3]=root.val+left[3]+right[3];// 计算以 root 为根的这棵 BST 所有节点之和
maxNum=Math.max(maxNum, res[3]);// 更新全局变量
}else{
// 以 root 为根的二叉树不是 BST
res[0]=0; // 其他的值都没必要计算了,因为用不到
}
return res;
}
}
系列问题
一、买卖股票
121. 买卖股票的最佳时机
给定一个数组prices,它的第 i个元素 prices[i]表示一支给定股票第i天的价格。
你只能选择某一天买入这只股票,并选择在未来的某一个不同的日子卖出该股票。设计一个算法来计算你所能获取的最大利润。
返回你可以从这笔交易中获取的最大利润。如果你不能获取任何利润,返回0。
示例 1:
输入:[7,1,5,3,6,4]
输出:5
解释:在第 2 天(股票价格 = 1)的时候买入,在第 5 天(股票价格 = 6)的时候卖出,最大利润 = 6-1 = 5 。
注意利润不能是 7-1 = 6, 因为卖出价格需要大于买入价格;同时,你不能在买入前卖出股票。
解题关键点:
关键不在于最大值还是最小值,而是数字的添加和减少。添加新数时,可以根据已有最值,推导出新的最值;而减少数字时,不一定能直接推出新的最值,不得不重新遍历。
class Solution {
public int maxProfit(int[] prices) {
if(prices.length<1) return 0;
int res=0;
int curMin=prices[0];
for(int i=1;i<prices.length;i++){
curMin=Math.min(curMin, prices[i]);
res=Math.max(res, prices[i]-curMin);
}
return res;
}
}
122. 买卖股票的最佳时机 II
给定一个数组prices,其中 prices[i]是一支给定股票第i天的价格。
设计一个算法来计算你所能获取的最大利润。你可以尽可能地完成更多的交易(多次买卖一支股票)。
注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
示例 1:
输入: prices = [7,1,5,3,6,4]
输出: 7
解释: 在第 2 天(股票价格 = 1)的时候买入,在第 3 天(股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5-1 = 4 。
随后,在第 4 天(股票价格 = 3)的时候买入,在第 5 天(股票价格 = 6)的时候卖出, 这笔交易所能获得利润 = 6-3 = 3 。
解题关键点:
遇到这种无穷 for 循环的情况,就是使用递归的强烈暗示。我们上一题的框架只能解决一次买卖的最大收益,现在的问题是,进行一次股票卖出后,下一次应该在什么时候交易呢?这个问题和原问题具有相同结构,规模减小,典型的递归场景。只要给原框架稍加改动即可。
//贪心算法:时间复杂度为O(N),能赚一点是一点
class Solution {
public int maxProfit(int[] prices) {
int maxProfit=0;
for(int i=1;i<prices.length;i++){
if(prices[i]>prices[i-1])
maxProfit+=(prices[i]-prices[i-1]);
}
return maxProfit;
}
}
//递归:时间复杂度为O(N^2),超时
class Solution {
public int maxProfit(int[] prices) {
int[] memo=new int[prices.length];
Arrays.fill(memo, -1);
return dfs(prices, 0, prices.length-1,memo);
}
private int dfs(int[] prices,int lo,int hi,int[] memo) {
if(lo>hi)
return 0;
if(memo[lo]!=-1) return memo[lo];//代表在lo处买入能获得的最大利润
int res=0;
int curMin=prices[lo];
for(int i=lo+1;i<=hi;i++){
curMin=Math.min(curMin, prices[i]);
res=Math.max(res, dfs(prices,i+1,hi,memo)+prices[i]-curMin);
}
memo[lo]=res;
return res;
}
}
123. 买卖股票的最佳时机 III
给定一个数组,它的第i个元素是一支给定的股票在第i天的价格。
设计一个算法来计算你所能获取的最大利润。你最多可以完成 两笔 交易。
注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
示例 1:
输入:prices = [3,3,5,0,0,3,1,4]
输出:6
解释:在第 4 天(股票价格 = 0)的时候买入,在第 6 天(股票价格 = 3)的时候卖出,这笔交易所能获得利润 = 3-0 = 3 。
随后,在第 7 天(股票价格 = 1)的时候买入,在第 8 天 (股票价格 = 4)的时候卖出,这笔交易所能获得利润 = 4-1 = 3 。
//递归:时间复杂度为O(2*N^2),超时
class Solution {
public int maxProfit(int[] prices) {
int[][] memo=new int[prices.length][2];
for(int i=0;i<memo.length;i++){
Arrays.fill(memo[i],-1);
}
return dfs(prices, 0, prices.length-1, 1, memo);
}
private int dfs(int[] prices,int lo,int hi,int k,int[][] memo){
if(lo>hi)
return 0;
if(k==-1)
return 0;
if(memo[lo][k]!=-1)
return memo[lo][k];
int res=0;
int curMin=prices[lo];
for(int i=lo+1;i<=hi;i++){
curMin=Math.min(curMin, prices[i]);
res=Math.max(res, dfs(prices, i+1, hi, k-1, memo)+prices[i]-curMin);
}
memo[lo][k]=res;
return res;
}
}
188. 买卖股票的最佳时机 IV
给定一个整数数组 prices,它的第i个元素 prices[i]是一支给定的股票在第i天的价格。
设计一个算法来计算你所能获取的最大利润。你最多可以完成k笔交易。
注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
示例 1:
输入:k = 2, prices = [2,4,1]
输出:2
解释:在第 1 天 (股票价格 = 2) 的时候买入,在第 2 天 (股票价格 = 4) 的时候卖出,这笔交易所能获得利润 = 4-2 = 2 。
//递归:时间复杂度为O(K*N^2),没超时
class Solution {
public int maxProfit(int k, int[] prices) {
int[][] memo=new int[prices.length][k];
for(int i=0;i<memo.length;i++){
Arrays.fill(memo[i],-1);
}
return dfs(prices, 0, prices.length-1, k-1, memo);
}
private int dfs(int[] prices,int lo,int hi,int k,int[][] memo){
if(lo>hi)
return 0;
if(k==-1)
return 0;
if(memo[lo][k]!=-1)
return memo[lo][k];
int res=0;
int curMin=prices[lo];
for(int i=lo+1;i<=hi;i++){
curMin=Math.min(curMin, prices[i]);
//需要加上共买k次的约束
res=Math.max(res, dfs(prices, i+1, hi, k-1, memo)+prices[i]-curMin);
}
memo[lo][k]=res;
return res;
}
}
309. 最佳买卖股票时机含冷冻期
给定一个整数数组,其中第 i 个元素代表了第 i 天的股票价格 。
设计一个算法计算出最大利润。在满足以下约束条件下,你可以尽可能地完成更多的交易(多次买卖一支股票):
- 你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
- 卖出股票后,你无法在第二天买入股票 (即冷冻期为 1 天)。
示例:
输入: [1,2,3,0,2]
输出: 3
解释: 对应的交易状态为: [买入, 卖出, 冷冻期, 买入, 卖出]
//递归:时间复杂度为O(N^2),没超时
class Solution {
public int maxProfit(int[] prices) {
int[] memo=new int[prices.length];
Arrays.fill(memo, -1);
return dfs(prices, 0, prices.length-1, memo);
}
private int dfs(int[] prices,int lo,int hi,int[] memo){
if(lo>hi)
return 0;
if(memo[lo]!=-1)
return memo[lo];
int res=0;
int curMin=prices[lo];
for(int i=lo+1;i<=hi;i++){
curMin=Math.min(curMin, prices[i]);
//要隔一天买入:修改下次的买入时间为i+2即可
res=Math.max(res, dfs(prices, i+2, hi, memo)+prices[i]-curMin);
}
memo[lo]=res;
return res;
}
}
714. 买卖股票的最佳时机含手续费
给定一个整数数组 prices,其中第 i 个元素代表了第 i 天的股票价格 ;非负整数 fee代表了交易股票的手续费用。
你可以无限次地完成交易,但是你每笔交易都需要付手续费。如果你已经购买了一个股票,在卖出它之前你就不能再继续购买股票了。
返回获得利润的最大值。
注意:这里的一笔交易指买入持有并卖出股票的整个过程,每笔交易你只需要为支付一次手续费。
示例 1:
输入: prices = [1, 3, 2, 8, 4, 9], fee = 2
输出: 8
解释: 能够达到的最大利润:
在此处买入 prices[0] = 1
在此处卖出 prices[3] = 8
在此处买入 prices[4] = 4
在此处卖出 prices[5] = 9
总利润: ((8 - 1) - 2) + ((9 - 4) - 2) = 8.
//递归:时间复杂度为O(N^2),超时
class Solution {
public int maxProfit(int[] prices, int fee) {
int[] memo=new int[prices.length];
Arrays.fill(memo, -1);
return dfs(prices, 0, prices.length-1, memo,fee);
}
private int dfs(int[] prices,int lo,int hi,int[] memo,int fee){
if(lo>hi)
return 0;
if(memo[lo]!=-1)
return memo[lo];
int res=0;
int curMin=prices[lo];
for(int i=lo+1;i<=hi;i++){
curMin=Math.min(curMin, prices[i]);
//每次卖出需要手续费,套进框架,把手续费从利润中减掉即可。
res=Math.max(res, dfs(prices, i+1, hi, memo,fee)+prices[i]-curMin-fee);
}
memo[lo]=res;
return res;
}
}
二、双指针技巧(快慢指针、左右子针、滑动窗口)
剑指 Offer 57. 和为s的两个数字
输入一个递增排序的数组和一个数字s,在数组中查找两个数,使得它们的和正好是s。如果有多对数字的和等于s,则输出任意一对即可。
示例 1:
输入:nums = [2,7,11,15], target = 9
输出:[2,7] 或者 [7,2]
class Solution {
public int[] twoSum(int[] nums, int target) {
int left=0;
int right=nums.length-1;
while (left<=right){
int sum=nums[left]+nums[right];
if(sum==target)
return new int[]{nums[left],nums[right]};
else if(sum<target)
left++;
else if(sum>target)
right--;
}
return new int[]{-1,-1};
}
}
148. 排序链表
给你链表的头结点 head ,请将其按升序排列并返回排序后的链表。
进阶:
- 你可以在 O(n log n)时间复杂度和常数级空间复杂度下,对链表进行排序吗?
示例 1:
输入:head = [4,2,1,3]
输出:[1,2,3,4]
class Solution {
public ListNode sortList(ListNode head) {
return sortList(head, null);
}
public ListNode sortList(ListNode head,ListNode tail) {//排序链表
//细分链表到一个节点或者为空时就返回
if(head==null)
return head;
if(head.next==tail){
head.next=null;
return head;
}
ListNode mid=halfNode(head, tail);
//不断排序链表
ListNode list1=sortList(head,mid);
ListNode list2=sortList(mid,tail);
//归并链表
ListNode merged=mergeList(list1,list2);
return merged;
}
private ListNode halfNode(ListNode node,ListNode tail){//找链表的中点
ListNode fast=node;
ListNode slow=node;
while (fast!=tail&&fast.next!=tail){
fast=fast.next.next;
slow=slow.next;
}
return slow;
}
private ListNode mergeList(ListNode list1,ListNode list2){//合并两个链表
ListNode newNode=new ListNode(-1);
ListNode temp=newNode;
while (list1!=null&&list2!=null){
if(list1.val<list2.val){
temp.next=list1;
list1=list1.next;
temp=temp.next;
}else {
temp.next=list2;
list2=list2.next;
temp=temp.next;
}
}
temp.next=(list1==null?list2:list1);
return newNode.next;
}
}
26. 删除有序数组中的重复项
给你一个有序数组nums,请你原地删除重复出现的元素,使每个元素只出现一次,返回删除后数组的新长度。
不要使用额外的数组空间,你必须在原地修改输入数组并在使用 O(1) 额外空间的条件下完成。
说明:
为什么返回数值是整数,但输出的答案是数组呢?
请注意,输入数组是以「引用」方式传递的,这意味着在函数里修改输入数组对于调用者是可见的。
你可以想象内部操作如下:
// nums 是以“引用”方式传递的。也就是说,不对实参做任何拷贝
int len = removeDuplicates(nums);
// 在函数里修改输入数组对于调用者是可见的。
// 根据你的函数返回的长度, 它会打印出数组中 该长度范围内 的所有元素。
for (int i = 0; i < len; i++) {
print(nums[i]);
}
示例 1:
输入:nums = [1,1,2]
输出:2, nums = [1,2]
解释:函数应该返回新的长度 2 ,并且原数组 nums 的前两个元素被修改为 1, 2 。不需要考虑数组中超出新长度后面的元素。
class Solution {
public int removeDuplicates(int[] nums) {
if(nums.length==0)
return 0;
int fast=0,slow=0;
while(fast<nums.length){
// 维护 nums[0..slow] 无重复
if(nums[fast]!=nums[slow]){
slow++;
nums[slow]=nums[fast];
}
fast++;
}
//数组长度为索引 + 1
return slow+1;
}
}
83. 删除排序链表中的重复元素
存在一个按升序排列的链表,给你这个链表的头节点head,请你删除所有重复的元素,使每个元素只出现一次。
返回同样按升序排列的结果链表。
示例 1:
输入:head = [1,1,2]
输出:[1,2]
class Solution {
public ListNode deleteDuplicates(ListNode head) {
if(head==null)
return null;
ListNode fast=head,slow=head;
while(fast!=null){
if(fast.val!=slow.val){
slow.next=fast;//类似 nums[slow] = nums[fast];
slow=slow.next;//类似 slow++;
}
//类似 fast++
fast=fast.next;
}
// 断开与后面重复元素的连接
slow.next=null;
return head;
}
}
27. 移除元素
给你一个数组nums和一个值val,你需要原地移除所有数值等于 val的元素,并返回移除后数组的新长度。
不要使用额外的数组空间,你必须仅使用O(1)额外空间并原地修改输入数组。
元素的顺序可以改变。你不需要考虑数组中超出新长度后面的元素。
输入:nums = [3,2,2,3], val = 3
输出:2, nums = [2,2]
解释:函数应该返回新的长度 2, 并且 nums 中的前两个元素均为 2。你不需要考虑数组中超出新长度后面的元素。例如,函数返回的新长度为 2 ,而 nums = [2,2,3,3] 或 nums = [2,2,0,0],也会被视作正确答案。
class Solution {
public int removeElement(int[] nums, int val) {
int fast=0,slow=0;
while(fast<nums.length){
if(nums[fast]!=val){
nums[slow]=nums[fast];
slow++;
}
fast++;
}
return slow;
}
}
283. 移动零
给定一个数组nums,编写一个函数将所有0移动到数组的末尾,同时保持非零元素的相对顺序。
示例:
输入: [0,1,0,3,12]
输出: [1,3,12,0,0]
说明:
- 必须在原数组上操作,不能拷贝额外的数组。
尽量减少操作次数。
class Solution {
public void moveZeroes(int[] nums) {
// 去除 nums 中的所有 0, 返回去除 0 之后的数组长度
int p=removeElement(nums,0);
// 将 p 之后的所有元素赋值为 0
for(;p<nums.length;p++)
nums[p]=0;
}
//借用第27. 移除元素
private int removeElement(int[] nums, int val) {
int fast=0,slow=0;
while(fast<nums.length){
if(nums[fast]!=val){
nums[slow]=nums[fast];
slow++;
}
fast++;
}
return slow;
}
}
或
//更优一点的方式
class Solution {
public void moveZeroes(int[] nums) {
int fast=0,slow=0;
while(fast<nums.length){
if(nums[fast]!=0){
swap(nums,slow,fast);
slow++;
}
fast++;
}
}
private void swap(int[] nums,int i,int j){
int temp=nums[i];
nums[i]=nums[j];
nums[j]=temp;
}
}
2Sum 3Sum 4Sum 问题(快慢双指针)
两数之和I(剑指 Offer 57. 和为s的两个数字)
输入一个递增排序的数组和一个数字s,在数组中查找两个数,使得它们的和正好是s。如果有多对数字的和等于s,则输出任意一对即可。
示例 1:
输入:nums = [2,7,11,15], target = 9
输出:[2,7] 或者 [7,2]class Solution {
public int[] twoSum(int[] nums, int target) {
// 先对数组排序
Arrays.sort(nums);
// 左右指针
int lo=0,hi=nums.length-1;
while (lo<hi){
int sum=nums[lo]+nums[hi];
// 根据 sum 和 target 的比较,移动左右指针
if(sum<target){
lo++;
}else if(sum>target){
hi--;
}else{
return new int[]{nums[lo],nums[hi]};
}
}
return new int[]{};
}
}
两数之和ii
nums中可能有多对儿元素之和都等于target,请你的算法返回所有和为target的元素对儿,其中不能出现重复。
比如说输入为nums = [1,3,1,2,2,3], target = 4,那么算法返回的结果就是:[[1,3],[2,2]]。
对于修改后的问题,关键难点是现在可能有多个和为target的数对儿,还不能重复,比如上述例子中[1,3]和[3,1]就算重复,只能算一次。class Solution{
public List<List<Integer>> twoSumTarget(int[] nums, int target) {
List<List<Integer>> res=new ArrayList<>();
Arrays.sort(nums);
int lo=0,hi=nums.length-1;
while (lo<hi){
int sum=nums[lo]+nums[hi];
int left=nums[lo],right=nums[hi];
if(sum<target){
while (lo<hi&&nums[lo]==left) lo++;
}else if(sum>target){
while (lo<hi&&nums[hi]==right) hi--;
}else{
ArrayList<Integer> temp=new ArrayList<>();
temp.add(nums[lo]);
temp.add(nums[hi]);
res.add(temp);
while (lo<hi&&nums[lo]==left) lo++;
while (lo<hi&&nums[hi]==right) hi--;
}
}
return res;
}
}
15. 三数之和
给你一个包含n个整数的数组 nums,判断 nums 中是否存在三个元素a,b,c ,使得 _a + b + c =_0 ?请你找出所有和为0且不重复的三元组。
注意:答案中不可以包含重复的三元组。
示例 1:
输入:nums = [-1,0,1,2,-1,-4]
输出:[[-1,-1,2],[-1,0,1]]class Solution {
public List<List<Integer>> threeSum(int[] nums) {
return threeSumTarget(nums,0);
}
public List<List<Integer>> threeSumTarget(int[] nums,int target) {
// 数组得排个序
Arrays.sort(nums);
List<List<Integer>> res=new ArrayList<>();
// 穷举 threeSum 的第一个数
for(int i=0;i<nums.length;i++){
// 对 target - nums[i] 计算 twoSum
List<List<Integer>> twoSum = twoSumTarget(nums, i + 1, target - nums[i]);
// 如果存在满足条件的二元组,再加上 nums[i] 就是结果三元组
for(List<Integer> temp:twoSum){
temp.add(nums[i]);
res.add(temp);
}
// 跳过第一个数字重复的情况,否则会出现重复结果
while(i<nums.length-1&&nums[i]==nums[i+1]) i++;
}
return res;
}
//nums 中所有和为 target 的二元组
public List<List<Integer>> twoSumTarget(int[] nums, int start,int target) {
List<List<Integer>> res=new ArrayList<>();
// 左指针改为从 start 开始,其他不变
int lo=start,hi=nums.length-1;
while (lo<hi){
int sum=nums[lo]+nums[hi];
int left=nums[lo],right=nums[hi];
if(sum<target){
while (lo<hi&&nums[lo]==left) lo++;
}else if(sum>target){
while (lo<hi&&nums[hi]==right) hi--;
}else{
ArrayList<Integer> temp=new ArrayList<>();
temp.add(nums[lo]);
temp.add(nums[hi]);
res.add(temp);
while (lo<hi&&nums[lo]==left) lo++;
while (lo<hi&&nums[hi]==right) hi--;
}
}
return res;
}
}
18. 四数之和
给定一个包含 n个整数的数组 nums 和一个目标值 target,判断 nums 中是否存在四个元素a,b,c 和d ,使得 a+b+c+d 的值与 target 相等?找出所有满足条件且不重复的四元组。
注意:答案中不可以包含重复的四元组。
示例 1:
输入:nums = [1,0,-1,0,-2,2], target = 0
输出:[[-2,-1,1,2],[-2,0,0,2],[-1,0,0,1]]class Solution {
public List<List<Integer>> fourSum(int[] nums, int target) {
// 数组需要排序
Arrays.sort(nums);
List<List<Integer>> res=new ArrayList<>();
// 穷举 fourSum 的第一个数
for(int i=0;i<nums.length;i++){
// 对 target - nums[i] 计算 threeSum
List<List<Integer>> threeSum = threeSumTarget(nums, i + 1, target - nums[i]);
// 如果存在满足条件的三元组,再加上 nums[i] 就是结果四元组
for(List<Integer> temp:threeSum){
temp.add(nums[i]);
res.add(temp);
}
// fourSum 的第一个数不能重复
while(i<nums.length-1&&nums[i]==nums[i+1]) i++;
}
return res;
}
public List<List<Integer>> threeSumTarget(int[] nums,int start,int target) {
List<List<Integer>> res=new ArrayList<>();
// i 从 start 开始穷举,其他都不变
for(int i=start;i<nums.length;i++){
List<List<Integer>> twoSum = twoSumTarget(nums, i + 1, target - nums[i]);
for(List<Integer> temp:twoSum){
temp.add(nums[i]);
res.add(temp);
}
while(i<nums.length-1&&nums[i]==nums[i+1]) i++;
}
return res;
}
public List<List<Integer>> twoSumTarget(int[] nums, int start,int target) {
List<List<Integer>> res=new ArrayList<>();
int lo=start,hi=nums.length-1;
while (lo<hi){
int sum=nums[lo]+nums[hi];
int left=nums[lo],right=nums[hi];
if(sum<target){
while (lo<hi&&nums[lo]==left) lo++;
}else if(sum>target){
while (lo<hi&&nums[hi]==right) hi--;
}else{
ArrayList<Integer> temp=new ArrayList<>();
temp.add(nums[lo]);
temp.add(nums[hi]);
res.add(temp);
while (lo<hi&&nums[lo]==left) lo++;
while (lo<hi&&nums[hi]==right) hi--;
}
}
return res;
}
}
1两数之和(补充的:这里是返回角标,不是返回数值,所以解法与上面是不同的)
给定一个整数数组nums 和一个整数目标值target,请你在该数组中找出和为目标值的那 两个 整数,并返回它们的数组下标。
你可以假设每种输入只会对应一个答案。但是,数组中同一个元素在答案里不能重复出现。
你可以按任意顺序返回答案。
示例 1:
输入:nums = [2,7,11,15], target = 9
输出:[0,1]
解释:因为 nums[0] + nums[1] == 9 ,返回 [0, 1] 。class Solution {
public int[] twoSum(int[] nums, int target) {
HashMap<Integer,Integer> map=new HashMap<>();
for(int i=0;i<nums.length;i++){
map.put(nums[i],i);
}
for(int i=0;i<nums.length;i++){
if(map.containsKey(target-nums[i])&&map.get(target-nums[i])!=i)
return new int[]{i,map.get(target-nums[i])};
}
return new int[]{};
}
}
三、滑动窗口算法(双指针的高级应用部分)
76. 最小覆盖子串
给你一个字符串s、一个字符串t。返回s中涵盖t所有字符的最小子串。如果s中不存在涵盖t所有字符的子串,则返回空字符串””。
注意:如果s中存在这样的子串,我们保证它是唯一的答案。
示例 1:
输入:s = “ADOBECODEBANC”, t = “ABC”
输出:“BANC”class Solution {
public String minWindow(String s, String t) {
int left=0,right=0;
HashMap<Character,Integer> window=new HashMap<>();
HashMap<Character,Integer> needs=new HashMap<>();
for(int i=0;i<t.length();i++){
needs.put(t.charAt(i), needs.getOrDefault(t.charAt(i), 0)+1);
}
int valid=0;
// 记录最小覆盖子串的起始索引及长度
int start=0,len=Integer.MAX_VALUE;
while (right<s.length()) {
// c 是将移入窗口的字符
char c = s.charAt(right);
// 右移窗口
right++;
// 进行窗口内数据的一系列更新
if (needs.containsKey(c)) {
window.put(c, window.getOrDefault(c, 0) + 1);
if (window.get(c) .equals(needs.get(c)) )
valid++;
}
// 判断左侧窗口是否要收缩
while (valid == needs.size()) {
// 在这里更新最小覆盖子串
if (right - left < len) {
len = right - left;
start = left;
}
// d 是将移出窗口的字符
char d = s.charAt(left);
// 左移窗口
left++;
// 进行窗口内数据的一系列更新
if (needs.containsKey(d)) {
if (window.get(d).equals(needs.get(d)))
valid--;
window.put(d, window.getOrDefault(d, 0) - 1);
}
}
}
// 返回最小覆盖子串
return len==Integer.MAX_VALUE?"":s.substring(start,start+len);
}
}
567. 字符串的排列
给定两个字符串 s1 和 s2,写一个函数来判断s2是否包含s1的排列。
换句话说,第一个字符串的排列之一是第二个字符串的子串。
示例 1:
输入: s1 = “ab” s2 = “eidbaooo”
输出: True
解释: s2 包含 s1 的排列之一 (“ba”).class Solution {
public boolean checkInclusion(String s1, String s2) {
HashMap<Character,Integer> window=new HashMap<>();
HashMap<Character,Integer> needs=new HashMap<>();
for(int i=0;i<s1.length();i++){
needs.put(s1.charAt(i), needs.getOrDefault(s1.charAt(i), 0)+1);
}
int left=0,right=0;
int valid=0;
while(right<s2.length()){
char c=s2.charAt(right);
right++;
// 进行窗口内数据的一系列更新
if(needs.containsKey(c)){
window.put(c, window.getOrDefault(c, 0)+1);
if(window.get(c).equals(needs.get(c)))
valid++;
}
// 判断左侧窗口是否要收缩
while(right-left>=s1.length()){// 这句写为while(right-left==s1.length()){也可以
// 在这里判断是否找到了合法的子串
if(valid==needs.size()){
return true;
}
char d=s2.charAt(left);
left++;
// 进行窗口内数据的一系列更新
if(needs.containsKey(d)){
if(needs.get(d).equals(window.get(d)))
valid--;
window.put(d, window.getOrDefault(d,0 )-1);
}
}
}
// 未找到符合条件的子串
return false;
}
}
438. 找到字符串中所有字母异位词
给定一个字符串 s 和一个非空字符串 p,找到 s 中所有是 p 的字母异位词的子串,返回这些子串的起始索引。
字符串只包含小写英文字母,并且字符串 s 和p 的长度都不超过 20100。
说明:
- 字母异位词指字母相同,但排列不同的字符串。
- 不考虑答案输出的顺序。
示例 1:
输入:
s: “cbaebabacd” p: “abc”
输出:
[0, 6]
解释:
起始索引等于 0 的子串是 “cba”, 它是 “abc” 的字母异位词。
起始索引等于 6 的子串是 “bac”, 它是 “abc” 的字母异位词。
class Solution {
public List<Integer> findAnagrams(String s, String p) {
List<Integer> res=new ArrayList<>();// 记录结果
HashMap<Character,Integer> window=new HashMap<>();
HashMap<Character,Integer> need=new HashMap<>();
for(int i=0;i<p.length();i++){
need.put(p.charAt(i), need.getOrDefault(p.charAt(i), 0)+1);
}
int left=0,right=0,valid=0;
while(right<s.length()){
char c=s.charAt(right);
right++;
// 进行窗口内数据的一系列更新
if(need.containsKey(c)){
window.put(c, window.getOrDefault(c, 0)+1);
if(need.get(c).equals(window.get(c)))
valid++;
}
// 判断左侧窗口是否要收缩
while(right-left>=p.length()){
// 当窗口符合条件时,把起始索引加入 res
if(valid==need.size()){
res.add(left);
}
char d=s.charAt(left);
left++;
// 进行窗口内数据的一系列更新
if(need.containsKey(d)){
if(need.get(d).equals(window.get(d)))
valid--;
window.put(d, window.getOrDefault(d, 0)-1);
}
}
}
return res;
}
}
3. 无重复字符的最长子串
给定一个字符串,请你找出其中不含有重复字符的 最长子串 的长度。
示例 1:
输入: s = “abcabcbb”
输出: 3
解释: 因为无重复字符的最长子串是 “abc”,所以其长度为 3。
class Solution {
public int lengthOfLongestSubstring(String s) {
int res=0;// 记录结果
HashMap<Character,Integer> window=new HashMap<>();
int left=0,right=0;
while(right<s.length()){
char c=s.charAt(right);
right++;
//进行窗口内数据的一系列更新
window.put(c, window.getOrDefault(c, 0)+1);
// 判断左侧窗口是否要收缩
while (window.get(c)>1){
char d=s.charAt(left);
left++;
// 进行窗口内数据的一系列更新
window.put(d, window.getOrDefault(d, 0)-1);
}
// 在这里更新答案
res=Math.max(res, (right-left));
}
return res;
}
}
四、BFS算法框架
111. 二叉树的最小深度
给定一个二叉树,找出其最小深度。
最小深度是从根节点到最近叶子节点的最短路径上的节点数量。
说明:叶子节点是指没有子节点的节点。
示例 1:
输入:root = [3,9,20,null,null,15,7]
输出:2
class Solution {
public int minDepth(TreeNode root) {
if(root==null)
return 0;
LinkedList<TreeNode> queue=new LinkedList<>();
queue.offerLast(root);
int res=1;//root 本身就是一层,depth 初始化为 1
while(!queue.isEmpty()){
//将当前队列中的所有节点向四周扩散
for(int i=queue.size();i>0;i--){
TreeNode node = queue.pollFirst();
//判断是否到达终点
if(node.left==null&&node.right==null)
return res;
//将 cur 的相邻节点加入队列
if(node.left!=null)
queue.add(node.left);
if(node.right!=null)
queue.add(node.right);
}
//这里增加步数
res++;
}
return res;
}
}
752. 打开转盘锁
你有一个带有四个圆形拨轮的转盘锁。每个拨轮都有10个数字:’0’, ‘1’, ‘2’, ‘3’, ‘4’, ‘5’, ‘6’, ‘7’, ‘8’, ‘9’。每个拨轮可以自由旋转:例如把’9’变为 ‘0’,’0’变为’9’。每次旋转都只能旋转一个拨轮的一位数字。
锁的初始数字为’0000’,一个代表四个拨轮的数字的字符串。
列表deadends包含了一组死亡数字,一旦拨轮的数字和列表里的任何一个元素相同,这个锁将会被永久锁定,无法再被旋转。
字符串target代表可以解锁的数字,你需要给出最小的旋转次数,如果无论如何不能解锁,返回 -1。
示例 1:
输入:deadends = [“0201”,”0101”,”0102”,”1212”,”2002”], target = “0202”
输出:6
解释:
可能的移动序列为 “0000” -> “1000” -> “1100” -> “1200” -> “1201” -> “1202” -> “0202”。
注意 “0000” -> “0001” -> “0002” -> “0102” -> “0202” 这样的序列是不能解锁的,
因为当拨动到 “0102” 时这个锁就会被锁定。
class Solution {
String plusOne(String s,int i){//往上拨
char[] ch = s.toCharArray();
if(ch[i]=='9'){
ch[i]='0';
}else{
ch[i]+=1;
}
return new String(ch);
}
String minusOne(String s,int i){//往下拨
char[] ch = s.toCharArray();
if(ch[i]=='0'){
ch[i]='9';
}else{
ch[i]-=1;
}
return new String(ch);
}
public int openLock(String[] deadends, String target) {
// 记录已经穷举过的密码,防止走回头路
HashSet<String> visited=new HashSet<>();
// 记录需要跳过的死亡密码
HashSet<String> deads=new HashSet<>();
for(String dead:deadends)
deads.add(dead);
LinkedList<String> queue=new LinkedList<>();
queue.offerLast("0000");
visited.add("0000");
// 从起点开始启动广度优先搜索
int step=0;
while(!queue.isEmpty()){
int size=queue.size();
//将当前队列中的所有节点向周围扩散
for(int i=0;i<size;i++){
String cur = queue.pollFirst();
//判断是否到达终点
if(deads.contains(cur))
continue;
if(cur.equals(target))
return step;
//将一个节点的未遍历相邻节点加入队列
for(int j=0;j<4;j++){
String up=plusOne(cur, j);
if(!visited.contains(up)){
queue.offerLast(up);
visited.add(up);
}
String down=minusOne(cur, j);
if(!visited.contains(down)){
queue.offerLast(down);
visited.add(down);
}
}
}
//在这里增加步数
step++;
}
//如果穷举完都没找到目标密码,那就是找不到了
return -1;
}
}
773. 滑动谜题
在一个 2 x 3 的板上(board)有 5 块砖瓦,用数字1~5来表示, 以及一块空缺用 0 来表示.
一次移动定义为选择 0 与一个相邻的数字(上下左右)进行交换.
最终当板 board 的结果是 [[1,2,3],[4,5,0]] 谜板被解开。
给出一个谜板的初始状态,返回最少可以通过多少次移动解开谜板,如果不能解开谜板,则返回 -1 。
输入:board = [[4,1,2],[5,0,3]]
输出:5
解释:
最少完成谜板的最少移动次数是 5 ,
提示:
class Solution {
public int slidingPuzzle(int[][] board) {
int m=2,n=3;
String start="";
String target="123450";
// 将 2x3 的数组转化成字符串
for(int i=0;i<m;i++){
for(int j=0;j<n;j++){
start=start.concat(board[i][j]+"");
}
}
// 记录一维字符串的相邻索引
int[][] neighbor={
{1,3},
{0,2,4},
{1,5},
{0,4},
{1,3,5},
{2,4}
};
LinkedList<String> queue=new LinkedList<>();
queue.offerLast(start);
HashSet<String> visited=new HashSet<>();
visited.add(start);
int step=0;
while(!queue.isEmpty()){
int size=queue.size();
for(int i=0;i<size;i++){
String cur = queue.pollFirst();
// 判断是否达到目标局面
if(cur.equals(target))
return step;
int index0=0;//找出当前0的位置
for(;cur.charAt(index0)!='0';index0++);
// 将数字 0 和相邻的数字交换位置
for(int j=0;j<neighbor[index0].length;j++){
String newCur=swap(cur,index0,neighbor[index0][j]);
// 防止走回头路
if(!visited.contains(newCur)){
queue.offerLast(newCur);
visited.add(newCur);
}
}
}
step++;
}
return -1;
}
private String swap(String cur, int i, int j) {
char[] ch = cur.toCharArray();
char temp=ch[i];
ch[i]=ch[j];
ch[j]=temp;
return new String(ch);
}
}
五、区间相关问题
1288. 删除被覆盖区间
给你一个区间列表,请你删除列表中被其他区间所覆盖的区间。
只有当 c <= a 且 b <= d 时,我们才认为区间 [a,b)被区间 [c,d)覆盖。
在完成所有删除操作后,请你返回列表中剩余区间的数目。
示例:
输入:intervals = [[1,4],[3,6],[2,8]]
输出:2
解释:区间 [3,6] 被区间 [2,8] 覆盖,所以它被删除了。
//labuladong完整版
class Solution {
public int removeCoveredIntervals(int[][] intervals) {
// 按照起点升序排列,起点相同时降序排列
Arrays.sort(intervals, (a, b) -> {
if (a[0] == b[0]) {
return b[1] - a[1];
}
return a[0] - b[0];
});
// 记录合并区间的起点和终点
int left = intervals[0][0];
int right = intervals[0][1];
int res = 0;
for (int i = 1; i < intervals.length; i++) {
int[] intv = intervals[i];
// 情况一,找到覆盖区间
if (left <= intv[0] && right >= intv[1]) {
res++;
}
// 情况二,找到相交区间,合并
if (right >= intv[0] && right <= intv[1]) {
right = intv[1];
}
// 情况三,完全不相交,更新起点和终点
if (right < intv[0]) {
left = intv[0];
right = intv[1];
}
}
return intervals.length - res;
}
}
//我的简化版本
class Solution {
public int removeCoveredIntervals(int[][] intervals) {
// 按照起点升序排列,起点相同时降序排列
Arrays.sort(intervals, (a,b)->{
if(a[0]==b[0]){
return b[1]-a[1];
}
return a[0]-b[0];
});
// 记录合并区间的起点和终点
int left=intervals[0][0];
int right=intervals[0][1];
int res=0;
for(int i=1;i<intervals.length;i++){
int[] intv=intervals[i];
if(left<=intv[0]&&right>=intv[1]){ // 情况一,找到覆盖区间
res++;
}else{//合并其余情况
// 情况二,找到相交区间,合并
// 情况三,完全不相交,更新起点和终点
left=intv[0];
right=intv[1];
}
}
return intervals.length-res;
}
}
56. 合并区间
以数组intervals表示若干个区间的集合,其中单个区间为intervals[i] = [starti, endi]。请你合并所有重叠的区间,并返回一个不重叠的区间数组,该数组需恰好覆盖输入中的所有区间。
示例 1:
输入:intervals = [[1,3],[2,6],[8,10],[15,18]]
输出:[[1,6],[8,10],[15,18]]
解释:区间 [1,3] 和 [2,6] 重叠, 将它们合并为 [1,6].
class Solution {
public int[][] merge(int[][] intervals) {
if(intervals.length==0)
return new int[0][];
//按区间的 start 升序排列
Arrays.sort(intervals,(a,b)->a[0]-b[0]);
List<int[]> res=new ArrayList<>();
res.add(intervals[0]);
for(int i=1;i<intervals.length;i++){
int[] cur=intervals[i];
//res 中最后一个元素的引用来比较
if(cur[0]<=res.get(res.size()-1)[1])
res.get(res.size()-1)[1]=Math.max(res.get(res.size()-1)[1],cur[1]);
else
//直接添加进结果,作为下一个待合并区间
res.add(cur);
}
return res.toArray(new int[res.size()][]);
}
}
986. 区间列表的交集
给定两个由一些闭区间组成的列表,firstList和secondList,其中firstList[i] = [starti, endi]而 secondList[j] = [startj, endj]。每个区间列表都是成对不相交的,并且已经排序。
返回这两个区间列表的交集。
形式上,闭区间[a, b](其中 a <= b)表示实数 x 的集合,而 a <= x <= b。
两个闭区间的交集是一组实数,要么为空集,要么为闭区间。例如,[1, 3]和[2, 4]的交集为[2, 3]。
示例 1:
输入:firstList = [[0,2],[5,10],[13,23],[24,25]], secondList = [[1,5],[8,12],[15,24],[25,26]]
输出:[[1,2],[5,5],[8,10],[15,23],[24,24],[25,25]]
class Solution {
public int[][] intervalIntersection(int[][] firstList, int[][] secondList) {
List<int[]> res=new ArrayList<>();
int i=0,j=0;
while(i<firstList.length&&j<secondList.length){
//取出两端区间的端点
int a1=firstList[i][0],a2=firstList[i][1];
int b1=secondList[j][0],b2=secondList[j][1];
//算出交集,加入 res
if(a2>=b1&&b2>=a1){
int[] temp=new int[]{Math.max(a1,b1),Math.min(a2,b2)};
res.add(temp);
}
//i与j进行递增的时机
int test=(b2<a2)?j++:i++;
}
return res.toArray(new int[res.size()][]);
}
}
六、单调栈
496. 下一个更大元素 I
给你两个没有重复元素的数组 nums1和 nums2 ,其中nums1 是 nums2 的子集。
请你找出nums1 中每个元素在 nums2 中的下一个比其大的值。
nums1 中数字 x 的下一个更大元素是指 x 在 nums2 中对应位置的右边的第一个比 x大的元素。如果不存在,对应位置输出-1。
示例 1:
输入: nums1 = [4,1,2], nums2 = [1,3,4,2].
输出: [-1,3,-1]
解释:
对于 num1 中的数字 4 ,你无法在第二个数组中找到下一个更大的数字,因此输出 -1 。
对于 num1 中的数字 1 ,第二个数组中数字1右边的下一个较大数字是 3 。
对于 num1 中的数字 2 ,第二个数组中没有下一个更大的数字,因此输出 -1 。
class Solution {
public int[] nextGreaterElement(int[] nums1, int[] nums2) {
HashMap<Integer,Integer> map=new HashMap<>();
Stack<Integer> s=new Stack<>();
// 倒着往栈里放
for(int i=nums2.length-1;i>=0;i--){
// 判定个子高矮
while(!s.isEmpty()&&s.peek()<=nums2[i]){
// 矮个起开,反正也被挡着了。。。
s.pop();
}
// nums[i] 身后的 next great number
map.put(nums2[i],s.isEmpty()?-1:s.peek());
s.push(nums2[i]);
}
int[] res=new int[nums1.length];
for(int i=0;i<nums1.length;i++){
res[i]=map.getOrDefault(nums1[i],-1);
}
return res;
}
}
503. 下一个更大元素 II
给定一个循环数组(最后一个元素的下一个元素是数组的第一个元素),输出每个元素的下一个更大元素。数字 x 的下一个更大的元素是按数组遍历顺序,这个数字之后的第一个比它更大的数,这意味着你应该循环地搜索它的下一个更大的数。如果不存在,则输出 -1。
示例 1:
输入: [1,2,1]
输出: [2,-1,2]
解释: 第一个 1 的下一个更大的数是 2;
数字 2 找不到下一个更大的数;
第二个 1 的下一个最大的数需要循环搜索,结果也是 2。
class Solution {
public int[] nextGreaterElements(int[] nums) {
Stack<Integer> s=new Stack<>();
int n=nums.length;
int[] res=new int[n];
//把这个循环数组「拉直」(复制该序列的前 n 个元素拼接在原序列的后面。)
//求得最后一个元素后的 next great number
for(int i=2*n-1;i>=0;i--){
while(!s.isEmpty()&&s.peek()<=nums[i%n]){
s.pop();
}
res[i%n]=s.isEmpty()?-1:s.peek();
s.push(nums[i%n]);
}
return res;
}
}
556. 下一个更大元素 III
给你一个正整数 n,请你找出符合条件的最小整数,其由重新排列n中存在的每位数字组成,并且其值大于n。如果不存在这样的正整数,则返回-1。
注意,返回的整数应当是一个32 位整数,如果存在满足题意的答案,但不是32 位整数,同样返回-1。
示例 1:
输入:n = 12
输出:21
739. 每日温度
请根据每日气温列表,重新生成一个列表。对应位置的输出为:要想观测到更高的气温,至少需要等待的天数。如果气温在这之后都不会升高,请在该位置用 0来代替。
例如,给定一个列表 temperatures = [73, 74, 75, 71, 69, 72, 76, 73],你的输出应该是 [1, 1, 4, 2, 1, 1, 0, 0]。
提示:气温列表长度的范围是 [1, 30000]。每个气温的值的均为华氏度,都是在 [30, 100] 范围内的整数。
class Solution {
public int[] dailyTemperatures(int[] temperatures) {
// 这里放元素索引,而不是元素
Stack<Integer> s=new Stack<>();
int[] res=new int[temperatures.length];
for(int i=res.length-1;i>=0;i--){
while(!s.isEmpty()&&temperatures[s.peek()]<=temperatures[i]){
s.pop();
}
// 得到索引间距
res[i]=s.isEmpty()?0:(s.peek()-i);
// 将索引入栈,而不是元素
s.push(i);
}
return res;
}
}
七、随机读取元素相关
380. 常数时间插入、删除和获取随机元素
设计一个支持在平均 时间复杂度O(1) 下,执行以下操作的数据结构。
- insert(val):当元素 val 不存在时,向集合中插入该项。
- remove(val):元素 val 存在时,从集合中移除该项。
getRandom:随机返回现有集合中的一项。每个元素应该有相同的概率被返回。
class RandomizedSet {
HashMap<Integer,Integer> map; // 记录每个元素对应在 map 中的索引
List<Integer> nums; // 存储元素的值
Random rand;//获取随机数
public RandomizedSet() {
map=new HashMap<>();
nums=new ArrayList<>();
rand=new Random();
}
public boolean insert(int val) {
if(map.containsKey(val)){// 若 val 已存在,不用再插入
return false;
}
map.put(val,nums.size()); // 记录 val 对应的索引值
nums.add(val);// 若 val 不存在,插入到 nums 尾部,
return true;
}
public boolean remove(int val) {
if(!map.containsKey(val)){ // 若 val 不存在,不用再删除
return false;
}
int index=map.get(val); // 先拿到 val 的索引
int lastNum=nums.get(nums.size()-1);
nums.set(index,lastNum);// 将最后一个元素放到索引index的位置处
nums.remove(nums.size()-1);// 在数组中删除最后一个元素
map.put(lastNum,index);//记得修改哈希表的索引值
map.remove(val);// 删除元素 val 对应的索引
return true;
}
public int getRandom() {
return nums.get(rand.nextInt(nums.size()));
}
}
381. O(1) 时间插入、删除和获取随机元素 - 允许重复
设计一个支持在平均 时间复杂度 O(1) 下, 执行以下操作的数据结构。
注意: 允许出现重复元素。insert(val):向集合中插入元素 val。
- remove(val):当 val 存在时,从集合中移除一个 val。
getRandom:从现有集合中随机获取一个元素。每个元素被返回的概率应该与其在集合中的数量呈线性相关。
class RandomizedCollection {
HashMap<Integer,HashSet<Integer>> map;
Random rand;
List<Integer> nums;//可以存放重复元素
public RandomizedCollection() {
map=new HashMap<>();
nums=new ArrayList<>();
rand=new Random();
}
public boolean insert(int val) {
nums.add(val);
HashSet<Integer> set=map.getOrDefault(val,new HashSet<>());
set.add(nums.size()-1);//存入当前val所在的角标
map.put(val,set);//把val及其对应的角标集合放进map中,方便查找
return set.size()==1;
}
public boolean remove(int val) {
if(!map.containsKey(val)){
return false;
}
//先获取到val对应的脚标集合
HashSet<Integer> set=map.get(val);
Iterator<Integer> it=set.iterator();
//取出Set集合中遍历到的第一个角标并移除
int index=it.next();
set.remove(index);
//取到nums列表里的最后一个元素,并从移除
int lastNum=nums.get(nums.size()-1);
map.get(lastNum).remove(nums.size()-1);
//交换最后一个元素,其实就是单方面的覆盖
nums.set(index,lastNum);
//这里保证如果删除的是最后一个,则不再把角标添加进去
if(index<nums.size()-1){
map.get(lastNum).add(index);
}
//如果某个数对应的角标已经没有了,删除key
if(set.size()==0){
map.remove(val);
}
nums.remove(nums.size()-1);
return true;
}
public int getRandom() {
return nums.get(rand.nextInt(nums.size()));
}
}
710. 黑名单中的随机数
给定一个包含[0,n)中不重复整数的黑名单blacklist,写一个函数从[0, n)中返回一个不在blacklist中的随机整数。
对它进行优化使其尽量少调用系统方法Math.random()。
提示:1 <= n <= 1000000000
- 0 <= blacklist.length < min(100000, N)
- [0, n) 不包含n,详细参见 interval notation) 。
示例 1:
输入:
[“Solution”,”pick”,”pick”,”pick”]
[[1,[]],[],[],[]]
输出:[null,0,0,0]
class Solution {
Random rand;
int sz;
HashMap<Integer,Integer> map;
public Solution(int n, int[] blacklist) {
rand=new Random();
sz=n-blacklist.length;
map=new HashMap<>();
for(int black:blacklist){
map.put(black,666);
}
int last=n-1;
for(int black:blacklist){
if(black>=sz)
continue;
while(map.containsKey(last)){
last--;
}
map.put(black,last);
last--;
}
}
public int pick() {
int num=rand.nextInt(sz);
if(map.containsKey(num)){
return map.get(num);
}
return num;
}
}
382. 链表随机节点
给定一个单链表,随机选择链表的一个节点,并返回相应的节点值。保证每个节点被选的概率一样。
进阶:如果链表十分大且长度未知,如何解决这个问题?你能否使用常数级空间复杂度实现?
示例:
// 初始化一个单链表 [1,2,3].
ListNode head = new ListNode(1);
head.next = new ListNode(2);
head.next.next = new ListNode(3);
Solution solution = new Solution(head);
// getRandom()方法应随机返回1,2,3中的一个,保证每个元素被返回的概率相等。
solution.getRandom();
class Solution {
ListNode head;
public Solution(ListNode head) {
this.head=head;
}
public int getRandom() {
Random rand=new Random();
ListNode temp=head;
int i=0;
int res=0;
// while 循环遍历链表
while(temp!=null){
// 生成一个 [0, i) 之间的整数,这个整数等于 0 的概率就是 1/i
if(rand.nextInt(++i)==0){
res=temp.val;
}
temp=temp.next;
}
return res;
}
}
382. 链表随机节点(拓展)
如果要随机选择k个数,只要在第i个元素处以k/i的概率选择该元素,以1 - k/i的概率保持原有选择即可。
代码如下:
class Solution{
public int[] getRandom(ListNode head, int k) {
Random r = new Random();
int[] res = new int[k];
ListNode p = head;
// 前 k 个元素先默认选上
for (int j = 0; j < k && p != null; j++) {
res[j] = p.val;
p = p.next;
}
int i = k;
// while 循环遍历链表
while (p != null) {
// 生成一个 [0, i) 之间的整数
int j = r.nextInt(++i);
// 这个整数小于 k 的概率就是 k/i
if (j < k) {
res[j] = p.val;
}
p = p.next;
}
return res;
}
}
398. 随机数索引
给定一个可能含有重复元素的整数数组,要求随机输出给定的数字的索引。 您可以假设给定的数字一定存在于数组中。
注意:数组大小可能非常大。 使用太多额外空间的解决方案将不会通过测试。
示例:
int[] nums = new int[] {1,2,3,3,3};
Solution solution = new Solution(nums);
// pick(3) 应该返回索引 2,3 或者 4。每个索引的返回概率应该相等。
solution.pick(3);
// pick(1) 应该返回 0。因为只有nums[0]等于1。
solution.pick(1);
class Solution {
int[] nums;
public Solution(int[] nums) {
this.nums=nums;
}
public int pick(int target) {
Random rand=new Random();
int res=0;
int count=0;
//遍历这个数组
for(int i=0;i<nums.length;i++){
//一旦目标数有多个,则要让它取到每个索引值的概率相等
if(nums[i]==target){
count++;
if(rand.nextInt(count)==0){
res=i;
}
}
}
return res;
}
}
384. 打乱数组
给你一个整数数组 nums ,设计算法来打乱一个没有重复元素的数组。
实现Solutionclass:
- Solution(int[] nums)使用整数数组nums初始化对象
- int[] reset()重设数组到它的初始状态并返回
- int[] shuffle()返回数组随机打乱后的结果
示例:
输入
[“Solution”, “shuffle”, “reset”, “shuffle”]
[[[1, 2, 3]], [], [], []]
输出
[null, [3, 1, 2], [1, 2, 3], [1, 3, 2]]
class Solution {
int[] nums;
int[] temp;
Random rand;
int n;
public Solution(int[] nums) {
this.nums=nums;
n=nums.length;
this.temp=nums.clone();
rand=new Random();
}
public int[] reset() {
nums=temp;
temp=temp.clone();
return temp;
}
public int[] shuffle() {
for(int i=0;i<n;i++){
int index=rand.nextInt(n-i)+i;//生成i到n范围内的随机数
swap(i,index);
}
return nums;
}
private void swap(int i,int j){
int temp=nums[i];
nums[i]=nums[j];
nums[j]=temp;
}
}