二叉查找樹

二叉查找樹


關於二叉查找樹的簡介 百度百科 和 維基百科node

本文使用Go語言進行描述算法


1) 二叉樹建立

有以下數列,建立一顆二叉查找樹數組

{50,22,30,16,18,43,56, 112,91,32,71,28}

使用以下的規則進行建立:數據結構

0)沒有鍵值相等的結點函數

1)若是要插入的節點鍵值比當前節點小,則插入到當前節點的左子樹,不然插入到當前節點的右子樹工具

首先,定義二叉樹節點的數據結構ui

 type BNode struct{
     key int
     value string
     lt, rt *BNode
 }

向二叉樹添加新節點的操做以下spa

func add_node(node *BNode, key int) (*BNode) {
    if nil == node {
        var n BNode
        n.key = key
        node = &n
    } else if node.key > key {
        node.lt = add_node(node.lt, key)
    } else {
        node.rt = add_node(node.rt, key)
    }

    return node
}

因此創建二叉查找樹的過程以下code

 func main(){
     list := []int {50,22,30,16,18,43,56, 112,91,32,71,28}
     var root * BNode = nil
     for _, v := range list {
         root = add_node(root, v);
     }
 }

其中 BNode 結構中的 value 沒有被使用。htm


2) 二叉樹遍歷

二叉樹創建好了,可是是存在於內存中,怎樣才能知道建立的沒問題呢?

咱們知道,對於一棵二叉樹,其(中根遍歷 + 先根遍歷),或者(中根遍歷 + 後根遍歷) 能夠逆向推導出二叉樹的結構。 因此接下來,咱們要對二叉樹進行一次中根遍歷和一次先根遍歷,並經過這兩組數據驗證下二叉樹結構。

先根遍歷的代碼以下:

 func pre_list(node *BNode) {
     if nil == node {
         return
     }
     fmt.Printf("%d ", node.key);
     pre_list(node.lt)
     pre_list(node.rt)
 }

中根遍歷的代碼以下:

 func mid_list(node *BNode) {
     if nil == node {
         return
     }
     mid_list(node.lt)
     show_node(node)
     mid_list(node.rt)
 }

主函數代碼以下:

func main(){
    list := []int {50,22,30,16,18,43,56, 112,91,32,71,28}
    var root * BNode = nil
    for _, v := range list {
        root = add_node(root, v);
    }

    pre_list(root)
    fmt.Fprintf(os.Stderr, "\n")
    mid_list(root);
    fmt.Fprintf(os.Stderr, "\n")
}

執行結果以下:

$ go run make_b_tree.go 
50 22 16 18 30 28 43 32 56 112 91 71 
16 18 22 28 30 32 43 50 56 71 91 112

咱們能夠根據上面的結果動手在紙上畫一下,看看有沒有建立成功。呵呵,開個玩笑。後面會講如何重建二叉樹。


3) 畫出二叉樹

除了動手畫出來,咱們還能夠藉助一些工具把它畫出來,好比 Graphviz 。

下面這段代碼是使用先根遍歷的方法畫出二叉樹的代碼,其做用是輸出一段 dot 腳本。

func show_dot_node(node *BNode){
    if nil == node {
        return
    }
    fmt.Printf("    %d[label=\"<f0> | <f1> %d | <f2> \"];\n", node.key, node.key)
}

func show_dot_line(from , to *BNode, tag string) {
    if nil == from || nil == to {
        return
    }
    fmt.Printf("    %d:%s -> %d:f1;\n", from.key, tag, to.key)
}

func show_list(node * BNode) {
    if nil == node {
        return
    }
    show_dot_node(node)
    show_dot_line(node, node.lt, "f0:sw")
    show_dot_line(node, node.rt, "f2:se")

    show_list(node.lt)
    show_list(node.rt)
}

func make_dot(root * BNode) {
    fmt.Printf("digraph G{\n\
    node[shape=record,style=filled,color=cadetblue3,fontcolor=white];\n")
    show_list(root)
    fmt.Printf("}\n")
}

主函數則變動以下:

func main(){
    list := []int {50,22,30,16,18,43,56, 112,91,32,71,28}
    var root * BNode = nil
    for _, v := range list {
        root = add_node(root, v);
    }

    make_dot(root);
}

執行結果以下:

digraph G{
    node[shape=record,style=filled,color=cadetblue3,fontcolor=white];
    50[label="<f0> | <f1> 50 | <f2> "];
    50:f0:sw -> 22:f1;
    50:f2:se -> 56:f1;
    22[label="<f0> | <f1> 22 | <f2> "];
    22:f0:sw -> 16:f1;
    22:f2:se -> 30:f1;
    16[label="<f0> | <f1> 16 | <f2> "];
    16:f2:se -> 18:f1;
    18[label="<f0> | <f1> 18 | <f2> "];
    30[label="<f0> | <f1> 30 | <f2> "];
    30:f0:sw -> 28:f1;
    30:f2:se -> 43:f1;
    28[label="<f0> | <f1> 28 | <f2> "];
    43[label="<f0> | <f1> 43 | <f2> "];
    43:f0:sw -> 32:f1;
    32[label="<f0> | <f1> 32 | <f2> "];
    56[label="<f0> | <f1> 56 | <f2> "];
    56:f2:se -> 112:f1;
    112[label="<f0> | <f1> 112 | <f2> "];
    112:f0:sw -> 91:f1;
    91[label="<f0> | <f1> 91 | <f2> "];
    91:f0:sw -> 71:f1;
    71[label="<f0> | <f1> 71 | <f2> "];
}
咱們把這段 dot 代碼寫入文件,btree.gv ,執行以下命令:
dot -Tpng -obtree.png btree.gv

