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

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

課程:《程序設計與數據結構》
班級: 1723
姓名: 譚鑫
學號:20172305
實驗教師:王志強
實驗日期:2018年10月13日
必修/選修: 必修html

實驗內容

  • 實驗二-1-實現二叉樹:
    • (1)參考教材p212,完成鏈樹LinkedBinaryTree的實現(getRight,contains,toString,preorder,postorder)
    • (2)用JUnit或本身編寫驅動類對本身實現的LinkedBinaryTree進行測試
  • 實驗二-2-中序先序序列構造二叉樹:
    • (1)基於LinkedBinaryTree,實現基於(中序,先序)序列構造惟一一棵二㕚樹的功能,好比給出中序HDIBEMJNAFCKGL和後序ABDHIEJMNCFGKL,構造出樹
    • (2)用JUnit或本身編寫驅動類對本身實現的功能進行測試
  • 實驗二-3-決策樹:
    • (1)設計並實現一棵決策樹
    • (2)用JUnit或本身編寫驅動類對本身實現的功能進行測試
  • 實驗二-4-表達式樹:
    • (1)輸入中綴表達式,使用樹將中綴表達式轉換爲後綴表達式,並輸出後綴表達式和計算結果
    • (2)用JUnit或本身編寫驅動類對本身實現的功能進行測試
  • 實驗二-5-二叉查找樹:
    • (1)完成PP11.3
    • (2)用JUnit或本身編寫驅動類對本身實現的功能進行測試
  • 實驗二-6-紅黑樹分析:
    • (1)參考文件對Java中的紅黑樹(TreeMap,HashMap)進行源碼分析,並在實驗報告中體現分析結果。(C:\Program Files\Java\jdk-11.0.1\lib\src\java.base\java\util)

實驗過程及結果

  • 實驗二-1-實現二叉樹:就是對二叉樹的測試,二叉樹不像二叉查找樹同樣能夠進行添加刪除。因此,在構造一個二叉樹的時候須要不斷的new一下,把每個結點進行拼接才能構造一棵樹。getRight,contains,toString,preorder,postorder。java

    • getRight和getLeft,是返回某結點的左側和右側的操做
    • contains,是斷定指定目標是否在該樹中的操做
    • toString,是將樹進行輸出
    • preorder,是對樹進行前序遍歷
    • postorder,是對樹進行後序遍歷
    • 具體分析在第六週博客
  • 實現的操做相對簡單,只是遍歷的時候發現迭代器運用起來感受很麻煩,因此就根據遍歷思想建立了四個簡單點的遍歷方法。
  • 實驗二-2-中序先序序列構造二叉樹:根據中序和先序的內容來判斷樹內元素的具體位置,經過先序來判斷根結點,再在中序來判斷根結點的左子樹和右子樹;在將兩個部分繼續在先序中查詢根結點,在中序中判斷結點的左右子樹,就會判斷出樹中各個結點的位置。
  • 實驗二-3-決策樹:實現一棵決策樹,這個實驗就是仿照書上第十章的背部疼痛診斷器來書寫的。我再此基礎上並無進行大的修改,只是仿照內容修改了讀取的文件內容。我所設計的決策樹內容如圖:算法

  • 實驗二-4-表達式樹:經過輸入的中綴表達式利用樹來實現中綴轉後綴,並由二叉樹轉換成後綴表達式並輸出。
    在實現後綴的過程當中,咱們能夠調用以前的書上代碼來實現後綴表達式的轉換成數字。
  • 實驗二-5-二叉查找樹:就是對二叉查找樹的測試,二叉查找樹會優於二叉樹能夠進行添加刪除。因此,在構造一個二叉查找樹的時候,把每個結點內的內容加進去就能夠。
  • 實驗二-6-紅黑樹:紅黑樹的問題是最麻煩的,根據每個樹的結點內容實現紅黑的添加,可是紅黑的添加會出現碰撞,這就須要咱們的具體分析過程,而TreeMap和TreeSet方法都是依靠紅黑樹來實現的。less

    紅黑樹的性質:
    • (1)每一個結點或者是黑色,或者是紅色
    • (2)根結點是黑色
    • (3)每一個葉結點是黑色(是指爲空的葉結點)
    • (4)若是一個結點是紅色的,則它的子節點必須是黑色的
    • (5)從一個結點到該結點的子孫結點的全部路徑上包含相同數目的黑結點
    • TreeMap和TreeSet是API的兩個重要成員,其中 TreeMap是Map接口的經常使用實現類,而TreeSet是Set接口的經常使用實現類。雖然TreeMap和TreeSet實現的接口規範不一樣,TreeMap實現的接口是Map,TreeSet實現的接口是Set,但TreeSet底層是經過TreeMap來實現的,所以兩者的實現方式徹底同樣。而TreeMap底層的實現就是用的紅黑樹數據結構來實現的。而HashMap基於哈希表的Map接口的實現。此實現提供全部可選的映射操做,並容許使用 null 值和 null 鍵。(除了非同步和容許使用 null 以外,HashMap 類與 Hashtable 大體相同。)此類不保證映射的順序,特別是它不保證該順序恆久不變。 此實現假定哈希函數將元素適當地分佈在各桶之間,可爲基本操做(get 和 put)提供穩定的性能。
    • TreeMap是一個有序的key-value集合,它是經過紅黑樹實現的。
    • TreeMap繼承於AbstractMap,因此它是一個Map,即一個key-value集合。
    • TreeMap實現了NavigableMap接口,意味着它支持一系列的導航方法。好比返回有序的key集合。
    • TreeMap實現了Cloneable接口,意味着它能被克隆。
    • TreeMap實現了java.io.Serializable接口,意味着它支持序列化。
    • TreeMap的簡單測試:
    • HashMap底層實現是鏈表數組而且實現了Map所有的方法。
    • HashMap的key用Set存放,因此想作到key不容許重複,key對應的類須要重寫hashCode和equals方法。
    • HashMap容許空鍵和空值,元素是無序的,並且順序會不定時改變。
    • HashMap插入、獲取的時間複雜度基本是 O(1),遍歷整個 Map 須要的時間與 桶的長度成正比。
    • HashMap兩個關鍵因子:初始容量、加載因子
    • HashMap的簡單測試:
  • 關於Map接口:Map接口表明由關鍵字以及它們的值組成的一些項的集合。關鍵字必須是惟一的,可是若干關鍵字的能夠映射到一些相同的值。所以,值不是惟一的。在SortedMap接口中,映射中的關鍵字保持邏輯上有序狀態。SortedMap接口的一種實現是TreeMap。在數組中咱們是經過數組下標來對其內容索引的,而在Map中咱們經過對象來對對象進行索引,用來索引的對象叫作key,其對應的對象叫作value。這就是咱們平時說的鍵值對。函數

