1. 程式人生 > >20172328《程式設計與資料結構》實驗二:樹

20172328《程式設計與資料結構》實驗二:樹

20172328《程式設計與資料結構》實驗二:樹

  • 課程:《軟體結構與資料結構》
  • 班級: 1723
  • 姓名: 李馨雨
  • 學號:20172328
  • 實驗教師:王志強老師
  • 實驗日期:2018年11月5日-2018年11月12日
  • 必修選修: 必修

    一、實驗要求內容

  • 實驗1:實現二叉樹
  • 參考教材p212,完成鏈樹LinkedBinaryTree的實現(getRight,contains,toString,preorder,postorder)
    用JUnit或自己編寫驅動類對自己實現的LinkedBinaryTree進行測試,提交測試程式碼執行截圖,要全屏,包含自己的學號資訊
  • 實驗2:中序先序序列構造二叉樹
  • 基於LinkedBinaryTree,實現基於(中序,先序)序列構造唯一一棵二㕚樹的功能,比如給出中序HDIBEMJNAFCKGL和後序ABDHIEJMNCFGKL,構造出附圖中的樹,用JUnit或自己編寫驅動類對自己實現的功能進行測試,提交測試程式碼執行截圖,要全屏,包含自己的學號資訊
  • 實驗3:決策樹
  • 自己設計並實現一顆決策樹,提交測試程式碼執行截圖,要全屏,包含自己的學號資訊,課下把程式碼推送到程式碼託管平臺
  • 實驗4:表示式樹
  • 輸入中綴表示式,使用樹將中綴表示式轉換為字尾表示式,並輸出字尾表示式和計算結果(如果沒有用樹,則為0分),提交測試程式碼執行截圖,要全屏,包含自己的學號資訊,課下把程式碼推送到程式碼託管平臺
  • 實驗5:二叉查詢樹
  • 完成PP11.3,提交測試程式碼執行截圖,要全屏,包含自己的學號資訊,課下把程式碼推送到程式碼託管平臺
  • 實驗6 : 紅黑樹分析
  • 參考本部落格:點選進入對Java中的紅黑樹(TreeMap,HashMap)進行原始碼分析,並在實驗報告中體現分析結果。

二、實驗過程及結果

  • 實驗1:實現二叉樹的解決過程及結果

  • 實驗2:中序先序序列構造二叉樹的解決過程及結果

  • 實驗3:決策樹的解決過程及結果

  • 實驗4:表示式樹的解決過程及結果

  • 實驗5:二叉查詢樹的解決過程及結果

  • 實驗6 : 紅黑樹分析的解決過程及結果

    寫在前面:剛找到TreeMap和HashMap的原始碼,其實是有些慌張不知所措的,靜下心來看一看,發現其實是對很多方法的註釋很長,所以兩個原始碼都是很長。

  • 首先,我們先要去了解Map是啥?Key是啥?而Value又是啥?

  • 在陣列中我們是通過陣列下標來對其內容索引的,而在Map中我們通過物件來對物件進行索引,用來索引的物件叫做key,其對應的物件叫做value。這就是平時說的鍵值對Key - value。

  • HashMap和TreeMap最本質的區別:
    • HashMap通過hashcode方法對其內容進行快速查詢,而 TreeMap中所有的元素都保持著某種固定的順序,如果你需要得到一個有序的結果你就應該使用TreeMap,因為HashMap中元素的排列順序是不固定的。
  • HashMap 是一個散列表,它儲存的內容是鍵值對(key-value)對映。HashMap繼承於AbstractMap,實現了Map、Cloneable、java.io.Serializable介面。HashMap的實現不是同步的,這意味著它不是執行緒安全的。它的key、value都可以為null。此外,HashMap中的對映不是有序的。在HashMap中通過get()來獲取value,通過put()來插入value,ContainsKey()則用來檢驗物件是否已經存在。可以看出,和ArrayList的操作相比,HashMap除了通過key索引其內容之外,別的方面差異並不大。
  • TreeMap 是一個有序的key-value集合,它是通過紅黑樹實現的。TreeMap繼承於AbstractMap,所以它是一個Map,即一個key-value集合。TreeMap實現了NavigableMap介面,意味著它支援一系列的導航方法。比如返回有序的key集合。TreeMap實現了Cloneable介面,意味著它能被克隆。TreeMap實現了java.io.Serializable介面,意味著它支援序列化。TreeMap基於紅黑樹(Red-Blacktree)實現。該對映根據其鍵的自然順序進行排序,或者根據建立對映時提供的 Comparator 進行排序,具體取決於使用的構造方法。TreeMap的基本操作 containsKey、get、put 和 remove 的時間複雜度是 log(n) 。
    另外,TreeMap是非同步的。 它的iterator 方法返回的迭代器是fail-fastl的。

  • HashMap的原始碼分析
  • HashMap的建構函式
// 預設建構函式。
HashMap()
// 指定“容量大小”的建構函式
HashMap(int capacity)
// 指定“容量大小”和“載入因子”的建構函式
HashMap(int capacity, float loadFactor)
// 包含“子Map”的建構函式
HashMap(Map<? extends K, ? extends V> map)
  • 關於HashMap建構函式的理解:
    • HashMap遵循集合框架的約束,提供了一個引數為空的建構函式與有一個引數且引數型別為Map的建構函式。除此之外,還提供了兩個建構函式,用於設定HashMap的容量(capacity)與平衡因子(loadFactor)。
    • 從程式碼上可以看到,容量與平衡因子都有個預設值,並且容量有個最大值
    • 預設的平衡因子為0.75,這是權衡了時間複雜度與空間複雜度之後的最好取值(JDK說是最好的),過高的因子會降低儲存空間但是查詢(lookup,包括HashMap中的put與get方法)的時間就會增加。
  • HashMap的繼承關係
public class HashMap<K,V> extends AbstractMap<K,V>
    implements Map<K,V>, Cloneable, Serializable 
  • 關於HashMap繼承和實現的理解:
    • 標記介面Cloneable,用於表明HashMap物件會重寫java.lang.Object#clone()方法,HashMap實現的是淺拷貝(shallow copy)。
    • 標記介面Serializable,用於表明HashMap物件可以被序列化
    • HashMap是一種基於雜湊表(hash table)實現的map,雜湊表(也叫關聯陣列)一種通用的資料結構,大多數的現代語言都原生支援,其概念也比較簡單:key經過hash函式作用後得到一個槽(buckets或slots)的索引(index),槽中儲存著我們想要獲取的值.
  • HashMap的一些重要物件和方法
  • HashMap中存放的是HashMap.Entry物件,它繼承自Map.Entry,其比較重要的是建構函式。Entry實現了單向連結串列的功能,用next成員變數來級連起來。
static class Entry<K,V> implements Map.Entry<K,V> {
    final K key;
    V value;
    Entry<K,V> next;
    int hash;
    Entry(int h, K k, V v, Entry<K,V> n) {
        value = v;
        next = n;
        key = k;
        hash = h;
    }
    // setter, getter, equals, toString 方法省略
    public final int hashCode() {
        //用key的hash值與上value的hash值作為Entry的hash值
        return Objects.hashCode(getKey()) ^ Objects.hashCode(getValue());
    }
    /**
     * This method is invoked whenever the value in an entry is
     * overwritten by an invocation of put(k,v) for a key k that's already
     * in the HashMap.
     */
    void recordAccess(HashMap<K,V> m) {
    }
    /**
     * This method is invoked whenever the entry is
     * removed from the table.
     */
    void recordRemoval(HashMap<K,V> m) {
    }
    }
  • HashMap內部維護了一個為陣列型別的Entry變數table,用來儲存新增進來的Entry物件。其實這是解決衝突的一個方式:鏈地址法(開雜湊法)
transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;
  • get操作
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;
}
  • put操作(含update操作)
private void inflateTable(int toSize) {
    //輔助函式,用於填充HashMap到指定的capacity
    // Find a power of 2 >= toSize
    int capacity = roundUpToPowerOf2(toSize);
    //threshold為resize的閾值,超過後HashMap會進行resize,內容的entry會進行rehash
    threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
    table = new Entry[capacity];
    initHashSeedAsNeeded(capacity);
}
/**
 * Associates the specified value with the specified key in this map.
 * If the map previously contained a mapping for the key, the old
 * value is replaced.
 */
