1. 程式人生 > >20172302 《Java軟體結構與資料結構》實驗二:樹實驗報告

20172302 《Java軟體結構與資料結構》實驗二:樹實驗報告

課程:《Java軟體結構與資料結構》

班級: 1723

姓名: 侯澤洋

學號:20172302

實驗教師:王志強老師

實驗日期:2018年11月5日

必修/選修: 必修

實驗內容

  • (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)

實驗過程及結果

(1)實驗一

完成鏈樹LinkedBinaryTree的實現(getRight,contains,toString,preorder,postorder)
這裡的方法編寫在學習樹時都有寫過,所以直接編寫了測試類,實驗結果如圖

(2)實驗二

基於LinkedBinaryTree,實現基於(中序,先序)序列構造唯一一棵二㕚樹的功能,這個需要新建類,類中寫了公有方法generate0,generate0再去呼叫私有方法generate,
這個主要是利用傳進來的先序和中序的字串,確定根結點,然後再確定其左右孩子,接下來遞迴該過程,直至將該字串讀取完成。
實驗結果截圖:

(3)實驗三

自己設計並實現一顆決策樹,設計了一棵決策樹去確定1至6之間的某個數。

11
Is the number greater than 3?
Is the number greater than 2?
Is the number greater than 4?
Is the number greater than 1?
Is the number greater than 5?
The number is 1.
The number is 2.
The number is 3.
The number is 4.
The number is 5.
The number is 6.
3 5 6
1 3 7
4 9 10
2 8 4
0 1 2

實驗結果見圖:

(4)實驗四

輸入中綴表示式,使用樹將中綴表示式轉換為字尾表示式,這裡是使用兩個棧,一個是操作符棧,另一個是運算元棧,其中運算元棧是運算元是以樹的型別進行儲存的。實驗結果見圖:

(5)實驗五

完成PP11.3;11.3在之前已經做過,測試了一次完成。

(6)實驗六

看了一下TreeMap和Hashmap的原始碼,一個3000多行,一個2400多行,放棄了,太多了。於是從網上找資料看了一些個的原始碼分析,這裡寫一些。
1.繼承結構
下面是HashMap與TreeMap的繼承結構:
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable {
public class TreeMap<K,V> extends AbstractMap<K,V> implements NavigableMap<K,V>, Cloneable, java.io.Serializable
可以看出它們都繼承了AbstractMap。而HashMap是直接實現的Map,TreeMap實現的是NavigableMap(Cloneable和Serializable忽略)。
2.TreeMap
TreeMap是NavbagableMap的實現,底層基於紅黑樹。這個Map按照Comparable將鍵值排序,或者按照在建立Map時提供的Compartor。
TreeMap的底層是基於紅黑樹的實現,所以像get、put、remove、containsKey這些方法都會花費log(n)的時間複雜度。這兒不會著重於紅黑樹的具體實現以及轉換,只要知道TreeMap的基本思路就可以了。
(1)put操作

    public V put(K key, V value) {
        Entry<K,V> t = root;
        // 1.如果根節點為 null,將新節點設為根節點
        if (t == null) {
            compare(key, key);
            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) {
            // 2.為 key 在紅黑樹找到合適的位置
            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 {
            // 與上面程式碼邏輯類似,省略
        }
        Entry<K,V> e = new Entry<>(key, value, parent);
        // 3.將新節點鏈入紅黑樹中
        if (cmp < 0)
            parent.left = e;
        else
            parent.right = e;
        // 4.插入新節點可能會破壞紅黑樹性質,這裡修正一下
        fixAfterInsertion(e);
        size++;
        modCount++;
        return null;
    }

從put方法可以看到,有幾步流程:

  1. 如果Map為空,那麼直接將新插入的值作為根結點。此時,如果提供了Compartor,就得看Compartor是否支援null鍵值;如果沒有提供Compartor,那麼將會丟擲NullPointerException。
  2. 如果Map不為空,那麼需要找到新插入的鍵值的父節點。在查詢過程中,如果遇到了鍵值相等的,那麼將會呼叫Entry.setValue()更新值。
  3. 一旦找到了父節點,那麼插入新節點,尺寸+1
    (2)get操作
    public V get(Object key) {
        Entry<K,V> p = getEntry(key);
        return (p==null ? null : p.value);
    }
    final Entry<K,V> getEntry(Object key) {
        // Offload comparator-based version for sake of performance
        if (comparator != null)
            return getEntryUsingComparator(key);
        if (key == null)
            throw new NullPointerException();
        @SuppressWarnings("unchecked")
            Comparable<? super K> k = (Comparable<? super K>) key;
        Entry<K,V> p = root;
        // 查詢操作的核心邏輯就在這個 while 迴圈裡
        while (p != null) {
            int cmp = k.compareTo(p.key);
            if (cmp < 0)
                p = p.left;
            else if (cmp > 0)
                p = p.right;
            else
                return p;
        }
        return null;
    }

從上面可以看到,get()方法的流程:

  1. 如果提供了Comparator,那麼使用getEntryUsingComparator()方法
  2. 如果沒有提供Comparator,並且鍵為null,丟擲NullPointerException
  3. 如果沒有提供Comparator且鍵不為null,將鍵強制轉換為Comparable介面,如果鍵沒有實現,那麼丟擲ClassCastExceotion
  4. 如果沒有提供Comparator且鍵不為null,且鍵實現了Comparable介面,那麼從根結點開始遍歷紅黑樹,一旦找到則返回節點,否則返回null
    (3)remove操作

3.HashMap
HashMap是基於Hash table實現的Map,它實現了Map中所有的可選的操作,並且允許key或value為null,近似的等價於Hashtable(除了HashMap是非同步並且允許null值);它不保證元素的順序;如果插入的元素被Hash函式正確的分散在不同的桶(槽,bucket)中,get和put操作都只需要常量時間。
(1)put操作

public V put(K key, V value) {
        //傳入key的hash值,對hashCode值做位運算
        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;
        //如果tab為null,則通過resize初始化
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        //計算key的索引,如果為當前位置為null,直接賦值
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            //如果當前位置不為null
            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;
            }
        }
        //結構變化次數+1
        ++modCount;
        //如果size超過最大限制,擴容
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

put()操作的主要是如下幾個步驟:
首先判斷Node[]陣列table是否為空或null,如果是空那麼進行一次resize,這次resize只是起到了一次初始化的作用。
根據key的值計算hash得到在table中的索引i,如果table[i]==null則新增新節點到table[i],然後判斷size是否超過了容量限制threshold,如果超過進行擴容。
如果在上一步table[i]不為null時,判斷table[i]節點是否和當前新增節點相同(這裡使用hash和equals判斷,因此需要保證hashCode()方法和equals()方法描述的一致性),如果相同則覆蓋該節點的value。
如果上一步判斷table[i]和當前節點不同,那麼判斷table[i]是否為紅黑樹節點,如果是紅黑樹節點則在紅黑樹中新增此key-value。
如果上一步判斷table[i]不是紅黑樹節點則遍歷table[i]連結串列,判斷連結串列長度是否超過8,如果超過則轉為紅黑樹儲存,如果沒有超過則在連結串列中插入此key-value。(jdk1.8以前使用頭插法插入)。在遍歷過程中,如果發現有相同的節點(比較hash和equals)就覆蓋value。
維護modCount和size等其他欄位。
(2)get操作

public V get(Object key) {
        Node<K,V> e;
        //傳入key的hash
        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;
        //這裡訪問(n - 1) & hash其實就是jdk1.7中indexFor方法的作用
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (first = tab[(n - 1) & hash]) != null) {
            //判斷桶索引位置的節點是不是相同(通過hash和equals判斷),如果相同返回此節點
            if (first.hash == hash && // always check first node
                ((k = first.key) == key || (key != null && key.equals(k))))
                return first;
            if ((e = first.next) != null) {
                //判斷是否是紅黑樹節點,如果是查詢紅黑樹
                if (first instanceof TreeNode)
                    return ((TreeNode<K,V>)first).getTreeNode(hash, key);
                do {
                    //如果是連結串列,遍歷連結串列
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        return e;
                } while ((e = e.next) != null);
            }
        }
        //如果不存在返回null
        return null;
    }

