【数据结构(初阶)】——二叉树

【数据结构】——二叉树

文章目录

  • 【数据结构】——二叉树
    • 前言
    • 1. 树的概念及结构
      • 1.1 树的概念
      • 1.2 树的结构
    • 2. 二叉树的概念及结构
      • 2.1 二叉树的概念
      • 2.2 二叉树的结构
      • 2.3 二叉树的性质
    • 3. 二叉树顺序结构及概念
      • 3.1 二叉树的顺序结构
      • 3.2 堆的概念及结构
      • 3.3 堆的实现
        • 3.3.1 堆的基本操作
        • 3.3.2 堆的基本实现
        • 3.3.4 文件中查找TopK问题
    • 4. 二叉树链式结构及概念
      • 4.1 二叉树链式结构的遍历
      • 4.2 二叉树的基本操作
      • 4.3 二叉树的基本实现
    • 结语

前言

小伙伴们,大家好呀,今天我们学习的是数据结构中的 二叉树

之前我们写过二叉树的OJ题,但是有很多小伙伴不知道 二叉树 讲的是什么,咱们今天就好好详细地讲讲

1. 树的概念及结构

1.1 树的概念

在数据结构中,有一种被称为树的结构。和链式结构相似,需要用一个节点中的指针去查找下一块位置。但与链表不同的是,指向其他节点的指针会大于一,其结构如图所示

在这里插入图片描述

根据上图我们能够很清楚的了解树的概念:每个节点都会存储数据和指针,树有一个特点,就是虽然指针很多,但是能找到一个固定节点的指针只有一个

为了方便我们更加清楚地描述树,接下来将会讲解相关概念

  • 节点的度:一个节点含有的子树个数(例如图中的节点A的度为2,节点D的度为1)
  • 叶子节点或终端节点:度为0的节点被称为叶节点(例如图中节点E、I、J、K)
  • 非叶子节点或非终端节点:度不为0的节点(例如图中节点A、B、C、D、F、G、H)
  • 双亲节点或父节点:有子节点的节点,非叶子节点都是某个节点的父节点(例如图中节点A、B等)
  • 孩子节点或子节点:一个节点有父节点则为该父节点的子节点(例如图中除了A节点之外都能够是子节点)
  • 兄弟节点:具有相同父节点的节点(例如节点D、E、F、G都是兄弟节点)
  • 树的度:整个树中最大的度(指节点A的度)
  • 节点的层次:开始为1层,向下递增(例如图中节点A的层数为1,节点K的层数是5)
  • 树的高度或深度:最大节点的层次,也就是最大层(这里一共有5层,所以树的高度为5)
  • 堂兄弟节点:父节点在同一层的节点,但是父节点不相同(例如图中节点D和G是堂兄弟节点)
  • 祖先节点:特定节点向上的所有节点都是祖先节点(例如图中节点I的祖先节点有A、C、F)
  • 子孙节点:以某结点为根的子树中任一结点都称为该结点的子孙(例如图中的节点A,剩下的节点都是子孙节点)

1.2 树的结构

由上图可知,树的子节点都是不固定的,那么我们没办法直接构建相同结构体来表示数。因此有人想出了另一种方法来接收描述树。用一个子节点来找到他的兄弟节点,如下图所示

在这里插入图片描述

红色部分为实际数据存储方式,蓝色为树原本结构

如此解决了结构体多定义的问题,形成了一种只有子节点指针和右兄弟指针的方式,这种方式称作孩子兄弟表示法

代码实现如下:

struct TreeNode
{int val; //存储的数据struct TreeNode* leftchild; // 左孩子指针struct TreeNode* rightbrother; // 右兄弟指针
};

孩子兄弟表示法示意图

在这里插入图片描述

2. 二叉树的概念及结构

2.1 二叉树的概念

二叉树(binary tree)是指树中节点的度不大于2的有序树,它是一种最简单且最重要的树。二叉树的递归定义为:二叉树是一棵空树,或者是一棵由一个根节点和两棵互不相交的,分别称作根的左子树和右子树组成的非空树;左子树和右子树又同样都是二叉树

如图就是一颗标准的二叉树

在这里插入图片描述

二叉树的特点:

  1. 每个结点最多有两棵子树,即二叉树不存在度大于2的结点
  2. 二叉树的子树有左右之分,其子树的次序不能颠倒