public V put(K key, V value) {
    if (table == EMPTY_TABLE) {
        inflateTable(threshold);
    }
    if (key == null)
        return putForNullKey(value);
    int hash = hash(key);
    int i = indexFor(hash, table.length);
    //這裡的迴圈是關鍵
    //當新增的key所對應的索引i,對應table[i]中已經有值時,進入迴圈體
    for (Entry<K,V> e = table[i]; e != null; e = e.next) {
        Object k;
        //判斷是否存在本次插入的key,如果存在用本次的value替換之前oldValue,相當於update操作
        //並返回之前的oldValue
        if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
            V oldValue = e.value;
            e.value = value;
            e.recordAccess(this);
            return oldValue;
        }
    }
    //如果本次新增key之前不存在於HashMap中,modCount加1,說明結構改變了
    modCount++;
    addEntry(hash, key, value, i);
    return null;
}
void addEntry(int hash, K key, V value, int bucketIndex) {
    //如果增加一個元素會後,HashMap的大小超過閾值,需要resize
    if ((size >= threshold) && (null != table[bucketIndex])) {
        //增加的幅度是之前的1倍
        resize(2 * table.length);
        hash = (null != key) ? hash(key) : 0;
        bucketIndex = indexFor(hash, table.length);
    }
    createEntry(hash, key, value, bucketIndex);
}
void createEntry(int hash, K key, V value, int bucketIndex) {
    //首先得到該索引處的衝突鏈Entries,有可能為null,不為null
    Entry<K,V> e = table[bucketIndex];
    //然後把新的Entry新增到衝突鏈的開頭,也就是說,後插入的反而在前面(第一次還真沒看明白)
    //需要注意的是table[bucketIndex]本身並不儲存節點資訊,
    //它就相當於是單向連結串列的頭指標,資料都存放在衝突鏈中。
    table[bucketIndex] = new Entry<>(hash, key, value, e);
    size++;
}
//下面看看HashMap是如何進行resize,廬山真面目就要揭曉了
void resize(int newCapacity) {
    Entry[] oldTable = table;
    int oldCapacity = oldTable.length;
    //如果已經達到最大容量,那麼就直接返回
    if (oldCapacity == MAXIMUM_CAPACITY) {
        threshold = Integer.MAX_VALUE;
        return;
    }
    Entry[] newTable = new Entry[newCapacity];
    //initHashSeedAsNeeded(newCapacity)的返回值決定了是否需要重新計算Entry的hash值
    transfer(newTable, initHashSeedAsNeeded(newCapacity));
    table = newTable;
    threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}
/**
 * Transfers all entries from current table to newTable.
 */
void transfer(Entry[] newTable, boolean rehash) {
    int newCapacity = newTable.length;
    //遍歷當前的table,將裡面的元素新增到新的newTable中
    for (Entry<K,V> e : table) {
        while(null != e) {
            Entry<K,V> next = e.next;
            if (rehash) {
                e.hash = null == e.key ? 0 : hash(e.key);
            }
            int i = indexFor(e.hash, newCapacity);
            e.next = newTable[i];
            //最後這兩句用了與put放過相同的技巧
            //將後插入的反而在前面
            newTable[i] = e;
            e = next;
        }
    }
}
/**
 * Initialize the hashing mask value. We defer initialization until we
 * really need it.
 */
