20172308 實驗二《程序設計與數據結構》樹 實驗報告

20172308 2018-2019-1 實驗2 《線性結構》報告

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

1.實驗內容

  • (1)樹之實現二叉樹:完成鏈樹LinkedBinaryTree的實現(getRight,contains,toString,preorder,postorder),並測試
  • (2)樹之中序先序序列構造二叉樹:基於LinkedBinaryTree,實現基於(中序,先序)序列構造惟一一棵二㕚樹的功能,並測試
  • (3)樹之決策樹:本身設計並實現一顆決策樹
  • (4)樹之表達式樹:輸入中綴表達式,使用樹將中綴表達式轉換爲後綴表達式,並輸出後綴表達式和計算結果(若是沒有用樹,則爲0分)
  • (5)樹之二叉查找樹:完成PP11.3
  • (6)樹之紅黑樹分析:參考相關資料對Java中的紅黑樹(TreeMap,HashMap)進行源碼分析,並在實驗報告中體現分析結果

2. 實驗過程及結果

  • (1)樹之實現二叉樹:
    1.完成鏈樹LinkedBinaryTree的方法:getRight,contains,toString,preorder,postorder
    2.對LinkedBinaryTree類進行測試
    3.實驗結果截圖:

  • (2)樹之中序先序序列構造二叉樹:
    1.基於LinkedBinaryTree,實現基於(中序,先序)序列構造惟一一棵二㕚樹的功能
    2.測試LinkedBinaryTree類
    3.實驗結果截圖:

  • (3)樹之決策樹:
    1.根據課本背部疼痛診斷設計一顆新的決策樹
    2.測試新的決策樹
    4.實驗結果截圖:

  • (4)樹之表達式樹:
    1.利用樹設計並實現中綴轉後綴表達式類
    2.利用樹設計並實現後綴表達式計算結果類
    3.並輸出後綴表達式和計算結果
    4.測試中綴轉後綴及計算結果類
    5.實驗結果截圖:

  • (5)樹之二叉查找樹:完成PP11.3
    1.鏈表實現的二叉樹,設計實現removeMax,findMin,findMax操做
    2.測試類
    3.實驗結果截圖:

  • (6)樹之紅黑樹分析:
    1.參考http://www.cnblogs.com/rocedu/p/7483915.html對Java中的紅黑樹(TreeMap,HashMap)進行源碼分析
    2.在實驗報告中體現分析結果

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

  • 問題1:實驗2.2:利用中序和先序構造出一棵樹並輸出,其實這個原理明白,但實現代碼的時候殊不知道從哪裏下手java

  • 問題1解決過程:
  • 構造出樹的原理就是:
    由先序遍歷的特色知道最早訪問的是根結點(結點),而後是左右子樹
    再由中序遍歷的結果找到根結點,並獲得左右子樹
    再返回先序遍歷,找到根結點左或右子樹的結點,再返回中序遍歷結果中找到此結點左右子樹
    如此遞歸下去......node

  • 舉個栗子:
    先序遍歷:   GDAFEMHZ
    中序遍歷:   ADEFGHMZ
    畫出樹:
        第一步,根據先序遍歷的特色,咱們知道根結點爲Ggit

     第二步,觀察中序遍歷ADEFGHMZ。其中root節點G左側的ADEF必然是root的左子樹,G右側的HMZ必然是root的右子樹。算法

     第三步,觀察左子樹ADEF,左子樹的中的根節點必然是大樹的root的leftchild。在先序遍歷中,大樹的root的leftchild位於root以後,因此左子樹的根節點爲D。數組

     第四步,一樣的道理,root的右子樹節點HMZ中的根節點也能夠經過前序遍歷求得。在先序遍歷中,必定是先把root和root的全部左子樹節點遍歷完以後纔會遍歷右子樹,而且遍歷的左子樹的第一個節點就是左子樹的根節點。同理,遍歷的右子樹的第一個節點就是右子樹的根節點。安全

     第五步,觀察發現,上面的過程是遞歸的。先找到當前樹的根節點,而後劃分爲左子樹,右子樹,而後進入左子樹重複上面的過程,而後進入右子樹重複上面的過程。最後就能夠還原一棵樹了。數據結構

該步遞歸的過程能夠簡潔表達以下:
1 肯定根,肯定左子樹,肯定右子樹
2 在左子樹中遞歸
3 在右子樹中遞歸
4 打印當前根
而後能夠畫出這個二叉樹的形狀:
併發

  • 而後是實現代碼:
