Java 树

什么是树?

结构严格派 唯一父节点和复数子节点 结构中立派 有前驱和后继关系就行 结构自由派 能存放内容就行
类型严格派 是一种数据结构 二叉树是树 链表也是树 栈也是树
类型中立派 和编程有关就行 包也是树 语句肯定是树 标识符都是树
类型自由派 和程序员有关就行 wifi 当然是树 衣服拉链也是树 馄饨也是树!

二叉树:

  • 二叉树:树有多种。每个节点最多只能有 2 个子节点的一种树的形式称为二叉树

    二叉树的子节点分为 左节点 和 右节点

  • 满二叉树:二叉树的 所有叶节点 都在 最后一层,且节点总数是 2n - 1

  • 完全二叉树:二叉树的 所有叶节点 都在 最后一层 和 倒数第二层,且最后一层的叶节点在左侧连续、倒数第二层的叶节点在右侧连续

二叉树的遍历:

  • 前序遍历:先输出父节点,再遍历左子树和右子树。

    自根节点起。先输出当前节点。再递归前序遍历左节点。那之后,递归前序遍历右节点。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    public static class Node {					// 节点类
    int val;
    Node left;
    Node right;
    }

    public static String traverse(Node root) {
    StringBuilder sb = new StringBuilder();
    traverse(root, sb);
    return sb.toString();
    }

    private static void traverse(Node root, StringBuilder sb) {
    if (root == null) return;
    sb.append(root.val).append(" "); // 先输出父节点
    traverse(root.left, sb); // 再遍历左子树
    traverse(root.right, sb); // 再遍历右子树
    }
  • 中序遍历:先遍历左子树,再输出父节点,再遍历右子树。

  • 后序遍历:先遍历左子树,再遍历右子树,再输出父节点。

顺序存储二叉树

从数据存储来看,数组与树可以相互转换。数组可以转换成树,树也能转换成数组。

顺序存储二叉树通常只考虑完全二叉树。将数组转换成树后,将可以进行前序、中序、后序遍历。

顺序存储二叉树的例子:

1
int[] array = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10};

该 array 的顺序存储二叉树为:

1
2
3
4
5
6
7
8
9
graph TD
A(0)---B(1)---C(3)
A---a(2)---aa(5)
B---D(4)
a---ab(6)
C---E(7)
C---F(8)
D---G(9)
D---H(10)

顺序存储二叉树的转换:

  • 数组下标为 0 的元素放在根节点。

  • 对于数组下标为 n 的元素,其左子节点的数组下标为 2 × n + 1、右子节点的数组下标为 2 × n + 2、父节点的数组下标为 (n - 1) / 2

    可以发现,所有左节点都是奇数下标,右节点都是偶数下标

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public static Node toTree(int[] array) {
Node root = new Node(array[0]);
List<Node> list = new ArrayList<Node>();
list.add(root); // 数组下标为 0 的元素放在根节点。
for (int i = 1; i < array.length; i++) { // 按照前述方法,创建每个元素节点,并放在对应父节点下
Node temp = new Node(array[i]);
list.add(temp);
Node parent = list.get((i - 1) / 2);
if (i % 2 == 0) parent.right = temp;
else parent.left = temp;
}
return root;
}

public static class Node {
public int val;
public Node left;
public Node right;

public Node(int val) {
this.val = val;
}
}

以上是一种显式的转换。也可以直接将数组视为抽象的顺序存储二叉树。

如:堆。——见 Java 数组、排序和查找

线索化二叉树

含有 n 各节点的二叉链表中,有 n + 1 个空指针域。利用这些空指针域,存放指向节点在某种遍历次序下的前驱和后继节点的指针,这种附加的指针称为 线索。加上了线索的二叉链表称为 线索链表,相应二叉树称为 线索二叉树

线索二叉树可分为:前序线索二叉树、中序线索二叉树、后续线索二叉树。

线索化二叉树后,那些左节点和右节点既可能指向 自身的子树,也可能指向自身的 前驱 / 后继 节点。因此,需要添加一组标记,以记录线索的种类。

这个遍历的场合,不能再使用递归方式遍历,而是改为线性方式遍历即可。

赫夫曼树

