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

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

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

  • 課程:《程式設計與資料結構》
  • 班級: 1723
  • 姓名: 吳恆佚
  • 學號:20172321
  • 實驗教師:王志強
  • 實驗日期:2018年11月11日
  • 必修/選修: 必修

一、實驗內容

實驗1要求——實現二叉樹

  • 參考教材p212,完成鏈樹LinkedBinaryTree的實現(getRight,contains,toString,preorder,postorder)
  • 用JUnit或自己編寫驅動類對自己實現的LinkedBinaryTree進行測試,提交測試程式碼執行截圖,要全屏,包含自己的學號資訊
  • 課下把程式碼推送到程式碼託管平臺

實驗2要求——中序先序序列構造二叉樹

  • 基於LinkedBinaryTree,實現基於(中序,先序)序列構造唯一一棵二㕚樹的功能,比如給出中序HDIBEMJNAFCKGL和後序ABDHIEJMNCFGKL,構造出附圖中的樹
  • 用JUnit或自己編寫驅動類對自己實現的功能進行測試,提交測試程式碼執行截圖,要全屏,包含自己的學號資訊
  • 課下把程式碼推送到程式碼託管平臺

實驗3要求——決策樹

  • 自己設計並實現一顆決策樹
  • 提交測試程式碼執行截圖,要全屏,包含自己的學號資訊
  • 課下把程式碼推送到程式碼託管平臺

實驗4要求——表示式樹

  • 輸入中綴表示式,使用樹將中綴表示式轉換為字尾表示式,並輸出字尾表示式和計算結果(如果沒有用樹,則為0分)
  • 提交測試程式碼執行截圖,要全屏,包含自己的學號資訊
  • 課下把程式碼推送到程式碼託管平臺

實驗5要求——二叉查詢樹

  • 完成PP11.3
  • 提交測試程式碼執行截圖,要全屏,包含自己的學號資訊
  • 課下把程式碼推送到程式碼託管平臺

實驗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

  • 本實驗要求實現getRight方法、contains方法、toString方法、preorder方法、postorder方法,此五個方法。

getRight方法

public LinkedBinaryTree1<T> getRight() {
        LinkedBinaryTree1 node = new LinkedBinaryTree1();
        node.root = root.getRight();
        return node;
    }

這個基本上是之前很久就實現的一個簡單的方法,先進行一個樹的初始化,可以得到一個新樹,然後通過下面一行的程式碼得到新的根,使我們得到合適的右子樹。

contains方法

    @Override
    public boolean contains(T targetElement) {
        if (find(targetElement) == targetElement) {
            return true;
        } else {
            return false;
        }

    }
    @Override
    public T find(T targetElement) throws ElementNotFoundException {
        BinaryTreeNode<T> current = findNode(targetElement, root);

        if (current == null)
            throw new ElementNotFoundException("LinkedBinaryTree");

        return (current.getElement());
    }

這個方法其實本身沒有什麼東西,但是裡面需要用到find方法,首先用find方法查詢這個結點,假如找到了這個結點就返回true,假如沒有找到就返回false。

toString方法

public String toString() {
        UnorderedListADT<BinaryTreeNode<T>> nodes = new week7.jiumingdaima.ArrayUnorderedList<BinaryTreeNode<T>>();
        UnorderedListADT<Integer> levelList = new week7.jiumingdaima.ArrayUnorderedList<Integer>();
        BinaryTreeNode<T> current;
        String result = "";
        int printDepth = this.getHeight();
        int possibleNodes = (int) Math.pow(2, printDepth + 1);
        int countNodes = 0;

        nodes.addToRear(root);
        Integer currentLevel = 0;
        Integer previousLevel = -1;
        levelList.addToRear(currentLevel);

        while (countNodes < possibleNodes) {
            countNodes = countNodes + 1;
            current = nodes.removeFirst();
            currentLevel = levelList.removeFirst();
            if (currentLevel > previousLevel) {
                result = result + "\n\n";
                previousLevel = currentLevel;
                for (int j = 0; j < ((Math.pow(2, (printDepth - currentLevel))) - 1); j++)
                    result = result + " ";
            } else {
                for (int i = 0; i < ((Math.pow(2, (printDepth - currentLevel + 1)) - 1)); i++) {
                    result = result + " ";
                }
            }
            if (current != null) {
                result = result + (current.getElement()).toString();
                nodes.addToRear(current.getLeft());
                levelList.addToRear(currentLevel + 1);
                nodes.addToRear(current.getRight());
                levelList.addToRear(currentLevel + 1);
            } else {
                nodes.addToRear(null);
                levelList.addToRear(currentLevel + 1);
                nodes.addToRear(null);
                levelList.addToRear(currentLevel + 1);
                result = result + " ";
            }

        }

        return result;
    }

