0%

通俗的理解 Redis 是一个单进程单线程模型的 KV 内存数据库,截止到目前官方会在年底发布多线程版本,并且 Redis 也有自己的持久化方案。采用 I/O 复用模型和非阻塞 IO 技术,被广泛应用在高并发场景中。

Redis 高性能的几个关键点:

  • 完全基于内存操作,数据也是存储在内存中。
  • 数据结构简单,很多查找和操作的时间复杂度在 O(1)。
  • 单线程模式,避免了上下文的切换和锁竞争。
  • 使用了 I/O 多路复用模型和非阻塞 IO。

Redis 同时支持多个客户端连接,采用 I/O 多路复用模型(select\poll\epoll)可以同时监听多个 IO 流事件。

多路指的是多个网络连接,复用指的是复用同一个线程。
采用多路IO复用技术可以让单个线程高效的处理多个连接请求(尽量减少网络IO的时间消耗),且Redis在内存中操作数据的速度非常快(内存内的操作不会成为这里的性能瓶颈),主要以上两点造就了Redis具有很高的吞吐量。

TODO I/O 多路复用

参考

http://researchlab.github.io/2018/10/08/redis-11-redisio/

发现自己没玩过的事情好多,上周末便实施了一次露营⛺️之路,叫上小伙伴一起报了个户外团,第一次去选择的比较休闲级别的,然后自带了几十斤装备在山上扎营看天。

去之前连帐篷都没有打开过,不过好在很好弄,去到了迅速选择好营地很快就扎好了。然后在山顶四处转了转。第一次出来玩还是挺兴奋的,但没多久就冻得回帐篷换上了衣服,对比去的时候早上北京的天气超级闷热。

周围也有好多同行的人在扎营,还有两只🐶子互相溜着玩,和它们的主人一样的兴奋。

到了傍晚,就开始下起了大雾,一直持续到了第二天早上,后来想想从山脚下看来这应该是☁️吧,那就是身在云中了。在这么个野外环境下,找厕所也是很方了,哈哈哈。

下了这么一晚上的雾,有点遗憾的是没能看到星空🌃,而且没能选到一个防露水的帐篷晚上被滴醒好几次😶。。。

早上还是挺可以的,看到了日出🌅和云海,还有四处觅食的🐂。其实云海肉眼看上去不太明显,但是做成一个加速的短视频就很好看了。

露营前简单做的攻略

说明:

  1. 加粗为必备
  2. 标 A 的说明已准备好

装备:

帐篷A防潮垫A睡袋A、气垫(防咯)、头灯A、手电A、登山包A充电宝A、手机

雨衣A、雨鞋套A、登山鞋或运动鞋长袖防风衣物、羽绒服、头巾、棉帽

暖贴A、水杯、水5L A湿巾A纸巾A、洗漱用品、垃圾袋A、耳塞(风声吵)、眼罩

自发热小火锅A、八宝粥、面包、筷子、红牛、便携气炉

药物(快客、思密达+ 、藿香正气、创可贴、防虫喷雾、布洛芬)、风油精 AAAAA

银行卡一张A、工具刀A、备用手机A

这里说一些注意点

  1. 帐篷一定要四季防雨防露水的,赶上下雨就惨了
  2. 防潮垫一定要有,并且根据情况准备厚点的防咯
  3. 暖贴可以准备一些晚上睡觉冷贴身上
  4. 垃圾袋还是多带点将垃圾带走
  5. 工具刀准备一下防止意外保护安全
  6. 如果带气炉的话要注意地铁不让进
  7. 吃的尽量带一些高热量食物,自发热小火锅我觉得必备

最后附上一张小朋友拍的很棒的照片。

在一个集合中只有为数不多的整数时,Redis 使用 intset 整数集合存储数据。具有如下特性:

1
2
3
4
5
6
7
8
typedef struct intset {
//编码方式
uint32_t encoding;
//元素数目
uint32_t length;
//保存元素的数组
int8_t contents[];
} intset;
  1. 数据从小到大排序并且自动去重。
  2. 数据类型实际存储在 encoding 中。
  3. 当 encoding 中的数据类型不能满足时会自动进行类型升级。
    1. 重新分配空间
    2. 迁移
    3. 添加新元素
    4. 时间复杂度为 O(n)
  4. 不支持降级操作。

优点:

  1. 灵活,不用考虑整数集合类型,直接添加自动升级。
  2. 节省空间,只在必要时进行升级。

升级操作是指将整数由 16 位、32 位、64 位的方式增加支持范围。

基于 Ubuntu 18.04.2 系统做过尝试

安装

1
apt-get install maven

仓库概念

~/.m2 目录下面是存放安装包的内容缓冲.

settings.xml 。。。

不要使用 IDE 内嵌 Maven

基本命令

mvn compile 编译源码
mvn package 打包
mvn test 测试
mvn clean 清除代码
mvn deploy 上传到私服
mvn install 上传到本地仓库

mvn -X install 开启调试日志
mvn -U package 清除缓存打包
mvn cobertura:cobertura 单元测试覆盖率
mvn dependency:tree 查看依赖关系

Maven SNAPSHOT 版本和 RELEASE 版本的区别

https://www.cnblogs.com/EasonJim/p/6852840.html

https://www.jianshu.com/p/7e8e67205b97

Maven 依赖冲突

https://www.jianshu.com/p/b08364ce234f

跳表(Skip List)

怎样理解跳表

用一种比较通俗的方式去说,跳表是一种带有 N 级索引的有序链表,其中 N 级索引的作用是可以加速查找到链表的目标节点。

比较大众化的解释是,跳表是一个随机化的数据结构,实质就是一种可以进行『二分』查找的有序链表。这里对于随机化的理解是 N 级索引节点的选择上。

跳表不仅能提高搜索性能,同时也可以提高插入和删除操作的性能。在一定程度上可以代替红黑树(Red-black tree)。

跳表的结构

从链表说起,对于普通链表中,即使其存储的数据是有序的,当我们要寻找一个数据,时间复杂度也会比较高 O(n)。

然后我们对链表建立一级索引,每两个节点提取一个节点放到索引层,其中的 down 指针指向下一级。

当我们寻找节点 16,只需要走过 1' -> 4' -> 7' -> 9' -> 13' -> 13 -> 16 节点即可(17'也要访问判断),而原始链表需要走过 10 个节点,节省了 2 个节点路径。如果我们再抽出二级索引后是这样子的。

寻找节点 16 只需要走过 1'' -> 7'' -> 13'' -> 13' -> 13 -> 16 节点。这样我们又可以加快查找到目标节点,图中举例节点比较靠前,试想节点靠后,并且增加了 N 级索引之后效率一定会提升很多。

跳表的性能指标

单一链表的查询时间复杂度是 O(n),插入、删除的时间复杂度也是 O(n)。

