20162311 实验二 树 实验报告
目录
树-1-实现二叉树
实验目的
完成教材上
LinkedBinaryTree
的实现
实验过程
首先分析
LinkedBinaryTree
这个类。它实现了BinaryTree
接口,所以要把接口里的方法全部实现。然后它还用到了BTNode<T>
这个节点类。这个节点类书上有,但是它的preorder()和postorder()方法还没实现,所以先实现这两个方法。那么怎么实现呢?其实很简单,课本上已经实现了inorder()方法,而我们知道preorder()和postorder()方法其实只是和inorder()方法的操作顺序不一样而已。所以我们照葫芦画瓢,实现preorder()和postorder()方法
//-----------------------------------------------------------------
// Performs an inorder traversal on this subtree, updating the
// specified iterator.
//-----------------------------------------------------------------
public void inorder (ArrayIterator<T> iter)
{
if (left != null)
left.inorder (iter);
iter.add (element);
if (right != null)
right.inorder (iter);
}
//-----------------------------------------------------------------
// The following methods are left as programming projects.
//-----------------------------------------------------------------
public void preorder (ArrayIterator<T> iter) {
iter.add (element);
if (left != null)
left.preorder (iter);
if (right != null)
right.preorder (iter);
}
public void postorder (ArrayIterator<T> iter) {
if (left != null)
left.postorder (iter);
if (right != null)
right.postorder (iter);
iter.add (element);
}
但是要注意preorder()和postorder()中的递归方法也要相应地改一下。
BTNode<T>
类实现好了,接下来可以实现LinkedBinaryTree
了。
- getRight()方法
参照getLeft()方法把其中的
root.getLeft
改成root.getRight
就行了
- preorder()和postorder()方法
这两和方法的实现和之前的实现类似,就不多说了,只要照着inorder()方法改一下就好
- contains()方法
这个方法的实现可以利用find()方法,如果找到了,返回true,否则返回false,代码如下
public boolean contains (T target) {
BTNode<T> node = null;
if (root != null)
node = root.find(target);
if (node == null)
return false;
else
return true;
}
- isEmpty()方法
这个只要判断size是否等于0就行了
public boolean isEmpty() {
return (size() == 0);
}
- toString()方法
我是用层序遍历来作为二叉树的toString,首先层序遍历返回一个Iterator类型的迭代器,将其转化为ArrayIterator类型,然后在调用它本身的toString方法即可
public String toString() {
String result = "";
ArrayIterator<T> iter = (ArrayIterator<T>) levelorder();
result = iter.toString();
return result;
}
- 写测试驱动类进行测试
实验代码链接
树-2-中序先序序列构造二叉树
实验目的
已知一棵树的中序和先序遍历,可以确定唯一一颗树
实验过程
大体思路是用数组存放一棵树的中序和先序遍历结果,然后先序的第一个元素为根节点,再在中序中找到根节点的位置,它左边的是左子树,右边的是右子树,然后由此可以确定那个左右子树的size,从而可以从先序数组中分离出左右子树的先序遍历,然后用递归的方法即可构造出一颗唯一的数。具体步骤可以参考《JAVA二叉树,给出先序遍历和中序遍历,构造出新的二叉树》
- 测试驱动类
实验代码链接
树-3-决策树
实验目的
利用决策树玩20问游戏
实验过程
决策树其实也就是一个二叉树,用户通过回答是或者否来得到子树,一直到最终结果,也就是叶节点。书上已经实现了相关代码,我将其修改了一下,把最后得到的结果改成了五门课,用户通过回答是或否,最终可以得到一个结果
- 测试驱动类
实验代码链接
树-4-表达式树
实验目的
使用二叉树来表示表达式,并提供方法进行计算
实验过程
首先说下我的思路。因为二叉树表示表达式要考虑到运算符的优先级,所以优先级高的要放在底层。我在写草稿的时候发现,如果一个表达式正确的放在了二叉树中的话,那么这颗二叉树的中序遍历就是中缀表达式,先序遍历就是前缀表达式,而我们之前已经实现了通过中序和先序遍历构造唯一一颗树,所以我想先求出前缀表达式。以前我们实现过中缀转后缀,但是有些忘了,于是我复习了一下,同时在网上查了一下资料,解决了中缀转前缀的问题,详情参考《Java实现由中缀表达式转为前缀表达式》。得到了中缀和前缀表达式,将其分别放入数组中,通过之前实现的方法,将其构造成一颗树。至于计算的话,我是先将二叉树进行后序遍历,得到后缀表达式,然后用14章实现的类来计算后缀表达式的结果,从而得到计算结果。
- 测试驱动类
实验代码链接
- InfixToPrefix(中缀转前缀,包含计算表达式树的方法)
- ExpressionTree
树-5-二叉查找树
实验目的
实现教材上二叉查找树的findMin()和findMax()方法
实验过程
这两个方法分别是查找最大值和最小值,而它的中序遍历正好是其中元素的升序排列,所以调用inorder()方法,返回迭代器的第一个元素为最小值,最后一个元素为最大值,具体代码如下
public T findMin() {
ArrayIterator<T> iter = (ArrayIterator<T>)inorder();
return iter.get(0);
}
public T findMax() {
ArrayIterator<T> iter = (ArrayIterator<T>)inorder();
return iter.get(iter.size()-1);
}
- 测试驱动类
实验代码链接
树-6-红黑树分析
实验目的
实验分析
在分析之前,首先让我们来了解一下红黑树。红黑树是一种一种特殊的二叉查找树,它满足二叉查找树的特征,还包括许多额外的信息。红黑树的每个节点上都有存储位表示节点的颜色,颜色是红(Red)或黑(Black)。
红黑树的特性:
- (1) 每个节点或者是黑色,或者是红色。
- (2) 根节点是黑色。
- (3) 每个叶子节点是黑色。 [注意:这里叶子节点,是指为空的叶子节点!]
- (4) 如果一个节点是红色的,则它的子节点必须是黑色的。
- (5) 从一个节点到该节点的子孙节点的所有路径上包含相同数目的黑节点。
关于它的特性,需要注意的是:
- 第一,特性(3)中的叶子节点,是只为空(NIL或null)的节点。
- 第二,特性(5),确保没有一条路径会比其他路径长出俩倍。因而,红黑树是相对是接*衡的二叉树。
具体请参考《红黑树(五)之 Java的实现》。接下来我们说说TreeMap
。TreeMap的实现是红黑树算法的实现。在查看它的源代码时我发现有一个类叫Entry<K,V>
在帮助文档中找不到,后来查资料得知它是TreeMap的内部类。
接下来具体分析一下其中的put方法
-
put()
这是帮助文档里的解释。进行put操作时,其实就是对红黑树进行添加节点的操作。而进行这个操作要考虑很多问题。 - 首先要考虑插入的新的节点是红还是黑
其次,红黑树是一棵平衡排序二叉树,普通的排序二叉树可能会出现失衡的情况所以要用到它的fixAfterInsertion方法,其中涉及到左旋,右旋,着色三个基本操作
以下是fixAfterInsertion()方法的源代码
private void fixAfterInsertion(Entry<K,V> x) {
x.color = RED;
while (x != null && x != root && x.parent.color == RED) {
if (parentOf(x) == leftOf(parentOf(parentOf(x)))) {
Entry<K,V> y = rightOf(parentOf(parentOf(x)));
if (colorOf(y) == RED) {
setColor(parentOf(x), BLACK);
setColor(y, BLACK);
setColor(parentOf(parentOf(x)), RED);
x = parentOf(parentOf(x));
} else {
if (x == rightOf(parentOf(x))) {
x = parentOf(x);
rotateLeft(x);
}
setColor(parentOf(x), BLACK);
setColor(parentOf(parentOf(x)), RED);
rotateRight(parentOf(parentOf(x)));
}
} else {
Entry<K,V> y = leftOf(parentOf(parentOf(x)));
if (colorOf(y) == RED) {
setColor(parentOf(x), BLACK);
setColor(y, BLACK);
setColor(parentOf(parentOf(x)), RED);
x = parentOf(parentOf(x));
} else {
if (x == leftOf(parentOf(x))) {
x = parentOf(x);
rotateRight(x);
}
setColor(parentOf(x), BLACK);
setColor(parentOf(parentOf(x)), RED);
rotateLeft(parentOf(parentOf(x)));
}
}
}
root.color = BLACK;
}
左旋:rotateLeft()
所谓左旋转,就是将新增节点(N)当做其父节点(P),将其父节点P当做新增节点(N)的左子节点。即:G.left —> N ,N.left —> P。
private void rotateLeft(Entry<K,V> p) {
if (p != null) {
Entry<K,V> r = p.right;
p.right = r.left;
if (r.left != null)
r.left.parent = p;
r.parent = p.parent;
if (p.parent == null)
root = r;
else if (p.parent.left == p)
p.parent.left = r;
else
p.parent.right = r;
r.left = p;
p.parent = r;
}
}
所谓右旋转即,P.right —> G、G.parent —> P。
(图片来自:http://www.cnblogs.com/yangecnu/p/Introduce-Red-Black-Tree.html)
着色
着色就是改变该节点的颜色,在红黑树中,它是依靠节点的颜色来维持平衡的。
private static <K,V> void setColor(Entry<K,V> p, boolean c) {
if (p != null)
p.color = c;
}
这些代码的理解我也是借助了参考资料(《根据红黑树的算法来分析TreeMap的实现》)才完成的,自己理解的话真的很难。我只是选了一个put方法来分析,算是一个很基本的方法,但它的实现并不简单。
以下是put方法的源代码
public V put(K key, V value) {
Entry<K,V> t = root;
if (t == null) {
compare(key, key); // type (and possibly null) check
root = new Entry<>(key, value, null);
size = 1;
modCount++;
return null;
}
int cmp;
Entry<K,V> parent;
// split comparator and comparable paths
Comparator<? super K> cpr = comparator;
if (cpr != null) {
do {
parent = t;
cmp = cpr.compare(key, t.key);
if (cmp < 0)
t = t.left;
else if (cmp > 0)
t = t.right;
else
return t.setValue(value);
} while (t != null);
}
else {
if (key == null)
throw new NullPointerException();
@SuppressWarnings("unchecked")
Comparable<? super K> k = (Comparable<? super K>) key;
do {
parent = t;
cmp = k.compareTo(t.key);
if (cmp < 0)
t = t.left;
else if (cmp > 0)
t = t.right;
else
return t.setValue(value);
} while (t != null);
}
Entry<K,V> e = new Entry<>(key, value, parent);
if (cmp < 0)
parent.left = e;
else
parent.right = e;
fixAfterInsertion(e);
size++;
modCount++;
return null;
}
至于HashMap,还是拿put方法来说。
在HashMap中有两个很重要的参数,容量(Capacity)和负载因子(Load factor)
put函数大致的思路为:
对key的hashCode()做hash,然后再计算index;
- 如果没碰撞直接放到bucket里;
- 如果碰撞了,以链表的形式存在buckets后;
- 如果碰撞导致链表过长(大于等于TREEIFY_THRESHOLD),就把链表转换成红黑树;
- 如果节点已经存在就替换old value(保证key的唯一性)
- 如果bucket满了(超过load factor*current capacity),就要resize。
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
以上这些也是参考了资料的。(《Java HashMap工作原理及实现
》),虽然分析了一些,但还是有很多不懂的地方,需要自己多花时间。我发现大部分方法都不是单独存在的,当你想查资料时,你会发现有很多相关的内容,这时就会觉得自己知道的太少了,还需要不停地学习。
遇到的问题和解决办法
- 问题1:完成第二个实验的代码后,我尝试运行,发现出现了空指针异常,如下图
根据它的提示我找到错误的代码行,原来是因为调用LinkedBinaryTree的第三个构造方法,不能给左右子树赋值为空,而我在代码里有一个赋值为null的操作
- 问题1解决办
为了避免这个异常,我将其改成了
binaryTree = new LinkedBinaryTree<>()
,这样它的值既是空,又避免了异常。
问题2:解决问题1后,我再此运行,又出现了数组越界异常
问题2解决办法
我在它提示有问题的代码处设置断点,进行单步跟踪。
到后面发现inorder数组有元素是null,而再下一步就出现问题,再下一步就出来了异常
到这里我怀疑是数组赋值出现了问题,于是找到测试驱动类里的数组赋值的代码,果然有问题,数组的最后一个元素没赋值,只要把最后的12改成13就行了
问题3:做第四个实验的时候,运行时出现了NumberFormate异常
问题3解决方案
它给的提示是后缀表达式的计算有问题,这时我突然想到,PostfixEvaluator类是用StringTokenizer类将传入的字符串(即后缀表达式) 以空格为标志分开成一个个字符的,而我用后序遍历得到的后缀表达式是没有空格的,所以我在生成后缀表达式的时候给它加上了空格
public static int calTree(LinkedBinaryTree<String> expression){
PostfixEvaluator evaluator = new PostfixEvaluator();
int result;
ArrayIterator<String> post = (ArrayIterator<String>) expression.postorder();
String postfix = "";
for (int i=0;i<post.size();i++){
postfix += post.get(i)+" ";
}
result = evaluator.evaluate(postfix);
return result;
}
这样问题就解决了
问题4:做第5个实验时,实现findMin和findMax方法,我想把它中序遍历得到的结果转化为数组,获取数组的第0个和最后一个元素即为最小和最大值,但是出现以下异常
问题4解决方案
既然无法转化,我就不转化成数组了,因为ArrayIterator继承自ArrayList类,它有一个get方法,所以我干脆直接调用get方法得到最小和最大值
public T findMin() {
ArrayIterator<T> iter = (ArrayIterator<T>)inorder();
return iter.get(0);
}
public T findMax() {
ArrayIterator<T> iter = (ArrayIterator<T>)inorder();
return iter.get(iter.size()-1);
}
总结
这周的实验主要是围绕树来进行的。通过实现树,二叉树,二叉查找树,决策树等来让我们更深入地理解树这种数据结构。总体来说本次实验做的比较顺利,对于树的理解也更加透彻了,但是在看了TreeMap和HashMap的源码之后就觉得自理解的还是太少了,还需要加强学习,对于java中的源代码,还要多看,多学习,要慢慢地能看懂,然后化为己用。