這個程式碼直接使用的書上ExpressionTree類的程式碼

preorder方法

    public Iterator<T> iteratorPreOrder() {
        ArrayUnorderedList<T> tempList = new ArrayUnorderedList<T>();
        preOrder(root, tempList);

        return new TreeIterator(tempList.iterator());
    }
    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) {
            // System.out.print(node.getElement()+" ");
            tempList.addToRear(node.getElement());
            preOrder(node.getLeft(), tempList);
            preOrder(node.getRight(), tempList);
        }
    }

這個方法要用到的迭代器方法和本身都是已經給出了的,我加了一段方便方法直接使用

postorder方法

    public ArrayUnorderedList postorder() {
        ArrayUnorderedList<T> tempList = new ArrayUnorderedList<T>();
        postOrder(root, tempList);
        return tempList;
    }

和前序遍歷一樣都是現成的方法,只是加了一段

結果

實驗2

  • 用給出的中序和先序來構建二叉樹

//根據前序和中序序列,建立二叉樹
    public int findroot(String[] S, String s, int begin, int end) {
        for (int i = begin; i <= end; i++) {
            if (S[i] == s) {
                return i;
            }
        }
        return -1;
    }

    public BinaryTreeNode getAtree(String[] preSort, int prestart, int preend, String[] inSort, int instart, int inend) {
        if (prestart > preend || instart > inend) {
            return null;
        }
        if (preSort.length != inSort.length) {
            try {
                throw new Exception("不滿足條件的非法輸入!");
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        BinaryTreeNode treeroot;
        String rootData = preSort[prestart];
        treeroot = new BinaryTreeNode(rootData);

        int rwhere = findroot(inSort, rootData, instart, inend);//找根節點的位置
        treeroot.left = getAtree(preSort, prestart + 1, prestart + rwhere - instart, inSort, instart, rwhere - 1);//左子樹
        treeroot.right = getAtree(preSort, prestart + rwhere - instart + 1, preend, inSort, rwhere + 1, inend);//右子樹

        return treeroot;

    }

    public void getAtree(String[] preOrder, String[] inOrder) {
        this.root = getAtree(preOrder, 0, preOrder.length - 1, inOrder, 0, inOrder.length - 1);
    }
  • 在上個實驗的二叉樹程式碼中新增getAtree方法,結合前序和中序序列,找到根結點和左右子樹,然後對左右子樹分別遞迴使用加getAtree方法,逐步往下建立樹。
  • 已知先序遍歷和中序遍歷得到二叉樹有三個步驟:

    找到根結點。因為先序遍歷按照先訪問根結點再訪問左右孩子的順序進行的,所以先序遍歷的第一個結點就是二叉樹的根。

    區分左右子樹。在確定了根結點之後,在中序遍歷結果中,根結點之前的就是左子樹,根結點之後的就是右子樹。如果跟結點前邊或後邊為空,那麼該方向子樹為空;如果根節點前邊和後邊都為空,那麼根節點已經為葉子節點。

    分別對左右子樹再重複第一、二步直至完全構造出該樹。

結果

實驗3

  • 自己寫一個決策樹,基本上就是仿造書上程式碼。
  • 程式碼就是書上的例題,如果要偷懶的話,直接把input.txt每個選項改一下就好了,我改了一下二叉樹的形狀,結果下面那堆數字就要全部改一下了
5 9 10
7 11 12
8 13 14
3 5 6
4 7 8
2 3 4
0 1 2

結果

實驗4

  • 第四個實驗要求輸入中綴表示式,使用樹將中綴表示式轉換為字尾表示式,並輸出字尾表示式和計算結果
  • 我覺得思路很簡單,就是先模仿之前的一個書上程式碼——用字尾表示式得到二叉樹,我們就用中綴表示式得到二叉樹,
public int evaluate(String expression) throws EmptyCollectionException {
        ExpressionTree operand1, operand2;
        char operator;
        String tempToken;

        Scanner parser = new Scanner(expression);

        while (parser.hasNext()) {
            tempToken = parser.next();
            operator = tempToken.charAt(0);

            if ((operator == '+') || (operator == '-') || (operator == '*') || (operator == '/'))
            {
                operand1 = getOperand(treeStack);
                operand2 = getOperand(treeStack);
                treeStack.push(new ExpressionTree(new ExpressionTreeOp(1, operator, 0), operand2, operand1));

            } else
            {
                treeStack.push(new ExpressionTree(new ExpressionTreeOp(2, ' ',
                        Integer.parseInt(tempToken)), null, null));
            }

        }

        return (treeStack.peek()).evaluateTree();
    }
  • 然後把這個二叉樹後序遍歷一遍就可以了。
public void posorder() {
        System.out.println("字尾表示式為: ");
        posOrder(root);
        System.out.println("");
    }

    public void posOrder(BinaryNode node) {
        if (node != null) {
            posOrder(node.getLeft());
            posOrder(node.getRight());
            System.out.print(node.getData() + " ");
        }
    }

結果

實驗5

  • 就是把以前的pp在執行一遍

removeMax方法

public T removeMax() throws EmptyCollectionException {
        // To be completed as a Programming Project
        T result = null;

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

            modCount--;
        }

        return result;
    }

findMin方法

public T findMin() throws EmptyCollectionException {
        // To be completed as a Programming Project
        T result = null;

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

結果

實驗6

HashMap

- HashMap可以說是Java中最常用的集合類框架之一,是Java語言中非常典型的資料結構,我們總會在不經意間用到它,很大程度上方便了我們日常開發。
- HashMap 是基於雜湊表的 Map 介面的實現。此實現提供所有可選的對映操作,並允許使用 null 值和 null 鍵。(除了非同步和允許使用 null 之外,HashMap 類與 Hashtable 大致相同。)此類不保證對映的順序,特別是它不保證該順序恆久不變。

- HashMap 的例項有兩個引數影響其效能:初始容量 和載入因子。容量 是雜湊表中桶的數量,初始容量只是雜湊表在建立時的容量。載入因子 是雜湊表在其容量自動增加之前可以達到多滿的一種尺度。當雜湊表中的條目數超出了載入因子與當前容量的乘積時,則要對該雜湊表進行 rehash 操作(即重建內部資料結構),從而雜湊表將具有大約兩倍的桶數。

建構函式和關鍵方法

  • HashMap遵循集合框架的約束,提供了一個引數為空的建構函式和有一個引數且引數型別為Map的建構函式。除此之外,還提供了兩個建構函式,用於設定HashMap的容量(capacity)與平衡因子(loadFactor)(平衡因子=|右子樹高度-左子樹高度|)。
// 預設建構函式。
public HashMap() {
    // 設定“載入因子”
    this.loadFactor = DEFAULT_LOAD_FACTOR;
    // 設定“HashMap閾值”,當HashMap中儲存資料的數量達到threshold時,就需要將HashMap的容量加倍。
    threshold = (int)(DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR);
    // 建立Entry陣列,用來儲存資料
    table = new Entry[DEFAULT_INITIAL_CAPACITY];
    init();
}

// 指定“容量大小”和“載入因子”的建構函式
public HashMap(int initialCapacity, float loadFactor) {
    if (initialCapacity < 0)
        throw new IllegalArgumentException("Illegal initial capacity: " +
                                           initialCapacity);
    // HashMap的最大容量只能是MAXIMUM_CAPACITY
    if (initialCapacity > MAXIMUM_CAPACITY)
        initialCapacity = MAXIMUM_CAPACITY;
    if (loadFactor <= 0 || Float.isNaN(loadFactor))
        throw new IllegalArgumentException("Illegal load factor: " +
                                           loadFactor);

    // Find a power of 2 >= initialCapacity
    int capacity = 1;
    while (capacity < initialCapacity)
        capacity <<= 1;

    // 設定“載入因子”
    this.loadFactor = loadFactor;
    // 設定“HashMap閾值”,當HashMap中儲存資料的數量達到threshold時,就需要將HashMap的容量加倍。
    threshold = (int)(capacity * loadFactor);
    // 建立Entry陣列,用來儲存資料
    table = new Entry[capacity];
    init();
}

// 指定“容量大小”的建構函式
public HashMap(int initialCapacity) {
    this(initialCapacity, DEFAULT_LOAD_FACTOR);
}

// 包含“子Map”的建構函式
public HashMap(Map<? extends K, ? extends V> m) {
    this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1,
                  DEFAULT_INITIAL_CAPACITY), DEFAULT_LOAD_FACTOR);
    // 將m中的全部元素逐個新增到HashMap中
    putAllForCreate(m);
}

  • containsKey()
    containsKey() 的作用是判斷HashMap是否包含key。
