数据结构--二叉树

一、树

1.1 树的定义

树是一种非线性的数据结构,它是由有限的集合组成具有层次的集合。

是有一个特殊的节点:根节点(该节点之前在也没有节点)。

树是递归定义的。

树是由根和子树构成。

注意:树形结构中,子树之间不能有交集,否则就不是树形结构。

1.2 树的相关概念

  • 结点的度:一个结点含有的子树的个数称为该结点的度;
  • 叶结点或终端结点:度为0的结点称为叶结点;
  • 非终端结点或分支结点:度不为0的结点;
  • 双亲结点或父结点:若一个结点含有子结点,则这个结点称为其子结点的父结点;
  • 孩子结点或子结点:一个结点含有的子树的根结点称为该结点的子结点;
  • 兄弟结点:具有相同父结点的结点互称为兄弟结点;
  • 树的度:一棵树中,最大的结点的度称为树的度;
  • 结点的层次:从根开始定义起,根为第1层,根的子结点为第2层,以此类推;
  • 树的高度或深度:树中结点的最大层次;
  • 堂兄弟结点:双亲在同一层的结点互为堂兄弟;
  • 结点的祖先:从根到该结点所经分支上的所有结点;
  • 子孙:以某结点为根的子树中任一结点都称为该结点的子孙。
  • 森林:由m(m>0)棵互不相交的树的集合称为森林;

1.3 树的表示

树的表示既要表示父子之间关系,又要存储本节点的值,使用孩子兄弟表示法;


二、二叉树

2.1 二叉树的基础概念

二叉树是一个有限的节点的集合:可以为空,或者是有根节点和左子树和右子树。要求:必须节点的度要小于等于2,同时二叉树分左右子树,所以称二叉树又为有向树。

特殊的二叉树:完全二叉树满二叉树

完全二叉树:假设树的高度为h,h-1层都是满的,但是,h层并不是满的,最后一层必须是从左到右连续的。

满二叉树:在完全二叉树的基础上,h层叶结点也均是满的。

2.二叉树的性质

  1. 二叉树的根节点如果层次为1,那么第i层中,节点最多有2^(i-1)个节点
  2. 深度为h的二叉树,节点最多有2^h-1个。
  3. 在二叉树中,如果度为0的节点数为n0,度为1的节点数为n1,度为2的节点数为n2,则有以下关系n0=n2+1。
  4. 若规定根结点的层数为1,具有N个结点的满二叉树的深度,h=log2(N+1),(log以2为底,(N+1)为对数的等式)
  5. 对于具有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,无右孩子。

2.2 二叉树的存储方式

二叉树的存储方式有两种:数组存储和链式存储。

数组存储:

从上面图中就可以看出,普通的二叉树使用数组存储时就会空出空间,造成浪费,可以用,但是不适合,完全二叉树可以使用数组存储,根据访问规律就可以找到目标节点。

规律:

  • 如果父亲下标为i,则左孩子下标为(2*i+1),右孩子下标为(2*i+2);
  • 如果孩子(不论是左孩子还是右孩子)的下标为i,则父亲的下标为(i-1)/ 2

链式存储:

使用链表来进行存储,二叉链表中,要包含左孩子和右孩子的指针,同时要包含该节点的数值;三叉链表中在二叉链表的基础上,同时保存了父亲节点的指针。

2.3 二叉树数组存储的实现

2.3.1 堆的基础概念

二叉树中使用顺序存储的方式最好是完全二叉树/满二叉树。其中堆(完全二叉树)就是用数组存储的方式来存储数据。

堆分为大堆和小堆,大堆是指所有的父亲节点都要大于孩子节点,就是大堆;小堆是所有的父亲节点都要小于孩子节点。根据大堆和小堆的概念,我们就可以知道,大堆中根节点那个数一定是数组中最大的,小堆中根节点的那个数一定是最小的。但是需要注意的是兄弟节点之间是没有大小关系的。

2.3.2 堆的实现

堆是是用数组存储方式来存储(和顺序表定义的方式一模一样)。先定一个这样的结构体。

cs 复制代码
typedef int HPDataType;
typedef struct Heap
{
	HPDataType* arr;
	int capacity;
	int size;
}HP;