给定 n 个权值作为 n 个叶节点,构造一棵二叉树。若该树的带权路径长度(WPL)最小,则称其为 最优二叉树(赫夫曼树、哈夫曼树、霍夫曼树)

  • 节点的带权路径长度:该节点的权 × 节点路径长度

  • 树的带权路径长度:所有的叶结点的带权路径长度之和

    赫夫曼树中,一定是权值较大的节点距离根更近。

赫夫曼树的例子:

1
2
3
4
5
6
7
graph TD
A(NaN)---B(NaN)---C(14)
B---b(NaN)---c(5)
b---D(NaN)---E(1)
D---F(2)
A---a(NaN)---aa(16)
a---ab(20)

生成赫夫曼树:

  1. 对数据进行排序。每个数据都可以创建一个节点
  2. 取出权值最小的两颗二叉树,合并为一棵新的二叉树。该二叉树权值是两棵子树的权值之和
  3. 将数据再次排序,重复合并步骤,直至剩余唯一的树,即为赫夫曼树

赫夫曼编码:

赫夫曼编码是一种编码方式,是一种程序算法。赫夫曼编码是赫夫曼树在电讯通信中的经典应用之一。

赫夫曼编码广泛应用于数据文件压缩,其压缩率在 20% ~ 90% 间

赫夫曼编码是可变字长编码的一种。是老赫在 1952 年提出的编码方法,称为 “最佳编码”

赫夫曼编码是无损处理方案。由于赫夫曼编码是按字节处理数据,因此可以处理所有文件

编码方式有三种:

  • 定长编码:

    如 ASCII 码,其每个字符占用长度为固定 8 字节

  • 变长编码:

    对字符进行统计,按照各个字符出现的次数进行编码。出现次数越多,编码越小。

    字符的编码不能是其他字符编码的前缀,这样的编码叫做前缀编码(消除二义性)。

  • 赫夫曼编码:

    按照字符的出现次数,构建赫夫曼树。之后,按照赫夫曼树结构,给字符规定编码。向左的路径记为 0,向右记为 1。

    这样得到的编码,一定是前缀编码。因为那些字符节点都是叶节点。赫夫曼行啊赫夫曼!

    之后,用规定的编码将指定字符串转化为字节数组。最后,传递字符数组即可。

注意事项:

  • 压缩已经过压缩处理的文件,那个压缩率会变低
  • 如果一个文件中重复的数据很少,缩效果也会不明显

二叉排序树

二叉排序树(BST,Binary Sort Tree):对于任何一个非叶节点,其左节点小于等于当前节点,右节点大于等于当前节点

二叉排序树的例子:

1
2
3
4
5
6
7
graph TD
A(10)---B(8)
B---D(4)---c(2)---d(1)
D---b(6)
B---E(9)
A---C(15)---e(12)
C---ee(23)

二叉排序树删除节点:

  • 删除叶节点的场合,将那个父节点的对应连接置空即可。

  • 删除有唯一子节点的节点场合,让那个父节点的对应连接改为指向子树即可。

  • 删除有两个子节点的节点的场合,将该节点置为正无穷或负无穷。

    之后维护该二叉排序树,直到该节点成为叶节点时,删除该节点即可。

平衡二叉树

二叉排序树可能形成一些奇怪的形状(如左子树全部为空),这样就不能发挥树形结构的比较优势。

平衡二叉树(AVL 树):也叫平衡二叉搜索树。非空时,其任意节点左右两个子树的高度差不超过 1,且左右子树也都是平衡二叉树。

平衡二叉树的实现方法有:红黑树、AVL、替罪羊树、Treap、伸展树等

  • 创建一个新节点。该节点的值等于根节点值
  • 使该新节点的左子树指向当前根节点的左子树。使该节点的右子树指向当前根节点右子树的左子树
  • 使当前根节点的右子树的左子树指向该新节点
  • 使当前根节点的右子树成为新的根节点。旧的根节点被废弃

简单的说,就是让根节点的右子树指向右子树的左子树。而右子树的左子树指向根节点。

合理性在于,根节点(root)的右子树(right)上的所有值都大于 root;而 right 的所有左子树的值,以及 root 所有左子树的值也一定小于 right 值

符合进行右旋转的条件(右子树高度 > 左子树高度 + 1)时,如果那个左子树的右子树高度高于其左子树高度,需要先对左子树进行左旋转。以此类推。

