TreeMap原理實現及經常使用方法

前面咱們分別講了Map接口的兩個實現類HashMapLinkedHashMap,本章咱們講一下Map接口另外一個重要的實現類TreeMap,TreeMap或許不如HashMap那麼經常使用,但存在即合理,它也有本身的應用場景,TreeMap能夠實現元素的自動排序。java

一. TreeMap概述

  1. TreeMap存儲K-V鍵值對,經過紅黑樹(R-B tree)實現;
  2. TreeMap繼承了NavigableMap接口,NavigableMap接口繼承了SortedMap接口,可支持一系列的導航定位以及導航操做的方法,固然只是提供了接口,須要TreeMap本身去實現;
  3. TreeMap實現了Cloneable接口,可被克隆,實現了Serializable接口,可序列化;
  4. TreeMap由於是經過紅黑樹實現,紅黑樹結構自然支持排序,默認狀況下經過Key值的天然順序進行排序;

二. 紅黑樹回顧

由於TreeMap的存儲結構是紅黑樹,咱們回顧一下紅黑樹的特色以及基本操做,紅黑樹的原理可參考關於紅黑樹(R-B tree)原理,看這篇如何。下圖爲典型的紅黑樹:node

紅黑樹規則特色:算法

  1. 節點分爲紅色或者黑色;
  2. 根節點必爲黑色;
  3. 葉子節點都爲黑色,且爲null;
  4. 鏈接紅色節點的兩個子節點都爲黑色(紅黑樹不會出現相鄰的紅色節點);
  5. 從任意節點出發,到其每一個葉子節點的路徑中包含相同數量的黑色節點;
  6. 新加入到紅黑樹的節點爲紅色節點;

紅黑樹自平衡基本操做:數據結構

  1. 變色:在不違反上述紅黑樹規則特色狀況下,將紅黑樹某個node節點顏色由紅變黑,或者由黑變紅;
  2. 左旋:逆時針旋轉兩個節點,讓一個節點被其右子節點取代,而該節點成爲右子節點的左子節點
  3. 右旋:順時針旋轉兩個節點,讓一個節點被其左子節點取代,而該節點成爲左子節點的右子節點

三. TreeMap構造

咱們先看一下TreeMap中主要的成員變量app

/**
 * 咱們前面提到TreeMap是能夠自動排序的,默認狀況下comparator爲null,這個時候按照key的天然順序進行排
 * 序,然而並非全部狀況下均可以直接使用key的天然順序,有時候咱們想讓Map的自動排序按照咱們本身的規則,
 * 這個時候你就須要傳遞Comparator的實現類
 */
private final Comparator<? super K> comparator;

/**
 * TreeMap的存儲結構既然是紅黑樹,那麼必然會有惟一的根節點。
 */
private transient Entry<K,V> root;

/**
 * Map中key-val對的數量,也便是紅黑樹中節點Entry的數量
 */
private transient int size = 0;

/**
 * 紅黑樹結構的調整次數
 */
private transient int modCount = 0;

上面的主要成員變量根節點root是Entry類的實體,咱們來看一下Entry類的源碼函數

static final class Entry<K,V> implements Map.Entry<K,V> {
    //key,val是存儲的原始數據
    K key;
    V value;
    //定義了節點的左孩子
    Entry<K,V> left;
    //定義了節點的右孩子
    Entry<K,V> right;
    //經過該節點能夠反過來往上找到本身的父親
    Entry<K,V> parent;
    //默認狀況下爲黑色節點,可調整
    boolean color = BLACK;

    /**
     * 構造器
     */
    Entry(K key, V value, Entry<K,V> parent) {
        this.key = key;
        this.value = value;
        this.parent = parent;
    }

    /**
     * 獲取節點的key值
     */
    public K getKey() {return key;}

    /**
     * 獲取節點的value值
     */
    public V getValue() {return value;}

    /**
     * 用新值替換當前值,並返回當前值
     */
    public V setValue(V value) {
        V oldValue = this.value;
        this.value = value;
        return oldValue;
    }

    public boolean equals(Object o) {
        if (!(o instanceof Map.Entry))
            return false;
        Map.Entry<?,?> e = (Map.Entry<?,?>)o;
        return valEquals(key,e.getKey()) && valEquals(value,e.getValue());
    }