成功的話,則會生成 btree.png 圖片,以下所示:

btree


4) 重建二叉樹

下面根據咱們獲得的(中根遍歷)和(先根遍歷)來重建二叉樹,兩組數據以下:

pre : 50 22 16 18 30 28 43 32 56 112 91 71 
mid : 16 18 22 28 30 32 43 50 56 71 91 112

重建規則以下:

0)沒有重複的數字

1)從(先根遍歷)的數組 pre_list 中取開頭的第一個數字A=pre_list[0], 這個數 A 就是這個數組所組成的樹BT的樹根

2)從(中根遍歷)的數組 mid_list 中找到第 1)步的數字A。 在mid_list中,全部在 A 左邊的數字都屬於 BT 的左子樹lt, 全部在 A 右邊的數字,都屬於 BT 的的右子樹rt。

3)遞歸解析lt和rt兩組數字

重建二叉樹的代碼以下:

定義二叉樹節點結構和輔助函數:

type BNode struct{
    key int
    value string
    lt, rt *BNode
}

func show_node(node * BNode) {
    if nil == node {
        return
    }
    fmt.Fprintf(os.Stderr, "%d ", node.key)
}

func pre_list(root *BNode) {
    if nil == root {
        return
    }
    show_node(root)
    pre_list(root.lt)
    pre_list(root.rt)
}

func mid_list(root *BNode) {
    if nil == root {
        return
    }
    mid_list(root.lt)
    show_node(root)
    mid_list(root.rt)
}

重建二叉樹

//查找一個數字在數列中的位置:
func get_num_pos(list []int, num int) (int) {
    var pos int = -1
    for i, v := range list {
        if num == v {
            pos = i
            break
        }
    }

    return pos;
}

//遞歸建樹
func rebuild_tree(tree * BNode, pre, mid []int) (* BNode) {
    if len(pre) <= 0 || len(mid) <= 0 {
        return tree
    }
    //(先根遍歷)的第一個數字就是這棵樹的樹根
    root := pre[0]
    var pos int
    if pos = get_num_pos(mid, root); pos < 0 {
        return tree
    }
    if nil == tree {
        var n BNode
        n.key = root
        tree = &n
    }
    //重建左子樹
    tree.lt = rebuild_tree(tree.lt, pre[1 : 1 + pos], mid[:pos])

    //重建右子樹
    tree.rt = rebuild_tree(tree.rt, pre[1 + pos :], mid[pos + 1:])

    return tree
}

func main() {
    pre := []int {50, 22, 16, 18, 30, 28, 43, 32, 56, 112, 91, 71}
    mid := []int {16, 18, 22, 28, 30, 32, 43, 50, 56, 71, 91, 112}

    tree := rebuild_tree(nil, pre, mid)

    //重建後再進行一次(先根遍歷)和一次(中根遍歷),檢查輸出結果是否和咱們輸入的相同。
    pre_list(tree)
    fmt.Fprintf(os.Stderr, "\n")
    mid_list(tree)
    fmt.Fprintf(os.Stderr, "\n")
}

執行代碼以下:

$ go run rbulid_binary_tree.go
50 22 16 18 30 28 43 32 56 112 91 71
16 18 22 28 30 32 43 50 56 71 91 112

看樣子結果相同 ~.~


5) 算法複雜度分析

接下來分析下二叉查找樹的空間複雜度和時間複雜度。

5.1)空間複雜度

空間複雜度比較好分析。咱們在建樹的時候,是否是須要對每個數據申請一次內存呢。 每一個數據一次,那就是有多少數據,就要申請多少次,有n個數據就要 申請n次, 因此空間光是申請用於存放數據的內存次數就是n,這個和數據的規模是正相關的, 而且關係是O(x * n),其中x是每一個數據佔用的內存數量。由於這個x在數據結構不變的狀況下是不變的, 是不會隨着數據規模而變化的,那就能夠忽略,由於x是個常數,與n無關。 因此只是申請存放數據的空間的空間複雜度爲O(n)。

那還有什麼地方須要空間呢?就是遞歸的時候,須要棧空間。 樹每深一層,就須要遞歸一次,也就須要保存一次棧空間。 在平均狀況下,樹的深度是lgN。可是在極端狀況下,樹的深度但是N啊。請看下面的圖。

a樹就是最差的樹,這哪兒還像是一棵樹啊,基本就是鏈表了;而b樹就是一棵好樹,深度最優。

ab.png

因此最壞的遞歸建樹棧空間也是O(n),不過最好的是O(lgN)。

