树和二叉树
树
树是一种非线性的数据结构,数据元素之间之间的关系是一对多的关系。
树的节点
节点:使用树结构存储的每一个数据元素都被称为节点。例如上图中的B就是一个节点。
根节点:每一个非空树都有且仅有一个根节点。如果一个节点没有父节点,那么这个节点就是树的根节点。例如上图中的A。
叶子节点:如果节点没有任何子节点,那么次节点就是叶子节点。
父节点、子节点、兄弟节点:上图中,A是B、C、D的父节点。B、C、D是A的子节点。B、C、D为兄弟节点。
节点的度:拥有的子节点数,为该节点的度。树内各节点的度的最大值为树的度。
树的存储结构
-
双亲存储结构
该存储利用了每个节点(根节点除外)的唯一双亲的性质。这种结构下,查找孩子节点很容易,查找父节点就需要遍历整个树结构。
-
孩子链存储结构
这种存储结构,每个节点不仅包含数据值,还包含指向所有孩子节点的指针。查找孩子节点很容易,查找父节点比较费时。如果树的度较大,存在较多的空指针域。
-
孩子兄弟链存储结构
这种存储结构的优点是,方便实现树和二叉树的转换。确定也孩子链存储结构的缺点一样,查找当前节点的父节点 比较麻烦。
二叉树
二叉树性质:
- 非空二叉树上叶子节点树等于双分支节点数+1
- 非空二叉树上第i层最多有2^(i-1)次方个节点
- 高度为h第二叉树最多有2^(h-1)次方个节点
满二叉树
如果二叉树中除了叶子结点,每个结点的度都为 2,则此二叉树称为满二叉树。
满二叉树的特点:
- 满二叉树中第 i 层的节点数为 2^(n-1)个。
- 深度为 k 的满二叉树必有 2^(k)-1个节点 ,叶子数为2^(k-1)。
- 满二叉树中不存在度为1节点,每一个分支中都有两个课深度相同的子树,且叶子节点都在最底层。
- 具有 n 个节点的满二叉树的深度为 log2(n+1)。
完全二叉树
如果二叉树中最多只有最下面两层的节点的度数小于2,且最后一层的结点依次从左到右分布,则此二叉树被称为完全二叉树。
对于任意一个完全二叉树,如果将含有的结点按照层次从左到右依次标号,对于任意一个结点 i :
- 当 i>1 时,父亲结点为结点 [i/2] 。(i=1 时,表示的是根结点,无父亲结点)
- 如果 2i>n(总结点的个数) ,则结点 i 肯定没有左孩子(为叶子结点);否则其左孩子是结点 2i 。
- 如果 2i+1>n ,则结点 i 肯定没有右孩子;否则右孩子是结点 2i+1 。
二叉树的存储结构
二叉树的顺序存储结构
使用顺序表(数组)存储二叉树。
顺序存储只适用于完全二叉树。换句话说,只有完全二叉树才可以使用顺序表存储。
上图的完全二叉树的顺序存储如下:
普通二叉树转换称完全二叉树:为二叉树额外添加一些节点,将其凑成完全二叉树。
由于完全二叉树的特点,从顺序表中还原完全二叉树也很简单:
若节点 i 有左右孩子,则其左孩子节点为 2i,右孩子节点为 2i+1。此性质可用于还原数组中存储的完全二叉树。
二叉树的链式存储结构
链式存储指用过一个链表存储二叉树。二叉树中每一个节点用链表中的一个链节点存储。节点结构如下:
lchild | data | rchild |
---|---|---|
左孩子指针 | 节点的值 | 右孩子指针 |
某些实际场景中,可能会做 “查找某节点的父节点” 的需求,这时可以在节点结构中再添加一个指针域,用于各个节点指向其父亲节点。
二叉树的遍历
按照一定的顺序访问二叉树中的所有节点,并且每个节点只访问一次的过程。
先序遍历
先序遍历过程:
- 访问当前节点;
- 访问当前节点的左子树;
- 若无当前节点没有左子树,访问右子树。
上图的先序遍历的结果为:ABDECFG.
class Node{
String data;
Node lchild;
Node rchild;
}
public void preOrder(Node root){//递归
if(root!=null){
System.out.println(root.data);
preOrder(root.lchild);
preOrder(root.rchild);
}
}
public void preOrder(Node root) {//非递归
List<Node> stack = new ArrayList<Node>();
int index = -1;
if (root != null) {
Node tmp = null;
stack.add(root);
index++;
while (index > -1) {
tmp = stack.get(index--);
System.out.print(tmp.data);
if (tmp.rchild != null) {
index++;
stack.add(tmp.rchild);
}
if (tmp.lchild != null) {
index++;
stack.add(tmp.lchild);
}
}
}
System.out.println();
}
后序遍历
后序遍历过程:从根节点出发,依次遍历各节点的左右子树,知道当前节点的左右子树遍历完成,才访问该节点元素。
上图的后序遍历的结果为:DEBFGCA.
class Node{
String data;
Node lchild;
Node rchild;
}
public void postOrder(Node root){//递归
if(root!=null){
postOrder(root.lchild);
postOrder(root.rchild);
System.out.println(root.data);
}
}
中序遍历
中序遍历过程:
- 访问当前节点的左子树;
- 访问根节点;
- 访问当前节点的右子树。
上图的中序遍历的结果为:DBEAFCG.
class Node{
String data;
Node lchild;
Node rchild;
}
public void inOrder(Node root){//递归
if(root!=null){
inOrder(root.lchild);
System.out.println(root.data);
inOrder(root.rchild);
}
}
public void inOrder(Node root) {//非递归
List<Node> stack = new ArrayList<Node>();
int index = -1;
if(root!=null){
Node tmp = root;
while(tmp!=null||index>-1){
while(tmp!=null){
index++;
stack.add(tmp);
tmp = tmp.lchild;
}
}
if(index >-1){
tmp = stack.get(index--);//出栈
System.out.print(tmp.data);//访问
tmp = tmp.rchild;
}
}
System.out.println();
}
层次遍历
基本思想:使用对列,从树的根节点开始,依次将其左孩子和右孩子入队。而后每次对列中一个节点出队,都将其左孩子和右孩子入队。知道所有节点都出队。
上图的层次遍历的结果为:ABCDEFG.
class Node{
String data;
Node lchild;
Node rchild;
}
public void levelOrder(Node root) {
List<Node> queue = new ArrayList<Node>();//队列
int front = 0;
queue.add(root);
int rear = 1;
while (front < rear) {
Node node = queue.get(front);//出队
System.out.println(node.data);
if (node.lchild != null) {
queue.add(node.lchild);
}
if (node.rchild != null) {
queue.add(node.rchild);
}
}
}
霍夫曼树
实际问题中,节点被赋予一个有实际意义的值,为该节点的权。从根节点到某个节点之间的路径长度和该节点上权重的乘积称为该节点的带权路径长度。树中所有叶子节点带全路径长度之和称为树的带全路径长度,通常为:
W
P
L
=
∑
i
=
1
100
w
i
l
i
WPL=\sum_{i=1}^{100}{w_il_i}
WPL=i=1∑100wili
其中n为叶子节点的个数,w和l分别表示叶子节点的权重和路径长度。
在n个带权叶子节点构成的所有二叉树中,带全路径长度WPL最小的二叉树被称为哈夫曼树。
哈夫曼树的构造
给n个节点,构造哈夫曼树的过程如下:
- 在 n 个权值中选出两个最小的权值,对应的两个结点组成一个新的二叉树,且新二叉树的根结点的权值为左右孩子权值的和;
- 在原有的 n 个权值中删除那两个最小的权值,同时将新的权值加入到 n–2 个权值的行列中,以此类推;
- 重复 1 和 2 ,直到所以的结点构建成了一棵二叉树为止,这棵树就是哈夫曼树。
节点1、3、5、7的构建哈夫曼树的过程如下:
对于具有n个节点的哈夫曼树,共有2n-1个节点。
哈夫曼树的节点结构
class HTNode{
Object data;//数据
int weight;//权重
int lchild;//左孩子
int rchild;//右孩子
int parent;//父节点
}
哈夫曼树的构造
public HTNode[] createHuffmanTree(int[] weights) {
HTNode[] htNodes = new HTNode[weights.length];
for (int i = 0; i < 2 * htNodes.length - 1; i++) {//初始化所有节点
HTNode htNode = new HTNode();
htNode.data = null;
if (i < htNodes.length) {
htNode.weight = weights[i];
} else {
htNode.weight = -1;
}
htNode.lchild = -1;
htNode.rchild = -1;
htNode.parent = -1;
htNodes[i] = htNode;
}
for (int i = htNodes.length; i < 2 * htNodes.length - 1; i++) {
int min1 = Integer.MIN_VALUE;
int min2 = Integer.MIN_VALUE;
int rnode = -1;
int lnode = -1;
for (int j = 0; j < i - 1; j++) {
if (htNodes[j].parent == -1) {
if (htNodes[j].weight < min1) {
min2 = min1;
rnode = lnode;
min1 = htNodes[j].weight;
lnode = j;
} else if (htNodes[j].weight < min2) {
min2 = htNodes[j].weight;
rnode = j;
}
}
}
htNodes[i].weight = htNodes[lnode].weight + htNodes[rnode].weight;
htNodes[i].lchild = lnode;
htNodes[i].rchild = rnode;
htNodes[lnode].parent = i;
htNodes[rnode].parent = i;
}
return htNodes;
}
其中数组的最后一个元素为树的根节点。
哈夫曼编码
哈夫曼编码就是在哈夫曼树的基础上构建的,这种编码方式最大的优点就是用最少的字符包含最多的信息内容。具体的方法如下:
- 设需要编码的字符集为(d1,d2,d3…dn);
- 各字符在文本出现的概率为(w1,w2,w3…wn);
- 以字符为节点,概率为权重,构建一棵哈夫曼树;
- 规定哈夫曼树中左分支右0,右分支右1;
- 从根节点到叶子节点所经历过的分支的0和1组成序列为该叶子节点对应的字符的编码,这样的编码为哈夫曼编码。
/**
* @param htNodes 哈夫曼树
* @param n 节点数
*/
public void huffmanCoding(HTNode[] htNodes, int n) {
for (int i = n - 1; i >= 0; i--) {//从叶子节点逆向开始
HTNode htNode = htNodes[i];
System.out.print(htNode.data.toString()+"的哈夫曼编码是:");//字符
int index = i;
while (htNode.parent != -1) {//一直到根
int parent = htNode.parent;
if (htNodes[parent].lchild == index) {//如果父节点的左节点等于本节点。输出0
System.out.print("0");
} else if (htNodes[parent].rchild == index) {//如果父节点的右节点等于本节点。输出1
System.out.print("1");
}
htNode = htNodes[parent];
index = parent;
}
System.out.println();
}
}
例:如果电文由a,b,c,d,e,f,g,h构成,出现的频率依次为0.07、0.19、0.02、0.03、0.06、0.32、0.03、0.21。
构成的哈夫曼树如下:
哈夫曼编码为:
a:1010 b:00 c:10000 d:1001 e:11 f:10001 g:01 h:1011