Merge branch 'master' of github.com:CyC2018/CS-Notes

This commit is contained in:
Chris Qi 2020-11-18 20:16:05 +00:00
commit 287b9b838b
385 changed files with 3139 additions and 47511 deletions

View File

@ -1,6 +1,6 @@
<div align="center">
<a href="https://gitstar-ranking.com/repositories"> <img src="https://badgen.net/badge/Rank/13?icon=github&color=4ab8a1"></a>
<a href="https://cyc2018.github.io/CS-Notes"> <img src="https://badgen.net/badge/CyC/%E5%9C%A8%E7%BA%BF%E9%98%85%E8%AF%BB?icon=sourcegraph&color=4ab8a1"></a>
<a href="https://www.cyc2018.xyz"> <img src="https://badgen.net/badge/CyC/%E5%9C%A8%E7%BA%BF%E9%98%85%E8%AF%BB?icon=sourcegraph&color=4ab8a1"></a>
<a href="assets/download.md"> <img src="https://badgen.net/badge/OvO/%E7%A6%BB%E7%BA%BF%E4%B8%8B%E8%BD%BD?icon=telegram&color=4ab8a1"></a>
<a href="assets/download.md"> <img src="https://badgen.net/badge/%e5%85%ac%e4%bc%97%e5%8f%b7/CyC2018?icon=rss&color=4ab8a1"></a>
</div>
@ -36,16 +36,11 @@
- [HTTP](https://github.com/CyC2018/CS-Notes/blob/master/notes/HTTP.md)
- [Socket](https://github.com/CyC2018/CS-Notes/blob/master/notes/Socket.md)
## :art: 面向对象
- [面向对象思想](https://github.com/CyC2018/CS-Notes/blob/master/notes/面向对象思想.md)
- [设计模式](https://github.com/CyC2018/CS-Notes/blob/master/notes/设计模式%20-%20目录.md)
## :floppy_disk: 数据库
- [数据库系统原理](https://github.com/CyC2018/CS-Notes/blob/master/notes/数据库系统原理.md)
- [SQL](https://github.com/CyC2018/CS-Notes/blob/master/notes/SQL.md)
- [Leetcode-Database 题解](https://github.com/CyC2018/CS-Notes/blob/master/notes/Leetcode-Database%20题解.md)
- [SQL 语法](https://github.com/CyC2018/CS-Notes/blob/master/notes/SQL%20语法.md)
- [SQL 练习](https://github.com/CyC2018/CS-Notes/blob/master/notes/SQL%20练习.md)
- [MySQL](https://github.com/CyC2018/CS-Notes/blob/master/notes/MySQL.md)
- [Redis](https://github.com/CyC2018/CS-Notes/blob/master/notes/Redis.md)
@ -66,6 +61,11 @@
- [缓存](https://github.com/CyC2018/CS-Notes/blob/master/notes/缓存.md)
- [消息队列](https://github.com/CyC2018/CS-Notes/blob/master/notes/消息队列.md)
## :art: 面向对象
- [面向对象思想](https://github.com/CyC2018/CS-Notes/blob/master/notes/面向对象思想.md)
- [设计模式](https://github.com/CyC2018/CS-Notes/blob/master/notes/设计模式%20-%20目录.md)
## :wrench: 工具
- [Git](https://github.com/CyC2018/CS-Notes/blob/master/notes/Git.md)

View File

@ -1,61 +1,2 @@
- [Github](https://github.com/CyC2018/CS-Notes)
# 😃 该网站已迁移至 >>> [www.cyc2018.xyz](http://www.cyc2018.xyz)
## 算法
- [剑指 Offer 题解](notes/剑指%20Offer%20题解%20-%20目录2.md) </br>
- [Leetcode 题解](notes/Leetcode%20题解%20-%20目录1.md) </br>
- [算法](notes/算法%20-%20目录1.md) </br>
## 💻 操作系统
- [计算机操作系统](notes/计算机操作系统%20-%20目录1.md) </br>
- [Linux](notes/Linux.md)
## 网络
- [计算机网络](notes/计算机网络%20-%20目录1.md) </br>
- [HTTP](notes/HTTP.md) </br>
- [Socket](notes/Socket.md)
## 🎨 面向对象
- [设计模式](notes/设计模式%20-%20目录1.md) </br>
- [面向对象思想](notes/面向对象思想.md)
## 💾 数据库
- [数据库系统原理](notes/数据库系统原理.md) </br>
- [SQL](notes/SQL.md) </br>
- [Leetcode-Database 题解](notes/Leetcode-Database%20题解.md) </br>
- [MySQL](notes/MySQL.md) </br>
- [Redis](notes/Redis.md)
## Java
- [Java 基础](notes/Java%20基础.md) </br>
- [Java 容器](notes/Java%20容器.md) </br>
- [Java 并发](notes/Java%20并发.md) </br>
- [Java 虚拟机](notes/Java%20虚拟机.md) </br>
- [Java I/O](notes/Java%20IO.md)
## 💡 系统设计
- [系统设计基础](notes/系统设计基础.md) </br>
- [分布式](notes/分布式.md) </br>
- [集群](notes/集群.md) </br>
- [攻击技术](notes/攻击技术.md) </br>
- [缓存](notes/缓存.md) </br>
- [消息队列](notes/消息队列.md)
## 🔧 工具
- [Git](notes/Git.md) </br>
- [Docker](notes/Docker.md) </br>
- [正则表达式](notes/正则表达式.md) </br>
- [构建工具](notes/构建工具.md)
<!--️欢迎关注我的公众号 CyC2018在公众号后台回复关键字 📚 **资料** 可领取复习大纲这份大纲是我花了一整年时间整理的面试知识点列表不仅系统整理了面试知识点而且标注了各个知识点的重要程度从而帮你理清多而杂的面试知识点可以说我基本是按照这份大纲来进行复习的这份大纲对我拿到了 BAT 头条等 Offer 起到很大的帮助你们完全可以和我一样根据大纲上列的知识点来进行复习就不用看很多不重要的内容也可以知道哪些内容很重要从而多安排一些复习时间
<br/><br/>
<div align="center">
<img src="https://cyc-1256109796.cos.ap-guangzhou.myqcloud.com/%E5%85%AC%E4%BC%97%E5%8F%B7.jpg" width="200px">
</div> -->

1
docs/_404.md Normal file
View File

@ -0,0 +1 @@
# 😃 该网站已迁移至 >>> [www.cyc2018.xyz](http://www.cyc2018.xyz)

View File

@ -7,5 +7,5 @@
[![stars](https://badgen.net/github/stars/CyC2018/CS-Notes?icon=github&color=4ab8a1)](https://github.com/CyC2018/CS-Notes) [![forks](https://badgen.net/github/forks/CyC2018/CS-Notes?icon=github&color=4ab8a1)](https://github.com/CyC2018/CS-Notes)
[开始阅读](README.md)
[开始阅读](http://www.cyc2018.xyz)

View File

@ -422,7 +422,8 @@
depth: 6
},
// subMaxLevel: 2,
coverpage: true
coverpage: true,
notFoundPage: true
}
</script>
<script src="https://cyc-1256109796.cos.ap-guangzhou.myqcloud.com/docsify.min.js"></script>

View File

@ -1,76 +0,0 @@
# 10.1 斐波那契数列
## 题目链接
[NowCoder](https://www.nowcoder.com/practice/c6c7742f5ba7442aada113136ddea0c3?tpId=13&tqId=11160&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking&from=cyc_github)
## 题目描述
求斐波那契数列的第 n n <= 39
<!--<div align="center"><img src="https://latex.codecogs.com/gif.latex?f(n)=\left\{\begin{array}{rcl}0&&{n=0}\\1&&{n=1}\\f(n-1)+f(n-2)&&{n>1}\end{array}\right." class="mathjax-pic"/></div> <br> -->
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/45be9587-6069-4ab7-b9ac-840db1a53744.jpg" width="330px"> </div><br>
## 解题思路
如果使用递归求解会重复计算一些子问题例如计算 f(4) 需要计算 f(3) f(2)计算 f(3) 需要计算 f(2) f(1)可以看到 f(2) 被重复计算了
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/c13e2a3d-b01c-4a08-a69b-db2c4e821e09.png" width="350px"/> </div><br>
递归是将一个问题划分成多个子问题求解动态规划也是如此但是动态规划会把子问题的解缓存起来从而避免重复求解子问题
```java
public int Fibonacci(int n) {
if (n <= 1)
return n;
int[] fib = new int[n + 1];
fib[1] = 1;
for (int i = 2; i <= n; i++)
fib[i] = fib[i - 1] + fib[i - 2];
return fib[n];
}
```
考虑到第 i 项只与第 i-1 和第 i-2 项有关因此只需要存储前两项的值就能求解第 i 从而将空间复杂度由 O(N) 降低为 O(1)
```java
public int Fibonacci(int n) {
if (n <= 1)
return n;
int pre2 = 0, pre1 = 1;
int fib = 0;
for (int i = 2; i <= n; i++) {
fib = pre2 + pre1;
pre2 = pre1;
pre1 = fib;
}
return fib;
}
```
由于待求解的 n 小于 40因此可以将前 40 项的结果先进行计算之后就能以 O(1) 时间复杂度得到第 n 项的值
```java
public class Solution {
private int[] fib = new int[40];
public Solution() {
fib[1] = 1;
for (int i = 2; i < fib.length; i++)
fib[i] = fib[i - 1] + fib[i - 2];
}
public int Fibonacci(int n) {
return fib[n];
}
}
```
<div align="center"><img width="320px" src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/githubio/公众号二维码-2.png"></img></div>

View File

@ -1,49 +0,0 @@
# 10.2 矩形覆盖
## 题目链接
[NowCoder](https://www.nowcoder.com/practice/72a5a919508a4251859fb2cfb987a0e6?tpId=13&tqId=11163&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking&from=cyc_github)
## 题目描述
我们可以用 2\*1 的小矩形横着或者竖着去覆盖更大的矩形请问用 n 2\*1 的小矩形无重叠地覆盖一个 2\*n 的大矩形总共有多少种方法
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/b903fda8-07d0-46a7-91a7-e803892895cf.gif" width="100px"> </div><br>
## 解题思路
n 1 只有一种覆盖方法
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/f6e146f1-57ad-411b-beb3-770a142164ef.png" width="100px"> </div><br>
n 2 有两种覆盖方法
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/fb3b8f7a-4293-4a38-aae1-62284db979a3.png" width="200px"> </div><br>
要覆盖 2\*n 的大矩形可以先覆盖 2\*1 的矩形再覆盖 2\*(n-1) 的矩形或者先覆盖 2\*2 的矩形再覆盖 2\*(n-2) 的矩形而覆盖 2\*(n-1) 2\*(n-2) 的矩形可以看成子问题该问题的递推公式如下
<!-- <div align="center"><img src="https://latex.codecogs.com/gif.latex?f(n)=\left\{\begin{array}{rcl}1&&{n=1}\\2&&{n=2}\\f(n-1)+f(n-2)&&{n>1}\end{array}\right." class="mathjax-pic"/></div> <br> -->
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/508c6e52-9f93-44ed-b6b9-e69050e14807.jpg" width="370px"> </div><br>
```java
public int RectCover(int n) {
if (n <= 2)
return n;
int pre2 = 1, pre1 = 2;
int result = 0;
for (int i = 3; i <= n; i++) {
result = pre2 + pre1;
pre2 = pre1;
pre1 = result;
}
return result;
}
```
<div align="center"><img width="320px" src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/githubio/公众号二维码-2.png"></img></div>

View File

@ -1,47 +0,0 @@
# 10.3 跳台阶
## 题目链接
[NowCoder](https://www.nowcoder.com/practice/8c82a5b80378478f9484d87d1c5f12a4?tpId=13&tqId=11161&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking&from=cyc_github)
## 题目描述
一只青蛙一次可以跳上 1 级台阶也可以跳上 2 求该青蛙跳上一个 n 级的台阶总共有多少种跳法
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/9dae7475-934f-42e5-b3b3-12724337170a.png" width="380px"> </div><br>
## 解题思路
n = 1 只有一种跳法
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/72aac98a-d5df-4bfa-a71a-4bb16a87474c.png" width="250px"> </div><br>
n = 2 有两种跳法
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/1b80288d-1b35-4cd3-aa17-7e27ab9a2389.png" width="300px"> </div><br>
n 阶台阶可以先跳 1 阶台阶再跳 n-1 阶台阶或者先跳 2 阶台阶再跳 n-2 阶台阶 n-1 n-2 阶台阶的跳法可以看成子问题该问题的递推公式为
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/508c6e52-9f93-44ed-b6b9-e69050e14807.jpg" width="350px"> </div><br>
```java
public int JumpFloor(int n) {
if (n <= 2)
return n;
int pre2 = 1, pre1 = 2;
int result = 0;
for (int i = 2; i < n; i++) {
result = pre2 + pre1;
pre2 = pre1;
pre1 = result;
}
return result;
}
```
<div align="center"><img width="320px" src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/githubio/公众号二维码-2.png"></img></div>

View File

@ -1,67 +0,0 @@
# 10.4 变态跳台阶
## 题目链接
[NowCoder](https://www.nowcoder.com/practice/22243d016f6b47f2a6928b4313c85387?tpId=13&tqId=11162&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking&from=cyc_github)
## 题目描述
一只青蛙一次可以跳上 1 级台阶也可以跳上 2 ... 它也可以跳上 n 求该青蛙跳上一个 n 级的台阶总共有多少种跳法
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/cd411a94-3786-4c94-9e08-f28320e010d5.png" width="380px"> </div><br>
## 解题思路
### 动态规划
```java
public int JumpFloorII(int target) {
int[] dp = new int[target];
Arrays.fill(dp, 1);
for (int i = 1; i < target; i++)
for (int j = 0; j < i; j++)
dp[i] += dp[j];
return dp[target - 1];
}
```
### 数学推导
跳上 n-1 级台阶可以从 n-2 级跳 1 级上去也可以从 n-3 级跳 2 级上去...那么
```
f(n-1) = f(n-2) + f(n-3) + ... + f(0)
```
同样跳上 n 级台阶可以从 n-1 级跳 1 级上去也可以从 n-2 级跳 2 级上去... 那么
```
f(n) = f(n-1) + f(n-2) + ... + f(0)
```
综上可得
```
f(n) - f(n-1) = f(n-1)
```
```
f(n) = 2*f(n-1)
```
所以 f(n) 是一个等比数列
```source-java
public int JumpFloorII(int target) {
return (int) Math.pow(2, target - 1);
}
```
<div align="center"><img width="320px" src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/githubio/公众号二维码-2.png"></img></div>

View File

@ -1,72 +0,0 @@
# 11. 旋转数组的最小数字
[NowCoder](https://www.nowcoder.com/practice/9f3231a991af4f55b95579b44b7a01ba?tpId=13&tqId=11159&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking&from=cyc_github)
## 题目描述
把一个数组最开始的若干个元素搬到数组的末尾我们称之为数组的旋转输入一个非递减排序的数组的一个旋转输出旋转数组的最小元素
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/0038204c-4b8a-42a5-921d-080f6674f989.png" width="210px"> </div><br>
## 解题思路
将旋转数组对半分可以得到一个包含最小元素的新旋转数组以及一个非递减排序的数组新的旋转数组的数组元素是原数组的一半从而将问题规模减少了一半这种折半性质的算法的时间复杂度为 O(logN)为了方便这里将 log<sub>2</sub>N 写为 logN
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/424f34ab-a9fd-49a6-9969-d76b42251365.png" width="300px"> </div><br>
此时问题的关键在于确定对半分得到的两个数组哪一个是旋转数组哪一个是非递减数组我们很容易知道非递减数组的第一个元素一定小于等于最后一个元素
通过修改二分查找算法进行求解l 代表 lowm 代表 midh 代表 high
- nums[m] <= nums[h] 表示 [m, h] 区间内的数组是非递减数组[l, m] 区间内的数组是旋转数组此时令 h = m
- 否则 [m + 1, h] 区间内的数组是旋转数组 l = m + 1
```java
public int minNumberInRotateArray(int[] nums) {
if (nums.length == 0)
return 0;
int l = 0, h = nums.length - 1;
while (l < h) {
int m = l + (h - l) / 2;
if (nums[m] <= nums[h])
h = m;
else
l = m + 1;
}
return nums[l];
}
```
如果数组元素允许重复会出现一个特殊的情况nums[l] == nums[m] == nums[h]此时无法确定解在哪个区间需要切换到顺序查找例如对于数组 {1,1,1,0,1}lm h 指向的数都为 1此时无法知道最小数字 0 在哪个区间
```java
public int minNumberInRotateArray(int[] nums) {
if (nums.length == 0)
return 0;
int l = 0, h = nums.length - 1;
while (l < h) {
int m = l + (h - l) / 2;
if (nums[l] == nums[m] && nums[m] == nums[h])
return minNumber(nums, l, h);
else if (nums[m] <= nums[h])
h = m;
else
l = m + 1;
}
return nums[l];
}
private int minNumber(int[] nums, int l, int h) {
for (int i = l; i < h; i++)
if (nums[i] > nums[i + 1])
return nums[i + 1];
return nums[l];
}
```
<div align="center"><img width="320px" src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/githubio/公众号二维码-2.png"></img></div>

View File

@ -1,71 +0,0 @@
# 12. 矩阵中的路径
[NowCoder](https://www.nowcoder.com/practice/c61c6999eecb4b8f88a98f66b273a3cc?tpId=13&tqId=11218&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking&from=cyc_github)
## 题目描述
判断在一个矩阵中是否存在一条包含某字符串所有字符的路径路径可以从矩阵中的任意一个格子开始每一步可以在矩阵中向上下左右移动一个格子如果一条路径经过了矩阵中的某一个格子则该路径不能再进入该格子
例如下面的矩阵包含了一条 bfce 路径
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/1db1c7ea-0443-478b-8df9-7e33b1336cc4.png" width="200px"> </div><br>
## 解题思路
使用回溯法backtracking进行求解它是一种暴力搜索方法通过搜索所有可能的结果来求解问题回溯法在一次搜索结束时需要进行回溯回退将这一次搜索过程中设置的状态进行清除从而开始一次新的搜索过程例如下图示例中 f 开始下一步有 4 种搜索可能如果先搜索 b需要将 b 标记为已经使用防止重复使用在这一次搜索结束之后需要将 b 的已经使用状态清除并搜索 c
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/dc964b86-7a08-4bde-a3d9-e6ddceb29f98.png" width="200px"> </div><br>
本题的输入是数组而不是矩阵二维数组因此需要先将数组转换成矩阵
```java
private final static int[][] next = {{0, -1}, {0, 1}, {-1, 0}, {1, 0}};
private int rows;
private int cols;
public boolean hasPath(char[] array, int rows, int cols, char[] str) {
if (rows == 0 || cols == 0) return false;
this.rows = rows;
this.cols = cols;
boolean[][] marked = new boolean[rows][cols];
char[][] matrix = buildMatrix(array);
for (int i = 0; i < rows; i++)
for (int j = 0; j < cols; j++)
if (backtracking(matrix, str, marked, 0, i, j))
return true;
return false;
}
private boolean backtracking(char[][] matrix, char[] str,
boolean[][] marked, int pathLen, int r, int c) {
if (pathLen == str.length) return true;
if (r < 0 || r >= rows || c < 0 || c >= cols
|| matrix[r][c] != str[pathLen] || marked[r][c]) {
return false;
}
marked[r][c] = true;
for (int[] n : next)
if (backtracking(matrix, str, marked, pathLen + 1, r + n[0], c + n[1]))
return true;
marked[r][c] = false;
return false;
}
private char[][] buildMatrix(char[] array) {
char[][] matrix = new char[rows][cols];
for (int r = 0, idx = 0; r < rows; r++)
for (int c = 0; c < cols; c++)
matrix[r][c] = array[idx++];
return matrix;
}
```
<div align="center"><img width="320px" src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/githubio/公众号二维码-2.png"></img></div>

View File

@ -1,65 +0,0 @@
# 13. 机器人的运动范围
[NowCoder](https://www.nowcoder.com/practice/6e5207314b5241fb83f2329e89fdecc8?tpId=13&tqId=11219&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking&from=cyc_github)
## 题目描述
地上有一个 m 行和 n 列的方格一个机器人从坐标 (0, 0) 的格子开始移动每一次只能向左右上下四个方向移动一格但是不能进入行坐标和列坐标的数位之和大于 k 的格子
例如 k 18 机器人能够进入方格 (35,37)因为 3+5+3+7=18但是它不能进入方格 (35,38)因为 3+5+3+8=19请问该机器人能够达到多少个格子
## 解题思路
使用深度优先搜索Depth First SearchDFS方法进行求解回溯是深度优先搜索的一种特例它在一次搜索过程中需要设置一些本次搜索过程的局部状态并在本次搜索结束之后清除状态而普通的深度优先搜索并不需要使用这些局部状态虽然还是有可能设置一些全局状态
```java
private static final int[][] next = {{0, -1}, {0, 1}, {-1, 0}, {1, 0}};
private int cnt = 0;
private int rows;
private int cols;
private int threshold;
private int[][] digitSum;
public int movingCount(int threshold, int rows, int cols) {
this.rows = rows;
this.cols = cols;
this.threshold = threshold;
initDigitSum();
boolean[][] marked = new boolean[rows][cols];
dfs(marked, 0, 0);
return cnt;
}
private void dfs(boolean[][] marked, int r, int c) {
if (r < 0 || r >= rows || c < 0 || c >= cols || marked[r][c])
return;
marked[r][c] = true;
if (this.digitSum[r][c] > this.threshold)
return;
cnt++;
for (int[] n : next)
dfs(marked, r + n[0], c + n[1]);
}
private void initDigitSum() {
int[] digitSumOne = new int[Math.max(rows, cols)];
for (int i = 0; i < digitSumOne.length; i++) {
int n = i;
while (n > 0) {
digitSumOne[i] += n % 10;
n /= 10;
}
}
this.digitSum = new int[rows][cols];
for (int i = 0; i < this.rows; i++)
for (int j = 0; j < this.cols; j++)
this.digitSum[i][j] = digitSumOne[i] + digitSumOne[j];
}
```
<div align="center"><img width="320px" src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/githubio/公众号二维码-2.png"></img></div>

View File

@ -1,59 +0,0 @@
# 14. 剪绳子
[Leetcode](https://leetcode.com/problems/integer-break/description/)
## 题目描述
把一根绳子剪成多段并且使得每段的长度乘积最大
```html
n = 2
return 1 (2 = 1 + 1)
n = 10
return 36 (10 = 3 + 3 + 4)
```
## 解题思路
### 贪心
尽可能多剪长度为 3 的绳子并且不允许有长度为 1 的绳子出现如果出现了就从已经切好长度为 3 的绳子中拿出一段与长度为 1 的绳子重新组合把它们切成两段长度为 2 的绳子
证明 n >= 5 3(n - 3) - n = 2n - 9 > 0 2(n - 2) - n = n - 4 > 0因此在 n >= 5 的情况下将绳子剪成一段为 2 或者 3得到的乘积会更大又因为 3(n - 3) - 2(n - 2) = n - 5 >= 0所以剪成一段长度为 3 比长度为 2 得到的乘积更大
```java
public int integerBreak(int n) {
if (n < 2)
return 0;
if (n == 2)
return 1;
if (n == 3)
return 2;
int timesOf3 = n / 3;
if (n - timesOf3 * 3 == 1)
timesOf3--;
int timesOf2 = (n - timesOf3 * 3) / 2;
return (int) (Math.pow(3, timesOf3)) * (int) (Math.pow(2, timesOf2));
}
```
### 动态规划
```java
public int integerBreak(int n) {
int[] dp = new int[n + 1];
dp[1] = 1;
for (int i = 2; i <= n; i++)
for (int j = 1; j < i; j++)
dp[i] = Math.max(dp[i], Math.max(j * (i - j), dp[j] * (i - j)));
return dp[n];
}
```
<div align="center"><img width="320px" src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/githubio/公众号二维码-2.png"></img></div>

View File

@ -1,47 +0,0 @@
# 15. 二进制中 1 的个数
[NowCoder](https://www.nowcoder.com/practice/8ee967e43c2c4ec193b040ea7fbb10b8?tpId=13&tqId=11164&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking&from=cyc_github)
## 题目描述
输入一个整数输出该数二进制表示中 1 的个数
### n&(n-1)
该位运算去除 n 的位级表示中最低的那一位
```
n : 10110100
n-1 : 10110011
n&(n-1) : 10110000
```
时间复杂度O(M)其中 M 表示 1 的个数
```java
public int NumberOf1(int n) {
int cnt = 0;
while (n != 0) {
cnt++;
n &= (n - 1);
}
return cnt;
}
```
### Integer.bitCount()
```java
public int NumberOf1(int n) {
return Integer.bitCount(n);
}
```
<div align="center"><img width="320px" src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/githubio/公众号二维码-2.png"></img></div>

View File

@ -1,43 +0,0 @@
# 16. 数值的整数次方
[NowCoder](https://www.nowcoder.com/practice/1a834e5e3e1a4b7ba251417554e07c00?tpId=13&tqId=11165&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking&from=cyc_github)
## 题目描述
给定一个 double 类型的浮点数 base int 类型的整数 exponent base exponent 次方
## 解题思路
下面的讨论中 x 代表 basen 代表 exponent
<!--<div align="center"><img src="https://latex.codecogs.com/gif.latex?x^n=\left\{\begin{array}{rcl}(x*x)^{n/2}&&{n\%2=0}\\x*(x*x)^{n/2}&&{n\%2=1}\end{array}\right." class="mathjax-pic"/></div> <br>-->
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/48b1d459-8832-4e92-938a-728aae730739.jpg" width="330px"> </div><br>
因为 (x\*x)<sup>n/2</sup> 可以通过递归求解并且每次递归 n 都减小一半因此整个算法的时间复杂度为 O(logN)
```java
public double Power(double base, int exponent) {
if (exponent == 0)
return 1;
if (exponent == 1)
return base;
boolean isNegative = false;
if (exponent < 0) {
exponent = -exponent;
isNegative = true;
}
double pow = Power(base * base, exponent / 2);
if (exponent % 2 != 0)
pow = pow * base;
return isNegative ? 1 / pow : pow;
}
```
<div align="center"><img width="320px" src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/githubio/公众号二维码-2.png"></img></div>

View File

@ -1,47 +0,0 @@
# 17. 打印从 1 到最大的 n 位数
## 题目描述
输入数字 n按顺序打印出从 1 到最大的 n 位十进制数比如输入 3则打印出 123 一直到最大的 3 位数即 999
## 解题思路
由于 n 可能会非常大因此不能直接用 int 表示数字而是用 char 数组进行存储
使用回溯法得到所有的数
```java
public void print1ToMaxOfNDigits(int n) {
if (n <= 0)
return;
char[] number = new char[n];
print1ToMaxOfNDigits(number, 0);
}
private void print1ToMaxOfNDigits(char[] number, int digit) {
if (digit == number.length) {
printNumber(number);
return;
}
for (int i = 0; i < 10; i++) {
number[digit] = (char) (i + '0');
print1ToMaxOfNDigits(number, digit + 1);
}
}
private void printNumber(char[] number) {
int index = 0;
while (index < number.length && number[index] == '0')
index++;
while (index < number.length)
System.out.print(number[index++]);
System.out.println();
}
```
<div align="center"><img width="320px" src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/githubio/公众号二维码-2.png"></img></div>

View File

@ -1,44 +0,0 @@
# 18.1 O(1) 时间内删除链表节点
## 解题思路
如果该节点不是尾节点那么可以直接将下一个节点的值赋给该节点然后令该节点指向下下个节点再删除下一个节点时间复杂度为 O(1)
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/1176f9e1-3442-4808-a47a-76fbaea1b806.png" width="600"/> </div><br>
否则就需要先遍历链表找到节点的前一个节点然后让前一个节点指向 null时间复杂度为 O(N)
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/4bf8d0ba-36f0-459e-83a0-f15278a5a157.png" width="600"/> </div><br>
综上如果进行 N 次操作那么大约需要操作节点的次数为 N-1+N=2N-1其中 N-1 表示 N-1 个不是尾节点的每个节点以 O(1) 的时间复杂度操作节点的总次数N 表示 1 个尾节点以 O(N) 的时间复杂度操作节点的总次数(2N-1)/N \~ 2因此该算法的平均时间复杂度为 O(1)
```java
public ListNode deleteNode(ListNode head, ListNode tobeDelete) {
if (head == null || tobeDelete == null)
return null;
if (tobeDelete.next != null) {
// 要删除的节点不是尾节点
ListNode next = tobeDelete.next;
tobeDelete.val = next.val;
tobeDelete.next = next.next;
} else {
if (head == tobeDelete)
// 只有一个节点
head = null;
else {
ListNode cur = head;
while (cur.next != tobeDelete)
cur = cur.next;
cur.next = null;
}
}
return head;
}
```
<div align="center"><img width="320px" src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/githubio/公众号二维码-2.png"></img></div>

View File

@ -1,32 +0,0 @@
# 18.2 删除链表中重复的结点
[NowCoder](https://www.nowcoder.com/practice/fc533c45b73a41b0b44ccba763f866ef?tpId=13&tqId=11209&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking&from=cyc_github)
## 题目描述
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/17e301df-52e8-4886-b593-841a16d13e44.png" width="450"/> </div><br>
## 解题描述
```java
public ListNode deleteDuplication(ListNode pHead) {
if (pHead == null || pHead.next == null)
return pHead;
ListNode next = pHead.next;
if (pHead.val == next.val) {
while (next != null && pHead.val == next.val)
next = next.next;
return deleteDuplication(next);
} else {
pHead.next = deleteDuplication(pHead.next);
return pHead;
}
}
```
<div align="center"><img width="320px" src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/githubio/公众号二维码-2.png"></img></div>

View File

@ -1,47 +0,0 @@
# 19. 正则表达式匹配
[NowCoder](https://www.nowcoder.com/practice/45327ae22b7b413ea21df13ee7d6429c?tpId=13&tqId=11205&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking&from=cyc_github)
## 题目描述
请实现一个函数用来匹配包括 '.' '\*' 的正则表达式模式中的字符 '.' 表示任意一个字符 '\*' 表示它前面的字符可以出现任意次包含 0
在本题中匹配是指字符串的所有字符匹配整个模式例如字符串 "aaa" 与模式 "a.a" "ab\*ac\*a" 匹配但是与 "aa.a" "ab\*a" 均不匹配
## 解题思路
应该注意到'.' 是用来当做一个任意字符 '\*' 是用来重复前面的字符这两个的作用不同不能把 '.' 的作用和 '\*' 进行类比从而把它当成重复前面字符一次
```java
public boolean match(char[] str, char[] pattern) {
int m = str.length, n = pattern.length;
boolean[][] dp = new boolean[m + 1][n + 1];
dp[0][0] = true;
for (int i = 1; i <= n; i++)
if (pattern[i - 1] == '*')
dp[0][i] = dp[0][i - 2];
for (int i = 1; i <= m; i++)
for (int j = 1; j <= n; j++)
if (str[i - 1] == pattern[j - 1] || pattern[j - 1] == '.')
dp[i][j] = dp[i - 1][j - 1];
else if (pattern[j - 1] == '*')
if (pattern[j - 2] == str[i - 1] || pattern[j - 2] == '.') {
dp[i][j] |= dp[i][j - 1]; // a* counts as single a
dp[i][j] |= dp[i - 1][j]; // a* counts as multiple a
dp[i][j] |= dp[i][j - 2]; // a* counts as empty
} else
dp[i][j] = dp[i][j - 2]; // a* only counts as empty
return dp[m][n];
}
```
<div align="center"><img width="320px" src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/githubio/公众号二维码-2.png"></img></div>

View File

@ -1,56 +0,0 @@
# 20. 表示数值的字符串
[NowCoder](https://www.nowcoder.com/practice/6f8c901d091949a5837e24bb82a731f2?tpId=13&tqId=11206&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking&from=cyc_github)
## 题目描述
```
true
"+100"
"5e2"
"-123"
"3.1416"
"-1E-16"
```
```
false
"12e"
"1a3.14"
"1.2.3"
"+-5"
"12e+4.3"
```
## 解题思路
使用正则表达式进行匹配
```html
[] 字符集合
() 分组
? 重复 0 ~ 1
+ 重复 1 ~ n
* 重复 0 ~ n
. 任意字符
\\. 转义后的 .
\\d 数字
```
```java
public boolean isNumeric(char[] str) {
if (str == null || str.length == 0)
return false;
return new String(str).matches("[+-]?\\d*(\\.\\d+)?([eE][+-]?\\d+)?");
}
```
<div align="center"><img width="320px" src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/githubio/公众号二维码-2.png"></img></div>

View File

@ -1,67 +0,0 @@
# 21. 调整数组顺序使奇数位于偶数前面
[NowCoder](https://www.nowcoder.com/practice/beb5aa231adc45b2a5dcc5b62c93f593?tpId=13&tqId=11166&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking&from=cyc_github)
## 题目描述
需要保证奇数和奇数偶数和偶数之间的相对位置不变这和书本不太一样
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/d03a2efa-ef19-4c96-97e8-ff61df8061d3.png" width="200px"> </div><br>
## 解题思路
方法一创建一个新数组时间复杂度 O(N)空间复杂度 O(N)
```java
public void reOrderArray(int[] nums) {
// 奇数个数
int oddCnt = 0;
for (int x : nums)
if (!isEven(x))
oddCnt++;
int[] copy = nums.clone();
int i = 0, j = oddCnt;
for (int num : copy) {
if (num % 2 == 1)
nums[i++] = num;
else
nums[j++] = num;
}
}
private boolean isEven(int x) {
return x % 2 == 0;
}
```
方法二使用冒泡思想每次都将当前偶数上浮到当前最右边时间复杂度 O(N<sup>2</sup>)空间复杂度 O(1)时间换空间
```java
public void reOrderArray(int[] nums) {
int N = nums.length;
for (int i = N - 1; i > 0; i--) {
for (int j = 0; j < i; j++) {
if (isEven(nums[j]) && !isEven(nums[j + 1])) {
swap(nums, j, j + 1);
}
}
}
}
private boolean isEven(int x) {
return x % 2 == 0;
}
private void swap(int[] nums, int i, int j) {
int t = nums[i];
nums[i] = nums[j];
nums[j] = t;
}
```
<div align="center"><img width="320px" src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/githubio/公众号二维码-2.png"></img></div>

View File

@ -1,34 +0,0 @@
# 22. 链表中倒数第 K 个结点
[NowCoder](https://www.nowcoder.com/practice/529d3ae5a407492994ad2a246518148a?tpId=13&tqId=11167&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking&from=cyc_github)
## 解题思路
设链表的长度为 N设置两个指针 P1 P2先让 P1 移动 K 个节点则还有 N - K 个节点可以移动此时让 P1 P2 同时移动可以知道当 P1 移动到链表结尾时P2 移动到第 N - K 个节点处该位置就是倒数第 K 个节点
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/6b504f1f-bf76-4aab-a146-a9c7a58c2029.png" width="500"/> </div><br>
```java
public ListNode FindKthToTail(ListNode head, int k) {
if (head == null)
return null;
ListNode P1 = head;
while (P1 != null && k-- > 0)
P1 = P1.next;
if (k > 0)
return null;
ListNode P2 = head;
while (P1 != null) {
P1 = P1.next;
P2 = P2.next;
}
return P2;
}
```
<div align="center"><img width="320px" src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/githubio/公众号二维码-2.png"></img></div>

View File

@ -1,50 +0,0 @@
# 23. 链表中环的入口结点
[NowCoder](https://www.nowcoder.com/practice/253d2c59ec3e4bc68da16833f79a38e4?tpId=13&tqId=11208&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking&from=cyc_github)
## 题目描述
一个链表中包含环请找出该链表的环的入口结点要求不能使用额外的空间
## 解题思路
使用双指针一个快指针 fast 每次移动两个节点一个慢指针 slow 每次移动一个节点因为存在环所以两个指针必定相遇在环中的某个节点上
假设环入口节点为 y1相遇所在节点为 z1
假设快指针 fast 在圈内绕了 N 则总路径长度为 x+Ny+(N-1)zz (N-1) 倍是因为快慢指针最后已经在 z1 节点相遇了后面就不需要再走了
而慢指针 slow 总路径长度为 x+y
因为快指针是慢指针的两倍因此 x+Ny+(N-1)z = 2(x+y)
我们要找的是环入口节点 y1也可以看成寻找长度 x 的值因此我们先将上面的等值分解为和 x 有关x=(N-2)y+(N-1)z
上面的等值没有很强的规律但是我们可以发现 y+z 就是圆环的总长度因此我们将上面的等式再分解x=(N-2)(y+z)+z这个等式左边是从起点x1 到环入口节点 y1 的长度而右边是在圆环中走过 (N-2) 再从相遇点 z1 再走过长度为 z 的长度此时我们可以发现如果让两个指针同时从起点 x1 和相遇点 z1 开始每次只走过一个距离那么最后他们会在环入口节点相遇
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/bb7fc182-98c2-4860-8ea3-630e27a5f29f.png" width="500"/> </div><br>
```java
public ListNode EntryNodeOfLoop(ListNode pHead) {
if (pHead == null || pHead.next == null)
return null;
ListNode slow = pHead, fast = pHead;
do {
fast = fast.next.next;
slow = slow.next;
} while (slow != fast);
fast = pHead;
while (slow != fast) {
slow = slow.next;
fast = fast.next;
}
return slow;
}
```
<div align="center"><img width="320px" src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/githubio/公众号二维码-2.png"></img></div>

View File

@ -1,43 +0,0 @@
# 24. 反转链表
[NowCoder](https://www.nowcoder.com/practice/75e878df47f24fdc9dc3e400ec6058ca?tpId=13&tqId=11168&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking&from=cyc_github)
## 解题思路
### 递归
```java
public ListNode ReverseList(ListNode head) {
if (head == null || head.next == null)
return head;
ListNode next = head.next;
head.next = null;
ListNode newHead = ReverseList(next);
next.next = head;
return newHead;
}
```
### 迭代
使用头插法
```java
public ListNode ReverseList(ListNode head) {
ListNode newList = new ListNode(-1);
while (head != null) {
ListNode next = head.next;
head.next = newList.next;
newList.next = head;
head = next;
}
return newList.next;
}
```
<div align="center"><img width="320px" src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/githubio/公众号二维码-2.png"></img></div>

View File

@ -1,58 +0,0 @@
# 25. 合并两个排序的链表
[NowCoder](https://www.nowcoder.com/practice/d8b6b4358f774294a89de2a6ac4d9337?tpId=13&tqId=11169&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking&from=cyc_github)
## 题目描述
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/c094d2bc-ec75-444b-af77-d369dfb6b3b4.png" width="400"/> </div><br>
## 解题思路
### 递归
```java
public ListNode Merge(ListNode list1, ListNode list2) {
if (list1 == null)
return list2;
if (list2 == null)
return list1;
if (list1.val <= list2.val) {
list1.next = Merge(list1.next, list2);
return list1;
} else {
list2.next = Merge(list1, list2.next);
return list2;
}
}
```
### 迭代
```java
public ListNode Merge(ListNode list1, ListNode list2) {
ListNode head = new ListNode(-1);
ListNode cur = head;
while (list1 != null && list2 != null) {
if (list1.val <= list2.val) {
cur.next = list1;
list1 = list1.next;
} else {
cur.next = list2;
list2 = list2.next;
}
cur = cur.next;
}
if (list1 != null)
cur.next = list1;
if (list2 != null)
cur.next = list2;
return head.next;
}
```
<div align="center"><img width="320px" src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/githubio/公众号二维码-2.png"></img></div>

View File

@ -1,34 +0,0 @@
# 26. 树的子结构
[NowCoder](https://www.nowcoder.com/practice/6e196c44c7004d15b1610b9afca8bd88?tpId=13&tqId=11170&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking&from=cyc_github)
## 题目描述
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/84a5b15a-86c5-4d8e-9439-d9fd5a4699a1.jpg" width="450"/> </div><br>
## 解题思路
```java
public boolean HasSubtree(TreeNode root1, TreeNode root2) {
if (root1 == null || root2 == null)
return false;
return isSubtreeWithRoot(root1, root2) || HasSubtree(root1.left, root2) || HasSubtree(root1.right, root2);
}
private boolean isSubtreeWithRoot(TreeNode root1, TreeNode root2) {
if (root2 == null)
return true;
if (root1 == null)
return false;
if (root1.val != root2.val)
return false;
return isSubtreeWithRoot(root1.left, root2.left) && isSubtreeWithRoot(root1.right, root2.right);
}
```
<div align="center"><img width="320px" src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/githubio/公众号二维码-2.png"></img></div>

View File

@ -1,32 +0,0 @@
# 27. 二叉树的镜像
[NowCoder](https://www.nowcoder.com/practice/564f4c26aa584921bc75623e48ca3011?tpId=13&tqId=11171&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking&from=cyc_github)
## 题目描述
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/0c12221f-729e-4c22-b0ba-0dfc909f8adf.jpg" width="300"/> </div><br>
## 解题思路
```java
public void Mirror(TreeNode root) {
if (root == null)
return;
swap(root);
Mirror(root.left);
Mirror(root.right);
}
private void swap(TreeNode root) {
TreeNode t = root.left;
root.left = root.right;
root.right = t;
}
```
<div align="center"><img width="320px" src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/githubio/公众号二维码-2.png"></img></div>

View File

@ -1,34 +0,0 @@
# 28. 对称的二叉树
[NowCoder](https://www.nowcoder.com/practice/ff05d44dfdb04e1d83bdbdab320efbcb?tpId=13&tqId=11211&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking&from=cyc_github)
## 题目描述
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/0c12221f-729e-4c22-b0ba-0dfc909f8adf.jpg" width="300"/> </div><br>
## 解题思路
```java
boolean isSymmetrical(TreeNode pRoot) {
if (pRoot == null)
return true;
return isSymmetrical(pRoot.left, pRoot.right);
}
boolean isSymmetrical(TreeNode t1, TreeNode t2) {
if (t1 == null && t2 == null)
return true;
if (t1 == null || t2 == null)
return false;
if (t1.val != t2.val)
return false;
return isSymmetrical(t1.left, t2.right) && isSymmetrical(t1.right, t2.left);
}
```
<div align="center"><img width="320px" src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/githubio/公众号二维码-2.png"></img></div>

View File

@ -1,39 +0,0 @@
# 29. 顺时针打印矩阵
[NowCoder](https://www.nowcoder.com/practice/9b4c81a02cd34f76be2659fa0d54342a?tpId=13&tqId=11172&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking&from=cyc_github)
## 题目描述
下图的矩阵顺时针打印结果为1, 2, 3, 4, 8, 12, 16, 15, 14, 13, 9, 5, 6, 7, 11, 10
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/48517227-324c-4664-bd26-a2d2cffe2bfe.png" width="200px"> </div><br>
## 解题思路
```java
public ArrayList<Integer> printMatrix(int[][] matrix) {
ArrayList<Integer> ret = new ArrayList<>();
int r1 = 0, r2 = matrix.length - 1, c1 = 0, c2 = matrix[0].length - 1;
while (r1 <= r2 && c1 <= c2) {
for (int i = c1; i <= c2; i++)
ret.add(matrix[r1][i]);
for (int i = r1 + 1; i <= r2; i++)
ret.add(matrix[i][c2]);
if (r1 != r2)
for (int i = c2 - 1; i >= c1; i--)
ret.add(matrix[r2][i]);
if (c1 != c2)
for (int i = r2 - 1; i > r1; i--)
ret.add(matrix[i][c1]);
r1++; r2--; c1++; c2--;
}
return ret;
}
```
<div align="center"><img width="320px" src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/githubio/公众号二维码-2.png"></img></div>

View File

@ -1,58 +0,0 @@
# 3. 数组中重复的数字
## 题目链接
[牛客网](https://www.nowcoder.com/practice/623a5ac0ea5b4e5f95552655361ae0a8?tpId=13&tqId=11203&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking&from=cyc_github)
## 题目描述
在一个长度为 n 的数组里的所有数字都在 0 n-1 的范围内数组中某些数字是重复的但不知道有几个数字是重复的也不知道每个数字重复几次请找出数组中任意一个重复的数字
```html
Input:
{2, 3, 1, 0, 2, 5}
Output:
2
```
## 解题思路
要求时间复杂度 O(N)空间复杂度 O(1)因此不能使用排序的方法也不能使用额外的标记数组
对于这种数组元素在 [0, n-1] 范围内的问题可以将值为 i 的元素调整到第 i 个位置上进行求解本题要求找出重复的数字因此在调整过程中如果第 i 位置上已经有一个值为 i 的元素就可以知道 i 值重复
(2, 3, 1, 0, 2, 5) 为例遍历到位置 4 该位置上的数为 2但是第 2 个位置上已经有一个 2 的值了因此可以知道 2 重复
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/643b6f18-f933-4ac5-aa7a-e304dbd7fe49.gif" width="350px"> </div><br>
```java
public boolean duplicate(int[] nums, int length, int[] duplication) {
if (nums == null || length <= 0)
return false;
for (int i = 0; i < length; i++) {
while (nums[i] != i) {
if (nums[i] == nums[nums[i]]) {
duplication[0] = nums[i];
return true;
}
swap(nums, i, nums[i]);
}
}
return false;
}
private void swap(int[] nums, int i, int j) {
int t = nums[i];
nums[i] = nums[j];
nums[j] = t;
}
```
<div align="center"><img width="320px" src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/githubio/公众号二维码-2.png"></img></div>

View File

@ -1,39 +0,0 @@
# 30. 包含 min 函数的栈
[NowCoder](https://www.nowcoder.com/practice/4c776177d2c04c2494f2555c9fcc1e49?tpId=13&tqId=11173&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking&from=cyc_github)
## 题目描述
定义栈的数据结构请在该类型中实现一个能够得到栈最小元素的 min 函数
## 解题思路
```java
private Stack<Integer> dataStack = new Stack<>();
private Stack<Integer> minStack = new Stack<>();
public void push(int node) {
dataStack.push(node);
minStack.push(minStack.isEmpty() ? node : Math.min(minStack.peek(), node));
}
public void pop() {
dataStack.pop();
minStack.pop();
}
public int top() {
return dataStack.peek();
}
public int min() {
return minStack.peek();
}
```
<div align="center"><img width="320px" src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/githubio/公众号二维码-2.png"></img></div>

View File

@ -1,36 +0,0 @@
# 31. 栈的压入弹出序列
[NowCoder](https://www.nowcoder.com/practice/d77d11405cc7470d82554cb392585106?tpId=13&tqId=11174&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking&from=cyc_github)
## 题目描述
输入两个整数序列第一个序列表示栈的压入顺序请判断第二个序列是否为该栈的弹出顺序假设压入栈的所有数字均不相等
例如序列 1,2,3,4,5 是某栈的压入顺序序列 4,5,3,2,1 是该压栈序列对应的一个弹出序列 4,3,5,1,2 就不可能是该压栈序列的弹出序列
## 解题思路
使用一个栈来模拟压入弹出操作
```java
public boolean IsPopOrder(int[] pushSequence, int[] popSequence) {
int n = pushSequence.length;
Stack<Integer> stack = new Stack<>();
for (int pushIndex = 0, popIndex = 0; pushIndex < n; pushIndex++) {
stack.push(pushSequence[pushIndex]);
while (popIndex < n && !stack.isEmpty()
&& stack.peek() == popSequence[popIndex]) {
stack.pop();
popIndex++;
}
}
return stack.isEmpty();
}
```
<div align="center"><img width="320px" src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/githubio/公众号二维码-2.png"></img></div>

View File

@ -1,44 +0,0 @@
# 32.1 从上往下打印二叉树
[NowCoder](https://www.nowcoder.com/practice/7fe2212963db4790b57431d9ed259701?tpId=13&tqId=11175&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking&from=cyc_github)
## 题目描述
从上往下打印出二叉树的每个节点同层节点从左至右打印
例如以下二叉树层次遍历的结果为1,2,3,4,5,6,7
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/d5e838cf-d8a2-49af-90df-1b2a714ee676.jpg" width="250"/> </div><br>
## 解题思路
使用队列来进行层次遍历
不需要使用两个队列分别存储当前层的节点和下一层的节点因为在开始遍历一层的节点时当前队列中的节点数就是当前层的节点数只要控制遍历这么多节点数就能保证这次遍历的都是当前层的节点
```java
public ArrayList<Integer> PrintFromTopToBottom(TreeNode root) {
Queue<TreeNode> queue = new LinkedList<>();
ArrayList<Integer> ret = new ArrayList<>();
queue.add(root);
while (!queue.isEmpty()) {
int cnt = queue.size();
while (cnt-- > 0) {
TreeNode t = queue.poll();
if (t == null)
continue;
ret.add(t.val);
queue.add(t.left);
queue.add(t.right);
}
}
return ret;
}
```
<div align="center"><img width="320px" src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/githubio/公众号二维码-2.png"></img></div>

View File

@ -1,39 +0,0 @@
# 32.2 把二叉树打印成多行
[NowCoder](https://www.nowcoder.com/practice/445c44d982d04483b04a54f298796288?tpId=13&tqId=11213&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking&from=cyc_github)
## 题目描述
和上题几乎一样
## 解题思路
```java
ArrayList<ArrayList<Integer>> Print(TreeNode pRoot) {
ArrayList<ArrayList<Integer>> ret = new ArrayList<>();
Queue<TreeNode> queue = new LinkedList<>();
queue.add(pRoot);
while (!queue.isEmpty()) {
ArrayList<Integer> list = new ArrayList<>();
int cnt = queue.size();
while (cnt-- > 0) {
TreeNode node = queue.poll();
if (node == null)
continue;
list.add(node.val);
queue.add(node.left);
queue.add(node.right);
}
if (list.size() != 0)
ret.add(list);
}
return ret;
}
```
<div align="center"><img width="320px" src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/githubio/公众号二维码-2.png"></img></div>

View File

@ -1,43 +0,0 @@
# 32.3 按之字形顺序打印二叉树
[NowCoder](https://www.nowcoder.com/practice/91b69814117f4e8097390d107d2efbe0?tpId=13&tqId=11212&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking&from=cyc_github)
## 题目描述
请实现一个函数按照之字形打印二叉树即第一行按照从左到右的顺序打印第二层按照从右至左的顺序打印第三行按照从左到右的顺序打印其他行以此类推
## 解题思路
```java
public ArrayList<ArrayList<Integer>> Print(TreeNode pRoot) {
ArrayList<ArrayList<Integer>> ret = new ArrayList<>();
Queue<TreeNode> queue = new LinkedList<>();
queue.add(pRoot);
boolean reverse = false;
while (!queue.isEmpty()) {
ArrayList<Integer> list = new ArrayList<>();
int cnt = queue.size();
while (cnt-- > 0) {
TreeNode node = queue.poll();
if (node == null)
continue;
list.add(node.val);
queue.add(node.left);
queue.add(node.right);
}
if (reverse)
Collections.reverse(list);
reverse = !reverse;
if (list.size() != 0)
ret.add(list);
}
return ret;
}
```
<div align="center"><img width="320px" src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/githubio/公众号二维码-2.png"></img></div>

View File

@ -1,41 +0,0 @@
# 33. 二叉搜索树的后序遍历序列
[NowCoder](https://www.nowcoder.com/practice/a861533d45854474ac791d90e447bafd?tpId=13&tqId=11176&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking&from=cyc_github)
## 题目描述
输入一个整数数组判断该数组是不是某二叉搜索树的后序遍历的结果假设输入的数组的任意两个数字都互不相同
例如下图是后序遍历序列 1,3,2 所对应的二叉搜索树
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/13454fa1-23a8-4578-9663-2b13a6af564a.jpg" width="150"/> </div><br>
## 解题思路
```java
public boolean VerifySquenceOfBST(int[] sequence) {
if (sequence == null || sequence.length == 0)
return false;
return verify(sequence, 0, sequence.length - 1);
}
private boolean verify(int[] sequence, int first, int last) {
if (last - first <= 1)
return true;
int rootVal = sequence[last];
int cutIndex = first;
while (cutIndex < last && sequence[cutIndex] <= rootVal)
cutIndex++;
for (int i = cutIndex; i < last; i++)
if (sequence[i] < rootVal)
return false;
return verify(sequence, first, cutIndex - 1) && verify(sequence, cutIndex, last - 1);
}
```
<div align="center"><img width="320px" src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/githubio/公众号二维码-2.png"></img></div>

View File

@ -1,43 +0,0 @@
# 34. 二叉树中和为某一值的路径
[NowCoder](https://www.nowcoder.com/practice/b736e784e3e34731af99065031301bca?tpId=13&tqId=11177&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking&from=cyc_github)
## 题目描述
输入一颗二叉树和一个整数打印出二叉树中结点值的和为输入整数的所有路径路径定义为从树的根结点开始往下一直到叶结点所经过的结点形成一条路径
下图的二叉树有两条和为 22 的路径10, 5, 7 10, 12
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/ed77b0e6-38d9-4a34-844f-724f3ffa2c12.jpg" width="200"/> </div><br>
## 解题思路
```java
private ArrayList<ArrayList<Integer>> ret = new ArrayList<>();
public ArrayList<ArrayList<Integer>> FindPath(TreeNode root, int target) {
backtracking(root, target, new ArrayList<>());
return ret;
}
private void backtracking(TreeNode node, int target, ArrayList<Integer> path) {
if (node == null)
return;
path.add(node.val);
target -= node.val;
if (target == 0 && node.left == null && node.right == null) {
ret.add(new ArrayList<>(path));
} else {
backtracking(node.left, target, path);
backtracking(node.right, target, path);
}
path.remove(path.size() - 1);
}
```
<div align="center"><img width="320px" src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/githubio/公众号二维码-2.png"></img></div>

View File

@ -1,74 +0,0 @@
# 35. 复杂链表的复制
[NowCoder](https://www.nowcoder.com/practice/f836b2c43afc4b35ad6adc41ec941dba?tpId=13&tqId=11178&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking&from=cyc_github)
## 题目描述
输入一个复杂链表每个节点中有节点值以及两个指针一个指向下一个节点另一个特殊指针指向任意一个节点返回结果为复制后复杂链表的 head
```java
public class RandomListNode {
int label;
RandomListNode next = null;
RandomListNode random = null;
RandomListNode(int label) {
this.label = label;
}
}
```
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/66a01953-5303-43b1-8646-0c77b825e980.png" width="300"/> </div><br>
## 解题思路
第一步在每个节点的后面插入复制的节点
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/dfd5d3f8-673c-486b-8ecf-d2082107b67b.png" width="600"/> </div><br>
第二步对复制节点的 random 链接进行赋值
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/cafbfeb8-7dfe-4c0a-a3c9-750eeb824068.png" width="600"/> </div><br>
第三步拆分
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/e151b5df-5390-4365-b66e-b130cd253c12.png" width="600"/> </div><br>
```java
public RandomListNode Clone(RandomListNode pHead) {
if (pHead == null)
return null;
// 插入新节点
RandomListNode cur = pHead;
while (cur != null) {
RandomListNode clone = new RandomListNode(cur.label);
clone.next = cur.next;
cur.next = clone;
cur = clone.next;
}
// 建立 random 链接
cur = pHead;
while (cur != null) {
RandomListNode clone = cur.next;
if (cur.random != null)
clone.random = cur.random.next;
cur = clone.next;
}
// 拆分
cur = pHead;
RandomListNode pCloneHead = pHead.next;
while (cur.next != null) {
RandomListNode next = cur.next;
cur.next = next.next;
cur = next;
}
return pCloneHead;
}
```
<div align="center"><img width="320px" src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/githubio/公众号二维码-2.png"></img></div>

View File

@ -1,41 +0,0 @@
# 36. 二叉搜索树与双向链表
[NowCoder](https://www.nowcoder.com/practice/947f6eb80d944a84850b0538bf0ec3a5?tpId=13&tqId=11179&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking&from=cyc_github)
## 题目描述
输入一棵二叉搜索树将该二叉搜索树转换成一个排序的双向链表要求不能创建任何新的结点只能调整树中结点指针的指向
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/05a08f2e-9914-4a77-92ef-aebeaecf4f66.jpg" width="400"/> </div><br>
## 解题思路
```java
private TreeNode pre = null;
private TreeNode head = null;
public TreeNode Convert(TreeNode root) {
inOrder(root);
return head;
}
private void inOrder(TreeNode node) {
if (node == null)
return;
inOrder(node.left);
node.left = pre;
if (pre != null)
pre.right = node;
pre = node;
if (head == null)
head = node;
inOrder(node.right);
}
```
<div align="center"><img width="320px" src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/githubio/公众号二维码-2.png"></img></div>

View File

@ -1,46 +0,0 @@
# 37. 序列化二叉树
[NowCoder](https://www.nowcoder.com/practice/cf7e25aa97c04cc1a68c8f040e71fb84?tpId=13&tqId=11214&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking&from=cyc_github)
## 题目描述
请实现两个函数分别用来序列化和反序列化二叉树
## 解题思路
```java
private String deserializeStr;
public String Serialize(TreeNode root) {
if (root == null)
return "#";
return root.val + " " + Serialize(root.left) + " " + Serialize(root.right);
}
public TreeNode Deserialize(String str) {
deserializeStr = str;
return Deserialize();
}
private TreeNode Deserialize() {
if (deserializeStr.length() == 0)
return null;
int index = deserializeStr.indexOf(" ");
String node = index == -1 ? deserializeStr : deserializeStr.substring(0, index);
deserializeStr = index == -1 ? "" : deserializeStr.substring(index + 1);
if (node.equals("#"))
return null;
int val = Integer.valueOf(node);
TreeNode t = new TreeNode(val);
t.left = Deserialize();
t.right = Deserialize();
return t;
}
```
<div align="center"><img width="320px" src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/githubio/公众号二维码-2.png"></img></div>

View File

@ -1,47 +0,0 @@
# 38. 字符串的排列
[NowCoder](https://www.nowcoder.com/practice/fe6b651b66ae47d7acce78ffdd9a96c7?tpId=13&tqId=11180&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking&from=cyc_github)
## 题目描述
输入一个字符串按字典序打印出该字符串中字符的所有排列例如输入字符串 abc则打印出由字符 a, b, c 所能排列出来的所有字符串 abc, acb, bac, bca, cab cba
## 解题思路
```java
private ArrayList<String> ret = new ArrayList<>();
public ArrayList<String> Permutation(String str) {
if (str.length() == 0)
return ret;
char[] chars = str.toCharArray();
Arrays.sort(chars);
backtracking(chars, new boolean[chars.length], new StringBuilder());
return ret;
}
private void backtracking(char[] chars, boolean[] hasUsed, StringBuilder s) {
if (s.length() == chars.length) {
ret.add(s.toString());
return;
}
for (int i = 0; i < chars.length; i++) {
if (hasUsed[i])
continue;
if (i != 0 && chars[i] == chars[i - 1] && !hasUsed[i - 1]) /* 保证不重复 */
continue;
hasUsed[i] = true;
s.append(chars[i]);
backtracking(chars, hasUsed, s);
s.deleteCharAt(s.length() - 1);
hasUsed[i] = false;
}
}
```
<div align="center"><img width="320px" src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/githubio/公众号二维码-2.png"></img></div>

View File

@ -1,34 +0,0 @@
# 39. 数组中出现次数超过一半的数字
[NowCoder](https://www.nowcoder.com/practice/e8a1b01a2df14cb2b228b30ee6a92163?tpId=13&tqId=11181&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking&from=cyc_github)
## 解题思路
多数投票问题可以利用 Boyer-Moore Majority Vote Algorithm 来解决这个问题使得时间复杂度为 O(N)
使用 cnt 来统计一个元素出现的次数当遍历到的元素和统计元素相等时 cnt++否则令 cnt--如果前面查找了 i 个元素 cnt == 0说明前 i 个元素没有 majority或者有 majority但是出现的次数少于 i / 2 因为如果多于 i / 2 的话 cnt 就一定不会为 0 此时剩下的 n - i 个元素中majority 的数目依然多于 (n - i) / 2因此继续查找就能找出 majority
```java
public int MoreThanHalfNum_Solution(int[] nums) {
int majority = nums[0];
for (int i = 1, cnt = 1; i < nums.length; i++) {
cnt = nums[i] == majority ? cnt + 1 : cnt - 1;
if (cnt == 0) {
majority = nums[i];
cnt = 1;
}
}
int cnt = 0;
for (int val : nums)
if (val == majority)
cnt++;
return cnt > nums.length / 2 ? majority : 0;
}
```
<div align="center"><img width="320px" src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/githubio/公众号二维码-2.png"></img></div>

View File

@ -1,56 +0,0 @@
# 4. 二维数组中的查找
## 题目链接
[牛客网](https://www.nowcoder.com/practice/abc3fe2ce8e146608e868a70efebf62e?tpId=13&tqId=11154&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking&from=cyc_github)
## 题目描述
给定一个二维数组其每一行从左到右递增排序从上到下也是递增排序给定一个数判断这个数是否在该二维数组中
```html
Consider the following matrix:
[
[1, 4, 7, 11, 15],
[2, 5, 8, 12, 19],
[3, 6, 9, 16, 22],
[10, 13, 14, 17, 24],
[18, 21, 23, 26, 30]
]
Given target = 5, return true.
Given target = 20, return false.
```
## 解题思路
要求时间复杂度 O(M + N)空间复杂度 O(1)其中 M 为行数N 列数
该二维数组中的一个数小于它的数一定在其左边大于它的数一定在其下边因此从右上角开始查找就可以根据 target 和当前元素的大小关系来缩小查找区间当前元素的查找区间为左下角的所有元素
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/35a8c711-0dc0-4613-95f3-be96c6c6e104.gif" width="400px"> </div><br>
```java
public boolean Find(int target, int[][] matrix) {
if (matrix == null || matrix.length == 0 || matrix[0].length == 0)
return false;
int rows = matrix.length, cols = matrix[0].length;
int r = 0, c = cols - 1; // 从右上角开始
while (r <= rows - 1 && c >= 0) {
if (target == matrix[r][c])
return true;
else if (target > matrix[r][c])
r++;
else
c--;
}
return false;
}
```
<div align="center"><img width="320px" src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/githubio/公众号二维码-2.png"></img></div>

View File

@ -1,88 +0,0 @@
# 40. 最小的 K 个数
[NowCoder](https://www.nowcoder.com/practice/6a296eb82cf844ca8539b57c23e6e9bf?tpId=13&tqId=11182&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking&from=cyc_github)
## 解题思路
### 快速选择
- 复杂度O(N) + O(1)
- 只有当允许修改数组元素时才可以使用
快速排序的 partition() 方法会返回一个整数 j 使得 a[l..j-1] 小于等于 a[j] a[j+1..h] 大于等于 a[j]此时 a[j] 就是数组的第 j 大元素可以利用这个特性找出数组的第 K 个元素这种找第 K 个元素的算法称为快速选择算法
```java
public ArrayList<Integer> GetLeastNumbers_Solution(int[] nums, int k) {
ArrayList<Integer> ret = new ArrayList<>();
if (k > nums.length || k <= 0)
return ret;
findKthSmallest(nums, k - 1);
/* findKthSmallest 会改变数组,使得前 k 个数都是最小的 k 个数 */
for (int i = 0; i < k; i++)
ret.add(nums[i]);
return ret;
}
public void findKthSmallest(int[] nums, int k) {
int l = 0, h = nums.length - 1;
while (l < h) {
int j = partition(nums, l, h);
if (j == k)
break;
if (j > k)
h = j - 1;
else
l = j + 1;
}
}
private int partition(int[] nums, int l, int h) {
int p = nums[l]; /* 切分元素 */
int i = l, j = h + 1;
while (true) {
while (i != h && nums[++i] < p) ;
while (j != l && nums[--j] > p) ;
if (i >= j)
break;
swap(nums, i, j);
}
swap(nums, l, j);
return j;
}
private void swap(int[] nums, int i, int j) {
int t = nums[i];
nums[i] = nums[j];
nums[j] = t;
}
```
### 大小为 K 的最小堆
- 复杂度O(NlogK) + O(K)
- 特别适合处理海量数据
应该使用大顶堆来维护最小堆而不能直接创建一个小顶堆并设置一个大小企图让小顶堆中的元素都是最小元素
维护一个大小为 K 的最小堆过程如下在添加一个元素之后如果大顶堆的大小大于 K那么需要将大顶堆的堆顶元素去除
```java
public ArrayList<Integer> GetLeastNumbers_Solution(int[] nums, int k) {
if (k > nums.length || k <= 0)
return new ArrayList<>();
PriorityQueue<Integer> maxHeap = new PriorityQueue<>((o1, o2) -> o2 - o1);
for (int num : nums) {
maxHeap.add(num);
if (maxHeap.size() > k)
maxHeap.poll();
}
return new ArrayList<>(maxHeap);
}
```
<div align="center"><img width="320px" src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/githubio/公众号二维码-2.png"></img></div>

View File

@ -1,47 +0,0 @@
# 41.1 数据流中的中位数
[NowCoder](https://www.nowcoder.com/practice/9be0172896bd43948f8a32fb954e1be1?tpId=13&tqId=11216&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking&from=cyc_github)
## 题目描述
如何得到一个数据流中的中位数如果从数据流中读出奇数个数值那么中位数就是所有数值排序之后位于中间的数值如果从数据流中读出偶数个数值那么中位数就是所有数值排序之后中间两个数的平均值
## 解题思路
```java
/* 大顶堆,存储左半边元素 */
private PriorityQueue<Integer> left = new PriorityQueue<>((o1, o2) -> o2 - o1);
/* 小顶堆,存储右半边元素,并且右半边元素都大于左半边 */
private PriorityQueue<Integer> right = new PriorityQueue<>();
/* 当前数据流读入的元素个数 */
private int N = 0;
public void Insert(Integer val) {
/* 插入要保证两个堆存于平衡状态 */
if (N % 2 == 0) {
/* N 为偶数的情况下插入到右半边
* 因为右半边元素都要大于左半边但是新插入的元素不一定比左半边元素来的大
* 因此需要先将元素插入左半边然后利用左半边为大顶堆的特点取出堆顶元素即为最大元素此时插入右半边 */
left.add(val);
right.add(left.poll());
} else {
right.add(val);
left.add(right.poll());
}
N++;
}
public Double GetMedian() {
if (N % 2 == 0)
return (left.peek() + right.peek()) / 2.0;
else
return (double) right.peek();
}
```
<div align="center"><img width="320px" src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/githubio/公众号二维码-2.png"></img></div>

View File

@ -1,32 +0,0 @@
# 41.2 字符流中第一个不重复的字符
[NowCoder](https://www.nowcoder.com/practice/00de97733b8e4f97a3fb5c680ee10720?tpId=13&tqId=11207&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking&from=cyc_github)
## 题目描述
请实现一个函数用来找出字符流中第一个只出现一次的字符例如当从字符流中只读出前两个字符 "go" 第一个只出现一次的字符是 "g"当从该字符流中读出前六个字符google" 时,第一个只出现一次的字符是 "l"
## 解题思路
```java
private int[] cnts = new int[256];
private Queue<Character> queue = new LinkedList<>();
public void Insert(char ch) {
cnts[ch]++;
queue.add(ch);
while (!queue.isEmpty() && cnts[queue.peek()] > 1)
queue.poll();
}
public char FirstAppearingOnce() {
return queue.isEmpty() ? '#' : queue.peek();
}
```
<div align="center"><img width="320px" src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/githubio/公众号二维码-2.png"></img></div>

View File

@ -1,31 +0,0 @@
# 42. 连续子数组的最大和
[NowCoder](https://www.nowcoder.com/practice/459bd355da1549fa8a49e350bf3df484?tpId=13&tqId=11183&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking&from=cyc_github)
## 题目描述
{6, -3, -2, 7, -15, 1, 2, 2}连续子数组的最大和为 8从第 0 个开始到第 3 个为止
## 解题思路
```java
public int FindGreatestSumOfSubArray(int[] nums) {
if (nums == null || nums.length == 0)
return 0;
int greatestSum = Integer.MIN_VALUE;
int sum = 0;
for (int val : nums) {
sum = sum <= 0 ? val : sum + val;
greatestSum = Math.max(greatestSum, sum);
}
return greatestSum;
}
```
<div align="center"><img width="320px" src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/githubio/公众号二维码-2.png"></img></div>

View File

@ -1,25 +0,0 @@
# 43. 1 n 整数中 1 出现的次数
[NowCoder](https://www.nowcoder.com/practice/bd7f978302044eee894445e244c7eee6?tpId=13&tqId=11184&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking&from=cyc_github)
## 解题思路
```java
public int NumberOf1Between1AndN_Solution(int n) {
int cnt = 0;
for (int m = 1; m <= n; m *= 10) {
int a = n / m, b = n % m;
cnt += (a + 8) / 10 * m + (a % 10 == 1 ? b + 1 : 0);
}
return cnt;
}
```
> [Leetcode : 233. Number of Digit One](https://leetcode.com/problems/number-of-digit-one/discuss/64381/4+-lines-O(log-n)-C++JavaPython)
<div align="center"><img width="320px" src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/githubio/公众号二维码-2.png"></img></div>

View File

@ -1,61 +0,0 @@
# 44. 数字序列中的某一位数字
## 题目描述
数字以 0123456789101112131415... 的格式序列化到一个字符串中求这个字符串的第 index
## 解题思路
```java
public int getDigitAtIndex(int index) {
if (index < 0)
return -1;
int place = 1; // 1 表示个位2 表示 十位...
while (true) {
int amount = getAmountOfPlace(place);
int totalAmount = amount * place;
if (index < totalAmount)
return getDigitAtIndex(index, place);
index -= totalAmount;
place++;
}
}
/**
* place 位数的数字组成的字符串长度
* 10, 90, 900, ...
*/
private int getAmountOfPlace(int place) {
if (place == 1)
return 10;
return (int) Math.pow(10, place - 1) * 9;
}
/**
* place 位数的起始数字
* 0, 10, 100, ...
*/
private int getBeginNumberOfPlace(int place) {
if (place == 1)
return 0;
return (int) Math.pow(10, place - 1);
}
/**
* place 位数组成的字符串中 index 个数
*/
private int getDigitAtIndex(int index, int place) {
int beginNumber = getBeginNumberOfPlace(place);
int shiftNumber = index / place;
String number = (beginNumber + shiftNumber) + "";
int count = index % place;
return number.charAt(count) - '0';
}
```
<div align="center"><img width="320px" src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/githubio/公众号二维码-2.png"></img></div>

View File

@ -1,34 +0,0 @@
# 45. 把数组排成最小的数
[NowCoder](https://www.nowcoder.com/practice/8fecd3f8ba334add803bf2a06af1b993?tpId=13&tqId=11185&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking&from=cyc_github)
## 题目描述
输入一个正整数数组把数组里所有数字拼接起来排成一个数打印能拼接出的所有数字中最小的一个例如输入数组 {332321}则打印出这三个数字能排成的最小数字为 321323
## 解题思路
可以看成是一个排序问题在比较两个字符串 S1 S2 的大小时应该比较的是 S1+S2 S2+S1 的大小如果 S1+S2 < S2+S1那么应该把 S1 排在前面否则应该把 S2 排在前面
```java
public String PrintMinNumber(int[] numbers) {
if (numbers == null || numbers.length == 0)
return "";
int n = numbers.length;
String[] nums = new String[n];
for (int i = 0; i < n; i++)
nums[i] = numbers[i] + "";
Arrays.sort(nums, (s1, s2) -> (s1 + s2).compareTo(s2 + s1));
String ret = "";
for (String str : nums)
ret += str;
return ret;
}
```
<div align="center"><img width="320px" src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/githubio/公众号二维码-2.png"></img></div>

View File

@ -1,38 +0,0 @@
# 46. 把数字翻译成字符串
[Leetcode](https://leetcode.com/problems/decode-ways/description/)
## 题目描述
给定一个数字按照如下规则翻译成字符串1 翻译成a2 翻译成b... 26 翻译成z一个数字有多种翻译可能例如 12258 一共有 5 分别是 abbehlbehavehabyhlyh实现一个函数用来计算一个数字有多少种不同的翻译方法
## 解题思路
```java
public int numDecodings(String s) {
if (s == null || s.length() == 0)
return 0;
int n = s.length();
int[] dp = new int[n + 1];
dp[0] = 1;
dp[1] = s.charAt(0) == '0' ? 0 : 1;
for (int i = 2; i <= n; i++) {
int one = Integer.valueOf(s.substring(i - 1, i));
if (one != 0)
dp[i] += dp[i - 1];
if (s.charAt(i - 2) == '0')
continue;
int two = Integer.valueOf(s.substring(i - 2, i));
if (two <= 26)
dp[i] += dp[i - 2];
}
return dp[n];
}
```
<div align="center"><img width="320px" src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/githubio/公众号二维码-2.png"></img></div>

View File

@ -1,42 +0,0 @@
# 47. 礼物的最大价值
[NowCoder](https://www.nowcoder.com/questionTerminal/72a99e28381a407991f2c96d8cb238ab)
## 题目描述
在一个 m\*n 的棋盘的每一个格都放有一个礼物每个礼物都有一定价值大于 0从左上角开始拿礼物每次向右或向下移动一格直到右下角结束给定一个棋盘求拿到礼物的最大价值例如对于如下棋盘
```
1 10 3 8
12 2 9 6
5 7 4 11
3 7 16 5
```
礼物的最大价值为 1+12+5+7+7+16+5=53
## 解题思路
应该用动态规划求解而不是深度优先搜索深度优先搜索过于复杂不是最优解
```java
public int getMost(int[][] values) {
if (values == null || values.length == 0 || values[0].length == 0)
return 0;
int n = values[0].length;
int[] dp = new int[n];
for (int[] value : values) {
dp[0] += value[0];
for (int i = 1; i < n; i++)
dp[i] = Math.max(dp[i], dp[i - 1]) + value[i];
}
return dp[n - 1];
}
```
<div align="center"><img width="320px" src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/githubio/公众号二维码-2.png"></img></div>

View File

@ -1,36 +0,0 @@
# 48. 最长不含重复字符的子字符串
## 题目描述
输入一个字符串只包含 a\~z 的字符求其最长不含重复字符的子字符串的长度例如对于 arabcacfr最长不含重复字符的子字符串为 acfr长度为 4
## 解题思路
```java
public int longestSubStringWithoutDuplication(String str) {
int curLen = 0;
int maxLen = 0;
int[] preIndexs = new int[26];
Arrays.fill(preIndexs, -1);
for (int curI = 0; curI < str.length(); curI++) {
int c = str.charAt(curI) - 'a';
int preI = preIndexs[c];
if (preI == -1 || curI - preI > curLen) {
curLen++;
} else {
maxLen = Math.max(maxLen, curLen);
curLen = curI - preI;
}
preIndexs[c] = curI;
}
maxLen = Math.max(maxLen, curLen);
return maxLen;
}
```
<div align="center"><img width="320px" src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/githubio/公众号二维码-2.png"></img></div>

View File

@ -1,37 +0,0 @@
# 49. 丑数
[NowCoder](https://www.nowcoder.com/practice/6aa9e04fc3794f68acf8778237ba065b?tpId=13&tqId=11186&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking&from=cyc_github)
## 题目描述
把只包含因子 23 5 的数称作丑数Ugly Number例如 68 都是丑数 14 不是因为它包含因子 7习惯上我们把 1 当做是第一个丑数求按从小到大的顺序的第 N 个丑数
## 解题思路
```java
public int GetUglyNumber_Solution(int N) {
if (N <= 6)
return N;
int i2 = 0, i3 = 0, i5 = 0;
int[] dp = new int[N];
dp[0] = 1;
for (int i = 1; i < N; i++) {
int next2 = dp[i2] * 2, next3 = dp[i3] * 3, next5 = dp[i5] * 5;
dp[i] = Math.min(next2, Math.min(next3, next5));
if (dp[i] == next2)
i2++;
if (dp[i] == next3)
i3++;
if (dp[i] == next5)
i5++;
}
return dp[N - 1];
}
```
<div align="center"><img width="320px" src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/githubio/公众号二维码-2.png"></img></div>

View File

@ -1,59 +0,0 @@
# 5. 替换空格
## 题目链接
[牛客网](https://www.nowcoder.com/practice/4060ac7e3e404ad1a894ef3e17650423?tpId=13&tqId=11155&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking&from=cyc_github)
## 题目描述
将一个字符串中的空格替换成 "%20"
```text
Input:
"A B"
Output:
"A%20B"
```
## 解题思路
在字符串尾部填充任意字符使得字符串的长度等于替换之后的长度因为一个空格要替换成三个字符%20所以当遍历到一个空格时需要在尾部填充两个任意字符
P1 指向字符串原来的末尾位置P2 指向字符串现在的末尾位置P1 P2 从后向前遍历 P1 遍历到一个空格时就需要令 P2 指向的位置依次填充 02%注意是逆序的否则就填充上 P1 指向字符的值从后向前遍是为了在改变 P2 所指向的内容时不会影响到 P1 遍历原来字符串的内容
P2 遇到 P1 P2 <= P1或者遍历结束P1 < 0退出
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/f7c1fea2-c1e7-4d31-94b5-0d9df85e093c.gif" width="350px"> </div><br>
```java
public String replaceSpace(StringBuffer str) {
int P1 = str.length() - 1;
for (int i = 0; i <= P1; i++)
if (str.charAt(i) == ' ')
str.append(" ");
int P2 = str.length() - 1;
while (P1 >= 0 && P2 > P1) {
char c = str.charAt(P1--);
if (c == ' ') {
str.setCharAt(P2--, '0');
str.setCharAt(P2--, '2');
str.setCharAt(P2--, '%');
} else {
str.setCharAt(P2--, c);
}
}
return str.toString();
}
```
<div align="center"><img width="320px" src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/githubio/公众号二维码-2.png"></img></div>

View File

@ -1,56 +0,0 @@
# 50. 第一个只出现一次的字符位置
[NowCoder](https://www.nowcoder.com/practice/1c82e8cf713b4bbeb2a5b31cf5b0417c?tpId=13&tqId=11187&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking&from=cyc_github)
## 题目描述
在一个字符串中找到第一个只出现一次的字符并返回它的位置
```
Input: abacc
Output: b
```
## 解题思路
最直观的解法是使用 HashMap 对出现次数进行统计但是考虑到要统计的字符范围有限因此可以使用整型数组代替 HashMap从而将空间复杂度由 O(N) 降低为 O(1)
```java
public int FirstNotRepeatingChar(String str) {
int[] cnts = new int[256];
for (int i = 0; i < str.length(); i++)
cnts[str.charAt(i)]++;
for (int i = 0; i < str.length(); i++)
if (cnts[str.charAt(i)] == 1)
return i;
return -1;
}
```
以上实现的空间复杂度还不是最优的考虑到只需要找到只出现一次的字符那么需要统计的次数信息只有 0,1,更大使用两个比特位就能存储这些信息
```java
public int FirstNotRepeatingChar2(String str) {
BitSet bs1 = new BitSet(256);
BitSet bs2 = new BitSet(256);
for (char c : str.toCharArray()) {
if (!bs1.get(c) && !bs2.get(c))
bs1.set(c); // 0 0 -> 0 1
else if (bs1.get(c) && !bs2.get(c))
bs2.set(c); // 0 1 -> 1 1
}
for (int i = 0; i < str.length(); i++) {
char c = str.charAt(i);
if (bs1.get(c) && !bs2.get(c)) // 0 1
return i;
}
return -1;
}
```
<div align="center"><img width="320px" src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/githubio/公众号二维码-2.png"></img></div>

View File

@ -1,55 +0,0 @@
# 51. 数组中的逆序对
[NowCoder](https://www.nowcoder.com/practice/96bd6684e04a44eb80e6a68efc0ec6c5?tpId=13&tqId=11188&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking&from=cyc_github)
## 题目描述
在数组中的两个数字如果前面一个数字大于后面的数字则这两个数字组成一个逆序对输入一个数组求出这个数组中的逆序对的总数
## 解题思路
```java
private long cnt = 0;
private int[] tmp; // 在这里声明辅助数组而不是在 merge() 递归函数中声明
public int InversePairs(int[] nums) {
tmp = new int[nums.length];
mergeSort(nums, 0, nums.length - 1);
return (int) (cnt % 1000000007);
}
private void mergeSort(int[] nums, int l, int h) {
if (h - l < 1)
return;
int m = l + (h - l) / 2;
mergeSort(nums, l, m);
mergeSort(nums, m + 1, h);
merge(nums, l, m, h);
}
private void merge(int[] nums, int l, int m, int h) {
int i = l, j = m + 1, k = l;
while (i <= m || j <= h) {
if (i > m)
tmp[k] = nums[j++];
else if (j > h)
tmp[k] = nums[i++];
else if (nums[i] <= nums[j])
tmp[k] = nums[i++];
else {
tmp[k] = nums[j++];
this.cnt += m - i + 1; // nums[i] > nums[j]说明 nums[i...mid] 都大于 nums[j]
}
k++;
}
for (k = l; k <= h; k++)
nums[k] = tmp[k];
}
```
<div align="center"><img width="320px" src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/githubio/公众号二维码-2.png"></img></div>

View File

@ -1,31 +0,0 @@
# 52. 两个链表的第一个公共结点
[NowCoder](https://www.nowcoder.com/practice/6ab1d9a29e88450685099d45c9e31e46?tpId=13&tqId=11189&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking&from=cyc_github)
## 题目描述
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/5f1cb999-cb9a-4f6c-a0af-d90377295ab8.png" width="500"/> </div><br>
## 解题思路
A 的长度为 a + cB 的长度为 b + c其中 c 为尾部公共部分长度可知 a + c + b = b + c + a
当访问链表 A 的指针访问到链表尾部时令它从链表 B 的头部重新开始访问链表 B同样地当访问链表 B 的指针访问到链表尾部时令它从链表 A 的头部重新开始访问链表 A这样就能控制访问 A B 两个链表的指针能同时访问到交点
```java
public ListNode FindFirstCommonNode(ListNode pHead1, ListNode pHead2) {
ListNode l1 = pHead1, l2 = pHead2;
while (l1 != l2) {
l1 = (l1 == null) ? pHead2 : l1.next;
l2 = (l2 == null) ? pHead1 : l2.next;
}
return l1;
}
```
<div align="center"><img width="320px" src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/githubio/公众号二维码-2.png"></img></div>

View File

@ -1,43 +0,0 @@
# 53. 数字在排序数组中出现的次数
[NowCoder](https://www.nowcoder.com/practice/70610bf967994b22bb1c26f9ae901fa2?tpId=13&tqId=11190&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking&from=cyc_github)
## 题目描述
```html
Input:
nums = 1, 2, 3, 3, 3, 3, 4, 6
K = 3
Output:
4
```
## 解题思路
```java
public int GetNumberOfK(int[] nums, int K) {
int first = binarySearch(nums, K);
int last = binarySearch(nums, K + 1);
return (first == nums.length || nums[first] != K) ? 0 : last - first;
}
private int binarySearch(int[] nums, int K) {
int l = 0, h = nums.length;
while (l < h) {
int m = l + (h - l) / 2;
if (nums[m] >= K)
h = m;
else
l = m + 1;
}
return l;
}
```
<div align="center"><img width="320px" src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/githubio/公众号二维码-2.png"></img></div>

View File

@ -1,34 +0,0 @@
# 54. 二叉查找树的第 K 个结点
[NowCoder](https://www.nowcoder.com/practice/ef068f602dde4d28aab2b210e859150a?tpId=13&tqId=11215&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking&from=cyc_github)
## 解题思路
利用二叉查找树中序遍历有序的特点
```java
private TreeNode ret;
private int cnt = 0;
public TreeNode KthNode(TreeNode pRoot, int k) {
inOrder(pRoot, k);
return ret;
}
private void inOrder(TreeNode root, int k) {
if (root == null || cnt >= k)
return;
inOrder(root.left, k);
cnt++;
if (cnt == k)
ret = root;
inOrder(root.right, k);
}
```
<div align="center"><img width="320px" src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/githubio/公众号二维码-2.png"></img></div>

View File

@ -1,24 +0,0 @@
# 55.1 二叉树的深度
[NowCoder](https://www.nowcoder.com/practice/435fb86331474282a3499955f0a41e8b?tpId=13&tqId=11191&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking&from=cyc_github)
## 题目描述
从根结点到叶结点依次经过的结点含根叶结点形成树的一条路径最长路径的长度为树的深度
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/ba355101-4a93-4c71-94fb-1da83639727b.jpg" width="350px"/> </div><br>
## 解题思路
```java
public int TreeDepth(TreeNode root) {
return root == null ? 0 : 1 + Math.max(TreeDepth(root.left), TreeDepth(root.right));
}
```
<div align="center"><img width="320px" src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/githubio/公众号二维码-2.png"></img></div>

View File

@ -1,37 +0,0 @@
# 55.2 平衡二叉树
[NowCoder](https://www.nowcoder.com/practice/8b3b95850edb4115918ecebdf1b4d222?tpId=13&tqId=11192&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking&from=cyc_github)
## 题目描述
平衡二叉树左右子树高度差不超过 1
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/af1d1166-63af-47b6-9aa3-2bf2bd37bd03.jpg" width="250px"/> </div><br>
## 解题思路
```java
private boolean isBalanced = true;
public boolean IsBalanced_Solution(TreeNode root) {
height(root);
return isBalanced;
}
private int height(TreeNode root) {
if (root == null || !isBalanced)
return 0;
int left = height(root.left);
int right = height(root.right);
if (Math.abs(left - right) > 1)
isBalanced = false;
return 1 + Math.max(left, right);
}
```
<div align="center"><img width="320px" src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/githubio/公众号二维码-2.png"></img></div>

View File

@ -1,35 +0,0 @@
# 56. 数组中只出现一次的数字
[NowCoder](https://www.nowcoder.com/practice/e02fdb54d7524710a7d664d082bb7811?tpId=13&tqId=11193&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking&from=cyc_github)
## 题目描述
一个整型数组里除了两个数字之外其他的数字都出现了两次找出这两个数
## 解题思路
两个不相等的元素在位级表示上必定会有一位存在不同将数组的所有元素异或得到的结果为不存在重复的两个元素异或的结果
diff &= -diff 得到出 diff 最右侧不为 0 的位也就是不存在重复的两个元素在位级表示上最右侧不同的那一位利用这一位就可以将两个元素区分开来
```java
public void FindNumsAppearOnce(int[] nums, int num1[], int num2[]) {
int diff = 0;
for (int num : nums)
diff ^= num;
diff &= -diff;
for (int num : nums) {
if ((num & diff) == 0)
num1[0] ^= num;
else
num2[0] ^= num;
}
}
```
<div align="center"><img width="320px" src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/githubio/公众号二维码-2.png"></img></div>

View File

@ -1,38 +0,0 @@
# 57.1 和为 S 的两个数字
[NowCoder](https://www.nowcoder.com/practice/390da4f7a00f44bea7c2f3d19491311b?tpId=13&tqId=11195&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking&from=cyc_github)
## 题目描述
输入一个递增排序的数组和一个数字 S在数组中查找两个数使得他们的和正好是 S如果有多对数字的和等于 S输出两个数的乘积最小的
## 解题思路
使用双指针一个指针指向元素较小的值一个指针指向元素较大的值指向较小元素的指针从头向尾遍历指向较大元素的指针从尾向头遍历
- 如果两个指针指向元素的和 sum == target那么得到要求的结果
- 如果 sum > target移动较大的元素使 sum 变小一些
- 如果 sum < target移动较小的元素使 sum 变大一些
```java
public ArrayList<Integer> FindNumbersWithSum(int[] array, int sum) {
int i = 0, j = array.length - 1;
while (i < j) {
int cur = array[i] + array[j];
if (cur == sum)
return new ArrayList<>(Arrays.asList(array[i], array[j]));
if (cur < sum)
i++;
else
j--;
}
return new ArrayList<>();
}
```
<div align="center"><img width="320px" src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/githubio/公众号二维码-2.png"></img></div>

View File

@ -1,50 +0,0 @@
# 57.2 和为 S 的连续正数序列
[NowCoder](https://www.nowcoder.com/practice/c451a3fd84b64cb19485dad758a55ebe?tpId=13&tqId=11194&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking&from=cyc_github)
## 题目描述
输出所有和为 S 的连续正数序列
例如和为 100 的连续序列有
```
[9, 10, 11, 12, 13, 14, 15, 16]
[18, 19, 20, 21, 22]
```
## 解题思路
```java
public ArrayList<ArrayList<Integer>> FindContinuousSequence(int sum) {
ArrayList<ArrayList<Integer>> ret = new ArrayList<>();
int start = 1, end = 2;
int curSum = 3;
while (end < sum) {
if (curSum > sum) {
curSum -= start;
start++;
} else if (curSum < sum) {
end++;
curSum += end;
} else {
ArrayList<Integer> list = new ArrayList<>();
for (int i = start; i <= end; i++)
list.add(i);
ret.add(list);
curSum -= start;
start++;
end++;
curSum += end;
}
}
return ret;
}
```
<div align="center"><img width="320px" src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/githubio/公众号二维码-2.png"></img></div>

View File

@ -1,54 +0,0 @@
# 58.1 翻转单词顺序列
[NowCoder](https://www.nowcoder.com/practice/3194a4f4cf814f63919d0790578d51f3?tpId=13&tqId=11197&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking&from=cyc_github)
## 题目描述
```html
Input:
"I am a student."
Output:
"student. a am I"
```
## 解题思路
题目应该有一个隐含条件就是不能用额外的空间虽然 Java 的题目输入参数为 String 类型需要先创建一个字符数组使得空间复杂度为 O(N)但是正确的参数类型应该和原书一样为字符数组并且只能使用该字符数组的空间任何使用了额外空间的解法在面试时都会大打折扣包括递归解法
正确的解法应该是和书上一样先旋转每个单词再旋转整个字符串
```java
public String ReverseSentence(String str) {
int n = str.length();
char[] chars = str.toCharArray();
int i = 0, j = 0;
while (j <= n) {
if (j == n || chars[j] == ' ') {
reverse(chars, i, j - 1);
i = j + 1;
}
j++;
}
reverse(chars, 0, n - 1);
return new String(chars);
}
private void reverse(char[] c, int i, int j) {
while (i < j)
swap(c, i++, j--);
}
private void swap(char[] c, int i, int j) {
char t = c[i];
c[i] = c[j];
c[j] = t;
}
```
<div align="center"><img width="320px" src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/githubio/公众号二维码-2.png"></img></div>

View File

@ -1,48 +0,0 @@
# 58.2 左旋转字符串
[NowCoder](https://www.nowcoder.com/practice/12d959b108cb42b1ab72cef4d36af5ec?tpId=13&tqId=11196&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking&from=cyc_github)
## 题目描述
```html
Input:
S="abcXYZdef"
K=3
Output:
"XYZdefabc"
```
## 解题思路
先将 "abc" "XYZdef" 分别翻转得到 "cbafedZYX"然后再把整个字符串翻转得到 "XYZdefabc"
```java
public String LeftRotateString(String str, int n) {
if (n >= str.length())
return str;
char[] chars = str.toCharArray();
reverse(chars, 0, n - 1);
reverse(chars, n, chars.length - 1);
reverse(chars, 0, chars.length - 1);
return new String(chars);
}
private void reverse(char[] chars, int i, int j) {
while (i < j)
swap(chars, i++, j--);
}
private void swap(char[] chars, int i, int j) {
char t = chars[i];
chars[i] = chars[j];
chars[j] = t;
}
```
<div align="center"><img width="320px" src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/githubio/公众号二维码-2.png"></img></div>

View File

@ -1,36 +0,0 @@
# 59. 滑动窗口的最大值
[NowCoder](https://www.nowcoder.com/practice/1624bc35a45c42c0bc17d17fa0cba788?tpId=13&tqId=11217&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking&from=cyc_github)
## 题目描述
给定一个数组和滑动窗口的大小找出所有滑动窗口里数值的最大值
例如如果输入数组 {2, 3, 4, 2, 6, 2, 5, 1} 及滑动窗口的大小 3那么一共存在 6 个滑动窗口他们的最大值分别为 {4, 4, 6, 6, 6, 5}
## 解题思路
```java
public ArrayList<Integer> maxInWindows(int[] num, int size) {
ArrayList<Integer> ret = new ArrayList<>();
if (size > num.length || size < 1)
return ret;
PriorityQueue<Integer> heap = new PriorityQueue<>((o1, o2) -> o2 - o1); /* 大顶堆 */
for (int i = 0; i < size; i++)
heap.add(num[i]);
ret.add(heap.peek());
for (int i = 0, j = i + size; j < num.length; i++, j++) { /* 维护一个大小为 size 的大顶堆 */
heap.remove(num[i]);
heap.add(num[j]);
ret.add(heap.peek());
}
return ret;
}
```
<div align="center"><img width="320px" src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/githubio/公众号二维码-2.png"></img></div>

View File

@ -1,96 +0,0 @@
# 6. 从尾到头打印链表
## 题目链接
[牛客网](https://www.nowcoder.com/practice/d0267f7f55b3412ba93bd35cfa8e8035?tpId=13&tqId=11156&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking&from=cyc_github)
## 题目描述
从尾到头反过来打印出每个结点的值
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/f5792051-d9b2-4ca4-a234-a4a2de3d5a57.png" width="300px"> </div><br>
## 解题思路
### 1. 使用递归
要逆序打印链表 1->2->33,2,1)可以先逆序打印链表 2->3(3,2)最后再打印第一个节点 1而链表 2->3 可以看成一个新的链表要逆序打印该链表可以继续使用求解函数也就是在求解函数中调用自己这就是递归函数
```java
public ArrayList<Integer> printListFromTailToHead(ListNode listNode) {
ArrayList<Integer> ret = new ArrayList<>();
if (listNode != null) {
ret.addAll(printListFromTailToHead(listNode.next));
ret.add(listNode.val);
}
return ret;
}
```
### 2. 使用头插法
头插法顾名思义是将节点插入到头部在遍历原始链表时将当前节点插入新链表的头部使其成为第一个节点
链表的操作需要维护后继关系例如在某个节点 node1 之后插入一个节点 node2我们可以通过修改后继关系来实现
```java
node3 = node1.next;
node2.next = node3;
node1.next = node2;
```
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/58c8e370-3bec-4c2b-bf17-c8d34345dd17.gif" width="220px"> </div><br>
为了能将一个节点插入头部我们引入了一个叫头结点的辅助节点该节点不存储值只是为了方便进行插入操作不要将头结点与第一个节点混起来第一个节点是链表中第一个真正存储值的节点
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/0dae7e93-cfd1-4bd3-97e8-325b032b716f-1572687622947.gif" width="420px"> </div><br>
```java
public ArrayList<Integer> printListFromTailToHead(ListNode listNode) {
// 头插法构建逆序链表
ListNode head = new ListNode(-1);
while (listNode != null) {
ListNode memo = listNode.next;
listNode.next = head.next;
head.next = listNode;
listNode = memo;
}
// 构建 ArrayList
ArrayList<Integer> ret = new ArrayList<>();
head = head.next;
while (head != null) {
ret.add(head.val);
head = head.next;
}
return ret;
}
```
### 3. 使用栈
栈具有后进先出的特点在遍历链表时将值按顺序放入栈中最后出栈的顺序即为逆序
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/9d1deeba-4ae1-41dc-98f4-47d85b9831bc.gif" width="340px"> </div><br>
```java
public ArrayList<Integer> printListFromTailToHead(ListNode listNode) {
Stack<Integer> stack = new Stack<>();
while (listNode != null) {
stack.add(listNode.val);
listNode = listNode.next;
}
ArrayList<Integer> ret = new ArrayList<>();
while (!stack.isEmpty())
ret.add(stack.pop());
return ret;
}
```
<div align="center"><img width="320px" src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/githubio/公众号二维码-2.png"></img></div>

View File

@ -1,81 +0,0 @@
# 60. n 个骰子的点数
## 题目链接
[Lintcode](https://www.lintcode.com/en/problem/dices-sum/)
## 题目描述
n 个骰子扔在地上求点数和为 s 的概率
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/195f8693-5ec4-4987-8560-f25e365879dd.png" width="300px"> </div><br>
## 解题思路
### 动态规划
使用一个二维数组 dp 存储点数出现的次数其中 dp\[i]\[j] 表示前 i 个骰子产生点数 j 的次数
空间复杂度O(N<sup>2</sup>)
```java
public List<Map.Entry<Integer, Double>> dicesSum(int n) {
final int face = 6;
final int pointNum = face * n;
long[][] dp = new long[n + 1][pointNum + 1];
for (int i = 1; i <= face; i++)
dp[1][i] = 1;
for (int i = 2; i <= n; i++)
for (int j = i; j <= pointNum; j++) /* 使用 i 个骰子最小点数为 i */
for (int k = 1; k <= face && k <= j; k++)
dp[i][j] += dp[i - 1][j - k];
final double totalNum = Math.pow(6, n);
List<Map.Entry<Integer, Double>> ret = new ArrayList<>();
for (int i = n; i <= pointNum; i++)
ret.add(new AbstractMap.SimpleEntry<>(i, dp[n][i] / totalNum));
return ret;
}
```
### 动态规划 + 旋转数组
空间复杂度O(N)
```java
public List<Map.Entry<Integer, Double>> dicesSum(int n) {
final int face = 6;
final int pointNum = face * n;
long[][] dp = new long[2][pointNum + 1];
for (int i = 1; i <= face; i++)
dp[0][i] = 1;
int flag = 1; /* 旋转标记 */
for (int i = 2; i <= n; i++, flag = 1 - flag) {
for (int j = 0; j <= pointNum; j++)
dp[flag][j] = 0; /* 旋转数组清零 */
for (int j = i; j <= pointNum; j++)
for (int k = 1; k <= face && k <= j; k++)
dp[flag][j] += dp[1 - flag][j - k];
}
final double totalNum = Math.pow(6, n);
List<Map.Entry<Integer, Double>> ret = new ArrayList<>();
for (int i = n; i <= pointNum; i++)
ret.add(new AbstractMap.SimpleEntry<>(i, dp[1 - flag][i] / totalNum));
return ret;
}
```
<div align="center"><img width="320px" src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/githubio/公众号二维码-2.png"></img></div>

View File

@ -1,46 +0,0 @@
# 61. 扑克牌顺子
## 题目链接
[NowCoder](https://www.nowcoder.com/practice/762836f4d43d43ca9deb273b3de8e1f4?tpId=13&tqId=11198&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking&from=cyc_github)
## 题目描述
五张牌其中大小鬼为癞子牌面为 0判断这五张牌是否能组成顺子
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/eaa506b6-0747-4bee-81f8-3cda795d8154.png" width="350px"> </div><br>
## 解题思路
```java
public boolean isContinuous(int[] nums) {
if (nums.length < 5)
return false;
Arrays.sort(nums);
// 统计癞子数量
int cnt = 0;
for (int num : nums)
if (num == 0)
cnt++;
// 使用癞子去补全不连续的顺子
for (int i = cnt; i < nums.length - 1; i++) {
if (nums[i + 1] == nums[i])
return false;
cnt -= nums[i + 1] - nums[i] - 1;
}
return cnt >= 0;
}
```
<div align="center"><img width="320px" src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/githubio/公众号二维码-2.png"></img></div>

View File

@ -1,30 +0,0 @@
# 62. 圆圈中最后剩下的数
## 题目链接
[NowCoder](https://www.nowcoder.com/practice/f78a359491e64a50bce2d89cff857eb6?tpId=13&tqId=11199&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking&from=cyc_github)
## 题目描述
让小朋友们围成一个大圈然后随机指定一个数 m让编号为 0 的小朋友开始报数每次喊到 m-1 的那个小朋友要出列唱首歌然后可以在礼品箱中任意的挑选礼物并且不再回到圈中从他的下一个小朋友开始继续 0...m-1 报数 .... 这样下去 .... 直到剩下最后一个小朋友可以不用表演
## 解题思路
约瑟夫环圆圈长度为 n 的解可以看成长度为 n-1 的解再加上报数的长度 m因为是圆圈所以最后需要对 n 取余
```java
public int LastRemaining_Solution(int n, int m) {
if (n == 0) /* 特殊输入的处理 */
return -1;
if (n == 1) /* 递归返回条件 */
return 0;
return (LastRemaining_Solution(n - 1, m) + m) % n;
}
```
<div align="center"><img width="320px" src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/githubio/公众号二维码-2.png"></img></div>

View File

@ -1,36 +0,0 @@
# 63. 股票的最大利润
## 题目链接
[Leetcode121. Best Time to Buy and Sell Stock ](https://leetcode.com/problems/best-time-to-buy-and-sell-stock/description/)
## 题目描述
可以有一次买入和一次卖出买入必须在前求最大收益
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/42661013-750f-420b-b3c1-437e9a11fb65.png" width="220px"> </div><br>
## 解题思路
使用贪心策略假设第 i 轮进行卖出操作买入操作价格应该在 i 之前并且价格最低
```java
public int maxProfit(int[] prices) {
if (prices == null || prices.length == 0)
return 0;
int soFarMin = prices[0];
int maxProfit = 0;
for (int i = 1; i < prices.length; i++) {
soFarMin = Math.min(soFarMin, prices[i]);
maxProfit = Math.max(maxProfit, prices[i] - soFarMin);
}
return maxProfit;
}
```
<div align="center"><img width="320px" src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/githubio/公众号二维码-2.png"></img></div>

View File

@ -1,32 +0,0 @@
# 64. 1+2+3+...+n
## 题目链接
[NowCoder](https://www.nowcoder.com/practice/7a0da8fc483247ff8800059e12d7caf1?tpId=13&tqId=11200&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking&from=cyc_github)
## 题目描述
要求不能使用乘除法forwhileifelseswitchcase 等关键字及条件判断语句 A ? B : C
## 解题思路
使用递归解法最重要的是指定返回条件但是本题无法直接使用 if 语句来指定返回条件
条件与 && 具有短路原则即在第一个条件语句为 false 的情况下不会去执行第二个条件语句利用这一特性将递归的返回条件取非然后作为 && 的第一个条件语句递归的主体转换为第二个条件语句那么当递归的返回条件为 true 的情况下就不会执行递归的主体部分递归返回
本题的递归返回条件为 n <= 0取非后就是 n > 0递归的主体部分为 sum += Sum_Solution(n - 1)转换为条件语句后就是 (sum += Sum_Solution(n - 1)) > 0
```java
public int Sum_Solution(int n) {
int sum = n;
boolean b = (n > 0) && ((sum += Sum_Solution(n - 1)) > 0);
return sum;
}
```
<div align="center"><img width="320px" src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/githubio/公众号二维码-2.png"></img></div>

View File

@ -1,28 +0,0 @@
# 65. 不用加减乘除做加法
## 题目链接
[NowCoder](https://www.nowcoder.com/practice/59ac416b4b944300b617d4f7f111b215?tpId=13&tqId=11201&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking&from=cyc_github)
## 题目描述
写一个函数求两个整数之和要求不得使用 +-\*/ 四则运算符号
## 解题思路
a ^ b 表示没有考虑进位的情况下两数的和(a & b) << 1 就是进位
递归会终止的原因是 (a & b) << 1 最右边会多一个 0那么继续递归进位最右边的 0 会慢慢增多最后进位会变为 0递归终止
```java
public int Add(int a, int b) {
return b == 0 ? a : Add(a ^ b, (a & b) << 1);
}
```
<div align="center"><img width="320px" src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/githubio/公众号二维码-2.png"></img></div>

View File

@ -1,33 +0,0 @@
# 66. 构建乘积数组
## 题目链接
[NowCoder](https://www.nowcoder.com/practice/94a4d381a68b47b7a8bed86f2975db46?tpId=13&tqId=11204&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking&from=cyc_github)
## 题目描述
给定一个数组 A[0, 1,..., n-1]请构建一个数组 B[0, 1,..., n-1]其中 B 中的元素 B[i]=A[0]\*A[1]\*...\*A[i-1]\*A[i+1]\*...\*A[n-1]要求不能使用除法
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/4240a69f-4d51-4d16-b797-2dfe110f30bd.png" width="250px"> </div><br>
## 解题思路
```java
public int[] multiply(int[] A) {
int n = A.length;
int[] B = new int[n];
for (int i = 0, product = 1; i < n; product *= A[i], i++) /* 从左往右累乘 */
B[i] = product;
for (int i = n - 1, product = 1; i >= 0; product *= A[i], i--) /* 从右往左累乘 */
B[i] *= product;
return B;
}
```
<div align="center"><img width="320px" src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/githubio/公众号二维码-2.png"></img></div>

View File

@ -1,46 +0,0 @@
# 67. 把字符串转换成整数
## 题目链接
[NowCoder](https://www.nowcoder.com/practice/1277c681251b4372bdef344468e4f26e?tpId=13&tqId=11202&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking&from=cyc_github)
## 题目描述
将一个字符串转换成一个整数字符串不是一个合法的数值则返回 0要求不能使用字符串转换整数的库函数
```html
Iuput:
+2147483647
1a33
Output:
2147483647
0
```
## 解题思路
```java
public int StrToInt(String str) {
if (str == null || str.length() == 0)
return 0;
boolean isNegative = str.charAt(0) == '-';
int ret = 0;
for (int i = 0; i < str.length(); i++) {
char c = str.charAt(i);
if (i == 0 && (c == '+' || c == '-')) /* 符号判定 */
continue;
if (c < '0' || c > '9') /* 非法输入 */
return 0;
ret = ret * 10 + (c - '0');
}
return isNegative ? -ret : ret;
}
```
<div align="center"><img width="320px" src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/githubio/公众号二维码-2.png"></img></div>

View File

@ -1,55 +0,0 @@
# 68. 树中两个节点的最低公共祖先
## 68.1 二叉查找树
### 题目链接
[Leetcode : 235. Lowest Common Ancestor of a Binary Search Tree](https://leetcode.com/problems/lowest-common-ancestor-of-a-binary-search-tree/description/)
### 解题思路
在二叉查找树中两个节点 p, q 的公共祖先 root 满足 root.val >= p.val && root.val <= q.val
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/047faac4-a368-4565-8331-2b66253080d3.jpg" width="250"/> </div><br>
```java
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
if (root == null)
return root;
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;
}
```
## 68.2 普通二叉树
### 题目链接
[Leetcode : 236. Lowest Common Ancestor of a Binary Tree](https://leetcode.com/problems/lowest-common-ancestor-of-a-binary-tree/description/)
### 解题思路
在左右子树中查找是否存在 p 或者 q如果 p q 分别在两个子树中那么就说明根节点就是最低公共祖先
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/d27c99f0-7881-4f2d-9675-c75cbdee3acd.jpg" width="250"/> </div><br>
```java
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
if (root == null || root == p || root == q)
return root;
TreeNode left = lowestCommonAncestor(root.left, p, q);
TreeNode right = lowestCommonAncestor(root.right, p, q);
return left == null ? right : right == null ? left : root;
}
```
<div align="center"><img width="320px" src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/githubio/公众号二维码-2.png"></img></div>

View File

@ -1,48 +0,0 @@
# 7. 重建二叉树
## 题目链接
[牛客网](https://www.nowcoder.com/practice/8a19cbe657394eeaac2f6ea9b0f6fcf6?tpId=13&tqId=11157&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking&from=cyc_github)
## 题目描述
根据二叉树的前序遍历和中序遍历的结果重建出该二叉树假设输入的前序遍历和中序遍历的结果中都不含重复的数字
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/image-20191102210342488.png" width="400"/> </div><br>
## 解题思路
前序遍历的第一个值为根节点的值使用这个值将中序遍历结果分成两部分左部分为树的左子树中序遍历结果右部分为树的右子树中序遍历的结果然后分别对左右子树递归地求解
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/60c4a44c-7829-4242-b3a1-26c3b513aaf0.gif" width="430px"> </div><br>
```java
// 缓存中序遍历数组每个值对应的索引
private Map<Integer, Integer> indexForInOrders = new HashMap<>();
public TreeNode reConstructBinaryTree(int[] pre, int[] in) {
for (int i = 0; i < in.length; i++)
indexForInOrders.put(in[i], i);
return reConstructBinaryTree(pre, 0, pre.length - 1, 0);
}
private TreeNode reConstructBinaryTree(int[] pre, int preL, int preR, int inL) {
if (preL > preR)
return null;
TreeNode root = new TreeNode(pre[preL]);
int inIndex = indexForInOrders.get(root.val);
int leftTreeSize = inIndex - inL;
root.left = reConstructBinaryTree(pre, preL + 1, preL + leftTreeSize, inL);
root.right = reConstructBinaryTree(pre, preL + leftTreeSize + 1, preR, inL + leftTreeSize + 1);
return root;
}
```
<div align="center"><img width="320px" src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/githubio/公众号二维码-2.png"></img></div>

View File

@ -1,74 +0,0 @@
# 8. 二叉树的下一个结点
## 题目链接
[牛客网](https://www.nowcoder.com/practice/9023a0c988684a53960365b889ceaf5e?tpId=13&tqId=11210&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking&from=cyc_github)
## 题目描述
给定一个二叉树和其中的一个结点请找出中序遍历顺序的下一个结点并且返回 注意树中的结点不仅包含左右子结点同时包含指向父结点的指针
```java
public class TreeLinkNode {
int val;
TreeLinkNode left = null;
TreeLinkNode right = null;
TreeLinkNode next = null; // 指向父结点的指针
TreeLinkNode(int val) {
this.val = val;
}
}
```
## 解题思路
我们先来回顾一下中序遍历的过程先遍历树的左子树再遍历根节点最后再遍历右子树所以最左节点是中序遍历的第一个节点
```java
void traverse(TreeNode root) {
if (root == null) return;
traverse(root.left);
visit(root);
traverse(root.right);
}
```
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/ad5cc8fc-d59b-45ce-8899-63a18320d97e.gif" width="300px"/> </div><br>
如果一个节点的右子树不为空那么该节点的下一个节点是右子树的最左节点
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/7008dc2b-6f13-4174-a516-28b2d75b0152.gif" width="300px"/> </div><br>
否则向上找第一个左链接指向的树包含该节点的祖先节点
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/094e3ac8-e080-4e94-9f0a-64c25abc695e.gif" width="300px"/> </div><br>
```java
public TreeLinkNode GetNext(TreeLinkNode pNode) {
if (pNode.right != null) {
TreeLinkNode node = pNode.right;
while (node.left != null)
node = node.left;
return node;
} else {
while (pNode.next != null) {
TreeLinkNode parent = pNode.next;
if (parent.left == pNode)
return parent;
pNode = pNode.next;
}
}
return null;
}
```
<div align="center"><img width="320px" src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/githubio/公众号二维码-2.png"></img></div>

View File

@ -1,42 +0,0 @@
# 9. 用两个栈实现队列
## 题目链接
[牛客网](https://www.nowcoder.com/practice/54275ddae22f475981afa2244dd448c6?tpId=13&tqId=11158&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking&from=cyc_github)
## 题目描述
用两个栈来实现一个队列完成队列的 Push Pop 操作
## 解题思路
in 栈用来处理入栈push操作out 栈用来处理出栈pop操作一个元素进入 in 栈之后出栈的顺序被反转当元素要出栈时需要先进入 out 此时元素出栈顺序再一次被反转因此出栈顺序就和最开始入栈顺序是相同的先进入的元素先退出这就是队列的顺序
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/3ea280b5-be7d-471b-ac76-ff020384357c.gif" width="450"/> </div><br>
```java
Stack<Integer> in = new Stack<Integer>();
Stack<Integer> out = new Stack<Integer>();
public void push(int node) {
in.push(node);
}
public int pop() throws Exception {
if (out.isEmpty())
while (!in.isEmpty())
out.push(in.pop());
if (out.isEmpty())
throw new Exception("queue is empty");
return out.pop();
}
```
<div align="center"><img width="320px" src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/githubio/公众号二维码-2.png"></img></div>

View File

@ -1,96 +0,0 @@
<!-- GFM-TOC -->
* [解决的问题](#一解决的问题)
* [与虚拟机的比较](#二与虚拟机的比较)
* [优势](#三优势)
* [使用场景](#四使用场景)
* [镜像与容器](#五镜像与容器)
* [参考资料](#参考资料)
<!-- GFM-TOC -->
# 解决的问题
由于不同的机器有不同的操作系统以及不同的库和组件在将一个应用部署到多台机器上需要进行大量的环境配置操作
Docker 主要解决环境配置问题它是一种虚拟化技术对进程进行隔离被隔离的进程独立于宿主操作系统和其它隔离的进程使用 Docker 可以不修改应用程序代码不需要开发人员学习特定环境下的技术就能够将现有的应用程序部署在其它机器上
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/011f3ef6-d824-4d43-8b2c-36dab8eaaa72-1.png" width="400px"/> </div><br>
# 与虚拟机的比较
虚拟机也是一种虚拟化技术它与 Docker 最大的区别在于它是通过模拟硬件并在硬件上安装操作系统来实现
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/be608a77-7b7f-4f8e-87cc-f2237270bf69.png" width="500"/> </div><br>
## 启动速度
启动虚拟机需要先启动虚拟机的操作系统再启动应用这个过程非常慢
而启动 Docker 相当于启动宿主操作系统上的一个进程
## 占用资源
虚拟机是一个完整的操作系统需要占用大量的磁盘内存和 CPU 资源一台机器只能开启几十个的虚拟机
Docker 只是一个进程只需要将应用以及相关的组件打包在运行时占用很少的资源一台机器可以开启成千上万个 Docker
# 优势
除了启动速度快以及占用资源少之外Docker 具有以下优势
## 更容易迁移
提供一致性的运行环境已经打包好的应用可以在不同的机器上进行迁移而不用担心环境变化导致无法运行
## 更容易维护
使用分层技术和镜像使得应用可以更容易复用重复的部分复用程度越高维护工作也越容易
## 更容易扩展
可以使用基础镜像进一步扩展得到新的镜像并且官方和开源社区提供了大量的镜像通过扩展这些镜像可以非常容易得到我们想要的镜像
# 使用场景
## 持续集成
持续集成指的是频繁地将代码集成到主干上这样能够更快地发现错误
Docker 具有轻量级以及隔离性的特点在将代码集成到一个 Docker 中不会对其它 Docker 产生影响
## 提供可伸缩的云服务
根据应用的负载情况可以很容易地增加或者减少 Docker
## 搭建微服务架构
Docker 轻量级的特点使得它很适合用于部署维护组合微服务
# 镜像与容器
镜像是一种静态的结构可以看成面向对象里面的类而容器是镜像的一个实例
镜像包含着容器运行时所需要的代码以及其它组件它是一种分层结构每一层都是只读的read-only layers构建镜像时会一层一层构建前一层是后一层的基础镜像的这种分层存储结构很适合镜像的复用以及定制
构建容器时通过在镜像的基础上添加一个可写层writable layer用来保存着容器运行过程中的修改
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/docker-filesystems-busyboxrw.png"/> </div><br>
# 参考资料
- [DOCKER 101: INTRODUCTION TO DOCKER WEBINAR RECAP](https://blog.docker.com/2017/08/docker-101-introduction-docker-webinar-recap/)
- [Docker 入门教程](http://www.ruanyifeng.com/blog/2018/02/docker-tutorial.html)
- [Docker container vs Virtual machine](http://www.bogotobogo.com/DevOps/Docker/Docker_Container_vs_Virtual_Machine.php)
- [How to Create Docker Container using Dockerfile](https://linoxide.com/linux-how-to/dockerfile-create-docker-container/)
- [理解 Docker2Docker 镜像](http://www.cnblogs.com/sammyliu/p/5877964.html)
- [为什么要使用 Docker](https://yeasy.gitbooks.io/docker_practice/introduction/why.html)
- [What is Docker](https://www.docker.com/what-docker)
- [持续集成是什么](http://www.ruanyifeng.com/blog/2015/09/continuous-integration.html)
<div align="center"><img width="320px" src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/githubio/公众号二维码-2.png"></img></div>

View File

@ -1,158 +0,0 @@
<!-- GFM-TOC -->
* [集中式与分布式](#集中式与分布式)
* [中心服务器](#中心服务器)
* [工作流](#工作流)
* [分支实现](#分支实现)
* [冲突](#冲突)
* [Fast forward](#fast-forward)
* [储藏Stashing](#储藏stashing)
* [SSH 传输设置](#ssh-传输设置)
* [.gitignore 文件](#gitignore-文件)
* [Git 命令一览](#git-命令一览)
* [参考资料](#参考资料)
<!-- GFM-TOC -->
# 集中式与分布式
Git 属于分布式版本控制系统 SVN 属于集中式
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/image-20191208200656794.png"/> </div><br>
集中式版本控制只有中心服务器拥有一份代码而分布式版本控制每个人的电脑上就有一份完整的代码
集中式版本控制有安全性问题当中心服务器挂了所有人都没办法工作了
集中式版本控制需要连网才能工作如果网速过慢那么提交一个文件会慢的无法让人忍受而分布式版本控制不需要连网就能工作
分布式版本控制新建分支合并分支操作速度非常快而集中式版本控制新建一个分支相当于复制一份完整代码
# 中心服务器
中心服务器用来交换每个用户的修改没有中心服务器也能工作但是中心服务器能够 24 小时保持开机状态这样就能更方便的交换修改
Github 就是一个中心服务器
# 工作流
新建一个仓库之后当前目录就成为了工作区工作区下有一个隐藏目录 .git它属于 Git 的版本库
Git 的版本库有一个称为 Stage 的暂存区以及最后的 History 版本库History 存储所有分支信息使用一个 HEAD 指针指向当前分支
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/image-20191208195941661.png"/> </div><br>
- git add files 把文件的修改添加到暂存区
- git commit 把暂存区的修改提交到当前分支提交之后暂存区就被清空了
- git reset -- files 使用当前分支上的修改覆盖暂存区用来撤销最后一次 git add files
- git checkout -- files 使用暂存区的修改覆盖工作目录用来撤销本地修改
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/image-20191208200014395.png"/> </div><br>
可以跳过暂存区域直接从分支中取出修改或者直接提交修改到分支中
- git commit -a 直接把所有文件的修改添加到暂存区然后执行提交
- git checkout HEAD -- files 取出最后一次修改可以用来进行回滚操作
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/image-20191208200543923.png"/> </div><br>
# 分支实现
使用指针将每个提交连接成一条时间线HEAD 指针指向当前分支指针
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/image-20191208203219927.png"/> </div><br>
新建分支是新建一个指针指向时间线的最后一个节点并让 HEAD 指针指向新分支表示新分支成为当前分支
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/image-20191208203142527.png"/> </div><br>
每次提交只会让当前分支指针向前移动而其它分支指针不会移动
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/image-20191208203112400.png"/> </div><br>
合并分支也只需要改变指针即可
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/image-20191208203010540.png"/> </div><br>
# 冲突
当两个分支都对同一个文件的同一行进行了修改在分支合并时就会产生冲突
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/image-20191208203034705.png"/> </div><br>
Git 会使用 <<<<<<< ======= >>>>>>> 标记出不同分支的内容只需要把不同分支中冲突部分修改成一样就能解决冲突
```
<<<<<<< HEAD
Creating a new branch is quick & simple.
=======
Creating a new branch is quick AND simple.
>>>>>>> feature1
```
# Fast forward
"快进式合并"fast-farward merge会直接将 master 分支指向合并的分支这种模式下进行分支合并会丢失分支信息也就不能在分支历史上看出分支信息
可以在合并时加上 --no-ff 参数来禁用 Fast forward 模式并且加上 -m 参数让合并时产生一个新的 commit
```
$ git merge --no-ff -m "merge with no-ff" dev
```
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/image-20191208203639712.png"/> </div><br>
# 储藏Stashing
在一个分支上操作之后如果还没有将修改提交到分支上此时进行切换分支那么另一个分支上也能看到新的修改这是因为所有分支都共用一个工作区的缘故
可以使用 git stash 将当前分支的修改储藏起来此时当前工作区的所有修改都会被存到栈中也就是说当前工作区是干净的没有任何未提交的修改此时就可以安全的切换到其它分支上了
```
$ git stash
Saved working directory and index state \ "WIP on master: 049d078 added the index file"
HEAD is now at 049d078 added the index file (To restore them type "git stash apply")
```
该功能可以用于 bug 分支的实现如果当前正在 dev 分支上进行开发但是此时 master 上有个 bug 需要修复但是 dev 分支上的开发还未完成不想立即提交在新建 bug 分支并切换到 bug 分支之前就需要使用 git stash dev 分支的未提交修改储藏起来
# SSH 传输设置
Git 仓库和 Github 中心仓库之间的传输是通过 SSH 加密
如果工作区下没有 .ssh 目录或者该目录下没有 id_rsa id_rsa.pub 这两个文件可以通过以下命令来创建 SSH Key
```
$ ssh-keygen -t rsa -C "youremail@example.com"
```
然后把公钥 id_rsa.pub 的内容复制到 Github "Account settings" SSH Keys
# .gitignore 文件
忽略以下文件
- 操作系统自动生成的文件比如缩略图
- 编译生成的中间文件比如 Java 编译产生的 .class 文件
- 自己的敏感信息比如存放口令的配置文件
不需要全部自己编写可以到 [https://github.com/github/gitignore](https://github.com/github/gitignore) 中进行查询
# Git 命令一览
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/7a29acce-f243-4914-9f00-f2988c528412.jpg" width=""> </div><br>
比较详细的地址http://www.cheat-sheets.org/saved-copy/git-cheat-sheet.pdf
# 参考资料
- [Git - 简明指南](http://rogerdudler.github.io/git-guide/index.zh.html)
- [图解 Git](http://marklodato.github.io/visual-git-guide/index-zh-cn.html)
- [廖雪峰 : Git 教程](https://www.liaoxuefeng.com/wiki/0013739516305929606dd18361248578c67b8067c8c017b000)
- [Learn Git Branching](https://learngitbranching.js.org/)
<div align="center"><img width="320px" src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/githubio/公众号二维码-2.png"></img></div>

View File

@ -1,948 +0,0 @@
<!-- GFM-TOC -->
* [ 基础概念](#-基础概念)
* [请求和响应报文](#请求和响应报文)
* [URL](#url)
* [HTTP 方法](#二http-方法)
* [GET](#get)
* [HEAD](#head)
* [POST](#post)
* [PUT](#put)
* [PATCH](#patch)
* [DELETE](#delete)
* [OPTIONS](#options)
* [CONNECT](#connect)
* [TRACE](#trace)
* [HTTP 状态码](#三http-状态码)
* [1XX 信息](#1xx-信息)
* [2XX 成功](#2xx-成功)
* [3XX 重定向](#3xx-重定向)
* [4XX 客户端错误](#4xx-客户端错误)
* [5XX 服务器错误](#5xx-服务器错误)
* [HTTP 首部](#四http-首部)
* [通用首部字段](#通用首部字段)
* [请求首部字段](#请求首部字段)
* [响应首部字段](#响应首部字段)
* [实体首部字段](#实体首部字段)
* [具体应用](#五具体应用)
* [连接管理](#连接管理)
* [Cookie](#cookie)
* [缓存](#缓存)
* [内容协商](#内容协商)
* [内容编码](#内容编码)
* [范围请求](#范围请求)
* [分块传输编码](#分块传输编码)
* [多部分对象集合](#多部分对象集合)
* [虚拟主机](#虚拟主机)
* [通信数据转发](#通信数据转发)
* [HTTPS](#六https)
* [加密](#加密)
* [认证](#认证)
* [完整性保护](#完整性保护)
* [HTTPS 的缺点](#https-的缺点)
* [HTTP/2.0](#七http20)
* [HTTP/1.x 缺陷](#http1x-缺陷)
* [二进制分帧层](#二进制分帧层)
* [服务端推送](#服务端推送)
* [首部压缩](#首部压缩)
* [HTTP/1.1 新特性](#八http11-新特性)
* [GET POST 比较](#九get--post-比较)
* [作用](#作用)
* [参数](#参数)
* [安全](#安全)
* [幂等性](#幂等性)
* [可缓存](#可缓存)
* [XMLHttpRequest](#xmlhttprequest)
* [参考资料](#参考资料)
<!-- GFM-TOC -->
# 基础概念
## 请求和响应报文
客户端发送一个请求报文给服务器服务器根据请求报文中的信息进行处理并将处理结果放入响应报文中返回给客户端
请求报文结构
- 第一行是包含了请求方法URL协议版本
- 接下来的多行都是请求首部 Header每个首部都有一个首部名称以及对应的值
- 一个空行用来分隔首部和内容主体 Body
- 最后是请求的内容主体
```
GET http://www.example.com/ HTTP/1.1
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8
Cache-Control: max-age=0
Host: www.example.com
If-Modified-Since: Thu, 17 Oct 2019 07:18:26 GMT
If-None-Match: "3147526947+gzip"
Proxy-Connection: keep-alive
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 xxx
param1=1&param2=2
```
响应报文结构
- 第一行包含协议版本状态码以及描述最常见的是 200 OK 表示请求成功了
- 接下来多行也是首部内容
- 一个空行分隔首部和内容主体
- 最后是响应的内容主体
```
HTTP/1.1 200 OK
Age: 529651
Cache-Control: max-age=604800
Connection: keep-alive
Content-Encoding: gzip
Content-Length: 648
Content-Type: text/html; charset=UTF-8
Date: Mon, 02 Nov 2020 17:53:39 GMT
Etag: "3147526947+ident+gzip"
Expires: Mon, 09 Nov 2020 17:53:39 GMT
Keep-Alive: timeout=4
Last-Modified: Thu, 17 Oct 2019 07:18:26 GMT
Proxy-Connection: keep-alive
Server: ECS (sjc/16DF)
Vary: Accept-Encoding
X-Cache: HIT
<!doctype html>
<html>
<head>
<title>Example Domain</title>
// 省略...
</body>
</html>
```
## URL
http 使用 URL **U** niform **R**esource **L**ocator统一资源定位符来定位资源它可以认为是是 URI**U**niform **R**esource **I**dentifier统一资源标识符的一个子集URL URI 的基础上增加了定位能力URI 除了包含 URL 之外还包含 URNUniform Resource Name统一资源名称它知识用来定义一个资源的名称并不具备定位该资源的能力例如 urn:isbn:0451450523 用来定义一个书籍但是却没有表示怎么找到这本书
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/8441b2c4-dca7-4d6b-8efb-f22efccaf331.png" width="500px"> </div><br>
- [wikipedia统一资源标志符](https://zh.wikipedia.org/wiki/统一资源标志符)
- [wikipedia: URL](https://en.wikipedia.org/wiki/URL)
- [rfc26163.2.2 http URL](https://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.2.2)
- [What is the difference between a URI, a URL and a URN?](https://stackoverflow.com/questions/176264/what-is-the-difference-between-a-uri-a-url-and-a-urn)
# HTTP 方法
客户端发送的 **请求报文** 第一行为请求行包含了方法字段
## GET
> 获取资源
当前网络请求中绝大部分使用的是 GET 方法
## HEAD
> 获取报文首部
GET 方法类似但是不返回报文实体主体部分
主要用于确认 URL 的有效性以及资源更新的日期时间等
## POST
> 传输实体主体
POST 主要用来传输数据 GET 主要用来获取资源
更多 POST GET 的比较请见第九章
## PUT
> 上传文件
由于自身不带验证机制任何人都可以上传文件因此存在安全性问题一般不使用该方法
```html
PUT /new.html HTTP/1.1
Host: example.com
Content-type: text/html
Content-length: 16
<p>New File</p>
```
## PATCH
> 对资源进行部分修改
PUT 也可以用于修改资源但是只能完全替代原始资源PATCH 允许部分修改
```html
PATCH /file.txt HTTP/1.1
Host: www.example.com
Content-Type: application/example
If-Match: "e0023aa4e"
Content-Length: 100
[description of changes]
```
## DELETE
> 删除文件
PUT 功能相反并且同样不带验证机制
```html
DELETE /file.html HTTP/1.1
```
## OPTIONS
> 查询支持的方法
查询指定的 URL 能够支持的方法
会返回 `Allow: GET, POST, HEAD, OPTIONS` 这样的内容
## CONNECT
> 要求在与代理服务器通信时建立隧道
使用 SSLSecure Sockets Layer安全套接层 TLSTransport Layer Security传输层安全协议把通信内容加密后经网络隧道传输
```html
CONNECT www.example.com:443 HTTP/1.1
```
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/dc00f70e-c5c8-4d20-baf1-2d70014a97e3.jpg" width=""/> </div><br>
## TRACE
> 追踪路径
服务器会将通信路径返回给客户端
发送请求时 Max-Forwards 首部字段中填入数值每经过一个服务器就会减 1当数值为 0 时就停止传输
通常不会使用 TRACE并且它容易受到 XST 攻击Cross-Site Tracing跨站追踪
- [rfc26169 Method Definitions](https://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html)
# HTTP 状态码
服务器返回的 **响应报文** 中第一行为状态行包含了状态码以及原因短语用来告知客户端请求的结果
| 状态码 | 类别 | 含义 |
| :---: | :---: | :---: |
| 1XX | Informational信息性状态码 | 接收的请求正在处理 |
| 2XX | Success成功状态码 | 请求正常处理完毕 |
| 3XX | Redirection重定向状态码 | 需要进行附加操作以完成请求 |
| 4XX | Client Error客户端错误状态码 | 服务器无法处理请求 |
| 5XX | Server Error服务器错误状态码 | 服务器处理请求出错 |
## 1XX 信息
- **100 Continue** 表明到目前为止都很正常客户端可以继续发送请求或者忽略这个响应
## 2XX 成功
- **200 OK**
- **204 No Content** 请求已经成功处理但是返回的响应报文不包含实体的主体部分一般在只需要从客户端往服务器发送信息而不需要返回数据时使用
- **206 Partial Content** 表示客户端进行了范围请求响应报文包含由 Content-Range 指定范围的实体内容
## 3XX 重定向
- **301 Moved Permanently** 永久性重定向
- **302 Found** 临时性重定向
- **303 See Other** 302 有着相同的功能但是 303 明确要求客户端应该采用 GET 方法获取资源
- 虽然 HTTP 协议规定 301302 状态下重定向时不允许把 POST 方法改成 GET 方法但是大多数浏览器都会在 301302 303 状态下的重定向把 POST 方法改成 GET 方法
- **304 Not Modified** 如果请求报文首部包含一些条件例如If-MatchIf-Modified-SinceIf-None-MatchIf-RangeIf-Unmodified-Since如果不满足条件则服务器会返回 304 状态码
- **307 Temporary Redirect** 临时重定向 302 的含义类似但是 307 要求浏览器不会把重定向请求的 POST 方法改成 GET 方法
## 4XX 客户端错误
- **400 Bad Request** 请求报文中存在语法错误
- **401 Unauthorized** 该状态码表示发送的请求需要有认证信息BASIC 认证DIGEST 认证如果之前已进行过一次请求则表示用户认证失败
- **403 Forbidden** 请求被拒绝
- **404 Not Found**
## 5XX 服务器错误
- **500 Internal Server Error** 服务器正在执行请求时发生错误
- **503 Service Unavailable** 服务器暂时处于超负载或正在进行停机维护现在无法处理请求
# HTTP 首部
4 种类型的首部字段通用首部字段请求首部字段响应首部字段和实体首部字段
各种首部字段及其含义如下不需要全记仅供查阅
## 通用首部字段
| 首部字段名 | 说明 |
| :--: | :--: |
| Cache-Control | 控制缓存的行为 |
| Connection | 控制不再转发给代理的首部字段管理持久连接|
| Date | 创建报文的日期时间 |
| Pragma | 报文指令 |
| Trailer | 报文末端的首部一览 |
| Transfer-Encoding | 指定报文主体的传输编码方式 |
| Upgrade | 升级为其他协议 |
| Via | 代理服务器的相关信息 |
| Warning | 错误通知 |
## 请求首部字段
| 首部字段名 | 说明 |
| :--: | :--: |
| Accept | 用户代理可处理的媒体类型 |
| Accept-Charset | 优先的字符集 |
| Accept-Encoding | 优先的内容编码 |
| Accept-Language | 优先的语言自然语言 |
| Authorization | Web 认证信息 |
| Expect | 期待服务器的特定行为 |
| From | 用户的电子邮箱地址 |
| Host | 请求资源所在服务器 |
| If-Match | 比较实体标记ETag |
| If-Modified-Since | 比较资源的更新时间 |
| If-None-Match | 比较实体标记 If-Match 相反 |
| If-Range | 资源未更新时发送实体 Byte 的范围请求 |
| If-Unmodified-Since | 比较资源的更新时间 If-Modified-Since 相反 |
| Max-Forwards | 最大传输逐跳数 |
| Proxy-Authorization | 代理服务器要求客户端的认证信息 |
| Range | 实体的字节范围请求 |
| Referer | 对请求中 URI 的原始获取方 |
| TE | 传输编码的优先级 |
| User-Agent | HTTP 客户端程序的信息 |
## 响应首部字段
| 首部字段名 | 说明 |
| :--: | :--: |
| Accept-Ranges | 是否接受字节范围请求 |
| Age | 推算资源创建经过时间 |
| ETag | 资源的匹配信息 |
| Location | 令客户端重定向至指定 URI |
| Proxy-Authenticate | 代理服务器对客户端的认证信息 |
| Retry-After | 对再次发起请求的时机要求 |
| Server | HTTP 服务器的安装信息 |
| Vary | 代理服务器缓存的管理信息 |
| WWW-Authenticate | 服务器对客户端的认证信息 |
## 实体首部字段
| 首部字段名 | 说明 |
| :--: | :--: |
| Allow | 资源可支持的 HTTP 方法 |
| Content-Encoding | 实体主体适用的编码方式 |
| Content-Language | 实体主体的自然语言 |
| Content-Length | 实体主体的大小 |
| Content-Location | 替代对应资源的 URI |
| Content-MD5 | 实体主体的报文摘要 |
| Content-Range | 实体主体的位置范围 |
| Content-Type | 实体主体的媒体类型 |
| Expires | 实体主体过期的日期时间 |
| Last-Modified | 资源的最后修改日期时间 |
# 具体应用
## 连接管理
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/HTTP1_x_Connections.png" width="800"/> </div><br>
### 1. 短连接与长连接
当浏览器访问一个包含多张图片的 HTML 页面时除了请求访问的 HTML 页面资源还会请求图片资源如果每进行一次 HTTP 通信就要新建一个 TCP 连接那么开销会很大
长连接只需要建立一次 TCP 连接就能进行多次 HTTP 通信
- HTTP/1.1 开始默认是长连接的如果要断开连接需要由客户端或者服务器端提出断开使用 `Connection : close`
- HTTP/1.1 之前默认是短连接的如果需要使用长连接则使用 `Connection : Keep-Alive`
### 2. 流水线
默认情况下HTTP 请求是按顺序发出的下一个请求只有在当前请求收到响应之后才会被发出由于受到网络延迟和带宽的限制在下一个请求被发送到服务器之前可能需要等待很长时间
流水线是在同一条长连接上连续发出请求而不用等待响应返回这样可以减少延迟
## Cookie
HTTP 协议是无状态的主要是为了让 HTTP 协议尽可能简单使得它能够处理大量事务HTTP/1.1 引入 Cookie 来保存状态信息
Cookie 是服务器发送到用户浏览器并保存在本地的一小块数据它会在浏览器之后向同一服务器再次发起请求时被携带上用于告知服务端两个请求是否来自同一浏览器由于之后每次请求都会需要携带 Cookie 数据因此会带来额外的性能开销尤其是在移动环境下
Cookie 曾一度用于客户端数据的存储因为当时并没有其它合适的存储办法而作为唯一的存储手段但现在随着现代浏览器开始支持各种各样的存储方式Cookie 渐渐被淘汰新的浏览器 API 已经允许开发者直接将数据存储到本地如使用 Web storage API本地存储和会话存储 IndexedDB
### 1. 用途
- 会话状态管理如用户登录状态购物车游戏分数或其它需要记录的信息
- 个性化设置如用户自定义设置主题等
- 浏览器行为跟踪如跟踪分析用户行为等
### 2. 创建过程
服务器发送的响应报文包含 Set-Cookie 首部字段客户端得到响应报文后把 Cookie 内容保存到浏览器中
```html
HTTP/1.0 200 OK
Content-type: text/html
Set-Cookie: yummy_cookie=choco
Set-Cookie: tasty_cookie=strawberry
[page content]
```
客户端之后对同一个服务器发送请求时会从浏览器中取出 Cookie 信息并通过 Cookie 请求首部字段发送给服务器
```html
GET /sample_page.html HTTP/1.1
Host: www.example.org
Cookie: yummy_cookie=choco; tasty_cookie=strawberry
```
### 3. 分类
- 会话期 Cookie浏览器关闭之后它会被自动删除也就是说它仅在会话期内有效
- 持久性 Cookie指定过期时间Expires或有效期max-age之后就成为了持久性的 Cookie
```html
Set-Cookie: id=a3fWa; Expires=Wed, 21 Oct 2015 07:28:00 GMT;
```
### 4. 作用域
Domain 标识指定了哪些主机可以接受 Cookie如果不指定默认为当前文档的主机不包含子域名如果指定了 Domain则一般包含子域名例如如果设置 Domain=mozilla.org Cookie 也包含在子域名中 developer.mozilla.org
Path 标识指定了主机下的哪些路径可以接受 Cookie URL 路径必须存在于请求 URL 以字符 %x2F ("/") 作为路径分隔符子路径也会被匹配例如设置 Path=/docs则以下地址都会匹配
- /docs
- /docs/Web/
- /docs/Web/HTTP
### 5. JavaScript
浏览器通过 `document.cookie` 属性可创建新的 Cookie也可通过该属性访问非 HttpOnly 标记的 Cookie
```html
document.cookie = "yummy_cookie=choco";
document.cookie = "tasty_cookie=strawberry";
console.log(document.cookie);
```
### 6. HttpOnly
标记为 HttpOnly Cookie 不能被 JavaScript 脚本调用跨站脚本攻击 (XSS) 常常使用 JavaScript `document.cookie` API 窃取用户的 Cookie 信息因此使用 HttpOnly 标记可以在一定程度上避免 XSS 攻击
```html
Set-Cookie: id=a3fWa; Expires=Wed, 21 Oct 2015 07:28:00 GMT; Secure; HttpOnly
```
### 7. Secure
标记为 Secure Cookie 只能通过被 HTTPS 协议加密过的请求发送给服务端但即便设置了 Secure 标记敏感信息也不应该通过 Cookie 传输因为 Cookie 有其固有的不安全性Secure 标记也无法提供确实的安全保障
### 8. Session
除了可以将用户信息通过 Cookie 存储在用户浏览器中也可以利用 Session 存储在服务器端存储在服务器端的信息更加安全
Session 可以存储在服务器上的文件数据库或者内存中也可以将 Session 存储在 Redis 这种内存型数据库中效率会更高
使用 Session 维护用户登录状态的过程如下
- 用户进行登录时用户提交包含用户名和密码的表单放入 HTTP 请求报文中
- 服务器验证该用户名和密码如果正确则把用户信息存储到 Redis 它在 Redis 中的 Key 称为 Session ID
- 服务器返回的响应报文的 Set-Cookie 首部字段包含了这个 Session ID客户端收到响应报文之后将该 Cookie 值存入浏览器中
- 客户端之后对同一个服务器进行请求时会包含该 Cookie 服务器收到之后提取出 Session ID Redis 中取出用户信息继续之前的业务操作
应该注意 Session ID 的安全性问题不能让它被恶意攻击者轻易获取那么就不能产生一个容易被猜到的 Session ID 此外还需要经常重新生成 Session ID在对安全性要求极高的场景下例如转账等操作除了使用 Session 管理用户状态之外还需要对用户进行重新验证比如重新输入密码或者使用短信验证码等方式
### 9. 浏览器禁用 Cookie
此时无法使用 Cookie 来保存用户信息只能使用 Session除此之外不能再将 Session ID 存放到 Cookie 而是使用 URL 重写技术 Session ID 作为 URL 的参数进行传递
### 10. Cookie Session 选择
- Cookie 只能存储 ASCII 码字符串 Session 则可以存储任何类型的数据因此在考虑数据复杂性时首选 Session
- Cookie 存储在浏览器中容易被恶意查看如果非要将一些隐私数据存在 Cookie 可以将 Cookie 值进行加密然后在服务器进行解密
- 对于大型网站如果用户所有的信息都存储在 Session 那么开销是非常大的因此不建议将所有的用户信息都存储到 Session
## 缓存
### 1. 优点
- 缓解服务器压力
- 降低客户端获取资源的延迟缓存通常位于内存中读取缓存的速度更快并且缓存服务器在地理位置上也有可能比源服务器来得近例如浏览器缓存
### 2. 实现方法
- 让代理服务器进行缓存
- 让客户端浏览器进行缓存
### 3. Cache-Control
HTTP/1.1 通过 Cache-Control 首部字段来控制缓存
**3.1 禁止进行缓存**
no-store 指令规定不能对请求或响应的任何一部分进行缓存
```html
Cache-Control: no-store
```
**3.2 强制确认缓存**
no-cache 指令规定缓存服务器需要先向源服务器验证缓存资源的有效性只有当缓存资源有效时才能使用该缓存对客户端的请求进行响应
```html
Cache-Control: no-cache
```
**3.3 私有缓存和公共缓存**
private 指令规定了将资源作为私有缓存只能被单独用户使用一般存储在用户浏览器中
```html
Cache-Control: private
```
public 指令规定了将资源作为公共缓存可以被多个用户使用一般存储在代理服务器中
```html
Cache-Control: public
```
**3.4 缓存过期机制**
max-age 指令出现在请求报文并且缓存资源的缓存时间小于该指令指定的时间那么就能接受该缓存
max-age 指令出现在响应报文表示缓存资源在缓存服务器中保存的时间
```html
Cache-Control: max-age=31536000
```
Expires 首部字段也可以用于告知缓存服务器该资源什么时候会过期
```html
Expires: Wed, 04 Jul 2012 08:26:05 GMT
```
- HTTP/1.1 会优先处理 max-age 指令
- HTTP/1.0 max-age 指令会被忽略掉
### 4. 缓存验证
需要先了解 ETag 首部字段的含义它是资源的唯一标识URL 不能唯一表示资源例如 `http://www.google.com/` 有中文和英文两个资源只有 ETag 才能对这两个资源进行唯一标识
```html
ETag: "82e22293907ce725faf67773957acd12"
```
可以将缓存资源的 ETag 值放入 If-None-Match 首部服务器收到该请求后判断缓存资源的 ETag 值和资源的最新 ETag 值是否一致如果一致则表示缓存资源有效返回 304 Not Modified
```html
If-None-Match: "82e22293907ce725faf67773957acd12"
```
Last-Modified 首部字段也可以用于缓存验证它包含在源服务器发送的响应报文中指示源服务器对资源的最后修改时间但是它是一种弱校验器因为只能精确到一秒所以它通常作为 ETag 的备用方案如果响应首部字段里含有这个信息客户端可以在后续的请求中带上 If-Modified-Since 来验证缓存服务器只在所请求的资源在给定的日期时间之后对内容进行过修改的情况下才会将资源返回状态码为 200 OK如果请求的资源从那时起未经修改那么返回一个不带有实体主体的 304 Not Modified 响应报文
```html
Last-Modified: Wed, 21 Oct 2015 07:28:00 GMT
```
```html
If-Modified-Since: Wed, 21 Oct 2015 07:28:00 GMT
```
## 内容协商
通过内容协商返回最合适的内容例如根据浏览器的默认语言选择返回中文界面还是英文界面
### 1. 类型
**1.1 服务端驱动型**
客户端设置特定的 HTTP 首部字段例如 AcceptAccept-CharsetAccept-EncodingAccept-Language服务器根据这些字段返回特定的资源
它存在以下问题
- 服务器很难知道客户端浏览器的全部信息
- 客户端提供的信息相当冗长HTTP/2 协议的首部压缩机制缓解了这个问题并且存在隐私风险HTTP 指纹识别技术
- 给定的资源需要返回不同的展现形式共享缓存的效率会降低而服务器端的实现会越来越复杂
**1.2 代理驱动型**
服务器返回 300 Multiple Choices 或者 406 Not Acceptable客户端从中选出最合适的那个资源
### 2. Vary
```html
Vary: Accept-Language
```
在使用内容协商的情况下只有当缓存服务器中的缓存满足内容协商条件时才能使用该缓存否则应该向源服务器请求该资源
例如一个客户端发送了一个包含 Accept-Language 首部字段的请求之后源服务器返回的响应包含 `Vary: Accept-Language` 内容缓存服务器对这个响应进行缓存之后在客户端下一次访问同一个 URL 资源并且 Accept-Language 与缓存中的对应的值相同时才会返回该缓存
## 内容编码
内容编码将实体主体进行压缩从而减少传输的数据量
常用的内容编码有gzipcompressdeflateidentity
浏览器发送 Accept-Encoding 首部其中包含有它所支持的压缩算法以及各自的优先级服务器则从中选择一种使用该算法对响应的消息主体进行压缩并且发送 Content-Encoding 首部来告知浏览器它选择了哪一种算法由于该内容协商过程是基于编码类型来选择资源的展现形式的响应报文的 Vary 首部字段至少要包含 Content-Encoding
## 范围请求
如果网络出现中断服务器只发送了一部分数据范围请求可以使得客户端只请求服务器未发送的那部分数据从而避免服务器重新发送所有数据
### 1. Range
在请求报文中添加 Range 首部字段指定请求的范围
```html
GET /z4d4kWk.jpg HTTP/1.1
Host: i.imgur.com
Range: bytes=0-1023
```
请求成功的话服务器返回的响应包含 206 Partial Content 状态码
```html
HTTP/1.1 206 Partial Content
Content-Range: bytes 0-1023/146515
Content-Length: 1024
...
(binary content)
```
### 2. Accept-Ranges
响应首部字段 Accept-Ranges 用于告知客户端是否能处理范围请求可以处理使用 bytes否则使用 none
```html
Accept-Ranges: bytes
```
### 3. 响应状态码
- 在请求成功的情况下服务器会返回 206 Partial Content 状态码
- 在请求的范围越界的情况下服务器会返回 416 Requested Range Not Satisfiable 状态码
- 在不支持范围请求的情况下服务器会返回 200 OK 状态码
## 分块传输编码
Chunked Transfer Encoding可以把数据分割成多块让浏览器逐步显示页面
## 多部分对象集合
一份报文主体内可含有多种类型的实体同时发送每个部分之间用 boundary 字段定义的分隔符进行分隔每个部分都可以有首部字段
例如上传多个表单时可以使用如下方式
```html
Content-Type: multipart/form-data; boundary=AaB03x
--AaB03x
Content-Disposition: form-data; name="submit-name"
Larry
--AaB03x
Content-Disposition: form-data; name="files"; filename="file1.txt"
Content-Type: text/plain
... contents of file1.txt ...
--AaB03x--
```
## 虚拟主机
HTTP/1.1 使用虚拟主机技术使得一台服务器拥有多个域名并且在逻辑上可以看成多个服务器
## 通信数据转发
### 1. 代理
代理服务器接受客户端的请求并且转发给其它服务器
使用代理的主要目的是
- 缓存
- 负载均衡
- 网络访问控制
- 访问日志记录
代理服务器分为正向代理和反向代理两种
- 用户察觉得到正向代理的存在
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/a314bb79-5b18-4e63-a976-3448bffa6f1b.png" width=""/> </div><br>
- 而反向代理一般位于内部网络中用户察觉不到
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/2d09a847-b854-439c-9198-b29c65810944.png" width=""/> </div><br>
### 2. 网关
与代理服务器不同的是网关服务器会将 HTTP 转化为其它协议进行通信从而请求其它非 HTTP 服务器的服务
### 3. 隧道
使用 SSL 等加密手段在客户端和服务器之间建立一条安全的通信线路
# HTTPS
HTTP 有以下安全性问题
- 使用明文进行通信内容可能会被窃听
- 不验证通信方的身份通信方的身份有可能遭遇伪装
- 无法证明报文的完整性报文有可能遭篡改
HTTPS 并不是新协议而是让 HTTP 先和 SSLSecure Sockets Layer通信再由 SSL TCP 通信也就是说 HTTPS 使用了隧道进行通信
通过使用 SSLHTTPS 具有了加密防窃听认证防伪装和完整性保护防篡改
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/ssl-offloading.jpg" width="700"/> </div><br>
## 加密
### 1. 对称密钥加密
对称密钥加密Symmetric-Key Encryption加密和解密使用同一密钥
- 优点运算速度快
- 缺点无法安全地将密钥传输给通信方
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/7fffa4b8-b36d-471f-ad0c-a88ee763bb76.png" width="600"/> </div><br>
### 2.非对称密钥加密
非对称密钥加密又称公开密钥加密Public-Key Encryption加密和解密使用不同的密钥
公开密钥所有人都可以获得通信发送方获得接收方的公开密钥之后就可以使用公开密钥进行加密接收方收到通信内容后使用私有密钥解密
非对称密钥除了用来加密还可以用来进行签名因为私有密钥无法被其他人获取因此通信发送方使用其私有密钥进行签名通信接收方使用发送方的公开密钥对签名进行解密就能判断这个签名是否正确
- 优点可以更安全地将公开密钥传输给通信发送方
- 缺点运算速度慢
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/39ccb299-ee99-4dd1-b8b4-2f9ec9495cb4.png" width="600"/> </div><br>
### 3. HTTPS 采用的加密方式
上面提到对称密钥加密方式的传输效率更高但是无法安全地将密钥 Secret Key 传输给通信方而非对称密钥加密方式可以保证传输的安全性因此我们可以利用非对称密钥加密方式将 Secret Key 传输给通信方HTTPS 采用混合的加密机制正是利用了上面提到的方案
- 使用非对称密钥加密方式传输对称密钥加密方式所需要的 Secret Key从而保证安全性;
- 获取到 Secret Key 再使用对称密钥加密方式进行通信从而保证效率下图中的 Session Key 就是 Secret Key
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/How-HTTPS-Works.png" width="600"/> </div><br>
## 认证
通过使用 **证书** 来对通信方进行认证
数字证书认证机构CACertificate Authority是客户端与服务器双方都可信赖的第三方机构
服务器的运营人员向 CA 提出公开密钥的申请CA 在判明提出申请者的身份之后会对已申请的公开密钥做数字签名然后分配这个已签名的公开密钥并将该公开密钥放入公开密钥证书后绑定在一起
进行 HTTPS 通信时服务器会把证书发送给客户端客户端取得其中的公开密钥之后先使用数字签名进行验证如果验证通过就可以开始通信了
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/2017-06-11-ca.png" width=""/> </div><br>
## 完整性保护
SSL 提供报文摘要功能来进行完整性保护
HTTP 也提供了 MD5 报文摘要功能但不是安全的例如报文内容被篡改之后同时重新计算 MD5 的值通信接收方是无法意识到发生了篡改
HTTPS 的报文摘要功能之所以安全是因为它结合了加密和认证这两个操作试想一下加密之后的报文遭到篡改之后也很难重新计算报文摘要因为无法轻易获取明文
## HTTPS 的缺点
- 因为需要进行加密解密等过程因此速度会更慢
- 需要支付证书授权的高额费用
# HTTP/2.0
## HTTP/1.x 缺陷
HTTP/1.x 实现简单是以牺牲性能为代价的
- 客户端需要使用多个连接才能实现并发和缩短延迟
- 不会压缩请求和响应首部从而导致不必要的网络流量
- 不支持有效的资源优先级致使底层 TCP 连接的利用率低下
## 二进制分帧层
HTTP/2.0 将报文分成 HEADERS 帧和 DATA 它们都是二进制格式的
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/86e6a91d-a285-447a-9345-c5484b8d0c47.png" width="400"/> </div><br>
在通信过程中只会有一个 TCP 连接存在它承载了任意数量的双向数据流Stream
- 一个数据流Stream都有一个唯一标识符和可选的优先级信息用于承载双向信息
- 消息Message是与逻辑请求或响应对应的完整的一系列帧
- Frame是最小的通信单位来自不同数据流的帧可以交错发送然后再根据每个帧头的数据流标识符重新组装
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/af198da1-2480-4043-b07f-a3b91a88b815.png" width="600"/> </div><br>
## 服务端推送
HTTP/2.0 在客户端请求一个资源时会把相关的资源一起发送给客户端客户端就不需要再次发起请求了例如客户端请求 page.html 页面服务端就把 script.js style.css 等与之相关的资源一起发给客户端
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/e3f1657c-80fc-4dfa-9643-bf51abd201c6.png" width="800"/> </div><br>
## 首部压缩
HTTP/1.1 的首部带有大量信息而且每次都要重复发送
HTTP/2.0 要求客户端和服务器同时维护和更新一个包含之前见过的首部字段表从而避免了重复传输
不仅如此HTTP/2.0 也使用 Huffman 编码对首部字段进行压缩
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/_u4E0B_u8F7D.png" width="600"/> </div><br>
# HTTP/1.1 新特性
详细内容请见上文
- 默认是长连接
- 支持流水线
- 支持同时打开多个 TCP 连接
- 支持虚拟主机
- 新增状态码 100
- 支持分块传输编码
- 新增缓存处理指令 max-age
# GET POST 比较
## 作用
GET 用于获取资源 POST 用于传输实体主体
## 参数
GET POST 的请求都能使用额外的参数但是 GET 的参数是以查询字符串出现在 URL POST 的参数存储在实体主体中不能因为 POST 参数存储在实体主体中就认为它的安全性更高因为照样可以通过一些抓包工具Fiddler查看
因为 URL 只支持 ASCII 因此 GET 的参数中如果存在中文等字符就需要先进行编码例如 `中文` 会转换为 `%E4%B8%AD%E6%96%87`而空格会转换为 `%20`POST 参数支持标准字符集
```
GET /test/demo_form.asp?name1=value1&name2=value2 HTTP/1.1
```
```
POST /test/demo_form.asp HTTP/1.1
Host: w3schools.com
name1=value1&name2=value2
```
## 安全
安全的 HTTP 方法不会改变服务器状态也就是说它只是可读的
GET 方法是安全的 POST 却不是因为 POST 的目的是传送实体主体内容这个内容可能是用户上传的表单数据上传成功之后服务器可能把这个数据存储到数据库中因此状态也就发生了改变
安全的方法除了 GET 之外还有HEADOPTIONS
不安全的方法除了 POST 之外还有 PUTDELETE
## 幂等性
幂等的 HTTP 方法同样的请求被执行一次与连续执行多次的效果是一样的服务器的状态也是一样的换句话说就是幂等方法不应该具有副作用统计用途除外
所有的安全方法也都是幂等的
在正确实现的条件下GETHEADPUT DELETE 等方法都是幂等的 POST 方法不是
GET /pageX HTTP/1.1 是幂等的连续调用多次客户端接收到的结果都是一样的
```
GET /pageX HTTP/1.1
GET /pageX HTTP/1.1
GET /pageX HTTP/1.1
GET /pageX HTTP/1.1
```
POST /add_row HTTP/1.1 不是幂等的如果调用多次就会增加多行记录
```
POST /add_row HTTP/1.1 -> Adds a 1nd row
POST /add_row HTTP/1.1 -> Adds a 2nd row
POST /add_row HTTP/1.1 -> Adds a 3rd row
```
DELETE /idX/delete HTTP/1.1 是幂等的即使不同的请求接收到的状态码不一样
```
DELETE /idX/delete HTTP/1.1 -> Returns 200 if idX exists
DELETE /idX/delete HTTP/1.1 -> Returns 404 as it just got deleted
DELETE /idX/delete HTTP/1.1 -> Returns 404
```
## 可缓存
如果要对响应进行缓存需要满足以下条件
- 请求报文的 HTTP 方法本身是可缓存的包括 GET HEAD但是 PUT DELETE 不可缓存POST 在多数情况下不可缓存的
- 响应报文的状态码是可缓存的包括200, 203, 204, 206, 300, 301, 404, 405, 410, 414, and 501
- 响应报文的 Cache-Control 首部字段没有指定不进行缓存
## XMLHttpRequest
为了阐述 POST GET 的另一个区别需要先了解 XMLHttpRequest
> XMLHttpRequest 是一个 API它为客户端提供了在客户端和服务器之间传输数据的功能它提供了一个通过 URL 来获取数据的简单方式并且不会使整个页面刷新这使得网页只更新一部分页面而不会打扰到用户XMLHttpRequest AJAX 中被大量使用
- 在使用 XMLHttpRequest POST 方法时浏览器会先发送 Header 再发送 Data但并不是所有浏览器会这么做例如火狐就不会
- GET 方法 Header Data 会一起发送
# 参考资料
- 上野宣. 图解 HTTP[M]. 人民邮电出版社, 2014.
- [MDN : HTTP](https://developer.mozilla.org/en-US/docs/Web/HTTP)
- [HTTP/2 简介](https://developers.google.com/web/fundamentals/performance/http2/?hl=zh-cn)
- [htmlspecialchars](http://php.net/manual/zh/function.htmlspecialchars.php)
- [Difference between file URI and URL in java](http://java2db.com/java-io/how-to-get-and-the-difference-between-file-uri-and-url-in-java)
- [How to Fix SQL Injection Using Java PreparedStatement & CallableStatement](https://software-security.sans.org/developer-how-to/fix-sql-injection-in-java-using-prepared-callable-statement)
- [浅谈 HTTP Get Post 的区别](https://www.cnblogs.com/hyddd/archive/2009/03/31/1426026.html)
- [Are http:// and www really necessary?](https://www.webdancers.com/are-http-and-www-necesary/)
- [HTTP (HyperText Transfer Protocol)](https://www.ntu.edu.sg/home/ehchua/programming/webprogramming/HTTP_Basics.html)
- [Web-VPN: Secure Proxies with SPDY & Chrome](https://www.igvita.com/2011/12/01/web-vpn-secure-proxies-with-spdy-chrome/)
- [File:HTTP persistent connection.svg](http://en.wikipedia.org/wiki/File:HTTP_persistent_connection.svg)
- [Proxy server](https://en.wikipedia.org/wiki/Proxy_server)
- [What Is This HTTPS/SSL Thing And Why Should You Care?](https://www.x-cart.com/blog/what-is-https-and-ssl.html)
- [What is SSL Offloading?](https://securebox.comodo.com/ssl-sniffing/ssl-offloading/)
- [Sun Directory Server Enterprise Edition 7.0 Reference - Key Encryption](https://docs.oracle.com/cd/E19424-01/820-4811/6ng8i26bn/index.html)
- [An Introduction to Mutual SSL Authentication](https://www.codeproject.com/Articles/326574/An-Introduction-to-Mutual-SSL-Authentication)
- [The Difference Between URLs and URIs](https://danielmiessler.com/study/url-uri/)
- [Cookie Session 的区别](https://juejin.im/entry/5766c29d6be3ff006a31b84e#comment)
- [COOKIE SESSION 有什么区别](https://www.zhihu.com/question/19786827)
- [Cookie/Session 的机制与安全](https://harttle.land/2015/08/10/cookie-session.html)
- [HTTPS 证书原理](https://shijianan.com/2017/06/11/https/)
- [What is the difference between a URI, a URL and a URN?](https://stackoverflow.com/questions/176264/what-is-the-difference-between-a-uri-a-url-and-a-urn)
- [XMLHttpRequest](https://developer.mozilla.org/zh-CN/docs/Web/API/XMLHttpRequest)
- [XMLHttpRequest (XHR) Uses Multiple Packets for HTTP POST?](https://blog.josephscott.org/2009/08/27/xmlhttprequest-xhr-uses-multiple-packets-for-http-post/)
- [Symmetric vs. Asymmetric Encryption What are differences?](https://www.ssl2buy.com/wiki/symmetric-vs-asymmetric-encryption-what-are-differences)
- [Web 性能优化与 HTTP/2](https://www.kancloud.cn/digest/web-performance-http2)
- [HTTP/2 简介](https://developers.google.com/web/fundamentals/performance/http2/?hl=zh-cn)
<div align="center"><img width="320px" src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/githubio/公众号二维码-2.png"></img></div>

View File

@ -1,627 +0,0 @@
<!-- GFM-TOC -->
* [概览](#一概览)
* [磁盘操作](#二磁盘操作)
* [字节操作](#三字节操作)
* [实现文件复制](#实现文件复制)
* [装饰者模式](#装饰者模式)
* [字符操作](#四字符操作)
* [编码与解码](#编码与解码)
* [String 的编码方式](#string-的编码方式)
* [Reader Writer](#reader--writer)
* [实现逐行输出文本文件的内容](#实现逐行输出文本文件的内容)
* [对象操作](#五对象操作)
* [序列化](#序列化)
* [Serializable](#serializable)
* [transient](#transient)
* [网络操作](#六网络操作)
* [InetAddress](#inetaddress)
* [URL](#url)
* [Sockets](#sockets)
* [Datagram](#datagram)
* [NIO](#七nio)
* [流与块](#流与块)
* [通道与缓冲区](#通道与缓冲区)
* [缓冲区状态变量](#缓冲区状态变量)
* [文件 NIO 实例](#文件-nio-实例)
* [选择器](#选择器)
* [套接字 NIO 实例](#套接字-nio-实例)
* [内存映射文件](#内存映射文件)
* [对比](#对比)
* [参考资料](#八参考资料)
<!-- GFM-TOC -->
# 概览
Java I/O 大概可以分成以下几类
- 磁盘操作File
- 字节操作InputStream OutputStream
- 字符操作Reader Writer
- 对象操作Serializable
- 网络操作Socket
- 新的输入/输出NIO
# 磁盘操作
File 类可以用于表示文件和目录的信息但是它不表示文件的内容
递归地列出一个目录下所有文件
```java
public static void listAllFiles(File dir) {
if (dir == null || !dir.exists()) {
return;
}
if (dir.isFile()) {
System.out.println(dir.getName());
return;
}
for (File file : dir.listFiles()) {
listAllFiles(file);
}
}
```
Java7 开始可以使用 Paths Files 代替 File
# 字节操作
## 实现文件复制
```java
public static void copyFile(String src, String dist) throws IOException {
FileInputStream in = new FileInputStream(src);
FileOutputStream out = new FileOutputStream(dist);
byte[] buffer = new byte[20 * 1024];
int cnt;
// read() 最多读取 buffer.length 个字节
// 返回的是实际读取的个数
// 返回 -1 的时候表示读到 eof即文件尾
while ((cnt = in.read(buffer, 0, buffer.length)) != -1) {
out.write(buffer, 0, cnt);
}
in.close();
out.close();
}
```
## 装饰者模式
Java I/O 使用了装饰者模式来实现 InputStream 为例
- InputStream 是抽象组件
- FileInputStream InputStream 的子类属于具体组件提供了字节流的输入操作
- FilterInputStream 属于抽象装饰者装饰者用于装饰组件为组件提供额外的功能例如 BufferedInputStream FileInputStream 提供缓存的功能
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/9709694b-db05-4cce-8d2f-1c8b09f4d921.png" width="650px"> </div><br>
实例化一个具有缓存功能的字节流对象时只需要在 FileInputStream 对象上再套一层 BufferedInputStream 对象即可
```java
FileInputStream fileInputStream = new FileInputStream(filePath);
BufferedInputStream bufferedInputStream = new BufferedInputStream(fileInputStream);
```
DataInputStream 装饰者提供了对更多数据类型进行输入的操作比如 intdouble 等基本类型
# 字符操作
## 编码与解码
编码就是把字符转换为字节而解码是把字节重新组合成字符
如果编码和解码过程使用不同的编码方式那么就出现了乱码
- GBK 编码中中文字符占 2 个字节英文字符占 1 个字节
- UTF-8 编码中中文字符占 3 个字节英文字符占 1 个字节
- UTF-16be 编码中中文字符和英文字符都占 2 个字节
UTF-16be 中的 be 指的是 Big Endian也就是大端相应地也有 UTF-16lele 指的是 Little Endian也就是小端
Java 的内存编码使用双字节编码 UTF-16be这不是指 Java 只支持这一种编码方式而是说 char 这种类型使用 UTF-16be 进行编码char 类型占 16 也就是两个字节Java 使用这种双字节编码是为了让一个中文或者一个英文都能使用一个 char 来存储
## String 的编码方式
String 可以看成一个字符序列可以指定一个编码方式将它编码为字节序列也可以指定一个编码方式将一个字节序列解码为 String
```java
String str1 = "中文";
byte[] bytes = str1.getBytes("UTF-8");
String str2 = new String(bytes, "UTF-8");
System.out.println(str2);
```
在调用无参数 getBytes() 方法时默认的编码方式不是 UTF-16be双字节编码的好处是可以使用一个 char 存储中文和英文而将 String 转为 bytes[] 字节数组就不再需要这个好处因此也就不再需要双字节编码getBytes() 的默认编码方式与平台有关一般为 UTF-8
```java
byte[] bytes = str1.getBytes();
```
## Reader Writer
不管是磁盘还是网络传输最小的存储单元都是字节而不是字符但是在程序中操作的通常是字符形式的数据因此需要提供对字符进行操作的方法
- InputStreamReader 实现从字节流解码成字符流
- OutputStreamWriter 实现字符流编码成为字节流
## 实现逐行输出文本文件的内容
```java
public static void readFileContent(String filePath) throws IOException {
FileReader fileReader = new FileReader(filePath);
BufferedReader bufferedReader = new BufferedReader(fileReader);
String line;
while ((line = bufferedReader.readLine()) != null) {
System.out.println(line);
}
// 装饰者模式使得 BufferedReader 组合了一个 Reader 对象
// 在调用 BufferedReader close() 方法时会去调用 Reader close() 方法
// 因此只要一个 close() 调用即可
bufferedReader.close();
}
```
# 对象操作
## 序列化
序列化就是将一个对象转换成字节序列方便存储和传输
- 序列化ObjectOutputStream.writeObject()
- 反序列化ObjectInputStream.readObject()
不会对静态变量进行序列化因为序列化只是保存对象的状态静态变量属于类的状态
## Serializable
序列化的类需要实现 Serializable 接口它只是一个标准没有任何方法需要实现但是如果不去实现它的话而进行序列化会抛出异常
```java
public static void main(String[] args) throws IOException, ClassNotFoundException {
A a1 = new A(123, "abc");
String objectFile = "file/a1";
ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream(objectFile));
objectOutputStream.writeObject(a1);
objectOutputStream.close();
ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream(objectFile));
A a2 = (A) objectInputStream.readObject();
objectInputStream.close();
System.out.println(a2);
}
private static class A implements Serializable {
private int x;
private String y;
A(int x, String y) {
this.x = x;
this.y = y;
}
@Override
public String toString() {
return "x = " + x + " " + "y = " + y;
}
}
```
## transient
transient 关键字可以使一些属性不会被序列化
ArrayList 中存储数据的数组 elementData 是用 transient 修饰的因为这个数组是动态扩展的并不是所有的空间都被使用因此就不需要所有的内容都被序列化通过重写序列化和反序列化方法使得可以只序列化数组中有内容的那部分数据
```java
private transient Object[] elementData;
```
# 网络操作
Java 中的网络支持
- InetAddress用于表示网络上的硬件资源 IP 地址
- URL统一资源定位符
- Sockets使用 TCP 协议实现网络通信
- Datagram使用 UDP 协议实现网络通信
## InetAddress
没有公有的构造函数只能通过静态方法来创建实例
```java
InetAddress.getByName(String host);
InetAddress.getByAddress(byte[] address);
```
## URL
可以直接从 URL 中读取字节流数据
```java
public static void main(String[] args) throws IOException {
URL url = new URL("http://www.baidu.com");
/* 字节流 */
InputStream is = url.openStream();
/* 字符流 */
InputStreamReader isr = new InputStreamReader(is, "utf-8");
/* 提供缓存功能 */
BufferedReader br = new BufferedReader(isr);
String line;
while ((line = br.readLine()) != null) {
System.out.println(line);
}
br.close();
}
```
## Sockets
- ServerSocket服务器端类
- Socket客户端类
- 服务器和客户端通过 InputStream OutputStream 进行输入输出
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/1e6affc4-18e5-4596-96ef-fb84c63bf88a.png" width="550px"> </div><br>
## Datagram
- DatagramSocket通信类
- DatagramPacket数据包类
# NIO
新的输入/输出 (NIO) 库是在 JDK 1.4 中引入的弥补了原来的 I/O 的不足提供了高速的面向块的 I/O
## 流与块
I/O NIO 最重要的区别是数据打包和传输的方式I/O 以流的方式处理数据 NIO 以块的方式处理数据
面向流的 I/O 一次处理一个字节数据一个输入流产生一个字节数据一个输出流消费一个字节数据为流式数据创建过滤器非常容易链接几个过滤器以便每个过滤器只负责复杂处理机制的一部分不利的一面是面向流的 I/O 通常相当慢
面向块的 I/O 一次处理一个数据块按块处理数据比按流处理数据要快得多但是面向块的 I/O 缺少一些面向流的 I/O 所具有的优雅性和简单性
I/O 包和 NIO 已经很好地集成了java.io.\* 已经以 NIO 为基础重新实现了所以现在它可以利用 NIO 的一些特性例如java.io.\* 包中的一些类包含以块的形式读写数据的方法这使得即使在面向流的系统中处理速度也会更快
## 通道与缓冲区
### 1. 通道
通道 Channel 是对原 I/O 包中的流的模拟可以通过它读取和写入数据
通道与流的不同之处在于流只能在一个方向上移动(一个流必须是 InputStream 或者 OutputStream 的子类)而通道是双向的可以用于读写或者同时用于读写
通道包括以下类型
- FileChannel从文件中读写数据
- DatagramChannel通过 UDP 读写网络中数据
- SocketChannel通过 TCP 读写网络中数据
- ServerSocketChannel可以监听新进来的 TCP 连接对每一个新进来的连接都会创建一个 SocketChannel
### 2. 缓冲区
发送给一个通道的所有数据都必须首先放到缓冲区中同样地从通道中读取的任何数据都要先读到缓冲区中也就是说不会直接对通道进行读写数据而是要先经过缓冲区
缓冲区实质上是一个数组但它不仅仅是一个数组缓冲区提供了对数据的结构化访问而且还可以跟踪系统的读/写进程
缓冲区包括以下类型
- ByteBuffer
- CharBuffer
- ShortBuffer
- IntBuffer
- LongBuffer
- FloatBuffer
- DoubleBuffer
## 缓冲区状态变量
- capacity最大容量
- position当前已经读写的字节数
- limit还可以读写的字节数
状态变量的改变过程举例
新建一个大小为 8 个字节的缓冲区此时 position 0 limit = capacity = 8capacity 变量不会改变下面的讨论会忽略它
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/1bea398f-17a7-4f67-a90b-9e2d243eaa9a.png"/> </div><br>
从输入通道中读取 5 个字节数据写入缓冲区中此时 position 5limit 保持不变
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/80804f52-8815-4096-b506-48eef3eed5c6.png"/> </div><br>
在将缓冲区的数据写到输出通道之前需要先调用 flip() 方法这个方法将 limit 设置为当前 position并将 position 设置为 0
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/952e06bd-5a65-4cab-82e4-dd1536462f38.png"/> </div><br>
从缓冲区中取 4 个字节到输出缓冲中此时 position 设为 4
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/b5bdcbe2-b958-4aef-9151-6ad963cb28b4.png"/> </div><br>
最后需要调用 clear() 方法来清空缓冲区此时 position limit 都被设置为最初位置
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/67bf5487-c45d-49b6-b9c0-a058d8c68902.png"/> </div><br>
## 文件 NIO 实例
以下展示了使用 NIO 快速复制文件的实例
```java
public static void fastCopy(String src, String dist) throws IOException {
/* 获得源文件的输入字节流 */
FileInputStream fin = new FileInputStream(src);
/* 获取输入字节流的文件通道 */
FileChannel fcin = fin.getChannel();
/* 获取目标文件的输出字节流 */
FileOutputStream fout = new FileOutputStream(dist);
/* 获取输出字节流的文件通道 */
FileChannel fcout = fout.getChannel();
/* 为缓冲区分配 1024 个字节 */
ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
while (true) {
/* 从输入通道中读取数据到缓冲区中 */
int r = fcin.read(buffer);
/* read() 返回 -1 表示 EOF */
if (r == -1) {
break;
}
/* 切换读写 */
buffer.flip();
/* 把缓冲区的内容写入输出文件中 */
fcout.write(buffer);
/* 清空缓冲区 */
buffer.clear();
}
}
```
## 选择器
NIO 常常被叫做非阻塞 IO主要是因为 NIO 在网络通信中的非阻塞特性被广泛使用
NIO 实现了 IO 多路复用中的 Reactor 模型一个线程 Thread 使用一个选择器 Selector 通过轮询的方式去监听多个通道 Channel 上的事件从而让一个线程就可以处理多个事件
通过配置监听的通道 Channel 为非阻塞那么当 Channel 上的 IO 事件还未到达时就不会进入阻塞状态一直等待而是继续轮询其它 Channel找到 IO 事件已经到达的 Channel 执行
因为创建和切换线程的开销很大因此使用一个线程来处理多个事件而不是一个线程处理一个事件对于 IO 密集型的应用具有很好地性能
应该注意的是只有套接字 Channel 才能配置为非阻塞 FileChannel 不能 FileChannel 配置非阻塞也没有意义
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/093f9e57-429c-413a-83ee-c689ba596cef.png" width="350px"> </div><br>
### 1. 创建选择器
```java
Selector selector = Selector.open();
```
### 2. 将通道注册到选择器上
```java
ServerSocketChannel ssChannel = ServerSocketChannel.open();
ssChannel.configureBlocking(false);
ssChannel.register(selector, SelectionKey.OP_ACCEPT);
```
通道必须配置为非阻塞模式否则使用选择器就没有任何意义了因为如果通道在某个事件上被阻塞那么服务器就不能响应其它事件必须等待这个事件处理完毕才能去处理其它事件显然这和选择器的作用背道而驰
在将通道注册到选择器上时还需要指定要注册的具体事件主要有以下几类
- SelectionKey.OP_CONNECT
- SelectionKey.OP_ACCEPT
- SelectionKey.OP_READ
- SelectionKey.OP_WRITE
它们在 SelectionKey 的定义如下
```java
public static final int OP_READ = 1 << 0;
public static final int OP_WRITE = 1 << 2;
public static final int OP_CONNECT = 1 << 3;
public static final int OP_ACCEPT = 1 << 4;
```
可以看出每个事件可以被当成一个位域从而组成事件集整数例如
```java
int interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE;
```
### 3. 监听事件
```java
int num = selector.select();
```
使用 select() 来监听到达的事件它会一直阻塞直到有至少一个事件到达
### 4. 获取到达的事件
```java
Set<SelectionKey> keys = selector.selectedKeys();
Iterator<SelectionKey> keyIterator = keys.iterator();
while (keyIterator.hasNext()) {
SelectionKey key = keyIterator.next();
if (key.isAcceptable()) {
// ...
} else if (key.isReadable()) {
// ...
}
keyIterator.remove();
}
```
### 5. 事件循环
因为一次 select() 调用不能处理完所有的事件并且服务器端有可能需要一直监听事件因此服务器端处理事件的代码一般会放在一个死循环内
```java
while (true) {
int num = selector.select();
Set<SelectionKey> keys = selector.selectedKeys();
Iterator<SelectionKey> keyIterator = keys.iterator();
while (keyIterator.hasNext()) {
SelectionKey key = keyIterator.next();
if (key.isAcceptable()) {
// ...
} else if (key.isReadable()) {
// ...
}
keyIterator.remove();
}
}
```
## 套接字 NIO 实例
```java
public class NIOServer {
public static void main(String[] args) throws IOException {
Selector selector = Selector.open();
ServerSocketChannel ssChannel = ServerSocketChannel.open();
ssChannel.configureBlocking(false);
ssChannel.register(selector, SelectionKey.OP_ACCEPT);
ServerSocket serverSocket = ssChannel.socket();
InetSocketAddress address = new InetSocketAddress("127.0.0.1", 8888);
serverSocket.bind(address);
while (true) {
selector.select();
Set<SelectionKey> keys = selector.selectedKeys();
Iterator<SelectionKey> keyIterator = keys.iterator();
while (keyIterator.hasNext()) {
SelectionKey key = keyIterator.next();
if (key.isAcceptable()) {
ServerSocketChannel ssChannel1 = (ServerSocketChannel) key.channel();
// 服务器会为每个新连接创建一个 SocketChannel
SocketChannel sChannel = ssChannel1.accept();
sChannel.configureBlocking(false);
// 这个新连接主要用于从客户端读取数据
sChannel.register(selector, SelectionKey.OP_READ);
} else if (key.isReadable()) {
SocketChannel sChannel = (SocketChannel) key.channel();
System.out.println(readDataFromSocketChannel(sChannel));
sChannel.close();
}
keyIterator.remove();
}
}
}
private static String readDataFromSocketChannel(SocketChannel sChannel) throws IOException {
ByteBuffer buffer = ByteBuffer.allocate(1024);
StringBuilder data = new StringBuilder();
while (true) {
buffer.clear();
int n = sChannel.read(buffer);
if (n == -1) {
break;
}
buffer.flip();
int limit = buffer.limit();
char[] dst = new char[limit];
for (int i = 0; i < limit; i++) {
dst[i] = (char) buffer.get(i);
}
data.append(dst);
buffer.clear();
}
return data.toString();
}
}
```
```java
public class NIOClient {
public static void main(String[] args) throws IOException {
Socket socket = new Socket("127.0.0.1", 8888);
OutputStream out = socket.getOutputStream();
String s = "hello world";
out.write(s.getBytes());
out.close();
}
}
```
## 内存映射文件
内存映射文件 I/O 是一种读和写文件数据的方法它可以比常规的基于流或者基于通道的 I/O 快得多
向内存映射文件写入可能是危险的只是改变数组的单个元素这样的简单操作就可能会直接修改磁盘上的文件修改数据与将数据保存到磁盘是没有分开的
下面代码行将文件的前 1024 个字节映射到内存中map() 方法返回一个 MappedByteBuffer它是 ByteBuffer 的子类因此可以像使用其他任何 ByteBuffer 一样使用新映射的缓冲区操作系统会在需要时负责执行映射
```java
MappedByteBuffer mbb = fc.map(FileChannel.MapMode.READ_WRITE, 0, 1024);
```
## 对比
NIO 与普通 I/O 的区别主要有以下两点
- NIO 是非阻塞的
- NIO 面向块I/O 面向流
# 参考资料
- Eckel B, 埃克尔, 昊鹏, . Java 编程思想 [M]. 机械工业出版社, 2002.
- [IBM: NIO 入门](https://www.ibm.com/developerworks/cn/education/java/j-nio/j-nio.html)
- [Java NIO Tutorial](http://tutorials.jenkov.com/java-nio/index.html)
- [Java NIO 浅析](https://tech.meituan.com/nio.html)
- [IBM: 深入分析 Java I/O 的工作机制](https://www.ibm.com/developerworks/cn/java/j-lo-javaio/index.html)
- [IBM: 深入分析 Java 中的中文编码问题](https://www.ibm.com/developerworks/cn/java/j-lo-chinesecoding/index.html)
- [IBM: Java 序列化的高级认识](https://www.ibm.com/developerworks/cn/java/j-lo-serial/index.html)
- [NIO 与传统 IO 的区别](http://blog.csdn.net/shimiso/article/details/24990499)
- [Decorator Design Pattern](http://stg-tud.github.io/sedc/Lecture/ws13-14/5.3-Decorator.html#mode=document)
- [Socket Multicast](http://labojava.blogspot.com/2012/12/socket-multicast.html)
<div align="center"><img width="320px" src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/githubio/公众号二维码-2.png"></img></div>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,760 +0,0 @@
<!-- GFM-TOC -->
* [运行时数据区域](#一运行时数据区域)
* [程序计数器](#程序计数器)
* [Java 虚拟机栈](#java-虚拟机栈)
* [本地方法栈](#本地方法栈)
* [](#)
* [方法区](#方法区)
* [运行时常量池](#运行时常量池)
* [直接内存](#直接内存)
* [垃圾收集](#二垃圾收集)
* [判断一个对象是否可被回收](#判断一个对象是否可被回收)
* [引用类型](#引用类型)
* [垃圾收集算法](#垃圾收集算法)
* [垃圾收集器](#垃圾收集器)
* [内存分配与回收策略](#三内存分配与回收策略)
* [Minor GC Full GC](#minor-gc--full-gc)
* [内存分配策略](#内存分配策略)
* [Full GC 的触发条件](#full-gc-的触发条件)
* [类加载机制](#四类加载机制)
* [类的生命周期](#类的生命周期)
* [类加载过程](#类加载过程)
* [类初始化时机](#类初始化时机)
* [类与类加载器](#类与类加载器)
* [类加载器分类](#类加载器分类)
* [双亲委派模型](#双亲委派模型)
* [自定义类加载器实现](#自定义类加载器实现)
* [参考资料](#参考资料)
<!-- GFM-TOC -->
本文大部分内容参考 **周志明深入理解 Java 虚拟机** 想要深入学习的话请看原书
# 运行时数据区域
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/5778d113-8e13-4c53-b5bf-801e58080b97.png" width="400px"> </div><br>
## 程序计数器
记录正在执行的虚拟机字节码指令的地址如果正在执行的是本地方法则为空
## Java 虚拟机栈
每个 Java 方法在执行的同时会创建一个栈帧用于存储局部变量表操作数栈常量池引用等信息从方法调用直至执行完成的过程对应着一个栈帧在 Java 虚拟机栈中入栈和出栈的过程
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/8442519f-0b4d-48f4-8229-56f984363c69.png" width="400px"> </div><br>
可以通过 -Xss 这个虚拟机参数来指定每个线程的 Java 虚拟机栈内存大小 JDK 1.4 中默认为 256K而在 JDK 1.5+ 默认为 1M
```java
java -Xss2M HackTheJava
```
该区域可能抛出以下异常
- 当线程请求的栈深度超过最大值会抛出 StackOverflowError 异常
- 栈进行动态扩展时如果无法申请到足够内存会抛出 OutOfMemoryError 异常
## 本地方法栈
本地方法栈与 Java 虚拟机栈类似它们之间的区别只不过是本地方法栈为本地方法服务
本地方法一般是用其它语言CC++ 或汇编语言等编写的并且被编译为基于本机硬件和操作系统的程序对待这些方法需要特别处理
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/66a6899d-c6b0-4a47-8569-9d08f0baf86c.png" width="300px"> </div><br>
##
所有对象都在这里分配内存是垃圾收集的主要区域"GC 堆"
现代的垃圾收集器基本都是采用分代收集算法其主要的思想是针对不同类型的对象采取不同的垃圾回收算法可以将堆分成两块
- 新生代Young Generation
- 老年代Old Generation
堆不需要连续内存并且可以动态增加其内存增加失败会抛出 OutOfMemoryError 异常
可以通过 -Xms -Xmx 这两个虚拟机参数来指定一个程序的堆内存大小第一个参数设置初始值第二个参数设置最大值
```java
java -Xms1M -Xmx2M HackTheJava
```
## 方法区
用于存放已被加载的类信息常量静态变量即时编译器编译后的代码等数据
和堆一样不需要连续的内存并且可以动态扩展动态扩展失败一样会抛出 OutOfMemoryError 异常
对这块区域进行垃圾回收的主要目标是对常量池的回收和对类的卸载但是一般比较难实现
HotSpot 虚拟机把它当成永久代来进行垃圾回收但很难确定永久代的大小因为它受到很多因素影响并且每次 Full GC 之后永久代的大小都会改变所以经常会抛出 OutOfMemoryError 异常为了更容易管理方法区 JDK 1.8 开始移除永久代并把方法区移至元空间它位于本地内存中而不是虚拟机内存中
方法区是一个 JVM 规范永久代与元空间都是其一种实现方式 JDK 1.8 之后原来永久代的数据被分到了堆和元空间中元空间存储类的元信息静态变量和常量池等放入堆中
## 运行时常量池
运行时常量池是方法区的一部分
Class 文件中的常量池编译器生成的字面量和符号引用会在类加载后被放入这个区域
除了在编译期生成的常量还允许动态生成例如 String 类的 intern()
## 直接内存
JDK 1.4 中新引入了 NIO 它可以使用 Native 函数库直接分配堆外内存然后通过 Java 堆里的 DirectByteBuffer 对象作为这块内存的引用进行操作这样能在一些场景中显著提高性能因为避免了在堆内存和堆外内存来回拷贝数据
# 垃圾收集
垃圾收集主要是针对堆和方法区进行程序计数器虚拟机栈和本地方法栈这三个区域属于线程私有的只存在于线程的生命周期内线程结束之后就会消失因此不需要对这三个区域进行垃圾回收
## 判断一个对象是否可被回收
### 1. 引用计数算法
为对象添加一个引用计数器当对象增加一个引用时计数器加 1引用失效时计数器减 1引用计数为 0 的对象可被回收
在两个对象出现循环引用的情况下此时引用计数器永远不为 0导致无法对它们进行回收正是因为循环引用的存在因此 Java 虚拟机不使用引用计数算法
```java
public class Test {
public Object instance = null;
public static void main(String[] args) {
Test a = new Test();
Test b = new Test();
a.instance = b;
b.instance = a;
a = null;
b = null;
doSomething();
}
}
```
在上述代码中a b 引用的对象实例互相持有了对象的引用因此当我们把对 a 对象与 b 对象的引用去除之后由于两个对象还存在互相之间的引用导致两个 Test 对象无法被回收
### 2. 可达性分析算法
GC Roots 为起始点进行搜索可达的对象都是存活的不可达的对象可被回收
Java 虚拟机使用该算法来判断对象是否可被回收GC Roots 一般包含以下内容
- 虚拟机栈中局部变量表中引用的对象
- 本地方法栈中 JNI 中引用的对象
- 方法区中类静态属性引用的对象
- 方法区中的常量引用的对象
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/83d909d2-3858-4fe1-8ff4-16471db0b180.png" width="350px"> </div><br>
### 3. 方法区的回收
因为方法区主要存放永久代对象而永久代对象的回收率比新生代低很多所以在方法区上进行回收性价比不高
主要是对常量池的回收和对类的卸载
为了避免内存溢出在大量使用反射和动态代理的场景都需要虚拟机具备类卸载功能
类的卸载条件很多需要满足以下三个条件并且满足了条件也不一定会被卸载
- 该类所有的实例都已经被回收此时堆中不存在该类的任何实例
- 加载该类的 ClassLoader 已经被回收
- 该类对应的 Class 对象没有在任何地方被引用也就无法在任何地方通过反射访问该类方法
### 4. finalize()
类似 C++ 的析构函数用于关闭外部资源但是 try-finally 等方式可以做得更好并且该方法运行代价很高不确定性大无法保证各个对象的调用顺序因此最好不要使用
当一个对象可被回收时如果需要执行该对象的 finalize() 方法那么就有可能在该方法中让对象重新被引用从而实现自救自救只能进行一次如果回收的对象之前调用了 finalize() 方法自救后面回收时不会再调用该方法
## 引用类型
无论是通过引用计数算法判断对象的引用数量还是通过可达性分析算法判断对象是否可达判定对象是否可被回收都与引用有关
Java 提供了四种强度不同的引用类型
### 1. 强引用
被强引用关联的对象不会被回收
使用 new 一个新对象的方式来创建强引用
```java
Object obj = new Object();
```
### 2. 软引用
被软引用关联的对象只有在内存不够的情况下才会被回收
使用 SoftReference 类来创建软引用
```java
Object obj = new Object();
SoftReference<Object> sf = new SoftReference<Object>(obj);
obj = null; // 使对象只被软引用关联
```
### 3. 弱引用
被弱引用关联的对象一定会被回收也就是说它只能存活到下一次垃圾回收发生之前
使用 WeakReference 类来创建弱引用
```java
Object obj = new Object();
WeakReference<Object> wf = new WeakReference<Object>(obj);
obj = null;
```
### 4. 虚引用
又称为幽灵引用或者幻影引用一个对象是否有虚引用的存在不会对其生存时间造成影响也无法通过虚引用得到一个对象
为一个对象设置虚引用的唯一目的是能在这个对象被回收时收到一个系统通知
使用 PhantomReference 来创建虚引用
```java
Object obj = new Object();
PhantomReference<Object> pf = new PhantomReference<Object>(obj, null);
obj = null;
```
## 垃圾收集算法
### 1. 标记 - 清除
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/005b481b-502b-4e3f-985d-d043c2b330aa.png" width="400px"> </div><br>
在标记阶段程序会检查每个对象是否为活动对象如果是活动对象则程序会在对象头部打上标记
在清除阶段会进行对象回收并取消标志位另外还会判断回收后的分块与前一个空闲分块是否连续若连续会合并这两个分块回收对象就是把对象作为分块连接到被称为 空闲链表 的单向链表之后进行分配时只需要遍历这个空闲链表就可以找到分块
在分配时程序会搜索空闲链表寻找空间大于等于新对象大小 size 的块 block如果它找到的块等于 size会直接返回这个分块如果找到的块大于 size会将块分割成大小为 size (block - size) 的两部分返回大小为 size 的分块并把大小为 (block - size) 的块返回给空闲链表
不足
- 标记和清除过程效率都不高
- 会产生大量不连续的内存碎片导致无法给大对象分配内存
### 2. 标记 - 整理
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/ccd773a5-ad38-4022-895c-7ac318f31437.png" width="400px"> </div><br>
让所有存活的对象都向一端移动然后直接清理掉端边界以外的内存
优点:
- 不会产生内存碎片
不足:
- 需要移动大量对象处理效率比较低
### 3. 复制
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/b2b77b9e-958c-4016-8ae5-9c6edd83871e.png" width="400px"> </div><br>
将内存划分为大小相等的两块每次只使用其中一块当这一块内存用完了就将还存活的对象复制到另一块上面然后再把使用过的内存空间进行一次清理
主要不足是只使用了内存的一半
现在的商业虚拟机都采用这种收集算法回收新生代但是并不是划分为大小相等的两块而是一块较大的 Eden 空间和两块较小的 Survivor 空间每次使用 Eden 和其中一块 Survivor在回收时 Eden Survivor 中还存活着的对象全部复制到另一块 Survivor 最后清理 Eden 和使用过的那一块 Survivor
HotSpot 虚拟机的 Eden Survivor 大小比例默认为 8:1保证了内存的利用率达到 90%如果每次回收有多于 10% 的对象存活那么一块 Survivor 就不够用了此时需要依赖于老年代进行空间分配担保也就是借用老年代的空间存储放不下的对象
### 4. 分代收集
现在的商业虚拟机采用分代收集算法它根据对象存活周期将内存划分为几块不同块采用适当的收集算法
一般将堆分为新生代和老年代
- 新生代使用复制算法
- 老年代使用标记 - 清除 或者 标记 - 整理 算法
## 垃圾收集器
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/c625baa0-dde6-449e-93df-c3a67f2f430f.jpg" width=""/> </div><br>
以上是 HotSpot 虚拟机中的 7 个垃圾收集器连线表示垃圾收集器可以配合使用
- 单线程与多线程单线程指的是垃圾收集器只使用一个线程而多线程使用多个线程
- 串行与并行串行指的是垃圾收集器与用户程序交替执行这意味着在执行垃圾收集的时候需要停顿用户程序并行指的是垃圾收集器和用户程序同时执行除了 CMS G1 之外其它垃圾收集器都是以串行的方式执行
### 1. Serial 收集器
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/22fda4ae-4dd5-489d-ab10-9ebfdad22ae0.jpg" width=""/> </div><br>
Serial 翻译为串行也就是说它以串行的方式执行
它是单线程的收集器只会使用一个线程进行垃圾收集工作
它的优点是简单高效在单个 CPU 环境下由于没有线程交互的开销因此拥有最高的单线程收集效率
它是 Client 场景下的默认新生代收集器因为在该场景下内存一般来说不会很大它收集一两百兆垃圾的停顿时间可以控制在一百多毫秒以内只要不是太频繁这点停顿时间是可以接受的
### 2. ParNew 收集器
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/81538cd5-1bcf-4e31-86e5-e198df1e013b.jpg" width=""/> </div><br>
它是 Serial 收集器的多线程版本
它是 Server 场景下默认的新生代收集器除了性能原因外主要是因为除了 Serial 收集器只有它能与 CMS 收集器配合使用
### 3. Parallel Scavenge 收集器
ParNew 一样是多线程收集器
其它收集器目标是尽可能缩短垃圾收集时用户线程的停顿时间而它的目标是达到一个可控制的吞吐量因此它被称为吞吐量优先收集器这里的吞吐量指 CPU 用于运行用户程序的时间占总时间的比值
停顿时间越短就越适合需要与用户交互的程序良好的响应速度能提升用户体验而高吞吐量则可以高效率地利用 CPU 时间尽快完成程序的运算任务适合在后台运算而不需要太多交互的任务
缩短停顿时间是以牺牲吞吐量和新生代空间来换取的新生代空间变小垃圾回收变得频繁导致吞吐量下降
可以通过一个开关参数打开 GC 自适应的调节策略GC Ergonomics就不需要手工指定新生代的大小-XmnEden Survivor 区的比例晋升老年代对象年龄等细节参数了虚拟机会根据当前系统的运行情况收集性能监控信息动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量
### 4. Serial Old 收集器
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/08f32fd3-f736-4a67-81ca-295b2a7972f2.jpg" width=""/> </div><br>
Serial 收集器的老年代版本也是给 Client 场景下的虚拟机使用如果用在 Server 场景下它有两大用途
- JDK 1.5 以及之前版本Parallel Old 诞生以前中与 Parallel Scavenge 收集器搭配使用
- 作为 CMS 收集器的后备预案在并发收集发生 Concurrent Mode Failure 时使用
### 5. Parallel Old 收集器
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/278fe431-af88-4a95-a895-9c3b80117de3.jpg" width=""/> </div><br>
Parallel Scavenge 收集器的老年代版本
在注重吞吐量以及 CPU 资源敏感的场合都可以优先考虑 Parallel Scavenge Parallel Old 收集器
### 6. CMS 收集器
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/62e77997-6957-4b68-8d12-bfd609bb2c68.jpg" width=""/> </div><br>
CMSConcurrent Mark SweepMark Sweep 指的是标记 - 清除算法
分为以下四个流程
- 初始标记仅仅只是标记一下 GC Roots 能直接关联到的对象速度很快需要停顿
- 并发标记进行 GC Roots Tracing 的过程它在整个回收过程中耗时最长不需要停顿
- 重新标记为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录需要停顿
- 并发清除不需要停顿
在整个过程中耗时最长的并发标记和并发清除过程中收集器线程都可以与用户线程一起工作不需要进行停顿
具有以下缺点
- 吞吐量低低停顿时间是以牺牲吞吐量为代价的导致 CPU 利用率不够高
- 无法处理浮动垃圾可能出现 Concurrent Mode Failure浮动垃圾是指并发清除阶段由于用户线程继续运行而产生的垃圾这部分垃圾只能到下一次 GC 时才能进行回收由于浮动垃圾的存在因此需要预留出一部分内存意味着 CMS 收集不能像其它收集器那样等待老年代快满的时候再回收如果预留的内存不够存放浮动垃圾就会出现 Concurrent Mode Failure这时虚拟机将临时启用 Serial Old 来替代 CMS
- 标记 - 清除算法导致的空间碎片往往出现老年代空间剩余但无法找到足够大连续空间来分配当前对象不得不提前触发一次 Full GC
### 7. G1 收集器
G1Garbage-First它是一款面向服务端应用的垃圾收集器在多 CPU 和大内存的场景下有很好的性能HotSpot 开发团队赋予它的使命是未来可以替换掉 CMS 收集器
堆被分为新生代和老年代其它收集器进行收集的范围都是整个新生代或者老年代 G1 可以直接对新生代和老年代一起回收
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/4cf711a8-7ab2-4152-b85c-d5c226733807.png" width="600"/> </div><br>
G1 把堆划分成多个大小相等的独立区域Region新生代和老年代不再物理隔离
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/9bbddeeb-e939-41f0-8e8e-2b1a0aa7e0a7.png" width="600"/> </div><br>
通过引入 Region 的概念从而将原来的一整块内存空间划分成多个的小空间使得每个小空间可以单独进行垃圾回收这种划分方法带来了很大的灵活性使得可预测的停顿时间模型成为可能通过记录每个 Region 垃圾回收时间以及回收所获得的空间这两个值是通过过去回收的经验获得并维护一个优先列表每次根据允许的收集时间优先回收价值最大的 Region
每个 Region 都有一个 Remembered Set用来记录该 Region 对象的引用对象所在的 Region通过使用 Remembered Set在做可达性分析的时候就可以避免全堆扫描
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/f99ee771-c56f-47fb-9148-c0036695b5fe.jpg" width=""/> </div><br>
如果不计算维护 Remembered Set 的操作G1 收集器的运作大致可划分为以下几个步骤
- 初始标记
- 并发标记
- 最终标记为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录虚拟机将这段时间对象变化记录在线程的 Remembered Set Logs 里面最终标记阶段需要把 Remembered Set Logs 的数据合并到 Remembered Set 这阶段需要停顿线程但是可并行执行
- 筛选回收首先对各个 Region 中的回收价值和成本进行排序根据用户所期望的 GC 停顿时间来制定回收计划此阶段其实也可以做到与用户程序一起并发执行但是因为只回收一部分 Region时间是用户可控制的而且停顿用户线程将大幅度提高收集效率
具备如下特点
- 空间整合整体来看是基于标记 - 整理算法实现的收集器从局部两个 Region 之间上来看是基于复制算法实现的这意味着运行期间不会产生内存空间碎片
- 可预测的停顿能让使用者明确指定在一个长度为 M 毫秒的时间片段内消耗在 GC 上的时间不得超过 N 毫秒
# 内存分配与回收策略
## Minor GC Full GC
- Minor GC回收新生代因为新生代对象存活时间很短因此 Minor GC 会频繁执行执行的速度一般也会比较快
- Full GC回收老年代和新生代老年代对象其存活时间长因此 Full GC 很少执行执行速度会比 Minor GC 慢很多
## 内存分配策略
### 1. 对象优先在 Eden 分配
大多数情况下对象在新生代 Eden 上分配 Eden 空间不够时发起 Minor GC
### 2. 大对象直接进入老年代
大对象是指需要连续内存空间的对象最典型的大对象是那种很长的字符串以及数组
经常出现大对象会提前触发垃圾收集以获取足够的连续空间分配给大对象
-XX:PretenureSizeThreshold大于此值的对象直接在老年代分配避免在 Eden Survivor 之间的大量内存复制
### 3. 长期存活的对象进入老年代
为对象定义年龄计数器对象在 Eden 出生并经过 Minor GC 依然存活将移动到 Survivor 年龄就增加 1 增加到一定年龄则移动到老年代中
-XX:MaxTenuringThreshold 用来定义年龄的阈值
### 4. 动态对象年龄判定
虚拟机并不是永远要求对象的年龄必须达到 MaxTenuringThreshold 才能晋升老年代如果在 Survivor 中相同年龄所有对象大小的总和大于 Survivor 空间的一半则年龄大于或等于该年龄的对象可以直接进入老年代无需等到 MaxTenuringThreshold 中要求的年龄
### 5. 空间分配担保
在发生 Minor GC 之前虚拟机先检查老年代最大可用的连续空间是否大于新生代所有对象总空间如果条件成立的话那么 Minor GC 可以确认是安全的
如果不成立的话虚拟机会查看 HandlePromotionFailure 的值是否允许担保失败如果允许那么就会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小如果大于将尝试着进行一次 Minor GC如果小于或者 HandlePromotionFailure 的值不允许冒险那么就要进行一次 Full GC
## Full GC 的触发条件
对于 Minor GC其触发条件非常简单 Eden 空间满时就将触发一次 Minor GC Full GC 则相对复杂有以下条件
### 1. 调用 System.gc()
只是建议虚拟机执行 Full GC但是虚拟机不一定真正去执行不建议使用这种方式而是让虚拟机管理内存
### 2. 老年代空间不足
老年代空间不足的常见场景为前文所讲的大对象直接进入老年代长期存活的对象进入老年代等
为了避免以上原因引起的 Full GC应当尽量不要创建过大的对象以及数组除此之外可以通过 -Xmn 虚拟机参数调大新生代的大小让对象尽量在新生代被回收掉不进入老年代还可以通过 -XX:MaxTenuringThreshold 调大对象进入老年代的年龄让对象在新生代多存活一段时间
### 3. 空间分配担保失败
使用复制算法的 Minor GC 需要老年代的内存空间作担保如果担保失败会执行一次 Full GC具体内容请参考上面的第 5 小节
### 4. JDK 1.7 及以前的永久代空间不足
JDK 1.7 及以前HotSpot 虚拟机中的方法区是用永久代实现的永久代中存放的为一些 Class 的信息常量静态变量等数据
当系统中要加载的类反射的类和调用的方法较多时永久代可能会被占满在未配置为采用 CMS GC 的情况下也会执行 Full GC如果经过 Full GC 仍然回收不了那么虚拟机会抛出 java.lang.OutOfMemoryError
为避免以上原因引起的 Full GC可采用的方法为增大永久代空间或转为使用 CMS GC
### 5. Concurrent Mode Failure
执行 CMS GC 的过程中同时有对象要放入老年代而此时老年代空间不足可能是 GC 过程中浮动垃圾过多导致暂时性的空间不足便会报 Concurrent Mode Failure 错误并触发 Full GC
# 类加载机制
类是在运行期间第一次使用时动态加载的而不是一次性加载所有类因为如果一次性加载那么会占用很多的内存
## 类的生命周期
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/335fe19c-4a76-45ab-9320-88c90d6a0d7e.png" width="600px"> </div><br>
包括以下 7 个阶段
- **加载Loading**
- **验证Verification**
- **准备Preparation**
- **解析Resolution**
- **初始化Initialization**
- 使用Using
- 卸载Unloading
## 类加载过程
包含了加载验证准备解析和初始化这 5 个阶段
### 1. 加载
加载是类加载的一个阶段注意不要混淆
加载过程完成以下三件事
- 通过类的完全限定名称获取定义该类的二进制字节流
- 将该字节流表示的静态存储结构转换为方法区的运行时存储结构
- 在内存中生成一个代表该类的 Class 对象作为方法区中该类各种数据的访问入口
其中二进制字节流可以从以下方式中获取
- ZIP 包读取成为 JAREARWAR 格式的基础
- 从网络中获取最典型的应用是 Applet
- 运行时计算生成例如动态代理技术 java.lang.reflect.Proxy 使用 ProxyGenerator.generateProxyClass 的代理类的二进制字节流
- 由其他文件生成例如由 JSP 文件生成对应的 Class
### 2. 验证
确保 Class 文件的字节流中包含的信息符合当前虚拟机的要求并且不会危害虚拟机自身的安全
### 3. 准备
类变量是被 static 修饰的变量准备阶段为类变量分配内存并设置初始值使用的是方法区的内存
实例变量不会在这阶段分配内存它会在对象实例化时随着对象一起被分配在堆中应该注意到实例化不是类加载的一个过程类加载发生在所有实例化操作之前并且类加载只进行一次实例化可以进行多次
初始值一般为 0 例如下面的类变量 value 被初始化为 0 而不是 123
```java
public static int value = 123;
```
如果类变量是常量那么它将初始化为表达式所定义的值而不是 0例如下面的常量 value 被初始化为 123 而不是 0
```java
public static final int value = 123;
```
### 4. 解析
将常量池的符号引用替换为直接引用的过程
其中解析过程在某些情况下可以在初始化阶段之后再开始这是为了支持 Java 的动态绑定
### 5. 初始化
<div data="modify -->"></div>
初始化阶段才真正开始执行类中定义的 Java 程序代码初始化阶段是虚拟机执行类构造器 &lt;clinit>() 方法的过程在准备阶段类变量已经赋过一次系统要求的初始值而在初始化阶段根据程序员通过程序制定的主观计划去初始化类变量和其它资源
&lt;clinit>() 是由编译器自动收集类中所有类变量的赋值动作和静态语句块中的语句合并产生的编译器收集的顺序由语句在源文件中出现的顺序决定特别注意的是静态语句块只能访问到定义在它之前的类变量定义在它之后的类变量只能赋值不能访问例如以下代码
```java
public class Test {
static {
i = 0; // 给变量赋值可以正常编译通过
System.out.print(i); // 这句编译器会提示非法向前引用
}
static int i = 1;
}
```
由于父类的 &lt;clinit>() 方法先执行也就意味着父类中定义的静态语句块的执行要优先于子类例如以下代码
```java
static class Parent {
public static int A = 1;
static {
A = 2;
}
}
static class Sub extends Parent {
public static int B = A;
}
public static void main(String[] args) {
System.out.println(Sub.B); // 2
}
```
接口中不可以使用静态语句块但仍然有类变量初始化的赋值操作因此接口与类一样都会生成 &lt;clinit>() 方法但接口与类不同的是执行接口的 &lt;clinit>() 方法不需要先执行父接口的 &lt;clinit>() 方法只有当父接口中定义的变量使用时父接口才会初始化另外接口的实现类在初始化时也一样不会执行接口的 &lt;clinit>() 方法
虚拟机会保证一个类的 &lt;clinit>() 方法在多线程环境下被正确的加锁和同步如果多个线程同时初始化一个类只会有一个线程执行这个类的 &lt;clinit>() 方法其它线程都会阻塞等待直到活动线程执行 &lt;clinit>() 方法完毕如果在一个类的 &lt;clinit>() 方法中有耗时的操作就可能造成多个线程阻塞在实际过程中此种阻塞很隐蔽
## 类初始化时机
### 1. 主动引用
虚拟机规范中并没有强制约束何时进行加载但是规范严格规定了有且只有下列五种情况必须对类进行初始化加载验证准备都会随之发生
- 遇到 newgetstaticputstaticinvokestatic 这四条字节码指令时如果类没有进行过初始化则必须先触发其初始化最常见的生成这 4 条指令的场景是使用 new 关键字实例化对象的时候读取或设置一个类的静态字段 final 修饰已在编译期把结果放入常量池的静态字段除外的时候以及调用一个类的静态方法的时候
- 使用 java.lang.reflect 包的方法对类进行反射调用的时候如果类没有进行初始化则需要先触发其初始化
- 当初始化一个类的时候如果发现其父类还没有进行过初始化则需要先触发其父类的初始化
- 当虚拟机启动时用户需要指定一个要执行的主类包含 main() 方法的那个类虚拟机会先初始化这个主类
- 当使用 JDK 1.7 的动态语言支持时如果一个 java.lang.invoke.MethodHandle 实例最后的解析结果为 REF_getStatic, REF_putStatic, REF_invokeStatic 的方法句柄并且这个方法句柄所对应的类没有进行过初始化则需要先触发其初始化
### 2. 被动引用
以上 5 种场景中的行为称为对一个类进行主动引用除此之外所有引用类的方式都不会触发初始化称为被动引用被动引用的常见例子包括
- 通过子类引用父类的静态字段不会导致子类初始化
```java
System.out.println(SubClass.value); // value 字段在 SuperClass 中定义
```
- 通过数组定义来引用类不会触发此类的初始化该过程会对数组类进行初始化数组类是一个由虚拟机自动生成的直接继承自 Object 的子类其中包含了数组的属性和方法
```java
SuperClass[] sca = new SuperClass[10];
```
- 常量在编译阶段会存入调用类的常量池中本质上并没有直接引用到定义常量的类因此不会触发定义常量的类的初始化
```java
System.out.println(ConstClass.HELLOWORLD);
```
## 类与类加载器
两个类相等需要类本身相等并且使用同一个类加载器进行加载这是因为每一个类加载器都拥有一个独立的类名称空间
这里的相等包括类的 Class 对象的 equals() 方法isAssignableFrom() 方法isInstance() 方法的返回结果为 true也包括使用 instanceof 关键字做对象所属关系判定结果为 true
## 类加载器分类
Java 虚拟机的角度来讲只存在以下两种不同的类加载器
- 启动类加载器Bootstrap ClassLoader使用 C++ 实现是虚拟机自身的一部分
- 所有其它类的加载器使用 Java 实现独立于虚拟机继承自抽象类 java.lang.ClassLoader
Java 开发人员的角度看类加载器可以划分得更细致一些
- 启动类加载器Bootstrap ClassLoader此类加载器负责将存放在 &lt;JRE_HOME>\lib 目录中的或者被 -Xbootclasspath 参数所指定的路径中的并且是虚拟机识别的仅按照文件名识别 rt.jar名字不符合的类库即使放在 lib 目录中也不会被加载类库加载到虚拟机内存中启动类加载器无法被 Java 程序直接引用用户在编写自定义类加载器时如果需要把加载请求委派给启动类加载器直接使用 null 代替即可
- 扩展类加载器Extension ClassLoader这个类加载器是由 ExtClassLoadersun.misc.Launcher$ExtClassLoader实现的它负责将 &lt;JAVA_HOME>/lib/ext 或者被 java.ext.dir 系统变量所指定路径中的所有类库加载到内存中开发者可以直接使用扩展类加载器
- 应用程序类加载器Application ClassLoader这个类加载器是由 AppClassLoadersun.misc.Launcher$AppClassLoader实现的由于这个类加载器是 ClassLoader 中的 getSystemClassLoader() 方法的返回值因此一般称为系统类加载器它负责加载用户类路径ClassPath上所指定的类库开发者可以直接使用这个类加载器如果应用程序中没有自定义过自己的类加载器一般情况下这个就是程序中默认的类加载器
## 双亲委派模型
应用程序是由三种类加载器互相配合从而实现类加载除此之外还可以加入自己定义的类加载器
下图展示了类加载器之间的层次关系称为双亲委派模型Parents Delegation Model该模型要求除了顶层的启动类加载器外其它的类加载器都要有自己的父类加载器这里的父子关系一般通过组合关系Composition来实现而不是继承关系Inheritance
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/0dd2d40a-5b2b-4d45-b176-e75a4cd4bdbf.png" width="500px"> </div><br>
### 1. 工作过程
一个类加载器首先将类加载请求转发到父类加载器只有当父类加载器无法完成时才尝试自己加载
### 2. 好处
使得 Java 类随着它的类加载器一起具有一种带有优先级的层次关系从而使得基础类得到统一
例如 java.lang.Object 存放在 rt.jar 如果编写另外一个 java.lang.Object 并放到 ClassPath 程序可以编译通过由于双亲委派模型的存在所以在 rt.jar 中的 Object 比在 ClassPath 中的 Object 优先级更高这是因为 rt.jar 中的 Object 使用的是启动类加载器 ClassPath 中的 Object 使用的是应用程序类加载器rt.jar 中的 Object 优先级更高那么程序中所有的 Object 都是这个 Object
### 3. 实现
以下是抽象类 java.lang.ClassLoader 的代码片段其中的 loadClass() 方法运行过程如下先检查类是否已经加载过如果没有则让父类加载器去加载当父类加载器加载失败时抛出 ClassNotFoundException此时尝试自己去加载
```java
public abstract class ClassLoader {
// The parent class loader for delegation
private final ClassLoader parent;
public Class<?> loadClass(String name) throws ClassNotFoundException {
return loadClass(name, false);
}
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
if (c == null) {
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
c = findClass(name);
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
}
```
## 自定义类加载器实现
以下代码中的 FileSystemClassLoader 是自定义类加载器继承自 java.lang.ClassLoader用于加载文件系统上的类它首先根据类的全名在文件系统上查找类的字节代码文件.class 文件然后读取该文件内容最后通过 defineClass() 方法来把这些字节代码转换成 java.lang.Class 类的实例
java.lang.ClassLoader loadClass() 实现了双亲委派模型的逻辑自定义类加载器一般不去重写它但是需要重写 findClass() 方法
```java
public class FileSystemClassLoader extends ClassLoader {
private String rootDir;
public FileSystemClassLoader(String rootDir) {
this.rootDir = rootDir;
}
protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] classData = getClassData(name);
if (classData == null) {
throw new ClassNotFoundException();
} else {
return defineClass(name, classData, 0, classData.length);
}
}
private byte[] getClassData(String className) {
String path = classNameToPath(className);
try {
InputStream ins = new FileInputStream(path);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int bufferSize = 4096;
byte[] buffer = new byte[bufferSize];
int bytesNumRead;
while ((bytesNumRead = ins.read(buffer)) != -1) {
baos.write(buffer, 0, bytesNumRead);
}
return baos.toByteArray();
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
private String classNameToPath(String className) {
return rootDir + File.separatorChar
+ className.replace('.', File.separatorChar) + ".class";
}
}
```
# 参考资料
- 周志明. 深入理解 Java 虚拟机 [M]. 机械工业出版社, 2011.
- [Chapter 2. The Structure of the Java Virtual Machine](https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-2.html#jvms-2.5.4)
- [Jvm memory](https://www.slideshare.net/benewu/jvm-memory)
[Getting Started with the G1 Garbage Collector](http://www.oracle.com/webfolder/technetwork/tutorials/obe/java/G1GettingStarted/index.html)
- [JNI Part1: Java Native Interface Introduction and Hello World application](http://electrofriends.com/articles/jni/jni-part1-java-native-interface/)
- [Memory Architecture Of JVM(Runtime Data Areas)](https://hackthejava.wordpress.com/2015/01/09/memory-architecture-by-jvmruntime-data-areas/)
- [JVM Run-Time Data Areas](https://www.programcreek.com/2013/04/jvm-run-time-data-areas/)
- [Android on x86: Java Native Interface and the Android Native Development Kit](http://www.drdobbs.com/architecture-and-design/android-on-x86-java-native-interface-and/240166271)
- [深入理解 JVM(2)GC 算法与内存分配策略](https://crowhawk.github.io/2017/08/10/jvm_2/)
- [深入理解 JVM(3)7 种垃圾收集器](https://crowhawk.github.io/2017/08/15/jvm_3/)
- [JVM Internals](http://blog.jamesdbloom.com/JVMInternals.html)
- [深入探讨 Java 类加载器](https://www.ibm.com/developerworks/cn/java/j-lo-classloader/index.html#code6)
- [Guide to WeakHashMap in Java](http://www.baeldung.com/java-weakhashmap)
- [Tomcat example source code file (ConcurrentCache.java)](https://alvinalexander.com/java/jwarehouse/apache-tomcat-6.0.16/java/org/apache/el/util/ConcurrentCache.java.shtml)
<div align="center"><img width="320px" src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/githubio/公众号二维码-2.png"></img></div>

View File

@ -1,321 +0,0 @@
<!-- GFM-TOC -->
* [1. 求开方](#1-求开方)
* [2. 大于给定元素的最小元素](#2-大于给定元素的最小元素)
* [3. 有序数组的 Single Element](#3-有序数组的-single-element)
* [4. 第一个错误的版本](#4-第一个错误的版本)
* [5. 旋转数组的最小数字](#5-旋转数组的最小数字)
* [6. 查找区间](#6-查找区间)
<!-- GFM-TOC -->
**正常实现**
```text
Input : [1,2,3,4,5]
key : 3
return the index : 2
```
```java
public int binarySearch(int[] nums, int key) {
int l = 0, h = nums.length - 1;
while (l <= h) {
int m = l + (h - l) / 2;
if (nums[m] == key) {
return m;
} else if (nums[m] > key) {
h = m - 1;
} else {
l = m + 1;
}
}
return -1;
}
```
**时间复杂度**
二分查找也称为折半查找每次都能将查找区间减半这种折半特性的算法时间复杂度为 O(logN)
**m 计算**
有两种计算中值 m 的方式
- m = (l + h) / 2
- m = l + (h - l) / 2
l + h 可能出现加法溢出也就是说加法的结果大于整型能够表示的范围但是 l h 都为正数因此 h - l 不会出现加法溢出问题所以最好使用第二种计算法方法
**未成功查找的返回值**
循环退出时如果仍然没有查找到 key那么表示查找失败可以有两种返回值
- -1以一个错误码表示没有查找到 key
- l key 插入到 nums 中的正确位置
**变种**
二分查找可以有很多变种实现变种要注意边界值的判断例如在一个有重复元素的数组中查找 key 的最左位置的实现如下
```java
public int binarySearch(int[] nums, int key) {
int l = 0, h = nums.length;
while (l < h) {
int m = l + (h - l) / 2;
if (nums[m] >= key) {
h = m;
} else {
l = m + 1;
}
}
return l;
}
```
该实现和正常实现有以下不同
- h 的赋值表达式为 h = m
- 循环条件为 l < h
- 最后返回 l 而不是 -1
nums[m] >= key 的情况下可以推导出最左 key 位于 [l, m] 区间中这是一个闭区间h 的赋值表达式为 h = m因为 m 位置也可能是解
h 的赋值表达式为 h = m 的情况下如果循环条件为 l <= h那么会出现循环无法退出的情况因此循环条件只能是 l < h以下演示了循环条件为 l <= h 时循环无法退出的情况
```text
nums = {0, 1, 2}, key = 1
l m h
0 1 2 nums[m] >= key
0 0 1 nums[m] < key
1 1 1 nums[m] >= key
1 1 1 nums[m] >= key
...
```
当循环体退出时不表示没有查找到 key因此最后返回的结果不应该为 -1为了验证有没有查找到需要在调用端判断一下返回位置上的值和 key 是否相等
# 1. 求开方
69\. Sqrt(x) (Easy)
[Leetcode](https://leetcode.com/problems/sqrtx/description/) / [力扣](https://leetcode-cn.com/problems/sqrtx/description/)
```html
Input: 4
Output: 2
Input: 8
Output: 2
Explanation: The square root of 8 is 2.82842..., and since we want to return an integer, the decimal part will be truncated.
```
一个数 x 的开方 sqrt 一定在 0 \~ x 之间并且满足 sqrt == x / sqrt可以利用二分查找在 0 \~ x 之间查找 sqrt
对于 x = 8它的开方是 2.82842...最后应该返回 2 而不是 3在循环条件为 l <= h 并且循环退出时h 总是比 l 1也就是说 h = 2l = 3因此最后的返回值应该为 h 而不是 l
```java
public int mySqrt(int x) {
if (x <= 1) {
return x;
}
int l = 1, h = x;
while (l <= h) {
int mid = l + (h - l) / 2;
int sqrt = x / mid;
if (sqrt == mid) {
return mid;
} else if (mid > sqrt) {
h = mid - 1;
} else {
l = mid + 1;
}
}
return h;
}
```
# 2. 大于给定元素的最小元素
744\. Find Smallest Letter Greater Than Target (Easy)
[Leetcode](https://leetcode.com/problems/find-smallest-letter-greater-than-target/description/) / [力扣](https://leetcode-cn.com/problems/find-smallest-letter-greater-than-target/description/)
```html
Input:
letters = ["c", "f", "j"]
target = "d"
Output: "f"
Input:
letters = ["c", "f", "j"]
target = "k"
Output: "c"
```
题目描述给定一个有序的字符数组 letters 和一个字符 target要求找出 letters 中大于 target 的最小字符如果找不到就返回第 1 个字符
```java
public char nextGreatestLetter(char[] letters, char target) {
int n = letters.length;
int l = 0, h = n - 1;
while (l <= h) {
int m = l + (h - l) / 2;
if (letters[m] <= target) {
l = m + 1;
} else {
h = m - 1;
}
}
return l < n ? letters[l] : letters[0];
}
```
# 3. 有序数组的 Single Element
540\. Single Element in a Sorted Array (Medium)
[Leetcode](https://leetcode.com/problems/single-element-in-a-sorted-array/description/) / [力扣](https://leetcode-cn.com/problems/single-element-in-a-sorted-array/description/)
```html
Input: [1, 1, 2, 3, 3, 4, 4, 8, 8]
Output: 2
```
题目描述一个有序数组只有一个数不出现两次找出这个数
要求以 O(logN) 时间复杂度进行求解因此不能遍历数组并进行异或操作来求解这么做的时间复杂度为 O(N)
index Single Element 在数组中的位置 index 之后数组中原来存在的成对状态被改变如果 m 为偶数并且 m + 1 < index那么 nums[m] == nums[m + 1]m + 1 >= index那么 nums[m] != nums[m + 1]
从上面的规律可以知道如果 nums[m] == nums[m + 1]那么 index 所在的数组位置为 [m + 2, h]此时令 l = m + 2如果 nums[m] != nums[m + 1]那么 index 所在的数组位置为 [l, m]此时令 h = m
因为 h 的赋值表达式为 h = m那么循环条件也就只能使用 l < h 这种形式
```java
public int singleNonDuplicate(int[] nums) {
int l = 0, h = nums.length - 1;
while (l < h) {
int m = l + (h - l) / 2;
if (m % 2 == 1) {
m--; // 保证 l/h/m 都在偶数位使得查找区间大小一直都是奇数
}
if (nums[m] == nums[m + 1]) {
l = m + 2;
} else {
h = m;
}
}
return nums[l];
}
```
# 4. 第一个错误的版本
278\. First Bad Version (Easy)
[Leetcode](https://leetcode.com/problems/first-bad-version/description/) / [力扣](https://leetcode-cn.com/problems/first-bad-version/description/)
题目描述给定一个元素 n 代表有 [1, 2, ..., n] 版本在第 x 位置开始出现错误版本导致后面的版本都错误可以调用 isBadVersion(int x) 知道某个版本是否错误要求找到第一个错误的版本
如果第 m 个版本出错则表示第一个错误的版本在 [l, m] 之间 h = m否则第一个错误的版本在 [m + 1, h] 之间 l = m + 1
因为 h 的赋值表达式为 h = m因此循环条件为 l < h
```java
public int firstBadVersion(int n) {
int l = 1, h = n;
while (l < h) {
int mid = l + (h - l) / 2;
if (isBadVersion(mid)) {
h = mid;
} else {
l = mid + 1;
}
}
return l;
}
```
# 5. 旋转数组的最小数字
153\. Find Minimum in Rotated Sorted Array (Medium)
[Leetcode](https://leetcode.com/problems/find-minimum-in-rotated-sorted-array/description/) / [力扣](https://leetcode-cn.com/problems/find-minimum-in-rotated-sorted-array/description/)
```html
Input: [3,4,5,1,2],
Output: 1
```
```java
public int findMin(int[] nums) {
int l = 0, h = nums.length - 1;
while (l < h) {
int m = l + (h - l) / 2;
if (nums[m] <= nums[h]) {
h = m;
} else {
l = m + 1;
}
}
return nums[l];
}
```
# 6. 查找区间
34\. Find First and Last Position of Element in Sorted Array
[Leetcode](https://leetcode.com/problems/find-first-and-last-position-of-element-in-sorted-array/) / [力扣](https://leetcode-cn.com/problems/find-first-and-last-position-of-element-in-sorted-array/)
```html
Input: nums = [5,7,7,8,8,10], target = 8
Output: [3,4]
Input: nums = [5,7,7,8,8,10], target = 6
Output: [-1,-1]
```
题目描述给定一个有序数组 nums 和一个目标 target要求找到 target nums 中的第一个位置和最后一个位置
可以用二分查找找出第一个位置和最后一个位置但是寻找的方法有所不同需要实现两个二分查找我们将寻找 target 最后一个位置转换成寻找 target+1 第一个位置再往前移动一个位置这样我们只需要实现一个二分查找代码即可
```java
public int[] searchRange(int[] nums, int target) {
int first = findFirst(nums, target);
int last = findFirst(nums, target + 1) - 1;
if (first == nums.length || nums[first] != target) {
return new int[]{-1, -1};
} else {
return new int[]{first, Math.max(first, last)};
}
}
private int findFirst(int[] nums, int target) {
int l = 0, h = nums.length; // 注意 h 的初始值
while (l < h) {
int m = l + (h - l) / 2;
if (nums[m] >= target) {
h = m;
} else {
l = m + 1;
}
}
return l;
}
```
在寻找第一个位置的二分查找代码中需要注意 h 的取值为 nums.length而不是 nums.length - 1先看以下示例
```
nums = [2,2], target = 2
```
如果 h 的取值为 nums.length - 1那么 last = findFirst(nums, target + 1) - 1 = 1 - 1 = 0这是因为 findLeft 只会返回 [0, nums.length - 1] 范围的值对于 findFirst([2,2], 3) 我们希望返回 3 插入 nums 中的位置也就是数组最后一个位置再往后一个位置 nums.length所以我们需要将 h 取值为 nums.length从而使得 findFirst返回的区间更大能够覆盖 target 大于 nums 最后一个元素的情况
<div align="center"><img width="320px" src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/githubio/公众号二维码-2.png"></img></div>

View File

@ -1,508 +0,0 @@
<!-- GFM-TOC -->
* [0. 原理](#0-原理)
* [1. 统计两个数的二进制表示有多少位不同](#1-统计两个数的二进制表示有多少位不同)
* [2. 数组中唯一一个不重复的元素](#2-数组中唯一一个不重复的元素)
* [3. 找出数组中缺失的那个数](#3-找出数组中缺失的那个数)
* [4. 数组中不重复的两个元素](#4-数组中不重复的两个元素)
* [5. 翻转一个数的比特位](#5-翻转一个数的比特位)
* [6. 不用额外变量交换两个整数](#6-不用额外变量交换两个整数)
* [7. 判断一个数是不是 2 n 次方](#7-判断一个数是不是-2--n-次方)
* [8. 判断一个数是不是 4 n 次方](#8--判断一个数是不是-4--n-次方)
* [9. 判断一个数的位级表示是否不会出现连续的 0 1](#9-判断一个数的位级表示是否不会出现连续的-0--1)
* [10. 求一个数的补码](#10-求一个数的补码)
* [11. 实现整数的加法](#11-实现整数的加法)
* [12. 字符串数组最大乘积](#12-字符串数组最大乘积)
* [13. 统计从 0 \~ n 每个数的二进制表示中 1 的个数](#13-统计从-0-\~-n-每个数的二进制表示中-1-的个数)
<!-- GFM-TOC -->
# 0. 原理
**基本原理**
0s 表示一串 01s 表示一串 1
```
x ^ 0s = x x & 0s = 0 x | 0s = x
x ^ 1s = ~x x & 1s = x x | 1s = 1s
x ^ x = 0 x & x = x x | x = x
```
利用 x ^ 1s = \~x 的特点可以将一个数的位级表示翻转利用 x ^ x = 0 的特点可以将三个数中重复的两个数去除只留下另一个数
```
1^1^2 = 2
```
利用 x & 0s = 0 x & 1s = x 的特点可以实现掩码操作一个数 num mask00111100 进行位与操作只保留 num 中与 mask 1 部分相对应的位
```
01011011 &
00111100
--------
00011000
```
利用 x | 0s = x x | 1s = 1s 的特点可以实现设值操作一个数 num mask00111100 进行位或操作 num 中与 mask 1 部分相对应的位都设置为 1
```
01011011 |
00111100
--------
01111111
```
**位与运算技巧**
n&(n-1) 去除 n 的位级表示中最低的那一位 1例如对于二进制表示 01011011减去 1 得到 01011010这两个数相与得到 01011010
```
01011011 &
01011010
--------
01011010
```
n&(-n) 得到 n 的位级表示中最低的那一位 1-n 得到 n 的反码加 1也就是 -n=\~n+1例如对于二进制表示 10110100-n 得到 01001100相与得到 00000100
```
10110100 &
01001100
--------
00000100
```
n-(n&(-n)) 则可以去除 n 的位级表示中最低的那一位 1 n&(n-1) 效果一样
**移位运算**
\>\> n 为算术右移相当于除以 2n例如 -7 \>\> 2 = -2
```
11111111111111111111111111111001 >> 2
--------
11111111111111111111111111111110
```
\>\>\> n 为无符号右移左边会补上 0例如 -7 \>\>\> 2 = 1073741822
```
11111111111111111111111111111001 >>> 2
--------
00111111111111111111111111111111
```
<< n 为算术左移相当于乘以 2n-7 << 2 = -28
```
11111111111111111111111111111001 << 2
--------
11111111111111111111111111100100
```
**mask 计算**
要获取 111111111 0 取反即可\~0
要得到只有第 i 位为 1 mask 1 向左移动 i-1 位即可1<<(i-1) 例如 1<<4 得到只有第 5 位为 1 mask 00010000
要得到 1 i 位为 1 mask(1<<i)-1 即可例如将 (1<<4)-1 = 00010000-1 = 00001111
要得到 1 i 位为 0 mask只需将 1 i 位为 1 mask 取反 \~((1<<i)-1)
**Java 中的位操作**
```html
static int Integer.bitCount(); // 统计 1 的数量
static int Integer.highestOneBit(); // 获得最高位
static String toBinaryString(int i); // 转换为二进制表示的字符串
```
# 1. 统计两个数的二进制表示有多少位不同
461. Hamming Distance (Easy)
[Leetcode](https://leetcode.com/problems/hamming-distance/) / [力扣](https://leetcode-cn.com/problems/hamming-distance/)
```html
Input: x = 1, y = 4
Output: 2
Explanation:
1 (0 0 0 1)
4 (0 1 0 0)
The above arrows point to positions where the corresponding bits are different.
```
对两个数进行异或操作位级表示不同的那一位为 1统计有多少个 1 即可
```java
public int hammingDistance(int x, int y) {
int z = x ^ y;
int cnt = 0;
while(z != 0) {
if ((z & 1) == 1) cnt++;
z = z >> 1;
}
return cnt;
}
```
使用 z&(z-1) 去除 z 位级表示最低的那一位
```java
public int hammingDistance(int x, int y) {
int z = x ^ y;
int cnt = 0;
while (z != 0) {
z &= (z - 1);
cnt++;
}
return cnt;
}
```
可以使用 Integer.bitcount() 来统计 1 个的个数
```java
public int hammingDistance(int x, int y) {
return Integer.bitCount(x ^ y);
}
```
# 2. 数组中唯一一个不重复的元素
136\. Single Number (Easy)
[Leetcode](https://leetcode.com/problems/single-number/description/) / [力扣](https://leetcode-cn.com/problems/single-number/description/)
```html
Input: [4,1,2,1,2]
Output: 4
```
两个相同的数异或的结果为 0对所有数进行异或操作最后的结果就是单独出现的那个数
```java
public int singleNumber(int[] nums) {
int ret = 0;
for (int n : nums) ret = ret ^ n;
return ret;
}
```
# 3. 找出数组中缺失的那个数
268\. Missing Number (Easy)
[Leetcode](https://leetcode.com/problems/missing-number/description/) / [力扣](https://leetcode-cn.com/problems/missing-number/description/)
```html
Input: [3,0,1]
Output: 2
```
题目描述数组元素在 0-n 之间但是有一个数是缺失的要求找到这个缺失的数
```java
public int missingNumber(int[] nums) {
int ret = 0;
for (int i = 0; i < nums.length; i++) {
ret = ret ^ i ^ nums[i];
}
return ret ^ nums.length;
}
```
# 4. 数组中不重复的两个元素
260\. Single Number III (Medium)
[Leetcode](https://leetcode.com/problems/single-number-iii/description/) / [力扣](https://leetcode-cn.com/problems/single-number-iii/description/)
两个不相等的元素在位级表示上必定会有一位存在不同
将数组的所有元素异或得到的结果为不存在重复的两个元素异或的结果
diff &= -diff 得到出 diff 最右侧不为 0 的位也就是不存在重复的两个元素在位级表示上最右侧不同的那一位利用这一位就可以将两个元素区分开来
```java
public int[] singleNumber(int[] nums) {
int diff = 0;
for (int num : nums) diff ^= num;
diff &= -diff; // 得到最右一位
int[] ret = new int[2];
for (int num : nums) {
if ((num & diff) == 0) ret[0] ^= num;
else ret[1] ^= num;
}
return ret;
}
```
# 5. 翻转一个数的比特位
190\. Reverse Bits (Easy)
[Leetcode](https://leetcode.com/problems/reverse-bits/description/) / [力扣](https://leetcode-cn.com/problems/reverse-bits/description/)
```java
public int reverseBits(int n) {
int ret = 0;
for (int i = 0; i < 32; i++) {
ret <<= 1;
ret |= (n & 1);
n >>>= 1;
}
return ret;
}
```
如果该函数需要被调用很多次可以将 int 拆成 4 byte然后缓存 byte 对应的比特位翻转最后再拼接起来
```java
private static Map<Byte, Integer> cache = new HashMap<>();
public int reverseBits(int n) {
int ret = 0;
for (int i = 0; i < 4; i++) {
ret <<= 8;
ret |= reverseByte((byte) (n & 0b11111111));
n >>= 8;
}
return ret;
}
private int reverseByte(byte b) {
if (cache.containsKey(b)) return cache.get(b);
int ret = 0;
byte t = b;
for (int i = 0; i < 8; i++) {
ret <<= 1;
ret |= t & 1;
t >>= 1;
}
cache.put(b, ret);
return ret;
}
```
# 6. 不用额外变量交换两个整数
[程序员代码面试指南 P317](#)
```java
a = a ^ b;
b = a ^ b;
a = a ^ b;
```
# 7. 判断一个数是不是 2 n 次方
231\. Power of Two (Easy)
[Leetcode](https://leetcode.com/problems/power-of-two/description/) / [力扣](https://leetcode-cn.com/problems/power-of-two/description/)
二进制表示只有一个 1 存在
```java
public boolean isPowerOfTwo(int n) {
return n > 0 && Integer.bitCount(n) == 1;
}
```
利用 1000 & 0111 == 0 这种性质得到以下解法
```java
public boolean isPowerOfTwo(int n) {
return n > 0 && (n & (n - 1)) == 0;
}
```
# 8. 判断一个数是不是 4 n 次方
342\. Power of Four (Easy)
[Leetcode](https://leetcode.com/problems/power-of-four/) / [力扣](https://leetcode-cn.com/problems/power-of-four/)
这种数在二进制表示中有且只有一个奇数位为 1例如 1610000
```java
public boolean isPowerOfFour(int num) {
return num > 0 && (num & (num - 1)) == 0 && (num & 0b01010101010101010101010101010101) != 0;
}
```
也可以使用正则表达式进行匹配
```java
public boolean isPowerOfFour(int num) {
return Integer.toString(num, 4).matches("10*");
}
```
# 9. 判断一个数的位级表示是否不会出现连续的 0 1
693\. Binary Number with Alternating Bits (Easy)
[Leetcode](https://leetcode.com/problems/binary-number-with-alternating-bits/description/) / [力扣](https://leetcode-cn.com/problems/binary-number-with-alternating-bits/description/)
```html
Input: 10
Output: True
Explanation:
The binary representation of 10 is: 1010.
Input: 11
Output: False
Explanation:
The binary representation of 11 is: 1011.
```
对于 1010 这种位级表示的数把它向右移动 1 位得到 101这两个数每个位都不同因此异或得到的结果为 1111
```java
public boolean hasAlternatingBits(int n) {
int a = (n ^ (n >> 1));
return (a & (a + 1)) == 0;
}
```
# 10. 求一个数的补码
476\. Number Complement (Easy)
[Leetcode](https://leetcode.com/problems/number-complement/description/) / [力扣](https://leetcode-cn.com/problems/number-complement/description/)
```html
Input: 5
Output: 2
Explanation: The binary representation of 5 is 101 (no leading zero bits), and its complement is 010. So you need to output 2.
```
题目描述不考虑二进制表示中的首 0 部分
对于 00000101要求补码可以将它与 00000111 进行异或操作那么问题就转换为求掩码 00000111
```java
public int findComplement(int num) {
if (num == 0) return 1;
int mask = 1 << 30;
while ((num & mask) == 0) mask >>= 1;
mask = (mask << 1) - 1;
return num ^ mask;
}
```
可以利用 Java Integer.highestOneBit() 方法来获得含有首 1 的数
```java
public int findComplement(int num) {
if (num == 0) return 1;
int mask = Integer.highestOneBit(num);
mask = (mask << 1) - 1;
return num ^ mask;
}
```
对于 10000000 这样的数要扩展成 11111111可以利用以下方法
```html
mask |= mask >> 1 11000000
mask |= mask >> 2 11110000
mask |= mask >> 4 11111111
```
```java
public int findComplement(int num) {
int mask = num;
mask |= mask >> 1;
mask |= mask >> 2;
mask |= mask >> 4;
mask |= mask >> 8;
mask |= mask >> 16;
return (mask ^ num);
}
```
# 11. 实现整数的加法
371\. Sum of Two Integers (Easy)
[Leetcode](https://leetcode.com/problems/sum-of-two-integers/description/) / [力扣](https://leetcode-cn.com/problems/sum-of-two-integers/description/)
a ^ b 表示没有考虑进位的情况下两数的和(a & b) << 1 就是进位
递归会终止的原因是 (a & b) << 1 最右边会多一个 0那么继续递归进位最右边的 0 会慢慢增多最后进位会变为 0递归终止
```java
public int getSum(int a, int b) {
return b == 0 ? a : getSum((a ^ b), (a & b) << 1);
}
```
# 12. 字符串数组最大乘积
318\. Maximum Product of Word Lengths (Medium)
[Leetcode](https://leetcode.com/problems/maximum-product-of-word-lengths/description/) / [力扣](https://leetcode-cn.com/problems/maximum-product-of-word-lengths/description/)
```html
Given ["abcw", "baz", "foo", "bar", "xtfn", "abcdef"]
Return 16
The two words can be "abcw", "xtfn".
```
题目描述字符串数组的字符串只含有小写字符求解字符串数组中两个字符串长度的最大乘积要求这两个字符串不能含有相同字符
本题主要问题是判断两个字符串是否含相同字符由于字符串只含有小写字符总共 26 因此可以用一个 32 位的整数来存储每个字符是否出现过
```java
public int maxProduct(String[] words) {
int n = words.length;
int[] val = new int[n];
for (int i = 0; i < n; i++) {
for (char c : words[i].toCharArray()) {
val[i] |= 1 << (c - 'a');
}
}
int ret = 0;
for (int i = 0; i < n; i++) {
for (int j = i + 1; j < n; j++) {
if ((val[i] & val[j]) == 0) {
ret = Math.max(ret, words[i].length() * words[j].length());
}
}
}
return ret;
}
```
# 13. 统计从 0 \~ n 每个数的二进制表示中 1 的个数
338\. Counting Bits (Medium)
[Leetcode](https://leetcode.com/problems/counting-bits/description/) / [力扣](https://leetcode-cn.com/problems/counting-bits/description/)
对于数字 6(110)它可以看成是 4(100) 再加一个 2(10)因此 dp[i] = dp[i&(i-1)] + 1;
```java
public int[] countBits(int num) {
int[] ret = new int[num + 1];
for(int i = 1; i <= num; i++){
ret[i] = ret[i&(i-1)] + 1;
}
return ret;
}
```
<div align="center"><img width="320px" src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/githubio/公众号二维码-2.png"></img></div>

View File

@ -1,117 +0,0 @@
<!-- GFM-TOC -->
* [1. 给表达式加括号](#1-给表达式加括号)
* [2. 不同的二叉搜索树](#2-不同的二叉搜索树)
<!-- GFM-TOC -->
# 1. 给表达式加括号
241\. Different Ways to Add Parentheses (Medium)
[Leetcode](https://leetcode.com/problems/different-ways-to-add-parentheses/description/) / [力扣](https://leetcode-cn.com/problems/different-ways-to-add-parentheses/description/)
```html
Input: "2-1-1".
((2-1)-1) = 0
(2-(1-1)) = 2
Output : [0, 2]
```
```java
public List<Integer> diffWaysToCompute(String input) {
List<Integer> ways = new ArrayList<>();
for (int i = 0; i < input.length(); i++) {
char c = input.charAt(i);
if (c == '+' || c == '-' || c == '*') {
List<Integer> left = diffWaysToCompute(input.substring(0, i));
List<Integer> right = diffWaysToCompute(input.substring(i + 1));
for (int l : left) {
for (int r : right) {
switch (c) {
case '+':
ways.add(l + r);
break;
case '-':
ways.add(l - r);
break;
case '*':
ways.add(l * r);
break;
}
}
}
}
}
if (ways.size() == 0) {
ways.add(Integer.valueOf(input));
}
return ways;
}
```
# 2. 不同的二叉搜索树
95\. Unique Binary Search Trees II (Medium)
[Leetcode](https://leetcode.com/problems/unique-binary-search-trees-ii/description/) / [力扣](https://leetcode-cn.com/problems/unique-binary-search-trees-ii/description/)
给定一个数字 n要求生成所有值为 1...n 的二叉搜索树
```html
Input: 3
Output:
[
[1,null,3,2],
[3,2,null,1],
[3,1,null,null,2],
[2,1,3],
[1,null,2,null,3]
]
Explanation:
The above output corresponds to the 5 unique BST's shown below:
1 3 3 2 1
\ / / / \ \
3 2 1 1 3 2
/ / \ \
2 1 2 3
```
```java
public List<TreeNode> generateTrees(int n) {
if (n < 1) {
return new LinkedList<TreeNode>();
}
return generateSubtrees(1, n);
}
private List<TreeNode> generateSubtrees(int s, int e) {
List<TreeNode> res = new LinkedList<TreeNode>();
if (s > e) {
res.add(null);
return res;
}
for (int i = s; i <= e; ++i) {
List<TreeNode> leftSubtrees = generateSubtrees(s, i - 1);
List<TreeNode> rightSubtrees = generateSubtrees(i + 1, e);
for (TreeNode left : leftSubtrees) {
for (TreeNode right : rightSubtrees) {
TreeNode root = new TreeNode(i);
root.left = left;
root.right = right;
res.add(root);
}
}
}
return res;
}
```
<div align="center"><img width="320px" src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/githubio/公众号二维码-2.png"></img></div>

File diff suppressed because it is too large Load Diff

View File

@ -1,299 +0,0 @@
<!-- GFM-TOC -->
* [1. 有序数组的 Two Sum](#1-有序数组的-two-sum)
* [2. 两数平方和](#2-两数平方和)
* [3. 反转字符串中的元音字符](#3-反转字符串中的元音字符)
* [4. 回文字符串](#4-回文字符串)
* [5. 归并两个有序数组](#5-归并两个有序数组)
* [6. 判断链表是否存在环](#6-判断链表是否存在环)
* [7. 最长子序列](#7-最长子序列)
<!-- GFM-TOC -->
双指针主要用于遍历数组两个指针指向不同的元素从而协同完成任务
# 1. 有序数组的 Two Sum
167\. Two Sum II - Input array is sorted (Easy)
[Leetcode](https://leetcode.com/problems/two-sum-ii-input-array-is-sorted/description/) / [力扣](https://leetcode-cn.com/problems/two-sum-ii-input-array-is-sorted/description/)
```html
Input: numbers={2, 7, 11, 15}, target=9
Output: index1=1, index2=2
```
题目描述在有序数组中找出两个数使它们的和为 target
使用双指针一个指针指向值较小的元素一个指针指向值较大的元素指向较小元素的指针从头向尾遍历指向较大元素的指针从尾向头遍历
- 如果两个指针指向元素的和 sum == target那么得到要求的结果
- 如果 sum > target移动较大的元素使 sum 变小一些
- 如果 sum < target移动较小的元素使 sum 变大一些
数组中的元素最多遍历一次时间复杂度为 O(N)只使用了两个额外变量空间复杂度为 O(1)
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/437cb54c-5970-4ba9-b2ef-2541f7d6c81e.gif" width="200px"> </div><br>
```java
public int[] twoSum(int[] numbers, int target) {
if (numbers == null) return null;
int i = 0, j = numbers.length - 1;
while (i < j) {
int sum = numbers[i] + numbers[j];
if (sum == target) {
return new int[]{i + 1, j + 1};
} else if (sum < target) {
i++;
} else {
j--;
}
}
return null;
}
```
# 2. 两数平方和
633\. Sum of Square Numbers (Easy)
[Leetcode](https://leetcode.com/problems/sum-of-square-numbers/description/) / [力扣](https://leetcode-cn.com/problems/sum-of-square-numbers/description/)
```html
Input: 5
Output: True
Explanation: 1 * 1 + 2 * 2 = 5
```
题目描述判断一个非负整数是否为两个整数的平方和
可以看成是在元素为 0\~target 的有序数组中查找两个数使得这两个数的平方和为 target如果能找到则返回 true表示 target 是两个整数的平方和
本题和 167\. Two Sum II - Input array is sorted 类似只有一个明显区别一个是和为 target一个是平方和为 target本题同样可以使用双指针得到两个数使其平方和为 target
本题的关键是右指针的初始化实现剪枝从而降低时间复杂度设右指针为 x左指针固定为 0为了使 0<sup>2</sup> + x<sup>2</sup> 的值尽可能接近 target我们可以将 x 取为 sqrt(target)
因为最多只需要遍历一次 0\~sqrt(target)所以时间复杂度为 O(sqrt(target))又因为只使用了两个额外的变量因此空间复杂度为 O(1)
```java
public boolean judgeSquareSum(int target) {
if (target < 0) return false;
int i = 0, j = (int) Math.sqrt(target);
while (i <= j) {
int powSum = i * i + j * j;
if (powSum == target) {
return true;
} else if (powSum > target) {
j--;
} else {
i++;
}
}
return false;
}
```
# 3. 反转字符串中的元音字符
345\. Reverse Vowels of a String (Easy)
[Leetcode](https://leetcode.com/problems/reverse-vowels-of-a-string/description/) / [力扣](https://leetcode-cn.com/problems/reverse-vowels-of-a-string/description/)
```html
Given s = "leetcode", return "leotcede".
```
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/a7cb8423-895d-4975-8ef8-662a0029c772.png" width="400px"> </div><br>
使用双指针一个指针从头向尾遍历一个指针从尾到头遍历当两个指针都遍历到元音字符时交换这两个元音字符
为了快速判断一个字符是不是元音字符我们将全部元音字符添加到集合 HashSet 从而以 O(1) 的时间复杂度进行该操作
- 时间复杂度为 O(N)只需要遍历所有元素一次
- 空间复杂度 O(1)只需要使用两个额外变量
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/ef25ff7c-0f63-420d-8b30-eafbeea35d11.gif" width="400px"> </div><br>
```java
private final static HashSet<Character> vowels = new HashSet<>(
Arrays.asList('a', 'e', 'i', 'o', 'u', 'A', 'E', 'I', 'O', 'U'));
public String reverseVowels(String s) {
if (s == null) return null;
int i = 0, j = s.length() - 1;
char[] result = new char[s.length()];
while (i <= j) {
char ci = s.charAt(i);
char cj = s.charAt(j);
if (!vowels.contains(ci)) {
result[i++] = ci;
} else if (!vowels.contains(cj)) {
result[j--] = cj;
} else {
result[i++] = cj;
result[j--] = ci;
}
}
return new String(result);
}
```
# 4. 回文字符串
680\. Valid Palindrome II (Easy)
[Leetcode](https://leetcode.com/problems/valid-palindrome-ii/description/) / [力扣](https://leetcode-cn.com/problems/valid-palindrome-ii/description/)
```html
Input: "abca"
Output: True
Explanation: You could delete the character 'c'.
```
题目描述可以删除一个字符判断是否能构成回文字符串
所谓的回文字符串是指具有左右对称特点的字符串例如 "abcba" 就是一个回文字符串
使用双指针可以很容易判断一个字符串是否是回文字符串令一个指针从左到右遍历一个指针从右到左遍历这两个指针同时移动一个位置每次都判断两个指针指向的字符是否相同如果都相同字符串才是具有左右对称性质的回文字符串
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/fcc941ec-134b-4dcd-bc86-1702fd305300.gif" width="250px"> </div><br>
本题的关键是处理删除一个字符在使用双指针遍历字符串时如果出现两个指针指向的字符不相等的情况我们就试着删除一个字符再判断删除完之后的字符串是否是回文字符串
在判断是否为回文字符串时我们不需要判断整个字符串因为左指针左边和右指针右边的字符之前已经判断过具有对称性质所以只需要判断中间的子字符串即可
在试着删除字符时我们既可以删除左指针指向的字符也可以删除右指针指向的字符
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/db5f30a7-8bfa-4ecc-ab5d-747c77818964.gif" width="300px"> </div><br>
```java
public boolean validPalindrome(String s) {
for (int i = 0, j = s.length() - 1; i < j; i++, j--) {
if (s.charAt(i) != s.charAt(j)) {
return isPalindrome(s, i, j - 1) || isPalindrome(s, i + 1, j);
}
}
return true;
}
private boolean isPalindrome(String s, int i, int j) {
while (i < j) {
if (s.charAt(i++) != s.charAt(j--)) {
return false;
}
}
return true;
}
```
# 5. 归并两个有序数组
88\. Merge Sorted Array (Easy)
[Leetcode](https://leetcode.com/problems/merge-sorted-array/description/) / [力扣](https://leetcode-cn.com/problems/merge-sorted-array/description/)
```html
Input:
nums1 = [1,2,3,0,0,0], m = 3
nums2 = [2,5,6], n = 3
Output: [1,2,2,3,5,6]
```
题目描述把归并结果存到第一个数组上
需要从尾开始遍历否则在 nums1 上归并得到的值会覆盖还未进行归并比较的值
```java
public void merge(int[] nums1, int m, int[] nums2, int n) {
int index1 = m - 1, index2 = n - 1;
int indexMerge = m + n - 1;
while (index1 >= 0 || index2 >= 0) {
if (index1 < 0) {
nums1[indexMerge--] = nums2[index2--];
} else if (index2 < 0) {
nums1[indexMerge--] = nums1[index1--];
} else if (nums1[index1] > nums2[index2]) {
nums1[indexMerge--] = nums1[index1--];
} else {
nums1[indexMerge--] = nums2[index2--];
}
}
}
```
# 6. 判断链表是否存在环
141\. Linked List Cycle (Easy)
[Leetcode](https://leetcode.com/problems/linked-list-cycle/description/) / [力扣](https://leetcode-cn.com/problems/linked-list-cycle/description/)
使用双指针一个指针每次移动一个节点一个指针每次移动两个节点如果存在环那么这两个指针一定会相遇
```java
public boolean hasCycle(ListNode head) {
if (head == null) {
return false;
}
ListNode l1 = head, l2 = head.next;
while (l1 != null && l2 != null && l2.next != null) {
if (l1 == l2) {
return true;
}
l1 = l1.next;
l2 = l2.next.next;
}
return false;
}
```
# 7. 最长子序列
524\. Longest Word in Dictionary through Deleting (Medium)
[Leetcode](https://leetcode.com/problems/longest-word-in-dictionary-through-deleting/description/) / [力扣](https://leetcode-cn.com/problems/longest-word-in-dictionary-through-deleting/description/)
```
Input:
s = "abpcplea", d = ["ale","apple","monkey","plea"]
Output:
"apple"
```
题目描述删除 s 中的一些字符使得它构成字符串列表 d 中的一个字符串找出能构成的最长字符串如果有多个相同长度的结果返回字典序的最小字符串
通过删除字符串 s 中的一个字符能得到字符串 t可以认为 t s 的子序列我们可以使用双指针来判断一个字符串是否为另一个字符串的子序列
```java
public String findLongestWord(String s, List<String> d) {
String longestWord = "";
for (String target : d) {
int l1 = longestWord.length(), l2 = target.length();
if (l1 > l2 || (l1 == l2 && longestWord.compareTo(target) < 0)) {
continue;
}
if (isSubstr(s, target)) {
longestWord = target;
}
}
return longestWord;
}
private boolean isSubstr(String s, String target) {
int i = 0, j = 0;
while (i < s.length() && j < target.length()) {
if (s.charAt(i) == target.charAt(j)) {
j++;
}
i++;
}
return j == target.length();
}
```
<div align="center"><img width="320px" src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/githubio/公众号二维码-2.png"></img></div>

View File

@ -1,140 +0,0 @@
<!-- GFM-TOC -->
* [1. 数组中两个数的和为给定值](#1-数组中两个数的和为给定值)
* [2. 判断数组是否含有重复元素](#2-判断数组是否含有重复元素)
* [3. 最长和谐序列](#3-最长和谐序列)
* [4. 最长连续序列](#4-最长连续序列)
<!-- GFM-TOC -->
哈希表使用 O(N) 空间复杂度存储数据并且以 O(1) 时间复杂度求解问题
- Java 中的 **HashSet** 用于存储一个集合可以查找元素是否在集合中如果元素有穷并且范围不大那么可以用一个布尔数组来存储一个元素是否存在例如对于只有小写字符的元素就可以用一个长度为 26 的布尔数组来存储一个字符集合使得空间复杂度降低为 O(1)
Java 中的 **HashMap** 主要用于映射关系从而把两个元素联系起来HashMap 也可以用来对元素进行计数统计此时键为元素值为计数 HashSet 类似如果元素有穷并且范围不大可以用整型数组来进行统计在对一个内容进行压缩或者其它转换时利用 HashMap 可以把原始内容和转换后的内容联系起来例如在一个简化 url 的系统中 [Leetcdoe : 535. Encode and Decode TinyURL (Medium)
[Leetcode](https://leetcode.com/problems/encode-and-decode-tinyurl/description/)利用 HashMap 就可以存储精简后的 url 到原始 url 的映射使得不仅可以显示简化的 url也可以根据简化的 url 得到原始 url 从而定位到正确的资源<EFBFBD>) / [力扣](https://leetcode-cn.com/problems/encode-and-decode-tinyurl/description/)利用 HashMap 就可以存储精简后的 url 到原始 url 的映射使得不仅可以显示简化的 url也可以根据简化的 url 得到原始 url 从而定位到正确的资源<EFBFBD>)
# 1. 数组中两个数的和为给定值
1\. Two Sum (Easy)
[Leetcode](https://leetcode.com/problems/two-sum/description/) / [力扣](https://leetcode-cn.com/problems/two-sum/description/)
可以先对数组进行排序然后使用双指针方法或者二分查找方法这样做的时间复杂度为 O(NlogN)空间复杂度为 O(1)
HashMap 存储数组元素和索引的映射在访问到 nums[i] 判断 HashMap 中是否存在 target - nums[i]如果存在说明 target - nums[i] 所在的索引和 i 就是要找的两个数该方法的时间复杂度为 O(N)空间复杂度为 O(N)使用空间来换取时间
```java
public int[] twoSum(int[] nums, int target) {
HashMap<Integer, Integer> indexForNum = new HashMap<>();
for (int i = 0; i < nums.length; i++) {
if (indexForNum.containsKey(target - nums[i])) {
return new int[]{indexForNum.get(target - nums[i]), i};
} else {
indexForNum.put(nums[i], i);
}
}
return null;
}
```
# 2. 判断数组是否含有重复元素
217\. Contains Duplicate (Easy)
[Leetcode](https://leetcode.com/problems/contains-duplicate/description/) / [力扣](https://leetcode-cn.com/problems/contains-duplicate/description/)
```java
public boolean containsDuplicate(int[] nums) {
Set<Integer> set = new HashSet<>();
for (int num : nums) {
set.add(num);
}
return set.size() < nums.length;
}
```
# 3. 最长和谐序列
594\. Longest Harmonious Subsequence (Easy)
[Leetcode](https://leetcode.com/problems/longest-harmonious-subsequence/description/) / [力扣](https://leetcode-cn.com/problems/longest-harmonious-subsequence/description/)
```html
Input: [1,3,2,2,5,2,3,7]
Output: 5
Explanation: The longest harmonious subsequence is [3,2,2,2,3].
```
和谐序列中最大数和最小数之差正好为 1应该注意的是序列的元素不一定是数组的连续元素
```java
public int findLHS(int[] nums) {
Map<Integer, Integer> countForNum = new HashMap<>();
for (int num : nums) {
countForNum.put(num, countForNum.getOrDefault(num, 0) + 1);
}
int longest = 0;
for (int num : countForNum.keySet()) {
if (countForNum.containsKey(num + 1)) {
longest = Math.max(longest, countForNum.get(num + 1) + countForNum.get(num));
}
}
return longest;
}
```
# 4. 最长连续序列
128\. Longest Consecutive Sequence (Hard)
[Leetcode](https://leetcode.com/problems/longest-consecutive-sequence/description/) / [力扣](https://leetcode-cn.com/problems/longest-consecutive-sequence/description/)
```html
Given [100, 4, 200, 1, 3, 2],
The longest consecutive elements sequence is [1, 2, 3, 4]. Return its length: 4.
```
要求以 O(N) 的时间复杂度求解
```java
public int longestConsecutive(int[] nums) {
Map<Integer, Integer> countForNum = new HashMap<>();
for (int num : nums) {
countForNum.put(num, 1);
}
for (int num : nums) {
forward(countForNum, num);
}
return maxCount(countForNum);
}
private int forward(Map<Integer, Integer> countForNum, int num) {
if (!countForNum.containsKey(num)) {
return 0;
}
int cnt = countForNum.get(num);
if (cnt > 1) {
return cnt;
}
cnt = forward(countForNum, num + 1) + 1;
countForNum.put(num, cnt);
return cnt;
}
private int maxCount(Map<Integer, Integer> countForNum) {
int max = 0;
for (int num : countForNum.keySet()) {
max = Math.max(max, countForNum.get(num));
}
return max;
}
```
<div align="center"><img width="320px" src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/githubio/公众号二维码-2.png"></img></div>

View File

@ -1,272 +0,0 @@
<!-- GFM-TOC -->
* [二分图](#二分图)
* [1. 判断是否为二分图](#1-判断是否为二分图)
* [拓扑排序](#拓扑排序)
* [1. 课程安排的合法性](#1-课程安排的合法性)
* [2. 课程安排的顺序](#2-课程安排的顺序)
* [并查集](#并查集)
* [1. 冗余连接](#1-冗余连接)
<!-- GFM-TOC -->
# 二分图
如果可以用两种颜色对图中的节点进行着色并且保证相邻的节点颜色不同那么这个图就是二分图
## 1. 判断是否为二分图
785\. Is Graph Bipartite? (Medium)
[Leetcode](https://leetcode.com/problems/is-graph-bipartite/description/) / [力扣](https://leetcode-cn.com/problems/is-graph-bipartite/description/)
```html
Input: [[1,3], [0,2], [1,3], [0,2]]
Output: true
Explanation:
The graph looks like this:
0----1
| |
| |
3----2
We can divide the vertices into two groups: {0, 2} and {1, 3}.
```
```html
Example 2:
Input: [[1,2,3], [0,2], [0,1,3], [0,2]]
Output: false
Explanation:
The graph looks like this:
0----1
| \ |
| \ |
3----2
We cannot find a way to divide the set of nodes into two independent subsets.
```
```java
public boolean isBipartite(int[][] graph) {
int[] colors = new int[graph.length];
Arrays.fill(colors, -1);
for (int i = 0; i < graph.length; i++) { // 处理图不是连通的情况
if (colors[i] == -1 && !isBipartite(i, 0, colors, graph)) {
return false;
}
}
return true;
}
private boolean isBipartite(int curNode, int curColor, int[] colors, int[][] graph) {
if (colors[curNode] != -1) {
return colors[curNode] == curColor;
}
colors[curNode] = curColor;
for (int nextNode : graph[curNode]) {
if (!isBipartite(nextNode, 1 - curColor, colors, graph)) {
return false;
}
}
return true;
}
```
# 拓扑排序
常用于在具有先序关系的任务规划中
## 1. 课程安排的合法性
207\. Course Schedule (Medium)
[Leetcode](https://leetcode.com/problems/course-schedule/description/) / [力扣](https://leetcode-cn.com/problems/course-schedule/description/)
```html
2, [[1,0]]
return true
```
```html
2, [[1,0],[0,1]]
return false
```
题目描述一个课程可能会先修课程判断给定的先修课程规定是否合法
本题不需要使用拓扑排序只需要检测有向图是否存在环即可
```java
public boolean canFinish(int numCourses, int[][] prerequisites) {
List<Integer>[] graphic = new List[numCourses];
for (int i = 0; i < numCourses; i++) {
graphic[i] = new ArrayList<>();
}
for (int[] pre : prerequisites) {
graphic[pre[0]].add(pre[1]);
}
boolean[] globalMarked = new boolean[numCourses];
boolean[] localMarked = new boolean[numCourses];
for (int i = 0; i < numCourses; i++) {
if (hasCycle(globalMarked, localMarked, graphic, i)) {
return false;
}
}
return true;
}
private boolean hasCycle(boolean[] globalMarked, boolean[] localMarked,
List<Integer>[] graphic, int curNode) {
if (localMarked[curNode]) {
return true;
}
if (globalMarked[curNode]) {
return false;
}
globalMarked[curNode] = true;
localMarked[curNode] = true;
for (int nextNode : graphic[curNode]) {
if (hasCycle(globalMarked, localMarked, graphic, nextNode)) {
return true;
}
}
localMarked[curNode] = false;
return false;
}
```
## 2. 课程安排的顺序
210\. Course Schedule II (Medium)
[Leetcode](https://leetcode.com/problems/course-schedule-ii/description/) / [力扣](https://leetcode-cn.com/problems/course-schedule-ii/description/)
```html
4, [[1,0],[2,0],[3,1],[3,2]]
There are a total of 4 courses to take. To take course 3 you should have finished both courses 1 and 2. Both courses 1 and 2 should be taken after you finished course 0. So one correct course order is [0,1,2,3]. Another correct ordering is[0,2,1,3].
```
使用 DFS 来实现拓扑排序使用一个栈存储后序遍历结果这个栈的逆序结果就是拓扑排序结果
证明对于任何先序关系v->w后序遍历结果可以保证 w 先进入栈中因此栈的逆序结果中 v 会在 w 之前
```java
public int[] findOrder(int numCourses, int[][] prerequisites) {
List<Integer>[] graphic = new List[numCourses];
for (int i = 0; i < numCourses; i++) {
graphic[i] = new ArrayList<>();
}
for (int[] pre : prerequisites) {
graphic[pre[0]].add(pre[1]);
}
Stack<Integer> postOrder = new Stack<>();
boolean[] globalMarked = new boolean[numCourses];
boolean[] localMarked = new boolean[numCourses];
for (int i = 0; i < numCourses; i++) {
if (hasCycle(globalMarked, localMarked, graphic, i, postOrder)) {
return new int[0];
}
}
int[] orders = new int[numCourses];
for (int i = numCourses - 1; i >= 0; i--) {
orders[i] = postOrder.pop();
}
return orders;
}
private boolean hasCycle(boolean[] globalMarked, boolean[] localMarked, List<Integer>[] graphic,
int curNode, Stack<Integer> postOrder) {
if (localMarked[curNode]) {
return true;
}
if (globalMarked[curNode]) {
return false;
}
globalMarked[curNode] = true;
localMarked[curNode] = true;
for (int nextNode : graphic[curNode]) {
if (hasCycle(globalMarked, localMarked, graphic, nextNode, postOrder)) {
return true;
}
}
localMarked[curNode] = false;
postOrder.push(curNode);
return false;
}
```
# 并查集
并查集可以动态地连通两个点并且可以非常快速地判断两个点是否连通
## 1. 冗余连接
684\. Redundant Connection (Medium)
[Leetcode](https://leetcode.com/problems/redundant-connection/description/) / [力扣](https://leetcode-cn.com/problems/redundant-connection/description/)
```html
Input: [[1,2], [1,3], [2,3]]
Output: [2,3]
Explanation: The given undirected graph will be like this:
1
/ \
2 - 3
```
题目描述有一系列的边连成的图找出一条边移除它之后该图能够成为一棵树
```java
public int[] findRedundantConnection(int[][] edges) {
int N = edges.length;
UF uf = new UF(N);
for (int[] e : edges) {
int u = e[0], v = e[1];
if (uf.connect(u, v)) {
return e;
}
uf.union(u, v);
}
return new int[]{-1, -1};
}
private class UF {
private int[] id;
UF(int N) {
id = new int[N + 1];
for (int i = 0; i < id.length; i++) {
id[i] = i;
}
}
void union(int u, int v) {
int uID = find(u);
int vID = find(v);
if (uID == vID) {
return;
}
for (int i = 0; i < id.length; i++) {
if (id[i] == uID) {
id[i] = vID;
}
}
}
int find(int p) {
return id[p];
}
boolean connect(int u, int v) {
return find(u) == find(v);
}
}
```
<div align="center"><img width="320px" src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/githubio/公众号二维码-2.png"></img></div>

View File

@ -1,244 +0,0 @@
<!-- GFM-TOC -->
* [1. 字符串循环移位包含](#1-字符串循环移位包含)
* [2. 字符串循环移位](#2-字符串循环移位)
* [3. 字符串中单词的翻转](#3-字符串中单词的翻转)
* [4. 两个字符串包含的字符是否完全相同](#4-两个字符串包含的字符是否完全相同)
* [5. 计算一组字符集合可以组成的回文字符串的最大长度](#5-计算一组字符集合可以组成的回文字符串的最大长度)
* [6. 字符串同构](#6-字符串同构)
* [7. 回文子字符串个数](#7-回文子字符串个数)
* [8. 判断一个整数是否是回文数](#8-判断一个整数是否是回文数)
* [9. 统计二进制字符串中连续 1 和连续 0 数量相同的子字符串个数](#9-统计二进制字符串中连续-1-和连续-0-数量相同的子字符串个数)
<!-- GFM-TOC -->
# 1. 字符串循环移位包含
[编程之美 3.1](#)
```html
s1 = AABCD, s2 = CDAA
Return : true
```
给定两个字符串 s1 s2要求判定 s2 是否能够被 s1 做循环移位得到的字符串包含
s1 进行循环移位的结果是 s1s1 的子字符串因此只要判断 s2 是否是 s1s1 的子字符串即可
# 2. 字符串循环移位
[编程之美 2.17](#)
```html
s = "abcd123" k = 3
Return "123abcd"
```
将字符串向右循环移动 k
abcd123 中的 abcd 123 单独翻转得到 dcba321然后对整个字符串进行翻转得到 123abcd
# 3. 字符串中单词的翻转
[程序员代码面试指南](#)
```html
s = "I am a student"
Return "student a am I"
```
将每个单词翻转然后将整个字符串翻转
# 4. 两个字符串包含的字符是否完全相同
242\. Valid Anagram (Easy)
[Leetcode](https://leetcode.com/problems/valid-anagram/description/) / [力扣](https://leetcode-cn.com/problems/valid-anagram/description/)
```html
s = "anagram", t = "nagaram", return true.
s = "rat", t = "car", return false.
```
可以用 HashMap 来映射字符与出现次数然后比较两个字符串出现的字符数量是否相同
由于本题的字符串只包含 26 个小写字符因此可以使用长度为 26 的整型数组对字符串出现的字符进行统计不再使用 HashMap
```java
public boolean isAnagram(String s, String t) {
int[] cnts = new int[26];
for (char c : s.toCharArray()) {
cnts[c - 'a']++;
}
for (char c : t.toCharArray()) {
cnts[c - 'a']--;
}
for (int cnt : cnts) {
if (cnt != 0) {
return false;
}
}
return true;
}
```
# 5. 计算一组字符集合可以组成的回文字符串的最大长度
409\. Longest Palindrome (Easy)
[Leetcode](https://leetcode.com/problems/longest-palindrome/description/) / [力扣](https://leetcode-cn.com/problems/longest-palindrome/description/)
```html
Input : "abccccdd"
Output : 7
Explanation : One longest palindrome that can be built is "dccaccd", whose length is 7.
```
使用长度为 256 的整型数组来统计每个字符出现的个数每个字符有偶数个可以用来构成回文字符串
因为回文字符串最中间的那个字符可以单独出现所以如果有单独的字符就把它放到最中间
```java
public int longestPalindrome(String s) {
int[] cnts = new int[256];
for (char c : s.toCharArray()) {
cnts[c]++;
}
int palindrome = 0;
for (int cnt : cnts) {
palindrome += (cnt / 2) * 2;
}
if (palindrome < s.length()) {
palindrome++; // 这个条件下 s 中一定有单个未使用的字符存在可以把这个字符放到回文的最中间
}
return palindrome;
}
```
# 6. 字符串同构
205\. Isomorphic Strings (Easy)
[Leetcode](https://leetcode.com/problems/isomorphic-strings/description/) / [力扣](https://leetcode-cn.com/problems/isomorphic-strings/description/)
```html
Given "egg", "add", return true.
Given "foo", "bar", return false.
Given "paper", "title", return true.
```
记录一个字符上次出现的位置如果两个字符串中的字符上次出现的位置一样那么就属于同构
```java
public boolean isIsomorphic(String s, String t) {
int[] preIndexOfS = new int[256];
int[] preIndexOfT = new int[256];
for (int i = 0; i < s.length(); i++) {
char sc = s.charAt(i), tc = t.charAt(i);
if (preIndexOfS[sc] != preIndexOfT[tc]) {
return false;
}
preIndexOfS[sc] = i + 1;
preIndexOfT[tc] = i + 1;
}
return true;
}
```
# 7. 回文子字符串个数
647\. Palindromic Substrings (Medium)
[Leetcode](https://leetcode.com/problems/palindromic-substrings/description/) / [力扣](https://leetcode-cn.com/problems/palindromic-substrings/description/)
```html
Input: "aaa"
Output: 6
Explanation: Six palindromic strings: "a", "a", "a", "aa", "aa", "aaa".
```
从字符串的某一位开始尝试着去扩展子字符串
```java
private int cnt = 0;
public int countSubstrings(String s) {
for (int i = 0; i < s.length(); i++) {
extendSubstrings(s, i, i); // 奇数长度
extendSubstrings(s, i, i + 1); // 偶数长度
}
return cnt;
}
private void extendSubstrings(String s, int start, int end) {
while (start >= 0 && end < s.length() && s.charAt(start) == s.charAt(end)) {
start--;
end++;
cnt++;
}
}
```
# 8. 判断一个整数是否是回文数
9\. Palindrome Number (Easy)
[Leetcode](https://leetcode.com/problems/palindrome-number/description/) / [力扣](https://leetcode-cn.com/problems/palindrome-number/description/)
要求不能使用额外空间也就不能将整数转换为字符串进行判断
将整数分成左右两部分右边那部分需要转置然后判断这两部分是否相等
```java
public boolean isPalindrome(int x) {
if (x == 0) {
return true;
}
if (x < 0 || x % 10 == 0) {
return false;
}
int right = 0;
while (x > right) {
right = right * 10 + x % 10;
x /= 10;
}
return x == right || x == right / 10;
}
```
# 9. 统计二进制字符串中连续 1 和连续 0 数量相同的子字符串个数
696\. Count Binary Substrings (Easy)
[Leetcode](https://leetcode.com/problems/count-binary-substrings/description/) / [力扣](https://leetcode-cn.com/problems/count-binary-substrings/description/)
```html
Input: "00110011"
Output: 6
Explanation: There are 6 substrings that have equal number of consecutive 1's and 0's: "0011", "01", "1100", "10", "0011", and "01".
```
```java
public int countBinarySubstrings(String s) {
int preLen = 0, curLen = 1, count = 0;
for (int i = 1; i < s.length(); i++) {
if (s.charAt(i) == s.charAt(i - 1)) {
curLen++;
} else {
preLen = curLen;
curLen = 1;
}
if (preLen >= curLen) {
count++;
}
}
return count;
}
```
<div align="center"><img width="320px" src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/githubio/公众号二维码-2.png"></img></div>

View File

@ -1,249 +0,0 @@
<!-- GFM-TOC -->
* [快速选择](#快速选择)
* [](#)
* [1. Kth Element](#1-kth-element)
* [桶排序](#桶排序)
* [1. 出现频率最多的 k 个元素](#1-出现频率最多的-k-个元素)
* [2. 按照字符出现次数对字符串排序](#2-按照字符出现次数对字符串排序)
* [荷兰国旗问题](#荷兰国旗问题)
* [1. 按颜色进行排序](#1-按颜色进行排序)
<!-- GFM-TOC -->
# 快速选择
用于求解 **Kth Element** 问题也就是第 K 个元素的问题
可以使用快速排序的 partition() 进行实现需要先打乱数组否则最坏情况下时间复杂度为 O(N<sup>2</sup>)
#
用于求解 **TopK Elements** 问题也就是 K 个最小元素的问题使用最小堆来实现 TopK 问题最小堆使用大顶堆来实现大顶堆的堆顶元素为当前堆的最大元素实现过程不断地往大顶堆中插入新元素当堆中元素的数量大于 k 移除堆顶元素也就是当前堆中最大的元素剩下的元素都为当前添加过的元素中最小的 K 个元素插入和移除堆顶元素的时间复杂度都为 log<sub>2</sub>N
堆也可以用于求解 Kth Element 问题得到了大小为 K 的最小堆之后因为使用了大顶堆来实现因此堆顶元素就是第 K 大的元素
快速选择也可以求解 TopK Elements 问题因为找到 Kth Element 之后再遍历一次数组所有小于等于 Kth Element 的元素都是 TopK Elements
可以看到快速选择和堆排序都可以求解 Kth Element TopK Elements 问题
## 1. Kth Element
215\. Kth Largest Element in an Array (Medium)
[Leetcode](https://leetcode.com/problems/kth-largest-element-in-an-array/description/) / [力扣](https://leetcode-cn.com/problems/kth-largest-element-in-an-array/description/)
```text
Input: [3,2,1,5,6,4] and k = 2
Output: 5
```
题目描述找到倒数第 k 个的元素
**排序** 时间复杂度 O(NlogN)空间复杂度 O(1)
```java
public int findKthLargest(int[] nums, int k) {
Arrays.sort(nums);
return nums[nums.length - k];
}
```
**** 时间复杂度 O(NlogK)空间复杂度 O(K)
```java
public int findKthLargest(int[] nums, int k) {
PriorityQueue<Integer> pq = new PriorityQueue<>(); // 小顶堆
for (int val : nums) {
pq.add(val);
if (pq.size() > k) // 维护堆的大小为 K
pq.poll();
}
return pq.peek();
}
```
**快速选择** 时间复杂度 O(N)空间复杂度 O(1)
```java
public int findKthLargest(int[] nums, int k) {
k = nums.length - k;
int l = 0, h = nums.length - 1;
while (l < h) {
int j = partition(nums, l, h);
if (j == k) {
break;
} else if (j < k) {
l = j + 1;
} else {
h = j - 1;
}
}
return nums[k];
}
private int partition(int[] a, int l, int h) {
int i = l, j = h + 1;
while (true) {
while (a[++i] < a[l] && i < h) ;
while (a[--j] > a[l] && j > l) ;
if (i >= j) {
break;
}
swap(a, i, j);
}
swap(a, l, j);
return j;
}
private void swap(int[] a, int i, int j) {
int t = a[i];
a[i] = a[j];
a[j] = t;
}
```
# 桶排序
## 1. 出现频率最多的 k 个元素
347\. Top K Frequent Elements (Medium)
[Leetcode](https://leetcode.com/problems/top-k-frequent-elements/description/) / [力扣](https://leetcode-cn.com/problems/top-k-frequent-elements/description/)
```html
Given [1,1,1,2,2,3] and k = 2, return [1,2].
```
设置若干个桶每个桶存储出现频率相同的数桶的下标表示数出现的频率即第 i 个桶中存储的数出现的频率为 i
把数都放到桶之后从后向前遍历桶最先得到的 k 个数就是出现频率最多的的 k 个数
```java
public int[] topKFrequent(int[] nums, int k) {
Map<Integer, Integer> frequencyForNum = new HashMap<>();
for (int num : nums) {
frequencyForNum.put(num, frequencyForNum.getOrDefault(num, 0) + 1);
}
List<Integer>[] buckets = new ArrayList[nums.length + 1];
for (int key : frequencyForNum.keySet()) {
int frequency = frequencyForNum.get(key);
if (buckets[frequency] == null) {
buckets[frequency] = new ArrayList<>();
}
buckets[frequency].add(key);
}
List<Integer> topK = new ArrayList<>();
for (int i = buckets.length - 1; i >= 0 && topK.size() < k; i--) {
if (buckets[i] == null) {
continue;
}
if (buckets[i].size() <= (k - topK.size())) {
topK.addAll(buckets[i]);
} else {
topK.addAll(buckets[i].subList(0, k - topK.size()));
}
}
int[] res = new int[k];
for (int i = 0; i < k; i++) {
res[i] = topK.get(i);
}
return res;
}
```
## 2. 按照字符出现次数对字符串排序
451\. Sort Characters By Frequency (Medium)
[Leetcode](https://leetcode.com/problems/sort-characters-by-frequency/description/) / [力扣](https://leetcode-cn.com/problems/sort-characters-by-frequency/description/)
```html
Input:
"tree"
Output:
"eert"
Explanation:
'e' appears twice while 'r' and 't' both appear once.
So 'e' must appear before both 'r' and 't'. Therefore "eetr" is also a valid answer.
```
```java
public String frequencySort(String s) {
Map<Character, Integer> frequencyForNum = new HashMap<>();
for (char c : s.toCharArray())
frequencyForNum.put(c, frequencyForNum.getOrDefault(c, 0) + 1);
List<Character>[] frequencyBucket = new ArrayList[s.length() + 1];
for (char c : frequencyForNum.keySet()) {
int f = frequencyForNum.get(c);
if (frequencyBucket[f] == null) {
frequencyBucket[f] = new ArrayList<>();
}
frequencyBucket[f].add(c);
}
StringBuilder str = new StringBuilder();
for (int i = frequencyBucket.length - 1; i >= 0; i--) {
if (frequencyBucket[i] == null) {
continue;
}
for (char c : frequencyBucket[i]) {
for (int j = 0; j < i; j++) {
str.append(c);
}
}
}
return str.toString();
}
```
# 荷兰国旗问题
荷兰国旗包含三种颜色
有三种颜色的球算法的目标是将这三种球按颜色顺序正确地排列它其实是三向切分快速排序的一种变种在三向切分快速排序中每次切分都将数组分成三个区间小于切分元素等于切分元素大于切分元素而该算法是将数组分成三个区间等于红色等于白色等于蓝色
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/7a3215ec-6fb7-4935-8b0d-cb408208f7cb.png"/> </div><br>
## 1. 按颜色进行排序
75\. Sort Colors (Medium)
[Leetcode](https://leetcode.com/problems/sort-colors/description/) / [力扣](https://leetcode-cn.com/problems/sort-colors/description/)
```html
Input: [2,0,2,1,1,0]
Output: [0,0,1,1,2,2]
```
题目描述只有 0/1/2 三种颜色
```java
public void sortColors(int[] nums) {
int zero = -1, one = 0, two = nums.length;
while (one < two) {
if (nums[one] == 0) {
swap(nums, ++zero, one++);
} else if (nums[one] == 2) {
swap(nums, --two, one);
} else {
++one;
}
}
}
private void swap(int[] nums, int i, int j) {
int t = nums[i];
nums[i] = nums[j];
nums[j] = t;
}
```
<div align="center"><img width="320px" src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/githubio/公众号二维码-2.png"></img></div>

File diff suppressed because it is too large Load Diff

View File

@ -1,540 +0,0 @@
<!-- GFM-TOC -->
* [素数分解](#素数分解)
* [整除](#整除)
* [最大公约数最小公倍数](#最大公约数最小公倍数)
* [1. 生成素数序列](#1-生成素数序列)
* [2. 最大公约数](#2-最大公约数)
* [3. 使用位操作和减法求解最大公约数](#3-使用位操作和减法求解最大公约数)
* [进制转换](#进制转换)
* [1. 7 进制](#1-7-进制)
* [2. 16 进制](#2-16-进制)
* [3. 26 进制](#3-26-进制)
* [阶乘](#阶乘)
* [1. 统计阶乘尾部有多少个 0](#1-统计阶乘尾部有多少个-0)
* [字符串加法减法](#字符串加法减法)
* [1. 二进制加法](#1-二进制加法)
* [2. 字符串加法](#2-字符串加法)
* [相遇问题](#相遇问题)
* [1. 改变数组元素使所有的数组元素都相等](#1-改变数组元素使所有的数组元素都相等)
* [多数投票问题](#多数投票问题)
* [1. 数组中出现次数多于 n / 2 的元素](#1-数组中出现次数多于-n--2-的元素)
* [其它](#其它)
* [1. 平方数](#1-平方数)
* [2. 3 n 次方](#2-3--n-次方)
* [3. 乘积数组](#3-乘积数组)
* [4. 找出数组中的乘积最大的三个数](#4-找出数组中的乘积最大的三个数)
<!-- GFM-TOC -->
# 素数分解
每一个数都可以分解成素数的乘积例如 84 = 2<sup>2</sup> \* 3<sup>1</sup> \* 5<sup>0</sup> \* 7<sup>1</sup> \* 11<sup>0</sup> \* 13<sup>0</sup> \* 17<sup>0</sup> \*
# 整除
x = 2<sup>m0</sup> \* 3<sup>m1</sup> \* 5<sup>m2</sup> \* 7<sup>m3</sup> \* 11<sup>m4</sup> \*
y = 2<sup>n0</sup> \* 3<sup>n1</sup> \* 5<sup>n2</sup> \* 7<sup>n3</sup> \* 11<sup>n4</sup> \*
如果 x 整除 yy mod x == 0则对于所有 imi <= ni
# 最大公约数最小公倍数
x y 的最大公约数为gcd(x,y) = 2<sup>min(m0,n0)</sup> \* 3<sup>min(m1,n1)</sup> \* 5<sup>min(m2,n2)</sup> \* ...
x y 的最小公倍数为lcm(x,y) = 2<sup>max(m0,n0)</sup> \* 3<sup>max(m1,n1)</sup> \* 5<sup>max(m2,n2)</sup> \* ...
## 1. 生成素数序列
204\. Count Primes (Easy)
[Leetcode](https://leetcode.com/problems/count-primes/description/) / [力扣](https://leetcode-cn.com/problems/count-primes/description/)
埃拉托斯特尼筛法在每次找到一个素数时将能被素数整除的数排除掉
```java
public int countPrimes(int n) {
boolean[] notPrimes = new boolean[n + 1];
int count = 0;
for (int i = 2; i < n; i++) {
if (notPrimes[i]) {
continue;
}
count++;
// i * i 开始因为如果 k < i那么 k * i 在之前就已经被去除过了
for (long j = (long) (i) * i; j < n; j += i) {
notPrimes[(int) j] = true;
}
}
return count;
}
```
## 2. 最大公约数
```java
int gcd(int a, int b) {
return b == 0 ? a : gcd(b, a % b);
}
```
最小公倍数为两数的乘积除以最大公约数
```java
int lcm(int a, int b) {
return a * b / gcd(a, b);
}
```
## 3. 使用位操作和减法求解最大公约数
[编程之美2.7](#)
对于 a b 的最大公约数 f(a, b)
- 如果 a b 均为偶数f(a, b) = 2\*f(a/2, b/2);
- 如果 a 是偶数 b 是奇数f(a, b) = f(a/2, b);
- 如果 b 是偶数 a 是奇数f(a, b) = f(a, b/2);
- 如果 a b 均为奇数f(a, b) = f(b, a-b);
2 和除 2 都可以转换为移位操作
```java
public int gcd(int a, int b) {
if (a < b) {
return gcd(b, a);
}
if (b == 0) {
return a;
}
boolean isAEven = isEven(a), isBEven = isEven(b);
if (isAEven && isBEven) {
return 2 * gcd(a >> 1, b >> 1);
} else if (isAEven && !isBEven) {
return gcd(a >> 1, b);
} else if (!isAEven && isBEven) {
return gcd(a, b >> 1);
} else {
return gcd(b, a - b);
}
}
```
# 进制转换
## 1. 7 进制
504\. Base 7 (Easy)
[Leetcode](https://leetcode.com/problems/base-7/description/) / [力扣](https://leetcode-cn.com/problems/base-7/description/)
```java
public String convertToBase7(int num) {
if (num == 0) {
return "0";
}
StringBuilder sb = new StringBuilder();
boolean isNegative = num < 0;
if (isNegative) {
num = -num;
}
while (num > 0) {
sb.append(num % 7);
num /= 7;
}
String ret = sb.reverse().toString();
return isNegative ? "-" + ret : ret;
}
```
Java static String toString(int num, int radix) 可以将一个整数转换为 radix 进制表示的字符串
```java
public String convertToBase7(int num) {
return Integer.toString(num, 7);
}
```
## 2. 16 进制
405\. Convert a Number to Hexadecimal (Easy)
[Leetcode](https://leetcode.com/problems/convert-a-number-to-hexadecimal/description/) / [力扣](https://leetcode-cn.com/problems/convert-a-number-to-hexadecimal/description/)
```html
Input:
26
Output:
"1a"
Input:
-1
Output:
"ffffffff"
```
负数要用它的补码形式
```java
public String toHex(int num) {
char[] map = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'};
if (num == 0) return "0";
StringBuilder sb = new StringBuilder();
while (num != 0) {
sb.append(map[num & 0b1111]);
num >>>= 4; // 因为考虑的是补码形式因此符号位就不能有特殊的意义需要使用无符号右移左边填 0
}
return sb.reverse().toString();
}
```
## 3. 26 进制
168\. Excel Sheet Column Title (Easy)
[Leetcode](https://leetcode.com/problems/excel-sheet-column-title/description/) / [力扣](https://leetcode-cn.com/problems/excel-sheet-column-title/description/)
```html
1 -> A
2 -> B
3 -> C
...
26 -> Z
27 -> AA
28 -> AB
```
因为是从 1 开始计算的而不是从 0 开始因此需要对 n 执行 -1 操作
```java
public String convertToTitle(int n) {
if (n == 0) {
return "";
}
n--;
return convertToTitle(n / 26) + (char) (n % 26 + 'A');
}
```
# 阶乘
## 1. 统计阶乘尾部有多少个 0
172\. Factorial Trailing Zeroes (Easy)
[Leetcode](https://leetcode.com/problems/factorial-trailing-zeroes/description/) / [力扣](https://leetcode-cn.com/problems/factorial-trailing-zeroes/description/)
尾部的 0 2 * 5 得来2 的数量明显多于 5 的数量因此只要统计有多少个 5 即可
对于一个数 N它所包含 5 的个数为N/5 + N/5<sup>2</sup> + N/5<sup>3</sup> + ...其中 N/5 表示不大于 N 的数中 5 的倍数贡献一个 5N/5<sup>2</sup> 表示不大于 N 的数中 5<sup>2</sup> 的倍数再贡献一个 5 ...
```java
public int trailingZeroes(int n) {
return n == 0 ? 0 : n / 5 + trailingZeroes(n / 5);
}
```
如果统计的是 N! 的二进制表示中最低位 1 的位置只要统计有多少个 2 即可该题目出自 [编程之美2.2](#) 和求解有多少个 5 一样2 的个数为 N/2 + N/2<sup>2</sup> + N/2<sup>3</sup> + ...
# 字符串加法减法
## 1. 二进制加法
67\. Add Binary (Easy)
[Leetcode](https://leetcode.com/problems/add-binary/description/) / [力扣](https://leetcode-cn.com/problems/add-binary/description/)
```html
a = "11"
b = "1"
Return "100".
```
```java
public String addBinary(String a, String b) {
int i = a.length() - 1, j = b.length() - 1, carry = 0;
StringBuilder str = new StringBuilder();
while (carry == 1 || i >= 0 || j >= 0) {
if (i >= 0 && a.charAt(i--) == '1') {
carry++;
}
if (j >= 0 && b.charAt(j--) == '1') {
carry++;
}
str.append(carry % 2);
carry /= 2;
}
return str.reverse().toString();
}
```
## 2. 字符串加法
415\. Add Strings (Easy)
[Leetcode](https://leetcode.com/problems/add-strings/description/) / [力扣](https://leetcode-cn.com/problems/add-strings/description/)
字符串的值为非负整数
```java
public String addStrings(String num1, String num2) {
StringBuilder str = new StringBuilder();
int carry = 0, i = num1.length() - 1, j = num2.length() - 1;
while (carry == 1 || i >= 0 || j >= 0) {
int x = i < 0 ? 0 : num1.charAt(i--) - '0';
int y = j < 0 ? 0 : num2.charAt(j--) - '0';
str.append((x + y + carry) % 10);
carry = (x + y + carry) / 10;
}
return str.reverse().toString();
}
```
# 相遇问题
## 1. 改变数组元素使所有的数组元素都相等
462\. Minimum Moves to Equal Array Elements II (Medium)
[Leetcode](https://leetcode.com/problems/minimum-moves-to-equal-array-elements-ii/description/) / [力扣](https://leetcode-cn.com/problems/minimum-moves-to-equal-array-elements-ii/description/)
```html
Input:
[1,2,3]
Output:
2
Explanation:
Only two moves are needed (remember each move increments or decrements one element):
[1,2,3] => [2,2,3] => [2,2,2]
```
每次可以对一个数组元素加一或者减一求最小的改变次数
这是个典型的相遇问题移动距离最小的方式是所有元素都移动到中位数理由如下
m 为中位数a b m 两边的两个元素 b > a要使 a b 相等它们总共移动的次数为 b - a这个值等于 (b - m) + (m - a)也就是把这两个数移动到中位数的移动次数
设数组长度为 N则可以找到 N/2 a b 的组合使它们都移动到 m 的位置
**解法 1**
先排序时间复杂度O(NlogN)
```java
public int minMoves2(int[] nums) {
Arrays.sort(nums);
int move = 0;
int l = 0, h = nums.length - 1;
while (l <= h) {
move += nums[h] - nums[l];
l++;
h--;
}
return move;
}
```
**解法 2**
使用快速选择找到中位数时间复杂度 O(N)
```java
public int minMoves2(int[] nums) {
int move = 0;
int median = findKthSmallest(nums, nums.length / 2);
for (int num : nums) {
move += Math.abs(num - median);
}
return move;
}
private int findKthSmallest(int[] nums, int k) {
int l = 0, h = nums.length - 1;
while (l < h) {
int j = partition(nums, l, h);
if (j == k) {
break;
}
if (j < k) {
l = j + 1;
} else {
h = j - 1;
}
}
return nums[k];
}
private int partition(int[] nums, int l, int h) {
int i = l, j = h + 1;
while (true) {
while (nums[++i] < nums[l] && i < h) ;
while (nums[--j] > nums[l] && j > l) ;
if (i >= j) {
break;
}
swap(nums, i, j);
}
swap(nums, l, j);
return j;
}
private void swap(int[] nums, int i, int j) {
int tmp = nums[i];
nums[i] = nums[j];
nums[j] = tmp;
}
```
# 多数投票问题
## 1. 数组中出现次数多于 n / 2 的元素
169\. Majority Element (Easy)
[Leetcode](https://leetcode.com/problems/majority-element/description/) / [力扣](https://leetcode-cn.com/problems/majority-element/description/)
先对数组排序最中间那个数出现次数一定多于 n / 2
```java
public int majorityElement(int[] nums) {
Arrays.sort(nums);
return nums[nums.length / 2];
}
```
可以利用 Boyer-Moore Majority Vote Algorithm 来解决这个问题使得时间复杂度为 O(N)可以这么理解该算法使用 cnt 来统计一个元素出现的次数当遍历到的元素和统计元素不相等时 cnt--如果前面查找了 i 个元素 cnt == 0说明前 i 个元素没有 majority或者有 majority但是出现的次数少于 i / 2因为如果多于 i / 2 的话 cnt 就一定不会为 0此时剩下的 n - i 个元素中majority 的数目依然多于 (n - i) / 2因此继续查找就能找出 majority
```java
public int majorityElement(int[] nums) {
int cnt = 0, majority = nums[0];
for (int num : nums) {
majority = (cnt == 0) ? num : majority;
cnt = (majority == num) ? cnt + 1 : cnt - 1;
}
return majority;
}
```
# 其它
## 1. 平方数
367\. Valid Perfect Square (Easy)
[Leetcode](https://leetcode.com/problems/valid-perfect-square/description/) / [力扣](https://leetcode-cn.com/problems/valid-perfect-square/description/)
```html
Input: 16
Returns: True
```
平方序列1,4,9,16,..
间隔3,5,7,...
间隔为等差数列使用这个特性可以得到从 1 开始的平方序列
```java
public boolean isPerfectSquare(int num) {
int subNum = 1;
while (num > 0) {
num -= subNum;
subNum += 2;
}
return num == 0;
}
```
## 2. 3 n 次方
326\. Power of Three (Easy)
[Leetcode](https://leetcode.com/problems/power-of-three/description/) / [力扣](https://leetcode-cn.com/problems/power-of-three/description/)
```java
public boolean isPowerOfThree(int n) {
return n > 0 && (1162261467 % n == 0);
}
```
## 3. 乘积数组
238\. Product of Array Except Self (Medium)
[Leetcode](https://leetcode.com/problems/product-of-array-except-self/description/) / [力扣](https://leetcode-cn.com/problems/product-of-array-except-self/description/)
```html
For example, given [1,2,3,4], return [24,12,8,6].
```
给定一个数组创建一个新数组新数组的每个元素为原始数组中除了该位置上的元素之外所有元素的乘积
要求时间复杂度为 O(N)并且不能使用除法
```java
public int[] productExceptSelf(int[] nums) {
int n = nums.length;
int[] products = new int[n];
Arrays.fill(products, 1);
int left = 1;
for (int i = 1; i < n; i++) {
left *= nums[i - 1];
products[i] *= left;
}
int right = 1;
for (int i = n - 2; i >= 0; i--) {
right *= nums[i + 1];
products[i] *= right;
}
return products;
}
```
## 4. 找出数组中的乘积最大的三个数
628\. Maximum Product of Three Numbers (Easy)
[Leetcode](https://leetcode.com/problems/maximum-product-of-three-numbers/description/) / [力扣](https://leetcode-cn.com/problems/maximum-product-of-three-numbers/description/)
```html
Input: [1,2,3,4]
Output: 24
```
```java
public int maximumProduct(int[] nums) {
int max1 = Integer.MIN_VALUE, max2 = Integer.MIN_VALUE, max3 = Integer.MIN_VALUE, min1 = Integer.MAX_VALUE, min2 = Integer.MAX_VALUE;
for (int n : nums) {
if (n > max1) {
max3 = max2;
max2 = max1;
max1 = n;
} else if (n > max2) {
max3 = max2;
max2 = n;
} else if (n > max3) {
max3 = n;
}
if (n < min1) {
min2 = min1;
min1 = n;
} else if (n < min2) {
min2 = n;
}
}
return Math.max(max1*max2*max3, max1*min1*min2);
}
```
<div align="center"><img width="320px" src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/githubio/公众号二维码-2.png"></img></div>

Some files were not shown because too many files have changed in this diff Show More