資料結構(5):樹
目錄
- 一、樹
- 二、二叉查詢樹
- 1、插入節點
- 2、查詢節點
- 3、遍歷二叉查詢樹
- 4、刪除二叉樹節點
- 三、其他
一、樹
為什麼要使用樹,因為陣列刪除、插入的效率太低。而連結串列查詢資料的速度太慢,在樹中能快速的查詢資料項、插入資料項和刪除資料項。
從上圖中我們可以發現“樹”這種結構和現實中的樹一樣的,節點之間用線相連,形成父子關係。
知道了樹,我們還需要知道以下概念
路徑:順著連線節點的邊從一個節點到另一個節點,所經過的節點順序排列稱為路徑。
根:樹最上面的節點稱為根節點。一個樹只有一個根。而且從根到任何節點有且只有一條路徑。
父節點:每個節點都有一條邊向上連線到另一個節點,這個節點就稱為是下面這個節點的父節點。
子節點:每個節點都一條邊向下連線到另一個節點,下面的節點就是該節點的子節點。
葉子節點:沒有子節點的節點
子樹:每個節點都可以作為一個子樹的根,它和它所有的子節點,子節點的子節點組合在一起就是一個子樹。
訪問:訪問一個節點是為了在這個節點上執行一些操作,如檢視節點的資料項。但是如果僅僅是經過一個節點,不認為是訪問了這個節點。
高度:節點到葉子節點的最長路徑(邊數)
深度:根節點到這個節點所經歷的邊的個數
層:一個節點的層數是指從根開始到這個節點有多少代。
大家可以對比參考下圖理解概念
二、二叉查詢樹
二叉樹:顧名思義,樹的每個節點最多隻能有兩個節點的樹,稱為二叉樹。
二叉查詢樹:在二叉樹的基礎上實現的快速查詢的樹,在二叉查詢樹中的任意一個節點,左子樹每個節點的值,都要小於這個節點的值,而右子樹每個節點的值都應大於這個節點的值。
要想實現二叉查詢樹,首先建立一個節點類儲存資料和左右子節點的引用
/** * 二叉樹節點 */ public class Node { //資料項 public long data; //左子節點 public Node leftChild; //右子節點 public Node rightChild; public Node(long data){ this.data=data; } }
建立Tree類儲存對根節點的引用及操作節點的相關方法
/**
* 二叉樹
*/
public class Tree {
//根節點
public Node root;
}
1、插入節點
從根節點開始查詢一個相應的節點,這個節點將稱為新插入節點的父節點。當父節點找到後,通過判斷新節點的值比父節點的值的大小來決定是連線到左子節點還是右子節點。
/**
* 插入節點
*
* @param value
*/
public void insert(long value) {
//將插入的值封裝為一個節點
Node newNode = new Node(value);
//引用當前節點
Node current = root;
//引用父節點
Node parentNode;
//如果root為null,也就是第一次插入
if (root == null) {
root = newNode;
return;
} else {
while (true) {
//父節點指向當前節點
parentNode = current;
//如果當前指向的節點資料比插入的要大,則向左走
if (current.data > value) {
current = current.leftChild;
if (current == null) {
parentNode.leftChild = newNode;
return;
}
} else {
current = current.rightChild;
if (current == null) {
parentNode.rightChild = newNode;
return;
}
}
}
}
}
2、查詢節點
從根節點開始查詢,如果查詢的節點值比當前節點的值小,則繼續查詢其左子樹,否則查詢其右子樹。
/**
* 查詢節點
*
* @param value
* @return
*/
public Node find(long value) {
//引用當前節點,從根節點開始
Node current = root;
//只要查詢的節點不等於null 且不與查詢的值相等就迴圈
while (current != null && current.data != value) {
//進行比較,比較查詢值和當前節點的大小
if (current.data > value) {
current = current.leftChild;
} else {
current = current.rightChild;
}
}
return current;
}
3、遍歷二叉查詢樹
遍歷樹:遍歷樹是根據一個特定的順序訪問樹的每一個節點,根據順序的不同分為前序、中序、後序遍歷三種,這三種遍歷方式的區別就在於列印根節點值的順序。
前序遍歷:1.訪問(列印)根節點2.前序遍歷左子樹3.前序遍歷右子樹
/**
* 前序遍歷
*/
public void frontOrder(Node localNode) {
if (localNode != null) {
//訪問根節點
System.out.println(localNode.data);
//前序遍歷左子樹
frontOrder(localNode.leftChild);
//前序遍歷右子樹
frontOrder(localNode.rightChild);
}
}
中序遍歷:1.中序遍歷左子樹2.訪問(列印)根節點3.中序遍歷右子樹
/**
* 中序遍歷
*
* @param localNode
*/
public void inOrder(Node localNode) {
if (localNode != null) {
//中序遍歷左子樹
inOrder(localNode.leftChild);
//訪問根節點
System.out.println(localNode.data);
//中序右子樹
inOrder(localNode.rightChild);
}
}
後序遍歷:1.後序遍歷左子樹2.後序遍歷右子樹3.訪問(列印)根節點
/**
* 後序遍歷
*/
public void afterOrder(Node localNode) {
if (localNode != null) {
//後序遍歷左子樹
afterOrder(localNode.leftChild);
//後序遍歷右子樹
afterOrder(localNode.rightChild);
//訪問根節點
System.out.println(localNode.data);
}
}
4、刪除二叉樹節點
刪除節點是二叉樹操作中最複雜的。在刪除之前首先要查詢要刪的節點。找到節點後,這個要刪除的節點可能會有三種情況需要考慮。
1.該節點是葉子節點,沒有子節點
要刪除葉節點,只需要改變該節點的父節點的引用值,將指向該節點的引用設定為null就可以了。
2.該節點有一個子節點
改變父節點的引用,將其直接指向要刪除節點的子節點。
3.該節點有兩個子節點
要刪除有兩個子節點的節點,就需要使用它的中序後繼來替代該節點,如下圖中50
有兩個子節點。
那麼什麼是要刪除的節點的中序後繼呢,對比上圖,我們來找要刪除節點的右子節點的左子節點,直到左葉子節點。這個節點為54
那麼剛好可以作為替換50
而不影響這個二叉查詢樹的規則。
/**
* 查詢中序後繼
*
* @param delNode
* @return
*/
public Node getSuccessor(Node delNode) {
Node successor = delNode;
Node successorParent = delNode;
//要刪除節點的右子樹
Node current = delNode.rightChild;
//向左子樹查詢直到最終一個左子節點
while (current != null) {
successorParent = successor;
successor = current;
current = current.leftChild;
}
//如果中序節點不為要刪除的右子樹
if (successor != delNode.rightChild) {
//將查到的節點的父節點的左子節點(也就是查到的節點)指向查到節點的右子樹
successorParent.leftChild = successor.rightChild;
//將查到的節點的右子樹指向要刪除節點的右子樹
successor.rightChild = delNode.rightChild;
}
return successor;
}
實現刪除
/**
* 刪除節點
*
* @param value
* @return
*/
public boolean delete(long value) {
//引用當前節點,從根節點開始
Node current = root;
//引用當前節點的父節點
Node parent = root;
//是否為左節點
boolean isLeftChild = true;
if (current == null) {
return false;
}
while (current.data != value) {
parent = current;
//進行比較,比較查詢值和當前節點的大小
if (current.data > value) {
current = current.leftChild;
isLeftChild = true;
} else {
current = current.rightChild;
isLeftChild = false;
}
//如果查詢不到
if (current == null) {
return false;
}
}
//刪除葉子節點
if (current.leftChild == null && current.rightChild == null) {
if (current == root) {
root = null;
} else
//如果它是父節點的左子節點
if (isLeftChild) {
parent.leftChild = null;
} else {
parent.rightChild = null;
}
} else if (current.rightChild == null) {//如果只有一個節點,當前節點是左
if (current == root) {
root = current.leftChild;
} else
//如果當前是左節點,則為父節點的左節點賦值
if (isLeftChild) {
parent.leftChild = current.leftChild;
} else {
parent.rightChild = current.leftChild;
}
} else if (current.leftChild == null) {//只有一個子節點
if (current == root) {
root = current.rightChild;
} else if (isLeftChild) {
parent.leftChild = current.rightChild;
} else {
parent.rightChild = current.rightChild;
}
} else {
//查詢要刪除節點的中序後繼
Node successor = getSuccessor(current);
if (current == root){
root = successor;
}else if(isLeftChild){
parent.leftChild = successor;
}else{
parent.rightChild = successor;
}
successor.leftChild = current.leftChild;
}
return true;
}
三、其他
- 使用中序遍歷二叉查詢樹,輸出數列是有序的,時間複雜度是O(n),因此是十分高效的。
- 有序的數列插入到二叉查詢樹中,將產生不平衡的二叉查詢樹,影響二叉查詢樹的效能。
- 學習二叉樹可多畫圖推演