數據結構和算法(Golang實現)(27)查找算法-二叉查找樹

二叉查找樹

二叉查找樹,又叫二叉排序樹,二叉搜索樹,是一種有特定規則的二叉樹,定義以下:node

  1. 它是一顆二叉樹,或者是空樹。
  2. 左子樹全部節點的值都小於它的根節點,右子樹全部節點的值都大於它的根節點。
  3. 左右子樹也是一顆二叉查找樹。

二叉查找樹的特色是,一直往左兒子往下找左兒子,能夠找到最小的元素,一直往右兒子找右兒子,能夠找到最大的元素。算法

看起來,咱們能夠用它來實現元素排序,但是咱們卻使用了二叉堆來實現了堆排序,由於二叉查找樹不保證是一個平衡的二叉樹,最壞狀況下二叉查找樹會退化成一個鏈表,也就是全部節點都沒有左子樹或者沒有右子樹,樹的層次太深致使排序性能太差。segmentfault

使用二分查找,能夠很快在一顆二叉查找樹中找到咱們須要的值。數組

咱們來分析二叉查找樹添加,刪除,查找元素的方法。數據結構

1、添加元素

如下是一個二叉查找樹的表示:併發

// 二叉查找樹
type BinarySearchTree struct {
    Root *BinarySearchTreeNode // 樹根節點
}

// 二叉查找樹節點
type BinarySearchTreeNode struct {
    Value int64                 // 值
    Times int64                 // 值出現的次數
    Left  *BinarySearchTreeNode // 左子樹
    Right *BinarySearchTreeNode // 右字樹
}

// 初始化一個二叉查找樹
func NewBinarySearchTree() *BinarySearchTree {
    return new(BinarySearchTree)
}

一個節點表明一個元素,節點的Value值是用來進行二叉查找的關鍵,當Value值重複時,咱們將值出現的次數Times加 1。添加元素代碼以下:數據結構和算法

// 添加元素
func (tree *BinarySearchTree) Add(value int64) {
    // 若是沒有樹根,證實是顆空樹,添加樹根後返回
    if tree.Root == nil {
        tree.Root = &BinarySearchTreeNode{Value: value}
        return
    }

    // 將值添加進去
    tree.Root.Add(value)
}

func (node *BinarySearchTreeNode) Add(value int64) {
    if value < node.Value {
        // 若是插入的值比節點的值小,那麼要插入到該節點的左子樹中
        // 若是左子樹爲空,直接添加
        if node.Left == nil {
            node.Left = &BinarySearchTreeNode{Value: value}
        } else {
            // 不然遞歸
            node.Left.Add(value)
        }
    } else if value > node.Value {
        // 若是插入的值比節點的值大,那麼要插入到該節點的右子樹中
        // 若是右子樹爲空,直接添加
        if node.Right == nil {
            node.Right = &BinarySearchTreeNode{Value: value}
        } else {
            // 不然遞歸
            node.Right.Add(value)
        }
    } else {
        // 值相同,不須要添加,值出現的次數加1便可
        node.Times = node.Times + 1
    }
}

若是添加元素時是顆空樹,那麼初始化根節點。函數

而後添加的值和根節點比較,判斷是要插入到根節點左子樹仍是右子樹,仍是不用插入。性能

當值比根節點小時,元素要插入到根節點的左子樹中,當值比根節點大時,元素要插入到根節點的右子樹中,相等時不插入,只更新次數。學習

而後再分別對根節點的左子樹和右子樹進行遞歸操做便可。

2、查找最大值或最小值的元素

查找最大值和最小值比較簡單,一直往左兒子往下找左兒子,能夠找到最小的元素,一直往右兒子找右兒子,能夠找到最大的元素。

// 找出最小值的節點
func (tree *BinarySearchTree) FindMinValue() *BinarySearchTreeNode {
    if tree.Root == nil {
        // 若是是空樹,返回空
        return nil
    }

    return tree.Root.FindMinValue()
}

func (node *BinarySearchTreeNode) FindMinValue() *BinarySearchTreeNode {
    // 左子樹爲空,表面已是最左的節點了,該值就是最小值
    if node.Left == nil {
        return node
    }

    // 一直左子樹遞歸
    return node.Left.FindMinValue()
}

// 找出最大值的節點
func (tree *BinarySearchTree) FindMaxValue() *BinarySearchTreeNode {
    if tree.Root == nil {
        // 若是是空樹,返回空
        return nil
    }

    return tree.Root.FindMaxValue()
}

