In depth understanding of data structure -- binary tree (basic chapter)

Posted by kid85 on Sun, 23 Jan 2022 19:30:36 +0100

Recently, I started to learn go. The company also arranged some corresponding exercises, including those related to binary tree. I happened to pick up some knowledge points in this regard

In the computer data structure, we often come into contact with the typical nonlinear data structure of tree. The following picture shows a standard tree structure

In the data structure, the tree is defined as follows.

A tree is a finite set of n (n > = 0) nodes. When n=0, it is called an empty tree. In any non empty tree, it has the following characteristics.

  1. There is and only one specific node called root
  2. When n > 1, the other nodes can be divided into m (M > 0) disjoint finite sets. Each set itself is a tree and is called the subtree of the root

What is a binary tree

Binary tree is a characteristic form of tree. Binary, as the name suggests, each node of this tree has at most 2 word nodes. Note that there are at most 2 nodes, maybe only 1, or no child nodes.

Binary tree has two special forms, one is called full binary tree, the other is called complete binary tree

If all non leaf nodes of a binary tree have left and right nodes, and all leaf nodes are at the same level, the tree is a full binary tree. As shown in the figure below

If a binary tree with n nodes is numbered in hierarchical order, the numbers of all nodes are 1 to n. If all nodes of the tree are in the same position as the nodes numbered 1 to n of the full binary tree with the same depth, the binary tree is a complete binary tree. As shown in the figure below

Traversal of binary tree

After introducing the concept of binary tree, we begin to practice. Binary tree is a typical nonlinear data structure. When traversing, we need to convert the nonlinear associated nodes into a linear sequence, which can be traversed in different ways, and the traversal sequence is also different.

From the point of view of the location relationship between nodes, the traversal of binary tree is divided into four kinds.
1. Preorder traversal
2. Zhongshun traversal
3. Post order traversal
4. Sequence traversal

From a more macro point of view, the traversal of binary trees can be divided into two categories
1. Depth first traversal (pre order traversal, middle order traversal, post order traversal)
2. Breadth first traversal (sequence traversal)

Depth first traversal

1. Preorder traversal

Before traversing the binary tree, the output order is the root node, left subtree and right subtree. As shown in the figure below

The code is implemented as follows

package main

import (
   "fmt"
)
//Define a binary tree structure
type binaryTree struct {
   value int
  leftNode *binaryTree
  rightNode *binaryTree
}

func main(){
   //0 means the node is empty
  num := []int{1,2,3,4,5,0,6}
  //Convert array slices into binary tree structures
  tree := createBinaryTree(0,num)
   //Binary tree data
  var front []int
  front = frontForeach(*tree,front)
   fmt.Println(front)
}

//Create binary tree
func createBinaryTree(i int,nums []int) *binaryTree {
   tree := &binaryTree{nums[i], nil, nil}
   //The array subscript of the left node is 1,3,5 2*i+1
  if i<len(nums) && 2*i+1 < len(nums) {
      tree.leftNode = createBinaryTree(2*i+1, nums)
   }
   //The array subscript of the right node is 2,4,6 2*i+2
  if i<len(nums) && 2*i+2 < len(nums) {
      tree.rightNode = createBinaryTree(2*i+2, nums)
   }
   return tree
}

//Preorder traversal
func frontForeach(tree binaryTree,num []int) []int{
   //Traverse the root node first
  if tree.value != 0 {
      num = append(num,tree.value)
   }
   var leftNum,rightNum []int
  //If there is a left node, traverse the left node tree
  if tree.leftNode != nil {
      leftNum = frontForeach(*tree.leftNode,leftNum)
      for _,value := range leftNum{
         num = append(num,value)
      }
   }
   //If there is a right node, traverse the right node tree
  if tree.rightNode != nil {
      rightNum = frontForeach(*tree.rightNode,rightNum)
      for _,value := range rightNum{
         num = append(num,value)
      }
   }

   return num
}

2. Middle order traversal

The middle order traversal of binary tree, and the output order is left subtree, root node and right subtree. As shown in the figure below

package main

import (
    "fmt"
)
//Define a binary tree structure
type binaryTree struct {
    value int
    leftNode *binaryTree
    rightNode *binaryTree
}

func main(){
    //0 means the node is empty
    num := []int{1,2,3,4,5,0,6}
    tree := createBinaryTree(0,num)
    //Binary tree data
    var middle []int
    middle = middleForeach(*tree,middle)
    fmt.Println(middle)
}

//Create binary tree
func createBinaryTree(i int,nums []int) *binaryTree  {
    tree := &binaryTree{nums[i], nil, nil}
    //The array subscript of the left node is 1,3,5 2*i+1
    if i<len(nums) && 2*i+1 < len(nums) {
        tree.leftNode = createBinaryTree(2*i+1, nums)
    }
    //The array subscript of the right node is 2,4,6 2*i+2
    if i<len(nums) && 2*i+2 < len(nums) {
        tree.rightNode = createBinaryTree(2*i+2, nums)
    }
    return tree
}


//Medium order traversal
func middleForeach(tree binaryTree,num []int) []int{
    var leftNum,rightNum []int
    //If there is a left node, traverse the left node tree
    if tree.leftNode != nil {
        leftNum = middleForeach(*tree.leftNode,leftNum)
        for _,value := range leftNum{
            num = append(num,value)
        }
    }

    //Traverse the root node first
    if tree.value != 0 {
        num = append(num,tree.value)
    }

    //If there is a right node, traverse the right node tree
    if tree.rightNode != nil {
        rightNum = middleForeach(*tree.rightNode,rightNum)
        for _,value := range rightNum{
            num = append(num,value)
        }
    }

    return num
}

3. Post order traversal

After traversing the binary tree, the output order is left subtree, right subtree and root node. As shown in the figure below

package main

import (
    "fmt"
)
//Define a binary tree structure
type binaryTree struct {
    value int
    leftNode *binaryTree
    rightNode *binaryTree
}

func main(){
    //0 means the node is empty
    num := []int{1,2,3,4,5,0,6}
    tree := createBinaryTree(0,num)
    //Binary tree data
    var behind []int
    behind = behindForeach(*tree,behind)
    fmt.Println(behind)
}

//Create binary tree
func createBinaryTree(i int,nums []int) *binaryTree  {
    tree := &binaryTree{nums[i], nil, nil}
    //The array subscript of the left node is 1,3,5 2*i+1
    if i<len(nums) && 2*i+1 < len(nums) {
        tree.leftNode = createBinaryTree(2*i+1, nums)
    }
    //The array subscript of the right node is 2,4,6 2*i+2
    if i<len(nums) && 2*i+2 < len(nums) {
        tree.rightNode = createBinaryTree(2*i+2, nums)
    }
    return tree
}


//Postorder traversal
func behindForeach(tree binaryTree,num []int) []int{
    var leftNum,rightNum []int
    //If there is a left node, traverse the left node tree
    if tree.leftNode != nil {
        leftNum = behindForeach(*tree.leftNode,leftNum)
        for _,value := range leftNum{
            num = append(num,value)
        }
    }

    //If there is a right node, traverse the right node tree
    if tree.rightNode != nil {
        rightNum = behindForeach(*tree.rightNode,rightNum)
        for _,value := range rightNum{
            num = append(num,value)
        }
    }
    //Traverse the root node first
    if tree.value != 0 {
        num = append(num,tree.value)
    }

    return num
}

The above three traversal methods are relatively simple, and the traversal order can be completed only during the traversal process. Let's look at another way of traversal.

breadth-first search

level traversal

As the name suggests, the binary tree traverses each node horizontally layer by layer according to the hierarchical relationship from root node to leaf node, as shown below

In terms of code implementation, it is different from depth first traversal. Instead of using recursion, it uses queue. In the traversal process, it completes sequence traversal by adding queue and reading queue head

package main

import (
    "fmt"
)
//Define a binary tree structure
type binaryTree struct {
    value int
    leftNode *binaryTree
    rightNode *binaryTree
}

func main(){
    //0 means the node is empty
    num := []int{1,2,3,4,5,0,6}
    tree := createBinaryTree(0,num)
    //Binary tree data
    var row []int
    row = rowForeach(*tree,row)
    fmt.Println(row)
}

//Create binary tree
func createBinaryTree(i int,nums []int) *binaryTree  {
    tree := &binaryTree{nums[i], nil, nil}
    //The array subscript of the left node is 1,3,5 2*i+1
    if i<len(nums) && 2*i+1 < len(nums) {
        tree.leftNode = createBinaryTree(2*i+1, nums)
    }
    //The array subscript of the right node is 2,4,6 2*i+2
    if i<len(nums) && 2*i+2 < len(nums) {
        tree.rightNode = createBinaryTree(2*i+2, nums)
    }
    return tree
}


//level traversal 
func rowForeach(tree binaryTree,num []int) []int  {
    //Define queue
    var queue []binaryTree
    queue = append(queue,tree)
    for len(queue) > 0{
        var first binaryTree
        first = queue[0]
        if first.value != 0 {
            //Take out the team head value
            num = append(num,first.value)
        }
        //Delete team leader
        queue = queue[1:]
        //If the left node exists, join the tail of the queue
        if first.leftNode != nil {
            queue = append(queue,*first.leftNode)
        }
        //If the right node exists, join the tail of the queue
        if first.rightNode != nil {
            queue = append(queue,*first.rightNode)
        }
    }

    return num
}

Binary reactor

Binary heap is essentially a complete binary tree, which is divided into two types

1. Maximum heap: any parent node of the large top heap is greater than or equal to the value of its left and right child nodes, as shown in the following figure

2. Minimum heap: any parent node of the small top heap is less than or equal to the value of its left and right child nodes, as shown in the following figure

The root node of a binary heap is called the top of the heap. The characteristics of maximum heap and minimum heap determine that the top of the maximum heap is the largest element in the whole heap; The smallest heap is the smallest element in the whole heap.

For binary heap, there are several operations as follows
1. Insert node
2. Delete node
3. Construct binary tree

In terms of code implementation, since the heap is a complete binary tree, array can be used for storage. Assuming that the subscript of the parent node is parent, its left child subscript is 2 * parent+1 and its right child subscript is 2 * parent+2

The code for constructing the maximum heap is as follows: the implementation idea is roughly to find the nearest non leaf node first. Assuming there are n nodes, the nearest non leaf node is n/2. Judge each node to determine whether the node is greater than the root node. If not, the node sinks. Through step-by-step cyclic judgment, a maximum heap is constructed

package main

import "fmt"

func main(){

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

//Build maximum heap
func buildMaxHeap(arr []int,arrLen int){
    for i := arrLen/2;i >= 0; i-- {
        sink(arr,i,arrLen)
    }
}

//Tree node value sinking judgment
func sink(arr []int,i,arrLen int)  {
    //Left node subscript
    left := i*2+1
    //Right node subscript
    right := i*2+2
    largest := i
    //If the left node is larger than the parent node, the value is exchanged
    if left < arrLen && arr[left] > arr[largest] {
        largest = left
    }
    if right < arrLen && arr[right] > arr[largest] {
        largest = right
    }
    //The node value is greater than the parent node value, and data exchange is required
    if largest != i {
        //Exchange value
        swap(arr,i,largest)
        //After the exchange is completed, continue to judge whether the exchanged node value needs to continue to sink
        sink(arr,largest,arrLen)
    }
}

//change of position
func swap(arr []int,i,j int)  {
    arr[i],arr[j] = arr[j],arr[i]
}

Priority queue

Queues are characterized by first in first out (FIFO)

Enter the queue and place the new element at the end of the queue; Out of the queue, the queue head element is removed first;

The priority queue no longer follows the principle of first in first out, but is divided into two cases.

  • The maximum priority queue, regardless of the queue entry order, is the current maximum element priority queue
  • The minimum priority queue, regardless of the queue order, is the smallest element of the current priority queue

After the code for building the maximum heap is implemented above, there are two basic operations: inserting a node and deleting a node. The inserting node must first be inserted into the last array, and judge whether the inserted value needs to float up to the parent node. Deleting a node is only allowed to delete the value of the root node. After deletion, the last value of the array is placed in the root node, and then adjusted, Sink the value of the completion node. When the above operations are completed, the priority queue is implemented

The implementation code is as follows:

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")

}

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

//Out of queue
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
    }
    //Swap last node and root node values
    swapNode(arr,0,arrLen-1)
    //Delete the last value of the slice
    arr = arr[0:arrLen-1]
    //Node subsidence
    sinkNode(arr,0,len(arr))
    return arr,first
}

//Construction of large top reactor
func buildMaxHeapQueue(arr []int,arrLen int){
    for i := arrLen/2;i >= 0; i-- {
        sinkNode(arr,i,arrLen)
    }
}

//Node value floating judgment
func floatingNode(arr []int,i int){
    var root int
    if i%2 == 0 {
        root = i/2-1
    }else {
        root = i/2
    }
    //Judge whether the parent node is smaller than the child node
    if arr[root] < arr[i] {
        swapNode(arr,root,i)
        if i > 1 {
            floatingNode(arr,root)
        }
    }
}

//Tree node value sinking judgment
func sinkNode(arr []int,i int,arrLen int)  {
    //Left node subscript
    left := i*2+1
    //Right node subscript
    right := i*2+2
    largest := i
    //If the left node is larger than the parent node, the value is exchanged
    if left < arrLen && arr[left] > arr[largest] {
        largest = left
    }
    if right < arrLen && arr[right] > arr[largest] {
        largest = right
    }
    //The node value is greater than the parent node value, and data exchange is required
    if largest != i {
        //Exchange value
        swapNode(arr,i,largest)
        //After the exchange is completed, continue to judge whether the exchanged node value needs to continue to sink
        sinkNode(arr,largest,arrLen)
    }
}

//change of position
func swapNode(arr []int,i,j int)  {
    arr[i],arr[j] = arr[j],arr[i]
}

Heap sort

Heap sort is a sort algorithm designed by using the data structure of heap.

The implementation idea is to build a maximum heap first, and then traverse again. Each traverse exchanges the root node with the value of the last element, and then isolate the last element, because at this time, the last element is the largest element, and then sink the value of the root node to form the maximum heap again, and then repeat the operation to complete the sorting.

package main

import "fmt"

func main(){

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

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

   arrLen := len(arr)
   //Construction of large top reactor
  buildMaxHeap(arr,arrLen)
   for i := arrLen-1; i >= 0; i-- {
      swap(arr,0,i)
      arrLen -= 1
      sink(arr,0,arrLen)
   }
   return arr
}

//Construction of large top reactor
func buildMaxHeap(arr []int,arrLen int){
   for i := arrLen/2;i >= 0; i-- {
      sink(arr,i,arrLen)
   }
}

//Tree node value sinking judgment
func sink(arr []int,i,arrLen int)  {
   //Left node subscript
  left := i*2+1
  //Right node subscript
  right := i*2+2
  largest := i
   //If the left node is larger than the parent node, the value is exchanged
  if left < arrLen && arr[left] > arr[largest] {
      largest = left
   }
   if right < arrLen && arr[right] > arr[largest] {
      largest = right
   }
   //The node value is greater than the parent node value, and data exchange is required
  if largest != i {
      //Exchange value
  swap(arr,i,largest)
      //After the exchange is completed, continue to judge whether the exchanged node value needs to continue to sink
  sink(arr,largest,arrLen)
   }
}

//change of position
func swap(arr []int,i,j int)  {
   arr[i],arr[j] = arr[j],arr[i]
}

Topics: Go data structure