20172305 2017-2018-2 《程序设计与数据结构》实验二报告
20172305 2017-2018-2 《程序设计与数据结构》实验报告
课程:《程序设计与数据结构》
班级: 1723
姓名: 谭鑫
学号:20172305
实验教师:王志强
实验日期:2018年10月13日
必修/选修: 必修
实验内容
-
实验二-1-实现二叉树:
- (1)参考教材p212,完成链树LinkedBinaryTree的实现(getRight,contains,toString,preorder,postorder)
- (2)用JUnit或自己编写驱动类对自己实现的LinkedBinaryTree进行测试
-
实验二-2-中序先序序列构造二叉树:
- (1)基于LinkedBinaryTree,实现基于(中序,先序)序列构造唯一一棵二㕚树的功能,比如给出中序HDIBEMJNAFCKGL和后序ABDHIEJMNCFGKL,构造出树
- (2)用JUnit或自己编写驱动类对自己实现的功能进行测试
-
实验二-3-决策树:
- (1)设计并实现一棵决策树
- (2)用JUnit或自己编写驱动类对自己实现的功能进行测试
-
实验二-4-表达式树:
- (1)输入中缀表达式,使用树将中缀表达式转换为后缀表达式,并输出后缀表达式和计算结果
- (2)用JUnit或自己编写驱动类对自己实现的功能进行测试
-
实验二-5-二叉查找树:
- (1)完成PP11.3
- (2)用JUnit或自己编写驱动类对自己实现的功能进行测试
-
实验二-6-红黑树分析:
- (1)参考文件对Java中的红黑树(TreeMap,HashMap)进行源码分析,并在实验报告中体现分析结果。(C:\Program Files\Java\jdk-11.0.1\lib\src\java.base\java\util)
实验过程及结果
-
实验二-1-实现二叉树:就是对二叉树的测试,二叉树不像二叉查找树一样可以进行添加删除。所以,在构造一个二叉树的时候需要不断的new一下,把每一个结点进行拼接才能构造一棵树。getRight,contains,toString,preorder,postorder。
- getRight和getLeft,是返回某结点的左侧和右侧的操作
- contains,是判定指定目标是否在该树中的操作
- toString,是将树进行输出
- preorder,是对树进行前序遍历
- postorder,是对树进行后序遍历
- 具体分析在第六周博客
-
实现的操作相对简单,只是遍历的时候发现迭代器运用起来感觉很麻烦,所以就根据遍历思想创建了四个简单点的遍历方法。
-
实验二-2-中序先序序列构造二叉树:根据中序和先序的内容来判断树内元素的具体位置,通过先序来判断根结点,再在中序来判断根结点的左子树和右子树;在将两个部分继续在先序中查询根结点,在中序中判断结点的左右子树,就会判断出树中各个结点的位置。
-
实验二-3-决策树:实现一棵决策树,这个实验就是仿照书上第十章的背部疼痛诊断器来书写的。我再此基础上并没有进行大的修改,只是仿照内容修改了读取的文件内容。我所设计的决策树内容如图:
-
实验二-4-表达式树:通过输入的中缀表达式利用树来实现中缀转后缀,并由二叉树转换成后缀表达式并输出。
在实现后缀的过程中,我们可以调用之前的书上代码来实现后缀表达式的转换成数字。 -
实验二-5-二叉查找树:就是对二叉查找树的测试,二叉查找树会优于二叉树可以进行添加删除。所以,在构造一个二叉查找树的时候,把每一个结点内的内容加进去就可以。
-
实验二-6-红黑树:红黑树的问题是最麻烦的,根据每一个树的结点内容实现红黑的添加,但是红黑的添加会出现碰撞,这就需要我们的具体分析过程,而TreeMap和TreeSet方法都是依靠红黑树来实现的。
红黑树的性质:
- (1)每个结点或者是黑色,或者是红色
- (2)根结点是黑色
- (3)每个叶结点是黑色(是指为空的叶结点)
- (4)如果一个结点是红色的,则它的子节点必须是黑色的
- (5)从一个结点到该结点的子孙结点的所有路径上包含相同数目的黑结点
- TreeMap和TreeSet是API的两个重要成员,其中 TreeMap是Map接口的常用实现类,而TreeSet是Set接口的常用实现类。虽然TreeMap和TreeSet实现的接口规范不同,TreeMap实现的接口是Map,TreeSet实现的接口是Set,但TreeSet底层是通过TreeMap来实现的,因此二者的实现方式完全一样。而TreeMap底层的实现就是用的红黑树数据结构来实现的。而HashMap基于哈希表的Map接口的实现。此实现提供所有可选的映射操作,并允许使用 null 值和 null 键。(除了非同步和允许使用 null 之外,HashMap 类与 Hashtable 大致相同。)此类不保证映射的顺序,特别是它不保证该顺序恒久不变。 此实现假定哈希函数将元素适当地分布在各桶之间,可为基本操作(get 和 put)提供稳定的性能。
- TreeMap是一个有序的key-value集合,它是通过红黑树实现的。
- TreeMap继承于AbstractMap,所以它是一个Map,即一个key-value集合。
- TreeMap实现了NavigableMap接口,意味着它支持一系列的导航方法。比如返回有序的key集合。
- TreeMap实现了Cloneable接口,意味着它能被克隆。
- TreeMap实现了java.io.Serializable接口,意味着它支持序列化。
- TreeMap的简单测试:
- HashMap底层实现是链表数组并且实现了Map全部的方法。
- HashMap的key用Set存放,所以想做到key不允许重复,key对应的类需要重写hashCode和equals方法。
- HashMap允许空键和空值,元素是无序的,而且顺序会不定时改变。
- HashMap插入、获取的时间复杂度基本是 O(1),遍历整个 Map 需要的时间与 桶的长度成正比。
- HashMap两个关键因子:初始容量、加载因子
- HashMap的简单测试:
-
关于Map接口:Map接口代表由关键字以及它们的值组成的一些项的集合。关键字必须是唯一的,但是若干关键字的可以映射到一些相同的值。因此,值不是唯一的。在SortedMap接口中,映射中的关键字保持逻辑上有序状态。SortedMap接口的一种实现是TreeMap。在数组中我们是通过数组下标来对其内容索引的,而在Map中我们通过对象来对对象进行索引,用来索引的对象叫做key,其对应的对象叫做value。这就是我们平时说的键值对。
// 默认构造函数。使用该构造函数,TreeMap中的元素按照自然排序进行排列。
TreeMap()
// 创建的TreeMap包含Map
TreeMap(Map<? extends K, ? extends V> copyFrom)
// 指定Tree的比较器
TreeMap(Comparator<? super K> comparator)
// 创建的TreeSet包含copyFrom
TreeMap(SortedMap<K, ? extends V> copyFrom)
//构建一个具有默认初始容量 (16) 和默认加载因子 (0.75) 的空哈希映像
HashMap()
//构建一个哈希映像,并且添加映像m的所有映射
HashMap(Map<? extends K,? extends V> m)
//构建一个拥有特定容量的空的哈希映像
HashMap(int initialCapacity)
//构建一个拥有特定容量和加载因子的空的哈希映像
HashMap(int initialCapacity, float loadFactor)
- TreeMap的Empty方法,firstEntry()和getFirstEntry()都是用于获取第一个节点。但是,firstEntry() 是对外接口;getFirstEntry()是内部接口。而且,firstEntry()是通过getFirstEntry()来实现的。两个方法并会显得很麻烦,因为通过firstEntry()方法和可以避免修改返回的Entry,这样保证了完整性,而且产生了两个方法,一个可以是避免修改的方法,一个可以是修改的方法。通过查阅资料可以总结出对firstEntry()返回的Entry对象只能进行getKey()、getValue()等读取操作;而对getFirstEntry()返回的对象除了可以进行读取操作之后,还可以通过setValue()修改值。
public Map.Entry<K,V> firstEntry() {
return exportEntry(getFirstEntry());
}
final Entry<K,V> getFirstEntry() {
Entry<K,V> p = root;
if (p != null)
while (p.left != null)
p = p.left;
return p;
}
- TreeMap的Key方法,返回大于/等于key的最小的键值对所对应的KEY,没有的话返回null。
public K ceilingKey(K key) {
return keyOrNull(getCeilingEntry(key));
}
- TreeMap的values方法,values方法是通过new Values()来实现返回TreeMap中值的集合,而Values()正好是集合类Value的构造函数,这样返回的是一个集合了。
public Collection<V> values() {
Collection<V> vs = values;
return (vs != null) ? vs : (values = new Values());
}
- TreeMap的entrySet方法,entrySet方法是返回TreeMap的所有键值对组成的集合,而且它单位是单个键值对。
public Set<Map.Entry<K,V>> entrySet() {
EntrySet es = entrySet;
return (es != null) ? es : (entrySet = new EntrySet());
}
- TreeMap还有两个遍历方法,顺序遍历和逆序遍历,顺序遍历,就是从第一个元素开始,逐个向后遍历;而倒序遍历则恰恰相反,它是从最后一个元素开始,逐个往前遍历。
- TreeMap遍历键值对的方法,先根据entrySet()获取TreeMap的“键值对”的Set集合,再通过迭代器遍历得到的集合。
Integer integ = null;
Iterator iter = map.entrySet().iterator();
while(iter.hasNext()) {
Map.Entry entry = (Map.Entry)iter.next();
key = (String)entry.getKey();
integ = (Integer)entry.getValue();
}
- TreeMap遍历键的方法,先keySet()获取TreeMap的键的Set集合,再通过迭代器遍历得到的集合。
String key = null;
Integer integer = null;
Iterator iter = map.keySet().iterator();
while (iter.hasNext()) {
key = (String)iter.next();
integer = (Integer)map.get(key);
}
- HashMap的主干是一个Entry数组。Entry是HashMap的基本组成单元,每一个Entry包含一个key-value键值对。HashMap由数组+链表组成的,数组是HashMap的主体,链表则是主要为了解决哈希冲突而存在的,如果定位到的数组位置不含链表(当前entry的next指向null),那么对于查找,添加等操作很快,仅需一次寻址即可;如果定位到的数组包含链表,对于添加操作,其时间复杂度为O(n),首先遍历链表,存在即覆盖,否则新增;对于查找操作来讲,仍需遍历链表,然后通过key对象的equals方法逐一比对查找。所以,性能考虑,HashMap中的链表出现越少,性能才会越好。
- HashMap的put方法,是向HashMap中添加一个键值对。
public V put(K key, V value) {
//如果table数组为空数组{},进行数组填充(为table分配实际内存空间),入参为threshold,此时threshold为initialCapacity 默认是1<<4(24=16)
if (table == EMPTY_TABLE) {
inflateTable(threshold);
}
//如果key为null,存储位置为table[0]或table[0]的冲突链上
if (key == null)
return putForNullKey(value);
int hash = hash(key);//对key的hashcode进一步计算,确保散列均匀
int i = indexFor(hash, table.length);//获取在table中的实际位置
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
//如果该对应数据已存在,执行覆盖操作。用新value替换旧value,并返回旧value
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;//保证并发访问时,若HashMap内部结构发生变化,快速响应失败
addEntry(hash, key, value, i);//新增一个entry
return null;
}
private void inflateTable(int toSize) {
int capacity = roundUpToPowerOf2(toSize);//capacity一定是2的次幂
threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);//此处为threshold赋值,取capacity*loadFactor和MAXIMUM_CAPACITY+1的最小值,capaticy一定不会超过MAXIMUM_CAPACITY,除非loadFactor大于1
table = new Entry[capacity];
initHashSeedAsNeeded(capacity);
}
- HashMap的get方法,是向HashMap中通过查键来找寻对应的值。
public V get(Object key) {
Node<K,V> e;
//还是先计算 哈希值
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
//tab 指向哈希表,n 为哈希表的长度,first 为 (n - 1) & hash 位置处的桶中的头一个节点
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
//如果桶里第一个元素就相等,直接返回
if (first.hash == hash &&
((k = first.key) == key || (key != null && key.equals(k))))
return first;
//否则就得慢慢遍历找
if ((e = first.next) != null) {
if (first instanceof TreeNode)
//如果是树形节点,就调用树形节点的 get 方法
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
do {
//do-while 遍历链表的所有节点
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}
- HashMap的remove方法,是根据key删除该key对应的键值对,该方法将会根据查找到匹配的键值对,将其从HashMap中删除,并且返回键值对的值。
public V remove(Object key) {
Node<K,V> e; // 定义一个节点变量,用来存储要被删除的节点(键值对)
return (e = removeNode(hash(key), key, null, false, true)) == null ?
null : e.value; // 调用removeNode方法
}
final Node<K,V> removeNode(int hash, Object key, Object value,boolean matchValue, boolean movable) {
Node<K,V>[] tab; Node<K,V> p; int n, index; // 声明节点数组、当前节点、数组长度、索引值
if ((tab = table) != null && (n = tab.length) > 0 && (p = tab[index = (n - 1) & hash]) != null) {
Node<K,V> node = null, e; K k; V v;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
node = p;
else if ((e = p.next) != null) {
if (p instanceof TreeNode)
node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
else {
do {
if (e.hash == hash &&
((k = e.key) == key ||
(key != null && key.equals(k)))) {
node = e;
break;
}
p = e;
} while ((e = e.next) != null);
}
}
if (node != null && (!matchValue || (v = node.value) == value || (value != null && value.equals(v)))) {
if (node instanceof TreeNode)
((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
else if (node == p)
tab[index] = node.next;
else
p.next = node.next;
modCount++;
size--;
afterNodeRemoval(node);
return node;
}
}
return null;
}
- HashMap的hash方法,通过将传入键的hashCode进行无符号右移 16 位,然后进行按位异或,得到这个键的哈希值。
public static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
实验过程中遇到的问题和解决过程
-
问题1:如何利用先序和后序转换成整棵树?
-
问题1的解决方案:根据先序来查找根结点的位置,再在中序查找到根结点,通过根结点的位置分出左子树和右子树两个部分。将每一部分重新看成一个树,并在先序中查找到这部分的根结点(此为根结点的右子结点),再进行右侧部分,不断地递归下去就会实现,当这个中序中的位置和后序中的元素一致,就会结束。
-
问题2:如何利用树实现中缀转后缀得出结果?
-
问题2的解决方案:在上学期实现的四则运算的思路来看,整个运算式需要先不断截取成一个字符,把运算符方法放到一个无序列表,将数放到一个定义为链表式树类的无序列表。针对运算的逻辑顺序,括号内的优先级最高,乘除的运算其次,最后是加减的运算。所以,在断开存放的时候先判断括号,如果遇到括号,那么直至查找到另一半括号,中间的内容都为括号内的,将这一部分整体作为一个新的运算式通过递归不断打开,使得括号内的内容拆成一棵树,将这部分的树存放到无序列表中。通过判断截取存放的过程将运算符为加减的存放到无序列表,遇到乘除的时候将存放树的无序列表取出最后一个(为乘除前一位内容可能是括号展成的树或是数字),判断后面是否为括号,如果为括号就取到另一半括号为止,这样同样是用到递归来实现拆分成树,将取到的乘除作为根结点,前面的内容作为左子树,后面的内容作为右子树;如果是不为括号,那么就是个数,就将数作为树,这不过是一个只有根结点没有左右结点的树,加上之前的乘除号重新做一个树,存放到无序列表中,最后该无序列表会只存放一个树,只需输出一下就好。
public BinaryTreeNode change(String strings){ //把“=”进行抠出 StringTokenizer tokenizer = new StringTokenizer(strings,"="); String string = tokenizer.nextToken(); //开始转换 StringTokenizer stringTokenizer = new StringTokenizer(string," "); ArrayUnorderedList<String> arrayUnorderedList1 = new ArrayUnorderedList<String>(); ArrayUnorderedList<LinkedBinaryTree> arrayUnorderedList2 = new ArrayUnorderedList<LinkedBinaryTree>(); while (stringTokenizer.hasMoreTokens()){ strings = stringTokenizer.nextToken(); //判断式子中是否有括号 boolean judge1 = true; if(strings.equals("(")){ String string1 = ""; while (judge1){ strings = stringTokenizer.nextToken(); //开始查询括号的另一个,如果查到的话就会跳出该循环 if (!strings.equals(")")) string1 += strings + " "; else break; } LinkedBinaryTree linkedBinaryTree = new LinkedBinaryTree(); linkedBinaryTree.root = (change(string1)); arrayUnorderedList2.addToRear(linkedBinaryTree); continue; } //判断运算符是否是加减的运算 if((strings.equals("+")|| strings.equals("-"))){ arrayUnorderedList1.addToRear(strings); }else ///判断运算符是否是乘除的运算 if((strings.equals("*")|| strings.equals("/"))){ LinkedBinaryTree left = arrayUnorderedList2.removeLast(); String strings2 = strings; strings = stringTokenizer.nextToken(); if(!strings.equals("(")) { LinkedBinaryTree right = new LinkedBinaryTree(strings); LinkedBinaryTree node = new LinkedBinaryTree(strings2, left, right); arrayUnorderedList2.addToRear(node); }else { String string3 = ""; boolean judge2 = true; while (judge2){ strings = stringTokenizer.nextToken(); if (!strings.equals(")")) string3 += strings + " "; else break; } LinkedBinaryTree linkedBinaryTree = new LinkedBinaryTree(); linkedBinaryTree.root = (change(string3)); LinkedBinaryTree node = new LinkedBinaryTree(strings2,left,linkedBinaryTree); arrayUnorderedList2.addToRear(node); } }else arrayUnorderedList2.addToRear(new LinkedBinaryTree(strings)); } while(arrayUnorderedList1.size()>0){ LinkedBinaryTree left = arrayUnorderedList2.removeFirst(); LinkedBinaryTree right = arrayUnorderedList2.removeFirst(); String oper = arrayUnorderedList1.removeFirst(); LinkedBinaryTree node = new LinkedBinaryTree(oper,left,right); arrayUnorderedList2.addToFront(node); } root = (arrayUnorderedList2.removeFirst()).getRootNode(); return root; }
其他
实验二的六个实验看似很多实际上就第二个、第四个和第六个实验需要认真修改的。这三个实验的不同提交时间可以减少不小麻烦。但是实验四的设计思路还是很费劲,构思出但是不会用代码实现,还有是否要加括号的问题,如果添加那么就需要在优先级上进行改写,好在最后写出来了。感觉这学期的实验更多的是偏向于逻辑算法的,就像本次实验这样,需要更多的思路要考虑。