數據結構學習筆記(0X09)--二叉樹

知識體系樹狀圖:樹

1. 二叉樹相關定義

二叉樹:每個節點最多有兩個“叉”的樹,也就是兩個子節點,分別是左子節點和右子節點。

滿二叉樹:葉子節點全都在最底層,除了葉子節點之外,每個節點都有左右兩個子節點,這種二叉樹就叫作滿二叉樹。

完全二叉樹:葉子節點都在最底下兩層,最後一層的葉子節點都靠左排列,並且除了最後一層,其他層的節點個數都要達到最大,這種二叉樹叫作完全二叉樹。

2. 如何存儲一個二叉樹

鏈式存儲法:每個節點有三個字段,其中一個存儲數據,另外兩個是指向左右子節點的指針。我們只要拎住根節點,就可以通過左右子節點的指針,把整棵樹都串起來。這種存儲方式我們比較常用。大部分二叉樹代碼都是通過這種結構來實現的。

基於數組的順序存儲法:我們把根節點存儲在下標 i = 1 的位置,那左子節點存儲在下標 2 * i = 2 的位置,右子節點存儲在 2 * i + 1 = 3 的位置。以此類推,B 節點的左子節點存儲在 2 * i = 2 * 2 = 4 的位置,右子節點存儲在 2 * i + 1 = 2 * 2 + 1 = 5 的位置。

二叉樹既可以用鏈式存儲,也可以用數組順序存儲。數組順序存儲的方式比較適合完全二叉樹,其他類型的二叉樹用數組存儲會比較浪費存儲空間。 

3. 二叉樹遍歷

如何將所有節點都遍歷打印出來呢?經典的方法有三種,前序遍歷、中序遍歷和後序遍歷。其中,前、中、後序,表示的是節點與它的左右子樹節點遍歷打印的先後順序。

  • 前序遍歷是指,對於樹中的任意節點來說,先打印這個節點,然後再打印它的左子樹,最後打印它的右子樹。
  • 中序遍歷是指,對於樹中的任意節點來說,先打印它的左子樹,然後再打印它本身,最後打印它的右子樹。
  • 後序遍歷是指,對於樹中的任意節點來說,先打印它的左子樹,然後再打印它的右子樹,最後打印這個節點本身。

記憶技巧:前序遍歷,又叫先序遍歷,可以稱爲“先根”遍歷,同理中序遍歷和後序遍歷可以分別思考爲“中根”、“後根”遍歷。

前、中、後序遍歷操作,遍歷的時間複雜度是 O(n)。 

golang代碼實現(遞歸版本):

package main

import (
	"fmt"
)

type NodeTmp struct {
	     Data string
	     Left *NodeTmp
	     Right *NodeTmp
}

func frontTree(node *NodeTmp) {
	if node == nil{
		return
	}

	fmt.Printf("%v ", node.Data)
	frontTree(node.Left)
	frontTree(node.Right)

}

func midTree(node *NodeTmp) {
	if node == nil{
		return
	}

	midTree(node.Left)
	fmt.Printf("%v ", node.Data)
	midTree(node.Right)
}

func backTree(node *NodeTmp) {
	if node == nil{
		return
	}

	backTree(node.Left)
	backTree(node.Right)
	fmt.Printf("%v ", node.Data)
}


func main() {
	node := &NodeTmp{"A", nil, nil}
	//左
	node.Left = &NodeTmp{"B", nil, nil}
	node.Left.Left = &NodeTmp{"D", nil, nil}
	node.Left.Right = &NodeTmp{"E", nil, nil}
	//右
	node.Right = &NodeTmp{"C", nil, nil}
	node.Right.Right = &NodeTmp{"F", nil, nil}

	//先序遍歷
	fmt.Println("先序遍歷")
	frontTree(node)
	fmt.Println()

	//中序遍歷
	fmt.Println("中序遍歷")
	midTree(node)
	fmt.Println()

	//後序遍歷
	fmt.Println("後序遍歷")
	backTree(node)
	fmt.Println()
}

 

4. 二叉查找樹

二叉查找樹是二叉樹中最常用的一種類型,也叫二叉搜索樹。顧名思義,二叉查找樹是爲了實現快速查找而生的。不過,它不僅僅支持快速查找一個數據,還支持快速插入、刪除一個數據。

二叉查找樹定義:在樹中的任意一個節點,其左子樹中的每個節點的值,都要小於這個節點的值,而右子樹節點的值都大於這個節點的值。

二叉查找樹的插入查詢刪除操作代碼:

package main

type Node struct {
	Data  int
	Left  *Node
	Right *Node
}

//查找
func (node *Node) find(data int) *Node {
	tmp := node
	for tmp != nil {
		if data < tmp.Data {
			tmp = tmp.Left
		} else if data > tmp.Data {
			tmp = tmp.Right
		} else {
			return tmp
		}
	}

	return nil
}

//插入
func (node *Node) insert(data int) {
	if node == nil {
		node = &Node{data, nil, nil}
	}

	tmp := node
	for tmp != nil {
		if data > tmp.Data {
			if tmp.Right == nil {
				tmp.Right = &Node{data, nil, nil}
				return
			}
			tmp = tmp.Right
		} else {
			if data < tmp.Data {
				if tmp.Left == nil {
					tmp.Left = &Node{data, nil, nil}
					return
				}
			}
			tmp = tmp.Left
		}
	}
}

//刪除
func (node *Node) delete(data int) {

}

func main() {

}

重要特性:就是中序遍歷二叉查找樹,可以輸出有序的數據序列,時間複雜度是 O(n),非常高效。因此,二叉查找樹也叫作二叉排序樹。

與散列表對比:

散列表的插入、刪除、查找操作的時間複雜度可以做到常量級的 O(1),非常高效。而二叉查找樹在比較平衡的情況下,插入、刪除、查找操作時間複雜度纔是 O(logn),相對散列表,好像並沒有什麼優勢,那我們爲什麼還要用二叉查找樹呢?

原因大概如下:

  1. 散列表中的數據是無序存儲的,如果要輸出有序的數據,需要先進行排序。而對於二叉查找樹來說,我們只需要中序遍歷,就可以在 O(n) 的時間複雜度內,輸出有序的數據序列。
  2. 散列表擴容耗時很多,而且當遇到散列衝突時,性能不穩定,儘管二叉查找樹的性能不穩定,但是在工程中,我們最常用的平衡二叉查找樹的性能非常穩定,時間複雜度穩定在 O(logn)。
  3. 籠統地來說,儘管散列表的查找等操作的時間複雜度是常量級的,但因爲哈希衝突的存在,這個常量不一定比 logn 小,所以實際的查找速度可能不一定比 O(logn) 快。加上哈希函數的耗時,也不一定就比平衡二叉查找樹的效率高。
  4. 散列表的構造比二叉查找樹要複雜,需要考慮的東西很多。比如散列函數的設計、衝突解決辦法、擴容、縮容等。平衡二叉查找樹只需要考慮平衡性這一個問題,而且這個問題的解決方案比較成熟、固定。
  5. 爲了避免過多的散列衝突,散列表裝載因子不能太大,特別是基於開放尋址法解決衝突的散列表,不然會浪費一定的存儲空間。

綜合這幾點,平衡二叉查找樹在某些方面還是優於散列表的,所以,這兩者的存在並不衝突。我們在實際的開發過程中,需要結合具體的需求來選擇使用哪一個。

5. 平衡二叉查找樹

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章