特殊的二叉树:

  1. 满二叉树:一个二叉树,如果每一个层的结点数都达到最大值,则这个二叉树就是满二叉树。也就是说,如果一个二叉树的层数为K,且结点总数是(2^k) -1 ,则它就是满二叉树,也就是每一层都是满的

    在这里插入图片描述

  2. 完全二叉树:对于深度为K的,有n个结点的二叉树,当且仅当其每一个结点都与深度为K的满二叉树中编号从1至n的结点一一对应时称之为完全二叉树。就是除了最后一层之外都是满的,并且最后一层的元素是连续的

    在这里插入图片描述

需要我们注意的是满二叉树是特殊的完全二叉树

2.2 二叉树的结构

有关二叉树的结构,我们可以从物理结构逻辑结构两个角度进行理解

逻辑结构(想象出来的):使用左右指针储存数据

在这里插入图片描述

物理结构(也叫存储结构,内存中存取数据的结构):使用数组存储数据

在这里插入图片描述

二叉树如果按照存储结构可以分为顺序结构链式结构两种主要形式

顺序结构:顺序结构存储就是使用数组来存储,一般使用数组只适合表示完全二叉树,因为不是完全二叉树会有空间的浪费。而现实中使用中只有堆才会使用数组来存储。二叉树顺序存储在物理上是一个数组,在逻辑上是一颗二叉树

这里插入图片描述

链式结构:用链表来表示一棵二叉树,即用链来指示元素的逻辑关系。 通常的方法是链表中每个结点由三个域组成,数据域和左右指针域,左右指针分别用来给出该结点左孩子和右孩子所在的链结点的存储地址

在这里插入图片描述

2.3 二叉树的性质

  • 若规定根节点的层数为1,则一棵非空二叉树的第i层上最多有2^(i-1) 个结点

  • 若规定根节点的层数为1,则深度为h的二叉树的最大结点数是2^h- 1

  • 对任何一棵二叉树, 如果度为0其叶结点个数为 n0, 度为2的分支结点个数为 n2,则有n0=n2+1

  • 若规定根节点的层数为1,具有n个结点的满二叉树的深度,h=Log2(n+1). (ps:Log2(n+1)是log以2为底,n+1为对数)

  • 对于具有n个结点的完全二叉树,如果按照从上至下从左至右的数组顺序对所有节点从0开始编号,则对于序号为i的结点有:

    1. 若i>0,i位置节点的双亲序号:(i-1)/2;i=0,i为根节点编号,无双亲节点
    2. 若2i+1<n,左孩子序号:2i+1,2i+1>=n否则无左孩子
    3. 若2i+2<n,右孩子序号:2i+2,2i+2>=n否则无右孩子
    

3. 二叉树顺序结构及概念

3.1 二叉树的顺序结构

普通的二叉树是不适合用数组来存储的,因为可能会存在大量的空间浪费。而完全二叉树更适合使用顺序结构存储。现实中我们通常把堆(一种二叉树)使用顺序结构的数组来存储,需要注意的是这里的堆和操作系统虚拟进程地址空间中的堆是两回事,一个是数据结构,一个是操作系统中管理内存的一块区域分段

在这里插入图片描述

3.2 堆的概念及结构

堆在物理(存储)结构上是数组,逻辑结构上就是完全二叉树

堆的性质:

  • 堆中某个节点的值总是不大于或不小于其父节点的值
  • 堆总是一棵完全二叉树

然而堆又分为大堆和小堆

大根堆就是整个完全二叉树的 任意一个根节点的值都比左右子树的值大

在这里插入图片描述

小根堆表示整个完全二叉树的 任意一个根节点的值都比左右子树的值小

在这里插入图片描述

我们不难发现堆是父亲和孩子是有关系的,但是兄弟之间是没有大小关系的

3.3 堆的实现

3.3.1 堆的基本操作
// 堆的构建
void HeapCreate(Heap* hp, HPDataType* a, int n);
// 堆的销毁
void HeapDestory(Heap* hp);
// 堆的插入
void HeapPush(Heap* hp, HPDataType x);
// 堆的删除
void HeapPop(Heap* hp);
// 取堆顶的数据
HPDataType HeapTop(Heap* hp);
// 堆的数据个数
int HeapSize(Heap* hp);
// 堆的判空
int HeapEmpty(Heap* hp);
// 对数组进行堆排序
void HeapSort(int* a, int n);
3.3.2 堆的基本实现

堆的定义

在物理结构上堆就是数组,所以这里我们可以先定义一个堆的结构体,里面存放栈数组的指针,有size来记录堆中数据的个数,capacity来记录堆的空间大小