我们要实现的操作有:

  • 堆的初始化、销毁
  • 堆的插入数据
  • 堆的删除数据
  • 返回堆中根节点的数据
  • 判断堆是否为空
2.3.2.1 堆的初始化、销毁

这个堆的初始化和销毁和顺序表的代码一样。

cs 复制代码
void HeapInit(HP* ps)
{
	assert(ps);
	ps->arr = NULL;
	ps->size = ps->capacity = 0;
}
void HeapDestroy(HP* ps)
{
	assert(ps);
	free(ps->arr);
	ps->arr = NULL;
	ps->size = ps->capacity = 0;
}
2.3.2.2 堆的插入

既然顺着双亲调整到合适的位置,那么就需要一个算法向上调整。

插入数据后,拿数据和它的所有的祖先进行比较,如果孩子小,就往上调整,将孩子和祖先进行交换。

cs 复制代码
void swap(HPDataType* a, HPDataType* b)
{
	int tmp = *a;
	*a = *b;
	*b = tmp;
}
void AdaptUp(HPDataType* a, int child)
{
	assert(a);
	int parent = (child - 1) / 2;
	while (child > 0)
	{
		if (a[parent] <= a[child])
		{
			break;
		}
		swap(&a[parent], &a[child]);
		child = parent;
		parent = (child - 1) / 2;
	}
}
void HeapPush(HP* ps, HPDataType x)
{//假设这个堆为小堆
	
	assert(ps);
    assert(ps->size>0);
	//再插入之前,应先看看数组中有没有空间
	if (ps->size == ps->capacity)
	{
		//需要扩容
		int newcapacity = ps->capacity == 0 ? 4 : 2 * ps->capacity;
		HPDataType* tmp = (HPDataType*)realloc(ps->arr, sizeof(int) * newcapacity);
		if (tmp == NULL)
		{
			perror("HeapPush::realloc fail");
		}
		ps->arr = tmp;
		ps->capacity = newcapacity;
	}
	//插入数据
	ps->arr[ps->size++] = x;
	AdaptUp(ps->arr, ps->size-1);
}

向上调整的时间复杂度:一个节点最多向上调整的次数是高度次,也就是log2(N+1),所以时间复杂度为 o(logN)。


2.3.2.3 堆的删除

对于删除根节点的数据,我们应该将根节点数据和末尾的数据交换 一下位置,然后size-- ,这样就能删除数据,但是这样会破坏堆的结构,所以我们就需要做一个向下调整的算法。

向下调整算法中,先进行左右孩子的比较,找出最小者(可以利用假设法),然后再与父亲节点比较。

cs 复制代码
void AdaptDown(HPDataType* a, int parent,int n)
{
	assert(a);
	int child = (2 * parent )+ 1;
	
	while (child< n) 
	{
		//先比较左右孩子的大小
		if (child + 1 < n && a[child + 1] < a[child])
		{
			child++;
		}
		if (a[child] < a[parent])
		{
			swap(&a[child], &a[parent]);
			parent = child;
			child = (2 * parent) + 1;
		}
		else
		{
			break;
		}
	}
}
void HeapPop(HP* ps)
{
	assert(ps);
	//删除数据直接size--就可以了,但是这种删除没有意义
	//我们的目标是删除根节点的数据
	swap(&(ps->arr[0]), &(ps->arr[ps->size - 1]));
	ps->size--;
	AdaptDown(ps->arr, 0,ps->size);
}

向下调整的时间复杂度:向下调整最多的次数是高度次,也就是log2(N+1),所以时间复杂度为 o(logN)。


2.3.2.4 返回堆顶元素
cs 复制代码
HPDataType HeapTop(HP* ps)
{
	assert(ps);
	assert(ps->size > 0);
	return ps->arr[0];
}
2.3.2.5 判断堆是否为空
cs 复制代码
bool HeapEmpty(HP* ps)
{
	assert(ps);
	return ps->size == 0;
}

通过堆的实现我们就可以完成打印的堆排序,并不是真正的堆排序。

我们首先要建堆,使用堆这个数据结构来建堆。

然后取堆顶元素并打印,最后再进行堆删除的操作。

