刷题总结
判空条件
字符串的判空和返回
if (digits == null)return null;
char[] chars = digits.toCharArray();
if (chars.length == 0)return null;
集合的判空和返回
if (nums == null) return null;
List<List<Integer>> result = new ArrayList<>();
if (nums.length < 3) return result;
数组的判空和返回
if (candidates == null || candidates.length == 0)return null;
字符数组和字符串的操作
- 对于string的传参就是对字符数组的传参
- 取二维数组中某一行:char[] letters = lettersArray[chars[idx] - ‘2’];
- 字符数组转String:list.add(new String(string));
- 对字符串的操作实际上就是对字符数组的操作:char[] string = new char[chars.length];
链表中的操作
三数之和中【15中等】
- 向一个链表中添加链表可以用Arrays.asList()内部多个元素
result.add(Arrays.asList(nums[i], nums[l], nums[r]));
集合操作
集合当作参数传递
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SIa12VHS-1677908641240)(…/…/…/%E5%AD%A6%E4%B9%A0%E8%B5%84%E6%96%99/100_%E7%AC%94%E8%AE%B0/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E4%B8%8E%E7%AE%97%E6%B3%95/assets/1677504173946.png)]
队列的操作
- 创建队列:Queue queue = new ArrayDeque<>();
- ArrayDeque内部以数组的形式保存集合中的元素,因此随机访问元素时有较好的性能;而LinkedList内部以链表的形式来保存集合中的元素,因此随机访问集合中的元素时虽然性能较差,但在插入、删除元素时性能非常出色
- offer()性能比add()性能高,满队列add抛异常,offer返回false
- poll()性能比remove()性能高,空队列remove抛异常,poll返回null
- 访问队列头元素:queue.peek()
深度优先遍历中
- result.add(new ArrayList<>(tmp));
- 不可以result.add(tmp);
第一章 数组2023.2.24
颜色分类【75中等】
- 三指针的应用
合并两个有序数组【88简单】
- 数组拷贝的技巧System.arraycopy(nums2, 0, nums1, 0, len2 + 1);
- 表示将nums2数组从下标0位置开始,拷贝到nums1数组中,从下标0位置开始,长度为len2+1、
有序数组的平方【977简单】
- 首尾指针出现,用while循环
- 循环结束条件分析需要包括等于的情况
- 举例原理就两个元素【1,2】,Left<Right那么确定result[1]的元素为4,right–为1
- 若结束条件为L<R,那么1=1结束,但是result[0]的值未覆盖,所以结束条件L<=R
面试题16 部分排序【中等】
- 如1 2 3 4 6 8 11 15 3 4 5
- 从左往右应该逐渐增大,需要排序的是寻找最后一个变小的位置,相反同理找最后一个变大的数
- 相等的话也应该记录那个位置,否则如果连续的这个数,它很可能应该在中间位置而不是两边
第二章 链表
移除链表等于val的所有元素【203简单】
-
创建虚拟头节点,tail指向虚拟头节点
-
ListNode dummyHead = new ListNode(0); ListNode last = dummyHead;
-
-
while循环结束需要将最后的tail的next指针清空。即
newTail.next = null;
-
最后返回是
-
dummyHead.next
-
两数相加【2中等】
- 创建虚拟头节点,tail指向虚拟头节点
- 注意:
- 如果一个不为空,就可以进行+操作,只不过另一个数初始化为0就好所以while中用或逻辑
- 内部判断链表不为空才可取next
- 取数如果有一个链表为空初始值应该为0,所以在最开始定义
- 最后需要指针右移
- 检查进位
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Uad8WkDO-1677908641242)(…/…/…/%E5%AD%A6%E4%B9%A0%E8%B5%84%E6%96%99/100_%E7%AC%94%E8%AE%B0/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E4%B8%8E%E7%AE%97%E6%B3%95/assets/1677303792275.png)]
BM11链表相加(二)【中等】
- 反转链表+两个链表相加
相交链表【160简单】
-
a + b = b + a;
-
暂存原来头节点,while不相等时:
headA = (headA == null) ? preB : headA.next; headB = (headB == null) ? preA : headB.next;
分隔链表【86中等】
-
两个虚拟头节点合并
-
最后右边链表的尾节点需要清空【同1题】
-
leftTail.next赋值后需要将leftTail指针后移,rightTail也同理
-
rightTail.next = head; rightTail = head; // 或者rightTail = rightTail.next
-
反转链表【206简单】背下来
public ListNode reverseList(ListNode head) {
if (head == null)return null;
if (head.next == null) return head;
// 函数返回什么类型就定义个新变量
ListNode newHead = null;
while (head != null) {
ListNode tmp = head.next;
head.next = newHead;
newHead = head; //指针后移【同4的第三步】
head = tmp;
}
return newHead;
}
BM2链表内指定区间反转【中等】
- 找到m-1个节点,因为需要更新m的前一个节点的 next,由于从虚拟头结点开始,所以遍历 m-1 次找到m的前一个节点
- 确定m的节点位置将m的下一个节点头插法插到m的前一个
- [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0GICVHlF-1677908641243)(…/…/…/%E5%AD%A6%E4%B9%A0%E8%B5%84%E6%96%99/100_%E7%AC%94%E8%AE%B0/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E4%B8%8E%E7%AE%97%E6%B3%95/assets/1677292942066.png)]
public ListNode reverseBetween (ListNode head, int m, int n) {
if (head == null) return null;
ListNode dummyNode = new ListNode(0);
ListNode pre = dummyNode;
dummyNode.next = head;
// 找到第m - 1个节点,即需要更新m的前一个节点的 next,由于从虚拟头结点开始,所以遍历 m-1 次找到m的前一个节点
for (int i = 0; i < m - 1; i++) {
pre = pre.next;
}
// 那么第m个节点就是pre.next
ListNode cur_m = pre.next;
// 将m的后一个节点插入到m之前并更新当前节点,前插所以需要找到m的前一个节点
for (int i = m; i < n; i++) {
ListNode tmp = cur_m.next; // 找到m的下一个节点
cur_m.next = tmp.next; // 更新m的next指针存的是 原来的next.next即tmp.next
tmp.next = pre.next; // tmp.next存的是当前节点即事先保存的pre.next
pre.next = tmp; // 当前指针后移,由于前插所以是pre后移
}
return dummyNode.next;
}
BM3链表中的节点每k个一组翻转【中等】
public ListNode reverseKGroup (ListNode head, int k) {
if(head==null) return head;
ListNode dummyNode = new ListNode(-1);
dummyNode.next = head;
int length = 0;
ListNode pre = dummyNode;ListNode cur = head;
while (head != null) {
length++;
head = head.next;
}
ListNode tmp = null;
for (int i = 0; i < length / k; i++) {
for (int j = 1; j < k; j++) {
tmp = cur.next;
cur.next = tmp.next;
tmp.next = pre.next;
pre.next = tmp;
}
pre = cur;
cur = cur.next;
}
return dummyNode.next;
}
快慢指针的应用【9-13】
回文链表【234简单】重点题
- 找中间节点【用快慢指针】
- 对中间节点的下一个节点开始反转【reserveList(mid.next)】
- while中要判断【rightHead!=null】不可以是leftHead,因为左半部分到中间节点都没有为null,只有右半部分在反转时候尾节点置为null
public boolean isPalindrome(ListNode head) {
// 空链表 或 只有一个节点
if (head == null || head.next == null)return true;
if (head.next.next == null) return head.val == head.next.val;
// 找中间节点
ListNode mid = middleNode(head);
ListNode rHead = reserveList(mid.next);
ListNode lHead = head;
ListNode rOldHead = rHead;
boolean result = true;
while (rHead != null) {
if (rHead.val != lHead.val) {
result = false;
break;
}
rHead = rHead.next;
lHead = lHead.next;
}
// 恢复右半部分(对右半部分再次反转
reserveList(rOldHead);
return result;
}
private ListNode middleNode(ListNode head) {
ListNode fast = head;
ListNode slow = head;
while (fast.next != null && fast.next.next != null) {
slow = slow.next;
fast = fast.next.next;
}
return slow;
}
// 反转链表
private ListNode reserveList(ListNode head) {
ListNode newHead = null;
while (head != null) {
ListNode tmp = head.next;
head.next = newHead;
newHead = head;
head = tmp;
}
return newHead;
}
找中间节点
private ListNode middleNode(ListNode head) {
ListNode fast = head;
ListNode slow = head;
while (fast.next != null && fast.next.next != null) {
slow = slow.next;
fast = fast.next.next;
}
return slow;
}
环形链表【141简单】
- 快慢指针
- 如果不是环while必会结束到尾节点是,所以return false;
- 如果是环,则相等直接return true;
public boolean hasCycle(ListNode head) {
if (head == null)return false;
ListNode slow = head;
ListNode fast = head;
// 1 2 3 4 5->奇数1 3 5,判断fast.next==null?
// 1 2 3 4->偶数1 3 null,判断fast==null?
while (fast != null && fast.next != null) {
slow = slow.next;
fast = fast.next.next;
if (fast == slow)
return true;
}
return false;
}
BM7 链表中环的入口结点【中等】
- 问题9环形链表的应用,只不过设置个标识位boolean flag
- slow == fast:将fast移动到头节点,slow和fast重新next就确定入口
- 如1 2 3 4 5在2处为入口节点
- fast 1->3->5->3->5->3->5
- slow 1->2->3->4->5
- 所以此时fast移动到1,slow在5(第5次),同时next找到相等的地方
public ListNode EntryNodeOfLoop(ListNode pHead) {
if (pHead == null)
return null;
ListNode fast = pHead;
ListNode slow = pHead;
boolean flag = false;
while (fast != null && fast.next != null) {
fast = fast.next.next;
slow = slow.next;
if (fast == slow) {
flag = true;
break;
}
}
if (flag) {
fast = pHead;
while (fast != slow) {
slow = slow.next;
fast = fast.next;
}
return fast;
} else
return null;
}
剑指 Offer 22. 链表中倒数第k个节点【简单】
- 最后k个那让fast指针先移动k步,然后slow和fast再一起移动不就好了吗
public ListNode getKthFromEnd(ListNode head, int k) {
if (head == null || k < 0) return null;
ListNode slow = head;
ListNode fast = head;
for (int i = 0; i < k; i++) {
if (fast != null)
fast = fast.next;
else
return null;
}
while (fast != null) {
slow = slow.next;
fast = fast.next;
}
return slow;
}
BM14链表的奇偶重排【简单】
- 奇偶项分别后移
public ListNode oddEvenList (ListNode head) {
if (head == null)
return head;
ListNode slow = head;
ListNode fast = head.next;
ListNode last = fast;
while (fast != null && fast.next != null) {
slow.next = slow.next.next;
fast.next = fast.next.next;
slow = slow.next;
fast = fast.next;
}
slow.next = last;
return head;
}
删除链表中的节点实则隐藏【237中等】
-
既然不删除,呢么就直接充当它的下一个节点
-
node.val = node.next.val; node.next = node.next.next;
合并两个有序链表【21简单】
- 创建虚拟头节点,tail指向虚拟头节点
- while中判断两个链表都不能为空,因为一个为空,则比较无法进行
- 最后将tail的next指向不为空的那一个,也可能直接指向kong
归并排序的应用
合并k个升序链表【23困难】
- 归并排序的应用:归并+二分
-
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fv1Cwu3t-1677908641245)(…/…/…/%E5%AD%A6%E4%B9%A0%E8%B5%84%E6%96%99/100_%E7%AC%94%E8%AE%B0/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E4%B8%8E%E7%AE%97%E6%B3%95/assets/1677249068640.png)]
public ListNode mergeKLists(ListNode[] lists) {
return merge(lists, 0, lists.length - 1);// [0,length - 1]
}
private ListNode merge(ListNode[] listNodes, int l, int r) {
if (l == r) return listNodes[l];
while (l < r) {
int mid = (l + r) >> 1;
// 0 1 2 3 4 ->mid = (0 + 4) / 2 = 2
// 0 1 2 3 4 5 ->mid = (0 + 5) / 2 = 2
ListNode listNode1 = merge(listNodes, l, mid);
ListNode listNode2 = merge(listNodes, mid + 1, r);
return mergeTwoLists(listNode1, listNode2);
}
return null;
}
二叉树
- 绝大部分题目都可以直接通过递归 遍历解决
- 递归需要想清楚这个方法的功能然后递归调用
- 二叉搜索树的中序遍历是升序的
递归三部曲
- 确定递归函数的参数和返回值
- 确定终止条件
- 确定单层递归的逻辑
二叉树的前序、中序、后序遍历
public int[] preorderTraversal (TreeNode root) {
//添加遍历结果的数组
List<Integer> list = new ArrayList();
//递归前序遍历
preorder(list, root);
//返回的结果
int[] res = new int[list.size()];
for(int i = 0; i < list.size(); i++)
res[i] = list.get(i);
return res;
}
public void preorder(List<Integer> list, TreeNode root){
//遇到空节点则返回
if(root == null)
return;
//先遍历根节点
list.add(root.val);
//再去左子树
preorder(list, root.left);
//最后去右子树
preorder(list, root.right);
}
// 递归
public int[] inorderTraversal (TreeNode root) {
List<Integer> list = new ArrayList<>();
inorder(list, root);
int []result = new int[list.size()];
for (int i = 0; i < list.size(); i++) {
result[i] = list.get(i);
}
return result;
}
public void inorder(List<Integer> list, TreeNode root) {
if (root == null)
return;
inorder(list, root.left);
list.add(root.val);
inorder(list, root.right);
}
// 递归
public int[] postorderTraversal (TreeNode root) {
List<Integer> list = new ArrayList<>();
postorder(list, root);
int []result = new int[list.size()];
for (int i = 0; i < list.size(); i++) {
result[i] = list.get(i);
}
return result;
}
private void postorder(List<Integer> list, TreeNode root) {
if (root == null)
return;
postorder(list, root.left);
postorder(list, root.right);
list.add(root.val);
}
层序遍历
二叉树的层序遍历【102中等】
public List<List<Integer>> levelOrder(TreeNode root) {
List<List<Integer>> result = new ArrayList<>();
if (root == null)return result;
Queue<TreeNode> queue = new ArrayDeque<>();
queue.add(root);
while (!queue.isEmpty()) {
List<Integer> tmp = new ArrayList<>();
int num = queue.size();
for (int i = 0; i < num; i++) {
TreeNode cur = queue.poll();
tmp.add(cur.val);
if (cur.left != null)
queue.add(cur.left);
if (cur.right != null)
queue.add(cur.right);
}
result.add(tmp);
}
return result;
}
从最后一层层序遍历【107中等】
result.add(0, tmp);
二叉树的右视图【199中等】
- 也是层序遍历,只不过当这一层遍历到队列的最后一个才加入到结果
public List<Integer> rightSideView(TreeNode root) {
List<Integer> result = new ArrayList<>();
Queue<TreeNode> queue = new ArrayDeque<>();
if (root == null)return result;
queue.add(root);
while (!queue.isEmpty()) {
int size = queue.size();
for (int i = 0; i < size; i++) {
TreeNode cur = queue.poll();
if (cur.left != null) {
queue.add(cur.left);
}
if (cur.right != null) {
queue.add(cur.right);
}
if (i == size - 1)
result.add(cur.val);
}
}
return result;
}
二叉树的层平均值【637简单】
- 二叉树的层平均值
public List<Double> averageOfLevels(TreeNode root) {
List<Double> result = new ArrayList<>();
Queue<TreeNode> queue = new ArrayDeque<>();
if (root == null) return result;
queue.add(root);
while (!queue.isEmpty()) {
int size = queue.size();
double tmp = 0;
for (int i = 0; i < size; i++) {
TreeNode cur = queue.poll();
if (cur.left != null)
queue.add(cur.left);
if (cur.right != null)
queue.add(cur.right);
tmp += cur.val;
}
result.add(tmp / size);
}
return result;
}
N 叉树的层序遍历【429中等】
class Node {
public int val;
public List<Node> children;
public Node() {}
public Node(int _val) {
val = _val;
}
public Node(int _val, List<Node> _children) {
val = _val;
children = _children;
}
}
public List<List<Integer>> levelOrder(Node root) {
List<List<Integer>> result = new ArrayList<>();
Queue<Node> queue = new ArrayDeque<>();
if (root == null) return result;
queue.add(root);
while (!queue.isEmpty()) {
int size = queue.size();
List<Integer> tmp = new ArrayList<>();
// 将这一层的节点分别取出,把他们的孩子节点依次加入
for (int i = 0; i < size; i++) {
Node cur = queue.poll();
tmp.add(cur.val);
List<Node> cur_children = cur.children;
if (cur_children == null || cur_children.size() == 0)
continue;
for (Node child : cur_children) {
if (child != null)
queue.add(child);
}
}
result.add(tmp);
}
return result;
}
填充每个节点的下一个右侧节点指针【116,117中等】
public Node connect(Node root) {
Queue<Node> queue = new ArrayDeque<>();
if (root != null) queue.add(root);
while (!queue.isEmpty()) {
int size = queue.size();
for (int i = 0; i < size; i++) {
Node cur = queue.poll();
if (i < size - 1)
cur.next = queue.peek(); // 当前节点的next等于队列第一个节点
// 扩展下一层节点
if (cur.left != null) queue.add(cur.left);
if (cur.right != null)queue.add(cur.right);
}
}
return root;
}
左叶子之和【404简单】
平时我们解二叉树的题目时,已经习惯了通过节点的左右孩子判断本节点的属性,而本题我们要通过节点的父节点判断本节点的属性。
public int sumOfLeftLeaves(TreeNode root) {
if (root == null) return 0;
int result = 0;
Queue<TreeNode> queue = new LinkedList<>();
queue.offer(root);
while (!queue.isEmpty()) {
int size = queue.size();
for (int i = 0; i < size; i++) {
TreeNode node = queue.poll();
if (node.left != null) {
queue.offer(node.left);
if (node.left.left == null && node.left.right == null)
result += node.left.val;
}
if (node.right != null) queue.add(node.right);
}
}
return result;
}
找树左下角的值【513中等】
- 只需要记录最后一行第一个节点的数值就可以了。
public int findBottomLeftValue(TreeNode root) {
if (root == null) return 0;
Queue<TreeNode> queue = new LinkedList<>();
queue.offer(root);
int result = 0;
while (!queue.isEmpty()) {
int size = queue.size();
for (int i = 0; i < size; i++) {
TreeNode node = queue.poll();
if (node.left != null)
queue.offer(node.left);
if (node.right != null)
queue.offer(node.right);
if (i == 0)
result = node.val;
}
}
return result;
}
二叉树的深度
最大深度【104简单】
public int maxDepth(TreeNode root) {
if (root == null)return 0;
int L = 0, R = 0;
if (root.left != null || root.right != null) {
L = maxDepth(root.left);
R = maxDepth(root.right);
}
return Math.max(L, R) + 1;
}
最小深度【111简单】
- 只有当左右孩子都为空的时候,才说明遍历的最低点了。如果其中一个孩子为空则不是最低点
public int minDepth(TreeNode root) {
if (root == null) return 0;
if (root.left != null && root.right == null)
return minDepth(root.left) + 1;
if (root.right != null && root.left == null)
return minDepth(root.right) + 1;
return Math.min(minDepth(root.left), minDepth(root.right)) + 1;
}
平衡二叉树【110简单】
-
平衡二叉树:一个二叉树每个节点 的左右两个子树的高度差的绝对值不超过 1 。
-
public boolean isBalanced(TreeNode root) { if (root == null) return true; else 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; int L = 0, R = 0; if (root.left != null || root.right != null) { L = depth(root.left); R = depth(root.right); } return Math.max(L, R) + 1; }
翻转二叉树【226简单】
public TreeNode invertTree(TreeNode root) {
if (root == null)return null;
// 交换他的左右孩子:先序遍历,先交换根节点的左右孩子,再递归
TreeNode tmp = root.left;
root.left = root.right;
root.right = tmp;
invertTree(root.left);
invertTree(root.right);
return root;
}
相同的树【100简单】
public boolean isSameTree(TreeNode p, TreeNode q) {
if (p == null && q == null)
return true;
else if (p == null || q == null)
return false;
else if (p.val != q.val)
return false;
else
return isSameTree(p.left, q.left) && isSameTree(p.right, q.right);
}
对称(镜像)二叉树【101简单】
左的左和右的右
&&左的右和右的左
都得相等
public boolean isSymmetric(TreeNode root) {
if (root == null)
return true;
else
return compare(root.left, root.right);
}
private boolean compare(TreeNode left, TreeNode right) {
if (left != null && right != null) {
if (left.val == right.val)
return compare(left.left, right.right) && compare(left.right, right.left);
else
return false;
} else if (left == null && right == null)
return true;
else
return false;
}
完全二叉树得节点个数【222中等】
递归求解
- 确定递归函数的参数和返回值
- 参数:根节点,返回值以root为根节点的节点个数
- 确定终止条件
- 如果为空节点的话,就返回0,表示节点数为0。
- 确定单层递归的逻辑
- 先求它的左子树的节点数量,再求右子树的节点数量,最后取总和再加一 ,加1就是目前节点为根节点的节点数量。
public int countNodes(TreeNode root) {
if(root == null) {
return 0;
}
return countNodes(root.left) + countNodes(root.right) + 1;
}
迭代求解
- 层序遍历,出队结果加一
另一棵树的子树【572简单】
递归-相同的树的应用
public boolean isSameTree(TreeNode p, TreeNode q) {
if (p == null && q == null)
return true;
else if (p == null || q == null)
return false;
else if (p.val != q.val)
return false;
else
return isSameTree(p.left, q.left) && isSameTree(p.right, q.right);
}
public boolean isSubtree(TreeNode root, TreeNode subRoot) {
if (subRoot == null)
return true;
else if (root == null && subRoot != null)
return false;
else
return isSubtree(root.left, subRoot) || isSubtree(root.right, subRoot) || isSameTree(root, subRoot);
}
后序遍历-序列化树
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sZ2dE5dv-1677908641261)(…/…/…/%E5%AD%A6%E4%B9%A0%E8%B5%84%E6%96%99/100_%E7%AC%94%E8%AE%B0/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E4%B8%8E%E7%AE%97%E6%B3%95/assets/1677594927493.png)]
// 二叉树的序列化
public boolean isSubtree(TreeNode root, TreeNode subRoot) {
if (root == null || subRoot == null)
return false;
return postSerialize(root).contains(postSerialize(subRoot));
}
// 利用后序遍历进行序列化
private String postSerialize(TreeNode root) {
StringBuilder sb = new StringBuilder();
postSerialize(root, sb);
return sb.toString();
}
private void postSerialize(TreeNode node, StringBuilder sb) {
if (node.left == null)
sb.append("#!");
else
postSerialize(node.left, sb);
if (node.right == null)
sb.append("#!");
else
postSerialize(node.right, sb);
sb.append(node.val).append("!");
}
构建二叉树
- 整体流程
- 写一个findRoot的方法,传进去一些参数,通过它返回根节点,再递归左和右
从中序与后序遍历序列构造二叉树【106中等】
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KcHoy88a-1677908641267)(…/…/…/%E5%AD%A6%E4%B9%A0%E8%B5%84%E6%96%99/100_%E7%AC%94%E8%AE%B0/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E4%B8%8E%E7%AE%97%E6%B3%95/assets/1677654357505.png)]
Map<Integer, Integer> inorderMap = new HashMap<>();
public TreeNode buildTree(int[] inorder, int[] postorder) {
for (int i = 0; i < inorder.length; i++) {
inorderMap.put(inorder[i], i);
}
return findNode(inorder, 0, inorder.length,
postorder, 0, postorder.length);
}
// 左闭右开区间
private TreeNode findNode(int[] inorder, int inBegin, int inEnd, int[] postorder, int postBegin, int postEnd) {
// 1递归结束条件
if (inBegin >= inEnd || postBegin >= postEnd)
return null;
// 2.后序数组的最后一个元素为根节点
int rootIndex = inorderMap.get(postorder[postEnd - 1]);
TreeNode root = new TreeNode(inorder[rootIndex]);
// 3.切中序数组、切后序数组、递归处理左区间
int lenOfLeft = rootIndex - inBegin; // 保存中序左子树个数,用来确定后序数列的个数
root.left = findNode(inorder, inBegin, rootIndex,
postorder, postBegin, postBegin + lenOfLeft);
root.right = findNode(inorder, rootIndex + 1, inEnd,
postorder, postBegin + lenOfLeft, postEnd - 1);
return root;
}
最大二叉树【654中等】
- 从中序遍历构造一个二叉搜索树的过程
public TreeNode constructMaximumBinaryTree(int[] nums) {
if (nums == null)return null;
return findRoot(nums, 0, nums.length);
}
// 左闭右开
private TreeNode findRoot(int[] nums, int left, int right) {
// 1. 由于左闭右开,如【1,1)是不合法区间,所以left=right时候也是不合法
if (left >= right) return null;
// 2. 找到最大值对应的索引
int maxIndex = left;
for (int i = left; i < right; i++) {
if (nums[i] > nums[maxIndex])
maxIndex = i;
}
// 3. 创建根节点
TreeNode node = new TreeNode(nums[maxIndex]);
// 4. 递归左右子树,因为中序遍历,所以确定左区间为左子树,右区间为右子树
node.left = findRoot(nums, left, maxIndex);
node.right = findRoot(nums, maxIndex + 1, right);
// 5. 返回跟节点
return node;
}
合并两个二叉树【617简单】
- 首先判断root1和root2是否都等于null
- 再判断全部等于null的情况,需要求递归左和右子树
- 有一个为空,返回另一个
public TreeNode mergeTrees(TreeNode root1, TreeNode root2) {
if (root1 == null && root2 == null) return null;
if (root1 != null && root2 != null) {
root1.val += root2.val;
root1.left = mergeTrees(root1.left, root2.left);
root1.right = mergeTrees(root1.right, root2.right);
return root1;
}
return root1 == null ? root2 : root1;
}
二叉搜索树中的搜索【700简单】
- 返回节点的值等于val的二叉搜索树
public TreeNode searchBST(TreeNode root, int val) {
if (root == null)
return null;
if (root.val == val)
return root;
else if (root.val < val)
return searchBST(root.right, val);
else
return searchBST(root.left, val);
}
二叉搜索树-中序遍历升序
验证二叉搜索树【98中等】
- 这个节点的值比左边最大的要小,比右边最小的要大
- 所以定义如果只有一个根节点,那么它左边最大值为Long.MIN_VALUE,右边最小为Long.MAX_VALUE
public boolean isValidBST(TreeNode root) {
return isBST(root, Long.MIN_VALUE, Long.MAX_VALUE);
}
private boolean isBST(TreeNode root, long minValue, long maxValue) {
if (root == null)
return true;
if (root.val <= minValue || root.val >= maxValue)
return false;
return isBST(root.left, minValue, root.val) &&
isBST(root.right, root.val, maxValue);
}
恢复二叉搜索树-中序遍历【99中等】
- 问题描述
- 这棵二叉树只有两个节点位置错误,所以找到他们的位置
- 解题思路
- 二叉搜索树的中序遍历是升序
- 所以第一个错误点从左往右找到第一个逆序对较大那一个
- 第二个错误点从左往右最后一个逆序对较小的点
/**
* 上一次中序遍历过的节点
*/
private TreeNode prev;
/**
* 第一个错误节点
*/
private TreeNode first;
/**
* 第二个错误节点
*/
private TreeNode second;
/**
* @param root :是一棵错误的二叉搜索树(有两个节点被错误交换)
*/
public void recoverTree(TreeNode root) {
// 二叉搜索树的中序遍历是升序
// 所以第一个错误点找第一个逆序对的较大节点
// 第二个错误点最后一个逆序对较小的点
findWrongNode(root);
// 交换两个错误点的值
int tmp = first.val;
first.val = second.val;
second.val = tmp;
}
// 中序遍历实现
private void findWrongNode(TreeNode root) {
if (root == null) return; // 递归结束条件
findWrongNode(root.left);
// 出现逆序对
if (prev != null && prev.val > root.val){
// 第二个错误节点是最后一个逆序对中较小的那个节点
second = root;
// 第一个错误节点是第一个逆序对中较大的那个节点,所以如果有值就不做操作
if (first != null)
return;
first = prev;
}
prev = root;
findWrongNode(root.right);
}
最大的BST子树
自顶向下(前序遍历)
- 解题思路
- 方法作用:以root为根节点的二叉树最大的BST子树的个数
- 如果root为根节点的二叉树是BST,则返回左子树个数+右子树个数+1(本身)
- 不是BST,需要取左右左右的最大
- 根本目标编写一个方法判断以root为根节点的二叉树是不是BST
- BST性质:root.val > 左边最大且小于右边最小,不满足直接return false
- 否则判断左和右都是不是BST,其中如果加入root为BST,则左的max为root.val,相反右的min为root.val
- 方法作用:以root为根节点的二叉树最大的BST子树的个数
public int largeBSTSubtree(TreeNode root){
if (root == null)return 0;
if (isBST(root, Integer.MIN_VALUE, Integer.MAX_VALUE))
return 1 + getCount(root.left) + getCount(root.right);
// root不是BST,返回左右子树较大那个
int left = largeBSTSubtree(root.left);
int right = largeBSTSubtree(root.right);
return Math.max(left, right);
}
private boolean isBST(TreeNode root, int min, int max) {
if (root == null)return true;
// 节点值不满足要求
if (root.val <= min || root.val >= max)
return false;
// 满足要求递归计算左和右是否为BST,其中如果加入root为BST,则左的max为root.val
return isBST(root.left, min, root.val) &&
isBST(root.right, root.val, max);
}
private int getCount(TreeNode root){
if (root == null)
return 0;
return 1 + getCount(root.left) + getCount(root.right);
}
自底向上(后序遍历)
/**
* 最大BST子树的信息
*/
private static class Info{
public TreeNode root; // 根节点
public int size; // 节点总数
public int max; // 最大值
public int min; // 最小值
public Info(TreeNode root, int size, int max, int min) {
this.root = root;
this.size = size;
this.max = max;
this.min = min;
}
}
/**
* 返回以root为根节点的二叉树的最大BST子树信息
* 自底向上 后序遍历
*/
private Info getInfo(TreeNode root) {
if (root == null) return null;
// left info:左子树的最大BST子树信息 可能为空
Info leftInfo = getInfo(root.left);
Info rightInfo = getInfo(root.right);
/**
* 获取到左和右的最大BST子树信息就是想推以root为根节点的最大BST子树的信息
* 加上root还是BST需要满足root.val > left.max && root.val < right.val
* 左子树是BST,右没有 并且root.val > left.max 也是BST
* 右子树是BST,左没有 并且root.val < right.val 也是BST
* 左右都没有,他也是BST,单节点肯定是BST
*/
// 1. 以root为根节点的二叉树并是BST
// -1 为了防止左为空而右不是BST的情况错误计算,所以判断leftBstSize和rightBstSize>=0就可以将左右和root串起来
int leftBstSize = -1, rightBstSize = -1, max = root.val, min = root.val;
if (root.left == null) {
leftBstSize = 0;
} else if (leftInfo.root == root.left && root.val > leftInfo.max) {
leftBstSize = leftInfo.size;
min = leftInfo.min;
}
if (root.right == null)
rightBstSize = 0;
else if (rightInfo.root == root.right && root.val < rightInfo.min) {
rightBstSize = rightInfo.size;
max = rightInfo.max;
}
// 必须符合上面其中一个条件,否则有左为空而右边不是BST的情况,这时不可以创建Info,所以初始值用-1
if (leftBstSize >= 0 && rightBstSize >= 0)
return new Info(root, leftBstSize + rightBstSize + 1, max, min);
// 2. 以root为根节点的二叉树并不是BST:左右都不为空
// 返回最大BST子树的节点数量最多的那一个Info
if (leftInfo != null && rightInfo != null)
return (leftInfo.size > rightInfo.size) ? leftInfo : rightInfo;
// 返回leftInfo rightInfo中不为null的呢个Info
return (leftInfo != null) ? leftInfo : rightInfo;
}
/**
* 以root为根节点的二叉树最大的BST子树
* @param root
* @return
*/
public int largeBSTSubtree(TreeNode root){
return (root == null) ? 0 : getInfo(root).size;
}
二叉搜索树的最小绝对值【530简单】
遇到在二叉搜索树上求什么最值,求差值之类的,都要思考一下二叉搜索树可是有序的,要利用好这一特点。
最小绝对值肯定是相邻的两个节点
TreeNode pre = null;
int result = 0;
public int getMinimumDifference(TreeNode root) {
if (root == null)return 0;
inOrder(root);
return result;
}
public void inOrder(TreeNode root){
if(root == null)
return;
// 左
inOrder(root.left);
// 最小肯定是某两个相邻
if (pre != null)
result = Math.min(result, root.val - pre.val);
pre = root;
// 右
inOrder(root.right);
}
二叉搜索树中的众数【501简单】
List<Integer> result;
int maxCount = 0;
int count = 0;
TreeNode pre = null;
public int[] findMode(TreeNode root) {
result = new ArrayList<>();
inOrder(root);
int[] ans = new int[result.size()]; // 不可放在上一行前面,因为通过inOrder函数才可以得到result.size()
for(int i = 0; i < result.size(); i++)
ans[i] = result.get(i);
return ans;
}
public void inOrder(TreeNode root){
if (root == null)return;
inOrder(root.left);
int rootValue = root.val;
// 计数pre == null第一个节点
if (pre == null)
count = 1;
else if (rootValue != pre.val) // 与前一个节点数值不相同
count = 1;
else if (pre.val == rootValue) // 与前一个节点数值相同
count++;
pre = root;
//更新结果以及maxCount
if (count > maxCount) { // 如果计数大于最大值,原先存的结果清空,因为个数不是最多
maxCount = count;// 更新最大频率
result.clear();// 很关键的一步,不要忘记清空result,之前result里的元素都失效了
result.add(rootValue);
} else if (count == maxCount)
result.add(rootValue);
inOrder(root.right);
}
二叉搜索树的最近公共祖先【235中等】
- root的val比p、q的val都大,则在左子树找公共节点
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
if (root.val > p.val && root.val > q.val)
return lowestCommonAncestor(root.left, p ,q);
if (root.val < p.val && root.val < q.val)
return lowestCommonAncestor(root.right, p ,q);
return root;
}
二叉树的最近公共祖先【236中等】
-
解题思路
- 首先明白这个方法是做什么的,然后再想如何递归
-
步骤
-
方法作用:以root为根节点的二叉树中查找p、q的最近公共祖先
- p,q可能在root一边或者再两边,更悲观p,q不在这棵树中
- 所以分别到root的左右子树找(即递归),如果在一边找到说明返回这一边的就是最近公共祖先
-
所以
- 如果p、q同时存在于这棵二叉树中,就能成功返回它们的最近公共祖先
- 如果p、q都不存在于这棵二叉树中,返回null
- 如果只有p存在于这棵二叉树中,返回p
- 如果只有q存在于这棵二叉树中,返回q
-
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) { if (root == null || root == p || root == q) return root; // 去以root.left为根节点的二叉树中查找p、q的最近公共祖先 TreeNode left = lowestCommonAncestor(root.left, p, q); // 去以root.right为根节点的二叉树中查找p、q的最近公共祖先 TreeNode right = lowestCommonAncestor(root.right, p, q); if (left != null && right != null) return root; return (left != null) ? left : right; }
-
二叉搜索树中的插入操作【701中等】
-
确定终止条件
- 终止条件就是找到遍历的节点为null的时候,就是要插入节点的位置了,并把插入的节点返回。
- 这里把添加的节点返回给上一层,就完成了父子节点的赋值操作了
-
确定单层递归的逻辑
- 如何通过递归函数返回值完成了新加入节点的父子关系赋值操作了,下一层将加入节点返回,本层用root->left或者root->right将其接住。
-
public TreeNode insertIntoBST(TreeNode root, int val) { // 终止条件就是找到遍历的节点为null的时候,就是要插入节点的位置了,并把插入的节点返回。 if(root == null){ TreeNode node = new TreeNode(val); return node; } // 如何通过递归函数返回值完成了新加入节点的父子关系赋值操作了,下一层将加入节点返回,本层用root->left或者root->right将其接住。 if (val < root.val) root.left = insertIntoBST(root.left, val); if (val > root.val) root.right = insertIntoBST(root.right, val); return root; }
删除二叉搜索树中的节点【450中等】
-
第一种情况:没找到删除的节点,遍历到空节点直接返回了
-
找到删除的节点
- 第二种情况:左右孩子都为空(叶子节点),直接删除节点, 返回NULL为根节点
- 第三种情况:删除节点的左孩子为空,右孩子不为空,删除节点,右孩子补位,返回右孩子为根节点
- 第四种情况:删除节点的右孩子为空,左孩子不为空,删除节点,左孩子补位,返回左孩子为根节点
- 第五种情况:左右孩子节点都不为空,则将删除节点的左子树头结点(左孩子)放到删除节点的右子树的最左面节点的左孩子上,返回删除节点右孩子为新的根节点。
-
如[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4erz0StE-1677908641271)(…/…/…/%E5%AD%A6%E4%B9%A0%E8%B5%84%E6%96%99/100_%E7%AC%94%E8%AE%B0/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E4%B8%8E%E7%AE%97%E6%B3%95/assets/1677742690428.png)]
- 删除7,首先root.val=2 < key=7,所以在root的right进行查找删除,返回root.right来承接递归的返回值
- 递归来到左右都不等于null的情况,找到root的右子树的最左节点,把root.left作为cur的左子树,返回root.right给第一步
- 最后返回根节点2,即整棵树
-
public TreeNode deleteNode(TreeNode root, int key) { // 没找到 if (root == null)return root; // 大于key到root的左子树去查找并删除,由于方法有返回值,所以用root.left去承接 if (root.val > key) root.left = deleteNode(root.left, key); if (root.val < key) root.right = deleteNode(root.right, key); // 相等 if (root.val == key) { // 1.左右都为空,直接删除它因为是叶子节点 if (root.left == null && root.right == null) return null; // 父节点的孩子变为空所以return null else if (root.left == null && root.right != null) return root.right; else if (root.left != null && root.right == null) return root.left; else {// 左右都不为空,需要用右子树的比较小的值来接它的左孩子,所以一直找到它右子树的最左节点 TreeNode cur = root.right; while (cur.left != null) cur = cur.left; cur.left = root.left;// 把要删除的节点(root)左子树放在cur的左孩子的位置 return root.right; } } return root; // 返回根节点整棵树 }
修剪二叉搜索树
-
对根结点 root 进行深度优先遍历。
- 对于当前访问的结点,如果结点为空结点,直接返回空结点;
- 如果结点的值小于 low,那么说明该结点及它的左子树都不符合要求,我们返回对它的右结点进行修剪后的结果;
- 如果结点的值大于 high,那么说明该结点及它的右子树都不符合要求,我们返回对它的左子树进行修剪后的结果;
- 如果结点的值位于区间 [low,high],我们将结点的左结点设为对它的左子树修剪后的结果,右结点设为对它的右子树进行修剪后的结果。
- 最终返回root,整棵树
-
public TreeNode trimBST(TreeNode root, int low, int high) { if (root == null)return null; if (root.val < low) return trimBST(root.right, low, high); if (root.val > high) return trimBST(root.left, low, high); // root在【low,high】区间 root.left = trimBST(root.left, low, high); root.right = trimBST(root.right, low, high); return root; }
将有序数组转换为二叉搜索树【108简单】
-
题目描述:有序数组转成高度平衡的二叉搜索树。
-
类似二分搜索,采用左闭右开区间,mid=(left+right) / 2
-
public TreeNode sortedArrayToBST(int[] nums) { return sortedArrayToBST(nums, 0, nums.length); } private TreeNode sortedArrayToBST(int[] nums, int left, int right){ if (left >= right) return null; int mid = left + (right - left) / 2; TreeNode root = new TreeNode(nums[mid]); root.left = sortedArrayToBST(nums, left, mid); root.right = sortedArrayToBST(nums, mid + 1, right); return root; }
把二叉搜索树转换为累加树【538中等】
-
题目描述:使每个节点
node
的新值等于原树中大于或等于node.val
的值之和。 -
第一步:递归函数参数以及返回值
- 这里很明确了,不需要递归函数的返回值做什么操作了,要遍历整棵树。
-
第二步:确定终止条件
- 遇到空就停止
-
第三步:确定单层递归的逻辑
- 注意要右中左来遍历二叉树, 中节点的处理逻辑就是让cur的数值加上前一个节点的数值。
-
重写void函数来实现求和逻辑
-
int sum = 0; public TreeNode convertBST(TreeNode root) { convertBST(root); return root; } private void convertBST1(TreeNode root) { if (root == null) return; convertBST1(root.right); sum += root.val; root.val = sum; convertBST(root.left); }
-
不用承接返回值,只做一些操作
-
public TreeNode convertBST(TreeNode root) { if (root == null) return null; convertBST(root.right); sum += root.val; root.val = sum; convertBST(root.left); return root; }
路径总和【112简单】
- 观察要求我们完成的函数,我们可以归纳出它的功能:询问是否存在从当前节点 root 到叶子节点的路径,满足其路径和为 sum。
- 假定从根节点到当前节点的值之和为 val,我们可以将这个大问题转化为一个小问题:是否存在从当前节点的子节点到叶子的路径,满足其路径和为 sum - val。
- 解题步骤
- 不难发现这满足递归的性质,
- 若当前节点就是叶子节点,那么我们直接判断 sum 是否等于 val 即可(因为路径和已经确定,就是当前节点的值,我们只需要判断该路径和是否满足条件)。
- 若当前节点不是叶子节点,我们只需要递归地询问它的左或右子节点是否能满足条件即可。
public boolean hasPathSum(TreeNode root, int targetSum) {
if (root == null)return false;
if (root.left == null && root.right == null)
return root.val == targetSum;
return hasPathSum(root.left, targetSum - root.val) || hasPathSum(root.right, targetSum - root.val);
}
N叉树
N叉树的前序遍历【589简单】
class Node {
public int val;
public List<Node> children;
public Node() {}
public Node(int _val) {
val = _val;
}
public Node(int _val, List<Node> _children) {
val = _val;
children = _children;
}
}
public List<Integer> preorder(Node root) {
List<Integer> result = new ArrayList<>();
preorder(root, result);
return result;
}
private void preorder(Node root, List<Integer> result) {
if (root == null) return;
result.add(root.val);
List<Node> children = root.children;
for (Node child : children) {
preorder(child, result);
}
}
N二叉树的后序遍历【590简单】
public List<Integer> postorder(Node root) {
List<Integer> result = new ArrayList<>();
postorder(root, result);
return result;
}
private void postorder(Node root, List<Integer> result) {
if (root == null) return;
List<Node> children = root.children;
for (Node child : children) {
postorder(child, result);
}
result.add(root.val);
}
N 叉树的层序遍历【429中等】
public List<List<Integer>> levelOrder(Node root) {
List<List<Integer>> result = new ArrayList<>();
Queue<Node> queue = new ArrayDeque<>();
if (root == null) return result;
queue.add(root);
while (!queue.isEmpty()) {
int size = queue.size();
List<Integer> tmp = new ArrayList<>();
// 将这一层的节点分别取出,把他们的孩子节点依次加入
for (int i = 0; i < size; i++) {
Node cur = queue.poll();
tmp.add(cur.val);
List<Node> cur_children = cur.children;
if (cur_children == null || cur_children.size() == 0)
continue;
for (Node child : cur_children) {
if (child != null)
queue.add(child);
}
}
result.add(tmp);
}
return result;
}
深度优先搜索
- 很多排列组合相关的问题,都可以通过dfs来解决
- 定义一个方法dfs用于深度优先遍历,为了不出错,首先可以只传索引不传其他参数,也就是说先把他们作为成员变量
- dfs(int index)中:
- 首先判断递归停止条件,
已经进入到最后一层了,不能再往下搜索
,这时把当前结果加入到结果集
- 再枚举这一层所有可能的选择(
可能多用for循环,两个时候可能直接枚举,根据条件选择某一个可能
,内部需要做的操作如下三条- 选择一种可能的选择加入到中间结果中
- 进入下一层搜索,即dfs(idx+1)
- 需要恢复现场的恢复,即删除最后加入或者换回原位置,String中不用恢复现场因为char字符数组会直接覆盖原位置
- 首先判断递归停止条件,
电话号码的字母组合【17中等】
- 二维字符数组与字符串的操作
- 取二维数组中某一行:char[] letters = lettersArray[chars[idx] - ‘2’];
- 字符数组转String:list.add(new String(string));
- 对字符串的操作实际上就是对字符数组的操作:char[] string = new char[chars.length];
private char[][] lettersArray = {
{'a', 'b', 'c'}, {'d', 'e', 'f'}, {'g', 'h', 'i'},
{'j', 'k', 'l'}, {'m', 'n', 'o'}, {'p', 'q', 'r', 's'},
{'t', 'u', 'v'}, {'w', 'x', 'y', 'z'}
};
public List<String> letterCombinations(String digits) {
if (digits == null) return null;
List<String> list = new ArrayList<>();
char[] chars = digits.toCharArray();
if (chars.length == 0) return list;
char[] string = new char[chars.length];
dfs(0, chars, string, list);
return list;
}
/**
* @param idx 正在搜索第idx层
*/
private void dfs(int idx, char[] chars, char[] string, List<String> list) {
// 已经进入到最后一层了,不能再往下搜索
if (idx == chars.length) {
// 得到了一个正确的解
list.add(new String(string));
return;
}
// 先枚举这一层可以做的所有选择
char[] letters = lettersArray[chars[idx] - '2'];
for (char letter : letters) {
string[idx] = letter;
dfs(idx + 1, chars, string, list);
}
}
无重复参数的全排列【46中等】
- 使用交换的思想
- 比方说三个字母的全排列
- 实际上就是三个字符都可以做第一位,那么就相当于这三个字符分别与索引为0的元素进行交换,然后递归索引为1的位置就只有从剩下两个字符选一个做,实则就是交换位置
有重复参数的全排列【47中等】
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NeLla71e-1677908641272)(…/…/…/%E5%AD%A6%E4%B9%A0%E8%B5%84%E6%96%99/100_%E7%AC%94%E8%AE%B0/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E4%B8%8E%E7%AE%97%E6%B3%95/assets/1677401502046.png)]
-
如上图,三次交换中第一次和第三次以及第四次实际上结果是一样的,所以考虑其中一种就好
-
那么怎么判断已经出现过呢
- 就是判断idx位置一直到i位置这一段中是否有和i位置的元素一样,有则说明已经选过这种结果
private boolean isRepeat(int idx, int i, int[] nums) { for (int j = idx; j < i; j++) { if (nums[j] == nums[i]) return true; } return false; }
完整代码
public List<List<Integer>> permuteUnique(int[] nums) {
if (nums == null) return null;
List<List<Integer>> result = new ArrayList<>();
if (nums.length == 0) return result;
dfs(0, nums, result);
return result;
}
private void dfs(int idx, int[] nums, List<List<Integer>> result) {
// 不再往下搜索的条件
if (idx == nums.length) {
List<Integer> tmp = new ArrayList<>();
for (int value : nums) {
tmp.add(value);
}
result.add(tmp);
}
// 枚举这一层所有可以做出的选择
for (int i = idx; i < nums.length; i++) {
if (isRepeat(nums, idx, i))continue;
swap(nums, idx, i);
dfs(idx + 1, nums, result);
// 恢复现场
swap(nums, idx, i);
}
}
private boolean isRepeat(int[] nums, int idx, int i) {
for (int j = idx; j < i; j++) {
if (nums[j] == nums[i])
return true;
}
return false;
}
private void swap(int[] nums, int i, int j) {
int tmp = nums[i];
nums[i] = nums[j];
nums[j] = tmp;
}
括号的生成【22中等】
- 当左右括号的数量一样时,只能选择左括号
- 当左括号数量大于0,可以选择左括号
- 当右括号数量大于0,且右括号数量不等于左括号数量,可以选择右括号
public List<String> generateParenthesis(int n) {
List<String> result = new ArrayList<>();
if (n < 0) return result;
if (n == 0) {
result.add("");
return result;
}
char[] tmp = new char[n * 2];
dfs(0, n, n, tmp, result);
return result;
}
/**
*
* @param idx :搜索的层数
* @param leftRemain:左括号剩余数量
* @param tmp :用来存放每一层的选择
*
*/
private void dfs(int idx,
int leftRemain, int rightRemain,
char[] tmp, List<String> result) {
if (idx == tmp.length) {
result.add(new String(tmp));
return;
}
// 枚举这一层所有可能的选择
// 选择一种可能之后,进入下一层搜索
// 本题只有两种选择,左括号和右括号,所以不用 for 循环
if (leftRemain > 0) {
tmp[idx] = '(';
dfs(idx + 1, leftRemain - 1, rightRemain, tmp, result);
}
if (rightRemain > 0 && leftRemain != rightRemain) {
tmp[idx] = ')';
dfs(idx + 1, leftRemain, rightRemain - 1, tmp, result);
}
}
二叉树的所有路径【257简单】
public List<String> binaryTreePaths(TreeNode root) {
List<String> result = new ArrayList<>();
List<Integer> tmp = new ArrayList<>();
dfs(root, tmp, result);
return result;
}
private void dfs(TreeNode root, List<Integer> tmp, List<String> result) {
tmp.add(root.val);
// 到叶子节点,将list转String再加入result
if (root.left == null && root.right == null){
System.out.println(getPathString(tmp));
result.add(getPathString(tmp));
return;
} else {
// 不是叶子节点
if (root.left != null) {
dfs(root.left, tmp, result);
tmp.remove(tmp.size() - 1);
}
if (root.right != null) {
dfs(root.right, tmp, result);
tmp.remove(tmp.size() - 1);
}
}
}
// 拼接字符串
private String getPathString(List<Integer> list) {
StringBuilder sb = new StringBuilder();
sb.append(list.get(0));
for (int i = 1; i < list.size(); i++) {
sb.append("->").append(list.get(i));
}
return sb.toString();
}
路径总和【113中等】
- 枚举每一条从根节点到叶子节点的路径。当我们遍历到叶子节点,且此时路径和恰为目标和时,我们就找到了一条满足条件的路径。
public List<List<Integer>> pathSum(TreeNode root, int targetSum) {
List<List<Integer>> result = new ArrayList<>();
if (root == null)return result;
List<Integer> tmp = new ArrayList<>();
dfs(root, tmp, result, targetSum);
return result;
}
private void dfs(TreeNode root, List<Integer> tmp, List<List<Integer>> result, int targetSum) {
// 1.递归停止条件
if (root == null)return;
// 2.枚举这一层所有可能的选择
tmp.add(root.val);
targetSum -= root.val;
// 3.进入下一层
if (root.left == null && root.right == null) { // 到叶子节点
if (targetSum == 0)
result.add(new ArrayList<>(tmp));
} else {
dfs(root.left, tmp, result, targetSum);
dfs(root.right, tmp, result, targetSum);
}
// 4
// 恢复现场
tmp.remove(tmp.size() - 1);
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZpkSUrHo-1677908641274)(…/…/…/%E5%AD%A6%E4%B9%A0%E8%B5%84%E6%96%99/100_%E7%AC%94%E8%AE%B0/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E4%B8%8E%E7%AE%97%E6%B3%95/assets/1677504113567.png)]
组合总和【39中等】
-
恢复现场target也需要加回来减去的值
-
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SPboUBUB-1677908641275)(…/…/…/%E5%AD%A6%E4%B9%A0%E8%B5%84%E6%96%99/100_%E7%AC%94%E8%AE%B0/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E4%B8%8E%E7%AE%97%E6%B3%95/assets/1677509840949.png)]
-
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LSz2wyKz-1677908641275)(…/…/…/%E5%AD%A6%E4%B9%A0%E8%B5%84%E6%96%99/100_%E7%AC%94%E8%AE%B0/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E4%B8%8E%E7%AE%97%E6%B3%95/assets/1677509879149.png)]
public List<List<Integer>> combinationSum(int[] candidates, int target) {
List<List<Integer>> result = new ArrayList<>();
if (candidates == null || candidates.length == 0)return null;
List<Integer> tmp = new ArrayList<>();
Arrays.sort(candidates);
dfs(0, candidates, target, tmp, result);
return result;
}
private void dfs(int begin, int[] candidates, int target, List<Integer> tmp, List<List<Integer>> result) {
// 结束
if (target == 0) {
result.add(new ArrayList<>(tmp));
return;
}
// 枚举这一层所有可能, i = begin 去重,
for (int i = begin; i < candidates.length; i++) {
// 选择一个
if (target < candidates[i])
break;
target -= candidates[i];
tmp.add(candidates[i]);
// 下一层
dfs(i, candidates, target, tmp, result);
// 恢复现场
tmp.remove(tmp.size() - 1);
target += candidates[i];
}
}
动态规划
- 动规是由前一个状态推导出来的,而贪心是局部直接选最优的
步骤
- 确定dp数组(dp table)以及下标的含义
- 确定递推公式
- dp数组初始化
基础题目
斐波那契数【509简单】
-
确定dp数组以及下标的含义:dp[i]的定义为:第i个数的斐波那契数值是dp[i]
-
确定递推公式:dp[i] = dp[i - 1] + dp[i - 2];
-
dp数组初始化:dp[0] = 0;dp[1] = 1;
-
public int fib(int n) { if (n < 2)return n; int[] dp = new int[n + 1]; dp[0] = 0; dp[1] = 1; for (int i = 2; i <= n; i++) { dp[i] = dp[i - 1] + dp[i - 2]; } return dp[n]; }
爬楼梯70【简单】
-
dp[i]: 爬到第i层楼梯,有dp[i]种方法
-
dp[i] = dp[i - 1] + dp[i - 2]
-
dp[1] = 1,dp[2] = 2
-
public int climbStairs(int n) { if (n < 2) return n; int[] dp =new int[n + 1]; dp[1] = 1; dp[2] = 2; for (int i = 3; i <= n; i++) { dp[i] = dp[i - 1] + dp[i - 2]; } return dp[n]; }
使用最小花费爬楼梯【746简单】
-
dp[i]的定义:到达第i台阶所花费的最少体力为dp[i]。
-
dp[i] = min(dp[i - 1] + cost[i - 1], dp[i - 2] + cost[i - 2]);
-
dp[0] = 0,dp[1] = 0;
-
public int minCostClimbingStairs(int[] cost) { int n = cost.length; int[] dp = new int[n + 1];//爬到第i层的花费 // 因为题干说可以选择从下标为 0 或下标为 1 的台阶开始,因此支付费用为0 dp[0] = 0; dp[1] = 0; for (int i = 2; i <= n; i++) { dp[i] = Math.min(dp[i - 1] + cost[i - 1], dp[i - 2] + cost[i - 2]); } return dp[n]; }
不同路径【62中等】
-
dp(i,j) :表示从(0 ,0)出发,到(i, j) 有dp(i,j)条不同的路径。
-
想要求dp(i,j),只能有两个方向来推导出来,即dp(i-1,j)和 dp(i,j-1)
-
第一行第一列初始值为1
-
public int uniquePaths(int m, int n) { // 从0,0到m,n的路径 int[][] dp = new int[m + 1][n + 1]; for (int i = 0; i < n; i++) { dp[0][i] = 1; } for (int i = 0; i < m; i++) { dp[i][0] = 1; } for (int i = 1; i < m; i++) { for (int j = 1; j < n; j++) { dp[i][j] = dp[i - 1][j] + dp[i][j - 1]; } } return dp[m - 1][n - 1]; }
不同路径2【63中等】
- 初始化时,遇到障碍,障碍之后都为0
- [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-v7831Jbz-1677908641276)(…/…/…/%E5%AD%A6%E4%B9%A0%E8%B5%84%E6%96%99/100_%E7%AC%94%E8%AE%B0/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E4%B8%8E%E7%AE%97%E6%B3%95/assets/1677758593751.png)]
- 所以for的结束条件一个是到m另一个 obstacleGrid(i,0)不等于0
public int uniquePathsWithObstacles(int[][] obstacleGrid) {
int m = obstacleGrid.length;
int n = obstacleGrid[0].length;
int[][] dp = new int[m][n];
for (int i = 0; i < m && obstacleGrid[i][0] == 0; i++) {
dp[i][0] = 1;
}
for (int j = 0; j < n && obstacleGrid[0][j] == 0; j++) {
dp[0][j] = 1;
}
for (int i = 1; i < m; i++) {
for (int j = 1; j < n; j++) {
if (obstacleGrid[i][j] == 0)
dp[i][j] = dp[i - 1][j] + dp[i][j - 1];
else
dp[i][j] = 0;
}
}
return dp[m - 1][n - 1];
}
整数拆分【343中等】
-
对于正整数 n,当 n≥2时,可以拆分成至少两个正整数的和。令 x 是拆分出的第一个正整数,则剩下的部分是 n−x,n−x 可以不继续拆分,或者继续拆分成至少两个正整数的和。由于每个正整数对应的最大乘积取决于比它小的正整数对应的最大乘积,因此可以使用动态规划求解。
-
创建数组dp,其中 dp[i] 表示将正整数 i 拆分成至少两个正整数的和之后,这些正整数的最大乘积。特别地,0 不是正整数,1 是最小的正整数,0 和 1都不能拆分,因此 dp[0]=dp[1]=0
-
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aDwtezVw-1677908641277)(…/…/…/%E5%AD%A6%E4%B9%A0%E8%B5%84%E6%96%99/100_%E7%AC%94%E8%AE%B0/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E4%B8%8E%E7%AE%97%E6%B3%95/assets/1677759435580.png)]
-
public int integerBreak(int n) { int[] dp = new int[n + 1]; for (int i = 2; i <= n; i++) { int max = 0; // 每个数对应的多种取值枚举产生j从1到i,另一个数为i- j for (int j = 1; j < i; j++) { max = Math.max(max, Math.max(j * (i - j), j * dp[i - j])); } dp[i] = max; } return dp[n]; }
不同的二叉搜索树
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KZ81vg5g-1677908641277)(…/…/…/%E5%AD%A6%E4%B9%A0%E8%B5%84%E6%96%99/100_%E7%AC%94%E8%AE%B0/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E4%B8%8E%E7%AE%97%E6%B3%95/assets/1677760239487.png)]
-
public int numTrees(int n) { // 1...n个节点组成的二叉搜索树 int[] dp = new int[n + 1]; dp[0] = 1; dp[1] = 1; for (int i = 2; i <= n; i++) { for (int j = 1; j < i + 1; j++) { dp[i] += dp[j - 1] * dp[i - j]; } } return dp[n]; }
01背包
基础理论
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4liE4gQb-1677908641278)(…/…/…/%E5%AD%A6%E4%B9%A0%E8%B5%84%E6%96%99/100_%E7%AC%94%E8%AE%B0/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E4%B8%8E%E7%AE%97%E6%B3%95/assets/1677808430306.png)]
二维dp数组01背包
确定dp数组以及下标的含义
对于背包问题,有一种写法, 是使用二维数组,即dp(i,j)表示从下标为[0-i]的物品里任意取,放进容量为j的背包,价值总和最大是多少。
确定递推公式
-
不放物品i:由dp[i - 1][j]推出,即背包容量为j,里面不放物品i的最大价值,此时dp[i][j]就是dp[i - 1][j]。(其实就是当物品i的重量大于背包j的重量时,物品i无法放进背包中,所以被背包内的价值依然和前面相同。)
-
放物品i:由dp[i - 1][j - weight[i]]推出,dp[i - 1][j - weight[i]] 为背包容量为j - weight[i]的时候不放物品i的最大价值,那么dp[i - 1][j - weight[i]] + value[i] (物品i的价值),就是背包放物品i得到的最大价值
所以递归公式: dp(i,j) = max(dp(i-1,j), dp(i-1,j- weight[i]) + value[i]);
dp数组如何初始化
-
关于初始化,一定要和dp数组的定义吻合,否则到递推公式的时候就会越来越乱。
-
首先从dp(i,j)的定义出发,如果背包容量j为0,即dp(i,0),无论选哪些物品,背包价值总和一定为0
-
由状态方程可以看出i是从i-1推导出来,那么i为0的时候就一定要初始化。
-
dp(0,j),即:i为0,存放编号0的物品的时候,各个容量的背包所能存放的最大价值。
-
很明显当 j < weight[0]的时候,dp(0,j)应该是 0,因为背包容量比编号0的物品重量还小。
当j >= weight[0]时,dp(0,j)应该是value[0],因为背包容量放足够放编号0物品。
-
-
遍历顺序-先物品后容量,先容量后物品都可以
-
原因
- 递归公式中可以看出dp(i,j)是靠dp(i-1,j)和dp(i,j- weight[i])推导出来的。上和左上方向
-
先遍历物品
-
// weight数组的大小 就是物品个数 for(int i = 1; i < weight.size(); i++) { // 遍历物品 for(int j = 0; j <= bagweight; j++) { // 遍历背包容量 if (j < weight[i]) dp[i][j] = dp[i - 1][j]; else dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]); } }
-
-
先遍历背包
-
// weight数组的大小 就是物品个数 for(int j = 0; j <= bagweight; j++) { // 遍历背包容量 for(int i = 1; i < weight.size(); i++) { // 遍历物品 if (j < weight[i]) dp[i][j] = dp[i - 1][j]; else dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]); } }
-
滚动数组01背包推荐
二维dp公式:dp(i,j) = max(dp(i-1,j), dp(i-1,j- weight[i]) + value[i])
其实可以发现如果把dp[i - 1]那一层拷贝到dp[i]上,表达式完全可以是:
dp(i,j) = max(dp(i,j), dp(i,j - weight[i]])+ value[i])
所以与其把dp[i - 1]这一层拷贝到dp[i]上,不如只用一个一维数组了,即滚动数组
确定dp数组的定义
dp[j]表示:容量为j的背包,所背的物品价值可以最大为dp[j]
一维dp数组的递推公式
- dp[j]可以通过dp[j - weight[i]]推导出来,dp[j - weight[i]]表示容量为j - weight[i]的背包所背的最大价值。
- dp[j - weight[i]] + value[i] 表示 容量为 j - 物品i重量 的背包 加上 物品i的价值。(也就是容量为j的背包,放入物品i了之后的价值即:dp[j])
- 此时dp[j]有两个选择,一个是取自己dp[j] 相当于 二维dp数组中的dp(i-1,j),即不放物品i,一个是取dp[j - weight[i]] + value[i],即放物品i,指定是取最大的,毕竟是求最大价值,
- 所以递推公式为:dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
一维dp数组如何初始化
- 下标0的位置,初始为0
一维dp数组遍历顺序
for(int i = 0; i < weight.size(); i++) { // 遍历物品
for(int j = bagWeight; j >= weight[i]; j--) { // 遍历背包容量
dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
}
}
- 倒序遍历是为了保证物品i只被放入一次!。但如果一旦正序遍历了,那么物品0就会被重复加入多次!
- 为什么二维dp数组历的时候不用倒序呢?
- 因为对于二维dp,dp[i][j]都是通过上一层即dp[i - 1][j]计算而来,本层的dp(i,j)并不会被覆盖!
- 本质上还是一个对二维数组的遍历,并且右下角的值依赖上一层左上角的值,因此需要保证左边的值仍然是上一层的,从右向左覆盖。
- [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7e0eRIlK-1677908641279)(…/…/…/%E5%AD%A6%E4%B9%A0%E8%B5%84%E6%96%99/100_%E7%AC%94%E8%AE%B0/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E4%B8%8E%E7%AE%97%E6%B3%95/assets/1677810617880.png)]
- 可不可以先遍历背包容量嵌套遍历物品呢
- 不可以。因为一维dp的写法,背包容量一定是要倒序遍历,如果遍历背包容量放在上一层,那么每个dp[j]就只会放入第一个物品
分割等和子集【416中等】是求 给定背包容量,能不能装满这个背包
-
题目描述:【1,5,11,5】分割成两个子集,使得两个子集的元素和相等
-
类似01背包,求总和除以2,即背包容量为11,选和不选元素,使得最大值为11
-
public boolean canPartition(int[] nums) { int len = nums.length; if (nums == null || len== 0) return false; int sum = 0; for (int num : nums) sum += num; if (sum % 2 == 1) return false; int target = sum / 2; int[] dp = new int[target + 1]; for (int i = 0; i < len; i++) { for (int j = target; j >= nums[i]; j--) { dp[j] = Math.max(dp[j], dp[j - nums[i]] + nums[i]); } } return dp[target] == target; }
最后一块石头的重量II【1049中等】是求 给定背包容量,尽可能装,最多能装多少
-
有一堆石头,用整数数组 stones 表示。其中 stones[i] 表示第 i 块石头的重量。
每一回合,从中选出任意两块石头,然后将它们一起粉碎。假设石头的重量分别为 x 和 y,且 x <= y。那么粉碎的可能结果如下:
如果 x == y,那么两块石头都会被完全粉碎;
如果 x != y,那么重量为 x 的石头将会完全粉碎,而重量为 y 的石头新重量为 y-x。
最后,最多只会剩下一块 石头。返回此石头 最小的可能重量 。如果没有石头剩下,就返回 0。 -
解题思路:类似上一题,不同是上一题平均分,这一题可能平均分不开,返回两个子集的和的最小差值
- 一部分和为sum/2=target,另一部分为sum-target
- 即寻找target重的背包最多能背的价值是多少(只不过相当于石头价值=重量)
-
public int lastStoneWeightII(int[] stones) { int sum = 0; for (int stone : stones) sum += stone; int target = sum / 2; int len = stones.length; // dp[j]重量为j的石头最多可以背的重量 int[] dp = new int[target + 1]; for (int i = 0; i < len; i++) { for (int j = target; j >= stones[i]; j--) { dp[j] = Math.max(dp[j], dp[j - stones[i]] + stones[i]); } } return sum - dp[target] - dp[target]; }
目标和【494中等】是求 给定背包容量,装满背包有多少种方法
-
给你一个整数数组
nums
和一个整数target
。向数组中的每个整数前添加'+'
或'-'
,然后串联起所有整数,可以构造一个 表达式 -
解题思路:分成两部分,其中left+right=sum,left-right=target,所以 left - (sum - left) = target 推导出 left = (target + sum)/2 。
- target是固定的,sum是固定的,left就可以求出来
- 此时问题就是在集合nums中找出和为left的组合。
-
本题还是有点难度,大家也可以记住,在求装满背包有几种方法的情况下,递推公式一般为:
-
dp[j] += dp[j - nums[i]];
-
-
public int findTargetSumWays(int[] nums, int target) { int sum = 0; for (int num: nums) { sum += num; } //如果target过大 sum将无法满足 if ( target < 0 && sum < -target) return 0; if ((target + sum) % 2 != 0) return 0; int leftNumber = (target + sum) / 2; if(leftNumber < 0) leftNumber = -leftNumber; int[] dp = new int[leftNumber + 1]; dp[0] = 1; int len = nums.length; for (int i = 0; i < len; i++) { for (int j = leftNumber; j >= nums[i]; j--) { dp[j] += dp[j - nums[i]]; } } return dp[leftNumber]; }
一和零【474中等】是求 给定背包容量,装满背包最多有多少个物品
-
dp(i,j):最多有i个0和j个1的strs的最大子集的大小为dp(i,j)
-
dp(i,j)可以由前一个strs里的字符串推导出来,strs里的字符串有zeroNum个0,oneNum个1。
dp(i,j)就可以是dp(i- zeroNum, j - oneNum) + 1。
-
然后我们在遍历的过程中,取dp(i,j)的最大值。
所以递推公式:dp(i,j) = max(dp(i,j), dp(i - zeroNum,j - oneNum) + 1);
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GLwCb2TR-1677908641279)(…/…/…/%E5%AD%A6%E4%B9%A0%E8%B5%84%E6%96%99/100_%E7%AC%94%E8%AE%B0/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E4%B8%8E%E7%AE%97%E6%B3%95/assets/1677826432179.png)]
-
public int findMaxForm(String[] strs, int m, int n) { // 1. dp[i][j]:最多有i个0和j个1的strs的最大子集的大小为dp[i][j]。 int[][] dp = new int[m + 1][n + 1]; int zeroNum, oneNum; // 2. dp[i][j] 可以由前一个strs里的字符串推导出来,strs里的字符串有zeroNum个0,oneNum个1。 for (String str : strs) { zeroNum = 0; oneNum = 0; for (char c : str.toCharArray()) { if (c == '0') zeroNum++; else oneNum++; } for (int i = m; i >= zeroNum; i--) { for (int j = n; j >= oneNum; j--) { dp[i][j] = Math.max(dp[i][j], dp[i - zeroNum][j - oneNum] + 1); } } } return dp[m][n]; }
完全背包
完全背包和01背包问题唯一不同的地方就是,每种物品有无限件。
- j从小到达遍历即可满足加入多次,和01背包的区别
// 先遍历物品,再遍历背包
for(int i = 0; i < weight.size(); i++) { // 遍历物品
for(int j = weight[i]; j <= bagWeight ; j++) { // 遍历背包容量
dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
}
}
兑换零钱【518中等】
- 初始化 dp[0]=1;
- 只有当不选取任何硬币时,金额之和才为 00,因此只有 11 种硬币组合。
- 遍历coins,对于其中的每个元素 coin,进行如下操作:
- 遍历 i 从 coin 到 amount,将dp[i−coin] 的值加到dp[i]。
- 最终得到dp[amount] 的值即为答案。
public int change(int amount, int[] coins) {
int[] dp = new int[amount + 1];
//初始化dp数组,表示金额为0时只有一种情况,也就是什么都不装
dp[0] = 1;
for (int i = 0; i < coins.length; i++) {
for (int j = coins[i]; j <= amount; j++) {
dp[j] += dp[j - coins[i]];
}
}
return dp[amount];
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-x9glf9QU-1677908641280)(…/…/…/%E5%AD%A6%E4%B9%A0%E8%B5%84%E6%96%99/100_%E7%AC%94%E8%AE%B0/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E4%B8%8E%E7%AE%97%E6%B3%95/assets/1677828792831.png)]
高频题
将零移动到数组的最后边【283简单】
-
那么就是用两个指针,一个找到是0的位置与不是0的位置进行交换
- i为遍历指针,cur为是0的位置
- 如果nums[i]不是0,这时cur所指位置如果和i位置一样,是不需要交换的,不一样说明cur在nums[i]等于0的位置
public void moveZeroes(int[] nums) { if (nums == nums)return; int cur = 0; for (int i = 0; i < nums.length; i++) { if (nums[i] == 0)continue; if (cur != i) { nums[cur] = nums[i]; nums[i] = 0; } cur++; } }
三数之和【15中等】
- 先对数组进行排序
- i从头开始遍历,并作为其中的一个数,另外两个数Left最左和Right最右
- 每次比较Left和Right位置的数加起来如果等于i位置数的相反数,呢么这三个数加入结果中
- 其中i位置符合条件不止一种结果,所以只要Left<Right就需要一直比较下去
- 若小于说明加的数过小,需要将Left++
- 这时也需要判断Left和Left+1位置数是否相等,相等直接跳过Left继续加一,所以while循环
- 同理,大于需要Right–
- 每次比较Left和Right位置的数加起来如果等于i位置数的相反数,呢么这三个数加入结果中
public List<List<Integer>> threeSum(int[] nums) {
if (nums == null) return null;
List<List<Integer>> result = new ArrayList<>();
if (nums.length < 3) return result;
// 排序
Arrays.sort(nums);
// i用于扫描三元组
for (int i = 0; i < nums.length - 2; i++) {
if (i > 0 && nums[i] == nums[i--])continue;
int l = i + 1, r = nums.length - 1;
int remain = -nums[i];
while (l < r) {
int sumLR = nums[l] + nums[r];
if (sumLR == remain) {
result.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 (sumLR < remain)
l++;
else
r--;
}
}
return result;
}
剑指Offer_62_圆圈中最后剩下的数字【简单】
- [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LKCMrUFT-1677908641281)(…/…/…/%E5%AD%A6%E4%B9%A0%E8%B5%84%E6%96%99/100_%E7%AC%94%E8%AE%B0/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E4%B8%8E%E7%AE%97%E6%B3%95/assets/1677492656490.png)]
- 编号从几开始,那么就抽出这个函数,在原函数中返回结果就+1
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-q38zuJBN-1677908641287)(…/…/…/%E5%AD%A6%E4%B9%A0%E8%B5%84%E6%96%99/100_%E7%AC%94%E8%AE%B0/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E4%B8%8E%E7%AE%97%E6%B3%95/assets/1677492748856.png)]
LRUCache的设计【62中等】
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VNBr14P9-1677908641288)(…/…/…/%E5%AD%A6%E4%B9%A0%E8%B5%84%E6%96%99/100_%E7%AC%94%E8%AE%B0/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E4%B8%8E%E7%AE%97%E6%B3%95/assets/1677492808120.png)]
- map的结构是<key, Node>
- get操作需要根据key获取到这个节点,然后删除这个节点将node加入到虚拟头节点的后面,因为最近最少使用,所以需要将刚访问的节点放到链表的头位置
- put操作
- 判断key位置有无元素,有则覆盖
- 再判断容量是否满,没满直接插入map中,并将这个节点插入到链表头
- 满则需要先从map中删除这个key,然后链表中删除这个节点,再插入新节点A
private Map<Integer, Node> map;
private int capacity;
// 双向链表的虚拟头结点
private Node first;
// 双向链表的虚拟尾结点
private Node last;
public LRUCache(int capacity) {
map = new HashMap<>(capacity);
this.capacity = capacity;
first = new Node();
last = new Node();
first.next = last;
last.prev = first;
}
public int get(int key) {
// 首先获取到map中的key所对应的节点
Node node = map.get(key);
if (node == null)return -1;
// 需要删除这个节点
removeNode(node);
// 将这个节点插入到虚拟头结点
addAfterFirst(node);
return node.value;
}
private void addAfterFirst(Node node) {
// 操作不可以反,node和first先连,和后面的链表就会断
// 1.node 与 first.next 连
first.next.prev = node;
node.next = first.next;
// 2.node 与 first 连
first.next = node;
node.prev = first;
}
private void removeNode(Node node) {
node.prev.next = node.next;
node.next.prev = node.prev;
}
public void put(int key, int value) {
Node node = map.get(key);
// 不等于空,覆盖原来值,并删除原来的节点,并把这个节点加入到虚拟头结点后面
if (node != null) {
node.value = value;
removeNode(node);
addAfterFirst(node);
} else {// 添加一对新的key-value
if (map.size() == capacity){
// 淘汰最近最少使用的节点,先从map中删除,再从node中删
removeNode(map.remove(last.prev.key));
}
map.put(key, node = new Node(key, value));
addAfterFirst(node);
}
}
private static class Node {
public int key;
public int value;
public Node prev;
public Node next;
public Node(int key, int value) {
this.key = key;
this.value = value;
}
public Node() {
}
}