public void buildTree(T[] inorder, T[] postorder) {//調用makeTree方法,即利用遞歸方法獲得樹
        BinaryTreeNode temp = makeTree(inorder, 0, inorder.length, postorder, 0, postorder.length);
        root = temp;

這個是調用了makeTree方法,以先序和中序做爲形參從而構造出樹的方法
下面是具體的makeTree方法實現:app

public BinaryTreeNode<T> makeTree(T[] inorder, int startInorder, int lenInorder, T[] preorder, int startPreorder, int lenPreorder) {
        if (lenInorder < 1) {//判斷中序的字符串長度(即元素個數)小於1,則返回null,即樹爲空
            return null;
        }
        BinaryTreeNode root;//建立根結點
        T rootelement = preorder[startPreorder];//preorder中的第一個元素就是當前處理的數據段的根節點
        root = new BinaryTreeNode(rootelement);//把給定的根結點元素放進root
        int temp;
        boolean isFound = false;
        for (temp = 0; temp < lenInorder; temp++) {
            if (inorder[startInorder + temp] == rootelement) {
                isFound = true;//此時找到結點,即將先序中的第一個元素(根元素)在中序中尋找到相同元素,獲得根結點的左右子樹
                break;
            }
        }
        if (!isFound)//若是不存在相等的狀況就跳出該函數
            return root;//即沒有左右子樹
        root.setLeft(makeTree(inorder, startInorder, temp, preorder, startPreorder + 1, temp));//遞歸找到並設置各結點左孩子
        root.setRight(makeTree(inorder, startInorder + temp + 1, lenInorder - temp - 1, preorder, startPreorder + temp + 1, lenPreorder - temp - 1));//遞歸找到並設置各結點右孩子
        //每次遞歸,先序序列中都要日後跳過temp個元素(由於他們都是從中序序列中得知的一個結點的左或右孩子),再加1,即爲下一個結點的元素
        return root;
    }

上面的代碼實現是參考並理解了餘坤澎同窗的代碼以後進行了細微修改獲得的,並做出了代碼註釋

【參考資料】
餘坤澎同窗的碼雲連接
Java實現二叉樹先序,中序,後序遍歷
二叉樹前序、中序、後序遍歷相互求法 (原理,程序)

  • 問題2:實驗2.3:本身設計一顆決策樹

  • 問題2解決過程:
    參考課本上的背部疼痛診斷類,在節點處放置問題,根據用戶輸入的Y或N,對應不一樣的左右孩子,並返回當前節點的元素(即下一個問題內容)
    即達到設計要求
    設計想法有兩種:
    一是改變書上決策樹的子樹狀況,設計其它的問題填充進去
    二是直接改變原子樹中的節點內容,使問題問題銜接的更緊湊(雖然這一種有點偷懶,但也是要在理解原決策樹的結構基礎上才能正確更改,不然會出現答非所問的狀況)

  • 問題3:實驗2.4:輸入中綴表達式,使用樹將中綴表達式轉換爲後綴表達式,並輸出後綴表達式和計算結果

  • 問題3解決過程:
    這個實驗應該是六個實驗裏面最很差作的一個,其中最關鍵的問題是如何將中綴轉換成後綴表達式,即如何把中綴裏面的操做數和操做符存儲在樹中,而後輸出的樹即爲後綴表達式
    後面的後綴表達式經過課本上的後綴表達式計算出結果便可

首先,第一個問題:如何構造出後綴表達式樹
表達式樹的特色:樹的樹葉是操做數(常數或變量),而其餘節點爲操做符
每次找到「最後計算」的運算符,做爲當前根節點,運算符左側表達式做爲左節點,右側表達式做爲右節點,而後遞歸處理

  • 舉個栗子:9+(3-1)*3+10/2對應的二叉樹的構造過程以下圖所示:

    此二叉樹作後序遍歷就獲得了後綴表達式

而後,這裏就存在一個優先級的問題,也就是存儲操做符時,先存儲哪一個才能保證輸出表達式時是正確的
咱們知道乘除的運算級要高於加減,因此要先按順序存儲乘除的運算符及其兩邊的操做數,而後將其做爲一個節點放在原來的列表位置,最後再存儲加減及其兩邊的操做數
這一段的代碼實現以下:

while (operList.size() > 0) {    //第三步,重複第二步,直到操做符取完爲止
            //第二,取出前兩個數字和一個操做符,組成一個新的數字節點
            for (int a = 0; a < operList.size(); a++){
               if(operList.get(a).equals("*") || operList.get(a).equals("/")){
                   Node left = numList.remove(a);
                   Node right = numList.remove(a);
                   String oper = operList.remove(a);
                   Node node = new Node(oper, left, right);
                   numList.add(a, node);
                   a--;
               }
               else
                   time++;
            }
            Node left = numList.remove(0);
            Node right = numList.remove(0);
            String oper = operList.remove(0);
            Node node = new Node(oper, left, right);

            numList.add(0, node);       //將新生的節點做爲第一個節點,同時之前index=0的節點變爲index=1
        }

經過在一個循環裏面判斷操做符有沒有乘除一級的運算,而後執行上述的相應操做
在for循環外面再按順序對加減一級的操做符進行相應操做,便可達到轉後綴的要求

最後一個,代碼的問題:在寫代碼的時候,會報出下圖中的錯誤

經過debug發現問題所在,是這個for循環的問題,這個循環不能正常結束,會超出範圍
那麼問題就是,爲何會不能正常結束循環?

這裏的變量a是當前乘除一級的操做符在列表中的索引值(這裏的操做數和操做符是存儲在列表裏的)
因此就致使了問題出現:ArrayList沒有空位置,刪除後的元素會被後面的元素自動補全
因此當一個乘除一級運算不少的中綴表達式,在找到乘除運算符的索引時,對應操做數中的索引值可能已經超出了循環的終止條件count的值(count是操做符的個數)
count是個定值,沒法保證對全部的表達式都能正確轉換成後綴表達式
通過不少次嘗試和思考,才找到了最終的終止條件,那就是上面給出的代碼 a < operList.size() 操做符的元素個數會變化,這裏用size()方法就解決了

【參考資料】
前綴,中綴,後綴表達式學習筆記(1)
前綴、中綴、後綴表達式和二叉樹
二叉樹應用——後綴表達式構建表達式樹
用二叉樹表示表達式
中綴表達式轉後綴表達式---棧--二叉樹---四則運算

樹之紅黑樹分析結果報告

1、首先要了解一下什麼是treeMap和HashMap
Map:在數組中咱們是經過數組下標來對其內容索引的,而在Map中咱們經過對象來對對象進行索引,用來索引的對象叫作key,其對應的對象叫作value(就是咱們平時說的鍵值對)

2、 HashMap
HashMap 是基於哈希表的 Map 接口的實現。此實現提供全部可選的映射操做,並容許使用 null 值和 null 鍵。(除了非同步和容許使用 null 以外,HashMap 類與 Hashtable 大體相同。)此類不保證映射的順序,特別是它不保證該順序恆久不變。

官方文檔

官方文檔以下:
此實現假定哈希函數將元素適當地分佈在各桶之間,可爲基本操做(get 和 put)提供穩定的性能。迭代 collection 視圖所需的時間與 HashMap 實例的「容量」(桶的數量)及其大小(鍵-值映射關係數)成比例。因此,若是迭代性能很重要,則不要將初始容量設置得過高(或將加載因子設置得過低)。

HashMap 的實例有兩個參數影響其性能:初始容量 和加載因子。容量 是哈希表中桶的數量,初始容量只是哈希表在建立時的容量。加載因子 是哈希表在其容量自動增長以前能夠達到多滿的一種尺度。當哈希表中的條目數超出了加載因子與當前容量的乘積時,則要對該哈希表進行 rehash 操做(即重建內部數據結構),從而哈希表將具備大約兩倍的桶數。

一般,默認加載因子 (.75) 在時間和空間成本上尋求一種折衷。加載因子太高雖然減小了空間開銷,但同時也增長了查詢成本(在大多數 HashMap 類的操做中,包括 get 和 put 操做,都反映了這一點)。在設置初始容量時應該考慮到映射中所需的條目數及其加載因子,以便最大限度地減小 rehash 操做次數。若是初始容量大於最大條目數除以加載因子,則不會發生 rehash 操做。

若是不少映射關係要存儲在 HashMap 實例中,則相對於按需執行自動的 rehash 操做以增大表的容量來講,使用足夠大的初始容量建立它將使得映射關係能更有效地存儲。

注意,此實現不是同步的。若是多個線程同時訪問一個哈希映射,而其中至少一個線程從結構上修改了該映射,則它必須 保持外部同步。(結構上的修改是指添加或刪除一個或多個映射關係的任何操做;僅改變與實例已經包含的鍵關聯的值不是結構上的修改。)這通常經過對天然封裝該映射的對象進行同步操做來完成。若是不存在這樣的對象,則應該使用 Collections.synchronizedMap 方法來「包裝」該映射。最好在建立時完成這一操做,以防止對映射進行意外的非同步訪問,以下所示:

Map m = Collections.synchronizedMap(new HashMap(…));
由全部此類的「collection 視圖方法」所返回的迭代器都是快速失敗 的:在迭代器建立以後,若是從結構上對映射進行修改,除非經過迭代器自己的 remove 方法,其餘任什麼時候間任何方式的修改,迭代器都將拋出 ConcurrentModificationException。所以,面對併發的修改,迭代器很快就會徹底失敗,而不冒在未來不肯定的時間發生任意不肯定行爲的風險。

HashMap中有三個關於紅黑樹的關鍵參數

//一個桶的樹化閾值
//當桶中元素個數超過這個值時,須要使用紅黑樹節點替換鏈表節點
//這個值必須爲 8,要否則頻繁轉換效率也不高
static final int TREEIFY_THRESHOLD = 8;
//一個樹的鏈表還原閾值
//當擴容時,桶中元素個數小於這個值,就會把樹形的桶元素 還原(切分)爲鏈表結構
//這個值應該比上面那個小,至少爲 6,避免頻繁轉換
static final int UNTREEIFY_THRESHOLD = 6;
//哈希表的最小樹形化容量
//當哈希表中的容量大於這個值時,表中的桶才能進行樹形化
//不然桶內元素太多時會擴容,而不是樹形化
//爲了不進行擴容、樹形化選擇的衝突,這個值不能小於 4 * TREEIFY_THRESHOLD
static final int MIN_TREEIFY_CAPACITY = 64;

treeifyBin

HashMap中樹形化最重要的一個方法treeifyBin() 即樹形化。在一個桶中的元素個數超過 TREEIFY_THRESHOLD(默認是8),就使用紅黑樹來替換鏈表。

final void treeifyBin(Node<K,V>[] tab, int hash) {
        int n, index; Node<K,V> e;
        //若是hash表爲空或者hash表的容量小於MIN_TREEIFY_CAPACITY(64),那麼就去新建或者擴容
        if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
            resize();
        else if ((e = tab[index = (n - 1) & hash]) != null) {
            TreeNode<K,V> hd = null, tl = null;
            do {
                //新建一個樹形節點,內容和當前鏈表節點一致
                TreeNode<K,V> p = replacementTreeNode(e, null);
                if (tl == null)
                    hd = p;   //頭節點
                else {
                    p.prev = tl;
                    tl.next = p;
                }
                tl = p;
            } while ((e = e.next) != null);
            //以前獲得的只是一個鏈表狀的二叉樹,下一步格式化紅黑樹
            if ((tab[index] = hd) != null)
                hd.treeify(tab);
        }
}
TreeNode<K,V> replacementTreeNode(Node<K,V> p, Node<K,V> next) {
        return new TreeNode<>(p.hash, p.key, p.value, next);
    }

HashMap類屬性及構造函數

public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable {

    // 序列號

    private static final long serialVersionUID = 362498820763181265L;    

    // 默認的初始容量是16

    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;   

    // 最大容量

    static final int MAXIMUM_CAPACITY = 1 << 30; 

    // 默認的填充因子

    static final float DEFAULT_LOAD_FACTOR = 0.75f;

    // 當桶(bucket)上的結點數大於這個值時會轉成紅黑樹

    static final int TREEIFY_THRESHOLD = 8; 

    // 當桶(bucket)上的結點數小於這個值時樹轉鏈表

    static final int UNTREEIFY_THRESHOLD = 6;

    // 桶中結構轉化爲紅黑樹對應的table的最小大小

    static final int MIN_TREEIFY_CAPACITY = 64;

    // 存儲元素的數組,老是2的冪次倍

    transient Node<k,v>[] table; 

    // 存放具體元素的集

    transient Set<map.entry<k,v>> entrySet;

    // 存放元素的個數,注意這個不等於數組的長度。

    transient int size;

    // 每次擴容和更改map結構的計數器

    transient int modCount;   

    // 臨界值 當實際大小(容量*填充因子)超過臨界值時,會進行擴容

    int threshold;

    // 填充因子

    final float loadFactor;

}

HashMap類的主要方法

1.putVal函數(put操做的基礎函數)

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,

                   boolean evict) {

        Node<K,V>[] tab; Node<K,V> p; int n, i;

        //檢測table是否爲空,若是爲空,則使用擴容函數進行初始化

        if ((tab = table) == null || (n = tab.length) == 0)

            n = (tab = resize()).length;

        //若是經過hash值取模獲得的桶爲空,則直接把新生成的節點放入該桶

        if ((p = tab[i = (n - 1) & hash]) == null)

            tab[i] = newNode(hash, key, value, null);

        else {//如下爲該桶不爲空的邏輯

            Node<K,V> e; K k;

            //判斷桶的第一個元素的key值是否相同(hash值相同,且能equals)

            //若是相同,則返回當前元素(函數末尾進行統一處理)

            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;

                    }

                    //若是在遍歷過程當中,發現了key值相同,則返回當前元素(函數末尾進行統一處理)

                    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;

                //若是onlyIfAbsent爲ture,則在oldValue爲空時才替換

                //不然直接替換

                if (!onlyIfAbsent || oldValue == null)

                    e.value = value;

                afterNodeAccess(e);

                return oldValue;

            }

        }

        ++modCount;//修改次數+1

        //map的size加1,而後判斷是否達到了threshold,不然進行擴容

        //threshold由Node[] table的長度及loadFactor控制

        if (++size > threshold)

            resize();

        //執行回調函數

        afterNodeInsertion(evict);

        return null;

    }