cs 复制代码
void test1()
{
	int a[] = { 3,5,8,9,7,12,58,74,15 };
	int len = sizeof(a) / sizeof(a[0]);
	HP hp = { 0 };
	HPInit(&hp);
	//先是一个堆,建堆
	for (int i = 0; i < len; i++)
	{
		HPPush(&hp, a[i]);
	}
	//打印堆排序(并不是真正意义上的堆排序)
	while(!HPEmpty(&hp))
	{
		int ret = HPTop(&hp);
		printf("%d ", ret);
		HPPop(&hp);
	}
	HPDestroy(&hp);
}

这种方式是在堆这个结构体里实现的排序,并且并不是真正的排序(数组里的元素并没有改变)。下面👇将要讲一下真正的排序。


2.3.3 堆的应用

升序:建大堆;降序:建小堆

以降序建小堆为例,整体思路是这样的:我不从前往后排,我是从后往前排,先选出最小的数,然后利用堆删除的思路(当然不是真删除,而是将不管那个数了),将最大数与末尾数进行交换,交换之后就不用管他了,只需要管前N-1个数,再调整一次,找到次大的数,再放到次末尾。最后按降序排出。升序建大堆的道理也是相通的。

建堆:

建堆有两种方法:向下调整建堆和向上调整建堆

  • 向上调整建堆:将首元素就作为一个堆,然后将数组之后的每个元素都进行向上调整,最后建成堆。

    cs 复制代码
    void test3()
    {
    	//建堆
    	int a[] = { 1,2,4,5,7,99,54,58,78 };
    	int len = sizeof(a) / sizeof(a[0]);
    	//向上调整建堆
    	for (int i = 1; i < len; i++)
    	{
    		AdjustUp(a, a[i]);
    	}
    }

    我们来讨论一下这个建堆的时间复杂度。 通过计算,向上调整算法建堆的时间复杂度为o(nlogn)。

  • 向下调整算法:向下调整要从倒数第一个非叶子节点开始进行向下调整。直到最后一个根节点停止。

    cs 复制代码
    void test3()
    {
    	//建堆
    	int a[] = { 3,5,8,9,7,12,58,74,15 };
    	int len = sizeof(a) / sizeof(a[0]);
    	//向下调整建堆
    	for (int i = (len - 1 - 1) / 2; i >= 0; i--)
    	{
    		AdjustDown(a, i, len);
    	}
    }

    我们来看看它的时间复杂度。 通过计算,向下调整算法建堆的时间复杂度为o(N)。

  • 通过二者比较可以看出向下调整算法比向上调整算法的更加效率高。所以我们建堆就可以使用向下调整算法。

建堆以后,我们就可以进行排序。

cs 复制代码
void HeapSort(int* arr, int n)
{
	int end = n - 1;
	//建堆
	for (int i = (end - 1) / 2; i >= 0; i--)
	{
		AdjustDown(arr, i, n);
	}
	//排序
	while (end > 0)
	{
		swap(&arr[0], &arr[end]);
		AdjustDown(arr, 0, end);
		end--;
	}
}

Top-K问题

再一串数组中,找出最大或最小的前k个数。

下面以找出最小的k个数为例:

cs 复制代码
void Topkmin(int* arr, int len, int k)
{
	//建堆
	for (int i = (len - 1 - 1) / 2; i >= 0; i--)
	{
		AdjustDown(arr, i, len);
	}
	int end = len - 1;
	while (k--)
	{
		swap(&arr[0], &arr[end]);
		printf("%d ", arr[end]);
		AdjustDown(arr, 0, end);
		end--;
	}
}
void test4()
{
	int a[] = { 3,5,8,9,7,12,58,74,15 };
	int len = sizeof(a) / sizeof(a[0]);
	
	int k = 0;
	scanf("%d", &k);
	Topkmin(a, len, k);
}

这种方法能行,当N>>k时,运行效率为O(N)。但是这个有一个致命的缺点。

n个数求k个最大的数:

思路:n个元素全部在文件中,我们取文件中数据的前k个,对这k个进行建小堆,拿堆顶元素和其他(n-k)个数据进行比较,如果若还有比堆顶元素还大的,就进行交换。这样前k个元素就都是比其他(n-k)个数大的了,就找出k个最大的数。

