1. 程式人生 > >20172303 2018-2019-1 《程式設計與資料結構》實驗二報告

20172303 2018-2019-1 《程式設計與資料結構》實驗二報告

20172303 2018-2019-1 《程式設計與資料結構》實驗二報告

  • 課程:《程式設計與資料結構》
  • 班級: 1723
  • 姓名: 範雯琪
  • 學號:20172303
  • 實驗教師:王志強
  • 助教:張師瑜/張之睿
  • 實驗日期:2018年11月5日
  • 必修/選修: 必修

實驗內容

本次實驗主要是關於樹的應用, 涉及了二叉樹、決策樹、表示式樹、二叉查詢樹、紅黑樹五種樹的型別,是對最近學習內容第十章和第十一章的一個總結。

節點一

  • 參考教材P212,完成鏈樹LinkedBinaryTree的實現(getRight,contains,toString,preorder,postorder),用JUnit或自己編寫驅動類對自己實現的LinkedBinaryTree進行測試。

節點二

  • 基於LinkedBinaryTree,實現基於(中序,先序)序列構造唯一一棵二㕚樹的功能,比如給出先序ABDHIEJMNCFGKL和中序HDIBEMJNAFCKGL,構造出附圖中的樹,用JUnit或自己編寫驅動類對自己實現的功能進行測試。

節點三

  • 自己設計並實現一顆決策樹。

節點四

  • 輸入中綴表示式,使用樹將中綴表示式轉換為字尾表示式,並輸出字尾表示式和計算結果(如果沒有用樹,則為0分)。

節點五

  • 完成PP11.3。

節點六

實驗過程及結果

節點一——實現二叉樹

  • getRight:getRight操作用於返回根的右子樹。當樹為空時,丟擲錯誤,當樹不為空時,通過遞迴返回根的右子樹。
public LinkedBinaryTree2<T> getRight()
    {
        if(root == null) {
            throw new EmptyCollectionException("BinaryTree");
        }
        LinkedBinaryTree2<T> result = new LinkedBinaryTree2<>();
        result.root = root.getRight();
        return result;
    }
  • containscontains操作的實現有兩種方法:一種是直接借用find方法,另一種是重新寫一個。
    • 方法一:借用find方法,find方法的作用是在二叉樹中找到指定目標元素,則返回對該元素的引用,所以當該元素的引用與查詢的元素相同時返回true,否則返回false。
    public boolean contains(T targetElement)
        {
            if (find(targetElement) == targetElement){return true;}
            else {return false;}
        }
    • 方法二:重新寫一個。具體解釋放在程式碼當中。
    public boolean contains(T targetElement)
    {
        BinaryTreeNode node = root;
        BinaryTreeNode temp = root;
        //找到的情況有三種:查詢元素就是根,查詢元素位於右子樹,查詢元素位於左子樹。
        //除了這三種情況下其餘情況都找不到元素,因此初始設定為false
        boolean result = false;
    
        //當樹為空時,返回false
        if (node == null){
            result = false;
        }
        //當查詢元素就是根時,返回true
        if (node.getElement().equals(targetElement)){
            result = true;
        }
        //對右子樹進行遍歷(在右子樹不為空的情況下)找到元素則返回true,否則對根的左子樹進行遍歷
        while (node.right != null){
            if (node.right.getElement().equals(targetElement)){
                result = true;
                break;
            }
            else {
                node = node.right;
            }
        }
        //對根的左子樹進行遍歷,找到元素則返回true,否則返回false
        while (temp.left.getElement().equals(targetElement)){
            if (temp.left.getElement().equals(targetElement)){
                result = true;
                break;
            }
            else {
                temp = temp.left;
            }
        }
        return result;
    }
  • toStringtoString方法我借用了ExpressionTree類中的PrintTree方法,具體內容曾在第七週部落格中說過。
  • preorderpreorder方法由於有inOrder方法的參考所以挺好寫的,修改一下三條程式碼(三條程式碼分別程式碼訪問根、訪問右孩子和訪問左孩子)的順序即可,使用了遞迴。在輸出時為了方便輸出我重新寫了一個ArrayUnorderedList類的公有方法,直接輸出列表,要比用迭代器輸出方便一些。
public ArrayUnorderedList preOrder(){
    ArrayUnorderedList<T> tempList = new ArrayUnorderedList<T>();
    preOrder(root,tempList);
    return tempList;
}
    
protected void preOrder(BinaryTreeNode<T> node,
                            ArrayUnorderedList<T> tempList) 
{
    if (node != null){
        //從根節點開始,先訪問左孩子,再訪問右孩子
        tempList.addToRear(node.getElement());
        preOrder(node.getLeft(),tempList);
        preOrder(node.getRight(),tempList);
    }
}
  • postOrderpostOrder方法與preorder方法類似,唯一的區別是後序遍歷先訪問左孩子,再訪問右孩子,最後訪問根結點,程式碼和上面差不多就不放了。

測試結果

節點二——中序先序序列構造二叉樹

  • 已知先序遍歷和中序遍歷得到二叉樹有三個步驟:
    • (1)找到根結點。因為先序遍歷按照先訪問根結點再訪問左右孩子的順序進行的,所以先序遍歷的第一個結點就是二叉樹的根。
    • (2)區分左右子樹。在確定了根結點之後,在中序遍歷結果中,根結點之前的就是左子樹,根結點之後的就是右子樹。如果跟結點前邊或後邊為空,那麼該方向子樹為空;如果根節點前邊和後邊都為空,那麼根節點已經為葉子節點。
    • (3)分別對左右子樹再重複第一、二步直至完全構造出該樹。
  • 在清楚了構造的步驟之後,實現就比較簡單了,在實現的過程中用了遞迴的方法。
public void initTree(String[] preOrder,String[] inOrder){
    BinaryTreeNode temp = initTree(preOrder,0,preOrder.length-1,inOrder,0,inOrder.length-1);
    root = temp;
}

private BinaryTreeNode initTree(String[] preOrder,int prefirst,int prelast,String[] inOrder,int infirst,int inlast){
    if(prefirst > prelast || infirst > inlast){
        return null;
    }
    String rootData = preOrder[prefirst];
    BinaryTreeNode head = new BinaryTreeNode(rootData);
    //找到根結點
    int rootIndex = findroot(inOrder,rootData,infirst,inlast);
    //構建左子樹
    BinaryTreeNode left = initTree(preOrder,prefirst + 1,prefirst + rootIndex - infirst,inOrder,infirst,rootIndex-1);
    //構建右子樹
    BinaryTreeNode right = initTree(preOrder,prefirst + rootIndex - infirst + 1,prelast,inOrder,rootIndex+1,inlast);
    head.left = left;
    head.right = right;
    return head;
}
//尋找根結點在中序遍歷陣列中的位置
public int findroot(String[] a, String x, int first, int last){
    for(int i = first;i<=last; i++){
        if(a[i] == x){
            return i;
        }
    }
    return -1;
}

測試結果

節點三——決策樹

  • 節點三的實現藉助了第十章背部疼痛診斷器的相關內容,其關鍵部分是DecisionTree類的實現。
    • DecisionTree的建構函式從檔案中讀取字串元素。儲存在樹結點中。然後建立新的結點,將之前定義的結點(或子樹)作為內部結點的子結點。
    public DecisionTTree(String filename) throws FileNotFoundException
    {
        //讀取字串
        File inputFile = new File(filename);
        Scanner scan = new Scanner(inputFile);
        int numberNodes = scan.nextInt();
        scan.nextLine();
        int root = 0, left, right;
    
        //儲存在根結點中
        List<LinkedBinaryTree<String>> nodes = new ArrayList<LinkedBinaryTree<String>>();
        for (int i = 0; i < numberNodes; i++) {
            nodes.add(i,new LinkedBinaryTree<String>(scan.nextLine()));
        }
    
        //建立子樹
        while (scan.hasNext())
        {
            root = scan.nextInt();
            left = scan.nextInt();
            right = scan.nextInt();
            scan.nextLine();
    
            nodes.set(root, new LinkedBinaryTree<String>((nodes.get(root)).getRootElement(),
                    nodes.get(left), nodes.get(right)));
        }
        tree = nodes.get(root);
    }
    • evaluate方法從根結點開始處理,用current表示正在處理的結點。在迴圈中,如果使用者的答案為N,則更新current使之指向左孩子,如果使用者的答案為Y,則更新current使之指向右孩子,迴圈直至current為葉子結點時結束,結束後返回current的根結點的引用。
    public void evaluate()
    {
        LinkedBinaryTree<String> current = tree;
        Scanner scan = new Scanner(System.in);
    
        while (current.size() > 1)
        {
            System.out.println (current.getRootElement());
            if (scan.nextLine().equalsIgnoreCase("N")) {
                current = current.getLeft();
            } else {
                current = current.getRight();
            }
        }
    
        System.out.println (current.getRootElement());
    }

測試結果

節點四——表示式樹

  • 這個測試我認為是所有測試中最難的一個, 尤其是關於如何使用樹實現這一部分,考慮了很久都沒有思路,後來重新翻看課本第十章表達式樹部分的內容,才有了思路,發現不是光用樹就能實現的,像上學期的四則運算一樣,這個也是要先建立兩個棧來存放操作符和運算元的。具體的解釋在下面的程式碼中都有。