2.put函數

public V put(K key, V value) {

        return putVal(hash(key), key, value, false, true);

    }

3.getNode函數(get操做的基礎函數)

final Node<K,V> getNode(int hash, Object key) {

        Node<K,V>[] tab; Node<K,V> first, e; int n; K k;

        //若是table不爲空,則再進行查詢操做

        if ((tab = table) != null && (n = tab.length) > 0 &&

            (first = tab[(n - 1) & hash]) != null) {

            //先檢查第一個元素是否key相同

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

            }

        }

        return null;

    }

4.resize()函數

final Node<K,V>[] resize() {

        Node<K,V>[] oldTab = table;

        int oldCap = (oldTab == null) ? 0 : oldTab.length;

        int oldThr = threshold;

        int newCap, newThr = 0;

        if (oldCap > 0) {

            //若是擴容以前的容量已經達到了最大值

            //則只把threshold變成Integer.MAX_VALUE,即不限制map的最大size,以後無論插入多少元素也不觸發resize

            if (oldCap >= MAXIMUM_CAPACITY) {

                threshold = Integer.MAX_VALUE;

                return oldTab;

            }//把新容量變成原來的2倍

            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&

                     oldCap >= DEFAULT_INITIAL_CAPACITY)

                newThr = oldThr << 1; // double threshold

        }

        else if (oldThr > 0) // 初始capacity(構造函數輸入的)被設置成了threshold

            newCap = oldThr;

        else { // oldThr=0的狀況,此時代表採用默認的參數進行初始化

            newCap = DEFAULT_INITIAL_CAPACITY;

            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);

        }

        if (newThr == 0) {

            float ft = (float)newCap * loadFactor;

            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?

                      (int)ft : Integer.MAX_VALUE);

        }

        //處理好newThr和newCap以後,開始resize()函數的真正邏輯

        threshold = newThr;//設置threshold

        @SuppressWarnings({"rawtypes","unchecked"})

            Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];

        table = newTab;

        if (oldTab != null) {

            for (int j = 0; j < oldCap; ++j) {

                Node<K,V> e;

                //若是原來桶的首元素不爲空,則進行復制邏輯

                if ((e = oldTab[j]) != null) {

                    oldTab[j] = null;

                    //若是該桶只裝了一個元素,則直接複製

                    if (e.next == null)

                        newTab[e.hash & (newCap - 1)] = e;

                    else if (e instanceof TreeNode)//紅黑樹的狀況

                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);

                    else { 

                    // 鏈式狀況,此時會把原來的鏈分紅兩個鏈loHead和hiHead

                    //loHead存儲(e.hash & oldCap) == 0的元素

                    //hiHead存儲(e.hash & oldCap) != 0的元素

                    //擴容以後,因爲新的capacity爲oldCap的2倍,且它們都爲2的整數冪

                    //對於該鏈上的元素,若是(e.hash & oldCap) == 0,則新的槽位(hash%capacity)==舊的槽位(hash%oldCap)

                    //不然,它們新的槽位都同樣,且都爲原來的槽位後移oldCap

                        Node<K,V> loHead = null, loTail = null;

                        Node<K,V> hiHead = null, hiTail = null;

                        Node<K,V> next;

                        do {

                            next = e.next;

                            if ((e.hash & oldCap) == 0) {

                                if (loTail == null)

                                    loHead = e;

                                else

                                    loTail.next = e;

                                loTail = e;

                            }

                            else {

                                if (hiTail == null)

                                    hiHead = e;

                                else

                                    hiTail.next = e;

                                hiTail = e;

                            }

                        } while ((e = next) != null);

                        if (loTail != null) {

                            loTail.next = null;

                            newTab[j] = loHead;

                        }

                        if (hiTail != null) {

                            hiTail.next = null;

                            newTab[j + oldCap] = hiHead;

                        }

                    }

                }

            }

        }

        return newTab;

    }