cs 复制代码
#include <stdlib.h>
#include <stdio.h>
#include <time.h>
//自己造的数据
void Createdata()
{
	FILE* pf = fopen("data.txt", "w");
	if (pf == NULL)
	{
		perror("fopen fail");
		exit(1);
	}
	for (int i = 0; i < 10000; i++)
	{
		int data = (rand()+i)%10000;
		fprintf(pf, "%d\n", data);
	}
	fclose(pf);
	pf = NULL;
	
}

void test5()
{
    //打开文件
	FILE* pf = fopen("data.txt", "r");
	if (pf == NULL)
	{
		perror("fopen fail");
		exit(1);
	}
	
	int k = 0;
	scanf("%d", &k);
	int* a = (int*)malloc(sizeof(int) * k);
	for (int j = 0; j < k; j++)
	{
		fscanf(pf, "%d", &a[j]);
	}
	//建小堆
	for (int i = (k - 2) / 2; i >= 0; i--)
	{
		AdjustDown(a, i, k);
	}
	int ret = 0;
	for (int i = k; i <= 10000; i++)
	{
		fscanf(pf, "%d", &ret);
        //比较,然后向下调整
		if (a[0] < ret)
		{
			swap(&a[0], &ret);
			AdjustDown(a, 0, k);
		}
	}
    //关闭文件
	fclose(pf);
	pf = NULL;
    //打印k个最大的数
	for (int i = 0; i < k; i++)
	{
		printf("%d ", a[i]);
	}
}
int main()
{
	srand((unsigned int)time(NULL));
	Createdata();
	test5();
	return 0;
}

2.4 二叉树的链式存储

在学习二叉树的操作时,我们首先要创建一个二叉树,我们可以首先使用最笨拙的方式:自己来创建节点,并且建立节点之间的联系。

cs 复制代码
typedef int BTDataType;
typedef struct BinaryTreeNode
{
	BTDataType val;
	struct BinaryTreeNode* left;
	struct BinaryTreeNode* right;
}BTNode;
BTNode* BuyNode(BTDataType x)
{
	BTNode* ret = (BTNode*)malloc(sizeof(BTNode));
	ret->val = x;
	ret->left = ret->right = NULL;
	return ret;
}
BTNode* CreateBinaryTree()
{
	BTNode* node1 = BuyNode(1);
	BTNode* node2 = BuyNode(2);
	BTNode* node3 = BuyNode(3);
	BTNode* node4 = BuyNode(4);
	BTNode* node5 = BuyNode(5);
	BTNode* node6 = BuyNode(6);
	BTNode* node8 = BuyNode(8);
	BTNode* node7 = BuyNode(7);

	node1->left = node2;
	node1->right = node3;
	node2->left = node4;
	node2->right = node5;
	node3->left = node6;
	node3->right = node7;
	node5->right = node8;
    return node1;
}
int main()
{
	BTNode* root=CreateBinaryTree();
	return 0;
}

2.4.1 二叉树的遍历

递归遍历

递归遍历分为前序遍历、中序遍历、后序遍历。

  • 前序遍历:遍历顺序:根、左子树、右子树。(DFS - > 深度优先遍历)

以上面二叉树为例:

首先从根开始,然后遍历左子树,而左子树又可以看成一颗完整的树,所以继续被分为根、左子树和右子树。由大问题不断分割成小问题,不断地逼近限制条件(根为空指针)。

cs 复制代码
//前序遍历
void preOrder(BTNode* root)
{
	if (root == NULL)
	{
		printf("N ");
		return;
	}
	printf("%d ", root->val);
	preOrder(root->left);
	preOrder(root->right);
}
  • 中序遍历:遍历顺序:左子树、根、右子树。

以上面二叉树为例:

cs 复制代码
//中序遍历
void inOrder(BTNode* root)
{
	if (root == NULL)
	{
		printf("N ");
		return;
	}
	inOrder(root->left);
	printf("%d ", root->val);
	inOrder(root->right);
}
  • 后序遍历:遍历顺序:左子树、右子树、根

以上面的二叉树为例:

cs 复制代码
//后序遍历
void postOrder(BTNode* root)
{
	if (root == NULL)
	{
		printf("N ");
		return;
	}
	postOrder(root->left);
	postOrder(root->right);
	printf("%d ", root->val);
}

非递归遍历

