请回答数据结构【二叉树和堆(下)】

BingWallpaper22

image-20220415131736757

1. Before BinaryTree

二叉树操作回顾二叉树的结构表示和概念

1.0 二叉树结构表示(二叉链)

image-20211127105248267

图片来源于Crash Course Computer Science

1.1 二叉树的概念

二叉树是:

  1. 空树
  2. 非空:根节点,根节点的左子树、根节点的右子树组成的。

image-20220331142344416

1.2 二叉树有什么不同?

之前的数据结构往往注重增删查改,然而实际上普通二叉树的增删查改意义不大,单纯存储数据不如线性表,那二叉树的意义是什么呢?

二叉树加上算法可以生成哈夫曼树,产生的哈夫曼编码可以用于文件压缩
二叉树真正有意义的是搜索,也就是产生搜索树
这样的树可能就是左边比父亲小,右边比父亲大

binary search

找一个节点遍历的次数最多就只有高度次,效率很高

但是极端情况下效率退化成O(N),那么还是和链表差不多,怎么解决呢?于是产生了平衡树,也就是红黑树和AVL树

2. 二叉树的遍历

首先面对二叉树,我们的思考往往要认为是递归结构

脑子里的编译器就是把一棵树要分成三个部分,根,左子树,右子树,每个子树又可以分为根,左子树,右子树…

二叉树结构,最简单的方式就是遍历。所谓二叉树遍历(Traversal)是按照某种特定的规则,依次对二叉
树中的节点进行相应的操作,并且每个节点只操作一次。访问结点所做的操作依赖于具体的应用问题。 遍历是二叉树上最重要的运算之一,也是二叉树上进行其它运算的基础。

2.1 四种遍历顺序:

按照规则,二叉树的遍历有:前序/中序/后序的递归结构遍历:

  1. 前序遍历(Preorder Traversal 亦称先序遍历)——访问根结点的操作发生在遍历其左右子树之前。
  2. 中序遍历(Inorder Traversal)——访问根结点的操作发生在遍历其左右子树之中(间)。
  3. 后序遍历(Postorder Traversal)——访问根结点的操作发生在遍历其左右子树之后。
遍历方式遍历顺序
前序遍历也叫先根遍历根->左子树->右子树
中序遍历也叫中根遍历左子树->根->右子树
后序遍历也叫后根遍历左子树->右子树->根
层序遍历h=1,2,3,4…

2.2 诸如此例

比如说像下面这样一个树

image-20220409213623639

前序遍历A->B->D->NULL->NULL->NULL->C->E->NULL->NULL->F->NULL->NULL
中根遍历NULL->D->NULL->B->NULL->A->NULL->E->NULL->C->NULL->F->NULL
后根遍历NULL->NULL->D->NULL->B->NULL->NULL->E->NULL->NULL->F->C->A

2.3 前序的递归解释:根->左子树->右子树

image-20220409213946333

2.4 前序遍历图解

image-20220409215714067

2.5 代码实现

2.5.1 实现之前

学习二叉树的基本操作前,需先要创建一棵二叉树,此处采用手动快速创建一棵简单的二叉树,快速进入二叉树操作

typedef int BTDataType;

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

BTNode* BuyBTNode(BTDataType x)
{
	BTNode* node = (BTNode*)malloc(sizeof(BTNode));
	if (node == NULL)
	{
		printf("malloc fail\n");
		exit(-1);
	}

	node->data = x;
	node->left = node->right = NULL;
	return node;
}

