20172318 2017-2018-2 《程序設計與數據結構》實驗2報告

20172318 2017-2018-2 《程序設計與數據結構》實驗2報告

課程:《程序設計與數據結構》
班級: 1723
姓名: 陸大嶽
學號:20172318
實驗教師:王志強
實驗日期:2018年11月11日
必修/選修: 必修html

1.實驗內容

  • 實驗一:實現二叉樹
    參考教材p212,完成鏈樹LinkedBinaryTree的實現(getRight,contains,toString,preorder,postorder)
    用JUnit或本身編寫驅動類對本身實現的LinkedBinaryTree進行測試java

  • 實驗二:中序先序序列構造二叉樹
    基於LinkedBinaryTree,實現基於(中序,先序)序列構造惟一一棵二㕚樹的功能,好比給出中序HDIBEMJNAFCKGL和後序ABDHIEJMNCFGKL,構造出附圖中的樹
    用JUnit或本身編寫驅動類對本身實現的功能進行測試數組

  • 實驗三:決策樹
    本身設計並實現一顆決策樹安全

  • 實驗四:表達式樹
    輸入中綴表達式,使用樹將中綴表達式轉換爲後綴表達式,並輸出後綴表達式和計算結果(若是沒有用樹,則爲0分)數據結構

  • 實驗五:二叉查找樹
    完成PP11.3併發

  • 實驗六:紅黑樹分析
    參考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)app

2. 實驗過程及結果

第一部分 實現二叉樹

第二部分 中序先序序列構造二叉樹

第三部分 決策樹

第四部分 表達式樹





第五部分 二叉查找樹



第六部分 紅黑樹分析

TreeMap 簡介
TreeMap 是一個有序的key-value集合,它是經過紅黑樹實現的。
TreeMap 繼承於AbstractMap,因此它是一個Map,即一個key-value集合。
TreeMap 實現了NavigableMap接口,意味着它支持一系列的導航方法。好比返回有序的key集合。
TreeMap 實現了Cloneable接口,意味着它能被克隆。
TreeMap 實現了java.io.Serializable接口,意味着它支持序列化。
TreeMap基於紅黑樹(Red-Black tree)實現。該映射根據其鍵的天然順序進行排序,或者根據建立映射時提供的 Comparator 進行排序,具體取決於使用的構造方法。
TreeMap的基本操做 containsKey、get、put 和 remove 的時間複雜度是 log(n) 。
另外,TreeMap是非同步的。 它的iterator 方法返回的迭代器是fail-fastl的。函數

  • TreeMap源碼剖析
    存儲結構
    TreeMap的排序是基於對key的排序實現的,它的每個Entry表明紅黑樹的一個節點,Entry的數據結構以下:
static final class Entry<K,V> implements Map.Entry<K,V> {    
     // 鍵    
     K key;    
     // 值    
     V value;    
     // 左孩子    
     Entry<K,V> left = null;    
     // 右孩子    
     Entry<K,V> right = null;    
     // 父節點    
     Entry<K,V> parent;    
     // 當前節點顏色    
     boolean color = BLACK;    
  
     // 構造函數    
     Entry(K key, V value, Entry<K,V> parent) {    
         this.key = key;    
         this.value = value;    
         this.parent = parent;    
     }    
。。。。。  
}

構造方法
先來看下TreeMap的構造方法。TreeMap一共有4個構造方法。源碼分析

一、無參構造方法
public TreeMap() {    
    comparator = null;    
}
採用無參構造方法,不指定比較器,這時候,排序的實現要依賴key.compareTo()方法,所以key必須實現Comparable接口,並覆寫其中的compareTo方法。
二、帶有比較器的構造方法
public TreeMap(Comparator<? super K> comparator) {    
    this.comparator = comparator;    
}
採用帶比較器的構造方法,這時候,排序依賴該比較器,key能夠不用實現Comparable接口。
三、帶Map的構造方法
public TreeMap(Map<? extends K, ? extends V> m) {    
    comparator = null;    
    putAll(m);    
}
該構造方法一樣不指定比較器,調用putAll方法將Map中的全部元素加入到TreeMap中。putAll的源碼以下:
// 將map中的所有節點添加到TreeMap中    
public void putAll(Map<? extends K, ? extends V> map) {    
    // 獲取map的大小    
    int mapSize = map.size();    
    // 若是TreeMap的大小是0,且map的大小不是0,且map是已排序的「key-value對」    
    if (size==0 && mapSize!=0 && map instanceof SortedMap) {    
        Comparator c = ((SortedMap)map).comparator();    
        // 若是TreeMap和map的比較器相等;    
        // 則將map的元素所有拷貝到TreeMap中,而後返回!    
        if (c == comparator || (c != null && c.equals(comparator))) {    
            ++modCount;    
            try {    
                buildFromSorted(mapSize, map.entrySet().iterator(),    
                            null, null);    
            } catch (java.io.IOException cannotHappen) {    
            } catch (ClassNotFoundException cannotHappen) {    
            }    
            return;    
        }    
    }    
    // 調用AbstractMap中的putAll();    
    // AbstractMap中的putAll()又會調用到TreeMap的put()    
    super.putAll(map);    
}   
    顯然,若是Map裏的元素是排好序的,就調用buildFromSorted方法來拷貝Map中的元素,這在下一個構造方法中會重點說起,而若是Map中的元素不是排好序的,就調用AbstractMap的putAll(map)方法,該方法源碼以下:

