Binary tree and binary search tree of basic algorithm summary

Posted by jordanwb on Wed, 20 Oct 2021 04:54:08 +0200

1, Tree definition

In computer science, tree (English: tree) is an abstract data type (ADT) or a data structure that implements this abstract data type, which is used to simulate a data set with the nature of tree structure. It is a set with hierarchical relationship composed of n (n > 0) finite nodes. It is called "tree" because it looks like an upside down tree, that is, it has roots up and leaves down.

This is an academic definition. In a simple vernacular, a tree can be regarded as a bunch of grapes.

2, Tree species

The following lists some trees that are often contacted during development, and briefly classifies them as follows:

3, Common terms

termmeaning
Degree of nodeThe number of subtrees contained in a node is called the degree of the node
Degree of treeIn a tree, the largest node degree is called the degree of the tree
Leaf nodeNode with zero degree
Branch nodeNode with non-zero degree
Parent nodeIf a node has children, it is called parent of the its children
Child nodeThe root node of the subtree contained in a node is called the child node of the node
Sibling nodeNodes with the same parent node are called siblings
arrangementStarting from the definition of the root, the root is the first layer, and the child node of the root is the second layer
depthFor any node n, the depth of n is the length of the unique path from the root to N, and the depth of the root is 0
heightFor any node n, the height of n is the longest path from n to a leaf, and the height of all leaves is 0
forestThe collection of m (m > = 0) disjoint trees is called forest

4, Binary tree definition

A binary tree has at most two subtrees per node (that is, there are no nodes with a degree greater than 2 in the binary tree), and the subtrees of the binary tree can be divided into left and right, and their order can not be reversed arbitrarily.

5, Properties of binary tree

There are several common properties:

  • There are at most $2^{n-1} $nodes (n ≥ 1) on the nth component of the binary tree

  • A binary tree with depth k has at most 2 k − 1 2^k-1 2k − 1 node (k ≥ 1)

  • For any binary tree T, if the number of terminal nodes is a and the node tree with degree 2 is b, then a=b+1

  • The depth of a complete binary tree with n nodes is l o g 2 n + 1 log_2n+1 log2​n+1

6, Implementation of binary tree

Here, we use golang as the programming language to implement binary tree. The principle is the same, and it can also be implemented in other languages. Here are several important methods about binary tree:

  • Traversing a binary tree (recursive / non recursive): before, during, after, and hierarchy

  • Depth of binary tree

6.1. Define binary tree

Define the basic operation interface first

type BinaryTreeOperation interface {
  RecursionPreOrderTraverse()  //   Recursive preorder traversal
  PreOrderTraverse()           //   Non recursive preorder traversal
  RecursionInOrderTraverse()   //   Recursive middle order traversal
  InOrderTraverse()            //   Non recursive middle order traversal
  RecursionPostOrderTraverse() //   Recursive postorder traversal
  PostOrderTraverse()          //   Non recursive postorder traversal
  LevelTraverse()              // level traversal 

  TreeDepth() int   // Tree depth
  CountLeaves() int // Number of leaves
  CountNodes() int  // Node number
}

Then the node and operation implementation class of binary tree are defined

// TreeNode binary tree node definition
type TreeNode struct {
  data int
  Left *TreeNode
  Right *TreeNode
}

// NewTreeNode builds node data
func NewTreeNode(left, right *TreeNode, data int) *TreeNode {
  return &TreeNode{Left: left, Right: right, data: data}
}

// BinaryTree binary tree implementation
type BinaryTree struct {
  root *TreeNode
}

Initialization data. The data constructed here is the binary tree in the definition of binary tree in part 4

// Constructor for NewBinaryTree binary tree
func NewBinaryTree() *BinaryTree {
  tree := &BinaryTree{}
  tree.InitTree()
  return tree
}

// InitTree initializes binary tree data
func (b *BinaryTree) InitTree() {
  node9 := NewTreeNode(nil, nil, 9)
  node8 := NewTreeNode(nil, nil, 8)
  node7 := NewTreeNode(node9, nil, 7)
  node6 := NewTreeNode(nil, nil, 6)
  node5 := NewTreeNode(node8, nil, 5)
  node4 := NewTreeNode(nil, nil, 4)
  node3 := NewTreeNode(node6, node7, 3)
  node2 := NewTreeNode(node4, node5, 2)
  b.root = NewTreeNode(node2, node3, 1)
}

The preparatory work has been completed. Now we begin to realize the related operations of binary tree!

6.2. Recursive traversal

Here, the anonymous function of golang is used to modify the shared variables to realize recursive traversal.

6.2.1. Preorder traversal

Traversal principle: if binary tree is empty, this operation is empty; Otherwise, access the root node, left subtree and right subtree.

func (b *BinaryTree) RecursionPreOrderTraverse() {
  // Store traversal data
  result := make([]int, 0)
  // Define anonymous functions
  var innerTraverse func(node *TreeNode)
  
  // Anonymous function implementation
  innerTraverse = func(node *TreeNode) {
    // Preorder traversal is root, left and right
    result = append(result, node.data)
    if node.Left != nil {
      innerTraverse(node.Left)
    }
    if node.Right != nil {
      innerTraverse(node.Right)
    }
  }
  // The current binary tree is not empty. Call the internal anonymous function
  if b.root != nil {
    innerTraverse(b.root)
  }
  // Output results
  fmt.Printf("Preorder traversal:%v\n", result)
}

6.2.2. Middle order traversal

Traversal principle: if binary tree is empty, this operation is empty; Otherwise, access the left subtree, root node and right subtree.

This is basically the same as the preorder traversal, but the position of append is different.

func (b BinaryTree) RecursionInOrderTraverse() {
  result := make([]int, 0)

  var innerTraverse func(node *TreeNode)

  innerTraverse = func(node *TreeNode) {
    if node.Left != nil {
      innerTraverse(node.Left)
    }
    result = append(result, node.data)
    if node.Right != nil {
      innerTraverse(node.Right)
    }
  }
  if b.root != nil {
    innerTraverse(b.root)
  }

  fmt.Printf("Middle order traversal:%v\n", result)
}

6.2.3. Post order traversal

Traversal principle: if binary tree is empty, this operation is empty; Otherwise, access the left subtree, right subtree and root node.

func (b BinaryTree) RecursionPostOrderTraverse() {
  result := make([]int, 0)

  var innerTraverse func(node *TreeNode)

  innerTraverse = func(node *TreeNode) {
    if node.Left != nil {
      innerTraverse(node.Left)
    }
    if node.Right != nil {
      innerTraverse(node.Right)
    }
    result = append(result, node.data)
  }
  if b.root != nil {
    innerTraverse(b.root)
  }

  fmt.Printf("Post order traversal:%v\n", result)
}

6.3. Non recursive traversal

6.3.1. Preorder traversal

Execution diagram: the execution process of pre, middle and post sequence traversal is the same, and the difference lies in the sequence of obtaining data.

Execution Description: This is equivalent to putting the recursive execution process on the desktop (recursion is equivalent to putting the executed function call into a function call stack). Out of the stack into the stack is equivalent to function call.

// PreOrderTraverse non recursive preorder traversal
func (b BinaryTree) PreOrderTraverse() {
  // Simulation stack
  stack := make([]*TreeNode, 0)
  // Store traversal data
  result := make([]int, 0)
  // Judge whether the binary tree is empty
  if b.root == nil {
    return
  }
  // Point the pointer to the root node
  start := b.root
  
  for start != nil || len(stack) != 0 {
    // First traverse the left subtree until the left subtree is nil
    if start != nil {
      // Each traversal puts the node data into the cache array
      result = append(result, start.data)
      // Save the current node to the simulated stack
      stack = append(stack, start)
      // Point the current pointer to the left subtree of the next node
      start = start.Left
    } else {
      // When the node of the pointer is nil, it indicates that the current pointer has pointed to the leaf node of the left subtree
      
      lens := len(stack) - 1
      // Get the node element at the top of the stack
      pop := stack[lens]
      // Simulated out of stack
      stack = stack[:lens]
      // The pointer points to the right subtree of the node
      start = pop.Right
    }
  }
  
  fmt.Printf("Non recursive preorder traversal:%v\n", result)
}

Execution process:

  • First, judge whether the current binary tree is an empty tree. If not, point the pointer to the root node;

  • Judge whether the current pointer is empty and whether the current stack is empty;

  • Then judge that the current pointer is not empty, put the node data pointed to by the previous pointer into the cache array, and put the current pointer on the stack;

  • Until the current pointer is empty, it indicates that the leaf node of the left subtree has been reached. At this time, it is necessary to backtrack (assign the top element of the stack to the current pointer, and then push the stack out of the stack). Until the node pointed to by the current pointer is not nil, assign the right subtree address of the node pointed to by the current pointer to the current pointer.

  • The following processes are similar in turn. When the elements in the stack are empty, it jumps out of the loop.

6.3.2. Middle order traversal

The middle order traversal needs to obtain data during backtracking.

func (b BinaryTree) InOrderTraverse() {
  // Simulation stack
  stack := make([]*TreeNode, 0)
  // Store traversal data
  result := make([]int, 0)
  // Judge whether the binary tree is empty
  if b.root == nil {
    return
  }

  start := b.root

  for start != nil || len(stack) != 0 {

    if start != nil {
      stack = append(stack, start)
      start = start.Left
    } else {
      lens := len(stack) - 1
      pop := stack[lens]
      result = append(result, pop.data)
      stack = stack[:lens]
      start = pop.Right
    }
  }
  fmt.Printf("Non recursive middle order traversal:%v\n", result)
}

6.3.3. Post order traversal

This subsequent traversal is troublesome. You need to ensure that the left and right child nodes are traversed before you can output the root node. Here, you need to add a delayed pointer to the element out of the stack.

func (b BinaryTree) PostOrderTraverse() {
  // Simulation stack
  stack := make([]*TreeNode, 0)
  // Store traversal data
  result := make([]int, 0)
  // Judge whether the binary tree is empty
  if b.root == nil {
    return
  }
  var temp *TreeNode

  stack = append(stack, b.root)

  for len(stack) != 0 {

    cur := stack[len(stack) - 1]
    // The current node is nil / the deferred pointer is not null, and the left or right pointer of the deferred pointer is the same as the current stack top pointer
    // This indicates that the left and right child nodes are traversed.
    if (cur.Left == nil && cur.Right == nil) || (temp != nil && (temp == cur.Left || temp == cur.Right)) {
      result = append(result, cur.data)
      temp = cur
      stack = stack[:len(stack) - 1]
    } else {
      // Ensure that the right node pointer is put into the stack first, so that the left node pointer can precede the right node pointer
      if cur.Right != nil {
        stack = append(stack, cur.Right)
      }
      if cur.Left != nil {
        stack = append(stack, cur.Left)
      }
    }

  }

  fmt.Printf("Non recursive postorder traversal:%v\n", result)
}

Sketch Map:

6.4. Level traversal

This level traversal also relies on queue first in first out.

// LevelTraverse hierarchy traversal
func (b BinaryTree) LevelTraverse() {
  // Store traversal data
  result := make([]int, 0)
  queue := make([]*TreeNode, 0)
  // Judge whether the binary tree is empty
  if b.root == nil {
    return
  }
  // Put the first element
  queue = append(queue, b.root)
  for len(queue) > 0 {
    // Gets the element of the queue header in the queue
    head := queue[0]
    // Let the team head out of the team
    queue = queue[1:]
    // Get header element
    result = append(result, head.data)
    // If the left subtree is not empty, join the end of the queue
    if head.Left != nil {
      queue = append(queue, head.Left)
    }
    // If the right subtree is not empty, it will be added to the end of the queue
    if head.Right != nil {
      queue = append(queue, head.Right)
    }
  }
  fmt.Printf("Hierarchy traversal:%v\n", result)
}

Sketch Map:

6.5. Maximum depth

Recursive implementation!

func (b BinaryTree) TreeDepth() int {
  if b.root == nil {
    return 0
  }
  var innerDepth func(node *TreeNode) int
  // Define anonymous functions
  innerDepth = func(node *TreeNode) int {

    if node == nil {
      return 0
    }

    l := innerDepth(node.Left) + 1
    r := innerDepth(node.Right) + 1

    if l < r {
      l, r = r, l
    }

    return l

  }
  return innerDepth(b.root)
}

Without recursion, you can also modify the level traversal to achieve the maximum depth acquisition.

7, Binary lookup tree

It can also be seen from the above that binary trees have no order and order. Search, insert and delete on the binary tree are not allowed. Therefore, the following describes the widely used binary search tree.

7.1. Properties of binary search tree

The properties of binary search tree are as follows:

  • If its left subtree is not empty, the values of all nodes on the left subtree are less than the values of its root node;

  • If its right subtree is not empty, the values of all nodes on the right subtree are greater than those of its root node;

A typical binary search tree is shown in the figure below:

7.2. Create (add node)

Binary search tree is evolved from binary tree and can be extended based on the above binary number.

To build a binary lookup tree, we need to pay attention to meeting the properties of binary lookup tree.

In addition, it should be noted that for the processing of duplicate elements of binary search tree, the basic data type of int is used here. If this processing method is found to be the same, it will not be inserted; However, the data area may be a complex data type such as object. Here, we need to consider adding additional areas to store or store them in another tree (this case will not be discussed too much here).

To create a binary search tree, it is better to find nodes through traversal and then insert them. Therefore, with the basis of the above binary tree, we can implement it with a little modification in the traversal method.

func (b *BinaryTree) Insert(data int) {
  // If the root node is nil, it is created
  if b.root == nil {
    b.root = NewTreeNode(nil, nil, data)
    return
  }
  // Define anonymous recursive functions
  var innerTraverse func(node *TreeNode)
  
  innerTraverse = func(node *TreeNode) {
    // If the current node data is less than data, you need to find it on the right subtree
    if node.data < data {
      // If the right subtree is nil at this time, it can be assigned directly
      if node.Right == nil {
        node.Right = NewTreeNode(nil, nil, data)
      } else {
        // No, look down
        innerTraverse(node.Right)
      }

    } else {
      // Similar to the right subtree operation
      if node.Left == nil {
        node.Left = NewTreeNode(nil, nil, data)
      } else {
        innerTraverse(node.Left)
      }
    }
  }
  innerTraverse(b.root)
}

At this time, a binary search tree in 7.1 is constructed as follows:

tree := binary_tree.SimpleTree()

tree.Insert(10)
tree.Insert(7)
tree.Insert(15)
tree.Insert(9)
tree.Insert(8)
tree.Insert(18)
tree.Insert(12)
tree.Insert(3)
tree.Insert(13)

tree.RecursionInOrderTraverse()  // Middle order traversal: [3 7 8 9 10 12 13 15 18]

The middle order traversal here is to sort the output according to the size order!

7.3. Node data search

Given a data search, whether the binary lookup tree exists

func (b *BinaryTree) Search(data int) bool {
  
  // If the root node is nil, false is returned directly
  if b.root == nil {
    return false
  }

  var innerTraverse func(node *TreeNode) bool

  innerTraverse = func(node *TreeNode) bool {
    // The current node is nil and returns false
    if node == nil {
      return false
    } else {
      var status bool
      // equal
      if node.data == data {
        status = true
      } else if node.data < data {
        // Less than, continue to traverse the right subtree
        status = innerTraverse(node.Right)
      } else {
        // Greater than, continue to traverse the left subtree
        status = innerTraverse(node.Left)
      }
      return status
    }
  }
  
  // call
  return innerTraverse(b.root)
}

7.4. Obtain maximum and minimum values

The structure of binary search tree can be very simple until the leftmost node is the minimum and the rightmost node is the maximum.

func (b *BinaryTree) MaxValue() (int, error) {
  if b.root == nil {
    return 0, errors.New("The current tree is empty")
  }

  p := b.root

  for p.Right != nil {
    p = p.Right
  }
  return p.data, nil
}

7.5. Delete the maximum and minimum nodes

Here is a method to delete the lowest node:

// DeleteMin deletes the smallest node in the binary lookup tree
func (b *BinaryTree) DeleteMin() bool {
  return b.deleteMin(b.root)
}

// DeleteMin deletes the smallest node under a node
func (b *BinaryTree) deleteMin(node *TreeNode) bool {
  if node == nil {
    return false
  }
  // Pointer used to save the parent node
  var prev *TreeNode
  // Traverse to find the smallest node
  for node.Left != nil {
    prev = node
    node = node.Left
  }
  // If the left node is not nil, the left child pointer of the parent node of the node to be deleted points to the right child of the node to be deleted
  if node.Right != nil {
    prev.Left = node.Right
  } else {
    // Otherwise, the left child pointer of the parent node of the node to be deleted directly points to nil
    prev.Left = nil
  }
  return true
}

First deletion:

The second deletion:

7.5. Delete node

The operation here is troublesome and involves four situations, which are described one by one below:

The first case: the deleted node does not exist: in this case, you can delete the node directly

The second case: the deleted node has a left child: at this time, directly let the parent node of the node point to the left child of the current node, and then delete the current node;

The third case: the deleted node has a right child: at this time, directly let the parent node of the node point to the left child of the current node, and then delete the current node;

The fourth case: the deleted node has left and right children: find the right subtree, find the smallest node, exchange the value with the current node, and finally delete it

Code implementation:

func (b *BinaryTree) min(node *TreeNode) (int, error) {
  if node == nil {
    return 0, errors.New("Node is empty")
  }
  for node.Left != nil {
    node = node.Left
  }
  return node.data, nil
}

// DeleteMin deletes the smallest node under a node
func (b *BinaryTree) deleteMin(node *TreeNode) (bool, error) {
  if node == nil {
    return false, errors.New("Current node is nil")
  }

  if node.Left == nil && node.Right == nil {
    return false, errors.New("The current node does not have child nodes")
  }
  var prev *TreeNode

  for node.Left != nil {
    prev = node
    node = node.Left
  }
  if node.Right != nil {
    prev.Left = node.Right
  } else {
    prev.Left = nil
  }
  return true, nil
}

// DeleteNode delete node
func (b *BinaryTree) DeleteNode(data int) bool {

  var prev *TreeNode
  var status bool
  curr := b.root

  if curr == nil {
    return false
  }

  // First find the node location to delete
  for curr != nil {
    if curr.data < data {
      prev = curr
      curr = curr.Right
      continue
    } else if curr.data > data {
      prev = curr
      curr = curr.Left
      continue
    } else {
      status = true
      break
    }
  }

  // If not found, return false directly
  if !status {
    return false
  }

  // Then delete the node
  if curr.Left == nil && curr.Right == nil {
    // The first case: the left and right subtrees of the found node are nil
    // However, there are two types of relationships with parent nodes:
    //    The first is: the left child pointer of the parent node points to the node to be deleted;
    //    The second method: the right child pointer of the parent node points to the node to be deleted;
    if prev.Right.data == curr.data {
      prev.Right = nil
    }
    if prev.Left.data == curr.data {
      prev.Left = nil
    }

  } else if curr.Left != nil && curr.Right == nil {
    // The second case: the left subtree of the found node is not empty, and the right subtree is nil
    // As in the first case, the combination of deleting a node and its parent node will also exist in two cases
    //    The first is: the left child pointer of the parent node points to the node to be deleted. You need to point the right subtree pointer of the parent node to the left subtree pointed to by the current node
    //    The second is: the right child pointer of the parent node points to the node to be deleted. You need to point the left subtree pointer of the parent node to the right subtree pointed to by the current node
    if prev.Right.data == curr.data {
      prev.Right = curr.Left
    }
    if prev.Left.data == curr.data {
      prev.Left = curr.Left
    }

  } else if curr.Left == nil && curr.Right != nil {
    // The third case: the right subtree of the node to be found is not empty, and the left subtree is nil, which is opposite to the second case
    if prev.Right.data == curr.data {
      prev.Right = curr.Right
    }
    if prev.Left.data == curr.data {
      prev.Left = curr.Right
    }
  } else {
    // The fourth case: the left and right subtrees are not empty
    // At this time, you need to find the smallest element in the right subtree and replace it with the content of the node to be deleted
    // Finally, delete the minimum node of the right subtree
    min, _ := b.min(curr.Right)
    curr.data = min
    if curr.Right.Left == nil && curr.Right.Right == nil {
      curr.Right = nil
    } else {
      b.deleteMin(curr.Right)
    }
  }
  return status
}

7.6. Note

Here, with the increase of nodes, stack overflow may occur when we use recursion. Non recursion is more recommended for the above operations.

Another problem is the degradation of binary search tree. Take an extreme example:

This situation also satisfies the condition of binary search tree. However, at this time, the binary search tree has approximately degenerated into a linked list. The search time complexity of such binary search tree immediately becomes O(n). It can be imagined that we must not let this happen. In order to solve this problem, we can use balanced binary tree (AVL).

Topics: Algorithm data structure