typedef int HPDataType;
typedef struct Heap
{HPDataType* a;int size;int capacity;
}Heap;

堆的初始化

这里我们先不给数组开辟空间,当堆里插入数据时我们再开辟空间

void HeapInit(Heap* hp)
{assert(php);hp->a = NULL;hp->capacity = hp->size = 0;
}

堆的销毁

堆的销毁就是释放掉给堆存放数据的空间,我们先free销毁数组,然后再给数组指针指向空,再将 top 和 capacity 都给0表示栈为空

void HeapDestory(Heap* hp)
{assert(hp);free(hp->a);hp->a = NULL;hp->capacity = hp->size = 0;
}

堆的插入

堆的插入我们需要得先开辟一定的空间,和队列一样的,扩容时 realloc 相比与 malloc 会更好,然后再更新a和capacity,赋值x,size++,堆插入的基本思想就是在堆的尾部插入x,然后就可以通过向上调整算法,将x调整到合适的位置,这里我们得好好讲一讲这个向上调整算法

void AdjustUp(HPDataType* a, int child)
{int parent = (child - 1) / 2;while (child > 0){if (a[child] < a[parent]){Swap(&a[child], &a[parent]);child = parent;parent = (child - 1) / 2;}else{break;}}
}
void HeapPush(Heap* hp, HPDataType x)
{assert(hp);if (hp->capacity == hp->size){int newcapacity = hp->capacity == 0 ? 4 : hp->capacity * 2;Heap* tmp = (Heap*)realloc(hp->a, newcapacity * sizeof(HPDataType));if (tmp == NULL){perror("realloc fail!");return;}hp->a = hp;hp->capacity = newcapacity;}hp->a[hp->size++] = x;AdjustUp(hp->a, hp->size - 1);
}

向上调整算法

我们将要插入的那个元素的位置视为孩子,利用这个位置找到父亲节点

上面写了孩子的下标为i,父亲的节点是(i-1)/2

拿这个图举列子,这个是建立小堆,所以小节点在上面

在这里插入图片描述

按照小堆来调整,所以当发现父亲比孩子大的数据就交换。

循环交替,互换父亲和孩子的位置,直到孩子的数组下标为0时循环就截至

void AdjustUp(HPDataType* a, int child)
{int parent = (child - 1) / 2;while (child > 0){if (a[child] < a[parent]){Swap(&a[child], &a[parent]);child = parent;parent = (child - 1) / 2;}else{break;}}
}

堆的删除

堆的删除就是要将堆顶位置的元素删除,先将堆顶元素和堆尾元素交换一下,然后直接size–,将交换后堆尾元素给删除掉,最后通过向下调整算法,将交换后的堆顶元素调整到合适的位置,这里我们再好好讲一讲这个向下调整算法

void AdjustDown(HPDataType* a, int n, int parent)
{int child = 2 * parent + 1;while (child < n)// child >= n 说明孩子不存在,调整到叶子了{if (child + 1 < n && a[child] > a[child + 1]){++child;}if (a[child] < a[parent]){Swap(&a[child], &a[parent]);parent = child;child = 2 * parent + 1;}else{break;}}
}
void HeapPop(Heap* hp)
{assert(hp);assert(hp->size > 0);Swap(&hp->a[0], &hp->a[hp->size - 1]);hp->size--;AdjustDown(hp->a, hp->size, 0);
}

向下调整算法

现在我们给出一个数组,逻辑上看做一颗完全二叉树。我们通过从根结点开始的向下调整算法可以把它调整成一个小堆。 向下调整算法有一个前提:左右子树必须是一个堆,才能调整

我们将要调整的那个元素的位置视为父亲,利用这个位置找到孩子节点

我们就将父亲的下标为i, 这时孩子节点有两个怎么办,我咋知道谁更小(大),我们就可以运用假设法的思想,找出较小(大)的节点

找到合适的孩子节点就交换

然后再将孩子节点传给父亲节点,然后继续往下找,直到孩子节点到达了叶子节点时循环就结束

下图是全过程:

在这里插入图片描述

void AdjustDown(HPDataType* a, int n, int parent)
{int child = 2 * parent + 1;while (child < n)// child >= n 说明孩子不存在,调整到叶子了{if (child + 1 < n && a[child] > a[child + 1]){++child;}if (a[child] < a[parent]){Swap(&a[child], &a[parent]);parent = child;child = 2 * parent + 1;}else{break;}}
}

获取堆顶元素

我们直接取数组下标为0位置的元素就行了,因为数组下标为0位置就是堆顶

HPDataType HeapTop(Heap* hp)
{assert(hp);assert(hp->size > 0);return hp->a[0];
}

判空

当数组中没有元素时堆为空,即size == 0

int HeapEmpty(Heap* hp)
{assert(hp);return hp->size == 0;
}

堆的数据个数

堆的数据个数相当于数组的元素个数,直接取size就行了

int HeapSize(Heap* hp)
{assert(hp);return hp->size;
}
3.3.4 文件中查找TopK问题

TOP-K 问题:即求数据结合中前 K 个最大的元素或者最小的元素,一般情况下数据量都比较大
比如:专业前 10 名、世界 500 强、富豪榜、游戏中前 100 的活跃玩家等
对于 Top-K 问题,能想到的最简单直接的方式就是排序,但是:如果数据量非常大,排序就不太可取了 ( 可能数据都不能一下子全部加载到内存中)

最佳的方式就是用堆来解决,基本思路如下:

  1. 用数据集合中前 K 个元素来建堆
  • 前 k 个最大的元素,则建小堆。建小堆,堆顶就是这K个元素的最小值,然后向后遍历其他数,如果其他数大于堆顶的元素,就弹出堆顶元素,插入这个较大的元素,这样遍历完成之后,堆中最小的元素都比剩下的数字大,这样堆中的K个元素就是所有元素前K大的
  • 前 k 个最小的元素,则建大堆。建大堆,堆顶就是这K个元素的最大值,然后向后遍历其他数,如果其他数小于堆顶的元素,就弹出堆顶元素,插入这个较小的元素,这样遍历完成之后,堆中最大的元素都比剩下的数字大,这样堆中的K个元素就是所有元素前K小的
  1. 用剩余的 N-K 个元素依次与堆顶元素来比较,不满足则替换堆顶元素
  • 将剩余 N-K 个元素依次与堆顶元素比完之后,堆中剩余的 K 个元素就是所求的前 K 个最小或者最大的元素。

文件中的TopK多一步是读取文件的一部分数据,因为文件可能很大,没办法全部加载到文件中,就可以循环使用一块缓冲区进行TopK,然后再加载文件中的内容,然后再执行TopK,一直到文件被读取完,这时候堆中的元素就是文件中TopK的元素

接下来就是代码实现:

void CreateNDate()
{// 造数据int n = 10000;srand(time(0));const char* file = "data.txt";FILE* fin = fopen(file, "w");if (fin == NULL){perror("fopen error");return;}for (size_t i = 0; i < n; ++i){int x = rand() % 1000000;fprintf(fin, "%d\n", x);}fclose(fin);
}void PrintTopK(int k)
{const char* file = "data.txt";FILE* fout = fopen(file, "r");if (fout == NULL){perror("fopen error");return;}int* kminheap = (int*)malloc(sizeof(int) * k);if (kminheap == NULL){perror("malloc error");return;}for (int i = 0; i < k; i++){fscanf(fout, "%d", &kminheap[i]);}// 建小堆for (int i = (k - 1 - 1) / 2; i >= 0; i--){AdjustDown(kminheap, k, i);}int val = 0;while (!feof(fout)){fscanf(fout, "%d", &val);if (val > kminheap[0]){kminheap[0] = val;AdjustDown(kminheap, k, 0);}}for (int i = 0; i < k; i++){printf("%d ", kminheap[i]);}printf("\n");
}

4. 二叉树链式结构及概念

4.1 二叉树链式结构的遍历

所谓遍历(Traversal)是指沿着某条搜索路线,依次对树中每个结点均做一次且仅做一次访问。访问结点所做的操作依赖于具体的应用问 题。 遍历是二叉树上最重要的运算之一,是二叉树上进行其它运算之基础

前序/中序/后序的递归结构遍历:是根据访问结点操作发生位置命名

  1. 前序遍历(Preorder Traversal 亦称先序遍历):访问根结点的操作发生在遍历其左右子树之前
  2. 中序遍历(Inorder Traversal):访问根结点的操作发生在遍历其左右子树之中(间)
  3. 后序遍历(Postorder Traversal):访问根结点的操作发生在遍历其左右子树之后

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

所以前序遍历、中序遍历和后序遍历分别又称为先根遍历、中根遍历和后根遍历

在这里插入图片描述

层序遍历:除了先序遍历、中序遍历、后序遍历外,还可以对二叉树进行层序遍历。设二叉树的根节点所在层数为1,层序遍历就是从所在二叉树的根节点出发,首先访问第一层的树根节点,然后从左到右访问第2层上的节点,接着是第三层的节点,以此类推,自上而下,自左至右逐层访问树的结点的过程就是层序遍历

在这里插入图片描述

4.2 二叉树的基本操作

// 二叉树前序遍历 
void BinaryTreePrevOrder(BTNode* root);
// 二叉树中序遍历
void BinaryTreeInOrder(BTNode* root);
// 二叉树后序遍历
void BinaryTreePostOrder(BTNode* root);
// 通过前序遍历的数组"ABD##E#H##CF##G##"构建二叉树
BTNode* BinaryTreeCreate(BTDataType* a, int n, int* pi);
// 二叉树销毁
void BinaryTreeDestory(BTNode** root);
// 二叉树节点个数
int BinaryTreeSize(BTNode* root);
// 二叉树叶子节点个数
int BinaryTreeLeafSize(BTNode* root);
// 二叉树第k层节点个数
int BinaryTreeLevelKSize(BTNode* root, int k);
// 二叉树查找值为x的节点
BTNode* BinaryTreeFind(BTNode* root, BTDataType x);
// 层序遍历
void BinaryTreeLevelOrder(BTNode* root);
// 判断二叉树是否是完全二叉树
int BinaryTreeComplete(BTNode* root);

4.3 二叉树的基本实现

树的定义

定义一个二叉树的结构体,里面存着节点的数据,还有左子树的结构体和右子树的结构体

typedef char BTDataType;
typedef struct BinaryTreeNode
{BTDataType data;struct BinaryTreeNode* left;struct BinaryTreeNode* right;
}BTNode;

创建树的节点

节点的创建我们需要 malloc一个结构体,再检查节点是否开辟成功,然后将节点数据赋值为X即可,再将左右指针指向空,最后返回开辟好的节点

BTNode* BuyNode(int x)//创建树的节点
{BTNode* node = (BTNode*)malloc(sizeof(BTNode));if (node == NULL){perror("malloc fail");exit(-1);}node->a = x;node->left = node->right = NULL;return node;
}

手动创建一颗树

我们可以先手动创建一个树试试

在写完创建树的节点的函数之后,我们再手动创建一颗树就变得更简单了

BTNode* CreatTree()//建树
{BTNode* node1 = BuyNode(1);BTNode* node2 = BuyNode(2);BTNode* node3 = BuyNode(3);BTNode* node4 = BuyNode(4);BTNode* node5 = BuyNode(5);BTNode* node6 = BuyNode(6);node1->left = node2;node1->right = node4;node2->left = node3;node4->left = node5;node4->right = node6;return node1;
}

在这里插入图片描述

这样做的话,一颗树二叉树就简单地构建好了

前序遍历

前序遍历一颗树时分为两种情况:

  1. 如果树为空,那就不用访问了直接 return 结束了
  2. 如果树不为空,那就先访问根节点,然后还需要继续左右子树前序遍历,那我们就递归函数解决
void BinaryTreePrevOrder(BTNode* root)
{if (root == NULL)return;printf("%c", root->data);BinaryTreePrevOrder(root->left);BinaryTreePrevOrder(root->right);
}

中序遍历

和前序遍历思想一样,只不过中序就先递归左子树 再访问根 再递归右子树即可

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

后序遍历

也是和前序遍历的思想一样,只不过后序就先递归左子树 再递归右子树 再递归根即可

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

通过前序遍历的数组"ABD##E#H##CF##G##"构建二叉树

用一个字符数组中递归地构建二叉树,数组 a 中的元素按照前序遍历的方式表示二叉树节点,如果数组元素是 #,表示空节点。函数通过递归的方式创建每个节点,直到数组处理完成

BTNode* BinaryTreeCreate(BTDataType* a, int n, int* pi)
{if (*pi >= n || a[*pi] == '#'){(*pi)++;return NULL;}// 创建根节点BTNode* root = (BTNode*)malloc(sizeof(BTNode));if (root == NULL){perror("malloc fail");exit(-1);}root->data = a[*pi];*pi++;// 创建左子树和右子树root->left = BinaryTreeCreate(a, n, pi);root->right = BinaryTreeCreate(a, n, pi);return root;
}

二叉树销毁

这里需要用到了后序遍历,如果先释放root的话就找不到root的左子树和右子树了,先找到左右子树,再去释放掉根节点。这样二叉树的销毁就完成了