func (node *BinarySearchTreeNode) FindMaxValue() *BinarySearchTreeNode {
    // 右子樹爲空,表面已是最右的節點了,該值就是最大值
    if node.Right == nil {
        return node
    }

    // 一直右子樹遞歸
    return node.Right.FindMaxValue()
}

3、查找指定元素

二分查找的技巧也在這裏有用武之地了:

// 查找節點
func (tree *BinarySearchTree) Find(value int64) *BinarySearchTreeNode {
    if tree.Root == nil {
        // 若是是空樹,返回空
        return nil
    }

    return tree.Root.Find(value)
}

func (node *BinarySearchTreeNode) Find(value int64) *BinarySearchTreeNode {
    if value == node.Value {
        // 若是該節點剛剛等於該值,那麼返回該節點
        return node
    } else if value < node.Value {
        // 若是查找的值小於節點值,從節點的左子樹開始找
        if node.Left == nil {
            // 左子樹爲空,表示找不到該值了,返回nil
            return nil
        }
        return node.Left.Find(value)
    } else {
        // 若是查找的值大於節點值,從節點的右子樹開始找
        if node.Right == nil {
            // 右子樹爲空,表示找不到該值了,返回nil
            return nil
        }
        return node.Right.Find(value)
    }
}

若是是空樹,返回 nil,不然與根節點比較。

若是剛恰好等於根節點的值,返回該節點,不然根據值的比較,繼續往左子樹或右字樹遞歸查找。

4、查找指定元素的父親

與查找指定元素同樣,只不過追蹤的是該元素的父親節點。

// 查找指定節點的父親
func (tree *BinarySearchTree) FindParent(value int64) *BinarySearchTreeNode {
    if tree.Root == nil {
        // 若是是空樹,返回空
        return nil
    }

    // 若是根節點等於該值,根節點其沒有父節點,返回nil
    if tree.Root.Value == value {
        return nil
    }
    return tree.Root.FindParent(value)
}

func (node *BinarySearchTreeNode) FindParent(value int64) *BinarySearchTreeNode {
    // 外層沒有值相等的斷定,由於在內層已經斷定完畢後返回父親節點。

    if value < node.Value {
        // 若是查找的值小於節點值,從節點的左子樹開始找
        leftTree := node.Left
        if leftTree == nil {
            // 左子樹爲空,表示找不到該值了,返回nil
            return nil
        }

        // 左子樹的根節點的值恰好等於該值,那麼父親就是如今的node,返回
        if leftTree.Value == value {
            return node
        } else {
            return leftTree.FindParent(value)
        }
    } else {
        // 若是查找的值大於節點值,從節點的右子樹開始找
        rightTree := node.Right
        if rightTree == nil {
            // 右子樹爲空,表示找不到該值了,返回nil
            return nil
        }

        // 右子樹的根節點的值恰好等於該值,那麼父親就是如今的node,返回
        if rightTree.Value == value {
            return node
        } else {
            return rightTree.FindParent(value)
        }
    }
}

代碼相應的進行了調整,方便獲取到父親節點。

若是返回的父親節點爲空,表示沒有父親。

5、刪除元素

刪除元素有四種狀況:

  1. 第一種狀況,刪除的是根節點,且根節點沒有兒子,直接刪除便可。
  2. 第二種狀況,刪除的節點有父親節點,但沒有子樹,也就是刪除的是葉子節點,直接刪除便可。
  3. 第三種狀況,刪除的節點下有兩個子樹,由於右子樹的值都比左子樹大,那麼用右子樹中的最小元素來替換刪除的節點,這時二叉查找樹的性質又知足了。右子樹的最小元素,只要一直往右子樹的左邊一直找一直找就能夠找到。
  4. 第四種狀況,刪除的節點只有一個子樹,那麼該子樹直接替換被刪除的節點便可。

代碼實現以下:

// 刪除指定的元素
func (tree *BinarySearchTree) Delete(value int64) {
    if tree.Root == nil {
        // 若是是空樹,直接返回
        return
    }

    // 查找該值是否存在
    node := tree.Root.Find(value)
    if node == nil {
        // 不存在該值,直接返回
        return
    }

    // 查找該值的父親節點
    parent := tree.Root.FindParent(value)

    // 第一種狀況,刪除的是根節點,且根節點沒有兒子
    if parent == nil && node.Left == nil && node.Right == nil {
        // 置空後直接返回
        tree.Root = nil
        return
    } else if node.Left == nil && node.Right == nil {
        // 第二種狀況,刪除的節點有父親節點,但沒有子樹

        // 若是刪除的是節點是父親的左兒子,直接將該值刪除便可
        if parent.Left != nil && value == parent.Left.Value {
            parent.Left = nil
        } else {
            // 刪除的原來是父親的右兒子,直接將該值刪除便可
            parent.Right = nil
        }
        return
    } else if node.Left != nil && node.Right != nil {
        // 第三種狀況,刪除的節點下有兩個子樹,由於右子樹的值都比左子樹大,那麼用右子樹中的最小元素來替換刪除的節點。
        // 右子樹的最小元素,只要一直往右子樹的左邊一直找一直找就能夠找到,替換後二叉查找樹的性質又知足了。

        // 找右子樹中最小的值,一直往右子樹的左邊找
        minNode := node.Right
        for minNode.Left != nil {
            minNode = minNode.Left
        }
        // 把最小的節點刪掉
        tree.Delete(minNode.Value)

        // 最小值的節點替換被刪除節點
        node.Value = minNode.Value
        node.Times = minNode.Times
    } else {
        // 第四種狀況,只有一個子樹,那麼該子樹直接替換被刪除的節點便可

        // 父親爲空,表示刪除的是根節點,替換樹根
        if parent == nil {
            if node.Left != nil {
                tree.Root = node.Left
            } else {
                tree.Root = node.Right
            }
            return
        }
        // 左子樹不爲空
        if node.Left != nil {
            // 若是刪除的是節點是父親的左兒子,讓刪除的節點的左子樹接班
            if parent.Left != nil && value == parent.Left.Value {
                parent.Left = node.Left
            } else {
                parent.Right = node.Left
            }
        } else {
            // 若是刪除的是節點是父親的左兒子,讓刪除的節點的右子樹接班
            if parent.Left != nil && value == parent.Left.Value {
                parent.Left = node.Right
            } else {
                parent.Right = node.Right
            }
        }
    }
}

首先查找到要刪除元素的節點:tree.Root.Find(value),而後找到該節點父親:tree.Root.FindParent(value),根據四種不一樣狀況對刪除節點進行補位。核心在於,第三種狀況下,刪除的節點有兩個子樹狀況下,須要用右子樹中最小的節點來替換被刪除節點。

上面的代碼能夠優化,能夠在查找刪除元素的節點時順道查出其父親節點,沒必要要分開查詢父親節點,在第三種狀況下查出右子樹的最小節點時能夠直接將其移除,沒必要遞歸使用tree.Delete(minNode.Value)

因爲這種通用形式的二叉查找樹實現甚少使用,大部分程序都使用了AVL樹或紅黑樹,以上優化理解便可。

6、中序遍歷(實現排序)

使用二叉查找樹能夠實現排序,只須要對樹進行中序遍歷便可。

咱們先打印出左子樹,而後打印根節點的值,再打印右子樹,這是一個遞歸的過程。

// 中序遍歷
func (tree *BinarySearchTree) MidOrder() {
    tree.Root.MidOrder()
}

func (node *BinarySearchTreeNode) MidOrder() {
    if node == nil {
        return
    }

    // 先打印左子樹
    node.Left.MidOrder()

    // 按照次數打印根節點
    for i := 0; i <= int(node.Times); i++ {
        fmt.Println(node.Value)
    }

    // 打印右子樹
    node.Right.MidOrder()
}

7、完整代碼

package main

import (
    "fmt"
)

// 二叉查找樹節點
type BinarySearchTree struct {
    Root *BinarySearchTreeNode // 樹根節點
}

// 二叉查找樹節點
type BinarySearchTreeNode struct {
    Value int64                 // 值
    Times int64                 // 值出現的次數
    Left  *BinarySearchTreeNode // 左子樹
    Right *BinarySearchTreeNode // 右字樹
}

// 初始化一個二叉查找樹
func NewBinarySearchTree() *BinarySearchTree {
    return new(BinarySearchTree)
}

// 添加元素
func (tree *BinarySearchTree) Add(value int64) {
    // 若是沒有樹根,證實是顆空樹,添加樹根後返回
    if tree.Root == nil {
        tree.Root = &BinarySearchTreeNode{Value: value}
        return
    }

    // 將值添加進去
    tree.Root.Add(value)
}

func (node *BinarySearchTreeNode) Add(value int64) {
    if value < node.Value {
        // 若是插入的值比節點的值小,那麼要插入到該節點的左子樹中
        // 若是左子樹爲空,直接添加
        if node.Left == nil {
            node.Left = &BinarySearchTreeNode{Value: value}
        } else {
            // 不然遞歸
            node.Left.Add(value)
        }
    } else if value > node.Value {
        // 若是插入的值比節點的值大,那麼要插入到該節點的右子樹中
        // 若是右子樹爲空,直接添加
        if node.Right == nil {
            node.Right = &BinarySearchTreeNode{Value: value}
        } else {
            // 不然遞歸
            node.Right.Add(value)
        }
    } else {
        // 值相同,不須要添加,值出現的次數加1便可
        node.Times = node.Times + 1
    }
}

