diff --git a/notes/Java 并发.md b/notes/Java 并发.md
index 8a2c8fcf..139f4353 100644
--- a/notes/Java 并发.md
+++ b/notes/Java 并发.md
@@ -569,7 +569,7 @@ ReentrantLock 多了一些高级功能。
在线程中调用另一个线程的 join() 方法,会将当前线程挂起,而不是忙等待, 直到目标线程结束。
-对于以下代码,虽然 b 线程先启动,但是因为在 b 线程中调用了 a 线程的 join() 方法,因此 b 线程会等待 a 线程结束才继续执行,因此最后能够保证 a 线程的输出先与 b 线程的输出。
+对于以下代码,虽然 b 线程先启动,但是因为在 b 线程中调用了 a 线程的 join() 方法,因此 b 线程会等待 a 线程结束才继续执行,因此最后能够保证 a 线程的输出先于 b 线程的输出。
```java
public class JoinExample {
@@ -887,7 +887,7 @@ java.util.concurrent.BlockingQueue 接口有以下阻塞队列的实现:
- **FIFO 队列** :LinkedBlockingQueue、ArrayListBlockingQueue(固定长度)
- **优先级队列** :PriorityBlockingQueue
-提供了阻塞的 take() 和 put() 方法:如果队列为空 take() 将阻塞,直到队列中有内容;如果队列为满 put() 将阻塞,指到队列有空闲位置。
+提供了阻塞的 take() 和 put() 方法:如果队列为空 take() 将阻塞,直到队列中有内容;如果队列为满 put() 将阻塞,直到队列有空闲位置。
**使用 BlockingQueue 实现生产者消费者问题**
diff --git a/notes/Leetcode 题解.md b/notes/Leetcode 题解.md
index 1e6c9b76..77593d89 100644
--- a/notes/Leetcode 题解.md
+++ b/notes/Leetcode 题解.md
@@ -2381,7 +2381,9 @@ dp[N] 即为所求。
```java
public int climbStairs(int n) {
- if (n <= 2) return n;
+ if (n <= 2) {
+ return n;
+ }
int pre2 = 1, pre1 = 2;
for (int i = 2; i < n; i++) {
int cur = pre1 + pre2;
@@ -2392,55 +2394,27 @@ public int climbStairs(int n) {
}
```
-**母牛生产**
-
-[程序员代码面试指南-P181](#)
-
-题目描述:假设农场中成熟的母牛每年都会生 1 头小母牛,并且永远不会死。第一年有 1 只小母牛,从第二年开始,母牛开始生小母牛。每只小母牛 3 年之后成熟又可以生小母牛。给定整数 N,求 N 年后牛的数量。
-
-第 i 年成熟的牛的数量为:
-
-
-
**强盗抢劫**
[198. House Robber (Easy)](https://leetcode.com/problems/house-robber/description/)
题目描述:抢劫一排住户,但是不能抢邻近的住户,求最大抢劫量。
-定义 dp 数组用来存储最大的抢劫量,其中 dp[i] 表示抢到第 i 个住户时的最大抢劫量。由于不能抢劫邻近住户,因此如果抢劫了第 i 个住户那么只能抢劫 i - 2 和 i - 3 的住户,所以
+定义 dp 数组用来存储最大的抢劫量,其中 dp[i] 表示抢到第 i 个住户时的最大抢劫量。由于不能抢劫邻近住户,因此如果抢劫了第 i 个住户那么只能抢劫 i - 2 或者 i - 3 的住户,所以
-O(n) 空间复杂度实现方法:
-
```java
public int rob(int[] nums) {
int n = nums.length;
- if(n == 0) return 0;
- if(n == 1) return nums[0];
- if(n == 2) return Math.max(nums[0], nums[1]);
- int[] dp = new int[n];
- dp[0] = nums[0];
- dp[1] = nums[1];
- dp[2] = nums[0] + nums[2];
- for(int i = 3; i < n; i++){
- dp[i] = Math.max(dp[i -2], dp[i - 3]) + nums[i];
+ if (n == 0) {
+ return 0;
}
- return Math.max(dp[n - 1], dp[n - 2]);
-}
-```
-
-O(1) 空间复杂度实现方法:
-
-```java
-public int rob(int[] nums) {
- int n = nums.length;
- if(n == 0) return 0;
- if(n == 1) return nums[0];
- if(n == 2) return Math.max(nums[0], nums[1]);
- int pre3 = nums[0], pre2 = nums[1], pre1 = nums[2] + nums[0];
- for(int i = 3; i < n; i++){
+ if (n == 1) {
+ return nums[0];
+ }
+ int pre3 = 0, pre2 = 0, pre1 = 0;
+ for (int i = 0; i < n; i++) {
int cur = Math.max(pre2, pre3) + nums[i];
pre3 = pre2;
pre2 = pre1;
@@ -2455,34 +2429,44 @@ public int rob(int[] nums) {
[213. House Robber II (Medium)](https://leetcode.com/problems/house-robber-ii/description/)
```java
-private int[] dp;
-
public int rob(int[] nums) {
- if (nums == null || nums.length == 0) return 0;
+ if (nums == null || nums.length == 0) {
+ return 0;
+ }
int n = nums.length;
- if (n == 1) return nums[0];
- dp = new int[n];
+ if (n == 1) {
+ return nums[0];
+ }
return Math.max(rob(nums, 0, n - 2), rob(nums, 1, n - 1));
}
private int rob(int[] nums, int first, int last) {
- if (last - first == 0) return nums[first];
- if (last - first == 1) return Math.max(nums[first], nums[first + 1]);
- dp[first] = nums[first];
- dp[first + 1] = nums[first + 1];
- dp[first + 2] = nums[first] + nums[first + 2];
- for (int i = first + 3; i <= last; i++) {
- dp[i] = Math.max(dp[i - 2], dp[i - 3]) + nums[i];
+ int pre3 = 0, pre2 = 0, pre1 = 0;
+ for (int i = first; i <= last; i++) {
+ int cur = Math.max(pre3, pre2) + nums[i];
+ pre3 = pre2;
+ pre2 = pre1;
+ pre1 = cur;
}
- return Math.max(dp[last], dp[last - 1]);
+ return Math.max(pre2, pre1);
}
```
+**母牛生产**
+
+[程序员代码面试指南-P181](#)
+
+题目描述:假设农场中成熟的母牛每年都会生 1 头小母牛,并且永远不会死。第一年有 1 只小母牛,从第二年开始,母牛开始生小母牛。每只小母牛 3 年之后成熟又可以生小母牛。给定整数 N,求 N 年后牛的数量。
+
+第 i 年成熟的牛的数量为:
+
+
+
**信件错排**
题目描述:有 N 个 信 和 信封,它们被打乱,求错误装信方式的数量。
-定义一个数组 dp 存储错误方式数量,dp[i] 表示 i 个信和信封的错误方式数量。假设第 i 个信装到第 j 个信封里面,而第 j 个信装到第 k 个信封里面。根据 i 和 k 是否相等,有两种情况:
+定义一个数组 dp 存储错误方式数量,dp[i] 表示前 i 个信和信封的错误方式数量。假设第 i 个信装到第 j 个信封里面,而第 j 个信装到第 k 个信封里面。根据 i 和 k 是否相等,有两种情况:
- i==k,交换 i 和 k 的信后,它们的信和信封在正确的位置,但是其余 i-2 封信有 dp[i-2] 种错误装信的方式。由于 j 有 i-1 种取值,因此共有 (i-1)\*dp[i-2] 种错误装信方式。
- i != k,交换 i 和 j 的信后,第 i 个信和信封在正确的位置,其余 i-1 封信有 dp[i-1] 种错误装信方式。由于 j 有 i-1 种取值,因此共有 (i-1)\*dp[i-1] 种错误装信方式。
@@ -2493,8 +2477,6 @@ private int rob(int[] nums, int first, int last) {
dp[N] 即为所求。
-和上楼梯问题一样,dp[i] 只与 dp[i-1] 和 dp[i-2] 有关,因此也可以只用两个变量来存储 dp[i-1] 和 dp[i-2]。
-
### 最长递增子序列
已知一个序列 {S1, S2,...,Sn} ,取出若干数组成新的序列 {Si1, Si2,..., Sim},其中 i1、i2 ... im 保持递增,即新序列中各个数仍然保持原数列中的先后顺序,称新序列为原序列的一个 **子序列** 。
@@ -2526,51 +2508,68 @@ public int lengthOfLIS(int[] nums) {
}
dp[i] = max;
}
- int ret = 0;
- for (int i = 0; i < n; i++) {
- ret = Math.max(ret, dp[i]);
- }
- return ret;
+ return Arrays.stream(dp).max().orElse(0);
}
```
-以上解法的时间复杂度为 O(N2) ,可以使用二分查找将时间复杂度降低为 O(NlogN)。定义一个 tails 数组,其中 tails[i] 存储长度为 i + 1 的最长递增子序列的最后一个元素。如果有多个长度相等的最长递增子序列,那么 tails[i] 就取最小值。例如对于数组 [4,5,6,3],有
+使用 Stream 求最大值会导致运行时间过长,可以改成以下形式:
-```html
-len = 1 : [4], [5], [6], [3] => tails[0] = 3
-len = 2 : [4, 5], [5, 6] => tails[1] = 5
-len = 3 : [4, 5, 6] => tails[2] = 6
+```java
+int ret = 0;
+for (int i = 0; i < n; i++) {
+ ret = Math.max(ret, dp[i]);
+}
+return ret;
```
+以上解法的时间复杂度为 O(N2) ,可以使用二分查找将时间复杂度降低为 O(NlogN)。
-对于一个元素 x,
+定义一个 tails 数组,其中 tails[i] 存储长度为 i + 1 的最长递增子序列的最后一个元素。对于一个元素 x,
- 如果它大于 tails 数组所有的值,那么把它添加到 tails 后面,表示最长递增子序列长度加 1;
- 如果 tails[i-1] < x <= tails[i],那么更新 tails[i-1] = x。
+例如对于数组 [4,3,6,5],有:
+
+```html
+tails len num
+[] 0 4
+[4] 1 3
+[3] 1 6
+[3,6] 2 5
+[3,5] 2 null
+```
+
可以看出 tails 数组保持有序,因此在查找 Si 位于 tails 数组的位置时就可以使用二分查找。
```java
public int lengthOfLIS(int[] nums) {
int n = nums.length;
int[] tails = new int[n];
- int size = 0;
- for (int i = 0; i < n; i++) {
- int index = binarySearch(tails, 0, size, nums[i]);
- tails[index] = nums[i];
- if (index == size) size++;
+ int len = 0;
+ for (int num : nums) {
+ int index = binarySearch(tails, len, num);
+ tails[index] = num;
+ if (index == len) {
+ len++;
+ }
}
- return size;
+ return len;
}
-private int binarySearch(int[] nums, int first, int last, int key) {
- while (first < last) {
- int mid = first + (last - first) / 2;
- if (nums[mid] == key) return mid;
- else if (nums[mid] > key) last = mid;
- else first = mid + 1;
+private int binarySearch(int[] tails, int len, int key) {
+ int l = 0, h = len;
+ while (l < h) {
+ int mid = l + (h - l) / 2;
+ if (tails[mid] == key) {
+ return mid;
+ } else if (tails[mid] > key) {
+ h = mid;
+ } else {
+ l = mid + 1;
+ }
}
- return first;
+ return l;
}
```
diff --git a/notes/MySQL.md b/notes/MySQL.md
index 0e39a69c..0805d38a 100644
--- a/notes/MySQL.md
+++ b/notes/MySQL.md
@@ -32,13 +32,13 @@
InnoDB 是 MySQL 默认的事务型存储引擎,只有在需要 InnoDB 不支持的特性时,才考虑使用其它存储引擎。
-采用 MVCC 来支持高并发,并且实现了四个标准的隔离级别,默认级别是可重复读(REPEATABLE READ),并且通过间隙锁(next-key locking)策略防止幻读的出现。间隙锁使得 InnoDB 不仅仅锁定查询涉及的行,还会对索引中的间隙进行锁定,以防止幻影行的插入。
+采用 MVCC 来支持高并发,并且实现了四个标准的隔离级别,默认级别是可重复读(REPEATABLE READ),并且通过间隙锁(next-key locking)策略防止幻影读。间隙锁使得 InnoDB 不仅仅锁定查询涉及的行,还会对索引中的间隙进行锁定,以防止幻影行的插入。
表是基于聚簇索引建立的,它对主键的查询性能有很大的提升。
内部做了很多优化,包括从磁盘读取数据时采用的可预测性读、能够自动在内存中创建哈希索引以加速读操作的自适应哈希索引、能够加速插入操作的插入缓冲区等。
-通过一些机制和工具支持真正的热备份。其它存储引擎不支持热备份,要获取一致性视图需要停止对所有表的写入,而在读写混合场景中,停止写入可能也意味着停止读取。
+通过一些机制和工具支持真正的在线热备份。其它存储引擎不支持在线热备份,要获取一致性视图需要停止对所有表的写入,而在读写混合场景中,停止写入可能也意味着停止读取。
## MyISAM
@@ -56,14 +56,16 @@ MyISAM 设计简单,数据以紧密格式存储。对于只读数据,或者
## 比较
-- 事务:InnoDB 是事务型的。
+- 事务:InnoDB 是事务型的,可以使用 Commit 和 Rollback 语句。
+
+- 并发:MyISAM 只支持表级锁,而 InnoDB 还支持行级锁。
+
+- 外键:InnoDB 支持外键。
- 备份:InnoDB 支持在线热备份。
- 崩溃恢复:MyISAM 崩溃后发生损坏的概率比 InnoDB 高很多,而且恢复的速度也更慢。
-- 并发:MyISAM 只支持表级锁,而 InnoDB 还支持行级锁。
-
- 其它特性:MyISAM 支持压缩表和空间数据索引。
# 二、数据类型
@@ -168,7 +170,7 @@ B+Tree 相比于 B-Tree 更适合外存索引,因为 B+Tree 内节点去掉了
为了减少磁盘 I/O,磁盘往往不是严格按需读取,而是每次都会预读。这样做的理论依据是计算机科学中著名的局部性原理:当一个数据被用到时,其附近的数据也通常会马上被使用。预读过程中,磁盘进行顺序读取,顺序读取不需要进行磁盘寻道,并且只需要很短的旋转时间,因此速度会非常快。
-操作系统一般将内存和磁盘分割成固态大小的块,每一块称为一页,内存与磁盘以页为单位交换数据。数据库系统将索引的一个节点的大小设置为页的大小,使得一次 I/O 就能完全载入一个节点,并且可以利用预读特性,临近的节点也能够被预先载入。
+操作系统一般将内存和磁盘分割成固态大小的块,每一块称为一页,内存与磁盘以页为单位交换数据。数据库系统将索引的一个节点的大小设置为页的大小,使得一次 I/O 就能完全载入一个节点,并且可以利用预读特性,相邻的节点也能够被预先载入。
更多内容请参考:[MySQL 索引背后的数据结构及算法原理](http://blog.codinglabs.org/articles/theory-of-mysql-index.html)
@@ -211,13 +213,13 @@ MyISAM 存储引擎支持空间数据索引,可以用于地理数据存储。
### 4. 全文索引
-MyISAM 存储引擎支持全文索引,用于查找文本中的关键词,而不是直接比值是否相等。查找条件使用 MATCH AGAINST,而不是普通的 WHERE。
+MyISAM 存储引擎支持全文索引,用于查找文本中的关键词,而不是直接比较是否相等。查找条件使用 MATCH AGAINST,而不是普通的 WHERE。
InnoDB 存储引擎在 MySQL 5.6.4 版本中也开始支持全文索引。
## 索引的优点
-- 大大减少了服务器需要扫描的数据量;
+- 大大减少了服务器需要扫描的数据行数。
- 帮助服务器避免进行排序和创建临时表(B+Tree 索引是有序的,可以用来做 ORDER BY 和 GROUP BY 操作);
@@ -291,7 +293,7 @@ customer_id_selectivity: 0.0373
**优点**
-- 可以把相关数据保存在一起,减少 I/O 操作。例如电子邮件表可以根据用户 ID 来聚集数据,这样只需要从磁盘读取少数的数据也就能获取某个用户的全部邮件,如果没有使用聚聚簇索引,则每封邮件都可能导致一次磁盘 I/O。
+- 可以把相关数据保存在一起,减少 I/O 操作。例如电子邮件表可以根据用户 ID 来聚集数据,这样只需要从磁盘读取少数的数据也就能获取某个用户的全部邮件,如果没有使用聚簇索引,则每封邮件都可能导致一次磁盘 I/O。
- 数据访问更快。
**缺点**
@@ -306,7 +308,7 @@ customer_id_selectivity: 0.0373
## 使用 Explain 进行分析
-Explain 用来分析 SELECT 查询语句,开发人员可以通过分析结果来优化查询语句。
+Explain 用来分析 SELECT 查询语句,开发人员可以通过分析 Explain 结果来优化查询语句。
比较重要的字段有:
@@ -405,7 +407,7 @@ SELECT * FROM post WHERE post.id IN (123,456,567,9098,8904);
### 1. 事务问题
-使用分布式事务。
+使用分布式事务,比如 XA 接口。
### 2. JOIN