void BinaryTreeDestory(BTNode** root)
{if (root == NULL || *root == NULL)return;// 递归销毁左子树和右子树BinaryTreeDestory(&(*root)->left);BinaryTreeDestory(&(*root)->right);// 释放当前节点free(*root);*root == NULL;
}

二叉树节点个数

如果二叉树为空就返回0,然后再去递归左右子树, +1 是根节点

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

二叉树叶子节点个数

如果二叉树为空就返回0,这里就需要我们理解什么是叶子节点了,我们说叶子节点是没有左右子树的节点,那就说明它的左右子树为空,然后再递归左右子树就行了

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

二叉树第K层节点个数

还是如果节点为空就返回0,如果根节点不为空,就继续往下访问,当k–到1时,就不能继续往下访问了,因为求的就是求二叉树第k层结点个数,所以再往下访问就是违背了要求,所以这里直接返回1,也就是到达哪一层的那个节点,然后就是递归左子树和右子树,并且需要传 k - 1 这个变量

int BinaryTreeLevelKSize(BTNode* root, int k)
{if (root == NULL)return 0;if (k == 1)return 1;// 递归成子问题解决return BinaryTreeLevelKSize(root->left, k - 1);+BinaryTreeLevelKSize(root->right, k - 1);
}

二叉树查找值为X的节点

需要注意的是这个返回值是一个指针,找到了就返回节点,没找到就去左子树找,左子树找到了就不用再往后执行代码了,如果没有找到就去右子树找,右子树找到了就不用再往后执行代码了,如果都没找到的话就返回空

BTNode* BinaryTreeFind(BTNode* root, BTDataType x)
{if (root == NULL)return NULL;if (root->data == x)return root;BTNode* ret1 = BinaryTreeFind(root->left, x);if (ret1)return ret1;BTNode* ret2 = BinaryTreeFind(root->left, x);if (ret2)return ret2;return NULL;
}

层序遍历

层序遍历,就是一层一层来遍历

先遍历第一层 1,再遍历第二层 2,4 ,再遍历最后一层 3,5,6。这里你可以发现使用递归无法实现,因为递归,你只能先把左子树或者右子树遍历完才能遍历其他的,但是这里层序遍历是一层一层来的

因为树不是连在一起的,根节点带左子树和右子树,所以这里我们可以用队列,一层带着一层入队列

现在1是根节点那就入队列,入队列的时候就把2,4带着,正好是第一二层打印顺序就搞定了,此时1再出队列,我们在实现队列时写了一个函数是取队头数据,所以直接取即可,这时1取走了,这时2就是队头了,取出队头2的数据,然后再将2带的左右子树入队列

在这里插入图片描述

此时2就是队头,将其取出,然后2的左子树和右子树入队列

在这里插入图片描述

接下来步骤一样 取出4,然后 5,6 入队列

在这里插入图片描述

此时,一个个出队列,直到队列为空,循环就结束了

在这里插入图片描述

void BinaryTreeLevelOrder(BTNode* root)
{if (root == NULL)return;Queue q;QueueInit(&q);if (root)QueuePush(&q, root);while (!QueueEmpty(&q)){BTNode* front = QueueFront(&q);QueuePop(&q);printf("%d", root->data);if (root->left)QueuePush(&q,root->left);if (root->right)QueuePush(&q,root->right);}BinaryTreeDestory(&q);
}

判断二叉树是否是完全二叉树

我们在这里再次回忆一下什么是完全二叉树,完全二叉树就是假设有k层, k - 1 层都是满节点,而第k层的节点存在必须是连续的,中间不能有空节点,如果中间有空节点,然后又有节点的话这种就不是完全二叉树

根据这个介绍,再根据上面队列的思想,入一个带左子树与右子树,如果我们遇到第一个空就开始判断如果接下来全是空即可说明是完全二叉树,如果空后又有节点就说明不是完全二叉树

如果还有数据没有入队,我们就可以不用管它,这时因为在空空后已经有数据出现了,所以不用入数据了,已经不是完全二叉树了