public boolean containsKey(Object key) {
    return getEntry(key) != null;
}

containsKey() 首先通過getEntry(key)獲取key對應的Entry,然後判斷該Entry是否為null。
getEntry()的原始碼如下:

final Entry<K,V> getEntry(Object key) {
    // 獲取雜湊值
    // HashMap將“key為null”的元素儲存在table[0]位置,“key不為null”的則呼叫hash()計算雜湊值
    int hash = (key == null) ? 0 : hash(key.hashCode());
    // 在“該hash值對應的連結串列”上查詢“鍵值等於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;
}

getEntry() 的作用就是返回“鍵為key”的鍵值對,它的實現原始碼中已經進行了說明。
這裡需要強調的是:HashMap將“key為null”的元素都放在table的位置0處,即table[0]中;“key不為null”的放在table的其餘位置!

  • put()
    put() 的作用是對外提供介面,讓HashMap物件可以通過put()將“key-value”新增到HashMap中。
public V put(K key, V value) {
    // 若“key為null”,則將該鍵值對新增到table[0]中。
    if (key == null)
        return putForNullKey(value);
    // 若“key不為null”,則計算該key的雜湊值,然後將其新增到該雜湊值對應的連結串列中。
    int hash = hash(key.hashCode());
    int i = indexFor(hash, table.length);
    for (Entry<K,V> e = table[i]; e != null; e = e.next) {
        Object k;
        // 若“該key”對應的鍵值對已經存在,則用新的value取代舊的value。然後退出!
        if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
            V oldValue = e.value;
            e.value = value;
            e.recordAccess(this);
            return oldValue;
        }
    }

    // 若“該key”對應的鍵值對不存在,則將“key-value”新增到table中
    modCount++;
    addEntry(hash, key, value, i);
    return null;
}
  • 若要新增到HashMap中的鍵值對對應的key已經存在HashMap中,則找到該鍵值對;然後新的value取代舊的value,並退出!
  • 若要新增到HashMap中的鍵值對對應的key不在HashMap中,則將其新增到該雜湊值對應的連結串列中,並呼叫addEntry()。
  • 說到addEntry(),就不得不說另一個函式createEntry()。

    addEntry()一般用在 新增Entry可能導致“HashMap的實際容量”超過“閾值”的情況下。例如,我們新建一個HashMap,然後不斷通過put()向HashMap中新增元素;put()是通過addEntry()新增Entry的。在這種情況下,我們不知道何時“HashMap的實際容量”會超過“閾值”;因此,需要呼叫addEntry()