以两个节点为跨度的话,那么跳表有如下总结:

  1. 第 k 级索引的节点个数是 n/(2^k)
  2. 假设有 h 级索引,最高级索引有 2 个节点,高度 h=log2n - 1 (2为底数),每一层都遍历 m 个节点,时间复杂度为 O(m*log n)。此时算得 m=3。

以多个节点为跨度,可以节省更多节点,是空间和时间上的相互折中。在实际开发中,索引节点只存储关键值和关键指针,之后链表节点才存储实际对象。

跳表的的查询、插入、更新、删除时间复杂度均为 O(log n)。

如何选择索引层?通过一个随机函数决定将节点插入到哪几级索引中,随机函数特点是要保证跳表索引大小和数据大小平衡性。

跳表在 Redis 中的应用

Redis 中有序集合通过散列表 + 跳表实现的,主要支持的功能有:

  • 插入一个数据;
  • 删除一个数据;
  • 查找一个数据;
  • 按照区间查找数据;
  • 迭代输出有序序列;

相比红黑树,跳表在区间查找上有更好的性能;并且实现起来也相对容易;可以通过调整索引策略来平衡性能和内存使用;红黑树插入删除时为了平衡高度需要旋转附近节点,高并发时需要锁,跳表不需要考虑。

关于源码分析:https://segmentfault.com/a/1190000013418471

参考

https://www.jianshu.com/p/dd01e8dc4d1f
https://blog.csdn.net/pcwl1206/article/details/83512600

15. 三数之和

题目

给定一个包含 n 个整数的数组 nums,判断 nums 中是否存在三个元素 a,b,c ,使得 a + b + c = 0 ?找出所有满足条件且不重复的三元组。

注意:答案中不可以包含重复的三元组。

例如, 给定数组 nums = [-1, 0, 1, 2, -1, -4],

满足要求的三元组集合为:
[
[-1, 0, 1],
[-1, -1, 2]
]

解题思路

初读这道题时忽略了一个点,就是在检查时,如果有两个重复的数字可以都用上组成一个三元组集合,但是在返回结果的时候不能第一个数字参与组成三元组后第二个相同数字继续组成三元组。

有三种思路,先进性排序处理,然后再进行处理。第一种就是做三层枚举;第二种就是两层枚举,在进行 set O(1) 查找;第三种是一层枚举,然后从剩余列表中的左右两边进行查找,满足三元组的添加记录。

下面是主要介绍第三种思路的细节

  1. 排序处理
  2. 从第 0 位置开始遍历
    1. 分别取剩余数组的首尾值进行求和
    2. 如果大于零则向前移动尾部游标
    3. 如果小于零则向后移动头部游标
    4. 如果等于零则添加记录
      1. 添加记录后对首尾游标向中间移动一格
    5. 如果首尾游标没有相交则继续 2.1 步骤处理
  3. 进行下一位置的遍历,直到数组尾部
  4. 返回结果

整个流程思路基本是这样子的,然后我们对于边界情况的处理单独进行描述

  1. 如果当前遍历位置值大于 0 则直接返回结果
  2. 对于我在 ① 中描述的情况,需要在 2.1 之前进行与判断,当前位置与上一位置值相同则跳过,进行排重处理
  3. 同样的情况处理在 4.1 之后也要进行首尾游标移动方向相邻值的排重处理

解题思维

  1. 首先需要做到的是充分理解题意,至少要做到能肉眼推导正确结果。
  2. 解决方案一般都会有多种,合理选择最优方案进行骨架设计 ②,充分考虑时间和空间复杂度。
  3. 然后解决边界条件 ③,优化代码可读性。
  4. 充分进行测试验证算法的正确性。

解题代码

最后放一下解题代码,也是参考别人的方案实现的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import java.util.*;

public class ThreeSum {

public static List<List<Integer>> threeSum(int[] nums) {
List<List<Integer>> resp = new ArrayList<>();
Arrays.sort(nums);
for (int i = 0; i < nums.length; i++) {
if (nums[i] > 0) break;
int l = i + 1;
int r = nums.length - 1;
if (i > 0 && nums[i-1] == nums[i]) continue;
while (l < r){
int sum = nums[i] + nums[l] + nums[r];
if ( sum> 0) r--;
else if (sum < 0) l++;
else if (sum == 0){
resp.add(Arrays.asList(nums[i], nums[l] , nums[r]));
while (l < r && nums[l] == nums[l+1]) l++;
while (l < r && nums[r] == nums[r-1]) r--;
l++;
r--;
}
}
}
return resp;
}
}

22. 括号生成

题目

给出 n 代表生成括号的对数,请你写出一个函数,使其能够生成所有可能的并且有效的括号组合。

例如,给出 n = 3,生成结果为:

1
2
3
4
5
6
7
8
9
10
11
12
[---
title: 51. N皇后
date: 2019-08-20
tags: [算法, leetcode]
id: 1
---
"((()))",
"(()())",
"(())()",
"()(())",
"()()()"
]

思路

根据递归做暴力处理,同时进行剪枝操作。

解答

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import java.util.ArrayList;
import java.util.List;

public class GenerateParenthesis {
public static void main(String[] args) {
GenerateParenthesis obj = new GenerateParenthesis();
List<String> stringList = obj.generateParenthesis(3);
System.out.println(stringList);
}

public List<String> generateParenthesis(int n) {
List<String> collect = new ArrayList<>();
gen(collect, "", n, n);
return collect;
}

public void gen(List<String> collect, String cur, int left, int right) {
if (left == 0 && right == 0) {
collect.add(cur);
return;
}
if (left > 0) {
gen(collect, cur + "(", left - 1, right);
}
if (right > 0 && right > left) {
gen(collect, cur + ")", left, right - 1);
}
}
}

49、242. 字母异位词

第一题:

242. 有效的字母异位词
给定两个字符串 s 和 t ,编写一个函数来判断 t 是否是 s 的字母异位词。

示例 1:

输入: s = “anagram”, t = “nagaram”
输出: true
示例 2:

输入: s = “rat”, t = “car”
输出: false

说明:
你可以假设字符串只包含小写字母。

解答第一个思路是使用 HashMap 进行字频统计再对比,第二个思路是字符串排序后进行比较。

第二题:

49. 字母异位词分组

给定一个字符串数组,将字母异位词组合在一起。字母异位词指字母相同,但排列不同的字符串。

示例:

输入: [“eat”, “tea”, “tan”, “ate”, “nat”, “bat”],
输出:
[
[“ate”,”eat”,”tea”],
[“nat”,”tan”],
[“bat”]
]
说明:

所有输入均为小写字母。
不考虑答案输出的顺序。

练习输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
import java.util.*;

