diff --git a/notes/2016 校招真题题解.md b/notes/2016 校招真题题解.md deleted file mode 100644 index da6709ab..00000000 --- a/notes/2016 校招真题题解.md +++ /dev/null @@ -1,742 +0,0 @@ - -* [前言](#前言) -* [1. 小米-小米Git](#1-小米-小米git) -* [2. 小米-懂二进制](#2-小米-懂二进制) -* [3. 小米-中国牛市](#3-小米-中国牛市) -* [4. 微软-LUCKY STRING](#4-微软-lucky-string) -* [5. 微软-Numeric Keypad](#5-微软-numeric-keypad) -* [6. 微软-Spring Outing](#6-微软-spring-outing) -* [7. 微软-S-expression](#7-微软-s-expression) -* [8. 华为-最高分是多少](#8-华为-最高分是多少) -* [9. 华为-简单错误记录](#9-华为-简单错误记录) -* [10. 华为-扑克牌大小](#10-华为-扑克牌大小) -* [11. 去哪儿-二分查找](#11-去哪儿-二分查找) -* [12. 去哪儿-首个重复字符](#12-去哪儿-首个重复字符) -* [13. 去哪儿-寻找Coder](#13-去哪儿-寻找coder) -* [14. 美团-最大差值](#14-美团-最大差值) -* [15. 美团-棋子翻转](#15-美团-棋子翻转) -* [16. 美团-拜访](#16-美团-拜访) -* [17. 美团-直方图内最大矩形](#17-美团-直方图内最大矩形) -* [18. 美团-字符串计数](#18-美团-字符串计数) -* [19. 美团-平均年龄](#19-美团-平均年龄) -* [20. 百度-罪犯转移](#20-百度-罪犯转移) -* [22. 百度-裁减网格纸](#22-百度-裁减网格纸) -* [23. 百度-钓鱼比赛](#23-百度-钓鱼比赛) -* [24. 百度-蘑菇阵](#24-百度-蘑菇阵) - - - -# 前言 - -省略的代码: - -```java -import java.util.*; -``` - -```java -public class Solution { -} -``` - -```java -public class Main { - public static void main(String[] args) { - Scanner in = new Scanner(System.in); - while (in.hasNext()) { - } - } -} -``` - -# 1. 小米-小米Git - -- 重建多叉树 -- 使用 LCA - -```java -private class TreeNode { - int id; - List childs = new ArrayList<>(); - - TreeNode(int id) { - this.id = id; - } -} - -public int getSplitNode(String[] matrix, int indexA, int indexB) { - int n = matrix.length; - boolean[][] linked = new boolean[n][n]; // 重建邻接矩阵 - for (int i = 0; i < n; i++) { - for (int j = 0; j < n; j++) { - linked[i][j] = matrix[i].charAt(j) == '1'; - } - } - TreeNode tree = constructTree(linked, 0); - TreeNode ancestor = LCA(tree, new TreeNode(indexA), new TreeNode(indexB)); - return ancestor.id; -} - -private TreeNode constructTree(boolean[][] linked, int root) { - TreeNode tree = new TreeNode(root); - for (int i = 0; i < linked[root].length; i++) { - if (linked[root][i]) { - linked[i][root] = false; // 因为题目给的邻接矩阵是双向的,在这里需要把它转为单向的 - tree.childs.add(constructTree(links, i)); - } - } - return tree; -} - -private TreeNode LCA(TreeNode root, TreeNode p, TreeNode q) { - if (root == null || root.id == p.id || root.id == q.id) return root; - TreeNode ancestor = null; - int cnt = 0; - for (int i = 0; i < root.childs.size(); i++) { - TreeNode tmp = LCA(root.childs.get(i), p, q); - if (tmp != null) { - ancestor = tmp; - cnt++; - } - } - return cnt == 2 ? root : ancestor; -} -``` - -# 2. 小米-懂二进制 - -对两个数进行异或,结果的二进制表示为 1 的那一位就是两个数不同的位。 - -```java -public int countBitDiff(int m, int n) { - return Integer.bitCount(m ^ n); -} -``` - -# 3. 小米-中国牛市 - -背包问题,可以设一个大小为 2 的背包。 - -状态转移方程如下: - -```html -dp[i, j] = max(dp[i, j-1], prices[j] - prices[jj] + dp[i-1, jj]) { jj in range of [0, j-1] } = max(dp[i, j-1], prices[j] + max(dp[i-1, jj] - prices[jj])) -``` - -```java -public int calculateMax(int[] prices) { - int n = prices.length; - int[][] dp = new int[3][n]; - for (int i = 1; i <= 2; i++) { - int localMax = dp[i - 1][0] - prices[0]; - for (int j = 1; j < n; j++) { - dp[i][j] = Math.max(dp[i][j - 1], prices[j] + localMax); - localMax = Math.max(localMax, dp[i - 1][j] - prices[j]); - } - } - return dp[2][n - 1]; -} -``` - -# 4. 微软-LUCKY STRING - -- 斐波那契数列可以预计算; -- 从头到尾遍历字符串的过程,每一轮循环都使用一个 Set 来保存从 i 到 j 出现的字符,并且 Set 保证了字符都不同,因此 Set 的大小就是不同字符的个数。 - -```java -Set fibSet = new HashSet<>(Arrays.asList(1, 2, 3, 5, 8, 13, 21, 34, 55, 89)); -Scanner in = new Scanner(System.in); -String str = in.nextLine(); -int n = str.length(); -Set ret = new HashSet<>(); -for (int i = 0; i < n; i++) { - Set set = new HashSet<>(); - for (int j = i; j < n; j++) { - set.add(str.charAt(j)); - int cnt = set.size(); - if (fibSet.contains(cnt)) { - ret.add(str.substring(i, j + 1)); - } - } -} -String[] arr = ret.toArray(new String[ret.size()]); -Arrays.sort(arr); -for (String s : arr) { - System.out.println(s); -} -``` - -# 5. 微软-Numeric Keypad - -```java -private static int[][] canReach = { - {1, 0, 0, 0, 0, 0, 0, 0, 0, 0}, // 0 - {1, 1, 1, 1, 1, 1, 1, 1, 1, 1}, // 1 - {1, 0, 1, 1, 0, 1, 1, 0, 1, 1}, // 2 - {0, 0, 0, 1, 0, 0, 1, 0, 0, 1}, // 3 - {1, 0, 0, 0, 1, 1, 1, 1, 1, 1}, // 4 - {1, 0, 0, 0, 0, 1, 1, 0, 1, 1}, // 5 - {0, 0, 0, 0, 0, 0, 1, 0, 0, 1}, // 6 - {1, 0, 0, 0, 0, 0, 0, 1, 1, 1}, // 7 - {1, 0, 0, 0, 0, 0, 0, 0, 1, 1}, // 8 - {0, 0, 0, 0, 0, 0, 0, 0, 0, 1} // 9 -}; - -private static boolean isLegal(char[] chars, int idx) { - if (idx >= chars.length || idx < 0) return true; - int cur = chars[idx] - '0'; - int next = chars[idx + 1] - '0'; - return canReach[cur][next] == 1; -} - -public static void main(String[] args) { - Scanner in = new Scanner(System.in); - int T = Integer.valueOf(in.nextLine()); - for (int i = 0; i < T; i++) { - String line = in.nextLine(); - char[] chars = line.toCharArray(); - for (int j = 0; j < chars.length - 1; j++) { - while (!isLegal(chars, j)) { - if (--chars[j + 1] < '0') { - chars[j--]--; - } - for (int k = j + 2; k < chars.length; k++) { - chars[k] = '9'; - } - } - } - System.out.println(new String(chars)); - } -} -``` - -# 6. 微软-Spring Outing - -下面以 N = 3,K = 4 来进行讨论。 - -初始时,令第 0 个地方成为待定地点,也就是呆在家里。 - -从第 4 个地点开始投票,每个人只需要比较第 4 个地方和第 0 个地方的优先级,里,如果超过半数的人选择了第 4 个地方,那么更新第 4 个地方成为待定地点。 - -从后往前不断重复以上步骤,不断更新待定地点,直到所有地方都已经投票。 - -上面的讨论中,先令第 0 个地点成为待定地点,是因为这样的话第 4 个地点就只需要和这个地点进行比较,而不用考虑其它情况。如果最开始先令第 1 个地点成为待定地点,那么在对第 2 个地点进行投票时,每个人不仅要考虑第 2 个地点与第 1 个地点的优先级,也要考虑与其后投票地点的优先级。 - -```java -int N = in.nextInt(); -int K = in.nextInt(); -int[][] votes = new int[N][K + 1]; -for (int i = 0; i < N; i++) { - for (int j = 0; j < K + 1; j++) { - int place = in.nextInt(); - votes[i][place] = j; - } -} -int ret = 0; -for (int place = K; place > 0; place--) { - int cnt = 0; - for (int i = 0; i < N; i++) { - if (votes[i][place] < votes[i][ret]) { - cnt++; - } - } - if (cnt > N / 2) { - ret = place; - } -} -System.out.println(ret == 0 ? "otaku" : ret); -``` - -# 7. 微软-S-expression - -# 8. 华为-最高分是多少 - -```java -int N = in.nextInt(); -int M = in.nextInt(); -int[] scores = new int[N]; -for (int i = 0; i < N; i++) { - scores[i] = in.nextInt(); -} -for (int i = 0; i < M; i++) { - String str = in.next(); - if (str.equals("U")) { - int id = in.nextInt() - 1; - int newScore = in.nextInt(); - scores[id] = newScore; - } else { - int idBegin = in.nextInt() - 1; - int idEnd = in.nextInt() - 1; - int ret = 0; - if (idBegin > idEnd) { - int t = idBegin; - idBegin = idEnd; - idEnd = t; - } - for (int j = idBegin; j <= idEnd; j++) { - ret = Math.max(ret, scores[j]); - } - System.out.println(ret); - } -} -``` - -# 9. 华为-简单错误记录 - -```java -HashMap map = new LinkedHashMap<>(); -while (in.hasNextLine()) { - String s = in.nextLine(); - String key = s.substring(s.lastIndexOf('\\') + 1); - map.put(key, map.containsKey(key) ? map.get(key) + 1 : 1); -} -List> list = new LinkedList<>(map.entrySet()); -Collections.sort(list, (o1, o2) -> o2.getValue() - o1.getValue()); -for (int i = 0; i < 8 && i < list.size(); i++) { - String[] token = list.get(i).getKey().split(" "); - String filename = token[0]; - String line = token[1]; - if (filename.length() > 16) filename = filename.substring(filename.length() - 16); - System.out.println(filename + " " + line + " " + list.get(i).getValue()); -} -``` - -# 10. 华为-扑克牌大小 - -```java -public class Main { - - private Map map = new HashMap<>(); - - public Main() { - map.put("3", 0); - map.put("4", 1); - map.put("5", 2); - map.put("6", 3); - map.put("7", 4); - map.put("8", 5); - map.put("9", 6); - map.put("10", 7); - map.put("J", 8); - map.put("Q", 9); - map.put("K", 10); - map.put("A", 11); - map.put("2", 12); - map.put("joker", 13); - map.put("JOKER ", 14); - } - - private String play(String s1, String s2) { - String[] token1 = s1.split(" "); - String[] token2 = s2.split(" "); - CardType type1 = computeCardType(token1); - CardType type2 = computeCardType(token2); - if (type1 == CardType.DoubleJoker) return s1; - if (type2 == CardType.DoubleJoker) return s2; - if (type1 == CardType.Bomb && type2 != CardType.Bomb) return s1; - if (type2 == CardType.Bomb && type1 != CardType.Bomb) return s2; - if (type1 != type2 || token1.length != token2.length) return "ERROR"; - for (int i = 0; i < token1.length; i++) { - int val1 = map.get(token1[i]); - int val2 = map.get(token2[i]); - if (val1 != val2) return val1 > val2 ? s1 : s2; - } - return "ERROR"; - } - - private CardType computeCardType(String[] token) { - boolean hasjoker = false, hasJOKER = false; - for (int i = 0; i < token.length; i++) { - if (token[i].equals("joker")) hasjoker = true; - else if (token[i].equals("JOKER")) hasJOKER = true; - } - if (hasjoker && hasJOKER) return CardType.DoubleJoker; - int maxContinueLen = 1; - int curContinueLen = 1; - String curValue = token[0]; - for (int i = 1; i < token.length; i++) { - if (token[i].equals(curValue)) curContinueLen++; - else { - curContinueLen = 1; - curValue = token[i]; - } - maxContinueLen = Math.max(maxContinueLen, curContinueLen); - } - if (maxContinueLen == 4) return CardType.Bomb; - if (maxContinueLen == 3) return CardType.Triple; - if (maxContinueLen == 2) return CardType.Double; - boolean isStraight = true; - for (int i = 1; i < token.length; i++) { - if (map.get(token[i]) - map.get(token[i - 1]) != 1) { - isStraight = false; - break; - } - } - if (isStraight && token.length == 5) return CardType.Straight; - return CardType.Sigal; - } - - private enum CardType { - DoubleJoker, Bomb, Sigal, Double, Triple, Straight; - } - - public static void main(String[] args) { - Main main = new Main(); - Scanner in = new Scanner(System.in); - while (in.hasNextLine()) { - String s = in.nextLine(); - String[] token = s.split("-"); - System.out.println(main.play(token[0], token[1])); - } - } -} -``` - -# 11. 去哪儿-二分查找 - -对于有重复元素的有序数组,二分查找需要注意以下要点: - -- if (val <= A[m]) h = m; -- 因为 h 的赋值为 m 而不是 m - 1,因此 while 循环的条件也就为 l < h。(如果是 m - 1 循环条件为 l <= h) - -```java -public int getPos(int[] A, int n, int val) { - int l = 0, h = n - 1; - while (l < h) { - int m = l + (h - l) / 2; - if (val <= A[m]) h = m; - else l = m + 1; - } - return A[h] == val ? h : -1; -} -``` - -# 12. 去哪儿-首个重复字符 - -```java -public char findFirstRepeat(String A, int n) { - boolean[] hasAppear = new boolean[256]; - for (int i = 0; i < n; i++) { - char c = A.charAt(i); - if(hasAppear[c]) return c; - hasAppear[c] = true; - } - return ' '; -} -``` - -# 13. 去哪儿-寻找Coder - -```java -public String[] findCoder(String[] A, int n) { - List> list = new ArrayList<>(); - for (String s : A) { - int cnt = 0; - String t = s.toLowerCase(); - int idx = -1; - while (true) { - idx = t.indexOf("coder", idx + 1); - if (idx == -1) break; - cnt++; - } - if (cnt != 0) { - list.add(new Pair<>(s, cnt)); - } - } - Collections.sort(list, (o1, o2) -> (o2.getValue() - o1.getValue())); - String[] ret = new String[list.size()]; - for (int i = 0; i < list.size(); i++) { - ret[i] = list.get(i).getKey(); - } - return ret; -} - -// 牛客网无法导入 javafx.util.Pair,这里就自己实现一下 Pair 类 -private class Pair { - T t; - K k; - - Pair(T t, K k) { - this.t = t; - this.k = k; - } - - T getKey() { - return t; - } - - K getValue() { - return k; - } -} -``` - -# 14. 美团-最大差值 - -贪心策略。 - -```java -public int getDis(int[] A, int n) { - int max = 0; - int soFarMin = A[0]; - for (int i = 1; i < n; i++) { - if(soFarMin > A[i]) soFarMin = A[i]; - else max = Math.max(max, A[i]- soFarMin); - } - return max; -} -``` - -# 15. 美团-棋子翻转 - -```java -public int[][] flipChess(int[][] A, int[][] f) { - int[][] direction = {{0, 1}, {0, -1}, {1, 0}, {-1, 0}}; - for (int[] ff : f) { - for (int[] dd : direction) { - int r = ff[0] + dd[0] - 1, c = ff[1] + dd[1] - 1; - if(r < 0 || r > 3 || c < 0 || c > 3) continue; - A[r][c] ^= 1; - } - } - return A; -} -``` - -# 16. 美团-拜访 - -```java -private Set paths; -private List curPath; - -public int countPath(int[][] map, int n, int m) { - paths = new HashSet<>(); - curPath = new ArrayList<>(); - for (int i = 0; i < n; i++) { - for (int j = 0; j < m; j++) { - if (map[i][j] == 1) { - map[i][j] = -1; - int[][] leftRightDirection = {{1, 0}, {-1, 0}}; - int[][] topDownDirection = {{0, 1}, {0, -1}}; - for (int[] lr : leftRightDirection) { - for (int[] td : topDownDirection) { - int[][] directions = {lr, td}; - backtracking(map, n, m, i, j, directions); - } - } - return paths.size(); - } - } - } - return 0; -} - -private void backtracking(int[][] map, int n, int m, int r, int c, int[][] directions) { - if (map[r][c] == 2) { - String path = ""; - for (int num : curPath) { - path += num; - } - paths.add(path); - return; - } - for (int i = 0; i < directions.length; i++) { - int nextR = r + directions[i][0]; - int nextC = c + directions[i][1]; - if (nextR < 0 || nextR >= n || nextC < 0 || nextC >= m || map[nextR][nextC] == -1) continue; - map[nextR][nextC] = map[nextR][nextC] == 2 ? 2 : -1; - curPath.add(nextR); - curPath.add(nextC); - backtracking(map, n, m, nextR, nextC, directions); - curPath.remove(curPath.size() - 1); - curPath.remove(curPath.size() - 1); - map[nextR][nextC] = map[nextR][nextC] == 2 ? 2 : 0; - } -} -``` - -# 17. 美团-直方图内最大矩形 - -```java -public int countArea(int[] A, int n) { - int max = 0; - for (int i = 0; i < n; i++) { - int min = A[i]; - for (int j = i; j < n; j++) { - min = Math.min(min, A[j]); - max = Math.max(max, min * (j - i + 1)); - } - } - return max; -} -``` - -# 18. 美团-字符串计数 - -字符串都是小写字符,可以把字符串当成是 26 进制。但是字典序的比较和普通的整数比较不同,是从左往右进行比较,例如 "ac" 和 "abc",字典序的比较结果为 "ac" > "abc",如果按照整数方法比较,因为 "abc" 是三位数,显然更大。 - -由于两个字符串的长度可能不想等,在 s1 空白部分和 s2 对应部分进行比较时,应该把 s1 的空白部分看成是 'a' 字符进行填充的。 - -还有一点要注意的是,s1 到 s2 长度为 leni 的字符串个数只比较前面 i 个字符。例如 'aaa' 和 'bbb' ,长度为 2 的个数为 'aa' 到 'bb' 的字符串个数,不需要考虑后面部分的字符。 - -在统计个数时,从 len1 开始一直遍历到最大合法长度,每次循环都统计长度为 i 的子字符串个数。 - -```java -String s1 = in.next(); -String s2 = in.next(); -int len1 = in.nextInt(); -int len2 = in.nextInt(); -int len = Math.min(s2.length(), len2); -int[] subtractArr = new int[len]; -for (int i = 0; i < len; i++) { - char c1 = i < s1.length() ? s1.charAt(i) : 'a'; - char c2 = s2.charAt(i); - subtractArr[i] = c2 - c1; -} -int ret = 0; -for (int i = len1; i <= len; i++) { - for (int j = 0; j < i; j++) { - ret += subtractArr[j] * Math.pow(26, i - j - 1); - } -} -System.out.println(ret - 1); -``` - -# 19. 美团-平均年龄 - -```java -int W = in.nextInt(); -double Y = in.nextDouble(); -double x = in.nextDouble(); -int N = in.nextInt(); -while (N-- > 0) { - Y++; // 老员工每年年龄都要加 1 - Y += (21 - Y) * x; -} -System.out.println((int) Math.ceil(Y)); -``` - -# 20. 百度-罪犯转移 - -部分和问题,将每次求的部分和缓存起来。 - -```java -int n = in.nextInt(); -int t = in.nextInt(); -int c = in.nextInt(); -int[] values = new int[n]; -for (int i = 0; i < n; i++) { - values[i] = in.nextInt(); -} -int cnt = 0; -int totalValue = 0; -for (int s = 0, e = c - 1; e < n; s++, e++) { - if (s == 0) { - for (int j = 0; j < c; j++) totalValue += values[j]; - } else { - totalValue = totalValue - values[s - 1] + values[e]; - } - if (totalValue <= t) cnt++; -} -System.out.println(cnt); -``` - -# 22. 百度-裁减网格纸 - -```java -int n = in.nextInt(); -int minX, minY, maxX, maxY; -minX = minY = Integer.MAX_VALUE; -maxX = maxY = Integer.MIN_VALUE; -for (int i = 0; i < n; i++) { - int x = in.nextInt(); - int y = in.nextInt(); - minX = Math.min(minX, x); - minY = Math.min(minY, y); - maxX = Math.max(maxX, x); - maxY = Math.max(maxY, y); -} -System.out.println((int) Math.pow(Math.max(maxX - minX, maxY - minY), 2)); -``` - -# 23. 百度-钓鱼比赛 - -P ( 至少钓一条鱼 ) = 1 - P ( 一条也钓不到 ) - -坑:读取概率矩阵的时候,需要一行一行进行读取,而不能直接用 in.nextDouble()。 - -```java -public static void main(String[] args) { - Scanner in = new Scanner(System.in); - while (in.hasNext()) { - int n = in.nextInt(); - int m = in.nextInt(); - int x = in.nextInt(); - int y = in.nextInt(); - int t = in.nextInt(); - in.nextLine(); // 坑 - double pcc = 0.0; - double sum = 0.0; - for (int i = 1; i <= n; i++) { - String[] token = in.nextLine().split(" "); // 坑 - for (int j = 1; j <= m; j++) { - double p = Double.parseDouble(token[j - 1]); - // double p = in.nextDouble(); - sum += p; - if (i == x && j == y) { - pcc = p; - } - } - } - double pss = sum / (n * m); - pcc = computePOfIRT(pcc, t); - pss = computePOfIRT(pss, t); - System.out.println(pcc > pss ? "cc" : pss > pcc ? "ss" : "equal"); - System.out.printf("%.2f\n", Math.max(pcc, pss)); - } -} - -// compute probability of independent repeated trials -private static double computePOfIRT(double p, int t) { - return 1 - Math.pow((1 - p), t); -} -``` - -# 24. 百度-蘑菇阵 - -这题用回溯会超时,需要用 DP。 - -dp[i][j] 表示到达 (i,j) 位置不会触碰蘑菇的概率。对于 N\*M 矩阵,如果 i == N || j == M,那么 (i,j) 只能有一个移动方向;其它情况下能有两个移动方向。 - -考虑以下矩阵,其中第 3 行和第 3 列只能往一个方向移动,而其它位置可以有两个方向移动。 - - -```java -int N = in.nextInt(); -int M = in.nextInt(); -int K = in.nextInt(); -boolean[][] mushroom = new boolean[N][M]; -while (K-- > 0) { - int x = in.nextInt(); - int y = in.nextInt(); - mushroom[x - 1][y - 1] = true; -} -double[][] dp = new double[N][M]; -dp[0][0] = 1; -for (int i = 0; i < N; i++) { - for (int j = 0; j < M; j++) { - if (mushroom[i][j]) dp[i][j] = 0; - else { - double cur = dp[i][j]; - if (i == N - 1 && j == M - 1) break; - if (i == N - 1) dp[i][j + 1] += cur; - else if (j == M - 1) dp[i + 1][j] += cur; - else { - dp[i][j + 1] += cur / 2; - dp[i + 1][j] += cur / 2; - } - } - } -} -System.out.printf("%.2f\n", dp[N - 1][M - 1]); -``` diff --git a/notes/HTTP.md b/notes/HTTP.md deleted file mode 100644 index 58c158bf..00000000 --- a/notes/HTTP.md +++ /dev/null @@ -1,407 +0,0 @@ - -* [基础概念](#基础概念) - * [Web 基础](#web-基础) - * [URL](#url) - * [请求和响应报文](#请求和响应报文) -* [HTTP 方法](#http-方法) - * [GET:获取资源](#get获取资源) - * [POST:传输实体主体](#post传输实体主体) - * [HEAD:获取报文首部](#head获取报文首部) - * [PUT:上传文件](#put上传文件) - * [DELETE:删除文件](#delete删除文件) - * [OPTIONS:查询支持的方法](#options查询支持的方法) - * [TRACE:追踪路径](#trace追踪路径) - * [CONNECT:要求用隧道协议连接代理](#connect要求用隧道协议连接代理) -* [HTTP 状态码](#http-状态码) - * [2XX 成功](#2xx-成功) - * [3XX 重定向](#3xx-重定向) - * [4XX 客户端错误](#4xx-客户端错误) - * [5XX 服务器错误](#5xx-服务器错误) -* [HTTP 首部](#http-首部) - * [通用首部字段](#通用首部字段) - * [请求首部字段](#请求首部字段) - * [响应首部字段](#响应首部字段) - * [实体首部字段](#实体首部字段) -* [具体应用](#具体应用) - * [Cookie](#cookie) - * [缓存](#缓存) - * [持久连接](#持久连接) - * [编码](#编码) - * [分块传输](#分块传输) - * [多部分对象集合](#多部分对象集合) - * [范围请求](#范围请求) - * [内容协商](#内容协商) - * [虚拟主机](#虚拟主机) - * [通信数据转发](#通信数据转发) -* [HTTPs](#https) - * [加密](#加密) - * [认证](#认证) - * [完整性](#完整性) -* [HTTP/1.0 与 HTTP/1.1 的区别](#http10-与-http11-的区别) - - - -# 基础概念 - -## Web 基础 - -- HTTP(HyperText Transfer Protocol,超文本传输协议)。 -- WWW(World Wide Web)的三种技术:HTML、HTTP、URL。 -- RFC(Request for Comments,征求修正意见书),互联网的设计文档。 - -## URL - -- URI(Uniform Resource Indentifier,统一资源标识符) -- URL(Uniform Resource Locator,统一资源定位符) -- URN(Uniform Resource Name,统一资源名称),例如 urn:isbn:0-486-27557-4 。 - -URI 包含 URL 和 URN,目前 WEB 只有 URL 比较流行,所以见到的基本都是 URL。 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//4102b7d0-39b9-48d8-82ae-ac4addb7ebfb.jpg) - -## 请求和响应报文 - -**请求报文** - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//9dbb5fc2-936b-4c6d-b3a7-9617aae45080.jpg) - -**响应报文** - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//c634b5ed-a14b-4302-b40e-3ee387dd3c8a.jpg) - -# HTTP 方法 - -客户端发送的请求报文第一行为请求行,包含了方法字段。 - -## GET:获取资源 - -## POST:传输实体主体 - -POST 主要目的不是获取资源,而是传输实体主体数据。 - -GET 和 POST 的请求都能使用额外的参数,但是 GET 的参数是以查询字符串出现在 URL中,而 POST 的参数存储在实体主体部分。 - -``` -GET /test/demo_form.asp?name1=value1&name2=value2 HTTP/1.1 -``` -``` -POST /test/demo_form.asp HTTP/1.1 -Host: w3schools.com -name1=value1&name2=value2 -``` - -GET 的传参方式相比于 POST 安全性较差,因为 GET 传的参数在 URL 是可见的,可能会泄露私密信息。并且 GET 只支持 ASCII 字符,如果参数为中文则可能会出现乱码,而 POST 支持标准字符集。 - -## HEAD:获取报文首部 - -和 GET 方法一样,但是不返回报文实体主体部分。 - -主要用于确认 URL 的有效性以及资源更新的日期时间等。 - -## PUT:上传文件 - -由于自身不带验证机制,任何人都可以上传文件,因此存在安全性问题,一般 WEB 网站不使用该方法。 - -## DELETE:删除文件 - -与 PUT 功能相反,并且同样不带验证机制。 - -## OPTIONS:查询支持的方法 - -查询指定的 URL 能够支持的方法。 - -会返回 Allow: GET, POST, HEAD, OPTIONS 这样的内容。 - -## TRACE:追踪路径 - -服务器会将通信路径返回给客户端。 - -发送请求时,在 Max-Forwards 首部字段中填入数值,每经过一个服务器就会减 1,当数值为 0 时就停止传输。 - -TRACE 一般不会使用,并且它容易受到 XST 攻击(Cross-Site Tracing,跨站追踪),因此更不会去使用它。 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//ca711108-e937-4d7d-99aa-61b325c61f1a.jpg) - -## CONNECT:要求用隧道协议连接代理 - -主要使用 SSL(Secure Sokets Layer,安全套接字)和 TLS(Transport Layer Security,传输层安全)协议把通信内容加密后经网络隧道传输。 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//d8355d56-aa2b-4452-8001-8475cc095af1.jpg) - -# HTTP 状态码 - -服务器返回的响应报文中第一行为状态行,包含了状态码以及原因短语,来告知客户端请求的结果。 - -| 状态码 | 类别 | 原因短语 | -| --- | --- | --- | -| 1XX | Informational(信息性状态码) | 接收的请求正在处理 | -| 2XX | Success(成功状态码) | 请求正常处理完毕 | -| 3XX | Redirection(重定向状态码) | 需要进行附加操作以完成请求 | -| 4XX | Client Error(客户端错误状态码) | 服务器无法处理请求 | -| 5XX | Server Error(服务器错误状态码) | 服务器处理请求出错 | - -## 2XX 成功 - -- **200 OK** - -- **204 No Content**:请求已经成功处理,但是返回的响应报文不包含实体的主体部分。一般在只需要从客户端往服务器发送信息,而不需要返回数据时使用。 - -- **206 Partial Content** - -## 3XX 重定向 - -- **301 Moved Permanently**:永久性重定向 - -- **302 Found**:临时性重定向 - -- **303 See Other** - -- 注:虽然 HTTP 协议规定 301、302 状态下重定向时不允许把 POST 方法改成 GET 方法,但是大多数浏览器都会 在 301、302 和 303 状态下的重定向把 POST 方法改成 GET 方法。 - -- **304 Not Modified**:如果请求报文首部包含一些条件,例如:If-Match,If-ModifiedSince,If-None-Match,If-Range,If-Unmodified-Since,但是不满足条件,则服务器会返回 304 状态码。 - -- **307 Temporary Redirect**:临时重定向,与 302 的含义类似,但是 307 要求浏览器不会把重定向请求的 POST 方法改成 GET 方法。 - -## 4XX 客户端错误 - -- **400 Bad Request**:请求报文中存在语法错误 - -- **401 Unauthorized**:该状态码表示发送的请求需要有通过 HTTP 认证(BASIC 认证、DIGEST 认证)的认证信息。如果之前已进行过一次请求,则表示用户认证失败。 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//b1b4cf7d-c54a-4ff1-9741-cd2eea331123.jpg) - -- **403 Forbidden**:请求被拒绝,服务器端没有必要给出拒绝的详细理由。 - -- **404 Not Found** - -## 5XX 服务器错误 - -- **500 Internal Server Error**:服务器正在执行请求时发生错误 - -- **503 Service Unavilable**:该状态码表明服务器暂时处于超负载或正在进行停机维护,现在无法处理请求。 - -# HTTP 首部 - -有 4 种类型的首部字段:通用首部字段、请求首部字段、响应首部字段和实体首部字段。 - -各种首部字段及其含义如下(不需要全记,仅供查阅): - -## 通用首部字段 - -| 首部字段名 | 说明 | -| -- | -- | -| Cache-Control | 控制缓存的行为 | -| Connection | 逐跳首部、 连接的管理 | -| Date | 创建报文的日期时间 | -| Pragma | 报文指令 | -| Trailer | 报文末端的首部一览 | -| Transfer-Encoding | 指定报文主体的传输编码方式 | -| Upgrade | 升级为其他协议 | -| Via | 代理服务器的相关信息 | -| Warning | 错误通知 | - -## 请求首部字段 - -| 首部字段名 | 说明 | -| -- | -- | -| Accept | 用户代理可处理的媒体类型 | -| Accept-Charset | 优先的字符集 | -| Accept-Encoding | 优先的内容编码 | -| Accept-Language | 优先的语言(自然语言) | -| Authorization | Web认证信息 | -| Expect | 期待服务器的特定行为 | -| From | 用户的电子邮箱地址 | -| Host | 请求资源所在服务器 | -| If-Match | 比较实体标记(ETag) | -| If-Modified-Since | 比较资源的更新时间 | -| If-None-Match | 比较实体标记(与 If-Match 相反) | -| If-Range | 资源未更新时发送实体 Byte 的范围请求 | -| If-Unmodified-Since | 比较资源的更新时间(与If-Modified-Since相反) | -| Max-Forwards | 最大传输逐跳数 | -| Proxy-Authorization | 代理服务器要求客户端的认证信息 | -| Range | 实体的字节范围请求 | -| Referer | 对请求中 URI 的原始获取方 | -| TE | 传输编码的优先级 | -| User-Agent | HTTP 客户端程序的信息 | - -## 响应首部字段 - -| 首部字段名 | 说明 | -| -- | -- | -| Accept-Ranges | 是否接受字节范围请求 | -| Age | 推算资源创建经过时间 | -| ETag | 资源的匹配信息 | -| Location | 令客户端重定向至指定URI | -| Proxy-Authenticate | 代理服务器对客户端的认证信息 | -| Retry-After | 对再次发起请求的时机要求 | -| Server | HTTP服务器的安装信息 | -| Vary | 代理服务器缓存的管理信息 | -| WWW-Authenticate | 服务器对客户端的认证信息 | - -## 实体首部字段 - -| 首部字段名 | 说明 | -| -- | -- | -| Allow | 资源可支持的HTTP方法 | -| Content-Encoding | 实体主体适用的编码方式 | -| Content-Language | 实体主体的自然语言 | -| Content-Length | 实体主体的大小(单位: 字节) | -| Content-Location | 替代对应资源的URI | -| Content-MD5 | 实体主体的报文摘要 | -| Content-Range | 实体主体的位置范围 | -| Content-Type | 实体主体的媒体类型 | -| Expires | 实体主体过期的日期时间 | -| Last-Modified | 资源的最后修改日期时间 | - -# 具体应用 - -## Cookie - -HTTP 协议是无状态的,主要是为了让 HTTP 协议尽可能简单,使得它能够处理大量事务。HTTP/1.1 引入 Cookie 来保存状态信息。 - -服务器发送的响应报文包含 Set-Cookie 字段,客户端得到响应报文后把 Cookie 内容保存到浏览器中。下次再发送请求时,从浏览器中读出 Cookie 值,在请求报文中包含 Cookie 字段,这样服务器就知道客户端的状态信息了。Cookie 状态信息保存在客户端浏览器中,而不是服务器上。 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//ff17c103-750a-4bb8-9afa-576327023af9.png) - -Set-Cookie 字段有以下属性: - -| 属性 | 说明 | -| -- | -- | -| NAME=VALUE | 赋予 Cookie 的名称和其值(必需项) | -| expires=DATE | Cookie 的有效期(若不明确指定则默认为浏览器关闭前为止) | -| path=PATH | 将服务器上的文件目录作为 Cookie 的适用对象(若不指定则默认为文档所在的文件目录) | -| domain=域名 | 作为 Cookie 适用对象的域名(若不指定则默认为创建 Cookie 的服务器的域名) | -| Secure | 仅在 HTTPS 安全通信时才会发送 Cookie | -| HttpOnly | 加以限制,使 Cookie 不能被 JavaScript 脚本访问 | - -**Session 和 Cookie 区别** - -Session 是服务器用来跟踪用户的一种手段,每个 Session 都有一个唯一标识:Session ID。当服务器创建了一个 Session 时,给客户端发送的响应报文就包含了 Set-Cookie 字段,其中有一个名为 sid 的键值对,这个键值对就是 Session ID。客户端收到后就把 Cookie 保存在浏览器中,并且之后发送的请求报文都包含 Session ID。HTTP 就是通过 Session 和 Cookie 这两种方式一起合作来实现跟踪用户状态的,Session 用于服务器端,Cookie 用于客户端。 - -**浏览器禁用 Cookie 的情况** - -会使用 URL 重写技术,在 URL 后面加上 sid=xxx 。 - -**使用 Cookie 实现用户名和密码的自动填写** - -网站脚本会自动从 Cookie 中读取用户名和密码,从而实现自动填写。 - -## 缓存 - -有两种缓存方法:让代理服务器进行缓存和让客户端浏览器进行缓存。 - -Cache-Control 用于控制缓存的行为。 - -Cache-Control: no-cache 有两种含义,如果是客户端向缓存服务器发送的请求报文中含有该指令,表示客户端不想要缓存的资源;如果是源服务器向缓存服务器发送的响应报文中含有该指令,表示缓存服务器不能对资源进行缓存。 - -Expires 字段可以用于告知缓存服务器该资源什么时候会过期。当首部字段 Cache-Control 有指定 max-age 指令时,比起首部字段 Expires,会优先处理 max-age 指令。 - -## 持久连接 - -当浏览器访问一个包含多张图片的 HTML 页面时,除了请求访问 HTML 页面资源,还会请求图片资源,如果每进行一次 HTTP 通信就要断开一次 TCP 连接,连接建立和断开的开销会很大。**持久连接** 只需要进行一次 TCP 连接就能进行多次 HTTP 通信。HTTP/1.1 开始,所有的连接默认都是持久连接。 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//c73a0b78-5f46-4d2d-a009-dab2a999b5d8.jpg) - -持久连接需要使用 Connection 首部字段进行管理。HTTP/1.1 开始 HTTP 默认是持久化连接的,如果要断开 TCP 连接,需要由客户端或者服务器端提出断开,使用 Connection: close;而在 HTTP/1.1 之前默认是非持久化连接的,如果要维持持续连接,需要使用 Keep-Alive。 - -管线化方式可以同时发送多个请求和响应,而不需要发送一个请求然后等待响应之后再发下一个请求。 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//6943e2af-5a70-4004-8bee-b33d60f39da3.jpg) - -## 编码 - -编码(Encoding)主要是为了对实体进行压缩。常用的编码有:gzip、compress、deflate、identity,其中 identity 表示不执行压缩的编码格式。 - -## 分块传输 - -分块传输(Chunked Transfer Coding)可以把数据分割成多块,让浏览器逐步显示页面。 - -## 多部分对象集合 - -一份报文主体内可含有多类型的实体同时发送,每个部分之间用 boundary 字段定义的分隔符进行分隔;每个部分都可以有首部字段。 - -例如,上传多个表单时可以使用如下方式: - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//decb0936-e83c-4a55-840a-fe8aa101ac61.png) - -## 范围请求 - -如果网络出现中断,服务器只发送了一部分数据,范围请求使得客户端能够只请求未发送的那部分数据,从而避免服务器端重新发送所有数据。 - -在请求报文首部中添加 Range 字段,然后指定请求的范围,例如 Range : bytes = 5001-10000。请求成功的话服务器发送 206 Partial Content 状态。 - -## 内容协商 - -通过内容协商返回最合适的内容,例如根据浏览器的默认语言选择返回中文界面还是英文界面。 - -涉及以下首部字段:Accept、Accept-Charset、Accept-Encoding、Accept-Language、Content-Language。 - -## 虚拟主机 - -使用虚拟主机技术,使得一台服务器拥有多个域名,并且在逻辑上可以看成多个服务器。 - -## 通信数据转发 - -**代理** - -代理服务器接受客户端的请求,并且转发给其它服务器。代理服务器一般是透明的,不会改变 URL。 - -使用代理的主要目的是:缓存、网络访问控制以及记录访问日志。 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//c07035c3-a9ba-4508-8e3c-d8ae4c6ee9ee.jpg) - -**网关** - -与代理服务器不同的是,网关服务器会将 HTTP 转化为其它协议进行通信,从而请求其它非 HTTP 服务器的服务。 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//81375888-6be1-476f-9521-42eea3e3154f.jpg) - -**隧道** - -使用 SSL 等加密手段,为客户端和服务器之间建立一条安全的通信线路。 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//64b95403-d976-421a-8b45-bac89c0b5185.jpg) - -# HTTPs - -HTTP 有以下安全性问题: - -1. 通信使用明文,内容可能会被窃听; -2. 不验证通信方的身份,因此有可能遭遇伪装; -3. 无法证明报文的完整性,所以有可能已遭篡改。 - -HTTPs 并不是新协议,而是 HTTP 先和 SSL(Secure Socket Layer)通信,再由 SSL 和 TCP 通信。通过使用 SSL,HTTPs 提供了加密、认证和完整性保护。 - -## 加密 - -有两种加密方式:对称密钥加密和公开密钥加密。对称密钥加密的加密和解密使用同一密钥,而公开密钥加密使用一对密钥用于加密和解密,分别为公开密钥和私有密钥。公开密钥所有人都可以获得,通信发送方获得接收方的公开密钥之后,就可以使用公开密钥进行加密,接收方收到通信内容后使用私有密钥解密。 - -对称密钥加密的缺点:无法安全传输密钥;公开密钥加密的缺点:相对来说更耗时。 - -HTTPs 采用 **混合的加密机制**,使用公开密钥加密用于传输对称密钥,之后使用对称密钥加密进行通信。(下图中,共享密钥即对称密钥) - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//110b1a9b-87cd-45c3-a21d-824623715b33.jpg) - -## 认证 - -通过使用 **证书** 来对通信方进行认证。证书中有公开密钥数据,如果可以验证公开密钥的确属于通信方的,那么就可以确定通信方是可靠的。 - -数字证书认证机构(CA,Certificate Authority)颁发的公开密钥证书,可以通过 CA 对其进行验证。 - -进行 HTTPs 通信时,服务器会把证书发送给客户端,客户端取得其中的公开密钥之后,就可以开始加密过程。 - -使用 OpenSSL 这套开源程序,每个人都可以构建一套属于自己的认证机构,从而自己给自己颁发服务器证书。浏览器在访问该服务器时,会显示“无法确认连接安全性”或“该网站的安全证书存在问题”等警告消息。 - -客户端证书需要用户自行安装,只有在业务需要非常高的安全性时才使用客户端证书,例如网上银行。 - -## 完整性 - -SSL 提供摘要功能来验证完整性。 - -# HTTP/1.0 与 HTTP/1.1 的区别 - -- HTTP/1.1 默认是长连接; -- HTTP/1.1 提供了范围请求功能; -- HTTP/1.1 提供了虚拟主机的功能; -- HTTP/1.1 多了一些缓存处理字段; -- HTTP/1.1 多了一些状态码; diff --git a/notes/JVM.md b/notes/JVM.md deleted file mode 100644 index cb7ced4e..00000000 --- a/notes/JVM.md +++ /dev/null @@ -1,674 +0,0 @@ - -* [内存模型](#内存模型) - * [1. 程序计数器](#1-程序计数器) - * [2. Java 虚拟机栈](#2-java-虚拟机栈) - * [3. 本地方法栈](#3-本地方法栈) - * [4. Java 堆](#4-java-堆) - * [5. 方法区](#5-方法区) - * [6. 运行时常量池](#6-运行时常量池) - * [7. 直接内存](#7-直接内存) -* [垃圾收集](#垃圾收集) - * [1. 判断一个对象是否可回收](#1-判断一个对象是否可回收) - * [1.1 引用计数](#11-引用计数) - * [1.2 可达性](#12-可达性) - * [1.3 引用类型](#13-引用类型) - * [1.3.1 强引用](#131-强引用) - * [1.3.2 软引用](#132-软引用) - * [1.3.3 弱引用](#133-弱引用) - * [1.3.4 虚引用](#134-虚引用) - * [1.3 方法区的回收](#13-方法区的回收) - * [1.4 finalize()](#14-finalize) - * [2. 垃圾收集算法](#2-垃圾收集算法) - * [2.1 标记 - 清除算法](#21-标记---清除算法) - * [2.2 复制算法](#22-复制算法) - * [2.3 标记 - 整理算法](#23-标记---整理算法) - * [2.4 分代收集算法](#24-分代收集算法) - * [3. 垃圾收集器](#3-垃圾收集器) - * [3.1 Serial 收集器](#31-serial-收集器) - * [3.2 ParNew 收集器](#32-parnew-收集器) - * [3.3 Parallel Scavenge 收集器](#33-parallel-scavenge-收集器) - * [3.4 Serial Old 收集器](#34-serial-old-收集器) - * [3.5 Parallel Old 收集器](#35-parallel-old-收集器) - * [3.6 CMS 收集器](#36-cms-收集器) - * [3.7 G1 收集器](#37-g1-收集器) - * [3.8 七种垃圾收集器的比较](#38-七种垃圾收集器的比较) - * [4. 内存分配与回收策略](#4-内存分配与回收策略) - * [4.1 优先在 Eden 分配](#41-优先在-eden-分配) - * [4.2 大对象直接进入老年代](#42-大对象直接进入老年代) - * [4.3 长期存活的对象进入老年代](#43-长期存活的对象进入老年代) - * [4.4 动态对象年龄判定](#44-动态对象年龄判定) - * [4.5 空间分配担保](#45-空间分配担保) - * [4.6 Full GC 的触发条件](#46-full-gc-的触发条件) - * [4.6.1 调用 System.gc()](#461-调用-systemgc) - * [4.6.2 老年代空间不足](#462-老年代空间不足) - * [4.6.3 空间分配担保失败](#463-空间分配担保失败) - * [4.6.4 JDK 1.7 及以前的永久代空间不足](#464-jdk-17-及以前的永久代空间不足) - * [4.6.5 Concurrent Mode Failure](#465-concurrent-mode-failure) -* [类加载机制](#类加载机制) - * [1 类的生命周期](#1-类的生命周期) - * [2. 类初始化时机](#2-类初始化时机) - * [3. 类加载过程](#3-类加载过程) - * [3.1 加载](#31-加载) - * [3.2 验证](#32-验证) - * [3.3 准备](#33-准备) - * [3.4 解析](#34-解析) - * [3.5 初始化](#35-初始化) - * [4. 类加载器](#4-类加载器) - * [4.1 类与类加载器](#41-类与类加载器) - * [4.2 类加载器分类](#42-类加载器分类) - * [4.3 双亲委派模型](#43-双亲委派模型) -* [JVM 参数](#jvm-参数) - * [GC 优化配置](#gc-优化配置) - * [GC 类型设置](#gc-类型设置) - - - -# 内存模型 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//dc695f48-4189-4fc7-b950-ed25f6c80f82.jpg) - -注:白色区域为线程私有的,蓝色区域为线程共享的。 - -## 1. 程序计数器 - -记录正在执行的虚拟机字节码指令的地址(如果正在执行的是 Native 方法则为空)。 - -## 2. Java 虚拟机栈 - -每个 Java 方法在执行的同时会创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在 Java 虚拟机栈中入栈和出栈的过程。 - -该区域可能抛出以下异常: - -1. 当线程请求的栈深度超过最大值,会抛出 StackOverflowError 异常; -2. 栈进行动态扩展时如果无法申请导足够内存,会抛出 OutOfMemoryError 异常。 - -## 3. 本地方法栈 - -与 Java 虚拟机栈类似,它们之间的区别只不过是本地方法栈为本地方法服务。 - -## 4. Java 堆 - -所有对象实例都在这里分配内存。 - -这块区域是垃圾收集器管理的主要区域("GC 堆 ")。现在收集器基本都是采用分代收集算法,Java 堆还可以分成:新生代和老年代(新生代还可以分成 Eden 空间、From Survivor 空间、To Survivor 空间等)。 - -不需要连续内存,可以通过 -Xmx 和 -Xms 来控制动态扩展内存大小,如果动态扩展失败会抛出 OutOfMemoryError 异常。 - -## 5. 方法区 - -用于存放已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。 - -和 Java 堆一样不需要连续的内存,并且可以动态扩展,动态扩展失败一样会抛出 OutOfMemoryError 异常。 - -对这块区域进行垃圾回收的主要目标是对常量池的回收和对类的卸载,但是一般比较难实现,HotSpot 虚拟机把它当成永久代来进行垃圾回收。 - -## 6. 运行时常量池 - -运行时常量池是方法区的一部分。 - -类加载后,Class 文件中的常量池(用于存放编译期生成的各种字面量和符号引用)就会被放到这个区域。 - -在运行期间也可以用过 String 类的 intern() 方法将新的常量放入该区域。 - -## 7. 直接内存 - -在 JDK 1.4 中新加入了 NIO 类,引入了一种基于通道(Channel)与缓冲区(Buffer)的 I/O 方式,它可以使用 Native 函数库直接分配堆外内存,然后通过一个存储在 Java 堆里的 DirectByteBuffer 对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在 Java 堆和 Native 堆中来回复制数据。 - -# 垃圾收集 - -程序计数器、虚拟机栈和本地方法栈这三个区域属于线程私有的,只存在于线程的生命周期内,线程结束之后也会消失,因此不需要对这三个区域进行垃圾回收。 - -垃圾回收主要是针对 Java 堆和方法区进行。 - -## 1. 判断一个对象是否可回收 - -### 1.1 引用计数 - -给对象添加一个引用计数器,当对象增加一个引用时计数器加 1,引用失效时计数器减 1。 - -引用计数为 0 的对象可被回收。 - -两个对象会出现循环引用问题,此时引用计数器永远不为 0,导致 GC 收集器无法回收。 - -```java -objA.instance = objB; -objB.instance = objA; -``` - -### 1.2 可达性 - -通过 GC Roots 作为起始点进行搜索,能够到达到的对象都是都是可用的,不可达的对象可被回收。 - -GC Roots 一般包含以下内容: - -1. 虚拟机栈中引用的对象 -2. 方法区中类静态属性引用的对象 -3. 方法区中的常量引用的对象 -4. 本地方法栈中引用的对象 - -### 1.3 引用类型 - -无论是通过引用计算算法判断对象的引用数量,还是通过可达性分析算法判断对象的引用链是否可达,判定独享是否存活都与“引用”有关。 - -#### 1.3.1 强引用 - -只要强引用存在,垃圾回收器永远不会回收调掉被引用的对象。 - -```java -Object obj = new Object(); -``` - -#### 1.3.2 软引用 - - -非必须引用,内存溢出之前进行回收。 - -```java -Object obj = new Object(); -SoftReference sf = new SoftReference(obj); -obj = null; -sf.get(); -``` - -sf 是对 obj 的一个软引用,通过 sf.get() 方法可以取到这个对象,当然,当这个对象被标记为需要回收的对象时,则返回 null; - -软引用主要用户实现类似缓存的功能,在内存足够的情况下直接通过软引用取值,无需从繁忙的真实来源查询数据,提升速度;当内存不足时,自动删除这部分缓存数据,从真正的来源查询这些数据。 - - -#### 1.3.3 弱引用 - -只能生存到下一次垃圾收集发生之前,当垃圾收集器工作时,无论当前内存是否足够,都会被回收。 - -```java -Object obj = new Object(); -WeakReference wf = new WeakReference(obj); -obj = null; -wf.get(); -wf.isEnQueued(); -``` - -#### 1.3.4 虚引用 - -又称为幽灵引用或者幻影引用,一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用取得一个对象实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。 - -```java -Object obj = new Object(); -PhantomReference pf = new PhantomReference(obj); -obj=null; -pf.get(); -pf.isEnQueued(); -``` - -### 1.3 方法区的回收 - -在方法区主要是对常量池的回收和对类的卸载。 - -常量池的回收和堆中对象回收类似。 - -类的卸载条件很多,需要满足以下三个条件,并且满足了也不一定会被卸载: - -1. 该类所有的实例都已经被回收,也就是 Java 堆中不存在该类的任何实例。 -2. 加载该类的 ClassLoader 已经被回收。 -3. 该类对应的 java.lang.Class 对象没有在任何地方被引用,也就无法在任何地方通过反射访问该类方法。 - -可以通过 -Xnoclassgc 参数来控制是否对类进行卸载。 - -在大量使用反射、动态代理、CGLib 等 ByteCode 框架、动态生成 JSP 以及 OSGo 这类频繁自定义 ClassLoader 的场景都需要虚拟机具备类卸载功能,以保证不会出现内存溢出。 - -### 1.4 finalize() - -当一个对象可被回收时,如果该对象有必要执行 finalize() 方法,那么就有可能可能通过在该方法中让对象重新被引用,从而实现自救。 - -finalize() 类似 C++ 的虚构函数,用来做关闭外部资源等工作。但是 try-finally 等方式可以做的更好,并且该方法运行代价高昂,不确定性大,无法保证各个对象的调用顺序,因此最好不要使用。 - -## 2. 垃圾收集算法 - -### 2.1 标记 - 清除算法 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//a4248c4b-6c1d-4fb8-a557-86da92d3a294.jpg) - -将需要回收的对象进行标记,然后清除。 - -不足: - -1. 标记和清除过程效率都不高 -2. 会产生大量碎片 - -之后的算法都是基于该算法进行改进。 - -### 2.2 复制算法 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//e6b733ad-606d-4028-b3e8-83c3a73a3797.jpg) - -将内存划分为大小相等的两块,每次只使用其中一块,当这一块内存用完了就将还存活的对象复制到另一块上面,然后再把使用过的内存空间进行一次清理。 - -主要不足是只使用了内存的一半。 - -现在的商业虚拟机都采用这种收集算法来回收新生代,但是并不是将内存划分为大小相等的两块,而是分为一块较大的 Eden 空间和两块较小的 Survior 空间,每次使用 Eden 空间和其中一块 Survivor。在回收时,将 Eden 和 Survivor 中还存活着的对象一次性复制到另一块 Survivor 空间上,最后清理 Eden 和 Survivor。HotSpot 虚拟机的 Eden 和 Survivor 的大小比例默认为 8:1,保证了内存的利用率达到 90 %。如果每次回收有多于 10% 的对象存活,那么一块 Survivor 空间就不够用了,需要依赖于老年代进行分配担保,也就是借用老年代的空间。 - -### 2.3 标记 - 整理算法 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//902b83ab-8054-4bd2-898f-9a4a0fe52830.jpg) - -让所有存活的对象都向一段移动,然后直接清理掉端边界以外的内存。 - -### 2.4 分代收集算法 - -现在的商业虚拟机采用分代收集算法,它使用了前面介绍的几种收集算法,根据对象存活周期将内存划分为几块,不同块采用适当的收集算法。 - -一般将 Java 堆分为新生代和老年代。 - -1. 新生代使用:复制算法 -2. 老年代使用:标记 - 清理 或者 标记 - 整理 算法。 - -## 3. 垃圾收集器 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//c625baa0-dde6-449e-93df-c3a67f2f430f.jpg) - -以上是 HotSpot 虚拟机中的 7 个垃圾收集器,连线表示垃圾收集器可以配合使用。 - -### 3.1 Serial 收集器 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//22fda4ae-4dd5-489d-ab10-9ebfdad22ae0.jpg) - -它是单线程的收集器,不仅意味着只会使用一个线程进行垃圾收集工作,更重要的是它在进行垃圾收集时,必须暂停所有其他工作线程,往往造成过长的等待时间。 - -它的优点是简单高效,对于单个 CPU 环境来说,由于没有线程交互的开销,因此拥有最高的单线程收集效率。 - -在 Client 应用场景中,分配给虚拟机管理的内存一般来说不会很大,该收集器收集几十兆甚至一两百兆的新生代停顿时间可以控制在一百多毫秒以内,只要不是太频繁,这点停顿是可以接受的。 - -### 3.2 ParNew 收集器 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//81538cd5-1bcf-4e31-86e5-e198df1e013b.jpg) - -它是 Serial 收集器的多线程版本。 - -是 Server 模式下的虚拟机首选新生代收集器,除了性能原因外,主要是因为除了 Serial 收集器,只有它能与 CMS 收集器配合工作。 - -默认开始的线程数量与 CPU 数量相同,可以使用 -XX:ParallelGCThreads 参数来设置线程数。 - -### 3.3 Parallel Scavenge 收集器 - -是并行的多线程收集器。 - -其它收集器关注点是尽可能缩短垃圾收集时用户线程的停顿时间,而它的目标是达到一个可控制的吞吐量,它被称为“吞吐量优先”收集器。这里的吞吐量指 CPU 用于运行用户代码的时间占总时间的比值。 - -停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能提升用户体验。而高吞吐量则可以高效率地利用 CPU 时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。 - -提供了两个参数用于精确控制吞吐量,分别是控制最大垃圾收集停顿时间 -XX:MaxGCPauseMillis 参数以及直接设置吞吐量大小的 -XX:GCTimeRatio 参数(值为大于 0 且小于 100 的整数)。缩短停顿时间是以牺牲吞吐量和新生代空间来换取的:新生代空间变小,垃圾回收变得频繁,导致吞吐量下降。 - -还提供了一个参数 -XX:+UseAdaptiveSizePolicy,这是一个开关参数,打开参数后,就不需要手工指定新生代的大小(-Xmn)、Eden 和 Survivor 区的比例(-XX:SurvivorRatio)、晋升老年代对象年龄(-XX:PretenureSizeThreshold)等细节参数了,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量,这种方式称为 GC 自适应的调节策略(GC Ergonomics)。自适应调节策略也是它与 ParNew 收集器的一个重要区别。 - -### 3.4 Serial Old 收集器 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//08f32fd3-f736-4a67-81ca-295b2a7972f2.jpg) - -Serial Old 是 Serial 收集器的老年代版本,也是给 Client 模式下的虚拟机使用。如果用在 Server 模式下,它有两大用途: - -1. 在 JDK 1.5 以及之前版本(Parallel Old 诞生以前)中与 Parallel Scavenge 收集器搭配使用。 -2. 作为 CMS 收集器的后备预案,在并发收集发生 Concurrent Mode Failure 时使用。 - -### 3.5 Parallel Old 收集器 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//278fe431-af88-4a95-a895-9c3b80117de3.jpg) - -是 Parallel Scavenge 收集器的老年代版本。 - -在注重吞吐量以及 CPU 资源敏感的场合,都可以优先考虑 Parallel Scavenge 加 Parallel Old 收集器。 - -### 3.6 CMS 收集器 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//62e77997-6957-4b68-8d12-bfd609bb2c68.jpg) - -CMS(Concurrent Mark Sweep),从 Mark Sweep 可以知道它是基于 标记 - 清除 算法实现的。 - -特点:并发收集、低停顿。 - -分为以下四个流程: - -1. 初始标记:仅仅只是标记一下 GC Roots 能直接关联到的对象,速度很快,需要停顿。 -2. 并发标记:进行 GC Roots Tracing 的过程,它在整个回收过程中耗时最长,不需要停顿。 -3. 重新标记:为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,需要停顿。 -4. 并发清除:不需要停顿。 - -在整个过程中耗时最长的并发标记和并发清除过程中,收集器线程都可以与用户线程一起工作,不需要进行停顿。 - -具有以下缺点: - -1. 对 CPU 资源敏感。CMS 默认启动的回收线程数是 (CPU 数量 + 3) / 4,当 CPU 不足 4 个时,CMS 对用户程序的影响就可能变得很大,如果本来 CPU 负载就比较大,还要分出一半的运算能力去执行收集器线程,就可能导致用户程序的执行速度忽然降低了 50%,其实也让人无法接受。并且低停顿时间是以牺牲吞吐量为代价的,导致 CPU 利用率变低。 - -2. 无法处理浮动垃圾。由于并发清理阶段用户线程还在运行着,伴随程序运行自然就还会有新的垃圾不断产生。这一部分垃圾出现在标记过程之后,CMS 无法在当次收集中处理掉它们,只好留到下一次 GC 时再清理掉,这一部分垃圾就被称为“浮动垃圾”。也是由于在垃圾收集阶段用户线程还需要运行,那也就还需要预留有足够的内存空间给用户线程使用,因此它不能像其他收集器那样等到老年代几乎完全被填满了再进行收集,需要预留一部分空间提供并发收集时的程序运作使用。可以使用 -XX:CMSInitiatingOccupancyFraction 的值来改变触发收集器工作的内存占用百分比,JDK 1.5 默认设置下该值为 68,也就是当老年代使用了 68% 的空间之后会触发收集器工作。如果该值设置的太高,导致浮动垃圾无法保存,那么就会出现 Concurrent Mode Failure,此时虚拟机将启动后备预案:临时启用 Serial Old 收集器来重新进行老年代的垃圾收集。 - -3. 标记 - 清除算法导致的空间碎片,给大对象分配带来很大麻烦,往往出现老年代空间剩余,但无法找到足够大连续空间来分配当前对象,不得不提前出发一次 Full GC。 - -### 3.7 G1 收集器 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//f99ee771-c56f-47fb-9148-c0036695b5fe.jpg) - -G1(Garbage-First)收集器是当今收集器技术发展最前沿的成果之一,它是一款面向服务端应用的垃圾收集器,HotSpot 开发团队赋予它的使命是(在比较长期的)未来可以替换掉 JDK 1.5 中发布的 CMS 收集器。 - -具备如下特点: - -- 并行与并发:能充分利用多 CPU 环境下的硬件优势,使用多个 CPU 来缩短停顿时间; -- 分代收集:分代概念依然得以保留,虽然它不需要其它收集器配合就能独立管理整个 GC 堆,但它能够采用不同方式去处理新创建的对象和已存活一段时间、熬过多次 GC 的旧对象来获取更好的收集效果。 -- 空间整合:整体来看是基于“标记 - 整理”算法实现的收集器,从局部(两个 Region 之间)上来看是基于“复制”算法实现的,这意味着运行期间不会产生内存空间碎片。 -- 可预测的停顿:这是它相对 CMS 的一大优势,降低停顿时间是 G1 和 CMS 共同的关注点,但 G1 除了降低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为 M 毫秒的时间片段内,消耗在 GC 上的时间不得超过 N 毫秒,这几乎已经是实时 Java(RTSJ)的垃圾收集器的特征了。 - -在 G1 之前的其他收集器进行收集的范围都是整个新生代或者老生代,而 G1 不再是这样,Java 堆的内存布局与其他收集器有很大区别,将整个 Java 堆划分为多个大小相等的独立区域(Region)。虽然还保留新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,而都是一部分 Region(不需要连续)的集合。 - -之所以能建立可预测的停顿时间模型,是因为它可以有计划地避免在整个 Java 堆中进行全区域的垃圾收集。它跟踪各个 Region 里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的 Region(这也就是 Garbage-First 名称的来由)。这种使用 Region 划分内存空间以及有优先级的区域回收方式,保证了它在有限的时间内可以获取尽可能高的收集效率。 - -Region 不可能是孤立的,一个对象分配在某个 Region 中,可以与整个 Java 堆任意的对象发生引用关系。在做可达性分析确定对象是否存活的时候,需要扫描整个 Java 堆才能保证准确性,这显然是对 GC 效率的极大伤害。为了避免全堆扫描的发生,每个 Region 都维护了一个与之对应的 Remembered Set。虚拟机发现程序在对 Reference 类型的数据进行写操作时,会产生一个 Write Barrier 暂时中断写操作,检查 Reference 引用的对象是否处于不同的 Region 之中,如果是,便通过 CardTable 把相关引用信息记录到被引用对象所属的 Region 的 Remembered Set 之中。当进行内存回收时,在 GC 根节点的枚举范围中加入 Remembered Set 即可保证不对全堆扫描也不会有遗漏。 - -如果不计算维护 Remembered Set 的操作,G1 收集器的运作大致可划分为以下几个步骤: - -1. 初始标记 -2. 并发标记 -3. 最终标记:为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程的 Remembered Set Logs 里面,最终标记阶段需要把 Remembered Set Logs 的数据合并到 Remembered Set 中。这阶段需要停顿线程,但是可并行执行。 -4. 筛选回收:首先对各个 Region 中的回收价值和成本进行排序,根据用户所期望的 GC 停顿是时间来制定回收计划。此阶段其实也可以做到与用户程序一起并发执行,但是因为只回收一部分 Region,时间是用户可控制的,而且停顿用户线程将大幅度提高收集效率。 - -### 3.8 七种垃圾收集器的比较 - -| 收集器 | 串行、并行 or 并发 | 新生代 / 老年代 | 算法 | 目标 | 适用场景 | -| --- | --- | --- | --- | --- | --- | -| **Serial** | 串行 | 新生代 | 复制算法 | 响应速度优先 | 单 CPU 环境下的 Client 模式 | -| **Serial Old** | 串行 | 老年代 | 标记 - 整理 | 响应速度优先 | 单 CPU 环境下的 Client 模式、CMS 的后备预案 | -| **ParNew** | 并行 | 新生代 | 复制算法 | 响应速度优先 | 多 CPU 环境时在 Server 模式下与 CMS 配合 | -| **Parallel Scavenge** | 并行 | 新生代 | 复制算法 | 吞吐量优先 | 在后台运算而不需要太多交互的任务 | -| **Parallel Old** | 并行 | 老年代 | 标记 - 整理 | 吞吐量优先 | 在后台运算而不需要太多交互的任务 | -| **CMS** | 并发 | 老年代 | 标记 - 清除 | 响应速度优先 | 集中在互联网站或 B/S 系统服务端上的 Java 应用 | -| **G1** | 并发 | both | 标记 - 整理 + 复制算法 | 响应速度优先 | 面向服务端应用,将来替换 CMS | - -## 4. 内存分配与回收策略 - -### 4.1 优先在 Eden 分配 - -大多数情况下,对象在新生代 Eden 区分配,当 Eden 区空间不够时,发起 Minor GC; - -### 4.2 大对象直接进入老年代 - -提供 -XX:PretenureSizeThreshold 参数,大于此值的对象直接在老年代分配,避免在 Eden 区和 Survivor 区之间的大量内存复制; -### 4.3 长期存活的对象进入老年代 - -JVM 为对象定义年龄计数器,经过 Minor GC 依然存活且被 Survivor 区容纳的,移动到 Survivor 区,年龄加 1,每经历一次 Minor GC 不被清理则年龄加 1,增加到一定年龄则移动到老年区(默认 15 岁,通过 -XX:MaxTenuringThreshold 设置); - - -### 4.4 动态对象年龄判定 - -若 Survivor 区中同年龄所有对象大小总和大于 Survivor 空间一半,则年龄大于等于该年龄的对象可以直接进入老年代; - -### 4.5 空间分配担保 - -在发生 Minor GC 之前,JVM 先检查老年代最大可用连续空间是否大于新生代所有对象总空间,成立的话 Minor GC 确认是安全的;否则继续检查老年代最大可用连续空间是否大于历次晋升到老年代对象的平均大小,大于的话进行 Minor GC,小于的话进行 Full GC。 - -## 4.6 Full GC 的触发条件 - -对于 Minor GC,其触发条件非常简单,当 Eden 区空间满时,就将触发一次 Minor GC。而 Full GC 则相对复杂,有以下条件: - -### 4.6.1 调用 System.gc() - -此方法的调用是建议 JVM 进行 Full GC,虽然只是建议而非一定,但很多情况下它会触发 Full GC,从而增加 Full GC 的频率,也即增加了间歇性停顿的次数。因此强烈建议能不使用此方法就不要使用,让虚拟机自己去管理它的内存,可通过 -XX:+ DisableExplicitGC 来禁止 RMI 调用 System.gc()。 - -### 4.6.2 老年代空间不足 - -老年代空间不足的常见场景为前文所讲的大对象直接进入老年代、长期存活的对象进入老年代等,当执行 Full GC 后空间仍然不足,则抛出如下错误: Java.lang.OutOfMemoryError: Java heap space 为避免以上两种状况引起的 Full GC,调优时应尽量做到让对象在 Minor GC 阶段被回收、让对象在新生代多存活一段时间及不要创建过大的对象及数组。 - -### 4.6.3 空间分配担保失败 - -使用复制算法的 Minor GC 需要老年代的内存空间作担保,如果出现了 HandlePromotionFailure 担保失败,则会触发 Full GC。 - -### 4.6.4 JDK 1.7 及以前的永久代空间不足 - -在 JDK 1.7 及以前,HotSpot 虚拟机中的方法区是用永久代实现的,永久代中存放的为一些 class 的信息、常量、静态变量等数据,当系统中要加载的类、反射的类和调用的方法较多时,Permanet Generation 可能会被占满,在未配置为采用 CMS GC 的情况下也会执行 Full GC。如果经过 Full GC 仍然回收不了,那么 JVM 会抛出如下错误信息:java.lang.OutOfMemoryError: PermGen space 为避免 PermGen 占满造成 Full GC 现象,可采用的方法为增大 PermGen 空间或转为使用 CMS GC。 - -在 JDK 1.8 中用元空间替换了永久代作为方法区的实现,元空间是本地内存,因此减少了一种 Full GC 触发的可能性。 - -### 4.6.5 Concurrent Mode Failure - -执行 CMS GC 的过程中同时有对象要放入老年代,而此时老年代空间不足(有时候“空间不足”是 CMS GC 时当前的浮动垃圾过多导致暂时性的空间不足触发 Full GC),便会报 Concurrent Mode Failure 错误,并触发 Full GC。 - -# 类加载机制 - -类是在运行期间动态加载的。 - -## 1 类的生命周期 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//32b8374a-e822-4720-af0b-c0f485095ea2.jpg) - -包括以下 7 个阶段: - -- **加载(Loading)** -- **验证(Verification)** -- **准备(Preparation)** -- **解析(Resolution)** -- **初始化(Initialization)** -- 使用(Using) -- 卸载(Unloading) - -其中解析过程在某些情况下可以在初始化阶段之后再开始,这是为了支持 Java 的动态绑定。 - -## 2. 类初始化时机 - -虚拟机规范中并没有强制约束何时进行加载,但是规范严格规定了有且只有下列五种情况必须对类进行初始化:( 加载、验证、准备都会随着发生 ) - -1. 遇到 new、getstatic、putstatic、invokestatic 这四条字节码指令时,如果类没有进行过初始化,则必须先触发其初始化。最常见的生成这 4 条指令的场景是:使用 new 关键字实例化对象的时候;读取或设置一个类的静态字段(被 final 修饰、已在编译器把结果放入常量池的静态字段除外)的时候;以及调用一个类的静态方法的时候。 - -2. 使用 java.lang.reflect 包的方法对类进行反射调用的时候,如果类没有进行初始化,则需要先触发其初始化。 - -3. 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。 - -4. 当虚拟机启动时,用户需要指定一个要执行的主类(包含 main() 方法的那个类),虚拟机会先初始化这个主类; - -5. 当使用 jdk1.7 的动态语言支持时,如果一个 java.lang.invoke.MethodHandle 实例最后的解析结果为 REF_getStatic, REF_putStatic, REF_invokeStatic 的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化; - -以上 5 种场景中的行为称为对一个类进行主动引用。除此之外,所有引用类的方式都不会触发初始化,称为被动引用。被动引用的常见例子包括: - -1\. 通过子类引用父类的静态字段,不会导致子类初始化。 - -```java -System.out.println(SubClass.value); // value 字段在 SuperClass 中定义 -``` - -2\. 通过数组定义来引用类,不会触发此类的初始化。该过程会对数组类进行初始化,数组类是一个由虚拟机自动生成的、直接继承自 Object 的子类,其中包含了数组的属性和方法。 - -```java -SuperClass[] sca = new SuperClass[10]; -``` - -3\. 常量在编译阶段会存入调用类的常量池中,本质上并没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化。 - -```java -System.out.println(ConstClass.HELLOWORLD); -``` - -## 3. 类加载过程 - -包含了加载、验证、准备、解析和初始化这 5 个阶段。 - -### 3.1 加载 - -加载是类加载的一个阶段,注意不要混淆。 - -加载过程完成以下三件事: - -1. 通过一个类的全限定名来获取定义此类的二进制字节流。 -2. 将这个字节流所代表的静态存储结构转化为方法区的运行时存储结构。 -3. 在内存中生成一个代表这个类的 Class 对象,作为方法区这个类的各种数据的访问入口。 - -其中二进制字节流可以从以下方式中获取: - -- 从 ZIP 包读取,这很常见,最终成为日后 JAR、EAR、WAR 格式的基础。 -- 从网络中获取,这种场景最典型的应用是 Applet。 -- 运行时计算生成,这种场景使用得最多得就是动态代理技术,在 java.lang.reflect.Proxy 中,就是用了 ProxyGenerator.generateProxyClass 的代理类的二进制字节流。 -- 由其他文件生成,典型场景是 JSP 应用,即由 JSP 文件生成对应的 Class 类。 -- 从数据库读取,这种场景相对少见,例如有些中间件服务器(如 SAP Netweaver)可以选择把程序安装到数据库中来完成程序代码在集群间的分发。 -... - -### 3.2 验证 - -确保 Class 文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。 - -主要有以下 4 个阶段: - -1. 文件格式验证 -2. 元数据验证(对字节码描述的信息进行语义分析) -3. 字节码验证(通过数据流和控制流分析,确保程序语义是合法、符合逻辑的,将对类的方法体进行校验分析) -4. 符号引用验证 - -### 3.3 准备 - -类变量是被 static 修饰的变量,准备阶段为类变量分配内存并设置初始值,使用的是方法区的内存。 - -实例变量不会在这阶段分配内存,它将会在对象实例化时随着对象一起分配在 Java 堆中。 - -初始值一般为 0 值,例如下面的类变量 value 被初始化为 0 而不是 123。 - -```java -public static int value = 123; -``` - -如果类变量是常量,那么会按照表达式来进行初始化,而不是赋值为 0。 - -```java -public static final int value = 123; -``` - -### 3.4 解析 - -将常量池的符号引用替换为直接引用的过程。 - -### 3.5 初始化 - -初始化阶段即虚拟机执行类构造器 <clinit>() 方法的过程。 - -在准备阶段,类变量已经赋过一次系统要求的初始值,而在初始化阶段,根据程序员通过程序制定的主观计划去初始化类变量和其它资源。 - -<clinit>() 方法具有以下特点: - -- 是由编译器自动收集类中所有类变量的赋值动作和静态语句块(static{} 块)中的语句合并产生的,编译器收集的顺序由语句在源文件中出现的顺序决定。特别注意的是,静态语句块只能访问到定义在它之前的类变量,定义在它之后的类变量只能赋值,不能访问。例如以下代码: - -```java -public class Test { - static { - i = 0; // 给变量赋值可以正常编译通过 - System.out.print(i); // 这句编译器会提示“非法向前引用” - } - static int i = 1; -} -``` - -- 与类的构造函数(或者说实例构造器 <init>())不同,不需要显式的调用父类的构造器。虚拟机会自动保证在子类的 <clinit>() 方法运行之前,父类的 <clinit>() 方法已经执行结束。因此虚拟机中第一个执行 <clinit>() 方法的类肯定为 java.lang.Object。 - -- 由于父类的 <clinit>() 方法先执行,也就意味着父类中定义的静态语句块要优于子类的变量赋值操作。例如以下代码: - -```java -static class Parent { - public static int A = 1; - static { - A = 2; - } -} - -static class Sub extends Parent { - public static int B = A; -} - -public static void main(String[] args) { - System.out.println(Sub.B); // 输出结果是父类中的静态变量值 A,也就是 2 -} -``` - -- <clinit>() 方法对于类或接口不是必须的,如果一个类中不包含静态语句块,也没有对类变量的赋值操作,编译器可以不为该类生成 <clinit>() 方法。 - -- 接口中不可以使用静态语句块,但仍然有类变量初始化的赋值操作,因此接口与类一样都会生成 <clinit>() 方法。但接口与类不同的是,执行接口的 <clinit>() 方法不需要先执行父接口的 <clinit>() 方法。只有当父接口中定义的变量使用时,父接口才会初始化。另外,接口的实现类在初始化时也一样不会执行接口的 <clinit>() 方法。 - -- 虚拟机会保证一个类的 <clinit>() 方法在多线程环境下被正确的加锁和同步,如果多个线程同时初始化一个类,只会有一个线程执行这个类的 <clinit>() 方法,其它线程都会阻塞等待,直到活动线程执行 <clinit>() 方法完毕。如果在一个类的 <clinit>() 方法中有耗时的操作,就可能造成多个进程阻塞,在实际过程中此种阻塞很隐蔽。 - -## 4. 类加载器 - -虚拟机设计团队把类加载阶段中的“通过一个类的全限定名来获取描述此类的二进制字节流 ( 即字节码 )”这个动作放到 Java 虚拟机外部去实现,以便让应用程序自己决定如何去获取所需要的类。实现这个动作的代码模块称为“类加载器”。 - -### 4.1 类与类加载器 - -对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在 Java 虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类名称空间。通俗而言:比较两个类是否“相等”(这里所指的“相等”,包括类的 Class 对象的 equals() 方法、isAssignableFrom() 方法、isInstance() 方法的返回结果,也包括使用 instanceof() 关键字对做对象所属关系判定等情况),只有在这两个类时由同一个类加载器加载的前提下才有意义,否则,即使这两个类来源于同一个 Class 文件,被同一个虚拟机加载,只要加载它们的类加载器不同,那这两个类就必定不相等。 - -### 4.2 类加载器分类 - -从 Java 虚拟机的角度来讲,只存在以下两种不同的类加载器: - -一种是启动类加载器(Bootstrap ClassLoader),这个类加载器用 C++ 实现,是虚拟机自身的一部分;另一种就是所有其他类的加载器,这些类由 Java 实现,独立于虚拟机外部,并且全都继承自抽象类 java.lang.ClassLoader。 - -从 Java 开发人员的角度看,类加载器可以划分得更细致一些: - -- 启动类加载器(Bootstrap ClassLoader) 此类加载器负责将存放在 \lib 目录中的,或者被 -Xbootclasspath 参数所指定的路径中的,并且是虚拟机识别的(仅按照文件名识别,如 rt.jar,名字不符合的类库即使放在 lib 目录中也不会被加载)类库加载到虚拟机内存中。 启动类加载器无法被 Java 程序直接引用,用户在编写自定义类加载器时,如果需要把加载请求委派给引导类加载器,直接使用 null 代替即可。 - -- 扩展类加载器(Extension ClassLoader) 这个类加载器是由 ExtClassLoader(sun.misc.Launcher$ExtClassLoader)实现的。它负责将 /lib/ext 或者被 java.ext.dir 系统变量所指定路径中的所有类库加载到内存中,开发者可以直接使用扩展类加载器。 - -- 应用程序类加载器(Application ClassLoader) 这个类加载器是由 AppClassLoader(sun.misc.Launcher$AppClassLoader)实现的。由于这个类加载器是 ClassLoader 中的 getSystemClassLoader() 方法的返回值,因此一般称为系统类加载器。它负责加载用户类路径(ClassPath)上所指定的类库,开发者可以直接使用这个类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。 - -### 4.3 双亲委派模型 - -应用程序都是由三种类加载器相互配合进行加载的,如果有必要,还可以加入自己定义的类加载器。下图展示的类加载器之间的层次关系,称为类加载器的双亲委派模型(Parents Delegation Model)。该模型要求除了顶层的启动类加载器外,其余的类加载器都应有自己的父类加载器,这里类加载器之间的父子关系一般通过组合(Composition)关系来实现,而不是通过继承(Inheritance)的关系实现。 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//2cdc3ce2-fa82-4c22-baaa-000c07d10473.jpg) - -**工作过程** - -如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载,而是把这个请求委派给父类加载器,每一个层次的加载器都是如此,依次递归,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成此加载请求(它搜索范围中没有找到所需类)时,子加载器才会尝试自己加载。 - -**好处** - -使用双亲委派模型来组织类加载器之间的关系,使得 Java 类随着它的类加载器一起具备了一种带有优先级的层次关系。例如类 java.lang.Object,它存放再 rt.jar 中,无论哪个类加载器要加载这个类,最终都是委派给处于模型最顶端的启动类加载器进行加载,因此 Object 类在程序的各种类加载器环境中都是同一个类。相反,如果没有双亲委派模型,由各个类加载器自行加载的话,如果用户编写了一个称为`java.lang.Object 的类,并放在程序的 ClassPath 中,那系统中将会出现多个不同的 Object 类,程序将变得一片混乱。如果开发者尝试编写一个与 rt.jar 类库中已有类重名的 Java 类,将会发现可以正常编译,但是永远无法被加载运行。 - -**实现** - -```java -protected synchronized Class loadClass(String name, boolean resolve) throws ClassNotFoundException{ - //check the class has been loaded or not - Class c = findLoadedClass(name); - if(c == null) { - try{ - if(parent != null) { - c = parent.loadClass(name, false); - } else{ - c = findBootstrapClassOrNull(name); - } - } catch(ClassNotFoundException e) { - //if throws the exception , the father can not complete the load - } - if(c == null) { - c = findClass(name); - } - } - if(resolve) { - resolveClass(c); - } - return c; -} -``` - -# JVM 参数 - -## GC 优化配置 - -| 配置 | 描述 | -| --- | --- | -| -Xms | 初始化堆内存大小 | -| -Xmx | 堆内存最大值 | -| -Xmn | 新生代大小 | -| -XX:PermSize | 初始化永久代大小 | -| -XX:MaxPermSize | 永久代最大容量 | - -## GC 类型设置 - -| 配置 | 描述 | -| --- | --- | -| -XX:+UseSerialGC | 串行垃圾回收器 | -| -XX:+UseParallelGC | 并行垃圾回收器 | -| -XX:+UseConcMarkSweepGC | 并发标记扫描垃圾回收器 | -| -XX:ParallelCMSThreads= | 并发标记扫描垃圾回收器 = 为使用的线程数量 | -| -XX:+UseG1GC | G1 垃圾回收器 | - -```java -java -Xmx12m -Xms3m -Xmn1m -XX:PermSize=20m -XX:MaxPermSize=20m -XX:+UseSerialGC -jar java-application.jar -``` diff --git a/notes/Java IO.md b/notes/Java IO.md deleted file mode 100644 index 1737ca9d..00000000 --- a/notes/Java IO.md +++ /dev/null @@ -1,407 +0,0 @@ - -* [概览](#概览) -* [磁盘操作](#磁盘操作) -* [字节操作](#字节操作) -* [字符操作](#字符操作) -* [对象操作](#对象操作) -* [网络操作](#网络操作) - * [1. InetAddress](#1-inetaddress) - * [2. URL](#2-url) - * [3. Sockets](#3-sockets) - * [4. Datagram](#4-datagram) -* [NIO](#nio) - * [1. 流与块](#1-流与块) - * [2. 通道与缓冲区](#2-通道与缓冲区) - * [2.1 通道](#21-通道) - * [2.2 缓冲区](#22-缓冲区) - * [3. 缓冲区状态变量](#3-缓冲区状态变量) - * [4. 读写文件实例](#4-读写文件实例) - * [5. 阻塞与非阻塞](#5-阻塞与非阻塞) - * [5.1 阻塞式 I/O](#51-阻塞式-io) - * [5.2 非阻塞式 I/O](#52-非阻塞式-io) - * [6. 套接字实例](#6-套接字实例) - * [6.1 ServerSocketChannel](#61-serversocketchannel) - * [6.2 Selectors](#62-selectors) - * [6.3 主循环](#63-主循环) - * [6.4 监听新连接](#64-监听新连接) - * [6.5 接受新的连接](#65-接受新的连接) - * [6.6 删除处理过的 SelectionKey](#66-删除处理过的-selectionkey) - * [6.7 传入的 I/O](#67-传入的-io) -* [参考资料](#参考资料) - - - -# 概览 - -Java 的 I/O 大概可以分成以下几类 - -1. 磁盘操作:File -2. 字节操作:InputStream 和 OutputStream -3. 字符操作:Reader 和 Writer -4. 对象操作:Serializable -5. 网络操作:Socket -6. 非阻塞式 IO:NIO - -# 磁盘操作 - -File 类可以用于表示文件和目录,但是它只用于表示文件的信息,而不表示文件的内容。 - -# 字节操作 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//8143787f-12eb-46ea-9bc3-c66d22d35285.jpg) - -Java I/O 使用了装饰者模式来实现。以 InputStream 为例,InputStream 是抽象组件,FileInputStream 是 InputStream 的子类,属于具体组件,提供了字节流的输入操作。FilterInputStream 属于抽象装饰者,装饰者用于装饰组件,为组件提供额外的功能,例如 BufferedInputStream 为 FileInputStream 提供缓存的功能。实例化一个具有缓存功能的字节流对象时,只需要在 FileInputStream 对象上再套一层 BufferedInputStream 对象即可。 - -```java -BufferedInputStream bis = new BufferedInputStream(new FileInputStream(file)); -``` - -DataInputStream 装饰者提供了对更多数据类型进行输入的操作,比如 int、double 等基本类型。 - -批量读入文件中的内容到字节数组中 - -```java -byte[] buf = new byte[20*1024]; -int bytes = 0; -// 最多读取 buf.length 个字节,返回的是实际读取的个数,返回 -1 的时候表示读到 eof,即文件尾 -while((bytes = in.read(buf, 0 , buf.length)) != -1) { - // ... -} -``` - -# 字符操作 - -不管是磁盘还是网络传输,最小的存储单元都是字节,而不是字符,所以 I/O 操作的都是字节而不是字符。但是在程序中操作的数据通常是字符形式,因此需要提供对字符进行操作的方法。 - -InputStreamReader 实现从文本文件的字节流解码成字符流;OutputStreamWriter 实现字符流编码成为文本文件的字节流。它们都继承自 Reader 和 Writer。 - -编码就是把字符转换为字节,而解码是把字节重新组合成字符。 - -```java -byte[] bytes = str.getBytes(encoding); // 编码 -String str = new String(bytes, encoding); // 解码 -``` - -GBK 编码中,中文占 2 个字节,英文占 1 个字节;UTF-8 编码中,中文占 3 个字节,英文占 1 个字节;Java 使用双字节编码 UTF-16be,中文和英文都占 2 个字节。 - -如果编码和解码过程使用不同的编码方式那么就出现了乱码。 - -# 对象操作 - -序列化就是将一个对象转换成字节序列,方便存储和传输。 - -序列化:ObjectOutputStream.writeObject() - -反序列化:ObjectInputStream.readObject() - -序列化的类需要实现 Serializable 接口,它只是一个标准,没有任何方法需要实现。 - -transient 关键字可以使一些属性不会被序列化。 - -**ArrayList 序列化和反序列化的实现**:ArrayList 中存储数据的数组是用 transient 修饰的,因为这个数组是动态扩展的,并不是所有的空间都被使用,因此就不需要所有的内容都被序列化。通过重写序列化和反序列化方法,使得可以只序列化数组中有内容的那部分数据。 - -``` -private transient Object[] elementData; -``` - -# 网络操作 - -Java 中的网络支持: - -1. InetAddress:用于表示网络上的硬件资源,即 IP 地址; -2. URL:统一资源定位符,通过 URL 可以直接读取或者写入网络上的数据; -3. Sockets:使用 TCP 协议实现网络通信; -4. Datagram:使用 UDP 协议实现网络通信。 - -## 1. InetAddress - -没有公有构造函数,只能通过静态方法来创建实例,比如 InetAddress.getByName(String host)、InetAddress.getByAddress(byte[] addr)。 - -## 2. URL - -可以直接从 URL 中读取字节流数据 - -```java -URL url = new URL("http://www.baidu.com"); -InputStream is = url.openStream(); // 字节流 -InputStreamReader isr = new InputStreamReader(is, "utf-8"); // 字符流 -BufferedReader br = new BufferedReader(isr); -String line = br.readLine(); -while (line != null) { - System.out.println(line); - line = br.readLine(); -} -br.close(); -isr.close(); -is.close(); -``` - -## 3. Sockets - -Socket 通信模型 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//fa4101d7-19ce-4a69-a84f-20bbe64320e5.jpg) - -- ServerSocket:服务器端类 -- Socket:客户端类 - -服务器和客户端通过 InputStream 和 OutputStream 进行输入输出。 - -## 4. Datagram - -- DatagramPacket:数据包类 -- DatagramSocket:通信类 - -# NIO - -NIO 将最耗时的 I/O 操作 ( 即填充和提取缓冲区 ) 转移回操作系统,因而 不需要程序员去控制就可以极大地提高运行速度。 - -## 1. 流与块 - -I/O 与 NIO 最重要的区别是数据打包和传输的方式。正如前面提到的,I/O 以流的方式处理数据,而 NIO 以块的方式处理数据。 - -面向流的 I/O 一次一个字节进行处理数据,一个输入流产生一个字节的数据,一个输出流消费一个字节的数据。为流式数据创建过滤器非常容易,链接几个过滤器,以便每个过滤器只负责单个复杂处理机制的一部分,这样也是相对简单的。不利的一面是,面向流的 I/O 通常相当慢。 - -一个面向块的 I/O 系统以块的形式处理数据,每一个操作都在一步中产生或者消费一个数据块。按块处理数据比按流处理数据要快得多。但是面向块的 I/O 缺少一些面向流的 I/O 所具有的优雅性和简单性。 - -I/O 包和 NIO 已经很好地集成了,java.io.\* 已经以 NIO 为基础重新实现了,所以现在它可以利用 NIO 的一些特性。例如, java.io.\* 包中的一些类包含以块的形式读写数据的方法,这使得即使在更面向流的系统中,处理速度也会更快。 - -## 2. 通道与缓冲区 - -### 2.1 通道 - -通道 Channel 是对原 I/O 包中的流的模拟,可以通过它读取和写入数据。 - -通道与流的不同之处在于,流只能在一个方向上移动,(一个流必须是 InputStream 或者 OutputStream 的子类), 而通道是双向的,可以用于读、写或者同时用于读写。 - -通道包括以下类型: - -- FileChannel:从文件中读写数据; -- DatagramChannel:通过 UDP 读写网络中数据; -- SocketChannel:通过 TCP 读写网络中数据; -- ServerSocketChannel:可以监听新进来的 TCP 连接,对每一个新进来的连接都会创建一个 SocketChannel。 - -### 2.2 缓冲区 - -发送给一个通道的所有对象都必须首先放到缓冲区中;同样地,从通道中读取的任何数据都要读到缓冲区中。也就是说,不会直接对通道进行读写数据,而是先经过缓冲区。 - -缓冲区实质上是一个数组,但它不仅仅是一个数组。缓冲区提供了对数据的结构化访问,而且还可以跟踪系统的读/写进程。 - -缓冲区包括以下类型: - -- ByteBuffer -- CharBuffer -- ShortBuffer -- IntBuffer -- LongBuffer -- FloatBuffer -- DoubleBuffer - - -## 3. 缓冲区状态变量 - -- capacity:最大容量; -- position:当前已经读写的字节数; -- limit:还可以读写的字节数。 - -状态变量的改变过程: - -1\. 新建一个大小为 8 个字节的缓冲区,此时 position 为 0,而 limit == capacity == 9。capacity 变量不会改变,下面的讨论会忽略它。 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//1bea398f-17a7-4f67-a90b-9e2d243eaa9a.png) - -2\. 从输入通道中读取 3 个字节数据写入缓冲区中,此时 position 移动设为 3,limit 保持不变。 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//4628274c-25b6-4053-97cf-d1239b44c43d.png) - -3\. 在将缓冲区的数据写到输出通道之前,需要先调用 flip() 方法,这个方法将 limit 设置为当前 position,并将 position 设置为 0。 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//952e06bd-5a65-4cab-82e4-dd1536462f38.png) - -4\. 从缓冲区中取 4 个字节到输出缓冲中,此时 position 设为 4。 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//b5bdcbe2-b958-4aef-9151-6ad963cb28b4.png) - -5\. 最后需要调用 clear() 方法来清空缓冲区,此时 position 和 limit 都被设置为最初位置。 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//67bf5487-c45d-49b6-b9c0-a058d8c68902.png) - -## 4. 读写文件实例 - -1\. 为要读取的文件创建 FileInputStream,之后通过 FileInputStream 获取输入 FileChannel; - -```java -FileInputStream fin = new FileInputStream("readandshow.txt"); -FileChannel fic = fin.getChannel(); -``` - -2\. 创建一个容量为 1024 的 Buffer - -```java -ByteBuffer buffer = ByteBuffer.allocate(1024); -``` - -3\. 将数据从输入 FileChannel 写入到 Buffer 中,如果没有数据的话, read() 方法会返回 -1 - -```java -int r = fcin.read(buffer); -if (r == -1) { - break; -} -``` - -4\. 为要写入的文件创建 FileOutputStream,之后通过 FileOutputStream 获取输出 FileChannel - -```java -FileOutputStream fout = new FileOutputStream("writesomebytes.txt"); -FileChannel foc = fout.getChannel(); -``` - -5\. 调用 flip() 切换读写 - -```java -buffer.flip(); -``` - -6\. 把 Buffer 中的数据读取到输出 FileChannel 中 - -```java -foc.write(buffer); -``` - -7\. 最后调用 clear() 重置缓冲区 - -```java -buffer.clear(); -``` - -## 5. 阻塞与非阻塞 - -应当注意,FileChannel 不能切换到非阻塞模式,套接字 Channel 可以。 - -### 5.1 阻塞式 I/O - -阻塞式 I/O 在调用 InputStream.read() 方法时会一直等到数据到来时(或超时)才会返回,在调用 ServerSocket.accept() 方法时,也会一直阻塞到有客户端连接才会返回,每个客户端连接过来后,服务端都会启动一个线程去处理该客户端的请求。 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//edc23f99-c46c-4200-b64e-07516828720d.jpg) - -### 5.2 非阻塞式 I/O - -由一个专门的线程来处理所有的 I/O 事件,并负责分发。 - -事件驱动机制:事件到的时候触发,而不是同步的去监视事件。 - -线程通信:线程之间通过 wait()、notify() 等方式通信,保证每次上下文切换都是有意义的,减少无谓的线程切换。 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//7fcb2fb0-2cd9-4396-bc2d-282becf963c3.jpg) - -## 6. 套接字实例 - -### 6.1 ServerSocketChannel - -每一个端口都需要有一个 ServerSocketChannel 用来监听连接。 - -```java -ServerSocketChannel ssc = ServerSocketChannel.open(); -ssc.configureBlocking(false); // 设置为非阻塞 - -ServerSocket ss = ssc.socket(); -InetSocketAddress address = new InetSocketAddress(ports[i]); -ss.bind(address); // 绑定端口号 -``` - -### 6.2 Selectors - -异步 I/O 通过 Selector 注册对特定 I/O 事件的兴趣 ― 可读的数据的到达、新的套接字连接等等,在发生这样的事件时,系统将会发送通知。 - -创建 Selectors 之后,就可以对不同的通道对象调用 register() 方法。register() 的第一个参数总是这个 Selector。第二个参数是 OP_ACCEPT,这里它指定我们想要监听 accept 事件,也就是在新的连接建立时所发生的事件。 - -SelectionKey 代表这个通道在此 Selector 上的这个注册。当某个 Selector 通知您某个传入事件时,它是通过提供对应于该事件的 SelectionKey 来进行的。SelectionKey 还可以用于取消通道的注册。 - -```java -Selector selector = Selector.open(); -SelectionKey key = ssc.register(selector, SelectionKey.OP_ACCEPT); -``` - -### 6.3 主循环 - -首先,我们调用 Selector 的 select() 方法。这个方法会阻塞,直到至少有一个已注册的事件发生。当一个或者更多的事件发生时, select() 方法将返回所发生的事件的数量。 - -接下来,我们调用 Selector 的 selectedKeys() 方法,它返回发生了事件的 SelectionKey 对象的一个 集合 。 - -我们通过迭代 SelectionKeys 并依次处理每个 SelectionKey 来处理事件。对于每一个 SelectionKey,您必须确定发生的是什么 I/O 事件,以及这个事件影响哪些 I/O 对象。 - -```java -int num = selector.select(); - -Set selectedKeys = selector.selectedKeys(); -Iterator it = selectedKeys.iterator(); - -while (it.hasNext()) { - SelectionKey key = (SelectionKey)it.next(); - // ... deal with I/O event ... -} -``` - -### 6.4 监听新连接 - -程序执行到这里,我们仅注册了 ServerSocketChannel,并且仅注册它们“接收”事件。为确认这一点,我们对 SelectionKey 调用 readyOps() 方法,并检查发生了什么类型的事件: - -```java -if ((key.readyOps() & SelectionKey.OP_ACCEPT) - == SelectionKey.OP_ACCEPT) { - // Accept the new connection - // ... -} -``` - -可以肯定地说, readOps() 方法告诉我们该事件是新的连接。 - -### 6.5 接受新的连接 - -因为我们知道这个服务器套接字上有一个传入连接在等待,所以可以安全地接受它;也就是说,不用担心 accept() 操作会阻塞: - -```java -ServerSocketChannel ssc = (ServerSocketChannel)key.channel(); -SocketChannel sc = ssc.accept(); -``` - -下一步是将新连接的 SocketChannel 配置为非阻塞的。而且由于接受这个连接的目的是为了读取来自套接字的数据,所以我们还必须将 SocketChannel 注册到 Selector上,如下所示: - -```java -sc.configureBlocking( false ); -SelectionKey newKey = sc.register( selector, SelectionKey.OP_READ ); -``` - -注意我们使用 register() 的 OP_READ 参数,将 SocketChannel 注册用于 读取 而不是 接受 新连接。 - -### 6.6 删除处理过的 SelectionKey - -在处理 SelectionKey 之后,我们几乎可以返回主循环了。但是我们必须首先将处理过的 SelectionKey 从选定的键集合中删除。如果我们没有删除处理过的键,那么它仍然会在主集合中以一个激活的键出现,这会导致我们尝试再次处理它。我们调用迭代器的 remove() 方法来删除处理过的 SelectionKey: - -```java -it.remove(); -``` - -现在我们可以返回主循环并接受从一个套接字中传入的数据(或者一个传入的 I/O 事件)了。 - -### 6.7 传入的 I/O - -当来自一个套接字的数据到达时,它会触发一个 I/O 事件。这会导致在主循环中调用 Selector.select(),并返回一个或者多个 I/O 事件。这一次, SelectionKey 将被标记为 OP_READ 事件,如下所示: - -```java -} else if ((key.readyOps() & SelectionKey.OP_READ) - == SelectionKey.OP_READ) { - // Read the data - SocketChannel sc = (SocketChannel)key.channel(); - // ... -} -``` - - -# 参考资料 - -- Eckel B, 埃克尔 , 昊鹏 , 等 . Java 编程思想 [M]. 机械工业出版社 , 2002. -- [IBM: NIO 入门](https://www.ibm.com/developerworks/cn/education/java/j-nio/j-nio.html) -- [ 深入分析 Java I/O 的工作机制 ](https://www.ibm.com/developerworks/cn/java/j-lo-javaio/index.html) -- [NIO 与传统 IO 的区别 ](http://blog.csdn.net/shimiso/article/details/24990499) diff --git a/notes/Java 基础.md b/notes/Java 基础.md deleted file mode 100644 index 70892d56..00000000 --- a/notes/Java 基础.md +++ /dev/null @@ -1,468 +0,0 @@ - -* [关键字](#关键字) - * [1. final](#1-final) - * [2. static](#2-static) -* [Object 通用方法](#object-通用方法) - * [1. 概览](#1-概览) - * [2. clone()](#2-clone) - * [3. equals()](#3-equals) -* [继承](#继承) - * [1. 访问权限](#1-访问权限) - * [2. 抽象类与接口的区别](#2-抽象类与接口的区别) - * [3. super()](#3-super) -* [String](#string) - * [1. String, StringBuffer and StringBuilder](#1-string,-stringbuffer-and-stringbuilder) - * [2. String 不可变的原因](#2-string-不可变的原因) - * [3. String.intern()](#3-stringintern) -* [基本类型与运算](#基本类型与运算) - * [1. 包装类型](#1-包装类型) - * [2. switch](#2-switch) -* [反射](#反射) -* [异常](#异常) -* [泛型](#泛型) -* [特性](#特性) - * [1. 三大特性](#1-三大特性) - * [2. Java 各版本的新特性](#2-java-各版本的新特性) - * [3. Java 与 C++ 的区别](#3-java-与-c++-的区别) - * [4. JRE or JDK](#4-jre-or-jdk) - - - - -# 关键字 - -## 1. final - -**数据** - -声明数据为常量,可以是编译时常量,也可以是在运行时被初始化后不能被改变的常量。 - -对于基本类型,final 使数值不变;对于引用对象,final 使引用不变,也就不能引用其它对象,但是被引用的对象本身是可以修改的。 - -**方法** - -声明方法不能被子类覆盖。 - -private 方法隐式地被指定为 final,如果在子类中定义的方法和基类中的一个 private 方法签名相同,此时子类的方法不是覆盖基类方法,而是重载了。 - -**类** - -声明类不允许被继承。 - -## 2. static - -**变量** - -静态变量在内存中只存在一份,只在类第一次实例化时初始化一次,同时类所有的实例都共享静态变量,可以直接通过类名来访问它。 - -但是实例变量则不同,它是伴随着实例的,每创建一个实例就会产生一个实例变量,它与该实例同生共死。 - -**方法** - -静态方法在类加载的时候就存在了,它不依赖于任何实例,所以 static 方法必须实现,也就是说他不能是抽象方法 abstract。 - -**静态语句块** - -静态语句块和静态变量一样在类第一次实例化时运行一次。 - -**初始化顺序** - -静态数据优先于其它数据的初始化,静态变量和静态语句块哪个先运行取决于它们在代码中的顺序。 - -```java -public static String staticField = " 静态变量 "; -``` - -```java -static { - System.out.println(" 静态初始化块 "); -} -``` - -实例变量和普通语句块的初始化在静态变量和静态语句块初始化结束之后。 - -```java -public String field = " 变量 "; -``` - -```java -{ - System.out.println(" 初始化块 "); -} -``` - -最后才是构造函数中的数据进行初始化 - -```java -public InitialOrderTest() { - System.out.println(" 构造器 "); -} -``` - -存在继承的情况下,初始化顺序为: - -1. 父类(静态变量、静态初始化块) -2. 子类(静态变量、静态初始化块) -3. 父类(变量、初始化块) -4. 父类(构造器) -5. 子类(变量、初始化块) -6. 子类(构造器) - - -# Object 通用方法 - -## 1. 概览 - -- public final native Class getClass() -- public native int hashCode() -- public boolean equals(Object obj) -- protected native Object clone() throws CloneNotSupportedException -- public String toString() -- public final native void notify() -- public final native void notifyAll() -- public final native void wait(long timeout) throws InterruptedException -- public final void wait(long timeout, int nanos) throws InterruptedException -- public final void wait() throws InterruptedException -- protected void finalize() throws Throwable { } - -## 2. clone() - -**浅拷贝** - -引用类型引用的是同一个对象,clone() 方法默认就是浅拷贝实现。 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//d990c0e7-64d1-4ba3-8356-111bc91e53c5.png) - -**深拷贝** - -可以使用序列化实现。 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//2e5620c4-b558-46fe-8f12-00c9dd597a61.png) - -> [How do I copy an object in Java?](https://stackoverflow.com/questions/869033/how-do-i-copy-an-object-in-java) - -## 3. equals() - -- 对于基本类型,== 就是判断两个值是否相等; -- 对于引用类型,== 是判断两个引用是否引用同一个对象,而 equals() 是判断引用的对象是否等价。 - -等价性:[ 散列 ](https://github.com/CyC2018/InterviewNotes/blob/master/notes/Java%20%E5%AE%B9%E5%99%A8.md#%E6%95%A3%E5%88%97) - -# 继承 - -## 1. 访问权限 - -Java 中有三个访问权限修饰符:private、protected 以及 public,如果不加访问修饰符,表示包级可见。 - -可以对类或类中的成员(字段以及方法)加上访问修饰符。成员可见表示其它类可以用成员所在类的对象访问到该成员;类可见表示其它类可以用这个类创建对象,可以把类当做包中的一个成员,然后包表示一个类,这样就好理解了。 - -protected 用于修饰成员,表示在继承体系中成员对于子类可见。但是这个访问修饰符对于类没有意义,因为包没有继承体系。 - -更详细的内容:[ 浅析 Java 中的访问权限控制 ](http://www.importnew.com/18097.html) - -## 2. 抽象类与接口的区别 - -抽象类至少包含一个抽象方法,该抽象方法必须在子类中实现。由于抽象类没有抽象方法的具体实现,因此不能对抽象类进行实例化。 - -```java -public abstract class GenericServlet implements Servlet, ServletConfig, Serializable { - // abstract method - abstract void service(ServletRequest req, ServletResponse res); - - void init() { - // Its implementation - } - // other method related to Servlet -} -``` - -接口定义了一组方法,但是接口都没有方法的实现,也就是说这些方法都是抽象方法。 - -```java -public interface Externalizable extends Serializable { - - void writeExternal(ObjectOutput out) throws IOException; - - void readExternal(ObjectInput in) throws IOException, ClassNotFoundException; -} -``` - -更详细的内容:[Java 抽象类与接口的区别 ](http://www.importnew.com/12399.html) - -## 3. super() - -用来访问父类的构造函数父类的方法,第二种情况中,子类需要重载父类的方法。 - -```java -public class Subclass extends Superclass { - // overrides printMethod in Superclass - public void printMethod() { - super.printMethod(); - System.out.println("Printed in Subclass"); - } - public static void main(String[] args) { - Subclass s = new Subclass(); - s.printMethod(); - } -} -``` - -> [Using the Keyword super](https://docs.oracle.com/javase/tutorial/java/IandI/super.html) - -# String - -## 1. String, StringBuffer and StringBuilder - -**是否可变** - -String 不可变,StringBuffer 和 StringBuilder 可变。 - -**是否线程安全** - -String 不可变,因此是线程安全的。 - -StringBuilder 不是线程安全的;StringBuffer 是线程安全的,使用 synchronized 来同步。 - -> [String, StringBuffer, and StringBuilder](https://stackoverflow.com/questions/2971315/string-stringbuffer-and-stringbuilder) - -## 2. String 不可变的原因 - -**可以缓存 hash 值** - -因为 String 的 hash 值经常被使用,例如 String 用做 HashMap 等。不可变的特性可以使得 hash 值也不可变,因此就只需要进行一次计算。 - -**String Pool 的需要** - -如果 String 已经被创建过了,那么就会从 String Pool 中取得引用。只有 String 是不可变的,才可能使用 String Pool。 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//f76067a5-7d5f-4135-9549-8199c77d8f1c.jpg) - -**安全性** - -String 经常作为参数,例如网络连接参数等,在作为网络连接参数的情况下,如果 String 是可变的,那么在网络连接过程中,String 被改变,改变 String 对象的那一方以为现在连接的是其它主机,而实际情况却不一定是。String 不可变性可以保证参数不可变。 - -**线程安全** - -String 不可变性天生具备线程安全,可以在多个线程中使用。 - -> [Why String is immutable in Java?](https://www.programcreek.com/2013/04/why-string-is-immutable-in-java/) - -## 3. String.intern() - -使用 String.intern() 可以保证所有相同内容的字符串变量引用相同的内存对象。 - -更详细的内容:[ 揭开 String.intern() 那神秘的面纱 ](https://www.jianshu.com/p/95f516cb75ef) - -# 基本类型与运算 - -## 1. 包装类型 - -八个基本类型:boolean 1 byte 8 char 16 short 16 int 32 float 32 long 64 double 64 - -基本类型都有对应的包装类型,它们之间的赋值使用自动装箱与拆箱完成。 - -```java -Integer x = 2; // 装箱 -int y = x; // 拆箱 -``` - -new Integer(123) 与 Integer.valueOf(123) 的区别在于,Integer.valueOf(123) 可能会使用缓存对象,因此多次使用 Integer.valueOf(123) 会取得同一个对象的引用。 - -```java -public static void main(String[] args) { - - Integer a = new Integer(1); - Integer b = new Integer(1); - - System.out.println("a==b? " + (a==b)); - - Integer c = Integer.valueOf(1); - Integer d = Integer.valueOf(1); - - System.out.println("c==d? " + (c==d)); - - } -``` - -```html -a==b? false -c==d? true -``` - -valueOf() 方法的实现比较简单,就是先判断值是否在缓存池中,如果在的话就直接使用缓存池的内容。 - -```java -public static Integer valueOf(int i) { - final int offset = 128; - if (i >= -128 && i <= 127) { // must cache - return IntegerCache.cache[i + offset]; - } - return new Integer(i); -} -``` - -The following is the list of primitives stored as immutable objects: - -- 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 - -自动装箱过程编译器会调用 valueOf() 方法,因此多个 Integer 对象使用装箱来创建并且值相同,那么就会引用相同的对象,这样做很显然是为了节省内存开销。 - -```java -Integer x = 1; -Integer y = 1; -System.out.println(c==d); // true -``` - -> [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) - -## 2. switch - -A switch works with the byte, short, char, and int primitive data types. It also works with enumerated types (discussed in Classes and Inheritance) and a few special classes that "wrap" certain primitive types: Character, Byte, Short, and Integer (discussed in Simple Data Objects). - -In the JDK 7 release, you can use a String object in the expression of a switch statement. - -switch 不支持 long,是因为 swicth 的设计初衷是为那些只需要对少数几个值进行等值判断,如果值过于复杂,那么还是用 if 比较合适。 - -> [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) - -switch 使用查找表的方式来实现,JVM 中使用的指令是 lookupswitch。 - -```java -public static void main(String... args) { - switch (1) { - case 1: - break; - case 2: - break; - } -} - -public static void main(java.lang.String[]); - Code: - Stack=1, Locals=1, Args_size=1 - 0: iconst_1 - 1: lookupswitch{ //2 - 1: 28; - 2: 31; - default: 31 } - 28: goto 31 - 31: return -``` - -> [How does Java's switch work under the hood?](https://stackoverflow.com/questions/12020048/how-does-javas-switch-work-under-the-hood) - - - - - - -# 反射 - -每个类都有一个 **Class** 对象,包含了与类有关的信息。当编译一个新类时,会产生一个同名的 .class 文件,该文件内容保存着 Class 对象。 - -类加载相当于 Class 对象的加载。类在第一次使用时才动态加载到 JVM 中,可以使用 Class.forName('com.mysql.jdbc.Driver.class') 这种方式来控制类的加载,该方法会返回一个 Class 对象。 - -反射可以提供运行时的类信息,并且这个类可以在运行时才加载进来,甚至在编译时期该类的 .class 不存在也可以加载进来。 - -Class 和 java.lang.reflect 一起对反射提供了支持,java.lang.reflect 类库包含了 **Field**、**Method** 以及 **Constructor** 类。可以使用 get() 和 set() 方法读取和修改 Field 对象关联的字段,可以使用 invoke() 方法调用与 Method 对象关联的方法,可以用 Constructor 创建新的对象。 - -IDE 使用反射机制获取类的信息,在使用一个类的对象时,能够把类的字段、方法和构造函数等信息列出来供用户选择。 - -更详细的内容:[ 深入解析 Java 反射(1)- 基础 ](http://www.sczyh30.com/posts/Java/java-reflection-1/) - -# 异常 - -Throwable 可以用来表示任何可以作为异常抛出的类,分为两种:**Error** 和 **Exception**,其中 Error 用来表示编译时系统错误。 - -Exception 分为两种:**受检异常** 和 **非受检异常**。受检异常需要用 try...catch... 语句捕获并进行处理,并且可以从异常中恢复;非受检异常是程序运行时错误,例如除 0 会引发 Arithmetic Exception,此时程序奔溃并且无法恢复。 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//48f8f98e-8dfd-450d-8b5b-df4688f0d377.jpg) - -更详细的内容: -- [Java 入门之异常处理 ](https://www.tianmaying.com/tutorial/Java-Exception) -- [Java 异常的面试问题及答案 -Part 1](http://www.importnew.com/7383.html) - -# 泛型 - -泛型提供了编译时的类型检测机制,该机制允许程序员在编译时检测到非法的类型。泛型是 Java 中一个非常重要的知识点,在 Java 集合类框架中泛型被广泛应用。 - -```java -public class Box { - // T stands for "Type" - private T t; - public void set(T t) { this.t = t; } - public T get() { return t; } -} -``` - -更详细的内容: - -- [Java 泛型详解 ](https://www.ziwenxie.site/2017/03/01/java-generic/) -- [10 道 Java 泛型面试题 ](https://cloud.tencent.com/developer/article/1033693) - -# 特性 - -## 1. 三大特性 - -[ 封装、继承、多态 ](https://github.com/CyC2018/InterviewNotes/blob/master/notes/%E9%9D%A2%E5%90%91%E5%AF%B9%E8%B1%A1%E6%80%9D%E6%83%B3.md#%E5%B0%81%E8%A3%85%E7%BB%A7%E6%89%BF%E5%A4%9A%E6%80%81) - -## 2. Java 各版本的新特性 - -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 - -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 - -> [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) - -## 3. Java 与 C++ 的区别 - -Java 是纯粹的面向对象语言,所有的对象都继承自 java.lang.Object,C++ 为了兼容 C 即支持面向对象也支持面向过程。 - -比较详细的内容: - -| Java | C++ | -| -- | -- | -| Java does not support pointers, templates, unions, operator overloading, structures etc. The Java language promoters initially said "No pointers!", but when many programmers questioned how you can work without pointers, the promoters began saying "Restricted pointers." Java supports what it calls "references". References act a lot like pointers in C++ languages but you cannot perform arithmetic on pointers in Java. References have types, and they're type-safe. These references cannot be interpreted as raw address and unsafe conversion is not allowed. | C++ supports structures, unions, templates, operator overloading, pointers and pointer arithmetic.| -| Java support automatic garbage collection. It does not support destructors as C++ does. | C++ support destructors, which is automatically invoked when the object is destroyed. | -| Java does not support conditional compilation and inclusion. | Conditional inclusion (#ifdef #ifndef type) is one of the main features of C++. | -| Java has built in support for threads. In Java, there is a `Thread` class that you inherit to create a new thread and override the `run()` method. | C++ has no built in support for threads. C++ relies on non-standard third-party libraries for thread support. | -| Java does not support default arguments. There is no scope resolution operator (::) in Java. The method definitions must always occur within a class, so there is no need for scope resolution there either. | C++ supports default arguments. C++ has scope resolution operator (::) which is used to to define a method outside a class and to access a global variable within from the scope where a local variable also exists with the same name. | -| There is no _goto_ statement in Java. The keywords `const` and `goto` are reserved, even though they are not used. | C++ has _goto_ statement. However, it is not considered good practice to use of _goto_ statement. | -| Java doesn't provide multiple inheritance, at least not in the same sense that C++ does. | C++ does support multiple inheritance. The keyword `virtual` is used to resolve ambiguities during multiple inheritance if there is any. | -| Exception handling in Java is different because there are no destructors. Also, in Java, try/catch must be defined if the function declares that it may throw an exception. | While in C++, you may not include the try/catch even if the function throws an exception. | -| Java has method overloading, but no operator overloading. The `String` class does use the `+` and `+=` operators to concatenate strings and `String`expressions use automatic type conversion, but that's a special built-in case. | C++ supports both method overloading and operator overloading. | -| Java has built-in support for documentation comments (`/** ... */`); therefore, Java source files can contain their own documentation, which is read by a separate tool usually `javadoc` and reformatted into HTML. This helps keeping documentation maintained in easy way. | C++ does not support documentation comments. | -| Java is interpreted for the most part and hence platform independent. | C++ generates object code and the same code may not run on different platforms. | - -> [What are the main differences between Java and C++?](http://cs-fundamentals.com/tech-interview/java/differences-between-java-and-cpp.php) - -## 4. 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" diff --git a/notes/Java 容器.md b/notes/Java 容器.md deleted file mode 100644 index ffc1cba8..00000000 --- a/notes/Java 容器.md +++ /dev/null @@ -1,362 +0,0 @@ - -* [概览](#概览) - * [1. List](#1-list) - * [2. Set](#2-set) - * [3. Queue](#3-queue) - * [4. Map](#4-map) - * [5. Java 1.0/1.1 容器](#5-java-1011-容器) -* [容器中的设计模式](#容器中的设计模式) - * [1. 迭代器模式](#1-迭代器模式) - * [2. 适配器模式](#2-适配器模式) -* [散列](#散列) -* [源码分析](#源码分析) - * [1. ArraList](#1-arralist) - * [2. Vector 与 Stack](#2-vector-与-stack) - * [3. LinkedList](#3-linkedlist) - * [4. TreeMap](#4-treemap) - * [5. HashMap](#5-hashmap) - * [6. LinkedHashMap](#6-linkedhashmap) - * [7. ConcurrentHashMap](#7-concurrenthashmap) -* [参考资料](#参考资料) - - - -# 概览 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//ebf03f56-f957-4435-9f8f-0f605661484d.jpg) - -容器主要包括 Collection 和 Map 两种,Collection 又包含了 List、Set 以及 Queue。 - -## 1. List - -- ArrayList:基于动态数组实现,支持随机访问; - -- LinkedList:基于双向循环链表实现,只能顺序访问,但是可以快速地在链表中间插入和删除元素。不仅如此,LinkedList 还可以用作栈、队列和双端队列。 - -## 2. Set - -- HashSet:基于 Hash 实现,支持快速查找,但是失去有序性; - -- TreeSet:基于红黑树实现,保持有序,但是查找效率不如 HashSet; - -- LinkedListHashSet:具有 HashSet 的查找效率,且内部使用链表维护元素的插入顺序,因此具有有序性。 - -## 3. Queue - -只有两个实现:LinkedList 和 PriorityQueue,其中 LinkedList 支持双向队列,PriorityQueue 是基于堆结构实现。 - -## 4. Map - -- HashMap:基于 Hash 实现 - -- LinkedHashMap:使用链表来维护元素的顺序,顺序为插入顺序或者最近最少使用(LRU)顺序 - -- TreeMap:基于红黑树实现 - -- ConcurrentHashMap:线程安全 Map,不涉及类似于 HashTable 的同步加锁 - -## 5. Java 1.0/1.1 容器 - -对于旧的容器,我们决不应该使用它们,只需要对它们进行了解。 - -- Vector:和 ArrayList 类似,但它是线程安全的 - -- HashTable:和 HashMap 类似,但它是线程安全的 - -# 容器中的设计模式 - -## 1. 迭代器模式 - -从概览图可以看到,每个集合类都有一个 Iterator 对象,可以通过这个迭代器对象来遍历集合中的元素。 - -[Java 中的迭代器模式 ](https://github.com/CyC2018/InterviewNotes/blob/master/notes/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F.md#92-java-%E5%86%85%E7%BD%AE%E7%9A%84%E8%BF%AD%E4%BB%A3%E5%99%A8) - -## 2. 适配器模式 - -java.util.Arrays#asList() 可以把数组类型转换为 List 类型。 - -```java - List list = Arrays.asList(1, 2, 3); - int[] arr = {1, 2, 3}; - list = Arrays.asList(arr); -``` - -# 散列 - -使用 hasCode() 来返回散列值,使用的是对象的地址。 - -而 equals() 是用来判断两个对象是否相等的,相等的两个对象散列值一定要相同,但是散列值相同的两个对象不一定相等。 - -相等必须满足以下五个性质: - -1. 自反性 -2. 对称性 -3. 传递性 -4. 一致性(多次调用 x.equals(y),结果不变) -5. 对任何不是 null 的对象 x 调用 x.equals(nul) 结果都为 false - -# 源码分析 - -建议先阅读 [ 算法 - 查找 ](https://github.com/CyC2018/InterviewNotes/blob/master/notes/%E7%AE%97%E6%B3%95.md#%E7%AC%AC%E4%B8%89%E7%AB%A0-%E6%9F%A5%E6%89%BE) 部分,对集合类源码的理解有很大帮助。 - -源码下载:[OpenJDK 1.7](http://download.java.net/openjdk/jdk7) - -## 1. ArraList - -[ArraList.java](https://github.com/CyC2018/InterviewNotes/blob/master/notes/src/ArrayList.java) - -实现了 RandomAccess 接口,因此支持随机访问,这是理所当然的,因为 ArrayList 是基于数组实现的。 - -```java -public class ArrayList extends AbstractList - implements List, RandomAccess, Cloneable, java.io.Serializable -``` - -基于数组实现,保存元素的数组使用 transient 修饰,这是因为该数组不一定所有位置都占满元素,因此也就没必要全部都进行序列化。需要重写 writeObject() 和 readObject()。 - -```java -private transient Object[] elementData; -``` - -数组的默认大小为 10 - -```java -public ArrayList(int initialCapacity) { - super(); - if (initialCapacity < 0) - throw new IllegalArgumentException("Illegal Capacity: "+ initialCapacity); - this.elementData = new Object[initialCapacity]; -} - -public ArrayList() { - this(10); -} -``` - -删除元素时调用 System.arraycopy() 对元素进行复制,因此删除操作成本很高,最好在创建时就指定大概的容量大小,减少复制操作的执行次数。 - -```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; // Let gc do its work - - return oldValue; -} -``` - -添加元素时使用 ensureCapacity() 方法来保证容量足够,如果不够时,需要进行扩容,使得新容量为旧容量的 1.5 倍。 - -modCount 用来记录 ArrayList 发生变化的次数,因为每次在进行 add() 和 addAll() 时都需要调用 ensureCapacity(),因此直接在 ensureCapacity() 中对 modCount 进行修改。 - -```java -public void ensureCapacity(int minCapacity) { - if (minCapacity > 0) - ensureCapacityInternal(minCapacity); -} - -private void ensureCapacityInternal(int minCapacity) { - modCount++; - // overflow-conscious code - if (minCapacity - elementData.length > 0) - grow(minCapacity); -} - -private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8; - -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 static int hugeCapacity(int minCapacity) { - if (minCapacity < 0) // overflow - throw new OutOfMemoryError(); - return (minCapacity > MAX_ARRAY_SIZE) ? - Integer.MAX_VALUE : - MAX_ARRAY_SIZE; -} -``` - -在进行序列化或者迭代等操作时,需要比较操作前后 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(); - - // Write out array length - s.writeInt(elementData.length); - - // Write out all elements in the proper order. - for (int i=0; i()); 返回一个线程安全的 ArrayList,也可以使用 concurrent 并发包下的 CopyOnWriteArrayList 类; - -**和 LinkedList 的区别** - -1. ArrayList 基于动态数组实现,LinkedList 基于双向循环链表实现; -2. ArrayList 支持随机访问,LinkedList 不支持; -3. LinkedList 在任意位置添加删除元素更快。 - -## 2. Vector 与 Stack - -[Vector.java](https://github.com/CyC2018/InterviewNotes/blob/master/notes/src/Vector.java) - -## 3. LinkedList - -[LinkedList.java](https://github.com/CyC2018/InterviewNotes/blob/master/notes/src/LinkedList.java) - -## 4. TreeMap - -[TreeMap.java](https://github.com/CyC2018/InterviewNotes/blob/master/notes/src/TreeMap.java) - -## 5. HashMap - -[HashMap.java](https://github.com/CyC2018/InterviewNotes/blob/master/notes/src/HashMap.java) - -使用拉链法来解决冲突。 - -默认容量 capacity 为 16,需要注意的是容量必须保证为 2 的次方。容量就是 Entry[] table 数组的长度,size 是数组的实际使用量。 - -threshold 规定了一个 size 的临界值,size 必须小于 threshold,如果大于等于,就必须进行扩容操作。 - -threshold = capacity * load_factor,其中 load_factor 为 table 数组能够使用的比例,load_factor 过大会导致聚簇的出现,从而影响查询和插入的效率,详见算法笔记。 - -```java -static final int DEFAULT_INITIAL_CAPACITY = 16; - -static final int MAXIMUM_CAPACITY = 1 << 30; - -static final float DEFAULT_LOAD_FACTOR = 0.75f; - -transient Entry[] table; - -transient int size; - -int threshold; - -final float loadFactor; - -transient int modCount; -``` - -从下面的添加元素代码中可以看出,当需要扩容时,令 capacity 为原来的两倍。 - -```java -void addEntry(int hash, K key, V value, int bucketIndex) { - Entry e = table[bucketIndex]; - table[bucketIndex] = new Entry<>(hash, key, value, e); - if (size++ >= threshold) - resize(2 * table.length); -} -``` - -Entry 用来表示一个键值对元素,其中的 next 指针在序列化时会使用。 - -```java -static class Entry implements Map.Entry { - final K key; - V value; - Entry next; - final int hash; -} -``` - -get() 操作需要分成两种情况,key 为 null 和 不为 null,从中可以看出 HashMap 允许插入 null 作为键。 - -```java -public V get(Object key) { - if (key == null) - return getForNullKey(); - int hash = hash(key.hashCode()); - for (Entry e = table[indexFor(hash, table.length)]; e != null; e = e.next) { - Object k; - if (e.hash == hash && ((k = e.key) == key || key.equals(k))) - return e.value; - } - return null; -} -``` - -put() 操作也需要根据 key 是否为 null 做不同的处理,需要注意的是如果本来没有 key 为 null 的键值对,新插入一个 key 为 null 的键值对时默认是放在数组的 0 位置,这是因为 null 不能计算 hash 值,也就无法知道应该放在哪个链表上。 - -```java -public V put(K key, V value) { - if (key == null) - return putForNullKey(value); - int hash = hash(key.hashCode()); - int i = indexFor(hash, table.length); - for (Entry e = table[i]; e != null; e = e.next) { - Object k; - if (e.hash == hash && ((k = e.key) == key || key.equals(k))) { - V oldValue = e.value; - e.value = value; - e.recordAccess(this); - return oldValue; - } - } - - modCount++; - addEntry(hash, key, value, i); - return null; -} -``` - -```java -private V putForNullKey(V value) { - for (Entry e = table[0]; e != null; e = e.next) { - if (e.key == null) { - V oldValue = e.value; - e.value = value; - e.recordAccess(this); - return oldValue; - } - } - modCount++; - addEntry(0, null, value, 0); - return null; -} -``` - -## 6. LinkedHashMap - -[LinkedHashMap.java](https://github.com/CyC2018/InterviewNotes/blob/master/notes/src/HashMap.java) - -## 7. ConcurrentHashMap - -[ConcurrentHashMap.java](https://github.com/CyC2018/InterviewNotes/blob/master/notes/src/HashMap.java) - -[ 探索 ConcurrentHashMap 高并发性的实现机制 ](https://www.ibm.com/developerworks/cn/java/java-lo-concurrenthashmap/) - -# 参考资料 - -- Java 编程思想 diff --git a/notes/Leetcode 题解.md b/notes/Leetcode 题解.md deleted file mode 100644 index fb9d66b3..00000000 --- a/notes/Leetcode 题解.md +++ /dev/null @@ -1,4943 +0,0 @@ - -* [算法思想](#算法思想) - * [二分查找](#二分查找) - * [贪心思想](#贪心思想) - * [双指针](#双指针) - * [排序](#排序) - * [快速选择](#快速选择) - * [堆排序](#堆排序) - * [桶排序](#桶排序) - * [搜索](#搜索) - * [BFS](#bfs) - * [DFS](#dfs) - * [Backtracking](#backtracking) - * [分治](#分治) - * [动态规划](#动态规划) - * [分割整数](#分割整数) - * [矩阵路径](#矩阵路径) - * [斐波那契数列](#斐波那契数列) - * [最长递增子序列](#最长递增子序列) - * [最长公共子系列](#最长公共子系列) - * [0-1 背包](#0-1-背包) - * [数组区间](#数组区间) - * [字符串编辑](#字符串编辑) - * [其它问题](#其它问题) - * [数学](#数学) - * [素数](#素数) - * [最大公约数](#最大公约数) - * [进制转换](#进制转换) - * [阶乘](#阶乘) - * [字符串加法减法](#字符串加法减法) - * [相遇问题](#相遇问题) - * [多数投票问题](#多数投票问题) - * [其它](#其它) -* [数据结构相关](#数据结构相关) - * [栈和队列](#栈和队列) - * [哈希表](#哈希表) - * [字符串](#字符串) - * [数组与矩阵](#数组与矩阵) - * [有序矩阵](#有序矩阵) - * [链表](#链表) - * [树](#树) - * [递归](#递归) - * [层次遍历](#层次遍历) - * [前中后序遍历](#前中后序遍历) - * [BST](#bst) - * [Trie](#trie) - * [图](#图) - * [位运算](#位运算) -* [参考资料](#参考资料) - - - -# 算法思想 - -## 二分查找 - -二分查找思想简单,但是在实现时有一些需要注意的细节: - -1. 在计算 mid 时不能使用 mid = (l + h) / 2 这种方式,因为 l + h 可能会导致加法溢出,应该使用 mid = l + (h - l) / 2。 - -2. 对 h 的赋值和循环条件有关,当循环条件为 l <= h 时,h = mid - 1;当循环条件为 l < h 时,h = mid。 -解释如下:在循环条件为 l <= h 时,如果 h = mid,会出现循环无法退出的情况,例如 l = 1,h = 1,此时 mid 也等于 1,如果此时继续执行 h = mid,那么就会无限循环;在循环条件为 l < h,如果 h = mid - 1,会错误跳过查找的数,例如对于数组 [1,2,3],要查找 1,最开始 l = 0,h = 2,mid = 1,判断 key < arr[mid] 执行 h = mid - 1 = 0,此时循环退出,直接把查找的数跳过了。 - -3. l 的赋值一般都为 l = mid + 1。 - -```java -public int search(int key, int[] arr) { - int l = 0, h = arr.length - 1; - while (l <= h) { - int mid = l + (h - l) / 2; - if (key == arr[mid]) return mid; - if (key < arr[mid]) h = mid - 1; - else l = mid + 1; - } - return -1; -} -``` - -**求开方** - -[Leetcode : 69. Sqrt(x) (Easy)](https://leetcode.com/problems/sqrtx/description/) - -一个数 x 的开方 sqrt 一定在 0 \~ x 之间,并且满足 sqrt == x / sqrt 。可以利用二分查找在 0 \~ x 之间查找 sqrt。 - -```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(sqrt < mid) h = mid - 1; - else l = mid + 1; - } - return h; -} -``` - -**摆硬币** - -[Leetcode : 441. Arranging Coins (Easy)](https://leetcode.com/problems/arranging-coins/description/) - -```html -n = 8 - -The coins can form the following rows: -¤ -¤ ¤ -¤ ¤ ¤ -¤ ¤ - -Because the 4th row is incomplete, we return 3. -``` - -题目描述:第 i 行摆 i 个,统计能够摆的行数。 - -返回 h 而不是 l,因为摆的硬币最后一行不能算进去。 - -```java -public int arrangeCoins(int n) { - int l = 0, h = n; - while(l <= h){ - int m = l + (h - l) / 2; - long x = m * (m + 1L) / 2; - if(x == n) return m; - else if(x < n) l = m + 1; - else h = m - 1; - } - return h; -} -``` - -可以不用二分查找,更直观的解法如下: - -```java -public int arrangeCoins(int n) { - int level = 1; - while (n > 0) { - n -= level; - level++; - } - return n == 0 ? level - 1 : level - 2; -} -``` - -**有序数组的 Single Element** - -[Leetcode : 540. Single Element in a Sorted Array (Medium)](https://leetcode.com/problems/single-element-in-a-sorted-array/description/) - -题目描述:一个有序数组只有一个数不出现两次,找出这个数。 - -```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]; -} -``` - -## 贪心思想 - -贪心思想保证每次操作都是局部最优的,并且最后得到的结果是全局最优的。 - -**分配饼干** - -[Leetcode : 455. Assign Cookies (Easy)](https://leetcode.com/problems/assign-cookies/description/) - -题目描述:每个孩子都有一个满足度,每个饼干都有一个大小,只有饼干的大小大于一个孩子的满足度,该孩子才会获得满足。求解最多可以获得满足的孩子数量。 - -因为最小的孩子最容易得到满足,因此先满足最小孩子。给一个孩子的饼干应当尽量小又能满足该孩子,这样大饼干就能拿来给满足度比较大的孩子。 - -证明:假设在某次选择中,贪心策略选择给第 i 个孩子分配第 m 个饼干,并且第 i 个孩子满足度最小,第 m 个饼干为可以满足第 i 个孩子的最小饼干,利用贪心策略最终可以满足 k 个孩子。假设最优策略在这次选择中给 i 个孩子分配第 n 个饼干,并且这个饼干大于第 m 个饼干。我们发现使用第 m 个饼干去替代第 n 个饼干完全不影响后续的结果,因此不存在比贪心策略更优的策略,即贪心策略就是最优策略。 - -```java -public int findContentChildren(int[] g, int[] s) { - Arrays.sort(g); - Arrays.sort(s); - int i = 0, j = 0; - while(i < g.length && j < s.length){ - if(g[i] <= s[j]) i++; - j++; - } - return i; -} -``` - -**投飞镖刺破气球** - -[Leetcode : 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 -``` - -题目描述:气球在一个水平数轴上摆放,可以重叠,飞镖垂直射向坐标轴,使得路径上的气球都会刺破。求解最小的投飞镖次数使所有气球都被刺破。 - -从左往右投飞镖,并且在每次投飞镖时满足以下条件: - -1. 左边已经没有气球了; -2. 本次投飞镖能够刺破最多的气球。 - -```java -public int findMinArrowShots(int[][] points) { - if(points.length == 0) return 0; - Arrays.sort(points,(a,b) -> (a[1] - b[1])); - int curPos = points[0][1]; - int ret = 1; - for (int i = 1; i < points.length; i++) { - if(points[i][0] <= curPos) { - continue; - } - curPos = points[i][1]; - ret++; - } - return ret; - } -``` - -**股票的最大收益** - -[Leetcode : 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; -} -``` - -**种植花朵** - -[Leetcode : 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 -``` - -题目描述:花朵之间至少需要一个单位的间隔。 - -```java -public boolean canPlaceFlowers(int[] flowerbed, int n) { - int cnt = 0; - for(int i = 0; i < flowerbed.length; i++){ - if(flowerbed[i] == 1) continue; - int pre = i == 0 ? 0 : flowerbed[i - 1]; - int next = i == flowerbed.length - 1 ? 0 : flowerbed[i + 1]; - if(pre == 0 && next == 0) { - cnt++; - flowerbed[i] = 1; - } - } - return cnt >= n; -} -``` - -**修改一个数成为非递减数组** - -[Leetcode : 665. Non-decreasing Array (Easy)](https://leetcode.com/problems/non-decreasing-array/description/) - -题目描述:判断一个数组能不能只修改一个数就成为非递减数组。 - -在出现 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; i++){ - if(nums[i] < nums[i - 1]){ - cnt++; - if(i - 2 >= 0 && nums[i - 2] > nums[i]) nums[i] = nums[i-1]; - else nums[i - 1] = nums[i]; - } - } - return cnt <= 1; -} -``` - -**判断是否为子串** - -[Leetcode : 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) { - for (int i = 0, pos = 0; i < s.length(); i++, pos++) { - pos = t.indexOf(s.charAt(i), pos); - if(pos == -1) return false; - } - return true; -} -``` - -**分隔字符串使同种字符出现在一起** - -[Leetcode : 763. Partition Labels (Medium)](https://leetcode.com/problems/partition-labels/description/) - -```java -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) { - List ret = new ArrayList<>(); - int[] lastIdxs = new int[26]; - for(int i = 0; i < S.length(); i++) lastIdxs[S.charAt(i) - 'a'] = i; - int startIdx = 0; - while(startIdx < S.length()) { - int endIdx = startIdx; - for(int i = startIdx; i < S.length() && i <= endIdx; i++) { - int lastIdx = lastIdxs[S.charAt(i) - 'a']; - if(lastIdx == i) continue; - if(lastIdx > endIdx) endIdx = lastIdx; - } - ret.add(endIdx - startIdx + 1); - startIdx = endIdx + 1; - } - return ret; -} -``` - -**根据身高和序号重组队列** - -[Leetcode : 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, new Comparator() { - public int compare(int[] a, int[] b) { - if(a[0] == b[0]) return a[1] - b[1]; - return b[0] - a[0]; - } - }); - - int n = people.length; - List tmp = new ArrayList<>(); - for(int i = 0; i < n; i++) { - tmp.add(people[i][1], new int[]{people[i][0], people[i][1]}); - } - - int[][] ret = new int[n][2]; - for(int i = 0; i < n; i++) { - ret[i][0] = tmp.get(i)[0]; - ret[i][1] = tmp.get(i)[1]; - } - return ret; -} -``` - -## 双指针 - -双指针主要用于遍历数组,两个指针指向不同的元素,从而协同完成任务。 - -**从一个已经排序的数组中查找出两个数,使它们的和为 0** - -[Leetcode :167. Two Sum II - Input array is sorted (Easy)](https://leetcode.com/problems/two-sum-ii-input-array-is-sorted/description/) - -使用双指针,一个指针指向元素较小的值,一个指针指向元素较大的值。指向较小元素的指针从头向尾遍历,指向较大元素的指针从尾向头遍历。 - -如果两个指针指向元素的和 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; -} -``` - -**反转字符串中的元音字符** - -[Leetcode : 345. Reverse Vowels of a String (Easy)](https://leetcode.com/problems/reverse-vowels-of-a-string/description/) - -使用双指针,指向待反转的两个元音字符,一个指针从头向尾遍历,一个指针从尾到头遍历。 - -```java -private HashSet vowels = new HashSet<>(Arrays.asList('a','e','i','o','u','A','E','I','O','U')); - -public String reverseVowels(String s) { - if(s.length() == 0) return 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; - i++; - } else if(!vowels.contains(cj)){ - result[j] = cj; - j--; - } else{ - result[i] = cj; - result[j] = ci; - i++; - j--; - } - } - return new String(result); -} -``` - -**两数平方和** - -[Leetcode : 633. Sum of Square Numbers (Easy)](https://leetcode.com/problems/sum-of-square-numbers/description/) - -题目描述:判断一个数是否为两个数的平方和,例如 5 = 12 + 22。 - -```java -public boolean judgeSquareSum(int c) { - int left = 0, right = (int) Math.sqrt(c); - while(left <= right){ - int powSum = left * left + right * right; - if(powSum == c) return true; - else if(powSum > c) right--; - else left++; - } - return false; -} -``` - -**回文字符串** - -[Leetcode : 680. Valid Palindrome II (Easy)](https://leetcode.com/problems/valid-palindrome-ii/description/) - -题目描述:字符串可以删除一个字符,判断是否能构成回文字符串。 - -```java -public boolean validPalindrome(String s) { - int i = 0, j = s.length() -1; - while(i < j){ - if(s.charAt(i) != s.charAt(j)){ - return isPalindrome(s, i, j - 1) || isPalindrome(s, i + 1, j); - } - i++; - j--; - } - return true; -} - -private boolean isPalindrome(String s, int l, int r){ - while(l < r){ - if(s.charAt(l) != s.charAt(r)) - return false; - l++; - r--; - } - return true; -} -``` - -**归并两个有序数组** - -[Leetcode : 88. Merge Sorted Array (Easy)](https://leetcode.com/problems/merge-sorted-array/description/) - -题目描述:把归并结果存到第一个数组上 - -```java -public void merge(int[] nums1, int m, int[] nums2, int n) { - int i = m - 1, j = n - 1; // 需要从尾开始遍历,否则在 nums1 上归并得到的值会覆盖还未进行归并比较的值 - int idx = m + n - 1; - while(i >= 0 || j >= 0){ - if(i < 0) nums1[idx] = nums2[j--]; - else if(j < 0) nums1[idx] = nums1[i--]; - else if(nums1[i] > nums2[j]) nums1[idx] = nums1[i--]; - else nums1[idx] = nums2[j--]; - idx--; - } -} -``` - -**判断链表是否存在环** - -[Leetcode : 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){ - if(l1 == l2) return true; - l1 = l1.next; - if(l2.next == null) break; - l2 = l2.next.next; - } - return false; -} -``` - -**最长子序列** - -[Leetcode : 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 中的一个字符串。要求在 d 中找到满足条件的最长字符串。 - -```java -public String findLongestWord(String s, List d) { - String ret = ""; - for (String str : d) { - for (int i = 0, j = 0; i < s.length() && j < str.length(); i++) { - if (s.charAt(i) == str.charAt(j)) j++; - if (j == str.length()) { - if (ret.length() < str.length() - || (ret.length() == str.length() && ret.compareTo(str) > 0)) { - ret = str; - } - } - } - } - return ret; -} -``` - -## 排序 - -### 快速选择 - -一般用于求解 **Kth Element** 问题,可以在 O(n) 时间复杂度,O(1) 空间复杂度完成求解工作。 - -与快速排序一样,快速选择一般需要先打乱数组,否则最坏情况下时间复杂度为 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** - -[Leetocde : 215. Kth Largest Element in an Array (Medium)](https://leetcode.com/problems/kth-largest-element-in-an-array/description/) - -**排序**:时间复杂度 O(nlgn),空间复杂度 O(1) 解法 - -```java -public int findKthLargest(int[] nums, int k) { - int N = nums.length; - Arrays.sort(nums); - return nums[N - k]; -} -``` - -**堆排序**:时间复杂度 O(nlgk),空间复杂度 O(k) - -```java -public int findKthLargest(int[] nums, int k) { - PriorityQueue pq = new PriorityQueue<>(); - for(int val : nums) { - pq.offer(val); - if(pq.size() > k) { - pq.poll(); - } - } - return pq.peek(); -} -``` - -**快速选择**:时间复杂度 O(n),空间复杂度 O(1) - -```java -public int findKthLargest(int[] nums, int k) { - k = nums.length - k; - int lo = 0; - int hi = nums.length - 1; - while (lo < hi) { - final int j = partition(nums, lo, hi); - if(j < k) { - lo = j + 1; - } else if (j > k) { - hi = j - 1; - } else { - break; - } - } - return nums[k]; - } - - private int partition(int[] a, int lo, int hi) { - int i = lo; - int j = hi + 1; - while(true) { - while(i < hi && less(a[++i], a[lo])); - while(j > lo && less(a[lo], a[--j])); - if(i >= j) { - break; - } - exch(a, i, j); - } - exch(a, lo, j); - return j; - } - - private void exch(int[] a, int i, int j) { - final int tmp = a[i]; - a[i] = a[j]; - a[j] = tmp; - } - - private boolean less(int v, int w) { - return v < w; - } -} -``` - -### 桶排序 - -**找出出现频率最多的 k 个数** - -[Leetcode : 347. Top K Frequent Elements (Medium)](https://leetcode.com/problems/top-k-frequent-elements/description/) - -```java -public List topKFrequent(int[] nums, int k) { - List ret = new ArrayList<>(); - Map map = new HashMap<>(); - for(int num : nums) { - map.put(num, map.getOrDefault(num, 0) + 1); - } - List[] bucket = new List[nums.length + 1]; - for(int key : map.keySet()) { - int frequency = map.get(key); - if(bucket[frequency] == null) { - bucket[frequency] = new ArrayList<>(); - } - bucket[frequency].add(key); - } - - for(int i = bucket.length - 1; i >= 0 && ret.size() < k; i--) { - if(bucket[i] != null) { - ret.addAll(bucket[i]); - } - } - return ret; -} -``` - -## 搜索 - -深度优先搜索和广度优先搜索广泛运用于树和图中,但是它们的应用远远不止如此。 - -### BFS - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//4ff355cf-9a7f-4468-af43-e5b02038facc.jpg) - -广度优先搜索的搜索过程有点像一层一层地进行遍历:从节点 0 出发,遍历到 6、2、1 和 5 这四个新节点。 - -继续从 6 开始遍历,得到节点 4 ;从 2 开始遍历,没有下一个节点;从 1 开始遍历,没有下一个节点;从 5 开始遍历,得到 3 和 4 节点。这一轮总共得到两个新节点:4 和 3 。 - -反复从新节点出发进行上述的遍历操作。 - -可以看到,每一轮遍历的节点都与根节点路径长度相同。设 di 表示第 i 个节点与根节点的路径长度,推导出一个结论:对于先遍历的节点 i 与后遍历的节点 j,有 di<=dj。利用这个结论,可以求解最短路径 **最优解** 问题:第一次遍历到目的节点,其所经过的路径为最短路径,如果继续遍历,之后再遍历到目的节点,所经过的路径就不是最短路径。 - -在程序实现 BFS 时需要考虑以下问题: - -- 队列:用来存储每一轮遍历的节点 -- 标记:对于遍历过得节点,应该将它标记,防止重复遍历; - -**计算在网格中从原点到特定点的最短路径长度** - -```html -[[1,1,0,1], -[1,0,1,0], -[1,1,1,1], -[1,0,1,1]] -``` - -```java -public int minPathLength(int[][] grids, int tr, int tc) { - int[][] next = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}}; - int m = grids.length, n = grids[0].length; - Queue queue = new LinkedList<>(); - queue.add(new Position(0, 0, 1)); - while (!queue.isEmpty()) { - Position pos = queue.poll(); - for (int i = 0; i < 4; i++) { - Position nextPos = new Position(pos.r + next[i][0], pos.c + next[i][1], pos.length + 1); - if (nextPos.r < 0 || nextPos.r >= m || nextPos.c < 0 || nextPos.c >= n) continue; - if (grids[nextPos.r][nextPos.c] != 1) continue; - grids[nextPos.r][nextPos.c] = 0; - if (nextPos.r == tr && nextPos.c == tc) return nextPos.length; - queue.add(nextPos); - } - } - return -1; -} - -private class Position { - int r, c, length; - public Position(int r, int c, int length) { - this.r = r; - this.c = c; - this.length = length; - } -} -``` - - -### DFS - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//f7f7e3e5-7dd4-4173-9999-576b9e2ac0a2.png) - -广度优先搜索一层一层遍历,每一层遍历到的所有新节点,要用队列先存储起来以备下一层遍历的时候再遍历;而深度优先搜索在遍历到一个新节点时立马对新节点进行遍历:从节点 0 出发开始遍历,得到到新节点 6 时,立马对新节点 6 进行遍历,得到新节点 4;如此反复以这种方式遍历新节点,直到没有新节点了,此时返回。返回到根节点 0 的情况是,继续对根节点 0 进行遍历,得到新节点 2,然后继续以上步骤。 - -从一个节点出发,使用 DFS 对一个图进行遍历时,能够遍历到的节点都是从初始节点可达的,DFS 常用来求解这种 **可达性** 问题。 - -在程序实现 DFS 时需要考虑以下问题: - -- 栈:用栈来保存当前节点信息,当遍历新节点返回时能够继续遍历当前节点。也可以使用递归栈。 -- 标记:和 BFS 一样同样需要对已经遍历过得节点进行标记。 - -**查找最大的连通面积** - -[Leetcode : 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 -public int maxAreaOfIsland(int[][] grid) { - int m = grid.length, n = grid[0].length; - int max = 0; - for(int i = 0; i < m; i++){ - for(int j = 0; j < n; j++){ - if(grid[i][j] == 1) max = Math.max(max, dfs(grid, i, j)); - } - } - return max; -} - -private int dfs(int[][] grid, int i, int j){ - int m = grid.length, n = grid[0].length; - if(i < 0 || i >= m || j < 0 || j >= n) return 0; - if(grid[i][j] == 0) return 0; - grid[i][j] = 0; - return dfs(grid, i + 1, j) + dfs(grid, i - 1, j) + dfs(grid, i, j + 1) + dfs(grid, i, j - 1) + 1; -} -``` - -**图的连通分量** - -[Leetcode : 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. -``` - -```java -public int findCircleNum(int[][] M) { - int n = M.length; - int ret = 0; - boolean[] hasFind = new boolean[n]; - for(int i = 0; i < n; i++) { - if(!hasFind[i]) { - dfs(M, i, hasFind); - ret++; - } - } - return ret; -} - -private void dfs(int[][] M, int i, boolean[] hasFind) { - hasFind[i] = true; - int n = M.length; - for(int k = 0; k < n; k++) { - if(M[i][k] == 1 && !hasFind[k]) { - dfs(M, k, hasFind); - } - } -} -``` - -**矩阵中的连通区域数量** - -[Leetcode : 200. Number of Islands (Medium)](https://leetcode.com/problems/number-of-islands/description/) - -```html -11110 -11010 -11000 -00000 -Answer: 1 -``` - -```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 ret = 0; - for (int i = 0; i < m; i++) { - for (int j = 0; j < n; j++) { - if (grid[i][j] == '1') { - dfs(grid, i, j); - ret++; - } - } - } - return ret; -} - -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 k = 0; k < direction.length; k++) { - dfs(grid, i + direction[k][0], j + direction[k][1]); - } -} -``` - -**输出二叉树中所有从根到叶子的路径** - -[Leetcode : 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 ret = new ArrayList(); - if(root == null) return ret; - dfs(root, "", ret); - return ret; -} - -private void dfs(TreeNode root, String prefix, List ret){ - if(root == null) return; - if(root.left == null && root.right == null){ - ret.add(prefix + root.val); - return; - } - prefix += (root.val + "->"); - dfs(root.left, prefix, ret); - dfs(root.right, prefix, ret); -} -``` - -**填充封闭区域** - -[Leetcode : 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 i = 0; i < direction.length; i++) { - dfs(board, r + direction[i][0], c + direction[i][1]); - } -} -``` - -**从两个方向都能到达的区域** - -[Leetcode : 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; - this.m = matrix.length; - this.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 i = 0; i < direction.length; i++) { - int nextR = direction[i][0] + r; - int nextC = direction[i][1] + c; - if (nextR < 0 || nextR >= m || nextC < 0 || nextC >= n - || matrix[r][c] > matrix[nextR][nextC]) continue; - dfs(nextR, nextC, canReach); - } -} -``` - -**N 皇后** - -[Leetcode : 51. N-Queens (Hard)](https://leetcode.com/problems/n-queens/description/) - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//1f080e53-4758-406c-bb5f-dbedf89b63ce.jpg) - -题目描述:在 n\*n 的矩阵中摆放 n 个皇后,并且每个皇后不能在同一行,同一列,同一对角线上,要求解所有的 n 皇后解。 - -一行一行地摆放,在确定一行中的那个皇后应该摆在哪一列时,需要用三个标记数组来确定某一列是否合法,这三个标记数组分别为:列标记数组、45 度对角线标记数组和 135 度对角线标记数组。 - -45 度对角线标记数组的维度为 2\*n - 1,通过下图可以明确 (r,c) 的位置所在的数组下标为 r + c。 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//85583359-1b45-45f2-9811-4f7bb9a64db7.jpg) - -135 度对角线标记数组的维度也是 2\*n - 1,(r,c) 的位置所在的数组下标为 n - 1 - (r - c)。 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//9e80f75a-b12b-4344-80c8-1f9ccc2d5246.jpg) - -```java -private List> ret; -private char[][] nQueens; -private boolean[] colUsed; -private boolean[] diagonals45Used; -private boolean[] diagonals135Used; -private int n; - -public List> solveNQueens(int n) { - ret = new ArrayList<>(); - nQueens = new char[n][n]; - Arrays.fill(nQueens, '.'); - colUsed = new boolean[n]; - diagonals45Used = new boolean[2 * n - 1]; - diagonals135Used = new boolean[2 * n - 1]; - this.n = n; - backstracking(0); - return ret; -} - -private void backstracking(int row) { - if (row == n) { - List list = new ArrayList<>(); - for (char[] chars : nQueens) { - list.add(new String(chars)); - } - ret.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; - backstracking(row + 1); - colUsed[col] = diagonals45Used[diagonals45Idx] = diagonals135Used[diagonals135Idx] = false; - nQueens[row][col] = '.'; - } -} -``` - -### Backtracking - -回溯是 DFS 的一种,它不是用在遍历图的节点上,而是用于求解 **排列组合** 问题,例如有 { 'a','b','c' } 三个字符,求解所有由这三个字符排列得到的字符串。 - -在程序实现时,回溯需要注意对元素进行标记的问题。使用递归实现的回溯,在访问一个新元素进入新的递归调用,此时需要将新元素标记为已经访问,这样才能在继续递归调用时不用重复访问该元素;但是在递归返回时,需要将该元素标记为未访问,因为只需要保证在一个递归链中不同时访问一个元素,而在不同的递归链是可以访问已经访问过但是不在当前递归链中的元素。 - -**数字键盘组合** - -[Leetcode : 17. Letter Combinations of a Phone Number (Medium)](https://leetcode.com/problems/letter-combinations-of-a-phone-number/description/) - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//a3f34241-bb80-4879-8ec9-dff2d81b514e.jpg) - -```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 ret = new ArrayList<>(); - if (digits != null && digits.length() != 0) { - combination("", digits, 0, ret); - } - return ret; -} - -private void combination(String prefix, String digits, int offset, List ret) { - if (offset == digits.length()) { - ret.add(prefix); - return; - } - String letters = KEYS[digits.charAt(offset) - '0']; - for (char c : letters.toCharArray()) { - combination(prefix + c, digits, offset + 1, ret); - } -} -``` - -**在矩阵中寻找字符串** - -[Leetcode : 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 static int[][] shift = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}}; -private static boolean[][] visited; -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; - visited = new boolean[m][n]; - for (int i = 0; i < m; i++) { - for (int j = 0; j < n; j++) { - if (dfs(board, word, 0, i, j)) return true; - } - } - return false; -} - -private boolean dfs(char[][] board, String word, int start, int r, int c) { - if (start == word.length()) { - return true; - } - if (r < 0 || r >= m || c < 0 || c >= n || board[r][c] != word.charAt(start) || visited[r][c] ) { - return false; - } - visited[r][c] = true; - for (int i = 0; i < shift.length; i++) { - int nextR = r + shift[i][0]; - int nextC = c + shift[i][1]; - if (dfs(board, word, start + 1, nextR, nextC)) return true; - } - visited[r][c] = false; - return false; -} -``` - -**IP 地址划分** - -[Leetcode : 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 -private List ret; - -public List restoreIpAddresses(String s) { - ret = new ArrayList<>(); - doRestore(0, "", s); - return ret; -} - -private void doRestore(int k, String path, String s) { - if (k == 4 || s.length() == 0) { - if (k == 4 && s.length() == 0) { - ret.add(path); - } - 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) { - doRestore(k + 1, path.length() != 0 ? path + "." + part : part, s.substring(i + 1)); - } - } -} -``` - -**排列** - -[Leetcode : 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> ret = new ArrayList<>(); - List permuteList = new ArrayList<>(); - boolean[] visited = new boolean[nums.length]; - backtracking(permuteList, visited, nums, ret); - return ret; -} - -private void backtracking(List permuteList, boolean[] visited, int[] nums, List> ret){ - if(permuteList.size() == nums.length){ - ret.add(new ArrayList(permuteList)); - return; - } - - for(int i = 0; i < visited.length; i++){ - if(visited[i]) continue; - visited[i] = true; - permuteList.add(nums[i]); - backtracking(permuteList, visited, nums, ret); - permuteList.remove(permuteList.size() - 1); - visited[i] = false; - } -} -``` - -**含有相同元素求排列** - -[Leetcode : 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> ret = new ArrayList<>(); - List permuteList = new ArrayList<>(); - Arrays.sort(nums); - boolean[] visited = new boolean[nums.length]; - backtracking(permuteList, visited, nums, ret); - return ret; -} - -private void backtracking(List permuteList, boolean[] visited, int[] nums, List> ret) { - if (permuteList.size() == nums.length) { - ret.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, visited, nums, ret); - permuteList.remove(permuteList.size() - 1); - visited[i] = false; - } -} -``` - -**组合** - -[Leetcode : 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> ret = new ArrayList<>(); - List combineList = new ArrayList<>(); - backtracking(1, n, k, combineList, ret); - return ret; -} - -private void backtracking(int start, int n, int k, List combineList, List> ret){ - if(k == 0){ - ret.add(new ArrayList(combineList)); // 这里要重新构造一个 List - return; - } - - for(int i = start; i <= n - k + 1; i++){ // 剪枝 - - combineList.add(i); // 把 i 标记为已访问 - backtracking(i + 1, n, k - 1, combineList, ret); - combineList.remove(combineList.size() - 1); // 把 i 标记为未访问 - } -} -``` - -**组合求和** - -[Leetcode : 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 - private List> ret; - - public List> combinationSum(int[] candidates, int target) { - ret = new ArrayList<>(); - doCombination(candidates, target, 0, new ArrayList<>()); - return ret; - } - - private void doCombination(int[] candidates, int target, int start, List list) { - if (target == 0) { - ret.add(new ArrayList<>(list)); - return; - } - for (int i = start; i < candidates.length; i++) { - if (candidates[i] <= target) { - list.add(candidates[i]); - doCombination(candidates, target - candidates[i], i, list); - list.remove(list.size() - 1); - } - } - } -``` - -**含有相同元素的求组合求和** - -[Leetcode : 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 -private List> ret; - -public List> combinationSum2(int[] candidates, int target) { - ret = new ArrayList<>(); - Arrays.sort(candidates); - doCombination(candidates, target, 0, new ArrayList<>(), new boolean[candidates.length]); - return ret; -} - -private void doCombination(int[] candidates, int target, int start, List list, boolean[] visited) { - if (target == 0) { - ret.add(new ArrayList<>(list)); - return; - } - for (int i = start; i < candidates.length; i++) { - if (i != 0 && candidates[i] == candidates[i - 1] && !visited[i - 1]) continue; - if (candidates[i] <= target) { - list.add(candidates[i]); - visited[i] = true; - doCombination(candidates, target - candidates[i], i + 1, list, visited); - visited[i] = false; - list.remove(list.size() - 1); - } - } -} -``` - -**子集** - -[Leetcode : 78. Subsets (Medium)](https://leetcode.com/problems/subsets/description/) - -题目描述:找出集合的所有子集,子集不能重复,[1, 2] 和 [2, 1] 这种子集算重复 - -```java -private List> ret; -private List subsetList; - -public List> subsets(int[] nums) { - ret = new ArrayList<>(); - subsetList = new ArrayList<>(); - for (int i = 0; i <= nums.length; i++) { - backtracking(0, i, nums); - } - return ret; -} - -private void backtracking(int startIdx, int size, int[] nums) { - if (subsetList.size() == size) { - ret.add(new ArrayList(subsetList)); - return; - } - - for (int i = startIdx; i < nums.length; i++) { - subsetList.add(nums[i]); - backtracking(i + 1, size, nums); // startIdx 设为下一个元素,使 subset 中的元素都递增排序 - subsetList.remove(subsetList.size() - 1); - } -} -``` - -**含有相同元素求子集** - -[Leetcode : 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 -private List> ret; -private List subsetList; -private boolean[] visited; - -public List> subsetsWithDup(int[] nums) { - ret = new ArrayList<>(); - subsetList = new ArrayList<>(); - visited = new boolean[nums.length]; - Arrays.sort(nums); - for (int i = 0; i <= nums.length; i++) { - backtracking(0, i, nums); - } - return ret; -} - -private void backtracking(int startIdx, int size, int[] nums) { - if (subsetList.size() == size) { - ret.add(new ArrayList(subsetList)); - return; - } - - for (int i = startIdx; i < nums.length; i++) { - if (i != 0 && nums[i] == nums[i - 1] && !visited[i - 1]) continue; - subsetList.add(nums[i]); - visited[i] = true; - backtracking(i + 1, size, nums); - visited[i] = false; - subsetList.remove(subsetList.size() - 1); - } -} -``` - -**分割字符串使得每部分都是回文数** - -[Leetcode : 131. Palindrome Partitioning (Medium)](https://leetcode.com/problems/palindrome-partitioning/description/) - -```java -private List> ret; - -public List> partition(String s) { - ret = new ArrayList<>(); - doPartion(new ArrayList<>(), s); - return ret; -} - -private void doPartion(List list, String s) { - if (s.length() == 0) { - ret.add(new ArrayList<>(list)); - return; - } - for (int i = 0; i < s.length(); i++) { - if (isPalindrome(s, 0, i)) { - list.add(s.substring(0, i + 1)); - doPartion(list, s.substring(i + 1)); - list.remove(list.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; -} -``` - -**数独** - -[Leetcode : 37. Sudoku Solver (Hard)](https://leetcode.com/problems/sudoku-solver/description/) - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//1ca52246-c443-48ae-b1f8-1cafc09ec75c.png) - -```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; - } - } - for (int i = 0; i < 9; i++) { - for (int j = 0; j < 9; j++) { - backtracking(i, j); - } - } -} - -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; -} -``` - -## 分治 - -**给表达式加括号** - -[Leetcode : 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) { - int n = input.length(); - List ret = new ArrayList<>(); - for (int i = 0; i < n; 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 '+': ret.add(l + r); break; - case '-': ret.add(l - r); break; - case '*': ret.add(l * r); break; - } - } - } - } - } - if (ret.size() == 0) ret.add(Integer.valueOf(input)); - return ret; -} -``` - -## 动态规划 - -递归和动态规划都是将原问题拆成多个子问题然后求解,他们之间最本质的区别是,动态规划保存了子问题的解。 - -### 分割整数 - -**分割整数的最大乘积** - -[Leetcode : 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]; -} -``` - -**按平方数来分割整数** - -[Leetcode : 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 squares = new ArrayList<>(); // 存储小于 n 的平方数 - int diff = 3; - while(square <= n) { - squares.add(square); - square += diff; - diff += 2; - } - int[] dp = new int[n + 1]; - for(int i = 1; i <= n; i++) { - int max = Integer.MAX_VALUE; - for(int s : squares) { - if(s > i) break; - max = Math.min(max, dp[i - s] + 1); - } - dp[i] = max; - } - return dp[n]; -} -``` - -**分割整数构成字母字符串** - -[Leetcode : 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]; -} -``` - -### 矩阵路径 - -**矩阵的总路径数** - -[Leetcode : 62. Unique Paths (Medium)](https://leetcode.com/problems/unique-paths/description/) - -题目描述:统计从矩阵左上角到右下角的路径总数,每次只能向左和向下移动。 - -```java -public int uniquePaths(int m, int n) { - int[] dp = new int[n]; - for (int i = 0; i < m; i++) { - for (int j = 0; j < n; j++) { - if(i == 0) dp[j] = 1; - else if(j != 0) dp[j] = dp[j] + dp[j - 1]; - } - } - return dp[n - 1]; -} -``` - -**矩阵的最小路径和** - -[Leetcode : 64. Minimum Path Sum (Medium)](https://leetcode.com/problems/minimum-path-sum/description/) - -题目描述:求从矩阵的左上角到右下角的最小路径和,每次只能向左和向下移动。 - -```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[0] = dp[0] + grid[i][0]; - else if(i == 0) dp[j] = dp[j - 1] + grid[0][j]; - else dp[j] = Math.min(dp[j - 1], dp[j]) + grid[i][j]; - } - } - return dp[n - 1]; -} -``` - -### 斐波那契数列 - -**爬楼梯** - -[Leetcode : 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[N] 即为所求。 - -考虑到 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 == 1) return 1; - if(n == 2) return 2; - // 前一个楼梯、后一个楼梯 - int pre1 = 2, pre2 = 1; - for(int i = 2; i < n; i++){ - int cur = pre1 + pre2; - pre2 = pre1; - pre1 = cur; - } - return pre1; -} -``` - -**母牛生产** - -[程序员代码面试指南-P181](#) - -题目描述:假设农场中成熟的母牛每年都会生 1 头小母牛,并且永远不会死。第一年有 1 只小母牛,从第二年开始,母牛开始生小母牛。每只小母牛 3 年之后成熟又可以生小母牛。给定整数 N,求 N 年后牛的数量。 - -第 i 年成熟的牛的数量为: - -

- -**强盗抢劫** - -[Leetcode : 198. House Robber (Easy)](https://leetcode.com/problems/house-robber/description/) - -题目描述:抢劫一排住户,但是不能抢邻近的住户,求最大抢劫量。 - -定义 dp 数组用来存储最大的抢劫量,其中 dp[i] 表示抢到第 i 个住户时的最大抢劫量。由于不能抢劫邻近住户,因此如果抢劫了第 i 个住户那么只能抢劫 i - 2 和 i - 3 的住户,所以 - -

- -O(n) 空间复杂度实现方法: - -```java -public int rob(int[] nums) { - int n = nums.length; - if(n == 0) return 0; - if(n == 1) return nums[0]; - if(n == 2) return Math.max(nums[0], nums[1]); - int[] dp = new int[n]; - dp[0] = nums[0]; - dp[1] = nums[1]; - dp[2] = nums[0] + nums[2]; - for(int i = 3; i < n; i++){ - dp[i] = Math.max(dp[i -2], dp[i - 3]) + nums[i]; - } - return Math.max(dp[n - 1], dp[n - 2]); -} -``` - -O(1) 空间复杂度实现方法: - -```java -public int rob(int[] nums) { - int n = nums.length; - if(n == 0) return 0; - if(n == 1) return nums[0]; - if(n == 2) return Math.max(nums[0], nums[1]); - int pre3 = nums[0], pre2 = nums[1], pre1 = nums[2] + nums[0]; - for(int i = 3; i < n; i++){ - int cur = Math.max(pre2, pre3) + nums[i]; - pre3 = pre2; - pre2 = pre1; - pre1 = cur; - } - return Math.max(pre1, pre2); -} -``` - -**强盗在环形街区抢劫** - -[Leetcode : 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 s, int e) { - int n = nums.length; - if(e - s == 0) return nums[s]; - if(e - s == 1) return Math.max(nums[s], nums[s + 1]); - int[] dp = new int[n]; - dp[s] = nums[s]; - dp[s + 1] = nums[s + 1]; - dp[s + 2] = nums[s] + nums[s + 2]; - for (int i = s + 3; i <= e; i++) { - dp[i] = Math.max(dp[i - 2], dp[i - 3]) + nums[i]; - } - return Math.max(dp[e], dp[e - 1]); -} -``` - - -**信件错排** - -题目描述:有 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 种取值,因此共有 (n-1)\*dp[i-1] 种错误装信方式。 - -综上所述,错误装信数量方式数量为: - -

- -dp[N] 即为所求。 - -和上楼梯问题一样,dp[i] 只与 dp[i-1] 和 dp[i-2] 有关,因此也可以只用两个变量来存储 dp[i-1] 和 dp[i-2]。 - -### 最长递增子序列 - -已知一个序列 {S1, S2,...,Sn} ,取出若干数组成新的序列 {Si1, Si2,..., Sim},其中 i1、i2 ... im 保持递增,即新序列中各个数仍然保持原数列中的先后顺序,称新序列为原序列的一个**子序列**。 - -如果在子序列中,当下标 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} 即为所求。 - -**最长递增子序列** - -[Leetcode : 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; - } - 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 的最长递增子序列的最后一个元素,例如对于数组 [4,5,6,3],有 - -```html -len = 1 : [4], [5], [6], [3] => tails[0] = 3 -len = 2 : [4, 5], [5, 6] => tails[1] = 5 -len = 3 : [4, 5, 6] => tails[2] = 6 -``` - -对于一个元素 x,如果它大于 tails 数组所有的值,那么把它添加到 tails 后面;如果 tails[i-1] < x <= tails[i],那么更新 tails[i] = x 。 - -可以看出 tails 数组保持有序,因此在查找 Si 位于 tails 数组的位置时就可以使用二分查找。 - -```java -public int lengthOfLIS(int[] nums) { - int n = nums.length; - int[] tails = new int[n]; - int size = 0; - for(int i = 0; i < n; i++){ - int idx = binarySearch(tails, 0, size, nums[i]); - tails[idx] = nums[i]; - if(idx == size) size++; - } - return size; -} - -private int binarySearch(int[] nums, int sIdx, int eIdx, int key){ - while(sIdx < eIdx){ - int mIdx = sIdx + (eIdx - sIdx) / 2; - if(nums[mIdx] == key) return mIdx; - else if(nums[mIdx] > key) eIdx = mIdx; - else sIdx = mIdx + 1; - } - return sIdx; -} -``` - -**最长摆动子序列** - -[Leetcode : 376. Wiggle Subsequence (Medium)](https://leetcode.com/problems/wiggle-subsequence/description/) - -要求:使用 O(n) 时间复杂度求解。 - -使用两个状态 up 和 down。 - -```java -public int wiggleMaxLength(int[] nums) { - int len = nums.length; - if (len == 0) return 0; - int up = 1, down = 1; - for (int i = 1; i < len; 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 。 -③ 由于 2 ,在求最终解时,最长公共子序列中 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] 表示体积不超过 j 的情况下,前 i 件物品能达到的最大价值。设第 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][W]; - for (int i = W - 1; i >= 0; i--) { - dp[0][i] = i > weights[0] ? values[0] : 0; - } - for (int i = 1; i < N; i++) { - for (int j = W - 1; j >= weights[i]; j--) { - dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - weights[i]] + values[i]); - } - for (int j = weights[i - 1] - 1; j >= 0; j--) { - dp[i][j] = dp[i - 1][j]; - } - } - return dp[N - 1][W - 1]; -} -``` - -**空间优化** - -在程序实现时可以对 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],在程序实现时需要按倒序来循环求解。 - -**无法使用贪心算法的解释** - -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 | - -**变种** - -完全背包:物品可以无限个,可以转换为 0-1 背包,令每种物品的体积和价值变为 1/2/4... 倍数,把它们都当成一个新物品,然后一种物品只能添加一次。 - -多重背包:物品数量有限制,同样可以转换为 0-1 背包。 - -多维费用背包:物品不仅有重量,还有体积,同时考虑这两种限制。 - -其它:物品之间相互约束或者依赖。 - -**划分数组为和相等的两部分** - -[Leetcode : 416. Partition Equal Subset Sum (Medium)](https://leetcode.com/problems/partition-equal-subset-sum/description/) - -可以看成一个背包大小为 sum/2 的 0-1 背包问题,但是也有不同的地方,这里没有价值属性,并且背包必须被填满。 - -以下实现使用了空间优化。 - -```java -public boolean canPartition(int[] nums) { - int sum = 0; - for (int num : nums) { - sum += num; - } - if (sum % 2 != 0) { - return false; - } - int W = sum / 2; - boolean[] dp = new boolean[W + 1]; - int n = nums.length; - for(int i = 0; i <= W; i++) { - if(nums[0] == i) dp[i] = true; - } - for(int i = 1; i < n; i++) { - for(int j = W; j >= nums[i]; j--) { - dp[j] = dp[j] || dp[j - nums[i]]; - } - } - - return dp[W]; -} -``` - -**字符串按单词列表分割** - -[Leetcode : 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". -``` - -```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) { - if (word.length() <= i - && word.equals(s.substring(i - word.length(), i))) { - dp[i] = dp[i] || dp[i - word.length()]; - } - } - } - return dp[n]; -} -``` - -**改变一组数的正负号使得它们的和为一给定数** - -[Leetcode : 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 = 0; - for (int num : nums) { - sum += num; - } - if (sum < S || (sum + S) % 2 == 1) { - return 0; - } - return subsetSum(nums, (sum + S) >>> 1); -} - -private int subsetSum(int[] nums, int targetSum) { - Arrays.sort(nums); - int[] dp = new int[targetSum + 1]; - dp[0] = 1; - for (int i = 0; i < nums.length; i++) { - int num = nums[i]; - for (int j = targetSum; j >= num; j--) { - dp[j] = dp[j] + dp[j - num]; - } - } - return dp[targetSum]; -} -``` - -**01字符构成最多的字符串** - -[Leetcode : 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: This 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 l = strs.length; - int[][] dp = new int[m + 1][n + 1]; - for (int i = 0; i < l; i++) { - String s = strs[i]; - int ones = 0, zeros = 0; - for (char c : s.toCharArray()) { - if (c == '0') zeros++; - else if (c == '1') ones++; - } - for (int j = m; j >= zeros; j--) { - for (int k = n; k >= ones; k--) { - if (zeros <= j && ones <= k) { - dp[j][k] = Math.max(dp[j][k], dp[j - zeros][k - ones] + 1); - } - } - } - } - return dp[m][n]; -} -``` - -**找零钱** - -[Leetcode : 322. Coin Change (Medium)](https://leetcode.com/problems/coin-change/description/) - -题目描述:给一些面额的硬币,要求用这些硬币来组成给定面额的钱数,并且使得硬币数量最少。硬币可以重复使用。 - -这是一个完全背包问题,完全背包问题和 0-1背包问题在实现上唯一的不同是,第二层循环是从 0 开始的,而不是从尾部开始。 - -```java -public int coinChange(int[] coins, int amount) { - int[] dp = new int[amount + 1]; - Arrays.fill(dp, amount + 1); - dp[0] = 0; - for (int i = 1; i <= amount; i++) { - for (int j = 0; j < coins.length; j++) { - if (coins[j] <= i) { - dp[i] = Math.min(dp[i], dp[i - coins[j]] + 1); - } - } - } - return dp[amount] > amount ? -1 : dp[amount]; -} -``` - -**组合总和** - -[Leetcode : 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) { - int[] dp = new int[target + 1]; - dp[0] = 1; - for (int i = 1; i <= target; i++) { - for (int j = 0; j < nums.length; j++) { - if(nums[j] <= i) { - dp[i] += dp[i - nums[j]]; - } - } - } - return dp[target]; -} -``` - -**只能进行两次的股票交易** - -[Leetcode : 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 次的股票交易** - -[Leetcode : 188. Best Time to Buy and Sell Stock IV (Hard)](https://leetcode.com/problems/best-time-to-buy-and-sell-stock-iv/description/) - -```html -dp[i, j] = max(dp[i, j-1], prices[j] - prices[jj] + dp[i-1, jj]) { jj in range of [0, j-1] } = max(dp[i, j-1], prices[j] + max(dp[i-1, jj] - prices[jj])) -``` - -```java -public int maxProfit(int k, int[] prices) { - int n = prices.length; - if (k >= n/2) { - int maxPro = 0; - for (int i = 1; i < n; i++) { - if (prices[i] > prices[i-1]) - maxPro += prices[i] - prices[i-1]; - } - return maxPro; - } - int[][] dp = new int[k + 1][n]; - for (int i = 1; i <= k; i++) { - int localMax = dp[i - 1][0] - prices[0]; - for (int j = 1; j < n; j++) { - dp[i][j] = Math.max(dp[i][j - 1], prices[j] + localMax); - localMax = Math.max(localMax, dp[i - 1][j] - prices[j]); - } - } - return dp[k][n - 1]; -} -``` - -### 数组区间 - -**数组区间和** - -[Leetcode : 303. Range Sum Query - Immutable (Easy)](https://leetcode.com/problems/range-sum-query-immutable/description/) - -求区间 i \~ j 的和,可以转换为 sum[j] - sum[i-1],其中 sum[i] 为 0 \~ j 的和。 - -```java -class NumArray { - - int[] nums; - - public NumArray(int[] nums) { - for(int i = 1; i < nums.length; i++) - nums[i] += nums[i - 1]; - this.nums = nums; - } - - public int sumRange(int i, int j) { - return i == 0 ? nums[j] : nums[j] - nums[i - 1]; - } -} -``` - -**子数组最大的和** - -[Leetcode : 53. Maximum Subarray (Easy)](https://leetcode.com/problems/maximum-subarray/description/) - -令 sum[i] 为以 num[i] 为结尾的子数组最大的和,可以由 sum[i-1] 得到 sum[i] 的值,如果 sum[i-1] 小于 0,那么以 num[i] 为结尾的子数组不能包含前面的内容,因为加上前面的部分,那么和一定会比 num[i] 还小。 - -```java -public int maxSubArray(int[] nums) { - int n = nums.length; - int[] sum = new int[n]; - sum[0] = nums[0]; - int max = sum[0]; - for(int i = 1; i < n; i++){ - sum[i] = (sum[i-1] > 0 ? sum[i-1] : 0) + nums[i]; - max = Math.max(max, sum[i]); - } - return max; -} -``` - -**数组中等差递增子区间的个数** - -[Leetcode : 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. -``` - -对于 (1,2,3,4),它有三种组成递增子区间的方式,而对于 (1,2,3,4,5),它组成递增子区间的方式除了 (1,2,3,4) 的三种外还多了一种,即 (1,2,3,4,5),因此 dp[i] = dp[i - 1] + 1。 - -```java -public int numberOfArithmeticSlices(int[] A) { - 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 ret = 0; - for(int cnt : dp) { - ret += cnt; - } - return ret; -} -``` - -### 字符串编辑 - -**删除两个字符串的字符使它们相等** - -[Leetcode : 583. Delete Operation for Two Strings (Medium)](https://leetcode.com/problems/delete-operation-for-two-strings/description/) - -可以转换为求两个字符串的最长公共子序列问题。 - -```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 = 0; i <= m; i++) { - for (int j = 0; j <= n; j++) { - if (i == 0 || j == 0) continue; - dp[i][j] = word1.charAt(i - 1) == word2.charAt(j - 1) ? dp[i - 1][j - 1] + 1 - : Math.max(dp[i][j - 1], dp[i - 1][j]); - } - } - return m + n - 2 * dp[m][n]; -} -``` - - -**修改一个字符串称为另一个字符串** // TODO - -[Leetcode : 72. Edit Distance (Hard)](https://leetcode.com/problems/edit-distance/description/) - - -### 其它问题 - -**需要冷却期的股票交易** - -[Leetcode : 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/) - -题目描述:交易之后需要有一天的冷却时间。 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//ac9b31ec-cef1-4880-a875-fc4571ca10e1.png) - -```html -s0[i] = max(s0[i - 1], s2[i - 1]); // Stay at s0, or rest from s2 -s1[i] = max(s1[i - 1], s0[i - 1] - prices[i]); // Stay at s1, or buy from s0 -s2[i] = s1[i - 1] + prices[i]; // Only one way from s1 -``` - - -```java -public int maxProfit(int[] prices) { - if (prices == null || prices.length == 0) return 0; - int n = prices.length; - int[] s0 = new int[n]; - int[] s1 = new int[n]; - int[] s2 = new int[n]; - s0[0] = 0; - s1[0] = -prices[0]; - s2[0] = Integer.MIN_VALUE; - for (int i = 1; i < n; i++) { - s0[i] = Math.max(s0[i - 1], s2[i - 1]); - s1[i] = Math.max(s1[i - 1], s0[i - 1] - prices[i]); - s2[i] = Math.max(s2[i - 1], s1[i - 1] + prices[i]); - } - return Math.max(s0[n - 1], s2[n - 1]); -} -``` - - -**统计从 0 \~ n 每个数的二进制表示中 1 的个数** - -[Leetcode : 338. Counting Bits (Medium)](https://leetcode.com/problems/counting-bits/description/) - -对于数字 6(110),它可以看成是数字 2(10) 前面加上一个 1 ,因此 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 : 646. Maximum Length of Pair Chain (Medium)](https://leetcode.com/problems/maximum-length-of-pair-chain/description/) - -对于 (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 = 0; i < n; i++) { - for(int j = 0; j < i; j++) { - if(pairs[i][0] > pairs[j][1]){ - dp[i] = Math.max(dp[i], dp[j] + 1); - } - } - } - - int ret = 0; - for(int num : dp) { - ret = Math.max(ret, num); - } - return ret; -} -``` - -**买入和售出股票最大的收益** - -[Leetcode : 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; -} -``` - -**复制粘贴字符** - -[Leetcode : 650. 2 Keys Keyboard (Medium)](https://leetcode.com/problems/2-keys-keyboard/description/) - -```java -public int minSteps(int n) { - int[] dp = new int[n + 1]; - for (int i = 2; i <= n; i++) { - dp[i] = i; - for (int j = i - 1; j >= 0; j--) { - if (i % j == 0) { - dp[i] = dp[j] + dp[i / j]; - break; - } - } - } - return dp[n]; -} -``` - -```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; -} -``` - -## 数学 - -### 素数 - -**素数分解** - -每一个数都可以分解成素数的乘积,例如 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) \* ... - -**生成素数序列** - -[Leetcode : 204. Count Primes (Easy)](https://leetcode.com/problems/count-primes/description/) - -埃拉托斯特尼筛法在每次找到一个素数时,将能被素数整除的数排除掉。 - -```java -public int countPrimes(int n) { - boolean[] notPrimes = new boolean[n + 1]; - int cnt = 0; - for(int i = 2; i < n; i++){ - if(notPrimes[i]) continue; - cnt++; - // 从 i * i 开始,因为如果 k < i,那么 k * i 在之前就已经被去除过了 - for(long j = (long) i * i; j < n; j += i){ - notPrimes[(int) j] = true; - } - } - return cnt; -} -``` - -### 最大公约数 - -```java -int gcd(int a, int b) { - if (b == 0) return a; - return gcd(b, a % b); -} -``` - -最大公倍数为两数的乘积除以最大公约数。 - -```java -int lcm(int a, int b){ - return a * b / gcd(a, b); -} -``` - -对于最大公约数问题,因为需要计算 a % b ,而这个操作是比较耗时的,可以使用 [ 编程之美:2.7]() 的方法,利用减法和移位操作来替换它。 - -对于 a 和 b 的最大公约数 f(a, b),有: - -1\. 如果 a 和 b 均为偶数,f(a, b) = 2\*f(a/2, b/2); -2\. 如果 a 是偶数 b 是奇数,f(a, b) = f(a/2, b); -3\. 如果 b 是偶数 a 是奇数,f(a, b) = f(a, b/2); -4\. 如果 a 和 b 均为奇数,f(a, b) = f(a, a-b); - -乘 2 和除 2 都可以转换为移位操作。 - -### 进制转换 - -Java 中 static String toString(int num, int radix) 可以将一个整数装换为 redix 进制表示的字符串。 - -**7 进制** - -[Leetcode : 504. Base 7 (Easy)](https://leetcode.com/problems/base-7/description/) - -```java -public String convertToBase7(int num) { - if (num < 0) { - return '-' + convertToBase7(-num); - } - if (num < 7) { - return num + ""; - } - return convertToBase7(num / 7) + num % 7; -} -``` - -**16 进制** - -[Leetcode : 405. Convert a Number to Hexadecimal (Easy)](https://leetcode.com/problems/convert-a-number-to-hexadecimal/description/) - -```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"; - String ret = ""; - while(num != 0){ - ret = map[(num & 0b1111)] + ret; - num >>>= 4; - } - return ret; -} -``` - -### 阶乘 - -**统计阶乘尾部有多少个 0** - -[Leetcode : 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 + ... - -### 字符串加法减法 - -**二进制加法** - -[Leetcode : 67. Add Binary (Easy)](https://leetcode.com/problems/add-binary/description/) - -```java -public String addBinary(String a, String b) { - int i = a.length() - 1, j = b.length() - 1, carry = 0; - String str = ""; - while(i >= 0 || j >= 0){ - if(i >= 0 && a.charAt(i--) == '1') carry++; - if(j >= 0 && b.charAt(j--) == '1') carry++; - str = (carry % 2) + str; - carry /= 2; - } - if(carry == 1) str = "1" + str; - return str; -} -``` - -**字符串加法** - -[Leetcode : 415. Add Strings (Easy)](https://leetcode.com/problems/add-strings/description/) - -题目描述:字符串的值为非负整数 - -```java -public String addStrings(String num1, String num2) { - StringBuilder sb = new StringBuilder(); - int carry = 0; - for(int i = num1.length() - 1, j = num2.length() - 1; i >= 0 || j >= 0 || carry == 1; i--, j--){ - int x = i < 0 ? 0 : num1.charAt(i) - '0'; - int y = j < 0 ? 0 : num2.charAt(j) - '0'; - sb.append((x + y + carry) % 10); - carry = (x + y + carry) / 10; - } - return sb.reverse().toString(); -} -``` - -### 相遇问题 - -**改变数组元素使所有的数组元素都相等** - -[Leetcode : 462. Minimum Moves to Equal Array Elements II (Medium)](https://leetcode.com/problems/minimum-moves-to-equal-array-elements-ii/description/) - -题目描述:每次可以对一个数组元素加一或者减一,求最小的改变次数。 - -这是个典型的相遇问题,移动距离最小的方式是所有元素都移动到中位数。理由如下: - -设 m 为中位数。a 和 b 是 m 两边的两个元素,且 b > a。要使 a 和 b 相等,它们总共移动的次数为 b - a,这个值等于 (b - m) + (m - a),也就是把这两个数移动到中位数的移动次数。 - -设数组长度为 N,则可以找到 N/2 对 a 和 b 的组合,使它们都移动到 m 的位置。 - -**解法 1** - -先排序,时间复杂度:O(NlgN) - -```java -public int minMoves2(int[] nums) { - Arrays.sort(nums); - int ret = 0; - int l = 0, h = nums.length - 1; - while(l <= h) { - ret += nums[h] - nums[l]; - l++; - h--; - } - return ret; -} -``` - -**解法 2** - -使用快速排序找到中位数,时间复杂度 O(N) - -```java -public int minMoves2(int[] nums) { - int ret = 0; - int n = nums.length; - int median = quickSelect(nums, 0, n - 1, n / 2 + 1); - for(int num : nums) ret += Math.abs(num - median); - return ret; -} - -private int quickSelect(int[] nums, int start, int end, int k) { - int l = start, r = end, privot = nums[(l + r) / 2]; - while(l <= r) { - while(nums[l] < privot) l++; - while(nums[r] > privot) r--; - if(l >= r) break; - swap(nums, l, r); - l++; r--; - } - int left = l - start + 1; - if(left > k) return quickSelect(nums, start, l - 1, k); - if(left == k && l == r) return nums[l]; - int right = r - start + 1; - return quickSelect(nums, r + 1, end, k - right); -} - -private void swap(int[] nums, int i, int j) { - int tmp = nums[i]; nums[i] = nums[j]; nums[j] = tmp; -} -``` - -### 多数投票问题 - -**数组中出现次数多于 n / 2 的元素** - -[Leetcode : 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 = 0; - for(int i = 0; i < nums.length; i++){ - if(cnt == 0) { - majority = nums[i]; - cnt++; - } - else if(majority == nums[i]) cnt++; - else cnt--; - } - return majority; -} -``` - -### 其它 - -**平方数** - -[Leetcode : 367. Valid Perfect Square (Easy)](https://leetcode.com/problems/valid-perfect-square/description/) - -平方序列: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 次方** - -[Leetcode : 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); -} -``` - -**找出数组中的乘积最大的三个数** - -[Leetcode : 628. Maximum Product of Three Numbers (Easy)](https://leetcode.com/problems/maximum-product-of-three-numbers/description/) - -```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); -} -``` - -**乘积数组** - -[Leetcode : 238. Product of Array Except Self (Medium)](https://leetcode.com/problems/product-of-array-except-self/description/) - -题目描述:给定一个数组,创建一个新数组,新数组的每个元素为原始数组中除了该位置上的元素之外所有元素的乘积。 - -题目要求:时间复杂度为 O(n),并且不能使用除法。 - -```java -public int[] productExceptSelf(int[] nums) { - int n = nums.length; - int[] ret = new int[n]; - ret[0] = 1; - for(int i = 1; i < n; i++) { - ret[i] = ret[i - 1] * nums[i - 1]; - } - int right = 1; - for(int i = n - 1; i >= 0; i--) { - ret[i] *= right; - right *= nums[i]; - } - return ret; -} -``` - -# 数据结构相关 - -## 栈和队列 - -**用栈实现队列** - -一个栈实现: - -```java -class MyQueue { - private Stack st = new Stack(); - - public void push(int x) { - Stack temp = new Stack(); - while(!st.isEmpty()){ - temp.push(st.pop()); - } - st.push(x); - while(!temp.isEmpty()){ - st.push(temp.pop()); - } - } - - public int pop() { - return st.pop(); - } - - public int peek() { - return st.peek(); - } - - public boolean empty() { - return st.isEmpty(); - } -} -``` - -两个栈实现: - -```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(); - } -} -``` - -**用队列实现栈** - -[Leetcode : 225. Implement Stack using Queues (Easy)](https://leetcode.com/problems/implement-stack-using-queues/description/) - -```java -class MyStack { - - private Queue queue; - - public MyStack() { - queue = new LinkedList<>(); - } - - public void push(int x) { - queue.add(x); - for(int i = 1; i < queue.size(); i++){ // 翻转 - queue.add(queue.remove()); - } - } - - public int pop() { - return queue.remove(); - } - - public int top() { - return queue.peek(); - } - - public boolean empty() { - return queue.isEmpty(); - } -} -``` - -**最小值栈** - -[Leetcode : 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); - if(x < min) { - min = x; - } - minStack.add(min); - } - - public void pop() { - dataStack.pop(); - minStack.pop(); - if(!minStack.isEmpty()) { - min = minStack.peek(); - } else{ - min = Integer.MAX_VALUE; - } - } - - public int top() { - return dataStack.peek(); - } - - public int getMin() { - return min; - } -} -``` - -对于实现最小值队列问题,可以先将队列使用栈来实现,然后就将问题转换为最小值栈,这个问题出现在 编程之美:3.7。 - -**用栈实现括号匹配** - -[Leetcode : 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(int i = 0; i < s.length(); i++){ - char c = s.charAt(i); - if(c == '(' || c == '{' || c == '[') stack.push(c); - else{ - if(stack.isEmpty()) return false; - char cStack = stack.pop(); - if(c == ')' && cStack != '(' || - c == ']' && cStack != '[' || - c == '}' && cStack != '{' ) { - return false; - } - } - } - return stack.isEmpty(); -} -``` - -**数组中比当前元素大的下一个数组元素的距离** - -```html -Input: [73, 74, 75, 71, 69, 72, 76, 73] -Output: [1, 1, 4, 2, 1, 1, 0, 0] -``` - -[Leetcode : 739. Daily Temperatures (Medium)](https://leetcode.com/problems/daily-temperatures/description/) - -使用栈来存储还未计算的元素。可以保证从栈顶向下元素递增,否则上面有一个比下面某个元素大的元素进入栈中,下面那个元素已经找到比它大的元素,因此会出栈。 - -```java -public int[] dailyTemperatures(int[] temperatures) { - int n = temperatures.length; - int[] ret = new int[n]; - Stack stack = new Stack<>(); - for(int i = 0; i < n; i++) { - while(!stack.isEmpty() && temperatures[i] > temperatures[stack.peek()]) { - int idx = stack.pop(); - ret[idx] = i - idx; - } - stack.add(i); - } - return ret; -} -``` - -**数组中下一个比当前数大的数** - -[Leetcode : 496. Next Greater Element I (Easy)](https://leetcode.com/problems/next-greater-element-i/description/) - -```html -Input: nums1 = [4,1,2], nums2 = [1,3,4,2]. -Output: [-1,3,-1] -``` - -在遍历数组时用 Stack 把数组中的数存起来,如果当前遍历的数比栈顶元素来的大,说明栈顶元素的下一个比它大的数就是当前元素。 - -```java -public int[] nextGreaterElement(int[] nums1, int[] nums2) { - Map map = new HashMap<>(); - Stack stack = new Stack<>(); - for(int num : nums2){ - while(!stack.isEmpty() && num > stack.peek()){ - map.put(stack.pop(), num); - } - stack.add(num); - } - int[] ret = new int[nums1.length]; - for(int i = 0; i < nums1.length; i++){ - if(map.containsKey(nums1[i])) ret[i] = map.get(nums1[i]); - else ret[i] = -1; - } - return ret; -} -``` - -**循环数组中下一个比当前元素大的数** - -[Leetcode : 503. Next Greater Element II (Medium)](https://leetcode.com/problems/next-greater-element-ii/description/) - -```java -public int[] nextGreaterElements(int[] nums) { - int n = nums.length, next[] = new int[n]; - Arrays.fill(next, -1); - Stack stack = new Stack<>(); - for (int i = 0; i < n * 2; i++) { - int num = nums[i % n]; - while (!stack.isEmpty() && nums[stack.peek()] < num) - next[stack.pop()] = num; - if (i < n) stack.push(i); - } - return next; -} -``` - - -## 哈希表 - -利用 Hash Table 可以快速查找一个元素是否存在等问题,但是需要一定的空间来存储。在优先考虑时间复杂度的情况下,可以利用 Hash Table 这种空间换取时间的做法。 - -Java 中的 **HashSet** 用于存储一个集合,并以 O(1) 的时间复杂度查找元素是否在集合中。 - -如果元素有穷,并且范围不大,那么可以用一个布尔数组来存储一个元素是否存在,例如对于只有小写字符的元素,就可以用一个长度为 26 的布尔数组来存储一个字符集合,使得空间复杂度降低为 O(1)。 - -Java 中的 **HashMap** 主要用于映射关系,从而把两个元素联系起来。 - -在对一个内容进行压缩或者其它转换时,利用 HashMap 可以把原始内容和转换后的内容联系起来。例如在一个简化 url 的系统中([Leetcdoe : 535. Encode and Decode TinyURL (Medium)](https://leetcode.com/problems/encode-and-decode-tinyurl/description/)),利用 HashMap 就可以存储精简后的 url 到原始 url 的映射,使得不仅可以显示简化的 url,也可以根据简化的 url 得到原始 url 从而定位到正确的资源。 - -HashMap 也可以用来对元素进行计数统计,此时键为元素,值为计数。和 HashSet 类似,如果元素有穷并且范围不大,可以用整型数组来进行统计。 - - -**数组中的两个数和为给定值** - -[Leetcode : 1. Two Sum (Easy)](https://leetcode.com/problems/two-sum/description/) - -可以先对数组进行排序,然后使用双指针方法或者二分查找方法。这样做的时间复杂度为 O(nlgn),空间复杂度为 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 map = new HashMap<>(); - for(int i = 0; i < nums.length; i++){ - if(map.containsKey(target - nums[i])) return new int[]{map.get(target - nums[i]), i}; - else map.put(nums[i], i); - } - return null; -} -``` - -**最长和谐序列** - -和谐序列中最大数和最小数只差正好为 1 - -[Leetcode : 594. Longest Harmonious Subsequence (Easy)](https://leetcode.com/problems/longest-harmonious-subsequence/description/) - -```java -public int findLHS(int[] nums) { - Map map = new HashMap<>(); - for (long num : nums) { - map.put(num, map.getOrDefault(num, 0) + 1); - } - int result = 0; - for (long key : map.keySet()) { - if (map.containsKey(key + 1)) { - result = Math.max(result, map.get(key + 1) + map.get(key)); - } - } - return result; -} -``` - -## 字符串 - -**两个字符串的包含的字符是否完全相同** - -[Leetcode : 242. Valid Anagram (Easy)](https://leetcode.com/problems/valid-anagram/description/) - -字符串只包含小写字符,总共有 26 个小写字符。可以用 Hash Table 来映射字符与出现次数,因为键值范围很小,因此可以用数组来进行映射。 - -使用长度为 26 的整型数组对字符串出现的字符进行统计,比较两个字符串出现的字符数量是否相同。 - -```java -public boolean isAnagram(String s, String t) { - int[] cnts = new int[26]; - for(int i = 0; i < s.length(); i++) cnts[s.charAt(i) - 'a'] ++; - for(int i = 0; i < t.length(); i++) cnts[t.charAt(i) - 'a'] --; - for(int i = 0; i < 26; i++) if(cnts[i] != 0) return false; - return true; -} -``` - -**字符串同构** - -[Leetcode : 205. Isomorphic Strings (Easy)](https://leetcode.com/problems/isomorphic-strings/description/) - -例如 "egg" 和 "add" 就属于同构字符串。 - -记录一个字符上次出现的位置,如果两个字符串中某个字符上次出现的位置一样,那么就属于同构。 - -```java -public boolean isIsomorphic(String s, String t) { - int[] m1 = new int[256]; - int[] m2 = new int[256]; - for(int i = 0; i < s.length(); i++){ - if(m1[s.charAt(i)] != m2[t.charAt(i)]) { - return false; - } - m1[s.charAt(i)] = i + 1; - m2[t.charAt(i)] = i + 1; - } - return true; -} -``` - -**计算一组字符集合可以组成的回文字符串的最大长度** - -[Leetcode : 409. Longest Palindrome](https://leetcode.com/problems/longest-palindrome/description/) - -使用长度为 128 的整型数组来统计每个字符出现的个数,每个字符有偶数个可以用来构成回文字符串。因为回文字符串最中间的那个字符可以单独出现,所以如果有单独的字符就把它放到最中间。 - -```java -public int longestPalindrome(String s) { - int[] cnts = new int[128]; // ascii 码总共 128 个 - for(char c : s.toCharArray()) cnts[c]++; - int ret = 0; - for(int cnt : cnts) ret += (cnt / 2) * 2; - if(ret < s.length()) ret ++; // 这个条件下 s 中一定有单个未使用的字符存在,可以把这个字符放到回文的最中间 - return ret; -} -``` - -**判断一个整数是否是回文数** - -[Leetcode : 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) return false; - if(x % 10 == 0) return false; - int right = 0; - while(x > right){ - right = right * 10 + x % 10; - x /= 10; - } - return x == right || x == right / 10; -} -``` - -**回文子字符串** - -[Leetcode : 647. Palindromic Substrings (Medium)](https://leetcode.com/problems/palindromic-substrings/description/) - -解决方案是从字符串的某一位开始,尝试着去扩展子字符串。 - -```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++; - } -} -``` - -**统计二进制字符串中连续 1 和 连续 0 数量相同的子字符串个数** - -```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". -``` - -[Leetcode : 696. Count Binary Substrings (Easy)](https://leetcode.com/problems/count-binary-substrings/description/) - -```java -public int countBinarySubstrings(String s) { - int preLen = 0, curLen = 1, ret = 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) ret++; - } - return ret; -} -``` - -**字符串循环移位包含** - -[ 编程之美:3.1](#) - -给定两个字符串 s1 和 s2 ,要求判定 s2 是否能够被 s1 做循环移位得到的字符串包含。 - -```html -s1 = AABCD, s2 = CDAA -Return : true -``` - -s1 进行循环移位的结果是 s1s1 的子字符串,因此只要判断 s2 是否是 s1s1 的子字符串即可。 - -**字符串循环移位** - -[ 编程之美:2.17](#) - -将字符串向右循环移动 k 位。 - -例如 abcd123 向右移动 3 位 得到 123abcd - -将 abcd123 中的 abcd 和 123 单独逆序,得到 dcba321,然后对整个字符串进行逆序,得到123abcd。 - -**字符串中单词的翻转** - -[程序员代码面试指南](#) - -例如将 "I am a student" 翻转成 "student a am I" - -将每个单词逆序,然后将整个字符串逆序。 - -## 数组与矩阵 - -**把数组中的 0 移到末尾** - -[Leetcode : 283. Move Zeroes (Easy)](https://leetcode.com/problems/move-zeroes/description/) - -```java - public void moveZeroes(int[] nums) { - int n = nums.length; - int idx = 0; - for(int i = 0; i < n; i++){ - if(nums[i] != 0) nums[idx++] = nums[i]; - } - while(idx < n){ - nums[idx++] = 0; - } - } -``` - -**一个数组元素在 [1, n] 之间,其中一个数被替换为另一个数,找出丢失的数和重复的数** - -[Leetcode : 645. Set Mismatch (Easy)](https://leetcode.com/problems/set-mismatch/description/) - -最直接的方法是先对数组进行排序,这种方法时间复杂度为 O(nlogn),本题可以以 O(n) 的时间复杂度、O(1) 空间复杂度来求解。 - -主要思想是让通过交换数组元素,使得数组上的元素在正确的位置上。 - -遍历数组,如果第 i 位上的元素不是 i + 1 ,那么就交换第 i 位 和 nums[i] - 1 位上的元素,使得 num[i] - 1 的元素为 nums[i] ,也就是该位的元素是正确的。交换操作需要循环进行,因为一次交换没办法使得第 i 位上的元素是正确的。但是要交换的两个元素可能就是重复元素,那么循环就可能永远进行下去,终止循环的方法是加上 nums[i] != nums[nums[i] - 1 条件。 - -类似题目: - -- [Leetcode :448. Find All Numbers Disappeared in an Array (Easy)](https://leetcode.com/problems/find-all-numbers-disappeared-in-an-array/description/),寻找所有丢失的元素 -- [Leetcode : 442. Find All Duplicates in an Array (Medium)](https://leetcode.com/problems/find-all-duplicates-in-an-array/description/),寻找所有重复的元素。 - -```java -public int[] findErrorNums(int[] nums) { - for(int i = 0; i < nums.length; i++){ - while(nums[i] != i + 1 && nums[i] != nums[nums[i] - 1]) swap(nums, i, nums[i] - 1); - } - - for(int i = 0; i < nums.length; i++){ - if(i + 1 != nums[i]) 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; -} -``` - -**找出数组中重复的数,数组值在 [0, n-1] 之间** - -[Leetcode : 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; -} -``` - -### 有序矩阵 - -有序矩阵指的是行和列分别有序的矩阵。 - -一般可以利用有序性使用二分查找方法。 - -```html -[ - [ 1, 5, 9], - [10, 11, 13], - [12, 13, 15] -] -``` - -**有序矩阵查找** - -[Leetocde : 240. Search a 2D Matrix II (Medium)](https://leetcode.com/problems/search-a-2d-matrix-ii/description/) - -```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** - -[Leetcode : 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; - } -} -``` - -## 链表 - -**判断两个链表的交点** - -[Leetcode : 160. Intersection of Two Linked Lists](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) { - if(headA == null || headB == null) return null; - ListNode l1 = headA, l2 = headB; - while(l1 != l2){ - l1 = (l1 == null) ? headB : l1.next; - l2 = (l2 == null) ? headA : l2.next; - } - return l1; -} -``` - -如果只是判断是否存在交点,那么就是另一个问题,即 编程之美:3.6 的问题。有两种解法:把第一个链表的结尾连接到第二个链表的开头,看第二个链表是否存在环;或者直接比较第一个链表最后一个节点和第二个链表最后一个节点是否相同。 - - - -**链表反转** - -[Leetcode : 206. Reverse Linked List](https://leetcode.com/problems/reverse-linked-list/description/) - -头插法能够按逆序构建链表。 - -```java -public ListNode reverseList(ListNode head) { - ListNode newHead = null; // 设为 null ,作为新链表的结尾 - while(head != null){ - ListNode nextNode = head.next; - head.next = newHead; - newHead = head; - head = nextNode; - } - return newHead; -} -``` - -**归并两个有序的链表** - -[Leetcode : 21. Merge Two Sorted Lists](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; - ListNode newHead = null; - if(l1.val < l2.val){ - newHead = l1; - newHead.next = mergeTwoLists(l1.next, l2); - } else{ - newHead = l2; - newHead.next = mergeTwoLists(l1, l2.next); - } - return newHead; -} -``` - -**从有序链表中删除重复节点** - -[Leetcode : 83. Remove Duplicates from Sorted List (Easy)](https://leetcode.com/problems/remove-duplicates-from-sorted-list/description/) - -```java -public ListNode deleteDuplicates(ListNode head) { - if(head == null || head.next == null) return head; - head.next = deleteDuplicates(head.next); - return head.next != null && head.val == head.next.val ? head.next : head; -} -``` - -**回文链表** - -[Leetcode : 234. Palindrome Linked List (Easy)](https://leetcode.com/problems/palindrome-linked-list/description/) - -切成两半,把后半段反转,然后比较两半是否相等。 - -```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 = slow.next; - } - - cut(head, slow); // 切成两个链表 - ListNode l1 = head, l2 = slow; - l2 = reverse(l2); - return isEqual(l1, l2); -} - -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; -} -``` - -**从链表中删除节点** - -[编程之美:3.4]() - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//2c968ec5-0967-49ce-ac06-f8f5c9ab33bc.jpg) - -```java -B.val = C.val; -B.next = C.next; -``` - -**链表元素按奇偶聚集** - -[Leetcode : 328. Odd Even Linked List (Medium)](https://leetcode.com/problems/odd-even-linked-list/description/) - -```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; -} -``` - -## 树 - -### 递归 - -一棵树要么是空树,要么有两个指针,每个指针指向一棵树。树是一种递归结构,很多树的问题可以使用递归来处理。 - -**树的高度** - -[Leetcode : 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; -} -``` - -**翻转树** - -[Leetcode : 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; -} -``` - -**归并两棵树** - -[Leetcode : 617. Merge Two Binary Trees (Easy)](https://leetcode.com/problems/merge-two-binary-trees/description/) - -```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/) - -题目描述:路径和定义为从 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); -} -``` - -**统计路径和等于一个数的路径数量** - -[Leetcode : 437. Path Sum III (Easy)](https://leetcode.com/problems/path-sum-iii/description/) - -题目描述:路径不一定以 root 开头并以 leaf 结尾,但是必须连续 - -pathSumStartWithRoot() 方法统计以某个节点开头的路径个数。 - -```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; -} -``` - -**树的对称** - -[Leetcode : 101. Symmetric Tree (Easy)](https://leetcode.com/problems/symmetric-tree/description/) - -```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); -} -``` - -**平衡树** - -[Leetcode : 110. Balanced Binary Tree (Easy)](https://leetcode.com/problems/balanced-binary-tree/description/) - -题目描述:左右子树高度差是否都小于等于 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); -} -``` - -**最小路径** - -[Leetcode : 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; -} -``` - -**统计左叶子节点的和** - -[Leetcode : 404. Sum of Left Leaves (Easy)](https://leetcode.com/problems/sum-of-left-leaves/description/) - -```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; -} -``` - -**修剪一棵树** - -[Leetcode : 669. Trim a Binary Search Tree (Easy)](https://leetcode.com/problems/trim-a-binary-search-tree/description/) - -题目描述:只保留值在 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; -} -``` - -**子树** - -[Leetcode : 572. Subtree of Another Tree (Easy)](https://leetcode.com/problems/subtree-of-another-tree/description/) - -```java -public boolean isSubtree(TreeNode s, TreeNode t) { - if(s == null && t == null) return true; - if(s == null || t == null) return false; - if(s.val == t.val && isSame(s, t)) return true; - return isSubtree(s.left, t) || isSubtree(s.right, t); -} - -private boolean isSame(TreeNode s, TreeNode t){ - if(s == null && t == null) return true; - if(s == null || t == null) return false; - if(s.val != t.val) return false; - return isSame(s.left, t.left) && isSame(s.right, t.right); -} -``` - -**从有序数组中构造二叉查找树** - -[Leetcode : 108. Convert Sorted Array to Binary Search Tree (Easy)](https://leetcode.com/problems/convert-sorted-array-to-binary-search-tree/description/) - -二叉查找树(BST):根节点大于等于左子树所有节点,小于等于右子树所有节点。 - -```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; -} -``` - -**两节点的最长路径** - -```html - 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; -} -``` - -**找出二叉树中第二小的节点** - -[Leetcode : 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; -} -``` - -**寻找两个节点的最近公共祖先** - -[Leetcode : 235. Lowest Common Ancestor of a Binary Search Tree (Easy)](https://leetcode.com/problems/lowest-common-ancestor-of-a-binary-search-tree/description/) - -```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; -} -``` - -**最近公共祖先** - -[Leetcode : 236. Lowest Common Ancestor of a Binary Tree (Medium) ](https://leetcode.com/problems/lowest-common-ancestor-of-a-binary-tree/description/) - -```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; -} -``` - -**最大相同节点值的路径长度** - -[Leetcode : 687. Longest Univalue Path (Easy)](https://pomotodo.com/app/) - -```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); -} -``` - -**间隔遍历** - -[Leetcode : 337. House Robber III (Medium)](https://leetcode.com/problems/house-robber-iii/description/) - -```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); -} -``` - -### 层次遍历 - -使用 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; -} -``` - -**得到左下角的节点** - -[Leetcode : 513. Find Bottom Left Tree Value (Easy)](https://leetcode.com/problems/find-bottom-left-tree-value/description/) - -```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); -} -``` - -**非递归实现二叉树的前序遍历** - -[Leetcode : 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<>(); - if (root == null) return ret; - Stack stack = new Stack<>(); - stack.push(root); - while (!stack.isEmpty()) { - TreeNode node = stack.pop(); - ret.add(node.val); - if (node.right != null) stack.push(node.right); - if (node.left != null) stack.push(node.left); // 先添加右子树再添加左子树,这样是为了让左子树在栈顶 - } - return ret; -} -``` - -**非递归实现二叉树的后续遍历** - -[Leetcode : ### 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<>(); - if (root == null) return ret; - Stack stack = new Stack<>(); - stack.push(root); - while (!stack.isEmpty()) { - TreeNode node = stack.pop(); - ret.add(node.val); - if (node.left != null) stack.push(node.left); - if (node.right != null) stack.push(node.right); - } - Collections.reverse(ret); - return ret; -} -``` - -**非递归实现二叉树的中序遍历** - -[Leetcode : 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<>(); - Stack stack = new Stack<>(); - TreeNode cur = root; - while(cur != null || !stack.isEmpty()) { - while(cur != null) { // 模拟递归栈的不断深入 - stack.add(cur); - cur = cur.left; - } - TreeNode node = stack.pop(); - ret.add(node.val); - cur = node.right; - } - return ret; -} -``` - -**使用中序遍历和前序遍历序列重建二叉树** //TODO - -### BST - -主要利用 BST 中序遍历有序的特点。 - -**在 BST 中寻找两个节点,使它们的和为一个给定值。** - -[653. Two Sum IV - Input is a BST](https://leetcode.com/problems/two-sum-iv-input-is-a-bst/description/) - -使用中序遍历得到有序数组之后,再利用双指针对数组进行查找。 - -应该注意到,这一题不能用分别在左右子树两部分来处理这种思想,因为两个待求的节点可能分别在左右子树中。 - -```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); -} -``` - -**在 BST 中查找最小的两个节点之差的绝对值** - -[Leetcode : 530. Minimum Absolute Difference in BST (Easy)](https://leetcode.com/problems/minimum-absolute-difference-in-bst/description/) - -利用 BST 的中序遍历为有序的性质,计算中序遍历中临近的两个节点之差的绝对值,取最小值。 - -```java -private int minDiff = Integer.MAX_VALUE; -private int preVal = -1; - -public int getMinimumDifference(TreeNode root) { - inorder(root); - return minDiff; -} - -private void inorder(TreeNode node){ - if(node == null) return; - inorder(node.left); - if(preVal != -1) minDiff = Math.min(minDiff, Math.abs(node.val - preVal)); - preVal = node.val; - inorder(node.right); -} -``` - -**把 BST 每个节点的值都加上比它大的节点的值** - -[Leetcode : Convert BST to Greater Tree (Easy)](https://leetcode.com/problems/convert-bst-to-greater-tree/description/) - -先遍历右子树。 - -```java -private int sum = 0; - -public TreeNode convertBST(TreeNode root) { - traver(root); - return root; -} - -private void traver(TreeNode root) { - if (root == null) { - return; - } - if (root.right != null) { - traver(root.right); - } - sum += root.val; - root.val = sum; - if (root.left != null) { - traver(root.left); - } -} -``` - -**寻找 BST 中出现次数最多的节点** - -```java -private int cnt = 1; -private int maxCnt = 1; -private TreeNode preNode = null; -private List list; - -public int[] findMode(TreeNode root) { - list = new ArrayList<>(); - inorder(root); - int[] ret = new int[list.size()]; - int idx = 0; - for(int num : list){ - ret[idx++] = num; - } - return ret; -} - -private void inorder(TreeNode node){ - if(node == null) return; - inorder(node.left); - if(preNode != null){ - if(preNode.val == node.val) cnt++; - else cnt = 1; - } - if(cnt > maxCnt){ - maxCnt = cnt; - list.clear(); - list.add(node.val); - } else if(cnt == maxCnt){ - list.add(node.val); - } - preNode = node; - inorder(node.right); -} -``` - -**寻找 BST 的第 k 个元素** - -[Leetcode : 230. Kth Smallest Element in a BST (Medium)](https://leetcode.com/problems/kth-smallest-element-in-a-bst/description/) - -递归解法: - -```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); -} -``` - -中序遍历解法: - -```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); -} -``` - - -### Trie - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//5c638d59-d4ae-4ba4-ad44-80bdc30f38dd.jpg) - -Trie,又称前缀树或字典树,用于判断字符串是否存在或者是否具有某种字符串前缀。 - -**实现一个 Trie** - -[Leetcode : 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(); - - /** Initialize your data structure here. */ - public Trie() { - } - - /** Inserts a word into the trie. */ - public void insert(String word) { - int idx = word.charAt(0) - 'a'; - insert(word, root); - } - - private void insert(String word, Node node){ - int idx = word.charAt(0) - 'a'; - if(node.childs[idx] == null){ - node.childs[idx] = new Node(); - } - if(word.length() == 1) node.childs[idx].isLeaf = true; - else insert(word.substring(1), node.childs[idx]); - } - - /** Returns if the word is in the trie. */ - public boolean search(String word) { - return search(word, root); - } - - private boolean search(String word, Node node){ - if(node == null) return false; - int idx = word.charAt(0) - 'a'; - if(node.childs[idx] == null) return false; - if(word.length() == 1) return node.childs[idx].isLeaf; - return search(word.substring(1), node.childs[idx]); - } - - /** Returns if there is any word in the trie that starts with the given prefix. */ - 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 idx = prefix.charAt(0) - 'a'; - return startWith(prefix.substring(1), node.childs[idx]); - } -} -``` - -**实现一个 Trie,用来求前缀和** - -[Leetcode : 677. Map Sum Pairs (Medium)](https://leetcode.com/problems/map-sum-pairs/description/) - -```java -class MapSum { - private class Trie { - int val; - Map childs; - boolean isWord; - - Trie() { - childs = new HashMap<>(); - } - } - - private Trie root; - - public MapSum() { - root = new Trie(); - } - - public void insert(String key, int val) { - Trie cur = root; - for(char c : key.toCharArray()) { - if(!cur.childs.containsKey(c)) { - Trie next = new Trie(); - cur.childs.put(c, next); - } - cur = cur.childs.get(c); - } - cur.val = val; - cur.isWord = true; - } - - public int sum(String prefix) { - Trie cur = root; - for(char c : prefix.toCharArray()) { - if(!cur.childs.containsKey(c)) return 0; - cur = cur.childs.get(c); - } - return dfs(cur); - } - - private int dfs(Trie cur) { - int sum = 0; - if(cur.isWord) { - sum += cur.val; - } - for(Trie next : cur.childs.values()) { - sum += dfs(next); - } - return sum; - } -} -``` - -## 图 - -## 位运算 - -**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 为算术右移,相当于除以 2n; -\>\>\> n 为无符号右移,左边会补上 0。 -<< n 为算术左移,相当于乘以 2n。 - -n&(n-1) 该位运算是去除 n 的位级表示中最低的那一位。例如对于二进制表示 10110**100**,减去 1 得到 10110**011**,这两个数相与得到 10110**000**。 - -n-n&(\~n+1) 概运算是去除 n 的位级表示中最高的那一位。 - -n&(-n) 该运算得到 n 的位级表示中最低的那一位。-n 得到 n 的反码加 1,对于二进制表示 10110**100**,-n 得到 01001**100**,相与得到 00000**100** - -**2. mask 计算** - -要获取 111111111,将 0 取反即可,\~0。 - -要得到只有第 i 位为 1 的 mask,将 1 向左移动 i 位即可,1<<i 。例如 1<<5 得到只有第 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. 位操作举例** - -① 获取第 i 位 - -num & 00010000 != 0 - -```java -(num & (1 << i)) != 0; -``` - -② 将第 i 位设置为 1 - -num | 00010000 - -```java -num | (1 << i); -``` - -③ 将第 i 位清除为 0 - -num & 11101111 - -```java -num & (~(1 << i)) -``` - -④ 将最高位到第 i 位清除为 0 - -num & 00001111 - -```java -num & ((1 << i) - 1); -``` - -⑤ 将第 0 位到第 i 位清除为 0 - -num & 11110000 - -```java -num & (~((1 << (i+1)) - 1)); -``` - -⑥ 将第 i 位设置为 0 或者 1 - -先将第 i 位清零,然后将 v 左移 i 位,执行“位或”运算。 - -```java -(num & (1 << i)) | (v << i); -``` - -**4. Java 中的位操作** - -```html -static int Integer.bitCount() // 统计 1 的数量 -static int Integer.highestOneBit() // 获得最高位 -static String toBinaryString(int i) // 转换位二进制表示的字符串 -``` - -**统计两个数的二进制表示有多少位不同** - -[Leetcode : 461. Hamming Distance (Easy)](https://leetcode.com/problems/hamming-distance/) - -对两个数进行异或操作,不同的那一位结果为 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; -} -``` - -可以使用 Integer.bitcount() 来统计 1 个的个数。 - -```java -public int hammingDistance(int x, int y) { - return Integer.bitCount(x ^ y); -} -``` - -**翻转一个数的比特位** - -[Leetcode : 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; -} -``` - -**不用额外变量交换两个整数** - -[程序员代码面试指南 :P317](#) - -```java -a = a ^ b; -b = a ^ b; -a = a ^ b; -``` - -将 c = a ^ b,那么 b ^ c = b ^ b ^ a = a,a ^ c = a ^ a ^ b = b。 - -**判断一个数是不是 4 的 n 次方** - -[Leetcode : 342. Power of Four (Easy)](https://leetcode.com/problems/power-of-four/) - -该数二进制表示有且只有一个奇数位为 1 ,其余的都为 0 ,例如 16 : 10000。可以每次把 1 向左移动 2 位,就能构造出这种数字,然后比较构造出来的数与要判断的数是否相同。 - -```java -public boolean isPowerOfFour(int num) { - int i = 1; - while(i > 0){ - if(i == num) return true; - i = i << 2; - } - return false; -} -``` - -也可以用 Java 的 Integer.toString() 方法将该数转换为 4 进制形式的字符串,然后判断字符串是否以 1 开头。 - -```java -public boolean isPowerOfFour(int num) { - return Integer.toString(num, 4).matches("10*"); -} -``` - -**判断一个数是不是 2 的 n 次方** - -[Leetcode : 231. Power of Two (Easy)](https://leetcode.com/problems/power-of-two/description/) - -同样可以用 Power of Four 的方法,但是 2 的 n 次方更特殊,它的二进制表示只有一个 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; -} -``` - -**数组中唯一一个不重复的元素** - -[Leetcode : 136. Single Number (Easy)](https://leetcode.com/problems/single-number/description/) - -两个相同的数异或的结果为 0,对所有数进行异或操作,最后的结果就是单独出现的那个数。 - -类似的有:[Leetcode : 389. Find the Difference (Easy)](https://leetcode.com/problems/find-the-difference/description/),两个字符串仅有一个字符不相同,使用异或操作可以以 O(1) 的空间复杂度来求解,而不需要使用 HashSet。 - -```java -public int singleNumber(int[] nums) { - int ret = 0; - for(int n : nums) ret = ret ^ n; - return ret; -} -``` - -**数组中不重复的两个元素** - -[Leetcode : 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; -} -``` - -**判断一个数的位级表示是否不会出现连续的 0 和 1** - -[Leetcode : 693. Binary Number with Alternating Bits (Easy)](https://leetcode.com/problems/binary-number-with-alternating-bits/description/) - -对于 10101 这种位级表示的数,把它向右移动 1 位得到 1010 ,这两个数每个位都不同,因此异或得到的结果为 11111。 - -```java -public boolean hasAlternatingBits(int n) { - int a = (n ^ (n >> 1)); - return (a & (a + 1)) == 0; -} -``` - -**求一个数的补码** - -[Leetcode : 476. Number Complement (Easy)](https://leetcode.com/problems/number-complement/description/) - -不考虑二进制表示中的首 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); -} -``` - -**实现整数的加法** - -[Leetcode : 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); -} -``` - -**字符串数组最大乘积** - -[Leetcode : 318. Maximum Product of Word Lengths (Medium)](https://leetcode.com/problems/maximum-product-of-word-lengths/description/) - -题目描述:字符串数组的字符串只含有小写字符。求解字符串数组中两个字符串长度的最大乘积,要求这两个字符串不能含有相同字符。 - -解题思路:本题主要问题是判断两个字符串是否含相同字符,由于字符串只含有小写字符,总共 26 位,因此可以用一个 32 位的整数来存储每个字符是否出现过。 - -```java -public int maxProduct(String[] words) { - int n = words.length; - if (n == 0) return 0; - 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; -} -``` - -# 参考资料 - -- [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. - diff --git a/notes/Linux.md b/notes/Linux.md deleted file mode 100644 index 2981c0cd..00000000 --- a/notes/Linux.md +++ /dev/null @@ -1,1051 +0,0 @@ - -* [常用操作以及概念](#常用操作以及概念) - * [求助](#求助) - * [关机](#关机) - * [查看进程](#查看进程) - * [查看端口](#查看端口) - * [PATH](#path) - * [运行等级](#运行等级) - * [sudo](#sudo) - * [GNU](#gnu) - * [包管理工具](#包管理工具) - * [常见发行版本](#常见发行版本) -* [分区](#分区) - * [磁盘的文件名](#磁盘的文件名) - * [分区表](#分区表) - * [1. MBR](#1-mbr) - * [2. GPT](#2-gpt) - * [开机检测程序](#开机检测程序) - * [1. BIOS](#1-bios) - * [2. UEFI](#2-uefi) - * [挂载](#挂载) -* [文件权限与目录配置](#文件权限与目录配置) - * [文件权限概念](#文件权限概念) - * [文件属性以及权限的修改](#文件属性以及权限的修改) - * [1. 修改文件所属群组](#1-修改文件所属群组) - * [2. 修改文件拥有者](#2-修改文件拥有者) - * [3. 修改权限](#3-修改权限) - * [目录的权限](#目录的权限) - * [文件默认权限](#文件默认权限) - * [目录配置](#目录配置) -* [文件与目录](#文件与目录) - * [文件时间](#文件时间) - * [文件与目录的基本操作](#文件与目录的基本操作) - * [1. ls](#1-ls) - * [2. cp](#2-cp) - * [3. rm](#3-rm) - * [4. mv](#4-mv) - * [获取文件内容](#获取文件内容) - * [1. cat](#1-cat) - * [2. tac](#2-tac) - * [3. more](#3-more) - * [4. less](#4-less) - * [5. head](#5-head) - * [6. tail](#6-tail) - * [7. od](#7-od) - * [8. touch](#8-touch) - * [指令与文件搜索](#指令与文件搜索) - * [1. which](#1-which) - * [2. whereis](#2-whereis) - * [3. locate](#3-locate) - * [4. find](#4-find) - * [4.1 与时间有关的选项](#41-与时间有关的选项) - * [4.2 与文件拥有者和所属群组有关的选项](#42-与文件拥有者和所属群组有关的选项) - * [4.3 与文件权限和名称有关的选项](#43-与文件权限和名称有关的选项) -* [磁盘与文件系统](#磁盘与文件系统) - * [文件系统的组成](#文件系统的组成) - * [inode](#inode) - * [目录的 inode 与 block](#目录的-inode-与-block) - * [实体链接与符号链接](#实体链接与符号链接) - * [1. 实体链接](#1-实体链接) - * [2. 符号链接](#2-符号链接) -* [压缩与打包](#压缩与打包) - * [压缩](#压缩) - * [1. gzip](#1-gzip) - * [2. bzip2](#2-bzip2) - * [3. xz](#3-xz) - * [打包](#打包) -* [Bash](#bash) - * [Bash 特性](#bash-特性) - * [变量操作](#变量操作) - * [指令搜索顺序](#指令搜索顺序) - * [数据流重定向](#数据流重定向) - * [管线指令](#管线指令) - * [1. 提取指令:cut](#1-提取指令cut) - * [2. 排序命令:sort、uniq](#2-排序命令sortuniq) - * [3. 双向输出重定向:tee](#3-双向输出重定向tee) - * [4. 字符转换指令:tr、col、expand、join、paste](#4-字符转换指令trcolexpandjoinpaste) - * [5. 分区指令:split](#5-分区指令split) -* [正规表示法与文件格式化处理](#正规表示法与文件格式化处理) - * [grep](#grep) - * [printf](#printf) - * [awk](#awk) -* [vim 三个模式](#vim-三个模式) -* [参考资料](#参考资料) - - - -# 常用操作以及概念 - -## 求助 - -**1. --help** - -指令的基本用法与选项介绍。 - -**2. man** - -man 是 manual 的缩写,将指令的具体信息显示出来。 - -当执行 man date 时,有 DATE(1) 出现,其中的数字代表指令的类型,常用的数字及其类型如下: - -| 代号 | 类型 | -| -- | -- | -| 1 | 用户在 shell 环境中可以操作的指令或者可执行文件 | -| 5 | 配置文件 | -| 8 | 系统管理员可以使用的管理指令 | - -**3. info** - -info 与 man 类似,但是 info 将文档分成一个个页面,每个页面可以进行跳转。 - -## 关机 - -**1. sync** - -为了加快对磁盘上文件的读写速度,位于内存中的文件数据不会立即同步到磁盘上,因此关机之前需要先进行 sync 同步操作。 - -**2. shutdown** - -```html -# /sbin/shutdown [-krhc] [时间] [警告讯息] --k : 不会关机,只是发送警告讯息,通知所有在线的用户 --r : 将系统的服务停掉后就重新启动 --h : 将系统的服务停掉后就立即关机 --c : 取消已经在进行的 shutdown 指令内容 -``` - -**3. 其它关机指令** - -reboot、halt、poweroff。 - -## 查看进程 - -```html -ps aux | grep threadx -``` - -## 查看端口 - - -```html -netstat -anp | grep 80 -``` - -## PATH - -可以在环境变量 PATH 中声明可执行文件的路径,路径之间用 : 分隔。 - -```html -/usr/local/bin:/usr/bin:/usr/local/sbin:/usr/sbin:/home/dmtsai/.local/bin:/home/dmtsai/bin -``` - -## 运行等级 - -- 0:关机模式 -- 1:单用户模式(可用于破解root密码) -- 2:无网络支持的多用户模式 -- 3:有网络支持的多用户模式(文本模式,工作中最常用的模式) -- 4:保留,未使用 -- 5:有网络支持的 X-windows 多用户模式(桌面) -- 6:重新引导系统,即重启 - -## sudo - -使用 sudo 允许一般用户使用 root 可执行的命令,只有在 /etc/sudoers 配置文件中添加的用户才能使用该指令。 - -## GNU - -GNU 计划,又译为革奴计划,它的目标是创建一套完全自由的操作系统,称为 GNU,其内容软件完全以 GPL 方式发布。其中 GPL 全称为 GNU 通用公共许可协议,包含了以下内容: - -- 以任何目的运行此程序的自由; -- 再复制的自由; -- 改进此程序,并公开发布改进的自由。 - -## 包管理工具 - -RPM 和 DPKG 为最常见的两类软件包管理工具。RPM 全称为 Redhat Package Manager,最早由 Red Hat 公司制定实施,随后被 GNU 开源操作系统接受并成为很多 Linux 系统 (RHEL) 的既定软件标准。与 RPM 进行竞争的是基于 Debian 操作系统 (UBUNTU) 的 DEB 软件包管理工具- DPKG,全称为 Debian Package,功能方面与 RPM 相似。 - -YUM 基于 RPM 包管理工具,具有依赖管理功能,并具有软件升级的功能。 - -## 常见发行版本 - -Linux 发行版是 Linux 内核及各种应用软件的集成版本。 - -| 基于的包管理工具 | 商业发行版 | 社区发行版 | -| --- | --- | --- | -| DPKG | Ubuntu | Debian | -| RPM | Red Hat | Fedora / CentOS | - -# 分区 - -## 磁盘的文件名 - -Linux 中每个硬件都被当做一个文件。 - -常见磁盘的文件名: - -- SCSI/SATA/USB 磁盘:/dev/sd[a-p] -- IDE 磁盘:/dev/hd[a-d] - -其中文件名后面的序号的确定与磁盘插入的顺序有关,而与磁盘所插入的插槽位置无关。 - -## 分区表 - -磁盘分区表主要有两种格式,一种是限制较多的 MBR 分区表,一种是较新且限制较少的 GPT 分区表。 - -### 1. MBR - -MBR 中,第一个扇区最重要,里面有:主要开机记录(Master boot record, MBR)及分区表(partition table),其中 MBR 占 446 bytes,partition table 占 64 bytes。 - -分区表只有 64 bytes,最多只能存储 4 个分区,这 4 个分区为主分区(Primary)和扩展分区(Extended)。其中扩展分区只有一个,它将其它空间用来记录分区表,可以记录更多的分区,因此通过扩展分区可以分出更多区分,这些分区称为逻辑分区。 - -Linux 也把分区当成文件,分区文件的命名方式为:磁盘文件名+编号,例如 /dev/sda1。注意,逻辑分区的编号从 5 开始。 - -### 2. GPT - -不同的磁盘有不同的扇区大小,例如 512 bytes 和最新磁盘的 4k。GPT 为了兼容所有磁盘,在定义扇区上使用逻辑区块地址(Logical Block Address, LBA)。 - -GPT 第 1 个区块记录了 MBR,紧接着是 33 个区块记录分区信息,并把最后的 33 个区块用于对分区信息进行备份。 - -GPT 没有扩展分区概念,都是主分区,最多可以分 128 个分区。 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//a5c25452-6fa5-49e7-9322-823077442775.jpg) - -## 开机检测程序 - -### 1. BIOS - -BIOS 是开机的时候计算机执行的第一个程序,这个程序知道可以开机的磁盘,并读取磁盘第一个扇区的 MBR,由 MBR 执行其中的开机管理程序,这个开机管理程序的会加载操作系统的核心文件。 - -MBR 中的开机管理程序提供以下功能:选单、载入核心文件以及转交其它开机管理程序。转交这个功能可以用来实现了多重引导,只需要将另一个操作系统的开机管理程序安装在其它分区的启动扇区上,在启动 MBR 中的开机管理程序时,就可以选择启动当前的操作系统或者转交给其它开机管理程序从而启动另一个操作系统。 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//f900f266-a323-42b2-bc43-218fdb8811a8.jpg) - -安装多重引导,最好先安装 Windows 再安装 Linux。因为安装 Windows 时会覆盖掉 MBR,而 Linux 可以选择将开机管理程序安装在 MBR 或者其它分区的启动扇区,并且可以设置开机管理程序的选单。 - -### 2. UEFI - -UEFI 相比于 BIOS 来说功能更为全面,也更为安全。 - -## 挂载 - -挂载利用目录作为分区的进入点,也就是说,进入目录之后就可以读取分区的数据。 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//249f3bb1-feee-4805-a259-a72699d638ca.jpg) - -# 文件权限与目录配置 - -## 文件权限概念 - -把用户分为三种:文件拥有者、群组以及其它人,对不同的用户有不同的文件权限。 - -使用 ls 查看一个文件时,会显示一个文件的信息,例如 drwxr-xr-x. 3 root root 17 May 6 00:14 .config,对这个信息的解释如下: - -- drwxr-xr-x:文件类型以及权限,第 1 位为文件类型字段,后 9 位为文件权限字段。 -- 3:链接数; -- root:文件拥有者; -- root:所属群组; -- 17:文件大小; -- May 6 00:14:文件最后被修改的时间; -- .config:文件名。 - -常见的文件类型及其含义有: - -- d:目录; -- -:文件; -- l:链接文件; - -9 位的文件权限字段中,每 3 个为一组,共 3 组,每一组分别代表对文件拥有者、所属群组以及其它人的文件权限。一组权限中的 3 位分别为 r、w、x 权限,表示可读、可写、可执行。 - -## 文件属性以及权限的修改 - -### 1. 修改文件所属群组 - -```html -# chgrp [-R] groupname dirname/filename --R:递归修改 -``` - -### 2. 修改文件拥有者 - -不仅可以修改文件拥有者,也可以修改文件所属群组。 - -```html -# chown [-R] 用户名:群组名 dirname/filename -``` - -### 3. 修改权限 - -可以将一组权限用数字来表示,此时一组权限的 3 个位当做二进制数字的位,从左到右每个位的权值为 4、2、1,即每个权限对应的数字权值为 r:4、w:2、x:1。 - -```html -# chmod [-R] xyz dirname/filename -``` - -范例:将 .bashrc 文件的权限修改为 -rwxr-xr--。 - -```html -# chmod 754 .bashrc -``` - -也可以使用符号来设定权限。 - -```html -# chmod [ugoa] [+-=] [rwx] dirname/filename -- u:拥有者 -- g:所属群组 -- o:其他人 -- a:所有人 -- +:添加权限 -- -:移除权限 -- =:设定权限 -``` - -范例:为 .bashrc 文件的所有用户添加写权限。 - -```html -# chmod a+w .bashrc -``` - -## 目录的权限 - -文件名不是存储在一个文件的内容中,而是存储在一个文件所在的目录中。因此,拥有文件的 w 权限并不能对文件名进行修改。 - -目录存储文件列表,一个目录的权限也就是对其文件列表的权限。因此,目录的 r 权限表示可以读取文件列表;w 权限表示可以修改文件列表,具体来说,就是添加删除文件,对文件名进行修改;x 权限可以让该目录成为工作目录,x 权限是 r 和 w 权限的基础,如果不能使一个目录成为工作目录,也就没办法读取文件列表以及对文件列表进行修改了。 - -## 文件默认权限 - -文件默认权限:文件默认没有可执行权限,因此为 666,也就是 -rw-rw-rw- 。 -目录默认权限:目录必须要能够进入,也就是必须拥有可执行权限,因此为 777 ,也就是 drwxrwxrwx。 - -可以通过 umask 设置或者查看文件的默认权限,通常以掩码的形式来表示,例如 002 表示其它用户的权限去除了一个 2 的权限,也就是写权限,因此建立新文件时默认的权限为 -rw-rw-r-- 。 - -## 目录配置 - -为了使不同 Linux 发行版本的目录结构保持一致性,Filesystem Hierarchy Standard (FHS) 规定了 Linux 的目录结构。最基础的三个目录如下: - -- / (root, 根目录) -- /usr (unix software resource):所有系统默认软件都会安装到这个目录; -- /var (variable):存放系统或程序运行过程中的数据文件。 - -完整的目录树如下: - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//27ace615-558f-4dfb-8ad4-7ac769c10118.jpg) - -# 文件与目录 - -## 文件时间 - -1. modification time (mtime):文件的内容更新就会更新; -2. status time (ctime):文件的状态(权限、属性)更新就会更新; -3. access time (atime):读取文件时就会更新。 - -## 文件与目录的基本操作 - -### 1. ls - -列出文件或者目录的信息,目录的信息就是其中包含的文件。 - -```html -# ls [-aAdfFhilnrRSt] file|dir --a :列出全部的文件 --d :仅列出目录本身 --l :以长数据串行列出,包含文件的属性与权限等等数据 -``` - -### 2. cp - -复制操作。 - -如果源文件有两个以上,则目的文件一定要是目录才行。 - -```html -cp [-adfilprsu] source destination --a :相当于 -dr --preserve=all 的意思,至于 dr 请参考下列说明 --d :若来源文件为链接文件,则复制链接文件属性而非文件本身 --i :若目标文件已经存在时,在覆盖前会先询问 --p :连同文件的属性一起复制过去 --r :递归持续复制 --u :destination 比 source 旧才更新 destination,或 destination 不存在的情况下才复制 ---preserve=all :除了 -p 的权限相关参数外,还加入 SELinux 的属性, links, xattr 等也复制了 -``` - -### 3. rm - -移除操作。 - -```html -# rm [-fir] 文件或目录 --r :递归删除 -``` - -### 4. mv - -移动操作。 - -```html -# mv [-fiu] source destination -# mv [options] source1 source2 source3 .... directory --f : force 强制的意思,如果目标文件已经存在,不会询问而直接覆盖 -``` - -## 获取文件内容 - -### 1. cat - -取得文件内容。 - -```html -# cat [-AbEnTv] filename --n :打印出行号,连同空白行也会有行号,-b 不会 -``` - -### 2. tac - -是 cat 的反向操作,从最后一行开始打印。 - -### 3. more - -可以一页一页查看文件内容,和文本编辑器类似。 - -### 4. less - -和 more 类似。 - -### 5. head - -可以取得文件前几行。 - -```html -# head [-n number] filename --n :后面接数字,代表显示几行的意思 -``` - -### 6. tail - -是 head 的反向操作,只是取得是后几行。 - -### 7. od - -可以以字符或者十六进制的形式显示二进制文件。 - -### 8. touch - -修改文件时间或者建立新文件。 - -```html -# touch [-acdmt] filename --a : 更新 atime --c : 更新 ctime,若该文件不存在则不建立新文件 --m : 更新 mtime --d : 后面可以接欲更新的日期而不用当前的日期,也可以使用 --date="日期或时间" --t :后面可以接欲更新的时间而不用当前的时间,格式为[YYYYMMDDhhmm] -``` - -## 指令与文件搜索 - -### 1. which - -指令搜索。 - -```html -# which [-a] command --a :将所有指令列出,而不是只列第一个 -``` - -### 2. whereis - -whereis 搜索文件的速度比较快,因为它只搜索几个特定的目录。 - -```html -# whereis [-bmsu] dirname/filename -``` - -### 3. locate - -locate 可以用关键字或者正则表达式进行搜索。 - -locate 使用 /var/lib/mlocate/ 这个数据库来进行搜索,它存储在内存中,并且每天更新一次,所以无法用 locate 搜索新建的文件。可以使用 updatedb 来立即更新数据库。 - -```html -# locate [-ir] keyword --r:接正则表达式 -``` - -### 4. find - -find 可以使用文件的属性和权限进行搜索。 - -```html -# find filename [option] -``` - -#### 4.1 与时间有关的选项 - -```html --mtime n :列出在 n 天前的那一天修改过内容的文件 --mtime +n :列出在 n 天之前(不含 n 天本身)修改过内容的文件 --mtime -n :列出在 n 天之内(含 n 天本身)修改过内容的文件 --newer file : 列出比 file 更新的文件 -``` - -+4、4 和 -4 的指示的时间范围如下: - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//658fc5e7-79c0-4247-9445-d69bf194c539.png) - -#### 4.2 与文件拥有者和所属群组有关的选项 - -```html --uid n --gid n --user name --group name --nouser :搜索拥有者不存在 /etc/passwd 的文件 --nogroup:搜索所属群组不存在于 /etc/group 的文件 -``` - -#### 4.3 与文件权限和名称有关的选项 - -```html --name filename --size [+-]SIZE:搜寻比 SIZE 还要大(+)或小(-)的文件。这个 SIZE 的规格有:c: 代表 byte,k: 代表 1024bytes。所以,要找比 50KB 还要大的文件,就是 -size +50k --type TYPE --perm mode :搜索权限等于 mode 的文件 --perm -mode :搜索权限包含 mode 的文件 --perm /mode :搜索权限包含任一 mode 的文件 -``` - -# 磁盘与文件系统 - -## 文件系统的组成 - -对分区进行格式化是为了在分区上建立文件系统。一个分区通常只能格式化为一个文件系统,但是磁盘阵列等技术可以将一个分区格式化为多个文件系统,因此只有文件系统能被挂载,而分区不能被挂载。 - -文件系统有以下三个结构: - -1. superblock:记录文件系统的整体信息,包括 inode 和 block 的总量、使用量、剩余量,以及文件系统的格式与相关信息等; -2. inode:一个文件占用一个 inode,记录文件的属性,同时记录此文件的内容所在的 block 号码; -3. block:记录文件的内容,文件太大时,会占用多个 block。 - -当要读取一个文件的内容时,先在 inode 中去查找文件内容所在的所有 block,然后把所有 block 的内容读出来。 - -磁盘碎片是指一个文件内容所在的 block 过于分散。 - -Ext2 文件系统使用了上述的文件结构,并在此之上加入了 block 群组的概念,也就是将一个文件系统划分为多个 block 群组,方便管理。 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//1974a836-aa6b-4fb8-bce1-6eb11969284a.jpg) - -## inode - -Ext2 文件系统支持的 block 大小有 1k、2k 和 4k 三种,不同的 block 大小限制了单一文件的大小。而每个 inode 大小是固定为 128 bytes。 - -inode 中记录了文件内容所在的 block,但是每个 block 非常小,一个大文件随便都需要几十万的 block,而一个 inode 大小有限,无法直接引用这么多 block。因此引入了间接、双间接、三间接引用。间接引用是指,让 inode 记录的引用 block 块当成 inode 用来记录引用信息。 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//89091427-7b2b-4923-aff6-44681319a8aa.jpg) - -inode 具体包含以下信息: - -- 该文件的存取模式(read/write/excute); -- 该文件的拥有者与群组(owner/group); -- 该文件的容量; -- 该文件建立或状态改变的时间(ctime); -- 最近一次的读取时间(atime); -- 最近修改的时间(mtime); -- 定义文件特性的旗标(flag),如 SetUID...; -- 该文件真正内容的指向 (pointer)。 - -## 目录的 inode 与 block - -建立一个目录时,会分配一个 inode 与至少一个 block。block 记录的内容是目录下所有文件的 inode 编号以及文件名。可以看出文件的 inode 本身不记录文件名,文件名记录在目录中,因此新增文件、删除文件、更改文件名这些操作与目录的 w 权限有关。 - -## 实体链接与符号链接 - -```html -# ln [-sf] source_filename dist_filename --s :默认是 hard link,加 -s 为 symbolic link --f :如果目标文件存在时,先删除目标文件 -``` - -### 1. 实体链接 - -hard link 只是在某个目录下新增一个条目,使得新增的条目链接到文件的 inode 上。删除任意一个条目,文件还是存在,只要引用数量不为 0。 - -有以下限制:不能跨越 File System;不能对目录进行链接。 - -```html -# ln /etc/crontab . -# ll -i /etc/crontab crontab -34474855 -rw-r--r--. 2 root root 451 Jun 10 2014 crontab -34474855 -rw-r--r--. 2 root root 451 Jun 10 2014 /etc/crontab -``` - -### 2. 符号链接 - -symbolic link 可以理解为 Windows 的快捷方式,通过建立一个独立的文件,这个文件的数据的读取指向链接的那个文件。当源文件被删除了,链接文件就打不开了。 - -symbolic link 可以为目录建立链接。 - -```html -# ll -i /etc/crontab /root/crontab2 -34474855 -rw-r--r--. 2 root root 451 Jun 10 2014 /etc/crontab -53745909 lrwxrwxrwx. 1 root root 12 Jun 23 22:31 /root/crontab2 -> /etc/crontab -``` - -# 压缩与打包 - -## 压缩 - -Linux 底下有很多压缩文件的扩展名,常见的如下: - -| 扩展名 | 压缩程序 | -| -- | -- | -| \*.Z | compress | -|\*.zip | zip | -|\*.gz | gzip| -|\*.bz2 | bzip2 | -|\*.xz | xz | -|\*.tar | tar 程序打包的数据,没有经过压缩 | -|\*.tar.gz | tar 程序打包的文件,经过 gzip 的压缩 | -|\*.tar.bz2 | tar 程序打包的文件,经过 bzip2 的压缩 | -|\*.tar.xz | tar 程序打包的文件,经过 xz 的压缩 | - -### 1. gzip - -gzip 是 Linux 使用最广的压缩指令,可以解开 compress、zip 与 gzip 所压缩的文件。 - -经过 gzip 压缩过,源文件就不存在了。 - -有 9 个不同的压缩等级可以使用。 - -可以使用 zcat、zmore、zless 来读取压缩文件的内容。 - -```html -$ gzip [-cdtv#] filename --c :将压缩的数据输出到屏幕上 --d :解压缩 --t :检验压缩文件是否出错 --v :显示压缩比等信息 --# : # 为数字的意思,代表压缩等级,数字越大压缩比越高,默认为6 -``` - -### 2. bzip2 - -提供比 gzip 更高的压缩比。 - -查看命令:bzcat、bzmore、bzless、bzgrep。 - -```html -$ bzip2 [-cdkzv#] filename --k :保留源文件 -``` - -### 3. xz - -提供比 bzip2 更佳的压缩比。 - -可以看到,gzip、bzip2、xz 的压缩比不断优化。不过要注意,压缩比越高,压缩的时间也越长。 - -查看命令:xzcat、xzmore、xzless、xzgrep。 - -```html -$ xz [-dtlkc#] filename -``` - -## 打包 - -压缩指令只能对一个文件进行压缩,而打包能够将多个文件打包成一个大文件。tar 不仅可以用于打包,也可以使用 gip、bzip2、xz 将打包文件进行压缩。 - -```html -$ tar [-z|-j|-J] [cv] [-f 新建的tar文件] filename... ==打包压缩 -$ tar [-z|-j|-J] [tv] [-f 已有的tar文件] ==查看 -$ tar [-z|-j|-J] [xv] [-f 已有的tar文件] [-C 目录] ==解压缩 --z :使用zip; --j :使用bzip2; --J :使用xz; --c :新建打包文件; --t :查看打包文件里面有哪些文件; --x :解打包或解压缩的功能; --v :在压缩/解压缩的过程中,显示正在处理的文件名; --f : filename:要处理的文件; --C 目录 : 在特定目录解压缩。 -``` - -| 使用方式 | 命令 | -| --- | --- | -| 打包压缩 | tar -jcv -f filename.tar.bz2 要被压缩的文件或目录名称 | -| 查 看 | tar -jtv -f filename.tar.bz2 | -| 解压缩 | tar -jxv -f filename.tar.bz2 -C 要解压缩的目录 | - - -# Bash - -可以通过 Shell 请求内核提供服务,Bash 正是 Shell 的一种。 - -## Bash 特性 - -**1. 命令历史** - -记录使用过的命令。本次登录所执行的命令都会暂时存放到内存中, \~/.bash_history 文件中记录的是前一次登录所执行过的命令。 - -**2. 命令与文件补全** - -快捷键:tab - -**3. 命名别名** - -例如 lm 是 ls -al 的别名。 - -**4. shell scripts** - -**5. 通配符** - -例如 ls -l /usr/bin/X\* 列出 /usr/bin 下面所有以 X 开头的文件。 - -## 变量操作 - -- 对一个变量赋值直接使用 = ; -- 对变量取用需要在变量前加上 \{} 的形式; -- 输出变量使用 echo 命令。 - -```bash -$ var=abc -$ echo $var -$ echo ${var} -``` - -变量内容如果有空格,需要使用双引号或者单引号。双引号内的特殊字符可以保留原本特性,例如var="lang is \(uname -r),则 version 的值为 3.10.0-229.el7.x86_64。 - -可以使用 export 命令将自定义变量转成环境变量,环境变量可以在子程序中使用,所谓子程序就是由当前 Bash 而产生的子 Bash。 - -Bash 的变量可以声明为数组和整数数字。注意数字类型没有浮点数。如果不进行声明,默认是字符串类型。变量的声明使用 declare 命令: - -```html -$ declare [-aixr] variable --a : 定义为数组类型 --i : 定义为整数类型 --x : 定义为环境变量 --r : 定义为readonly类型 -``` - -使用 [ ] 来对数组进行操作: - -```bash -$ array[1]=a -$ array[2]=b -$ echo ${array[1]} -``` - -## 指令搜索顺序 - -1. 以绝对或相对路径来执行指令,例如 /bin/ls 或者 ./ls ; -2. 由别名找到该指令来执行; -3. 由 Bash 内建的指令来执行; -4. 按 \$PATH 变量指定的搜索路径的顺序找到第一个指令来执行。 - -## 数据流重定向 - -重定向就是使用文件代替标准输入、标准输出和标准错误输出。 - - -1. 标准输入(stdin)       :代码为 0 ,使用 < 或 << ; -2. 标准输出(stdout)     :代码为 1 ,使用 > 或 >> ; -3. 标准错误输出(stderr):代码为 2 ,使用 2> 或 2>> ; - -其中,有一个箭头的表示以覆盖的方式重定向,而有两个箭头的表示以追加的方式重定向。 - -可以将不需要的标准输出以及标准错误输出重定向到 /dev/null,相当于扔进垃圾箱。 - -如果需要将标准输出以及标准错误输出同时重定向到一个文件,需要将某个输出转换为另一个输出,例如 2>&1 表示将标准错误输出转换为标准输出。 - -```bash -$ find /home -name .bashrc > list 2>&1 -``` - -## 管线指令 - -管线是将一个命令的标准输出作为另一个命令的标准输入,在数据需要经过多个步骤的处理之后才能得到我们想要的格式时就可以使用管线。在命令之间使用 | 分隔各个管线命令。 - -```bash -$ ls -al /etc | less -``` - -### 1. 提取指令:cut - -提取过程一行一行地进行。 - -cut 对数据进行切分,取出想要的部分。 - -```html -$ cut --d :分隔符 --f :经过 -d 分隔后,使用 -f n 取出第 n 个区间 --c :以字符为单位取出区间 -``` - -范例 1:last 将显示的登入者的信息,要求仅显示用户名。 - -```html -$ last -root pts/1 192.168.201.101 Sat Feb 7 12:35 still logged in -root pts/1 192.168.201.101 Fri Feb 6 12:13 - 18:46 (06:33) -root pts/1 192.168.201.254 Thu Feb 5 22:37 - 23:53 (01:16) - -$ last | cut -d ' ' -f 1 -``` - -范例 2:将 export 输出的讯息,取得第 12 字符以后的所有字符串。 - -```html -$ export -declare -x HISTCONTROL="ignoredups" -declare -x HISTSIZE="1000" -declare -x HOME="/home/dmtsai" -declare -x HOSTNAME="study.centos.vbird" -.....(其他省略)..... - -$ export | cut -c 12 -``` - -### 2. 排序命令:sort、uniq - -**sort** 进行排序。 - -```html -$ sort [-fbMnrtuk] [file or stdin] --f :忽略大小写 --b :忽略最前面的空格 --M :以月份的名字来排序,例如 JAN,DEC --n :使用数字 --r :反向排序 --u :相当于 unique,重复的内容只出现一次 --t :分隔符,默认为 tab --k :指定排序的区间 -``` - -范例:/etc/passwd 内容是以 : 来分隔的,以第三栏来排序。 - -```html -$ cat /etc/passwd | sort -t ':' -k 3 -root:x:0:0:root:/root:/bin/bash -dmtsai:x:1000:1000:dmtsai:/home/dmtsai:/bin/bash -alex:x:1001:1002::/home/alex:/bin/bash -arod:x:1002:1003::/home/arod:/bin/bash -``` - -**uniq** 可以将重复的数据只取一个。 - -```html -$ uniq [-ic] --i :忽略大小写 --c :进行计数 -``` - -范例:取得每个人的登录总次数 - -```html -$ last | cut -d ' ' -f 1 | sort | uniq -c -1 -6 (unknown -47 dmtsai -4 reboot -7 root -1 wtmp -``` - -### 3. 双向输出重定向:tee - -输出重定向会将输出内容重定向到文件中,而 **tee** 不仅能够完成这个功能,还能保留屏幕上的输出。也就是说,使用 tee 指令,一个输出会同时传送到文件和屏幕上。 - -```html -$ tee [-a] file -``` - -### 4. 字符转换指令:tr、col、expand、join、paste - - **tr** 用来删除一行中的字符,或者对字符进行替换。 - -```html -$ tr [-ds] SET1 ... --d : 删除行中 SET1 这个字符串 -``` - -范例,将 last 输出的信息所有小写转换为大写。 - -```html -$ last | tr '[a-z]' '[A-Z]' -``` - - **col** 将 tab 字符转为空格字符。 - -```html -$ col [-xb] --x : 将 tab 键转换成对等的空格键 -``` - -**expand** 将 tab 转换一定数量的空格,默认是 8 个。 - -```html -$ expand [-t] file --t :tab 转为空格的数量 -``` - -**join** 将有相同数据的那一行合并在一起。 - -```html -$ join [-ti12] file1 file2 --t :分隔符,默认为空格 --i :忽略大小写的差异 --1 :第一个文件所用的比较字段 --2 :第二个文件所用的比较字段 -``` - -**paste** 直接将两行粘贴在一起。 - -```html -$ paste [-d] file1 file2 --d :分隔符,默认为 tab -``` - -### 5. 分区指令:split - -**split** 将一个文件划分成多个文件。 - -```html -$ split [-bl] file PREFIX --b :以大小来进行分区,可加单位,例如 b, k, m 等 --l :以行数来进行分区。 -- PREFIX :分区文件的前导名称 -``` - -# 正规表示法与文件格式化处理 - -## grep - -使用正则表示式把匹配的行提取出来。 - -```html -$ grep [-acinv] [--color=auto] 搜寻字符串 filename --a : 将 binary 文件以 text 文件的方式进行搜寻 --c : 计算找到个数 --i : 忽略大小写 --n : 输出行号 --v : 反向选择,亦即显示出没有 搜寻字符串 内容的那一行 ---color=auto :找到的关键字加颜色显示 -``` - -范例:把含有 the 字符串的行提取出来(注意默认会有 --color=auto 选项,因此以下内容在 Linux 中有颜色显示 the 字符串) - -```html -$ grep -n 'the' regular_express.txt -8:I can't finish the test. -12:the symbol '*' is represented as start. -15:You are the best is mean you are the no. 1. -16:The world Happy is the same with "glad". -18:google is the best tools for search keyword -``` - -因为 { 与 } 的符号在 shell 是有特殊意义的,因此必须要使用使用转义字符进行转义。 - -```html -$ grep -n 'go\{2,5\}g' regular_express.txt -``` - -## printf - -用于格式化输出。 - -它不属于管道命令,在给 printf 传数据时需要使用 $( ) 形式。 - -```html -$ printf '%10s %5i %5i %5i %8.2f \n' $(cat printf.txt) - DmTsai 80 60 92 77.33 - VBird 75 55 80 70.00 - Ken 60 90 70 73.33 -``` - -## awk - -```html -$ awk '条件类型1{动作1} 条件类型2{动作2} ...' filename -``` - -awk 每次处理一行,处理的最小单位是字段,每个字段的命名方式为:\0 表示一整行。 - -范例 1:取出登录用户的用户名和 ip - -```html -$ last -n 5 -dmtsai pts/0 192.168.1.100 Tue Jul 14 17:32 still logged in -dmtsai pts/0 192.168.1.100 Thu Jul 9 23:36 - 02:58 (03:22) -dmtsai pts/0 192.168.1.100 Thu Jul 9 17:23 - 23:36 (06:12) -dmtsai pts/0 192.168.1.100 Thu Jul 9 08:02 - 08:17 (00:14) -dmtsai tty1 Fri May 29 11:55 - 12:11 (00:15) - -$ last -n 5 | awk '{print $1 "\t" $3} -``` - -awk 变量: - -| 变量名称 | 代表意义 | -| -- | -- | -| NF | 每一行拥有的字段总数 | -| NR | 目前所处理的是第几行数据 | -| FS | 目前的分隔字符,默认是空格键 | - -范例 2:输出正在处理的行号,并显示每一行有多少字段 - -```html -$ last -n 5 | awk '{print $1 "\t lines: " NR "\t columns: " NF}' -dmtsai lines: 1 columns: 10 -dmtsai lines: 2 columns: 10 -dmtsai lines: 3 columns: 10 -dmtsai lines: 4 columns: 10 -dmtsai lines: 5 columns: 9 -``` - -可以使用大于等于逻辑,其中等于使用 ==。 - -范例 3:/etc/passwd 文件第三个字段为 UID,对 UID 小于 10 的数据进行处理。 - -```text -cat /etc/passwd | awk 'BEGIN {FS=":"} $3 < 10 {print $1 "\t " $3}' -root 0 -bin 1 -daemon 2 -``` - -# vim 三个模式 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//341c632a-1fc1-4068-9b9f-bf7ef68ebb4c.jpg) - -在指令列模式下,有以下命令用于离开或者存储文件。 - -| 命令 | 作用 | -| -- | -- | -| :w | 写入磁盘| -| :w! | 当文件为只读时,强制写入磁盘。到底能不能写入,与用户对该文件的权限有关 | -| :q | 离开| -| :q! | 强制离开不保存| -| :wq | 写入磁盘后离开| -| :wq!| 强制写入磁盘后离开| - - -# 参考资料 - -- 鸟哥. 鸟 哥 的 Linux 私 房 菜 基 础 篇 第 三 版[J]. 2009. -- [Linux 平台上的软件包管理](https://www.ibm.com/developerworks/cn/linux/l-cn-rpmdpkg/index.html) - diff --git a/notes/MySQL.md b/notes/MySQL.md deleted file mode 100644 index e7feb5e2..00000000 --- a/notes/MySQL.md +++ /dev/null @@ -1,422 +0,0 @@ - -* [存储引擎](#存储引擎) - * [1. InnoDB](#1-innodb) - * [2. MyISAM](#2-myisam) - * [3. InnoDB 与 MyISAM 的比较](#3-innodb-与-myisam-的比较) -* [数据类型](#数据类型) - * [1. 整型](#1-整型) - * [2. 浮点数](#2-浮点数) - * [3. 字符串](#3-字符串) - * [4. 时间和日期](#4-时间和日期) -* [索引](#索引) - * [1. 索引分类](#1-索引分类) - * [1.1 B-Tree 索引](#11-b-tree-索引) - * [1.2 哈希索引](#12-哈希索引) - * [1.3. 空间索引数据(R-Tree)](#13-空间索引数据r-tree) - * [1.4 全文索引](#14-全文索引) - * [2. 索引的优点](#2-索引的优点) - * [3. 索引优化](#3-索引优化) - * [3.1 独立的列](#31-独立的列) - * [3.2 前缀索引](#32-前缀索引) - * [3.3 多列索引](#33-多列索引) - * [3.4 索引列的顺序](#34-索引列的顺序) - * [3.5 聚簇索引](#35-聚簇索引) - * [3.6 覆盖索引](#36-覆盖索引) - * [4. B-Tree 和 B+Tree 原理](#4-b-tree-和-b+tree-原理) - * [4. 1 B-Tree](#4-1-b-tree) - * [4.2 B+Tree](#42-b+tree) - * [4.3 带有顺序访问指针的 B+Tree](#43-带有顺序访问指针的-b+tree) - * [4.4 为什么使用 B-Tree 和 B+Tree](#44-为什么使用-b-tree-和-b+tree) -* [查询性能优化](#查询性能优化) - * [1. Explain](#1-explain) - * [2. 减少返回的列](#2-减少返回的列) - * [3. 减少返回的行](#3-减少返回的行) - * [4. 拆分大的 DELETE 或 INSERT 语句](#4-拆分大的-delete-或-insert-语句) -* [分库与分表](#分库与分表) -* [故障转移和故障恢复](#故障转移和故障恢复) - * [1. 故障转移](#1-故障转移) - * [2. 故障恢复](#2-故障恢复) -* [参考资料](#参考资料) - - - -# 存储引擎 - -## 1. InnoDB - -InnoDB 是 MySQL 的默认事务型引擎,只有在需要 InnoDB 不支持的特性时,才考虑使用其它存储引擎。 - -采用 MVCC 来支持高并发,并且实现了四个标准的隔离级别,默认级别是可重复读。 - -表是基于聚簇索引建立的,它对主键的查询性能有很高的提升。 - -内部做了很多优化,包括从磁盘读取数据时采用的可预测性读,能够自动在内存中创建 hash 索引以加速读操作的自适应哈希索引,以及能够加速插入操作的插入缓冲区等。 - -通过一些机制和工具支持真正的热备份。 - -## 2. MyISAM - -MyISAM 提供了大量的特性,包括全文索引、压缩、空间函数(GIS)等。但 MyISAM 不支持事务和行级锁,而且奔溃后无法安全恢复。 - -只能对整张表加锁,而不是针对行。 - -可以手工或者自动执行检查和修复操作,但是和事务恢复以及奔溃恢复不同,可能导致一些数据丢失,而且修复操作是非常慢的。 - -可以包含动态或者静态的行。 - -如果指定了 DELAY_KEY_WRITE 选项,在每次修改执行完成时,不会立即将修改的索引数据写入磁盘,而是会写到内存中的键缓冲区,只有在清理键缓冲区或者关闭表的时候才会将对应的索引块写入磁盘。这种方式可以极大的提升写入性能,但是在数据库或者主机奔溃时会造成索引损坏,需要执行修复操作。 - -如果表在创建并导入数据以后,不会再进行修改操作,那么这样的表适合采用 MyISAM 压缩表。 - -对于只读数据,或者表比较小、可以容忍修复操作,则依然可以继续使用 MyISAM。 - -MyISAM 设计简单,数据以紧密格式存储,所以在某些场景下性能很好。 - -## 3. InnoDB 与 MyISAM 的比较 - -**事务** - -InnoDB 是事务型的。 - -**备份** - -InnoDB 支持在线热备份。 - -**奔溃恢复** - -MyISAM 奔溃后发生损坏的概率比 InnoDB 高很多,而且恢复的速度也更慢。 - -**并发** - -MyISAM 只支持表级锁,而 InnoDB 还支持行级锁。 - -**其它特性** - -MyISAM 支持全文索引,地理空间索引; - -# 数据类型 - -## 1. 整型 - -TINYINT, SMALLINT, MEDIUMINT, INT, BIGINT 分别使用 8, 16, 24, 64 位存储空间,一般情况下越小的列越好。 - -INT(11) 中的数字只是规定了交互工具显示字符的个数,对于存储和计算来说是没有意义的。 - -## 2. 浮点数 - -FLOAT 和 DOUBLE 为浮点类型,DECIMAL 为高精度小数类型。CPU 原生支持浮点运算,但是不支持 DECIMAl 类型的计算,因此 DECIMAL 的计算比浮点类型需要更高的代价。 - -FLOAT、DOUBLE 和 DECIMAL 都可以指定列宽,例如 DECIMAL(18, 9) 表示总共 18 位,取 9 位存储小数部分,剩下 9 位存储整数部分。 - -## 3. 字符串 - -主要有 CHAR 和 VARCHAR 两种类型,一种是定长的,一种是变长的。 - -VARCHAR 这种变长类型能够节省空间,因为只需要存储必要的内容。但是在执行 UPDATE 时可能会使行变得比原来长,当超出一个页所能容纳的大小时,就要执行额外的操作,MyISAM 会将行拆成不同的片段存储,而 InnoDB 则需要分裂页来使行放进页内。 - -VARCHAR 会保留字符串末尾的空格,而 CHAR 会删除。 - -## 4. 时间和日期 - -MySQL 提供了两种相似的日期时间类型:DATATIME 和 TIMESTAMP。 - -**DATATIME** - -能够保存从 1001 年到 9999 年的日期和时间,精度为秒,使用 8 字节的存储空间。 - -它与时区无关。 - -默认情况下,MySQL 以一种可排序的、无歧义的格式显示 DATATIME 值,例如“2008-01016 22:37:08”,这是 ANSI 标准定义的日期和时间表示方法。 - -**TIMESTAMP** - -和 UNIX 时间戳相同,保存从 1970 年 1 月 1 日午夜(格林威治时间)以来的秒数,使用 4 个字节,只能表示从 1970 年 到 2038 年。 - -它和时区有关。 - -MySQL 提供了 FROM_UNIXTIME() 函数把 Unxi 时间戳转换为日期,并提供了 UNIX_TIMESTAMP() 函数把日期转换为 Unix 时间戳。 - -默认情况下,如果插入时没有指定 TIMESTAMP 列的值,会将这个值设置为当前时间。 - -应该尽量使用 TIMESTAMP,因为它比 DATETIME 空间效率更高。 - -# 索引 - -索引是在存储引擎层实现的,而不是在服务器层实现的,所以不同存储引擎具有不同的索引类型和实现。 - -索引能够轻易将查询性能提升几个数量级。 - -对于非常小的表、大部分情况下简单的全表扫描比建立索引更高效。对于中到大型的表,索引就非常有效。但是对于特大型的表,建立和使用索引的代价将会随之增长。这种情况下,需要用到一种技术可以直接区分出需要查询的一组数据,而不是一条记录一条记录地匹配,例如可以使用分区技术。 - -## 1. 索引分类 - -### 1.1 B-Tree 索引 - -B-Tree 索引是大多数 MySQL 存储引擎的默认索引类型。 - -因为不再需要进行全表扫描,只需要对树进行搜索即可,因此查找速度快很多。 - -可以指定多个列作为索引列,多个索引列共同组成键。B-Tree 索引适用于全键值、键值范围和键前缀查找,其中键前缀查找只适用于最左前缀查找。 - -除了用于查找,还可以用于排序和分组。 - -如果不是按照索引列的顺序进行查找,则无法使用索引。 - -### 1.2 哈希索引 - -基于哈希表实现,优点是查找非常快。 - -在 MySQL 中只有 Memory 引擎显式支持哈希索引。 - -InnoDB 引擎有一个特殊的功能叫“自适应哈希索引”,当某个索引值被使用的非常频繁时,会在 B-Tree 索引之上再创建一个哈希索引,这样就让 B-Tree 索引具有哈希索引的一些优点,比如快速的哈希查找。 - -限制:哈希索引只包含哈希值和行指针,而不存储字段值,所以不能使用索引中的值来避免读取行。不过,访问内存中的行的速度很快,所以大部分情况下这一点对性能影响并不明显;无法用于分组与排序;只支持精确查找,无法用于部分查找和范围查找;如果哈希冲突很多,查找速度会变得很慢。 - -### 1.3. 空间索引数据(R-Tree) - -MyISAM 存储引擎支持空间索引,可以用于地理数据存储。 - -空间索引会从所有维度来索引数据,可以有效地使用任意维度来进行组合查询。 - -### 1.4 全文索引 - -MyISAM 存储引擎支持全文索引,用于查找文本中的关键词,而不是直接比较索引中的值。 - -使用 MATCH AGAINST,而不是普通的 WHERE。 - -## 2. 索引的优点 - -- 大大减少了服务器需要扫描的数据量; - -- 帮助服务器避免进行排序和创建临时表; - -- 将随机 I/O 变为顺序 I/O。 - -## 3. 索引优化 - -### 3.1 独立的列 - -在进行查询时,索引列不能是表达式的一部分,也不能是函数的参数,否则无法使用索引。 - -例如下面的查询不能使用 actor_id 列的索引: - -```sql -SELECT actor_id FROM sakila.actor WHERE actor_id + 1 = 5; -``` - -### 3.2 前缀索引 - -对于 BLOB、TEXT 和 VARCHAR 类型的列,必须使用前缀索引,只索引开始的部分字符。 - -对于前缀长度的选取需要根据 **索引选择性** 来确定:不重复的索引值和记录总数的比值。选择性越高,查询效率也越高。最大值为 1 ,此时每个记录都有唯一的索引与其对应。 - -### 3.3 多列索引 - -在需要使用多个列作为条件进行查询时,使用多列索引比使用多个单列索引性能更好。例如下面的语句中,最好把 actor_id 和 file_id 设置为多列索引。 - -```sql -SELECT file_id, actor_ id FROM sakila.film_actor -WhERE actor_id = 1 OR film_id = 1; -``` - -### 3.4 索引列的顺序 - -让选择性最强的索引列放在前面,例如下面显示的结果中 customer_id 的选择性比 staff_id 更高,因此最好把 customer_id 列放在多列索引的前面。 - -```sql -SELECT COUNT(DISTINCT staff_id)/COUNT(*) AS staff_id_selectivity, -COUNT(DISTINCT customer_id)/COUNT(*) AS customer_id_selectivity, -COUNT(*) -FROM payment; -``` - -```html - staff_id_selectivity: 0.0001 -customer_id_selectivity: 0.0373 - COUNT(*): 16049 -``` - -### 3.5 聚簇索引 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//b9e9ae8c-e216-4c01-b267-a50dbeb98fa4.jpg) - -聚簇索引并不是一种索引类型,而是一种数据存储方式。 - -术语“聚簇”表示数据行和相邻的键值紧密地存储在一起,InnoDB 的聚簇索引的数据行存放在 B-Tree 的叶子页中。 - -因为无法把数据行存放在两个不同的地方,所以一个表只能有一个聚簇索引。 - -**优点** - -1. 可以把相关数据保存在一起,减少 I/O 操作; -2. 因为数据保存在 B-Tree 中,因此数据访问更快。 - -**缺点** - -1. 聚簇索引最大限度提高了 I/O 密集型应用的性能,但是如果数据全部放在内存,就没必要用聚簇索引。 -2. 插入速度严重依赖于插入顺序,按主键的顺序插入是最快的。 -3. 更新操作代价很高,因为每个被更新的行都会移动到新的位置。 -4. 当插入到某个已满的页中,存储引擎会将该页分裂成两个页面来容纳该行,页分裂会导致表占用更多的磁盘空间。 -5. 如果行比较稀疏,或者由于页分裂导致数据存储不连续时,聚簇索引可能导致全表扫描速度变慢。 - -### 3.6 覆盖索引 - -索引包含所有需要查询的字段的值。 - -## 4. B-Tree 和 B+Tree 原理 - -### 4. 1 B-Tree - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//5ed71283-a070-4b21-85ae-f2cbfd6ba6e1.jpg) - -为了描述 B-Tree,首先定义一条数据记录为一个二元组 [key, data],key 为记录的键,data 为数据记录除 key 外的数据。 - -B-Tree 是满足下列条件的数据结构: - -- 所有叶节点具有相同的深度,也就是说 B-Tree 是平衡的; -- 一个节点中的 key 从左到右非递减排列; -- 如果某个指针的左右相邻 key 分别是 keyi 和 keyi+1,且不为 null,则该指针指向节点的所有 key 大于 keyi 且小于 keyi+1。 - -在 B-Tree 中按 key 检索数据的算法非常直观:首先从根节点进行二分查找,如果找到则返回对应节点的 data,否则对相应区间的指针指向的节点递归进行查找,直到找到节点或找到 null 指针,前者查找成功,后者查找失败。 - -由于插入删除新的数据记录会破坏 B-Tree 的性质,因此在插入删除时,需要对树进行一个分裂、合并、转移等操作以保持 B-Tree 性质。 - -### 4.2 B+Tree - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//63cd5b50-d6d8-4df6-8912-ef4a1dd5ba13.jpg) - -与 B-Tree 相比,B+Tree 有以下不同点: - -- 每个节点的指针上限为 2d 而不是 2d+1; -- 内节点不存储 data,只存储 key,叶子节点不存储指针。 - -### 4.3 带有顺序访问指针的 B+Tree - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//1ee5f0a5-b8df-43b9-95ab-c516c54ec797.jpg) - -一般在数据库系统或文件系统中使用的 B+Tree 结构都在经典 B+Tree 基础上进行了优化,在叶子节点增加了顺序访问指针,做这个优化的目的是为了提高区间访问的性能。 - -### 4.4 为什么使用 B-Tree 和 B+Tree - -红黑树等数据结构也可以用来实现索引,但是文件系统及数据库系统普遍采用 B-/+Tree 作为索引结构。 - -页是计算机管理存储器的逻辑块,硬件及操作系统往往将主存和磁盘存储区分割为连续的大小相等的块,每个存储块称为一页(在许多操作系统中,页得大小通常为 4k),主存和磁盘以页为单位交换数据。 - -一般来说,索引本身也很大,不可能全部存储在内存中,因此索引往往以索引文件的形式存储的磁盘上。为了减少磁盘 I/O,磁盘往往不是严格按需读取,而是每次都会预读。这样做的理论依据是计算机科学中著名的局部性原理:当一个数据被用到时,其附近的数据也通常会马上被使用。数据库系统的设计者巧妙利用了磁盘预读原理,将一个节点的大小设为等于一个页,这样每个节点只需要一次 I/O 就可以完全载入。B-Tree 中一次检索最多需要 h-1 次 I/O(根节点常驻内存),渐进复杂度为 O(h)=O(logdN)。一般实际应用中,出度 d 是非常大的数字,通常超过 100,因此 h 非常小(通常不超过 3)。而红黑树这种结构,h 明显要深的多。并且于逻辑上很近的节点(父子)物理上可能很远,无法利用局部性,效率明显比 B-Tree 差很多。 - -B+Tree 更适合外存索引,原因和内节点出度 d 有关。由于 B+Tree 内节点去掉了 data 域,因此可以拥有更大的出度,拥有更好的性能。 - -# 查询性能优化 - -## 1. Explain - -用来分析 SQL 语句,分析结果中比较重要的字段有: - -- select_type : 查询类型,有简单查询、联合查询和子查询 - -- key : 使用的索引 - -- rows : 扫描的行数 - -## 2. 减少返回的列 - -慢查询主要是因为访问了过多数据,除了访问过多行之外,也包括访问过多列。 - -最好不要使用 SELECT * 语句,要根据需要选择查询的列。 - -## 3. 减少返回的行 - -最好使用 LIMIT 语句来取出想要的那些行。 - -还可以建立索引来减少条件语句的全表扫描。例如对于下面的语句,不适用索引的情况下需要进行全表扫描,而使用索引只需要扫描几行记录即可,使用 Explain 语句可以通过观察 rows 字段来看出这种差异。 - -```sql -SELECT * FROM sakila.film_actor WHERE film_id = 1; -``` - -## 4. 拆分大的 DELETE 或 INSERT 语句 - -如果一次性执行的话,可能一次锁住很多数据、占满整个事务日志、耗尽系统资源、阻塞很多小的但重要的查询。 - -```sql -DELEFT FROM messages WHERE create < DATE_SUB(NOW(), INTERVAL 3 MONTH); -``` -```sql -rows_affected = 0 -do { - rows_affected = do_query( - "DELETE FROM messages WHERE create < DATE_SUB(NOW(), INTERVAL 3 MONTH) LIMIT 10000") -} while rows_affected > 0 -``` - -# 分库与分表 - -**1. 分表与分区的不同** - -分表,就是讲一张表分成多个小表,这些小表拥有不同的表名;而分区是将一张表的数据分为多个区块,这些区块可以存储在同一个磁盘上,也可以存储在不同的磁盘上,这种方式下表仍然只有一个。 - -**2. 使用分库与分表的原因** - -随着时间和业务的发展,数据库中的表会越来越多,并且表中的数据量也会越来越大,那么读写操作的开销也会随着增大。 - -**3. 垂直切分** - -将表按功能模块、关系密切程度划分出来,部署到不同的库上。例如,我们会建立商品数据库 payDB、用户数据库 userDB 等,分别用来存储项目与商品有关的表和与用户有关的表。 - -**4. 水平切分** - -把表中的数据按照某种规则存储到多个结构相同的表中,例如按 id 的散列值、性别等进行划分, - -**5. 垂直切分与水平切分的选择** - -如果数据库中的表太多,并且项目各项业务逻辑清晰,那么垂直切分是首选。 - -如果数据库的表不多,但是单表的数据量很大,应该选择水平切分。 - -**6. 水平切分的实现方式** - -最简单的是使用 merge 存储引擎。 - -**7. 分库与分表存在的问题** - -(1) 事务问题 - -在执行分库分表之后,由于数据存储到了不同的库上,数据库事务管理出现了困难。如果依赖数据库本身的分布式事务管理功能去执行事务,将付出高昂的性能代价;如果由应用程序去协助控制,形成程序逻辑上的事务,又会造成编程方面的负担。 - -(2) 跨库跨表连接问题 - -在执行了分库分表之后,难以避免会将原本逻辑关联性很强的数据划分到不同的表、不同的库上。这时,表的连接操作将受到限制,我们无法连接位于不同分库的表,也无法连接分表粒度不同的表,导致原本只需要一次查询就能够完成的业务需要进行多次才能完成。 - -# 故障转移和故障恢复 - -故障转移也叫做切换,当主库出现故障时就切换到备库,使备库成为主库。故障恢复顾名思义就是从故障中恢复过来,并且保证数据的正确性。 - -## 1. 故障转移 - -**1.1 提升备库或切换角色** - -提升一台备库为主库,或者在一个主-主复制结构中调整主动和被动角色。 - -**1.2 虚拟 IP 地址和 IP 托管** - -为 MySQL 实例指定一个逻辑 IP 地址,当 MySQL 实例失效时,可以将 IP 地址转移到另一台 MySQL 服务器上。 - -**1.3 中间件解决方案** - -通过代理,可以路由流量到可以使用的服务器上。 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//fabd5fa0-b75e-48d0-9e2c-31471945ceb9.jpg) - -**1.4 在应用中处理故障转移** - -将故障转移整合到应用中可能导致应用变得太过笨拙。 - -## 2. 故障恢复 - - -# 参考资料 - -- 高性能 MySQL -- [MySQL 索引背后的数据结构及算法原理 ](http://blog.codinglabs.org/articles/theory-of-mysql-index.html) -- [MySQL 索引优化全攻略 ](http://www.runoob.com/w3cnote/mysql-index.html) -- [20+ 条 MySQL 性能优化的最佳经验 ](https://www.jfox.info/20-tiao-mysql-xing-nen-you-hua-de-zui-jia-jing-yan.html) diff --git a/notes/SQL 语法.md b/notes/SQL 语法.md deleted file mode 100644 index eea75f44..00000000 --- a/notes/SQL 语法.md +++ /dev/null @@ -1,735 +0,0 @@ - -* [基础](#基础) -* [查询](#查询) -* [排序](#排序) -* [过滤](#过滤) -* [通配符](#通配符) -* [计算字段](#计算字段) -* [函数](#函数) - * [文本处理](#文本处理) - * [日期和时间处理](#日期和时间处理) - * [数值处理](#数值处理) - * [汇总](#汇总) -* [分组](#分组) -* [子查询](#子查询) -* [连接](#连接) - * [内连接](#内连接) - * [自连接](#自连接) - * [自然连接](#自然连接) - * [外连接](#外连接) -* [组合查询](#组合查询) -* [插入](#插入) -* [更新](#更新) -* [删除](#删除) -* [创建表](#创建表) -* [修改表](#修改表) -* [视图](#视图) -* [存储过程](#存储过程) -* [游标](#游标) -* [触发器](#触发器) -* [事务处理](#事务处理) -* [字符集](#字符集) -* [权限管理](#权限管理) - - - -# 基础 - -模式:定义了数据如何存储、存储什么样的数据以及数据如何分解等信息,数据库和表都有模式。 - -主键的值不允许修改,也不允许复用(不能使用已经删除的主键值赋给新数据行的主键)。 - -SQL(Structured Query Language),标准 SQL 由 ANSI 标准委员会管理,从而称为 ANSI SQL,各个 DBMS 都有自己的实现,如 PL/SQL、Transact-SQL 等。 - -# 查询 - -SQL 语句不区分大小写,但是数据库表名、列名和值是否区分依赖于具体的 DBMS 以及配置。 - -**DISTINCT** - -相同值只会出现一次。它作用于所有列,也就是说所有列的值都相同才算相同。 - -```sql -SELECT DISTINCT col1, col2 -FROM mytable; -``` - -**LIMIT** - -限制返回的行数。可以有两个参数,第一个参数为起始行,从 0 开始;第二个参数为返回的总行数。 - -返回前 5 行的 SQL: - -```sql -SELECT * -FROM mytable -LIMIT 5; -``` - -```sql -SELECT * -FROM mytable -LIMIT 0, 5; -``` - -返回第 3 \~ 5 行: - -```sql -SELECT * -FROM mytable -LIMIT 2, 3; -``` - -**注释** - -```sql -# 注释 -SELECT * -FROM mytable -- 注释 -/* 注释1 - 注释2 */ -``` - -# 排序 - -**ASC**:升序(默认) -**DESC**:降序 - -可以按多个列进行排序: - -```sql -SELECT * -FROM mytable -ORDER BY col1 DESC, col2 ASC; -``` - -# 过滤 - -在应用层也可以过滤数据,但是不在服务器端进行过滤的数据非常大,导致通过网络传输了很多多余的数据,从而浪费了网络带宽。 - -```sql -SELECT * -FROM mytable -WHERE col IS NULL; -``` - -下表显示了 WHERE 子句可用的操作符 - -| 操作符 | 说明 | -| ------------ | ------------ | -| = < > | 等于 小于 大于 | -| <> != | 不等于 | -| <= !> | 小于等于 | -| >= !< | 大于等于 | -| BETWEEN | 在两个值之间 | -| IS NULL | 为NULL值 | - -应该注意到,NULL 与 0 、空字符串都不同。 - -**AND OR** 用于连接多个过滤条件。优先处理 AND,因此当一个过滤表达式涉及到多个 AND 和 OR 时,应当使用 () 来决定优先级。 - -**IN** 操作符用于匹配一组值,其后也可以接一个 SELECT 子句,从而匹配子查询得到的一组值。 - -**NOT** 操作符用于否定一个条件。 - -# 通配符 - -通配符也是用在过滤语句中,只能用于文本字段。 - -- **%** 匹配 >=0 个任意字符,类似于 \*; - -- **\_** 匹配 ==1 个任意字符,类似于 \.; - -- **[ ]** 可以匹配集合内的字符,用脱字符 ^ 可以对其进行否定 - -使用 Like 来进行通配符匹配。 - -```sql -SELECT * -FROM mytable -WHERE col LIKE '[^AB]%' -- 不以AB开头的任意文本 -``` - -不要滥用通配符,通配符位于开头处匹配会非常慢。 - -# 计算字段 - -在数据库服务器上完成数据的转换和格式化的工作往往比客户端上快得多,并且转换和格式化后的数据量更少的话可以减少网络通信量。 - -计算字段通常需要使用 **AS** 来取别名,否则输出的时候字段名为计算表达式。 - -```sql -SELECT col1*col2 AS alias -FROM mytable -``` - -**Concat()** 用于连接两个字段。许多数据库会使用空格把一个值填充为列宽,因此连接的结果会出现一些不必要的空格,使用 **TRIM()** 可以去除首尾空格。 - -```sql -SELECT Concat(TRIM(col1), ' (', TRIM(col2), ')') -FROM mytable -``` - -# 函数 - -各个 DBMS 的函数都是不相同的,因此不可移植。 - -## 文本处理 - -| 函数 | 说明 | -| ------------ | ------------ | -| LEFT() RIGHT() | 左边或者右边的字符 | -| LOWER() UPPER() | 转换为小写或者大写 | -| LTRIM() RTIM() | 去除左边或者右边的空格 | -| LENGTH() | 长度 | -| SUNDEX() | 转换为语音值 | - -其中,**SOUNDEX()** 是将一个字符串转换为描述其语音表示的字母数字模式的算法,它是根据发音而不是字母比较。 - -```sql -SELECT * -FROM mytable -WHERE SOUNDEX(col1) = SOUNDEX('apple') -``` -## 日期和时间处理 - -日期格式:YYYY-MM-DD - -时间格式:HH:MM:SS - -|函 数 | 说 明| -| --- | --- | -| AddDate() | 增加一个日期(天、周等)| -| AddTime() | 增加一个时间(时、分等)| -| CurDate() | 返回当前日期 | -| CurTime() | 返回当前时间 | -|Date() |返回日期时间的日期部分| -|DateDiff() |计算两个日期之差| -|Date_Add() |高度灵活的日期运算函数| -|Date_Format() |返回一个格式化的日期或时间串| -|Day()| 返回一个日期的天数部分| -|DayOfWeek() |对于一个日期,返回对应的星期几| -|Hour() |返回一个时间的小时部分| -|Minute() |返回一个时间的分钟部分| -|Month() |返回一个日期的月份部分| -|Now() |返回当前日期和时间| -|Second() |返回一个时间的秒部分| -|Time() |返回一个日期时间的时间部分| -|Year() |返回一个日期的年份部分| - -```sql -mysql> SELECT NOW(); - -> '2017-06-28 14:01:52' -``` - -## 数值处理 - -| 函数 | 说明 | -| --- | --- | -| SIN() | 正弦 | -|COS() | 余弦 | -| TAN() | 正切 | -| ABS() | 绝对值 | -| SQRT() | 平方根| -| MOD() | 余数| -| EXP() | 指数| -| PI() | 圆周率| -|RAND() | 随机数| - -## 汇总 - -|函 数 |说 明| -| --- | --- | -|AVG() |返回某列的平均值| -|COUNT()| 返回某列的行数| -|MAX()| 返回某列的最大值| -|MIN()| 返回某列的最小值| -|SUM() |返回某列值之和| - -AVG() 会忽略 NULL 行。 - -DISTINCT 关键字会只汇总不同的值。 - -```sql -SELECT AVG(DISTINCT col1) AS avg_col -FROM mytable -``` - -# 分组 - -分组就是把相同的数据放在同一组中。 - -可以对每组数据使用汇总函数进行处理,例如求每组数的平均值等。 - -按 col 排序并分组数据: - -```sql -SELECT col, COUNT(*) AS num -FROM mytable -GROUP BY col; -``` - -WHERE 过滤行,HAVING 过滤分组,行过滤应当先与分组过滤; - -```sql -SELECT col, COUNT(*) AS num -FROM mytable -WHERE col > 2 -GROUP BY col -HAVING COUNT(*) >= 2; -``` - -GROUP BY 的排序结果为分组字段,而 ORDER BY 也可以以聚集字段来进行排序。 - -```sql -SELECT col, COUNT(*) AS num -FROM mytable -GROUP BY col -ORDER BY num; -``` - -分组规定: - -1. GROUP BY 子句出现在 WHERE 子句之后,ORDER BY 子句之前; -2. 除了汇总计算语句之外,SELECT 语句中的每一列都必须在 GROUP BY 子句中给出; -3. NULL 的行会单独分为一组; -4. 大多数 SQL 实现不支持 GROUP BY 列具有可变长度的数据类型。 - -# 子查询 - -子查询中只能返回一个列。 - -可以将子查询的结果作为 WHRER 语句的过滤条件: - -``` -SELECT * -FROM mytable1 -WHERE col1 IN (SELECT col2 - FROM mytable2); -``` - -下面的语句可以检索出客户的订单数量。子查询语句会对检索出的每个客户执行一次: - -```sql -SELECT cust_name, (SELECT COUNT(*) - FROM Orders - WHERE Orders.cust_id = Customers.cust_id) - AS orders_num -FROM Customers -ORDER BY cust_name; -``` - -# 连接 - -连接用于连接多个表,使用 JOIN 关键字,并且条件语句使用 ON。 - -连接可以替换子查询,并且比子查询的效率一般会更快。 - -可以用 AS 给列名、计算字段和表名取别名,给表名取别名是为了简化 SQL 语句以及连接相同表。 - -## 内连接 - -内连接又称等值连接,使用 INNER JOIN 关键字。 - -``` -select a, b, c -from A inner join B -on A.key = B.key -``` - -可以不明确使用 INNER JOIN,而使用普通查询并在 WHERE 中将两个表中要连接的列用等值方法连接起来。 - -``` -select a, b, c -from A, B -where A.key = B.key -``` - -在没有条件语句的情况下返回笛卡尔积。 - -## 自连接 - -自连接可以看成内连接的一种,只是连接的表是自身而已。 - -一张员工表,包含员工姓名和员工所属部门,要找出与 Jim 处在同一部门的所有员工姓名。 - -**子查询版本** - -``` -select name -from employee -where department = ( - select department - from employee - where name = "Jim"); -``` - -**自连接版本** - -``` -select name -from employee as e1, employee as e2 -where e1.department = e2.department - and e1.name = "Jim"; -``` - -连接一般比子查询的效率高。 - -## 自然连接 - -自然连接是把同名列通过等值测试连接起来的,同名列可以有多个。 - -内连接和自然连接的区别:内连接提供连接的列,而自然连接自动连接所有同名列;内连接属于自然连接。 - -``` -select * -from employee natural join department; -``` - -## 外连接 - -外连接保留了没有关联的那些行。分为左外连接,右外连接以及全外连接,左外连接就是保留左表的所有行。 - -检索所有顾客的订单信息,包括还没有订单信息的顾客。 - -``` -select Customers.cust_id, Orders.order_num - from Customers left outer join Orders - on Customers.cust_id = Orders.curt_id -``` - -如果需要统计顾客的订单数,使用聚集函数。 - -``` -select Customers.cust_id, - COUNT(Orders.order_num) as num_ord -from Customers left outer join Orders -on Customers.cust_id = Orders.curt_id -group by Customers.cust_id -``` - -# 组合查询 - -使用 **UNION** 来连接两个查询,每个查询必须包含相同的列、表达式或者聚集函数。 - -默认会去除相同行,如果需要保留相同行,使用 UNION ALL 。 - -只能包含一个 ORDER BY 子句,并且必须位于语句的最后。 - -```sql -SELECT col -FROM mytable -WHERE col = 1 -UNION -SELECT col -FROM mytable -WHERE col =2; -``` - -# 插入 - -**普通插入** - -```sql -INSERT INTO mytable(col1, col2) -VALUES(val1, val2); -``` - -**插入检索出来的数据** - -```sql -INSERT INTO mytable1(col1, col2) -SELECT col1, col2 -FROM mytable2; -``` - -**将一个表的内容复制到一个新表** - -```sql -CREATE TABLE newtable AS -SELECT * FROM mytable; -``` - -# 更新 - -```sql -UPDATE mytable -SET col = val -WHERE id = 1; -``` - -# 删除 - -```sql -DELETE FROM mytable -WHERE id = 1; -``` - -**TRUNCATE TABLE** 可以清空表,也就是删除所有行。 - -使用更新和删除操作时一定要用 WHERE 子句,不然会把整张表的数据都破坏。可以先用 SELECT 语句进行测试,防止错误删除。 - -# 创建表 - -```sql -CREATE TABLE mytable ( - id INT NOT NULL AUTO_INCREMENT, - col1 INT NOT NULL DEFAULT 1, - col2 VARCHAR(45) NULL, - col3 DATE NULL, - PRIMARY KEY (`id`)); -``` - -# 修改表 - -**添加列** - -```sql -ALTER TABLE mytable -ADD col CHAR(20); -``` - -**删除列** - -```sql -ALTER TABLE mytable -DROP COLUMN col; -``` - -**删除表** - -```sql -DROP TABLE mytable; -``` - -# 视图 - -视图是虚拟的表,本身不包含数据,也就不能对其进行索引操作。对视图的操作和对普通表的操作一样。 - -视图具有如下好处: - -1. 简化复杂的 SQL 操作,比如复杂的联结; -2. 只使用实际表的一部分数据; -3. 通过只给用户访问视图的权限,保证数据的安全性; -4. 更改数据格式和表示。 - -```sql -CREATE VIEW myview AS -SELECT Concat(col1, col2) AS concat_col, col3*col4 AS count_col -FROM mytable -WHERE col5 = val; -``` - -# 存储过程 - -存储过程可以看成是对一系列 SQL 操作的批处理; - -**使用存储过程的好处** - -1. 把实现封装在了存储过程中,不仅简单,也保证了安全性; -2. 可以复用代码; -3. 由于是预先编译,因此具有很高的性能。 - -**创建存储过程** - -命令行中创建存储过程需要自定义分隔符,因为命令行是以 ; 为结束符,而存储过程中也包含了分号,因此会错误把这部分分号当成是结束符,造成语法错误。 - -包含 in、out 和 inout 三种参数。 - -给变量赋值都需要用 select into 语句。 - -每次只能给一个变量赋值,不支持集合的操作。 - -```sql -delimiter // - -create procedure myprocedure( out ret int ) - begin - declare y int; - select sum(col1) - from mytable - into y; - select y*y into ret; - end // -delimiter ; -``` - -```sql -call myprocedure(@ret); -select @ret; -``` - -# 游标 - -在存储过程中使用游标可以对一个结果集进行移动遍历。 - -游标主要用于交互式应用,其中用户需要对数据集中的任意行进行浏览和修改。 - -**使用游标的四个步骤:** - -1. 声明游标,这个过程没有实际检索出数据; -2. 打开游标; -3. 取出数据; -4. 关闭游标; - -```sql -delimiter // -create procedure myprocedure(out ret int) - begin - declare done boolean default 0; - - declare mycursor cursor for - select col1 from mytable; - # 定义了一个continue handler,当 sqlstate '02000' 这个条件出现时,会执行 set done = 1 - declare continue handler for sqlstate '02000' set done = 1; - - open mycursor; - - repeat - fetch mycursor into ret; - select ret; - until done end repeat; - - close mycursor; - end // - delimiter ; -``` - -# 触发器 - -触发器会在某个表执行以下语句时而自动执行:DELETE、INSERT、UPDATE - -触发器必须指定在语句执行之前还是之后自动执行,之前执行使用 BEFORE 关键字,之后执行使用 AFTER 关键字。BEFORE 用于数据验证和净化。 - -INSERT 触发器包含一个名为 NEW 的虚拟表。 - -```sql -CREATE TRIGGER mytrigger AFTER INSERT ON mytable -FOR EACH ROW SELECT NEW.col; -``` - -DELETE 触发器包含一个名为 OLD 的虚拟表,并且是只读的。 - -UPDATE 触发器包含一个名为 NEW 和一个名为 OLD 的虚拟表,其中 NEW 是可以被修改地,而 OLD 是只读的。 - -可以使用触发器来进行审计跟踪,把修改记录到另外一张表中。 - -MySQL 不允许在触发器中使用 CALL 语句 ,也就是不能调用存储过程。 - -# 事务处理 - -**基本术语** - -1. 事务(transaction)指一组 SQL 语句; -2. 回退(rollback)指撤销指定 SQL 语句的过程; -3. 提交(commit)指将未存储的 SQL 语句结果写入数据库表; -4. 保留点(savepoint)指事务处理中设置的临时占位符(placeholder),你可以对它发布回退(与回退整个事务处理不同)。 - -不能回退 SELECT 语句,回退 SELECT 语句也没意义;也不能回退 CRETE 和 DROP 语句。 - -MySQL 的事务提交默认是隐式提交,也就是每执行一条语句就会提交一次。当出现 START TRANSACTION 语句时,会关闭隐式提交;当 COMMIT 或 ROLLBACK 语句执行后,事务会自动关闭,重新恢复隐式提交。 - -通过设置 autocommit 为 0 可以取消自动提交,直到 autocommit 被设置为 1 才会提交;autocommit 标记是针对每个连接而不是针对服务器的。 - -如果没有设置保留点,ROLLBACK 会回退到 START TRANSACTION 语句处;如果设置了保留点,并且在 ROLLBACK 中指定该保留点,则会回退到该保留点。 - -```sql -START TRANSACTION -// ... -SAVEPOINT delete1 -// ... -ROLLBACK TO delete1 -// ... -COMMIT -``` - -# 字符集 - -**基本术语** - -1. 字符集为字母和符号的集合; -2. 编码为某个字符集成员的内部表示; -3. 校对字符指定如何比较,主要用于排序和分组。 - -除了给表指定字符集和校对外,也可以给列指定: - -```sql -CREATE TABLE mytable -(col VARCHAR(10) CHARACTER SET latin COLLATE latin1_general_ci ) -DEFAULT CHARACTER SET hebrew COLLATE hebrew_general_ci; -``` - -可以在排序、分组时指定校对: - -```sql -SELECT * -FROM mytable -ORDER BY col COLLATE latin1_general_ci; -``` - -# 权限管理 - -MySQL 的账户信息保存在 mysql 这个数据库中。 - -```sql -USE mysql; -SELECT user FROM user; -``` - -**创建账户** - -```sql -CREATE USER myuser IDENTIFIED BY 'mypassword'; -``` - -新创建的账户没有任何权限。 - -**修改账户名** - -```sql -RENAME myuser TO newuser; -``` - -**删除账户** - -```sql -DROP USER myuser; -``` - -**查看权限** - -```sql -SHOW GRANTS FOR myuser; -``` -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//c73aa08e-a987-43c9-92be-adea4a884c25.png) - -账户用 username@host 的形式定义,username@% 使用的是默认主机名。 - -**授予权限** - -```sql -GRANT SELECT, INSERT ON mydatabase.* TO myuser; -``` - -**删除权限** - -```sql -REVOKE SELECT, INSERT ON mydatabase.* FROM myuser; -``` - -GRANT 和 REVOKE 可在几个层次上控制访问权限: - -- 整个服务器,使用 GRANT ALL和 REVOKE ALL; -- 整个数据库,使用 ON database.\*; -- 特定的表,使用 ON database.table; -- 特定的列; -- 特定的存储过程。 - -**更改密码** - -必须使用 Password() 函数 - -```sql -SET PASSWROD FOR myuser = Password('newpassword'); -``` - diff --git a/notes/代码可读性.md b/notes/代码可读性.md deleted file mode 100644 index 47af4412..00000000 --- a/notes/代码可读性.md +++ /dev/null @@ -1,341 +0,0 @@ - -* [可读性的重要性](#可读性的重要性) -* [用名字表达代码含义](#用名字表达代码含义) -* [名字不能带来歧义](#名字不能带来歧义) -* [良好的代码风格](#良好的代码风格) -* [编写注释](#编写注释) -* [如何编写注释](#如何编写注释) -* [提高控制流的可读性](#提高控制流的可读性) -* [拆分长表达式](#拆分长表达式) -* [变量与可读性](#变量与可读性) -* [抽取函数](#抽取函数) -* [一次只做一件事](#一次只做一件事) -* [用自然语言表述代码](#用自然语言表述代码) -* [减少代码量](#减少代码量) - - - -# 可读性的重要性 - -编程有很大一部分时间是在阅读代码,不仅要阅读自己的代码,而且要阅读别人的代码。因此,可读性良好的代码能够大大提高编程效率。 - -可读性良好的代码往往会让代码架构更好,因为程序员更愿意去修改这部分代码,而且也更容易修改。 - -只有在核心领域为了效率才可以放弃可读性,否则可读性是第一位。 - -# 用名字表达代码含义 - -一些比较有表达力的单词: - -| 单词 | 可替代单词 | -| --- | --- | -| send | deliver、dispatch、announce、distribute、route | -| find | search、extract、locate、recover | -| start| launch、create、begin、open| -| make | create、set up、build、generate、compose、add、new | - -使用 i、j、k 作为循环迭代器的名字过于简单,user_i、member_i 这种名字会更有表达力。因为循环层次越多,代码越难理解,有表达力的迭代器名字可读性会更高 - -为名字添加形容词等信息能让名字更具有表达力,但是名字也会变长。名字长短的准则是:作用域越大,名字越长。因此只有在短作用域才能使用一些简单名字。 - -# 名字不能带来歧义 - -起完名字要思考一下别人会对这个名字有何解读,会不会误解了原本想表达的含义。 - -用 min、max 表示数量范围;用 first、last 表示访问空间的包含范围,begin、end 表示访问空间的排除范围,即 end 不包含尾部。 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//05907ab4-42c5-4b5e-9388-6617f6c97bea.jpg) - -布尔相关的命名加上 is、can、should、has 等前缀。 - -# 良好的代码风格 - -适当的空行和缩进。 - -排列整齐的注释: - -``` -int a = 1; // 注释 -int b = 11; // 注释 -int c = 111; // 注释 -``` - -语句顺序不能随意,比如与 html 表单相关联的变量的赋值应该和表单在 html 中的顺序一致; - -把相关的代码按块组织起来放在一起。 - -# 编写注释 - -阅读代码首先会注意到注释,如果注释没太大作用,那么就会浪费代码阅读的时间。那些能直接看出含义的代码不需要写注释,特别是并不需要为每个方法都加上注释,比如那些简单的 getter 和 setter 方法,为这些方法写注释反而让代码可读性更差。 - -不能因为有注释就随便起个名字,而是争取起个好名字而不写注释。 - -可以用注释来记录采用当前解决办法的思考过程,从而让读者更容易理解代码。 - -注释用来提醒一些特殊情况。 - -用 TODO 等做标记: - -| 标记 | 用法 | -|---|---| -|TODO| 待做 | -|FIXME| 待修复 | -|HACH| 粗糙的解决方案 | -|XXX| 危险!这里有重要的问题 | - -# 如何编写注释 - -尽量简洁明了: - -``` -// The first String is student's name -// The Second Integer is student's score -Map scoreMap = new HashMap<>(); -``` - -``` -// Student' name -> Student's score -Map scoreMap = new HashMap<>(); -``` - -添加测试用例来说明: - -``` -//... -// Example: add(1, 2), return 3 -int add(int x, int y) { - return x + y; -} -``` - -在很复杂的函数调用中对每个参数标上名字: - -``` -int a = 1; -int b = 2; -int num = add(\* x = *\ a, \* y = *\ b); -``` - -使用专业名词来缩短概念上的解释,比如用设计模式名来说明代码。 - -# 提高控制流的可读性 - -条件表达式中,左侧是变量,右侧是常数。比如下面第一个语句正确: - -``` -if(len < 10) -if(10 > len) -``` - -if / else 条件语句,逻辑的处理顺序为:① 正逻辑;② 关键逻辑;③ 简单逻辑。 -``` -if(a == b) { - // 正逻辑 -} else{ - // 反逻辑 -} -``` - -只有在逻辑简单的情况下使用 ? : 三目运算符来使代码更紧凑,否则应该拆分成 if / else; - -do / while 的条件放在后面,不够简单明了,并且会有一些迷惑的地方,最好使用 while 来代替。 - -如果只有一个 goto 目标,那么 goto 尚且还能接受,但是过于复杂的 goto 会让代码可读性特别差,应该避免使用 goto。 - -在嵌套的循环中,用一些 return 语句往往能减少嵌套的层数。 - -# 拆分长表达式 - -长表达式的可读性很差,可以引入一些解释变量从而拆分表达式: - -``` -if line.split(':')[0].strip() == "root": - ... -``` -``` -username = line.split(':')[0].strip() -if username == "root": - ... -``` - -使用摩根定理简化一些逻辑表达式: - -``` -if(!a && !b) { - ... -} -``` -``` -if(a || b) { - ... -} -``` - -# 变量与可读性 - -**去除控制流变量**。在循环中通过使用 break 或者 return 可以减少控制流变量的使用。 - -``` -boolean done = false; -while(/* condition */ && !done) { - ... - if(...) { - done = true; - continue; - } -} -``` -``` -while(/* condition */) { - ... - if(...) { - break; - } -} -``` - -**减小变量作用域**。作用域越小,越容易定位到变量所有使用的地方。 - -JavaScript 可以用闭包减小作用域。以下代码中 submit_form 是函数变量,submitted 变量控制函数不会被提交两次。第一个实现中 submitted 是全局变量,第二个实现把 submitted 放到匿名函数中,从而限制了起作用域范围。 - -``` -submitted = false; -var submit_form = function(form_name) { - if(submitted) { - return; - } - submitted = true; -}; -``` - -``` -var submit_form = (function() { - var submitted = false; - return function(form_name) { - if(submitted) { - return; - } - submitted = true; - } -}()); // () 使得外层匿名函数立即执行 -``` - -JavaScript 中没有用 var 声明的变量都是全局变量,而全局变量很容易造成迷惑,因此应当总是用 var 来声明变量。 - -变量定义的位置应当离它使用的位置最近。 - -**实例解析** - -在一个网页中有以下文本输入字段: - -``` - - - - -``` - -现在要接受一个字符串并把它放到第一个空的 input 字段中,初始实现如下: - -``` -var setFirstEmptyInput = function(new_alue) { - var found = false; - var i = 1; - var elem = document.getElementById('input' + i); - while(elem != null) { - if(elem.value === '') { - found = true; - break; - } - i++; - elem = document.getElementById('input' + i); - } - if(found) elem.value = new_value; - return elem; -} -``` - -以上实现有以下问题: - -- found 可以去除; -- elem 作用域过大; -- 可以用 for 循环代替 while 循环; - -``` -var setFirstEmptyInput = function(new_value) { - for(var i = 1; true; i++) { - var elem = document.getElementById('input' + i); - if(elem === null) { - return null; - } - if(elem.value === '') { - elem.value = new_value; - return elem; - } - } -}; -``` - -# 抽取函数 - -工程学就是把大问题拆分成小问题再把这些问题的解决方案放回一起。 - -首先应该明确一个函数的高层次目标,然后对于不是直接为了这个目标工作的代码,抽取出来放到独立的函数中。 - -介绍性的代码: - -``` -int findClostElement(int[] arr) { - int clostIdx; - int clostDist = Interger.MAX_VALUE; - for(int i = 0; i < arr.length; i++) { - int x = ...; - int y = ...; - int z = ...; - int value = x * y * z; - int dist = Math.sqrt(Math.pow(value, 2), Math.pow(arr[i], 2)); - if(dist < clostDist) { - clostIdx = i; - clostDist = value; - } - } - return clostIdx; -} -``` - -以上代码中循环部分主要计算距离,这部分不属于代码高层次目标,高层次目标是寻找最小距离的值,因此可以把这部分代替提取到独立的函数中。这样做也带来一个额外的好处有:可以单独进行测试、可以快速找到程序错误并修改。 - -``` -public int findClostElement(int[] arr) { - int clostIdx; - int clostDist = Interger.MAX_VALUE; - for(int i = 0; i < arr.length; i++) { - int dist = computDist(arr, i); - if(dist < clostDist) { - clostIdx = i; - clostDist = value; - } - } - return clostIdx; -} -``` - -并不是函数抽取的越多越好,如果抽取过多,在阅读代码的时候可能需要不断跳来跳去。只有在当前函数不需要去了解某一块代码细节而能够表达其内容时,把这块代码抽取成子函数才是好的。 - -函数抽取也用于减小代码的冗余。 - -# 一次只做一件事 - -只做一件事的代码很容易让人知道其要做的事; - -基本流程:列出代码所做的所有任务;把每个任务拆分到不同的函数,或者不同的段落。 - -# 用自然语言表述代码 - -先用自然语言书写代码逻辑,也就是伪代码,然后再写代码,这样代码逻辑会更清晰。 - -# 减少代码量 - -不要过度设计,编码过程会有很多变化,过度设计的内容到最后往往是无用的。 - -多用标准库实现。 diff --git a/notes/代码风格规范.md b/notes/代码风格规范.md deleted file mode 100644 index 6724fa14..00000000 --- a/notes/代码风格规范.md +++ /dev/null @@ -1,14 +0,0 @@ -# Google Java Style Guide - -- http://www.hawstein.com/posts/google-java-style.html -- http://google.github.io/styleguide/javaguide.html - -# Google C++ Style Guide - -- http://zh-google-styleguide.readthedocs.io/en/latest/google-cpp-styleguide/contents/ -- http://google.github.io/styleguide/cppguide.html - -# Google Python Style Guide - -- http://zh-google-styleguide.readthedocs.io/en/latest/google-python-styleguide/contents/ -- http://google.github.io/styleguide/pyguide.html diff --git a/notes/剑指 offer 题解.md b/notes/剑指 offer 题解.md deleted file mode 100644 index a2fd402c..00000000 --- a/notes/剑指 offer 题解.md +++ /dev/null @@ -1,1817 +0,0 @@ - -* [第二章 面试需要的基础知识](#第二章-面试需要的基础知识) - * [2. 实现 Singleton](#2-实现-singleton) - * [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-数值的整数次方) - * [18.2 删除链表中重复的结点](#182-删除链表中重复的结点) - * [19. 正则表达式匹配](#19-正则表达式匹配) - * [20. 表示数值的字符串](#20-表示数值的字符串) - * [21. 调整数组顺序使奇数位于偶数前面](#21-调整数组顺序使奇数位于偶数前面) - * [22. 链表中倒数第 k 个结点](#22-链表中倒数第-k-个结点) - * [23. 链表中环的入口结点](#23-链表中环的入口结点) - * [24. 反转链表](#24-反转链表) - * [25. 合并两个排序的链表](#25-合并两个排序的链表) - * [26. 树的子结构](#26-树的子结构) -* [第四章 解决面试题的思路](#第四章-解决面试题的思路) - * [27. 二叉树的镜像](#27-二叉树的镜像) - * [28.1 对称的二叉树](#281-对称的二叉树) - * [28.2 平衡二叉树](#282-平衡二叉树) - * [29. 顺时针打印矩阵](#29-顺时针打印矩阵) - * [30. 包含 min 函数的栈](#30-包含-min-函数的栈) - * [31. 栈的压入、弹出序列](#31-栈的压入弹出序列) - * [32.1 从上往下打印二叉树](#321-从上往下打印二叉树) - * [32.3 把二叉树打印成多行](#323--把二叉树打印成多行) - * [32.3 按之字形顺序打印二叉树](#323-按之字形顺序打印二叉树) - * [33. 二叉搜索树的后序遍历序列](#33-二叉搜索树的后序遍历序列) - * [34. 二叉树中和为某一值的路径](#34-二叉树中和为某一值的路径) - * [35. 复杂链表的复制](#35-复杂链表的复制) - * [36. 二叉搜索树与双向链表](#36-二叉搜索树与双向链表) - * [37. 序列化二叉树](#37-序列化二叉树) - * [38. 字符串的排列](#38-字符串的排列) -* [第五章 优化时间和空间效率](#第五章-优化时间和空间效率) - * [39. 数组中出现次数超过一半的数字](#39-数组中出现次数超过一半的数字) - * [40. 最小的 K 个数](#40-最小的-k-个数) - * [41.1 数据流中的中位数](#411-数据流中的中位数) - * [14.2 字符流中第一个不重复的字符](#142-字符流中第一个不重复的字符) - * [42. 连续子数组的最大和](#42-连续子数组的最大和) - * [43. 从 1 到 n 整数中 1 出现的次数](#43-从-1-到-n-整数中-1-出现的次数) - * [45. 把数组排成最小的数](#45-把数组排成最小的数) - * [49. 丑数](#49-丑数) - * [50. 第一个只出现一次的字符位置](#50-第一个只出现一次的字符位置) - * [51. 数组中的逆序对](#51-数组中的逆序对) - * [52. 两个链表的第一个公共结点](#52-两个链表的第一个公共结点) -* [第六章 面试中的各项能力](#第六章-面试中的各项能力) - * [53 数字在排序数组中出现的次数](#53-数字在排序数组中出现的次数) - * [54. 二叉搜索树的第 k 个结点](#54-二叉搜索树的第-k-个结点) - * [55 二叉树的深度](#55-二叉树的深度) - * [56. 数组中只出现一次的数字](#56-数组中只出现一次的数字) - * [57.1 和为 S 的两个数字](#571-和为-s-的两个数字) - * [57.2 和为 S 的连续正数序列](#572-和为-s-的连续正数序列) - * [58.1 翻转单词顺序列](#581-翻转单词顺序列) - * [58.2 左旋转字符串](#582-左旋转字符串) - * [59. 滑动窗口的最大值](#59-滑动窗口的最大值) - * [61. 扑克牌顺子](#61-扑克牌顺子) - * [62. 圆圈中最后剩下的数](#62-圆圈中最后剩下的数) - * [63. 股票的最大利润](#63-股票的最大利润) - * [64. 求 1+2+3+...+n](#64-求-1+2+3++n) - * [65. 不用加减乘除做加法](#65-不用加减乘除做加法) - * [66. 构建乘积数组](#66-构建乘积数组) -* [第七章 两个面试案例](#第七章-两个面试案例) - * [67. 把字符串转换成整数](#67-把字符串转换成整数) - * [68. 树中两个节点的最低公共祖先](#68-树中两个节点的最低公共祖先) - - - -# 第二章 面试需要的基础知识 - -## 2. 实现 Singleton - -[单例模式](https://github.com/CyC2018/InterviewNotes/blob/master/notes/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F.md#%E7%AC%AC-5-%E7%AB%A0-%E5%8D%95%E4%BB%B6%E6%A8%A1%E5%BC%8F) - -## 3. 数组中重复的数字 - -**题目描述** - -在一个长度为 n 的数组里的所有数字都在 0 到 n-1 的范围内。数组中某些数字是重复的,但不知道有几个数字是重复的。也不知道每个数字重复几次。请找出数组中任意一个重复的数字。例如,如果输入长度为 7 的数组 {2, 3, 1, 0, 2, 5, 3},那么对应的输出是第一个重复的数字 2。 - -**解题思路** - -这种数组元素在 [0, n-1] 范围内的问题,可以将值为 i 的元素放到第 i 个位置上。 - -```java -public boolean duplicate(int numbers[], int length, int[] duplication) { - if(numbers == null || length <= 0) return false; - for (int i = 0; i < length; i++) { - while (numbers[i] != i && numbers[i] != numbers[numbers[i]]) { - swap(numbers, i, numbers[i]); - } - if (numbers[i] != i && numbers[i] == numbers[numbers[i]]) { - duplication[0] = numbers[i]; - return true; - } - } - return false; -} - -private void swap(int[] numbers, int i, int j) { - int t = numbers[i]; - numbers[i] = numbers[j]; - numbers[j] = t; -} -``` - -## 4. 二维数组中的查找 - -**题目描述** - -在一个二维数组中,每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺序排序。请完成一个函数,输入这样的一个二维数组和一个整数,判断数组中是否含有该整数。 - -**解题思路** - -从右上角开始查找。因为矩阵中的一个数,它左边的数都比它来的小,下边的数都比它来的大。因此,从右上角开始查找,就可以根据 target 和当前元素的大小关系来改变行和列的下标,从而缩小查找区间。 - -```java -public boolean Find(int target, int [][] array) { - if (array == null || array.length == 0 || array[0].length == 0) return false; - int m = array.length, n = array[0].length; - int row = 0, col = n - 1; - while (row < m && col >= 0) { - if (target == array[row][col]) return true; - else if (target < array[row][col]) col--; - else row++; - } - return false; -} -``` - -## 5. 替换空格 - -**题目描述** - -请实现一个函数,将一个字符串中的空格替换成“%20”。例如,当字符串为 We Are Happy. 则经过替换之后的字符串为 We%20Are%20Happy。 - -**题目要求** - -以 O(1) 的空间复杂度和 O(n) 的空间复杂度来求解。 - -**解题思路** - -从后向前改变字符串。 - -```java -public String replaceSpace(StringBuffer str) { - int n = str.length(); - for (int i = 0; i < n; i++) { - if (str.charAt(i) == ' ') str.append(" "); // 尾部填充两个 - } - - int idxOfOriginal = n - 1; - int idxOfNew = str.length() - 1; - while (idxOfOriginal >= 0 && idxOfNew > idxOfOriginal) { - if (str.charAt(idxOfOriginal) == ' ') { - str.setCharAt(idxOfNew--, '0'); - str.setCharAt(idxOfNew--, '2'); - str.setCharAt(idxOfNew--, '%'); - } else { - str.setCharAt(idxOfNew--, str.charAt(idxOfOriginal)); - } - idxOfOriginal--; - } - return str.toString(); -} -``` - -## 6. 从尾到头打印链表 - -正向遍历然后调用 Collections.reverse()。 - -```java -public ArrayList printListFromTailToHead(ListNode listNode) { - ArrayList ret = new ArrayList<>(); - while (listNode != null) { - ret.add(listNode.val); - listNode = listNode.next; - } - Collections.reverse(ret); - return ret; -} -``` - -使用 Stack - -```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; -} -``` - -递归 - -```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); // 头结点 - ListNode cur = listNode; - while (cur != null) { - ListNode next = cur.next; - cur.next = head.next; - head.next = cur; - cur = next; - } - ArrayList ret = new ArrayList<>(); - head = head.next; - while (head != null) { - ret.add(head.val); - head = head.next; - } - return ret; -} -``` - -## 7. 重建二叉树 - -**题目描述** - -根据二叉树的前序遍历和中序遍历的结果,重建出该二叉树。 - -```java -public TreeNode reConstructBinaryTree(int[] pre, int[] in) { - return reConstructBinaryTree(pre, 0, pre.length - 1, in, 0, in.length - 1); -} - -private TreeNode reConstructBinaryTree(int[] pre, int preL, int preR, int[] in, int inL, int inR) { - if (preL == preR) return new TreeNode(pre[preL]); - if (preL > preR || inL > inR) return null; - TreeNode root = new TreeNode(pre[preL]); - int midIdx = inL; - while (midIdx <= inR && in[midIdx] != root.val) midIdx++; - int leftTreeSize = midIdx - inL; - root.left = reConstructBinaryTree(pre, preL + 1, preL + leftTreeSize, in, inL, inL + leftTreeSize - 1); - root.right = reConstructBinaryTree(pre, preL + leftTreeSize + 1, preR, in, inL + leftTreeSize + 1, inR); - return root; -} -``` - -## 8. 二叉树的下一个结点 - -**题目描述** - -给定一个二叉树和其中的一个结点,请找出中序遍历顺序的下一个结点并且返回。注意,树中的结点不仅包含左右子结点,同时包含指向父结点的指针。 - -**解题思路** - -- 如果一个节点有右子树不为空,那么该节点的下一个节点是右子树的最左节点; -- 否则,向上找第一个左链接指向的树包含该节点的祖先节点。 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//6fec7f56-a685-4232-b03e-c92a8dfba486.png) - -```java -public TreeLinkNode GetNext(TreeLinkNode pNode) { - if (pNode == null) return null; - if (pNode.right != null) { - pNode = pNode.right; - while (pNode.left != null) pNode = pNode.left; - return pNode; - } else { - TreeLinkNode parent = pNode.next; - while (parent != null) { - if (parent.left == pNode) return parent; - pNode = pNode.next; - parent = pNode.next; - } - } - return null; -} -``` - -## 9. 用两个栈实现队列 - -**解题思路** - -添加到栈中的序列顺序会被反转,如果进行两次反转,那么得到的序列依然是正向的。因此,添加的数据需要同时压入两个栈之后才能出栈,这样就能保证出栈的顺序为先进先出。 - -```java -Stack stack1 = new Stack(); -Stack stack2 = new Stack(); - -public void push(int node) { - stack1.push(node); -} - -public int pop() { - if (stack2.isEmpty()) { - while (!stack1.isEmpty()) { - stack2.push(stack1.pop()); - } - } - return stack2.pop(); -} -``` - -## 10.1 斐波那契数列 - -```java -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 跳台阶 - -```java -public int JumpFloor(int target) { - if (target == 1) return 1; - int[] dp = new int[target]; - dp[0] = 1; - dp[1] = 2; - for (int i = 2; i < dp.length; i++) { - dp[i] = dp[i - 1] + dp[i - 2]; - } - return dp[target - 1]; -} -``` - -## 10.3 变态跳台阶 - -```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]; -} -``` - -## 10.4 矩形覆盖 - -**题目描述** - -我们可以用 2\*1 的小矩形横着或者竖着去覆盖更大的矩形。请问用 n 个 2\*1 的小矩形无重叠地覆盖一个 2\*n 的大矩形,总共有多少种方法? - -```java -public int RectCover(int target) { - if (target <= 2) return target; - return RectCover(target - 1) + RectCover(target - 2); -} -``` - -## 11. 旋转数组的最小数字 - -**题目描述** - -把一个数组最开始的若干个元素搬到数组的末尾,我们称之为数组的旋转。输入一个非递减排序的数组的一个旋转,输出旋转数组的最小元素。例如数组 {3, 4, 5, 1, 2} 为 {1, 2, 3, 4, 5} 的一个旋转,该数组的最小值为 1。NOTE:给出的所有元素都大于 0,若数组大小为 0,请返回 0。 - - -O(N) 时间复杂度解法: - -```java -public int minNumberInRotateArray(int[] array) { - if (array.length == 0) return 0; - for (int i = 0; i < array.length - 1; i++) { - if (array[i] > array[i + 1]) return array[i + 1]; - } - return 0; -} -``` - -O(lgN) 时间复杂度解法: - -```java -public int minNumberInRotateArray(int[] array) { - if (array.length == 0) return 0; - int l = 0, r = array.length - 1; - int mid = -1; - while (array[l] >= array[r]) { - if (r - l == 1) return array[r]; - mid = l + (r - l) / 2; - if (array[mid] >= array[l]) l = mid; - else if (array[mid] <= array[r]) r = mid; - } - return array[mid]; -} -``` - -## 12. 矩阵中的路径 - -**题目描述** - -请设计一个函数,用来判断在一个矩阵中是否存在一条包含某字符串所有字符的路径。路径可以从矩阵中的任意一个格子开始,每一步可以在矩阵中向左,向右,向上,向下移动一个格子。如果一条路径经过了矩阵中的某一个格子,则该路径不能再进入该格子。例如 a b c e s f c s a d e e 矩阵中包含一条字符串 "bcced" 的路径,但是矩阵中不包含 "abcb" 路径,因为字符串的第一个字符 b 占据了矩阵中的第一行第二个格子之后,路径不能再次进入该格子。 - -```java -private int[][] next = {{0, -1}, {0, 1}, {-1, 0}, {1, 0}}; -private int rows; -private int cols; - -public boolean hasPath(char[] matrix, int rows, int cols, char[] str) { - if (rows == 0 || cols == 0) return false; - this.rows = rows; - this.cols = cols; - // 一维数组重建二维矩阵 - char[][] newMatrix = new char[rows][cols]; - for (int i = 0, idx = 0; i < rows; i++) { - for (int j = 0; j < cols; j++) { - newMatrix[i][j] = matrix[idx++]; - } - } - for (int i = 0; i < rows; i++) { - for (int j = 0; j < cols; j++) { - if (backtracking(newMatrix, str, new boolean[rows][cols], 0, i, j)) return true; - } - } - return false; -} - -private boolean backtracking(char[][] matrix, char[] str, boolean[][] used, int pathLen, int curR, int curC) { - if (pathLen == str.length) return true; - if (curR < 0 || curR >= rows || curC < 0 || curC >= cols) return false; - if (matrix[curR][curC] != str[pathLen]) return false; - if (used[curR][curC]) return false; - used[curR][curC] = true; - for (int i = 0; i < next.length; i++) { - if (backtracking(matrix, str, used, pathLen + 1, curR + next[i][0], curC + next[i][1])) - return true; - } - used[curR][curC] = false; - return false; -} -``` - - -## 13. 机器人的运动范围 - -**题目描述** - -地上有一个 m 行和 n 列的方格。一个机器人从坐标 (0,0) 的格子开始移动,每一次只能向左右上下四个方向移动一格,但是不能进入行坐标和列坐标的数位之和大于 k 的格子。例如,当 k 为 18 时,机器人能够进入方格(35,37),因为 3+5+3+7=18。但是,它不能进入方格(35,38),因为 3+5+3+8=19。请问该机器人能够达到多少个格子? - -```java -private int cnt = 0; -private int[][] next = {{0, -1}, {0, 1}, {-1, 0}, {1, 0}}; -private int[][] digitSum; - -public int movingCount(int threshold, int rows, int cols) { - initDigitSum(rows, cols); - dfs(new boolean[rows][cols], threshold, rows, cols, 0, 0); - return cnt; -} - -private void dfs(boolean[][] visited, int threshold, int rows, int cols, int r, int c) { - if (r < 0 || r >= rows || c < 0 || c >= cols) return; - if (visited[r][c]) return; - visited[r][c] = true; - if (this.digitSum[r][c] > threshold) return; - this.cnt++; - for (int i = 0; i < this.next.length; i++) { - dfs(visited, threshold, rows, cols, r + next[i][0], c + next[i][1]); - } -} - -private void initDigitSum(int rows, int cols) { - 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 < rows; i++) { - for (int j = 0; j < cols; j++) { - this.digitSum[i][j] = digitSumOne[i] + digitSumOne[j]; - } - } -} -``` - -## 14. 剪绳子 - -**题目描述** - -把一根绳子剪成多段,并且使得每段的长度乘积最大。 - -**动态规划解法** - -[分割整数](https://github.com/CyC2018/InterviewNotes/blob/master/notes/Leetcode%20%E9%A2%98%E8%A7%A3.md#%E5%88%86%E5%89%B2%E6%95%B4%E6%95%B0) - -**贪心解法** - -尽可能多得剪长度为 3 的绳子,并且不允许有长度为 1 的绳子出现,如果出现了,就从已经切好长度为 3 的绳子中拿出一段与长度为 1 的绳子重新组合,把它们切成两段长度为 2 的绳子。 - -```java -int maxProductAfterCuttin(int length) { - if (length < 2) return 0; - if (length == 2) return 1; - if (length == 3) return 2; - int timesOf3 = length / 3; - if (length - timesOf3 * 3 == 1) timesOf3--; - int timesOf2 = (length - timesOf3 * 3) / 2; - return (int) (Math.pow(3, timesOf3)) * (int) (Math.pow(2, timesOf2)); -} -``` - -## 15. 二进制中 1 的个数 - -使用库函数: - -```java -public int NumberOf1(int n) { - return Integer.bitCount(n); -} -``` - -O(lgM) 时间复杂度解法,其中 M 表示 1 的个数: - -n&(n-1) 该位运算是去除 n 的位级表示中最低的那一位。例如对于二进制表示 10110100,减去 1 得到 10110011,这两个数相与得到 10110000。 - -```java -public int NumberOf1(int n) { - int cnt = 0; - while (n != 0) { - cnt++; - n &= (n - 1); - } - return cnt; -} -``` - -# 第三章 高质量的代码 - -## 16. 数值的整数次方 - -```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; -} -``` - -## 18.2 删除链表中重复的结点 - -```java -public ListNode deleteDuplication(ListNode pHead) { - if (pHead == null) return null; - if (pHead.next == null) return pHead; - if (pHead.val == pHead.next.val) { - ListNode next = pHead.next; - while (next != null && pHead.val == next.val) { - next = next.next; - } - return deleteDuplication(next); - } else { - pHead.next = deleteDuplication(pHead.next); - return pHead; - } -} -``` - -## 19. 正则表达式匹配 - -**题目描述** - -请实现一个函数用来匹配包括 '.' 和 '\*' 的正则表达式。模式中的字符 '.' 表示任意一个字符,而 '\*' 表示它前面的字符可以出现任意次(包含 0 次)。 在本题中,匹配是指字符串的所有字符匹配整个模式。例如,字符串 "aaa" 与模式 "a.a" 和 "ab\*ac\*a" 匹配,但是与 "aa.a" 和 "ab\*a" 均不匹配 - -```java -public boolean match(char[] str, char[] pattern) { - int n = str.length, m = pattern.length; - boolean[][] dp = new boolean[n + 1][m + 1]; - dp[0][0] = true; - for (int i = 1; i <= m; i++) { - if (pattern[i - 1] == '*') dp[0][i] = dp[0][i - 2]; - } - for (int i = 1; i <= n; i++) { - for (int j = 1; j <= m; 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 - 2]; - else dp[i][j] = dp[i][j - 1] || dp[i][j - 2] || dp[i - 1][j]; - } - } - } - return dp[n][m]; -} -``` - -## 20. 表示数值的字符串 - -**题目描述** - -请实现一个函数用来判断字符串是否表示数值(包括整数和小数)。例如,字符串 "+100","5e2","-123","3.1416" 和 "-1E-16" 都表示数值。 但是 "12e","1a3.14","1.2.3","+-5" 和 "12e+4.3" 都不是。 - -```java -public boolean isNumeric(char[] str) { - String string = String.valueOf(str); - return string.matches("[\\+-]?[0-9]*(\\.[0-9]*)?([eE][\\+-]?[0-9]+)?"); -} -``` - -## 21. 调整数组顺序使奇数位于偶数前面 - -**题目要求** - -保证奇数和奇数,偶数和偶数之间的相对位置不变,这和书本不太一样。 - -时间复杂度 : O(n2) -空间复杂度 : O(1) - -```java -public void reOrderArray(int[] array) { - int n = array.length; - for (int i = 0; i < n; i++) { - if (array[i] % 2 == 0) { - int nextOddIdx = i + 1; - while (nextOddIdx < n && array[nextOddIdx] % 2 == 0) nextOddIdx++; - if (nextOddIdx == n) break; - int nextOddVal = array[nextOddIdx]; - for (int j = nextOddIdx; j > i; j--) { - array[j] = array[j - 1]; - } - array[i] = nextOddVal; - } - } -} -``` - -时间复杂度 : O(n) -空间复杂度 : O(n) - -```java -public void reOrderArray(int[] array) { - int oddCnt = 0; - for (int num : array) if (num % 2 == 1) oddCnt++; - int[] copy = array.clone(); - int i = 0, j = oddCnt; - for (int num : copy) { - if (num % 2 == 1) array[i++] = num; - else array[j++] = num; - } -} -``` - -## 22. 链表中倒数第 k 个结点 - -```java -public ListNode FindKthToTail(ListNode head, int k) { - if (head == null) return null; - ListNode fast, slow; - fast = slow = head; - while (fast != null && k-- > 0) fast = fast.next; - if (k > 0) return null; - while (fast != null) { - fast = fast.next; - slow = slow.next; - } - return slow; -} -``` - -## 23. 链表中环的入口结点 - -```java -public ListNode EntryNodeOfLoop(ListNode pHead) { - if (pHead == null) return null; - ListNode slow = pHead, fast = pHead; - while (fast != null && fast.next != null) { - fast = fast.next.next; - slow = slow.next; - if (slow == fast) { - fast = pHead; - while (slow != fast) { - slow = slow.next; - fast = fast.next; - } - return slow; - } - } - return null; -} -``` - -## 24. 反转链表 - -```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. 合并两个排序的链表 - -```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. 树的子结构 - -```java -public boolean HasSubtree(TreeNode root1, TreeNode root2) { - if (root1 == null || root2 == null) return false; - return isSubtree(root1, root2) || HasSubtree(root1.left, root2) || HasSubtree(root1.right, root2); -} - -private boolean isSubtree(TreeNode root1, TreeNode root2) { - if (root1 == null && root2 == null) return true; - if (root1 == null) return false; - if (root2 == null) return true; - if (root1.val != root2.val) return false; - return isSubtree(root1.left, root2.left) && isSubtree(root1.right, root2.right); -} -``` - -# 第四章 解决面试题的思路 - -## 27. 二叉树的镜像 - -```java -public void Mirror(TreeNode root) { - if (root == null) return; - TreeNode t = root.left; - root.left = root.right; - root.right = t; - Mirror(root.left); - Mirror(root.right); -} -``` - -## 28.1 对称的二叉树 - -```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); -} -``` - -## 28.2 平衡二叉树 - -```java -private boolean isBalanced = true; - -public boolean IsBalanced_Solution(TreeNode root) { - height(root); - return isBalanced; -} - -private int height(TreeNode root) { - if (root == null) 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); -} -``` - -## 29. 顺时针打印矩阵 - -```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 函数的栈 - -```java -private Stack stack = new Stack<>(); -private Stack minStack = new Stack<>(); -private int min = Integer.MAX_VALUE; - -public void push(int node) { - stack.push(node); - if (min > node) min = node; - minStack.push(min); -} - -public void pop() { - stack.pop(); - minStack.pop(); - min = minStack.peek(); -} - -public int top() { - return stack.peek(); -} - -public int min() { - return minStack.peek(); -} -``` - -## 31. 栈的压入、弹出序列 - -```java -public boolean IsPopOrder(int[] pushA, int[] popA) { - int n = pushA.length; - Stack stack = new Stack<>(); - for (int i = 0, j = 0; i < n; i++) { - stack.push(pushA[i]); - while (j < n && stack.peek() == popA[j]) { - stack.pop(); - j++; - } - } - return stack.isEmpty(); -} -``` - -## 32.1 从上往下打印二叉树 - -```java -public ArrayList PrintFromTopToBottom(TreeNode root) { - Queue queue = new LinkedList<>(); - ArrayList ret = new ArrayList<>(); - if (root == null) return ret; - queue.add(root); - while (!queue.isEmpty()) { - int cnt = queue.size(); - for (int i = 0; i < cnt; i++) { - TreeNode t = queue.poll(); - if (t.left != null) queue.add(t.left); - if (t.right != null) queue.add(t.right); - ret.add(t.val); - } - } - return ret; -} -``` - -## 32.3 把二叉树打印成多行 - -```java -ArrayList> Print(TreeNode pRoot) { - ArrayList> ret = new ArrayList<>(); - if (pRoot == null) return ret; - Queue queue = new LinkedList<>(); - queue.add(pRoot); - while (!queue.isEmpty()) { - int cnt = queue.size(); - ArrayList list = new ArrayList<>(); - for (int i = 0; i < cnt; i++) { - TreeNode node = queue.poll(); - list.add(node.val); - if (node.left != null) queue.add(node.left); - if (node.right != null) queue.add(node.right); - } - ret.add(list); - } - return ret; -} -``` - -## 32.3 按之字形顺序打印二叉树 - -```java -public ArrayList> Print(TreeNode pRoot) { - ArrayList> ret = new ArrayList<>(); - if (pRoot == null) return ret; - Queue queue = new LinkedList<>(); - queue.add(pRoot); - boolean reverse = false; - while (!queue.isEmpty()) { - int cnt = queue.size(); - ArrayList list = new ArrayList<>(); - for (int i = 0; i < cnt; i++) { - TreeNode node = queue.poll(); - list.add(node.val); - if (node.left != null) queue.add(node.left); - if (node.right != null) queue.add(node.right); - } - if (reverse) { - Collections.reverse(list); - reverse = false; - } else { - reverse = true; - } - ret.add(list); - } - return ret; -} -``` - - -## 33. 二叉搜索树的后序遍历序列 - -```java -public boolean VerifySquenceOfBST(int[] sequence) { - if (sequence.length == 0) return false; - return verify(sequence, 0, sequence.length - 1); -} - -private boolean verify(int[] sequence, int start, int end) { - if (end - start <= 1) return true; - int rootVal = sequence[end]; - int cutIdx = start; - while (cutIdx < end) { - if (sequence[cutIdx] > rootVal) break; - cutIdx++; - } - for (int i = cutIdx + 1; i < end; i++) { - if (sequence[i] < rootVal) return false; - } - return verify(sequence, start, cutIdx - 1) && verify(sequence, cutIdx, end - 1); -} -``` - -## 34. 二叉树中和为某一值的路径 - -```java -private ArrayList> ret = new ArrayList<>(); - -public ArrayList> FindPath(TreeNode root, int target) { - dfs(root, target, 0, new ArrayList<>()); - return ret; -} - -private void dfs(TreeNode node, int target, int curSum, ArrayList path) { - if (node == null) return; - curSum += node.val; - path.add(node.val); - if (curSum == target && node.left == null && node.right == null) { - ret.add(new ArrayList(path)); - } else { - dfs(node.left, target, curSum, path); - dfs(node.right, target, curSum, path); - } - path.remove(path.size() - 1); -} -``` - -## 35. 复杂链表的复制 - -**题目描述** - -输入一个复杂链表(每个节点中有节点值,以及两个指针,一个指向下一个节点,另一个特殊指针指向任意一个节点),返回结果为复制后复杂链表的 head。(注意,输出结果中请不要返回参数中的节点引用,否则判题程序会直接返回空) - -第一步,在每个节点的后面插入复制的节点。 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//f8b12555-967b-423d-a84e-bc9eff104b8b.jpg) - -第二步,对复制节点的 random 链接进行赋值。 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//7b877a2a-8fd1-40d8-a34c-c445827300b8.jpg) - -第三步,拆分。 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//b2b6253c-c701-4b30-aff4-bc3c713542a7.jpg) - - -```java -public RandomListNode Clone(RandomListNode pHead) { - if (pHead == null) return null; - // 插入新节点 - RandomListNode cur = pHead; - while (cur != null) { - RandomListNode node = new RandomListNode(cur.label); - node.next = cur.next; - cur.next = node; - cur = node.next; - } - // 建立 random 链接 - cur = pHead; - while (cur != null) { - RandomListNode clone = cur.next; - if (cur.random != null) { - clone.random = cur.random.next; - } - cur = clone.next; - } - // 拆分 - RandomListNode pCloneHead = pHead.next; - cur = pHead; - while (cur.next != null) { - RandomListNode t = cur.next; - cur.next = t.next; - cur = t; - } - return pCloneHead; -} -``` - -## 36. 二叉搜索树与双向链表 - -**题目描述** - -输入一棵二叉搜索树,将该二叉搜索树转换成一个排序的双向链表。要求不能创建任何新的结点,只能调整树中结点指针的指向。 - -```java -private TreeNode pre = null; -public TreeNode Convert(TreeNode pRootOfTree) { - if(pRootOfTree == null) return null; - inOrder(pRootOfTree); - while(pRootOfTree.left != null) pRootOfTree = pRootOfTree.left; - return pRootOfTree; -} - -private void inOrder(TreeNode node) { - if(node == null) return; - inOrder(node.left); - node.left = pre; - if(pre != null) pre.right = node; - pre = node; - inOrder(node.right); -} -``` - -## 37. 序列化二叉树 - -```java -private String serizeString = ""; - -String Serialize(TreeNode root) { - if (root == null) return "#"; - return root.val + " " + Serialize(root.left) + " " - + Serialize(root.right); -} - -TreeNode Deserialize(String str) { - this.serizeString = str; - return Deserialize(); -} - -private TreeNode Deserialize() { - if (this.serizeString.length() == 0) return null; - int idx = this.serizeString.indexOf(" "); - if (idx == -1) return null; - String sub = this.serizeString.substring(0, idx); - this.serizeString = this.serizeString.substring(idx + 1); - if (sub.equals("#")) { - return null; - } - int val = Integer.valueOf(sub); - TreeNode t = new TreeNode(val); - t.left = Deserialize(); - t.right = Deserialize(); - return t; -} -``` - -## 38. 字符串的排列 - -**题目描述** - -输入一个字符串 , 按字典序打印出该字符串中字符的所有排列。例如输入字符串 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 new ArrayList<>(); - char[] chars = str.toCharArray(); - Arrays.sort(chars); - backtracking(chars, new boolean[chars.length], ""); - return ret; -} - -private void backtracking(char[] chars, boolean[] used, String s) { - if (s.length() == chars.length) { - ret.add(s); - return; - } - for (int i = 0; i < chars.length; i++) { - if (used[i]) continue; - if (i != 0 && chars[i] == chars[i - 1] && !used[i - 1]) continue; // 保证不重复 - used[i] = true; - backtracking(chars, used, s + chars[i]); - used[i] = false; - } -} -``` - -# 第五章 优化时间和空间效率 - -## 39. 数组中出现次数超过一半的数字 - -```java -public int MoreThanHalfNum_Solution(int[] array) { - int cnt = 1, num = array[0]; - for (int i = 1; i < array.length; i++) { - if (array[i] == num) cnt++; - else cnt--; - if (cnt == 0) { - num = array[i]; - cnt = 1; - } - } - cnt = 0; - for (int i = 0; i < array.length; i++) { - if (num == array[i]) cnt++; - } - return cnt > array.length / 2 ? num : 0; -} -``` - - -## 40. 最小的 K 个数 - -构建大小为 k 的小顶堆。 - -时间复杂度:O(nlgk) -空间复杂度:O(k) - -```java -public ArrayList GetLeastNumbers_Solution(int[] input, int k) { - if (k > input.length || k <= 0) return new ArrayList<>(); - PriorityQueue pq = new PriorityQueue<>((o1, o2) -> o2 - o1); - for (int num : input) { - pq.add(num); - if (pq.size() > k) { - pq.poll(); - } - } - ArrayList ret = new ArrayList<>(pq); - return ret; -} -``` - -利用快速选择 - -时间复杂度:O(n) -空间复杂度:O(1) - -```java -public ArrayList GetLeastNumbers_Solution(int[] input, int k) { - if (k > input.length || k <= 0) return new ArrayList<>(); - int kthSmallest = findKthSmallest(input, k - 1); - ArrayList ret = new ArrayList<>(); - for (int num : input) { - if(num <= kthSmallest && ret.size() < k) ret.add(num); - } - return ret; -} - -public int findKthSmallest(int[] nums, int k) { - int lo = 0; - int hi = nums.length - 1; - while (lo < hi) { - int j = partition(nums, lo, hi); - if (j < k) { - lo = j + 1; - } else if (j > k) { - hi = j - 1; - } else { - break; - } - } - return nums[k]; -} - -private int partition(int[] a, int lo, int hi) { - int i = lo; - int j = hi + 1; - while (true) { - while (i < hi && less(a[++i], a[lo])) ; - while (j > lo && less(a[lo], a[--j])) ; - if (i >= j) { - break; - } - exch(a, i, j); - } - exch(a, lo, j); - return j; -} - -private void exch(int[] a, int i, int j) { - final int tmp = a[i]; - a[i] = a[j]; - a[j] = tmp; -} - -private boolean less(int v, int w) { - return v < w; -} -``` - -## 41.1 数据流中的中位数 - - -**题目描述** - -如何得到一个数据流中的中位数?如果从数据流中读出奇数个数值,那么中位数就是所有数值排序之后位于中间的数值。如果从数据流中读出偶数个数值,那么中位数就是所有数值排序之后中间两个数的平均值。 - -```java -private PriorityQueue maxHeap = new PriorityQueue<>((o1, o2) -> o2-o1); // 实现左边部分 -private PriorityQueue minHeep = new PriorityQueue<>(); // 实现右边部分,右边部分所有元素大于左边部分 -private int cnt = 0; - -public void Insert(Integer num) { - // 插入要保证两个堆存于平衡状态 - if(cnt % 2 == 0) { - // 为偶数的情况下插入到最小堆,先经过最大堆筛选,这样就能保证最大堆中的元素都小于最小堆中的元素 - maxHeap.add(num); - minHeep.add(maxHeap.poll()); - } else { - minHeep.add(num); - maxHeap.add(minHeep.poll()); - } - cnt++; -} - -public Double GetMedian() { - if(cnt % 2 == 0) { - return (maxHeap.peek() + minHeep.peek()) / 2.0; - } else { - return (double) minHeep.peek(); - } -} -``` - -## 14.2 字符流中第一个不重复的字符 - -**题目描述** - -请实现一个函数用来找出字符流中第一个只出现一次的字符。例如,当从字符流中只读出前两个字符 "go" 时,第一个只出现一次的字符是 "g"。当从该字符流中读出前六个字符“google" 时,第一个只出现一次的字符是 "l"。 - -```java -//Insert one char from stringstream -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(); - } -} - -//return the first appearence once char in current stringstream -public char FirstAppearingOnce() { - if (queue.isEmpty()) return '#'; - return queue.peek(); -} -``` - - -## 42. 连续子数组的最大和 - -```java -public int FindGreatestSumOfSubArray(int[] array) { - if(array.length == 0) return 0; - int ret = Integer.MIN_VALUE; - int sum = 0; - for(int num : array) { - if(sum <= 0) sum = num; - else sum += num; - ret = Math.max(ret, sum); - } - return ret; -} -``` - -## 43. 从 1 到 n 整数中 1 出现的次数 - -解题参考:[Leetcode : 233. Number of Digit One](https://leetcode.com/problems/number-of-digit-one/discuss/64381/4+-lines-O(log-n)-C++JavaPython) - -```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; -} -``` - -## 45. 把数组排成最小的数 - -**题目描述** - -输入一个正整数数组,把数组里所有数字拼接起来排成一个数,打印能拼接出的所有数字中最小的一个。例如输入数组 {3,32,321},则打印出这三个数字能排成的最小数字为 321323。 - -```java -public String PrintMinNumber(int[] numbers) { - 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; -} -``` - -## 49. 丑数 - -**题目描述** - -把只包含因子 2、3 和 5 的数称作丑数(Ugly Number)。例如 6、8 都是丑数,但 14 不是,因为它包含因子 7。 习惯上我们把 1 当做是第一个丑数。求按从小到大的顺序的第 N 个丑数。 - -```java -public int GetUglyNumber_Solution(int index) { - if (index <= 6) return index; - int i2 = 0, i3 = 0, i5 = 0; - int cnt = 1; - int[] dp = new int[index]; - dp[0] = 1; - while (cnt < index) { - int n2 = dp[i2] * 2, n3 = dp[i3] * 3, n5 = dp[i5] * 5; - int tmp = Math.min(n2, Math.min(n3, n5)); - dp[cnt++] = tmp; - if (tmp == n2) i2++; - if (tmp == n3) i3++; - if (tmp == n5) i5++; - } - return dp[index - 1]; -} -``` - -## 50. 第一个只出现一次的字符位置 - -```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; -} -``` - -## 51. 数组中的逆序对 - -```java -private long cnt = 0; - -public int InversePairs(int[] array) { - mergeSortUp2Down(array, 0, array.length - 1); - return (int) (cnt % 1000000007); -} - -private void mergeSortUp2Down(int[] a, int start, int end) { - if (end - start < 1) return; - int mid = start + (end - start) / 2; - mergeSortUp2Down(a, start, mid); - mergeSortUp2Down(a, mid + 1, end); - merge(a, start, mid, end); -} - -private void merge(int[] a, int start, int mid, int end) { - int[] tmp = new int[end - start + 1]; - int i = start, j = mid + 1, k = 0; - while (i <= mid || j <= end) { - if (i > mid) tmp[k] = a[j++]; - else if (j > end) tmp[k] = a[i++]; - else if (a[i] < a[j]) tmp[k] = a[i++]; - else { - tmp[k] = a[j++]; - this.cnt += mid - i + 1; // a[i] > a[j] ,说明 a[i...mid] 都大于 a[j] - } - k++; - } - - for (k = 0; k < tmp.length; k++) { - a[start + k] = tmp[k]; - } -} -``` - -## 52. 两个链表的第一个公共结点 - -```java -public ListNode FindFirstCommonNode(ListNode pHead1, ListNode pHead2) { - ListNode l1 = pHead1, l2 = pHead2; - while (l1 != l2) { - if (l1 == null) l1 = pHead2; - else l1 = l1.next; - if (l2 == null) l2 = pHead1; - else l2 = l2.next; - } - return l1; -} -``` - -# 第六章 面试中的各项能力 - -## 53 数字在排序数组中出现的次数 - -```java -public int GetNumberOfK(int[] array, int k) { - int l = 0, h = array.length - 1; - while (l <= h) { - int m = l + (h - l) / 2; - if (array[m] >= k) h = m - 1; - else l = m + 1; - } - int cnt = 0; - while (l < array.length && array[l++] == k) cnt++; - return cnt; -} -``` - -## 54. 二叉搜索树的第 k 个结点 - -```java -TreeNode ret; -int cnt = 0; - -TreeNode KthNode(TreeNode pRoot, int k) { - inorder(pRoot, k); - return ret; -} - -private void inorder(TreeNode root, int k) { - if (root == null) return; - if (cnt > k) return; - inorder(root.left, k); - cnt++; - if (cnt == k) ret = root; - inorder(root.right, k); -} -``` - -## 55 二叉树的深度 - -```java -public int TreeDepth(TreeNode root) { - if (root == null) return 0; - return 1 + Math.max(TreeDepth(root.left), TreeDepth(root.right)); -} -``` - -## 56. 数组中只出现一次的数字 - -**题目描述** - -一个整型数组里除了两个数字之外,其他的数字都出现了两次,找出这两个数。 - -**解题思路** - -两个不相等的元素在位级表示上必定会有一位存在不同。 - -将数组的所有元素异或得到的结果为不存在重复的两个元素异或的结果。 - -diff &= -diff 得到出 diff 最右侧不为 0 的位,也就是不存在重复的两个元素在位级表示上最右侧不同的那一位,利用这一位就可以将两个元素区分开来。 - -```java -public void FindNumsAppearOnce(int[] array, int num1[], int num2[]) { - int diff = 0; - for (int num : array) diff ^= num; - // 得到最右一位 - diff &= -diff; - for (int num : array) { - if ((num & diff) == 0) num1[0] ^= num; - else num2[0] ^= num; - } -} -``` - -## 57.1 和为 S 的两个数字 - -**题目描述** - -输入一个递增排序的数组和一个数字 S,在数组中查找两个数,是的他们的和正好是 S,如果有多对数字的和等于 S,输出两个数的乘积最小的。 - -```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])); - else if (cur < sum) i++; - else j--; - } - return new ArrayList(); -} -``` - -## 57.2 和为 S 的连续正数序列 - -**题目描述** - -和为 100 的连续序列有 18, 19, 20, 21, 22 - -```java -public ArrayList> FindContinuousSequence(int sum) { - ArrayList> ret = new ArrayList<>(); - int start = 1, end = 2; - int mid = sum / 2; - int curSum = 3; - while (start <= mid && 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 翻转单词顺序列 - -**题目描述** - -输入:"I am a student." - -输出:"student. a am I" - -```java -public String ReverseSentence(String str) { - if (str.length() == 0) return str; - int n = str.length(); - char[] chars = str.toCharArray(); - int start = 0, end = 0; - while (end <= n) { - if (end == n || chars[end] == ' ') { - reverse(chars, start, end - 1); - start = end + 1; - } - end++; - } - reverse(chars, 0, n - 1); - return new String(chars); -} - -private void reverse(char[] c, int start, int end) { - while (start < end) { - char t = c[start]; - c[start] = c[end]; - c[end] = t; - start++; - end--; - } -} -``` - -## 58.2 左旋转字符串 - -**题目描述** - -对于一个给定的字符序列 S,请你把其循环左移 K 位后的序列输出。例如,字符序列 S=”abcXYZdef”, 要求输出循环左移 3 位后的结果,即“XYZdefabc”。 - -```java -public String LeftRotateString(String str, int n) { - if (str.length() == 0) return ""; - char[] c = str.toCharArray(); - reverse(c, 0, n - 1); - reverse(c, n, c.length - 1); - reverse(c, 0, c.length - 1); - return new String(c); -} - -private void reverse(char[] c, int i, int j) { - while (i < j) { - char t = c[i]; - c[i] = c[j]; - c[j] = t; - i++; - j--; - } -} -``` - -## 59. 滑动窗口的最大值 - -**题目描述** - -给定一个数组和滑动窗口的大小,找出所有滑动窗口里数值的最大值。例如,如果输入数组 {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 = 1; i + size - 1 < num.length; i++) { - heap.remove(num[i - 1]); - heap.add(num[i + size - 1]); - ret.add(heap.peek()); - } - return ret; -} -``` - -## 61. 扑克牌顺子 - -**题目描述** - -五张牌,其中大小鬼为癞子,牌面大小为 0。判断是否能组成顺子。 - -```java -public boolean isContinuous(int[] numbers) { - if (numbers.length < 5) return false; - Arrays.sort(numbers); - int cnt = 0; - for (int num : numbers) if (num == 0) cnt++; - for (int i = cnt; i < numbers.length - 1; i++) { - if (numbers[i + 1] == numbers[i]) return false; - int cut = numbers[i + 1] - numbers[i] - 1; - if (cut > cnt) return false; - cnt -= cut; - } - return true; -} -``` - -## 62. 圆圈中最后剩下的数 - -**题目描述** - -让小朋友们围成一个大圈。然后 , 他随机指定一个数 m, 让编号为 0 的小朋友开始报数。每次喊到 m-1 的那个小朋友要出列唱首歌 , 然后可以在礼品箱中任意的挑选礼物 , 并且不再回到圈中 , 从他的下一个小朋友开始 , 继续 0...m-1 报数 .... 这样下去 .... 直到剩下最后一个小朋友 , 可以不用表演。 - -**解题思路** - -约瑟夫环 - -```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. 股票的最大利润 - -**题目描述** - -可以有一次买入和一次卖出,买入必须在前。求最大收益。 - -```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; -} -``` - -## 64. 求 1+2+3+...+n - -**题目描述** - -求 1+2+3+...+n,要求不能使用乘除法、for、while、if、else、switch、case 等关键字及条件判断语句(A?B:C) - -```java -public int Sum_Solution(int n) { - int sum = n; - boolean b = (n > 0) && ((sum += Sum_Solution(n - 1)) > 0); - return sum; -} -``` - -## 65. 不用加减乘除做加法 - -a ^ b 表示没有考虑进位的情况下两数的和,(a & b) << 1 就是进位。递归会终止的原因是 (a & b) << 1 最右边会多一个 0,那么继续递归,进位最右边的 0 会慢慢增多,最后进位会变为 0,递归终止。 - -```java -public int Add(int num1, int num2) { - if(num2 == 0) return num1; - return Add(num1 ^ num2, (num1 & num2) << 1); -} -``` - -## 66. 构建乘积数组 - -**题目描述** - -给定一个数组 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[][] dp = new int[n][n]; - for (int i = 0; i < n; i++) { - dp[i][i] = A[i]; - } - for (int i = 0; i < n; i++) { - for (int j = i + 1; j < n; j++) { - dp[i][j] = dp[i][j - 1] * A[j]; - } - } - - int[] B = new int[n]; - Arrays.fill(B, 1); - for (int i = 0; i < n; i++) { - if (i != 0) B[i] *= dp[0][i - 1]; - if (i != n - 1) B[i] *= dp[i + 1][n - 1]; - } - return B; -} -``` - -# 第七章 两个面试案例 - -## 67. 把字符串转换成整数 - -```java -public int StrToInt(String str) { - if (str.length() == 0) return 0; - char[] chars = str.toCharArray(); - boolean isNegative = chars[0] == '-'; - int ret = 0; - for (int i = 0; i < chars.length; i++) { - if (i == 0 && (chars[i] == '+' || chars[i] == '-')) continue; - if (chars[i] < '0' || chars[i] > '9') return 0; - ret = ret * 10 + (chars[i] - '0'); - } - return isNegative ? -ret : ret; -} -``` - -## 68. 树中两个节点的最低公共祖先 - -树是二叉查找树的最低公共祖先问题: - -```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; -} -``` diff --git a/notes/算法.md b/notes/算法.md deleted file mode 100644 index bdad52f6..00000000 --- a/notes/算法.md +++ /dev/null @@ -1,1326 +0,0 @@ - -* [算法分析](#算法分析) - * [1. 函数转换](#1-函数转换) - * [2. 数学模型](#2-数学模型) - * [3. ThreeSum](#3-threesum) - * [4. 倍率实验](#4-倍率实验) - * [5. 注意事项](#5-注意事项) -* [排序](#排序) - * [1. 初级排序算法](#1-初级排序算法) - * [1.1 约定](#11-约定) - * [1.2 选择排序](#12-选择排序) - * [1.3 插入排序](#13-插入排序) - * [1.4 选择排序和插入排序的比较](#14-选择排序和插入排序的比较) - * [1.5 希尔排序](#15-希尔排序) - * [2 归并排序](#2-归并排序) - * [2.1 归并方法](#21-归并方法) - * [2.2 自顶向下归并排序](#22-自顶向下归并排序) - * [2.3 自底向上归并排序](#23-自底向上归并排序) - * [3.快速排序](#3快速排序) - * [3.1 基本算法](#31-基本算法) - * [3.2 切分](#32-切分) - * [3.3 性能分析](#33-性能分析) - * [3.4 算法改进](#34-算法改进) - * [3.4.1 切换到插入排序](#341-切换到插入排序) - * [3.4.2 三取样](#342-三取样) - * [3.4.3 三向切分](#343-三向切分) - * [4. 优先队列](#4-优先队列) - * [4.1 堆](#41-堆) - * [4.2 上浮和下沉](#42-上浮和下沉) - * [4.3 插入元素](#43-插入元素) - * [4.4 删除最大元素](#44-删除最大元素) - * [4.5 堆排序](#45-堆排序) - * [4.6 分析](#46-分析) - * [5. 应用](#5-应用) - * [5.1 排序算法的比较](#51-排序算法的比较) - * [5.2 Java 的排序算法实现](#52-java-的排序算法实现) - * [5.3 基于切分的快速选择算法](#53-基于切分的快速选择算法) -* [查找](#查找) - * [1. 符号表](#1-符号表) - * [1.1 无序符号表](#11-无序符号表) - * [1.2 有序符号表](#12-有序符号表) - * [1.3 二分查找实现有序符号表](#13-二分查找实现有序符号表) - * [2. 二叉查找树](#2-二叉查找树) - * [2.1 get()](#21-get) - * [2.2 put()](#22-put) - * [2.3 分析](#23-分析) - * [2.4 floor()](#24-floor) - * [2.5 rank()](#25-rank) - * [2.6 min()](#26-min) - * [2.7 deleteMin()](#27-deletemin) - * [2.8 delete()](#28-delete) - * [2.9 keys()](#29-keys) - * [2.10 性能分析](#210-性能分析) - * [3. 平衡查找树](#3-平衡查找树) - * [3.1 2-3 查找树](#31-2-3-查找树) - * [3.1.1 插入操作](#311-插入操作) - * [3.1.2 性质](#312-性质) - * [3.2 红黑二叉查找树](#32-红黑二叉查找树) - * [3.2.1 左旋转](#321-左旋转) - * [3.2.2 右旋转](#322-右旋转) - * [3.2.3 颜色转换](#323-颜色转换) - * [3.2.4 插入](#324-插入) - * [3.2.5 删除最小键](#325-删除最小键) - * [3.2.6 分析](#326-分析) - * [4. 散列表](#4-散列表) - * [4.1 散列函数](#41-散列函数) - * [4.2 基于拉链法的散列表](#42-基于拉链法的散列表) - * [4.3 基于线性探测法的散列表](#43-基于线性探测法的散列表) - * [4.3.1 查找](#431-查找) - * [4.3.2 插入](#432-插入) - * [4.3.3 删除](#433-删除) - * [4.3.4 调整数组大小](#434-调整数组大小) - * [5. 应用](#5-应用) - * [5.1 各种符号表实现的比较](#51-各种符号表实现的比较) - * [5.2 Java 的符号表实现](#52-java-的符号表实现) - * [5.3 集合类型](#53-集合类型) - * [5.4 稀疏向量乘法](#54-稀疏向量乘法) - - - -# 算法分析 - -## 1. 函数转换 - -指数函数可以转换为线性函数,从而在函数图像上显示的更直观。 - -T(N)=aN3 转换为 lg(T(N))=3lgN+lga - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//5510045a-8f32-487f-a756-463e51a6dab0.png) - -## 2. 数学模型 - -**近似** - -使用 \~f(N) 来表示所有随着 N 的增大除以 f(N) 的结果趋近于 1 的函数 , 例如 N3/6-N2/2+N/3 \~ N3/6。 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//ca3a793e-06e5-4ff3-b28e-a9c20540d164.png) - -**增长数量级** - -增长数量级将算法与它的实现隔离开来,一个算法的增长数量级为 N3 与它是否用 Java 实现,是否运行与特定计算机上无关。 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//1ea4dc9a-c4dd-46b5-bb11-49f98d57ded1.png) - -**内循环** - -执行最频繁的指令决定了程序执行的总时间,把这些指令称为程序的内循环。 - -**成本模型** - -使用成本模型来评估算法,例如数组的访问次数就是一种成本模型。 - -## 3. ThreeSum - -ThreeSum 程序用于统计一个数组中三元组的和为 0 的数量。 - -```java -public class ThreeSum { - public static int count(int[] a) { - int N = a.length; - int cnt = 0; - for (int i = 0; i < N; i++) { - for (int j = i + 1; j < N; j++) { - for (int k = j + 1; k < N; k++) { - if (a[i] + a[j] + a[k] == 0) { - cnt++; - } - } - } - } - return cnt; - } -} -``` - -该程序的内循环为 if (a[i] + a[j] + a[k] == 0) 语句,总共执行的次数为 N3/6-N2/2+N/3,因此它的近似执行次数为 \~N3/6,增长数量级为 N3。 - -**改进** - -通过将数组先排序,对两个元素求和,并用二分查找方法查找是否存在该和的相反数,如果存在,就说明存在三元组的和为 0。 - -该方法可以将 ThreeSum 算法增长数量级降低为 N2logN。 - -```java -public class ThreeSumFast { - public static int count(int[] a) { - Arrays.sort(a); - int N = a.length; - int cnt = 0; - for (int i = 0; i < N; i++) { - for (int j = i + 1; j < N; j++) { - for (int k = j + 1; k < N; k++) { - // rank() 方法返回元素在数组中的下标,如果元素不存在,这里会返回 -1。应该注意这里的下标必须大于 j,这样就不会重复统计了。 - if (BinarySearch.rank(-a[i] - a[j], a) > j) { - cnt++; - } - } - } - } - return cnt; - } -} -``` - -## 4. 倍率实验 - -如果 T(N) \~ aNblgN,那么 T(2N)/T(N) \~ 2b,例如对于暴力方法的 ThreeSum 算法,近似时间为 \~N3/6,对它进行倍率实验得到如下结果: - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//6f5ed46f-86d7-4852-a34f-c1cf1b6343a0.png) - -可见 T(2N)/T(N)\~23,也就是 b 为 3。 - -## 5. 注意事项 - -**大常数** - -在求近似时,如果低级项的常数系数很大,那么近似的结果就是错误的。 - -**缓存** - -计算机系统会使用缓存技术来组织内存,访问数组相邻的元素会比访问不相邻的元素快很多。 - -**对最坏情况下的性能的保证** - -在核反应堆、心脏起搏器或者刹车控制器中的软件,最坏情况下的性能是十分重要的。 - -**随机化算法** - -通过打乱输入,去除算法对输入的依赖。 - -**均摊分析** - -将所有操作的总成本所以操作总数来将成本均摊。例如对一个空栈进行 N 次连续的 push() 调用需要访问数组的元素为 N+4+8+16+...+2N=5N-4(N 是向数组写入元素,其余的都是调整数组大小时进行复制需要的访问数组操作),均摊后每次操作访问数组的平均次数为常数。 - - -# 排序 - -## 1. 初级排序算法 - -### 1.1 约定 - -待排序的元素需要实现 Java 的 Comparable 接口,该接口有 compareTo() 方法。 - -研究排序算法的成本模型时,计算的是比较和交换的次数。 - -使用辅助函数 less() 和 exch() 来进行比较和交换的操作,使得代码的可读性和可移植性更好。 - -```java -private boolean less(Comparable v, Comparable w){ - return v.compareTo(w) < 0; -} - -private void exch(Comparable[] a, int i, int j){ - Comparable t = a[i]; - a[i] = a[j]; - a[j] = t; -} -``` - -### 1.2 选择排序 - -找到数组中的最小元素,然后将它与数组的第一个元素交换位置。然后再从剩下的元素中找到最小的元素,将它与数组的第二个元素交换位置。不断进行这样的操作,直到将整个数组排序。 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//222768a7-914f-4d64-b874-d98f3b926fb6.jpg) - -```java -public class Selection { - public static void sort(Comparable[] a) { - int N = a.length; - for (int i = 0; i < N; i++) { - int min = i; - for (int j = i + 1; j < N; j++) { - if (less(a[j], a[min])) min = j; - } - exch(a, i, min); - } - } -} -``` - -选择排序需要 \~N2/2 次比较和 \~N 次交换,它的运行时间与输入无关,这个特点使得它对一个已经排序的数组也需要这么多的比较和交换操作。 - -### 1.3 插入排序 - -将一个元素插入到已排序的数组中,使得插入之后的数组也是有序的。插入排序从左到右插入每个元素,每次插入之后左部的子数组是有序的。 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//065c3bbb-3ea0-4dbf-8f26-01d0e0ba7db7.png) - -```java -public class Insertion { - public static void sort(Comparable[] a) { - int N = a.length; - for (int i = 1; i < N; i++) { - for (int j = i; j > 0 && less(a[j], a[j - 1]); j--) { - exch(a, j, j - 1); - } - } - } -} -``` - -插入排序的复杂度取决于数组的初始顺序,如果数组已经部分有序了,那么插入排序会很快。平均情况下插入排序需要 \~N2/4 比较以及 \~N2/4 次交换,最坏的情况下需要 \~N2/2 比较以及 \~N2/2 次交换,最坏的情况是数组是逆序的;而最好的情况下需要 N-1 次比较和 0 次交换,最好的情况就是数组已经有序了。 - -插入排序对于部分有序数组和小规模数组特别高效。 - -### 1.4 选择排序和插入排序的比较 - -对于随机排序的无重复主键的数组,插入排序和选择排序的运行时间是平方级别的,两者之比是一个较小的常数。 - -### 1.5 希尔排序 - -对于大规模的数组,插入排序很慢,因为它只能交换相邻的元素,如果要把元素从一端移到另一端,就需要很多次操作。 - -希尔排序的出现就是为了改进插入排序的这种局限性,它通过交换不相邻的元素,使得元素更快的移到正确的位置上。 - -希尔排序使用插入排序对间隔 h 的序列进行排序,如果 h 很大,那么元素就能很快的移到很远的地方。通过不断减小 h,最后令 h=1,就可以使得整个数组是有序的。 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//8320bad6-3f91-4a15-8e3d-68e8f39649b5.png) - -```java -public class Shell { - public static void sort(Comparable[] a) { - int N = a.length; - int h = 1; - while (h < N / 3) { - h = 3 * h + 1;// 1, 4, 13, 40, ... - } - while (h >= 1) { - for (int i = h; i < N; i++) { - for (int j = i; j >= h && less(a[j], a[j - h]); j -= h) { - exch(a, j, j - h); - } - } - h = h / 3; - } - } -} -``` - -希尔排序的运行时间达不到平方级别,使用递增序列 1, 4, 13, 40, ... 的希尔排序所需要的比较次数不会超过 N 的若干倍乘于递增序列的长度。后面介绍的高级排序算法只会比希尔排序快两倍左右。 - -## 2 归并排序 - -归并排序的思想是将数组分成两部分,分别进行排序,然后归并起来。 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//dcf265ad-fe35-424d-b4b7-d149cdf239f4.png) - -### 2.1 归并方法 - -```java -public class MergeSort { - private static Comparable[] aux; - - private static void merge(Comparable[] a, int lo, int mid, int hi) { - int i = lo, j = mid + 1; - - for (int k = lo; k <= hi; k++) { - aux[k] = a[k]; // 将数据复制到辅助数组 - } - - for (int k = lo; k <= hi; k++) { - if (i > mid) a[k] = aux[j++]; - else if (j > hi) a[k] = aux[i++]; - else if (aux[i].compareTo(a[j]) < 0) a[k] = aux[i++]; // 先进行这一步,保证稳定性 - else a[k] = aux[j++]; - } - } -} -``` - -### 2.2 自顶向下归并排序 - -```java -public static void sort(Comparable[] a) { - aux = new Comparable[a.length]; - sort(a, 0, a.length - 1); -} - -private static void sort(Comparable[] a, int lo, int hi) { - if (hi <= lo) return; - int mid = lo + (hi - lo) / 2; - sort(a, lo, mid); - sort(a, mid + 1, hi); - merge(a, lo, mid, hi); -} -``` - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//6468a541-3a9a-4008-82b6-03a0fe941d2a.png) - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//c7665f73-c52f-4ce4-aed3-592bbd76265b.png) - -很容易看出该排序算法的时间复杂度为 O(NlgN)。 - -因为小数组的递归操作会过于频繁,因此使用插入排序来处理小数组将会获得更高的性能。 - -### 2.3 自底向上归并排序 - -先归并那些微型数组,然后成对归并得到的子数组。 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//c7b9b4c8-83d1-4eb0-8408-ea6576a9ed90.png) - -```java -public static void busort(Comparable[] a) { - int N = a.length; - aux = new Comparable[N]; - for (int sz = 1; sz < N; sz += sz) { - for (int lo = 0; lo < N - sz; lo += sz + sz) { - merge(a, lo, lo + sz - 1, Math.min(lo + sz + sz - 1, N - 1)); - } - } -} -``` - -## 3.快速排序 - -### 3.1 基本算法 - -归并排序将数组分为两个子数组分别排序,并将有序的子数组归并使得整个数组排序;快速排序通过一个切分元素将数组分为两个子数组,左子数组小于等于切分元素,右子数组大于等于切分元素,将这两个子数组排序也就将整个数组排序了。 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//61b4832d-71f3-413c-84b6-237e219b9fdc.png) - -```java -public class QuickSort { - public static void sort(Comparable[] a) { - shuffle(a); - sort(a, 0, a.length - 1); - } - - private static void sort(Comparable[] a, int lo, int hi) { - if (hi <= lo) return; - int j = partition(a, lo, hi); - sort(a, lo, j - 1); - sort(a, j + 1, hi); - } -} -``` - -### 3.2 切分 - -取 a[lo] 作为切分元素,然后从数组的左端向右扫描直到找到第一个大于等于它的元素,再从数组的右端向左扫描找到第一个小于等于它的元素,交换这两个元素,并不断继续这个过程,就可以保证左指针的左侧元素都不大于切分元素,右指针 j 的右侧元素都不小于切分元素。当两个指针相遇时,将切分元素 a[lo] 和左子数组最右侧的元素 a[j] 交换然后返回 j 即可。 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//e198c201-f386-4491-8ad6-f7e433bf992d.png) - -```java -private static int partition(Comparable[] a, int lo, int hi) { - int i = lo, j = hi + 1; - Comparable v = a[lo]; - while (true) { - while (less(a[++i], v)) if (i == hi) break; - while (less(v, a[--j])) if (j == lo) break; - if (i >= j) break; - exch(a, i, j); - } - exch(a, lo, j); - return j; -} -``` - -### 3.3 性能分析 - -快速排序是原地排序,不需要辅助数组,但是递归调用需要辅助栈。 - -快速排序最好的情况下是每次都正好能将数组对半分,这样递归调用次数才是最少的。这种情况下比较次数为 CN=2CN/2+N,也就是复杂度为 O(NlgN)。 - -最坏的情况下,第一次从最小的元素切分,第二次从第二小的元素切分,如此这般。因此最坏的情况下需要比较 N2/2。为了防止数组最开始就是有序的,在进行快速排序时需要随机打乱数组。 - -### 3.4 算法改进 - -#### 3.4.1 切换到插入排序 - -因为快速排序在小数组中也会调用自己,对于小数组,插入排序比快速排序的性能更好,因此在小数组中可以切换到插入排序。 - -#### 3.4.2 三取样 - -最好的情况下是每次都能取数组的中位数作为切分元素,但是计算中位数的代价很高。人们发现取 3 个元素并将大小居中的元素作为切分元素的效果最好。 - -#### 3.4.3 三向切分 - -对于有大量重复元素的数组,可以将数组切分为三部分,分别对应小于、等于和大于切分元素。 - -三向切分快速排序对于只有若干不同主键的随机数组可以在线性时间内完成排序。 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//9d2226dc-c4a3-40ec-9b3e-a46bf86af499.png) - -```java -public class Quick3Way { - public static void sort(Comparable[] a, int lo, int hi) { - if (hi <= lo) return; - int lt = lo, i = lo + 1, gt = hi; - Comparable v = a[lo]; - while (i <= gt) { - int cmp = a[i].compareTo(v); - if (cmp < 0) exch(a, lt++, i++); - else if (cmp > 0) exch(a, i, gt--); - else i++; - } - sort(a, lo, lt - 1); - sort(a, gt + 1, hi); - } -} -``` - -## 4. 优先队列 - -优先队列主要用于处理最大元素。 - -### 4.1 堆 - -定义:一颗二叉树的每个节点都大于等于它的两个子节点。 - -堆可以用数组来表示,因为堆是一种完全二叉树,而完全二叉树很容易就存储在数组中。位置 k 的节点的父节点位置为 k/2,而它的两个子节点的位置分别为 2k 和 2k+1。这里我们不使用数组索引为 0 的位置,是为了更清晰地理解节点的关系。 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//a9b6c1db-0f4a-4e91-8ac8-6b19bd106b51.png) - -```java -public class MaxPQ { - private Key[] pq; - private int N = 0; - - public MaxPQ(int maxN) { - pq = (Key[]) new Comparable[maxN + 1]; - } - - public boolean isEmpty() { - return N == 0; - } - - public int size() { - return N; - } - - private boolean less(int i, int j) { - return pq[i].compareTo(pq[j]) < 0; - } - - private void exch(int i, int j) { - Key t = pq[i]; - pq[i] = pq[j]; - pq[j] = t; - } -} -``` - -### 4.2 上浮和下沉 - -在堆中,当一个节点比父节点大,那么需要交换这个两个节点。交换后还可能比它新的父节点大,因此需要不断地进行比较和交换操作。把这种操作称为上浮。 - -```java -private void swim(int k) { - while (k > 1 && less(k / 2, k)) { - exch(k / 2, k); - k = k / 2; - } -} -``` - -类似地,当一个节点比子节点来得小,也需要不断的向下比较和交换操作,把这种操作称为下沉。一个节点有两个子节点,应当与两个子节点中最大那么节点进行交换。 - -```java -private void sink(int k) { - while (2 * k <= N) { - int j = 2 * k; - if (j < N && less(j, j + 1)) j++; - if (!less(k, j)) break; - exch(k, j); - k = j; - } -} -``` - -### 4.3 插入元素 - -将新元素放到数组末尾,然后上浮到合适的位置。 - -```java -public void insert(Key v) { - pq[++N] = v; - swim(N); -} -``` - -### 4.4 删除最大元素 - -从数组顶端删除最大的元素,并将数组的最后一个元素放到顶端,并让这个元素下沉到合适的位置。 - -```java -public Key delMax() { - Key max = pq[1]; - exch(1, N--); - pq[N + 1] = null; - sink(1); - return max; -} -``` - -### 4.5 堆排序 - -由于堆可以很容易得到最大的元素并删除它,不断地进行这种操作可以得到一个递减序列。如果把最大元素和当前堆中数组的最后一个元素交换位置,并且不删除它,那么就可以得到一个从尾到头的递减序列,从正向来看就是一个递增序列。因此很容易使用堆来进行排序,并且堆排序是原地排序,不占用额外空间。 - -堆排序要分两个阶段,第一个阶段是把无序数组建立一个堆;第二个阶段是交换最大元素和当前堆的数组最后一个元素,并且进行下沉操作维持堆的有序状态。 - -无序数组建立堆最直接的方法是从左到右遍历数组,然后进行上浮操作。一个更高效的方法是从右至左进行下沉操作,如果一个节点的两个节点都已经是堆有序,那么进行下沉操作可以使得这个节点为根节点的堆有序。叶子节点不需要进行下沉操作,因此可以忽略叶子节点的元素,因此只需要遍历一半的元素即可。 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//a2670745-a7b1-497b-90a4-dbddc4e2006d.jpg) - -```java -public static void sort(Comparable[] a){ - int N = a.length; - for(int k = N/2; k >= 1; k--){ - sink(a, k, N); - } - while(N > 1){ - exch(a, 1, N--); - sink(a, 1, N); - } -} -``` - -### 4.6 分析 - -一个堆的高度为 lgN,因此在堆中插入元素和删除最大元素的复杂度都为 lgN。 - -对于堆排序,由于要对 N 个节点进行下沉操作,因此复杂度为 NlgN。 - -堆排序时一种原地排序,没有利用额外的空间。 - -现代操作系统很少使用堆排序,因为它无法利用缓存,也就是数组元素很少和相邻的元素进行比较。 - -## 5. 应用 - -### 5.1 排序算法的比较 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//be53c00b-2534-4dc6-ad03-c55995c47db9.jpg) - -快速排序时最快的通用排序算法,它的内循环的指令很少,而且它还能利用缓存,因为它总是顺序地访问数据。它的运行时间增长数量级为 \~cNlgN,这里的 c 比其他线性对数级别的排序算法都要小。使用三向切分之后,实际应用中可能出现的某些分布的输入能够达到线性级别,而其它排序算法仍然需要线性对数时间。 - -### 5.2 Java 的排序算法实现 - -Java 系统库中的主要排序方法为 java.util.Arrays.sort(),对于原始数据类型使用三向切分的快速排序,对于引用类型使用归并排序。 - -### 5.3 基于切分的快速选择算法 - -快速排序的 partition() 方法,会将数组的 a[lo] 至 a[hi] 重新排序并返回一个整数 j 使得 a[lo..j-1] 小于等于 a[j],且 a[j+1..hi] 大于等于 a[j]。那么如果 j=k,a[j] 就是第 k 个数。 - -该算法是线性级别的,因为每次正好将数组二分,那么比较的总次数为 (N+N/2+N/4+..),直到找到第 k 个元素,这个和显然小于 2N。 - -```java -public static Comparable select(Comparable[] a, int k) { - int lo = 0, hi = a.length - 1; - while (hi > lo) { - int j = partion(a, lo, hi); - if (j == k) return a[k]; - else if (j > k) hi = j - 1; - else lo = j + 1; - } - return a[k]; -} -``` - -# 查找 - -本章使用三种经典的数据类型来实现高效的符号表:二叉查找树、红黑树和散列表。 - -## 1. 符号表 - -### 1.1 无序符号表 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//b69d7184-ab62-4957-ba29-fb4fa25f9b65.jpg) - -### 1.2 有序符号表 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//ba6ae411-82da-4d86-a434-6776d1731e8e.jpg) - -有序符号表的键需要实现 Comparable 接口。 - -查找的成本模型:键的比较次数,在不进行比较时使用数组的访问次数。 - -### 1.3 二分查找实现有序符号表 - -使用一对平行数组,一个存储键一个存储值。 - -需要创建一个 Key 类型的 Comparable 对象数组和一个 Value 类型的 Object 对象数组。 - -rank() 方法至关重要,当键在表中时,它能够知道该键的位置;当键不在表中时,它也能知道在何处插入新键。 - -复杂度:二分查找最多需要 lgN+1 次比较,使用二分查找实现的符号表的查找操作所需要的时间最多是对数级别的。但是插入操作需要移动数组元素,是线性级别的。 - -```java -public class BinarySearchST, Value> { - private Key[] keys; - private Value[] values; - private int N; - - public BinarySearchST(int capacity) { - keys = (Key[]) new Comparable[capacity]; - values = (Value[]) new Object[capacity]; - } - - public int size() { - return N; - } - - public Value get(Key key) { - int i = rank(key); - if (i < N && keys[i].compareTo(key) == 0) { - return values[i]; - } - return null; - } - - public int rank(Key key) { - int lo = 0, hi = N - 1; - while (lo <= hi) { - int mid = lo + (hi - lo) / 2; - int cmp = key.compareTo(keys[mid]); - if (cmp == 0) return mid; - else if (cmp < 0) hi = mid - 1; - else lo = mid + 1; - } - return lo; - } - - public void put(Key key, Value value) { - int i = rank(key); - if (i < N && keys[i].compareTo(key) == 0) { - values[i] = value; - return; - } - for (int j = N; j > i; j--) { - keys[j] = keys[j - 1]; - values[j] = values[j - 1]; - } - keys[i] = key; - values[i] = value; - N++; - } - - public Key ceiling(Key key){ - int i = rank(key); - return keys[i]; - } -} -``` - -## 2. 二叉查找树 - -**二叉树** 定义为一个空链接,或者是一个有左右两个链接的节点,每个链接都指向一颗子二叉树。 - -**二叉查找树**(BST)是一颗二叉树,并且每个节点的键都大于其左子树中的任意节点的键而小于右子树的任意节点的键。 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//25226bb2-92cc-40cb-9e7f-c44e79fbb64a.jpg) - -二叉查找树的查找操作每次迭代都会让区间减少一半,和二分查找类似。 - -```java -public class BST, Value> { - private Node root; - - private class Node { - private Key key; - private Value val; - private Node left, right; - // 以该节点为根的子树中节点总数 - private int N; - - public Node(Key key, Value val, int N) { - this.key = key; - this.val = val; - this.N = N; - } - } - - public int size() { - return size(root); - } - - private int size(Node x) { - if (x == null) return 0; - return x.N; - } -} -``` - -### 2.1 get() - -如果树是空的,则查找未命中;如果被查找的键和根节点的键相等,查找命中,否则递归地在子树中查找:如果被查找的键较小就在左子树中查找,较大就在右子树中查找。 - -```java -public Value get(Key key) { - return get(root, key); -} -private Value get(Node x, Key key) { - if (x == null) return null; - int cmp = key.compareTo(x.key); - if (cmp == 0) return x.val; - else if (cmp < 0) return get(x.left, key); - else return get(x.right, key); -} -``` - -### 2.2 put() - -当插入的键不存在于树中,需要创建一个新节点,并且更新上层节点的链接使得该节点正确链接到树中。 - -```java -public void put(Key key, Value val) { - root = put(root, key, val); -} -private Node put(Node x, Key key, Value val) { - if (x == null) return new Node(key, val, 1); - int cmp = key.compareTo(x.key); - if (cmp == 0) x.val = val; - else if (cmp < 0) x.left = put(x.left, key, val); - else x.right = put(x.right, key, val); - x.N = size(x.left) + size(x.right) + 1; - return x; -} -``` - -### 2.3 分析 - -二叉查找树的算法运行时间取决于树的形状,而树的形状又取决于键被插入的先后顺序。最好的情况下树是完全平衡的,每条空链接和根节点的距离都为 lgN。在最坏的情况下,树的高度为 N。 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//73a3983d-dd18-4373-897e-64b706a7e370.jpg) - -复杂度:查找和插入操作都为对数级别。 - -### 2.4 floor() - -如果 key 小于根节点的 key,那么小于等于 key 的最大键节点一定在左子树中;如果 key 大于根节点的 key,只有当根节点右子树中存在小于等于 key 的节点,小于等于 key 的最大键节点才在右子树中,否则根节点就是小于等于 key 的最大键节点。 - -```java -public Key floor(Key key) { - Node x = floor(root, key); - if (x == null) return null; - return x.key; -} -private Node floor(Node x, Key key) { - if (x == null) return null; - int cmp = key.compareTo(x.key); - if (cmp == 0) return x; - if (cmp < 0) return floor(x.left, key); - Node t = floor(x.right, key); - if (t != null) { - return t; - } else { - return x; - } -} -``` - -### 2.5 rank() - -```java -public int rank(Key key) { - return rank(key, root); -} -private int rank(Key key, Node x) { - if (x == null) return 0; - int cmp = key.compareTo(x.key); - if (cmp == 0) return size(x.left); - else if (cmp < 0) return rank(key, x.left); - else return 1 + size(x.left) + rank(key, x.right); -} -``` - -### 2.6 min() - -```java -private Node min(Node x) { - if (x.left == null) return x; - return min(x.left); -} -``` - -### 2.7 deleteMin() - -令指向最小节点的链接指向最小节点的右子树。 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//6e2cb20a-8d2a-46fe-9ac7-68a2126b7bd5.jpg) - -```java -public void deleteMin() { - root = deleteMin(root); -} -public Node deleteMin(Node x) { - if (x.left == null) return x.right; - x.left = deleteMin(x.left); - x.N = size(x.left) + size(x.right) + 1; - return x; -} -``` - -### 2.8 delete() - -如果待删除的节点只有一个子树,那么只需要让指向待删除节点的链接指向唯一的子树即可;否则,让右子树的最小节点替换该节点。 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//b488282d-bfe0-464f-9e91-1f5b83a975bd.jpg) - -```java -public void delete(Key key) { - root = delete(root, key); -} -private Node delete(Node x, Key key) { - if (x == null) return null; - int cmp = key.compareTo(x.key); - if (cmp < 0) x.left = delete(x.left, key); - else if (cmp > 0) x.right = delete(x.right, key); - else { - if (x.right == null) return x.left; - if (x.left == null) return x.right; - Node t = x; - x = min(t.right); - x.right = deleteMin(t.right); - x.left = t.left; - } - x.N = size(x.left) + size(x.right) + 1; - return x; -} -``` - -### 2.9 keys() - -利用二叉查找树中序遍历的结果为有序序列的特点。 - -```java -public Iterable keys(Key lo, Key hi) { - Queue queue = new LinkedList<>(); - keys(root, queue, lo, hi); - return queue; -} -private void keys(Node x, Queue queue, Key lo, Key hi) { - if (x == null) return; - int cmpLo = lo.compareTo(x.key); - int cmpHi = hi.compareTo(x.key); - if (cmpLo < 0) keys(x.left, queue, lo, hi); - if (cmpLo <= 0 && cmpHi >= 0) queue.add(x.key); - if (cmpHi > 0) keys(x.right, queue, lo, hi); -} -``` - -### 2.10 性能分析 - -复杂度:二叉查找树所有操作在最坏的情况下所需要的时间都和树的高度成正比。 - -## 3. 平衡查找树 - -### 3.1 2-3 查找树 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//2548f2ec-7b00-4ec7-b286-20fc3022e084.jpg) - -一颗完美平衡的 2-3 查找树的所有空链接到根节点的距离应该是相同的。 - -#### 3.1.1 插入操作 - -当插入之后产生一个临时 4- 节点时,需要将 4- 节点分裂成 3 个 2- 节点,并将中间的 2- 节点移到上层节点中。如果上移操作继续产生临时 4- 节点则一直进行分裂上移,直到不存在临时 4- 节点。 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//912174d8-0786-4222-b7ef-a611d36e5db9.jpg) - -#### 3.1.2 性质 - -2-3 查找树插入操作的变换都是局部的,除了相关的节点和链接之外不必修改或者检查树的其它部分,而这些局部变换不会影响树的全局有序性和平衡性。 - -2-3 查找树的查找和插入操作复杂度和插入顺序 **无关**,在最坏的情况下查找和插入操作访问的节点必然不超过 logN 个,含有 10 亿个节点的 2-3 查找树最多只需要访问 30 个节点就能进行任意的查找和插入操作。 - -### 3.2 红黑二叉查找树 - -2-3 查找树需要用到 2- 节点和 3- 节点,红黑树使用红链接来实现 3- 节点。指向一个节点的链接颜色如果为红色,那么这个节点和上层节点表示的是一个 3- 节点,而黑色则是普通链接。 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//7080a928-06ba-4e10-9792-b8dd190dc8e2.jpg) - -红黑树具有以下性质: - -1. 红链接都为左链接; -2. 完美黑色平衡,即任意空链接到根节点的路径上的黑链接数量相同。 - -画红黑树时可以将红链接画平。 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//62077f5d-a06d-4129-9b43-78715b82cb03.png) - -```java -public class RedBlackBST, Value> { - private Node root; - private static final boolean RED = true; - private static final boolean BLACK = false; - - private class Node { - Key key; - Value val; - Node left, right; - int N; - boolean color; - - Node(Key key, Value val, int n, boolean color) { - this.key = key; - this.val = val; - N = n; - this.color = color; - } - } - - private boolean isRed(Node x) { - if (x == null) return false; - return x.color == RED; - } -} -``` - -#### 3.2.1 左旋转 - -因为合法的红链接都为左链接,如果出现右链接为红链接,那么就需要进行左旋转操作。 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//33a4e822-2dd0-481e-ac89-7f6161034402.jpg) - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//5e0cef33-4087-4f21-a428-16d5fddda671.jpg) - -```java -public Node rotateLeft(Node h) { - Node x = h.right; - h.right = x.left; - x.left = h; - x.color = h.color; - h.color = RED; - x.N = h.N; - h.N = 1 + size(h.left) + size(h.right); - return x; -} -``` - -#### 3.2.2 右旋转 - -进行右旋转是为了转换两个连续的左红链接,这会在之后的插入过程中探讨。 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//dfd078b2-aa4f-4c50-8319-232922d822b8.jpg) - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//3f8d8c9d-a9a9-4d7a-813c-2de05ee5a97e.jpg) - -```java -public Node rotateRight(Node h) { - Node x = h.left; - h.left = x.right; - x.color = h.color; - h.color = RED; - x.N = h.N; - h.N = 1 + size(h.left) + size(h.right); - return x; -} -``` - -#### 3.2.3 颜色转换 - -一个 4- 节点在红黑树中表现为一个节点的左右子节点都是红色的。分裂 4- 节点除了需要将子节点的颜色由红变黑之外,同时需要将父节点的颜色由黑变红,从 2-3 树的角度看就是将中间节点移到上层节点。 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//de7c5a31-55f5-4e9d-92ec-4ed5b2ec3828.jpg) - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//e5ad625e-729d-4a8d-923a-7c3df5773e1c.jpg) - -```java -void flipColors(Node h){ - h.color = RED; - h.left.color = BLACK; - h.right.color = BLACK; -} -``` - -#### 3.2.4 插入 - -先将一个节点按二叉查找树的方法插入到正确位置,然后再进行如下颜色操作: - -- 如果右子节点是红色的而左子节点是黑色的,进行左旋转; -- 如果左子节点是红色的且它的左子节点也是红色的,进行右旋转; -- 如果左右子节点均为红色的,进行颜色转换。 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//40639782-5df2-4e96-a4f3-f9dd664d0ca1.jpg) - -```java -public void put(Key key, Value val) { - root = put(root, key, val); - root.color = BLACK; -} - -private Node put(Node x, Key key, Value val) { - if (x == null) return new Node(key, val, 1, RED); - int cmp = key.compareTo(x.key); - if (cmp == 0) x.val = val; - else if (cmp < 0) x.left = put(x.left, key, val); - else x.right = put(x.right, key, val); - - if (isRed(x.right) && !isRed(x.left)) x = rotateLeft(x); - if (isRed(x.left) && isRed(x.left.left)) x = rotateRight(x); - if (isRed(x.left) && isRed(x.right)) flipColors(x); - - x.N = size(x.left) + size(x.right) + 1; - return x; -} -``` - -可以看到该插入操作和 BST 的插入操作类似,只是在最后加入了旋转和颜色变换操作即可。 - -根节点一定为黑色,因为根节点没有上层节点,也就没有上层节点的左链接指向根节点。flipColors() 有可能会使得根节点的颜色变为红色,每当根节点由红色变成黑色时树的黑链接高度加 1. - -#### 3.2.5 删除最小键 - -如果最小键在一个 2- 节点中,那么删除该键会留下一个空链接,就破坏了平衡性,因此要确保最小键不在 2- 节点中。将 2- 节点转换成 3- 节点或者 4- 节点有两种方法,一种是向上层节点拿一个 key,一种是向兄弟节点拿一个 key。如果上层节点是 2- 节点,那么就没办法从上层节点拿 key 了,因此要保证删除路径上的所有节点都不是 2- 节点。在向下删除的过程中,保证以下情况之一发生: - -1. 如果当前节点的左子节点不是 2- 节点,完成; -2. 如果当前节点的左子节点是 2- 节点而它的兄弟节点不是 2- 节点,向兄弟节点拿一个 key 过来; -3. 如果当前节点的左子节点和它的兄弟节点都是 2- 节点,将左子节点、父节点中的最小键和最近的兄弟节点合并为一个 4- 节点。 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//b001fa64-307c-49af-b4b2-2043fc26154e.png) - -最后得到一个含有最小键的 3- 节点或者 4- 节点,直接从中删除。然后再从头分解所有临时的 4- 节点。 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//70b66757-755c-4e17-a7b7-5ce808023643.png) - -#### 3.2.6 分析 - -一颗大小为 N 的红黑树的高度不会超过 2lgN。最坏的情况下是它所对应的 2-3 树中构成最左边的路径节点全部都是 3- 节点而其余都是 2- 节点。 - -红黑树大多数的操作所需要的时间都是对数级别的。 - -## 4. 散列表 - -散列表类似于数组,可以把散列表的散列值看成数组的索引值。访问散列表和访问数组元素一样快速,它可以在常数时间内实现查找和插入的符号表。 - -由于无法通过散列值知道键的大小关系,因此散列表无法实现有序性操作。 - -### 4.1 散列函数 - -对于一个大小为 M 的散列表,散列函数能够把任意键转换为 [0, M-1] 内的正整数,该正整数即为 hash 值。 - -散列表有冲突的存在,也就是两个不同的键可能有相同的 hash 值。 - -散列函数应该满足以下三个条件: - -1. 一致性:相等的键应当有相等的 hash 值。 -2. 高效性:计算应当简便,有必要的话可以把 hash 值缓存起来,在调用 hash 函数时直接返回。 -3. 均匀性:所有键的 hash 值应当均匀地分布到 [0, M-1] 之间,这个条件至关重要,直接影响到散列表的性能。 - -除留余数法可以将整数散列到 [0, M-1] 之间,例如一个正整数 k,计算 k%M 既可得到一个 [0, M-1] 之间的 hash 值。注意 M 必须是一个素数,否则无法利用键包含的所有信息。例如 M 为 10k,那么只能利用键的后 k 位。 - -对于其它数,可以将其转换成整数的形式,然后利用除留余数法。例如对于浮点数,可以将其表示成二进制形式,然后使用二进制形式的整数值进行除留余数法。 - -对于有多部分组合的键,每部分都需要计算 hash 值,并且最后合并时需要让每部分 hash 值都具有同等重要的地位。可以将该键看成 R 进制的整数,键中每部分都具有不同的权值。 - -例如,字符串的散列函数实现如下 - -```java -int hash = 0; -for(int i = 0; i < s.length(); i++) - hash = (R * hash + s.charAt(i)) % M; -``` - -再比如,拥有多个成员的自定义类的哈希函数如下 - -```java -int hash = (((day * R + month) % M) * R + year) % M; -``` - -R 的值不是很重要,通常取 31。 - -Java 中的 hashCode() 实现了 hash 函数,但是默认使用对象的内存地址值。在使用 hashCode() 函数时,应当结合除留余数法来使用。因为内存地址是 32 位整数,我们只需要 31 位的非负整数,因此应当屏蔽符号位之后再使用除留余数法。 - -```java -int hash = (x.hashCode() & 0x7fffffff) % M; -``` - -使用 Java 自带的 HashMap 等自带的哈希表实现时,只需要去实现 Key 类型的 hashCode() 函数即可。Java 规定 hashCode() 能够将键均匀分布于所有的 32 位整数,Java 中的 String、Integer 等对象的 hashCode() 都能实现这一点。以下展示了自定义类型如何实现 hashCode()。 - -```java -public class Transaction{ - private final String who; - private final Date when; - private final double amount; - - public int hashCode(){ - int hash = 17; - hash = 31 * hash + who.hashCode(); - hash = 31 * hash + when.hashCode(); - hash = 31 * hash + ((Double) amount).hashCode(); - return hash; - } -} -``` - -### 4.2 基于拉链法的散列表 - -拉链法使用链表来存储 hash 值相同的键,从而解决冲突。此时查找需要分两步,首先查找 Key 所在的链表,然后在链表中顺序查找。 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//540133af-aaaf-4208-8f7f-33cb89ac9621.png) - -对于 N 个键,M 条链表 (N>M),如果哈希函数能够满足均匀性的条件,每条链表的大小趋向于 N/M,因此未命中的查找和插入操作所需要的比较次数为 \~N/M。 - -### 4.3 基于线性探测法的散列表 - -线性探测法使用空位来解决冲突,当冲突发生时,向前探测一个空位来存储冲突的键。使用线程探测法,数组的大小 M 应当大于键的个数 N(M>N)。 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//2b3410f1-9559-4dd1-bc3d-e3e572247be2.png) - -```java -public class LinearProbingHashST { - private int N; - private int M = 16; - private Key[] keys; - private Value[] vals; - - public LinearProbingHashST() { - init(); - } - - public LinearProbingHashST(int M) { - this.M = M; - init(); - } - - private void init() { - keys = (Key[]) new Object[M]; - vals = (Value[]) new Object[M]; - } - - private int hash(Key key) { - return (key.hashCode() & 0x7fffffff) % M; - } -} -``` - -#### 4.3.1 查找 - -```java -public Value get(Key key) { - for (int i = hash(key); keys[i] != null; i = (i + 1) % M) { - if (keys[i].equals(key)) { - return vals[i]; - } - } - return null; -} -``` - -#### 4.3.2 插入 - -```java -public void put(Key key, Value val) { - int i; - for (i = hash(key); keys[i] != null; i = (i + 1) % M) { - if (keys[i].equals(key)) { - vals[i] = val; - return; - } - } - keys[i] = key; - vals[i] = val; - N++; - resize(); -} -``` - -#### 4.3.3 删除 - -删除操作应当将右侧所有相邻的键值重新插入散列表中。 - -```java -public void delete(Key key) { - if (!contains(key)) return; - int i = hash(key); - while (!key.equals(keys[i])) { - i = (i + 1) % M; - } - keys[i] = null; - vals[i] = null; - i = (i + 1) % M; - while (keys[i] != null) { - Key keyToRedo = keys[i]; - Value valToRedo = vals[i]; - keys[i] = null; - vals[i] = null; - N--; - put(keyToRedo, valToRedo); - i = (i + 1) % M; - } - N--; - resize(); -} -``` - -#### 4.3.4 调整数组大小 - -线性探测法的成本取决于连续条目的长度,连续条目也叫聚簇。当聚簇很长时,在查找和插入时也需要进行很多次探测。 - -α = N/M,把 α 称为利用率。理论证明,当 α 小于 1/2 时探测的预计次数只在 1.5 到 2.5 之间。 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//0ddebc5c-7c24-46b1-98db-4fa5e54db16b.png) - -为了保证散列表的性能,应当调整数组的大小,使得 α 在 [1/4, 1/2] 之间。 - -```java -private void resize() { - if (N >= M / 2) resize(2 * M); - else if (N <= M / 8) resize(M / 2); -} - -private void resize(int cap) { - LinearProbingHashST t = new LinearProbingHashST<>(cap); - for (int i = 0; i < M; i++) { - if (keys[i] != null) { - t.put(keys[i], vals[i]); - } - } - keys = t.keys; - vals = t.vals; - M = t.M; -} -``` - -虽然每次重新调整数组都需要重新把每个键值对插入到散列表,但是从摊还分析的角度来看,所需要的代价却是很小的。从下图可以看出,每次数组长度加倍后,累计平均值都会增加 1,因为表中每个键都需要重新计算散列值,但是随后平均值会下降。 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//01658047-0d86-4a7a-a8ca-7ea20fa1fdde.png) - -## 5. 应用 - -### 5.1 各种符号表实现的比较 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//9ee83c8c-1165-476c-85a6-e6e434e5307a.jpg) - -应当优先考虑散列表,当需要有序性操作时使用红黑树。 - -### 5.2 Java 的符号表实现 - -Java 的 java.util.TreeMap 和 java.util.HashMap 分别是基于红黑树和拉链法的散列表的符号表实现。 - -### 5.3 集合类型 - -除了符号表,集合类型也经常使用,它只有键没有值,可以用集合类型来存储一系列的键然后判断一个键是否在集合中。 - -### 5.4 稀疏向量乘法 - -当向量为稀疏向量时,可以使用符号表来存储向量中的非 0 索引和值,使得乘法运算只需要对那些非 0 元素进行即可。 - -```java -import java.util.HashMap; - -public class SparseVector { - private HashMap hashMap; - - public SparseVector(double[] vector) { - hashMap = new HashMap<>(); - for (int i = 0; i < vector.length; i++) { - if (vector[i] != 0) { - hashMap.put(i, vector[i]); - } - } - } - - public double get(int i) { - return hashMap.getOrDefault(i, 0.0); - } - - public double dot(SparseVector other) { - double sum = 0; - for (int i : hashMap.keySet()) { - sum += this.get(i) * other.get(i); - } - return sum; - } -} -``` diff --git a/notes/计算机操作系统.md b/notes/计算机操作系统.md deleted file mode 100644 index 19820759..00000000 --- a/notes/计算机操作系统.md +++ /dev/null @@ -1,735 +0,0 @@ - -* [第一章 概述](#第一章-概述) - * [操作系统基本特征](#操作系统基本特征) - * [1. 并发](#1-并发) - * [2. 共享](#2-共享) - * [3. 虚拟](#3-虚拟) - * [4. 异步](#4-异步) - * [系统调用](#系统调用) - * [中断分类](#中断分类) - * [1. 外中断](#1-外中断) - * [2. 异常](#2-异常) - * [3. 陷入](#3-陷入) - * [大内核和微内核](#大内核和微内核) - * [1. 大内核](#1-大内核) - * [2. 微内核](#2-微内核) -* [第二章 进程管理](#第二章-进程管理) - * [进程与线程](#进程与线程) - * [1. 进程](#1-进程) - * [2. 线程](#2-线程) - * [3. 区别](#3-区别) - * [进程状态的切换](#进程状态的切换) - * [调度算法](#调度算法) - * [1. 批处理系统中的调度](#1-批处理系统中的调度) - * [1.1 先来先服务](#11-先来先服务) - * [1.2 短作业优先](#12-短作业优先) - * [1.3 最短剩余时间优先](#13-最短剩余时间优先) - * [2. 交互式系统中的调度](#2-交互式系统中的调度) - * [2.1 优先权优先](#21-优先权优先) - * [2.2 时间片轮转](#22-时间片轮转) - * [2.3 多级反馈队列](#23-多级反馈队列) - * [2.4 短进程优先](#24-短进程优先) - * [3. 实时系统中的调度](#3-实时系统中的调度) - * [进程同步](#进程同步) - * [1. 临界区](#1-临界区) - * [2. 同步与互斥](#2-同步与互斥) - * [3. 信号量](#3-信号量) - * [4. 管程](#4-管程) - * [进程通信](#进程通信) - * [1. 管道](#1-管道) - * [2. 信号量](#2-信号量) - * [3. 消息队列](#3-消息队列) - * [4. 信号](#4-信号) - * [5. 共享内存](#5-共享内存) - * [6. 套接字](#6-套接字) - * [经典同步问题](#经典同步问题) - * [1. 读者-写者问题](#1-读者-写者问题) - * [2. 哲学家进餐问题](#2-哲学家进餐问题) -* [第三章 死锁](#第三章-死锁) - * [死锁的条件](#死锁的条件) - * [死锁的处理方法](#死锁的处理方法) - * [1. 鸵鸟策略](#1-鸵鸟策略) - * [2. 死锁预防](#2-死锁预防) - * [2.1 破坏互斥条件](#21-破坏互斥条件) - * [2.2 破坏请求与保持条件](#22-破坏请求与保持条件) - * [2.3 破坏不可抢占条件](#23-破坏不可抢占条件) - * [2.4 破坏环路等待](#24-破坏环路等待) - * [3. 死锁避免](#3-死锁避免) - * [3.1 安全状态](#31-安全状态) - * [3.2 单个资源的银行家算法](#32-单个资源的银行家算法) - * [3.3 多个资源的银行家算法](#33-多个资源的银行家算法) - * [4. 死锁检测与死锁恢复](#4-死锁检测与死锁恢复) - * [4.1 死锁检测算法](#41-死锁检测算法) - * [4.2 死锁恢复](#42-死锁恢复) -* [第四章 存储器管理](#第四章-存储器管理) - * [虚拟内存](#虚拟内存) - * [分页与分段](#分页与分段) - * [1. 分页](#1-分页) - * [2. 分段](#2-分段) - * [3. 段页式](#3-段页式) - * [4. 分页与分段区别](#4-分页与分段区别) - * [页面置换算法](#页面置换算法) - * [1. 最佳(Optimal)](#1-最佳optimal) - * [2. 先进先出(FIFO)](#2-先进先出fifo) - * [3. 最近最久未使用(LRU, Least Recently Used)](#3-最近最久未使用lru,-least-recently-used) - * [4. 时钟(Clock)](#4-时钟clock) -* [第五章 设备管理](#第五章-设备管理) - * [磁盘调度算法](#磁盘调度算法) - * [1. 先来先服务(FCFS, First Come First Serverd)](#1-先来先服务fcfs,-first-come-first-serverd) - * [2. 最短寻道时间优先(SSTF, Shortest Seek Time First)](#2-最短寻道时间优先sstf,-shortest-seek-time-first) - * [3. 扫描算法(SCAN)](#3-扫描算法scan) - * [4. 循环扫描算法(CSCAN)](#4-循环扫描算法cscan) -* [参考资料](#参考资料) - - - -# 第一章 概述 - -## 操作系统基本特征 - -### 1. 并发 - -并发性是指宏观上在一段时间内能同时运行多个程序,而并行性则指同一时刻能运行多个指令。 - -并行需要硬件支持,如多流水线或者多处理器。 - -操作系统通过引入进程和线程,使得程序能够并发运行。 - -### 2. 共享 - -共享是指系统中的资源可以供多个并发进程共同使用。 - -有两种共享方式:互斥共享和同时共享。 - -互斥共享的资源称为临界资源,例如打印机等,在同一时间只允许一个进程访问,否则会出现错误,需要用同步机制来实现对临界资源的访问。 - -### 3. 虚拟 - -虚拟技术把一个物理实体转换为多个逻辑实体。主要有两种虚拟技术:时分复用技术和空分复用技术,例如多个进程能在同一个处理器上并发执行使用了时分复用技术,让每个进程轮流占有处理器,每次只执行一小个时间片并快速切换。 - -### 4. 异步 - -异步是指进程不是一次性执行完毕,而是走走停停,以不可知的速度向前推进。 - -## 系统调用 - -如果一个进程在用户态需要用到操作系统的一些功能,就需要使用系统调用从而陷入内核,由操作系统代为完成。 - -可以由系统调用请求的功能有设备管理、文件管理、进程管理、进程通信、存储器管理等。 - -## 中断分类 - -### 1. 外中断 - -由 CPU 执行指令以外的事件引起,如 I/O 结束中断,表示设备输入/输出处理已经完成,处理器能够发送下一个输入/输出请求。此外还有时钟中断、控制台中断等。 - -### 2. 异常 - -由 CPU 执行指令的内部事件引起,如非法操作码、地址越界、算术溢出等。 - -### 3. 陷入 - -在用户程序中使用系统调用。 - -## 大内核和微内核 - -### 1. 大内核 - -大内核是将操作系统功能作为一个紧密结合的整体放到内核,由于各模块共享信息,因此有很高的性能。 - -### 2. 微内核 - -由于操作系统不断复杂,因此将一部分操作系统功能移出内核,从而降低内核的复杂性。移出的部分根据分层的原则划分成若干服务,相互独立。但是需要频繁地在用户态和核心态之间进行切换,会有一定的性能损失。 - -# 第二章 进程管理 - -## 进程与线程 - -### 1. 进程 - -进程是操作系统进行资源分配的基本单位。 - -进程控制块 (Process Control Block, PCB) 描述进程的基本信息和运行状态,所谓的创建进程和撤销进程,都是指对 PCB 的操作。 - -### 2. 线程 - -一个进程中可以有多个线程,线程是独立调度的基本单位。同一个进程中的多个线程之间可以并发执行,它们共享进程资源。 - -### 3. 区别 - -- 拥有资源:进程是资源分配的基本单位,但是线程不拥有资源,线程可以访问率属进程的资源。 - -- 调度:线程是独立调度的基本单位,在同一进程中,线程的切换不会引起进程切换,从一个进程内的线程切换到另一个进程中的线程时,会引起进程切换。 - -- 系统开销:由于创建或撤销进程时,系统都要为之分配或回收资源,如内存空间、I/O 设备等,因此操作系统所付出的开销远大于创建或撤销线程时的开销。类似地,在进行进程切换时,涉及当前执行进程 CPU 环境的保存及新调度进程 CPU 环境的设置。而线程切换时只需保存和设置少量寄存器内容,开销很小。 - -- 通信方面:进程间通信 (IPC) 需要进程同步和互斥手段的辅助,以保证数据的一致性,而线程间可以通过直接读/写同一进程中的数据段(如全局变量)来进行通信。 - -举例:QQ 和浏览器是两个进程,浏览器进程里面有很多线程,例如 HTTP 请求线程、事件响应线程、渲染线程等等,线程的并发执行使得在浏览器中点击一个新链接从而发起 HTTP 请求时,浏览器还可以响应用户的其它事件。 - -## 进程状态的切换 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//1706ce58-a081-4fed-9b36-c3c0d7e22b3a.jpg) - -阻塞状态是缺少需要的资源从而由运行状态转换而来,但是该资源不包括 CPU,缺少 CPU 会让进程从运行态转换为就绪态。 - -只有就绪态和运行态可以相互转换,其它的都是单向转换。就绪状态的进程通过调度算法从而获得 CPU 时间,转为运行状态;而运行状态的进程,在分配给它的 CPU 时间片用完之后就会转为就绪状态,等待下一次调度。 - -## 调度算法 - -需要针对不同环境来讨论调度算法。 - -### 1. 批处理系统中的调度 - -#### 1.1 先来先服务 - -first-come first-serverd(FCFS)。 - -调度最先进入就绪队列的作业。 - -有利于长作业,但不利于短作业,因为短作业必须一直等待前面的长作业执行完毕才能执行,而长作业又需要执行很长时间,造成了短作业等待时间过长。 - -#### 1.2 短作业优先 - -shortest job first(SJF)。 - -调度估计运行时间最短的作业。 - -长作业有可能会饿死,处于一直等待短作业执行完毕的状态。如果一直有短作业到来,那么长作业永远得不到调度。 - -#### 1.3 最短剩余时间优先 - -shortest remaining time next(SRTN)。 - -### 2. 交互式系统中的调度 - -#### 2.1 优先权优先 - -除了可以手动赋予优先权之外,还可以把响应比作为优先权,这种调度方式叫做高响应比优先调度算法。 - -响应比 = (等待时间 + 要求服务时间) / 要求服务时间 = 响应时间 / 要求服务时间 - -这种调度算法主要是为了解决 SJF 中长作业可能会饿死的问题,因为随着等待时间的增长,响应比也会越来越高。 - -#### 2.2 时间片轮转 - -将所有就绪进程按 FCFS 的原则排成一个队列,每次调度时,把 CPU 分配给队首进程,该进程可以执行一个时间片。当时间片用完时,由计时器发出时钟中断,调度程序便停止该进程的执行,并将它送往就绪队列的末尾,同时继续把 CPU 分配给队首的进程。 - -时间片轮转算法的效率和时间片的大小有很大关系。因为每次进程切换都要保存进程的信息并且载入新进程的信息,如果时间片太小,进程切换太频繁,在进程切换上就会花过多时间。 - -#### 2.3 多级反馈队列 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//042cf928-3c8e-4815-ae9c-f2780202c68f.png) - -1. 设置多个就绪队列,并为各个队列赋予不同的优先级。第一个队列的优先级最高,第二个队列次之,其余各队列的优先权逐个降低。该算法赋予各个队列中进程执行时间片的大小也各不相同,在优先权越高的队列中,为每个进程所规定的执行时间片就越小。 - -2. 当一个新进程进入内存后,首先将它放入第一队列的末尾,按 FCFS 原则排队等待调度。当轮到该进程执行时,如它能在该时间片内完成,便可准备撤离系统;如果它在一个时间片结束时尚未完成,调度程序便将该进程转入下一个队列的队尾。 - -3. 仅当前 i -1 个队列均空时,才会调度第 i 个队列中的进程。 - -优点:实时性好,同时适合运行短作业和长作业。 - -#### 2.4 短进程优先 - -### 3. 实时系统中的调度 - -实时系统要一个服务请求在一个确定时间内得到响应。 - -分为硬实时和软实时,前者必须满足绝对的截止时间,后者可以容忍一定的超时。 - -## 进程同步 - -### 1. 临界区 - -对临界资源进行访问的那段代码称为临界区。 - -为了互斥访问临界资源,每个进程在进入临界区之前,需要先进行检查。 - -```html -// entry section -// critical section; -// exit section -``` - -### 2. 同步与互斥 - -同步指多个进程按一定顺序执行;互斥指多个进程在同一时刻只有一个进程能进入临界区。 - -同步是在对临界区互斥访问的基础上,通过其它机制来实现有序访问的。 - -### 3. 信号量 - -**信号量(Samaphore)** 是一个整型变量,可以对其执行 down 和 up 操作,也就是常见的 P 和 V 操作。 - -- **down** : 如果信号量大于 0 ,执行 -1 操作;如果信号量等于 0,将进程睡眠,等待信号量大于 0; -- **up**:对信号量执行 +1 操作,并且唤醒睡眠的进程,让进程完成 down 操作。 - -down 和 up 操作需要被设计成原语,不可分割,通常的做法是在执行这些操作的时候屏蔽中断。 - -如果信号量的取值只能为 0 或者 1,那么就成为了**互斥量(Mutex)**,0 表示临界区已经加锁,1 表示临界区解锁。 - -```c -typedef int samaphore; -samaphore mutex = 1; -void P1() { - down(mutex); - // 临界区 - up(mutex); -} - -void P2() { - down(mutex); - // 临界区 - up(mutex); -} -``` - -**使用信号量实现生产者-消费者问题** - -使用一个互斥量 mutex 来对临界资源进行访问;empty 记录空缓冲区的数量,full 记录满缓冲区的数量。 - -注意,必须先执行 down 操作再用互斥量对临界区加锁,否则会出现死锁。因为如果都先对临界区加锁,然后再执行 down 操作,那么可能会出现这种情况:生产者对临界区加锁后,执行 down(empty) 操作,发现 empty = 0,此时生成者睡眠。消费者此时不能进入临界区,因为生产者对临界区加锁了,也就无法执行 up(empty) 操作,那么生产者和消费者就会一直等待下去。 - -```c -#define N 100 -typedef int samaphore; -samaphore mutex = 1; -samaphore empty = N; -samaphore full = 0; - -void producer() { - while(TRUE){ - int item = produce_item; - down(empty); - down(mutex); - insert_item(item); - up(mutex); - up(full); - } -} - -void consumer() { - while(TRUE){ - down(full); - down(mutex); - int item = remove_item(item); - up(mutex); - up(empty); - consume_item(item); - } -} -``` - -### 4. 管程 - -使用信号量机制实现的生产者消费者问题需要客户端代码做很多控制,而管程把控制的代码独立出来,不仅不容易出错,也使得客户端代码调用更容易。 - -c 语言不支持管程,下面的示例代码使用了类 Pascal 语言来描述管程。示例代码中的管程提供了 insert() 和 remove() 方法,客户端代码通过调用这两个方法来解决生产者-消费者问题。 - -```pascal -monitor ProducerConsumer - integer i; - condition c; - - procedure insert(); - begin - - end; - - procedure remove(); - begin - - end; -end monitor; -``` - -管程有一个重要特性:在一个时刻只能有一个进程使用管程。进程在无法继续执行的时候不能一直占用管程,否者其它进程永远不能使用管程。 - -管程引入了 **条件变量** 以及相关的操作:**wait()** 和 **signal()** 来实现同步操作。对条件变量执行 wait() 操作会导致调用进程阻塞,把管程让出来让另一个进程持有。signal() 操作用于唤醒被阻塞的进程。 - -**使用管程实现生成者-消费者问题** - -```pascal -monitor ProducerConsumer - condition full, empty; - integer count := 0; - condition c; - - procedure insert(item: integer); - begin - if count = N then wait(full); - insert_item(item); - count := count + 1; - if count = 1 ten signal(empty); - end; - - function remove: integer; - begin - if count = 0 then wait(empty); - remove = remove_item; - count := count - 1; - if count = N -1 then signal(full); - end; -end monitor; - -procedure producer -begin - while true do - begin - item = produce_item; - ProducerConsumer.insert(item); - end -end; - -procedure consumer -begin - while true do - begin - item = ProducerConsumer.remove; - consume_item(item); - end -end; -``` - -## 进程通信 - -进程通信可以看成是不同进程间的线程通信,对于同一个进程内线程的通信方式,主要使用信号量、条件变量等同步机制。 - -### 1. 管道 - -管道是单向的、先进先出的、无结构的、固定大小的字节流,它把一个进程的标准输出和另一个进程的标准输入连接在一起。写进程在管道的尾端写入数据,读进程在管道的首端读出数据。数据读出后将从管道中移走,其它读进程都不能再读到这些数据。 - -管道提供了简单的流控制机制,进程试图读空管道时,在有数据写入管道前,进程将一直阻塞。同样地,管道已经满时,进程再试图写管道,在其它进程从管道中移走数据之前,写进程将一直阻塞。 - -Linux 中管道是通过空文件来实现。 - -管道有三种: - -1. 普通管道:有两个限制:一是只支持半双工通信方式,即只能单向传输;二是只能在父子进程之间使用; -2. 流管道:去除第一个限制,支持双向传输; -3. 命名管道:去除第二个限制,可以在不相关进程之间进行通信。 - -### 2. 信号量 - -信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,防止某进程正在访问共享资源时,其它进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。 - -### 3. 消息队列 - -消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。 - -### 4. 信号 - -信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生。 - -### 5. 共享内存 - -共享内存就是映射一段能被其它进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的 IPC 方式,它是针对其它 IPC 运行效率低而专门设计的。它往往与其它通信机制(如信号量)配合使用,来实现进程间的同步和通信。 - -### 6. 套接字 - -套接字也是一种进程间通信机制,与其它通信机制不同的是,它可用于不同机器间的进程通信。 - -## 经典同步问题 - -生产者和消费者问题前面已经讨论过。 - -### 1. 读者-写者问题 - -允许多个进程同时对数据进行读操作,但是不允许读和写以及写和写操作同时发生。 - -一个整型变量 count 记录在对数据进行读操作的进程数量,一个互斥量 count_mutex 用于对 count 加锁,一个互斥量 data_mutex 用于对读写的数据加锁。 - -```c -typedef int semaphore; -semaphore count_mutex = 1; -semaphore data_mutex = 1; -int count = 0; - -void reader() { - while(TRUE) { - down(count_mutex); - count++; - if(count == 1) down(data_mutex); // 第一个读者需要对数据进行加锁,防止写进程访问 - up(count_mutex); - read(); - down(count_mutex); - count--; - if(count == 0) up(data_mutex); - up(count_mutex); - } -} - -void writer() { - while(TRUE) { - down(data_mutex); - write(); - up(data_mutex); - } -} -``` - -### 2. 哲学家进餐问题 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//a9077f06-7584-4f2b-8c20-3a8e46928820.jpg) - -五个哲学家围着一张圆周,每个哲学家面前放着饭。哲学家的生活有两种交替活动:吃饭以及思考。当一个哲学家吃饭时,需要先一根一根拿起左右两边的筷子。 - -下面是一种错误的解法,考虑到如果每个哲学家同时拿起左手边的筷子,那么就无法拿起右手边的筷子,造成死锁。 - -```c -#define N 5 -#define LEFT (i + N - 1) % N -#define RIGHT (i + N) % N -typedef int semaphore; -semaphore chopstick[N]; - -void philosopher(int i) { - while(TURE){ - think(); - down(chopstick[LEFT[i]]); - down(chopstick[RIGHT[i]]); - eat(); - up(chopstick[RIGHT[i]]); - up(chopstick[LEFT[i]]); - } -} -``` - -为了防止死锁的发生,可以加一点限制,只允许同时拿起左右两边的筷子,方法是引入一个互斥量,对拿起两个筷子的那段代码加锁。 - -```c -semaphore mutex = 1; - -void philosopher(int i) { - while(TURE){ - think(); - down(mutex); - down(chopstick[LEFT[i]]); - down(chopstick[RIGHT[i]]); - up(mutex); - eat(); - down(mutex); - up(chopstick[RIGHT[i]]); - up(chopstick[LEFT[i]]); - up(mutex); - } -} -``` - -# 第三章 死锁 - -## 死锁的条件 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//c037c901-7eae-4e31-a1e4-9d41329e5c3e.png) - -1. 互斥 -2. 请求与保持 -3. 不可抢占 -4. 环路等待 - -其中,请求与保持是指一个进程因请求资源而阻塞时,对已获得的资源保持不放。 - -## 死锁的处理方法 - -### 1. 鸵鸟策略 - -把头埋在沙子里,假装根本没发生问题。 - -这种策略不可取。 - -### 2. 死锁预防 - -在程序运行之前预防发生死锁。 - -#### 2.1 破坏互斥条件 - -例如假脱机打印机技术允许若干个进程同时输出,唯一真正请求物理打印机的进程是打印机守护进程。 - -#### 2.2 破坏请求与保持条件 - -一种实现方式是规定所有进程在开始执行前请求所需要的全部资源。 - -#### 2.3 破坏不可抢占条件 - -#### 2.4 破坏环路等待 - -给资源统一编号,进程只能按编号顺序来请求资源。 - -### 3. 死锁避免 - -在程序运行时避免发生死锁。 - -#### 3.1 安全状态 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//ed523051-608f-4c3f-b343-383e2d194470.png) - -图 a 的第二列 has 表示已拥有的资源数,第三列 max 表示总共需要的资源数,free 表示还有可以使用的资源数。从图 a 开始出发,先让 B 拥有所需的所有资源,运行结束后释放 B,此时 free 变为 4;接着以同样的方式运行 C 和 A,使得所有进程都能成功运行,因此可以称图 a 所示的状态时安全的。 - -定义:如果没有死锁发生,并且即使所有进程突然请求对资源的最大需求,也仍然存在某种调度次序能够使得每一个进程运行完毕,则称该状态是安全的。 - -#### 3.2 单个资源的银行家算法 - -一个小城镇的银行家,他向一群客户分别承诺了一定的贷款额度,算法要做的是判断对请求的满足是否会进入不安全状态,如果是,就拒绝请求;否则予以分配。 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//d160ec2e-cfe2-4640-bda7-62f53e58b8c0.png) - -上图 c 为不安全状态,因此算法会拒绝之前的请求,从而避免进入图 c 中的状态。 - -#### 3.3 多个资源的银行家算法 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//62e0dd4f-44c3-43ee-bb6e-fedb9e068519.png) - -上图中有五个进程,四个资源。左边的图表示已经分配的资源,右边的图表示还需要分配的资源。最右边的 E、P 以及 A 分别表示:总资源、已分配资源以及可用资源,注意这三个为向量,而不是具体数值,例如 A=(1020),表示 4 个资源分别还剩下 1/0/2/0。 - -检查一个状态是否安全的算法如下: - -- 查找右边的矩阵是否存在一行小于等于向量 A。如果不存在这样的行,那么系统将会发生死锁,状态是不安全的。 -- 假若找到这样一行,将该进程标记为终止,并将其已分配资源加到 A 中。 -- 重复以上两步,直到所有进程都标记为终止,则状态时安全的。 - -### 4. 死锁检测与死锁恢复 - -不试图阻止死锁,而是当检测到死锁发生时,采取措施进行恢复。 - -#### 4.1 死锁检测算法 - -死锁检测的基本思想是,如果一个进程所请求的资源能够被满足,那么就让它执行,释放它拥有的所有资源,然后让其它能满足条件的进程执行。 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//e1eda3d5-5ec8-4708-8e25-1a04c5e11f48.png) - -上图中,有三个进程四个资源,每个数据代表的含义如下: - -- E 向量:资源总量 -- A 向量:资源剩余量 -- C 矩阵:每个进程所拥有的资源数量,每一行都代表一个进程拥有资源的数量 -- R 矩阵:每个进程请求的资源数量 - -进程 P1 和 P2 所请求的资源都得不到满足,只有进程 P3 可以,让 P3 执行,之后释放 P3 拥有的资源,此时 A = (2 2 2 0)。P1 可以执行,执行后释放 P1 拥有的资源,A = (4 2 2 2) ,P2 也可以执行。所有进程都可以顺利执行,没有死锁。 - -算法总结如下: - -每个进程最开始时都不被标记,执行过程有可能被标记。当算法结束时,任何没有被标记的进程都是死锁进程。 - -1. 寻找一个没有标记的进程 Pi,它所请求的资源小于等于 A。 -2. 如果找到了这样一个进程,那么将 C 矩阵的第 i 行向量加到 A 中,标记该进程,并转回 1。 -3. 如果有没有这样一个进程,算法终止。 - -#### 4.2 死锁恢复 - -- 利用抢占恢复 -- 杀死进程 - -# 第四章 存储器管理 - -## 虚拟内存 - -每个程序拥有自己的地址空间,这个地址空间被分割成多个块,每一块称为一页。这些页被映射到物理内存,但不需要映射到连续的物理内存,也不需要所有页都必须在物理内存中。 - -当程序引用到一部分在物理内存中的地址空间时,由硬件执行必要的映射,将缺失的部分装入物理内存并重新执行失败的指令。 - -## 分页与分段 - -### 1. 分页 - -用户程序的地址空间被划分为若干固定大小的区域,称为“页”。相应地,内存空间分成若干个物理块,页和块的大小相等。可将用户程序的任一页放在内存的任一块中,实现了离散分配,由一个页表来维护它们之间的映射关系。 - -### 2. 分段 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//22de0538-7c6e-4365-bd3b-8ce3c5900216.png) - -上图为一个编译器在编译过程中建立的多个表,有 4 个表是动态增长的,如果使用分页系统的一维地址空间,动态递增的特点会导致覆盖问题的出现。 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//e0900bb2-220a-43b7-9aa9-1d5cd55ff56e.png) - -分段的做法是把每个表分成段,一个段构成一个独立的地址空间。每个段的长度可以不同,并且可以动态增长。 - -每个段都需要程序员来划分。 - -### 3. 段页式 - -用分段方法来分配和管理虚拟存储器。程序的地址空间按逻辑单位分成基本独立的段,而每一段有自己的段名,再把每段分成固定大小的若干页。 - -用分页方法来分配和管理实存。即把整个主存分成与上述页大小相等的存储块,可装入作业的任何一页。 - -程序对内存的调入或调出是按页进行的,但它又可按段实现共享和保护。 - -### 4. 分页与分段区别 - -- 对程序员的透明性:分页透明,但是分段需要程序员显示划分每个段。 - -- 地址空间的维度:分页是一维地址空间,分段是二维的。 - -- 大小是否可以改变:页的大小不可变,段的大小可以动态改变。 - -- 出现的原因:分页主要用于实现虚拟内存,从而获得更大的地址空间;分段主要是为了使程序和数据可以被划分为逻辑上独立的地址空间并且有助于共享和保护。 - -## 页面置换算法 - -在程序运行过程中,若其所要访问的页面不在内存而需要把它们调入内存,但是内存已无空闲空间时,系统必须从内存中调出一个页面到磁盘对换区中,并且将程序所需要的页面调入内存中。页面置换算法的主要目标是使页面置换频率最低(也可以说缺页率最低)。 - -### 1. 最佳(Optimal) - -所选择的被换出的页面将是最长时间内不再被访问,通常可以保证获得最低的缺页率。 - -是一种理论上的算法,因为无法知道一个页面多长时间会被再访问到。 - -举例:一个系统为某进程分配了三个物理块,并有如下页面引用序列: - -7,0,1,2,0,3,0,4,2,3,0,3,2,1,2,0,1,7,0,1 - -进程运行时,先将 7,0,1 三个页面装入内存。当进程要访问页面 2 时,产生缺页中断,会将页面 7 换出,因为页面 7 再次被访问的时间最长。 - -### 2. 先进先出(FIFO) - -所选择换出的页面是最先进入的页面。 - -该算法会将那些经常被访问的页面也被换出,从而使缺页率升高。 - -### 3. 最近最久未使用(LRU, Least Recently Used) - -虽然无法知道将来要使用的页面情况,但是可以知道过去使用页面的情况。LRU 将最近最久未使用的页面换出。 - -可以用栈来实现该算法,栈中存储页面的页面号。当进程访问一个页面时,将该页面的页面号从栈移除,并将它压入栈顶。这样,最近被访问的页面的页面号总是在栈顶,而最近最久未使用的页面的页面号总是在栈底。 - -4,7,0,7,1,0,1,2,1,2,6 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//eb859228-c0f2-4bce-910d-d9f76929352b.png) - -### 4. 时钟(Clock) - -Clock 页面置换算法需要用到一个访问位,当一个页面被访问时,将访问为置为 1。 - -首先,将内存中的所有页面链接成一个循环队列,当缺页中断发生时,检查当前指针所指向页面的访问位,如果访问位为 0,就将该页面换出;否则将该页的访问位设置为 0,给该页面第二次的机会,移动指针继续检查。 - -# 第五章 设备管理 - -## 磁盘调度算法 - -当多个进程同时请求访问磁盘时,需要进行磁盘调度来控制对磁盘的访问。磁盘调度的主要目标是使磁盘的平均寻道时间最少。 - -### 1. 先来先服务(FCFS, First Come First Serverd) - -根据进程请求访问磁盘的先后次序来进行调度。优点是公平和简单,缺点也很明显,因为未对寻道做任何优化,使平均寻道时间可能较长。 - -### 2. 最短寻道时间优先(SSTF, Shortest Seek Time First) - -要求访问的磁道与当前磁头所在磁道距离最近的优先进行调度。这种算法并不能保证平均寻道时间最短,但是比 FCFS 好很多。 - -### 3. 扫描算法(SCAN) - -SSTF 会出现进行饥饿现象。考虑以下情况,新进程请求访问的磁道与磁头所在磁道的距离总是比一个在等待的进程来的近,那么等待的进程会一直等待下去。 - -SCAN 算法在 SSTF 算法之上考虑了磁头的移动方向,要求所请求访问的磁道在磁头当前移动方向上才能够得到调度。因为考虑了移动方向,那么一个进程请求访问的磁道一定会得到调度。 - -当一个磁头自里向外移动时,移到最外侧会改变移动方向为自外向里,这种移动的规律类似于电梯的运行,因此又常称 SCAN 算法为电梯调度算法。 - -### 4. 循环扫描算法(CSCAN) - -CSCAN 对 SCAN 进行了改动,要求磁头始终沿着一个方向移动。 - -# 参考资料 - -- Tanenbaum A S, Bos H. Modern operating systems[M]. Prentice Hall Press, 2014. -- 汤子瀛, 哲凤屏, 汤小丹. 计算机操作系统[M]. 西安电子科技大学出版社, 2001. -- Bryant, R. E., & O’Hallaron, D. R. (2004). 深入理解计算机系统. -- [进程间的几种通信方式](http://blog.csdn.net/yufaw/article/details/7409596) diff --git a/notes/计算机网络.md b/notes/计算机网络.md deleted file mode 100644 index ac8245c0..00000000 --- a/notes/计算机网络.md +++ /dev/null @@ -1,843 +0,0 @@ - -* [第一章 概述](#第一章-概述) - * [网络的网络](#网络的网络) - * [ISP](#isp) - * [互联网的组成](#互联网的组成) - * [主机之间的通信方式](#主机之间的通信方式) - * [电路交换与分组交换](#电路交换与分组交换) - * [1. 电路交换](#1-电路交换) - * [2. 报文交换](#2-报文交换) - * [3. 分组交换](#3-分组交换) - * [时延](#时延) - * [1. 发送时延](#1-发送时延) - * [2. 传播时延](#2-传播时延) - * [3. 处理时延](#3-处理时延) - * [4. 排队时延](#4-排队时延) - * [计算机网络体系结构*](#计算机网络体系结构) - * [1. 七层协议](#1-七层协议) - * [2. 五层协议](#2-五层协议) - * [3. 数据在各层之间的传递过程](#3-数据在各层之间的传递过程) - * [4. TCP/IP 体系结构](#4-tcpip-体系结构) -* [第二章 物理层](#第二章-物理层) - * [通信方式](#通信方式) - * [带通调制](#带通调制) - * [信道复用技术](#信道复用技术) - * [1. 频分复用、时分复用](#1-频分复用时分复用) - * [2. 统计时分复用](#2-统计时分复用) - * [3. 波分复用](#3-波分复用) - * [4. 码分复用](#4-码分复用) -* [第三章 数据链路层](#第三章-数据链路层) - * [三个基本问题](#三个基本问题) - * [1. 封装成帧](#1-封装成帧) - * [2. 透明传输](#2-透明传输) - * [3. 差错检测](#3-差错检测) - * [点对点信道 - PPP 协议](#点对点信道---ppp-协议) - * [局域网的拓扑](#局域网的拓扑) - * [广播信道 - CSMA/CD 协议*](#广播信道---csmacd-协议) - * [集线器](#集线器) - * [MAC 层](#mac-层) - * [虚拟局域网](#虚拟局域网) -* [第四章 网络层*](#第四章-网络层) - * [网际协议 IP 概述](#网际协议-ip-概述) - * [IP 数据报格式](#ip-数据报格式) - * [IP 地址编址](#ip-地址编址) - * [1. 分类](#1-分类) - * [2. 子网划分](#2-子网划分) - * [3. 无分类](#3-无分类) - * [IP 地址和 MAC 地址](#ip-地址和-mac-地址) - * [地址解析协议 ARP](#地址解析协议-arp) - * [路由器的结构](#路由器的结构) - * [交换机与路由器的区别](#交换机与路由器的区别) - * [路由器分组转发流程](#路由器分组转发流程) - * [路由选择协议](#路由选择协议) - * [1. 内部网关协议 RIP](#1-内部网关协议-rip) - * [2. 内部网关协议 OSPF](#2-内部网关协议-ospf) - * [3. 外部网关协议 BGP](#3-外部网关协议-bgp) - * [网际控制报文协议 ICMP](#网际控制报文协议-icmp) - * [分组网间探测 PING](#分组网间探测-ping) - * [IP 多播](#ip-多播) - * [虚拟专用网 VPN](#虚拟专用网-vpn) - * [网络地址转换 NAT](#网络地址转换-nat) -* [第五章 运输层*](#第五章-运输层) - * [UDP 和 TCP 的特点](#udp-和-tcp-的特点) - * [UDP 首部格式](#udp-首部格式) - * [TCP 首部格式](#tcp-首部格式) - * [TCP 的三次握手](#tcp-的三次握手) - * [TCP 的四次挥手](#tcp-的四次挥手) - * [TCP 滑动窗口](#tcp-滑动窗口) - * [TCP 可靠传输](#tcp-可靠传输) - * [TCP 流量控制](#tcp-流量控制) - * [TCP 拥塞控制](#tcp-拥塞控制) - * [慢开始与拥塞避免](#慢开始与拥塞避免) - * [快重传与快恢复](#快重传与快恢复) -* [第六章 应用层*](#第六章-应用层) - * [域名系统 DNS](#域名系统-dns) - * [1. 层次结构](#1-层次结构) - * [2. 解析过程](#2-解析过程) - * [文件传输协议 FTP](#文件传输协议-ftp) - * [远程终端协议 TELNET](#远程终端协议-telnet) - * [万维网 WWW](#万维网-www) - * [电子邮件协议](#电子邮件协议) - * [POP3](#pop3) - * [IMAP](#imap) - * [SMTP](#smtp) - * [动态主机配置协议 DHCP](#动态主机配置协议-dhcp) - * [点对点传输 P2P](#点对点传输-p2p) - * [Web 页面请求过程](#web-页面请求过程) - * [常用端口](#常用端口) -* [参考资料](#参考资料) - - - -# 第一章 概述 - -## 网络的网络 - -网络把主机连接起来,而互联网是把多种不同的网络连接起来,因此互联网是网络的网络。 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//f1fb826b-ecf4-4ddb-91f0-2bafecf08869.jpg) - -## ISP - -互联网服务提供商 ISP 可以从互联网管理机构获得许多 IP 地址,同时拥有通信线路以及路由器等联网设备,个人或机构向 ISP 缴纳一定的费用就可以接入互联网。 - -目前的互联网是一种多层次 ISP 结构,ISP 根据覆盖面积的大小分为主干 ISP、地区 ISP 和本地 ISP。 - -互联网交换点 IXP 允许两个 ISP 直接相连而不用经过第三个 ISP。 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//0f8c0a60-d4c6-47f4-978d-1a5c393fedac.jpg) - -## 互联网的组成 - -1. 边缘部分:所有连接在互联网上的主机,用户可以直接使用; -2. 核心部分:由大量的网络和连接这些网络的路由器组成,为边缘部分的主机提供服务。 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//8ab40d6d-bd7c-47d3-afe8-6a8bc9f5d04c.jpg) - -## 主机之间的通信方式 - -**1. 客户 - 服务器(C/S)** - -客户即是服务请求方,服务器是服务提供方。 - -**2. 对等(P2P)** - -不区分客户和服务器。 - -## 电路交换与分组交换 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//c50d230c-8b89-4644-8f62-8708d03aac5b.jpg) - -### 1. 电路交换 - -电路交换用于电话通信系统,两个用户要通信之前需要建立一条专用的物理链路,并且在整个通信过程中始终占用该链路。由于通信的过程中不可能一直在使用传输线路,因此电路交换对线路的利用率很低,往往不到 10%。 - -### 2. 报文交换 - -报文交换用于邮局通信系统,邮局接收到一份报文之后,先存储下来,然后把相同目的地的报文一起转发到下一个目的地,这个过程就是存储转发过程。 - -### 3. 分组交换 - -分组交换也使用了存储转发,但是转发的是分组而不是报文。把整块数据称为一个报文,由于一个报文可能很长,需要先进行切分,来满足分组能处理的大小。在每个切分的数据前面加上首部之后就成为了分组,首部包含了目的地址和源地址等控制信息。 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//2366c2ad-5859-4d4e-805f-7e2b88061cd8.jpg) - -存储转发允许在一条传输线路上传送多个主机的分组,因此不需要占用端到端的线路资源。 - -相比于报文交换,由于分组比报文更小,存储转发的速度也就更快。 - -## 时延 - -总时延 = 发送时延 + 传播时延 + 处理时延 + 排队时延 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//ceee91c2-da26-4169-94c3-e4608b46b9ac.png) - -### 1. 发送时延 - -主机或路由器发送数据帧所需要的时间。 - -

- -其中 l 表示数据帧的长度,v 表示发送速率。 - -### 2. 传播时延 - -电磁波在信道中传播一定的距离需要花费的时间,电磁波传播速度接近光速。 - -

- -其中 l 表示信道长度,v 表示电磁波在信道上的传播速率。 - -### 3. 处理时延 - -主机或路由器收到分组时进行处理所需要的时间,例如分析首部,从分组中提取数据部分等。 - -### 4. 排队时延 - -分组在路由器的输入队列和输出队列中排队等待的时间,取决于网络当前的通信量。 - -## 计算机网络体系结构* - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//1005dc9d-9049-4b06-9524-6171e56ebd8c.png) - -### 1. 七层协议 - -如图 a 所示,其中表示层和会话层用途如下: - -1. 表示层:信息的语法语义以及它们的关联,如加密解密、转换翻译、压缩解压缩; -2. 会话层:不同机器上的用户之间建立及管理会话。 - -### 2. 五层协议 - -1. 应用层:为特定应用程序提供数据传输服务,例如 HTTP、DNS 等。数据单位为报文。 - -2. 运输层:提供的是进程间的通用数据传输服务。由于应用层协议很多,定义通用的运输层协议就可以支持不断增多的应用层协议。运输层包括两种协议:传输控制协议 TCP,提供面向连接、可靠的数据传输服务,数据单位为报文段;用户数据报协议 UDP,提供无连接、尽最大努力的数据传输服务,数据单位为用户数据报。TCP 主要提供完整性服务,UDP 主要提供及时性服务。 - -3. 网络层:为主机之间提供服务,而不是像运输层协议那样是为主机中的进程提供服务。网络层把运输层产生的报文段或者用户数据报封装成分组来进行传输。 - -4. 数据链路层:网络层针对的还是主机之间,而主机之间可以有很多链路,链路层协议就是为相邻结点之间提供服务。数据链路层把网络层传来的分组封装成帧。 - -5. 物理层:考虑的是怎样在传输媒体上传输数据比特流,而不是指具体的传输媒体。物理层的作用是尽可能屏蔽传输媒体和通信手段的差异,使物理层上的数据链路层感觉不到这些差异。 - -### 3. 数据在各层之间的传递过程 - -在向下的过程中,需要添加下层协议所需要的首部或者尾部,而在向上的过程中不断拆开首部和尾部。 - -路由器只有下面三层协议,因为路由器位于网络核心中,不需要为进程或者应用程序提供服务,因此也就不需要运输层和应用层。 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//ac106e7e-489a-4082-abd9-dabebe48394c.jpg) - -### 4. TCP/IP 体系结构 - -它只有四层,相当于五层协议中数据链路层和物理层合并为网络接口层。 - -现在的 TCP/IP 体系结构不严格遵循 OSI 分层概念,应用层可能会直接使用 IP 层或者网络接口层。 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//37b74a34-251c-45f8-88a4-614ec953f7e9.png) - -TCP/IP 协议族是一种沙漏形状,中间小两边大,IP 协议在其中占用举足轻重的地位。 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//93cbce0c-c37d-429c-815b-861976a46bd8.png) - -# 第二章 物理层 - -## 通信方式 - -1. 单向通信,又称为单工通信; -2. 双向交替通信,又称为半双工通信; -3. 双向同时通信,又称为全双工通信。 - -## 带通调制 - -模拟信号是连续的信号,数字信号是离散的信号。带通调制把数字信号转换为模拟信号。 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//d2c55c84-aa1f-43c1-bd97-457bcb7816b3.png) - -## 信道复用技术 - -### 1. 频分复用、时分复用 - -频分复用的所有用户在相同的时间占用不同的频率带宽资源;时分复用的所有用户在不同的时间占用相同的频率带宽资源。 - -使用这两种方式进行通信,在通信的过程中用户会一直占用一部分信道资源。但是由于计算机数据的突发性质,没必要一直占用信道资源而不让出给其它用户使用,因此这两种方式对信道的利用率都不高。 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//543d47a1-f0dd-414f-b23c-0c142c814854.png) - -### 2. 统计时分复用 - -是对时分复用的一种改进,不固定每个用户在时分复用帧中的位置,只要有数据就集中起来组成时分复用帧然后发送。 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//29058e09-bb72-4040-a73d-4c497895e9ce.jpg) - -### 3. 波分复用 - -光的频分复用。由于光的频率很高,因此习惯上用波长而不是频率来表示所使用的光载波。 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//78534153-88d1-4f83-a6e0-59064dbdc43a.png) - -### 4. 码分复用 - -为每个用户分配 m bit 的码片,并且所有的码片正交,对于任意两个码片 有 - -

- -为了方便,取 m=8,设码片 为 00011011。在拥有该码片的用户发送比特 1 时就发送该码片,发送比特 0 时就发送该码片的反码 11100100。 - -在计算时将 00011011 记作 (-1 -1 -1 +1 +1 -1 +1 +1),可以得到 - -

- -

- -其中 的反码。 - -利用上面的式子我们知道,当接收端使用码片 对接收到的数据进行内积运算时,结果为 0 的是其它用户发送的数据,结果为 1 的是用户发送的比特 1,结果为 -1 的是用户发送的比特 0。 - -码分复用需要发送的数据量为原先的 m 倍。 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//0042edad-8e3b-4279-bd93-6906fcd1b640.jpg) - -# 第三章 数据链路层 - -## 三个基本问题 - -### 1. 封装成帧 - -将网络层传下来的分组添加首部和尾部,用于标记帧的开始和结束。 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//3402d1c0-7020-4249-9a7f-12ea2ea6adf7.jpg) - -### 2. 透明传输 - -透明表示一个实际存在的事物看起来好像不存在一样。 - -帧中有首部和尾部,如果帧的数据部分含有和首部尾部相同的内容,那么帧的开始和结束位置就会被错误的判定。需要在数据中出现首部尾部相同的内容前面插入转义字符,如果需要传输的内容正好就是转义字符,那么就在转义字符前面再加个转义字符,在接收端进行处理之后可以还原出原始数据。这个过程透明传输的内容是转义字符,用户察觉不到转义字符的存在。 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//4146e14b-56b9-433c-8e3d-74b1b325399c.jpg) - -### 3. 差错检测 - -目前数据链路层广泛使用了循环冗余检验(CRC)来检查比特差错。 - -## 点对点信道 - PPP 协议 - -互联网用户通常需要连接到某个 ISP 之后才能接入到互联网,PPP 协议就是用户计算机和 ISP 进行通信时所使用的数据链路层协议。 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//8393f520-d824-44ea-a5f3-1c1a73d735fb.jpg) - -在 PPP 的帧中,F 字段为帧的定界符,A 和 C 暂时没有意义。FCS 是使用 CRC 的检验序列。信息部分的长度不超过 1500。 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//0f39c274-b79c-4e83-8c7c-94fc2747832d.jpg) - -## 局域网的拓扑 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//8b15e36f-69b4-46b6-a07c-7234ac7c7927.jpg) - -## 广播信道 - CSMA/CD 协议* - -在广播信道上,同一时间只能允许一台计算机发送数据。 - -CSMA/CD 表示载波监听多点接入 / 碰撞检测。 - -- **多点接入**:说明这是总线型网络,许多计算机以多点的方式连接到总线上。 -- **载波监听**:每个站都必须不停地检听信道。在发送前,如果检听信道正在使用,就必须等待。 -- **碰撞检测**:在发送中,如果检听信道已有其它站正在发送数据,就表示发生了碰撞。虽然每一个站在发送数据之前都已经检听信道为空闲,但是由于电磁波的传播时延的存在,还是有可能会发生碰撞。 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//f9ed4da5-0032-41e6-991a-36d995ec28fd.png) - -记端到端的传播时延为 τ,最先发送的站点最多经过 2τ 就可以知道是否发生了碰撞,称 2τ 为 **争用期**。只有经过争用期之后还没有检测到碰撞,才能肯定这次发送不会发生碰撞。 - -当发生碰撞时,站点要停止发送,等待一段时间再发送。这个时间采用 **截断二进制指数退避算法** 来确定,从离散的整数集合 {0, 1, .., (2k-1)} 中随机取出一个数,记作 r,然后取 r 倍的争用期作为重传等待时间。 - -## 集线器 - -从表面上看,使用集线器的局域网在物理上是一个星型网。但是集线器使用电子器件来模拟实际缆线的工作,逻辑上仍是一个总线网,整个系统仍像一个传统以太网那样运行。 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//3294ff06-f942-425e-aecc-ca04e45566d4.png) - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//b56ef52e-3d0f-4cdd-97dc-eaed893444a5.jpg) - -## MAC 层 - -MAC 地址是 6 字节(48 位)的地址,用于唯一表示网络适配器(网卡),一台主机拥有多少个适配器就有多少个 MAC 地址,例如笔记本电脑普遍存在无线网络适配器和有线网络适配器。 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//50d38e84-238f-4081-8876-14ef6d7938b5.jpg) - -- **类型**:标记上层使用的协议; -- **数据**:长度在 46-1500 之间,如果太小则需要填充; -- **FCS**:帧检验序列,使用的是 CRC 检验方法; -- **前同步码**:只是为了计算 FCS 临时加入的,计算结束之后会丢弃。 - -## 虚拟局域网 - -虚拟局域网可以建立与物理位置无关的逻辑组,只有在同一个虚拟局域网中的成员才会收到链路层广播信息,例如下图中 (A1, A2, A3, A4) 属于一个虚拟局域网,A1 发送的广播会被 A2、A3、A4 收到,而其它站点收不到。 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//a74b70ac-323a-4b31-b4d5-90569b8a944b.png) - -# 第四章 网络层* - -## 网际协议 IP 概述 - -因为网络层是整个互联网的核心,因此应当让网络层尽可能简单。网络层向上只提供简单灵活的、无连接的、尽最大努力交互的数据报服务。 - -使用 IP 协议,可以把异构的物理网络连接起来,使得在网络层看起来好像是一个统一的网络。 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//fe3d224c-8ffd-40f9-85b1-86ffe1393f6c.jpg) - -与 IP 协议配套使用的还有三个协议: - -1. 地址解析协议 ARP(Address Resolution Protocol) -2. 网际控制报文协议 ICMP(Internet Control Message Protocol) -3. 网际组管理协议 IGMP(Internet Group Management Protocol) - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//163cf8b4-5f30-46c9-af00-316a71b3c890.jpg) - -## IP 数据报格式 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//8681db55-0873-434b-aa98-83d07e8392ae.jpg) - -- **版本** : 有 4(IPv4)和 6(IPv6)两个值; - -- **首部长度** : 占 4 位,因此最大值为 15。值为 1 表示的是 1 个 32 位字的长度,也就是 4 字节。因为首部固定长度为 20 字节,因此该值最小为 5。如果可选部分的长度不是 4 字节的整数倍,就用尾部的填充部分来填充。 - -- **区分服务** : 用来获得更好的服务,一般情况下不使用。 - -- **总长度** : 包括首部长度和数据部分长度。 - -- **标识** : 在数据报长度过长从而发生分片的情况下,相同数据报的不同分片具有相同的标识符。 - -- **片偏移** : 和标识符一起,用于发生分片的情况。片偏移的单位为 8 字节。 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//45c86855-9b18-4cf4-a9a7-f8b6eb78d133.png) - -- **生存时间** :TTL,它的存在是为了防止无法交付的数据报在互联网中不断兜圈子。以路由器跳数为单位,当 TTL 为 0 时就丢弃数据报。 - -- **协议**:指出携带的数据应该上交给哪个协议进行处理,例如 ICMP、TCP、UDP 等。 - -- **首部检验和**:因为数据报每经过一个路由器,都要重新计算检验和,因此检验和不包含数据部分可以减少计算的工作量。 - -## IP 地址编址 - -IP 地址的编址方式经历了三个历史阶段: - -1. 分类; -2. 子网划分; -3. 无分类。 - -### 1. 分类 - -由两部分组成,网络号和主机号,其中不同类别具有不同的网络号长度,并且是固定的。 - -IP 地址 ::= {< 网络号 >, < 主机号 >} - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//2ddd6132-60be-4a72-9daa-3d9756191f4a.png) - -### 2. 子网划分 - -通过在网络号字段中拿一部分作为子网号,把两级 IP 地址划分为三级 IP 地址。注意,外部网络看不到子网的存在。 - -IP 地址 ::= {< 网络号 >, < 子网号 >, < 主机号 >} - -要使用子网,必须配置子网掩码。一个 B 类地址的默认子网掩码为 255.255.0.0,如果 B 类地址的子网占两个比特,那么子网掩码为 11111111 11111111 11000000 000000,也就是 255.255.192.0。 - -### 3. 无分类 - -无分类编址 CIDR 消除了传统 A 类、B 类和 C 类地址以及划分子网的概念,使用网络前缀和主机号来对 IP 地址进行编码,网络前缀的长度可以根据需要变化。 - -IP 地址 ::= {< 网络前缀号 >, < 主机号 >} - -CIDR 的记法上采用在 IP 地址后面加上网络前缀长度的方法,例如 128.14.35.7/20 表示前 20 位为网络前缀。 - -CIDR 的地址掩码可以继续称为子网掩码,子网掩码首 1 长度为网络前缀的长度。 - -一个 CIDR 地址块中有很多地址,一个 CIDR 表示的网络就可以表示原来的很多个网络,并且在路由表中只需要一个路由就可以代替原来的多个路由,减少了路由表项的数量。把这种通过使用网络前缀来减少路由表项的方式称为路由聚合,也称为 **构成超网**。 - -在路由表中的项目由“网络前缀”和“下一跳地址”组成,在查找时可能会得到不止一个匹配结果,应当采用最长前缀匹配来确定应该匹配哪一个。 - -## IP 地址和 MAC 地址 - -网络层实现主机之间的通信,而链路层实现具体每段链路之间的通信。因此在通信过程中,IP 数据报的源地址和目的地址始终不变,而 MAC 地址随着链路的改变而改变。 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//86b71296-0d1e-4a63-bcd9-54955b6b781b.jpg) - -## 地址解析协议 ARP - -实现由 IP 地址得到 MAC 地址。 - -每个主机都有一个 ARP 高速缓存,存放映射表。如果一个 IP 地址到 MAC 地址的映射不在该表中,主机通过广播的方式发送 ARP 请求分组,匹配 IP 地址的主机会发送 ARP 响应分组告知其 MAC 地址。 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//8bc6fc2c-d198-4759-b06c-18d94d851e97.png) - -## 路由器的结构 - -路由器从功能上可以划分为两大部分:路由选择和分组转发。 - -分组转发部分由三部分组成:交换结构、一组输入端口和一组输出端口。 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//3a676c54-b559-4466-9b21-eb10f1e25879.jpg) - -交换结构的交换网络有以下三种实现方式: - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//7f82fd18-7f16-4125-ada6-bb6b795b4fda.png) - -## 交换机与路由器的区别 - -- 交换机工作于数据链路层,能识别 MAC 地址,根据 MAC 地址转发链路层数据帧。具有自学机制来维护 IP 地址与 MAC 地址的映射。 -- 路由器位于网络层,能识别 IP 地址并根据 IP 地址转发分组。维护着路由表,根据路由表选择最佳路线。 - -## 路由器分组转发流程 - -1. 从数据报的首部提取目的主机的 IP 地址 D,得到目的网络地址 N。(路由表项是网络号而不是 IP 地址,这样做大大减少了路由表条目数量) -2. 若 N 就是与此路由器直接相连的某个网络地址,则进行直接交付; -3. 若路由表中有目的地址为 D 的特定主机路由,则把数据报传送给表中所指明的下一跳路由器; -4. 若路由表中有到达网络 N 的路由,则把数据报传送给路由表中所指明的下一跳路由器; -5. 若路由表中有一个默认路由,则把数据报传送给路由表中所指明的默认路由器; -6. 报告转发分组出错。 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//8d211911-0e62-4190-ab00-d8610adec4a0.jpg) - -## 路由选择协议 - -互联网使用的路由选择协议都是自适应的,能随着网络通信量和拓扑结构的变化而自适应地进行调整。 - -互联网可以划分为许多较小的自治系统 AS,一个 AS 可以使用一种和别的 AS 不同的路由选择协议。 - -可以把路由选择协议划分为两大类: - -1. 内部网关协议 IGP(Interior Gateway Protocol):在 AS 内部使用,如 RIP 和 OSPF。 -2. 外部网关协议 EGP(External Gateway Protocol):在 AS 之间使用,如 BGP。 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//e0be6970-5b0e-44a2-bc71-df4d61c42b8f.jpg) - -### 1. 内部网关协议 RIP - -RIP 是一种分布式的基于距离向量的路由选择协议。距离是指跳数,直接相连的路由器跳数为 1,跳数最多为 15,超过 15 表示不可达。 - -RIP 按固定的时间间隔仅和相邻路由器交换自己的路由表,经过若干次交换之后,所有路由器最终会知道到达本自治系统中任何一个网络的最短距离和下一跳路由器地址。 - -距离向量算法: - -1. 对地址为 X 的相邻路由器发来的 RIP 报文,先修改报文中的所有项目,把下一跳字段中的地址改为 X,并把所有的距离字段加 1; -2. 对修改后的 RIP 报文中的每一个项目,进行以下步骤: - - 若原来的路由表中没有目的网络 N,则把该项目添加到路由表中; - - 否则:若下一跳路由器地址是 X,则把收到的项目替换原来路由表中的项目;否则:若收到的项目中的距离 d 小于路由表中的距离,则进行更新(例如原始路由表项为 Net2, 5, P,新表项为 Net2, 4, X,则更新);否则什么也不做。 -3. 若 3 分钟还没有收到相邻路由器的更新路由表,则把该相邻路由器标为不可达,即把距离置为 16。 - -RIP 协议实现简单,开销小,但是 RIP 能使用的最大距离为 15,限制了网络的规模。并且当网络出现故障时,要经过比较长的时间才能将此消息传送到所有路由器。 - -### 2. 内部网关协议 OSPF - -开放最短路径优先 OSPF,是为了克服 RIP 的缺点而开发出来的。 - -开放表示 OSPF 不受某一家厂商控制,而是公开发表的;最短路径优先表示使用了 Dijkstra 提出的最短路径算法 SPF。 - -OSPF 具有以下特点: - -- 向本自治系统中的所有路由器发送信息,这种方法是洪泛法。 -- 发送的信息就是与相邻路由器的链路状态,链路状态包括与哪些路由器相连以及链路的度量,度量用费用、距离、时延、带宽等来表示。 -- 只有当链路状态发生变化时,路由器才会发送信息。 - -所有路由器都具有全网的拓扑结构图,并且是一致的。相比于 RIP,OSPF 的更新过程收敛的很快。 - -### 3. 外部网关协议 BGP - -AS 之间的路由选择很困难,主要是互联网规模很大。并且各个 AS 内部使用不同的路由选择协议,就无法准确定义路径的度量。并且 AS 之间的路由选择必须考虑有关的策略,比如有些 AS 不愿意让其它 AS 经过。 - -BGP 只能寻找一条比较好的路由,而不是最佳路由。它采用路径向量路由选择协议。 - -每个 AS 都必须配置 BGP 发言人,通过在两个相邻 BGP 发言人之间建立 TCP 连接来交换路由信息。 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//eb6271de-22c9-4f4b-8b31-eab1f560efac.png) - -## 网际控制报文协议 ICMP - -ICMP 是为了更有效地转发 IP 数据报和提高交付成功的机会。它封装在 IP 数据报中,但是不属于高层协议。 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//9b5e0fa0-9274-4219-a3a9-84fbb509c735.jpg) - -ICMP 报文分为差错报告报文和询问报文。 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//6e11b122-95ce-4869-bf7d-3b0d7591707e.jpg) - -## 分组网间探测 PING - -PING 是 ICMP 的一个重要应用,主要用来测试两台主机之间的连通性。 - -PING 的过程: - -1. PING 同一个网段的主机:查找目的主机的 MAC 地址,然后直接交付。如果无法查找到 MAC 地址,就要进行一次 ARP 请求。 -2. PING 不同网段的主机:发送到网关让其进行转发。同样要发送到网关也需要通过查找网关的 MAC 地址,根据 MAC 地址进行转发。 - -## IP 多播 - -在一对多的通信中,多播不需要将分组复制多份,从而大大节约网络资源。 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//c77b6a18-dfac-42a2-ac89-7e99481275dc.jpg) - -## 虚拟专用网 VPN - -由于 IP 地址的紧缺,一个机构能申请到的 IP 地址数往往远小于本机构所拥有的主机数。并且一个机构并不需要把所有的主机接入到外部的互联网中,机构内的计算机可以使用仅在本机构有效的 IP 地址(专用地址)。 - -有三个专用地址块: - -1. 10.0.0.0 \~ 10.255.255.255 -2. 172.16.0.0 \~ 172.31.255.255 -3. 192.168.0.0 \~ 192.168.255.255 - -VPN 使用公用的互联网作为本机构各专用网之间的通信载体。专用指机构内的主机只与本机构内的其它主机通信;虚拟指“好像是”,而实际上并不是,它有经过公用的互联网。 - -下图中,场所 A 和 B 的通信部经过互联网,如果场所 A 的主机 X 要和另一个场所 B 的主机 Y 通信,IP 数据报的源地址是 10.1.0.1,目的地址是 10.2.0.3。数据报先发送到与互联网相连的路由器 R1,R1 对内部数据进行加密,然后重新加上数据报的首部,源地址是路由器 R1 的全球地址 125.1.2.3,目的地址是路由器 R2 的全球地址 194.4.5.6。路由器 R2 收到数据报后将数据部分进行解密,恢复原来的数据报,此时目的地址为 10.2.0.3,就交付给 Y。 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//bf4ed077-d481-4db7-9e7a-85d841a5a8c3.jpg) - -## 网络地址转换 NAT - -专用网内部的主机使用本地 IP 地址又想和互联网上的主机通信时,可以使用 NAT 来将本地 IP 转换为全球 IP。 - -在以前,NAT 将本地 IP 和全球 IP 一一对应,这种方式下拥有 n 个全球 IP 地址的专用网内最多只可以同时有 n 台主机接入互联网。为了更有效地利用全球 IP 地址,现在常用的 NAT 转换表把运输层的端口号也用上了,使得多个专用网内部的主机共用一个全球 IP 地址。使用端口号的 NAT 也叫做网络地址与端口转换 NAPT。 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//0f31bc7a-d60b-48a6-8e3f-597708369e52.png) - -# 第五章 运输层* - -网络层只把分组发送到目的主机,但是真正通信的并不是主机而是主机中的进程。 - -运输层提供了应用进程间的逻辑通信。运输层向高层用户屏蔽了下面网络层的核心细节,使应用程序看见的好像在两个运输层实体之间有一条端到端的逻辑通信信道。 - -## UDP 和 TCP 的特点 - -- 用户数据包协议 UDP(User Datagram Protocol)是无连接的,尽最大可能交付,没有拥塞控制,面向报文(对于应用程序传下来的报文不合并也不拆分,只是添加 UDP 首部)。 - -- 传输控制协议 TCP(Transmission Control Protocol) 是面向连接的,提供可靠交付,有流量控制,拥塞控制,提供全双工通信,面向字节流(把应用层传下来的报文看成字节流,把字节流组织成大小不等的数据块) - -## UDP 首部格式 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//bd6c05f3-02ee-4c8a-b374-40c87154a898.jpg) - -首部字段只有 8 个字节,包括源端口、目的端口、长度、检验和。12 字节的伪首部是为了计算检验和而临时添加的。 - -## TCP 首部格式 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//21a00b02-c0a6-4bcd-9af0-5ec6bb66e34c.jpg) - -- **序号** :用于对字节流进行编号,例如序号为 301,表示第一个字节的编号为 301,如果携带的数据长度为 100 字节,那么下一个报文段的序号应为 401。 - -- **确认号** :期望收到的下一个报文段的序号。例如 B 正确收到 A 发送来的一个报文段,序号为 501,携带的数据长度为 200 字节,因此 B 期望下一个报文段的序号为 701,B 发送给 A 的确认报文段中确认号就为 701。 - -- **数据偏移** :指的是数据部分距离报文段起始处的偏移量,实际上指的是首部的长度。 - -- **确认 ACK** :当 ACK=1 时确认号字段有效,否则无效。TCP 规定,在连接建立后所有传送的报文段都必须把 ACK 置 1。 - -- **同步 SYN** :在连接建立时用来同步序号。当 SYN=1,ACK=0 时表示这是一个连接请求报文段。若对方同意建立连接,则响应报文中 SYN=1,ACK=1。 - -- **终止 FIN** :用来释放一个连接,当 FIN=1 时,表示此报文段的发送方的数据已发送完毕,并要求释放运输连接。 - -- **窗口** :窗口值作为接收方让发送方设置其发送窗口的依据。之所以要有这个限制,是因为接收方的数据缓存空间是有限的。 - -## TCP 的三次握手 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//086871db-5871-460f-97b7-126cd738bb0e.jpg) - -假设 A 为客户端,B 为服务器端。 - -1. 首先 B 处于 LISTEN(监听)状态,等待客户的连接请求。 -2. A 向 B 发送连接请求报文段,SYN=1,ACK=0,选择一个初始的序号 x。 -3. B 收到连接请求报文段,如果同意建立连接,则向 A 发送连接确认报文段,SYN=1,ACK=1,确认号为 x+1,同时也选择一个初始的序号 y。 -4. A 收到 B 的连接确认报文段后,还要向 B 发出确认,确认号为 y+1,序号为 x+1。 -5. B 收到 A 的确认后,连接建立。 - - -## TCP 的四次挥手 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//78f65456-666b-4044-b4ee-f7692dbbc0d3.jpg) - -以下描述不讨论序号和确认号,因为序号和确认号的规则比较简单。并且不讨论 ACK,因为 ACK 在连接建立之后都为 1。 - -1. A 发送连接释放报文段,FIN=1; -2. B 收到之后发出确认,此时 TCP 属于半关闭状态,B 能向 A 发送数据但是 A 不能向 B 发送数据; -3. 当 B 要不再需要连接时,发送连接释放请求报文段,FIN=1; -4. A 收到后发出确认,此时连接释放。 - -**TIME_WAIT** - -客户端接收到服务器端的 FIN 报文后进入此状态,此时并不是直接进入 CLOSED 状态,还需要等待一个时间计时器设置的时间。这么做有两个理由: - -1. 确保最后一个确认报文段能够到达。如果 B 没收到 A 发送来的确认报文段,那么就会重新发送连接释放请求报文段,A 等待一段时间就是为了处理这种情况的发生。 -2. 可能存在“已失效的连接请求报文段”,为了防止这种报文段出现在本次连接之外,需要等待一段时间。 - -## TCP 滑动窗口 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//223fc26e-2fd6-484c-bcb7-443cac134f15.jpg) - -窗口是缓存的一部分,用来暂时存放字节流。发送方和接收方各有一个窗口,接收方通过 TCP 报文段中的窗口字段告诉发送方自己的窗口大小,发送方根据这个值和其它信息设置自己的窗口大小。 - -发送窗口内的字节都允许被发送,接收窗口内的字节都允许被接收。如果发送窗口左部的字节已经发送并且收到了确认,那么就将发送窗口向右滑动一定距离,直到左部第一个字节不是已发送并且已确认的状态;接收窗口的滑动类似,接收窗口左部字节已经发送确认并交付主机,就向右滑动接收窗口。 - -接收窗口只会对窗口内最后一个按序到达的字节进行确认,例如接收窗口已经收到的字节为 {31, 32, 34, 35},其中 {31, 32} 按序到达,而 {34, 35} 就不是,因此只对字节 32 进行确认。发送方得到一个字节的确认之后,就知道这个字节之前的所有字节都已经被接收。 - -## TCP 可靠传输 - -TCP 使用超时重传来实现可靠传输:如果一个已经发送的报文段在超时时间内没有收到确认,那么就重传这个报文段。 - -一个报文段从发送再到接收到确认所经过的时间称为往返时间 RTT,加权平均往返时间 RTTs 计算如下: - -

- -超时时间 RTO 应该略大于 RRTs,TCP 使用的超时时间计算如下: - -

- -其中 RTTd 为偏差,它与新的 RRT 和 RRTs 有关。 - -## TCP 流量控制 - -流量控制是为了控制发送方发送速率,保证接收方来得及接收。 - -接收方发送的确认报文中的窗口字段可以用来控制发送方窗口大小,从而影响发送方的发送速率。例如将窗口字段设置为 0,则发送方不能发送数据。 - -## TCP 拥塞控制 - -如果网络出现拥塞,分组将会丢失,此时发送方会继续重传,从而导致网络拥塞程度更高。因此当出现拥塞时,应当控制发送方的速率。这一点和流量控制很像,但是出发点不同。流量控制是为了让接收方能来得及接受,而拥塞控制是为了降低整个网络的拥塞程度。 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//a69af9bb-b5ad-4896-862d-697e5ee4feb1.png) - -TCP 主要通过四种算法来进行拥塞控制:慢开始、拥塞避免、快重传、快恢复。发送方需要维护有一个叫做拥塞窗口(cwnd)的状态变量。注意拥塞窗口与发送方窗口的区别,拥塞窗口只是一个状态变量,实际决定发送方能发送多少数据的是发送方窗口。 - -为了便于讨论,做如下假设: - -1. 接收方有足够大的接收缓存,因此不会发生流量控制; -2. 虽然 TCP 的窗口基于字节,但是这里设窗口的大小单位为报文段。 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//346244ff-98c1-4f12-9a87-d0832e8c04cf.jpg) - -### 慢开始与拥塞避免 - -发送的最初执行慢开始,令 cwnd=1,发送方只能发送 1 个报文段;当收到确认后,将 cwnd 加倍,因此之后发送方能够发送的报文段为:2、4、8 ... - -注意到慢开始每个轮次都将 cwnd 加倍,这样会让 cwnd 增长速度非常快,从而使得发送方发送的速度增长速度过快,网络拥塞的可能也就更高。设置一个慢开始门限 ssthresh,当 cwnd >= ssthresh 时,进入拥塞避免,每个轮次只将 cwnd 加 1。 - -如果出现了超时,则令 ssthresh = cwnd / 2,然后重新执行慢开始。 - -### 快重传与快恢复 - -在接收方,要求每次接收到报文段都应该发送对已收到有序报文段的确认,例如已经接收到 M1 和 M2,此时收到 M4,应当发送对 M2 的确认。 - -在发送方,如果收到三个重复确认,那么可以确认下一个报文段丢失,例如收到三个 M2 ,则 M3 丢失。此时执行快重传,立即重传下一个报文段。 - -在这种情况下,只是丢失个别报文段,而不是网络拥塞,因此执行快恢复,令 ssthresh = cwnd / 2 ,cwnd = ssthresh,注意到此时直接进入拥塞避免。 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//b18d679b-c8e2-4564-88ee-7600090e46da.jpg) - -# 第六章 应用层* - -## 域名系统 DNS - -把主机名解析为 IP 地址。 - -被设计成分布式系统。 - -### 1. 层次结构 - -一个域名由多个层次构成,从上层到下层分别为顶级域名、二级域名、三级域名以及四级域名。所有域名可以画成一颗域名树。 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//c2117f61-1177-4768-bf33-cf4f950d911c.png) - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//a4b162e5-db2a-4a27-b213-1fe481c5a06a.png) - -域名服务器可以分为以下四类: - -1. 根域名服务器:解析顶级域名; -2. 顶级域名服务器:解析二级域名; -3. 权限域名服务器:解析区内的域名; -4. 本地域名服务器:也称为默认域名服务器。可以在其中配置高速缓存。 - -区和域的概念不同,可以在一个域中划分多个区。图 b 在域 abc.com 中划分了两个区:abc.com 和 y.abc.com - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//fc0c6b2d-68c7-4de8-aaaa-97355a4f0472.jpg) - -因此就需要两个权限域名服务器: - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//8b335d94-c1ca-42e1-ad48-bb179d28a4f1.jpg) - -### 2. 解析过程 - -主机向本地域名服务器解析的过程采用递归,而本地域名服务器向其它域名服务器解析可以使用递归和迭代两种方式。 - -迭代的方式下,本地域名服务器向一个域名服务器解析请求解析之后,结果返回到本地域名服务器,然后本地域名服务器继续向其它域名服务器请求解析;而递归地方式下,结果不是直接返回的,而是继续向前请求解析,最后的结果才会返回。 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//6bc61bb8-3b1c-4dc8-ac25-cef925ace0eb.jpg) - -## 文件传输协议 FTP - -FTP 在运输层使用 TCP,并且需要建立两个并行的 TCP 连接:控制连接和数据连接。控制连接在整个会话期间一直保持打开,而数据连接在数据传送完毕之后就关闭。控制连接使用端口号 21,数据连接使用端口号 20。 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//58633775-8584-4a01-ad3f-eee4d9a466e1.jpg) - -## 远程终端协议 TELNET - -TELNET 用于登录到远程主机上,并且远程主机上的输出也会返回。 - -TELNET 可以适应许多计算机和操作系统的差异,例如不同操作系统系统的换行符定义。 - -## 万维网 WWW - -[HTTP](https://github.com/CyC2018/InterviewNotes/blob/master/notes/HTTP.md) - -## 电子邮件协议 - -一个电子邮件系统由三部分组成:用户代理、邮件服务器以及邮件发送协议和读取协议。其中发送协议常用 SMTP,读取协议常用 POP3 和 IMAP。 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//de1e46d2-748f-4da3-a29e-7de7bc840366.jpg) - -### POP3 - -POP3 的特点是只要用户从服务器上读取了邮件,就把该邮件删除。 - -### IMAP - -IMAP 协议中客户端和服务器上的邮件保持同步,如果不去手动删除邮件,那么服务器上的邮件也不会被删除。IMAP 这种做法可以让用户随时随地去访问服务器上的邮件。IMAP 协议也支持创建自定义的文件夹。 - -### SMTP - -SMTP 只能发送 ASCII 码,而互联网邮件扩充 MIME 可以发送二进制文件。MIME 并没有改动或者取代 SMTP,而是增加邮件主题的结构,定义了非 ASCII 码的编码规则。 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//ed5522bb-3a60-481c-8654-43e7195a48fe.png) - -## 动态主机配置协议 DHCP - -DHCP 提供了即插即用的连网方式,用户不再需要去手动配置 IP 地址等信息。 - -DHCP 配置的内容不仅是 IP 地址,还包括子网掩码、默认路由器 IP 地址、域名服务器的 IP 地址。 - -工作方式如下:需要 IP 地址的主机广播发送 DHCP 发现报文(将目的地址置为全 1,即 255.255.255.255:67,源地址设置为全 0,即 0.0.0.0:68),DHCP 服务器收到发现报文之后,则在 IP 地址池中取一个地址,发送 DHCP 提供报文给该主机。 - -## 点对点传输 P2P - -把某个文件分发的所有对等集合称为一个洪流。文件的数据单元称为文件块,它的大小是固定的。一个新的对等方加入某个洪流,一开始并没有文件块,但是能够从其它对等方中逐渐地下载到一些文件块,与此同时,它也为别的对等方上传一些文件块。 - -每个洪流都有一个基础设施,称为追踪器。当一个对等方加入洪流时,必须向追踪器登记,并周期性地通知追踪器它仍在洪流中。可以在任何时间加入和退出某个洪流。 - -一个新的对等方加入洪流时,追踪器会随机从洪流中选择若干个对等方,并让新对等方与这些对等方建立连接,把这些对等方称为相邻对等方。接收和发送文件块都是在相邻对等方中进行。 - -当一个对等方需要很多文件块时,通过使用最稀有优先的策略来取得文件块,也就是一个文件块在相邻对等方中副本最少,那么就优先请求这个文件块。 - -当很多对等方向同一个对等方请求文件块时,该对等方优先选择以最高速率向其发送文件块的对等方。 - -P2P 是一个分布式系统,任何时候都有对等方加入或者退出。使用分布式散列表 DHT,可以查找洪流中的资源和 IP 地址映射。 - -## Web 页面请求过程 - -1. 向 DNS 服务器发送 DNS 查询报文来解析域名。 - -2. 开始进行 HTTP 会话,需要先建立 TCP 连接。 - -3. 在运输层的传输过程中,HTTP 报文被封装进 TCP 中。HTTP 请求报文使用端口号 80,因为服务器监听的是 80 端口。连接建立之后,服务器会随机分配一个端口号给特定的客户端,之后的 TCP 传输都是用这个分配的端口号。 - -4. 在网络层的传输过程中,TCP 报文段会被封装进 IP 分组中,IP 分组经过路由选择,最后到达目的地。 - -5. 在链路层,IP 分组会被封装进 MAC 帧中,IP 地址解析成 MAC 地址需要使用 ARP。 - -6. 客户端发送 HTTP 请求报文,请求获取页面。 - -7. 服务器发送 HTTP 相应报文,客户端从而获取该页面。 - -8. 浏览器得到页面内容之后,解析并渲染,向用户展示页面。 - - -## 常用端口 - -| 应用层协议 | 端口号 | 运输层协议 | -| -- | -- | -- | -| DNS | 53 | UDP | -| FTP | 控制连接 21,数据连接 20 | TCP | -| TELNET | 23 | TCP | -| DHCP | 67 68 | UDP | -| HTTP | 80 | TCP | -| SMTP | 25 | TCP | -| POP3 | 110 | TCP | -| IMAP | 143 | TCP | - -# 参考资料 - -- 计算机网络 第七版 -- 计算机网络 自顶向下方法 diff --git a/notes/设计模式.md b/notes/设计模式.md deleted file mode 100644 index 346a44bb..00000000 --- a/notes/设计模式.md +++ /dev/null @@ -1,1730 +0,0 @@ - -* [第一章 设计模式入门](#第一章-设计模式入门) -* [第二章 观察者模式](#第二章-观察者模式) -* [第三章 装饰模式](#第三章-装饰模式) -* [第四章 工厂模式](#第四章-工厂模式) - * [1. 简单工厂](#1-简单工厂) - * [2. 工厂方法模式](#2--工厂方法模式) - * [3. 抽象工厂模式](#3--抽象工厂模式) -* [第五章 单件模式](#第五章-单件模式) -* [第六章 命令模式](#第六章-命令模式) -* [第七章 适配器模式与外观模式](#第七章-适配器模式与外观模式) - * [1. 适配器模式](#1-适配器模式) - * [2. 外观模式](#2-外观模式) -* [第八章 模板方法模式](#第八章-模板方法模式) -* [第九章 迭代器和组合模式](#第九章-迭代器和组合模式) - * [1. 迭代器模式](#1-迭代器模式) - * [2. Java 内置的迭代器](#2-java-内置的迭代器) - * [3. 组合模式](#3-组合模式) -* [第十章 状态模式](#第十章-状态模式) -* [第十一章 代理模式](#第十一章-代理模式) -* [第十二章 复合模式](#第十二章-复合模式) - * [MVC](#mvc) -* [第十三章 与设计模式相处](#第十三章-与设计模式相处) -* [第十四章 剩下的模式](#第十四章-剩下的模式) - - - -# 第一章 设计模式入门 - -**1. 设计模式概念** - -设计模式不是代码,而是解决问题的方案,学习现有的设计模式可以做到经验复用。 - -拥有设计模式词汇,在沟通时就能用更少的词汇来讨论,并且不需要了解底层细节。 - -**2. 问题描述** - -设计不同种类的鸭子拥有不同的叫声和飞行方式。 - -**3. 简单实现方案** - -使用继承的解决方案如下,这种方案代码无法复用,如果两个鸭子类拥有同样的飞行方式,就有两份重复的代码。 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//144d28a0-1dc5-4aba-8961-ced5bc88428a.jpg) - -**4. 设计原则** - -**封装变化**在这里变化的是鸭子叫和飞行的行为方式。 - -**针对接口编程,而不是针对实现编程** 变量声明的类型为父类,而不是具体的某个子类。父类中的方法实现不在父类,而是在各个子类。程序在运行时可以动态改变变量所指向的子类类型。 - -运用这一原则,将叫和飞行的行为抽象出来,实现多种不同的叫和飞行的子类,让子类去实现具体的叫和飞行方式。 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//1c8ccf5c-7ecd-4b8a-b160-3f72a510ce26.png) - -**多用组合,少用继承** 组合也就是 has-a 关系,通过组合,可以在运行时动态改变实现,只要通过改变父类对象具体指向哪个子类即可。而继承就不能做到这些,继承体系在创建类时就已经确定。 - -运用这一原则,在 Duck 类中组合 FlyBehavior 和 QuackBehavior 类,performQuack() 和 performFly() 方法委托给这两个类去处理。通过这种方式,一个 Duck 子类可以根据需要去实例化 FlyBehavior 和 QuackBehavior 的子类对象,并且也可以动态地进行改变。 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//29574e6f-295c-444e-83c7-b162e8a73a83.jpg) - -**5. 整体设计图** - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//e13833c8-e215-462e-855c-1d362bb8d4a0.jpg) - -**6. 模式定义** - -**策略模式** :定义了算法族,分别封装起来,让它们之间可以互相替换,此模式让算法的变化独立于使用算法的客户。 - -**7. 实现代码** - -```java -public abstract class Duck { - FlyBehavior flyBehavior; - QuackBehavior quackBehavior; - - public Duck(){ - } - - public void performFly(){ - flyBehavior.fly(); - } - - public void setFlyBehavior(FlyBehavior fb){ - flyBehavior = fb; - } - - public void performQuack(){ - quackBehavior.quack(); - } - - public void setQuackBehavior(QuackBehavior qb){ - quackBehavior = qb; - } -} -``` -```java -public class MallarDuck extends Duck{ - public MallarDuck(){ - flyBehavior = new FlyWithWings(); - quackBehavior = new Quack(); - } -} -``` -```java -public interface FlyBehavior { - void fly(); -} -``` -```java -public class FlyNoWay implements FlyBehavior{ - @Override - public void fly() { - System.out.println("FlyBehavior.FlyNoWay"); - } -} -``` -```java -public class FlyWithWings implements FlyBehavior{ - @Override - public void fly() { - System.out.println("FlyBehavior.FlyWithWings"); - } -} -``` -```java -public interface QuackBehavior { - void quack(); -} -``` -```java -public class Quack implements QuackBehavior{ - @Override - public void quack() { - System.out.println("QuackBehavior.Quack"); - } -} -``` -```java -public class MuteQuack implements QuackBehavior{ - @Override - public void quack() { - System.out.println("QuackBehavior.MuteQuack"); - } -} -``` -```java -public class Squeak implements QuackBehavior{ - @Override - public void quack() { - System.out.println("QuackBehavior.Squeak"); - } -} -``` -```java -public class MiniDuckSimulator { - public static void main(String[] args) { - Duck mallarDuck = new MallarDuck(); - mallarDuck.performQuack(); - mallarDuck.performFly(); - mallarDuck.setFlyBehavior(new FlyNoWay()); - mallarDuck.performFly(); - } -} -``` -执行结果 -```html -QuackBehavior.Quack -FlyBehavior.FlyWithWings -FlyBehavior.FlyNoWay -``` - -# 第二章 观察者模式 - -**1. 模式定义** - -定义了对象之间的一对多依赖,当一个对象改变状态时,它的所有依赖者都会受到通知并自动更新。主题(Subject)是被观察的对象,而其所有依赖者(Observer)成为观察者。 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//26cb5e7e-6fa3-44ad-854e-fe24d1a5278c.jpg) - -**2. 模式类图** - -主题中具有注册和移除观察者,并通知所有注册者的功能,主题是通过维护一张观察者列表来实现这些操作的。 - -观察者拥有一个主题对象的引用,因为注册、移除还有数据都在主题当中,必须通过操作主题才能完成相应功能。 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//5c558190-fccd-4b5e-98ed-1896653fc97f.jpg) - -**3. 问题描述** - -天气数据布告板会在天气信息发生改变时更新其内容,布告板有多个,并且在将来会继续增加。 - -**4. 解决方案类图** - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//760a5d63-d96d-4dd9-bf9a-c3d126b2f401.jpg) - -**5. 设计原则** - -**为交互对象之间的松耦合设计而努力** 当两个对象之间松耦合,它们依然可以交互,但是不太清楚彼此的细节。由于松耦合的两个对象之间互相依赖程度很低,因此系统具有弹性,能够应对变化。 - -**6. 实现代码** - -```java -public interface Subject { - public void resisterObserver(Observer o); - public void removeObserver(Observer o); - public void notifyObserver(); -} -``` -```java -import java.util.ArrayList; -import java.util.List; - -public class WeatherData implements Subject { - private List observers; - private float temperature; - private float humidity; - private float pressure; - - public WeatherData() { - observers = new ArrayList<>(); - } - - @Override - public void resisterObserver(Observer o) { - observers.add(o); - } - - @Override - public void removeObserver(Observer o) { - int i = observers.indexOf(o); - if (i >= 0) { - observers.remove(i); - } - } - - @Override - public void notifyObserver() { - for (Observer o : observers) { - o.update(temperature, humidity, pressure); - } - } - - public void setMeasurements(float temperature, float humidity, float pressure) { - this.temperature = temperature; - this.humidity = humidity; - this.pressure = pressure; - notifyObserver(); - } -} -``` -```java -public interface Observer { - public void update(float temp, float humidity, float pressure); -} -``` -```java -public class CurrentConditionsDisplay implements Observer { - private Subject weatherData; - - public CurrentConditionsDisplay(Subject weatherData) { - this.weatherData = weatherData; - weatherData.resisterObserver(this); - } - - @Override - public void update(float temp, float humidity, float pressure) { - System.out.println("CurrentConditionsDisplay.update:" + temp + " " + humidity + " " + pressure); - } -} -``` -```java -public class StatisticsDisplay implements Observer { - private Subject weatherData; - - public StatisticsDisplay(Subject weatherData) { - this.weatherData = weatherData; - weatherData.resisterObserver(this); - } - - @Override - public void update(float temp, float humidity, float pressure) { - System.out.println("StatisticsDisplay.update:" + temp + " " + humidity + " " + pressure); - } -} -``` -```java -public class WeatherStation { - public static void main(String[] args) { - WeatherData weatherData = new WeatherData(); - CurrentConditionsDisplay currentConditionsDisplay = new CurrentConditionsDisplay(weatherData); - StatisticsDisplay statisticsDisplay = new StatisticsDisplay(weatherData); - - weatherData.setMeasurements(0, 0, 0); - weatherData.setMeasurements(1, 1, 1); - } -} -``` -执行结果 -```html -CurrentConditionsDisplay.update:0.0 0.0 0.0 -StatisticsDisplay.update:0.0 0.0 0.0 -CurrentConditionsDisplay.update:1.0 1.0 1.0 -StatisticsDisplay.update:1.0 1.0 1.0 -``` - -# 第三章 装饰模式 - -**1. 问题描述** - -设计不同种类的饮料,并且每种饮料可以动态添加新的材料,比如可以添加牛奶。计算一种饮料的价格。 - -**2. 模式定义** - -动态地将责任附加到对象上。在扩展功能上,装饰者提供了比继承更有弹性的替代方案。 - -下图中 DarkRoast 对象被 Mocha 包裹,Mocha 对象又被 Whip 包裹,并且他们都继承自相同父类,都有 cost() 方法,但是外层对象的 cost() 方法实现调用了内层对象的 cost() 方法。因此,如果要在 DarkRoast 上添加 Mocha,那么只需要用 Mocha 包裹 DarkRoast,如果还需要 Whip ,就用 Whip 包裹 Mocha,最后调用 cost() 方法能把三种对象的价格都包含进去。 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//41a4cb30-f393-4b3b-abe4-9941ccf8fa1f.jpg) - -**3. 模式类图** - -装饰者和具体组件都继承自组件类型,其中具体组件的方法实现不需要依赖于其它对象,而装饰者拥有一个组件类型对象,这样它可以装饰其它装饰者或者具体组件。所谓装饰,就是把这个装饰者套在被装饰的对象之外,从而动态扩展被装饰者的功能。装饰者的方法有一部分是自己的,这属于它的功能,然后调用被装饰者的方法实现,从而也保留了被装饰者的功能。可以看到,具体组件应当是装饰层次的最低层,因为只有具体组件有直接实现而不需要委托给其它对象去处理。 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//3dc454fb-efd4-4eb8-afde-785b2182caeb.jpg) - -**4. 问题解决方案的类图** - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//9c997ac5-c8a7-44fe-bf45-2c10eb773e53.jpg) - -**5. 设计原则** - -**类应该对扩展开放,对修改关闭。** 也就是添加新功能时不需要修改代码。在本章问题中该原则体现在,在饮料中添加新的材料,而不需要去修改饮料的代码。观察则模式也符合这个原则。不可能所有类都能实现这个原则,应当把该原则应用于设计中最有可能改变的地方。 - -**6. Java I/O 中的装饰者模式** - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//2a40042a-03c8-4556-ad1f-72d89f8c555c.jpg) - -**7. 代码实现** - -```java -public interface Beverage { - public double cost(); -} -``` -```java -public class HouseBlend implements Beverage{ - @Override - public double cost() { - return 1; - } -} -``` -```java -public class DarkRoast implements Beverage{ - @Override - public double cost() { - return 1; - } -} -``` -```java -public abstract class CondimentDecorator implements Beverage{ - protected Beverage beverage; -} -``` -```java -public class Mocha extends CondimentDecorator { - - public Mocha(Beverage beverage) { - this.beverage = beverage; - } - - @Override - public double cost() { - return 1 + beverage.cost(); - } -} -``` -```java -public class Milk extends CondimentDecorator { - - public Milk(Beverage beverage) { - this.beverage = beverage; - } - - @Override - public double cost() { - return 1 + beverage.cost(); - } -} -``` -```java -public class StartbuzzCoffee { - public static void main(String[] args) { - Beverage beverage = new HouseBlend(); - beverage = new Mocha(beverage); - beverage = new Milk(beverage); - System.out.println(beverage.cost()); - } -} -``` - -输出 - -```html -3.0 -``` - -# 第四章 工厂模式 - -## 1. 简单工厂 - -**1. 问题描述** - -有不同的 Pizza,根据不同的情况用不同的子类实例化一个 Pizza 对象。 - -**2. 定义** - -简单工厂不是设计模式,更像是一种编程习惯。在实例化一个超类的对象时,可以用它的所有子类来进行实例化,要根据具体需求来决定使用哪个子类。在这种情况下,把实例化的操作放到工厂来中,让工厂类来决定应该用哪个子类来实例化。这样做把客户对象和具体子类的实现解耦,客户对象不再需要知道有哪些子类以及实例化哪个子类。因为客户类往往有多个,如果不使用简单工厂,那么所有的客户类都要知道所有子类的细节,一旦子类发生改变,例如增加子类,那么所有的客户类都要发生改变。 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//c470eb9b-fb05-45c5-8bb7-1057dc3c16de.jpg) - -**3. 解决方案类图** - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//dc3e704c-7c57-42b8-93ea-ddd068665964.jpg) - - -**4. 代码实现** - -```java -public interface Pizza { - public void make(); -} -``` -```java -public class CheesePizza implements Pizza{ - @Override - public void make() { - System.out.println("CheesePizza"); - } -} -``` -```java -public class GreekPizza implements Pizza{ - @Override - public void make() { - System.out.println("GreekPizza"); - } -} -``` -```java -public class SimplePizzaFactory { - public Pizza createPizza(String type) { - if (type.equals("cheese")) { - return new CheesePizza(); - } else if (type.equals("greek")) { - return new GreekPizza(); - } else { - throw new UnsupportedOperationException(); - } - } -} -``` -```java -public class PizzaStore { - public static void main(String[] args) { - SimplePizzaFactory simplePizzaFactory = new SimplePizzaFactory(); - Pizza pizza = simplePizzaFactory.createPizza("cheese"); - pizza.make(); - } -} -``` - -运行结果 - -```java -CheesePizza -``` - -## 2. 工厂方法模式 - -**1. 问题描述** - -每个地区的 Pizza 店虽然种类相同,但是都有自己的风味,需要单独区分。例如,一个客户点了纽约的 cheese 种类的 Pizza 和在芝加哥点的相同种类的 Pizza 是不同的。 - -**2. 模式定义** - -定义了一个创建对象的接口,但由子类决定要实例化的类是哪一个。工厂方法让类把实例化推迟到子类。 - -**3. 模式类图** - -在简单工厂中,创建对象的是另一个类,而在工厂方法中,是由子类来创建对象。下图中,Creator 有一个 anOperation() 方法,这个方法需要用到一组产品类,这组产品类由每个子类来创建。 - -可以为每个子类创建单独的简单工厂来创建每一个产品类,但是把简单工厂中创建对象的代码放到子类中来可以减少类的数目,因为子类不算是产品类,因此完全可以这么做。 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//903093ec-acc8-4f9b-bf2c-b990b9a5390c.jpg) - -**4. 解决方案类图** - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//664f8901-5dc7-4644-a072-dad88cc5133a.jpg) - -**5. 代码实现** - -```java -public interface Pizza { - public void make(); -} -``` -```java -public interface PizzaStore { - public Pizza orderPizza(String item); -} -``` -```java -public class NYStyleCheesePizza implements Pizza{ - @Override - public void make() { - System.out.println("NYStyleCheesePizza is making.."); - } -} -``` -```java -public class NYStyleVeggiePizza implements Pizza { - @Override - public void make() { - System.out.println("NYStyleVeggiePizza is making.."); - } -} -``` -```java -public class ChicagoStyleCheesePizza implements Pizza{ - @Override - public void make() { - System.out.println("ChicagoStyleCheesePizza is making.."); - } -} -``` -```java -public class ChicagoStyleVeggiePizza implements Pizza{ - @Override - public void make() { - System.out.println("ChicagoStyleVeggiePizza is making.."); - } -} -``` -```java -public class NYPizzaStore implements PizzaStore { - @Override - public Pizza orderPizza(String item) { - Pizza pizza = null; - if (item.equals("cheese")) { - pizza = new NYStyleCheesePizza(); - } else if (item.equals("veggie")) { - pizza = new NYStyleVeggiePizza(); - } else { - throw new UnsupportedOperationException(); - } - pizza.make(); - return pizza; - } -} -``` -```java -public class ChicagoPizzaStore implements PizzaStore { - @Override - public Pizza orderPizza(String item) { - Pizza pizza = null; - if (item.equals("cheese")) { - pizza = new ChicagoStyleCheesePizza(); - } else if (item.equals("veggie")) { - pizza = new ChicagoStyleVeggiePizza(); - } else { - throw new UnsupportedOperationException(); - } - pizza.make(); - return pizza; - } -} -``` -```java -public class PizzaTestDrive { - public static void main(String[] args) { - PizzaStore nyStore = new NYPizzaStore(); - nyStore.orderPizza("cheese"); - PizzaStore chicagoStore = new ChicagoPizzaStore(); - chicagoStore.orderPizza("cheese"); - } -} -``` - -运行结果 - -```html -NYStyleCheesePizza is making.. -ChicagoStyleCheesePizza is making.. -``` - -## 3. 抽象工厂模式 - -**1. 设计原则** - -**依赖倒置原则**:要依赖抽象,不要依赖具体类。听起来像是针对接口编程,不针对实现编程,但是这个原则说明了:不能让高层组件依赖底层组件,而且,不管高层或底层组件,两者都应该依赖于抽象。例如,下图中 PizzaStore 属于高层组件,它依赖的是 Pizza 的抽象类,这样就可以不用关心 Pizza 的具体实现细节。 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//ddf72ca9-c0be-49d7-ab81-57a99a974c8e.jpg) - -**2. 模式定义** - -提供一个接口,用于创建相关或依赖对象的家族,而不需要明确指定具体类。 - -**3. 模式类图** - -抽象工厂模式创建的是对象家族,也就是很多对象而不是一个对象,并且这些对象是相关的,也就是说必须一起创建出来。而工厂模式只是用于创建一个对象,这和抽象工厂模式有很大不同。并且,抽象工厂模式也用到了工厂模式来创建单一对象,在类图左部,AbstractFactory 中的 CreateProductA 和 CreateProductB 方法都是让子类来实现,这两个方法单独来看就是在创建一个对象,这符合工厂模式的定义。至于创建对象的家族这一概念是在 Client 体现,Client 要通过 AbstractFactory 同时调用两个方法来创建出两个对象,在这里这两个对象就有很大的相关性,Client 需要这两个对象的协作才能完成任务。从高层次来看,抽象工厂使用了组合,即 Cilent 组合了 AbstractFactory ,而工厂模式使用了继承。 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//d301774f-e0d2-41f3-95f4-bfe39859b52e.jpg) - -**4. 解决方案类图** - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//8785dabd-1285-4bd0-b3aa-b05cc060a24a.jpg) - -**5. 代码实现** - -```java -public interface Dough { - public String doughType(); -} -``` -```java -public class ThickCrustDough implements Dough{ - - @Override - public String doughType() { - return "ThickCrustDough"; - } -} -``` -```java -public class ThinCrustDough implements Dough { - @Override - public String doughType() { - return "ThinCrustDough"; - } -} -``` -```java -public interface Sauce { - public String sauceType(); -} -``` -```java -public class MarinaraSauce implements Sauce { - @Override - public String sauceType() { - return "MarinaraSauce"; - } -} -``` -```java -public class PlumTomatoSauce implements Sauce { - @Override - public String sauceType() { - return "PlumTomatoSauce"; - } -} -``` -```java -public interface PizzaIngredientFactory { - public Dough createDough(); - public Sauce createSauce(); -} -``` -```java -public class NYPizzaIngredientFactory implements PizzaIngredientFactory{ - @Override - public Dough createDough() { - return new ThickCrustDough(); - } - - @Override - public Sauce createSauce() { - return new MarinaraSauce(); - } -} -``` -```java -public class ChicagoPizzaIngredientFactory implements PizzaIngredientFactory{ - @Override - public Dough createDough() { - return new ThinCrustDough(); - } - - @Override - public Sauce createSauce() { - return new PlumTomatoSauce(); - } -} -``` -```java -public class NYPizzaStore { - private PizzaIngredientFactory ingredientFactory; - - public NYPizzaStore() { - ingredientFactory = new NYPizzaIngredientFactory(); - } - - public void makePizza() { - Dough dough = ingredientFactory.createDough(); - Sauce sauce = ingredientFactory.createSauce(); - System.out.println(dough.doughType()); - System.out.println(sauce.sauceType()); - } -} -``` -```java -public class NYPizzaStoreTestDrive { - public static void main(String[] args) { - NYPizzaStore nyPizzaStore = new NYPizzaStore(); - nyPizzaStore.makePizza(); - } -} -``` - -运行结果 - -```html -ThickCrustDough -MarinaraSauce -``` - -# 第五章 单件模式 - -**1. 模式定义** - -确保一个类只有一个实例,并提供了一个全局访问点。 - -**2. 模式类图** - -单件模式的 Java 实现用一个私有构造器、一个私有静态变量以及一个公有静态函数,该函数返回私有变量,使得所有通过该函数获取的对象都指向这个唯一的私有静态变量。 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//59aff6c1-8bc5-48e4-9e9c-082baeb2f274.jpg) - -**3. 经典实现** - -以下实现中,私有静态变量被延迟化实例化,这样做的好处是,如果没有用到该类,那么就不会创建该私有静态变量,从而节约资源。这个实现在多线程环境下是不安全的,因为多个线程能够同时进入 if(uniqueInstance == null) 内的语句块,那么就会多次实例化 uniqueInstance 私有静态变量。 - -```java -public class Singleton { - - private static Singleton uniqueInstance; - - private Singleton() { - } - - public static Singleton getUniqueInstance() { - if (uniqueInstance == null) { - uniqueInstance = new Singleton(); - } - return uniqueInstance; - } -} -``` - -**4. 线程不安全问题的解决方案一** - -只需要对 getUniqueInstance() 方法加锁,就能让该方法一次只能一个线程访问,从而避免了对 uniqueInstance 变量进行多次实例化的问题。但是这样有一个问题是一次只能一个线程进入,性能上会有一定的浪费。 - -```java - public static synchronized Singleton getUniqueInstance() { - if (uniqueInstance == null) { - uniqueInstance = new Singleton(); - } - return uniqueInstance; - } -``` - -**5. 线程不安全问题的解决方案二** - -不用延迟实例化,采用直接实例化。 - -```java -private static Singleton uniqueInstance = new Singleton(); -``` - -**6. 线程不安全问题的解决方案三** - -考虑第一个解决方案,它是直接对 getUniqueInstance() 方法进行加锁,而实际上只需要对 uniqueInstance = new Singleton(); 这条语句加锁即可。使用两个条件语句来判断 uniqueInstance 是否已经实例化,如果没有实例化才需要加锁。 - -```java -public class Singleton { - - private volatile static Singleton uniqueInstance; - - private Singleton() { - } - - public static Singleton getUniqueInstance() { - if (uniqueInstance == null) { - synchronized (Singleton.class) { - if (uniqueInstance == null) { - uniqueInstance = new Singleton(); - } - } - } - return uniqueInstance; - } -} -``` - -# 第六章 命令模式 - -**1. 问题描述** - -设计一个遥控器,它有很多按钮,每个按钮可以发起一个命令,让一个家电完成相应操作。有非常多的家电,并且也会增加家电。 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//7b8f0d8e-a4fa-4c9d-b9a0-3e6a11cb3e33.jpg) - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//c3ca36b2-8459-4cf1-98b0-cc95a0e94f20.jpg) - -**2. 模式定义** - -将命令封装成对象,以便使用不同的命令来参数化其它对象。 - -**3. 模式类图** - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//1e09d75f-6268-4425-acf8-8ecd1b4a0ef3.jpg) - -**4. 解决方案类图** - -Invoker 是遥控器,它可以设置命令,并且调用命令对象的 execute() 方法。Receiver 是电灯,是命令真正的执行者。ConcreteCommand 类组合了一个 Receiver 对象,命令的执行委托给 Receiver 对象来处理,也就是 LightOnCommand 命令的 excute 方法委托给 Light 对象来处理,Light 对象通过调用 on() 方法来完成操作。Invoker 不是 Client 对象,是因为命令的创建不是在 Invoker 中完成的,因此需要额外的 Client 对象来处理这些操作。 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//5ef94f62-98ce-464d-a646-842d9c72c8b8.jpg) - -**5. 代码实现** - -```java -public interface Command { - public void execute(); -} -``` -```java -public class Light { - - public void on() { - System.out.println("Light is on!"); - } - - public void off() { - System.out.println("Light is off!"); - } -} -``` -```java -public class LightOnCommand implements Command{ - Light light; - - public LightOnCommand(Light light) { - this.light = light; - } - - @Override - public void execute() { - light.on(); - } -} -``` -```java -/** - * 遥控器类 - */ -public class SimpleRemoteControl { - Command slot; - - public SimpleRemoteControl() { - - } - - public void setCommand(Command command) { - this.slot = command; - } - - public void buttonWasPressed() { - slot.execute(); - } - -} -``` -```java -public class RemoteLoader { - public static void main(String[] args) { - SimpleRemoteControl remote = new SimpleRemoteControl(); - Light light = new Light(); - LightOnCommand lightOnCommand = new LightOnCommand(light); - remote.setCommand(lightOnCommand); - remote.buttonWasPressed(); - } -} -``` - -输出 - -```html -Light is on! -``` - -# 第七章 适配器模式与外观模式 - -## 1. 适配器模式 - -**1. 模式定义** - -将一个类的接口,转换为客户期望的另一个接口。适配器让原本不兼容的类可以合作无间。 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//8e8ba824-7a9e-4934-a212-e6a41dcc1602.jpg) - -**2. 模式类图** - -有两种适配器模式的实现,一种是对象方式,一种是类方式。对象方式是通过组合的方法,让适配器类(Adapter)拥有一个待适配的对象(Adaptee),从而把相应的处理委托给待适配的对象。类方式用到多重继承,Adapter 可以看成 Target 和 Adaptee 类型,先把它当成 Adaptee 类型然后实例化一个 Adapter 对象,再把它当成 Target 类型的,这样 Client 就可以把这个对象当成 Target 的对象来处理。 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//253bd869-ea48-4092-9aed-6906ccb2f3b0.jpg) - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//a797959a-0ed5-475b-8d97-df157c672019.jpg) - -**3. 问题描述** - -让鸭子(Duck)适配火鸡(Turkey),Duck 有 quack() 方法,而 Turkey 只有 gobble() 方法。也就是要让 Turkey 也有 Duck 的 quack() 方法。 - -**4. 解决方案类图** - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//1a511c76-bb6b-40ab-b8aa-39eeb619d673.jpg) - -**5. 代码实现** - -```java -public interface Duck { - public void quack(); - public void fly(); -} -``` -```java -public interface Turkey { - public void gobble(); - public void fly(); -} -``` -```java -public class WildTurkey implements Turkey{ - @Override - public void gobble() { - System.out.println("gobble!"); - } - - @Override - public void fly() { - System.out.println("fly!"); - } -} -``` -```java -public class TurkeyAdapter implements Duck{ - Turkey turkey; - - public TurkeyAdapter(Turkey turkey) { - this.turkey = turkey; - } - - @Override - public void quack() { - turkey.gobble(); - } - - @Override - public void fly() { - turkey.fly(); - } -} -``` -```java -public class DuckTestDrive { - public static void main(String[] args) { - Turkey turkey = new WildTurkey(); - Duck duck = new TurkeyAdapter(turkey); - duck.quack(); - duck.fly(); - } -} -``` - -运行结果 -```html -gobble! -fly! -``` - -## 2. 外观模式 - -**1. 模式定义** - -提供了一个统一的接口,用来访问子系统中的一群接口。外观定义了一个高层接口,让子系统更容易使用。 - -**2. 模式类图** - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//78f2314e-2643-41df-8f3d-b7e28294094b.jpg) - -**3. 问题描述** - -家庭影院中有众多电器,当要进行观看电影时需要对很多电器进行操作。要求简化这些操作,使得家庭影院类只提供一个简化的接口,例如提供一个看电影的接口而不用具体操作众多电器。 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//106f5585-b2e7-4718-be5d-3b322d1ef42a.jpg) - -**4. 解决方案类图** - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//25387681-89f8-4365-a2fa-83b86449ee84.jpg) - -**5. 设计原则** - -**最少知识原则**:只和你的密友谈话。也就是应当使得客户对象所需要交互的对象应当尽可能少。 - -# 第八章 模板方法模式 - -**1. 模式定义** - -在一个方法中定义一个算法的骨架,而将一些步骤延迟到子类中。 - -模板方法使得子类可以在不改变算法结构的情况下,重新定义算法中的某些步骤。 - -**2. 模式类图** - -模板方法 templateMethod() 定义了算法的骨架,确定了 primitiveOperation1() 和 primitiveOperation2() 方法执行的顺序,而 primitiveOperation1() 和 primitiveOperation2() 让子类去具体实现。 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//ed62f400-192c-4185-899b-187958201f0c.jpg) - -**3. 问题描述** - -冲咖啡和冲茶都有类似的流程,但是某些步骤会有点不一样。 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//d8f873fc-00bc-41ee-a87c-c1b4c0172844.png) - -**4. 解决方案类图** - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//aa20c123-b6b5-432a-83d3-45dc39172192.jpg) - -**5. 设计原则** - -**好莱坞原则**:别调用(打电话给)我们,我们会调用(打电话给)你。这一原则可以防止依赖腐败,即防止高层组件依赖底层组件,底层组件又依赖高层组件。该原则在模板方法的体现为,只有父类会调用子类,子类不会调用父类。 - -**6. 钩子** - -钩子(hock):某些步骤在不同实现中可有可无,可以先定义一个什么都不做的方法,把它加到模板方法 templteMethod() 中,如果子类需要它就覆盖默认实现并加上自己的实现。 - -**7. 代码实现** - -```java -public abstract class CaffeineBeverage { - - final void prepareRecipe(){ - boilWater(); - brew(); - pourInCup(); - addCondiments(); - } - - abstract void brew(); - - abstract void addCondiments(); - - void boilWater(){ - System.out.println("boilWater"); - } - - void pourInCup(){ - System.out.println("pourInCup"); - } -} -``` -```java -public class Coffee extends CaffeineBeverage{ - @Override - void brew() { - System.out.println("Coffee.brew"); - } - - @Override - void addCondiments() { - System.out.println("Coffee.addCondiments"); - } -} -``` -```java -public class Tea extends CaffeineBeverage{ - @Override - void brew() { - System.out.println("Tea.brew"); - } - - @Override - void addCondiments() { - System.out.println("Tea.addCondiments"); - } -} -``` -```java -public class CaffeineBeverageTestDrive { - public static void main(String[] args) { - CaffeineBeverage caffeineBeverage = new Coffee(); - caffeineBeverage.prepareRecipe(); - System.out.println("-----------"); - caffeineBeverage = new Tea(); - caffeineBeverage.prepareRecipe(); - } -} -``` - -运行结果 - -```html -boilWater -Coffee.brew -pourInCup -Coffee.addCondiments ------------ -boilWater -Tea.brew -pourInCup -Tea.addCondiments -``` - -# 第九章 迭代器和组合模式 - -## 1. 迭代器模式 - -**1. 模式定义** - -提供一种顺序访问一个聚合对象中的各个元素的方法,而又不暴露其内部的表示。 - -**2. 模式类图** - -客户类拥有一个聚合对象和迭代器对象,迭代器对象是聚合对象生成的。只需要迭代器定义好移动的操作,就可以让聚合对象能够顺序遍历。 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//439deca7-fed0-4c89-87e5-7088d10f1fdb.jpg) - -**3. 代码实现** - -```java -public class Aggregate { - - private int[] items; - - public Aggregate() { - items = new int[10]; - for (int i = 0; i < items.length; i++) { - items[i] = i; - } - } - - public Iterator createIterator() { - return new ConcreteIterator(items); - } - -} -``` -```java -public interface Iterator { - boolean hasNext(); - int next(); -} -``` -```java -public class ConcreteIterator implements Iterator { - - private int[] items; - private int position = 0; - - public ConcreteIterator(int[] items) { - this.items = items; - } - - @Override - public boolean hasNext() { - return position < items.length; - } - - @Override - public int next() { - return items[position++]; - } -} -``` -```java -public class Client { - public static void main(String[] args) { - Aggregate aggregate = new Aggregate(); - Iterator iterator = aggregate.createIterator(); - while(iterator.hasNext()){ - System.out.println(iterator.next()); - } - } -} -``` -运行结果 -```html -0 -1 -2 -3 -4 -5 -6 -7 -8 -9 -``` - -## 2. Java 内置的迭代器 - -**1. 实现接口** - -Java 中已经有了 Iterator 接口,在使用 Java 实现时,需要让聚合对象实现 Iterable 接口,该接口有一个 iterator() 方法会返回一个 Iterator 对象。使用 Java 内置的迭代器实现,客户对象可以使用 foreach 循环来遍历聚合对象中的每个元素。 - -**2. 代码实现** - -```java -import java.util.Iterator; - -public class Aggregate implements Iterable{ - - private int[] items; - - public Aggregate() { - items = new int[10]; - for (int i = 0; i < items.length; i++) { - items[i] = i; - } - } - - @Override - public Iterator iterator() { - return new ConcreteIterator(items); - } -} -``` -```java -import java.util.Iterator; - -public class ConcreteIterator implements Iterator { - - private int[] items; - private int position = 0; - - public ConcreteIterator(int[] items) { - this.items = items; - } - - @Override - public boolean hasNext() { - return position < items.length; - } - - @Override - public Integer next() { - return items[position++]; - } -} -``` -```java -public class Client { - public static void main(String[] args) { - Aggregate aggregate = new Aggregate(); - for (int item : aggregate) { - System.out.println(item); - } - } -} -``` - -## 3. 组合模式 - -**1. 设计原则** - -一个类应该只有一个引起改变的原因。 - -**2. 模式定义** - -允许将对象组合成树形结构来表现“整体 / 部分”层次结构。 - -组合能让客户以一致的方式处理个别对象以及对象组合。 - -**3. 模式类图** - -由于组合(Composite)类拥有一个组件(Component)对象,因此组合对象位于树形结构的中间,它还可以继续操作这个组件对象,并忽略组件对象的具体类型。 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//f99c019e-7e91-4c2e-b94d-b031c402dcb5.jpg) - -**4. 代码实现** - -```java -public abstract class Component { - protected String name; - - public Component(String name) { - this.name = name; - } - - abstract public void addChild(Component component); - - public void print() { - print(0); - } - - abstract protected void print(int level); -} -``` -```java -public class Leaf extends Component { - public Leaf(String name) { - super(name); - } - - @Override - public void addChild(Component component) { - throw new UnsupportedOperationException(); // 牺牲透明性换取单一职责原则 , 这样就不用考虑是叶子节点还是组合节点 - } - - @Override - protected void print(int level) { - for (int i = 0; i < level; i++) { - System.out.print("--"); - } - System.out.println("left:" + name); - } -} -``` -```java -import java.util.ArrayList; -import java.util.List; - -public class Composite extends Component { - - private List childs; - - public Composite(String name) { - super(name); - childs = new ArrayList<>(); - } - - @Override - public void addChild(Component component) { - childs.add(component); - } - - @Override - protected void print(int level) { - for (int i = 0; i < level; i++) { - System.out.print("--"); - } - System.out.println("Composite:" + name); - for (Component component : childs) { - component.print(level + 1); - } - } -} -``` -```java -public class Client { - public static void main(String[] args) { - Composite root = new Composite("root"); - Component node1 = new Leaf("1"); - Component node2 = new Composite("2"); - Component node3 = new Leaf("3"); - root.addChild(node1); - root.addChild(node2); - root.addChild(node3); - Component node21 = new Leaf("21"); - Component node22 = new Composite("22"); - node2.addChild(node21); - node2.addChild(node22); - Component node221 = new Leaf("221"); - node22.addChild(node221); - root.print(); - } -} -``` -运行结果 - -```html -Composite:root ---left:1 ---Composite:2 -----left:21 -----Composite:22 -------left:221 ---left:3 -``` - -# 第十章 状态模式 - -**1. 模式定义** - -允许对象在内部状态改变时改变它的行为,对象看起来好像修改了它的类。 - -状态模式的类图和策略模式一样,并且都是能够动态改变对象的行为。但是状态模式是通过状态对象的状态转移来改变客户对象组合的状态对象,而策略模式是通过客户对象本身的决策来改变组合的策略对象。例如,状态模式下,客户对象委托状态对象进行一个处理操作,那么状态对象有可能发生状态转移,使得客户对象拥有的状态对象发生改变。状态对象组合了客户对象,状态转移是状态对象通过改变客户对象所组合的状态对象实现的。 - -**2. 模式类图** - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//c28fd93a-0d55-4a19-810f-72652feee00d.jpg) - -**3. 问题描述** - -糖果销售机有多种状态,每种状态下销售机有不同的行为,状态可以发生转移,使得销售机的行为也发生改变。 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//f7d880c9-740a-4a16-ac6d-be502281b4b2.jpg) - -**4. 直接解决方案** - -在糖果机的每个操作函数里面,判断当前的状态,根据不同的状态进行不同的处理,并且发生不同的状态转移。这种解决方案把所有的实现细节都放到客户类,这样在新增状态的时候就要去修改客户类的代码。 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//62ebbb63-8fd7-4488-a866-76a9dc911662.png) - -**5. 使用状态模式的解决方案** - -状态的转移被移到状态类里面,客户类的每个操作只需要委托给状态类即可,而不需要知道当前是什么状态以及状态时如何进行转移的。 - - -**6. 代码实现** - -```java -public interface State { - /** - * 投入25 分钱 - */ - void insertQuarter(); - - /** - * 退回25 分钱 - */ - void ejectQuarter(); - - /** - * 转动曲柄 - */ - void turnCrank(); - - /** - * 发放糖果 - */ - void dispense(); -} -``` -```java -public class HasQuarterState implements State{ - private GumballMachine gumballMachine; - - public HasQuarterState(GumballMachine gumballMachine){ - this.gumballMachine = gumballMachine; - } - - @Override - public void insertQuarter() { - System.out.println("You can't insert another quarter"); - } - - @Override - public void ejectQuarter() { - System.out.println("Quarter returned"); - gumballMachine.setState(gumballMachine.getNoQuarterState()); - } - - @Override - public void turnCrank() { - System.out.println("You turned..."); - gumballMachine.setState(gumballMachine.getSoldState()); - } - - @Override - public void dispense() { - System.out.println("No gumball dispensed"); - } -} -``` -```java -public class NoQuarterState implements State { - GumballMachine gumballMachine; - - public NoQuarterState(GumballMachine gumballMachine) { - this.gumballMachine = gumballMachine; - } - - @Override - public void insertQuarter() { - System.out.println("You insert a quarter"); - gumballMachine.setState(gumballMachine.getHasQuarterState()); - } - - @Override - public void ejectQuarter() { - System.out.println("You haven't insert a quarter"); - } - - @Override - public void turnCrank() { - System.out.println("You turned, but there's no quarter"); - } - - @Override - public void dispense() { - System.out.println("You need to pay first"); - } -} -``` -```java -public class SoldOutState implements State { - - GumballMachine gumballMachine; - - public SoldOutState(GumballMachine gumballMachine) { - this.gumballMachine = gumballMachine; - } - - @Override - public void insertQuarter() { - System.out.println("You can't insert a quarter, the machine is sold out"); - } - - @Override - public void ejectQuarter() { - System.out.println("You can't eject, you haven't inserted a quarter yet"); - } - - @Override - public void turnCrank() { - System.out.println("You turned, but there are no gumballs"); - } - - @Override - public void dispense() { - System.out.println("No gumball dispensed"); - } -} -``` -```java -public class SoldState implements State { - GumballMachine gumballMachine; - - public SoldState(GumballMachine gumballMachine) { - this.gumballMachine = gumballMachine; - } - - @Override - public void insertQuarter() { - System.out.println("Please wait, we're already giving you a gumball"); - } - - @Override - public void ejectQuarter() { - System.out.println("Sorry, you already turned the crank"); - } - - @Override - public void turnCrank() { - System.out.println("Turning twice doesn't get you another gumball!"); - } - - @Override - public void dispense() { - gumballMachine.releaseBall(); - if(gumballMachine.getCount()>0){ - gumballMachine.setState(gumballMachine.getNoQuarterState()); - } else{ - System.out.println("Oops, out of gumballs"); - gumballMachine.setState(gumballMachine.getSoldOutState()); - } - } -} -``` -```java -public class GumballMachine { - private State soldOutState; - private State noQuarterState; - private State hasQuarterState; - private State soldState; - - private State state; - private int count = 0; - - public GumballMachine(int numberGumballs) { - count = numberGumballs; - soldOutState = new SoldOutState(this); - noQuarterState = new NoQuarterState(this); - hasQuarterState = new HasQuarterState(this); - soldState = new SoldState(this); - - if (numberGumballs > 0) { - state = noQuarterState; - } else { - state = soldOutState; - } - } - - public void insertQuarter() { - state.insertQuarter(); - } - - public void ejectQuarter() { - state.ejectQuarter(); - } - - public void turnCrank() { - state.turnCrank(); - state.dispense(); - } - - public void setState(State state) { - this.state = state; - } - - public void releaseBall() { - System.out.println("A gumball comes rolling out the slot..."); - if (count != 0) { - count -= 1; - } - } - - public State getSoldOutState() { - return soldOutState; - } - - public State getNoQuarterState() { - return noQuarterState; - } - - public State getHasQuarterState() { - return hasQuarterState; - } - - public State getSoldState() { - return soldState; - } - - public int getCount() { - return count; - } -} -``` -```java -public class GumballMachineTestDrive { - public static void main(String[] args) { - GumballMachine gumballMachine = new GumballMachine(5); - - gumballMachine.insertQuarter(); - gumballMachine.turnCrank(); - - gumballMachine.insertQuarter(); - gumballMachine.ejectQuarter(); - gumballMachine.turnCrank(); - - gumballMachine.insertQuarter(); - gumballMachine.turnCrank(); - gumballMachine.insertQuarter(); - gumballMachine.turnCrank(); - gumballMachine.ejectQuarter(); - - gumballMachine.insertQuarter(); - gumballMachine.insertQuarter(); - gumballMachine.turnCrank(); - gumballMachine.insertQuarter(); - gumballMachine.turnCrank(); - gumballMachine.insertQuarter(); - gumballMachine.turnCrank(); - } -} -``` -运行结果 -```html -You insert a quarter -You turned... -A gumball comes rolling out the slot... -You insert a quarter -Quarter returned -You turned, but there's no quarter -You need to pay first -You insert a quarter -You turned... -A gumball comes rolling out the slot... -You insert a quarter -You turned... -A gumball comes rolling out the slot... -You haven't insert a quarter -You insert a quarter -You can't insert another quarter -You turned... -A gumball comes rolling out the slot... -You insert a quarter -You turned... -A gumball comes rolling out the slot... -Oops, out of gumballs -You can't insert a quarter, the machine is sold out -You turned, but there are no gumballs -No gumball dispensed -``` - -# 第十一章 代理模式 - -# 第十二章 复合模式 - -## MVC - -**传统 MVC** - -视图使用组合模式,模型使用了观察者模式,控制器使用了策略模式。 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//4f67611d-492f-4958-9fa0-4948010e345f.jpg) - -**Web 中的 MVC** - -模式不再使用观察者模式。 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//1dd56e61-2970-4d27-97c2-6e81cee86978.jpg) - -# 第十三章 与设计模式相处 - -定义:在某 **情境** 下,针对某 **问题** 的某种 **解决方案**。 - -过度使用设计模式可能导致代码被过度工程化,应该总是用最简单的解决方案完成工作,并在真正需要模式的地方才使用它。 - -反模式:不好的解决方案来解决一个问题。主要作用是为了警告不要使用这些解决方案。 - -模式分类: - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//524a237c-ffd7-426f-99c2-929a6bf4c847.jpg) - -# 第十四章 剩下的模式 diff --git a/notes/重构.md b/notes/重构.md deleted file mode 100644 index a25e566b..00000000 --- a/notes/重构.md +++ /dev/null @@ -1,980 +0,0 @@ - -* [第一章 第一个案例](#第一章-第一个案例) -* [第二章 重构原则](#第二章-重构原则) -* [第三章 代码的坏味道](#第三章-代码的坏味道) - * [1. Duplicated Code(重复代码)](#1-duplicated-code重复代码) - * [2. Long Method(过长函数)](#2-long-method过长函数) - * [3. Large Class(过大的类)](#3-large-class过大的类) - * [4. Long Parameter List(过长的参数列)](#4-long-parameter-list过长的参数列) - * [5. Divergent Change(发散式变化)](#5-divergent-change发散式变化) - * [6. Shotgun Surgery(散弹式修改)](#6-shotgun-surgery散弹式修改) - * [7. Feature Envy(依恋情结)](#7-feature-envy依恋情结) - * [8. Data Clumps(数据泥团)](#8-data-clumps数据泥团) - * [9. Primitive Obsession(基本类型偏执)](#9-primitive-obsession基本类型偏执) - * [10. Switch Statements(switch 惊悚现身)](#10-switch-statementsswitch-惊悚现身) - * [11. Parallel Inheritance Hierarchies(平行继承体系)](#11-parallel-inheritance-hierarchies平行继承体系) - * [12. Lazy Class(冗余类)](#12-lazy-class冗余类) - * [13. Speculative Generality(夸夸其谈未来性)](#13-speculative-generality夸夸其谈未来性) - * [14. Temporary Field(令人迷惑的暂时字段)](#14-temporary-field令人迷惑的暂时字段) - * [15. Message Chains(过度耦合的消息链)](#15-message-chains过度耦合的消息链) - * [16. Middle Man(中间人)](#16-middle-man中间人) - * [17. Inappropriate Intimacy(狎昵关系)](#17-inappropriate-intimacy狎昵关系) - * [18. Alernative Classes with Different Interfaces(异曲同工的类)](#18-alernative-classes-with-different-interfaces异曲同工的类) - * [19. Incomplete Library Class(不完美的类库)](#19-incomplete-library-class不完美的类库) - * [20. Data Class(幼稚的数据类)](#20-data-class幼稚的数据类) - * [21. Refused Bequest(被拒绝的馈赠)](#21-refused-bequest被拒绝的馈赠) - * [22. Comments(过多的注释)](#22-comments过多的注释) -* [第四章 构筑测试体系](#第四章-构筑测试体系) -* [第五章 重构列表](#第五章-重构列表) -* [第六章 重新组织函数](#第六章-重新组织函数) - * [1. Extract Method(提炼函数)](#1-extract-method提炼函数) - * [2. Inline Method(内联函数)](#2-inline-method内联函数) - * [3. Inline Temp(内联临时变量)](#3-inline-temp内联临时变量) - * [4. Replace Temp with Query(以查询取代临时变量)](#4-replace-temp-with-query以查询取代临时变量) - * [5. Introduce Explaining Variable(引起解释变量)](#5-introduce-explaining-variable引起解释变量) - * [6. Split Temporary Variable(分解临时变量)](#6-split-temporary-variable分解临时变量) - * [7. Remove Assigments to Parameters(移除对参数的赋值)](#7-remove-assigments-to-parameters移除对参数的赋值) - * [8. Replace Method with Method Object(以函数对象取代函数)](#8-replace-method-with-method-object以函数对象取代函数) - * [9. Subsititute Algorithn(替换算法)](#9-subsititute-algorithn替换算法) -* [第七章 在对象之间搬移特性](#第七章-在对象之间搬移特性) - * [1. Move Method(搬移函数)](#1-move-method搬移函数) - * [2. Move Field(搬移字段)](#2-move-field搬移字段) - * [3. Extract Class(提炼类)](#3-extract-class提炼类) - * [4. Inline Class(将类内联化)](#4-inline-class将类内联化) - * [5. Hide Delegate(隐藏“委托关系”)](#5-hide-delegate隐藏“委托关系”) - * [6. Remove Middle Man(移除中间人)](#6-remove-middle-man移除中间人) - * [7. Introduce Foreign Method(引入外加函数)](#7-introduce-foreign-method引入外加函数) - * [8. Introduce Local Extension(引入本地扩展)](#8-introduce-local-extension引入本地扩展) -* [第八章 重新组织数据](#第八章-重新组织数据) - * [1. Self Encapsulate Field(自封装字段)](#1-self-encapsulate-field自封装字段) - * [2. Replace Data Value with Object(以对象取代数据值)](#2-replace-data-value-with-object以对象取代数据值) - * [3. Change Value to Reference(将值对象改成引用对象)](#3-change-value-to-reference将值对象改成引用对象) - * [4. Change Reference to value(将引用对象改为值对象)](#4-change-reference-to-value将引用对象改为值对象) - * [5. Replace Array with Object(以对象取代数组)](#5-replace-array-with-object以对象取代数组) - * [6. Duplicate Observed Data(赋值“被监视数据”)](#6-duplicate-observed-data赋值“被监视数据”) - * [7. Change Unidirectional Association to Bidirectional(将单向关联改为双向关联)](#7-change-unidirectional-association-to-bidirectional将单向关联改为双向关联) - * [8. Change Bidirectional Association to Unidirectional(将双向关联改为单向关联)](#8-change-bidirectional-association-to-unidirectional将双向关联改为单向关联) - * [9. Replace Magic Number with Symbolic Constant(以字面常量取代魔法数)](#9-replace-magic-number-with-symbolic-constant以字面常量取代魔法数) - * [10. Encapsulate Field(封装字段)](#10-encapsulate-field封装字段) - * [11. Encapsulate Collection(封装集合)](#11-encapsulate-collection封装集合) - * [12. Replace Record with Data Class(以数据类取代记录)](#12-replace-record-with-data-class以数据类取代记录) - * [13. Replace Type Code with Class(以类取代类型码)](#13-replace-type-code-with-class以类取代类型码) - * [14. Replace Type Code with Subcalsses(以子类取代类型码)](#14-replace-type-code-with-subcalsses以子类取代类型码) - * [15. Replace Type Code with State/Strategy (以 State/Strategy 取代类型码)](#15-replace-type-code-with-statestrategy-以-statestrategy-取代类型码) - * [16. Replace Subclass with Fields(以字段取代子类)](#16-replace-subclass-with-fields以字段取代子类) -* [第九章 简化条件表达式](#第九章-简化条件表达式) - * [1. Decompose Conditional(分解条件表达式)](#1-decompose-conditional分解条件表达式) - * [2. Consolidate Conditional Expression(合并条件表达式)](#2-consolidate-conditional-expression合并条件表达式) - * [3. Consolidate Duplicate Conditional Fragments (合并重复的条件片段)](#3-consolidate-duplicate-conditional-fragments-合并重复的条件片段) - * [4. Remove Control Flag(移除控制标记)](#4-remove-control-flag移除控制标记) - * [5. Replace Nested Conditional with Guard Clauses (以卫语句取代嵌套条件表达式)](#5-replace-nested-conditional-with-guard-clauses-以卫语句取代嵌套条件表达式) - * [6. Replace Conditional with Polymorphism (以多态取代条件表达式)](#6-replace-conditional-with-polymorphism-以多态取代条件表达式) - * [7. Introduce Null Object(引入Null对象)](#7-introduce-null-object引入null对象) - * [8. Introduce Assertion(引入断言)](#8-introduce-assertion引入断言) -* [第十章 简化函数调用](#第十章-简化函数调用) - * [1. Rename Method(函数改名)](#1-rename-method函数改名) - * [2. Add Parameter(添加参数)](#2-add-parameter添加参数) - * [3. Remove Parameter(移除参数)](#3-remove-parameter移除参数) - * [4. Separate Query from Modifier(将查询函数和修改函数分离)](#4-separate-query-from-modifier将查询函数和修改函数分离) - * [5. Parameterize Method(令函数携带参数)](#5-parameterize-method令函数携带参数) - * [6. Replace Parameter with Explicit Methods(以明确函数取代参数)](#6-replace-parameter-with-explicit-methods以明确函数取代参数) - * [7. Preserve Whole Object(保持对象完整)](#7-preserve-whole-object保持对象完整) - * [8. Replace Parameter with Methods(以函数取代参数)](#8-replace-parameter-with-methods以函数取代参数) - * [9. Introduce Parameter Object(引入参数对象)](#9-introduce-parameter-object引入参数对象) - * [10. Remove Setting Method(移除设值函数)](#10-remove-setting-method移除设值函数) - * [11. Hide Method(隐藏函数)](#11-hide-method隐藏函数) - * [12. Replace Constructor with Factory Method (以工厂函数取代构造函数)](#12-replace-constructor-with-factory-method-以工厂函数取代构造函数) - * [13. Encapsulate Downcast(封装向下转型)](#13-encapsulate-downcast封装向下转型) - * [14. Replace Error Code with Exception (以异常取代错误码)](#14-replace-error-code-with-exception-以异常取代错误码) - * [15. Replace Exception with Test(以测试取代异常)](#15-replace-exception-with-test以测试取代异常) -* [第十一章 处理概括关系](#第十一章-处理概括关系) - * [1. Pull Up Field(字段上移)](#1-pull-up-field字段上移) - * [2. Pull Up Method(函数上移)](#2-pull-up-method函数上移) - * [3. Pull Up Constructor Body(构造函数本体上移)](#3-pull-up-constructor-body构造函数本体上移) - * [4. Push Down Method(函数下移)](#4-push-down-method函数下移) - * [5. Push Down Field(字段下移)](#5-push-down-field字段下移) - * [6. Extract Subclass(提炼子类)](#6-extract-subclass提炼子类) - * [7. Extract Superclass(提炼超类)](#7-extract-superclass提炼超类) - * [8. Extract Interface(提炼接口)](#8-extract-interface提炼接口) - * [9. Collapse Hierarchy(折叠继承体系)](#9-collapse-hierarchy折叠继承体系) - * [10. Form Template Method(塑造模板函数)](#10-form-template-method塑造模板函数) - * [11. Replace Inheritance with Delegation (以委托取代继承)](#11-replace-inheritance-with-delegation-以委托取代继承) - * [12. Replace Delegation with Inheritance (以继承取代委托)](#12-replace-delegation-with-inheritance-以继承取代委托) - - - -# 第一章 第一个案例 - -如果你发现自己需要为程序添加一个特性,而代码结构使你无法很方便地达成目的,那就先重构这个程序。 - -在重构前,需要先构建好可靠的测试环境,确保安全地重构。 - -重构是以微小的步伐修改程序,如果犯下错误,很容易便可以发现它。 - -**案例分析** - -影片出租店应用程序,包括三个类:Movie、Rental 和 Customer,Rental 包含租赁的 Movie 以及天数。 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//a758c8b2-0ac7-438f-90c2-3923ffad6328.png) - -最开始的实现是把所有的计费代码都放在 Customer 类中,在变化发生时,需要对这部分代码进行更改。本案例中可能发生的变化有:一种类别的计费方式发生改变;添加新的电影类别。考虑到计费代码可能存在于多处,一旦发生改变时,就需要对所有计费代码进行修改。 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//9e5e3cc6-3107-4051-b584-8ff077638fe6.png) - -以下是继承 Movie 的多态方案。但是由于一部 Movie 的类别会动态改变,因此这种方案不可行。 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//2a502516-5d34-4eef-8f39-916298a60035.png) - -引入 Price 来反应类别信息,通过组合的方式在 Movie 中加入 Price 对象,这样每种类别的计费方式都封装在不同的 Price 子类中,并且 Movie 对象也可以动态改变类别。这种方式可以很好地适应上述提到的变化。 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//c02a83b8-a6b9-4d00-a509-6f0516beaf5e.png) - -重构后的时序图和类图: - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//95f4559c-3d2a-4176-b365-4fbc46c76cf1.png) - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//293b9326-02fc-4ad8-8c79-b4a7b5ba60d3.png) - -# 第二章 重构原则 - -重构是对软件内部结构的一种调整,目的是在不改变软件可观察行为的前提下,提高其可理解性,降低其修改成本。 - -重构的好处:改进软件设计;使软件更容易理解;帮助找到 bug;提高编程速度。 - -三次法则:第一次做某件事时只管去做;第二次做类似事情时可以去做;第三次再做类似的事,就应该重构。 - -间接层与重构:计算机科学中的很多问题可以通过增加一个间接层来解决,间接层具有以下价值:允许逻辑共享;分开解释意图和实现;隔离变化;封装条件逻辑。重构可以理解为在适当的位置插入间接层以及在不需要时移除间接层。 - -修改接口:可以保留旧接口,让旧接口去调用新接口,并且使用 Java 提供的 @deprecation 将旧接口标记为弃用。除非真有必要,不要发布接口,并且不要过早发布接口。 - -当现有代码过于混乱时,应当重写而不是重构。一个折中的办法是,将代码封装成一个个组件,然后对各个组件做重写或者重构的决定。 - -软件开发无法预先设计,因为开发过程有很多变化发生,在最开始不可能都把所有情况考虑进去。重构可以简化设计,重构在一个简单的设计上进行修修改改,当变化发生时,以一种灵活的方式去应对变化,进而带来更好的设计。 - -为了软代码更容易理解,重构可能会导致性能减低。在编写代码时,不用对性能过多关注,只有在最后性能优化阶段再考虑性能问题。应当只关注关键代码的性能,因为只有一小部分的代码是关键代码。 - -# 第三章 代码的坏味道 - -## 1. Duplicated Code(重复代码) - -同一个类的两个函数有相同表达式,则用 Extract Method 提取出重复代码; - -两个互为兄弟的子类含有相同的表达式,先使用 Extract Method,然后把提取出来的函数 Pull Up Method 推入超类。 - -如果只是部分相同,用 Extract Method 分离出相似部分和差异部分,然后使用 Form Template Method 这种模板方法设计模式。 - -如果两个毫不相关的类出现重复代码,则使用 Extract Class 方法将重复代码提取到一个独立类中。 - -## 2. Long Method(过长函数) - -间接层的价值:解释能力、共享能力、选择能力; - -分解函数的原则:当需要用注释来说明一段代码时,就需要把这部分代码写入一个独立的函数中。 - -Extract Method 会把很多参数和临时变量都当做参数,可以用 Replace Temp with Query 消除临时变量,Introduce Parameter Object 和 Preserve Whole Object 可以将过长的参数列变得更简洁。 - -条件和循环往往也需要提取到新的函数中。 - -## 3. Large Class(过大的类) - -过大的类做了过多事情,需要使用 Extract Class 或 Extract Subclass。 - -先确定客户端如何使用它们,然后运用 Extract Interface 为每一种使用方式提取出一个接口。 - -## 4. Long Parameter List(过长的参数列) - -## 5. Divergent Change(发散式变化) - -一个类受到多种变化的影响; - -针对某种原因的变化,使用 Extract Class 将它提炼到一个类中。 - -## 6. Shotgun Surgery(散弹式修改) - -一个变化引起多个类修改; - -使用 Move Method 和 Move Field 把所有需要修改地代码放到同一个类中。 - -## 7. Feature Envy(依恋情结) - -一个函数对某个类的兴趣高于对自己所处类的兴趣,通常是过多访问其它类的数据。 - -使用 Move Method 将它移到该去的地方,如果对多个类都有 Feature Envy,先用 Extract Method 提取出多个函数。 - -## 8. Data Clumps(数据泥团) - -有些数据经常一起出现,比如两个类具有相同的字段、许多函数有相同的参数。使用 Extract Class 将它们放在一起。 - -## 9. Primitive Obsession(基本类型偏执) - -使用类往往比使用基本类型更好,使用 Replace Data Value with Object 将数据值替换为对象。 - -## 10. Switch Statements(switch 惊悚现身) - -## 11. Parallel Inheritance Hierarchies(平行继承体系) - -每当为某个类增加一个子类,必须也为另一个类相应增加一个子类。 - -这种结果会带来一些重复性,消除重复性的一般策略:让一个继承体系的实例引用另一个继承体系的实例。 - -## 12. Lazy Class(冗余类) - -如果一个类没有做足够多的工作,就应该消失。 - -## 13. Speculative Generality(夸夸其谈未来性) - -有些内容是用来处理未来可能发生的变化,但是往往会造成系统难以理解和维护,并且预测未来可能发生的改变很可能和最开始的设想相反。因此,如果不是必要,就不要这么做。 - -## 14. Temporary Field(令人迷惑的暂时字段) - -某个字段仅为某种特定情况而设,这样的代码不易理解,因为通常认为对象在所有时候都需要它的所有字段。 - -把这种字段和特定情况的处理操作使用 Extract Class 提炼到一个独立类中。 - -## 15. Message Chains(过度耦合的消息链) - -一个对象请求另一个对象,然后再向后者请求另一个对象,然后...,这就是消息链。采用这种方式,意味着客户代码将与对象间的关系紧密耦合。 - -改用函数链,用函数委托另一个对象来处理。 - -## 16. Middle Man(中间人) - -中间人负责处理委托给它的操作,如果一个类中有过多的函数都委托给其它类,那就是过度运用委托,应当 Remove Middle Man,直接与负责的对象打交道。 - -## 17. Inappropriate Intimacy(狎昵关系) - -两个类多于亲密,花费太多时间去探讨彼此的 private 成分。 - -## 18. Alernative Classes with Different Interfaces(异曲同工的类) - -## 19. Incomplete Library Class(不完美的类库) - -类库的设计者不可能设计出完美的类库,当我们需要对类库进行一些修改时,可以使用以下两种方法:如果只是修改一两个函数,使用 Introduce Foreign Method;如果要添加一大堆额外行为,使用 Introduce Local Extension。 - -## 20. Data Class(幼稚的数据类) - -它只拥有一些数据字段。 - -找出字段使用的地方,然后把相应的操作移到 Data Class 中。 - -## 21. Refused Bequest(被拒绝的馈赠) - -子类继承超类的所有函数和数据,但是它只想要一部分。 - -为子类新建一个兄弟类,不需要的函数或数据使用 Push Down Method 和 Push Down Field 下推给那个兄弟。 - -## 22. Comments(过多的注释) - -使用 Extract Method 提炼出需要注释的部分,然后用函数名来解释函数的行为。 - -# 第四章 构筑测试体系 - -Java 可以使用 Junit 进行单元测试。 - -单元测试的对象是类的方法,而功能测以客户的角度保证软件正常运行。 - -应当集中测试可能出错的边界条件。 - -# 第五章 重构列表 - -小步前进,频繁测试。 - -# 第六章 重新组织函数 - -## 1. Extract Method(提炼函数) - -将这段代码放进一个独立函数中,并让函数名称解释该函数的用途。 - -## 2. Inline Method(内联函数) - -一个函数的本体与名称同样清楚易懂。 - -在函数调用点插入函数本体,然后移除该函数。 - -## 3. Inline Temp(内联临时变量) - -一个临时变量,只被简单表达式赋值一次,而它妨碍了其它重构手法。 - -将所有对该变量的引用替换为对它赋值的那个表达式自身。 - -```java -double basePrice = anOrder.basePrice(); -return basePrice > 1000; -``` - -```java -return anOrder.basePrice() > 1000; -``` - -## 4. Replace Temp with Query(以查询取代临时变量) - -以临时变量保存某一表达式的运算结果,将这个表达式提炼到一个独立函数中,将所有对临时变量的引用点替换为对新函数的调用。Replace Temp with Query 往往是 Extract Method 之前必不可少的一个步骤,因为局部变量会使代码难以提炼。 - -```java -double basePrice = quantity * itemPrice; -if(basePrice > 1000) - return basePrice * 0.95; -else - return basePrice * 0.98; -``` - -```java -if(basePrice() > 1000) - return basePrice() * 0.95; -else - return basePrice() * 0.98; - -// ... -double basePrice(){ - return quantity * itemPrice; -} -``` - -## 5. Introduce Explaining Variable(引起解释变量) - -将复杂表达式(或其中一部分)的结果放进一个临时变量,以此变量名称来解释表达式用途。 - -```java -if((platform.toUpperCase().indexOf("MAC") > -1) && - (browser.toUpperCase().indexOf("IE") > -1) && - wasInitialized() && resize > 0) { - // do something -} -``` - -```java -final boolean isMacOS = platform.toUpperCase().indexOf("MAC") > -1; -final boolean isIEBrower = browser.toUpperCase().indexOf("IE") > -1; -final boolean wasResized = resize > 0; - -if(isMacOS && isIEBrower && wasInitialized() && wasResized) { - // do something -} -``` - -## 6. Split Temporary Variable(分解临时变量) - -某个临时变量被赋值超过一次,它既不是循环变量,也不是用于收集计算结果。 - -针对每次赋值,创造一个独立、对应的临时变量,每个临时变量只承担一个责任。 - -## 7. Remove Assigments to Parameters(移除对参数的赋值) - -以一个临时变量取代对该参数的赋值。 - -```java -int discount (int inputVal, int quentity, int yearToDate){ - if (inputVal > 50) inputVal -= 2; -``` - -```java -int discount (int inputVal, int quentity, int yearToDate){ - int result = inputVal; - if (inputVal > 50) result -= 2; -``` - -## 8. Replace Method with Method Object(以函数对象取代函数) - -当对一个大型函数采用 Extract Method 时,由于包含了局部变量使得很难进行该操作。 - -将这个函数放进一个单独对象中,如此一来局部变量就成了对象内的字段。然后可以在同一个对象中将这个大型函数分解为多个小型函数。 - -## 9. Subsititute Algorithn(替换算法) - -# 第七章 在对象之间搬移特性 - -## 1. Move Method(搬移函数) - -类中的某个函数与另一个类进行更多交流:调用后者或者被后者调用。 - -将这个函数搬移到另一个类中。 - -## 2. Move Field(搬移字段) - -类中的某个字段被另一个类更多地用到,这里的用到是指调用取值设值函数,应当把该字段移到另一个类中。 - -## 3. Extract Class(提炼类) - -某个类做了应当由两个类做的事。 - -应当建立一个新类,将相关的字段和函数从旧类搬移到新类。 - -## 4. Inline Class(将类内联化) - -与 Extract Class 相反。 - -## 5. Hide Delegate(隐藏“委托关系”) - -建立所需的函数,隐藏委托关系。 - -```java -class Person{ - Department department; - - public Department getDepartment(){ - return department; - } -} - -class Department{ - private Person manager; - - public Person getManager(){ - return manager; - } -} -``` - -如果客户希望知道某人的经理是谁,必须获得 Department 对象,这样就对客户揭露了 Department 的工作原理。 - -```java -Person manager = john.getDepartment().getManager(); -``` - -通过为 Peron 建立一个函数来隐藏这种委托关系。 - -```java -public Person getManager(){ - return department.getManager(); -} -``` - -## 6. Remove Middle Man(移除中间人) - -与 Hide Delegate 相反,本方法需要移除委托函数,让客户直接调用委托类。 - -Hide Delegate 有很大好处,但是它的代价是:每当客户要使用受托类的新特性时,就必须在服务器端添加一个简单的委托函数。随着受委托的特性越来越多,服务器类完全变成了一个“中间人”。 - -## 7. Introduce Foreign Method(引入外加函数) - -需要为提供服务的类添加一个函数,但是无法修改这个类。 - -可以在客户类中建立一个函数,并以第一参数形式传入一个服务类的实例,让客户类组合服务器实例。 - -## 8. Introduce Local Extension(引入本地扩展) - -和 Introduce Foreign Method 目的一样,但是 Introduce Local Extension 通过建立新的类来实现。有两种方式:子类或者包装类,子类就是通过继承实现,包装类就是通过组合实现。 - -# 第八章 重新组织数据 - -## 1. Self Encapsulate Field(自封装字段) - -为字段建立取值/设值函数,并用这些函数来访问字段。只有当子类想访问超类的一个字段,又想在子类中将对这个字段访问改为一个计算后的值,才使用这种方式,否则直接访问字段的方式简洁明了。 - -## 2. Replace Data Value with Object(以对象取代数据值) - -在开发初期,往往会用简单的数据项表示简单的情况,但是随着开发的进行,一些简单数据项会具有一些特殊行为。比如一开始会把电话号码存成字符串,但是随后发现电话号码需要“格式化”、“抽取区号”之类的特殊行为。 - -## 3. Change Value to Reference(将值对象改成引用对象) - -将彼此相等的实例替换为同一个对象。这就要用一个工厂来创建这种唯一对象,工厂类中需要保留一份已经创建对象的列表,当要创建一个对象时,先查找这份列表中是否已经存在该对象,如果存在,则返回列表中的这个对象;否则,新建一个对象,添加到列表中,并返回该对象。 - -## 4. Change Reference to value(将引用对象改为值对象) - -以 Change Value to Reference 相反。值对象有个非常重要的特性:它是不可变的,不可变表示如果要改变这个对象,必须用一个新的对象来替换旧对象,而不是修改旧对象。 - -需要为值对象实现 equals() 和 hashCode() 方法 - -## 5. Replace Array with Object(以对象取代数组) - -有一个数组,其中的元素各自代表不同的东西。 - -以对象替换数组,对于数组中的每个元素,以一个字段来表示,这样方便操作,也更容易理解。 - -## 6. Duplicate Observed Data(赋值“被监视数据”) - -一些领域数据置身于 GUI 控件中,而领域函数需要访问这些数据。 - -将该数据赋值到一个领域对象中,建立一个 Oberver 模式,用以同步领域对象和 GUI 对象内的重复数据。 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//e024bd7e-fb4e-4239-9451-9a6227f50b00.jpg) - -## 7. Change Unidirectional Association to Bidirectional(将单向关联改为双向关联) - -当两个类都需要对方的特性时,可以使用双向关联。 - -有两个类,分别为订单 Order 和客户 Customer,Order 引用了 Customer,Customer 也需要引用 Order 来查看其所有订单详情。 - -```java -class Order{ - private Customer customer; - public void setCustomer(Customer customer){ - if(this.customer != null) - this.customer.removeOrder(this); - this.customer = customer; - this.customer.add(this); - } -} -``` -```java -class Curstomer{ - private Set orders = new HashSet<>(); - public void removeOrder(Order order){ - orders.remove(order); - } - public void addOrder(Order order){ - orders.add(order); - } -} -``` - -注意到,这里让 Curstomer 类来控制关联关系。有以下原则来决定哪个类来控制关联关系:如果某个对象是组成另一个对象的部件,那么由后者负责控制关联关系;如果是一对多关系,则由单一引用那一方来控制关联关系。 - -## 8. Change Bidirectional Association to Unidirectional(将双向关联改为单向关联) - -和 Change Unidirectional Association to Bidirectiona 为反操作。 - -双向关联维护成本高,并且也不易于理解。大量的双向连接很容易造成“僵尸对象”:某个对象本身已经死亡了,却保留在系统中,因为它的引用还没有全部完全清除。 - -## 9. Replace Magic Number with Symbolic Constant(以字面常量取代魔法数) - -创建一个常量,根据其意义为它命名,并将字面常量换位这个常量。 - -## 10. Encapsulate Field(封装字段) - -public 字段应当改为 private,并提供相应的访问函数。 - -## 11. Encapsulate Collection(封装集合) - -函数返回集合的一个只读副本,并在这个类中提供添加/移除集合元素的函数。如果函数返回集合自身,会让用户得以修改集合内容而集合拥有者却一无所知。 - -## 12. Replace Record with Data Class(以数据类取代记录) - -## 13. Replace Type Code with Class(以类取代类型码) - -类中有一个数值类型码,但它并不影响类的行为,就用一个新类替换该数值类型码。如果类型码出现在 switch 语句中,需要使用 Replace Conditional with Polymorphism 去掉 switch,首先必须运用 Replace Type Code with Subcalss 或 Replace Type Code with State/Strategy 去掉类型码。 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//27c2e0b3-8f95-453d-bedc-6398a8566ce9.jpg) - -## 14. Replace Type Code with Subcalsses(以子类取代类型码) - -有一个不可变的类型码,它会影响类的行为,以子类取代这个类型码。 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//c41d3977-e0e7-4ee4-93e1-d84f1ae3e20e.jpg) - -## 15. Replace Type Code with State/Strategy (以 State/Strategy 取代类型码) - -有一个可变的类型码,它会影响类的行为,以状态对象取代类型码。 - -和 Replace Type Code with Subcalsses 的区别是 Replace Type Code with State/Strategy 的类型码是动态可变的,前者通过继承的方式来实现,后者通过组合的方式来实现。因为类型码可变,如果通过继承的方式,一旦一个对象的类型码改变,那么就要改变用新的对象来取代旧对象,而客户端难以改变新的对象。但是通过组合的方式,改变引用的状态类是很容易的。 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//81fd1d6f-a3b2-4160-9a0a-1f7cb50ba440.jpg) - -## 16. Replace Subclass with Fields(以字段取代子类) - -各个子类的唯一差别只在“返回常量数据”的函数上。 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//f2e0cee9-ecdc-4a96-853f-d9f6a1ad6ad1.jpg) - -# 第九章 简化条件表达式 - -## 1. Decompose Conditional(分解条件表达式) - -对于一个复杂的条件语句,可以从 if、then、else 三个段落中分别提炼出独立函数。 - -```java -if(data.befor(SUMMER_START) || data.after(SUMMER_END)) - charge = quantity * winterRate + winterServiceCharge; -else charge = quantity * summerRate; -``` - -```java -if(notSummer(date)) - charge = winterCharge(quantity); -else charge = summerCharge(quantity); -``` - -## 2. Consolidate Conditional Expression(合并条件表达式) - -有一系列条件测试,都得到相同结果。 - -将这些测试合并为一个条件表达式,并将这个条件表达式提炼成为一个独立函数。 - -```java -double disabilityAmount(){ - if (seniority < 2) return 0; - if (monthsDisabled > 12 ) return 0; - if (isPartTime) return 0; - // ... -} -``` -```java -double disabilityAmount(){ - if (isNotEligibleForDisability()) return 0; - // ... -} -``` - -## 3. Consolidate Duplicate Conditional Fragments (合并重复的条件片段) - -在条件表达式的每个分支上有着相同的一段代码。 - -将这段重复代码搬移到条件表达式之外。 - -```java -if (isSpecialDeal()){ - total = price * 0.95; - send(); -} else { - total = price * 0.98; - send(); -} -``` - -```java -if (isSpecialDeal()) { - total = price * 0.95; -} else { - total = price * 0.98; -} -send(); -``` - -## 4. Remove Control Flag(移除控制标记) - -在一系列布尔表达式中,某个变量带有“控制标记”的作用。 - -用 break语 句或 return 语句来取代控制标记。 - -## 5. Replace Nested Conditional with Guard Clauses (以卫语句取代嵌套条件表达式) - -如果某个条件极其罕见,就应该单独检查该条件,并在该条件为真时立刻从函数中返回,这样的单独检查常常被称为“卫语句”(guard clauses)。 - -条件表达式通常有两种表现形式。第一种形式是:所有分支都属于正常行为。第二种形式则是:条件表达式提供的答案中只有一种是正常行为,其他都是不常见的情况,可以使用卫语句表现所有特殊情况。 - -```java -double getPayAmount() { - double result; - if (isDead) result = deadAmount(); - else { - if (isSeparated) result = separatedAmount(); - else { - if (isRetired) result = retiredAmount(); - else result = normalPayAmount(); - }; - } - return result; -}; -``` - -```java -double getPayAmount() { - if (isDead) return deadAmount(); - if (isSeparated) return separatedAmount(); - if (isRetired) return retiredAmount(); - return normalPayAmount(); -}; -``` - -## 6. Replace Conditional with Polymorphism (以多态取代条件表达式) - -将这个条件表达式的每个分支放进一个子类内的覆写函数中,然后将原始函数声明为抽象函数。需要先使用 Replace Type Code with Subclass 或 Replace Type Code with State/Strategy 来建立继承结果。 - -```java -double getSpeed() { - switch (type) { - case EUROPEAN: - return getBaseSpeed(); - case AFRICAN: - return getBaseSpeed()- getLoadFactor()* numberOfCoconuts; - case NORWEGIAN_BLUE: - return isNailed ? 0 : getBaseSpeed(voltage); - } - throw new RuntimeException("Should be unreachable"); -} -``` - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//1c8432c8-2552-457f-b117-1da36c697221.jpg) - -## 7. Introduce Null Object(引入Null对象) - -将 null 值替换为 null 对象。这样做的好处在于,不需要询问对象是否为空,直接调用就行。 - -```java -if (customer == null) plan = BillingPlan.basic(); -else plan = customer.getPlan(); -``` - -## 8. Introduce Assertion(引入断言) - -以断言明确表现某种假设。断言只能用于开发过程中,产品代码中不会有断言。 - -```java -double getExpenseLimit() { - // should have either expense limit or a primary project - return (expenseLimit != NULL_EXPENSE) ? expenseLimit : primaryProject.getMemberExpenseLimit(); -} -``` - -```java -double getExpenseLimit() { - Assert.isTrue (expenseLimit != NULL_EXPENSE || primaryProject != null); - return (expenseLimit != NULL_EXPENSE) ? expenseLimit : primaryProject.getMemberExpenseLimit(); -} -``` - -# 第十章 简化函数调用 - -## 1. Rename Method(函数改名) - -使函数名能解释函数的用途。 - -## 2. Add Parameter(添加参数) - -使函数不需要通过调用获得某个信息。 - -## 3. Remove Parameter(移除参数) - -与 Add Parameter 相反,改用调用的方式来获得某个信息。 - -## 4. Separate Query from Modifier(将查询函数和修改函数分离) - -某个函数即返回对象状态值,又修改对象状态。 - -应当建立两个不同的函数,其中一个负责查询,另一个负责修改。任何有返回值的函数,都不应该有看得到的副作用。 - -```java -getTotalOutstandingAndSetReadyForSummaries(); -``` - -```java -getTotalOutstanding(); -setReadyForSummaries(); -``` - -## 5. Parameterize Method(令函数携带参数) - -若干函数做了类似的工作,但在函数本体中却包含了不同的值。 - -建立单一函数,以参数表达那些不同的值。 - -```java -fivePercentRaise(); -tenPercentRaise(); -``` -```java -raise(percentage); -``` - -## 6. Replace Parameter with Explicit Methods(以明确函数取代参数) - -有一个函数,完全取决于参数值而采取不同行为。 - -针对该参数的每一个可能值,建立一个独立函数。 - -```java -void setValue(String name, int value){ - if (name.equals("height")){ - height = value; - return; - } - if (name.equals("width")){ - width = value; - return; - } - Assert.shouldNeverReachHere(); -} -``` - -```java -void setHeight(int arg){ - height = arg; -} -void setWidth(int arg){ - width = arg; -} -``` - -## 7. Preserve Whole Object(保持对象完整) - -从某个对象中取出若干值,将它们作为某一次函数调用时的参数。 - -改为传递整个对象。 - -```java -int low = daysTempRange().getLow(); -int high = daysTempRange().getHigh(); -withinPlan = plan.withinRange(low,high); -``` - -```java -withinPlan = plan.withinRange(daysTempRange()); -``` - -## 8. Replace Parameter with Methods(以函数取代参数) - -对象调用某个函数,并将所得结果作为参数,传递给另一个函数。而接受该参数的函数本身也能够调用前一个函数。 - -让参数接收者去除该项参数,而是直接调用前一个函数。 - -```java -int basePrice = _quantity * _itemPrice; -discountLevel = getDiscountLevel(); -double finalPrice = discountedPrice (basePrice, discountLevel); -``` - -```java -int basePrice = _quantity * _itemPrice; -double finalPrice = discountedPrice (basePrice); -``` - -## 9. Introduce Parameter Object(引入参数对象) - -某些参数总是很自然地同时出现,这些参数就是 Data Clumps。 - -以一个对象取代这些参数。 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//08738dd0-ae8e-404a-ba78-a6b1b7d225b3.jpg) - -## 10. Remove Setting Method(移除设值函数) - -类中的某个字段应该在对象创建时被设值,然后就不再改变。 - -去掉该字段的所有设值函数,并将该字段设为 final。 - -## 11. Hide Method(隐藏函数) - -有一个函数,从来没有被其他任何类用到。 - -将这个函数修改为 private。 - -## 12. Replace Constructor with Factory Method (以工厂函数取代构造函数) - -希望在创建对象时不仅仅是做简单的建构动作。 - -将构造函数替换为工厂函数。 - -## 13. Encapsulate Downcast(封装向下转型) - -某个函数返回的对象,需要由函数调用者执行向下转型(downcast)。 - -将向下转型动作移到函数中。 - -```java -Object lastReading(){ - return readings.lastElement(); -} -``` -```java -Reading lastReading(){ - return (Reading)readings.lastElement(); -} -``` - -## 14. Replace Error Code with Exception (以异常取代错误码) - -某个函数返回一个特定的代码,用以表示某种错误情况。 - -改用异常,异常将普通程序和错误处理分开,使代码更容易理解。 - -## 15. Replace Exception with Test(以测试取代异常) - -面对一个调用者可以预先检查的条件,你抛出了一个异常。 - -修改调用者,使它在调用函数之前先做检查。 - -```java -double getValueForPeriod(int periodNumber) { - try { - return values[periodNumber]; - } catch (ArrayIndexOutOfBoundsException e) { - return 0; - } -} -``` -```java -double getValueForPeriod(int periodNumber) { - if (periodNumber >= values.length) return 0; - return values[periodNumber]; -``` - -# 第十一章 处理概括关系 - -## 1. Pull Up Field(字段上移) - -两个子类拥有相同的字段。 - -将该字段移至超类。 - -## 2. Pull Up Method(函数上移) - -有些函数,在各个子类中产生完全相同的结果。 - -将该函数移至超类。 - -## 3. Pull Up Constructor Body(构造函数本体上移) - -你在各个子类中拥有一些构造函数,它们的本体几乎完全一致。 - -在超类中新建一个构造函数,并在子类构造函数中调用它。 - -```java -class Manager extends Employee... - -public Manager(String name, String id, int grade) { - this.name = name; - this.id = id; - this.grade = grade; -} -``` - -```java -public Manager(String name, String id, int grade) { - super(name, id); - this.grade = grade; -} -``` - -## 4. Push Down Method(函数下移) - -超类中的某个函数只与部分子类有关。 - -将这个函数移到相关的那些子类去。 - -## 5. Push Down Field(字段下移) - -超类中的某个字段只被部分子类用到。 - -将这个字段移到需要它的那些子类去。 - -## 6. Extract Subclass(提炼子类) - -类中的某些特性只被某些实例用到。 - -新建一个子类,将上面所说的那一部分特性移到子类中。 - -## 7. Extract Superclass(提炼超类) - -两个类有相似特性。 - -为这两个类建立一个超类,将相同特性移至超类。 - -## 8. Extract Interface(提炼接口) - -若干客户使用类接口中的同一子集,或者两个类的接口有部分相同。 - -将相同的子集提炼到一个独立接口中。 - -## 9. Collapse Hierarchy(折叠继承体系) - -超类和子类之间无太大区别。 - -将它们合为一体。 - -## 10. Form Template Method(塑造模板函数) - -你有一些子类,其中相应的某些函数以相同顺序执行类似的操作,但各个操作的细节上有所不同。 - -将这些操作分别放进独立函数中,并保持它们都有相同的签名,于是原函数也就变得相同了。然后将原函数上移至超类。(模板方法模式) - -## 11. Replace Inheritance with Delegation (以委托取代继承) - -某个子类只使用超类接口中的一部分,或是根本不需要继承而来的数据。 - -在子类中新建一个字段用以保存超类,调整子类函数,令它改而委托超类,然后去掉两者之间的继承关系。 - -## 12. Replace Delegation with Inheritance (以继承取代委托) - -你在两个类之间使用委托关系,并经常为整个接口编写许多极简单的委托函数。 - -让委托类继承受托类。 diff --git a/notes/面向对象思想.md b/notes/面向对象思想.md deleted file mode 100644 index 6e758727..00000000 --- a/notes/面向对象思想.md +++ /dev/null @@ -1,308 +0,0 @@ - -* [S.O.L.I.D](#solid) - * [1. 单一责任原则](#1-单一责任原则) - * [2. 开放封闭原则](#2-开放封闭原则) - * [3. 里氏替换原则](#3-里氏替换原则) - * [4. 接口分离原则](#4-接口分离原则) - * [5. 依赖倒置原则](#5-依赖倒置原则) -* [封装、继承、多态](#封装继承多态) - * [1. 封装](#1-封装) - * [2. 继承](#2-继承) - * [3. 多态](#3-多态) -* [UML](#uml) - * [1. 类图](#1-类图) - * [2. 时序图](#2-时序图) -* [参考资料](#参考资料) - - - -# S.O.L.I.D - -S.O.L.I.D是面向对象设计和编程(OOD&OOP)中几个重要编码原则(Programming Priciple)的首字母缩写。 - -|简写 |全拼 |中文翻译| -| -- | -- | -- | -|SRP| The Single Responsibility Principle |单一责任原则| -|OCP| The Open Closed Principle | 开放封闭原则| -|LSP| The Liskov Substitution Principle |里氏替换原则| -|ISP| The Interface Segregation Principle |接口分离原则| -|DIP| The Dependency Inversion Principle |依赖倒置原则| - - -## 1. 单一责任原则 - -当需要修改某个类的时候原因有且只有一个。换句话说就是让一个类只做一种类型责任,当这个类需要承当其他类型的责任的时候,就需要分解这个类。 - -## 2. 开放封闭原则 - -软件实体应该是可扩展,而不可修改的。也就是说,对扩展是开放的,而对修改是封闭的。 - -## 3. 里氏替换原则 - -当一个子类的实例应该能够替换任何其超类的实例时,它们之间才具有 is-a 关系。 - -## 4. 接口分离原则 - -不能强迫用户去依赖那些他们不使用的接口。换句话说,使用多个专门的接口比使用单一的总接口总要好。 - -## 5. 依赖倒置原则 - -1. 高层模块不应该依赖于低层模块,二者都应该依赖于抽象 -2. 抽象不应该依赖于细节,细节应该依赖于抽象 - -# 封装、继承、多态 - -封装、继承、多态是面向对象的三大特性。 - -## 1. 封装 - -利用抽象数据类型将数据和基于数据的操作封装在一起,使其构成一个不可分割的独立实体,数据被保护在抽象数据类型的内部,尽可能地隐藏内部的细节,只保留一些对外接口使之与外部发生联系。用户是无需知道对象内部的细节,但可以通过该对象对外的提供的接口来访问该对象。 - -封装有三大好处: - -1. 良好的封装能够减少耦合。 - -2. 类内部的结构可以自由修改。 - -3. 可以对成员进行更精确的控制。 - -4. 隐藏信息,实现细节。 - -以下 Person 类封装 name、gender、age 等属性,外界只能通过 get() 方法获取一个 Person 对象的 name 属性和 gender 属性,而无法获取 age 属性,但是 age 属性可以供 work() 方法使用。 - -注意到 gender 属性使用 int 数据类型进行存储,封装使得用户注意不到这种实现细节。并且在需要修改使用的数据类型时,也可以在不影响客户端代码的情况下进行。 - -```java -public class Person { - private String name; - private int gender; - private int age; - - public String getName() { - return name; - } - - public String getGender() { - return gender == 0 ? "man" : "woman"; - } - - public void work() { - if(18 <= age && age <= 50) { - System.out.println(name + " is working very hard!"); - } else { - System.out.println(name + " can't work!"); - } - } -} -``` - -## 2. 继承 - -继承实现了 **is-a** 关系,例如 Cat 和 Animal 就是一种 is-a 关系,因此可以将 Cat 继承自 Animal,从而获得 Animal 非 private 的属性和方法。 - -Cat 可以当做 Animal 来使用,也就是可以使用 Animal 引用 Cat 对象,这种子类转换为父类称为 **向上转型**。 - -继承应该遵循里氏替换原则:当一个子类的实例应该能够替换任何其超类的实例时,它们之间才具有 is-a 关系。 - -```java -Animal animal = new Cat(); -``` - -## 3. 多态 - -多态分为编译时多态和运行时多态。编译时多态主要指方法的重装,运行时多态指程序中定义的对象引用所指向的具体类型在运行期间才确定。 - -多态有三个条件:1. 继承;2. 覆盖父类方法;3. 向上转型。 - -下面的代码中,乐器类(Instrument)有两个子类:Wind 和 Percussion,它们都覆盖了 play() 方法,并且在 main() 方法中使用父类 Instrument 来引用 Wind 和 Percussion 对象。在 Instrument 引用调用 play() 方法时,会执行实际引用对象所在类的 play() 方法,而不是 Instrument 类的方法。 - -```java -public class Instrument { - public void play() { - System.out.println("Instument is playing..."); - } -} - -public class Wind extends Instrument { - public void play() { - System.out.println("Wind is playing..."); - } -} - -public class Percussion extends Instrument { - public void play() { - System.out.println("Percussion is playing..."); - } -} - -public class Music { - public static void main(String[] args){ - List instruments = new ArrayList<>(); - instruments.add(new Wind()); - instruments.add(new Percussion()); - for(Instrument instrument : instruments){ - instrument.play(); - } - } -} - -``` - - - -# UML - -## 1. 类图 - -**1.1 继承相关** - -继承有两种形式: 泛化(generalize)和实现(realize),表现为 is-a 关系。 - -① 泛化关系(generalization) - -从具体类中继承 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//29badd92-109f-4f29-abb9-9857f5973928.png) - -② 实现关系(realize) - -从抽象类或者接口中继承 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//4b16e1d3-3a60-472c-9756-2f31b1c48abe.png) - -**1.2 整体和部分** - -① 聚合关系(aggregation) - -表示整体由部分组成,但是整体和部分不是强依赖的,整体不存在了部分还是会存在。以下表示 B 由 A 组成: - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//34259bb8-ca3a-4872-8771-9e946782d9c3.png) - - -② 组合关系(composition) - -和聚合不同,组合中整体和部分是强依赖的,整体不存在了部分也不存在了。比如公司和部门,公司没了部门就不存在了。但是公司和员工就属于聚合关系了,因为公司没了员工还在。 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//7dda050d-ac35-4f47-9f51-18f18ed6fa9a.png) - -**1.3 相互联系** - -① 关联关系(association) - -表示不同类对象之间有关联,这是一种静态关系,与运行过程的状态无关,在最开始就可以确定。因此也可以用 1 对 1、多对 1、多对多这种关联关系来表示。比如学生和学校就是一种关联关系,一个学校可以有很多学生,但是一个学生只属于一个学校,因此这是一种多对一的关系,在运行开始之前就可以确定。 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//4ccd294c-d6b2-421b-839e-d88336ff5fb7.png) - -② 依赖关系(dependency) - -和关联关系不同的是, 依赖关系是在运行过程中起作用的。一般依赖作为类的构造器或者方法的参数传入。双向依赖时一种不好的设计。 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//47ca2614-509f-476e-98fc-50ec9f9d43c0.png) - - -## 2. 时序图 - -**2.1 定义** - -时序图描述了对象之间传递消息的时间顺序,它用来表示用例的行为顺序。它的主要作用是通过对象间的交互来描述用例(注意是对象),从而寻找类的操作。 - -**2.2 赤壁之战时序图** - -从虚线从上往下表示时间的推进。 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//80c5aff8-fc46-4810-aeaa-215b5c60a003.png) - -可见,通过时序图可以知道每个类具有以下操作: - -```java -publc class 刘备 { - public void 应战(); -} - -publc class 孔明 { - public void 拟定策略(); - public void 联合孙权(); - private void 借东风火攻(); -} - -public class 关羽 { - public void 防守荊州(); -} - -public class 张飞 { - public void 防守荆州前线(); -} - -public class 孙权 { - public void 领兵相助(); -} -``` - -**2.3 活动图、时序图之间的关系** - -活动图示从用户的角度来描述用例; - -时序图是从计算机的角度(对象间的交互)描述用例。 - -**2.4 类图与时序图的关系** - -类图描述系统的静态结构,时序图描述系统的动态行为。 - -**2.5 时序图的组成** - -① 对象 - -有三种表现形式 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//25b8adad-2ef6-4f30-9012-c306b4e49897.png) - -在画图时,应该遵循以下原则: - -1. 把交互频繁的对象尽可能地靠拢。 - -2. 把初始化整个交互活动的对象(有时是一个参与者)放置在最左边。 - -② 生命线 - -生命线从对象的创建开始到对象销毁时终止 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//b7b0eac6-e7ea-4fb6-8bfb-95fec6f235e2.png) - -③ 消息 - -对象之间的交互式通过发送消息来实现的。 - -消息有4种类型: - -1\. 简单消息,不区分同步异步。 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//a13b62da-0fa8-4224-a615-4cadacc08871.png) - -2\. 同步消息,发送消息之后需要暂停活动来等待回应。 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//33821037-dc40-4266-901c-e5b38e618426.png) - -3\. 异步消息,发送消息之后不需要等待。 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//dec6c6cc-1b5f-44ed-b8fd-464fcf849dac.png) - -4\. 返回消息,可选。 - -④ 激活 - -生命线上的方框表示激活状态,其它时间处于休眠状态。 - -![](https://github.com/CyC2018/InterviewNotes/blob/master/pics//6ab5de9b-1c1e-4118-b2c3-fb6c7ed7de6f.png) - - -# 参考资料 - -- Java 编程思想 - -- [面向对象设计的SOLID原则](http://www.cnblogs.com/shanyou/archive/2009/09/21/1570716.html) - -- [看懂UML类图和时序图](http://design-patterns.readthedocs.io/zh_CN/latest/read_uml.html#generalization) - -- [UML系列——时序图(顺序图)sequence diagram](http://www.cnblogs.com/wolf-sun/p/UML-Sequence-diagram.html) - -- [面向对象编程三大特性------封装、继承、多态](http://blog.csdn.net/jianyuerensheng/article/details/51602015)