20172314 《程序設計與數據結構》實驗報告——樹
課程:《程序設計與數據結構》
班級: 1723
姓名: 方藝雯
學號:20172314
實驗教師:王誌強
實驗日期:2018年11月8日
必修/選修: 必修
1、實驗內容及要求
實驗二-1-實現二叉樹
參考教材p212,完成鏈樹LinkedBinaryTree的實現(getRight,contains,toString,preorder,postorder)用JUnit或自己編寫驅動類對自己實現的LinkedBinaryTree進行測試
實驗二 樹-2-中序先序序列構造二叉樹
基於LinkedBinaryTree,實現基於(中序,先序)序列構造唯一一棵二?樹的功能,比如給出中序HDIBEMJNAFCKGL和後序ABDHIEJMNCFGKL,構造出附圖中的樹,用JUnit或自己編寫驅動類對自己實現的功能進行測試
實驗二 樹-3-決策樹
自己設計並實現一顆決策樹
實驗二 樹-4-表達式樹
輸入中綴表達式,使用樹將中綴表達式轉換為後綴表達式,並輸出後綴表達式和計算結果
實驗二 樹-5-二叉查找樹
完成PP11.3
實驗二 樹-6-紅黑樹分析
參考http://www.cnblogs.com/rocedu/p/7483915.html對Java中的紅黑樹(TreeMap,HashMap)進行源碼分析,並在實驗報告中體現分析結果。(C:\Program Files\Java\jdk-11.0.1\lib\src\java.base\java\util)
實驗過程及結果
實驗2-1
getRight方法核心代碼為
LinkedBinaryTree<T> result = new LinkedBinaryTree <T>(); result.root = root.getRight();
preorder,postorder方法核心代碼為
使用了遞歸的方法進行遍歷//前序遍歷 public void preOrder(BinaryTreeNode<T> root) { if (root != null) { System.out.print(root.element + " "); preOrder(root.left); preOrder(root.right); } } //後序遍歷 public void postOrder(BinaryTreeNode<T> root) { if (root != null) { postOrder(root.left); postOrder(root.right); System.out.print(root.element + " "); } }
contains,toString,方法均使用課本代碼,其中toString是將PrintTree改名。
實驗結果
實驗2-2
核心代碼為:
public BinaryTreeNode<T> reBuildTree(String[] pre, String[] in, int preStart, int preEnd, int inStart, int inEnd) { BinaryTreeNode root = new BinaryTreeNode(pre[preStart]); root.left = null; root.right = null; if (preStart == preEnd && inStart == inEnd) {//只有一個元素時 return root; } int a = 0; for(a= inStart; a < inEnd; a++){//找到中序遍歷中根節點的位置 if (pre[preStart] == in[a]) { break; } } int leftLength = a - inStart;//找到左子樹的元素個數 int rightLength = inEnd - a;//找到右子樹的元素個數 if (leftLength > 0) {//左右子樹分別進行以上操作 root.left= reBuildTree(pre, in, preStart+1, preStart+leftLength, inStart, a-1); } if (rightLength > 0) { root.right = reBuildTree(pre, in, preStart+1+leftLength, preEnd, a+1, inEnd); } return root; }
- 在原來的二叉樹代碼中添加reBuildTree方法,結合前序和中序序列,找到根結點和左右子樹,然後對左右子樹分別遞歸使用reBuildTree方法,逐步往下建立樹。
最後使用
方法調用reBuildTree(String[] pre, String[] in, int preStart, int preEnd, int inStart, int inEnd)方法,完成樹的重建。public void reBuildTree(String [] pre, String [] in) { BinaryTreeNode a = reBuildTree(pre, in, 0, pre.length-1, 0, in.length-1); root = a; }
實驗結果
實驗2-3
核心代碼
evaluate方法用來決策,從文件中讀入問題之後,根據用戶輸入的結果,來進行下一步選擇,輸出左子樹或右子樹。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()); }
實驗結果
實驗2-4
核心代碼
public BinaryTreeNode BuildTree(String str) { ArrayList<BinaryTreeNode> num = new ArrayList<BinaryTreeNode>(); ArrayList<String> symbol = new ArrayList<String>(); StringTokenizer st = new StringTokenizer(str); //得到輸入的數字和符號 String next; while (st.hasMoreTokens()) { next = st.nextToken(); if (next.equals("(")) { String str1 = ""; next = st.nextToken(); while (!next.equals(")")) {//計算括號內的內容,當找到右括號時,進行下面的步驟構造樹 str1 += next + " "; next = st.nextToken(); } num.add(BuildTree(str1));//括號裏的優先,創建一棵樹 if (st.hasMoreTokens()) { next = st.nextToken(); } else break; } if (!next.equals("+") && !next.equals("-") && !next.equals("*") && !next.equals("/")) { num.add(new BinaryTreeNode(next)); //是數字進入num } if (next.equals("+") || next.equals("-")) { BinaryTreeNode<String> tempNode = new BinaryTreeNode<>(next); next = st.nextToken(); if (!next.equals("(")) { symbol.add(tempNode.element);//優先級低,存入符號集 num.add(new BinaryTreeNode(next)); } else { symbol.add(tempNode.element); String temp = st.nextToken(); String s = ""; while (!temp.equals(")")) {//收集括號內的信息 s += temp + " "; temp = st.nextToken(); } num.add(BuildTree(s));//對括號內的建樹 } } if (next.equals("*") || next.equals("/")) { BinaryTreeNode<String> tempNode = new BinaryTreeNode<>(next); next = st.nextToken(); if (!next.equals("(")) {//沒有括號時,以* / 為父結點建樹,num中最後兩個數分別為左右孩子 tempNode.setLeft(num.remove(num.size() - 1)); tempNode.setRight(new BinaryTreeNode<String>(next)); num.add(tempNode);//將這個樹添加到num中 } else { //遇到括號,num的最後一個數為左孩子,剩下的都是右子樹 String temp = st.nextToken(); tempNode.setLeft(num.remove(num.size() - 1));//把* 或/ 前面的數變為左子樹 String s = ""; while (!temp.equals(")")) {//括號中內容全部是的是右子樹 s += temp + " "; temp = st.nextToken(); } tempNode.setRight(BuildTree(s)); num.add(tempNode); } } } int i = symbol.size(); while (i > 0) {//最後把num中存放的小樹,整合成一棵完整的樹。 BinaryTreeNode<T> root = new BinaryTreeNode(symbol.remove(symbol.size() - 1)); root.setRight(num.remove(num.size() - 1)); root.setLeft(num.remove(num.size() - 1)); num.add(root); i--; } return num.get(0);//輸出最終的樹 }
- 要考慮優先級,括號的優先級最高,括號中的式子構建子樹,然後再次存入數組中,其次是乘除,取數字組的最後兩個數與符號構建二叉樹,最後是加減。然後從num數組中將最後得到的各個優先級的子樹根據symbol數組裏的符號構建最終的樹。
實驗結果
實驗2-5
核心代碼
public T findMin() { T result = null; if (isEmpty()) throw new EmptyCollectionException("LinkedBinarySearchTree"); else { if (root.left == null) { result = root.element; //root = root.right; } else { BinaryTreeNode<T> parent = root; BinaryTreeNode<T> current = root.left; while (current.left != null) { parent = current; current = current.left; } result = current.element; //parent.left = current.right; } //modCount--; } return result; }
@Override public T findMax() { T result = null; if (isEmpty()) throw new EmptyCollectionException("LinkedBinarySearchTree"); else { if (root.right == null) { result = root.element; //root = root.left; } else { BinaryTreeNode<T> parent = root; BinaryTreeNode<T> current = root.right; while (current.right != null) { parent = current; current = current.right; } result = current.element; //parent.right = current.left; } //modCount--; } return result; }
public T removeMax() { T result = null; if (isEmpty()) throw new EmptyCollectionException("LinkedBinarySearchTree"); else { if (root.right == null) { result = root.element; root = root.left; } else { BinaryTreeNode<T> parent = root; BinaryTreeNode<T> current = root.right; while (current.right != null) {//找右子樹,他為最大 parent = current; current = current.right; } result = current.element;//得到最大值 parent.right = current.left;//用左孩子替換最大的 } modCount--; } return result; }
findMin和findMax類似,removeMax是在findMax的基礎上找到之後將其替換。由於二叉查找樹左孩子小於根小於右孩子,例如findMin操作,當左孩子為空時,返回根結點(最小),否則向下查找到最後一個左孩子,返回其值。
實驗結果
實驗2-6
- 首先介紹一下紅黑樹:
- 每個節點都只能是紅色或者黑色
- 根節點是黑色
- 每個葉子節點是黑色的
- 如果一個節點是紅色的,則它的兩個子節點都是黑色的
- 從任意一個節點到每個葉子節點的所有路徑都包含相同數目的黑色節點
- key的兩種排序方式
- 自然排序:TreeMap的所有key必須實現Comparable接口,並且所有key應該是同一個類的對象,否則將會拋ClassCastException異常
* 指定排序:這種排序需要在構造TreeMap時,傳入一個Comparator對象,該對象負責對TreeMap中的key進行排序
- 自然排序:TreeMap的所有key必須實現Comparable接口,並且所有key應該是同一個類的對象,否則將會拋ClassCastException異常
TreeMap類的繼承關系
它繼承並實現了Map,所以TreeMap具有和Map一樣執行put,get的操作,直接通過key取value值。同時實現SortedMap,支持遍歷時按元素的大小有序遍歷。public class TreeMap<K,V> extends AbstractMap<K,V>implements NavigableMap<K,V>, Cloneable, Serializable
TreeMap 是一個有序的key-value集合,它是通過紅黑樹實現的。
TreeMap 繼承於AbstractMap,所以它是一個Map,即一個key-value集合。
TreeMap 實現了NavigableMap接口,意味著它支持一系列的導航方法。比如返回有序的key集合。
TreeMap 實現了Cloneable接口,意味著它能被克隆。
TreeMap 實現了java.io.Serializable接口,意味著它支持序列化。
TreeMap基於紅黑樹(Red-Blacktree)實現。該映射根據其鍵的自然順序進行排序,或者根據創建映射時提供的 Comparator進行排序,具體取決於使用的構造方法。構造函數
// 默認構造函數 public TreeMap() { comparator = null; } // 帶比較器的構造函數 public TreeMap(Comparator<? super K> comparator) { this.comparator = comparator; } // 帶Map的構造函數,Map會成為TreeMap的子集 public TreeMap(Map<? extends K, ? extends V> m) { comparator = null; putAll(m); } // 帶SortedMap的構造函數,SortedMap會成為TreeMap的子集 public TreeMap(SortedMap<K, ? extends V> m) { comparator = m.comparator(); try { buildFromSorted(m.size(), m.entrySet().iterator(), null, null); } catch (java.io.IOException cannotHappen) { } catch (ClassNotFoundException cannotHappen) { } }
從TreeMap中刪除第一個節點方法
public Map.Entry<K,V> pollFirstEntry() { // 獲取第一個節點 Entry<K,V> p = getFirstEntry(); Map.Entry<K,V> result = exportEntry(p); // 刪除第一個節點 if (p != null) deleteEntry(p); return result; }
返回小於key值的最大的鍵值對所對應的KEY,沒有的話返回null
public K lowerKey(K key) { return keyOrNull(getLowerEntry(key)); }
獲取Map的頭部,範圍從第一個節點 到 toKey.
public NavigableMap<K,V> headMap(K toKey, boolean inclusive) { return new AscendingSubMap(this, true, null, true, false, toKey, inclusive); }
刪除當前結點
需註意當lastReturned的左右孩子都不為空時,要將其賦值給next。是因為刪除lastReturned節點之後,next節點指向的仍然是下一個節點。根據紅黑樹的特性可知:當被刪除節點有兩個兒子時。那麽,首先把它的後繼節點的內容復制給該節點的內容,之後刪除它的後繼節點。這意味著當被刪除節點有兩個兒子時,刪除當前節點之後,新的當前節點實際上是原有的後繼節點(即下一個節點)。而此時next仍然指向新的當前節點。也就是說next是仍然是指向下一個節點,能繼續遍歷紅黑樹。
public void remove() { if (lastReturned == null) throw new IllegalStateException(); if (modCount != expectedModCount) throw new ConcurrentModificationException(); if (lastReturned.left != null && lastReturned.right != null) next = lastReturned; deleteEntry(lastReturned); expectedModCount = modCount; lastReturned = null; } }
firstEntry()和getFirstEntry()
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; }
firstEntry()和getFirstEntry()都是用於獲取第一個節點,firstEntry()是對外接口;getFirstEntry() 是內部接口。而且,firstEntry()是通過getFirstEntry() 來實現的。之所以不直接調用getFirstEntry()是為了防止用戶修改返回的Entry。我們可以調用Entry的getKey()、getValue()來獲取key和value值,以及調用setValue()來修改value的值,而對firstEntry()返回的Entry對象只能進行getKey()、getValue()等讀取操作。所以要調用 firstEntry()獲取。
HashMap
HashMap由數組+鏈表組成的,數組是HashMap的主體,鏈表則是主要為了解決哈希沖突而存在的,如果定位到的數組位置不含鏈表(當前entry的next指向null),那麽對於查找,添加等操作很快,僅需一次尋址即可;如果定位到的數組包含鏈表,對於添加操作,其時間復雜度為O(n),首先遍歷鏈表,存在即覆蓋,否則新增,對於查找操作來講,仍需遍歷鏈表,然後通過key對象的equals方法逐一比對查找。所以,性能考慮,HashMap中的鏈表出現越少,性能才會越好。get方法
get方法通過key值返回對應value,如果key為null,直接去table[0]處檢索public V get(Object key) {//如果key為null,則直接去table[0]處去檢索即可。 if (key == null) return getForNullKey(); Entry<K,V> entry = getEntry(key); return null == entry ? null : entry.getValue(); }
getEntry方法
get方法的實現相對簡單,key(hashcode)-->hash-->indexFor-->最終索引位置,找到對應位置table[i],再查看是否有鏈表,遍歷鏈表,通過key的equals方法比對查找對應的記錄。要註意的是,上面在定位到數組位置之後然後遍歷鏈表的時候,e.hash==hash是有必要的,不能僅通過equals判斷。因為如果傳入的key對象重寫了equals方法卻沒有重寫hashCode,而恰巧此對象定位到這個數組位置,如果僅僅用equals判斷可能是相等的,但其hashCode和當前對象不一致,這種情況,根據Object的hashCode的約定,不能返回當前對象,而應該返回null。final Entry<K,V> getEntry(Object key) { if (size == 0) { return null; } //通過key的hashcode值計算hash值 int hash = (key == null) ? 0 : hash(key); //indexFor (hash&length-1) 獲取最終數組索引,然後遍歷鏈表,通過equals方法比對找出對應記錄 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; }
roundUpToPowerOf2方法
這個處理使得數組長度一定為2的次冪,Integer.highestOneBit是用來獲取最左邊的bit(其他bit位為0)所代表的數值。private static int roundUpToPowerOf2(int number) { // assert number >= 0 : "number must be non-negative"; return number >= MAXIMUM_CAPACITY ? MAXIMUM_CAPACITY : (number > 1) ? Integer.highestOneBit((number - 1) << 1) : 1; }
那麽為什麽數組長度一定是2的次冪呢?
這樣會保證低位全為1,而擴容後只有一位差異,也就是多出了最左位的1,這樣在通過 h&(length-1)的時候,只要h對應的最左邊的那一個差異位為0,就能保證得到的新的數組索引和老數組索引一致,同時,數組長度保持2的次冪,length-1的低位都為1,會使得獲得的數組索引index更加均勻,如果不是2的次冪,也就是低位不是全為1此時,h的低位部分不再具有唯一性了,哈希沖突的幾率會變的更大。
遇到的問題及解決
- 問題一:實驗二-2的中序先序序列構造二叉樹的實現
問題一解決:前序序列可以確定根結點,由循環得出
for(a= inStart; a < inEnd; a++){//找到中序遍歷中根節點的位置 if (pre[preStart] == in[a]) { break; } }
那麽在中序序列中,根結點左右的元素即可確立,確定左右元素的數目leftLength和rightLength,若大於0,則分別進行
root.left= reBuildTree(pre, in, preStart+1, preStart+leftLength, inStart, a-1);
和
root.right = reBuildTree(pre, in, preStart+1+leftLength, preEnd, a+1, inEnd);
再次確定左右子樹的根結點,如此循環,直到所有的結點被確定,這時樹就形成了。
其他
這次的實驗報告有一點難度,花費挺長時間的,不過對樹的了解更加深入了,學習到很多。
參考
- Java 集合系列12之 TreeMap詳細介紹(源碼解析)和使用示例
- HashMap實現原理及源碼分析
20172314 《程序設計與數據結構》實驗報告——樹