一. 初识算法
1.1 什么是算法?
定义
在数学和计算机科学领域,算法是一系列有限的严谨指令,通常用于解决一类特定问题或执行计算
In mathematics and computer science, an algorithm (/ˈælɡərɪðəm/) is a finite sequence of rigorous instructions, typically used to solve a class of specific problems or to perform a computation.
Introduction to Algorithm
不正式的说,算法就是任何定义优良的计算过程:接收一些值作为输入,在有限的时间内,产生一些值作为输出。
Informally, an algorithm is any well-defined computational procedure that takes some value, or set of values, as input and produces some value, or set of values, as output in a finite amount of time.
1.2 什么是数据结构?
定义
在计算机科学领域,数据结构是一种数据组织、管理和存储格式,通常被选择用来高效访问数据
In computer science, a data structure is a data organization, management, and storage format that is usually chosen for efficient access to data
Introduction to Algorithm
数据结构是一种存储和组织数据的方式,旨在便于访问和修改
A data structure is a way to store and organize data in order to facilitate access and modifications
可以说,程序 = 数据结构 + 算法 ,它们是每一位程序员的基本功,下来我们通过对一个非常著名的二分查找算法的讲解来认识一下算法
1.3 二分查找
二分查找算法也称折半查找,是一种非常高效的工作于有序数组的查找算法。后续的课程中还会学习更多的查找算法,但在此之前,不妨用它作为入门。
1) 基础版
需求:在有序 数组 A A A 内,查找值 t a r g e t target t a r g e t
算法描述
前提
给定一个内含 n n n 个元素的有序数组 A A A ,满足 A 0 ≤ A 1 ≤ A 2 ≤ ⋯ ≤ A n − 1 A_{0}\leq A_{1}\leq A_{2}\leq \cdots \leq A_{n-1} A 0 ≤ A 1 ≤ A 2 ≤ ⋯ ≤ A n − 1 ,一个待查值 t a r g e t target t a r g e t
1
设置 i = 0 i=0 i = 0 ,j = n − 1 j=n-1 j = n − 1
2
如果 i > j i \gt j i > j ,结束查找,没找到
3
设置 m = f l o o r ( i + j 2 ) m = floor(\frac {i+j}{2}) m = f l o o r ( 2 i + j ) ,m m m 为中间索引,f l o o r floor f l o o r 是向下取整(≤ i + j 2 \leq \frac {i+j}{2} ≤ 2 i + j 的最小整数)
4
如果 t a r g e t < A m target < A_{m} t a r g e t < A m 设置 j = m − 1 j = m - 1 j = m − 1 ,跳到第2步
5
如果 A m < t a r g e t A_{m} < target A m < t a r g e t 设置 i = m + 1 i = m + 1 i = m + 1 ,跳到第2步
6
如果 A m = t a r g e t A_{m} = target A m = t a r g e t ,结束查找,找到了
P.S.
对于一个算法来讲,都有较为严谨的描述,上面是一个例子
后续讲解时,以简明直白为目标,不会总以上面的方式来描述算法
java 实现
public static int binarySearch (int [] a, int target) { int i = 0 , j = a.length - 1 ; while (i <= j) { int m = (i + j) >>> 1 ; if (target < a[m]) { j = m - 1 ; } else if (a[m] < target) { i = m + 1 ; } else { return m; } } return -1 ; }
i , j i,j i , j 对应着搜索区间 [ 0 , a . l e n g t h − 1 ] [0,a.length-1] [ 0 , a . l e n g t h − 1 ] (注意是闭合的区间),i < = j i<=j i < = j 意味着搜索区间内还有未比较的元素,i , j i,j i , j 指向的元素也可能是比较的目标
思考:如果不加 i = = j i==j i = = j 行不行?
回答:不行,因为这意味着 i , j i,j i , j 指向的元素会漏过比较
m m m 对应着中间位置,中间位置左边和右边的元素可能不相等(差一个),不会影响结果
如果某次未找到,那么缩小后的区间内不包含 m m m
2) 改变版
另一种写法
public static int binarySearch (int [] a, int target) { int i = 0 , j = a.length; while (i < j) { int m = (i + j) >>> 1 ; if (target < a[m]) { j = m; } else if (a[m] < target) { i = m + 1 ; } else { return m; } } return -1 ; }
i , j i,j i , j 对应着搜索区间 [ 0 , a . l e n g t h ) [0,a.length) [ 0 , a . l e n g t h ) (注意是左闭右开的区间),i < j i<j i < j 意味着搜索区间内还有未比较的元素,j j j 指向的一定不是 查找目标
思考:为啥这次不加 i = = j i==j i = = j 的条件了?
回答:这回 j j j 指向的不是查找目标,如果还加 i = = j i==j i = = j 条件,就意味着 j j j 指向的还会再次比较,找不到时,会死循环
如果某次要缩小右边界,那么 j = m j=m j = m ,因为此时的 m m m 已经不是 查找目标了
1.4 衡量算法好坏
时间复杂度
下面的查找算法也能得出与之前二分查找一样的结果,那你能说出它差在哪里吗?
public static int search (int [] a, int k) { for ( int i = 0 ; i < a.length; i++ ) { if (a[i] == k) { return i; } } return -1 ; }
考虑最坏情况下(没找到)例如 [1,2,3,4]
查找 5
int i = 0
只执行一次
i < a.length
受数组元素个数 n n n 的影响,比较 n + 1 n+1 n + 1 次
i++
受数组元素个数 n n n 的影响,自增 n n n 次
a[i] == k
受元素个数 n n n 的影响,比较 n n n 次
return -1
,执行一次
粗略认为每行代码执行时间是 t t t ,假设 n = 4 n=4 n = 4 那么
总执行时间是 ( 1 + 4 + 1 + 4 + 4 + 1 ) ∗ t = 15 t (1+4+1+4+4+1)*t = 15t ( 1 + 4 + 1 + 4 + 4 + 1 ) ∗ t = 1 5 t
可以推导出更一般地公式为,T = ( 3 ∗ n + 3 ) t T = (3*n+3)t T = ( 3 ∗ n + 3 ) t
如果套用二分查找算法,还是 [1,2,3,4]
查找 5
public static int binarySearch (int [] a, int target) { int i = 0 , j = a.length - 1 ; while (i <= j) { int m = (i + j) >>> 1 ; if (target < a[m]) { j = m - 1 ; } else if (a[m] < target) { i = m + 1 ; } else { return m; } } return -1 ; }
int i = 0, j = a.length - 1
各执行 1 次
i <= j
比较 f l o o r ( log 2 ( n ) + 1 ) floor(\log_{2}(n)+1) f l o o r ( log 2 ( n ) + 1 ) 再加 1 次
(i + j) >>> 1
计算 f l o o r ( log 2 ( n ) + 1 ) floor(\log_{2}(n)+1) f l o o r ( log 2 ( n ) + 1 ) 次
接下来 if() else if() else
会执行 3 ∗ f l o o r ( log 2 ( n ) + 1 ) 3* floor(\log_{2}(n)+1) 3 ∗ f l o o r ( log 2 ( n ) + 1 ) 次,分别为
if 比较
else if 比较
else if 比较成立后的赋值语句
return -1
,执行一次
结果:
总执行时间为 ( 2 + ( 1 + 3 ) + 3 + 3 ∗ 3 + 1 ) ∗ t = 19 t (2 + (1+3) + 3 + 3 * 3 +1)*t = 19t ( 2 + ( 1 + 3 ) + 3 + 3 ∗ 3 + 1 ) ∗ t = 1 9 t
更一般地公式为 ( 4 + 5 ∗ f l o o r ( log 2 ( n ) + 1 ) ) ∗ t (4 + 5 * floor(\log_{2}(n)+1))*t ( 4 + 5 ∗ f l o o r ( log 2 ( n ) + 1 ) ) ∗ t
注意:
左侧未找到和右侧未找到结果不一样,这里不做分析
两个算法比较,可以看到 n n n 在较小的时候,二者花费的次数差不多
但随着 n n n 越来越大,比如说 n = 1000 n=1000 n = 1 0 0 0 时,用二分查找算法(红色)也就是 54 t 54t 5 4 t ,而蓝色算法则需要 3003 t 3003t 3 0 0 3 t
画图采用的是 Desmos | 图形计算器
计算机科学中,时间复杂度 是用来衡量:一个算法的执行,随数据规模增大,而增长的时间成本
如何表示时间复杂度呢?
大 O O O 表示法
其中
c , c 1 , c 2 c, c_1, c_2 c , c 1 , c 2 都为一个常数
f ( n ) f(n) f ( n ) 是实际执行代码行数与 n 的函数
g ( n ) g(n) g ( n ) 是经过化简,变化趋势与 f ( n ) f(n) f ( n ) 一致的 n 的函数
渐进上界
渐进上界(asymptotic upper bound):从某个常数 n 0 n_0 n 0 开始,c ∗ g ( n ) c*g(n) c ∗ g ( n ) 总是位于 f ( n ) f(n) f ( n ) 上方,那么记作 O ( g ( n ) ) O(g(n)) O ( g ( n ) )
例1
f ( n ) = 3 ∗ n + 3 f(n) = 3*n+3 f ( n ) = 3 ∗ n + 3
g ( n ) = n g(n) = n g ( n ) = n
取 c = 4 c=4 c = 4 ,在n 0 = 3 n_0=3 n 0 = 3 之后,g ( n ) g(n) g ( n ) 可以作为 f ( n ) f(n) f ( n ) 的渐进上界,因此表示法写作 O ( n ) O(n) O ( n )
例2
f ( n ) = 5 ∗ f l o o r ( l o g 2 ( n ) ) + 9 f(n) = 5*floor(log_2(n)) + 9 f ( n ) = 5 ∗ f l o o r ( l o g 2 ( n ) ) + 9
g ( n ) = l o g 2 ( n ) g(n) = log_2(n) g ( n ) = l o g 2 ( n )
O ( l o g 2 ( n ) ) O(log_2(n)) O ( l o g 2 ( n ) )
已知 f ( n ) f(n) f ( n ) 来说,求 g ( n ) g(n) g ( n )
表达式中相乘的常量,可以省略,如
f ( n ) = 100 ∗ n 2 f(n) = 100*n^2 f ( n ) = 1 0 0 ∗ n 2 中的 100 100 1 0 0
多项式中数量规模更小(低次项)的表达式,如
f ( n ) = n 2 + n f(n)=n^2+n f ( n ) = n 2 + n 中的 n n n
f ( n ) = n 3 + n 2 f(n) = n^3 + n^2 f ( n ) = n 3 + n 2 中的 n 2 n^2 n 2
不同底数的对数,渐进上界可以用一个对数函数 log n \log n log n 表示
例如:l o g 2 ( n ) log_2(n) l o g 2 ( n ) 可以替换为 l o g 10 ( n ) log_{10}(n) l o g 1 0 ( n ) ,因为 l o g 2 ( n ) = l o g 10 ( n ) l o g 10 ( 2 ) log_2(n) = \frac{log_{10}(n)}{log_{10}(2)} l o g 2 ( n ) = l o g 1 0 ( 2 ) l o g 1 0 ( n ) ,相乘的常量 1 l o g 10 ( 2 ) \frac{1}{log_{10}(2)} l o g 1 0 ( 2 ) 1 可以省略
类似的,对数的常数次幂可省略
如:l o g ( n c ) = c ∗ l o g ( n ) log(n^c) = c * log(n) l o g ( n c ) = c ∗ l o g ( n )
常见大 O O O 表示法
按时间复杂度从低到高
黑色横线 O ( 1 ) O(1) O ( 1 ) ,常量时间,意味着算法时间并不随数据规模而变化
绿色 O ( l o g ( n ) ) O(log(n)) O ( l o g ( n ) ) ,对数时间
蓝色 O ( n ) O(n) O ( n ) ,线性时间,算法时间与数据规模成正比
橙色 O ( n ∗ l o g ( n ) ) O(n*log(n)) O ( n ∗ l o g ( n ) ) ,拟线性时间
红色 O ( n 2 ) O(n^2) O ( n 2 ) 平方时间
黑色朝上 O ( 2 n ) O(2^n) O ( 2 n ) 指数时间
没画出来的 O ( n ! ) O(n!) O ( n ! )
渐进下界
渐进下界(asymptotic lower bound):从某个常数 n 0 n_0 n 0 开始,c ∗ g ( n ) c*g(n) c ∗ g ( n ) 总是位于 f ( n ) f(n) f ( n ) 下方,那么记作 Ω ( g ( n ) ) \Omega(g(n)) Ω ( g ( n ) )
渐进紧界
渐进紧界(asymptotic tight bounds):从某个常数 n 0 n_0 n 0 开始,f ( n ) f(n) f ( n ) 总是在 c 1 ∗ g ( n ) c_1*g(n) c 1 ∗ g ( n ) 和 c 2 ∗ g ( n ) c_2*g(n) c 2 ∗ g ( n ) 之间,那么记作 Θ ( g ( n ) ) \Theta(g(n)) Θ ( g ( n ) )
空间复杂度
与时间复杂度类似,一般也使用大 O O O 表示法来衡量:一个算法执行随数据规模增大,而增长的额外 空间成本
public static int binarySearchBasic (int [] a, int target) { int i = 0 , j = a.length - 1 ; while (i <= j) { int m = (i + j) >>> 1 ; if (target < a[m]) { j = m - 1 ; } else if (a[m] < target) { i = m + 1 ; } else { return m; } } return -1 ; }
二分查找性能
下面分析二分查找算法的性能
时间复杂度
最坏情况:O ( log n ) O(\log n) O ( log n )
最好情况:如果待查找元素恰好在数组中央,只需要循环一次 O ( 1 ) O(1) O ( 1 )
空间复杂度
需要常数个指针 i , j , m i,j,m i , j , m ,因此额外占用的空间是 O ( 1 ) O(1) O ( 1 )
1.5 再看二分查找
1) 平衡版
public static int binarySearchBalance (int [] a, int target) { int i = 0 , j = a.length; while (1 < j - i) { int m = (i + j) >>> 1 ; if (target < a[m]) { j = m; } else { i = m; } } return (a[i] == target) ? i : -1 ; }
思想:
左闭右开的区间,i i i 指向的可能是目标,而 j j j 指向的不是目标
不奢望循环内通过 m m m 找出目标, 缩小区间直至剩 1 个, 剩下的这个可能就是要找的(通过 i i i )
j − i > 1 j - i > 1 j − i > 1 的含义是,在范围内待比较的元素个数 > 1
改变 i i i 边界时,它指向的可能是目标,因此不能 m + 1 m+1 m + 1
循环内的平均比较次数减少了
时间复杂度 Θ ( l o g ( n ) ) \Theta(log(n)) Θ ( l o g ( n ) )
2) Java 版
private static int binarySearch0 (long [] a, int fromIndex, int toIndex, long key) { int low = fromIndex; int high = toIndex - 1 ; while (low <= high) { int mid = (low + high) >>> 1 ; long midVal = a[mid]; if (midVal < key) low = mid + 1 ; else if (midVal > key) high = mid - 1 ; else return mid; } return -(low + 1 ); }
例如 [ 1 , 3 , 5 , 6 ] [1,3,5,6] [ 1 , 3 , 5 , 6 ] 要插入 2 2 2 那么就是找到一个位置,这个位置左侧元素都比它小
等循环结束,若没找到,low 左侧元素肯定都比 target 小,因此 low 即插入点
插入点取负是为了与找到情况区分
-1 是为了把索引 0 位置的插入点与找到的情况进行区分
3) Leftmost 与 Rightmost
有时我们希望返回的是最左侧的重复元素,如果用 Basic 二分查找
对于数组 [ 1 , 2 , 3 , 4 , 4 , 5 , 6 , 7 ] [1, 2, 3, 4, 4, 5, 6, 7] [ 1 , 2 , 3 , 4 , 4 , 5 , 6 , 7 ] ,查找元素4,结果是索引3
对于数组 [ 1 , 2 , 4 , 4 , 4 , 5 , 6 , 7 ] [1, 2, 4, 4, 4, 5, 6, 7] [ 1 , 2 , 4 , 4 , 4 , 5 , 6 , 7 ] ,查找元素4,结果也是索引3,并不是最左侧的元素
public static int binarySearchLeftmost1 (int [] a, int target) { int i = 0 , j = a.length - 1 ; int candidate = -1 ; while (i <= j) { int m = (i + j) >>> 1 ; if (target < a[m]) { j = m - 1 ; } else if (a[m] < target) { i = m + 1 ; } else { candidate = m; j = m - 1 ; } } return candidate; }
如果希望返回的是最右侧元素
public static int binarySearchRightmost1 (int [] a, int target) { int i = 0 , j = a.length - 1 ; int candidate = -1 ; while (i <= j) { int m = (i + j) >>> 1 ; if (target < a[m]) { j = m - 1 ; } else if (a[m] < target) { i = m + 1 ; } else { candidate = m; i = m + 1 ; } } return candidate; }
应用
对于 Leftmost 与 Rightmost,可以返回一个比 -1 更有用的值
Leftmost 改为
public static int binarySearchLeftmost (int [] a, int target) { int i = 0 , j = a.length - 1 ; while (i <= j) { int m = (i + j) >>> 1 ; if (target <= a[m]) { j = m - 1 ; } else { i = m + 1 ; } } return i; }
leftmost 返回值的另一层含义:< t a r g e t \lt target < t a r g e t 的元素个数
小于等于中间值,都要向左找
Rightmost 改为
public static int binarySearchRightmost (int [] a, int target) { int i = 0 , j = a.length - 1 ; while (i <= j) { int m = (i + j) >>> 1 ; if (target < a[m]) { j = m - 1 ; } else { i = m + 1 ; } } return i - 1 ; }
几个名词
范围查询 :
查询 x < 4 x \lt 4 x < 4 ,0.. l e f t m o s t ( 4 ) − 1 0 .. leftmost(4) - 1 0 . . l e f t m o s t ( 4 ) − 1
查询 x ≤ 4 x \leq 4 x ≤ 4 ,0.. r i g h t m o s t ( 4 ) 0 .. rightmost(4) 0 . . r i g h t m o s t ( 4 )
查询 4 < x 4 \lt x 4 < x ,$rightmost(4) + 1 … \infty $
查询 4 ≤ x 4 \leq x 4 ≤ x , l e f t m o s t ( 4 ) . . ∞ leftmost(4) .. \infty l e f t m o s t ( 4 ) . . ∞
查询 4 ≤ x ≤ 7 4 \leq x \leq 7 4 ≤ x ≤ 7 ,l e f t m o s t ( 4 ) . . r i g h t m o s t ( 7 ) leftmost(4) .. rightmost(7) l e f t m o s t ( 4 ) . . r i g h t m o s t ( 7 )
查询 4 < x < 7 4 \lt x \lt 7 4 < x < 7 ,r i g h t m o s t ( 4 ) + 1.. l e f t m o s t ( 7 ) − 1 rightmost(4)+1 .. leftmost(7)-1 r i g h t m o s t ( 4 ) + 1 . . l e f t m o s t ( 7 ) − 1
求排名 :l e f t m o s t ( t a r g e t ) + 1 leftmost(target) + 1 l e f t m o s t ( t a r g e t ) + 1
t a r g e t target t a r g e t 可以不存在,如:l e f t m o s t ( 5 ) + 1 = 6 leftmost(5)+1 = 6 l e f t m o s t ( 5 ) + 1 = 6
t a r g e t target t a r g e t 也可以存在,如:l e f t m o s t ( 4 ) + 1 = 3 leftmost(4)+1 = 3 l e f t m o s t ( 4 ) + 1 = 3
求前任(predecessor) :l e f t m o s t ( t a r g e t ) − 1 leftmost(target) - 1 l e f t m o s t ( t a r g e t ) − 1
l e f t m o s t ( 3 ) − 1 = 1 leftmost(3) - 1 = 1 l e f t m o s t ( 3 ) − 1 = 1 ,前任 a 1 = 2 a_1 = 2 a 1 = 2
l e f t m o s t ( 4 ) − 1 = 1 leftmost(4) - 1 = 1 l e f t m o s t ( 4 ) − 1 = 1 ,前任 a 1 = 2 a_1 = 2 a 1 = 2
求后任(successor) :r i g h t m o s t ( t a r g e t ) + 1 rightmost(target)+1 r i g h t m o s t ( t a r g e t ) + 1
r i g h t m o s t ( 5 ) + 1 = 5 rightmost(5) + 1 = 5 r i g h t m o s t ( 5 ) + 1 = 5 ,后任 a 5 = 7 a_5 = 7 a 5 = 7
r i g h t m o s t ( 4 ) + 1 = 5 rightmost(4) + 1 = 5 r i g h t m o s t ( 4 ) + 1 = 5 ,后任 a 5 = 7 a_5 = 7 a 5 = 7
求最近邻居 :
习题
1) 时间复杂度估算
用函数 f ( n ) f(n) f ( n ) 表示算法效率与数据规模的关系,假设每次解决问题需要 1 微秒(1 0 − 6 10^{-6} 1 0 − 6 秒),进行估算:
如果 f ( n ) = n 2 f(n) = n^2 f ( n ) = n 2 那么 1 秒能解决多少次问题?1 天呢?
如果 f ( n ) = l o g 2 ( n ) f(n) = log_2(n) f ( n ) = l o g 2 ( n ) 那么 1 秒能解决多少次问题?1 天呢?
如果 f ( n ) = n ! f(n) = n! f ( n ) = n ! 那么 1 秒能解决多少次问题?1 天呢?
参考解答
1秒 1 0 6 = 1000 \sqrt{10^6} = 1000 1 0 6 = 1 0 0 0 次,1 天 1 0 6 ∗ 3600 ∗ 24 ≈ 293938 \sqrt{10^6 * 3600 * 24} \approx 293938 1 0 6 ∗ 3 6 0 0 ∗ 2 4 ≈ 2 9 3 9 3 8 次
1秒 $2^{1,000,000} $ 次,一天 2 86 , 400 , 000 , 000 2^{86,400,000,000} 2 8 6 , 4 0 0 , 0 0 0 , 0 0 0
推算如下
10 ! = 3 , 628 , 800 10! = 3,628,800 1 0 ! = 3 , 6 2 8 , 8 0 0 1秒能解决 1 , 000 , 000 1,000,000 1 , 0 0 0 , 0 0 0 次,因此次数为 9 次
14 ! = 87 , 178 , 291 , 200 14!=87,178,291,200 1 4 ! = 8 7 , 1 7 8 , 2 9 1 , 2 0 0 ,一天能解决 86 , 400 , 000 , 000 86,400,000,000 8 6 , 4 0 0 , 0 0 0 , 0 0 0 次,因此次数为 13 次
2) 耗时估算
一台机器对200个单词进行排序花了200秒(使用冒泡排序),那么花费800秒,大概可以对多少个单词进行排序
a. 400
b. 600
c. 800
d. 1600
答案
解释
冒泡排序时间复杂度是 O ( N 2 ) O(N^2) O ( N 2 )
时间增长 4 倍,而因此能处理的数据量是原来的 4 = 2 \sqrt{4} = 2 4 = 2 倍
3) E01. 二分查找-Leetcode 704
要点 :减而治之,可以用递归或非递归实现
给定一个 n 个元素有序的(升序)整型数组 nums 和一个目标值 target ,写一个函数搜索 nums 中的 target,如果目标值存在返回下标,否则返回 -1
例如
输入: nums = [-1 ,0 ,3 ,5 ,9 ,12 ], target = 9 输出: 4 解释: 9 出现在 nums 中并且下标为 4 输入: nums = [-1 ,0 ,3 ,5 ,9 ,12 ], target = 2 输出: -1 解释: 2 不存在 nums 中因此返回 -1
参考答案 :略,可以用讲过的任意一种二分求解
4) E02. 搜索插入位置-Leetcode 35
要点 :理解谁代表插入位置
给定一个排序数组和一个目标值
在数组中找到目标值,并返回其索引
如果目标值不存在于数组中,返回它将会被按顺序插入的位置
例如
输入: nums = [1,3,5,6], target = 5 输出: 2 输入: nums = [1,3,5,6], target = 2 输出: 1 输入: nums = [1,3,5,6], target = 7 输出: 4
参考答案1 :用二分查找基础版代码改写,基础版中,找到返回 m,没找到 i 代表插入点,因此有
public int searchInsert (int [] a, int target) { int i = 0 , j = a.length - 1 ; while (i <= j) { int m = (i + j) >>> 1 ; if (target < a[m]) { j = m - 1 ; } else if (a[m] < target) { i = m + 1 ; } else { return m; } } return i; }
参考答案2 :用二分查找平衡版改写,平衡版中
如果 target == a[i] 返回 i 表示找到
如果 target < a[i],例如 target = 2,a[i] = 3,这时就应该在 i 位置插入 2
如果 a[i] < target,例如 a[i] = 3,target = 4,这时就应该在 i+1 位置插入 4
public static int searchInsert (int [] a, int target) { int i = 0 , j = a.length; while (1 < j - i) { int m = (i + j) >>> 1 ; if (target < a[m]) { j = m; } else { i = m; } } return (target <= a[i]) ? i : i + 1 ; }
参考答案3 :用 leftmost 版本解,返回值即为插入位置(并能处理元素重复的情况)
public int searchInsert (int [] a, int target) { int i = 0 , j = a.length - 1 ; while (i <= j) { int m = (i + j) >>> 1 ; if (target <= a[m]) { j = m - 1 ; } else { i = m + 1 ; } } return i; }
5) E03. 搜索开始结束位置-Leetcode 34
给你一个按照非递减顺序排列的整数数组 nums,和一个目标值 target。请你找出给定目标值在数组中的开始位置和结束位置。
如果数组中不存在目标值 target,返回 [-1, -1]。
你必须设计并实现时间复杂度为 O(log n) 的算法解决此问题
例如
输入:nums = [5,7,7,8,8,10], target = 8 输出:[3,4] 输入:nums = [5,7,7,8,8,10], target = 6 输出:[-1,-1] 输入:nums = [], target = 0 输出:[-1,-1]
参考答案
public static int left (int [] a, int target) { int i = 0 , j = a.length - 1 ; int candidate = -1 ; while (i <= j) { int m = (i + j) >>> 1 ; if (target < a[m]) { j = m - 1 ; } else if (a[m] < target) { i = m + 1 ; } else { candidate = m; j = m - 1 ; } } return candidate; } public static int right (int [] a, int target) { int i = 0 , j = a.length - 1 ; int candidate = -1 ; while (i <= j) { int m = (i + j) >>> 1 ; if (target < a[m]) { j = m - 1 ; } else if (a[m] < target) { i = m + 1 ; } else { candidate = m; i = m + 1 ; } } return candidate; } public static int [] searchRange(int [] nums, int target) { int x = left(nums, target); if (x == -1 ) { return new int [] {-1 , -1 }; } else { return new int [] {x, right(nums, target)}; } }
二. 基础数据结构
2.1 数组
1) 概述
定义
在计算机科学中,数组是由一组元素(值或变量)组成的数据结构,每个元素有至少一个索引或键来标识
In computer science, an array is a data structure consisting of a collection of elements (values or variables), each identified by at least one array index or key
因为数组内的元素是连续存储 的,所以数组中元素的地址,可以通过其索引计算出来,例如:
int [] array = {1 ,2 ,3 ,4 ,5 }
知道了数组的数据 起始地址 B a s e A d d r e s s BaseAddress B a s e A d d r e s s ,就可以由公式 B a s e A d d r e s s + i ∗ s i z e BaseAddress + i * size B a s e A d d r e s s + i ∗ s i z e 计算出索引 i i i 元素的地址
i i i 即索引,在 Java、C 等语言都是从 0 开始
s i z e size s i z e 是每个元素占用字节,例如 i n t int i n t 占 4 4 4 ,d o u b l e double d o u b l e 占 8 8 8
小测试
byte [] array = {1 ,2 ,3 ,4 ,5 }
已知 array 的数据 的起始地址是 0x7138f94c8,那么元素 3 的地址是什么?
答:0x7138f94c8 + 2 * 1 = 0x7138f94ca
空间占用
Java 中数组结构为
8 字节 markword
4 字节 class 指针(压缩 class 指针的情况)
4 字节 数组大小(决定了数组最大容量是 2 32 2^{32} 2 3 2 )
数组元素 + 对齐字节(java 中所有对象大小都是 8 字节的整数倍,不足的要用对齐字节补足)
例如
int [] array = {1 , 2 , 3 , 4 , 5 };
的大小为 40 个字节,组成如下
8 + 4 + 4 + 5*4 + 4(alignment)
随机访问性能
即根据索引查找元素,时间复杂度是 O ( 1 ) O(1) O ( 1 )
2) 动态数组
java 版本
public class DynamicArray implements Iterable <Integer> { private int size = 0 ; private int capacity = 8 ; private int [] array = {}; public void addLast (int element) { add(size, element); } public void add (int index, int element) { checkAndGrow(); if (index >= 0 && index < size) { System.arraycopy(array, index, array, index + 1 , size - index); } array[index] = element; size++; } private void checkAndGrow () { if (size == 0 ) { array = new int [capacity]; } else if (size == capacity) { capacity += capacity >> 1 ; int [] newArray = new int [capacity]; System.arraycopy(array, 0 , newArray, 0 , size); array = newArray; } } public int remove (int index) { int removed = array[index]; if (index < size - 1 ) { System.arraycopy(array, index + 1 , array, index, size - index - 1 ); } size--; return removed; } public int get (int index) { return array[index]; } public void foreach (Consumer<Integer> consumer) { for (int i = 0 ; i < size; i++) { consumer.accept(array[i]); } } @Override public Iterator<Integer> iterator () { return new Iterator <Integer>() { int i = 0 ; @Override public boolean hasNext () { return i < size; } @Override public Integer next () { return array[i++]; } }; } public IntStream stream () { return IntStream.of(Arrays.copyOfRange(array, 0 , size)); } }
这些方法实现,都简化了 index 的有效性判断,假设输入的 index 都是合法的
插入或删除性能
头部位置,时间复杂度是 O ( n ) O(n) O ( n )
中间位置,时间复杂度是 O ( n ) O(n) O ( n )
尾部位置,时间复杂度是 O ( 1 ) O(1) O ( 1 ) (均摊来说)
3) 二维数组
int [][] array = { {11 , 12 , 13 , 14 , 15 }, {21 , 22 , 23 , 24 , 25 }, {31 , 32 , 33 , 34 , 35 }, };
内存图如下
更一般的,对一个二维数组 A r r a y [ m ] [ n ] Array[m][n] A r r a y [ m ] [ n ]
m m m 是外层数组的长度,可以看作 row 行
n n n 是内层数组的长度,可以看作 column 列
当访问 A r r a y [ i ] [ j ] Array[i][j] A r r a y [ i ] [ j ] ,0 ≤ i < m , 0 ≤ j < n 0\leq i \lt m, 0\leq j \lt n 0 ≤ i < m , 0 ≤ j < n 时,就相当于
先找到第 i i i 个内层数组(行)
再找到此内层数组中第 j j j 个元素(列)
小测试
Java 环境下(不考虑类指针和引用压缩,此为默认情况),有下面的二维数组
byte [][] array = { {11 , 12 , 13 , 14 , 15 }, {21 , 22 , 23 , 24 , 25 }, {31 , 32 , 33 , 34 , 35 }, };
已知 array 对象 起始地址是 0x1000,那么 23 这个元素的地址是什么?
答:
起始地址 0x1000
外层数组大小:16字节对象头 + 3元素 * 每个引用4字节 + 4 对齐字节 = 32 = 0x20
第一个内层数组大小:16字节对象头 + 5元素 * 每个byte1字节 + 3 对齐字节 = 24 = 0x18
第二个内层数组,16字节对象头 = 0x10,待查找元素索引为 2
最后结果 = 0x1000 + 0x20 + 0x18 + 0x10 + 2*1 = 0x104a
4) 局部性原理
这里只讨论空间局部性
cpu 读取内存(速度慢)数据后,会将其放入高速缓存(速度快)当中,如果后来的计算再用到此数据,在缓存中能读到的话,就不必读内存了
缓存的最小存储单位是缓存行(cache line),一般是 64 bytes,一次读的数据少了不划算啊,因此最少读 64 bytes 填满一个缓存行,因此读入某个数据时也会读取其临近的数据 ,这就是所谓空间局部性
对效率的影响
比较下面 ij 和 ji 两个方法的执行效率
int rows = 1000000 ;int columns = 14 ;int [][] a = new int [rows][columns];StopWatch sw = new StopWatch ();sw.start("ij" ); ij(a, rows, columns); sw.stop(); sw.start("ji" ); ji(a, rows, columns); sw.stop(); System.out.println(sw.prettyPrint());
ij 方法
public static void ij (int [][] a, int rows, int columns) { long sum = 0L ; for (int i = 0 ; i < rows; i++) { for (int j = 0 ; j < columns; j++) { sum += a[i][j]; } } System.out.println(sum); }
ji 方法
public static void ji (int [][] a, int rows, int columns) { long sum = 0L ; for (int j = 0 ; j < columns; j++) { for (int i = 0 ; i < rows; i++) { sum += a[i][j]; } } System.out.println(sum); }
执行结果
0 0 StopWatch '': running time = 96283300 ns --------------------------------------------- ns % Task name --------------------------------------------- 016196200 017% ij 080087100 083% ji
可以看到 ij 的效率比 ji 快很多,为什么呢?
缓存是有限的,当新数据来了后,一些旧的缓存行数据就会被覆盖
如果不能充分利用缓存的数据,就会造成效率低下
以 ji 执行为例,第一次内循环要读入 [ 0 , 0 ] [0,0] [ 0 , 0 ] 这条数据,由于局部性原理,读入 [ 0 , 0 ] [0,0] [ 0 , 0 ] 的同时也读入了 [ 0 , 1 ] . . . [ 0 , 13 ] [0,1] ... [0,13] [ 0 , 1 ] . . . [ 0 , 1 3 ] ,如图所示
但很遗憾,第二次内循环要的是 [ 1 , 0 ] [1,0] [ 1 , 0 ] 这条数据,缓存中没有,于是再读入了下图的数据
这显然是一种浪费,因为 [ 0 , 1 ] . . . [ 0 , 13 ] [0,1] ... [0,13] [ 0 , 1 ] . . . [ 0 , 1 3 ] 包括 [ 1 , 1 ] . . . [ 1 , 13 ] [1,1] ... [1,13] [ 1 , 1 ] . . . [ 1 , 1 3 ] 这些数据虽然读入了缓存,却没有及时用上,而缓存的大小是有限的,等执行到第九次内循环时
缓存的第一行数据已经被新的数据 [ 8 , 0 ] . . . [ 8 , 13 ] [8,0] ... [8,13] [ 8 , 0 ] . . . [ 8 , 1 3 ] 覆盖掉了,以后如果再想读,比如 [ 0 , 1 ] [0,1] [ 0 , 1 ] ,又得到内存去读了
同理可以分析 ij 函数则能充分利用局部性原理加载到的缓存数据
举一反三
I/O 读写时同样可以体现局部性原理
数组可以充分利用局部性原理,那么链表呢?
答:链表不行,因为链表的元素并非相邻存储
5) 越界检查
java 中对数组元素的读写都有越界检查,类似于下面的代码
bool is_within_bounds (int index) const { return 0 <= index && index < length (); }
代码位置:openjdk\src\hotspot\share\oops\arrayOop.hpp
只不过此检查代码,不需要由程序员自己来调用,JVM 会帮我们调用
习题
E01. 合并有序数组 - 对应 Leetcode 88
将数组内两个区间内的有序元素合并
例
可以视作两个有序区间
[1, 5, 6] 和 [2, 4, 10, 11]
合并后,结果仍存储于原有空间
方法1
递归
merge(left=[1 ,5 ,6 ],right=[2 ,4 ,10 ,11 ],a2=[]){ merge(left=[5 ,6 ],right=[2 ,4 ,10 ,11 ],a2=[1 ]){ merge(left=[5 ,6 ],right=[4 ,10 ,11 ],a2=[1 ,2 ]){ merge(left=[5 ,6 ],right=[10 ,11 ],a2=[1 ,2 ,4 ]){ merge(left=[6 ],right=[10 ,11 ],a2=[1 ,2 ,4 ,5 ]){ merge(left=[],right=[10 ,11 ],a2=[1 ,2 ,4 ,5 ,6 ]){ } } } } } }
代码
public static void merge (int [] a1, int i, int iEnd, int j, int jEnd, int [] a2, int k) { if (i > iEnd) { System.arraycopy(a1, j, a2, k, jEnd - j + 1 ); return ; } if (j > jEnd) { System.arraycopy(a1, i, a2, k, iEnd - i + 1 ); return ; } if (a1[i] < a1[j]) { a2[k] = a1[i]; merge(a1, i + 1 , iEnd, j, jEnd, a2, k + 1 ); } else { a2[k] = a1[j]; merge(a1, i, iEnd, j + 1 , jEnd, a2, k + 1 ); } }
测试
int [] a1 = {1 , 5 , 6 , 2 , 4 , 10 , 11 };int [] a2 = new int [a1.length];merge(a1, 0 , 2 , 3 , 6 , a2, 0 );
方法2
代码
public static void merge (int [] a1, int i, int iEnd, int j, int jEnd, int [] a2) { int k = i; while (i <= iEnd && j <= jEnd) { if (a1[i] < a1[j]) { a2[k] = a1[i]; i++; } else { a2[k] = a1[j]; j++; } k++; } if (i > iEnd) { System.arraycopy(a1, j, a2, k, jEnd - j + 1 ); } if (j > jEnd) { System.arraycopy(a1, i, a2, k, iEnd - i + 1 ); } }
测试
int [] a1 = {1 , 5 , 6 , 2 , 4 , 10 , 11 };int [] a2 = new int [a3.length];merge(a1, 0 , 2 , 3 , 6 , a2);
2.2 链表
1) 概述
定义
在计算机科学中,链表是数据元素的线性集合,其每个元素都指向下一个元素,元素存储上并不连续
In computer science, a linked list is a linear collection of data elements whose order is not given by their physical placement in memory. Instead, each element points to the next.
可以分类为
循环链表,通常的链表尾节点 tail 指向的都是 null,而循环链表的 tail 指向的是头节点 head
链表内还有一种特殊的节点称为哨兵(Sentinel)节点,也叫做哑元( Dummy)节点,它不存储数据,通常用作头尾,用来简化边界判断,如下图所示
随机访问性能
根据 index 查找,时间复杂度 O ( n ) O(n) O ( n )
插入或删除性能
起始位置:O ( 1 ) O(1) O ( 1 )
结束位置:如果已知 tail 尾节点是 O ( 1 ) O(1) O ( 1 ) ,不知道 tail 尾节点是 O ( n ) O(n) O ( n )
中间位置:根据 index 查找时间 + O ( 1 ) O(1) O ( 1 )
2) 单向链表
根据单向链表的定义,首先定义一个存储 value 和 next 指针的类 Node,和一个描述头部节点的引用
public class SinglyLinkedList { private Node head; private static class Node { int value; Node next; public Node (int value, Node next) { this .value = value; this .next = next; } } }
Node 定义为内部类,是为了对外隐藏 实现细节,没必要让类的使用者关心 Node 结构
定义为 static 内部类,是因为 Node 不需要 与 SinglyLinkedList 实例相关,多个 SinglyLinkedList实例能共用 Node 类定义
头部添加
public class SinglyLinkedList { public void addFirst (int value) { this .head = new Node (value, this .head); } }
如果 this.head == null,新增节点指向 null,并作为新的 this.head
如果 this.head != null,新增节点指向原来的 this.head,并作为新的 this.head
while 遍历
public class SinglyLinkedList { public void loop () { Node curr = this .head; while (curr != null ) { curr = curr.next; } } }
for 遍历
public class SinglyLinkedList { public void loop () { for (Node curr = this .head; curr != null ; curr = curr.next) { } } }
以上两种遍历都可以把要做的事 以 Consumer 函数的方式传递进来
Consumer 的规则是一个参数 ,无返回值 ,因此像 System.out::println 方法等都是 Consumer
调用 Consumer 时,将当前节点 curr.value 作为参数传递给它
迭代器遍历
public class SinglyLinkedList implements Iterable <Integer> { private class NodeIterator implements Iterator <Integer> { Node curr = head; public boolean hasNext () { return curr != null ; } public Integer next () { int value = curr.value; curr = curr.next; return value; } } public Iterator<Integer> iterator () { return new NodeIterator (); } }
hasNext 用来判断是否还有必要调用 next
next 做两件事
NodeIterator 要定义为非 static 内部类 ,是因为它与 SinglyLinkedList 实例相关,是对某个 SinglyLinkedList 实例的迭代
递归遍历
public class SinglyLinkedList implements Iterable <Integer> { public void loop () { recursion(this .head); } private void recursion (Node curr) { if (curr == null ) { return ; } recursion(curr.next); } }
尾部添加
public class SinglyLinkedList { private Node findLast () { if (this .head == null ) { return null ; } Node curr; for (curr = this .head; curr.next != null ; ) { curr = curr.next; } return curr; } public void addLast (int value) { Node last = findLast(); if (last == null ) { addFirst(value); return ; } last.next = new Node (value, null ); } }
注意,找最后一个节点,终止条件是 curr.next == null
分成两个方法是为了代码清晰,而且 findLast() 之后还能复用
尾部添加多个
public class SinglyLinkedList { public void addLast (int first, int ... rest) { Node sublist = new Node (first, null ); Node curr = sublist; for (int value : rest) { curr.next = new Node (value, null ); curr = curr.next; } Node last = findLast(); if (last == null ) { this .head = sublist; return ; } last.next = sublist; } }
根据索引获取
public class SinglyLinkedList { private Node findNode (int index) { int i = 0 ; for (Node curr = this .head; curr != null ; curr = curr.next, i++) { if (index == i) { return curr; } } return null ; } private IllegalArgumentException illegalIndex (int index) { return new IllegalArgumentException (String.format("index [%d] 不合法%n" , index)); } public int get (int index) { Node node = findNode(index); if (node != null ) { return node.value; } throw illegalIndex(index); } }
插入
public class SinglyLinkedList { public void insert (int index, int value) { if (index == 0 ) { addFirst(value); return ; } Node prev = findNode(index - 1 ); if (prev == null ) { throw illegalIndex(index); } prev.next = new Node (value, prev.next); } }
删除
public class SinglyLinkedList { public void remove (int index) { if (index == 0 ) { if (this .head != null ) { this .head = this .head.next; return ; } else { throw illegalIndex(index); } } Node prev = findNode(index - 1 ); Node curr; if (prev != null && (curr = prev.next) != null ) { prev.next = curr.next; } else { throw illegalIndex(index); } } }
第一个 if 块对应着 removeFirst 情况
最后一个 if 块对应着至少得两个节点的情况
3) 单向链表(带哨兵)
观察之前单向链表的实现,发现每个方法内几乎都有判断是不是 head 这样的代码,能不能简化呢?
用一个不参与数据存储的特殊 Node 作为哨兵,它一般被称为哨兵或哑元,拥有哨兵节点的链表称为带头链表
public class SinglyLinkedListSentinel { private Node head = new Node (Integer.MIN_VALUE, null ); }
加入哨兵节点后,代码会变得比较简单,先看几个工具方法
public class SinglyLinkedListSentinel { private Node findNode (int index) { int i = -1 ; for (Node curr = this .head; curr != null ; curr = curr.next, i++) { if (i == index) { return curr; } } return null ; } private Node findLast () { Node curr; for (curr = this .head; curr.next != null ; ) { curr = curr.next; } return curr; } }
findNode 与之前类似,只是 i 初始值设置为 -1 对应哨兵,实际传入的 index 也是 [ − 1 , ∞ ) [-1, \infty) [ − 1 , ∞ )
findLast 绝不会返回 null 了,就算没有其它节点,也会返回哨兵作为最后一个节点
这样,代码简化为
public class SinglyLinkedListSentinel { public void addLast (int value) { Node last = findLast(); last.next = new Node (value, null ); } public void insert (int index, int value) { Node prev = findNode(index - 1 ); if (prev != null ) { prev.next = new Node (value, prev.next); } else { throw illegalIndex(index); } } public void remove (int index) { Node prev = findNode(index - 1 ); Node curr; if (prev != null && (curr = prev.next) != null ) { prev.next = curr.next; } else { throw illegalIndex(index); } } public void addFirst (int value) { this .head.next = new Node (value, this .head.next); } }
对于删除,前面说了【最后一个 if 块对应着至少得两个节点的情况】,现在有了哨兵,就凑足了两个节点
4) 双向链表(带哨兵)
public class DoublyLinkedListSentinel implements Iterable <Integer> { private final Node head; private final Node tail; public DoublyLinkedListSentinel () { head = new Node (null , 666 , null ); tail = new Node (null , 888 , null ); head.next = tail; tail.prev = head; } private Node findNode (int index) { int i = -1 ; for (Node p = head; p != tail; p = p.next, i++) { if (i == index) { return p; } } return null ; } public void addFirst (int value) { insert(0 , value); } public void removeFirst () { remove(0 ); } public void addLast (int value) { Node prev = tail.prev; Node added = new Node (prev, value, tail); prev.next = added; tail.prev = added; } public void removeLast () { Node removed = tail.prev; if (removed == head) { throw illegalIndex(0 ); } Node prev = removed.prev; prev.next = tail; tail.prev = prev; } public void insert (int index, int value) { Node prev = findNode(index - 1 ); if (prev == null ) { throw illegalIndex(index); } Node next = prev.next; Node inserted = new Node (prev, value, next); prev.next = inserted; next.prev = inserted; } public void remove (int index) { Node prev = findNode(index - 1 ); if (prev == null ) { throw illegalIndex(index); } Node removed = prev.next; if (removed == tail) { throw illegalIndex(index); } Node next = removed.next; prev.next = next; next.prev = prev; } private IllegalArgumentException illegalIndex (int index) { return new IllegalArgumentException ( String.format("index [%d] 不合法%n" , index)); } @Override public Iterator<Integer> iterator () { return new Iterator <Integer>() { Node p = head.next; @Override public boolean hasNext () { return p != tail; } @Override public Integer next () { int value = p.value; p = p.next; return value; } }; } static class Node { Node prev; int value; Node next; public Node (Node prev, int value, Node next) { this .prev = prev; this .value = value; this .next = next; } } }
5) 环形链表(带哨兵)
双向环形链表带哨兵,这时哨兵既作为头,也作为尾
参考实现
public class DoublyLinkedListSentinel implements Iterable <Integer> { @Override public Iterator<Integer> iterator () { return new Iterator <>() { Node p = sentinel.next; @Override public boolean hasNext () { return p != sentinel; } @Override public Integer next () { int value = p.value; p = p.next; return value; } }; } static class Node { Node prev; int value; Node next; public Node (Node prev, int value, Node next) { this .prev = prev; this .value = value; this .next = next; } } private final Node sentinel = new Node (null , -1 , null ); public DoublyLinkedListSentinel () { sentinel.next = sentinel; sentinel.prev = sentinel; } public void addFirst (int value) { Node next = sentinel.next; Node prev = sentinel; Node added = new Node (prev, value, next); prev.next = added; next.prev = added; } public void addLast (int value) { Node prev = sentinel.prev; Node next = sentinel; Node added = new Node (prev, value, next); prev.next = added; next.prev = added; } public void removeFirst () { Node removed = sentinel.next; if (removed == sentinel) { throw new IllegalArgumentException ("非法" ); } Node a = sentinel; Node b = removed.next; a.next = b; b.prev = a; } public void removeLast () { Node removed = sentinel.prev; if (removed == sentinel) { throw new IllegalArgumentException ("非法" ); } Node a = removed.prev; Node b = sentinel; a.next = b; b.prev = a; } public void removeByValue (int value) { Node removed = findNodeByValue(value); if (removed != null ) { Node prev = removed.prev; Node next = removed.next; prev.next = next; next.prev = prev; } } private Node findNodeByValue (int value) { Node p = sentinel.next; while (p != sentinel) { if (p.value == value) { return p; } p = p.next; } return null ; } }
习题
E01. 反转单向链表-Leetcode 206
对应力扣题目 206. 反转链表 - 力扣(LeetCode)
输入:head = [1,2,3,4,5] 输出:[5,4,3,2,1] 输入:[1,2] 输出:[2,1] 输入:[] 输出:[]
方法1
构造一个新链表,从旧链表 依次拿到每个节点,创建新节点添加至新链表 头部,完成后新链表即是倒序的
public ListNode reverseList (ListNode o1) { ListNode n1 = null ; ListNode p = o1; while (p != null ) { n1 = new ListNode (p.val, n1); p = p.next; } return n1; }
评价:简单直白,就是得新创建节点对象
方法2
与方法1 类似,构造一个新链表,从旧链表头部 移除节点,添加到新链表头部 ,完成后新链表即是倒序的,区别在于原题目未提供节点外层的容器类,这里提供一个,另外一个区别是并不去构造新节点
static class List { ListNode head; public List (ListNode head) { this .head = head; } public ListNode removeFirst () { ListNode first = head; if (first != null ) { head = first.next; } return first; } public void addFirst (ListNode first) { first.next = head; head = first; } }
代码
public ListNode reverseList (ListNode head) { List list1 = new List (head); List list2 = new List (null ); ListNode first; while ((first = list1.removeFirst()) != null ) { list2.addFirst(first); } return list2.head; }
评价:更加面向对象,如果实际写代码而非刷题,更多会这么做
方法3
递归,在归 时让 5 → 4 5 \rightarrow 4 5 → 4 ,4 → 3 4 \rightarrow 3 4 → 3 …
首先,写一个递归方法,返回值用来拿到最后一个节点
public ListNode reverseList (ListNode p) { if (p == null || p.next == null ) { return p; } ListNode last = reverseList(p.next); return last; }
注意1:递归终止条件是 curr.next == null,目的是到最后一个节点就结束递归,与之前递归遍历不一样
注意2:需要考虑空链表即 p == null 的情况
可以先测试一下
ListNode o5 = new ListNode (5 , null );ListNode o4 = new ListNode (4 , o5);ListNode o3 = new ListNode (3 , o4);ListNode o2 = new ListNode (2 , o3);ListNode o1 = new ListNode (1 , o2);ListNode n1 = new E01Leetcode206 ().reverseList(o1);System.out.println(n1);
会打印
下面为伪码 调用过程,假设节点分别是 1 → 2 → 3 → 4 → 5 → n u l l 1 \rightarrow 2 \rightarrow 3 \rightarrow 4 \rightarrow 5 \rightarrow null 1 → 2 → 3 → 4 → 5 → n u l l ,先忽略返回值
reverseList(ListNode p = 1 ) { reverseList(ListNode p = 2 ) { reverseList(ListNode p = 3 ) { reverseList(ListNode p = 4 ) { reverseList(ListNode p = 5 ) { if (p == null || p.next == null ) { return p; } } } } } }
接下来,从 p = 4 开始,要让 5 → 4 5 \rightarrow 4 5 → 4 ,4 → 3 4 \rightarrow 3 4 → 3 …
reverseList(ListNode p = 1 ) { reverseList(ListNode p = 2 ) { reverseList(ListNode p = 3 ) { reverseList(ListNode p = 4 ) { reverseList(ListNode p = 5 ) { if (p == null || p.next == null ) { return p; } } } } } }
最终代码为:
public ListNode reverseList (ListNode p) { if (p == null || p.next == null ) { return p; } ListNode last = reverseList(p.next); p.next.next = p; p.next = null ; return last; }
Q:为啥不能在递 的过程中倒序?
A:比如
$ 1 \rightarrow 2 \rightarrow 3 $ 如果递的过程中让 2 → 1 2 \rightarrow 1 2 → 1 那么此时 2 → 3 2 \rightarrow 3 2 → 3 就被覆盖,不知道接下来递给谁
而归的时候让 3 → 2 3 \rightarrow 2 3 → 2 不会影响上一层的 1 → 2 1 \rightarrow 2 1 → 2
评价:单向链表没有 prev 指针,但利用递归的特性【记住了】链表每次调用时相邻两个节点是谁
方法4
从链表每次拿到第二个节点,将其从链表断开,插入头部,直至它为 null 结束
设置指针 o1(旧头)、n1(新头)、o2(旧老二),分别指向第一,第一,第二节点
n 1 o 1 1 → o 2 2 → 3 → 4 → 5 → n u l l \frac{n1 \ o1}{1} \rightarrow \frac{o2}{2} \rightarrow 3 \rightarrow 4 \rightarrow 5 \rightarrow null 1 n 1 o 1 → 2 o 2 → 3 → 4 → 5 → n u l l
将 o2 节点从链表断开,即 o1 节点指向第三节点
$ \frac{n1 \ o1}{1} \rightarrow 3 \rightarrow 4 \rightarrow 5 \rightarrow null$ ,o 2 2 \frac{o2}{2} 2 o 2
o2 节点链入链表头部,即
o 2 2 → n 1 o 1 1 → 3 → 4 → 5 → n u l l \frac{o2}{2} \rightarrow \frac{n1 \ o1}{1} \rightarrow 3 \rightarrow 4 \rightarrow 5 \rightarrow null 2 o 2 → 1 n 1 o 1 → 3 → 4 → 5 → n u l l
n1 指向 o2
n 1 o 2 2 → o 1 1 → 3 → 4 → 5 → n u l l \frac{n1 \ o2}{2} \rightarrow \frac{o1}{1} \rightarrow 3 \rightarrow 4 \rightarrow 5 \rightarrow null 2 n 1 o 2 → 1 o 1 → 3 → 4 → 5 → n u l l
o2 指向 o1 的下一个节点,即
n 1 2 → o 1 1 → o 2 3 → 4 → 5 → n u l l \frac{n1}{2} \rightarrow \frac{o1}{1} \rightarrow \frac{o2}{3} \rightarrow 4 \rightarrow 5 \rightarrow null 2 n 1 → 1 o 1 → 3 o 2 → 4 → 5 → n u l l
重复以上 2 ∼ 5 2\sim5 2 ∼ 5 步,直到 o2 指向 null
还应当考虑边界条件,即链表中不满两个元素时,无需走以上逻辑
参考答案
public ListNode reverseList (ListNode o1) { if (o1 == null || o1.next == null ) { return o1; } ListNode o2 = o1.next; ListNode n1 = o1; while (o2 != null ) { o1.next = o2.next; o2.next = n1; n1 = o2; o2 = o1.next; } return n1; }
方法5
要点:把链表分成两部分,思路就是不断从链表2的头,往链表1的头搬移
n1 指向 null,代表新链表 一开始没有元素,o1 指向原链表 的首节点
n 1 n u l l \frac{n1}{null} n u l l n 1 ,o 1 1 → 2 → 3 → 4 → 5 → n u l l \frac{o1}{1} \rightarrow 2 \rightarrow 3 \rightarrow 4 \rightarrow 5 \rightarrow null 1 o 1 → 2 → 3 → 4 → 5 → n u l l
开始循环,o2 指向原链表 次节点
n 1 n u l l \frac{n1}{null} n u l l n 1 ,o 1 1 → o 2 2 → 3 → 4 → 5 → n u l l \frac{o1}{1} \rightarrow \frac{o2}{2} \rightarrow 3 \rightarrow 4 \rightarrow 5 \rightarrow null 1 o 1 → 2 o 2 → 3 → 4 → 5 → n u l l
搬移
o 1 1 → n 1 n u l l \frac{o1}{1} \rightarrow \frac{n1}{null} 1 o 1 → n u l l n 1 , o 2 2 → 3 → 4 → 5 → n u l l \frac{o2}{2} \rightarrow 3 \rightarrow 4 \rightarrow 5 \rightarrow null 2 o 2 → 3 → 4 → 5 → n u l l
指针复位
n 1 1 → n u l l \frac{n1}{1} \rightarrow null 1 n 1 → n u l l , o 1 o 2 2 → 3 → 4 → 5 → n u l l \frac{o1 \ o2}{2} \rightarrow 3 \rightarrow 4 \rightarrow 5 \rightarrow null 2 o 1 o 2 → 3 → 4 → 5 → n u l l
重复 2 ∼ 4 2\sim4 2 ∼ 4 步
当 o1 = null 时退出循环
参考答案
public ListNode reverseList (ListNode o1) { if (o1 == null || o1.next == null ) { return o1; } ListNode n1 = null ; while (o1 != null ) { ListNode o2 = o1.next; o1.next = n1; n1 = o1; o1 = o2; } return n1; }
评价:本质上与方法2 相同,只是方法2更为面向对象
E02. 根据值删除节点-Leetcode 203
例如
输入:head = [1,2,6,3,6], val = 6 输出:[1,2,3] 输入:head = [], val = 1 输出:[] 输入:head = [7,7,7,7], val = 7 输出:[]
方法1
图中 s 代表 sentinel 哨兵(如果不加哨兵,则删除第一个节点要特殊处理),例如要删除 6
p1 p2 s -> 1 -> 2 -> 6 -> 3 -> 6 -> null
p1 p2 s -> 1 -> 2 -> 6 -> 3 -> 6 -> null p1 p2 s -> 1 -> 2 -> 6 -> 3 -> 6 -> null
p2 == 6,删除它,注意 p1 此时保持不变,p2 后移
p1 p2 s -> 1 -> 2 -> 3 -> 6 -> null
p1 p2 s -> 1 -> 2 -> 3 -> 6 -> null
p2 == 6,删除它,注意 p1 此时保持不变,p2 后移
p1 p2 s -> 1 -> 2 -> 3 -> null
最后代码
public ListNode removeElements (ListNode head, int val) { ListNode sentinel = new ListNode (-1 , head); ListNode p1 = sentinel; ListNode p2; while ((p2 = p1.next) != null ) { if (p2.val == val) { p1.next = p2.next; } else { p1 = p1.next; } } return sentinel.next; }
方法2
思路,递归函数负责返回:从当前节点(我)开始,完成删除的子链表
若我与 v 相等,应该返回下一个节点递归结果
若我与 v 不等,应该返回我,但我的 next 应该更新(让我能带上后续删过的子链表)
removeElements(ListNode p=1 , int v=6 ){ 1. next=removeElements(ListNode p=2 , int v=6 ){ 2. next=removeElements(ListNode p=6 , int v=6 ){ removeElements(ListNode p=3 , int v=6 ){ 3. next=removeElements(ListNode p=6 , int v=6 ){ removeElements(ListNode p=null , int v=6 ){ return null } } return 3 } } return 2 } return 1 }
代码
public ListNode removeElements (ListNode head, int val) { if (head == null ) { return null ; } if (head.val == val) { return removeElements(head.next, val); } else { head.next = removeElements(head.next, val); return head; } }
E03. 删除倒数节点-Leetcode 19
例如
输入:head = [1,2,3,4,5], n = 2 输出:[1,2,3,5] 输入:head = [1], n = 1 输出:[] 输入:head = [1,2], n = 1 输出:[1]
另外题目提示
方法1
思路,写一个递归函数,用来返回下一个节点的倒数序号
recursion(ListNode p=1 , int n=2 ) { recursion(ListNode p=2 , int n=2 ) { recursion(ListNode p=3 , int n=2 ) { recursion(ListNode p=4 , int n=2 ) { recursion(ListNode p=5 , int n=2 ) { recursion(ListNode p=null , int n=2 ) { return 0 ; } return 1 ; } return 2 ; } if (返回值 == n == 2 ) { } return 3 ; } return 4 ; } return 5 ; }
但上述代码有一个问题,就是若删除的是第一个节点,它没有上一个节点,因此可以加一个哨兵来解决
代码
public ListNode removeNthFromEnd (ListNode head, int n) { ListNode sentinel = new ListNode (-1 , head); recursion(sentinel, n); return sentinel.next; } public int recursion (ListNode p, int n) { if (p == null ) { return 0 ; } int nth = recursion(p.next, n); if (nth == n) { p.next = p.next.next; } return nth + 1 ; }
Q:p.next.next 不怕空指针吗?
A:
p 是待删除节点的上一个节点,如果能递归回到 p,那么 p.next 肯定有值,不会是 null
且题目说明了 n >=1,不会因为 nth == 0 而让 p.next 指向最后的 null
方法2
快慢指针,p1 指向待删节点的上一个,p2 先走 n + 1 步
i=0 p2 s -> 1 -> 2 -> 3 -> 4 -> 5 -> null i=1 p2 s -> 1 -> 2 -> 3 -> 4 -> 5 -> null i=2 p2 s -> 1 -> 2 -> 3 -> 4 -> 5 -> null i=3 从此开始 p1 p2 依次向右平移, 直到 p2 移动到末尾 p1 p2 s -> 1 -> 2 -> 3 -> 4 -> 5 -> null p1 p2 s -> 1 -> 2 -> 3 -> 4 -> 5 -> null
代码
public ListNode removeNthFromEnd (ListNode head, int n) { ListNode s = new ListNode (-1 , head); ListNode p1 = s; ListNode p2 = s; for (int i = 0 ; i < n + 1 ; i++) { p2 = p2.next; } while (p2 != null ) { p1 = p1.next; p2 = p2.next; } p1.next = p1.next.next; return s.next; }
方法3
public ListNode removeNthFromEnd (ListNode head, int n) { Composite c = recursion(head, n); return c.node; } static class Composite { ListNode node; int nth; public Composite (ListNode node, int nth) { this .node = node; this .nth = nth; } } public Composite recursion (ListNode p, int n) { if (p == null ) { return new Composite (null , 1 ); } Composite c = recursion(p.next, n); if (c.nth != n) { p.next = c.node; c.node = p; } c.nth +=1 ; return c; }
E04. 有序链表去重-Leetcode 83
例如
输入:head = [1,1,2] 输出:[1,2] 输入:head = [1,1,2,3,3] 输出:[1,2,3]
注意:重复元素保留一个
方法1
p1 p2 1 -> 1 -> 2 -> 3 -> 3 -> null
p1.val == p2.val 那么删除 p2,注意 p1 此时保持不变
p1 p2 1 -> 2 -> 3 -> 3 -> null
p1.val != p2.val 那么 p1,p2 向后移动
p1 p2 1 -> 2 -> 3 -> 3 -> null p1 p2 1 -> 2 -> 3 -> 3 -> null
代码
public ListNode deleteDuplicates (ListNode head) { if (head == null || head.next == null ) { return head; } ListNode p1 = head; ListNode p2; while ((p2 = p1.next) != null ) { if (p1.val == p2.val) { p1.next = p2.next; } else { p1 = p1.next; } } return head; }
方法2
递归函数负责返回:从当前节点(我)开始,完成去重的链表
若我与 next 重复,返回 next
若我与 next 不重复,返回我,但 next 应当更新
deleteDuplicates(ListNode p=1 ) { deleteDuplicates(ListNode p=1 ) { 1. next=deleteDuplicates(ListNode p=2 ) { 2. next=deleteDuplicates(ListNode p=3 ) { deleteDuplicates(ListNode p=3 ) { return 3 } } return 2 } return 1 } }
代码
public ListNode deleteDuplicates (ListNode p) { if (p == null || p.next == null ) { return p; } if (p.val == p.next.val) { return deleteDuplicates(p.next); } else { p.next = deleteDuplicates(p.next); return p; } }
E05. 有序链表去重-Leetcode 82
例如
输入:head = [1,2,3,3,4,4,5] 输出:[1,2,5] 输入:head = [1,1,1,2,3] 输出:[2,3]
注意:重复元素一个不留
方法1
递归函数负责返回:从当前节点(我)开始,完成去重的链表
若我与 next 重复,一直找到下一个不重复的节点,以它的返回结果为准
若我与 next 不重复,返回我,同时更新 next
deleteDuplicates(ListNode p = 1 ) { deleteDuplicates(ListNode p = 1 ) { deleteDuplicates(ListNode p = 1 ) { deleteDuplicates(ListNode p = 2 ) { 2. next=deleteDuplicates(ListNode p = 3 ) { return 3 } return 2 } } } }
代码
public ListNode deleteDuplicates (ListNode p) { if (p == null || p.next == null ) { return p; } if (p.val == p.next.val) { ListNode x = p.next.next; while (x != null && x.val == p.val) { x = x.next; } return deleteDuplicates(x); } else { p.next = deleteDuplicates(p.next); return p; } }
方法2
p1 是待删除的上一个节点,每次循环对比 p2、p3 的值
如果 p2 与 p3 的值重复,那么 p3 继续后移,直到找到与 p2 不重复的节点,p1 指向 p3 完成删除
如果 p2 与 p3 的值不重复,p1,p2,p3 向后平移一位,继续上面的操作
p2 或 p3 为 null 退出循环
p2 为 null 的情况,比如链表为 1 1 1 null
p1 p2 p3 s, 1, 1, 1, 2, 3, null p1 p2 p3 s, 1, 1, 1, 2, 3, null p1 p2 p3 s, 1, 1, 1, 2, 3, null p1 p3 s, 2, 3, null p1 p2 p3 s, 2, 3, null p1 p2 p3 s, 2, 3, null
代码
public ListNode deleteDuplicates (ListNode head) { if (head == null || head.next == null ) { return head; } ListNode s = new ListNode (-1 , head); ListNode p1 = s; ListNode p2; ListNode p3; while ((p2 = p1.next) != null && (p3 = p2.next) != null ) { if (p2.val == p3.val) { while ((p3 = p3.next) != null && p3.val == p2.val) { } p1.next = p3; } else { p1 = p1.next; } } return s.next; }
E06. 合并有序链表-Leetcode 21
例
输入:l1 = [1,2,4], l2 = [1,3,4] 输出:[1,1,2,3,4,4] 输入:l1 = [], l2 = [] 输出:[] 输入:l1 = [], l2 = [0] 输出:[0]
方法1
谁小,把谁链给 p,p 和小的都向后平移一位
当 p1、p2 有一个为 null,退出循环,把不为 null 的链给 p
p1 1 3 8 9 null p2 2 4 null p s null
代码
public ListNode mergeTwoLists (ListNode p1, ListNode p2) { ListNode s = new ListNode (-1 , null ); ListNode p = s; while (p1 != null && p2 != null ) { if (p1.val < p2.val) { p.next = p1; p1 = p1.next; } else { p.next = p2; p2 = p2.next; } p = p.next; } if (p1 != null ) { p.next = p1; } if (p2 != null ) { p.next = p2; } return s.next; }
方法2
递归函数应该返回
更小的那个链表节点,并把它剩余节点与另一个链表再次递归
返回之前,更新此节点的 next
mergeTwoLists(p1=[1 ,3 ,8 ,9 ], p2=[2 ,4 ]) { 1. next=mergeTwoLists(p1=[3 ,8 ,9 ], p2=[2 ,4 ]) { 2. next=mergeTwoLists(p1=[3 ,8 ,9 ], p2=[4 ]) { 3. next=mergeTwoLists(p1=[8 ,9 ], p2=[4 ]) { 4. next=mergeTwoLists(p1=[8 ,9 ], p2=null ) { return [8 ,9 ] } return 4 } return 3 } return 2 } return 1 }
E07. 合并多个有序链表-Leetcode 23
例
输入:lists = [[1,4,5],[1,3,4],[2,6]] 输出:[1,1,2,3,4,4,5,6] 解释:链表数组如下: [ 1->4->5, 1->3->4, 2->6 ] 将它们合并到一个有序链表中得到。 1->1->2->3->4->4->5->6
方法1
递归
public ListNode mergeKLists (ListNode[] lists) { if (lists.length == 0 ) { return null ; } return split(lists, 0 , lists.length - 1 ); } public ListNode split (ListNode[] lists, int i, int j) { System.out.println(i + " " + j); if (j == i) { return lists[i]; } int m = (i + j) >>> 1 ; return mergeTwoLists( split(lists, i, m), split(lists, m + 1 , j) ); }
还可以用优先级队列求解,这个放在后面讲
E08. 查找链表中间节点-Leetcode 876
例如
输入:[1,2,3,4,5] 输出:此列表中的结点 3 (序列化形式:[3,4,5]) 输入:[1,2,3,4,5,6] 输出:此列表中的结点 4 (序列化形式:[4,5,6])
解法:快慢指针,快指针一次走两步,慢指针一次走一步,当快指针到链表结尾时,慢指针恰好走到链表的一半
public ListNode middleNode (ListNode head) { ListNode p1 = head; ListNode p2 = head; while (p2 != null && p2.next != null ) { p1 = p1.next; p2 = p2.next; p2 = p2.next; } return p1; }
E09. 回文链表-Leetcode 234
所谓回文指正着读、反着读,结果一样,例如
它们都是回文链表,不是回文的例子
[1,2,3,1] --反过来--> [1,3,2,1]
解法
public boolean isPalindrome (ListNode head) { ListNode middle = middle(head); ListNode newHead = reverse(middle); while (newHead != null ) { if (newHead.val != head.val) { return false ; } newHead = newHead.next; head = head.next; } return true ; } private ListNode reverse (ListNode o1) { ListNode n1 = null ; while (o1 != null ) { ListNode o2 = o1.next; o1.next = n1; n1 = o1; o1 = o2; } return n1; } private ListNode middle (ListNode head) { ListNode p1 = head; ListNode p2 = head; while (p2 != null && p2.next != null ) { p1 = p1.next; p2 = p2.next.next; } return p1; }
优化后解法
public boolean isPalindrome (ListNode h1) { if (h1 == null || h1.next == null ) { return true ; } ListNode p1 = h1; ListNode p2 = h1; ListNode n1 = null ; ListNode o1 = h1; while (p2 != null && p2.next != null ) { p1 = p1.next; p2 = p2.next.next; o1.next = n1; n1 = o1; o1 = p1; } if (p2 != null ) { p1 = p1.next; } while (n1 != null ) { if (n1.val != p1.val) { return false ; } p1 = p1.next; n1 = n1.next; } return true ; }
E10. 环形链表-Leetcode 141
本题以及下题,实际是 Floyd’s Tortoise and Hare Algorithm (Floyd 龟兔赛跑算法)
除了 Floyd 判环算法外,还有其它的判环算法,详见 https://en.wikipedia.org/wiki/Cycle_detection
如果链表上存在环,那么在环上以不同速度前进的两个指针必定会在某个时刻相遇。算法分为两个阶段
阶段1
龟一次走一步,兔子一次走两步
当兔子能走到终点时,不存在环
当兔子能追上龟时,可以判断存在环
阶段2
从它们第一次相遇开始,龟回到起点,兔子保持原位不变
龟和兔子一次都走一步
当再次相遇时,地点就是环的入口
为什么呢?
设起点到入口走 a 步(本例是 7),绕环一圈长度为 b(本例是 5),
那么从起点开始,走 a + 绕环 n 圈,都能找到环入口
第一次相遇时
兔走了 a + 绕环 n 圈(本例 2 圈) + k,k 是它们相遇距环入口位置(本例 3,不重要)
龟走了 a + 绕环 n 圈(本例 0 圈) + k,当然它绕的圈数比兔少
兔走的距离是龟的两倍,所以龟走的 = 兔走的 - 龟走的 = 绕环 n 圈
而前面分析过,如果走 a + 绕环 n 圈,都能找到环入口,因此从相遇点开始,再走 a 步,就是环入口
阶段1 参考代码(判断是否有环)
public boolean hasCycle (ListNode head) { ListNode h = head; ListNode t = head; while (h != null && h.next != null ) { t = t.next; h = h.next.next; if (h == t){ return true ; } } return false ; }
E11. 环形链表-Leetcode 142
阶段2 参考代码(找到环入口)
public ListNode detectCycle (ListNode head) { ListNode t = head; ListNode h = head; while (h != null && h.next != null ) { t = t.next; h = h.next.next; if (h == t) { t = head; while (true ) { if (h == t) { return h; } h = h.next; t = t.next; } } } return null ; }
还有一道扩展题目,也可以用判环算法思想来解:就是 287 题,寻找重复数
Ex1. 删除节点-Leetcode 237
这道题目比较简单,留给大家自己练习
例如
输入:head = [4,5,1,9], node = 5 输出:[4,1,9] 输入:head = [4,5,1,9], node = 1 输出:[4,5,9]
注意:被删除的节点不是 末尾节点
参考答案
public class Ex1Leetcode237 { public void deleteNode (ListNode node) { node.val = node.next.val; node.next = node.next.next; } public static void main (String[] args) { ListNode o5 = new ListNode (5 , null ); ListNode o4 = new ListNode (4 , o5); ListNode o3 = new ListNode (3 , o4); ListNode o2 = new ListNode (2 , o3); ListNode o1 = new ListNode (1 , o2); System.out.println(o1); new E0xLeetcode237 ().deleteNode(o3); System.out.println(o1); } }
输出
Ex2. 共尾链表-Leetcode 160
原题叫做相交 链表,个人觉得用共尾 链表更形象些,此题更像是一道脑筋急转弯,留给大家练习
例如,下图的两个链表 [1, 2, 4, 5] 与 [3, 4, 5] 它们中 [4, 5] 是相同的,此时应返回节点 4
非共尾的情况,如下图所示,此时返回 null
思路,称两个链表为 a=[1, 2, 4, 5],b=[3, 4, 5],图中用 N 代表 null
遍历 a,遇到 null 时改道遍历 b
与此同时,遍历 b,遇到 null 时改道遍历 a
在此过程中,如果遇到相同 的节点,即为找寻目标,返回即可,如下图中的第二次出现的 4
相同节点应该比较其引用值 ,图中数字只是为了便于区分
1 2 4 5 N 3 4 5 N 3 4 5 N 1 2 4 5 N
如果两个链表长度相同,则可以更早找到目标,例如 a=[1, 4, 5],b=[3, 4, 5],第一次出现 4 时,即可返回
1 4 5 N 3 4 5 N 3 4 5 N 1 4 5 N
如果是非共尾的情况,如 a=[1, 2, 4],b=[3, 5],可以看到,唯一相等的情况,是遍历到最后那个 N 此时退出循环
1 2 4 N 3 5 N 3 5 N 1 2 4 N
代码
public ListNode getIntersectionNode (ListNode a, ListNode b) { ListNode p1 = a; ListNode p2 = b; while (true ) { if (p1 == p2) { return p1; } if (p1 == null ) { p1 = b; } else { p1 = p1.next; } if (p2 == null ) { p2 = a; } else { p2 = p2.next; } } }
2.3 递归
1) 概述
定义
计算机科学中,递归是一种解决计算问题的方法,其中解决方案取决于同一类问题的更小子集
In computer science, recursion is a method of solving a computational problem where the solution depends on solutions to smaller instances of the same problem.
比如单链表递归遍历的例子:
void f (Node node) { if (node == null ) { return ; } println("before:" + node.value) f(node.next); println("after:" + node.value) }
说明:
自己调用自己,如果说每个函数对应着一种解决方案,自己调用自己意味着解决方案是一样的(有规律的)
每次调用,函数处理的数据会较上次缩减(子集),而且最后会缩减至无需继续递归
内层函数调用(子集处理)完成,外层函数才能算调用完成
原理
假设链表中有 3 个节点,value 分别为 1,2,3,以上代码的执行流程就类似于下面的伪码
void f (Node node = 1 ) { println("before:" + node.value) void f (Node node = 2 ) { println("before:" + node.value) void f (Node node = 3 ) { println("before:" + node.value) void f (Node node = null ) { if (node == null ) { return ; } } println("after:" + node.value) } println("after:" + node.value) } println("after:" + node.value) }
思路
确定能否使用递归求解
推导出递推关系,即父问题与子问题的关系,以及递归的结束条件
例如之前遍历链表的递推关系为
f ( n ) = { 停止 n = n u l l f ( n . n e x t ) n ≠ n u l l f(n) =
\begin{cases}
停止& n = null \\
f(n.next) & n \neq null
\end{cases}
f ( n ) = { 停 止 f ( n . n e x t ) n = n u l l n = n u l l
深入到最里层叫做递
从最里层出来叫做归
在递 的过程中,外层函数内的局部变量(以及方法参数)并未消失,归 的时候还可以用到
2) 单路递归 Single Recursion
E01. 阶乘
用递归方法求阶乘
f ( n ) = { 1 n = 1 n ∗ f ( n − 1 ) n > 1 f(n) =
\begin{cases}
1 & n = 1\\
n * f(n-1) & n > 1
\end{cases}
f ( n ) = { 1 n ∗ f ( n − 1 ) n = 1 n > 1
代码
private static int f (int n) { if (n == 1 ) { return 1 ; } return n * f(n - 1 ); }
拆解伪码 如下,假设 n 初始值为 3
f(int n = 3 ) { return 3 * f(int n = 2 ) { return 2 * f(int n = 1 ) { if (n == 1 ) { return 1 ; } } } }
E02. 反向打印字符串
用递归反向打印字符串,n 为字符在整个字符串 str 中的索引位置
递 :n 从 0 开始,每次 n + 1,一直递到 n == str.length() - 1
归 :从 n == str.length() 开始归,从归打印,自然是逆序的
递推关系
f ( n ) = { 停止 n = s t r . l e n g t h ( ) f ( n + 1 ) 0 ≤ n ≤ s t r . l e n g t h ( ) − 1 f(n) =
\begin{cases}
停止 & n = str.length() \\
f(n+1) & 0 \leq n \leq str.length() - 1
\end{cases}
f ( n ) = { 停 止 f ( n + 1 ) n = s t r . l e n g t h ( ) 0 ≤ n ≤ s t r . l e n g t h ( ) − 1
代码为
public static void reversePrint (String str, int index) { if (index == str.length()) { return ; } reversePrint(str, index + 1 ); System.out.println(str.charAt(index)); }
拆解伪码 如下,假设字符串为 “abc”
void reversePrint (String str, int index = 0 ) { void reversePrint (String str, int index = 1 ) { void reversePrint (String str, int index = 2 ) { void reversePrint (String str, int index = 3 ) { if (index == str.length()) { return ; } } System.out.println(str.charAt(index)); } System.out.println(str.charAt(index)); } System.out.println(str.charAt(index)); }
E03. 二分查找(单路递归)
public static int binarySearch (int [] a, int target) { return recursion(a, target, 0 , a.length - 1 ); } public static int recursion (int [] a, int target, int i, int j) { if (i > j) { return -1 ; } int m = (i + j) >>> 1 ; if (target < a[m]) { return recursion(a, target, i, m - 1 ); } else if (a[m] < target) { return recursion(a, target, m + 1 , j); } else { return m; } }
E04. 冒泡排序(单路递归)
public static void main (String[] args) { int [] a = {3 , 2 , 6 , 1 , 5 , 4 , 7 }; bubble(a, 0 , a.length - 1 ); System.out.println(Arrays.toString(a)); } private static void bubble (int [] a, int low, int high) { if (low == high) { return ; } int j = low; for (int i = low; i < high; i++) { if (a[i] > a[i + 1 ]) { swap(a, i, i + 1 ); j = i; } } bubble(a, low, j); } private static void swap (int [] a, int i, int j) { int t = a[i]; a[i] = a[j]; a[j] = t; }
low 与 high 为未排序范围
j 表示的是未排序的边界,下一次递归时的 high
发生交换,意味着有无序情况
最后一次交换(以后没有无序)时,左侧 i 仍是无序,右侧 i+1 已然有序
视频中讲解的是只考虑 high 边界的情况,参考以上代码,理解在 low … high 范围内的处理方法
E05. 插入排序(单路递归)
public static void main (String[] args) { int [] a = {3 , 2 , 6 , 1 , 5 , 7 , 4 }; insertion(a, 1 , a.length - 1 ); System.out.println(Arrays.toString(a)); } private static void insertion (int [] a, int low, int high) { if (low > high) { return ; } int i = low - 1 ; int t = a[low]; while (i >= 0 && a[i] > i) { a[i + 1 ] = a[i]; i--; } if (i + 1 != low) { a[i + 1 ] = t; } insertion(a, low + 1 , high); }
已排序区域:[0 … i … low-1]
未排序区域:[low … high]
视频中讲解的是只考虑 low 边界的情况,参考以上代码,理解 low-1 … high 范围内的处理方法
扩展:利用二分查找 leftmost 版本,改进寻找插入位置的代码
E06. 约瑟夫问题(单路递归)
n n n 个人排成圆圈,从头开始报数,每次数到第 m m m 个人(m m m 从 1 1 1 开始)杀之,继续从下一个人重复以上过程,求最后活下来的人是谁?
方法1
根据最后的存活者 a 倒推出它在上一轮的索引号
f(n,m)
本轮索引
为了让 a 是这个索引,上一轮应当这样排
规律
f(1,3)
0
x x x a
(0 + 3) % 2
f(2,3)
1
x x x 0 a
(1 + 3) % 3
f(3,3)
1
x x x 0 a
(1 + 3) % 4
f(4,3)
0
x x x a
(0 + 3) % 5
f(5,3)
3
x x x 0 1 2 a
(3 + 3) % 6
f(6,3)
0
x x x a
方法2
设 n 为总人数,m 为报数次数,解返回的是这些人的索引,从0开始
f(n, m)
解
规律
f(1, 3)
0
f(2, 3)
0 1 => 1
3%2=1
f(3, 3)
0 1 2 => 0 1
3%3=0
f(4, 3)
0 1 2 3 => 3 0 1
3%4=3
f(5, 3)
0 1 2 3 4 => 3 4 0 1
3%5=3
f(6, 3)
0 1 2 3 4 5 => 3 4 5 0 1
3%6=3
一. 找出等价函数
规律:下次报数的起点为 k = m % n k = m \% n k = m % n
首次出列人的序号是 k − 1 k-1 k − 1 ,剩下的的 n − 1 n-1 n − 1 个人重新组成约瑟夫环
下次从 k k k 开始数,序号如下
k , k + 1 , . . . , 0 , 1 , k − 2 k,\ k+1, \ ...\ ,\ 0,\ 1,\ k-2 k , k + 1 , . . . , 0 , 1 , k − 2 ,如上例中 3 4 5 0 1 3\ 4\ 5\ 0\ 1 3 4 5 0 1
这个函数称之为 g ( n − 1 , m ) g(n-1,m) g ( n − 1 , m ) ,它的最终结果与 f ( n , m ) f(n,m) f ( n , m ) 是相同的。
二. 找到映射函数
现在想办法找到 g ( n − 1 , m ) g(n-1,m) g ( n − 1 , m ) 与 f ( n − 1 , m ) f(n-1, m) f ( n − 1 , m ) 的对应关系,即
3 → 0 4 → 1 5 → 2 0 → 3 1 → 4 3 \rightarrow 0 \\
4 \rightarrow 1 \\
5 \rightarrow 2 \\
0 \rightarrow 3 \\
1 \rightarrow 4 \\
3 → 0 4 → 1 5 → 2 0 → 3 1 → 4
映射函数为
m a p p i n g ( x ) = { x − k x = [ k . . n − 1 ] x + n − k x = [ 0.. k − 2 ] mapping(x) =
\begin{cases}
x-k & x=[k..n-1] \\
x+n-k & x=[0..k-2]
\end{cases}
m a p p i n g ( x ) = { x − k x + n − k x = [ k . . n − 1 ] x = [ 0 . . k − 2 ]
等价于下面函数
m a p p i n g ( x ) = ( x + n − k ) % n mapping(x) = (x + n - k)\%{n}
m a p p i n g ( x ) = ( x + n − k ) % n
代入测试一下
3 → ( 3 + 6 − 3 ) % 6 → 0 4 → ( 4 + 6 − 3 ) % 6 → 1 5 → ( 5 + 6 − 3 ) % 6 → 2 0 → ( 0 + 6 − 3 ) % 6 → 3 1 → ( 1 + 6 − 3 ) % 6 → 4 3 \rightarrow (3+6-3)\%6 \rightarrow 0 \\
4 \rightarrow (4+6-3)\%6 \rightarrow 1 \\
5 \rightarrow (5+6-3)\%6 \rightarrow 2 \\
0 \rightarrow (0+6-3)\%6 \rightarrow 3 \\
1 \rightarrow (1+6-3)\%6 \rightarrow 4 \\
3 → ( 3 + 6 − 3 ) % 6 → 0 4 → ( 4 + 6 − 3 ) % 6 → 1 5 → ( 5 + 6 − 3 ) % 6 → 2 0 → ( 0 + 6 − 3 ) % 6 → 3 1 → ( 1 + 6 − 3 ) % 6 → 4
综上有
f ( n − 1 , m ) = m a p p i n g ( g ( n − 1 , m ) ) f(n-1,m) = mapping(g(n-1,m))
f ( n − 1 , m ) = m a p p i n g ( g ( n − 1 , m ) )
三. 求逆映射函数
映射函数是根据 x 计算 y,逆映射函数即根据 y 得到 x
m a p p i n g − 1 ( x ) = ( x + k ) % n mapping^{-1}(x) = (x + k)\%n
m a p p i n g − 1 ( x ) = ( x + k ) % n
代入测试一下
0 → ( 0 + 3 ) % 6 → 3 1 → ( 1 + 3 ) % 6 → 4 2 → ( 2 + 3 ) % 6 → 5 3 → ( 3 + 3 ) % 6 → 0 4 → ( 4 + 3 ) % 6 → 1 0 \rightarrow (0+3)\%6 \rightarrow 3 \\
1 \rightarrow (1+3)\%6 \rightarrow 4 \\
2 \rightarrow (2+3)\%6 \rightarrow 5 \\
3 \rightarrow (3+3)\%6 \rightarrow 0 \\
4 \rightarrow (4+3)\%6 \rightarrow 1 \\
0 → ( 0 + 3 ) % 6 → 3 1 → ( 1 + 3 ) % 6 → 4 2 → ( 2 + 3 ) % 6 → 5 3 → ( 3 + 3 ) % 6 → 0 4 → ( 4 + 3 ) % 6 → 1
因此可以求得
g ( n − 1 , m ) = m a p p i n g − 1 ( f ( n − 1 , m ) ) g(n-1,m) = mapping^{-1}(f(n-1,m))
g ( n − 1 , m ) = m a p p i n g − 1 ( f ( n − 1 , m ) )
四. 递推式
代入推导
f ( n , m ) = g ( n − 1 , m ) = m a p p i n g − 1 ( f ( n − 1 , m ) ) = ( f ( n − 1 , m ) + k ) % n = ( f ( n − 1 , m ) + m % n ) % n = ( f ( n − 1 , m ) + m ) % n \begin{aligned}
f(n,m) = \ & g(n-1,m) \\
= \ & mapping^{-1}(f(n-1,m)) \\
= \ & (f(n-1,m) + k) \% n \\
= \ & (f(n-1,m) + m\%n) \% n \\
= \ & (f(n-1,m) + m) \% n \\
\end{aligned}
f ( n , m ) = = = = = g ( n − 1 , m ) m a p p i n g − 1 ( f ( n − 1 , m ) ) ( f ( n − 1 , m ) + k ) % n ( f ( n − 1 , m ) + m % n ) % n ( f ( n − 1 , m ) + m ) % n
最后一步化简是利用了模运算法则
( a + b ) % n = ( a % n + b % n ) % n (a+b)\%n = (a\%n + b\%n) \%n ( a + b ) % n = ( a % n + b % n ) % n 例如
( 6 + 6 ) % 5 = 2 = ( 6 + 6 % 5 ) % 5 (6+6)\%5 = 2 = (6+6\%5)\%5 ( 6 + 6 ) % 5 = 2 = ( 6 + 6 % 5 ) % 5
( 6 + 5 ) % 5 = 1 = ( 6 + 5 % 5 ) % 5 (6+5)\%5 = 1 = (6+5\%5)\%5 ( 6 + 5 ) % 5 = 1 = ( 6 + 5 % 5 ) % 5
( 6 + 4 ) % 5 = 0 = ( 6 + 4 % 5 ) % 5 (6+4)\%5 = 0 = (6+4\%5)\%5 ( 6 + 4 ) % 5 = 0 = ( 6 + 4 % 5 ) % 5
最终递推式
f ( n , m ) = { ( f ( n − 1 , m ) + m ) % n n > 1 0 n = 1 f(n,m) =
\begin{cases}
(f(n-1,m) + m) \% n & n>1\\
0 & n = 1
\end{cases}
f ( n , m ) = { ( f ( n − 1 , m ) + m ) % n 0 n > 1 n = 1
3) 多路递归 Multi Recursion
E01. 斐波那契数列-Leetcode 70
之前的例子是每个递归函数只包含一个自身的调用,这称之为 single recursion
如果每个递归函数例包含多个自身调用,称之为 multi recursion
递推关系
f ( n ) = { 0 n = 0 1 n = 1 f ( n − 1 ) + f ( n − 2 ) n > 1 f(n) =
\begin{cases}
0 & n=0 \\
1 & n=1 \\
f(n-1) + f(n-2) & n>1
\end{cases}
f ( n ) = ⎩ ⎪ ⎪ ⎨ ⎪ ⎪ ⎧ 0 1 f ( n − 1 ) + f ( n − 2 ) n = 0 n = 1 n > 1
下面的表格列出了数列的前几项
F 0
F 1
F 2
F 3
F 4
F 5
F 6
F 7
F 8
F 9
F 10
F 11
F 12
F 13
0
1
1
2
3
5
8
13
21
34
55
89
144
233
实现
public static int f (int n) { if (n == 0 ) { return 0 ; } if (n == 1 ) { return 1 ; } return f(n - 1 ) + f(n - 2 ); }
执行流程
绿色代表正在执行(对应递),灰色代表执行结束(对应归)
递不到头,不能归,对应着深度优先搜索
时间复杂度
递归的次数也符合斐波那契规律,2 ∗ f ( n + 1 ) − 1 2 * f(n+1)-1 2 ∗ f ( n + 1 ) − 1
时间复杂度推导过程
斐波那契通项公式 f ( n ) = 1 5 ∗ ( 1 + 5 2 n − 1 − 5 2 n ) f(n) = \frac{1}{\sqrt{5}}*({\frac{1+\sqrt{5}}{2}}^n - {\frac{1-\sqrt{5}}{2}}^n) f ( n ) = 5 1 ∗ ( 2 1 + 5 n − 2 1 − 5 n )
简化为:f ( n ) = 1 2.236 ∗ ( 1.618 n − ( − 0.618 ) n ) f(n) = \frac{1}{2.236}*({1.618}^n - {(-0.618)}^n) f ( n ) = 2 . 2 3 6 1 ∗ ( 1 . 6 1 8 n − ( − 0 . 6 1 8 ) n )
带入递归次数公式 2 ∗ 1 2.236 ∗ ( 1.618 n + 1 − ( − 0.618 ) n + 1 ) − 1 2*\frac{1}{2.236}*({1.618}^{n+1} - {(-0.618)}^{n+1})-1 2 ∗ 2 . 2 3 6 1 ∗ ( 1 . 6 1 8 n + 1 − ( − 0 . 6 1 8 ) n + 1 ) − 1
时间复杂度为 Θ ( 1.61 8 n ) \Theta(1.618^n) Θ ( 1 . 6 1 8 n )
更多 Fibonacci 参考
以上时间复杂度分析,未考虑大数相加的因素
变体1 - 兔子问题
第一个月,有一对未成熟的兔子(黑色,注意图中个头较小)
第二个月,它们成熟
第三个月,它们能产下一对新的小兔子(蓝色)
所有兔子遵循相同规律,求第 n n n 个月的兔子数
分析
兔子问题如何与斐波那契联系起来呢?设第 n 个月兔子数为 f ( n ) f(n) f ( n )
f ( n ) f(n) f ( n ) = 上个月兔子数 + 新生的小兔子数
而【新生的小兔子数】实际就是【上个月成熟的兔子数】
因为需要一个月兔子就成熟,所以【上个月成熟的兔子数】也就是【上上个月的兔子数】
上个月兔子数,即 f ( n − 1 ) f(n-1) f ( n − 1 )
上上个月的兔子数,即 f ( n − 2 ) f(n-2) f ( n − 2 )
因此本质还是斐波那契数列,只是从其第一项开始
变体2 - 青蛙爬楼梯
楼梯有 n n n 阶
青蛙要爬到楼顶,可以一次跳一阶,也可以一次跳两阶
只能向上跳,问有多少种跳法
分析
n
跳法
规律
1
(1)
暂时看不出
2
(1,1) (2)
暂时看不出
3
(1,1,1) (1,2) (2,1)
暂时看不出
4
(1,1,1,1) (1,2,1) (2,1,1) (1,1,2) (2,2)
最后一跳,跳一个台阶的,基于f(3) 最后一跳,跳两个台阶的,基于f(2)
5
…
…
E02. 汉诺塔(多路递归)
Tower of Hanoi,是一个源于印度古老传说:大梵天创建世界时做了三根金刚石柱,在一根柱子从下往上按大小顺序摞着 64 片黄金圆盘,大梵天命令婆罗门把圆盘重新摆放在另一根柱子上,并且规定
下面的动图演示了4片圆盘的移动方法
使用程序代码模拟圆盘的移动过程,并估算出时间复杂度
思路
题解
public class E02HanoiTower { static LinkedList<Integer> a = new LinkedList <>(); static LinkedList<Integer> b = new LinkedList <>(); static LinkedList<Integer> c = new LinkedList <>(); static void init (int n) { for (int i = n; i >= 1 ; i--) { a.add(i); } } static void h (int n, LinkedList<Integer> a, LinkedList<Integer> b, LinkedList<Integer> c) { if (n == 0 ) { return ; } h(n - 1 , a, c, b); c.addLast(a.removeLast()); print(); h(n - 1 , b, a, c); } private static void print () { System.out.println("-----------------------" ); System.out.println(a); System.out.println(b); System.out.println(c); } public static void main (String[] args) { init(3 ); print(); h(3 , a, b, c); } }
E03. 杨辉三角
分析
把它斜着看
1 1 1 1 2 1 1 3 3 1 1 4 6 4 1
行 i i i ,列 j j j ,那么 [ i ] [ j ] [i][j] [ i ] [ j ] 的取值应为 [ i − 1 ] [ j − 1 ] + [ i − 1 ] [ j ] [i-1][j-1] + [i-1][j] [ i − 1 ] [ j − 1 ] + [ i − 1 ] [ j ]
当 j = 0 j=0 j = 0 或 i = j i=j i = j 时,[ i ] [ j ] [i][j] [ i ] [ j ] 取值为 1 1 1
题解
public static void print (int n) { for (int i = 0 ; i < n; i++) { if (i < n - 1 ) { System.out.printf("%" + 2 * (n - 1 - i) + "s" , " " ); } for (int j = 0 ; j < i + 1 ; j++) { System.out.printf("%-4d" , element(i, j)); } System.out.println(); } } public static int element (int i, int j) { if (j == 0 || i == j) { return 1 ; } return element(i - 1 , j - 1 ) + element(i - 1 , j); }
优化1
是 multiple recursion,因此很多递归调用是重复的,例如
recursion(3, 1) 分解为
recursion(2, 0) + recursion(2, 1)
而 recursion(3, 2) 分解为
recursion(2, 1) + recursion(2, 2)
这里 recursion(2, 1) 就重复调用了,事实上它会重复很多次,可以用 static AtomicInteger counter = new AtomicInteger(0) 来查看递归函数的调用总次数
事实上,可以用 memoization 来进行优化:
public static void print1 (int n) { int [][] triangle = new int [n][]; for (int i = 0 ; i < n; i++) { triangle[i] = new int [i + 1 ]; for (int j = 0 ; j <= i; j++) { System.out.printf("%-4d" , element1(triangle, i, j)); } System.out.println(); } } public static int element1 (int [][] triangle, int i, int j) { if (triangle[i][j] > 0 ) { return triangle[i][j]; } if (j == 0 || i == j) { triangle[i][j] = 1 ; return triangle[i][j]; } triangle[i][j] = element1(triangle, i - 1 , j - 1 ) + element1(triangle, i - 1 , j); return triangle[i][j]; }
将数组作为递归函数内可以访问的遍历,如果 t r i a n g l e [ i ] [ j ] triangle[i][j] t r i a n g l e [ i ] [ j ] 已经有值,说明该元素已经被之前的递归函数计算过,就不必重复计算了
优化2
public static void print2 (int n) { int [] row = new int [n]; for (int i = 0 ; i < n; i++) { createRow(row, i); for (int j = 0 ; j <= i; j++) { System.out.printf("%-4d" , row[j]); } System.out.println(); } } private static void createRow (int [] row, int i) { if (i == 0 ) { row[0 ] = 1 ; return ; } for (int j = i; j > 0 ; j--) { row[j] = row[j - 1 ] + row[j]; } }
注意:还可以通过每一行的前一项计算出下一项,不必借助上一行,这与杨辉三角的另一个特性有关,暂不展开了
其它题目
力扣对应题目,但递归不适合在力扣刷高分,因此只列出相关题目,不做刷题讲解了
题号
名称
Leetcode118
杨辉三角
Leetcode119
杨辉三角II
4) 递归优化-记忆法
上述代码存在很多重复的计算,例如求 f ( 5 ) f(5) f ( 5 ) 递归分解过程
可以看到(颜色相同的是重复的):
f ( 3 ) f(3) f ( 3 ) 重复了 2 次
f ( 2 ) f(2) f ( 2 ) 重复了 3 次
f ( 1 ) f(1) f ( 1 ) 重复了 5 次
f ( 0 ) f(0) f ( 0 ) 重复了 3 次
随着 n n n 的增大,重复次数非常可观,如何优化呢?
Memoization 记忆法(也称备忘录)是一种优化技术,通过存储函数调用结果(通常比较昂贵),当再次出现相同的输入(子问题)时,就能实现加速效果,改进后的代码
public static void main (String[] args) { int n = 13 ; int [] cache = new int [n + 1 ]; Arrays.fill(cache, -1 ); cache[0 ] = 0 ; cache[1 ] = 1 ; System.out.println(f(cache, n)); } public static int f (int [] cache, int n) { if (cache[n] != -1 ) { return cache[n]; } cache[n] = f(cache, n - 1 ) + f(cache, n - 2 ); return cache[n]; }
优化后的图示,只要结果被缓存,就不会执行其子问题
改进后的时间复杂度为 O ( n ) O(n) O ( n )
请自行验证改进后的效果
请自行分析改进后的空间复杂度
注意
记忆法是动态规划的一种情况,强调的是自顶向下的解决
记忆法的本质是空间换时间
5) 递归优化-尾递归
爆栈
用递归做 n + ( n − 1 ) + ( n − 2 ) . . . + 1 n + (n-1) + (n-2) ... + 1 n + ( n − 1 ) + ( n − 2 ) . . . + 1
public static long sum (long n) { if (n == 1 ) { return 1 ; } return n + sum(n - 1 ); }
在我的机器上 n = 12000 n = 12000 n = 1 2 0 0 0 时,爆栈了
Exception in thread "main" java.lang.StackOverflowError at Test.sum(Test.java:10) at Test.sum(Test.java:10) at Test.sum(Test.java:10) at Test.sum(Test.java:10) at Test.sum(Test.java:10) ...
为什么呢?
每次方法调用是需要消耗一定的栈内存的,这些内存用来存储方法参数、方法内局部变量、返回地址等等
方法调用占用的内存需要等到方法结束时 才会释放
而递归调用我们之前讲过,不到最深不会回头,最内层方法没完成之前,外层方法都结束不了
例如,s u m ( 3 ) sum(3) s u m ( 3 ) 这个方法内有个需要执行 3 + s u m ( 2 ) 3 + sum(2) 3 + s u m ( 2 ) ,s u m ( 2 ) sum(2) s u m ( 2 ) 没返回前,加号前面的 3 3 3 不能释放
看下面伪码
long sum (long n = 3 ) { return 3 + long sum (long n = 2 ) { return 2 + long sum (long n = 1 ) { return 1 ; } } }
尾调用
如果函数的最后一步是调用一个函数,那么称为尾调用,例如
function a ( ) { return b () }
下面三段代码不能 叫做尾调用
function a ( ) { const c = b () return c }
function a ( ) { return b () + 1 }
function a (x ) { return b () + x }
一些语言 的编译器能够对尾调用做优化,例如
function a ( ) { return b () } function b ( ) { return c () } function c ( ) { return 1000 } a ()
没优化之前的伪码
function a ( ) { return function b ( ) { return function c ( ) { return 1000 } } }
优化后伪码 如下
为何尾递归才能优化?
调用 a 时
a 返回时发现:没什么可留给 b 的,将来返回的结果 b 提供就可以了,用不着我 a 了,我的内存就可以释放
调用 b 时
b 返回时发现:没什么可留给 c 的,将来返回的结果 c 提供就可以了,用不着我 b 了,我的内存就可以释放
如果调用 a 时
不是尾调用,例如 return b() + 1,那么 a 就不能提前结束,因为它还得利用 b 的结果做加法
尾递归
尾递归是尾调用的一种特例,也就是最后一步执行的是同一个函数
尾递归避免爆栈
安装 Scala
Scala 入门
object Main { def main (args: Array [String ]): Unit = { println("Hello Scala" ) } }
Scala 是 java 的近亲,java 中的类都可以拿来重用
类型是放在变量后面的
Unit 表示无返回值,类似于 void
不需要以分号作为结尾,当然加上也对
还是先写一个会爆栈的函数
def sum (n: Long ): Long = { if (n == 1 ) { return 1 } return n + sum(n - 1 ) }
Scala 最后一行代码若作为返回值,可以省略 return
不出所料,在 n = 11000 n = 11000 n = 1 1 0 0 0 时,还是出了异常
println(sum(11000 )) Exception in thread "main" java.lang.StackOverflowError at Main $.sum(Main .scala:25 ) at Main $.sum(Main .scala:25 ) at Main $.sum(Main .scala:25 ) at Main $.sum(Main .scala:25 ) ...
这是因为以上代码,还不是尾调用,要想成为尾调用,那么:
最后一行代码,必须是一次函数调用
内层函数必须摆脱 与外层函数的关系,内层函数执行后 不依赖于外层的变量或常量
def sum (n: Long ): Long = { if (n == 1 ) { return 1 } return n + sum(n - 1 ) }
如何让它执行后就摆脱对 n 的依赖呢?
不能等递归回来再做加法,那样就必须保留外层的 n
把 n 当做内层函数的一个参数传进去,这时 n 就属于内层函数了
传参时就完成累加, 不必等回来时累加
改写后代码如下
@tailrec def sum (n: Long , accumulator: Long ): Long = { if (n == 1 ) { return 1 + accumulator } return sum(n - 1 , n + accumulator) }
accumulator 作为累加器
@tailrec 注解是 scala 提供的,用来检查方法是否符合尾递归
这回 sum(10000000, 0) 也没有问题,打印 50000005000000
执行流程如下,以伪码 表示 s u m ( 4 , 0 ) sum(4, 0) s u m ( 4 , 0 )
def sum (n = 4 , accumulator = 0 ): Long = { return sum(4 - 1 , 4 + accumulator) } def sum (n = 3 , accumulator = 4 ): Long = { return sum(3 - 1 , 3 + accumulator) } def sum (n = 2 , accumulator = 7 ): Long = { return sum(2 - 1 , 2 + accumulator) } def sum (n = 1 , accumulator = 9 ): Long = { if (1 == 1 ) { return 1 + accumulator } }
本质上,尾递归优化是将函数的递归 调用,变成了函数的循环 调用
改循环避免爆栈
public static void main (String[] args) { long n = 100000000 ; long sum = 0 ; for (long i = n; i >= 1 ; i--) { sum += i; } System.out.println(sum); }
6) 递归时间复杂度-Master theorem
若有递归式
T ( n ) = a T ( n b ) + f ( n ) T(n) = aT(\frac{n}{b}) + f(n)
T ( n ) = a T ( b n ) + f ( n )
其中
T ( n ) T(n) T ( n ) 是问题的运行时间,n n n 是数据规模
a a a 是子问题个数
T ( n b ) T(\frac{n}{b}) T ( b n ) 是子问题运行时间,每个子问题被拆成原问题数据规模的 n b \frac{n}{b} b n
f ( n ) f(n) f ( n ) 是除递归外执行的计算
令 x = log b a x = \log_{b}{a} x = log b a ,即 x = log 子问题缩小倍数 子问题个数 x = \log_{子问题缩小倍数}{子问题个数} x = log 子 问 题 缩 小 倍 数 子 问 题 个 数
那么
T ( n ) = { Θ ( n x ) f ( n ) = O ( n c ) 并且 c < x Θ ( n x log n ) f ( n ) = Θ ( n x ) Θ ( n c ) f ( n ) = Ω ( n c ) 并且 c > x T(n) =
\begin{cases}
\Theta(n^x) & f(n) = O(n^c) 并且 c \lt x\\
\Theta(n^x\log{n}) & f(n) = \Theta(n^x)\\
\Theta(n^c) & f(n) = \Omega(n^c) 并且 c \gt x
\end{cases}
T ( n ) = ⎩ ⎪ ⎪ ⎨ ⎪ ⎪ ⎧ Θ ( n x ) Θ ( n x log n ) Θ ( n c ) f ( n ) = O ( n c ) 并 且 c < x f ( n ) = Θ ( n x ) f ( n ) = Ω ( n c ) 并 且 c > x
例1
T ( n ) = 2 T ( n 2 ) + n 4 T(n) = 2T(\frac{n}{2}) + n^4 T ( n ) = 2 T ( 2 n ) + n 4
此时 x = 1 < 4 x = 1 < 4 x = 1 < 4 ,由后者决定整个时间复杂度 Θ ( n 4 ) \Theta(n^4) Θ ( n 4 )
如果觉得对数不好算,可以换为求【b b b 的几次方能等于 a a a 】
例2
T ( n ) = T ( 7 n 10 ) + n T(n) = T(\frac{7n}{10}) + n T ( n ) = T ( 1 0 7 n ) + n
a = 1 , b = 10 7 , x = 0 , c = 1 a=1, b=\frac{10}{7}, x=0, c=1 a = 1 , b = 7 1 0 , x = 0 , c = 1
此时 x = 0 < 1 x = 0 < 1 x = 0 < 1 ,由后者决定整个时间复杂度 Θ ( n ) \Theta(n) Θ ( n )
例3
T ( n ) = 16 T ( n 4 ) + n 2 T(n) = 16T(\frac{n}{4}) + n^2 T ( n ) = 1 6 T ( 4 n ) + n 2
a = 16 , b = 4 , x = 2 , c = 2 a=16, b=4, x=2, c=2 a = 1 6 , b = 4 , x = 2 , c = 2
此时 x = 2 = c x=2 = c x = 2 = c ,时间复杂度 Θ ( n 2 log n ) \Theta(n^2 \log{n}) Θ ( n 2 log n )
例4
T ( n ) = 7 T ( n 3 ) + n 2 T(n)=7T(\frac{n}{3}) + n^2 T ( n ) = 7 T ( 3 n ) + n 2
a = 7 , b = 3 , x = 1. ? , c = 2 a=7, b=3, x=1.?, c=2 a = 7 , b = 3 , x = 1 . ? , c = 2
此时 x = log 3 7 < 2 x = \log_{3}{7} < 2 x = log 3 7 < 2 ,由后者决定整个时间复杂度 Θ ( n 2 ) \Theta(n^2) Θ ( n 2 )
例5
T ( n ) = 7 T ( n 2 ) + n 2 T(n) = 7T(\frac{n}{2}) + n^2 T ( n ) = 7 T ( 2 n ) + n 2
a = 7 , b = 2 , x = 2. ? , c = 2 a=7, b=2, x=2.?, c=2 a = 7 , b = 2 , x = 2 . ? , c = 2
此时 x = l o g 2 7 > 2 x = log_2{7} > 2 x = l o g 2 7 > 2 ,由前者决定整个时间复杂度 Θ ( n log 2 7 ) \Theta(n^{\log_2{7}}) Θ ( n l o g 2 7 )
例6
T ( n ) = 2 T ( n 4 ) + n T(n) = 2T(\frac{n}{4}) + \sqrt{n} T ( n ) = 2 T ( 4 n ) + n
a = 2 , b = 4 , x = 0.5 , c = 0.5 a=2, b=4, x = 0.5, c=0.5 a = 2 , b = 4 , x = 0 . 5 , c = 0 . 5
此时 x = 0.5 = c x = 0.5 = c x = 0 . 5 = c ,时间复杂度 Θ ( n log n ) \Theta(\sqrt{n}\ \log{n}) Θ ( n log n )
例7. 二分查找递归
int f (int [] a, int target, int i, int j) { if (i > j) { return -1 ; } int m = (i + j) >>> 1 ; if (target < a[m]) { return f(a, target, i, m - 1 ); } else if (a[m] < target) { return f(a, target, m + 1 , j); } else { return m; } }
子问题个数 a = 1 a = 1 a = 1
子问题数据规模缩小倍数 b = 2 b = 2 b = 2
除递归外执行的计算是常数级 c = 0 c=0 c = 0
T ( n ) = T ( n 2 ) + n 0 T(n) = T(\frac{n}{2}) + n^0 T ( n ) = T ( 2 n ) + n 0
此时 x = 0 = c x=0 = c x = 0 = c ,时间复杂度 Θ ( log n ) \Theta(\log{n}) Θ ( log n )
例8. 归并排序递归
void split(B[], i, j, A[]) { if (j - i <= 1 ) return ; m = (i + j) / 2 ; // 递归 split(A, i, m, B); split(A, m, j, B); // 合并 merge(B, i, m, j, A); }
子问题个数 a = 2 a=2 a = 2
子问题数据规模缩小倍数 b = 2 b=2 b = 2
除递归外,主要时间花在合并上,它可以用 f ( n ) = n f(n) = n f ( n ) = n 表示
T ( n ) = 2 T ( n 2 ) + n T(n) = 2T(\frac{n}{2}) + n T ( n ) = 2 T ( 2 n ) + n
此时 x = 1 = c x=1=c x = 1 = c ,时间复杂度 Θ ( n log n ) \Theta(n\log{n}) Θ ( n log n )
例9. 快速排序递归
algorithm quicksort(A, lo, hi) is if lo >= hi || lo < 0 then return // 分区 p := partition(A, lo, hi) // 递归 quicksort(A, lo, p - 1 ) quicksort(A, p + 1 , hi)
子问题个数 a = 2 a=2 a = 2
子问题数据规模缩小倍数
如果分区分的好,b = 2 b=2 b = 2
如果分区没分好,例如分区1 的数据是 0,分区 2 的数据是 n − 1 n-1 n − 1
除递归外,主要时间花在分区上,它可以用 f ( n ) = n f(n) = n f ( n ) = n 表示
情况1 - 分区分的好
T ( n ) = 2 T ( n 2 ) + n T(n) = 2T(\frac{n}{2}) + n T ( n ) = 2 T ( 2 n ) + n
此时 x = 1 = c x=1=c x = 1 = c ,时间复杂度 Θ ( n log n ) \Theta(n\log{n}) Θ ( n log n )
情况2 - 分区没分好
T ( n ) = T ( n − 1 ) + T ( 1 ) + n T(n) = T(n-1) + T(1) + n T ( n ) = T ( n − 1 ) + T ( 1 ) + n
7) 递归时间复杂度-展开求解
像下面的递归式,都不能用主定理求解
例1 - 递归求和
long sum (long n) { if (n == 1 ) { return 1 ; } return n + sum(n - 1 ); }
T ( n ) = T ( n − 1 ) + c T(n) = T(n-1) + c T ( n ) = T ( n − 1 ) + c ,T ( 1 ) = c T(1) = c T ( 1 ) = c
下面为展开过程
T ( n ) = T ( n − 2 ) + c + c T(n) = T(n-2) + c + c T ( n ) = T ( n − 2 ) + c + c
T ( n ) = T ( n − 3 ) + c + c + c T(n) = T(n-3) + c + c + c T ( n ) = T ( n − 3 ) + c + c + c
…
T ( n ) = T ( n − ( n − 1 ) ) + ( n − 1 ) c T(n) = T(n-(n-1)) + (n-1)c T ( n ) = T ( n − ( n − 1 ) ) + ( n − 1 ) c
其中 T ( n − ( n − 1 ) ) T(n-(n-1)) T ( n − ( n − 1 ) ) 即 T ( 1 ) T(1) T ( 1 )
带入求得 T ( n ) = c + ( n − 1 ) c = n c T(n) = c + (n-1)c = nc T ( n ) = c + ( n − 1 ) c = n c
时间复杂度为 O ( n ) O(n) O ( n )
例2 - 递归冒泡排序
void bubble (int [] a, int high) { if (0 == high) { return ; } for (int i = 0 ; i < high; i++) { if (a[i] > a[i + 1 ]) { swap(a, i, i + 1 ); } } bubble(a, high - 1 ); }
T ( n ) = T ( n − 1 ) + n T(n) = T(n-1) + n T ( n ) = T ( n − 1 ) + n ,T ( 1 ) = c T(1) = c T ( 1 ) = c
下面为展开过程
T ( n ) = T ( n − 2 ) + ( n − 1 ) + n T(n) = T(n-2) + (n-1) + n T ( n ) = T ( n − 2 ) + ( n − 1 ) + n
T ( n ) = T ( n − 3 ) + ( n − 2 ) + ( n − 1 ) + n T(n) = T(n-3) + (n-2) + (n-1) + n T ( n ) = T ( n − 3 ) + ( n − 2 ) + ( n − 1 ) + n
…
T ( n ) = T ( 1 ) + 2 + . . . + n = T ( 1 ) + ( n − 1 ) 2 + n 2 = c + n 2 2 + n 2 − 1 T(n) = T(1) + 2 + ... + n = T(1) + (n-1)\frac{2+n}{2} = c + \frac{n^2}{2} + \frac{n}{2} -1 T ( n ) = T ( 1 ) + 2 + . . . + n = T ( 1 ) + ( n − 1 ) 2 2 + n = c + 2 n 2 + 2 n − 1
时间复杂度 O ( n 2 ) O(n^2) O ( n 2 )
注:
等差数列求和为 个数 ∗ ∣ 首项 − 末项 ∣ 2 个数*\frac{\vert首项-末项\vert}{2} 个 数 ∗ 2 ∣ 首 项 − 末 项 ∣
例3 - 递归快排
快速排序分区没分好的极端情况
T ( n ) = T ( n − 1 ) + T ( 1 ) + n T(n) = T(n-1) + T(1) + n T ( n ) = T ( n − 1 ) + T ( 1 ) + n ,T ( 1 ) = c T(1) = c T ( 1 ) = c
T ( n ) = T ( n − 1 ) + c + n T(n) = T(n-1) + c + n T ( n ) = T ( n − 1 ) + c + n
下面为展开过程
T ( n ) = T ( n − 2 ) + c + ( n − 1 ) + c + n T(n) = T(n-2) + c + (n-1) + c + n T ( n ) = T ( n − 2 ) + c + ( n − 1 ) + c + n
T ( n ) = T ( n − 3 ) + c + ( n − 2 ) + c + ( n − 1 ) + c + n T(n) = T(n-3) + c + (n-2) + c + (n-1) + c + n T ( n ) = T ( n − 3 ) + c + ( n − 2 ) + c + ( n − 1 ) + c + n
…
T ( n ) = T ( n − ( n − 1 ) ) + ( n − 1 ) c + 2 + . . . + n = n 2 2 + 2 c n + n 2 − 1 T(n) = T(n-(n-1)) + (n-1)c + 2+...+n = \frac{n^2}{2} + \frac{2cn+n}{2} -1 T ( n ) = T ( n − ( n − 1 ) ) + ( n − 1 ) c + 2 + . . . + n = 2 n 2 + 2 2 c n + n − 1
时间复杂度 O ( n 2 ) O(n^2) O ( n 2 )
不会推导的同学可以进入 https://www.wolframalpha.com/
例1 输入 f(n) = f(n - 1) + c, f(1) = c
例2 输入 f(n) = f(n - 1) + n, f(1) = c
例3 输入 f(n) = f(n - 1) + n + c, f(1) = c
2.4 队列
1) 概述
计算机科学中,queue 是以顺序的方式维护的一组数据集合,在一端添加数据,从另一端移除数据。习惯来说,添加的一端称为尾 ,移除的一端称为头 ,就如同生活中的排队买商品
In computer science, a queue is a collection of entities that are maintained in a sequence and can be modified by the addition of entities at one end of the sequence and the removal of entities from the other end of the sequence
先定义一个简化的队列接口
public interface Queue <E> { boolean offer (E value) ; E poll () ; E peek () ; boolean isEmpty () ; boolean isFull () ; }
2) 链表实现
下面以单向环形带哨兵 链表方式来实现队列
代码
public class LinkedListQueue <E> implements Queue <E>, Iterable<E> { private static class Node <E> { E value; Node<E> next; public Node (E value, Node<E> next) { this .value = value; this .next = next; } } private Node<E> head = new Node <>(null , null ); private Node<E> tail = head; private int size = 0 ; private int capacity = Integer.MAX_VALUE; { tail.next = head; } public LinkedListQueue () { } public LinkedListQueue (int capacity) { this .capacity = capacity; } @Override public boolean offer (E value) { if (isFull()) { return false ; } Node<E> added = new Node <>(value, head); tail.next = added; tail = added; size++; return true ; } @Override public E poll () { if (isEmpty()) { return null ; } Node<E> first = head.next; head.next = first.next; if (first == tail) { tail = head; } size--; return first.value; } @Override public E peek () { if (isEmpty()) { return null ; } return head.next.value; } @Override public boolean isEmpty () { return head == tail; } @Override public boolean isFull () { return size == capacity; } @Override public Iterator<E> iterator () { return new Iterator <E>() { Node<E> p = head.next; @Override public boolean hasNext () { return p != head; } @Override public E next () { E value = p.value; p = p.next; return value; } }; } }
3) 环形数组实现
好处
对比普通数组,起点和终点更为自由,不用考虑数据移动
“环”意味着不会存在【越界】问题
数组性能更佳
环形数组比较适合实现有界队列、RingBuffer 等
下标计算
例如,数组长度是 5,当前位置是 3 ,向前走 2 步,此时下标为 ( 3 + 2 ) % 5 = 0 (3 + 2)\%5 = 0 ( 3 + 2 ) % 5 = 0
( c u r + s t e p ) % l e n g t h (cur + step) \% length
( c u r + s t e p ) % l e n g t h
cur 当前指针位置
step 前进步数
length 数组长度
注意:
如果 step = 1,也就是一次走一步,可以在 >= length 时重置为 0 即可
判断空
判断满
满之后的策略可以根据业务需求决定
代码
public class ArrayQueue <E> implements Queue <E>, Iterable<E>{ private int head = 0 ; private int tail = 0 ; private final E[] array; private final int length; @SuppressWarnings("all") public ArrayQueue (int capacity) { length = capacity + 1 ; array = (E[]) new Object [length]; } @Override public boolean offer (E value) { if (isFull()) { return false ; } array[tail] = value; tail = (tail + 1 ) % length; return true ; } @Override public E poll () { if (isEmpty()) { return null ; } E value = array[head]; head = (head + 1 ) % length; return value; } @Override public E peek () { if (isEmpty()) { return null ; } return array[head]; } @Override public boolean isEmpty () { return tail == head; } @Override public boolean isFull () { return (tail + 1 ) % length == head; } @Override public Iterator<E> iterator () { return new Iterator <E>() { int p = head; @Override public boolean hasNext () { return p != tail; } @Override public E next () { E value = array[p]; p = (p + 1 ) % array.length; return value; } }; } }
判断空、满方法2
引入 size
public class ArrayQueue2 <E> implements Queue <E>, Iterable<E> { private int head = 0 ; private int tail = 0 ; private final E[] array; private final int capacity; private int size = 0 ; @SuppressWarnings("all") public ArrayQueue2 (int capacity) { this .capacity = capacity; array = (E[]) new Object [capacity]; } @Override public boolean offer (E value) { if (isFull()) { return false ; } array[tail] = value; tail = (tail + 1 ) % capacity; size++; return true ; } @Override public E poll () { if (isEmpty()) { return null ; } E value = array[head]; head = (head + 1 ) % capacity; size--; return value; } @Override public E peek () { if (isEmpty()) { return null ; } return array[head]; } @Override public boolean isEmpty () { return size == 0 ; } @Override public boolean isFull () { return size == capacity; } @Override public Iterator<E> iterator () { return new Iterator <E>() { int p = head; @Override public boolean hasNext () { return p != tail; } @Override public E next () { E value = array[p]; p = (p + 1 ) % capacity; return value; } }; } }
判断空、满方法3
public class ArrayQueue3 <E> implements Queue <E>, Iterable<E> { private int head = 0 ; private int tail = 0 ; private final E[] array; private final int capacity; @SuppressWarnings("all") public ArrayQueue3 (int capacity) { if ((capacity & capacity - 1 ) != 0 ) { throw new IllegalArgumentException ("capacity 必须为 2 的幂" ); } this .capacity = capacity; array = (E[]) new Object [this .capacity]; } @Override public boolean offer (E value) { if (isFull()) { return false ; } array[tail & capacity - 1 ] = value; tail++; return true ; } @Override public E poll () { if (isEmpty()) { return null ; } E value = array[head & capacity - 1 ]; head++; return value; } @Override public E peek () { if (isEmpty()) { return null ; } return array[head & capacity - 1 ]; } @Override public boolean isEmpty () { return tail - head == 0 ; } @Override public boolean isFull () { return tail - head == capacity; } @Override public Iterator<E> iterator () { return new Iterator <E>() { int p = head; @Override public boolean hasNext () { return p != tail; } @Override public E next () { E value = array[p & capacity - 1 ]; p++; return value; } }; } }
习题
E01. 二叉树层序遍历-Leetcode 102
class Solution { public List<List<Integer>> levelOrder (TreeNode root) { List<List<Integer>> result = new ArrayList <>(); if (root == null ) { return result; } LinkedListQueue<TreeNode> queue = new LinkedListQueue <>(); queue.offer(root); int c1 = 1 ; while (!queue.isEmpty()) { int c2 = 0 ; List<Integer> level = new ArrayList <>(); for (int i = 0 ; i < c1; i++) { TreeNode node = queue.poll(); level.add(node.val); if (node.left != null ) { queue.offer(node.left); c2++; } if (node.right != null ) { queue.offer(node.right); c2++; } } c1 = c2; result.add(level); } return result; } static class LinkedListQueue <E> { private static class Node <E> { E value; Node<E> next; public Node (E value, Node<E> next) { this .value = value; this .next = next; } } private final Node<E> head = new Node <>(null , null ); private Node<E> tail = head; int size = 0 ; private int capacity = Integer.MAX_VALUE; { tail.next = head; } public LinkedListQueue () { } public LinkedListQueue (int capacity) { this .capacity = capacity; } public boolean offer (E value) { if (isFull()) { return false ; } Node<E> added = new Node <>(value, head); tail.next = added; tail = added; size++; return true ; } public E poll () { if (isEmpty()) { return null ; } Node<E> first = head.next; head.next = first.next; if (first == tail) { tail = head; } size--; return first.value; } public E peek () { if (isEmpty()) { return null ; } return head.next.value; } public boolean isEmpty () { return head == tail; } public boolean isFull () { return size == capacity; } } }
Ex1. 设计队列-Leetcode 622
由于与课堂例题差别不大,这里只给出参考解答
基于链表的实现
public class Ex1Leetcode622 { private static class Node { int value; Node next; Node(int value, Node next) { this .value = value; this .next = next; } } private final Node head = new Node (-1 , null ); private Node tail = head; private int size = 0 ; private int capacity = 0 ; { tail.next = head; } public Ex1Leetcode622 (int capacity) { this .capacity = capacity; } public boolean enQueue (int value) { if (isFull()) { return false ; } Node added = new Node (value, head); tail.next = added; tail = added; size++; return true ; } public boolean deQueue () { if (isEmpty()) { return false ; } Node first = head.next; head.next = first.next; if (first == tail) { tail = head; } size--; return true ; } public int Front () { if (isEmpty()) { return -1 ; } return head.next.value; } public int Rear () { if (isEmpty()) { return -1 ; } return tail.value; } public boolean isEmpty () { return head == tail; } public boolean isFull () { return size == capacity; } }
注意:
Leetcode 的实现里 deQueue(出队)返回值是布尔值,并不会返回队头元素
它期望用法是先用 Front 返回对头元素,再 deQueue 出队
2.5 栈
1) 概述
计算机科学中,stack 是一种线性的数据结构,只能在其一端添加数据和移除数据。习惯来说,这一端称之为栈顶 ,另一端不能操作数据的称之为栈底 ,就如同生活中的一摞书
先提供一个栈接口
public interface Stack <E> { boolean push (E value) ; E pop () ; E peek () ; boolean isEmpty () ; boolean isFull () ; }
2) 链表实现
public class LinkedListStack <E> implements Stack <E>, Iterable<E> { private final int capacity; private int size; private final Node<E> head = new Node <>(null , null ); public LinkedListStack (int capacity) { this .capacity = capacity; } @Override public boolean push (E value) { if (isFull()) { return false ; } head.next = new Node <>(value, head.next); size++; return true ; } @Override public E pop () { if (isEmpty()) { return null ; } Node<E> first = head.next; head.next = first.next; size--; return first.value; } @Override public E peek () { if (isEmpty()) { return null ; } return head.next.value; } @Override public boolean isEmpty () { return head.next == null ; } @Override public boolean isFull () { return size == capacity; } @Override public Iterator<E> iterator () { return new Iterator <E>() { Node<E> p = head.next; @Override public boolean hasNext () { return p != null ; } @Override public E next () { E value = p.value; p = p.next; return value; } }; } static class Node <E> { E value; Node<E> next; public Node (E value, Node<E> next) { this .value = value; this .next = next; } } }
3) 数组实现
public class ArrayStack <E> implements Stack <E>, Iterable<E>{ private final E[] array; private int top = 0 ; @SuppressWarnings("all") public ArrayStack (int capacity) { this .array = (E[]) new Object [capacity]; } @Override public boolean push (E value) { if (isFull()) { return false ; } array[top++] = value; return true ; } @Override public E pop () { if (isEmpty()) { return null ; } return array[--top]; } @Override public E peek () { if (isEmpty()) { return null ; } return array[top-1 ]; } @Override public boolean isEmpty () { return top == 0 ; } @Override public boolean isFull () { return top == array.length; } @Override public Iterator<E> iterator () { return new Iterator <E>() { int p = top; @Override public boolean hasNext () { return p > 0 ; } @Override public E next () { return array[--p]; } }; } }
4) 应用
模拟如下方法调用
public static void main (String[] args) { System.out.println("main1" ); System.out.println("main2" ); method1(); method2(); System.out.println("main3" ); } public static void method1 () { System.out.println("method1" ); method3(); } public static void method2 () { System.out.println("method2" ); } public static void method3 () { System.out.println("method3" ); }
模拟代码
public class CPU { static class Frame { int exit; public Frame (int exit) { this .exit = exit; } } static int pc = 1 ; static ArrayStack<Frame> stack = new ArrayStack <>(100 ); public static void main (String[] args) { stack.push(new Frame (-1 )); while (!stack.isEmpty()) { switch (pc) { case 1 -> { System.out.println("main1" ); pc++; } case 2 -> { System.out.println("main2" ); pc++; } case 3 -> { stack.push(new Frame (pc + 1 )); pc = 100 ; } case 4 -> { stack.push(new Frame (pc + 1 )); pc = 200 ; } case 5 -> { System.out.println("main3" ); pc = stack.pop().exit; } case 100 -> { System.out.println("method1" ); stack.push(new Frame (pc + 1 )); pc = 300 ; } case 101 -> { pc = stack.pop().exit; } case 200 -> { System.out.println("method2" ); pc = stack.pop().exit; } case 300 -> { System.out.println("method3" ); pc = stack.pop().exit; } } } } }
习题
E01. 有效的括号-Leetcode 20
一个字符串中可能出现 []
()
和 {}
三种括号,判断该括号是否有效
有效的例子
无效的例子
思路
遇到左括号, 把要配对的右括号放入栈顶
遇到右括号, 若此时栈为空, 返回 false,否则把它与栈顶元素对比
若相等, 栈顶元素弹出, 继续对比下一组
若不等, 无效括号直接返回 false
循环结束
若栈为空, 表示所有括号都配上对, 返回 true
若栈不为空, 表示右没配对的括号, 应返回 false
答案(用到了课堂案例中的 ArrayStack 类)
public boolean isValid (String s) { ArrayStack<Character> stack = new ArrayStack <>(s.length() / 2 + 1 ); for (int i = 0 ; i < s.length(); i++) { char c = s.charAt(i); if (c == '(' ) { stack.push(')' ); } else if (c == '[' ) { stack.push(']' ); } else if (c == '{' ) { stack.push('}' ); } else { if (!stack.isEmpty() && stack.peek() == c) { stack.pop(); } else { return false ; } } } return stack.isEmpty(); }
E02. 后缀表达式求值-Leetcode 120
后缀表达式也称为逆波兰表达式,即运算符写在后面
从左向右进行计算
不必考虑运算符优先级,即不用包含括号
示例
输入:tokens = ["2","1","+","3","*"] 输出:9 即:(2 + 1) * 3 输入:tokens = ["4","13","5","/","+"] 输出:6 即:4 + (13 / 5)
题目假设
数字都视为整数
数字和运算符个数给定正确,不会有除零发生
代码
public int evalRPN (String[] tokens) { LinkedList<Integer> numbers = new LinkedList <>(); for (String t : tokens) { switch (t) { case "+" -> { Integer b = numbers.pop(); Integer a = numbers.pop(); numbers.push(a + b); } case "-" -> { Integer b = numbers.pop(); Integer a = numbers.pop(); numbers.push(a - b); } case "*" -> { Integer b = numbers.pop(); Integer a = numbers.pop(); numbers.push(a * b); } case "/" -> { Integer b = numbers.pop(); Integer a = numbers.pop(); numbers.push(a / b); } default -> numbers.push(Integer.parseInt(t)); } } return numbers.pop(); }
E03. 中缀表达式转后缀
public class E03InfixToSuffix { public static void main (String[] args) { System.out.println(infixToSuffix("a+b" )); System.out.println(infixToSuffix("a+b-c" )); System.out.println(infixToSuffix("a+b*c" )); System.out.println(infixToSuffix("a*b-c" )); System.out.println(infixToSuffix("(a+b)*c" )); System.out.println(infixToSuffix("a+b*c+(d*e+f)*g" )); } static String infixToSuffix (String exp) { LinkedList<Character> stack = new LinkedList <>(); StringBuilder sb = new StringBuilder (exp.length()); for (int i = 0 ; i < exp.length(); i++) { char c = exp.charAt(i); switch (c) { case '+' , '-' , '*' , '/' -> { if (stack.isEmpty()) { stack.push(c); } else { if (priority(c) > priority(stack.peek())) { stack.push(c); } else { while (!stack.isEmpty() && priority(stack.peek()) >= priority(c)) { sb.append(stack.pop()); } stack.push(c); } } } case '(' -> { stack.push(c); } case ')' -> { while (!stack.isEmpty() && stack.peek() != '(' ) { sb.append(stack.pop()); } stack.pop(); } default -> { sb.append(c); } } } while (!stack.isEmpty()) { sb.append(stack.pop()); } return sb.toString(); } static int priority (char c) { return switch (c) { case '(' -> 0 ; case '*' , '/' -> 2 ; case '+' , '-' -> 1 ; default -> throw new IllegalArgumentException ("不合法字符:" + c); }; } }
E04. 双栈模拟队列-Leetcode 232
给力扣题目用的自实现 栈,可以定义为静态内部类
class ArrayStack <E> { private E[] array; private int top; @SuppressWarnings("all") public ArrayStack (int capacity) { this .array = (E[]) new Object [capacity]; } public boolean push (E value) { if (isFull()) { return false ; } array[top++] = value; return true ; } public E pop () { if (isEmpty()) { return null ; } return array[--top]; } public E peek () { if (isEmpty()) { return null ; } return array[top - 1 ]; } public boolean isEmpty () { return top == 0 ; } public boolean isFull () { return top == array.length; } }
参考解答,注意:题目已说明
public class E04Leetcode232 { ArrayStack<Integer> s1 = new ArrayStack <>(100 ); ArrayStack<Integer> s2 = new ArrayStack <>(100 ); public void push (int x) { s2.push(x); } public int pop () { if (s1.isEmpty()) { while (!s2.isEmpty()) { s1.push(s2.pop()); } } return s1.pop(); } public int peek () { if (s1.isEmpty()) { while (!s2.isEmpty()) { s1.push(s2.pop()); } } return s1.peek(); } public boolean empty () { return s1.isEmpty() && s2.isEmpty(); } }
E05. 单队列模拟栈-Leetcode 225
给力扣题目用的自实现 队列,可以定义为静态内部类
public class ArrayQueue3 <E> { private final E[] array; int head = 0 ; int tail = 0 ; @SuppressWarnings("all") public ArrayQueue3 (int c) { c -= 1 ; c |= c >> 1 ; c |= c >> 2 ; c |= c >> 4 ; c |= c >> 8 ; c |= c >> 16 ; c += 1 ; array = (E[]) new Object [c]; } public boolean offer (E value) { if (isFull()) { return false ; } array[tail & (array.length - 1 )] = value; tail++; return true ; } public E poll () { if (isEmpty()) { return null ; } E value = array[head & (array.length - 1 )]; head++; return value; } public E peek () { if (isEmpty()) { return null ; } return array[head & (array.length - 1 )]; } public boolean isEmpty () { return head == tail; } public boolean isFull () { return tail - head == array.length; } }
参考解答,注意:题目已说明
调用 push、pop 等方法的次数最多 100
每次调用 pop 和 top 都能保证栈不为空
public class E05Leetcode225 { ArrayQueue3<Integer> queue = new ArrayQueue3 <>(100 ); int size = 0 ; public void push (int x) { queue.offer(x); for (int i = 0 ; i < size; i++) { queue.offer(queue.poll()); } size++; } public int pop () { size--; return queue.poll(); } public int top () { return queue.peek(); } public boolean empty () { return queue.isEmpty(); } }
2.6 双端队列
1) 概述
双端队列、队列、栈对比
定义
特点
队列
一端删除(头)另一端添加(尾)
First In First Out
栈
一端删除和添加(顶)
Last In First Out
双端队列
两端都可以删除、添加
优先级队列
优先级高者先出队
延时队列
根据延时时间确定优先级
并发非阻塞队列
队列空或满时不阻塞
并发阻塞队列
队列空时删除阻塞、队列满时添加阻塞
注1:
Java 中 LinkedList 即为典型双端队列实现,不过它同时实现了 Queue 接口,也提供了栈的 push pop 等方法
注2:
接口定义
public interface Deque <E> { boolean offerFirst (E e) ; boolean offerLast (E e) ; E pollFirst () ; E pollLast () ; E peekFirst () ; E peekLast () ; boolean isEmpty () ; boolean isFull () ; }
2) 链表实现
public class LinkedListDeque <E> implements Deque <E>, Iterable<E> { @Override public boolean offerFirst (E e) { if (isFull()) { return false ; } size++; Node<E> a = sentinel; Node<E> b = sentinel.next; Node<E> offered = new Node <>(a, e, b); a.next = offered; b.prev = offered; return true ; } @Override public boolean offerLast (E e) { if (isFull()) { return false ; } size++; Node<E> a = sentinel.prev; Node<E> b = sentinel; Node<E> offered = new Node <>(a, e, b); a.next = offered; b.prev = offered; return true ; } @Override public E pollFirst () { if (isEmpty()) { return null ; } Node<E> a = sentinel; Node<E> polled = sentinel.next; Node<E> b = polled.next; a.next = b; b.prev = a; size--; return polled.value; } @Override public E pollLast () { if (isEmpty()) { return null ; } Node<E> polled = sentinel.prev; Node<E> a = polled.prev; Node<E> b = sentinel; a.next = b; b.prev = a; size--; return polled.value; } @Override public E peekFirst () { if (isEmpty()) { return null ; } return sentinel.next.value; } @Override public E peekLast () { if (isEmpty()) { return null ; } return sentinel.prev.value; } @Override public boolean isEmpty () { return size == 0 ; } @Override public boolean isFull () { return size == capacity; } @Override public Iterator<E> iterator () { return new Iterator <E>() { Node<E> p = sentinel.next; @Override public boolean hasNext () { return p != sentinel; } @Override public E next () { E value = p.value; p = p.next; return value; } }; } static class Node <E> { Node<E> prev; E value; Node<E> next; public Node (Node<E> prev, E value, Node<E> next) { this .prev = prev; this .value = value; this .next = next; } } Node<E> sentinel = new Node <>(null , null , null ); int capacity; int size; public LinkedListDeque (int capacity) { sentinel.next = sentinel; sentinel.prev = sentinel; this .capacity = capacity; } }
3) 数组实现
public class ArrayDeque1 <E> implements Deque <E>, Iterable<E> { @Override public boolean offerFirst (E e) { if (isFull()) { return false ; } head = dec(head, array.length); array[head] = e; return true ; } @Override public boolean offerLast (E e) { if (isFull()) { return false ; } array[tail] = e; tail = inc(tail, array.length); return true ; } @Override public E pollFirst () { if (isEmpty()) { return null ; } E e = array[head]; array[head] = null ; head = inc(head, array.length); return e; } @Override public E pollLast () { if (isEmpty()) { return null ; } tail = dec(tail, array.length); E e = array[tail]; array[tail] = null ; return e; } @Override public E peekFirst () { if (isEmpty()) { return null ; } return array[head]; } @Override public E peekLast () { if (isEmpty()) { return null ; } return array[dec(tail, array.length)]; } @Override public boolean isEmpty () { return head == tail; } @Override public boolean isFull () { if (tail > head) { return tail - head == array.length - 1 ; } else if (tail < head) { return head - tail == 1 ; } else { return false ; } } @Override public Iterator<E> iterator () { return new Iterator <E>() { int p = head; @Override public boolean hasNext () { return p != tail; } @Override public E next () { E e = array[p]; p = inc(p, array.length); return e; } }; } E[] array; int head; int tail; @SuppressWarnings("unchecked") public ArrayDeque1 (int capacity) { array = (E[]) new Object [capacity + 1 ]; } static int inc (int i, int length) { if (i + 1 >= length) { return 0 ; } return i + 1 ; } static int dec (int i, int length) { if (i - 1 < 0 ) { return length - 1 ; } return i - 1 ; } }
数组实现中,如果存储的是基本类型,那么无需考虑内存释放,例如
但如果存储的是引用类型,应当设置该位置的引用为 null,以便内存及时释放
习题
E01. 二叉树 Z 字层序遍历-Leetcode 103
public class E01Leetcode103 { public List<List<Integer>> zigzagLevelOrder (TreeNode root) { List<List<Integer>> result = new ArrayList <>(); if (root == null ) { return result; } LinkedList<TreeNode> queue = new LinkedList <>(); queue.offer(root); boolean leftToRight = true ; int c1 = 1 ; while (!queue.isEmpty()) { int c2 = 0 ; LinkedList<Integer> deque = new LinkedList <>(); for (int i = 0 ; i < c1; i++) { TreeNode n = queue.poll(); if (leftToRight) { deque.offerLast(n.val); } else { deque.offerFirst(n.val); } if (n.left != null ) { queue.offer(n.left); c2++; } if (n.right != null ) { queue.offer(n.right); c2++; } } c1 = c2; leftToRight = !leftToRight; result.add(deque); } return result; } public static void main (String[] args) { TreeNode root = new TreeNode ( new TreeNode ( new TreeNode (4 ), 2 , new TreeNode (5 ) ), 1 , new TreeNode ( new TreeNode (6 ), 3 , new TreeNode (7 ) ) ); List<List<Integer>> lists = new E01Leetcode103 ().zigzagLevelOrder(root); for (List<Integer> list : lists) { System.out.println(list); } } }
Ex1. 设计双端队列-Leetcode 641
与课堂例题也是差别不大,略
2.7 优先级队列
1) 无序数组实现
要点
入队保持顺序
出队前找到优先级最高的出队,相当于一次选择排序
public class PriorityQueue1 <E extends Priority > implements Queue <E> { Priority[] array; int size; public PriorityQueue1 (int capacity) { array = new Priority [capacity]; } @Override public boolean offer (E e) { if (isFull()) { return false ; } array[size++] = e; return true ; } private int selectMax () { int max = 0 ; for (int i = 1 ; i < size; i++) { if (array[i].priority() > array[max].priority()) { max = i; } } return max; } @Override public E poll () { if (isEmpty()) { return null ; } int max = selectMax(); E e = (E) array[max]; remove(max); return e; } private void remove (int index) { if (index < size - 1 ) { System.arraycopy(array, index + 1 , array, index, size - 1 - index); } array[--size] = null ; } @Override public E peek () { if (isEmpty()) { return null ; } int max = selectMax(); return (E) array[max]; } @Override public boolean isEmpty () { return size == 0 ; } @Override public boolean isFull () { return size == array.length; } }
2) 有序数组实现
要点
入队后排好序,优先级最高的排列在尾部
出队只需删除尾部元素即可
public class PriorityQueue2 <E extends Priority > implements Queue <E> { Priority[] array; int size; public PriorityQueue2 (int capacity) { array = new Priority [capacity]; } @Override public boolean offer (E e) { if (isFull()) { return false ; } insert(e); size++; return true ; } private void insert (E e) { int i = size - 1 ; while (i >= 0 && array[i].priority() > e.priority()) { array[i + 1 ] = array[i]; i--; } array[i + 1 ] = e; } @Override public E poll () { if (isEmpty()) { return null ; } E e = (E) array[size - 1 ]; array[--size] = null ; return e; } @Override public E peek () { if (isEmpty()) { return null ; } return (E) array[size - 1 ]; } @Override public boolean isEmpty () { return size == 0 ; } @Override public boolean isFull () { return size == array.length; } }
3) 堆实现
计算机科学中,堆是一种基于树的数据结构,通常用完全二叉树 实现。堆的特性如下
在大顶堆中,任意节点 C 与它的父节点 P 符合 P . v a l u e ≥ C . v a l u e P.value \geq C.value P . v a l u e ≥ C . v a l u e
而小顶堆中,任意节点 C 与它的父节点 P 符合 P . v a l u e ≤ C . v a l u e P.value \leq C.value P . v a l u e ≤ C . v a l u e
最顶层的节点(没有父亲)称之为 root 根节点
In computer science, a heap is a specialized tree-based data structure which is essentially an almost complete tree that satisfies the heap property : in a max heap , for any given node C, if P is a parent node of C, then the key (the value ) of P is greater than or equal to the key of C. In a min heap , the key of P is less than or equal to the key of C. The node at the “top” of the heap (with no parents) is called the root node
例1 - 满二叉树(Full Binary Tree)特点:每一层都是填满的
例2 - 完全二叉树(Complete Binary Tree)特点:最后一层可能未填满,靠左对齐
例3 - 大顶堆
例4 - 小顶堆
完全二叉树可以使用数组来表示
特征
如果从索引 0 开始存储节点数据
节点 i i i 的父节点为 f l o o r ( ( i − 1 ) / 2 ) floor((i-1)/2) f l o o r ( ( i − 1 ) / 2 ) ,当 i > 0 i>0 i > 0 时
节点 i i i 的左子节点为 2 i + 1 2i+1 2 i + 1 ,右子节点为 2 i + 2 2i+2 2 i + 2 ,当然它们得 < s i z e < size < s i z e
如果从索引 1 开始存储节点数据
节点 i i i 的父节点为 f l o o r ( i / 2 ) floor(i/2) f l o o r ( i / 2 ) ,当 i > 1 i > 1 i > 1 时
节点 i i i 的左子节点为 2 i 2i 2 i ,右子节点为 2 i + 1 2i+1 2 i + 1 ,同样得 < s i z e < size < s i z e
代码
public class PriorityQueue4 <E extends Priority > implements Queue <E> { Priority[] array; int size; public PriorityQueue4 (int capacity) { array = new Priority [capacity]; } @Override public boolean offer (E offered) { if (isFull()) { return false ; } int child = size++; int parent = (child - 1 ) / 2 ; while (child > 0 && offered.priority() > array[parent].priority()) { array[child] = array[parent]; child = parent; parent = (child - 1 ) / 2 ; } array[child] = offered; return true ; } private void swap (int i, int j) { Priority t = array[i]; array[i] = array[j]; array[j] = t; } @Override public E poll () { if (isEmpty()) { return null ; } swap(0 , size - 1 ); size--; Priority e = array[size]; array[size] = null ; shiftDown(0 ); return (E) e; } void shiftDown (int parent) { int left = 2 * parent + 1 ; int right = left + 1 ; int max = parent; if (left < size && array[left].priority() > array[max].priority()) { max = left; } if (right < size && array[right].priority() > array[max].priority()) { max = right; } if (max != parent) { swap(max, parent); shiftDown(max); } } @Override public E peek () { if (isEmpty()) { return null ; } return (E) array[0 ]; } @Override public boolean isEmpty () { return size == 0 ; } @Override public boolean isFull () { return size == array.length; } }
习题
E01. 合并多个有序链表-Leetcode 23
这道题目之前解答过,现在用刚学的优先级队列来实现一下
题目中要从小到大排列,因此选择用小顶堆来实现,自定义小顶堆如下
public class MinHeap { ListNode[] array; int size; public MinHeap (int capacity) { array = new ListNode [capacity]; } public void offer (ListNode offered) { int child = size++; int parent = (child - 1 ) / 2 ; while (child > 0 && offered.val < array[parent].val) { array[child] = array[parent]; child = parent; parent = (child - 1 ) / 2 ; } array[child] = offered; } public ListNode poll () { if (isEmpty()) { return null ; } swap(0 , size - 1 ); size--; ListNode e = array[size]; array[size] = null ; down(0 ); return e; } private void down (int parent) { int left = 2 * parent + 1 ; int right = left + 1 ; int min = parent; if (left < size && array[left].val < array[min].val) { min = left; } if (right < size && array[right].val < array[min].val) { min = right; } if (min != parent) { swap(min, parent); down(min); } } private void swap (int i, int j) { ListNode t = array[i]; array[i] = array[j]; array[j] = t; } public boolean isEmpty () { return size == 0 ; } }
代码
public class E01Leetcode23 { public ListNode mergeKLists (ListNode[] lists) { MinHeap queue = new MinHeap (lists.length); for (ListNode head : lists) { if (head != null ) { queue.offer(head); } } ListNode s = new ListNode (-1 , null ); ListNode p = s; while (!queue.isEmpty()) { ListNode node = queue.poll(); p.next = node; p = node; if (node.next != null ) { queue.offer(node.next); } } return s.next; } }
提问:
能否将每个链表的所有元素全部加入堆,再一个个从堆顶移除?
回答:
可以是可以,但对空间占用就高了,堆的一个优点就是用有限的空间做事情
2.8 阻塞队列
之前的队列在很多场景下都不能很好地工作,例如
大部分场景要求分离向队列放入(生产者)、从队列拿出(消费者)两个角色、它们得由不同的线程来担当,而之前的实现根本没有考虑线程安全问题
队列为空,那么在之前的实现里会返回 null,如果就是硬要拿到一个元素呢?只能不断循环尝试
队列为满,那么再之前的实现里会返回 false,如果就是硬要塞入一个元素呢?只能不断循环尝试
因此我们需要解决的问题有
用锁保证线程安全
用条件变量让等待非空线程 与等待不满线程 进入等待 状态,而不是不断循环尝试,让 CPU 空转
有同学对线程安全还没有足够的认识,下面举一个反例,两个线程都要执行入队操作(几乎在同一时刻)
public class TestThreadUnsafe { private final String[] array = new String [10 ]; private int tail = 0 ; public void offer (String e) { array[tail] = e; tail++; } @Override public String toString () { return Arrays.toString(array); } public static void main (String[] args) { TestThreadUnsafe queue = new TestThreadUnsafe (); new Thread (()-> queue.offer("e1" ), "t1" ).start(); new Thread (()-> queue.offer("e2" ), "t2" ).start(); } }
执行的时间序列如下,假设初始状态 tail = 0,在执行过程中由于 CPU 在两个线程之间切换,造成了指令交错
线程1
线程2
说明
array[tail]=e1
线程1 向 tail 位置加入 e1 这个元素,但还没来得及执行 tail++
array[tail]=e2
线程2 向 tail 位置加入 e2 这个元素,覆盖掉了 e1
tail++
tail 自增为1
tail++
tail 自增为2
最后状态 tail 为 2,数组为 [e2, null, null …]
糟糕的是,由于指令交错的顺序不同,得到的结果不止以上一种,宏观上造成混乱的效果
1) 单锁实现
Java 中要防止代码段交错执行,需要使用锁,有两种选择
synchronized 代码块,属于关键字级别提供锁保护,功能少
ReentrantLock 类,功能丰富
以 ReentrantLock 为例
ReentrantLock lock = new ReentrantLock ();public void offer (String e) { lock.lockInterruptibly(); try { array[tail] = e; tail++; } finally { lock.unlock(); } }
只要两个线程执行上段代码时,锁对象是同一个,就能保证 try 块内的代码的执行不会出现指令交错现象,即执行顺序只可能是下面两种情况之一
线程1
线程2
说明
lock.lockInterruptibly()
t1对锁对象上锁
array[tail]=e1
lock.lockInterruptibly()
即使 CPU 切换到线程2,但由于t1已经对该对象上锁,因此线程2卡在这儿进不去
tail++
切换回线程1 执行后续代码
lock.unlock()
线程1 解锁
array[tail]=e2
线程2 此时才能获得锁,执行它的代码
tail++
另一种情况是线程2 先获得锁,线程1 被挡在外面
要明白保护的本质 ,本例中是保护的是 tail 位置读写的安全
事情还没有完,上面的例子是队列还没有放满的情况,考虑下面的代码(这回锁同时保护了 tail 和 size 的读写安全)
ReentrantLock lock = new ReentrantLock ();int size = 0 ;public void offer (String e) { lock.lockInterruptibly(); try { if (isFull()) { } array[tail] = e; tail++; size++; } finally { lock.unlock(); } } private boolean isFull () { return size == array.length; }
之前是返回 false 表示添加失败,前面分析过想达到这么一种效果:
在队列满时,不是立刻返回,而是当前线程进入等待
什么时候队列不满了,再唤醒这个等待的线程,从上次的代码处继续向下运行
ReentrantLock 可以配合条件变量来实现,代码进化为
ReentrantLock lock = new ReentrantLock ();Condition tailWaits = lock.newCondition(); int size = 0 ;public void offer (String e) { lock.lockInterruptibly(); try { while (isFull()) { tailWaits.await(); } array[tail] = e; tail++; size++; } finally { lock.unlock(); } } private boolean isFull () { return size == array.length; }
条件变量底层也是个队列,用来存储这些需要等待的线程,当队列满了,就会将 offer 线程加入条件队列,并暂时释放锁
将来我们的队列如果不满了(由 poll 线程那边得知)可以调用 tailWaits.signal() 来唤醒 tailWaits 中首个等待的线程,被唤醒的线程会再次抢到锁,从上次 await 处继续向下运行
思考为何要用 while 而不是 if,设队列容量是 3
操作前
offer(4)
offer(5)
poll()
操作后
[1 2 3]
队列满,进入tailWaits 等待
[1 2 3]
[1 2 3]
取走 1,队列不满,唤醒线程
[2 3]
[2 3]
抢先获得锁,发现不满,放入 5
[2 3 5]
[2 3 5]
从上次等待处直接向下执行
[2 3 5 ?]
关键点:
从 tailWaits 中唤醒的线程,会与新来的 offer 的线程争抢锁,谁能抢到是不一定的,如果后者先抢到,就会导致条件又发生变化
这种情况称之为虚假唤醒 ,唤醒后应该重新检查条件,看是不是得重新进入等待
最后的实现代码
public class BlockingQueue1 <E> implements BlockingQueue <E> { private final E[] array; private int head = 0 ; private int tail = 0 ; private int size = 0 ; @SuppressWarnings("all") public BlockingQueue1 (int capacity) { array = (E[]) new Object [capacity]; } ReentrantLock lock = new ReentrantLock (); Condition tailWaits = lock.newCondition(); Condition headWaits = lock.newCondition(); @Override public void offer (E e) throws InterruptedException { lock.lockInterruptibly(); try { while (isFull()) { tailWaits.await(); } array[tail] = e; if (++tail == array.length) { tail = 0 ; } size++; headWaits.signal(); } finally { lock.unlock(); } } @Override public void offer (E e, long timeout) throws InterruptedException { lock.lockInterruptibly(); try { long t = TimeUnit.MILLISECONDS.toNanos(timeout); while (isFull()) { if (t <= 0 ) { return ; } t = tailWaits.awaitNanos(t); } array[tail] = e; if (++tail == array.length) { tail = 0 ; } size++; headWaits.signal(); } finally { lock.unlock(); } } @Override public E poll () throws InterruptedException { lock.lockInterruptibly(); try { while (isEmpty()) { headWaits.await(); } E e = array[head]; array[head] = null ; if (++head == array.length) { head = 0 ; } size--; tailWaits.signal(); return e; } finally { lock.unlock(); } } private boolean isEmpty () { return size == 0 ; } private boolean isFull () { return size == array.length; } }
public void offer(E e, long timeout) throws InterruptedException 是带超时的版本,可以只等待一段时间,而不是永久等下去,类似的 poll 也可以做带超时的版本,这个留给大家了
注意
JDK 中 BlockingQueue 接口的方法命名与我的示例有些差异
方法 offer(E e) 是非阻塞的实现,阻塞实现方法为 put(E e)
方法 poll() 是非阻塞的实现,阻塞实现方法为 take()
2) 双锁实现
单锁的缺点在于:
生产和消费几乎是不冲突的,唯一冲突的是生产者和消费者它们有可能同时修改 size
冲突的主要是生产者之间:多个 offer 线程修改 tail
冲突的还有消费者之间:多个 poll 线程修改 head
如果希望进一步提高性能,可以用两把锁
ReentrantLock headLock = new ReentrantLock (); Condition headWaits = headLock.newCondition(); ReentrantLock tailLock = new ReentrantLock (); Condition tailWaits = tailLock.newCondition();
先看看 offer 方法的初步实现
@Override public void offer (E e) throws InterruptedException { tailLock.lockInterruptibly(); try { while (isFull()) { tailWaits.await(); } array[tail] = e; if (++tail == array.length) { tail = 0 ; } size++; } finally { tailLock.unlock(); } }
上面代码的缺点是 size 并不受 tailLock 保护,tailLock 与 headLock 是两把不同的锁,并不能实现互斥的效果。因此,size 需要用下面的代码保证原子性
AtomicInteger size = new AtomicInteger (0 ); size.getAndIncrement(); size.getAndDecrement();
代码修改为
@Override public void offer (E e) throws InterruptedException { tailLock.lockInterruptibly(); try { while (isFull()) { tailWaits.await(); } array[tail] = e; if (++tail == array.length) { tail = 0 ; } size.getAndIncrement(); } finally { tailLock.unlock(); } }
对称地,可以写出 poll 方法
@Override public E poll () throws InterruptedException { E e; headLock.lockInterruptibly(); try { while (isEmpty()) { headWaits.await(); } e = array[head]; if (++head == array.length) { head = 0 ; } size.getAndDecrement(); } finally { headLock.unlock(); } return e; }
下面来看一个难题,就是如何通知 headWaits 和 tailWaits 中等待的线程,比如 poll 方法拿走一个元素,通知 tailWaits:我拿走一个,不满了噢,你们可以放了,因此代码改为
@Override public E poll () throws InterruptedException { E e; headLock.lockInterruptibly(); try { while (isEmpty()) { headWaits.await(); } e = array[head]; if (++head == array.length) { head = 0 ; } size.getAndDecrement(); tailWaits.signal(); } finally { headLock.unlock(); } return e; }
问题在于要使用这些条件变量的 await(), signal() 等方法需要先获得与之关联的锁,上面的代码若直接运行会出现以下错误
java.lang.IllegalMonitorStateException
那有同学说,加上锁不就行了吗,于是写出了下面的代码
发现什么问题了?两把锁这么嵌套使用,非常容易出现死锁,如下所示
因此得避免嵌套,两段加锁的代码变成了下面平级的样子
性能还可以进一步提升
代码调整后 offer 并没有同时获取 tailLock 和 headLock 两把锁,因此两次加锁之间会有空隙 ,这个空隙内可能有其它的 offer 线程添加了更多的元素,那么这些线程都要执行 signal(),通知 poll 线程队列非空吗?
每次调用 signal() 都需要这些 offer 线程先获得 headLock 锁,成本较高,要想法减少 offer 线程获得 headLock 锁的次数
可以加一个条件:当 offer 增加前队列为空,即从 0 变化到不空,才由此 offer 线程来通知 headWaits,其它情况不归它管
队列从 0 变化到不空,会唤醒一个等待的 poll 线程,这个线程被唤醒后,肯定能拿到 headLock 锁,因此它具备了唤醒 headWaits 上其它 poll 线程的先决条件。如果检查出此时有其它 offer 线程新增了元素(不空,但不是从0变化而来),那么不妨由此 poll 线程来唤醒其它 poll 线程
这个技巧被称之为级联通知(cascading notifies),类似的原因
在 poll 时队列从满变化到不满,才由此 poll 线程来唤醒一个等待的 offer 线程,目的也是为了减少 poll 线程对 tailLock 上锁次数,剩下等待的 offer 线程由这个 offer 线程间接唤醒
最终的代码为
public class BlockingQueue2 <E> implements BlockingQueue <E> { private final E[] array; private int head = 0 ; private int tail = 0 ; private final AtomicInteger size = new AtomicInteger (0 ); ReentrantLock headLock = new ReentrantLock (); Condition headWaits = headLock.newCondition(); ReentrantLock tailLock = new ReentrantLock (); Condition tailWaits = tailLock.newCondition(); public BlockingQueue2 (int capacity) { this .array = (E[]) new Object [capacity]; } @Override public void offer (E e) throws InterruptedException { int c; tailLock.lockInterruptibly(); try { while (isFull()) { tailWaits.await(); } array[tail] = e; if (++tail == array.length) { tail = 0 ; } c = size.getAndIncrement(); if (c + 1 < array.length) { tailWaits.signal(); } } finally { tailLock.unlock(); } if (c == 0 ) { headLock.lock(); try { headWaits.signal(); } finally { headLock.unlock(); } } } @Override public E poll () throws InterruptedException { E e; int c; headLock.lockInterruptibly(); try { while (isEmpty()) { headWaits.await(); } e = array[head]; if (++head == array.length) { head = 0 ; } c = size.getAndDecrement(); if (c > 1 ) { headWaits.signal(); } } finally { headLock.unlock(); } if (c == array.length) { tailLock.lock(); try { tailWaits.signal(); } finally { tailLock.unlock(); } } return e; } private boolean isEmpty () { return size.get() == 0 ; } private boolean isFull () { return size.get() == array.length; } }
双锁实现的非常精巧,据说作者 Doug Lea 花了一年的时间才完善了此段代码
2.9 堆
以大顶堆为例,相对于之前的优先级队列,增加了堆化等方法
public class MaxHeap { int [] array; int size; public MaxHeap (int capacity) { this .array = new int [capacity]; } public int peek () { return array[0 ]; } public int poll () { int top = array[0 ]; swap(0 , size - 1 ); size--; down(0 ); return top; } public int poll (int index) { int deleted = array[index]; up(Integer.MAX_VALUE, index); poll(); return deleted; } public void replace (int replaced) { array[0 ] = replaced; down(0 ); } public boolean offer (int offered) { if (size == array.length) { return false ; } up(offered, size); size++; return true ; } private void up (int offered, int index) { int child = index; while (child > 0 ) { int parent = (child - 1 ) / 2 ; if (offered > array[parent]) { array[child] = array[parent]; } else { break ; } child = parent; } array[child] = offered; } public MaxHeap (int [] array) { this .array = array; this .size = array.length; heapify(); } private void heapify () { for (int i = size / 2 - 1 ; i >= 0 ; i--) { down(i); } } private void down (int parent) { int left = parent * 2 + 1 ; int right = left + 1 ; int max = parent; if (left < size && array[left] > array[max]) { max = left; } if (right < size && array[right] > array[max]) { max = right; } if (max != parent) { swap(max, parent); down(max); } } private void swap (int i, int j) { int t = array[i]; array[i] = array[j]; array[j] = t; } public static void main (String[] args) { int [] array = {2 , 3 , 1 , 7 , 6 , 4 , 5 }; MaxHeap heap = new MaxHeap (array); System.out.println(Arrays.toString(heap.array)); while (heap.size > 1 ) { heap.swap(0 , heap.size - 1 ); heap.size--; heap.down(0 ); } System.out.println(Arrays.toString(heap.array)); } }
建堆
Floyd 建堆算法作者(也是之前龟兔赛跑判环作者):
找到最后一个非叶子节点
从后向前,对每个节点执行下潜
一些规律
一棵满二叉树节点个数为 2 h − 1 2^h-1 2 h − 1 ,如下例中高度 h = 3 h=3 h = 3 节点数是 2 3 − 1 = 7 2^3-1=7 2 3 − 1 = 7
非叶子节点范围为 [ 0 , s i z e / 2 − 1 ] [0, size/2-1] [ 0 , s i z e / 2 − 1 ]
算法时间复杂度分析
下面看交换次数的推导:设节点高度为 3
本层节点数
高度
下潜最多交换次数(高度-1)
4567 这层
4
1
0
23这层
2
2
1
1这层
1
3
2
每一层的交换次数为:节点个数 ∗ 此节点交换次数 节点个数*此节点交换次数 节 点 个 数 ∗ 此 节 点 交 换 次 数 ,总的交换次数为
4 ∗ 0 + 2 ∗ 1 + 1 ∗ 2 8 2 ∗ 0 + 8 4 ∗ 1 + 8 8 ∗ 2 8 2 1 ∗ 0 + 8 2 2 ∗ 1 + 8 2 3 ∗ 2 \begin{aligned}
& 4 * 0 + 2 * 1 + 1 * 2 \\
& \frac{8}{2}*0 + \frac{8}{4}*1 + \frac{8}{8}*2 \\
& \frac{8}{2^1}*0 + \frac{8}{2^2}*1 + \frac{8}{2^3}*2\\
\end{aligned}
4 ∗ 0 + 2 ∗ 1 + 1 ∗ 2 2 8 ∗ 0 + 4 8 ∗ 1 + 8 8 ∗ 2 2 1 8 ∗ 0 + 2 2 8 ∗ 1 + 2 3 8 ∗ 2
即
∑ i = 1 h ( 2 h 2 i ∗ ( i − 1 ) ) \sum_{i=1}^{h}(\frac{2^h}{2^i}*(i-1))
i = 1 ∑ h ( 2 i 2 h ∗ ( i − 1 ) )
在 https://www.wolframalpha.com/ 输入
Sum[\(40)Divide[Power[2,x],Power[2,i]]*\(40)i-1\(41)\(41),{i,1,x}]
推导出
2 h − h − 1 2^h -h -1
2 h − h − 1
其中 2 h ≈ n 2^h \approx n 2 h ≈ n ,h ≈ log 2 n h \approx \log_2{n} h ≈ log 2 n ,因此有时间复杂度 O ( n ) O(n) O ( n )
习题
E01. 堆排序
算法描述
heapify 建立大顶堆
将堆顶与堆底交换(最大元素被交换到堆底),缩小并下潜调整堆
重复第二步直至堆里剩一个元素
可以使用之前课堂例题的大顶堆来实现
int [] array = {1 , 2 , 3 , 4 , 5 , 6 , 7 };MaxHeap maxHeap = new MaxHeap (array);System.out.println(Arrays.toString(maxHeap.array)); while (maxHeap.size > 1 ) { maxHeap.swap(0 , maxHeap.size - 1 ); maxHeap.size--; maxHeap.down(0 ); } System.out.println(Arrays.toString(maxHeap.array));
E02. 数组中第K大元素-Leetcode 215
小顶堆(可删去用不到代码)
class MinHeap { int [] array; int size; public MinHeap (int capacity) { array = new int [capacity]; } private void heapify () { for (int i = (size >> 1 ) - 1 ; i >= 0 ; i--) { down(i); } } public int poll () { swap(0 , size - 1 ); size--; down(0 ); return array[size]; } public int poll (int index) { swap(index, size - 1 ); size--; down(index); return array[size]; } public int peek () { return array[0 ]; } public boolean offer (int offered) { if (size == array.length) { return false ; } up(offered); size++; return true ; } public void replace (int replaced) { array[0 ] = replaced; down(0 ); } private void up (int offered) { int child = size; while (child > 0 ) { int parent = (child - 1 ) >> 1 ; if (offered < array[parent]) { array[child] = array[parent]; } else { break ; } child = parent; } array[child] = offered; } private void down (int parent) { int left = (parent << 1 ) + 1 ; int right = left + 1 ; int min = parent; if (left < size && array[left] < array[min]) { min = left; } if (right < size && array[right] < array[min]) { min = right; } if (min != parent) { swap(min, parent); down(min); } } private void swap (int i, int j) { int t = array[i]; array[i] = array[j]; array[j] = t; } }
题解
public int findKthLargest (int [] numbers, int k) { MinHeap heap = new MinHeap (k); for (int i = 0 ; i < k; i++) { heap.offer(numbers[i]); } for (int i = k; i < numbers.length; i++) { if (numbers[i] > heap.peek()){ heap.replace(numbers[i]); } } return heap.peek(); }
求数组中的第 K 大元素,使用堆并不是最佳选择,可以采用快速选择算法
E03. 数据流中第K大元素-Leetcode 703
上题的小顶堆加一个方法
class MinHeap { public boolean isFull () { return size == array.length; } }
题解
class KthLargest { private MinHeap heap; public KthLargest (int k, int [] nums) { heap = new MinHeap (k); for (int i = 0 ; i < nums.length; i++) { add(nums[i]); } } public int add (int val) { if (!heap.isFull()){ heap.offer(val); } else if (val > heap.peek()){ heap.replace(val); } return heap.peek(); } }
求数据流中的第 K 大元素,使用堆最合适不过
E04. 数据流的中位数-Leetcode 295
可以扩容的 heap, max 用于指定是大顶堆还是小顶堆
public class Heap { int [] array; int size; boolean max; public int size () { return size; } public Heap (int capacity, boolean max) { this .array = new int [capacity]; this .max = max; } public int peek () { return array[0 ]; } public int poll () { int top = array[0 ]; swap(0 , size - 1 ); size--; down(0 ); return top; } public int poll (int index) { int deleted = array[index]; swap(index, size - 1 ); size--; down(index); return deleted; } public void replace (int replaced) { array[0 ] = replaced; down(0 ); } public void offer (int offered) { if (size == array.length) { grow(); } up(offered); size++; } private void grow () { int capacity = size + (size >> 1 ); int [] newArray = new int [capacity]; System.arraycopy(array, 0 , newArray, 0 , size); array = newArray; } private void up (int offered) { int child = size; while (child > 0 ) { int parent = (child - 1 ) / 2 ; boolean cmp = max ? offered > array[parent] : offered < array[parent]; if (cmp) { array[child] = array[parent]; } else { break ; } child = parent; } array[child] = offered; } public Heap (int [] array, boolean max) { this .array = array; this .size = array.length; this .max = max; heapify(); } private void heapify () { for (int i = size / 2 - 1 ; i >= 0 ; i--) { down(i); } } private void down (int parent) { int left = parent * 2 + 1 ; int right = left + 1 ; int min = parent; if (left < size && (max ? array[left] > array[min] : array[left] < array[min])) { min = left; } if (right < size && (max ? array[right] > array[min] : array[right] < array[min])) { min = right; } if (min != parent) { swap(min, parent); down(min); } } private void swap (int i, int j) { int t = array[i]; array[i] = array[j]; array[j] = t; } }
题解
private Heap left = new Heap (10 , false );private Heap right = new Heap (10 , true );public void addNum (int num) { if (left.size() == right.size()) { right.offer(num); left.offer(right.poll()); } else { left.offer(num); right.offer(left.poll()); } } public double findMedian () { if (left.size() == right.size()) { return (left.peek() + right.peek()) / 2.0 ; } else { return left.peek(); } }
本题还可以使用平衡二叉搜索树求解,不过代码比两个堆复杂
2.10 二叉树
二叉树是这么一种树状结构:每个节点最多有两个孩子,左孩子和右孩子
重要的二叉树结构
完全二叉树(complete binary tree)是一种二叉树结构,除最后一层以外,每一层都必须填满,填充时要遵从先左后右
平衡二叉树(balance binary tree)是一种二叉树结构,其中每个节点的左右子树高度相差不超过 1
1) 存储
存储方式分为两种
定义树节点与左、右孩子引用(TreeNode)
使用数组,前面讲堆时用过,若以 0 作为树的根,索引可以通过如下方式计算
父 = floor((子 - 1) / 2)
左孩子 = 父 * 2 + 1
右孩子 = 父 * 2 + 2
2) 遍历
遍历也分为两种
广度优先遍历(Breadth-first order):尽可能先访问距离根最近的节点,也称为层序遍历
深度优先遍历(Depth-first order):对于二叉树,可以进一步分成三种(要深入到叶子节点)
pre-order 前序遍历,对于每一棵子树,先访问该节点,然后是左子树,最后是右子树
in-order 中序遍历,对于每一棵子树,先访问左子树,然后是该节点,最后是右子树
post-order 后序遍历,对于每一棵子树,先访问左子树,然后是右子树,最后是该节点
广度优先
本轮开始时队列
本轮访问节点
[1]
1
[2, 3]
2
[3, 4]
3
[4, 5, 6]
4
[5, 6]
5
[6, 7, 8]
6
[7, 8]
7
[8]
8
[]
初始化,将根节点加入队列
循环处理队列中每个节点,直至队列为空
每次循环内处理节点后,将它的孩子节点(即下一层的节点)加入队列
注意
深度优先
栈暂存
已处理
前序遍历
中序遍历
[1]
1 ✔️ 左💤 右💤
1
[1, 2]
2✔️ 左💤 右💤 1✔️ 左💤 右💤
2
[1, 2, 4]
4✔️ 左✔️ 右✔️ 2✔️ 左💤 右💤 1✔️ 左💤 右💤
4
4
[1, 2]
2✔️ 左✔️ 右✔️ 1✔️ 左💤 右💤
2
[1]
1✔️ 左✔️ 右💤
1
[1, 3]
3✔️ 左💤 右💤 1✔️ 左✔️ 右💤
3
[1, 3, 5]
5✔️ 左✔️ 右✔️ 3✔️ 左💤 右💤 1✔️ 左✔️ 右💤
5
5
[1, 3]
3✔️ 左✔️ 右💤 1✔️ 左✔️ 右💤
3
[1, 3, 6]
6✔️ 左✔️ 右✔️ 3✔️ 左✔️ 右💤 1✔️ 左✔️ 右💤
6
6
[1, 3]
3✔️ 左✔️ 右✔️ 1✔️ 左✔️ 右💤
[1]
1✔️ 左✔️ 右✔️
[]
递归实现
static void preOrder (TreeNode node) { if (node == null ) { return ; } System.out.print(node.val + "\t" ); preOrder(node.left); preOrder(node.right); } static void inOrder (TreeNode node) { if (node == null ) { return ; } inOrder(node.left); System.out.print(node.val + "\t" ); inOrder(node.right); } static void postOrder (TreeNode node) { if (node == null ) { return ; } postOrder(node.left); postOrder(node.right); System.out.print(node.val + "\t" ); }
非递归实现
前序遍历
LinkedListStack<TreeNode> stack = new LinkedListStack <>(); TreeNode curr = root;while (!stack.isEmpty() || curr != null ) { if (curr != null ) { stack.push(curr); System.out.println(curr); curr = curr.left; } else { TreeNode pop = stack.pop(); curr = pop.right; } }
中序遍历
LinkedListStack<TreeNode> stack = new LinkedListStack <>(); TreeNode curr = root;while (!stack.isEmpty() || curr != null ) { if (curr != null ) { stack.push(curr); curr = curr.left; } else { TreeNode pop = stack.pop(); System.out.println(pop); curr = pop.right; } }
后序遍历
LinkedListStack<TreeNode> stack = new LinkedListStack <>(); TreeNode curr = root;TreeNode pop = null ;while (!stack.isEmpty() || curr != null ) { if (curr != null ) { stack.push(curr); curr = curr.left; } else { TreeNode peek = stack.peek(); if (peek.right == null || peek.right == pop) { pop = stack.pop(); System.out.println(pop); } else { curr = peek.right; } } }
对于后序遍历,向回走时,需要处理完右子树才能 pop 出栈。如何知道右子树处理完成呢?
对于前、中两种遍历,实际以上代码从右子树向回走时,并未走完全程(stack 提前出栈了)后序遍历以上代码是走完全程了
统一写法
下面是一种统一的写法,依据后序遍历修改
LinkedList<TreeNode> stack = new LinkedList <>(); TreeNode curr = root; TreeNode pop = null ; while (curr != null || !stack.isEmpty()) { if (curr != null ) { colorPrintln("前: " + curr.val, 31 ); stack.push(curr); curr = curr.left; } else { TreeNode peek = stack.peek(); if (peek.right == null ) { colorPrintln("中: " + peek.val, 36 ); pop = stack.pop(); colorPrintln("后: " + pop.val, 34 ); } else if (peek.right == pop) { pop = stack.pop(); colorPrintln("后: " + pop.val, 34 ); } else { colorPrintln("中: " + peek.val, 36 ); curr = peek.right; } } } public static void colorPrintln (String origin, int color) { System.out.printf("\033[%dm%s\033[0m%n" , color, origin); }
一张图演示三种遍历
红色:前序遍历顺序
绿色:中序遍历顺序
蓝色:后续遍历顺序
习题
E01. 前序遍历二叉树-Leetcode 144
E02. 中序遍历二叉树-Leetcode 94
E03. 后序遍历二叉树-Leetcode 145
E04. 对称二叉树-Leetcode 101
public boolean isSymmetric (TreeNode root) { return check(root.left, root.right); } public boolean check (TreeNode left, TreeNode right) { if (left == null && right == null ) { return true ; } if (left == null || right == null ) { return false ; } if (left.val != right.val) { return false ; } return check(left.left, right.right) && check(left.right, right.left); }
类似题目:Leetcode 100 题 - 相同的树
E05. 二叉树最大深度-Leetcode 104
后序遍历求解
public int maxDepth (TreeNode node) { if (node == null ) { return 0 ; } int d1 = maxDepth(node.left); int d2 = maxDepth(node.right); return Integer.max(d1, d2) + 1 ; }
后序遍历求解-非递归
public int maxDepth (TreeNode root) { TreeNode curr = root; LinkedList<TreeNode> stack = new LinkedList <>(); int max = 0 ; TreeNode pop = null ; while (curr != null || !stack.isEmpty()) { if (curr != null ) { stack.push(curr); int size = stack.size(); if (size > max) { max = size; } curr = curr.left; } else { TreeNode peek = stack.peek(); if (peek.right == null || peek.right == pop) { pop = stack.pop(); } else { curr = peek.right; } } } return max; }
层序遍历求解
public int maxDepth (TreeNode root) { if (root == null ) { return 0 ; } Queue<TreeNode> queue = new LinkedList <>(); queue.offer(root); int level = 0 ; while (!queue.isEmpty()) { level++; int size = queue.size(); for (int i = 0 ; i < size; i++) { TreeNode node = queue.poll(); if (node.left != null ) { queue.offer(node.left); } if (node.right != null ) { queue.offer(node.right); } } } return level; }
E06. 二叉树最小深度-Leetcode 111
后序遍历求解
public int minDepth (TreeNode node) { if (node == null ) { return 0 ; } int d1 = minDepth(node.left); int d2 = minDepth(node.right); if (d1 == 0 || d2 == 0 ) { return d1 + d2 + 1 ; } return Integer.min(d1, d2) + 1 ; }
相较于求最大深度,应当考虑:
当右子树为 null,应当返回左子树深度加一
当左子树为 null,应当返回右子树深度加一
上面两种情况满足时,不应该再把为 null 子树的深度 0 参与最小值比较,例如这样
正确深度为 2,若把为 null 的右子树的深度 0 考虑进来,会得到错误结果 1
正确深度为 3,若把为 null 的左子树的深度 0 考虑进来,会得到错误结果 1
层序遍历求解
遇到的第一个叶子节点所在层就是最小深度
例如,下面的树遇到的第一个叶子节点 3 所在的层就是最小深度,其他 4,7 等叶子节点深度更深,也更晚遇到
代码
public int minDepth (TreeNode root) { if (root == null ) { return 0 ; } Queue<TreeNode> queue = new LinkedList <>(); queue.offer(root); int level = 0 ; while (!queue.isEmpty()) { level++; int size = queue.size(); for (int i = 0 ; i < size; i++) { TreeNode node = queue.poll(); if (node.left == null && node.right == null ) { return level; } if (node.left != null ) { queue.offer(node.left); } if (node.right != null ) { queue.offer(node.right); } } } return level; }
效率会高于之前后序遍历解法,因为找到第一个叶子节点后,就无需后续的层序遍历了
E07. 翻转二叉树-Leetcode 226
public TreeNode invertTree (TreeNode root) { fn(root); return root; } private void fn (TreeNode node) { if (node == null ) { return ; } TreeNode t = node.left; node.left = node.right; node.right = t; fn(node.left); fn(node.right); }
先交换、再递归或是先递归、再交换都可以
E08. 后缀表达式转二叉树
static class TreeNode { public String val; public TreeNode left; public TreeNode right; public TreeNode (String val) { this .val = val; } public TreeNode (TreeNode left, String val, TreeNode right) { this .left = left; this .val = val; this .right = right; } @Override public String toString () { return this .val; } } public TreeNode constructExpressionTree (String[] tokens) { LinkedList<TreeNode> stack = new LinkedList <>(); for (String t : tokens) { switch (t) { case "+" , "-" , "*" , "/" -> { TreeNode right = stack.pop(); TreeNode left = stack.pop(); TreeNode parent = new TreeNode (t); parent.left = left; parent.right = right; stack.push(parent); } default -> { stack.push(new TreeNode (t)); } } } return stack.peek(); }
E09. 根据前序与中序遍历结果构造二叉树-Leetcode 105
先通过前序遍历结果定位根节点
再结合中序遍历结果切分左右子树
public class E09Leetcode105 { public TreeNode buildTree (int [] preOrder, int [] inOrder) { if (preOrder.length == 0 ) { return null ; } int rootValue = preOrder[0 ]; TreeNode root = new TreeNode (rootValue); for (int i = 0 ; i < inOrder.length; i++) { if (inOrder[i] == rootValue) { int [] inLeft = Arrays.copyOfRange(inOrder, 0 , i); int [] inRight = Arrays.copyOfRange(inOrder, i + 1 , inOrder.length); int [] preLeft = Arrays.copyOfRange(preOrder, 1 , i + 1 ); int [] preRight = Arrays.copyOfRange(preOrder, i + 1 , inOrder.length); root.left = buildTree(preLeft, inLeft); root.right = buildTree(preRight, inRight); break ; } } return root; } }
E10. 根据中序与后序遍历结果构造二叉树-Leetcode 106
先通过后序遍历结果定位根节点
再结合中序遍历结果切分左右子树
public TreeNode buildTree (int [] inOrder, int [] postOrder) { if (inOrder.length == 0 ) { return null ; } int rootValue = postOrder[postOrder.length - 1 ]; TreeNode root = new TreeNode (rootValue); for (int i = 0 ; i < inOrder.length; i++) { if (inOrder[i] == rootValue) { int [] inLeft = Arrays.copyOfRange(inOrder, 0 , i); int [] inRight = Arrays.copyOfRange(inOrder, i + 1 , inOrder.length); int [] postLeft = Arrays.copyOfRange(postOrder, 0 , i); int [] postRight = Arrays.copyOfRange(postOrder, i, postOrder.length - 1 ); root.left = buildTree(inLeft, postLeft); root.right = buildTree(inRight, postRight); break ; } } return root; }
三. 基础算法
3.1 查找概述
查找算法是一种在数据集中寻找特定数据项的方法。通常,数据集是在计算机程序中存储的,例如数组、链表或散列表。在编写程序时,查找算法是非常重要的,它有助于快速找到所需的数据。在本文中,我们将介绍一些基本的查找算法及其特点。
线性查找
线性查找也称为顺序查找,是一种最简单的查找算法。在这种算法中,我们从数据集的开头开始,逐个比较每个数据项,以寻找要查找的数据。如果我们找到了目标数据,查找过程就结束了。如果我们到达数据集的末尾,仍然找不到目标数据,则可以认为它不存在于数据集中。
线性查找的时间复杂度是O(n),其中n是数据集的大小。因此,它在大型数据集中可能会很慢。然而,在小型数据集中,它仍然是一种非常有用的算法。
二分查找
二分查找也称为折半查找,是一种更快速的查找算法。但前提是,数据集必须已经排序。在二分查找中,我们取数据集的中间值,然后将目标与中间值进行比较。如果目标小于中间值,则在左侧子集中继续查找;如果目标大于中间值,则在右侧子集中继续查找。每次比较都会缩小要搜索的数据集的大小。
二分查找的时间复杂度是O(log n),其中n是数据集的大小。这种算法在大型数据集中非常有效,但在小型数据集中可能并不是最快的选择。
哈希表查找
哈希表查找也称为散列表查找,是另一种常见的查找算法。它利用哈希函数将数据项映射到散列表中的位置。在查找过程中,我们只需通过哈希函数计算目标数据的位置,然后检查该位置是否包含目标数据。
哈希表查找的时间复杂度是O(1)。这使得它成为大型数据集中最快的查找算法之一。但是,哈希表查找的效率取决于哈希函数的质量。如果两个数据项映射到相同的位置,就会发生哈希冲突,这可能会导致性能下降。
小结
在编写程序时,我们需要选择适合数据集大小和其他要求的最佳查找算法。例如,如果数据集很小,则线性查找可能是最快的选择;如果数据集已经排序,则二分查找是非常有用的。然而,在大型数据集中,哈希表查找通常是最好的选择。了解不同类型的查找算法及其特点可以帮助我们在编写程序时做出明智的选择。
不管是之前学过的数组、链表、队列、还是栈,这些线性结构中,如果想在其中查找一个元素,效率是比较慢的,只有O ( N ) O(N) O ( N ) ,因此如果你的需求是实现数据的快速查找,那么就需要新的数据结构 支持。
还记得最先介绍的那个二分查找算法吗?它的查找效率能够达到 O ( log N ) O(\log{N}) O ( log N ) ,是不是还不错?不过呢,它需要对数组事先排好序,而排序的成本是比较高的。那么有没有一个折中的办法呢?有,那就是接下来要给大家介绍的二叉搜索树 ,它插入元素后,自然就是排好序的,接下来的查询也自然而然可以应用二分查找算法进行高效搜索。
3.2 二叉搜索树
概述
历史
二叉搜索树最早是由Bernoulli兄弟在18世纪中提出的,但是真正推广和应用该数据结构的是1960年代的D.L. Gries。他的著作《The Science of Programming》中详细介绍了二叉搜索树的实现和应用。
在计算机科学的发展中,二叉搜索树成为了一种非常基础的数据结构,被广泛应用在各种领域,包括搜索、排序、数据库索引等。随着计算机算力的提升和对数据结构的深入研究,二叉搜索树也不断被优化和扩展,例如AVL树、红黑树等。
特性
二叉搜索树(也称二叉排序树)是符合下面特征的二叉树:
树节点增加 key 属性,用来比较谁大谁小,key 不可以重复
对于任意一个树节点,它的 key 比左子树的 key 都大,同时也比右子树的 key 都小,例如下图所示
轻易看出要查找 7 (从根开始)自然就可应用二分查找算法,只需三次比较
与 4 比,较之大,向右找
与 6 比,较之大,继续向右找
与 7 比,找到
查找的时间复杂度与树高 相关,插入、删除也是如此。
如果这棵树长得还不赖(左右平衡)上图,那么时间复杂度均是 O ( log N ) O(\log{N}) O ( log N )
当然,这棵树如果长得丑(左右高度相差过大)下图,那么这时是最糟的情况,时间复杂度是 O ( N ) O(N) O ( N )
注:
二叉搜索树 - 英文 binary search tree,简称 BST
二叉排序树 - 英文 binary ordered tree 或 binary sorted tree
实现
定义节点
static class BSTNode { int key; Object value; BSTNode left; BSTNode right; public BSTNode (int key) { this .key = key; this .value = key; } public BSTNode (int key, Object value) { this .key = key; this .value = value; } public BSTNode (int key, Object value, BSTNode left, BSTNode right) { this .key = key; this .value = value; this .left = left; this .right = right; } }
查询
递归实现
public Object get (int key) { return doGet(root, key); } private Object doGet (BSTNode node, int key) { if (node == null ) { return null ; } if (key < node.key) { return doGet(node.left, key); } else if (node.key < key) { return doGet(node.right, key); } else { return node.value; } }
非递归实现
public Object get (int key) { BSTNode node = root; while (node != null ) { if (key < node.key) { node = node.left; } else if (node.key < key) { node = node.right; } else { return node.value; } } return null ; }
Comparable
如果希望让除 int 外更多的类型能够作为 key,一种方式是 key 必须实现 Comparable 接口。
public class BSTTree2 <T extends Comparable <T>> { static class BSTNode <T> { T key; Object value; BSTNode<T> left; BSTNode<T> right; public BSTNode (T key) { this .key = key; this .value = key; } public BSTNode (T key, Object value) { this .key = key; this .value = value; } public BSTNode (T key, Object value, BSTNode<T> left, BSTNode<T> right) { this .key = key; this .value = value; this .left = left; this .right = right; } } BSTNode<T> root; public Object get (T key) { return doGet(root, key); } private Object doGet (BSTNode<T> node, T key) { if (node == null ) { return null ; } int result = node.key.compareTo(key); if (result > 0 ) { return doGet(node.left, key); } else if (result < 0 ) { return doGet(node.right, key); } else { return node.value; } } }
还有一种做法不要求 key 实现 Comparable 接口,而是在构造 Tree 时把比较规则作为 Comparator 传入,将来比较 key 大小时都调用此 Comparator 进行比较,这种做法可以参考 Java 中的 java.util.TreeMap
最小
递归实现
public Object min () { return doMin(root); } public Object doMin (BSTNode node) { if (node == null ) { return null ; } if (node.left == null ) { return node.value; } return doMin(node.left); }
非递归实现
public Object min () { if (root == null ) { return null ; } BSTNode p = root; while (p.left != null ) { p = p.left; } return p.value; }
最大
递归实现
public Object max () { return doMax(root); } public Object doMax (BSTNode node) { if (node == null ) { return null ; } if (node.left == null ) { return node.value; } return doMin(node.right); }
非递归实现
public Object max () { if (root == null ) { return null ; } BSTNode p = root; while (p.right != null ) { p = p.right; } return p.value; }
新增
递归实现
public void put (int key, Object value) { root = doPut(root, key, value); } private BSTNode doPut (BSTNode node, int key, Object value) { if (node == null ) { return new BSTNode (key, value); } if (key < node.key) { node.left = doPut(node.left, key, value); } else if (node.key < key) { node.right = doPut(node.right, key, value); } else { node.value = value; } return node; }
若找到 key,走 else 更新找到节点的值
若没找到 key,走第一个 if,创建并返回新节点
返回的新节点,作为上次递归时 node 的左孩子或右孩子
缺点是,会有很多不必要的赋值操作
非递归实现
public void put (int key, Object value) { BSTNode node = root; BSTNode parent = null ; while (node != null ) { parent = node; if (key < node.key) { node = node.left; } else if (node.key < key) { node = node.right; } else { node.value = value; return ; } } if (parent == null ) { root = new BSTNode (key, value); } else if (key < parent.key) { parent.left = new BSTNode (key, value); } else { parent.right = new BSTNode (key, value); } }
前驱后继
一个节点的前驱(前任)节点是指比它小的节点中,最大的那个
一个节点的后继(后任)节点是指比它大的节点中,最小的那个
例如上图中
1 没有前驱,后继是 2
2 前驱是 1,后继是 3
3 前驱是 2,后继是 4
…
简单的办法是中序遍历,即可获得排序结果,此时很容易找到前驱后继
要效率更高,需要研究一下规律,找前驱 分成 2 种情况:
节点有左子树,此时前驱节点就是左子树的最大值,图中属于这种情况的有
2 的前驱是1
4 的前驱是 3
6 的前驱是 5
7 的前驱是 6
节点没有左子树,若离它最近的祖先自从左而来,此祖先即为前驱,如
3 的祖先 2 自左而来,前驱 2
5 的祖先 4 自左而来,前驱 4
8 的祖先 7 自左而来,前驱 7
1 没有这样的祖先,前驱 null
找后继 也分成 2 种情况
节点有右子树,此时后继节点即为右子树的最小值,如
2 的后继 3
3 的后继 4
5 的后继 6
7 的后继 8
节点没有右子树,若离它最近的祖先自从右而来,此祖先即为后继,如
1 的祖先 2 自右而来,后继 2
4 的祖先 5 自右而来,后继 5
6 的祖先 7 自右而来,后继 7
8 没有这样的祖先,后继 null
public Object predecessor (int key) { BSTNode ancestorFromLeft = null ; BSTNode p = root; while (p != null ) { if (key < p.key) { p = p.left; } else if (p.key < key) { ancestorFromLeft = p; p = p.right; } else { break ; } } if (p == null ) { return null ; } if (p.left != null ) { return max(p.left); } return ancestorFromLeft != null ? ancestorFromLeft.value : null ; } public Object successor (int key) { BSTNode ancestorFromRight = null ; BSTNode p = root; while (p != null ) { if (key < p.key) { ancestorFromRight = p; p = p.left; } else if (p.key < key) { p = p.right; } else { break ; } } if (p == null ) { return null ; } if (p.right != null ) { return min(p.right); } return ancestorFromRight != null ? ancestorFromRight.value : null ; }
删除
要删除某节点(称为 D),必须先找到被删除节点的父节点,这里称为 Parent
删除节点没有左孩子,将右孩子托孤给 Parent
删除节点没有右孩子,将左孩子托孤给 Parent
删除节点左右孩子都没有,已经被涵盖在情况1、情况2 当中,把 null 托孤给 Parent
删除节点左右孩子都有,可以将它的后继节点(称为 S)托孤给 Parent,设 S 的父亲为 SP,又分两种情况
SP 就是被删除节点,此时 D 与 S 紧邻,只需将 S 托孤给 Parent
SP 不是被删除节点,此时 D 与 S 不相邻,此时需要将 S 的后代托孤给 SP,再将 S 托孤给 Parent
非递归实现
public Object delete (int key) { BSTNode p = root; BSTNode parent = null ; while (p != null ) { if (key < p.key) { parent = p; p = p.left; } else if (p.key < key) { parent = p; p = p.right; } else { break ; } } if (p == null ) { return null ; } if (p.left == null ) { shift(parent, p, p.right); } else if (p.right == null ) { shift(parent, p, p.left); } else { BSTNode s = p.right; BSTNode sParent = p; while (s.left != null ) { sParent = s; s = s.left; } if (sParent != p) { shift(sParent, s, s.right); s.right = p.right; } shift(parent, p, s); s.left = p.left; } return p.value; } private void shift (BSTNode parent, BSTNode deleted, BSTNode child) { if (parent == null ) { root = child; } else if (deleted == parent.left) { parent.left = child; } else { parent.right = child; } }
递归实现
public Object delete (int key) { ArrayList<Object> result = new ArrayList <>(); root = doDelete(root, key, result); return result.isEmpty() ? null : result.get(0 ); } public BSTNode doDelete (BSTNode node, int key, ArrayList<Object> result) { if (node == null ) { return null ; } if (key < node.key) { node.left = doDelete(node.left, key, result); return node; } if (node.key < key) { node.right = doDelete(node.right, key, result); return node; } result.add(node.value); if (node.left != null && node.right != null ) { BSTNode s = node.right; while (s.left != null ) { s = s.left; } s.right = doDelete(node.right, s.key, new ArrayList <>()); s.left = node.left; return s; } return node.left != null ? node.left : node.right; }
说明
ArrayList<Object> result
用来保存被删除节点的值
第二、第三个 if 对应没找到的情况,继续递归查找和删除,注意后续的 doDelete 返回值代表删剩下的,因此需要更新
最后一个 return 对应删除节点只有一个孩子的情况,返回那个不为空的孩子,待删节点自己因没有返回而被删除
第四个 if 对应删除节点有两个孩子的情况,此时需要找到后继节点,并在待删除节点的右子树中删掉后继节点,最后用后继节点替代掉待删除节点返回,别忘了改变后继节点的左右指针
找小的
public List<Object> less (int key) { ArrayList<Object> result = new ArrayList <>(); BSTNode p = root; LinkedList<BSTNode> stack = new LinkedList <>(); while (p != null || !stack.isEmpty()) { if (p != null ) { stack.push(p); p = p.left; } else { BSTNode pop = stack.pop(); if (pop.key < key) { result.add(pop.value); } else { break ; } p = pop.right; } } return result; }
找大的
public List<Object> greater (int key) { ArrayList<Object> result = new ArrayList <>(); BSTNode p = root; LinkedList<BSTNode> stack = new LinkedList <>(); while (p != null || !stack.isEmpty()) { if (p != null ) { stack.push(p); p = p.left; } else { BSTNode pop = stack.pop(); if (pop.key > key) { result.add(pop.value); } p = pop.right; } } return result; }
但这样效率不高,可以用 RNL 遍历
注:
Pre-order, NLR
In-order, LNR
Post-order, LRN
Reverse pre-order, NRL
Reverse in-order, RNL
Reverse post-order, RLN
public List<Object> greater (int key) { ArrayList<Object> result = new ArrayList <>(); BSTNode p = root; LinkedList<BSTNode> stack = new LinkedList <>(); while (p != null || !stack.isEmpty()) { if (p != null ) { stack.push(p); p = p.right; } else { BSTNode pop = stack.pop(); if (pop.key > key) { result.add(pop.value); } else { break ; } p = pop.left; } } return result; }
找之间
public List<Object> between (int key1, int key2) { ArrayList<Object> result = new ArrayList <>(); BSTNode p = root; LinkedList<BSTNode> stack = new LinkedList <>(); while (p != null || !stack.isEmpty()) { if (p != null ) { stack.push(p); p = p.left; } else { BSTNode pop = stack.pop(); if (pop.key >= key1 && pop.key <= key2) { result.add(pop.value); } else if (pop.key > key2) { break ; } p = pop.right; } } return result; }
小结
优点:
如果每个节点的左子树和右子树的大小差距不超过一,可以保证搜索操作的时间复杂度是 O(log n),效率高。
插入、删除结点等操作也比较容易实现,效率也比较高。
对于有序数据的查询和处理,二叉查找树非常适用,可以使用中序遍历得到有序序列。
缺点:
如果输入的数据是有序或者近似有序的,就会出现极度不平衡的情况,可能导致搜索效率下降,时间复杂度退化成O(n)。
对于频繁地插入、删除操作,需要维护平衡二叉查找树,例如红黑树、AVL 树等,否则搜索效率也会下降。
对于存在大量重复数据的情况,需要做相应的处理,否则会导致树的深度增加,搜索效率下降。
对于结点过多的情况,由于树的空间开销较大,可能导致内存消耗过大,不适合对内存要求高的场景。
习题
E01. 删除节点-Leetcode 450
例题已经讲过,用非递归和递归均可实现,这里只给出递归参考代码
public TreeNode deleteNode (TreeNode node, int key) { if (node == null ) { return null ; } if (key < node.val) { node.left = deleteNode(node.left, key); return node; } if (node.val < key) { node.right = deleteNode(node.right, key); return node; } if (node.left == null ) { return node.right; } if (node.right == null ) { return node.left; } TreeNode s = node.right; while (s.left != null ) { s = s.left; } s.right = deleteNode(node.right, s.val); s.left = node.left; return s; }
E02. 新增节点-Leetcode 701
例题也讲过了(put),下面给出递归实现
public TreeNode insertIntoBST (TreeNode node, int val) { if (node == null ) { return new TreeNode (val); } if (val < node.val) { node.left = insertIntoBST(node.left, val); } else if (node.val < val) { node.right = insertIntoBST(node.right, val); } return node; }
注意事项与上题相同,不再赘述
题目提示输入的 val 一定与树中节点不同,因此只需考虑新增 情况,不会出现更新 情况
E03. 查询节点-Leetcode 700
例题讲过,下面给出递归实现
public TreeNode searchBST (TreeNode node, int val) { if (node == null ) { return null ; } if (val < node.val) { return searchBST(node.left, val); } else if (node.val < val) { return searchBST(node.right, val); } else { return node; } }
E04. 验证二叉搜索树-Leetcode 98
中序非递归实现
public boolean isValidBST (TreeNode root) { TreeNode p = root; LinkedList<TreeNode> stack = new LinkedList <>(); long prev = Long.MIN_VALUE; while (p != null || !stack.isEmpty()) { if (p != null ) { stack.push(p); p = p.left; } else { TreeNode pop = stack.pop(); if (prev >= pop.val) { return false ; } prev = pop.val; p = pop.right; } } return true ; }
记录 prev 需要用 long,否则若测试用例中最小的节点为 Integer.MIN_VALUE 则测试会失败
注意,如果相邻两个节点相等,也不应当通过测试,例如,下面的树也是不合法的
中序递归实现
public boolean isValidBST (TreeNode root) { if (root == null ) { return true ; } return doValid(new AtomicLong (Long.MIN_VALUE),root); } public boolean doValid (AtomicLong prev, TreeNode node) { if (node == null ) { return true ; } boolean a = doValid(prev, node.left); if (prev.get() >= node.val) { return false ; } prev.set(node.val); boolean b = doValid(prev, node.right); return a && b; }
上下限递归
public boolean isValidBST (TreeNode node) { return doValid(node, Long.MIN_VALUE, Long.MAX_VALUE); } private boolean doValid (TreeNode node, long min, long max) { if (node == null ) { return true ; } if (node.val <= min || node.val >= max) { return false ; } return doValid(node.left, min, node.val) && doValid(node.right, node.val, max); }
设每个节点必须在一个范围内:( m i n , m a x ) (min, max) ( m i n , m a x ) ,不包含边界,若节点值超过这个范围,则返回 false
对于 node.left 范围肯定是 ( m i n , n o d e . v a l ) (min, node.val) ( m i n , n o d e . v a l )
对于 node.right 范围肯定是 ( n o d e . v a l , m a x ) (node.val, max) ( n o d e . v a l , m a x )
一开始不知道 min,max 则取 java 中长整数的最小、最大值
本质是前序遍历 + 剪枝
E05. 求范围和-Leetcode 938
中序递归实现
public int rangeSumBST (TreeNode node, int low, int high) { if (node == null ) { return 0 ; } int a = rangeSumBST(node.left, low, high); int b = 0 ; if (node.val >= low && node.val <= high) { b = node.val; } return a + b + rangeSumBST(node.right, low, high); }
中序非递归实现
public int rangeSumBST (TreeNode node, int low, int high) { TreeNode p = node; LinkedList<TreeNode> stack = new LinkedList <>(); int sum = 0 ; while (p != null || !stack.isEmpty()) { if (p != null ) { stack.push(p); p = p.left; } else { TreeNode pop = stack.pop(); if (pop.val > high) { break ; } if (pop.val >= low) { sum += pop.val; } p = pop.right; } } return sum; }
上下限递归实现
public int rangeSumBST (TreeNode node, int low, int high) { if (node == null ) { return 0 ; } if (node.val < low) { return rangeSumBST(node.right, low, high); } if (node.val > high) { return rangeSumBST(node.left, low, high); } return node.val + rangeSumBST(node.left, low, high) + rangeSumBST(node.right, low, high); }
leetcode 执行耗时 0 ms
node.val < low 只需考虑它右子树的累加结果
node.val > high 只需考虑它左子树的累加结果
node.val 在范围内,需要把当前节点的值加上其左右子树的累加结果
E06. 根据前序遍历结果构造二叉搜索树-Leetcode 1008
直接插入
注意:根据前序遍历的结果,可以唯一地构造出一个二叉搜索树
public TreeNode bstFromPreorder (int [] preorder) { TreeNode root = insert(null , preorder[0 ]); for (int i = 1 ; i < preorder.length; i++) { insert(root, preorder[i]); } return root; } private TreeNode insert (TreeNode node, int val) { if (node == null ) { return new TreeNode (val); } if (val < node.val) { node.left = insert(node.left, val); } else if (node.val < val){ node.right = insert(node.right, val); } return node; }
上限法
public TreeNode bstFromPreorder (int [] preorder) { return insert(preorder, Integer.MAX_VALUE); } int i = 0 ;private TreeNode insert (int [] preorder, int max) { if (i == preorder.length) { return null ; } int val = preorder[i]; System.out.println(val + String.format("[%d]" , max)); if (val > max) { return null ; } TreeNode node = new TreeNode (val); i++; node.left = insert(preorder, node.val); node.right = insert(preorder, max); return node; }
依次处理 prevorder 中每个值, 返回创建好的节点或 null 作为上个节点的孩子
如果超过上限, 返回 null
如果没超过上限, 创建节点, 并将其左右孩子设置完整后返回
i++ 需要放在设置左右孩子之前,意思是从剩下的元素中挑选左右孩子
分治法
public TreeNode bstFromPreorder (int [] preorder) { return partition(preorder, 0 , preorder.length - 1 ); } private TreeNode partition (int [] preorder, int start, int end) { if (start > end) { return null ; } TreeNode root = new TreeNode (preorder[start]); int index = start + 1 ; while (index <= end) { if (preorder[index] > preorder[start]) { break ; } index++; } root.left = partition(preorder, start + 1 , index - 1 ); root.right = partition(preorder, index, end); return root; }
刚开始 8, 5, 1, 7, 10, 12,方法每次执行,确定本次的根节点和左右子树的分界线
第一次确定根节点为 8,左子树 5, 1, 7,右子树 10, 12
对 5, 1, 7 做递归操作,确定根节点是 5, 左子树是 1, 右子树是 7
对 1 做递归操作,确定根节点是 1,左右子树为 null
对 7 做递归操作,确定根节点是 7,左右子树为 null
对 10, 12 做递归操作,确定根节点是 10,左子树为 null,右子树为 12
对 12 做递归操作,确定根节点是 12,左右子树为 null
递归结束,返回本范围内的根节点
E07. 二叉搜索树的最近公共祖先-Leetcode 235
要点:若 p,q 在 ancestor 的两侧,则 ancestor 就是它们的最近公共祖先
public TreeNode lowestCommonAncestor (TreeNode root, TreeNode p, TreeNode q) { TreeNode ancestor = root; while (ancestor.val > p.val && ancestor.val > q.val || ancestor.val < p.val && ancestor.val < q.val) { if (ancestor.val > p.val) { ancestor = ancestor.left; } else { ancestor = ancestor.right; } } return ancestor; }
其它题目
题号
名称
Leetcode 236
二叉树的最近公共祖先
Leetcode 114
二叉树展开为链表
Leetcode 108
有序数组构造平衡二叉搜索树
Leetcode 1382
二叉搜索树变为平衡
3.3 AVL 树
概述
历史
AVL 树是一种自平衡二叉搜索树,由托尔·哈斯特罗姆在 1960 年提出并在 1962 年发表。它的名字来源于发明者的名字:Adelson-Velsky 和 Landis,他们是苏联数学家,于 1962 年发表了一篇论文,详细介绍了 AVL 树的概念和性质。
在二叉搜索树中,如果插入的元素按照特定的顺序排列,可能会导致树变得非常不平衡,从而降低搜索、插入和删除的效率。为了解决这个问题,AVL 树通过在每个节点中维护一个平衡因子来确保树的平衡。平衡因子是左子树的高度减去右子树的高度。如果平衡因子的绝对值大于等于 2,则通过旋转操作来重新平衡树。
AVL 树是用于存储有序数据的一种重要数据结构,它是二叉搜索树的一种改进和扩展。它不仅能够提高搜索、插入和删除操作的效率,而且还能够确保树的深度始终保持在 O(log n) 的水平。随着计算机技术的不断发展,AVL 树已经成为了许多高效算法和系统中必不可少的一种基础数据结构。
前面介绍过,如果一棵二叉搜索树长的不平衡,那么查询的效率会受到影响,如下图
通过旋转可以让树重新变得平衡,并且不会改变二叉搜索树的性质(即左边仍然小,右边仍然大)
如何判断失衡?
如果一个节点的左右孩子,高度差超过 1 ,则此节点失衡,才需要旋转
处理高度
如何得到节点高度?一种方式之前做过的一道题目:E05. 求二叉树的最大深度(高度),但由于求高度是一个非常频繁的操作,因此将高度作为节点的一个属性,将来新增或删除时及时更新,默认为 1(按力扣说法)
static class AVLNode { int height = 1 ; int key; Object value; AVLNode left; AVLNode right; }
求高度代码
这里加入了 height 函数方便求节点为 null 时的高度
private int height (AVLNode node) { return node == null ? 0 : node.height; }
更新高度代码
将来新增、删除、旋转时,高度都可能发生变化,需要更新。下面是更新高度的代码
private void updateHeight (AVLNode node) { node.height = Integer.max(height(node.left), height(node.right)) + 1 ; }
何时触发失衡判断?
定义平衡因子(balance factor)如下
平衡因子 = 左子树高度 − 右子树高度 平衡因子 = 左子树高度 - 右子树高度
平 衡 因 子 = 左 子 树 高 度 − 右 子 树 高 度
当平衡因子
bf = 0,1,-1 时,表示左右平衡
bf > 1 时,表示左边太高
bf < -1 时,表示右边太高
对应代码
private int bf (AVLNode node) { return height(node.left) - height(node.right); }
当插入新节点,或删除节点时,引起高度变化时,例如
目前此树平衡,当再插入一个 4 时,节点们的高度都产生了相应的变化,8 节点失衡了
在比如说,下面这棵树一开始也是平衡的
当删除节点 8 时,节点们的高度都产生了相应的变化,6 节点失衡了
失衡的四种情况
LL
失衡节点(图中 8 红色)的 bf > 1,即左边更高
失衡节点的左孩子(图中 6)的 bf >= 0 即左孩子这边也是左边更高或等高
LR
失衡节点(图中 8)的 bf > 1,即左边更高
失衡节点的左孩子(图中 6 红色)的 bf < 0 即左孩子这边是右边更高
对称的还有两种情况
RL
失衡节点(图中 3)的 bf <-1,即右边更高
失衡节点的右孩子(图中 6 红色)的 bf > 0,即右孩子这边左边更高
RR
失衡节点(图中 3)的 bf <-1,即右边更高
失衡节点的右孩子(图中 6 红色)的 bf <= 0,即右孩子这边右边更高或等高
实现
解决失衡
失衡可以通过树的旋转解决。什么是树的旋转呢?它是在不干扰元素顺序的情况下更改结构,通常用来让树的高度变得平衡。
观察下面一棵二叉搜索树,可以看到,旋转后,并未改变树的左小右大特性,但根、父、孩子节点都发生了变化
4 2 / \ 4 right / \ 2 5 --------------------> 1 4 / \ <-------------------- / \ 1 3 2 left 3 5
右旋
旋转前
红色节点,旧根(失衡节点)
黄色节点,旧根的左孩子,将来作为新根,旧根是它右孩子
绿色节点,新根的右孩子,将来要换爹作为旧根的左孩子
旋转后
代码
private AVLNode rightRotate (AVLNode red) { AVLNode yellow = red.left; AVLNode green = yellow.right; yellow.right = red; red.left = green; return yellow; }
左旋
旋转前
红色节点,旧根(失衡节点)
黄色节点,旧根的右孩子,将来作为新根,旧根是它左孩子
绿色节点,新根的左孩子,将来要换爹作为旧根的右孩子
旋转后
代码
private AVLNode leftRotate (AVLNode red) { AVLNode yellow = red.right; AVLNode green = yellow.left; yellow.left = red; red.right = green; return yellow; }
左右旋
指先左旋左子树,再右旋根节点(失衡),这时一次旋转并不能解决失衡
左子树旋转后
根右旋前
根右旋后
代码
private AVLNode leftRightRotate (AVLNode root) { root.left = leftRotate(root.left); return rightRotate(root); }
右左旋
指先右旋右子树,再左旋根节点(失衡)
右子树右旋后
根左旋前
根左旋后
代码
private AVLNode rightLeftRotate (AVLNode root) { root.right = rightRotate(root.right); return leftRotate(root); }
判断及调整平衡代码
private AVLNode balance (AVLNode node) { if (node == null ) { return null ; } int bf = bf(node); if (bf > 1 && bf(node.left) >= 0 ) { return rightRotate(node); } else if (bf > 1 && bf(node.left) < 0 ) { return rightLeftRotate(node); } else if (bf < -1 && bf(node.right) > 0 ) { return leftRightRotate(node); } else if (bf < -1 && bf(node.right) <= 0 ) { return rightRotate(node); } return node; }
以上四种旋转代码里,都需要更新高度,需要更新的节点是红色、黄色,而绿色节点高度不变
新增
public void put (int key, Object value) { root = doPut(root, key, value); } private AVLNode doPut (AVLNode node, int key, Object value) { if (node == null ) { return new AVLNode (key, value); } if (key == node.key) { node.value = value; return node; } if (key < node.key) { node.left = doPut(node.left, key, value); } else { node.right = doPut(node.right, key, value); } updateHeight(node); return balance(node); }
删除
public void remove (int key) { root = doRemove(root, key); } private AVLNode doRemove (AVLNode node, int key) { if (node == null ) { return null ; } if (key < node.key) { node.left = doRemove(node.left, key); } else if (node.key < key) { node.right = doRemove(node.right, key); } else { if (node.left == null ) { node = node.right; } else if (node.right == null ) { node = node.left; } else { AVLNode s = node.right; while (s.left != null ) { s = s.left; } s.right = doRemove(node.right, s.key); s.left = node.left; node = s; } } if (node == null ) { return null ; } updateHeight(node); return balance(node); }
完整代码备份
public class AVLTree { static class AVLNode { int height = 1 ; int key; Object value; AVLNode left; AVLNode right; public AVLNode (int key) { this .key = key; } public AVLNode (int key, Object value) { this .key = key; this .value = value; } public AVLNode (int key, Object value, AVLNode left, AVLNode right) { this .key = key; this .value = value; this .left = left; this .right = right; } } AVLNode root; private AVLNode leftRotate (AVLNode p) { AVLNode r = p.right; AVLNode b = r.left; r.left = p; p.right = b; updateHeight(p); updateHeight(r); return r; } private void updateHeight (AVLNode node) { node.height = Integer.max(height(node.left), height(node.right)) + 1 ; } private AVLNode rightRotate (AVLNode r) { AVLNode a = r.left; AVLNode b = a.right; a.right = r; r.left = b; updateHeight(r); updateHeight(a); return a; } private AVLNode leftRightRotate (AVLNode p) { AVLNode r = p.left; p.left = leftRotate(r); return rightRotate(p); } private AVLNode rightLeftRotate (AVLNode p) { AVLNode r = p.right; p.right = rightRotate(r); return leftRotate(p); } private int height (AVLNode node) { return node == null ? 0 : node.height; } public void remove (int key) { root = doRemove(root, key); } private AVLNode doRemove (AVLNode node, int key) { if (node == null ) { return null ; } if (key < node.key) { node.left = doRemove(node.left, key); } else if (node.key < key) { node.right = doRemove(node.right, key); } else { if (node.left == null ) { node = node.right; } else if (node.right == null ) { node = node.left; } else { AVLNode s = node.right; while (s.left != null ) { s = s.left; } s.right = doRemove(node.right, s.key); s.left = node.left; node = s; } } if (node == null ) { return null ; } updateHeight(node); return balance(node); } public void put (int key, Object value) { root = doPut(root, key, value); } private AVLNode doPut (AVLNode node, int key, Object value) { if (node == null ) { return new AVLNode (key, value); } if (key == node.key) { node.value = value; return node; } if (key < node.key) { node.left = doPut(node.left, key, value); } else { node.right = doPut(node.right, key, value); } updateHeight(node); return balance(node); } private int bf (AVLNode node) { return height(node.left) - height(node.right); } private AVLNode balance (AVLNode node) { if (node == null ) { return null ; } int bf = bf(node); if (bf > 1 && bf(node.left) >= 0 ) { return rightRotate(node); } else if (bf > 1 && bf(node.left) < 0 ) { return rightLeftRotate(node); } else if (bf < -1 && bf(node.right) > 0 ) { return leftRightRotate(node); } else if (bf < -1 && bf(node.right) <= 0 ) { return rightRotate(node); } return node; } }
小结
AVL树的优点:
AVL树是一种自平衡树,保证了树的高度平衡,从而保证了树的查询和插入操作的时间复杂度均为O(logn)。
相比于一般二叉搜索树,AVL树对查询效率的提升更为显著,因为其左右子树高度的差值不会超过1,避免了二叉搜索树退化为链表的情况,使得整棵树的高度更低。
AVL树的删除操作比较简单,只需要像插入一样旋转即可,在旋转过程中树的平衡性可以得到维护。
AVL树的缺点:
AVL树每次插入或删除节点时需要进行旋转操作,这个操作比较耗时,因此在一些应用中不太适用。
在AVL树进行插入或删除操作时,为保持树的平衡需要不断进行旋转操作,在一些高并发环节和大数据量环境下,这可能会导致多余的写锁导致性能瓶颈。
AVL树的旋转操作相对较多,因此在一些应用中可能会造成较大的空间浪费。
3.4 红黑树
概述
历史
红黑树是一种自平衡二叉查找树,最早由一位名叫Rudolf Bayer的德国计算机科学家于1972年发明。然而,最初的树形结构不是现在的红黑树,而是一种称为B树的结构,它是一种多叉树,可用于在磁盘上存储大量数据。
在1980年代早期,计算机科学家Leonard Adleman和Daniel Sleator推广了红黑树,并证明了它的自平衡性和高效性。从那时起,红黑树成为了最流行的自平衡二叉查找树之一,并被广泛应用于许多领域,如编译器、操作系统、数据库等。
红黑树的名字来源于红色节点和黑色节点的交替出现,它们的颜色是用来维护树的平衡性的关键。它们的颜色具有特殊的意义,黑色节点代表普通节点,而红色节点代表一个新添加的节点,它们必须满足一些特定的规则才能维持树的平衡性。
红黑树也是一种自平衡的二叉搜索树,较之 AVL,插入和删除时旋转次数更少
红黑树特性
所有节点都有两种颜色:红🔴、黑⚫
所有 null 视为黑色⚫
红色🔴节点不能相邻
根节点是黑色⚫
从根到任意一个叶子节点,路径中的黑色⚫节点数一样
实现
插入情况
插入节点均视为红色🔴
case 1:插入节点为根节点,将根节点变黑⚫
case 2:插入节点的父亲若为黑色⚫,树的红黑性质不变,无需调整
插入节点的父亲为红色🔴,触发红红相邻
case 3:叔叔为红色🔴
父亲变为黑色⚫,为了保证黑色平衡,连带的叔叔也变为黑色⚫
祖父如果是黑色不变,会造成这颗子树黑色过多,因此祖父节点变为红色🔴
祖父如果变成红色,可能会接着触发红红相邻,因此对将祖父进行递归调整
case 4:叔叔为黑色⚫
父亲为左孩子,插入节点也是左孩子,此时即 LL 不平衡
让父亲变黑⚫,为了保证这颗子树黑色不变,将祖父变成红🔴,但叔叔子树少了一个黑色
祖父右旋,补齐一个黑色给叔叔,父亲旋转上去取代祖父,由于它是黑色,不会再次触发红红相邻
父亲为左孩子,插入节点是右孩子,此时即 LR 不平衡
父亲为右孩子,插入节点也是右孩子,此时即 RR 不平衡
让父亲变黑⚫,为了保证这颗子树黑色不变,将祖父变成红🔴,但叔叔子树少了一个黑色
祖父左旋,补齐一个黑色给叔叔,父亲旋转上去取代祖父,由于它是黑色,不会再次触发红红相邻
父亲为右孩子,插入节点是左孩子,此时即 RL 不平衡
删除情况
case0:如果删除节点有两个孩子
交换删除节点和后继节点的 key,value,递归删除后继节点,直到该节点没有孩子或只剩一个孩子
如果删除节点没有孩子或只剩一个孩子
case 1:删的是根节点
删完了,直接将 root = null
用剩余节点替换了根节点的 key,value,根节点孩子 = null,颜色保持黑色⚫不变
删黑色会失衡,删红色不会失衡,但删黑色有一种简单情况
case 2:删的是黑⚫,剩下的是红🔴,剩下这个红节点变黑⚫
删除节点和剩下节点都是黑⚫,触发双黑,双黑意思是,少了一个黑
case 3:被调整节点的兄弟为红🔴,此时两个侄子定为黑 ⚫
删除节点是左孩子,父亲左旋
删除节点是右孩子,父亲右旋
父亲和兄弟要变色,保证旋转后颜色平衡
旋转的目的是让黑侄子变为删除节点的黑兄弟,对删除节点再次递归,进入 case 4 或 case 5
case 4:被调整节点的兄弟为黑⚫,两个侄子都为黑 ⚫
将兄弟变红🔴,目的是将删除节点和兄弟那边的黑色高度同时减少 1
如果父亲是红🔴,则需将父亲变为黑,避免红红,此时路径黑节点数目不变
如果父亲是黑⚫,说明这条路径还是少黑,再次让父节点触发双黑
case 5:被调整节点的兄弟为黑⚫,至少一个红🔴侄子
如果兄弟是左孩子,左侄子是红🔴,LL 不平衡
将来删除节点这边少个黑,所以最后旋转过来的父亲需要变成黑⚫,平衡起见,左侄子也是黑⚫
原来兄弟要成为父亲,需要保留父亲颜色
如果兄弟是左孩子,右侄子是红🔴,LR 不平衡
将来删除节点这边少个黑,所以最后旋转过来的父亲需要变成黑⚫
右侄子会取代原来父亲,因此它保留父亲颜色
兄弟已经是黑了⚫,无需改变
如果兄弟是右孩子,右侄子是红🔴,RR 不平衡
将来删除节点这边少个黑,所以最后旋转过来的父亲需要变成黑⚫,平衡起见,右侄子也是黑⚫
原来兄弟要成为父亲,需要保留父亲颜色
如果兄弟是右孩子,左侄子是红🔴,RL 不平衡
将来删除节点这边少个黑,所以最后旋转过来的父亲需要变成黑⚫
左侄子会取代原来父亲,因此它保留父亲颜色
兄弟已经是黑了⚫,无需改变
完整代码
package com.itheima.datastructure.redblacktree;import static com.itheima.datastructure.redblacktree.RedBlackTree.Color.BLACK;import static com.itheima.datastructure.redblacktree.RedBlackTree.Color.RED;public class RedBlackTree { enum Color { RED, BLACK; } Node root; static class Node { int key; Object value; Node left; Node right; Node parent; Color color = RED; public Node (int key, Object value) { this .key = key; this .value = value; } public Node (int key) { this .key = key; } public Node (int key, Color color) { this .key = key; this .color = color; } public Node (int key, Color color, Node left, Node right) { this .key = key; this .color = color; this .left = left; this .right = right; if (left != null ) { left.parent = this ; } if (right != null ) { right.parent = this ; } } boolean isLeftChild () { return parent != null && parent.left == this ; } Node uncle () { if (parent == null || parent.parent == null ) { return null ; } if (parent.isLeftChild()) { return parent.parent.right; } else { return parent.parent.left; } } Node sibling () { if (parent == null ) { return null ; } if (this .isLeftChild()) { return parent.right; } else { return parent.left; } } } boolean isRed (Node node) { return node != null && node.color == RED; } boolean isBlack (Node node) { return node == null || node.color == BLACK; } private void rightRotate (Node pink) { Node parent = pink.parent; Node yellow = pink.left; Node green = yellow.right; if (green != null ) { green.parent = pink; } yellow.right = pink; yellow.parent = parent; pink.left = green; pink.parent = yellow; if (parent == null ) { root = yellow; } else if (parent.left == pink) { parent.left = yellow; } else { parent.right = yellow; } } private void leftRotate (Node pink) { Node parent = pink.parent; Node yellow = pink.right; Node green = yellow.left; if (green != null ) { green.parent = pink; } yellow.left = pink; yellow.parent = parent; pink.right = green; pink.parent = yellow; if (parent == null ) { root = yellow; } else if (parent.left == pink) { parent.left = yellow; } else { parent.right = yellow; } } public void put (int key, Object value) { Node p = root; Node parent = null ; while (p != null ) { parent = p; if (key < p.key) { p = p.left; } else if (p.key < key) { p = p.right; } else { p.value = value; return ; } } Node inserted = new Node (key, value); if (parent == null ) { root = inserted; } else if (key < parent.key) { parent.left = inserted; inserted.parent = parent; } else { parent.right = inserted; inserted.parent = parent; } fixRedRed(inserted); } void fixRedRed (Node x) { if (x == root) { x.color = BLACK; return ; } if (isBlack(x.parent)) { return ; } Node parent = x.parent; Node uncle = x.uncle(); Node grandparent = parent.parent; if (isRed(uncle)) { parent.color = BLACK; uncle.color = BLACK; grandparent.color = RED; fixRedRed(grandparent); return ; } if (parent.isLeftChild() && x.isLeftChild()) { parent.color = BLACK; grandparent.color = RED; rightRotate(grandparent); } else if (parent.isLeftChild()) { leftRotate(parent); x.color = BLACK; grandparent.color = RED; rightRotate(grandparent); } else if (!x.isLeftChild()) { parent.color = BLACK; grandparent.color = RED; leftRotate(grandparent); } else { rightRotate(parent); x.color = BLACK; grandparent.color = RED; leftRotate(grandparent); } } public void remove (int key) { Node deleted = find(key); if (deleted == null ) { return ; } doRemove(deleted); } public boolean contains (int key) { return find(key) != null ; } private Node find (int key) { Node p = root; while (p != null ) { if (key < p.key) { p = p.left; } else if (p.key < key) { p = p.right; } else { return p; } } return null ; } private Node findReplaced (Node deleted) { if (deleted.left == null && deleted.right == null ) { return null ; } if (deleted.left == null ) { return deleted.right; } if (deleted.right == null ) { return deleted.left; } Node s = deleted.right; while (s.left != null ) { s = s.left; } return s; } private void fixDoubleBlack (Node x) { if (x == root) { return ; } Node parent = x.parent; Node sibling = x.sibling(); if (isRed(sibling)) { if (x.isLeftChild()) { leftRotate(parent); } else { rightRotate(parent); } parent.color = RED; sibling.color = BLACK; fixDoubleBlack(x); return ; } if (sibling != null ) { if (isBlack(sibling.left) && isBlack(sibling.right)) { sibling.color = RED; if (isRed(parent)) { parent.color = BLACK; } else { fixDoubleBlack(parent); } } else { if (sibling.isLeftChild() && isRed(sibling.left)) { rightRotate(parent); sibling.left.color = BLACK; sibling.color = parent.color; } else if (sibling.isLeftChild() && isRed(sibling.right)) { sibling.right.color = parent.color; leftRotate(sibling); rightRotate(parent); } else if (!sibling.isLeftChild() && isRed(sibling.left)) { sibling.left.color = parent.color; rightRotate(sibling); leftRotate(parent); } else { leftRotate(parent); sibling.right.color = BLACK; sibling.color = parent.color; } parent.color = BLACK; } } else { fixDoubleBlack(parent); } } private void doRemove (Node deleted) { Node replaced = findReplaced(deleted); Node parent = deleted.parent; if (replaced == null ) { if (deleted == root) { root = null ; } else { if (isBlack(deleted)) { fixDoubleBlack(deleted); } else { } if (deleted.isLeftChild()) { parent.left = null ; } else { parent.right = null ; } deleted.parent = null ; } return ; } if (deleted.left == null || deleted.right == null ) { if (deleted == root) { root.key = replaced.key; root.value = replaced.value; root.left = root.right = null ; } else { if (deleted.isLeftChild()) { parent.left = replaced; } else { parent.right = replaced; } replaced.parent = parent; deleted.left = deleted.right = deleted.parent = null ; if (isBlack(deleted) && isBlack(replaced)) { fixDoubleBlack(replaced); } else { replaced.color = BLACK; } } return ; } int t = deleted.key; deleted.key = replaced.key; replaced.key = t; Object v = deleted.value; deleted.value = replaced.value; replaced.value = v; doRemove(replaced); } }
小结
维度
普通二叉搜索树
AVL树
红黑树
查询
平均O(logn),最坏O(n)
O(logn)
O(logn)
插入
平均O(logn),最坏O(n)
O(logn)
O(logn)
删除
平均O(logn),最坏O(n)
O(logn)
O(logn)
平衡性
不平衡
严格平衡
近似平衡
结构
二叉树
自平衡的二叉树
具有红黑性质的自平衡二叉树
查找效率
低
高
高
插入删除效率
低
中等
高
普通二叉搜索树插入、删除、查询的时间复杂度与树的高度相关,因此在最坏情况下,时间复杂度为O(n),而且容易退化成链表,查找效率低。
AVL树是一种高度平衡的二叉搜索树,其左右子树的高度差不超过1。因此,它能够在logn的平均时间内完成插入、删除、查询操作,但是在维护平衡的过程中,需要频繁地进行旋转操作,导致插入删除效率较低。
红黑树是一种近似平衡的二叉搜索树,它在保持高度平衡的同时,又能够保持较高的插入删除效率。红黑树通过节点着色和旋转操作来维护平衡。红黑树在维护平衡的过程中,能够进行较少的节点旋转操作,因此插入删除效率较高,并且查询效率也较高。
综上所述,红黑树具有较高的综合性能,是一种广泛应用的数据结构。
3.5 B 树
ai 问题列表
请用中文回答:B-树历史
请用中文回答:100万的数据使用 avl 树来存储,树高是多少?
请用中文回答:100万的数据,如果存储到B-树(最小度数是500),那么树高大约是多少?
请用中文回答:B-树的特性有哪些?
概述
历史
B树(B-Tree)结构是一种高效存储和查询数据的方法,它的历史可以追溯到1970年代早期。B树的发明人Rudolf Bayer和Edward M. McCreight分别发表了一篇论文介绍了B树。这篇论文是1972年发表于《ACM Transactions on Database Systems》中的,题目为"Organization and Maintenance of Large Ordered Indexes"。
这篇论文提出了一种能够高效地维护大型有序索引的方法,这种方法的主要思想是将每个节点扩展成多个子节点,以减少查找所需的次数。B树结构非常适合应用于磁盘等大型存储器的高效操作,被广泛应用于关系数据库和文件系统中。
B树结构有很多变种和升级版,例如B+树,B*树和SB树等。这些变种和升级版本都基于B树的核心思想,通过调整B树的参数和结构,提高了B树在不同场景下的性能表现。
总的来说,B树结构是一个非常重要的数据结构,为高效存储和查询大量数据提供了可靠的方法。它的历史可以追溯到上个世纪70年代,而且在今天仍然被广泛应用于各种场景。
B 的含义
B-树的名称是由其发明者Rudolf Bayer提出的。Bayer和McCreight从未解释B代表什么,人们提出了许多可能的解释,比如Boeing、balanced、between、broad、bushy和Bayer等。但McCreight表示,越是思考B-trees中的B代表什么,就越能更好地理解B-trees
特性
一棵 B-树具有以下性质
特性1:每个节点 x 具有
属性 n,表示节点 x 中 key 的个数
属性 leaf,表示节点是否是叶子节点
节点 key 可以有多个,以升序存储
特性2:每个非叶子节点中的孩子数是 n + 1、叶子节点没有孩子
特性3:最小度数t(节点的孩子数称为度)和节点中键数量的关系如下:
最小度数t
键数量范围
2
1 ~ 3
3
2 ~ 5
4
3 ~ 7
…
…
n
(n-1) ~ (2n-1)
其中,当节点中键数量达到其最大值时,即 3、5、7 … 2n-1,需要分裂
特性4:叶子节点的深度都相同
问:
B-树为什么有最小度数的限制?
答:
B树中有最小度数的限制是为了保证B树的平衡特性。
在B树中,每个节点都可以有多个子节点,这使得B树可以存储大量的键值,但也带来了一些问题。如果节点的子节点数量太少,那么就可能导致B树的高度过高,从而降低了B树的效率。此外,如果节点的子节点数量太多,那么就可能导致节点的搜索、插入和删除操作变得复杂和低效。
最小度数的限制通过限制节点的子节点数量,来平衡这些问题。在B树中,每个节点的子节点数量都必须在一定的范围内,即t到2t之间(其中t为最小度数)
B-树与 2-3 树、2-3-4 树的关系
可以这样总结它们之间的关系:
2-3树是最小度数为2的B树,其中每个节点可以包含2个或3个子节点。
2-3-4树是最小度数为2的B树的一种特殊情况,其中每个节点可以包含2个、3个或4个子节点。
B树是一种更加一般化的平衡树,可以适应不同的应用场景,其节点可以包含任意数量的键值,节点的度数取决于最小度数t的设定。
实现
定义节点
static class Node { boolean leaf = true ; int keyNumber; int t; int [] keys; Node[] children; public Node (int t) { this .t = t; this .keys = new int [2 * t - 1 ]; this .children = new Node [2 * t]; } @Override public String toString () { return Arrays.toString(Arrays.copyOfRange(keys, 0 , keyNumber)); } }
leaf 表示是否为叶子节点
keyNumber 为 keys 中有效 key 数目
t 为最小度数,它决定了节点中key 的最小、最大数目,分别是 t-1 和 2t-1
keys 存储此节点的 key
children 存储此节点的 child
toString 只是为了方便调试和测试,非必须
实际 keys 应当改为 entries 以便同时保存 key 和 value,刚开始简化实现
多路查找
为上面节点类添加 get 方法
Node get (int key) { int i = 0 ; while (i < keyNumber && keys[i] < key) { i++; } if (i < keyNumber && keys[i] == key) { return this ; } if (leaf) { return null ; } return children[i].get(key); }
插入 key 和 child
为上面节点类添加 insertKey 和 insertChild 方法
void insertKey (int key, int index) { System.arraycopy(keys, index, keys, index + 1 , keyNumber - index); keys[index] = key; keyNumber++; } void insertChild (Node child, int index) { System.arraycopy(children, index, children, index + 1 , keyNumber - index); children[index] = child; }
作用是向 keys 数组或 children 数组指定 index 处插入新数据,注意
由于使用了静态数组,并且不会在新增或删除时改变它的大小,因此需要额外的 keyNumber 来指定数组内有效 key 的数目
插入时 keyNumber++
删除时减少 keyNumber 的值即可
children 不会单独维护数目,它比 keys 多一个
如果这两个方法同时调用,注意它们的先后顺序,insertChild 后调用,因为它计算复制元素个数时用到了 keyNumber
定义树
public class BTree { final int t; final int MIN_KEY_NUMBER; final int MAX_KEY_NUMBER; Node root; public BTree () { this (2 ); } public BTree (int t) { this .t = t; MIN_KEY_NUMBER = t - 1 ; MAX_KEY_NUMBER = 2 * t - 1 ; root = new Node (t); } }
插入
public void put (int key) { doPut(null , 0 , root, key); } private void doPut (Node parent, int index, Node node, int key) { int i = 0 ; while (i < node.keyNumber && node.keys[i] < key) { i++; } if (i < node.keyNumber && node.keys[i] == key) { return ; } if (node.leaf) { node.insertKey(key, i); } else { doPut(node, i, node.children[i], key); } if (isFull(node)) { split(parent, index, node); } }
首先查找本节点中的插入位置 i,如果没有空位(key 被找到),应该走更新的逻辑,目前什么没做
接下来分两种情况
如果节点是叶子节点,可以直接插入了
如果节点是非叶子节点,需要继续在 children[i] 处继续递归插入
无论哪种情况,插入完成后都可能超过节点 keys 数目限制,此时应当执行节点分裂
参数中的 parent 和 index 都是给分裂方法用的,代表当前节点父节点,和分裂节点是第几个孩子
判断依据为:
boolean isFull (Node node) { return node.keyNumber == MAX_KEY_NUMBER; }
分裂
void split (Node parent, int index , Node left) { if (parent == null ) { Node newRoot = new Node (this .t); newRoot.leaf = false ; newRoot.insertChild(root, 0 ); root = newRoot; parent = newRoot; } Node right = new Node (this .t); right.leaf = left.leaf; right.keyNumber = t - 1 ; System.arraycopy(left.keys, t, right.keys, 0 , t - 1 ); if (!left.leaf) { System.arraycopy(left.children, t, right.children, 0 , t); } left.keyNumber = t - 1 ; int mid = left.keys[t - 1 ]; parent.insertKey(mid, index); parent.insertChild(right, index + 1 ); }
分两种情况:
如果 parent == null 表示要分裂的是根节点,此时需要创建新根,原来的根节点作为新根的 0 孩子
否则
创建 right 节点(分裂后大于当前 left 节点的),把 t 以后的 key 和 child 都拷贝过去
t-1 处的 key 插入到 parent 的 index 处,index 指 left 作为孩子时的索引
right 节点作为 parent 的孩子插入到 index + 1 处
删除
case 1:当前节点是叶子节点,没找到
case 2:当前节点是叶子节点,找到了
case 3:当前节点是非叶子节点,没找到
case 4:当前节点是非叶子节点,找到了
case 5:删除后 key 数目 < 下限(不平衡)
case 6:根节点
完整代码
package com.itheima.algorithm.btree;import java.util.Arrays;@SuppressWarnings("all") public class BTree { static class Node { int [] keys; Node[] children; int keyNumber; boolean leaf = true ; int t; public Node (int t) { this .t = t; this .children = new Node [2 * t]; this .keys = new int [2 * t - 1 ]; } public Node (int [] keys) { this .keys = keys; } @Override public String toString () { return Arrays.toString(Arrays.copyOfRange(keys, 0 , keyNumber)); } Node get (int key) { int i = 0 ; while (i < keyNumber) { if (keys[i] == key) { return this ; } if (keys[i] > key) { break ; } i++; } if (leaf) { return null ; } return children[i].get(key); } void insertKey (int key, int index) { System.arraycopy(keys, index, keys, index + 1 , keyNumber - index); keys[index] = key; keyNumber++; } void insertChild (Node child, int index) { System.arraycopy(children, index, children, index + 1 , keyNumber - index); children[index] = child; } int removeKey (int index) { int t = keys[index]; System.arraycopy(keys, index + 1 , keys, index, --keyNumber - index); return t; } int removeLeftmostKey () { return removeKey(0 ); } int removeRightmostKey () { return removeKey(keyNumber - 1 ); } Node removeChild (int index) { Node t = children[index]; System.arraycopy(children, index + 1 , children, index, keyNumber - index); children[keyNumber] = null ; return t; } Node removeLeftmostChild () { return removeChild(0 ); } Node removeRightmostChild () { return removeChild(keyNumber); } void moveToLeft (Node left) { int start = left.keyNumber; if (!leaf) { for (int i = 0 ; i <= keyNumber; i++) { left.children[start + i] = children[i]; } } for (int i = 0 ; i < keyNumber; i++) { left.keys[left.keyNumber++] = keys[i]; } } Node leftSibling (int index) { return index > 0 ? children[index - 1 ] : null ; } Node rightSibling (int index) { return index == keyNumber ? null : children[index + 1 ]; } } Node root; int t; final int MIN_KEY_NUMBER; final int MAX_KEY_NUMBER; public BTree () { this (2 ); } public BTree (int t) { this .t = t; root = new Node (t); MAX_KEY_NUMBER = 2 * t - 1 ; MIN_KEY_NUMBER = t - 1 ; } public boolean contains (int key) { return root.get(key) != null ; } public void put (int key) { doPut(root, key, null , 0 ); } private void doPut (Node node, int key, Node parent, int index) { int i = 0 ; while (i < node.keyNumber) { if (node.keys[i] == key) { return ; } if (node.keys[i] > key) { break ; } i++; } if (node.leaf) { node.insertKey(key, i); } else { doPut(node.children[i], key, node, i); } if (node.keyNumber == MAX_KEY_NUMBER) { split(node, parent, index); } } void split (Node left, Node parent, int index) { if (parent == null ) { Node newRoot = new Node (t); newRoot.leaf = false ; newRoot.insertChild(left, 0 ); this .root = newRoot; parent = newRoot; } Node right = new Node (t); right.leaf = left.leaf; System.arraycopy(left.keys, t, right.keys, 0 , t - 1 ); if (!left.leaf) { System.arraycopy(left.children, t, right.children, 0 , t); for (int i = t; i <= left.keyNumber; i++) { left.children[i] = null ; } } right.keyNumber = t - 1 ; left.keyNumber = t - 1 ; int mid = left.keys[t - 1 ]; parent.insertKey(mid, index); parent.insertChild(right, index + 1 ); } public void remove (int key) { doRemove(root, key, null , 0 ); } private void doRemove (Node node, int key, Node parent, int index) { int i = 0 ; while (i < node.keyNumber) { if (node.keys[i] >= key) { break ; } i++; } if (node.leaf) { if (notFound(node, key, i)) { return ; } node.removeKey(i); } else { if (notFound(node, key, i)) { doRemove(node.children[i], key, node, i); } else { Node s = node.children[i + 1 ]; while (!s.leaf) { s = s.children[0 ]; } int k = s.keys[0 ]; node.keys[i] = k; doRemove(node.children[i + 1 ], k, node, i + 1 ); } } if (node.keyNumber < MIN_KEY_NUMBER) { balance(node, parent, index); } } private boolean notFound (Node node, int key, int i) { return i >= node.keyNumber || (i < node.keyNumber && node.keys[i] != key); } private void balance (Node node, Node parent, int i) { if (node == root) { if (root.keyNumber == 0 && root.children[0 ] != null ) { root = root.children[0 ]; } return ; } Node leftSibling = parent.leftSibling(i); Node rightSibling = parent.rightSibling(i); if (leftSibling != null && leftSibling.keyNumber > MIN_KEY_NUMBER) { rightRotate(node, leftSibling, parent, i); return ; } if (rightSibling != null && rightSibling.keyNumber > MIN_KEY_NUMBER) { leftRotate(node, rightSibling, parent, i); return ; } if (leftSibling != null ) { mergeToLeft(leftSibling, parent, i - 1 ); } else { mergeToLeft(node, parent, i); } } private void mergeToLeft (Node left, Node parent, int i) { Node right = parent.removeChild(i + 1 ); left.insertKey(parent.removeKey(i), left.keyNumber); right.moveToLeft(left); } private void rightRotate (Node node, Node leftSibling, Node parent, int i) { node.insertKey(parent.keys[i - 1 ], 0 ); if (!leftSibling.leaf) { node.insertChild(leftSibling.removeRightmostChild(), 0 ); } parent.keys[i - 1 ] = leftSibling.removeRightmostKey(); } private void leftRotate (Node node, Node rightSibling, Node parent, int i) { node.insertKey(parent.keys[i], node.keyNumber); if (!rightSibling.leaf) { node.insertChild(rightSibling.removeLeftmostChild(), node.keyNumber + 1 ); } parent.keys[i] = rightSibling.removeLeftmostKey(); } }
3.6 哈希表
第一版
未考虑 hash 码的生成,假定该 hash 码由我们提供
public class HashTable { static class Entry { int hash; Object key; Object value; Entry next; public Entry (int hash, Object key, Object value) { this .hash = hash; this .key = key; this .value = value; } } Entry[] table = new Entry [16 ]; int size = 0 ; float loadFactor = 0.75f ; int threshold = (int ) (loadFactor * table.length); Object get (int hash, Object key) { int idx = hash & (table.length - 1 ); if (table[idx] == null ) { return null ; } Entry p = table[idx]; while (p != null ) { if (p.key.equals(key)) { return p.value; } p = p.next; } return null ; } void put (int hash, Object key, Object value) { int idx = hash & (table.length - 1 ); if (table[idx] == null ) { table[idx] = new Entry (hash, key, value); } else { Entry p = table[idx]; while (true ) { if (p.key.equals(key)) { p.value = value; return ; } if (p.next == null ) { break ; } p = p.next; } p.next = new Entry (hash, key, value); } size++; if (size > threshold) { resize(); } } private void resize () { Entry[] newTable = new Entry [table.length << 1 ]; for (int i = 0 ; i < table.length; i++) { Entry p = table[i]; if (p != null ) { Entry a = null ; Entry b = null ; Entry aHead = null ; Entry bHead = null ; while (p != null ) { if ((p.hash & table.length) == 0 ) { if (a != null ) { a.next = p; } else { aHead = p; } a = p; } else { if (b != null ) { b.next = p; } else { bHead = p; } b = p; } p = p.next; } if (a != null ) { a.next = null ; newTable[i] = aHead; } if (b != null ) { b.next = null ; newTable[i + table.length] = bHead; } } } table = newTable; threshold = (int ) (loadFactor * table.length); } Object remove (int hash, Object key) { int idx = hash & (table.length - 1 ); if (table[idx] == null ) { return null ; } Entry p = table[idx]; Entry prev = null ; while (p != null ) { if (p.key.equals(key)) { if (prev == null ) { table[idx] = p.next; } else { prev.next = p.next; } size--; return p.value; } prev = p; p = p.next; } return null ; } }
生成 hashCode
hash 算法是将任意对象,分配一个编号 的过程,其中编号是一个有限范围内的数字(如 int 范围内)
Object.hashCode
Object 的 hashCode 方法默认是生成随机数作为 hash 值(会缓存在对象头当中)
缺点是包含相同值 的不同对象,他们的 hashCode 不一样,不能够用 hash 值来反映对象的值 特征,因此诸多子类都会重写 hashCode 方法
String.hashCode
public static void main (String[] args) { String s1 = "bac" ; String s2 = new String ("abc" ); System.out.println(s1.hashCode()); System.out.println(s2.hashCode()); int hash = 0 ; for (int i = 0 ; i < s1.length(); i++) { char c = s1.charAt(i); System.out.println((int ) c); hash = (hash << 5 ) - hash + c; } System.out.println(hash); }
经验表明如果每次乘的是较大质数,可以有更好地降低 hash 冲突,因此改【乘 10】为【乘 31】
【乘 31】可以等价为【乘 32 - hash】,进一步可以转为更高效地【左移5位 - hash】
检查 hash 表的分散性
public void print () { int [] sum = new int [table.length]; for (int i = 0 ; i < table.length; i++) { Entry p = table[i]; while (p != null ) { sum[i]++; p = p.next; } } System.out.println(Arrays.toString(sum)); Map<Integer, Long> result = Arrays.stream(sum).boxed() .collect(Collectors.groupingBy(s -> s, Collectors.counting())); System.out.println(result); }
测试
public static void main (String[] args) throws IOException { HashTable table = new HashTable (); for (int i = 0 ; i < 200000 ; i++) { Object obj = new Object (); table.put(obj, obj); } table.print(); table = new HashTable (); List<String> strings = Files.readAllLines(Path.of("words" )); for (String string : strings) { table.put(string, string); } table.print(); }
MurmurHash
思考
我们的代码里使用了尾插法,如果改成头插法呢?
JDK 的 HashMap 中采用了将对象 hashCode 高低位相互异或的方式减少冲突,怎么理解
我们的 HashTable 中表格容量是 2 的 n 次方,很多优化都是基于这个前提,能否不用 2 的 n 次方作为表格容量?
JDK 的 HashMap 在链表长度过长会转换成红黑树,对此你怎么看
习题
E01. 两数之和-Leetcode 1
public class E01Leetcode1 { public int [] twoSum(int [] nums, int target) { HashMap<Integer, Integer> map = new HashMap <>(); for (int i = 0 ; i < nums.length; i++) { int k = target - nums[i]; if (map.containsKey(k)) { return new int []{i, map.get(k)}; } map.put(nums[i], i); } return null ; } }
注意:题目明确说明只会存在一个有效答案 ,因此不会执行到最后的 return null
E02. 无重复字符的最长字串-Leetcode 3
public int lengthOfLongestSubstring (String s) { HashMap<Character, Integer> map = new HashMap <>(); int begin = 0 ; int maxLength = 0 ; for (int end = 0 ; end < s.length(); end++) { char ch = s.charAt(end); if (map.containsKey(ch)) { begin = Math.max(begin, map.get(ch) + 1 ); map.put(ch, end); } else { map.put(ch, end); } System.out.println(s.substring(begin, end + 1 )); maxLength = Math.max(maxLength, end - begin + 1 ); } return maxLength; }
begin 调整时的解释,遇到重复的 begin 应该向右调整,例如
遇到重复的 a,这时 begin 应该调整到上个重复字符 a 索引加 1 处,即 map.get(‘a’) + 1 = 1,
但还有一种情况需要考虑,就是连续遇到两次重复,例如
遇到重复的 b,这时 begin 应该调整到上个重复字符 b 索引加 1 处,即 map.get(‘b’) + 1 = 2
不过接下来,又遇到了重复的 a,此时若还执行 map.get(‘a’) + 1 = 1,则 begin 相当于向左退了,不对
应该是 Math.max(2, map.get(‘a’) + 1),即 begin 应该是两个重复字符索引中更靠右者
题目中说明 s 由英文字母、数字、符号和空格组成,因此它的范围是有限的(在 0 ~127 之内),可以用数组来替代 HashMap 优化,如下
public int lengthOfLongestSubstring (String s) { int [] map = new int [128 ]; Arrays.fill(map, -1 ); int begin = 0 ; int maxLength = 0 ; for (int end = 0 ; end < s.length(); end++) { char ch = s.charAt(end); if (map[ch] != -1 ) { begin = Math.max(begin, map[ch] + 1 ); map[ch] = end; } else { map[ch] = end; } System.out.println(s.substring(begin, end + 1 )); maxLength = Math.max(maxLength, end - begin + 1 ); } return maxLength; }
E03. 字母异位词分组-Leetcode 49
解法1
public List<List<String>> groupAnagrams (String[] strs) { HashMap<String, List<String>> map = new HashMap <>(); for (String str : strs) { char [] chars = str.toCharArray(); Arrays.sort(chars); String key = new String (chars); List<String> strings = map.computeIfAbsent(key, k -> new ArrayList <>()); strings.add(str); } return new ArrayList <>(map.values()); }
解法2
static class ArrayKey { int [] key = new int [26 ]; public ArrayKey (String str) { for (int i = 0 ; i < str.length(); i++) { char ch = str.charAt(i); key[ch - 'a' ]++; } } @Override public boolean equals (Object o) { if (this == o) return true ; if (o == null || getClass() != o.getClass()) return false ; ArrayKey arrayKey = (ArrayKey) o; return Arrays.equals(key, arrayKey.key); } @Override public int hashCode () { return Arrays.hashCode(key); } } public List<List<String>> groupAnagrams (String[] strs) { HashMap<ArrayKey, List<String>> map = new HashMap <>(); for (String str : strs) { List<String> strings = map.computeIfAbsent(new ArrayKey (str), k -> new ArrayList <>()); strings.add(str); } return new ArrayList <>(map.values()); }
E04. 判断有没有重复元素-Leetcode 217
public boolean containsDuplicate (int [] nums) { HashSet<Integer> set = new HashSet <>(); for (int key : nums) { if (!set.add(key)) { return true ; } } return false ; }
E05. 找出出现一次的数字-Leetcode 136
解法1:用 HashSet
public int singleNumber (int [] nums) { HashSet<Integer> set = new HashSet <>(); for (int num : nums) { if (!set.add(num)) { set.remove(num); } } return set.toArray(new Integer [0 ])[0 ]; }
解法2:用 xor
public int singleNumber (int [] nums) { int num = nums[0 ]; for (int i = 1 ; i < nums.length; i++) { num = num ^ nums[i]; } return num; }
E06. 判断字母异位词-Leetcode 242
public boolean isAnagram (String s, String t) { return Arrays.equals(getKey(s), getKey(t)); } private static int [] getKey(String s) { int [] array = new int [26 ]; char [] chars = s.toCharArray(); for (char ch : chars) { array[ch - 97 ]++; } return array; }
其中用 s.toCharArray() 性能明显高于用 s.charAt() 一个个获取字符
E07. 第一个不重复字符-Leetcode 387
public int firstUniqChar (String s) { int [] array = new int [26 ]; char [] chars = s.toCharArray(); for (char ch : chars) { array[ch-97 ]++; } for (int i = 0 ; i < chars.length; i++) { char ch = chars[i]; if (array[ch - 97 ] == 1 ) { return i; } } return -1 ; }
E08. 出现次数最多的单词-Leetcode 819
简洁解法 14 ms
public String mostCommonWord (String paragraph, String[] banned) { Set<String> banSet = Set.of(banned); HashMap<String, Integer> map = new HashMap <>(); String[] split = paragraph.toLowerCase().split("[^A-Za-z]+" ); for (String key : split) { if (banSet.contains(key)) { continue ; } map.compute(key, (k, v) -> v == null ? 1 : v + 1 ); } Optional<Map.Entry<String, Integer>> optional = map.entrySet().stream().max(Map.Entry.comparingByValue()); return optional.map(Map.Entry::getKey).orElse(null ); }
后两行避免 lambda,12 ms
public String mostCommonWord (String paragraph, String[] banned) { Set<String> banSet = Set.of(banned); String[] split = paragraph.toLowerCase().split("[^A-Za-z]+" ); HashMap<String, Integer> map = new HashMap <>(); for (String key : split) { if (banSet.contains(key)) { continue ; } map.compute(key, (k, v) -> v == null ? 1 : v + 1 ); } Integer max = 0 ; String maxKey = null ; for (Map.Entry<String, Integer> e : map.entrySet()) { Integer value = e.getValue(); if (value > max) { max = value; maxKey = e.getKey(); } } return maxKey; }
避免正则匹配 5ms
public String mostCommonWord (String paragraph, String[] banned) { Set<String> banSet = Set.of(banned); HashMap<String, Integer> map = new HashMap <>(); char [] chars = paragraph.toLowerCase().toCharArray(); StringBuilder sb = new StringBuilder (); for (char ch : chars) { if (ch >= 'a' && ch <= 'z' ) { sb.append(ch); } else { put(banSet, map, sb); sb = new StringBuilder (); } } put(banSet, map, sb); Integer max = 0 ; String maxKey = null ; for (Map.Entry<String, Integer> e : map.entrySet()) { Integer value = e.getValue(); if (value > max) { max = value; maxKey = e.getKey(); } } return maxKey; } private static void put (Set<String> banSet, HashMap<String, Integer> map, StringBuilder sb) { if (sb.length() > 0 ) { String key = sb.toString(); if (!banSet.contains(key)) { map.compute(key, (k, v) -> v == null ? 1 : v + 1 ); } } }
sb 避免每次新建 4ms
E09. 根据前序与中序遍历结果构造二叉树-Leetcode105 Improved
public class E09Leetcode105Improved { HashMap<Integer, Integer> map = new HashMap <>(); public TreeNode buildTree (int [] preOrder, int [] inOrder) { for (int i = 0 ; i < inOrder.length; i++) { map.put(inOrder[i], i); } return helper(preOrder, 0 , 0 , inOrder.length - 1 ); } private TreeNode helper (int [] preOrder, int preBegin, int inBegin, int inEnd) { if (inBegin > inEnd) { return null ; } int rootValue = preOrder[preBegin]; TreeNode root = new TreeNode (rootValue); int i = map.get(rootValue); int leftSize = i - inBegin; System.out.println("元素:" + rootValue + " left[" + (preBegin + 1 ) + "] inOrder 索引范围[" + inBegin + "~" + (i - 1 ) + "]" ); System.out.println("元素:" + rootValue + " right[" + (preBegin + 1 + leftSize) + "] inOrder 索引范围[" + (i + 1 ) + "~" + inEnd + "]" ); root.left = helper(preOrder, preBegin + 1 , inBegin, i - 1 ); root.right = helper(preOrder, preBegin + 1 + leftSize, i + 1 , inEnd); return root; } public static void main (String[] args) { int [] preOrder = {1 , 2 , 4 , 3 , 6 , 7 }; int [] inOrder = {4 , 2 , 1 , 6 , 3 , 7 }; TreeNode root = new E09Leetcode105Improved ().buildTree(preOrder, inOrder); System.out.println(root); } }
E10. 根据中序与后序遍历结果构造二叉树-Leetcode106 Improved
public class E10Leetcode106Improved { HashMap<Integer, Integer> map = new HashMap <>(); public TreeNode buildTree (int [] inOrder, int [] postOrder) { for (int i = 0 ; i < inOrder.length; i++) { map.put(inOrder[i], i); } return helper(postOrder, postOrder.length - 1 , 0 , inOrder.length - 1 ); } private TreeNode helper (int [] postOrder, int postEnd, int inBegin, int inEnd) { if (inBegin > inEnd) { return null ; } int rootValue = postOrder[postEnd]; TreeNode root = new TreeNode (rootValue); Integer i = map.get(rootValue); int rightSize = inEnd - i; System.out.println("元素:" + rootValue + " left[" + (postEnd - 1 - rightSize) + "] inOrder 索引范围[" + inBegin + "~" + (i - 1 ) + "]" ); System.out.println("元素:" + rootValue + " right[" + (postEnd - 1 ) + "] inOrder 索引范围[" + (i + 1 ) + "~" + inEnd + "]" ); root.left = helper(postOrder, postEnd - 1 - rightSize, inBegin, i - 1 ); root.right = helper(postOrder, postEnd - 1 , i + 1 , inEnd); return root; } public static void main (String[] args) { int [] postOrder = {4 , 2 , 6 , 7 , 3 , 1 }; int [] inOrder = {4 , 2 , 1 , 6 , 3 , 7 }; TreeNode root = new E10Leetcode106Improved ().buildTree(inOrder, postOrder); System.out.println(root); } }
3.7 排序算法
概述
比较排序算法
算法
最好
最坏
平均
空间
稳定
思想
注意事项
冒泡
O(n)
O(n 2 n^2 n 2 )
O(n 2 n^2 n 2 )
O(1)
Y
比较
最好情况需要额外判断
选择
O(n 2 n^2 n 2 )
O(n 2 n^2 n 2 )
O(n 2 n^2 n 2 )
O(1)
N
比较
交换次数一般少于冒泡
堆
O(n l o g n nlogn n l o g n )
O(n l o g n nlogn n l o g n )
O(n l o g n nlogn n l o g n )
O(1)
N
选择
堆排序的辅助性较强,理解前先理解堆的数据结构
插入
O(n)
O(n 2 n^2 n 2 )
O(n 2 n^2 n 2 )
O(1)
Y
比较
插入排序对于近乎有序的数据处理速度比较快,复杂度有所下降,可以提前结束
希尔
O(nlogn)
O(n 2 n^2 n 2 )
O(n l o g n nlogn n l o g n )
O(1)
N
插入
gap序列的构造有多种方式,不同方式处理的数据复杂度可能不同
归并
O(n l o g n nlogn n l o g n )
O(n l o g n nlogn n l o g n )
O(n l o g n nlogn n l o g n )
O(n)
Y
分治
需要额外的O(n)的存储空间
快速
O(n l o g n nlogn n l o g n )
O(n 2 n^2 n 2 )
O(n l o g n nlogn n l o g n )
O(logn)
N
分治
快排可能存在最坏情况,需要把枢轴值选取得尽量随机化来缓解最坏情况下的时间复杂度
非比较排序算法
非比较排序算法
时间复杂度
空间复杂度
稳定性
计数排序
O(n+k)
O(n+k)
稳定
桶排序
O(n+k)
O(n+k)
稳定
基数排序
O(d*(n+k))
O(n+k)
稳定
其中
稳定 vs 不稳定
Java 中的排序
Arrays.sort
JDK 7~13 中的排序实现
排序目标
条件
采用算法
int[] long[] float[] double[]
size < 47
混合插入排序 (pair)
size < 286
双基准点快排
有序度低
双基准点快排
有序度高
归并排序
byte[]
size <= 29
插入排序
size > 29
计数排序
char[] short[]
size < 47
插入排序
size < 286
双基准点快排
有序度低
双基准点快排
有序度高
归并排序
size > 3200
计数排序
Object[]
-Djava.util.Arrays.useLegacyMergeSort=true
传统归并排序
TimSort
JDK 14~20 中的排序实现
排序目标
条件
采用算法
int[] long[] float[] double[]
size < 44 并位于最左侧
插入排序
size < 65 并不是最左侧
混合插入排序 (pin)
有序度低
双基准点快排
递归次数超过 384
堆排序
对于整个数组或非最左侧 size > 4096,有序度高
归并排序
byte[]
size <= 64
插入排序
size > 64
计数排序
char[] short[]
size < 44
插入排序
再大
双基准点快排
递归次数超过 384
计数排序
size > 1750
计数排序
Object[]
-Djava.util.Arrays.useLegacyMergeSort=true
传统归并排序
TimSort
其中 TimSort 是用归并+二分插入排序的混合排序算法
值得注意的是从 JDK 8 开始支持 Arrays.parallelSort 并行排序
根据最新的提交记录来看 JDK 21 可能会引入基数排序等优化
外部排序
1) 冒泡排序
要点
每轮冒泡不断地比较相邻 的两个元素,如果它们是逆序的,则交换它们的位置
下一轮冒泡,可以调整未排序的右边界,减少不必要比较
以数组 3、2、1 的冒泡排序为例,第一轮冒泡
第二轮冒泡
未排序区域内就剩一个元素,结束
优化手段:每次循环时,若能确定更合适的 右边界,则可以减少冒泡轮数
以数组 3、2、1、4、5 为例,第一轮结束后记录的 x,即为右边界
非递归版代码
public class BubbleSort { private static void bubble (int [] a) { int j = a.length - 1 ; while (true ) { int x = 0 ; for (int i = 0 ; i < j; i++) { if (a[i] > a[i + 1 ]) { int t = a[i]; a[i] = a[i + 1 ]; a[i + 1 ] = t; x = i; } } j = x; if (j == 0 ) { break ; } } } public static void main (String[] args) { int [] a = {6 , 5 , 4 , 3 , 2 , 1 }; System.out.println(Arrays.toString(a)); bubble(a); System.out.println(Arrays.toString(a)); } }
2) 选择排序
要点
每一轮选择,找出最大(最小)的元素,并把它交换到合适的位置
以下面的数组选择最大值为例
非递归实现
public class SelectionSort { public static void sort (int [] a) { for (int right = a.length - 1 ; right > 0 ; right--) { int max = right; for (int i = 0 ; i < right; i++) { if (a[i] > a[max]) { max = i; } } if (max != right) { swap(a, max, right); } } } private static void swap (int [] a, int i, int j) { int t = a[i]; a[i] = a[j]; a[j] = t; } public static void main (String[] args) { int [] a = {6 , 5 , 4 , 3 , 2 , 1 }; System.out.println(Arrays.toString(a)); sort(a); System.out.println(Arrays.toString(a)); } }
3) 堆排序
要点:
建立大顶堆
每次将堆顶元素(最大值)交换到末尾,调整堆顶元素,让它重新符合大顶堆特性
建堆
交换,下潜调整
代码
public class HeapSort { public static void sort (int [] a) { heapify(a, a.length); for (int right = a.length - 1 ; right > 0 ; right--) { swap(a, 0 , right); down(a, 0 , right); } } private static void heapify (int [] array, int size) { for (int i = size / 2 - 1 ; i >= 0 ; i--) { down(array, i, size); } } private static void down (int [] array, int parent, int size) { while (true ) { int left = parent * 2 + 1 ; int right = left + 1 ; int max = parent; if (left < size && array[left] > array[max]) { max = left; } if (right < size && array[right] > array[max]) { max = right; } if (max == parent) { break ; } swap(array, max, parent); parent = max; } } private static void swap (int [] a, int i, int j) { int t = a[i]; a[i] = a[j]; a[j] = t; } public static void main (String[] args) { int [] a = {2 , 3 , 1 , 7 , 6 , 4 , 5 }; System.out.println(Arrays.toString(a)); sort(a); System.out.println(Arrays.toString(a)); } }
4) 插入排序
要点
将数组分为两部分 [0 … low-1] [low … a.length-1]
左边 [0 … low-1] 是已排序部分
右边 [low … a.length-1] 是未排序部分
每次从未排序区域取出 low 位置的元素 , 插入到已排序区域
例
代码
public class InsertionSort { public static void sort (int [] a) { for (int low = 1 ; low < a.length; low++) { int t = a[low]; int i = low - 1 ; while (i >= 0 && t < a[i]) { a[i + 1 ] = a[i]; i--; } if (i != low - 1 ) { a[i + 1 ] = t; } } } public static void main (String[] args) { int [] a = {9 , 3 , 7 , 2 , 5 , 8 , 1 , 4 }; System.out.println(Arrays.toString(a)); sort(a); System.out.println(Arrays.toString(a)); } }
5) 希尔排序
要点
简单的说,就是分组实现插入,每组元素间隙称为 gap
每轮排序后 gap 逐渐变小,直至 gap 为 1 完成排序
对插入排序的优化,让元素更快速地交换到最终位置
下图演示了 gap = 4,gap = 2,gap = 1 的三轮排序前后比较
代码
public class ShellSort { public static void sort (int [] a) { for (int gap = a.length>>1 ; gap >0 ; gap=gap>>1 ) { for (int low = gap; low < a.length; low ++) { int t = a[low]; int i = low - gap; while (i >= 0 && t < a[i]) { a[i + gap] = a[i]; i -= gap; } if (i != low - gap) { a[i + gap] = t; } } } } public static void main (String[] args) { int [] a = {9 , 3 , 7 , 2 , 5 , 8 , 1 , 4 }; System.out.println(Arrays.toString(a)); sort(a); System.out.println(Arrays.toString(a)); } }
6) 归并排序
递归实现
要点
分 - 每次从中间切一刀,处理的数据少一半
治 - 当数据仅剩一个时可以认为有序
合 - 两个有序的结果,可以进行合并排序(参见数组练习 E01. 合并有序数组)
代码
public class MergeSortTopDown { public static void merge (int [] a1, int i, int iEnd, int j, int jEnd, int [] a2) { int k = i; while (i <= iEnd && j <= jEnd) { if (a1[i] < a1[j]) { a2[k] = a1[i]; i++; } else { a2[k] = a1[j]; j++; } k++; } if (i > iEnd) { System.arraycopy(a1, j, a2, k, jEnd - j + 1 ); } if (j > jEnd) { System.arraycopy(a1, i, a2, k, iEnd - i + 1 ); } } public static void sort (int [] a1) { int [] a2 = new int [a1.length]; split(a1, 0 , a1.length - 1 , a2); } private static void split (int [] a1, int left, int right, int [] a2) { int [] array = Arrays.copyOfRange(a1, left, right + 1 ); if (left == right) { return ; } int m = (left + right) >>> 1 ; split(a1, left, m, a2); split(a1, m + 1 , right, a2); merge(a1, left, m, m + 1 , right, a2); System.arraycopy(a2, left, a1, left, right - left + 1 ); } public static void main (String[] args) { int [] a = {9 , 3 , 7 , 2 , 8 , 5 , 1 , 4 }; System.out.println(Arrays.toString(a)); sort(a); System.out.println(Arrays.toString(a)); } }
时间复杂度
两个长度为 m 和 n 的链表合并,时间复杂度是 m + n
归并,时间复杂度:f ( n ) = 2 f ( n / 2 ) + n , f ( 1 ) = c f(n) = 2f(n/2) + n, f(1)=c f ( n ) = 2 f ( n / 2 ) + n , f ( 1 ) = c ,等价解 f ( n ) = n l o g 2 n + c n f(n) = nlog_2{n} + cn f ( n ) = n l o g 2 n + c n
8 / \ 4 4 / \ / \ 2 2 2 2 || || || || 11 11 11 11 f(8) = 2f(4) + 8 f(4) = 2f(2) + 4 f(2) = 2f(1) + 2 f(1) = 1 f(8) = 8 + 24 f(4) = 4 + 8 f(2) = 2 + 2 f(1) = 1
当 n = 16 时,结果 80
当 n = 64 时,结果 448
若逐一合并,时间复杂度:f ( n ) = ∑ n = 0 n − 1 n + 1 f(n)=\sum\limits_{n=0}^{n-1}n+1 f ( n ) = n = 0 ∑ n − 1 n + 1 ,等价解 f ( n ) = 1 2 ( n 2 + n ) f(n)=\frac{1}{2}(n^2+n) f ( n ) = 2 1 ( n 2 + n )
1|0 => 1 1|1 => 2 1|2 => 3 1|3 => 4 1|4 => 5 1|5 => 6 1|6 => 7 1|7 => 8 36
当 n = 16 时,结果 136
当 n = 64 时,结果 2080
非递归实现
public class MergeSortBottomUp { public static void merge (int [] a1, int i, int iEnd, int j, int jEnd, int [] a2) { int k = i; while (i <= iEnd && j <= jEnd) { if (a1[i] < a1[j]) { a2[k] = a1[i]; i++; } else { a2[k] = a1[j]; j++; } k++; } if (i > iEnd) { System.arraycopy(a1, j, a2, k, jEnd - j + 1 ); } if (j > jEnd) { System.arraycopy(a1, i, a2, k, iEnd - i + 1 ); } } public static void sort (int [] a1) { int n = a1.length; int [] a2 = new int [n]; for (int width = 1 ; width < n; width *= 2 ) { for (int i = 0 ; i < n; i += 2 * width) { int m = Integer.min(i + width - 1 , n - 1 ); int j = Integer.min(i + 2 * width - 1 , n - 1 ); System.out.println(i + " " + m + " " + j); merge(a1, i, m, m + 1 , j, a2); } System.arraycopy(a2, 0 , a1, 0 , n); } } public static void main (String[] args) { int [] a = {9 , 3 , 7 , 2 , 8 , 5 , 1 , 4 }; System.out.println(Arrays.toString(a)); sort(a); System.out.println(Arrays.toString(a)); } }
7) 归并+插入
小数据量且有序度高时,插入排序效果高
大数据量用归并效果好
可以结合二者
public class MergeInsertionSort { public static void insertion (int [] a, int left, int right) { for (int low = left + 1 ; low <= right; low++) { int t = a[low]; int i = low - 1 ; while (i >= left && t < a[i]) { a[i + 1 ] = a[i]; i--; } if (i != low - 1 ) { a[i + 1 ] = t; } } } public static void merge (int [] a1, int i, int iEnd, int j, int jEnd, int [] a2) { int k = i; while (i <= iEnd && j <= jEnd) { if (a1[i] < a1[j]) { a2[k] = a1[i]; i++; } else { a2[k] = a1[j]; j++; } k++; } if (i > iEnd) { System.arraycopy(a1, j, a2, k, jEnd - j + 1 ); } if (j > jEnd) { System.arraycopy(a1, i, a2, k, iEnd - i + 1 ); } } public static void sort (int [] a1) { int [] a2 = new int [a1.length]; split(a1, 0 , a1.length - 1 , a2); } private static void split (int [] a1, int left, int right, int [] a2) { if (right == left) { return ; } if (right - left <= 32 ) { insertion(a1, left, right); System.out.println("insert..." + left + " " + right +" " +Arrays.toString(a1)); return ; } int m = (left + right) >>> 1 ; split(a1, left, m, a2); split(a1, m + 1 , right, a2); System.out.println(left + " " + right + " " +Arrays.toString(a1)); merge(a1, left, m, m + 1 , right, a2); System.arraycopy(a2, left, a1, left, right - left + 1 ); } public static void main (String[] args) { int [] a = {9 , 3 , 7 , 2 , 8 , 5 , 1 , 4 }; System.out.println(Arrays.toString(a)); sort(a); System.out.println(Arrays.toString(a)); } }
8) 快速排序
单边循环(lomuto分区)要点
选择最右侧元素作为基准点
j 找比基准点小的,i 找比基准点大的,一旦找到,二者进行交换
交换时机:j 找到小的,且与 i 不相等
i 找到 >= 基准点元素后,不应自增
最后基准点与 i 交换,i 即为基准点最终索引
例:
i 和 j 都从左边出发向右查找,i 找到比基准点4大的5,j找到比基准点小的2,停下来交换
i 找到了比基准点大的5,j 找到比基准点小的3,停下来交换
j 到达right 处结束,right 与 i 交换,一轮分区结束
代码
public class QuickSortLomuto { public static void sort (int [] a) { quick(a, 0 , a.length - 1 ); } private static void quick (int [] a, int left, int right) { if (left >= right) { return ; } int p = partition(a, left, right); quick(a, left, p - 1 ); quick(a, p + 1 , right); } private static int partition (int [] a, int left, int right) { int pv = a[right]; int i = left; int j = left; while (j < right) { if (a[j] < pv) { if (i != j) { swap(a, i, j); } i++; } j++; } swap(a, i, right); return i; } private static void swap (int [] a, int i, int j) { int t = a[i]; a[i] = a[j]; a[j] = t; } public static void main (String[] args) { int [] a = {5 , 3 , 7 , 2 , 9 , 8 , 1 , 4 }; System.out.println(Arrays.toString(a)); sort(a); System.out.println(Arrays.toString(a)); } }
双边循环要点
选择最左侧元素作为基准点
j 找比基准点小的,i 找比基准点大的,一旦找到,二者进行交换
最后基准点与 i 交换,i 即为基准点最终索引
例:
i 找到比基准点大的5停下来,j 找到比基准点小的1停下来(包含等于),二者交换
i 找到8,j 找到3,二者交换,i 找到7,j 找到2,二者交换
i == j,退出循环,基准点与 i 交换
代码
public class QuickSortHoare { public static void sort (int [] a) { quick(a, 0 , a.length - 1 ); } private static void quick (int [] a, int left, int right) { if (left >= right) { return ; } int p = partition(a, left, right); quick(a, left, p - 1 ); quick(a, p + 1 , right); } private static int partition (int [] a, int left, int right) { int i = left; int j = right; int pv = a[left]; while (i < j) { while (i < j && a[j] > pv) { j--; } while (i < j && pv >= a[i]) { i++; } swap(a, i, j); } swap(a, left, j); return j; } private static void swap (int [] a, int i, int j) { int t = a[i]; a[i] = a[j]; a[j] = t; } public static void main (String[] args) { int [] a = {9 , 3 , 7 , 2 , 8 , 5 , 1 , 4 }; System.out.println(Arrays.toString(a)); sort(a); System.out.println(Arrays.toString(a)); } }
随机基准点
使用随机数作为基准点,避免万一最大值或最小值作为基准点导致的分区不均衡
例
改进代码
int idx = ThreadLocalRandom.current().nextInt(right - left + 1 ) + left;swap(a, idx, left);
处理重复值
如果重复值较多,则原来算法中的分区效果也不好,如下图中左侧所示,需要想办法改为右侧的分区效果
改进代码
public class QuickSortHandleDuplicate { public static void sort (int [] a) { quick(a, 0 , a.length - 1 ); } private static void quick (int [] a, int left, int right) { if (left >= right) { return ; } int p = partition(a, left, right); quick(a, left, p - 1 ); quick(a, p + 1 , right); } private static int partition (int [] a, int left, int right) { int idx = ThreadLocalRandom.current().nextInt(right - left + 1 ) + left; swap(a, left, idx); int pv = a[left]; int i = left + 1 ; int j = right; while (i <= j) { while (i <= j && a[i] < pv) { i++; } while (i <= j && a[j] > pv) { j--; } if (i <= j) { swap(a, i, j); i++; j--; } } swap(a, j, left); return j; } private static void swap (int [] a, int i, int j) { int t = a[i]; a[i] = a[j]; a[j] = t; } public static void main (String[] args) { int [] a = {2 , 1 , 3 , 2 }; System.out.println(Arrays.toString(a)); sort(a); System.out.println(Arrays.toString(a)); } }
核心思想是
改进前,i 只找大于的,j 会找小于等于的。一个不找等于、一个找等于,势必导致等于的值分布不平衡
改进后,二者都会找等于的交换,等于的值会平衡分布在基准点两边
细节:
因为一开始 i 就可能等于 j,因此外层循环需要加等于条件保证至少进入一次,让 j 能减到正确位置
内层 while 循环中 i <= j 的 = 也不能去掉,因为 i == j 时也要做一次与基准点的判断,好让 i 及 j 正确
i == j 时,也要做一次 i++ 和 j-- 使下次循环二者不等才能退出
因为最后退出循环时 i 会大于 j,因此最终与基准点交换的是 j
内层两个 while 循环的先后顺序不再重要
9) 计数排序
方法1(简化后的计数排序)
public static void sort (int [] a) { int min = a[0 ]; int max = a[0 ]; for (int i : a) { if (i > max) { max = i; } else if (i < min) { min = i; } } int [] counting = new int [max - min + 1 ]; for (int i : a) { counting[i - min]++; } int k = 0 ; for (int i = 0 ; i < counting.length; i++) { while (counting[i] > 0 ) { a[k] = i + min; counting[i]--; k++; } } }
针对 byte [],因为数据范围已知,省去了求最大、最小值的过程,java 中对 char[]、short[]、byte[] 的排序都可能采用 counting 排序
public static void sort (byte [] a) { int [] counting = new int [256 ]; for (int i : a) { counting[i & 0xFF ]++; } int k = a.length-1 ; for (int i = 128 + 256 ; k >= 0 ; ) { while (counting[--i & 0xFF ] ==0 ); int v = i & 0xFF ; int c = counting[i & 0xFF ]; for (int j = 0 ; j < c; j++) { a[k] = (byte ) v; k--; } } }
稳定计数排序
public static void sort2 (int [] a) { int min = a[0 ]; int max = a[0 ]; for (int i : a) { if (i > max) { max = i; } else if (i < min) { min = i; } } int [] counting = new int [max - min + 1 ]; for (int i : a) { counting[i - min]++; } for (int i = 1 ; i < counting.length; i++) { counting[i] = counting[i] + counting[i - 1 ]; } int [] b = new int [a.length]; for (int i = a.length - 1 ; i >= 0 ; i--) { int j = a[i] - min; counting[j]--; b[counting[j]] = a[i]; } System.arraycopy(b, 0 , a, 0 , a.length); }
10) 桶排序
初步实现
public class BucketSort { public static void main (String[] args) { int [] ages = {20 , 18 , 66 , 25 , 67 , 30 }; System.out.println(Arrays.toString(ages)); sort(ages); System.out.println(Arrays.toString(ages)); } public static void sort (int [] a) { DynamicArray[] buckets = new DynamicArray [10 ]; for (int i = 0 ; i < buckets.length; i++) { buckets[i] = new DynamicArray (); } for (int v : a) { DynamicArray bucket = buckets[v / 10 ]; bucket.addLast(v); } for (DynamicArray bucket : buckets) { System.out.println(Arrays.toString(bucket.array())); } int k = 0 ; for (DynamicArray bucket : buckets) { int [] array = bucket.array(); InsertionSort.sort(array); for (int v : array) { a[k++] = v; } } } }
通用
public class BucketSortGeneric { public static void main (String[] args) { int [] ages = {20 , 10 , 28 , 66 , 25 , 31 , 67 , 30 , 70 }; System.out.println(Arrays.toString(ages)); sort(ages, 20 ); System.out.println(Arrays.toString(ages)); } public static void sort (int [] a, int range) { int max = a[0 ]; int min = a[0 ]; for (int i = 1 ; i < a.length; i++) { if (a[i] > max) { max = a[i]; } if (a[i] < min) { min = a[i]; } } DynamicArray[] buckets = new DynamicArray [(max - min) / range + 1 ]; System.out.println(buckets.length); for (int i = 0 ; i < buckets.length; i++) { buckets[i] = new DynamicArray (); } for (int age : a) { buckets[(age - min) / range].addLast(age); } int k = 0 ; for (DynamicArray bucket : buckets) { int [] array = bucket.array(); InsertionSort.sort(array); System.out.println(Arrays.toString(array)); for (int v : array) { a[k++] = v; } } } }
11) 基数排序
public class RadixSort { public static void radixSort (String[] a, int length) { ArrayList<String>[] buckets = new ArrayList [128 ]; for (int i = 0 ; i < buckets.length; i++) { buckets[i] = new ArrayList <>(); } for (int i = length - 1 ; i >= 0 ; i--) { for (String s : a) { buckets[s.charAt(i)].add(s); } int k = 0 ; for (ArrayList<String> bucket : buckets) { for (String s : bucket) { a[k++] = s; } bucket.clear(); } } } public static void main (String[] args) { String[] phoneNumbers = new String [10 ]; phoneNumbers[0 ] = "138" ; phoneNumbers[1 ] = "139" ; phoneNumbers[2 ] = "136" ; phoneNumbers[3 ] = "137" ; phoneNumbers[4 ] = "135" ; phoneNumbers[5 ] = "134" ; phoneNumbers[6 ] = "150" ; phoneNumbers[7 ] = "151" ; phoneNumbers[8 ] = "152" ; phoneNumbers[9 ] = "157" ; RadixSort.radixSort(phoneNumbers, 3 ); for (String phoneNumber : phoneNumbers) { System.out.println(phoneNumber); } } }
基数排序是稳定排序,因此先排个位、再排十位,十位的排序不会打乱个位取值相等的元素顺序
习题
E01. 根据另一个数组次序排序-Leetcode 1122
public class E01Leetcode1122 { public int [] relativeSortArray(int [] arr1, int [] arr2) { int [] count = new int [1001 ]; for (int i : arr1) { count[i]++; } int [] result = new int [arr1.length]; int k = 0 ; for (int i : arr2) { while (count[i] > 0 ) { result[k++] = i; count[i]--; } } for (int i = 0 ; i < count.length; i++) { while (count[i] > 0 ) { result[k++] = i; count[i]--; } } return result; } }
E02. 按出现频率排序-Leetcode 1636
public class E02Leetcode1636 { public int [] frequencySort(int [] nums) { int [] count = new int [201 ]; for (int i : nums) { count[i + 100 ]++; } return Arrays.stream(nums).boxed().sorted((a, b) -> { int fa = count[a + 100 ]; int fb = count[b + 100 ]; if (fa == fb) { return Integer.compare(b, a); } else { return fa - fb; } }).mapToInt(Integer::intValue).toArray(); } }
E03. 最大间距-Leetcode 164
解法1:桶排序 - 超过内存 限制
public class E03Leetcode164_1 { public int maximumGap (int [] nums) { int n = nums.length; if (n < 2 ) { return 0 ; } sort(nums, 1 ); int ret = 0 ; for (int i = 1 ; i < n; i++) { ret = Math.max(ret, nums[i] - nums[i - 1 ]); } return ret; } public static void sort (int [] a, int range) { int max = a[0 ]; int min = a[0 ]; for (int i = 1 ; i < a.length; i++) { if (a[i] > max) { max = a[i]; } if (a[i] < min) { min = a[i]; } } DynamicArray[] buckets = new DynamicArray [(max - min) / range + 1 ]; for (int i = 0 ; i < buckets.length; i++) { buckets[i] = new DynamicArray (); } for (int age : a) { buckets[(age - min) / range].addLast(age); } int k = 0 ; for (DynamicArray bucket : buckets) { int [] array = bucket.array(); InsertionSort.sort(array); for (int v : array) { a[k++] = v; } } } public static void main (String[] args) { int [] nums = {13 , 26 , 16 , 11 }; int r = new E03Leetcode164_1 ().maximumGap(nums); System.out.println(r); } }
解法2:基数排序
public class E03Leetcode164 { public int maximumGap (int [] a) { if (a.length < 2 ) { return 0 ; } int max = a[0 ]; for (int i = 1 ; i < a.length; i++) { max = Math.max(a[i], max); } ArrayList<Integer>[] buckets = new ArrayList [10 ]; for (int i = 0 ; i < buckets.length; i++) { buckets[i] = new ArrayList <>(); } long exp = 1 ; while (max >= exp) { for (int j : a) { buckets[(j / (int ) exp) % 10 ].add(j); } int k = 0 ; for (ArrayList<Integer> bucket : buckets) { for (Integer i : bucket) { a[k++] = i; } bucket.clear(); } exp *= 10 ; } int r = 0 ; for (int i = 1 ; i < a.length; i++) { r = Math.max(r, a[i] - a[i - 1 ]); } return r; } public static void main (String[] args) { int [] nums = {3 , 6 , 16 , 1 }; int r = new E03Leetcode164 ().maximumGap(nums); System.out.println(r); } }
解法3:桶排序 - 合理化桶个数
public class E03Leetcode164_3 { public int maximumGap (int [] nums) { if (nums.length < 2 ) { return 0 ; } int max = nums[0 ]; int min = nums[0 ]; for (int i1 = 1 ; i1 < nums.length; i1++) { if (nums[i1] > max) { max = nums[i1]; } if (nums[i1] < min) { min = nums[i1]; } } int range = Math.max((max - min) / (nums.length - 1 ), 1 ); DynamicArray[] buckets = new DynamicArray [(max - min) / range + 1 ]; for (int i1 = 0 ; i1 < buckets.length; i1++) { buckets[i1] = new DynamicArray (); } for (int age : nums) { buckets[(age - min) / range].addLast(age); } int k = 0 ; for (DynamicArray bucket : buckets) { int [] array = bucket.array(); InsertionSort.sort(array); System.out.println(Arrays.toString(array)); for (int v : array) { nums[k++] = v; } } int r = 0 ; for (int i = 1 ; i < nums.length; i++) { r = Math.max(r, nums[i] - nums[i - 1 ]); } return r; } public static void main (String[] args) { int [] nums = {15252 , 16764 , 27963 , 7817 , 26155 , 20757 , 3478 , 22602 , 20404 , 6739 , 16790 , 10588 , 16521 , 6644 , 20880 , 15632 , 27078 , 25463 , 20124 , 15728 , 30042 , 16604 , 17223 , 4388 , 23646 , 32683 , 23688 , 12439 , 30630 , 3895 , 7926 , 22101 , 32406 , 21540 , 31799 , 3768 , 26679 , 21799 , 23740 }; int r = new E03Leetcode164_3 ().maximumGap(nums); System.out.println(r); } }
解法4:在解法3的基础上,只保留桶内最大最小值
public class E03Leetcode164_4 { public int maximumGap (int [] nums) { if (nums.length < 2 ) { return 0 ; } int max = nums[0 ]; int min = nums[0 ]; for (int i = 1 ; i < nums.length; i++) { if (nums[i] > max) { max = nums[i]; } if (nums[i] < min) { min = nums[i]; } } if (max == min) { return 0 ; } int range = Math.max(1 , (max - min) / nums.length); int size = (max - min) / range + 1 ; Pair[] buckets = new Pair [size]; for (int i : nums) { int idx = (i - min) / range; if (buckets[idx] == null ) { buckets[idx] = new Pair (); } buckets[idx].add(i); } System.out.println(Arrays.toString(buckets)); int r = 0 ; int lastMax = buckets[0 ].max; for (int i = 1 ; i < buckets.length; i++) { Pair pair = buckets[i]; if (pair != null ) { r = Math.max(r, pair.min - lastMax); lastMax = pair.max; } } return r; } static class Pair { int max = 0 ; int min = 1000_000_000 ; public void add (int v) { max = Math.max(max, v); min = Math.min(min, v); } @Override public String toString () { return "[" + min + "," + max + "]" ; } } public static void main (String[] args) { int [] nums = {9 , 1 , 6 , 5 }; int r = new E03Leetcode164_4 ().maximumGap(nums); System.out.println(r); } }
排序数组-Leetcode 912
排序链表-Leetcode 148
其它题目
题目编号
题目标题
排序算法类型
1122
数组的相对排序
计数排序
1636
按照频率将数组升序排序
计数排序
164
最大间距
基数排序、桶排序
315
计算右侧小于当前元素的个数
基数排序
347
前 K 个高频元素
桶排序
题目编号
题目标题
排序算法类型
75
颜色分类
三向切分快速排序
215
数组中的第K个最大元素
堆排序
493
翻转对
归并排序
493
翻转对
树状数组
524
通过删除字母匹配到字典里最长单词
循环排序
977
有序数组的平方
双指针法
四. 高级数据结构与算法设计
4.1 图
1) 概念
图是由顶点(vertex)和边(edge)组成的数据结构,例如
graph LR A--->B A--->C B--->D C--->D
该图有四个顶点:A、B、C、D 以及四条有向边,有向图中,边是单向的
有向 vs 无向
如果是无向图,那么边是双向的,下面是一个无向图的例子
graph LR A---B A---C B---D C---D
度
度 是指与该顶点相邻的边的数量
graph LR A((A))---B((B)) A---C((C)) B---D((D)) C---D D---E((E)) D---F((F)) E---F A & B & C & D & E & F
例如上图中
A、B、C、E、F 这几个顶点度数为 2
D 顶点度数为 4
有向图中,细分为入度 和出度 ,参见下图
graph LR A((A))-->B((B)) A-->C((C)) B-->D((D)) C-->D D-->E((E)) D-->F((F)) E-->F A & B & C & D & E & F
A (2 out / 0 in)
B、C、E (1 out / 1 in)
D (2 out / 2 in)
F (0 out / 2 in)
权
边可以有权重,代表从源顶点到目标顶点的距离、费用、时间或其他度量。
graph LR BJ((北京)) WH((武汉)) GZ((广州)) SH((上海)) BJ---800km-->WH BJ---1900km-->GZ BJ---1200km-->SH WH---1050km-->GZ WH---700km-->SH
路径
路径被定义为从一个顶点到另一个顶点的一系列连续边,例如上图中【北京】到【上海】有多条路径
路径长度
不考虑权重,长度就是边的数量
考虑权重,一般就是权重累加
环
在有向图中,从一个顶点开始,可以通过若干条有向边返回到该顶点,那么就形成了一个环
graph LR A((A)) B((B)) C((C)) D((D)) E((E)) A--->B B--->C C--->D D--->E E--->A
图的连通性
如果两个顶点之间存在路径,则这两个顶点是连通的,所有顶点都连通,则该图被称之为连通图,若子图连通,则称为连通分量
graph LR A --- B A --- C C --- D D --- E B --- E F --- G G --- H H --- F I --- J
2) 图的表示
比如说,下面的图
graph LR A---B A---C B---D C---D
用邻接矩阵 可以表示为:
A B C D A 0 1 1 0 B 1 0 0 1 C 1 0 0 1 D 0 1 1 0
用邻接表 可以表示为:
A -> B -> C B -> A -> D C -> A -> D D -> B -> C
有向图的例子
graph LR A--->B A--->C B--->D C--->D
A B C D A 0 1 1 0 B 0 0 0 1 C 0 0 0 1 D 0 0 0 0
A - B - C B - D C - D D - empty
3) Java 表示
顶点
public class Vertex { String name; List<Edge> edges; int inDegree; int status; boolean visited; private static final int INF = Integer.MAX_VALUE; int dist = INF; Vertex prev = null ; }
边
public class Edge { Vertex linked; int weight; public Edge (Vertex linked) { this (linked, 1 ); } public Edge (Vertex linked, int weight) { this .linked = linked; this .weight = weight; } }
4) DFS
public class Dfs { public static void main (String[] args) { Vertex v1 = new Vertex ("v1" ); Vertex v2 = new Vertex ("v2" ); Vertex v3 = new Vertex ("v3" ); Vertex v4 = new Vertex ("v4" ); Vertex v5 = new Vertex ("v5" ); Vertex v6 = new Vertex ("v6" ); v1.edges = List.of(new Edge (v3), new Edge (v2), new Edge (v6)); v2.edges = List.of(new Edge (v4)); v3.edges = List.of(new Edge (v4), new Edge (v6)); v4.edges = List.of(new Edge (v5)); v5.edges = List.of(); v6.edges = List.of(new Edge (v5)); dfs1(v1); } private static void dfs2 (Vertex v) { LinkedList<Vertex> stack = new LinkedList <>(); stack.push(v); while (!stack.isEmpty()) { Vertex pop = stack.pop(); pop.visited = true ; System.out.println(pop.name); for (Edge edge : pop.edges) { if (!edge.linked.visited) { stack.push(edge.linked); } } } } private static void dfs1 (Vertex v) { v.visited = true ; System.out.println(v.name); for (Edge edge : v.edges) { if (!edge.linked.visited) { dfs(edge.linked); } } } }
5) BFS
public class Bfs { public static void main (String[] args) { Vertex v1 = new Vertex ("v1" ); Vertex v2 = new Vertex ("v2" ); Vertex v3 = new Vertex ("v3" ); Vertex v4 = new Vertex ("v4" ); Vertex v5 = new Vertex ("v5" ); Vertex v6 = new Vertex ("v6" ); v1.edges = List.of(new Edge (v3), new Edge (v2), new Edge (v6)); v2.edges = List.of(new Edge (v4)); v3.edges = List.of(new Edge (v4), new Edge (v6)); v4.edges = List.of(new Edge (v5)); v5.edges = List.of(); v6.edges = List.of(new Edge (v5)); bfs(v1); } private static void bfs (Vertex v) { LinkedList<Vertex> queue = new LinkedList <>(); v.visited = true ; queue.offer(v); while (!queue.isEmpty()) { Vertex poll = queue.poll(); System.out.println(poll.name); for (Edge edge : poll.edges) { if (!edge.linked.visited) { edge.linked.visited = true ; queue.offer(edge.linked); } } } } }
6) 拓扑排序
graph LR HTML[网页基础] --> WEB SE[Java 基础] --> WEB[Java Web] DB[数据库] --> Spring WEB --> Spring[Spring框架] Spring --> Micro[微服务框架] Micro --> Project[实战项目]
Kahn
public class TopologicalSort { public static void main (String[] args) { Vertex v1 = new Vertex ("网页基础" ); Vertex v2 = new Vertex ("Java基础" ); Vertex v3 = new Vertex ("JavaWeb" ); Vertex v4 = new Vertex ("Spring框架" ); Vertex v5 = new Vertex ("微服务框架" ); Vertex v6 = new Vertex ("数据库" ); Vertex v7 = new Vertex ("实战项目" ); v1.edges = List.of(new Edge (v3)); v2.edges = List.of(new Edge (v3)); v3.edges = List.of(new Edge (v4)); v6.edges = List.of(new Edge (v4)); v4.edges = List.of(new Edge (v5)); v5.edges = List.of(new Edge (v7)); v7.edges = List.of(); List<Vertex> graph = List.of(v1, v2, v3, v4, v5, v6, v7); for (Vertex v : graph) { for (Edge edge : v.edges) { edge.linked.inDegree++; } } LinkedList<Vertex> queue = new LinkedList <>(); for (Vertex v : graph) { if (v.inDegree == 0 ) { queue.offer(v); } } List<String> result = new ArrayList <>(); while (!queue.isEmpty()) { Vertex poll = queue.poll(); result.add(poll.name); for (Edge edge : poll.edges) { edge.linked.inDegree--; if (edge.linked.inDegree == 0 ) { queue.offer(edge.linked); } } } if (result.size() != graph.size()) { System.out.println("出现环" ); } else { for (String key : result) { System.out.println(key); } } } }
DFS
public class TopologicalSortDFS { public static void main (String[] args) { Vertex v1 = new Vertex ("网页基础" ); Vertex v2 = new Vertex ("Java基础" ); Vertex v3 = new Vertex ("JavaWeb" ); Vertex v4 = new Vertex ("Spring框架" ); Vertex v5 = new Vertex ("微服务框架" ); Vertex v6 = new Vertex ("数据库" ); Vertex v7 = new Vertex ("实战项目" ); v1.edges = List.of(new Edge (v3)); v2.edges = List.of(new Edge (v3)); v3.edges = List.of(new Edge (v4)); v6.edges = List.of(new Edge (v4)); v4.edges = List.of(new Edge (v5)); v5.edges = List.of(new Edge (v7)); v7.edges = List.of(); List<Vertex> graph = List.of(v1, v2, v3, v4, v5, v6, v7); LinkedList<String> result = new LinkedList <>(); for (Vertex v : graph) { if (v.status==0 ) { dfs(v, result); } } System.out.println(result); } private static void dfs (Vertex v, LinkedList<String> result) { if (v.status == 2 ) { return ; } if (v.status == 1 ) { throw new RuntimeException ("发现环" ); } v.status = 1 ; for (Edge edge : v.edges) { dfs(edge.linked, result); } v.status = 2 ; result.push(v.name); } }
7) 最短路径
Dijkstra
Edsger Wybe Dijkstra
艾兹格·维布·迪克斯特拉(Edsger Wybe Dijkstra,/ˈdaɪkstrə/ DYKE-strə;荷兰语:[ˈɛtsxər ˈʋibə ˈdɛikstra] 1930年5月11日-2002年8月6日)是一位荷兰计算机科学家、程序员、软件工程师、系统科学家和科学散文家。他因对开发结构化编程语言做出的基础贡献而获得了1972年的图灵奖,并担任德克萨斯大学奥斯汀分校的斯伦贝谢百年计算机科学主席,任职时间从1984年到2000年。在他于2002年去世前不久,他因其在程序计算的自稳定性方面的工作而获得了ACM PODC分布式计算有影响力论文奖。为了纪念他,该年度奖项在接下来的一年更名为迪克斯特拉奖。
迪克斯特拉在计算机科学领域的贡献
最短路径算法,也称为迪克斯特拉算法,现代计算机科学本科课程中广泛教授
Shunting yard算法
THE OS 操作系统
银行家算法
用于协调多个处理器和程序的信号量构造
在分布式计算领域提出概念:自稳定性
graph LR 1--7-->2 1--9--->3 1--14--->6 6--9--->5 3--2--->6 2--15--->4 3--11--->4 4--6--->5
算法描述:
将所有顶点标记为未访问。创建一个未访问顶点的集合。
为每个顶点分配一个临时距离值
对于我们的初始顶点,将其设置为零
对于所有其他顶点,将其设置为无穷大。
每次选择最小临时距离的未访问顶点,作为新的当前顶点
对于当前顶点,遍历其所有未访问的邻居,并更新它们的临时距离为更小
例如,1->6 的距离是 14,而1->3->6 的距离是11。这时将距离更新为 11
否则,将保留上次距离值
当前顶点的邻居处理完成后,把它从未访问集合中删除
public class Dijkstra { public static void main (String[] args) { Vertex v1 = new Vertex ("v1" ); Vertex v2 = new Vertex ("v2" ); Vertex v3 = new Vertex ("v3" ); Vertex v4 = new Vertex ("v4" ); Vertex v5 = new Vertex ("v5" ); Vertex v6 = new Vertex ("v6" ); v1.edges = List.of(new Edge (v3, 9 ), new Edge (v2, 7 ), new Edge (v6, 14 )); v2.edges = List.of(new Edge (v4, 15 )); v3.edges = List.of(new Edge (v4, 11 ), new Edge (v6, 2 )); v4.edges = List.of(new Edge (v5, 6 )); v5.edges = List.of(); v6.edges = List.of(new Edge (v5, 9 )); List<Vertex> graph = List.of(v1, v2, v3, v4, v5, v6); dijkstra(graph, v1); } private static void dijkstra (List<Vertex> graph, Vertex source) { ArrayList<Vertex> list = new ArrayList <>(graph); source.dist = 0 ; while (!list.isEmpty()) { Vertex curr = chooseMinDistVertex(list); updateNeighboursDist(curr, list); list.remove(curr); } for (Vertex v : graph) { System.out.println(v.name + " " + v.dist); } } private static void updateNeighboursDist (Vertex curr, ArrayList<Vertex> list) { for (Edge edge : curr.edges) { Vertex n = edge.linked; if (list.contains(n)) { int dist = curr.dist + edge.weight; if (dist < n.dist) { n.dist = dist; } } } } private static Vertex chooseMinDistVertex (ArrayList<Vertex> list) { Vertex min = list.get(0 ); for (int i = 1 ; i < list.size(); i++) { if (list.get(i).dist < min.dist) { min = list.get(i); } } return min; } }
改进 - 优先级队列
创建一个优先级队列,放入所有顶点(队列大小会达到边的数量)
为每个顶点分配一个临时距离值
对于我们的初始顶点,将其设置为零
对于所有其他顶点,将其设置为无穷大。
每次选择最小临时距离的未访问顶点,作为新的当前顶点
对于当前顶点,遍历其所有未访问的邻居,并更新它们的临时距离为更小,若距离更新需加入队列
例如,1->6 的距离是 14,而1->3->6 的距离是11。这时将距离更新为 11
否则,将保留上次距离值
当前顶点的邻居处理完成后,把它从队列中删除
public class DijkstraPriorityQueue { public static void main (String[] args) { Vertex v1 = new Vertex ("v1" ); Vertex v2 = new Vertex ("v2" ); Vertex v3 = new Vertex ("v3" ); Vertex v4 = new Vertex ("v4" ); Vertex v5 = new Vertex ("v5" ); Vertex v6 = new Vertex ("v6" ); v1.edges = List.of(new Edge (v3, 9 ), new Edge (v2, 7 ), new Edge (v6, 14 )); v2.edges = List.of(new Edge (v4, 15 )); v3.edges = List.of(new Edge (v4, 11 ), new Edge (v6, 2 )); v4.edges = List.of(new Edge (v5, 6 )); v5.edges = List.of(); v6.edges = List.of(new Edge (v5, 9 )); List<Vertex> graph = List.of(v1, v2, v3, v4, v5, v6); dijkstra(graph, v1); } private static void dijkstra (List<Vertex> graph, Vertex source) { PriorityQueue<Vertex> queue = new PriorityQueue <>(Comparator.comparingInt(v -> v.dist)); source.dist = 0 ; for (Vertex v : graph) { queue.offer(v); } while (!queue.isEmpty()) { System.out.println(queue); Vertex curr = queue.peek(); if (!curr.visited) { updateNeighboursDist(curr, queue); curr.visited = true ; } queue.poll(); } for (Vertex v : graph) { System.out.println(v.name + " " + v.dist + " " + (v.prev != null ? v.prev.name : "null" )); } } private static void updateNeighboursDist (Vertex curr, PriorityQueue<Vertex> queue) { for (Edge edge : curr.edges) { Vertex n = edge.linked; if (!n.visited) { int dist = curr.dist + edge.weight; if (dist < n.dist) { n.dist = dist; n.prev = curr; queue.offer(n); } } } } }
问题
graph LR v1 --2--> v2 v1 --1--> v3 v2 --"-2"--> v3 v3 --1--> v4
按照 Dijkstra 算法,得出
v1 -> v2 最短距离2
v1 -> v3 最短距离1
v1 -> v4 最短距离2
事实应当是
v1 -> v2 最短距离2
v1 -> v3 最短距离0
v1 -> v4 最短距离1
Bellman-Ford
public class BellmanFord { public static void main (String[] args) { Vertex v1 = new Vertex ("v1" ); Vertex v2 = new Vertex ("v2" ); Vertex v3 = new Vertex ("v3" ); Vertex v4 = new Vertex ("v4" ); v1.edges = List.of(new Edge (v2, 2 )); v2.edges = List.of(new Edge (v3, -4 )); v3.edges = List.of(new Edge (v4, 1 ), new Edge (v1, 1 )); v4.edges = List.of(); List<Vertex> graph = List.of(v1, v2, v3, v4); bellmanFord(graph, v1); } private static void bellmanFord (List<Vertex> graph, Vertex source) { source.dist = 0 ; int size = graph.size(); for (int i = 0 ; i < size - 1 ; i++) { for (Vertex s : graph) { for (Edge edge : s.edges) { Vertex e = edge.linked; if (s.dist != Integer.MAX_VALUE && s.dist + edge.weight < e.dist) { e.dist = s.dist + edge.weight; e.prev = s; } } } } for (Vertex v : graph) { System.out.println(v + " " + (v.prev != null ? v.prev.name : "null" )); } } }
负环
graph LR v1 --2--> v2 v2 --"-4"--> v3 v3 --1--> v4 v3 --1--> v1
如果在【顶点-1】轮处理完成后,还能继续找到更短距离,表示发现了负环
Floyd-Warshall
graph LR v1 --"-2"--> v3 v2 --"4"--> v1 v2 --"3"--> v3 v3 --2--> v4 v4 --"-1"--> v2
public class FloydWarshall { public static void main (String[] args) { Vertex v1 = new Vertex ("v1" ); Vertex v2 = new Vertex ("v2" ); Vertex v3 = new Vertex ("v3" ); Vertex v4 = new Vertex ("v4" ); v1.edges = List.of(new Edge (v3, -2 )); v2.edges = List.of(new Edge (v1, 4 ), new Edge (v3, 3 )); v3.edges = List.of(new Edge (v4, 2 )); v4.edges = List.of(new Edge (v2, -1 )); List<Vertex> graph = List.of(v1, v2, v3, v4); floydWarshall(graph); } static void floydWarshall (List<Vertex> graph) { int size = graph.size(); int [][] dist = new int [size][size]; Vertex[][] prev = new Vertex [size][size]; for (int i = 0 ; i < size; i++) { Vertex v = graph.get(i); Map<Vertex, Integer> map = v.edges.stream().collect(Collectors.toMap(e -> e.linked, e -> e.weight)); for (int j = 0 ; j < size; j++) { Vertex u = graph.get(j); if (v == u) { dist[i][j] = 0 ; } else { dist[i][j] = map.getOrDefault(u, Integer.MAX_VALUE); prev[i][j] = map.get(u) != null ? v : null ; } } } print(prev); for (int k = 0 ; k < size; k++) { for (int i = 0 ; i < size; i++) { for (int j = 0 ; j < size; j++) { if (dist[i][k] != Integer.MAX_VALUE && dist[k][j] != Integer.MAX_VALUE && dist[i][k] + dist[k][j] < dist[i][j]) { dist[i][j] = dist[i][k] + dist[k][j]; prev[i][j] = prev[k][j]; } } } } print(prev); } static void path (Vertex[][] prev, List<Vertex> graph, int i, int j) { LinkedList<String> stack = new LinkedList <>(); System.out.print("[" + graph.get(i).name + "," + graph.get(j).name + "] " ); stack.push(graph.get(j).name); while (i != j) { Vertex p = prev[i][j]; stack.push(p.name); j = graph.indexOf(p); } System.out.println(stack); } static void print (int [][] dist) { System.out.println("-------------" ); for (int [] row : dist) { System.out.println(Arrays.stream(row).boxed() .map(x -> x == Integer.MAX_VALUE ? "∞" : String.valueOf(x)) .map(s -> String.format("%2s" , s)) .collect(Collectors.joining("," , "[" , "]" ))); } } static void print (Vertex[][] prev) { System.out.println("-------------------------" ); for (Vertex[] row : prev) { System.out.println(Arrays.stream(row).map(v -> v == null ? "null" : v.name) .map(s -> String.format("%5s" , s)) .collect(Collectors.joining("," , "[" , "]" ))); } } }
负环
如果在 3 层循环结束后,在 dist 数组的对角线处(i==j 处)发现了负数,表示出现了负环
8) 最小生成树
Prim
public class Prim { public static void main (String[] args) { Vertex v1 = new Vertex ("v1" ); Vertex v2 = new Vertex ("v2" ); Vertex v3 = new Vertex ("v3" ); Vertex v4 = new Vertex ("v4" ); Vertex v5 = new Vertex ("v5" ); Vertex v6 = new Vertex ("v6" ); Vertex v7 = new Vertex ("v7" ); v1.edges = List.of(new Edge (v2, 2 ), new Edge (v3, 4 ), new Edge (v4, 1 )); v2.edges = List.of(new Edge (v1, 2 ), new Edge (v4, 3 ), new Edge (v5, 10 )); v3.edges = List.of(new Edge (v1, 4 ), new Edge (v4, 2 ), new Edge (v6, 5 )); v4.edges = List.of(new Edge (v1, 1 ), new Edge (v2, 3 ), new Edge (v3, 2 ), new Edge (v5, 7 ), new Edge (v6, 8 ), new Edge (v7, 4 )); v5.edges = List.of(new Edge (v2, 10 ), new Edge (v4, 7 ), new Edge (v7, 6 )); v6.edges = List.of(new Edge (v3, 5 ), new Edge (v4, 8 ), new Edge (v7, 1 )); v7.edges = List.of(new Edge (v4, 4 ), new Edge (v5, 6 ), new Edge (v6, 1 )); List<Vertex> graph = List.of(v1, v2, v3, v4, v5, v6, v7); prim(graph, v1); } static void prim (List<Vertex> graph, Vertex source) { ArrayList<Vertex> list = new ArrayList <>(graph); source.dist = 0 ; while (!list.isEmpty()) { Vertex min = chooseMinDistVertex(list); updateNeighboursDist(min); list.remove(min); min.visited = true ; System.out.println("---------------" ); for (Vertex v : graph) { System.out.println(v); } } } private static void updateNeighboursDist (Vertex curr) { for (Edge edge : curr.edges) { Vertex n = edge.linked; if (!n.visited) { int dist = edge.weight; if (dist < n.dist) { n.dist = dist; n.prev = curr; } } } } private static Vertex chooseMinDistVertex (ArrayList<Vertex> list) { Vertex min = list.get(0 ); for (int i = 1 ; i < list.size(); i++) { if (list.get(i).dist < min.dist) { min = list.get(i); } } return min; } }
Kruskal
public class Kruskal { static class Edge implements Comparable <Edge> { List<Vertex> vertices; int start; int end; int weight; public Edge (List<Vertex> vertices, int start, int end, int weight) { this .vertices = vertices; this .start = start; this .end = end; this .weight = weight; } public Edge (int start, int end, int weight) { this .start = start; this .end = end; this .weight = weight; } @Override public int compareTo (Edge o) { return Integer.compare(this .weight, o.weight); } @Override public String toString () { return vertices.get(start).name + "<->" + vertices.get(end).name + "(" + weight + ")" ; } } public static void main (String[] args) { Vertex v1 = new Vertex ("v1" ); Vertex v2 = new Vertex ("v2" ); Vertex v3 = new Vertex ("v3" ); Vertex v4 = new Vertex ("v4" ); Vertex v5 = new Vertex ("v5" ); Vertex v6 = new Vertex ("v6" ); Vertex v7 = new Vertex ("v7" ); List<Vertex> vertices = List.of(v1, v2, v3, v4, v5, v6, v7); PriorityQueue<Edge> queue = new PriorityQueue <>(List.of( new Edge (vertices,0 , 1 , 2 ), new Edge (vertices,0 , 2 , 4 ), new Edge (vertices,0 , 3 , 1 ), new Edge (vertices,1 , 3 , 3 ), new Edge (vertices,1 , 4 , 10 ), new Edge (vertices,2 , 3 , 2 ), new Edge (vertices,2 , 5 , 5 ), new Edge (vertices,3 , 4 , 7 ), new Edge (vertices,3 , 5 , 8 ), new Edge (vertices,3 , 6 , 4 ), new Edge (vertices,4 , 6 , 6 ), new Edge (vertices,5 , 6 , 1 ) )); kruskal(vertices.size(), queue); } static void kruskal (int size, PriorityQueue<Edge> queue) { List<Edge> result = new ArrayList <>(); DisjointSet set = new DisjointSet (size); while (result.size() < size - 1 ) { Edge poll = queue.poll(); int s = set.find(poll.start); int e = set.find(poll.end); if (s != e) { result.add(poll); set.union(s, e); } } for (Edge edge : result) { System.out.println(edge); } } }
9) 不相交集合(并查集合)
基础
public class DisjointSet { int [] s; public DisjointSet (int size) { s = new int [size]; for (int i = 0 ; i < size; i++) { s[i] = i; } } public int find (int x) { if (x == s[x]) { return x; } return find(s[x]); } public void union (int x, int y) { s[y] = x; } @Override public String toString () { return Arrays.toString(s); } }
路径压缩
public int find (int x) { if (x == s[x]) { return x; } return s[x] = find(s[x]); }
Union By Size
public class DisjointSetUnionBySize { int [] s; int [] size; public DisjointSetUnionBySize (int size) { s = new int [size]; this .size = new int [size]; for (int i = 0 ; i < size; i++) { s[i] = i; this .size[i] = 1 ; } } public int find (int x) { if (x == s[x]) { return x; } return s[x] = find(s[x]); } public void union (int x, int y) { if (size[x] < size[y]) { int t = x; x = y; y = t; } s[y] = x; size[x] = size[x] + size[y]; } @Override public String toString () { return "内容:" +Arrays.toString(s) + "\n大小:" + Arrays.toString(size); } public static void main (String[] args) { DisjointSetUnionBySize set = new DisjointSetUnionBySize (5 ); set.union(1 , 2 ); set.union(3 , 4 ); set.union(1 , 3 ); System.out.println(set); } }
图-相关题目
题目编号
题目标题
算法思想
547
省份数量
DFS、BFS、并查集
797
所有可能路径
DFS、BFS
1584
连接所有点的最小费用
最小生成树
743
网络延迟时间
单源最短路径
787
K 站中转内最便宜的航班
单源最短路径
207
课程表
拓扑排序
210
课程表 II
拓扑排序
4.2 Greedy Algorithm
1) 贪心例子
称之为贪心算法或贪婪算法,核心思想是
将寻找最优解的问题分为若干个步骤
每一步骤都采用贪心原则,选取当前最优解
因为没有考虑所有可能,局部最优的堆叠不一定 让最终解最优
贪心算法是一种在每一步选择中都采取在当前状态下最好或最优(即最有利)的选择,从而希望导致结果是最好或最优的算法。这种算法通常用于求解优化问题,如最小生成树、背包问题等。
贪心算法的应用:
背包问题:给定一组物品和一个背包,每个物品有一定的重量和价值,要求在不超过背包容量的情况下,尽可能多地装入物品。
活动选择问题:在一个活动集合中,每次只能参加一个活动,问如何安排时间以最大化所有活动的收益。
编辑距离问题:给定两个字符串,求它们之间的最小编辑距离(即将一个字符串转换为另一个字符串所需的最少操作次数)。
网络流问题:给定一张有向图和一些起点和终点,求最大流量。
找零问题:给定一定数量的硬币和需要找零的金额,求使用最少的硬币数。
常见问题及解答:
贪心算法一定会找到最优解吗?
答:不一定。贪心算法只保证在每一步选择中都是最优的,但并不能保证整个问题的最优解。例如,背包问题中的贪心算法可能会导致最后一个物品没有被装入背包。
如何判断一个问题是否适合用贪心算法解决?
答:一个问题如果可以用递归的方式分解成若干个子问题,且每个子问题都有明确的最优解(即局部最优),那么这个问题就可以用贪心算法解决。
贪心算法的时间复杂度是多少?
答:贪心算法的时间复杂度取决于问题的规模和具体实现。一般来说,对于规模较小的问题,贪心算法的时间复杂度可以达到O(nlogn)或O(n2);对于规模较大的问题,可能需要O(n 3)或更高。
几个贪心的例子
Dijkstra
while (!list.isEmpty()) { Vertex curr = chooseMinDistVertex(list); updateNeighboursDist(curr); list.remove(curr); curr.visited = true ; }
没找到最短路径的例子:负边存在时,可能得不到正确解
问题出在贪心的原则会认为本次已经找到了该顶点的最短路径,下次不会再处理它(curr.visited = true)
与之对比,Bellman-Ford 并没有考虑局部距离最小的顶点,而是每次都处理所有边,所以不会出错,当然效率不如 Dijkstra
Prim
while (!list.isEmpty()) { Vertex curr = chooseMinDistVertex(list); updateNeighboursDist(curr); list.remove(curr); curr.visited = true ; }
Kruskal
while (list.size() < size - 1 ) { Edge poll = queue.poll(); int i = set.find(poll.start); int j = set.find(poll.end); if (i != j) { list.add(poll); set.union(i, j); } }
其它贪心的例子
2) 零钱兑换问题
有几个解(零钱兑换 II)Leetcode 518
public class Leetcode518 { public int change (int [] coins, int amount) { return rec(0 , coins, amount, new LinkedList <>(), true ); } public int rec (int index, int [] coins, int remainder, LinkedList<Integer> stack, boolean first) { if (!first) { stack.push(coins[index]); } int count = 0 ; if (remainder < 0 ) { print("无解:" , stack); } else if (remainder == 0 ) { print("有解:" , stack); count = 1 ; } else { for (int i = index; i < coins.length; i++) { count += rec(i, coins, remainder - coins[i], stack, false ); } } if (!stack.isEmpty()) { stack.pop(); } return count; } private static void print (String prompt, LinkedList<Integer> stack) { ArrayList<Integer> print = new ArrayList <>(); ListIterator<Integer> iterator = stack.listIterator(stack.size()); while (iterator.hasPrevious()) { print.add(iterator.previous()); } System.out.println(prompt + print); } public static void main (String[] args) { Leetcode518 leetcode = new Leetcode518 (); int count = leetcode.change(new int []{15 , 10 , 1 }, 21 ); System.out.println(count); } }
最优解(零钱兑换)- 穷举法 Leetcode 322
public class Leetcode322 { static int min = -1 ; public int coinChange (int [] coins, int amount) { rec(0 , coins, amount, new AtomicInteger (-1 ), new LinkedList <>(), true ); return min; } public void rec (int index, int [] coins, int remainder, AtomicInteger count, LinkedList<Integer> stack, boolean first) { if (!first) { stack.push(coins[index]); } count.incrementAndGet(); if (remainder == 0 ) { System.out.println(stack); if (min == -1 ) { min = count.get(); } else { min = Integer.min(min, count.get()); } } else if (remainder > 0 ) { for (int i = index; i < coins.length; i++) { rec(i, coins, remainder - coins[i], count, stack, false ); } } count.decrementAndGet(); if (!stack.isEmpty()) { stack.pop(); } } public static void main (String[] args) { Leetcode322 leetcode = new Leetcode322 (); int count = leetcode.coinChange(new int []{25 , 10 , 5 , 1 }, 41 ); System.out.println(count); } }
最优解(零钱兑换)- 贪心法 Leetcode 322
public class Leetcode322 { public int coinChange (int [] coins, int amount) { int remainder = amount; int count = 0 ; for (int coin : coins) { while (remainder - coin > 0 ) { remainder -= coin; count++; } if (remainder - coin == 0 ) { remainder = 0 ; count++; break ; } } if (remainder > 0 ) { return -1 ; } else { return count; } } public static void main (String[] args) { Leetcode322 leetcode = new Leetcode322 (); int count = leetcode.coinChange(new int []{5 , 2 , 1 }, 5 ); System.out.println(count); } }
3) Huffman 编码问题
问题引入
什么是编码?
简单说就是建立【字符】到【数字】的对应关系,如下面大家熟知的 ASC II 编码表,例如,可以查表得知字符【a】对应的数字是十六进制数【0x61】
\
00
01
02
03
04
05
06
07
08
09
0a
0b
0c
0d
0e
0f
0000
00
01
02
03
04
05
06
07
08
09
0a
0b
0c
0d
0e
0f
0010
10
11
12
13
14
15
16
17
18
19
1a
1b
1c
1d
1e
1f
0020
20
!
"
#
$
%
&
’
(
)
*
+
,
-
.
/
0030
0
1
2
3
4
5
6
7
8
9
:
;
<
=
>
?
0040
@
A
B
C
D
E
F
G
H
I
J
K
L
M
N
O
0050
P
Q
R
S
T
U
V
W
X
Y
Z
[
\
]
^
_
0060
`
a
b
c
d
e
f
g
h
i
j
k
l
m
n
o
0070
p
q
r
s
t
u
v
w
x
y
z
{
|
}
~
7f
注:一些直接以十六进制数字标识的是那些不可打印字符
传输时的编码
java 中每个 char 对应的数字会占用固定长度 2 个字节
如果在传输中仍采用上述规则,传递 abbccccccc 这 10 个字符
实际的字节为 0061006200620063006300630063006300630063(16进制表示)
总共 20 个字节,不经济
现在希望找到一种最节省字节的传输方式,怎么办?
假设传输的字符中只包含 a,b,c 这 3 个字符,有同学重新设计一张二进制编码表,见下图
现在还是传递 abbccccccc 这 10 个字符
实际的字节为 01110101010101010 (二进制表示)
总共需要 17 bits,也就是 2 个字节多一点,行不行?
不行,因为解码会出现问题,因为 10 会被错误的解码成 ba,而不是 c
解码后结果为 abbbababababababa,是错误的
怎么解决?必须保证编码后的二进制数字,要能区分它们的前缀(prefix-free)
用满二叉树结构编码,可以确保前缀不重复
向左走 0,向右走 1
走到叶子字符,累计起来的 0 和 1 就是该字符的二进制编码
再来试一遍
a 的编码 0
b 的编码 10
c 的编码 11
现在还是传递 abbccccccc 这 10 个字符
实际的字节为 0101011111111111111(二进制表示)
总共需要 19 bits,也是 2 个字节多一点,并且解码没有问题了,行不行?
这回解码没问题了,但并非最少字节,因为 c 的出现频率高(7 次)a 的出现频率低(1 次),因此出现频率高的字符编码成短数字更经济
考察下面的树
现在还是传递 abbccccccc 这 10 个字符
实际的字节为 000101 1111111 (二进制表示)
总共需要 13 bits,这棵树就称之为 Huffman 树
根据 Huffman 树对字符和数字进行编解码,就是 Huffman 编解码
Huffman 树
public class HuffmanTree { static class Node { Character ch; int freq; Node left; Node right; String code; public Node (Character ch) { this .ch = ch; } public Node (int freq, Node left, Node right) { this .freq = freq; this .left = left; this .right = right; } int freq () { return freq; } boolean isLeaf () { return left == null ; } @Override public String toString () { return "Node{" + "ch=" + ch + ", freq=" + freq + '}' ; } } String str; Map<Character, Node> map = new HashMap <>(); public HuffmanTree (String str) { this .str = str; char [] chars = str.toCharArray(); for (char c : chars) { Node node = map.computeIfAbsent(c, Node::new ); node.freq++; } PriorityQueue<Node> queue = new PriorityQueue <>(Comparator.comparingInt(Node::freq)); queue.addAll(map.values()); while (queue.size() >= 2 ) { Node x = queue.poll(); Node y = queue.poll(); int freq = x.freq + y.freq; queue.offer(new Node (freq, x, y)); } Node root = queue.poll(); int sum = dfs(root, new StringBuilder ()); for (Node node : map.values()) { System.out.println(node + " " + node.code); } System.out.println("总共会占用 bits:" + sum); } private int dfs (Node node, StringBuilder code) { int sum = 0 ; if (node.isLeaf()) { node.code = code.toString(); sum = node.freq * code.length(); } else { sum += dfs(node.left, code.append("0" )); sum += dfs(node.right, code.append("1" )); } if (code.length() > 0 ) { code.deleteCharAt(code.length() - 1 ); } return sum; } public static void main (String[] args) { new HuffmanTree ("abbccccccc" ); } }
注意
Node::new 是一个 Function,根据 key(即字符)生成 Node 对象
对应的是 public Node(Character ch) 有参构造
Huffman 编解码
补充两个方法,注意为了简单期间用了编解码都用字符串演示,实际应该按 bits 编解码
public class HuffmanTree { public String encode () { char [] chars = str.toCharArray(); StringBuilder sb = new StringBuilder (); for (char c : chars) { sb.append(map.get(c).code); } return sb.toString(); } public String decode (String str) { char [] chars = str.toCharArray(); int i = 0 ; StringBuilder sb = new StringBuilder (); Node node = root; while (i < chars.length) { if (!node.isLeaf()) { if (chars[i] == '0' ) { node = node.left; } else if (chars[i] == '1' ) { node = node.right; } i++; } if (node.isLeaf()) { sb.append(node.ch); node = root; } } return sb.toString(); } @SuppressWarnings("all") public static void main (String[] args) { HuffmanTree tree = new HuffmanTree ("abbccccccc" ); String encoded = tree.encode(); System.out.println(encoded); System.out.println(tree.decode(encoded)); } }
注意
循环中非叶子节点 i 要自增,但叶子节点 i 暂不自增
第一个非叶子的 if 判断结束后,仍需要第二个叶子的 if 判断,因为在第一个 if 内 node 发生了变化
相关题目
题目编号
题目标题
算法思路
1167(Plus 题目)
连接棒材的最低费用
Huffman 树、贪心
参考解答
public class Leetcode1167 { int connectSticks (int [] sticks) { PriorityQueue<Integer> queue = new PriorityQueue <>(); for (int stick : sticks) { queue.offer(stick); } int sum = 0 ; while (queue.size() >= 2 ) { Integer x = queue.poll(); Integer y = queue.poll(); int c = x + y; sum += c; queue.offer(c); } return sum; } public static void main (String[] args) { Leetcode1167 leetcode = new Leetcode1167 (); System.out.println(leetcode.connectSticks(new int []{1 , 8 , 3 , 5 })); System.out.println(leetcode.connectSticks(new int []{2 , 4 , 3 })); } }
4) 活动选择问题
public class ActivitySelectionProblem { static class Activity { int index; int start; int finish; public Activity (int index, int start, int finish) { this .index = index; this .start = start; this .finish = finish; } @Override public String toString () { return "Activity(" + index + ")" ; } } public static void main (String[] args) { Activity[] activities = new Activity []{ new Activity (0 , 1 , 3 ), new Activity (1 , 2 , 4 ), new Activity (2 , 3 , 5 ) }; select(activities, activities.length); } public static void select (Activity[] activities, int n) { List<Activity> result = new ArrayList <>(); int i, j; i = 0 ; result.add(activities[i]); for (j = 1 ; j < n; j++) { if (activities[j].start >= activities[i].finish) { result.add(activities[j]); i = j; } } System.out.println(result); } }
无重叠区间-Leetcode 435
题目编号
题目标题
算法思路
435
无重叠区间
贪心
参考解答
public int eraseOverlapIntervals (int [][] intervals) { Arrays.sort(intervals, Comparator.comparingInt(a -> a[1 ])); int i, j; i = 0 ; int count = 1 ; for (j = 1 ; j < intervals.length; j++) { if (intervals[j][0 ] >= intervals[i][1 ]) { i = j; count++; } } return intervals.length - count; }
找到不重叠的最多的活动数(count),即活动选择问题原始需求
在此基础上,活动总数 - count,就是题目要的排除数量
5) 分数背包问题
贪心法
public class FractionalKnapsackProblem { static class Item { int index; int weight; int value; public Item (int index, int weight, int value) { this .index = index; this .weight = weight; this .value = value; } int unitPrice () { return value / weight; } @Override public String toString () { return "Item(" + index + ")" ; } } public static void main (String[] args) { Item[] items = new Item []{ new Item (0 , 4 , 24 ), new Item (1 , 8 , 160 ), new Item (2 , 2 , 4000 ), new Item (3 , 6 , 108 ), new Item (4 , 1 , 4000 ), }; select(items, 10 ); } static void select (Item[] items, int total) { Arrays.sort(items, Comparator.comparingInt(Item::unitPrice).reversed()); int remainder = total; int max = 0 ; for (Item item : items) { if (remainder - item.weight > 0 ) { max += item.value; remainder -= item.weight; } else { max += remainder * item.unitPrice(); break ; } } System.out.println("最高价值为:" + max); } }
6) 0-1 背包问题
贪心法
可能得不到最优解
public class KnapsackProblem { static class Item { int index; int weight; int value; public Item (int index, int weight, int value) { this .index = index; this .weight = weight; this .value = value; } public int unitValue () { return value / weight; } @Override public String toString () { return "Item(" + index + ")" ; } } public static void main (String[] args) { Item[] items = new Item []{ new Item (0 , 1 , 1_000_000 ), new Item (1 , 4 , 1600 ), new Item (2 , 8 , 2400 ), new Item (3 , 5 , 30 ) }; select(items, 10 ); } static void select (Item[] items, int total) { Arrays.sort(items, Comparator.comparingInt(Item::unitValue).reversed()); int max = 0 ; for (Item item : items) { System.out.println(item); if (total >= item.weight) { total -= item.weight; max += item.value; } else { } } System.out.println("最大价值是:" + max); } }
贪心算法的局限
问题名称
是否能用贪心得到最优解
替换解法
Dijkstra(不存在负边)
✔️
Dijkstra(存在负边)
❌
Bellman-Ford
Prim
✔️
Kruskal
✔️
零钱兑换
❌
动态规划
Huffman 树
✔️
活动选择问题
✔️
分数背包问题
✔️
0-1 背包问题
❌
动态规划
7) Set cover problem
集合覆盖问题
4.3 Dynamic-Programming
1) Fibonacci
public class Fibonacci { public static void main (String[] args) { System.out.println(fibonacci(13 )); } public static int fibonacci (int n) { int [] dp = new int [n + 1 ]; dp[0 ] = 0 ; dp[1 ] = 1 ; if (n < 2 ) { return dp[n]; } for (int i = 2 ; i <= n; i++) { dp[i] = dp[i - 2 ] + dp[i - 1 ]; } return dp[n]; } }
降维
public class Fibonacci { public static void main (String[] args) { System.out.println(fibonacci(13 )); } public static int fibonacci (int n) { if (n == 0 ) { return 0 ; } if (n == 1 ) { return 1 ; } int a = 0 ; int b = 1 ; for (int i = 2 ; i <= n; i++) { int c = b + a; a = b; b = c; } return b; } }
2) 最短路径 - Bellman-Ford
public class BellmanFord { static class Edge { int from; int to; int weight; public Edge (int from, int to, int weight) { this .from = from; this .to = to; this .weight = weight; } } public static void main (String[] args) { List<Edge> edges = List.of( new Edge (6 , 5 , 9 ), new Edge (4 , 5 , 6 ), new Edge (1 , 6 , 14 ), new Edge (3 , 6 , 2 ), new Edge (3 , 4 , 11 ), new Edge (2 , 4 , 15 ), new Edge (1 , 3 , 9 ), new Edge (1 , 2 , 7 ) ); int [] dp = new int [7 ]; dp[1 ] = 0 ; for (int i = 2 ; i < dp.length; i++) { dp[i] = Integer.MAX_VALUE; } print(dp); for (int i = 0 ; i < 5 ; i++) { for (Edge e : edges) { if (dp[e.from] != Integer.MAX_VALUE) { dp[e.to] = Integer.min(dp[e.to], dp[e.from] + e.weight); } } } print(dp); } static void print (int [] dp) { System.out.println(Arrays.stream(dp) .mapToObj(i -> i == Integer.MAX_VALUE ? "∞" : String.valueOf(i)) .collect(Collectors.joining("," , "[" , "]" ))); } }
3) 不同路径-Leetcode 62
机器人要从左上角走到右下角,每次只能向右 或向下 ,问一共有多少条不同路径?
分析,先考虑较为简单的情况
可能路径有三种情况:
分析:设坐标为,共有 m 行 n 列
(0,0) (0,1) (1,0) (1,1) (2,0) (2,1)
如果终点是 (0,1) 那么只有一种走法
如果终点是 (1,0) 那么也只有一种走法
如果终点是 (1,1) 呢,它的走法是从它的上方走下来,或者从它的左边走过来,因此走法 = (0,1) + (1,0) = 2种
如果终点是 (2,0) 那么也只有一种走法
如果终点是 (2,1) 呢,它的走法是从它的上方走下来,或者从它的左边走过来,因此走法 = (1,1) + (2,0) = 3种
总结规律发现:
终点是 (0,1) (0,2) (0,3) … (0,n) 走法只有1种
终点是 (1,0) (2,0) (3,0) … (m,0) 走法也只有1种
除了上面两种情况以外,(i,j) 处的走法等于(i-1,j) + (i,j-1) 的走法之和,即为递推公式
画表格
0 1 1 1 1 1 1 1 2 3 4 5 6 7 1 3 6 10 15 21 28
题解
public class UniquePaths { public static void main (String[] args) { int count = new UniquePaths ().uniquePaths(3 , 7 ); System.out.println(count); } public int uniquePaths (int m, int n) { int [][] dp = new int [m][n]; for (int i = 0 ; i < m; i++) { dp[i][0 ] = 1 ; } for (int j = 0 ; j < n; j++) { dp[0 ][j] = 1 ; } for (int i = 1 ; i < m; i++) { for (int j = 1 ; j < n; j++) { dp[i][j] = dp[i - 1 ][j] + dp[i][j - 1 ]; } } return dp[m - 1 ][n - 1 ]; } }
降维
public class UniquePaths { public static void main (String[] args) { int count = new UniquePaths ().uniquePaths(3 , 7 ); System.out.println(count); } public int uniquePaths (int m, int n) { int [] dp = new int [n]; Arrays.fill(dp, 1 ); for (int i = 1 ; i < m; i++) { dp[0 ] = 1 ; for (int j = 1 ; j < n; j++) { dp[j] = dp[j] + dp[j - 1 ]; } } return dp[n - 1 ]; } }
类似于不规则的杨辉三角
4) 0-1 背包问题
public class KnapsackProblem { static class Item { int index; String name; int weight; int value; public Item (int index, String name, int weight, int value) { this .index = index; this .name = name; this .weight = weight; this .value = value; } @Override public String toString () { return "Item(" + name + ")" ; } } public static void main (String[] args) { Item[] items = new Item []{ new Item (1 , "黄金" , 4 , 1600 ), new Item (2 , "宝石" , 8 , 2400 ), new Item (3 , "白银" , 5 , 30 ), new Item (4 , "钻石" , 1 , 10_000 ), }; System.out.println(select(items, 10 )); } static int select (Item[] items, int total) { int [][] dp = new int [items.length][total + 1 ]; print(dp); Item item0 = items[0 ]; for (int j = 0 ; j < total + 1 ; j++) { if (j >= item0.weight) { dp[0 ][j] = item0.value; } } print(dp); for (int i = 1 ; i < dp.length; i++) { Item item = items[i]; for (int j = 1 ; j < total + 1 ; j++) { int x = dp[i - 1 ][j]; if (j >= item.weight) { int y = dp[i - 1 ][j - item.weight] + item.value; dp[i][j] = Integer.max(x, y); } else { dp[i][j] = x; } } print(dp); } return dp[dp.length - 1 ][total]; } static void print (int [][] dp) { System.out.println(" " + "-" .repeat(63 )); Object[] array = IntStream.range(0 , dp[0 ].length + 1 ).boxed().toArray(); System.out.printf(("%5d " .repeat(dp[0 ].length)) + "%n" , array); for (int [] d : dp) { array = Arrays.stream(d).boxed().toArray(); System.out.printf(("%5d " .repeat(d.length)) + "%n" , array); } } }
降维
static int select (Item[] items, int total) { int [] dp = new int [total + 1 ]; for (Item item : items) { for (int j = total; j > 0 ; j--) { if (j >= item.weight) { dp[j] = Integer.max(dp[j], item.value + dp[j - item.weight]); } } System.out.println(Arrays.toString(dp)); } return dp[total]; }
注意:内层循环需要倒序,否则 dp[j - item.weight] 的结果会被提前覆盖
5) 完全背包问题
public class KnapsackProblemComplete { static class Item { int index; String name; int weight; int value; public Item (int index, String name, int weight, int value) { this .index = index; this .name = name; this .weight = weight; this .value = value; } @Override public String toString () { return "Item(" + name + ")" ; } } public static void main (String[] args) { Item[] items = new Item []{ new Item (1 , "青铜" , 2 , 3 ), new Item (2 , "白银" , 3 , 4 ), new Item (3 , "黄金" , 4 , 7 ), }; System.out.println(select(items, 6 )); } private static int select (Item[] items, int total) { int [][] dp = new int [items.length][total + 1 ]; Item item0 = items[0 ]; for (int j = 0 ; j < total + 1 ; j++) { if (j >= item0.weight) { dp[0 ][j] = dp[0 ][j - item0.weight] + item0.value; } } print(dp); for (int i = 1 ; i < items.length; i++) { Item item = items[i]; for (int j = 1 ; j < total + 1 ; j++) { int x = dp[i - 1 ][j]; if (j >= item.weight) { int y = dp[i][j - item.weight] + item.value; dp[i][j] = Integer.max(x, y); } else { dp[i][j] = x; } } print(dp); } return dp[dp.length - 1 ][total]; } static void print (int [][] dp) { System.out.println(" " + "-" .repeat(63 )); Object[] array = IntStream.range(0 , dp[0 ].length + 1 ).boxed().toArray(); System.out.printf(("%5d " .repeat(dp[0 ].length)) + "%n" , array); for (int [] d : dp) { array = Arrays.stream(d).boxed().toArray(); System.out.printf(("%5d " .repeat(d.length)) + "%n" , array); } } }
降维
private static int select (Item[] items, int total) { int [] dp = new int [total + 1 ]; for (Item item : items) { for (int j = 0 ; j < total + 1 ; j++) { if (j >= item.weight) { dp[j] = Integer.max(dp[j], dp[j - item.weight] + item.value); } } System.out.println(Arrays.toString(dp)); } return dp[total]; }
6) 零钱兑换问题-Leetcode322
public class ChangeMakingProblemLeetcode322 { public int coinChange (int [] coins, int amount) { int max = amount + 1 ; int [][] dp = new int [coins.length][amount + 1 ]; for (int j = 1 ; j < amount + 1 ; j++) { if (j >= coins[0 ]) { dp[0 ][j] = 1 + dp[0 ][j - coins[0 ]]; } else { dp[0 ][j] = max; } } for (int i = 1 ; i < coins.length; i++) { for (int j = 1 ; j < amount + 1 ; j++) { if (j >= coins[i]) { dp[i][j] = Math.min(dp[i - 1 ][j], 1 + dp[i][j - coins[i]]); } else { dp[i][j] = dp[i - 1 ][j]; } } print(dp); } int r = dp[coins.length - 1 ][amount]; return r > amount ? -1 : r; } public static void main (String[] args) { ChangeMakingProblemLeetcode322 leetcode = new ChangeMakingProblemLeetcode322 (); int count = leetcode.coinChange(new int []{1 , 2 , 5 }, 5 ); System.out.println(count); } static void print (int [][] dp) { System.out.println("-" .repeat(18 )); Object[] array = IntStream.range(0 , dp[0 ].length + 1 ).boxed().toArray(); System.out.printf(("%2d " .repeat(dp[0 ].length)) + "%n" , array); for (int [] d : dp) { array = Arrays.stream(d).boxed().toArray(); System.out.printf(("%2d " .repeat(d.length)) + "%n" , array); } } }
降维
public int coinChange (int [] coins, int amount) { int [] dp = new int [amount + 1 ]; Arrays.fill(dp, amount + 1 ); dp[0 ] = 0 ; for (int coin : coins) { for (int j = coin; j < amount + 1 ; j++) { dp[j] = Math.min(dp[j], 1 + dp[j - coin]); } } int r = dp[amount]; return r > amount ? -1 : r; }
零钱兑换 II-Leetcode 518
public class ChangeMakingProblemLeetcode518 { public int change (int [] coins, int amount) { int [] dp = new int [amount + 1 ]; dp[0 ] = 1 ; for (int coin : coins) { for (int j = coin; j < amount + 1 ; j++) { dp[j] = dp[j] + dp[j - coin]; } } return dp[amount]; } public static void main (String[] args) { ChangeMakingProblemLeetcode518 leetcode = new ChangeMakingProblemLeetcode518 (); int count = leetcode.change(new int []{1 , 2 , 5 }, 5 ); System.out.println(count); } }
7) 钢条切割问题
public class CutRodProblem { static int cut (int [] values, int n) { int [][] dp = new int [values.length][n + 1 ]; for (int i = 1 ; i < values.length; i++) { int v = values[i]; for (int j = 1 ; j < n + 1 ; j++) { if (j >= i) { dp[i][j] = Integer.max(dp[i - 1 ][j], v + dp[i][j - i]); } else { dp[i][j] = dp[i - 1 ][j]; } } print(dp); } return dp[values.length - 1 ][n]; } public static void main (String[] args) { System.out.println(cut(new int []{0 , 1 , 5 , 8 , 9 }, 4 )); } }
降维
static int cut (int [] values, int n) { int [] dp = new int [n + 1 ]; for (int i = 1 ; i < values.length; i++) { int v = values[i]; for (int j = i; j < n + 1 ; j++) { dp[j] = Integer.max(dp[j], v + dp[j - i]); } System.out.println(Arrays.toString(dp)); } return dp[n]; }
本质上是完全背包问题,把钢条总长度看作背包容量,切分后的钢条看作物品。只是
类似题目 Leetcode-343 整数拆分
public class Leetcode343 { public int integerBreak (int n) { int [] dp = new int [n + 1 ]; Arrays.fill(dp, 1 ); dp[0 ] = 1 ; for (int i = 1 ; i < n; i++) { for (int j = 0 ; j < n + 1 ; j++) { if (j >= i) { dp[j] = Integer.max(dp[j], i * dp[j - i]); } } System.out.println(Arrays.toString(dp)); } return dp[n]; } public int integerBreak2 (int n) { int [][] dp = new int [n][n + 1 ]; Arrays.fill(dp[0 ], 1 ); for (int i = 1 ; i < n; i++) { dp[i][0 ] = 1 ; } for (int i = 1 ; i < n; i++) { for (int j = 0 ; j < n + 1 ; j++) { if (j >= i) { dp[i][j] = Integer.max(dp[i - 1 ][j], i * dp[i][j - i]); } else { dp[i][j] = dp[i - 1 ][j]; } } print(dp); } return dp[n - 1 ][n]; } public static void main (String[] args) { Leetcode343 code = new Leetcode343 (); System.out.println(code.integerBreak(4 )); System.out.println(code.integerBreak(10 )); } }
8) 最长公共子串
public class LCSubstring { static int lcs (String a, String b) { int [][] dp = new int [b.length()][a.length()]; int max = 0 ; for (int i = 0 ; i < b.length(); i++) { for (int j = 0 ; j < a.length(); j++) { if (a.charAt(j) == b.charAt(i)) { if (i == 0 || j == 0 ) { dp[i][j] = 1 ; } else { dp[i][j] = dp[i - 1 ][j - 1 ] + 1 ; } max = Integer.max(dp[i][j], max); } else { dp[i][j] = 0 ; } } } print(dp, a, b); return max; } static void print (int [][] dp, String a, String b) { System.out.println("-" .repeat(23 )); Object[] array = a.chars().mapToObj(i -> String.valueOf((char ) i)).toArray(); System.out.printf(" " +"%2s " .repeat(a.length()) + "%n" , array); for (int i = 0 ; i < b.length(); i++) { int [] d = dp[i]; array = Arrays.stream(d).boxed().toArray(); System.out.printf(b.charAt(i) + " " + "%2d " .repeat(d.length) + "%n" , array); } } public static void main (String[] args) { System.out.println(lcs("itheima" , "then" )); } }
类似题目 Leetcode-718 最长重复子数组
public class Leetcode718 { public int findLength (int [] nums1, int [] nums2) { int m = nums1.length + 1 ; int n = nums2.length + 1 ; int [] dp = new int [n]; int max = 0 ; for (int i = 1 ; i < m; i++) { for (int j = n - 1 ; j > 0 ; j--) { if (nums1[i - 1 ] == nums2[j - 1 ]) { dp[j] = dp[j - 1 ] + 1 ; max = Integer.max(max, dp[j]); } else { dp[j] = 0 ; } } } return max; } public int findLength1 (int [] nums1, int [] nums2) { int m = nums1.length; int n = nums2.length; int [] dp = new int [n]; int max = 0 ; for (int i = 0 ; i < m; i++) { for (int j = n - 1 ; j >= 0 ; j--) { if (nums1[i] == nums2[j]) { if (i == 0 || j == 0 ) { dp[j] = 1 ; } else { dp[j] = dp[j - 1 ] + 1 ; } max = Integer.max(max, dp[j]); } else { dp[j] = 0 ; } } } return max; } public int findLength2 (int [] nums1, int [] nums2) { int [][] dp = new int [nums1.length][nums2.length]; int max = 0 ; for (int i = 0 ; i < nums1.length; i++) { for (int j = 0 ; j < nums2.length; j++) { if (nums1[i] == nums2[j]) { if (i == 0 || j == 0 ) { dp[i][j] = 1 ; } else { dp[i][j] = dp[i - 1 ][j - 1 ] + 1 ; } max = Integer.max(max, dp[i][j]); } else { dp[i][j] = 0 ; } } } return max; } public static void main (String[] args) { Leetcode718 code = new Leetcode718 (); System.out.println(code.findLength(new int []{1 , 2 , 3 , 2 , 1 }, new int []{3 , 2 , 1 , 4 , 7 })); System.out.println(code.findLength(new int []{1 , 0 , 0 , 0 , 1 }, new int []{1 , 0 , 0 , 1 , 1 })); } }
9) 最长公共子序列
最长公共子序列-Leetcode 1143
public class LCSubsequence { public int longestCommonSubsequence (String text1, String text2) { int m = text1.length(); int n = text2.length(); int [][] dp = new int [m + 1 ][n + 1 ]; for (int i = 1 ; i < m + 1 ; i++) { char a = text1.charAt(i - 1 ); for (int j = 1 ; j < n + 1 ; j++) { char b = text2.charAt(j - 1 ); if (a == b) { dp[i][j] = dp[i - 1 ][j - 1 ] + 1 ; } else { dp[i][j] = Integer.max(dp[i - 1 ][j], dp[i][j - 1 ]); } } } print(dp, text2, text1); return dp[m][n]; } static void print (int [][] dp, String a, String b) { System.out.println("-" .repeat(23 )); Object[] array = a.chars().mapToObj(i -> String.valueOf((char ) i)).toArray(); System.out.printf(" " + "%2s " .repeat(a.length()) + "%n" , array); for (int i = 0 ; i < b.length(); i++) { int [] d = dp[i + 1 ]; array = Arrays.stream(d).boxed().toArray(); System.out.printf(b.charAt(i) + " " + "%2d " .repeat(d.length) + "%n" , array); } } public static void main (String[] args) { LCSubsequence code = new LCSubsequence (); System.out.println(code.longestCommonSubsequence("abcde" , "ace" )); System.out.println(code.longestCommonSubsequence("ba" , "yby" )); } }
两个字符串的删除操作-Leetcode 583
public class Leetcode538 { public static void main (String[] args) { Leetcode538 code = new Leetcode538 (); System.out.println(code.minDistance("leetcode" , "etco" )); System.out.println(code.minDistance("eat" , "sea" )); System.out.println(code.minDistance("park" , "spake" )); } public int minDistance (String word1, String word2) { int m = word1.length(); int n = word2.length(); char [] chars1 = word1.toCharArray(); char [] chars2 = word2.toCharArray(); int [][] dp = new int [m + 1 ][n + 1 ]; for (int i = 1 ; i < m + 1 ; i++) { int x = chars1[i - 1 ]; for (int j = 1 ; j < n + 1 ; j++) { int y = chars2[j - 1 ]; if (x == y) { dp[i][j] = dp[i - 1 ][j - 1 ] + 1 ; } else { dp[i][j] = Integer.max(dp[i - 1 ][j], dp[i][j - 1 ]); } } } return m + n - dp[m][n] - dp[m][n]; } }
10) 最长上升子序列-Leetcode 300
public class Leetcode300 { public int lengthOfLIS (int [] nums) { int [] dp = new int [nums.length]; Arrays.fill(dp, 1 ); for (int i = 1 ; i < nums.length; i++) { for (int j = 0 ; j < i; j++) { if (nums[i] > nums[j]) { dp[i] = Integer.max(dp[i], dp[j] + 1 ); } } System.out.println(Arrays.toString(dp)); } return Arrays.stream(dp).max().getAsInt(); } public static void main (String[] args) { Leetcode300 code = new Leetcode300 (); System.out.println(code.lengthOfLIS(new int []{1 , 3 , 6 , 4 , 9 })); } }
11) Catalan 数
public class Catalan { public static void main (String[] args) { System.out.println(catalan(6 )); } static int catalan (int n) { int [] dp = new int [n + 1 ]; dp[0 ] = 1 ; dp[1 ] = 1 ; for (int i = 2 ; i < n + 1 ; i++) { for (int j = 0 ; j < i; j++) { System.out.print("(" + j + " " + (i - 1 - j) + ")\t" ); dp[i] += dp[j] * dp[i - 1 - j]; } System.out.println(); System.out.println(Arrays.toString(dp)); } return dp[n]; } }
Leetcode-96 不同的二叉搜索树
class Solution { public int numTrees (int n) { int [] dp = new int [n + 1 ]; dp[0 ] = 1 ; dp[1 ] = 1 ; for (int j = 2 ; j < n + 1 ; j++) { for (int i = 0 ; i < j; i++) { dp[j] += dp[i] * dp[j - 1 - i]; } } return dp[n]; } }
Leetcode-22 括号生成
public class Leetcode22 { public List<String> generateParenthesis (int n) { ArrayList<String>[] dp = new ArrayList [n + 1 ]; dp[0 ] = new ArrayList <>(List.of("" )); dp[1 ] = new ArrayList <>(List.of("()" )); for (int j = 2 ; j < n + 1 ; j++) { dp[j] = new ArrayList <>(); for (int i = 0 ; i < j; i++) { System.out.printf("(%d,%d)\t" , i, j - 1 - i); for (String k1 : dp[i]) { for (String k2 : dp[j - 1 - i]) { dp[j].add("(" + k1 + ")" + k2); } } } System.out.println(dp[j]); } return dp[n]; } public static void main (String[] args) { Leetcode22 code = new Leetcode22 (); System.out.println(code.generateParenthesis(4 )); } }
买票找零问题
售票处售卖球票,每张票 50 元。有2n人前来买票
其中一半人手持 50 元钞票
另一半人手持 100 元钞票
若售票处开始没有任何零钱,问:有多少种排队方式,能够让售票顺畅进行。
思路:
把手持 50 元钞票的人视为左括号
把手持 100 元钞票的人视为右括号
左右括号合法配对,即先出现左括号,再出现右括号,就可以让售票顺畅执行
可以看到,问题又变成了求解 n 的卡特兰数
其它问题
题号
标题
Leetcode 331
验证二叉树的前序序列化
Leetcode 894
所有可能的满二叉树
12) 打家劫舍-Leetcode 198
public class HouseRobberLeetcode198 { public int rob (int [] nums) { int len = nums.length; if (len == 1 ) { return nums[0 ]; } int [] dp = new int [len]; dp[0 ] = nums[0 ]; dp[1 ] = Integer.max(nums[0 ], nums[1 ]); for (int i = 2 ; i < len; i++) { dp[i] = Integer.max(dp[i - 2 ] + nums[i], dp[i - 1 ]); } return dp[len - 1 ]; } public static void main (String[] args) { HouseRobberLeetcode198 code = new HouseRobberLeetcode198 (); System.out.println(code.rob(new int []{2 , 7 , 9 , 3 , 1 })); System.out.println(code.rob(new int []{2 , 1 , 1 , 2 })); } }
13) Travelling salesman problem
旅行商问题
java 代码
public class TravellingSalesmanProblem { public static void main (String[] args) { int [][] graph = { {0 , 1 , 2 , 3 }, {1 , 0 , 6 , 4 }, {2 , 6 , 0 , 5 }, {3 , 4 , 5 , 0 }, }; System.out.println(6 >> (0 -1 )); } static int tsp1 (int [][] graph) { int n = graph.length; int [][] dp = new int [1 << n][n]; for (int [] row : dp) { Arrays.fill(row, Integer.MAX_VALUE / 2 ); } dp[1 ][0 ] = 0 ; for (int mask = 1 ; mask < 1 << n; mask++) { for (int i = 0 ; i < n; i++) { if ((mask & 1 << i) == 0 ) continue ; for (int j = 0 ; j < n; j++) { if ((mask & 1 << j) != 0 ) continue ; dp[mask | 1 << j][j] = Math.min(dp[mask | 1 << j][j], dp[mask][i] + graph[i][j]); } } print(dp); } int res = Integer.MAX_VALUE; for (int i = 0 ; i < n; i++) { res = Math.min(res, dp[(1 << n) - 1 ][i] + graph[i][0 ]); } return res; } static boolean contains (int set, int city) { return (set >> (city - 1 ) & 1 ) == 1 ; } static int exclude (int set, int city) { return set ^ (1 << (city - 1 )); } static int tsp (int [][] g) { int n = g.length; int m = 1 << (n - 1 ); int [][] dp = new int [n][m]; for (int i = 0 ; i < n; i++) { dp[i][0 ] = g[i][0 ]; } for (int j = 1 ; j < m; j++) { for (int i = 0 ; i < n; i++) { dp[i][j] = Integer.MAX_VALUE / 2 ; if (contains(j, i)) continue ; for (int k = 1 ; k < n; k++) { if (contains(j, k)) { dp[i][j] = Math.min(dp[i][j], g[i][k] + dp[k][exclude(j, k)]); } } } print(dp); } return dp[0 ][m - 1 ]; } static void print (int [][] dist) { System.out.println("-------------------------" ); for (int [] row : dist) { System.out.println(Arrays.stream(row).boxed() .map(x -> x >= Integer.MAX_VALUE / 2 ? "∞" : String.valueOf(x)) .map(s -> String.format("%2s" , s)) .collect(Collectors.joining("," , "[" , "]" ))); } } }
其它题目
题号
标题
无
集合覆盖问题
无
扔鸡蛋问题
Leetcode 72
编辑距离
Leetcode 121
买股票的最佳时机
组合总和 IV-Leetcode 377
不要被题目名字误导了,本题类似于零钱兑换518题,区别在于零钱兑换求的是组合数,本题求的是排列数
public class CombinationLeetcode377 { static int combinationSum4 (int [] nums, int target) { return change(nums, target); } static int change (int [] coins, int amount) { int [] dp = new int [amount + 1 ]; dp[0 ] = 1 ; for (int j = 1 ; j < amount + 1 ; j++) { for (int coin : coins) { if (j >= coin) { dp[j] += dp[j - coin]; } } System.out.println(Arrays.toString(dp)); } return dp[amount]; } public static void main (String[] args) { System.out.println(combinationSum4(new int []{1 , 2 , 3 }, 4 )); } }
4.4 Divide and Conquer
1) 概述
分治思想
将大问题划分为两个到多个子问题
子问题可以继续拆分成更小的子问题,直到能够简单求解
如有必要,将子问题的解进行合并,得到原始问题的解
之前学过的一些经典分而治之的例子
二分查找
快速排序
归并排序
合并K个排序链表 - LeetCode 23
二分查找
public static int binarySearch (int [] a, int target) { return recursion(a, target, 0 , a.length - 1 ); } public static int recursion (int [] a, int target, int i, int j) { if (i > j) { return -1 ; } int m = (i + j) >>> 1 ; if (target < a[m]) { return recursion(a, target, i, m - 1 ); } else if (a[m] < target) { return recursion(a, target, m + 1 , j); } else { return m; } }
减而治之,每次搜索范围内元素减少一半
快速排序
public static void sort (int [] a) { quick(a, 0 , a.length - 1 ); } private static void quick (int [] a, int left, int right) { if (left >= right) { return ; } int p = partition(a, left, right); quick(a, left, p - 1 ); quick(a, p + 1 , right); }
分而治之,这次分区基准点,在划分后两个区域分别进行下次分区
归并排序
public static void sort (int [] a1) { int [] a2 = new int [a1.length]; split(a1, 0 , a1.length - 1 , a2); } private static void split (int [] a1, int left, int right, int [] a2) { int [] array = Arrays.copyOfRange(a1, left, right + 1 ); if (left == right) { return ; } int m = (left + right) >>> 1 ; split(a1, left, m, a2); split(a1, m + 1 , right, a2); merge(a1, left, m, m + 1 , right, a2); System.arraycopy(a2, left, a1, left, right - left + 1 ); }
分而治之,分到区间内只有一个元素,合并区间
合并K个排序链表 - LeetCode 23
public ListNode mergeKLists (ListNode[] lists) { if (lists.length == 0 ) { return null ; } return split(lists, 0 , lists.length - 1 ); } public ListNode split (ListNode[] lists, int i, int j) { System.out.println(i + " " + j); if (j == i) { return lists[i]; } int m = (i + j) >>> 1 ; return mergeTwoLists( split(lists, i, m), split(lists, m + 1 , j) ); }
分而治之,分到区间内只有一个链表,合并区间
对比动态规划
都需要拆分子问题
动态规划的子问题有重叠、因此需要记录之前子问题解,避免重复运算
分而治之的子问题无重叠
2) 快速选择算法
public class Utils { static int quick (int [] a, int left, int right, int index) { int p = partition(a, left, right); if (p == index) { return a[p]; } if (p < index) { return quick(a, p + 1 , right, index); } else { return quick(a, left, p - 1 , index); } } static int partition (int [] a, int left, int right) { int idx = ThreadLocalRandom.current().nextInt(right - left + 1 ) + left; swap(a, left, idx); int pv = a[left]; int i = left + 1 ; int j = right; while (i <= j) { while (i <= j && a[i] < pv) { i++; } while (i <= j && a[j] > pv) { j--; } if (i <= j) { swap(a, i, j); i++; j--; } } swap(a, j, left); return j; } static void swap (int [] a, int i, int j) { int t = a[i]; a[i] = a[j]; a[j] = t; } }
数组中第k个最大元素-Leetcode 215
public class FindKthLargestLeetcode215 { public int findKthLargest (int [] a, int k) { return Utils.quick(a, 0 , a.length - 1 , a.length - k); } public static void main (String[] args) { FindKthLargestLeetcode215 code = new FindKthLargestLeetcode215 (); System.out.println(code.findKthLargest(new int []{3 , 2 , 1 , 5 , 6 , 4 }, 2 )); System.out.println(code.findKthLargest(new int []{3 , 2 , 3 , 1 , 2 , 4 , 5 , 5 , 6 }, 4 )); } }
数组中位数
public class FindMedian { public static double findMedian (int [] nums) { if (nums.length % 2 != 0 ) { return findIndex(nums, nums.length / 2 ); } else { System.out.println((nums.length / 2 - 1 ) + "," + (nums.length / 2 )); int a = findIndex(nums, nums.length / 2 ); int b = findIndex(nums, nums.length / 2 - 1 ); return (a + b) / 2.0 ; } } public static void main (String[] args) { System.out.println(findMedian(new int []{3 , 1 , 5 , 4 })); System.out.println(findMedian(new int []{3 , 1 , 5 , 4 , 7 , 8 })); System.out.println(findMedian(new int []{4 , 5 , 1 })); System.out.println(findMedian(new int []{4 , 5 , 1 , 6 , 3 })); } static int findIndex (int [] a, int index) { return Utils.quick(a, 0 , a.length - 1 , index); } }
3) 快速幂-Leetcode 50
public class QuickPowLeetcode50 { static double myPow (double x, int n) { if (n == 0 ) { return 1 ; } double mul = 1 ; long N = n; if (n < 0 ) { N = -N; } while (N > 0 ) { if ((N & 1 ) == 1 ) { mul *= x; } x = x * x; N = N >> 1 ; } return n > 0 ? mul : 1 / mul; } static double myPow1 (double x, int n) { long N = n; if (N < 0 ) { return 1.0 / rec(x, -N); } return rec(x, n); } static double rec (double x, long n) { if (n == 0 ) { return 1 ; } if (n == 1 ) { return x; } double y = rec(x, n / 2 ); if ((n & 1 ) == 1 ) { return x * y * y; } return y * y; } public static void main (String[] args) { System.out.println(myPow(2 , 10 )); System.out.println(myPow(2.1 , 3 )); System.out.println(myPow(2 , -2 )); System.out.println(myPow(2 , 0 )); System.out.println(myPow(2 , -2147483648 )); } }
4) 平方根整数部分-Leetcode 69
public class SqrtLeetcode69 { static int mySqrt (int x) { int i = 1 , j = x; int r = 0 ; while (i <= j) { int m = (i + j) >>> 1 ; if (x / m >= m) { r = m; i = m+1 ; } else { j = m-1 ; } } return r; } public static void main (String[] args) { System.out.println(mySqrt(1 )); System.out.println(mySqrt(2 )); System.out.println(mySqrt(4 )); System.out.println(mySqrt(8 )); System.out.println(mySqrt(9 )); } }
while(i <= j) 含义是在此区间内,只要有数字还未尝试,就不算结束
r 的作用是保留最近一次当 m 2 < = x m^2 <= x m 2 < = x 的 m 的值
使用除法而非乘法,避免大数相乘越界
5) 至少k个重复字符的最长子串-Leetcode 395
public class LongestSubstringLeetcode395 { static int longestSubstring (String s, int k) { if (s.length() < k) { return 0 ; } int [] counts = new int [26 ]; char [] chars = s.toCharArray(); for (char c : chars) { counts[c - 'a' ]++; } System.out.println(Arrays.toString(counts)); for (int i = 0 ; i < chars.length; i++) { char c = chars[i]; int count = counts[c - 'a' ]; if (count > 0 && count < k) { int j = i + 1 ; while (j < s.length() && counts[chars[j] - 'a' ] < k) { j++; } System.out.println(s.substring(0 , i) + "\t" + s.substring(j)); return Integer.max( longestSubstring(s.substring(0 , i), k), longestSubstring(s.substring(j), k) ); } } return s.length(); } public static void main (String[] args) { System.out.println(longestSubstring("aaaccbbb" , 3 )); System.out.println(longestSubstring("dddxaabaaabaacciiiiefbff" , 3 )); } }
4.5 Backtracking Algorithm
1) 入门例子
public class Backtracking { public static void main (String[] args) { rec(1 , new LinkedList <>()); } static void rec (int n, LinkedList<String> list) { if (n == 3 ) { return ; } System.out.println("before:" + list); list.push("a" ); rec(n + 1 , list); list.pop(); System.out.println("after:" + list); } }
2) 全排列-Leetcode 46
public class PermuteLeetcode46 { static List<List<Integer>> permute (int [] nums) { boolean [] visited = new boolean [nums.length]; LinkedList<Integer> stack = new LinkedList <>(); List<List<Integer>> r = new ArrayList <>(); rec(nums, visited, stack, r); return r; } static void rec (int [] nums, boolean [] visited, LinkedList<Integer> stack, List<List<Integer>> r) { if (stack.size() == nums.length) { r.add(new ArrayList <>(stack)); return ; } for (int i = 0 ; i < nums.length; i++) { if (visited[i]){ continue ; } stack.push(nums[i]); visited[i] = true ; rec(nums, visited, stack, r); stack.pop(); visited[i] = false ; } } public static void main (String[] args) { List<List<Integer>> permute = permute(new int []{1 , 2 , 3 }); for (List<Integer> s : permute) { System.out.println(s); } } }
3) 全排列II-Leetcode 47
public class PermuteLeetcode47 { static List<List<Integer>> permuteUnique (int [] nums) { Arrays.sort(nums); List<List<Integer>> result = new ArrayList <>(); dfs(nums, new boolean [nums.length], new LinkedList <>(), result); return result; } static void dfs (int [] nums, boolean [] visited, LinkedList<Integer> stack, List<List<Integer>> result) { if (stack.size() == nums.length) { result.add(new ArrayList <>(stack)); return ; } for (int i = 0 ; i < nums.length; i++) { if (i > 0 && nums[i] == nums[i - 1 ] && !visited[i-1 ]) { continue ; } if (!visited[i]) { stack.push(nums[i]); visited[i] = true ; dfs(nums, visited, stack, result); visited[i] = false ; stack.pop(); } } } public static void main (String[] args) { int [] nums = {1 , 1 , 3 }; List<List<Integer>> permute = permuteUnique(nums); for (List<Integer> list : permute) { System.out.println(list); } } }
排好序,这样重复的数字会相邻
定好规则:必须 1 固定之后才能固定 1’,即 1 的 visited = true 才能继续处理 1’
在遍历时,遇到了 nums[i] == nums[i - 1]
(即 1 和 1‘ 这种情况),进一步检查 i-1 位置的数字有没有 visited,没有,则不处理(剪枝)
4) 组合-Leetcode 77
public class CombinationLeetcode77 { static List<List<Integer>> combinationSum (int n, int k) { List<List<Integer>> result = new ArrayList <>(); dfs(n, k, 1 , new LinkedList <>(), result); return result; } static int count = 0 ; static void dfs (int n, int k, int start, LinkedList<Integer> stack, List<List<Integer>> result) { count++; if (k == 0 ) { result.add(new ArrayList <>(stack)); System.out.println(stack); return ; } for (int i = start; i <= n; i++) { if (k > n - i + 1 ) { continue ; } stack.push(i); dfs(n, k - 1 , i + 1 , stack, result); stack.pop(); } } public static void main (String[] args) { List<List<Integer>> lists = combinationSum(5 , 4 ); System.out.println(count); } }
k 代表剩余要组合的个数
n - i + 1
代表剩余可用数字
剪枝条件是:剩余可用数字要大于剩余组合数
为啥放在外面不行?即这行代码:if (k > n - start + 1) { return; }
因为它只考虑了 start 一种情况,而实际在循环内要处理的是 start,start+1 … n 这多种情况
似乎 ArrayList 作为 stack 性能高一些,见下面代码,但是这道题在 leetcode 上执行时间不稳定,相同代码都会有较大时间差异(15ms vs 9ms)
class Solution { public List<List<Integer>> combine (int n, int k) { List<List<Integer>> result = new ArrayList <>(); if (k == 0 || n < k) return result; dfs(n, k, 1 , new ArrayList <>(), result); return result; } static void dfs (int n, int k, int start, ArrayList<Integer> stack, List<List<Integer>> result) { if (k == 0 ) { result.add(new ArrayList <>(stack)); return ; } for (int i = start; i <= n; i++) { if (k-1 > n - i) { continue ; } stack.add(i); dfs(n, k - 1 , i + 1 , stack, result); stack.remove(stack.size()-1 ); } } }
5) 组合总和-Leetcode 39
public class CombinationLeetcode39 { static List<List<Integer>> combinationSum (int [] candidates, int target) { List<List<Integer>> result = new ArrayList <>(); dfs(target, 0 ,candidates, new LinkedList <>(), result); return result; } static void dfs (int target, int start, int [] candidates, LinkedList<Integer> stack, List<List<Integer>> result) { if (target == 0 ) { result.add(new ArrayList <>(stack)); return ; } for (int i = start; i < candidates.length; i++) { int candidate = candidates[i]; if (target < candidate) { continue ; } stack.push(candidate); dfs(target - candidate, i, candidates, stack, result); stack.pop(); } } public static void main (String[] args) { List<List<Integer>> lists = combinationSum(new int []{6 , 3 , 2 , 7 }, 7 ); for (List<Integer> list : lists) { System.out.println(list); } } }
与之前的零钱兑换问题其实是一样的,只是
本题求的是:所有组合的具体信息
零钱兑换问题求的是:所有组合中数字最少的、所有组合个数…
6) 组合总和 II-Leetcode 40
public class CombinationLeetcode40 { static List<List<Integer>> combinationSum2 (int [] candidates, int target) { Arrays.sort(candidates); List<List<Integer>> result = new ArrayList <>(); dfs(target, 0 , candidates, new boolean [candidates.length], new LinkedList <>(), result); return result; } static void dfs (int target, int start, int [] candidates, boolean [] visited, LinkedList<Integer> stack, List<List<Integer>> result) { if (target == 0 ) { result.add(new ArrayList <>(stack)); return ; } for (int i = start; i < candidates.length; i++) { int candidate = candidates[i]; if (target < candidate) { continue ; } if (i > 0 && candidate == candidates[i - 1 ] && !visited[i - 1 ]) { continue ; } visited[i] = true ; stack.push(candidate); dfs(target - candidate, i + 1 , candidates, visited, stack, result); stack.pop(); visited[i] = false ; } } public static void main (String[] args) { int [] candidates = {10 , 1 , 2 , 7 , 6 , 1 , 5 }; List<List<Integer>> lists = combinationSum2(candidates, 8 ); for (List<Integer> list : lists) { System.out.println(list); } } }
7) 组合总和 III-Leetcode 216
public class CombinationLeetcode216 { static List<List<Integer>> combinationSum3 (int k, int target) { List<List<Integer>> result = new ArrayList <>(); dfs(1 , target, k, new LinkedList <>(), result); return result; } static int count = 0 ; static void dfs (int start, int target, int k, LinkedList<Integer> stack, List<List<Integer>> result) { count++; if (target == 0 && stack.size() == k) { result.add(new ArrayList <>(stack)); return ; } for (int i = start; i <= 9 ; i++) { if (target < i){ continue ; } if (stack.size() == k) { continue ; } stack.push(i); dfs(i + 1 , target - i, k, stack, result); stack.pop(); } } public static void main (String[] args) { List<List<Integer>> lists = combinationSum3(2 , 18 ); for (List<Integer> list : lists) { System.out.println(list); } System.out.println(count); } }
这道题更类似于 77 题,区别在于
77 题的数字范围 n 更大 [1,20],而本题数字范围限制为 [1,9]
本题不仅仅找到组合,还要让组合之和等于 target(类似于 39 题)
剪枝优化
如果剩余的和 target 还没 i 大,这时减完 i 是负数,肯定无法满足要求,因此有剪枝条件:
如果组合的数字个数已经到达了上限 k,还没有凑够 target,也没必要继续递归,因此有:
8) N 皇后 Leetcode 51
public class NQueenLeetcode51 { static List<List<String>> solveNQueens (int n) { List<List<String>> result = new ArrayList <>(); char [][] table = new char [n][n]; for (int i = 0 ; i < n; i++) { Arrays.fill(table[i], '.' ); } dfs(0 , n, table, result); return result; } static void dfs (int i, int n, char [][] table, List<List<String>> result) { if (i == n) { ArrayList<String> list = new ArrayList <>(); for (char [] chars : table) { list.add(String.valueOf(chars)); } result.add(list); return ; } for (int j = 0 ; j < n; j++) { if (notValid(table, i, j)) { continue ; } table[i][j] = 'Q' ; dfs(i + 1 , n, table, result); table[i][j] = '.' ; } } static boolean notValid (char [][] table, int row, int col) { int n = table.length; for (int i = 0 ; i < n; i++) { if (table[i][col] == 'Q' ) { return true ; } } for (int i = row - 1 , j = col - 1 ; i >= 0 && j >= 0 ; i--, j--) { if (table[i][j] == 'Q' ) { return true ; } } for (int i = row - 1 , j = col + 1 ; i >= 0 && j < n; i--, j++) { if (table[i][j] == 'Q' ) { return true ; } } return false ; } public static void main (String[] args) { int count = 0 ; for (List<String> table : solveNQueens(8 )) { for (String row : table) { System.out.println(row); } count++; System.out.println("--------------------- " + count); } } }
public class NQueenLeetcode51 { static List<List<String>> solveNQueens (int n) { List<List<String>> result = new ArrayList <>(); char [][] table = new char [n][n]; boolean [] va = new boolean [n]; boolean [] vb = new boolean [2 * n - 1 ]; boolean [] vc = new boolean [2 * n - 1 ]; for (int i = 0 ; i < n; i++) { Arrays.fill(table[i], '.' ); } dfs(0 , n, table, result, va, vb, vc); return result; } static void dfs (int i, int n, char [][] table, List<List<String>> result, boolean [] va, boolean [] vb, boolean [] vc) { if (i == n) { ArrayList<String> list = new ArrayList <>(); for (char [] chars : table) { list.add(String.valueOf(chars)); } result.add(list); return ; } for (int j = 0 ; j < n; j++) { if (va[j] || vb[i + j] || vc[i - j + n - 1 ]) { continue ; } va[j] = true ; vb[i + j] = true ; vc[i - j + n - 1 ] = true ; table[i][j] = 'Q' ; dfs(i + 1 , n, table, result, va, vb, vc); table[i][j] = '.' ; va[j] = false ; vb[i + j] = false ; vc[i - j + n - 1 ] = false ; } } public static void main (String[] args) { int count = 0 ; for (List<String> table : solveNQueens(4 )) { for (String row : table) { System.out.println(row); } count++; System.out.println("--------------------- " + count); } } }
9) 解数独-Leetcode37
public class SudokuLeetcode37 { record Pair (int i, int j) { } static void solveSudoku (char [][] table) { int n = 9 ; boolean [][] va = new boolean [n][n]; boolean [][] vb = new boolean [n][n]; boolean [][][] vc = new boolean [3 ][3 ][n]; List<Pair> blanks = new ArrayList <>(); for (int i = 0 ; i < n; i++) { for (int j = 0 ; j < n; j++) { if (table[i][j] != '.' ) { int x = table[i][j] - '0' - 1 ; va[i][x] = true ; vb[j][x] = true ; vc[i / 3 ][j / 3 ][x] = true ; } else { blanks.add(new Pair (i, j)); } } } dfs(0 , blanks, table, va, vb, vc); } static boolean dfs (int p, List<Pair> blanks, char [][] table, boolean [][] va, boolean [][] vb, boolean [][][] vc) { if (p == blanks.size()) { print(table); return true ; } int n = table.length; for (int d = 0 ; d < n; d++) { Pair pair = blanks.get(p); if (va[pair.i][d] || vb[pair.j][d] || vc[pair.i / 3 ][pair.j / 3 ][d]) { continue ; } char ch = (char ) (d + '0' + 1 ); table[pair.i][pair.j] = ch; va[pair.i][d] = true ; vb[pair.j][d] = true ; vc[pair.i / 3 ][pair.j / 3 ][d] = true ; boolean dfs = dfs(p + 1 , blanks, table, va, vb, vc); if (dfs) { return true ; } table[pair.i][pair.j] = '.' ; va[pair.i][d] = false ; vb[pair.j][d] = false ; vc[pair.i / 3 ][pair.j / 3 ][d] = false ; } return false ; } public static void main (String[] args) { char [][] table = { {'5' , '3' , '.' , '.' , '7' , '.' , '.' , '.' , '.' }, {'6' , '.' , '.' , '1' , '9' , '5' , '.' , '.' , '.' }, {'.' , '9' , '8' , '.' , '.' , '.' , '.' , '6' , '.' }, {'8' , '.' , '.' , '.' , '6' , '.' , '.' , '.' , '3' }, {'4' , '.' , '.' , '8' , '.' , '3' , '.' , '.' , '1' }, {'7' , '.' , '.' , '.' , '2' , '.' , '.' , '.' , '6' }, {'.' , '6' , '.' , '.' , '.' , '.' , '2' , '8' , '.' }, {'.' , '.' , '.' , '4' , '1' , '9' , '.' , '.' , '5' }, {'.' , '.' , '.' , '.' , '8' , '.' , '.' , '7' , '9' } }; solveSudoku(table); print(table); } static char [][] solved = { {'5' , '3' , '4' , '6' , '7' , '8' , '9' , '1' , '2' }, {'6' , '7' , '2' , '1' , '9' , '5' , '3' , '4' , '8' }, {'1' , '9' , '8' , '3' , '4' , '2' , '5' , '6' , '7' }, {'8' , '5' , '9' , '7' , '6' , '1' , '4' , '2' , '3' }, {'4' , '2' , '6' , '8' , '5' , '3' , '7' , '9' , '1' }, {'7' , '1' , '3' , '9' , '2' , '4' , '8' , '5' , '6' }, {'9' , '6' , '1' , '5' , '3' , '7' , '2' , '8' , '4' }, {'2' , '8' , '7' , '4' , '1' , '9' , '6' , '3' , '5' }, {'3' , '4' , '5' , '2' , '8' , '6' , '1' , '7' , '9' } }; static void print (char [][] table) { for (char [] chars : table) { System.out.println(new String (chars)); } System.out.println(Arrays.deepEquals(table, solved)); } }
public class SudokuLeetcode37 { static void solveSudoku (char [][] table) { int n = 9 ; boolean [][] va = new boolean [n][n]; boolean [][] vb = new boolean [n][n]; boolean [][][] vc = new boolean [3 ][3 ][n]; for (int i = 0 ; i < n; i++) { for (int j = 0 ; j < n; j++) { if (table[i][j] != '.' ) { int x = table[i][j] - '0' - 1 ; va[i][x] = true ; vb[j][x] = true ; vc[i / 3 ][j / 3 ][x] = true ; } } } dfs(table, va, vb, vc, 0 , 0 ); } static boolean dfs (char [][] table, boolean [][] va, boolean [][] vb, boolean [][][] vc, int i, int j) { while (table[i][j] != '.' ) { if (++j >= 9 ) { j = 0 ; i++; } if (i >= 9 ) { return true ; } } int n = table.length; for (int d = 0 ; d < n; d++) { if (va[i][d] || vb[j][d] || vc[i / 3 ][j / 3 ][d]) { continue ; } char ch = (char ) (d + '0' + 1 ); table[i][j] = ch; va[i][d] = true ; vb[j][d] = true ; vc[i / 3 ][j / 3 ][d] = true ; boolean dfs = dfs(table, va, vb, vc, i, j); if (dfs) { return true ; } table[i][j] = '.' ; va[i][d] = false ; vb[j][d] = false ; vc[i / 3 ][j / 3 ][d] = false ; } return false ; } public static void main (String[] args) { char [][] table = { {'5' , '3' , '.' , '.' , '7' , '.' , '.' , '.' , '.' }, {'6' , '.' , '.' , '1' , '9' , '5' , '.' , '.' , '.' }, {'.' , '9' , '8' , '.' , '.' , '.' , '.' , '6' , '.' }, {'8' , '.' , '.' , '.' , '6' , '.' , '.' , '.' , '3' }, {'4' , '.' , '.' , '8' , '.' , '3' , '.' , '.' , '1' }, {'7' , '.' , '.' , '.' , '2' , '.' , '.' , '.' , '6' }, {'.' , '6' , '.' , '.' , '.' , '.' , '2' , '8' , '.' }, {'.' , '.' , '.' , '4' , '1' , '9' , '.' , '.' , '5' }, {'.' , '.' , '.' , '.' , '8' , '.' , '.' , '7' , '9' } }; solveSudoku(table); print(table); } static char [][] solved = { {'5' , '3' , '4' , '6' , '7' , '8' , '9' , '1' , '2' }, {'6' , '7' , '2' , '1' , '9' , '5' , '3' , '4' , '8' }, {'1' , '9' , '8' , '3' , '4' , '2' , '5' , '6' , '7' }, {'8' , '5' , '9' , '7' , '6' , '1' , '4' , '2' , '3' }, {'4' , '2' , '6' , '8' , '5' , '3' , '7' , '9' , '1' }, {'7' , '1' , '3' , '9' , '2' , '4' , '8' , '5' , '6' }, {'9' , '6' , '1' , '5' , '3' , '7' , '2' , '8' , '4' }, {'2' , '8' , '7' , '4' , '1' , '9' , '6' , '3' , '5' }, {'3' , '4' , '5' , '2' , '8' , '6' , '1' , '7' , '9' } }; static void print (char [][] table) { for (char [] chars : table) { System.out.println(new String (chars)); } System.out.println(Arrays.deepEquals(table, solved)); } }
其它题目
题号
标题
说明
Leetcode 1219
黄金矿工
无
马踏棋盘(The Knight’s tour problem)
无
Rat in a Maze
与 Leetcode 62 不同路径区别在于,该题问的是有多少种走法,而本题只是找到其中一种走法实现
4.6 Leetcode 双指针
下面是的题目都会涉及双指针,除此外,还有
Leetcode3 最长不重复子串,在 hash 表部分讲过了
快排中
二分中
…
移动零-Leetcode 283
public class MoveZeroesLeetcode283 { static void moveZeroes (int [] nums) { int i = 0 ; int j = 0 ; while (j < nums.length) { if (nums[j] != 0 ) { int t = nums[i]; nums[i] = nums[j]; nums[j] = t; i++; } j++; } } public static void main (String[] args) { int [] nums = {0 , 1 , 0 , 3 , 12 }; moveZeroes(nums); System.out.println(Arrays.toString(nums)); } }
两数之和 II-Leetcode 167
public class SumLeetcode167 { public static void main (String[] args) { System.out.println(Arrays.toString(twoSum(new int []{2 , 7 , 11 , 15 }, 9 ))); } static public int [] twoSum(int [] numbers, int target) { return twoSum(numbers, 0 , numbers.length - 1 , target); } static int [] twoSum(int [] nums, int left, int right, int target) { int i = left; int j = right; while (i < j) { if (nums[i] + nums[j] < target) { i++; } else if (nums[i] + nums[j] > target) { j--; } else { break ; } } return new int []{i + 1 , j + 1 }; } }
与 Leetcode 1 的两数之和区别在于,本题的数组是升序排好的
三数之和-Leetcode 15
public class SumLeetcode15 { static List<List<Integer>> threeSum (int [] nums) { Arrays.sort(nums); List<List<Integer>> result = new LinkedList <>(); dfs(3 , 0 , nums.length - 1 , 0 , nums, new LinkedList <>(), result); return result; } static void dfs (int n, int i, int j, int target, int [] nums, LinkedList<Integer> stack, List<List<Integer>> result) { if (n == 2 ) { twoSum(i, j, nums, target, stack, result); return ; } for (int k = i; k < j - (n - 2 ); k++) { if (k > i && nums[k] == nums[k - 1 ]) { continue ; } stack.push(nums[k]); dfs(n - 1 , k + 1 , j, target - nums[k], nums, stack, result); stack.pop(); } } static int count; static public void twoSum (int i, int j, int [] numbers, int target, LinkedList<Integer> stack, List<List<Integer>> result) { count++; while (i < j) { int sum = numbers[i] + numbers[j]; if (sum < target) { i++; } else if (sum > target) { j--; } else { ArrayList<Integer> list = new ArrayList <>(stack); list.add(numbers[i]); list.add(numbers[j]); result.add(list); i++; j--; while (i < j && numbers[i] == numbers[i - 1 ]) { i++; } while (i < j && numbers[j] == numbers[j + 1 ]) { j--; } } } } public static void main (String[] args) { long start = System.currentTimeMillis(); int [] candidates = {-4 , -1 , -1 , 0 , 0 , 1 , 1 , 2 }; System.out.println("数据量:" + candidates.length); System.out.println(threeSum(candidates)); System.out.println("耗费时间:" + (System.currentTimeMillis() - start)); System.out.println("递归次数:" + count); } }
本题与之前的两数之和(Leetcode 1 和 Leetcode 167)相比,区别在于
两数之和里明确说了,只有一个答案,而本题要找出所有答案
本题要考虑去重
本题类似于 组合总和 II(Leetcode 40) 区别在于
40 题要求列出任意数之和等于 target 的所有组合,而本题要求三数之和等于 target 的所有组合
40 题使用回溯的办法时间复杂度是 O ( 2 n ∗ n ) O(2^n * n) O ( 2 n ∗ n ) ,而本题的三数限制了递归次数仅有一次,并且每次递归终点是求两数之和时间复杂度为 O ( n ) O(n) O ( n ) ,因此总时间复杂度为 O ( n 2 ) O(n^2) O ( n 2 )
小优化:固定数字时,应该预留三个数字做三数之和,预览两个数字做两数之和,因此有 k < j - (n - 2)
四数之和-Leetcode 18
public class SumLeetcode18 { static List<List<Integer>> fourSum (int [] nums, int target) { Arrays.sort(nums); List<List<Integer>> result = new LinkedList <>(); dfs(4 , 0 , nums.length - 1 , target, nums, new LinkedList <>(), result); return result; } static void dfs (int n, int i, int j, int target, int [] nums, LinkedList<Integer> stack, List<List<Integer>> result) { if (n == 2 ) { twoSum(i, j, nums, target, stack, result); return ; } for (int k = i; k < j - (n - 2 ); k++) { if (k > i && nums[k] == nums[k - 1 ]) { continue ; } stack.push(nums[k]); dfs(n - 1 , k + 1 , j, target - nums[k], nums, stack, result); stack.pop(); } } static int count; static public void twoSum (int i, int j, int [] numbers, int target, LinkedList<Integer> stack, List<List<Integer>> result) { count++; while (i < j) { int sum = numbers[i] + numbers[j]; if (sum < target) { i++; } else if (sum > target) { j--; } else { ArrayList<Integer> list = new ArrayList <>(stack); list.add(numbers[i]); list.add(numbers[j]); result.add(list); i++; j--; while (i < j && numbers[i] == numbers[i - 1 ]) { i++; } while (i < j && numbers[j] == numbers[j + 1 ]) { j--; } } } } public static void main (String[] args) { System.out.println(fourSum(new int []{1 , 0 , -1 , 0 , -2 , 2 }, 0 )); } }
盛最多水的容器-Leetcode 11
public class MostWaterLeetcode11 { static int maxArea (int [] height) { int i = 0 ; int j = height.length - 1 ; int max = 0 ; while (i < j) { int min = Integer.min(height[i], height[j]); max = Integer.max(max, (j - i) * min); while (i < j && height[i] <= min) { i++; } while (i < j && height[j] <= min) { j--; } } return max; } public static void main (String[] args) { System.out.println(maxArea(new int []{1 , 8 , 6 , 2 , 5 , 4 , 8 , 3 , 7 })); System.out.println(maxArea(new int []{2 ,1 })); } }
反转字符数组-Leetcode 344
双指针
public class ReverseStringLeetcode344 { public static void main (String[] args) { char [] array = "abcde" .toCharArray(); reverseString(array); System.out.println(Arrays.toString(array)); } static void reverseString (char [] s) { recursion(s, 0 , s.length - 1 ); } public static void recursion (char [] array, int i, int j) { if (i >= j) { return ; } swap(array, i, j); recursion(array, ++i, --j); } public static void swap (char [] array, int i, int j) { char c = array[i]; array[i] = array[j]; array[j] = c; } }
第一次交换的是 array[0] 和 array[4]
第二次交换的是 array[1] 和 array[3]
第三次 i = j = 2,开始返回
如果 array.length 是偶数,则会在 i > j 时返回
4.7 Leetcode 单调队列和栈
单调递减队列
public class MonotonicStack <T extends Comparable <T>> { private final LinkedList<T> stack = new LinkedList <>(); public void push (T t) { while (!stack.isEmpty() && stack.peek().compareTo(t) < 0 ) { stack.pop(); } stack.push(t); } public void pop () { stack.pop(); } public T peek () { return stack.peek(); } @Override public String toString () { return stack.toString(); } public static void main (String[] args) { MonotonicStack<Integer> stack = new MonotonicStack <>(); for (int i : new int []{0 , 1 , 0 , 2 , 1 , 0 , 1 , 3 , 2 , 1 , 2 , 1 }) { stack.push(i); System.out.println(stack); } } }
最大滑动窗口-Leetcode 239
public class SlidingWindowMaximumLeetcode239 { static int [] maxSlidingWindow(int [] nums, int k) { MonotonicQueue<Integer> q = new MonotonicQueue <>(); int [] output = new int [nums.length - (k - 1 )]; for (int i = 0 ; i < nums.length; i++) { if (i >= k && nums[i - k] == q.peek()) { q.poll(); } int num = nums[i]; q.offer(num); if (i >= k - 1 ) { output[i - (k - 1 )] = q.peek(); } } return output; } public static void main (String[] args) { System.out.println(Arrays.toString(maxSlidingWindow(new int []{1 , 3 , -1 , -3 , 5 , 3 , 6 , 7 }, 3 ))); System.out.println(Arrays.toString(maxSlidingWindow(new int []{7 , 2 , 4 }, 2 ))); System.out.println(Arrays.toString(maxSlidingWindow(new int []{1 , 3 , 1 , 2 , 0 , 5 }, 3 ))); System.out.println(Arrays.toString(maxSlidingWindow(new int []{-7 , -8 , 7 , 5 , 7 , 1 , 6 , 0 }, 4 ))); } }
如果每移动一次窗口,就在 k 个数里找最大值,时间复杂度约为 O ( n ∗ k ) O(n*k) O ( n ∗ k )
利用了单调队列后,每个元素都最多入队 、出队 一次,找最大值就在队头找,时间复杂度为 O ( n ) O(n) O ( n )
单调递减栈
public class MonotonicStack { static class ValueAndIndex { int value; int i; public ValueAndIndex (int value, int i) { this .value = value; this .i = i; } @Override public String toString () { return "%d" .formatted(value); } } private final LinkedList<ValueAndIndex> stack = new LinkedList <>(); public void push (int value, int i, TriConsumer onPop) { while (!stack.isEmpty() && stack.peek().value < value) { ValueAndIndex pop = stack.pop(); ValueAndIndex peek = stack.peek(); if (peek != null ) { onPop.accept(pop.value, peek.value, peek.i); } } stack.push(new ValueAndIndex (value, i)); } @Override public String toString () { return stack.toString(); } }
接雨水-Leetcode 42
public class TrappingRainWaterLeetcode42 { public static void main (String[] args) { System.out.println(trap(new int []{0 , 1 , 0 , 2 , 1 , 0 , 1 , 3 , 2 , 1 , 2 , 1 })); System.out.println(trap(new int []{4 , 2 , 0 , 3 , 2 , 5 })); } record Data (int height, int i) { } static int trap (int [] heights) { int sum = 0 ; LinkedList<Data> stack = new LinkedList <>(); for (int i = 0 ; i < heights.length; i++) { Data right = new Data (heights[i], i); while (!stack.isEmpty() && stack.peek().height < heights[i]) { Data pop = stack.pop(); Data left = stack.peek(); if (left != null ) { sum += (Integer.min(left.height, right.height) - pop.height) * (right.i - left.i - 1 ); } } stack.push(right); } return sum; } }
维护一个单调栈
当加入新柱子(right)时,如果发现要弹出之前的柱子,表示遇到了凹陷的地方
此时栈里没有更左边的柱子,表示拦不住雨水
栈里有左边柱子(left)就可以计算雨水容量:( r i g h t . i − l e f t . i − 1 ) ∗ M i n ( r i g h t . h e i g h t , l e f t . h e i g h t ) − p o p . h e i g h t (right.i - left.i-1)*Min(right.height,left.height)-pop.height ( r i g h t . i − l e f t . i − 1 ) ∗ M i n ( r i g h t . h e i g h t , l e f t . h e i g h t ) − p o p . h e i g h t
4.8 Leetcode 字符串
indexOf-Leetcode 28
native string matching
public class StrStrLeetcode28 { static int strStr (String haystack, String needle) { char [] text = haystack.toCharArray(); char [] pattern = needle.toCharArray(); int n = text.length; int m = pattern.length; for (int i = 0 ; i <= n - m; i++) { int j; for (j = 0 ; j < m; j++) { if (pattern[j] != text[i + j]) { break ; } } if (j == m) { return i; } } return -1 ; } public static void main (String[] args) { System.out.println(strStr("aaacaaab" , "aaab" )); } }
kmp string matching
public class StrStrLeetcode28KMP { static int strStr (String haystack, String needle) { char [] text = haystack.toCharArray(); char [] pattern = needle.toCharArray(); int n = text.length; int m = pattern.length; int [] lps = lps(pattern); int i = 0 ; int j = 0 ; while ((n - i) >= (m - j)) { if (text[i] == pattern[j]) { i++; j++; } else if (j != 0 ) { j = lsp[j - 1 ]; } else { i++; } if (j == m) { return i - j; } } return -1 ; } static int [] lps(char [] pattern) { int [] lps = new int [pattern.length]; int i = 1 ; int j = 0 ; while (i < pattern.length) { if (pattern[i] == pattern[j]) { j++; lps[i] = j; i++; } else if (j != 0 ) { j = lps[j - 1 ]; } else { i++; } } return lps; } public static void main (String[] args) { System.out.println(strStr("aaaaaaab" , "aaab" )); System.out.println(Arrays.toString(lsp("ababaca" .toCharArray()))); } }
很多文章里,把 lps 数组的向后平移一位,lps 用 -1 填充,这个平移后的数组称为 next
这样可以用 -1 代替 j == 0 的判断
并可以在 j > 0 向前移动时,做少量优化(不用 next 数组也能做同样优化)
其它字符串匹配算法有:BM 算法、sunday 算法、Horspool 算法等
最长公共前缀-Leetcode 14
public class LCPLeetcode14 { static String longestCommonPrefix (String[] strings) { char [] first = strings[0 ].toCharArray(); for (int i = 0 ; i < first.length; i++) { char ch = first[i]; for (int j = 1 ; j < strings.length; j++) { if (i == strings[j].length() || ch != strings[j].charAt(i)) { return new String (first, 0 , i); } } } return strings[0 ]; } public static void main (String[] args) { System.out.println(longestCommonPrefix(new String []{"flower" , "flow" , "flight" })); System.out.println(longestCommonPrefix(new String []{"dog" ,"racecar" ,"car" })); System.out.println(longestCommonPrefix(new String []{"ab" ,"a" })); System.out.println(longestCommonPrefix(new String []{"dog" ,"dogaa" ,"dogbb" })); } }
最长回文子串-Leetcode 5
public class LongestPalindromeLeetcode5 { public static void main (String[] args) { System.out.println(longestPalindrome("babad" )); System.out.println(longestPalindrome("cbbd" )); System.out.println(longestPalindrome("a" )); } record Result (int i, int length) { static Result max (Result r1, Result r2, Result r3) { Result m = r1; if (r2.length > m.length) { m = r2; } if (r3.length > m.length) { m = r3; } return m; } } static String longestPalindrome (String s) { char [] chars = s.toCharArray(); Result max = new Result (0 , 1 ); for (int i = 0 ; i < chars.length - 1 ; i++) { Result r1 = extend(chars, i, i); Result r2 = extend(chars, i, i + 1 ); max = Result.max(max, r1, r2); } return new String (chars, max.i, max.length); } private static Result extend (char [] chars, int i, int j) { int len = chars.length; while (i >= 0 && j < len && chars[i] == chars[j]) { i--; j++; } i++; return new Result (i, j - i); } }
最小覆盖子串-Leetcode 76
public class MinWindowLeetcode76_2 { public static void main (String[] args) { System.out.println(minWindow("ADOBECODEBANC" , "ABC" )); System.out.println(minWindow("aaabbbbbcdd" , "abcdd" )); } record Answer (int count, int i, int j) { } static String minWindow (String s, String t) { char [] source = s.toCharArray(); char [] target = t.toCharArray(); int [] targetCountMap = new int [128 ]; int [] windowCountMap = new int [128 ]; for (char ch : target) { targetCountMap[ch]++; } int i = 0 ; int j = 0 ; Answer answer = new Answer (Integer.MAX_VALUE, i, j); int passCount = 0 ; for (int count : targetCountMap) { if (count > 0 ) { passCount++; } } int pass = 0 ; while (j < source.length) { char right = source[j]; int c = ++windowCountMap[right]; if (c == targetCountMap[right]) { pass++; } while (pass == passCount && i <= j) { if (j - i < answer.count) { answer = new Answer (j - i, i, j); } char left = source[i]; windowCountMap[left]--; if (windowCountMap[left] < targetCountMap[left]) { pass--; } i++; } j++; } return answer.count != Integer.MAX_VALUE ? s.substring(answer.i, answer.j + 1 ) : "" ; } }
4.9 Leetcode 设计
LRU 缓存-Leetcode 146
public class LRUCacheLeetcode146 { static class LRUCache { static class Node { Node prev; Node next; int key; int value; public Node (int key, int value) { this .key = key; this .value = value; } } static class DoublyLinkedList { private final Node head; private final Node tail; DoublyLinkedList() { head = tail = new Node (-1 , -1 ); head.next = tail; tail.prev = head; } void addFirst (Node newFirst) { Node oldFirst = head.next; newFirst.prev = head; newFirst.next = oldFirst; head.next = newFirst; oldFirst.prev = newFirst; } void remove (Node node) { Node prev = node.prev; Node next = node.next; prev.next = next; next.prev = prev; } Node removeLast () { Node last = tail.prev; remove(last); return last; } } private final HashMap<Integer, Node> map = new HashMap <>(); private final DoublyLinkedList linkedList = new DoublyLinkedList (); private final int capacity; public LRUCache (int capacity) { this .capacity = capacity; } public int get (int key) { Node node = map.get(key); if (node == null ) { return -1 ; } linkedList.remove(node); linkedList.addFirst(node); return node.value; } public void put (int key, int value) { if (map.containsKey(key)) { Node node = map.get(key); node.value = value; linkedList.remove(node); linkedList.addFirst(node); } else { Node node = new Node (key, value); map.put(key, node); linkedList.addFirst(node); if (map.size() > capacity) { Node last = linkedList.removeLast(); map.remove(last.key); } } } } public static void main (String[] args) { LRUCache cache = new LRUCache (2 ); cache.put(1 , 1 ); cache.put(2 , 2 ); System.out.println(cache.get(1 )); cache.put(3 , 3 ); System.out.println(cache.get(2 )); cache.put(4 , 4 ); System.out.println(cache.get(1 )); System.out.println(cache.get(3 )); } }
注意:
这里很重要的一点是,map 中存储 node,可以省去在双向链表中查找 node 的时间,这样让使用最近访问的节点移动到链表头时达到 O ( 1 ) O(1) O ( 1 ) 的需求
同时我们应当意识到,node 的引用不能修改了(不方便修改,真要改得同时改链表)
例如,不能在更新时用新的 node 对象替换,而应该在原有的 node 上修改 value
LFU 缓存-Leetcode 460
public class LFUCacheLeetcode460 { static class LFUCache { static class Node { Node prev; Node next; int key; int value; int freq; public Node () { } public Node (int key, int value, int freq) { this .key = key; this .value = value; this .freq = freq; } } static class DoublyLinkedList { private final Node head; private final Node tail; int size = 0 ; public DoublyLinkedList () { head = tail = new Node (); head.next = tail; tail.prev = head; } void remove (Node node) { Node prev = node.prev; Node next = node.next; prev.next = next; next.prev = prev; node.prev = node.next = null ; size--; } void addFirst (Node newFirst) { Node oldFirst = head.next; newFirst.prev = head; newFirst.next = oldFirst; head.next = newFirst; oldFirst.prev = newFirst; size++; } Node removeLast () { Node last = tail.prev; remove(last); return last; } boolean isEmpty () { return size == 0 ; } } private final HashMap<Integer, DoublyLinkedList> freqMap = new HashMap <>(); private final HashMap<Integer, Node> kvMap = new HashMap <>(); private final int capacity; private int minFreq; public LFUCache (int capacity) { this .capacity = capacity; } public int get (int key) { Node node = kvMap.get(key); if (node == null ) { return -1 ; } DoublyLinkedList list = freqMap.get(node.freq); list.remove(node); if (node.freq == minFreq && list.isEmpty()) { minFreq++; } node.freq++; freqMap.computeIfAbsent(node.freq, k -> new DoublyLinkedList ()).addFirst(node); return node.value; } public void put (int key, int value) { if (kvMap.containsKey(key)) { Node node = kvMap.get(key); DoublyLinkedList list = freqMap.get(node.freq); list.remove(node); if (node.freq == minFreq && list.isEmpty()) { minFreq++; } node.freq++; node.value = value; freqMap.computeIfAbsent(node.freq, k -> new DoublyLinkedList ()).addFirst(node); } else { if (kvMap.size() == capacity) { Node last = freqMap.get(minFreq).removeLast(); kvMap.remove(last.key); } Node node = new Node (key, value, 1 ); kvMap.put(key, node); minFreq = 1 ; freqMap.computeIfAbsent(node.freq, k -> new DoublyLinkedList ()).addFirst(node); } } } public static void main (String[] args) { LFUCache cache = new LFUCache (2 ); cache.put(1 , 1 ); cache.put(2 , 2 ); System.out.println(cache.get(1 )); cache.put(3 , 3 ); System.out.println(cache.get(2 )); System.out.println(cache.get(3 )); cache.put(4 , 4 ); System.out.println(cache.get(1 )); System.out.println(cache.get(3 )); System.out.println(cache.get(4 )); } }
随机数
线性同余发生器
公式 n e x t S e e d = ( s e e d ∗ a + c ) m o d m nextSeed = (seed * a + c) \mod m n e x t S e e d = ( s e e d ∗ a + c ) m o d m
public class MyRandom { private final int a; private final int c; private final int m; private int seed; public MyRandom (int a, int c, int m, int seed) { this .a = a; this .c = c; this .m = m; this .seed = seed; } public int nextInt () { return seed = (a * seed + c) % m; } public static void main (String[] args) { MyRandom r1 = new MyRandom (7 , 0 , 11 , 1 ); System.out.println(Arrays.toString(IntStream.generate(r1::nextInt).limit(30 ).toArray())); MyRandom r2 = new MyRandom (7 , 0 , Integer.MAX_VALUE, 1 ); System.out.println(Arrays.toString(IntStream.generate(r2::nextInt).limit(30 ).toArray())); } }
32 位随机数生成器
乘法会超过 int 范围导致随机性被破坏
java 版
public class MyRandom2 { private static final long a = 0x5DEECE66DL ; private static final long c = 0xBL ; private static final long m = 1L << 48 ; private long seed; public MyRandom2 (long seed) { this .seed = (seed ^ a) & (m - 1 ); } public int nextInt () { seed = (a * seed + c) & (m - 1 ); return ((int ) (seed >>> 16 )); } public static void main (String[] args) { Random r1 = new Random (1 ); MyRandom2 r2 = new MyRandom2 (1 ); System.out.println(Arrays.toString(IntStream.generate(r1::nextInt).limit(10 ).toArray())); System.out.println(Arrays.toString(IntStream.generate(r2::nextInt).limit(10 ).toArray())); } }
0x5DEECE66DL * 0x5DEECE66DL 不会超过 long 的范围
m 决定只取 48 位随机数
对于 nextInt,只取 48 位随机数的高 32 位
跳表-Leetcode 1206
randomLevel
设计一个方法调用若干次,每次返回 1~max 的数字,从 1 开始,返回数字的比例减半,例如 max = 4 ,让大概
50% 的几率返回 1
25% 的几率返回 2
12.5% 的几率返回 3
12.5% 的几率返回 4
static int randomLevel (int max) { int level = 1 ; while (Math.random() < 0.5 && level < max) { level++; } return level; }
跳表
public class SkipListLeetcode1206 { public static void main (String[] args) { HashMap<Integer, Integer> map = new HashMap <>(); for (int i = 0 ; i < 1000 ; i++) { int level = Skiplist.randomLevel(); map.compute(level, (k, v) -> v == null ? 1 : v + 1 ); } System.out.println(map.entrySet().stream().collect(Collectors.toMap(Map.Entry::getKey, e -> String.format("%d%%" , e.getValue() * 100 / 1000 )))); } static class Skiplist { static final int MAX = 4 ; static int randomLevel () { int level = 1 ; while (Math.random() < 0.5 && level < MAX) { level++; } return level; } private final Node head = new Node (-1 ); static class Node { int val; Node[] next; public Node (int val) { this .val = val; this .next = new Node [MAX]; } } private Node[] find(int val) { Node[] path = new Node [MAX]; Node curr = head; for (int lvl = MAX - 1 ; lvl >= 0 ; lvl--) { while (curr.next[lvl] != null && curr.next[lvl].val < val) { curr = curr.next[lvl]; } path[lvl] = curr; } return path; } public boolean search (int val) { Node[] path = find(val); Node node = path[0 ].next[0 ]; return node != null && node.val == val; } public void add (int val) { Node[] path = find(val); int lv = randomLevel(); Node node = new Node (val); for (int i = 0 ; i < lv; i++) { node.next[i] = path[i].next[i]; path[i].next[i] = node; } } public boolean erase (int val) { Node[] path = find(val); Node node = path[0 ].next[0 ]; if (node == null || node.val != val) { return false ; } for (int i = 0 ; i < MAX; i++) { if (path[i].next[i] != node) { break ; } path[i].next[i] = node.next[i]; } return true ; } } }
下楼梯规则
若 next 指针为 null,或者 next 指向的节点值 >= 目标,向下找
若 next 指针不为 null,且 next 指向的节点值 < 目标,向右找
节点的【高度】
高度并不需要额外属性来记录,而是由之前节点 next == 本节点的个数来决定,或是本节点 next 数组长度
本实现选择了第一种方式来决定高度,本节点的 next 数组长度统一为 MAX
最小栈-Leetcode 155
解法1
static class MinStack { LinkedList<Integer> stack = new LinkedList <>(); LinkedList<Integer> min = new LinkedList <>(); public MinStack () { min.push(Integer.MAX_VALUE); } public void push (int val) { stack.push(val); min.push(Math.min(val, min.peek())); } public void pop () { if (stack.isEmpty()) { return ; } stack.pop(); min.pop(); } public int top () { return stack.peek(); } public int getMin () { return min.peek(); } }
解法2
static class MinStack2 { record Data (int val, int min) { } final LinkedList<Data> stack = new LinkedList <>(); public void push (int val) { if (stack.isEmpty()) { stack.push(new Data (val, val)); } else { Data peek = stack.peek(); stack.push(new Data (val, Math.min(val, peek.min))); } } public void pop () { stack.pop(); } public int top () { return stack.peek().val; } public int getMin () { return stack.peek().min; } }
TinyURL 的加密与解密-Leetcode 535
public class TinyURLLeetcode535 { public static void main (String[] args) { System.out.println(CodecSequence.toBase62(237849728 )); } static class CodecSequence { private static final char [] toBase62 = { 'A' , 'B' , 'C' , 'D' , 'E' , 'F' , 'G' , 'H' , 'I' , 'J' , 'K' , 'L' , 'M' , 'N' , 'O' , 'P' , 'Q' , 'R' , 'S' , 'T' , 'U' , 'V' , 'W' , 'X' , 'Y' , 'Z' , 'a' , 'b' , 'c' , 'd' , 'e' , 'f' , 'g' , 'h' , 'i' , 'j' , 'k' , 'l' , 'm' , 'n' , 'o' , 'p' , 'q' , 'r' , 's' , 't' , 'u' , 'v' , 'w' , 'x' , 'y' , 'z' , '0' , '1' , '2' , '3' , '4' , '5' , '6' , '7' , '8' , '9' }; public static String toBase62 (int number) { if (number == 0 ) { return String.valueOf(toBase62[0 ]); } StringBuilder sb = new StringBuilder (); while (number > 0 ) { int r = number % 62 ; sb.append(toBase62[r]); number = number / 62 ; } return sb.toString(); } private final Map<String, String> longToShort = new HashMap <>(); private final Map<String, String> shortToLong = new HashMap <>(); private static final String SHORT_PREFIX = "http://tinyurl.com/" ; private static int id = 1 ; public String encode (String longUrl) { String shortUrl = longToShort.get(longUrl); if (shortUrl != null ) { return shortUrl; } shortUrl = SHORT_PREFIX + id; longToShort.put(longUrl, shortUrl); shortToLong.put(shortUrl, longUrl); id++; return shortUrl; } public String decode (String shortUrl) { return shortToLong.get(shortUrl); } } static class CodecHashCode { private final Map<String, String> longToShort = new HashMap <>(); private final Map<String, String> shortToLong = new HashMap <>(); private static final String SHORT_PREFIX = "http://tinyurl.com/" ; public String encode (String longUrl) { String shortUrl = longToShort.get(longUrl); if (shortUrl != null ) { return shortUrl; } int id = longUrl.hashCode(); while (true ) { shortUrl = SHORT_PREFIX + id; if (!shortToLong.containsKey(shortUrl)) { longToShort.put(longUrl, shortUrl); shortToLong.put(shortUrl, longUrl); break ; } id++; } return shortUrl; } public String decode (String shortUrl) { return shortToLong.get(shortUrl); } } static class CodecRandom { private final Map<String, String> longToShort = new HashMap <>(); private final Map<String, String> shortToLong = new HashMap <>(); private static final String SHORT_PREFIX = "http://tinyurl.com/" ; public String encode (String longUrl) { String shortUrl = longToShort.get(longUrl); if (shortUrl != null ) { return shortUrl; } while (true ) { int id = ThreadLocalRandom.current().nextInt(); shortUrl = SHORT_PREFIX + id; if (!shortToLong.containsKey(shortUrl)) { longToShort.put(longUrl, shortUrl); shortToLong.put(shortUrl, longUrl); break ; } } return shortUrl; } public String decode (String shortUrl) { return shortToLong.get(shortUrl); } } }
线性合并
static class Twitter2 { static int time; static class Tweet { int id; int time; Tweet next; public Tweet (int id, int time, Tweet next) { this .id = id; this .time = time; this .next = next; } public int id () { return id; } public int time () { return time; } } static class User { Integer id; public User (Integer id) { this .id = id; } Set<Integer> followees = new HashSet <>(); Tweet head = new Tweet (-1 , -1 , null ); } private final Map<Integer, User> userMap = new HashMap <Integer, User>(); public void postTweet (int userId, int tweetId) { User user = userMap.computeIfAbsent(userId, User::new ); user.head.next = new Tweet (tweetId, time++, user.head.next); } public List<Integer> getNewsFeed (int userId) { User user = userMap.get(userId); if (user == null ) { return List.of(); } Tweet p1 = user.head.next; for (Integer id : user.followees) { p1 = merge(p1, userMap.get(id).head.next); } LinkedList<Integer> result = new LinkedList <>(); int count = 0 ; while (p1 != null && count < 10 ) { result.addLast(p1.id); p1 = p1.next; count++; } return result; } private Tweet merge (Tweet p1, Tweet p2) { Tweet head = new Tweet (-1 , -1 , null ); Tweet p0 = head; int count = 0 ; while (p1 != null && p2 != null && count < 10 ) { if (p1.time > p2.time) { p0.next = new Tweet (p1.id, p1.time, null ); p0 = p0.next; p1 = p1.next; } else { p0.next = new Tweet (p2.id, p2.time, null ); p0 = p0.next; p2 = p2.next; } count++; } while (p1 != null && count < 10 ) { p0.next = new Tweet (p1.id, p1.time, null ); p0 = p0.next; p1 = p1.next; count++; } while (p2 != null && count < 10 ) { p0.next = new Tweet (p2.id, p2.time, null ); p0 = p0.next; p2 = p2.next; count++; } return head.next; } public void follow (int userId, int followeeId) { User user = userMap.computeIfAbsent(userId, User::new ); User followee = userMap.computeIfAbsent(followeeId, User::new ); user.followees.add(followeeId); } public void unfollow (int userId, int followeeId) { User user = userMap.get(userId); if (user != null ) { user.followees.remove(followeeId); } } }
优先级队列合并
public class TwitterLeetcode355 { static class Twitter { static class Tweet { int id; int time; Tweet next; public Tweet (int id, int time, Tweet next) { this .id = id; this .time = time; this .next = next; } public int getId () { return id; } public int getTime () { return time; } } static class User { int id; public User (int id) { this .id = id; } Set<Integer> followees = new HashSet <>(); Tweet head = new Tweet (-1 , -1 , null ); } private final Map<Integer, User> userMap = new HashMap <>(); private static int time; public void postTweet (int userId, int tweetId) { User user = userMap.computeIfAbsent(userId, User::new ); user.head.next = new Tweet (tweetId, time++, user.head.next); } public void follow (int userId, int followeeId) { User user = userMap.computeIfAbsent(userId, User::new ); User followee = userMap.computeIfAbsent(followeeId, User::new ); user.followees.add(followee.id); } public void unfollow (int userId, int followeeId) { User user = userMap.get(userId); if (user != null ) { user.followees.remove(followeeId); } } public List<Integer> getNewsFeed (int userId) { User user = userMap.get(userId); if (user == null ) { return List.of(); } PriorityQueue<Tweet> queue = new PriorityQueue <>(Comparator.comparingInt(Tweet::getTime).reversed()); if (user.head.next != null ) { queue.offer(user.head.next); } for (Integer id : user.followees) { User followee = userMap.get(id); if (followee.head.next != null ) { queue.offer(followee.head.next); } } List<Integer> res = new ArrayList <>(); int count = 0 ; while (!queue.isEmpty() && count < 10 ) { Tweet max = queue.poll(); res.add(max.id); if (max.next != null ) { queue.offer(max.next); } count++; } return res; } } }
4.10 股票问题
Leetcode 121
public class SharesI { static int maxProfit (int [] prices) { int i = 0 ; int j = 1 ; int max = 0 ; while (j < prices.length) { if (prices[j] - prices[i] > 0 ) { max = Math.max(max, prices[j] - prices[i]); j++; } else { i = j; j++; } } return max; } public static void main (String[] args) { System.out.println(maxProfit(new int []{9 , 3 , 12 , 1 , 2 , 3 , 11 })); } }
Leetcode 122
public class SharesIILeetcode122 { static int maxProfit (int [] prices) { int i = 0 ; int j = 1 ; int max = 0 ; while (j < prices.length) { if (prices[j] - prices[i] > 0 ) { max += prices[j] - prices[i]; } i = j; j++; } return max; } public static void main (String[] args) { System.out.println(maxProfit(new int []{9 , 3 , 12 , 1 , 2 , 3 })); System.out.println(maxProfit(new int []{7 , 1 , 5 , 3 , 6 , 4 })); } }
Leetcode 714
public class SharesLeetcode714 { static int maxProfit (int [] prices, int fee) { int b1 = -prices[0 ]; int s1 = 0 ; for (int i = 1 ; i < prices.length; i++) { int s0 = Math.max(s1, b1 + prices[i] - fee); int b0 = Math.max(b1, s1 - prices[i]); s1 = s0; b1 = b0; } return s1; } public static void main (String[] args) { System.out.println(maxProfit(new int []{1 , 3 , 2 , 8 , 4 , 9 }, 2 )); System.out.println(maxProfit(new int []{1 , 3 , 7 , 2 , 18 , 3 }, 3 )); System.out.println(maxProfit(new int []{1 , 3 , 7 , 5 , 10 , 3 }, 3 )); System.out.println(maxProfit(new int []{1 , 3 , 7 , 5 , 10 , 11 , 3 }, 3 )); System.out.println(maxProfit(new int []{2 ,1 ,4 ,4 ,2 ,3 ,2 ,5 ,1 ,2 }, 1 )); } }
降维
static int maxProfit (int [] prices, int fee) { int _buy = -prices[0 ]; int _sell = 0 ; for (int i = 1 ; i < prices.length; i++) { int buy = Math.max(_buy, _sell - prices[i]); int sell = Math.max(_sell, _buy + prices[i] - fee); _buy = buy; _sell = sell; } return _sell; }
结构优化(非性能)
static int maxProfit (int [] prices, int fee) { int buy = Integer.MIN_VALUE; int sell = 0 ; for (int price : prices) { buy = Math.max(buy, sell - price); sell = Math.max(sell, buy + price - fee); } return sell; }
在计算这次的 sell 时,用这次的 buy 代替上次 buy(证明见上方注释)
设置 buy 的初始值为最小,可以让循环统一从 0 开始
Leetcode 309
public class SharesLeetcode309 { static int maxProfit (int [] prices) { if (prices.length == 1 ) { return 0 ; } int [] buy = new int [prices.length]; int [] sell = new int [prices.length]; buy[0 ] = -prices[0 ]; sell[0 ] = 0 ; buy[1 ] = Math.max(-prices[0 ], -prices[1 ]); sell[1 ] = Math.max(sell[0 ], buy[0 ] + prices[1 ]); for (int i = 2 ; i < prices.length; i++) { buy[i] = Math.max(buy[i - 1 ], sell[i - 2 ] - prices[i]); sell[i] = Math.max(sell[i - 1 ], buy[i - 1 ] + prices[i]); } return sell[prices.length - 1 ]; } public static void main (String[] args) { System.out.println(maxProfit(new int []{1 , 2 , 3 , 0 , 2 })); } }
降维
static int maxProfit (int [] prices) { if (prices.length == 1 ) { return 0 ; } int __sell = 0 ; int _sell = 0 ; int _buy = -prices[0 ]; for (int i = 1 ; i < prices.length; i++) { int buy = Math.max(_buy, __sell - prices[i]); int sell = Math.max(_sell, prices[i] + _buy); _buy = buy; __sell = _sell; _sell = sell; } return _sell; }
Leetcode 123
public class SharesIIILeetcode123 { static int maxProfit (int [] prices) { int buy1 = Integer.MIN_VALUE; int sell1 = 0 ; int buy2 = Integer.MIN_VALUE; int sell2 = 0 ; for (int price : prices) { buy1 = Math.max(buy1, -price); sell1 = Math.max(sell1, buy1 + price); buy2 = Math.max(buy2, sell1 - price); sell2 = Math.max(sell2, buy2 + price); } return sell2; } public static void main (String[] args) { System.out.println(maxProfit(new int []{3 , 3 , 5 , 0 , 0 , 3 , 1 , 4 })); } }
Leetcode 188
public class SharesLeetcode188 { static int maxProfit (int [] prices) { int i = 0 ; int j = 1 ; int sum = 0 ; while (j < prices.length) { if (prices[j] - prices[i] > 0 ) { sum += prices[j] - prices[i]; } i++; j++; } return sum; } static int maxProfit (int k, int [] prices) { if (k > prices.length / 2 ) { return maxProfit(prices); } int [] buy = new int [k]; int [] sell = new int [k]; Arrays.fill(buy, Integer.MIN_VALUE); for (int price : prices) { buy[0 ] = Math.max(buy[0 ], -price); sell[0 ] = Math.max(sell[0 ], buy[0 ] + price); for (int j = 1 ; j < k; j++) { buy[j] = Math.max(buy[j], sell[j - 1 ] - price); sell[j] = Math.max(sell[j], buy[j] + price); } } return sell[k - 1 ]; } public static void main (String[] args) { System.out.println(maxProfit(2 , new int []{3 , 3 , 5 , 0 , 0 , 3 , 1 , 4 })); } }
附录
参考文章
注:以上内容来自网络资源,仅出于分享目的,若侵权请联系删除。