深入理解資料結構--二叉樹(基礎篇)

oliver-l 發表於 2021-06-29
演算法 資料結構

最近開始學習go,公司也安排了一些對應的練習題,其中就包括二叉樹相關的練習題,剛好也順便撿起這方面的知識點

在計算機資料結構中,我們會經常接觸到樹這種典型的非線性資料結構,下面圖片展示的就是一個標準的樹結構

深入理解資料結構--二叉樹

在資料結構中,樹的定義如下。

樹(tree)是n(n>=0)個節點的有限集。當n=0時,稱為空樹。在任意一個非空樹中,有如下特點。

  1. 有且僅有一個特定的稱為根的節點
  2. 當n>1時,其餘節點可分為m(m>0)個互不相交的有限集,每一個集合本身又是一個樹,並稱為根的子樹

什麼是二叉樹

二叉樹(binary tree)是樹的一種特性形式。二叉,顧名思義,這種樹的每個節點最多有2個位元組點。注意,這裡是最多有2個,也可能只有1個,或者沒有孩子節點。

二叉樹還有二種特殊形式,一個叫作滿二叉樹,另一個叫作完全二叉樹

一個二叉樹的所有非葉子節點都存在左右節點,並且所有葉子節點都在同一層級上,那麼這個樹就是滿二叉樹。如下圖所示

深入理解資料結構--二叉樹

對一個有n個節點的二叉樹,按層級順序編號,則所有節點的編號為1到n。如果這個樹所有節點和同樣深度的滿二叉樹的編號為1到n的節點位置相同,則這個二叉樹為完全二叉樹。如下圖所示

深入理解資料結構--二叉樹

二叉樹的遍歷

介紹完二叉樹的概念,開始進入實踐,二叉樹是典型的非線性資料結構,遍歷時需要把非線性關聯的節點轉化成一個線性的序列,以不同的方式來遍歷,遍歷出的序列順序也不同。

從節點之間位置關係的角度來看,二叉樹的遍歷分為4種。
1.前序遍歷
2.中順遍歷
3.後序遍歷
4.層序遍歷

從更巨集觀的角度來看,二叉樹的遍歷歸結為兩大類
1.深度優先遍歷(前序遍歷,中序遍歷,後序遍歷)
2.廣度優先遍歷(層序遍歷)

深度優先遍歷

1.前序遍歷

二叉樹的前序遍歷,輸出順序是根節點、左子樹、右子樹。如下圖所示

深入理解資料結構--二叉樹

程式碼實現如下


package main

import (
   "fmt"
)
//定義二叉樹結構體
type binaryTree struct {
   value int
  leftNode *binaryTree
  rightNode *binaryTree
}

func main(){
   //0代表節點為空
  num := []int{1,2,3,4,5,0,6}
  //將陣列切片轉化為二叉樹結構體
  tree := createBinaryTree(0,num)
   //二叉樹資料
  var front []int
  front = frontForeach(*tree,front)
   fmt.Println(front)
}

//建立二叉樹
func createBinaryTree(i int,nums []int) *binaryTree {
   tree := &binaryTree{nums[i], nil, nil}
   //左節點的陣列下標為1,3,5...2*i+1
  if i<len(nums) && 2*i+1 < len(nums) {
      tree.leftNode = createBinaryTree(2*i+1, nums)
   }
   //右節點的陣列下標為2,4,6...2*i+2
  if i<len(nums) && 2*i+2 < len(nums) {
      tree.rightNode = createBinaryTree(2*i+2, nums)
   }
   return tree
}

//前序遍歷
func frontForeach(tree binaryTree,num []int) []int{
   //先遍歷根節點
  if tree.value != 0 {
      num = append(num,tree.value)
   }
   var leftNum,rightNum []int
  //若存在左節點,遍歷左節點樹
  if tree.leftNode != nil {
      leftNum = frontForeach(*tree.leftNode,leftNum)
      for _,value := range leftNum{
         num = append(num,value)
      }
   }
   //若存在右節點,遍歷右節點樹
  if tree.rightNode != nil {
      rightNum = frontForeach(*tree.rightNode,rightNum)
      for _,value := range rightNum{
         num = append(num,value)
      }
   }

   return num
}

2.中序遍歷

二叉樹的中序遍歷,輸出順序是左子樹、根節點、右子樹。如下圖所示

深入理解資料結構--二叉樹

package main