createEntry() 一般用在 新增Entry不會導致“HashMap的實際容量”超過“閾值”的情況下。例如,我們呼叫HashMap“帶有Map”的建構函式,它繪將Map的全部元素新增到HashMap中;但在新增之前,我們已經計算好“HashMap的容量和閾值”。也就是,可以確定“即使將Map中的全部元素新增到HashMap中,都不會超過HashMap的閾值”。此時,呼叫createEntry()即可。

  • get()
    get() 的作用是獲取key對應的value,它的實現程式碼如下:
public V get(Object key) {
    if (key == null)
        return getForNullKey();
    // 獲取key的hash值
    int hash = hash(key.hashCode());
    // 在“該hash值對應的連結串列”上查詢“鍵值等於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.equals(k)))
            return e.value;
    }
    return null;
}
  • remove()
    remove() 的作用是刪除“鍵為key”元素
public V remove(Object key) {
    Entry<K,V> e = removeEntryForKey(key);
    return (e == null ? null : e.value);
}


// 刪除“鍵為key”的元素
final Entry<K,V> removeEntryForKey(Object key) {
    // 獲取雜湊值。若key為null,則雜湊值為0;否則呼叫hash()進行計算
    int hash = (key == null) ? 0 : hash(key.hashCode());
    int i = indexFor(hash, table.length);
    Entry<K,V> prev = table[i];
    Entry<K,V> e = prev;

    // 刪除連結串列中“鍵為key”的元素
    // 本質是“刪除單向連結串列中的節點”
    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)
                table[i] = next;
            else
                prev.next = next;
            e.recordRemoval(this);
            return e;
        }
        prev = e;
        e = next;
    }

    return e;
}

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

  • 問題1:在實現決策樹的時候,我發現了這堆看起來夏姬霸亂寫的東西他們其實是有排列順序的,然後在我想順便改改就行時,竟然不行!!!

  • 解決方案: 首先我們可以很容易的發現第一行的那個13是代表了方塊的個數。

因為這一大堆英語並不是很直觀,我在他們前面進行了編號。然後在我自己試著寫的時候發現0 1 2必須在最下面一排,1是左孩子,2是右孩子,再上面一排數字必須是先安排右孩子和他的孩子們,就是說必須是

2 5 6
1 3 4

而不是

1 3 4
2 5 6

除非沒有右孩子

四、感想

  • 腦闊有點疼,好像每次寫實驗報告都是大過節的吧,上次國慶來著,不過每次實驗都能找出以前程式碼裡的許多瑕疵和一些知識上的盲區,尤其是這次的實驗4和實驗6,我又學到了好多新知識呢。。。滿臉開心

五、參考資料