在系列的第一篇文章中說過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,底層使用了紅黑樹這種數據結構來實現。紅黑樹是一種應用很是普遍的樹結構,在這裏先簡單說下紅黑樹這種數據結構相比較其餘樹類型結構的優缺點:算法
在分析TreeMap<K,V>的源碼以前,咱們先從二叉排序樹提及,由於紅黑樹也是一顆二叉樹,只不過是知足必定條件的二叉樹,理解了二叉排序樹能夠更方便理解紅黑樹。 二叉樹排序樹是一種很是典型的樹結構,一般使用鏈表作爲存儲結構(也可使用數組)。因爲樹結構每一個節點都會存儲父子節點的引用,用鏈表結構更容易表達。若是使用數組來存儲,當出現空子節點時對數組空間是一種浪費,同時在查找特定元素時因爲數組的元素沒有父子節點的引用,只能根據必定規則來遍歷,很是不方便,因此大多數狀況下都使用鏈表來存儲樹結構。 二叉樹能夠經過中序遍歷獲得一個有序的序列,查找和刪除都很是方便,通常狀況下時間複雜度爲O(logn),最壞O(n)。 排序二叉樹要麼是一顆空樹,要麼具備如下性質:數據庫
二叉樹作爲一種樹結構,遍歷的目的是爲了依次訪問樹中全部的節點,而且使每一個節點只被訪問一遍。 他的遍歷的方式不少,通常有前中後序和層序遍歷四種。 中序遍歷就是先訪問左子樹,再訪問根節點,最後訪問右節點,根據二叉排序樹的性質能夠知道,經過中序遍歷能夠獲得一個由小到大(默認狀況下)的排序序列。因此中序遍歷使用的最頻繁。 下圖是中序遍歷的圖例和代碼實現: 數組
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種狀況:性能
下面按照二叉樹刪除的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>中對知足紅黑樹性質所進行的操做。