【数据结构】二叉树链式结构的实现

简介: 【数据结构】二叉树链式结构的实现

一、二叉树的链式存储


   概念:二叉树的链式存储结构是指用 链表 来表示一棵二叉树,即用链来指示元素的逻辑关系。 通常的方法是链表中每个结点由三个域组成,数据域和左右指针域,左右指针分别用来给出该结点左孩子和右孩子所在的链结点的存储地址 。链式结构又分为二叉链和三叉链,当前我们学习中一般都是二叉链,后面较难的红黑树等会用到三叉链。


结构示意图:

3c14fe8b3f5c7860e0f7092c06c78668.png


逻辑结构

c07b57f8cd2c2c09086b04210e729366.png


二叉链和三叉链的结构设计:

typedef int BTDataType;
// 二叉链
struct BinaryTreeNode
{
    struct BinTreeNode* left; // 指向当前节点左孩子
    struct BinTreeNode* right; // 指向当前节点右孩子
    BTDataType data; // 当前节点值域
}
// 三叉链
struct BinaryTreeNode
{
    struct BinTreeNode* parent; // 指向当前节点的双亲
    struct BinTreeNode* left; // 指向当前节点左孩子
    struct BinTreeNode* right; // 指向当前节点右孩子
    BTDataType data; // 当前节点值域
};



二、二叉树链式结构的实现


在学习二叉树的基本操作前,需要创建一棵二叉树,然后才能学习其相关的基本操作。为了降低学习成本,我们直接手动创建一棵简单的二叉树,快速进入二叉树操作学习,等二叉树结构了解的差不多时,我们反过头再来研究二叉树真正的创建方式。


而对于增删改等功能,对于我们当前学习的二叉树并没有价值,所以我们的学习主要围绕遍历二叉树,或者计算节点或高度来进行~


结构设计


此处我们使用的结构为 二叉链 的结构:

typedef int BTDataType;
// 二叉链
struct BinaryTreeNode
{
    struct BinTreeNode* left; // 指向当前节点左孩子
    struct BinTreeNode* right; // 指向当前节点右孩子
    BTDataType data; // 当前节点值域
}


手动构建二叉树


对于今天的学习我们直接构建一棵简单的二叉树,如下图所示:

f2bf90462ff00cf91ab897e009bf87b6.png

// 创建节点
BTNode* BuyBTNode(BTDataType x)
{
  BTNode* newnode = (BTNode*)malloc(sizeof(BTNode));
  if (newnode == NULL)
  {
    perror("malloc fail");
    exit(-1);
  }
  newnode->data = x;
  newnode->left = NULL;
  newnode->right = NULL;
  return newnode;
}
void TestBTree1()
{
  // 构建二叉树
  BTNode* n1 = BuyBTNode(1);
  BTNode* n2 = BuyBTNode(2);
  BTNode* n3 = BuyBTNode(3);
  BTNode* n4 = BuyBTNode(4);
  BTNode* n5 = BuyBTNode(5);
  BTNode* n6 = BuyBTNode(6);
  n1->left = n2;
  n1->right = n4;
  n2->left = n3;
  n4->left = n5;
  n4->right = n6;
}


前序遍历


前序遍历(Preorder Traversal 亦称先序遍历) —— 访问根结点的操作发生在遍历其左右子树之前。


由于被访问的结点必是某子树的根,所以N(Node)、L(Left subtree)和R(Right subtree)又可解释为根、根的左子树和根的右子树。所以 前序遍历 又可以被称为 NLR


前序遍历图解:

062da2c55b92d789cb82c487420f1d46.png


前序遍历的访问次序是 先访问根节点,再访问左子树,再访问右子树


那么当前树的遍历结果为:1 2 3 NULL NULL NULL 4 5 NULL NULL 6 NULL NULL

代码:

void PreOrder(BTNode* root)
{
  if (root == NULL)
  {
    printf("NULL ");
    return;
  }
  printf("%d ", root->data);
  PreOrder(root->left);
  PreOrder(root->right);
}


由于是第一个接口,我们在这里详细画一下递归展开图,之后的接口就不详细演示了,需要大家自己下去画啦:


87c3f319b2cbbd31f5861b6970910d9f.png


中序遍历


