数组
26. 删除有序数组中的重复项
- https://leetcode.cn/problems/remove-duplicates-from-sorted-array/
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-J8ALKfcQ-1653289135551)(C:\Users\黄佳林\AppData\Roaming\Typora\typora-user-images\image-20220513101134390.png)]
解题思路
**思路1:**类似双指针,定义一个变量来记录数组真实的长度,将非重复数组不断装填进去
思路2:
解法: 双指针
首先注意数组是有序的,那么重复的元素一定会相邻。
要求删除重复元素,实际上就是将不重复的元素移到数组的左侧。
考虑用 2 个指针,一个在前记作 p,一个在后记作 q,算法流程如下:
1.比较 p 和 q 位置的元素是否相等。
如果相等,q 后移 1 位
如果不相等,将 q 位置的元素复制到 p+1 位置上,p 后移一位,q 后移 1 位
重复上述过程,直到 q 等于数组长度。
返回 p + 1,即为新数组长度。
画个图理解一下
作者:max-LFszNScOfE
链接:https://leetcode.cn/problems/remove-duplicates-from-sorted-array/solution/shuang-zhi-zhen-shan-chu-zhong-fu-xiang-dai-you-hu/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
代码
class Solution {
public int removeDuplicates(int[] nums) {
if (nums.length == 0) {
return 0;
}
int size = 1;
for (int i = 0; i < nums.length - 1; i++) {
if (nums[i] != nums[i + 1]) {
nums[size] = nums[i + 1];
size++;
}
}
return size;
}
}
public int removeDuplicates(int[] nums) {
if(nums == null || nums.length == 0) return 0;
int p = 0;
int q = 1;
while(q < nums.length){
if(nums[p] != nums[q]){
nums[p + 1] = nums[q];
p++;
}
q++;
}
return p + 1;
}
作者:max-LFszNScOfE
链接:https://leetcode.cn/problems/remove-duplicates-from-sorted-array/solution/shuang-zhi-zhen-shan-chu-zhong-fu-xiang-dai-you-hu/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
- 没得说,多看几次记住就行了
209. 长度最小的子数组
- https://leetcode.cn/problems/minimum-size-subarray-sum/
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3cCLBHrA-1653289135561)(C:\Users\黄佳林\AppData\Roaming\Typora\typora-user-images\image-20220513104341106.png)]
解题思路
在本题中实现滑动窗口,主要确定如下三点:
- 窗口内是什么?
- 如何移动窗口的起始位置?
- 如何移动窗口的结束位置?
窗口就是 满足其和 ≥ s 的长度最小的 连续 子数组。
窗口的起始位置如何移动:如果当前窗口的值大于s了,窗口就要向前移动了(也就是该缩小了)。
窗口的结束位置如何移动:窗口的结束位置就是遍历数组的指针,窗口的起始位置设置为数组的起始位置就可以了。
解题的关键在于 窗口的起始位置如何移动,如图所示:
可以发现滑动窗口的精妙之处在于根据当前子序列和大小的情况,不断调节子序列的起始位置。从而将O(n^2)暴力解法降为O(n)。
代码
class Solution {
public int minSubArrayLen(int target, int[] nums) {
int left = 0, right = 0, sum = 0, min = Integer.MAX_VALUE;
for (right = 0; right < nums.length; right++) {
sum += nums[right];
while (sum >= target) {
min = Math.min(min, right - left + 1);
sum -= nums[left];
left++;
}
}
return min == Integer.MAX_VALUE ? 0 : min;
}
}
977. 有序数组的平方
- https://leetcode.cn/problems/squares-of-a-sorted-array/
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VRxyr7hJ-1653289135564)(C:\Users\黄佳林\AppData\Roaming\Typora\typora-user-images\image-20220513104800458.png)]
解题思路
- 暴力:求数组每个元素的平方然后对数组排序
- 双指针:前后各一个指针(因为需要平方,所以不知道前后那个更大),求出平法将较大的那个放入新数组
代码
public int[] sortedSquares(int[] nums) {
for (int i = 0; i < nums.length; i++) {
nums[i] = nums[i] * nums[i];
}
Arrays.sort(nums);
return nums;
}
public int[] sortedSquares(int[] nums) {
int right = nums.length - 1;
int left = 0;
int[] result = new int[nums.length];
int index = result.length - 1;
while (left <= right) {
if (nums[left] * nums[left] > nums[right] * nums[right]) {
result[index--] = nums[left] * nums[left];
++left;
} else {
result[index--] = nums[right] * nums[right];
--right;
}
}
return result;
}
链表
剑指 Offer 18. 删除链表的节点
解题思想
- 思路:找到要删除的节点,把下一个节点的值覆盖到当前节点,然后删除下一个节点
- 思路2:一前一后两个指针,前一个指针指到要删除节点时后一个节点跳过前一个节点(如果是最后尾节点比较难处理)
代码
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) { val = x; }
* }
*/
class Solution {
public ListNode deleteNode(ListNode head, int val) {
//思路:找到要删除的节点,把下一个节点的值覆盖到当前节点,然后删除下一个节点
//思路2:一前一后两个指针,前一个指针指到要删除节点时后一个节点跳过前一个节点
//判定指针是否在尾部,或者头部
if (head == null) {
return null;
}
ListNode p = head;
if (p.val == val) {//如果是头节点则返回下一个节点
return head.next;
}
p = p.next;
ListNode back = head;
while (p != null) {
if (p.val == val) {
break;
}
p = p.next;
back = back.next;
}
if (p.next != null) {//不是尾节点
p.val = p.next.val;
p.next = p.next.next;
} else {//是尾节点
back.next = null;
}
return head;
}
}
剑指 Offer 22. 链表中倒数第k个节点
解题思想
思路:使两个指针间隔k个节点,当前面的指针到尾指针的时候直接返回后一个指针指的节点
代码
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) { val = x; }
* }
*/
class Solution {
public ListNode getKthFromEnd(ListNode head, int k) {
//思路:使两个指针间隔k个节点
ListNode a = head;
ListNode b = head;
for (int i = 0; i < k - 1; i++) {
if (a.next == null) {
return null;
} else {
a = a.next;
}
}
while (a.next != null) {
a = a.next;
b = b.next;
}
return b;
}
}
- 注意循环的次数
剑指 Offer 24. 反转链表
解题思想
- 三指针法
- 将链表的每个节点存储起来,然后取出来重构
代码
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) { val = x; }
* }
*/
class Solution {
public ListNode reverseList(ListNode head) {
//三指针法
ListNode pre = null;
ListNode curr=head;
ListNode next=null;
while (curr != null) {
next = curr.next;
curr.next = pre;
pre = curr;
curr = next;
}
return pre;
}
}
剑指 Offer 25. 合并两个排序的链表
解题思想
利用回溯,将节点依次连接
代码
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) { val = x; }
* }
*/
class Solution {
public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
if(l1==null){
return l2;
}
if(l2==null){
return l1;
}
ListNode res=null;
if(l1.val>l2.val){
res=l2;
res.next=mergeTwoLists(l1,l2.next);
}else{
res=l1;
res.next=mergeTwoLists(l1.next,l2);
}
return res;
}
}
- 看清楚操作
剑指 Offer 35. 复杂链表的复制
解题思想
利用map存储,key为原链表节点,value为干净的新节点
代码
/*
// Definition for a Node.
class Node {
int val;
Node next;
Node random;
public Node(int val) {
this.val = val;
this.next = null;
this.random = null;
}
}
*/
class Solution {
public Node copyRandomList(Node head) {
if (head == null) {
return null;
}
Node q = head;
//map中存的是(原节点,拷贝节点)的一个映射
HashMap<Node, Node> map = new HashMap<>();
while (q != null) {
map.put(q,new Node(q.val));
q=q.next;
}
//将拷贝的新的节点组织成一个链表
q = head;
while (q != null) {
map.get(q).next=map.get(q.next);
map.get(q).random=map.get(q.random);
q=q.next;
}
return map.get(head);
}
}
剑指 Offer 52. 两个链表的第一个公共节点
解题思路
- 两个指针,分别指向两个链表头部
- 到达末端再转移到另一个链表头部
- 两个指针相遇便是公共节点
代码
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) {
* val = x;
* next = null;
* }
* }
*/
public class Solution {
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
//两个指针,分别指向两个链表头部
//到达末端再转移到另一个链表头部
//两个指针相遇
if (headA == null || headB == null) {
return null;
}
ListNode a = headA;
ListNode b = headB;
while (a != b) {
if (a == null) {
a = headB;
} else {
a = a.next;
}
if (b == null) {
b = headA;
} else {
b = b.next;
}
}
return a;
}
}
2. 两数相加
解题思路
创建一个新的链表去表示和,carry表示十位上的数,将个位上的数填入链表中。
代码
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode() {}
* ListNode(int val) { this.val = val; }
* ListNode(int val, ListNode next) { this.val = val; this.next = next; }
* }
*/
class Solution {
public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
ListNode dummy = new ListNode(0);
ListNode curr = dummy;
int carry = 0;
while (l1 != null || l2 != null) {
int sum = 0;
if (l1 != null) {
sum += l1.val;
l1 = l1.next;
}
if (l2 != null) {
sum += l2.val;
l2 = l2.next;
}
sum += carry;
curr.next = new ListNode(sum % 10);
carry = sum / 10;
curr = curr.next;
}
if (carry > 0) {
curr.next = new ListNode(carry);
}
return dummy.next;
}
}
141. 环形链表
解题思路
方法一:哈希表
思路及算法
最容易想到的方法是遍历所有节点,每次遍历到一个节点时,判断该节点此前是否被访问过。
具体地,我们可以使用哈希表来存储所有已经访问过的节点。每次我们到达一个节点,如果该节点已经存在于哈希表中,则说明该链表是环形链表,否则就将该节点加入哈希表中。重复这一过程,直到我们遍历完整个链表即可。
方法二:快慢指针
思路及算法
我们可以根据上述思路来解决本题。具体地,我们定义两个指针,一快一满。慢指针每次只移动一步,而快指针每次移动两步。初始时,慢指针在位置 head,而快指针在位置 head.next。这样一来,如果在移动的过程中,快指针反过来追上慢指针,就说明该链表为环形链表。否则快指针将到达链表尾部,该链表不为环形链表。
代码
public class Solution {
public boolean hasCycle(ListNode head) {
Set<ListNode> seen = new HashSet<ListNode>();
while (head != null) {
if (!seen.add(head)) {
return true;
}
head = head.next;
}
return false;
}
}
/**
* Definition for singly-linked list.
* class ListNode {
* int val;
* ListNode next;
* ListNode(int x) {
* val = x;
* next = null;
* }
* }
*/
public class Solution {
public boolean hasCycle(ListNode head) {
if (head == null || head.next == null) {
return false;
}
ListNode slow = head;
ListNode fast = head.next;
while (fast != slow) {
if (fast == null || fast.next == null) {
return false;
}
slow = slow.next;
fast = fast.next.next;
}
return true;
}
}
142. 环形链表 II
解题思路
方法一:哈希表
思路与算法
一个非常直观的思路是:我们遍历链表中的每个节点,并将它记录下来;一旦遇到了此前遍历过的节点,就可以判定链表中存在环。借助哈希表可以很方便地实现。
代码
public class Solution {
public ListNode detectCycle(ListNode head) {
ListNode pos = head;
Set<ListNode> visited = new HashSet<ListNode>();
while (pos != null) {
if (visited.contains(pos)) {
return pos;
} else {
visited.add(pos);
}
pos = pos.next;
}
return null;
}
}
148.排序链表(链表重构)
解题思路
节点存到集合中然后排序,重构
代码
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode() {}
* ListNode(int val) { this.val = val; }
* ListNode(int val, ListNode next) { this.val = val; this.next = next; }
* }
*/
class Solution {
public ListNode sortList(ListNode head) {
if (head == null) {
return null;
}
ListNode tmp = head;
ArrayList<ListNode> list = new ArrayList<>();
while (tmp != null) {
list.add(tmp);
tmp = tmp.next;
}
list.sort(Comparator.comparingInt(o -> o.val));
ListNode a = list.get(0);
for (int i = 1; i < list.size(); i++) {
list.get(i - 1).next = list.get(i);
}
list.get(list.size() - 1).next = null;
return a;
}
}
树
剑指 Offer 07. 重建二叉树
解题思想
知识点:
- 前序遍历列表:第一个元素永远是 【根节点 (root)】
- 中序遍历列表:根节点 (root)【左边】的所有元素都在根节点的【左分支】,【右边】的所有元素都在根节点的【右分支】
算法思路:
- 通过【前序遍历列表】确定【根节点 (root)】
- 将【中序遍历列表】的节点分割成【左分支节点】和【右分支节点】
- 递归寻找【左分支节点】中的【根节点 (left child)】和 【右分支节点】中的【根节点 (right child)】
代码
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
class Solution {
public TreeNode buildTree(int[] preorder, int[] inorder) {
int n = preorder.length;
if (n == 0) {
return null;
}
int rootVal = preorder[0], rootIndex = 0;
for (int i = 0; i < n; i++) {
if (inorder[i] == rootVal) {
rootIndex = i;
break;
}
}
TreeNode root = new TreeNode(rootVal);
// 以index个为划分,
root.left = buildTree(Arrays.copyOfRange(preorder, 1, rootIndex + 1), Arrays.copyOfRange(inorder, 0, rootIndex));
root.right = buildTree(Arrays.copyOfRange(preorder, rootIndex + 1, n), Arrays.copyOfRange(inorder, rootIndex + 1, n));
return root;
}
}
- 关于代码中边界的问题如果记不清可以画图自己理解一下
剑指 Offer 26. 树的子结构
解题思想
- 先用先序遍历找到A中B的相等节点
- 再以这个节点为根节点开始判断两个树结构是否相等
- 如果结构不相等再继续寻找下一个相等节点
- 把搜索的代码合并到默认的函数里
代码
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
class Solution {
public boolean isSubStructure(TreeNode A, TreeNode B) {
if(A == null || B == null) return false;
return compare(A, B) || isSubStructure(A.left, B) || isSubStructure(A.right, B);
}
public boolean compare(TreeNode A, TreeNode B){
if(B == null) return true;
if(A == null) return false;
return A.val == B.val && compare(A.left, B.left) && compare(A.right, B.right);
}
}
- 当A、B都不为null的时候才讨论,然后依次从每个节点比较(首先要遇到A.val==B.val)
剑指 Offer 27. 二叉树的镜像
解题思想
借助个临时节点来不断替换左右节点的值。
代码
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
class Solution {
public TreeNode mirrorTree(TreeNode root) {
if(root==null){
return null;
}
TreeNode tmp=root.left;
root.left=root.right;
root.right=tmp;
mirrorTree(root.left);
mirrorTree(root.right);
return root;
}
}
- 思想很巧,记住就完事了
剑指 Offer 28. 对称的二叉树
解题思想
递归,判断每一层节点是否对称checkTree(left.left,right.right)&&checkTree(left.right,right.left)
代码
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
class Solution {
public boolean isSymmetric(TreeNode root) {
if (root == null){
return true;
}
return checkTree(root.left, root.right);
}
public boolean checkTree(TreeNode left,TreeNode right){
if(left==null&&right==null){
return true;
}
if(left==null||right==null||left.val!=right.val){
return false;
}
return checkTree(left.left,right.right)&&checkTree(left.right,right.left);
}
}
剑指 Offer 32 - I. 从上到下打印二叉树
解题思想
层序遍历,利用一个queue
代码
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
class Solution {
public int[] levelOrder(TreeNode root) {
//广度搜索
if (root == null) {
return new int[]{};
}
Queue<TreeNode> treeNodeQueue = new LinkedList<>();
treeNodeQueue.add(root);
ArrayList<Integer> list = new ArrayList<>();
while (!treeNodeQueue.isEmpty()) {
TreeNode peek = treeNodeQueue.poll();
list.add(peek.val);
if (peek.left != null) {
treeNodeQueue.add(peek.left);
}
if (peek.right != null) {
treeNodeQueue.add(peek.right);
}
}
int[] ints = new int[list.size()];
int a = 0;
for (Integer integer : list) {
ints[a++] = integer;
}
return ints;
}
}
剑指 Offer 33. 二叉搜索树的后序遍历序列
解题思想
- 最后一个数为根节点,通过根节点不断切割左右子树,递归判断左右子树是否为二叉搜索树。
代码
class Solution {
public boolean verifyPostorder(int[] postorder) {
if (postorder == null || postorder.length == 0) {
return true;
}
int root = postorder[postorder.length - 1];
//寻找分界点
int i = 0;
for (i = 0; i < postorder.length - 1; i++) {
if (postorder[i] > root) {
break;
}
}
//验证后半段
int j = i;
for (; j < postorder.length - 1; j++) {
if (postorder[j] < root) {
return false;
}
}
//前半段
boolean a = true;
if (i > 0) {
int[] ints = new int[i];
System.arraycopy(postorder, 0, ints, 0, i);
a = verifyPostorder(ints);
}
//后半段
boolean b = true;
if (i < postorder.length - 1) {
int[] ints = new int[postorder.length - 1 - i];
System.arraycopy(postorder, i, ints, 0, postorder.length - 1 - i);
b = verifyPostorder(ints);
}
return a && b;
}
}
剑指 Offer 34. 二叉树中和为某一值的路径
解题思想
注意:得到target时要校验一下是不是叶子节点
代码
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
public List<List<Integer>> pathSum(TreeNode root, int target) {
List<List<Integer>> lists = new ArrayList<>();
if (root == null) {
return lists;
}
//深度优先遍历
dfs(root, target, lists, new ArrayList<>());
return lists;
}
private void dfs(TreeNode root, int target, List<List<Integer>> lists, List<Integer> list) {
int tmp = root.val;
list.add(tmp);
if (tmp == target && root.left == null && root.right == null) {
lists.add(new ArrayList<>(list));
}
if (root.left != null) {
dfs(root.left, target - tmp, lists, list);
}
if (root.right != null) {
dfs(root.right, target - tmp, lists, list);
}
list.remove(list.size() - 1);
}
}
剑指 Offer 54. 二叉搜索树的第k大节点
解题思路
树的中序遍历就是从小到大的排序
代码
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
class Solution {
ArrayList<Integer> list = new ArrayList<>();
int k;
public int kthLargest(TreeNode root, int k) {
this.k = k;
middleOrder(root);
return list.get(list.size() - 1);
}
private void middleOrder(TreeNode root) {
if (root.right != null) {
middleOrder(root.right);
}
if (list.size() == k) {
return;
}
list.add(root.val);
if (root.left != null) {
middleOrder(root.left);
}
}
}
剑指 Offer 55 - I. 二叉树的深度
解题思路
就是dfs
代码
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
class Solution {
public int maxDepth(TreeNode root) {
if(root==null){
return 0;
}
return Math.max(maxDepth(root.left),maxDepth(root.right))+1;
}
}
剑指 Offer 55 - II. 平衡二叉树
解题思路
利用上一题的二叉树深度,判断任何节点左右子树深度差都不超过1
Math.abs(depth(root.left) - depth(root.right)) <= 1 && isBalanced(root.left) && isBalanced(root.right)
代码
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
class Solution {
public boolean isBalanced(TreeNode root) {
if (root == null) {
return true;
}
return Math.abs(depth(root.left) - depth(root.right)) <= 1 && isBalanced(root.left) && isBalanced(root.right);
}
private int depth(TreeNode root) {
if (root == null) {
return 0;
}
return Math.max(depth(root.left), depth(root.right)) + 1;
}
}
剑指 Offer 68 - I. 二叉搜索树的最近公共祖先
解题思路
p、q的值都小于当前值则在左树,p、q的值都大于当前值则一定在右树,否则返回当前节点
代码
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
class Solution {
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
//递推工作:
//当 p, qp,q 都在 rootroot 的 右子树 中,则开启递归 root.rightroot.right 并返回;
//否则,当 p, qp,q 都在 rootroot 的 左子树 中,则开启递归 root.leftroot.left 并返回;
if(root.val>p.val&&root.val>q.val){
return lowestCommonAncestor(root.left,p,q);
}else if(root.val<p.val&&root.val<q.val){
return lowestCommonAncestor(root.right,p,q);
}
return root;
}
}
剑指 Offer 68 - II. 二叉树的最近公共祖先
解题思路
就把root当作其中的案例,这样就能相通为什么要
if (root.val == p.val || root.val == q.val) {
return root;
}
代码
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
class Solution {
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
if (root == null) {
return null;
}
if (root.val == p.val || root.val == q.val) {
return root;
}
TreeNode left = lowestCommonAncestor(root.left, p, q);
TreeNode right = lowestCommonAncestor(root.right, p, q);
if (left != null && right != null) {
return root;
} else if (left != null) {
return left;
} else {
return right;
}
}
}
- 很巧妙,这种思想
96. 不同的二叉搜索树(dp问题)
解题思路
假设n个节点存在二叉排序树的个数是G(n),1为根节点,2为根节点,…,n为根节点,当1为根节点时,其左子树节点个数为0,右子树节点个数为n-1,同理当2为根节点时,其左子树节点个数为1,右子树节点为n-2,所以可得G(n) = G(0)*G(n-1)+G(1)*(n-2)+...+G(n-1)*G(0)
代码
class Solution {
public int numTrees(int n) {
if (n <= 2) return n;
int[] dp = new int[n + 1];
dp[0] = 1;
dp[1] = 1;
dp[2] = 2;
for (int i = 3; i <= n; i++) {
for (int j = 0; j < i; j++) {
dp[i] += dp[j] * dp[i - 1 - j];
}
}
return dp[n];
}
}
98. 验证二叉搜索树
解题思路
-
中序遍历为递增
-
或是使用递归:
要解决这道题首先我们要了解二叉搜索树有什么性质可以给我们利用,由题目给出的信息我们可以知道:如果该二叉树的左子树不为空,则左子树上所有节点的值均小于它的根节点的值; 若它的右子树不空,则右子树上所有节点的值均大于它的根节点的值;它的左右子树也为二叉搜索树。
这启示我们设计一个递归函数 helper(root, lower, upper) 来递归判断,函数表示考虑以 root 为根的子树,判断子树中所有节点的值是否都在 (l,r)(l,r) 的范围内(注意是开区间)。如果 root 节点的值 val 不在 (l,r)(l,r) 的范围内说明不满足条件直接返回,否则我们要继续递归调用检查它的左右子树是否满足,如果都满足才说明这是一棵二叉搜索树。
那么根据二叉搜索树的性质,在递归调用左子树时,我们需要把上界 upper 改为 root.val,即调用 helper(root.left, lower, root.val),因为左子树里所有节点的值均小于它的根节点的值。同理递归调用右子树时,我们需要把下界 lower 改为 root.val,即调用 helper(root.right, root.val, upper)。
函数递归调用的入口为 helper(root, -inf, +inf), inf 表示一个无穷大的值。
代码
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
//中序遍历为升序
ArrayList<Integer> list = new ArrayList<>();
public boolean isValidBST2(TreeNode root) {
if (root == null) {
return true;
}
zhongxuDigui(root);
for (int i = 0; i < list.size() - 1; i++) {
if (list.get(i + 1) <= list.get(i)) {
return false;
}
}
return true;
}
// 用递归的方法进行中序遍历
private void zhongxuDigui(TreeNode treeNode) {
if (treeNode.left != null) {
zhongxuDigui(treeNode.left);
}
list.add(treeNode.val);
if (treeNode.right != null) {
zhongxuDigui(treeNode.right);
}
}
public boolean isValidBST(TreeNode root) {
if (root == null) {
return true;
}
return validate(root, Long.MIN_VALUE, Long.MAX_VALUE);
}
private boolean validate(TreeNode root, long min, long max) {
if (root == null) {
return true;
}
if (root.val <= min || root.val >= max) {
return false;
}
return validate(root.left, min, root.val) && validate(root.right, root.val, max);
}
114. 二叉树展开为链表(树重构)
解题思路
先序遍历后,再重构树
取出当前节点和前一个节点,前一个节点左树为空,右树为当前节点
代码
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
public void flatten(TreeNode root) {
if (root == null) {
return;
}
List<TreeNode> list = new ArrayList<>();//存放的是指针地址
preorderTraversal(root,list);
for (int i = 1; i < list.size(); i++) {
TreeNode prev = list.get(i - 1), curr = list.get(i);
prev.left = null;
prev.right = curr;
}
}
public void preorderTraversal(TreeNode root, List<TreeNode> list) {
if (root != null) {
list.add(root);
preorderTraversal(root.left, list);
preorderTraversal(root.right, list);
}
}
}
437. 路径总和 III
解题思路
用dfs去搜索每一条路径
代码
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
int num = 0;
public int pathSum(TreeNode root, int sum) {
if (root == null) {
return 0;
}
dfs(root, sum);
pathSum(root.left, sum);
pathSum(root.right, sum);
return num;
}
private void dfs(TreeNode root, int sum) {
if (root == null) {
return;
}
sum -= root.val;
if (sum == 0) {//中途出现0,后续还可能为0
num++;
}
dfs(root.left, sum);
dfs(root.right, sum);
}
}
538. 把二叉搜索树转换为累加树
解题思路
-
BST的中序遍历就是从小到大,那么反过来就是从大到小,然后累加就好了.
-
将节点存入到list中,倒序取出累加val
代码
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
int num=0;
public TreeNode convertBST(TreeNode root) {
if(root!=null){
convertBST(root.right);
root.val=root.val+num;
num=root.val;
convertBST(root.left);
return root;
}
return null;
}
}
543. 二叉树的直径
解题思路
遍历每一个节点,以每一个节点为中心点计算最长路径(左子树边长+右子树边长),更新全局变量max。(边的个数是直径)
代码
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
int max = 0;
public int diameterOfBinaryTree(TreeNode root) {
if (root == null) {
return 0;
}
dfs(root);
return max;
}
private int dfs(TreeNode root) {
if (root == null) {
return 0;
}
int leftSize = root.left == null ? 0 : dfs(root.left) + 1;
int rightSize = root.right == null ? 0 : dfs(root.right) + 1;
max = Math.max(max, leftSize + rightSize);
return Math.max(leftSize, rightSize);
}
}
617. 合并二叉树
解题思路
总结下递归的条件:
- 终止条件:树 1 的节点为 null,或者树 2 的节点为 null
- 递归函数内:将两个树的节点相加后,再赋给树 1 的节点。再递归的执行两个树的左节点,递归执行两个树的右节点
代码
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
public TreeNode mergeTrees(TreeNode t1, TreeNode t2) {
if (t1 == null) {
return t2;
}
if (t2 == null) {
return t1;
}
TreeNode treeNode = new TreeNode(t1.val + t2.val);
treeNode.left = mergeTrees(t1.left, t2.left);
treeNode.right = mergeTrees(t1.right, t2.right);
return treeNode;
}
}
回溯
剑指 Offer 16. 数值的整数次方
解题思想
- 递归出口:n为0,返回1
- 若n为奇数,算
myPow(x, n - 1)
,转化为偶数 - 若n为偶数,算
myPow(x * x, n / 2)
代码
class Solution {
public double myPow(double x, int n) {
if(n==0){
return 1;
}else if(n<0){
return 1/(x*myPow(x,-n-1));
}else if(n%2==1){
return x*myPow(x,n-1);
}else{
return myPow(x*x,n/2);
}
}
}
剑指 Offer 64. 求1+2+…+n
解题思路
和上一题异曲同工,简单回溯
代码
class Solution {
public int sumNums(int n) {
if (n == 1) {
return 1;
}
return n + sumNums(n - 1);
}
}
深度优先(dfs)
剑指 Offer 12. 矩阵中的路径
解题思想
dfs + 回溯;
- 使用二维布尔数组记录之前的位置是否已经被访问过,如果已经被访问过的话,则在 dfs 的过程中,直接 return false 即可。也就是说,此路已经不通了;
- 如果当前遍历到的字符不等于 board[i][j] 位置上的字符,那么说明此路也是不通的,因此返回 false;
- 至于递归结束的条件:如果指针 start 能够来到 word 的最后一个字符,那么说明能够在矩阵 board 中找到一条路径,此时返回 true;
- 在遍历到当前 board[i][j] 的时候,首先应将该位置的 visited[i][j] 设置为 true,表明访问过;
- 然后,递归地去 board[i][j] 的上下左右四个方向去找,剩下的路径;
- 在 dfs 的过程当中,如果某条路已经不通了,那么那么需要回溯,也就是将 visited[i][j] 位置的布尔值重新赋值为 fasle;
- 最后,返回 ans 即可。
代码
class Solution {
public boolean exist(char[][] board, String word) {
if (board == null || board[0] == null || board.length == 0 || board[0].length == 0 || word == null || word.equals("")) {
return false;
}
boolean[][] isVisited = new boolean[board.length][board[0].length];
char[] chars = word.toCharArray();
for (int i = 0; i < board.length; i++) {
for (int j = 0; j < board[0].length; j++) {
if (board[i][j] == chars[0]) {//调用函数
if (search(isVisited, board, chars, i, j, 0)) {
return true;
}
}
}
}
return false;
}
private boolean search(boolean[][] isVisited, char[][] board, char[] chars, int i, int j, int index) {
if (index == chars.length) {
return true;
}
if (i < 0 || j < 0 || i >= board.length || j >= board[0].length || isVisited[i][j] || board[i][j] != chars[index]) {
return false;
}
isVisited[i][j] = true;
boolean res = search(isVisited, board, chars, i + 1, j, index + 1)
|| search(isVisited, board, chars, i - 1, j, index + 1)
|| search(isVisited, board, chars, i, j + 1, index + 1)
|| search(isVisited, board, chars, i, j - 1, index + 1);
if (!res) {
isVisited[i][j] = false;
}
return res;
}
}
- 最后
isVisited[i][j] = false;
是因为起点可能不止一个,所以要恢复成原来的样子。
剑指 Offer 38. 字符串的排列
解题思想
经典的dfs题
代码
class Solution {
public String[] permutation(String s) {
char[] chars=s.toCharArray();
HashSet<String> strSet=new HashSet<>();
boolean[] ifShow=new boolean[chars.length];
dfs(chars,strSet,ifShow,new StringBuilder());
return strSet.toArray(new String[strSet.size()]);
}
public void dfs(char[] chars,HashSet<String> strSet,boolean[] ifShow,StringBuilder sb){
if(sb.length()==chars.length){
strSet.add(sb.toString());
return;
}
for(int i=0;i<chars.length;i++){
if(!ifShow[i]){
ifShow[i]=true;
dfs(chars,strSet,ifShow,sb.append(chars[i]));
sb.deleteCharAt(sb.length()-1);
ifShow[i]=false;
}
}
}
}
17. 电话号码的字母组合
解题思路
方法一:回溯
首先使用哈希表存储每个数字对应的所有可能的字母,然后进行回溯操作。
回溯过程中维护一个字符串,表示已有的字母排列(如果未遍历完电话号码的所有数字,则已有的字母排列是不完整的)。该字符串初始为空。每次取电话号码的一位数字,从哈希表中获得该数字对应的所有可能的字母,并将其中的一个字母插入到已有的字母排列后面,然后继续处理电话号码的后一位数字,直到处理完电话号码中的所有数字,即得到一个完整的字母排列。然后进行回退操作,遍历其余的字母排列。
回溯算法用于寻找所有的可行解,如果发现一个解不可行,则会舍弃不可行的解。在这道题中,由于每个数字对应的每个字母都可能进入字母组合,因此不存在不可行的解,直接穷举所有的解即可。
代码
class Solution {
public List<String> letterCombinations(String digits) {
ArrayList<String> list = new ArrayList<>();
if (digits.length() == 0) {
return list;
}
String[] strings = {null, null, "abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz"};
dfs(strings, digits, 0, new StringBuilder(), list);
return list;
}
private void dfs(String[] strings, String digits, int index, StringBuilder sb, List<String> list) {
//剪枝
if (sb.length() == digits.length()) {
list.add(sb.toString());
return;
}
//选区节点
String string = strings[digits.charAt(index)-'0'];
for (int i = 0; i < string.length(); i++) {
sb.append(string.charAt(i));
dfs(strings, digits, index + 1, sb, list);
sb.deleteCharAt(sb.length() - 1);
}
}
}
- 注意这里
String string = strings[digits.charAt(index)-'0'];//选区节点
22. 括号生成
解题思想
思路:
- 左括号数量必须大于等于右括号数量,
- 当左括号数量等于右括号数量等于n则返回结果集;
- 左括号小于n继续产生左括号,
- 左括号大于右括号则产生右括号
代码
class Solution {
public List<String> generateParenthesis(int n) {
//思路:左括号数量必须大于等于右括号数量,当左括号数量等于右括号数量等于n则返回结果集
ArrayList<String> list = new ArrayList<>();
if (n == 1) {
list.add("()");
return list;
}
int countR = 0, countL = 0;
toCreate(n, list, countL, countR, "");
return list;
}
public void toCreate(int n, List<String> list, int left, int right, String str) {
if (right > left) {
return;
}
if (right == left && right == n) {
list.add(str);
}
if (left < n) {
toCreate(n, list, left + 1, right, str + "(");
}
if (right < left) {
toCreate(n, list, left, right + 1, str + ")");
}
}
}
39. 组合总和
解题思路
dfs的经典题目,但是我看着有点像背包方案数,不过里面的元素可以重复取
代码
class Solution {
public List<List<Integer>> combinationSum(int[] candidates, int target) {
if (candidates.length == 0) {
return new ArrayList<>();
}
ArrayList<List<Integer>> lists = new ArrayList<>();
backtrack(lists, candidates, 0, target, new ArrayList<>());
return lists;
}
private void backtrack(List<List<Integer>> lists, int[] candidates, int index, int target, List<Integer> list) {
if (target == 0) {
lists.add(new ArrayList<>(list));
return;
}
for (int i = index; i < candidates.length; i++) {
if (candidates[i] <= target) {//限制小数在前,大数在后
list.add(candidates[i]);
backtrack(lists, candidates, i, target - candidates[i], list);
list.remove(list.size() - 1);
}
}
}
}
46. 全排列
解题思想
典型的dfs,用一个数据结构记录是否访问过数据
代码
class Solution {
public List<List<Integer>> permute(int[] nums) {
ArrayList<List<Integer>> result = new ArrayList<>();
HashMap<Integer, Boolean> map = new HashMap<>();
for (int num : nums) {
map.put(num, false);
}
toAllSort(nums, result, map, new ArrayList<>());
return result;
}
public void toAllSort(int[] nums, List<List<Integer>> result, HashMap<Integer, Boolean> map, List<Integer> list) {
if (list.size() == nums.length) {
result.add(new ArrayList<>(list));
return;
}
for (int num : nums) {
if (!map.get(num)) {
list.add(num);
map.put(num, true);
toAllSort(nums, result, map, list);
list.remove(list.size() - 1);
map.put(num, false);
}
}
}
}
78. 子集(解题思路)
解题思路
- 回溯法的模板很好记,但是何时用start变量,何时用visited数组呢?
- 当处理第i+a个元素时,不再考虑第i个元素了,用start变量。
- 而visited数组,则用于处理元素的重复访问,也可用于处理重复元素,在46.全排列中不存在重复元素但存在元素的重复访问。二者的结合使用可参照90.子集II
代码
class Solution {
public List<List<Integer>> subsets(int[] nums) {
List<List<Integer>> result = new ArrayList<>();
dfs(nums, result, 0, new ArrayList<>());
return result;
}
public void dfs(int[] nums, List<List<Integer>> result, int index, List<Integer> subset) {
result.add(new ArrayList<>(subset));
for (int i = index; i < nums.length; i++) {
subset.add(nums[i]);
dfs(nums, result, i + 1, subset);
subset.remove(subset.size() - 1);
}
}
}
贪心
56. 合并区间(这个*)
解题思想
对二维数组先排序,然后和已确定数组进行比较,如果已存入的右边界小于当前左边界或则当前是第一个数组则不存入,否则更新右边界为更大的那一个
代码
class Solution {
public int[][] merge(int[][] intervals) {
//思路:
//1.先将二维数组每一行按第一列排序得到诸如 [ [0,2], [1,5], [6,8], [10,11] ]
//2.循环遍历每一行,给结果数组添加数据,有以下添加情况
//3.对于结果数组 merge 的第一行,直接 add 进去即先将 [0,2] 添加
//4.对于 merge 的其他行,若无重叠也直接添加如 [6,8], [10,11]
//5.若有重叠,则修改上一行如 [0,2], [1,5] -> [0,5]
int n = intervals.length;
//通过 sort 函数对二维数组每一行按第一列元素进行排序
//重写比较器方法,o1[] - o2[] 表示当 o1 大于 o2 时,将 o1 放在 o2 后面,即基本的升序排序
//而 o1[0] - o2[0] 表示按二维数组的每一行第一列元素排序,类似的 o[1] - o2[1]代表按第二列进行排序
Arrays.sort(intervals, new Comparator<int[]>() {
@Override
public int compare(int[] o1, int[] o2) {
return o1[0] - o2[0];
}
});
List<int[]> merge = new ArrayList<int[]>();
for (int i = 0; i < n; i++) {
//创建变量指向每行的左右元素(两列)
int left = intervals[i][0];
int right = intervals[i][1];
//直接 add 的情况:当为第一行或者相邻两行无重叠时
//解释:两行无重叠,即对应在 merge 中上一行的第 1 列小于本行第 0 列
if (merge.size() == 0 || merge.get(merge.size() - 1)[1] < left) {
merge.add(new int[]{left, right});
}
//合并的情况:当有重叠时,将 merge 中上一行的右边界更新
else {
merge.get(merge.size() - 1)[1] = Math.max(merge.get(merge.size() - 1)[1], right);
}
}
//可以学习下此种将 list 转二维数组的方法
return merge.toArray(new int[merge.size()][]);
}
}
452. 用最少数量的箭引爆气球(和这个*)
- https://leetcode.cn/problems/minimum-number-of-arrows-to-burst-balloons/
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fCfli1fX-1653289135580)(C:\Users\黄佳林\AppData\Roaming\Typora\typora-user-images\image-20220514201622489.png)]
解题思路
以上为思考过程,已经确定下来使用贪心了,那么开始解题。
为了让气球尽可能的重叠,需要对数组进行排序。
那么按照气球起始位置排序,还是按照气球终止位置排序呢?
其实都可以!只不过对应的遍历顺序不同,我就按照气球的起始位置排序了。
既然按照起始位置排序,那么就从前向后遍历气球数组,靠左尽可能让气球重复。
从前向后遍历遇到重叠的气球了怎么办?
如果气球重叠了,重叠气球中右边边界的最小值 之前的区间一定需要一个弓箭。
以题目示例: [[10,16],[2,8],[1,6],[7,12]]为例,如图:(方便起见,已经排序)
可以看出首先第一组重叠气球,一定是需要一个箭,气球3,的左边界大于了 第一组重叠气球的最小右边界,所以再需要一支箭来射气球3了。
代码
class Solution {
public int findMinArrowShots(int[][] points) {
if (points.length == 0) return 0;
Arrays.sort(points, (o1, o2) -> Integer.compare(o1[0], o2[0]));
int count = 1;
for (int i = 1; i < points.length; i++) {
if (points[i][0] > points[i - 1][1]) {
count++;
} else {
points[i][1] = Math.min(points[i][1],points[i - 1][1]);
}
}
return count;
}
}
435. 无重叠区间(还有这个*)
- https://leetcode.cn/problems/non-overlapping-intervals/
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WA9zf0r2-1653289135583)(C:\Users\黄佳林\AppData\Roaming\Typora\typora-user-images\image-20220514202353785.png)]
解题思路
本题其实和452.用最少数量的箭引爆气球 (opens new window)非常像,弓箭的数量就相当于是非交叉区间的数量,只要把弓箭那道题目代码里射爆气球的判断条件加个等号(认为[0,1][1,2]不是相邻区间),然后用总区间数减去弓箭数量 就是要移除的区间数量了。
代码
public int eraseOverlapIntervals(int[][] intervals) {
Arrays.sort(intervals, Comparator.comparingInt(o -> o[0]));
int len = intervals.length;
// points 不为空至少需要一支箭
int count = 1;
for (int i = 1; i < len; i++) {
if (intervals[i - 1][1] <= intervals[i][0]) {
// 需要一支箭
count++;
} else {
// 气球i和气球i-1挨着
// 更新重叠气球最小右边界
intervals[i][1] = Math.min(intervals[i - 1][1], intervals[i][1]);
}
}
return len - count;
}
455. 分发饼干
- https://leetcode.cn/problems/assign-cookies/
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3de5VHNP-1653289135585)(C:\Users\黄佳林\AppData\Roaming\Typora\typora-user-images\image-20220514142231025.png)]
解题思路
让小孩胃口和饼干尺寸都从小到大排序,然后找到能够吃下的尺寸
代码
class Solution {
// 思路1:优先考虑饼干,小饼干先喂饱小胃口
public int findContentChildren(int[] g, int[] s) {
Arrays.sort(g);
Arrays.sort(s);
int start = 0;
int count = 0;
for (int i = 0; i < s.length && start < g.length; i++) {
if (s[i] >= g[start]) {
start++;
count++;
}
}
return count;
}
}
376. 摆动序列
- https://leetcode.cn/problems/wiggle-subsequence/
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0SS55QUI-1653289135590)(C:\Users\黄佳林\AppData\Roaming\Typora\typora-user-images\image-20220514144335083.png)]
解题思路
考虑用动态规划的思想来解决这个问题。
很容易可以发现,对于我们当前考虑的这个数,要么是作为山峰(即nums[i] > nums[i-1]),要么是作为山谷(即nums[i] < nums[i - 1])。
- 设dp状态
dp[i][0]
,表示考虑前i个数,第i个数作为山峰的摆动子序列的最长长度 - 设dp状态
dp[i][1]
,表示考虑前i个数,第i个数作为山谷的摆动子序列的最长长度
则转移方程为:
dp[i][0] = max(dp[i][0], dp[j][1] + 1)
,其中0 < j < i
且nums[j] < nums[i]
,表示将nums[i]接到前面某个山谷后面,作为山峰。dp[i][1] = max(dp[i][1], dp[j][0] + 1)
,其中0 < j < i
且nums[j] > nums[i]
,表示将nums[i]接到前面某个山峰后面,作为山谷。
初始状态:
由于一个数可以接到前面的某个数后面,也可以以自身为子序列的起点,所以初始状态为:dp[0][0] = dp[0][1] = 1
。
利用不知道上面方法但是很牛批的方法解决(来自大佬代码)
初始化down和up值为1,遇到峰值down+1,遇到峰谷up+1
代码
// DP
class Solution {
public int wiggleMaxLength(int[] nums) {
// 0 i 作为波峰的最大长度
// 1 i 作为波谷的最大长度
int dp[][] = new int[nums.length][2];
dp[0][0] = dp[0][1] = 1;
for (int i = 1; i < nums.length; i++){
//i 自己可以成为波峰或者波谷
dp[i][0] = dp[i][1] = 1;
for (int j = 0; j < i; j++){
if (nums[j] > nums[i]){
// i 是波谷
dp[i][1] = Math.max(dp[i][1], dp[j][0] + 1);
}
if (nums[j] < nums[i]){
// i 是波峰
dp[i][0] = Math.max(dp[i][0], dp[j][1] + 1);
}
}
}
return Math.max(dp[nums.length - 1][0], dp[nums.length - 1][1]);
}
}
// 大佬
class Solution {
public int wiggleMaxLength(int[] nums) {
int n = nums.length;
if (n < 2) {
return n;
}
int up = 1, down = 1;
for (int i = 1; i < n; i++) {
if (nums[i - 1] > nums[i]) {
down = up + 1;
}
if (nums[i - 1] < nums[i]) {
up = down + 1;
}
}
return Math.max(down, up);
}
}
763. 划分字母区间
- https://leetcode.cn/problems/partition-labels/
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-W1QR7G3G-1653289135592)(C:\Users\黄佳林\AppData\Roaming\Typora\typora-user-images\image-20220514195123709.png)]
解题思路
在遍历的过程中相当于是要找每一个字母的边界,如果找到之前遍历过的所有字母的最远边界,说明这个边界就是分割点了。此时前面出现过所有字母,最远也就到这个边界了。
可以分为如下两步:
- 统计每一个字符最后出现的位置
- 从头遍历字符,并更新字符的最远出现下标,如果找到字符最远出现位置下标和当前下标相等了,则找到了分割点
如图:
代码
class Solution {
public List<Integer> partitionLabels(String S) {
List<Integer> list = new LinkedList<>();
int[] edge = new int[26];
char[] chars = S.toCharArray();
for (int i = 0; i < chars.length; i++) {
edge[chars[i] - 'a'] = i;
}
int idx = 0;
int last = -1;
for (int i = 0; i < chars.length; i++) {
idx = Math.max(idx,edge[chars[i] - 'a']);
if (i == idx) {
list.add(i - last);
last = i;
}
}
return list;
}
}
860. 柠檬水找零
- https://leetcode.cn/problems/lemonade-change/
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gFDEv2NV-1653289135595)(C:\Users\黄佳林\AppData\Roaming\Typora\typora-user-images\image-20220514204133211.png)]
解题思路
只需要维护三种金额的数量,5,10和20。
有如下三种情况:
- 情况一:账单是5,直接收下。
- 情况二:账单是10,消耗一个5,增加一个10
- 情况三:账单是20,优先消耗一个10和一个5,如果不够,再消耗三个5
此时大家就发现 情况一,情况二,都是固定策略,都不用我们来做分析了,而唯一不确定的其实在情况三。
而情况三逻辑也不复杂甚至感觉纯模拟就可以了,其实情况三这里是有贪心的。
账单是20的情况,为什么要优先消耗一个10和一个5呢?
因为美元10只能给账单20找零,而美元5可以给账单10和账单20找零,美元5更万能!
所以局部最优:遇到账单20,优先消耗美元10,完成本次找零。全局最优:完成全部账单的找零。
局部最优可以推出全局最优,并找不出反例,那么就试试贪心算法!
代码
class Solution {
public boolean lemonadeChange(int[] bills) {
int five = 0, ten = 0;
for (int bill : bills) {
if (bill == 5) {
five++;
} else if (bill == 10) {
five--;
ten++;
} else {
if (ten > 0) {
ten--;
five--;
} else {
five -= 3;
}
}
if (five < 0) {
return false;
}
}
return true;
}
}
406. 根据身高重建队列
- https://leetcode.cn/problems/queue-reconstruction-by-height/
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zgp6nAp3-1653289135600)(C:\Users\黄佳林\AppData\Roaming\Typora\typora-user-images\image-20220514222841832.png)]
解题思路
排序完的people: [[7,0], [7,1], [6,1], [5,0], [5,2],[4,4]]
插入的过程:
- 插入[7,0]:[[7,0]]
- 插入[7,1]:[[7,0],[7,1]]
- 插入[6,1]:[[7,0],[6,1],[7,1]]
- 插入[5,0]:[[5,0],[7,0],[6,1],[7,1]]
- 插入[5,2]:[[5,0],[7,0],[5,2],[6,1],[7,1]]
- 插入[4,4]:[[5,0],[7,0],[5,2],[6,1],[4,4],[7,1]]
此时就按照题目的要求完成了重新排列。
代码
class Solution {
public int[][] reconstructQueue(int[][] people) {
Arrays.sort(people, (o1, o2) -> {
if (o1[0] == o2[0]) {
return o1[1] - o2[1];
}
return o2[0] - o1[0];
});
LinkedList<int[]> list = new LinkedList<>();
for (int[] person : people) {
list.add(person[1], person);
}
return list.toArray(new int[people.length][people[0].length]);
}
}
134. 加油站
- https://leetcode.cn/problems/gas-station/
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1j0xmQSV-1653289135601)(C:\Users\黄佳林\AppData\Roaming\Typora\typora-user-images\image-20220514225455639.png)]
解题思路
https://leetcode.cn/problems/gas-station/solution/tan-yi-tan-dan-che-bian-mo-tuo-dai-ma-ji-mf45/
思路:首先判断总油量是否小于总油耗,如果是则肯定不能走一圈。如果否,那肯定能跑一圈。接下来就是循环数组,从第一个站开始,计算每一站剩余的油量,如果油量为负了,就以这个站为起点从新计算。如果到达某一个点为负,说明起点到这个点中间的所有站点都不能到达该点。
复杂度:时间复杂度O(n),空间复杂度O(1)
作者:zz1998
链接:https://leetcode.cn/problems/gas-station/solution/tan-yi-tan-dan-che-bian-mo-tuo-dai-ma-ji-mf45/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
代码
动态规划(dp)
- 标*题目为类似题目
剑指 Offer 14- I. 剪绳子*
解题思想
因为绳子必须要切分成>1段,所以当绳子长度为2、3的时候要单独讨论。核心代码就是dp[i] = Math.max(dp[i], dp[i - j]*dp[j]);
代码
class Solution {
public int cuttingRope(int n) {
int[] dp = new int[n+1];
if(n <= 3) return n-1;
/*解决大问题的时候用到小问题的解并不是这三个数
真正的dp[1] = 0,dp[2] = 1,dp[3] = 2
但是当n=4时,4=2+2 2*2=4 而dp[2]=1是不对的
也就是说当n=1/2/3时,分割后反而比没分割的值要小,当大问题用到dp[j]时,说明已经分成了一个j一个i-j,这两部分又可以再分,但是再分不能比他本身没分割的要小,如果分了更小还不如不分
所以在这里指定大问题用到的dp[1],dp[2],dp[3]是他本身*/
dp[1] = 1;dp[2] = 2;dp[3] = 3;
for(int i = 4; i <= n; i++) {
for(int j = 1; j <= i/2; j++) {
//j<=i/2是因为1*3和3*1是一样的,没必要计算在内,只要计算到1*3和2*2就好了
dp[i] = Math.max(dp[i],dp[i-j]*dp[j]);
}
}
return dp[n];
}
}
剑指 Offer 14- II. 剪绳子 II
- https://leetcode-cn.com/problems/jian-sheng-zi-ii-lcof/
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aiBxWTjl-1653289135609)(C:\Users\黄佳林\AppData\Roaming\Typora\typora-user-images\image-20220510111027340.png)]
解题思路
尽可能去剪长度为3的绳子
代码
class Solution {
public int cuttingRope(int n) {
//尽可能去剪长度为3的绳子
if (n == 2) {
return 1;
} else if (n == 3) {
return 2;
}
long rs = 1;
//绳子长度大于3
while (n > 4) {
rs *= 3;
rs %= 1000000007;
n -= 3;
}
// 乘以剩余线段长,只有2、3、4的可能
return (int) ((rs * n) % 1000000007);
}
}
剑指 Offer 15. 二进制中1的个数
解题思想
把一个整数减去1,再和原整数做与运算,会把该整数最右边一个1变成0.那么一个整数的二进制有多少个1,就可以进行多少次这样的操作。
代码
public class Solution {
// you need to treat n as an unsigned value
public int hammingWeight(int n) {
int count = 0;
while (n!=0){
count++;
n = (n-1)&n;
}
return count;
}
}
以及
public class Solution {
// you need to treat n as an unsigned value
public int hammingWeight(int n) {
return Integer.bitCount(n);
}
}
行不通,可能和输入有关吧
public int hammingWeight(int n) {
if (n == 0) {
return 0;
} else if (n == 1) {
return 1;
}
int[] dp = new int[n + 1];
dp[0] = 0;
dp[1] = 1;
for (int i = 2; i <= n; i++) {
if (i % 2 == 1) {
dp[i] = dp[i - 1] + 1;
} else {
dp[i] = dp[i / 2];
}
}
return dp[n];
}
461. 汉明距离
解题思路
对x和y取异或,这样二进制中的相同位变成0,不同位变成1。然后再获取二进制中的1的个数就行了
不断对z&(z-1)就可以获得1出现的次数
代码
public int hammingDistance(int x, int y) {
return Integer.bitCount(x ^ y);
}
class Solution {
public int hammingDistance(int x, int y) {
int z = x ^ y;
int count = 0;
while (z != 0) {
count++;
z = (z - 1) & z;
}
return count;
}
}
338. 比特位计数
解题思路
分奇数和偶数:
- 偶数的二进制1个数超级简单,因为偶数是相当于被某个更小的数乘2,乘2怎么来的?在二进制运算中,就是左移一位,也就是在低位多加1个0,那样就说明
dp[i] = dp[i / 2]
- 奇数稍微难想到一点,奇数由不大于该数的偶数+1得到,偶数+1在二进制位上会发生什么?会在低位多加1个1,那样就说明dp[i] = dp[i-1] + 1,当然也可以写成
dp[i] = dp[i / 2] + 1
代码
class Solution {
public int[] countBits(int num) {
int[] ints = new int[num + 1];
ints[0] = 0;
for (int i = 1; i <= num; i++) {
if (i % 2 == 1) {
ints[i] = ints[i - 1] + 1;
} else {
ints[i] = ints[i / 2];
}
}
return ints;
}
}
剑指 Offer 42. 连续子数组的最大和
解题思想
动态规划,每次比较当前值和旧值加上当前值的大小dp[i]=Math.max(dp[i-1]+nums[i],nums[i]); maxSum=Math.max(maxSum,dp[i]);
代码
class Solution {
public int maxSubArray(int[] nums) {
int maxSum=nums[0];
int[] dp=new int[nums.length];
dp[0]=nums[0];
for(int i=1;i<nums.length;i++){
dp[i]=Math.max(dp[i-1]+nums[i],nums[i]);
maxSum=Math.max(maxSum,dp[i]);
}
return maxSum;
}
}
152. 乘积最大子数组
解题思路
比较当前值和旧值乘当前知道的大小,遇到负数交换最大值与最小值
代码
class Solution {
public int maxProduct(int[] nums) {
int min = 1;
int max = 1;
int res = Integer.MIN_VALUE;
for (int num : nums) {
if (num < 0) {
int tmp = min;
min = max;
max = tmp;
}
max = Math.max(max * num, num);
min = Math.min(min * num, num);
res = Math.max(max, res);
}
return res;
}
}
剑指 Offer 49. 丑数
解题思路
这个题用三指针,第一个丑数是1,以后的丑数都是基于前面的小丑数分别乘2,3,5构成的。我们每次添加进去一个当前计算出来个三个丑数的最小的一个,并且是谁计算的,谁指针就后移一位。
代码
class Solution {
public int nthUglyNumber(int n) {
int[] dp = new int[n]; // 使用dp数组来存储丑数序列
dp[0] = 1; // dp[0]已知为1
int a = 0, b = 0, c = 0; // 下个应该通过乘2来获得新丑数的数据是第a个, 同理b, c
for(int i = 1; i < n; i++){
// 第a丑数个数需要通过乘2来得到下个丑数,第b丑数个数需要通过乘2来得到下个丑数,同理第c个数
int n2 = dp[a] * 2, n3 = dp[b] * 3, n5 = dp[c] * 5;
dp[i] = Math.min(Math.min(n2, n3), n5);
if(dp[i] == n2){
a++; // 第a个数已经通过乘2得到了一个新的丑数,那下个需要通过乘2得到一个新的丑数的数应该是第(a+1)个数
}
if(dp[i] == n3){
b++; // 第 b个数已经通过乘3得到了一个新的丑数,那下个需要通过乘3得到一个新的丑数的数应该是第(b+1)个数
}
if(dp[i] == n5){
c++; // 第 c个数已经通过乘5得到了一个新的丑数,那下个需要通过乘5得到一个新的丑数的数应该是第(c+1)个数
}
}
return dp[n-1];
}
}
剑指 Offer 63. 股票的最大利润
解题思路
记录最小价格,然后每循环一次比较最小价格,并比较获得最大利益
代码
public int maxProfit(int[] prices) {
int max = 0, min = prices[0];
for (int i = 1; i < prices.length; i++) {
min = Math.min(prices[i], min);
max = Math.max(max, prices[i] - min);
}
return max;
}
}
想的很复杂,但代码实现起来确实很巧妙。
55. 跳跃游戏(多看一眼)*
解题思路
- 如果所有元素都不为0, 那么一定可以跳到最后;
- 从后往前遍历,如果遇到nums[i] = 0,就找i前面的元素j,使得nums[j] > i - j。如果找不到,则不可能跳跃到num[i+1],返回false。
代码
class Solution {
public boolean canJump(int[] nums) {
int n = nums.length;
boolean[] dp = new boolean[n];
dp[0] = true;
for (int i = 1; i < n; i++){
for (int j = 0; j < i; j++){
if (dp[j] && (j + nums[j] >= i)){
dp[i] = true;
break;
}
}
}
return dp[n-1];
}
}
45. 跳跃游戏 II
- https://leetcode.cn/problems/jump-game-ii/
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-feVZksFV-1653289135612)(C:\Users\黄佳林\AppData\Roaming\Typora\typora-user-images\image-20220514161629555.png)]
解题思路
设定dp[i]代表移动到位置i,需要的最少步骤数
可以想到:当前位置i,由上一位置j向右移动最多nums[j]而得到(可能不需要移动nums[j]就到了位置i)
那么就可以列出状态转移方程:
dp[i] = min(dp[i], dp[j] + 1)
作者:GRY
链接:https://leetcode.cn/problems/jump-game-ii/solution/dong-tai-gui-hua-by-gry-h26o/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
代码
public static int jump(int[] nums) {
int len = nums.length;
int[] dp = new int[len];
dp[0] = 0;
for (int i = 1; i < len; i++) {
dp[i] = len;
for (int j = 0; j < i; j++) {
if (j + nums[j] >= i) {
dp[i] = Math.min(dp[i], dp[j] + 1);
}
}
}
return dp[len - 1];
}
56. 合并区间
解题思想
对二维数组先排序,然后和已确定数组进行比较,如果已存入的右边界小于当前左边界或则当前是第一个数组则不存入,否则更新右边界为更大的那一个
代码
class Solution {
public int[][] merge(int[][] intervals) {
//思路:
//1.先将二维数组每一行按第一列排序得到诸如 [ [0,2], [1,5], [6,8], [10,11] ]
//2.循环遍历每一行,给结果数组添加数据,有以下添加情况
//3.对于结果数组 merge 的第一行,直接 add 进去即先将 [0,2] 添加
//4.对于 merge 的其他行,若无重叠也直接添加如 [6,8], [10,11]
//5.若有重叠,则修改上一行如 [0,2], [1,5] -> [0,5]
int n = intervals.length;
//通过 sort 函数对二维数组每一行按第一列元素进行排序
//重写比较器方法,o1[] - o2[] 表示当 o1 大于 o2 时,将 o1 放在 o2 后面,即基本的升序排序
//而 o1[0] - o2[0] 表示按二维数组的每一行第一列元素排序,类似的 o[1] - o2[1]代表按第二列进行排序
Arrays.sort(intervals, new Comparator<int[]>() {
@Override
public int compare(int[] o1, int[] o2) {
return o1[0] - o2[0];
}
});
List<int[]> merge = new ArrayList<int[]>();
for (int i = 0; i < n; i++) {
//创建变量指向每行的左右元素(两列)
int left = intervals[i][0];
int right = intervals[i][1];
//直接 add 的情况:当为第一行或者相邻两行无重叠时
//解释:两行无重叠,即对应在 merge 中上一行的第 1 列小于本行第 0 列
if (merge.size() == 0 || merge.get(merge.size() - 1)[1] < left) {
merge.add(new int[]{left, right});
}
//合并的情况:当有重叠时,将 merge 中上一行的右边界更新
else {
merge.get(merge.size() - 1)[1] = Math.max(merge.get(merge.size() - 1)[1], right);
}
}
//可以学习下此种将 list 转二维数组的方法
return merge.toArray(new int[merge.size()][]);
}
}
62. 不同路径
- https://leetcode.cn/problems/unique-paths/
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IxGPgYFa-1653289135615)(C:\Users\黄佳林\AppData\Roaming\Typora\typora-user-images\image-20220513115647309.png)]
解题思路
当前位置的路径数=从上面来的路径数+从左面来的路径数
代码
class Solution {
public int uniquePaths(int m, int n) {
int[][] dp = new int[m][n];
dp[0][0] = 1;
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (i > 0) dp[i][j] += dp[i - 1][j];
if (j > 0) dp[i][j] += dp[i][j - 1];
}
}
return dp[m - 1][n - 1];
}
}
- 注意代码的简洁性
63. 不同路径 II
- https://leetcode.cn/problems/unique-paths-ii/
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zCeZWcMV-1653289135618)(C:\Users\黄佳林\AppData\Roaming\Typora\typora-user-images\image-20220513120038417.png)]
解题思路
在原有的基础上多一个石头判断(有可能起始位置就有一块石头)
代码
public int uniquePathsWithObstacles(int[][] obstacleGrid) {
for (int i = 0; i < obstacleGrid.length; i++) {
for (int j = 0; j < obstacleGrid[0].length; j++) {
if (obstacleGrid[i][j] == 1) {
obstacleGrid[i][j] = 0;
continue;
}
if (i == 0 && j == 0) {
obstacleGrid[i][j] = 1;
continue;
}
if (i > 0) obstacleGrid[i][j] += obstacleGrid[i - 1][j];
if (j > 0) obstacleGrid[i][j] += obstacleGrid[i][j - 1];
}
}
return obstacleGrid[obstacleGrid.length - 1][obstacleGrid[0].length - 1];
}
70. 爬楼梯
解题思想
- 思路一:可以一次上一个阶梯,也可以一次上两次阶梯
- 思路二:[1,2]完全背包,背包容量为阶梯数
代码
class Solution {
public int climbStairs(int n) {
int[] dp = new int[n + 1];
dp[0] = 1;
dp[1] = 1;
for (int i = 2; i <= n; i++) {
dp[i] = dp[i - 1] + dp[i - 2];
}
return dp[n];
}
}
class Solution {
public int climbStairs(int n) {
int[] dp = new int[n + 1];
int[] weight = {1,2};
dp[0] = 1;
for (int i = 0; i <= n; i++) {
for (int j = 0; j < weight.length; j++) {
if (i >= weight[j]) dp[i] += dp[i - weight[j]];
}
}
return dp[n];
}
}
746. 使用最小花费爬楼梯
- https://leetcode.cn/problems/min-cost-climbing-stairs/
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CGL42ySH-1653289135623)(C:\Users\黄佳林\AppData\Roaming\Typora\typora-user-images\image-20220513114511740.png)]
解题思路
典型动态规划,动态规划最重要的一点就是要明白dp[i]代表的是什么意思。dp[i]到达高度为i的解题消耗的最少花费。
dp[i] = Math.min(dp[i - 1] + cost[i - 1], dp[i - 2] + cost[i - 2]);
代码
public int minCostClimbingStairs(int[] cost) {
if (cost.length == 1) {
return cost[0];
}
if (cost.length == 2) {
return Math.min(cost[0], cost[1]);
}
int[] dp = new int[cost.length + 1];//定义dp为到达i的花费
//dp[i] = min(dp[i-1]+cost[i-1],dp[i-2]+cost[i-2])
dp[0] = 0;
dp[1] = 0;
for (int i = 2; i < dp.length; i++) {
dp[i] = Math.min(dp[i - 1] + cost[i - 1], dp[i - 2] + cost[i - 2]);
}
return dp[dp.length - 1];
}
91.解码方法(要记住)
- https://leetcode.cn/problems/decode-ways/
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kcjaeosJ-1653289135627)(C:\Users\黄佳林\AppData\Roaming\Typora\typora-user-images\image-20220510164047677.png)]
解题思路
代码
class Solution {
public int numDecodings(String s) {
// "226"
int length = s.length();
if (s.charAt(0) == '0') {
return 0;
}
int[] dp = new int[length];
dp[0] = 1;
for (int i = 1; i < length; i++) {
if (s.charAt(i) == '0') {
if (s.charAt(i - 1) == '1' || s.charAt(i - 1) == '2') {
dp[i] = dp[Math.max(i - 2, 0)];
} else {
return 0;
}
} else if (s.charAt(i - 1) == '1') {
dp[i] = dp[i - 1] + dp[Math.max(i - 2, 0)];
} else if (s.charAt(i - 1) == '2' && s.charAt(i) >= '1' && s.charAt(i) <= '6') {
dp[i] = dp[i - 1] + dp[Math.max(i - 2, 0)];
} else {
dp[i] = dp[i - 1];
}
}
return dp[length-1];
}
}
139. 单词拆分(多看几眼)
解题思路
动态规划
- s 串能否分解为单词表的单词(前 s.length 个字符的 s 串能否分解为单词表单词)
- 将大问题分解为规模小一点的子问题:
- 前 i 个字符的子串,能否分解成单词
- 剩余子串,是否为单个单词。
- dp[i]:长度为i的s[0:i-1]子串是否能拆分成单词。题目求:dp[s.length]
代码
class Solution {
public boolean wordBreak(String s, List<String> wordDict) {
int length = s.length();
boolean[] dp = new boolean[length + 1];
dp[0] = true;
for (int i = 1; i <= length; i++) {
for (int j = 0; j < i; j++) {
if (dp[j] && wordDict.contains(s.substring(j, i))) {
dp[i] = true;
break;
}
}
}
return dp[length];
}
}
dp[j] && wordDict.contains(s.substring(j, i))
认真理解这一句,dp[j]代表的是前j个字符的状态,其实下标是j-1,所以后面切分字符串的时候要从j开始到i结束(i代表的是i-1下标)
198. 打家劫舍
解题思路
选择不打劫当前房间,或则打劫当前房间和上上间房间
代码
class Solution {
public int rob(int[] nums) {
if (nums.length==0){
return 0;
}else if (nums.length==1){
return nums[0];
}
int[] dp = new int[nums.length];
dp[0] = nums[0];
dp[1] = Math.max(nums[0], nums[1]);
for (int i = 2; i < nums.length; i++) {
dp[i] = Math.max(dp[i - 1], dp[i - 2] + nums[i]);
}
return dp[nums.length - 1];
}
}
337. 打家劫舍 III
解题思路
当前节点root,和root.left.right和root.left.left和root.right.right和root.right.left的值递归下去,用map记录该root的最大值
代码
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
public int rob(TreeNode root) {
HashMap<TreeNode, Integer> memo = new HashMap<>();
return robInternal(root, memo);
}
public int robInternal(TreeNode root, HashMap<TreeNode, Integer> memo) {
if (root == null) return 0;
if (memo.containsKey(root)) return memo.get(root);
int money = root.val;
if (root.left != null) {
money += (robInternal(root.left.left, memo) + robInternal(root.left.right, memo));
}
if (root.right != null) {
money += (robInternal(root.right.left, memo) + robInternal(root.right.right, memo));
}
int result = Math.max(money, robInternal(root.left, memo) + robInternal(root.right, memo));
memo.put(root, result);
return result;
}
}
221. 最大正方形
解题思路
当我们判断以某个点为正方形右下角时最大的正方形时,那它的上方,左方和左上方三个点也一定是某个正方形的右下角,否则该点为右下角的正方形最大就是它自己了。这是定性的判断,那具体的最大正方形边长呢?我们知道,该点为右下角的正方形的最大边长,最多比它的上方,左方和左上方为右下角的正方形的边长多1,最好的情况是是它的上方,左方和左上方为右下角的正方形的大小都一样的,这样加上该点就可以构成一个更大的正方形。 但如果它的上方,左方和左上方为右下角的正方形的大小不一样,合起来就会缺了某个角落,这时候只能取那三个正方形中最小的正方形的边长加1了。假设dpi表示以i,j为右下角的正方形的最大边长,则有 dp[i][j] = min(dp[i-1][j-1], dp[i-1][j], dp[i][j-1]) + 1
当然,如果这个点在原矩阵中本身就是0的话,那dp[i]肯定就是0了。
代码
class Solution {
public int maximalSquare(char[][] matrix) {
int[][] dp = new int[matrix.length][matrix[0].length];
int maxSize = 0;
for (int i = 0; i < matrix.length; i++) {
for (int j = 0; j < matrix[0].length; j++) {
if (matrix[i][j] == '0') {
dp[i][j] = 0;
} else {
if (i == 0 || j == 0) {
dp[i][j] = 1;
} else {
dp[i][j] = Math.min(dp[i - 1][j], Math.min(dp[i][j - 1], dp[i - 1][j - 1]))+1;
}
}
maxSize = Math.max(maxSize, dp[i][j]);
}
}
return maxSize * maxSize;
}
}
279. 完全平方数(多看一眼)*
解题思路
- 首先初始化长度为 n+1 的数组 dp,每个位置都为 0
- 如果 n 为 0,则结果为 0
- 对数组进行遍历,下标为 i,每次都将当前数字先更新为最大的结果,即
dp[i]=i
,比如 i=4,最坏结果为 4=1+1+1+1 即为 4 个数字 - 动态转移方程为:
dp[i] = MIN(dp[i], dp[i - j * j] + 1)
,i 表示当前数字,j*j 表示平方数 - 时间复杂度:O(n*sqrt(n))O(n∗sqrt(n)),sqrt 为平方根
代码
class Solution {
public int numSquares(int n) {
int[] dp = new int[n + 1]; // 默认初始化值都为0
for (int i = 1; i <= n; i++) {
dp[i] = i; // 最坏的情况就是每次+1
for (int j = 1; i - j * j >= 0; j++) {
dp[i] = Math.min(dp[i], dp[i - j * j] + 1); // 动态转移方程
}
}
return dp[n];
}
}
300. 最长递增子序列(多看理解)*
解题思路
这道题有两种做法:
- 一种是DP也就是动态规划,很简单,第i个元素之前的最小上升子序列的长度无非就是max(dp[i],dp[j]+1)
- 那么另一种做法就是二分查找法,也很简单,无非就是再新建一个数组,然后第一个数先放进去,然后第二个数和第一个数比较,如果说大于第一个数,那么就接在他后面,如果小于第一个数,那么就替换,一般的,如果有i个数,那么每进来一个新的数,都要用二分查找法来得知要替换在哪个位置的数。因为有个for循环,所以是O(N),在加上循环里有个二分查找,所以最后是O(NlogN)的时间复杂度。
代码
class Solution {
public int lengthOfLIS(int[] nums) {
if (nums == null || nums.length == 0) {
return 0;
}
int[] dp = new int[nums.length];
Arrays.fill(dp, 1);
dp[0] = 1;
int maxSize = 1;
for (int i = 1; i < nums.length; i++) {
for (int j = 0; j < i; j++) {
if (nums[i] > nums[j]) {
dp[i] = Math.max(dp[j] + 1, dp[i]);
}
}
maxSize = Math.max(maxSize, dp[i]);
}
return maxSize;
}
}
- 秒啊,这类题仿佛都有一种规律
718. 最长重复子数组(新)
- https://leetcode.cn/problems/maximum-length-of-repeated-subarray/
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BMveb9PS-1653289135636)(C:\Users\黄佳林\AppData\Roaming\Typora\typora-user-images\image-20220513193229758.png)]
解题思路
注意题目中说的子数组,其实就是连续子序列。这种问题动规最拿手,动规五部曲分析如下:
- 确定dp数组(dp table)以及下标的含义
dp[i][j] :以下标i - 1为结尾的A,和以下标j - 1为结尾的B,最长重复子数组长度为dp[i][j]。
此时细心的同学应该发现,那dp[0][0]是什么含义呢?总不能是以下标-1为结尾的A数组吧。
其实dp[i][j]的定义也就决定着,我们在遍历dp[i][j]的时候i 和 j都要从1开始。
那有同学问了,我就定义dp[i][j]为 以下标i为结尾的A,和以下标j 为结尾的B,最长重复子数组长度。不行么?
行倒是行! 但实现起来就麻烦一点,大家看下面的dp数组状态图就明白了。
- 举例推导dp数组
拿示例1中,A: [1,2,3,2,1],B: [3,2,1,4,7]为例,画一个dp数组的状态变化,如下:
代码
class Solution {
public int findLength(int[] nums1, int[] nums2) {
int l1 = nums1.length;
int l2 = nums2.length;
int max = 0;
int[][] dp = new int[l1 + 1][l2 + 1];
for (int i = 1; i <= l1; i++) {
for (int j = 1; j <= l2; j++) {
if (nums1[i - 1] == nums2[j - 1]) {
dp[i][j] = dp[i - 1][j - 1] + 1;
}
max = Math.max(max, dp[i][j]);
}
}
return max;
}
}
- 看图解理解起来比较直观
1143. 最长公共子序列(新)
- https://leetcode.cn/problems/longest-common-subsequence/
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Zs1FW4jH-1653289135641)(C:\Users\黄佳林\AppData\Roaming\Typora\typora-user-images\image-20220513195643910.png)]
解题思路
反正我是想不出来这个状态转移方程,除非我做过
- 确定dp数组(dp table)以及下标的含义
dp[i][j]:长度为[0, i - 1]的字符串text1与长度为[0, j - 1]的字符串text2的最长公共子序列为dp[i][j]
有同学会问:为什么要定义长度为[0, i - 1]的字符串text1,定义为长度为[0, i]的字符串text1不香么?
这样定义是为了后面代码实现方便,如果非要定义为为长度为[0, i]的字符串text1也可以,大家可以试一试!
- 确定递推公式
主要就是两大情况: text1[i - 1] 与 text2[j - 1]相同,text1[i - 1] 与 text2[j - 1]不相同
如果text1[i - 1] 与 text2[j - 1]相同,那么找到了一个公共元素,所以dp[i][j] = dp[i - 1][j - 1] + 1;
如果text1[i - 1] 与 text2[j - 1]不相同,那就看看text1[0, i - 2]与text2[0, j - 1]的最长公共子序列 和 text1[0, i - 1]与text2[0, j - 1]的最长公共子序列,取最大的。
即:dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]);
- 确定遍历顺序
从递推公式,可以看出,有三个方向可以推出dp[i][j],如图:
那么为了在递推的过程中,这三个方向都是经过计算的数值,所以要从前向后,从上到下来遍历这个矩阵。
- 举例推导dp数组
以输入:text1 = “abcde”, text2 = “ace” 为例,dp状态如图:
最后红框dp[text1.size()][text2.size()]为最终结果
代码
class Solution {
public int longestCommonSubsequence(String text1, String text2) {
int[][] dp = new int[text1.length() + 1][text2.length() + 1]; // 先对dp数组做初始化操作
for (int i = 1 ; i <= text1.length() ; i++) {
char char1 = text1.charAt(i - 1);
for (int j = 1; j <= text2.length(); j++) {
char char2 = text2.charAt(j - 1);
if (char1 == char2) { // 开始列出状态转移方程
dp[i][j] = dp[i - 1][j - 1] + 1;
} else {
dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
}
}
}
return dp[text1.length()][text2.length()];
}
}
- 不要挣扎,记住就行了
392. 判断子序列(新)
- https://leetcode.cn/problems/is-subsequence/
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-K2MpyErU-1653289135643)(C:\Users\黄佳林\AppData\Roaming\Typora\typora-user-images\image-20220513220334652.png)]
解题思路
仔细想一下和1143题一样,只需要最后判断一些公共数组长度是否等于s的长度
代码
class Solution {
public boolean isSubsequence(String s, String t) {
int l1 = s.length();
int l2 = t.length();
int[][] dp = new int[l1 + 1][l2 + 1];
for (int i = 1; i <= l1; i++) {
for (int j = 1; j <= l2; j++) {
if (s.charAt(i - 1) == t.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]);
}
}
}
return dp[l1][l2] == l1;
}
}
1035. 不相交的线(新)
- https://leetcode.cn/problems/uncrossed-lines/
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GDZ8hKvp-1653289135646)(C:\Users\黄佳林\AppData\Roaming\Typora\typora-user-images\image-20220513203505553.png)]
解题思路
**本题说是求绘制的最大连线数,其实就是求两个字符串的最长公共子序列的长度!**那么这一题就和上一题1143一模一样了。
代码
class Solution {
public int maxUncrossedLines(int[] A, int[] B) {
int [][] dp = new int[A.length+1][B.length+1];
for(int i=1;i<=A.length;i++) {
for(int j=1;j<=B.length;j++) {
if (A[i-1]==B[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]);
}
}
}
return dp[A.length][B.length];
}
}
583. 两个字符串的删除操作(新)
- https://leetcode.cn/problems/delete-operation-for-two-strings/
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GgSquJQK-1653289135648)(C:\Users\黄佳林\AppData\Roaming\Typora\typora-user-images\image-20220513224544613.png)]
解题思路
又是和1143一样,求出最长公共序列,然后return word1.size()+word2.size()-dp[word1.size()][word2.size()]*2;
代码
class Solution {
public int minDistance(String word1, String word2) {
int l1 = word1.length();
int l2 = word2.length();
int[][] dp = new int[l1 + 1][l2 + 1];
for (int i = 1; i <= l1; i++) {
for (int j = 1; j <= l2; j++) {
if (word1.charAt(i - 1) == word2.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]);
}
}
}
return l1+l2-dp[l1][l2]*2;
}
}
72. 编辑距离(新,难)
- https://leetcode.cn/problems/edit-distance/
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Zd2G6XrL-1653289135650)(C:\Users\黄佳林\AppData\Roaming\Typora\typora-user-images\image-20220513225712857.png)]
解题思路
- 确定dp含义
dp[i][j] 表示以下标i-1为结尾的字符串word1,和以下标j-1为结尾的字符串word2,最近编辑距离为dp[i][j]。
这里在强调一下:为啥要表示下标i-1为结尾的字符串呢,为啥不表示下标i为结尾的字符串呢?
用i来表示也可以! 但我统一以下标i-1为结尾的字符串,在下面的递归公式中会容易理解一点。
- 递推公式
在确定递推公式的时候,首先要考虑清楚编辑的几种操作,整理如下:
if (word1[i - 1] == word2[j - 1])
不操作
if (word1[i - 1] != word2[j - 1])
增
删
换
在下面的讲解中,如果哪里看不懂,就回想一下dp[i][j]
的定义,就明白了。
在整个动规的过程中,最为关键就是正确理解dp[i][j]
的定义!
if (word1[i - 1] != word2[j - 1])
,此时就需要编辑了,如何编辑呢?
- 操作一:word1删除一个元素,那么就是以下标i - 2为结尾的word1 与 j-1为结尾的word2的最近编辑距离 再加上一个操作。
即 dp[i][j] = dp[i - 1][j] + 1;
- 操作二:word2删除一个元素,那么就是以下标i - 1为结尾的word1 与 j-2为结尾的word2的最近编辑距离 再加上一个操作。
即 dp[i][j] = dp[i][j - 1] + 1;
这里有同学发现了,怎么都是删除元素,添加元素去哪了。
word2添加一个元素,相当于word1删除一个元素,例如 word1 = "ad" ,word2 = "a"
,word1
删除元素'd'
和 word2
添加一个元素'd'
,变成word1="a", word2="ad"
, 最终的操作数是一样! dp数组如下图所示意的:
a a d
+-----+-----+ +-----+-----+-----+
| 0 | 1 | | 0 | 1 | 2 |
+-----+-----+ ===> +-----+-----+-----+
a | 1 | 0 | a | 1 | 0 | 1 |
+-----+-----+ +-----+-----+-----+
d | 2 | 1 |
+-----+-----+
操作三:替换元素,word1
替换word1[i - 1]
,使其与word2[j - 1]
相同,此时不用增加元素,那么以下标i-2
为结尾的word1
与 j-2
为结尾的word2
的最近编辑距离 加上一个替换元素的操作。
即 dp[i][j] = dp[i - 1][j - 1] + 1;
综上,当 if (word1[i - 1] != word2[j - 1])
时取最小的,即:dp[i][j] = min({dp[i - 1][j - 1], dp[i - 1][j], dp[i][j - 1]}) + 1;
递归公式代码如下:
if (word1[i - 1] == word2[j - 1]) {
dp[i][j] = dp[i - 1][j - 1];
}
else {
dp[i][j] = min({dp[i - 1][j - 1], dp[i - 1][j], dp[i][j - 1]}) + 1;
}
- 初始化dp
再回顾一下dp[i][j]的定义:
dp[i][j] 表示以下标i-1为结尾的字符串word1,和以下标j-1为结尾的字符串word2,最近编辑距离为dp[i][j]。
那么dp[i][0] 和 dp[0][j] 表示什么呢?
dp[i][0] :以下标i-1为结尾的字符串word1,和空字符串word2,最近编辑距离为dp[i][0]。
那么dp[i][0]就应该是i,对word1里的元素全部做删除操作,即:dp[i][0] = i;
同理dp[0][j] = j;
所以C++代码如下:
for (int i = 0; i <= word1.size(); i++) dp[i][0] = i;
for (int j = 0; j <= word2.size(); j++) dp[0][j] = j;
所以在dp矩阵中一定是从左到右从上到下去遍历。
代码如下:
for (int i = 1; i <= word1.size(); i++) {
for (int j = 1; j <= word2.size(); j++) {
if (word1[i - 1] == word2[j - 1]) {
dp[i][j] = dp[i - 1][j - 1];
}
else {
dp[i][j] = min({dp[i - 1][j - 1], dp[i - 1][j], dp[i][j - 1]}) + 1;
}
}
}
以示例1为例,输入:word1 = "horse", word2 = "ros"
为例,dp矩阵状态图如下:
代码
public int minDistance(String word1, String word2) {
int m = word1.length();
int n = word2.length();
int[][] dp = new int[m + 1][n + 1];
// 初始化
for (int i = 1; i <= m; i++) {
dp[i][0] = i;
}
for (int j = 1; j <= n; j++) {
dp[0][j] = j;
}
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
// 因为dp数组有效位从1开始
// 所以当前遍历到的字符串的位置为i-1 | j-1
if (word1.charAt(i - 1) == word2.charAt(j - 1)) {
dp[i][j] = dp[i - 1][j - 1];
} else {
dp[i][j] = Math.min(Math.min(dp[i - 1][j - 1], dp[i][j - 1]), dp[i - 1][j]) + 1;
}
}
}
return dp[m][n];
}
53. 最大子数组和
- https://leetcode.cn/problems/maximum-subarray/
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-y1QcpWNu-1653289135656)(C:\Users\黄佳林\AppData\Roaming\Typora\typora-user-images\image-20220513215243452.png)]
解题思路
- 确定dp数组(dp table)以及下标的含义
dp[i]:包括下标i之前的最大连续子序列和为dp[i]。
- 确定递推公式
dp[i]只有两个方向可以推出来:
- dp[i - 1] + nums[i],即:nums[i]加入当前连续子序列和
- nums[i],即:从头开始计算当前连续子序列和
一定是取最大的,所以dp[i] = max(dp[i - 1] + nums[i], nums[i]);
代码
/**
* 1.dp[i]代表当前下标对应的最大值
* 2.递推公式 dp[i] = max (dp[i-1]+nums[i],nums[i]) res = max(res,dp[i])
* 3.初始化 都为 0
* 4.遍历方向,从前往后
* 5.举例推导结果。。。
*
* @param nums
* @return
*/
public static int maxSubArray(int[] nums) {
if (nums.length == 0) {
return 0;
}
int res = nums[0];
int[] dp = new int[nums.length];
dp[0] = nums[0];
for (int i = 1; i < nums.length; i++) {
dp[i] = Math.max(dp[i - 1] + nums[i], nums[i]);
res = res > dp[i] ? res : dp[i];
}
return res;
}
322. 零钱兑换
解题思路
可以看成完全背包问题,dp的是次数
代码
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 = 0; i < coins.length; i++) {
for (int j = coins[i]; j <= amount; j++) {
dp[j] = Math.min(dp[j], dp[j - coins[i]] + 1);
}
}
return dp[amount] == (amount + 1) ? -1 : dp[amount];
}
}
518. 零钱兑换 II
- https://leetcode.cn/problems/coin-change-2/
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4nO99Qqz-1653289135660)(C:\Users\黄佳林\AppData\Roaming\Typora\typora-user-images\image-20220513163419563.png)]
解题思路
01背包的求解方案数,方案数的前提是dp[amount]实现了最大值
代码
class Solution {
public int change(int amount, int[] coins) {
int[] dp = new int[amount + 1];
int[] count = new int[amount + 1];
Arrays.fill(count, 1);
for (int i = 0; i < coins.length; i++) {
for (int j = coins[i]; j <= amount; j++) {
if (dp[j] == dp[j - coins[i]] + coins[i]) {
count[j] += count[j - coins[i]];
} else if (dp[j] < dp[j - coins[i]] + coins[i]) {
count[j] = count[j - coins[i]];
}
dp[j] = Math.max(dp[j], dp[j - coins[i]] + coins[i]);
}
}
return dp[amount] == amount ? count[amount] : 0;
}
}
- 注意最后的判断条件return dp[amount] == amount ? count[amount] : 0;
416. 分割等和子集
- https://leetcode.cn/problems/partition-equal-subset-sum/
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rNdsK7ys-1653289135666)(C:\Users\黄佳林\AppData\Roaming\Typora\typora-user-images\image-20220513123401072.png)]
解题思路
01背包问题,可以将sum/2为背包容量
代码
class Solution {
public boolean canPartition(int[] nums) {
int sum = 0;
for (int num : nums) {
sum += num;
}
if (sum % 2 != 0) {
return false;
}
sum /= 2;
int[] dp = new int[sum + 1];
for (int i = 0; i < nums.length; i++) {
for (int j = sum; j >= nums[i]; j--) {
dp[j] = Math.max(dp[j], dp[j - nums[i]] + nums[i]);
}
}
return dp[sum] == sum;
}
}
494. 目标和
解题思路
- 假设数组总和为sum,其中加法数的总和为x,那么减法数对应的总和就是sum - x。
- 所以我们要求的是 x - (sum - x) = target, 即x = (target + sum) / 2, x必需是一个整数
- 此时问题就转化为,装满容量为x背包,有几种方法。
- 1、dp[j] 表示:填满j(包括j)这么大容积的包,有dp[j]种方法。
- 2、确定递推式,例如dp[5]有多少方法呢,也就是把 所有的 dp[j - nums[i]] 累加起来
- 求组合类问题的通用公式:dp[j] += dp[j - nums[i]] (注意此时求的是累加和,并没有max)
代码
class Solution {
public int findTargetSumWays(int[] nums, int S) {
int sum = 0;
for (int num : nums) {
sum += num;
}
if (Math.abs(S) > sum || (sum + S) % 2 != 0) {
return 0;
}
int target = (sum + S) / 2;
int[] dp = new int[target + 1];
int[] count = new int[target + 1];
Arrays.fill(count, 1);
for (int i = 0; i < nums.length; i++) {
for (int j = target; j >= nums[i]; j--) {
if (dp[j] < dp[j - nums[i]] + nums[i]) {
// 选择当前物品更合适
count[j] = count[j - nums[i]];
} else if (dp[j] == dp[j - nums[i]] + nums[i]) {
// 选不择选择当前物品都合适
count[j] += count[j - nums[i]];
} else {
// 不选择当前物品更合适
}
dp[j] = Math.max(dp[j], dp[j - nums[i]] + nums[i]);
}
}
return count[target];
}
}
1049. 最后一块石头的重量 II
- https://leetcode.cn/problems/last-stone-weight-ii/
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nl6tWRK9-1653289135670)(C:\Users\黄佳林\AppData\Roaming\Typora\typora-user-images\image-20220513161051911.png)]
解题思路
01背包问题,背包容量设置为石头总容量的一半即可。
代码
class Solution {
public int lastStoneWeightII(int[] stones) {
int sum = 0;
for (int stone : stones) {
sum += stone;
}
int weight = sum / 2;
int[] dp = new int[weight + 1];
for (int i = 0; i < stones.length; i++) {
for (int j = weight; j >= stones[i]; j--) {
dp[j] = Math.max(dp[j], dp[j - stones[i]] + stones[i]);
}
}
return Math.abs((sum - dp[weight]) - dp[weight]);
}
}
474. 一和零
- https://leetcode.cn/problems/ones-and-zeroes/
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Urn9BNka-1653289135675)(C:\Users\黄佳林\AppData\Roaming\Typora\typora-user-images\image-20220513162933368.png)]
解题思路
相当于二维费用的背包(同时拥有体积和质量)
代码
class Solution {
public int findMaxForm(String[] strs, int m, int n) {
// dp[i][j] 表示有i个1和j个0最大集合
int[][] dp = new int[m + 1][n + 1];
for (int i = 0; i < strs.length; i++) {
int zeroNum = 0;
int oneNum = 0;
for (char c : strs[i].toCharArray()) {
if (c == '0') zeroNum++;
else oneNum++;
}
for (int j = m; j >= zeroNum; j--) {
for (int k = n; k >= oneNum; k--) {
dp[j][k] = Math.max(dp[j][k], dp[j - zeroNum][k - oneNum] + 1);
}
}
}
return dp[m][n];
}
}
121. 买卖股票的最佳时机
- https://leetcode.cn/problems/best-time-to-buy-and-sell-stock/
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sURrX5HP-1653289135680)(C:\Users\黄佳林\AppData\Roaming\Typora\typora-user-images\image-20220513173222795.png)]
解题思路
循环判断当前是否最小,用当前值减去最小得到股票价值
代码
class Solution {
public int maxProfit(int[] prices) {
int max = 0, min = prices[0];
for (int i = 1; i < prices.length; i++) {
min = Math.min(prices[i], min);
max = Math.max(max, prices[i] - min);
}
return max;
}
}
122. 买卖股票的最佳时机 II
- https://leetcode.cn/problems/best-time-to-buy-and-sell-stock-ii/
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oqxKDoaC-1653289135682)(C:\Users\黄佳林\AppData\Roaming\Typora\typora-user-images\image-20220513173611251.png)]
解题思路
遇到低价就卖出
代码
class Solution {
public int maxProfit(int[] prices) {
int sum=0;
for (int i = 1; i < prices.length; i++) {
if (prices[i]>prices[i-1]){
sum+=prices[i]-prices[i-1];
}
}
return sum;
}
}
- 取巧方法,可以用动态规划但是我看不懂
647. 回文子串
- https://leetcode.cn/problems/palindromic-substrings/
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mfBpeqVM-1653289135686)(C:\Users\黄佳林\AppData\Roaming\Typora\typora-user-images\image-20220513232743096.png)]
解题思路
详情看这个网址,重要的是看清楚遍历顺序
代码
class Solution {
public int countSubstrings(String s) {
int len, ans = 0;
if (s == null || (len = s.length()) < 1) return 0;
//dp[i][j]:s字符串下标i到下标j的字串是否是一个回文串,即s[i, j]
boolean[][] dp = new boolean[len][len];
for (int i = len - 1; i >= 0; i--) { // 注意遍历顺序
for (int j = i; j < len; j++) {
//当两端字母一样时,才可以两端收缩进一步判断
if (s.charAt(i) == s.charAt(j)) {
//i++,j--,即两端收缩之后i,j指针指向同一个字符或者i超过j了,必然是一个回文串
if (j - i < 3) {
dp[i][j] = true;
} else {
//否则通过收缩之后的字串判断
dp[i][j] = dp[i + 1][j - 1];
}
} else {//两端字符不一样,不是回文串
dp[i][j] = false;
}
}
}
//遍历每一个字串,统计回文串个数
for (int i = 0; i < len; i++) {
for (int j = 0; j < len; j++) {
if (dp[i][j]) ans++;
}
}
return ans;
}
}
516. 最长回文子序列
- https://leetcode.cn/problems/longest-palindromic-subsequence/
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QKa8wi0n-1653289135690)(C:\Users\黄佳林\AppData\Roaming\Typora\typora-user-images\image-20220513233919175.png)]
解题思路
代码
public class Solution {
public int longestPalindromeSubseq(String s) {
int len = s.length();
int[][] dp = new int[len + 1][len + 1];
for (int i = len - 1; i >= 0; i--) { // 从后往前遍历 保证情况不漏
dp[i][i] = 1; // 初始化
for (int j = i + 1; j < len; j++) {
if (s.charAt(i) == s.charAt(j)) {
dp[i][j] = dp[i + 1][j - 1] + 2;
} else {
dp[i][j] = Math.max(dp[i + 1][j], Math.max(dp[i][j], dp[i][j - 1]));
}
}
}
// 因为遍历到最后i=0,j=len-11;
return dp[0][len - 1];
}
}
其它(思维拓展)
剑指 Offer 45. 把数组排成最小的数
解题思想
手写排序算法Arrays.sort(strings,(x,y)->(x+y).compareTo(y+x));//注意
代码
class Solution {
public String minNumber(int[] nums) {
if (nums==null||nums.length==0){
return "";
}
String[] strings = new String[nums.length];
int a=0;
for (int num : nums) {
strings[a++]=String.valueOf(num);
}
//System.arraycopy(nums,0,integers,0,nums.length);
Arrays.sort(strings,(x,y)->(x+y).compareTo(y+x));//注意
StringBuilder stringBuilder = new StringBuilder();
for (String s : strings) {
stringBuilder.append(s);
}
return stringBuilder.toString();
}
}
剑指 Offer 57 - II. 和为s的连续正数序列
解题思路
双指针,当前sum小于target则end++,sum大于target则start++;
代码
class Solution {
public int[][] findContinuousSequence(int target) {
//滑动窗口
int start = 1;//左边界
int end = 1;//右边界
int sum = 0;//窗口和
ArrayList<int[]> ints = new ArrayList<>();//用于记录结果集
while (start <= target / 2) {
sum = (start + end) * (end - start + 1) / 2;
if (sum < target) {
end++;
}
if (sum > target) {
start++;
}
if (sum == target) {
int[] tmp = new int[end - start + 1];
int a = 0;
for (int i = start; i <= end; i++) {
tmp[a++] = i;
}
ints.add(tmp);
//成功找到后对start和end初始化
start++;
end = start;
}
}
return ints.toArray(new int[ints.size()][]);
}
}
剑指 Offer 61. 扑克牌中的顺子
解题思路
记录大小王的个数和牌的中断个数,如果出现两张牌一样就false
代码
class Solution {
public boolean isStraight(int[] nums) {
Arrays.sort(nums);
int num0 = 0;//大王小王的个数
int num1 = 0;//中断牌数
for (int i = 0; i < nums.length - 1; i++) {
if (nums[i] == 0) {
num0++;
continue;
}
if (nums[i + 1] - nums[i] > 1) {
num1 += (nums[i + 1] - nums[i] - 1);
} else if (nums[i + 1] - nums[i] == 0) {
return false;
}
}
if (num0 >= num1) {
return true;
} else {
return false;
}
}
}
剑指 Offer 62. 圆圈中最后剩下的数字
解题思路
反推法,最后一个位置肯定是0,然后pos = (pos+m) % i
这里解释的比较清楚
https://leetcode.cn/problems/yuan-quan-zhong-zui-hou-sheng-xia-de-shu-zi-lcof/solution/javajie-jue-yue-se-fu-huan-wen-ti-gao-su-ni-wei-sh/
代码
class Solution {
public int lastRemaining(int n, int m) {
int pos = 0;
// i指的是数组长度
for (int i = 2; i <= n; i++) {
pos = (pos + m) % i;
}
return pos;
}
}
11. 盛最多水的容器
解题思路
双指针法,数组头尾各放置一个指针,如果左边高end++,反之start++;每次循环都记录容器量
代码
class Solution {
public int maxArea(int[] height) {
int a = 0, b = height.length - 1;
int rs = 0;
while (a < b) {
rs = Math.max(rs, (b - a) * Math.min(height[a], height[b]));
if (height[a] < height[b]) {
a++;
} else {
b--;
}
}
return rs;
}
}
15. 三数之和
解题思路
思路
- 标签:数组遍历
- 首先对数组进行排序,排序后固定一个数 nums[i],再使用左右指针指向 nums[i]后面的两端,数字分别为 nums[L] 和 nums[R],计算三个数的和 sum 判断是否满足为 0,满足则添加进结果集
- 如果 nums[i]大于 0,则三数之和必然无法等于 0,结束循环
- 如果
nums[i] == nums[i-1]
,则说明该数字重复,会导致结果重复,所以应该跳过 - 当 sum == 0 时,
nums[L] == nums[L+1]
则会导致结果重复,应该跳过,L++ - 当 sum == 0 时,
nums[R] == nums[R−1]
则会导致结果重复,应该跳过,R−− - 当 sum < 0 时,L++;
- 当 sum > 0 时,R–;
- 时间复杂度:
O(n^2)
,n 为数组长度
代码
class Solution {
public static List<List<Integer>> threeSum(int[] nums) {
List<List<Integer>> ans = new ArrayList();
int len = nums.length;
if(nums == null || len < 3) return ans;
Arrays.sort(nums); // 排序
for (int i = 0; i < len ; i++) {
if(nums[i] > 0) break; // 如果当前数字大于0,则三数之和一定大于0,所以结束循环
if(i > 0 && nums[i] == nums[i-1]) continue; // 去重
int L = i+1; // L为当前数的下一个树的下标
int R = len-1;
while(L < R){
int sum = nums[i] + nums[L] + nums[R];
if(sum == 0){
ans.add(Arrays.asList(nums[i],nums[L],nums[R]));
while (L<R && nums[L] == nums[L+1]) L++; // 去重
while (L<R && nums[R] == nums[R-1]) R--; // 去重
L++;
R--;
}
else if (sum < 0) L++;
else if (sum > 0) R--;
}
}
return ans;
}
}
作者:guanpengchn
链接:https://leetcode-cn.com/problems/3sum/solution/hua-jie-suan-fa-15-san-shu-zhi-he-by-guanpengchn/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
31. 下一个排列(该会了)
解题思想
算法思路:
- 从后往前找出第一个后大于前的位置i与i-1
- 对于i之后的元素排序[i,len)左闭右开
- 排序后找到[i,len)第一个大于i-1位置元素的位置进行交换
- 返回结果
- 如果不存在这种情况,则直接对原数组进行排序
参考
代码
class Solution {
public void nextPermutation(int[] nums) {
if (nums.length == 1) {
return;
}
int a = nums.length - 1, i, j;
for (i = a; i >= 0; i--) {
if (i - 1 >= 0 && nums[i] > nums[i - 1]) {
Arrays.sort(nums, i, nums.length);
for (j = i; j < nums.length; j++) {
if (nums[j] > nums[i - 1]) {
int tmp = nums[j];
nums[j] = nums[i - 1];
nums[i - 1] = tmp;
return;
}
}
}
}
Arrays.sort(nums);
return;
}
}
33. 搜索旋转排序数组
解题思想
思路:
如果中间的数小于最右边的数,则右半段是有序的,
若中间数大于最右边数,则左半段是有序的,
我们只要在有序的半段里用首尾两个数组来判断目标值是否在这一区域内,这样就可以确定保留哪半边了
代码
class Solution {
public int search(int[] nums, int target) {
int a = 0, b = nums.length - 1;
while (a <= b) {
int mid = a + (b - a) / 2;
if (nums[mid] == target) {
return mid;
//4,5,6,7,8,9,0,1,2
} else if (nums[mid] > nums[a]) {//前半段有序
if (nums[mid] > target && target >= nums[a]) {
b = mid-1;
} else {
a = mid+1;
}
} else {//后半段有序
if (nums[mid] < target && target <= nums[b]) {
a = mid+1;
} else {
b = mid-1;
}
}
}
return -1;
}
}
56. 合并区间(这个*)
解题思想
对二维数组先排序,然后和已确定数组进行比较,如果已存入的右边界小于当前左边界或则当前是第一个数组则不存入,否则更新右边界为更大的那一个
代码
class Solution {
public int[][] merge(int[][] intervals) {
//思路:
//1.先将二维数组每一行按第一列排序得到诸如 [ [0,2], [1,5], [6,8], [10,11] ]
//2.循环遍历每一行,给结果数组添加数据,有以下添加情况
//3.对于结果数组 merge 的第一行,直接 add 进去即先将 [0,2] 添加
//4.对于 merge 的其他行,若无重叠也直接添加如 [6,8], [10,11]
//5.若有重叠,则修改上一行如 [0,2], [1,5] -> [0,5]
int n = intervals.length;
//通过 sort 函数对二维数组每一行按第一列元素进行排序
//重写比较器方法,o1[] - o2[] 表示当 o1 大于 o2 时,将 o1 放在 o2 后面,即基本的升序排序
//而 o1[0] - o2[0] 表示按二维数组的每一行第一列元素排序,类似的 o[1] - o2[1]代表按第二列进行排序
Arrays.sort(intervals, new Comparator<int[]>() {
@Override
public int compare(int[] o1, int[] o2) {
return o1[0] - o2[0];
}
});
List<int[]> merge = new ArrayList<int[]>();
for (int i = 0; i < n; i++) {
//创建变量指向每行的左右元素(两列)
int left = intervals[i][0];
int right = intervals[i][1];
//直接 add 的情况:当为第一行或者相邻两行无重叠时
//解释:两行无重叠,即对应在 merge 中上一行的第 1 列小于本行第 0 列
if (merge.size() == 0 || merge.get(merge.size() - 1)[1] < left) {
merge.add(new int[]{left, right});
}
//合并的情况:当有重叠时,将 merge 中上一行的右边界更新
else {
merge.get(merge.size() - 1)[1] = Math.max(merge.get(merge.size() - 1)[1], right);
}
}
//可以学习下此种将 list 转二维数组的方法
return merge.toArray(new int[merge.size()][]);
}
}
452. 用最少数量的箭引爆气球(和这个*)
- https://leetcode.cn/problems/minimum-number-of-arrows-to-burst-balloons/
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-crqYPQet-1653289135693)(C:\Users\黄佳林\AppData\Roaming\Typora\typora-user-images\image-20220514201622489.png)]
解题思路
以上为思考过程,已经确定下来使用贪心了,那么开始解题。
为了让气球尽可能的重叠,需要对数组进行排序。
那么按照气球起始位置排序,还是按照气球终止位置排序呢?
其实都可以!只不过对应的遍历顺序不同,我就按照气球的起始位置排序了。
既然按照起始位置排序,那么就从前向后遍历气球数组,靠左尽可能让气球重复。
从前向后遍历遇到重叠的气球了怎么办?
如果气球重叠了,重叠气球中右边边界的最小值 之前的区间一定需要一个弓箭。
以题目示例: [[10,16],[2,8],[1,6],[7,12]]为例,如图:(方便起见,已经排序)
可以看出首先第一组重叠气球,一定是需要一个箭,气球3,的左边界大于了 第一组重叠气球的最小右边界,所以再需要一支箭来射气球3了。
代码
class Solution {
public int findMinArrowShots(int[][] points) {
if (points.length == 0) return 0;
Arrays.sort(points, (o1, o2) -> Integer.compare(o1[0], o2[0]));
int count = 1;
for (int i = 1; i < points.length; i++) {
if (points[i][0] > points[i - 1][1]) {
count++;
} else {
points[i][1] = Math.min(points[i][1],points[i - 1][1]);
}
}
return count;
}
}
435. 无重叠区间(还有这个*)
- https://leetcode.cn/problems/non-overlapping-intervals/
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JYtgtcSY-1653289135695)(C:\Users\黄佳林\AppData\Roaming\Typora\typora-user-images\image-20220514202353785.png)]
解题思路
本题其实和452.用最少数量的箭引爆气球 (opens new window)非常像,弓箭的数量就相当于是非交叉区间的数量,只要把弓箭那道题目代码里射爆气球的判断条件加个等号(认为[0,1][1,2]不是相邻区间),然后用总区间数减去弓箭数量 就是要移除的区间数量了。
代码
public int eraseOverlapIntervals(int[][] intervals) {
Arrays.sort(intervals, Comparator.comparingInt(o -> o[0]));
int len = intervals.length;
// points 不为空至少需要一支箭
int count = 1;
for (int i = 1; i < len; i++) {
if (intervals[i - 1][1] <= intervals[i][0]) {
// 需要一支箭
count++;
} else {
// 气球i和气球i-1挨着
// 更新重叠气球最小右边界
intervals[i][1] = Math.min(intervals[i - 1][1], intervals[i][1]);
}
}
return len - count;
}
238. 除自身以外数组的乘积
解题思路
巧妙记录每个元素的左右乘积,时间复杂度O(n),空间复杂度0(1)。
a数组下标i记录从下标为0到下标为i的nums元素累乘的结果;
b数组下标i记录从下标为i到下标为len-1的nums元素累乘的结果。
最后只需要取结果a[i-1]*b[i+1]即可。
代码
class Solution {
public int[] productExceptSelf(int[] nums) {
int[] ints = new int[nums.length];
int[] a = new int[nums.length];
int[] b = new int[nums.length];
int sumA=1,sumB=1;
for (int i = 0; i < nums.length; i++) {
sumA*=nums[i];
a[i]=sumA;
sumB*=nums[nums.length-i-1];
b[nums.length-i-1]=sumB;
}
ints[0]=b[1];
ints[nums.length-1]=a[nums.length-2];
for (int i = 1; i < nums.length-1; i++) {
ints[i]=a[i-1]*b[i+1];
}
return ints;
}
}
347. 前 K 个高频元素
解题思路
用map将数字和出现的频率记录,然后将map存入list,对list排序
代码
class Solution {
//https://www.jb51.net/article/90537.htm
public int[] topKFrequent(int[] nums, int k) {
Map<Integer, Integer> map = new HashMap<>();
for (int num : nums) {
if (map.containsKey(num)) {
map.put(num, map.get(num) + 1);
} else {
map.put(num, 1);
}
}
ArrayList<Map.Entry<Integer, Integer>> list = new ArrayList<>(map.entrySet());
list.sort((o1, o2) -> o2.getValue() - o1.getValue());
int[] ints = new int[k];
for (int i = 0; i < k; i++) {
ints[i] = list.get(i).getKey();
}
return ints;
}
}
394. 字符串解码(不懂)
解题思想
字符串解码(辅助栈法 / 递归法,清晰图解)
https://leetcode-cn.com/problems/decode-string/solution/decode-string-fu-zhu-zhan-fa-di-gui-fa-by-jyd/
代码
class Solution {
public String decodeString(String s) {
StringBuilder res = new StringBuilder();
int multi = 0;
LinkedList<Integer> stack_multi = new LinkedList<>();
LinkedList<String> stack_res = new LinkedList<>();
for(Character c : s.toCharArray()) {
if(c == '[') {
stack_multi.addLast(multi);
stack_res.addLast(res.toString());
multi = 0;
res = new StringBuilder();
}
else if(c == ']') {
StringBuilder tmp = new StringBuilder();
int cur_multi = stack_multi.removeLast();
for(int i = 0; i < cur_multi; i++) tmp.append(res);
res = new StringBuilder(stack_res.removeLast() + tmp);
}
else if(c >= '0' && c <= '9') multi = multi * 10 + Integer.parseInt(c + "");
else res.append(c);
}
return res.toString();
}
}
581. 最短无序连续子数组
解题思路
一个简单的思路:
将原数组拷贝一份进行排序,然后与原数组进行对比,从头开始比较找始点,从尾部开始找结束点
另一种方法:
很简单,如果最右端的一部分已经排好序,这部分的每个数都比它左边的最大值要大,同理,如果最左端的一部分排好序,这每个数都比它右边的最小值小。所以我们从左往右遍历,如果i位置上的数比它左边部分最大值小,则这个数肯定要排序, 就这样找到右端不用排序的部分,同理找到左端不用排序的部分,它们之间就是需要排序的部分
代码
class Solution {
public int findUnsortedSubarray(int[] nums) {
int[] snums = nums.clone();
Arrays.sort(snums);
int start = snums.length, end = 0;
for (int i = 0; i < snums.length; i++) {
if (snums[i] != nums[i]) {
start = Math.min(start, i);
end = Math.max(end, i);
}
}
return (end - start >= 0 ? end - start + 1 : 0);
}
}
647. 回文子串
解题思路
中心拓展法,单中心和双中心各拓展一次,遇到回文一般都是这种处理方式
private void count(char[] array, int start, int end) {
while (start >= 0 && end < array.length && array[start] == array[end]) {
max++;
start--;
end++;
}
}
代码
class Solution {
int max = 0;
public int countSubstrings(String s) {
//中心拓展法
char[] array = s.toCharArray();
for (int i = 0; i < array.length; i++) {
count(array, i, i);
count(array, i, i + 1);
}
return max;
}
private void count(char[] array, int start, int end) {
while (start >= 0 && end < array.length && array[start] == array[end]) {
max++;
start--;
end++;
}
}
}
739. 每日温度(记忆)
解题思路
这个题目的标签是 栈 ,我们考虑一下怎么借助 栈 来解决。
不过这个栈有点特殊,它是 递减栈 :栈里只有递减元素。
具体操作如下:
遍历整个数组,如果栈不空,且当前数字大于栈顶元素,那么如果直接入栈的话就不是 递减栈 ,所以需要取出栈顶元素,由于当前数字大于栈顶元素的数字,而且一定是第一个大于栈顶元素的数,直接求出下标差就是二者的距离。
继续看新的栈顶元素,直到当前数字小于等于栈顶元素停止,然后将数字入栈,这样就可以一直保持递减栈,且每个数字和第一个大于它的数的距离也可以算出来。
代码
class Solution {
public int[] dailyTemperatures(int[] temperatures) {
Stack<Integer> stack = new Stack<>();
int[] res = new int[temperatures.length];
for (int i = 0; i < temperatures.length; i++) {
while (!stack.isEmpty() && temperatures[stack.peek()] < temperatures[i]) {
int pop = stack.pop();
res[pop] = i - pop;
}
stack.push(i);
}
return res;
}
}
738. 单调递增的数字
- https://leetcode.cn/problems/monotone-increasing-digits/
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-H1icKYJ8-1653289135701)(C:\Users\黄佳林\AppData\Roaming\Typora\typora-user-images\image-20220514165232015.png)]
解题思路
从右向左扫描数字,若发现当前数字比其左边一位小,则把其左边一位数字减1,并将其右边所有位改成9
代码
public int monotoneIncreasingDigits(int n) {
char[] chars = String.valueOf(n).toCharArray();
for (int i = chars.length - 1; i > 0; i--) {
if (chars[i - 1] > chars[i]) {
chars[i - 1] -= 1;
for (int j = i; j < chars.length; j++) {
chars[j] = '9';
}
}
}
StringBuilder sb = new StringBuilder();
for (char aChar : chars) {
sb.append(aChar);
}
return Integer.parseInt(sb.toString());
}
public int monotoneIncreasingDigits_2(int n) {
char[] chars = String.valueOf(n).toCharArray();
int start = chars.length;
for (int i = chars.length - 1; i > 0; i--) {
if (chars[i - 1] > chars[i]) {
chars[i - 1] -= 1;
start = i;
}
}
for (int j = start; j < chars.length; j++) {
chars[j] = '9';
}
StringBuilder sb = new StringBuilder();
for (char aChar : chars) {
sb.append(aChar);
}
return Integer.parseInt(sb.toString());
}
763. 划分字母区间
- https://leetcode.cn/problems/partition-labels/
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yC9OeZ92-1653289135702)(C:\Users\黄佳林\AppData\Roaming\Typora\typora-user-images\image-20220514195123709.png)]
解题思路
在遍历的过程中相当于是要找每一个字母的边界,如果找到之前遍历过的所有字母的最远边界,说明这个边界就是分割点了。此时前面出现过所有字母,最远也就到这个边界了。
可以分为如下两步:
- 统计每一个字符最后出现的位置
- 从头遍历字符,并更新字符的最远出现下标,如果找到字符最远出现位置下标和当前下标相等了,则找到了分割点
如图:
代码
class Solution {
public List<Integer> partitionLabels(String S) {
List<Integer> list = new LinkedList<>();
int[] edge = new int[26];
char[] chars = S.toCharArray();
for (int i = 0; i < chars.length; i++) {
edge[chars[i] - 'a'] = i;
}
int idx = 0;
int last = -1;
for (int i = 0; i < chars.length; i++) {
idx = Math.max(idx,edge[chars[i] - 'a']);
if (i == idx) {
list.add(i - last);
last = i;
}
}
return list;
}
}