int BinaryTreeComplete(BTNode* root)
{if (root == NULL)return;Queue q;QueueInit(&q);if (root)QueuePush(&q, root);while (!QueueEmpty(&q)){BTNode* front = QueueFront(&q);QueuePop(&q);// 遇到第一个空,就可以开始判断,如果队列中还有非空,就不是完全二叉树if (front == NULL){break;}QueuePush(&q, root->left);QueuePush(&q, root->right);}while (!QueueEmpty(&q)){BTNode* front = QueueFront(&q);QueuePop(&q);if (front){BinaryTreeDestory(&q);return false;}}BinaryTreeDestory(&q);return true;
}

结语

这些就是 数据结构(初阶)——二叉树 的全部内容了,要是想做一点题的可以看看这篇哦【数据结构】——二叉树OJ题

感谢你能看到这里,希望这篇文章对你有用,溜了溜了,我们下篇再见吧

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/422763.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

OpenAI 的 o1 大模型在数学和编码方面有了几乎 10 倍的能力提升!

你有没有想过,有一天人工智能可以在数学和编程这两个领域里,真正成为人类的“得力助手”,甚至是超越我们?最近,OpenAI 发布的 o1大模型在这方面取得了几乎 10 倍的能力提升。10 倍!你没有看错。这样的进步让人不禁怀疑:AI 真的能做到“秒懂”数学和编程吗?今天,我们就…

远程访问NAS速度慢??那是因为你没用对。。。

虽然局域网&#xff08;内网&#xff09;、公网&#xff08;外网&#xff09;经常被提到&#xff0c;但很多人依旧搞不懂分不清楚。。。 其实&#xff0c;简单的方法就是把局域网IP比喻成公司的内部通讯&#xff0c;公网IP看作公共通讯平台。 这样拥有公网IP能被直接远程访问&…

redis内存清理和linux系统清理缓存以及redis启动

1清空所有数据库 redis-cli FLUSHALL 2清空所有数据库redis-cli FLUSHDB 3. 删除指定的缓存键 redis-cli DEL <key>4. 设置键过期 redis-cli EXPIRE <key> <seconds>例如&#xff1a; redis-cli EXPIRE mykey 605.启动redis 这个启动命令要在/usr/loca…

【Canvas与密铺】90年代马赛克密铺效果 1920x1080

【成图】 【代码】 <!DOCTYPE html> <html lang"utf-8"> <meta http-equiv"Content-Type" content"text/html; charsetutf-8"/> <head><title>20世纪90年代马赛克瓷砖效果1920x1080</title><style type&…

MySQL:bin log

redo log 它是物理日志&#xff0c;记录内容是“在某个数据页上做了什么修改”&#xff0c;属于 InnoDB 存储引擎。 而 binlog 是逻辑日志&#xff0c;记录内容是语句的原始逻辑&#xff0c;类似于“给 ID2 这一行的 c 字段加 1”&#xff0c;属于MySQL Server 层。 不管用什…

如何处理DDOS攻击问题

随着信息技术的飞速发展&#xff0c;网络已成为现代社会不可或缺的一部分&#xff0c;极大地便利了个人社交和商业活动。然而&#xff0c;网络空间在创造无限机遇的同时&#xff0c;也潜藏着诸多威胁&#xff0c;其中分布式拒绝服务攻击&#xff08;DDoS&#xff0c;Distribute…

全球工业经济系统极端降水暴露数据集(2010年、2016-2035年和2046-2065年)

全球工业经济系统极端降水暴露数据集 数据介绍 1. 数据的时间覆盖范围&#xff1a; 数据收集时期为2010年、2016-2035年和2046-2065年。 2. 空间覆盖和投影&#xff1a; 空间覆盖范围&#xff1a;全球 经度&#xff1a;-180 - 180 纬度&#xff1a;-90 - 90 投影&#x…

qemu和libvirt的配置对比

libvirt的很多配置选项其实是调用了qemu的接口&#xff0c;但也有增加和优化的地方&#xff0c;本文主要总结这些配置选项&#xff0c;当个手册来查询。 按照centos停服前最后一版centos-8.5.2111提供的rpm查看http://mirrors.aliyun.com/centos/8.5.2111/AppStream/aarch64/o…

【JUC】16-Java对象内存布局和对象头

1. 对象的内存布局 在HotSpot虚拟机里&#xff0c;对象在堆内存中的存储布局可以分为三个部分&#xff1a;对象头、实例数据和对齐填充。 对象头&#xff1a;由对象标记和类型指针。

[数据集][目标检测]烟叶病害检测数据集VOC+YOLO格式612张3类别

数据集格式&#xff1a;Pascal VOC格式YOLO格式(不包含分割路径的txt文件&#xff0c;仅仅包含jpg图片以及对应的VOC格式xml文件和yolo格式txt文件) 图片数量(jpg文件个数)&#xff1a;612 标注数量(xml文件个数)&#xff1a;612 标注数量(txt文件个数)&#xff1a;612 标注类别…

精心整理|算法备案大模型备案最新数据汇总

根据《互联网信息服务算法推荐管理规定》第二条 在中华人民共和国境内应用算法推荐技术提供互联网信息服务&#xff08;以下简称算法推荐服务&#xff09;&#xff0c;适用本规定。法律、行政法规另有规定的&#xff0c;依照其规定。前款所称应用算法推荐技术&#xff0c;是指利…

Excel数据转置|Excel数据旋转90°

Excel数据转置|Excel数据旋转90 将需要转置的数据复制在旁边空格处点击鼠标右键&#xff0c;选择图中转置按钮&#xff0c;即可完成数据的转置。&#xff01;&#xff01;&#xff01;&#xff01;非常有用啊啊啊&#xff01;&#xff01;&#xff01;

【数据结构-一维差分】力扣1854. 人口最多的年份

给你一个二维整数数组 logs &#xff0c;其中每个 logs[i] [birthi, deathi] 表示第 i 个人的出生和死亡年份。 年份 x 的 人口 定义为这一年期间活着的人的数目。第 i 个人被计入年份 x 的人口需要满足&#xff1a;x 在闭区间 [birthi, deathi - 1] 内。注意&#xff0c;人不…

【C++登堂入室】类和对象(中)——类的6个默认成员函数

目录 一、类的6个默认成员函数 ​编辑二、构造函数 2.1 概念 2.2 特性 三、析构函数 3.1 概念 3.2 特性 四、拷贝构造函数 4.1 概念 4.2 特征 五、赋值运算符重载 5.1 运算符重载 5.2 赋值运算符重载 5.3 前置和后置重载 六、日期类的实现 七、const成员 八、…

解锁企业潜能,Vatee万腾平台引领智能新纪元

在数字化转型的浪潮中&#xff0c;企业正站在一个前所未有的十字路口&#xff0c;面对着前所未有的机遇与挑战。解锁企业内在潜能&#xff0c;实现跨越式发展&#xff0c;已成为众多企业的共同追求。而Vatee万腾平台&#xff0c;作为智能科技的先锋&#xff0c;正以其强大的智能…

随机分类,保持均衡水平Python

1、目的&#xff1a; 10000个样本有4个指标&#xff0c;按照逾期金额分10类&#xff0c;确保每类别逾期金额均衡。 2、数据&#xff1a; 3、思路&#xff1a; 将10000个样本按照逾期金额排序&#xff0c; 等距分箱为2500个类别 增加一列随机数 根据类别和随机数升序排列 增加…

Linux学习-Docker文件系统

Overlayfs Overlayfs 是一种类似 aufs 的一种堆叠文件系统&#xff0c;于 2014 年正式合入 Linux-3.18 主线内核&#xff0c;目前其功能已经基本稳定&#xff08;虽然还存在一些特性尚未实现&#xff09;且被逐渐推广。 Overlayfs 是一种堆叠文件系统&#xff0c;它依赖并建立…

在VB.net中,DateTime类使用,举例说明

标题 在VB.net中&#xff0c;DateTime类使用&#xff0c;举例说明 前面学习相关 1.在VB.net中&#xff0c;如何把"20240906"转化成日期格式 2.在VB.net中 DateTime有什么属性与方法 3.在VB.net中&#xff0c;Stopwatch有什么属性与方法 正文 在VB.NET中&#xff0c;D…

利用 Zero-1-2-3 进行多视图 3D 重建:从单图像到多视图 3D 模型的生成

3D 模型生成在计算机视觉领域有着广泛的应用&#xff0c;从虚拟现实到自动驾驶&#xff0c;基于单张图像的 3D 重建技术正在迅速发展。这篇博客将带你深入探索如何使用 Zero-1-2-3 框架进行多视图 3D 重建&#xff0c;通过详细解析该框架中的代码结构和功能&#xff0c;帮助你理…

【ArcGIS Pro实操第七期】栅格数据合并、裁剪及统计:以全球不透水面积为例

【ArcGIS Pro实操第七期】批量裁剪&#xff1a;以全球不透水面积为例 准备&#xff1a;数据下载ArcGIS Pro批量裁剪数据集1 数据拼接2 数据裁剪3 数据统计&#xff1a;各栅格取值3.1 栅格计算器-精确提取-栅格数据特定值3.2 数据统计 4 不透水面积变化分析 参考 准备&#xff1…