import (
    "fmt"
)
//定義二叉樹結構體
type binaryTree struct {
    value int
    leftNode *binaryTree
    rightNode *binaryTree
}

func main(){
    //0代表節點為空
    num := []int{1,2,3,4,5,0,6}
    tree := createBinaryTree(0,num)
    //二叉樹資料
    var middle []int
    middle = middleForeach(*tree,middle)
    fmt.Println(middle)
}

//建立二叉樹
func createBinaryTree(i int,nums []int) *binaryTree  {
    tree := &binaryTree{nums[i], nil, nil}
    //左節點的陣列下標為1,3,5...2*i+1
    if i<len(nums) && 2*i+1 < len(nums) {
        tree.leftNode = createBinaryTree(2*i+1, nums)
    }
    //右節點的陣列下標為2,4,6...2*i+2
    if i<len(nums) && 2*i+2 < len(nums) {
        tree.rightNode = createBinaryTree(2*i+2, nums)
    }
    return tree
}


//中序遍歷
func middleForeach(tree binaryTree,num []int) []int{
    var leftNum,rightNum []int
    //若存在左節點,遍歷左節點樹
    if tree.leftNode != nil {
        leftNum = middleForeach(*tree.leftNode,leftNum)
        for _,value := range leftNum{
            num = append(num,value)
        }
    }

    //先遍歷根節點
    if tree.value != 0 {
        num = append(num,tree.value)
    }

    //若存在右節點,遍歷右節點樹
    if tree.rightNode != nil {
        rightNum = middleForeach(*tree.rightNode,rightNum)
        for _,value := range rightNum{
            num = append(num,value)
        }
    }

    return num
}

3.後序遍歷

二叉樹的後序遍歷,輸出順序是左子樹、右子樹、根節點。如下圖所示

深入理解資料結構--二叉樹

package main

import (
    "fmt"
)
//定義二叉樹結構體
type binaryTree struct {
    value int
    leftNode *binaryTree
    rightNode *binaryTree
}

func main(){
    //0代表節點為空
    num := []int{1,2,3,4,5,0,6}
    tree := createBinaryTree(0,num)
    //二叉樹資料
    var behind []int
    behind = behindForeach(*tree,behind)
    fmt.Println(behind)
}

//建立二叉樹
func createBinaryTree(i int,nums []int) *binaryTree  {
    tree := &binaryTree{nums[i], nil, nil}
    //左節點的陣列下標為1,3,5...2*i+1
    if i<len(nums) && 2*i+1 < len(nums) {
        tree.leftNode = createBinaryTree(2*i+1, nums)
    }
    //右節點的陣列下標為2,4,6...2*i+2
    if i<len(nums) && 2*i+2 < len(nums) {
        tree.rightNode = createBinaryTree(2*i+2, nums)
    }
    return tree
}


//後序遍歷
func behindForeach(tree binaryTree,num []int) []int{
    var leftNum,rightNum []int
    //若存在左節點,遍歷左節點樹
    if tree.leftNode != nil {
        leftNum = behindForeach(*tree.leftNode,leftNum)
        for _,value := range leftNum{
            num = append(num,value)
        }
    }

    //若存在右節點,遍歷右節點樹
    if tree.rightNode != nil {
        rightNum = behindForeach(*tree.rightNode,rightNum)
        for _,value := range rightNum{
            num = append(num,value)
        }
    }
    //先遍歷根節點
    if tree.value != 0 {
        num = append(num,tree.value)
    }

    return num
}

以上三種遍歷方式相對比較簡單,只需要的遍歷過程中調整遍歷順序即可完成,下面我們看看另一種方式的遍歷

廣度優先遍歷

層序遍歷

顧名思義,就是二叉樹按照從根節點到葉子節點的層次關係,一層一層橫向遍歷各個節點,如下所示

深入理解資料結構--二叉樹

程式碼實現上,與深度優先遍歷有所區別,沒有使用遞迴的方式去實現,而是使用了佇列的方式,遍歷過程中,通過新增佇列,讀取隊頭的方式,完成層序遍歷

package main

import (
    "fmt"
)
//定義二叉樹結構體
type binaryTree struct {
    value int
    leftNode *binaryTree
    rightNode *binaryTree
}

func main(){
    //0代表節點為空
    num := []int{1,2,3,4,5,0,6}
    tree := createBinaryTree(0,num)
    //二叉樹資料
    var row []int
    row = rowForeach(*tree,row)
    fmt.Println(row)
}