實驗過程中遇到的問題和解決過程

  • 問題1:做實驗2時我的樹列印的始終是不完整的,只有A—J這些個元素,後面的元素就是消失了。

  • 問題1解決方案:通過Debug,第二次時發現了問題,原來是我substring方法用的有問題,檢視API中substring方法的具體介紹:

           它的引數是包括起始索引,但不包括終止索引,而我在編寫過程當中是默認了它是起始索引和終止索引都包括在內,這就會導致遺漏了一部分的元素,因此出現了這個問題,在原來的基礎上把終止索引加1後即可解決該問題。

  • 問題2:實驗4,實驗4想了好久都沒有一點思路,我不清楚怎麼去使用樹,在哪使用樹,樹是用來儲存什麼的?

  • 問題2解決方案:我的想法一開始是落在把操作符所有的全部存在一棵樹上,根據它的優先順序去儲存,寫了之後才發現想的不對,因為這樣我沒有辦法再去把他們按照正確順序取出來。後面問了郭愷,他跟我說的是應該建立兩個棧,兩個棧中儲存的資料型別分別是String和樹型別,Sring型別的是操作符,樹型別的是運算元,他提供的這個思路解決了我的問題。終於把這個問題弄明白了,於是開始重新編寫。下面是關於操作符的優先順序處理:
if (isOperator(m)) {

                if (m.equals("*") || m.equals("/"))
                    stack.push(m);
                else if (stack.empty())
                    stack.push(m);
                else {
                        while (!stack.isEmpty()) {
                            String s1 = String.valueOf(stack.pop());
                            LinkedBinaryTree operand3 = linkedBinaryTreeStack.pop();
                            LinkedBinaryTree operand4 = linkedBinaryTreeStack.pop();
                            LinkedBinaryTree<String> linkedBinaryTree1 =
                            new LinkedBinaryTree<String>(s1, operand4, operand3);
                            linkedBinaryTreeStack.push(linkedBinaryTree1);
                            if (stack.isEmpty())
                                break;
                    }
                    stack.push(m);
                }
            }

       如果它是乘或除可以直接入棧,而當它是加或減時,需要把棧裡的比它優先順序高的取出來,取出來的時候需要同時從數棧裡取出兩個運算元,構成一棵新的樹,再放入樹的棧中,迴圈直至棧中沒有元素,然後再把該操作符放入棧中,這樣就可以實現了。

其他(感悟、思考等)

  • 本次實驗做的過程中讓我感覺最難的就是實驗4,自己一開始怎麼想都是沒有思路,在郭愷(非常感謝)給我的思路下,終於算是把這個實驗做完了。HashMap和TreeMap原始碼分析不動啊,太多了。

參考資料