// 默認構造函數。使用該構造函數,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)
//構建一個具備默認初始容量 (16) 和默認加載因子 (0.75) 的空哈希映像
    HashMap()

//構建一個哈希映像,而且添加映像m的全部映射
    HashMap(Map<? extends K,? extends V> m)

//構建一個擁有特定容量的空的哈希映像
    HashMap(int initialCapacity)

//構建一個擁有特定容量和加載因子的空的哈希映像
    HashMap(int initialCapacity, float loadFactor)
  • TreeMap的Empty方法,firstEntry()和getFirstEntry()都是用於獲取第一個節點。可是,firstEntry() 是對外接口;getFirstEntry()是內部接口。並且,firstEntry()是經過getFirstEntry()來實現的。兩個方法並會顯得很麻煩,由於經過firstEntry()方法和能夠避免修改返回的Entry,這樣保證了完整性,並且產生了兩個方法,一個能夠是避免修改的方法,一個能夠是修改的方法。經過查閱資料能夠總結出對firstEntry()返回的Entry對象只能進行getKey()、getValue()等讀取操做;而對getFirstEntry()返回的對象除了能夠進行讀取操做以後,還能夠經過setValue()修改值。
public Map.Entry<K,V> firstEntry() {
    return exportEntry(getFirstEntry());
}

final Entry<K,V> getFirstEntry() {
    Entry<K,V> p = root;
    if (p != null)
        while (p.left != null)
            p = p.left;
    return p;
}
  • TreeMap的Key方法,返回大於/等於key的最小的鍵值對所對應的KEY,沒有的話返回null。
public K ceilingKey(K key) {
    return keyOrNull(getCeilingEntry(key));
}
  • TreeMap的values方法,values方法是經過new Values()來實現返回TreeMap中值的集合,而Values()正好是集合類Value的構造函數,這樣返回的是一個集合了。
public Collection<V> values() {
    Collection<V> vs = values;
    return (vs != null) ? vs : (values = new Values());
}
  • TreeMap的entrySet方法,entrySet方法是返回TreeMap的全部鍵值對組成的集合,並且它單位是單個鍵值對。
public Set<Map.Entry<K,V>> entrySet() {
    EntrySet es = entrySet;
    return (es != null) ? es : (entrySet = new EntrySet());
}
  • TreeMap還有兩個遍歷方法,順序遍歷和逆序遍歷,順序遍歷,就是從第一個元素開始,逐個向後遍歷;而倒序遍歷則偏偏相反,它是從最後一個元素開始,逐個往前遍歷。
  • TreeMap遍歷鍵值對的方法,先根據entrySet()獲取TreeMap的「鍵值對」的Set集合,再經過迭代器遍歷獲得的集合。
