数据结构总结–树(JAVA)
树的相关知识总结
重新学习数据结构时,对树的一些较为重要的算法和知识点做一个总结,发此博客巩固知识,如果能帮助到大家,那么不胜荣幸。
树的概念
树是n(n >= 0)个结点组成的有限集合,其构成条件是:
(1)有且只有一个特定的成为根的结点
(2)其余的结点可以分为若干个互不相交的有限集合,每个集合又是一棵树,我们称其为子树
接下来是关于树的一些专业名词,结合下图会更容易理解:
度:一棵树的子树之和,如A有B,C,D三棵子树,故A的度为3,同理C的度为0。
深度(层数/高度):像楼房一样,我们记根为第一层,向下一层一层的数
就行,例如上图的深度就是4。
(注:以下名词利用辈分关系就能很好的理解)
孩子:理解了深度就很容易理解,如果两层间的结点是连起来的,那么更高一层的结点就是更低一层的孩子。比如上图,A和B连起来了,说明他们之间有关系,B就是A的孩子。
父亲:理解孩子以后显而易见A是B的父亲,同理B是E,F的父亲。
祖先&&子孙:就上述而言,A是B的父亲,B是E,F的父亲,关系说的远一些,就是E,F是A的子孙,A是E,F的祖先。
路径:连接两个结点的线段的数目,例如A到E的路径长度就2。
森林:就字面意思就很容易理解,若干棵组成的集合
经过上述对数的基本了解,再根据树的构成特点很容易写出如下代码:
package algorithm;
public class Tree {
//数据域
Object data;
//存数据
Tree[] child;
Tree(Object data){
this.data = data;
}
Tree(Object data, Tree[] child){
this.data = data;
this.child = child;
}
//建立子树
public void setchild(Tree[] child){
this.child = child;
}
//遍历
public void Tree_traversal(){
System.out.print(data + " ");
if(this.child != null){
for(Tree i:this.child){
i.Tree_traversal();
}
}
}
public static void main(String[] args) {
Tree root = new Tree(0);
Tree node1 = new Tree(1);
Tree node2 = new Tree(2);
Tree node3 = new Tree(3);
Tree node4 = new Tree(4);
Tree node5 = new Tree(5);
Tree node6 = new Tree(6);
root.setchild(new Tree[]{node1, node2, node3});
node1.setchild(new Tree[]{node4, node5});
node2.setchild(new Tree[]{node6});
root.Tree_traversal();
}
}
树较二叉树最为特殊的一点就是,它的根的子节点数目是不受限制的,因此可以在创建若干个结点以后,再将所需的子节点放入根中。
上述代码采用的是先序遍历。
树的先序遍历 = 二叉树的先序遍历
树的中序遍历 = 二叉树的后序遍历
剩下的遍历方法不在列举
二叉树
二叉树的概念
简单地说,二叉树就是每个结点最多只有两个且分左右的子树的特殊树,左边的叫左子树,右边的叫右子树,在结点数目n > 1时,没有子树的结点成为叶子。概念不再过多说明,重要的是性质
二叉树的性质
现实中的大部分可以与树挂钩的问题,一般都是使用二叉树进行解决的,因此了解其性质十分重要。
二叉树有如下重要性质:
以下图进行归纳,不再进行数学证明。
(1)二叉树第i层上的节点数目最多为2^(i-1)次,当(i>0)时。
从图中很容易得出结论,每个结点都是以2的倍数增长,当然要出去第一层,用第三层验证即可。
(2)深度为k的二叉树最多有2^(k) - 1个结点,同样的,根据上图验证即可。
(3)在任意一棵二叉树中,若终端结点的个数为n0,度为2的结点数为n2,则n0 = n2 + 1
这个稍加证明即可:
二叉树的结点类型只有三种,(1)度为0,设个数为n0。(2)度为1,设个数为n1。(3)度为2,设个数为n2。
那么不难得出总结点数n = n0 + n1 + n2
再来仔细观察,上述中的孩子总数为n00 + n11 + n22,而根节点是所有孩子的父亲,所以总结点数也可以写成n = n1 + 2n2 + 1,两式减一下即可。
(4)书上还列举了一个n个结点的二叉树的深度,其实就是对特征(2)取个反函数。
(5)完全二叉树:完全二叉树的概念十分重要,什么是完全二叉树?简单来说就是从一棵二叉树的右下角开始删结点,先删右再删左,一层一层的删。这就说明了一个问题,有左孩子不一定有右孩子,但没有左孩子一定没有右孩子。
二叉树的存储
在了解了二叉树的性质之后,趁热打铁,了解二叉树是如何存储的。
依然使用上图列举
设一棵二叉树有n个结点,i为范围内任意结点
(1)若i > 1,则i的双亲编号为i/2。
(很容易发现上图的编号是一层一层的按顺序命名的,在程序中也是一样,将Object类中的结点存入也是按顺序的)
例如2和3的双亲就是,2/2 = 3/2 = 1
(2)2i <= n时,说明2i是i结点的左孩子,若2i>n说明该节点是叶子。
例如上图,42 = 8 <= 8,8就是4的左孩子,82 = 16 > 8,8就是叶子。
(3)2i + 1 <= n,说明2i+1是i的右孩子,反之无右孩子
(4)若i为奇数且i > 1,则i - 1为i的左兄弟,同理若i为偶数且i < n, i + 1为i的右兄弟。
了解上述以后可以得出二叉树的代码:
package algorithm;
import java.util.*;
public class BinaryTree {
public BinaryTree root;
public BinaryTree left;
public BinaryTree right;
//这个实例变量在重构二叉树中会用到,如果只是为了创建二叉树可以忽略
public Object new_root;
//数据域
public Object data;
//结点
public List<BinaryTree>datas;
//构造方法
BinaryTree(){
}
BinaryTree(BinaryTree left, BinaryTree right, Object data){
this.left = left;
this.right = right;
this.data = data;
}
//继上一个使用,创建一个左右孩子都为空的结点
BinaryTree(Object data){
this(null, null, data);
this.new_root = data;
}
//创建树
public void creat_btree(Object[] btree){
datas = new ArrayList<>();
//放进集合中
for(Object i:btree){
datas.add(new BinaryTree(i));
}
//第一个元素作为根
root = datas.get(0);
//分层依次填入
for(int k = 0; k < btree.length/2; k++){
//左孩子为2k
datas.get(k).left = datas.get(k*2 + 1);
//右孩子为2*k + 1,且不一定有右孩子
if((k*2 + 2) < datas.size()){
datas.get(k).right = datas.get(k*2 + 2);
}
}
}
public void pre_traversal(BinaryTree root){
if(root != null){
System.out.print(root.data + " ");
//找左孩子
pre_traversal(root.left);
//找右孩子
pre_traversal(root.right);
}
}
public void mid_traversal(BinaryTree root){
if(root != null){
mid_traversal(root.left);
System.out.print(root.data + " ");
mid_traversal(root.right);
}
}
public void pro_traversal(BinaryTree root){
if(root != null){
pro_traversal(root.left);
pro_traversal(root.right);
System.out.print(root.data + " ");
}
}
public static void main(String[] args) {
BinaryTree bt = new BinaryTree();
//Object[] btree = {1, 2, 3 , 4, 5, 6, 7, 8};
Object[] btree = {"A", "B", "C", "D", "E", "F", "G", "H"};
bt.creat_btree(btree);
bt.pre_traversal(bt.root);
System.out.println("");
bt.mid_traversal(bt.root);
System.out.println("");
bt.pro_traversal(bt.root);
}
}
上述代码包含了树的三种遍历:
先序遍历:先输出根结点再输出左孩子,最后输出右孩子。
中序遍历:先输出左孩子再输出根结点,最后输出右孩子。
后序遍历:先输出左孩子再输出右孩子,最后输出根结点。
上述输出为:
A B D H E C F G
H D B E A F C G
H D E B F G C A
重构二叉树
有见到过一道题目:
大概意思就是说给你序列让你把这棵二叉树还原出来然后再遍历给它看一遍。一般称这种问题为重构二叉树。
解决这种问题需要知道:
用先序遍历和中序遍历可以求出后序遍历。
用中序遍历和后序遍历可以求出先序遍历。
我这里以给出先序遍历和中序遍历求后序遍历做出示范:
先序序列:A B D H E C F G
中序序列:H D B E A F C G
由先序遍历:先输出根结点再输出左孩子,最后输出右孩子。
中序遍历:先输出左孩子再输出根结点,最后输出右孩子。
后序遍历:先输出左孩子再输出右孩子,最后输出根结点。
易得出,根在中序遍历中起到了分隔的作用,即根的左侧为左子树,根的右侧为右子树。我们再来观察先序遍历,先序遍历的第一个结点一定是根节点。由此不难想出:第一步,我们在输入先序序列之后可以将先序序列的第一个值保存下来,拿到中序序列里进行搜索,将整棵树左右分明。第二步,那就紧接着第一步向下索引,每找到一个根节点就将其左右分明。(也就是向下递归)。
以下是图解:
以先序遍历的特性,先根再左最后右的性质,依次在中序遍历中查找它的位置,然后再查找它的孩子,这层找完了就找下一层。
代码大致这样:
//pre是先序序列,mid是中序序列
public static BinaryTree res_binarytree(Object[] pre, Object[] mid){
//空值返回null
if(0 == pre.length || 0 == mid.length){
return null;
}
//创建根节点
BinaryTree new_bt = new BinaryTree(pre[0]);
//在中序遍历中找根,并记为index,没找到就默认为-1
int index = -1;
for(int i = 0; i < mid.length; i++){
if(new_bt.new_root == mid[i]){
index = i;
break;
}
}
//重要的一部分,需要将前序和中序的左子树和右子树分离出来
//值得注意的是先序序列的左子树要从1开始拷贝
Object[] left_pre = Arrays.copyOfRange(pre, 1, index + 1);
Object[] right_pre = Arrays.copyOfRange(pre, index + 1, pre.length);
//中
Object[] left_mid = Arrays.copyOfRange(mid, 0, index);
Object[] right_mid = Arrays.copyOfRange(mid, index + 1, mid.length);
//用递归,找每个结点的孩子
new_bt.left = res_binarytree(left_pre, left_mid);
new_bt.right = res_binarytree(right_pre, right_mid);
return new_bt;
}
思路是与上面一致的,先把根放到第一个结点上,再在mid中找它在哪里。
把位置记下来,用copyOfRange进行拷贝。需要注意到的是,两个序列的左右子树都需要分出来,你只分出来一个怎么进行递归呢?
下面是完整的代码:
package algorithm;
//该程序是与上面的创建二叉树联用的,BinaryTree在上面
/*
如果不想用上面的可以创一个
class BinaryTree{
BinaryTree left;
BinaryTree right;
int data;
BinaryTree(int data){
this.data = data;
}
}
这样形式的也可以,因为重构二叉树和生成二叉树是有所不同的
*/
import java.util.Arrays;
public class Res_binarytree {
public static void main(String[] args) {
Object[] pre = new Object[]{"A", "B", "D", "H", "E", "C", "F", "G"};
Object[] mid = new Object[]{"H", "D", "B", "E", "A", "F", "C", "G"};
BinaryTree bt = res_binarytree(pre, mid);
pro_traversal(bt);
}
public static void pro_traversal(BinaryTree root){
if(root != null){
pro_traversal(root.left);
pro_traversal(root.right);
System.out.print(" " + root.new_root);
}
}
public static BinaryTree res_binarytree(Object[] pre, Object[] mid){
//空值返回null
if(0 == pre.length || 0 == mid.length){
return null;
}
//创建根节点
BinaryTree new_bt = new BinaryTree(pre[0]);
//在中序遍历中找根,并记为index,没找到就默认为-1
int index = -1;
for(int i = 0; i < mid.length; i++){
if(new_bt.new_root == mid[i]){
index = i;
break;
}
}
//重要的一部分,需要将前序和中序的左子树和右子树分离出来
Object[] left_pre = Arrays.copyOfRange(pre, 1, index + 1);
Object[] right_pre = Arrays.copyOfRange(pre, index + 1, pre.length);
//中
Object[] left_mid = Arrays.copyOfRange(mid, 0, index + 1);
Object[] right_mid = Arrays.copyOfRange(mid, index + 1, mid.length);
//用递归,找每个结点的孩子
new_bt.left = res_binarytree(left_pre, left_mid);
new_bt.right = res_binarytree(right_pre, right_mid);
return new_bt;
}
}
一般树转化为二叉树
首先需要知道,每个森林每棵一般树都有与之唯一对应的二叉树,
一般树的先序遍历 = 二叉树的先序遍历
一般树的中序遍历 = 二叉树的后序遍历
而一般树转化为二叉树的方法就是,将结点的兄弟练成一条线:
然后把左兄弟与父亲的线留下,去掉其它连线,像这样:
接下来把这个树往上一拉(你可以把它想象成实物放在桌子上,然后被拉起来受到重力作用那样),原本在左边的就是左孩子,在右边的就是右孩子。
最后就变成了这样:
感觉一般树变二叉树的方法和我们平时开的玩笑很像,我拿你当兄弟你却相当我爹??
好了废话不多说,其实理解了上述的方法以后还是很容易想出代码要怎么写的。无非就是创建一棵树和一棵二叉树,把树的数据域放到二叉树里面,并通过递归来找每个结点的子树,子树的第一个结点作为左孩子,其余全部变成右孩子。
但是经过实践以后我发现有一个点是需要我们注意的:就是如果每次填入数据都要创建一棵新的二叉树和树是非常麻烦的,我们可以通过使用接口来找到两种树的共有特征!比如不管是哪种树一定都有数据域和遍历吧,可以把数据域放进接口中让两种树一起用就会方便很多。
因为树和二叉树的生成我都已经在上面写过,所以接下来就只写出算法的代码,有需要,你们结合上述程序就行:
//一般树转化二叉树的方法
BinaryTree Transform_binaryTree(){
//本身
Tree new_tree = this;
BinaryTree new_btree = new BinaryTree();
//先将树的数据保存到二叉树中
new_btree.data = new_tree.data;
//可以想成交换的算法,通过temp作为中间变量,使下一次找到上一次
BinaryTree temp = new_btree;
//先判断是否有孩子
if(new_tree.child != null){
//递归填入
for(int i = 0; i < new_tree.child.length; i++){
BinaryTree btree = (new_tree.child[i]).Transform_binaryTree();
//根结点子类的第一个结点一定是左孩子
if(i == 0){
temp.left = btree;
temp = temp.left;
}
else{
temp.right = btree;
temp = temp.right;
}
}
}
return new_btree;
}
使用接口就直接定义共同特征,然后然两个类继承就行。
哈夫曼树
终于是到了树的最后一部分,哈夫曼树又称最优二叉树,也就是给你几个有大小节点,让你画一棵树,让路径长度达到最小。这是非常重要的知识。
一棵二叉树的路程怎么算?说白了就是 每个值 * 路径的长度的和。例如下图(绝对不是因为作者懒得画图):
比如a的路径WPL = 72 + 52 + 22 + 42 = 36
b的WPL = 42 + 21 + 73 + 53 = 46
c的WPL = 71 + 52 + 23 + 43 = 35
其实根据上图就很容易发现只要把值越大的点放在里根越近的地方就可以实现路径最优。
那么接下来就介绍一下哈夫曼树的构造方法:
现在给出权值为1,2,3,4,5,6,7,8的八个结点,求最优二叉树。
(1)把几个结点都拿出来放在一层上:
(2)然后然最小的两个数相加,求得的和作为一个新的根,其它的结点移到和这个根同一层:
(3)一直重复(2)直到出最后一个结点为止:
最后就是像上面那样,很容易发现每个根节点右边都只有一片叶子。
了解了上述之后,不难想出代码怎么写。无疑就是将要求中的树进行排序,然后把每个生成的结点求出来生成一个新的序列再进行排序,最后把两个序列都放在二叉树里,和的那个序列全为右子树(左子树也行),题目给你的那个也一样。
下面的程序我将大的放在了右子树,因为哈夫曼树的根恰好就是和序列的最大值,而且生成二叉树是需要先生成左孩子的,这样操作更方便。
package algorithm;
/**
*
* @author 32226
*/
import java.util.*;
public class Huffman_tree {
//属性
Huffman_tree left;
Huffman_tree right;
Huffman_tree root;
//数据域
int data;
List<Huffman_tree>datas;
Huffman_tree(){
}
Huffman_tree(Huffman_tree left, Huffman_tree right, int data){
this.left = left;
this.right = right;
this.data = data;
}
//创建一个结点
Huffman_tree(int data){
this(null, null, data);
}
//创建哈夫曼树
public void creat_huffman_left(int[] left, int[] right){
datas = new ArrayList<>();
List<Huffman_tree>datas2 = new ArrayList<>();
//这里我默认将较大的结点放在左孩子上,和教材相反,其实不影响求权值
for(int i:left){
//把对象写入
datas.add(new Huffman_tree(i));
}
for(int j:right){
datas2.add(new Huffman_tree(j));
}
//选最大值为根
root = datas.get(0);
for(int k = 0; k < left.length-1; k++){
datas.get(k).left = datas.get(k + 1);
datas.get(k).right = datas2.get(k);
}
//整个程序需要注意的是这里,因为两个序列的长度差一,而且树的最底层的左右子树是原序列里的两个最小值
datas.add(new Huffman_tree());
datas.get(left.length - 1).left = datas2.get(right.length - 1);
datas.get(left.length - 1).right = datas2.get(right.length - 2);
}
//先序遍历
public void pre_traversal(Huffman_tree root){
if(root != null){
System.out.print(root.data + " ");
pre_traversal(root.left);
pre_traversal(root.right);
}
}
//采用降序方便填入
public static int[] Order(int[] tree){
for(int i = 0; i < tree.length - 1; i++){
for(int j = 0; j < tree.length - 1; j++){
if(tree[j] < tree[j + 1]){
int temp = tree[j];
tree[j] = tree[j + 1];
tree[j + 1] = temp;
}
}
}
return tree;
}
//计算最优解
public static int[] solution(int[] tree){
int[] new_tree = new int[tree.length - 1];
new_tree[0] = tree[tree.length - 1] + tree[tree.length - 2];
for(int i = tree.length - 2; i > 0 ; i--){
new_tree[new_tree.length - i] = new_tree[new_tree.length - i - 1] + tree[i - 1];
}
return new_tree;
}
public static void main(String[] args) {
int[] a = new int[]{1, 2, 3, 4, 5, 6, 7, 8};
Huffman_tree tree = new Huffman_tree();
int[] left = Order(solution(Order(a)));
int[] right = Order(a);
tree.creat_huffman_left(left, right);
tree.pre_traversal(tree.root);
}
}
总结
以上就是我在复习数据结构——树时的一些想法,不仅代码写的烂,第一次写文章,文章写的也不太好,但还是凑合着看吧,以后会多写博客的,这是一个很好的学习手段。
不定期会写java,python,lua,以及饥荒mod开发教程的,如果文章有什么问题请留言!谢谢!