二叉查找樹定義
每棵子樹頭節點的值都比各自左子樹上所有節點值要大,也都比各自右子樹上所有節點值要小。
二叉查找樹的中序遍歷序列一定是從小到大排列的。
二叉查找樹節點定義
/// <summary>
/// 二叉查找樹節點
/// </summary>
public class Node
{
/// <summary>
/// 節點值
/// </summary>
public int Data { get; set; }
/// <summary>
/// 左子節點
/// </summary>
public Node Left { get; set; }
/// <summary>
/// 右子節點
/// </summary>
public Node Right { get; set; }
/// <summary>
/// 打印節點值
/// </summary>
public void DisplayNode()
{
Console.Write(Data + " ");
}
}
插入節點
二叉查找樹的插入節點操作相對比較簡單,只需要找到要插入節點的位置放置即可。
插入節點的整體流程:
把父節點設置為當前節點,即根節點。
如果新節點內的數據值小於當前節點內的數據值,那么把當前節點設置為當前節點的左子節點。如果新節點內的數據值大於當前節點內的數據值,那么就跳到步驟 4。
如果當前節點的左子節點的數值為空(null),就把新節點插入在這里並且退出循環。否則,跳到 while 循環的下一次循環操作中。
把當前節點設置為當前節點的右子節點。
如果當前節點的右子節點的數值為空(null),就把新節點插入在這里並且退出循環。否則,跳到 while 循環的下一次循環操作中。
代碼實現:
public class BinarySearchTree
{
public Node root;
public BinarySearchTree()
{
root = null;
}
/// <summary>
/// 二叉查找樹插入結點
/// </summary>
/// <param name="i"></param>
public void Insert(int i)
{
Node newNode = new Node
{
Data = i
};
if (root == null)
{
root = newNode;
}
else
{
Node current = root;
Node parent;
while (true)
{
parent = current;
if (i < current.Data)
{
current = current.Left;
if (current == null)
{
parent.Left = newNode;
break;
}
}
else
{
current = current.Right;
if (current == null)
{
parent.Right = newNode;
break;
}
}
}
}
}
}
因為二叉查找樹的中序遍歷序列一定是由小到大排列的,所以我們可以通過中序遍歷測試二叉查找樹的插入操作。關於二叉樹遍歷操作可以移步我的上一篇博客【圖解數據結構】 二叉樹遍歷。
中序遍歷代碼實現:
/// <summary>
/// 二叉查找樹中序遍歷
/// </summary>
/// <param name="node"></param>
public void InOrder(Node node)
{
if (node != null)
{
InOrder(node.Left);
node.DisplayNode();
InOrder(node.Right);
}
}
測試代碼:
class BinarySearchTreeTest
{
static void Main(string[] args)
{
BinarySearchTree bst = new BinarySearchTree();
bst.Insert(23);
bst.Insert(45);
bst.Insert(16);
bst.Insert(37);
bst.Insert(3);
bst.Insert(99);
bst.Insert(22);
Console.WriteLine("中序遍歷: ");
bst.InOrder(bst.root);
Console.ReadKey();
}
}
測試結果:
上面的測試代碼形成了一棵這樣的二叉查找樹:
查找節點
對於 二叉查找樹(BST) 有三件最容易做的事情:查找一個特殊數值,找到最小值,以及找到最大值。
查找最小值
根據二叉查找樹的性質,二叉查找樹的最小值一定是在左子樹的最左側子節點。
所以實現很簡單,就是從根結點出發找出二叉查找樹左子樹的最左側子節點。
代碼實現:
/// <summary>
/// 查找二叉查找樹最小值
/// </summary>
/// <returns></returns>
public int FindMin()
{
Node current = root;
while (current.Left != null)
{
current = current.Left;
}
return current.Data;
}
查找最大值
根據二叉查找樹的性質,二叉查找樹的最大值一定是在右子樹的最右側子節點。
所以實現很簡單,就是從根結點出發找出二叉查找樹右子樹的最右側子節點。
代碼實現:
/// <summary>
/// 查找二叉查找樹最大值
/// </summary>
/// <returns></returns>
public int FindMax()
{
Node current = root;
while (current.Right != null)
{
current = current.Right;
}
return current.Data;
}
查找特定值
根據二叉查找樹的性質,從根結點開始,比較特定值和根結點值的大小。如果比根結點值大,則說明特定值在根結點右子樹上,繼續在右子節點執行此操作;如果比根結點值小,則說明特定值在根結點左子樹上,繼續在左子節點執行此操作。如果到執行完成都沒有找到和特定值相等的節點值,那么二叉查找樹中沒有包含此特定值的節點。
代碼實現:
/// <summary>
/// 查找二叉查找樹特定值節點
/// </summary>
/// <param name="key">特定值</param>
/// <returns></returns>
public Node Find(int key)
{
Node current = root;
while (current.Data != key)
{
if (key < current.Data)
{
current = current.Left;
}
if (key > current.Data)
{
current = current.Right;
}
// 如果已到達 BST 的末尾
if (current == null)
{
return null;
}
}
return current;
}
刪除節點
相對於前面的操作,二叉查找樹的刪除節點操作就顯得要復雜一些了,因為刪除節點會有破壞 BST 正確
層次順序的風險。
我們都知道在二叉查找樹中的結點可分為:沒有子節點的節點,帶有一個子節點的節點 ,帶有兩個子節點的節點 。那么可以將二叉查找樹的刪除節點操作簡單拆分一下,以便於我們的理解。如下圖:
刪除葉子節點
刪除葉子節點是最簡單的事情。 唯一要做的就是把目標節點的父節點的一個子節點設置為空(null)。
查看這個節點的左子節點和右子節點是否為空(null),都為空(null)說明為葉子節點。
然后檢測這個節點是否是根節點。如果是,就把它設置為空(null)。
否則,如果isLeftChild 為true,把父節點的左子節點設置為空(null);如果isLeftChild 為false,把父節點的右子節點設置為空(null)。
代碼實現:
//要刪除的結點是葉子結點的處理
if (current.Left == null && current.Right == null)
{
if (current == root)
root = null;
else if (isLeftChild)
parent.Left = null;
else
{
parent.Right = null;
}
}
刪除帶有一個子節點的節點
當要刪除的節點有一個子節點的時候,需要檢查四個條件:
- 這個節點的子節點可能是左子節點;
- 這個節點的子節點可能是右子節點;
- 要刪除的這個節點可能是左子節點;
- 要刪除的這個節點可能是右子節點。
代碼實現:
//要刪除的結點是帶有一個子節點的節點的處理
//首先判斷子結點是左子節點還是右子節點,然后再判斷當前節點是左子節點還是右子節點
else if (current.Right == null)
if (current == root)
root = current.Left;
else if (isLeftChild)
parent.Left = current.Left;
else
parent.Right = current.Left;
else if (current.Left == null)
if (current == root)
root = current.Right;
else if (isLeftChild)
parent.Left = current.Right;
else
parent.Right = current.Right;
刪除帶有兩個子節點的節點
如果要刪除標記為 52 的節點,需要重構這棵樹。這里不能用起始節點為 54 的子樹來替換它,因為 54 已經有一個左子節點了。這個問題的答案是把中序后繼節點移動到要刪除節點的位置上。 當然還要區分后繼節點本身是否有子節點。
這里我們需要了解一下后繼節點的定義。
一個節點的后繼節點是指,這個節點在中序遍歷序列中的下一個節點。相應的,前驅節點是指這個節點在中序遍歷序列中的上一個節點。
舉個例子,下圖中的二叉樹中序遍歷序列為: DBEAFCG,則A的后繼節點為F,A的前驅節點為E。
了解了這些,刪除帶有兩個子節點的節點的操作就可以轉化為尋找要刪除節點的后繼節點並且把要刪除節點的右子樹賦給后繼結點的右子節點,這里需要注意的是如果后繼節點本身有子節點,則需要將后繼節點的子結點賦給后繼節點父節點的左子節點。
先上獲取后繼結點的代碼,然后舉個例子說明:
/// <summary>
/// 獲取后繼結點
/// </summary>
/// <param name="delNode">要刪除的結點</param>
/// <returns></returns>
public Node GetSuccessor(Node delNode)
{
//后繼節點的父節點
Node successorParent = delNode;
//后繼節點
Node successor = delNode.Right;
Node current = delNode.Right.Left;
while (current != null)
{
successorParent = successor;
successor = current;
current = current.Left;
}
//如果后繼結點不是要刪除結點的右子結點,
//則要將后繼節點的子結點賦給后繼節點父節點的左節點
//刪除結點的右子結點賦給后繼結點作為 后繼結點的后繼結點
if (successor != delNode.Right)
{
successorParent.Left = successor.Right;
successor.Right = delNode.Right;
}
return successor;
}
刪除帶有兩個子節點的節點的代碼實現:
//要刪除的結點是帶有兩個子節點的節點的處理
else
{
Node successor = GetSuccessor(current);
if (current == root)
root = successor;
else if (isLeftChild)
parent.Left = successor;
else
parent.Right = successor;
//因為后繼結點是要刪除結點右子樹的最左側結點
//所以后繼結點的左子樹肯定是要刪除結點左子樹
successor.Left = current.Left;
}
我們觀察到刪除節點的后繼節點一定是刪除節點右子樹的最左側節點。這里有3種情況:
后繼節點是刪除節點的子節點
刪除節點37,后繼節點40是刪除節點37的子節點。delNode
是結點37,successor
是節點40,delNode.Right
是節點40,successor == delNode.Right
,后繼節點為刪除節點的子節點,這種情況是最簡單的。
后繼節點不是刪除節點的子節點
后繼節點38是刪除節點37右子樹的最左側節點。delNode
是節點37,successor
是節點38,successorParent
是節點40,delNode.Right
是節點40。successor != delNode.Right
,所以要將 successorParent.Left = successor.Right;successor.Right = delNode.Right;
。因為successor.Right==null
,所以successorParent.Left = null
。successor.Right = delNode.Right
,節點40成為了節點38的右子節點。因為刪除節點的后繼節點一定是刪除節點右子樹的最左側節點,所以后繼節點肯定沒有左子節點。刪除節點被刪除后,后繼結點會補到刪除節點的位置。successor.Left = current.Left;
,也就是刪除節點的左子節點變成了后繼節點的左子節點。
完成刪除節點后的搜索二叉樹變為:
后繼節點不是刪除節點的子節點且有子節點
這種情況和上一種情況相似,唯一的區別是后繼節點有子節點(注意肯定是右子節點)。也就是successorParent.Left = successor.Right;
,后繼節點的右子節點變成后繼結點父節點的左子節點。因為successor.Right
是節點39,所以節點40的左子節點變成了節點39。其它操作和上一種情況完全相同。
完成刪除節點后的搜索二叉樹變為:
刪除節點操作的整體流程:
- 把后繼節點的右子節點賦值為后繼節點的父節點的左子節點。
- 把要刪除節點的右子節點賦值為后繼節點的右子節點。
- 從父節點的右子節點中移除當前節點,並且把它指向后繼節點。
- 從當前節點中移除當前節點的左子節點,並且把它指向后繼節點的左子節點。
綜合以上刪除節點的三種情況,刪除節點操作的完整代碼如下:
/// <summary>
/// 二叉查找樹刪除節點
/// </summary>
/// <param name="key"></param>
/// <returns></returns>
public bool Delete(int key)
{
//要刪除的當前結點
Node current = root;
//當前結點的父結點
Node parent = root;
//當前結點是否是左子樹
bool isLeftChild = true;
//先通過二分查找找出要刪除的結點
while (current.Data != key)
{
parent = current;
if (key < current.Data)
{
isLeftChild = true;
current = current.Left;
}
else
{
isLeftChild = false;
current = current.Right;
}
if (current == null)
return false;
}
//要刪除的結點是葉子結點的處理
if (current.Left == null && current.Right == null)
{
if (current == root)
root = null;
else if (isLeftChild)
parent.Left = null;
else
{
parent.Right = null;
}
}
//要刪除的結點是帶有一個子節點的節點的處理
else if (current.Right == null)
if (current == root)
root = current.Left;
else if (isLeftChild)
parent.Left = current.Left;
else
parent.Right = current.Left;
else if (current.Left == null)
if (current == root)
root = current.Right;
else if (isLeftChild)
parent.Left = current.Right;
else
parent.Right = current.Right;
//要刪除的結點是帶有兩個子節點的節點的處理
else
{
Node successor = GetSuccessor(current);
if (current == root)
root = successor;
else if (isLeftChild)
parent.Left = successor;
else
parent.Right = successor;
//因為后繼結點是要刪除結點右子樹的最左側結點
//所以后繼結點的左子樹肯定是要刪除結點左子樹
successor.Left = current.Left;
}
return true;
}
/// <summary>
/// 獲取后繼結點
/// </summary>
/// <param name="delNode">要刪除的結點</param>
/// <returns></returns>
public Node GetSuccessor(Node delNode)
{
//后繼節點的父節點
Node successorParent = delNode;
//后繼節點
Node successor = delNode.Right;
Node current = delNode.Right.Left;
while (current != null)
{
successorParent = successor;
successor = current;
current = current.Left;
}
//如果后繼結點不是要刪除結點的右子結點,
//則要將后繼節點的子結點賦給后繼節點父節點的左節點
//刪除結點的右子結點賦給后繼結點作為 后繼結點的后繼結點
if (successor != delNode.Right)
{
successorParent.Left = successor.Right;
successor.Right = delNode.Right;
}
return successor;
}
刪除節點測試
我們還是使用中序遍歷進行測試,首先構造二叉查找樹:
static void Main(string[] args)
{
BinarySearchTree bst = new BinarySearchTree();
bst.Insert(23);
bst.Insert(45);
bst.Insert(16);
bst.Insert(37);
bst.Insert(3);
bst.Insert(99);
bst.Insert(22);
bst.Insert(40);
bst.Insert(35);
bst.Insert(38);
bst.Insert(44);
bst.Insert(39);
}
構造出的二叉查找樹:
測試分三種情況:
測試刪除葉子節點
刪除葉子節點39
Console.Write("刪除節點前: ");
bst.InOrder(bst.root);
bst.Delete(39);
Console.Write("刪除節點后: ");
bst.InOrder(bst.root);
測試結果:
測試刪除帶有一個子節點的節點
刪除帶有一個子節點的節點38
Console.Write("刪除節點前: ");
bst.InOrder(bst.root);
bst.Delete(38);
Console.Write("刪除節點后: ");
bst.InOrder(bst.root);
測試結果:
測試刪除帶有兩個子節點的節點
刪除帶有兩個子節點的節點37
Console.Write("刪除節點前: ");
bst.InOrder(bst.root);
bst.Delete(37);
Console.Write("刪除節點后: ");
bst.InOrder(bst.root);
測試結果:
參考:
《數據結構與算法 C#語言描述》
《大話數據結構》
《數據結構與算法分析 C語言描述》
五一大家都出去happy了,為什么我還要自己在家擼代碼,是因為愛嗎?是因為責任嗎?都不是。是因為我的心里只有學習(其實是因為窮)。哈哈,提前祝大家五一快樂,吃好玩好!