BTNode* CreatBinaryTree()
{
	BTNode* node1 = BuyBTNode(1);
	BTNode* node2 = BuyBTNode(2);
	BTNode* node3 = BuyBTNode(3);
	BTNode* node4 = BuyBTNode(4);
	BTNode* node5 = BuyBTNode(5);
	BTNode* node6 = BuyBTNode(6);

	node1->left = node2;
	node1->right = node4;
	node2->left = node3;
	node4->left = node5;
	node4->right = node6;

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

思考调用递归的过程

image-20220409215754069

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

这是刚刚手动构造的树得出的结果

前序遍历结果:1 2 3 4 5 6
中序遍历结果:3 2 1 5 4 6
后序遍历结果:3 1 5 6 4 1

这和之前诸如此例走的是一模一样的

image-20211125165041666

2.5.5 TreeSize

下面的问题都涉及到了使用一种思想,也就是分治思想,将一个复杂问题,把它分成规模更小的问题,子问题再分成问题更小规模的子问题,直到子问题不能再分割,直接能出结果

image-20211127110444583

分治的思想就是,一级一级让下一级的处理事务,然后上报上级,而不是上级直接找下级,正如上图的分级,当每一级的附属部门小于等于两个时,就是一个二叉树问题

2.5.5.1 想法一
//想法1
void BTreeSize(BTNode* root)
{
	int count = 0;
	if (root == NULL)
		return ;

	++count;
	BTreeSize(root->left);
	BTreeSize(root->right);
}

递归有很多的栈帧,导致每一个count都可能是不一样的,不可行

image-20220410095132567

2.5.5.2 想法二
//想法2
 int count = 0;
void BTreeSize(BTNode* root)
{
	if (root == NULL)
		return count;

	++count;
	BTreeSize(root->left);
	BTreeSize(root->right);
}

全局变量不是存在栈帧上的,是存在静态区的,所以不会产生之前的问题

2.5.5.3 想法三
//想法3
int BTreeSize(BTNode* root)
{
	static int count = 0;
	if (root == NULL)
		return count;

	++count;
	BTreeSize(root->left);
	BTreeSize(root->right);

	return count;
}

静态变量需要添加返回值,其实和全局变量类似

然而这两者其实有个最大的通病就是静态变量的初始化只初始化一次

全局变量倒是可以在main函数中调用一次初始化一次,但是都是有线程安全问题,所以不好

capture_20220410094856326

2.5.5.3 想法四

传地址就可以了

//想法四
//思想:遍历 + 计数
void BTreeSize(BTNode * root, int* pCount)
{
	if (root == NULL)
		return;

	++(*pCount);
	BTreeSize(root->left, pCount);
	BTreeSize(root->right, pCount);
}
2.5.5.4 想法五
int BTreeSize(BTNode* root) {
	return root == NULL ? 0 :
		BTreeSize(root->left)
		+ BTreeSize(root->right) + 1;
}

思路还是划归为子问题

空树,最小规模子问题,节点个数返回0

非空,左子树节点个数+右子树节点个数+1(自己)

2.5.6 BTreeLeafSize
return 0
自己是叶子节点return 1
都不是return 左子树叶子节点+右子树叶子节点
int BTreeLeafSize(BTNode* root)
{
	if (root == NULL)
		return 0;

	if (root->left == NULL && root->right == NULL)
		return 1;

	return BTreeLeafSize(root->left) + BTreeLeafSize(root->right);
}
2.5.7 BTreeKLevelSize

求第k层的节点的个数,k >= 1

空树返回0
非空,且k==1返回1
非空且k>1继续往下走往左边和右边递归加起来计算总数
int BTreeKLevelSize(BTNode* root, int k)
{
	assert(k >= 1);

	if (root == NULL)
		return 0;

	if (k == 1)
		return 1;

	return BTreeKLevelSize(root->left, k - 1)
		+ BTreeKLevelSize(root->right, k - 1);
}
2.5.8 BTreeDepth

获取左右子树中深度更深的子树,然后返回最大值即可

int BTreeDepth(BTNode* root)
{
	if (root == NULL)
		return 0;

	int leftDepth = BTreeDepth(root->left);
	int rightDepth = BTreeDepth(root->right);

	return leftDepth > rightDepth ? leftDepth + 1 : rightDepth + 1;
}
2.5.9 BTreeFind
空树return NULL
要找的不是根节点往左树递归
左边没找到往右树递归左边找到了return 左子树
右边也没找到,返回NULL,然后往上走重复判断右边找到了return 右子树
// 二叉树查找值为x的结点
BTNode* BTreeFind(BTNode* root, BTDataType x)
{
	if (root == NULL)
		return NULL;

	if (root->data == x)
		return root;

	BTNode* ret1 = BTreeFind(root->left, x);
	if (ret1)
		return ret1;

	//return BTreeFind(root->right, x);
	BTNode* ret2 = BTreeFind(root->right, x);
	if (ret2)
		return ret2;

	return NULL;
}
2.5.10 BTreeDestory

可以看成后序删除相当于先把child删除再删除根

// 二叉树销毁
void BTreeDestory(BTNode* root)
{
	if (root == NULL)
	{
		return;
	}
	BTreeDestory(root->left);
	BTreeDestory(root->right);
	free(root);
}

image-20211127161010731

但是还有一个问题

image-20211127161437900

注意A还是野指针,所以要置空,这里有两种方式,要么在主函数置空,要么就函数传二级指针来置空

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

image-20211127163107445

不过为了接口一致性还是用第一种方法

也可以换成cpp用&

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

2.6 二叉树的深度优先遍历与广度优先遍历

2.6.1 深度优先DFS

前序遍历属于深度优先遍历,是以走深度为主,如果无法再深了就回到之前

扫雷在自动展开的时候就是深度优先遍历

2.6.2 广度优先BFS

Binary Tree Zigzag Level Order Traversal at Level

一层一层走,一般借助于队列来实现,比如说层序遍历,比如如图结构的遍历

  1. 先把根入队列,借助队列先进先出的性质

  2. 出队头的数据,把他的下一层传进去

image-20220415101325032

特点:是队列先进先出的性质,然后上一层出的时候,带入下一层

注意在头文件里面,要包含一个前置声明把队列中的数据存放为二叉树节点

//前置声明
struct BinaryTreeNode;
typedef struct BinaryTreeNode* QDataType;
typedef struct BinaryTreeNode
{
	struct BinaryTreeNode* left;
	struct BinaryTreeNode* right;
	BTDataType data;
}BTNode;
typedef struct BinaryTreeNode* QDataType;
typedef struct QueueNode
{
	QDataType data;
	struct QueueNode* next;
}QNode;
typedef struct Queue
{
	QNode* head;
	QNode* tail;
}Queue;

二叉树的广度优先遍历

// 层序遍历
void LevelOrder(BTNode* root)
{
	Queue q;
	QueueInit(&q);

	if (root)
	{
		QueuePush(&q, root);
	}

	while (!QueueEmpty(&q))
	{
		BTNode* front = QueueFront(&q);
		QueuePop(&q);

		printf("%d ", front->data);
		if (front->left)
		{
			QueuePush(&q, front->left);
		}

		if (front->right)
		{
			QueuePush(&q, front->right);
		}
	}

	printf("\n");
	QueueDestory(&q);
}

用纯C写确实非常复杂,还要自己写一个Queue,下面用cpp写一个试试看

#include <iostream>
#include<queue>
using std::queue;
using std::cout;
using std::endl;
namespace allen
{
	void LevelOrder(BTNode*  root)
	{
		queue<BTNode*> q;
		//若根节点不为空,放入根
		if (root)
		{
			q.push(root);
		}
		while (! q.empty())//依次拿出根的左右孩子,并cout
		{
			BTNode* front = q.front();
			q.pop();//删除指向节点的指针
			cout << front->data<<" ";
			
			//放入左
			if (front->left)
			{
				q.push(front->left);
			}
			
			//放入右
			if (front->right)
			{
				q.push(front->right);
			}
		}
		cout << endl;
	}
}

2.6.3 BTreeComplete判断一颗树是不是完全二叉树

借助层序遍历的原理,来判断是不是完全二叉树

如果是层序排列走的话,那么节点应该是连续的,此时我们考虑空节点这次也进队列

思考一下,如果是一个完全二叉树的话,当空节点从队列里面出来之后,队列剩下的应该是全空,如果后面还有节点,那么肯定说明不是完全二叉树

如果出了空后面还有节点,那么肯定不是完全二叉树

image-20211127204747123

如果最后出的全是空,那就是一个完全二叉树

image-20211127211618087

// 判断二叉树是否是完全二叉树
bool BTreeComplete(BTNode* root)
{
	Queue q;
	QueueInit(&q);

	if (root)
		QueuePush(&q, root);

	while (!QueueEmpty(&q))
	{
		BTNode* front = QueueFront(&q);
		QueuePop(&q);
		//如果发现该队列头是空的话,那么直接break
		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 false;
		}
	}

	QueueDestory(&q);
	return true;
}