綜合來講,空間是[O(n)+O(lgN)] ~ [2 O(n)],這裏要取比較大的一個,也就是2O(n),也就是O(n)。

5.2) 時間複雜度

時間複雜度主要是考察增、刪、查三個操做所面臨的時間複雜度。 不管增長一個節點仍是刪除一個節點,首先都是查詢這個節點的位置。因此咱們首先介紹查詢一個節點的時間複雜度。

5.2.1)查詢一個節點的時間複雜度

仍是以上圖爲表明,若是要查詢其中的某一個節點,好比要查詢b1,須要比較的節點一次是b4->b2->b1, 因此查詢b1節點須要的時間是3。若是查詢b4呢,那就只須要和b4比較一次就能夠了。 因此查詢一個節點所須要的最大時間,是和樹的深度成正比的。那麼在上圖b樹上,時間複雜度就是O(lnN)。 那麼在a樹上查詢呢?查a1的話,只須要和a1比較一次就行了,可是若是要查a7呢,那就須要查詢7次了。 因此二叉查找樹的時間複雜度是O(lgN) ~ O(n),取最壞的狀況,那就是O(n)了。

5.2.2)增長一個節點的時間複雜度

增長一個節點,須要查詢到該節點須要插入的位置,因此花費時間應該是在查詢的基礎上在+1,因此是O(n)。

5.2.3)刪除一個節點的時間複雜度

二叉查找樹刪除節點能夠分爲三種狀況:

a)要刪除的目標節點是葉子節點。

此時只須要把這個節點刪除便可,由於此節點沒有子樹,直接刪除就能夠了。以下圖,刪除節點2。

delleaf

b)要刪除的目標節點有一個子樹。

i)若是隻有左子樹,就讓這個節點的父節點指向這個節點的左子樹。

ii)若是隻有右子樹,就讓這個節點的父節點指向這個節點的右子樹。以下圖,刪除節點3。

delone

c)要刪除的目標節點有兩個子樹。

i)方法一,找到要刪除的節點的前驅,這個節點的前驅確定是沒有右子樹的,用這個節點的前驅替換這個節點,並刪除這個節點。

ii)方法二,找到要刪除的節點的後繼,這個節點的後繼確定是沒有左子樹的,用這個節點的後繼替換這個節點,並刪除這個節點。

前驅和後繼的含義:

節點key的前驅,就是中序遍歷時,比key小的全部節點中最大的那個節點。

節點key的後繼,就是中序遍歷時,比key小的全部節點中最大的那個節點。

不管是用前驅進行替換,仍是用後繼進行替換,思路都是狀況c)轉換爲狀況a)或者狀況b)。

使用前驅進行替換:

deltwo

使用後繼進行替換:

deltwo_next

刪除操做說了,那麼時間複雜度呢

由於刪除一個節點的時候,首先須要進行查找,以後或者直接刪除這個節點, 或者使用前驅或者後繼替換後進行刪除,首先查找的時間複雜度是O(lgN), 直接刪除的時間複雜度是O(1)。 替換刪除呢,由於替換刪除的時候,查找前驅或者後繼的時候, 是在當前節點的基礎上進行查找的,因此查找前驅或後繼的時間加上查找要刪除的節點的時間, 一共是O(lgN)。最壞是O(N)。

因此刪除操做的時間複雜度在O(lgN)~O(N)之間。

平均來講會小於O(N),更接近O(lnN)一些。

刪除一個節點(採用前驅節點替換) Go語言描述以下:

//根據 key 值移除一個節點
func remove_node(tree * BNode, key int) (n, t *BNode){
    if nil == tree {
        return nil,nil
    }
    //找到 key 所在的節點,刪除它
    if key == tree.key {
        n, tree = del_node(tree)
    } else if key > tree.key {
        n, tree.rt = remove_node(tree.rt, key)
    } else {
        n, tree.lt = remove_node(tree.lt, key)
    }

    return n, tree
}

//刪除一個節點的操做
func del_node(tree * BNode) (n, t*BNode) {
    if nil == tree {
        return nil, nil
    }
    //直接刪除葉子節點
    if nil == tree.lt && nil == tree.rt {
        return tree, nil
    }
    //不是葉子節點,說明有子樹存在
    //沒有左子樹,說明只有右子樹,直接返回右子樹
    if nil == tree.lt {
        return tree, tree.rt
    }
    //只有左子樹存在,直接返回左子樹
    if nil == tree.rt {
        return tree, tree.lt
    }

    //左右子樹都存在,獲取前驅節點
    n, t = get_pre_node(tree.lt)

    n.lt = t
    n.rt = tree.rt

    return tree, n
}

//獲取前驅節點
func get_pre_node(node * BNode) (n, t *BNode) {
    if nil == node {
        return nil, nil
    }
    if nil != node.rt {
        n, node.rt = get_pre_node(node.rt)
        return n, node
    }

    //刪除找到的前驅節點,並刪除此節點後返回
    return del_node(node)
}

 

能夠調用remove_node(tree, key)函數刪除key對應的節點,而且返回刪除的節點。 


 

同步發表:http://www.fengbohello.top/blog/p/kqlo

相關文章
相關標籤/搜索