public class Anagram {
public static void main(String[] args) {
System.out.println(isAnagram("anagram", "nagaram"));
System.out.println(isAnagram2("anagram", "nagaram"));
groupAnagrams(new String[]{"eat", "ate", "aaa"});
}

public static boolean isAnagram(String s, String t) {
if (null == s && null == t) {
return true;
} else if (null == s || null == t) {
return false;
} else if (s.length() != t.length()) {
return false;
}
Map<Character, Integer> left = new HashMap<>();
Map<Character, Integer> right = new HashMap<>();
for (int i = 0; i < s.length(); i++) {
left.put(s.charAt(i), left.getOrDefault(s.charAt(i), 0) + 1);
}
for (int j = 0; j < t.length(); j++) {
right.put(t.charAt(j), right.getOrDefault(t.charAt(j), 0) + 1);
}
for (Map.Entry<Character, Integer> c : left.entrySet()) {
if (!c.getValue().equals(right.getOrDefault(c.getKey(), 0))) {
return false;
}
}

return true;
}

public static boolean isAnagram2(String s, String t) {
if (null == s && null == t) {
return true;
} else if (null == s || null == t) {
return false;
} else if (s.length() != t.length()) {
return false;
}
char[] left = s.toCharArray();
char[] right = t.toCharArray();
Arrays.sort(left);
Arrays.sort(right);
for (int i = 0; i < left.length; i++) {
if (left[i] != right[i]) {
return false;
}
}
return true;
}

public static List<List<String>> groupAnagrams(String[] strs) {
Map<String, List<String>> res = new HashMap<>();
for (String sr : strs) {
char[] chars = sr.toCharArray();
Arrays.sort(chars);
String sortedSr = new String(chars);
if (!res.containsKey(sortedSr)) {
res.put(sortedSr, new ArrayList<>());
}
List<String> mapVal = res.get(sortedSr);
mapVal.add(sr);
}
List<List<String>> rep = new ArrayList<>();
for (Map.Entry<String, List<String>> entry : res.entrySet()) {
rep.add(entry.getValue());
}
return rep;
}
}

51. N皇后

题目

n 皇后问题研究的是如何将 n 个皇后放置在 n×n 的棋盘上,并且使皇后彼此之间不能相互攻击。

8_queens

上图为 8 皇后问题的一种解法。

给定一个整数 n,返回所有不同的 n 皇后问题的解决方案。

每一种解法包含一个明确的 n 皇后问题的棋子放置方案,该方案中 ‘Q’ 和 ‘.’ 分别代表了皇后和空位。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
输入: 4
输出: [
[".Q..", // 解法 1
"...Q",
"Q...",
"..Q."],

["..Q.", // 解法 2
"Q...",
"...Q",
".Q.."]
]
解释: 4 皇后问题存在两个不同的解法。

思路

解题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

public class SolveNQueens {
public static void main(String[] args) {
SolveNQueens queens = new SolveNQueens();
System.out.println(queens.solveNQueens(5));
}

public List<List<String>> solveNQueens(int n) {
List<List<String>> resp = new ArrayList<>();
Set<Integer> cols = new HashSet<>();
Set<Integer> pie = new HashSet<>();
Set<Integer> na = new HashSet<>();
dfs(n, 0, new ArrayList<>(), resp, cols, pie, na);
return resp;
}

public void dfs(int max, int row, List<String> curState, List<List<String>> resp,
Set<Integer> cols, Set<Integer> pie, Set<Integer> na) {
// 终结条件
if (row >= max) {
if (curState.size() == max) {
resp.add(curState);
}
return;
}
// 循环列
for (int col = 0; col < max; col++) {
if (cols.contains(col) || pie.contains(row + col) || na.contains(row - col)) {
continue;
}
cols.add(col);
pie.add(row + col);
na.add(row - col);
curState.add(trans(col, max));
int size = curState.size();
List<String> newState = (max - row == 1) ? new ArrayList<String>() {{
addAll(curState);
}} : curState;
// 递归层
dfs(max, row + 1, newState, resp, cols, pie, na);
cols.remove(col);
pie.remove(row + col);
na.remove(row - col);
curState.remove(size - 1);
}
}

public String trans(int point, int max) {
char[] chars = new char[max];
for (int i = 0; i < max; i++) {
chars[i] = i == point ? 'Q' : '.';
}
return String.valueOf(chars);
}
}

69. x 的平方根

实现 int sqrt(int x) 函数。

计算并返回 x 的平方根,其中 x 是非负整数。

由于返回类型是整数,结果只保留整数的部分,小数部分将被舍去。

示例 1:

1
2
输入: 4
输出: 2

示例 2:

1
2
3
4
输入: 8
输出: 2
说明: 8 的平方根是 2.82842...,
  由于返回类型是整数,小数部分将被舍去。

思路

二分查找比较

需要注意的地方有两个

  1. 注意开始边界问题
  2. 注意类型长度越界

解答

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public class MySqrt {
public static void main(String[] args) {
MySqrt mySqrt = new MySqrt();
System.out.println(mySqrt.mySqrt(2147395599));

}

// 边界问题
// 1. 0\1边界
// 类型长度越界
public int mySqrt(int x) {
if (x == 0) return 0;
if (x == 1) return 1;
return mySqrt(x, 0, x);
}

public int mySqrt(long x, long left, long right) {
long cur = (right - left) / 2 + left;
long cur2 = cur * cur;
if (cur2 == x) {
return (int) cur;
} else if (right - left == 1) {
return (int) left;
}

if (cur2 < x) {
left = cur;
} else if (cur2 > x) {
right = cur;
} else {
return (int) cur;
}
return mySqrt(x, left, right);
}
}

扩展

牛顿迭代法


98. 验证二叉搜索树

题目

给定一个二叉树,判断其是否是一个有效的二叉搜索树。

假设一个二叉搜索树具有如下特征:

节点的左子树只包含小于当前节点的数。
节点的右子树只包含大于当前节点的数。
所有左子树和右子树自身必须也是二叉搜索树。
示例 1:

输入:

1
2
3
  2
/ \
1 3

输出: true
示例 2:

输入:

1
2
3
4
5
  5
/ \
1 4
  / \
  3 6

输出: false
解释: 输入为: [5,1,4,null,null,3,6]。
根节点的值为 5 ,但是其右子节点值为 4 。

思路

首先拿到这个题看起来思路比较简单,实现起来还有有点困难,而且在思考过程中踩过一个坑,又爬上来的。哎,看题还是要全面点。

  1. 首先想到就是中序遍历了,放到一个列表中,然后比较大小即可。
  2. 或者是做一个递归操作,判断当前节点是否在一个范围即可。

思路是这么两个思路,代码实现起来为了执行效率做了短路处理,就是边遍历边检查,遇到错误就一路返回不再进行后面处理,当然这是思路理顺后的优化。

