20172303 2018-2019-1 《程序設計與數據結構》實驗二報告
阿新 • • 發佈:2018-11-11
課本 min() 源碼解析 成員變量 平衡 完整 情況下 vat access
20172303 2018-2019-1 《程序設計與數據結構》實驗二報告
- 課程:《程序設計與數據結構》
- 班級: 1723
- 姓名: 範雯琪
- 學號:20172303
- 實驗教師:王誌強
- 助教:張師瑜/張之睿
- 實驗日期:2018年11月5日
- 必修/選修: 必修
實驗內容
本次實驗主要是關於樹的應用, 涉及了二叉樹、決策樹、表達式樹、二叉查找樹、紅黑樹五種樹的類型,是對最近學習內容第十章和第十一章的一個總結。
節點一
- 參考教材P212,完成鏈樹LinkedBinaryTree的實現(getRight,contains,toString,preorder,postorder),用JUnit或自己編寫驅動類對自己實現的LinkedBinaryTree進行測試。
節點二
- 基於LinkedBinaryTree,實現基於(中序,先序)序列構造唯一一棵二?樹的功能,比如給出先序ABDHIEJMNCFGKL和中序HDIBEMJNAFCKGL,構造出附圖中的樹,用JUnit或自己編寫驅動類對自己實現的功能進行測試。
節點三
- 自己設計並實現一顆決策樹。
節點四
- 輸入中綴表達式,使用樹將中綴表達式轉換為後綴表達式,並輸出後綴表達式和計算結果(如果沒有用樹,則為0分)。
節點五
- 完成PP11.3。
節點六
- 參考Java Collections API源碼分析對Java中的紅黑樹(TreeMap,HashMap)進行源碼分析,並在實驗報告中體現分析結果。
實驗過程及結果
節點一——實現二叉樹
- getRight:
getRight
操作用於返回根的右子樹。當樹為空時,拋出錯誤,當樹不為空時,通過遞歸返回根的右子樹。
public LinkedBinaryTree2<T> getRight() { if(root == null) { throw new EmptyCollectionException("BinaryTree"); } LinkedBinaryTree2<T> result = new LinkedBinaryTree2<>(); result.root = root.getRight(); return result; }
- contains:
contains
操作的實現有兩種方法:一種是直接借用find
方法,另一種是重新寫一個。- 方法一:借用
find
方法,find
方法的作用是在二叉樹中找到指定目標元素,則返回對該元素的引用,所以當該元素的引用與查找的元素相同時返回true,否則返回false。
public boolean contains(T targetElement) { if (find(targetElement) == targetElement){return true;} else {return false;} }
- 方法二:重新寫一個。具體解釋放在代碼當中。
public boolean contains(T targetElement) { BinaryTreeNode node = root; BinaryTreeNode temp = root; //找到的情況有三種:查找元素就是根,查找元素位於右子樹,查找元素位於左子樹。 //除了這三種情況下其余情況都找不到元素,因此初始設置為false boolean result = false; //當樹為空時,返回false if (node == null){ result = false; } //當查找元素就是根時,返回true if (node.getElement().equals(targetElement)){ result = true; } //對右子樹進行遍歷(在右子樹不為空的情況下)找到元素則返回true,否則對根的左子樹進行遍歷 while (node.right != null){ if (node.right.getElement().equals(targetElement)){ result = true; break; } else { node = node.right; } } //對根的左子樹進行遍歷,找到元素則返回true,否則返回false while (temp.left.getElement().equals(targetElement)){ if (temp.left.getElement().equals(targetElement)){ result = true; break; } else { temp = temp.left; } } return result; }
- 方法一:借用
- toString:
toString
方法我借用了ExpressionTree類
中的PrintTree
方法,具體內容曾在第七周博客中說過。 - preorder:
preorder
方法由於有inOrder
方法的參考所以挺好寫的,修改一下三條代碼(三條代碼分別代碼訪問根、訪問右孩子和訪問左孩子)的順序即可,使用了遞歸。在輸出時為了方便輸出我重新寫了一個ArrayUnorderedList類
的公有方法,直接輸出列表,要比用叠代器輸出方便一些。
public ArrayUnorderedList preOrder(){
ArrayUnorderedList<T> tempList = new ArrayUnorderedList<T>();
preOrder(root,tempList);
return tempList;
}
protected void preOrder(BinaryTreeNode<T> node,
ArrayUnorderedList<T> tempList)
{
if (node != null){
//從根節點開始,先訪問左孩子,再訪問右孩子
tempList.addToRear(node.getElement());
preOrder(node.getLeft(),tempList);
preOrder(node.getRight(),tempList);
}
}
- postOrder:
postOrder
方法與preorder
方法類似,唯一的區別是後序遍歷先訪問左孩子,再訪問右孩子,最後訪問根結點,代碼和上面差不多就不放了。
測試結果
節點二——中序先序序列構造二叉樹
- 已知先序遍歷和中序遍歷得到二叉樹有三個步驟:
- (1)找到根結點。因為先序遍歷按照先訪問根結點再訪問左右孩子的順序進行的,所以先序遍歷的第一個結點就是二叉樹的根。
- (2)區分左右子樹。在確定了根結點之後,在中序遍歷結果中,根結點之前的就是左子樹,根結點之後的就是右子樹。如果跟結點前邊或後邊為空,那麽該方向子樹為空;如果根節點前邊和後邊都為空,那麽根節點已經為葉子節點。
- (3)分別對左右子樹再重復第一、二步直至完全構造出該樹。
- 在清楚了構造的步驟之後,實現就比較簡單了,在實現的過程中用了遞歸的方法。
public void initTree(String[] preOrder,String[] inOrder){
BinaryTreeNode temp = initTree(preOrder,0,preOrder.length-1,inOrder,0,inOrder.length-1);
root = temp;
}
private BinaryTreeNode initTree(String[] preOrder,int prefirst,int prelast,String[] inOrder,int infirst,int inlast){
if(prefirst > prelast || infirst > inlast){
return null;
}
String rootData = preOrder[prefirst];
BinaryTreeNode head = new BinaryTreeNode(rootData);
//找到根結點
int rootIndex = findroot(inOrder,rootData,infirst,inlast);
//構建左子樹
BinaryTreeNode left = initTree(preOrder,prefirst + 1,prefirst + rootIndex - infirst,inOrder,infirst,rootIndex-1);
//構建右子樹
BinaryTreeNode right = initTree(preOrder,prefirst + rootIndex - infirst + 1,prelast,inOrder,rootIndex+1,inlast);
head.left = left;
head.right = right;
return head;
}
//尋找根結點在中序遍歷數組中的位置
public int findroot(String[] a, String x, int first, int last){
for(int i = first;i<=last; i++){
if(a[i] == x){
return i;
}
}
return -1;
}
測試結果
節點三——決策樹
- 節點三的實現借助了第十章背部疼痛診斷器的相關內容,其關鍵部分是
DecisionTree
類的實現。DecisionTree
的構造函數從文件中讀取字符串元素。存儲在樹結點中。然後創建新的結點,將之前定義的結點(或子樹)作為內部結點的子結點。
public DecisionTTree(String filename) throws FileNotFoundException { //讀取字符串 File inputFile = new File(filename); Scanner scan = new Scanner(inputFile); int numberNodes = scan.nextInt(); scan.nextLine(); int root = 0, left, right; //存儲在根結點中 List<LinkedBinaryTree<String>> nodes = new ArrayList<LinkedBinaryTree<String>>(); for (int i = 0; i < numberNodes; i++) { nodes.add(i,new LinkedBinaryTree<String>(scan.nextLine())); } //建立子樹 while (scan.hasNext()) { root = scan.nextInt(); left = scan.nextInt(); right = scan.nextInt(); scan.nextLine(); nodes.set(root, new LinkedBinaryTree<String>((nodes.get(root)).getRootElement(), nodes.get(left), nodes.get(right))); } tree = nodes.get(root); }
evaluate
方法從根結點開始處理,用current表示正在處理的結點。在循環中,如果用戶的答案為N,則更新current使之指向左孩子,如果用戶的答案為Y,則更新current使之指向右孩子,循環直至current為葉子結點時結束,結束後返回current的根結點的引用。
public void evaluate() { LinkedBinaryTree<String> current = tree; Scanner scan = new Scanner(System.in); while (current.size() > 1) { System.out.println (current.getRootElement()); if (scan.nextLine().equalsIgnoreCase("N")) { current = current.getLeft(); } else { current = current.getRight(); } } System.out.println (current.getRootElement()); }
測試結果
節點四——表達式樹
- 這個測試我認為是所有測試中最難的一個, 尤其是關於如何使用樹實現這一部分,考慮了很久都沒有思路,後來重新翻看課本第十章表達式樹部分的內容,才有了思路,發現不是光用樹就能實現的,像上學期的四則運算一樣,這個也是要先建立兩個棧來存放操作符和操作數的。具體的解釋在下面的代碼中都有。
public static String toSuffix(String infix) {
String result = "";
//將字符串轉換為數組
String[] array = infix.split("\\s+");
//存放操作數
Stack<LinkedBinaryTree> num = new Stack();
//存放操作符
Stack<LinkedBinaryTree> op = new Stack();
for (int a = 0; a < array.length; a++) {
//如果是操作數,開始循環
if (array[a].equals("+") || array[a].equals("-") || array[a].equals("*") || array[a].equals("/")) {
if (op.empty()) {
//如果棧是空的,將數組中的元素建立新樹結點並壓入操作符棧
op.push(new LinkedBinaryTree<>(array[a]));
} else {
//如果棧頂元素為+或-且數組的元素為*或/時,將元素建立新樹結點並壓入操作符棧
if ((op.peek().root.element).equals("+") || (op.peek().root.element).equals("-") && array[a].equals("*") || array[a].equals("/")) {
op.push(new LinkedBinaryTree(array[a]));
} else {
//將操作數棧中的兩個元素作為左右孩子,操作符棧中的元素作為根建立新樹
LinkedBinaryTree right = num.pop();
LinkedBinaryTree left = num.pop();
LinkedBinaryTree temp = new LinkedBinaryTree(op.pop().root.element, left, right);
//將樹壓入操作數棧,並將數組中的元素建立新樹結點並壓入操作符棧
num.push(temp);
op.push(new LinkedBinaryTree(array[a]));
}
}
} else {
//將數組元素建立新樹結點並壓入操作數棧
num.push(new LinkedBinaryTree<>(array[a]));
}
}
while (!op.empty()) {
LinkedBinaryTree right = num.pop();
LinkedBinaryTree left = num.pop();
LinkedBinaryTree temp = new LinkedBinaryTree(op.pop().root.element, left, right);
num.push(temp);
}
//輸出後綴表達式
Iterator itr=num.pop().iteratorPostOrder();
while (itr.hasNext()){
result+=itr.next()+" ";
}
return result;
}
測試結果
節點五——二叉查找樹
- 因為書上給出了
removeMin
的實現方法,二叉查找樹有一個特殊的性質就是最小的元素存儲在樹的左邊,最大的元素存儲在樹的右邊。因此實現removeMax
方法只需要把removeMin
方法中所有的left和right對調即可。二叉查找樹的刪除操作有三種情況,要依據這三種情況來實現代碼,我在第七周博客教材內容總結中已經分析過了,就不在這裏貼代碼了。 - 實現了
removeMin
和removeMax
後,其實findMin
和findMax
就很簡單了,因為在實現刪除操作時首先先要找到最大/最小值,因此只要把找到之後的步驟刪掉,返回找到的最大值或最小值的元素即可。
public T findMin() throws EmptyCollectionException
{
T result;
if (isEmpty()){
throw new EmptyCollectionException("LinkedBinarySearchTree");
}
else {
if (root.left == null){
result = root.element;
}
else {
BinaryTreeNode<T> parent = root;
BinaryTreeNode<T> current = root.left;
while (current.left != null){
parent = current;
current = current.left;
}
result = current.element;
}
}
return result;
}
public T findMax() throws EmptyCollectionException
{
T result;
if (isEmpty()){
throw new EmptyCollectionException("LinkedBinarySearchTree");
}
else {
if (root.right == null){
result = root.element;
}
else {
BinaryTreeNode<T> parent = root;
BinaryTreeNode<T> current = root.right;
while (current.right != null){
parent = current;
current = current.right;
}
result = current.element;
}
}
return result;
}
測試結果
節點六——紅黑樹分析
- 在jdk1.8版本後,java對HashMap做了改進,在鏈表長度大於8的時候,將後面的數據存在紅黑樹中,以加快檢索速度。而TreeMap的實現原理就是紅黑樹,因此分析紅黑樹時我們要分析HashMap和TreeMap的源碼。
HashMap
- HashMap是一種基於哈希表(hash table)實現的map,哈希表(也叫關聯數組)一種通用的數據結構,大多數的現代語言都原生支持,其概念也比較簡單:key經過hash函數作用後得到一個槽(buckets或slots)的索引(index),槽中保存著我們想要獲取的值,如下圖所示:
- HashMap的方法較多,此處選擇構造函數、get操作和remove操作進行分析。
- 構造函數
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; threshold = initialCapacity; init(); } public HashMap(int initialCapacity) { this(initialCapacity, DEFAULT_LOAD_FACTOR); } public HashMap() { this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR); }
- HashMap遵循集合框架的約束,提供了一個參數為空的構造函數和有一個參數且參數類型為Map的構造函數。除此之外,還提供了兩個構造函數,用於設置HashMap的容量(capacity)與平衡因子(loadFactor)(平衡因子=|右子樹高度-左子樹高度|)。
- HashMap遵循集合框架的約束,提供了一個參數為空的構造函數和有一個參數且參數類型為Map的構造函數。除此之外,還提供了兩個構造函數,用於設置HashMap的容量(capacity)與平衡因子(loadFactor)(平衡因子=|右子樹高度-左子樹高度|)。
- get操作
- get操作用於返回指定鍵所映射的值;如果對於該鍵來說,此映射不包含任何映射關系,則返回null。
- 這裏需要說明兩個東西:Entry——Entry實現了單向鏈表的功能,用next成員變量來級連起來。table[ ]——HashMap內部維護了一個為數組類型的Entry變量table,用來保存添加進來的Entry對象。
public V get(Object key) { //當key為空時,返回null if (key == null) return getForNullKey(); Entry<K,V> entry = getEntry(key); return null == entry ? null : entry.getValue(); } private V getForNullKey() { if (size == 0) { return null; } //key為null的Entry用於放在table[0]中,但是在table[0]沖突鏈中的Entry的key不一定為null,因此,需要遍歷沖突鏈,查找key是否存在 for (Entry<K,V> e = table[0]; e != null; e = e.next) { if (e.key == null) return e.value; } return null; } final Entry<K,V> getEntry(Object key) { if (size == 0) { return null; } int hash = (key == null) ? 0 : hash(key); //首先定位到索引在table中的位置 //然後遍歷沖突鏈,查找key是否存在 for (Entry<K,V> e = table[indexFor(hash, table.length)]; e != null; e = e.next) { Object k; if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) return e; } return null; }
- remove操作
- remove操作用於在指定鍵存在的情況下,從此映射中移除指定鍵的映射關系。
public V remove(Object key) { Entry<K,V> e = removeEntryForKey(key); //當指定鍵key存在時,返回key的value。 return (e == null ? null : e.value); } final Entry<K,V> removeEntryForKey(Object key) { if (size == 0) { return null; } int hash = (key == null) ? 0 : hash(key); int i = indexFor(hash, table.length); //這裏用了兩個Entry對象,相當於兩個指針,為的是防止出現鏈表指向為空,即沖突鏈斷裂的情況 Entry<K,V> prev = table[i]; Entry<K,V> e = prev; //當table[i]中存在沖突鏈時,開始遍歷裏面的元素 while (e != null) { Entry<K,V> next = e.next; Object k; if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) { modCount++; size--; if (prev == e) //當沖突鏈只有一個Entry時 table[i] = next; else prev.next = next; e.recordRemoval(this); return e; } prev = e; e = next; } return e; }
- 而在HashMap中涉及到紅黑樹的,是put操作。
- put操作
- put操作用於在此映射中關聯指定值與指定鍵。
public V put(K key, V value) { return putVal(hash(key), key, value, false, true); } //單個位置鏈表長度減小到6,將紅黑樹轉化會鏈表 static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); } /** * 插入key-value 鍵值對具體實現 */ final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { Node<K,V>[] tab; Node<K,V> p; int n, i; // 判斷 若hashmap內沒有值 則重構hashmap if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; // 若指定位置hashcode 未被占用 則直接將該鍵值對插入 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); // 若此時鏈表內長度大於等於7 將鏈表轉化為紅黑樹 並將節點插入 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:在實現節點一的時候,輸出的並不是遍歷結果而是地址
- 問題1解決方法:說實話這就是一個第十章沒學好的殘留問題,當時學的時候我就沒有把這一部分補充完整,對於叠代器的使用也不熟練,完成節點一的過程中,我想到的解決方法是重新寫了一個
ArrayUnorderedList類
的公有方法,將該無序列表直接輸出(代碼在節點一的過程中有)。後來實驗結束後詢問同學學會了將叠代器方法的遍歷結果輸出。
//以後序遍歷為例
String result = "";
Iterator itr = tree.iteratorPostOrder();
while (itr.hasNext()){
result += itr.next() + " ";
}
return result;
- 問題2:在實現節點二的時候無法輸出構造好的樹。
- 問題2解決方法:通過Debug,首先確定樹是構造好的,沒有出現樹為空的情況。
- 那麽問題就應該是出在
toString
方法中,後來發現原因出在了root上,在toString
方法中,root從一開始就是空的,並沒有獲取到我構造的樹的根結點。
- 然後我嘗試在
ReturnBinaryTree
類中加入了一個獲取根的方法,結果最後輸出的是根的地址。
- 最後參考了余坤澎同學的代碼,把
ReturnBinaryTree
類中的方法放的toString
所在的LinkedBinaryTree
類中,因為此時它能夠獲取到構造的樹的根節點,因此就能正常輸出了。
- 問題3:在實現決策樹的過程中,文件裏的內容為什麽以這樣的順序排列?
- 問題3解決方法:這個要結合
DecisionTree
類來看,首先第一行的13代表了這顆決策樹中的節點個數,所以在DecisionTree
類中的int numberNodes = scan.nextInt();
一句其實就是獲取文件的第一行記錄節點個數的值。接下來文件中按照層序遍歷的順序將二叉樹中的元素一一列出來,最後文件中的幾行數字其實代表了每個結點及其左右孩子的位置(仍然按照層序遍歷的順序),並且是從最後一層不是葉子結點的那一層的結點開始,比如[3,7,8]就代表了層序遍歷中第3個元素的左孩子為第7個元素,右孩子為第8個元素。 - 我剛開始把根結點設置成第1個元素發現怎麽都對不上,後來發現這裏定義了根結點為第0個元素,所以最後一個元素為第12個元素而不是第13個。
其他(感悟、思考等)
- 其實本次實驗整體上來說還是比較簡單的,唯一有難度的可能只有節點四和節點六。在這個過程中幫我復習了很多,而且逼著我去解決了一些曾經在教材學習中不願面對的問題,nice~~
參考資料
- Java Collections API源碼分析
- Java HashMap 源碼解析
- Java TreeMap 源碼解析
20172303 2018-2019-1 《程序設計與數據結構》實驗二報告