本文使用Go語言進行描述算法
有以下數列,建立一顆二叉查找樹數組
{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
二叉樹創建好了,可是是存在於內存中,怎樣才能知道建立的沒問題呢?
咱們知道,對於一棵二叉樹,其(中根遍歷 + 先根遍歷),或者(中根遍歷 + 後根遍歷) 能夠逆向推導出二叉樹的結構。 因此接下來,咱們要對二叉樹進行一次中根遍歷和一次先根遍歷,並經過這兩組數據驗證下二叉樹結構。
先根遍歷的代碼以下:
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
咱們能夠根據上面的結果動手在紙上畫一下,看看有沒有建立成功。呵呵,開個玩笑。後面會講如何重建二叉樹。
除了動手畫出來,咱們還能夠藉助一些工具把它畫出來,好比 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 圖片,以下所示:
下面根據咱們獲得的(中根遍歷)和(先根遍歷)來重建二叉樹,兩組數據以下:
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
看樣子結果相同 ~.~
接下來分析下二叉查找樹的空間複雜度和時間複雜度。
空間複雜度比較好分析。咱們在建樹的時候,是否是須要對每個數據申請一次內存呢。 每一個數據一次,那就是有多少數據,就要申請多少次,有n個數據就要 申請n次, 因此空間光是申請用於存放數據的內存次數就是n,這個和數據的規模是正相關的, 而且關係是O(x * n),其中x是每一個數據佔用的內存數量。由於這個x在數據結構不變的狀況下是不變的, 是不會隨着數據規模而變化的,那就能夠忽略,由於x是個常數,與n無關。 因此只是申請存放數據的空間的空間複雜度爲O(n)。
那還有什麼地方須要空間呢?就是遞歸的時候,須要棧空間。 樹每深一層,就須要遞歸一次,也就須要保存一次棧空間。 在平均狀況下,樹的深度是lgN。可是在極端狀況下,樹的深度但是N啊。請看下面的圖。
a樹就是最差的樹,這哪兒還像是一棵樹啊,基本就是鏈表了;而b樹就是一棵好樹,深度最優。
因此最壞的遞歸建樹棧空間也是O(n),不過最好的是O(lgN)。
綜合來講,空間是[O(n)+O(lgN)] ~ [2 O(n)],這裏要取比較大的一個,也就是2O(n),也就是O(n)。
時間複雜度主要是考察增、刪、查三個操做所面臨的時間複雜度。 不管增長一個節點仍是刪除一個節點,首先都是查詢這個節點的位置。因此咱們首先介紹查詢一個節點的時間複雜度。
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。
b)要刪除的目標節點有一個子樹。
i)若是隻有左子樹,就讓這個節點的父節點指向這個節點的左子樹。
ii)若是隻有右子樹,就讓這個節點的父節點指向這個節點的右子樹。以下圖,刪除節點3。
c)要刪除的目標節點有兩個子樹。
i)方法一,找到要刪除的節點的前驅,這個節點的前驅確定是沒有右子樹的,用這個節點的前驅替換這個節點,並刪除這個節點。
ii)方法二,找到要刪除的節點的後繼,這個節點的後繼確定是沒有左子樹的,用這個節點的後繼替換這個節點,並刪除這個節點。
前驅和後繼的含義:
節點key的前驅,就是中序遍歷時,比key小的全部節點中最大的那個節點。
節點key的後繼,就是中序遍歷時,比key小的全部節點中最大的那個節點。
不管是用前驅進行替換,仍是用後繼進行替換,思路都是狀況c)轉換爲狀況a)或者狀況b)。
使用前驅進行替換:
使用後繼進行替換:
刪除操做說了,那麼時間複雜度呢
由於刪除一個節點的時候,首先須要進行查找,以後或者直接刪除這個節點, 或者使用前驅或者後繼替換後進行刪除,首先查找的時間複雜度是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對應的節點,而且返回刪除的節點。