Java集合(3)一 紅黑樹、TreeMap與TreeSet(上)

引言

在系列的第一篇文章中說過Map<K,V>接口與Set<E>接口,Set<E>接口定義了一組不能添加劇復元素的集,不能經過索引來訪問的集;Map<K,V>接口定義了從鍵映射到值的一組對象。同時也說過了由於鍵集不能重複的特性,Map<K,V>的鍵集由Set<E>來實現。 經過查看TreeSet<E>的構造函數,能夠看出他是經過TreeMap<K,V>來實現的,只不過僅使用了key。因此在這篇文章中咱們會詳細講解TreeMap<K,V>,對TreeSet<E>就不作過多說明。java

public TreeSet() {
    this(new TreeMap<E,Object>());
}

public TreeSet(Comparator<? super E> comparator) {
    this(new TreeMap<>(comparator));
}
複製代碼

框架結構

TreeMap<K,V>繼承了SortedMap<K,V>接口,SortedMap<K,V>提供了排序功能,經過comparator方法用來對TreeMap裏面的每一個對象進行比較來達到排序的目的。默認的排序是升序排序,也能夠經過構造函數傳入比較器Comparator來進行排序。node

//SortedMap接口包含的比較方法comparator
public interface SortedMap<K,V> extends Map<K,V> {
    Comparator<? super K> comparator();
}

public interface Comparator<T> {
    int compare(T o1, T o2);

    boolean equals(Object obj);
}
//TreeMap構造函數傳入比較器Comparator
public TreeMap(Comparator<? super K> comparator) {
    this.comparator = comparator;
}

public Comparator<? super K> comparator() {
    return comparator;
}
//TreeSet構造函數傳入比較器Comparator
public TreeSet(Comparator<? super E> comparator) {
    this(new TreeMap<>(comparator));
}

public Comparator<? super E> comparator() {
    return m.comparator();
}
複製代碼

數據結構

TreeMap<K,V>是有序的Map,底層使用了紅黑樹這種數據結構來實現。紅黑樹是一種應用很是普遍的樹結構,在這裏先簡單說下紅黑樹這種數據結構相比較其餘樹類型結構的優缺點:算法

  1. 紅黑樹是一種自平衡的二叉查找樹,也叫對稱二叉B樹,紅黑樹的查找、插入和刪除的時間複雜度都爲O(logn),應用很是普遍。
  2. 紅黑樹相對於AVL樹(平衡二叉樹),犧牲了部分平衡性(紅黑樹不是徹底平衡的二叉樹)以換取插入/刪除操做時更少的旋轉操做,總體在插入/刪除的性能上要優於AVL樹。因此不少在內存中排序的數據結構都使用紅黑樹來而不是使用AVL樹來存儲。
  3. 紅黑樹相對於B-樹和B+樹,相同節點的狀況下紅黑樹因爲深度比B-和B+樹要深的多,對IO讀寫很是頻繁,因此適合放在內存中的少許讀取,而B-和B+樹因爲每一個節點元素很是之多,訪問IO的次數就相對少,適合存儲在磁盤中的大量數據,相似數據庫記錄的存儲結構。 因爲本文篇幅有限,文章中將重點講述二叉樹和紅黑樹,對AVL樹、B-樹、B+樹不作過多講解。

二叉排序樹

在分析TreeMap<K,V>的源碼以前,咱們先從二叉排序樹提及,由於紅黑樹也是一顆二叉樹,只不過是知足必定條件的二叉樹,理解了二叉排序樹能夠更方便理解紅黑樹。 二叉樹排序樹是一種很是典型的樹結構,一般使用鏈表作爲存儲結構(也可使用數組)。因爲樹結構每一個節點都會存儲父子節點的引用,用鏈表結構更容易表達。若是使用數組來存儲,當出現空子節點時對數組空間是一種浪費,同時在查找特定元素時因爲數組的元素沒有父子節點的引用,只能根據必定規則來遍歷,很是不方便,因此大多數狀況下都使用鏈表來存儲樹結構。 二叉樹能夠經過中序遍歷獲得一個有序的序列,查找和刪除都很是方便,通常狀況下時間複雜度爲O(logn),最壞O(n)。 排序二叉樹要麼是一顆空樹,要麼具備如下性質:數據庫

  1. 若任意節點的左子樹不空,則左子樹上全部節點的值均小於它的根節點的值;
  2. 若任意節點的右子樹不空,則右子樹上全部節點的值均大於它的根節點的值;
  3. 任意節點的左、右子樹也分別爲二叉查找樹;
  4. 沒有鍵值相等的節點。 下圖是一顆典型的二叉樹:

二叉樹遍歷

二叉樹作爲一種樹結構,遍歷的目的是爲了依次訪問樹中全部的節點,而且使每一個節點只被訪問一遍。 他的遍歷的方式不少,通常有前中後序和層序遍歷四種。 中序遍歷就是先訪問左子樹,再訪問根節點,最後訪問右節點,根據二叉排序樹的性質能夠知道,經過中序遍歷能夠獲得一個由小到大(默認狀況下)的排序序列。因此中序遍歷使用的最頻繁。 下圖是中序遍歷的圖例和代碼實現: 數組

public class BinaryTree {
    public void traversalBinaryTree(TreeNode tree) {
		//若是到了葉子節點則退出當前方法,繼續向下尋找
		if (tree == null) {
			return;
		}
		//迭代查找左節點,一直到最左邊的葉子節點
		traversalBinaryTree(tree.left);
		System.out.println(tree.value);
		//迭代查找右節點,一直到最左邊的葉子節點
		traversalBinaryTree(tree.right);
	}

    class TreeNode {
		//節點的值
		int value;
		//左節點
		TreeNode left;
		//右節點
		TreeNode right;
		//父節點
		TreeNode parent;
		
		public TreeNode(int treeValue, TreeNode parentNode) {
			value = treeValue;
			parent = parentNode;
			left = null;
			right = null;
		}
	}
}
複製代碼

前序遍歷和後序遍歷:數據結構

//前序遍歷
System.out.println(tree.value);
traversalBinaryTree(tree.left);
traversalBinaryTree(tree.right);
//後序遍歷
traversalBinaryTree(tree.left);
traversalBinaryTree(tree.right);
System.out.println(tree.value);
複製代碼

二叉樹添加

明白了二叉樹的遍歷,理解二叉樹的添加就很是簡單,經過中序遍歷從小到大查找到要添加值的空葉子節點爲止,咱們來實現一個二叉樹的添加方法:框架

public void addBinaryTreeNode(int value) {
    //根節點
    TreeNode tree = root;
    if (tree == null) {
        //根節點爲空則新建一個跟節點
        root = new TreeNode(value, null);
        return;
    }
    //用來存儲新節點的父節點
    TreeNode parentNode;
    do {
        //使用上次循環後的節點作爲引用
        parentNode = tree;
        //若是新插入的 value 小於當前value,則向左邊查找
        if (value < tree.value) {
            tree = tree.left;
        //若是新插入的 value 大於當前value,則向右邊查找
        } else if (value > tree.value) {
            tree = tree.right;
        //若是相等則證實有相同節點,不添加
        } else {
            return;
        }
    } while (tree != null);
    //新建節點,parentNode爲新節點的父節點
    TreeNode node = new TreeNode(value, parentNode);
    //新節點爲左節點或者右節點
    if (value < parentNode.value) {
        parentNode.left = node;
    } else {
        parentNode.right = node;
    }	
}

public static void main(String[] args) {

    BinaryTree binaryTree = new BinaryTree();

    binaryTree.addBinaryTreeNode(10);
    binaryTree.addBinaryTreeNode(5);
    binaryTree.addBinaryTreeNode(20);
    binaryTree.addBinaryTreeNode(7);
    binaryTree.addBinaryTreeNode(6);
    binaryTree.addBinaryTreeNode(3);
    binaryTree.addBinaryTreeNode(15);
    binaryTree.addBinaryTreeNode(30);

    binaryTree.traversalBinaryTree(binaryTree.root);
}

// Console:
// 3
// 5
// 6
// 7
// 10
// 15
// 20
// 30
複製代碼

經過上面二叉樹添加節點的邏輯,咱們再來分析TreeMap<K,V>源碼中添加節點的實現。 TreeMap<K,V>經過put(K key, V value)方法將key和value放在一個Entry<K,V>節點中,Entry<K,V>至關於上面代碼中的Node節點。函數