3、TreeMap
(1)TreeMap():構建一個空的映像樹
(2)TreeMap(Map m): 構建一個映像樹,而且添加映像m中全部元素
(3)TreeMap(Comparator c): 構建一個映像樹,而且使用特定的比較器對關鍵字進行排序
(4)TreeMap(SortedMap s): 構建一個映像樹,添加映像樹s中全部映射,而且使用與有序映像s相同的比較器排序。

官方文檔:

基於紅黑樹(Red-Black tree)的 NavigableMap 實現。該映射根據其鍵的天然順序進行排序,或者根據建立映射時提供的 Comparator 進行排序,具體取決於使用的構造方法。

此實現爲 containsKey、get、put 和 remove 操做提供受保證的 log(n) 時間開銷。這些算法是 Cormen、Leiserson 和 Rivest 的 Introduction to Algorithms 中的算法的改編。

注意,若是要正確實現 Map 接口,則有序映射所保持的順序(不管是否明確提供了比較器)都必須與 equals 一致。(關於與 equals 一致 的精肯定義,請參閱 Comparable 或 Comparator)。這是由於 Map 接口是按照 equals 操做定義的,但有序映射使用它的 compareTo(或 compare)方法對全部鍵進行比較,所以從有序映射的觀點來看,此方法認爲相等的兩個鍵就是相等的。即便排序與 equals 不一致,有序映射的行爲仍然是 定義良好的,只不過沒有遵照 Map 接口的常規協定。

