链表
剑指 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. 不同的二叉搜索树
解题思路
假设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);
}
}
}
动态规划(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++) {
dp[i] = Math.max(dp[i],dp[i-j]*dp[j]);
}
}
return dp[n];
}
}
剑指 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];
}
}
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()][]);
}
}
70. 爬楼梯
解题思想
可以一次上一个阶梯,也可以一次上两次阶梯
代码
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];
}
}
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;
}
}
322. 零钱兑换
解题思路
可以看成01背包问题,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];
}
}
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];
}
}
其它(思维拓展)
剑指 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
代码
class Solution {
public int lastRemaining(int n, int m) {
int pos = 0;
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;
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;
}
}
238. 除自身以外数组的乘积
解题思路
巧妙记录每个元素的左右乘积,时间复杂度O(n),空间复杂度0(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;
}
}