中序遍历没坑,直接写就行了,有坑的是第二种操作,刚开始觉得只要比较当前节点的父节点和两个子节点就好了,就像下面画的,已 C 点为当前节点进行处理,实际是一个错误的思路,并且情况也分析的不对。

意识到问题后就重新分析,把当前节点作为最底端的节点,我们去比较的都是当前节点和父辈及以上的节点的大小,也就是拆出来四种情况。

其中 表示当前节点,和其它点的相对位置表示左右子节点关系,min -> max 指向当前节点值必须在此区间中才可以,+∞-∞ 为单点情况的边界表示。最后拆分出这四种子情况,只要任一节点符合这四种情况之一即当前节点满足,当所有节点均满足则二叉搜索🌲有效,事实根据这个思路写出的代码验证是可行的。比较开心的是重新思考后的思路写出来的代码一次通过✌️。

代码

解法1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
class Solution {
public boolean isValidBST(TreeNode root){
return forEachNode(root, new ArrayList<>());
}

public boolean forEachNode(TreeNode node, List<Integer> val){
if (null == node) {
return true;
}
if (!forEachNode(node.left, val)){
return false;
}
if (!validOrAdd(val, node)){
return false;
}
if (!forEachNode(node.right, val)){
return false;
}
return true;
}
public boolean validOrAdd(List<Integer> val, TreeNode node){
if(val.size() > 0 && val.get(val.size() - 1) >= node.val){
return false;
}else{
return val.add(node.val);
}
}
}

解法2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
class Solution {
public boolean isValidBST(TreeNode root){
return subValidBSTLeft(root, null, null);
}

public boolean subValidBSTLeft(TreeNode node, Integer min, Integer max) {
if (null == node){
return true;
}
if (null == min && null == max){
}else if (null == min && null !=max && node.val < max){
}else if (null != min && null == max && min < node.val){
}else if (null != min && null != max && min < node.val && node.val < max){
}else {
return false;
}
// left
if (!subValidBSTLeft(node.left, min, node.val)){
return false;
}
// right
if (!subValidBSTLeft(node.right, node.val, max)){
return false;
}
return true;
}
}

102. 二叉树的层次遍历

问题

给定一个二叉树,返回其按层次遍历的节点值。 (即逐层地,从左到右访问所有节点)。

例如:
给定二叉树: [3,9,20,null,null,15,7],

1
2
3
4
5
  3
/ \
9 20
/ \
15 7

返回其层次遍历结果:

1
2
3
4
5
[
[3],
[9,20],
[15,7]
]

解答

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Solution {
public List<List<Integer>> levelOrder(TreeNode root) {
List<List<Integer>> resp = new ArrayList<>();
levelOrder(root, 0, resp);
return resp;
}
public void levelOrder(TreeNode cur, int cur_level, List<List<Integer>> resp) {
if(null == cur){
return;
}
if(cur_level == resp.size()){
resp.add(new ArrayList<>());
}
resp.get(cur_level).add(cur.val);
levelOrder(cur.left, cur_level+1, resp);
levelOrder(cur.right, cur_level+1, resp);
}
}

一次过


104. 二叉树的最大深度

问题

给定一个二叉树,找出其最大深度。

二叉树的深度为根节点到最远叶子节点的最长路径上的节点数。

说明: 叶子节点是指没有子节点的节点。

示例:
给定二叉树 [3,9,20,null,null,15,7],

1
2
3
4
5
  3
/ \
9 20
/ \
15 7

返回它的最大深度 3 。

解答

1
2
3
4
5
6
7
8
9
10
11
class Solution {
public int maxDepth(TreeNode root) {
return maxDepth(root, 0);
}
public int maxDepth(TreeNode cur, int level) {
if(null == cur) return level;
int left_level = maxDepth(cur.left, level + 1);
int right_level = maxDepth(cur.right, level + 1);
return left_level > right_level ? left_level: right_level;
}
}

一次过


111. 二叉树的最小深度

题目

给定一个二叉树,找出其最小深度。

最小深度是从根节点到最近叶子节点的最短路径上的节点数量。

说明: 叶子节点是指没有子节点的节点。

示例:

给定二叉树 [3,9,20,null,null,15,7],

1
2
3
4
5
  3
/ \
9 20
/ \
15 7

返回它的最小深度 2.

解答

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Solution {
public int minDepth(TreeNode root) {
return minDepth(root, 0);
}

public int minDepth(TreeNode cur, int level) {
if (null == cur) return level;
if (null == cur.left && null == cur.right) {
return level + 1;
} else if (null != cur.left && null == cur.right) {
return minDepth(cur.left, level + 1);
} else if (null == cur.left && null != cur.right) {
return minDepth(cur.right, level + 1);
} else {
int left_level = minDepth(cur.left, level + 1);
int right_level = minDepth(cur.right, level + 1);
return left_level > right_level ? right_level : left_level;
}
}
}

141. 环形链表

给定一个链表,判断链表中是否有环。

为了表示给定链表中的环,我们使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。 如果 pos 是 -1,则在该链表中没有环。

正常的解题思路,通过记录走过的节点来判断是否有环。

时间复杂度:O(n),对于含有 n 个元素的链表,我们访问每个元素最多一次。添加一个结点到哈希表中只需要花费 O(1) 的时间。

空间复杂度:O(n),空间取决于添加到哈希表中的元素数目,最多可以添加 n 个元素。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/**
* Definition for singly-linked list.
* class ListNode {
* int val;
* ListNode next;
* ListNode(int x) {
* val = x;
* next = null;
* }
* }
*/
public class Solution {
public boolean hasCycle(ListNode head) {
HashSet<Integer> points = new HashSet<Integer>();
ListNode cur = head;
while (null != cur){
int curHashCode = cur.hashCode();
if(points.contains(curHashCode)){
return true;
}
points.add(curHashCode);
cur = cur.next;
}
return false;
}
}

第二种解题思路是双指针大小步,一个指针每次走一步,另一个指针每次走两步,如果有环的话则两个指针最终会相遇。

在最糟糕的情形下,时间复杂度为 O(N+K),也就是 O(n)。

空间复杂度:O(1),我们只使用了慢指针和快指针两个结点,所以空间复杂度为 O(1)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
/**
* Definition for singly-linked list.
* class ListNode {
* int val;
* ListNode next;
* ListNode(int x) {
* val = x;
* next = null;
* }
* }
*/
public class Solution {
public boolean hasCycle(ListNode head) {
if (null == head){
return false;
}else if (null == head.next){
return false;
}else if (head == head.next.next){
return true;
}
ListNode minCur = head.next;
ListNode maxCur = head.next.next;
while (minCur != maxCur){
if (null == minCur.next){
return false;
}else if (null == maxCur.next){
return false;
}else if (null == maxCur.next.next){
return false;
}
minCur = minCur.next;
maxCur = maxCur.next.next;
if (minCur == maxCur){
return true;
}
}
return false;
}
}