层序遍历就属于非递归遍历。

  • 层序遍历:(BFS - > 广度优先遍历)

层序遍历结合了队列进行遍历。

队列的代码可以看一下文章:栈和队列

首先先将根节点入队,然后出根节点,同时将它的左右孩子依次入队。按照上一层带动下一层的思想,进行考虑。同时空指针是不需要入队的。

cs 复制代码
void LevelOrder(BTNode* root)
{
	//队列中的节点元素类型为指针
	Q q;
	//队列初始化
	QInit(&q);
	//根节点不为空才能入队列
	if (root)
	{
		QueuePush(&q, root);
	}
	
	while (!QIsEmpty(&q))
	{
		BTNode* top = QueueTop(&q);
		QueuePop(&q);
		printf("%d ", top->val);
		//不将空指针放入队列中
		if (top->left)
		{
			QueuePush(&q, top->left);
		}
		if (top->right)
		{
			QueuePush(&q, top->right);
		}
	}
	//队列的销毁
	QDestroy(&q);
}

2.4.2 二叉树中的基础操作

  • 求节点个数

求节点个数我们可以使用递归的方式,分为两种情况:根节点为空时,那么节点的个数为0;如果根节不为空,那么节点个数就为左子树的节点个数加上右子树节点的个数+1。

再想为啥是递归呢?

因为求左子树的节点个数又可以将其看成一个子树,再求其左子树的节点个数+右子树的节点个数+1。因此这是相似的问题,又是将大问题 逐渐转化为小问题。

cs 复制代码
//求二叉树节点个数
int BinaryTreeSize(BTNode* root)
{
	if (root == NULL)
	{
		return 0;
	}
	return BinaryTreeSize(root->left) + BinaryTreeSize(root->right)+ 1;
}

  • 求二叉树的高度

求二叉树的高度,我们求的高度一定是左子树和右子树之间中最大的高度+1。

分为以下几种情况:

1.根节点为空:那么就返回0;

2.根节点不为空:就返回左右子树最大的那个树的高度+1;

这里需要考虑的是:我们能直接返回一个三目表达式(比较左右子树的高度大小)吗?(如下)

cs 复制代码
int BinaryTreeHeight(BTNode* root)
{
	if (root == NULL)
	{
		return 0;
	}
	// 重复递归计算左右子树高度,大量冗余计算,时间复杂度爆炸
	return BinaryTreeHeight(root->left) > BinaryTreeHeight(root->right) 
		? BinaryTreeHeight(root->left) + 1 
		: BinaryTreeHeight(root->right) + 1;
}

答案是不能的,因为假设我们的树的高度只有2层,比较一下左右子树的大小,会进行递归调用,但是算出最大的之后+1,由于没有存数据就会导致重新递归,会出现效率问题。当二叉树的深度足够深时,就会超出时间限制。

cs 复制代码
//求二叉树的高度
int BinaryTreeHeight(BTNode* root)
{
	if (root == NULL)
	{
		return 0;
	}
	int leftHeight = BinaryTreeHeight(root->left);
	int rightHeight = BinaryTreeHeight(root->right);
	return leftHeight > rightHeight ? leftHeight + 1 : rightHeight + 1;
}

  • 求二叉树的叶子节点个数

求叶子节点个数:分为以下几种情况:

1.空树:返回0;

2.非空树:如果左右孩子都是空指针,说明为叶子节点,返回1。

如果以上都不满足,那么就继续往下递归,返回左右子树的叶子节点个数。

cs 复制代码
//求叶子节点的个数
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 层的节点个数

二叉树的问题想成根和子树的问题。

求第 k 层的节点个数,那么也就是求左子树相对于原二叉树 第 k 层的节点个数和右子树相对于原二叉树第 k 层节点个数之和。而求左 / 右子树相对于原二叉树第 k 层的节点个数相当于求左 / 右子树第 k-1 层的节点个数。逐渐缩小范围,靠近限制条件:k=1时,节点个数就是1;空树:节点个数就是0。

思路:

1.空树:返回0;

2.非空树但k=1:返回1

3.k > 1 :返回左子树和右子树第 k-1层的节点个数。