public V put(K key, V value) {
    //根節點
    Entry<K,V> t = root;
    if (t == null) {
        compare(key, key); // type (and possibly null) check
        //根節點爲空則新建一個跟節點
        root = new Entry<>(key, value, null);
        //記錄Map元素的數量
        size = 1;
        modCount++;
        return null;
    }
    int cmp;
    Entry<K,V> parent;
    // split comparator and comparable paths
    Comparator<? super K> cpr = comparator;
    //若是comparator不爲空則表明使用定製的比較器進行排序
    if (cpr != null) {
        do {
            parent = t;
            cmp = cpr.compare(key, t.key);
            //若是新插入的key小於當前key,則向左邊查找
            if (cmp < 0)
                t = t.left;
            //若是新插入的key大於當前key,則向右邊查找
            else if (cmp > 0)
                t = t.right;
            //相等則覆蓋
            else
                return t.setValue(value);
        } while (t != null);
    }
    //若是comparator爲空則使用默認比較器進行排序
    else {
        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);
    }
    //經過上面查找到插入節點的父節點parent並初始化新節點Entry<K,V>
    Entry<K,V> e = new Entry<>(key, value, parent);
    //若是新插入的key小於父節點的key,則將插入節點做爲父節點的左孩子
    if (cmp < 0)
        parent.left = e;
    //若是新插入的key大於父節點的key,則將插入節點做爲父節點的右孩子
    else
        parent.right = e;
    //重點:修復紅黑樹(後面會說)
    fixAfterInsertion(e);
    size++;
    modCount++;
    return null;
}
複製代碼

二叉樹刪除

二叉樹的刪除相比添加複雜一些,由於若是刪除的節點不是葉子節點,須要考慮由那個節點來替代當前節點的位置。刪除能夠分6種狀況:性能

  1. 刪除的節點沒有左右子節點,而且沒有父節點,則爲根節點,直接刪除便可;
  2. 刪除的節點沒有左右子節點,有父節點,是爲葉子節點,直接刪除便可;
  3. 刪除的節點是根節點,有左節點或右節點,用左節點或者右節點替換被刪除的根節點;
  4. 刪除的節點不是根節點,只有左節點,用左節點替換被刪除的節點;
  5. 刪除的節點不是根節點,只有右節點,用右節點替換被刪除的節點;
  6. 刪除的節點有左右子節點,用刪除節點的直接後繼節點替換被刪除的節點的值,而後刪除直接後繼節點,狀況轉換爲二、4或者5。

下面按照二叉樹刪除的6種狀況咱們來實現一個二叉樹的刪除算法:this

public void removeBinaryTreeNode(int value) {
    // 根節點
    TreeNode tree = root;
    if (tree == null) {
        return;
    }
    TreeNode currentNode = findBinaryTreeNode(value);
    if (currentNode == null) {
        return;
    }
    
    if (currentNode.left == null && currentNode.right == null) {
        //狀況一 刪除根節點,而且沒有左右子節點
        if (currentNode.parent == null) {
            root = null;
        } else {
            //狀況二 刪除葉子節點
            if (currentNode.parent.left == currentNode) {
                currentNode.parent.left = null;
            } else {
                currentNode.parent.right = null;
            }
            currentNode.parent = null;
        }
    } else if (currentNode.left == null || currentNode.right == null) {
        TreeNode replaceNode = currentNode.left == null ? currentNode.right : currentNode.left;
        replaceNode.parent = currentNode.parent;
        //狀況三 刪除根節點 而且只有一個子節點
        if (currentNode.parent == null) {
            root = replaceNode;
        //狀況四 不是根節點 只有左節點
        } else if (currentNode == currentNode.parent.left) {
            currentNode.parent.left = replaceNode;
        //狀況五 不是根節點 只有右節點
        } else {
            currentNode.parent.right = replaceNode;
        }
        currentNode.parent = currentNode.left = currentNode.right = null;
    }  else {
        //狀況六 同時有左右節點
        //successorNode 須要刪除節點的後繼節點
        TreeNode successorNode = currentNode.right;
        TreeNode parentNode;
        //查找後繼節點
        do {
            parentNode =  successorNode;
            successorNode = successorNode.left;
        } while (successorNode != null);
        successorNode = parentNode;
        //覆蓋須要刪除的節點的值爲後繼節點的值
        currentNode.value = successorNode.value;
        //後繼節點的左節點必定爲空,若是不爲空則說明當前節點不是後繼節點
        if (successorNode.right != null) {
            //關聯後繼節點的右節點和後繼節點的父節點
            TreeNode replaceNode = successorNode.right;
            replaceNode.parent = successorNode.parent;
            if (successorNode.parent.left == successorNode) {
                successorNode.parent.left = replaceNode;
            }
            if (successorNode.parent.right == successorNode) {
                successorNode.parent.right = replaceNode;
            }
        }
        //刪除後繼節點
        successorNode.parent = successorNode.left = successorNode.right = null;
    }
}
//查找當前值所對應的樹節點
public TreeNode findBinaryTreeNode(int value) {
    // 根節點
    TreeNode tree = root;
    if (tree == null) {
        return null;
    }
    // 用來存儲新節點的父節點
    TreeNode parentNode;
    do {
        // 循環迭代從小到大查找直到葉子爲空
        parentNode = tree;
        if (value < tree.value) {
            tree = tree.left;
        } else if (value > tree.value) {
            tree = tree.right;
        } else {
            return parentNode;
        }
    } while (tree != null);
    return null;
}
複製代碼

