auto commit
This commit is contained in:
parent
f5ad47b470
commit
7e61fc1360
|
@ -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/数据库系统原理.md) </br>
|
||||
- [SQL 语法](notes/SQL%20语法.md) </br>
|
||||
- [SQL 练习](notes/SQL%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)
|
||||
|
||||
## 🎨 面向对象
|
||||
|
||||
- [设计模式](notes/设计模式%20-%20目录1.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
1
docs/_404.md
Normal file
|
@ -0,0 +1 @@
|
|||
# 😃 该网站已迁移至 >>> [www.cyc2018.xyz](http://www.cyc2018.xyz)
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -1,74 +0,0 @@
|
|||
# 11. 旋转数组的最小数字
|
||||
|
||||
## 题目链接
|
||||
|
||||
[牛客网](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(log<sub>2</sub>N)。
|
||||
|
||||
<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 代表 low,m 代表 mid,h 代表 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},l、m 和 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>
|
|
@ -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>
|
|
@ -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 Search,DFS)方法进行求解。回溯是深度优先搜索的一种特例,它在一次搜索过程中需要设置一些本次搜索过程的局部状态,并在本次搜索结束之后清除状态。而普通的深度优先搜索并不需要使用这些局部状态,虽然还是有可能设置一些全局状态。
|
||||
|
||||
```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>
|
|
@ -1,74 +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 的绳子。以下为证明过程。
|
||||
|
||||
将绳子拆成 1 和 n-1,则 1(n-1)-n=-1<0,即拆开后的乘积一定更小,所以不能出现长度为 1 的绳子。
|
||||
|
||||
将绳子拆成 2 和 n-2,则 2(n-2)-n = n-4,在 n>=4 时这样拆开能得到的乘积会比不拆更大。
|
||||
|
||||
将绳子拆成 3 和 n-3,则 3(n-3)-n = 2n-9,在 n>=5 时效果更好。
|
||||
|
||||
将绳子拆成 4 和 n-4,因为 4=2\*2,因此效果和拆成 2 一样。
|
||||
|
||||
将绳子拆成 5 和 n-5,因为 5=2+3,而 5<2\*3,所以不能出现 5 的绳子,而是尽可能拆成 2 和 3。
|
||||
|
||||
将绳子拆成 6 和 n-6,因为 6=3+3,而 6<3\*3,所以不能出现 6 的绳子,而是拆成 3 和 3。这里 6 同样可以拆成 6=2+2+2,但是 3(n - 3) - 2(n - 2) = n - 5 >= 0,在 n>=5 的情况下将绳子拆成 3 比拆成 2 效果更好。
|
||||
|
||||
继续拆成更大的绳子可以发现都比拆成 2 和 3 的效果更差,因此我们只考虑将绳子拆成 2 和 3,并且优先拆成 3,当拆到绳子长度 n 等于 4 时,也就是出现 3+1,此时只能拆成 2+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>
|
|
@ -1,34 +0,0 @@
|
|||
# 15. 二进制中 1 的个数
|
||||
|
||||
## 题目链接
|
||||
|
||||
[牛客网](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 的位级表示中最低的那一位 1 设置为 0。不断将 1 设置为 0,直到 n 为 0。时间复杂度:O(M),其中 M 表示 1 的个数。
|
||||
|
||||
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/image-20201105004127554.png" width="500px"> </div><br>
|
||||
|
||||
|
||||
```java
|
||||
public int NumberOf1(int n) {
|
||||
int cnt = 0;
|
||||
while (n != 0) {
|
||||
cnt++;
|
||||
n &= (n - 1);
|
||||
}
|
||||
return cnt;
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<div align="center"><img width="320px" src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/githubio/公众号二维码-2.png"></img></div>
|
|
@ -1,53 +0,0 @@
|
|||
# 16. 数值的整数次方
|
||||
|
||||
## 题目链接
|
||||
|
||||
[牛客网](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 类型的浮点数 x和 int 类型的整数 n,求 x 的 n 次方。
|
||||
|
||||
## 解题思路
|
||||
|
||||
<!-- <div align="center"><img src="https://latex.codecogs.com/gif.latex?x^n=\left\{\begin{array}{rcl}x^{n/2}*x^{n/2}&&{n\%2=0}\\x*(x^{n/2}*x^{n/2})&&{n\%2=1}\end{array}\right." class="mathjax-pic"/></div> <br> -->
|
||||
|
||||
最直观的解法是将 x 重复乘 n 次,x\*x\*x...\*x,那么时间复杂度为 O(N)。因为乘法是可交换的,所以可以将上述操作拆开成两半 (x\*x..\*x)\* (x\*x..\*x),两半的计算是一样的,因此只需要计算一次。而且对于新拆开的计算,又可以继续拆开。这就是分治思想,将原问题的规模拆成多个规模较小的子问题,最后子问题的解合并起来。
|
||||
|
||||
本题中子问题是 x<sup>n/2</sup>,在将子问题合并时将子问题的解乘于自身相乘即可。但如果 n 不为偶数,那么拆成两半还会剩下一个 x,在将子问题合并时还需要需要多乘于一个 x。
|
||||
|
||||
|
||||
|
||||
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/image-20201105012506187.png" width="400px"> </div><br>
|
||||
|
||||
|
||||
因为 (x\*x)<sup>n/2</sup> 可以通过递归求解,并且每次递归 n 都减小一半,因此整个算法的时间复杂度为 O(logN)。
|
||||
|
||||
```java
|
||||
public double Power(double x, int n) {
|
||||
boolean isNegative = false;
|
||||
if (n < 0) {
|
||||
n = -n;
|
||||
isNegative = true;
|
||||
}
|
||||
double res = pow(x, n);
|
||||
return isNegative ? 1 / res : res;
|
||||
}
|
||||
|
||||
private double pow(double x, int n) {
|
||||
if (n == 0) return 1;
|
||||
if (n == 1) return x;
|
||||
double res = pow(x, n / 2);
|
||||
res = res * res;
|
||||
if (n % 2 != 0) res *= x;
|
||||
return res;
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<div align="center"><img width="320px" src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/githubio/公众号二维码-2.png"></img></div>
|
|
@ -1,47 +0,0 @@
|
|||
# 17. 打印从 1 到最大的 n 位数
|
||||
|
||||
## 题目描述
|
||||
|
||||
输入数字 n,按顺序打印出从 1 到最大的 n 位十进制数。比如输入 3,则打印出 1、2、3 一直到最大的 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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -1,69 +0,0 @@
|
|||
# 21. 调整数组顺序使奇数位于偶数前面
|
||||
|
||||
## 题目链接
|
||||
|
||||
[牛客网](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)
|
||||
|
||||
## 题目描述
|
||||
|
||||
需要保证奇数和奇数,偶数和偶数之间的相对位置不变,这和书本不太一样。例如对于 [1,2,3,4,5],调整后得到 [1,3,5,2,4],而不能是 {5,1,3,4,2} 这种相对位置改变的结果。
|
||||
|
||||
<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>
|
|
@ -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>
|
|
@ -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)z。z 为 (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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -1,36 +0,0 @@
|
|||
# 26. 树的子结构
|
||||
|
||||
## 题目链接
|
||||
|
||||
[牛客网](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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -1,51 +0,0 @@
|
|||
# 29. 顺时针打印矩阵
|
||||
|
||||
## 题目链接
|
||||
|
||||
[牛客网](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/image-20201104010349296.png" width="300px"> </div><br>
|
||||
|
||||
|
||||
|
||||
## 解题思路
|
||||
|
||||
一层一层从外到里打印,观察可知每一层打印都有相同的处理步骤,唯一不同的是上下左右的边界不同了。因此使用四个变量 r1, r2, c1, c2 分别存储上下左右边界值,从而定义当前最外层。打印当前最外层的顺序:从左到右打印最上一行->从上到下打印最右一行->从右到左打印最下一行->从下到上打印最左一行。应当注意只有在 r1 != r2 时才打印最下一行,也就是在当前最外层的行数大于 1 时才打印最下一行,这是因为当前最外层只有一行时,继续打印最下一行,会导致重复打印。打印最左一行也要做同样处理。
|
||||
|
||||
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/image-20201104010609223.png" width="500px"> </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>
|
|
@ -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>
|
|
@ -1,45 +0,0 @@
|
|||
# 30. 包含 min 函数的栈
|
||||
|
||||
## 题目链接
|
||||
|
||||
[牛客网](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() 函数的栈,该方法返回当前栈中最小的值。
|
||||
|
||||
## 解题思路
|
||||
|
||||
使用一个额外的 minStack,栈顶元素为当前栈中最小的值。在对栈进行 push 入栈和 pop 出栈操作时,同样需要对 minStack 进行入栈出栈操作,从而使 minStack 栈顶元素一直为当前栈中最小的值。在进行 push 操作时,需要比较入栈元素和当前栈中最小值,将值较小的元素 push 到 minStack 中。
|
||||
|
||||
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/image-20201104013936126.png" width="350px"> </div><br>
|
||||
|
||||
```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>
|
|
@ -1,39 +0,0 @@
|
|||
# 31. 栈的压入、弹出序列
|
||||
|
||||
## 题目链接
|
||||
|
||||
[牛客网](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 就不可能是该压栈序列的弹出序列。
|
||||
|
||||
## 解题思路
|
||||
|
||||
使用一个栈来模拟压入弹出操作。每次入栈一个元素后,都要判断一下栈顶元素是不是当前出栈序列 popSequence 的第一个元素,如果是的话则执行出栈操作并将 popSequence 往后移一位,继续进行判断。
|
||||
|
||||
```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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -1,92 +0,0 @@
|
|||
# 40. 最小的 K 个数
|
||||
|
||||
## 题目链接
|
||||
|
||||
[牛客网](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)
|
||||
|
||||
## 解题思路
|
||||
|
||||
### 大小为 K 的最小堆
|
||||
|
||||
- 复杂度:O(NlogK) + O(K)
|
||||
- 特别适合处理海量数据
|
||||
|
||||
维护一个大小为 K 的最小堆过程如下:使用大顶堆。在添加一个元素之后,如果大顶堆的大小大于 K,那么将大顶堆的堆顶元素去除,也就是将当前堆中值最大的元素去除,从而使得留在堆中的元素都比被去除的元素来得小。
|
||||
|
||||
应该使用大顶堆来维护最小堆,而不能直接创建一个小顶堆并设置一个大小,企图让小顶堆中的元素都是最小元素。
|
||||
|
||||
Java 的 PriorityQueue 实现了堆的能力,PriorityQueue 默认是小顶堆,可以在在初始化时使用 Lambda 表达式 (o1, o2) -> o2 - o1 来实现大顶堆。其它语言也有类似的堆数据结构。
|
||||
|
||||
```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);
|
||||
}
|
||||
```
|
||||
|
||||
### 快速选择
|
||||
|
||||
- 复杂度: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;
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<div align="center"><img width="320px" src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/githubio/公众号二维码-2.png"></img></div>
|
|
@ -1,49 +0,0 @@
|
|||
# 41.1 数据流中的中位数
|
||||
|
||||
## 题目链接
|
||||
|
||||
[牛客网](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>
|
|
@ -1,38 +0,0 @@
|
|||
# 41.2 字符流中第一个不重复的字符
|
||||
|
||||
## 题目描述
|
||||
|
||||
[牛客网](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"。
|
||||
|
||||
## 解题思路
|
||||
|
||||
使用统计数组来统计每个字符出现的次数,本题涉及到的字符为都为 ASCII 码,因此使用一个大小为 128 的整型数组就能完成次数统计任务。
|
||||
|
||||
使用队列来存储到达的字符,并在每次有新的字符从字符流到达时移除队列头部那些出现次数不再是一次的元素。因为队列是先进先出顺序,因此队列头部的元素为第一次只出现一次的字符。
|
||||
|
||||
```java
|
||||
private int[] cnts = new int[128];
|
||||
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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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)
|
||||
|
||||
## 题目描述
|
||||
|
||||
输入一个正整数数组,把数组里所有数字拼接起来排成一个数,打印能拼接出的所有数字中最小的一个。例如输入数组 {3,32,321},则打印出这三个数字能排成的最小数字为 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>
|
|
@ -1,38 +0,0 @@
|
|||
# 46. 把数字翻译成字符串
|
||||
|
||||
[Leetcode](https://leetcode.com/problems/decode-ways/description/)
|
||||
|
||||
## 题目描述
|
||||
|
||||
给定一个数字,按照如下规则翻译成字符串:1 翻译成“a”,2 翻译成“b”... 26 翻译成“z”。一个数字有多种翻译可能,例如 12258 一共有 5 种,分别是 abbeh,lbeh,aveh,abyh,lyh。实现一个函数,用来计算一个数字有多少种不同的翻译方法。
|
||||
|
||||
## 解题思路
|
||||
|
||||
```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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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)
|
||||
|
||||
## 题目描述
|
||||
|
||||
把只包含因子 2、3 和 5 的数称作丑数(Ugly Number)。例如 6、8 都是丑数,但 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>
|
|
@ -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>
|
|
@ -1,60 +0,0 @@
|
|||
# 50. 第一个只出现一次的字符位置
|
||||
|
||||
## 题目链接
|
||||
|
||||
[牛客网](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)
|
||||
|
||||
## 题目描述
|
||||
|
||||
在一个字符串中找到第一个只出现一次的字符,并返回它的位置。字符串只包含 ASCII 码字符。
|
||||
|
||||
```
|
||||
Input: abacc
|
||||
Output: b
|
||||
```
|
||||
|
||||
## 解题思路
|
||||
|
||||
最直观的解法是使用 HashMap 对出现次数进行统计:字符做为 key,出现次数作为 value,遍历字符串每次都将 key 对应的 value 加 1。最后再遍历这个 HashMap 就可以找出出现次数为 1 的字符。
|
||||
|
||||
考虑到要统计的字符范围有限,也可以使用整型数组代替 HashMap。ASCII 码只有 128 个字符,因此可以使用长度为 128 的整型数组来存储每个字符出现的次数。
|
||||
|
||||
```java
|
||||
public int FirstNotRepeatingChar(String str) {
|
||||
int[] cnts = new int[128];
|
||||
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(128);
|
||||
BitSet bs2 = new BitSet(128);
|
||||
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>
|
|
@ -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>
|
|
@ -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 + c,B 的长度为 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>
|
|
@ -1,80 +0,0 @@
|
|||
# 53. 数字在排序数组中出现的次数
|
||||
|
||||
## 题目链接
|
||||
|
||||
[牛客网](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
|
||||
```
|
||||
|
||||
## 解题思路
|
||||
|
||||
只要能找出给定的数字 k 在有序数组第一个位置和最后一个位置,就能知道该数字出现的次数。
|
||||
|
||||
先考虑如何实现寻找数字在有序数组的第一个位置。正常的二分查找如下,在查找到给定元素 k 之后,立即返回当前索引下标。
|
||||
|
||||
```java
|
||||
public int binarySearch(int[] nums, int K) {
|
||||
int l = 0, h = nums.length - 1;
|
||||
while (l <= h) {
|
||||
int m = l + (h - l) / 2;
|
||||
if (nums[m] == K) {
|
||||
return m;
|
||||
} else if (nums[m] > K) {
|
||||
h = m - 1;
|
||||
} else {
|
||||
l = m + 1;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
```
|
||||
|
||||
但是在查找第一个位置时,找到元素之后应该继续往前找。也就是当 nums[m]>=k 时,在左区间继续查找,左区间应该包含 m 位置。
|
||||
|
||||
```java
|
||||
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;
|
||||
}
|
||||
```
|
||||
|
||||
查找最后一个位置可以转换成寻找 k+1 的第一个位置,并再往前移动一个位置。
|
||||
|
||||
```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;
|
||||
}
|
||||
```
|
||||
|
||||
需要注意以上实现的查找第一个位置的 binarySearch 方法,h 的初始值为 nums.length,而不是 nums.length - 1。先看以下示例:
|
||||
|
||||
```
|
||||
nums = [2,2], k = 2
|
||||
```
|
||||
|
||||
如果 h 的取值为 nums.length - 1,那么在查找最后一个位置时,binarySearch(nums, k + 1) - 1 = 1 - 1 = 0。这是因为 binarySearch 只会返回 [0, nums.length - 1] 范围的值,对于 binarySearch([2,2], 3) ,我们希望返回 3 插入 nums 中的位置,也就是数组最后一个位置再往后一个位置,即 nums.length。所以我们需要将 h 取值为 nums.length,从而使得 binarySearch 返回的区间更大,能够覆盖 k 大于 nums 最后一个元素的情况。
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<div align="center"><img width="320px" src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/githubio/公众号二维码-2.png"></img></div>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -1,41 +0,0 @@
|
|||
# 56. 数组中只出现一次的数字
|
||||
|
||||
## 题目链接
|
||||
|
||||
[牛客网](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)
|
||||
|
||||
## 题目描述
|
||||
|
||||
一个整型数组里除了两个数字之外,其他的数字都出现了两次,找出这两个数。
|
||||
|
||||
## 解题思路
|
||||
|
||||
两个相等的元素异或的结果为 0,而 0 与任意数 x 异或的结果都为 x。
|
||||
|
||||
对本题给的数组的所有元素执行异或操作,得到的是两个不存在重复的元素异或的结果。例如对于数组 [x,x,y,y,z,k],x^x^y^y^z^k = 0^y^y^z^k = y^y^z^k = 0^z^k = z^k。
|
||||
|
||||
两个不相等的元素在位级表示上一定会有所不同,因此这两个元素异或得到的结果 diff 一定不为 0。位运算 diff & -diff 能得到 diff 位级表示中最右侧为 1 的位,令 diff = diff & -diff。将 diff 作为区分两个元素的依据,一定有一个元素对 diff 进行异或的结果为 0,另一个结果非 0。设不相等的两个元素分别为 z 和 k,遍历数组所有元素,判断元素与 diff 的异或结果是否为 0,如果是的话将元素与 z 进行异或并赋值给 z,否则与 k 进行异或并赋值给 k。数组中相等的元素一定会同时与 z 或者与 k 进行异或操作,而不是一个与 z 进行异或,一个与 k 进行异或。而且这些相等的元素异或的结果为 0,因此最后 z 和 k 只是不相等的两个元素与 0 异或的结果,也就是不相等两个元素本身。
|
||||
|
||||
下面的解法中,num1 和 num2 数组的第一个元素是用来保持返回值的... 实际开发中不推荐这种返回值的方式。
|
||||
|
||||
```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>
|
|
@ -1,40 +0,0 @@
|
|||
# 57.1 和为 S 的两个数字
|
||||
|
||||
## 题目链接
|
||||
|
||||
[牛客网](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,输出两个数的乘积最小的。
|
||||
|
||||
## 解题思路
|
||||
|
||||
使用双指针,一个指针指向元素较小的值,一个指针指向元素较大的值。指向较小元素的指针从头向尾遍历,指向较大元素的指针从尾向头遍历。
|
||||
|
||||
- 如果两个指针指向元素的和 sum == target,那么这两个元素即为所求。
|
||||
- 如果 sum > target,移动较大的元素,使 sum 变小一些;
|
||||
- 如果 sum < target,移动较小的元素,使 sum 变大一些。
|
||||
|
||||
```java
|
||||
public ArrayList<Integer> FindNumbersWithSum(int[] nums, int target) {
|
||||
int i = 0, j = nums.length - 1;
|
||||
while (i < j) {
|
||||
int cur = nums[i] + array[j];
|
||||
if (cur == target)
|
||||
return new ArrayList<>(Arrays.asList(nums[i], nums[j]));
|
||||
if (cur < target)
|
||||
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>
|
|
@ -1,50 +0,0 @@
|
|||
# 57.2 和为 S 的连续正数序列
|
||||
|
||||
## 题目描述
|
||||
|
||||
[牛客网](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>
|
|
@ -1,56 +0,0 @@
|
|||
# 58.1 翻转单词顺序列
|
||||
|
||||
## 题目描述
|
||||
|
||||
[牛客网](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>
|
|
@ -1,50 +0,0 @@
|
|||
# 58.2 左旋转字符串
|
||||
|
||||
## 题目链接
|
||||
|
||||
[牛客网](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>
|
|
@ -1,44 +0,0 @@
|
|||
# 59. 滑动窗口的最大值
|
||||
|
||||
## 题目链接
|
||||
|
||||
[牛客网](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}。
|
||||
|
||||
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/image-20201104020702453.png" width="500px"> </div><br>
|
||||
|
||||
## 解题思路
|
||||
|
||||
维护一个大小为窗口大小的大顶堆,顶堆元素则为当前窗口的最大值。
|
||||
|
||||
假设窗口的大小为 M,数组的长度为 N。在窗口向右移动时,需要先在堆中删除离开窗口的元素,并将新到达的元素添加到堆中,这两个操作的时间复杂度都为 log<sub>2</sub>M,因此算法的时间复杂度为 O(Nlog<sub>2</sub>M),空间复杂度为 O(M)。
|
||||
|
||||
```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>
|
|
@ -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->3(3,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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -1,36 +0,0 @@
|
|||
# 63. 股票的最大利润
|
||||
|
||||
## 题目链接
|
||||
|
||||
[Leetcode:121. 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>
|
|
@ -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)
|
||||
|
||||
## 题目描述
|
||||
|
||||
要求不能使用乘除法、for、while、if、else、switch、case 等关键字及条件判断语句 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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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/)
|
||||
- [理解 Docker(2):Docker 镜像](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>
|
|
@ -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>
|
|
@ -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¶m2=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 之外,还包含 URN(Uniform 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)
|
||||
- [rfc2616:3.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
|
||||
|
||||
> 要求在与代理服务器通信时建立隧道
|
||||
|
||||
使用 SSL(Secure Sockets Layer,安全套接层)和 TLS(Transport 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,跨站追踪)。
|
||||
|
||||
- [rfc2616:9 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 协议规定 301、302 状态下重定向时不允许把 POST 方法改成 GET 方法,但是大多数浏览器都会在 301、302 和 303 状态下的重定向把 POST 方法改成 GET 方法。
|
||||
|
||||
- **304 Not Modified** :如果请求报文首部包含一些条件,例如:If-Match,If-Modified-Since,If-None-Match,If-Range,If-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 首部字段,例如 Accept、Accept-Charset、Accept-Encoding、Accept-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 与缓存中的对应的值相同时才会返回该缓存。
|
||||
|
||||
## 内容编码
|
||||
|
||||
内容编码将实体主体进行压缩,从而减少传输的数据量。
|
||||
|
||||
常用的内容编码有:gzip、compress、deflate、identity。
|
||||
|
||||
浏览器发送 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 先和 SSL(Secure Sockets Layer)通信,再由 SSL 和 TCP 通信,也就是说 HTTPS 使用了隧道进行通信。
|
||||
|
||||
通过使用 SSL,HTTPS 具有了加密(防窃听)、认证(防伪装)和完整性保护(防篡改)。
|
||||
|
||||
<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>
|
||||
|
||||
## 认证
|
||||
|
||||
通过使用 **证书** 来对通信方进行认证。
|
||||
|
||||
数字证书认证机构(CA,Certificate 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 之外还有:HEAD、OPTIONS。
|
||||
|
||||
不安全的方法除了 POST 之外还有 PUT、DELETE。
|
||||
|
||||
## 幂等性
|
||||
|
||||
幂等的 HTTP 方法,同样的请求被执行一次与连续执行多次的效果是一样的,服务器的状态也是一样的。换句话说就是,幂等方法不应该具有副作用(统计用途除外)。
|
||||
|
||||
所有的安全方法也都是幂等的。
|
||||
|
||||
在正确实现的条件下,GET,HEAD,PUT 和 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>
|
|
@ -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 装饰者提供了对更多数据类型进行输入的操作,比如 int、double 等基本类型。
|
||||
|
||||
# 四、字符操作
|
||||
|
||||
## 编码与解码
|
||||
|
||||
编码就是把字符转换为字节,而解码是把字节重新组合成字符。
|
||||
|
||||
如果编码和解码过程使用不同的编码方式那么就出现了乱码。
|
||||
|
||||
- GBK 编码中,中文字符占 2 个字节,英文字符占 1 个字节;
|
||||
- UTF-8 编码中,中文字符占 3 个字节,英文字符占 1 个字节;
|
||||
- UTF-16be 编码中,中文字符和英文字符都占 2 个字节。
|
||||
|
||||
UTF-16be 中的 be 指的是 Big Endian,也就是大端。相应地也有 UTF-16le,le 指的是 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 = 8。capacity 变量不会改变,下面的讨论会忽略它。
|
||||
|
||||
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/1bea398f-17a7-4f67-a90b-9e2d243eaa9a.png"/> </div><br>
|
||||
|
||||
② 从输入通道中读取 5 个字节数据写入缓冲区中,此时 position 为 5,limit 保持不变。
|
||||
|
||||
<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
|
@ -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 虚拟机栈类似,它们之间的区别只不过是本地方法栈为本地方法服务。
|
||||
|
||||
本地方法一般是用其它语言(C、C++ 或汇编语言等)编写的,并且被编译为基于本机硬件和操作系统的程序,对待这些方法需要特别处理。
|
||||
|
||||
<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),就不需要手工指定新生代的大小(-Xmn)、Eden 和 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>
|
||||
|
||||
CMS(Concurrent Mark Sweep),Mark Sweep 指的是标记 - 清除算法。
|
||||
|
||||
分为以下四个流程:
|
||||
|
||||
- 初始标记:仅仅只是标记一下 GC Roots 能直接关联到的对象,速度很快,需要停顿。
|
||||
- 并发标记:进行 GC Roots Tracing 的过程,它在整个回收过程中耗时最长,不需要停顿。
|
||||
- 重新标记:为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,需要停顿。
|
||||
- 并发清除:不需要停顿。
|
||||
|
||||
在整个过程中耗时最长的并发标记和并发清除过程中,收集器线程都可以与用户线程一起工作,不需要进行停顿。
|
||||
|
||||
具有以下缺点:
|
||||
|
||||
- 吞吐量低:低停顿时间是以牺牲吞吐量为代价的,导致 CPU 利用率不够高。
|
||||
- 无法处理浮动垃圾,可能出现 Concurrent Mode Failure。浮动垃圾是指并发清除阶段由于用户线程继续运行而产生的垃圾,这部分垃圾只能到下一次 GC 时才能进行回收。由于浮动垃圾的存在,因此需要预留出一部分内存,意味着 CMS 收集不能像其它收集器那样等待老年代快满的时候再回收。如果预留的内存不够存放浮动垃圾,就会出现 Concurrent Mode Failure,这时虚拟机将临时启用 Serial Old 来替代 CMS。
|
||||
- 标记 - 清除算法导致的空间碎片,往往出现老年代空间剩余,但无法找到足够大连续空间来分配当前对象,不得不提前触发一次 Full GC。
|
||||
|
||||
### 7. G1 收集器
|
||||
|
||||
G1(Garbage-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 包读取,成为 JAR、EAR、WAR 格式的基础。
|
||||
- 从网络中获取,最典型的应用是 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 程序代码。初始化阶段是虚拟机执行类构造器 <clinit>() 方法的过程。在准备阶段,类变量已经赋过一次系统要求的初始值,而在初始化阶段,根据程序员通过程序制定的主观计划去初始化类变量和其它资源。
|
||||
|
||||
<clinit>() 是由编译器自动收集类中所有类变量的赋值动作和静态语句块中的语句合并产生的,编译器收集的顺序由语句在源文件中出现的顺序决定。特别注意的是,静态语句块只能访问到定义在它之前的类变量,定义在它之后的类变量只能赋值,不能访问。例如以下代码:
|
||||
|
||||
```java
|
||||
public class Test {
|
||||
static {
|
||||
i = 0; // 给变量赋值可以正常编译通过
|
||||
System.out.print(i); // 这句编译器会提示“非法向前引用”
|
||||
}
|
||||
static int i = 1;
|
||||
}
|
||||
```
|
||||
|
||||
由于父类的 <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
|
||||
}
|
||||
```
|
||||
|
||||
接口中不可以使用静态语句块,但仍然有类变量初始化的赋值操作,因此接口与类一样都会生成 <clinit>() 方法。但接口与类不同的是,执行接口的 <clinit>() 方法不需要先执行父接口的 <clinit>() 方法。只有当父接口中定义的变量使用时,父接口才会初始化。另外,接口的实现类在初始化时也一样不会执行接口的 <clinit>() 方法。
|
||||
|
||||
虚拟机会保证一个类的 <clinit>() 方法在多线程环境下被正确的加锁和同步,如果多个线程同时初始化一个类,只会有一个线程执行这个类的 <clinit>() 方法,其它线程都会阻塞等待,直到活动线程执行 <clinit>() 方法完毕。如果在一个类的 <clinit>() 方法中有耗时的操作,就可能造成多个线程阻塞,在实际过程中此种阻塞很隐蔽。
|
||||
|
||||
## 类初始化时机
|
||||
|
||||
### 1. 主动引用
|
||||
|
||||
虚拟机规范中并没有强制约束何时进行加载,但是规范严格规定了有且只有下列五种情况必须对类进行初始化(加载、验证、准备都会随之发生):
|
||||
|
||||
- 遇到 new、getstatic、putstatic、invokestatic 这四条字节码指令时,如果类没有进行过初始化,则必须先触发其初始化。最常见的生成这 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)此类加载器负责将存放在 <JRE_HOME>\lib 目录中的,或者被 -Xbootclasspath 参数所指定的路径中的,并且是虚拟机识别的(仅按照文件名识别,如 rt.jar,名字不符合的类库即使放在 lib 目录中也不会被加载)类库加载到虚拟机内存中。启动类加载器无法被 Java 程序直接引用,用户在编写自定义类加载器时,如果需要把加载请求委派给启动类加载器,直接使用 null 代替即可。
|
||||
|
||||
- 扩展类加载器(Extension ClassLoader)这个类加载器是由 ExtClassLoader(sun.misc.Launcher$ExtClassLoader)实现的。它负责将 <JAVA_HOME>/lib/ext 或者被 java.ext.dir 系统变量所指定路径中的所有类库加载到内存中,开发者可以直接使用扩展类加载器。
|
||||
|
||||
- 应用程序类加载器(Application ClassLoader)这个类加载器是由 AppClassLoader(sun.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>
|
|
@ -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 = 2,l = 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>
|
|
@ -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 表示一串 0,1s 表示一串 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 与 mask:00111100 进行位与操作,只保留 num 中与 mask 的 1 部分相对应的位。
|
||||
|
||||
```
|
||||
01011011 &
|
||||
00111100
|
||||
--------
|
||||
00011000
|
||||
```
|
||||
|
||||
利用 x | 0s = x 和 x | 1s = 1s 的特点,可以实现设值操作。一个数 num 与 mask:00111100 进行位或操作,将 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,例如 16(10000)。
|
||||
|
||||
```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>
|
|
@ -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
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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
|
@ -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 整除 y(y mod x == 0),则对于所有 i,mi <= 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 的倍数贡献一个 5,N/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>
|
|
@ -1,459 +0,0 @@
|
|||
<!-- GFM-TOC -->
|
||||
* [1. 把数组中的 0 移到末尾](#1-把数组中的-0-移到末尾)
|
||||
* [2. 改变矩阵维度](#2-改变矩阵维度)
|
||||
* [3. 找出数组中最长的连续 1](#3-找出数组中最长的连续-1)
|
||||
* [4. 有序矩阵查找](#4-有序矩阵查找)
|
||||
* [5. 有序矩阵的 Kth Element](#5-有序矩阵的-kth-element)
|
||||
* [6. 一个数组元素在 [1, n] 之间,其中一个数被替换为另一个数,找出重复的数和丢失的数](#6-一个数组元素在-[1-n]-之间,其中一个数被替换为另一个数,找出重复的数和丢失的数)
|
||||
* [7. 找出数组中重复的数,数组值在 [1, n] 之间](#7-找出数组中重复的数,数组值在-[1-n]-之间)
|
||||
* [8. 数组相邻差值的个数](#8-数组相邻差值的个数)
|
||||
* [9. 数组的度](#9-数组的度)
|
||||
* [10. 对角元素相等的矩阵](#10-对角元素相等的矩阵)
|
||||
* [11. 嵌套数组](#11-嵌套数组)
|
||||
* [12. 分隔数组](#12-分隔数组)
|
||||
<!-- GFM-TOC -->
|
||||
|
||||
|
||||
# 1. 把数组中的 0 移到末尾
|
||||
|
||||
283\. Move Zeroes (Easy)
|
||||
|
||||
[Leetcode](https://leetcode.com/problems/move-zeroes/description/) / [力扣](https://leetcode-cn.com/problems/move-zeroes/description/)
|
||||
|
||||
```html
|
||||
For example, given nums = [0, 1, 0, 3, 12], after calling your function, nums should be [1, 3, 12, 0, 0].
|
||||
```
|
||||
|
||||
```java
|
||||
public void moveZeroes(int[] nums) {
|
||||
int idx = 0;
|
||||
for (int num : nums) {
|
||||
if (num != 0) {
|
||||
nums[idx++] = num;
|
||||
}
|
||||
}
|
||||
while (idx < nums.length) {
|
||||
nums[idx++] = 0;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
# 2. 改变矩阵维度
|
||||
|
||||
566\. Reshape the Matrix (Easy)
|
||||
|
||||
[Leetcode](https://leetcode.com/problems/reshape-the-matrix/description/) / [力扣](https://leetcode-cn.com/problems/reshape-the-matrix/description/)
|
||||
|
||||
```html
|
||||
Input:
|
||||
nums =
|
||||
[[1,2],
|
||||
[3,4]]
|
||||
r = 1, c = 4
|
||||
|
||||
Output:
|
||||
[[1,2,3,4]]
|
||||
|
||||
Explanation:
|
||||
The row-traversing of nums is [1,2,3,4]. The new reshaped matrix is a 1 * 4 matrix, fill it row by row by using the previous list.
|
||||
```
|
||||
|
||||
```java
|
||||
public int[][] matrixReshape(int[][] nums, int r, int c) {
|
||||
int m = nums.length, n = nums[0].length;
|
||||
if (m * n != r * c) {
|
||||
return nums;
|
||||
}
|
||||
int[][] reshapedNums = new int[r][c];
|
||||
int index = 0;
|
||||
for (int i = 0; i < r; i++) {
|
||||
for (int j = 0; j < c; j++) {
|
||||
reshapedNums[i][j] = nums[index / n][index % n];
|
||||
index++;
|
||||
}
|
||||
}
|
||||
return reshapedNums;
|
||||
}
|
||||
```
|
||||
|
||||
# 3. 找出数组中最长的连续 1
|
||||
|
||||
485\. Max Consecutive Ones (Easy)
|
||||
|
||||
[Leetcode](https://leetcode.com/problems/max-consecutive-ones/description/) / [力扣](https://leetcode-cn.com/problems/max-consecutive-ones/description/)
|
||||
|
||||
```java
|
||||
public int findMaxConsecutiveOnes(int[] nums) {
|
||||
int max = 0, cur = 0;
|
||||
for (int x : nums) {
|
||||
cur = x == 0 ? 0 : cur + 1;
|
||||
max = Math.max(max, cur);
|
||||
}
|
||||
return max;
|
||||
}
|
||||
```
|
||||
|
||||
# 4. 有序矩阵查找
|
||||
|
||||
240\. Search a 2D Matrix II (Medium)
|
||||
|
||||
[Leetcode](https://leetcode.com/problems/search-a-2d-matrix-ii/description/) / [力扣](https://leetcode-cn.com/problems/search-a-2d-matrix-ii/description/)
|
||||
|
||||
```html
|
||||
[
|
||||
[ 1, 5, 9],
|
||||
[10, 11, 13],
|
||||
[12, 13, 15]
|
||||
]
|
||||
```
|
||||
|
||||
```java
|
||||
public boolean searchMatrix(int[][] matrix, int target) {
|
||||
if (matrix == null || matrix.length == 0 || matrix[0].length == 0) return false;
|
||||
int m = matrix.length, n = matrix[0].length;
|
||||
int row = 0, col = n - 1;
|
||||
while (row < m && col >= 0) {
|
||||
if (target == matrix[row][col]) return true;
|
||||
else if (target < matrix[row][col]) col--;
|
||||
else row++;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
```
|
||||
|
||||
# 5. 有序矩阵的 Kth Element
|
||||
|
||||
378\. Kth Smallest Element in a Sorted Matrix ((Medium))
|
||||
|
||||
[Leetcode](https://leetcode.com/problems/kth-smallest-element-in-a-sorted-matrix/description/) / [力扣](https://leetcode-cn.com/problems/kth-smallest-element-in-a-sorted-matrix/description/)
|
||||
|
||||
```html
|
||||
matrix = [
|
||||
[ 1, 5, 9],
|
||||
[10, 11, 13],
|
||||
[12, 13, 15]
|
||||
],
|
||||
k = 8,
|
||||
|
||||
return 13.
|
||||
```
|
||||
|
||||
解题参考:[Share my thoughts and Clean Java Code](https://leetcode-cn.com/problems/kth-smallest-element-in-a-sorted-matrix/discuss/85173)
|
||||
|
||||
二分查找解法:
|
||||
|
||||
```java
|
||||
public int kthSmallest(int[][] matrix, int k) {
|
||||
int m = matrix.length, n = matrix[0].length;
|
||||
int lo = matrix[0][0], hi = matrix[m - 1][n - 1];
|
||||
while (lo <= hi) {
|
||||
int mid = lo + (hi - lo) / 2;
|
||||
int cnt = 0;
|
||||
for (int i = 0; i < m; i++) {
|
||||
for (int j = 0; j < n && matrix[i][j] <= mid; j++) {
|
||||
cnt++;
|
||||
}
|
||||
}
|
||||
if (cnt < k) lo = mid + 1;
|
||||
else hi = mid - 1;
|
||||
}
|
||||
return lo;
|
||||
}
|
||||
```
|
||||
|
||||
堆解法:
|
||||
|
||||
```java
|
||||
public int kthSmallest(int[][] matrix, int k) {
|
||||
int m = matrix.length, n = matrix[0].length;
|
||||
PriorityQueue<Tuple> pq = new PriorityQueue<Tuple>();
|
||||
for(int j = 0; j < n; j++) pq.offer(new Tuple(0, j, matrix[0][j]));
|
||||
for(int i = 0; i < k - 1; i++) { // 小根堆,去掉 k - 1 个堆顶元素,此时堆顶元素就是第 k 的数
|
||||
Tuple t = pq.poll();
|
||||
if(t.x == m - 1) continue;
|
||||
pq.offer(new Tuple(t.x + 1, t.y, matrix[t.x + 1][t.y]));
|
||||
}
|
||||
return pq.poll().val;
|
||||
}
|
||||
|
||||
class Tuple implements Comparable<Tuple> {
|
||||
int x, y, val;
|
||||
public Tuple(int x, int y, int val) {
|
||||
this.x = x; this.y = y; this.val = val;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(Tuple that) {
|
||||
return this.val - that.val;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
# 6. 一个数组元素在 [1, n] 之间,其中一个数被替换为另一个数,找出重复的数和丢失的数
|
||||
|
||||
645\. Set Mismatch (Easy)
|
||||
|
||||
[Leetcode](https://leetcode.com/problems/set-mismatch/description/) / [力扣](https://leetcode-cn.com/problems/set-mismatch/description/)
|
||||
|
||||
```html
|
||||
Input: nums = [1,2,2,4]
|
||||
Output: [2,3]
|
||||
```
|
||||
|
||||
```html
|
||||
Input: nums = [1,2,2,4]
|
||||
Output: [2,3]
|
||||
```
|
||||
|
||||
最直接的方法是先对数组进行排序,这种方法时间复杂度为 O(NlogN)。本题可以以 O(N) 的时间复杂度、O(1) 空间复杂度来求解。
|
||||
|
||||
主要思想是通过交换数组元素,使得数组上的元素在正确的位置上。
|
||||
|
||||
```java
|
||||
public int[] findErrorNums(int[] nums) {
|
||||
for (int i = 0; i < nums.length; i++) {
|
||||
while (nums[i] != i + 1 && nums[nums[i] - 1] != nums[i]) {
|
||||
swap(nums, i, nums[i] - 1);
|
||||
}
|
||||
}
|
||||
for (int i = 0; i < nums.length; i++) {
|
||||
if (nums[i] != i + 1) {
|
||||
return new int[]{nums[i], i + 1};
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private void swap(int[] nums, int i, int j) {
|
||||
int tmp = nums[i];
|
||||
nums[i] = nums[j];
|
||||
nums[j] = tmp;
|
||||
}
|
||||
```
|
||||
|
||||
# 7. 找出数组中重复的数,数组值在 [1, n] 之间
|
||||
|
||||
287\. Find the Duplicate Number (Medium)
|
||||
|
||||
[Leetcode](https://leetcode.com/problems/find-the-duplicate-number/description/) / [力扣](https://leetcode-cn.com/problems/find-the-duplicate-number/description/)
|
||||
|
||||
要求不能修改数组,也不能使用额外的空间。
|
||||
|
||||
二分查找解法:
|
||||
|
||||
```java
|
||||
public int findDuplicate(int[] nums) {
|
||||
int l = 1, h = nums.length - 1;
|
||||
while (l <= h) {
|
||||
int mid = l + (h - l) / 2;
|
||||
int cnt = 0;
|
||||
for (int i = 0; i < nums.length; i++) {
|
||||
if (nums[i] <= mid) cnt++;
|
||||
}
|
||||
if (cnt > mid) h = mid - 1;
|
||||
else l = mid + 1;
|
||||
}
|
||||
return l;
|
||||
}
|
||||
```
|
||||
|
||||
双指针解法,类似于有环链表中找出环的入口:
|
||||
|
||||
```java
|
||||
public int findDuplicate(int[] nums) {
|
||||
int slow = nums[0], fast = nums[nums[0]];
|
||||
while (slow != fast) {
|
||||
slow = nums[slow];
|
||||
fast = nums[nums[fast]];
|
||||
}
|
||||
fast = 0;
|
||||
while (slow != fast) {
|
||||
slow = nums[slow];
|
||||
fast = nums[fast];
|
||||
}
|
||||
return slow;
|
||||
}
|
||||
```
|
||||
|
||||
# 8. 数组相邻差值的个数
|
||||
|
||||
667\. Beautiful Arrangement II (Medium)
|
||||
|
||||
[Leetcode](https://leetcode.com/problems/beautiful-arrangement-ii/description/) / [力扣](https://leetcode-cn.com/problems/beautiful-arrangement-ii/description/)
|
||||
|
||||
```html
|
||||
Input: n = 3, k = 2
|
||||
Output: [1, 3, 2]
|
||||
Explanation: The [1, 3, 2] has three different positive integers ranging from 1 to 3, and the [2, 1] has exactly 2 distinct integers: 1 and 2.
|
||||
```
|
||||
|
||||
题目描述:数组元素为 1\~n 的整数,要求构建数组,使得相邻元素的差值不相同的个数为 k。
|
||||
|
||||
让前 k+1 个元素构建出 k 个不相同的差值,序列为:1 k+1 2 k 3 k-1 ... k/2 k/2+1.
|
||||
|
||||
```java
|
||||
public int[] constructArray(int n, int k) {
|
||||
int[] ret = new int[n];
|
||||
ret[0] = 1;
|
||||
for (int i = 1, interval = k; i <= k; i++, interval--) {
|
||||
ret[i] = i % 2 == 1 ? ret[i - 1] + interval : ret[i - 1] - interval;
|
||||
}
|
||||
for (int i = k + 1; i < n; i++) {
|
||||
ret[i] = i + 1;
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
```
|
||||
|
||||
# 9. 数组的度
|
||||
|
||||
697\. Degree of an Array (Easy)
|
||||
|
||||
[Leetcode](https://leetcode.com/problems/degree-of-an-array/description/) / [力扣](https://leetcode-cn.com/problems/degree-of-an-array/description/)
|
||||
|
||||
```html
|
||||
Input: [1,2,2,3,1,4,2]
|
||||
Output: 6
|
||||
```
|
||||
|
||||
题目描述:数组的度定义为元素出现的最高频率,例如上面的数组度为 3。要求找到一个最小的子数组,这个子数组的度和原数组一样。
|
||||
|
||||
```java
|
||||
public int findShortestSubArray(int[] nums) {
|
||||
Map<Integer, Integer> numsCnt = new HashMap<>();
|
||||
Map<Integer, Integer> numsLastIndex = new HashMap<>();
|
||||
Map<Integer, Integer> numsFirstIndex = new HashMap<>();
|
||||
for (int i = 0; i < nums.length; i++) {
|
||||
int num = nums[i];
|
||||
numsCnt.put(num, numsCnt.getOrDefault(num, 0) + 1);
|
||||
numsLastIndex.put(num, i);
|
||||
if (!numsFirstIndex.containsKey(num)) {
|
||||
numsFirstIndex.put(num, i);
|
||||
}
|
||||
}
|
||||
int maxCnt = 0;
|
||||
for (int num : nums) {
|
||||
maxCnt = Math.max(maxCnt, numsCnt.get(num));
|
||||
}
|
||||
int ret = nums.length;
|
||||
for (int i = 0; i < nums.length; i++) {
|
||||
int num = nums[i];
|
||||
int cnt = numsCnt.get(num);
|
||||
if (cnt != maxCnt) continue;
|
||||
ret = Math.min(ret, numsLastIndex.get(num) - numsFirstIndex.get(num) + 1);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
```
|
||||
|
||||
# 10. 对角元素相等的矩阵
|
||||
|
||||
766\. Toeplitz Matrix (Easy)
|
||||
|
||||
[Leetcode](https://leetcode.com/problems/toeplitz-matrix/description/) / [力扣](https://leetcode-cn.com/problems/toeplitz-matrix/description/)
|
||||
|
||||
```html
|
||||
1234
|
||||
5123
|
||||
9512
|
||||
|
||||
In the above grid, the diagonals are "[9]", "[5, 5]", "[1, 1, 1]", "[2, 2, 2]", "[3, 3]", "[4]", and in each diagonal all elements are the same, so the answer is True.
|
||||
```
|
||||
|
||||
```java
|
||||
public boolean isToeplitzMatrix(int[][] matrix) {
|
||||
for (int i = 0; i < matrix[0].length; i++) {
|
||||
if (!check(matrix, matrix[0][i], 0, i)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
for (int i = 0; i < matrix.length; i++) {
|
||||
if (!check(matrix, matrix[i][0], i, 0)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean check(int[][] matrix, int expectValue, int row, int col) {
|
||||
if (row >= matrix.length || col >= matrix[0].length) {
|
||||
return true;
|
||||
}
|
||||
if (matrix[row][col] != expectValue) {
|
||||
return false;
|
||||
}
|
||||
return check(matrix, expectValue, row + 1, col + 1);
|
||||
}
|
||||
```
|
||||
|
||||
# 11. 嵌套数组
|
||||
|
||||
565\. Array Nesting (Medium)
|
||||
|
||||
[Leetcode](https://leetcode.com/problems/array-nesting/description/) / [力扣](https://leetcode-cn.com/problems/array-nesting/description/)
|
||||
|
||||
```html
|
||||
Input: A = [5,4,0,3,1,6,2]
|
||||
Output: 4
|
||||
Explanation:
|
||||
A[0] = 5, A[1] = 4, A[2] = 0, A[3] = 3, A[4] = 1, A[5] = 6, A[6] = 2.
|
||||
|
||||
One of the longest S[K]:
|
||||
S[0] = {A[0], A[5], A[6], A[2]} = {5, 6, 2, 0}
|
||||
```
|
||||
|
||||
题目描述:S[i] 表示一个集合,集合的第一个元素是 A[i],第二个元素是 A[A[i]],如此嵌套下去。求最大的 S[i]。
|
||||
|
||||
```java
|
||||
public int arrayNesting(int[] nums) {
|
||||
int max = 0;
|
||||
for (int i = 0; i < nums.length; i++) {
|
||||
int cnt = 0;
|
||||
for (int j = i; nums[j] != -1; ) {
|
||||
cnt++;
|
||||
int t = nums[j];
|
||||
nums[j] = -1; // 标记该位置已经被访问
|
||||
j = t;
|
||||
|
||||
}
|
||||
max = Math.max(max, cnt);
|
||||
}
|
||||
return max;
|
||||
}
|
||||
```
|
||||
|
||||
# 12. 分隔数组
|
||||
|
||||
769\. Max Chunks To Make Sorted (Medium)
|
||||
|
||||
[Leetcode](https://leetcode.com/problems/max-chunks-to-make-sorted/description/) / [力扣](https://leetcode-cn.com/problems/max-chunks-to-make-sorted/description/)
|
||||
|
||||
```html
|
||||
Input: arr = [1,0,2,3,4]
|
||||
Output: 4
|
||||
Explanation:
|
||||
We can split into two chunks, such as [1, 0], [2, 3, 4].
|
||||
However, splitting into [1, 0], [2], [3], [4] is the highest number of chunks possible.
|
||||
```
|
||||
|
||||
题目描述:分隔数组,使得对每部分排序后数组就为有序。
|
||||
|
||||
```java
|
||||
public int maxChunksToSorted(int[] arr) {
|
||||
if (arr == null) return 0;
|
||||
int ret = 0;
|
||||
int right = arr[0];
|
||||
for (int i = 0; i < arr.length; i++) {
|
||||
right = Math.max(right, arr[i]);
|
||||
if (right == i) ret++;
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<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
Loading…
Reference in New Issue
Block a user