2018-2019-20172309 《程序設計與數據結構(下)》實驗二報告

課程:《程序設計與數據結構(下)》
班級:1723
姓名: 王志偉
學號:20172309
實驗教師:王志強老師
實驗日期:2018年11月2日
必修/選修: 必修html

實驗內容:

實驗一:實現二叉樹。

1.參考教材p212,完成鏈樹LinkedBinaryTree的實現(getRight,contains,toString,preorder,postorder).java

2.用JUnit或本身編寫驅動類對本身實現的LinkedBinaryTree進行測試,提交測試代碼運行截圖,要全屏,包含本身的學號信息.node

3.課下把代碼推送到代碼託管平臺.git

實驗二:前序、中序遍歷構造二叉樹。

1.基於LinkedBinaryTree,實現基於(中序,先序)序列構造惟一一棵二㕚樹的功能,好比給出中序HDIBEMJNAFCKGL和後序ABDHIEJMNCFGKL,構造出附圖中的樹.算法

2.用JUnit或本身編寫驅動類對本身實現的功能進行測試,提交測試代碼運行截圖,要全屏,包含本身的學號信息.chrome

3.課下把代碼推送到代碼託管平臺.express

實驗三:理解決策樹。

1.本身設計並實現一顆決策樹數組

2.提交測試代碼運行截圖,要全屏,包含本身的學號信息安全

3.課下把代碼推送到代碼託管平臺數據結構

實驗四:運用表達式樹。

1.完成PP11.3

2.提交測試代碼運行截圖,要全屏,包含本身的學號信息

3.課下把代碼推送到代碼託管平臺

實驗五:實現二叉查找樹。

1.完成PP11.3

2.提交測試代碼運行截圖,要全屏,包含本身的學號信息

3.課下把代碼推送到代碼託管平臺

實驗六:紅黑樹分析。

參考點擊這裏對Java中的紅黑樹(TreeMap,HashMap)進行源碼分析,並在實驗報告中體現分析結果。

(文件位於:C:\Program Files\Java\jdk-11.0.1\lib\src\java.base\java\util)

實驗過程及結果:

實驗一:

這個實驗是要求咱們實現幾個方法,而後進行測試。

  • 1.獲得右子樹:getright()
public LinkedBinaryTree<T> getRight()
    {
       return right;
    }
//這個類是一個二叉樹類,裏面屬性left、right,分別表明左子樹和右子樹,要想獲得右子樹,直接return right;便可!
  • 2.是否含有元素的方法:contains(T targeElement)

    這裏編寫了兩個方法,值得注意的是:一個是公有的(public),另外一個是私有的(private)。說明公有的方法用於外部類調用,而這個私有類的方法被用於用於公有方法

public boolean contains(T targetElement) 
    {
        return findNode(targetElement,root)!=null;//調用私有方法findNode()
    }

private BinaryTreeNode<T> findNode(T targetElement, 
                                        BinaryTreeNode<T> next)
    {//這個方法在樹中查找目標元素,沒找到時,分別在其左右子樹中查找、**運用了遞歸**!
        if (next == null)
            return null;
        
        if (next.getElement().equals(targetElement))
            return next;
        
        BinaryTreeNode<T> temp = findNode(targetElement, next.getLeft());
        
        if (temp == null)
            temp = findNode(targetElement, next.getRight());
        
        return temp;
    }
  • 輸出的方法:toString()

    這個輸出方法經過構建兩個無序列表,分別應於放置結點和對應的級數,好用於計算後面輸出的空格數。其中最難的是每一層相鄰的兩個結點之間的空格數。

