20172301 《Java软件结构与数据结构》实验二报告
课程:《Java软件结构与数据结构》
班级: 1723
姓名: 郭恺
学号:20172301
实验教师:王志强老师
实验日期:2018年11月20日
必修/选修: 必修
一.实验内容
实验1
实验2
实验3
实验4
实验5
实验6
二.实验过程及结果
实验1
LinkedBinaryTree
因为是之前的程序项目,所以实现起来很容易。
getRight()
方法,首先在LinkedBinaryTree
类里面声明一个全局变量
protected LinkedBinaryTree<T> left,right;
然后在构造函数里,添加下面两行代码。
// 创建以指定元素为根元素的二叉树,把树作为它的左子树和右子树
public LinkedBinaryTree(T element, LinkedBinaryTree<T> left,
LinkedBinaryTree<T> right)
{
root = new BinaryTreeNode<T>(element);
root.setLeft(left.root);
root.setRight(right.root);
this.left = left;
this.right = right;
}
然后直接返回right
即可。
// 返回此树的根的右子树。
public LinkedBinaryTree<T> getRight()
{
return right;
}
contains
方法基于私有方法findAgain
实现。只需要判断在树里能否找到目标元素即可。
public boolean contains(T targetElement)
{
return findAgain(targetElement, root) != null;
}
toString
方法,这里为了让输出是一个树型,我用了之前ExpressionTree
的printTree
方法。同样,toString
方法可以考虑使用四种遍历方式。preorder
方法和postorder
方法,实现理念和书上给的中序遍历一样。只需要调整一下左右孩子还有结点的顺序。
// 执行递归先序遍历。
protected void preOrder(BinaryTreeNode<T> node, ArrayUnorderedList<T> tempList)
{
if (node != null)
{
tempList.addToRear(node.getElement());
preOrder(node.getLeft(), tempList);
preOrder(node.getRight(), tempList);
}
}
// 执行递归后序遍历。
protected void postOrder(BinaryTreeNode<T> node,
ArrayUnorderedList<T> tempList)
{
if (node != null)
{
postOrder(node.getLeft(), tempList);
postOrder(node.getRight(), tempList);
tempList.addToRear(node.getElement());
}
}
- 接下来是测试类和测试结果
实验2
- 根据之前上课讲的方法,我们首先要确定如何推断出根和根的左右孩子。例如题目中的两个字符串,中序
HDIBEMJNAFCKGL
和先序ABDHIEJMNCFGKL
。
中序分左右,先后序定根。
这里,我一开始是没有什么思路的,因为我自己推导先序和中序构造树的时候,并没有系统的方法。所以我也在网上找到了一些资料,如何有步骤的去分析先序和中序。 - 会分为以下几个步骤:
- 确定整棵二叉树的根节点即先序遍历中的第一个元素root
- 确定root在中序遍历元素的位置,root左边的元素为二叉树的左子树元素Lchild,右边为右子树元素Rchild
- 在先序遍历中找到最先出现Lchild中元素的那个元素,为Lchild的根节点——root的左孩子节点,同理找出Rchild的根节点——root的右孩子节点
- 重复2,3步骤直至二叉树构建完成;
- 那么,我们沿着这个思路,就能根据先序得知
A
是树的根结点。那么根据中序得知A
的左边是左子树,A
的右边是右子树。 - 同理,根据先序
B
即是左子树的根,根据中序得知B
的左边是左子树的左孩子,B
的右边是左子树的右孩子。 - 依次往下,知道左右孩子为空的时候。这时候即可返回以
A
为根的树。 - 所以,运用递归,分别递归实现左子树和右子树。
- 最后,以
A
为根结点和递归实现的左右子树,创建一个新树。 - 接下来是测试代码和代码结果。
实验3
- 决策树相对来说比较简单,因为书上的背部疼痛诊断器给出了一个决策树的流程。我们只需要设计一棵新的决策树即可。
- 文件里包含着一些数字,是因为第一个数字是表明你有几个语句,然后下面的是为了构建左右子树。
- 测试代码和结果
实验4
- 实验四是输入中缀表达式,使用树将中缀表达式转换为后缀表达式,并输出后缀表达式和计算结果。书上有一个
ExpresstionTree
是关于后缀表达式计算的一个树。 - 这个我一开始也没有什么思路,但是实际上也就是一个表达式树,符号作为根,然后操作数是他的左右子树。 我参考书上后缀计算的例子,运用一个表达式式栈,注意:这个栈里面存放的是树型的。为了实现的方便,我直接存放的是二叉树,并没有用表达式树。然后还有一个符号栈。这里还需要考虑一个优先级的问题,** 优先级高的要在底层。**
- 具体的实现思路和判断条件和上学期的差不多,我并没有改动多少,主要注意的就是树的存放和符号左右两个操作数的顺序。我便不多赘述。括号问题将在问题中详细说明。
- 代码测试和结果
实验5
- 实验五是之前的课后项目,已经实现过。二叉查找树的特点是,左子树的最左结点是最小值,树的右子树的最右结点是最大值,。所以,实现
findMin
和findMax
只要分别查找最左结点和最右结点即可。 - 测试代码和结果
实验6
在看源代码之前,我觉得有必要学习一下如何去系统的看程序源代码。这里做一些摘录:
第一,找准入口出口,不要直接跳进去看,任何代码都有触发点,无论是http request,还是服务器自动启动,还是main函数,还是其他的,先从入口开始。
第二,手边一支笔一张纸,除非你是Jeff,否则你不会记得那么多跳转的。一个跳转就写下来函数/方法名和参数,读完一遍,就有了一个sequence diagram雏形 。
第三,私有方法掠过,只要记住输入输出即可,无需看具体实现
-
红黑树遵循以下五点性质:
性质1 结点是红色或黑色。
性质2 根结点是黑色。
性质3 每个叶子结点(NIL结点,空结点)是黑色的。
性质4 每个红色结点的两个子结点都是黑色。(从每个叶子到根的所有路径上不能有两个连续的红色结点)
性质5 从任一结点到其每个叶子结点的所有路径都包含相同数目的黑色结点。 -
TreeMap
- 首先看一下
TreeMap
声明
public class TreeMap<K,V> extends AbstractMap<K,V> implements NavigableMap<K,V>, Cloneable, java.io.Serializable {
- 构造方法
public TreeMap() { comparator = null; }
无参构造方法,不指定比较器,排序的实现要依赖
key.compareTo()
方法,因此key
必须实现Comparable
接口,并覆写其中的compareTo
方法。public TreeMap(Comparator<? super K> comparator) { this.comparator = comparator; }
采用带比较器的构造方法,排序依赖该比较器,
key
可以不用实现Comparable
接口。public TreeMap(Map<? extends K, ? extends V> m) { comparator = null; putAll(m); }
构造方法同样不指定比较器,调用
putAll
方法将Map
中的所有元素加入到TreeMap
中。public TreeMap(SortedMap<K, ? extends V> m) { comparator = m.comparator(); try { buildFromSorted(m.size(), m.entrySet().iterator(), null, null); } catch (java.io.IOException | ClassNotFoundException cannotHappen) { } }
将比较器指定为m的比较器,而后调用buildFromSorted方法,将SortedMap中的元素插入到TreeMap中,根据SortedMap创建的TreeMap,将SortedMap中对应的元素添加到TreeMap中。
- 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; } //如果Map不为空,找到插入新节点的父节点 int cmp; Entry<K,V> parent; Comparator<? super K> cpr = comparator; // 如果比较器不为空 if (cpr != null) { do { // 使用 parent 上次循环后的 t 所引用的 Entry parent = t; // 拿新插入的key和t的key进行比较 cmp = cpr.compare(key, t.key); // 如果新插入的key小于t的key,t等于t的左结点 if (cmp < 0) t = t.left; // 如果新插入的key大于t的key,t等于t的右结点 else if (cmp > 0) t = t.right; else // 如果两个key相等,新value覆盖原有的value,并返回原有的value 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); } // 将新插入的结点作为parent结点的子结点 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
- 首先看一下
HashMap
类的声明,可以了解他继承了什么类和实现了哪些接口。
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable {
<K,V>应该表示的是一种映射关系。
HashMap 的实例有两个参数影响其性能:初始容量 和加载因子。
容量是哈希表中桶的数量,初始容量只是哈希表在创建时的容量。
加载因子是哈希表在其容量自动增加之前可以达到多满的一种尺度。当哈希表中的条目数超出了加载因子与当前容量的乘积时,则要对该哈希表进行 rehash 操作(即重建内部数据结构),从而哈希表将具有大约两倍的桶数。- 这里有两个比较重要的变量,容量和加载因子。容量的值是2的n次幂,加载因子默认为0.75。
这里加载因子为什么默认是0.75呢?
通常,默认加载因子 (0.75) 在时间和空间成本上寻求一种折衷。加载因子过高虽然减少了空间开销,但同时也增加了查询成本(在大多数 HashMap 类的操作中,包括 get 和 put 操作,都反映了这一点)。在设置初始容量时应该考虑到映射中所需的条目数及其加载因子,以便最大限度地减少 rehash 操作次数。
如果初始容量大于最大条目数除以加载因子,则不会发生 rehash 操作。
这里的rehash操作,我觉得类似于数组的扩容。加载因子就是表示数组链表填满的程度。
加载因子越大,填满的元素越多,空间利用率高了,但冲突的机会加大了.链表长度会越来越长,查找效率降低。
加载因子越小,填满的元素越少,冲突的机会减小了,但空间浪费多了.表中的数据将过于稀疏,很多空间还没用,就开始扩容了。因此,必须在 "冲突的机会"与"空间利用率"之间寻找一种平衡与折衷.
如果机器内存足够,并且想要提高查询速度的话可以将加载因子设置小一点;相反如果机器内存紧张,并且对查询速度没有什么要求的话可以将加载因子设置大一点。不过一般我们都不用去设置它,让它取默认值0.75就好了。
- 构造函数:
// 初始容量(必须是2的n次幂),负载因子 public HashMap(int initialCapacity, float loadFactor) { if (initialCapacity < 0) throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity); if (initialCapacity > MAXIMUM_CAPACITY) initialCapacity = MAXIMUM_CAPACITY; if (loadFactor <= 0 || Float.isNaN(loadFactor)) throw new IllegalArgumentException("Illegal load factor: " + loadFactor); this.loadFactor = loadFactor; this.threshold = tableSizeFor(initialCapacity); }
- 我发现其中还有一个指针类,
HashMap
使用了数组,链表和红黑树,多种实现。
static class Node<K,V> implements Map.Entry<K,V> { final int hash; final K key; V value; Node<K,V> next; Node(int hash, K key, V value, Node<K,V> next) { this.hash = hash; // 哈希值 this.key = key; // 键 this.value = value; // 值 this.next = next; // 下一个 } public final K getKey() { return key; } public final V getValue() { return value; } public final String toString() { return key + "=" + value; } public final int hashCode() { return Objects.hashCode(key) ^ Objects.hashCode(value); } public final V setValue(V newValue) { V oldValue = value; value = newValue; return oldValue; } // 判断两个Node是否相等 // 若两个Node的“key”和“value”都相等,则返回true。 // 否则,返回false public final boolean equals(Object o) { if (o == this) return true; if (o instanceof Map.Entry) { Map.Entry<?,?> e = (Map.Entry<?,?>)o; if (Objects.equals(key, e.getKey()) && Objects.equals(value, e.getValue())) return true; } return false; } }
next
,是用来处理冲突的。HashMap
本来就是一个数组,如果数组中某一个索引发生了冲突,那么就会形成链表。而链表到一定程度的时候,就会形成红黑树。put
操作
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) // resize()方法是重新调整HashMap的大小 n = (tab = resize()).length; // 若不为null,计算该key的哈希值,然后将其添加到该哈希值对应的链表中 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; }
- 尽管明白一些基本操作,但还是没有理解大多数代码。
- 首先看一下
三. 实验过程中遇到的问题和解决过程
-
问题1:实验二基于(中序,先序)序列构造唯一一棵二㕚树的功能,出现了
ArrayIndexOutOfBoundsException
异常,简单来说就是数组为空。
如图
-
问题1解决方案:
-
首先,找到问题出现的行数,然后设置断点进行调试。
-
根据我以上实验二的思路,我根据先序判断出来的根结点,把中序分成了两个数组,分别是中序左子树和中序右子树,同理,把先序也分成两个数组,分别是先序左子树和先序右子树。
-
根据调试发现,在新建了子树的中序和先序数组之后,我并没有把原来数组的值导入到新的数组当中。所以导致运行时抛出了数组空的异常。
如图
-
那我只需要使用一个循环遍历原数组的操作,把元素赋值进新的数组。即,以根结点为分界线,分别存入左右子树当中。小于根结点对应索引的元素全在左子树,大于根结点的全在右子树。
for (int y = 0; y < in.length; y++) { // 把原数组的数存入新的数组当中 if (y < x) { inLeft[y] = in[y]; preLeft[y] = pre[y + 1]; } else if (y > x) { inRight[y - x - 1] = in[y]; preRight[y - x - 1] = pre[y]; } }
-
-
问题2:实验四树的输出格式不对,从而导致后缀表达式输出格式不正确。
如图
-
问题2解决方案:
-
实现二叉树中缀转后缀之后,却发现后序遍历输出树的时候出来的后缀表达式格式不对。
-
同样是经过调试发现,在符号入栈的时候,转化为
LinkedBinaryTree
类型存储的时候,并以LinkedBinaryTree
类型为根结点的时候,会根据其toString
方法,变成,+
,所以输出树的时候前面会多出很多行。
如图
-
我的解决办法是,把操作数栈改成
String
类型的,以String
类型的符号作为根结点,这样,结果可以正常输出。
如图
-
-
问题3: 实验四中缀转后缀的括号问题。
-
问题3解决方案:
- 一开始我的括号问题想得很复杂,后来谭鑫同学告诉我,括号里其实也是一个表达式,只要递归调用一下就可以了。确实是,如果把括号里面的看成一个表达式,那么就和正常的没有括号的是一样的啦。
- 但是后来我还是完成了自己写的非递归实现,尽管代码可能有些复杂,但是条例还是清晰的,并且代码判断条件也是不缺少的。
// 处理括号 if (tempToken.equals("(")) { op.push(tempToken); tempToken = stringTokenizer.nextToken(); while (!tempToken.equals(")")) { if (tempToken.equals("+") || tempToken.equals("-")) { if (!op.isEmpty()) { // 栈不空,判断“(” if (op.peek().equals("(")) op.push(tempToken); else { String b = op.pop(); operand1 = getOperand(expre); operand2 = getOperand(expre); LinkedBinaryTree a = new LinkedBinaryTree(b, operand2, operand1); expre.push(a); op.push(tempToken); } } else { // 栈为空,运算符入栈 op.push(tempToken); } } else if (tempToken.equals("*") || tempToken.equals("/")) { if (!op.isEmpty()) { if (op.peek().equals("*") || op.peek().equals("/")) { String b = op.pop(); operand1 = getOperand(expre); operand2 = getOperand(expre); LinkedBinaryTree a = new LinkedBinaryTree(b, operand2, operand1); expre.push(a); op.push(tempToken); } else { op.push(tempToken); } } } else { // 操作数入栈 LinkedBinaryTree a = new LinkedBinaryTree(tempToken); expre.push(a); } tempToken = stringTokenizer.nextToken(); } while (true) { String b = op.pop(); if (!b.equals("(")) { operand1 = getOperand(expre); operand2 = getOperand(expre); LinkedBinaryTree a = new LinkedBinaryTree(b, operand2, operand1); expre.push(a); } else { // 终止循环 break; } } }
- 这里我用了两个while循环,
第一个while循环 ,就是分别把符号入到符号栈里,把表达树入到表达式栈里。如果压入栈的操作符优先级大于等于栈顶的符号,那么就会弹出栈顶的符号,并且以弹出的符号为根和表达式树弹出的两个树,形成一个新的树,再存放到表达式栈里。
第二个while循环 ,是把栈里面括号里面的树和操作符全部弹出,此时栈里面剩下的应该都是优先级相等的,所以我们只需要把括号里的形成一棵树就可以了。 - 总体来说确实相对于递归来说较为复杂,以后还是应该考虑代码的优化和完整性,不能以实现目的为目的。
其他(感悟、思考等)
- 这周实验花费了较多的时间。虽然说有些项目是之前的作业并且完成了实现。但是对于实验二和实验四没有很清晰系统的思路,导致花费了太多时间去设计。还是强调代码的全局性和前瞻性。比如说实验四,括号的实现如果用递归来理解的话确实有事半功倍的效果。可是我却把自己的思维局限在了多个判断条件上面。尽管实现了,但是从代码复杂性来说是不够完美的。同样的,对于HashMap和TreeMap也只是了解到了一些皮毛,但是,我们可以从其源代码上发现自己设计实现代码时所缺少的东西,才能有所收获。代码可能相似,但思想不会。