經過上面二叉樹刪除節點的邏輯,再來分析TreeMap<K,V>源碼中刪除節點的實現。 TreeMap<K,V>經過remove(Object key)來刪除key所表明的節點,先經過getEntry(key)查找到須要刪除的節點Entry<K,V> p,而後經過deleteEntry(Entry<K,V> p)刪除p節點。

public V remove(Object key) {
    //查找到key所表明的節點p
    Entry<K,V> p = getEntry(key);
    if (p == null)
        return null;

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

private void deleteEntry(Entry<K,V> p) {
    modCount++;
    size--;

    // If strictly internal, copy successor's element to p and then make p
    // point to successor.
    //若是被刪除的節點左右孩子都不爲空,則查找到P的直接後繼節點,用後繼節點的鍵和值覆蓋P的鍵和值,而後刪除後繼節點便可(其實是狀況六)
    //這一步很是巧妙,將要刪除的節點轉換爲要刪除節點的直接後繼節點,狀況六轉換爲狀況二,四,五
    if (p.left != null && p.right != null) {
        //查找到P的直接後繼節點
        Entry<K,V> s = successor(p);
        //後繼節點覆蓋鍵和值到P
        p.key = s.key;
        p.value = s.value;
        //將要刪除的節點變爲P的後繼節點,刪除後繼節點便可
        p = s;
    } // p has 2 children

    // Start fixup at replacement node, if it exists.
    //查找用來替換的節點
    //通過上面的步驟,若是P存在左節點,則不存在右節點,直接用左節點替換便可。由於若是左右節點都存在,則會查找到後繼(後繼節點的左節點必定爲空)。
    //若是P左節點不存在,存在右節點,則直接用右節點來替換便可。
    Entry<K,V> replacement = (p.left != null ? p.left : p.right);
    //經過上面的步驟說明左右節點只可能同時存在一個,replacement爲左右子節點當中的任何一個
    if (replacement != null) {
        // Link replacement to parent
        //p節點將要刪除,將用來替換的節點的父節點指向p的父節點
        replacement.parent = p.parent;
        //p的父節點爲空,則說明刪除的是根節點(狀況三)
        if (p.parent == null)
            root = replacement;
        //replacement替換爲左節點(狀況四)
        else if (p == p.parent.left)
            p.parent.left  = replacement;
        //replacement替換爲右節點(狀況五)
        else
            p.parent.right = replacement;

        // Null out links so they are OK to use by fixAfterDeletion.
        //刪除P節點
        p.left = p.right = p.parent = null;

        // Fix replacement
        //重點:修復紅黑樹(後面會說)
        if (p.color == BLACK)
            fixAfterDeletion(replacement);
    //若是替換的節點爲空,p的父節點也爲空則爲根節點,直接刪除便可(狀況一)
    } else if (p.parent == null) { // return if we are the only node.
        root = null;
    //若是替換的節點爲空,p的父節點不爲空,說明爲葉子節點(狀況二)
    } else { // No children. Use self as phantom replacement and unlink.
        //重點:修復紅黑樹(後面會說)
        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;
        }
    }
}
//查找t節點的直接後繼節點
static <K,V> TreeMap.Entry<K,V> successor(Entry<K,V> t) {
    if (t == null)
        return null;
    else if (t.right != null) {
        Entry<K,V> p = t.right;
        while (p.left != null)
            p = p.left;
        return p;
    } else {
        Entry<K,V> p = t.parent;
        Entry<K,V> ch = t;
        while (p != null && ch == p.right) {
            ch = p;
            p = p.parent;
        }
        return p;
    }
}
複製代碼

總結

在這篇文章中咱們本身實現了二叉樹的遍歷、添加節點以及刪除節點的操做邏輯,而後詳解了TreeMap<K,V>中關於節點刪除和添加的邏輯,略過了紅黑樹的操做。 看完後相信你們對二叉樹的基本操做有了必定了解,下一篇文章中將詳細講解TreeMap<K,V>中對知足紅黑樹性質所進行的操做。

相關文章
相關標籤/搜索