public static String  toSuffix(String infix) {
    String result = "";
    //將字串轉換為陣列
    String[] array = infix.split("\\s+");
    //存放運算元
    Stack<LinkedBinaryTree> num = new Stack();
    //存放操作符
    Stack<LinkedBinaryTree> op = new Stack();

    for (int a = 0; a < array.length; a++) {
        //如果是運算元,開始迴圈
        if (array[a].equals("+") || array[a].equals("-") || array[a].equals("*") || array[a].equals("/")) {
            if (op.empty()) {
                //如果棧是空的,將陣列中的元素建立新樹結點並壓入操作符棧
                op.push(new LinkedBinaryTree<>(array[a]));
            } else {
                //如果棧頂元素為+或-且陣列的元素為*或/時,將元素建立新樹結點並壓入操作符棧
                if ((op.peek().root.element).equals("+") || (op.peek().root.element).equals("-") && array[a].equals("*") || array[a].equals("/")) {
                    op.push(new LinkedBinaryTree(array[a]));
                } else {
                //將運算元棧中的兩個元素作為左右孩子,操作符棧中的元素作為根建立新樹
                    LinkedBinaryTree right = num.pop();
                    LinkedBinaryTree left = num.pop();
                    LinkedBinaryTree temp = new LinkedBinaryTree(op.pop().root.element, left, right);
                    //將樹壓入運算元棧,並將陣列中的元素建立新樹結點並壓入操作符棧
                    num.push(temp);
                    op.push(new LinkedBinaryTree(array[a]));
                }
            }
        } else {
            //將陣列元素建立新樹結點並壓入運算元棧
            num.push(new LinkedBinaryTree<>(array[a]));
        }
    }
    while (!op.empty()) {
        LinkedBinaryTree right = num.pop();
        LinkedBinaryTree left = num.pop();
        LinkedBinaryTree temp = new LinkedBinaryTree(op.pop().root.element, left, right);
        num.push(temp);
    }
    //輸出字尾表示式
    Iterator itr=num.pop().iteratorPostOrder();
    while (itr.hasNext()){
        result+=itr.next()+" ";
    }
    return result;
}

測試結果

節點五——二叉查詢樹

  • 因為書上給出了removeMin的實現方法,二叉查詢樹有一個特殊的性質就是最小的元素儲存在樹的左邊,最大的元素儲存在樹的右邊。因此實現removeMax方法只需要把removeMin方法中所有的left和right對調即可。二叉查詢樹的刪除操作有三種情況,要依據這三種情況來實現程式碼,我在第七週部落格教材內容總結中已經分析過了,就不在這裡貼程式碼了。
  • 實現了removeMinremoveMax後,其實findMinfindMax就很簡單了,因為在實現刪除操作時首先先要找到最大/最小值,因此只要把找到之後的步驟刪掉,返回找到的最大值或最小值的元素即可。
public T findMin() throws EmptyCollectionException
    {
        T result;
        if (isEmpty()){
            throw new EmptyCollectionException("LinkedBinarySearchTree");
        }
        else {
            if (root.left == null){
                result = root.element;
            }
            else {
                BinaryTreeNode<T> parent = root;
                BinaryTreeNode<T> current = root.left;
                while (current.left != null){
                    parent = current;
                    current = current.left;
                }
                result = current.element;
            }
        }
        return result;
    }

    
public T findMax() throws EmptyCollectionException
{
    T result;

    if (isEmpty()){
        throw new EmptyCollectionException("LinkedBinarySearchTree");
        }
    else {
        if (root.right == null){
            result = root.element;
        }
        else {
            BinaryTreeNode<T> parent = root;
            BinaryTreeNode<T> current = root.right;
            while (current.right != null){
                parent = current;
                current = current.right;
            }
            result = current.element;
        }
    }
    return result;
}

測試結果

節點六——紅黑樹分析

  • 在jdk1.8版本後,java對HashMap做了改進,在連結串列長度大於8的時候,將後面的資料存在紅黑樹中,以加快檢索速度。而TreeMap的實現原理就是紅黑樹,因此分析紅黑樹時我們要分析HashMap和TreeMap的原始碼。

