深入理解数据结构--二叉树(基础篇)

C/C++
381
0
0
2022-05-04
标签   数据结构

最近开始学习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+1if i<len(nums) && 2*i+1 < len(nums) {
      tree.leftNode = createBinaryTree(2*i+1, nums)}//右节点的数组下标为2,4,6...2*i+2if 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+1if i<len(nums) && 2*i+1 < len(nums) {
        tree.leftNode = createBinaryTree(2*i+1, nums)}//右节点的数组下标为2,4,6...2*i+2if 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+1if i<len(nums) && 2*i+1 < len(nums) {
        tree.leftNode = createBinaryTree(2*i+1, nums)}//右节点的数组下标为2,4,6...2*i+2if 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+1if i<len(nums) && 2*i+1 < len(nums) {
        tree.leftNode = createBinaryTree(2*i+1, nums)}//右节点的数组下标为2,4,6...2*i+2if 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 []intreturn 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 intif 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 -= 1sink(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]
}