final boolean initHashSeedAsNeeded(int capacity) {
    boolean currentAltHashing = hashSeed != 0;
    boolean useAltHashing = sun.misc.VM.isBooted() &&
            (capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
    //這裡說明了,在hashSeed不為0或滿足useAltHash時,會重算Entry的hash值
    //至於useAltHashing的作用可以參考下面的連結
    // http://stackoverflow.com/questions/29918624/what-is-the-use-of-holder-class-in-hashmap
    boolean switching = currentAltHashing ^ useAltHashing;
    if (switching) {
        hashSeed = useAltHashing
            ? sun.misc.Hashing.randomHashSeed(this)
            : 0;
    }
    return switching;
}
  • remove操作
public V remove(Object key) {
    Entry<K,V> e = removeEntryForKey(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;
}
  • TreeMap的原始碼分析
  • TreeMap的建構函式
// 預設建構函式。使用該建構函式,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) 
  • TreeMap的繼承關係
public class TreeMap<K,V>
    extends AbstractMap<K,V>
    implements NavigableMap<K,V>, Cloneable, java.io.Serializable
  • 關於TreeMap繼承和實現的理解:
    • TreeMap實現繼承於AbstractMap,並且實現了NavigableMap介面。
    • TreeMap的本質是R-B Tree(紅黑樹),它包含幾個重要的成員變數: root, size, comparator
    • root是紅黑數的根節點。它是Entry型別,Entry是紅黑數的結點,它包含了紅黑數的6個基本組成成分:key(鍵)、value(值)、left(左孩子)、right(右孩子)、parent(父節點)、color(顏色)。Entry結點根據key進行排序,Entry節點包含的內容為value。
    • 紅黑數排序時,根據Entry中的key進行排序;Entry中的key比較大小是根據比較器comparator來進行判斷的。size是紅黑數中結點的個數。
  • TreeMap的一些重要方法:
  • 是否包含key結點:
public boolean containsKey(Object key) {
        return getEntry(key) != null;
    }
  • 是否包含某個值:
public boolean containsValue(Object value) {
        for (Entry<K,V> e = getFirstEntry(); e != null; e = successor(e))
            if (valEquals(value, e.value))
                return true;
        return false;
    }
  • 返回某一TreeMap上的value值
public V get(Object key) {
        Entry<K,V> p = getEntry(key);
        return (p==null ? null : p.value);
    }
  • 將某一個特定的Map存入TreeMap並進行自動排序
public void putAll(Map<? extends K, ? extends V> map) {
        int mapSize = map.size();
        if (size==0 && mapSize!=0 && map instanceof SortedMap) {
            Comparator<?> c = ((SortedMap<?,?>)map).comparator();
            if (c == comparator || (c != null && c.equals(comparator))) {
                ++modCount;
                try {
                    buildFromSorted(mapSize, map.entrySet().iterator(),
                                    null, null);
                } catch (java.io.IOException | ClassNotFoundException cannotHappen) {
                }
                return;
            }
        }
        super.putAll(map);
    }
  • 返回某一個結點
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 (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;
    }
  • 然後TreeMap中就是NavigableMap API 的方法,SubMaps、public Methods、View classes、Red-black mechanics了。

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

  • 問題1:首先是在做實驗1的時候自己不能完整的輸出一個數,只能輸出包含樹根(有三個數)的二叉樹,連線在左孩子上的左子樹無法正常輸出。
  • 問題1的解決:通過詢問王文彬同學,他教我理解了我程式碼中存在的問題,其實是因為我構造了兩個子樹,但卻沒有連線在一起形成一個完整的樹,修改後將左子樹left加入整個樹的構造中就可以了。

  • 問題2:在實驗二我理解完前序輸出和中序輸出的奧妙之後,終於在苦苦的編碼戰鬥中寫完了程式時,測試一下,結果卻很適合給學長學姐們國考加油!

  • 解決過程:看圖說話

  • 問題3:在實驗三中我的決策樹被我獨具匠心的寫成了一個穿搭教程。但是但是在做的時候還是出現了BUG,當時我記成了Y是通向左子樹、N是通向右子樹,所以我出現了前言不接後語的問題,當時因為記反了但自己又不知道找了好久的問題。還有就是我當時多加了兩個回答語句體,在讀檔案的時候我把新連結的子樹順序放到了最後,結果出現了問題跳躍,不能銜接。
  • 通過認真的多次研究修改,終於我的決策樹完美出道了。

  • 問題4和問題4的解決:在做實驗4的時候沒有思路,看了郭愷同學的程式碼理解了一些,是建立了兩個棧,兩個棧中儲存的資料型別分別是String和樹型別,Sring型別來解決操作符,樹型別的來解決運算元。

其他(感悟、思考等)

我覺得很多東西理解和程式碼實現不是一回事,理解了我也不知道如何精確下手,但是在編寫的時候我又能更深刻的理解好多遍。雖然過程及其“撕心裂肺”,但是還是要多多受虐,才能在下次受虐的時候減輕疼痛。

四、參考資料