cs 复制代码
// 二叉树第k层结点个数
int BinaryTreeLevelKSize(BTNode* root, int k)
{
	if (root == NULL)
	{
		return 0;
	}
	if (k == 1)
	{
		return 1;
	}
	if (k > 1)
	{
		return BinaryTreeLevelKSize(root->left, k-1) + BinaryTreeLevelKSize(root->right, k-1);
	}
}

  • 二叉树查找值为x的节点
cs 复制代码
BTNode* BTNodeFind(BTNode* root, BTDataType x)
{
    //空树就返回NULL
	if (root == NULL)
	{
		return NULL;
	}
    //节点值相等就返回这个节点指针
	if (root->val == x)
	{
		return root;
	}
	//不为空树根节点值也不相等,看其左右子树
	//如果能找到那么返回值不为NULL
	BTNode* ret1=BTNodeFind(root->left, x);
	if (ret1)
	{
		return ret1;
	}
	BTNode* ret2 = BTNodeFind(root->right, x);
	if (ret2)
	{
		return ret2;
	}
	//如果都不满足,那么就找不到,返回NULL
	return NULL;
}

2.4.3 二叉树的基础oj题

cs 复制代码
bool _isUnivalTree(struct TreeNode* root, int val)
{
    if (root == NULL)
    {
        return true;
    }
    if (root->val != val)
    {
        return false;
    }
    return _isUnivalTree(root->left, val) && _isUnivalTree(root->right, val);
}
bool isUnivalTree(struct TreeNode* root) {
    return _isUnivalTree(root, root->val);
}
cs 复制代码
bool isSameTree(struct TreeNode* p, struct TreeNode* q) {
    if(p==NULL && q==NULL)
    {
        return true;
    }
    if (p==NULL || q==NULL)
    {
        return false;
    }
    if(p->val!=q->val)
    {
        return false;
    }
    return isSameTree(p->left,q->left) && isSameTree(p->right,q->right);
}
cs 复制代码
 int TreeSize(struct TreeNode* root)
 {
    return root==NULL? 0 :TreeSize(root->left) + TreeSize(root->right) + 1;
 }
 void preOrder(struct TreeNode* root,int* a,int* pi)
 {
    if(root==NULL)
    {
        return;
    }
    a[(*pi)++]=root->val;
    preOrder(root->left,a,pi);
    preOrder(root->right,a,pi);
 }
int* preorderTraversal(struct TreeNode* root, int* returnSize) {
    int size=TreeSize(root);
    *returnSize=size;
    int* a=(int* )malloc(sizeof(int)*(*returnSize));
    int i=0;
    preOrder(root,a,&i);
    return a;
}

需要注意的两点:

  1. 在oj题中,returnSize是指向数组大小的指针,也叫输出型参数。
  2. 在前序遍历时,要将值放入数组中,需要传数组大小的参数,但是不能传值,因为在一个函数栈帧中 i 值的改变并不能改变其他函数栈帧中的值。
cs 复制代码
bool _isSymmetric(struct TreeNode* p,struct TreeNode* q)
{
    if(p==NULL && q==NULL)
    {
        return true;
    }
    if(p==NULL || q==NULL)
    {
        return false;
    }
    if(p->val != q->val)
    {
        return false;
    }
    return _isSymmetric(p->left,q->right) && _isSymmetric(p->right,q->left);
}
bool isSymmetric(struct TreeNode* root) {
    if(root ==NULL)
    {
        return true;
    }
    return _isSymmetric(root->left,root->right);
}
cs 复制代码
bool IsSameTree(struct TreeNode* p,struct TreeNode* q)
{
    if(p==NULL && q==NULL)
    {
        return true;
    }
    if(p==NULL || q==NULL)
    {
        return false;
    } 
    if(p->val !=q->val)
    {
        return false;
    }
    return IsSameTree(p->left,q->left) && IsSameTree(p->right,q->right);
}
bool isSubtree(struct TreeNode* root, struct TreeNode* subRoot) {
    if(root==NULL && subRoot==NULL)
    {
        return true;
    }
    if(root==NULL || subRoot==NULL)
    {
        return false;
    }
    if(root->val == subRoot->val)
    {
        if(IsSameTree(root,subRoot))
        {
            return true;
        }    
    }
    return isSubtree(root->left,subRoot) || isSubtree(root->right,subRoot);
}