146. LRU缓存

运用你所掌握的数据结构,设计和实现一个 LRU (最近最少使用) 缓存机制。它应该支持以下操作: 获取数据 get 和 写入数据 put 。

获取数据 get(key) - 如果密钥 (key) 存在于缓存中,则获取密钥的值(总是正数),否则返回 -1。
写入数据 put(key, value) - 如果密钥不存在,则写入其数据值。当缓存容量达到上限时,它应该在写入新数据之前删除最近最少使用的数据值,从而为新的数据值留出空间。

链接:https://leetcode-cn.com/problems/lru-cache

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
from collections import OrderedDict

class LRUCache(object):

def __init__(self, capacity):
"""
:type capacity: int
"""
self._cache = OrderedDict()
self._size = capacity

def get(self, key):
"""
:type key: int
:rtype: int
"""
if key not in self._cache:
return -1
val = self._cache.pop(key)
self._cache[key] = val
return val

def put(self, key, value):
"""
:type key: int
:type value: int
:rtype: None
"""
if key in self._cache:
self._cache.pop(key)
self._cache[key] = value
else:
if len(self._cache) == self._size:
self._cache.popitem(last=False)
self._cache[key] = value


# Your LRUCache object will be instantiated and called as such:
# obj = LRUCache(capacity)
# param_1 = obj.get(key)
# obj.put(key,value)

有序字典的解法
时间复杂度 O(1)
空间复杂度 O(capacity)

Java 解法需要 LinkedHashMap TODO

LRU(Least Recently Used)最少最近使用,一种页面置换算法。

LFU(Least Frequently Used)最近最不常用。

其它

LRU


206. 反转链表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
public class ReverseLinkedList {
public static void main(String[] args) {
ListNode listNode = ListNode.of(
1,
ListNode.of(
2,
ListNode.of(
3,
ListNode.of(
4,
ListNode.of(
5, null
)
)
)
)
);
ListNode next = listNode;
while (null != next) {
System.out.println(next.val);
next = next.next;
}

System.out.println("===");
ListNode reverse = reverse3(listNode);

ListNode next2 = reverse;
while (null != next2) {
System.out.println(next2.val);
next2 = next2.next;
}
}

/**
* 官方推荐的
* @param head
* @return
*/
public static ListNode reverse2(ListNode head) {
// prev -> curr -> nextTemp
ListNode prev = null;
ListNode curr = head;
while (curr != null) {
ListNode nextTemp = curr.next;
curr.next = prev;
prev = curr;
curr = nextTemp;
}
return prev;
}

/**
* 自己写的
* @param head
* @return
*/
public static ListNode reverse(ListNode head) {
// cur -> prev -> tmp
ListNode cur = head;
ListNode next = head.next;
head.next = null;
while (null != cur) {
ListNode tmp = null;
if (null != next) {
tmp = next.next;
next.next = cur;
}
if (null == next) {
break;
}
cur = next;
next = tmp;
}
return cur;
}

/**
* 复写的
* 1. 记录前一个节点,当前节点
* 2. 迭代
* 3. 取出当前节点到临时变量
* 4. -- 现在有 prev -> curr -> next 三个节点
* 5. 将 curr.next -> prev,改变指向方向
* 6. 依次挪动节点位置 prev = curr , curr = nextTemp
* 7. 最后返回 prev
*
* 时间复杂度 O(n)
* 空间复杂度 O(1)
* @param head
* @return
*/
public static ListNode reverse3(ListNode head) {
// prev -> curr -> next
ListNode prev = null;
ListNode curr = head;
while (null != curr) {
ListNode next = curr.next;
curr.next = prev;
prev = curr;
curr = next;
}
return prev;
}
}

208. 实现 Trie (前缀树)

题目

实现一个 Trie (前缀树),包含 insert, search, 和 startsWith 这三个操作。

示例:

Trie trie = new Trie();

trie.insert(“apple”);
trie.search(“apple”); // 返回 true
trie.search(“app”); // 返回 false
trie.startsWith(“app”); // 返回 true
trie.insert(“app”);
trie.search(“app”); // 返回 true
说明:

你可以假设所有的输入都是由小写字母 a-z 构成的。
保证所有输入均为非空字符串。

分析

前缀树
Trie
字典树

解答

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
public class Trie {
private final int SIZE = 26;
private Node root;

private class Node {
private boolean isStr;
private int num;
private Node[] child;

public Node() {
child = new Node[SIZE];
isStr = false;
num = 1;
}
}

public Trie() {
root = new Node();
}

public void insert(String word) {
if (null == word || word.isEmpty()) {
return;
}
Node pNode = this.root;
for (int i = 0; i < word.length(); i++) {
int index = word.charAt(i) - 'a';
if (pNode.child[index] == null) {
Node tmp = new Node();
pNode.child[index] = tmp;
} else {
pNode.child[index].num++;
}
pNode = pNode.child[index];
}
pNode.isStr = true;
}

public boolean search(String word) {
if (null == word || word.isEmpty()) {
return false;
}
Node pNode = this.root;
for (int i = 0; i < word.length(); i++) {
int index = word.charAt(i) - 'a';
if (pNode.child[index] == null || (word.length() - i == 1 && pNode.child[index].isStr == false)) {
return false;
}
pNode = pNode.child[index];
}
return true;
}

public boolean startsWith(String prefix) {
if (null == prefix || prefix.isEmpty()) {
return false;
}
Node pNode = this.root;
for (int i = 0; i < prefix.length(); i++) {
int index = prefix.charAt(i) - 'a';
if (pNode.child[index] == null) {
return false;
}
pNode = pNode.child[index];
}
return true;
}
}

703. 数据流中的第K大元素

设计一个找到数据流中第K大元素的类(class)。注意是排序后的第K大元素,不是第K个不同的元素。

你的 KthLargest 类需要一个同时接收整数 k 和整数数组nums 的构造器,它包含数据流中的初始元素。每次调用 KthLargest.add,返回当前数据流中第K大的元素。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import java.util.PriorityQueue;

public class KthLargest {
private PriorityQueue<Integer> minHeap;
private int kSize;

public KthLargest(int k, int[] nums) {
kSize = k;
minHeap=new PriorityQueue<Integer>(kSize);
for (int i = 0; i< nums.length; i++){
add(nums[i]);
}
}

public int add(int val) {
if (minHeap.size() < kSize){
minHeap.offer(val);
}else if (minHeap.peek() < val){
minHeap.poll();
minHeap.offer(val);
}
return minHeap.peek();
}
}

Java中PriorityQueue通过二叉小顶堆实现,可以用一棵完全二叉树表示。

关于堆操作:https://shmilyaw-hotmail-com.iteye.com/blog/1775868

操作说明:

方法名功能描述
add(Ee)在队列头部增加一个元素,如果容量已满,则抛出异常,成功则返回true。
clear()清空
contains(Objecto)检查是否包含当前参数元素
offer(Ee)在队列头部增加一个元素,如果容量已满,则返回false,成功加入,返回true。
peek()返回队列头部节点,但不移除队列头节点。
poll()将队列头部元素移出队列并返回。
remove(Objecto)将队列头部元素移出队列并返回,如果队列为空,则抛出异常。
size()返回长度

在这几年的微服务开发过程中遇到过两次因为网络问题导致的系统故障,并且没有做好降级策略,导致系统的不可用时间增加,所以今天专门整理一篇关于网络故障的问题分析处理以及开发中需要注意的地方。

基础部分

TCP 连接,先抛大图:

a

主要分为三部分:

  1. 建立连接
  2. 传输数据
  3. 关闭连接

原理不做过多介绍,主要说说常见的异常和模拟方式。

常见的异常类型

b
上面的异常是一些常见的功能性异常,其它性能方面的异常不在本文讨论范围。

实施手段

需要的工具

  • python 脚本
  • iptables,对网络流量进行规则过滤
  • tcpkill,用来断开网络构造异常
  • curl,发起 http 访问请求

Python脚本

主要作用是启动一个TCP监听,然后将接收到的数据在转发回去。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#! /usr/bin/python
# -*- coding:utf-8 -*-
import socket
import sys
def start_tcp_server(ip, port):
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_address = (ip, port)
print('Starting listen on ip %s, port %s' % server_address)
sock.bind(server_address)
try:
sock.listen(1)
except socket.error as exc:
print('Fail to listen on port %s' % exc)
return
while True:
print("Waiting for connection")
client, addr = sock.accept()
data = client.recv(1000)
client.send(data)
client.close()
print(data)
if __name__ == '__main__':
start_tcp_server('0.0.0.0', 12345)

iptables 基本使用

1
2
3
4
5
// 查看当前生效规则
iptables -L -n
// 清空所有规则
iptables --flush
iptables -F

tcpkill 基本使用

https://yq.aliyun.com/articles/59308

1
2
3
4
// 安装
sudo apt-get install dsniff
// 使用
...

curl 超时设置

使用 curl 有两个超时时间,一个是连接超时时间,另一个是数据传输的最大允许时间。
连接超时时间用 --connect-timeout 参数来指定,数据传输的最大允许时间用 -m 参数来指定。

1
curl --connect-timeout 10 -m 20 "http://192.168.1.110:12345"

实施过程

  1. A机器启动Python脚本,监听12345端口。
  2. B级器通过curl命令进行访问。
  3. 在访问过程中通过配置iptables来实现网络的各种异常情况。
  4. 通过 tcpkill 来实现连接中断的异常情况。

正常访问

1
2
3
4
5
xyz@xyz-pc:~$ curl "http://192.168.1.110:12345"
GET / HTTP/1.1
Host: 192.168.1.110:12345
User-Agent: curl/7.58.0
Accept: */*

查看和清除规则

1
2
3
4
5
6
7
8
9
10
11
12
13
xyz@xyz-pc:~$ sudo iptables -L -n
[sudo] xyz 的密码:
Chain INPUT (policy ACCEPT)
target prot opt source destination

Chain FORWARD (policy ACCEPT)
target prot opt source destination

Chain OUTPUT (policy ACCEPT)
target prot opt source destination
DROP tcp -- 0.0.0.0/0 0.0.0.0/0 tcp dpt:12345 flags:0x17/0x02

xyz@xyz-pc:~$ sudo iptables -F

连接超时

1
2
3
xyz@xyz-pc:~$ sudo iptables -A OUTPUT -p tcp --syn --dport 12345 -j DROP
xyz@xyz-pc:~$ curl --connect-timeout 10 "http://192.168.1.110:12345"
curl: (28) Connection timed out after 10001 milliseconds

读取数据超时

1
2
3
xyz@xyz-pc:~$ sudo iptables -A OUTPUT -p tcp -m state --state ESTABLISHED --dport 12345 -j DROP
xyz@xyz-pc:~$ curl --connect-timeout 10 -m 20 "http://192.168.1.110:12345"
curl: (28) Operation timed out after 20001 milliseconds with 0 bytes received

拒绝连接

1
2
3
xyz@xyz-pc:~$ sudo iptables -A OUTPUT -p tcp --dport 12345 -j REJECT
xyz@xyz-pc:~$ curl --connect-timeout 10 -m 20 "http://192.168.1.110:12345"
curl: (7) Failed to connect to 192.168.1.110 port 12345: 拒绝连接

连接被重置

这里需要将Python脚本的 client.close() 注释掉。

1
2
3
4
5
6
7
xyz@xyz-pc:~$ sudo tcpkill -iwlp8s0 port 12345
xyz@xyz-pc:~$ curl --connect-timeout 10 "http://192.168.1.110:12345"
GET / HTTP/1.1
Host: 192.168.1.110:12345
User-Agent: curl/7.58.0
Accept: */*
curl: (56) Recv failure: 连接被对方重设

总结

在越来越多的企业微服务化进程中,肯定会遇到网络请求的各种问题,当我们在做一个基础组件或者进行网络通信请求时需要考虑到这些异常情况,最好还是将各种常见的情况模拟实施一下,来保证服务的稳定性。首先要说的是请求的超时设置,不论是在进行 HTTP 访问还是封装后的 RPC 请求,超时设置是最基本的。
基于不同语言的不同组件实现质量来说。曾经遇到过一个问题是,一个服务处于假死状态,Java 的客户端中默认超时和多线程可以使主线程服务不会受到过多影响,golang 中的客户端默认设置了一个很长的超时时间,服务在一定程度上受到了影响,而Python的客户端超时时间也是很长,还有就是Python只有一个主线程再跑,所以此时服务会被 hang 住了。
所以这里还有一个问题就是服务降级,当前服务如果出现问题,重试几次后仍然失败,那么是否降级来保证当前服务的可用性,其实考虑的是异常服务对于当下的重要性,是否在整个核心服务链路当中,否则的话进行降级处理。
还有一个关键点是慎用重试,偶然的网络波动导致的异常在重试下会很有效,但是当遇到服务性能导致的超时问题时,就遇到大量的客户端重试导致请求翻倍,很可能会直接把服务打挂,所以不要轻易使用重试,可以通过一些额外的补偿机制来提高服务稳定性。

未防止爬虫盗爬我的文章,在末尾打个广告。这篇文章首发在我的个人博客 知一的指纹 http://noogel.xyz ,有需要技术交流的朋友可以加我个人微信:zhi2012666

参考

https://blog.csdn.net/llianlianpay/article/details/79768890
https://www.cnblogs.com/gl1573/p/10129382.html
https://blog.csdn.net/wangyiyungw/article/details/85004905