Integer integ = null;
Iterator iter = map.entrySet().iterator();
while(iter.hasNext()) {
    Map.Entry entry = (Map.Entry)iter.next();
    key = (String)entry.getKey();
    integ = (Integer)entry.getValue();
}
  • TreeMap遍歷鍵的方法,先keySet()獲取TreeMap的鍵的Set集合,再經過迭代器遍歷獲得的集合。
String key = null;
Integer integer = null;
Iterator iter = map.keySet().iterator();
while (iter.hasNext()) {
    key = (String)iter.next();
    integer = (Integer)map.get(key);
}
  • HashMap的主幹是一個Entry數組。Entry是HashMap的基本組成單元,每個Entry包含一個key-value鍵值對。HashMap由數組+鏈表組成的,數組是HashMap的主體,鏈表則是主要爲了解決哈希衝突而存在的,若是定位到的數組位置不含鏈表(當前entry的next指向null),那麼對於查找,添加等操做很快,僅需一次尋址便可;若是定位到的數組包含鏈表,對於添加操做,其時間複雜度爲O(n),首先遍歷鏈表,存在即覆蓋,不然新增;對於查找操做來說,仍需遍歷鏈表,而後經過key對象的equals方法逐一比對查找。因此,性能考慮,HashMap中的鏈表出現越少,性能纔會越好。
  • HashMap的put方法,是向HashMap中添加一個鍵值對。
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;
    }
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);
    }
  • HashMap的get方法,是向HashMap中經過查鍵來找尋對應的值。
public V get(Object key) {
    Node<K,V> e;
    //仍是先計算 哈希值
    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;
    //tab 指向哈希表,n 爲哈希表的長度,first 爲 (n - 1) & hash 位置處的桶中的頭一個節點
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (first = tab[(n - 1) & hash]) != null) {
        //若是桶裏第一個元素就相等,直接返回
        if (first.hash == hash &&
            ((k = first.key) == key || (key != null && key.equals(k))))
            return first;
        //不然就得慢慢遍歷找
        if ((e = first.next) != null) {
            if (first instanceof TreeNode)
                //若是是樹形節點,就調用樹形節點的 get 方法
                return ((TreeNode<K,V>)first).getTreeNode(hash, key);
            do {
                //do-while 遍歷鏈表的全部節點
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    return e;
            } while ((e = e.next) != null);
        }
    }
    return null;
}
  • HashMap的remove方法,是根據key刪除該key對應的鍵值對,該方法將會根據查找到匹配的鍵值對,將其從HashMap中刪除,而且返回鍵值對的值。
public V remove(Object key) {
    Node<K,V> e; // 定義一個節點變量,用來存儲要被刪除的節點(鍵值對)
    return (e = removeNode(hash(key), key, null, false, true)) == null ?
        null : e.value; // 調用removeNode方法
}
final Node<K,V> removeNode(int hash, Object key, Object value,boolean matchValue, boolean movable) {
    Node<K,V>[] tab; Node<K,V> p; int n, index; // 聲明節點數組、當前節點、數組長度、索引值
    if ((tab = table) != null && (n = tab.length) > 0 && (p = tab[index = (n - 1) & hash]) != null) {
        Node<K,V> node = null, e; K k; V v; 
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            node = p;
        else if ((e = p.next) != null) {
            if (p instanceof TreeNode)
                node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
            else {
                do {
                    if (e.hash == hash &&
                        ((k = e.key) == key ||
                            (key != null && key.equals(k)))) {
                        node = e;
                        break;
                    }
                    p = e; 
                } while ((e = e.next) != null);
            }
        }
        if (node != null && (!matchValue || (v = node.value) == value || (value != null && value.equals(v)))) {
            if (node instanceof TreeNode) 
                ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
            else if (node == p)
                tab[index] = node.next; 
            else 
                p.next = node.next;
            modCount++; 
            size--;
            afterNodeRemoval(node); 
            return node;
        }
    }
    return null;
}
  • HashMap的hash方法,經過將傳入鍵的hashCode進行無符號右移 16 位,而後進行按位異或,獲得這個鍵的哈希值。