HashMap

  • HashMap是一種基於雜湊表(hash table)實現的map,雜湊表(也叫關聯陣列)一種通用的資料結構,大多數的現代語言都原生支援,其概念也比較簡單:key經過hash函式作用後得到一個槽(buckets或slots)的索引(index),槽中儲存著我們想要獲取的值,如下圖所示:
  • HashMap的方法較多,此處選擇建構函式、get操作和remove操作進行分析。
  • 建構函式
    public HashMap(int initialCapacity, float loadFactor) { if (initialCapacity < 0) throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity); if (initialCapacity > MAXIMUM_CAPACITY) initialCapacity = MAXIMUM_CAPACITY; if (loadFactor <= 0 || Float.isNaN(loadFactor)) throw new IllegalArgumentException("Illegal load factor: " + loadFactor); this.loadFactor = loadFactor; threshold = initialCapacity; init(); } public HashMap(int initialCapacity) { this(initialCapacity, DEFAULT_LOAD_FACTOR); } public HashMap() { this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR); }
    • HashMap遵循集合框架的約束,提供了一個引數為空的建構函式和有一個引數且引數型別為Map的建構函式。除此之外,還提供了兩個建構函式,用於設定HashMap的容量(capacity)與平衡因子(loadFactor)(平衡因子=|右子樹高度-左子樹高度|)。
  • get操作
    • get操作用於返回指定鍵所對映的值;如果對於該鍵來說,此對映不包含任何對映關係,則返回null。
    • 這裡需要說明兩個東西:Entry——Entry實現了單向連結串列的功能,用next成員變數來級連起來。table[ ]——HashMap內部維護了一個為陣列型別的Entry變數table,用來儲存新增進來的Entry物件。
    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;
    }
  • remove操作
    • remove操作用於在指定鍵存在的情況下,從此對映中移除指定鍵的對映關係。
    public V remove(Object key) {
    Entry<K,V> e = removeEntryForKey(key);
    //當指定鍵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;
    }
  • 而在HashMap中涉及到紅黑樹的,是put操作。
  • put操作
    • put操作用於在此對映中關聯指定值與指定鍵。
    public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }
    //單個位置連結串列長度減小到6,將紅黑樹轉化會連結串列
    static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    } 
    /**
     * 插入key-value 鍵值對具體實現
     */
     final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        // 判斷 若hashmap內沒有值 則重構hashmap
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        // 若指定位置hashcode 未被佔用 則直接將該鍵值對插入
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        // 發生衝突時 解決方法
        else {
            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);
                        // 若此時連結串列內長度大於等於7 將連結串列轉化為紅黑樹 並將節點插入
                        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;
            }
        }
        ++modCount;
        // 若容量不足 則擴容
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

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

  • 問題1:在實現節點一的時候,輸出的並不是遍歷結果而是地址
  • 問題1解決方法:說實話這就是一個第十章沒學好的殘留問題,當時學的時候我就沒有把這一部分補充完整,對於迭代器的使用也不熟練,完成節點一的過程中,我想到的解決方法是重新寫了一個ArrayUnorderedList類的公有方法,將該無序列表直接輸出(程式碼在節點一的過程中有)。後來實驗結束後詢問同學學會了將迭代器方法的遍歷結果輸出。
//以後序遍歷為例
String result = "";
Iterator itr = tree.iteratorPostOrder();
    while (itr.hasNext()){
        result += itr.next() + " ";
    }
return result;
  • 問題2:在實現節點二的時候無法輸出構造好的樹。
  • 問題2解決方法:通過Debug,首先確定樹是構造好的,沒有出現樹為空的情況。
  • 那麼問題就應該是出在toString方法中,後來發現原因出在了root上,在toString方法中,root從一開始就是空的,並沒有獲取到我構造的樹的根結點。
  • 然後我嘗試在ReturnBinaryTree類中加入了一個獲取根的方法,結果最後輸出的是根的地址。
  • 最後參考了餘坤澎同學的程式碼,把ReturnBinaryTree類中的方法放的toString所在的LinkedBinaryTree類中,因為此時它能夠獲取到構造的樹的根節點,因此就能正常輸出了。
  • 問題3:在實現決策樹的過程中,檔案裡的內容為什麼以這樣的順序排列?
  • 問題3解決方法:這個要結合DecisionTree類來看,首先第一行的13代表了這顆決策樹中的節點個數,所以在DecisionTree類中的int numberNodes = scan.nextInt();一句其實就是獲取檔案的第一行記錄節點個數的值。接下來檔案中按照層序遍歷的順序將二叉樹中的元素一一列出來,最後檔案中的幾行數字其實代表了每個結點及其左右孩子的位置(仍然按照層序遍歷的順序),並且是從最後一層不是葉子結點的那一層的結點開始,比如[3,7,8]就代表了層序遍歷中第3個元素的左孩子為第7個元素,右孩子為第8個元素。
  • 我剛開始把根結點設定成第1個元素髮現怎麼都對不上,後來發現這裡定義了根結點為第0個元素,所以最後一個元素為第12個元素而不是第13個。

其他(感悟、思考等)

  • 其實本次實驗整體上來說還是比較簡單的,唯一有難度的可能只有節點四和節點六。在這個過程中幫我複習了很多,而且逼著我去解決了一些曾經在教材學習中不願面對的問題,nice~~

參考資料