3. 缮甲厉兵

1.给以下两种顺序可不可以由此来建一个唯一的树

顺序1顺序2可以/不可以建树
前序中序可以
后序中序可以
前序后序不可以

2. 一棵非空的二叉树的先序遍历序列与后序遍历序列正好相反,则该二叉树一定满足( )

  • 所有的结点均无左孩子

  • 所有的结点均无右孩子

  • 只有一个叶子结点

  • 至多只有一个结点

如果前序遍历和后序遍历序列正好相反,说明它是一个单边树,对于单边树,只有一个叶子节点。

3.如果一颗二叉树的前序遍历的结果是ABCD,则满足条件的不同的二叉树有( )种

  • 13
  • 14
  • 15
  • 16

首先这棵二叉树的高度一定在3~4层之间:

三层:

A(B(C,D),()), A((),B(C,D)), A(B(C,()),D), A(B((),C),D),

A(B,C(D,())), A(B,C((),D))

四层:

如果为四层,就是单边树,每一层只有一个节点,除过根节点,其他节点都有两种选择,在上层节点的左边还是右边,所以2*2*2共8种

总共为14种。

4. 已知某二叉树的前序遍历序列为5 7 4 9 6 2 1,中序遍历序列为4 7 5 6 9 1 2,则其后序遍历序列为( )

  • 4 2 5 7 6 9 1

  • 4 2 7 5 6 9 1

  • 4 7 6 1 2 9 5

  • 4 7 2 9 5 6 1