[java] view plain copy
public void putAll(Map<? extends K, ? extends V> m) {    
    for (Map.Entry<? extends K, ? extends V> e : m.entrySet())    
        put(e.getKey(), e.getValue());    
}   
    很明顯它是將Map中的元素一個個put(插入)到TreeMap中的,主要由於Map中的元素是無序存放的,所以要一個個插入到紅黑樹中,使其有序存放,並知足紅黑樹的性質。
    四、帶有SortedMap的構造方法


[java] view plain copy
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) {    
    }    
}
首先將比較器指定爲m的比較器,這取決於生成m時調用構造方法是否傳入了指定的構造器,然後調用buildFromSorted方法,將SortedMap中的元素插入到TreeMap中,因爲SortedMap中的元素師有序的,實際上它是根據SortedMap建立的TreeMap,將SortedMap中對應的元素添加到TreeMap中。

插入刪除
插入操做即對應TreeMap的put方法,put操做實際上只需按照二叉排序樹的插入步驟來操做便可,插入到指定位置後,再作調整,使其保持紅黑樹的特性。put源碼的實現:post

public V put(K key, V value) {    
    Entry<K,V> t = root;    
    // 若紅黑樹爲空,則插入根節點    
    if (t == null) {    
    // TBD:    
    // 5045147: (coll) Adding null to an empty TreeSet should    
    // throw NullPointerException    
    //    
    // compare(key, key); // type check    
        root = new Entry<K,V>(key, value, null);    
        size = 1;    
        modCount++;    
        return null;    
    }    
    int cmp;    
    Entry<K,V> parent;    
    // split comparator and comparable paths    
    Comparator<? super K> cpr = comparator;    
    // 找出(key, value)在二叉排序樹中的插入位置。    
    // 紅黑樹是以key來進行排序的,因此這裏以key來進行查找。    
    if (cpr != null) {    
        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 {    
        if (key == null)    
            throw new NullPointerException();    
        Comparable<? super K> k = (Comparable<? super K>) key;    
        do {    
            parent = t;    
            cmp = k.compareTo(t.key);    
            if (cmp < 0)    
                t = t.left;    
            else if (cmp > 0)    
                t = t.right;    
            else   
                return t.setValue(value);    
        } while (t != null);    
    }    
    // 爲(key-value)新建節點    
    Entry<K,V> e = new Entry<K,V>(key, value, parent);    
    if (cmp < 0)    
        parent.left = e;    
    else   
        parent.right = e;    
    // 插入新的節點後,調用fixAfterInsertion調整紅黑樹。    
    fixAfterInsertion(e);    
    size++;    
    modCount++;    
    return null;    
}
這裏的fixAfterInsertion即是節點插入後對樹進行調整的方法

刪除操做及對應TreeMap的deleteEntry方法,deleteEntry方法一樣也只需按照二叉排序樹的操做步驟實現便可,刪除指定節點後,再對樹進行調整便可。deleteEntry方法的實現源碼以下:

// 刪除「紅黑樹的節點p」    
private void deleteEntry(Entry<K,V> p) {    
    modCount++;    
    size--;    
    if (p.left != null && p.right != null) {    
        Entry<K,V> s = successor (p);    
        p.key = s.key;    
        p.value = s.value;    
        p = s;    
    }   
    Entry<K,V> replacement = (p.left != null ? p.left : p.right);    
    if (replacement != null) {    
        replacement.parent = p.parent;    
        if (p.parent == null)    
            root = replacement;    
        else if (p == p.parent.left)    
            p.parent.left  = replacement;    
        else   
            p.parent.right = replacement;    
  
        p.left = p.right = p.parent = null;    
  
        if (p.color == BLACK)    
            fixAfterDeletion(replacement);    
    } else if (p.parent == null) {   
        root = null;    
    } else {  
        if (p.color == BLACK)    
            fixAfterDeletion(p);    
  
        if (p.parent != null) {    
            if (p == p.parent.left)    
                p.parent.left = null;    
            else if (p == p.parent.right)    
                p.parent.right = null;    
            p.parent = null;    
        }    
    }    
}

HashMap簡介
HashMap 是一個散列表,它存儲的內容是鍵值對(key-value)映射。
HashMap 繼承於AbstractMap,實現了Map、Cloneable、java.io.Serializable接口。
HashMap 的實現不是同步的,這意味着它不是線程安全的。它的key、value均可覺得null。此外,HashMap中的映射不是有序的。
HashMap 的實例有兩個參數影響其性能:「初始容量」 和 「加載因子」。容量 是哈希表中桶的數量,初始容量 只是哈希表在建立時的容量。加載因子 是哈希表在其容量自動增長以前能夠達到多滿的一種尺度。當哈希表中的條目數超出了加載因子與當前容量的乘積時,則要對該哈希表進行 rehash 操做(即重建內部數據結構),從而哈希表將具備大約兩倍的桶數。
一般,默認加載因子是 0.75, 這是在時間和空間成本上尋求一種折衷。加載因子太高雖然減小了空間開銷,但同時也增長了查詢成本(在大多數 HashMap 類的操做中,包括 get 和 put 操做,都反映了這一點)。在設置初始容量時應該考慮到映射中所需的條目數及其加載因子,以便最大限度地減小 rehash 操做次數。若是初始容量大於最大條目數除以加載因子,則不會發生 rehash 操做。

  • HashMap實現原理
    HashMap的主幹是一個Entry數組。Entry是HashMap的基本組成單元,每個Entry包含一個key-value鍵值對。
transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;

Entry是HashMap中的一個靜態內部類。代碼以下

static class Entry<K,V> implements Map.Entry<K,V> {
        final K key;
        V value;
        Entry<K,V> next;//存儲指向下一個Entry的引用,單鏈表結構
        int hash;//對key的hashcode值進行hash運算後獲得的值,存儲在Entry,避免重複計算

        /**
         * Creates new entry.
         */
        Entry(int h, K k, V v, Entry<K,V> n) {
            value = v;
            next = n;
            key = k;
            hash = h;
        }

其餘幾個重要字段

//實際存儲的key-value鍵值對的個數
transient int size;
//閾值,當table == {}時,該值爲初始容量(初始容量默認爲16);當table被填充了,也就是爲table分配內存空間後,threshold通常爲 capacity*loadFactory。HashMap在進行擴容時須要參考threshold,後面會詳細談到
int threshold;
//負載因子,表明了table的填充度有多少,默認是0.75
final float loadFactor;
//用於快速失敗,因爲HashMap非線程安全,在對HashMap進行迭代時,若是期間其餘線程的參與致使HashMap的結構發生變化了(好比put,remove等操做),須要拋出異常ConcurrentModificationException
transient int modCount;

put操做的實現

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;
    }

inflateTable方法

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);
    }

  inflateTable這個方法用於爲主幹數組table在內存中分配存儲空間,經過roundUpToPowerOf2(toSize)能夠確保capacity爲大於或等於toSize的最接近toSize的二次冪,好比toSize=13,則capacity=16;to_size=16,capacity=16;to_size=17,capacity=32.

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;
    }

