20172302 《Java軟件結構與數據結構》實驗二:樹實驗報告

課程:《Java軟件結構與數據結構》

班級: 1723

姓名: 侯澤洋

學號:20172302

實驗教師:王志強老師

實驗日期:2018年11月5日

必修/選修: 必修

實驗內容

  • (1)參考教材p212,完成鏈樹LinkedBinaryTree的實現(getRight,contains,toString,preorder,postorder;用JUnit或本身編寫驅動類對本身實現的LinkedBinaryTree進行測試,提交測試代碼運行截圖,要全屏,包含本身的學號信息php

  • (2)基於LinkedBinaryTree,實現基於(中序,先序)序列構造惟一一棵二㕚樹的功能,好比給出中序HDIBEMJNAFCKGL和先序ABDHIEJMNCFGKL,構造出附圖中的樹;用JUnit或本身編寫驅動類對本身實現的功能進行測試,提交測試代碼運行截圖,要全屏,包含本身的學號信息html

  • (3)本身設計並實現一顆決策樹;提交測試代碼運行截圖,要全屏,包含本身的學號信息java

  • (4)輸入中綴表達式,使用樹將中綴表達式轉換爲後綴表達式,並輸出後綴表達式和計算結果;提交測試代碼運行截圖,要全屏,包含本身的學號信息node

  • (5)完成PP11.3;提交測試代碼運行截圖,要全屏,包含本身的學號信息web

  • (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,
這個主要是利用傳進來的先序和中序的字符串,肯定根結點,而後再肯定其左右孩子,接下來遞歸該過程,直至將該字符串讀取完成。
實驗結果截圖:
app

(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源碼分析太難了,整個徹底弄懂還不太可能。

參考資料

相關文章
相關標籤/搜索