线段树

线段树(Segment Tree)是一棵二叉树。其每个节点表示一个闭区间,父节点的区间内包含所有子节点的区间。

  • 对于每个非叶节点,将其区间平均划分成两个子区间。左节点指向其中较小区间,右节点指向那个较大区间

    换言之,对于非叶节点 [L, R],其左子节点是 [L, (L + R) / 2],右子节点是 [((L + R) / 2) + 1, R]

  • 对于每个叶节点,其区间仅包含一个元素。即,其区间的左界等于右界。

线段树的例子:

在区间 [1, 9] 中,记录 [2, 9] 的样子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
graph TB
Root(&#91&#49,9&#93)

Root---L(&#91&#49,5&#93)
style R fill: #BFFFFC
Root---R(&#91&#54,9&#93)

L---LL(&#91&#49,3&#93)
style LR fill: #BFFFFC
L---LR(&#91&#52,5&#93)
R---RL(&#91&#54,7&#93)
R---RR(&#91&#56,9&#93)

LL---LLL(&#91&#49,2&#93)
style LLR fill: #BFFFFC
LL---LLR(&#91&#51,3&#93)
LR---LRL(&#91&#52,4&#93)
LR---LRR(&#91&#53,5&#93)
RL---RLL(&#91&#54,6&#93)
RL---RLR(&#91&#55,7&#93)
RR---RRL(&#91&#56,8&#93)
RR---RRR(&#91&#57,9&#93)

LLL---LLLL(&#91&#49,1&#93)
style LLLR fill: #BFFFFC
LLL---LLLR(&#91&#50,2&#93)

线段树是近似的完全二叉树。有时,线段树的节点是随着线段树的更新逐渐建立的,此时线段树不处于完全二叉树的状态。

线段树的更新:

标记区间时,按照 广度优先搜索 的思想,从根节点开始遍历区间。

比如,添加区间 [START, END] 时:

  • 如果一个节点的区间内所有元素都被标记,则标记这个节点

    对于区间 [L, R],如果 L >= STRAT 且 R <= END,则标记该节点

  • 如果一个节点的区间内部分元素被标记,则继续遍历其左右节点

    对于区间 [L, R],MID = (L + R) / 2

    如果 MID >= L,则需要遍历其左节点。如果 MID < R,则需要遍历其右节点

标记节点时,只需在该节点添加懒标记,而不必对所有子节点进行标记。

懒标记:

使用懒标记,可以只更新到满足条件的区间,而不必对所有子区间一一更新。此后再次遍历到该节点时,再对懒标记进行下推

上述例子中,记录区间 [2, 7] 时,仅更新了 [2, 2]、[3, 3]、[4, 5]、[6, 9] 这些节点。

以节点 [6, 9] 为例,该区间上被添加了懒标记,代表该区间及所有子区间都被记录了一次。下次遍历到这个节点时,懒标记被下推给子节点 [6, 7]、[8, 9]

线段树的查询:

一个区间的元素和,等于 其子区间各自元素和 的合计值

一个区间中的最大值,等于 其子区间各自最大值 中的较大值

多路查找树

二叉树虽然效率较高,但需要加载到内存中。节点过多时就可能出现问题。

如:需要进行多次 I / O 操作,导致构建速度慢;造成二叉树高度很大,降低操作速度。

每个节点可以拥有更多数据项和更多子节点的树,就是多叉树(multiway tree)。

多叉树通过重新组织节点,能减少树的高度,能对二叉树进行优化。

  • 节点的度:节点的子节点数量
  • 树的度 / 阶:树中所有节点的度的最大值

2-3 树

2-3 树是最简单的 B 树结构。其具有如下特点:

  • 所有叶节点都在同一层。节点包含不超过 2 个值。

  • 有两个子节点的节点叫 二节点。二节点要么没有子节点,要么有两个子节点。

    有三个子节点的节点叫 三节点。三节点要么没有子节点,要么有三个子节点。

  • 2-3 树是由 二节点 和 三节点 构成的树。其节点仍遵循二叉排序树的规则。

    对于二节点:其左子树的值需小于当前节点、右子树的值需大于当前节点

    对于三节点:其左子树的值小于当前节点的最小值,中子树的值需介于当前节点的两个值之间,右子树的值大于当前节点的最大值

2-3 树的例子:

1
2
3
4
5
6
7
8
graph TD
A(10)---B(6)
B---D(1, 3)
B---E(7)
A---C(15, 20)
C---F(11, 12)
C---G(19)
C---H(22, 32)
  • 插入节点时,如果不能满足条件,即需要拆分。

    拆分时先拆上层。上层满时,才拆本层。拆分后仍要满足规则

B 树

B 树(b-tree,balance tree)。2-3 树与 2-3-4 树都是 B 树的种类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
graph TD
ROOT(30, 60<br/>P1 - P2 - P3)
ROOT---R1(10, 20<br/>P1 - P2 - P3)
ROOT---R2(40, 50<br/>. - P2 - P3)
ROOT---R3(70, 80<br/>P1 - P2 - P3)
R1---R11(3, 6)
R1---R12(12, 13)
R1---R13(23, 24)
R2---R21( )
R2---R22(41, 48)
R2---R23(55, 57)
R3---R31(61, 62)
R3---R32(73, 74)
R3---R33(84, 86)

B 树具有如下特点:

  • 树树我啊,所有叶节点都在同一层呢。

  • 搜索时,从根节点起,对当前节点内的关键字(有序)进行二分查找。

    命中则结束。否则,进入那个对应范围的子节点。那个命中可能发生在叶节点,也可能在非叶节点。

    如果当前节点为空,则表示没有找到。

  • B 树的关键字集合分布在整棵树中,非叶节点和叶节点都存放数据

  • B 树的搜索性能等价于在关键字全集内进行二分查找

B+ 树:

B+ 树是 B 树的变体。

使用链表存储数据时,查找数据缓慢。因此将链表数据分为若干段,将每段的索引节点保存为树。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
graph TD
ROOT(0, 32, 61<br/>A - B - C)
ROOT---R1(0, 12, 23<br/>A - B - C)
ROOT---R2(32, 40, 52<br/>A - B - C)
ROOT---R3(61, 73, 84<br/>A - B - C)
R1---R11(0<br/>4<br/>9)
R1---R12(12<br/>13<br/>17)
R1---R13(23<br/>24<br/>25)
R2---R21(32<br/>38<br/>39)
R2---R22(40<br/>41<br/>48)
R2---R23(52<br/>55<br/>57)
R3---R31(61<br/>62<br/>66)
R3---R32(73<br/>74<br/>79)
R3---R33(84<br/>86<br/>87)
subgraph 数据链表
R11
R12
R13
R21
R22
R23
R31
R32
R33
end

B+ 树具有如下特点:

  • B+ 树的关键字都出现在叶节点的链表中,链表中数据是有序的。

    非叶节点只相当于叶节点的索引(稀疏索引),叶节点相当于是存储数据的数据层(稠密索引)。

  • B+ 树的命中只可能发生在叶节点。

  • B+ 树的搜索性能也等价于在关键字全集内进行二分查找

  • B+ 树更适合文件索引系统

B* 树:

B* 树是 B+ 树的变体,其在非根、非叶节点间加入了兄弟指针。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
graph TD
ROOT(0, 32, 61<br/>A - B - C)
ROOT---R1(0, 12, 23<br/>A - B - C)
ROOT---R2(32, 40, 52<br/>A - B - C)
ROOT---R3(61, 73, 84<br/>A - B - C)
R1---R11(0<br/>4<br/>9)
R1---R12(12<br/>13<br/>17)
R1---R13(23<br/>24<br/>25)
R2---R21(32<br/>38<br/>39)
R2---R22(40<br/>41<br/>48)
R2---R23(52<br/>55<br/>57)
R3---R31(61<br/>62<br/>66)
R3---R32(73<br/>74<br/>79)
R3---R33(84<br/>86<br/>87)
subgraph 数据链表
R11
R12
R13
R21
R22
R23
R31
R32
R33
end
subgraph 索引相连
R1
R2
R3
end

B* 树具有以下特点:

  • B* 树定义了非叶子节点关键字个数至少为 (2 / 3) * M。其块的最低使用率为 2 / 3,而 B+ 树最低使用率为 1 / 2
  • B* 树分配新节点的概率更低,空间使用率更高

前缀树

前缀树(字典树、单词查找树、键树),是一种多路查找树。利用元素的公共前缀来减少查询时间。

下面是一个存储了数个单词(a、act、art、cat、can、cant、roin)的前缀树:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
graph TB
R[ ]
style a fill: #C0F0E0
R --- a((a))
R --- c[c]
R --- r[r]
a --- ac[c]
a --- ar[r]
style act fill: #C0F0E0
ac --- act((t))
style art fill: #C0F0E0
ar --- art((t))
c --- ca[a]
style cat fill: #C0F0E0
ca --- cat((t))
style can fill: #C0F0E0
ca --- can((n))
style cant fill: #C0F0E0
can --- cant((t))
r --- ro[o]
ro --- roi[i]
style roin fill: #C0F0E0
roi --- roin((n))

前缀树具有如下特点:

  • 根节点不包含字符,除根节点外每一个节点包含一个字符。

  • 节点的路径即为一条存储字符串。特别的,根节点表示空字符串

    每个节点持有一个计数器,计算该节点处存储的字符串数量。

  • 所有的子节点都与父节点具有相同前缀。

  • 在前缀树中,查询字符串的时间复杂度为 O(L),其中 L 为字符串长度

实现前缀树:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
class TrimTree {
/* 节点类 */
private static class Node {
Map<Character, Node> next = null;
int count = 0;
Node() {
this.next = new HashMap<>();
this.count = 0;
}
}

private Node root = null; // 根节点

/* 构造器 */
public TrimTree(String... strings) {
this.root = new Node();
for (String s : strings) add(s);
}

/* 添加字符串 */
public void add(String s) {
Node p = root;
for (char c : s.toCharArray()) {
if (!p.next.containsKey(c)) p.next.put(c, new Node());
p = p.next.get(c);
}
p.count++;
}

/* 查找字符串 */
public int search(String s) {
Node p = root;
for (char c : s.toCharArray()) {
if (!p.next.containsKey(c)) return 0;
p = p.next.get(c);
}
return p.count;
}
}

线性表局限于一个直接前驱和一个直接后继的关系。

树可能有数个直接后继,但只能有一个直接前驱(父节点)

当需要表示多对多关系时,就需要 

图是一种数据结构。每个节点可以有零个或多个相邻元素。

两个节点间的连接称为 边(edge),节点也被称为 顶点(vertex)

图的分类:

  • 按照 顶点间的连接有无方向 分为:有向图、无向图
  • 按照 是否带权 分为:带权图(网)、非带权图
  • 按照 表示方式 分为:二维数组表示(邻接矩阵)、链表表示(邻接表)

一组连接的节点:

1
2
3
4
5
6
graph TD
A(1)---B(0)
A---C(2)
B---C
B---D(3)
B---E(4)

邻接矩阵:

1
2
3
4
5
6
   0  1  2  3  4
00, 1, 1, 1, 1
1 |1, 0, 1, 0, 0|
2 |1, 1, 0, 0, 0|
3 |1, 0, 0, 0, 0|
41, 0, 0, 0, 0

其中,(0, 1) == 1 表示 节点 0 与 节点 1 相连

邻接矩阵为每个顶点都分配了 n 个边的空间。这样,造成了空间的损失

邻接表:

1
2
3
4
5
0 [1]→[2]→[3]→[4]→
1 [0]→[2]→
2 [0]→[1]→
3 [0]→
4 [0]→

邻接表为每个节点创建一个链表,链表中是与其相连的节点。邻接表由 数组 + 链表 组成

邻接表只关心存在的边,不关心不存在的边,因此没有空间浪费

深度优先搜索 DFS

深度优先搜索(Depth First Search),其策略是优先纵向挖掘深入,而不是对一个节点的所有节点先进行横向访问。

从初始访问节点出发,首先访问其第一个相邻节点。之后,从那个访问节点出发,递归访问第一个相邻节点。直到一个节点的路径完全访问结束后,才访问第二个节点。

  • 访问初始节点 s,标记其为已访问
  • 从 s 的第一个相邻节点起,以递归方式对其进行深度优先搜索。
  • 当前节点没有可访问的相邻节点时,就完成了对一条路径访问。此时才返回上一级,继续搜索下一节点。

广度优先搜索 BFS

广度优先搜索(Broad First Search),其策略是优先横向访问所有相邻节点,而不是对一条路径进行纵向挖掘。

从初始访问节点出发,记录所有相邻节点。之后,访问先前记录节点,并记录所有相邻节点。直到没有能访问的节点为止,就完成了对所有连接节点的搜索。

  • 记录初始节点 s
  • 访问上一次记录的节点,将其标记为已访问。将那些节点的所有可访问的相邻节点记录。
  • 重复上一步,直到没有可访问的节点时,就完成了对所有连接节点的访问。

附录

F1 实现赫夫曼编码/解码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
/* 压缩数据包 */
class DataBox {
public byte[] data; // 压缩信息主体
public Map<Byte, String> key; // 赫夫曼表
public int step; // 补位数
}


class Huff {
/* 将数据压缩,返回一个压缩包 */
public static DataBox huff(byte[] data) {
DataBox dataBox = new DataBox();
dataBox.key = getHuffMap(data); // 在压缩包内记录编码表
dataBox.data = toHuff(data, dataBox); // 在压缩包内记录压缩后数据,也会记录补位数
return dataBox;
}

/* 根据要压缩的数据,计算那个赫夫曼表 */
private static Map<Byte, String> getHuffMap(byte[] val) {
if (val == null || val.length == 0) return new HashMap<>();
Map<Byte, Node> huff = new HashMap<>();
for (byte c : val) { // 记录每个字符出现的次数
if (huff.containsKey(c)) huff.get(c).times++;
else huff.put(c, new Node(c, 1));
}
PriorityQueue<Node> pq = new PriorityQueue<>(huff.values());
while (pq.size() > 1) { // 生成赫夫曼树
Node temp = new Node(pq.remove(), pq.remove());
pq.add(temp);
}
Map<Byte, String> ret = new HashMap<>();
update(ret, pq.remove(), ""); // 根据那个赫夫曼树,生成赫夫曼编码
if (ret.size() == 1) ret.put(val[0], "0"); // 特别地,只有唯一字符从场合这样处理
return ret;
}

/* 根据赫夫曼表,将数据压缩 */
private static byte[] toHuff(byte[] val, DataBox d) {
StringBuilder sb = new StringBuilder();
for (byte c : val) { // 得到压缩后的 bit 字符串
sb.append(d.key.get(c));
}
byte[] ret = new byte[(sb.length() + 7) / 8]; // 压缩后的数据放在 byte 数组中
d.step = sb.length() % 8; // 记录那个补位数
for (int i = 0; i < ret.length; i ++) {
if (i >= ret.length - 1 && d.step != 0) { // 最后一位可能有补位。那个场合,让有效数字在最左侧
ret[i] = (byte) (Integer.parseInt(sb.substring(8 * i), 2) << (8 - d.step));
} else ret[i] = (byte) Integer.parseInt(sb.substring(8 * i, 8 * i + 8), 2);
}
return ret;
}

/* 该方法能遍历赫夫曼树,以获取赫夫曼表 */
private static void update(Map<Byte, String> ss, Node root, String s) {
if (root == null) return;
else if (root.right == null && root.left == null) {
ss.put(root.val, s); // 是叶节点的场合,记录这个编码值
}
if (root.left != null) update(ss, root.left, s + "0"); // 向左路径记为 0
if (root.right != null) update(ss, root.right, s + "1"); // 向右路径记为 1
}

/* 解压压缩包 */
public static byte[] antiHuff(DataBox d) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < d.data.length; i++) { // 获取那个压缩数据的编码
String temp = null;
if (i >= d.data.length - 1 && d.step != 0) sb.append((temp = Integer.toBinaryString(d.data[i] | 256)), temp.length() - 8, temp.length() - 8 + d.step); // 遍历到最后,要处理那个补位
else sb.append((temp = Integer.toBinaryString(d.data[i] | 256)).substring(temp.length() - 8));
}
Map<String, Byte> anti = new HashMap<>();
for (Byte aByte : d.key.keySet()) { // 将编码表转化为解码表
anti.put(d.key.get(aByte), aByte);
}
List<Byte> ret = new ArrayList<>();
StringBuilder s = new StringBuilder();
for (int i = 0; i < sb.length(); i++) { // 按照解码表,把压缩编码转化为未解压编码
s.append(sb.charAt(i));
if (anti.containsKey(s.toString())) {
ret.add(anti.get(s.toString()));
s = new StringBuilder();
}
}
byte[] bt = new byte[ret.size()]; // 将 Byte 数组转化为 byte 数组
for (int i = 0; i < bt.length; i++) {
bt[i] = ret.get(i);
}
return bt;
}

/* 节点类,是构建赫夫曼树时用到的类 */
static class Node implements Comparable<Node> {
public byte val; // 代表的 byte 值
public int times; // 出现的次数
public Node left;
public Node right;

public Node(Node l, Node r) {
this.left = l;
this.right = r;
this.times = l.times + r.times;
}

public Node(byte val, int pow) {
this.val = val;
this.times = pow;
}


public Node(byte val) {
this.val = val;
this.times = 0;
}

@Override
public int compareTo(Node o) {
return this.times - o.times;
}
}
}

F2 实现平衡二叉树

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
class AVL{
public Node root = null; // 根节点

/* 添加一个值(添加一个节点)
val:要添加的值 */
public void add(int val) {
Node toAdd = new Node(val);
if (root == null) root = toAdd;
else {
Node par = root;
Node temp = root;
while (temp != null) { // 确定其插入位置
par = temp;
if (val > temp.val) {
temp = temp.right;
toAdd.way = true;
} else {
temp = temp.left;
toAdd.way = false;
}
}
if (toAdd.way) { // 将其插入到指定位置
par.right = toAdd;
par.right.parent = par;
} else {
par.left = toAdd;
par.left.parent = par;
}
while (true) { // 维护该平衡二叉树
par = toAVL(par);
if (par.parent == null) {
root = par;
break;
} else {
if (par.way) par.parent.right = par;
else par.parent.left = par;
par = par.parent;
}
}
}
}

/* 维护平衡二叉树
root: 待检查节点 */
private static Node toAVL(Node root) {
if (root == null) return null;
int gap = root.rightHeight() - root.leftHeight();
if (Math.abs(gap) > 1) { // |gap| > 1 时,需要旋转
if (gap > 0) { // gap > 0 需要左旋,否则右旋
if (root.right.leftHeight() > root.right.rightHeight()) root.right = roll(root.right, true);
return roll(root, false);
} else {
if (root.left.rightHeight() > root.left.leftHeight()) root.left = roll(root.left, false);
return roll(root, true);
}
} else return root;
}

/* 对该节点进行旋转。
root:待旋转节点
dirR:true 的场合右旋,否则左旋 */
private static Node roll(Node root, boolean dirR) {
Node temp = null;
if (dirR) {
temp = root.left;
root.left = temp.right;
if (temp.right != null) {
temp.right.way = false;
temp.right.parent = root;
}
temp.right = root;
} else {
temp = root.right;
root.right = temp.left;
if (temp.left != null) {
temp.left.way = true;
temp.left.parent = root;
}
temp.left = root;
}
temp.way = root.way;
temp.parent = root.parent;
root.way = dirR;
root.parent = temp;
return temp;
}

/* 一个展示树的方法。供 debug 用 */
public static void show(Node root) {
LinkedList<Node> a = new LinkedList<>();
LinkedList<Node> b = new LinkedList<>();
a.add(root);
while (!a.isEmpty()) {
Node temp = a.removeFirst();
System.out.print(temp.val + " ");
if (temp.left != null) b.add(temp.left);
if (temp.right != null) b.add(temp.right);
if (a.isEmpty()) {
System.out.println();
a = b;
b = new LinkedList<>();
}
}
System.out.println("共 " + count(root) + " 个节点");
}

/* 一个清点树中节点的方法 */
public static int count(Node root) {
if (root == null) return 0;
return 1 + count(root.left) + count(root.right);
}

public static class Node {
public int val; // 值
public Node left; // 左节点
public Node right; // 右节点
public Node parent; // 父节点
boolean way = false; // false:该节点是左节点;true:是右节点

public Node(int val) {
this.val = val;
}

public int leftHeight() { // 左子树高度
return (left == null ? 0 : left.height());
}

public int rightHeight() { // 右子树高度
return (right == null ? 0 : right.height());
}

public int height() { // 该节点树高度
return Math.max(leftHeight(), rightHeight()) + 1;
}
}
}