java實現二叉查詢樹(插入、刪除、遍歷、查詢)
閒話:
繼續擼資料結構和演算法。看資料結構推薦一個視覺化工具吧(http://visualgo.net/),沒有圖憑腦袋想是很痛苦的。
正文:
二叉查詢樹,也叫二叉搜尋樹、有序二叉樹,排序二叉樹,滿足以下性質(非嚴謹描述):
1.對於每個節點,其左子節點要麼為空,要麼值小於該節點值。
2.對於每個節點,其右子節點要麼為空,要麼值大於該節點值。
3.沒有鍵值相等的點。
通俗的歸納一下性質,二叉查詢樹中每個節點的值都大於其左子節點,小於其右子節點(如果左右子節點存在的話)。所以二叉查詢樹中每個節點的左邊,整棵左樹都是小於它的節點;右邊,整棵右樹都是大於它的節點。
例圖:
基於這樣的特性,查詢的時候就很好操作了,從根節點開始,查詢,如果值大於節點值,往右找;如果值小於節點值,往左找;如果值剛好相等,就找到了。是不是看著就能寫出程式碼了?這種查詢過程很像二分查詢法,但是那個是陣列結構,這個是樹結構。
二叉查詢樹的操作基本概括為:插入值,刪除值,查詢值以及二叉樹的遍歷。
這裡面,刪除是最麻煩的。
(本來覺得寫資料結構還是用c語言最好的,直接可以操作指標,清晰明瞭效率高,但是c確實丟了太久了,而且現在主要目的是溫習資料結構和演算法的知識,所以只能放棄用c的想法,以後如果需要再學習,先用最熟悉的java來實現程式碼)
下面來看具體的操作和邏輯,附帶貼上程式碼。
首先是準備工作,java寫,沒指標,只有利用引用了,節點類是少不了的:
然後是插入操作,根據特性,邏輯和查詢差不多,從根節點比較,小於則繼續比較其左節點;大於則比較其右節點;直到當某節點左或右節點為空時,在空值處,插入新節點。/** * 節點 * * @author zhangyu * * @param <T> */ public class Node<T extends BaseData> { // 節點資料 T data; // 父節點,左右子節點 Node<T> fatherNode, leftChildNode, rightChildNode; //是否是左節點、是否是右節點 boolean isLeftChild = false, isRightChild = false; //左節點是否存在 public boolean haveLeftChild() { return !(leftChildNode == null); } //右節點是否存在 public boolean haveRightChild() { return !(rightChildNode == null); } //構造方法 public Node(boolean isLeft, boolean isRight) { isLeftChild = isLeft; isRightChild = isRight; } }
例圖(插入65,黃色線為比較的軌跡):
程式碼:
/**
* 插入節點
*
* @param insertData 待插入的資料
* @param node 開始比較的節點
*/
private void insertNode(T insertData, Node<T> node) {
int compareResult = insertData.compareTo(node.data);
if (compareResult == 0)// 相等
return;
else if (compareResult > 0) {// 大於節點值
if (node.rightChildNode == null) {
node.rightChildNode = new Node<T>(false, true);
node.rightChildNode.data = insertData;// 插入值
node.rightChildNode.fatherNode = node;
return;
} else
insertNode(insertData, node.rightChildNode);// 繼續對比右子節點
} else {// 小於節點值
if (node.leftChildNode == null) {
node.leftChildNode = new Node<T>(true, false);
node.leftChildNode.data = insertData;// 插入值
node.leftChildNode.fatherNode = node;
return;
} else
insertNode(insertData, node.leftChildNode);// 繼續對比左子節點
}
}
/**
* 插入節點
*
* @param insertData 待插入的資料
*/
public void insertNode(T insertData) {
if (treeRoot.data == null) {
treeRoot.data = insertData;
return;
}
insertNode(insertData, treeRoot);
}
然後來看查詢操作,跟插入邏輯幾乎是一樣的,直接看程式碼吧:
/**
* 從某個節點開始搜尋
*
* @param target 目標值
* @param startSearchNode 開始搜尋的節點
* @return
*/
public Node searchNode(T target, Node startNode) {
int compareResult = target.compareTo(startNode.data);
if (compareResult == 0)
return startNode;
else if (compareResult > 0 && startNode.rightChildNode != null)
return searchNode(target, startNode.rightChildNode);
else if (compareResult < 0 && startNode.leftChildNode != null)
return searchNode(target, startNode.leftChildNode);
else
return null;
}
/**
* 查詢資料所在節點
*
* @param target 目標資料
* @return null或資料所在節點
*/
public Node searchNode(T target) {
if (treeRoot.data == null)
return null;
return searchNode(target, treeRoot);
}
/**
* 查詢資料
* @param target 目標資料(有部分檢索需要的資訊即可)
* @return 完整目標資料
*/
public BaseData searchData(T target) {
Node node = searchNode(target);
if (node != null)
return node.data;
return null;
}
然後看刪除操作,這個刪除真的是有點麻煩,為了把這部分理清楚,把程式碼調通,幾乎花費了一整天的時間,慢慢捋來~
刪除分為以下幾種情況:
1.被刪除的節點只有左節點或者只有右節點,這種情況好辦,因為節點在一條鏈上,沒有分叉,就像處理連結串列一樣把這個節點摘掉就行了。讓它的父節點關聯它的子節點,它的子節點關聯它的父節點就完事。如果它沒有父節點,說明它是根節點,直接將其子節點作為根節點就行。
2.被刪除的節點沒有子節點,這種情況也很簡單,它是葉子節點,直接置空,將其父節點對應的子節點也置空,就完事。
3.被刪除的節點有左右子節點。這種情況就有點麻煩了。
這裡需要了解兩個概念,叫“前驅”和“後繼”。分別是樹中小於它的最大值和大於它的最小值,如果把樹結構中的所有節點按順序拍好的話,它的前驅和它的後繼兩個節點剛好在它左右緊挨著它。當一個節點被刪除時,為了保證二叉樹的結構不被破壞,要讓它的前驅或者後繼節點來代替它的位置,然後將它的前驅或者後繼節點同樣做刪除操作。
那麼怎樣找前驅或者後繼呢。小於它的最大值,就是在樹中在它左邊最靠右的那個節點。同樣,大於它的最小值,就是在樹中在它右邊最靠左的那個節點。當一個節點既有左子節點又有右子節點時,前驅就是它的左子節點的右子節點的右子節點...直到最右子節點;後繼就是它的右子節點的左子節點的左子節點...直到最左子節點。上個圖吧:
23的後繼是32;98的前驅是76。
上圖中,當23被刪除,則可以用32代替它,也可以用12代替它,然後刪除掉32(或者12)的原節點就行了;同理,當98被刪除時,可以用76代替它,或者用99代替它,然後刪除76(或者99)的原節點就行了。當然,如果被刪除節點是根節點,就用代替它的節點作為根節點然後刪除代替節點的原節點就行了。再次推薦你用視覺化工具http://zh.visualgo.net/bst來看二叉樹的各種操作動畫,簡單明瞭。
所以刪除操作程式碼如下(程式碼用的是後繼節點替代待刪除節點):
/**
* 刪除節點
*
* @param node 待刪除節點
*/
private void deleteNode(Node node) {
// 如果按順序排列好節點,它的前驅和後繼就是這個序列上緊挨著它左右兩側的節點.
// 如果節點只有左節點或者只有右節點
if (node.haveLeftChild() && !node.haveRightChild()) {// 只有左節點
if (node.isLeftChild) {
node.fatherNode.leftChildNode = node.leftChildNode;
} else if (node.isRightChild) {
node.fatherNode.rightChildNode = node.leftChildNode;
} else// 待刪除節點是根節點
treeRoot = node.leftChildNode;
node.leftChildNode.fatherNode = node.fatherNode;
} else if (node.haveRightChild() && !node.haveLeftChild()) {// 只有右節點
if (node.isLeftChild) {
node.fatherNode.leftChildNode = node.rightChildNode;
} else if (node.isRightChild) {
node.fatherNode.rightChildNode = node.rightChildNode;
} else// 待刪除節點是根節點
treeRoot = node.rightChildNode;
node.rightChildNode.fatherNode = node.fatherNode;
} else if (node.haveLeftChild() && node.haveRightChild()) {// 有左右子節點
Node successorNode = getSuccessorNode(node);
if (successorNode == node.rightChildNode) {// 後繼節點是右子節點
successorNode.fatherNode = node.fatherNode;
if (node.isLeftChild)
node.fatherNode.leftChildNode = successorNode;
else if (node.isRightChild)
node.fatherNode.rightChildNode = successorNode;
else {// 是根節點
successorNode = treeRoot;
}
successorNode.fatherNode = node.fatherNode;
successorNode.leftChildNode = node.leftChildNode;
node.leftChildNode.fatherNode = successorNode;
} else {// 後繼節點是右子節點的最左子節點
if (successorNode.haveRightChild()) {// 左子節點有右子樹
successorNode.fatherNode.leftChildNode = successorNode.rightChildNode;
successorNode.rightChildNode.fatherNode = successorNode.fatherNode;
replaceNode(node, successorNode);
} else {// 左子節點沒有右子樹
// 葉節點,直接刪除
successorNode.fatherNode.leftChildNode = null;
replaceNode(node, successorNode);
}
}
} else {// 沒有子節點
if (node.isLeftChild) {
node.fatherNode.leftChildNode = null;
} else if (node.isRightChild) {
node.fatherNode.rightChildNode = null;
}
}
node = null;
}
/**
* 非相鄰節點的替換邏輯(非相鄰加粗!)
* @param node 被替換節點
* @param replaceNode 替換的節點
*/
private void replaceNode(Node node, Node replaceNode) {
if (node.isLeftChild)
node.fatherNode.leftChildNode = replaceNode;
else if (node.isRightChild)
node.fatherNode.rightChildNode = replaceNode;
else {// node是根節點
treeRoot = replaceNode;
}
node.leftChildNode.fatherNode = node.rightChildNode.fatherNode = replaceNode;
replaceNode.leftChildNode = node.leftChildNode;
replaceNode.rightChildNode = node.rightChildNode;
}
/**
* 獲取一個節點的後繼節點
* @param node
* @return
*/
private Node getSuccessorNode(Node node) {
if (!node.haveRightChild()) {// 沒有右子樹
return null;
}
Node targetNode = node.rightChildNode;
while (targetNode.haveLeftChild()) {// 找右子樹的最左孩子,保證返回的節點一定沒有左子樹
targetNode = targetNode.leftChildNode;
}
return targetNode;
}
/**
* 刪除數中的資料
* @param baseData
*/
public void deleteData(T baseData) {
Node node = searchNode(baseData);
deleteNode(node);
}
然後還有一個遍歷操作,中規中矩的先序遍歷,遞迴操作:
/**
* 遍歷節點
* @param node
*/
private void preOrder(Node node) {
System.out.println("" + node.data.toString());
if (node.haveLeftChild())
preOrder(node.leftChildNode);
if (node.haveRightChild())
preOrder(node.rightChildNode);
}
/**
* 遍歷樹(前序遍歷)
*/
public void preOrder() {
if (treeRoot == null)
return;
preOrder(treeRoot);
}
二叉搜尋樹大概就這樣吧,附上文中程式碼資源:點選開啟連結