这周在家里学习主攻点在:
- LeetCode
- 递归
- 队列
- 重新整理简历
- 专栏
- 架构思维扫盲
- MySQL实战
- 数据结构与算法
- 递归
- 队列
- Java并发编程
- volatile关键字深入理解
- 视频
- 多线程
- JMM、volatile(结合专栏一起理解)
1. although
这周算法题,主攻队列与递归相关题目。
1.1. 队列
1.1.1. 641 设计实现双端队列
链表可实现双向队列,使用数组也可以实现(循环队列)
- JMM、volatile(结合专栏一起理解)
- 多线程
package com.myserious.code.cdz;
/**
* 641 设计实现双端队列。
*
* 你的实现需要支持以下操作:
* <p>
* MyCircularDeque(k):构造函数,双端队列的大小为k。
* insertFront():将一个元素添加到双端队列头部。 如果操作成功返回 true。
* insertLast():将一个元素添加到双端队列尾部。如果操作成功返回 true。
* deleteFront():从双端队列头部删除一个元素。 如果操作成功返回 true。
* deleteLast():从双端队列尾部删除一个元素。如果操作成功返回 true。
* getFront():从双端队列头部获得一个元素。如果双端队列为空,返回 -1。
* getRear():获得双端队列的最后一个元素。 如果双端队列为空,返回 -1。
* isEmpty():检查双端队列是否为空。
* isFull():检查双端队列是否满了。
* 示例:
* <p>
* MyCircularDeque circularDeque = new MycircularDeque(3); // 设置容量大小为3
* circularDeque.insertLast(1); // 返回 true
* circularDeque.insertLast(2); // 返回 true
* circularDeque.insertFront(3); // 返回 true
* circularDeque.insertFront(4); // 已经满了,返回 false
* circularDeque.getRear(); // 返回 2
* circularDeque.isFull(); // 返回 true
* circularDeque.deleteLast(); // 返回 true
* circularDeque.insertFront(4); // 返回 true
* circularDeque.getFront(); // 返回 4
*
*
* <p>
* 提示:
* <p>
* 所有值的范围为 [1, 1000]
* 操作次数的范围为 [1, 1000]
* 请不要使用内置的双端队列库。
* <p>
* 来源:力扣(LeetCode)
* 链接:https://leetcode-cn.com/problems/design-circular-deque
* 著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
*/
public class Week9MMyCircularDeque641 {
/**
* 错误一 添加时没有考虑到前后连接
* ["MyCircularDeque","insertFront","getFront","isEmpty","deleteFront","insertLast","getRear","insertLast","insertFront","deleteLast","insertLast","isEmpty"]
* [[8],[5],[],[],[],[3],[],[7],[7],[],[4],[]]
* <p>
* <p>
* 错误二 队列为空返回-1 注意审题
* ["MyCircularDeque",
* "insertFront","deleteLast","getRear",
* "getFront",
* "getFront",
* "deleteFront","insertFront","insertLast",
* "insertFront","getFront","insertFront"]
* [[4],[9],[],[],[],[],[],[6],[5],[9],[],[6]]
*
* @param args
*/
public static void main(String[] args) {
MyCircularDeque deque = new MyCircularDeque(4);
deque.insertFront(4);
System.out.println(deque.deleteLast());
System.out.println(deque.getRear());
System.out.println(deque.getFront());
System.out.println(deque.getFront());
System.out.println(deque.deleteFront());
deque.insertFront(6);
deque.insertLast(5);
deque.insertFront(9);
System.out.println(deque.getFront());
deque.insertFront(6);
}
/**
* 链表实现
* {@link com.myserious.code.cdz.althoughexpand.Week9LoopQueue 使用的是数组实现}
*/
static class MyCircularDeque {
private ListNode head;
private ListNode tail;
private int size;
private int len;
/**
* Initialize your data structure here. Set the size of the deque to be k.
*/
public MyCircularDeque(int k) {
this.size = k;
}
/**
* Adds an item at the front of Deque. Return true if the operation is successful.
*/
public boolean insertFront(int value) {
if (isFull()) {
return false;
}
ListNode node = new ListNode(value);
if (len == 0) {
head = node;
tail = node;
} else {
head.pre = node;
node.next = head;
head = node;
}
len++;
return true;
}
/**
* Adds an item at the rear of Deque. Return true if the operation is successful.
*/
public boolean insertLast(int value) {
if (isFull()) return false;
ListNode node = new ListNode(value);
if (len == 0) {
head = node;
tail = node;
} else {
node.pre = tail;
tail.next = node;
tail = node;
}
len++;
return true;
}
/**
* Deletes an item from the front of Deque. Return true if the operation is successful.
*/
public boolean deleteFront() {
if (isEmpty()) return false;
if (len == 1) {
head = null;
tail = null;
} else {
ListNode temp = head;
head = temp.next;
temp.next = null;
}
len--;
return true;
}
/**
* Deletes an item from the rear of Deque. Return true if the operation is successful.
*/
public boolean deleteLast() {
if (isEmpty()) return false;
if (len == 1) {
head = null;
tail = null;
} else {
ListNode temp = tail;
tail = temp.pre;
temp.pre = null;
}
len--;
return true;
}
/**
* Get the front item from the deque.
*/
public int getFront() {
if (isEmpty()) return -1;
return head.val;
}
/**
* Get the last item from the deque.
*/
public int getRear() {
if (isEmpty()) return -1;
return tail.val;
}
/**
* Checks whether the circular deque is empty or not.
*/
public boolean isEmpty() {
return len == 0;
}
/**
* Checks whether the circular deque is full or not.
*/
public boolean isFull() {
return len == size;
}
class ListNode {
int val;
ListNode pre;
ListNode next;
ListNode(int x) {
val = x;
}
}
}
}
1.1.2. 621 任务调度器
关键在:理解公式(count[25]-1)*(n+1)+maxCountNum
package com.myserious.code.cdz;
import java.util.Arrays;
/**
*
* 621. 任务调度器
*
* 给定一个用字符数组表示的 CPU 需要执行的任务列表。其中包含使用大写的 A - Z 字母表示的26 种不同种类的任务。任务可以以任意顺序执行,并且每个任务都可以在 1 个单位时间内执行完。CPU 在任何一个单位时间内都可以执行一个任务,或者在待命状态。
*
* 然而,两个相同种类的任务之间必须有长度为 n 的冷却时间,因此至少有连续 n 个单位时间内 CPU 在执行不同的任务,或者在待命状态。
*
* 你需要计算完成所有任务所需要的最短时间。
*
* 示例 1:
*
* 输入: tasks = ["A","A","A","B","B","B"], n = 2
* 输出: 8
* 执行顺序: A -> B -> (待命) -> A -> B -> (待命) -> A -> B.
* 注:
*
* 任务的总个数为 [1, 10000]。
* n 的取值范围为 [0, 100]。
*
* 来源:力扣(LeetCode)
* 链接:https://leetcode-cn.com/problems/task-scheduler
* 著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
*/
public class Week9MLeastInterval621 {
public int leastInterval(char[] tasks, int n) {
int[] count = new int[26];
//统计 任务出现频率
for (char task : tasks) {
count[task-'A']++;
}
//升序
Arrays.sort(count);
//统计相同 最大频率任务个数
int maxCountNum = 0;
for (int i = count.length-1; i > 0; i--) {
if (count[25]!=count[i]){
break;
}
maxCountNum++;
}
//(count[25]-1)*(n+1)+maxCountNum
//count[25]-1 最大频率,减一的原因是:最后一次单独执行不需要间隔
//n+1 每个任务之间的间隔加一,其本身不能算数 比如 n=2 ; a -> 间隔 -> 间隔 -> a ,所以将(a -> 间隔 -> 间隔)当做一组
//最后执行的一组maxCountNum,是频率最高的几个在执行
//但是还有一种情况是:中间都插满了,但是剩下的频率低的任务没有执行完,这种情况就是 最短执行时间就是任务的数量。
return Math.max(((count[25]-1)*(n+1)+maxCountNum),tasks.length);
}
}
1.1.3. 622 循环队列实现
数组实现循环队列,注意点:
- head与tail计算需要% capacity,这样才能形成一个环。
- tail指针为空元素,所以集合空间申请时要将传入值加1
- 拿出tail元素的计算:
(tail-1+capacity)%capacity- 加上capacity的原因防止tail-1为负数的情况
package com.myserious.code.cdz;
/**
* 设计你的循环队列实现。 循环队列是一种线性数据结构,其操作表现基于 FIFO(先进先出)原则并且队尾被连接在队首之后以形成一个循环。它也被称为“环形缓冲器”。
* <p>
* 循环队列的一个好处是我们可以利用这个队列之前用过的空间。在一个普通队列里,一旦一个队列满了,我们就不能插入下一个元素,即使在队列前面仍有空间。但是使用循环队列,我们能使用这些空间去存储新的值。
* <p>
* 你的实现应该支持如下操作:
* <p>
* MyCircularQueue(k): 构造器,设置队列长度为 k 。
* Front: 从队首获取元素。如果队列为空,返回 -1 。
* Rear: 获取队尾元素。如果队列为空,返回 -1 。
* enQueue(value): 向循环队列插入一个元素。如果成功插入则返回真。
* deQueue(): 从循环队列中删除一个元素。如果成功删除则返回真。
* isEmpty(): 检查循环队列是否为空。
* isFull(): 检查循环队列是否已满。
*
* <p>
* 示例:
* <p>
* MyCircularQueue circularQueue = new MycircularQueue(3); // 设置长度为 3
* <p>
* circularQueue.enQueue(1); // 返回 true
* <p>
* circularQueue.enQueue(2); // 返回 true
* <p>
* circularQueue.enQueue(3); // 返回 true
* <p>
* circularQueue.enQueue(4); // 返回 false,队列已满
* <p>
* circularQueue.Rear(); // 返回 3
* <p>
* circularQueue.isFull(); // 返回 true
* <p>
* circularQueue.deQueue(); // 返回 true
* <p>
* circularQueue.enQueue(4); // 返回 true
* <p>
* circularQueue.Rear(); // 返回 4
*
*
* <p>
* 提示:
* <p>
* 所有的值都在 0 至 1000 的范围内;
* 操作数将在 1 至 1000 的范围内;
* 请不要使用内置的队列库。
* <p>
* 来源:力扣(LeetCode)
* 链接:https://leetcode-cn.com/problems/design-circular-queue
* 著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
*/
public class Week9MMyCircularQueue622 {
class MyCircularQueue {
private int[] arr;
private int capacity;
private int head;
private int tail;
/** Initialize your data structure here. Set the size of the queue to be k. */
public MyCircularQueue(int k) {
arr = new int[k+1];
capacity = k+1;
}
/** Insert an element into the circular queue. Return true if the operation is successful. */
public boolean enQueue(int value) {
if (isFull()){
return false;
}
arr[tail] = value;
tail = (tail+1)%capacity;
return true;
}
/** Delete an element from the circular queue. Return true if the operation is successful. */
public boolean deQueue() {
if (isEmpty()){
return false;
}
head = (head+1)%capacity;
return true;
}
/** Get the front item from the queue. */
public int Front() {
if (isEmpty()){
return -1;
}
return arr[head];
}
/** Get the last item from the queue. */
public int Rear() {
if (isEmpty()){
return -1;
}
//拿出最后一个
//tail指向的是最后一个的后一位
//加上capacity的原因防止tail-1为负数的情况
return arr[(tail-1+capacity)%capacity];
}
/** Checks whether the circular queue is empty or not. */
public boolean isEmpty() {
return (head-tail)==0;
}
/** Checks whether the circular queue is full or not. */
public boolean isFull() {
return (tail+1)%capacity==head;
}
}
}
1.2. 递归
1.2.1. 938 二叉搜索树的范围和
递归可以使用自定义栈模拟。
package com.myserious.code.cdz;
import java.util.Stack;
/**
* 938. 二叉搜索树的范围和
* <p>
* 给定二叉搜索树的根结点 root,返回 L 和 R(含)之间的所有结点的值的和。
* <p>
* 二叉搜索树保证具有唯一的值。
* <p>
*
* <p>
* 示例 1:
* <p>
* 输入:root = [10,5,15,3,7,null,18], L = 7, R = 15
* 输出:32
* 示例 2:
* <p>
* 输入:root = [10,5,15,3,7,13,18,1,null,6], L = 6, R = 10
* 输出:23
*
* <p>
* 提示:
* <p>
* 树中的结点数量最多为 10000 个。
* 最终的答案保证小于 2^31。
* <p>
* 来源:力扣(LeetCode)
* 链接:https://leetcode-cn.com/problems/range-sum-of-bst
* 著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
*/
public class Week9ERangeSumBST938 {
int ans = 0;
public int rangeSumBST(TreeNode root, int L, int R) {
dfs(root, L, R);
return ans;
}
private void dfs(TreeNode root, int L, int R) {
if (root != null) {
int val = root.val;
if (val >= L && val <= R) {
ans += val;
}//不能使用else if
//下面两个判断,是向外扩散着遍历
if (val > L) {
dfs(root.left, L, R);
}
if (val < R) {
dfs(root.right, L, R);
}
}
}
public int rangeSumBST1(TreeNode root, int L, int R) {
if (root==null) return 0;
int val = root.val;
if (val>=L&&val<=R){
return val+rangeSumBST1(root.left,L,R)+rangeSumBST1(root.right,L,R);
}else if (val<L){
return rangeSumBST1(root.right,L,R);
}else {
return rangeSumBST1(root.left,L,R);
}
}
/**
* 方法内部模拟栈的方式 递归
* @param root
* @param L
* @param R
* @return
*/
public int rangeSumBST2(TreeNode root, int L, int R) {
Stack<TreeNode> stack = new Stack<>();
stack.push(root);
int res = 0;
while (!stack.empty()){
TreeNode pop = stack.pop();
//注意 自定义栈里的空元素判断
if (pop!=null){
int val = pop.val;
if (val>=L&&val<=R){
res+=val;
stack.push(pop.left);
stack.push(pop.right);
}else if (val<L){
stack.push(pop.right);
}else {
stack.push(pop.left);
}
}
}
return res;
}
public class TreeNode {
int val;
TreeNode left;
TreeNode right;
TreeNode(int x) {
val = x;
}
}
}
1.2.2. 1137 泰波那契序列
n-1 n-2 n-3 理解是:
- 给出了三个的结果,并且n是从0开始的
- 到这个条件里来就说明n>3
- 3-1=2; 3-2=1; 3-3=0;
package com.myserious.code.cdz;
/**
* 泰波那契序列 Tn 定义如下:
* <p>
* T0 = 0, T1 = 1, T2 = 1, 且在 n >= 0 的条件下 Tn+3 = Tn + Tn+1 + Tn+2
* <p>
* 给你整数 n,请返回第 n 个泰波那契数 Tn 的值。
* <p>
*
* <p>
* 示例 1:
* <p>
* 输入:n = 4
* 输出:4
* 解释:
* T_3 = 0 + 1 + 1 = 2
* T_4 = 1 + 1 + 2 = 4
* 示例 2:
* <p>
* 输入:n = 25
* 输出:1389537
*
* <p>
* 提示:
* <p>
* 0 <= n <= 37
* 答案保证是一个 32 位整数,即 answer <= 2^31 - 1。
* <p>
* 来源:力扣(LeetCode)
* 链接:https://leetcode-cn.com/problems/n-th-tribonacci-number
* 著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
*/
public class Week9ETribonacci1137 {
//关键点,使用一个数组保存结果
//这样才能返回取出
private int[] nums = new int[38];
public int tribonacci(int n) {
if (nums[n]!=0){
return nums[n];
}
if (n==0){
return 0;
}else if (n==1||n==2){
return 1;
}else {
//n-1 n-2 n-3 理解是:给出了三个的结果,并且n是从0开始的
//到这个条件里来就说明n>3
// 3-1=2; 3-2=1; 3-3=0;
int res = tribonacci(n - 1) + tribonacci(n - 2) + tribonacci(n - 3);
nums[n]= res;
return res;
}
}
}
1.2.3. 783 二叉搜索树结点最小距离
任意两点间的距离,思路:
- 方法一:
- 遍历出所有节点,放入list中
- list排序
- list遍历,前后相减
- 时间复杂度为 O(nlg(n))
- dfs函数调用的时间复杂度为 n
- for循环时间复杂度也为 n
- 而还有使用到了一个内置的排序功能 时间复杂度为 nlgn
- 所以整体的时间复杂度为 O(n+n+nlgn)简约为 O(nlgn)
- 方法二:
- 中序遍历,直接算出结果
- 中序遍历出树,就是一串递增的数字
package com.myserious.code.cdz;
import java.util.*;
/**
* 给定一个二叉搜索树的根结点 root, 返回树中任意两节点的差的最小值。
* <p>
* 示例:
* <p>
* 输入: root = [4,2,6,1,3,null,null]
* 输出: 1
* 解释:
* 注意,root是树结点对象(TreeNode object),而不是数组。
* <p>
* 给定的树 [4,2,6,1,3,null,null] 可表示为下图:
* <p>
* 4
* / \
* 2 6
* / \
* 1 3
* <p>
* 最小的差值是 1, 它是节点1和节点2的差值, 也是节点3和节点2的差值。
* 注意:
* <p>
* 二叉树的大小范围在 2 到 100。
* 二叉树总是有效的,每个节点的值都是整数,且不重复。
* <p>
* 来源:力扣(LeetCode)
* 链接:https://leetcode-cn.com/problems/minimum-distance-between-bst-nodes
* 著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
*/
public class Week9EMinDiffInBST783 {
/**
* 时间复杂度为 O(nlg(n))
* 原因:分析
* dfs函数调用的时间复杂度为 n
* for循环时间复杂度也为 n
* 而还有使用到了一个内置的排序功能 时间复杂度为 nlgn
* 所以整体的时间复杂度为 O(n+n+nlgn)简约为 O(nlgn)
*/
private List<Integer> values = new ArrayList<>();
public int minDiffInBST(TreeNode root) {
dfs(root);
values.sort(null);
int ans = Integer.MAX_VALUE;
for (int i = 0; i < values.size()-1; i++) {
ans = Math.min(ans,values.get(i+1)-values.get(i));
}
return ans;
}
private void dfs(TreeNode root) {
if (root==null) return;
values.add(root.val);
dfs(root.right);
dfs(root.left);
}
/**
* 方法二:中序遍历,直接算出结果
* 时间复杂度为O(N) N节点数量
* 空间复杂度O(H) H为树的高度
*
* 时间复杂度好理解,inorder中的 的代码运行次数
* 空间复杂度为:方法调用栈的大小
*
* 为什么栈的大小是 树的高度H而不是节点数量N?
* 原因:
* 首先这是一个平衡二叉树,第一个递归调用,栈的空间耗费了H空间,并且释放
* 第二次递归调用,又是耗费了H的空间,然后释放,所以是H空间。
*/
private TreeNode pre;
private int ans = Integer.MAX_VALUE;
public int minDiffInBST1(TreeNode root) {
inorder(root);
return ans;
}
/**
* 二叉搜索树的中序遍历是有序递增序列
* @param root
*/
private void inorder(TreeNode root) {
if (root==null)return;
inorder(root.left);
if (pre!=null){
ans =Math.min(ans,root.val -pre.val) ;
}
//每次计算完都需要赋值
pre = root;
inorder(root.right);
System.out.println();
}
//思路正确,方法错误,
// 重点在:任意两个节点
// 将所有节点元素拿出,然后进行排序,然后前后相减,得到所有的差值
// 拿出最小的那个差值即可
// private void bst(TreeNode root) {
// TreeNode left = root.left;
// TreeNode right = root.right;
// int a= 0;
// if (left!=null&&right!=null){
// a = Math.min(Math.abs(left.val-root.val),Math.abs(right.val-root.val));
// }else if (left!=null){
// a = Math.abs(left.val-root.val);
// }else if (right!=null){
// a = Math.abs(right.val-root.val);
// }else {
// return;
// }
// mixList.sort(Comparator.comparingInt(Integer::intValue));
// mixList.add(a);
// bst(root.left);
// bst(root.right);
// }
public class TreeNode {
int val;
TreeNode left;
TreeNode right;
TreeNode(int x) {
val = x;
}
}
}
1.2.4. 687 最长同值路径
题目意思:
- 最长的相同节点的路径
- 路径:可以理解成边长 注意点:
- 递归是left与right当与node.val不相等时,返回的是0
package com.myserious.code.cdz;
/**
*
* 687. 最长同值路径
* 给定一个二叉树,找到最长的路径,这个路径中的每个节点具有相同值。 这条路径可以经过也可以不经过根节点。
* <p>
* 注意:两个节点之间的路径长度由它们之间的边数表示。
* <p>
* 示例 1:
* <p>
* 输入:
* <p>
* 5
* / \
* 4 5
* / \ \
* 1 1 5
* 输出:
* <p>
* 2
* 示例 2:
* <p>
* 输入:
* <p>
* 1
* / \
* 4 5
* / \ \
* 4 4 5
* 输出:
* <p>
* 2
* 注意: 给定的二叉树不超过10000个结点。 树的高度不超过1000。
* <p>
* 来源:力扣(LeetCode)
* 链接:https://leetcode-cn.com/problems/longest-univalue-path
* 著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
*/
public class Week9ELongestUnivaluePath687 {
private int ans;
/**
* 题目看懂很重要
* 根本没有看懂题目,
* 办法:去网上看视频
* @param root
* @return
*/
public int longestUnivaluePath(TreeNode root) {
arrowLength(root);
return ans;
}
private int arrowLength(TreeNode node) {
if (node==null)return 0;
int left = arrowLength(node.left);
int right = arrowLength(node.right);
//为什么不能直接返回left 与right的值
//这里可以想象,递归是将问题简化为最小的一次单位
//也就是说,只统计node这一个节点的相同子节点
//如果这一次node.val!=node.left.val&&node.val!=node.right.val
//那么其 left与right下面所有所统计的相同节点数量都是无效的,所以必须为0!
// if (node.left!=null&&node.val==node.left.val) left++;
// if (node.right!=null&&node.val==node.right.val) right++;
left = (node.left!=null&&node.val==node.left.val)? left+1:0;
right =(node.right!=null&&node.val==node.right.val)?right+1:0;
ans = Math.max(ans,left+right);
return Math.max(left,right);
}
public class TreeNode {
int val;
TreeNode left;
TreeNode right;
TreeNode(int x) {
val = x;
}
}
}
1.2.5. 894 所有可能的满二叉树
注意点:
- 偶数不能是满二叉树
- 满二叉树:节点要么有left与right、要么没有
- 技巧:
- 总节点数为N
- root的left有
i个时 - right有
N-i-1个
package com.myserious.code.cdz;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
/**
* 894. 所有可能的满二叉树
* 满二叉树是一类二叉树,其中每个结点恰好有 0 或 2 个子结点。
* <p>
* 返回包含 N 个结点的所有可能满二叉树的列表。 答案的每个元素都是一个可能树的根结点。
* <p>
* 答案中每个树的每个结点都必须有 node.val=0。
* <p>
* 你可以按任何顺序返回树的最终列表。
* <p>
*
* <p>
* 示例:
* <p>
* 输入:7
* 输出:[[0,0,0,null,null,0,0,null,null,0,0],[0,0,0,null,null,0,0,0,0],[0,0,0,0,0,0,0],[0,0,0,0,0,null,null,null,null,0,0],[0,0,0,0,0,null,null,0,0]]
* 解释:
* <p>
*
* <p>
* 提示:
* <p>
* 1 <= N <= 20
* 在真实的面试中遇到过这道题?
* <p>
* 来源:力扣(LeetCode)
* 链接:https://leetcode-cn.com/problems/all-possible-full-binary-trees
* 著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
*/
public class Week9MAllPossibleFBT894 {
private Map<Integer,List<TreeNode>> memo = new HashMap<>();
public List<TreeNode> allPossibleFBT(int N) {
if (!memo.containsKey(N)){
List<TreeNode> ans = new LinkedList<>();
if (N==1){
TreeNode treeNode = new TreeNode(0);
ans.add(treeNode);
}else if (N%2==1){
//奇数才有满二叉树
//为什么i不能等于N
//不能等于N的原因:
//一个节点必须 有左右子节点 或者没有
//当i=N时 root节点不满足left和right都有值
for (int i = 1; i < N; i+=2) {
for (TreeNode left :allPossibleFBT(i)){
for (TreeNode right :allPossibleFBT(N-i-1)){
TreeNode root = new TreeNode(0);
root.left = left;
root.right = right;
ans.add(root);
}
}
}
}
memo.put(N,ans);
}
return memo.get(N);
}
public static void main(String[] args) {
for (int i = 1; i < 21; i+=2) {
System.out.println(i);
}
}
public class TreeNode {
int val;
TreeNode left;
TreeNode right;
TreeNode(int x) {
val = x;
}
}
}
2. review
3. tip
3.1. 数据库
3.1.1. 重建索引,释放空间
删除表内容,表空间大小并没有相应缩减问题
这是因为键索引没有释放:
重建非主键索引,直接删除,然后重新设置即可
alter table T drop index k;
alter table T add index(k);
重建主键索引,不能删除主键索引,这样会导致所有索引重建。
解决:使用 alter table T engine=InnoDB
3.1.2. 回表的意思
索引的数据结构:
主键索引上会存入主键ID
非主键索引上会存入非主键字段加上主键ID
回表的意思是:
当使用非主键索引查询时,MySQL会先搜索非主键索引定位,找到符合条件的后,在去主键索引搜索一次找到行。
3.1.3. MySQL索引问题
表字段
a--|b—|c—|d—|
InnoDB会把主键字段放到索引定义字段后面,当然同时也会去重。 列如: 如果主键索引为(a,b)的话
c如果为普通索引,MySQL内部其实是:(c,a,b)索引形式
所以如果设置为(c,a)联合普通索引,内部:(c,a,b) 是与设置c索引一样的
注意:如果是(c,b)联合索引的话,内部为:(c,b,a)
3.2. 算法
3.2.1. 递归技巧
- 避免问题
- 使用共享值来限制递归层数,避免栈溢出。
- 使用散列表记录计算值,避免重复计算提高性能。
- 写代码技巧
- 拆分子任务,就当这些任务已经完成了,然后进行思考。
- 调试技巧
- 打印日志+断点
3.3. 多线程
3.3.1. volatile与happens-before原则结合
volatile修饰的变量可以保证可见性,被volatile修饰的变量,前所有满足happens-before原则的变量都可以保证可见性,即使其没有被volatile修饰。
- 打印日志+断点
class VolatileExample {
int x = 0;
volatile boolean v = false;
public void writer() {
x = 42;
v = true;
}
public void reader() {
if (v == true) { // 这里x会是多少呢? } }}
System.out.println("x="+x);
}
}
}
Tip:v被volatile修饰,其之前所有的共享变量赋值,都会发生于v赋值之前,但不保证顺序。
3.3.2. JRE、JDK、JVM辨析
- JRE Java运行时 环境
- JDK Java开发工具包(包含JRE)
- JVM Java虚拟机(JRE的一部分)
3.3.3. JVM内存结构、Java内存模型、Java对象模型区别
JVM内存结构:Java虚拟机运行时的区域有关 Java内存模型:并发,不同平台的统一指令翻译规范 Java对象模型:Java对象在虚拟机中的对象表现形式
3.3.4. JMM是什么?
- JMM是规范
- 是关键字、工具类的原理
- 重要的三点内容
- 重排序、可见性、原子性
3.4. 架构
3.4.1. 高可用(四个九)
可用性是指:
1-停机时间/总应运行时间4个九,可用性达99.99%,也就是说一年不超过53分钟的停机时间。
- 重排序、可见性、原子性
这不是一件容易的事情,很多大厂拼尽了所有资源也许也做不到这一点。 让我想到在架构设计时,与todo-list相同重要的是no-todo-list。 而衍生一下,做任何复杂的事情,甚至有时候,no-todo-list比todo-list更重要。 如现在很多人支援湖北等地,因为疫情爆发迅猛,物资不够,很多人利用国际支援,对湖北各地进行支援。 而我看到最好的是张潇雨的计划,比先制定no-todo-list,不去管那些大的医院,因为那里肯定集中了很多的资源,还有可能不让进去。 现在的计划是如何将手上的资源,在最短的时间内发放到有效的,最多的医院。
4. share
4.1. 魔鬼问题
4.1.1. TreadLocal的使用场景是什么?
- 打印日志
- spring的上线文 注意会有内存泄露的问题。
4.1.2. CAS并发工具之间区别是什么?
4.1.2.1. CountDownLatch与Semaphore与CyclicBarrier之间区别及其应用场景是什么?
CountDownLatch:
- 初始值后没有到零就一直堵塞。
- 使用场景
- 一个线程等待多个线程
- 比如说一个旅游团队长,需要等待所有成员到齐了才能出发
CyclicBarrier:
- 初始值后没有到零一直堵塞,
- 当到达零时,有回调函数(初始化时可以设置)
- 当回调函数执行完后,重新恢复初始值
- 使用场景
- 一组线程之间相互等待
- 比如几个驴友之间不离不弃,一个人走的比较快了,等待另一个人,这个往复下去。
Semaphore:
- 初始值后,可以加减值
- 当值为零时堵塞,不为零时通过
- 使用场景
- 限流
参考: https://time.geekbang.org/column/article/89461 https://time.geekbang.org/column/article/88499
4.1.3. 可见性问题(图)为什么会有可见性问题?
画图可见性问题原因:
- 多级缓存 画图Java解决可见性问题思路:
- 抽象JMM模型
- 可见性
- 有序性
- 原子性
4.1.3.1. 主存中到底存在什么样的变量?什么样的变量存在主存?
答:类中定义的变量在主存中,方法类的变量不在主存中。