从北京出发一路向北有个坝上草原,趁着七月天热的时候去避避暑,看惯了城市的生活,周末偶尔出去放松一下也是好的。便约了几个同事一起自驾过去玩的。

去的前一天预报说下雨☔️,结果当天也只是一路阴天☁️,等到了坝上草原也没有下,恰巧那天晚上北京下的,完美避过。下面这张是在去的时候怀柔拍的,这边的山⛰相对高耸一点,夏天还有点绿色,冬天就真的是凸凸的。

等到了坝上草原这边,真的是天高云阔空气清新。而且也有点冷🥶,哈哈哈也就二十度左右,所以带件外套真的是很有必要的。

在丰宁,一路上都是这样子的。

整个旅途并没有排的很紧凑,而是自驾的舒适随意。导航到了一个草原的入口,然后爬上山坡一路向草原深处走去。顺着一条小路走过一个又一个山丘,草原的广阔无垠尽收眼底。同时,七月的日子里草原也是超级冷的,小风飕飕的,所以准备穿着冲锋衣来是最明智的选择了。

在草原,远远望去会看到一个个的村庄,彼此离得很远感觉出村的路就那么一条,不过生活在这样的地方应该会很惬意。

要说印象最深的还是说草原的夕阳🌇了吧,在城市里因为高楼的遮挡可能早早的就见不到太阳了。而草原的夕阳可以在很晚,直到太阳落到地平线以下。拍下面这张图的时间是在傍晚 7 点半。草原的能见度真的很高,远处的山⛰感觉要几十公里外了。

来这样的环境下偶尔放松放松,心情真的也会很好了。还有一张像极了 Windows 经典的桌面背景,蓝天白云绿草地。

下面👇是去之前简单做的攻略:

要带的东西:

防晒霜、太阳镜、帽子、雨伞

防蚊虫叮咬的、创可贴、风油精、快客

野餐垫、食物、水果、红牛

身份证!!!!!

手机、充电宝、

纸质现金

昼夜温差大(28 - 15度)带一件春秋外套

穿户外登山、运动鞋!!!

摄影装备(选填)

玩点:

看日出

柳树沟(50元)

情人谷(免费)

草原娱乐场(滑草??、射箭、CS、骑马)

烤串

草原天路

路线图:
(需要的可以找我要~)

首先要说的是重构最基本的定义:重构是在不敢编软件可观察行为的前提下改善其内部结构。

每一个开发人员肯定都经历过『坏代码』的味道。在一个古老又庞大的项目中,这里面一些函数的作用和逻辑变的很难理解,没有人了解这里的所有 case,加上没有足够的注释,之前开发的人员离职等诸多因素,可维护性非常低,谁都不愿意碰,这时候再改动一个需求,会很容易引入一些 bug。当你遇到上面的这些情况时那么时候要把这摊『臭水坑』清理一番了。

我们知道要做重构这件事了,那么『工欲善其事必先利其器』,重构也是有诸多手段的,有许多被前人验证过的重构手法来帮助我们改善项目代码的健康状况。接下来讲讲一些小的也是简单实现的重构方式。

flight over Barcelona ...

小重构

重复的代码

重复代码的抽象有几种方式,一种是将重复的代码或者相似的代码,可以提取到一个扩展函数中,然后在多个地方调用;或者将多个相似类中的相同代码抽象到父类中,子类调用,但是按照组合优于继承的设计方式,不建议这样做;再有是对相似流程代码抽象出模板方法,子类实现差异化逻辑。

过长的函数

在计算机领域有这样一句名言:『计算机科学相信所有问题都可以通过添加一个间接层来解决』。如果我们没有良好的系统设计经验和深刻理解面向对象思想(业务系统主流的编程思想),就很容按照过程式的思想去写代码,就会出现职责庞大的函数或类,有着超多的分支判断逻辑,各种补丁代码块。这里一部分是系统设计的问题,另一方面没有很好的拆分职责。一个很好的办法就是将分支中的代码块抽离成小函数,把大类拆分成职责较为单一的小类。再有让小函数容易理解的关键是一个好名字(关于起名字这块可以单独说说);再有大函数中的临时变量可能阻碍你的拆分,可以把这些临时变量通过查询的方式获取,既提高了可读性又能共享给其它地方用。

过大的类

过大的类就像过长的函数,冗杂且难以理解。我们通俗的说这个类的职责太重了,导致里面又很多的实例变量。改造的办法是将多个实例变量分组,然后拆分不同的类去处理,这样来拆分出一些单一职责类。再有就是可以确定类的使用方式,提炼出来接口帮助理解这个类。

过长的参数列表

过长的参数列表可能是这样产生,最初定义接口只有两个参数,那么随着业务扩展,这个函数产生的职责越来越大,随之参数越加越多。这种的解决方案是搞一个参数对象,将原先的参数都保存到参数对象实例中,然后传递这个实例到函数中处理。

Pelee

然后呢

我把这写最基本的风险小的方式叫做小重构,可以让我们的代码变得稍微好一些。其实你在做小重构的过程中可以慢慢形成对于这个系统业务流程的理解,以及对于系统设计(大)重构方向上的思路。那么什么时候或者什么时机就要开始重构?

如果让我接需求改系统一个部分的代码,做完如果再次需求改动不是很容易改的时候,基于事不过三的原则,我会在需求中做一些重构来弥补设计上的缺陷;再有就是修复 bug 的时候,如果不是很好修复,我也是要先进行适当的重构的再去解决的;或者我们集中进行 codereview 的时候提出来需要进行的重构时。

一个好的项目是需要有一个好的设计基础,因为我们不能只想着今天做什么,还要想明天可能会做什么,只做好今天,而明天到来发现无法做到,那么也是失败的,想的多了就会出现过度设计,也是包袱。所以写好代码是一件挺难的事情,写之前多思考一下。今天先写这么多~

首先这篇文章是建立在有一些编程基础之上来展开的,做为一种效率学习编程语言的自我总结输出。把编程语言当做一个工具,而这些不同种类的工具有很多的共通之处,抓住其中的关键之处可以大大提升学习效率,也是一篇自我总结的学习方法论,里面有的方法可能不适合我,但也会讲讲。如果要学习一门编程语言,先要问一下为什么要学?学会了能做什么?要达到什么样的目标?只有把这些问题想清楚了再去做,不然稀里糊涂不知所以,很可能半途而废。想明白了就要坚持去做,不要再东望望西看看,想、做、坚持三位一体是学到的基本三要素。

学习途径分析

网上的学习资料纷繁复杂,各种培训课程满网都有,那么大致分为以下几种吧:

总结来说学习路径是这样的。

  • 入门:适合通过看视频和培训来实现,然后通过搜索引擎和博客文章论坛协助解决遇到的各种问题。
  • 提高:通过看书和大佬的博客文章交流论坛等来加深理解。
  • 实践:实践也是提高的一种方式,可以通过学习别人项目来仿写实践。