public static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

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

  • 問題1:如何利用先序和後序轉換成整棵樹?
  • 問題1的解決方案:根據先序來查找根結點的位置,再在中序查找到根結點,經過根結點的位置分出左子樹和右子樹兩個部分。將每一部分從新當作一個樹,並在先序中查找到這部分的根結點(此爲根結點的右子結點),再進行右側部分,不斷地遞歸下去就會實現,當這個中序中的位置和後序中的元素一致,就會結束。

  • 問題2:如何利用樹實現中綴轉後綴得出結果?
  • 問題2的解決方案:在上學期實現的四則運算的思路來看,整個運算式須要先不斷截取成一個字符,把運算符方法放到一個無序列表,將數放到一個定義爲鏈表式樹類的無序列表。針對運算的邏輯順序,括號內的優先級最高,乘除的運算其次,最後是加減的運算。因此,在斷開存放的時候先判斷括號,若是遇到括號,那麼直至查找到另外一半括號,中間的內容都爲括號內的,將這一部分總體做爲一個新的運算式經過遞歸不斷打開,使得括號內的內容拆成一棵樹,將這部分的樹存放到無序列表中。經過判斷截取存放的過程將運算符爲加減的存放到無序列表,遇到乘除的時候將存放樹的無序列表取出最後一個(爲乘除前一位內容多是括號展成的樹或是數字),判斷後面是否爲括號,若是爲括號就取到另外一半括號爲止,這樣一樣是用到遞歸來實現拆分紅樹,將取到的乘除做爲根結點,前面的內容做爲左子樹,後面的內容做爲右子樹;若是是不爲括號,那麼就是個數,就將數做爲樹,這不過是一個只有根結點沒有左右結點的樹,加上以前的乘除號從新作一個樹,存放到無序列表中,最後該無序列表會只存放一個樹,只需輸出一下就好。

    public BinaryTreeNode change(String strings){
       //把「=」進行摳出
       StringTokenizer tokenizer = new StringTokenizer(strings,"=");
       String string = tokenizer.nextToken();
       //開始轉換
       StringTokenizer stringTokenizer = new StringTokenizer(string," ");
       ArrayUnorderedList<String> arrayUnorderedList1 = new ArrayUnorderedList<String>();
       ArrayUnorderedList<LinkedBinaryTree> arrayUnorderedList2 = new ArrayUnorderedList<LinkedBinaryTree>();
       while (stringTokenizer.hasMoreTokens()){
           strings = stringTokenizer.nextToken();
           //判斷式子中是否有括號
           boolean judge1 = true;
           if(strings.equals("(")){
               String string1 = "";
               while (judge1){
                   strings = stringTokenizer.nextToken();
                   //開始查詢括號的另外一個,若是查到的話就會跳出該循環
                   if (!strings.equals(")"))
                       string1 += strings + " ";
                   else
                       break;
               }  
               LinkedBinaryTree linkedBinaryTree = new LinkedBinaryTree();
               linkedBinaryTree.root = (change(string1));
               arrayUnorderedList2.addToRear(linkedBinaryTree);
               continue;
           } 
           //判斷運算符是不是加減的運算
           if((strings.equals("+")|| strings.equals("-"))){
               arrayUnorderedList1.addToRear(strings);
           }else
               ///判斷運算符是不是乘除的運算
               if((strings.equals("*")|| strings.equals("/"))){
               LinkedBinaryTree left = arrayUnorderedList2.removeLast();
               String strings2 = strings;
               strings = stringTokenizer.nextToken();
               if(!strings.equals("(")) {
                   LinkedBinaryTree right = new LinkedBinaryTree(strings);
                   LinkedBinaryTree node = new LinkedBinaryTree(strings2, left, right);
                   arrayUnorderedList2.addToRear(node);
               }else {
                   String string3 = "";
                   boolean judge2 = true;
                   while (judge2){
                       strings = stringTokenizer.nextToken();
                       if (!strings.equals(")"))
                           string3 += strings + " ";
                       else break;
                   } 
                   LinkedBinaryTree linkedBinaryTree = new LinkedBinaryTree();
                   linkedBinaryTree.root = (change(string3));
                   LinkedBinaryTree node = new LinkedBinaryTree(strings2,left,linkedBinaryTree);
                   arrayUnorderedList2.addToRear(node);
               }
           }else
               arrayUnorderedList2.addToRear(new LinkedBinaryTree(strings));
       }
    
       while(arrayUnorderedList1.size()>0){
           LinkedBinaryTree left = arrayUnorderedList2.removeFirst();
           LinkedBinaryTree right = arrayUnorderedList2.removeFirst();
           String oper = arrayUnorderedList1.removeFirst();
           LinkedBinaryTree node = new LinkedBinaryTree(oper,left,right);
           arrayUnorderedList2.addToFront(node);
       }
       root = (arrayUnorderedList2.removeFirst()).getRootNode();
       return root;
      }

其餘

實驗二的六個實驗看似不少實際上就第二個、第四個和第六個實驗須要認真修改的。這三個實驗的不一樣提交時間能夠減小不小麻煩。可是實驗四的設計思路仍是很費勁,構思出可是不會用代碼實現,還有是否要加括號的問題,若是添加那麼就須要在優先級上進行改寫,好在最後寫出來了。感受這學期的實驗更多的是偏向於邏輯算法的,就像本次實驗這樣,須要更多的思路要考慮。

參考資料

相關文章
相關標籤/搜索