2.4.4 二叉树的创建和销毁

  • 判断是否为完全二叉树

首先要想完全二叉树的特点:前 h-1 层为满的二叉树,最后一层节点数可能满,也可能不满,但是节点一定从左到右是连续的。

我们怎么样才能知道从左到右是连续的呢?

当说到从左到右,我们可以想到层序遍历(它就是每一层从左到右遍历),当我们遍历到第一个空时,分为两种情况:

1.队列中全部是空,没有非空节点,那么这就是完全二叉树。

2.队列中存在非空节点,那么就不是非空节点。

  • 是否存在已经遍历到第一个空节点,但是下一层还有的非空节点并没有入队列?

答案是不存在的。因为第一个空节点后面的非空节点的根节点一定是空节点之前的非空节点,已经遍历到空节点,说明前面的非空节点已经出队列了,而出队列必定他们下一层的孩子就已经入队列里了。所以不存在。

cs 复制代码
// 判断二叉树是否是完全二叉树
bool BinaryTreeComplete(BTNode* root)
{
	Q q;
	QInit(&q);
	QueuePush(&q, root);
	while (!QIsEmpty(&q))
	{
		BTNode* top = QueueTop(&q);
		QueuePop(&q);
		//遇到第一个空就停止,如果队列中还有非空节点,那么就不是完全二叉树
		if (top == NULL)
		{
			break;
		}
		QueuePush(&q,top->left);
		QueuePush(&q,top->right);
	}
	while (!QIsEmpty(&q))
	{
		BTNode* top = QueueTop(&q);
		QueuePop(&q);
		if (top != NULL)
		{
			QDestroy(&q);
			return false;
		}
	}
	QDestroy(&q);
	return true;
}

cs 复制代码
#include <stdio.h>
#include <stdlib.h>
typedef char BTDateType;
typedef struct BinaryTreeNode
{
    BTDateType val;
    struct BinaryTreeNode* left;
    struct BinaryTreeNode* right;
}BTNode;

void inOrder(BTNode* root)
{
    if(root ==NULL)
    {
        return;
    }
    inOrder(root->left);
    printf("%c ",root->val);
    inOrder(root->right);
}
BTNode* CreateBinaryTree(BTDateType* a,int* pi)
{
    if(a[*pi] == '#')
    {
        (*pi)++;
        return NULL;
    }
    BTNode* root=(BTNode*)malloc(sizeof(BTNode));
    root->val=a[*pi];
    (*pi)++;
    root->left=CreateBinaryTree(a, pi);
    root->right=CreateBinaryTree(a, pi);
    return root;
}
int main() {
    char a[100];
    //输入
    scanf("%s",a);
    //输出
    //1.前序遍历的方法建立二叉树
    int i=0;
    BTNode* root=CreateBinaryTree(a,&i);
    //2.中序遍历
    inOrder(root);
    return 0;
}

这里和前序遍历一样,一定要注意穿过数组的大小,必须是是指针控制,否则无效。


  • 二叉树的销毁
cs 复制代码
//二叉树的销毁
void BinaryTreeDestroy(BTNode* root)
{
	if (root == NULL)
	{
		return;
	}
	BinaryTreeDestroy(root->left);
	BinaryTreeDestroy(root->right);
	free(root);
}

三、 总结

这节花费了好长时间整理,收藏起来,多看几遍,每一遍都会有不一样的收获!如果有不对的地方,请及时指出,我会及时更正。

相关推荐
不如语冰8 小时前
AI大模型入门-参数的传递
数据结构·人工智能·pytorch·python
流浪0019 小时前
数据结构篇(四):线性表——链表——双链表
数据结构·链表
小七在进步10 小时前
数据结构:堆排序
数据结构
2401_841495641 天前
【数据结构】B+树
数据结构·数据库·c++·b+树·概念·结构·操作原理
皓月斯语1 天前
【C++基础】三目运算符
开发语言·数据结构·c++
剑锋所指,所向披靡!1 天前
数据结构之拓扑排序
数据结构
夜不会漫长1 天前
数据结构:链表
数据结构·链表
剑锋所指,所向披靡!1 天前
数据结构之关键路径
数据结构·算法
笨笨饿1 天前
101_详解USB协议
java·jvm·数据结构