中序遍历(Inorder Traversal) —— 访问根结点的操作发生在遍历其左右子树之中(间)。

中序遍历 又可以被称为 LNR


中序遍历的访问次序是 先访问左子树,再访问根节点,再访问右子树


那么当前树的遍历结果为:NULL 3 NULL 2 NULL 1 NULL 5 NULL 4 NULL 6 NULL

void InOrder(BTNode* root)
{
  if (root == NULL)
  {
    printf("NULL ");
    return;
  }
  InOrder(root->left);
  printf("%d ", root->data);
  InOrder(root->right);
}


后序遍历


后序遍历(Postorder Traversal)——访问根结点的操作发生在遍历其左右子树之后。


后序遍历 又可以被称为 LRN

后序遍历的访问次序是 先访问左子树,再访问右子树,再访问根节点

那么当前树的遍历结果为:NULL NULL 3 NULL 2 NULL NULL 5 NULL NULL 6 4 1

void PostOrder(BTNode* root)
{
  if (root == NULL)
  {
    printf("NULL ");
    return;
  }
  PostOrder(root->left);
  PostOrder(root->right);
  printf("%d ", root->data);
}



层序遍历


层序遍历就是从第一层开始从左向右逐个遍历。


那么当前树的遍历结果为:1 2 4 3 5 6 。


平常的层序遍历只要求打印出遍历结果即可,我们这边稍加改良,分别输出每一层的遍历结果。

思路:


这里我们需要使用一个队列进行辅助。


一开始,如果二叉树非空,则入一个元素到队列中。并给定一个 levelSize 用来计算二叉树每层的大小。当前大小为1.


然后给定一个循环截止条件为队列为空。当 levelSize 不为0,那么就打印队头元素, 并且出队头。之后如果出队的元素的左右子树非空则入队列;当 levelSize == 0 时,重新计算 当前层数的大小。

循环结束之后,层序遍历完成,这时销毁队列。


void LevelOrder(BTNode* root)
{
  Queue q;
  QueueInit(&q);
  // 如果根非空,则入队列
  if (root != NULL)
  {
    QueuePush(&q, root);
  }
  // 算出每层的大小
  int levelSize = QueueSize(&q);
  // 不为空,则继续
  while (!QueueEmpty(&q))
  {
    // 如果一层不为空,则继续出
    while (levelSize--)
    {
      BTNode* front = QueueFront(&q);
      printf("%d ", front->data);
          QueuePop(&q);
      // 非空入队列
      if (front->left)
      {
        QueuePush(&q, front->left);
      }
      if (front->right)
      {
        QueuePush(&q, front->right);
      }
    }
    printf("\n");
    levelSize = QueueSize(&q);
  }
  printf("\n");
  QueueDestroy(&q);
}


一句话总结就是:出上一层,带下一层



计算二叉树大小


计算二叉树大小,即计算二叉树中 节点的个数


   这里我们可以引申出两种思路:


       给定一个 size ,遍历二叉树,分别遍历左右子树,遍历过程中遍历到非空节点 size++,遍历到空,则返回 0。


       二叉树的大小 = 根节点 + 左子树节点 + 右子树节点,将其分成多个子问题,递归求解。

我们先看 思路1 该如何实现:


如果 size 是局部变量,可不可行?当然不可行! 我们遍历的过程还是递归,当每次递归调用时,都会开辟新的函数栈帧,递归中的 size 不是同一个 size ,每次递归时的 size 一开始都是0,无法成功计算出大小。


所以这时我们可以使用全局变量 size,这样 size 在全局数据区,在整个工程内都有效。但是请注意,由于 size 不会销毁,所以多次计算大小时, size 会累加,所以 要注意在每次调用之前将 size = 0 ,避免出错。


int size = 0;
int TreeSize1(BTNode* root)
{
  if (root == NULL)
  {
    return 0;
  }
  size++;
  TreeSize1(root->left);
  TreeSize1(root->right);
  return size;
}


思路1每次调用之前置0都很麻烦,代码也不是那么简洁,那么 思路2 能否改良这些?

思路2正能解决这两个缺陷,并且非常简洁!


如果访问节点为空,则返回0;如果访问节点不为空,则 +1返回,就这样分别递归左子树和右子树,最后的返回结果就是节点个数。


