二叉树
使用二叉树原因:
有序数组 VS 链表
|
有序数组 |
链表 |
比较 |
插入 |
O[N] |
O[1] |
链表插入删除快 |
查找 |
O[log(2)N] |
O[N] |
有序数组查找快 |
删除 |
O[N] |
O[1] |
链表插入删除快 |
在有序数组中插入数据项太慢,在链表中查找太慢.
要是能有一种数据结构,既能像链表那样快速的插入和删除.又能像有序数组那样快速查找.
=è
树实现了这些特点.成为最有意思的数据结构之一.
几种二叉树:
非平衡树:
概念:大部分的节点在根的一边或者另外一边
造成非平衡的原因:
数据项插入顺序造成的.如果关键字是随机插入的.则树或多或少更平衡一些.
如果插入顺序是升序<11 18 33 42 65>或者降序.
则所有的值都是右节点<升序>或者左节点<降序>.这样树就不平衡了.
平衡树:
完全二叉树:
二叉树:
若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值;
若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值;
它的左、右子树也分别为二叉查找树。
是否为空:
判断根节点是否为空,就可以判断整棵树是否为空:
查找结点:
效率O[log2N]:
取决于层数,节点在哪一层就有多少次比较.
分析:
图解:
public Node find(int name);
输入参数为目标数据项
Current 指针指向的节点为当前数据项
1 将current 指针指向root 根节点
2 进入循环.循环条件: 当前指针所指向的节点数据项不等于目标数据项
2.1 如果目标数据项小于当前数据项
2.2 指针移动向左节点
2.3如果目标数据项大于当前数据项
2.4 指针移动向右节点
2.5 当前节点为空 抛出找不到数据项的异常.
3 返回当前数据项
插入结点 :
分析:首先要定位插入位置.该过程类似于要找到一个不存在节点的过程.
图解:
假设要插入关键字为45的节点.第一步找到该节点的插入位置.45比60小但是比40大.所以turn right转向50.
45小于50,所以turn left,但是50 没有左子节点.它的leftchild字段等于null.找到null后 ,就等于找到新节点的位置了.为45建立一个新的节点.并把它连接到50的左子节点处.
1 新建节点
2 检查树是否为空树
将根节点赋予新值
3 如果树不为空
3.1 用当前指针搜索整个树.并保持与parentNode的关系
3.1.1 新数据项小于当前节点.当前节点指向左子节点
如果左子节点为空.把新节点接到parentNode的左子节点处
返回函数
3.1.2 新数据项大于当前节点.当前节点指向右子节点
如果右子节点为空.把新节点接到parentNode的左右节点处
返回函数
3.1.3 新数据项等于当前数据项
抛出数据重复异常
二叉树插入顺序与树形结构
执行如下代码:
BinaryTree theTree = new BinaryTree();
theTree.insert(20, "E");
theTree.insert(50, "B");
theTree.insert(40, "G");
theTree.insert(10, "A");
theTree.insert(20, "C");
theTree.insert(60, "F");
theTree.insert(70, "H");
二叉树结构以及插入顺序
遍历树:
有三种方式遍历整棵树:中序遍历,前序遍历,后序遍历
如下图所示 :
不同的遍历方式,显示节点顺序也不同.
中序遍历inorder <最常用>
特点:所有节点按关键字值升序被访问到.
遍历节点可以应用于任何二叉树.而不只是二叉搜索树.
关键点是该节点是否有子节点.与节点上面的值无关.
应用:二叉树创建有序数列
算法:递归.
用一个节点作为参数.初始化时这个节点是根节点.
1 调用自身来遍历节点的左子树
2 访问这个节点<访问这个节点可代表显示节点,把节点写入文件等>
3 调用自身来遍历节点的右子树.
代码:
Private void inOrder(node localRoot){
//当前节点不为0的情况下
If(localRoot!=null){
//递归遍历左子节点
inOrder(localRoot.leftChild);
//打印当前节点
System.out.print(localRoot.iData+” “);
//递归遍历右子节点
inOrder(localRoot.rightChild);
}
}
具体步骤:
遍历一颗只有三个节点的树 :
下图为,节点作为输入参数的顺序.
图形结构:
前序遍历preorder
代码:
输出结果以及分析:
后序遍历postorder
前序遍历后序遍历的应用:
编写程序来解析或者分析代数表达式.
根节点:用于保存运算符号.其他节点或者保存变量名,或者保存运算符号,每一棵子树都是一个合法的代数表达式.
编写程序根据后序表达式建立树.
可以相当容易地根据输入的后缀表达式建立一颗像图8.11那样的树.
方法类似于计算后缀表达式.方法类似于计算后缀表达式.
不过这里面不是在栈中储存操作数,而是储存整个子树.
顺序的读取后缀字符串:
遇到操作数时候的操作步骤:
1 建立一棵树.它只有一个保存操作数的节点
2 这棵树入栈
遇到操作符时候的操作步骤
1 两个操作数的树B和C出栈
2 建立一棵新的树A 操作符是根
3 把B接到A的右子节点处
4 把C接到A的左子节点处
5 把得到的新树压入节点
这样,在分解完成后缀表达式后,把栈中所剩的唯一的一个数据项出栈。奇妙的是这个数据项是一颗完整的树,
表示整个算术表达式.
interface Tree { Node root = null; //根据目标数据项 查找指定节点 public Node find(String value); public void insert(int iData, String name); public boolean delete(String name); //找到目标结点的中继节点 private Node getSuccessor(Node delNode); //根据输入选项判断执行前序中序后序哪种遍历方式 public void traverse(int i); // 前序遍历 中序遍历 后序遍历 private void preOrder(Node localNode); private void inOrder(Node localNode); private void postOrder(Node localNode); //显示树形结构 public void displayTree(); //查找最小子节点 Public Node minimum(); //查找最大子节点 Public Node maxmum(); }
|
二叉树的方法:
interface Tree { Node root = null; //根据目标数据项 查找指定节点 public Node find(String value); public void insert(int iData, String name); public boolean delete(String name); //找到目标结点的中继节点 private Node getSuccessor(Node delNode); //根据输入选项判断执行前序中序后序哪种遍历方式 public void traverse(int i); // 前序遍历 中序遍历 后序遍历 private void preOrder(Node localNode); private void inOrder(Node localNode); private void postOrder(Node localNode); //显示树形结构 public void displayTree(); //查找最小子节点 Public Node minimum(); //查找最大子节点 Public Node maxmum(); }
|
可以用递归替代的是 :
//find node with given name 递归的查找方法
public Node findRec(Node localRoot ,String target)
//递归方法查找最小的子节点
public Node minimumRecs(Node localNode)
//递归方法查找最大节点的
public Node maxmumRecs(Node localNode)
查找最大值和最小值:
查找最小值:
首先走到根的左子节点处,接着走到那个节点的左子节点.以此类推.直到找到一个没有左子节点的节点.这个节点就是最小值的节点.
查找最大值:
一直向右,直到找到没有右子节点的节点.这个节点就是最大值的节点.
删除节点:
该节点是叶节点 <没有子节点>.
1 找到该节点
2 判断该目标结点是否真的没有子节点的
2.1 如果该节点是根节点.将树的根节点置为空.整个树即使空
2.2 判断该节点时父节点的左子节点或者右子节点.
2.2.1 将该节点与其父节点disconnect
删除有一个子节点的节点:
这个节点只有两个连接.:连接向父节点.和连向他唯一的子节点.
需要从这个序列中剪断这个节点.把它的子节点直接练到它的父节点上.
这个过程要求改变父节点适当的引用.是目标结点的父节点指向它的子节点.
注意; 引用使得移动整棵子树非常容易.只要断开连向子树的旧的引用.建立新的引用连到别处去就可以了.虽然子树种可能有很多节点.但不必要一个一个移动它们.实际上.它们的移动只是概念上移动到其他位置.和其他节点关联.
四种情况:
|
目标结点有左子节点 |
目标结点有右子节点 |
目标是父节点的左子节点 |
current.rightChild=null; isLeftChild=true; |
current.leftChild=null; isLeftChild=true; |
目标是父节点的右子节点 |
current.rightChild=null; isLeftChild=false; |
current.leftChild=null; isLeftChild=false; |
1 找到该节点
2 判断该目标结点是否真的没有子节点的
2.1 如果该节点是根节点.将树的根节点置为空.整个树即使空
2.2 判断该节点时父节点的左子节点或者右子节点.
2.2.1 将该节点与其父节点disconnect
3 确认目标结点只有左子节点
3.1 目标结点是根节点
3.2 目标结点是父节点的左子节点
3.3 目标结点是父节点的右子节点
4 确认目标结点只有右子节点
4.1 目标结点是根节点
4.2 目标结点是父节点的左子节点
4.3 目标结点是父节点的右子节点
删除有两个子节点的节点:
窍门:
删除有两个子节点的节点用它的中继后续来替代该节点.
中继后续:
定义:
对于每一个节点来说,比该节点关键字值次高的节点就是他的中继后续.可以简称为该节点的后继。
寻找中继后续过程:
1 找到目标的节点的的右子节点.<它的关键字的值一定比初始节点大>
2 然后转向,目标结点的右子节点的左子节点那里.
3 第二步继续类推,继续找左子节点的左子节点.这个路径上的最后一个左子节点就是初始节点的后继.
4 如果初始节点的右子节点没有左子节点.那么这个右子节点本身就是后继.
1 起始:
2 首先找到中继节点:
3 之后:处理中继节点的子节点
4 代码:
// 找到中继节点 处理中继节点子节点和新的右子节点 public Node getSuccessor(Node delNode) { //找到中继节点 //中继节点父节点 Node successorParent = delNode; //中继节点 Node successor = delNode; //当前节点 Node current = delNode.getRightNode(); while(current!=null){ successorParent = successor; successor = current; current = current.getLeftNode(); } //如果中继节点不是右子节点 if(successor!=delNode.getRightNode()){ //设置中继节点的右节点 将其交给它的父节点 successorParent.setLeftNode(successor.getRightNode()); //设置中继节点新的右子节点 successor.setRightNode(delNode.getRightNode()); } return successor; }
用中继节点替代要删除的节点:
// 如果左右子节点都存有 用中继节点替代该节点 处理中继节点和待删除节点的父节点和中继节点新的右子节点 else { //找到 中继节点 <同时右子节点已经设置完毕> Node successor = getSuccessor(current); //如果待删除节点是跟节点 if(root.compareTo(successor)==0 ){ root = successor; } //如果是左子节点 else if(isLeftChild){ parent.setLeftNode(successor); } //如果是右子节点 else{ parent.setRightNode(successor); } //设置中继节点的左子节点 <右子节点已经设置完毕> successor.setLeftNode(current.getLeftNode()); }
二叉树效率:
层数L 与节点个数N的关系:
哈夫曼huffman编码:
应用:
使用二叉树以令人惊讶的方式来压缩数据.
规则:
1 编码时候,出现次数最多的字符所占的位数应该最少.
2 每个代码都不能是其他代码的前缀.
步骤:
1 创建一段信息的频率表
SUSIE SAYS IT IS EASY
2 制定哈夫曼编码[A1]
最终哈夫曼编码表为:
2.1 首先确定了如下两个字符的编码: [A2]
S 10
空格 00
2.2 分析其他字符的哈夫曼编码
三位组合有8 <2^3>种可能性.
000 001 010 011 100 101 110 和 111
2.3 排除10和00开头的数字组合.只剩如下四种组合
010 011 110 111
2.4
011 U和换行符代码的开始
111 用于E和Y的开始
A 010
I 110
2.5
整个消息编码后为:
用哈夫曼树解码:
哈夫曼树 :
特点:
1 字符在消息中出现的频率越高,在树中的位置就越高.
2 每个圆圈外面的数字就是频率.非叶节点外面的数字就是他子节点的频率的和.
3 创建哈夫曼树
http://www.java3z.com/cwbwebhome/article/article5/51057.html
对已经生成的哈夫曼树进行编码访问的节点顺序
4 解码
从根开始,遇到0就向左走到下一个节点.遇到1就向右.
例如A字符是010.
代码:
E:\WorkspaceAlgorithm\Algorithm\review\com\cici\tree\exercise
分析代码
HuffmanTree.setHuffmanCoderString()
Exp 1 :
Exp 2:
代码结构1 :
1 接口 combinable.该接口继承comparable接口,包含combinate()
方法.用于合并两个元素
2哈夫曼树类.
该类的结构:
该类的方法
l public int compareTo(HuffmanTree<T> o) ß比较两个哈夫曼树
l private void setHuffmanCoderString() ß为每个节点设置 0 1 编码
l public void printHuffmanCoderString() ß只打印哈夫曼树的叶节点
3 生成哈夫曼树的工厂类.
4 UnitClass
自定一个用于测试的单元类.
用数组表示树
用引用的方式表达树:
数据模型
代码原型:
用数组表达树:
原理:
节点存放在数组中,而非引用相连.节点在数组中的位置,对应它在引用中的位置.
树中的每个位置,无论是否存在节点.都对应数组中的一个位置.意味着要在数组的响应位置插入一个数据项.
树中没有节点的位置在数组中的对应位置用0 或null来表示.
设节点索引值为index.则节点的左子节点是:
2*index+1
它的左子节点是:2*index+1
它的右子节点是:2*index+2
他的父节点是: (index-1)/2
数据模型:
方法总结:
//初始化 ArrayNode[] cArr 数组容器
//数组长度为 size
public ArrNodeHigherArray(int size);
//直接将参数数组赋值给ArrayNode[] cArr数组容器
public ArrNodeHigherArray(ArrayNode[] cArr);
//得到自身持有的 ArrayNode数组容器
public ArrayNode[] getCArr();
public void setNElems(int nElems);
public int getNElems();
// 得到当前数组的迭代器
public ArrNodeHigherArrayIterator getIterator();
//扩展持有的ArrayNode数组容器 容器长度扩大一倍
public void expand();
//判断ArrayNode容器是否还有ArrayNode 数据项存在
public boolean isEmpty();
//返回ArrayNode 数组的长度 : 从第一个元素到 最后一个不为空的元素
public int size();
//判断ArrayNode容器 中的数据项是否已经占满整个容器
public boolean isFull();
//线性查找 效率O[N]
//返回-1则代表没有找到目标数据项
public int find(ArrayNode tree);
}
//无序数组的插入 数组末尾插入
public void insert(ArrayNode value);
//在指定数据 node1 前 插入新数据 node2
//返回新数据项 在ArrayNode 容器中的下标
public int insertBefore(ArrayNode node1,ArrayNode node2);
//在指定数据 node1 后插入新数据 node2
//返回新数据项 在ArrayNode 容器中的下标
public int insertAfter(ArrayNode node1,ArrayNode node2);
//删除指定元素
//remove the target element and also move higher element down
public ArrayNode delete1(Character value);
// 删除指定元素 return the target element
//remove the target element and leave the position to be null
public ArrayNode delete2(Character value);
//ArrayNode容器中 数据项依次显示显示 包含数组容器中的null值
public void display();
对比普通无序数组
二叉树的数组实现中的数组允许容器中null值的存在.而普通的无序数组只能依次存入数据 .
练习:
1
将书中的tree.java.修改成用户输入字母的字符串建立的二叉树.<如A,B等等>
建立树. 每个字母在各自的叶节点中显示.父节点可以有非字母标志<’+’>.
保证每个父节点都恰好有两个字节点.树可以不是平衡树.没有快速的方法来查找节点.
最后结果可以如下:
1 的解法:
建立一个树的数组.树的节点数据域为用户输入的字母.
把每个节点作为一棵树,节点就是根.将这些只有根节点的树放到数组中.
下面建立一棵以’+’为根的树.两个单节点树为它的子节点.依次把数组中的单节点树加到这颗大树中.
...................................................... ......................................................
+
+ +
+ + + g
a b c d e f -- --
...................................................... ......................................................
2 扩展编程作业8.1中的功能来创建平衡树.
思路;
保证节点尽可能出现在最底层.
依次将数组容器中的树中的节点变成树的底层叶节点.
通过递归二分查找发实现.遍历到数组容器的每个元素.并将这些元素赋值给树的左右节点.
核心思路;
先看一小段核心核心思路的代码:
public class Test { private char[] cArr; public Test(char[] c){ cArr = c; } public static void main(String[] args) { char [] cArr ={'a','b','c','d','e','f'}; Test t = new Test(cArr); t.method(cArr,0,cArr.length-1); } public void method(char[] c,int a,int b){ //当数组被割据的只剩下一个数据项的时候 即打印出来 if(a==b){ System.out.print(c[a]+" "); } else{ //对数组的前一半不断的割据 method(c,a,(a+b)/2); //对数组的后一半不断的割据 method(c,(a+b)/2+1,b); } } } |
输出结果:
a b c d e f
执行过程:
关键递归方法分析:
主程序:
执行到createTree(Tree[] array,int left,int right);方法第11行:
最多压入三栈:
第一栈:
第二栈:
第三栈:
边界条件的栈
返回array[0];到Line 43
输出结果的分析: