From 316436952b917537b79ae1e69a5b7eb9277e8ab0 Mon Sep 17 00:00:00 2001 From: CyC2018 <1029579233@qq.com> Date: Fri, 8 Mar 2019 20:04:59 +0800 Subject: [PATCH 01/56] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e0ebf6dc..688be90d 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ ## :pencil2: 算法 - [剑指 Offer 题解](https://github.com/CyC2018/CS-Notes/blob/master/docs/notes/剑指%20offer%20题解.md) -- [Leetcode 题解](https://github.com/CyC2018/CS-Notes/blob/master/docs/notes/Leetcode%20题解.md) +- [Leetcode 题解](https://github.com/CyC2018/CS-Notes/blob/master/docs/notes/Leetcode%20题解%20-%20目录.md) - [算法](https://github.com/CyC2018/CS-Notes/blob/master/docs/notes/算法.md) ## :computer: 操作系统 From 1ce0d0ec6a260e7d704e21c87773c99a1486dfaa Mon Sep 17 00:00:00 2001 From: CyC2018 <1029579233@qq.com> Date: Fri, 8 Mar 2019 20:31:07 +0800 Subject: [PATCH 02/56] auto commit --- docs/notes/Leetcode 题解 - 二分查找.md | 292 + docs/notes/Leetcode 题解 - 位运算.md | 428 ++ docs/notes/Leetcode 题解 - 分治.md | 50 + docs/notes/Leetcode 题解 - 动态规划.md | 1222 ++++ docs/notes/Leetcode 题解 - 双指针.md | 239 + docs/notes/Leetcode 题解 - 哈希表.md | 124 + docs/notes/Leetcode 题解 - 图.md | 258 + docs/notes/Leetcode 题解 - 字符串.md | 226 + docs/notes/Leetcode 题解 - 排序.md | 227 + docs/notes/Leetcode 题解 - 搜索.md | 1269 ++++ docs/notes/Leetcode 题解 - 数学.md | 510 ++ docs/notes/Leetcode 题解 - 数组与矩阵.md | 434 ++ docs/notes/Leetcode 题解 - 栈和队列.md | 221 + docs/notes/Leetcode 题解 - 树.md | 1121 ++++ docs/notes/Leetcode 题解 - 目录.md | 39 + docs/notes/Leetcode 题解 - 贪心思想.md | 367 ++ docs/notes/Leetcode 题解 - 链表.md | 328 + docs/notes/Leetcode 题解.md | 7111 ---------------------- 18 files changed, 7355 insertions(+), 7111 deletions(-) create mode 100644 docs/notes/Leetcode 题解 - 二分查找.md create mode 100644 docs/notes/Leetcode 题解 - 位运算.md create mode 100644 docs/notes/Leetcode 题解 - 分治.md create mode 100644 docs/notes/Leetcode 题解 - 动态规划.md create mode 100644 docs/notes/Leetcode 题解 - 双指针.md create mode 100644 docs/notes/Leetcode 题解 - 哈希表.md create mode 100644 docs/notes/Leetcode 题解 - 图.md create mode 100644 docs/notes/Leetcode 题解 - 字符串.md create mode 100644 docs/notes/Leetcode 题解 - 排序.md create mode 100644 docs/notes/Leetcode 题解 - 搜索.md create mode 100644 docs/notes/Leetcode 题解 - 数学.md create mode 100644 docs/notes/Leetcode 题解 - 数组与矩阵.md create mode 100644 docs/notes/Leetcode 题解 - 栈和队列.md create mode 100644 docs/notes/Leetcode 题解 - 树.md create mode 100644 docs/notes/Leetcode 题解 - 目录.md create mode 100644 docs/notes/Leetcode 题解 - 贪心思想.md create mode 100644 docs/notes/Leetcode 题解 - 链表.md delete mode 100644 docs/notes/Leetcode 题解.md diff --git a/docs/notes/Leetcode 题解 - 二分查找.md b/docs/notes/Leetcode 题解 - 二分查找.md new file mode 100644 index 00000000..98f60142 --- /dev/null +++ b/docs/notes/Leetcode 题解 - 二分查找.md @@ -0,0 +1,292 @@ +* [点击阅读面试进阶指南 ](https://github.com/CyC2018/Backend-Interview-Guide) + +* [原理](#原理) + * [1. 正常实现](#1-正常实现) + * [2. 时间复杂度](#2-时间复杂度) + * [3. m 计算](#3-m-计算) + * [4. 返回值](#4-返回值) + * [5. 变种](#5-变种) +* [求开方](#求开方) +* [大于给定元素的最小元素](#大于给定元素的最小元素) +* [有序数组的 Single Element](#有序数组的-single-element) +* [第一个错误的版本](#第一个错误的版本) +* [旋转数组的最小数字](#旋转数组的最小数字) +* [查找区间](#查找区间) + + + +# 原理 + +## 1. 正常实现 + +```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; +} +``` + +## 2. 时间复杂度 + +二分查找也称为折半查找,每次都能将查找区间减半,这种折半特性的算法时间复杂度为 O(logN)。 + +## 3. m 计算 + +有两种计算中值 m 的方式: + +- m = (l + h) / 2 +- m = l + (h - l) / 2 + +l + h 可能出现加法溢出,最好使用第二种方式。 + +## 4. 返回值 + +循环退出时如果仍然没有查找到 key,那么表示查找失败。可以有两种返回值: + +- -1:以一个错误码表示没有查找到 key +- l:将 key 插入到 nums 中的正确位置 + +## 5. 变种 + +二分查找可以有很多变种,变种实现要注意边界值的判断。例如在一个有重复元素的数组中查找 key 的最左位置的实现如下: + +```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) { + h = m; + } else { + l = m + 1; + } + } + return l; +} +``` + +该实现和正常实现有以下不同: + +- 循环条件为 l < h +- h 的赋值表达式为 h = m +- 最后返回 l 而不是 -1 + +在 nums[m] >= key 的情况下,可以推导出最左 key 位于 [l, m] 区间中,这是一个闭区间。h 的赋值表达式为 h = m,因为 m 位置也可能是解。 + +在 h 的赋值表达式为 h = mid 的情况下,如果循环条件为 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 是否相等。 + +# 求开方 + +[69. Sqrt(x) (Easy)](https://leetcode.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; +} +``` + +# 大于给定元素的最小元素 + +[744. Find Smallest Letter Greater Than Target (Easy)](https://leetcode.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]; +} +``` + +# 有序数组的 Single Element + +[540. Single Element in a Sorted Array (Medium)](https://leetcode.com/problems/single-element-in-a-sorted-array/description/) + +```html +Input: [1, 1, 2, 3, 3, 4, 4, 8, 8] +Output: 2 +``` + +题目描述:一个有序数组只有一个数不出现两次,找出这个数。要求以 O(logN) 时间复杂度进行求解。 + +令 index 为 Single Element 在数组中的位置。如果 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]; +} +``` + +# 第一个错误的版本 + +[278. First Bad Version (Easy)](https://leetcode.com/problems/first-bad-version/description/) + +题目描述:给定一个元素 n 代表有 [1, 2, ..., n] 版本,可以调用 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; +} +``` + +# 旋转数组的最小数字 + +[153. Find Minimum in Rotated Sorted Array (Medium)](https://leetcode.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]; +} +``` + +# 查找区间 + +[34. Search for a Range (Medium)](https://leetcode.com/problems/search-for-a-range/description/) + +```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] +``` + +```java +public int[] searchRange(int[] nums, int target) { + int first = binarySearch(nums, target); + int last = binarySearch(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 binarySearch(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; +} +``` + diff --git a/docs/notes/Leetcode 题解 - 位运算.md b/docs/notes/Leetcode 题解 - 位运算.md new file mode 100644 index 00000000..4c6e7c4d --- /dev/null +++ b/docs/notes/Leetcode 题解 - 位运算.md @@ -0,0 +1,428 @@ +* [点击阅读面试进阶指南 ](https://github.com/CyC2018/Backend-Interview-Guide) + +* [原理](#原理) + * [1. 基本原理](#1-基本原理) + * [2. mask 计算](#2-mask-计算) + * [3. Java 中的位操作](#3-java-中的位操作) +* [例题](#例题) + * [统计两个数的二进制表示有多少位不同](#统计两个数的二进制表示有多少位不同) + * [数组中唯一一个不重复的元素](#数组中唯一一个不重复的元素) + * [找出数组中缺失的那个数](#找出数组中缺失的那个数) + * [数组中不重复的两个元素](#数组中不重复的两个元素) + * [翻转一个数的比特位](#翻转一个数的比特位) + * [不用额外变量交换两个整数](#不用额外变量交换两个整数) + * [判断一个数是不是 2 的 n 次方](#判断一个数是不是-2-的-n-次方) + * [判断一个数是不是 4 的 n 次方](#判断一个数是不是-4-的-n-次方) + * [判断一个数的位级表示是否不会出现连续的 0 和 1](#判断一个数的位级表示是否不会出现连续的-0-和-1) + * [求一个数的补码](#求一个数的补码) + * [实现整数的加法](#实现整数的加法) + * [字符串数组最大乘积](#字符串数组最大乘积) + * [统计从 0 \~ n 每个数的二进制表示中 1 的个数](#统计从-0-\~-n-每个数的二进制表示中-1-的个数) + + + +# 原理 + +## 1. 基本原理 + +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 的特点,可以将三个数中重复的两个数去除,只留下另一个数。 +- 利用 x & 0s = 0 和 x & 1s = x 的特点,可以实现掩码操作。一个数 num 与 mask:00111100 进行位与操作,只保留 num 中与 mask 的 1 部分相对应的位。 +- 利用 x | 0s = x 和 x | 1s = 1s 的特点,可以实现设值操作。一个数 num 与 mask:00111100 进行位或操作,将 num 中与 mask 的 1 部分相对应的位都设置为 1。 + +位与运算技巧: + +- n&(n-1) 去除 n 的位级表示中最低的那一位。例如对于二进制表示 10110100,减去 1 得到 10110011,这两个数相与得到 10110000。 +- n&(-n) 得到 n 的位级表示中最低的那一位。-n 得到 n 的反码加 1,对于二进制表示 10110100,-n 得到 01001100,相与得到 00000100。 +- n-n&(\~n+1) 去除 n 的位级表示中最高的那一位。 + +移位运算: + +- \>\> n 为算术右移,相当于除以 2n; +- \>\>\> n 为无符号右移,左边会补上 0。 +- << n 为算术左移,相当于乘以 2n。 + +## 2. 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 即可,例如将 1<<(4+1)-1 = 00010000-1 = 00001111。 + +要得到 1 到 i 位为 0 的 mask,只需将 1 到 i 位为 1 的 mask 取反,即 \~(1<<(i+1)-1)。 + +## 3. Java 中的位操作 + +```html +static int Integer.bitCount(); // 统计 1 的数量 +static int Integer.highestOneBit(); // 获得最高位 +static String toBinaryString(int i); // 转换为二进制表示的字符串 +``` + +# 例题 + +## 统计两个数的二进制表示有多少位不同 + +[461. Hamming Distance (Easy)](https://leetcode.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); +} +``` + +## 数组中唯一一个不重复的元素 + +[136. Single Number (Easy)](https://leetcode.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; +} +``` + +## 找出数组中缺失的那个数 + +[268. Missing Number (Easy)](https://leetcode.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; +} +``` + +## 数组中不重复的两个元素 + +[260. Single Number III (Medium)](https://leetcode.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; +} +``` + +## 翻转一个数的比特位 + +[190. Reverse Bits (Easy)](https://leetcode.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 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; +} +``` + +## 不用额外变量交换两个整数 + +[程序员代码面试指南 :P317](#) + +```java +a = a ^ b; +b = a ^ b; +a = a ^ b; +``` + +## 判断一个数是不是 2 的 n 次方 + +[231. Power of Two (Easy)](https://leetcode.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; +} +``` + +## 判断一个数是不是 4 的 n 次方 + +[342. Power of Four (Easy)](https://leetcode.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*"); +} +``` + +## 判断一个数的位级表示是否不会出现连续的 0 和 1 + +[693. Binary Number with Alternating Bits (Easy)](https://leetcode.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; +} +``` + +## 求一个数的补码 + +[476. Number Complement (Easy)](https://leetcode.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); +} +``` + +## 实现整数的加法 + +[371. Sum of Two Integers (Easy)](https://leetcode.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); +} +``` + +## 字符串数组最大乘积 + +[318. Maximum Product of Word Lengths (Medium)](https://leetcode.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; +} +``` + +## 统计从 0 \~ n 每个数的二进制表示中 1 的个数 + +[338. Counting Bits (Medium)](https://leetcode.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; +} +``` + diff --git a/docs/notes/Leetcode 题解 - 分治.md b/docs/notes/Leetcode 题解 - 分治.md new file mode 100644 index 00000000..aaa09f60 --- /dev/null +++ b/docs/notes/Leetcode 题解 - 分治.md @@ -0,0 +1,50 @@ +* [点击阅读面试进阶指南 ](https://github.com/CyC2018/Backend-Interview-Guide) + +* [给表达式加括号](#给表达式加括号) + + + +# 给表达式加括号 + +[241. Different Ways to Add Parentheses (Medium)](https://leetcode.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 diffWaysToCompute(String input) { + List ways = new ArrayList<>(); + for (int i = 0; i < input.length(); i++) { + char c = input.charAt(i); + if (c == '+' || c == '-' || c == '*') { + List left = diffWaysToCompute(input.substring(0, i)); + List 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; +} +``` diff --git a/docs/notes/Leetcode 题解 - 动态规划.md b/docs/notes/Leetcode 题解 - 动态规划.md new file mode 100644 index 00000000..0a7a3cdb --- /dev/null +++ b/docs/notes/Leetcode 题解 - 动态规划.md @@ -0,0 +1,1222 @@ +* [点击阅读面试进阶指南 ](https://github.com/CyC2018/Backend-Interview-Guide) + +* [斐波那契数列](#斐波那契数列) + * [爬楼梯](#爬楼梯) + * [强盗抢劫](#强盗抢劫) + * [强盗在环形街区抢劫](#强盗在环形街区抢劫) + * [信件错排](#信件错排) + * [母牛生产](#母牛生产) +* [矩阵路径](#矩阵路径) + * [矩阵的最小路径和](#矩阵的最小路径和) + * [矩阵的总路径数](#矩阵的总路径数) +* [数组区间](#数组区间) + * [数组区间和](#数组区间和) + * [数组中等差递增子区间的个数](#数组中等差递增子区间的个数) +* [分割整数](#分割整数) + * [分割整数的最大乘积](#分割整数的最大乘积) + * [按平方数来分割整数](#按平方数来分割整数) + * [分割整数构成字母字符串](#分割整数构成字母字符串) +* [最长递增子序列](#最长递增子序列) + * [最长递增子序列](#最长递增子序列) + * [一组整数对能够构成的最长链](#一组整数对能够构成的最长链) + * [最长摆动子序列](#最长摆动子序列) +* [最长公共子序列](#最长公共子序列) +* [0-1 背包](#0-1-背包) + * [空间优化](#空间优化) + * [无法使用贪心算法的解释](#无法使用贪心算法的解释) + * [变种](#变种) + * [划分数组为和相等的两部分](#划分数组为和相等的两部分) + * [改变一组数的正负号使得它们的和为一给定数](#改变一组数的正负号使得它们的和为一给定数) + * [01 字符构成最多的字符串](#01-字符构成最多的字符串) + * [找零钱的最少硬币数](#找零钱的最少硬币数) + * [找零钱的硬币数组合](#找零钱的硬币数组合) + * [字符串按单词列表分割](#字符串按单词列表分割) + * [组合总和](#组合总和) +* [股票交易](#股票交易) + * [需要冷却期的股票交易](#需要冷却期的股票交易) + * [需要交易费用的股票交易](#需要交易费用的股票交易) + * [只能进行两次的股票交易](#只能进行两次的股票交易) + * [只能进行 k 次的股票交易](#只能进行-k-次的股票交易) +* [字符串编辑](#字符串编辑) + * [删除两个字符串的字符使它们相等](#删除两个字符串的字符使它们相等) + * [编辑距离](#编辑距离) + * [复制粘贴字符](#复制粘贴字符) + + + +递归和动态规划都是将原问题拆成多个子问题然后求解,他们之间最本质的区别是,动态规划保存了子问题的解,避免重复计算。 + +# 斐波那契数列 + +## 爬楼梯 + +[70. Climbing Stairs (Easy)](https://leetcode.com/problems/climbing-stairs/description/) + +题目描述:有 N 阶楼梯,每次可以上一阶或者两阶,求有多少种上楼梯的方法。 + +定义一个数组 dp 存储上楼梯的方法数(为了方便讨论,数组下标从 1 开始),dp[i] 表示走到第 i 个楼梯的方法数目。 + +第 i 个楼梯可以从第 i-1 和 i-2 个楼梯再走一步到达,走到第 i 个楼梯的方法数为走到第 i-1 和第 i-2 个楼梯的方法数之和。 + + + +

+ + +考虑到 dp[i] 只与 dp[i - 1] 和 dp[i - 2] 有关,因此可以只用两个变量来存储 dp[i - 1] 和 dp[i - 2],使得原来的 O(N) 空间复杂度优化为 O(1) 复杂度。 + +```java +public int climbStairs(int n) { + if (n <= 2) { + return n; + } + int pre2 = 1, pre1 = 2; + for (int i = 2; i < n; i++) { + int cur = pre1 + pre2; + pre2 = pre1; + pre1 = cur; + } + return pre1; +} +``` + +## 强盗抢劫 + +[198. House Robber (Easy)](https://leetcode.com/problems/house-robber/description/) + +题目描述:抢劫一排住户,但是不能抢邻近的住户,求最大抢劫量。 + +定义 dp 数组用来存储最大的抢劫量,其中 dp[i] 表示抢到第 i 个住户时的最大抢劫量。 + +由于不能抢劫邻近住户,如果抢劫了第 i -1 个住户,那么就不能再抢劫第 i 个住户,所以 + + + +

+ +```java +public int rob(int[] nums) { + int pre2 = 0, pre1 = 0; + for (int i = 0; i < nums.length; i++) { + int cur = Math.max(pre2 + nums[i], pre1); + pre2 = pre1; + pre1 = cur; + } + return pre1; +} +``` + +## 强盗在环形街区抢劫 + +[213. House Robber II (Medium)](https://leetcode.com/problems/house-robber-ii/description/) + +```java +public int rob(int[] nums) { + if (nums == null || nums.length == 0) { + return 0; + } + int n = nums.length; + if (n == 1) { + return nums[0]; + } + return Math.max(rob(nums, 0, n - 2), rob(nums, 1, n - 1)); +} + +private int rob(int[] nums, int first, int last) { + int pre2 = 0, pre1 = 0; + for (int i = first; i <= last; i++) { + int cur = Math.max(pre1, pre2 + nums[i]); + pre2 = pre1; + pre1 = cur; + } + return pre1; +} +``` + +## 信件错排 + +题目描述:有 N 个 信 和 信封,它们被打乱,求错误装信方式的数量。 + +定义一个数组 dp 存储错误方式数量,dp[i] 表示前 i 个信和信封的错误方式数量。假设第 i 个信装到第 j 个信封里面,而第 j 个信装到第 k 个信封里面。根据 i 和 k 是否相等,有两种情况: + +- i==k,交换 i 和 k 的信后,它们的信和信封在正确的位置,但是其余 i-2 封信有 dp[i-2] 种错误装信的方式。由于 j 有 i-1 种取值,因此共有 (i-1)\*dp[i-2] 种错误装信方式。 +- i != k,交换 i 和 j 的信后,第 i 个信和信封在正确的位置,其余 i-1 封信有 dp[i-1] 种错误装信方式。由于 j 有 i-1 种取值,因此共有 (i-1)\*dp[i-1] 种错误装信方式。 + +综上所述,错误装信数量方式数量为: + + + +

+ +## 母牛生产 + +[程序员代码面试指南-P181](#) + +题目描述:假设农场中成熟的母牛每年都会生 1 头小母牛,并且永远不会死。第一年有 1 只小母牛,从第二年开始,母牛开始生小母牛。每只小母牛 3 年之后成熟又可以生小母牛。给定整数 N,求 N 年后牛的数量。 + +第 i 年成熟的牛的数量为: + + + +

+ +# 矩阵路径 + +## 矩阵的最小路径和 + +[64. Minimum Path Sum (Medium)](https://leetcode.com/problems/minimum-path-sum/description/) + +```html +[[1,3,1], + [1,5,1], + [4,2,1]] +Given the above grid map, return 7. Because the path 1→3→1→1→1 minimizes the sum. +``` + +题目描述:求从矩阵的左上角到右下角的最小路径和,每次只能向右和向下移动。 + +```java +public int minPathSum(int[][] grid) { + if (grid.length == 0 || grid[0].length == 0) { + return 0; + } + int m = grid.length, n = grid[0].length; + int[] dp = new int[n]; + for (int i = 0; i < m; i++) { + for (int j = 0; j < n; j++) { + if (j == 0) { + dp[j] = dp[j]; // 只能从上侧走到该位置 + } else if (i == 0) { + dp[j] = dp[j - 1]; // 只能从左侧走到该位置 + } else { + dp[j] = Math.min(dp[j - 1], dp[j]); + } + dp[j] += grid[i][j]; + } + } + return dp[n - 1]; +} +``` + +## 矩阵的总路径数 + +[62. Unique Paths (Medium)](https://leetcode.com/problems/unique-paths/description/) + +题目描述:统计从矩阵左上角到右下角的路径总数,每次只能向右或者向下移动。 + +

+ +```java +public int uniquePaths(int m, int n) { + int[] dp = new int[n]; + Arrays.fill(dp, 1); + for (int i = 1; i < m; i++) { + for (int j = 1; j < n; j++) { + dp[j] = dp[j] + dp[j - 1]; + } + } + return dp[n - 1]; +} +``` + +也可以直接用数学公式求解,这是一个组合问题。机器人总共移动的次数 S=m+n-2,向下移动的次数 D=m-1,那么问题可以看成从 S 中取出 D 个位置的组合数量,这个问题的解为 C(S, D)。 + +```java +public int uniquePaths(int m, int n) { + int S = m + n - 2; // 总共的移动次数 + int D = m - 1; // 向下的移动次数 + long ret = 1; + for (int i = 1; i <= D; i++) { + ret = ret * (S - D + i) / i; + } + return (int) ret; +} +``` + +# 数组区间 + +## 数组区间和 + +[303. Range Sum Query - Immutable (Easy)](https://leetcode.com/problems/range-sum-query-immutable/description/) + +```html +Given nums = [-2, 0, 3, -5, 2, -1] + +sumRange(0, 2) -> 1 +sumRange(2, 5) -> -1 +sumRange(0, 5) -> -3 +``` + +求区间 i \~ j 的和,可以转换为 sum[j + 1] - sum[i],其中 sum[i] 为 0 \~ i - 1 的和。 + +```java +class NumArray { + + private int[] sums; + + public NumArray(int[] nums) { + sums = new int[nums.length + 1]; + for (int i = 1; i <= nums.length; i++) { + sums[i] = sums[i - 1] + nums[i - 1]; + } + } + + public int sumRange(int i, int j) { + return sums[j + 1] - sums[i]; + } +} +``` + +## 数组中等差递增子区间的个数 + +[413. Arithmetic Slices (Medium)](https://leetcode.com/problems/arithmetic-slices/description/) + +```html +A = [1, 2, 3, 4] +return: 3, for 3 arithmetic slices in A: [1, 2, 3], [2, 3, 4] and [1, 2, 3, 4] itself. +``` + +dp[i] 表示以 A[i] 为结尾的等差递增子区间的个数。 + +在 A[i] - A[i - 1] == A[i - 1] - A[i - 2] 的条件下,{A[i - 2], A[i - 1], A[i]} 是一个等差递增子区间。如果 {A[i - 3], A[i - 2], A[i - 1]} 是一个等差递增子区间,那么 {A[i - 3], A[i - 2], A[i - 1], A[i]} 也是等差递增子区间,dp[i] = dp[i-1] + 1。 + +```java +public int numberOfArithmeticSlices(int[] A) { + if (A == null || A.length == 0) { + return 0; + } + int n = A.length; + int[] dp = new int[n]; + for (int i = 2; i < n; i++) { + if (A[i] - A[i - 1] == A[i - 1] - A[i - 2]) { + dp[i] = dp[i - 1] + 1; + } + } + int total = 0; + for (int cnt : dp) { + total += cnt; + } + return total; +} +``` + +# 分割整数 + +## 分割整数的最大乘积 + +[343. Integer Break (Medim)](https://leetcode.com/problems/integer-break/description/) + +题目描述:For example, given n = 2, return 1 (2 = 1 + 1); given n = 10, return 36 (10 = 3 + 3 + 4). + +```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 - 1; j++) { + dp[i] = Math.max(dp[i], Math.max(j * dp[i - j], j * (i - j))); + } + } + return dp[n]; +} +``` + +## 按平方数来分割整数 + +[279. Perfect Squares(Medium)](https://leetcode.com/problems/perfect-squares/description/) + +题目描述:For example, given n = 12, return 3 because 12 = 4 + 4 + 4; given n = 13, return 2 because 13 = 4 + 9. + +```java +public int numSquares(int n) { + List squareList = generateSquareList(n); + int[] dp = new int[n + 1]; + for (int i = 1; i <= n; i++) { + int min = Integer.MAX_VALUE; + for (int square : squareList) { + if (square > i) { + break; + } + min = Math.min(min, dp[i - square] + 1); + } + dp[i] = min; + } + return dp[n]; +} + +private List generateSquareList(int n) { + List squareList = new ArrayList<>(); + int diff = 3; + int square = 1; + while (square <= n) { + squareList.add(square); + square += diff; + diff += 2; + } + return squareList; +} +``` + +## 分割整数构成字母字符串 + +[91. Decode Ways (Medium)](https://leetcode.com/problems/decode-ways/description/) + +题目描述:Given encoded message "12", it could be decoded as "AB" (1 2) or "L" (12). + +```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]; +} +``` + +# 最长递增子序列 + +已知一个序列 {S1, S2,...,Sn},取出若干数组成新的序列 {Si1, Si2,..., Sim},其中 i1、i2 ... im 保持递增,即新序列中各个数仍然保持原数列中的先后顺序,称新序列为原序列的一个 **子序列** 。 + +如果在子序列中,当下标 ix > iy 时,Six > Siy,称子序列为原序列的一个 **递增子序列** 。 + +定义一个数组 dp 存储最长递增子序列的长度,dp[n] 表示以 Sn 结尾的序列的最长递增子序列长度。对于一个递增子序列 {Si1, Si2,...,Sim},如果 im < n 并且 Sim < Sn,此时 {Si1, Si2,..., Sim, Sn} 为一个递增子序列,递增子序列的长度增加 1。满足上述条件的递增子序列中,长度最长的那个递增子序列就是要找的,在长度最长的递增子序列上加上 Sn 就构成了以 Sn 为结尾的最长递增子序列。因此 dp[n] = max{ dp[i]+1 | Si < Sn && i < n} 。 + +因为在求 dp[n] 时可能无法找到一个满足条件的递增子序列,此时 {Sn} 就构成了递增子序列,需要对前面的求解方程做修改,令 dp[n] 最小为 1,即: + + + +

+ +对于一个长度为 N 的序列,最长递增子序列并不一定会以 SN 为结尾,因此 dp[N] 不是序列的最长递增子序列的长度,需要遍历 dp 数组找出最大值才是所要的结果,max{ dp[i] | 1 <= i <= N} 即为所求。 + +## 最长递增子序列 + +[300. Longest Increasing Subsequence (Medium)](https://leetcode.com/problems/longest-increasing-subsequence/description/) + +```java +public int lengthOfLIS(int[] nums) { + int n = nums.length; + int[] dp = new int[n]; + for (int i = 0; i < n; i++) { + int max = 1; + for (int j = 0; j < i; j++) { + if (nums[i] > nums[j]) { + max = Math.max(max, dp[j] + 1); + } + } + dp[i] = max; + } + return Arrays.stream(dp).max().orElse(0); +} +``` + +使用 Stream 求最大值会导致运行时间过长,可以改成以下形式: + +```java +int ret = 0; +for (int i = 0; i < n; i++) { + ret = Math.max(ret, dp[i]); +} +return ret; +``` + +以上解法的时间复杂度为 O(N2),可以使用二分查找将时间复杂度降低为 O(NlogN)。 + +定义一个 tails 数组,其中 tails[i] 存储长度为 i + 1 的最长递增子序列的最后一个元素。对于一个元素 x, + +- 如果它大于 tails 数组所有的值,那么把它添加到 tails 后面,表示最长递增子序列长度加 1; +- 如果 tails[i-1] < x <= tails[i],那么更新 tails[i] = x。 + +例如对于数组 [4,3,6,5],有: + +```html +tails len num +[] 0 4 +[4] 1 3 +[3] 1 6 +[3,6] 2 5 +[3,5] 2 null +``` + +可以看出 tails 数组保持有序,因此在查找 Si 位于 tails 数组的位置时就可以使用二分查找。 + +```java +public int lengthOfLIS(int[] nums) { + int n = nums.length; + int[] tails = new int[n]; + int len = 0; + for (int num : nums) { + int index = binarySearch(tails, len, num); + tails[index] = num; + if (index == len) { + len++; + } + } + return len; +} + +private int binarySearch(int[] tails, int len, int key) { + int l = 0, h = len; + while (l < h) { + int mid = l + (h - l) / 2; + if (tails[mid] == key) { + return mid; + } else if (tails[mid] > key) { + h = mid; + } else { + l = mid + 1; + } + } + return l; +} +``` + +## 一组整数对能够构成的最长链 + +[646. Maximum Length of Pair Chain (Medium)](https://leetcode.com/problems/maximum-length-of-pair-chain/description/) + +```html +Input: [[1,2], [2,3], [3,4]] +Output: 2 +Explanation: The longest chain is [1,2] -> [3,4] +``` + +题目描述:对于 (a, b) 和 (c, d) ,如果 b < c,则它们可以构成一条链。 + +```java +public int findLongestChain(int[][] pairs) { + if (pairs == null || pairs.length == 0) { + return 0; + } + Arrays.sort(pairs, (a, b) -> (a[0] - b[0])); + int n = pairs.length; + int[] dp = new int[n]; + Arrays.fill(dp, 1); + for (int i = 1; i < n; i++) { + for (int j = 0; j < i; j++) { + if (pairs[j][1] < pairs[i][0]) { + dp[i] = Math.max(dp[i], dp[j] + 1); + } + } + } + return Arrays.stream(dp).max().orElse(0); +} +``` + +## 最长摆动子序列 + +[376. Wiggle Subsequence (Medium)](https://leetcode.com/problems/wiggle-subsequence/description/) + +```html +Input: [1,7,4,9,2,5] +Output: 6 +The entire sequence is a wiggle sequence. + +Input: [1,17,5,10,13,15,10,5,16,8] +Output: 7 +There are several subsequences that achieve this length. One is [1,17,10,13,10,16,8]. + +Input: [1,2,3,4,5,6,7,8,9] +Output: 2 +``` + +要求:使用 O(N) 时间复杂度求解。 + +```java +public int wiggleMaxLength(int[] nums) { + if (nums == null || nums.length == 0) { + return 0; + } + int up = 1, down = 1; + for (int i = 1; i < nums.length; i++) { + if (nums[i] > nums[i - 1]) { + up = down + 1; + } else if (nums[i] < nums[i - 1]) { + down = up + 1; + } + } + return Math.max(up, down); +} +``` + +# 最长公共子序列 + +对于两个子序列 S1 和 S2,找出它们最长的公共子序列。 + +定义一个二维数组 dp 用来存储最长公共子序列的长度,其中 dp[i][j] 表示 S1 的前 i 个字符与 S2 的前 j 个字符最长公共子序列的长度。考虑 S1i 与 S2j 值是否相等,分为两种情况: + +- 当 S1i==S2j 时,那么就能在 S1 的前 i-1 个字符与 S2 的前 j-1 个字符最长公共子序列的基础上再加上 S1i 这个值,最长公共子序列长度加 1,即 dp[i][j] = dp[i-1][j-1] + 1。 +- 当 S1i != S2j 时,此时最长公共子序列为 S1 的前 i-1 个字符和 S2 的前 j 个字符最长公共子序列,或者 S1 的前 i 个字符和 S2 的前 j-1 个字符最长公共子序列,取它们的最大者,即 dp[i][j] = max{ dp[i-1][j], dp[i][j-1] }。 + +综上,最长公共子序列的状态转移方程为: + + + +

+ +对于长度为 N 的序列 S1 和长度为 M 的序列 S2,dp[N][M] 就是序列 S1 和序列 S2 的最长公共子序列长度。 + +与最长递增子序列相比,最长公共子序列有以下不同点: + +- 针对的是两个序列,求它们的最长公共子序列。 +- 在最长递增子序列中,dp[i] 表示以 Si 为结尾的最长递增子序列长度,子序列必须包含 Si ;在最长公共子序列中,dp[i][j] 表示 S1 中前 i 个字符与 S2 中前 j 个字符的最长公共子序列长度,不一定包含 S1i 和 S2j。 +- 在求最终解时,最长公共子序列中 dp[N][M] 就是最终解,而最长递增子序列中 dp[N] 不是最终解,因为以 SN 为结尾的最长递增子序列不一定是整个序列最长递增子序列,需要遍历一遍 dp 数组找到最大者。 + +```java +public int lengthOfLCS(int[] nums1, int[] nums2) { + int n1 = nums1.length, n2 = nums2.length; + int[][] dp = new int[n1 + 1][n2 + 1]; + for (int i = 1; i <= n1; i++) { + for (int j = 1; j <= n2; j++) { + if (nums1[i - 1] == nums2[j - 1]) { + dp[i][j] = dp[i - 1][j - 1] + 1; + } else { + dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]); + } + } + } + return dp[n1][n2]; +} +``` + +# 0-1 背包 + +有一个容量为 N 的背包,要用这个背包装下物品的价值最大,这些物品有两个属性:体积 w 和价值 v。 + +定义一个二维数组 dp 存储最大价值,其中 dp[i][j] 表示前 i 件物品体积不超过 j 的情况下能达到的最大价值。设第 i 件物品体积为 w,价值为 v,根据第 i 件物品是否添加到背包中,可以分两种情况讨论: + +- 第 i 件物品没添加到背包,总体积不超过 j 的前 i 件物品的最大价值就是总体积不超过 j 的前 i-1 件物品的最大价值,dp[i][j] = dp[i-1][j]。 +- 第 i 件物品添加到背包中,dp[i][j] = dp[i-1][j-w] + v。 + +第 i 件物品可添加也可以不添加,取决于哪种情况下最大价值更大。因此,0-1 背包的状态转移方程为: + + + +

+ +```java +public int knapsack(int W, int N, int[] weights, int[] values) { + int[][] dp = new int[N + 1][W + 1]; + for (int i = 1; i <= N; i++) { + int w = weights[i - 1], v = values[i - 1]; + for (int j = 1; j <= W; j++) { + if (j >= w) { + dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - w] + v); + } else { + dp[i][j] = dp[i - 1][j]; + } + } + } + return dp[N][W]; +} +``` + +## 空间优化 + +在程序实现时可以对 0-1 背包做优化。观察状态转移方程可以知道,前 i 件物品的状态仅与前 i-1 件物品的状态有关,因此可以将 dp 定义为一维数组,其中 dp[j] 既可以表示 dp[i-1][j] 也可以表示 dp[i][j]。此时, + + + +

+ +因为 dp[j-w] 表示 dp[i-1][j-w],因此不能先求 dp[i][j-w],以防将 dp[i-1][j-w] 覆盖。也就是说要先计算 dp[i][j] 再计算 dp[i][j-w],在程序实现时需要按倒序来循环求解。 + +```java +public int knapsack(int W, int N, int[] weights, int[] values) { + int[] dp = new int[W + 1]; + for (int i = 1; i <= N; i++) { + int w = weights[i - 1], v = values[i - 1]; + for (int j = W; j >= 1; j--) { + if (j >= w) { + dp[j] = Math.max(dp[j], dp[j - w] + v); + } + } + } + return dp[W]; +} +``` + +## 无法使用贪心算法的解释 + +0-1 背包问题无法使用贪心算法来求解,也就是说不能按照先添加性价比最高的物品来达到最优,这是因为这种方式可能造成背包空间的浪费,从而无法达到最优。考虑下面的物品和一个容量为 5 的背包,如果先添加物品 0 再添加物品 1,那么只能存放的价值为 16,浪费了大小为 2 的空间。最优的方式是存放物品 1 和物品 2,价值为 22. + +| id | w | v | v/w | +| --- | --- | --- | --- | +| 0 | 1 | 6 | 6 | +| 1 | 2 | 10 | 5 | +| 2 | 3 | 12 | 4 | + +## 变种 + +- 完全背包:物品数量为无限个 + +- 多重背包:物品数量有限制 + +- 多维费用背包:物品不仅有重量,还有体积,同时考虑这两种限制 + +- 其它:物品之间相互约束或者依赖 + +## 划分数组为和相等的两部分 + +[416. Partition Equal Subset Sum (Medium)](https://leetcode.com/problems/partition-equal-subset-sum/description/) + +```html +Input: [1, 5, 11, 5] + +Output: true + +Explanation: The array can be partitioned as [1, 5, 5] and [11]. +``` + +可以看成一个背包大小为 sum/2 的 0-1 背包问题。 + +```java +public boolean canPartition(int[] nums) { + int sum = computeArraySum(nums); + if (sum % 2 != 0) { + return false; + } + int W = sum / 2; + boolean[] dp = new boolean[W + 1]; + dp[0] = true; + for (int num : nums) { // 0-1 背包一个物品只能用一次 + for (int i = W; i >= num; i--) { // 从后往前,先计算 dp[i] 再计算 dp[i-num] + dp[i] = dp[i] || dp[i - num]; + } + } + return dp[W]; +} + +private int computeArraySum(int[] nums) { + int sum = 0; + for (int num : nums) { + sum += num; + } + return sum; +} +``` + +## 改变一组数的正负号使得它们的和为一给定数 + +[494. Target Sum (Medium)](https://leetcode.com/problems/target-sum/description/) + +```html +Input: nums is [1, 1, 1, 1, 1], S is 3. +Output: 5 +Explanation: + +-1+1+1+1+1 = 3 ++1-1+1+1+1 = 3 ++1+1-1+1+1 = 3 ++1+1+1-1+1 = 3 ++1+1+1+1-1 = 3 + +There are 5 ways to assign symbols to make the sum of nums be target 3. +``` + +该问题可以转换为 Subset Sum 问题,从而使用 0-1 背包的方法来求解。 + +可以将这组数看成两部分,P 和 N,其中 P 使用正号,N 使用负号,有以下推导: + +```html + sum(P) - sum(N) = target +sum(P) + sum(N) + sum(P) - sum(N) = target + sum(P) + sum(N) + 2 * sum(P) = target + sum(nums) +``` + +因此只要找到一个子集,令它们都取正号,并且和等于 (target + sum(nums))/2,就证明存在解。 + +```java +public int findTargetSumWays(int[] nums, int S) { + int sum = computeArraySum(nums); + if (sum < S || (sum + S) % 2 == 1) { + return 0; + } + int W = (sum + S) / 2; + int[] dp = new int[W + 1]; + dp[0] = 1; + for (int num : nums) { + for (int i = W; i >= num; i--) { + dp[i] = dp[i] + dp[i - num]; + } + } + return dp[W]; +} + +private int computeArraySum(int[] nums) { + int sum = 0; + for (int num : nums) { + sum += num; + } + return sum; +} +``` + +DFS 解法: + +```java +public int findTargetSumWays(int[] nums, int S) { + return findTargetSumWays(nums, 0, S); +} + +private int findTargetSumWays(int[] nums, int start, int S) { + if (start == nums.length) { + return S == 0 ? 1 : 0; + } + return findTargetSumWays(nums, start + 1, S + nums[start]) + + findTargetSumWays(nums, start + 1, S - nums[start]); +} +``` + +## 01 字符构成最多的字符串 + +[474. Ones and Zeroes (Medium)](https://leetcode.com/problems/ones-and-zeroes/description/) + +```html +Input: Array = {"10", "0001", "111001", "1", "0"}, m = 5, n = 3 +Output: 4 + +Explanation: There are totally 4 strings can be formed by the using of 5 0s and 3 1s, which are "10","0001","1","0" +``` + +这是一个多维费用的 0-1 背包问题,有两个背包大小,0 的数量和 1 的数量。 + +```java +public int findMaxForm(String[] strs, int m, int n) { + if (strs == null || strs.length == 0) { + return 0; + } + int[][] dp = new int[m + 1][n + 1]; + for (String s : strs) { // 每个字符串只能用一次 + int ones = 0, zeros = 0; + for (char c : s.toCharArray()) { + if (c == '0') { + zeros++; + } else { + ones++; + } + } + for (int i = m; i >= zeros; i--) { + for (int j = n; j >= ones; j--) { + dp[i][j] = Math.max(dp[i][j], dp[i - zeros][j - ones] + 1); + } + } + } + return dp[m][n]; +} +``` + +## 找零钱的最少硬币数 + +[322. Coin Change (Medium)](https://leetcode.com/problems/coin-change/description/) + +```html +Example 1: +coins = [1, 2, 5], amount = 11 +return 3 (11 = 5 + 5 + 1) + +Example 2: +coins = [2], amount = 3 +return -1. +``` + +题目描述:给一些面额的硬币,要求用这些硬币来组成给定面额的钱数,并且使得硬币数量最少。硬币可以重复使用。 + +- 物品:硬币 +- 物品大小:面额 +- 物品价值:数量 + +因为硬币可以重复使用,因此这是一个完全背包问题。完全背包只需要将 0-1 背包中逆序遍历 dp 数组改为正序遍历即可。 + +```java +public int coinChange(int[] coins, int amount) { + if (amount == 0 || coins == null || coins.length == 0) { + return 0; + } + int[] dp = new int[amount + 1]; + for (int coin : coins) { + for (int i = coin; i <= amount; i++) { //将逆序遍历改为正序遍历 + if (i == coin) { + dp[i] = 1; + } else if (dp[i] == 0 && dp[i - coin] != 0) { + dp[i] = dp[i - coin] + 1; + } else if (dp[i - coin] != 0) { + dp[i] = Math.min(dp[i], dp[i - coin] + 1); + } + } + } + return dp[amount] == 0 ? -1 : dp[amount]; +} +``` + +## 找零钱的硬币数组合 + +[518\. Coin Change 2 (Medium)](https://leetcode.com/problems/coin-change-2/description/) + +```text-html-basic +Input: amount = 5, coins = [1, 2, 5] +Output: 4 +Explanation: there are four ways to make up the amount: +5=5 +5=2+2+1 +5=2+1+1+1 +5=1+1+1+1+1 +``` + +完全背包问题,使用 dp 记录可达成目标的组合数目。 + +```java +public int change(int amount, int[] coins) { + if (amount == 0 || coins == null || coins.length == 0) { + return 0; + } + int[] dp = new int[amount + 1]; + dp[0] = 1; + for (int coin : coins) { + for (int i = coin; i <= amount; i++) { + dp[i] += dp[i - coin]; + } + } + return dp[amount]; +} +``` + +## 字符串按单词列表分割 + +[139. Word Break (Medium)](https://leetcode.com/problems/word-break/description/) + +```html +s = "leetcode", +dict = ["leet", "code"]. +Return true because "leetcode" can be segmented as "leet code". +``` + +dict 中的单词没有使用次数的限制,因此这是一个完全背包问题。该问题涉及到字典中单词的使用顺序,因此可理解为涉及顺序的完全背包问题。 + +求解顺序的完全背包问题时,对物品的迭代应该放在最里层。 + +```java +public boolean wordBreak(String s, List wordDict) { + int n = s.length(); + boolean[] dp = new boolean[n + 1]; + dp[0] = true; + for (int i = 1; i <= n; i++) { + for (String word : wordDict) { // 对物品的迭代应该放在最里层 + int len = word.length(); + if (len <= i && word.equals(s.substring(i - len, i))) { + dp[i] = dp[i] || dp[i - len]; + } + } + } + return dp[n]; +} +``` + +## 组合总和 + +[377. Combination Sum IV (Medium)](https://leetcode.com/problems/combination-sum-iv/description/) + +```html +nums = [1, 2, 3] +target = 4 + +The possible combination ways are: +(1, 1, 1, 1) +(1, 1, 2) +(1, 2, 1) +(1, 3) +(2, 1, 1) +(2, 2) +(3, 1) + +Note that different sequences are counted as different combinations. + +Therefore the output is 7. +``` + +涉及顺序的完全背包。 + +```java +public int combinationSum4(int[] nums, int target) { + if (nums == null || nums.length == 0) { + return 0; + } + int[] maximum = new int[target + 1]; + maximum[0] = 1; + Arrays.sort(nums); + for (int i = 1; i <= target; i++) { + for (int j = 0; j < nums.length && nums[j] <= i; j++) { + maximum[i] += maximum[i - nums[j]]; + } + } + return maximum[target]; +} +``` + +# 股票交易 + +## 需要冷却期的股票交易 + +[309. Best Time to Buy and Sell Stock with Cooldown(Medium)](https://leetcode.com/problems/best-time-to-buy-and-sell-stock-with-cooldown/description/) + +题目描述:交易之后需要有一天的冷却时间。 + +

+ +```java +public int maxProfit(int[] prices) { + if (prices == null || prices.length == 0) { + return 0; + } + int N = prices.length; + int[] buy = new int[N]; + int[] s1 = new int[N]; + int[] sell = new int[N]; + int[] s2 = new int[N]; + s1[0] = buy[0] = -prices[0]; + sell[0] = s2[0] = 0; + for (int i = 1; i < N; i++) { + buy[i] = s2[i - 1] - prices[i]; + s1[i] = Math.max(buy[i - 1], s1[i - 1]); + sell[i] = Math.max(buy[i - 1], s1[i - 1]) + prices[i]; + s2[i] = Math.max(s2[i - 1], sell[i - 1]); + } + return Math.max(sell[N - 1], s2[N - 1]); +} +``` + +## 需要交易费用的股票交易 + +[714. Best Time to Buy and Sell Stock with Transaction Fee (Medium)](https://leetcode.com/problems/best-time-to-buy-and-sell-stock-with-transaction-fee/description/) + +```html +Input: prices = [1, 3, 2, 8, 4, 9], fee = 2 +Output: 8 +Explanation: The maximum profit can be achieved by: +Buying at prices[0] = 1 +Selling at prices[3] = 8 +Buying at prices[4] = 4 +Selling at prices[5] = 9 +The total profit is ((8 - 1) - 2) + ((9 - 4) - 2) = 8. +``` + +题目描述:每交易一次,都要支付一定的费用。 + +

+ +```java +public int maxProfit(int[] prices, int fee) { + int N = prices.length; + int[] buy = new int[N]; + int[] s1 = new int[N]; + int[] sell = new int[N]; + int[] s2 = new int[N]; + s1[0] = buy[0] = -prices[0]; + sell[0] = s2[0] = 0; + for (int i = 1; i < N; i++) { + buy[i] = Math.max(sell[i - 1], s2[i - 1]) - prices[i]; + s1[i] = Math.max(buy[i - 1], s1[i - 1]); + sell[i] = Math.max(buy[i - 1], s1[i - 1]) - fee + prices[i]; + s2[i] = Math.max(s2[i - 1], sell[i - 1]); + } + return Math.max(sell[N - 1], s2[N - 1]); +} +``` + + +## 只能进行两次的股票交易 + +[123. Best Time to Buy and Sell Stock III (Hard)](https://leetcode.com/problems/best-time-to-buy-and-sell-stock-iii/description/) + +```java +public int maxProfit(int[] prices) { + int firstBuy = Integer.MIN_VALUE, firstSell = 0; + int secondBuy = Integer.MIN_VALUE, secondSell = 0; + for (int curPrice : prices) { + if (firstBuy < -curPrice) { + firstBuy = -curPrice; + } + if (firstSell < firstBuy + curPrice) { + firstSell = firstBuy + curPrice; + } + if (secondBuy < firstSell - curPrice) { + secondBuy = firstSell - curPrice; + } + if (secondSell < secondBuy + curPrice) { + secondSell = secondBuy + curPrice; + } + } + return secondSell; +} +``` + +## 只能进行 k 次的股票交易 + +[188. Best Time to Buy and Sell Stock IV (Hard)](https://leetcode.com/problems/best-time-to-buy-and-sell-stock-iv/description/) + +```java +public int maxProfit(int k, int[] prices) { + int n = prices.length; + if (k >= n / 2) { // 这种情况下该问题退化为普通的股票交易问题 + int maxProfit = 0; + for (int i = 1; i < n; i++) { + if (prices[i] > prices[i - 1]) { + maxProfit += prices[i] - prices[i - 1]; + } + } + return maxProfit; + } + int[][] maxProfit = new int[k + 1][n]; + for (int i = 1; i <= k; i++) { + int localMax = maxProfit[i - 1][0] - prices[0]; + for (int j = 1; j < n; j++) { + maxProfit[i][j] = Math.max(maxProfit[i][j - 1], prices[j] + localMax); + localMax = Math.max(localMax, maxProfit[i - 1][j] - prices[j]); + } + } + return maxProfit[k][n - 1]; +} +``` + +# 字符串编辑 + +## 删除两个字符串的字符使它们相等 + +[583. Delete Operation for Two Strings (Medium)](https://leetcode.com/problems/delete-operation-for-two-strings/description/) + +```html +Input: "sea", "eat" +Output: 2 +Explanation: You need one step to make "sea" to "ea" and another step to make "eat" to "ea". +``` + +可以转换为求两个字符串的最长公共子序列问题。 + +```java +public int minDistance(String word1, String word2) { + int m = word1.length(), n = word2.length(); + int[][] dp = new int[m + 1][n + 1]; + for (int i = 1; i <= m; i++) { + for (int j = 1; j <= n; j++) { + if (word1.charAt(i - 1) == word2.charAt(j - 1)) { + dp[i][j] = dp[i - 1][j - 1] + 1; + } else { + dp[i][j] = Math.max(dp[i][j - 1], dp[i - 1][j]); + } + } + } + return m + n - 2 * dp[m][n]; +} +``` + +## 编辑距离 + +[72. Edit Distance (Hard)](https://leetcode.com/problems/edit-distance/description/) + +```html +Example 1: + +Input: word1 = "horse", word2 = "ros" +Output: 3 +Explanation: +horse -> rorse (replace 'h' with 'r') +rorse -> rose (remove 'r') +rose -> ros (remove 'e') +Example 2: + +Input: word1 = "intention", word2 = "execution" +Output: 5 +Explanation: +intention -> inention (remove 't') +inention -> enention (replace 'i' with 'e') +enention -> exention (replace 'n' with 'x') +exention -> exection (replace 'n' with 'c') +exection -> execution (insert 'u') +``` + +题目描述:修改一个字符串成为另一个字符串,使得修改次数最少。一次修改操作包括:插入一个字符、删除一个字符、替换一个字符。 + +```java +public int minDistance(String word1, String word2) { + if (word1 == null || word2 == null) { + return 0; + } + int m = word1.length(), n = word2.length(); + int[][] dp = new int[m + 1][n + 1]; + for (int i = 1; i <= m; i++) { + dp[i][0] = i; + } + for (int i = 1; i <= n; i++) { + dp[0][i] = i; + } + for (int i = 1; i <= m; i++) { + for (int j = 1; j <= n; j++) { + if (word1.charAt(i - 1) == word2.charAt(j - 1)) { + dp[i][j] = dp[i - 1][j - 1]; + } else { + dp[i][j] = Math.min(dp[i - 1][j - 1], Math.min(dp[i][j - 1], dp[i - 1][j])) + 1; + } + } + } + return dp[m][n]; +} +``` + +## 复制粘贴字符 + +[650. 2 Keys Keyboard (Medium)](https://leetcode.com/problems/2-keys-keyboard/description/) + +题目描述:最开始只有一个字符 A,问需要多少次操作能够得到 n 个字符 A,每次操作可以复制当前所有的字符,或者粘贴。 + +``` +Input: 3 +Output: 3 +Explanation: +Intitally, we have one character 'A'. +In step 1, we use Copy All operation. +In step 2, we use Paste operation to get 'AA'. +In step 3, we use Paste operation to get 'AAA'. +``` + +```java +public int minSteps(int n) { + if (n == 1) return 0; + for (int i = 2; i <= Math.sqrt(n); i++) { + if (n % i == 0) return i + minSteps(n / i); + } + return n; +} +``` + +```java +public int minSteps(int n) { + int[] dp = new int[n + 1]; + int h = (int) Math.sqrt(n); + for (int i = 2; i <= n; i++) { + dp[i] = i; + for (int j = 2; j <= h; j++) { + if (i % j == 0) { + dp[i] = dp[j] + dp[i / j]; + break; + } + } + } + return dp[n]; +} +``` diff --git a/docs/notes/Leetcode 题解 - 双指针.md b/docs/notes/Leetcode 题解 - 双指针.md new file mode 100644 index 00000000..343bc680 --- /dev/null +++ b/docs/notes/Leetcode 题解 - 双指针.md @@ -0,0 +1,239 @@ +* [点击阅读面试进阶指南 ](https://github.com/CyC2018/Backend-Interview-Guide) + +* [有序数组的 Two Sum](#有序数组的-two-sum) +* [两数平方和](#两数平方和) +* [反转字符串中的元音字符](#反转字符串中的元音字符) +* [回文字符串](#回文字符串) +* [归并两个有序数组](#归并两个有序数组) +* [判断链表是否存在环](#判断链表是否存在环) +* [最长子序列](#最长子序列) + + + +双指针主要用于遍历数组,两个指针指向不同的元素,从而协同完成任务。 + +# 有序数组的 Two Sum + +[Leetcode :167. Two Sum II - Input array is sorted (Easy)](https://leetcode.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 变大一些。 + +```java +public int[] twoSum(int[] numbers, int target) { + 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; +} +``` + +# 两数平方和 + +[633. Sum of Square Numbers (Easy)](https://leetcode.com/problems/sum-of-square-numbers/description/) + +```html +Input: 5 +Output: True +Explanation: 1 * 1 + 2 * 2 = 5 +``` + +题目描述:判断一个数是否为两个数的平方和。 + +```java +public boolean judgeSquareSum(int c) { + int i = 0, j = (int) Math.sqrt(c); + while (i <= j) { + int powSum = i * i + j * j; + if (powSum == c) { + return true; + } else if (powSum > c) { + j--; + } else { + i++; + } + } + return false; +} +``` + +# 反转字符串中的元音字符 + +[345. Reverse Vowels of a String (Easy)](https://leetcode.com/problems/reverse-vowels-of-a-string/description/) + +```html +Given s = "leetcode", return "leotcede". +``` + +使用双指针指向待反转的两个元音字符,一个指针从头向尾遍历,一个指针从尾到头遍历。 + +```java +private final static HashSet vowels = new HashSet<>(Arrays.asList('a', 'e', 'i', 'o', 'u', 'A', 'E', 'I', 'O', 'U')); + +public String reverseVowels(String s) { + 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); +} +``` + +# 回文字符串 + +[680. Valid Palindrome II (Easy)](https://leetcode.com/problems/valid-palindrome-ii/description/) + +```html +Input: "abca" +Output: True +Explanation: You could delete the character 'c'. +``` + +题目描述:可以删除一个字符,判断是否能构成回文字符串。 + +```java +public boolean validPalindrome(String s) { + int i = -1, j = s.length(); + while (++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; +} +``` + +# 归并两个有序数组 + +[88. Merge Sorted Array (Easy)](https://leetcode.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--]; + } + } +} +``` + +# 判断链表是否存在环 + +[141. Linked List Cycle (Easy)](https://leetcode.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; +} +``` + +# 最长子序列 + +[524. Longest Word in Dictionary through Deleting (Medium)](https://leetcode.com/problems/longest-word-in-dictionary-through-deleting/description/) + +``` +Input: +s = "abpcplea", d = ["ale","apple","monkey","plea"] + +Output: +"apple" +``` + +题目描述:删除 s 中的一些字符,使得它构成字符串列表 d 中的一个字符串,找出能构成的最长字符串。如果有多个相同长度的结果,返回字典序的最小字符串。 + +```java +public String findLongestWord(String s, List 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 (isValid(s, target)) { + longestWord = target; + } + } + return longestWord; +} + +private boolean isValid(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(); +} +``` diff --git a/docs/notes/Leetcode 题解 - 哈希表.md b/docs/notes/Leetcode 题解 - 哈希表.md new file mode 100644 index 00000000..63788a8f --- /dev/null +++ b/docs/notes/Leetcode 题解 - 哈希表.md @@ -0,0 +1,124 @@ +* [点击阅读面试进阶指南 ](https://github.com/CyC2018/Backend-Interview-Guide) + +* [数组中两个数的和为给定值](#数组中两个数的和为给定值) +* [判断数组是否含有重复元素](#判断数组是否含有重复元素) +* [最长和谐序列](#最长和谐序列) +* [最长连续序列](#最长连续序列) + + + +哈希表使用 O(N) 空间复杂度存储数据,并且以 O(1) 时间复杂度求解问题。 + +- Java 中的 **HashSet** 用于存储一个集合,可以查找元素是否在集合中。如果元素有穷,并且范围不大,那么可以用一个布尔数组来存储一个元素是否存在。例如对于只有小写字符的元素,就可以用一个长度为 26 的布尔数组来存储一个字符集合,使得空间复杂度降低为 O(1)。 + +- Java 中的 **HashMap** 主要用于映射关系,从而把两个元素联系起来。HashMap 也可以用来对元素进行计数统计,此时键为元素,值为计数。和 HashSet 类似,如果元素有穷并且范围不大,可以用整型数组来进行统计。在对一个内容进行压缩或者其它转换时,利用 HashMap 可以把原始内容和转换后的内容联系起来。例如在一个简化 url 的系统中 [Leetcdoe : 535. Encode and Decode TinyURL (Medium)](https://leetcode.com/problems/encode-and-decode-tinyurl/description/),利用 HashMap 就可以存储精简后的 url 到原始 url 的映射,使得不仅可以显示简化的 url,也可以根据简化的 url 得到原始 url 从而定位到正确的资源。 + + +# 数组中两个数的和为给定值 + +[1. Two Sum (Easy)](https://leetcode.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 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; +} +``` + +# 判断数组是否含有重复元素 + +[217. Contains Duplicate (Easy)](https://leetcode.com/problems/contains-duplicate/description/) + +```java +public boolean containsDuplicate(int[] nums) { + Set set = new HashSet<>(); + for (int num : nums) { + set.add(num); + } + return set.size() < nums.length; +} +``` + +# 最长和谐序列 + +[594. Longest Harmonious Subsequence (Easy)](https://leetcode.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 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; +} +``` + +# 最长连续序列 + +[128. Longest Consecutive Sequence (Hard)](https://leetcode.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 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 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 countForNum) { + int max = 0; + for (int num : countForNum.keySet()) { + max = Math.max(max, countForNum.get(num)); + } + return max; +} +``` diff --git a/docs/notes/Leetcode 题解 - 图.md b/docs/notes/Leetcode 题解 - 图.md new file mode 100644 index 00000000..d275a757 --- /dev/null +++ b/docs/notes/Leetcode 题解 - 图.md @@ -0,0 +1,258 @@ +* [点击阅读面试进阶指南 ](https://github.com/CyC2018/Backend-Interview-Guide) + +* [二分图](#二分图) + * [判断是否为二分图](#判断是否为二分图) +* [拓扑排序](#拓扑排序) + * [课程安排的合法性](#课程安排的合法性) + * [课程安排的顺序](#课程安排的顺序) +* [并查集](#并查集) + * [冗余连接](#冗余连接) + + + +# 二分图 + +如果可以用两种颜色对图中的节点进行着色,并且保证相邻的节点颜色不同,那么这个图就是二分图。 + +## 判断是否为二分图 + +[785. Is Graph Bipartite? (Medium)](https://leetcode.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; +} +``` + +# 拓扑排序 + +常用于在具有先序关系的任务规划中。 + +## 课程安排的合法性 + +[207. Course Schedule (Medium)](https://leetcode.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[] 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[] 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; +} +``` + +## 课程安排的顺序 + +[210. Course Schedule II (Medium)](https://leetcode.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[] 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 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[] graphic, + int curNode, Stack 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; +} +``` + +# 并查集 + +并查集可以动态地连通两个点,并且可以非常快速地判断两个点是否连通。 + +## 冗余连接 + +[684. Redundant Connection (Medium)](https://leetcode.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); + } +} +``` diff --git a/docs/notes/Leetcode 题解 - 字符串.md b/docs/notes/Leetcode 题解 - 字符串.md new file mode 100644 index 00000000..9489116b --- /dev/null +++ b/docs/notes/Leetcode 题解 - 字符串.md @@ -0,0 +1,226 @@ +* [点击阅读面试进阶指南 ](https://github.com/CyC2018/Backend-Interview-Guide) + +* [字符串循环移位包含](#字符串循环移位包含) +* [字符串循环移位](#字符串循环移位) +* [字符串中单词的翻转](#字符串中单词的翻转) +* [两个字符串包含的字符是否完全相同](#两个字符串包含的字符是否完全相同) +* [计算一组字符集合可以组成的回文字符串的最大长度](#计算一组字符集合可以组成的回文字符串的最大长度) +* [字符串同构](#字符串同构) +* [回文子字符串个数](#回文子字符串个数) +* [判断一个整数是否是回文数](#判断一个整数是否是回文数) +* [统计二进制字符串中连续 1 和连续 0 数量相同的子字符串个数](#统计二进制字符串中连续-1-和连续-0-数量相同的子字符串个数) + + + +# 字符串循环移位包含 + +[编程之美 3.1](#) + +```html +s1 = AABCD, s2 = CDAA +Return : true +``` + +给定两个字符串 s1 和 s2,要求判定 s2 是否能够被 s1 做循环移位得到的字符串包含。 + +s1 进行循环移位的结果是 s1s1 的子字符串,因此只要判断 s2 是否是 s1s1 的子字符串即可。 + +# 字符串循环移位 + +[编程之美 2.17](#) + +```html +s = "abcd123" k = 3 +Return "123abcd" +``` + +将字符串向右循环移动 k 位。 + +将 abcd123 中的 abcd 和 123 单独翻转,得到 dcba321,然后对整个字符串进行翻转,得到 123abcd。 + +# 字符串中单词的翻转 + +[程序员代码面试指南](#) + +```html +s = "I am a student" +Return "student a am I" +``` + +将每个单词翻转,然后将整个字符串翻转。 + +# 两个字符串包含的字符是否完全相同 + +[242. Valid Anagram (Easy)](https://leetcode.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; +} +``` + +# 计算一组字符集合可以组成的回文字符串的最大长度 + +[409. Longest Palindrome (Easy)](https://leetcode.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; +} +``` + +# 字符串同构 + +[205. Isomorphic Strings (Easy)](https://leetcode.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; +} +``` + +# 回文子字符串个数 + +[647. Palindromic Substrings (Medium)](https://leetcode.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++; + } +} +``` + +# 判断一个整数是否是回文数 + +[9. Palindrome Number (Easy)](https://leetcode.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; +} +``` + +# 统计二进制字符串中连续 1 和连续 0 数量相同的子字符串个数 + +[696. Count Binary Substrings (Easy)](https://leetcode.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; +} +``` diff --git a/docs/notes/Leetcode 题解 - 排序.md b/docs/notes/Leetcode 题解 - 排序.md new file mode 100644 index 00000000..b9ef2451 --- /dev/null +++ b/docs/notes/Leetcode 题解 - 排序.md @@ -0,0 +1,227 @@ +* [点击阅读面试进阶指南 ](https://github.com/CyC2018/Backend-Interview-Guide) + +* [快速选择](#快速选择) +* [堆排序](#堆排序) + * [Kth Element](#kth-element) +* [桶排序](#桶排序) + * [出现频率最多的 k 个数](#出现频率最多的-k-个数) + * [按照字符出现次数对字符串排序](#按照字符出现次数对字符串排序) +* [荷兰国旗问题](#荷兰国旗问题) + * [按颜色进行排序](#按颜色进行排序) + + + +# 快速选择 + +用于求解 **Kth Element** 问题,使用快速排序的 partition() 进行实现。 + +需要先打乱数组,否则最坏情况下时间复杂度为 O(N2)。 + +# 堆排序 + +用于求解 **TopK Elements** 问题,通过维护一个大小为 K 的堆,堆中的元素就是 TopK Elements。 + +堆排序也可以用于求解 Kth Element 问题,堆顶元素就是 Kth Element。 + +快速选择也可以求解 TopK Elements 问题,因为找到 Kth Element 之后,再遍历一次数组,所有小于等于 Kth Element 的元素都是 TopK Elements。 + +可以看到,快速选择和堆排序都可以求解 Kth Element 和 TopK Elements 问题。 + +## Kth Element + +[215. Kth Largest Element in an Array (Medium)](https://leetcode.com/problems/kth-largest-element-in-an-array/description/) + +题目描述:找到第 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 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; +} +``` + +# 桶排序 + +## 出现频率最多的 k 个数 + +[347. Top K Frequent Elements (Medium)](https://leetcode.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 List topKFrequent(int[] nums, int k) { + Map frequencyForNum = new HashMap<>(); + for (int num : nums) { + frequencyForNum.put(num, frequencyForNum.getOrDefault(num, 0) + 1); + } + List[] 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 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())); + } + } + return topK; +} +``` + +## 按照字符出现次数对字符串排序 + +[451. Sort Characters By Frequency (Medium)](https://leetcode.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 frequencyForNum = new HashMap<>(); + for (char c : s.toCharArray()) + frequencyForNum.put(c, frequencyForNum.getOrDefault(c, 0) + 1); + + List[] 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(); +} +``` + +# 荷兰国旗问题 + +荷兰国旗包含三种颜色:红、白、蓝。 + +有三种颜色的球,算法的目标是将这三种球按颜色顺序正确地排列。 + +它其实是三向切分快速排序的一种变种,在三向切分快速排序中,每次切分都将数组分成三个区间:小于切分元素、等于切分元素、大于切分元素,而该算法是将数组分成三个区间:等于红色、等于白色、等于蓝色。 + +

+ +## 按颜色进行排序 + +[75. Sort Colors (Medium)](https://leetcode.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; +} +``` diff --git a/docs/notes/Leetcode 题解 - 搜索.md b/docs/notes/Leetcode 题解 - 搜索.md new file mode 100644 index 00000000..b12e6e6a --- /dev/null +++ b/docs/notes/Leetcode 题解 - 搜索.md @@ -0,0 +1,1269 @@ +* [点击阅读面试进阶指南 ](https://github.com/CyC2018/Backend-Interview-Guide) + +* [BFS](#bfs) + * [计算在网格中从原点到特定点的最短路径长度](#计算在网格中从原点到特定点的最短路径长度) + * [组成整数的最小平方数数量](#组成整数的最小平方数数量) + * [最短单词路径](#最短单词路径) +* [DFS](#dfs) + * [查找最大的连通面积](#查找最大的连通面积) + * [矩阵中的连通分量数目](#矩阵中的连通分量数目) + * [好友关系的连通分量数目](#好友关系的连通分量数目) + * [填充封闭区域](#填充封闭区域) + * [能到达的太平洋和大西洋的区域](#能到达的太平洋和大西洋的区域) +* [Backtracking](#backtracking) + * [数字键盘组合](#数字键盘组合) + * [IP 地址划分](#ip-地址划分) + * [在矩阵中寻找字符串](#在矩阵中寻找字符串) + * [输出二叉树中所有从根到叶子的路径](#输出二叉树中所有从根到叶子的路径) + * [排列](#排列) + * [含有相同元素求排列](#含有相同元素求排列) + * [组合](#组合) + * [组合求和](#组合求和) + * [含有相同元素的求组合求和](#含有相同元素的求组合求和) + * [1-9 数字的组合求和](#1-9-数字的组合求和) + * [子集](#子集) + * [含有相同元素求子集](#含有相同元素求子集) + * [分割字符串使得每个部分都是回文数](#分割字符串使得每个部分都是回文数) + * [数独](#数独) + * [N 皇后](#n-皇后) + + + +深度优先搜索和广度优先搜索广泛运用于树和图中,但是它们的应用远远不止如此。 + +# BFS + +

+ +广度优先搜索一层一层地进行遍历,每层遍历都以上一层遍历的结果作为起点,遍历一个距离能访问到的所有节点。需要注意的是,遍历过的节点不能再次被遍历。 + +第一层: + +- 0 -> {6,2,1,5} + +第二层: + +- 6 -> {4} +- 2 -> {} +- 1 -> {} +- 5 -> {3} + +第三层: + +- 4 -> {} +- 3 -> {} + +每一层遍历的节点都与根节点距离相同。设 di 表示第 i 个节点与根节点的距离,推导出一个结论:对于先遍历的节点 i 与后遍历的节点 j,有 di <= dj。利用这个结论,可以求解最短路径等 **最优解** 问题:第一次遍历到目的节点,其所经过的路径为最短路径。应该注意的是,使用 BFS 只能求解无权图的最短路径。 + +在程序实现 BFS 时需要考虑以下问题: + +- 队列:用来存储每一轮遍历得到的节点; +- 标记:对于遍历过的节点,应该将它标记,防止重复遍历。 + +## 计算在网格中从原点到特定点的最短路径长度 + +```html +[[1,1,0,1], + [1,0,1,0], + [1,1,1,1], + [1,0,1,1]] +``` + +1 表示可以经过某个位置,求解从 (0, 0) 位置到 (tr, tc) 位置的最短路径长度。 + +```java +public int minPathLength(int[][] grids, int tr, int tc) { + final int[][] direction = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}}; + final int m = grids.length, n = grids[0].length; + Queue> queue = new LinkedList<>(); + queue.add(new Pair<>(0, 0)); + int pathLength = 0; + while (!queue.isEmpty()) { + int size = queue.size(); + pathLength++; + while (size-- > 0) { + Pair cur = queue.poll(); + int cr = cur.getKey(), cc = cur.getValue(); + grids[cr][cc] = 0; // 标记 + for (int[] d : direction) { + int nr = cr + d[0], nc = cc + d[1]; + if (nr < 0 || nr >= m || nc < 0 || nc >= n || grids[nr][nc] == 0) { + continue; + } + if (nr == tr && nc == tc) { + return pathLength; + } + queue.add(new Pair<>(nr, nc)); + } + } + } + return -1; +} +``` + +## 组成整数的最小平方数数量 + +[279. Perfect Squares (Medium)](https://leetcode.com/problems/perfect-squares/description/) + +```html +For example, given n = 12, return 3 because 12 = 4 + 4 + 4; given n = 13, return 2 because 13 = 4 + 9. +``` + +可以将每个整数看成图中的一个节点,如果两个整数之差为一个平方数,那么这两个整数所在的节点就有一条边。 + +要求解最小的平方数数量,就是求解从节点 n 到节点 0 的最短路径。 + +本题也可以用动态规划求解,在之后动态规划部分中会再次出现。 + +```java +public int numSquares(int n) { + List squares = generateSquares(n); + Queue queue = new LinkedList<>(); + boolean[] marked = new boolean[n + 1]; + queue.add(n); + marked[n] = true; + int level = 0; + while (!queue.isEmpty()) { + int size = queue.size(); + level++; + while (size-- > 0) { + int cur = queue.poll(); + for (int s : squares) { + int next = cur - s; + if (next < 0) { + break; + } + if (next == 0) { + return level; + } + if (marked[next]) { + continue; + } + marked[next] = true; + queue.add(next); + } + } + } + return n; +} + +/** + * 生成小于 n 的平方数序列 + * @return 1,4,9,... + */ +private List generateSquares(int n) { + List squares = new ArrayList<>(); + int square = 1; + int diff = 3; + while (square <= n) { + squares.add(square); + square += diff; + diff += 2; + } + return squares; +} +``` + +## 最短单词路径 + +[127. Word Ladder (Medium)](https://leetcode.com/problems/word-ladder/description/) + +```html +Input: +beginWord = "hit", +endWord = "cog", +wordList = ["hot","dot","dog","lot","log","cog"] + +Output: 5 + +Explanation: As one shortest transformation is "hit" -> "hot" -> "dot" -> "dog" -> "cog", +return its length 5. +``` + +```html +Input: +beginWord = "hit" +endWord = "cog" +wordList = ["hot","dot","dog","lot","log"] + +Output: 0 + +Explanation: The endWord "cog" is not in wordList, therefore no possible transformation. +``` + +题目描述:找出一条从 beginWord 到 endWord 的最短路径,每次移动规定为改变一个字符,并且改变之后的字符串必须在 wordList 中。 + +```java +public int ladderLength(String beginWord, String endWord, List wordList) { + wordList.add(beginWord); + int N = wordList.size(); + int start = N - 1; + int end = 0; + while (end < N && !wordList.get(end).equals(endWord)) { + end++; + } + if (end == N) { + return 0; + } + List[] graphic = buildGraphic(wordList); + return getShortestPath(graphic, start, end); +} + +private List[] buildGraphic(List wordList) { + int N = wordList.size(); + List[] graphic = new List[N]; + for (int i = 0; i < N; i++) { + graphic[i] = new ArrayList<>(); + for (int j = 0; j < N; j++) { + if (isConnect(wordList.get(i), wordList.get(j))) { + graphic[i].add(j); + } + } + } + return graphic; +} + +private boolean isConnect(String s1, String s2) { + int diffCnt = 0; + for (int i = 0; i < s1.length() && diffCnt <= 1; i++) { + if (s1.charAt(i) != s2.charAt(i)) { + diffCnt++; + } + } + return diffCnt == 1; +} + +private int getShortestPath(List[] graphic, int start, int end) { + Queue queue = new LinkedList<>(); + boolean[] marked = new boolean[graphic.length]; + queue.add(start); + marked[start] = true; + int path = 1; + while (!queue.isEmpty()) { + int size = queue.size(); + path++; + while (size-- > 0) { + int cur = queue.poll(); + for (int next : graphic[cur]) { + if (next == end) { + return path; + } + if (marked[next]) { + continue; + } + marked[next] = true; + queue.add(next); + } + } + } + return 0; +} +``` + +# DFS + +

+ +广度优先搜索一层一层遍历,每一层得到的所有新节点,要用队列存储起来以备下一层遍历的时候再遍历。 + +而深度优先搜索在得到一个新节点时立即对新节点进行遍历:从节点 0 出发开始遍历,得到到新节点 6 时,立马对新节点 6 进行遍历,得到新节点 4;如此反复以这种方式遍历新节点,直到没有新节点了,此时返回。返回到根节点 0 的情况是,继续对根节点 0 进行遍历,得到新节点 2,然后继续以上步骤。 + +从一个节点出发,使用 DFS 对一个图进行遍历时,能够遍历到的节点都是从初始节点可达的,DFS 常用来求解这种 **可达性** 问题。 + +在程序实现 DFS 时需要考虑以下问题: + +- 栈:用栈来保存当前节点信息,当遍历新节点返回时能够继续遍历当前节点。可以使用递归栈。 +- 标记:和 BFS 一样同样需要对已经遍历过的节点进行标记。 + +## 查找最大的连通面积 + +[695. Max Area of Island (Easy)](https://leetcode.com/problems/max-area-of-island/description/) + +```html +[[0,0,1,0,0,0,0,1,0,0,0,0,0], + [0,0,0,0,0,0,0,1,1,1,0,0,0], + [0,1,1,0,1,0,0,0,0,0,0,0,0], + [0,1,0,0,1,1,0,0,1,0,1,0,0], + [0,1,0,0,1,1,0,0,1,1,1,0,0], + [0,0,0,0,0,0,0,0,0,0,1,0,0], + [0,0,0,0,0,0,0,1,1,1,0,0,0], + [0,0,0,0,0,0,0,1,1,0,0,0,0]] +``` + +```java +private int m, n; +private int[][] direction = {{0, 1}, {0, -1}, {1, 0}, {-1, 0}}; + +public int maxAreaOfIsland(int[][] grid) { + if (grid == null || grid.length == 0) { + return 0; + } + m = grid.length; + n = grid[0].length; + int maxArea = 0; + for (int i = 0; i < m; i++) { + for (int j = 0; j < n; j++) { + maxArea = Math.max(maxArea, dfs(grid, i, j)); + } + } + return maxArea; +} + +private int dfs(int[][] grid, int r, int c) { + if (r < 0 || r >= m || c < 0 || c >= n || grid[r][c] == 0) { + return 0; + } + grid[r][c] = 0; + int area = 1; + for (int[] d : direction) { + area += dfs(grid, r + d[0], c + d[1]); + } + return area; +} +``` + +## 矩阵中的连通分量数目 + +[200. Number of Islands (Medium)](https://leetcode.com/problems/number-of-islands/description/) + +```html +Input: +11000 +11000 +00100 +00011 + +Output: 3 +``` + +可以将矩阵表示看成一张有向图。 + +```java +private int m, n; +private int[][] direction = {{0, 1}, {0, -1}, {1, 0}, {-1, 0}}; + +public int numIslands(char[][] grid) { + if (grid == null || grid.length == 0) { + return 0; + } + m = grid.length; + n = grid[0].length; + int islandsNum = 0; + for (int i = 0; i < m; i++) { + for (int j = 0; j < n; j++) { + if (grid[i][j] != '0') { + dfs(grid, i, j); + islandsNum++; + } + } + } + return islandsNum; +} + +private void dfs(char[][] grid, int i, int j) { + if (i < 0 || i >= m || j < 0 || j >= n || grid[i][j] == '0') { + return; + } + grid[i][j] = '0'; + for (int[] d : direction) { + dfs(grid, i + d[0], j + d[1]); + } +} +``` + +## 好友关系的连通分量数目 + +[547. Friend Circles (Medium)](https://leetcode.com/problems/friend-circles/description/) + +```html +Input: +[[1,1,0], + [1,1,0], + [0,0,1]] + +Output: 2 + +Explanation:The 0th and 1st students are direct friends, so they are in a friend circle. +The 2nd student himself is in a friend circle. So return 2. +``` + +题目描述:好友关系可以看成是一个无向图,例如第 0 个人与第 1 个人是好友,那么 M[0][1] 和 M[1][0] 的值都为 1。 + +```java +private int n; + +public int findCircleNum(int[][] M) { + n = M.length; + int circleNum = 0; + boolean[] hasVisited = new boolean[n]; + for (int i = 0; i < n; i++) { + if (!hasVisited[i]) { + dfs(M, i, hasVisited); + circleNum++; + } + } + return circleNum; +} + +private void dfs(int[][] M, int i, boolean[] hasVisited) { + hasVisited[i] = true; + for (int k = 0; k < n; k++) { + if (M[i][k] == 1 && !hasVisited[k]) { + dfs(M, k, hasVisited); + } + } +} +``` + +## 填充封闭区域 + +[130. Surrounded Regions (Medium)](https://leetcode.com/problems/surrounded-regions/description/) + +```html +For example, +X X X X +X O O X +X X O X +X O X X + +After running your function, the board should be: +X X X X +X X X X +X X X X +X O X X +``` + +题目描述:使被 'X' 包围的 'O' 转换为 'X'。 + +先填充最外侧,剩下的就是里侧了。 + +```java +private int[][] direction = {{0, 1}, {0, -1}, {1, 0}, {-1, 0}}; +private int m, n; + +public void solve(char[][] board) { + if (board == null || board.length == 0) { + return; + } + + m = board.length; + n = board[0].length; + + for (int i = 0; i < m; i++) { + dfs(board, i, 0); + dfs(board, i, n - 1); + } + for (int i = 0; i < n; i++) { + dfs(board, 0, i); + dfs(board, m - 1, i); + } + + for (int i = 0; i < m; i++) { + for (int j = 0; j < n; j++) { + if (board[i][j] == 'T') { + board[i][j] = 'O'; + } else if (board[i][j] == 'O') { + board[i][j] = 'X'; + } + } + } +} + +private void dfs(char[][] board, int r, int c) { + if (r < 0 || r >= m || c < 0 || c >= n || board[r][c] != 'O') { + return; + } + board[r][c] = 'T'; + for (int[] d : direction) { + dfs(board, r + d[0], c + d[1]); + } +} +``` + +## 能到达的太平洋和大西洋的区域 + +[417. Pacific Atlantic Water Flow (Medium)](https://leetcode.com/problems/pacific-atlantic-water-flow/description/) + +```html +Given the following 5x5 matrix: + + Pacific ~ ~ ~ ~ ~ + ~ 1 2 2 3 (5) * + ~ 3 2 3 (4) (4) * + ~ 2 4 (5) 3 1 * + ~ (6) (7) 1 4 5 * + ~ (5) 1 1 2 4 * + * * * * * Atlantic + +Return: +[[0, 4], [1, 3], [1, 4], [2, 2], [3, 0], [3, 1], [4, 0]] (positions with parentheses in above matrix). +``` + +左边和上边是太平洋,右边和下边是大西洋,内部的数字代表海拔,海拔高的地方的水能够流到低的地方,求解水能够流到太平洋和大西洋的所有位置。 + +```java + +private int m, n; +private int[][] matrix; +private int[][] direction = {{0, 1}, {0, -1}, {1, 0}, {-1, 0}}; + +public List pacificAtlantic(int[][] matrix) { + List ret = new ArrayList<>(); + if (matrix == null || matrix.length == 0) { + return ret; + } + + m = matrix.length; + n = matrix[0].length; + this.matrix = matrix; + boolean[][] canReachP = new boolean[m][n]; + boolean[][] canReachA = new boolean[m][n]; + + for (int i = 0; i < m; i++) { + dfs(i, 0, canReachP); + dfs(i, n - 1, canReachA); + } + for (int i = 0; i < n; i++) { + dfs(0, i, canReachP); + dfs(m - 1, i, canReachA); + } + + for (int i = 0; i < m; i++) { + for (int j = 0; j < n; j++) { + if (canReachP[i][j] && canReachA[i][j]) { + ret.add(new int[]{i, j}); + } + } + } + + return ret; +} + +private void dfs(int r, int c, boolean[][] canReach) { + if (canReach[r][c]) { + return; + } + canReach[r][c] = true; + for (int[] d : direction) { + int nextR = d[0] + r; + int nextC = d[1] + c; + if (nextR < 0 || nextR >= m || nextC < 0 || nextC >= n + || matrix[r][c] > matrix[nextR][nextC]) { + + continue; + } + dfs(nextR, nextC, canReach); + } +} +``` + +# Backtracking + +Backtracking(回溯)属于 DFS。 + +- 普通 DFS 主要用在 **可达性问题** ,这种问题只需要执行到特点的位置然后返回即可。 +- 而 Backtracking 主要用于求解 **排列组合** 问题,例如有 { 'a','b','c' } 三个字符,求解所有由这三个字符排列得到的字符串,这种问题在执行到特定的位置返回之后还会继续执行求解过程。 + +因为 Backtracking 不是立即就返回,而要继续求解,因此在程序实现时,需要注意对元素的标记问题: + +- 在访问一个新元素进入新的递归调用时,需要将新元素标记为已经访问,这样才能在继续递归调用时不用重复访问该元素; +- 但是在递归返回时,需要将元素标记为未访问,因为只需要保证在一个递归链中不同时访问一个元素,可以访问已经访问过但是不在当前递归链中的元素。 + +## 数字键盘组合 + +[17. Letter Combinations of a Phone Number (Medium)](https://leetcode.com/problems/letter-combinations-of-a-phone-number/description/) + +

+ +```html +Input:Digit string "23" +Output: ["ad", "ae", "af", "bd", "be", "bf", "cd", "ce", "cf"]. +``` + +```java +private static final String[] KEYS = {"", "", "abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz"}; + +public List letterCombinations(String digits) { + List combinations = new ArrayList<>(); + if (digits == null || digits.length() == 0) { + return combinations; + } + doCombination(new StringBuilder(), combinations, digits); + return combinations; +} + +private void doCombination(StringBuilder prefix, List combinations, final String digits) { + if (prefix.length() == digits.length()) { + combinations.add(prefix.toString()); + return; + } + int curDigits = digits.charAt(prefix.length()) - '0'; + String letters = KEYS[curDigits]; + for (char c : letters.toCharArray()) { + prefix.append(c); // 添加 + doCombination(prefix, combinations, digits); + prefix.deleteCharAt(prefix.length() - 1); // 删除 + } +} +``` + +## IP 地址划分 + +[93. Restore IP Addresses(Medium)](https://leetcode.com/problems/restore-ip-addresses/description/) + +```html +Given "25525511135", +return ["255.255.11.135", "255.255.111.35"]. +``` + +```java +public List restoreIpAddresses(String s) { + List addresses = new ArrayList<>(); + StringBuilder tempAddress = new StringBuilder(); + doRestore(0, tempAddress, addresses, s); + return addresses; +} + +private void doRestore(int k, StringBuilder tempAddress, List addresses, String s) { + if (k == 4 || s.length() == 0) { + if (k == 4 && s.length() == 0) { + addresses.add(tempAddress.toString()); + } + return; + } + for (int i = 0; i < s.length() && i <= 2; i++) { + if (i != 0 && s.charAt(0) == '0') { + break; + } + String part = s.substring(0, i + 1); + if (Integer.valueOf(part) <= 255) { + if (tempAddress.length() != 0) { + part = "." + part; + } + tempAddress.append(part); + doRestore(k + 1, tempAddress, addresses, s.substring(i + 1)); + tempAddress.delete(tempAddress.length() - part.length(), tempAddress.length()); + } + } +} +``` + +## 在矩阵中寻找字符串 + +[79. Word Search (Medium)](https://leetcode.com/problems/word-search/description/) + +```html +For example, +Given board = +[ + ['A','B','C','E'], + ['S','F','C','S'], + ['A','D','E','E'] +] +word = "ABCCED", -> returns true, +word = "SEE", -> returns true, +word = "ABCB", -> returns false. +``` + +```java +private final static int[][] direction = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}}; +private int m; +private int n; + +public boolean exist(char[][] board, String word) { + if (word == null || word.length() == 0) { + return true; + } + if (board == null || board.length == 0 || board[0].length == 0) { + return false; + } + + m = board.length; + n = board[0].length; + boolean[][] hasVisited = new boolean[m][n]; + + for (int r = 0; r < m; r++) { + for (int c = 0; c < n; c++) { + if (backtracking(0, r, c, hasVisited, board, word)) { + return true; + } + } + } + + return false; +} + +private boolean backtracking(int curLen, int r, int c, boolean[][] visited, final char[][] board, final String word) { + if (curLen == word.length()) { + return true; + } + if (r < 0 || r >= m || c < 0 || c >= n + || board[r][c] != word.charAt(curLen) || visited[r][c]) { + + return false; + } + + visited[r][c] = true; + + for (int[] d : direction) { + if (backtracking(curLen + 1, r + d[0], c + d[1], visited, board, word)) { + return true; + } + } + + visited[r][c] = false; + + return false; +} +``` + +## 输出二叉树中所有从根到叶子的路径 + +[257. Binary Tree Paths (Easy)](https://leetcode.com/problems/binary-tree-paths/description/) + +```html + 1 + / \ +2 3 + \ + 5 +``` + +```html +["1->2->5", "1->3"] +``` + +```java + +public List binaryTreePaths(TreeNode root) { + List paths = new ArrayList<>(); + if (root == null) { + return paths; + } + List values = new ArrayList<>(); + backtracking(root, values, paths); + return paths; +} + +private void backtracking(TreeNode node, List values, List paths) { + if (node == null) { + return; + } + values.add(node.val); + if (isLeaf(node)) { + paths.add(buildPath(values)); + } else { + backtracking(node.left, values, paths); + backtracking(node.right, values, paths); + } + values.remove(values.size() - 1); +} + +private boolean isLeaf(TreeNode node) { + return node.left == null && node.right == null; +} + +private String buildPath(List values) { + StringBuilder str = new StringBuilder(); + for (int i = 0; i < values.size(); i++) { + str.append(values.get(i)); + if (i != values.size() - 1) { + str.append("->"); + } + } + return str.toString(); +} +``` + +## 排列 + +[46. Permutations (Medium)](https://leetcode.com/problems/permutations/description/) + +```html +[1,2,3] have the following permutations: +[ + [1,2,3], + [1,3,2], + [2,1,3], + [2,3,1], + [3,1,2], + [3,2,1] +] +``` + +```java +public List> permute(int[] nums) { + List> permutes = new ArrayList<>(); + List permuteList = new ArrayList<>(); + boolean[] hasVisited = new boolean[nums.length]; + backtracking(permuteList, permutes, hasVisited, nums); + return permutes; +} + +private void backtracking(List permuteList, List> permutes, boolean[] visited, final int[] nums) { + if (permuteList.size() == nums.length) { + permutes.add(new ArrayList<>(permuteList)); // 重新构造一个 List + return; + } + for (int i = 0; i < visited.length; i++) { + if (visited[i]) { + continue; + } + visited[i] = true; + permuteList.add(nums[i]); + backtracking(permuteList, permutes, visited, nums); + permuteList.remove(permuteList.size() - 1); + visited[i] = false; + } +} +``` + +## 含有相同元素求排列 + +[47. Permutations II (Medium)](https://leetcode.com/problems/permutations-ii/description/) + +```html +[1,1,2] have the following unique permutations: +[[1,1,2], [1,2,1], [2,1,1]] +``` + +数组元素可能含有相同的元素,进行排列时就有可能出现重复的排列,要求重复的排列只返回一个。 + +在实现上,和 Permutations 不同的是要先排序,然后在添加一个元素时,判断这个元素是否等于前一个元素,如果等于,并且前一个元素还未访问,那么就跳过这个元素。 + +```java +public List> permuteUnique(int[] nums) { + List> permutes = new ArrayList<>(); + List permuteList = new ArrayList<>(); + Arrays.sort(nums); // 排序 + boolean[] hasVisited = new boolean[nums.length]; + backtracking(permuteList, permutes, hasVisited, nums); + return permutes; +} + +private void backtracking(List permuteList, List> permutes, boolean[] visited, final int[] nums) { + if (permuteList.size() == nums.length) { + permutes.add(new ArrayList<>(permuteList)); + return; + } + + for (int i = 0; i < visited.length; i++) { + if (i != 0 && nums[i] == nums[i - 1] && !visited[i - 1]) { + continue; // 防止重复 + } + if (visited[i]){ + continue; + } + visited[i] = true; + permuteList.add(nums[i]); + backtracking(permuteList, permutes, visited, nums); + permuteList.remove(permuteList.size() - 1); + visited[i] = false; + } +} +``` + +## 组合 + +[77. Combinations (Medium)](https://leetcode.com/problems/combinations/description/) + +```html +If n = 4 and k = 2, a solution is: +[ + [2,4], + [3,4], + [2,3], + [1,2], + [1,3], + [1,4], +] +``` + +```java +public List> combine(int n, int k) { + List> combinations = new ArrayList<>(); + List combineList = new ArrayList<>(); + backtracking(combineList, combinations, 1, k, n); + return combinations; +} + +private void backtracking(List combineList, List> combinations, int start, int k, final int n) { + if (k == 0) { + combinations.add(new ArrayList<>(combineList)); + return; + } + for (int i = start; i <= n - k + 1; i++) { // 剪枝 + combineList.add(i); + backtracking(combineList, combinations, i + 1, k - 1, n); + combineList.remove(combineList.size() - 1); + } +} +``` + +## 组合求和 + +[39. Combination Sum (Medium)](https://leetcode.com/problems/combination-sum/description/) + +```html +given candidate set [2, 3, 6, 7] and target 7, +A solution set is: +[[7],[2, 2, 3]] +``` + +```java +public List> combinationSum(int[] candidates, int target) { + List> combinations = new ArrayList<>(); + backtracking(new ArrayList<>(), combinations, 0, target, candidates); + return combinations; +} + +private void backtracking(List tempCombination, List> combinations, + int start, int target, final int[] candidates) { + + if (target == 0) { + combinations.add(new ArrayList<>(tempCombination)); + return; + } + for (int i = start; i < candidates.length; i++) { + if (candidates[i] <= target) { + tempCombination.add(candidates[i]); + backtracking(tempCombination, combinations, i, target - candidates[i], candidates); + tempCombination.remove(tempCombination.size() - 1); + } + } +} +``` + +## 含有相同元素的求组合求和 + +[40. Combination Sum II (Medium)](https://leetcode.com/problems/combination-sum-ii/description/) + +```html +For example, given candidate set [10, 1, 2, 7, 6, 1, 5] and target 8, +A solution set is: +[ + [1, 7], + [1, 2, 5], + [2, 6], + [1, 1, 6] +] +``` + +```java +public List> combinationSum2(int[] candidates, int target) { + List> combinations = new ArrayList<>(); + Arrays.sort(candidates); + backtracking(new ArrayList<>(), combinations, new boolean[candidates.length], 0, target, candidates); + return combinations; +} + +private void backtracking(List tempCombination, List> combinations, + boolean[] hasVisited, int start, int target, final int[] candidates) { + + if (target == 0) { + combinations.add(new ArrayList<>(tempCombination)); + return; + } + for (int i = start; i < candidates.length; i++) { + if (i != 0 && candidates[i] == candidates[i - 1] && !hasVisited[i - 1]) { + continue; + } + if (candidates[i] <= target) { + tempCombination.add(candidates[i]); + hasVisited[i] = true; + backtracking(tempCombination, combinations, hasVisited, i + 1, target - candidates[i], candidates); + hasVisited[i] = false; + tempCombination.remove(tempCombination.size() - 1); + } + } +} +``` + +## 1-9 数字的组合求和 + +[216. Combination Sum III (Medium)](https://leetcode.com/problems/combination-sum-iii/description/) + +```html +Input: k = 3, n = 9 + +Output: + +[[1,2,6], [1,3,5], [2,3,4]] +``` + +从 1-9 数字中选出 k 个数不重复的数,使得它们的和为 n。 + +```java +public List> combinationSum3(int k, int n) { + List> combinations = new ArrayList<>(); + List path = new ArrayList<>(); + backtracking(k, n, 1, path, combinations); + return combinations; +} + +private void backtracking(int k, int n, int start, + List tempCombination, List> combinations) { + + if (k == 0 && n == 0) { + combinations.add(new ArrayList<>(tempCombination)); + return; + } + if (k == 0 || n == 0) { + return; + } + for (int i = start; i <= 9; i++) { + tempCombination.add(i); + backtracking(k - 1, n - i, i + 1, tempCombination, combinations); + tempCombination.remove(tempCombination.size() - 1); + } +} +``` + +## 子集 + +[78. Subsets (Medium)](https://leetcode.com/problems/subsets/description/) + +找出集合的所有子集,子集不能重复,[1, 2] 和 [2, 1] 这种子集算重复 + +```java +public List> subsets(int[] nums) { + List> subsets = new ArrayList<>(); + List tempSubset = new ArrayList<>(); + for (int size = 0; size <= nums.length; size++) { + backtracking(0, tempSubset, subsets, size, nums); // 不同的子集大小 + } + return subsets; +} + +private void backtracking(int start, List tempSubset, List> subsets, + final int size, final int[] nums) { + + if (tempSubset.size() == size) { + subsets.add(new ArrayList<>(tempSubset)); + return; + } + for (int i = start; i < nums.length; i++) { + tempSubset.add(nums[i]); + backtracking(i + 1, tempSubset, subsets, size, nums); + tempSubset.remove(tempSubset.size() - 1); + } +} +``` + +## 含有相同元素求子集 + +[90. Subsets II (Medium)](https://leetcode.com/problems/subsets-ii/description/) + +```html +For example, +If nums = [1,2,2], a solution is: + +[ + [2], + [1], + [1,2,2], + [2,2], + [1,2], + [] +] +``` + +```java +public List> subsetsWithDup(int[] nums) { + Arrays.sort(nums); + List> subsets = new ArrayList<>(); + List tempSubset = new ArrayList<>(); + boolean[] hasVisited = new boolean[nums.length]; + for (int size = 0; size <= nums.length; size++) { + backtracking(0, tempSubset, subsets, hasVisited, size, nums); // 不同的子集大小 + } + return subsets; +} + +private void backtracking(int start, List tempSubset, List> subsets, boolean[] hasVisited, + final int size, final int[] nums) { + + if (tempSubset.size() == size) { + subsets.add(new ArrayList<>(tempSubset)); + return; + } + for (int i = start; i < nums.length; i++) { + if (i != 0 && nums[i] == nums[i - 1] && !hasVisited[i - 1]) { + continue; + } + tempSubset.add(nums[i]); + hasVisited[i] = true; + backtracking(i + 1, tempSubset, subsets, hasVisited, size, nums); + hasVisited[i] = false; + tempSubset.remove(tempSubset.size() - 1); + } +} +``` + +## 分割字符串使得每个部分都是回文数 + +[131. Palindrome Partitioning (Medium)](https://leetcode.com/problems/palindrome-partitioning/description/) + +```html +For example, given s = "aab", +Return + +[ + ["aa","b"], + ["a","a","b"] +] +``` + +```java +public List> partition(String s) { + List> partitions = new ArrayList<>(); + List tempPartition = new ArrayList<>(); + doPartition(s, partitions, tempPartition); + return partitions; +} + +private void doPartition(String s, List> partitions, List tempPartition) { + if (s.length() == 0) { + partitions.add(new ArrayList<>(tempPartition)); + return; + } + for (int i = 0; i < s.length(); i++) { + if (isPalindrome(s, 0, i)) { + tempPartition.add(s.substring(0, i + 1)); + doPartition(s.substring(i + 1), partitions, tempPartition); + tempPartition.remove(tempPartition.size() - 1); + } + } +} + +private boolean isPalindrome(String s, int begin, int end) { + while (begin < end) { + if (s.charAt(begin++) != s.charAt(end--)) { + return false; + } + } + return true; +} +``` + +## 数独 + +[37. Sudoku Solver (Hard)](https://leetcode.com/problems/sudoku-solver/description/) + +

+ +```java +private boolean[][] rowsUsed = new boolean[9][10]; +private boolean[][] colsUsed = new boolean[9][10]; +private boolean[][] cubesUsed = new boolean[9][10]; +private char[][] board; + +public void solveSudoku(char[][] board) { + this.board = board; + for (int i = 0; i < 9; i++) + for (int j = 0; j < 9; j++) { + if (board[i][j] == '.') { + continue; + } + int num = board[i][j] - '0'; + rowsUsed[i][num] = true; + colsUsed[j][num] = true; + cubesUsed[cubeNum(i, j)][num] = true; + } + backtracking(0, 0); +} + +private boolean backtracking(int row, int col) { + while (row < 9 && board[row][col] != '.') { + row = col == 8 ? row + 1 : row; + col = col == 8 ? 0 : col + 1; + } + if (row == 9) { + return true; + } + for (int num = 1; num <= 9; num++) { + if (rowsUsed[row][num] || colsUsed[col][num] || cubesUsed[cubeNum(row, col)][num]) { + continue; + } + rowsUsed[row][num] = colsUsed[col][num] = cubesUsed[cubeNum(row, col)][num] = true; + board[row][col] = (char) (num + '0'); + if (backtracking(row, col)) { + return true; + } + board[row][col] = '.'; + rowsUsed[row][num] = colsUsed[col][num] = cubesUsed[cubeNum(row, col)][num] = false; + } + return false; +} + +private int cubeNum(int i, int j) { + int r = i / 3; + int c = j / 3; + return r * 3 + c; +} +``` + +## N 皇后 + +[51. N-Queens (Hard)](https://leetcode.com/problems/n-queens/description/) + +

+ +在 n\*n 的矩阵中摆放 n 个皇后,并且每个皇后不能在同一行,同一列,同一对角线上,求所有的 n 皇后的解。 + +一行一行地摆放,在确定一行中的那个皇后应该摆在哪一列时,需要用三个标记数组来确定某一列是否合法,这三个标记数组分别为:列标记数组、45 度对角线标记数组和 135 度对角线标记数组。 + +45 度对角线标记数组的长度为 2 \* n - 1,通过下图可以明确 (r, c) 的位置所在的数组下标为 r + c。 + +

+ +135 度对角线标记数组的长度也是 2 \* n - 1,(r, c) 的位置所在的数组下标为 n - 1 - (r - c)。 + +

+ +```java +private List> solutions; +private char[][] nQueens; +private boolean[] colUsed; +private boolean[] diagonals45Used; +private boolean[] diagonals135Used; +private int n; + +public List> solveNQueens(int n) { + solutions = new ArrayList<>(); + nQueens = new char[n][n]; + for (int i = 0; i < n; i++) { + Arrays.fill(nQueens[i], '.'); + } + colUsed = new boolean[n]; + diagonals45Used = new boolean[2 * n - 1]; + diagonals135Used = new boolean[2 * n - 1]; + this.n = n; + backtracking(0); + return solutions; +} + +private void backtracking(int row) { + if (row == n) { + List list = new ArrayList<>(); + for (char[] chars : nQueens) { + list.add(new String(chars)); + } + solutions.add(list); + return; + } + + for (int col = 0; col < n; col++) { + int diagonals45Idx = row + col; + int diagonals135Idx = n - 1 - (row - col); + if (colUsed[col] || diagonals45Used[diagonals45Idx] || diagonals135Used[diagonals135Idx]) { + continue; + } + nQueens[row][col] = 'Q'; + colUsed[col] = diagonals45Used[diagonals45Idx] = diagonals135Used[diagonals135Idx] = true; + backtracking(row + 1); + colUsed[col] = diagonals45Used[diagonals45Idx] = diagonals135Used[diagonals135Idx] = false; + nQueens[row][col] = '.'; + } +} +``` diff --git a/docs/notes/Leetcode 题解 - 数学.md b/docs/notes/Leetcode 题解 - 数学.md new file mode 100644 index 00000000..eadb76de --- /dev/null +++ b/docs/notes/Leetcode 题解 - 数学.md @@ -0,0 +1,510 @@ +* [点击阅读面试进阶指南 ](https://github.com/CyC2018/Backend-Interview-Guide) + +* [素数分解](#素数分解) +* [整除](#整除) + * [最大公约数最小公倍数](#最大公约数最小公倍数) + * [生成素数序列](#生成素数序列) + * [最大公约数](#最大公约数) + * [使用位操作和减法求解最大公约数](#使用位操作和减法求解最大公约数) +* [进制转换](#进制转换) + * [7 进制](#7-进制) + * [16 进制](#16-进制) + * [26 进制](#26-进制) +* [阶乘](#阶乘) + * [统计阶乘尾部有多少个 0](#统计阶乘尾部有多少个-0) +* [字符串加法减法](#字符串加法减法) + * [二进制加法](#二进制加法) + * [字符串加法](#字符串加法) +* [相遇问题](#相遇问题) + * [改变数组元素使所有的数组元素都相等](#改变数组元素使所有的数组元素都相等) + * [解法 1](#解法-1) + * [解法 2](#解法-2) +* [多数投票问题](#多数投票问题) + * [数组中出现次数多于 n / 2 的元素](#数组中出现次数多于-n--2-的元素) +* [其它](#其它) + * [平方数](#平方数) + * [3 的 n 次方](#3-的-n-次方) + * [乘积数组](#乘积数组) + * [找出数组中的乘积最大的三个数](#找出数组中的乘积最大的三个数) + + + +# 素数分解 + +每一个数都可以分解成素数的乘积,例如 84 = 22 \* 31 \* 50 \* 71 \* 110 \* 130 \* 170 \* … + +# 整除 + +令 x = 2m0 \* 3m1 \* 5m2 \* 7m3 \* 11m4 \* … + +令 y = 2n0 \* 3n1 \* 5n2 \* 7n3 \* 11n4 \* … + +如果 x 整除 y(y mod x == 0),则对于所有 i,mi <= ni。 + +## 最大公约数最小公倍数 + +x 和 y 的最大公约数为:gcd(x,y) = 2min(m0,n0) \* 3min(m1,n1) \* 5min(m2,n2) \* ... + +x 和 y 的最小公倍数为:lcm(x,y) = 2max(m0,n0) \* 3max(m1,n1) \* 5max(m2,n2) \* ... + +## 生成素数序列 + +[204. Count Primes (Easy)](https://leetcode.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; +} +``` + +### 最大公约数 + +```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); +} +``` + +## 使用位操作和减法求解最大公约数 + +[编程之美: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); + } +} +``` + +# 进制转换 + +## 7 进制 + +[504. Base 7 (Easy)](https://leetcode.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); +} +``` + +## 16 进制 + +[405. Convert a Number to Hexadecimal (Easy)](https://leetcode.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(); +} +``` + +## 26 进制 + +[168. Excel Sheet Column Title (Easy)](https://leetcode.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'); +} +``` + +# 阶乘 + +## 统计阶乘尾部有多少个 0 + +[172. Factorial Trailing Zeroes (Easy)](https://leetcode.com/problems/factorial-trailing-zeroes/description/) + +尾部的 0 由 2 * 5 得来,2 的数量明显多于 5 的数量,因此只要统计有多少个 5 即可。 + +对于一个数 N,它所包含 5 的个数为:N/5 + N/52 + N/53 + ...,其中 N/5 表示不大于 N 的数中 5 的倍数贡献一个 5,N/52 表示不大于 N 的数中 52 的倍数再贡献一个 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/22 + N/23 + ... + +# 字符串加法减法 + +## 二进制加法 + +[67. Add Binary (Easy)](https://leetcode.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(); +} +``` + +## 字符串加法 + +[415. Add Strings (Easy)](https://leetcode.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(); +} +``` + +# 相遇问题 + +## 改变数组元素使所有的数组元素都相等 + +[462. Minimum Moves to Equal Array Elements II (Medium)](https://leetcode.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; +} +``` + +# 多数投票问题 + +## 数组中出现次数多于 n / 2 的元素 + +[169. Majority Element (Easy)](https://leetcode.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; +} +``` + +# 其它 + +## 平方数 + +[367. Valid Perfect Square (Easy)](https://leetcode.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; +} +``` + +## 3 的 n 次方 + +[326. Power of Three (Easy)](https://leetcode.com/problems/power-of-three/description/) + +```java +public boolean isPowerOfThree(int n) { + return n > 0 && (1162261467 % n == 0); +} +``` + +## 乘积数组 + +[238. Product of Array Except Self (Medium)](https://leetcode.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; +} +``` + +## 找出数组中的乘积最大的三个数 + +[628. Maximum Product of Three Numbers (Easy)](https://leetcode.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); +} +``` diff --git a/docs/notes/Leetcode 题解 - 数组与矩阵.md b/docs/notes/Leetcode 题解 - 数组与矩阵.md new file mode 100644 index 00000000..cbaddae9 --- /dev/null +++ b/docs/notes/Leetcode 题解 - 数组与矩阵.md @@ -0,0 +1,434 @@ +* [点击阅读面试进阶指南 ](https://github.com/CyC2018/Backend-Interview-Guide) + +* [把数组中的 0 移到末尾](#把数组中的-0-移到末尾) +* [改变矩阵维度](#改变矩阵维度) +* [找出数组中最长的连续 1](#找出数组中最长的连续-1) +* [有序矩阵查找](#有序矩阵查找) +* [有序矩阵的 Kth Element](#有序矩阵的-kth-element) +* [一个数组元素在 [1, n] 之间,其中一个数被替换为另一个数,找出重复的数和丢失的数](#一个数组元素在-[1,-n]-之间,其中一个数被替换为另一个数,找出重复的数和丢失的数) +* [找出数组中重复的数,数组值在 [1, n] 之间](#找出数组中重复的数,数组值在-[1,-n]-之间) +* [数组相邻差值的个数](#数组相邻差值的个数) +* [数组的度](#数组的度) +* [对角元素相等的矩阵](#对角元素相等的矩阵) +* [嵌套数组](#嵌套数组) +* [分隔数组](#分隔数组) + + + +# 把数组中的 0 移到末尾 + +[283. Move Zeroes (Easy)](https://leetcode.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; + } +} +``` + +# 改变矩阵维度 + +[566. Reshape the Matrix (Easy)](https://leetcode.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; +} +``` + +# 找出数组中最长的连续 1 + +[485. Max Consecutive Ones (Easy)](https://leetcode.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; +} +``` + +# 有序矩阵查找 + +[240. Search a 2D Matrix II (Medium)](https://leetcode.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; +} +``` + +# 有序矩阵的 Kth Element + +[378. Kth Smallest Element in a Sorted Matrix ((Medium))](https://leetcode.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.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 pq = new PriorityQueue(); + 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 { + 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; + } +} +``` + +# 一个数组元素在 [1, n] 之间,其中一个数被替换为另一个数,找出重复的数和丢失的数 + +[645. Set Mismatch (Easy)](https://leetcode.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; +} +``` + +类似题目: + +- [448. Find All Numbers Disappeared in an Array (Easy)](https://leetcode.com/problems/find-all-numbers-disappeared-in-an-array/description/),寻找所有丢失的元素 +- [442. Find All Duplicates in an Array (Medium)](https://leetcode.com/problems/find-all-duplicates-in-an-array/description/),寻找所有重复的元素。 + +# 找出数组中重复的数,数组值在 [1, n] 之间 + +[287. Find the Duplicate Number (Medium)](https://leetcode.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; +} +``` + +# 数组相邻差值的个数 + +[667. Beautiful Arrangement II (Medium)](https://leetcode.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; +} +``` + +# 数组的度 + +[697. Degree of an Array (Easy)](https://leetcode.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 numsCnt = new HashMap<>(); + Map numsLastIndex = new HashMap<>(); + Map 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; +} +``` + +# 对角元素相等的矩阵 + +[766. Toeplitz Matrix (Easy)](https://leetcode.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); +} +``` + +# 嵌套数组 + +[565. Array Nesting (Medium)](https://leetcode.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; +} +``` + +# 分隔数组 + +[769. Max Chunks To Make Sorted (Medium)](https://leetcode.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; +} +``` diff --git a/docs/notes/Leetcode 题解 - 栈和队列.md b/docs/notes/Leetcode 题解 - 栈和队列.md new file mode 100644 index 00000000..c6bf5964 --- /dev/null +++ b/docs/notes/Leetcode 题解 - 栈和队列.md @@ -0,0 +1,221 @@ +* [点击阅读面试进阶指南 ](https://github.com/CyC2018/Backend-Interview-Guide) + +* [用栈实现队列](#用栈实现队列) +* [用队列实现栈](#用队列实现栈) +* [最小值栈](#最小值栈) +* [用栈实现括号匹配](#用栈实现括号匹配) +* [数组中元素与下一个比它大的元素之间的距离](#数组中元素与下一个比它大的元素之间的距离) +* [循环数组中比当前元素大的下一个元素](#循环数组中比当前元素大的下一个元素) + + + +# 用栈实现队列 + +[232. Implement Queue using Stacks (Easy)](https://leetcode.com/problems/implement-queue-using-stacks/description/) + +栈的顺序为后进先出,而队列的顺序为先进先出。使用两个栈实现队列,一个元素需要经过两个栈才能出队列,在经过第一个栈时元素顺序被反转,经过第二个栈时再次被反转,此时就是先进先出顺序。 + +```java +class MyQueue { + + private Stack in = new Stack<>(); + private Stack out = new Stack<>(); + + public void push(int x) { + in.push(x); + } + + public int pop() { + in2out(); + return out.pop(); + } + + public int peek() { + in2out(); + return out.peek(); + } + + private void in2out() { + if (out.isEmpty()) { + while (!in.isEmpty()) { + out.push(in.pop()); + } + } + } + + public boolean empty() { + return in.isEmpty() && out.isEmpty(); + } +} +``` + +# 用队列实现栈 + +[225. Implement Stack using Queues (Easy)](https://leetcode.com/problems/implement-stack-using-queues/description/) + +在将一个元素 x 插入队列时,为了维护原来的后进先出顺序,需要让 x 插入队列首部。而队列的默认插入顺序是队列尾部,因此在将 x 插入队列尾部之后,需要让除了 x 之外的所有元素出队列,再入队列。 + +```java +class MyStack { + + private Queue queue; + + public MyStack() { + queue = new LinkedList<>(); + } + + public void push(int x) { + queue.add(x); + int cnt = queue.size(); + while (cnt-- > 1) { + queue.add(queue.poll()); + } + } + + public int pop() { + return queue.remove(); + } + + public int top() { + return queue.peek(); + } + + public boolean empty() { + return queue.isEmpty(); + } +} +``` + +# 最小值栈 + +[155. Min Stack (Easy)](https://leetcode.com/problems/min-stack/description/) + +```java +class MinStack { + + private Stack dataStack; + private Stack minStack; + private int min; + + public MinStack() { + dataStack = new Stack<>(); + minStack = new Stack<>(); + min = Integer.MAX_VALUE; + } + + public void push(int x) { + dataStack.add(x); + min = Math.min(min, x); + minStack.add(min); + } + + public void pop() { + dataStack.pop(); + minStack.pop(); + min = minStack.isEmpty() ? Integer.MAX_VALUE : minStack.peek(); + } + + public int top() { + return dataStack.peek(); + } + + public int getMin() { + return minStack.peek(); + } +} +``` + +对于实现最小值队列问题,可以先将队列使用栈来实现,然后就将问题转换为最小值栈,这个问题出现在 编程之美:3.7。 + +# 用栈实现括号匹配 + +[20. Valid Parentheses (Easy)](https://leetcode.com/problems/valid-parentheses/description/) + +```html +"()[]{}" + +Output : true +``` + +```java +public boolean isValid(String s) { + Stack stack = new Stack<>(); + for (char c : s.toCharArray()) { + if (c == '(' || c == '{' || c == '[') { + stack.push(c); + } else { + if (stack.isEmpty()) { + return false; + } + char cStack = stack.pop(); + boolean b1 = c == ')' && cStack != '('; + boolean b2 = c == ']' && cStack != '['; + boolean b3 = c == '}' && cStack != '{'; + if (b1 || b2 || b3) { + return false; + } + } + } + return stack.isEmpty(); +} +``` + +# 数组中元素与下一个比它大的元素之间的距离 + +[739. Daily Temperatures (Medium)](https://leetcode.com/problems/daily-temperatures/description/) + +```html +Input: [73, 74, 75, 71, 69, 72, 76, 73] +Output: [1, 1, 4, 2, 1, 1, 0, 0] +``` + +在遍历数组时用栈把数组中的数存起来,如果当前遍历的数比栈顶元素来的大,说明栈顶元素的下一个比它大的数就是当前元素。 + +```java +public int[] dailyTemperatures(int[] temperatures) { + int n = temperatures.length; + int[] dist = new int[n]; + Stack indexs = new Stack<>(); + for (int curIndex = 0; curIndex < n; curIndex++) { + while (!indexs.isEmpty() && temperatures[curIndex] > temperatures[indexs.peek()]) { + int preIndex = indexs.pop(); + dist[preIndex] = curIndex - preIndex; + } + indexs.add(curIndex); + } + return dist; +} +``` + +# 循环数组中比当前元素大的下一个元素 + +[503. Next Greater Element II (Medium)](https://leetcode.com/problems/next-greater-element-ii/description/) + +```text +Input: [1,2,1] +Output: [2,-1,2] +Explanation: The first 1's next greater number is 2; +The number 2 can't find next greater number; +The second 1's next greater number needs to search circularly, which is also 2. +``` + +与 739. Daily Temperatures (Medium) 不同的是,数组是循环数组,并且最后要求的不是距离而是下一个元素。 + +```java +public int[] nextGreaterElements(int[] nums) { + int n = nums.length; + int[] next = new int[n]; + Arrays.fill(next, -1); + Stack pre = new Stack<>(); + for (int i = 0; i < n * 2; i++) { + int num = nums[i % n]; + while (!pre.isEmpty() && nums[pre.peek()] < num) { + next[pre.pop()] = num; + } + if (i < n){ + pre.push(i); + } + } + return next; +} +``` diff --git a/docs/notes/Leetcode 题解 - 树.md b/docs/notes/Leetcode 题解 - 树.md new file mode 100644 index 00000000..2ca20ee6 --- /dev/null +++ b/docs/notes/Leetcode 题解 - 树.md @@ -0,0 +1,1121 @@ +* [点击阅读面试进阶指南 ](https://github.com/CyC2018/Backend-Interview-Guide) + +* [递归](#递归) + * [树的高度](#树的高度) + * [平衡树](#平衡树) + * [两节点的最长路径](#两节点的最长路径) + * [翻转树](#翻转树) + * [归并两棵树](#归并两棵树) + * [判断路径和是否等于一个数](#判断路径和是否等于一个数) + * [统计路径和等于一个数的路径数量](#统计路径和等于一个数的路径数量) + * [子树](#子树) + * [树的对称](#树的对称) + * [最小路径](#最小路径) + * [统计左叶子节点的和](#统计左叶子节点的和) + * [相同节点值的最大路径长度](#相同节点值的最大路径长度) + * [间隔遍历](#间隔遍历) + * [找出二叉树中第二小的节点](#找出二叉树中第二小的节点) +* [层次遍历](#层次遍历) + * [一棵树每层节点的平均数](#一棵树每层节点的平均数) + * [得到左下角的节点](#得到左下角的节点) +* [前中后序遍历](#前中后序遍历) + * [非递归实现二叉树的前序遍历](#非递归实现二叉树的前序遍历) + * [非递归实现二叉树的后序遍历](#非递归实现二叉树的后序遍历) + * [非递归实现二叉树的中序遍历](#非递归实现二叉树的中序遍历) +* [BST](#bst) + * [修剪二叉查找树](#修剪二叉查找树) + * [寻找二叉查找树的第 k 个元素](#寻找二叉查找树的第-k-个元素) + * [把二叉查找树每个节点的值都加上比它大的节点的值](#把二叉查找树每个节点的值都加上比它大的节点的值) + * [二叉查找树的最近公共祖先](#二叉查找树的最近公共祖先) + * [二叉树的最近公共祖先](#二叉树的最近公共祖先) + * [从有序数组中构造二叉查找树](#从有序数组中构造二叉查找树) + * [根据有序链表构造平衡的二叉查找树](#根据有序链表构造平衡的二叉查找树) + * [在二叉查找树中寻找两个节点,使它们的和为一个给定值](#在二叉查找树中寻找两个节点,使它们的和为一个给定值) + * [在二叉查找树中查找两个节点之差的最小绝对值](#在二叉查找树中查找两个节点之差的最小绝对值) + * [寻找二叉查找树中出现次数最多的值](#寻找二叉查找树中出现次数最多的值) +* [Trie](#trie) + * [实现一个 Trie](#实现一个-trie) + * [实现一个 Trie,用来求前缀和](#实现一个-trie,用来求前缀和) + + + +# 递归 + +一棵树要么是空树,要么有两个指针,每个指针指向一棵树。树是一种递归结构,很多树的问题可以使用递归来处理。 + +## 树的高度 + +[104. Maximum Depth of Binary Tree (Easy)](https://leetcode.com/problems/maximum-depth-of-binary-tree/description/) + +```java +public int maxDepth(TreeNode root) { + if (root == null) return 0; + return Math.max(maxDepth(root.left), maxDepth(root.right)) + 1; +} +``` + +## 平衡树 + +[110. Balanced Binary Tree (Easy)](https://leetcode.com/problems/balanced-binary-tree/description/) + +```html + 3 + / \ + 9 20 + / \ + 15 7 +``` + +平衡树左右子树高度差都小于等于 1 + +```java +private boolean result = true; + +public boolean isBalanced(TreeNode root) { + maxDepth(root); + return result; +} + +public int maxDepth(TreeNode root) { + if (root == null) return 0; + int l = maxDepth(root.left); + int r = maxDepth(root.right); + if (Math.abs(l - r) > 1) result = false; + return 1 + Math.max(l, r); +} +``` + +## 两节点的最长路径 + +[543. Diameter of Binary Tree (Easy)](https://leetcode.com/problems/diameter-of-binary-tree/description/) + +```html +Input: + + 1 + / \ + 2 3 + / \ + 4 5 + +Return 3, which is the length of the path [4,2,1,3] or [5,2,1,3]. +``` + +```java +private int max = 0; + +public int diameterOfBinaryTree(TreeNode root) { + depth(root); + return max; +} + +private int depth(TreeNode root) { + if (root == null) return 0; + int leftDepth = depth(root.left); + int rightDepth = depth(root.right); + max = Math.max(max, leftDepth + rightDepth); + return Math.max(leftDepth, rightDepth) + 1; +} +``` + +## 翻转树 + +[226. Invert Binary Tree (Easy)](https://leetcode.com/problems/invert-binary-tree/description/) + +```java +public TreeNode invertTree(TreeNode root) { + if (root == null) return null; + TreeNode left = root.left; // 后面的操作会改变 left 指针,因此先保存下来 + root.left = invertTree(root.right); + root.right = invertTree(left); + return root; +} +``` + +## 归并两棵树 + +[617. Merge Two Binary Trees (Easy)](https://leetcode.com/problems/merge-two-binary-trees/description/) + +```html +Input: + Tree 1 Tree 2 + 1 2 + / \ / \ + 3 2 1 3 + / \ \ + 5 4 7 + +Output: + 3 + / \ + 4 5 + / \ \ + 5 4 7 +``` + +```java +public TreeNode mergeTrees(TreeNode t1, TreeNode t2) { + if (t1 == null && t2 == null) return null; + if (t1 == null) return t2; + if (t2 == null) return t1; + TreeNode root = new TreeNode(t1.val + t2.val); + root.left = mergeTrees(t1.left, t2.left); + root.right = mergeTrees(t1.right, t2.right); + return root; +} +``` + +## 判断路径和是否等于一个数 + +[Leetcdoe : 112. Path Sum (Easy)](https://leetcode.com/problems/path-sum/description/) + +```html +Given the below binary tree and sum = 22, + + 5 + / \ + 4 8 + / / \ + 11 13 4 + / \ \ + 7 2 1 + +return true, as there exist a root-to-leaf path 5->4->11->2 which sum is 22. +``` + +路径和定义为从 root 到 leaf 的所有节点的和。 + +```java +public boolean hasPathSum(TreeNode root, int sum) { + if (root == null) return false; + if (root.left == null && root.right == null && root.val == sum) return true; + return hasPathSum(root.left, sum - root.val) || hasPathSum(root.right, sum - root.val); +} +``` + +## 统计路径和等于一个数的路径数量 + +[437. Path Sum III (Easy)](https://leetcode.com/problems/path-sum-iii/description/) + +```html +root = [10,5,-3,3,2,null,11,3,-2,null,1], sum = 8 + + 10 + / \ + 5 -3 + / \ \ + 3 2 11 + / \ \ +3 -2 1 + +Return 3. The paths that sum to 8 are: + +1. 5 -> 3 +2. 5 -> 2 -> 1 +3. -3 -> 11 +``` + +路径不一定以 root 开头,也不一定以 leaf 结尾,但是必须连续。 + +```java +public int pathSum(TreeNode root, int sum) { + if (root == null) return 0; + int ret = pathSumStartWithRoot(root, sum) + pathSum(root.left, sum) + pathSum(root.right, sum); + return ret; +} + +private int pathSumStartWithRoot(TreeNode root, int sum) { + if (root == null) return 0; + int ret = 0; + if (root.val == sum) ret++; + ret += pathSumStartWithRoot(root.left, sum - root.val) + pathSumStartWithRoot(root.right, sum - root.val); + return ret; +} +``` + +## 子树 + +[572. Subtree of Another Tree (Easy)](https://leetcode.com/problems/subtree-of-another-tree/description/) + +```html +Given tree s: + 3 + / \ + 4 5 + / \ + 1 2 + +Given tree t: + 4 + / \ + 1 2 + +Return true, because t has the same structure and node values with a subtree of s. + +Given tree s: + + 3 + / \ + 4 5 + / \ + 1 2 + / + 0 + +Given tree t: + 4 + / \ + 1 2 + +Return false. +``` + +```java +public boolean isSubtree(TreeNode s, TreeNode t) { + if (s == null) return false; + return isSubtreeWithRoot(s, t) || isSubtree(s.left, t) || isSubtree(s.right, t); +} + +private boolean isSubtreeWithRoot(TreeNode s, TreeNode t) { + if (t == null && s == null) return true; + if (t == null || s == null) return false; + if (t.val != s.val) return false; + return isSubtreeWithRoot(s.left, t.left) && isSubtreeWithRoot(s.right, t.right); +} +``` + +## 树的对称 + +[101. Symmetric Tree (Easy)](https://leetcode.com/problems/symmetric-tree/description/) + +```html + 1 + / \ + 2 2 + / \ / \ +3 4 4 3 +``` + +```java +public boolean isSymmetric(TreeNode root) { + if (root == null) return true; + return isSymmetric(root.left, root.right); +} + +private boolean isSymmetric(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 isSymmetric(t1.left, t2.right) && isSymmetric(t1.right, t2.left); +} +``` + +## 最小路径 + +[111. Minimum Depth of Binary Tree (Easy)](https://leetcode.com/problems/minimum-depth-of-binary-tree/description/) + +树的根节点到叶子节点的最小路径长度 + +```java +public int minDepth(TreeNode root) { + if (root == null) return 0; + int left = minDepth(root.left); + int right = minDepth(root.right); + if (left == 0 || right == 0) return left + right + 1; + return Math.min(left, right) + 1; +} +``` + +## 统计左叶子节点的和 + +[404. Sum of Left Leaves (Easy)](https://leetcode.com/problems/sum-of-left-leaves/description/) + +```html + 3 + / \ + 9 20 + / \ + 15 7 + +There are two left leaves in the binary tree, with values 9 and 15 respectively. Return 24. +``` + +```java +public int sumOfLeftLeaves(TreeNode root) { + if (root == null) return 0; + if (isLeaf(root.left)) return root.left.val + sumOfLeftLeaves(root.right); + return sumOfLeftLeaves(root.left) + sumOfLeftLeaves(root.right); +} + +private boolean isLeaf(TreeNode node){ + if (node == null) return false; + return node.left == null && node.right == null; +} +``` + +## 相同节点值的最大路径长度 + +[687. Longest Univalue Path (Easy)](https://leetcode.com/problems/longest-univalue-path/) + +```html + 1 + / \ + 4 5 + / \ \ + 4 4 5 + +Output : 2 +``` + +```java +private int path = 0; + +public int longestUnivaluePath(TreeNode root) { + dfs(root); + return path; +} + +private int dfs(TreeNode root){ + if (root == null) return 0; + int left = dfs(root.left); + int right = dfs(root.right); + int leftPath = root.left != null && root.left.val == root.val ? left + 1 : 0; + int rightPath = root.right != null && root.right.val == root.val ? right + 1 : 0; + path = Math.max(path, leftPath + rightPath); + return Math.max(leftPath, rightPath); +} +``` + +## 间隔遍历 + +[337. House Robber III (Medium)](https://leetcode.com/problems/house-robber-iii/description/) + +```html + 3 + / \ + 2 3 + \ \ + 3 1 +Maximum amount of money the thief can rob = 3 + 3 + 1 = 7. +``` + +```java +public int rob(TreeNode root) { + if (root == null) return 0; + int val1 = root.val; + if (root.left != null) val1 += rob(root.left.left) + rob(root.left.right); + if (root.right != null) val1 += rob(root.right.left) + rob(root.right.right); + int val2 = rob(root.left) + rob(root.right); + return Math.max(val1, val2); +} +``` + +## 找出二叉树中第二小的节点 + +[671. Second Minimum Node In a Binary Tree (Easy)](https://leetcode.com/problems/second-minimum-node-in-a-binary-tree/description/) + +```html +Input: + 2 + / \ + 2 5 + / \ + 5 7 + +Output: 5 +``` + +一个节点要么具有 0 个或 2 个子节点,如果有子节点,那么根节点是最小的节点。 + +```java +public int findSecondMinimumValue(TreeNode root) { + if (root == null) return -1; + if (root.left == null && root.right == null) return -1; + int leftVal = root.left.val; + int rightVal = root.right.val; + if (leftVal == root.val) leftVal = findSecondMinimumValue(root.left); + if (rightVal == root.val) rightVal = findSecondMinimumValue(root.right); + if (leftVal != -1 && rightVal != -1) return Math.min(leftVal, rightVal); + if (leftVal != -1) return leftVal; + return rightVal; +} +``` + +# 层次遍历 + +使用 BFS 进行层次遍历。不需要使用两个队列来分别存储当前层的节点和下一层的节点,因为在开始遍历一层的节点时,当前队列中的节点数就是当前层的节点数,只要控制遍历这么多节点数,就能保证这次遍历的都是当前层的节点。 + +## 一棵树每层节点的平均数 + +[637. Average of Levels in Binary Tree (Easy)](https://leetcode.com/problems/average-of-levels-in-binary-tree/description/) + +```java +public List averageOfLevels(TreeNode root) { + List ret = new ArrayList<>(); + if (root == null) return ret; + Queue queue = new LinkedList<>(); + queue.add(root); + while (!queue.isEmpty()) { + int cnt = queue.size(); + double sum = 0; + for (int i = 0; i < cnt; i++) { + TreeNode node = queue.poll(); + sum += node.val; + if (node.left != null) queue.add(node.left); + if (node.right != null) queue.add(node.right); + } + ret.add(sum / cnt); + } + return ret; +} +``` + +## 得到左下角的节点 + +[513. Find Bottom Left Tree Value (Easy)](https://leetcode.com/problems/find-bottom-left-tree-value/description/) + +```html +Input: + + 1 + / \ + 2 3 + / / \ + 4 5 6 + / + 7 + +Output: +7 +``` + +```java +public int findBottomLeftValue(TreeNode root) { + Queue queue = new LinkedList<>(); + queue.add(root); + while (!queue.isEmpty()) { + root = queue.poll(); + if (root.right != null) queue.add(root.right); + if (root.left != null) queue.add(root.left); + } + return root.val; +} +``` + +# 前中后序遍历 + +```html + 1 + / \ + 2 3 + / \ \ +4 5 6 +``` + +- 层次遍历顺序:[1 2 3 4 5 6] +- 前序遍历顺序:[1 2 4 5 3 6] +- 中序遍历顺序:[4 2 5 1 3 6] +- 后序遍历顺序:[4 5 2 6 3 1] + +层次遍历使用 BFS 实现,利用的就是 BFS 一层一层遍历的特性;而前序、中序、后序遍历利用了 DFS 实现。 + +前序、中序、后序遍只是在对节点访问的顺序有一点不同,其它都相同。 + +① 前序 + +```java +void dfs(TreeNode root) { + visit(root); + dfs(root.left); + dfs(root.right); +} +``` + +② 中序 + +```java +void dfs(TreeNode root) { + dfs(root.left); + visit(root); + dfs(root.right); +} +``` + +③ 后序 + +```java +void dfs(TreeNode root) { + dfs(root.left); + dfs(root.right); + visit(root); +} +``` + +## 非递归实现二叉树的前序遍历 + +[144. Binary Tree Preorder Traversal (Medium)](https://leetcode.com/problems/binary-tree-preorder-traversal/description/) + +```java +public List preorderTraversal(TreeNode root) { + List ret = new ArrayList<>(); + Stack stack = new Stack<>(); + stack.push(root); + while (!stack.isEmpty()) { + TreeNode node = stack.pop(); + if (node == null) continue; + ret.add(node.val); + stack.push(node.right); // 先右后左,保证左子树先遍历 + stack.push(node.left); + } + return ret; +} +``` + +## 非递归实现二叉树的后序遍历 + +[145. Binary Tree Postorder Traversal (Medium)](https://leetcode.com/problems/binary-tree-postorder-traversal/description/) + +前序遍历为 root -> left -> right,后序遍历为 left -> right -> root。可以修改前序遍历成为 root -> right -> left,那么这个顺序就和后序遍历正好相反。 + +```java +public List postorderTraversal(TreeNode root) { + List ret = new ArrayList<>(); + Stack stack = new Stack<>(); + stack.push(root); + while (!stack.isEmpty()) { + TreeNode node = stack.pop(); + if (node == null) continue; + ret.add(node.val); + stack.push(node.left); + stack.push(node.right); + } + Collections.reverse(ret); + return ret; +} +``` + +## 非递归实现二叉树的中序遍历 + +[94. Binary Tree Inorder Traversal (Medium)](https://leetcode.com/problems/binary-tree-inorder-traversal/description/) + +```java +public List inorderTraversal(TreeNode root) { + List ret = new ArrayList<>(); + if (root == null) return ret; + Stack stack = new Stack<>(); + TreeNode cur = root; + while (cur != null || !stack.isEmpty()) { + while (cur != null) { + stack.push(cur); + cur = cur.left; + } + TreeNode node = stack.pop(); + ret.add(node.val); + cur = node.right; + } + return ret; +} +``` + +# BST + +二叉查找树(BST):根节点大于等于左子树所有节点,小于等于右子树所有节点。 + +二叉查找树中序遍历有序。 + +## 修剪二叉查找树 + +[669. Trim a Binary Search Tree (Easy)](https://leetcode.com/problems/trim-a-binary-search-tree/description/) + +```html +Input: + + 3 + / \ + 0 4 + \ + 2 + / + 1 + + L = 1 + R = 3 + +Output: + + 3 + / + 2 + / + 1 +``` + +题目描述:只保留值在 L \~ R 之间的节点 + +```java +public TreeNode trimBST(TreeNode root, int L, int R) { + if (root == null) return null; + if (root.val > R) return trimBST(root.left, L, R); + if (root.val < L) return trimBST(root.right, L, R); + root.left = trimBST(root.left, L, R); + root.right = trimBST(root.right, L, R); + return root; +} +``` + +## 寻找二叉查找树的第 k 个元素 + +[230. Kth Smallest Element in a BST (Medium)](https://leetcode.com/problems/kth-smallest-element-in-a-bst/description/) + + +中序遍历解法: + +```java +private int cnt = 0; +private int val; + +public int kthSmallest(TreeNode root, int k) { + inOrder(root, k); + return val; +} + +private void inOrder(TreeNode node, int k) { + if (node == null) return; + inOrder(node.left, k); + cnt++; + if (cnt == k) { + val = node.val; + return; + } + inOrder(node.right, k); +} +``` + +递归解法: + +```java +public int kthSmallest(TreeNode root, int k) { + int leftCnt = count(root.left); + if (leftCnt == k - 1) return root.val; + if (leftCnt > k - 1) return kthSmallest(root.left, k); + return kthSmallest(root.right, k - leftCnt - 1); +} + +private int count(TreeNode node) { + if (node == null) return 0; + return 1 + count(node.left) + count(node.right); +} +``` + +## 把二叉查找树每个节点的值都加上比它大的节点的值 + +[Convert BST to Greater Tree (Easy)](https://leetcode.com/problems/convert-bst-to-greater-tree/description/) + +```html +Input: The root of a Binary Search Tree like this: + + 5 + / \ + 2 13 + +Output: The root of a Greater Tree like this: + + 18 + / \ + 20 13 +``` + +先遍历右子树。 + +```java +private int sum = 0; + +public TreeNode convertBST(TreeNode root) { + traver(root); + return root; +} + +private void traver(TreeNode node) { + if (node == null) return; + traver(node.right); + sum += node.val; + node.val = sum; + traver(node.left); +} +``` + +## 二叉查找树的最近公共祖先 + +[235. Lowest Common Ancestor of a Binary Search Tree (Easy)](https://leetcode.com/problems/lowest-common-ancestor-of-a-binary-search-tree/description/) + +```html + _______6______ + / \ + ___2__ ___8__ + / \ / \ +0 4 7 9 + / \ + 3 5 + +For example, the lowest common ancestor (LCA) of nodes 2 and 8 is 6. Another example is LCA of nodes 2 and 4 is 2, since a node can be a descendant of itself according to the LCA definition. +``` + +```java +public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) { + if (root.val > p.val && root.val > q.val) return lowestCommonAncestor(root.left, p, q); + if (root.val < p.val && root.val < q.val) return lowestCommonAncestor(root.right, p, q); + return root; +} +``` + +## 二叉树的最近公共祖先 + +[236. Lowest Common Ancestor of a Binary Tree (Medium) ](https://leetcode.com/problems/lowest-common-ancestor-of-a-binary-tree/description/) + +```html + _______3______ + / \ + ___5__ ___1__ + / \ / \ +6 2 0 8 + / \ + 7 4 + +For example, the lowest common ancestor (LCA) of nodes 5 and 1 is 3. Another example is LCA of nodes 5 and 4 is 5, since a node can be a descendant of itself according to the LCA definition. +``` + +```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; +} +``` + +## 从有序数组中构造二叉查找树 + +[108. Convert Sorted Array to Binary Search Tree (Easy)](https://leetcode.com/problems/convert-sorted-array-to-binary-search-tree/description/) + +```java +public TreeNode sortedArrayToBST(int[] nums) { + return toBST(nums, 0, nums.length - 1); +} + +private TreeNode toBST(int[] nums, int sIdx, int eIdx){ + if (sIdx > eIdx) return null; + int mIdx = (sIdx + eIdx) / 2; + TreeNode root = new TreeNode(nums[mIdx]); + root.left = toBST(nums, sIdx, mIdx - 1); + root.right = toBST(nums, mIdx + 1, eIdx); + return root; +} +``` + +## 根据有序链表构造平衡的二叉查找树 + +[109. Convert Sorted List to Binary Search Tree (Medium)](https://leetcode.com/problems/convert-sorted-list-to-binary-search-tree/description/) + +```html +Given the sorted linked list: [-10,-3,0,5,9], + +One possible answer is: [0,-3,9,-10,null,5], which represents the following height balanced BST: + + 0 + / \ + -3 9 + / / + -10 5 +``` + +```java +public TreeNode sortedListToBST(ListNode head) { + if (head == null) return null; + if (head.next == null) return new TreeNode(head.val); + ListNode preMid = preMid(head); + ListNode mid = preMid.next; + preMid.next = null; // 断开链表 + TreeNode t = new TreeNode(mid.val); + t.left = sortedListToBST(head); + t.right = sortedListToBST(mid.next); + return t; +} + +private ListNode preMid(ListNode head) { + ListNode slow = head, fast = head.next; + ListNode pre = head; + while (fast != null && fast.next != null) { + pre = slow; + slow = slow.next; + fast = fast.next.next; + } + return pre; +} +``` + +## 在二叉查找树中寻找两个节点,使它们的和为一个给定值 + +[653. Two Sum IV - Input is a BST (Easy)](https://leetcode.com/problems/two-sum-iv-input-is-a-bst/description/) + +```html +Input: + + 5 + / \ + 3 6 + / \ \ +2 4 7 + +Target = 9 + +Output: True +``` + +使用中序遍历得到有序数组之后,再利用双指针对数组进行查找。 + +应该注意到,这一题不能用分别在左右子树两部分来处理这种思想,因为两个待求的节点可能分别在左右子树中。 + +```java +public boolean findTarget(TreeNode root, int k) { + List nums = new ArrayList<>(); + inOrder(root, nums); + int i = 0, j = nums.size() - 1; + while (i < j) { + int sum = nums.get(i) + nums.get(j); + if (sum == k) return true; + if (sum < k) i++; + else j--; + } + return false; +} + +private void inOrder(TreeNode root, List nums) { + if (root == null) return; + inOrder(root.left, nums); + nums.add(root.val); + inOrder(root.right, nums); +} +``` + +## 在二叉查找树中查找两个节点之差的最小绝对值 + +[530. Minimum Absolute Difference in BST (Easy)](https://leetcode.com/problems/minimum-absolute-difference-in-bst/description/) + +```html +Input: + + 1 + \ + 3 + / + 2 + +Output: + +1 +``` + +利用二叉查找树的中序遍历为有序的性质,计算中序遍历中临近的两个节点之差的绝对值,取最小值。 + +```java +private int minDiff = Integer.MAX_VALUE; +private TreeNode preNode = null; + +public int getMinimumDifference(TreeNode root) { + inOrder(root); + return minDiff; +} + +private void inOrder(TreeNode node) { + if (node == null) return; + inOrder(node.left); + if (preNode != null) minDiff = Math.min(minDiff, node.val - preNode.val); + preNode = node; + inOrder(node.right); +} +``` + +## 寻找二叉查找树中出现次数最多的值 + +[501. Find Mode in Binary Search Tree (Easy)](https://leetcode.com/problems/find-mode-in-binary-search-tree/description/) + +```html + 1 + \ + 2 + / + 2 + +return [2]. +``` + +答案可能不止一个,也就是有多个值出现的次数一样多。 + +```java +private int curCnt = 1; +private int maxCnt = 1; +private TreeNode preNode = null; + +public int[] findMode(TreeNode root) { + List maxCntNums = new ArrayList<>(); + inOrder(root, maxCntNums); + int[] ret = new int[maxCntNums.size()]; + int idx = 0; + for (int num : maxCntNums) { + ret[idx++] = num; + } + return ret; +} + +private void inOrder(TreeNode node, List nums) { + if (node == null) return; + inOrder(node.left, nums); + if (preNode != null) { + if (preNode.val == node.val) curCnt++; + else curCnt = 1; + } + if (curCnt > maxCnt) { + maxCnt = curCnt; + nums.clear(); + nums.add(node.val); + } else if (curCnt == maxCnt) { + nums.add(node.val); + } + preNode = node; + inOrder(node.right, nums); +} +``` + +# Trie + +

+ +Trie,又称前缀树或字典树,用于判断字符串是否存在或者是否具有某种字符串前缀。 + +## 实现一个 Trie + +[208. Implement Trie (Prefix Tree) (Medium)](https://leetcode.com/problems/implement-trie-prefix-tree/description/) + +```java +class Trie { + + private class Node { + Node[] childs = new Node[26]; + boolean isLeaf; + } + + private Node root = new Node(); + + public Trie() { + } + + public void insert(String word) { + insert(word, root); + } + + private void insert(String word, Node node) { + if (node == null) return; + if (word.length() == 0) { + node.isLeaf = true; + return; + } + int index = indexForChar(word.charAt(0)); + if (node.childs[index] == null) { + node.childs[index] = new Node(); + } + insert(word.substring(1), node.childs[index]); + } + + public boolean search(String word) { + return search(word, root); + } + + private boolean search(String word, Node node) { + if (node == null) return false; + if (word.length() == 0) return node.isLeaf; + int index = indexForChar(word.charAt(0)); + return search(word.substring(1), node.childs[index]); + } + + public boolean startsWith(String prefix) { + return startWith(prefix, root); + } + + private boolean startWith(String prefix, Node node) { + if (node == null) return false; + if (prefix.length() == 0) return true; + int index = indexForChar(prefix.charAt(0)); + return startWith(prefix.substring(1), node.childs[index]); + } + + private int indexForChar(char c) { + return c - 'a'; + } +} +``` + +## 实现一个 Trie,用来求前缀和 + +[677. Map Sum Pairs (Medium)](https://leetcode.com/problems/map-sum-pairs/description/) + +```html +Input: insert("apple", 3), Output: Null +Input: sum("ap"), Output: 3 +Input: insert("app", 2), Output: Null +Input: sum("ap"), Output: 5 +``` + +```java +class MapSum { + + private class Node { + Node[] child = new Node[26]; + int value; + } + + private Node root = new Node(); + + public MapSum() { + + } + + public void insert(String key, int val) { + insert(key, root, val); + } + + private void insert(String key, Node node, int val) { + if (node == null) return; + if (key.length() == 0) { + node.value = val; + return; + } + int index = indexForChar(key.charAt(0)); + if (node.child[index] == null) { + node.child[index] = new Node(); + } + insert(key.substring(1), node.child[index], val); + } + + public int sum(String prefix) { + return sum(prefix, root); + } + + private int sum(String prefix, Node node) { + if (node == null) return 0; + if (prefix.length() != 0) { + int index = indexForChar(prefix.charAt(0)); + return sum(prefix.substring(1), node.child[index]); + } + int sum = node.value; + for (Node child : node.child) { + sum += sum(prefix, child); + } + return sum; + } + + private int indexForChar(char c) { + return c - 'a'; + } +} +``` + diff --git a/docs/notes/Leetcode 题解 - 目录.md b/docs/notes/Leetcode 题解 - 目录.md new file mode 100644 index 00000000..ee3e594b --- /dev/null +++ b/docs/notes/Leetcode 题解 - 目录.md @@ -0,0 +1,39 @@ +* [点击阅读面试进阶指南 ](https://github.com/CyC2018/Backend-Interview-Guide) + +* [算法思想](#算法思想) +* [数据结构相关](#数据结构相关) +* [参考资料](#参考资料) + + + +# 算法思想 + +- [双指针](Leetcode%20题解%20-%20双指针.md) +- [排序](Leetcode%20题解%20-%20排序.md) +- [贪心思想](Leetcode%20题解%20-%20贪心思想.md) +- [二分查找](Leetcode%20题解%20-%20二分查找.md) +- [分治](Leetcode%20题解%20-%20分治.md) +- [搜索](Leetcode%20题解%20-%20搜索.md) +- [动态规划](Leetcode%20题解%20-%20动态规划.md) +- [数学](Leetcode%20题解%20-%20数学.md) + +# 数据结构相关 + +- [链表](Leetcode%20题解%20-%20链表.md) +- [树](Leetcode%20题解%20-%20树.md) +- [栈和队列](Leetcode%20题解%20-%20栈和队列.md) +- [哈希表](Leetcode%20题解%20-%20哈希表.md) +- [字符串](Leetcode%20题解%20-%20字符串.md) +- [数组与矩阵](Leetcode%20题解%20-%20数组与矩阵.md) +- [图](Leetcode%20题解%20-%20图.md) +- [位运算](Leetcode%20题解%20-%20位运算.md) + +# 参考资料 + + +- Leetcode +- Weiss M A, 冯舜玺. 数据结构与算法分析——C 语言描述[J]. 2004. +- Sedgewick R. Algorithms[M]. Pearson Education India, 1988. +- 何海涛, 软件工程师. 剑指 Offer: 名企面试官精讲典型编程题[M]. 电子工业出版社, 2014. +- 《编程之美》小组. 编程之美[M]. 电子工业出版社, 2008. +- 左程云. 程序员代码面试指南[M]. 电子工业出版社, 2015. diff --git a/docs/notes/Leetcode 题解 - 贪心思想.md b/docs/notes/Leetcode 题解 - 贪心思想.md new file mode 100644 index 00000000..e022b1df --- /dev/null +++ b/docs/notes/Leetcode 题解 - 贪心思想.md @@ -0,0 +1,367 @@ +* [点击阅读面试进阶指南 ](https://github.com/CyC2018/Backend-Interview-Guide) + +* [分配饼干](#分配饼干) +* [不重叠的区间个数](#不重叠的区间个数) +* [投飞镖刺破气球](#投飞镖刺破气球) +* [根据身高和序号重组队列](#根据身高和序号重组队列) +* [分隔字符串使同种字符出现在一起](#分隔字符串使同种字符出现在一起) +* [种植花朵](#种植花朵) +* [判断是否为子序列](#判断是否为子序列) +* [修改一个数成为非递减数组](#修改一个数成为非递减数组) +* [股票的最大收益](#股票的最大收益) +* [子数组最大的和](#子数组最大的和) +* [买入和售出股票最大的收益](#买入和售出股票最大的收益) + + + +保证每次操作都是局部最优的,并且最后得到的结果是全局最优的。 + +# 分配饼干 + +[455. Assign Cookies (Easy)](https://leetcode.com/problems/assign-cookies/description/) + +```html +Input: [1,2], [1,2,3] +Output: 2 + +Explanation: You have 2 children and 3 cookies. The greed factors of 2 children are 1, 2. +You have 3 cookies and their sizes are big enough to gratify all of the children, +You need to output 2. +``` + +题目描述:每个孩子都有一个满足度,每个饼干都有一个大小,只有饼干的大小大于等于一个孩子的满足度,该孩子才会获得满足。求解最多可以获得满足的孩子数量。 + +给一个孩子的饼干应当尽量小又能满足该孩子,这样大饼干就能拿来给满足度比较大的孩子。因为最小的孩子最容易得到满足,所以先满足最小的孩子。 + +证明:假设在某次选择中,贪心策略选择给当前满足度最小的孩子分配第 m 个饼干,第 m 个饼干为可以满足该孩子的最小饼干。假设存在一种最优策略,给该孩子分配第 n 个饼干,并且 m < n。我们可以发现,经过这一轮分配,贪心策略分配后剩下的饼干一定有一个比最优策略来得大。因此在后续的分配中,贪心策略一定能满足更多的孩子。也就是说不存在比贪心策略更优的策略,即贪心策略就是最优策略。 + +```java +public int findContentChildren(int[] g, int[] s) { + Arrays.sort(g); + Arrays.sort(s); + int gi = 0, si = 0; + while (gi < g.length && si < s.length) { + if (g[gi] <= s[si]) { + gi++; + } + si++; + } + return gi; +} +``` + +# 不重叠的区间个数 + +[435. Non-overlapping Intervals (Medium)](https://leetcode.com/problems/non-overlapping-intervals/description/) + +```html +Input: [ [1,2], [1,2], [1,2] ] + +Output: 2 + +Explanation: You need to remove two [1,2] to make the rest of intervals non-overlapping. +``` + +```html +Input: [ [1,2], [2,3] ] + +Output: 0 + +Explanation: You don't need to remove any of the intervals since they're already non-overlapping. +``` + +题目描述:计算让一组区间不重叠所需要移除的区间个数。 + +先计算最多能组成的不重叠区间个数,然后用区间总个数减去不重叠区间的个数。 + +在每次选择中,区间的结尾最为重要,选择的区间结尾越小,留给后面的区间的空间越大,那么后面能够选择的区间个数也就越大。 + +按区间的结尾进行排序,每次选择结尾最小,并且和前一个区间不重叠的区间。 + +```java +public int eraseOverlapIntervals(Interval[] intervals) { + if (intervals.length == 0) { + return 0; + } + Arrays.sort(intervals, Comparator.comparingInt(o -> o.end)); + int cnt = 1; + int end = intervals[0].end; + for (int i = 1; i < intervals.length; i++) { + if (intervals[i].start < end) { + continue; + } + end = intervals[i].end; + cnt++; + } + return intervals.length - cnt; +} +``` + +使用 lambda 表示式创建 Comparator 会导致算法运行时间过长,如果注重运行时间,可以修改为普通创建 Comparator 语句: + +```java +Arrays.sort(intervals, new Comparator() { + @Override + public int compare(Interval o1, Interval o2) { + return o1.end - o2.end; + } +}); +``` + +# 投飞镖刺破气球 + +[452. Minimum Number of Arrows to Burst Balloons (Medium)](https://leetcode.com/problems/minimum-number-of-arrows-to-burst-balloons/description/) + +``` +Input: +[[10,16], [2,8], [1,6], [7,12]] + +Output: +2 +``` + +题目描述:气球在一个水平数轴上摆放,可以重叠,飞镖垂直投向坐标轴,使得路径上的气球都会刺破。求解最小的投飞镖次数使所有气球都被刺破。 + +也是计算不重叠的区间个数,不过和 Non-overlapping Intervals 的区别在于,[1, 2] 和 [2, 3] 在本题中算是重叠区间。 + +```java +public int findMinArrowShots(int[][] points) { + if (points.length == 0) { + return 0; + } + Arrays.sort(points, Comparator.comparingInt(o -> o[1])); + int cnt = 1, end = points[0][1]; + for (int i = 1; i < points.length; i++) { + if (points[i][0] <= end) { + continue; + } + cnt++; + end = points[i][1]; + } + return cnt; +} +``` + +# 根据身高和序号重组队列 + +[406. Queue Reconstruction by Height(Medium)](https://leetcode.com/problems/queue-reconstruction-by-height/description/) + +```html +Input: +[[7,0], [4,4], [7,1], [5,0], [6,1], [5,2]] + +Output: +[[5,0], [7,0], [5,2], [6,1], [4,4], [7,1]] +``` + +题目描述:一个学生用两个分量 (h, k) 描述,h 表示身高,k 表示排在前面的有 k 个学生的身高比他高或者和他一样高。 + +为了使插入操作不影响后续的操作,身高较高的学生应该先做插入操作,否则身高较小的学生原先正确插入的第 k 个位置可能会变成第 k+1 个位置。 + +身高降序、k 值升序,然后按排好序的顺序插入队列的第 k 个位置中。 + +```java +public int[][] reconstructQueue(int[][] people) { + if (people == null || people.length == 0 || people[0].length == 0) { + return new int[0][0]; + } + Arrays.sort(people, (a, b) -> (a[0] == b[0] ? a[1] - b[1] : b[0] - a[0])); + List queue = new ArrayList<>(); + for (int[] p : people) { + queue.add(p[1], p); + } + return queue.toArray(new int[queue.size()][]); +} +``` + +# 分隔字符串使同种字符出现在一起 + +[763. Partition Labels (Medium)](https://leetcode.com/problems/partition-labels/description/) + +```html +Input: S = "ababcbacadefegdehijhklij" +Output: [9,7,8] +Explanation: +The partition is "ababcbaca", "defegde", "hijhklij". +This is a partition so that each letter appears in at most one part. +A partition like "ababcbacadefegde", "hijhklij" is incorrect, because it splits S into less parts. +``` + +```java +public List partitionLabels(String S) { + int[] lastIndexsOfChar = new int[26]; + for (int i = 0; i < S.length(); i++) { + lastIndexsOfChar[char2Index(S.charAt(i))] = i; + } + List partitions = new ArrayList<>(); + int firstIndex = 0; + while (firstIndex < S.length()) { + int lastIndex = firstIndex; + for (int i = firstIndex; i < S.length() && i <= lastIndex; i++) { + int index = lastIndexsOfChar[char2Index(S.charAt(i))]; + if (index > lastIndex) { + lastIndex = index; + } + } + partitions.add(lastIndex - firstIndex + 1); + firstIndex = lastIndex + 1; + } + return partitions; +} + +private int char2Index(char c) { + return c - 'a'; +} +``` + + +# 种植花朵 + +[605. Can Place Flowers (Easy)](https://leetcode.com/problems/can-place-flowers/description/) + +```html +Input: flowerbed = [1,0,0,0,1], n = 1 +Output: True +``` + +题目描述:花朵之间至少需要一个单位的间隔,求解是否能种下 n 朵花。 + +```java +public boolean canPlaceFlowers(int[] flowerbed, int n) { + int len = flowerbed.length; + int cnt = 0; + for (int i = 0; i < len && cnt < n; i++) { + if (flowerbed[i] == 1) { + continue; + } + int pre = i == 0 ? 0 : flowerbed[i - 1]; + int next = i == len - 1 ? 0 : flowerbed[i + 1]; + if (pre == 0 && next == 0) { + cnt++; + flowerbed[i] = 1; + } + } + return cnt >= n; +} +``` + +# 判断是否为子序列 + +[392. Is Subsequence (Medium)](https://leetcode.com/problems/is-subsequence/description/) + +```html +s = "abc", t = "ahbgdc" +Return true. +``` + +```java +public boolean isSubsequence(String s, String t) { + int index = -1; + for (char c : s.toCharArray()) { + index = t.indexOf(c, index + 1); + if (index == -1) { + return false; + } + } + return true; +} +``` + +# 修改一个数成为非递减数组 + +[665. Non-decreasing Array (Easy)](https://leetcode.com/problems/non-decreasing-array/description/) + +```html +Input: [4,2,3] +Output: True +Explanation: You could modify the first 4 to 1 to get a non-decreasing array. +``` + +题目描述:判断一个数组能不能只修改一个数就成为非递减数组。 + +在出现 nums[i] < nums[i - 1] 时,需要考虑的是应该修改数组的哪个数,使得本次修改能使 i 之前的数组成为非递减数组,并且 **不影响后续的操作** 。优先考虑令 nums[i - 1] = nums[i],因为如果修改 nums[i] = nums[i - 1] 的话,那么 nums[i] 这个数会变大,就有可能比 nums[i + 1] 大,从而影响了后续操作。还有一个比较特别的情况就是 nums[i] < nums[i - 2],只修改 nums[i - 1] = nums[i] 不能使数组成为非递减数组,只能修改 nums[i] = nums[i - 1]。 + +```java +public boolean checkPossibility(int[] nums) { + int cnt = 0; + for (int i = 1; i < nums.length && cnt < 2; i++) { + if (nums[i] >= nums[i - 1]) { + continue; + } + cnt++; + if (i - 2 >= 0 && nums[i - 2] > nums[i]) { + nums[i] = nums[i - 1]; + } else { + nums[i - 1] = nums[i]; + } + } + return cnt <= 1; +} +``` + +# 股票的最大收益 + +[122. Best Time to Buy and Sell Stock II (Easy)](https://leetcode.com/problems/best-time-to-buy-and-sell-stock-ii/description/) + +题目描述:一次股票交易包含买入和卖出,多个交易之间不能交叉进行。 + +对于 [a, b, c, d],如果有 a <= b <= c <= d ,那么最大收益为 d - a。而 d - a = (d - c) + (c - b) + (b - a) ,因此当访问到一个 prices[i] 且 prices[i] - prices[i-1] > 0,那么就把 prices[i] - prices[i-1] 添加到收益中,从而在局部最优的情况下也保证全局最优。 + +```java +public int maxProfit(int[] prices) { + int profit = 0; + for (int i = 1; i < prices.length; i++) { + if (prices[i] > prices[i - 1]) { + profit += (prices[i] - prices[i - 1]); + } + } + return profit; +} +``` + +# 子数组最大的和 + +[53. Maximum Subarray (Easy)](https://leetcode.com/problems/maximum-subarray/description/) + +```html +For example, given the array [-2,1,-3,4,-1,2,1,-5,4], +the contiguous subarray [4,-1,2,1] has the largest sum = 6. +``` + +```java +public int maxSubArray(int[] nums) { + if (nums == null || nums.length == 0) { + return 0; + } + int preSum = nums[0]; + int maxSum = preSum; + for (int i = 1; i < nums.length; i++) { + preSum = preSum > 0 ? preSum + nums[i] : nums[i]; + maxSum = Math.max(maxSum, preSum); + } + return maxSum; +} +``` + +# 买入和售出股票最大的收益 + +[121. Best Time to Buy and Sell Stock (Easy)](https://leetcode.com/problems/best-time-to-buy-and-sell-stock/description/) + +题目描述:只进行一次交易。 + +只要记录前面的最小价格,将这个最小价格作为买入价格,然后将当前的价格作为售出价格,查看当前收益是不是最大收益。 + +```java +public int maxProfit(int[] prices) { + int n = prices.length; + if (n == 0) return 0; + int soFarMin = prices[0]; + int max = 0; + for (int i = 1; i < n; i++) { + if (soFarMin > prices[i]) soFarMin = prices[i]; + else max = Math.max(max, prices[i] - soFarMin); + } + return max; +} +``` + diff --git a/docs/notes/Leetcode 题解 - 链表.md b/docs/notes/Leetcode 题解 - 链表.md new file mode 100644 index 00000000..f8e17044 --- /dev/null +++ b/docs/notes/Leetcode 题解 - 链表.md @@ -0,0 +1,328 @@ +* [点击阅读面试进阶指南 ](https://github.com/CyC2018/Backend-Interview-Guide) + +* [找出两个链表的交点](#找出两个链表的交点) +* [链表反转](#链表反转) +* [归并两个有序的链表](#归并两个有序的链表) +* [从有序链表中删除重复节点](#从有序链表中删除重复节点) +* [删除链表的倒数第 n 个节点](#删除链表的倒数第-n-个节点) +* [交换链表中的相邻结点](#交换链表中的相邻结点) +* [链表求和](#链表求和) +* [回文链表](#回文链表) +* [分隔链表](#分隔链表) +* [链表元素按奇偶聚集](#链表元素按奇偶聚集) + + + +链表是空节点,或者有一个值和一个指向下一个链表的指针,因此很多链表问题可以用递归来处理。 + +# 找出两个链表的交点 + +[160. Intersection of Two Linked Lists (Easy)](https://leetcode.com/problems/intersection-of-two-linked-lists/description/) + +```html +A: a1 → a2 + ↘ + c1 → c2 → c3 + ↗ +B: b1 → b2 → b3 +``` + +要求:时间复杂度为 O(N),空间复杂度为 O(1) + +设 A 的长度为 a + c,B 的长度为 b + c,其中 c 为尾部公共部分长度,可知 a + c + b = b + c + a。 + +当访问 A 链表的指针访问到链表尾部时,令它从链表 B 的头部开始访问链表 B;同样地,当访问 B 链表的指针访问到链表尾部时,令它从链表 A 的头部开始访问链表 A。这样就能控制访问 A 和 B 两个链表的指针能同时访问到交点。 + +```java +public ListNode getIntersectionNode(ListNode headA, ListNode headB) { + ListNode l1 = headA, l2 = headB; + while (l1 != l2) { + l1 = (l1 == null) ? headB : l1.next; + l2 = (l2 == null) ? headA : l2.next; + } + return l1; +} +``` + +如果只是判断是否存在交点,那么就是另一个问题,即 [编程之美 3.6]() 的问题。有两种解法: + +- 把第一个链表的结尾连接到第二个链表的开头,看第二个链表是否存在环; +- 或者直接比较两个链表的最后一个节点是否相同。 + +# 链表反转 + +[206. Reverse Linked List (Easy)](https://leetcode.com/problems/reverse-linked-list/description/) + +递归 + +```java +public ListNode reverseList(ListNode head) { + if (head == null || head.next == null) { + return head; + } + ListNode next = head.next; + ListNode newHead = reverseList(next); + next.next = head; + head.next = null; + return newHead; +} +``` + +头插法 + +```java +public ListNode reverseList(ListNode head) { + ListNode newHead = new ListNode(-1); + while (head != null) { + ListNode next = head.next; + head.next = newHead.next; + newHead.next = head; + head = next; + } + return newHead.next; +} +``` + +# 归并两个有序的链表 + +[21. Merge Two Sorted Lists (Easy)](https://leetcode.com/problems/merge-two-sorted-lists/description/) + +```java +public ListNode mergeTwoLists(ListNode l1, ListNode l2) { + if (l1 == null) return l2; + if (l2 == null) return l1; + if (l1.val < l2.val) { + l1.next = mergeTwoLists(l1.next, l2); + return l1; + } else { + l2.next = mergeTwoLists(l1, l2.next); + return l2; + } +} +``` + +# 从有序链表中删除重复节点 + +[83. Remove Duplicates from Sorted List (Easy)](https://leetcode.com/problems/remove-duplicates-from-sorted-list/description/) + +```html +Given 1->1->2, return 1->2. +Given 1->1->2->3->3, return 1->2->3. +``` + +```java +public ListNode deleteDuplicates(ListNode head) { + if (head == null || head.next == null) return head; + head.next = deleteDuplicates(head.next); + return head.val == head.next.val ? head.next : head; +} +``` + +# 删除链表的倒数第 n 个节点 + +[19. Remove Nth Node From End of List (Medium)](https://leetcode.com/problems/remove-nth-node-from-end-of-list/description/) + +```html +Given linked list: 1->2->3->4->5, and n = 2. +After removing the second node from the end, the linked list becomes 1->2->3->5. +``` + +```java +public ListNode removeNthFromEnd(ListNode head, int n) { + ListNode fast = head; + while (n-- > 0) { + fast = fast.next; + } + if (fast == null) return head.next; + ListNode slow = head; + while (fast.next != null) { + fast = fast.next; + slow = slow.next; + } + slow.next = slow.next.next; + return head; +} +``` + +# 交换链表中的相邻结点 + +[24. Swap Nodes in Pairs (Medium)](https://leetcode.com/problems/swap-nodes-in-pairs/description/) + +```html +Given 1->2->3->4, you should return the list as 2->1->4->3. +``` + +题目要求:不能修改结点的 val 值,O(1) 空间复杂度。 + +```java +public ListNode swapPairs(ListNode head) { + ListNode node = new ListNode(-1); + node.next = head; + ListNode pre = node; + while (pre.next != null && pre.next.next != null) { + ListNode l1 = pre.next, l2 = pre.next.next; + ListNode next = l2.next; + l1.next = next; + l2.next = l1; + pre.next = l2; + + pre = l1; + } + return node.next; +} +``` + +# 链表求和 + +[445. Add Two Numbers II (Medium)](https://leetcode.com/problems/add-two-numbers-ii/description/) + +```html +Input: (7 -> 2 -> 4 -> 3) + (5 -> 6 -> 4) +Output: 7 -> 8 -> 0 -> 7 +``` + +题目要求:不能修改原始链表。 + +```java +public ListNode addTwoNumbers(ListNode l1, ListNode l2) { + Stack l1Stack = buildStack(l1); + Stack l2Stack = buildStack(l2); + ListNode head = new ListNode(-1); + int carry = 0; + while (!l1Stack.isEmpty() || !l2Stack.isEmpty() || carry != 0) { + int x = l1Stack.isEmpty() ? 0 : l1Stack.pop(); + int y = l2Stack.isEmpty() ? 0 : l2Stack.pop(); + int sum = x + y + carry; + ListNode node = new ListNode(sum % 10); + node.next = head.next; + head.next = node; + carry = sum / 10; + } + return head.next; +} + +private Stack buildStack(ListNode l) { + Stack stack = new Stack<>(); + while (l != null) { + stack.push(l.val); + l = l.next; + } + return stack; +} +``` + +# 回文链表 + +[234. Palindrome Linked List (Easy)](https://leetcode.com/problems/palindrome-linked-list/description/) + +题目要求:以 O(1) 的空间复杂度来求解。 + +切成两半,把后半段反转,然后比较两半是否相等。 + +```java +public boolean isPalindrome(ListNode head) { + if (head == null || head.next == null) return true; + ListNode slow = head, fast = head.next; + while (fast != null && fast.next != null) { + slow = slow.next; + fast = fast.next.next; + } + if (fast != null) slow = slow.next; // 偶数节点,让 slow 指向下一个节点 + cut(head, slow); // 切成两个链表 + return isEqual(head, reverse(slow)); +} + +private void cut(ListNode head, ListNode cutNode) { + while (head.next != cutNode) { + head = head.next; + } + head.next = null; +} + +private ListNode reverse(ListNode head) { + ListNode newHead = null; + while (head != null) { + ListNode nextNode = head.next; + head.next = newHead; + newHead = head; + head = nextNode; + } + return newHead; +} + +private boolean isEqual(ListNode l1, ListNode l2) { + while (l1 != null && l2 != null) { + if (l1.val != l2.val) return false; + l1 = l1.next; + l2 = l2.next; + } + return true; +} +``` + +# 分隔链表 + +[725. Split Linked List in Parts(Medium)](https://leetcode.com/problems/split-linked-list-in-parts/description/) + +```html +Input: +root = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], k = 3 +Output: [[1, 2, 3, 4], [5, 6, 7], [8, 9, 10]] +Explanation: +The input has been split into consecutive parts with size difference at most 1, and earlier parts are a larger size than the later parts. +``` + +题目描述:把链表分隔成 k 部分,每部分的长度都应该尽可能相同,排在前面的长度应该大于等于后面的。 + +```java +public ListNode[] splitListToParts(ListNode root, int k) { + int N = 0; + ListNode cur = root; + while (cur != null) { + N++; + cur = cur.next; + } + int mod = N % k; + int size = N / k; + ListNode[] ret = new ListNode[k]; + cur = root; + for (int i = 0; cur != null && i < k; i++) { + ret[i] = cur; + int curSize = size + (mod-- > 0 ? 1 : 0); + for (int j = 0; j < curSize - 1; j++) { + cur = cur.next; + } + ListNode next = cur.next; + cur.next = null; + cur = next; + } + return ret; +} +``` + +# 链表元素按奇偶聚集 + +[328. Odd Even Linked List (Medium)](https://leetcode.com/problems/odd-even-linked-list/description/) + +```html +Example: +Given 1->2->3->4->5->NULL, +return 1->3->5->2->4->NULL. +``` + +```java +public ListNode oddEvenList(ListNode head) { + if (head == null) { + return head; + } + ListNode odd = head, even = head.next, evenHead = even; + while (even != null && even.next != null) { + odd.next = odd.next.next; + odd = odd.next; + even.next = even.next.next; + even = even.next; + } + odd.next = evenHead; + return head; +} +``` diff --git a/docs/notes/Leetcode 题解.md b/docs/notes/Leetcode 题解.md deleted file mode 100644 index ef41be44..00000000 --- a/docs/notes/Leetcode 题解.md +++ /dev/null @@ -1,7111 +0,0 @@ -* [点击阅读面试进阶指南 ](https://github.com/CyC2018/Backend-Interview-Guide) - -* [算法思想](#算法思想) - * [双指针](#双指针) - * [排序](#排序) - * [快速选择](#快速选择) - * [堆排序](#堆排序) - * [桶排序](#桶排序) - * [荷兰国旗问题](#荷兰国旗问题) - * [贪心思想](#贪心思想) - * [二分查找](#二分查找) - * [分治](#分治) - * [搜索](#搜索) - * [BFS](#bfs) - * [DFS](#dfs) - * [Backtracking](#backtracking) - * [动态规划](#动态规划) - * [斐波那契数列](#斐波那契数列) - * [矩阵路径](#矩阵路径) - * [数组区间](#数组区间) - * [分割整数](#分割整数) - * [最长递增子序列](#最长递增子序列) - * [最长公共子序列](#最长公共子序列) - * [0-1 背包](#0-1-背包) - * [股票交易](#股票交易) - * [字符串编辑](#字符串编辑) - * [数学](#数学) - * [素数](#素数) - * [最大公约数](#最大公约数) - * [进制转换](#进制转换) - * [阶乘](#阶乘) - * [字符串加法减法](#字符串加法减法) - * [相遇问题](#相遇问题) - * [多数投票问题](#多数投票问题) - * [其它](#其它) -* [数据结构相关](#数据结构相关) - * [链表](#链表) - * [树](#树) - * [递归](#递归) - * [层次遍历](#层次遍历) - * [前中后序遍历](#前中后序遍历) - * [BST](#bst) - * [Trie](#trie) - * [栈和队列](#栈和队列) - * [哈希表](#哈希表) - * [字符串](#字符串) - * [数组与矩阵](#数组与矩阵) - * [图](#图) - * [二分图](#二分图) - * [拓扑排序](#拓扑排序) - * [并查集](#并查集) - * [位运算](#位运算) -* [参考资料](#参考资料) - - - -# 算法思想 - -## 双指针 - -双指针主要用于遍历数组,两个指针指向不同的元素,从而协同完成任务。 - -**有序数组的 Two Sum** - -[Leetcode :167. Two Sum II - Input array is sorted (Easy)](https://leetcode.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 变大一些。 - -```java -public int[] twoSum(int[] numbers, int target) { - 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; -} -``` - -**两数平方和** - -[633. Sum of Square Numbers (Easy)](https://leetcode.com/problems/sum-of-square-numbers/description/) - -```html -Input: 5 -Output: True -Explanation: 1 * 1 + 2 * 2 = 5 -``` - -题目描述:判断一个数是否为两个数的平方和。 - -```java -public boolean judgeSquareSum(int c) { - int i = 0, j = (int) Math.sqrt(c); - while (i <= j) { - int powSum = i * i + j * j; - if (powSum == c) { - return true; - } else if (powSum > c) { - j--; - } else { - i++; - } - } - return false; -} -``` - -**反转字符串中的元音字符** - -[345. Reverse Vowels of a String (Easy)](https://leetcode.com/problems/reverse-vowels-of-a-string/description/) - -```html -Given s = "leetcode", return "leotcede". -``` - -使用双指针指向待反转的两个元音字符,一个指针从头向尾遍历,一个指针从尾到头遍历。 - -```java -private final static HashSet vowels = new HashSet<>(Arrays.asList('a', 'e', 'i', 'o', 'u', 'A', 'E', 'I', 'O', 'U')); - -public String reverseVowels(String s) { - 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); -} -``` - -**回文字符串** - -[680. Valid Palindrome II (Easy)](https://leetcode.com/problems/valid-palindrome-ii/description/) - -```html -Input: "abca" -Output: True -Explanation: You could delete the character 'c'. -``` - -题目描述:可以删除一个字符,判断是否能构成回文字符串。 - -```java -public boolean validPalindrome(String s) { - int i = -1, j = s.length(); - while (++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; -} -``` - -**归并两个有序数组** - -[88. Merge Sorted Array (Easy)](https://leetcode.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--]; - } - } -} -``` - -**判断链表是否存在环** - -[141. Linked List Cycle (Easy)](https://leetcode.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; -} -``` - -**最长子序列** - -[524. Longest Word in Dictionary through Deleting (Medium)](https://leetcode.com/problems/longest-word-in-dictionary-through-deleting/description/) - -``` -Input: -s = "abpcplea", d = ["ale","apple","monkey","plea"] - -Output: -"apple" -``` - -题目描述:删除 s 中的一些字符,使得它构成字符串列表 d 中的一个字符串,找出能构成的最长字符串。如果有多个相同长度的结果,返回字典序的最小字符串。 - -```java -public String findLongestWord(String s, List 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 (isValid(s, target)) { - longestWord = target; - } - } - return longestWord; -} - -private boolean isValid(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(); -} -``` - -## 排序 - -### 快速选择 - -用于求解 **Kth Element** 问题,使用快速排序的 partition() 进行实现。 - -需要先打乱数组,否则最坏情况下时间复杂度为 O(N2)。 - -### 堆排序 - -用于求解 **TopK Elements** 问题,通过维护一个大小为 K 的堆,堆中的元素就是 TopK Elements。 - -堆排序也可以用于求解 Kth Element 问题,堆顶元素就是 Kth Element。 - -快速选择也可以求解 TopK Elements 问题,因为找到 Kth Element 之后,再遍历一次数组,所有小于等于 Kth Element 的元素都是 TopK Elements。 - -可以看到,快速选择和堆排序都可以求解 Kth Element 和 TopK Elements 问题。 - -**Kth Element** - -[215. Kth Largest Element in an Array (Medium)](https://leetcode.com/problems/kth-largest-element-in-an-array/description/) - -题目描述:找到第 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 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; -} -``` - -### 桶排序 - -**出现频率最多的 k 个数** - -[347. Top K Frequent Elements (Medium)](https://leetcode.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 List topKFrequent(int[] nums, int k) { - Map frequencyForNum = new HashMap<>(); - for (int num : nums) { - frequencyForNum.put(num, frequencyForNum.getOrDefault(num, 0) + 1); - } - List[] 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 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())); - } - } - return topK; -} -``` - -**按照字符出现次数对字符串排序** - -[451. Sort Characters By Frequency (Medium)](https://leetcode.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 frequencyForNum = new HashMap<>(); - for (char c : s.toCharArray()) - frequencyForNum.put(c, frequencyForNum.getOrDefault(c, 0) + 1); - - List[] 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(); -} -``` - -### 荷兰国旗问题 - -荷兰国旗包含三种颜色:红、白、蓝。 - -有三种颜色的球,算法的目标是将这三种球按颜色顺序正确地排列。 - -它其实是三向切分快速排序的一种变种,在三向切分快速排序中,每次切分都将数组分成三个区间:小于切分元素、等于切分元素、大于切分元素,而该算法是将数组分成三个区间:等于红色、等于白色、等于蓝色。 - -

- -**按颜色进行排序** - -[75. Sort Colors (Medium)](https://leetcode.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; -} -``` - -## 贪心思想 - -保证每次操作都是局部最优的,并且最后得到的结果是全局最优的。 - -**分配饼干** - -[455. Assign Cookies (Easy)](https://leetcode.com/problems/assign-cookies/description/) - -```html -Input: [1,2], [1,2,3] -Output: 2 - -Explanation: You have 2 children and 3 cookies. The greed factors of 2 children are 1, 2. -You have 3 cookies and their sizes are big enough to gratify all of the children, -You need to output 2. -``` - -题目描述:每个孩子都有一个满足度,每个饼干都有一个大小,只有饼干的大小大于等于一个孩子的满足度,该孩子才会获得满足。求解最多可以获得满足的孩子数量。 - -给一个孩子的饼干应当尽量小又能满足该孩子,这样大饼干就能拿来给满足度比较大的孩子。因为最小的孩子最容易得到满足,所以先满足最小的孩子。 - -证明:假设在某次选择中,贪心策略选择给当前满足度最小的孩子分配第 m 个饼干,第 m 个饼干为可以满足该孩子的最小饼干。假设存在一种最优策略,给该孩子分配第 n 个饼干,并且 m < n。我们可以发现,经过这一轮分配,贪心策略分配后剩下的饼干一定有一个比最优策略来得大。因此在后续的分配中,贪心策略一定能满足更多的孩子。也就是说不存在比贪心策略更优的策略,即贪心策略就是最优策略。 - -```java -public int findContentChildren(int[] g, int[] s) { - Arrays.sort(g); - Arrays.sort(s); - int gi = 0, si = 0; - while (gi < g.length && si < s.length) { - if (g[gi] <= s[si]) { - gi++; - } - si++; - } - return gi; -} -``` - -**不重叠的区间个数** - -[435. Non-overlapping Intervals (Medium)](https://leetcode.com/problems/non-overlapping-intervals/description/) - -```html -Input: [ [1,2], [1,2], [1,2] ] - -Output: 2 - -Explanation: You need to remove two [1,2] to make the rest of intervals non-overlapping. -``` - -```html -Input: [ [1,2], [2,3] ] - -Output: 0 - -Explanation: You don't need to remove any of the intervals since they're already non-overlapping. -``` - -题目描述:计算让一组区间不重叠所需要移除的区间个数。 - -先计算最多能组成的不重叠区间个数,然后用区间总个数减去不重叠区间的个数。 - -在每次选择中,区间的结尾最为重要,选择的区间结尾越小,留给后面的区间的空间越大,那么后面能够选择的区间个数也就越大。 - -按区间的结尾进行排序,每次选择结尾最小,并且和前一个区间不重叠的区间。 - -```java -public int eraseOverlapIntervals(Interval[] intervals) { - if (intervals.length == 0) { - return 0; - } - Arrays.sort(intervals, Comparator.comparingInt(o -> o.end)); - int cnt = 1; - int end = intervals[0].end; - for (int i = 1; i < intervals.length; i++) { - if (intervals[i].start < end) { - continue; - } - end = intervals[i].end; - cnt++; - } - return intervals.length - cnt; -} -``` - -使用 lambda 表示式创建 Comparator 会导致算法运行时间过长,如果注重运行时间,可以修改为普通创建 Comparator 语句: - -```java -Arrays.sort(intervals, new Comparator() { - @Override - public int compare(Interval o1, Interval o2) { - return o1.end - o2.end; - } -}); -``` - -**投飞镖刺破气球** - -[452. Minimum Number of Arrows to Burst Balloons (Medium)](https://leetcode.com/problems/minimum-number-of-arrows-to-burst-balloons/description/) - -``` -Input: -[[10,16], [2,8], [1,6], [7,12]] - -Output: -2 -``` - -题目描述:气球在一个水平数轴上摆放,可以重叠,飞镖垂直投向坐标轴,使得路径上的气球都会刺破。求解最小的投飞镖次数使所有气球都被刺破。 - -也是计算不重叠的区间个数,不过和 Non-overlapping Intervals 的区别在于,[1, 2] 和 [2, 3] 在本题中算是重叠区间。 - -```java -public int findMinArrowShots(int[][] points) { - if (points.length == 0) { - return 0; - } - Arrays.sort(points, Comparator.comparingInt(o -> o[1])); - int cnt = 1, end = points[0][1]; - for (int i = 1; i < points.length; i++) { - if (points[i][0] <= end) { - continue; - } - cnt++; - end = points[i][1]; - } - return cnt; -} -``` - -**根据身高和序号重组队列** - -[406. Queue Reconstruction by Height(Medium)](https://leetcode.com/problems/queue-reconstruction-by-height/description/) - -```html -Input: -[[7,0], [4,4], [7,1], [5,0], [6,1], [5,2]] - -Output: -[[5,0], [7,0], [5,2], [6,1], [4,4], [7,1]] -``` - -题目描述:一个学生用两个分量 (h, k) 描述,h 表示身高,k 表示排在前面的有 k 个学生的身高比他高或者和他一样高。 - -为了使插入操作不影响后续的操作,身高较高的学生应该先做插入操作,否则身高较小的学生原先正确插入的第 k 个位置可能会变成第 k+1 个位置。 - -身高降序、k 值升序,然后按排好序的顺序插入队列的第 k 个位置中。 - -```java -public int[][] reconstructQueue(int[][] people) { - if (people == null || people.length == 0 || people[0].length == 0) { - return new int[0][0]; - } - Arrays.sort(people, (a, b) -> (a[0] == b[0] ? a[1] - b[1] : b[0] - a[0])); - List queue = new ArrayList<>(); - for (int[] p : people) { - queue.add(p[1], p); - } - return queue.toArray(new int[queue.size()][]); -} -``` - -**分隔字符串使同种字符出现在一起** - -[763. Partition Labels (Medium)](https://leetcode.com/problems/partition-labels/description/) - -```html -Input: S = "ababcbacadefegdehijhklij" -Output: [9,7,8] -Explanation: -The partition is "ababcbaca", "defegde", "hijhklij". -This is a partition so that each letter appears in at most one part. -A partition like "ababcbacadefegde", "hijhklij" is incorrect, because it splits S into less parts. -``` - -```java -public List partitionLabels(String S) { - int[] lastIndexsOfChar = new int[26]; - for (int i = 0; i < S.length(); i++) { - lastIndexsOfChar[char2Index(S.charAt(i))] = i; - } - List partitions = new ArrayList<>(); - int firstIndex = 0; - while (firstIndex < S.length()) { - int lastIndex = firstIndex; - for (int i = firstIndex; i < S.length() && i <= lastIndex; i++) { - int index = lastIndexsOfChar[char2Index(S.charAt(i))]; - if (index > lastIndex) { - lastIndex = index; - } - } - partitions.add(lastIndex - firstIndex + 1); - firstIndex = lastIndex + 1; - } - return partitions; -} - -private int char2Index(char c) { - return c - 'a'; -} -``` - - -**种植花朵** - -[605. Can Place Flowers (Easy)](https://leetcode.com/problems/can-place-flowers/description/) - -```html -Input: flowerbed = [1,0,0,0,1], n = 1 -Output: True -``` - -题目描述:花朵之间至少需要一个单位的间隔,求解是否能种下 n 朵花。 - -```java -public boolean canPlaceFlowers(int[] flowerbed, int n) { - int len = flowerbed.length; - int cnt = 0; - for (int i = 0; i < len && cnt < n; i++) { - if (flowerbed[i] == 1) { - continue; - } - int pre = i == 0 ? 0 : flowerbed[i - 1]; - int next = i == len - 1 ? 0 : flowerbed[i + 1]; - if (pre == 0 && next == 0) { - cnt++; - flowerbed[i] = 1; - } - } - return cnt >= n; -} -``` - -**判断是否为子序列** - -[392. Is Subsequence (Medium)](https://leetcode.com/problems/is-subsequence/description/) - -```html -s = "abc", t = "ahbgdc" -Return true. -``` - -```java -public boolean isSubsequence(String s, String t) { - int index = -1; - for (char c : s.toCharArray()) { - index = t.indexOf(c, index + 1); - if (index == -1) { - return false; - } - } - return true; -} -``` - -**修改一个数成为非递减数组** - -[665. Non-decreasing Array (Easy)](https://leetcode.com/problems/non-decreasing-array/description/) - -```html -Input: [4,2,3] -Output: True -Explanation: You could modify the first 4 to 1 to get a non-decreasing array. -``` - -题目描述:判断一个数组能不能只修改一个数就成为非递减数组。 - -在出现 nums[i] < nums[i - 1] 时,需要考虑的是应该修改数组的哪个数,使得本次修改能使 i 之前的数组成为非递减数组,并且 **不影响后续的操作** 。优先考虑令 nums[i - 1] = nums[i],因为如果修改 nums[i] = nums[i - 1] 的话,那么 nums[i] 这个数会变大,就有可能比 nums[i + 1] 大,从而影响了后续操作。还有一个比较特别的情况就是 nums[i] < nums[i - 2],只修改 nums[i - 1] = nums[i] 不能使数组成为非递减数组,只能修改 nums[i] = nums[i - 1]。 - -```java -public boolean checkPossibility(int[] nums) { - int cnt = 0; - for (int i = 1; i < nums.length && cnt < 2; i++) { - if (nums[i] >= nums[i - 1]) { - continue; - } - cnt++; - if (i - 2 >= 0 && nums[i - 2] > nums[i]) { - nums[i] = nums[i - 1]; - } else { - nums[i - 1] = nums[i]; - } - } - return cnt <= 1; -} -``` - -**股票的最大收益** - -[122. Best Time to Buy and Sell Stock II (Easy)](https://leetcode.com/problems/best-time-to-buy-and-sell-stock-ii/description/) - -题目描述:一次股票交易包含买入和卖出,多个交易之间不能交叉进行。 - -对于 [a, b, c, d],如果有 a <= b <= c <= d ,那么最大收益为 d - a。而 d - a = (d - c) + (c - b) + (b - a) ,因此当访问到一个 prices[i] 且 prices[i] - prices[i-1] > 0,那么就把 prices[i] - prices[i-1] 添加到收益中,从而在局部最优的情况下也保证全局最优。 - -```java -public int maxProfit(int[] prices) { - int profit = 0; - for (int i = 1; i < prices.length; i++) { - if (prices[i] > prices[i - 1]) { - profit += (prices[i] - prices[i - 1]); - } - } - return profit; -} -``` - -**子数组最大的和** - -[53. Maximum Subarray (Easy)](https://leetcode.com/problems/maximum-subarray/description/) - -```html -For example, given the array [-2,1,-3,4,-1,2,1,-5,4], -the contiguous subarray [4,-1,2,1] has the largest sum = 6. -``` - -```java -public int maxSubArray(int[] nums) { - if (nums == null || nums.length == 0) { - return 0; - } - int preSum = nums[0]; - int maxSum = preSum; - for (int i = 1; i < nums.length; i++) { - preSum = preSum > 0 ? preSum + nums[i] : nums[i]; - maxSum = Math.max(maxSum, preSum); - } - return maxSum; -} -``` - -**买入和售出股票最大的收益** - -[121. Best Time to Buy and Sell Stock (Easy)](https://leetcode.com/problems/best-time-to-buy-and-sell-stock/description/) - -题目描述:只进行一次交易。 - -只要记录前面的最小价格,将这个最小价格作为买入价格,然后将当前的价格作为售出价格,查看当前收益是不是最大收益。 - -```java -public int maxProfit(int[] prices) { - int n = prices.length; - if (n == 0) return 0; - int soFarMin = prices[0]; - int max = 0; - for (int i = 1; i < n; i++) { - if (soFarMin > prices[i]) soFarMin = prices[i]; - else max = Math.max(max, prices[i] - soFarMin); - } - return max; -} -``` - -## 二分查找 - -**正常实现** - -```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 可能出现加法溢出,最好使用第二种方式。 - -**返回值** - -循环退出时如果仍然没有查找到 key,那么表示查找失败。可以有两种返回值: - -- -1:以一个错误码表示没有查找到 key -- l:将 key 插入到 nums 中的正确位置 - -**变种** - -二分查找可以有很多变种,变种实现要注意边界值的判断。例如在一个有重复元素的数组中查找 key 的最左位置的实现如下: - -```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) { - h = m; - } else { - l = m + 1; - } - } - return l; -} -``` - -该实现和正常实现有以下不同: - -- 循环条件为 l < h -- h 的赋值表达式为 h = m -- 最后返回 l 而不是 -1 - -在 nums[m] >= key 的情况下,可以推导出最左 key 位于 [l, m] 区间中,这是一个闭区间。h 的赋值表达式为 h = m,因为 m 位置也可能是解。 - -在 h 的赋值表达式为 h = mid 的情况下,如果循环条件为 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 是否相等。 - -**求开方** - -[69. Sqrt(x) (Easy)](https://leetcode.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; -} -``` - -**大于给定元素的最小元素** - -[744. Find Smallest Letter Greater Than Target (Easy)](https://leetcode.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]; -} -``` - -**有序数组的 Single Element** - -[540. Single Element in a Sorted Array (Medium)](https://leetcode.com/problems/single-element-in-a-sorted-array/description/) - -```html -Input: [1, 1, 2, 3, 3, 4, 4, 8, 8] -Output: 2 -``` - -题目描述:一个有序数组只有一个数不出现两次,找出这个数。要求以 O(logN) 时间复杂度进行求解。 - -令 index 为 Single Element 在数组中的位置。如果 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]; -} -``` - -**第一个错误的版本** - -[278. First Bad Version (Easy)](https://leetcode.com/problems/first-bad-version/description/) - -题目描述:给定一个元素 n 代表有 [1, 2, ..., n] 版本,可以调用 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; -} -``` - -**旋转数组的最小数字** - -[153. Find Minimum in Rotated Sorted Array (Medium)](https://leetcode.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]; -} -``` - -**查找区间** - -[34. Search for a Range (Medium)](https://leetcode.com/problems/search-for-a-range/description/) - -```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] -``` - -```java -public int[] searchRange(int[] nums, int target) { - int first = binarySearch(nums, target); - int last = binarySearch(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 binarySearch(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; -} -``` - -## 分治 - -**给表达式加括号** - -[241. Different Ways to Add Parentheses (Medium)](https://leetcode.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 diffWaysToCompute(String input) { - List ways = new ArrayList<>(); - for (int i = 0; i < input.length(); i++) { - char c = input.charAt(i); - if (c == '+' || c == '-' || c == '*') { - List left = diffWaysToCompute(input.substring(0, i)); - List 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; -} -``` - -## 搜索 - -深度优先搜索和广度优先搜索广泛运用于树和图中,但是它们的应用远远不止如此。 - -### BFS - -

- -广度优先搜索一层一层地进行遍历,每层遍历都以上一层遍历的结果作为起点,遍历一个距离能访问到的所有节点。需要注意的是,遍历过的节点不能再次被遍历。 - -第一层: - -- 0 -> {6,2,1,5} - -第二层: - -- 6 -> {4} -- 2 -> {} -- 1 -> {} -- 5 -> {3} - -第三层: - -- 4 -> {} -- 3 -> {} - -每一层遍历的节点都与根节点距离相同。设 di 表示第 i 个节点与根节点的距离,推导出一个结论:对于先遍历的节点 i 与后遍历的节点 j,有 di <= dj。利用这个结论,可以求解最短路径等 **最优解** 问题:第一次遍历到目的节点,其所经过的路径为最短路径。应该注意的是,使用 BFS 只能求解无权图的最短路径。 - -在程序实现 BFS 时需要考虑以下问题: - -- 队列:用来存储每一轮遍历得到的节点; -- 标记:对于遍历过的节点,应该将它标记,防止重复遍历。 - -**计算在网格中从原点到特定点的最短路径长度** - -```html -[[1,1,0,1], - [1,0,1,0], - [1,1,1,1], - [1,0,1,1]] -``` - -1 表示可以经过某个位置,求解从 (0, 0) 位置到 (tr, tc) 位置的最短路径长度。 - -```java -public int minPathLength(int[][] grids, int tr, int tc) { - final int[][] direction = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}}; - final int m = grids.length, n = grids[0].length; - Queue> queue = new LinkedList<>(); - queue.add(new Pair<>(0, 0)); - int pathLength = 0; - while (!queue.isEmpty()) { - int size = queue.size(); - pathLength++; - while (size-- > 0) { - Pair cur = queue.poll(); - int cr = cur.getKey(), cc = cur.getValue(); - grids[cr][cc] = 0; // 标记 - for (int[] d : direction) { - int nr = cr + d[0], nc = cc + d[1]; - if (nr < 0 || nr >= m || nc < 0 || nc >= n || grids[nr][nc] == 0) { - continue; - } - if (nr == tr && nc == tc) { - return pathLength; - } - queue.add(new Pair<>(nr, nc)); - } - } - } - return -1; -} -``` - -**组成整数的最小平方数数量** - -[279. Perfect Squares (Medium)](https://leetcode.com/problems/perfect-squares/description/) - -```html -For example, given n = 12, return 3 because 12 = 4 + 4 + 4; given n = 13, return 2 because 13 = 4 + 9. -``` - -可以将每个整数看成图中的一个节点,如果两个整数之差为一个平方数,那么这两个整数所在的节点就有一条边。 - -要求解最小的平方数数量,就是求解从节点 n 到节点 0 的最短路径。 - -本题也可以用动态规划求解,在之后动态规划部分中会再次出现。 - -```java -public int numSquares(int n) { - List squares = generateSquares(n); - Queue queue = new LinkedList<>(); - boolean[] marked = new boolean[n + 1]; - queue.add(n); - marked[n] = true; - int level = 0; - while (!queue.isEmpty()) { - int size = queue.size(); - level++; - while (size-- > 0) { - int cur = queue.poll(); - for (int s : squares) { - int next = cur - s; - if (next < 0) { - break; - } - if (next == 0) { - return level; - } - if (marked[next]) { - continue; - } - marked[next] = true; - queue.add(next); - } - } - } - return n; -} - -/** - * 生成小于 n 的平方数序列 - * @return 1,4,9,... - */ -private List generateSquares(int n) { - List squares = new ArrayList<>(); - int square = 1; - int diff = 3; - while (square <= n) { - squares.add(square); - square += diff; - diff += 2; - } - return squares; -} -``` - -**最短单词路径** - -[127. Word Ladder (Medium)](https://leetcode.com/problems/word-ladder/description/) - -```html -Input: -beginWord = "hit", -endWord = "cog", -wordList = ["hot","dot","dog","lot","log","cog"] - -Output: 5 - -Explanation: As one shortest transformation is "hit" -> "hot" -> "dot" -> "dog" -> "cog", -return its length 5. -``` - -```html -Input: -beginWord = "hit" -endWord = "cog" -wordList = ["hot","dot","dog","lot","log"] - -Output: 0 - -Explanation: The endWord "cog" is not in wordList, therefore no possible transformation. -``` - -题目描述:找出一条从 beginWord 到 endWord 的最短路径,每次移动规定为改变一个字符,并且改变之后的字符串必须在 wordList 中。 - -```java -public int ladderLength(String beginWord, String endWord, List wordList) { - wordList.add(beginWord); - int N = wordList.size(); - int start = N - 1; - int end = 0; - while (end < N && !wordList.get(end).equals(endWord)) { - end++; - } - if (end == N) { - return 0; - } - List[] graphic = buildGraphic(wordList); - return getShortestPath(graphic, start, end); -} - -private List[] buildGraphic(List wordList) { - int N = wordList.size(); - List[] graphic = new List[N]; - for (int i = 0; i < N; i++) { - graphic[i] = new ArrayList<>(); - for (int j = 0; j < N; j++) { - if (isConnect(wordList.get(i), wordList.get(j))) { - graphic[i].add(j); - } - } - } - return graphic; -} - -private boolean isConnect(String s1, String s2) { - int diffCnt = 0; - for (int i = 0; i < s1.length() && diffCnt <= 1; i++) { - if (s1.charAt(i) != s2.charAt(i)) { - diffCnt++; - } - } - return diffCnt == 1; -} - -private int getShortestPath(List[] graphic, int start, int end) { - Queue queue = new LinkedList<>(); - boolean[] marked = new boolean[graphic.length]; - queue.add(start); - marked[start] = true; - int path = 1; - while (!queue.isEmpty()) { - int size = queue.size(); - path++; - while (size-- > 0) { - int cur = queue.poll(); - for (int next : graphic[cur]) { - if (next == end) { - return path; - } - if (marked[next]) { - continue; - } - marked[next] = true; - queue.add(next); - } - } - } - return 0; -} -``` - -### DFS - -

- -广度优先搜索一层一层遍历,每一层得到的所有新节点,要用队列存储起来以备下一层遍历的时候再遍历。 - -而深度优先搜索在得到一个新节点时立即对新节点进行遍历:从节点 0 出发开始遍历,得到到新节点 6 时,立马对新节点 6 进行遍历,得到新节点 4;如此反复以这种方式遍历新节点,直到没有新节点了,此时返回。返回到根节点 0 的情况是,继续对根节点 0 进行遍历,得到新节点 2,然后继续以上步骤。 - -从一个节点出发,使用 DFS 对一个图进行遍历时,能够遍历到的节点都是从初始节点可达的,DFS 常用来求解这种 **可达性** 问题。 - -在程序实现 DFS 时需要考虑以下问题: - -- 栈:用栈来保存当前节点信息,当遍历新节点返回时能够继续遍历当前节点。可以使用递归栈。 -- 标记:和 BFS 一样同样需要对已经遍历过的节点进行标记。 - -**查找最大的连通面积** - -[695. Max Area of Island (Easy)](https://leetcode.com/problems/max-area-of-island/description/) - -```html -[[0,0,1,0,0,0,0,1,0,0,0,0,0], - [0,0,0,0,0,0,0,1,1,1,0,0,0], - [0,1,1,0,1,0,0,0,0,0,0,0,0], - [0,1,0,0,1,1,0,0,1,0,1,0,0], - [0,1,0,0,1,1,0,0,1,1,1,0,0], - [0,0,0,0,0,0,0,0,0,0,1,0,0], - [0,0,0,0,0,0,0,1,1,1,0,0,0], - [0,0,0,0,0,0,0,1,1,0,0,0,0]] -``` - -```java -private int m, n; -private int[][] direction = {{0, 1}, {0, -1}, {1, 0}, {-1, 0}}; - -public int maxAreaOfIsland(int[][] grid) { - if (grid == null || grid.length == 0) { - return 0; - } - m = grid.length; - n = grid[0].length; - int maxArea = 0; - for (int i = 0; i < m; i++) { - for (int j = 0; j < n; j++) { - maxArea = Math.max(maxArea, dfs(grid, i, j)); - } - } - return maxArea; -} - -private int dfs(int[][] grid, int r, int c) { - if (r < 0 || r >= m || c < 0 || c >= n || grid[r][c] == 0) { - return 0; - } - grid[r][c] = 0; - int area = 1; - for (int[] d : direction) { - area += dfs(grid, r + d[0], c + d[1]); - } - return area; -} -``` - -**矩阵中的连通分量数目** - -[200. Number of Islands (Medium)](https://leetcode.com/problems/number-of-islands/description/) - -```html -Input: -11000 -11000 -00100 -00011 - -Output: 3 -``` - -可以将矩阵表示看成一张有向图。 - -```java -private int m, n; -private int[][] direction = {{0, 1}, {0, -1}, {1, 0}, {-1, 0}}; - -public int numIslands(char[][] grid) { - if (grid == null || grid.length == 0) { - return 0; - } - m = grid.length; - n = grid[0].length; - int islandsNum = 0; - for (int i = 0; i < m; i++) { - for (int j = 0; j < n; j++) { - if (grid[i][j] != '0') { - dfs(grid, i, j); - islandsNum++; - } - } - } - return islandsNum; -} - -private void dfs(char[][] grid, int i, int j) { - if (i < 0 || i >= m || j < 0 || j >= n || grid[i][j] == '0') { - return; - } - grid[i][j] = '0'; - for (int[] d : direction) { - dfs(grid, i + d[0], j + d[1]); - } -} -``` - -**好友关系的连通分量数目** - -[547. Friend Circles (Medium)](https://leetcode.com/problems/friend-circles/description/) - -```html -Input: -[[1,1,0], - [1,1,0], - [0,0,1]] - -Output: 2 - -Explanation:The 0th and 1st students are direct friends, so they are in a friend circle. -The 2nd student himself is in a friend circle. So return 2. -``` - -题目描述:好友关系可以看成是一个无向图,例如第 0 个人与第 1 个人是好友,那么 M[0][1] 和 M[1][0] 的值都为 1。 - -```java -private int n; - -public int findCircleNum(int[][] M) { - n = M.length; - int circleNum = 0; - boolean[] hasVisited = new boolean[n]; - for (int i = 0; i < n; i++) { - if (!hasVisited[i]) { - dfs(M, i, hasVisited); - circleNum++; - } - } - return circleNum; -} - -private void dfs(int[][] M, int i, boolean[] hasVisited) { - hasVisited[i] = true; - for (int k = 0; k < n; k++) { - if (M[i][k] == 1 && !hasVisited[k]) { - dfs(M, k, hasVisited); - } - } -} -``` - -**填充封闭区域** - -[130. Surrounded Regions (Medium)](https://leetcode.com/problems/surrounded-regions/description/) - -```html -For example, -X X X X -X O O X -X X O X -X O X X - -After running your function, the board should be: -X X X X -X X X X -X X X X -X O X X -``` - -题目描述:使被 'X' 包围的 'O' 转换为 'X'。 - -先填充最外侧,剩下的就是里侧了。 - -```java -private int[][] direction = {{0, 1}, {0, -1}, {1, 0}, {-1, 0}}; -private int m, n; - -public void solve(char[][] board) { - if (board == null || board.length == 0) { - return; - } - - m = board.length; - n = board[0].length; - - for (int i = 0; i < m; i++) { - dfs(board, i, 0); - dfs(board, i, n - 1); - } - for (int i = 0; i < n; i++) { - dfs(board, 0, i); - dfs(board, m - 1, i); - } - - for (int i = 0; i < m; i++) { - for (int j = 0; j < n; j++) { - if (board[i][j] == 'T') { - board[i][j] = 'O'; - } else if (board[i][j] == 'O') { - board[i][j] = 'X'; - } - } - } -} - -private void dfs(char[][] board, int r, int c) { - if (r < 0 || r >= m || c < 0 || c >= n || board[r][c] != 'O') { - return; - } - board[r][c] = 'T'; - for (int[] d : direction) { - dfs(board, r + d[0], c + d[1]); - } -} -``` - -**能到达的太平洋和大西洋的区域** - -[417. Pacific Atlantic Water Flow (Medium)](https://leetcode.com/problems/pacific-atlantic-water-flow/description/) - -```html -Given the following 5x5 matrix: - - Pacific ~ ~ ~ ~ ~ - ~ 1 2 2 3 (5) * - ~ 3 2 3 (4) (4) * - ~ 2 4 (5) 3 1 * - ~ (6) (7) 1 4 5 * - ~ (5) 1 1 2 4 * - * * * * * Atlantic - -Return: -[[0, 4], [1, 3], [1, 4], [2, 2], [3, 0], [3, 1], [4, 0]] (positions with parentheses in above matrix). -``` - -左边和上边是太平洋,右边和下边是大西洋,内部的数字代表海拔,海拔高的地方的水能够流到低的地方,求解水能够流到太平洋和大西洋的所有位置。 - -```java - -private int m, n; -private int[][] matrix; -private int[][] direction = {{0, 1}, {0, -1}, {1, 0}, {-1, 0}}; - -public List pacificAtlantic(int[][] matrix) { - List ret = new ArrayList<>(); - if (matrix == null || matrix.length == 0) { - return ret; - } - - m = matrix.length; - n = matrix[0].length; - this.matrix = matrix; - boolean[][] canReachP = new boolean[m][n]; - boolean[][] canReachA = new boolean[m][n]; - - for (int i = 0; i < m; i++) { - dfs(i, 0, canReachP); - dfs(i, n - 1, canReachA); - } - for (int i = 0; i < n; i++) { - dfs(0, i, canReachP); - dfs(m - 1, i, canReachA); - } - - for (int i = 0; i < m; i++) { - for (int j = 0; j < n; j++) { - if (canReachP[i][j] && canReachA[i][j]) { - ret.add(new int[]{i, j}); - } - } - } - - return ret; -} - -private void dfs(int r, int c, boolean[][] canReach) { - if (canReach[r][c]) { - return; - } - canReach[r][c] = true; - for (int[] d : direction) { - int nextR = d[0] + r; - int nextC = d[1] + c; - if (nextR < 0 || nextR >= m || nextC < 0 || nextC >= n - || matrix[r][c] > matrix[nextR][nextC]) { - - continue; - } - dfs(nextR, nextC, canReach); - } -} -``` - -### Backtracking - -Backtracking(回溯)属于 DFS。 - -- 普通 DFS 主要用在 **可达性问题** ,这种问题只需要执行到特点的位置然后返回即可。 -- 而 Backtracking 主要用于求解 **排列组合** 问题,例如有 { 'a','b','c' } 三个字符,求解所有由这三个字符排列得到的字符串,这种问题在执行到特定的位置返回之后还会继续执行求解过程。 - -因为 Backtracking 不是立即就返回,而要继续求解,因此在程序实现时,需要注意对元素的标记问题: - -- 在访问一个新元素进入新的递归调用时,需要将新元素标记为已经访问,这样才能在继续递归调用时不用重复访问该元素; -- 但是在递归返回时,需要将元素标记为未访问,因为只需要保证在一个递归链中不同时访问一个元素,可以访问已经访问过但是不在当前递归链中的元素。 - -**数字键盘组合** - -[17. Letter Combinations of a Phone Number (Medium)](https://leetcode.com/problems/letter-combinations-of-a-phone-number/description/) - -

- -```html -Input:Digit string "23" -Output: ["ad", "ae", "af", "bd", "be", "bf", "cd", "ce", "cf"]. -``` - -```java -private static final String[] KEYS = {"", "", "abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz"}; - -public List letterCombinations(String digits) { - List combinations = new ArrayList<>(); - if (digits == null || digits.length() == 0) { - return combinations; - } - doCombination(new StringBuilder(), combinations, digits); - return combinations; -} - -private void doCombination(StringBuilder prefix, List combinations, final String digits) { - if (prefix.length() == digits.length()) { - combinations.add(prefix.toString()); - return; - } - int curDigits = digits.charAt(prefix.length()) - '0'; - String letters = KEYS[curDigits]; - for (char c : letters.toCharArray()) { - prefix.append(c); // 添加 - doCombination(prefix, combinations, digits); - prefix.deleteCharAt(prefix.length() - 1); // 删除 - } -} -``` - -**IP 地址划分** - -[93. Restore IP Addresses(Medium)](https://leetcode.com/problems/restore-ip-addresses/description/) - -```html -Given "25525511135", -return ["255.255.11.135", "255.255.111.35"]. -``` - -```java -public List restoreIpAddresses(String s) { - List addresses = new ArrayList<>(); - StringBuilder tempAddress = new StringBuilder(); - doRestore(0, tempAddress, addresses, s); - return addresses; -} - -private void doRestore(int k, StringBuilder tempAddress, List addresses, String s) { - if (k == 4 || s.length() == 0) { - if (k == 4 && s.length() == 0) { - addresses.add(tempAddress.toString()); - } - return; - } - for (int i = 0; i < s.length() && i <= 2; i++) { - if (i != 0 && s.charAt(0) == '0') { - break; - } - String part = s.substring(0, i + 1); - if (Integer.valueOf(part) <= 255) { - if (tempAddress.length() != 0) { - part = "." + part; - } - tempAddress.append(part); - doRestore(k + 1, tempAddress, addresses, s.substring(i + 1)); - tempAddress.delete(tempAddress.length() - part.length(), tempAddress.length()); - } - } -} -``` - -**在矩阵中寻找字符串** - -[79. Word Search (Medium)](https://leetcode.com/problems/word-search/description/) - -```html -For example, -Given board = -[ - ['A','B','C','E'], - ['S','F','C','S'], - ['A','D','E','E'] -] -word = "ABCCED", -> returns true, -word = "SEE", -> returns true, -word = "ABCB", -> returns false. -``` - -```java -private final static int[][] direction = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}}; -private int m; -private int n; - -public boolean exist(char[][] board, String word) { - if (word == null || word.length() == 0) { - return true; - } - if (board == null || board.length == 0 || board[0].length == 0) { - return false; - } - - m = board.length; - n = board[0].length; - boolean[][] hasVisited = new boolean[m][n]; - - for (int r = 0; r < m; r++) { - for (int c = 0; c < n; c++) { - if (backtracking(0, r, c, hasVisited, board, word)) { - return true; - } - } - } - - return false; -} - -private boolean backtracking(int curLen, int r, int c, boolean[][] visited, final char[][] board, final String word) { - if (curLen == word.length()) { - return true; - } - if (r < 0 || r >= m || c < 0 || c >= n - || board[r][c] != word.charAt(curLen) || visited[r][c]) { - - return false; - } - - visited[r][c] = true; - - for (int[] d : direction) { - if (backtracking(curLen + 1, r + d[0], c + d[1], visited, board, word)) { - return true; - } - } - - visited[r][c] = false; - - return false; -} -``` - -**输出二叉树中所有从根到叶子的路径** - -[257. Binary Tree Paths (Easy)](https://leetcode.com/problems/binary-tree-paths/description/) - -```html - 1 - / \ -2 3 - \ - 5 -``` - -```html -["1->2->5", "1->3"] -``` - -```java - -public List binaryTreePaths(TreeNode root) { - List paths = new ArrayList<>(); - if (root == null) { - return paths; - } - List values = new ArrayList<>(); - backtracking(root, values, paths); - return paths; -} - -private void backtracking(TreeNode node, List values, List paths) { - if (node == null) { - return; - } - values.add(node.val); - if (isLeaf(node)) { - paths.add(buildPath(values)); - } else { - backtracking(node.left, values, paths); - backtracking(node.right, values, paths); - } - values.remove(values.size() - 1); -} - -private boolean isLeaf(TreeNode node) { - return node.left == null && node.right == null; -} - -private String buildPath(List values) { - StringBuilder str = new StringBuilder(); - for (int i = 0; i < values.size(); i++) { - str.append(values.get(i)); - if (i != values.size() - 1) { - str.append("->"); - } - } - return str.toString(); -} -``` - -**排列** - -[46. Permutations (Medium)](https://leetcode.com/problems/permutations/description/) - -```html -[1,2,3] have the following permutations: -[ - [1,2,3], - [1,3,2], - [2,1,3], - [2,3,1], - [3,1,2], - [3,2,1] -] -``` - -```java -public List> permute(int[] nums) { - List> permutes = new ArrayList<>(); - List permuteList = new ArrayList<>(); - boolean[] hasVisited = new boolean[nums.length]; - backtracking(permuteList, permutes, hasVisited, nums); - return permutes; -} - -private void backtracking(List permuteList, List> permutes, boolean[] visited, final int[] nums) { - if (permuteList.size() == nums.length) { - permutes.add(new ArrayList<>(permuteList)); // 重新构造一个 List - return; - } - for (int i = 0; i < visited.length; i++) { - if (visited[i]) { - continue; - } - visited[i] = true; - permuteList.add(nums[i]); - backtracking(permuteList, permutes, visited, nums); - permuteList.remove(permuteList.size() - 1); - visited[i] = false; - } -} -``` - -**含有相同元素求排列** - -[47. Permutations II (Medium)](https://leetcode.com/problems/permutations-ii/description/) - -```html -[1,1,2] have the following unique permutations: -[[1,1,2], [1,2,1], [2,1,1]] -``` - -数组元素可能含有相同的元素,进行排列时就有可能出现重复的排列,要求重复的排列只返回一个。 - -在实现上,和 Permutations 不同的是要先排序,然后在添加一个元素时,判断这个元素是否等于前一个元素,如果等于,并且前一个元素还未访问,那么就跳过这个元素。 - -```java -public List> permuteUnique(int[] nums) { - List> permutes = new ArrayList<>(); - List permuteList = new ArrayList<>(); - Arrays.sort(nums); // 排序 - boolean[] hasVisited = new boolean[nums.length]; - backtracking(permuteList, permutes, hasVisited, nums); - return permutes; -} - -private void backtracking(List permuteList, List> permutes, boolean[] visited, final int[] nums) { - if (permuteList.size() == nums.length) { - permutes.add(new ArrayList<>(permuteList)); - return; - } - - for (int i = 0; i < visited.length; i++) { - if (i != 0 && nums[i] == nums[i - 1] && !visited[i - 1]) { - continue; // 防止重复 - } - if (visited[i]){ - continue; - } - visited[i] = true; - permuteList.add(nums[i]); - backtracking(permuteList, permutes, visited, nums); - permuteList.remove(permuteList.size() - 1); - visited[i] = false; - } -} -``` - -**组合** - -[77. Combinations (Medium)](https://leetcode.com/problems/combinations/description/) - -```html -If n = 4 and k = 2, a solution is: -[ - [2,4], - [3,4], - [2,3], - [1,2], - [1,3], - [1,4], -] -``` - -```java -public List> combine(int n, int k) { - List> combinations = new ArrayList<>(); - List combineList = new ArrayList<>(); - backtracking(combineList, combinations, 1, k, n); - return combinations; -} - -private void backtracking(List combineList, List> combinations, int start, int k, final int n) { - if (k == 0) { - combinations.add(new ArrayList<>(combineList)); - return; - } - for (int i = start; i <= n - k + 1; i++) { // 剪枝 - combineList.add(i); - backtracking(combineList, combinations, i + 1, k - 1, n); - combineList.remove(combineList.size() - 1); - } -} -``` - -**组合求和** - -[39. Combination Sum (Medium)](https://leetcode.com/problems/combination-sum/description/) - -```html -given candidate set [2, 3, 6, 7] and target 7, -A solution set is: -[[7],[2, 2, 3]] -``` - -```java -public List> combinationSum(int[] candidates, int target) { - List> combinations = new ArrayList<>(); - backtracking(new ArrayList<>(), combinations, 0, target, candidates); - return combinations; -} - -private void backtracking(List tempCombination, List> combinations, - int start, int target, final int[] candidates) { - - if (target == 0) { - combinations.add(new ArrayList<>(tempCombination)); - return; - } - for (int i = start; i < candidates.length; i++) { - if (candidates[i] <= target) { - tempCombination.add(candidates[i]); - backtracking(tempCombination, combinations, i, target - candidates[i], candidates); - tempCombination.remove(tempCombination.size() - 1); - } - } -} -``` - -**含有相同元素的求组合求和** - -[40. Combination Sum II (Medium)](https://leetcode.com/problems/combination-sum-ii/description/) - -```html -For example, given candidate set [10, 1, 2, 7, 6, 1, 5] and target 8, -A solution set is: -[ - [1, 7], - [1, 2, 5], - [2, 6], - [1, 1, 6] -] -``` - -```java -public List> combinationSum2(int[] candidates, int target) { - List> combinations = new ArrayList<>(); - Arrays.sort(candidates); - backtracking(new ArrayList<>(), combinations, new boolean[candidates.length], 0, target, candidates); - return combinations; -} - -private void backtracking(List tempCombination, List> combinations, - boolean[] hasVisited, int start, int target, final int[] candidates) { - - if (target == 0) { - combinations.add(new ArrayList<>(tempCombination)); - return; - } - for (int i = start; i < candidates.length; i++) { - if (i != 0 && candidates[i] == candidates[i - 1] && !hasVisited[i - 1]) { - continue; - } - if (candidates[i] <= target) { - tempCombination.add(candidates[i]); - hasVisited[i] = true; - backtracking(tempCombination, combinations, hasVisited, i + 1, target - candidates[i], candidates); - hasVisited[i] = false; - tempCombination.remove(tempCombination.size() - 1); - } - } -} -``` - -**1-9 数字的组合求和** - -[216. Combination Sum III (Medium)](https://leetcode.com/problems/combination-sum-iii/description/) - -```html -Input: k = 3, n = 9 - -Output: - -[[1,2,6], [1,3,5], [2,3,4]] -``` - -从 1-9 数字中选出 k 个数不重复的数,使得它们的和为 n。 - -```java -public List> combinationSum3(int k, int n) { - List> combinations = new ArrayList<>(); - List path = new ArrayList<>(); - backtracking(k, n, 1, path, combinations); - return combinations; -} - -private void backtracking(int k, int n, int start, - List tempCombination, List> combinations) { - - if (k == 0 && n == 0) { - combinations.add(new ArrayList<>(tempCombination)); - return; - } - if (k == 0 || n == 0) { - return; - } - for (int i = start; i <= 9; i++) { - tempCombination.add(i); - backtracking(k - 1, n - i, i + 1, tempCombination, combinations); - tempCombination.remove(tempCombination.size() - 1); - } -} -``` - -**子集** - -[78. Subsets (Medium)](https://leetcode.com/problems/subsets/description/) - -找出集合的所有子集,子集不能重复,[1, 2] 和 [2, 1] 这种子集算重复 - -```java -public List> subsets(int[] nums) { - List> subsets = new ArrayList<>(); - List tempSubset = new ArrayList<>(); - for (int size = 0; size <= nums.length; size++) { - backtracking(0, tempSubset, subsets, size, nums); // 不同的子集大小 - } - return subsets; -} - -private void backtracking(int start, List tempSubset, List> subsets, - final int size, final int[] nums) { - - if (tempSubset.size() == size) { - subsets.add(new ArrayList<>(tempSubset)); - return; - } - for (int i = start; i < nums.length; i++) { - tempSubset.add(nums[i]); - backtracking(i + 1, tempSubset, subsets, size, nums); - tempSubset.remove(tempSubset.size() - 1); - } -} -``` - -**含有相同元素求子集** - -[90. Subsets II (Medium)](https://leetcode.com/problems/subsets-ii/description/) - -```html -For example, -If nums = [1,2,2], a solution is: - -[ - [2], - [1], - [1,2,2], - [2,2], - [1,2], - [] -] -``` - -```java -public List> subsetsWithDup(int[] nums) { - Arrays.sort(nums); - List> subsets = new ArrayList<>(); - List tempSubset = new ArrayList<>(); - boolean[] hasVisited = new boolean[nums.length]; - for (int size = 0; size <= nums.length; size++) { - backtracking(0, tempSubset, subsets, hasVisited, size, nums); // 不同的子集大小 - } - return subsets; -} - -private void backtracking(int start, List tempSubset, List> subsets, boolean[] hasVisited, - final int size, final int[] nums) { - - if (tempSubset.size() == size) { - subsets.add(new ArrayList<>(tempSubset)); - return; - } - for (int i = start; i < nums.length; i++) { - if (i != 0 && nums[i] == nums[i - 1] && !hasVisited[i - 1]) { - continue; - } - tempSubset.add(nums[i]); - hasVisited[i] = true; - backtracking(i + 1, tempSubset, subsets, hasVisited, size, nums); - hasVisited[i] = false; - tempSubset.remove(tempSubset.size() - 1); - } -} -``` - -**分割字符串使得每个部分都是回文数** - -[131. Palindrome Partitioning (Medium)](https://leetcode.com/problems/palindrome-partitioning/description/) - -```html -For example, given s = "aab", -Return - -[ - ["aa","b"], - ["a","a","b"] -] -``` - -```java -public List> partition(String s) { - List> partitions = new ArrayList<>(); - List tempPartition = new ArrayList<>(); - doPartition(s, partitions, tempPartition); - return partitions; -} - -private void doPartition(String s, List> partitions, List tempPartition) { - if (s.length() == 0) { - partitions.add(new ArrayList<>(tempPartition)); - return; - } - for (int i = 0; i < s.length(); i++) { - if (isPalindrome(s, 0, i)) { - tempPartition.add(s.substring(0, i + 1)); - doPartition(s.substring(i + 1), partitions, tempPartition); - tempPartition.remove(tempPartition.size() - 1); - } - } -} - -private boolean isPalindrome(String s, int begin, int end) { - while (begin < end) { - if (s.charAt(begin++) != s.charAt(end--)) { - return false; - } - } - return true; -} -``` - -**数独** - -[37. Sudoku Solver (Hard)](https://leetcode.com/problems/sudoku-solver/description/) - -

- -```java -private boolean[][] rowsUsed = new boolean[9][10]; -private boolean[][] colsUsed = new boolean[9][10]; -private boolean[][] cubesUsed = new boolean[9][10]; -private char[][] board; - -public void solveSudoku(char[][] board) { - this.board = board; - for (int i = 0; i < 9; i++) - for (int j = 0; j < 9; j++) { - if (board[i][j] == '.') { - continue; - } - int num = board[i][j] - '0'; - rowsUsed[i][num] = true; - colsUsed[j][num] = true; - cubesUsed[cubeNum(i, j)][num] = true; - } - backtracking(0, 0); -} - -private boolean backtracking(int row, int col) { - while (row < 9 && board[row][col] != '.') { - row = col == 8 ? row + 1 : row; - col = col == 8 ? 0 : col + 1; - } - if (row == 9) { - return true; - } - for (int num = 1; num <= 9; num++) { - if (rowsUsed[row][num] || colsUsed[col][num] || cubesUsed[cubeNum(row, col)][num]) { - continue; - } - rowsUsed[row][num] = colsUsed[col][num] = cubesUsed[cubeNum(row, col)][num] = true; - board[row][col] = (char) (num + '0'); - if (backtracking(row, col)) { - return true; - } - board[row][col] = '.'; - rowsUsed[row][num] = colsUsed[col][num] = cubesUsed[cubeNum(row, col)][num] = false; - } - return false; -} - -private int cubeNum(int i, int j) { - int r = i / 3; - int c = j / 3; - return r * 3 + c; -} -``` - -**N 皇后** - -[51. N-Queens (Hard)](https://leetcode.com/problems/n-queens/description/) - -

- -在 n\*n 的矩阵中摆放 n 个皇后,并且每个皇后不能在同一行,同一列,同一对角线上,求所有的 n 皇后的解。 - -一行一行地摆放,在确定一行中的那个皇后应该摆在哪一列时,需要用三个标记数组来确定某一列是否合法,这三个标记数组分别为:列标记数组、45 度对角线标记数组和 135 度对角线标记数组。 - -45 度对角线标记数组的长度为 2 \* n - 1,通过下图可以明确 (r, c) 的位置所在的数组下标为 r + c。 - -

- -135 度对角线标记数组的长度也是 2 \* n - 1,(r, c) 的位置所在的数组下标为 n - 1 - (r - c)。 - -

- -```java -private List> solutions; -private char[][] nQueens; -private boolean[] colUsed; -private boolean[] diagonals45Used; -private boolean[] diagonals135Used; -private int n; - -public List> solveNQueens(int n) { - solutions = new ArrayList<>(); - nQueens = new char[n][n]; - for (int i = 0; i < n; i++) { - Arrays.fill(nQueens[i], '.'); - } - colUsed = new boolean[n]; - diagonals45Used = new boolean[2 * n - 1]; - diagonals135Used = new boolean[2 * n - 1]; - this.n = n; - backtracking(0); - return solutions; -} - -private void backtracking(int row) { - if (row == n) { - List list = new ArrayList<>(); - for (char[] chars : nQueens) { - list.add(new String(chars)); - } - solutions.add(list); - return; - } - - for (int col = 0; col < n; col++) { - int diagonals45Idx = row + col; - int diagonals135Idx = n - 1 - (row - col); - if (colUsed[col] || diagonals45Used[diagonals45Idx] || diagonals135Used[diagonals135Idx]) { - continue; - } - nQueens[row][col] = 'Q'; - colUsed[col] = diagonals45Used[diagonals45Idx] = diagonals135Used[diagonals135Idx] = true; - backtracking(row + 1); - colUsed[col] = diagonals45Used[diagonals45Idx] = diagonals135Used[diagonals135Idx] = false; - nQueens[row][col] = '.'; - } -} -``` - -## 动态规划 - -递归和动态规划都是将原问题拆成多个子问题然后求解,他们之间最本质的区别是,动态规划保存了子问题的解,避免重复计算。 - -### 斐波那契数列 - -**爬楼梯** - -[70. Climbing Stairs (Easy)](https://leetcode.com/problems/climbing-stairs/description/) - -题目描述:有 N 阶楼梯,每次可以上一阶或者两阶,求有多少种上楼梯的方法。 - -定义一个数组 dp 存储上楼梯的方法数(为了方便讨论,数组下标从 1 开始),dp[i] 表示走到第 i 个楼梯的方法数目。 - -第 i 个楼梯可以从第 i-1 和 i-2 个楼梯再走一步到达,走到第 i 个楼梯的方法数为走到第 i-1 和第 i-2 个楼梯的方法数之和。 - - - -

- - -考虑到 dp[i] 只与 dp[i - 1] 和 dp[i - 2] 有关,因此可以只用两个变量来存储 dp[i - 1] 和 dp[i - 2],使得原来的 O(N) 空间复杂度优化为 O(1) 复杂度。 - -```java -public int climbStairs(int n) { - if (n <= 2) { - return n; - } - int pre2 = 1, pre1 = 2; - for (int i = 2; i < n; i++) { - int cur = pre1 + pre2; - pre2 = pre1; - pre1 = cur; - } - return pre1; -} -``` - -**强盗抢劫** - -[198. House Robber (Easy)](https://leetcode.com/problems/house-robber/description/) - -题目描述:抢劫一排住户,但是不能抢邻近的住户,求最大抢劫量。 - -定义 dp 数组用来存储最大的抢劫量,其中 dp[i] 表示抢到第 i 个住户时的最大抢劫量。 - -由于不能抢劫邻近住户,如果抢劫了第 i -1 个住户,那么就不能再抢劫第 i 个住户,所以 - - - -

- -```java -public int rob(int[] nums) { - int pre2 = 0, pre1 = 0; - for (int i = 0; i < nums.length; i++) { - int cur = Math.max(pre2 + nums[i], pre1); - pre2 = pre1; - pre1 = cur; - } - return pre1; -} -``` - -**强盗在环形街区抢劫** - -[213. House Robber II (Medium)](https://leetcode.com/problems/house-robber-ii/description/) - -```java -public int rob(int[] nums) { - if (nums == null || nums.length == 0) { - return 0; - } - int n = nums.length; - if (n == 1) { - return nums[0]; - } - return Math.max(rob(nums, 0, n - 2), rob(nums, 1, n - 1)); -} - -private int rob(int[] nums, int first, int last) { - int pre2 = 0, pre1 = 0; - for (int i = first; i <= last; i++) { - int cur = Math.max(pre1, pre2 + nums[i]); - pre2 = pre1; - pre1 = cur; - } - return pre1; -} -``` - -**信件错排** - -题目描述:有 N 个 信 和 信封,它们被打乱,求错误装信方式的数量。 - -定义一个数组 dp 存储错误方式数量,dp[i] 表示前 i 个信和信封的错误方式数量。假设第 i 个信装到第 j 个信封里面,而第 j 个信装到第 k 个信封里面。根据 i 和 k 是否相等,有两种情况: - -- i==k,交换 i 和 k 的信后,它们的信和信封在正确的位置,但是其余 i-2 封信有 dp[i-2] 种错误装信的方式。由于 j 有 i-1 种取值,因此共有 (i-1)\*dp[i-2] 种错误装信方式。 -- i != k,交换 i 和 j 的信后,第 i 个信和信封在正确的位置,其余 i-1 封信有 dp[i-1] 种错误装信方式。由于 j 有 i-1 种取值,因此共有 (i-1)\*dp[i-1] 种错误装信方式。 - -综上所述,错误装信数量方式数量为: - - - -

- -**母牛生产** - -[程序员代码面试指南-P181](#) - -题目描述:假设农场中成熟的母牛每年都会生 1 头小母牛,并且永远不会死。第一年有 1 只小母牛,从第二年开始,母牛开始生小母牛。每只小母牛 3 年之后成熟又可以生小母牛。给定整数 N,求 N 年后牛的数量。 - -第 i 年成熟的牛的数量为: - - - -

- -### 矩阵路径 - -**矩阵的最小路径和** - -[64. Minimum Path Sum (Medium)](https://leetcode.com/problems/minimum-path-sum/description/) - -```html -[[1,3,1], - [1,5,1], - [4,2,1]] -Given the above grid map, return 7. Because the path 1→3→1→1→1 minimizes the sum. -``` - -题目描述:求从矩阵的左上角到右下角的最小路径和,每次只能向右和向下移动。 - -```java -public int minPathSum(int[][] grid) { - if (grid.length == 0 || grid[0].length == 0) { - return 0; - } - int m = grid.length, n = grid[0].length; - int[] dp = new int[n]; - for (int i = 0; i < m; i++) { - for (int j = 0; j < n; j++) { - if (j == 0) { - dp[j] = dp[j]; // 只能从上侧走到该位置 - } else if (i == 0) { - dp[j] = dp[j - 1]; // 只能从左侧走到该位置 - } else { - dp[j] = Math.min(dp[j - 1], dp[j]); - } - dp[j] += grid[i][j]; - } - } - return dp[n - 1]; -} -``` - -**矩阵的总路径数** - -[62. Unique Paths (Medium)](https://leetcode.com/problems/unique-paths/description/) - -题目描述:统计从矩阵左上角到右下角的路径总数,每次只能向右或者向下移动。 - -

- -```java -public int uniquePaths(int m, int n) { - int[] dp = new int[n]; - Arrays.fill(dp, 1); - for (int i = 1; i < m; i++) { - for (int j = 1; j < n; j++) { - dp[j] = dp[j] + dp[j - 1]; - } - } - return dp[n - 1]; -} -``` - -也可以直接用数学公式求解,这是一个组合问题。机器人总共移动的次数 S=m+n-2,向下移动的次数 D=m-1,那么问题可以看成从 S 中取出 D 个位置的组合数量,这个问题的解为 C(S, D)。 - -```java -public int uniquePaths(int m, int n) { - int S = m + n - 2; // 总共的移动次数 - int D = m - 1; // 向下的移动次数 - long ret = 1; - for (int i = 1; i <= D; i++) { - ret = ret * (S - D + i) / i; - } - return (int) ret; -} -``` - -### 数组区间 - -**数组区间和** - -[303. Range Sum Query - Immutable (Easy)](https://leetcode.com/problems/range-sum-query-immutable/description/) - -```html -Given nums = [-2, 0, 3, -5, 2, -1] - -sumRange(0, 2) -> 1 -sumRange(2, 5) -> -1 -sumRange(0, 5) -> -3 -``` - -求区间 i \~ j 的和,可以转换为 sum[j + 1] - sum[i],其中 sum[i] 为 0 \~ i - 1 的和。 - -```java -class NumArray { - - private int[] sums; - - public NumArray(int[] nums) { - sums = new int[nums.length + 1]; - for (int i = 1; i <= nums.length; i++) { - sums[i] = sums[i - 1] + nums[i - 1]; - } - } - - public int sumRange(int i, int j) { - return sums[j + 1] - sums[i]; - } -} -``` - -**数组中等差递增子区间的个数** - -[413. Arithmetic Slices (Medium)](https://leetcode.com/problems/arithmetic-slices/description/) - -```html -A = [1, 2, 3, 4] -return: 3, for 3 arithmetic slices in A: [1, 2, 3], [2, 3, 4] and [1, 2, 3, 4] itself. -``` - -dp[i] 表示以 A[i] 为结尾的等差递增子区间的个数。 - -在 A[i] - A[i - 1] == A[i - 1] - A[i - 2] 的条件下,{A[i - 2], A[i - 1], A[i]} 是一个等差递增子区间。如果 {A[i - 3], A[i - 2], A[i - 1]} 是一个等差递增子区间,那么 {A[i - 3], A[i - 2], A[i - 1], A[i]} 也是等差递增子区间,dp[i] = dp[i-1] + 1。 - -```java -public int numberOfArithmeticSlices(int[] A) { - if (A == null || A.length == 0) { - return 0; - } - int n = A.length; - int[] dp = new int[n]; - for (int i = 2; i < n; i++) { - if (A[i] - A[i - 1] == A[i - 1] - A[i - 2]) { - dp[i] = dp[i - 1] + 1; - } - } - int total = 0; - for (int cnt : dp) { - total += cnt; - } - return total; -} -``` - -### 分割整数 - -**分割整数的最大乘积** - -[343. Integer Break (Medim)](https://leetcode.com/problems/integer-break/description/) - -题目描述:For example, given n = 2, return 1 (2 = 1 + 1); given n = 10, return 36 (10 = 3 + 3 + 4). - -```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 - 1; j++) { - dp[i] = Math.max(dp[i], Math.max(j * dp[i - j], j * (i - j))); - } - } - return dp[n]; -} -``` - -**按平方数来分割整数** - -[279. Perfect Squares(Medium)](https://leetcode.com/problems/perfect-squares/description/) - -题目描述:For example, given n = 12, return 3 because 12 = 4 + 4 + 4; given n = 13, return 2 because 13 = 4 + 9. - -```java -public int numSquares(int n) { - List squareList = generateSquareList(n); - int[] dp = new int[n + 1]; - for (int i = 1; i <= n; i++) { - int min = Integer.MAX_VALUE; - for (int square : squareList) { - if (square > i) { - break; - } - min = Math.min(min, dp[i - square] + 1); - } - dp[i] = min; - } - return dp[n]; -} - -private List generateSquareList(int n) { - List squareList = new ArrayList<>(); - int diff = 3; - int square = 1; - while (square <= n) { - squareList.add(square); - square += diff; - diff += 2; - } - return squareList; -} -``` - -**分割整数构成字母字符串** - -[91. Decode Ways (Medium)](https://leetcode.com/problems/decode-ways/description/) - -题目描述:Given encoded message "12", it could be decoded as "AB" (1 2) or "L" (12). - -```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]; -} -``` - -### 最长递增子序列 - -已知一个序列 {S1, S2,...,Sn},取出若干数组成新的序列 {Si1, Si2,..., Sim},其中 i1、i2 ... im 保持递增,即新序列中各个数仍然保持原数列中的先后顺序,称新序列为原序列的一个 **子序列** 。 - -如果在子序列中,当下标 ix > iy 时,Six > Siy,称子序列为原序列的一个 **递增子序列** 。 - -定义一个数组 dp 存储最长递增子序列的长度,dp[n] 表示以 Sn 结尾的序列的最长递增子序列长度。对于一个递增子序列 {Si1, Si2,...,Sim},如果 im < n 并且 Sim < Sn,此时 {Si1, Si2,..., Sim, Sn} 为一个递增子序列,递增子序列的长度增加 1。满足上述条件的递增子序列中,长度最长的那个递增子序列就是要找的,在长度最长的递增子序列上加上 Sn 就构成了以 Sn 为结尾的最长递增子序列。因此 dp[n] = max{ dp[i]+1 | Si < Sn && i < n} 。 - -因为在求 dp[n] 时可能无法找到一个满足条件的递增子序列,此时 {Sn} 就构成了递增子序列,需要对前面的求解方程做修改,令 dp[n] 最小为 1,即: - - - -

- -对于一个长度为 N 的序列,最长递增子序列并不一定会以 SN 为结尾,因此 dp[N] 不是序列的最长递增子序列的长度,需要遍历 dp 数组找出最大值才是所要的结果,max{ dp[i] | 1 <= i <= N} 即为所求。 - -**最长递增子序列** - -[300. Longest Increasing Subsequence (Medium)](https://leetcode.com/problems/longest-increasing-subsequence/description/) - -```java -public int lengthOfLIS(int[] nums) { - int n = nums.length; - int[] dp = new int[n]; - for (int i = 0; i < n; i++) { - int max = 1; - for (int j = 0; j < i; j++) { - if (nums[i] > nums[j]) { - max = Math.max(max, dp[j] + 1); - } - } - dp[i] = max; - } - return Arrays.stream(dp).max().orElse(0); -} -``` - -使用 Stream 求最大值会导致运行时间过长,可以改成以下形式: - -```java -int ret = 0; -for (int i = 0; i < n; i++) { - ret = Math.max(ret, dp[i]); -} -return ret; -``` - -以上解法的时间复杂度为 O(N2),可以使用二分查找将时间复杂度降低为 O(NlogN)。 - -定义一个 tails 数组,其中 tails[i] 存储长度为 i + 1 的最长递增子序列的最后一个元素。对于一个元素 x, - -- 如果它大于 tails 数组所有的值,那么把它添加到 tails 后面,表示最长递增子序列长度加 1; -- 如果 tails[i-1] < x <= tails[i],那么更新 tails[i] = x。 - -例如对于数组 [4,3,6,5],有: - -```html -tails len num -[] 0 4 -[4] 1 3 -[3] 1 6 -[3,6] 2 5 -[3,5] 2 null -``` - -可以看出 tails 数组保持有序,因此在查找 Si 位于 tails 数组的位置时就可以使用二分查找。 - -```java -public int lengthOfLIS(int[] nums) { - int n = nums.length; - int[] tails = new int[n]; - int len = 0; - for (int num : nums) { - int index = binarySearch(tails, len, num); - tails[index] = num; - if (index == len) { - len++; - } - } - return len; -} - -private int binarySearch(int[] tails, int len, int key) { - int l = 0, h = len; - while (l < h) { - int mid = l + (h - l) / 2; - if (tails[mid] == key) { - return mid; - } else if (tails[mid] > key) { - h = mid; - } else { - l = mid + 1; - } - } - return l; -} -``` - -**一组整数对能够构成的最长链** - -[646. Maximum Length of Pair Chain (Medium)](https://leetcode.com/problems/maximum-length-of-pair-chain/description/) - -```html -Input: [[1,2], [2,3], [3,4]] -Output: 2 -Explanation: The longest chain is [1,2] -> [3,4] -``` - -题目描述:对于 (a, b) 和 (c, d) ,如果 b < c,则它们可以构成一条链。 - -```java -public int findLongestChain(int[][] pairs) { - if (pairs == null || pairs.length == 0) { - return 0; - } - Arrays.sort(pairs, (a, b) -> (a[0] - b[0])); - int n = pairs.length; - int[] dp = new int[n]; - Arrays.fill(dp, 1); - for (int i = 1; i < n; i++) { - for (int j = 0; j < i; j++) { - if (pairs[j][1] < pairs[i][0]) { - dp[i] = Math.max(dp[i], dp[j] + 1); - } - } - } - return Arrays.stream(dp).max().orElse(0); -} -``` - -**最长摆动子序列** - -[376. Wiggle Subsequence (Medium)](https://leetcode.com/problems/wiggle-subsequence/description/) - -```html -Input: [1,7,4,9,2,5] -Output: 6 -The entire sequence is a wiggle sequence. - -Input: [1,17,5,10,13,15,10,5,16,8] -Output: 7 -There are several subsequences that achieve this length. One is [1,17,10,13,10,16,8]. - -Input: [1,2,3,4,5,6,7,8,9] -Output: 2 -``` - -要求:使用 O(N) 时间复杂度求解。 - -```java -public int wiggleMaxLength(int[] nums) { - if (nums == null || nums.length == 0) { - return 0; - } - int up = 1, down = 1; - for (int i = 1; i < nums.length; i++) { - if (nums[i] > nums[i - 1]) { - up = down + 1; - } else if (nums[i] < nums[i - 1]) { - down = up + 1; - } - } - return Math.max(up, down); -} -``` - -### 最长公共子序列 - -对于两个子序列 S1 和 S2,找出它们最长的公共子序列。 - -定义一个二维数组 dp 用来存储最长公共子序列的长度,其中 dp[i][j] 表示 S1 的前 i 个字符与 S2 的前 j 个字符最长公共子序列的长度。考虑 S1i 与 S2j 值是否相等,分为两种情况: - -- 当 S1i==S2j 时,那么就能在 S1 的前 i-1 个字符与 S2 的前 j-1 个字符最长公共子序列的基础上再加上 S1i 这个值,最长公共子序列长度加 1,即 dp[i][j] = dp[i-1][j-1] + 1。 -- 当 S1i != S2j 时,此时最长公共子序列为 S1 的前 i-1 个字符和 S2 的前 j 个字符最长公共子序列,或者 S1 的前 i 个字符和 S2 的前 j-1 个字符最长公共子序列,取它们的最大者,即 dp[i][j] = max{ dp[i-1][j], dp[i][j-1] }。 - -综上,最长公共子序列的状态转移方程为: - - - -

- -对于长度为 N 的序列 S1 和长度为 M 的序列 S2,dp[N][M] 就是序列 S1 和序列 S2 的最长公共子序列长度。 - -与最长递增子序列相比,最长公共子序列有以下不同点: - -- 针对的是两个序列,求它们的最长公共子序列。 -- 在最长递增子序列中,dp[i] 表示以 Si 为结尾的最长递增子序列长度,子序列必须包含 Si ;在最长公共子序列中,dp[i][j] 表示 S1 中前 i 个字符与 S2 中前 j 个字符的最长公共子序列长度,不一定包含 S1i 和 S2j。 -- 在求最终解时,最长公共子序列中 dp[N][M] 就是最终解,而最长递增子序列中 dp[N] 不是最终解,因为以 SN 为结尾的最长递增子序列不一定是整个序列最长递增子序列,需要遍历一遍 dp 数组找到最大者。 - -```java -public int lengthOfLCS(int[] nums1, int[] nums2) { - int n1 = nums1.length, n2 = nums2.length; - int[][] dp = new int[n1 + 1][n2 + 1]; - for (int i = 1; i <= n1; i++) { - for (int j = 1; j <= n2; j++) { - if (nums1[i - 1] == nums2[j - 1]) { - dp[i][j] = dp[i - 1][j - 1] + 1; - } else { - dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]); - } - } - } - return dp[n1][n2]; -} -``` - -### 0-1 背包 - -有一个容量为 N 的背包,要用这个背包装下物品的价值最大,这些物品有两个属性:体积 w 和价值 v。 - -定义一个二维数组 dp 存储最大价值,其中 dp[i][j] 表示前 i 件物品体积不超过 j 的情况下能达到的最大价值。设第 i 件物品体积为 w,价值为 v,根据第 i 件物品是否添加到背包中,可以分两种情况讨论: - -- 第 i 件物品没添加到背包,总体积不超过 j 的前 i 件物品的最大价值就是总体积不超过 j 的前 i-1 件物品的最大价值,dp[i][j] = dp[i-1][j]。 -- 第 i 件物品添加到背包中,dp[i][j] = dp[i-1][j-w] + v。 - -第 i 件物品可添加也可以不添加,取决于哪种情况下最大价值更大。因此,0-1 背包的状态转移方程为: - - - -

- -```java -public int knapsack(int W, int N, int[] weights, int[] values) { - int[][] dp = new int[N + 1][W + 1]; - for (int i = 1; i <= N; i++) { - int w = weights[i - 1], v = values[i - 1]; - for (int j = 1; j <= W; j++) { - if (j >= w) { - dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - w] + v); - } else { - dp[i][j] = dp[i - 1][j]; - } - } - } - return dp[N][W]; -} -``` - -**空间优化** - -在程序实现时可以对 0-1 背包做优化。观察状态转移方程可以知道,前 i 件物品的状态仅与前 i-1 件物品的状态有关,因此可以将 dp 定义为一维数组,其中 dp[j] 既可以表示 dp[i-1][j] 也可以表示 dp[i][j]。此时, - - - -

- -因为 dp[j-w] 表示 dp[i-1][j-w],因此不能先求 dp[i][j-w],以防将 dp[i-1][j-w] 覆盖。也就是说要先计算 dp[i][j] 再计算 dp[i][j-w],在程序实现时需要按倒序来循环求解。 - -```java -public int knapsack(int W, int N, int[] weights, int[] values) { - int[] dp = new int[W + 1]; - for (int i = 1; i <= N; i++) { - int w = weights[i - 1], v = values[i - 1]; - for (int j = W; j >= 1; j--) { - if (j >= w) { - dp[j] = Math.max(dp[j], dp[j - w] + v); - } - } - } - return dp[W]; -} -``` - -**无法使用贪心算法的解释** - -0-1 背包问题无法使用贪心算法来求解,也就是说不能按照先添加性价比最高的物品来达到最优,这是因为这种方式可能造成背包空间的浪费,从而无法达到最优。考虑下面的物品和一个容量为 5 的背包,如果先添加物品 0 再添加物品 1,那么只能存放的价值为 16,浪费了大小为 2 的空间。最优的方式是存放物品 1 和物品 2,价值为 22. - -| id | w | v | v/w | -| --- | --- | --- | --- | -| 0 | 1 | 6 | 6 | -| 1 | 2 | 10 | 5 | -| 2 | 3 | 12 | 4 | - -**变种** - -- 完全背包:物品数量为无限个 - -- 多重背包:物品数量有限制 - -- 多维费用背包:物品不仅有重量,还有体积,同时考虑这两种限制 - -- 其它:物品之间相互约束或者依赖 - -**划分数组为和相等的两部分** - -[416. Partition Equal Subset Sum (Medium)](https://leetcode.com/problems/partition-equal-subset-sum/description/) - -```html -Input: [1, 5, 11, 5] - -Output: true - -Explanation: The array can be partitioned as [1, 5, 5] and [11]. -``` - -可以看成一个背包大小为 sum/2 的 0-1 背包问题。 - -```java -public boolean canPartition(int[] nums) { - int sum = computeArraySum(nums); - if (sum % 2 != 0) { - return false; - } - int W = sum / 2; - boolean[] dp = new boolean[W + 1]; - dp[0] = true; - for (int num : nums) { // 0-1 背包一个物品只能用一次 - for (int i = W; i >= num; i--) { // 从后往前,先计算 dp[i] 再计算 dp[i-num] - dp[i] = dp[i] || dp[i - num]; - } - } - return dp[W]; -} - -private int computeArraySum(int[] nums) { - int sum = 0; - for (int num : nums) { - sum += num; - } - return sum; -} -``` - -**改变一组数的正负号使得它们的和为一给定数** - -[494. Target Sum (Medium)](https://leetcode.com/problems/target-sum/description/) - -```html -Input: nums is [1, 1, 1, 1, 1], S is 3. -Output: 5 -Explanation: - --1+1+1+1+1 = 3 -+1-1+1+1+1 = 3 -+1+1-1+1+1 = 3 -+1+1+1-1+1 = 3 -+1+1+1+1-1 = 3 - -There are 5 ways to assign symbols to make the sum of nums be target 3. -``` - -该问题可以转换为 Subset Sum 问题,从而使用 0-1 背包的方法来求解。 - -可以将这组数看成两部分,P 和 N,其中 P 使用正号,N 使用负号,有以下推导: - -```html - sum(P) - sum(N) = target -sum(P) + sum(N) + sum(P) - sum(N) = target + sum(P) + sum(N) - 2 * sum(P) = target + sum(nums) -``` - -因此只要找到一个子集,令它们都取正号,并且和等于 (target + sum(nums))/2,就证明存在解。 - -```java -public int findTargetSumWays(int[] nums, int S) { - int sum = computeArraySum(nums); - if (sum < S || (sum + S) % 2 == 1) { - return 0; - } - int W = (sum + S) / 2; - int[] dp = new int[W + 1]; - dp[0] = 1; - for (int num : nums) { - for (int i = W; i >= num; i--) { - dp[i] = dp[i] + dp[i - num]; - } - } - return dp[W]; -} - -private int computeArraySum(int[] nums) { - int sum = 0; - for (int num : nums) { - sum += num; - } - return sum; -} -``` - -DFS 解法: - -```java -public int findTargetSumWays(int[] nums, int S) { - return findTargetSumWays(nums, 0, S); -} - -private int findTargetSumWays(int[] nums, int start, int S) { - if (start == nums.length) { - return S == 0 ? 1 : 0; - } - return findTargetSumWays(nums, start + 1, S + nums[start]) - + findTargetSumWays(nums, start + 1, S - nums[start]); -} -``` - -**01 字符构成最多的字符串** - -[474. Ones and Zeroes (Medium)](https://leetcode.com/problems/ones-and-zeroes/description/) - -```html -Input: Array = {"10", "0001", "111001", "1", "0"}, m = 5, n = 3 -Output: 4 - -Explanation: There are totally 4 strings can be formed by the using of 5 0s and 3 1s, which are "10","0001","1","0" -``` - -这是一个多维费用的 0-1 背包问题,有两个背包大小,0 的数量和 1 的数量。 - -```java -public int findMaxForm(String[] strs, int m, int n) { - if (strs == null || strs.length == 0) { - return 0; - } - int[][] dp = new int[m + 1][n + 1]; - for (String s : strs) { // 每个字符串只能用一次 - int ones = 0, zeros = 0; - for (char c : s.toCharArray()) { - if (c == '0') { - zeros++; - } else { - ones++; - } - } - for (int i = m; i >= zeros; i--) { - for (int j = n; j >= ones; j--) { - dp[i][j] = Math.max(dp[i][j], dp[i - zeros][j - ones] + 1); - } - } - } - return dp[m][n]; -} -``` - -**找零钱的最少硬币数** - -[322. Coin Change (Medium)](https://leetcode.com/problems/coin-change/description/) - -```html -Example 1: -coins = [1, 2, 5], amount = 11 -return 3 (11 = 5 + 5 + 1) - -Example 2: -coins = [2], amount = 3 -return -1. -``` - -题目描述:给一些面额的硬币,要求用这些硬币来组成给定面额的钱数,并且使得硬币数量最少。硬币可以重复使用。 - -- 物品:硬币 -- 物品大小:面额 -- 物品价值:数量 - -因为硬币可以重复使用,因此这是一个完全背包问题。完全背包只需要将 0-1 背包中逆序遍历 dp 数组改为正序遍历即可。 - -```java -public int coinChange(int[] coins, int amount) { - if (amount == 0 || coins == null || coins.length == 0) { - return 0; - } - int[] dp = new int[amount + 1]; - for (int coin : coins) { - for (int i = coin; i <= amount; i++) { //将逆序遍历改为正序遍历 - if (i == coin) { - dp[i] = 1; - } else if (dp[i] == 0 && dp[i - coin] != 0) { - dp[i] = dp[i - coin] + 1; - } else if (dp[i - coin] != 0) { - dp[i] = Math.min(dp[i], dp[i - coin] + 1); - } - } - } - return dp[amount] == 0 ? -1 : dp[amount]; -} -``` - -**找零钱的硬币数组合** - -[518\. Coin Change 2 (Medium)](https://leetcode.com/problems/coin-change-2/description/) - -```text-html-basic -Input: amount = 5, coins = [1, 2, 5] -Output: 4 -Explanation: there are four ways to make up the amount: -5=5 -5=2+2+1 -5=2+1+1+1 -5=1+1+1+1+1 -``` - -完全背包问题,使用 dp 记录可达成目标的组合数目。 - -```java -public int change(int amount, int[] coins) { - if (amount == 0 || coins == null || coins.length == 0) { - return 0; - } - int[] dp = new int[amount + 1]; - dp[0] = 1; - for (int coin : coins) { - for (int i = coin; i <= amount; i++) { - dp[i] += dp[i - coin]; - } - } - return dp[amount]; -} -``` - -**字符串按单词列表分割** - -[139. Word Break (Medium)](https://leetcode.com/problems/word-break/description/) - -```html -s = "leetcode", -dict = ["leet", "code"]. -Return true because "leetcode" can be segmented as "leet code". -``` - -dict 中的单词没有使用次数的限制,因此这是一个完全背包问题。该问题涉及到字典中单词的使用顺序,因此可理解为涉及顺序的完全背包问题。 - -求解顺序的完全背包问题时,对物品的迭代应该放在最里层。 - -```java -public boolean wordBreak(String s, List wordDict) { - int n = s.length(); - boolean[] dp = new boolean[n + 1]; - dp[0] = true; - for (int i = 1; i <= n; i++) { - for (String word : wordDict) { // 对物品的迭代应该放在最里层 - int len = word.length(); - if (len <= i && word.equals(s.substring(i - len, i))) { - dp[i] = dp[i] || dp[i - len]; - } - } - } - return dp[n]; -} -``` - -**组合总和** - -[377. Combination Sum IV (Medium)](https://leetcode.com/problems/combination-sum-iv/description/) - -```html -nums = [1, 2, 3] -target = 4 - -The possible combination ways are: -(1, 1, 1, 1) -(1, 1, 2) -(1, 2, 1) -(1, 3) -(2, 1, 1) -(2, 2) -(3, 1) - -Note that different sequences are counted as different combinations. - -Therefore the output is 7. -``` - -涉及顺序的完全背包。 - -```java -public int combinationSum4(int[] nums, int target) { - if (nums == null || nums.length == 0) { - return 0; - } - int[] maximum = new int[target + 1]; - maximum[0] = 1; - Arrays.sort(nums); - for (int i = 1; i <= target; i++) { - for (int j = 0; j < nums.length && nums[j] <= i; j++) { - maximum[i] += maximum[i - nums[j]]; - } - } - return maximum[target]; -} -``` - -### 股票交易 - -**需要冷却期的股票交易** - -[309. Best Time to Buy and Sell Stock with Cooldown(Medium)](https://leetcode.com/problems/best-time-to-buy-and-sell-stock-with-cooldown/description/) - -题目描述:交易之后需要有一天的冷却时间。 - -

- -```java -public int maxProfit(int[] prices) { - if (prices == null || prices.length == 0) { - return 0; - } - int N = prices.length; - int[] buy = new int[N]; - int[] s1 = new int[N]; - int[] sell = new int[N]; - int[] s2 = new int[N]; - s1[0] = buy[0] = -prices[0]; - sell[0] = s2[0] = 0; - for (int i = 1; i < N; i++) { - buy[i] = s2[i - 1] - prices[i]; - s1[i] = Math.max(buy[i - 1], s1[i - 1]); - sell[i] = Math.max(buy[i - 1], s1[i - 1]) + prices[i]; - s2[i] = Math.max(s2[i - 1], sell[i - 1]); - } - return Math.max(sell[N - 1], s2[N - 1]); -} -``` - -**需要交易费用的股票交易** - -[714. Best Time to Buy and Sell Stock with Transaction Fee (Medium)](https://leetcode.com/problems/best-time-to-buy-and-sell-stock-with-transaction-fee/description/) - -```html -Input: prices = [1, 3, 2, 8, 4, 9], fee = 2 -Output: 8 -Explanation: The maximum profit can be achieved by: -Buying at prices[0] = 1 -Selling at prices[3] = 8 -Buying at prices[4] = 4 -Selling at prices[5] = 9 -The total profit is ((8 - 1) - 2) + ((9 - 4) - 2) = 8. -``` - -题目描述:每交易一次,都要支付一定的费用。 - -

- -```java -public int maxProfit(int[] prices, int fee) { - int N = prices.length; - int[] buy = new int[N]; - int[] s1 = new int[N]; - int[] sell = new int[N]; - int[] s2 = new int[N]; - s1[0] = buy[0] = -prices[0]; - sell[0] = s2[0] = 0; - for (int i = 1; i < N; i++) { - buy[i] = Math.max(sell[i - 1], s2[i - 1]) - prices[i]; - s1[i] = Math.max(buy[i - 1], s1[i - 1]); - sell[i] = Math.max(buy[i - 1], s1[i - 1]) - fee + prices[i]; - s2[i] = Math.max(s2[i - 1], sell[i - 1]); - } - return Math.max(sell[N - 1], s2[N - 1]); -} -``` - - -**只能进行两次的股票交易** - -[123. Best Time to Buy and Sell Stock III (Hard)](https://leetcode.com/problems/best-time-to-buy-and-sell-stock-iii/description/) - -```java -public int maxProfit(int[] prices) { - int firstBuy = Integer.MIN_VALUE, firstSell = 0; - int secondBuy = Integer.MIN_VALUE, secondSell = 0; - for (int curPrice : prices) { - if (firstBuy < -curPrice) { - firstBuy = -curPrice; - } - if (firstSell < firstBuy + curPrice) { - firstSell = firstBuy + curPrice; - } - if (secondBuy < firstSell - curPrice) { - secondBuy = firstSell - curPrice; - } - if (secondSell < secondBuy + curPrice) { - secondSell = secondBuy + curPrice; - } - } - return secondSell; -} -``` - -**只能进行 k 次的股票交易** - -[188. Best Time to Buy and Sell Stock IV (Hard)](https://leetcode.com/problems/best-time-to-buy-and-sell-stock-iv/description/) - -```java -public int maxProfit(int k, int[] prices) { - int n = prices.length; - if (k >= n / 2) { // 这种情况下该问题退化为普通的股票交易问题 - int maxProfit = 0; - for (int i = 1; i < n; i++) { - if (prices[i] > prices[i - 1]) { - maxProfit += prices[i] - prices[i - 1]; - } - } - return maxProfit; - } - int[][] maxProfit = new int[k + 1][n]; - for (int i = 1; i <= k; i++) { - int localMax = maxProfit[i - 1][0] - prices[0]; - for (int j = 1; j < n; j++) { - maxProfit[i][j] = Math.max(maxProfit[i][j - 1], prices[j] + localMax); - localMax = Math.max(localMax, maxProfit[i - 1][j] - prices[j]); - } - } - return maxProfit[k][n - 1]; -} -``` - -### 字符串编辑 - -**删除两个字符串的字符使它们相等** - -[583. Delete Operation for Two Strings (Medium)](https://leetcode.com/problems/delete-operation-for-two-strings/description/) - -```html -Input: "sea", "eat" -Output: 2 -Explanation: You need one step to make "sea" to "ea" and another step to make "eat" to "ea". -``` - -可以转换为求两个字符串的最长公共子序列问题。 - -```java -public int minDistance(String word1, String word2) { - int m = word1.length(), n = word2.length(); - int[][] dp = new int[m + 1][n + 1]; - for (int i = 1; i <= m; i++) { - for (int j = 1; j <= n; j++) { - if (word1.charAt(i - 1) == word2.charAt(j - 1)) { - dp[i][j] = dp[i - 1][j - 1] + 1; - } else { - dp[i][j] = Math.max(dp[i][j - 1], dp[i - 1][j]); - } - } - } - return m + n - 2 * dp[m][n]; -} -``` - -**编辑距离** - -[72. Edit Distance (Hard)](https://leetcode.com/problems/edit-distance/description/) - -```html -Example 1: - -Input: word1 = "horse", word2 = "ros" -Output: 3 -Explanation: -horse -> rorse (replace 'h' with 'r') -rorse -> rose (remove 'r') -rose -> ros (remove 'e') -Example 2: - -Input: word1 = "intention", word2 = "execution" -Output: 5 -Explanation: -intention -> inention (remove 't') -inention -> enention (replace 'i' with 'e') -enention -> exention (replace 'n' with 'x') -exention -> exection (replace 'n' with 'c') -exection -> execution (insert 'u') -``` - -题目描述:修改一个字符串成为另一个字符串,使得修改次数最少。一次修改操作包括:插入一个字符、删除一个字符、替换一个字符。 - -```java -public int minDistance(String word1, String word2) { - if (word1 == null || word2 == null) { - return 0; - } - int m = word1.length(), n = word2.length(); - int[][] dp = new int[m + 1][n + 1]; - for (int i = 1; i <= m; i++) { - dp[i][0] = i; - } - for (int i = 1; i <= n; i++) { - dp[0][i] = i; - } - for (int i = 1; i <= m; i++) { - for (int j = 1; j <= n; j++) { - if (word1.charAt(i - 1) == word2.charAt(j - 1)) { - dp[i][j] = dp[i - 1][j - 1]; - } else { - dp[i][j] = Math.min(dp[i - 1][j - 1], Math.min(dp[i][j - 1], dp[i - 1][j])) + 1; - } - } - } - return dp[m][n]; -} -``` - -**复制粘贴字符** - -[650. 2 Keys Keyboard (Medium)](https://leetcode.com/problems/2-keys-keyboard/description/) - -题目描述:最开始只有一个字符 A,问需要多少次操作能够得到 n 个字符 A,每次操作可以复制当前所有的字符,或者粘贴。 - -``` -Input: 3 -Output: 3 -Explanation: -Intitally, we have one character 'A'. -In step 1, we use Copy All operation. -In step 2, we use Paste operation to get 'AA'. -In step 3, we use Paste operation to get 'AAA'. -``` - -```java -public int minSteps(int n) { - if (n == 1) return 0; - for (int i = 2; i <= Math.sqrt(n); i++) { - if (n % i == 0) return i + minSteps(n / i); - } - return n; -} -``` - -```java -public int minSteps(int n) { - int[] dp = new int[n + 1]; - int h = (int) Math.sqrt(n); - for (int i = 2; i <= n; i++) { - dp[i] = i; - for (int j = 2; j <= h; j++) { - if (i % j == 0) { - dp[i] = dp[j] + dp[i / j]; - break; - } - } - } - return dp[n]; -} -``` - -## 数学 - -### 素数 - -**素数分解** - -每一个数都可以分解成素数的乘积,例如 84 = 22 \* 31 \* 50 \* 71 \* 110 \* 130 \* 170 \* … - -**整除** - -令 x = 2m0 \* 3m1 \* 5m2 \* 7m3 \* 11m4 \* … - -令 y = 2n0 \* 3n1 \* 5n2 \* 7n3 \* 11n4 \* … - -如果 x 整除 y(y mod x == 0),则对于所有 i,mi <= ni。 - -**最大公约数最小公倍数** - -x 和 y 的最大公约数为:gcd(x,y) = 2min(m0,n0) \* 3min(m1,n1) \* 5min(m2,n2) \* ... - -x 和 y 的最小公倍数为:lcm(x,y) = 2max(m0,n0) \* 3max(m1,n1) \* 5max(m2,n2) \* ... - -**生成素数序列** - -[204. Count Primes (Easy)](https://leetcode.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; -} -``` - -### 最大公约数 - -```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); -} -``` - -**使用位操作和减法求解最大公约数** - -[编程之美: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); - } -} -``` - -### 进制转换 - -**7 进制** - -[504. Base 7 (Easy)](https://leetcode.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); -} -``` - -**16 进制** - -[405. Convert a Number to Hexadecimal (Easy)](https://leetcode.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(); -} -``` - -**26 进制** - -[168. Excel Sheet Column Title (Easy)](https://leetcode.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'); -} -``` - -### 阶乘 - -**统计阶乘尾部有多少个 0** - -[172. Factorial Trailing Zeroes (Easy)](https://leetcode.com/problems/factorial-trailing-zeroes/description/) - -尾部的 0 由 2 * 5 得来,2 的数量明显多于 5 的数量,因此只要统计有多少个 5 即可。 - -对于一个数 N,它所包含 5 的个数为:N/5 + N/52 + N/53 + ...,其中 N/5 表示不大于 N 的数中 5 的倍数贡献一个 5,N/52 表示不大于 N 的数中 52 的倍数再贡献一个 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/22 + N/23 + ... - -### 字符串加法减法 - -**二进制加法** - -[67. Add Binary (Easy)](https://leetcode.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(); -} -``` - -**字符串加法** - -[415. Add Strings (Easy)](https://leetcode.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(); -} -``` - -### 相遇问题 - -**改变数组元素使所有的数组元素都相等** - -[462. Minimum Moves to Equal Array Elements II (Medium)](https://leetcode.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; -} -``` - -### 多数投票问题 - -**数组中出现次数多于 n / 2 的元素** - -[169. Majority Element (Easy)](https://leetcode.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; -} -``` - -### 其它 - -**平方数** - -[367. Valid Perfect Square (Easy)](https://leetcode.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; -} -``` - -**3 的 n 次方** - -[326. Power of Three (Easy)](https://leetcode.com/problems/power-of-three/description/) - -```java -public boolean isPowerOfThree(int n) { - return n > 0 && (1162261467 % n == 0); -} -``` - -**乘积数组** - -[238. Product of Array Except Self (Medium)](https://leetcode.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; -} -``` - -**找出数组中的乘积最大的三个数** - -[628. Maximum Product of Three Numbers (Easy)](https://leetcode.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); -} -``` - -# 数据结构相关 - -## 链表 - -链表是空节点,或者有一个值和一个指向下一个链表的指针,因此很多链表问题可以用递归来处理。 - -**找出两个链表的交点** - -[160. Intersection of Two Linked Lists (Easy)](https://leetcode.com/problems/intersection-of-two-linked-lists/description/) - -```html -A: a1 → a2 - ↘ - c1 → c2 → c3 - ↗ -B: b1 → b2 → b3 -``` - -要求:时间复杂度为 O(N),空间复杂度为 O(1) - -设 A 的长度为 a + c,B 的长度为 b + c,其中 c 为尾部公共部分长度,可知 a + c + b = b + c + a。 - -当访问 A 链表的指针访问到链表尾部时,令它从链表 B 的头部开始访问链表 B;同样地,当访问 B 链表的指针访问到链表尾部时,令它从链表 A 的头部开始访问链表 A。这样就能控制访问 A 和 B 两个链表的指针能同时访问到交点。 - -```java -public ListNode getIntersectionNode(ListNode headA, ListNode headB) { - ListNode l1 = headA, l2 = headB; - while (l1 != l2) { - l1 = (l1 == null) ? headB : l1.next; - l2 = (l2 == null) ? headA : l2.next; - } - return l1; -} -``` - -如果只是判断是否存在交点,那么就是另一个问题,即 [编程之美 3.6]() 的问题。有两种解法: - -- 把第一个链表的结尾连接到第二个链表的开头,看第二个链表是否存在环; -- 或者直接比较两个链表的最后一个节点是否相同。 - -**链表反转** - -[206. Reverse Linked List (Easy)](https://leetcode.com/problems/reverse-linked-list/description/) - -递归 - -```java -public ListNode reverseList(ListNode head) { - if (head == null || head.next == null) { - return head; - } - ListNode next = head.next; - ListNode newHead = reverseList(next); - next.next = head; - head.next = null; - return newHead; -} -``` - -头插法 - -```java -public ListNode reverseList(ListNode head) { - ListNode newHead = new ListNode(-1); - while (head != null) { - ListNode next = head.next; - head.next = newHead.next; - newHead.next = head; - head = next; - } - return newHead.next; -} -``` - -**归并两个有序的链表** - -[21. Merge Two Sorted Lists (Easy)](https://leetcode.com/problems/merge-two-sorted-lists/description/) - -```java -public ListNode mergeTwoLists(ListNode l1, ListNode l2) { - if (l1 == null) return l2; - if (l2 == null) return l1; - if (l1.val < l2.val) { - l1.next = mergeTwoLists(l1.next, l2); - return l1; - } else { - l2.next = mergeTwoLists(l1, l2.next); - return l2; - } -} -``` - -**从有序链表中删除重复节点** - -[83. Remove Duplicates from Sorted List (Easy)](https://leetcode.com/problems/remove-duplicates-from-sorted-list/description/) - -```html -Given 1->1->2, return 1->2. -Given 1->1->2->3->3, return 1->2->3. -``` - -```java -public ListNode deleteDuplicates(ListNode head) { - if (head == null || head.next == null) return head; - head.next = deleteDuplicates(head.next); - return head.val == head.next.val ? head.next : head; -} -``` - -**删除链表的倒数第 n 个节点** - -[19. Remove Nth Node From End of List (Medium)](https://leetcode.com/problems/remove-nth-node-from-end-of-list/description/) - -```html -Given linked list: 1->2->3->4->5, and n = 2. -After removing the second node from the end, the linked list becomes 1->2->3->5. -``` - -```java -public ListNode removeNthFromEnd(ListNode head, int n) { - ListNode fast = head; - while (n-- > 0) { - fast = fast.next; - } - if (fast == null) return head.next; - ListNode slow = head; - while (fast.next != null) { - fast = fast.next; - slow = slow.next; - } - slow.next = slow.next.next; - return head; -} -``` - -**交换链表中的相邻结点** - -[24. Swap Nodes in Pairs (Medium)](https://leetcode.com/problems/swap-nodes-in-pairs/description/) - -```html -Given 1->2->3->4, you should return the list as 2->1->4->3. -``` - -题目要求:不能修改结点的 val 值,O(1) 空间复杂度。 - -```java -public ListNode swapPairs(ListNode head) { - ListNode node = new ListNode(-1); - node.next = head; - ListNode pre = node; - while (pre.next != null && pre.next.next != null) { - ListNode l1 = pre.next, l2 = pre.next.next; - ListNode next = l2.next; - l1.next = next; - l2.next = l1; - pre.next = l2; - - pre = l1; - } - return node.next; -} -``` - -**链表求和** - -[445. Add Two Numbers II (Medium)](https://leetcode.com/problems/add-two-numbers-ii/description/) - -```html -Input: (7 -> 2 -> 4 -> 3) + (5 -> 6 -> 4) -Output: 7 -> 8 -> 0 -> 7 -``` - -题目要求:不能修改原始链表。 - -```java -public ListNode addTwoNumbers(ListNode l1, ListNode l2) { - Stack l1Stack = buildStack(l1); - Stack l2Stack = buildStack(l2); - ListNode head = new ListNode(-1); - int carry = 0; - while (!l1Stack.isEmpty() || !l2Stack.isEmpty() || carry != 0) { - int x = l1Stack.isEmpty() ? 0 : l1Stack.pop(); - int y = l2Stack.isEmpty() ? 0 : l2Stack.pop(); - int sum = x + y + carry; - ListNode node = new ListNode(sum % 10); - node.next = head.next; - head.next = node; - carry = sum / 10; - } - return head.next; -} - -private Stack buildStack(ListNode l) { - Stack stack = new Stack<>(); - while (l != null) { - stack.push(l.val); - l = l.next; - } - return stack; -} -``` - -**回文链表** - -[234. Palindrome Linked List (Easy)](https://leetcode.com/problems/palindrome-linked-list/description/) - -题目要求:以 O(1) 的空间复杂度来求解。 - -切成两半,把后半段反转,然后比较两半是否相等。 - -```java -public boolean isPalindrome(ListNode head) { - if (head == null || head.next == null) return true; - ListNode slow = head, fast = head.next; - while (fast != null && fast.next != null) { - slow = slow.next; - fast = fast.next.next; - } - if (fast != null) slow = slow.next; // 偶数节点,让 slow 指向下一个节点 - cut(head, slow); // 切成两个链表 - return isEqual(head, reverse(slow)); -} - -private void cut(ListNode head, ListNode cutNode) { - while (head.next != cutNode) { - head = head.next; - } - head.next = null; -} - -private ListNode reverse(ListNode head) { - ListNode newHead = null; - while (head != null) { - ListNode nextNode = head.next; - head.next = newHead; - newHead = head; - head = nextNode; - } - return newHead; -} - -private boolean isEqual(ListNode l1, ListNode l2) { - while (l1 != null && l2 != null) { - if (l1.val != l2.val) return false; - l1 = l1.next; - l2 = l2.next; - } - return true; -} -``` - -**分隔链表** - -[725. Split Linked List in Parts(Medium)](https://leetcode.com/problems/split-linked-list-in-parts/description/) - -```html -Input: -root = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], k = 3 -Output: [[1, 2, 3, 4], [5, 6, 7], [8, 9, 10]] -Explanation: -The input has been split into consecutive parts with size difference at most 1, and earlier parts are a larger size than the later parts. -``` - -题目描述:把链表分隔成 k 部分,每部分的长度都应该尽可能相同,排在前面的长度应该大于等于后面的。 - -```java -public ListNode[] splitListToParts(ListNode root, int k) { - int N = 0; - ListNode cur = root; - while (cur != null) { - N++; - cur = cur.next; - } - int mod = N % k; - int size = N / k; - ListNode[] ret = new ListNode[k]; - cur = root; - for (int i = 0; cur != null && i < k; i++) { - ret[i] = cur; - int curSize = size + (mod-- > 0 ? 1 : 0); - for (int j = 0; j < curSize - 1; j++) { - cur = cur.next; - } - ListNode next = cur.next; - cur.next = null; - cur = next; - } - return ret; -} -``` - -**链表元素按奇偶聚集** - -[328. Odd Even Linked List (Medium)](https://leetcode.com/problems/odd-even-linked-list/description/) - -```html -Example: -Given 1->2->3->4->5->NULL, -return 1->3->5->2->4->NULL. -``` - -```java -public ListNode oddEvenList(ListNode head) { - if (head == null) { - return head; - } - ListNode odd = head, even = head.next, evenHead = even; - while (even != null && even.next != null) { - odd.next = odd.next.next; - odd = odd.next; - even.next = even.next.next; - even = even.next; - } - odd.next = evenHead; - return head; -} -``` - -## 树 - -### 递归 - -一棵树要么是空树,要么有两个指针,每个指针指向一棵树。树是一种递归结构,很多树的问题可以使用递归来处理。 - -**树的高度** - -[104. Maximum Depth of Binary Tree (Easy)](https://leetcode.com/problems/maximum-depth-of-binary-tree/description/) - -```java -public int maxDepth(TreeNode root) { - if (root == null) return 0; - return Math.max(maxDepth(root.left), maxDepth(root.right)) + 1; -} -``` - -**平衡树** - -[110. Balanced Binary Tree (Easy)](https://leetcode.com/problems/balanced-binary-tree/description/) - -```html - 3 - / \ - 9 20 - / \ - 15 7 -``` - -平衡树左右子树高度差都小于等于 1 - -```java -private boolean result = true; - -public boolean isBalanced(TreeNode root) { - maxDepth(root); - return result; -} - -public int maxDepth(TreeNode root) { - if (root == null) return 0; - int l = maxDepth(root.left); - int r = maxDepth(root.right); - if (Math.abs(l - r) > 1) result = false; - return 1 + Math.max(l, r); -} -``` - -**两节点的最长路径** - -[543. Diameter of Binary Tree (Easy)](https://leetcode.com/problems/diameter-of-binary-tree/description/) - -```html -Input: - - 1 - / \ - 2 3 - / \ - 4 5 - -Return 3, which is the length of the path [4,2,1,3] or [5,2,1,3]. -``` - -```java -private int max = 0; - -public int diameterOfBinaryTree(TreeNode root) { - depth(root); - return max; -} - -private int depth(TreeNode root) { - if (root == null) return 0; - int leftDepth = depth(root.left); - int rightDepth = depth(root.right); - max = Math.max(max, leftDepth + rightDepth); - return Math.max(leftDepth, rightDepth) + 1; -} -``` - -**翻转树** - -[226. Invert Binary Tree (Easy)](https://leetcode.com/problems/invert-binary-tree/description/) - -```java -public TreeNode invertTree(TreeNode root) { - if (root == null) return null; - TreeNode left = root.left; // 后面的操作会改变 left 指针,因此先保存下来 - root.left = invertTree(root.right); - root.right = invertTree(left); - return root; -} -``` - -**归并两棵树** - -[617. Merge Two Binary Trees (Easy)](https://leetcode.com/problems/merge-two-binary-trees/description/) - -```html -Input: - Tree 1 Tree 2 - 1 2 - / \ / \ - 3 2 1 3 - / \ \ - 5 4 7 - -Output: - 3 - / \ - 4 5 - / \ \ - 5 4 7 -``` - -```java -public TreeNode mergeTrees(TreeNode t1, TreeNode t2) { - if (t1 == null && t2 == null) return null; - if (t1 == null) return t2; - if (t2 == null) return t1; - TreeNode root = new TreeNode(t1.val + t2.val); - root.left = mergeTrees(t1.left, t2.left); - root.right = mergeTrees(t1.right, t2.right); - return root; -} -``` - -**判断路径和是否等于一个数** - -[Leetcdoe : 112. Path Sum (Easy)](https://leetcode.com/problems/path-sum/description/) - -```html -Given the below binary tree and sum = 22, - - 5 - / \ - 4 8 - / / \ - 11 13 4 - / \ \ - 7 2 1 - -return true, as there exist a root-to-leaf path 5->4->11->2 which sum is 22. -``` - -路径和定义为从 root 到 leaf 的所有节点的和。 - -```java -public boolean hasPathSum(TreeNode root, int sum) { - if (root == null) return false; - if (root.left == null && root.right == null && root.val == sum) return true; - return hasPathSum(root.left, sum - root.val) || hasPathSum(root.right, sum - root.val); -} -``` - -**统计路径和等于一个数的路径数量** - -[437. Path Sum III (Easy)](https://leetcode.com/problems/path-sum-iii/description/) - -```html -root = [10,5,-3,3,2,null,11,3,-2,null,1], sum = 8 - - 10 - / \ - 5 -3 - / \ \ - 3 2 11 - / \ \ -3 -2 1 - -Return 3. The paths that sum to 8 are: - -1. 5 -> 3 -2. 5 -> 2 -> 1 -3. -3 -> 11 -``` - -路径不一定以 root 开头,也不一定以 leaf 结尾,但是必须连续。 - -```java -public int pathSum(TreeNode root, int sum) { - if (root == null) return 0; - int ret = pathSumStartWithRoot(root, sum) + pathSum(root.left, sum) + pathSum(root.right, sum); - return ret; -} - -private int pathSumStartWithRoot(TreeNode root, int sum) { - if (root == null) return 0; - int ret = 0; - if (root.val == sum) ret++; - ret += pathSumStartWithRoot(root.left, sum - root.val) + pathSumStartWithRoot(root.right, sum - root.val); - return ret; -} -``` - -**子树** - -[572. Subtree of Another Tree (Easy)](https://leetcode.com/problems/subtree-of-another-tree/description/) - -```html -Given tree s: - 3 - / \ - 4 5 - / \ - 1 2 - -Given tree t: - 4 - / \ - 1 2 - -Return true, because t has the same structure and node values with a subtree of s. - -Given tree s: - - 3 - / \ - 4 5 - / \ - 1 2 - / - 0 - -Given tree t: - 4 - / \ - 1 2 - -Return false. -``` - -```java -public boolean isSubtree(TreeNode s, TreeNode t) { - if (s == null) return false; - return isSubtreeWithRoot(s, t) || isSubtree(s.left, t) || isSubtree(s.right, t); -} - -private boolean isSubtreeWithRoot(TreeNode s, TreeNode t) { - if (t == null && s == null) return true; - if (t == null || s == null) return false; - if (t.val != s.val) return false; - return isSubtreeWithRoot(s.left, t.left) && isSubtreeWithRoot(s.right, t.right); -} -``` - -**树的对称** - -[101. Symmetric Tree (Easy)](https://leetcode.com/problems/symmetric-tree/description/) - -```html - 1 - / \ - 2 2 - / \ / \ -3 4 4 3 -``` - -```java -public boolean isSymmetric(TreeNode root) { - if (root == null) return true; - return isSymmetric(root.left, root.right); -} - -private boolean isSymmetric(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 isSymmetric(t1.left, t2.right) && isSymmetric(t1.right, t2.left); -} -``` - -**最小路径** - -[111. Minimum Depth of Binary Tree (Easy)](https://leetcode.com/problems/minimum-depth-of-binary-tree/description/) - -树的根节点到叶子节点的最小路径长度 - -```java -public int minDepth(TreeNode root) { - if (root == null) return 0; - int left = minDepth(root.left); - int right = minDepth(root.right); - if (left == 0 || right == 0) return left + right + 1; - return Math.min(left, right) + 1; -} -``` - -**统计左叶子节点的和** - -[404. Sum of Left Leaves (Easy)](https://leetcode.com/problems/sum-of-left-leaves/description/) - -```html - 3 - / \ - 9 20 - / \ - 15 7 - -There are two left leaves in the binary tree, with values 9 and 15 respectively. Return 24. -``` - -```java -public int sumOfLeftLeaves(TreeNode root) { - if (root == null) return 0; - if (isLeaf(root.left)) return root.left.val + sumOfLeftLeaves(root.right); - return sumOfLeftLeaves(root.left) + sumOfLeftLeaves(root.right); -} - -private boolean isLeaf(TreeNode node){ - if (node == null) return false; - return node.left == null && node.right == null; -} -``` - -**相同节点值的最大路径长度** - -[687. Longest Univalue Path (Easy)](https://leetcode.com/problems/longest-univalue-path/) - -```html - 1 - / \ - 4 5 - / \ \ - 4 4 5 - -Output : 2 -``` - -```java -private int path = 0; - -public int longestUnivaluePath(TreeNode root) { - dfs(root); - return path; -} - -private int dfs(TreeNode root){ - if (root == null) return 0; - int left = dfs(root.left); - int right = dfs(root.right); - int leftPath = root.left != null && root.left.val == root.val ? left + 1 : 0; - int rightPath = root.right != null && root.right.val == root.val ? right + 1 : 0; - path = Math.max(path, leftPath + rightPath); - return Math.max(leftPath, rightPath); -} -``` - -**间隔遍历** - -[337. House Robber III (Medium)](https://leetcode.com/problems/house-robber-iii/description/) - -```html - 3 - / \ - 2 3 - \ \ - 3 1 -Maximum amount of money the thief can rob = 3 + 3 + 1 = 7. -``` - -```java -public int rob(TreeNode root) { - if (root == null) return 0; - int val1 = root.val; - if (root.left != null) val1 += rob(root.left.left) + rob(root.left.right); - if (root.right != null) val1 += rob(root.right.left) + rob(root.right.right); - int val2 = rob(root.left) + rob(root.right); - return Math.max(val1, val2); -} -``` - -**找出二叉树中第二小的节点** - -[671. Second Minimum Node In a Binary Tree (Easy)](https://leetcode.com/problems/second-minimum-node-in-a-binary-tree/description/) - -```html -Input: - 2 - / \ - 2 5 - / \ - 5 7 - -Output: 5 -``` - -一个节点要么具有 0 个或 2 个子节点,如果有子节点,那么根节点是最小的节点。 - -```java -public int findSecondMinimumValue(TreeNode root) { - if (root == null) return -1; - if (root.left == null && root.right == null) return -1; - int leftVal = root.left.val; - int rightVal = root.right.val; - if (leftVal == root.val) leftVal = findSecondMinimumValue(root.left); - if (rightVal == root.val) rightVal = findSecondMinimumValue(root.right); - if (leftVal != -1 && rightVal != -1) return Math.min(leftVal, rightVal); - if (leftVal != -1) return leftVal; - return rightVal; -} -``` - -### 层次遍历 - -使用 BFS 进行层次遍历。不需要使用两个队列来分别存储当前层的节点和下一层的节点,因为在开始遍历一层的节点时,当前队列中的节点数就是当前层的节点数,只要控制遍历这么多节点数,就能保证这次遍历的都是当前层的节点。 - -**一棵树每层节点的平均数** - -[637. Average of Levels in Binary Tree (Easy)](https://leetcode.com/problems/average-of-levels-in-binary-tree/description/) - -```java -public List averageOfLevels(TreeNode root) { - List ret = new ArrayList<>(); - if (root == null) return ret; - Queue queue = new LinkedList<>(); - queue.add(root); - while (!queue.isEmpty()) { - int cnt = queue.size(); - double sum = 0; - for (int i = 0; i < cnt; i++) { - TreeNode node = queue.poll(); - sum += node.val; - if (node.left != null) queue.add(node.left); - if (node.right != null) queue.add(node.right); - } - ret.add(sum / cnt); - } - return ret; -} -``` - -**得到左下角的节点** - -[513. Find Bottom Left Tree Value (Easy)](https://leetcode.com/problems/find-bottom-left-tree-value/description/) - -```html -Input: - - 1 - / \ - 2 3 - / / \ - 4 5 6 - / - 7 - -Output: -7 -``` - -```java -public int findBottomLeftValue(TreeNode root) { - Queue queue = new LinkedList<>(); - queue.add(root); - while (!queue.isEmpty()) { - root = queue.poll(); - if (root.right != null) queue.add(root.right); - if (root.left != null) queue.add(root.left); - } - return root.val; -} -``` - -### 前中后序遍历 - -```html - 1 - / \ - 2 3 - / \ \ -4 5 6 -``` - -- 层次遍历顺序:[1 2 3 4 5 6] -- 前序遍历顺序:[1 2 4 5 3 6] -- 中序遍历顺序:[4 2 5 1 3 6] -- 后序遍历顺序:[4 5 2 6 3 1] - -层次遍历使用 BFS 实现,利用的就是 BFS 一层一层遍历的特性;而前序、中序、后序遍历利用了 DFS 实现。 - -前序、中序、后序遍只是在对节点访问的顺序有一点不同,其它都相同。 - -① 前序 - -```java -void dfs(TreeNode root) { - visit(root); - dfs(root.left); - dfs(root.right); -} -``` - -② 中序 - -```java -void dfs(TreeNode root) { - dfs(root.left); - visit(root); - dfs(root.right); -} -``` - -③ 后序 - -```java -void dfs(TreeNode root) { - dfs(root.left); - dfs(root.right); - visit(root); -} -``` - -**非递归实现二叉树的前序遍历** - -[144. Binary Tree Preorder Traversal (Medium)](https://leetcode.com/problems/binary-tree-preorder-traversal/description/) - -```java -public List preorderTraversal(TreeNode root) { - List ret = new ArrayList<>(); - Stack stack = new Stack<>(); - stack.push(root); - while (!stack.isEmpty()) { - TreeNode node = stack.pop(); - if (node == null) continue; - ret.add(node.val); - stack.push(node.right); // 先右后左,保证左子树先遍历 - stack.push(node.left); - } - return ret; -} -``` - -**非递归实现二叉树的后序遍历** - -[145. Binary Tree Postorder Traversal (Medium)](https://leetcode.com/problems/binary-tree-postorder-traversal/description/) - -前序遍历为 root -> left -> right,后序遍历为 left -> right -> root。可以修改前序遍历成为 root -> right -> left,那么这个顺序就和后序遍历正好相反。 - -```java -public List postorderTraversal(TreeNode root) { - List ret = new ArrayList<>(); - Stack stack = new Stack<>(); - stack.push(root); - while (!stack.isEmpty()) { - TreeNode node = stack.pop(); - if (node == null) continue; - ret.add(node.val); - stack.push(node.left); - stack.push(node.right); - } - Collections.reverse(ret); - return ret; -} -``` - -**非递归实现二叉树的中序遍历** - -[94. Binary Tree Inorder Traversal (Medium)](https://leetcode.com/problems/binary-tree-inorder-traversal/description/) - -```java -public List inorderTraversal(TreeNode root) { - List ret = new ArrayList<>(); - if (root == null) return ret; - Stack stack = new Stack<>(); - TreeNode cur = root; - while (cur != null || !stack.isEmpty()) { - while (cur != null) { - stack.push(cur); - cur = cur.left; - } - TreeNode node = stack.pop(); - ret.add(node.val); - cur = node.right; - } - return ret; -} -``` - -### BST - -二叉查找树(BST):根节点大于等于左子树所有节点,小于等于右子树所有节点。 - -二叉查找树中序遍历有序。 - -**修剪二叉查找树** - -[669. Trim a Binary Search Tree (Easy)](https://leetcode.com/problems/trim-a-binary-search-tree/description/) - -```html -Input: - - 3 - / \ - 0 4 - \ - 2 - / - 1 - - L = 1 - R = 3 - -Output: - - 3 - / - 2 - / - 1 -``` - -题目描述:只保留值在 L \~ R 之间的节点 - -```java -public TreeNode trimBST(TreeNode root, int L, int R) { - if (root == null) return null; - if (root.val > R) return trimBST(root.left, L, R); - if (root.val < L) return trimBST(root.right, L, R); - root.left = trimBST(root.left, L, R); - root.right = trimBST(root.right, L, R); - return root; -} -``` - -**寻找二叉查找树的第 k 个元素** - -[230. Kth Smallest Element in a BST (Medium)](https://leetcode.com/problems/kth-smallest-element-in-a-bst/description/) - - -中序遍历解法: - -```java -private int cnt = 0; -private int val; - -public int kthSmallest(TreeNode root, int k) { - inOrder(root, k); - return val; -} - -private void inOrder(TreeNode node, int k) { - if (node == null) return; - inOrder(node.left, k); - cnt++; - if (cnt == k) { - val = node.val; - return; - } - inOrder(node.right, k); -} -``` - -递归解法: - -```java -public int kthSmallest(TreeNode root, int k) { - int leftCnt = count(root.left); - if (leftCnt == k - 1) return root.val; - if (leftCnt > k - 1) return kthSmallest(root.left, k); - return kthSmallest(root.right, k - leftCnt - 1); -} - -private int count(TreeNode node) { - if (node == null) return 0; - return 1 + count(node.left) + count(node.right); -} -``` - -**把二叉查找树每个节点的值都加上比它大的节点的值** - -[Convert BST to Greater Tree (Easy)](https://leetcode.com/problems/convert-bst-to-greater-tree/description/) - -```html -Input: The root of a Binary Search Tree like this: - - 5 - / \ - 2 13 - -Output: The root of a Greater Tree like this: - - 18 - / \ - 20 13 -``` - -先遍历右子树。 - -```java -private int sum = 0; - -public TreeNode convertBST(TreeNode root) { - traver(root); - return root; -} - -private void traver(TreeNode node) { - if (node == null) return; - traver(node.right); - sum += node.val; - node.val = sum; - traver(node.left); -} -``` - -**二叉查找树的最近公共祖先** - -[235. Lowest Common Ancestor of a Binary Search Tree (Easy)](https://leetcode.com/problems/lowest-common-ancestor-of-a-binary-search-tree/description/) - -```html - _______6______ - / \ - ___2__ ___8__ - / \ / \ -0 4 7 9 - / \ - 3 5 - -For example, the lowest common ancestor (LCA) of nodes 2 and 8 is 6. Another example is LCA of nodes 2 and 4 is 2, since a node can be a descendant of itself according to the LCA definition. -``` - -```java -public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) { - if (root.val > p.val && root.val > q.val) return lowestCommonAncestor(root.left, p, q); - if (root.val < p.val && root.val < q.val) return lowestCommonAncestor(root.right, p, q); - return root; -} -``` - -**二叉树的最近公共祖先** - -[236. Lowest Common Ancestor of a Binary Tree (Medium) ](https://leetcode.com/problems/lowest-common-ancestor-of-a-binary-tree/description/) - -```html - _______3______ - / \ - ___5__ ___1__ - / \ / \ -6 2 0 8 - / \ - 7 4 - -For example, the lowest common ancestor (LCA) of nodes 5 and 1 is 3. Another example is LCA of nodes 5 and 4 is 5, since a node can be a descendant of itself according to the LCA definition. -``` - -```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; -} -``` - -**从有序数组中构造二叉查找树** - -[108. Convert Sorted Array to Binary Search Tree (Easy)](https://leetcode.com/problems/convert-sorted-array-to-binary-search-tree/description/) - -```java -public TreeNode sortedArrayToBST(int[] nums) { - return toBST(nums, 0, nums.length - 1); -} - -private TreeNode toBST(int[] nums, int sIdx, int eIdx){ - if (sIdx > eIdx) return null; - int mIdx = (sIdx + eIdx) / 2; - TreeNode root = new TreeNode(nums[mIdx]); - root.left = toBST(nums, sIdx, mIdx - 1); - root.right = toBST(nums, mIdx + 1, eIdx); - return root; -} -``` - -**根据有序链表构造平衡的二叉查找树** - -[109. Convert Sorted List to Binary Search Tree (Medium)](https://leetcode.com/problems/convert-sorted-list-to-binary-search-tree/description/) - -```html -Given the sorted linked list: [-10,-3,0,5,9], - -One possible answer is: [0,-3,9,-10,null,5], which represents the following height balanced BST: - - 0 - / \ - -3 9 - / / - -10 5 -``` - -```java -public TreeNode sortedListToBST(ListNode head) { - if (head == null) return null; - if (head.next == null) return new TreeNode(head.val); - ListNode preMid = preMid(head); - ListNode mid = preMid.next; - preMid.next = null; // 断开链表 - TreeNode t = new TreeNode(mid.val); - t.left = sortedListToBST(head); - t.right = sortedListToBST(mid.next); - return t; -} - -private ListNode preMid(ListNode head) { - ListNode slow = head, fast = head.next; - ListNode pre = head; - while (fast != null && fast.next != null) { - pre = slow; - slow = slow.next; - fast = fast.next.next; - } - return pre; -} -``` - -**在二叉查找树中寻找两个节点,使它们的和为一个给定值** - -[653. Two Sum IV - Input is a BST (Easy)](https://leetcode.com/problems/two-sum-iv-input-is-a-bst/description/) - -```html -Input: - - 5 - / \ - 3 6 - / \ \ -2 4 7 - -Target = 9 - -Output: True -``` - -使用中序遍历得到有序数组之后,再利用双指针对数组进行查找。 - -应该注意到,这一题不能用分别在左右子树两部分来处理这种思想,因为两个待求的节点可能分别在左右子树中。 - -```java -public boolean findTarget(TreeNode root, int k) { - List nums = new ArrayList<>(); - inOrder(root, nums); - int i = 0, j = nums.size() - 1; - while (i < j) { - int sum = nums.get(i) + nums.get(j); - if (sum == k) return true; - if (sum < k) i++; - else j--; - } - return false; -} - -private void inOrder(TreeNode root, List nums) { - if (root == null) return; - inOrder(root.left, nums); - nums.add(root.val); - inOrder(root.right, nums); -} -``` - -**在二叉查找树中查找两个节点之差的最小绝对值** - -[530. Minimum Absolute Difference in BST (Easy)](https://leetcode.com/problems/minimum-absolute-difference-in-bst/description/) - -```html -Input: - - 1 - \ - 3 - / - 2 - -Output: - -1 -``` - -利用二叉查找树的中序遍历为有序的性质,计算中序遍历中临近的两个节点之差的绝对值,取最小值。 - -```java -private int minDiff = Integer.MAX_VALUE; -private TreeNode preNode = null; - -public int getMinimumDifference(TreeNode root) { - inOrder(root); - return minDiff; -} - -private void inOrder(TreeNode node) { - if (node == null) return; - inOrder(node.left); - if (preNode != null) minDiff = Math.min(minDiff, node.val - preNode.val); - preNode = node; - inOrder(node.right); -} -``` - -**寻找二叉查找树中出现次数最多的值** - -[501. Find Mode in Binary Search Tree (Easy)](https://leetcode.com/problems/find-mode-in-binary-search-tree/description/) - -```html - 1 - \ - 2 - / - 2 - -return [2]. -``` - -答案可能不止一个,也就是有多个值出现的次数一样多。 - -```java -private int curCnt = 1; -private int maxCnt = 1; -private TreeNode preNode = null; - -public int[] findMode(TreeNode root) { - List maxCntNums = new ArrayList<>(); - inOrder(root, maxCntNums); - int[] ret = new int[maxCntNums.size()]; - int idx = 0; - for (int num : maxCntNums) { - ret[idx++] = num; - } - return ret; -} - -private void inOrder(TreeNode node, List nums) { - if (node == null) return; - inOrder(node.left, nums); - if (preNode != null) { - if (preNode.val == node.val) curCnt++; - else curCnt = 1; - } - if (curCnt > maxCnt) { - maxCnt = curCnt; - nums.clear(); - nums.add(node.val); - } else if (curCnt == maxCnt) { - nums.add(node.val); - } - preNode = node; - inOrder(node.right, nums); -} -``` - -### Trie - -

- -Trie,又称前缀树或字典树,用于判断字符串是否存在或者是否具有某种字符串前缀。 - -**实现一个 Trie** - -[208. Implement Trie (Prefix Tree) (Medium)](https://leetcode.com/problems/implement-trie-prefix-tree/description/) - -```java -class Trie { - - private class Node { - Node[] childs = new Node[26]; - boolean isLeaf; - } - - private Node root = new Node(); - - public Trie() { - } - - public void insert(String word) { - insert(word, root); - } - - private void insert(String word, Node node) { - if (node == null) return; - if (word.length() == 0) { - node.isLeaf = true; - return; - } - int index = indexForChar(word.charAt(0)); - if (node.childs[index] == null) { - node.childs[index] = new Node(); - } - insert(word.substring(1), node.childs[index]); - } - - public boolean search(String word) { - return search(word, root); - } - - private boolean search(String word, Node node) { - if (node == null) return false; - if (word.length() == 0) return node.isLeaf; - int index = indexForChar(word.charAt(0)); - return search(word.substring(1), node.childs[index]); - } - - public boolean startsWith(String prefix) { - return startWith(prefix, root); - } - - private boolean startWith(String prefix, Node node) { - if (node == null) return false; - if (prefix.length() == 0) return true; - int index = indexForChar(prefix.charAt(0)); - return startWith(prefix.substring(1), node.childs[index]); - } - - private int indexForChar(char c) { - return c - 'a'; - } -} -``` - -**实现一个 Trie,用来求前缀和** - -[677. Map Sum Pairs (Medium)](https://leetcode.com/problems/map-sum-pairs/description/) - -```html -Input: insert("apple", 3), Output: Null -Input: sum("ap"), Output: 3 -Input: insert("app", 2), Output: Null -Input: sum("ap"), Output: 5 -``` - -```java -class MapSum { - - private class Node { - Node[] child = new Node[26]; - int value; - } - - private Node root = new Node(); - - public MapSum() { - - } - - public void insert(String key, int val) { - insert(key, root, val); - } - - private void insert(String key, Node node, int val) { - if (node == null) return; - if (key.length() == 0) { - node.value = val; - return; - } - int index = indexForChar(key.charAt(0)); - if (node.child[index] == null) { - node.child[index] = new Node(); - } - insert(key.substring(1), node.child[index], val); - } - - public int sum(String prefix) { - return sum(prefix, root); - } - - private int sum(String prefix, Node node) { - if (node == null) return 0; - if (prefix.length() != 0) { - int index = indexForChar(prefix.charAt(0)); - return sum(prefix.substring(1), node.child[index]); - } - int sum = node.value; - for (Node child : node.child) { - sum += sum(prefix, child); - } - return sum; - } - - private int indexForChar(char c) { - return c - 'a'; - } -} -``` - - -## 栈和队列 - -**用栈实现队列** - -[232. Implement Queue using Stacks (Easy)](https://leetcode.com/problems/implement-queue-using-stacks/description/) - -栈的顺序为后进先出,而队列的顺序为先进先出。使用两个栈实现队列,一个元素需要经过两个栈才能出队列,在经过第一个栈时元素顺序被反转,经过第二个栈时再次被反转,此时就是先进先出顺序。 - -```java -class MyQueue { - - private Stack in = new Stack<>(); - private Stack out = new Stack<>(); - - public void push(int x) { - in.push(x); - } - - public int pop() { - in2out(); - return out.pop(); - } - - public int peek() { - in2out(); - return out.peek(); - } - - private void in2out() { - if (out.isEmpty()) { - while (!in.isEmpty()) { - out.push(in.pop()); - } - } - } - - public boolean empty() { - return in.isEmpty() && out.isEmpty(); - } -} -``` - -**用队列实现栈** - -[225. Implement Stack using Queues (Easy)](https://leetcode.com/problems/implement-stack-using-queues/description/) - -在将一个元素 x 插入队列时,为了维护原来的后进先出顺序,需要让 x 插入队列首部。而队列的默认插入顺序是队列尾部,因此在将 x 插入队列尾部之后,需要让除了 x 之外的所有元素出队列,再入队列。 - -```java -class MyStack { - - private Queue queue; - - public MyStack() { - queue = new LinkedList<>(); - } - - public void push(int x) { - queue.add(x); - int cnt = queue.size(); - while (cnt-- > 1) { - queue.add(queue.poll()); - } - } - - public int pop() { - return queue.remove(); - } - - public int top() { - return queue.peek(); - } - - public boolean empty() { - return queue.isEmpty(); - } -} -``` - -**最小值栈** - -[155. Min Stack (Easy)](https://leetcode.com/problems/min-stack/description/) - -```java -class MinStack { - - private Stack dataStack; - private Stack minStack; - private int min; - - public MinStack() { - dataStack = new Stack<>(); - minStack = new Stack<>(); - min = Integer.MAX_VALUE; - } - - public void push(int x) { - dataStack.add(x); - min = Math.min(min, x); - minStack.add(min); - } - - public void pop() { - dataStack.pop(); - minStack.pop(); - min = minStack.isEmpty() ? Integer.MAX_VALUE : minStack.peek(); - } - - public int top() { - return dataStack.peek(); - } - - public int getMin() { - return minStack.peek(); - } -} -``` - -对于实现最小值队列问题,可以先将队列使用栈来实现,然后就将问题转换为最小值栈,这个问题出现在 编程之美:3.7。 - -**用栈实现括号匹配** - -[20. Valid Parentheses (Easy)](https://leetcode.com/problems/valid-parentheses/description/) - -```html -"()[]{}" - -Output : true -``` - -```java -public boolean isValid(String s) { - Stack stack = new Stack<>(); - for (char c : s.toCharArray()) { - if (c == '(' || c == '{' || c == '[') { - stack.push(c); - } else { - if (stack.isEmpty()) { - return false; - } - char cStack = stack.pop(); - boolean b1 = c == ')' && cStack != '('; - boolean b2 = c == ']' && cStack != '['; - boolean b3 = c == '}' && cStack != '{'; - if (b1 || b2 || b3) { - return false; - } - } - } - return stack.isEmpty(); -} -``` - -**数组中元素与下一个比它大的元素之间的距离** - -[739. Daily Temperatures (Medium)](https://leetcode.com/problems/daily-temperatures/description/) - -```html -Input: [73, 74, 75, 71, 69, 72, 76, 73] -Output: [1, 1, 4, 2, 1, 1, 0, 0] -``` - -在遍历数组时用栈把数组中的数存起来,如果当前遍历的数比栈顶元素来的大,说明栈顶元素的下一个比它大的数就是当前元素。 - -```java -public int[] dailyTemperatures(int[] temperatures) { - int n = temperatures.length; - int[] dist = new int[n]; - Stack indexs = new Stack<>(); - for (int curIndex = 0; curIndex < n; curIndex++) { - while (!indexs.isEmpty() && temperatures[curIndex] > temperatures[indexs.peek()]) { - int preIndex = indexs.pop(); - dist[preIndex] = curIndex - preIndex; - } - indexs.add(curIndex); - } - return dist; -} -``` - -**循环数组中比当前元素大的下一个元素** - -[503. Next Greater Element II (Medium)](https://leetcode.com/problems/next-greater-element-ii/description/) - -```text -Input: [1,2,1] -Output: [2,-1,2] -Explanation: The first 1's next greater number is 2; -The number 2 can't find next greater number; -The second 1's next greater number needs to search circularly, which is also 2. -``` - -与 739. Daily Temperatures (Medium) 不同的是,数组是循环数组,并且最后要求的不是距离而是下一个元素。 - -```java -public int[] nextGreaterElements(int[] nums) { - int n = nums.length; - int[] next = new int[n]; - Arrays.fill(next, -1); - Stack pre = new Stack<>(); - for (int i = 0; i < n * 2; i++) { - int num = nums[i % n]; - while (!pre.isEmpty() && nums[pre.peek()] < num) { - next[pre.pop()] = num; - } - if (i < n){ - pre.push(i); - } - } - return next; -} -``` - -## 哈希表 - -哈希表使用 O(N) 空间复杂度存储数据,并且以 O(1) 时间复杂度求解问题。 - -- Java 中的 **HashSet** 用于存储一个集合,可以查找元素是否在集合中。如果元素有穷,并且范围不大,那么可以用一个布尔数组来存储一个元素是否存在。例如对于只有小写字符的元素,就可以用一个长度为 26 的布尔数组来存储一个字符集合,使得空间复杂度降低为 O(1)。 - -- Java 中的 **HashMap** 主要用于映射关系,从而把两个元素联系起来。HashMap 也可以用来对元素进行计数统计,此时键为元素,值为计数。和 HashSet 类似,如果元素有穷并且范围不大,可以用整型数组来进行统计。在对一个内容进行压缩或者其它转换时,利用 HashMap 可以把原始内容和转换后的内容联系起来。例如在一个简化 url 的系统中 [Leetcdoe : 535. Encode and Decode TinyURL (Medium)](https://leetcode.com/problems/encode-and-decode-tinyurl/description/),利用 HashMap 就可以存储精简后的 url 到原始 url 的映射,使得不仅可以显示简化的 url,也可以根据简化的 url 得到原始 url 从而定位到正确的资源。 - - -**数组中两个数的和为给定值** - -[1. Two Sum (Easy)](https://leetcode.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 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; -} -``` - -**判断数组是否含有重复元素** - -[217. Contains Duplicate (Easy)](https://leetcode.com/problems/contains-duplicate/description/) - -```java -public boolean containsDuplicate(int[] nums) { - Set set = new HashSet<>(); - for (int num : nums) { - set.add(num); - } - return set.size() < nums.length; -} -``` - -**最长和谐序列** - -[594. Longest Harmonious Subsequence (Easy)](https://leetcode.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 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; -} -``` - -**最长连续序列** - -[128. Longest Consecutive Sequence (Hard)](https://leetcode.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 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 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 countForNum) { - int max = 0; - for (int num : countForNum.keySet()) { - max = Math.max(max, countForNum.get(num)); - } - return max; -} -``` - -## 字符串 - -**字符串循环移位包含** - -[编程之美 3.1](#) - -```html -s1 = AABCD, s2 = CDAA -Return : true -``` - -给定两个字符串 s1 和 s2,要求判定 s2 是否能够被 s1 做循环移位得到的字符串包含。 - -s1 进行循环移位的结果是 s1s1 的子字符串,因此只要判断 s2 是否是 s1s1 的子字符串即可。 - -**字符串循环移位** - -[编程之美 2.17](#) - -```html -s = "abcd123" k = 3 -Return "123abcd" -``` - -将字符串向右循环移动 k 位。 - -将 abcd123 中的 abcd 和 123 单独翻转,得到 dcba321,然后对整个字符串进行翻转,得到 123abcd。 - -**字符串中单词的翻转** - -[程序员代码面试指南](#) - -```html -s = "I am a student" -Return "student a am I" -``` - -将每个单词翻转,然后将整个字符串翻转。 - -**两个字符串包含的字符是否完全相同** - -[242. Valid Anagram (Easy)](https://leetcode.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; -} -``` - -**计算一组字符集合可以组成的回文字符串的最大长度** - -[409. Longest Palindrome (Easy)](https://leetcode.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; -} -``` - -**字符串同构** - -[205. Isomorphic Strings (Easy)](https://leetcode.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; -} -``` - -**回文子字符串个数** - -[647. Palindromic Substrings (Medium)](https://leetcode.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++; - } -} -``` - -**判断一个整数是否是回文数** - -[9. Palindrome Number (Easy)](https://leetcode.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; -} -``` - -**统计二进制字符串中连续 1 和连续 0 数量相同的子字符串个数** - -[696. Count Binary Substrings (Easy)](https://leetcode.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; -} -``` - -## 数组与矩阵 - -**把数组中的 0 移到末尾** - -[283. Move Zeroes (Easy)](https://leetcode.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; - } -} -``` - -**改变矩阵维度** - -[566. Reshape the Matrix (Easy)](https://leetcode.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; -} -``` - -**找出数组中最长的连续 1** - -[485. Max Consecutive Ones (Easy)](https://leetcode.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; -} -``` - -**有序矩阵查找** - -[240. Search a 2D Matrix II (Medium)](https://leetcode.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; -} -``` - -**有序矩阵的 Kth Element** - -[378. Kth Smallest Element in a Sorted Matrix ((Medium))](https://leetcode.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.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 pq = new PriorityQueue(); - 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 { - 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; - } -} -``` - -**一个数组元素在 [1, n] 之间,其中一个数被替换为另一个数,找出重复的数和丢失的数** - -[645. Set Mismatch (Easy)](https://leetcode.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; -} -``` - -类似题目: - -- [448. Find All Numbers Disappeared in an Array (Easy)](https://leetcode.com/problems/find-all-numbers-disappeared-in-an-array/description/),寻找所有丢失的元素 -- [442. Find All Duplicates in an Array (Medium)](https://leetcode.com/problems/find-all-duplicates-in-an-array/description/),寻找所有重复的元素。 - -**找出数组中重复的数,数组值在 [1, n] 之间** - -[287. Find the Duplicate Number (Medium)](https://leetcode.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; -} -``` - -**数组相邻差值的个数** - -[667. Beautiful Arrangement II (Medium)](https://leetcode.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; -} -``` - -**数组的度** - -[697. Degree of an Array (Easy)](https://leetcode.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 numsCnt = new HashMap<>(); - Map numsLastIndex = new HashMap<>(); - Map 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; -} -``` - -**对角元素相等的矩阵** - -[766. Toeplitz Matrix (Easy)](https://leetcode.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); -} -``` - -**嵌套数组** - -[565. Array Nesting (Medium)](https://leetcode.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; -} -``` - -**分隔数组** - -[769. Max Chunks To Make Sorted (Medium)](https://leetcode.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; -} -``` - -## 图 - -### 二分图 - -如果可以用两种颜色对图中的节点进行着色,并且保证相邻的节点颜色不同,那么这个图就是二分图。 - -**判断是否为二分图** - -[785. Is Graph Bipartite? (Medium)](https://leetcode.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; -} -``` - -### 拓扑排序 - -常用于在具有先序关系的任务规划中。 - -**课程安排的合法性** - -[207. Course Schedule (Medium)](https://leetcode.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[] 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[] 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; -} -``` - -**课程安排的顺序** - -[210. Course Schedule II (Medium)](https://leetcode.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[] 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 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[] graphic, - int curNode, Stack 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; -} -``` - -### 并查集 - -并查集可以动态地连通两个点,并且可以非常快速地判断两个点是否连通。 - -**冗余连接** - -[684. Redundant Connection (Medium)](https://leetcode.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); - } -} -``` - -## 位运算 - -**1. 基本原理** - -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 的特点,可以将三个数中重复的两个数去除,只留下另一个数。 -- 利用 x & 0s = 0 和 x & 1s = x 的特点,可以实现掩码操作。一个数 num 与 mask:00111100 进行位与操作,只保留 num 中与 mask 的 1 部分相对应的位。 -- 利用 x | 0s = x 和 x | 1s = 1s 的特点,可以实现设值操作。一个数 num 与 mask:00111100 进行位或操作,将 num 中与 mask 的 1 部分相对应的位都设置为 1。 - -位与运算技巧: - -- n&(n-1) 去除 n 的位级表示中最低的那一位。例如对于二进制表示 10110 **100** ,减去 1 得到 10110**011**,这两个数相与得到 10110**000**。 -- n&(-n) 得到 n 的位级表示中最低的那一位。-n 得到 n 的反码加 1,对于二进制表示 10110 **100** ,-n 得到 01001**100**,相与得到 00000**100**。 -- n-n&(\~n+1) 去除 n 的位级表示中最高的那一位。 - -移位运算: - -- \>\> n 为算术右移,相当于除以 2n; -- \>\>\> n 为无符号右移,左边会补上 0。 -- << n 为算术左移,相当于乘以 2n。 - -**2. 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 即可,例如将 1<<(4+1)-1 = 00010000-1 = 00001111。 - -要得到 1 到 i 位为 0 的 mask,只需将 1 到 i 位为 1 的 mask 取反,即 \~(1<<(i+1)-1)。 - -**3. Java 中的位操作** - -```html -static int Integer.bitCount(); // 统计 1 的数量 -static int Integer.highestOneBit(); // 获得最高位 -static String toBinaryString(int i); // 转换为二进制表示的字符串 -``` - -**统计两个数的二进制表示有多少位不同** - -[461. Hamming Distance (Easy)](https://leetcode.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); -} -``` - -**数组中唯一一个不重复的元素** - -[136. Single Number (Easy)](https://leetcode.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; -} -``` - -**找出数组中缺失的那个数** - -[268. Missing Number (Easy)](https://leetcode.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; -} -``` - -**数组中不重复的两个元素** - -[260. Single Number III (Medium)](https://leetcode.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; -} -``` - -**翻转一个数的比特位** - -[190. Reverse Bits (Easy)](https://leetcode.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 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; -} -``` - -**不用额外变量交换两个整数** - -[程序员代码面试指南 :P317](#) - -```java -a = a ^ b; -b = a ^ b; -a = a ^ b; -``` - -**判断一个数是不是 2 的 n 次方** - -[231. Power of Two (Easy)](https://leetcode.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; -} -``` - -**判断一个数是不是 4 的 n 次方** - -[342. Power of Four (Easy)](https://leetcode.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*"); -} -``` - -**判断一个数的位级表示是否不会出现连续的 0 和 1** - -[693. Binary Number with Alternating Bits (Easy)](https://leetcode.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; -} -``` - -**求一个数的补码** - -[476. Number Complement (Easy)](https://leetcode.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); -} -``` - -**实现整数的加法** - -[371. Sum of Two Integers (Easy)](https://leetcode.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); -} -``` - -**字符串数组最大乘积** - -[318. Maximum Product of Word Lengths (Medium)](https://leetcode.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; -} -``` - -**统计从 0 \~ n 每个数的二进制表示中 1 的个数** - -[338. Counting Bits (Medium)](https://leetcode.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; -} -``` - -# 参考资料 - -- [Leetcode](https://leetcode.com/problemset/algorithms/?status=Todo) -- Weiss M A, 冯舜玺. 数据结构与算法分析——C 语言描述[J]. 2004. -- Sedgewick R. Algorithms[M]. Pearson Education India, 1988. -- 何海涛, 软件工程师. 剑指 Offer: 名企面试官精讲典型编程题[M]. 电子工业出版社, 2014. -- 《编程之美》小组. 编程之美[M]. 电子工业出版社, 2008. -- 左程云. 程序员代码面试指南[M]. 电子工业出版社, 2015. From 61de2ea5207ef6a81aa56e9e3e6cc61dee238dc8 Mon Sep 17 00:00:00 2001 From: CyC2018 <1029579233@qq.com> Date: Fri, 8 Mar 2019 20:37:35 +0800 Subject: [PATCH 03/56] Update README.md --- docs/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/README.md b/docs/README.md index 435ac1e4..6a1a4552 100644 --- a/docs/README.md +++ b/docs/README.md @@ -3,7 +3,7 @@ ## ✏️ 算法 - [剑指 Offer 题解](notes/剑指%20offer%20题解.md)
-- [Leetcode 题解](notes/Leetcode%20题解)
+- [Leetcode 题解](notes/Leetcode%20题解%20-%20目录.md)
- [算法](notes/算法.md)
- [点击订阅面试进阶指南](https://xiaozhuanlan.com/CyC2018) From 100bf5c905282831e3236d541fa5d6a104cd1f99 Mon Sep 17 00:00:00 2001 From: CyC2018 <1029579233@qq.com> Date: Fri, 8 Mar 2019 20:41:28 +0800 Subject: [PATCH 04/56] auto commit --- docs/notes/Docker.md | 3 +- docs/notes/Git.md | 3 +- docs/notes/HTTP.md | 3 +- docs/notes/Java IO.md | 3 +- docs/notes/Java 基础.md | 5 +-- docs/notes/Java 容器.md | 3 +- docs/notes/Java 并发.md | 3 +- docs/notes/Java 虚拟机.md | 3 +- docs/notes/Leetcode 题解 - 二分查找.md | 30 +++++++------- docs/notes/Leetcode 题解 - 位运算.md | 3 +- docs/notes/Leetcode 题解 - 分治.md | 7 ++-- docs/notes/Leetcode 题解 - 动态规划.md | 3 +- docs/notes/Leetcode 题解 - 双指针.md | 3 +- docs/notes/Leetcode 题解 - 哈希表.md | 19 +++++---- docs/notes/Leetcode 题解 - 图.md | 3 +- docs/notes/Leetcode 题解 - 字符串.md | 3 +- docs/notes/Leetcode 题解 - 排序.md | 3 +- docs/notes/Leetcode 题解 - 搜索.md | 3 +- docs/notes/Leetcode 题解 - 数学.md | 3 +- docs/notes/Leetcode 题解 - 数组与矩阵.md | 51 ++++++++++++------------ docs/notes/Leetcode 题解 - 栈和队列.md | 3 +- docs/notes/Leetcode 题解 - 树.md | 3 +- docs/notes/Leetcode 题解 - 目录.md | 3 +- docs/notes/Leetcode 题解 - 贪心思想.md | 3 +- docs/notes/Leetcode 题解 - 链表.md | 3 +- docs/notes/Leetcode-Database 题解.md | 3 +- docs/notes/Linux.md | 3 +- docs/notes/MySQL.md | 3 +- docs/notes/Redis.md | 3 +- docs/notes/SQL.md | 3 +- docs/notes/Socket.md | 3 +- docs/notes/代码可读性.md | 3 +- docs/notes/代码风格规范.md | 3 +- docs/notes/分布式.md | 3 +- docs/notes/剑指 offer 题解.md | 3 +- docs/notes/攻击技术.md | 3 +- docs/notes/数据库系统原理.md | 3 +- docs/notes/构建工具.md | 3 +- docs/notes/正则表达式.md | 3 +- docs/notes/消息队列.md | 3 +- docs/notes/算法.md | 3 +- docs/notes/系统设计基础.md | 3 +- docs/notes/缓存.md | 3 +- docs/notes/计算机操作系统.md | 3 +- docs/notes/计算机网络.md | 3 +- docs/notes/设计模式.md | 3 +- docs/notes/集群.md | 3 +- docs/notes/面向对象思想.md | 3 +- 48 files changed, 98 insertions(+), 143 deletions(-) diff --git a/docs/notes/Docker.md b/docs/notes/Docker.md index acbdd6f5..43c02cbb 100644 --- a/docs/notes/Docker.md +++ b/docs/notes/Docker.md @@ -1,5 +1,4 @@ -* [点击阅读面试进阶指南 ](https://github.com/CyC2018/Backend-Interview-Guide) - +* [点击阅读面试进阶指南 ](https://github.com/CyC2018/Backend-Interview-Guide) * [一、解决的问题](#一解决的问题) * [二、与虚拟机的比较](#二与虚拟机的比较) * [三、优势](#三优势) diff --git a/docs/notes/Git.md b/docs/notes/Git.md index 6cd8a829..de3dfb67 100644 --- a/docs/notes/Git.md +++ b/docs/notes/Git.md @@ -1,5 +1,4 @@ -* [点击阅读面试进阶指南 ](https://github.com/CyC2018/Backend-Interview-Guide) - +* [点击阅读面试进阶指南 ](https://github.com/CyC2018/Backend-Interview-Guide) * [集中式与分布式](#集中式与分布式) * [中心服务器](#中心服务器) * [工作流](#工作流) diff --git a/docs/notes/HTTP.md b/docs/notes/HTTP.md index 92f1846b..40906c69 100644 --- a/docs/notes/HTTP.md +++ b/docs/notes/HTTP.md @@ -1,5 +1,4 @@ -* [点击阅读面试进阶指南 ](https://github.com/CyC2018/Backend-Interview-Guide) - +* [点击阅读面试进阶指南 ](https://github.com/CyC2018/Backend-Interview-Guide) * [一 、基础概念](#一-基础概念) * [URI](#uri) * [请求和响应报文](#请求和响应报文) diff --git a/docs/notes/Java IO.md b/docs/notes/Java IO.md index 1c634d1d..4686d39a 100644 --- a/docs/notes/Java IO.md +++ b/docs/notes/Java IO.md @@ -1,5 +1,4 @@ -* [点击阅读面试进阶指南 ](https://github.com/CyC2018/Backend-Interview-Guide) - +* [点击阅读面试进阶指南 ](https://github.com/CyC2018/Backend-Interview-Guide) * [一、概览](#一概览) * [二、磁盘操作](#二磁盘操作) * [三、字节操作](#三字节操作) diff --git a/docs/notes/Java 基础.md b/docs/notes/Java 基础.md index 06f40fc4..aef744f2 100644 --- a/docs/notes/Java 基础.md +++ b/docs/notes/Java 基础.md @@ -1,5 +1,4 @@ -* [点击阅读面试进阶指南 ](https://github.com/CyC2018/Backend-Interview-Guide) - +* [点击阅读面试进阶指南 ](https://github.com/CyC2018/Backend-Interview-Guide) * [一、数据类型](#一数据类型) * [基本类型](#基本类型) * [包装类型](#包装类型) @@ -54,7 +53,7 @@ - double/64 - boolean/\~ -boolean 只有两个值:true、false,可以使用 1 bit 来存储,但是具体大小没有明确规定。JVM 会在编译时期将 boolean 类型的数据转换为 int,使用 1 来表示 true,0 表示 false。JVM 并不支持 boolean 数组,而是使用 byte 数组来表示 int 数组来表示。 +boolean 只有两个值:true、false,可以使用 1 bit 来存储,但是具体大小没有明确规定。JVM 会在编译时期将 boolean 类型的数据转换为 int,使用 1 来表示 true,0 表示 false。JVM 并不直接支持 boolean 数组,而是使用 byte 数组来表示 int 数组来表示。 - [Primitive Data Types](https://docs.oracle.com/javase/tutorial/java/nutsandbolts/datatypes.html) - [The Java® Virtual Machine Specification](https://docs.oracle.com/javase/specs/jvms/se8/jvms8.pdf) diff --git a/docs/notes/Java 容器.md b/docs/notes/Java 容器.md index c62bb02b..ff91f9cd 100644 --- a/docs/notes/Java 容器.md +++ b/docs/notes/Java 容器.md @@ -1,5 +1,4 @@ -* [点击阅读面试进阶指南 ](https://github.com/CyC2018/Backend-Interview-Guide) - +* [点击阅读面试进阶指南 ](https://github.com/CyC2018/Backend-Interview-Guide) * [一、概览](#一概览) * [Collection](#collection) * [Map](#map) diff --git a/docs/notes/Java 并发.md b/docs/notes/Java 并发.md index 394d6189..c1edbb2f 100644 --- a/docs/notes/Java 并发.md +++ b/docs/notes/Java 并发.md @@ -1,5 +1,4 @@ -* [点击阅读面试进阶指南 ](https://github.com/CyC2018/Backend-Interview-Guide) - +* [点击阅读面试进阶指南 ](https://github.com/CyC2018/Backend-Interview-Guide) * [一、线程状态转换](#一线程状态转换) * [新建(New)](#新建new) * [可运行(Runnable)](#可运行runnable) diff --git a/docs/notes/Java 虚拟机.md b/docs/notes/Java 虚拟机.md index b2a5fe69..698e6dd6 100644 --- a/docs/notes/Java 虚拟机.md +++ b/docs/notes/Java 虚拟机.md @@ -1,5 +1,4 @@ -* [点击阅读面试进阶指南 ](https://github.com/CyC2018/Backend-Interview-Guide) - +* [点击阅读面试进阶指南 ](https://github.com/CyC2018/Backend-Interview-Guide) * [一、运行时数据区域](#一运行时数据区域) * [程序计数器](#程序计数器) * [Java 虚拟机栈](#java-虚拟机栈) diff --git a/docs/notes/Leetcode 题解 - 二分查找.md b/docs/notes/Leetcode 题解 - 二分查找.md index 98f60142..ea8e616a 100644 --- a/docs/notes/Leetcode 题解 - 二分查找.md +++ b/docs/notes/Leetcode 题解 - 二分查找.md @@ -1,17 +1,17 @@ -* [点击阅读面试进阶指南 ](https://github.com/CyC2018/Backend-Interview-Guide) - +* [点击阅读面试进阶指南 ](https://github.com/CyC2018/Backend-Interview-Guide) * [原理](#原理) * [1. 正常实现](#1-正常实现) * [2. 时间复杂度](#2-时间复杂度) * [3. m 计算](#3-m-计算) * [4. 返回值](#4-返回值) * [5. 变种](#5-变种) -* [求开方](#求开方) -* [大于给定元素的最小元素](#大于给定元素的最小元素) -* [有序数组的 Single Element](#有序数组的-single-element) -* [第一个错误的版本](#第一个错误的版本) -* [旋转数组的最小数字](#旋转数组的最小数字) -* [查找区间](#查找区间) +* [例题](#例题) + * [1. 求开方](#1-求开方) + * [2. 大于给定元素的最小元素](#2-大于给定元素的最小元素) + * [3. 有序数组的 Single Element](#3-有序数组的-single-element) + * [4. 第一个错误的版本](#4-第一个错误的版本) + * [5. 旋转数组的最小数字](#5-旋转数组的最小数字) + * [6. 查找区间](#6-查找区间) @@ -97,7 +97,9 @@ l m h 当循环体退出时,不表示没有查找到 key,因此最后返回的结果不应该为 -1。为了验证有没有查找到,需要在调用端判断一下返回位置上的值和 key 是否相等。 -# 求开方 +# 例题 + +## 1. 求开方 [69. Sqrt(x) (Easy)](https://leetcode.com/problems/sqrtx/description/) @@ -135,7 +137,7 @@ public int mySqrt(int x) { } ``` -# 大于给定元素的最小元素 +## 2. 大于给定元素的最小元素 [744. Find Smallest Letter Greater Than Target (Easy)](https://leetcode.com/problems/find-smallest-letter-greater-than-target/description/) @@ -169,7 +171,7 @@ public char nextGreatestLetter(char[] letters, char target) { } ``` -# 有序数组的 Single Element +## 3. 有序数组的 Single Element [540. Single Element in a Sorted Array (Medium)](https://leetcode.com/problems/single-element-in-a-sorted-array/description/) @@ -204,7 +206,7 @@ public int singleNonDuplicate(int[] nums) { } ``` -# 第一个错误的版本 +## 4. 第一个错误的版本 [278. First Bad Version (Easy)](https://leetcode.com/problems/first-bad-version/description/) @@ -229,7 +231,7 @@ public int firstBadVersion(int n) { } ``` -# 旋转数组的最小数字 +## 5. 旋转数组的最小数字 [153. Find Minimum in Rotated Sorted Array (Medium)](https://leetcode.com/problems/find-minimum-in-rotated-sorted-array/description/) @@ -253,7 +255,7 @@ public int findMin(int[] nums) { } ``` -# 查找区间 +## 6. 查找区间 [34. Search for a Range (Medium)](https://leetcode.com/problems/search-for-a-range/description/) diff --git a/docs/notes/Leetcode 题解 - 位运算.md b/docs/notes/Leetcode 题解 - 位运算.md index 4c6e7c4d..2c446a1f 100644 --- a/docs/notes/Leetcode 题解 - 位运算.md +++ b/docs/notes/Leetcode 题解 - 位运算.md @@ -1,5 +1,4 @@ -* [点击阅读面试进阶指南 ](https://github.com/CyC2018/Backend-Interview-Guide) - +* [点击阅读面试进阶指南 ](https://github.com/CyC2018/Backend-Interview-Guide) * [原理](#原理) * [1. 基本原理](#1-基本原理) * [2. mask 计算](#2-mask-计算) diff --git a/docs/notes/Leetcode 题解 - 分治.md b/docs/notes/Leetcode 题解 - 分治.md index aaa09f60..331fed03 100644 --- a/docs/notes/Leetcode 题解 - 分治.md +++ b/docs/notes/Leetcode 题解 - 分治.md @@ -1,10 +1,9 @@ -* [点击阅读面试进阶指南 ](https://github.com/CyC2018/Backend-Interview-Guide) - -* [给表达式加括号](#给表达式加括号) +* [点击阅读面试进阶指南 ](https://github.com/CyC2018/Backend-Interview-Guide) +* [1. 给表达式加括号](#1-给表达式加括号) -# 给表达式加括号 +# 1. 给表达式加括号 [241. Different Ways to Add Parentheses (Medium)](https://leetcode.com/problems/different-ways-to-add-parentheses/description/) diff --git a/docs/notes/Leetcode 题解 - 动态规划.md b/docs/notes/Leetcode 题解 - 动态规划.md index 0a7a3cdb..6ef0d76a 100644 --- a/docs/notes/Leetcode 题解 - 动态规划.md +++ b/docs/notes/Leetcode 题解 - 动态规划.md @@ -1,5 +1,4 @@ -* [点击阅读面试进阶指南 ](https://github.com/CyC2018/Backend-Interview-Guide) - +* [点击阅读面试进阶指南 ](https://github.com/CyC2018/Backend-Interview-Guide) * [斐波那契数列](#斐波那契数列) * [爬楼梯](#爬楼梯) * [强盗抢劫](#强盗抢劫) diff --git a/docs/notes/Leetcode 题解 - 双指针.md b/docs/notes/Leetcode 题解 - 双指针.md index 343bc680..ead999f7 100644 --- a/docs/notes/Leetcode 题解 - 双指针.md +++ b/docs/notes/Leetcode 题解 - 双指针.md @@ -1,5 +1,4 @@ -* [点击阅读面试进阶指南 ](https://github.com/CyC2018/Backend-Interview-Guide) - +* [点击阅读面试进阶指南 ](https://github.com/CyC2018/Backend-Interview-Guide) * [有序数组的 Two Sum](#有序数组的-two-sum) * [两数平方和](#两数平方和) * [反转字符串中的元音字符](#反转字符串中的元音字符) diff --git a/docs/notes/Leetcode 题解 - 哈希表.md b/docs/notes/Leetcode 题解 - 哈希表.md index 63788a8f..95a1772e 100644 --- a/docs/notes/Leetcode 题解 - 哈希表.md +++ b/docs/notes/Leetcode 题解 - 哈希表.md @@ -1,9 +1,8 @@ -* [点击阅读面试进阶指南 ](https://github.com/CyC2018/Backend-Interview-Guide) - -* [数组中两个数的和为给定值](#数组中两个数的和为给定值) -* [判断数组是否含有重复元素](#判断数组是否含有重复元素) -* [最长和谐序列](#最长和谐序列) -* [最长连续序列](#最长连续序列) +* [点击阅读面试进阶指南 ](https://github.com/CyC2018/Backend-Interview-Guide) +* [1. 数组中两个数的和为给定值](#1-数组中两个数的和为给定值) +* [2. 判断数组是否含有重复元素](#2-判断数组是否含有重复元素) +* [3. 最长和谐序列](#3-最长和谐序列) +* [4. 最长连续序列](#4-最长连续序列) @@ -14,7 +13,7 @@ - Java 中的 **HashMap** 主要用于映射关系,从而把两个元素联系起来。HashMap 也可以用来对元素进行计数统计,此时键为元素,值为计数。和 HashSet 类似,如果元素有穷并且范围不大,可以用整型数组来进行统计。在对一个内容进行压缩或者其它转换时,利用 HashMap 可以把原始内容和转换后的内容联系起来。例如在一个简化 url 的系统中 [Leetcdoe : 535. Encode and Decode TinyURL (Medium)](https://leetcode.com/problems/encode-and-decode-tinyurl/description/),利用 HashMap 就可以存储精简后的 url 到原始 url 的映射,使得不仅可以显示简化的 url,也可以根据简化的 url 得到原始 url 从而定位到正确的资源。 -# 数组中两个数的和为给定值 +# 1. 数组中两个数的和为给定值 [1. Two Sum (Easy)](https://leetcode.com/problems/two-sum/description/) @@ -36,7 +35,7 @@ public int[] twoSum(int[] nums, int target) { } ``` -# 判断数组是否含有重复元素 +# 2. 判断数组是否含有重复元素 [217. Contains Duplicate (Easy)](https://leetcode.com/problems/contains-duplicate/description/) @@ -50,7 +49,7 @@ public boolean containsDuplicate(int[] nums) { } ``` -# 最长和谐序列 +# 3. 最长和谐序列 [594. Longest Harmonious Subsequence (Easy)](https://leetcode.com/problems/longest-harmonious-subsequence/description/) @@ -78,7 +77,7 @@ public int findLHS(int[] nums) { } ``` -# 最长连续序列 +# 4. 最长连续序列 [128. Longest Consecutive Sequence (Hard)](https://leetcode.com/problems/longest-consecutive-sequence/description/) diff --git a/docs/notes/Leetcode 题解 - 图.md b/docs/notes/Leetcode 题解 - 图.md index d275a757..5550ac85 100644 --- a/docs/notes/Leetcode 题解 - 图.md +++ b/docs/notes/Leetcode 题解 - 图.md @@ -1,5 +1,4 @@ -* [点击阅读面试进阶指南 ](https://github.com/CyC2018/Backend-Interview-Guide) - +* [点击阅读面试进阶指南 ](https://github.com/CyC2018/Backend-Interview-Guide) * [二分图](#二分图) * [判断是否为二分图](#判断是否为二分图) * [拓扑排序](#拓扑排序) diff --git a/docs/notes/Leetcode 题解 - 字符串.md b/docs/notes/Leetcode 题解 - 字符串.md index 9489116b..f9f28dfa 100644 --- a/docs/notes/Leetcode 题解 - 字符串.md +++ b/docs/notes/Leetcode 题解 - 字符串.md @@ -1,5 +1,4 @@ -* [点击阅读面试进阶指南 ](https://github.com/CyC2018/Backend-Interview-Guide) - +* [点击阅读面试进阶指南 ](https://github.com/CyC2018/Backend-Interview-Guide) * [字符串循环移位包含](#字符串循环移位包含) * [字符串循环移位](#字符串循环移位) * [字符串中单词的翻转](#字符串中单词的翻转) diff --git a/docs/notes/Leetcode 题解 - 排序.md b/docs/notes/Leetcode 题解 - 排序.md index b9ef2451..ece72b6d 100644 --- a/docs/notes/Leetcode 题解 - 排序.md +++ b/docs/notes/Leetcode 题解 - 排序.md @@ -1,5 +1,4 @@ -* [点击阅读面试进阶指南 ](https://github.com/CyC2018/Backend-Interview-Guide) - +* [点击阅读面试进阶指南 ](https://github.com/CyC2018/Backend-Interview-Guide) * [快速选择](#快速选择) * [堆排序](#堆排序) * [Kth Element](#kth-element) diff --git a/docs/notes/Leetcode 题解 - 搜索.md b/docs/notes/Leetcode 题解 - 搜索.md index b12e6e6a..d13a0277 100644 --- a/docs/notes/Leetcode 题解 - 搜索.md +++ b/docs/notes/Leetcode 题解 - 搜索.md @@ -1,5 +1,4 @@ -* [点击阅读面试进阶指南 ](https://github.com/CyC2018/Backend-Interview-Guide) - +* [点击阅读面试进阶指南 ](https://github.com/CyC2018/Backend-Interview-Guide) * [BFS](#bfs) * [计算在网格中从原点到特定点的最短路径长度](#计算在网格中从原点到特定点的最短路径长度) * [组成整数的最小平方数数量](#组成整数的最小平方数数量) diff --git a/docs/notes/Leetcode 题解 - 数学.md b/docs/notes/Leetcode 题解 - 数学.md index eadb76de..4806f11c 100644 --- a/docs/notes/Leetcode 题解 - 数学.md +++ b/docs/notes/Leetcode 题解 - 数学.md @@ -1,5 +1,4 @@ -* [点击阅读面试进阶指南 ](https://github.com/CyC2018/Backend-Interview-Guide) - +* [点击阅读面试进阶指南 ](https://github.com/CyC2018/Backend-Interview-Guide) * [素数分解](#素数分解) * [整除](#整除) * [最大公约数最小公倍数](#最大公约数最小公倍数) diff --git a/docs/notes/Leetcode 题解 - 数组与矩阵.md b/docs/notes/Leetcode 题解 - 数组与矩阵.md index cbaddae9..5bf46788 100644 --- a/docs/notes/Leetcode 题解 - 数组与矩阵.md +++ b/docs/notes/Leetcode 题解 - 数组与矩阵.md @@ -1,21 +1,20 @@ -* [点击阅读面试进阶指南 ](https://github.com/CyC2018/Backend-Interview-Guide) - -* [把数组中的 0 移到末尾](#把数组中的-0-移到末尾) -* [改变矩阵维度](#改变矩阵维度) -* [找出数组中最长的连续 1](#找出数组中最长的连续-1) -* [有序矩阵查找](#有序矩阵查找) -* [有序矩阵的 Kth Element](#有序矩阵的-kth-element) -* [一个数组元素在 [1, n] 之间,其中一个数被替换为另一个数,找出重复的数和丢失的数](#一个数组元素在-[1,-n]-之间,其中一个数被替换为另一个数,找出重复的数和丢失的数) -* [找出数组中重复的数,数组值在 [1, n] 之间](#找出数组中重复的数,数组值在-[1,-n]-之间) -* [数组相邻差值的个数](#数组相邻差值的个数) -* [数组的度](#数组的度) -* [对角元素相等的矩阵](#对角元素相等的矩阵) -* [嵌套数组](#嵌套数组) -* [分隔数组](#分隔数组) +* [点击阅读面试进阶指南 ](https://github.com/CyC2018/Backend-Interview-Guide) +* [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-分隔数组) -# 把数组中的 0 移到末尾 +# 1. 把数组中的 0 移到末尾 [283. Move Zeroes (Easy)](https://leetcode.com/problems/move-zeroes/description/) @@ -37,7 +36,7 @@ public void moveZeroes(int[] nums) { } ``` -# 改变矩阵维度 +# 2. 改变矩阵维度 [566. Reshape the Matrix (Easy)](https://leetcode.com/problems/reshape-the-matrix/description/) @@ -73,7 +72,7 @@ public int[][] matrixReshape(int[][] nums, int r, int c) { } ``` -# 找出数组中最长的连续 1 +# 3. 找出数组中最长的连续 1 [485. Max Consecutive Ones (Easy)](https://leetcode.com/problems/max-consecutive-ones/description/) @@ -88,7 +87,7 @@ public int findMaxConsecutiveOnes(int[] nums) { } ``` -# 有序矩阵查找 +# 4. 有序矩阵查找 [240. Search a 2D Matrix II (Medium)](https://leetcode.com/problems/search-a-2d-matrix-ii/description/) @@ -114,7 +113,7 @@ public boolean searchMatrix(int[][] matrix, int target) { } ``` -# 有序矩阵的 Kth Element +# 5. 有序矩阵的 Kth Element [378. Kth Smallest Element in a Sorted Matrix ((Medium))](https://leetcode.com/problems/kth-smallest-element-in-a-sorted-matrix/description/) @@ -180,7 +179,7 @@ class Tuple implements Comparable { } ``` -# 一个数组元素在 [1, n] 之间,其中一个数被替换为另一个数,找出重复的数和丢失的数 +# 6. 一个数组元素在 [1, n] 之间,其中一个数被替换为另一个数,找出重复的数和丢失的数 [645. Set Mismatch (Easy)](https://leetcode.com/problems/set-mismatch/description/) @@ -225,7 +224,7 @@ private void swap(int[] nums, int i, int j) { - [448. Find All Numbers Disappeared in an Array (Easy)](https://leetcode.com/problems/find-all-numbers-disappeared-in-an-array/description/),寻找所有丢失的元素 - [442. Find All Duplicates in an Array (Medium)](https://leetcode.com/problems/find-all-duplicates-in-an-array/description/),寻找所有重复的元素。 -# 找出数组中重复的数,数组值在 [1, n] 之间 +# 7. 找出数组中重复的数,数组值在 [1, n] 之间 [287. Find the Duplicate Number (Medium)](https://leetcode.com/problems/find-the-duplicate-number/description/) @@ -267,7 +266,7 @@ public int findDuplicate(int[] nums) { } ``` -# 数组相邻差值的个数 +# 8. 数组相邻差值的个数 [667. Beautiful Arrangement II (Medium)](https://leetcode.com/problems/beautiful-arrangement-ii/description/) @@ -295,7 +294,7 @@ public int[] constructArray(int n, int k) { } ``` -# 数组的度 +# 9. 数组的度 [697. Degree of an Array (Easy)](https://leetcode.com/problems/degree-of-an-array/description/) @@ -334,7 +333,7 @@ public int findShortestSubArray(int[] nums) { } ``` -# 对角元素相等的矩阵 +# 10. 对角元素相等的矩阵 [766. Toeplitz Matrix (Easy)](https://leetcode.com/problems/toeplitz-matrix/description/) @@ -372,7 +371,7 @@ private boolean check(int[][] matrix, int expectValue, int row, int col) { } ``` -# 嵌套数组 +# 11. 嵌套数组 [565. Array Nesting (Medium)](https://leetcode.com/problems/array-nesting/description/) @@ -406,7 +405,7 @@ public int arrayNesting(int[] nums) { } ``` -# 分隔数组 +# 12. 分隔数组 [769. Max Chunks To Make Sorted (Medium)](https://leetcode.com/problems/max-chunks-to-make-sorted/description/) diff --git a/docs/notes/Leetcode 题解 - 栈和队列.md b/docs/notes/Leetcode 题解 - 栈和队列.md index c6bf5964..13fdffb7 100644 --- a/docs/notes/Leetcode 题解 - 栈和队列.md +++ b/docs/notes/Leetcode 题解 - 栈和队列.md @@ -1,5 +1,4 @@ -* [点击阅读面试进阶指南 ](https://github.com/CyC2018/Backend-Interview-Guide) - +* [点击阅读面试进阶指南 ](https://github.com/CyC2018/Backend-Interview-Guide) * [用栈实现队列](#用栈实现队列) * [用队列实现栈](#用队列实现栈) * [最小值栈](#最小值栈) diff --git a/docs/notes/Leetcode 题解 - 树.md b/docs/notes/Leetcode 题解 - 树.md index 2ca20ee6..195a43f9 100644 --- a/docs/notes/Leetcode 题解 - 树.md +++ b/docs/notes/Leetcode 题解 - 树.md @@ -1,5 +1,4 @@ -* [点击阅读面试进阶指南 ](https://github.com/CyC2018/Backend-Interview-Guide) - +* [点击阅读面试进阶指南 ](https://github.com/CyC2018/Backend-Interview-Guide) * [递归](#递归) * [树的高度](#树的高度) * [平衡树](#平衡树) diff --git a/docs/notes/Leetcode 题解 - 目录.md b/docs/notes/Leetcode 题解 - 目录.md index ee3e594b..d9333f81 100644 --- a/docs/notes/Leetcode 题解 - 目录.md +++ b/docs/notes/Leetcode 题解 - 目录.md @@ -1,5 +1,4 @@ -* [点击阅读面试进阶指南 ](https://github.com/CyC2018/Backend-Interview-Guide) - +* [点击阅读面试进阶指南 ](https://github.com/CyC2018/Backend-Interview-Guide) * [算法思想](#算法思想) * [数据结构相关](#数据结构相关) * [参考资料](#参考资料) diff --git a/docs/notes/Leetcode 题解 - 贪心思想.md b/docs/notes/Leetcode 题解 - 贪心思想.md index e022b1df..8df05990 100644 --- a/docs/notes/Leetcode 题解 - 贪心思想.md +++ b/docs/notes/Leetcode 题解 - 贪心思想.md @@ -1,5 +1,4 @@ -* [点击阅读面试进阶指南 ](https://github.com/CyC2018/Backend-Interview-Guide) - +* [点击阅读面试进阶指南 ](https://github.com/CyC2018/Backend-Interview-Guide) * [分配饼干](#分配饼干) * [不重叠的区间个数](#不重叠的区间个数) * [投飞镖刺破气球](#投飞镖刺破气球) diff --git a/docs/notes/Leetcode 题解 - 链表.md b/docs/notes/Leetcode 题解 - 链表.md index f8e17044..f5c0bb7b 100644 --- a/docs/notes/Leetcode 题解 - 链表.md +++ b/docs/notes/Leetcode 题解 - 链表.md @@ -1,5 +1,4 @@ -* [点击阅读面试进阶指南 ](https://github.com/CyC2018/Backend-Interview-Guide) - +* [点击阅读面试进阶指南 ](https://github.com/CyC2018/Backend-Interview-Guide) * [找出两个链表的交点](#找出两个链表的交点) * [链表反转](#链表反转) * [归并两个有序的链表](#归并两个有序的链表) diff --git a/docs/notes/Leetcode-Database 题解.md b/docs/notes/Leetcode-Database 题解.md index 629aba39..41c0b572 100644 --- a/docs/notes/Leetcode-Database 题解.md +++ b/docs/notes/Leetcode-Database 题解.md @@ -1,5 +1,4 @@ -* [点击阅读面试进阶指南 ](https://github.com/CyC2018/Backend-Interview-Guide) - +* [点击阅读面试进阶指南 ](https://github.com/CyC2018/Backend-Interview-Guide) * [595. Big Countries](#595-big-countries) * [627. Swap Salary](#627-swap-salary) * [620. Not Boring Movies](#620-not-boring-movies) diff --git a/docs/notes/Linux.md b/docs/notes/Linux.md index 061bedbb..7340847b 100644 --- a/docs/notes/Linux.md +++ b/docs/notes/Linux.md @@ -1,5 +1,4 @@ -* [点击阅读面试进阶指南 ](https://github.com/CyC2018/Backend-Interview-Guide) - +* [点击阅读面试进阶指南 ](https://github.com/CyC2018/Backend-Interview-Guide) * [一、常用操作以及概念](#一常用操作以及概念) * [快捷键](#快捷键) * [求助](#求助) diff --git a/docs/notes/MySQL.md b/docs/notes/MySQL.md index 329e4ecb..6fb44a68 100644 --- a/docs/notes/MySQL.md +++ b/docs/notes/MySQL.md @@ -1,5 +1,4 @@ -* [点击阅读面试进阶指南 ](https://github.com/CyC2018/Backend-Interview-Guide) - +* [点击阅读面试进阶指南 ](https://github.com/CyC2018/Backend-Interview-Guide) * [一、索引](#一索引) * [B+ Tree 原理](#b-tree-原理) * [MySQL 索引](#mysql-索引) diff --git a/docs/notes/Redis.md b/docs/notes/Redis.md index 4291f6a8..13a03b05 100644 --- a/docs/notes/Redis.md +++ b/docs/notes/Redis.md @@ -1,5 +1,4 @@ -* [点击阅读面试进阶指南 ](https://github.com/CyC2018/Backend-Interview-Guide) - +* [点击阅读面试进阶指南 ](https://github.com/CyC2018/Backend-Interview-Guide) * [一、概述](#一概述) * [二、数据类型](#二数据类型) * [STRING](#string) diff --git a/docs/notes/SQL.md b/docs/notes/SQL.md index 2ac1b071..ddbdba3d 100644 --- a/docs/notes/SQL.md +++ b/docs/notes/SQL.md @@ -1,5 +1,4 @@ -* [点击阅读面试进阶指南 ](https://github.com/CyC2018/Backend-Interview-Guide) - +* [点击阅读面试进阶指南 ](https://github.com/CyC2018/Backend-Interview-Guide) * [一、基础](#一基础) * [二、创建表](#二创建表) * [三、修改表](#三修改表) diff --git a/docs/notes/Socket.md b/docs/notes/Socket.md index f30b5b1f..f148f81e 100644 --- a/docs/notes/Socket.md +++ b/docs/notes/Socket.md @@ -1,5 +1,4 @@ -* [点击阅读面试进阶指南 ](https://github.com/CyC2018/Backend-Interview-Guide) - +* [点击阅读面试进阶指南 ](https://github.com/CyC2018/Backend-Interview-Guide) * [一、I/O 模型](#一io-模型) * [阻塞式 I/O](#阻塞式-io) * [非阻塞式 I/O](#非阻塞式-io) diff --git a/docs/notes/代码可读性.md b/docs/notes/代码可读性.md index cae693f7..1a2e1f90 100644 --- a/docs/notes/代码可读性.md +++ b/docs/notes/代码可读性.md @@ -1,5 +1,4 @@ -* [点击阅读面试进阶指南 ](https://github.com/CyC2018/Backend-Interview-Guide) - +* [点击阅读面试进阶指南 ](https://github.com/CyC2018/Backend-Interview-Guide) * [一、可读性的重要性](#一可读性的重要性) * [二、用名字表达代码含义](#二用名字表达代码含义) * [三、名字不能带来歧义](#三名字不能带来歧义) diff --git a/docs/notes/代码风格规范.md b/docs/notes/代码风格规范.md index c94a413b..35db1821 100644 --- a/docs/notes/代码风格规范.md +++ b/docs/notes/代码风格规范.md @@ -1,5 +1,4 @@ -* [点击阅读面试进阶指南 ](https://github.com/CyC2018/Backend-Interview-Guide) - +* [点击阅读面试进阶指南 ](https://github.com/CyC2018/Backend-Interview-Guide) diff --git a/docs/notes/分布式.md b/docs/notes/分布式.md index c9673335..a1358401 100644 --- a/docs/notes/分布式.md +++ b/docs/notes/分布式.md @@ -1,5 +1,4 @@ -* [点击阅读面试进阶指南 ](https://github.com/CyC2018/Backend-Interview-Guide) - +* [点击阅读面试进阶指南 ](https://github.com/CyC2018/Backend-Interview-Guide) * [一、分布式锁](#一分布式锁) * [数据库的唯一索引](#数据库的唯一索引) * [Redis 的 SETNX 指令](#redis-的-setnx-指令) diff --git a/docs/notes/剑指 offer 题解.md b/docs/notes/剑指 offer 题解.md index 6e6f70cb..bc433f7e 100644 --- a/docs/notes/剑指 offer 题解.md +++ b/docs/notes/剑指 offer 题解.md @@ -1,5 +1,4 @@ -* [点击阅读面试进阶指南 ](https://github.com/CyC2018/Backend-Interview-Guide) - +* [点击阅读面试进阶指南 ](https://github.com/CyC2018/Backend-Interview-Guide) * [3. 数组中重复的数字](#3-数组中重复的数字) * [4. 二维数组中的查找](#4-二维数组中的查找) * [5. 替换空格](#5-替换空格) diff --git a/docs/notes/攻击技术.md b/docs/notes/攻击技术.md index b698e496..bef3916a 100644 --- a/docs/notes/攻击技术.md +++ b/docs/notes/攻击技术.md @@ -1,5 +1,4 @@ -* [点击阅读面试进阶指南 ](https://github.com/CyC2018/Backend-Interview-Guide) - +* [点击阅读面试进阶指南 ](https://github.com/CyC2018/Backend-Interview-Guide) * [一、跨站脚本攻击](#一跨站脚本攻击) * [二、跨站请求伪造](#二跨站请求伪造) * [三、SQL 注入攻击](#三sql-注入攻击) diff --git a/docs/notes/数据库系统原理.md b/docs/notes/数据库系统原理.md index 34a2a3e3..7fe58e24 100644 --- a/docs/notes/数据库系统原理.md +++ b/docs/notes/数据库系统原理.md @@ -1,5 +1,4 @@ -* [点击阅读面试进阶指南 ](https://github.com/CyC2018/Backend-Interview-Guide) - +* [点击阅读面试进阶指南 ](https://github.com/CyC2018/Backend-Interview-Guide) * [一、事务](#一事务) * [概念](#概念) * [ACID](#acid) diff --git a/docs/notes/构建工具.md b/docs/notes/构建工具.md index fcdd6b44..dfb63cbf 100644 --- a/docs/notes/构建工具.md +++ b/docs/notes/构建工具.md @@ -1,5 +1,4 @@ -* [点击阅读面试进阶指南 ](https://github.com/CyC2018/Backend-Interview-Guide) - +* [点击阅读面试进阶指南 ](https://github.com/CyC2018/Backend-Interview-Guide) * [一、构建工具的作用](#一构建工具的作用) * [二、Java 主流构建工具](#二java-主流构建工具) * [三、Maven](#三maven) diff --git a/docs/notes/正则表达式.md b/docs/notes/正则表达式.md index 2b6505a4..22bdec70 100644 --- a/docs/notes/正则表达式.md +++ b/docs/notes/正则表达式.md @@ -1,5 +1,4 @@ -* [点击阅读面试进阶指南 ](https://github.com/CyC2018/Backend-Interview-Guide) - +* [点击阅读面试进阶指南 ](https://github.com/CyC2018/Backend-Interview-Guide) * [一、概述](#一概述) * [二、匹配单个字符](#二匹配单个字符) * [三、匹配一组字符](#三匹配一组字符) diff --git a/docs/notes/消息队列.md b/docs/notes/消息队列.md index 16ac80df..20e27f4c 100644 --- a/docs/notes/消息队列.md +++ b/docs/notes/消息队列.md @@ -1,5 +1,4 @@ -* [点击阅读面试进阶指南 ](https://github.com/CyC2018/Backend-Interview-Guide) - +* [点击阅读面试进阶指南 ](https://github.com/CyC2018/Backend-Interview-Guide) * [一、消息模型](#一消息模型) * [点对点](#点对点) * [发布/订阅](#发布订阅) diff --git a/docs/notes/算法.md b/docs/notes/算法.md index 3492026f..3c85688f 100644 --- a/docs/notes/算法.md +++ b/docs/notes/算法.md @@ -1,5 +1,4 @@ -* [点击阅读面试进阶指南 ](https://github.com/CyC2018/Backend-Interview-Guide) - +* [点击阅读面试进阶指南 ](https://github.com/CyC2018/Backend-Interview-Guide) * [一、前言](#一前言) * [二、算法分析](#二算法分析) * [数学模型](#数学模型) diff --git a/docs/notes/系统设计基础.md b/docs/notes/系统设计基础.md index 2b095426..43cc166c 100644 --- a/docs/notes/系统设计基础.md +++ b/docs/notes/系统设计基础.md @@ -1,5 +1,4 @@ -* [点击阅读面试进阶指南 ](https://github.com/CyC2018/Backend-Interview-Guide) - +* [点击阅读面试进阶指南 ](https://github.com/CyC2018/Backend-Interview-Guide) * [一、性能](#一性能) * [二、伸缩性](#二伸缩性) * [三、扩展性](#三扩展性) diff --git a/docs/notes/缓存.md b/docs/notes/缓存.md index 3e43a32b..428b2080 100644 --- a/docs/notes/缓存.md +++ b/docs/notes/缓存.md @@ -1,5 +1,4 @@ -* [点击阅读面试进阶指南 ](https://github.com/CyC2018/Backend-Interview-Guide) - +* [点击阅读面试进阶指南 ](https://github.com/CyC2018/Backend-Interview-Guide) * [一、缓存特征](#一缓存特征) * [二、LRU](#二lru) * [三、缓存位置](#三缓存位置) diff --git a/docs/notes/计算机操作系统.md b/docs/notes/计算机操作系统.md index 9520d1be..a0768936 100644 --- a/docs/notes/计算机操作系统.md +++ b/docs/notes/计算机操作系统.md @@ -1,5 +1,4 @@ -* [点击阅读面试进阶指南 ](https://github.com/CyC2018/Backend-Interview-Guide) - +* [点击阅读面试进阶指南 ](https://github.com/CyC2018/Backend-Interview-Guide) * [一、概述](#一概述) * [基本特征](#基本特征) * [基本功能](#基本功能) diff --git a/docs/notes/计算机网络.md b/docs/notes/计算机网络.md index 47908e26..a5b964cb 100644 --- a/docs/notes/计算机网络.md +++ b/docs/notes/计算机网络.md @@ -1,5 +1,4 @@ -* [点击阅读面试进阶指南 ](https://github.com/CyC2018/Backend-Interview-Guide) - +* [点击阅读面试进阶指南 ](https://github.com/CyC2018/Backend-Interview-Guide) * [一、概述](#一概述) * [网络的网络](#网络的网络) * [ISP](#isp) diff --git a/docs/notes/设计模式.md b/docs/notes/设计模式.md index 4a35404f..270e0331 100644 --- a/docs/notes/设计模式.md +++ b/docs/notes/设计模式.md @@ -1,5 +1,4 @@ -* [点击阅读面试进阶指南 ](https://github.com/CyC2018/Backend-Interview-Guide) - +* [点击阅读面试进阶指南 ](https://github.com/CyC2018/Backend-Interview-Guide) * [一、概述](#一概述) * [二、创建型](#二创建型) * [1. 单例(Singleton)](#1-单例singleton) diff --git a/docs/notes/集群.md b/docs/notes/集群.md index 1ab268db..8030c238 100644 --- a/docs/notes/集群.md +++ b/docs/notes/集群.md @@ -1,5 +1,4 @@ -* [点击阅读面试进阶指南 ](https://github.com/CyC2018/Backend-Interview-Guide) - +* [点击阅读面试进阶指南 ](https://github.com/CyC2018/Backend-Interview-Guide) * [一、负载均衡](#一负载均衡) * [负载均衡算法](#负载均衡算法) * [转发实现](#转发实现) diff --git a/docs/notes/面向对象思想.md b/docs/notes/面向对象思想.md index 75f7ca8f..7a2f769c 100644 --- a/docs/notes/面向对象思想.md +++ b/docs/notes/面向对象思想.md @@ -1,5 +1,4 @@ -* [点击阅读面试进阶指南 ](https://github.com/CyC2018/Backend-Interview-Guide) - +* [点击阅读面试进阶指南 ](https://github.com/CyC2018/Backend-Interview-Guide) * [一、三大特性](#一三大特性) * [封装](#封装) * [继承](#继承) From 815c1975d0ca8a4fdc7a5be1e9413e4c38333eec Mon Sep 17 00:00:00 2001 From: CyC2018 <1029579233@qq.com> Date: Fri, 8 Mar 2019 20:43:38 +0800 Subject: [PATCH 05/56] auto commit --- docs/notes/Docker.md | 2 +- docs/notes/Git.md | 2 +- docs/notes/HTTP.md | 2 +- docs/notes/Java IO.md | 2 +- docs/notes/Java 基础.md | 2 +- docs/notes/Java 容器.md | 2 +- docs/notes/Java 并发.md | 2 +- docs/notes/Java 虚拟机.md | 2 +- docs/notes/Leetcode 题解 - 二分查找.md | 2 +- docs/notes/Leetcode 题解 - 位运算.md | 2 +- docs/notes/Leetcode 题解 - 分治.md | 2 +- docs/notes/Leetcode 题解 - 动态规划.md | 2 +- docs/notes/Leetcode 题解 - 双指针.md | 2 +- docs/notes/Leetcode 题解 - 哈希表.md | 2 +- docs/notes/Leetcode 题解 - 图.md | 2 +- docs/notes/Leetcode 题解 - 字符串.md | 2 +- docs/notes/Leetcode 题解 - 排序.md | 2 +- docs/notes/Leetcode 题解 - 搜索.md | 2 +- docs/notes/Leetcode 题解 - 数学.md | 2 +- docs/notes/Leetcode 题解 - 数组与矩阵.md | 2 +- docs/notes/Leetcode 题解 - 栈和队列.md | 2 +- docs/notes/Leetcode 题解 - 树.md | 2 +- docs/notes/Leetcode 题解 - 目录.md | 2 +- docs/notes/Leetcode 题解 - 贪心思想.md | 2 +- docs/notes/Leetcode 题解 - 链表.md | 2 +- docs/notes/Leetcode-Database 题解.md | 2 +- docs/notes/Linux.md | 2 +- docs/notes/MySQL.md | 2 +- docs/notes/Redis.md | 2 +- docs/notes/SQL.md | 2 +- docs/notes/Socket.md | 2 +- docs/notes/代码可读性.md | 2 +- docs/notes/代码风格规范.md | 2 +- docs/notes/分布式.md | 2 +- docs/notes/剑指 offer 题解.md | 2 +- docs/notes/攻击技术.md | 2 +- docs/notes/数据库系统原理.md | 2 +- docs/notes/构建工具.md | 2 +- docs/notes/正则表达式.md | 2 +- docs/notes/消息队列.md | 2 +- docs/notes/算法.md | 2 +- docs/notes/系统设计基础.md | 2 +- docs/notes/缓存.md | 2 +- docs/notes/计算机操作系统.md | 2 +- docs/notes/计算机网络.md | 2 +- docs/notes/设计模式.md | 2 +- docs/notes/集群.md | 2 +- docs/notes/面向对象思想.md | 2 +- 48 files changed, 48 insertions(+), 48 deletions(-) diff --git a/docs/notes/Docker.md b/docs/notes/Docker.md index 43c02cbb..783acfb4 100644 --- a/docs/notes/Docker.md +++ b/docs/notes/Docker.md @@ -1,4 +1,4 @@ -* [点击阅读面试进阶指南 ](https://github.com/CyC2018/Backend-Interview-Guide) + * [一、解决的问题](#一解决的问题) * [二、与虚拟机的比较](#二与虚拟机的比较) * [三、优势](#三优势) diff --git a/docs/notes/Git.md b/docs/notes/Git.md index de3dfb67..a329bf49 100644 --- a/docs/notes/Git.md +++ b/docs/notes/Git.md @@ -1,4 +1,4 @@ -* [点击阅读面试进阶指南 ](https://github.com/CyC2018/Backend-Interview-Guide) + * [集中式与分布式](#集中式与分布式) * [中心服务器](#中心服务器) * [工作流](#工作流) diff --git a/docs/notes/HTTP.md b/docs/notes/HTTP.md index 40906c69..1b1c6800 100644 --- a/docs/notes/HTTP.md +++ b/docs/notes/HTTP.md @@ -1,4 +1,4 @@ -* [点击阅读面试进阶指南 ](https://github.com/CyC2018/Backend-Interview-Guide) + * [一 、基础概念](#一-基础概念) * [URI](#uri) * [请求和响应报文](#请求和响应报文) diff --git a/docs/notes/Java IO.md b/docs/notes/Java IO.md index 4686d39a..55915c74 100644 --- a/docs/notes/Java IO.md +++ b/docs/notes/Java IO.md @@ -1,4 +1,4 @@ -* [点击阅读面试进阶指南 ](https://github.com/CyC2018/Backend-Interview-Guide) + * [一、概览](#一概览) * [二、磁盘操作](#二磁盘操作) * [三、字节操作](#三字节操作) diff --git a/docs/notes/Java 基础.md b/docs/notes/Java 基础.md index aef744f2..fcadcfb2 100644 --- a/docs/notes/Java 基础.md +++ b/docs/notes/Java 基础.md @@ -1,4 +1,4 @@ -* [点击阅读面试进阶指南 ](https://github.com/CyC2018/Backend-Interview-Guide) + * [一、数据类型](#一数据类型) * [基本类型](#基本类型) * [包装类型](#包装类型) diff --git a/docs/notes/Java 容器.md b/docs/notes/Java 容器.md index ff91f9cd..761220b3 100644 --- a/docs/notes/Java 容器.md +++ b/docs/notes/Java 容器.md @@ -1,4 +1,4 @@ -* [点击阅读面试进阶指南 ](https://github.com/CyC2018/Backend-Interview-Guide) + * [一、概览](#一概览) * [Collection](#collection) * [Map](#map) diff --git a/docs/notes/Java 并发.md b/docs/notes/Java 并发.md index c1edbb2f..1bc84a00 100644 --- a/docs/notes/Java 并发.md +++ b/docs/notes/Java 并发.md @@ -1,4 +1,4 @@ -* [点击阅读面试进阶指南 ](https://github.com/CyC2018/Backend-Interview-Guide) + * [一、线程状态转换](#一线程状态转换) * [新建(New)](#新建new) * [可运行(Runnable)](#可运行runnable) diff --git a/docs/notes/Java 虚拟机.md b/docs/notes/Java 虚拟机.md index 698e6dd6..870f7b6f 100644 --- a/docs/notes/Java 虚拟机.md +++ b/docs/notes/Java 虚拟机.md @@ -1,4 +1,4 @@ -* [点击阅读面试进阶指南 ](https://github.com/CyC2018/Backend-Interview-Guide) + * [一、运行时数据区域](#一运行时数据区域) * [程序计数器](#程序计数器) * [Java 虚拟机栈](#java-虚拟机栈) diff --git a/docs/notes/Leetcode 题解 - 二分查找.md b/docs/notes/Leetcode 题解 - 二分查找.md index ea8e616a..4a1bce8b 100644 --- a/docs/notes/Leetcode 题解 - 二分查找.md +++ b/docs/notes/Leetcode 题解 - 二分查找.md @@ -1,4 +1,4 @@ -* [点击阅读面试进阶指南 ](https://github.com/CyC2018/Backend-Interview-Guide) + * [原理](#原理) * [1. 正常实现](#1-正常实现) * [2. 时间复杂度](#2-时间复杂度) diff --git a/docs/notes/Leetcode 题解 - 位运算.md b/docs/notes/Leetcode 题解 - 位运算.md index 2c446a1f..cad7948a 100644 --- a/docs/notes/Leetcode 题解 - 位运算.md +++ b/docs/notes/Leetcode 题解 - 位运算.md @@ -1,4 +1,4 @@ -* [点击阅读面试进阶指南 ](https://github.com/CyC2018/Backend-Interview-Guide) + * [原理](#原理) * [1. 基本原理](#1-基本原理) * [2. mask 计算](#2-mask-计算) diff --git a/docs/notes/Leetcode 题解 - 分治.md b/docs/notes/Leetcode 题解 - 分治.md index 331fed03..e77b3f66 100644 --- a/docs/notes/Leetcode 题解 - 分治.md +++ b/docs/notes/Leetcode 题解 - 分治.md @@ -1,4 +1,4 @@ -* [点击阅读面试进阶指南 ](https://github.com/CyC2018/Backend-Interview-Guide) + * [1. 给表达式加括号](#1-给表达式加括号) diff --git a/docs/notes/Leetcode 题解 - 动态规划.md b/docs/notes/Leetcode 题解 - 动态规划.md index 6ef0d76a..8d06aae6 100644 --- a/docs/notes/Leetcode 题解 - 动态规划.md +++ b/docs/notes/Leetcode 题解 - 动态规划.md @@ -1,4 +1,4 @@ -* [点击阅读面试进阶指南 ](https://github.com/CyC2018/Backend-Interview-Guide) + * [斐波那契数列](#斐波那契数列) * [爬楼梯](#爬楼梯) * [强盗抢劫](#强盗抢劫) diff --git a/docs/notes/Leetcode 题解 - 双指针.md b/docs/notes/Leetcode 题解 - 双指针.md index ead999f7..00a630d8 100644 --- a/docs/notes/Leetcode 题解 - 双指针.md +++ b/docs/notes/Leetcode 题解 - 双指针.md @@ -1,4 +1,4 @@ -* [点击阅读面试进阶指南 ](https://github.com/CyC2018/Backend-Interview-Guide) + * [有序数组的 Two Sum](#有序数组的-two-sum) * [两数平方和](#两数平方和) * [反转字符串中的元音字符](#反转字符串中的元音字符) diff --git a/docs/notes/Leetcode 题解 - 哈希表.md b/docs/notes/Leetcode 题解 - 哈希表.md index 95a1772e..c7f0ba29 100644 --- a/docs/notes/Leetcode 题解 - 哈希表.md +++ b/docs/notes/Leetcode 题解 - 哈希表.md @@ -1,4 +1,4 @@ -* [点击阅读面试进阶指南 ](https://github.com/CyC2018/Backend-Interview-Guide) + * [1. 数组中两个数的和为给定值](#1-数组中两个数的和为给定值) * [2. 判断数组是否含有重复元素](#2-判断数组是否含有重复元素) * [3. 最长和谐序列](#3-最长和谐序列) diff --git a/docs/notes/Leetcode 题解 - 图.md b/docs/notes/Leetcode 题解 - 图.md index 5550ac85..a3ea29a2 100644 --- a/docs/notes/Leetcode 题解 - 图.md +++ b/docs/notes/Leetcode 题解 - 图.md @@ -1,4 +1,4 @@ -* [点击阅读面试进阶指南 ](https://github.com/CyC2018/Backend-Interview-Guide) + * [二分图](#二分图) * [判断是否为二分图](#判断是否为二分图) * [拓扑排序](#拓扑排序) diff --git a/docs/notes/Leetcode 题解 - 字符串.md b/docs/notes/Leetcode 题解 - 字符串.md index f9f28dfa..28a7e286 100644 --- a/docs/notes/Leetcode 题解 - 字符串.md +++ b/docs/notes/Leetcode 题解 - 字符串.md @@ -1,4 +1,4 @@ -* [点击阅读面试进阶指南 ](https://github.com/CyC2018/Backend-Interview-Guide) + * [字符串循环移位包含](#字符串循环移位包含) * [字符串循环移位](#字符串循环移位) * [字符串中单词的翻转](#字符串中单词的翻转) diff --git a/docs/notes/Leetcode 题解 - 排序.md b/docs/notes/Leetcode 题解 - 排序.md index ece72b6d..14c8013f 100644 --- a/docs/notes/Leetcode 题解 - 排序.md +++ b/docs/notes/Leetcode 题解 - 排序.md @@ -1,4 +1,4 @@ -* [点击阅读面试进阶指南 ](https://github.com/CyC2018/Backend-Interview-Guide) + * [快速选择](#快速选择) * [堆排序](#堆排序) * [Kth Element](#kth-element) diff --git a/docs/notes/Leetcode 题解 - 搜索.md b/docs/notes/Leetcode 题解 - 搜索.md index d13a0277..62f70eae 100644 --- a/docs/notes/Leetcode 题解 - 搜索.md +++ b/docs/notes/Leetcode 题解 - 搜索.md @@ -1,4 +1,4 @@ -* [点击阅读面试进阶指南 ](https://github.com/CyC2018/Backend-Interview-Guide) + * [BFS](#bfs) * [计算在网格中从原点到特定点的最短路径长度](#计算在网格中从原点到特定点的最短路径长度) * [组成整数的最小平方数数量](#组成整数的最小平方数数量) diff --git a/docs/notes/Leetcode 题解 - 数学.md b/docs/notes/Leetcode 题解 - 数学.md index 4806f11c..5933dbf7 100644 --- a/docs/notes/Leetcode 题解 - 数学.md +++ b/docs/notes/Leetcode 题解 - 数学.md @@ -1,4 +1,4 @@ -* [点击阅读面试进阶指南 ](https://github.com/CyC2018/Backend-Interview-Guide) + * [素数分解](#素数分解) * [整除](#整除) * [最大公约数最小公倍数](#最大公约数最小公倍数) diff --git a/docs/notes/Leetcode 题解 - 数组与矩阵.md b/docs/notes/Leetcode 题解 - 数组与矩阵.md index 5bf46788..f071481b 100644 --- a/docs/notes/Leetcode 题解 - 数组与矩阵.md +++ b/docs/notes/Leetcode 题解 - 数组与矩阵.md @@ -1,4 +1,4 @@ -* [点击阅读面试进阶指南 ](https://github.com/CyC2018/Backend-Interview-Guide) + * [1. 把数组中的 0 移到末尾](#1-把数组中的-0-移到末尾) * [2. 改变矩阵维度](#2-改变矩阵维度) * [3. 找出数组中最长的连续 1](#3-找出数组中最长的连续-1) diff --git a/docs/notes/Leetcode 题解 - 栈和队列.md b/docs/notes/Leetcode 题解 - 栈和队列.md index 13fdffb7..6ae10a07 100644 --- a/docs/notes/Leetcode 题解 - 栈和队列.md +++ b/docs/notes/Leetcode 题解 - 栈和队列.md @@ -1,4 +1,4 @@ -* [点击阅读面试进阶指南 ](https://github.com/CyC2018/Backend-Interview-Guide) + * [用栈实现队列](#用栈实现队列) * [用队列实现栈](#用队列实现栈) * [最小值栈](#最小值栈) diff --git a/docs/notes/Leetcode 题解 - 树.md b/docs/notes/Leetcode 题解 - 树.md index 195a43f9..fd965ac1 100644 --- a/docs/notes/Leetcode 题解 - 树.md +++ b/docs/notes/Leetcode 题解 - 树.md @@ -1,4 +1,4 @@ -* [点击阅读面试进阶指南 ](https://github.com/CyC2018/Backend-Interview-Guide) + * [递归](#递归) * [树的高度](#树的高度) * [平衡树](#平衡树) diff --git a/docs/notes/Leetcode 题解 - 目录.md b/docs/notes/Leetcode 题解 - 目录.md index d9333f81..ba23120e 100644 --- a/docs/notes/Leetcode 题解 - 目录.md +++ b/docs/notes/Leetcode 题解 - 目录.md @@ -1,4 +1,4 @@ -* [点击阅读面试进阶指南 ](https://github.com/CyC2018/Backend-Interview-Guide) + * [算法思想](#算法思想) * [数据结构相关](#数据结构相关) * [参考资料](#参考资料) diff --git a/docs/notes/Leetcode 题解 - 贪心思想.md b/docs/notes/Leetcode 题解 - 贪心思想.md index 8df05990..28766025 100644 --- a/docs/notes/Leetcode 题解 - 贪心思想.md +++ b/docs/notes/Leetcode 题解 - 贪心思想.md @@ -1,4 +1,4 @@ -* [点击阅读面试进阶指南 ](https://github.com/CyC2018/Backend-Interview-Guide) + * [分配饼干](#分配饼干) * [不重叠的区间个数](#不重叠的区间个数) * [投飞镖刺破气球](#投飞镖刺破气球) diff --git a/docs/notes/Leetcode 题解 - 链表.md b/docs/notes/Leetcode 题解 - 链表.md index f5c0bb7b..52d25731 100644 --- a/docs/notes/Leetcode 题解 - 链表.md +++ b/docs/notes/Leetcode 题解 - 链表.md @@ -1,4 +1,4 @@ -* [点击阅读面试进阶指南 ](https://github.com/CyC2018/Backend-Interview-Guide) + * [找出两个链表的交点](#找出两个链表的交点) * [链表反转](#链表反转) * [归并两个有序的链表](#归并两个有序的链表) diff --git a/docs/notes/Leetcode-Database 题解.md b/docs/notes/Leetcode-Database 题解.md index 41c0b572..a8a00d5b 100644 --- a/docs/notes/Leetcode-Database 题解.md +++ b/docs/notes/Leetcode-Database 题解.md @@ -1,4 +1,4 @@ -* [点击阅读面试进阶指南 ](https://github.com/CyC2018/Backend-Interview-Guide) + * [595. Big Countries](#595-big-countries) * [627. Swap Salary](#627-swap-salary) * [620. Not Boring Movies](#620-not-boring-movies) diff --git a/docs/notes/Linux.md b/docs/notes/Linux.md index 7340847b..65a3db9b 100644 --- a/docs/notes/Linux.md +++ b/docs/notes/Linux.md @@ -1,4 +1,4 @@ -* [点击阅读面试进阶指南 ](https://github.com/CyC2018/Backend-Interview-Guide) + * [一、常用操作以及概念](#一常用操作以及概念) * [快捷键](#快捷键) * [求助](#求助) diff --git a/docs/notes/MySQL.md b/docs/notes/MySQL.md index 6fb44a68..fdc723be 100644 --- a/docs/notes/MySQL.md +++ b/docs/notes/MySQL.md @@ -1,4 +1,4 @@ -* [点击阅读面试进阶指南 ](https://github.com/CyC2018/Backend-Interview-Guide) + * [一、索引](#一索引) * [B+ Tree 原理](#b-tree-原理) * [MySQL 索引](#mysql-索引) diff --git a/docs/notes/Redis.md b/docs/notes/Redis.md index 13a03b05..4b5b5e4a 100644 --- a/docs/notes/Redis.md +++ b/docs/notes/Redis.md @@ -1,4 +1,4 @@ -* [点击阅读面试进阶指南 ](https://github.com/CyC2018/Backend-Interview-Guide) + * [一、概述](#一概述) * [二、数据类型](#二数据类型) * [STRING](#string) diff --git a/docs/notes/SQL.md b/docs/notes/SQL.md index ddbdba3d..4a44aee8 100644 --- a/docs/notes/SQL.md +++ b/docs/notes/SQL.md @@ -1,4 +1,4 @@ -* [点击阅读面试进阶指南 ](https://github.com/CyC2018/Backend-Interview-Guide) + * [一、基础](#一基础) * [二、创建表](#二创建表) * [三、修改表](#三修改表) diff --git a/docs/notes/Socket.md b/docs/notes/Socket.md index f148f81e..a57fd1dc 100644 --- a/docs/notes/Socket.md +++ b/docs/notes/Socket.md @@ -1,4 +1,4 @@ -* [点击阅读面试进阶指南 ](https://github.com/CyC2018/Backend-Interview-Guide) + * [一、I/O 模型](#一io-模型) * [阻塞式 I/O](#阻塞式-io) * [非阻塞式 I/O](#非阻塞式-io) diff --git a/docs/notes/代码可读性.md b/docs/notes/代码可读性.md index 1a2e1f90..72193fc2 100644 --- a/docs/notes/代码可读性.md +++ b/docs/notes/代码可读性.md @@ -1,4 +1,4 @@ -* [点击阅读面试进阶指南 ](https://github.com/CyC2018/Backend-Interview-Guide) + * [一、可读性的重要性](#一可读性的重要性) * [二、用名字表达代码含义](#二用名字表达代码含义) * [三、名字不能带来歧义](#三名字不能带来歧义) diff --git a/docs/notes/代码风格规范.md b/docs/notes/代码风格规范.md index 35db1821..6c9ed1c8 100644 --- a/docs/notes/代码风格规范.md +++ b/docs/notes/代码风格规范.md @@ -1,4 +1,4 @@ -* [点击阅读面试进阶指南 ](https://github.com/CyC2018/Backend-Interview-Guide) + diff --git a/docs/notes/分布式.md b/docs/notes/分布式.md index a1358401..7ac86c29 100644 --- a/docs/notes/分布式.md +++ b/docs/notes/分布式.md @@ -1,4 +1,4 @@ -* [点击阅读面试进阶指南 ](https://github.com/CyC2018/Backend-Interview-Guide) + * [一、分布式锁](#一分布式锁) * [数据库的唯一索引](#数据库的唯一索引) * [Redis 的 SETNX 指令](#redis-的-setnx-指令) diff --git a/docs/notes/剑指 offer 题解.md b/docs/notes/剑指 offer 题解.md index bc433f7e..2710e20b 100644 --- a/docs/notes/剑指 offer 题解.md +++ b/docs/notes/剑指 offer 题解.md @@ -1,4 +1,4 @@ -* [点击阅读面试进阶指南 ](https://github.com/CyC2018/Backend-Interview-Guide) + * [3. 数组中重复的数字](#3-数组中重复的数字) * [4. 二维数组中的查找](#4-二维数组中的查找) * [5. 替换空格](#5-替换空格) diff --git a/docs/notes/攻击技术.md b/docs/notes/攻击技术.md index bef3916a..115456cb 100644 --- a/docs/notes/攻击技术.md +++ b/docs/notes/攻击技术.md @@ -1,4 +1,4 @@ -* [点击阅读面试进阶指南 ](https://github.com/CyC2018/Backend-Interview-Guide) + * [一、跨站脚本攻击](#一跨站脚本攻击) * [二、跨站请求伪造](#二跨站请求伪造) * [三、SQL 注入攻击](#三sql-注入攻击) diff --git a/docs/notes/数据库系统原理.md b/docs/notes/数据库系统原理.md index 7fe58e24..62c7cd55 100644 --- a/docs/notes/数据库系统原理.md +++ b/docs/notes/数据库系统原理.md @@ -1,4 +1,4 @@ -* [点击阅读面试进阶指南 ](https://github.com/CyC2018/Backend-Interview-Guide) + * [一、事务](#一事务) * [概念](#概念) * [ACID](#acid) diff --git a/docs/notes/构建工具.md b/docs/notes/构建工具.md index dfb63cbf..b4acd7aa 100644 --- a/docs/notes/构建工具.md +++ b/docs/notes/构建工具.md @@ -1,4 +1,4 @@ -* [点击阅读面试进阶指南 ](https://github.com/CyC2018/Backend-Interview-Guide) + * [一、构建工具的作用](#一构建工具的作用) * [二、Java 主流构建工具](#二java-主流构建工具) * [三、Maven](#三maven) diff --git a/docs/notes/正则表达式.md b/docs/notes/正则表达式.md index 22bdec70..065e12d1 100644 --- a/docs/notes/正则表达式.md +++ b/docs/notes/正则表达式.md @@ -1,4 +1,4 @@ -* [点击阅读面试进阶指南 ](https://github.com/CyC2018/Backend-Interview-Guide) + * [一、概述](#一概述) * [二、匹配单个字符](#二匹配单个字符) * [三、匹配一组字符](#三匹配一组字符) diff --git a/docs/notes/消息队列.md b/docs/notes/消息队列.md index 20e27f4c..f6038a77 100644 --- a/docs/notes/消息队列.md +++ b/docs/notes/消息队列.md @@ -1,4 +1,4 @@ -* [点击阅读面试进阶指南 ](https://github.com/CyC2018/Backend-Interview-Guide) + * [一、消息模型](#一消息模型) * [点对点](#点对点) * [发布/订阅](#发布订阅) diff --git a/docs/notes/算法.md b/docs/notes/算法.md index 3c85688f..2849449c 100644 --- a/docs/notes/算法.md +++ b/docs/notes/算法.md @@ -1,4 +1,4 @@ -* [点击阅读面试进阶指南 ](https://github.com/CyC2018/Backend-Interview-Guide) + * [一、前言](#一前言) * [二、算法分析](#二算法分析) * [数学模型](#数学模型) diff --git a/docs/notes/系统设计基础.md b/docs/notes/系统设计基础.md index 43cc166c..884bbe81 100644 --- a/docs/notes/系统设计基础.md +++ b/docs/notes/系统设计基础.md @@ -1,4 +1,4 @@ -* [点击阅读面试进阶指南 ](https://github.com/CyC2018/Backend-Interview-Guide) + * [一、性能](#一性能) * [二、伸缩性](#二伸缩性) * [三、扩展性](#三扩展性) diff --git a/docs/notes/缓存.md b/docs/notes/缓存.md index 428b2080..a8f9e77b 100644 --- a/docs/notes/缓存.md +++ b/docs/notes/缓存.md @@ -1,4 +1,4 @@ -* [点击阅读面试进阶指南 ](https://github.com/CyC2018/Backend-Interview-Guide) + * [一、缓存特征](#一缓存特征) * [二、LRU](#二lru) * [三、缓存位置](#三缓存位置) diff --git a/docs/notes/计算机操作系统.md b/docs/notes/计算机操作系统.md index a0768936..fcf9e222 100644 --- a/docs/notes/计算机操作系统.md +++ b/docs/notes/计算机操作系统.md @@ -1,4 +1,4 @@ -* [点击阅读面试进阶指南 ](https://github.com/CyC2018/Backend-Interview-Guide) + * [一、概述](#一概述) * [基本特征](#基本特征) * [基本功能](#基本功能) diff --git a/docs/notes/计算机网络.md b/docs/notes/计算机网络.md index a5b964cb..38999867 100644 --- a/docs/notes/计算机网络.md +++ b/docs/notes/计算机网络.md @@ -1,4 +1,4 @@ -* [点击阅读面试进阶指南 ](https://github.com/CyC2018/Backend-Interview-Guide) + * [一、概述](#一概述) * [网络的网络](#网络的网络) * [ISP](#isp) diff --git a/docs/notes/设计模式.md b/docs/notes/设计模式.md index 270e0331..00af5a8c 100644 --- a/docs/notes/设计模式.md +++ b/docs/notes/设计模式.md @@ -1,4 +1,4 @@ -* [点击阅读面试进阶指南 ](https://github.com/CyC2018/Backend-Interview-Guide) + * [一、概述](#一概述) * [二、创建型](#二创建型) * [1. 单例(Singleton)](#1-单例singleton) diff --git a/docs/notes/集群.md b/docs/notes/集群.md index 8030c238..5eb5ecec 100644 --- a/docs/notes/集群.md +++ b/docs/notes/集群.md @@ -1,4 +1,4 @@ -* [点击阅读面试进阶指南 ](https://github.com/CyC2018/Backend-Interview-Guide) + * [一、负载均衡](#一负载均衡) * [负载均衡算法](#负载均衡算法) * [转发实现](#转发实现) diff --git a/docs/notes/面向对象思想.md b/docs/notes/面向对象思想.md index 7a2f769c..51bff5f7 100644 --- a/docs/notes/面向对象思想.md +++ b/docs/notes/面向对象思想.md @@ -1,4 +1,4 @@ -* [点击阅读面试进阶指南 ](https://github.com/CyC2018/Backend-Interview-Guide) + * [一、三大特性](#一三大特性) * [封装](#封装) * [继承](#继承) From 40a3f7e93282d74366fb01d0816163a5e15e71f7 Mon Sep 17 00:00:00 2001 From: CyC2018 <1029579233@qq.com> Date: Fri, 8 Mar 2019 20:52:10 +0800 Subject: [PATCH 06/56] auto commit --- docs/notes/Leetcode 题解 - 目录1.md | 38 +++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 docs/notes/Leetcode 题解 - 目录1.md diff --git a/docs/notes/Leetcode 题解 - 目录1.md b/docs/notes/Leetcode 题解 - 目录1.md new file mode 100644 index 00000000..67081594 --- /dev/null +++ b/docs/notes/Leetcode 题解 - 目录1.md @@ -0,0 +1,38 @@ + +* [算法思想](#算法思想) +* [数据结构相关](#数据结构相关) +* [参考资料](#参考资料) + + + +# 算法思想 + +- [双指针](notes/Leetcode%20题解%20-%20双指针.md) +- [排序](notes/Leetcode%20题解%20-%20排序.md) +- [贪心思想](notes/Leetcode%20题解%20-%20贪心思想.md) +- [二分查找](notes/Leetcode%20题解%20-%20二分查找.md) +- [分治](notes/Leetcode%20题解%20-%20分治.md) +- [搜索](notes/Leetcode%20题解%20-%20搜索.md) +- [动态规划](notes/Leetcode%20题解%20-%20动态规划.md) +- [数学](notes/Leetcode%20题解%20-%20数学.md) + +# 数据结构相关 + +- [链表](notes/Leetcode%20题解%20-%20链表.md) +- [树](notes/Leetcode%20题解%20-%20树.md) +- [栈和队列](notes/Leetcode%20题解%20-%20栈和队列.md) +- [哈希表](notes/Leetcode%20题解%20-%20哈希表.md) +- [字符串](notes/Leetcode%20题解%20-%20字符串.md) +- [数组与矩阵](notes/Leetcode%20题解%20-%20数组与矩阵.md) +- [图](notes/Leetcode%20题解%20-%20图.md) +- [位运算](notes/Leetcode%20题解%20-%20位运算.md) + +# 参考资料 + + +- Leetcode +- Weiss M A, 冯舜玺. 数据结构与算法分析——C 语言描述[J]. 2004. +- Sedgewick R. Algorithms[M]. Pearson Education India, 1988. +- 何海涛, 软件工程师. 剑指 Offer: 名企面试官精讲典型编程题[M]. 电子工业出版社, 2014. +- 《编程之美》小组. 编程之美[M]. 电子工业出版社, 2008. +- 左程云. 程序员代码面试指南[M]. 电子工业出版社, 2015. From 3c0df553139610b1ae20254b4902b0e2046bd574 Mon Sep 17 00:00:00 2001 From: CyC2018 <1029579233@qq.com> Date: Fri, 8 Mar 2019 20:52:43 +0800 Subject: [PATCH 07/56] Update README.md --- docs/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/README.md b/docs/README.md index 6a1a4552..97d3d9a0 100644 --- a/docs/README.md +++ b/docs/README.md @@ -3,7 +3,7 @@ ## ✏️ 算法 - [剑指 Offer 题解](notes/剑指%20offer%20题解.md)
-- [Leetcode 题解](notes/Leetcode%20题解%20-%20目录.md)
+- [Leetcode 题解](notes/Leetcode%20题解%20-%20目录1.md)
- [算法](notes/算法.md)
- [点击订阅面试进阶指南](https://xiaozhuanlan.com/CyC2018) From f9897bd529cf7d2d4245228bc8eadb6e380054e4 Mon Sep 17 00:00:00 2001 From: CyC2018 <1029579233@qq.com> Date: Fri, 8 Mar 2019 21:29:22 +0800 Subject: [PATCH 08/56] auto commit --- README.md | 2 +- docs/notes/剑指 Offer 题解 - 10~19.md | 685 ++++++ docs/notes/剑指 Offer 题解 - 20~29.md | 378 +++ docs/notes/剑指 Offer 题解 - 30~39.md | 493 ++++ docs/notes/剑指 Offer 题解 - 3~9.md | 382 ++++ docs/notes/剑指 Offer 题解 - 40~49.md | 440 ++++ docs/notes/剑指 Offer 题解 - 50~59.md | 499 ++++ docs/notes/剑指 Offer 题解 - 60~68.md | 341 +++ docs/notes/剑指 Offer 题解 - 目录.md | 15 + docs/notes/剑指 offer 题解.md | 3042 ------------------------- 10 files changed, 3234 insertions(+), 3043 deletions(-) create mode 100644 docs/notes/剑指 Offer 题解 - 10~19.md create mode 100644 docs/notes/剑指 Offer 题解 - 20~29.md create mode 100644 docs/notes/剑指 Offer 题解 - 30~39.md create mode 100644 docs/notes/剑指 Offer 题解 - 3~9.md create mode 100644 docs/notes/剑指 Offer 题解 - 40~49.md create mode 100644 docs/notes/剑指 Offer 题解 - 50~59.md create mode 100644 docs/notes/剑指 Offer 题解 - 60~68.md create mode 100644 docs/notes/剑指 Offer 题解 - 目录.md delete mode 100644 docs/notes/剑指 offer 题解.md diff --git a/README.md b/README.md index 688be90d..220820f6 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ ## :pencil2: 算法 -- [剑指 Offer 题解](https://github.com/CyC2018/CS-Notes/blob/master/docs/notes/剑指%20offer%20题解.md) +- [剑指 Offer 题解](https://github.com/CyC2018/CS-Notes/blob/master/docs/notes/剑指%20offer%20题解%20-%20目录.md) - [Leetcode 题解](https://github.com/CyC2018/CS-Notes/blob/master/docs/notes/Leetcode%20题解%20-%20目录.md) - [算法](https://github.com/CyC2018/CS-Notes/blob/master/docs/notes/算法.md) diff --git a/docs/notes/剑指 Offer 题解 - 10~19.md b/docs/notes/剑指 Offer 题解 - 10~19.md new file mode 100644 index 00000000..71b055e5 --- /dev/null +++ b/docs/notes/剑指 Offer 题解 - 10~19.md @@ -0,0 +1,685 @@ + +* [10.1 斐波那契数列](#101-斐波那契数列) + * [题目描述](#题目描述) + * [解题思路](#解题思路) +* [10.2 矩形覆盖](#102-矩形覆盖) + * [题目描述](#题目描述) + * [解题思路](#解题思路) +* [10.3 跳台阶](#103-跳台阶) + * [题目描述](#题目描述) + * [解题思路](#解题思路) +* [10.4 变态跳台阶](#104-变态跳台阶) + * [题目描述](#题目描述) + * [解题思路](#解题思路) + * [动态规划](#动态规划) + * [数学推导](#数学推导) +* [11. 旋转数组的最小数字](#11-旋转数组的最小数字) + * [题目描述](#题目描述) + * [解题思路](#解题思路) +* [12. 矩阵中的路径](#12-矩阵中的路径) + * [题目描述](#题目描述) + * [解题思路](#解题思路) +* [13. 机器人的运动范围](#13-机器人的运动范围) + * [题目描述](#题目描述) + * [解题思路](#解题思路) +* [14. 剪绳子](#14-剪绳子) + * [题目描述](#题目描述) + * [解题思路](#解题思路) + * [贪心](#贪心) + * [动态规划](#动态规划) +* [15. 二进制中 1 的个数](#15-二进制中-1-的个数) + * [题目描述](#题目描述) + * [n&(n-1)](#n&n-1) + * [Integer.bitCount()](#integerbitcount) +* [16. 数值的整数次方](#16-数值的整数次方) + * [题目描述](#题目描述) + * [解题思路](#解题思路) +* [17. 打印从 1 到最大的 n 位数](#17-打印从-1-到最大的-n-位数) + * [题目描述](#题目描述) + * [解题思路](#解题思路) +* [18.1 在 O(1) 时间内删除链表节点](#181-在-o1-时间内删除链表节点) + * [解题思路](#解题思路) +* [18.2 删除链表中重复的结点](#182-删除链表中重复的结点) + * [题目描述](#题目描述) + * [解题描述](#解题描述) +* [19. 正则表达式匹配](#19-正则表达式匹配) + * [题目描述](#题目描述) + * [解题思路](#解题思路) + + + +# 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) + +## 题目描述 + +求斐波那契数列的第 n 项,n <= 39。 + + + +

+ +## 解题思路 + +如果使用递归求解,会重复计算一些子问题。例如,计算 f(10) 需要计算 f(9) 和 f(8),计算 f(9) 需要计算 f(8) 和 f(7),可以看到 f(8) 被重复计算了。 + +

+ + +递归是将一个问题划分成多个子问题求解,动态规划也是如此,但是动态规划会把子问题的解缓存起来,从而避免重复求解子问题。 + +```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; + fib[2] = 2; + for (int i = 2; i < fib.length; i++) + fib[i] = fib[i - 1] + fib[i - 2]; + } + + public int Fibonacci(int n) { + return fib[n]; + } +} +``` + +# 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) + +## 题目描述 + +我们可以用 2\*1 的小矩形横着或者竖着去覆盖更大的矩形。请问用 n 个 2\*1 的小矩形无重叠地覆盖一个 2\*n 的大矩形,总共有多少种方法? + +

+ +## 解题思路 + +```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; +} +``` + +# 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) + +## 题目描述 + +一只青蛙一次可以跳上 1 级台阶,也可以跳上 2 级。求该青蛙跳上一个 n 级的台阶总共有多少种跳法。 + +

+ +## 解题思路 + +```java +public int JumpFloor(int n) { + if (n <= 2) + return n; + int pre2 = 1, pre1 = 2; + int result = 1; + for (int i = 2; i < n; i++) { + result = pre2 + pre1; + pre2 = pre1; + pre1 = result; + } + return result; +} +``` + +# 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) + +## 题目描述 + +一只青蛙一次可以跳上 1 级台阶,也可以跳上 2 级... 它也可以跳上 n 级。求该青蛙跳上一个 n 级的台阶总共有多少种跳法。 + +

+ +## 解题思路 + +### 动态规划 + +```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); +} +``` + + +# 11. 旋转数组的最小数字 + +[NowCoder](https://www.nowcoder.com/practice/9f3231a991af4f55b95579b44b7a01ba?tpId=13&tqId=11159&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) + +## 题目描述 + +把一个数组最开始的若干个元素搬到数组的末尾,我们称之为数组的旋转。输入一个非递减排序的数组的一个旋转,输出旋转数组的最小元素。 + +例如数组 {3, 4, 5, 1, 2} 为 {1, 2, 3, 4, 5} 的一个旋转,该数组的最小值为 1。 + +## 解题思路 + +在一个有序数组中查找一个元素可以用二分查找,二分查找也称为折半查找,每次都能将查找区间减半,这种折半特性的算法时间复杂度都为 O(logN)。 + +本题可以修改二分查找算法进行求解: + +- 当 nums[m] <= nums[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]; +} +``` + +# 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) + +## 题目描述 + +请设计一个函数,用来判断在一个矩阵中是否存在一条包含某字符串所有字符的路径。路径可以从矩阵中的任意一个格子开始,每一步可以在矩阵中向左,向右,向上,向下移动一个格子。如果一条路径经过了矩阵中的某一个格子,则该路径不能再进入该格子。 + +例如下面的矩阵包含了一条 bfce 路径。 + +

+ +## 解题思路 + +```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 i = 0, idx = 0; i < rows; i++) + for (int j = 0; j < cols; j++) + matrix[i][j] = array[idx++]; + return matrix; +} +``` + +# 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) + +## 题目描述 + +地上有一个 m 行和 n 列的方格。一个机器人从坐标 (0, 0) 的格子开始移动,每一次只能向左右上下四个方向移动一格,但是不能进入行坐标和列坐标的数位之和大于 k 的格子。 + +例如,当 k 为 18 时,机器人能够进入方格 (35,37),因为 3+5+3+7=18。但是,它不能进入方格 (35,38),因为 3+5+3+8=19。请问该机器人能够达到多少个格子? + +## 解题思路 + +```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]; +} +``` + +# 14. 剪绳子 + +[Leetcode](https://leetcode.com/problems/integer-break/description/) + +## 题目描述 + +把一根绳子剪成多段,并且使得每段的长度乘积最大。 + +```html +n = 2 +return 1 (2 = 1 + 1) + +n = 10 +return 36 (10 = 3 + 3 + 4) +``` + +## 解题思路 + +### 贪心 + +尽可能多剪长度为 3 的绳子,并且不允许有长度为 1 的绳子出现。如果出现了,就从已经切好长度为 3 的绳子中拿出一段与长度为 1 的绳子重新组合,把它们切成两段长度为 2 的绳子。 + +证明:当 n >= 5 时,3(n - 3) - n = 2n - 9 > 0,且 2(n - 2) - n = n - 4 > 0。因此在 n >= 5 的情况下,将绳子剪成一段为 2 或者 3,得到的乘积会更大。又因为 3(n - 3) - 2(n - 2) = n - 5 >= 0,所以剪成一段长度为 3 比长度为 2 得到的乘积更大。 + +```java +public int integerBreak(int n) { + if (n < 2) + return 0; + if (n == 2) + return 1; + if (n == 3) + return 2; + int timesOf3 = n / 3; + if (n - timesOf3 * 3 == 1) + timesOf3--; + int timesOf2 = (n - timesOf3 * 3) / 2; + return (int) (Math.pow(3, timesOf3)) * (int) (Math.pow(2, timesOf2)); +} +``` + +### 动态规划 + +```java +public int integerBreak(int n) { + int[] dp = new int[n + 1]; + dp[1] = 1; + for (int i = 2; i <= n; i++) + for (int j = 1; j < i; j++) + dp[i] = Math.max(dp[i], Math.max(j * (i - j), dp[j] * (i - j))); + return dp[n]; +} +``` + +# 15. 二进制中 1 的个数 + +[NowCoder](https://www.nowcoder.com/practice/8ee967e43c2c4ec193b040ea7fbb10b8?tpId=13&tqId=11164&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) + +## 题目描述 + +输入一个整数,输出该数二进制表示中 1 的个数。 + +### n&(n-1) + +该位运算去除 n 的位级表示中最低的那一位。 + +``` +n : 10110100 +n-1 : 10110011 +n&(n-1) : 10110000 +``` + +时间复杂度:O(M),其中 M 表示 1 的个数。 + + +```java +public int NumberOf1(int n) { + int cnt = 0; + while (n != 0) { + cnt++; + n &= (n - 1); + } + return cnt; +} +``` + + +### Integer.bitCount() + +```java +public int NumberOf1(int n) { + return Integer.bitCount(n); +} +``` + +# 16. 数值的整数次方 + +[NowCoder](https://www.nowcoder.com/practice/1a834e5e3e1a4b7ba251417554e07c00?tpId=13&tqId=11165&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) + +## 题目描述 + +给定一个 double 类型的浮点数 base 和 int 类型的整数 exponent,求 base 的 exponent 次方。 + +## 解题思路 + +下面的讨论中 x 代表 base,n 代表 exponent。 + + + +

+ +因为 (x\*x)n/2 可以通过递归求解,并且每次递归 n 都减小一半,因此整个算法的时间复杂度为 O(logN)。 + +```java +public double Power(double base, int exponent) { + if (exponent == 0) + return 1; + if (exponent == 1) + return base; + boolean isNegative = false; + if (exponent < 0) { + exponent = -exponent; + isNegative = true; + } + double pow = Power(base * base, exponent / 2); + if (exponent % 2 != 0) + pow = pow * base; + return isNegative ? 1 / pow : pow; +} +``` + +# 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(); +} +``` + +# 18.1 在 O(1) 时间内删除链表节点 + +## 解题思路 + +① 如果该节点不是尾节点,那么可以直接将下一个节点的值赋给该节点,然后令该节点指向下下个节点,再删除下一个节点,时间复杂度为 O(1)。 + +

+ +② 如果链表只有一个节点,那么直接 + +② 否则,就需要先遍历链表,找到节点的前一个节点,然后让前一个节点指向 null,时间复杂度为 O(N)。 + +

+ +综上,如果进行 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; +} +``` + +# 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) + +## 题目描述 + +

+ +## 解题描述 + +```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; + } +} +``` + +# 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) + +## 题目描述 + +请实现一个函数用来匹配包括 '.' 和 '\*' 的正则表达式。模式中的字符 '.' 表示任意一个字符,而 '\*' 表示它前面的字符可以出现任意次(包含 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]; +} +``` diff --git a/docs/notes/剑指 Offer 题解 - 20~29.md b/docs/notes/剑指 Offer 题解 - 20~29.md new file mode 100644 index 00000000..1af2bdcc --- /dev/null +++ b/docs/notes/剑指 Offer 题解 - 20~29.md @@ -0,0 +1,378 @@ + +* [20. 表示数值的字符串](#20-表示数值的字符串) + * [题目描述](#题目描述) + * [解题思路](#解题思路) +* [21. 调整数组顺序使奇数位于偶数前面](#21-调整数组顺序使奇数位于偶数前面) + * [题目描述](#题目描述) + * [解题思路](#解题思路) +* [22. 链表中倒数第 K 个结点](#22-链表中倒数第-k-个结点) + * [解题思路](#解题思路) +* [23. 链表中环的入口结点](#23-链表中环的入口结点) + * [题目描述](#题目描述) + * [解题思路](#解题思路) +* [24. 反转链表](#24-反转链表) + * [解题思路](#解题思路) + * [递归](#递归) + * [迭代](#迭代) +* [25. 合并两个排序的链表](#25-合并两个排序的链表) + * [题目描述](#题目描述) + * [解题思路](#解题思路) + * [递归](#递归) + * [迭代](#迭代) +* [26. 树的子结构](#26-树的子结构) + * [题目描述](#题目描述) + * [解题思路](#解题思路) +* [27. 二叉树的镜像](#27-二叉树的镜像) + * [题目描述](#题目描述) + * [解题思路](#解题思路) +* [28 对称的二叉树](#28-对称的二叉树) + * [题目描述](#题目描述) + * [解题思路](#解题思路) +* [29. 顺时针打印矩阵](#29-顺时针打印矩阵) + * [题目描述](#题目描述) + * [解题思路](#解题思路) + + + +# 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) + +## 题目描述 + +```html +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+)?"); +} +``` + +# 21. 调整数组顺序使奇数位于偶数前面 + +[NowCoder](https://www.nowcoder.com/practice/beb5aa231adc45b2a5dcc5b62c93f593?tpId=13&tqId=11166&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) + +## 题目描述 + +需要保证奇数和奇数,偶数和偶数之间的相对位置不变,这和书本不太一样。 + +

+ +## 解题思路 + +```java +public void reOrderArray(int[] nums) { + // 奇数个数 + int oddCnt = 0; + for (int val : nums) + if (val % 2 == 1) + 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; + } +} +``` + +# 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) + +## 解题思路 + +设链表的长度为 N。设两个指针 P1 和 P2,先让 P1 移动 K 个节点,则还有 N - K 个节点可以移动。此时让 P1 和 P2 同时移动,可以知道当 P1 移动到链表结尾时,P2 移动到 N - K 个节点处,该位置就是倒数第 K 个节点。 + +

+ +```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; +} +``` + +# 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) + +## 题目描述 + +一个链表中包含环,请找出该链表的环的入口结点。要求不能使用额外的空间。 + +## 解题思路 + +使用双指针,一个指针 fast 每次移动两个节点,一个指针 slow 每次移动一个节点。因为存在环,所以两个指针必定相遇在环中的某个节点上。假设相遇点在下图的 z1 位置,此时 fast 移动的节点数为 x+2y+z,slow 为 x+y,由于 fast 速度比 slow 快一倍,因此 x+2y+z=2(x+y),得到 x=z。 + +在相遇点,slow 要到环的入口点还需要移动 z 个节点,如果让 fast 重新从头开始移动,并且速度变为每次移动一个节点,那么它到环入口点还需要移动 x 个节点。在上面已经推导出 x=z,因此 fast 和 slow 将在环入口点相遇。 + +

+ + +```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; +} +``` + +# 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) + +## 解题思路 + +### 递归 + +```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; +} +``` + +# 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) + +## 题目描述 + +

+ +## 解题思路 + +### 递归 + +```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; +} +``` + +# 26. 树的子结构 + +[NowCoder](https://www.nowcoder.com/practice/6e196c44c7004d15b1610b9afca8bd88?tpId=13&tqId=11170&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) + +## 题目描述 + +

+ +## 解题思路 + +```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); +} +``` + +# 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) + +## 题目描述 + +

+ +## 解题思路 + +```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; +} +``` + +# 28 对称的二叉树 + +[NowCder](https://www.nowcoder.com/practice/ff05d44dfdb04e1d83bdbdab320efbcb?tpId=13&tqId=11211&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) + +## 题目描述 + +

+ +## 解题思路 + +```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); +} +``` + +# 29. 顺时针打印矩阵 + +[NowCoder](https://www.nowcoder.com/practice/9b4c81a02cd34f76be2659fa0d54342a?tpId=13&tqId=11172&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) + +## 题目描述 + +下图的矩阵顺时针打印结果为:1, 2, 3, 4, 8, 12, 16, 15, 14, 13, 9, 5, 6, 7, 11, 10 + +

+ +## 解题思路 + +```java +public ArrayList printMatrix(int[][] matrix) { + ArrayList 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; +} +``` diff --git a/docs/notes/剑指 Offer 题解 - 30~39.md b/docs/notes/剑指 Offer 题解 - 30~39.md new file mode 100644 index 00000000..f60b43a3 --- /dev/null +++ b/docs/notes/剑指 Offer 题解 - 30~39.md @@ -0,0 +1,493 @@ + +* [30. 包含 min 函数的栈](#30-包含-min-函数的栈) + * [题目描述](#题目描述) + * [解题思路](#解题思路) +* [31. 栈的压入、弹出序列](#31-栈的压入弹出序列) + * [题目描述](#题目描述) + * [解题思路](#解题思路) +* [32.1 从上往下打印二叉树](#321-从上往下打印二叉树) + * [题目描述](#题目描述) + * [解题思路](#解题思路) +* [32.2 把二叉树打印成多行](#322-把二叉树打印成多行) + * [题目描述](#题目描述) + * [解题思路](#解题思路) +* [32.3 按之字形顺序打印二叉树](#323-按之字形顺序打印二叉树) + * [题目描述](#题目描述) + * [解题思路](#解题思路) +* [33. 二叉搜索树的后序遍历序列](#33-二叉搜索树的后序遍历序列) + * [题目描述](#题目描述) + * [解题思路](#解题思路) +* [34. 二叉树中和为某一值的路径](#34-二叉树中和为某一值的路径) + * [题目描述](#题目描述) + * [解题思路](#解题思路) +* [35. 复杂链表的复制](#35-复杂链表的复制) + * [题目描述](#题目描述) + * [解题思路](#解题思路) +* [36. 二叉搜索树与双向链表](#36-二叉搜索树与双向链表) + * [题目描述](#题目描述) + * [解题思路](#解题思路) +* [37. 序列化二叉树](#37-序列化二叉树) + * [题目描述](#题目描述) + * [解题思路](#解题思路) +* [38. 字符串的排列](#38-字符串的排列) + * [题目描述](#题目描述) + * [解题思路](#解题思路) +* [39. 数组中出现次数超过一半的数字](#39-数组中出现次数超过一半的数字) + * [解题思路](#解题思路) + + + +# 30. 包含 min 函数的栈 + +[NowCoder](https://www.nowcoder.com/practice/4c776177d2c04c2494f2555c9fcc1e49?tpId=13&tqId=11173&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) + +## 题目描述 + +定义栈的数据结构,请在该类型中实现一个能够得到栈最小元素的 min 函数。 + +## 解题思路 + +```java +private Stack dataStack = new Stack<>(); +private Stack 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(); +} +``` + +# 31. 栈的压入、弹出序列 + +[NowCoder](https://www.nowcoder.com/practice/d77d11405cc7470d82554cb392585106?tpId=13&tqId=11174&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) + +## 题目描述 + +输入两个整数序列,第一个序列表示栈的压入顺序,请判断第二个序列是否为该栈的弹出顺序。假设压入栈的所有数字均不相等。 + +例如序列 1,2,3,4,5 是某栈的压入顺序,序列 4,5,3,2,1 是该压栈序列对应的一个弹出序列,但 4,3,5,1,2 就不可能是该压栈序列的弹出序列。 + +## 解题思路 + +使用一个栈来模拟压入弹出操作。 + +```java +public boolean IsPopOrder(int[] pushSequence, int[] popSequence) { + int n = pushSequence.length; + Stack 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(); +} +``` + +# 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) + +## 题目描述 + +从上往下打印出二叉树的每个节点,同层节点从左至右打印。 + +例如,以下二叉树层次遍历的结果为:1,2,3,4,5,6,7 + +

+ +## 解题思路 + +使用队列来进行层次遍历。 + +不需要使用两个队列分别存储当前层的节点和下一层的节点,因为在开始遍历一层的节点时,当前队列中的节点数就是当前层的节点数,只要控制遍历这么多节点数,就能保证这次遍历的都是当前层的节点。 + +```java +public ArrayList PrintFromTopToBottom(TreeNode root) { + Queue queue = new LinkedList<>(); + ArrayList 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; +} +``` + +# 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) + +## 题目描述 + +和上题几乎一样。 + +## 解题思路 + +```java +ArrayList> Print(TreeNode pRoot) { + ArrayList> ret = new ArrayList<>(); + Queue queue = new LinkedList<>(); + queue.add(pRoot); + while (!queue.isEmpty()) { + ArrayList 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; +} +``` + +# 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) + +## 题目描述 + +请实现一个函数按照之字形打印二叉树,即第一行按照从左到右的顺序打印,第二层按照从右至左的顺序打印,第三行按照从左到右的顺序打印,其他行以此类推。 + +## 解题思路 + +```java +public ArrayList> Print(TreeNode pRoot) { + ArrayList> ret = new ArrayList<>(); + Queue queue = new LinkedList<>(); + queue.add(pRoot); + boolean reverse = false; + while (!queue.isEmpty()) { + ArrayList 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; +} +``` + +# 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) + +## 题目描述 + +输入一个整数数组,判断该数组是不是某二叉搜索树的后序遍历的结果。假设输入的数组的任意两个数字都互不相同。 + +例如,下图是后序遍历序列 1,3,2 所对应的二叉搜索树。 + +

+ +## 解题思路 + +```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); +} +``` + +# 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) + +## 题目描述 + +输入一颗二叉树和一个整数,打印出二叉树中结点值的和为输入整数的所有路径。路径定义为从树的根结点开始往下一直到叶结点所经过的结点形成一条路径。 + +下图的二叉树有两条和为 22 的路径:10, 5, 7 和 10, 12 + +

+ +## 解题思路 + +```java +private ArrayList> ret = new ArrayList<>(); + +public ArrayList> FindPath(TreeNode root, int target) { + backtracking(root, target, new ArrayList<>()); + return ret; +} + +private void backtracking(TreeNode node, int target, ArrayList 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); +} +``` + +# 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) + +## 题目描述 + +输入一个复杂链表(每个节点中有节点值,以及两个指针,一个指向下一个节点,另一个特殊指针指向任意一个节点),返回结果为复制后复杂链表的 head。 + +```java +public class RandomListNode { + int label; + RandomListNode next = null; + RandomListNode random = null; + + RandomListNode(int label) { + this.label = label; + } +} +``` + +

+ +## 解题思路 + +第一步,在每个节点的后面插入复制的节点。 + +

+ +第二步,对复制节点的 random 链接进行赋值。 + +

+ +第三步,拆分。 + +

+ +```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; +} +``` + +# 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) + +## 题目描述 + +输入一棵二叉搜索树,将该二叉搜索树转换成一个排序的双向链表。要求不能创建任何新的结点,只能调整树中结点指针的指向。 + +

+ +## 解题思路 + +```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); +} +``` + +# 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) + +## 题目描述 + +请实现两个函数,分别用来序列化和反序列化二叉树。 + +## 解题思路 + +```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; +} +``` + +# 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) + +## 题目描述 + +输入一个字符串,按字典序打印出该字符串中字符的所有排列。例如输入字符串 abc,则打印出由字符 a, b, c 所能排列出来的所有字符串 abc, acb, bac, bca, cab 和 cba。 + +## 解题思路 + +```java +private ArrayList ret = new ArrayList<>(); + +public ArrayList 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; + } +} +``` + +# 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) + +## 解题思路 + +多数投票问题,可以利用 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; +} +``` diff --git a/docs/notes/剑指 Offer 题解 - 3~9.md b/docs/notes/剑指 Offer 题解 - 3~9.md new file mode 100644 index 00000000..178998a9 --- /dev/null +++ b/docs/notes/剑指 Offer 题解 - 3~9.md @@ -0,0 +1,382 @@ + +* [3. 数组中重复的数字](#3-数组中重复的数字) + * [题目描述](#题目描述) + * [解题思路](#解题思路) +* [4. 二维数组中的查找](#4-二维数组中的查找) + * [题目描述](#题目描述) + * [解题思路](#解题思路) +* [5. 替换空格](#5-替换空格) + * [题目描述](#题目描述) + * [解题思路](#解题思路) +* [6. 从尾到头打印链表](#6-从尾到头打印链表) + * [题目描述](#题目描述) + * [解题思路](#解题思路) + * [使用递归](#使用递归) + * [使用头插法](#使用头插法) + * [使用栈](#使用栈) +* [7. 重建二叉树](#7-重建二叉树) + * [题目描述](#题目描述) + * [解题思路](#解题思路) +* [8. 二叉树的下一个结点](#8-二叉树的下一个结点) + * [题目描述](#题目描述) + * [解题思路](#解题思路) +* [9. 用两个栈实现队列](#9-用两个栈实现队列) + * [题目描述](#题目描述) + * [解题思路](#解题思路) + + + +# 3. 数组中重复的数字 + +[NowCoder](https://www.nowcoder.com/practice/623a5ac0ea5b4e5f95552655361ae0a8?tpId=13&tqId=11203&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) + +## 题目描述 + +在一个长度为 n 的数组里的所有数字都在 0 到 n-1 的范围内。数组中某些数字是重复的,但不知道有几个数字是重复的,也不知道每个数字重复几次。请找出数组中任意一个重复的数字。 + +```html +Input: +{2, 3, 1, 0, 2, 5} + +Output: +2 +``` + +## 解题思路 + +要求是时间复杂度 O(N),空间复杂度 O(1)。因此不能使用排序的方法,也不能使用额外的标记数组。 + +对于这种数组元素在 [0, n-1] 范围内的问题,可以将值为 i 的元素调整到第 i 个位置上进行求解。 + +以 (2, 3, 1, 0, 2, 5) 为例,遍历到位置 4 时,该位置上的数为 2,但是第 2 个位置上已经有一个 2 的值了,因此可以知道 2 重复: + +

+ +```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; +} +``` + +# 4. 二维数组中的查找 + +[NowCoder](https://www.nowcoder.com/practice/abc3fe2ce8e146608e868a70efebf62e?tpId=13&tqId=11154&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) + +## 题目描述 + +给定一个二维数组,其每一行从左到右递增排序,从上到下也是递增排序。给定一个数,判断这个数是否在该二维数组中。 + +```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)。 + +该二维数组中的一个数,它左边的数都比它小,下边的数都比它大。因此,从右上角开始查找,就可以根据 target 和当前元素的大小关系来缩小查找区间,当前元素的查找区间为左下角的所有元素。 + +

+ +```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; +} +``` + +# 5. 替换空格 + +[NowCoder](https://www.nowcoder.com/practice/4060ac7e3e404ad1a894ef3e17650423?tpId=13&tqId=11155&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) + +## 题目描述 + + +将一个字符串中的空格替换成 "%20"。 + +```text +Input: +"A B" + +Output: +"A%20B" +``` + +## 解题思路 + +在字符串尾部填充任意字符,使得字符串的长度等于替换之后的长度。因为一个空格要替换成三个字符(%20),因此当遍历到一个空格时,需要在尾部填充两个任意字符。 + +令 P1 指向字符串原来的末尾位置,P2 指向字符串现在的末尾位置。P1 和 P2 从后向前遍历,当 P1 遍历到一个空格时,就需要令 P2 指向的位置依次填充 02%(注意是逆序的),否则就填充上 P1 指向字符的值。 + +从后向前遍是为了在改变 P2 所指向的内容时,不会影响到 P1 遍历原来字符串的内容。 + +

+ +```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(); +} +``` + +# 6. 从尾到头打印链表 + +[NowCoder](https://www.nowcoder.com/practice/d0267f7f55b3412ba93bd35cfa8e8035?tpId=13&tqId=11156&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) + +## 题目描述 + +从尾到头反过来打印出每个结点的值。 + +

+ +## 解题思路 + +### 使用递归 + +

+ + +```java +public ArrayList printListFromTailToHead(ListNode listNode) { + ArrayList ret = new ArrayList<>(); + if (listNode != null) { + ret.addAll(printListFromTailToHead(listNode.next)); + ret.add(listNode.val); + } + return ret; +} +``` + +### 使用头插法 + +利用链表头插法为逆序的特点。 + +头结点和第一个节点的区别: + +- 头结点是在头插法中使用的一个额外节点,这个节点不存储值; +- 第一个节点就是链表的第一个真正存储值的节点。 + + +

+ +```java +public ArrayList 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 ret = new ArrayList<>(); + head = head.next; + while (head != null) { + ret.add(head.val); + head = head.next; + } + return ret; +} +``` + +### 使用栈 + +

+ +```java +public ArrayList printListFromTailToHead(ListNode listNode) { + Stack stack = new Stack<>(); + while (listNode != null) { + stack.add(listNode.val); + listNode = listNode.next; + } + ArrayList ret = new ArrayList<>(); + while (!stack.isEmpty()) + ret.add(stack.pop()); + return ret; +} +``` + +# 7. 重建二叉树 + +[NowCoder](https://www.nowcoder.com/practice/8a19cbe657394eeaac2f6ea9b0f6fcf6?tpId=13&tqId=11157&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) + +## 题目描述 + +根据二叉树的前序遍历和中序遍历的结果,重建出该二叉树。假设输入的前序遍历和中序遍历的结果中都不含重复的数字。 + +```html +preorder = [3,9,20,15,7] +inorder = [9,3,15,20,7] +``` + +

+ +## 解题思路 + +前序遍历的第一个值为根节点的值,使用这个值将中序遍历结果分成两部分,左部分为树的左子树中序遍历结果,右部分为树的右子树中序遍历的结果。 + +

+ +```java +// 缓存中序遍历数组每个值对应的索引 +private Map 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; +} +``` + +# 8. 二叉树的下一个结点 + +[NowCoder](https://www.nowcoder.com/practice/9023a0c988684a53960365b889ceaf5e?tpId=13&tqId=11210&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) + +## 题目描述 + +给定一个二叉树和其中的一个结点,请找出中序遍历顺序的下一个结点并且返回。注意,树中的结点不仅包含左右子结点,同时包含指向父结点的指针。 + +```java +public class TreeLinkNode { + + int val; + TreeLinkNode left = null; + TreeLinkNode right = null; + TreeLinkNode next = null; + + TreeLinkNode(int val) { + this.val = val; + } +} +``` + +## 解题思路 + +① 如果一个节点的右子树不为空,那么该节点的下一个节点是右子树的最左节点; + +

+ +② 否则,向上找第一个左链接指向的树包含该节点的祖先节点。 + +

+ +```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; +} +``` + +# 9. 用两个栈实现队列 + +[NowCoder](https://www.nowcoder.com/practice/54275ddae22f475981afa2244dd448c6?tpId=13&tqId=11158&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) + +## 题目描述 + +用两个栈来实现一个队列,完成队列的 Push 和 Pop 操作。 + +## 解题思路 + +in 栈用来处理入栈(push)操作,out 栈用来处理出栈(pop)操作。一个元素进入 in 栈之后,出栈的顺序被反转。当元素要出栈时,需要先进入 out 栈,此时元素出栈顺序再一次被反转,因此出栈顺序就和最开始入栈顺序是相同的,先进入的元素先退出,这就是队列的顺序。 + +

+ + +```java +Stack in = new Stack(); +Stack out = new Stack(); + +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(); +} +``` + diff --git a/docs/notes/剑指 Offer 题解 - 40~49.md b/docs/notes/剑指 Offer 题解 - 40~49.md new file mode 100644 index 00000000..d4edc7ae --- /dev/null +++ b/docs/notes/剑指 Offer 题解 - 40~49.md @@ -0,0 +1,440 @@ + +* [40. 最小的 K 个数](#40-最小的-k-个数) + * [解题思路](#解题思路) + * [快速选择](#快速选择) + * [大小为 K 的最小堆](#大小为-k-的最小堆) +* [41.1 数据流中的中位数](#411-数据流中的中位数) + * [题目描述](#题目描述) + * [解题思路](#解题思路) +* [41.2 字符流中第一个不重复的字符](#412-字符流中第一个不重复的字符) + * [题目描述](#题目描述) + * [解题思路](#解题思路) +* [42. 连续子数组的最大和](#42-连续子数组的最大和) + * [题目描述](#题目描述) + * [解题思路](#解题思路) +* [43. 从 1 到 n 整数中 1 出现的次数](#43-从-1-到-n-整数中-1-出现的次数) + * [解题思路](#解题思路) +* [44. 数字序列中的某一位数字](#44-数字序列中的某一位数字) + * [题目描述](#题目描述) + * [解题思路](#解题思路) +* [45. 把数组排成最小的数](#45-把数组排成最小的数) + * [题目描述](#题目描述) + * [解题思路](#解题思路) +* [46. 把数字翻译成字符串](#46-把数字翻译成字符串) + * [题目描述](#题目描述) + * [解题思路](#解题思路) +* [47. 礼物的最大价值](#47-礼物的最大价值) + * [题目描述](#题目描述) + * [解题思路](#解题思路) +* [48. 最长不含重复字符的子字符串](#48-最长不含重复字符的子字符串) + * [题目描述](#题目描述) + * [解题思路](#解题思路) +* [49. 丑数](#49-丑数) + * [题目描述](#题目描述) + * [解题思路](#解题思路) + + + +# 40. 最小的 K 个数 + +[NowCoder](https://www.nowcoder.com/practice/6a296eb82cf844ca8539b57c23e6e9bf?tpId=13&tqId=11182&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) + +## 解题思路 + +### 快速选择 + +- 复杂度: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 GetLeastNumbers_Solution(int[] nums, int k) { + ArrayList ret = new ArrayList<>(); + if (k > nums.length || k <= 0) + return ret; + findKthSmallest(nums, k - 1); + /* findKthSmallest 会改变数组,使得前 k 个数都是最小的 k 个数 */ + for (int i = 0; i < k; i++) + ret.add(nums[i]); + return ret; +} + +public void findKthSmallest(int[] nums, int k) { + int l = 0, h = nums.length - 1; + while (l < h) { + int j = partition(nums, l, h); + if (j == k) + break; + if (j > k) + h = j - 1; + else + l = j + 1; + } +} + +private int partition(int[] nums, int l, int h) { + int p = nums[l]; /* 切分元素 */ + int i = l, j = h + 1; + while (true) { + while (i != h && nums[++i] < p) ; + while (j != l && nums[--j] > p) ; + if (i >= j) + break; + swap(nums, i, j); + } + swap(nums, l, j); + return j; +} + +private void swap(int[] nums, int i, int j) { + int t = nums[i]; + nums[i] = nums[j]; + nums[j] = t; +} +``` + +### 大小为 K 的最小堆 + +- 复杂度:O(NlogK) + O(K) +- 特别适合处理海量数据 + +应该使用大顶堆来维护最小堆,而不能直接创建一个小顶堆并设置一个大小,企图让小顶堆中的元素都是最小元素。 + +维护一个大小为 K 的最小堆过程如下:在添加一个元素之后,如果大顶堆的大小大于 K,那么需要将大顶堆的堆顶元素去除。 + +```java +public ArrayList GetLeastNumbers_Solution(int[] nums, int k) { + if (k > nums.length || k <= 0) + return new ArrayList<>(); + PriorityQueue maxHeap = new PriorityQueue<>((o1, o2) -> o2 - o1); + for (int num : nums) { + maxHeap.add(num); + if (maxHeap.size() > k) + maxHeap.poll(); + } + return new ArrayList<>(maxHeap); +} +``` + +# 41.1 数据流中的中位数 + +[NowCoder](https://www.nowcoder.com/practice/9be0172896bd43948f8a32fb954e1be1?tpId=13&tqId=11216&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) + +## 题目描述 + +如何得到一个数据流中的中位数?如果从数据流中读出奇数个数值,那么中位数就是所有数值排序之后位于中间的数值。如果从数据流中读出偶数个数值,那么中位数就是所有数值排序之后中间两个数的平均值。 + +## 解题思路 + +```java +/* 大顶堆,存储左半边元素 */ +private PriorityQueue left = new PriorityQueue<>((o1, o2) -> o2 - o1); +/* 小顶堆,存储右半边元素,并且右半边元素都大于左半边 */ +private PriorityQueue 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(); +} +``` + +# 41.2 字符流中第一个不重复的字符 + +[NowCoder](https://www.nowcoder.com/practice/00de97733b8e4f97a3fb5c680ee10720?tpId=13&tqId=11207&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) + +## 题目描述 + +请实现一个函数用来找出字符流中第一个只出现一次的字符。例如,当从字符流中只读出前两个字符 "go" 时,第一个只出现一次的字符是 "g"。当从该字符流中读出前六个字符“google" 时,第一个只出现一次的字符是 "l"。 + +## 解题思路 + +```java +private int[] cnts = new int[256]; +private Queue 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(); +} +``` + +# 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) + +## 题目描述 + +{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; +} +``` + +# 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) + +## 解题思路 + +```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) + +# 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'; +} +``` + +# 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) + +## 题目描述 + +输入一个正整数数组,把数组里所有数字拼接起来排成一个数,打印能拼接出的所有数字中最小的一个。例如输入数组 {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; +} +``` + +# 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]; +} +``` + +# 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]; +} +``` + +# 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; +} +``` + +# 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) + +## 题目描述 + +把只包含因子 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]; +} +``` diff --git a/docs/notes/剑指 Offer 题解 - 50~59.md b/docs/notes/剑指 Offer 题解 - 50~59.md new file mode 100644 index 00000000..0ef74752 --- /dev/null +++ b/docs/notes/剑指 Offer 题解 - 50~59.md @@ -0,0 +1,499 @@ + +* [50. 第一个只出现一次的字符位置](#50-第一个只出现一次的字符位置) + * [题目描述](#题目描述) + * [解题思路](#解题思路) +* [51. 数组中的逆序对](#51-数组中的逆序对) + * [题目描述](#题目描述) + * [解题思路](#解题思路) +* [52. 两个链表的第一个公共结点](#52-两个链表的第一个公共结点) + * [题目描述](#题目描述) + * [解题思路](#解题思路) +* [53. 数字在排序数组中出现的次数](#53-数字在排序数组中出现的次数) + * [题目描述](#题目描述) + * [解题思路](#解题思路) +* [54. 二叉查找树的第 K 个结点](#54-二叉查找树的第-k-个结点) + * [解题思路](#解题思路) +* [55.1 二叉树的深度](#551-二叉树的深度) + * [题目描述](#题目描述) + * [解题思路](#解题思路) +* [55.2 平衡二叉树](#552-平衡二叉树) + * [题目描述](#题目描述) + * [解题思路](#解题思路) +* [56. 数组中只出现一次的数字](#56-数组中只出现一次的数字) + * [题目描述](#题目描述) + * [解题思路](#解题思路) +* [57.1 和为 S 的两个数字](#571-和为-s-的两个数字) + * [题目描述](#题目描述) + * [解题思路](#解题思路) +* [57.2 和为 S 的连续正数序列](#572-和为-s-的连续正数序列) + * [题目描述](#题目描述) + * [解题思路](#解题思路) +* [58.1 翻转单词顺序列](#581-翻转单词顺序列) + * [题目描述](#题目描述) + * [解题思路](#解题思路) +* [58.2 左旋转字符串](#582-左旋转字符串) + * [题目描述](#题目描述) + * [解题思路](#解题思路) +* [59. 滑动窗口的最大值](#59-滑动窗口的最大值) + * [题目描述](#题目描述) + * [解题思路](#解题思路) + + + +# 50. 第一个只出现一次的字符位置 + +[NowCoder](https://www.nowcoder.com/practice/1c82e8cf713b4bbeb2a5b31cf5b0417c?tpId=13&tqId=11187&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) + +## 题目描述 + +在一个字符串中找到第一个只出现一次的字符,并返回它的位置。 + +## 解题思路 + +最直观的解法是使用 HashMap 对出现次数进行统计,但是考虑到要统计的字符范围有限,因此可以使用整型数组代替 HashMap。 + +```java +public int FirstNotRepeatingChar(String str) { + int[] cnts = new int[256]; + for (int i = 0; i < str.length(); i++) + cnts[str.charAt(i)]++; + for (int i = 0; i < str.length(); i++) + if (cnts[str.charAt(i)] == 1) + return i; + return -1; +} +``` + +以上实现的空间复杂度还不是最优的。考虑到只需要找到只出现一次的字符,那么需要统计的次数信息只有 0,1,更大,使用两个比特位就能存储这些信息。 + +```java +public int FirstNotRepeatingChar2(String str) { + BitSet bs1 = new BitSet(256); + BitSet bs2 = new BitSet(256); + for (char c : str.toCharArray()) { + if (!bs1.get(c) && !bs2.get(c)) + bs1.set(c); // 0 0 -> 0 1 + else if (bs1.get(c) && !bs2.get(c)) + bs2.set(c); // 0 1 -> 1 1 + } + for (int i = 0; i < str.length(); i++) { + char c = str.charAt(i); + if (bs1.get(c) && !bs2.get(c)) // 0 1 + return i; + } + return -1; +} +``` + +# 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) + +## 题目描述 + +在数组中的两个数字,如果前面一个数字大于后面的数字,则这两个数字组成一个逆序对。输入一个数组,求出这个数组中的逆序对的总数。 + +## 解题思路 + +```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]; +} +``` + +# 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) + +## 题目描述 + +

+ +## 解题思路 + +设 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; +} +``` + +# 53. 数字在排序数组中出现的次数 + +[NowCoder](https://www.nowcoder.com/practice/70610bf967994b22bb1c26f9ae901fa2?tpId=13&tqId=11190&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) + +## 题目描述 + +```html +Input: +nums = 1, 2, 3, 3, 3, 3, 4, 6 +K = 3 + +Output: +4 +``` + +## 解题思路 + +```java +public int GetNumberOfK(int[] nums, int K) { + int first = binarySearch(nums, K); + int last = binarySearch(nums, K + 1); + return (first == nums.length || nums[first] != K) ? 0 : last - first; +} + +private int binarySearch(int[] nums, int K) { + int l = 0, h = nums.length; + while (l < h) { + int m = l + (h - l) / 2; + if (nums[m] >= K) + h = m; + else + l = m + 1; + } + return l; +} +``` + +# 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) + +## 解题思路 + +利用二叉查找树中序遍历有序的特点。 + +```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); +} +``` + +# 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) + +## 题目描述 + +从根结点到叶结点依次经过的结点(含根、叶结点)形成树的一条路径,最长路径的长度为树的深度。 + +

+ +## 解题思路 + +```java +public int TreeDepth(TreeNode root) { + return root == null ? 0 : 1 + Math.max(TreeDepth(root.left), TreeDepth(root.right)); +} +``` + +# 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) + +## 题目描述 + +平衡二叉树左右子树高度差不超过 1。 + +

+ +## 解题思路 + +```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); +} +``` + +# 56. 数组中只出现一次的数字 + +[NowCoder](https://www.nowcoder.com/practice/e02fdb54d7524710a7d664d082bb7811?tpId=13&tqId=11193&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) + +## 题目描述 + +一个整型数组里除了两个数字之外,其他的数字都出现了两次,找出这两个数。 + +## 解题思路 + +两个不相等的元素在位级表示上必定会有一位存在不同,将数组的所有元素异或得到的结果为不存在重复的两个元素异或的结果。 + +diff &= -diff 得到出 diff 最右侧不为 0 的位,也就是不存在重复的两个元素在位级表示上最右侧不同的那一位,利用这一位就可以将两个元素区分开来。 + +```java +public void FindNumsAppearOnce(int[] nums, int num1[], int num2[]) { + int diff = 0; + for (int num : nums) + diff ^= num; + diff &= -diff; + for (int num : nums) { + if ((num & diff) == 0) + num1[0] ^= num; + else + num2[0] ^= num; + } +} +``` + +# 57.1 和为 S 的两个数字 + +[NowCoder](https://www.nowcoder.com/practice/390da4f7a00f44bea7c2f3d19491311b?tpId=13&tqId=11195&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) + +## 题目描述 + +输入一个递增排序的数组和一个数字 S,在数组中查找两个数,使得他们的和正好是 S。如果有多对数字的和等于 S,输出两个数的乘积最小的。 + +## 解题思路 + +使用双指针,一个指针指向元素较小的值,一个指针指向元素较大的值。指向较小元素的指针从头向尾遍历,指向较大元素的指针从尾向头遍历。 + +- 如果两个指针指向元素的和 sum == target,那么得到要求的结果; +- 如果 sum > target,移动较大的元素,使 sum 变小一些; +- 如果 sum < target,移动较小的元素,使 sum 变大一些。 + +```java +public ArrayList FindNumbersWithSum(int[] array, int sum) { + int i = 0, j = array.length - 1; + while (i < j) { + int cur = array[i] + array[j]; + if (cur == sum) + return new ArrayList<>(Arrays.asList(array[i], array[j])); + if (cur < sum) + i++; + else + j--; + } + return new ArrayList<>(); +} +``` + +# 57.2 和为 S 的连续正数序列 + +[NowCoder](https://www.nowcoder.com/practice/c451a3fd84b64cb19485dad758a55ebe?tpId=13&tqId=11194&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) + +## 题目描述 + +输出所有和为 S 的连续正数序列。 + +例如和为 100 的连续序列有: + +``` +[9, 10, 11, 12, 13, 14, 15, 16] +[18, 19, 20, 21, 22]。 +``` + +## 解题思路 + +```java +public ArrayList> FindContinuousSequence(int sum) { + ArrayList> 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 list = new ArrayList<>(); + for (int i = start; i <= end; i++) + list.add(i); + ret.add(list); + curSum -= start; + start++; + end++; + curSum += end; + } + } + return ret; +} +``` + +# 58.1 翻转单词顺序列 + +[NowCoder](https://www.nowcoder.com/practice/3194a4f4cf814f63919d0790578d51f3?tpId=13&tqId=11197&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) + +## 题目描述 + +```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; +} +``` + +# 58.2 左旋转字符串 + +[NowCoder](https://www.nowcoder.com/practice/12d959b108cb42b1ab72cef4d36af5ec?tpId=13&tqId=11196&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) + +## 题目描述 + +```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; +} +``` + +# 59. 滑动窗口的最大值 + +[NowCoder](https://www.nowcoder.com/practice/1624bc35a45c42c0bc17d17fa0cba788?tpId=13&tqId=11217&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) + +## 题目描述 + +给定一个数组和滑动窗口的大小,找出所有滑动窗口里数值的最大值。 + +例如,如果输入数组 {2, 3, 4, 2, 6, 2, 5, 1} 及滑动窗口的大小 3,那么一共存在 6 个滑动窗口,他们的最大值分别为 {4, 4, 6, 6, 6, 5}。 + +## 解题思路 + +```java +public ArrayList maxInWindows(int[] num, int size) { + ArrayList ret = new ArrayList<>(); + if (size > num.length || size < 1) + return ret; + PriorityQueue 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; +} +``` diff --git a/docs/notes/剑指 Offer 题解 - 60~68.md b/docs/notes/剑指 Offer 题解 - 60~68.md new file mode 100644 index 00000000..028dad51 --- /dev/null +++ b/docs/notes/剑指 Offer 题解 - 60~68.md @@ -0,0 +1,341 @@ + +* [60. n 个骰子的点数](#60-n-个骰子的点数) + * [题目描述](#题目描述) + * [解题思路](#解题思路) + * [动态规划解法](#动态规划解法) + * [动态规划解法 + 旋转数组](#动态规划解法--旋转数组) +* [61. 扑克牌顺子](#61-扑克牌顺子) + * [题目描述](#题目描述) + * [解题思路](#解题思路) +* [62. 圆圈中最后剩下的数](#62-圆圈中最后剩下的数) + * [题目描述](#题目描述) + * [解题思路](#解题思路) +* [63. 股票的最大利润](#63-股票的最大利润) + * [题目描述](#题目描述) + * [解题思路](#解题思路) +* [64. 求 1+2+3+...+n](#64-求-123n) + * [题目描述](#题目描述) + * [解题思路](#解题思路) +* [65. 不用加减乘除做加法](#65-不用加减乘除做加法) + * [题目描述](#题目描述) + * [解题思路](#解题思路) +* [66. 构建乘积数组](#66-构建乘积数组) + * [题目描述](#题目描述) + * [解题思路](#解题思路) +* [67. 把字符串转换成整数](#67-把字符串转换成整数) + * [题目描述](#题目描述) + * [解题思路](#解题思路) +* [68. 树中两个节点的最低公共祖先](#68-树中两个节点的最低公共祖先) + * [解题思路](#解题思路) + * [二叉查找树](#二叉查找树) + * [普通二叉树](#普通二叉树) + + + +# 60. n 个骰子的点数 + +[Lintcode](https://www.lintcode.com/en/problem/dices-sum/) + +## 题目描述 + +把 n 个骰子仍在地上,求点数和为 s 的概率。 + +

+ +## 解题思路 + +### 动态规划解法 + +使用一个二维数组 dp 存储点数出现的次数,其中 dp[i][j] 表示前 i 个骰子产生点数 j 的次数。 + +空间复杂度:O(N2) + +```java +public List> 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> 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> 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> ret = new ArrayList<>(); + for (int i = n; i <= pointNum; i++) + ret.add(new AbstractMap.SimpleEntry<>(i, dp[1 - flag][i] / totalNum)); + + return ret; +} +``` + +# 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) + +## 题目描述 + +五张牌,其中大小鬼为癞子,牌面大小为 0。判断这五张牌是否能组成顺子。 + +

+ +## 解题思路 + +```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; +} +``` + +# 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) + +## 题目描述 + +让小朋友们围成一个大圈。然后,随机指定一个数 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; +} +``` + +# 63. 股票的最大利润 + +[Leetcode](https://leetcode.com/problems/best-time-to-buy-and-sell-stock/description/) + +## 题目描述 + +可以有一次买入和一次卖出,那么买入必须在前。求最大收益。 + +

+ +## 解题思路 + +使用贪心策略,假设第 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; +} +``` + +# 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) + +## 题目描述 + +要求不能使用乘除法、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; +} +``` + +# 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) + +## 题目描述 + +写一个函数,求两个整数之和,要求不得使用 +、-、\*、/ 四则运算符号。 + +## 解题思路 + +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); +} +``` + +# 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) + +## 题目描述 + +给定一个数组 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]。要求不能使用除法。 + +

+ +## 解题思路 + +```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; +} +``` + +# 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) + +## 题目描述 + +将一个字符串转换成一个整数,字符串不是一个合法的数值则返回 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; +} +``` + +# 68. 树中两个节点的最低公共祖先 + +## 解题思路 + +### 二叉查找树 + +

+ +[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。 + +```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; +} +``` + +### 普通二叉树 + +

+ +[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 分别在两个子树中,那么就说明根节点就是最低公共祖先。 + +```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; +} +``` diff --git a/docs/notes/剑指 Offer 题解 - 目录.md b/docs/notes/剑指 Offer 题解 - 目录.md new file mode 100644 index 00000000..844cfeee --- /dev/null +++ b/docs/notes/剑指 Offer 题解 - 目录.md @@ -0,0 +1,15 @@ + + + + +部分绘图文件可以在这里免费下载:[剑指 Offer](https://www.processon.com/view/5a3e4c7be4b0909c1aa18b49),后续会慢慢把所有题目都配上 GIF 演示图。 + +- [0\~9](剑指%20Offer%20题解%20-%200\~9.md) +- [10\~19](剑指%20Offer%20题解%20-%2010\~19.md) +- [20\~29](剑指%20Offer%20题解%20-%2020\~29.md) +- [30\~39](剑指%20Offer%20题解%20-%2030\~39.md) +- [40\~49](剑指%20Offer%20题解%20-%2040\~49.md) +- [50\~59](剑指%20Offer%20题解%20-%2050\~59.md) +- [60\~68](剑指%20Offer%20题解%20-%2060\~68.md) + +参考文献:何海涛. 剑指 Offer[M]. 电子工业出版社, 2012. diff --git a/docs/notes/剑指 offer 题解.md b/docs/notes/剑指 offer 题解.md deleted file mode 100644 index 2710e20b..00000000 --- a/docs/notes/剑指 offer 题解.md +++ /dev/null @@ -1,3042 +0,0 @@ - -* [3. 数组中重复的数字](#3-数组中重复的数字) -* [4. 二维数组中的查找](#4-二维数组中的查找) -* [5. 替换空格](#5-替换空格) -* [6. 从尾到头打印链表](#6-从尾到头打印链表) -* [7. 重建二叉树](#7-重建二叉树) -* [8. 二叉树的下一个结点](#8-二叉树的下一个结点) -* [9. 用两个栈实现队列](#9-用两个栈实现队列) -* [10.1 斐波那契数列](#101-斐波那契数列) -* [10.2 矩形覆盖](#102-矩形覆盖) -* [10.3 跳台阶](#103-跳台阶) -* [10.4 变态跳台阶](#104-变态跳台阶) -* [11. 旋转数组的最小数字](#11-旋转数组的最小数字) -* [12. 矩阵中的路径](#12-矩阵中的路径) -* [13. 机器人的运动范围](#13-机器人的运动范围) -* [14. 剪绳子](#14-剪绳子) -* [15. 二进制中 1 的个数](#15-二进制中-1-的个数) -* [16. 数值的整数次方](#16-数值的整数次方) -* [17. 打印从 1 到最大的 n 位数](#17-打印从-1-到最大的-n-位数) -* [18.1 在 O(1) 时间内删除链表节点](#181-在-o1-时间内删除链表节点) -* [18.2 删除链表中重复的结点](#182-删除链表中重复的结点) -* [19. 正则表达式匹配](#19-正则表达式匹配) -* [20. 表示数值的字符串](#20-表示数值的字符串) -* [21. 调整数组顺序使奇数位于偶数前面](#21-调整数组顺序使奇数位于偶数前面) -* [22. 链表中倒数第 K 个结点](#22-链表中倒数第-k-个结点) -* [23. 链表中环的入口结点](#23-链表中环的入口结点) -* [24. 反转链表](#24-反转链表) -* [25. 合并两个排序的链表](#25-合并两个排序的链表) -* [26. 树的子结构](#26-树的子结构) -* [27. 二叉树的镜像](#27-二叉树的镜像) -* [28 对称的二叉树](#28-对称的二叉树) -* [29. 顺时针打印矩阵](#29-顺时针打印矩阵) -* [30. 包含 min 函数的栈](#30-包含-min-函数的栈) -* [31. 栈的压入、弹出序列](#31-栈的压入弹出序列) -* [32.1 从上往下打印二叉树](#321-从上往下打印二叉树) -* [32.2 把二叉树打印成多行](#322-把二叉树打印成多行) -* [32.3 按之字形顺序打印二叉树](#323-按之字形顺序打印二叉树) -* [33. 二叉搜索树的后序遍历序列](#33-二叉搜索树的后序遍历序列) -* [34. 二叉树中和为某一值的路径](#34-二叉树中和为某一值的路径) -* [35. 复杂链表的复制](#35-复杂链表的复制) -* [36. 二叉搜索树与双向链表](#36-二叉搜索树与双向链表) -* [37. 序列化二叉树](#37-序列化二叉树) -* [38. 字符串的排列](#38-字符串的排列) -* [39. 数组中出现次数超过一半的数字](#39-数组中出现次数超过一半的数字) -* [40. 最小的 K 个数](#40-最小的-k-个数) -* [41.1 数据流中的中位数](#411-数据流中的中位数) -* [41.2 字符流中第一个不重复的字符](#412-字符流中第一个不重复的字符) -* [42. 连续子数组的最大和](#42-连续子数组的最大和) -* [43. 从 1 到 n 整数中 1 出现的次数](#43-从-1-到-n-整数中-1-出现的次数) -* [44. 数字序列中的某一位数字](#44-数字序列中的某一位数字) -* [45. 把数组排成最小的数](#45-把数组排成最小的数) -* [46. 把数字翻译成字符串](#46-把数字翻译成字符串) -* [47. 礼物的最大价值](#47-礼物的最大价值) -* [48. 最长不含重复字符的子字符串](#48-最长不含重复字符的子字符串) -* [49. 丑数](#49-丑数) -* [50. 第一个只出现一次的字符位置](#50-第一个只出现一次的字符位置) -* [51. 数组中的逆序对](#51-数组中的逆序对) -* [52. 两个链表的第一个公共结点](#52-两个链表的第一个公共结点) -* [53. 数字在排序数组中出现的次数](#53-数字在排序数组中出现的次数) -* [54. 二叉查找树的第 K 个结点](#54-二叉查找树的第-k-个结点) -* [55.1 二叉树的深度](#551-二叉树的深度) -* [55.2 平衡二叉树](#552-平衡二叉树) -* [56. 数组中只出现一次的数字](#56-数组中只出现一次的数字) -* [57.1 和为 S 的两个数字](#571-和为-s-的两个数字) -* [57.2 和为 S 的连续正数序列](#572-和为-s-的连续正数序列) -* [58.1 翻转单词顺序列](#581-翻转单词顺序列) -* [58.2 左旋转字符串](#582-左旋转字符串) -* [59. 滑动窗口的最大值](#59-滑动窗口的最大值) -* [60. n 个骰子的点数](#60-n-个骰子的点数) -* [61. 扑克牌顺子](#61-扑克牌顺子) -* [62. 圆圈中最后剩下的数](#62-圆圈中最后剩下的数) -* [63. 股票的最大利润](#63-股票的最大利润) -* [64. 求 1+2+3+...+n](#64-求-123n) -* [65. 不用加减乘除做加法](#65-不用加减乘除做加法) -* [66. 构建乘积数组](#66-构建乘积数组) -* [67. 把字符串转换成整数](#67-把字符串转换成整数) -* [68. 树中两个节点的最低公共祖先](#68-树中两个节点的最低公共祖先) -* [参考文献](#参考文献) - - - -部分绘图文件可以在这里免费下载:[剑指 Offer](https://www.processon.com/view/5a3e4c7be4b0909c1aa18b49),后续会慢慢把所有题目都配上 GIF 演示图。 - -# 3. 数组中重复的数字 - -[NowCoder](https://www.nowcoder.com/practice/623a5ac0ea5b4e5f95552655361ae0a8?tpId=13&tqId=11203&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) - -## 题目描述 - -在一个长度为 n 的数组里的所有数字都在 0 到 n-1 的范围内。数组中某些数字是重复的,但不知道有几个数字是重复的,也不知道每个数字重复几次。请找出数组中任意一个重复的数字。 - -```html -Input: -{2, 3, 1, 0, 2, 5} - -Output: -2 -``` - -## 解题思路 - -要求是时间复杂度 O(N),空间复杂度 O(1)。因此不能使用排序的方法,也不能使用额外的标记数组。 - -对于这种数组元素在 [0, n-1] 范围内的问题,可以将值为 i 的元素调整到第 i 个位置上进行求解。 - -以 (2, 3, 1, 0, 2, 5) 为例,遍历到位置 4 时,该位置上的数为 2,但是第 2 个位置上已经有一个 2 的值了,因此可以知道 2 重复: - -

- -```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; -} -``` - -# 4. 二维数组中的查找 - -[NowCoder](https://www.nowcoder.com/practice/abc3fe2ce8e146608e868a70efebf62e?tpId=13&tqId=11154&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) - -## 题目描述 - -给定一个二维数组,其每一行从左到右递增排序,从上到下也是递增排序。给定一个数,判断这个数是否在该二维数组中。 - -```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)。 - -该二维数组中的一个数,它左边的数都比它小,下边的数都比它大。因此,从右上角开始查找,就可以根据 target 和当前元素的大小关系来缩小查找区间,当前元素的查找区间为左下角的所有元素。 - -

- -```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; -} -``` - -# 5. 替换空格 - -[NowCoder](https://www.nowcoder.com/practice/4060ac7e3e404ad1a894ef3e17650423?tpId=13&tqId=11155&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) - -## 题目描述 - - -将一个字符串中的空格替换成 "%20"。 - -```text -Input: -"A B" - -Output: -"A%20B" -``` - -## 解题思路 - -在字符串尾部填充任意字符,使得字符串的长度等于替换之后的长度。因为一个空格要替换成三个字符(%20),因此当遍历到一个空格时,需要在尾部填充两个任意字符。 - -令 P1 指向字符串原来的末尾位置,P2 指向字符串现在的末尾位置。P1 和 P2 从后向前遍历,当 P1 遍历到一个空格时,就需要令 P2 指向的位置依次填充 02%(注意是逆序的),否则就填充上 P1 指向字符的值。 - -从后向前遍是为了在改变 P2 所指向的内容时,不会影响到 P1 遍历原来字符串的内容。 - -

- -```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(); -} -``` - -# 6. 从尾到头打印链表 - -[NowCoder](https://www.nowcoder.com/practice/d0267f7f55b3412ba93bd35cfa8e8035?tpId=13&tqId=11156&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) - -## 题目描述 - -从尾到头反过来打印出每个结点的值。 - -

- -## 解题思路 - -### 使用递归 - -

- -```java -public ArrayList printListFromTailToHead(ListNode listNode) { - ArrayList ret = new ArrayList<>(); - if (listNode != null) { - ret.addAll(printListFromTailToHead(listNode.next)); - ret.add(listNode.val); - } - return ret; -} -``` - -### 使用头插法 - -利用链表头插法为逆序的特点。 - -头结点和第一个节点的区别: - -- 头结点是在头插法中使用的一个额外节点,这个节点不存储值; -- 第一个节点就是链表的第一个真正存储值的节点。 - -

- -```java -public ArrayList 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 ret = new ArrayList<>(); - head = head.next; - while (head != null) { - ret.add(head.val); - head = head.next; - } - return ret; -} -``` - -### 使用栈 - -

- -```java -public ArrayList printListFromTailToHead(ListNode listNode) { - Stack stack = new Stack<>(); - while (listNode != null) { - stack.add(listNode.val); - listNode = listNode.next; - } - ArrayList ret = new ArrayList<>(); - while (!stack.isEmpty()) - ret.add(stack.pop()); - return ret; -} -``` - -# 7. 重建二叉树 - -[NowCoder](https://www.nowcoder.com/practice/8a19cbe657394eeaac2f6ea9b0f6fcf6?tpId=13&tqId=11157&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) - -## 题目描述 - -根据二叉树的前序遍历和中序遍历的结果,重建出该二叉树。假设输入的前序遍历和中序遍历的结果中都不含重复的数字。 - -```html -preorder = [3,9,20,15,7] -inorder = [9,3,15,20,7] -``` - -

- -## 解题思路 - -前序遍历的第一个值为根节点的值,使用这个值将中序遍历结果分成两部分,左部分为树的左子树中序遍历结果,右部分为树的右子树中序遍历的结果。 - -

- -```java -// 缓存中序遍历数组每个值对应的索引 -private Map 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; -} -``` - -# 8. 二叉树的下一个结点 - -[NowCoder](https://www.nowcoder.com/practice/9023a0c988684a53960365b889ceaf5e?tpId=13&tqId=11210&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) - -## 题目描述 - -给定一个二叉树和其中的一个结点,请找出中序遍历顺序的下一个结点并且返回。注意,树中的结点不仅包含左右子结点,同时包含指向父结点的指针。 - -```java -public class TreeLinkNode { - - int val; - TreeLinkNode left = null; - TreeLinkNode right = null; - TreeLinkNode next = null; - - TreeLinkNode(int val) { - this.val = val; - } -} -``` - -## 解题思路 - -① 如果一个节点的右子树不为空,那么该节点的下一个节点是右子树的最左节点; - -

- -② 否则,向上找第一个左链接指向的树包含该节点的祖先节点。 - -

- -```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; -} -``` - -# 9. 用两个栈实现队列 - -[NowCoder](https://www.nowcoder.com/practice/54275ddae22f475981afa2244dd448c6?tpId=13&tqId=11158&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) - -## 题目描述 - -用两个栈来实现一个队列,完成队列的 Push 和 Pop 操作。 - -## 解题思路 - -in 栈用来处理入栈(push)操作,out 栈用来处理出栈(pop)操作。一个元素进入 in 栈之后,出栈的顺序被反转。当元素要出栈时,需要先进入 out 栈,此时元素出栈顺序再一次被反转,因此出栈顺序就和最开始入栈顺序是相同的,先进入的元素先退出,这就是队列的顺序。 - -

- - -```java -Stack in = new Stack(); -Stack out = new Stack(); - -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(); -} -``` - -# 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) - -## 题目描述 - -求斐波那契数列的第 n 项,n <= 39。 - - - -

- -## 解题思路 - -如果使用递归求解,会重复计算一些子问题。例如,计算 f(10) 需要计算 f(9) 和 f(8),计算 f(9) 需要计算 f(8) 和 f(7),可以看到 f(8) 被重复计算了。 - -

- - -递归是将一个问题划分成多个子问题求解,动态规划也是如此,但是动态规划会把子问题的解缓存起来,从而避免重复求解子问题。 - -```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; - fib[2] = 2; - for (int i = 2; i < fib.length; i++) - fib[i] = fib[i - 1] + fib[i - 2]; - } - - public int Fibonacci(int n) { - return fib[n]; - } -} -``` - -# 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) - -## 题目描述 - -我们可以用 2\*1 的小矩形横着或者竖着去覆盖更大的矩形。请问用 n 个 2\*1 的小矩形无重叠地覆盖一个 2\*n 的大矩形,总共有多少种方法? - -

- -## 解题思路 - -```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; -} -``` - -# 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) - -## 题目描述 - -一只青蛙一次可以跳上 1 级台阶,也可以跳上 2 级。求该青蛙跳上一个 n 级的台阶总共有多少种跳法。 - -

- -## 解题思路 - -```java -public int JumpFloor(int n) { - if (n <= 2) - return n; - int pre2 = 1, pre1 = 2; - int result = 1; - for (int i = 2; i < n; i++) { - result = pre2 + pre1; - pre2 = pre1; - pre1 = result; - } - return result; -} -``` - -# 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) - -## 题目描述 - -一只青蛙一次可以跳上 1 级台阶,也可以跳上 2 级... 它也可以跳上 n 级。求该青蛙跳上一个 n 级的台阶总共有多少种跳法。 - -

- -## 解题思路 - -### 动态规划 - -```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); -} -``` - - -# 11. 旋转数组的最小数字 - -[NowCoder](https://www.nowcoder.com/practice/9f3231a991af4f55b95579b44b7a01ba?tpId=13&tqId=11159&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) - -## 题目描述 - -把一个数组最开始的若干个元素搬到数组的末尾,我们称之为数组的旋转。输入一个非递减排序的数组的一个旋转,输出旋转数组的最小元素。 - -例如数组 {3, 4, 5, 1, 2} 为 {1, 2, 3, 4, 5} 的一个旋转,该数组的最小值为 1。 - -## 解题思路 - -在一个有序数组中查找一个元素可以用二分查找,二分查找也称为折半查找,每次都能将查找区间减半,这种折半特性的算法时间复杂度都为 O(logN)。 - -本题可以修改二分查找算法进行求解: - -- 当 nums[m] <= nums[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]; -} -``` - -# 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) - -## 题目描述 - -请设计一个函数,用来判断在一个矩阵中是否存在一条包含某字符串所有字符的路径。路径可以从矩阵中的任意一个格子开始,每一步可以在矩阵中向左,向右,向上,向下移动一个格子。如果一条路径经过了矩阵中的某一个格子,则该路径不能再进入该格子。 - -例如下面的矩阵包含了一条 bfce 路径。 - -

- -## 解题思路 - -```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 i = 0, idx = 0; i < rows; i++) - for (int j = 0; j < cols; j++) - matrix[i][j] = array[idx++]; - return matrix; -} -``` - -# 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) - -## 题目描述 - -地上有一个 m 行和 n 列的方格。一个机器人从坐标 (0, 0) 的格子开始移动,每一次只能向左右上下四个方向移动一格,但是不能进入行坐标和列坐标的数位之和大于 k 的格子。 - -例如,当 k 为 18 时,机器人能够进入方格 (35,37),因为 3+5+3+7=18。但是,它不能进入方格 (35,38),因为 3+5+3+8=19。请问该机器人能够达到多少个格子? - -## 解题思路 - -```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]; -} -``` - -# 14. 剪绳子 - -[Leetcode](https://leetcode.com/problems/integer-break/description/) - -## 题目描述 - -把一根绳子剪成多段,并且使得每段的长度乘积最大。 - -```html -n = 2 -return 1 (2 = 1 + 1) - -n = 10 -return 36 (10 = 3 + 3 + 4) -``` - -## 解题思路 - -### 贪心 - -尽可能多剪长度为 3 的绳子,并且不允许有长度为 1 的绳子出现。如果出现了,就从已经切好长度为 3 的绳子中拿出一段与长度为 1 的绳子重新组合,把它们切成两段长度为 2 的绳子。 - -证明:当 n >= 5 时,3(n - 3) - n = 2n - 9 > 0,且 2(n - 2) - n = n - 4 > 0。因此在 n >= 5 的情况下,将绳子剪成一段为 2 或者 3,得到的乘积会更大。又因为 3(n - 3) - 2(n - 2) = n - 5 >= 0,所以剪成一段长度为 3 比长度为 2 得到的乘积更大。 - -```java -public int integerBreak(int n) { - if (n < 2) - return 0; - if (n == 2) - return 1; - if (n == 3) - return 2; - int timesOf3 = n / 3; - if (n - timesOf3 * 3 == 1) - timesOf3--; - int timesOf2 = (n - timesOf3 * 3) / 2; - return (int) (Math.pow(3, timesOf3)) * (int) (Math.pow(2, timesOf2)); -} -``` - -### 动态规划 - -```java -public int integerBreak(int n) { - int[] dp = new int[n + 1]; - dp[1] = 1; - for (int i = 2; i <= n; i++) - for (int j = 1; j < i; j++) - dp[i] = Math.max(dp[i], Math.max(j * (i - j), dp[j] * (i - j))); - return dp[n]; -} -``` - -# 15. 二进制中 1 的个数 - -[NowCoder](https://www.nowcoder.com/practice/8ee967e43c2c4ec193b040ea7fbb10b8?tpId=13&tqId=11164&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) - -## 题目描述 - -输入一个整数,输出该数二进制表示中 1 的个数。 - -### n&(n-1) - -该位运算去除 n 的位级表示中最低的那一位。 - -``` -n : 10110100 -n-1 : 10110011 -n&(n-1) : 10110000 -``` - -时间复杂度:O(M),其中 M 表示 1 的个数。 - - -```java -public int NumberOf1(int n) { - int cnt = 0; - while (n != 0) { - cnt++; - n &= (n - 1); - } - return cnt; -} -``` - - -### Integer.bitCount() - -```java -public int NumberOf1(int n) { - return Integer.bitCount(n); -} -``` - -# 16. 数值的整数次方 - -[NowCoder](https://www.nowcoder.com/practice/1a834e5e3e1a4b7ba251417554e07c00?tpId=13&tqId=11165&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) - -## 题目描述 - -给定一个 double 类型的浮点数 base 和 int 类型的整数 exponent,求 base 的 exponent 次方。 - -## 解题思路 - -下面的讨论中 x 代表 base,n 代表 exponent。 - - - -

- -因为 (x\*x)n/2 可以通过递归求解,并且每次递归 n 都减小一半,因此整个算法的时间复杂度为 O(logN)。 - -```java -public double Power(double base, int exponent) { - if (exponent == 0) - return 1; - if (exponent == 1) - return base; - boolean isNegative = false; - if (exponent < 0) { - exponent = -exponent; - isNegative = true; - } - double pow = Power(base * base, exponent / 2); - if (exponent % 2 != 0) - pow = pow * base; - return isNegative ? 1 / pow : pow; -} -``` - -# 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(); -} -``` - -# 18.1 在 O(1) 时间内删除链表节点 - -## 解题思路 - -① 如果该节点不是尾节点,那么可以直接将下一个节点的值赋给该节点,然后令该节点指向下下个节点,再删除下一个节点,时间复杂度为 O(1)。 - -

- -② 如果链表只有一个节点,那么直接 - -② 否则,就需要先遍历链表,找到节点的前一个节点,然后让前一个节点指向 null,时间复杂度为 O(N)。 - -

- -综上,如果进行 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; -} -``` - -# 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) - -## 题目描述 - -

- -## 解题描述 - -```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; - } -} -``` - -# 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) - -## 题目描述 - -请实现一个函数用来匹配包括 '.' 和 '\*' 的正则表达式。模式中的字符 '.' 表示任意一个字符,而 '\*' 表示它前面的字符可以出现任意次(包含 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]; -} -``` - -# 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) - -## 题目描述 - -```html -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+)?"); -} -``` - -# 21. 调整数组顺序使奇数位于偶数前面 - -[NowCoder](https://www.nowcoder.com/practice/beb5aa231adc45b2a5dcc5b62c93f593?tpId=13&tqId=11166&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) - -## 题目描述 - -需要保证奇数和奇数,偶数和偶数之间的相对位置不变,这和书本不太一样。 - -

- -## 解题思路 - -```java -public void reOrderArray(int[] nums) { - // 奇数个数 - int oddCnt = 0; - for (int val : nums) - if (val % 2 == 1) - 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; - } -} -``` - -# 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) - -## 解题思路 - -设链表的长度为 N。设两个指针 P1 和 P2,先让 P1 移动 K 个节点,则还有 N - K 个节点可以移动。此时让 P1 和 P2 同时移动,可以知道当 P1 移动到链表结尾时,P2 移动到 N - K 个节点处,该位置就是倒数第 K 个节点。 - -

- -```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; -} -``` - -# 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) - -## 题目描述 - -一个链表中包含环,请找出该链表的环的入口结点。要求不能使用额外的空间。 - -## 解题思路 - -使用双指针,一个指针 fast 每次移动两个节点,一个指针 slow 每次移动一个节点。因为存在环,所以两个指针必定相遇在环中的某个节点上。假设相遇点在下图的 z1 位置,此时 fast 移动的节点数为 x+2y+z,slow 为 x+y,由于 fast 速度比 slow 快一倍,因此 x+2y+z=2(x+y),得到 x=z。 - -在相遇点,slow 要到环的入口点还需要移动 z 个节点,如果让 fast 重新从头开始移动,并且速度变为每次移动一个节点,那么它到环入口点还需要移动 x 个节点。在上面已经推导出 x=z,因此 fast 和 slow 将在环入口点相遇。 - -

- - -```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; -} -``` - -# 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) - -## 解题思路 - -### 递归 - -```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; -} -``` - -# 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) - -## 题目描述 - -

- -## 解题思路 - -### 递归 - -```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; -} -``` - -# 26. 树的子结构 - -[NowCoder](https://www.nowcoder.com/practice/6e196c44c7004d15b1610b9afca8bd88?tpId=13&tqId=11170&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) - -## 题目描述 - -

- -## 解题思路 - -```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); -} -``` - -# 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) - -## 题目描述 - -

- -## 解题思路 - -```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; -} -``` - -# 28 对称的二叉树 - -[NowCder](https://www.nowcoder.com/practice/ff05d44dfdb04e1d83bdbdab320efbcb?tpId=13&tqId=11211&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) - -## 题目描述 - -

- -## 解题思路 - -```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); -} -``` - -# 29. 顺时针打印矩阵 - -[NowCoder](https://www.nowcoder.com/practice/9b4c81a02cd34f76be2659fa0d54342a?tpId=13&tqId=11172&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) - -## 题目描述 - -下图的矩阵顺时针打印结果为:1, 2, 3, 4, 8, 12, 16, 15, 14, 13, 9, 5, 6, 7, 11, 10 - -

- -## 解题思路 - -```java -public ArrayList printMatrix(int[][] matrix) { - ArrayList 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; -} -``` - -# 30. 包含 min 函数的栈 - -[NowCoder](https://www.nowcoder.com/practice/4c776177d2c04c2494f2555c9fcc1e49?tpId=13&tqId=11173&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) - -## 题目描述 - -定义栈的数据结构,请在该类型中实现一个能够得到栈最小元素的 min 函数。 - -## 解题思路 - -```java -private Stack dataStack = new Stack<>(); -private Stack 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(); -} -``` - -# 31. 栈的压入、弹出序列 - -[NowCoder](https://www.nowcoder.com/practice/d77d11405cc7470d82554cb392585106?tpId=13&tqId=11174&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) - -## 题目描述 - -输入两个整数序列,第一个序列表示栈的压入顺序,请判断第二个序列是否为该栈的弹出顺序。假设压入栈的所有数字均不相等。 - -例如序列 1,2,3,4,5 是某栈的压入顺序,序列 4,5,3,2,1 是该压栈序列对应的一个弹出序列,但 4,3,5,1,2 就不可能是该压栈序列的弹出序列。 - -## 解题思路 - -使用一个栈来模拟压入弹出操作。 - -```java -public boolean IsPopOrder(int[] pushSequence, int[] popSequence) { - int n = pushSequence.length; - Stack 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(); -} -``` - -# 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) - -## 题目描述 - -从上往下打印出二叉树的每个节点,同层节点从左至右打印。 - -例如,以下二叉树层次遍历的结果为:1,2,3,4,5,6,7 - -

- -## 解题思路 - -使用队列来进行层次遍历。 - -不需要使用两个队列分别存储当前层的节点和下一层的节点,因为在开始遍历一层的节点时,当前队列中的节点数就是当前层的节点数,只要控制遍历这么多节点数,就能保证这次遍历的都是当前层的节点。 - -```java -public ArrayList PrintFromTopToBottom(TreeNode root) { - Queue queue = new LinkedList<>(); - ArrayList 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; -} -``` - -# 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) - -## 题目描述 - -和上题几乎一样。 - -## 解题思路 - -```java -ArrayList> Print(TreeNode pRoot) { - ArrayList> ret = new ArrayList<>(); - Queue queue = new LinkedList<>(); - queue.add(pRoot); - while (!queue.isEmpty()) { - ArrayList 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; -} -``` - -# 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) - -## 题目描述 - -请实现一个函数按照之字形打印二叉树,即第一行按照从左到右的顺序打印,第二层按照从右至左的顺序打印,第三行按照从左到右的顺序打印,其他行以此类推。 - -## 解题思路 - -```java -public ArrayList> Print(TreeNode pRoot) { - ArrayList> ret = new ArrayList<>(); - Queue queue = new LinkedList<>(); - queue.add(pRoot); - boolean reverse = false; - while (!queue.isEmpty()) { - ArrayList 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; -} -``` - -# 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) - -## 题目描述 - -输入一个整数数组,判断该数组是不是某二叉搜索树的后序遍历的结果。假设输入的数组的任意两个数字都互不相同。 - -例如,下图是后序遍历序列 1,3,2 所对应的二叉搜索树。 - -

- -## 解题思路 - -```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); -} -``` - -# 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) - -## 题目描述 - -输入一颗二叉树和一个整数,打印出二叉树中结点值的和为输入整数的所有路径。路径定义为从树的根结点开始往下一直到叶结点所经过的结点形成一条路径。 - -下图的二叉树有两条和为 22 的路径:10, 5, 7 和 10, 12 - -

- -## 解题思路 - -```java -private ArrayList> ret = new ArrayList<>(); - -public ArrayList> FindPath(TreeNode root, int target) { - backtracking(root, target, new ArrayList<>()); - return ret; -} - -private void backtracking(TreeNode node, int target, ArrayList 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); -} -``` - -# 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) - -## 题目描述 - -输入一个复杂链表(每个节点中有节点值,以及两个指针,一个指向下一个节点,另一个特殊指针指向任意一个节点),返回结果为复制后复杂链表的 head。 - -```java -public class RandomListNode { - int label; - RandomListNode next = null; - RandomListNode random = null; - - RandomListNode(int label) { - this.label = label; - } -} -``` - -

- -## 解题思路 - -第一步,在每个节点的后面插入复制的节点。 - -

- -第二步,对复制节点的 random 链接进行赋值。 - -

- -第三步,拆分。 - -

- -```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; -} -``` - -# 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) - -## 题目描述 - -输入一棵二叉搜索树,将该二叉搜索树转换成一个排序的双向链表。要求不能创建任何新的结点,只能调整树中结点指针的指向。 - -

- -## 解题思路 - -```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); -} -``` - -# 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) - -## 题目描述 - -请实现两个函数,分别用来序列化和反序列化二叉树。 - -## 解题思路 - -```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; -} -``` - -# 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) - -## 题目描述 - -输入一个字符串,按字典序打印出该字符串中字符的所有排列。例如输入字符串 abc,则打印出由字符 a, b, c 所能排列出来的所有字符串 abc, acb, bac, bca, cab 和 cba。 - -## 解题思路 - -```java -private ArrayList ret = new ArrayList<>(); - -public ArrayList 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; - } -} -``` - -# 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) - -## 解题思路 - -多数投票问题,可以利用 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; -} -``` - -# 40. 最小的 K 个数 - -[NowCoder](https://www.nowcoder.com/practice/6a296eb82cf844ca8539b57c23e6e9bf?tpId=13&tqId=11182&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) - -## 解题思路 - -### 快速选择 - -- 复杂度: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 GetLeastNumbers_Solution(int[] nums, int k) { - ArrayList ret = new ArrayList<>(); - if (k > nums.length || k <= 0) - return ret; - findKthSmallest(nums, k - 1); - /* findKthSmallest 会改变数组,使得前 k 个数都是最小的 k 个数 */ - for (int i = 0; i < k; i++) - ret.add(nums[i]); - return ret; -} - -public void findKthSmallest(int[] nums, int k) { - int l = 0, h = nums.length - 1; - while (l < h) { - int j = partition(nums, l, h); - if (j == k) - break; - if (j > k) - h = j - 1; - else - l = j + 1; - } -} - -private int partition(int[] nums, int l, int h) { - int p = nums[l]; /* 切分元素 */ - int i = l, j = h + 1; - while (true) { - while (i != h && nums[++i] < p) ; - while (j != l && nums[--j] > p) ; - if (i >= j) - break; - swap(nums, i, j); - } - swap(nums, l, j); - return j; -} - -private void swap(int[] nums, int i, int j) { - int t = nums[i]; - nums[i] = nums[j]; - nums[j] = t; -} -``` - -### 大小为 K 的最小堆 - -- 复杂度:O(NlogK) + O(K) -- 特别适合处理海量数据 - -应该使用大顶堆来维护最小堆,而不能直接创建一个小顶堆并设置一个大小,企图让小顶堆中的元素都是最小元素。 - -维护一个大小为 K 的最小堆过程如下:在添加一个元素之后,如果大顶堆的大小大于 K,那么需要将大顶堆的堆顶元素去除。 - -```java -public ArrayList GetLeastNumbers_Solution(int[] nums, int k) { - if (k > nums.length || k <= 0) - return new ArrayList<>(); - PriorityQueue maxHeap = new PriorityQueue<>((o1, o2) -> o2 - o1); - for (int num : nums) { - maxHeap.add(num); - if (maxHeap.size() > k) - maxHeap.poll(); - } - return new ArrayList<>(maxHeap); -} -``` - -# 41.1 数据流中的中位数 - -[NowCoder](https://www.nowcoder.com/practice/9be0172896bd43948f8a32fb954e1be1?tpId=13&tqId=11216&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) - -## 题目描述 - -如何得到一个数据流中的中位数?如果从数据流中读出奇数个数值,那么中位数就是所有数值排序之后位于中间的数值。如果从数据流中读出偶数个数值,那么中位数就是所有数值排序之后中间两个数的平均值。 - -## 解题思路 - -```java -/* 大顶堆,存储左半边元素 */ -private PriorityQueue left = new PriorityQueue<>((o1, o2) -> o2 - o1); -/* 小顶堆,存储右半边元素,并且右半边元素都大于左半边 */ -private PriorityQueue 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(); -} -``` - -# 41.2 字符流中第一个不重复的字符 - -[NowCoder](https://www.nowcoder.com/practice/00de97733b8e4f97a3fb5c680ee10720?tpId=13&tqId=11207&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) - -## 题目描述 - -请实现一个函数用来找出字符流中第一个只出现一次的字符。例如,当从字符流中只读出前两个字符 "go" 时,第一个只出现一次的字符是 "g"。当从该字符流中读出前六个字符“google" 时,第一个只出现一次的字符是 "l"。 - -## 解题思路 - -```java -private int[] cnts = new int[256]; -private Queue 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(); -} -``` - -# 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) - -## 题目描述 - -{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; -} -``` - -# 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) - -## 解题思路 - -```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) - -# 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'; -} -``` - -# 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) - -## 题目描述 - -输入一个正整数数组,把数组里所有数字拼接起来排成一个数,打印能拼接出的所有数字中最小的一个。例如输入数组 {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; -} -``` - -# 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]; -} -``` - -# 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]; -} -``` - -# 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; -} -``` - -# 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) - -## 题目描述 - -把只包含因子 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]; -} -``` - -# 50. 第一个只出现一次的字符位置 - -[NowCoder](https://www.nowcoder.com/practice/1c82e8cf713b4bbeb2a5b31cf5b0417c?tpId=13&tqId=11187&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) - -## 题目描述 - -在一个字符串中找到第一个只出现一次的字符,并返回它的位置。 - -## 解题思路 - -最直观的解法是使用 HashMap 对出现次数进行统计,但是考虑到要统计的字符范围有限,因此可以使用整型数组代替 HashMap。 - -```java -public int FirstNotRepeatingChar(String str) { - int[] cnts = new int[256]; - for (int i = 0; i < str.length(); i++) - cnts[str.charAt(i)]++; - for (int i = 0; i < str.length(); i++) - if (cnts[str.charAt(i)] == 1) - return i; - return -1; -} -``` - -以上实现的空间复杂度还不是最优的。考虑到只需要找到只出现一次的字符,那么需要统计的次数信息只有 0,1,更大,使用两个比特位就能存储这些信息。 - -```java -public int FirstNotRepeatingChar2(String str) { - BitSet bs1 = new BitSet(256); - BitSet bs2 = new BitSet(256); - for (char c : str.toCharArray()) { - if (!bs1.get(c) && !bs2.get(c)) - bs1.set(c); // 0 0 -> 0 1 - else if (bs1.get(c) && !bs2.get(c)) - bs2.set(c); // 0 1 -> 1 1 - } - for (int i = 0; i < str.length(); i++) { - char c = str.charAt(i); - if (bs1.get(c) && !bs2.get(c)) // 0 1 - return i; - } - return -1; -} -``` - -# 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) - -## 题目描述 - -在数组中的两个数字,如果前面一个数字大于后面的数字,则这两个数字组成一个逆序对。输入一个数组,求出这个数组中的逆序对的总数。 - -## 解题思路 - -```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]; -} -``` - -# 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) - -## 题目描述 - -

- -## 解题思路 - -设 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; -} -``` - -# 53. 数字在排序数组中出现的次数 - -[NowCoder](https://www.nowcoder.com/practice/70610bf967994b22bb1c26f9ae901fa2?tpId=13&tqId=11190&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) - -## 题目描述 - -```html -Input: -nums = 1, 2, 3, 3, 3, 3, 4, 6 -K = 3 - -Output: -4 -``` - -## 解题思路 - -```java -public int GetNumberOfK(int[] nums, int K) { - int first = binarySearch(nums, K); - int last = binarySearch(nums, K + 1); - return (first == nums.length || nums[first] != K) ? 0 : last - first; -} - -private int binarySearch(int[] nums, int K) { - int l = 0, h = nums.length; - while (l < h) { - int m = l + (h - l) / 2; - if (nums[m] >= K) - h = m; - else - l = m + 1; - } - return l; -} -``` - -# 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) - -## 解题思路 - -利用二叉查找树中序遍历有序的特点。 - -```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); -} -``` - -# 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) - -## 题目描述 - -从根结点到叶结点依次经过的结点(含根、叶结点)形成树的一条路径,最长路径的长度为树的深度。 - -

- -## 解题思路 - -```java -public int TreeDepth(TreeNode root) { - return root == null ? 0 : 1 + Math.max(TreeDepth(root.left), TreeDepth(root.right)); -} -``` - -# 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) - -## 题目描述 - -平衡二叉树左右子树高度差不超过 1。 - -

- -## 解题思路 - -```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); -} -``` - -# 56. 数组中只出现一次的数字 - -[NowCoder](https://www.nowcoder.com/practice/e02fdb54d7524710a7d664d082bb7811?tpId=13&tqId=11193&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) - -## 题目描述 - -一个整型数组里除了两个数字之外,其他的数字都出现了两次,找出这两个数。 - -## 解题思路 - -两个不相等的元素在位级表示上必定会有一位存在不同,将数组的所有元素异或得到的结果为不存在重复的两个元素异或的结果。 - -diff &= -diff 得到出 diff 最右侧不为 0 的位,也就是不存在重复的两个元素在位级表示上最右侧不同的那一位,利用这一位就可以将两个元素区分开来。 - -```java -public void FindNumsAppearOnce(int[] nums, int num1[], int num2[]) { - int diff = 0; - for (int num : nums) - diff ^= num; - diff &= -diff; - for (int num : nums) { - if ((num & diff) == 0) - num1[0] ^= num; - else - num2[0] ^= num; - } -} -``` - -# 57.1 和为 S 的两个数字 - -[NowCoder](https://www.nowcoder.com/practice/390da4f7a00f44bea7c2f3d19491311b?tpId=13&tqId=11195&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) - -## 题目描述 - -输入一个递增排序的数组和一个数字 S,在数组中查找两个数,使得他们的和正好是 S。如果有多对数字的和等于 S,输出两个数的乘积最小的。 - -## 解题思路 - -使用双指针,一个指针指向元素较小的值,一个指针指向元素较大的值。指向较小元素的指针从头向尾遍历,指向较大元素的指针从尾向头遍历。 - -- 如果两个指针指向元素的和 sum == target,那么得到要求的结果; -- 如果 sum > target,移动较大的元素,使 sum 变小一些; -- 如果 sum < target,移动较小的元素,使 sum 变大一些。 - -```java -public ArrayList FindNumbersWithSum(int[] array, int sum) { - int i = 0, j = array.length - 1; - while (i < j) { - int cur = array[i] + array[j]; - if (cur == sum) - return new ArrayList<>(Arrays.asList(array[i], array[j])); - if (cur < sum) - i++; - else - j--; - } - return new ArrayList<>(); -} -``` - -# 57.2 和为 S 的连续正数序列 - -[NowCoder](https://www.nowcoder.com/practice/c451a3fd84b64cb19485dad758a55ebe?tpId=13&tqId=11194&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) - -## 题目描述 - -输出所有和为 S 的连续正数序列。 - -例如和为 100 的连续序列有: - -``` -[9, 10, 11, 12, 13, 14, 15, 16] -[18, 19, 20, 21, 22]。 -``` - -## 解题思路 - -```java -public ArrayList> FindContinuousSequence(int sum) { - ArrayList> 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 list = new ArrayList<>(); - for (int i = start; i <= end; i++) - list.add(i); - ret.add(list); - curSum -= start; - start++; - end++; - curSum += end; - } - } - return ret; -} -``` - -# 58.1 翻转单词顺序列 - -[NowCoder](https://www.nowcoder.com/practice/3194a4f4cf814f63919d0790578d51f3?tpId=13&tqId=11197&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) - -## 题目描述 - -```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; -} -``` - -# 58.2 左旋转字符串 - -[NowCoder](https://www.nowcoder.com/practice/12d959b108cb42b1ab72cef4d36af5ec?tpId=13&tqId=11196&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) - -## 题目描述 - -```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; -} -``` - -# 59. 滑动窗口的最大值 - -[NowCoder](https://www.nowcoder.com/practice/1624bc35a45c42c0bc17d17fa0cba788?tpId=13&tqId=11217&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) - -## 题目描述 - -给定一个数组和滑动窗口的大小,找出所有滑动窗口里数值的最大值。 - -例如,如果输入数组 {2, 3, 4, 2, 6, 2, 5, 1} 及滑动窗口的大小 3,那么一共存在 6 个滑动窗口,他们的最大值分别为 {4, 4, 6, 6, 6, 5}。 - -## 解题思路 - -```java -public ArrayList maxInWindows(int[] num, int size) { - ArrayList ret = new ArrayList<>(); - if (size > num.length || size < 1) - return ret; - PriorityQueue 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; -} -``` - -# 60. n 个骰子的点数 - -[Lintcode](https://www.lintcode.com/en/problem/dices-sum/) - -## 题目描述 - -把 n 个骰子仍在地上,求点数和为 s 的概率。 - -

- -## 解题思路 - -### 动态规划解法 - -使用一个二维数组 dp 存储点数出现的次数,其中 dp[i][j] 表示前 i 个骰子产生点数 j 的次数。 - -空间复杂度:O(N2) - -```java -public List> 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> 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> 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> ret = new ArrayList<>(); - for (int i = n; i <= pointNum; i++) - ret.add(new AbstractMap.SimpleEntry<>(i, dp[1 - flag][i] / totalNum)); - - return ret; -} -``` - -# 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) - -## 题目描述 - -五张牌,其中大小鬼为癞子,牌面大小为 0。判断这五张牌是否能组成顺子。 - -

- -## 解题思路 - -```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; -} -``` - -# 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) - -## 题目描述 - -让小朋友们围成一个大圈。然后,随机指定一个数 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; -} -``` - -# 63. 股票的最大利润 - -[Leetcode](https://leetcode.com/problems/best-time-to-buy-and-sell-stock/description/) - -## 题目描述 - -可以有一次买入和一次卖出,那么买入必须在前。求最大收益。 - -

- -## 解题思路 - -使用贪心策略,假设第 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; -} -``` - -# 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) - -## 题目描述 - -要求不能使用乘除法、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; -} -``` - -# 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) - -## 题目描述 - -写一个函数,求两个整数之和,要求不得使用 +、-、\*、/ 四则运算符号。 - -## 解题思路 - -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); -} -``` - -# 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) - -## 题目描述 - -给定一个数组 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]。要求不能使用除法。 - -

- -## 解题思路 - -```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; -} -``` - -# 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) - -## 题目描述 - -将一个字符串转换成一个整数,字符串不是一个合法的数值则返回 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; -} -``` - -# 68. 树中两个节点的最低公共祖先 - -## 解题思路 - -### 二叉查找树 - -

- -[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。 - -```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; -} -``` - -### 普通二叉树 - -

- -[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 分别在两个子树中,那么就说明根节点就是最低公共祖先。 - -```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; -} -``` - -# 参考文献 - -- 何海涛. 剑指 Offer[M]. 电子工业出版社, 2012. From 8e0dafa0add8b52ee865738d4fb0a1268de7716c Mon Sep 17 00:00:00 2001 From: CyC2018 <1029579233@qq.com> Date: Fri, 8 Mar 2019 21:30:54 +0800 Subject: [PATCH 09/56] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 220820f6..b9acbac5 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ ## :pencil2: 算法 -- [剑指 Offer 题解](https://github.com/CyC2018/CS-Notes/blob/master/docs/notes/剑指%20offer%20题解%20-%20目录.md) +- [剑指 Offer 题解](https://github.com/CyC2018/CS-Notes/blob/master/docs/notes/剑指%20Offer%20题解%20-%20目录.md) - [Leetcode 题解](https://github.com/CyC2018/CS-Notes/blob/master/docs/notes/Leetcode%20题解%20-%20目录.md) - [算法](https://github.com/CyC2018/CS-Notes/blob/master/docs/notes/算法.md) From 9564a5521b845b7d1caddaadcdaf95334724d288 Mon Sep 17 00:00:00 2001 From: CyC2018 <1029579233@qq.com> Date: Fri, 8 Mar 2019 21:31:41 +0800 Subject: [PATCH 10/56] =?UTF-8?q?Update=20=E5=89=91=E6=8C=87=20Offer=20?= =?UTF-8?q?=E9=A2=98=E8=A7=A3=20-=20=E7=9B=AE=E5=BD=95.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/notes/剑指 Offer 题解 - 目录.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/notes/剑指 Offer 题解 - 目录.md b/docs/notes/剑指 Offer 题解 - 目录.md index 844cfeee..9e78bed9 100644 --- a/docs/notes/剑指 Offer 题解 - 目录.md +++ b/docs/notes/剑指 Offer 题解 - 目录.md @@ -4,7 +4,7 @@ 部分绘图文件可以在这里免费下载:[剑指 Offer](https://www.processon.com/view/5a3e4c7be4b0909c1aa18b49),后续会慢慢把所有题目都配上 GIF 演示图。 -- [0\~9](剑指%20Offer%20题解%20-%200\~9.md) +- [3\~9](剑指%20Offer%20题解%20-%203\~9.md) - [10\~19](剑指%20Offer%20题解%20-%2010\~19.md) - [20\~29](剑指%20Offer%20题解%20-%2020\~29.md) - [30\~39](剑指%20Offer%20题解%20-%2030\~39.md) From 93ecf6bf0649c51d05e8eca5f140239b7cb50475 Mon Sep 17 00:00:00 2001 From: CyC2018 <1029579233@qq.com> Date: Fri, 8 Mar 2019 21:34:25 +0800 Subject: [PATCH 11/56] Update README.md --- docs/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/README.md b/docs/README.md index 97d3d9a0..b57e670d 100644 --- a/docs/README.md +++ b/docs/README.md @@ -2,7 +2,7 @@ ## ✏️ 算法 -- [剑指 Offer 题解](notes/剑指%20offer%20题解.md)
+- [剑指 Offer 题解](notes/剑指%20offer%20题解%20-%20目录1.md)
- [Leetcode 题解](notes/Leetcode%20题解%20-%20目录1.md)
- [算法](notes/算法.md)
- [点击订阅面试进阶指南](https://xiaozhuanlan.com/CyC2018) From a409978f773583360bdd84f936b0b2ffba3c2e76 Mon Sep 17 00:00:00 2001 From: CyC2018 <1029579233@qq.com> Date: Fri, 8 Mar 2019 21:35:41 +0800 Subject: [PATCH 12/56] auto commit --- docs/notes/剑指 Offer 题解 - 目录.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/notes/剑指 Offer 题解 - 目录.md b/docs/notes/剑指 Offer 题解 - 目录.md index 9e78bed9..844cfeee 100644 --- a/docs/notes/剑指 Offer 题解 - 目录.md +++ b/docs/notes/剑指 Offer 题解 - 目录.md @@ -4,7 +4,7 @@ 部分绘图文件可以在这里免费下载:[剑指 Offer](https://www.processon.com/view/5a3e4c7be4b0909c1aa18b49),后续会慢慢把所有题目都配上 GIF 演示图。 -- [3\~9](剑指%20Offer%20题解%20-%203\~9.md) +- [0\~9](剑指%20Offer%20题解%20-%200\~9.md) - [10\~19](剑指%20Offer%20题解%20-%2010\~19.md) - [20\~29](剑指%20Offer%20题解%20-%2020\~29.md) - [30\~39](剑指%20Offer%20题解%20-%2030\~39.md) From d909a309fe7e3d88e36ad09f334aea693d3fdf79 Mon Sep 17 00:00:00 2001 From: CyC2018 <1029579233@qq.com> Date: Fri, 8 Mar 2019 21:39:27 +0800 Subject: [PATCH 13/56] auto commit --- docs/notes/HTTP.md | 943 +++--- docs/notes/Java IO.md | 707 +++-- docs/notes/Java 基础.md | 1523 +++++---- docs/notes/Java 容器.md | 1345 ++++---- docs/notes/Java 并发.md | 1806 ++++++----- docs/notes/Java 虚拟机.md | 753 +++-- docs/notes/Leetcode 题解 - 二分查找.md | 371 ++- docs/notes/Leetcode 题解 - 位运算.md | 460 ++- docs/notes/Leetcode 题解 - 分治.md | 73 +- docs/notes/Leetcode 题解 - 动态规划.md | 1579 +++++----- docs/notes/Leetcode 题解 - 双指针.md | 289 +- docs/notes/Leetcode 题解 - 哈希表.md | 160 +- docs/notes/Leetcode 题解 - 图.md | 345 +-- docs/notes/Leetcode 题解 - 字符串.md | 259 +- docs/notes/Leetcode 题解 - 排序.md | 284 +- docs/notes/Leetcode 题解 - 搜索.md | 1720 +++++------ docs/notes/Leetcode 题解 - 数学.md | 578 ++-- docs/notes/Leetcode 题解 - 数组与矩阵.md | 528 ++-- docs/notes/Leetcode 题解 - 栈和队列.md | 288 +- docs/notes/Leetcode 题解 - 树.md | 1362 ++++---- docs/notes/Leetcode 题解 - 目录.md | 65 +- docs/notes/Leetcode 题解 - 目录1.md | 65 +- docs/notes/Leetcode 题解 - 贪心思想.md | 419 ++- docs/notes/Leetcode 题解 - 链表.md | 412 ++- docs/notes/Leetcode-Database 题解.md | 903 +++--- docs/notes/Linux.md | 1261 ++++---- docs/notes/MySQL.md | 392 ++- docs/notes/Redis.md | 719 +++-- docs/notes/SQL.md | 766 +++-- docs/notes/Socket.md | 373 ++- docs/notes/代码可读性.md | 326 +- docs/notes/代码风格规范.md | 10 +- docs/notes/分布式.md | 336 +- docs/notes/剑指 Offer 题解 - 10~19.md | 827 +++-- docs/notes/剑指 Offer 题解 - 20~29.md | 445 ++- docs/notes/剑指 Offer 题解 - 30~39.md | 606 ++-- docs/notes/剑指 Offer 题解 - 3~9.md | 429 ++- docs/notes/剑指 Offer 题解 - 40~49.md | 558 ++-- docs/notes/剑指 Offer 题解 - 50~59.md | 565 ++-- docs/notes/剑指 Offer 题解 - 60~68.md | 373 ++- docs/notes/剑指 Offer 题解 - 目录.md | 22 +- docs/notes/剑指 Offer 题解 - 目录1.md | 11 + docs/notes/攻击技术.md | 151 +- docs/notes/数据库系统原理.md | 576 ++-- docs/notes/构建工具.md | 146 +- docs/notes/正则表达式.md | 345 +-- docs/notes/消息队列.md | 61 +- docs/notes/算法.md | 2934 +++++++++--------- docs/notes/系统设计基础.md | 72 +- docs/notes/缓存.md | 306 +- docs/notes/计算机操作系统.md | 1091 ++++--- docs/notes/计算机网络.md | 900 +++--- docs/notes/设计模式.md | 3578 +++++++++++----------- docs/notes/集群.md | 149 +- docs/notes/面向对象思想.md | 341 +-- 55 files changed, 17462 insertions(+), 18444 deletions(-) create mode 100644 docs/notes/剑指 Offer 题解 - 目录1.md diff --git a/docs/notes/HTTP.md b/docs/notes/HTTP.md index 1b1c6800..eda176cb 100644 --- a/docs/notes/HTTP.md +++ b/docs/notes/HTTP.md @@ -1,879 +1,838 @@ - -* [一 、基础概念](#一-基础概念) - * [URI](#uri) - * [请求和响应报文](#请求和响应报文) -* [二、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) -* [参考资料](#参考资料) - +# 一 、基础概念 +## URI -# 一 、基础概念 +URI 包含 URL 和 URN。 -## URI + -URI 包含 URL 和 URN。 +## 请求和响应报文 -

+### 1. 请求报文 -## 请求和响应报文 + -### 1. 请求报文 +### 2. 响应报文 -

+ -### 2. 响应报文 +# 二、HTTP 方法 -

+客户端发送的 **请求报文** 第一行为请求行,包含了方法字段。 -# 二、HTTP 方法 +## GET -客户端发送的 **请求报文** 第一行为请求行,包含了方法字段。 +> 获取资源 -## GET +当前网络请求中,绝大部分使用的是 GET 方法。 -> 获取资源 +## HEAD -当前网络请求中,绝大部分使用的是 GET 方法。 +> 获取报文首部 -## HEAD +和 GET 方法类似,但是不返回报文实体主体部分。 -> 获取报文首部 +主要用于确认 URL 的有效性以及资源更新的日期时间等。 -和 GET 方法类似,但是不返回报文实体主体部分。 +## POST -主要用于确认 URL 的有效性以及资源更新的日期时间等。 +> 传输实体主体 -## POST +POST 主要用来传输数据,而 GET 主要用来获取资源。 -> 传输实体主体 +更多 POST 与 GET 的比较请见第九章。 -POST 主要用来传输数据,而 GET 主要用来获取资源。 +## PUT -更多 POST 与 GET 的比较请见第九章。 - -## PUT - -> 上传文件 +> 上传文件 由于自身不带验证机制,任何人都可以上传文件,因此存在安全性问题,一般不使用该方法。 ```html -PUT /new.html HTTP/1.1 -Host: example.com -Content-type: text/html -Content-length: 16 +PUT /new.html HTTP/1.1 +Host: example.com +Content-type: text/html +Content-length: 16 -

New File

+

New File

``` -## PATCH +## PATCH -> 对资源进行部分修改 +> 对资源进行部分修改 -PUT 也可以用于修改资源,但是只能完全替代原始资源,PATCH 允许部分修改。 +PUT 也可以用于修改资源,但是只能完全替代原始资源,PATCH 允许部分修改。 ```html -PATCH /file.txt HTTP/1.1 -Host: www.example.com -Content-Type: application/example -If-Match: "e0023aa4e" -Content-Length: 100 +PATCH /file.txt HTTP/1.1 +Host: www.example.com +Content-Type: application/example +If-Match: "e0023aa4e" +Content-Length: 100 -[description of changes] +[description of changes] ``` -## DELETE +## DELETE -> 删除文件 +> 删除文件 -与 PUT 功能相反,并且同样不带验证机制。 +与 PUT 功能相反,并且同样不带验证机制。 ```html -DELETE /file.html HTTP/1.1 +DELETE /file.html HTTP/1.1 ``` -## OPTIONS +## OPTIONS -> 查询支持的方法 +> 查询支持的方法 -查询指定的 URL 能够支持的方法。 +查询指定的 URL 能够支持的方法。 -会返回 `Allow: GET, POST, HEAD, OPTIONS` 这样的内容。 +会返回 `Allow: GET, POST, HEAD, OPTIONS` 这样的内容。 -## CONNECT +## CONNECT -> 要求在与代理服务器通信时建立隧道 +> 要求在与代理服务器通信时建立隧道 -使用 SSL(Secure Sockets Layer,安全套接层)和 TLS(Transport Layer Security,传输层安全)协议把通信内容加密后经网络隧道传输。 +使用 SSL(Secure Sockets Layer,安全套接层)和 TLS(Transport Layer Security,传输层安全)协议把通信内容加密后经网络隧道传输。 ```html -CONNECT www.example.com:443 HTTP/1.1 +CONNECT www.example.com:443 HTTP/1.1 ``` -

+ -## TRACE +## TRACE -> 追踪路径 +> 追踪路径 服务器会将通信路径返回给客户端。 -发送请求时,在 Max-Forwards 首部字段中填入数值,每经过一个服务器就会减 1,当数值为 0 时就停止传输。 +发送请求时,在 Max-Forwards 首部字段中填入数值,每经过一个服务器就会减 1,当数值为 0 时就停止传输。 -通常不会使用 TRACE,并且它容易受到 XST 攻击(Cross-Site Tracing,跨站追踪)。 +通常不会使用 TRACE,并且它容易受到 XST 攻击(Cross-Site Tracing,跨站追踪)。 -# 三、HTTP 状态码 +# 三、HTTP 状态码 -服务器返回的 **响应报文** 中第一行为状态行,包含了状态码以及原因短语,用来告知客户端请求的结果。 +服务器返回的 **响应报文** 中第一行为状态行,包含了状态码以及原因短语,用来告知客户端请求的结果。 -| 状态码 | 类别 | 含义 | -| :---: | :---: | :---: | -| 1XX | Informational(信息性状态码) | 接收的请求正在处理 | -| 2XX | Success(成功状态码) | 请求正常处理完毕 | -| 3XX | Redirection(重定向状态码) | 需要进行附加操作以完成请求 | -| 4XX | Client Error(客户端错误状态码) | 服务器无法处理请求 | -| 5XX | Server Error(服务器错误状态码) | 服务器处理请求出错 | +| 状态码 | 类别 | 含义 | +| :---: | :---: | :---: | +| 1XX | Informational(信息性状态码) | 接收的请求正在处理 | +| 2XX | Success(成功状态码) | 请求正常处理完毕 | +| 3XX | Redirection(重定向状态码) | 需要进行附加操作以完成请求 | +| 4XX | Client Error(客户端错误状态码) | 服务器无法处理请求 | +| 5XX | Server Error(服务器错误状态码) | 服务器处理请求出错 | -## 1XX 信息 +## 1XX 信息 -- **100 Continue** :表明到目前为止都很正常,客户端可以继续发送请求或者忽略这个响应。 +- **100 Continue**:表明到目前为止都很正常,客户端可以继续发送请求或者忽略这个响应。 -## 2XX 成功 +## 2XX 成功 -- **200 OK** +- **200 OK** -- **204 No Content** :请求已经成功处理,但是返回的响应报文不包含实体的主体部分。一般在只需要从客户端往服务器发送信息,而不需要返回数据时使用。 +- **204 No Content**:请求已经成功处理,但是返回的响应报文不包含实体的主体部分。一般在只需要从客户端往服务器发送信息,而不需要返回数据时使用。 -- **206 Partial Content** :表示客户端进行了范围请求,响应报文包含由 Content-Range 指定范围的实体内容。 +- **206 Partial Content**:表示客户端进行了范围请求,响应报文包含由 Content-Range 指定范围的实体内容。 -## 3XX 重定向 +## 3XX 重定向 -- **301 Moved Permanently** :永久性重定向 +- **301 Moved Permanently**:永久性重定向 -- **302 Found** :临时性重定向 +- **302 Found**:临时性重定向 -- **303 See Other** :和 302 有着相同的功能,但是 303 明确要求客户端应该采用 GET 方法获取资源。 +- **303 See Other**:和 302 有着相同的功能,但是 303 明确要求客户端应该采用 GET 方法获取资源。 -- 注:虽然 HTTP 协议规定 301、302 状态下重定向时不允许把 POST 方法改成 GET 方法,但是大多数浏览器都会在 301、302 和 303 状态下的重定向把 POST 方法改成 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 状态码。 +- **304 Not Modified**:如果请求报文首部包含一些条件,例如:If-Match,If-Modified-Since,If-None-Match,If-Range,If-Unmodified-Since,如果不满足条件,则服务器会返回 304 状态码。 -- **307 Temporary Redirect** :临时重定向,与 302 的含义类似,但是 307 要求浏览器不会把重定向请求的 POST 方法改成 GET 方法。 +- **307 Temporary Redirect**:临时重定向,与 302 的含义类似,但是 307 要求浏览器不会把重定向请求的 POST 方法改成 GET 方法。 -## 4XX 客户端错误 +## 4XX 客户端错误 -- **400 Bad Request** :请求报文中存在语法错误。 +- **400 Bad Request**:请求报文中存在语法错误。 -- **401 Unauthorized** :该状态码表示发送的请求需要有认证信息(BASIC 认证、DIGEST 认证)。如果之前已进行过一次请求,则表示用户认证失败。 +- **401 Unauthorized**:该状态码表示发送的请求需要有认证信息(BASIC 认证、DIGEST 认证)。如果之前已进行过一次请求,则表示用户认证失败。 -- **403 Forbidden** :请求被拒绝。 +- **403 Forbidden**:请求被拒绝。 -- **404 Not Found** +- **404 Not Found** -## 5XX 服务器错误 +## 5XX 服务器错误 -- **500 Internal Server Error** :服务器正在执行请求时发生错误。 +- **500 Internal Server Error**:服务器正在执行请求时发生错误。 -- **503 Service Unavailable** :服务器暂时处于超负载或正在进行停机维护,现在无法处理请求。 +- **503 Service Unavailable**:服务器暂时处于超负载或正在进行停机维护,现在无法处理请求。 -# 四、HTTP 首部 +# 四、HTTP 首部 -有 4 种类型的首部字段:通用首部字段、请求首部字段、响应首部字段和实体首部字段。 +有 4 种类型的首部字段:通用首部字段、请求首部字段、响应首部字段和实体首部字段。 各种首部字段及其含义如下(不需要全记,仅供查阅): -## 通用首部字段 +## 通用首部字段 -| 首部字段名 | 说明 | -| :--: | :--: | -| Cache-Control | 控制缓存的行为 | -| Connection | 控制不再转发给代理的首部字段、管理持久连接| -| Date | 创建报文的日期时间 | -| Pragma | 报文指令 | -| Trailer | 报文末端的首部一览 | -| Transfer-Encoding | 指定报文主体的传输编码方式 | -| Upgrade | 升级为其他协议 | -| Via | 代理服务器的相关信息 | -| Warning | 错误通知 | +| 首部字段名 | 说明 | +| :--: | :--: | +| 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 | 用户代理可处理的媒体类型 | +| 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 | 服务器对客户端的认证信息 | +| 首部字段名 | 说明 | +| :--: | :--: | +| 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 | 资源的最后修改日期时间 | +| 首部字段名 | 说明 | +| :--: | :--: | +| Allow | 资源可支持的 HTTP 方法 | +| Content-Encoding | 实体主体适用的编码方式 | +| Content-Language | 实体主体的自然语言 | +| Content-Length | 实体主体的大小 | +| Content-Location | 替代对应资源的 URI | +| Content-MD5 | 实体主体的报文摘要 | +| Content-Range | 实体主体的位置范围 | +| Content-Type | 实体主体的媒体类型 | +| Expires | 实体主体过期的日期时间 | +| Last-Modified | 资源的最后修改日期时间 | -# 五、具体应用 +# 五、具体应用 -## 连接管理 +## 连接管理 -

+ -### 1. 短连接与长连接 +### 1. 短连接与长连接 -当浏览器访问一个包含多张图片的 HTML 页面时,除了请求访问 HTML 页面资源,还会请求图片资源。如果每进行一次 HTTP 通信就要新建一个 TCP 连接,那么开销会很大。 +当浏览器访问一个包含多张图片的 HTML 页面时,除了请求访问 HTML 页面资源,还会请求图片资源。如果每进行一次 HTTP 通信就要新建一个 TCP 连接,那么开销会很大。 -长连接只需要建立一次 TCP 连接就能进行多次 HTTP 通信。 +长连接只需要建立一次 TCP 连接就能进行多次 HTTP 通信。 -- 从 HTTP/1.1 开始默认是长连接的,如果要断开连接,需要由客户端或者服务器端提出断开,使用 `Connection : close`; -- 在 HTTP/1.1 之前默认是短连接的,如果需要使用长连接,则使用 `Connection : Keep-Alive`。 +- 从 HTTP/1.1 开始默认是长连接的,如果要断开连接,需要由客户端或者服务器端提出断开,使用 `Connection : close`; +- 在 HTTP/1.1 之前默认是短连接的,如果需要使用长连接,则使用 `Connection : Keep-Alive`。 -### 2. 流水线 +### 2. 流水线 -默认情况下,HTTP 请求是按顺序发出的,下一个请求只有在当前请求收到响应之后才会被发出。由于会受到网络延迟和带宽的限制,在下一个请求被发送到服务器之前,可能需要等待很长时间。 +默认情况下,HTTP 请求是按顺序发出的,下一个请求只有在当前请求收到响应之后才会被发出。由于会受到网络延迟和带宽的限制,在下一个请求被发送到服务器之前,可能需要等待很长时间。 流水线是在同一条长连接上发出连续的请求,而不用等待响应返回,这样可以避免连接延迟。 -## Cookie +## Cookie -HTTP 协议是无状态的,主要是为了让 HTTP 协议尽可能简单,使得它能够处理大量事务。HTTP/1.1 引入 Cookie 来保存状态信息。 +HTTP 协议是无状态的,主要是为了让 HTTP 协议尽可能简单,使得它能够处理大量事务。HTTP/1.1 引入 Cookie 来保存状态信息。 -Cookie 是服务器发送到用户浏览器并保存在本地的一小块数据,它会在浏览器之后向同一服务器再次发起请求时被携带上,用于告知服务端两个请求是否来自同一浏览器。由于之后每次请求都会需要携带 Cookie 数据,因此会带来额外的性能开销(尤其是在移动环境下)。 +Cookie 是服务器发送到用户浏览器并保存在本地的一小块数据,它会在浏览器之后向同一服务器再次发起请求时被携带上,用于告知服务端两个请求是否来自同一浏览器。由于之后每次请求都会需要携带 Cookie 数据,因此会带来额外的性能开销(尤其是在移动环境下)。 -Cookie 曾一度用于客户端数据的存储,因为当时并没有其它合适的存储办法而作为唯一的存储手段,但现在随着现代浏览器开始支持各种各样的存储方式,Cookie 渐渐被淘汰。新的浏览器 API 已经允许开发者直接将数据存储到本地,如使用 Web storage API(本地存储和会话存储)或 IndexedDB。 +Cookie 曾一度用于客户端数据的存储,因为当时并没有其它合适的存储办法而作为唯一的存储手段,但现在随着现代浏览器开始支持各种各样的存储方式,Cookie 渐渐被淘汰。新的浏览器 API 已经允许开发者直接将数据存储到本地,如使用 Web storage API(本地存储和会话存储)或 IndexedDB。 -### 1. 用途 +### 1. 用途 -- 会话状态管理(如用户登录状态、购物车、游戏分数或其它需要记录的信息) -- 个性化设置(如用户自定义设置、主题等) -- 浏览器行为跟踪(如跟踪分析用户行为等) +- 会话状态管理(如用户登录状态、购物车、游戏分数或其它需要记录的信息) +- 个性化设置(如用户自定义设置、主题等) +- 浏览器行为跟踪(如跟踪分析用户行为等) -### 2. 创建过程 +### 2. 创建过程 -服务器发送的响应报文包含 Set-Cookie 首部字段,客户端得到响应报文后把 Cookie 内容保存到浏览器中。 +服务器发送的响应报文包含 Set-Cookie 首部字段,客户端得到响应报文后把 Cookie 内容保存到浏览器中。 ```html -HTTP/1.0 200 OK -Content-type: text/html -Set-Cookie: yummy_cookie=choco -Set-Cookie: tasty_cookie=strawberry +HTTP/1.0 200 OK +Content-type: text/html +Set-Cookie: yummy_cookie=choco +Set-Cookie: tasty_cookie=strawberry -[page content] +[page content] ``` -客户端之后对同一个服务器发送请求时,会从浏览器中取出 Cookie 信息并通过 Cookie 请求首部字段发送给服务器。 +客户端之后对同一个服务器发送请求时,会从浏览器中取出 Cookie 信息并通过 Cookie 请求首部字段发送给服务器。 ```html -GET /sample_page.html HTTP/1.1 -Host: www.example.org -Cookie: yummy_cookie=choco; tasty_cookie=strawberry +GET /sample_page.html HTTP/1.1 +Host: www.example.org +Cookie: yummy_cookie=choco; tasty_cookie=strawberry ``` -### 3. 分类 +### 3. 分类 -- 会话期 Cookie:浏览器关闭之后它会被自动删除,也就是说它仅在会话期内有效。 -- 持久性 Cookie:指定一个特定的过期时间(Expires)或有效期(max-age)之后就成为了持久性的 Cookie。 +- 会话期 Cookie:浏览器关闭之后它会被自动删除,也就是说它仅在会话期内有效。 +- 持久性 Cookie:指定一个特定的过期时间(Expires)或有效期(max-age)之后就成为了持久性的 Cookie。 ```html -Set-Cookie: id=a3fWa; Expires=Wed, 21 Oct 2015 07:28:00 GMT; +Set-Cookie: id=a3fWa; Expires=Wed, 21 Oct 2015 07:28:00 GMT; ``` -### 4. 作用域 +### 4. 作用域 -Domain 标识指定了哪些主机可以接受 Cookie。如果不指定,默认为当前文档的主机(不包含子域名)。如果指定了 Domain,则一般包含子域名。例如,如果设置 Domain=mozilla.org,则 Cookie 也包含在子域名中(如 developer.mozilla.org)。 +Domain 标识指定了哪些主机可以接受 Cookie。如果不指定,默认为当前文档的主机(不包含子域名)。如果指定了 Domain,则一般包含子域名。例如,如果设置 Domain=mozilla.org,则 Cookie 也包含在子域名中(如 developer.mozilla.org)。 -Path 标识指定了主机下的哪些路径可以接受 Cookie(该 URL 路径必须存在于请求 URL 中)。以字符 %x2F ("/") 作为路径分隔符,子路径也会被匹配。例如,设置 Path=/docs,则以下地址都会匹配: +Path 标识指定了主机下的哪些路径可以接受 Cookie(该 URL 路径必须存在于请求 URL 中)。以字符 %x2F ("/") 作为路径分隔符,子路径也会被匹配。例如,设置 Path=/docs,则以下地址都会匹配: -- /docs -- /docs/Web/ -- /docs/Web/HTTP +- /docs +- /docs/Web/ +- /docs/Web/HTTP -### 5. JavaScript +### 5. JavaScript -通过 `document.cookie` 属性可创建新的 Cookie,也可通过该属性访问非 HttpOnly 标记的 Cookie。 +通过 `document.cookie` 属性可创建新的 Cookie,也可通过该属性访问非 HttpOnly 标记的 Cookie。 ```html -document.cookie = "yummy_cookie=choco"; -document.cookie = "tasty_cookie=strawberry"; +document.cookie = "yummy_cookie=choco"; +document.cookie = "tasty_cookie=strawberry"; console.log(document.cookie); ``` -### 6. HttpOnly +### 6. HttpOnly -标记为 HttpOnly 的 Cookie 不能被 JavaScript 脚本调用。跨站脚本攻击 (XSS) 常常使用 JavaScript 的 `document.cookie` API 窃取用户的 Cookie 信息,因此使用 HttpOnly 标记可以在一定程度上避免 XSS 攻击。 +标记为 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 +Set-Cookie: id=a3fWa; Expires=Wed, 21 Oct 2015 07:28:00 GMT; Secure; HttpOnly ``` -### 7. Secure +### 7. Secure -标记为 Secure 的 Cookie 只能通过被 HTTPS 协议加密过的请求发送给服务端。但即便设置了 Secure 标记,敏感信息也不应该通过 Cookie 传输,因为 Cookie 有其固有的不安全性,Secure 标记也无法提供确实的安全保障。 +标记为 Secure 的 Cookie 只能通过被 HTTPS 协议加密过的请求发送给服务端。但即便设置了 Secure 标记,敏感信息也不应该通过 Cookie 传输,因为 Cookie 有其固有的不安全性,Secure 标记也无法提供确实的安全保障。 -### 8. Session +### 8. Session -除了可以将用户信息通过 Cookie 存储在用户浏览器中,也可以利用 Session 存储在服务器端,存储在服务器端的信息更加安全。 +除了可以将用户信息通过 Cookie 存储在用户浏览器中,也可以利用 Session 存储在服务器端,存储在服务器端的信息更加安全。 -Session 可以存储在服务器上的文件、数据库或者内存中。也可以将 Session 存储在 Redis 这种内存型数据库中,效率会更高。 +Session 可以存储在服务器上的文件、数据库或者内存中。也可以将 Session 存储在 Redis 这种内存型数据库中,效率会更高。 -使用 Session 维护用户登录状态的过程如下: +使用 Session 维护用户登录状态的过程如下: -- 用户进行登录时,用户提交包含用户名和密码的表单,放入 HTTP 请求报文中; -- 服务器验证该用户名和密码,如果正确则把用户信息存储到 Redis 中,它在 Redis 中的 Key 称为 Session ID; -- 服务器返回的响应报文的 Set-Cookie 首部字段包含了这个 Session ID,客户端收到响应报文之后将该 Cookie 值存入浏览器中; -- 客户端之后对同一个服务器进行请求时会包含该 Cookie 值,服务器收到之后提取出 Session ID,从 Redis 中取出用户信息,继续之前的业务操作。 +- 用户进行登录时,用户提交包含用户名和密码的表单,放入 HTTP 请求报文中; +- 服务器验证该用户名和密码,如果正确则把用户信息存储到 Redis 中,它在 Redis 中的 Key 称为 Session ID; +- 服务器返回的响应报文的 Set-Cookie 首部字段包含了这个 Session ID,客户端收到响应报文之后将该 Cookie 值存入浏览器中; +- 客户端之后对同一个服务器进行请求时会包含该 Cookie 值,服务器收到之后提取出 Session ID,从 Redis 中取出用户信息,继续之前的业务操作。 -应该注意 Session ID 的安全性问题,不能让它被恶意攻击者轻易获取,那么就不能产生一个容易被猜到的 Session ID 值。此外,还需要经常重新生成 Session ID。在对安全性要求极高的场景下,例如转账等操作,除了使用 Session 管理用户状态之外,还需要对用户进行重新验证,比如重新输入密码,或者使用短信验证码等方式。 +应该注意 Session ID 的安全性问题,不能让它被恶意攻击者轻易获取,那么就不能产生一个容易被猜到的 Session ID 值。此外,还需要经常重新生成 Session ID。在对安全性要求极高的场景下,例如转账等操作,除了使用 Session 管理用户状态之外,还需要对用户进行重新验证,比如重新输入密码,或者使用短信验证码等方式。 -### 9. 浏览器禁用 Cookie +### 9. 浏览器禁用 Cookie -此时无法使用 Cookie 来保存用户信息,只能使用 Session。除此之外,不能再将 Session ID 存放到 Cookie 中,而是使用 URL 重写技术,将 Session ID 作为 URL 的参数进行传递。 +此时无法使用 Cookie 来保存用户信息,只能使用 Session。除此之外,不能再将 Session ID 存放到 Cookie 中,而是使用 URL 重写技术,将 Session ID 作为 URL 的参数进行传递。 -### 10. Cookie 与 Session 选择 +### 10. Cookie 与 Session 选择 -- Cookie 只能存储 ASCII 码字符串,而 Session 则可以存取任何类型的数据,因此在考虑数据复杂性时首选 Session; -- Cookie 存储在浏览器中,容易被恶意查看。如果非要将一些隐私数据存在 Cookie 中,可以将 Cookie 值进行加密,然后在服务器进行解密; -- 对于大型网站,如果用户所有的信息都存储在 Session 中,那么开销是非常大的,因此不建议将所有的用户信息都存储到 Session 中。 +- Cookie 只能存储 ASCII 码字符串,而 Session 则可以存取任何类型的数据,因此在考虑数据复杂性时首选 Session; +- Cookie 存储在浏览器中,容易被恶意查看。如果非要将一些隐私数据存在 Cookie 中,可以将 Cookie 值进行加密,然后在服务器进行解密; +- 对于大型网站,如果用户所有的信息都存储在 Session 中,那么开销是非常大的,因此不建议将所有的用户信息都存储到 Session 中。 -## 缓存 +## 缓存 -### 1. 优点 +### 1. 优点 -- 缓解服务器压力; -- 降低客户端获取资源的延迟:缓存通常位于内存中,读取缓存的速度更快。并且缓存在地理位置上也有可能比源服务器来得近,例如浏览器缓存。 +- 缓解服务器压力; +- 降低客户端获取资源的延迟:缓存通常位于内存中,读取缓存的速度更快。并且缓存在地理位置上也有可能比源服务器来得近,例如浏览器缓存。 -### 2. 实现方法 +### 2. 实现方法 -- 让代理服务器进行缓存; -- 让客户端浏览器进行缓存。 +- 让代理服务器进行缓存; +- 让客户端浏览器进行缓存。 -### 3. Cache-Control +### 3. Cache-Control -HTTP/1.1 通过 Cache-Control 首部字段来控制缓存。 +HTTP/1.1 通过 Cache-Control 首部字段来控制缓存。 -**3.1 禁止进行缓存** +**3.1 禁止进行缓存** -no-store 指令规定不能对请求或响应的任何一部分进行缓存。 +no-store 指令规定不能对请求或响应的任何一部分进行缓存。 ```html -Cache-Control: no-store +Cache-Control: no-store ``` -**3.2 强制确认缓存** +**3.2 强制确认缓存** -no-cache 指令规定缓存服务器需要先向源服务器验证缓存资源的有效性,只有当缓存资源有效才将能使用该缓存对客户端的请求进行响应。 +no-cache 指令规定缓存服务器需要先向源服务器验证缓存资源的有效性,只有当缓存资源有效才将能使用该缓存对客户端的请求进行响应。 ```html -Cache-Control: no-cache +Cache-Control: no-cache ``` -**3.3 私有缓存和公共缓存** +**3.3 私有缓存和公共缓存** -private 指令规定了将资源作为私有缓存,只能被单独用户所使用,一般存储在用户浏览器中。 +private 指令规定了将资源作为私有缓存,只能被单独用户所使用,一般存储在用户浏览器中。 ```html -Cache-Control: private +Cache-Control: private ``` -public 指令规定了将资源作为公共缓存,可以被多个用户所使用,一般存储在代理服务器中。 +public 指令规定了将资源作为公共缓存,可以被多个用户所使用,一般存储在代理服务器中。 ```html -Cache-Control: public +Cache-Control: public ``` -**3.4 缓存过期机制** +**3.4 缓存过期机制** -max-age 指令出现在请求报文中,并且缓存资源的缓存时间小于该指令指定的时间,那么就能接受该缓存。 +max-age 指令出现在请求报文中,并且缓存资源的缓存时间小于该指令指定的时间,那么就能接受该缓存。 -max-age 指令出现在响应报文中,表示缓存资源在缓存服务器中保存的时间。 +max-age 指令出现在响应报文中,表示缓存资源在缓存服务器中保存的时间。 ```html -Cache-Control: max-age=31536000 +Cache-Control: max-age=31536000 ``` -Expires 首部字段也可以用于告知缓存服务器该资源什么时候会过期。 +Expires 首部字段也可以用于告知缓存服务器该资源什么时候会过期。 ```html -Expires: Wed, 04 Jul 2012 08:26:05 GMT +Expires: Wed, 04 Jul 2012 08:26:05 GMT ``` -- 在 HTTP/1.1 中,会优先处理 max-age 指令; -- 在 HTTP/1.0 中,max-age 指令会被忽略掉。 +- 在 HTTP/1.1 中,会优先处理 max-age 指令; +- 在 HTTP/1.0 中,max-age 指令会被忽略掉。 -### 4. 缓存验证 +### 4. 缓存验证 -需要先了解 ETag 首部字段的含义,它是资源的唯一标识。URL 不能唯一表示资源,例如 `http://www.google.com/` 有中文和英文两个资源,只有 ETag 才能对这两个资源进行唯一标识。 +需要先了解 ETag 首部字段的含义,它是资源的唯一标识。URL 不能唯一表示资源,例如 `http://www.google.com/` 有中文和英文两个资源,只有 ETag 才能对这两个资源进行唯一标识。 ```html -ETag: "82e22293907ce725faf67773957acd12" +ETag: "82e22293907ce725faf67773957acd12" ``` -可以将缓存资源的 ETag 值放入 If-None-Match 首部,服务器收到该请求后,判断缓存资源的 ETag 值和资源的最新 ETag 值是否一致,如果一致则表示缓存资源有效,返回 304 Not Modified。 +可以将缓存资源的 ETag 值放入 If-None-Match 首部,服务器收到该请求后,判断缓存资源的 ETag 值和资源的最新 ETag 值是否一致,如果一致则表示缓存资源有效,返回 304 Not Modified。 ```html -If-None-Match: "82e22293907ce725faf67773957acd12" +If-None-Match: "82e22293907ce725faf67773957acd12" ``` -Last-Modified 首部字段也可以用于缓存验证,它包含在源服务器发送的响应报文中,指示源服务器对资源的最后修改时间。但是它是一种弱校验器,因为只能精确到一秒,所以它通常作为 ETag 的备用方案。如果响应首部字段里含有这个信息,客户端可以在后续的请求中带上 If-Modified-Since 来验证缓存。服务器只在所请求的资源在给定的日期时间之后对内容进行过修改的情况下才会将资源返回,状态码为 200 OK。如果请求的资源从那时起未经修改,那么返回一个不带有消息主体的 304 Not Modified 响应。 +Last-Modified 首部字段也可以用于缓存验证,它包含在源服务器发送的响应报文中,指示源服务器对资源的最后修改时间。但是它是一种弱校验器,因为只能精确到一秒,所以它通常作为 ETag 的备用方案。如果响应首部字段里含有这个信息,客户端可以在后续的请求中带上 If-Modified-Since 来验证缓存。服务器只在所请求的资源在给定的日期时间之后对内容进行过修改的情况下才会将资源返回,状态码为 200 OK。如果请求的资源从那时起未经修改,那么返回一个不带有消息主体的 304 Not Modified 响应。 ```html -Last-Modified: Wed, 21 Oct 2015 07:28:00 GMT +Last-Modified: Wed, 21 Oct 2015 07:28:00 GMT ``` ```html -If-Modified-Since: Wed, 21 Oct 2015 07:28:00 GMT +If-Modified-Since: Wed, 21 Oct 2015 07:28:00 GMT ``` -## 内容协商 +## 内容协商 通过内容协商返回最合适的内容,例如根据浏览器的默认语言选择返回中文界面还是英文界面。 -### 1. 类型 +### 1. 类型 -**1.1 服务端驱动型** +**1.1 服务端驱动型** -客户端设置特定的 HTTP 首部字段,例如 Accept、Accept-Charset、Accept-Encoding、Accept-Language,服务器根据这些字段返回特定的资源。 +客户端设置特定的 HTTP 首部字段,例如 Accept、Accept-Charset、Accept-Encoding、Accept-Language,服务器根据这些字段返回特定的资源。 它存在以下问题: -- 服务器很难知道客户端浏览器的全部信息; -- 客户端提供的信息相当冗长(HTTP/2 协议的首部压缩机制缓解了这个问题),并且存在隐私风险(HTTP 指纹识别技术); -- 给定的资源需要返回不同的展现形式,共享缓存的效率会降低,而服务器端的实现会越来越复杂。 +- 服务器很难知道客户端浏览器的全部信息; +- 客户端提供的信息相当冗长(HTTP/2 协议的首部压缩机制缓解了这个问题),并且存在隐私风险(HTTP 指纹识别技术); +- 给定的资源需要返回不同的展现形式,共享缓存的效率会降低,而服务器端的实现会越来越复杂。 -**1.2 代理驱动型** +**1.2 代理驱动型** -服务器返回 300 Multiple Choices 或者 406 Not Acceptable,客户端从中选出最合适的那个资源。 +服务器返回 300 Multiple Choices 或者 406 Not Acceptable,客户端从中选出最合适的那个资源。 -### 2. Vary +### 2. Vary ```html -Vary: Accept-Language +Vary: Accept-Language ``` 在使用内容协商的情况下,只有当缓存服务器中的缓存满足内容协商条件时,才能使用该缓存,否则应该向源服务器请求该资源。 -例如,一个客户端发送了一个包含 Accept-Language 首部字段的请求之后,源服务器返回的响应包含 `Vary: Accept-Language` 内容,缓存服务器对这个响应进行缓存之后,在客户端下一次访问同一个 URL 资源,并且 Accept-Language 与缓存中的对应的值相同时才会返回该缓存。 +例如,一个客户端发送了一个包含 Accept-Language 首部字段的请求之后,源服务器返回的响应包含 `Vary: Accept-Language` 内容,缓存服务器对这个响应进行缓存之后,在客户端下一次访问同一个 URL 资源,并且 Accept-Language 与缓存中的对应的值相同时才会返回该缓存。 -## 内容编码 +## 内容编码 内容编码将实体主体进行压缩,从而减少传输的数据量。 常用的内容编码有:gzip、compress、deflate、identity。 -浏览器发送 Accept-Encoding 首部,其中包含有它所支持的压缩算法,以及各自的优先级。服务器则从中选择一种,使用该算法对响应的消息主体进行压缩,并且发送 Content-Encoding 首部来告知浏览器它选择了哪一种算法。由于该内容协商过程是基于编码类型来选择资源的展现形式的,在响应的 Vary 首部至少要包含 Content-Encoding。 +浏览器发送 Accept-Encoding 首部,其中包含有它所支持的压缩算法,以及各自的优先级。服务器则从中选择一种,使用该算法对响应的消息主体进行压缩,并且发送 Content-Encoding 首部来告知浏览器它选择了哪一种算法。由于该内容协商过程是基于编码类型来选择资源的展现形式的,在响应的 Vary 首部至少要包含 Content-Encoding。 -## 范围请求 +## 范围请求 如果网络出现中断,服务器只发送了一部分数据,范围请求可以使得客户端只请求服务器未发送的那部分数据,从而避免服务器重新发送所有数据。 -### 1. Range +### 1. Range -在请求报文中添加 Range 首部字段指定请求的范围。 +在请求报文中添加 Range 首部字段指定请求的范围。 ```html -GET /z4d4kWk.jpg HTTP/1.1 -Host: i.imgur.com -Range: bytes=0-1023 +GET /z4d4kWk.jpg HTTP/1.1 +Host: i.imgur.com +Range: bytes=0-1023 ``` -请求成功的话服务器返回的响应包含 206 Partial Content 状态码。 +请求成功的话服务器返回的响应包含 206 Partial Content 状态码。 ```html -HTTP/1.1 206 Partial Content -Content-Range: bytes 0-1023/146515 -Content-Length: 1024 +HTTP/1.1 206 Partial Content +Content-Range: bytes 0-1023/146515 +Content-Length: 1024 ... -(binary content) +(binary content) ``` -### 2. Accept-Ranges +### 2. Accept-Ranges -响应首部字段 Accept-Ranges 用于告知客户端是否能处理范围请求,可以处理使用 bytes,否则使用 none。 +响应首部字段 Accept-Ranges 用于告知客户端是否能处理范围请求,可以处理使用 bytes,否则使用 none。 ```html -Accept-Ranges: bytes +Accept-Ranges: bytes ``` -### 3. 响应状态码 +### 3. 响应状态码 -- 在请求成功的情况下,服务器会返回 206 Partial Content 状态码。 -- 在请求的范围越界的情况下,服务器会返回 416 Requested Range Not Satisfiable 状态码。 -- 在不支持范围请求的情况下,服务器会返回 200 OK 状态码。 +- 在请求成功的情况下,服务器会返回 206 Partial Content 状态码。 +- 在请求的范围越界的情况下,服务器会返回 416 Requested Range Not Satisfiable 状态码。 +- 在不支持范围请求的情况下,服务器会返回 200 OK 状态码。 -## 分块传输编码 +## 分块传输编码 -Chunked Transfer Coding,可以把数据分割成多块,让浏览器逐步显示页面。 +Chunked Transfer Coding,可以把数据分割成多块,让浏览器逐步显示页面。 -## 多部分对象集合 +## 多部分对象集合 -一份报文主体内可含有多种类型的实体同时发送,每个部分之间用 boundary 字段定义的分隔符进行分隔,每个部分都可以有首部字段。 +一份报文主体内可含有多种类型的实体同时发送,每个部分之间用 boundary 字段定义的分隔符进行分隔,每个部分都可以有首部字段。 例如,上传多个表单时可以使用如下方式: ```html -Content-Type: multipart/form-data; boundary=AaB03x +Content-Type: multipart/form-data; boundary=AaB03x --AaB03x -Content-Disposition: form-data; name="submit-name" +Content-Disposition: form-data; name="submit-name" Larry --AaB03x -Content-Disposition: form-data; name="files"; filename="file1.txt" -Content-Type: text/plain +Content-Disposition: form-data; name="files"; filename="file1.txt" +Content-Type: text/plain -... contents of file1.txt ... +... contents of file1.txt ... --AaB03x-- ``` -## 虚拟主机 +## 虚拟主机 -HTTP/1.1 使用虚拟主机技术,使得一台服务器拥有多个域名,并且在逻辑上可以看成多个服务器。 +HTTP/1.1 使用虚拟主机技术,使得一台服务器拥有多个域名,并且在逻辑上可以看成多个服务器。 -## 通信数据转发 +## 通信数据转发 -### 1. 代理 +### 1. 代理 代理服务器接受客户端的请求,并且转发给其它服务器。 使用代理的主要目的是: -- 缓存 -- 负载均衡 -- 网络访问控制 -- 访问日志记录 +- 缓存 +- 负载均衡 +- 网络访问控制 +- 访问日志记录 代理服务器分为正向代理和反向代理两种: -- 用户察觉得到正向代理的存在。 +- 用户察觉得到正向代理的存在。 -

+ -- 而反向代理一般位于内部网络中,用户察觉不到。 +- 而反向代理一般位于内部网络中,用户察觉不到。 -

+ -### 2. 网关 +### 2. 网关 -与代理服务器不同的是,网关服务器会将 HTTP 转化为其它协议进行通信,从而请求其它非 HTTP 服务器的服务。 +与代理服务器不同的是,网关服务器会将 HTTP 转化为其它协议进行通信,从而请求其它非 HTTP 服务器的服务。 -### 3. 隧道 +### 3. 隧道 -使用 SSL 等加密手段,在客户端和服务器之间建立一条安全的通信线路。 +使用 SSL 等加密手段,在客户端和服务器之间建立一条安全的通信线路。 -# 六、HTTPS +# 六、HTTPS -HTTP 有以下安全性问题: +HTTP 有以下安全性问题: -- 使用明文进行通信,内容可能会被窃听; -- 不验证通信方的身份,通信方的身份有可能遭遇伪装; -- 无法证明报文的完整性,报文有可能遭篡改。 +- 使用明文进行通信,内容可能会被窃听; +- 不验证通信方的身份,通信方的身份有可能遭遇伪装; +- 无法证明报文的完整性,报文有可能遭篡改。 -HTTPS 并不是新协议,而是让 HTTP 先和 SSL(Secure Sockets Layer)通信,再由 SSL 和 TCP 通信,也就是说 HTTPS 使用了隧道进行通信。 +HTTPS 并不是新协议,而是让 HTTP 先和 SSL(Secure Sockets Layer)通信,再由 SSL 和 TCP 通信,也就是说 HTTPS 使用了隧道进行通信。 -通过使用 SSL,HTTPS 具有了加密(防窃听)、认证(防伪装)和完整性保护(防篡改)。 +通过使用 SSL,HTTPS 具有了加密(防窃听)、认证(防伪装)和完整性保护(防篡改)。 -

+ -## 加密 +## 加密 -### 1. 对称密钥加密 +### 1. 对称密钥加密 -对称密钥加密(Symmetric-Key Encryption),加密和解密使用同一密钥。 +对称密钥加密(Symmetric-Key Encryption),加密和解密使用同一密钥。 -- 优点:运算速度快; -- 缺点:无法安全地将密钥传输给通信方。 +- 优点:运算速度快; +- 缺点:无法安全地将密钥传输给通信方。 -

+ -### 2.非对称密钥加密 +### 2.非对称密钥加密 -非对称密钥加密,又称公开密钥加密(Public-Key Encryption),加密和解密使用不同的密钥。 +非对称密钥加密,又称公开密钥加密(Public-Key Encryption),加密和解密使用不同的密钥。 公开密钥所有人都可以获得,通信发送方获得接收方的公开密钥之后,就可以使用公开密钥进行加密,接收方收到通信内容后使用私有密钥解密。 非对称密钥除了用来加密,还可以用来进行签名。因为私有密钥无法被其他人获取,因此通信发送方使用其私有密钥进行签名,通信接收方使用发送方的公开密钥对签名进行解密,就能判断这个签名是否正确。 -- 优点:可以更安全地将公开密钥传输给通信发送方; -- 缺点:运算速度慢。 +- 优点:可以更安全地将公开密钥传输给通信发送方; +- 缺点:运算速度慢。 -

+ -### 3. HTTPS 采用的加密方式 +### 3. HTTPS 采用的加密方式 -HTTPS 采用混合的加密机制,使用非对称密钥加密用于传输对称密钥来保证传输过程的安全性,之后使用对称密钥加密进行通信来保证通信过程的效率。(下图中的 Session Key 就是对称密钥) +HTTPS 采用混合的加密机制,使用非对称密钥加密用于传输对称密钥来保证传输过程的安全性,之后使用对称密钥加密进行通信来保证通信过程的效率。(下图中的 Session Key 就是对称密钥) -

+ -## 认证 +## 认证 -通过使用 **证书** 来对通信方进行认证。 +通过使用 **证书** 来对通信方进行认证。 -数字证书认证机构(CA,Certificate Authority)是客户端与服务器双方都可信赖的第三方机构。 +数字证书认证机构(CA,Certificate Authority)是客户端与服务器双方都可信赖的第三方机构。 -服务器的运营人员向 CA 提出公开密钥的申请,CA 在判明提出申请者的身份之后,会对已申请的公开密钥做数字签名,然后分配这个已签名的公开密钥,并将该公开密钥放入公开密钥证书后绑定在一起。 +服务器的运营人员向 CA 提出公开密钥的申请,CA 在判明提出申请者的身份之后,会对已申请的公开密钥做数字签名,然后分配这个已签名的公开密钥,并将该公开密钥放入公开密钥证书后绑定在一起。 -进行 HTTPS 通信时,服务器会把证书发送给客户端。客户端取得其中的公开密钥之后,先使用数字签名进行验证,如果验证通过,就可以开始通信了。 +进行 HTTPS 通信时,服务器会把证书发送给客户端。客户端取得其中的公开密钥之后,先使用数字签名进行验证,如果验证通过,就可以开始通信了。 -

+ -## 完整性保护 +## 完整性保护 -SSL 提供报文摘要功能来进行完整性保护。 +SSL 提供报文摘要功能来进行完整性保护。 -HTTP 也提供了 MD5 报文摘要功能,但不是安全的。例如报文内容被篡改之后,同时重新计算 MD5 的值,通信接收方是无法意识到发生了篡改。 +HTTP 也提供了 MD5 报文摘要功能,但不是安全的。例如报文内容被篡改之后,同时重新计算 MD5 的值,通信接收方是无法意识到发生了篡改。 -HTTPS 的报文摘要功能之所以安全,是因为它结合了加密和认证这两个操作。试想一下,加密之后的报文,遭到篡改之后,也很难重新计算报文摘要,因为无法轻易获取明文。 +HTTPS 的报文摘要功能之所以安全,是因为它结合了加密和认证这两个操作。试想一下,加密之后的报文,遭到篡改之后,也很难重新计算报文摘要,因为无法轻易获取明文。 -## HTTPS 的缺点 +## HTTPS 的缺点 -- 因为需要进行加密解密等过程,因此速度会更慢; -- 需要支付证书授权的高额费用。 +- 因为需要进行加密解密等过程,因此速度会更慢; +- 需要支付证书授权的高额费用。 -# 七、HTTP/2.0 +# 七、HTTP/2.0 -## HTTP/1.x 缺陷 +## HTTP/1.x 缺陷 -HTTP/1.x 实现简单是以牺牲性能为代价的: +HTTP/1.x 实现简单是以牺牲性能为代价的: -- 客户端需要使用多个连接才能实现并发和缩短延迟; -- 不会压缩请求和响应首部,从而导致不必要的网络流量; -- 不支持有效的资源优先级,致使底层 TCP 连接的利用率低下。 +- 客户端需要使用多个连接才能实现并发和缩短延迟; +- 不会压缩请求和响应首部,从而导致不必要的网络流量; +- 不支持有效的资源优先级,致使底层 TCP 连接的利用率低下。 -## 二进制分帧层 +## 二进制分帧层 -HTTP/2.0 将报文分成 HEADERS 帧和 DATA 帧,它们都是二进制格式的。 +HTTP/2.0 将报文分成 HEADERS 帧和 DATA 帧,它们都是二进制格式的。 -

+ -在通信过程中,只会有一个 TCP 连接存在,它承载了任意数量的双向数据流(Stream)。 +在通信过程中,只会有一个 TCP 连接存在,它承载了任意数量的双向数据流(Stream)。 -- 一个数据流(Stream)都有一个唯一标识符和可选的优先级信息,用于承载双向信息。 -- 消息(Message)是与逻辑请求或响应对应的完整的一系列帧。 -- 帧(Frame)是最小的通信单位,来自不同数据流的帧可以交错发送,然后再根据每个帧头的数据流标识符重新组装。 +- 一个数据流(Stream)都有一个唯一标识符和可选的优先级信息,用于承载双向信息。 +- 消息(Message)是与逻辑请求或响应对应的完整的一系列帧。 +- 帧(Frame)是最小的通信单位,来自不同数据流的帧可以交错发送,然后再根据每个帧头的数据流标识符重新组装。 -

+ -## 服务端推送 +## 服务端推送 -HTTP/2.0 在客户端请求一个资源时,会把相关的资源一起发送给客户端,客户端就不需要再次发起请求了。例如客户端请求 page.html 页面,服务端就把 script.js 和 style.css 等与之相关的资源一起发给客户端。 +HTTP/2.0 在客户端请求一个资源时,会把相关的资源一起发送给客户端,客户端就不需要再次发起请求了。例如客户端请求 page.html 页面,服务端就把 script.js 和 style.css 等与之相关的资源一起发给客户端。 -

+ -## 首部压缩 +## 首部压缩 -HTTP/1.1 的首部带有大量信息,而且每次都要重复发送。 +HTTP/1.1 的首部带有大量信息,而且每次都要重复发送。 -HTTP/2.0 要求客户端和服务器同时维护和更新一个包含之前见过的首部字段表,从而避免了重复传输。 +HTTP/2.0 要求客户端和服务器同时维护和更新一个包含之前见过的首部字段表,从而避免了重复传输。 -不仅如此,HTTP/2.0 也使用 Huffman 编码对首部字段进行压缩。 +不仅如此,HTTP/2.0 也使用 Huffman 编码对首部字段进行压缩。 -

+ -# 八、HTTP/1.1 新特性 +# 八、HTTP/1.1 新特性 详细内容请见上文 -- 默认是长连接 -- 支持流水线 -- 支持同时打开多个 TCP 连接 -- 支持虚拟主机 -- 新增状态码 100 -- 支持分块传输编码 -- 新增缓存处理指令 max-age +- 默认是长连接 +- 支持流水线 +- 支持同时打开多个 TCP 连接 +- 支持虚拟主机 +- 新增状态码 100 +- 支持分块传输编码 +- 新增缓存处理指令 max-age -# 九、GET 和 POST 比较 +# 九、GET 和 POST 比较 -## 作用 +## 作用 -GET 用于获取资源,而 POST 用于传输实体主体。 +GET 用于获取资源,而 POST 用于传输实体主体。 -## 参数 +## 参数 -GET 和 POST 的请求都能使用额外的参数,但是 GET 的参数是以查询字符串出现在 URL 中,而 POST 的参数存储在实体主体中。不能因为 POST 参数存储在实体主体中就认为它的安全性更高,因为照样可以通过一些抓包工具(Fiddler)查看。 +GET 和 POST 的请求都能使用额外的参数,但是 GET 的参数是以查询字符串出现在 URL 中,而 POST 的参数存储在实体主体中。不能因为 POST 参数存储在实体主体中就认为它的安全性更高,因为照样可以通过一些抓包工具(Fiddler)查看。 -因为 URL 只支持 ASCII 码,因此 GET 的参数中如果存在中文等字符就需要先进行编码。例如 `中文` 会转换为 `%E4%B8%AD%E6%96%87`,而空格会转换为 `%20`。POST 参考支持标准字符集。 +因为 URL 只支持 ASCII 码,因此 GET 的参数中如果存在中文等字符就需要先进行编码。例如 `中文` 会转换为 `%E4%B8%AD%E6%96%87`,而空格会转换为 `%20`。POST 参考支持标准字符集。 ``` -GET /test/demo_form.asp?name1=value1&name2=value2 HTTP/1.1 +GET /test/demo_form.asp?name1=value1&name2=value2 HTTP/1.1 ``` ``` -POST /test/demo_form.asp HTTP/1.1 -Host: w3schools.com +POST /test/demo_form.asp HTTP/1.1 +Host: w3schools.com name1=value1&name2=value2 ``` -## 安全 +## 安全 -安全的 HTTP 方法不会改变服务器状态,也就是说它只是可读的。 +安全的 HTTP 方法不会改变服务器状态,也就是说它只是可读的。 -GET 方法是安全的,而 POST 却不是,因为 POST 的目的是传送实体主体内容,这个内容可能是用户上传的表单数据,上传成功之后,服务器可能把这个数据存储到数据库中,因此状态也就发生了改变。 +GET 方法是安全的,而 POST 却不是,因为 POST 的目的是传送实体主体内容,这个内容可能是用户上传的表单数据,上传成功之后,服务器可能把这个数据存储到数据库中,因此状态也就发生了改变。 -安全的方法除了 GET 之外还有:HEAD、OPTIONS。 +安全的方法除了 GET 之外还有:HEAD、OPTIONS。 -不安全的方法除了 POST 之外还有 PUT、DELETE。 +不安全的方法除了 POST 之外还有 PUT、DELETE。 -## 幂等性 +## 幂等性 -幂等的 HTTP 方法,同样的请求被执行一次与连续执行多次的效果是一样的,服务器的状态也是一样的。换句话说就是,幂等方法不应该具有副作用(统计用途除外)。 +幂等的 HTTP 方法,同样的请求被执行一次与连续执行多次的效果是一样的,服务器的状态也是一样的。换句话说就是,幂等方法不应该具有副作用(统计用途除外)。 所有的安全方法也都是幂等的。 -在正确实现的条件下,GET,HEAD,PUT 和 DELETE 等方法都是幂等的,而 POST 方法不是。 +在正确实现的条件下,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 -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 不是幂等的,如果调用多次,就会增加多行记录: ``` -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 +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 是幂等的,即便不同的请求接收到的状态码不一样: ``` -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 +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 首部字段没有指定不进行缓存。 +- 请求报文的 HTTP 方法本身是可缓存的,包括 GET 和 HEAD,但是 PUT 和 DELETE 不可缓存,POST 在多数情况下不可缓存的。 +- 响应报文的状态码是可缓存的,包括:200, 203, 204, 206, 300, 301, 404, 405, 410, 414, and 501。 +- 响应报文的 Cache-Control 首部字段没有指定不进行缓存。 -## XMLHttpRequest +## XMLHttpRequest -为了阐述 POST 和 GET 的另一个区别,需要先了解 XMLHttpRequest: +为了阐述 POST 和 GET 的另一个区别,需要先了解 XMLHttpRequest: -> XMLHttpRequest 是一个 API,它为客户端提供了在客户端和服务器之间传输数据的功能。它提供了一个通过 URL 来获取数据的简单方式,并且不会使整个页面刷新。这使得网页只更新一部分页面而不会打扰到用户。XMLHttpRequest 在 AJAX 中被大量使用。 +> XMLHttpRequest 是一个 API,它为客户端提供了在客户端和服务器之间传输数据的功能。它提供了一个通过 URL 来获取数据的简单方式,并且不会使整个页面刷新。这使得网页只更新一部分页面而不会打扰到用户。XMLHttpRequest 在 AJAX 中被大量使用。 -- 在使用 XMLHttpRequest 的 POST 方法时,浏览器会先发送 Header 再发送 Data。但并不是所有浏览器会这么做,例如火狐就不会。 -- 而 GET 方法 Header 和 Data 会一起发送。 +- 在使用 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) +- 上野宣. 图解 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) +---bottom---CyC--- +![](pics/417cb02e-853d-4288-a36e-9161ded2c9fd_200.png) +![](pics/HTTP_RequestMessageExample.png) +![](pics/HTTP_ResponseMessageExample.png) +![](pics/dc00f70e-c5c8-4d20-baf1-2d70014a97e3.jpg) +![](pics/HTTP1_x_Connections.png) +![](pics/a314bb79-5b18-4e63-a976-3448bffa6f1b.png) +![](pics/2d09a847-b854-439c-9198-b29c65810944.png) +![](pics/ssl-offloading.jpg) +![](pics/7fffa4b8-b36d-471f-ad0c-a88ee763bb76.png) +![](pics/39ccb299-ee99-4dd1-b8b4-2f9ec9495cb4.png) +![](pics/How-HTTPS-Works.png) +![](pics/2017-06-11-ca.png) +![](pics/86e6a91d-a285-447a-9345-c5484b8d0c47.png) +![](pics/af198da1-2480-4043-b07f-a3b91a88b815.png) +![](pics/e3f1657c-80fc-4dfa-9643-bf51abd201c6.png) +![](pics/_u4E0B_u8F7D.png) diff --git a/docs/notes/Java IO.md b/docs/notes/Java IO.md index 55915c74..e5ccf766 100644 --- a/docs/notes/Java IO.md +++ b/docs/notes/Java IO.md @@ -1,319 +1,286 @@ - -* [一、概览](#一概览) -* [二、磁盘操作](#二磁盘操作) -* [三、字节操作](#三字节操作) - * [实现文件复制](#实现文件复制) - * [装饰者模式](#装饰者模式) -* [四、字符操作](#四字符操作) - * [编码与解码](#编码与解码) - * [String 的编码方式](#string-的编码方式) - * [Reader 与 Writer](#reader-与-writer) - * [实现逐行输出文本文件的内容](#实现逐行输出文本文件的内容) -* [五、对象操作](#五对象操作) - * [序列化](#序列化) - * [Serializable](#serializable) - * [transient](#transient) -* [六、网络操作](#六网络操作) - * [InetAddress](#inetaddress) - * [URL](#url) - * [Sockets](#sockets) - * [Datagram](#datagram) -* [七、NIO](#七nio) - * [流与块](#流与块) - * [通道与缓冲区](#通道与缓冲区) - * [缓冲区状态变量](#缓冲区状态变量) - * [文件 NIO 实例](#文件-nio-实例) - * [选择器](#选择器) - * [套接字 NIO 实例](#套接字-nio-实例) - * [内存映射文件](#内存映射文件) - * [对比](#对比) -* [八、参考资料](#八参考资料) - +# 一、概览 +Java 的 I/O 大概可以分成以下几类: -# 一、概览 +- 磁盘操作:File +- 字节操作:InputStream 和 OutputStream +- 字符操作:Reader 和 Writer +- 对象操作:Serializable +- 网络操作:Socket +- 新的输入/输出:NIO -Java 的 I/O 大概可以分成以下几类: +# 二、磁盘操作 -- 磁盘操作:File -- 字节操作:InputStream 和 OutputStream -- 字符操作:Reader 和 Writer -- 对象操作:Serializable -- 网络操作:Socket -- 新的输入/输出:NIO - -# 二、磁盘操作 - -File 类可以用于表示文件和目录的信息,但是它不表示文件的内容。 +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); - } +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。 +从 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); +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; +    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); - } +    // read() 最多读取 buffer.length 个字节 +    // 返回的是实际读取的个数 +    // 返回 -1 的时候表示读到 eof,即文件尾 +    while ((cnt = in.read(buffer, 0, buffer.length)) != -1) { +        out.write(buffer, 0, cnt); +    } - in.close(); - out.close(); +    in.close(); +    out.close(); } ``` -## 装饰者模式 +## 装饰者模式 -Java I/O 使用了装饰者模式来实现。以 InputStream 为例, +Java I/O 使用了装饰者模式来实现。以 InputStream 为例, -- InputStream 是抽象组件; -- FileInputStream 是 InputStream 的子类,属于具体组件,提供了字节流的输入操作; -- FilterInputStream 属于抽象装饰者,装饰者用于装饰组件,为组件提供额外的功能。例如 BufferedInputStream 为 FileInputStream 提供缓存的功能。 +- InputStream 是抽象组件; +- FileInputStream 是 InputStream 的子类,属于具体组件,提供了字节流的输入操作; +- FilterInputStream 属于抽象装饰者,装饰者用于装饰组件,为组件提供额外的功能。例如 BufferedInputStream 为 FileInputStream 提供缓存的功能。 -

+![](index_files/c2c2b633-c03a-426e-b436-5719a194667b.png) -实例化一个具有缓存功能的字节流对象时,只需要在 FileInputStream 对象上再套一层 BufferedInputStream 对象即可。 +实例化一个具有缓存功能的字节流对象时,只需要在 FileInputStream 对象上再套一层 BufferedInputStream 对象即可。 ```java -FileInputStream fileInputStream = new FileInputStream(filePath); -BufferedInputStream bufferedInputStream = new BufferedInputStream(fileInputStream); +FileInputStream fileInputStream = new FileInputStream(filePath); +BufferedInputStream bufferedInputStream = new BufferedInputStream(fileInputStream); ``` -DataInputStream 装饰者提供了对更多数据类型进行输入的操作,比如 int、double 等基本类型。 +DataInputStream 装饰者提供了对更多数据类型进行输入的操作,比如 int、double 等基本类型。 -# 四、字符操作 +# 四、字符操作 -## 编码与解码 +## 编码与解码 编码就是把字符转换为字节,而解码是把字节重新组合成字符。 如果编码和解码过程使用不同的编码方式那么就出现了乱码。 -- GBK 编码中,中文字符占 2 个字节,英文字符占 1 个字节; -- UTF-8 编码中,中文字符占 3 个字节,英文字符占 1 个字节; -- UTF-16be 编码中,中文字符和英文字符都占 2 个字节。 +- GBK 编码中,中文字符占 2 个字节,英文字符占 1 个字节; +- UTF-8 编码中,中文字符占 3 个字节,英文字符占 1 个字节; +- UTF-16be 编码中,中文字符和英文字符都占 2 个字节。 -UTF-16be 中的 be 指的是 Big Endian,也就是大端。相应地也有 UTF-16le,le 指的是 Little Endian,也就是小端。 +UTF-16be 中的 be 指的是 Big Endian,也就是大端。相应地也有 UTF-16le,le 指的是 Little Endian,也就是小端。 -Java 的内存编码使用双字节编码 UTF-16be,这不是指 Java 只支持这一种编码方式,而是说 char 这种类型使用 UTF-16be 进行编码。char 类型占 16 位,也就是两个字节,Java 使用这种双字节编码是为了让一个中文或者一个英文都能使用一个 char 来存储。 +Java 的内存编码使用双字节编码 UTF-16be,这不是指 Java 只支持这一种编码方式,而是说 char 这种类型使用 UTF-16be 进行编码。char 类型占 16 位,也就是两个字节,Java 使用这种双字节编码是为了让一个中文或者一个英文都能使用一个 char 来存储。 -## String 的编码方式 +## String 的编码方式 -String 可以看成一个字符序列,可以指定一个编码方式将它编码为字节序列,也可以指定一个编码方式将一个字节序列解码为 String。 +String 可以看成一个字符序列,可以指定一个编码方式将它编码为字节序列,也可以指定一个编码方式将一个字节序列解码为 String。 ```java -String str1 = "中文"; -byte[] bytes = str1.getBytes("UTF-8"); -String str2 = new String(bytes, "UTF-8"); +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。 +在调用无参数 getBytes() 方法时,默认的编码方式不是 UTF-16be。双字节编码的好处是可以使用一个 char 存储中文和英文,而将 String 转为 bytes[] 字节数组就不再需要这个好处,因此也就不再需要双字节编码。getBytes() 的默认编码方式与平台有关,一般为 UTF-8。 ```java -byte[] bytes = str1.getBytes(); +byte[] bytes = str1.getBytes(); ``` -## Reader 与 Writer +## Reader 与 Writer 不管是磁盘还是网络传输,最小的存储单元都是字节,而不是字符。但是在程序中操作的通常是字符形式的数据,因此需要提供对字符进行操作的方法。 -- InputStreamReader 实现从字节流解码成字符流; -- OutputStreamWriter 实现字符流编码成为字节流。 +- InputStreamReader 实现从字节流解码成字符流; +- OutputStreamWriter 实现字符流编码成为字节流。 -## 实现逐行输出文本文件的内容 +## 实现逐行输出文本文件的内容 ```java -public static void readFileContent(String filePath) throws IOException { +public static void readFileContent(String filePath) throws IOException { - FileReader fileReader = new FileReader(filePath); - BufferedReader bufferedReader = new BufferedReader(fileReader); +    FileReader fileReader = new FileReader(filePath); +    BufferedReader bufferedReader = new BufferedReader(fileReader); - String line; - while ((line = bufferedReader.readLine()) != null) { - System.out.println(line); - } +    String line; +    while ((line = bufferedReader.readLine()) != null) { +        System.out.println(line); +    } - // 装饰者模式使得 BufferedReader 组合了一个 Reader 对象 - // 在调用 BufferedReader 的 close() 方法时会去调用 Reader 的 close() 方法 - // 因此只要一个 close() 调用即可 - bufferedReader.close(); +    // 装饰者模式使得 BufferedReader 组合了一个 Reader 对象 +    // 在调用 BufferedReader 的 close() 方法时会去调用 Reader 的 close() 方法 +    // 因此只要一个 close() 调用即可 +    bufferedReader.close(); } ``` -# 五、对象操作 +# 五、对象操作 -## 序列化 +## 序列化 序列化就是将一个对象转换成字节序列,方便存储和传输。 -- 序列化:ObjectOutputStream.writeObject() -- 反序列化:ObjectInputStream.readObject() +- 序列化:ObjectOutputStream.writeObject() +- 反序列化:ObjectInputStream.readObject() 不会对静态变量进行序列化,因为序列化只是保存对象的状态,静态变量属于类的状态。 -## Serializable +## Serializable -序列化的类需要实现 Serializable 接口,它只是一个标准,没有任何方法需要实现,但是如果不去实现它的话而进行序列化,会抛出异常。 +序列化的类需要实现 Serializable 接口,它只是一个标准,没有任何方法需要实现,但是如果不去实现它的话而进行序列化,会抛出异常。 ```java -public static void main(String[] args) throws IOException, ClassNotFoundException { +public static void main(String[] args) throws IOException, ClassNotFoundException { - A a1 = new A(123, "abc"); - String objectFile = "file/a1"; +    A a1 = new A(123, "abc"); +    String objectFile = "file/a1"; - ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream(objectFile)); - objectOutputStream.writeObject(a1); - objectOutputStream.close(); +    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); +    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 static class A implements Serializable { - private int x; - private String y; +    private int x; +    private String y; - A(int x, String y) { - this.x = x; - this.y = y; - } +    A(int x, String y) { +        this.x = x; +        this.y = y; +    } - @Override - public String toString() { - return "x = " + x + " " + "y = " + y; - } +    @Override +    public String toString() { +        return "x = " + x + "  " + "y = " + y; +    } } ``` -## transient +## transient -transient 关键字可以使一些属性不会被序列化。 +transient 关键字可以使一些属性不会被序列化。 -ArrayList 中存储数据的数组 elementData 是用 transient 修饰的,因为这个数组是动态扩展的,并不是所有的空间都被使用,因此就不需要所有的内容都被序列化。通过重写序列化和反序列化方法,使得可以只序列化数组中有内容的那部分数据。 +ArrayList 中存储数据的数组 elementData 是用 transient 修饰的,因为这个数组是动态扩展的,并不是所有的空间都被使用,因此就不需要所有的内容都被序列化。通过重写序列化和反序列化方法,使得可以只序列化数组中有内容的那部分数据。 ```java -private transient Object[] elementData; +private transient Object[] elementData; ``` -# 六、网络操作 +# 六、网络操作 -Java 中的网络支持: +Java 中的网络支持: -- InetAddress:用于表示网络上的硬件资源,即 IP 地址; -- URL:统一资源定位符; -- Sockets:使用 TCP 协议实现网络通信; -- Datagram:使用 UDP 协议实现网络通信。 +- InetAddress:用于表示网络上的硬件资源,即 IP 地址; +- URL:统一资源定位符; +- Sockets:使用 TCP 协议实现网络通信; +- Datagram:使用 UDP 协议实现网络通信。 -## InetAddress +## InetAddress 没有公有的构造函数,只能通过静态方法来创建实例。 ```java -InetAddress.getByName(String host); -InetAddress.getByAddress(byte[] address); +InetAddress.getByName(String host); +InetAddress.getByAddress(byte[] address); ``` -## URL +## URL -可以直接从 URL 中读取字节流数据。 +可以直接从 URL 中读取字节流数据。 ```java -public static void main(String[] args) throws IOException { +public static void main(String[] args) throws IOException { - URL url = new URL("http://www.baidu.com"); +    URL url = new URL("http://www.baidu.com"); - /* 字节流 */ - InputStream is = url.openStream(); +    /* 字节流 */ +    InputStream is = url.openStream(); - /* 字符流 */ - InputStreamReader isr = new InputStreamReader(is, "utf-8"); +    /* 字符流 */ +    InputStreamReader isr = new InputStreamReader(is, "utf-8"); - /* 提供缓存功能 */ - BufferedReader br = new BufferedReader(isr); +    /* 提供缓存功能 */ +    BufferedReader br = new BufferedReader(isr); - String line; - while ((line = br.readLine()) != null) { - System.out.println(line); - } +    String line; +    while ((line = br.readLine()) != null) { +        System.out.println(line); +    } - br.close(); +    br.close(); } ``` -## Sockets +## Sockets -- ServerSocket:服务器端类 -- Socket:客户端类 -- 服务器和客户端通过 InputStream 和 OutputStream 进行输入输出。 +- ServerSocket:服务器端类 +- Socket:客户端类 +- 服务器和客户端通过 InputStream 和 OutputStream 进行输入输出。 -

+![](index_files/f77f06b6-7359-4066-b85d-3f6c09ddf425.jpg) -## Datagram +## Datagram -- DatagramSocket:通信类 -- DatagramPacket:数据包类 +- DatagramSocket:通信类 +- DatagramPacket:数据包类 -# 七、NIO +# 七、NIO -新的输入/输出 (NIO) 库是在 JDK 1.4 中引入的,弥补了原来的 I/O 的不足,提供了高速的、面向块的 I/O。 +新的输入/输出 (NIO) 库是在 JDK 1.4 中引入的,弥补了原来的 I/O 的不足,提供了高速的、面向块的 I/O。 -## 流与块 +## 流与块 -I/O 与 NIO 最重要的区别是数据打包和传输的方式,I/O 以流的方式处理数据,而 NIO 以块的方式处理数据。 +I/O 与 NIO 最重要的区别是数据打包和传输的方式,I/O 以流的方式处理数据,而 NIO 以块的方式处理数据。 -面向流的 I/O 一次处理一个字节数据:一个输入流产生一个字节数据,一个输出流消费一个字节数据。为流式数据创建过滤器非常容易,链接几个过滤器,以便每个过滤器只负责复杂处理机制的一部分。不利的一面是,面向流的 I/O 通常相当慢。 +面向流的 I/O 一次处理一个字节数据:一个输入流产生一个字节数据,一个输出流消费一个字节数据。为流式数据创建过滤器非常容易,链接几个过滤器,以便每个过滤器只负责复杂处理机制的一部分。不利的一面是,面向流的 I/O 通常相当慢。 -面向块的 I/O 一次处理一个数据块,按块处理数据比按流处理数据要快得多。但是面向块的 I/O 缺少一些面向流的 I/O 所具有的优雅性和简单性。 +面向块的 I/O 一次处理一个数据块,按块处理数据比按流处理数据要快得多。但是面向块的 I/O 缺少一些面向流的 I/O 所具有的优雅性和简单性。 -I/O 包和 NIO 已经很好地集成了,java.io.\* 已经以 NIO 为基础重新实现了,所以现在它可以利用 NIO 的一些特性。例如,java.io.\* 包中的一些类包含以块的形式读写数据的方法,这使得即使在面向流的系统中,处理速度也会更快。 +I/O 包和 NIO 已经很好地集成了,java.io.\* 已经以 NIO 为基础重新实现了,所以现在它可以利用 NIO 的一些特性。例如,java.io.\* 包中的一些类包含以块的形式读写数据的方法,这使得即使在面向流的系统中,处理速度也会更快。 -## 通道与缓冲区 +## 通道与缓冲区 -### 1. 通道 +### 1. 通道 -通道 Channel 是对原 I/O 包中的流的模拟,可以通过它读取和写入数据。 +通道 Channel 是对原 I/O 包中的流的模拟,可以通过它读取和写入数据。 -通道与流的不同之处在于,流只能在一个方向上移动(一个流必须是 InputStream 或者 OutputStream 的子类),而通道是双向的,可以用于读、写或者同时用于读写。 +通道与流的不同之处在于,流只能在一个方向上移动(一个流必须是 InputStream 或者 OutputStream 的子类),而通道是双向的,可以用于读、写或者同时用于读写。 通道包括以下类型: -- FileChannel:从文件中读写数据; -- DatagramChannel:通过 UDP 读写网络中数据; -- SocketChannel:通过 TCP 读写网络中数据; -- ServerSocketChannel:可以监听新进来的 TCP 连接,对每一个新进来的连接都会创建一个 SocketChannel。 +- FileChannel:从文件中读写数据; +- DatagramChannel:通过 UDP 读写网络中数据; +- SocketChannel:通过 TCP 读写网络中数据; +- ServerSocketChannel:可以监听新进来的 TCP 连接,对每一个新进来的连接都会创建一个 SocketChannel。 -### 2. 缓冲区 +### 2. 缓冲区 发送给一个通道的所有数据都必须首先放到缓冲区中,同样地,从通道中读取的任何数据都要先读到缓冲区中。也就是说,不会直接对通道进行读写数据,而是要先经过缓冲区。 @@ -321,300 +288,308 @@ I/O 包和 NIO 已经很好地集成了,java.io.\* 已经以 NIO 为基础重 缓冲区包括以下类型: -- ByteBuffer -- CharBuffer -- ShortBuffer -- IntBuffer -- LongBuffer -- FloatBuffer -- DoubleBuffer +- ByteBuffer +- CharBuffer +- ShortBuffer +- IntBuffer +- LongBuffer +- FloatBuffer +- DoubleBuffer -## 缓冲区状态变量 +## 缓冲区状态变量 -- capacity:最大容量; -- position:当前已经读写的字节数; -- limit:还可以读写的字节数。 +- capacity:最大容量; +- position:当前已经读写的字节数; +- limit:还可以读写的字节数。 状态变量的改变过程举例: -① 新建一个大小为 8 个字节的缓冲区,此时 position 为 0,而 limit = capacity = 8。capacity 变量不会改变,下面的讨论会忽略它。 +① 新建一个大小为 8 个字节的缓冲区,此时 position 为 0,而 limit = capacity = 8。capacity 变量不会改变,下面的讨论会忽略它。 -

+![](index_files/1bea398f-17a7-4f67-a90b-9e2d243eaa9a.png) -② 从输入通道中读取 5 个字节数据写入缓冲区中,此时 position 为 5,limit 保持不变。 +② 从输入通道中读取 5 个字节数据写入缓冲区中,此时 position 为 5,limit 保持不变。 -

+![](index_files/80804f52-8815-4096-b506-48eef3eed5c6.png) -③ 在将缓冲区的数据写到输出通道之前,需要先调用 flip() 方法,这个方法将 limit 设置为当前 position,并将 position 设置为 0。 +③ 在将缓冲区的数据写到输出通道之前,需要先调用 flip() 方法,这个方法将 limit 设置为当前 position,并将 position 设置为 0。 -

+![](index_files/952e06bd-5a65-4cab-82e4-dd1536462f38.png) -④ 从缓冲区中取 4 个字节到输出缓冲中,此时 position 设为 4。 +④ 从缓冲区中取 4 个字节到输出缓冲中,此时 position 设为 4。 -

+![](index_files/b5bdcbe2-b958-4aef-9151-6ad963cb28b4.png) -⑤ 最后需要调用 clear() 方法来清空缓冲区,此时 position 和 limit 都被设置为最初位置。 +⑤ 最后需要调用 clear() 方法来清空缓冲区,此时 position 和 limit 都被设置为最初位置。 -

+![](index_files/67bf5487-c45d-49b6-b9c0-a058d8c68902.png) -## 文件 NIO 实例 +## 文件 NIO 实例 -以下展示了使用 NIO 快速复制文件的实例: +以下展示了使用 NIO 快速复制文件的实例: ```java -public static void fastCopy(String src, String dist) throws IOException { +public static void fastCopy(String src, String dist) throws IOException { - /* 获得源文件的输入字节流 */ - FileInputStream fin = new FileInputStream(src); +    /* 获得源文件的输入字节流 */ +    FileInputStream fin = new FileInputStream(src); - /* 获取输入字节流的文件通道 */ - FileChannel fcin = fin.getChannel(); +    /* 获取输入字节流的文件通道 */ +    FileChannel fcin = fin.getChannel(); - /* 获取目标文件的输出字节流 */ - FileOutputStream fout = new FileOutputStream(dist); +    /* 获取目标文件的输出字节流 */ +    FileOutputStream fout = new FileOutputStream(dist); - /* 获取输出字节流的文件通道 */ - FileChannel fcout = fout.getChannel(); +    /* 获取输出字节流的文件通道 */ +    FileChannel fcout = fout.getChannel(); - /* 为缓冲区分配 1024 个字节 */ - ByteBuffer buffer = ByteBuffer.allocateDirect(1024); +    /* 为缓冲区分配 1024 个字节 */ +    ByteBuffer buffer = ByteBuffer.allocateDirect(1024); - while (true) { +    while (true) { - /* 从输入通道中读取数据到缓冲区中 */ - int r = fcin.read(buffer); +        /* 从输入通道中读取数据到缓冲区中 */ +        int r = fcin.read(buffer); - /* read() 返回 -1 表示 EOF */ - if (r == -1) { - break; - } +        /* read() 返回 -1 表示 EOF */ +        if (r == -1) { +            break; +        } - /* 切换读写 */ - buffer.flip(); +        /* 切换读写 */ +        buffer.flip(); - /* 把缓冲区的内容写入输出文件中 */ - fcout.write(buffer); +        /* 把缓冲区的内容写入输出文件中 */ +        fcout.write(buffer); - /* 清空缓冲区 */ - buffer.clear(); - } +        /* 清空缓冲区 */ +        buffer.clear(); +    } } ``` -## 选择器 +## 选择器 -NIO 常常被叫做非阻塞 IO,主要是因为 NIO 在网络通信中的非阻塞特性被广泛使用。 +NIO 常常被叫做非阻塞 IO,主要是因为 NIO 在网络通信中的非阻塞特性被广泛使用。 -NIO 实现了 IO 多路复用中的 Reactor 模型,一个线程 Thread 使用一个选择器 Selector 通过轮询的方式去监听多个通道 Channel 上的事件,从而让一个线程就可以处理多个事件。 +NIO 实现了 IO 多路复用中的 Reactor 模型,一个线程 Thread 使用一个选择器 Selector 通过轮询的方式去监听多个通道 Channel 上的事件,从而让一个线程就可以处理多个事件。 -通过配置监听的通道 Channel 为非阻塞,那么当 Channel 上的 IO 事件还未到达时,就不会进入阻塞状态一直等待,而是继续轮询其它 Channel,找到 IO 事件已经到达的 Channel 执行。 +通过配置监听的通道 Channel 为非阻塞,那么当 Channel 上的 IO 事件还未到达时,就不会进入阻塞状态一直等待,而是继续轮询其它 Channel,找到 IO 事件已经到达的 Channel 执行。 -因为创建和切换线程的开销很大,因此使用一个线程来处理多个事件而不是一个线程处理一个事件,对于 IO 密集型的应用具有很好地性能。 +因为创建和切换线程的开销很大,因此使用一个线程来处理多个事件而不是一个线程处理一个事件,对于 IO 密集型的应用具有很好地性能。 -应该注意的是,只有套接字 Channel 才能配置为非阻塞,而 FileChannel 不能,为 FileChannel 配置非阻塞也没有意义。 +应该注意的是,只有套接字 Channel 才能配置为非阻塞,而 FileChannel 不能,为 FileChannel 配置非阻塞也没有意义。 -

+![](index_files/8fdbb8f5-2cf8-41dc-b5f1-99d98abb52d9.jpg) -### 1. 创建选择器 +### 1. 创建选择器 ```java -Selector selector = Selector.open(); +Selector selector = Selector.open(); ``` -### 2. 将通道注册到选择器上 +### 2. 将通道注册到选择器上 ```java -ServerSocketChannel ssChannel = ServerSocketChannel.open(); +ServerSocketChannel ssChannel = ServerSocketChannel.open(); ssChannel.configureBlocking(false); -ssChannel.register(selector, SelectionKey.OP_ACCEPT); +ssChannel.register(selector, SelectionKey.OP_ACCEPT); ``` 通道必须配置为非阻塞模式,否则使用选择器就没有任何意义了,因为如果通道在某个事件上被阻塞,那么服务器就不能响应其它事件,必须等待这个事件处理完毕才能去处理其它事件,显然这和选择器的作用背道而驰。 在将通道注册到选择器上时,还需要指定要注册的具体事件,主要有以下几类: -- SelectionKey.OP_CONNECT -- SelectionKey.OP_ACCEPT -- SelectionKey.OP_READ -- SelectionKey.OP_WRITE +- SelectionKey.OP_CONNECT +- SelectionKey.OP_ACCEPT +- SelectionKey.OP_READ +- SelectionKey.OP_WRITE -它们在 SelectionKey 的定义如下: +它们在 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; +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; +int interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE; ``` -### 3. 监听事件 +### 3. 监听事件 ```java -int num = selector.select(); +int num = selector.select(); ``` -使用 select() 来监听到达的事件,它会一直阻塞直到有至少一个事件到达。 +使用 select() 来监听到达的事件,它会一直阻塞直到有至少一个事件到达。 -### 4. 获取到达的事件 +### 4. 获取到达的事件 ```java -Set keys = selector.selectedKeys(); -Iterator keyIterator = keys.iterator(); -while (keyIterator.hasNext()) { - SelectionKey key = keyIterator.next(); - if (key.isAcceptable()) { - // ... - } else if (key.isReadable()) { - // ... - } - keyIterator.remove(); +Set keys = selector.selectedKeys(); +Iterator keyIterator = keys.iterator(); +while (keyIterator.hasNext()) { +    SelectionKey key = keyIterator.next(); +    if (key.isAcceptable()) { +        // ... +    } else if (key.isReadable()) { +        // ... +    } +    keyIterator.remove(); } ``` -### 5. 事件循环 +### 5. 事件循环 -因为一次 select() 调用不能处理完所有的事件,并且服务器端有可能需要一直监听事件,因此服务器端处理事件的代码一般会放在一个死循环内。 +因为一次 select() 调用不能处理完所有的事件,并且服务器端有可能需要一直监听事件,因此服务器端处理事件的代码一般会放在一个死循环内。 ```java -while (true) { - int num = selector.select(); - Set keys = selector.selectedKeys(); - Iterator keyIterator = keys.iterator(); - while (keyIterator.hasNext()) { - SelectionKey key = keyIterator.next(); - if (key.isAcceptable()) { - // ... - } else if (key.isReadable()) { - // ... - } - keyIterator.remove(); - } +while (true) { +    int num = selector.select(); +    Set keys = selector.selectedKeys(); +    Iterator keyIterator = keys.iterator(); +    while (keyIterator.hasNext()) { +        SelectionKey key = keyIterator.next(); +        if (key.isAcceptable()) { +            // ... +        } else if (key.isReadable()) { +            // ... +        } +        keyIterator.remove(); +    } } ``` -## 套接字 NIO 实例 +## 套接字 NIO 实例 ```java -public class NIOServer { +public class NIOServer { - public static void main(String[] args) throws IOException { +    public static void main(String[] args) throws IOException { - Selector selector = Selector.open(); +        Selector selector = Selector.open(); - ServerSocketChannel ssChannel = ServerSocketChannel.open(); - ssChannel.configureBlocking(false); - ssChannel.register(selector, SelectionKey.OP_ACCEPT); +        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); +        ServerSocket serverSocket = ssChannel.socket(); +        InetSocketAddress address = new InetSocketAddress("127.0.0.1", 8888); +        serverSocket.bind(address); - while (true) { +        while (true) { - selector.select(); - Set keys = selector.selectedKeys(); - Iterator keyIterator = keys.iterator(); +            selector.select(); +            Set keys = selector.selectedKeys(); +            Iterator keyIterator = keys.iterator(); - while (keyIterator.hasNext()) { +            while (keyIterator.hasNext()) { - SelectionKey key = keyIterator.next(); +                SelectionKey key = keyIterator.next(); - if (key.isAcceptable()) { +                if (key.isAcceptable()) { - ServerSocketChannel ssChannel1 = (ServerSocketChannel) key.channel(); +                    ServerSocketChannel ssChannel1 = (ServerSocketChannel) key.channel(); - // 服务器会为每个新连接创建一个 SocketChannel - SocketChannel sChannel = ssChannel1.accept(); - sChannel.configureBlocking(false); +                    // 服务器会为每个新连接创建一个 SocketChannel +                    SocketChannel sChannel = ssChannel1.accept(); +                    sChannel.configureBlocking(false); - // 这个新连接主要用于从客户端读取数据 - sChannel.register(selector, SelectionKey.OP_READ); +                    // 这个新连接主要用于从客户端读取数据 +                    sChannel.register(selector, SelectionKey.OP_READ); - } else if (key.isReadable()) { +                } else if (key.isReadable()) { - SocketChannel sChannel = (SocketChannel) key.channel(); - System.out.println(readDataFromSocketChannel(sChannel)); - sChannel.close(); - } +                    SocketChannel sChannel = (SocketChannel) key.channel(); +                    System.out.println(readDataFromSocketChannel(sChannel)); +                    sChannel.close(); +                } - keyIterator.remove(); - } - } - } +                keyIterator.remove(); +            } +        } +    } - private static String readDataFromSocketChannel(SocketChannel sChannel) throws IOException { +    private static String readDataFromSocketChannel(SocketChannel sChannel) throws IOException { - ByteBuffer buffer = ByteBuffer.allocate(1024); - StringBuilder data = new StringBuilder(); +        ByteBuffer buffer = ByteBuffer.allocate(1024); +        StringBuilder data = new StringBuilder(); - while (true) { +        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(); - } +            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 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(); - } +    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 快得多。 +内存映射文件 I/O 是一种读和写文件数据的方法,它可以比常规的基于流或者基于通道的 I/O 快得多。 向内存映射文件写入可能是危险的,只是改变数组的单个元素这样的简单操作,就可能会直接修改磁盘上的文件。修改数据与将数据保存到磁盘是没有分开的。 -下面代码行将文件的前 1024 个字节映射到内存中,map() 方法返回一个 MappedByteBuffer,它是 ByteBuffer 的子类。因此,可以像使用其他任何 ByteBuffer 一样使用新映射的缓冲区,操作系统会在需要时负责执行映射。 +下面代码行将文件的前 1024 个字节映射到内存中,map() 方法返回一个 MappedByteBuffer,它是 ByteBuffer 的子类。因此,可以像使用其他任何 ByteBuffer 一样使用新映射的缓冲区,操作系统会在需要时负责执行映射。 ```java -MappedByteBuffer mbb = fc.map(FileChannel.MapMode.READ_WRITE, 0, 1024); +MappedByteBuffer mbb = fc.map(FileChannel.MapMode.READ_WRITE, 0, 1024); ``` -## 对比 +## 对比 -NIO 与普通 I/O 的区别主要有以下两点: +NIO 与普通 I/O 的区别主要有以下两点: -- NIO 是非阻塞的; -- 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) +- 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) +---bottom---CyC--- +![](index_files/DP-Decorator-java.io.png) +![](index_files/ClienteServidorSockets1521731145260.jpg) +![](index_files/1bea398f-17a7-4f67-a90b-9e2d243eaa9a.png) +![](index_files/4628274c-25b6-4053-97cf-d1239b44c43d.png) +![](index_files/952e06bd-5a65-4cab-82e4-dd1536462f38.png) +![](index_files/b5bdcbe2-b958-4aef-9151-6ad963cb28b4.png) +![](index_files/67bf5487-c45d-49b6-b9c0-a058d8c68902.png) diff --git a/docs/notes/Java 基础.md b/docs/notes/Java 基础.md index fcadcfb2..5e71e986 100644 --- a/docs/notes/Java 基础.md +++ b/docs/notes/Java 基础.md @@ -1,657 +1,615 @@ - -* [一、数据类型](#一数据类型) - * [基本类型](#基本类型) - * [包装类型](#包装类型) - * [缓存池](#缓存池) -* [二、String](#二string) - * [概览](#概览) - * [不可变的好处](#不可变的好处) - * [String, StringBuffer and StringBuilder](#string,-stringbuffer-and-stringbuilder) - * [String Pool](#string-pool) - * [new String("abc")](#new-string"abc") -* [三、运算](#三运算) - * [参数传递](#参数传递) - * [float 与 double](#float-与-double) - * [隐式类型转换](#隐式类型转换) - * [switch](#switch) -* [四、继承](#四继承) - * [访问权限](#访问权限) - * [抽象类与接口](#抽象类与接口) - * [super](#super) - * [重写与重载](#重写与重载) -* [五、Object 通用方法](#五object-通用方法) - * [概览](#概览) - * [equals()](#equals) - * [hashCode()](#hashcode) - * [toString()](#tostring) - * [clone()](#clone) -* [六、关键字](#六关键字) - * [final](#final) - * [static](#static) -* [七、反射](#七反射) -* [八、异常](#八异常) -* [九、泛型](#九泛型) -* [十、注解](#十注解) -* [十一、特性](#十一特性) - * [Java 各版本的新特性](#java-各版本的新特性) - * [Java 与 C++ 的区别](#java-与-c-的区别) - * [JRE or JDK](#jre-or-jdk) -* [参考资料](#参考资料) - +# 一、数据类型 +## 基本类型 -# 一、数据类型 +- byte/8 +- char/16 +- short/16 +- int/32 +- float/32 +- long/64 +- double/64 +- boolean/~ -## 基本类型 +boolean 只有两个值:true、false,可以使用 1 bit 来存储,但是具体大小没有明确规定。JVM 会在编译时期将 boolean 类型的数据转换为 int,使用 1 来表示 true,0 表示 false。JVM 并不直接支持 boolean 数组,而是使用 byte 数组来表示 int 数组来表示。 -- byte/8 -- char/16 -- short/16 -- int/32 -- float/32 -- long/64 -- double/64 -- boolean/\~ +- [Primitive Data Types](https://docs.oracle.com/javase/tutorial/java/nutsandbolts/datatypes.html) +- [The Java® Virtual Machine Specification](https://docs.oracle.com/javase/specs/jvms/se8/jvms8.pdf) -boolean 只有两个值:true、false,可以使用 1 bit 来存储,但是具体大小没有明确规定。JVM 会在编译时期将 boolean 类型的数据转换为 int,使用 1 来表示 true,0 表示 false。JVM 并不直接支持 boolean 数组,而是使用 byte 数组来表示 int 数组来表示。 - -- [Primitive Data Types](https://docs.oracle.com/javase/tutorial/java/nutsandbolts/datatypes.html) -- [The Java® Virtual Machine Specification](https://docs.oracle.com/javase/specs/jvms/se8/jvms8.pdf) - -## 包装类型 +## 包装类型 基本类型都有对应的包装类型,基本类型与其对应的包装类型之间的赋值使用自动装箱与拆箱完成。 ```java -Integer x = 2; // 装箱 -int y = x; // 拆箱 +Integer x = 2;     // 装箱 +int y = x;         // 拆箱 ``` -## 缓存池 +## 缓存池 -new Integer(123) 与 Integer.valueOf(123) 的区别在于: +new Integer(123) 与 Integer.valueOf(123) 的区别在于: -- new Integer(123) 每次都会新建一个对象; -- Integer.valueOf(123) 会使用缓存池中的对象,多次调用会取得同一个对象的引用。 +- new Integer(123) 每次都会新建一个对象; +- Integer.valueOf(123) 会使用缓存池中的对象,多次调用会取得同一个对象的引用。 ```java -Integer x = new Integer(123); -Integer y = new Integer(123); -System.out.println(x == y); // false -Integer z = Integer.valueOf(123); -Integer k = Integer.valueOf(123); -System.out.println(z == k); // true +Integer x = new Integer(123); +Integer y = new Integer(123); +System.out.println(x == y);    // false +Integer z = Integer.valueOf(123); +Integer k = Integer.valueOf(123); +System.out.println(z == k);   // true ``` -valueOf() 方法的实现比较简单,就是先判断值是否在缓存池中,如果在的话就直接返回缓存池的内容。 +valueOf() 方法的实现比较简单,就是先判断值是否在缓存池中,如果在的话就直接返回缓存池的内容。 ```java -public static Integer valueOf(int i) { - if (i >= IntegerCache.low && i <= IntegerCache.high) - return IntegerCache.cache[i + (-IntegerCache.low)]; - return new Integer(i); +public static Integer valueOf(int i) { +    if (i >= IntegerCache.low && i <= IntegerCache.high) +        return IntegerCache.cache[i + (-IntegerCache.low)]; +    return new Integer(i); } ``` -在 Java 8 中,Integer 缓存池的大小默认为 -128\~127。 +在 Java 8 中,Integer 缓存池的大小默认为 -128~127。 ```java -static final int low = -128; -static final int high; -static final Integer cache[]; +static final int low = -128; +static final int high; +static final Integer cache[]; -static { - // high value may be configured by property - int h = 127; - String integerCacheHighPropValue = - sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high"); - if (integerCacheHighPropValue != null) { - try { - int i = parseInt(integerCacheHighPropValue); - i = Math.max(i, 127); - // Maximum array size is Integer.MAX_VALUE - h = Math.min(i, Integer.MAX_VALUE - (-low) -1); - } catch( NumberFormatException nfe) { - // If the property cannot be parsed into an int, ignore it. - } - } - high = h; +static { +    // high value may be configured by property +    int h = 127; +    String integerCacheHighPropValue = +        sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high"); +    if (integerCacheHighPropValue != null) { +        try { +            int i = parseInt(integerCacheHighPropValue); +            i = Math.max(i, 127); +            // Maximum array size is Integer.MAX_VALUE +            h = Math.min(i, Integer.MAX_VALUE - (-low) -1); +        } catch( NumberFormatException nfe) { +            // If the property cannot be parsed into an int, ignore it. +        } +    } +    high = h; - cache = new Integer[(high - low) + 1]; - int j = low; - for(int k = 0; k < cache.length; k++) - cache[k] = new Integer(j++); +    cache = new Integer[(high - low) + 1]; +    int j = low; +    for(int k = 0; k < cache.length; k++) +        cache[k] = new Integer(j++); - // range [-128, 127] must be interned (JLS7 5.1.7) - assert IntegerCache.high >= 127; +    // range [-128, 127] must be interned (JLS7 5.1.7) +    assert IntegerCache.high >= 127; } ``` -编译器会在自动装箱过程调用 valueOf() 方法,因此多个 Integer 实例使用自动装箱来创建并且值相同,那么就会引用相同的对象。 +编译器会在自动装箱过程调用 valueOf() 方法,因此多个 Integer 实例使用自动装箱来创建并且值相同,那么就会引用相同的对象。 ```java -Integer m = 123; -Integer n = 123; -System.out.println(m == n); // true +Integer m = 123; +Integer n = 123; +System.out.println(m == n); // true ``` 基本类型对应的缓冲池如下: -- boolean values true and false -- all byte values -- short values between -128 and 127 -- int values between -128 and 127 -- char in the range \u0000 to \u007F +- boolean values true and false +- all byte values +- short values between -128 and 127 +- int values between -128 and 127 +- char in the range \u0000 to \u007F 在使用这些基本类型对应的包装类型时,就可以直接使用缓冲池中的对象。 -[StackOverflow : Differences between new Integer(123), Integer.valueOf(123) and just 123 +[StackOverflow : Differences between new Integer(123), Integer.valueOf(123) and just 123 ](https://stackoverflow.com/questions/9030817/differences-between-new-integer123-integer-valueof123-and-just-123) -# 二、String +# 二、String -## 概览 +## 概览 -String 被声明为 final,因此它不可被继承。 +String 被声明为 final,因此它不可被继承。 -在 Java 8 中,String 内部使用 char 数组存储数据。 +在 Java 8 中,String 内部使用 char 数组存储数据。 ```java -public final class String - implements java.io.Serializable, Comparable, CharSequence { - /** The value is used for character storage. */ - private final char value[]; +public final class String +    implements java.io.Serializable, Comparable, CharSequence { +    /** The value is used for character storage. */ +    private final char value[]; } ``` -在 Java 9 之后,String 类的实现改用 byte 数组存储字符串,同时使用 `coder` 来标识使用了哪种编码。 +在 Java 9 之后,String 类的实现改用 byte 数组存储字符串,同时使用 `coder` 来标识使用了哪种编码。 ```java -public final class String - implements java.io.Serializable, Comparable, CharSequence { - /** The value is used for character storage. */ - private final byte[] value; +public final class String +    implements java.io.Serializable, Comparable, CharSequence { +    /** The value is used for character storage. */ +    private final byte[] value; - /** The identifier of the encoding used to encode the bytes in {@code value}. */ - private final byte coder; +    /** The identifier of the encoding used to encode the bytes in {@code value}. */ +    private final byte coder; } ``` -value 数组被声明为 final,这意味着 value 数组初始化之后就不能再引用其它数组。并且 String 内部没有改变 value 数组的方法,因此可以保证 String 不可变。 +value 数组被声明为 final,这意味着 value 数组初始化之后就不能再引用其它数组。并且 String 内部没有改变 value 数组的方法,因此可以保证 String 不可变。 -## 不可变的好处 +## 不可变的好处 -**1. 可以缓存 hash 值** +**1. 可以缓存 hash 值** -因为 String 的 hash 值经常被使用,例如 String 用做 HashMap 的 key。不可变的特性可以使得 hash 值也不可变,因此只需要进行一次计算。 +因为 String 的 hash 值经常被使用,例如 String 用做 HashMap 的 key。不可变的特性可以使得 hash 值也不可变,因此只需要进行一次计算。 -**2. String Pool 的需要** +**2. String Pool 的需要** -如果一个 String 对象已经被创建过了,那么就会从 String Pool 中取得引用。只有 String 是不可变的,才可能使用 String Pool。 +如果一个 String 对象已经被创建过了,那么就会从 String Pool 中取得引用。只有 String 是不可变的,才可能使用 String Pool。 -

+![](index_files/474e5579-38b1-47d2-8f76-a13ae086b039.jpg) -**3. 安全性** +**3. 安全性** -String 经常作为参数,String 不可变性可以保证参数不可变。例如在作为网络连接参数的情况下如果 String 是可变的,那么在网络连接过程中,String 被改变,改变 String 对象的那一方以为现在连接的是其它主机,而实际情况却不一定是。 +String 经常作为参数,String 不可变性可以保证参数不可变。例如在作为网络连接参数的情况下如果 String 是可变的,那么在网络连接过程中,String 被改变,改变 String 对象的那一方以为现在连接的是其它主机,而实际情况却不一定是。 -**4. 线程安全** +**4. 线程安全** -String 不可变性天生具备线程安全,可以在多个线程中安全地使用。 +String 不可变性天生具备线程安全,可以在多个线程中安全地使用。 -[Program Creek : Why String is immutable in Java?](https://www.programcreek.com/2013/04/why-string-is-immutable-in-java/) +[Program Creek : Why String is immutable in Java?](https://www.programcreek.com/2013/04/why-string-is-immutable-in-java/) -## String, StringBuffer and StringBuilder +## String, StringBuffer and StringBuilder -**1. 可变性** +**1. 可变性** -- String 不可变 -- StringBuffer 和 StringBuilder 可变 +- String 不可变 +- StringBuffer 和 StringBuilder 可变 -**2. 线程安全** +**2. 线程安全** -- String 不可变,因此是线程安全的 -- StringBuilder 不是线程安全的 -- StringBuffer 是线程安全的,内部使用 synchronized 进行同步 +- String 不可变,因此是线程安全的 +- StringBuilder 不是线程安全的 +- StringBuffer 是线程安全的,内部使用 synchronized 进行同步 -[StackOverflow : String, StringBuffer, and StringBuilder](https://stackoverflow.com/questions/2971315/string-stringbuffer-and-stringbuilder) +[StackOverflow : String, StringBuffer, and StringBuilder](https://stackoverflow.com/questions/2971315/string-stringbuffer-and-stringbuilder) -## String Pool +## String Pool -字符串常量池(String Pool)保存着所有字符串字面量(literal strings),这些字面量在编译时期就确定。不仅如此,还可以使用 String 的 intern() 方法在运行过程中将字符串添加到 String Pool 中。 +字符串常量池(String Pool)保存着所有字符串字面量(literal strings),这些字面量在编译时期就确定。不仅如此,还可以使用 String 的 intern() 方法在运行过程中将字符串添加到 String Pool 中。 -当一个字符串调用 intern() 方法时,如果 String Pool 中已经存在一个字符串和该字符串值相等(使用 equals() 方法进行确定),那么就会返回 String Pool 中字符串的引用;否则,就会在 String Pool 中添加一个新的字符串,并返回这个新字符串的引用。 +当一个字符串调用 intern() 方法时,如果 String Pool 中已经存在一个字符串和该字符串值相等(使用 equals() 方法进行确定),那么就会返回 String Pool 中字符串的引用;否则,就会在 String Pool 中添加一个新的字符串,并返回这个新字符串的引用。 -下面示例中,s1 和 s2 采用 new String() 的方式新建了两个不同字符串,而 s3 和 s4 是通过 s1.intern() 方法取得一个字符串引用。intern() 首先把 s1 引用的字符串放到 String Pool 中,然后返回这个字符串引用。因此 s3 和 s4 引用的是同一个字符串。 +下面示例中,s1 和 s2 采用 new String() 的方式新建了两个不同字符串,而 s3 和 s4 是通过 s1.intern() 方法取得一个字符串引用。intern() 首先把 s1 引用的字符串放到 String Pool 中,然后返回这个字符串引用。因此 s3 和 s4 引用的是同一个字符串。 ```java -String s1 = new String("aaa"); -String s2 = new String("aaa"); -System.out.println(s1 == s2); // false -String s3 = s1.intern(); -String s4 = s1.intern(); -System.out.println(s3 == s4); // true +String s1 = new String("aaa"); +String s2 = new String("aaa"); +System.out.println(s1 == s2);           // false +String s3 = s1.intern(); +String s4 = s1.intern(); +System.out.println(s3 == s4);           // true ``` -如果是采用 "bbb" 这种字面量的形式创建字符串,会自动地将字符串放入 String Pool 中。 +如果是采用 "bbb" 这种字面量的形式创建字符串,会自动地将字符串放入 String Pool 中。 ```java -String s5 = "bbb"; -String s6 = "bbb"; -System.out.println(s5 == s6); // true +String s5 = "bbb"; +String s6 = "bbb"; +System.out.println(s5 == s6);  // true ``` -在 Java 7 之前,String Pool 被放在运行时常量池中,它属于永久代。而在 Java 7,String Pool 被移到堆中。这是因为永久代的空间有限,在大量使用字符串的场景下会导致 OutOfMemoryError 错误。 +在 Java 7 之前,String Pool 被放在运行时常量池中,它属于永久代。而在 Java 7,String Pool 被移到堆中。这是因为永久代的空间有限,在大量使用字符串的场景下会导致 OutOfMemoryError 错误。 -- [StackOverflow : What is String interning?](https://stackoverflow.com/questions/10578984/what-is-string-interning) -- [深入解析 String#intern](https://tech.meituan.com/in_depth_understanding_string_intern.html) +- [StackOverflow : What is String interning?](https://stackoverflow.com/questions/10578984/what-is-string-interning) +- [深入解析 String#intern](https://tech.meituan.com/in_depth_understanding_string_intern.html) -## new String("abc") +## new String("abc") -使用这种方式一共会创建两个字符串对象(前提是 String Pool 中还没有 "abc" 字符串对象)。 +使用这种方式一共会创建两个字符串对象(前提是 String Pool 中还没有 "abc" 字符串对象)。 -- "abc" 属于字符串字面量,因此编译时期会在 String Pool 中创建一个字符串对象,指向这个 "abc" 字符串字面量; -- 而使用 new 的方式会在堆中创建一个字符串对象。 +- "abc" 属于字符串字面量,因此编译时期会在 String Pool 中创建一个字符串对象,指向这个 "abc" 字符串字面量; +- 而使用 new 的方式会在堆中创建一个字符串对象。 -创建一个测试类,其 main 方法中使用这种方式来创建字符串对象。 +创建一个测试类,其 main 方法中使用这种方式来创建字符串对象。 ```java -public class NewStringTest { - public static void main(String[] args) { - String s = new String("abc"); - } +public class NewStringTest { +    public static void main(String[] args) { +        String s = new String("abc"); +    } } ``` -使用 javap -verbose 进行反编译,得到以下内容: +使用 javap -verbose 进行反编译,得到以下内容: ```java -// ... -Constant pool: -// ... - #2 = Class #18 // java/lang/String - #3 = String #19 // abc -// ... - #18 = Utf8 java/lang/String - #19 = Utf8 abc -// ... +// ... +Constant pool: +// ... +   #2 = Class              #18            // java/lang/String +   #3 = String             #19            // abc +// ... +  #18 = Utf8               java/lang/String +  #19 = Utf8               abc +// ... - public static void main(java.lang.String[]); - descriptor: ([Ljava/lang/String;)V - flags: ACC_PUBLIC, ACC_STATIC - Code: - stack=3, locals=2, args_size=1 - 0: new #2 // class java/lang/String - 3: dup - 4: ldc #3 // String abc - 6: invokespecial #4 // Method java/lang/String."":(Ljava/lang/String;)V - 9: astore_1 -// ... +  public static void main(java.lang.String[]); +    descriptor: ([Ljava/lang/String;)V +    flags: ACC_PUBLIC, ACC_STATIC +    Code: +      stack=3, locals=2, args_size=1 +         0: new           #2                  // class java/lang/String +         3: dup +         4: ldc           #3                  // String abc +         6: invokespecial #4                  // Method java/lang/String."":(Ljava/lang/String;)V +         9: astore_1 +// ... ``` -在 Constant Pool 中,#19 存储这字符串字面量 "abc",#3 是 String Pool 的字符串对象,它指向 #19 这个字符串字面量。在 main 方法中,0: 行使用 new #2 在堆中创建一个字符串对象,并且使用 ldc #3 将 String Pool 中的字符串对象作为 String 构造函数的参数。 +在 Constant Pool 中,#19 存储这字符串字面量 "abc",#3 是 String Pool 的字符串对象,它指向 #19 这个字符串字面量。在 main 方法中,0: 行使用 new #2 在堆中创建一个字符串对象,并且使用 ldc #3 将 String Pool 中的字符串对象作为 String 构造函数的参数。 -以下是 String 构造函数的源码,可以看到,在将一个字符串对象作为另一个字符串对象的构造函数参数时,并不会完全复制 value 数组内容,而是都会指向同一个 value 数组。 +以下是 String 构造函数的源码,可以看到,在将一个字符串对象作为另一个字符串对象的构造函数参数时,并不会完全复制 value 数组内容,而是都会指向同一个 value 数组。 ```java -public String(String original) { - this.value = original.value; - this.hash = original.hash; +public String(String original) { +    this.value = original.value; +    this.hash = original.hash; } ``` -# 三、运算 +# 三、运算 -## 参数传递 +## 参数传递 -Java 的参数是以值传递的形式传入方法中,而不是引用传递。 +Java 的参数是以值传递的形式传入方法中,而不是引用传递。 -以下代码中 Dog dog 的 dog 是一个指针,存储的是对象的地址。在将一个参数传入一个方法时,本质上是将对象的地址以值的方式传递到形参中。因此在方法中使指针引用其它对象,那么这两个指针此时指向的是完全不同的对象,在一方改变其所指向对象的内容时对另一方没有影响。 +以下代码中 Dog dog 的 dog 是一个指针,存储的是对象的地址。在将一个参数传入一个方法时,本质上是将对象的地址以值的方式传递到形参中。因此在方法中使指针引用其它对象,那么这两个指针此时指向的是完全不同的对象,在一方改变其所指向对象的内容时对另一方没有影响。 ```java -public class Dog { +public class Dog { - String name; +    String name; - Dog(String name) { - this.name = name; - } +    Dog(String name) { +        this.name = name; +    } - String getName() { - return this.name; - } +    String getName() { +        return this.name; +    } - void setName(String name) { - this.name = name; - } +    void setName(String name) { +        this.name = name; +    } - String getObjectAddress() { - return super.toString(); - } +    String getObjectAddress() { +        return super.toString(); +    } } ``` ```java -public class PassByValueExample { - public static void main(String[] args) { - Dog dog = new Dog("A"); - System.out.println(dog.getObjectAddress()); // Dog@4554617c - func(dog); - System.out.println(dog.getObjectAddress()); // Dog@4554617c - System.out.println(dog.getName()); // A - } +public class PassByValueExample { +    public static void main(String[] args) { +        Dog dog = new Dog("A"); +        System.out.println(dog.getObjectAddress()); // Dog@4554617c +        func(dog); +        System.out.println(dog.getObjectAddress()); // Dog@4554617c +        System.out.println(dog.getName());          // A +    } - private static void func(Dog dog) { - System.out.println(dog.getObjectAddress()); // Dog@4554617c - dog = new Dog("B"); - System.out.println(dog.getObjectAddress()); // Dog@74a14482 - System.out.println(dog.getName()); // B - } +    private static void func(Dog dog) { +        System.out.println(dog.getObjectAddress()); // Dog@4554617c +        dog = new Dog("B"); +        System.out.println(dog.getObjectAddress()); // Dog@74a14482 +        System.out.println(dog.getName());          // B +    } } ``` 如果在方法中改变对象的字段值会改变原对象该字段值,因为改变的是同一个地址指向的内容。 ```java -class PassByValueExample { - public static void main(String[] args) { - Dog dog = new Dog("A"); - func(dog); - System.out.println(dog.getName()); // B - } +class PassByValueExample { +    public static void main(String[] args) { +        Dog dog = new Dog("A"); +        func(dog); +        System.out.println(dog.getName());          // B +    } - private static void func(Dog dog) { - dog.setName("B"); - } +    private static void func(Dog dog) { +        dog.setName("B"); +    } } ``` -[StackOverflow: Is Java “pass-by-reference” or “pass-by-value”?](https://stackoverflow.com/questions/40480/is-java-pass-by-reference-or-pass-by-value) +[StackOverflow: Is Java “pass-by-reference” or “pass-by-value”?](https://stackoverflow.com/questions/40480/is-java-pass-by-reference-or-pass-by-value) -## float 与 double +## float 与 double -Java 不能隐式执行向下转型,因为这会使得精度降低。 +Java 不能隐式执行向下转型,因为这会使得精度降低。 -1.1 字面量属于 double 类型,不能直接将 1.1 直接赋值给 float 变量,因为这是向下转型。 +1.1 字面量属于 double 类型,不能直接将 1.1 直接赋值给 float 变量,因为这是向下转型。 ```java -// float f = 1.1; +// float f = 1.1; ``` -1.1f 字面量才是 float 类型。 +1.1f 字面量才是 float 类型。 ```java -float f = 1.1f; +float f = 1.1f; ``` -## 隐式类型转换 +## 隐式类型转换 -因为字面量 1 是 int 类型,它比 short 类型精度要高,因此不能隐式地将 int 类型下转型为 short 类型。 +因为字面量 1 是 int 类型,它比 short 类型精度要高,因此不能隐式地将 int 类型下转型为 short 类型。 ```java -short s1 = 1; -// s1 = s1 + 1; +short s1 = 1; +// s1 = s1 + 1; ``` -但是使用 += 或者 ++ 运算符可以执行隐式类型转换。 +但是使用 += 或者 ++ 运算符可以执行隐式类型转换。 ```java -s1 += 1; -// s1++; +s1 += 1; +// s1++; ``` -上面的语句相当于将 s1 + 1 的计算结果进行了向下转型: +上面的语句相当于将 s1 + 1 的计算结果进行了向下转型: ```java -s1 = (short) (s1 + 1); +s1 = (short) (s1 + 1); ``` -[StackOverflow : Why don't Java's +=, -=, *=, /= compound assignment operators require casting?](https://stackoverflow.com/questions/8710619/why-dont-javas-compound-assignment-operators-require-casting) +[StackOverflow : Why don't Java's +=, -=, *=, /= compound assignment operators require casting?](https://stackoverflow.com/questions/8710619/why-dont-javas-compound-assignment-operators-require-casting) -## switch +## switch -从 Java 7 开始,可以在 switch 条件判断语句中使用 String 对象。 +从 Java 7 开始,可以在 switch 条件判断语句中使用 String 对象。 ```java -String s = "a"; -switch (s) { - case "a": - System.out.println("aaa"); - break; - case "b": - System.out.println("bbb"); - break; +String s = "a"; +switch (s) { +    case "a": +        System.out.println("aaa"); +        break; +    case "b": +        System.out.println("bbb"); +        break; } ``` -switch 不支持 long,是因为 switch 的设计初衷是对那些只有少数的几个值进行等值判断,如果值过于复杂,那么还是用 if 比较合适。 +switch 不支持 long,是因为 switch 的设计初衷是对那些只有少数的几个值进行等值判断,如果值过于复杂,那么还是用 if 比较合适。 ```java -// long x = 111; -// switch (x) { // Incompatible types. Found: 'long', required: 'char, byte, short, int, Character, Byte, Short, Integer, String, or an enum' -// case 111: -// System.out.println(111); -// break; -// case 222: -// System.out.println(222); -// break; -// } +// long x = 111; +// switch (x) { // Incompatible types. Found: 'long', required: 'char, byte, short, int, Character, Byte, Short, Integer, String, or an enum' +//     case 111: +//         System.out.println(111); +//         break; +//     case 222: +//         System.out.println(222); +//         break; +// } ``` -[StackOverflow : Why can't your switch statement data type be long, Java?](https://stackoverflow.com/questions/2676210/why-cant-your-switch-statement-data-type-be-long-java) +[StackOverflow : Why can't your switch statement data type be long, Java?](https://stackoverflow.com/questions/2676210/why-cant-your-switch-statement-data-type-be-long-java) -# 四、继承 +# 四、继承 -## 访问权限 +## 访问权限 -Java 中有三个访问权限修饰符:private、protected 以及 public,如果不加访问修饰符,表示包级可见。 +Java 中有三个访问权限修饰符:private、protected 以及 public,如果不加访问修饰符,表示包级可见。 可以对类或类中的成员(字段以及方法)加上访问修饰符。 -- 类可见表示其它类可以用这个类创建实例对象。 -- 成员可见表示其它类可以用这个类的实例对象访问到该成员; +- 类可见表示其它类可以用这个类创建实例对象。 +- 成员可见表示其它类可以用这个类的实例对象访问到该成员; -protected 用于修饰成员,表示在继承体系中成员对于子类可见,但是这个访问修饰符对于类没有意义。 +protected 用于修饰成员,表示在继承体系中成员对于子类可见,但是这个访问修饰符对于类没有意义。 -设计良好的模块会隐藏所有的实现细节,把它的 API 与它的实现清晰地隔离开来。模块之间只通过它们的 API 进行通信,一个模块不需要知道其他模块的内部工作情况,这个概念被称为信息隐藏或封装。因此访问权限应当尽可能地使每个类或者成员不被外界访问。 +设计良好的模块会隐藏所有的实现细节,把它的 API 与它的实现清晰地隔离开来。模块之间只通过它们的 API 进行通信,一个模块不需要知道其他模块的内部工作情况,这个概念被称为信息隐藏或封装。因此访问权限应当尽可能地使每个类或者成员不被外界访问。 如果子类的方法重写了父类的方法,那么子类中该方法的访问级别不允许低于父类的访问级别。这是为了确保可以使用父类实例的地方都可以使用子类实例,也就是确保满足里氏替换原则。 -字段决不能是公有的,因为这么做的话就失去了对这个字段修改行为的控制,客户端可以对其随意修改。例如下面的例子中,AccessExample 拥有 id 公有字段,如果在某个时刻,我们想要使用 int 存储 id 字段,那么就需要修改所有的客户端代码。 +字段决不能是公有的,因为这么做的话就失去了对这个字段修改行为的控制,客户端可以对其随意修改。例如下面的例子中,AccessExample 拥有 id 公有字段,如果在某个时刻,我们想要使用 int 存储 id 字段,那么就需要修改所有的客户端代码。 ```java -public class AccessExample { - public String id; +public class AccessExample { +    public String id; } ``` -可以使用公有的 getter 和 setter 方法来替换公有字段,这样的话就可以控制对字段的修改行为。 +可以使用公有的 getter 和 setter 方法来替换公有字段,这样的话就可以控制对字段的修改行为。 ```java -public class AccessExample { +public class AccessExample { - private int id; +    private int id; - public String getId() { - return id + ""; - } +    public String getId() { +        return id + ""; +    } - public void setId(String id) { - this.id = Integer.valueOf(id); - } +    public void setId(String id) { +        this.id = Integer.valueOf(id); +    } } ``` 但是也有例外,如果是包级私有的类或者私有的嵌套类,那么直接暴露成员不会有特别大的影响。 ```java -public class AccessWithInnerClassExample { +public class AccessWithInnerClassExample { - private class InnerClass { - int x; - } +    private class InnerClass { +        int x; +    } - private InnerClass innerClass; +    private InnerClass innerClass; - public AccessWithInnerClassExample() { - innerClass = new InnerClass(); - } +    public AccessWithInnerClassExample() { +        innerClass = new InnerClass(); +    } - public int getValue() { - return innerClass.x; // 直接访问 - } +    public int getValue() { +        return innerClass.x;  // 直接访问 +    } } ``` -## 抽象类与接口 +## 抽象类与接口 -**1. 抽象类** +**1. 抽象类** -抽象类和抽象方法都使用 abstract 关键字进行声明。抽象类一般会包含抽象方法,抽象方法一定位于抽象类中。 +抽象类和抽象方法都使用 abstract 关键字进行声明。抽象类一般会包含抽象方法,抽象方法一定位于抽象类中。 抽象类和普通类最大的区别是,抽象类不能被实例化,需要继承抽象类才能实例化其子类。 ```java -public abstract class AbstractClassExample { +public abstract class AbstractClassExample { - protected int x; - private int y; +    protected int x; +    private int y; - public abstract void func1(); +    public abstract void func1(); - public void func2() { - System.out.println("func2"); - } +    public void func2() { +        System.out.println("func2"); +    } } ``` ```java -public class AbstractExtendClassExample extends AbstractClassExample { - @Override - public void func1() { - System.out.println("func1"); - } +public class AbstractExtendClassExample extends AbstractClassExample { +    @Override +    public void func1() { +        System.out.println("func1"); +    } } ``` ```java -// AbstractClassExample ac1 = new AbstractClassExample(); // 'AbstractClassExample' is abstract; cannot be instantiated -AbstractClassExample ac2 = new AbstractExtendClassExample(); +// AbstractClassExample ac1 = new AbstractClassExample(); // 'AbstractClassExample' is abstract; cannot be instantiated +AbstractClassExample ac2 = new AbstractExtendClassExample(); ac2.func1(); ``` -**2. 接口** +**2. 接口** -接口是抽象类的延伸,在 Java 8 之前,它可以看成是一个完全抽象的类,也就是说它不能有任何的方法实现。 +接口是抽象类的延伸,在 Java 8 之前,它可以看成是一个完全抽象的类,也就是说它不能有任何的方法实现。 -从 Java 8 开始,接口也可以拥有默认的方法实现,这是因为不支持默认方法的接口的维护成本太高了。在 Java 8 之前,如果一个接口想要添加新的方法,那么要修改所有实现了该接口的类。 +从 Java 8 开始,接口也可以拥有默认的方法实现,这是因为不支持默认方法的接口的维护成本太高了。在 Java 8 之前,如果一个接口想要添加新的方法,那么要修改所有实现了该接口的类。 -接口的成员(字段 + 方法)默认都是 public 的,并且不允许定义为 private 或者 protected。 +接口的成员(字段 + 方法)默认都是 public 的,并且不允许定义为 private 或者 protected。 -接口的字段默认都是 static 和 final 的。 +接口的字段默认都是 static 和 final 的。 ```java -public interface InterfaceExample { +public interface InterfaceExample { - void func1(); +    void func1(); - default void func2(){ - System.out.println("func2"); - } +    default void func2(){ +        System.out.println("func2"); +    } - int x = 123; - // int y; // Variable 'y' might not have been initialized - public int z = 0; // Modifier 'public' is redundant for interface fields - // private int k = 0; // Modifier 'private' not allowed here - // protected int l = 0; // Modifier 'protected' not allowed here - // private void fun3(); // Modifier 'private' not allowed here +    int x = 123; +    // int y;               // Variable 'y' might not have been initialized +    public int z = 0;       // Modifier 'public' is redundant for interface fields +    // private int k = 0;   // Modifier 'private' not allowed here +    // protected int l = 0; // Modifier 'protected' not allowed here +    // private void fun3(); // Modifier 'private' not allowed here } ``` ```java -public class InterfaceImplementExample implements InterfaceExample { - @Override - public void func1() { - System.out.println("func1"); - } +public class InterfaceImplementExample implements InterfaceExample { +    @Override +    public void func1() { +        System.out.println("func1"); +    } } ``` ```java -// InterfaceExample ie1 = new InterfaceExample(); // 'InterfaceExample' is abstract; cannot be instantiated -InterfaceExample ie2 = new InterfaceImplementExample(); +// InterfaceExample ie1 = new InterfaceExample(); // 'InterfaceExample' is abstract; cannot be instantiated +InterfaceExample ie2 = new InterfaceImplementExample(); ie2.func1(); System.out.println(InterfaceExample.x); ``` -**3. 比较** +**3. 比较** -- 从设计层面上看,抽象类提供了一种 IS-A 关系,那么就必须满足里式替换原则,即子类对象必须能够替换掉所有父类对象。而接口更像是一种 LIKE-A 关系,它只是提供一种方法实现契约,并不要求接口和实现接口的类具有 IS-A 关系。 -- 从使用上来看,一个类可以实现多个接口,但是不能继承多个抽象类。 -- 接口的字段只能是 static 和 final 类型的,而抽象类的字段没有这种限制。 -- 接口的成员只能是 public 的,而抽象类的成员可以有多种访问权限。 +- 从设计层面上看,抽象类提供了一种 IS-A 关系,那么就必须满足里式替换原则,即子类对象必须能够替换掉所有父类对象。而接口更像是一种 LIKE-A 关系,它只是提供一种方法实现契约,并不要求接口和实现接口的类具有 IS-A 关系。 +- 从使用上来看,一个类可以实现多个接口,但是不能继承多个抽象类。 +- 接口的字段只能是 static 和 final 类型的,而抽象类的字段没有这种限制。 +- 接口的成员只能是 public 的,而抽象类的成员可以有多种访问权限。 -**4. 使用选择** +**4. 使用选择** 使用接口: -- 需要让不相关的类都实现一个方法,例如不相关的类都可以实现 Compareable 接口中的 compareTo() 方法; -- 需要使用多重继承。 +- 需要让不相关的类都实现一个方法,例如不相关的类都可以实现 Compareable 接口中的 compareTo() 方法; +- 需要使用多重继承。 使用抽象类: -- 需要在几个相关的类中共享代码。 -- 需要能控制继承来的成员的访问权限,而不是都为 public。 -- 需要继承非静态和非常量字段。 +- 需要在几个相关的类中共享代码。 +- 需要能控制继承来的成员的访问权限,而不是都为 public。 +- 需要继承非静态和非常量字段。 -在很多情况下,接口优先于抽象类。因为接口没有抽象类严格的类层次结构要求,可以灵活地为一个类添加行为。并且从 Java 8 开始,接口也可以有默认的方法实现,使得修改接口的成本也变的很低。 +在很多情况下,接口优先于抽象类。因为接口没有抽象类严格的类层次结构要求,可以灵活地为一个类添加行为。并且从 Java 8 开始,接口也可以有默认的方法实现,使得修改接口的成本也变的很低。 -- [深入理解 abstract class 和 interface](https://www.ibm.com/developerworks/cn/java/l-javainterface-abstract/) -- [When to Use Abstract Class and Interface](https://dzone.com/articles/when-to-use-abstract-class-and-intreface) +- [深入理解 abstract class 和 interface](https://www.ibm.com/developerworks/cn/java/l-javainterface-abstract/) +- [When to Use Abstract Class and Interface](https://dzone.com/articles/when-to-use-abstract-class-and-intreface) -## super +## super -- 访问父类的构造函数:可以使用 super() 函数访问父类的构造函数,从而委托父类完成一些初始化的工作。 -- 访问父类的成员:如果子类重写了父类的某个方法,可以通过使用 super 关键字来引用父类的方法实现。 +- 访问父类的构造函数:可以使用 super() 函数访问父类的构造函数,从而委托父类完成一些初始化的工作。 +- 访问父类的成员:如果子类重写了父类的某个方法,可以通过使用 super 关键字来引用父类的方法实现。 ```java -public class SuperExample { +public class SuperExample { - protected int x; - protected int y; +    protected int x; +    protected int y; - public SuperExample(int x, int y) { - this.x = x; - this.y = y; - } +    public SuperExample(int x, int y) { +        this.x = x; +        this.y = y; +    } - public void func() { - System.out.println("SuperExample.func()"); - } +    public void func() { +        System.out.println("SuperExample.func()"); +    } } ``` ```java -public class SuperExtendExample extends SuperExample { +public class SuperExtendExample extends SuperExample { - private int z; +    private int z; - public SuperExtendExample(int x, int y, int z) { - super(x, y); - this.z = z; - } +    public SuperExtendExample(int x, int y, int z) { +        super(x, y); +        this.z = z; +    } - @Override - public void func() { - super.func(); - System.out.println("SuperExtendExample.func()"); - } +    @Override +    public void func() { +        super.func(); +        System.out.println("SuperExtendExample.func()"); +    } } ``` ```java -SuperExample e = new SuperExtendExample(1, 2, 3); +SuperExample e = new SuperExtendExample(1, 2, 3); e.func(); ``` @@ -660,250 +618,250 @@ SuperExample.func() SuperExtendExample.func() ``` -[Using the Keyword super](https://docs.oracle.com/javase/tutorial/java/IandI/super.html) +[Using the Keyword super](https://docs.oracle.com/javase/tutorial/java/IandI/super.html) -## 重写与重载 +## 重写与重载 -**1. 重写(Override)** +**1. 重写(Override)** 存在于继承体系中,指子类实现了一个与父类在方法声明上完全相同的一个方法。 为了满足里式替换原则,重写有有以下两个限制: -- 子类方法的访问权限必须大于等于父类方法; -- 子类方法的返回类型必须是父类方法返回类型或为其子类型。 +- 子类方法的访问权限必须大于等于父类方法; +- 子类方法的返回类型必须是父类方法返回类型或为其子类型。 -使用 @Override 注解,可以让编译器帮忙检查是否满足上面的两个限制条件。 +使用 @Override 注解,可以让编译器帮忙检查是否满足上面的两个限制条件。 -**2. 重载(Overload)** +**2. 重载(Overload)** 存在于同一个类中,指一个方法与已经存在的方法名称上相同,但是参数类型、个数、顺序至少有一个不同。 应该注意的是,返回值不同,其它都相同不算是重载。 -**3. 实例** +**3. 实例** ```java -class A { - public String show(D obj) { - return ("A and D"); - } +class A { +    public String show(D obj) { +        return ("A and D"); +    } - public String show(A obj) { - return ("A and A"); - } +    public String show(A obj) { +        return ("A and A"); +    } } -class B extends A { - public String show(B obj) { - return ("B and B"); - } +class B extends A { +    public String show(B obj) { +        return ("B and B"); +    } - public String show(A obj) { - return ("B and A"); - } +    public String show(A obj) { +        return ("B and A"); +    } } -class C extends B { +class C extends B { } -class D extends B { +class D extends B { } ``` ```java -public class Test { +public class Test { - public static void main(String[] args) { - A a1 = new A(); - A a2 = new B(); - B b = new B(); - C c = new C(); - D d = new D(); - System.out.println(a1.show(b)); // A and A - System.out.println(a1.show(c)); // A and A - System.out.println(a1.show(d)); // A and D - System.out.println(a2.show(b)); // B and A - System.out.println(a2.show(c)); // B and A - System.out.println(a2.show(d)); // A and D - System.out.println(b.show(b)); // B and B - System.out.println(b.show(c)); // B and B - System.out.println(b.show(d)); // A and D - } +    public static void main(String[] args) { +        A a1 = new A(); +        A a2 = new B(); +        B b = new B(); +        C c = new C(); +        D d = new D(); +        System.out.println(a1.show(b)); // A and A +        System.out.println(a1.show(c)); // A and A +        System.out.println(a1.show(d)); // A and D +        System.out.println(a2.show(b)); // B and A +        System.out.println(a2.show(c)); // B and A +        System.out.println(a2.show(d)); // A and D +        System.out.println(b.show(b));  // B and B +        System.out.println(b.show(c));  // B and B +        System.out.println(b.show(d));  // A and D +    } } ``` 涉及到重写时,方法调用的优先级为: -- this.show(O) -- super.show(O) -- this.show((super)O) -- super.show((super)O) +- this.show(O) +- super.show(O) +- this.show((super)O) +- super.show((super)O) -# 五、Object 通用方法 +# 五、Object 通用方法 -## 概览 +## 概览 ```java -public native int hashCode() +public native int hashCode() -public boolean equals(Object obj) +public boolean equals(Object obj) -protected native Object clone() throws CloneNotSupportedException +protected native Object clone() throws CloneNotSupportedException -public String toString() +public String toString() -public final native Class getClass() +public final native Class getClass() -protected void finalize() throws Throwable {} +protected void finalize() throws Throwable {} -public final native void notify() +public final native void notify() -public final native void notifyAll() +public final native void notifyAll() -public final native void wait(long timeout) throws InterruptedException +public final native void wait(long timeout) throws InterruptedException -public final void wait(long timeout, int nanos) throws InterruptedException +public final void wait(long timeout, int nanos) throws InterruptedException -public final void wait() throws InterruptedException +public final void wait() throws InterruptedException ``` -## equals() +## equals() -**1. 等价关系** +**1. 等价关系** -Ⅰ 自反性 +Ⅰ 自反性 ```java -x.equals(x); // true +x.equals(x); // true ``` -Ⅱ 对称性 +Ⅱ 对称性 ```java -x.equals(y) == y.equals(x); // true +x.equals(y) == y.equals(x); // true ``` -Ⅲ 传递性 +Ⅲ 传递性 ```java -if (x.equals(y) && y.equals(z)) - x.equals(z); // true; +if (x.equals(y) && y.equals(z)) +    x.equals(z); // true; ``` -Ⅳ 一致性 +Ⅳ 一致性 -多次调用 equals() 方法结果不变 +多次调用 equals() 方法结果不变 ```java -x.equals(y) == x.equals(y); // true +x.equals(y) == x.equals(y); // true ``` -Ⅴ 与 null 的比较 +Ⅴ 与 null 的比较 -对任何不是 null 的对象 x 调用 x.equals(null) 结果都为 false +对任何不是 null 的对象 x 调用 x.equals(null) 结果都为 false ```java -x.equals(null); // false; +x.equals(null); // false; ``` -**2. 等价与相等** +**2. 等价与相等** -- 对于基本类型,== 判断两个值是否相等,基本类型没有 equals() 方法。 -- 对于引用类型,== 判断两个变量是否引用同一个对象,而 equals() 判断引用的对象是否等价。 +- 对于基本类型,== 判断两个值是否相等,基本类型没有 equals() 方法。 +- 对于引用类型,== 判断两个变量是否引用同一个对象,而 equals() 判断引用的对象是否等价。 ```java -Integer x = new Integer(1); -Integer y = new Integer(1); -System.out.println(x.equals(y)); // true -System.out.println(x == y); // false +Integer x = new Integer(1); +Integer y = new Integer(1); +System.out.println(x.equals(y)); // true +System.out.println(x == y);      // false ``` -**3. 实现** +**3. 实现** -- 检查是否为同一个对象的引用,如果是直接返回 true; -- 检查是否是同一个类型,如果不是,直接返回 false; -- 将 Object 对象进行转型; -- 判断每个关键域是否相等。 +- 检查是否为同一个对象的引用,如果是直接返回 true; +- 检查是否是同一个类型,如果不是,直接返回 false; +- 将 Object 对象进行转型; +- 判断每个关键域是否相等。 ```java -public class EqualExample { +public class EqualExample { - private int x; - private int y; - private int z; +    private int x; +    private int y; +    private int z; - public EqualExample(int x, int y, int z) { - this.x = x; - this.y = y; - this.z = z; - } +    public EqualExample(int x, int y, int z) { +        this.x = x; +        this.y = y; +        this.z = z; +    } - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; +    @Override +    public boolean equals(Object o) { +        if (this == o) return true; +        if (o == null || getClass() != o.getClass()) return false; - EqualExample that = (EqualExample) o; +        EqualExample that = (EqualExample) o; - if (x != that.x) return false; - if (y != that.y) return false; - return z == that.z; - } +        if (x != that.x) return false; +        if (y != that.y) return false; +        return z == that.z; +    } } ``` -## hashCode() +## hashCode() -hashCode() 返回散列值,而 equals() 是用来判断两个对象是否等价。等价的两个对象散列值一定相同,但是散列值相同的两个对象不一定等价。 +hashCode() 返回散列值,而 equals() 是用来判断两个对象是否等价。等价的两个对象散列值一定相同,但是散列值相同的两个对象不一定等价。 -在覆盖 equals() 方法时应当总是覆盖 hashCode() 方法,保证等价的两个对象散列值也相等。 +在覆盖 equals() 方法时应当总是覆盖 hashCode() 方法,保证等价的两个对象散列值也相等。 -下面的代码中,新建了两个等价的对象,并将它们添加到 HashSet 中。我们希望将这两个对象当成一样的,只在集合中添加一个对象,但是因为 EqualExample 没有实现 hasCode() 方法,因此这两个对象的散列值是不同的,最终导致集合添加了两个等价的对象。 +下面的代码中,新建了两个等价的对象,并将它们添加到 HashSet 中。我们希望将这两个对象当成一样的,只在集合中添加一个对象,但是因为 EqualExample 没有实现 hasCode() 方法,因此这两个对象的散列值是不同的,最终导致集合添加了两个等价的对象。 ```java -EqualExample e1 = new EqualExample(1, 1, 1); -EqualExample e2 = new EqualExample(1, 1, 1); -System.out.println(e1.equals(e2)); // true -HashSet set = new HashSet<>(); +EqualExample e1 = new EqualExample(1, 1, 1); +EqualExample e2 = new EqualExample(1, 1, 1); +System.out.println(e1.equals(e2)); // true +HashSet set = new HashSet<>(); set.add(e1); set.add(e2); -System.out.println(set.size()); // 2 +System.out.println(set.size());   // 2 ``` -理想的散列函数应当具有均匀性,即不相等的对象应当均匀分布到所有可能的散列值上。这就要求了散列函数要把所有域的值都考虑进来。可以将每个域都当成 R 进制的某一位,然后组成一个 R 进制的整数。R 一般取 31,因为它是一个奇素数,如果是偶数的话,当出现乘法溢出,信息就会丢失,因为与 2 相乘相当于向左移一位。 +理想的散列函数应当具有均匀性,即不相等的对象应当均匀分布到所有可能的散列值上。这就要求了散列函数要把所有域的值都考虑进来。可以将每个域都当成 R 进制的某一位,然后组成一个 R 进制的整数。R 一般取 31,因为它是一个奇素数,如果是偶数的话,当出现乘法溢出,信息就会丢失,因为与 2 相乘相当于向左移一位。 -一个数与 31 相乘可以转换成移位和减法:`31*x == (x<<5)-x`,编译器会自动进行这个优化。 +一个数与 31 相乘可以转换成移位和减法:`31*x == (x<<5)-x`,编译器会自动进行这个优化。 ```java @Override -public int hashCode() { - int result = 17; - result = 31 * result + x; - result = 31 * result + y; - result = 31 * result + z; - return result; +public int hashCode() { +    int result = 17; +    result = 31 * result + x; +    result = 31 * result + y; +    result = 31 * result + z; +    return result; } ``` -## toString() +## toString() -默认返回 ToStringExample@4554617c 这种形式,其中 @ 后面的数值为散列码的无符号十六进制表示。 +默认返回 ToStringExample@4554617c 这种形式,其中 @ 后面的数值为散列码的无符号十六进制表示。 ```java -public class ToStringExample { +public class ToStringExample { - private int number; +    private int number; - public ToStringExample(int number) { - this.number = number; - } +    public ToStringExample(int number) { +        this.number = number; +    } } ``` ```java -ToStringExample example = new ToStringExample(123); +ToStringExample example = new ToStringExample(123); System.out.println(example.toString()); ``` @@ -911,290 +869,290 @@ System.out.println(example.toString()); ToStringExample@4554617c ``` -## clone() +## clone() -**1. cloneable** +**1. cloneable** -clone() 是 Object 的 protected 方法,它不是 public,一个类不显式去重写 clone(),其它类就不能直接去调用该类实例的 clone() 方法。 +clone() 是 Object 的 protected 方法,它不是 public,一个类不显式去重写 clone(),其它类就不能直接去调用该类实例的 clone() 方法。 ```java -public class CloneExample { - private int a; - private int b; +public class CloneExample { +    private int a; +    private int b; } ``` ```java -CloneExample e1 = new CloneExample(); -// CloneExample e2 = e1.clone(); // 'clone()' has protected access in 'java.lang.Object' +CloneExample e1 = new CloneExample(); +// CloneExample e2 = e1.clone(); // 'clone()' has protected access in 'java.lang.Object' ``` -重写 clone() 得到以下实现: +重写 clone() 得到以下实现: ```java -public class CloneExample { - private int a; - private int b; +public class CloneExample { +    private int a; +    private int b; - @Override - public CloneExample clone() throws CloneNotSupportedException { - return (CloneExample)super.clone(); - } +    @Override +    public CloneExample clone() throws CloneNotSupportedException { +        return (CloneExample)super.clone(); +    } } ``` ```java -CloneExample e1 = new CloneExample(); -try { - CloneExample e2 = e1.clone(); -} catch (CloneNotSupportedException e) { - e.printStackTrace(); +CloneExample e1 = new CloneExample(); +try { +    CloneExample e2 = e1.clone(); +} catch (CloneNotSupportedException e) { +    e.printStackTrace(); } ``` ```html -java.lang.CloneNotSupportedException: CloneExample +java.lang.CloneNotSupportedException: CloneExample ``` -以上抛出了 CloneNotSupportedException,这是因为 CloneExample 没有实现 Cloneable 接口。 +以上抛出了 CloneNotSupportedException,这是因为 CloneExample 没有实现 Cloneable 接口。 -应该注意的是,clone() 方法并不是 Cloneable 接口的方法,而是 Object 的一个 protected 方法。Cloneable 接口只是规定,如果一个类没有实现 Cloneable 接口又调用了 clone() 方法,就会抛出 CloneNotSupportedException。 +应该注意的是,clone() 方法并不是 Cloneable 接口的方法,而是 Object 的一个 protected 方法。Cloneable 接口只是规定,如果一个类没有实现 Cloneable 接口又调用了 clone() 方法,就会抛出 CloneNotSupportedException。 ```java -public class CloneExample implements Cloneable { - private int a; - private int b; +public class CloneExample implements Cloneable { +    private int a; +    private int b; - @Override - public Object clone() throws CloneNotSupportedException { - return super.clone(); - } +    @Override +    public Object clone() throws CloneNotSupportedException { +        return super.clone(); +    } } ``` -**2. 浅拷贝** +**2. 浅拷贝** 拷贝对象和原始对象的引用类型引用同一个对象。 ```java -public class ShallowCloneExample implements Cloneable { +public class ShallowCloneExample implements Cloneable { - private int[] arr; +    private int[] arr; - public ShallowCloneExample() { - arr = new int[10]; - for (int i = 0; i < arr.length; i++) { - arr[i] = i; - } - } +    public ShallowCloneExample() { +        arr = new int[10]; +        for (int i = 0; i < arr.length; i++) { +            arr[i] = i; +        } +    } - public void set(int index, int value) { - arr[index] = value; - } +    public void set(int index, int value) { +        arr[index] = value; +    } - public int get(int index) { - return arr[index]; - } +    public int get(int index) { +        return arr[index]; +    } - @Override - protected ShallowCloneExample clone() throws CloneNotSupportedException { - return (ShallowCloneExample) super.clone(); - } +    @Override +    protected ShallowCloneExample clone() throws CloneNotSupportedException { +        return (ShallowCloneExample) super.clone(); +    } } ``` ```java -ShallowCloneExample e1 = new ShallowCloneExample(); -ShallowCloneExample e2 = null; -try { - e2 = e1.clone(); -} catch (CloneNotSupportedException e) { - e.printStackTrace(); +ShallowCloneExample e1 = new ShallowCloneExample(); +ShallowCloneExample e2 = null; +try { +    e2 = e1.clone(); +} catch (CloneNotSupportedException e) { +    e.printStackTrace(); } -e1.set(2, 222); -System.out.println(e2.get(2)); // 222 +e1.set(2, 222); +System.out.println(e2.get(2)); // 222 ``` -**3. 深拷贝** +**3. 深拷贝** 拷贝对象和原始对象的引用类型引用不同对象。 ```java -public class DeepCloneExample implements Cloneable { +public class DeepCloneExample implements Cloneable { - private int[] arr; +    private int[] arr; - public DeepCloneExample() { - arr = new int[10]; - for (int i = 0; i < arr.length; i++) { - arr[i] = i; - } - } +    public DeepCloneExample() { +        arr = new int[10]; +        for (int i = 0; i < arr.length; i++) { +            arr[i] = i; +        } +    } - public void set(int index, int value) { - arr[index] = value; - } +    public void set(int index, int value) { +        arr[index] = value; +    } - public int get(int index) { - return arr[index]; - } +    public int get(int index) { +        return arr[index]; +    } - @Override - protected DeepCloneExample clone() throws CloneNotSupportedException { - DeepCloneExample result = (DeepCloneExample) super.clone(); - result.arr = new int[arr.length]; - for (int i = 0; i < arr.length; i++) { - result.arr[i] = arr[i]; - } - return result; - } +    @Override +    protected DeepCloneExample clone() throws CloneNotSupportedException { +        DeepCloneExample result = (DeepCloneExample) super.clone(); +        result.arr = new int[arr.length]; +        for (int i = 0; i < arr.length; i++) { +            result.arr[i] = arr[i]; +        } +        return result; +    } } ``` ```java -DeepCloneExample e1 = new DeepCloneExample(); -DeepCloneExample e2 = null; -try { - e2 = e1.clone(); -} catch (CloneNotSupportedException e) { - e.printStackTrace(); +DeepCloneExample e1 = new DeepCloneExample(); +DeepCloneExample e2 = null; +try { +    e2 = e1.clone(); +} catch (CloneNotSupportedException e) { +    e.printStackTrace(); } -e1.set(2, 222); -System.out.println(e2.get(2)); // 2 +e1.set(2, 222); +System.out.println(e2.get(2)); // 2 ``` -**4. clone() 的替代方案** +**4. clone() 的替代方案** -使用 clone() 方法来拷贝一个对象即复杂又有风险,它会抛出异常,并且还需要类型转换。Effective Java 书上讲到,最好不要去使用 clone(),可以使用拷贝构造函数或者拷贝工厂来拷贝一个对象。 +使用 clone() 方法来拷贝一个对象即复杂又有风险,它会抛出异常,并且还需要类型转换。Effective Java 书上讲到,最好不要去使用 clone(),可以使用拷贝构造函数或者拷贝工厂来拷贝一个对象。 ```java -public class CloneConstructorExample { +public class CloneConstructorExample { - private int[] arr; +    private int[] arr; - public CloneConstructorExample() { - arr = new int[10]; - for (int i = 0; i < arr.length; i++) { - arr[i] = i; - } - } +    public CloneConstructorExample() { +        arr = new int[10]; +        for (int i = 0; i < arr.length; i++) { +            arr[i] = i; +        } +    } - public CloneConstructorExample(CloneConstructorExample original) { - arr = new int[original.arr.length]; - for (int i = 0; i < original.arr.length; i++) { - arr[i] = original.arr[i]; - } - } +    public CloneConstructorExample(CloneConstructorExample original) { +        arr = new int[original.arr.length]; +        for (int i = 0; i < original.arr.length; i++) { +            arr[i] = original.arr[i]; +        } +    } - public void set(int index, int value) { - arr[index] = value; - } +    public void set(int index, int value) { +        arr[index] = value; +    } - public int get(int index) { - return arr[index]; - } +    public int get(int index) { +        return arr[index]; +    } } ``` ```java -CloneConstructorExample e1 = new CloneConstructorExample(); -CloneConstructorExample e2 = new CloneConstructorExample(e1); -e1.set(2, 222); -System.out.println(e2.get(2)); // 2 +CloneConstructorExample e1 = new CloneConstructorExample(); +CloneConstructorExample e2 = new CloneConstructorExample(e1); +e1.set(2, 222); +System.out.println(e2.get(2)); // 2 ``` -# 六、关键字 +# 六、关键字 -## final +## final -**1. 数据** +**1. 数据** 声明数据为常量,可以是编译时常量,也可以是在运行时被初始化后不能被改变的常量。 -- 对于基本类型,final 使数值不变; -- 对于引用类型,final 使引用不变,也就不能引用其它对象,但是被引用的对象本身是可以修改的。 +- 对于基本类型,final 使数值不变; +- 对于引用类型,final 使引用不变,也就不能引用其它对象,但是被引用的对象本身是可以修改的。 ```java -final int x = 1; -// x = 2; // cannot assign value to final variable 'x' -final A y = new A(); -y.a = 1; +final int x = 1; +// x = 2;  // cannot assign value to final variable 'x' +final A y = new A(); +y.a = 1; ``` -**2. 方法** +**2. 方法** 声明方法不能被子类重写。 -private 方法隐式地被指定为 final,如果在子类中定义的方法和基类中的一个 private 方法签名相同,此时子类的方法不是重写基类方法,而是在子类中定义了一个新的方法。 +private 方法隐式地被指定为 final,如果在子类中定义的方法和基类中的一个 private 方法签名相同,此时子类的方法不是重写基类方法,而是在子类中定义了一个新的方法。 -**3. 类** +**3. 类** 声明类不允许被继承。 -## static +## static -**1. 静态变量** +**1. 静态变量** -- 静态变量:又称为类变量,也就是说这个变量属于类的,类所有的实例都共享静态变量,可以直接通过类名来访问它。静态变量在内存中只存在一份。 -- 实例变量:每创建一个实例就会产生一个实例变量,它与该实例同生共死。 +- 静态变量:又称为类变量,也就是说这个变量属于类的,类所有的实例都共享静态变量,可以直接通过类名来访问它。静态变量在内存中只存在一份。 +- 实例变量:每创建一个实例就会产生一个实例变量,它与该实例同生共死。 ```java -public class A { +public class A { - private int x; // 实例变量 - private static int y; // 静态变量 +    private int x;         // 实例变量 +    private static int y;  // 静态变量 - public static void main(String[] args) { - // int x = A.x; // Non-static field 'x' cannot be referenced from a static context - A a = new A(); - int x = a.x; - int y = A.y; - } +    public static void main(String[] args) { +        // int x = A.x;  // Non-static field 'x' cannot be referenced from a static context +        A a = new A(); +        int x = a.x; +        int y = A.y; +    } } ``` -**2. 静态方法** +**2. 静态方法** 静态方法在类加载的时候就存在了,它不依赖于任何实例。所以静态方法必须有实现,也就是说它不能是抽象方法。 ```java -public abstract class A { - public static void func1(){ - } - // public abstract static void func2(); // Illegal combination of modifiers: 'abstract' and 'static' +public abstract class A { +    public static void func1(){ +    } +    // public abstract static void func2();  // Illegal combination of modifiers: 'abstract' and 'static' } ``` -只能访问所属类的静态字段和静态方法,方法中不能有 this 和 super 关键字。 +只能访问所属类的静态字段和静态方法,方法中不能有 this 和 super 关键字。 ```java -public class A { +public class A { - private static int x; - private int y; +    private static int x; +    private int y; - public static void func1(){ - int a = x; - // int b = y; // Non-static field 'y' cannot be referenced from a static context - // int b = this.y; // 'A.this' cannot be referenced from a static context - } +    public static void func1(){ +        int a = x; +        // int b = y;  // Non-static field 'y' cannot be referenced from a static context +        // int b = this.y;     // 'A.this' cannot be referenced from a static context +    } } ``` -**3. 静态语句块** +**3. 静态语句块** 静态语句块在类初始化时运行一次。 ```java -public class A { - static { - System.out.println("123"); - } +public class A { +    static { +        System.out.println("123"); +    } - public static void main(String[] args) { - A a1 = new A(); - A a2 = new A(); - } +    public static void main(String[] args) { +        A a1 = new A(); +        A a2 = new A(); +    } } ``` @@ -1202,195 +1160,198 @@ public class A { 123 ``` -**4. 静态内部类** +**4. 静态内部类** 非静态内部类依赖于外部类的实例,而静态内部类不需要。 ```java -public class OuterClass { +public class OuterClass { - class InnerClass { - } +    class InnerClass { +    } - static class StaticInnerClass { - } +    static class StaticInnerClass { +    } - public static void main(String[] args) { - // InnerClass innerClass = new InnerClass(); // 'OuterClass.this' cannot be referenced from a static context - OuterClass outerClass = new OuterClass(); - InnerClass innerClass = outerClass.new InnerClass(); - StaticInnerClass staticInnerClass = new StaticInnerClass(); - } +    public static void main(String[] args) { +        // InnerClass innerClass = new InnerClass(); // 'OuterClass.this' cannot be referenced from a static context +        OuterClass outerClass = new OuterClass(); +        InnerClass innerClass = outerClass.new InnerClass(); +        StaticInnerClass staticInnerClass = new StaticInnerClass(); +    } } ``` 静态内部类不能访问外部类的非静态的变量和方法。 -**5. 静态导包** +**5. 静态导包** -在使用静态变量和方法时不用再指明 ClassName,从而简化代码,但可读性大大降低。 +在使用静态变量和方法时不用再指明 ClassName,从而简化代码,但可读性大大降低。 ```java -import static com.xxx.ClassName.* +import static com.xxx.ClassName.* ``` -**6. 初始化顺序** +**6. 初始化顺序** 静态变量和静态语句块优先于实例变量和普通语句块,静态变量和静态语句块的初始化顺序取决于它们在代码中的顺序。 ```java -public static String staticField = "静态变量"; +public static String staticField = "静态变量"; ``` ```java -static { - System.out.println("静态语句块"); +static { +    System.out.println("静态语句块"); } ``` ```java -public String field = "实例变量"; +public String field = "实例变量"; ``` ```java { - System.out.println("普通语句块"); +    System.out.println("普通语句块"); } ``` 最后才是构造函数的初始化。 ```java -public InitialOrderTest() { - System.out.println("构造函数"); +public InitialOrderTest() { +    System.out.println("构造函数"); } ``` 存在继承的情况下,初始化顺序为: -- 父类(静态变量、静态语句块) -- 子类(静态变量、静态语句块) -- 父类(实例变量、普通语句块) -- 父类(构造函数) -- 子类(实例变量、普通语句块) -- 子类(构造函数) +- 父类(静态变量、静态语句块) +- 子类(静态变量、静态语句块) +- 父类(实例变量、普通语句块) +- 父类(构造函数) +- 子类(实例变量、普通语句块) +- 子类(构造函数) -# 七、反射 +# 七、反射 -每个类都有一个 **Class** 对象,包含了与类有关的信息。当编译一个新类时,会产生一个同名的 .class 文件,该文件内容保存着 Class 对象。 +每个类都有一个 **Class** 对象,包含了与类有关的信息。当编译一个新类时,会产生一个同名的 .class 文件,该文件内容保存着 Class 对象。 -类加载相当于 Class 对象的加载,类在第一次使用时才动态加载到 JVM 中。也可以使用 `Class.forName("com.mysql.jdbc.Driver")` 这种方式来控制类的加载,该方法会返回一个 Class 对象。 +类加载相当于 Class 对象的加载,类在第一次使用时才动态加载到 JVM 中。也可以使用 `Class.forName("com.mysql.jdbc.Driver")` 这种方式来控制类的加载,该方法会返回一个 Class 对象。 -反射可以提供运行时的类信息,并且这个类可以在运行时才加载进来,甚至在编译时期该类的 .class 不存在也可以加载进来。 +反射可以提供运行时的类信息,并且这个类可以在运行时才加载进来,甚至在编译时期该类的 .class 不存在也可以加载进来。 -Class 和 java.lang.reflect 一起对反射提供了支持,java.lang.reflect 类库主要包含了以下三个类: +Class 和 java.lang.reflect 一起对反射提供了支持,java.lang.reflect 类库主要包含了以下三个类: -- **Field** :可以使用 get() 和 set() 方法读取和修改 Field 对象关联的字段; -- **Method** :可以使用 invoke() 方法调用与 Method 对象关联的方法; -- **Constructor** :可以用 Constructor 创建新的对象。 +- **Field**:可以使用 get() 和 set() 方法读取和修改 Field 对象关联的字段; +- **Method**:可以使用 invoke() 方法调用与 Method 对象关联的方法; +- **Constructor**:可以用 Constructor 创建新的对象。 -**反射的优点:** +**反射的优点:** -* **可扩展性** :应用程序可以利用全限定名创建可扩展对象的实例,来使用来自外部的用户自定义类。 -* **类浏览器和可视化开发环境** :一个类浏览器需要可以枚举类的成员。可视化开发环境(如 IDE)可以从利用反射中可用的类型信息中受益,以帮助程序员编写正确的代码。 -* **调试器和测试工具** : 调试器需要能够检查一个类里的私有成员。测试工具可以利用反射来自动地调用类里定义的可被发现的 API 定义,以确保一组测试中有较高的代码覆盖率。 +*   **可扩展性** :应用程序可以利用全限定名创建可扩展对象的实例,来使用来自外部的用户自定义类。 +*   **类浏览器和可视化开发环境** :一个类浏览器需要可以枚举类的成员。可视化开发环境(如 IDE)可以从利用反射中可用的类型信息中受益,以帮助程序员编写正确的代码。 +*   **调试器和测试工具** : 调试器需要能够检查一个类里的私有成员。测试工具可以利用反射来自动地调用类里定义的可被发现的 API 定义,以确保一组测试中有较高的代码覆盖率。 -**反射的缺点:** +**反射的缺点:** 尽管反射非常强大,但也不能滥用。如果一个功能可以不用反射完成,那么最好就不用。在我们使用反射技术时,下面几条内容应该牢记于心。 -* **性能开销** :反射涉及了动态类型的解析,所以 JVM 无法对这些代码进行优化。因此,反射操作的效率要比那些非反射操作低得多。我们应该避免在经常被执行的代码或对性能要求很高的程序中使用反射。 +*   **性能开销** :反射涉及了动态类型的解析,所以 JVM 无法对这些代码进行优化。因此,反射操作的效率要比那些非反射操作低得多。我们应该避免在经常被执行的代码或对性能要求很高的程序中使用反射。 -* **安全限制** :使用反射技术要求程序必须在一个没有安全限制的环境中运行。如果一个程序必须在有安全限制的环境中运行,如 Applet,那么这就是个问题了。 +*   **安全限制** :使用反射技术要求程序必须在一个没有安全限制的环境中运行。如果一个程序必须在有安全限制的环境中运行,如 Applet,那么这就是个问题了。 -* **内部暴露** :由于反射允许代码执行一些在正常情况下不被允许的操作(比如访问私有的属性和方法),所以使用反射可能会导致意料之外的副作用,这可能导致代码功能失调并破坏可移植性。反射代码破坏了抽象性,因此当平台发生改变的时候,代码的行为就有可能也随着变化。 +*   **内部暴露** :由于反射允许代码执行一些在正常情况下不被允许的操作(比如访问私有的属性和方法),所以使用反射可能会导致意料之外的副作用,这可能导致代码功能失调并破坏可移植性。反射代码破坏了抽象性,因此当平台发生改变的时候,代码的行为就有可能也随着变化。 -- [Trail: The Reflection API](https://docs.oracle.com/javase/tutorial/reflect/index.html) -- [深入解析 Java 反射(1)- 基础](http://www.sczyh30.com/posts/Java/java-reflection-1/) +- [Trail: The Reflection API](https://docs.oracle.com/javase/tutorial/reflect/index.html) +- [深入解析 Java 反射(1)- 基础](http://www.sczyh30.com/posts/Java/java-reflection-1/) -# 八、异常 +# 八、异常 -Throwable 可以用来表示任何可以作为异常抛出的类,分为两种: **Error** 和 **Exception**。其中 Error 用来表示 JVM 无法处理的错误,Exception 分为两种: +Throwable 可以用来表示任何可以作为异常抛出的类,分为两种:**Error** 和 **Exception**。其中 Error 用来表示 JVM 无法处理的错误,Exception 分为两种: -- **受检异常** :需要用 try...catch... 语句捕获并进行处理,并且可以从异常中恢复; -- **非受检异常** :是程序运行时错误,例如除 0 会引发 Arithmetic Exception,此时程序崩溃并且无法恢复。 +- **受检异常**:需要用 try...catch... 语句捕获并进行处理,并且可以从异常中恢复; +- **非受检异常**:是程序运行时错误,例如除 0 会引发 Arithmetic Exception,此时程序崩溃并且无法恢复。 -

+ -- [Java 入门之异常处理](https://www.tianmaying.com/tutorial/Java-Exception) -- [Java 异常的面试问题及答案 -Part 1](http://www.importnew.com/7383.html) +- [Java 入门之异常处理](https://www.tianmaying.com/tutorial/Java-Exception) +- [Java 异常的面试问题及答案 -Part 1](http://www.importnew.com/7383.html) -# 九、泛型 +# 九、泛型 ```java -public class Box { - // T stands for "Type" - private T t; - public void set(T t) { this.t = t; } - public T get() { return t; } +public class Box { +    // T stands for "Type" +    private T t; +    public void set(T t) { this.t = t; } +    public T get() { return t; } } ``` -- [Java 泛型详解](http://www.importnew.com/24029.html) -- [10 道 Java 泛型面试题](https://cloud.tencent.com/developer/article/1033693) +- [Java 泛型详解](http://www.importnew.com/24029.html) +- [10 道 Java 泛型面试题](https://cloud.tencent.com/developer/article/1033693) -# 十、注解 +# 十、注解 -Java 注解是附加在代码中的一些元信息,用于一些工具在编译、运行时进行解析和使用,起到说明、配置的功能。注解不会也不能影响代码的实际逻辑,仅仅起到辅助性的作用。 +Java 注解是附加在代码中的一些元信息,用于一些工具在编译、运行时进行解析和使用,起到说明、配置的功能。注解不会也不能影响代码的实际逻辑,仅仅起到辅助性的作用。 -[注解 Annotation 实现原理与自定义注解例子](https://www.cnblogs.com/acm-bingzi/p/javaAnnotation.html) +[注解 Annotation 实现原理与自定义注解例子](https://www.cnblogs.com/acm-bingzi/p/javaAnnotation.html) -# 十一、特性 +# 十一、特性 -## Java 各版本的新特性 +## Java 各版本的新特性 -**New highlights in Java SE 8** +**New highlights in Java SE 8** -1. Lambda Expressions -2. Pipelines and Streams -3. Date and Time API -4. Default Methods -5. Type Annotations -6. Nashhorn JavaScript Engine -7. Concurrent Accumulators -8. Parallel operations -9. PermGen Error Removed +1. Lambda Expressions +2. Pipelines and Streams +3. Date and Time API +4. Default Methods +5. Type Annotations +6. Nashhorn JavaScript Engine +7. Concurrent Accumulators +8. Parallel operations +9. PermGen Error Removed -**New highlights in Java SE 7** +**New highlights in Java SE 7** -1. Strings in Switch Statement -2. Type Inference for Generic Instance Creation -3. Multiple Exception Handling -4. Support for Dynamic Languages -5. Try with Resources -6. Java nio Package -7. Binary Literals, Underscore in literals -8. Diamond Syntax +1. Strings in Switch Statement +2. Type Inference for Generic Instance Creation +3. Multiple Exception Handling +4. Support for Dynamic Languages +5. Try with Resources +6. Java nio Package +7. Binary Literals, Underscore in literals +8. Diamond Syntax -- [Difference between Java 1.8 and Java 1.7?](http://www.selfgrowth.com/articles/difference-between-java-18-and-java-17) -- [Java 8 特性](http://www.importnew.com/19345.html) +- [Difference between Java 1.8 and Java 1.7?](http://www.selfgrowth.com/articles/difference-between-java-18-and-java-17) +- [Java 8 特性](http://www.importnew.com/19345.html) -## Java 与 C++ 的区别 +## Java 与 C++ 的区别 -- Java 是纯粹的面向对象语言,所有的对象都继承自 java.lang.Object,C++ 为了兼容 C 即支持面向对象也支持面向过程。 -- Java 通过虚拟机从而实现跨平台特性,但是 C++ 依赖于特定的平台。 -- Java 没有指针,它的引用可以理解为安全指针,而 C++ 具有和 C 一样的指针。 -- Java 支持自动垃圾回收,而 C++ 需要手动回收。 -- Java 不支持多重继承,只能通过实现多个接口来达到相同目的,而 C++ 支持多重继承。 -- Java 不支持操作符重载,虽然可以对两个 String 对象执行加法运算,但是这是语言内置支持的操作,不属于操作符重载,而 C++ 可以。 -- Java 的 goto 是保留字,但是不可用,C++ 可以使用 goto。 -- Java 不支持条件编译,C++ 通过 #ifdef #ifndef 等预处理命令从而实现条件编译。 +- Java 是纯粹的面向对象语言,所有的对象都继承自 java.lang.Object,C++ 为了兼容 C 即支持面向对象也支持面向过程。 +- Java 通过虚拟机从而实现跨平台特性,但是 C++ 依赖于特定的平台。 +- Java 没有指针,它的引用可以理解为安全指针,而 C++ 具有和 C 一样的指针。 +- Java 支持自动垃圾回收,而 C++ 需要手动回收。 +- Java 不支持多重继承,只能通过实现多个接口来达到相同目的,而 C++ 支持多重继承。 +- Java 不支持操作符重载,虽然可以对两个 String 对象执行加法运算,但是这是语言内置支持的操作,不属于操作符重载,而 C++ 可以。 +- Java 的 goto 是保留字,但是不可用,C++ 可以使用 goto。 +- Java 不支持条件编译,C++ 通过 #ifdef #ifndef 等预处理命令从而实现条件编译。 -[What are the main differences between Java and C++?](http://cs-fundamentals.com/tech-interview/java/differences-between-java-and-cpp.php) +[What are the main differences between Java and C++?](http://cs-fundamentals.com/tech-interview/java/differences-between-java-and-cpp.php) -## JRE or JDK +## JRE or JDK -- JRE is the JVM program, Java application need to run on JRE. -- JDK is a superset of JRE, JRE + tools for developing java programs. e.g, it provides the compiler "javac" +- JRE is the JVM program, Java application need to run on JRE. +- JDK is a superset of JRE, JRE + tools for developing java programs. e.g, it provides the compiler "javac" -# 参考资料 +# 参考资料 -- Eckel B. Java 编程思想[M]. 机械工业出版社, 2002. -- Bloch J. Effective java[M]. Addison-Wesley Professional, 2017. +- Eckel B. Java 编程思想[M]. 机械工业出版社, 2002. +- Bloch J. Effective java[M]. Addison-Wesley Professional, 2017. +---bottom---CyC--- +![](index_files/f76067a5-7d5f-4135-9549-8199c77d8f1c.jpg) +![](index_files/PPjwP.png) diff --git a/docs/notes/Java 容器.md b/docs/notes/Java 容器.md index 761220b3..a19de992 100644 --- a/docs/notes/Java 容器.md +++ b/docs/notes/Java 容器.md @@ -1,329 +1,309 @@ - -* [一、概览](#一概览) - * [Collection](#collection) - * [Map](#map) -* [二、容器中的设计模式](#二容器中的设计模式) - * [迭代器模式](#迭代器模式) - * [适配器模式](#适配器模式) -* [三、源码分析](#三源码分析) - * [ArrayList](#arraylist) - * [Vector](#vector) - * [CopyOnWriteArrayList](#copyonwritearraylist) - * [LinkedList](#linkedlist) - * [HashMap](#hashmap) - * [ConcurrentHashMap](#concurrenthashmap) - * [LinkedHashMap](#linkedhashmap) - * [WeakHashMap](#weakhashmap) -* [参考资料](#参考资料) - +# 一、概览 + +容器主要包括 Collection 和 Map 两种,Collection 存储着对象的集合,而 Map 存储着键值对(两个对象)的映射表。 + +## Collection + +![](index_files/6_2001550476096035.png) -# 一、概览 +### 1. Set -容器主要包括 Collection 和 Map 两种,Collection 存储着对象的集合,而 Map 存储着键值对(两个对象)的映射表。 +- TreeSet:基于红黑树实现,支持有序性操作,例如根据一个范围查找元素的操作。但是查找效率不如 HashSet,HashSet 查找的时间复杂度为 O(1),TreeSet 则为 O(logN)。 -## Collection +- HashSet:基于哈希表实现,支持快速查找,但不支持有序性操作。并且失去了元素的插入顺序信息,也就是说使用 Iterator 遍历 HashSet 得到的结果是不确定的。 -

+- LinkedHashSet:具有 HashSet 的查找效率,且内部使用双向链表维护元素的插入顺序。 + +### 2. List + +- ArrayList:基于动态数组实现,支持随机访问。 + +- Vector:和 ArrayList 类似,但它是线程安全的。 + +- LinkedList:基于双向链表实现,只能顺序访问,但是可以快速地在链表中间插入和删除元素。不仅如此,LinkedList 还可以用作栈、队列和双向队列。 + +### 3. Queue + +- LinkedList:可以用它来实现双向队列。 + +- PriorityQueue:基于堆结构实现,可以用它来实现优先队列。 + +## Map + +![](index_files/2_2001550426232419.png) + +- TreeMap:基于红黑树实现。 + +- HashMap:基于哈希表实现。 + +- HashTable:和 HashMap 类似,但它是线程安全的,这意味着同一时刻多个线程可以同时写入 HashTable 并且不会导致数据不一致。它是遗留类,不应该去使用它。现在可以使用 ConcurrentHashMap 来支持线程安全,并且 ConcurrentHashMap 的效率会更高,因为 ConcurrentHashMap 引入了分段锁。 + +- LinkedHashMap:使用双向链表来维护元素的顺序,顺序为插入顺序或者最近最少使用(LRU)顺序。 -### 1. Set +# 二、容器中的设计模式 -- TreeSet:基于红黑树实现,支持有序性操作,例如根据一个范围查找元素的操作。但是查找效率不如 HashSet,HashSet 查找的时间复杂度为 O(1),TreeSet 则为 O(logN)。 +## 迭代器模式 -- HashSet:基于哈希表实现,支持快速查找,但不支持有序性操作。并且失去了元素的插入顺序信息,也就是说使用 Iterator 遍历 HashSet 得到的结果是不确定的。 +![](index_files/91aa7c29-438f-4fcc-8c63-2a75899139de.png) -- LinkedHashSet:具有 HashSet 的查找效率,且内部使用双向链表维护元素的插入顺序。 +Collection 继承了 Iterable 接口,其中的 iterator() 方法能够产生一个 Iterator 对象,通过这个对象就可以迭代遍历 Collection 中的元素。 -### 2. List - -- ArrayList:基于动态数组实现,支持随机访问。 - -- Vector:和 ArrayList 类似,但它是线程安全的。 - -- LinkedList:基于双向链表实现,只能顺序访问,但是可以快速地在链表中间插入和删除元素。不仅如此,LinkedList 还可以用作栈、队列和双向队列。 - -### 3. Queue - -- LinkedList:可以用它来实现双向队列。 - -- PriorityQueue:基于堆结构实现,可以用它来实现优先队列。 - -## Map - -

- -- TreeMap:基于红黑树实现。 - -- HashMap:基于哈希表实现。 - -- HashTable:和 HashMap 类似,但它是线程安全的,这意味着同一时刻多个线程可以同时写入 HashTable 并且不会导致数据不一致。它是遗留类,不应该去使用它。现在可以使用 ConcurrentHashMap 来支持线程安全,并且 ConcurrentHashMap 的效率会更高,因为 ConcurrentHashMap 引入了分段锁。 - -- LinkedHashMap:使用双向链表来维护元素的顺序,顺序为插入顺序或者最近最少使用(LRU)顺序。 - - -# 二、容器中的设计模式 - -## 迭代器模式 - -

- -Collection 继承了 Iterable 接口,其中的 iterator() 方法能够产生一个 Iterator 对象,通过这个对象就可以迭代遍历 Collection 中的元素。 - -从 JDK 1.5 之后可以使用 foreach 方法来遍历实现了 Iterable 接口的聚合对象。 +从 JDK 1.5 之后可以使用 foreach 方法来遍历实现了 Iterable 接口的聚合对象。 ```java -List list = new ArrayList<>(); +List list = new ArrayList<>(); list.add("a"); list.add("b"); -for (String item : list) { - System.out.println(item); +for (String item : list) { +    System.out.println(item); } ``` -## 适配器模式 +## 适配器模式 -java.util.Arrays#asList() 可以把数组类型转换为 List 类型。 +java.util.Arrays#asList() 可以把数组类型转换为 List 类型。 ```java @SafeVarargs -public static List asList(T... a) +public static  List asList(T... a) ``` -应该注意的是 asList() 的参数为泛型的变长参数,不能使用基本类型数组作为参数,只能使用相应的包装类型数组。 +应该注意的是 asList() 的参数为泛型的变长参数,不能使用基本类型数组作为参数,只能使用相应的包装类型数组。 ```java -Integer[] arr = {1, 2, 3}; -List list = Arrays.asList(arr); +Integer[] arr = {1, 2, 3}; +List list = Arrays.asList(arr); ``` -也可以使用以下方式调用 asList(): +也可以使用以下方式调用 asList(): ```java -List list = Arrays.asList(1, 2, 3); +List list = Arrays.asList(1, 2, 3); ``` -# 三、源码分析 +# 三、源码分析 -如果没有特别说明,以下源码分析基于 JDK 1.8。 +如果没有特别说明,以下源码分析基于 JDK 1.8。 -在 IDEA 中 double shift 调出 Search EveryWhere,查找源码文件,找到之后就可以阅读源码。 +在 IDEA 中 double shift 调出 Search EveryWhere,查找源码文件,找到之后就可以阅读源码。 -## ArrayList +## ArrayList -### 1. 概览 +### 1. 概览 -实现了 RandomAccess 接口,因此支持随机访问。这是理所当然的,因为 ArrayList 是基于数组实现的。 +实现了 RandomAccess 接口,因此支持随机访问。这是理所当然的,因为 ArrayList 是基于数组实现的。 ```java -public class ArrayList extends AbstractList - implements List, RandomAccess, Cloneable, java.io.Serializable +public class ArrayList extends AbstractList +        implements List, RandomAccess, Cloneable, java.io.Serializable ``` -数组的默认大小为 10。 +数组的默认大小为 10。 ```java -private static final int DEFAULT_CAPACITY = 10; +private static final int DEFAULT_CAPACITY = 10; ``` -

+![](index_files/7935be3d-c2b3-4213-90c9-1e68ec4ac4e7.png) -### 2. 扩容 +### 2. 扩容 -添加元素时使用 ensureCapacityInternal() 方法来保证容量足够,如果不够时,需要使用 grow() 方法进行扩容,新容量的大小为 `oldCapacity + (oldCapacity >> 1)`,也就是旧容量的 1.5 倍。 +添加元素时使用 ensureCapacityInternal() 方法来保证容量足够,如果不够时,需要使用 grow() 方法进行扩容,新容量的大小为 `oldCapacity + (oldCapacity >> 1)`,也就是旧容量的 1.5 倍。 -扩容操作需要调用 `Arrays.copyOf()` 把原数组整个复制到新数组中,这个操作代价很高,因此最好在创建 ArrayList 对象时就指定大概的容量大小,减少扩容操作的次数。 +扩容操作需要调用 `Arrays.copyOf()` 把原数组整个复制到新数组中,这个操作代价很高,因此最好在创建 ArrayList 对象时就指定大概的容量大小,减少扩容操作的次数。 ```java -public boolean add(E e) { - ensureCapacityInternal(size + 1); // Increments modCount!! - elementData[size++] = e; - return true; +public boolean add(E e) { +    ensureCapacityInternal(size + 1);  // Increments modCount!! +    elementData[size++] = e; +    return true; } -private void ensureCapacityInternal(int minCapacity) { - if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { - minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity); - } - ensureExplicitCapacity(minCapacity); +private void ensureCapacityInternal(int minCapacity) { +    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { +        minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity); +    } +    ensureExplicitCapacity(minCapacity); } -private void ensureExplicitCapacity(int minCapacity) { - modCount++; - // overflow-conscious code - if (minCapacity - elementData.length > 0) - grow(minCapacity); +private void ensureExplicitCapacity(int minCapacity) { +    modCount++; +    // overflow-conscious code +    if (minCapacity - elementData.length > 0) +        grow(minCapacity); } -private void grow(int minCapacity) { - // overflow-conscious code - int oldCapacity = elementData.length; - int newCapacity = oldCapacity + (oldCapacity >> 1); - if (newCapacity - minCapacity < 0) - newCapacity = minCapacity; - if (newCapacity - MAX_ARRAY_SIZE > 0) - newCapacity = hugeCapacity(minCapacity); - // minCapacity is usually close to size, so this is a win: - elementData = Arrays.copyOf(elementData, newCapacity); +private void grow(int minCapacity) { +    // overflow-conscious code +    int oldCapacity = elementData.length; +    int newCapacity = oldCapacity + (oldCapacity >> 1); +    if (newCapacity - minCapacity < 0) +        newCapacity = minCapacity; +    if (newCapacity - MAX_ARRAY_SIZE > 0) +        newCapacity = hugeCapacity(minCapacity); +    // minCapacity is usually close to size, so this is a win: +    elementData = Arrays.copyOf(elementData, newCapacity); } ``` -### 3. 删除元素 +### 3. 删除元素 -需要调用 System.arraycopy() 将 index+1 后面的元素都复制到 index 位置上,该操作的时间复杂度为 O(N),可以看出 ArrayList 删除元素的代价是非常高的。 +需要调用 System.arraycopy() 将 index+1 后面的元素都复制到 index 位置上,该操作的时间复杂度为 O(N),可以看出 ArrayList 删除元素的代价是非常高的。 ```java -public E remove(int index) { - rangeCheck(index); - modCount++; - E oldValue = elementData(index); - int numMoved = size - index - 1; - if (numMoved > 0) - System.arraycopy(elementData, index+1, elementData, index, numMoved); - elementData[--size] = null; // clear to let GC do its work - return oldValue; +public E remove(int index) { +    rangeCheck(index); +    modCount++; +    E oldValue = elementData(index); +    int numMoved = size - index - 1; +    if (numMoved > 0) +        System.arraycopy(elementData, index+1, elementData, index, numMoved); +    elementData[--size] = null; // clear to let GC do its work +    return oldValue; } ``` -### 4. Fail-Fast +### 4. Fail-Fast -modCount 用来记录 ArrayList 结构发生变化的次数。结构发生变化是指添加或者删除至少一个元素的所有操作,或者是调整内部数组的大小,仅仅只是设置元素的值不算结构发生变化。 +modCount 用来记录 ArrayList 结构发生变化的次数。结构发生变化是指添加或者删除至少一个元素的所有操作,或者是调整内部数组的大小,仅仅只是设置元素的值不算结构发生变化。 -在进行序列化或者迭代等操作时,需要比较操作前后 modCount 是否改变,如果改变了需要抛出 ConcurrentModificationException。 +在进行序列化或者迭代等操作时,需要比较操作前后 modCount 是否改变,如果改变了需要抛出 ConcurrentModificationException。 ```java -private void writeObject(java.io.ObjectOutputStream s) - throws java.io.IOException{ - // Write out element count, and any hidden stuff - int expectedModCount = modCount; - s.defaultWriteObject(); +private void writeObject(java.io.ObjectOutputStream s) +    throws java.io.IOException{ +    // Write out element count, and any hidden stuff +    int expectedModCount = modCount; +    s.defaultWriteObject(); - // Write out size as capacity for behavioural compatibility with clone() - s.writeInt(size); +    // Write out size as capacity for behavioural compatibility with clone() +    s.writeInt(size); - // Write out all elements in the proper order. - for (int i=0; i