int TreeSize2(BTNode* root)
{
  return root == NULL ? 0 : TreeSize2(root->left) + TreeSize2(root->right) + 1;
}



计算叶子节点个数


叶子节点,就是没有左右孩子的节点。


如果节点为空,那么不是叶子结点,返回0;如果左右孩子都为空,那么就是叶子结点,返回1。

最后分别递归计算左右子树的叶子结点。


int TreeLeafSize(BTNode* root)
{
  if (root == NULL)
  {
    return 0;
  }
  if (root->left == NULL && root->right == NULL)
  {
    return 1;
  }
  return TreeLeafSize(root->left) + TreeLeafSize(root->right);
}



计算二叉树高度


二叉树的高度就是二叉树左右子树中最高的一边加上当前 1(根节点)。


我们可以分别递归到左右子树的最后一层,如果当前节点为空,返回0;不为空则比较当前左右子树的高度,+1返回高度较高的一边。


在递归过程中使用 leftHeight 和 rightHeight 分别记录当前高度,防止重复递归调用。


int TreeHeight(BTNode* root)
{
  if (root == NULL)
  {
    return 0;
  }
  int leftHeight = TreeHeight(root->left);
  int rightHeight = TreeHeight(root->right);
  return leftHeight > rightHeight ? leftHeight + 1 : rightHeight + 1;
}


计算第 k 层的节点个数

假设 k 足够大:


   对于第一层,需要计算的是 第 k 层的节点个数;

   对于第二层,需要计算的是 第 k - 1 层的节点个数;


   … … … … … …

   对于第 k 层,计算的就是 第 1 层( 当前层数 ) 的节点个数。


那么有了这个思路,我们就可以解决当前这个接口:


如果 当前节点为空,则返回0;


如果 节点不为空,且 k == 1,那么说明已经到达第 k 层,返回1;


如果 节点不为空,且 k > 1,那么说明需要继续递归,分别递归左右子树的 第 k - 1 层。


int TreeLevalSize(BTNode* root, int k)
{
  if (root == NULL)
  {
    return 0;
  }
  if (k == 1)
  {
    return 1;
  }
  return LevalSize(root->left, k - 1) + LevalSize(root->right, k - 1);
}



查找某个值对应的节点


如果查找节点为空,则返回空;如果找到了则返回当前节点。


否则就分别递归查找左右子树,在查找过程中可以用变量来保存查找的值,如果查找返回的结果非空,那么说明找到了,就返回结果;如果左右子树都没有找到,则返回空。

BTNode* TreeFind(BTNode* root, int x)
{
  // 如果 root 为空,则返回空
  if (root == NULL)
  {
    return NULL;
  }
  // 找到了
  if (root->data == x)
  {
    return root;
  }
  // 递归左右子树
  BTNode* ret1 = TreeFind(root->left, x);
  // 如果左子树非空,则找到了,返回
  if (ret1 != NULL)
    return ret1;
  BTNode* ret2 = TreeFind(root->right, x);
  if (ret2 != NULL)
    return ret2;
  // 到这里没找到,就得返回NULL
  return NULL;
}



判断是否为完全二叉树


判断是否为完全二叉树需要用到层序遍历的思想。


依然是给一个辅助队列。


如果 root 非空,则将 root 入队。


然后给定循环队列为空则停止,取队头元素,并出队头;这时将取出元素的左右子树都放入,一旦出元素出到 NULL 这时结束循环。


完全二叉树是连续的,一旦出现 NULL ,那么后面的元素都应该是空。如果空指针后还有非空元素,那么一定不是完全二叉树。

就像这样:


8a2213f2e4c420763c7839800337147e.png


这时继续出队列,如果出队列过程中遇到非空元素,则销毁队列返回假;否则不断出队列元素。


如果循环结束还没有返回,说明后面都是空指针,这时销毁队列返回真

bool TreeComplete(BTNode* root)
{
  // 使用层序遍历思想
  Queue q;
  QueueInit(&q);
  // 如果非空,则入队列
  if (root)
    QueuePush(&q, root);
  while (!QueueEmpty(&q))
  {
    BTNode* front = QueueFront(&q);
    QueuePop(&q);
    // 一旦出队列到空,就出去判断
    if (front == NULL)
    {
      break;
    }
    else
    {
      QueuePush(&q, front->left);
      QueuePush(&q, front->right);
    }
  }
  while (!QueueEmpty(&q))
  {
    BTNode* front = QueueFront(&q);
    if (front != NULL)
    {
      QueueDestroy(&q);
      return false;
    }
    else
    {
      QueuePop(&q);
    }
  }
  QueueDestroy(&q);
  return true;
}



销毁


销毁二叉树,我们使用 后序遍历 的思想销毁。


遍历到最后一层,先销毁左子树,再销毁右子树,如果遇到空指针,那么直接返回。

void TreeDestroy(BTNode* root)
{
  if (root == NULL)
  {
    return;
  }
  TreeDestroy(root->left);
  TreeDestroy(root->right);
  free(root);
}





这里我们需要注意一下,由于传的是一级指针,所以改变形参并不影响实参。在调用处销毁后需要 置空,防止误用。




三、完整代码


层序遍历和判断是否为完全二叉树所需的队列我就不发出来了,在之前的队列文章中,我们已经实现过。


问题1:队列中存储元素类型为二叉树的节点的指针,Queue.h 引入的位置。


队列的头文件 Queue.h 需要在树的结构后引入,因为队列中存储的元素是 struct BinaryTreeNode*。

问题2:如果直接在 Queue.h 中将队列数据类型定义成 BTNode* 会在 Queue.c 中报错,因为在 Queue.c 中找不到 BTNode。


这个问题有 两种解决方案 :


   将 test.c 中关于 BTNode 的定义拷贝到 Queue.h 中。


   用原生的节点指针类型 struct BinaryTreeNode*,然后在 Queue.h 中前置声明。



test.c

