今天用PHP實現了一下二分搜尋樹(BST),實現的程式碼還是很簡單的,下面來總結一下。
首先先來介紹一下二分搜尋樹。二分搜尋樹,本質上就是一棵二叉樹,它並不一定是一棵滿二樹,也並不一定是一棵完全二叉樹,但是二分搜尋樹中的每一個非葉子節點的值都要大於其左子節點的值,同時還要小於其右子節點的值。參考下面的圖,可以更好的理解二分搜尋樹。
二分搜尋樹的基本概念非常的簡單,很好理解。根據上面的概念,我們可以很輕鬆的得到下面的結論:
(1):任意一個非葉子節點的左子樹中的所有的節點的值都要小於該非葉子節點,並且都要小於該非葉子節點的右子樹中的所有節點,這一點對於後面實現節點的刪除來說至關重要。
(2):整棵二分搜尋樹中的最大值節點一定是處於該二分搜尋樹中最左邊的葉子節點,最小值節點一定是處於該二分搜尋樹中最右邊的葉子節點,根據這個規律我們後面可以很輕鬆的查詢到整棵樹中的最大值以及最小值。
(3):由於上面的性質,二分搜尋樹被廣泛用於”字典”這種資料型別的實現,比如說PHP中的array資料型別。參照上面的圖片,假如我們每一個節點中的數值代表array中的key,那麼如果我們再在每一個節點中設定一個value,那麼我是不是就實現了key=>value這種鍵值對了那。說到這裡,又得同學就會說了,為什麼一定要使用樹這種資料結構來實現key=>value這種鍵值對那,普通的陣列也可以實現的啊。關於這一點,主要是因為通過陣列來實現的話,查詢,修改等操作的平均時間複雜度是O(N),而採用二分搜尋樹來實現的話,平均時間複雜度只有O(logN),下面我們有具體的程式碼來大家可以分析一下。
上面說了一些二分搜尋的基本概念,下面我們就用PHP來實現一個二分搜尋樹。這裡需要先說一點,二分搜尋樹由於不具備堆的特殊性質,並且其還是鏈式結構,所以很難用陣列進行實現。我最早學習演算法與資料結構的時候,是通過C中的指標來實現這種典型的鏈式結構的,現在換成了PHP,沒了指標,一開始真心不習慣,後來發現使用PHP中的物件完全可以模擬指標來實現二分搜尋樹。好了,廢話不說了,下面直接用PHP來實現一個滿足key=>value鍵值對的二分搜尋樹。
(1):定義用來描述二分搜尋樹中節點的Node類
<?php
//Node類
class Node{
//當前節點的key
public $key = null;
//當前節點的value
public $value = null;
//當前節點的左子節點
public $left = null;
//當前節點的右子節點
public $right = null;
}
//每一個例項化Node類的物件都用來描述二分搜尋樹中一個節點
//其中的屬性key,value是來存放鍵值對資訊
//屬性right用來記錄該節點的右子節點,屬性left用來記錄該節點的左子節點。
?>複製程式碼
(2):初始化構建一個二分搜尋樹
//使用陣列構建二分搜尋樹
//將陣列傳遞到函式中,遍歷迴圈陣列,將陣列中的元素依次新增到二分搜尋樹中
function bulid_binary_search_tree($arr){
if(empty($arr)){
return null;
}
//初始情況下,建立一個根節點
$root = new Node();
$level = 1;
foreach($arr as $key=>$value){
//為根節點進行賦值操作
if($level == 1){
$root->key = $key;
$root->value = $value;
}else{
//建立一個新節點
$new_node = new Node();
$new_node->key = $key;
$new_node->value = $value;
//將新建立的節點,新增到初始化好的二分搜尋樹中
/*
新增操作,是初始化二分搜尋樹的關鍵操作
因為在處理新增節點的過程之中要根據傳入的資料的大小,不斷的分析新節點需要存放的位置
*/
insert_binary_search_tree($root,$new_node);
}
$level++;
}
return $root;
}
//新增節點到二分搜尋樹中
//思路如下:如果待新增的節點的key大於(小於)根節點,就將該節點與根節點的右(左)子節點繼續比較
//如果根節點的右(左)節點為空,也就是說根節點並不存在右(左)子節點,就將該節點放置到
//根節點的右(左)子節點的位置上。如果存在右(左)子節點,就將該新增的節點與右(左)子節
點繼續按照上面的方式比較,直到放置到合適的位置為止
function insert_binary_search_tree($root,$new_node){
//新新增的節點與根節點進行比較
//如果新節點的key與根節點的key一致的話,就使用新的節點中的value替換root節點的value
if($root->key == $new_node->key){
//進行替換操作
$root->value = $new_node->value;
return true;
}elseif($root->key > $new_node->key){ //如果新新增的節點的key小於root節點的key
if($root->left == null){
$root->left = $new_node;
return true;
}else{
$root = $root->left;
insert_binary_search_tree($root,$new_node);
}
}else{ //如果新新增的節點的key大於root節點的key
if($root->right == null){
$root->right = $new_node;
return true;
}else{
$root = $root->right;
insert_binary_search_tree($root,$new_node);
}
}
}
//已經準備好的初始化的陣列
$node_arr = [2=>"222",1=>"111",0=>"000",4=>"444",3=>"333"];
//呼叫初始化函式,建立二分搜尋樹
$result = build_binary_search_tree($node_arr);
echo "<pre/>";
//列印生成的二分搜尋樹
var_dump($result);複製程式碼
OK,上面的程式碼就實現了二分搜尋樹,下面是列印的結果,怎麼樣是不是一目瞭然那?
這裡需要說明的是,我們插入的陣列node_arr中第一個新節點是key=2的節點,所以該節點就是我們整個二分搜尋樹中的根節點。上面我們已經初始化成功了一個二分搜尋樹,這只是一小步,我們還要實現二分搜尋樹中元素的查詢,遍歷以及節點刪除等功能。
(3):查詢二分搜尋樹中是否存在某個key值
我們一開始就已經提到過了,採用二分搜尋樹這種資料結構進行資料的查詢的平均時間複雜度是O(logN),是要優與順序資料的O(N)的。下面是兩個實現查詢key的函式,一個用來查詢key是否存在,一個是返回key所對用的value,在功能上和PHP中的isset()函式以及陣列名[key]非常的相像。
<?php
//查詢key是否存在
function contain_key($root,$key){
//如果遞迴到null,直接進行返回
if($root == null){
return false;
}
//比較當前節點的key是否和需要查詢的key相等
if($root->key == $key){
return true;
}elseif($root->key > $key){ //如果當前節點的key大於需要查詢的key
return contain_key($root->left,$key);
}elseif($root->key < $key){ //如果當前節點的key小於需要查詢的key
return contain_key($root->right,$key);
}
}
//查詢key所對應的value
function get_value($root,$key){
//如果遞迴到null,直接進行返回
if($root == null){
return false;
}
//比較當前節點的key是否和需要查詢的key相等
if($root->key == $key){
return $root->value;
}elseif($root->key > $key){ //如果當前節點的key大於需要查詢的key
return get_value($root->left,$key);
}elseif($root->key < $key){ //如果當前節點的key小於需要查詢的key
return get_value($root->right,$key);
}
}
?>複製程式碼
我們之前還提到過,二分搜尋樹中最大值以及最小值的所處位置的特性,下面我們就利用上面提到的特性來查詢的二分搜素中的最大值以及最小值,在功能上非常類似PHP中的max()函式以及min()函式。
<?php
//獲取最大值
function find_max($root){
if($root->right !=null ){
return find_max($root->right);
}else{
return $root->key;
}
}
//獲取最小值
function find_min($root){
if($root->left !=null ){
return find_min($root->left);
}else{
return $root->key;
}
}
?>複製程式碼
上面的程式碼邏輯很簡單,相信大家都能看的明白,這裡就不贅述了。
(4):上面我們已經完成了資料的查詢,下面我們實現一下二分搜尋樹的遍歷操作
二分搜尋樹的遍歷操作分為兩大類,一種是深度優先遍歷,一種是廣度優先遍歷。深度優先遍歷就是不斷的遞迴向下遍歷,廣度優先遍歷就是按照二分搜尋樹的層級,從上到下,每次遍歷出一層所有的節點,然後再向下查詢。平常人家經常談論的前序遍歷,中序遍歷以及後序遍歷都屬於深度優先遍歷,三者之間的差別在於獲取當前節點的先後順序不一樣而已,下面我們就用程式碼實現一下上面提到的各種遍歷方式。
1):前序遍歷
<?php
function tree_before($root){
if($root != null){
echo $root->value."<br/>";
tree_before($root->left);
tree_before($root->right);
}
}
?>複製程式碼
列印結果如下: 222,111,000,444,333
2):中序遍歷
<?php
function tree_mid($root){
if($root != null){
tree_mid($root->left);
echo $root->value."<br/>";
tree_mid($root->right);
}
}
?>複製程式碼
列印結果如下: 000,111,222,333,444
3):後續遍歷
<?php
function tree_next($root){
if($root != null){
tree_next($root->left);
tree_next($root->right);
echo $root->value."<br/>";
}
}
?>複製程式碼
列印結果如下: 000,111,333,444,222
大家可以看到,上面的遍歷程式碼非常的簡單,三則之間不同在於訪問當前節點的順序不一樣,其他完全一致,都是採用遞迴的方式進行呼叫的。這裡需要說的是,二分搜尋樹這種資料結構非常適合使用遞迴的方式來進行處理,這也算是其特性之一了。還有一點非常的重要,就是中序遍歷的結果是有序的,這一點大家可以簡單分析一下節點的訪問路徑就明白了,所以,如果想要對二分搜尋樹中的元素進行排序的話,使用二分搜尋樹再合適不過了。
接下來我們們再說說廣度優先遍歷的實現。廣度優先遍歷的實現,需要藉助佇列來實現,下面看程式碼。
<?php
function tree_level($root,&$queue){
if($root == null){
return;
}else{
echo $root->key."<br/>";
}
if($root->left != null){
$queue[] = $root->left;
}
if($root->right != null){
$queue[] = $root->right;
}
if(count($queue)>=1){
$item = array_shift($queue);
tree_level($item,$queue);
}
}
$array = [];
tree_level($result,$array);
?>複製程式碼
基本的思路如下:我們首先訪問根節點,然後將根節點的兩個子節點存放到array()中,然後每次從array()中取出一個沒有遍歷的節點,輸出該節點的值,並將該節點的左右兩個節點儲存到array()中,依次類推,直到取到到所有節點的都沒有左右子節點為止。
(5):最後看一下如何刪除二分搜尋中的一個節點
刪除一個節點的方式,也非常的簡單,通常情況下,我們都是採用Hubbard deletion思想來實現。基本的思路如下:如果要刪除一個節點,如果其不含有子節點,可以直接進行刪除。如果其含有子節點,就需要在其子節點中找到一個節點來替換掉這個需要被刪除的節點。這裡需要注意的是,繼續替換操作的子節點,要能夠繼續維持二分搜尋樹的基本性質,那麼哪個子節點才能做到那?答案就是需要刪除的節點的左子樹中最大值或者是右子樹中的最小值。為什麼那?因為根據二分搜尋樹的基本性質,左子樹中的所有節點都要小於父節點,右子節點中的所有節點都大於父節點。刪除掉該節點後,左子樹中的最大值肯定小於右子樹中的所有值,那麼它就可以來替換需要刪除的節點。反之,右子樹中的最小值也可以。
看下面的程式碼:
<?php
function find_min_node(&$root){
if($root->left !=null ){
return find_min_node($root->left);
}else{
return $root;
}
}
function delete_node(&$root,$key){
//根據傳遞進來的key,找到這個節點,並且要找到這個節點的父節點
//如果需要刪除的節點是根節點
if($root->key == $key){
$parent_node = $root->right;
//根據$parent_node,查詢到右子樹中的最小的節點
$node = find_min_node($parent_node);
//將root節點進行替換
$root->key = $node->key;
$root->value = $node->value;
//刪除子節點
unset($node);
return $root;
}else{
//如果需要刪除的節點並不是根節點
//如果這個節點沒有子節點
if($root->left == null && $root->right == null){
//直接刪除這個節點
//pass
//return
}
//如果這個節點沒有右子節點
if($root->right ==null ){
//獲取左子樹中的最大值節點
//進行替換刪除的操作
//pass
//return
}else{
//獲取右子樹中的最小值節點
//進行替換刪除的操作
//pass
//return
}
}
}
?>複製程式碼
在最後,我們還得說一下,二分搜尋樹的缺點。假設我們需要將陣列[1,2,3,4,5]中的元素依次存放到一個空的二分搜尋樹中,那麼就會生成一個以1為根節點,2為1的右子節點,3為2的右子節,4為3的右子節點,5為4的右子節點的二分搜尋樹,你回發現這棵二分搜尋樹的任意一個非葉子節點都不存在左子樹。我們可以將這種情況視為二分搜尋樹的最壞情況,整棵樹的高度為n(平均情況下為logN),再進行查詢等操作的時候,時間複雜度將會下降到O(N)級別。對於這種情況的出現,並不是沒有解決的辦法,使用平衡樹就可以解決,有興趣的同學可以研究一下。