public String toString() 
    {
        UnorderedListADT<BinaryTreeNode<T>> nodes =
                new UnorderedListArrayList<BinaryTreeNode<T>>();
        UnorderedListADT<Integer> levelList =
                new UnorderedListArrayList<Integer>();
        BinaryTreeNode<T> current;
        String result = "";
        int printDepth = this.getHeight();
        int possibleNodes = (int)Math.pow(2, printDepth + 1);
        int countNodes = 0;
        
        nodes.addToRear((BinaryTreeNode<T>) 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;
    }
  • 前序、後序遍歷方法:preOrder()

    前序遍歷的順序是:先遍歷該結點,而後是他們的孩子。
    所以用僞代碼表示爲://詳細代碼見here

Visit node;//前序遍歷
  preOrder(leftChild); 
  preOrder(rightChild);
postOrder(leftChild);
  postOrder(rightChild);
  visit node;//後序遍歷

運行結果:

實驗二:

1.實驗二是說給定咋們中序遍歷和後序遍歷的結果,讓後讓咋們生成一棵樹。科普:當給定前序和後序遍歷會結果得不到一棵樹,由於分不清左右結點

2.咱們基於一個事實:中序遍歷必定是 { 左子樹節點集合 }root{ 右子樹節點集合 }

3.後序序遍歷的做用就是找到每顆子樹的root位置

public void generateTree(T[] A,T[] B){//A、B兩個數組分別是放前序、後序遍歷結果的。
        BinaryTreeNode<T> temp = getTree(A, B);
        root=temp;
    }
    private BinaryTreeNode<T> getTree(T[] inOrder,T [] postOrder){
        int length = postOrder.length;
        T rootElement = postOrder[postOrder.length-1];
       BinaryTreeNode<T> temp = new BinaryTreeNode<T>(rootElement);
       
        if (length==1)
            return temp;
        else{
            int index = 0;
            while (inOrder[index]!=rootElement)
                index++;

            if (index>0){//分界結點的左邊又能夠生成一個子中序遍歷數組和後序遍歷數組。以後運用遞歸。
                T [] leftInOrder = (T[])new Object[index];
                for(int i=0;i<leftInOrder.length;i++)
                    leftInOrder[i]=inOrder[i];
                T [] leftPostOrder = (T[])new Object[index];
                for (int i=0;i<leftPostOrder.length;i++)
                    leftPostOrder[i] = postOrder[i];
                temp.setLeft(getTree(leftInOrder,leftPostOrder));
            }
            if (length-index-1>0){//右邊也同樣,生成子兩個數組,運用遞歸。
                T [] rightInOrder = (T[])new Object[length-index-1];
                for (int i=0;i<length-index-1;i++)
                    rightInOrder[i]=inOrder[i+index+1];
                T [] rightPostOrder = (T[])new Object[length-index-1];
                for (int i=0;i<length-index-1;i++)
                    rightPostOrder[i]=postOrder[i+index];
                temp.setRight(getTree(rightInOrder,rightPostOrder));
            }
        }
        return temp;
    }
  • 運行結果:

實驗三:

  • 這個實驗就是本身製做一棵決策樹,咱們只要修改TXT文件就OK!
  • 修改後的文件爲:
  • 運行結果爲:

    實驗四:

  • 該實驗是要求咱們用樹的性質將一箇中綴表達式轉換成後綴表達式,並獲得計算結果。
  • 所以咱們大概能夠想到:它的答題構架是這樣的
public ExpressionTree getPostfixTree(String expression){//傳進去的是一箇中綴表達式
    
    。。。一大串代碼。。。
    
    return EXpressionTree;//通過一大串代碼獲得一顆表達式樹。
    
    }
  • 通過我TM打十把王者的時間,把它寫好了。
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 == '/')){
                if (ope.empty())
                    ope.push(tempToken);//當儲存符號的棧爲空時,直接進棧
                else{
                    String a =ope.peek()+"";//由於當ope.peek()='-'時,計算機認爲ope.peek()=='-'爲false,因此要轉化爲string 使用equals()方法
                    if (((a.equals("+"))||(a.equals("-")))&&((operator=='*')||(operator=='/')))
                        ope.push(tempToken);//當獲得的符號的優先級大於棧頂元素時,直接進棧
                    else {
                        String s = String.valueOf(ope.pop());
                        char temp = s.charAt(0);
                            operand1 = getOperand(treeExpression);
                            operand2 = getOperand(treeExpression);
                            treeExpression.push(new ExpressionTree
                                    (new ExpressionTreeOp(1, temp, 0), operand2, operand1));
                            ope.push(operator);
                    }//當獲得的符號的優先級小於棧頂元素或者優先級相同時時,數字棧出來兩個運算數,造成新的樹進棧
                }
            }
            else
                treeExpression.push(new ExpressionTree(new ExpressionTreeOp
                        (2,' ',Integer.parseInt(tempToken)), null, null));
        }
        while(!ope.empty()){
           String a = String.valueOf(ope.pop());
           operator = a.charAt(0);
            operand1 = getOperand(treeExpression);
            operand2 = getOperand(treeExpression);
            treeExpression.push(new ExpressionTree
                    (new ExpressionTreeOp(1, operator, 0), operand2, operand1));
        }
        return treeExpression.peek();
  • 運行結果爲:

    生成樹後,咱們發現把這顆樹直接後序遍歷就能夠獲得後綴表達式。詳細代碼click here

實驗五:

  • 實驗要求爲完成findMax()、removeMax()、findMin()方法。
  • 咱們必須明白,二叉查找樹具備左子樹小於父結點,右子樹大於或等於父結點的性質。所以樹的最左側會放置最小元素、而最右側會放置最大元素
  • 代碼以下:

    去除最小元素方法。

public T removeMin() throws EmptyCollectionException 
    {
        T result = null;

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

            modCount--;
        }
 
        return result;
    }

去除最大元素方法。

public T removeMax() throws EmptyCollectionException
    {
        T result= null;

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

        return result;
    }

找到最大元素方法。

public T findMax() throws EmptyCollectionException {
        if (isEmpty())
            System.out.println("BinarySearchTree is empty!");

        return findmax(root).element;
    }
  • 實驗結果:

實驗六:

  • 首先來介紹什麼是Map:

    1. 在數組中咱們是經過數組下標來對其內容索引的,而在Map中咱們經過對象來對對象進行索引,用來索引的對象叫作key,其對應的對象叫作value。這就是咱們平時說的鍵值對.
    2. 他們有一個顯著的特徵:HashMap經過hashcode對其內容進行快速查找,而 TreeMap中全部的元素都保持着某種固定的順序,若是你須要獲得一個有序的結果你就應該使用TreeMap(HashMap中元素的排列順序是不固定的)。
  • 先看看他們兩的類頭:
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抽象類和SortedMap接口
    • AbstractMap抽象類:(HashMap繼承AbstractMap)覆蓋了equals()和hashCode()方法以確保兩個相等映射返回相同的哈希碼。若是兩個映射大小相等、包含一樣的鍵且每一個鍵在這兩個映射中對應的值都相同,則這兩個映射相等。映射的哈希碼是映射元素哈希碼的總和,其中每一個元素是Map.Entry接口的一個實現。所以,不論映射內部順序如何,兩個相等映射會報告相同的哈希碼。
    • SortedMap接口:(TreeMap繼承自SortedMap)它用來保持鍵的有序順序。SortedMap接口爲映像的視圖(子集),包括兩個端點提供了訪問方法。除了排序是做用於映射的鍵之外,處理SortedMap和處理SortedSet同樣。添加到SortedMap實現類的元素必須實現Comparable接口,不然您必須給它的構造函數提供一個Comparator接口的實現。TreeMap類是它的惟一一份實現。
  • 兩種常規Map實現
    • HashMap:基於哈希表實現。使用HashMap要求添加的鍵類明肯定義了hashCode()和equals()[能夠重寫hashCode()和equals()],爲了優化HashMap空間的使用,您能夠調優初始容量和負載因子。
      • HashMap(): 構建一個空的哈希映像
      • HashMap(Map m): 構建一個哈希映像,而且添加映像m的全部映射
      • HashMap(int initialCapacity): 構建一個擁有特定容量的空的哈希映像
      • HashMap(int initialCapacity, float loadFactor): 構建一個擁有特定容量和加載因子的空的哈希映像
    • TreeMap:基於紅黑樹實現。TreeMap沒有調優選項,由於該樹總處於平衡狀態。
      • TreeMap():構建一個空的映像樹
      • TreeMap(Map m): 構建一個映像樹,而且添加映像m中全部元素
      • TreeMap(Comparator c): 構建一個映像樹,而且使用特定的比較器對關鍵字進行排序
        -TreeMap(SortedMap s): 構建一個映像樹,添加映像樹s中全部映射,而且使用與有序映像s相同的比較器排序
  • 兩種常規Map性能
    HashMap:適用於在Map中插入、刪除和定位元素。
    Treemap:適用於按天然順序或自定義順序遍歷鍵(key)。

  • 總結
    HashMap一般比TreeMap快一點(樹和哈希表的數據結構使然),建議多使用HashMap,在須要排序的Map時候才用TreeMap。