#define _CRT_SECURE_NO_WARNINGS 1 
#include <stdio.h>
#include <stdlib.h>
typedef int BTDataType;
typedef struct BinaryTreeNode
{
  BTDataType data;
  struct BinaryTreeNode* left;
  struct BinaryTreeNode* right;
}BTNode;
#include "Queue.h"
BTNode* BuyBTNode(BTDataType x)
{
  BTNode* newnode = (BTNode*)malloc(sizeof(BTNode));
  if (newnode == NULL)
  {
    perror("malloc fail");
    exit(-1);
  }
  newnode->data = x;
  newnode->left = NULL;
  newnode->right = NULL;
  return newnode;
}
// 前序遍历
void PreOrder(BTNode* root)
{
  if (root == NULL)
  {
    printf("NULL ");
    return;
  }
  printf("%d ", root->data);
  PreOrder(root->left);
  PreOrder(root->right);
}
// 二叉树中序遍历
void InOrder(BTNode* root)
{
  if (root == NULL)
  {
    printf("NULL ");
    return;
  }
  InOrder(root->left);
  printf("%d ", root->data);
  InOrder(root->right);
}
// 二叉树后序遍历
void PostOrder(BTNode* root)
{
  if (root == NULL)
  {
    printf("NULL ");
    return;
  }
  PostOrder(root->left);
  PostOrder(root->right);
  printf("%d ", root->data);
}
// 计算二叉树大小
int size = 0;
int TreeSize1(BTNode* root)
{
  if (root == NULL)
  {
    return 0;
  }
  size++;
  TreeSize1(root->left);
  TreeSize1(root->right);
  return size;
}
int TreeSize2(BTNode* root)
{
  return root == NULL ? 0 : TreeSize2(root->left) + TreeSize2(root->right) + 1;
}
int TreeLeafSize(BTNode* root)
{
  if (root == NULL)
  {
    return 0;
  }
  if (root->left == NULL && root->right == NULL)
  {
    return 1;
  }
  return TreeLeafSize(root->left) + TreeLeafSize(root->right);
}
int TreeHeight(BTNode* root)
{
  if (root == NULL)
  {
    return 0;
  }
  int leftHeight = TreeHeight(root->left);
  int rightHeight = TreeHeight(root->right);
  return leftHeight > rightHeight ? leftHeight + 1 : rightHeight + 1;
}
// 层序遍历
//void LevelOrder(BTNode* root)
//{
//  Queue q;
//  QueueInit(&q);
//
//  // 如果根非空,则入队列
//  if (root != NULL)
//  {
//    QueuePush(&q, root);
//  }
//
//  // 队列非空则进行遍历
//  while (!QueueEmpty(&q))
//  {
//    BTNode* front = QueueFront(&q);
//    printf("%d ", front->data);
//    QueuePop(&q);
//
//    // 非空入队列
//    if (front->left)
//    {
//      QueuePush(&q, front->left);
//    }
//    
//    if (front->right)
//    {
//      QueuePush(&q, front->right);
//    }
//  }
//
//  printf("\n");
//  QueueDestroy(&q);
//}
// 控制一下层序遍历一层一层出
void LevelOrder(BTNode* root)
{
  Queue q;
  QueueInit(&q);
  // 如果根非空,则入队列
  if (root != NULL)
  {
    QueuePush(&q, root);
  }
  // 算出每层的大小
  int levelSize = QueueSize(&q);
  // 不为空,则继续
  while (!QueueEmpty(&q))
  {
    // 如果一层不为空,则继续出
    while (levelSize--)
    {
      BTNode* front = QueueFront(&q);
      printf("%d ", front->data);
          QueuePop(&q);
      // 非空入队列
      if (front->left)
      {
        QueuePush(&q, front->left);
      }
      if (front->right)
      {
        QueuePush(&q, front->right);
      }
    }
    printf("\n");
    levelSize = QueueSize(&q);
  }
  printf("\n");
  QueueDestroy(&q);
}
int TreeKLevelSize(BTNode* root, int k)
{
  if (root == NULL)
  {
    return 0;
  }
  if (k == 1)
  {
    return 1;
  }
  if (k > 1)
  {
    return TreeKLevelSize(root->left, k - 1) + TreeKLevelSize(root->right, k - 1);
  }
}
BTNode* TreeFind(BTNode* root, int x)
{
  // 如果 root 为空,则返回空
  if (root == NULL)
  {
    return NULL;
  }
  // 找到了
  if (root->data == x)
  {
    return root;
  }
  // 否则递归左右子树
  BTNode* ret1 = TreeFind(root->left, x);
  // 如果左子树非空,则找到了,返回
  if (ret1 != NULL)
    return ret1;
  BTNode* ret2 = TreeFind(root->right, x);
  if (ret2 != NULL)
    return ret2;
  // 到这里没找到,就得返回NULL
  return NULL;
}
// 判断二叉树是否是完全二叉树
bool TreeComplete(BTNode* root)
{
  // 使用层序遍历思想
  Queue q;
  QueueInit(&q);
  // 如果非空,则入队列
  if (root)
    QueuePush(&q, root);
  while (!QueueEmpty(&q))
  {
    BTNode* front = QueueFront(&q);
    QueuePop(&q);
    // 一旦出队列到空,就出去判断
    if (front == NULL)
    {
      break;
    }
    else
    {
      QueuePush(&q, front->left);
      QueuePush(&q, front->right);
    }
  }
  while (!QueueEmpty(&q))
  {
    BTNode* front = QueueFront(&q);
    if (front != NULL)
    {
      QueueDestroy(&q);
      return false;
    }
    else
    {
      QueuePop(&q);
    }
  }
  QueueDestroy(&q);
  return true;
}
// 销毁
void TreeDestroy(BTNode* root)
{
  if (root == NULL)
  {
    return;
  }
  TreeDestroy(root->left);
  TreeDestroy(root->right);
  free(root);
}
void TestBTree1()
{
  // 构建二叉树
  BTNode* n1 = BuyBTNode(1);
  BTNode* n2 = BuyBTNode(2);
  BTNode* n3 = BuyBTNode(3);
  BTNode* n4 = BuyBTNode(4);
  BTNode* n5 = BuyBTNode(5);
  BTNode* n6 = BuyBTNode(6);
  n1->left = n2;
  n1->right = n4;
  n2->left = n3;
  n4->left = n5;
  n4->right = n6;
  PreOrder(n1);
  printf("\n");
  InOrder(n1);
  printf("\n");
  PostOrder(n1);
  printf("\n");
}
void TestBTree2()
{
  // 构建二叉树
  BTNode* n1 = BuyBTNode(1);
  BTNode* n2 = BuyBTNode(2);
  BTNode* n3 = BuyBTNode(3);
  BTNode* n4 = BuyBTNode(4);
  BTNode* n5 = BuyBTNode(5);
  BTNode* n6 = BuyBTNode(6);
  BTNode* n7 = BuyBTNode(7);
  n1->left = n2;
  n1->right = n4;
  n2->left = n3;
  n4->left = n5;
  n4->right = n6;
  n2->right = n7;
  /*size = 0;
  printf("%d\n", TreeSize1(n1));
  size = 0;
  printf("%d\n", TreeSize1(n1));
  size = 0;
  printf("%d\n", TreeSize1(n1));
  printf("%d\n", TreeSize2(n1));
  printf("%d\n", TreeLeafSize(n1));
  printf("%d\n", TreeHeight(n1));
  printf("%d\n", TreeKLevelSize(n1, 3));*/
  printf("%d\n", TreeComplete(n1));
  LevelOrder(n1);
  TreeDestroy(n1);
  n1 = NULL;
}
int main()
{
  //TestBTree1();
  TestBTree2();
  return 0;
}