roundUpToPowerOf2中的這段處理使得數組長度必定爲2的次冪,Integer.highestOneBit是用來獲取最左邊的bit(其餘bit位爲0)所表明的數值.

hash函數

//這是一個神奇的函數,用了不少的異或,移位等運算,對key的hashcode進一步進行計算以及二進制位的調整等來保證最終獲取的存儲位置儘可能分佈均勻
final int hash(Object k) {
        int h = hashSeed;
        if (0 != h && k instanceof String) {
            return sun.misc.Hashing.stringHash32((String) k);
        }

        h ^= k.hashCode();

        h ^= (h >>> 20) ^ (h >>> 12);
        return h ^ (h >>> 7) ^ (h >>> 4);
    }

以上hash函數計算出的值,經過indexFor進一步處理來獲取實際的存儲位置

  /**
     * 返回數組下標
     */
    static int indexFor(int h, int length) {
        return h & (length-1);
    }

h&(length-1)保證獲取的index必定在數組範圍內,

HashMap和TreeMap比較

(1)HashMap:適用於在Map中插入、刪除和定位元素。
(2)Treemap:適用於按天然順序或自定義順序遍歷鍵(key)。
(3)HashMap一般比TreeMap快一點(樹和哈希表的數據結構使然),建議多使用HashMap,在須要排序的Map時候才用TreeMap.
(4)HashMap 非線程安全 TreeMap 非線程安全
(5)HashMap的結果是沒有排序的,而TreeMap輸出的結果是排好序的。

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

問題1:

問題1解決方案:將樹的實現順序更改一下

其餘(感悟、思考等)

參考資料

相關文章
相關標籤/搜索