// 找出最小值的節點
func (tree *BinarySearchTree) FindMinValue() *BinarySearchTreeNode {
    if tree.Root == nil {
        // 若是是空樹,返回空
        return nil
    }

    return tree.Root.FindMinValue()
}

func (node *BinarySearchTreeNode) FindMinValue() *BinarySearchTreeNode {
    // 左子樹爲空,表面已是最左的節點了,該值就是最小值
    if node.Left == nil {
        return node
    }

    // 一直左子樹遞歸
    return node.Left.FindMinValue()
}

// 找出最大值的節點
func (tree *BinarySearchTree) FindMaxValue() *BinarySearchTreeNode {
    if tree.Root == nil {
        // 若是是空樹,返回空
        return nil
    }

    return tree.Root.FindMaxValue()
}

func (node *BinarySearchTreeNode) FindMaxValue() *BinarySearchTreeNode {
    // 右子樹爲空,表面已是最右的節點了,該值就是最大值
    if node.Right == nil {
        return node
    }

    // 一直右子樹遞歸
    return node.Right.FindMaxValue()
}

// 查找指定節點
func (tree *BinarySearchTree) Find(value int64) *BinarySearchTreeNode {
    if tree.Root == nil {
        // 若是是空樹,返回空
        return nil
    }

    return tree.Root.Find(value)
}

func (node *BinarySearchTreeNode) Find(value int64) *BinarySearchTreeNode {
    if value == node.Value {
        // 若是該節點剛剛等於該值,那麼返回該節點
        return node
    } else if value < node.Value {
        // 若是查找的值小於節點值,從節點的左子樹開始找
        if node.Left == nil {
            // 左子樹爲空,表示找不到該值了,返回nil
            return nil
        }
        return node.Left.Find(value)
    } else {
        // 若是查找的值大於節點值,從節點的右子樹開始找
        if node.Right == nil {
            // 右子樹爲空,表示找不到該值了,返回nil
            return nil
        }
        return node.Right.Find(value)
    }
}

// 查找指定節點的父親
func (tree *BinarySearchTree) FindParent(value int64) *BinarySearchTreeNode {
    if tree.Root == nil {
        // 若是是空樹,返回空
        return nil
    }

    // 若是根節點等於該值,根節點其沒有父節點,返回nil
    if tree.Root.Value == value {
        return nil
    }
    return tree.Root.FindParent(value)
}

func (node *BinarySearchTreeNode) FindParent(value int64) *BinarySearchTreeNode {
    // 外層沒有值相等的斷定,由於在內層已經斷定完畢後返回父親節點。

    if value < node.Value {
        // 若是查找的值小於節點值,從節點的左子樹開始找
        leftTree := node.Left
        if leftTree == nil {
            // 左子樹爲空,表示找不到該值了,返回nil
            return nil
        }

        // 左子樹的根節點的值恰好等於該值,那麼父親就是如今的node,返回
        if leftTree.Value == value {
            return node
        } else {
            return leftTree.FindParent(value)
        }
    } else {
        // 若是查找的值大於節點值,從節點的右子樹開始找
        rightTree := node.Right
        if rightTree == nil {
            // 右子樹爲空,表示找不到該值了,返回nil
            return nil
        }

        // 右子樹的根節點的值恰好等於該值,那麼父親就是如今的node,返回
        if rightTree.Value == value {
            return node
        } else {
            return rightTree.FindParent(value)
        }
    }
}

