树是一种非线性的数据结构,它是由n(n>=0)个有限结点组成一个具有层次关系的集合。把它叫做树是因为它看起来像一棵倒挂的树,也就是说它是根朝上,而叶朝下的。
注:
如果知道树的度就可以用以下方式表示树:
struct TreeNode
{
int data;
struct TreeNode* subs[6]; //树的度
};
注意:这种树的表示方式会有内存浪费,因为有的节点的度不一定都和树的度一样。
在不知道树的度的情况下可以用以下方式表示树:
struct TreeNode
{
int data;
SeqList subs; //顺序表存储节点的指针
};
树结构相对线性表就比较复杂了,要存储表示起来就比较麻烦了,既然保存值域,也要保存结点和结点之间的关系,实际中树有很多种表示方式如:双亲表示法,孩子表示法、孩子双亲表示法以及孩子兄弟表示法等。其中最常用的是孩子兄弟表示法。
双亲表示法:
struct TreeNode
{
int data;
struct TreeNode* parent
};
//双亲表示法(但不实用)
左孩子右兄弟表示法(孩子兄弟表示法)
typedef int DataType;
struct Node
{
struct Node* _firstChild1; // 左边开始的第一个孩子结点
struct Node* _pNextBrother; // 指向其下一个(右边)兄弟结点
DataType _data; // 结点中的数据域
};
应用:Windows操作系统利用这种结构表示文件系统
一棵二叉树是结点的一个有限集合,该集合:
注意:
注意:对于任意的二叉树都是由以下几种情况复合而成的(这些又称为二叉树):
满二叉树:一个二叉树,如果每一个层的结点数都达到最大值,则这个二叉树就是满二叉树。也就是说,如果一个二叉树的层数为K(层数从1开始计算),且结点总数是2^k-1 ,则它就是满二叉树(每一层都是满的)。
完全二叉树:完全二叉树是效率很高的数据结构,完全二叉树是由满二叉树而引出来的。对于深度为K的,有n个结点的二叉树,当且仅当其每一个结点都与深度为K的满二叉树中编号从1至n的结点一一对应时称之为完全二叉树。
总结:
若规定根节点的层数为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为对数),这个公式是通过2^k-1=n计算出来的
完全二叉树中度为一的最少有0个,最多有1个
对于具有n个结点的完全二叉树,如果按照从上至下从左至右的数组顺序对所有节点从0开始编号,则对于序号为i的结点有:
二叉树一般可以使用两种结构存储,一种顺序结构,一种链式结构。
顺序存储
顺序结构存储就是使用数组来存储,一般使用数组只适合表示完全二叉树,因为不是完全二叉树会有空间的浪费。而现实中使用中只有堆才会使用数组来存储。二叉树顺序存储在物理上是一个数组,在逻辑上是一颗二叉树。
链式存储
二叉树的链式存储结构是指用链表来表示一棵二叉树,即用链来指示元素的逻辑关系。 通常的方法是链表中每个结点由三个域组成,数据域和左右指针域,左右指针分别用来给出该结点左孩子和右孩子所在的链结点的存储地址 。链式结构又分为二叉链和三叉链,当前使用的一般都是二叉链。
typedef int BTDataType;
// 二叉链
struct BinaryTreeNode
{
struct BinTreeNode* _pLeft; // 指向当前节点左孩子
struct BinTreeNode* _pRight; // 指向当前节点右孩子
BTDataType _data; // 当前节点值域
}
// 三叉链
struct BinaryTreeNode
{
struct BinTreeNode* _pParent; // 指向当前节点的双亲
struct BinTreeNode* _pLeft; // 指向当前节点左孩子
struct BinTreeNode* _pRight; // 指向当前节点右孩子
BTDataType _data; // 当前节点值域
};
**总结:**满二叉树和完全二叉树适合用顺序存储,因为节点是挨着存放的。但是并不意味着只有这两个特殊的二叉树能使用顺序存储,普通的二叉树也可以。这种的二叉树使用顺序存储会存在一定的空间浪费。说明顺序存储只适合存储完全二叉树或者满二叉树。
普通的二叉树是不适合用数组来存储的,因为可能会存在大量的空间浪费。而完全二叉树更适合使用顺序结构存储。现实中通常把堆(一种二叉树)使用顺序结构的数组来存储,需要注意的是这里的堆和操作系统虚拟进程地址空间中的堆是两回事,一个是数据结构,一个是操作系统中管理内存的一块区域分段。
注:
数据结构和操作系统这两门学科中都有栈和堆这两个名词,这两个名词之间没有关联关系。栈和堆在数据结构中栈是后进先出的线性表数据结构、堆是用于排序以及选top数的二叉树数据结构。栈和堆在操作系统中栈是函数调用会建立栈帧(其中栈帧就是在栈上开空间的)、堆是动态开辟内存空间在堆上开辟,它们两个是对内存的一个区域的划分的名称(栈和堆是内存划分两个区域的名称)。
堆是用数组存储表示的完全二叉树
如果有一个关键码的集合K = { k 0 , k 1 , ⋯ , k n − 1 k_0,k_1, \cdots , k_{n-1} k0,k1,⋯,kn−1},把它的所有元素按完全二叉树的顺序存储方式存储在一个一维数组中,并满足: K i K_i Ki <= K 2 ∗ i + 1 K_{2*i+1} K2∗i+1且 K i K_i Ki <= K 2 ∗ i + 2 K_{2*i+2} K2∗i+2( K i K_i Ki >= K 2 ∗ i + 1 K_{2*i+1} K2∗i+1且 K i K_i Ki >= K 2 ∗ i + 2 K_{2*i+2} K2∗i+2 ) i = 0,1,2…,则称为小堆(或大堆)。将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆。
堆的性质:
总结:
堆向下调整算法
现在给出一个数组,逻辑上看做一颗完全二叉树。可以通过从根节点开始的向下调整算法可以把它调整成一个小堆。向下调整算法有一个前提:左右子树必须是一个堆(小堆或大堆)(根下面的左右子树是小堆或者大堆),才能调整。
int array[] = {27,15,19,18,28,34,65,49,25,37};
堆向下调整算法是对于根而言选出左右孩子小(大)的那一个,跟父亲比如果比父亲小(大)则交换。接着再以要交换的节点作为父亲循环往复向下调整,直到左右孩子不存在或者没有发生交换则终止。
注:堆向下调整算法是针对根下面左右子树都是堆的特征的树调整成堆
总结:
堆向下调整算法:
void Swap(int* px, int* py)
{
int tmp = *px;
*px = *py;
*py = tmp;
}
// 条件:左右子树都是小堆/大堆
void AdjustDown(int* a, int n, int parent) //这里之所以写节点向下调整的位置,是因为创建堆是从倒数第一个非叶子节点开始的其所在位置不一定是根节点。
{
int child = parent * 2 + 1;
while (child < n) //停止持续向下调整的条件:其一是判断父亲是否是叶子节点(孩子是否存在),用数组下标判断是否越界
{
// 选出左右孩子中小 or 大的那个
if (child+1 < n && a[child+1] > a[child]) //注意当左孩子存在时右孩子不一定存在有可能发生越界,如果右孩子不存在则左孩子就是孩子中最小(大)的
{
++child;
}
// 1、如果小 or 大的孩子比父亲小 or 大,则交换,继续往下调整
// 2、如果小 or 大 的孩子比父亲大 or 小,则结束调整(停止持续向下调整的条件其二)
if (a[child] > a[parent])
{
Swap(&a[child], &a[parent]);
parent = child;
child = parent * 2 + 1;
}
else
{
break;
}
}
}
堆的创建
给出一个数组,这个数组逻辑上可以看做一颗完全二叉树,但是还不是一个堆,现在可以通过算法,把它构建成一个堆。根节点左右子树不是堆,这时可以从倒数的第一个非叶子节点的子树开始调整,一直调整到根节点的树,就可以调整成堆。
int a[] = {1,5,3,8,7,6};
堆的创建:
void HeapCreat(int* a, int n)
{
//建堆
for (int i = (n-1-1)/2; i >= 0; --i)
{
AdjustDown(a, n, i);
}
}
建堆时间复杂度
因为堆是完全二叉树,而满二叉树也是完全二叉树,此处为了简化使用满二叉树来证明(时间复杂度本来看的就是近似值,多几个节点不影响最终结果):
堆的定义
typedef int HPDataTpye;
typedef struct Heap
{
HPDataTpye* a; //堆的物理结构为数组
int size; //存储有效数据个数(最后一个数据的下一个位置)
int capacity; //存储数据的容量
}HP;
注:优先级队列(priority_queue)的底层用堆实现的,优先级队列不满足先进先出,是按优先级出队列的。
堆的初始化
堆的初始化首先要开辟出一段和调用方传递过来的数组一样大的空间,并将该数组中的数据拷贝到新开辟的数组空间中去,其次对新申请一段空间的数组中的数据进行建堆,最后将记录堆中有效数据个数和记录堆容量的变量进行初始化即可。
void HeapInit(HP* php, HPDataTpye* a, int n)
{
assert(php);
php->a = (HPDataTpye*)malloc(sizeof(HPDataTpye)*n);
if (php->a == NULL)
{
printf("malloc fail\n");
exit(-1);
}
memcpy(php->a, a, sizeof(HPDataTpye)*n);
// 建堆
for (int i = (n-2)/2; i >= 0; --i)
{
AdjustDown(php->a, n, i);
}
php->size = n;
php->capacity = n;
}
堆的销毁
堆的销毁是将申请的数组空间释放掉并将指向这个数组的指针置空,最后将记录堆中有效数据个数和记录堆容量的变量置成0即可。
void HeapDestroy(HP* php)
{
assert(php);
free(php->a);
php->a = NULL;
php->size = php->capacity = 0;
}
堆的打印
堆的打印只需将数组中的数据循环打印一遍即可
void HeapPrint(HP* php)
{
for (int i = 0; i < php->size; ++i)
{
printf("%d ", php->a[i]);
}
printf("\n");
}
向上调整算法
先插入一个10到数组的尾上,再进行向上调整算法,直到满足堆。
向上调整算法是在大堆中如果该节点的大小比父亲要小就终止,如果比父亲大则需要和父亲进行交换,然后再向上迭代,直到调到根节点为止。
注:
void AdjustUp(int* a, int child)
{
int parent = (child - 1) / 2;
//while (parent >= 0) //当parent小于0终止调整 写这个条件是不对的 parent不会小于0(parent = (child - 1) / 2;),当parent=0时进入循环child=0,parent=0,会再次进入循环直到break跳出循环。写这个条件虽然结果是对的但是逻辑是错的。
while (child > 0)
{
if (a[child] < a[parent])
{
Swap(&a[child], &a[parent]);
child = parent;
parent = (child - 1) / 2;
}
else //孩子小于父亲(在大堆中)
{
break;
}
}
}
注意:可以利用向上调整算法可以建堆
给出一个数组,这个数组逻辑上可以看做一颗完全二叉树,但是还不是一个堆,现在可以通过算法,把它构建成一个堆。根节点左右子树不是堆,这时可以从根的左孩子节点(数组中的第二个元素)开始调整,一直调整到最后一个节点的树,就可以调整成堆(把第一个数看作一个堆,第二个数插入进去,前两个数就是堆了,依次类推)。
注:向上调整的前提是其他的数是个堆
堆的创建:
void HeapCreat(int* a, int n)
{
//建堆
for (int i = 1; i < n; ++i)
{
AdjustUp(a, i);
}
}
利用向上调整算法建堆的时间复杂度为O(N)。
堆的插入
堆的插入首先判断堆的存储空间是否已满,如果已满需要进行扩容,增容成功需要将堆的容量进行修改,然后插入数据(只需在数组尾部插入数据即可),再将记录堆中有效数据个数的变量进行++,最后再用向上调整算法将这个数组继续保持成堆即可。
注:堆和线性表是有所不同的,堆不是线性表,虽然堆的物理结构是数组(连续的、线性的),但堆的逻辑结构(实际中表示的东西)是二叉树。因此堆是不分头插、尾插的。
// 插入x,保持它继续是堆
void HeapPush(HP* php, HPDataTpye x)
{
assert(php);
if (php->size == php->capacity)
{
HPDataTpye* tmp = (HPDataTpye*)realloc(php->a, php->capacity * 2 * sizeof(HPDataTpye));
if (php->a == NULL)
{
printf("realloc fail\n");
exit(-1);
}
php->capacity *= 2;
}
php->a[php->size] = x;
php->size++;
AdjustUp(php->a, php->size - 1);
}
堆的删除
堆的删除是删除最大的数(在大堆中),把次大的找出来( 删除堆顶数据,删除后保持它继续是堆)。堆的删除可以先将数组中的第一个数据删除(将后面的数依次挪到前面来),再这些数重新建堆,不过时间复杂度为O(N),时间效率太低。更优的方式是首先把数组中的第一个和最后一个数进行交换,然后删除最后一个数,最后再使用向下调整算法继续保持堆即可。其中这个算法的时间复杂度为O( l o g N logN logN)。
删除堆是删除堆顶的数据,将堆顶的数据根最后一个数据一换,然后删除数组最后一个数据,再进行向下调整算法。
// 删除堆顶数据,删除后保持它继续是堆
void HeapPop(HP* php)
{
assert(php);
assert(!HeapEmpty(php));
Swap(&php->a[0], &php->a[php->size - 1]);
--php->size;
AdjustDown(php->a, php->size, 0);
}
获取堆顶的数据
获取堆顶的数据只需将数组中第一个元素返回即可
// 获取堆顶的数据,也就是最值
HPDataTpye HeapTop(HP* php)
{
assert(php);
assert(!HeapEmpty(php));
return php->a[0];
}
堆的判空
堆的判空只需判断堆中的有效数据个数是否为零,如果为零则返回真,否则返回假。
bool HeapEmpty(HP* php)
{
assert(php);
return php->size == 0;
}
获取堆中数据个数
获取堆中数据个数只需将记录堆中有效数据个数的变量返回即可
int HeapSize(HP* php)
{
assert(php);
return php->size;
}
堆排序即利用堆的思想来进行排序,总共分为两个步骤:
堆排序首先要建堆,其次记录数组中最后一个元素的位置,将选出来的最小(大)值所在的第一个位置和最后位置的数进行交换,紧接着将交换完之后的从根开始进行向下调整(将数组的个数进行减一从而达到不用将最后一个数算到堆中的目的,因为最小(大)的数已经排到最后了),从而选出次小(大)的数,然后将记录数组中最后一个元素的位置向前挪动一个位置,接着将选出的次小(大)的数的所在位置和记录数组位置中的数进行交换,依此类推循环往复直到记录数组下标的位置为0时终止。
注:
堆排序实现:
void Swap(int* px, int* py)
{
int tmp = *px;
*px = *py;
*py = tmp;
}
// 条件:左右子树都是小堆/大堆
void AdjustDown(int* a, int n, int parent)
{
int child = parent * 2 + 1;
while (child < n)
{
// 选出左右孩子中小(大)的那个
if (child+1 < n && a[child+1] > a[child])
{
++child;
}
// 1、如果小(大)的孩子比父亲小(大),则交换,继续往下调整
// 2、如果小(大)的孩子比父亲大(小),则结束调整
if (a[child] > a[parent])
{
Swap(&a[child], &a[parent]);
parent = child;
child = parent * 2 + 1;
}
else
{
break;
}
}
}
// 堆排序 -> 效率更高
// 堆排序的时间复杂度为O(N*logN)
void HeapSort(int* a, int n)
{
// 排升序->建大堆
// 排降序->建小堆
// 建堆的时间复杂度 O(N)
for (int i = (n-1-1)/2; i >= 0; --i)
{
AdjustDown(a, n, i);
}
// 向下调整算法(最多调整高度次) 时间复杂度为O(logN)
int end = n - 1;
while (end > 0)
{
Swap(&a[0], &a[end]);
AdjustDown(a, end, 0);
--end;
}
}
TOP-K问题(一般情况下K远小于N):即求数据集合中前K个最大的元素或者最小的元素,一般情况下数据量都比较大。
对于Top-K问题,能想到的方法有:
注:找数据集合中前K个最大的元素要建小堆,这是因为找最大的建大堆,有可能最大的数就放在这个堆里面,最大的数放在堆顶,而其他数(前K大的数)这些就进不来了,只能找到最大的,但是前K大的数就找不出来了,而建小堆的话,只要最大的前K个数里面的某一个数没进堆里面,那说明堆里面都放的是比这K个数小的数,而小堆最小的数是放到上面的,那最大的K个数来了以后,它肯定比堆顶的数大(这是因为当最大前K个数中的一些数没进堆,那么说明其他数在堆里面,堆中这些没进TOP-K的数肯定比TOP-K中没进堆的要小;当最大的前K个数中某一个来了以后,一定比堆顶的数据大,一定能进堆)。
对于Top-K问题,能想到的最简单直接的方式就是排序,但是:如果数据量非常大,排序就不太可取了(可能数据都不能一下子全部加载到内存中)。最佳的方式就是用堆来解决,基本思路如下:
用剩余的N-K个元素依次与堆顶元素来比较,不满足则替换堆顶元素
将剩余N-K个元素依次与堆顶元素比完之后,堆中剩余的K个元素就是所求的前K个最小或者最大的元素
TOP-K的实现:
void PrintTopK(int* a, int n, int k)
{
HP hp;
HeapInit(&hp, a, k);
for (int i = k; i < n; ++i)
{
if (a[i] > HeapTop(&hp))
{
HeapPop(&hp);
HeapPush(&hp, a[i]);
//还可以用另外一种方法:将堆顶的数据进行修改,再用向下调整算法保持堆即可
}
}
HeapPrint(&hp);
HeapDestroy(&hp);
}
二叉树的定义
typedef char BTDataType;
typedef struct BinaryTreeNode
{
BTDataType data;
struct BinaryTreeNode* left;
struct BinaryTreeNode* right;
}BTNode;
注:普通二叉树的增删查改是没有意义,因此开始不用学习增删查改,主要是学习它的结构。
二叉树:
注:从二叉树中可以看出,二叉树定义是递归式的,因此后序基本操作中基本都是按照该概念实现的。
二叉树的遍历有四种遍历形式:前序遍历(先根遍历)、中序遍历(中根遍历)、后序遍历、层序遍历
学习二叉树结构,最简单的方式就是遍历。所谓二叉树遍历(Traversal)是按照某种特定的规则,依次对二叉树中的节点进行相应的操作,并且每个节点只操作一次。访问结点所做的操作依赖于具体的应用问题。 遍历是二叉树上最重要的运算之一,也是二叉树上进行其它运算的基础。
按照规则,二叉树的遍历有:前序/中序/后序的递归结构遍历:
由于被访问的结点必是某子树的根,所以N(Node)、L(Left subtree)和R(Right subtree)又可解释为
根、根的左子树和根的右子树。NLR、LNR和LRN分别又称为先根遍历、中根遍历和后根遍历。
二叉树前序遍历
二叉树前序遍历首先如果这棵树为空直接返回(因为空树没法访问),如果这棵树不为空那么把这棵树分为三部分:根、左子树、右子树,而左子树和右子树又要分成根、左子树、右子树,依此类推,直到这棵子树是空时才终止。
void PreOrder(BTNode* root) {
if (root == NULL) {
printf("NULL "); //这里遇到空树也进行打印
return;
}
printf("%c ", root->_data);
PreOrder(root->_left);
PreOrder(root->_right);
}
前序遍历递归图解:
二叉树中序遍历
// 二叉树中序遍历
void InOrder(BTNode* root)
{
if (root == NULL) {
printf("NULL ");
return;
}
InOrder(root->_left);
printf("%c ", root->_data);
InOrder(root->_right);
}
二叉树后序遍历
// 二叉树后序遍历
void PostOrder(BTNode* root)
{
if (root == NULL) {
printf("NULL ");
return;
}
PostOrder(root->_left);
PostOrder(root->_right);
printf("%c ", root->_data);
}
注:递归不一定是分治,但是基本上都算是符合分治的思想的(大部分都是,有些地方不一定),基本上大部分的分治都可以用递归
层序遍历:除了先序遍历、中序遍历、后序遍历外,还可以对二叉树进行层序遍历。设二叉树的根节点所在层数为1,层序遍历就是从所在二叉树的根节点出发,首先访问第一层的树根节点,然后从左到右访问第2层上的节点,接着是第三层的节点,以此类推,自上而下,自左至右逐层访问树的结点的过程就是层序遍历。
层序遍历使用递归不方便遍历,使用队列来进行遍历,先把根放进队列,根出来的时候把左右孩子放到队列中去,接着上一层出来带下一层(当一层出完了下一层就都进去了),当队列为空层序遍历就结束了(核心思路是先入第一层根节点,上一层出来带入下一层,直到队列为空结束)。
// 层序遍历
void BinaryTreeLevelOrder(BTNode* root)
{
Queue q;
QueueInit(&q);
if (root)
{
QueuePush(&q, root);
}
while (!QueueEmpty(&q))
{
BTNode* front = QueueFront(&q);
QueuePop(&q);
printf("%c ", front->data);
if (front->left)
{
QueuePush(&q, front->left);
}
if (front->right)
{
QueuePush(&q, front->right);
}
}
printf("\n");
QueueDestory(&q);
}
注:前序(更严格的深度优先遍历)、后序、中序遍历都是深度优先遍历;层序遍历是广度优先遍历
二叉树节点个数
方法:
// 二叉树节点个数
// 1、遍历 -- 全局变量
int size = 0;
void BinaryTreeSize(BTNode* root)
{
if (root == NULL)
return;
else
++size;
BinaryTreeSize(root->left);
BinaryTreeSize(root->right);
}
// 2、遍历 -- 局部变量
void BinaryTreeSize(BTNode* root, int* psize)
{
if (root == NULL)
return;
else
++(*psize);
BinaryTreeSize(root->left, psize);
BinaryTreeSize(root->right, psize);
}
// 分治
int BinaryTreeSize(BTNode* root)
{
return root == NULL ? 0 : 1
+ BinaryTreeSize(root->left)
+ BinaryTreeSize(root->right); //利用二叉树后序递归的思想完成二叉树节点个数
}
二叉树叶子节点个数
二叉树叶子节点个数利用分治的思想,判断当前指针所指向的节点是否为空,如果为空直接返回0,如果当前节点是叶子节点那么返回1,否则把当前树分成根、左子树、右子树,那么当前树中的叶子节点的个数就是左、右子树的叶子节点个数之和。
// 二叉树叶子节点个数
int BinaryTreeLeafSize(BTNode* root)
{
if (root == NULL)
{
return 0;
}
else if (root->left == NULL && root->right == NULL)
{
return 1;
}
else
{
return BinaryTreeLeafSize(root->left)
+ BinaryTreeLeafSize(root->right);
}
}
二叉树第k层节点个数(k>=1)
判断当前指针所指向的节点是否为空,如果为空直接返回0,如果k为第一层时则直接返回1(因为当前节点正是所求层的节点),都不满足的情况下求当前树的第k层节点个数等于求左、右子树的第k-1层节点个数之和
// 二叉树第k层节点个数
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);
}
二叉树深度(高度)
判断当前指针所指向的节点是否为空,如果为空直接返回0,否则求当前二叉树的深度等于左、右子树的深度的较大值+1。
注:深度(高度):二叉树中最长的那条路径的节点个数
// 二叉树深度/高度
int BinaryTreeDepth(BTNode* root)
{
if (root == NULL)
{
return 0;
}
int leftDepth = BinaryTreeDepth(root->left);
int rightDepth = BinaryTreeDepth(root->right);
return leftDepth > rightDepth ? leftDepth + 1 : rightDepth + 1;
}
二叉树查找值为x的节点
二叉树查找值为x的节点首先判断当前指针所指向的节点是否为空,如果为空直接返回NULL,接着如果当前指针所指向的节点的值和所要找的x相等那么返回当前指针,然后先去左子树找,找到了就返回找到的节点的指针,如果左子树没找到,那么就去右子树找,如果找到了返回找到的节点的指针,最后还没找到返回空指针。
// 二叉树查找值为x的节点
BTNode* BinaryTreeFind(BTNode* root, BTDataType x)
{
if (root == NULL)
{
return NULL;
}
if (root->data == x)
{
return root;
}
BTNode* retLeft = BinaryTreeFind(root->left, x);
if (retLeft)
{
return retLeft;
}
BTNode* retRight = BinaryTreeFind(root->right, x);
if (retRight)
{
return retRight;
}
return NULL;
}
二叉树销毁
二叉树销毁是要把每个节点都释放掉,要递归销毁这棵树,判断当前指针所指向的节点是否为空,如果为空直接返回。(如果当前节点不是空,那么不建议销毁当前树,把当前节点销毁了,释放掉这个当前节点,这个节点里面存的内容就会被置成随机值也就找不到该节点的左、右节点,建议采取后序的方式销毁。)如果当前节点不是空,先销毁当前树的左子树,再去销毁当前树的右子树,最后销毁当前节点即可。
// 二叉树销毁
void BinaryTreeDestory(BTNode* root)
{
if (root == NULL)
{
return;
}
BinaryTreeDestory(root->left);
BinaryTreeDestory(root->right);
free(root);
}
注:
判断二叉树是否是完全二叉树
判断二叉树是否是完全二叉树,利用层序遍历把空也入队列,如果是完全二叉树那么非空是连续的、空也是连续的;如果不是完全二叉树那么非空不是连续的、空不连续。当出到空以后,队列中全是空就是完全二叉树;其中出队列的过程中如果有非空那么就不是完全二叉函数。
// 判断二叉树是否是完全二叉树
bool BinaryTreeComplete(BTNode* root)
{
Queue q;
QueueInit(&q);
if (root)
{
QueuePush(&q, root);
}
while (!QueueEmpty(&q))
{
BTNode* front = QueueFront(&q);
QueuePop(&q);
if (front == NULL)
{
break;
}
QueuePush(&q, front->left);
QueuePush(&q, front->right);
}
// 出到空,以后,队列中全是空,就是完全二叉树
// 还有非空,就不是完全二叉树
while (!QueueEmpty(&q))
{
BTNode* front = QueueFront(&q);
QueuePop(&q);
if (front)
{
QueueDestory(&q); //防止内存泄漏(return之前要把队列销毁掉)
return false;
}
}
QueueDestory(&q);
return true;
}