由於最近想要閱讀下 JDK1.8 中 HashMap 的具體實現,但是由於 HashMap 的實現中用到了紅黑樹,所以我覺得有必要先復習下紅黑樹的相關知識,所以寫下這篇隨筆備忘,有不對的地方請指出~
學習紅黑樹,我覺得有必要從二叉搜索樹開始學起,本篇隨筆就主要介紹 Java 實現二叉搜索樹的查找、插入、刪除、遍歷等內容。
二叉搜索樹需滿足以下四個條件:
- 若任意節點的左子樹不空,則左子樹上所有結點的值均小於它的根結點的值;
- 若任意節點的右子樹不空,則右子樹上所有結點的值均大於它的根結點的值;
- 任意節點的左、右子樹也分別為二叉查找樹;
- 沒有鍵值相等的節點。
二叉搜索樹舉例:
圖一
接下來將基於圖一介紹二叉搜索樹相關操作。
首先,應先有一個節點對象相關的類,命名為 Node。
1 class Node { 2 int key; 3 int value; 4 Node leftChild; 5 Node rightChild; 6 7 public Node(int key, int value) { 8 this.key = key; 9 this.value = value; 10 } 11 12 public void displayNode() { 13 14 } 15 }
Node 類中包含 key 值,用於確定節點在樹中相應位置,value 值代表要存儲的內容,還含有指向左右孩子節點的兩個引用。
接下來看下搜索樹相應的類:
1 class Tree { 2 Node root;//保存樹的根 3 4 public Node find(int key) {//查找指定節點 5 6 } 7 8 public void insert(int key, int value) {//插入節點 9 10 } 11 12 public boolean delete(int key) {//刪除指定節點 13 14 } 15 16 private Node getDirectPostNode(Node delNode) {//得到待刪除節點的直接后繼節點 17 18 } 19 20 public void preOrder(Node rootNode) {//先序遍歷樹 21 22 } 23 24 public void inOrder(Node rootNode) {//中序遍歷樹 25 26 } 27 28 public void postOrder(Node rootNode) {//后序遍歷樹 29 30 } 31 }
類中表示樹的框架,包含查找、插入、遍歷、刪除相應方法,其中刪除節點操作最為復雜,接下來一一介紹。
一、查找某個節點
由於二叉搜索樹定義上的特殊性,只需根據輸入的 key 值從根開始進行比較,若小於根的 key 值,則與根的左子樹比較,大於根的key值與根的右子樹比較,以此類推,找到則返回相應節點,否則返回 null。
1 public Node find(int key) { 2 Node currentNode = root; 3 while (currentNode != null && currentNode.key != key) { 4 if (key < currentNode.key) { 5 currentNode = currentNode.leftChild; 6 } else { 7 currentNode = currentNode.rightChild; 8 } 9 } 10 return currentNode; 11 }
二、插入節點
與查找操作相似,由於二叉搜索樹的特殊性,待插入的節點也需要從根節點開始進行比較,小於根節點則與根節點左子樹比較,反之則與右子樹比較,直到左子樹為空或右子樹為空,則插入到相應為空的位置,在比較的過程中要注意保存父節點的信息 及 待插入的位置是父節點的左子樹還是右子樹,才能插入到正確的位置。
1 public void insert(int key, int value) { 2 if (root == null) { 3 root = new Node(key, value); 4 return; 5 } 6 Node currentNode = root; 7 Node parentNode = root; 8 boolean isLeftChild = true; 9 while (currentNode != null) { 10 parentNode = currentNode; 11 if (key < currentNode.key) { 12 currentNode = currentNode.leftChild; 13 isLeftChild = true; 14 } else { 15 currentNode = currentNode.rightChild; 16 isLeftChild = false; 17 } 18 } 19 Node newNode = new Node(key, value); 20 if (isLeftChild) { 21 parentNode.leftChild = newNode; 22 } else { 23 parentNode.rightChild = newNode; 24 } 25 }
三、遍歷二叉搜索樹
遍歷操作與遍歷普通二叉樹操作完全相同,不贅述。
1 public void preOrder(Node rootNode) { 2 if (rootNode != null) { 3 System.out.println(rootNode.key + " " + rootNode.value); 4 preOrder(rootNode.leftChild); 5 preOrder(rootNode.rightChild); 6 } 7 } 8 9 public void inOrder(Node rootNode) { 10 if (rootNode != null) { 11 inOrder(rootNode.leftChild); 12 System.out.println(rootNode.key + " " + rootNode.value); 13 inOrder(rootNode.rightChild); 14 } 15 } 16 17 public void postOrder(Node rootNode) { 18 if (rootNode != null) { 19 postOrder(rootNode.leftChild); 20 postOrder(rootNode.rightChild); 21 System.out.println(rootNode.key + " " + rootNode.value); 22 } 23 }
四、刪除指定節點。
在二叉搜索樹中刪除節點操作較復雜,可分為以下三種情況。
1、待刪除的節點為葉子節點,可直接刪除。
public boolean delete(int key) { Node currentNode = root;//用來保存待刪除節點 Node parentNode = root;//用來保存待刪除節點的父親節點 boolean isLeftChild = true;//用來確定待刪除節點是父親節點的左孩子還是右孩子 while (currentNode != null && currentNode.key != key) { parentNode = currentNode; if (key < currentNode.key) { currentNode = currentNode.leftChild; isLeftChild = true; } else { currentNode = currentNode.rightChild; isLeftChild = false; } } if (currentNode == null) { return false; } if (currentNode.leftChild == null && currentNode.rightChild == null) {//要刪除的節點為葉子節點 if (currentNode == root) root = null; else if (isLeftChild) parentNode.leftChild = null; else parentNode.rightChild = null; } ...... }
2、待刪除節點只有一個孩子節點
例如刪除圖一中的 key 值為 11 的節點,只需將 key 值為 13 的節點的左孩子指向 key 值為 12的節點即可達到刪除 key 值為 11 的節點的目的。
由以上分析可得代碼如下(接上述 delete 方法省略號后):
1 else if (currentNode.rightChild == null) {//要刪除的節點只有左孩子 2 if (currentNode == root) 3 root = currentNode.leftChild; 4 else if (isLeftChild) 5 parentNode.leftChild = currentNode.leftChild; 6 else 7 parentNode.rightChild = currentNode.leftChild; 8 } else if (currentNode.leftChild == null) {//要刪除的節點只有右孩子 9 if (currentNode == root) 10 root = currentNode.rightChild; 11 else if (isLeftChild) 12 parentNode.leftChild = currentNode.rightChild; 13 else 14 parentNode.rightChild = currentNode.rightChild; 15 }
......
3、待刪除節點既有左孩子,又有右孩子。
例如刪除圖一中 key 值為 10 的節點,這時就需要用 key 值為 10 的節點的中序后繼節點(節點 11)來代替 key 值為 10 的節點,並刪除 key 值為 10 的節點的中序后繼節點,由中序遍歷相關規則可知, key 值為 10 的節點的直接中序后繼節點一定是其右子樹中 key 值最小的節點,所以此中序后繼節點一定不含子節點或者只含有一個右孩子,刪除此中序后繼節點就屬於上述 1,2 所述情況。圖一中 key 值為 10 的節點的直接中序后繼節點 為 11,節點 11 含有一個右孩子 12。
所以刪除 圖一中 key 值為 10 的節點分為以下幾步:
a、找到 key 值為 10 的節點的直接中序后繼節點(即其右子樹中值最小的節點 11),並刪除此直接中序后繼節點。
1 private Node getDirectPostNode(Node delNode) {//方法作用為得到待刪除節點的直接后繼節點 2 3 Node parentNode = delNode;//用來保存待刪除節點的直接后繼節點的父親節點 4 Node direcrPostNode = delNode;//用來保存待刪除節點的直接后繼節點 5 Node currentNode = delNode.rightChild; 6 while (currentNode != null) { 7 parentNode = direcrPostNode; 8 direcrPostNode = currentNode; 9 currentNode = currentNode.leftChild; 10 } 11 if (direcrPostNode != delNode.rightChild) {//從樹中刪除此直接后繼節點 12 parentNode.leftChild = direcrPostNode.rightChild; 13 direcrPostNode.rightChild = null; 14 } 15 return direcrPostNode;//返回此直接后繼節點 16 17 }
b、將此后繼節點的 key、value 值賦給待刪除節點的 key,value值。(接情況二中省略號代碼之后)
1 else { //要刪除的節點既有左孩子又有右孩子 2 3 //思路:用待刪除節點右子樹中的key值最小節點的值來替代要刪除的節點的值,然后刪除右子樹中key值最小的節點 4 //右子樹key最小的節點一定不含左子樹,所以刪除這個key最小的節點一定是屬於葉子節點或者只有右子樹的節點 5 Node directPostNode = getDirectPostNode(currentNode); 6 currentNode.key = directPostNode.key; 7 currentNode.value = directPostNode.value; 8 9 }
至此刪除指定節點的操作結束。
最后給出完整代碼及簡單測試代碼及測試結果:
1 class Node { 2 int key; 3 int value; 4 Node leftChild; 5 Node rightChild; 6 7 public Node(int key, int value) { 8 this.key = key; 9 this.value = value; 10 } 11 12 public void displayNode() { 13 14 } 15 } 16 17 class Tree { 18 Node root; 19 20 public Node find(int key) { 21 Node currentNode = root; 22 while (currentNode != null && currentNode.key != key) { 23 if (key < currentNode.key) { 24 currentNode = currentNode.leftChild; 25 } else { 26 currentNode = currentNode.rightChild; 27 } 28 } 29 return currentNode; 30 } 31 32 public void insert(int key, int value) { 33 if (root == null) { 34 root = new Node(key, value); 35 return; 36 } 37 Node currentNode = root; 38 Node parentNode = root; 39 boolean isLeftChild = true; 40 while (currentNode != null) { 41 parentNode = currentNode; 42 if (key < currentNode.key) { 43 currentNode = currentNode.leftChild; 44 isLeftChild = true; 45 } else { 46 currentNode = currentNode.rightChild; 47 isLeftChild = false; 48 } 49 } 50 Node newNode = new Node(key, value); 51 if (isLeftChild) { 52 parentNode.leftChild = newNode; 53 } else { 54 parentNode.rightChild = newNode; 55 } 56 } 57 58 public boolean delete(int key) { 59 Node currentNode = root; 60 Node parentNode = root; 61 boolean isLeftChild = true; 62 while (currentNode != null && currentNode.key != key) { 63 parentNode = currentNode; 64 if (key < currentNode.key) { 65 currentNode = currentNode.leftChild; 66 isLeftChild = true; 67 } else { 68 currentNode = currentNode.rightChild; 69 isLeftChild = false; 70 } 71 } 72 if (currentNode == null) { 73 return false; 74 } 75 if (currentNode.leftChild == null && currentNode.rightChild == null) { 76 //要刪除的節點為葉子節點 77 if (currentNode == root) 78 root = null; 79 else if (isLeftChild) 80 parentNode.leftChild = null; 81 else 82 parentNode.rightChild = null; 83 } else if (currentNode.rightChild == null) {//要刪除的節點只有左孩子 84 if (currentNode == root) 85 root = currentNode.leftChild; 86 else if (isLeftChild) 87 parentNode.leftChild = currentNode.leftChild; 88 else 89 parentNode.rightChild = currentNode.leftChild; 90 } else if (currentNode.leftChild == null) {//要刪除的節點只有右孩子 91 if (currentNode == root) 92 root = currentNode.rightChild; 93 else if (isLeftChild) 94 parentNode.leftChild = currentNode.rightChild; 95 else 96 parentNode.rightChild = currentNode.rightChild; 97 } else { //要刪除的節點既有左孩子又有右孩子 98 //思路:用待刪除節點右子樹中的key值最小節點的值來替代要刪除的節點的值,然后刪除右子樹中key值最小的節點 99 //右子樹key最小的節點一定不含左子樹,所以刪除這個key最小的節點一定是屬於葉子節點或者只有右子樹的節點 100 Node directPostNode = getDirectPostNode(currentNode); 101 currentNode.key = directPostNode.key; 102 currentNode.value = directPostNode.value; 103 } 104 return true; 105 } 106 107 private Node getDirectPostNode(Node delNode) {//方法作用為得到待刪除節點的直接后繼節點 108 109 Node parentNode = delNode;//用來保存待刪除節點的直接后繼節點的父親節點 110 Node direcrPostNode = delNode;//用來保存待刪除節點的直接后繼節點 111 Node currentNode = delNode.rightChild; 112 while (currentNode != null) { 113 parentNode = direcrPostNode; 114 direcrPostNode = currentNode; 115 currentNode = currentNode.leftChild; 116 } 117 if (direcrPostNode != delNode.rightChild) {//從樹中刪除此直接后繼節點 118 parentNode.leftChild = direcrPostNode.rightChild; 119 direcrPostNode.rightChild = null; 120 } 121 return direcrPostNode;//返回此直接后繼節點 122 123 } 124 125 public void preOrder(Node rootNode) { 126 if (rootNode != null) { 127 System.out.println(rootNode.key + " " + rootNode.value); 128 preOrder(rootNode.leftChild); 129 preOrder(rootNode.rightChild); 130 } 131 } 132 133 public void inOrder(Node rootNode) { 134 if (rootNode != null) { 135 inOrder(rootNode.leftChild); 136 System.out.println("key: " + rootNode.key + " " + "value: " + rootNode.value); 137 inOrder(rootNode.rightChild); 138 } 139 } 140 141 public void postOrder(Node rootNode) { 142 if (rootNode != null) { 143 postOrder(rootNode.leftChild); 144 postOrder(rootNode.rightChild); 145 System.out.println(rootNode.key + " " + rootNode.value); 146 } 147 }
private void destroy(Node tree) { if (tree==null) return ; if (tree.left != null) destroy(tree.leftChild); if (tree.right != null) destroy(tree.rightChild); tree=null; }
public void destory() {
destory(root);
}
148 }
149 public class BinarySearchTreeApp {
150 public static void main(String[] args) {
151 Tree tree = new Tree();
152 tree.insert(6, 6);//插入操作,構造圖一所示的二叉樹
153 tree.insert(3, 3);
154 tree.insert(14, 14);
155 tree.insert(16, 16);
156 tree.insert(10, 10);
157 tree.insert(9, 9);
158 tree.insert(13, 13);
159 tree.insert(11, 11);
160 tree.insert(12, 12);
161 162 System.out.println("刪除前遍歷結果");
163 tree.inOrder(tree.root);//中序遍歷操作
164 165 System.out.println("刪除節點10之后遍歷結果");
166 tree.delete(10);//刪除操作
167 tree.inOrder(tree.root); 168
}
169 }
測試結果: