本文根據《大話數據結構》一書,實現了Java版的二叉排序樹/二叉搜索樹。html
在上篇博客中,順序表的插入和刪除效率還能夠,但查找效率很低;而有序線性表中,可使用折半、插值、斐波那契等查找方法來實現,但由於要保持有序,其插入和刪除操做很耗費時間。java
二叉排序樹(Binary Sort Tree),又稱爲二叉搜索樹,則能夠在高效率的查找下,同時保持插入和刪除操做也又較高的效率。下圖爲典型的二叉排序樹。node
二叉查找樹具備如下性質:算法
(1) 若任意節點的左子樹不空,則左子樹上全部結點的值均小於它的根結點的值;數據結構
(2) 任意節點的右子樹不空,則右子樹上全部結點的值均大於它的根結點的值;ide
(3) 任意節點的左、右子樹也分別爲二叉查找樹。post
思路:查找值與結點數據對比,根據大小肯定往左子樹仍是右子樹進行下一步比較。測試
採用遞歸的查找算法this
/* * 查找 */ public boolean SearchBST(int key) { return SearchBST(key, root); } private boolean SearchBST(int key, Node node) { if (node == null) return false; if (node.data == key) { return true; } else if (node.data < key) { return SearchBST(key, node.rChild); } else { return SearchBST(key, node.lChild); } }
採用非遞歸的查找算法url
/* * 查找,非遞歸 */ public boolean SearchBST2(int key) { Node p = root; while (p != null) { if (p.data > key) { p = p.lChild; } else if (p.data < key) { p = p.rChild; } else { return true; } } return false; }
思路:與查找相似,但須要一個父節點來進行賦值。
採用非遞歸的插入算法:
/* * 插入,本身想的,非遞歸 */ public boolean InsertBST(int key) { Node newNode = new Node(key); if (root == null) { root = newNode; return true; } Node f = null; // 指向父結點 Node p = root; // 當前結點的指針 while (p != null) { if (p.data > key) { f = p; p = p.lChild; } else if (p.data < key) { f = p; p = p.rChild; } else { System.out.println("樹中已有相同數據,再也不插入!"); return false; } } if (f.data > key) { f.lChild = newNode; } else if (f.data < key) { f.rChild = newNode; } return true; }
採用遞歸的插入算法:
/* * 插入,參考別人博客,遞歸 * 思路:把null狀況排除後用遞歸,不然沒法賦值 */ public boolean InsertBST2(int key) { if (root == null) { root = new Node(key); return true; } return InsertBST2(key, root); } private boolean InsertBST2(int key, Node node) { if (node.data > key) { if (node.lChild == null) { node.lChild = new Node(key); return true; } else { return InsertBST2(key, node.lChild); } } else if (node.data < key) { if (node.rChild == null) { node.rChild = new Node(key); return true; } else { return InsertBST2(key, node.rChild); } } else { System.out.println("樹中已有相同數據,再也不插入!"); return false; } }
新補充:在寫【Java】 大話數據結構(12) 查找算法(3) (平衡二叉樹(AVL樹))這篇博客時,發現如下的插入方法比較好(若是沒有要求返回值必須爲boolean格式的話):(推薦使用此類方法)
/* * 插入操做 */ public void insert(int key) { root = insert(root, key); } private Node insert(Node node, int key) { if (node == null) { // System.out.println("插入成功!"); // 也能夠定義一個布爾變量來保存插入成功與否 return new Node(key); } if (key == node.data) { System.out.println("數據重複,沒法插入!"); } else if (key < node.data) { node.lChild=insert(node.lChild, key); } else { node.rChild=insert(node.rChild, key); } return node; }
思路:
(1)刪除葉子結點
直接刪除;
(2)刪除僅有左或右子樹的結點
子樹移動到刪除結點的位置便可;
(3)刪除左右子樹都有的結點
找到刪除結點p的直接前驅(或直接後驅)s,用s來替換結點p,而後刪除結點s,以下圖所示。
首先找到刪除結點位置及其父結點
/* * 刪除操做,先找到刪除結點位置及其父結點 * 由於須要有父結點,因此暫時沒想到遞歸的方法(除了令Node對象帶個parent屬性) */ public boolean deleteBST(int key) { if (root == null) { System.out.println("空表,刪除失敗"); return false; } Node f = null; // 指向父結點 Node p = root; // 指向當前結點 while (p != null) { if (p.data > key) { f = p; p = p.lChild; } else if (p.data < key) { f = p; p = p.rChild; } else { delete(p, f); return true; } } System.out.println("該數據不存在"); return false; }
再根據上述思路進行結點p的刪除:(需注意刪除結點爲根節點的狀況)
/* * 刪除結點P的操做 * 必需要有父結點,由於Java沒法直接取得變量p的地址(沒法使用*p=(*p)->lChild) */ private void delete(Node p, Node f) {// p爲刪除結點,f爲其父結點 if (p.lChild == null) { // 左子樹爲空,重接右子樹 if (p == root) { // 被刪除結點爲根結點時,沒法利用f,該狀況不能忽略 root = root.rChild; p = null; } else { if (f.data > p.data) { // 被刪結點爲父結點的左結點,下同 f.lChild = p.rChild; p = null; // 釋放結點別忘了 } else {// 被刪結點爲父結點的右結點,下同 f.rChild = p.rChild; p = null; } } } else if (p.rChild == null) { // 右子樹爲空,重接左子樹 if (p == root) { // 被刪除結點爲根結點 root = root.lChild; p = null; } else { if (f.data > p.data) { f.lChild = p.lChild; p = null; } else { f.rChild = p.lChild; p = null; } } } else { // 左右子樹都不爲空,刪除位置用前驅結點替代 Node q, s; q = p; s = p.lChild; while (s.rChild != null) { // 找到待刪結點的最大前驅s q = s; s = s.rChild; } p.data = s.data; // 改變p的data就OK if (q != p) { q.rChild = s.lChild; } else { q.lChild = s.lChild; } s = null; } }
package BST; /** * 二叉排序樹(二叉查找樹) * 如果泛型,則要求知足T extends Comparable<T> static問題 * @author Yongh * */ class Node { int data; Node lChild, rChild; public Node(int data) { this.data = data; lChild = null; rChild = null; } } public class BSTree { private Node root; public BSTree() { root = null; } /* * 查找 */ public boolean SearchBST(int key) { return SearchBST(key, root); } private boolean SearchBST(int key, Node node) { if (node == null) return false; if (node.data == key) { return true; } else if (node.data < key) { return SearchBST(key, node.rChild); } else { return SearchBST(key, node.lChild); } } /* * 查找,非遞歸 */ public boolean SearchBST2(int key) { Node p = root; while (p != null) { if (p.data > key) { p = p.lChild; } else if (p.data < key) { p = p.rChild; } else { return true; } } return false; } /* * 插入,本身想的,非遞歸 */ public boolean InsertBST(int key) { Node newNode = new Node(key); if (root == null) { root = newNode; return true; } Node f = null; // 指向父結點 Node p = root; // 當前結點的指針 while (p != null) { if (p.data > key) { f = p; p = p.lChild; } else if (p.data < key) { f = p; p = p.rChild; } else { System.out.println("數據重複,沒法插入!"); return false; } } if (f.data > key) { f.lChild = newNode; } else if (f.data < key) { f.rChild = newNode; } return true; } /* * 插入,參考別人博客,遞歸 * 思路:相似查找, * 但若方法中的node爲null的話,將沒法插入新數據,需排除null的狀況 */ public boolean InsertBST2(int key) { if (root == null) { root = new Node(key); return true; } return InsertBST2(key, root); } private boolean InsertBST2(int key, Node node) { if (node.data > key) { if (node.lChild == null) { // 有null的狀況下,纔有父結點 node.lChild = new Node(key); return true; } else { return InsertBST2(key, node.lChild); } } else if (node.data < key) { if (node.rChild == null) { node.rChild = new Node(key); return true; } else { return InsertBST2(key, node.rChild); } } else { System.out.println("數據重複,沒法插入!"); return false; } } /* * 這樣的插入是錯誤的(node沒法真正被賦值) */ /* private boolean InsertBST2(int key, Node node) { if(node!=null) { if (node.data > key) return InsertBST2(key, node.lChild); else if (node.data < key) return InsertBST2(key, node.rChild); else return false;//重複 }else { node=new Node(key); return true; } } */ /* * 刪除操做,先找到刪除結點位置及其父結點 * 由於須要有父結點,因此暫時沒想到遞歸的方法(除了令Node對象帶個parent屬性) */ public boolean deleteBST(int key) { if (root == null) { System.out.println("空表,刪除失敗"); return false; } Node f = null; // 指向父結點 Node p = root; // 指向當前結點 while (p != null) { if (p.data > key) { f = p; p = p.lChild; } else if (p.data < key) { f = p; p = p.rChild; } else { delete(p, f); System.out.println("刪除成功!"); return true; } } System.out.println("該數據不存在"); return false; } /* * 刪除結點P的操做 * 必需要有父結點,由於Java沒法直接取得變量p的地址(沒法使用*p=(*p)->lChild) */ private void delete(Node p, Node f) {// p爲刪除結點,f爲其父結點 if (p.lChild == null) { // 左子樹爲空,重接右子樹 if (p == root) { // 被刪除結點爲根結點,該狀況不能忽略 root = root.rChild; p = null; } else { if (f.data > p.data) { // 被刪結點爲父結點的左結點,下同 f.lChild = p.rChild; p = null; // 釋放結點別忘了 } else {// 被刪結點爲父結點的右結點,下同 f.rChild = p.rChild; p = null; } } } else if (p.rChild == null) { // 右子樹爲空,重接左子樹 if (p == root) { // 被刪除結點爲根結點 root = root.lChild; p = null; } else { if (f.data > p.data) { f.lChild = p.lChild; p = null; } else { f.rChild = p.lChild; p = null; } } } else { // 左右子樹都不爲空,刪除位置用前驅結點替代 Node q, s; q = p; s = p.lChild; while (s.rChild != null) { // 找到待刪結點的最大前驅s q = s; s = s.rChild; } p.data = s.data; // 改變p的data就OK if (q != p) { q.rChild = s.lChild; } else { q.lChild = s.lChild; } s = null; } } /* * 中序遍歷 */ public void inOrder() { inOrder(root); System.out.println(); } public void inOrder(Node node) { if (node == null) return; inOrder(node.lChild); System.out.print(node.data + " "); inOrder(node.rChild); } /* * 測試代碼 */ public static void main(String[] args) { BSTree aTree = new BSTree(); BSTree bTree = new BSTree(); int[] arr = { 62, 88, 58, 47, 35, 73, 51, 99, 37, 93 }; for (int a : arr) { aTree.InsertBST(a); bTree.InsertBST2(a); } aTree.inOrder(); bTree.inOrder(); System.out.println(aTree.SearchBST(35)); System.out.println(bTree.SearchBST2(99)); aTree.deleteBST(47); aTree.inOrder(); } }
35 37 47 51 58 62 73 88 93 99 35 37 47 51 58 62 73 88 93 99 true true 刪除成功! 35 37 51 58 62 73 88 93 99
小結(本身編寫時的注意點):
查找:操做簡單,注意遞歸的方法沒有循環while (p!=null),而是並列的幾個判斷;
插入:非遞歸時,要有父結點;遞歸時,要注意排除null的狀況;
刪除:記住要分兩步,第一步找結點位置時也要把父結點帶上;第二步刪除結點時,要令p=null,還要注意p==root的狀況以及q==p的狀況。