    public int hashCode() {
        int keyHash = (key==null ? 0 : key.hashCode());
        int valueHash = (value==null ? 0 : value.hashCode());
        return keyHash ^ valueHash;
    }

    public String toString() {
        return key + "=" + value;
    }
}

Entry靜態內部類實現了Map的內部接口Entry,提供了紅黑樹存儲結構的java實現,經過left屬性能夠創建左子樹,經過right屬性能夠創建右子樹,經過parent能夠往上找到父節點。動畫

大致的實現結構圖以下:ui

TreeMap構造函數:this

//默認構造函數,按照key的天然順序排列
public TreeMap() {comparator = null;}
//傳遞Comparator具體實現,按照該實現規則進行排序
public TreeMap(Comparator<? super K> comparator) {this.comparator = comparator;}
//傳遞一個map實體構建TreeMap,按照默認規則排序
public TreeMap(Map<? extends K, ? extends V> m) {
    comparator = null;
    putAll(m);
}
//傳遞一個map實體構建TreeMap,按照傳遞的map的排序規則進行排序
public TreeMap(SortedMap<K, ? extends V> m) {
    comparator = m.comparator();
    try {
        buildFromSorted(m.size(), m.entrySet().iterator(), null, null);
    } catch (java.io.IOException cannotHappen) {
    } catch (ClassNotFoundException cannotHappen) {
    }
}

四. put方法

put方法爲Map的核心方法,TreeMap的put方法大概流程以下:

咱們來分析一下源碼

public V put(K key, V value) {
    Entry<K,V> t = root;
    /**
     * 若是根節點都爲null,還沒創建起來紅黑樹,咱們先new Entry並賦值給root把紅黑樹創建起來,這個時候紅
     * 黑樹中已經有一個節點了,同時修改操做+1。
     */
    if (t == null) {
        compare(key, key); 
        root = new Entry<>(key, value, null);
        size = 1;
        modCount++;
        return null;
    }
    /**
     * 若是節點不爲null,定義一個cmp,這個變量用來進行二分查找時的比較;定義parent,是new Entry時必須
     * 要的參數
     */
    int cmp;
    Entry<K,V> parent;
    // cpr表示有無本身定義的排序規則,分兩種狀況遍歷執行
    Comparator<? super K> cpr = comparator;
    if (cpr != null) {
        /**
         * 從root節點開始遍歷,經過二分查找逐步向下找
         * 第一次循環:從根節點開始,這個時候parent就是根節點,而後經過自定義的排序算法
         * cpr.compare(key, t.key)比較傳入的key和根節點的key值,若是傳入的key<root.key,那麼
         * 繼續在root的左子樹中找,從root的左孩子節點(root.left)開始:若是傳入的key>root.key,
         * 那麼繼續在root的右子樹中找,從root的右孩子節點(root.right)開始;若是剛好key==root.key,
         * 那麼直接根據root節點的value值便可。
         * 後面的循環規則同樣,當遍歷到的當前節點做爲起始節點,逐步往下找
         *
         * 須要注意的是:這裏並無對key是否爲null進行判斷,建議本身的實現Comparator時應該要考慮在內
         */
        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 {
        //從這裏看出,當默認排序時,key值是不能爲null的
        if (key == null)
            throw new NullPointerException();
        @SuppressWarnings("unchecked")
        Comparable<? super K> k = (Comparable<? super K>) key;
        //這裏的實現邏輯和上面同樣,都是經過二分查找,就再也不多說了
        do {
            parent = t;
            cmp = k.compareTo(t.key);
            if (cmp < 0)
                t = t.left;
            else if (cmp > 0)
                t = t.right;
            else
                return t.setValue(value);
        } while (t != null);
    }
    /**
     * 能執行到這裏,說明前面並無找到相同的key,節點已經遍歷到最後了,咱們只須要new一個Entry放到
     * parent下面便可,但放到左子節點上仍是右子節點上,就須要按照紅黑樹的規則來。
     */
    Entry<K,V> e = new Entry<>(key, value, parent);
    if (cmp < 0)
        parent.left = e;
    else
        parent.right = e;
    /**
     * 節點加進去了,並不算完,咱們在前面紅黑樹原理章節提到過,通常狀況下加入節點都會對紅黑樹的結構形成
     * 破壞,咱們須要經過一些操做來進行自動平衡處置,如【變色】【左旋】【右旋】
     */
    fixAfterInsertion(e);
    size++;
    modCount++;
    return null;
}

put方法源碼中經過fixAfterInsertion(e)方法來進行自平衡處理,咱們回顧一下插入時自平衡調整的邏輯,下表中看不懂的名詞能夠參考關於紅黑樹(R-B tree)原理,看這篇如何

無需調整 【變色】便可實現平衡 【旋轉+變色】纔可實現平衡
狀況1: 當父節點爲黑色時插入子節點 空樹插入根節點,將根節點紅色變爲黑色 父節點爲紅色左節點,叔父節點爲黑色,插入左子節點,那麼經過【左左節點旋轉】
狀況2: - 父節點和叔父節點都爲紅色 父節點爲紅色左節點,叔父節點爲黑色,插入右子節點,那麼經過【左右節點旋轉】
狀況3: - - 父節點爲紅色右節點,叔父節點爲黑色,插入左子節點,那麼經過【右左節點旋轉】
狀況4: - - 父節點爲紅色右節點,叔父節點爲黑色,插入右子節點,那麼經過【右右節點旋轉】

接下來咱們看一看這個方法

private void fixAfterInsertion(Entry<K,V> x) {
    //新插入的節點爲紅色節點
    x.color = RED;
    //咱們知道父節點爲黑色時,並不須要進行樹結構調整,只有當父節點爲紅色時,才須要調整
    while (x != null && x != root && x.parent.color == RED) {
        //若是父節點是左節點,對應上表中狀況1和狀況2
        if (parentOf(x) == leftOf(parentOf(parentOf(x)))) {
            Entry<K,V> y = rightOf(parentOf(parentOf(x)));
            //若是叔父節點爲紅色,對應於「父節點和叔父節點都爲紅色」,此時經過變色便可實現平衡
            //此時父節點和叔父節點都設置爲黑色,祖父節點設置爲紅色
            if (colorOf(y) == RED) {
                setColor(parentOf(x), BLACK);
                setColor(y, BLACK);
                setColor(parentOf(parentOf(x)), RED);
                x = parentOf(parentOf(x));
            } else {
                //若是插入節點是黑色,插入的是右子節點,經過【左右節點旋轉】(這裏先進行父節點左旋)
                if (x == rightOf(parentOf(x))) {
                    x = parentOf(x);
                    rotateLeft(x);
                }
                //設置父節點和祖父節點顏色
                setColor(parentOf(x), BLACK);
                setColor(parentOf(parentOf(x)), RED);
                //進行祖父節點右旋(這裏【變色】和【旋轉】並無嚴格的前後順序,達成目的就行)
                rotateRight(parentOf(parentOf(x)));
            }
        } else {
            //父節點是右節點的狀況
            Entry<K,V> y = leftOf(parentOf(parentOf(x)));
            //對應於「父節點和叔父節點都爲紅色」,此時經過變色便可實現平衡
            if (colorOf(y) == RED) {
                setColor(parentOf(x), BLACK);
                setColor(y, BLACK);
                setColor(parentOf(parentOf(x)), RED);
                x = parentOf(parentOf(x));
            } else {
                //若是插入節點是黑色,插入的是左子節點,經過【右左節點旋轉】(這裏先進行父節點右旋)
                if (x == leftOf(parentOf(x))) {
                    x = parentOf(x);
                    rotateRight(x);
                }
                setColor(parentOf(x), BLACK);
                setColor(parentOf(parentOf(x)), RED);
                //進行祖父節點左旋(這裏【變色】和【旋轉】並無嚴格的前後順序,達成目的就行)
                rotateLeft(parentOf(parentOf(x)));
            }
        }
    }
    //根節點必須爲黑色
    root.color = BLACK;
}

源碼中經過 rotateLeft 進行【左旋】,經過 rotateRight 進行【右旋】。都很是相似,咱們就看一下【左旋】的代碼,【左旋】規則以下:「逆時針旋轉兩個節點,讓一個節點被其右子節點取代,而該節點成爲右子節點的左子節點」。

private void rotateLeft(Entry<K,V> p) {
    if (p != null) {
        /**
         * 斷開當前節點p與其右子節點的關聯,從新將節點p的右子節點的地址指向節點p的右子節點的左子節點
         * 這個時候節點r沒有父節點
         */
        Entry<K,V> r = p.right;
        p.right = r.left;
        //將節點p做爲節點r的父節點
        if (r.left != null)
            r.left.parent = p;
        //將節點p的父節點和r的父節點指向同一處
        r.parent = p.parent;
        //p的父節點爲null,則將節點r設置爲root
        if (p.parent == null)
            root = r;
        //若是節點p是左子節點,則將該左子節點替換爲節點r
        else if (p.parent.left == p)
            p.parent.left = r;
        //若是節點p爲右子節點,則將該右子節點替換爲節點r
        else
            p.parent.right = r;
        //從新創建p與r的關係
        r.left = p;
        p.parent = r;
    }
}

就算是看了上面的註釋仍是並不清晰,看下圖你就懂了

五. get 方法

get方法是經過二分查找的思想,咱們看一下源碼

public V get(Object key) {
    Entry<K,V> p = getEntry(key);
    return (p==null ? null : p.value);
}
/**
 * 從root節點開始遍歷,經過二分查找逐步向下找
 * 第一次循環:從根節點開始,這個時候parent就是根節點,而後經過k.compareTo(p.key)比較傳入的key和
 * 根節點的key值;
 * 若是傳入的key<root.key, 那麼繼續在root的左子樹中找,從root的左孩子節點(root.left)開始;
 * 若是傳入的key>root.key, 那麼繼續在root的右子樹中找,從root的右孩子節點(root.right)開始;
 * 若是剛好key==root.key,那麼直接根據root節點的value值便可。
 * 後面的循環規則同樣,當遍歷到的當前節點做爲起始節點,逐步往下找
 */
//默認排序狀況下的查找
final Entry<K,V> getEntry(Object key) {
    
    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 (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;
}
/**
 * 從root節點開始遍歷,經過二分查找逐步向下找
 * 第一次循環:從根節點開始,這個時候parent就是根節點,而後經過自定義的排序算法
 * cpr.compare(key, t.key)比較傳入的key和根節點的key值,若是傳入的key<root.key,那麼
 * 繼續在root的左子樹中找,從root的左孩子節點(root.left)開始:若是傳入的key>root.key,
 * 那麼繼續在root的右子樹中找,從root的右孩子節點(root.right)開始;若是剛好key==root.key,
 * 那麼直接根據root節點的value值便可。
 * 後面的循環規則同樣,當遍歷到的當前節點做爲起始節點,逐步往下找
 */
//自定義排序規則下的查找
final Entry<K,V> getEntryUsingComparator(Object key) {
    @SuppressWarnings("unchecked")
    K k = (K) key;
    Comparator<? super K> cpr = comparator;
    if (cpr != null) {
        Entry<K,V> p = root;
        while (p != null) {
            int cmp = cpr.compare(k, p.key);
            if (cmp < 0)
                p = p.left;
            else if (cmp > 0)
                p = p.right;
            else
                return p;
        }
    }
    return null;
}

六. remove方法

remove方法能夠分爲兩個步驟,先是找到這個節點,直接調用了上面介紹的getEntry(Object key),這個步驟咱們就不說了,直接說第二個步驟,找到後的刪除操做。

public V remove(Object key) {
    Entry<K,V> p = getEntry(key);
    if (p == null)
        return null;

    V oldValue = p.value;
    deleteEntry(p);
    return oldValue;
}

經過deleteEntry(p)進行刪除操做,刪除操做的原理咱們在前面已經講過

  1. 刪除的是根節點,則直接將根節點置爲null;
  2. 待刪除節點的左右子節點都爲null,刪除時將該節點置爲null;
  3. 待刪除節點的左右子節點有一個有值,則用有值的節點替換該節點便可;
  4. 待刪除節點的左右子節點都不爲null,則找前驅或者後繼,將前驅或者後繼的值複製到該節點中,而後刪除前驅或者後繼(前驅:左子樹中值最大的節點,後繼:右子樹中值最小的節點);
private void deleteEntry(Entry<K,V> p) {
    modCount++;
    size--;
    //當左右子節點都不爲null時,經過successor(p)遍歷紅黑樹找到前驅或者後繼
    if (p.left != null && p.right != null) {
        Entry<K,V> s = successor(p);
        //將前驅或者後繼的key和value複製到當前節點p中,而後刪除節點s(經過將節點p引用指向s)
        p.key = s.key;
        p.value = s.value;
        p = s;
    } 
    Entry<K,V> replacement = (p.left != null ? p.left : p.right);
    /**
     * 至少有一個子節點不爲null,直接用這個有值的節點替換掉當前節點,給replacement的parent屬性賦值,給
     * parent節點的left屬性和right屬性賦值,同時要記住葉子節點必須爲null,而後用fixAfterDeletion方法
     * 進行自平衡處理
     */
    if (replacement != null) {
        //將待刪除節點的子節點掛到待刪除節點的父節點上。
        replacement.parent = p.parent;
        if (p.parent == null)
            root = replacement;
        else if (p == p.parent.left)
            p.parent.left  = replacement;
        else
            p.parent.right = replacement;
        p.left = p.right = p.parent = null;
        /**
         * p若是是紅色節點的話,那麼其子節點replacement必然爲紅色的,並不影響紅黑樹的結構
         * 但若是p爲黑色節點的話,那麼其父節點以及子節點均可能是紅色的,那麼很明顯可能會存在紅色相連的情
         * 況,所以須要進行自平衡的調整
         */
        if (p.color == BLACK)
            fixAfterDeletion(replacement);
    } else if (p.parent == null) {//這種狀況就不用多說了吧
        root = null;
    } else { 
        /**
         * 若是p節點爲黑色,那麼p節點刪除後,就可能違背每一個節點到其葉子節點路徑上黑色節點數量一致的規則,
         * 所以須要進行自平衡的調整
         */ 
        if (p.color == BLACK)
            fixAfterDeletion(p);
        if (p.parent != null) {
            if (p == p.parent.left)
                p.parent.left = null;
            else if (p == p.parent.right)
                p.parent.right = null;
            p.parent = null;
        }
    }
}

操做的操做其實很簡單,場景也很少,咱們看一下刪除後的自平衡操做方法fixAfterDeletion

private void fixAfterDeletion(Entry<K,V> x) {
    /**
     * 當x不是root節點且顏色爲黑色時
     */
    while (x != root && colorOf(x) == BLACK) {
        /**
         * 首先分爲兩種狀況,當前節點x是左節點或者當前節點x是右節點,這兩種狀況下面都是四種場景,這裏經過
         * 代碼分析一下x爲左節點的狀況,右節點可參考左節點理解,由於它們很是相似
         */
        if (x == leftOf(parentOf(x))) {
            Entry<K,V> sib = rightOf(parentOf(x));

            /**
             * 場景1:當x是左黑色節點,兄弟節點sib是紅色節點
             * 兄弟節點由紅轉黑,父節點由黑轉紅,按父節點左旋,
             * 左旋後樹的結構變化了,這時從新賦值sib,這個時候sib指向了x的兄弟節點
             */
            if (colorOf(sib) == RED) {
                setColor(sib, BLACK);
                setColor(parentOf(x), RED);
                rotateLeft(parentOf(x));
                sib = rightOf(parentOf(x));
            }

            /**
             * 場景2:節點x、x的兄弟節點sib、sib的左子節點和右子節點都爲黑色時,須要將該節點sib由黑變
             * 紅,同時將x指向當前x的父節點
             */
            if (colorOf(leftOf(sib))  == BLACK &&
                colorOf(rightOf(sib)) == BLACK) {
                setColor(sib, RED);
                x = parentOf(x);
            } else {
                /**
                 * 場景3:節點x、x的兄弟節點sib、sib的右子節點都爲黑色,sib的左子節點爲紅色時,
                 * 須要將sib左子節點設置爲黑色,sib節點設置爲紅色,同時按sib右旋,再將sib指向x的
                 * 兄弟節點
                 */
                if (colorOf(rightOf(sib)) == BLACK) {
                    setColor(leftOf(sib), BLACK);
                    setColor(sib, RED);
                    rotateRight(sib);
                    sib = rightOf(parentOf(x));
                }
                /**
                 * 場景4:節點x、x的兄弟節點sib都爲黑色,而sib的左右子節點都爲紅色或者右子節點爲紅色、
                 * 左子節點爲黑色,此時須要將sib節點的顏色設置成和x的父節點p相同的顏色,
                 * 設置x的父節點爲黑色,設置sib右子節點爲黑色,左旋x的父節點p,而後將x賦值爲root
                 */
                setColor(sib, colorOf(parentOf(x)));
                setColor(parentOf(x), BLACK);
                setColor(rightOf(sib), BLACK);
                rotateLeft(parentOf(x));
                x = root;
            }
        } else {//x是右節點的狀況
            Entry<K,V> sib = leftOf(parentOf(x));

            if (colorOf(sib) == RED) {
                setColor(sib, BLACK);
                setColor(parentOf(x), RED);
                rotateRight(parentOf(x));
                sib = leftOf(parentOf(x));
            }

            if (colorOf(rightOf(sib)) == BLACK &&
                colorOf(leftOf(sib)) == BLACK) {
                setColor(sib, RED);
                x = parentOf(x);
            } else {
                if (colorOf(leftOf(sib)) == BLACK) {
                    setColor(rightOf(sib), BLACK);
                    setColor(sib, RED);
                    rotateLeft(sib);
                    sib = leftOf(parentOf(x));
                }
                setColor(sib, colorOf(parentOf(x)));
                setColor(parentOf(x), BLACK);
                setColor(leftOf(sib), BLACK);
                rotateRight(parentOf(x));
                x = root;
            }
        }
    }

    setColor(x, BLACK);
}

當待操做節點爲左節點時,上面描述了四種場景,並且場景之間能夠相互轉換,如deleteEntry後進入了場景1,通過場景1的一些列操做後,紅黑樹的結構並無調整完成,而是進入了場景2,場景2執行完成後跳出循環,將待操做節點設置爲黑色,完成。咱們下面用圖來講明一下四種場景幫助理解,固然你們最好本身手動畫一下。

場景1:

當x是左黑色節點,兄弟節點sib是紅色節點,須要兄弟節點由紅轉黑,父節點由黑轉紅,按父節點左旋,左旋後樹的結構變化了,這時從新賦值sib,這個時候sib指向了x的兄弟節點。

但通過這一系列操做後,並無結束,而是可能到了場景2,或者場景3和4

場景2:

節點x、x的兄弟節點sib、sib的左子節點和右子節點都爲黑色時,須要將該節點sib由黑變紅,同時將x指向當前x的父節點

通過場景2的一系列操做後,循環就結束了,咱們跳出循環,將節點x設置爲黑色,自平衡調整完成。

場景3:

節點x、x的兄弟節點sib、sib的右子節點都爲黑色,sib的左子節點爲紅色時,須要將sib左子節點設置爲黑色,sib節點設置爲紅色,同時按sib右旋,再將sib指向x的兄弟節點

並無完,場景3的一系列操做後,會進入到場景4

場景4:

節點x、x的兄弟節點sib都爲黑色,而sib的左右子節點都爲紅色或者右子節點爲紅色、左子節點爲黑色,此時須要將sib節點的顏色設置成和x的父節點p相同的顏色,設置x的父節點顏色爲黑色,設置sib右孩子的顏色爲黑色,左旋x的父節點p,而後將x賦值爲root

四種場景講完了,刪除後的自平衡操做不太好理解,代碼層面的已經弄明白了,但若是讓我本身去實現的話,仍是差了一些,還須要再研究。

七. 遍歷

遍歷比較簡單,TreeMap的遍歷可使用map.values(), map.keySet(),map.entrySet(),map.forEach(),這裏再也不多說。

八. 總結

本文詳細介紹了TreeMap的基本特色,並對其底層數據結構紅黑樹進行了回顧,同時講述了其自動排序的原理,並從源碼的角度結合紅黑樹圖形對put方法、get方法、remove方法進行了講解,最後簡單提了一下遍歷操做,如有不對之處,請批評指正,望共同進步,謝謝!

相關文章
相關標籤/搜索