4.1.4. 高性能
4.1.4.1. 什么是高性能?
高性能其实是站在用户层面的一件事情,就是一个用户在请求或执行一项任务时所感知到的,耗费的时间。
4.1.4.2. 如果提升高性能?
架构层面上做文章,有垂直于水平两个方向。
- 垂直方向:升级软硬件
- 软件:系统方面优化减少IO操作
- 硬件
- 多核处理器
- RAID磁盘
- SSD
- 等
- 水平方向
- 多台机器
- 业务拆分
4.1.4.3. 高性能与伸缩性有什么区别?
一个是站在用户角度来思考架构,一个是站在提供者角度思考架构。 高性能是需要每次用户请求,得到快速响应。 伸缩性,是在单位时间内,且在可接受的用户响应范围内,系统接受更多的请求。
其实架构的工作是平衡,在业务层面上,设计出符合当前的设计。
不可能一味的所有接口都去追求高性能,而判断哪些需要高性能,哪些不需要,对未来进行拓展预留地方,都是架构所要做的事情。
4.1.5. 平衡二叉树中序遍历时间复杂度为什么为O(H)?

4.2. 修改一个小表导致整个数据库崩溃的案例
来源:https://time.geekbang.org/column/article/69862
原理:MySQL会隐式加入MDL(元数据锁(meta data lock,MDL))也叫**读写锁。
修改一张小表导致数据库崩溃的原因就在此。 原因:
- MDL读是可以重入的,没有执行完不会释放。
- 当先有session读一个此小表(没有释放MDL)
- 此时如果进行DDL操作
- DDL会等待所有之前的session全部释放完MDL之后才会进行写操作(block这张表)
- 后续所有进行查询都会被block
- 积累过多后就会导致崩溃
解决办法:
- 停服务,进行DDL
- 使用工具,DDL加上wait等待时间(MariaDB 已经合并了 AliSQL 的这个功能,所以这两个开源分支目前都支持 DDL NOWAIT/WAIT n 这个语法。)