适合学习或交流的地方,Stack Overflow、博客园、简书、GitHub、官方论坛等等。关于搜索引擎,入门用百度就可以了,提高和实践建议用谷歌。

指定计划

这个也挺关键的,我就没有做好,也不太好定。

查问题的办法

学习过程中很关键的一点就是遇到问题如何解决问题,解决问题的速度和方法很大程度上决定我们后期学习的进度和自信心,那么我总结了几条比较关键的要素说明。

学会如何看异常信息

不论写 demo 还是实际项目开发中,肯定会遇到一堆异常情况,然后控制台打印一堆杂乱信息,首先要做的是理清楚其中的信息结构,其实就是日志啦,大致分为以下几种:

  • DEBUG:一般在开发调试期间开启,可以比较清楚的了解程序在运行过程中的各种状态和传参等。
  • INFO:正常运行日志信息,当程序访问量很大的情况下很多,会收集到日志系统。
  • WARN:对于程序可能出现的潜在问题的地方记录信息,或者不再支持的方法或库等。
  • ERROR:对于程序运行中的非预期异常访问、状态、请求需要记录错误信息和错误状态。
  • TRACE:这个在不同语言表的可能不同,主要是概括为带有调用堆栈信息的异常日志。

那么在测试过程中为了快速定位问题,还是要打印 TRACE 级别的异常日志,那么异常信息如何看呢?

Python 的异常信息示例

1
2
3
4
5
6
7
8
9
10
11
12
13
xyz-macdeMacBook-Pro:dev-demo xyz$ python mixin_demo.py 
Init people.
People can eat!
People can drink!
People can
Traceback (most recent call last):
File "mixin_demo.py", line 44, in <module>
people = People()
File "mixin_demo.py", line 39, in __init__
print "People can ", self.sleep()
File "mixin_demo.py", line 30, in sleep
raise NotImplementedError(u"can't sleep.")
NotImplementedError: can't sleep.

在执行 Python 脚本的时候执行异常报错,Traceback 是我们要看的异常堆栈信息,自顶向下从 "mixin_demo.py 的 44 行开始执行,执行到 mixin_demo.py 第 30 行报错,异常类型 NotImplementedError,异常内容为 can't sleep.。也就说我们看到报错信息后,然后可以看到对应报错的位置以及调用链,方便快速定位到问题。

Java 的异常结构展示顺序有点不同,它的信息最底部是调用入口,然后向上一直到报错点,还有报错信息也在上面。这里只是一个简单的示例,通常 Java 的异常堆栈信息很长,调用链很深,这就需要有一定的经验来判断问题实际产生的原因。

1
2
3
4
5
6
7
8
9
10
objc[20307]: Class JavaLaunchHelper is implemented in both /Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/bin/java (0x1077344c0) and /Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/jre/lib/libinstrument.dylib (0x10874d4e0). One of the two will be used. Which one is undefined.
Exception in thread "main" org.springframework.beans.factory.NoSuchBeanDefinitionException: No bean named 'teacher2' available
at org.springframework.beans.factory.support.DefaultListableBeanFactory.getBeanDefinition(DefaultListableBeanFactory.java:775)
at org.springframework.beans.factory.support.AbstractBeanFactory.getMergedLocalBeanDefinition(AbstractBeanFactory.java:1221)
at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:294)
at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:199)
at org.springframework.beans.factory.support.CglibSubclassingInstantiationStrategy$LookupOverrideMethodInterceptor.intercept(CglibSubclassingInstantiationStrategy.java:290)
at com.noogel.xyz.lookup.GetBeanTest$$EnhancerBySpringCGLIB$$e79f57a6.getBean(<generated>)
at com.noogel.xyz.lookup.GetBeanTest.showMe(GetBeanTest.java:5)
at LookupTestMain.main(LookupTestMain.java:9)

使用IDE断点调试

IDE 可以帮我们做很多事,大大提高了我们的开发效率,那么如何用好这样一个工具也是很关键的,这里说一下利用 IDE 进行 debug 调试。

通过上图我们可以看出 IDE 的调试功能很丰富,如果用好对于提高学习编程效率很有帮助。

查问题打日志

在 Python 中有一个查问题很好的利器 iPython,可以很方便的直接传参执行函数,对于线上问题,这样虽然很方便但是不可取。像 Java8 这样版本下的语言又没有很好用的一个工具,那么可以通过打日志的方式来操作,需要对可能的问题点分析然后增加日志辅助判断,打日志是一个很合适的查线上问题的方式。

向有经验的人请教

老师傅一出手就知有没有,如果遇到问题没有思路或者自己憋住了,需要及时向有经验的人请教,才是一个正确的选择。

了解框架的运行机制

俗话说『打铁还需自身硬』,遇到了问题总不能一直向别人请教吧,还是要自己去搞定的,上面的介绍的方法搞不定怎么办,那就要平时花心思在使用的框架上学习了,因为你不可能直接裸写代码的,一些轮子直接搬过来用固然高效,但是不懂构造轮子还是用不好的,容易出问题,所以要平时多去积累了。

以上讲的都是一些可以提高学习效率的方法。先写这些,后续想到新的再继续更新。

实践一个项目

一般视频和图书的最后一部分都会有初级的实践项目,可以用来综合练习一下所学到的知识。可以看完再自己提需求自己实现一遍。

再有就是 GitHub 上有很多,不过要好好找找合适,可以拿来练手,最好是找一些多 star 的项目,对于技术提升很有帮助,发现问题还可以提 issue 交流,然后提 PR 解决。

另外一个就是某某公司意外泄漏或者开源的项目源码,这些都是经历生产环境反复验证的代码,具有很高的参考性。

举个例子就是在 Java 中 spring MVC 是一个很著名的项目,那么学习它,有的人就手敲 spring 项目,边学习边实现其中关键的代码。GitHub 地址:https://github.com/code4craft/tiny-spring

了解语言的技术栈

学习一门编程语言肯定是用来解决实际问题或找一份工作的,那么你要知道并不仅仅是学习这门编程语言,而是整个技术栈。了解一个语言的技术栈可以去招聘网站上看,一般都会写至少需要精通一门编程语言,熟练使用 MySQL 解决并优化问题,熟练使用并了解各种 MQ 原理等等。那么这些都是需要去了解的,起初学习可以为了用而用的去实践一个更完整的需求。

比如说我需要了解公司的微服务架构,那么我需要看它都用了哪些技术栈,然后自己再手敲一些项目,并且 docker 化,自动注册服务发现来管理多个 RPC 服务等等。。。

总之,一个是多看多实践多思考,然后就是多交流,闭门造车是不行的。

了解语言的运行机制

语言的内存模型?
语言的并发模型?
语言的垃圾回收机制?