相关文章
|
1月前
|
机器学习/深度学习 存储 算法
数据结构实验之二叉树实验基础
本实验旨在掌握二叉树的基本特性和遍历算法,包括先序、中序、后序的递归与非递归遍历方法。通过编程实践,加深对二叉树结构的理解,学习如何计算二叉树的深度、叶子节点数等属性。实验内容涉及创建二叉树、实现各种遍历算法及求解特定节点数量。
81 4
|
1月前
|
存储 搜索推荐 算法
【数据结构】树型结构详解 + 堆的实现(c语言)(附源码)
本文介绍了树和二叉树的基本概念及结构,重点讲解了堆这一重要的数据结构。堆是一种特殊的完全二叉树,常用于实现优先队列和高效的排序算法(如堆排序)。文章详细描述了堆的性质、存储方式及其实现方法,包括插入、删除和取堆顶数据等操作的具体实现。通过这些内容,读者可以全面了解堆的原理和应用。
75 16
|
1月前
|
C语言
【数据结构】二叉树(c语言)(附源码)
本文介绍了如何使用链式结构实现二叉树的基本功能,包括前序、中序、后序和层序遍历,统计节点个数和树的高度,查找节点,判断是否为完全二叉树,以及销毁二叉树。通过手动创建一棵二叉树,详细讲解了每个功能的实现方法和代码示例,帮助读者深入理解递归和数据结构的应用。
129 8
|
2月前
|
存储 算法 关系型数据库
数据结构与算法学习二一:多路查找树、二叉树与B树、2-3树、B+树、B*树。(本章为了解基本知识即可,不做代码学习)
这篇文章主要介绍了多路查找树的基本概念,包括二叉树的局限性、多叉树的优化、B树及其变体(如2-3树、B+树、B*树)的特点和应用,旨在帮助读者理解这些数据结构在文件系统和数据库系统中的重要性和效率。
31 0
数据结构与算法学习二一:多路查找树、二叉树与B树、2-3树、B+树、B*树。(本章为了解基本知识即可,不做代码学习)
|
2月前
|
存储 算法 搜索推荐
数据结构与算法学习十七:顺序储存二叉树、线索化二叉树
这篇文章主要介绍了顺序存储二叉树和线索化二叉树的概念、特点、实现方式以及应用场景。
35 0
数据结构与算法学习十七:顺序储存二叉树、线索化二叉树
|
2月前
|
Java
【用Java学习数据结构系列】震惊,二叉树原来是要这么学习的(二)
【用Java学习数据结构系列】震惊,二叉树原来是要这么学习的(二)
31 1
|
2月前
|
算法 Java C语言
【用Java学习数据结构系列】震惊,二叉树原来是要这么学习的(一)
【用Java学习数据结构系列】震惊,二叉树原来是要这么学习的(一)
29 1
|
2月前
|
存储
【数据结构】二叉树链式结构——感受递归的暴力美学
【数据结构】二叉树链式结构——感受递归的暴力美学
|
2月前
|
存储 算法
探索数据结构:分支的世界之二叉树与堆
探索数据结构:分支的世界之二叉树与堆
|
2月前
探索顺序结构:栈的实现方式
探索顺序结构:栈的实现方式