通过前序遍历找到子树的根,根据根元素在中序遍历的位置找到子树的左右区间。故:

根为: 5

5的左子树:4 7 5的右子树: 6 9 1 2

5的左子树的根为:7 5的右子树的根为:9

7的左子树: 4 7的右:空 9的左子树:6 9的右子树:2

故这棵树的结构为:

​ 5

​ / \

​ 7 9

​ / / \

​ 4 6 2

​ /

​ 1

后序遍历: 4 7 6 1 2 9 5

选择题做完了,再做几道编程题

入门二叉树-一起来递归【下】

入门二叉树-一起来递归【上】

最后给到我的代码仓库,有关代码已经放入仓库https://gitee.com/allen9012/c-language/tree/master/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/BinaryTree3

  • 3
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
回答: 数据结构中的和栈与操作系统中的和栈有一些区别。在数据结构中,和栈是两种不同的数据结构。栈是一种线性结构,可以使用数组或链表实现,其中元素的地址连续。而是一种树形结构,是一种特殊的完全二叉树,其中根节点是最大(或最小)节点。的存储一般使用数组来实现。\[1\]\[2\] 而在操作系统中,和栈是用来管理内存的两个重要概念。栈是一种后进先出(LIFO)的数据结构,用于存储函数调用的局部变量、函数参数和返回地址等信息。栈的大小是固定的,由操作系统在程序运行时分配和管理。而是用于动态分配内存的一块区域,用于存储程序运行时动态创建的对象和数据。的大小可以根据需要进行动态调整,但需要手动管理内存的分配和释放。\[3\] 因此,数据结构中的和栈主要是用于存储和操作数据的数据结构,而操作系统中的和栈是用于管理内存的概念。它们的实现和用途有所不同。 #### 引用[.reference_title] - *1* *2* [与栈的区别(数据结构角度)](https://blog.csdn.net/aabbccas/article/details/127739109)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^koosearch_v1,239^v3^insert_chatgpt"}} ] [.reference_item] - *3* [和栈的概念和区别](https://blog.csdn.net/pt666/article/details/70876410)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^koosearch_v1,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

言之命至9012

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值