HashMap代碼分析:

  • HashMap類源代碼:
public class HashMap<K,V>extends AbstractMap<K,V>
    implements Map<K,V>, Cloneable, Serializable
{
    /**
     * The default initial capacity - MUST be a power of two.
     * 默認的容量必須爲2的冪
     */
    static final int DEFAULT_INITIAL_CAPACITY = 16;

    /**
     * The maximum capacity, used if a higher value is implicitly specified
     * by either of the constructors with arguments.
     * MUST be a power of two <= 1<<30.
     *默認最大值
     */
    static final int MAXIMUM_CAPACITY = 1 << 30;

    /**
     * The load factor used when none specified in constructor.
     * 負載因子 
    */
    static final float DEFAULT_LOAD_FACTOR = 0.75f;

    /**
     * The table, resized as necessary. Length MUST Always be a power of two.
     * 到這裏就發現了,HashMap就是一個Entry[]類型的數組了。
     */
    transient Entry<K,V>[] table;
  • HashMap類構造函數源代碼:
/**    
  * Constructs an empty <tt>HashMap</tt> with the specified initial    
  * capacity and load factor.     
  * @param  initialCapacity the initial capacity   
  * @param  loadFactor      the load factor    
  * @throws IllegalArgumentException if the initial capacity is negative    
  *   or the load factor is nonpositive
  */
   // 初始容量(必須是2的n次冪),負載因子
   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);     
        // Find a power of 2 >= initialCapacity
       int capacity = 1;
       // 獲取最小於initialCapacity的最大值,這個值是2的n次冪,因此咱們定義初始容量的時候儘可能寫2的冪
       while (capacity < initialCapacity)
      // 使用位移計算效率更高
      capacity <<= 1;       
      this.loadFactor = loadFactor;
      //哈希表的最大容量的計算,取兩個值中小的一個  
      threshold = (int)Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
      //建立容量爲capacity的Entry[]類型的數組
      table = new Entry[capacity];
       useAltHashing = sun.misc.VM.isBooted() && (capacity >=                   Holder.ALTERNATIVE_HASHING_THRESHOLD);       
init();
    }
  • HashMap 的實例有兩個參數影響其性能:初始容量載入因子。容量是哈希表中桶的數量。初始容量僅僅是哈希表在建立時的容量。載入因子是哈希表在其容量本身主動添加以前可以達到多滿的一種尺度。當哈希表中的條目數超出了載入因子與當前容量的乘積時,則要對該哈希表進行rehash 操做(即重建內部數據結構),從而哈希表將具備大約兩倍的桶數。
  • HashMap底層是哈希表實現
  • HashMap--put方法
public V put(K key, V value) {
      //key爲null的entry老是放在數組的頭節點上,也就是上面說的"桶"中
      if (key == null)
        return putForNullKey(value);
      // 獲取key的哈希值
      int hash = hash(key);
      // 經過key的哈希值和table的長度取模肯定‘桶’(bucket)的位置
      int i = indexFor(hash, table.length);
      for (Entry<K,V> e = table[i]; e != null; e = e.next) {
          Object k;
         //假設key映射的entry在鏈表中已存在,則entry的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;
            }
        }
        modCount++;
        addEntry(hash, key, value, i);
        return null;
    }
  • addEntry(hash,key,value,i)方法:
//bucketIndex  桶的索引值,桶中僅僅能存儲一個值(一個Entry 對象)也就是頭節點
 void addEntry(int hash, K key, V value, int bucketIndex) {
    // 假設數組中存儲的元素個數大於數組的臨界值(這個臨界值就是 數組長度*負載因子的值 )則進行擴容
      if ((size >= threshold) && (null != table[bucketIndex])) {
    // 擴容,將大小擴爲原來的兩倍
       resize(2 * table.length);
        hash = (null != key) ?
 hash(key) : 0;
        bucketIndex = indexFor(hash, table.length);
       }
      createEntry(hash, key, value, bucketIndex);
    }

