文章目录
- 1. 需要判断输入的两个参数的大小/长度
- 2. 数学分式的化简
- 3. 二叉树操作的小总结
- 4. MySQL分组内取前几名的问题
- 5. SQL中的小问题
- 6. 对哈希表的初步理解
- 7. 字符串大小写转换(使用位运算)
- 8. 异或运算
- 9. 取两端的值
- 10. bfs与dfs的应用
- 11. i++ , ++i ,i+1
- 12. *二分查找
- 13. 使用质数统计
- 14. BFS总结
- 15. 洗牌算法
- 16. DP动态规化
- 17. 卡特兰数(Catalan)
- 18. 并查集
- 19. 返回代码本身
- 20. 减治、分治与变治
- 21. 位运算操作小技巧
- 21. 快慢指针
- 22. 结果对`1e9+7(1000000007)`取模
- 23. 约瑟夫环问题
- 24. 求最大公约数
- 25. 字典树
- 26. 二维数组中的bfs
- 27. 单调栈
- 28. 方阵翻转替代旋转
- 29. 数状数组
- 30. Morris 中序遍历
- 其它
刷题遇到的小知识点,在这里做个笔记,主要是记录想法、理解、一些牛逼的操作、没见过的算法、数据结构。
看到一篇博客,感触良多,适合正在努力学习算法的你:算法这一站是新的起点
多看,多练
1. 需要判断输入的两个参数的大小/长度
67题,需要根据输入的字符串a、b中长度较短的进行循环,所以我的思路是,首先要判断两个字符串的长度,再进行调换……
public String addBinary(String a, String b) {
int len1 = a.length();
int len2 = b.length();
if(len1 < len2) return addBinary(b, a); //这里是重点,码住!!
/*
后面是其它操作,省略……
*/
}
适用的地方:在方法开始的位置,递归的时候前面没有做太多的操作,不然得不偿失
2. 数学分式的化简
(题号:LCP2)
a + 1 / b 必定是最简分数,所以不用求GCD(最大公约数,还有一个词叫LCM是最小公倍数)了。 (前提:a是整数,b是一个最简分数) 因为b是最简分数,所以 1 / b肯定也是一个最简分数,加上一个整数仍然是最简分数:(ab + 1)/ b = a …… 1
对于求最大公约数的方法,有辗转相除法和更相减损法
辗转相除法:两个数相除取余,然后用被除数与余数相除求余,直到余数为零,此时的被除数即为最大公约数
3. 二叉树操作的小总结
- 当做根、左子树、右子树三部分,左子树、右子树具体是什么样的不管
- 找出终止条件,就是什么时候return,return什么
- 只考虑当前这一步要完成什么功能
4. MySQL分组内取前几名的问题
(185题)一个不错的思路:比如说取每个部门中工资前三高的员工(limit用不了),自连接,条件是工资比我高的员工,然后判断工资比我高的员工(注意相同工资的去重)的个数是不是小于3,是的话说明我就是工资前三高 的员工
5. SQL中的小问题
- CASE END 模仿多分枝选择判断,CASE END 可以用在更新语句SET后边,如
SET 字段=CASE ... END
CASE
WHEN 字段与某个值比较 THEN '解释数据1'
WHEN 字段与某个值比较可以使用聚合函数 THEN '解释数据2'
ELSE '解释数据3'
END
- CASE END模仿switch
CASE 字段名
WHEN '值1' THEN '解释数据1'
WHEN '值2' THEN '解释数据2'
ELSE '解释数据3'
END
- 掌握union的用法
- 在where后面不能用函数,可以用算式,如
id % 2 = 0
,而在其它地方要用聚合函数,如MOD(id, 2) = 0
- IF函数的使用:
IF(判断条件, "true条件为true时", "条件为false时")
6. 对哈希表的初步理解
(1)初步理解
哈希表是最典型的时间换空间,对于一个数组,可以使用哈希表,将数组中的内容当做索引(key),数组下标当做值,当要检索数组中是否存在某个值时,速度比数组快,一个简单的应用:在一个元素不重复的数组中找两个数的和是0(可以是任何数,为了方便假定是0),可以先把数组元素存入到散列表中,在遍历数组时,在散列表中查找是否存在(0-arr[i]
)的元素。
(2)二次遇上再理解
(第442题:题目大致的意思,一个数组中有些元素出现两次而其他元素出现一次,找到所有出现两次的元素,限定条件:1 ≤ a[i] ≤ n (n为数组长度))
我没有注意到这个限定条件,直接就是遍历一遍数组,把每个元素存入到散列表中,如果表中已经存在该值了,就把这个值返回,这个思路的时间复杂度O(n),空间复杂度也是O(n)。当打开评论的时候,真的是另一个新天地,大佬们的代码在时间复杂度没变的同时居然没有使用额外的空间!!
大佬们的思路:因为限定条件的存在,所以可以将数组中的元素不重复的散列在输入数组的范围中,然后再通过正负来标记一个数是否出现过。
7. 字符串大小写转换(使用位运算)
大写变小写、小写变大写:字符 ^= 32
大写变小写、小写变小写:字符 |= 32
小写变大写、大写变大写:字符 &= -33
8. 异或运算
(1)初理解
- 交换律:
a ^ b ^ c <=> a ^ c ^ b
- 任何数于0异或为任何数:
0 ^ n => n
- 相同的数异或为0:
n ^ n => 0
第136题,一个数组中只有一个数出现一次,其余的数均出现了两次,就可以对整个数组元素求异或,最后得到的结果就是那个只出现了一次的数
(2)再理解
第260题,136题的升级版,一数数组中只出现一次的数变为了两个。这时候的思路:先整体异或一遍,求出两个只出现一次的数 x,y 的异或结果xor,根据 xor 二进制位上为 1 的位(比如说最后一个为 1 的位),将数组分成两部分,x,y 刚好被分别分到了数组的两个部分中,然后对两部分的数组分别进行异或运算,就可以求出两个数
- 补充:x ^ y != 0,故二进制中存在某个位为 1
- 如果 xor 的某个二进制位为 1,则说明x,y二进制位的该位一定不同
其它位运算:
9. 取两端的值
一段字符串,左往中间走加1计数,从中间往右走减1计数,取最左边的数(计数为1)和最右边的数(计数为0),这时候两个数不统一(一个为0,一个为1),这时可以用下面的方法:一个先加1或者减1再判断,另一个先判断再加1或者减1
//这里两次判断均使用的是数字0,
for (int i = 0; i < inputs.length; i++) {
char currentChar = inputs[i];
if (currentChar == '(') {
//注意这里是先判断,再加1
if (count > 0) {
sb.append(currentChar);
}
count++;
} else {
//这里是先减1,再判断
count--;
if (count > 0) {
sb.append(currentChar);
}
}
}
10. bfs与dfs的应用
(1)都可以用
遍历图和二叉数
(2)bfs(队列)
计算二叉数和图的深度均可,计算二叉数的深度目前为止个人觉得使用dfs又顺手一点
(3)dfs(栈)
- 计算二叉数的深度:dfs可以将上一层的深度加1,不需要额外的存储空间,而bfs计算深度就要在每个节点中加一块记录当前节点深度的属性,dfs仅能计算二叉数的深度,若要计算图的深度,只能使用bfs
判断路径是否可达,计算一块区域的面积,
注意:有的和路径相关的,求最佳啥的,是使用 dp 来做的,要具体分析
11. i++ , ++i ,i+1
使用时要考虑原来的值到底变不变,先加1 还是后加1,
i+1不会改变原来的值。
i++和++i都会改变原来的值,单独使用时,都可以。但是和其它函数一起使用时,就要注意了,
12. *二分查找
(1)Arrays.binarySearch()使用
-
搜索值是数组元素,从
0
开始计数,得到搜索值的索引值; -
搜索值不是数组元素,且在数组范围内,从
1
开始计数,得“ - 插入点索引值”; -
搜索值不是数组元素,且大于数组内元素,索引值为 – (length + 1);
-
搜索值不是数组元素,且小于数组内元素,索引值为 – 1。
总之:如果搜不到,则插入点为 - ( 索引值 + 1 )
//LIS代码片段
for (int num : nums) {
int i = Arrays.binarySearch(dp, 0, len, num);
if (i < 0) {
i = -(i + 1);
}
dp[i] = num;
if (i == len) {
len++;
}
}
(2)*手动实现
- lo和hi表示的区间为左闭右开
- 当区间为偶数时,mid会落在较大的那个数上
- 搜索值是数组元素,则 lo(左指针) 和 hi(右指针)均指向搜索值的下标(从0开始);
- 搜索值 x 在数组中不存在,则 lo(左指针) 和 hi(右指针)均指向数组中小于 x 的最大的数的下一个下标(即大于x的最小数的下标)
int arr = {......};
int lo = 0, hi = arr.length;
while(lo < hi) {
//这里也可以写成 /2,因为只要是 2 的方幂,Java 的编译器都会转成位运算去计算
//mid = low + (high - low) >> 1 这样写更稳妥一些,写成下面的形式可能出现int溢出
int mid = (hi + lo) >>> 1;
if(arr[mid] < x)
lo = mid + 1;
else
hi = mid;
}
return arr[lo];
13. 使用质数统计
893题,对于一个没有顺序、小写字母、长度较短的字符串,都可以用这种方式处理,来统计每个字母出现次数是否相同
- 每个字母对应一个质数,出现一次该字母就在原数字(起始为1)上乘以该质数,最后对比数字就可以知道字符串是否相同
int[] primes = new int[]{2,3,5,7,11,13,17,19,23,29,31,37,41,43,47,53,59,61,67,71,73,79,83,89,97,101};
Set<String> set = new HashSet<>();
for(String a : A){
long odd = 1;
long even = 1;
char[] cs = a.toCharArray();
for(int i=0;i<cs.length;i++){
if(i % 2 == 0){
even *= primes[cs[i]-'a'];
}else{
odd *= primes[cs[i]-'a'];
}
}
set.add(odd+"_"+even);
}
14. BFS总结
(1)使用Set集合来标记遍历过的节点,遍历过的加到Set中(在入队时加入),使用contains方法确定是否遍历过
(2)带层数的bfs:
//先创建一个队列,并将队列的起始节点加入到队列中
Queue<Integer> queue = new LinkedList<>();
queue.offer(id);
//使用Set集合来去除掉已经遍历过的节点
Set<Integer> visited = new HashSet<>();
visited.add(id);
//len为层数
int len = 0;
//bfs,如果要选取某一层的元素,则在这里加个条件:len < 层数
while (!queue.isEmpty()) {
//这个size为当前层的元素个数
int size = queue.size();
//将当前层的握有元素出队,并将下一层的所有元素入队
for (int i = 0; i < size; i++) {
Integer a = queue.poll();
for (int j = 0; j < friends[idd].length; j++) {
if (!visited.contains(friends[idd][j])) {
//如果该元素之前没有被遍历过的话就加入到队列和Set集合中
queue.add(friends[idd][j]);
visited.add(friends[idd][j]);
}
}
}
//层数加1
len++;
}
15. 洗牌算法
-
Knuth 洗牌算法的伪代码:
- 基本思想:i 从后向前,每次随机一个 [0…i] 之间的下标,然后将 arr[i] 和这个随机的下标元素,也就是 arr[Math.random() * (i+1) ] 交换位置。
- 证明:对于原排列最后一个数字:很显然他在第n个位置的概率是1/n,在倒数第二个位置概率是[(n-1)/n] * [1/(n-1)] = 1/n,在倒数第k个位置的概率是[(n-1)/n] * [(n-2)/(n-1)] *…* [(n-k+1)/(n-k+2)] *[1/(n-k+1)] = 1/n。对于原排列的其他数字也可以同上求得他们在每个位置的概率都是1/n。
- 时间复杂度为O(n),空间复杂度为O(1),缺点必须知道数组长度n
for(int i = n - 1; i >= 0 ; i -- ) swap(arr[i], arr[Math.random() * (i + 1)])
-
Inside-Out Algorithm
- 基本思想:从前向后扫描数据,把位置i的数据随机插入到前 i+1个(从0个到第i个)位置中(假设为k),然后把数组中位置k的数字和位置i的数字交换。
- 证明:原数组的第 i 个元素在新数组的前 i 个位置的概率都是:(1/i) * [i/(i+1)] * [(i+1)/(i+2)] *…* [(n-1)/n] = 1/n,(即第i次刚好随机放到了该位置,在后面的n-i 次选择中该数字不被选中)
- 时间复杂度为O(n),空间复杂度为O(n) ,可以不知道数组的长度
int i = 0; int[] array = {......}; while(i < n){ int tmp = Math.random() * (i + 1);//随机生成一个从0到i(包含i)的数 swap(array[i], array[tmp]);//交换下标为i的数和下标为tmp的数 i++;//后移 }
-
Java.util.Collections
类下有一个静态的shuffle()
方法,可以对List
集合进行洗牌,如下:-
static void shuffle(List<?> list)
:使用默认随机源对列表进行置换,所有置换发生的可能性都是大致相等的。 -
static void shuffle(List<?> list, Random rand)
:使用指定的随机源对指定列表进行置换,所有置换发生的可能性都是大致相等的,假定随机源是公平的。
注意:如果给定一个整型数组,用
Arrays.asList()
方法将其转化为一个集合类,有两种途径:-
用
List<Integer> list=ArrayList(Arrays.asList(ia))
,用shuffle()
打乱不会改变底层数组的顺序。 -
用
List<Integer> list=Arrays.aslist(ia)
,然后用shuffle()
打乱会改变底层数组的顺序。
-
Knuth 洗牌算法:https://www.jianshu.com/p/4be78c20095e
三种洗牌算法shuffle:https://blog.csdn.net/qq_26399665/article/details/79831490
16. DP动态规化
先列出dp方程,再根据dp方程来写程序,
基本上DP的题,能列出dp方程,程序也就写出来了,还有就是,要能想到这道题是用dp来做
一些经典的动态规化题,必刷:
17. 卡特兰数(Catalan)
Catalan数的定义:令h(0)=1
,Catalan数满足递归式:h(n)= h(0)*h(n-1) + h(1)*h(n-2) + ... + h(n-1)h(0) (其中n>=0)
。该递推关系的解为:h(n) = C(2n-2,n-1)/n,n=1,2,3,...
(其中C(2n-2,n-1)表示2n-2个中取n-1个的组合数)。
卡特兰数的前几位分别是:规定h(0)=1,而h(1)=1,h(2)=2,h(3)=5,h(4)=14,h(5)=42,h(6)=132,h(7)=429,h(8)=1430,h(9)=4862,h(10)=16796,h(11)=58786,h(12)=208012,h(13)=742900,h(14)=2674440,h(15)=9694845。
常见的题型:
-
n个节点构成的二叉搜索树,有多少种可能(93题)?简单思路:左子树有0个节点,则右子树有n-1个节点,左子树有1个节点,则右子数有n-2个节点……以此类推,可以dp来做
-
n对括号有多少种合法匹配方式?考虑n对括号,相当于有2n个符号,n个左括号、n个右括号。动态规化的思想:可以设问题的解为dp(2n)。第0个符号肯定为左括号,与之匹配的右括号必须为第2i+1字符。因为如果是第2i个字符,那么第0个字符与第2i个字符间包含奇数个字符,而奇数个字符是无法构成匹配的。通过简单分析,可以得出如下的递推式
f(2i) = f(0)*f(2i-2) + f(2)*f(2i - 4) + ... + f(2i - 4)*f(2) + f(2i-2)*f(0)
。简单解释一下,f(0) * f(2n-2)表示第0个字符与第1个字符匹配为一个括号,以这个括号为准,剩下的括号被分成了这对括号里的0个字符,和这对括号外的2n-2个字符,然后对这两部分求解。f(2)*f(2n-4)表示第0个字符与第3个字符匹配,同时剩余字符分成两个部分,一部分为2个字符,另一部分为2n-4个字符。依次类推。 -
进出栈问题:一个栈(无穷大)的进栈序列为1,2,3,…,n,有多少个不同的出栈序列?和括号匹配问题相同,进栈看成左括号,出栈看成是右括号。
参考了这个,写的不错:Catalan数相关的算法问题
18. 并查集
上周的周赛(2020.1.12),第三题(1319题)就是用并查集写的,可惜我太菜了,没有写出来,所以特地来学习一下
先看了别人的博客,对并查集有了一定的了解:
虽然是C语言写的,但是写的非常有趣,适合入门:超有爱的并查集~
- 并查集就是一个数组,用数组表示多个树,数组的下标 i 代表的是树中节点的编号,数组中的元素 arr[i] 为节点 i 的父节点,这样连起来就是一个树。树的根节点的父节点为它自己,即:
i == arr[i]
- 一个数组中有几个
i == arr[i]
,就是有几个树(连通分量)
常用方法模版:
public int makeConnected(int n, int[][] connections) {
if (n - 1 > connections.length) {
return -1;
}
int[] arr = new int[n];
//初始化并查集
for (int i = 0; i < arr.length; i++) {
arr[i] = i;
}
//合并
for (int[] connection : connections) {
union(connection[0], connection[1]);
}
//计算连通分量的个数
int count = 0;
for (int i = 0; i < n; i++) {
if (parent[i] == i) {
count++;
}
}
}
//查找树的根,同时进行路径压缩
private int findRoot(int[] arr, int node) {
return arr[node] == node ? node : (arr[node] = findRoot(arr, arr[node]));
}
//合并两个树
private void union(int[] arr, int node1, int node2) {
int root1 = findRoot(node1);
int root2 = findRoot(node2);
if (root1 != root2) {
arr[root1] = root2;
}
}
19. 返回代码本身
一道读题就得读半天的题:你需要返回一个字符串,这个字符串就是你提交的代码本身。
关键点:
- 由于换行需要返回的字符串中也要"
\n
",所以代码就写一行了 char c = 34;
34表示的字符为双引号:""
",涉及字符串的双引号, 记得用ASCII码输出代替,不要用"
,你会陷入无限套娃的烦恼
class Solution { public String q() { char c = 34; return s+c+s+c+';'+'}'; } static String s = "class Solution { public String q() { char c = 34; return s+c+s+c+';'+'}'; } static String s = ";}
20. 减治、分治与变治
21. 位运算操作小技巧
n&(n-1)
:将n
的二进制表示中的最低位的1
改为0
21. 快慢指针
- 选取链表中倒数第k个节点:快慢指针,先让快指针走k步,然后两个指针同步走,当快指针走到头时,慢指针就是链表倒数第k个节点。
- Floyd 判圈算法,又称龟兔赛跑算法:用来检测一个链表是否有环。如果链表上存在环,那么在某个环上以不同速度前进的2个指针必定会在某个时刻相遇。
- 环起点的判断:当2个指针相遇时,将其中一个指针移到链表头部,另一个指针还是在他们相遇的地方,然后都以步长为1向后移动,当他们再次相遇时,即为环的起点。
- 环长度的计算
22. 结果对1e9+7(1000000007)
取模
大数阶乘,大数的排列组合等,一般都要求将输出结果对1000000007
取模,主要有以下原因:
1000000007
是一个质数int32
位的最大值为2147483647
,所以对于int32
位来说1000000007
足够大int64
位的最大值为2^63-1
,对于1000000007
来说它的平方不会在int64
中溢出,因为(a∗b)%c=((a%c)∗(b%c))%c
,所以相乘时两边都对1000000007
取模,再保存在int64
里面不会溢出 。
23. 约瑟夫环问题
约瑟夫问题是个著名的问题:N
个人围成一圈,第一个人从1
开始报数,报M
的将被杀掉,下一个人接着从1
开始报。如此反复,最后剩下一个,求最后的胜利者。
递推公式:f(N, M) = (f(N−1, M) + M) % N
f(N,M)
表示,N个人报数,每报到M时杀掉那个人,最终胜利者的编号f(N−1,M)
表示,N-1个人报数,每报到M时杀掉那个人,最终胜利者的编号
原理这篇博客讲的挺不错的:约瑟夫环——公式法(递推公式)
关键点:
- 问题1:假设我们已经知道11个人时,胜利者的下标位置为6。那下一轮10个人时,胜利者的下标位置为多少?
- 答:其实吧,第一轮删掉编号为3的人后,之后的人都往前面移动了3位,胜利这也往前移动了3位,所以他的下标位置由6变成3。
- 问题2:假设我们已经知道10个人时,胜利者的下标位置为3。那下一轮11个人时,胜利者的下标位置为多少?
- 答:这可以看错是上一个问题的逆过程,大家都往后移动3位,所以
f(11, 3) = f(10, 3) + 3
。不过有可能数组会越界,所以最后模上当前人数的个数,f(11, 3) =(f(10, 3) + 3)% 11
24. 求最大公约数
-
求两个数的最大公约数:
// 辗转相除法 private int gcd (int a, int b) { return b == 0? a: gcd(b, a % b); }
-
求多个数的最大公约数:(gcd的结合律)
gcd(a, b, c) = gcd(gcd(a, b), c)
25. 字典树
字典树又名前缀树,Trie
树,是一种存储大量字符串的树形数据结构,相比于HashMap存储,在存储单词(和语种无关,任意语言都可以)的场景上,节省了大量的内存空间。
下图演示了一个保存了8个单词的字典树的结构,8个单词分别是:“A”, “to”, “tea”, “ted”, “ten”, “i”, “in”, “inn”。
怎么理解这颗树呢?你从根节点走到叶子节点,尝试走一下所有的路径。你会发现,每条从根节点到叶子节点的路径都构成了单词(有的不需要走到叶子节点也是单词,比如 “i
” 和 “in
”)。trie
树里的每个节点只需要保存当前的字符就可以了(当然你也可以额外记录别的信息,比如记录一下如果以当前节点结束是否构成单词)。
-
trid树的练习:208. 实现 Trie (前缀树)
-
trie
树的使用:
- 搜索引擎中,输入一个字,会出现以该字为前缀的相关搜索
- 区块链:
trie
树的进阶版,Merkle Patricia Tree
,他能够高效、安全地验证大型数据结构中的数据 - IP路由,倒排索引
- 分词
26. 二维数组中的bfs
经常会遇到在二维数组中使用bfs时,要遍历四个方向或者八个方向,这时可以使用下面的方法避免写4个判断或者4个循环
//定义偏移数组
int[] dx = {0, 0, 1, -1}; //八个方向:{0, 0, 1, -1, 1, 1, -1, -1}
int[] dy = {1, -1, 0, 0}; //八个方向:{1, -1, 0, 0, 1, -1, -1, 1}
//在偏移数组中循环4(偏移数组的长度)次
for (int i = 0; i < 4; i++) {
//获取偏移之后的数组坐标
int newX = x + dx[i];
int newY = y + dy[i];
//越界检查与条件判断,m和n分别表示二维数组的大小为m*n
if (newX < 0 || newX >= m || newY < 0 || newY >= n) {
continue;
}
/*
执行其它操作
*/
}
27. 单调栈
单调栈就是比普通的栈多一个性质,即维护一个栈内元素单调递增或者递减。比如当前某个单调递减的栈的元素从栈底到栈顶分别是:[10, 9, 8, 3, 2]
,如果要入栈元素5
,需要先把 2
和 3
从栈中pop
出去,满足单调递减为止,即变成[10, 9, 8]
,然后再入栈5
,就是[10, 9, 8, 5]
。
相应的题:
28. 方阵翻转替代旋转
先沿对角线翻转,再沿水平线或者垂直线翻转,可以实现方阵顺时针或逆时针旋转90度。
不同的对角线和水平/垂直线搭配,旋转的方向不同:
- 左上-右下 + 水平:逆时针90度
- 左上-右下 + 垂直:顺时针90度
- 右上-左下 + 水平:顺时针90度
- 右上-左下 + 垂直:逆时针90度
29. 数状数组
树状数组(Fenwick Tree)是用数组来模拟树形结构,可以解决大部分基于区间上的更新以及求和问题。树状数组中修改和查询的复杂度都是O(logN)
。
数状数组的功能主要有下面两个:
- 单点更新
update(i, v)
: 把序列i
位置的数加上一个值v
- 区间查询
query(i)
: 查询序列[1]
到[i]
区间的和,即i
位置的前缀和
public class FenwickTree {
private int[] tree;
private int len;
public FenwickTree(int n) {
this.len = n;
tree = new int[n + 1];
}
/**
* 单点更新:将 index 这个位置 + delta
*
* @param i
* @param delta
*/
public void update(int i, int delta) {
// 从下到上,最多到 size,可以等于 size
while (i <= this.len) {
tree[i] += delta;
i += lowbit(i);
}
}
// 区间查询:查询小于等于 tree[index] 的元素个数
// 查询的语义是「前缀和」
public int query(int i) {
// 从右到左查询
int sum = 0;
while (i > 0) {
sum += tree[i];
i -= lowbit(i);
}
return sum;
}
//求 x 的二进制中从最低位到高位连续零的长度(称为lowbit)
public int lowbit(int x) {
return x & (-x);
}
}
用到的地方:
- 求数组中的逆序对(也可以使用归并排序来实现)
参考:
30. Morris 中序遍历
无论是二叉树的中序遍历还是用 stack 模拟递归,都需要 O(n)
的空间复杂度。
Morris 遍历是一种 O(1)
空间复杂度 的遍历方法,其本质是 线索二叉树(Threaded Binary Tree),利用二叉树中 n+1 个指向 NULL 的指针。
先明确一些基础概念:
- 中序遍历:先遍历节点的左子树,然后遍历当前节点,最后遍历当前节点的右子树。对于二叉搜索树,其中序遍历是递增的。
- 前驱节点与后继节点:对一棵二叉树进行中序遍历,遍历后的顺序,当前节点的前一个节点为该节点的前驱节点,后一个节点为后继节点。
Morris 中序遍历步骤:
- 如果当前节点没有左子树,则遍历这个点,然后跳转到当前节点的右子树。
- 如果当前节点有左子树,那么先找到它的前驱节点。它的前驱节点一定在左子树上,我们可以在左子树上一直向右行走,找到当前点的前驱节点。
- 如果前驱节点没有右子树,就将前驱节点的
right
指针指向当前节点。这一步是为了在遍历完前驱节点后能找到前驱节点的后继,也就是当前节点。 - 如果前驱节点的右子树为当前节点,说明前驱节点已经被遍历过并被修改了
right
指针,这个时候我们重新将前驱的右孩子设置为空,遍历当前的点,然后跳转到当前节点的右子树。
- 如果前驱节点没有右子树,就将前驱节点的
// cur 为当前节点, pre 为前驱节点
TreeNode cur = root, pre = null;
while (cur != null) {
if (cur.left == null) {
//当前节点没有左子树的情况
System.out.println(cur.val);
cur = cur.right;
continue;
}
//当前节点有左子树,先找到他的前驱节点
pre = cur.left;
while (pre.right != null && pre.right != cur) {
pre = pre.right;
}
if (pre.right == null) {
//前驱节点没有被改过,说明左子树没有被遍历,遍历左子树
pre.right = cur;
cur = cur.left;
} else {
//前驱节点被改过,说明左子树遍历过了,接下来就是遍历当前节点和右子树
pre.right = null;
System.out.println(cur.val);
cur = cur.right;
}
}
二叉树节点类:
public class TreeNode { int val; TreeNode left; TreeNode right; TreeNode(int x) { val = x; } }
其它
map.getOrDefault(key, defaultValue)
:如果没有key,则取出defaultValue,否则取出key对应的value值- 根据map的value排序:
- 用map的方法
map.entrySet()
生成Set集合,遍历集合将键值对存放到优先队列PriorityQueue中,并指定Comparator
- 用map的方法
javafx.util.Pair对象
:指一对
键值对,只能存放一对,可以用于方法返回两个参数的情况,与map中的Entry类似,方法也相同(getKey(),getValue()),不过比map更轻量。使用时导包:javafx.util.Pair
- 清空StringBuilder:
stringBuilder.setLength(0);
持续更新中……