注意,此實現不是同步的。若是多個線程同時訪問一個映射,而且其中至少一個線程從結構上修改了該映射,則其必須 外部同步。(結構上的修改是指添加或刪除一個或多個映射關係的操做;僅改變與現有鍵關聯的值不是結構上的修改。)這通常是經過對天然封裝該映射的對象執行同步操做來完成的。若是不存在這樣的對象,則應該使用 Collections.synchronizedSortedMap 方法來「包裝」該映射。最好在建立時完成這一操做,以防止對映射進行意外的不一樣步訪問,以下所示:

SortedMap m = Collections.synchronizedSortedMap(new TreeMap(…));
collection(由此類全部的「collection 視圖方法」返回)的 iterator 方法返回的迭代器都是快速失敗 的:在迭代器建立以後,若是從結構上對映射進行修改,除非經過迭代器自身的 remove 方法,不然在其餘任什麼時候間以任何方式進行修改都將致使迭代器拋出 ConcurrentModificationException。所以,對於併發的修改,迭代器很快就徹底失敗,而不會冒着在未來不肯定的時間發生不肯定行爲的風險。

構造方法: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中

四、帶有SortedMap的構造方法

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源碼的實現:

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;  

    }

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;  

            }  

        }  

    }

fixAfterDeletion方法即是節點刪除後對樹進行調整的方法

總結:
一、TreeMap是根據key進行排序的,它的排序和定位須要依賴比較器或覆寫Comparable接口,也所以不須要key覆寫hashCode方法和equals方法,就能夠排除掉重複的key,而HashMap的key則須要經過覆寫hashCode方法和equals方法來確保沒有重複的key
二、TreeMap的查詢、插入、刪除效率均沒有HashMap高,通常只有要對key排序時才使用TreeMap
三、TreeMap的key不能爲null,而HashMap的key能夠爲null。

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

【參考資料】
Java Collections API源碼分析
【Java集合源碼剖析】TreeMap源碼剖析
TreeMap(紅黑樹)源碼分析
HashMap和TreeMap區別詳解以及底層實現

4.感悟

本次實驗對樹的學習以後運用的檢測性很強,只有真正對樹的結構和原理熟練掌握才能作好實驗 同時也讓我知道了我對樹的運用還很欠缺,仍然須要繼續努力、學習

相關文章
相關標籤/搜索