總結:

  1. HashMap 是鏈式數組(存儲鏈表的數組)實現查詢速度可以。而且能高速的獲取key相應的value;

  2. 查詢速度的影響因素有 容量和負載因子,容量大負載因子小查詢速度快但浪費空間,反之則相反。

  3. 數組的index值是(key keyword, hashcode爲key的哈希值。 len 數組的大小):hashcode%len的值來肯定,假設容量大負載因子小則index一樣(index一樣也就是指向了同一個桶)的機率小。鏈表長度小則查詢速度快。反之index一樣的機率大鏈表比較長查詢速度慢。

  4. 對於HashMap以及其子類來講。他們是採用hash算法來決定集合中元素的存儲位置,當初始化HashMap的時候系統會建立一個長度爲capacity的Entry數組,這個數組裏可以存儲元素的位置稱爲桶(bucket),每一個桶都有其指定索引,系統可以依據索引高速訪問該桶中存儲的元素。

  5. 無論什麼時候HashMap 中的每個桶都僅僅存儲一個元素(Entry 對象)。
    由於Entry對象可以包括一個引用變量用於指向下一個Entry,所以可能出現HashMap 的桶(bucket)中僅僅有一個Entry,但這個Entry指向還有一個Entry 這樣就造成了一個Entry 鏈。

  6. 經過上面的源代碼發現HashMap在底層將key_value對當成一個整體進行處理(Entry 對象)這個整體就是一個Entry對象,當系統決定存儲HashMap中的key_value對時,全然沒有考慮Entry中的value,而不過依據key的hash值來決定每個Entry的存儲位置。

實驗過程當中遇到的問題:

  • 問題一.爲何爲實現某一個功能,通常都寫兩個方法:Public、Private方法,而後咱們用就調用這個pubic方法?好比上面的實驗二中的generateTree()方法與getTree()方法?
    • 通過查資料知道這樣作主要有兩個好處:
      • 一. 從代碼上看,以上面的實驗二方法爲例、二者的傳入數據雖然相同,都是兩個數組。可是他們的返回值不一樣,也就是說:有返回值的方法,使用該方法時可以獲得一些類型的數據再來利用。
      • 二. 從總體上看,這是一種保護機制,不讓本身這個類之外的方法去隨便使用這個類的數據,能夠保護他的數據,只能經過調用本身類的方法去操縱這些數據。這樣會很安全。
  • 問題二.實驗三TXT文件中的數字是什麼東西?
    • 通過與侯澤洋討論才知道,這個東西是用於構建樹的,而數字是它們每一個數字的索引。
      像是這樣:
    • 還有一個問題就是爲何他的順序是

      3 7 8
      4 9 10
      5 11 12
      1 3 4
      2 5 6
      0 1 2//這樣的

      0 1 2
      2 5 6
      1 3 6
      5 11 12
      4 9 10
      3 7 8//而非得是倒着來呢?

    • 其實這樣是很合理的,由於咱們構建一顆決策樹時,是先構建子樹,而後再把子樹聯合起來的。因此是倒着來的。
  • 問題三及問題四.進行實驗四的時候出現:

    -而後通過自習調試才發現是判斷條件出現了問題:當ope.peek()=「+」時,他認爲ope.peek()=="+"爲false,我也不知道怎麼回事!
    • 通過修改:ope.Peek().equals("+")解決了這個問題。

    • 可是在後面又出現了問題:結果代表又是符號優先級出現問題!可是我保證、後面的答案也證實個人邏輯沒錯!
    • 通過調試,仍是發現那個判斷條件出現了問題!!!煩的一批!!!
      • 以後我直接把全部的數據轉換成String類型再來判斷、才解決了問題!

收穫感悟

這一章的實驗雖然只有幾個難一點的,好比實驗四和實驗六,但確實讓人更頭疼!他別是實驗四,看到在實驗提交前好多人都無論在什麼課上都在想怎麼作!而後本身在三四小時就把它解決了,想一想仍是能夠的!也許這就是不間斷地思考一個問題的好處吧!

參考文獻:

相關文章
相關標籤/搜索