// 刪除指定的元素
func (tree *BinarySearchTree) Delete(value int64) {
    if tree.Root == nil {
        // 若是是空樹,直接返回
        return
    }

    // 查找該值是否存在
    node := tree.Root.Find(value)
    if node == nil {
        // 不存在該值,直接返回
        return
    }

    // 查找該值的父親節點
    parent := tree.Root.FindParent(value)

    // 第一種狀況,刪除的是根節點,且根節點沒有兒子
    if parent == nil && node.Left == nil && node.Right == nil {
        // 置空後直接返回
        tree.Root = nil
        return
    } else if node.Left == nil && node.Right == nil {
        //  第二種狀況,刪除的節點有父親節點,但沒有子樹

        // 若是刪除的是節點是父親的左兒子,直接將該值刪除便可
        if parent.Left != nil && value == parent.Left.Value {
            parent.Left = nil
        } else {
            // 刪除的原來是父親的右兒子,直接將該值刪除便可
            parent.Right = nil
        }
        return
    } else if node.Left != nil && node.Right != nil {
        // 第三種狀況,刪除的節點下有兩個子樹,由於右子樹的值都比左子樹大,那麼用右子樹中的最小元素來替換刪除的節點,這時二叉查找樹的性質又知足了。

        // 找右子樹中最小的值,一直往右子樹的左邊找
        minNode := node.Right
        for minNode.Left != nil {
            minNode = minNode.Left
        }
        // 把最小的節點刪掉
        tree.Delete(minNode.Value)

        // 最小值的節點替換被刪除節點
        node.Value = minNode.Value
        node.Times = minNode.Times
    } else {
        // 第四種狀況,只有一個子樹,那麼該子樹直接替換被刪除的節點便可

        // 父親爲空,表示刪除的是根節點,替換樹根
        if parent == nil {
            if node.Left != nil {
                tree.Root = node.Left
            } else {
                tree.Root = node.Right
            }
            return
        }
        // 左子樹不爲空
        if node.Left != nil {
            // 若是刪除的是節點是父親的左兒子,讓刪除的節點的左子樹接班
            if parent.Left != nil && value == parent.Left.Value {
                parent.Left = node.Left
            } else {
                parent.Right = node.Left
            }
        } else {
            // 若是刪除的是節點是父親的左兒子,讓刪除的節點的右子樹接班
            if parent.Left != nil && value == parent.Left.Value {
                parent.Left = node.Right
            } else {
                parent.Right = node.Right
            }
        }
    }
}

// 中序遍歷
func (tree *BinarySearchTree) MidOrder() {
    tree.Root.MidOrder()
}

func (node *BinarySearchTreeNode) MidOrder() {
    if node == nil {
        return
    }

    // 先打印左子樹
    node.Left.MidOrder()

    // 按照次數打印根節點
    for i := 0; i <= int(node.Times); i++ {
        fmt.Println(node.Value)
    }

    // 打印右子樹
    node.Right.MidOrder()
}

func main() {
    values := []int64{3, 6, 8, 20, 9, 2, 6, 8, 9, 3, 5, 40, 7, 9, 13, 6, 8}

    // 初始化二叉查找樹並添加元素
    tree := NewBinarySearchTree()
    for _, v := range values {
        tree.Add(v)
    }

    // 找到最大值或最小值的節點
    fmt.Println("find min value:", tree.FindMinValue())
    fmt.Println("find max value:", tree.FindMaxValue())

    // 查找不存在的99
    node := tree.Find(99)
    if node != nil {
        fmt.Println("find it 99!")
    } else {
        fmt.Println("not find it 99!")
    }

    // 查找存在的9
    node = tree.Find(9)
    if node != nil {
        fmt.Println("find it 9!")
    } else {
        fmt.Println("not find it 9!")
    }

    // 刪除存在的9後,再查找9
    tree.Delete(9)
    node = tree.Find(9)
    if node != nil {
        fmt.Println("find it 9!")
    } else {
        fmt.Println("not find it 9!")
    }

    // 中序遍歷,實現排序
    tree.MidOrder()
}

運行程序後,結果:

find min value: &{2 0 <nil> <nil>}
find max value: &{40 0 <nil> <nil>}
not find it 99!
find it 9!
not find it 9!
2
3
3
5
6
6
6
7
8
8
8
13
20
40

8、總結

二叉查找樹可能退化爲鏈表,也多是一顆很是平衡的二叉樹,查找,添加,刪除元素的時間複雜度取決於樹的高度h

  1. 當二叉樹是滿的時,樹的高度是最小的,此時樹節點數量n和高度h的關係爲:h = log(n)
  2. 當二叉樹是一個鏈表時,此時樹節點數量n和高度h的關係爲:h = n

二叉查找樹的效率來源其二分查找的特徵,時間複雜度在於二叉樹的高度,所以查找,添加和刪除的時間複雜度範圍爲log(n)~n

爲了提升二叉查找樹查找的速度,樹的高度要儘量的小。AVL樹和紅黑樹都是相對平衡的二叉查找樹,由於特殊的旋轉平衡操做,樹的高度被大大壓低。它們查找效率較高,添加,刪除,查找操做的平均時間複雜度都爲log(n),常常在各類程序中被使用。

二叉查找樹是後面要學習的高級數據結構AVL樹,紅黑樹的基礎。

系列文章入口

我是陳星星,歡迎閱讀我親自寫的 數據結構和算法(Golang實現),文章首發於 閱讀更友好的GitBook

相關文章
相關標籤/搜索