//建立二叉樹
func createBinaryTree(i int,nums []int) *binaryTree  {
    tree := &binaryTree{nums[i], nil, nil}
    //左節點的陣列下標為1,3,5...2*i+1
    if i<len(nums) && 2*i+1 < len(nums) {
        tree.leftNode = createBinaryTree(2*i+1, nums)
    }
    //右節點的陣列下標為2,4,6...2*i+2
    if i<len(nums) && 2*i+2 < len(nums) {
        tree.rightNode = createBinaryTree(2*i+2, nums)
    }
    return tree
}


//層序遍歷
func rowForeach(tree binaryTree,num []int) []int  {
    //定義佇列
    var queue []binaryTree
    queue = append(queue,tree)
    for len(queue) > 0{
        var first binaryTree
        first = queue[0]
        if first.value != 0 {
            //取出隊頭值
            num = append(num,first.value)
        }
        //將隊頭刪除
        queue = queue[1:]
        //左節點存在則加入隊尾
        if first.leftNode != nil {
            queue = append(queue,*first.leftNode)
        }
        //右節點存在則加入隊尾
        if first.rightNode != nil {
            queue = append(queue,*first.rightNode)
        }
    }

    return num
}

二叉堆

二叉堆本質上是一種完全二叉樹,它分為兩個型別

1.最大堆:大頂堆任何一個父節點,都大於或等於它左、右孩子節點的值,如下圖所示

深入理解資料結構--二叉樹

2.最小堆:小頂堆的任何一個父節點,都小於或等於它左、右孩子節點的值,如下圖所示

深入理解資料結構--二叉樹

二叉堆的根節點叫作堆頂。最大堆和最小堆的特點決定了:最大堆的堆頂是整個堆中的最大元素;最小堆的堆頂事整個堆中的最小元素。

對於二叉堆,有如下幾種操作
1.插入節點
2.刪除節點
3.構建二叉樹

在程式碼實現上,由於堆是一個完全二叉樹,所以在儲存上可以使用陣列實現,假設父節點的下標為parent,那麼它的左孩子下標就是2 * parent+1,右孩子下標就是2 * parent+2

深入理解資料結構--二叉樹

構建最大堆程式碼如下:實現思路大致先找到最近的非葉子結點,假設有n個結點,則最近的非葉子結點為n/2,對每個結點進行判斷,判斷結點是否大於根節點,若不大於則節點下沉,這樣一步步的迴圈判斷,就構建了一個最大堆

package main

import "fmt"

func main(){

    var numArr = []int{2,8,23,4,5,77,65,43}
    buildMaxHeap(numArr,len(numArr))
    fmt.Print(numArr)
}

//構建最大堆
func buildMaxHeap(arr []int,arrLen int){
    for i := arrLen/2;i >= 0; i-- {
        sink(arr,i,arrLen)
    }
}

//樹節點值下沉判斷
func sink(arr []int,i,arrLen int)  {
    //左節點下標
    left := i*2+1
    //右節點下標
    right := i*2+2
    largest := i
    //若左節點大於父節點,則交換值
    if left < arrLen && arr[left] > arr[largest] {
        largest = left
    }
    if right < arrLen && arr[right] > arr[largest] {
        largest = right
    }
    //存在節點值大於父節點值,需要交換資料
    if largest != i {
        //交換值
        swap(arr,i,largest)
        //交換完成後,繼續判斷交換的節點值是否需要繼續下沉
        sink(arr,largest,arrLen)
    }
}

//交換位置
func swap(arr []int,i,j int)  {
    arr[i],arr[j] = arr[j],arr[i]
}

優先佇列

佇列的特點是先進先出(FIFO)

入佇列,將新元素置於隊尾;出佇列,隊頭元素最先被移出;

而優先佇列不再遵循先入先出的原則,而是分為兩種情況。

  • 最大優先佇列,無論入隊順序如何,都是當前最大的元素優先出列
  • 最小優先佇列,無論入隊順序如何,都是當前最小的元素優先出列

在上面實現了構建最大堆的程式碼後,還有兩個基本操作,插入節點和刪除節點,插入節點必須先往陣列最後插入,並進行判斷插入的值是否需要上浮到父節點,刪除節點只允許刪除根節點的值,刪除後再將陣列最後的值放置到根節點,再進行調整,完成節點的值下沉。當完成上述操作後,就實現了優先佇列

實現程式碼如下:

package main

import "fmt"

func main(){

    var numArr = []int{2,8,23,4,5,77,65,43}

    arrLen := len(numArr)
    buildMaxHeapQueue(numArr,arrLen)
    var out int
    numArr,out = outQueue(numArr)
    fmt.Print(numArr,out)
    fmt.Print("\n")
    numArr = inQueue(numArr,55)
    fmt.Print(numArr)
    fmt.Print("\n")
    numArr,out = outQueue(numArr)
    fmt.Print(numArr,out)
    fmt.Print("\n")

}

//入佇列
func inQueue(arr []int,num int) []int  {
    arr = append(arr,num)
    floatingNode(arr,len(arr)-1)
    return arr
}

//出佇列
func outQueue(arr []int) ([]int,int){
    first := arr[0]
    arrLen := len(arr)
    if arrLen == 0 {
        panic("nothing")
    }
    if arrLen == 1 {
        var emptyArr []int
        return emptyArr,first
    }
    //交換最後一個節點和根節點值
    swapNode(arr,0,arrLen-1)
    //將切片最後的值刪除
    arr = arr[0:arrLen-1]
    //節點下沉
    sinkNode(arr,0,len(arr))
    return arr,first
}

//構建大頂堆
func buildMaxHeapQueue(arr []int,arrLen int){
    for i := arrLen/2;i >= 0; i-- {
        sinkNode(arr,i,arrLen)
    }
}

//節點值上浮判斷
func floatingNode(arr []int,i int){
    var root int
    if i%2 == 0 {
        root = i/2-1
    }else {
        root = i/2
    }
    //判斷父節點是否小於子節點
    if arr[root] < arr[i] {
        swapNode(arr,root,i)
        if i > 1 {
            floatingNode(arr,root)
        }
    }
}

//樹節點值下沉判斷
func sinkNode(arr []int,i int,arrLen int)  {
    //左節點下標
    left := i*2+1
    //右節點下標
    right := i*2+2
    largest := i
    //若左節點大於父節點,則交換值
    if left < arrLen && arr[left] > arr[largest] {
        largest = left
    }
    if right < arrLen && arr[right] > arr[largest] {
        largest = right
    }
    //存在節點值大於父節點值,需要交換資料
    if largest != i {
        //交換值
        swapNode(arr,i,largest)
        //交換完成後,繼續判斷交換的節點值是否需要繼續下沉
        sinkNode(arr,largest,arrLen)
    }
}

//交換位置
func swapNode(arr []int,i,j int)  {
    arr[i],arr[j] = arr[j],arr[i]
}

堆排序

堆排序(Heapsort)是指利用堆這種資料結構所設計的一種排序演算法。

其實現思想在於先構建一個最大堆,再重新遍歷,每次遍歷將根節點與最後一個元素值交換,然後隔離最後一個元素,因為此時最後一個元素為最大的元素,然後根節點的值進行下沉操作,再次形成最大堆,再重複進行操作,就完成了排序。


package main

import "fmt"

func main(){

   var numArr = []int{2,8,23,4,5,77,65,43}

   sort := heapSort(numArr)
   fmt.Print(sort)
}
//堆排序
func heapSort(arr []int) []int{

   arrLen := len(arr)
   //構建大頂堆
  buildMaxHeap(arr,arrLen)
   for i := arrLen-1; i >= 0; i-- {
      swap(arr,0,i)
      arrLen -= 1
      sink(arr,0,arrLen)
   }
   return arr
}

//構建大頂堆
func buildMaxHeap(arr []int,arrLen int){
   for i := arrLen/2;i >= 0; i-- {
      sink(arr,i,arrLen)
   }
}

//樹節點值下沉判斷
func sink(arr []int,i,arrLen int)  {
   //左節點下標
  left := i*2+1
  //右節點下標
  right := i*2+2
  largest := i
   //若左節點大於父節點,則交換值
  if left < arrLen && arr[left] > arr[largest] {
      largest = left
   }
   if right < arrLen && arr[right] > arr[largest] {
      largest = right
   }
   //存在節點值大於父節點值,需要交換資料
  if largest != i {
      //交換值
  swap(arr,i,largest)
      //交換完成後,繼續判斷交換的節點值是否需要繼續下沉
  sink(arr,largest,arrLen)
   }
}

//交換位置
func swap(arr []int,i,j int)  {
   arr[i],arr[j] = arr[j],arr[i]
}
本作品採用《CC 協議》,轉載必須註明作者和本文連結