数据结构:二叉树链式结构

1.前置说明

在学习二叉树链式结构的基本操作前,需要创建一颗链式二叉树,然后才能学习其基本操作

由于现在对于二叉树结构掌握还不够深入,为了降低学习成本,此处手动构建一颗简单的链式二叉树,方便快速进入链式二叉树的操作和学习,等二叉树结构了解的差不多的时候,再回头研究二叉树的创建方式

本处创建简易二叉树实现代码(注意:此代码并不是创建二叉树的方式,真正创建二叉树方式后序详解重点讲解。)

typedef int BTDataType;

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

BTNode* BuyBTNode(BTDataType x)
{
	BTNode* newNode = (BTNode*)malloc(sizeof(BTNode));
	assert(newNode);
	newNode->data = x;
	newNode->left = newNode->right = NULL;
}

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;
	node2->left = node3;
	node1->right = node4;
	node4->left = node5;
	node4->right = node6;

	return node1;
}

再看二叉树基本操作前,再回顾下二叉树的概念,二叉树是:

1. 空树

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

从概念中可以看出,二叉树定义是递归式的,因此后序基本操作中基本都是按照该概念实现的。


2.二叉树的遍历

2.1 前序、中序、后序遍历

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

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

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

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

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

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

2.1.1 前序遍历

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

通俗一点讲,就是先访问根节点,再去访问左子树,最后才是右子树

换成遍历角度来说,遍历打印,先打印根节点的数据,再打印其左子树的数据,再打印其右子树的数据,在实现中就要把每个节点都当成是根节点遍历,遍历完根节点后再把其左右子树作为独立的根节点进行遍历,直到遍历完整颗树

实现代码:

前序遍历
void PreOrder(BTNode* root)
{
	if (root == NULL)
	{
		printf("NULL ");
		return;
	}

	如果root!= NULL 打印root的值,并且把左右子节点进行递归
	printf("%d ", root->data);
	PreOrder(root->left);
	PreOrder(root->right);
}

实现步骤图

 2.1.2 中序遍历

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

换个角度讲,就是先访问左子树,直到访问到左子树的叶子节点后再访问(打印根节点)后再访问其右子树

每颗子树的展开都当做一个独立的树,这颗独立的树由根节点、左子树、右子树组成

实现代码:

中序遍历
void InOrder(BTNode* root)
{
	if (root == NULL)
	{
		printf("NULL ");
		return;
	}
    
    先访问数的左子树,直到左子树访问完
	InOrder(root->left);
    再返回访问根节点
	printf("%d ", root->data);
    最终再去访问右子树
	InOrder(root->right);
}

实现步骤图

2.1.3 后序遍历

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

思想与上面相同,执行逻辑发生变化

实现代码

后序遍历
void PostOrder(BTNode* root)
{
	if (root == NULL)
	{
		printf("NULL ");
		return;
	}

	PostOrder(root->left);
	PostOrder(root->right);
	printf("%d ", root->data);
}

实现步骤图

2.2 层序遍历

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

示意图↓

实现思想:

要实现链式二叉树的层序遍历,对于其基本结构的理解必不可少,每个节点中,都会存有其左子树和其右子树的地址,以这个特性,再结合利用队列先进先出的特性先把树的起始根节点放入队列,然后每次从队列中出数据时,把Pop的这个数据中的左子树和右子树入到队列中

即实现每出一个根节点,就带入它的子节点,每个节点又可以看成是独立的根节点,每次Pop的都是先进的根节点

实现思想动图化

实现代码

队列代码
#pragma once

#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <assert.h>

struct BinaryTreeNode;

typedef struct BinaryTreeNode* QDataType;

typedef struct QueueNode
{
	QDataType val;
	struct QueueNode* next;
}QNode;

typedef struct Queue
{	
	QNode* head;
	QNode* tail;

}Queue;

//初始化
void QueueInit(Queue* ps);
//销毁
void QueueDestory(Queue* ps);
//插入数据
void QueuePush(Queue* ps, QDataType x);
//出数据
void QueuePop(Queue* ps);
//获取队头数据
QDataType QueueFront(Queue* ps);
//获取队尾数据
QDataType QueueBack(Queue* ps);
//队列中的数据数量
size_t QueueSize(Queue* ps);
//队列是否为空
bool QueueEmpty(Queue* ps);

void QueuePrint(Queue* ps);

#define _CRT_SECURE_NO_WARNINGS 1

#include "Queue.h"

void QueuePrint(Queue* ps)
{
	assert(ps);
	QNode* cur = ps->head;
	while (cur)
	{
		QNode* next = cur->next;
		printf("%d ", cur->val);
		cur = next;
	}
	printf("\n\n");
}

//初始化
void QueueInit(Queue* ps)
{
	assert(ps);
	ps->head = ps->tail = NULL;
}
//销毁
void QueueDestory(Queue* ps)
{
	assert(ps);
	QNode* cur = ps->head;
	while (cur)
	{
		QNode* next = cur->next;
		free(cur);
		cur = next;
	}
	ps->head = ps->tail = NULL;
}
//插入数据
void QueuePush(Queue* ps, QDataType x)
{
	assert(ps);
	QNode* newNode = (QNode*)malloc(sizeof(QNode));
	assert(newNode);
	newNode->val = x;
	newNode->next = NULL;

	if (ps->head == NULL)
	{
		ps->head = ps->tail = newNode;
	}
	else
	{
		ps->tail->next = newNode;
		ps->tail = ps->tail->next;
	}
}
//出数据
void QueuePop(Queue* ps)
{
	assert(ps);
	assert(ps->head);

	QNode* delt = ps->head;
	ps->head = ps->head->next;
	free(delt);
	delt = NULL;
}
//获取队头数据
QDataType QueueFront(Queue* ps)
{
	assert(ps);
	assert(ps->head != NULL);
	return ps->head->val;
}
//获取队尾数据
QDataType QueueBack(Queue* ps)
{
	assert(ps);
	assert(ps->head != NULL);
	return ps->tail->val;
}
//队列中的数据数量
size_t QueueSize(Queue* ps)
{
	assert(ps);
	size_t ts = 0;
	QNode* size = ps->head;
	while (size)
	{
			ts++;
			size = size->next;
	}
	return ts;
}
//队列是否为空
bool QueueEmpty(Queue* ps)
{
	assert(&ps);
	return ps->head == NULL;
}
层序遍历实现
void LevelOrder(BTNode* root)
{
	Queue qt;
	QueueInit(&qt);

    初始化队列中的头数据为树的根
	if (root)
	{
		QueuePush(&qt, root);
	}
    
    如果队列不为空,则一直出数据,出数据的同时入其左右子节点
    如果子节点不为空,则入队列,为空则不入
	while (!QueueEmpty(&qt))
	{
		BTNode* front = QueueFront(&qt);
		QueuePop(&qt);

		if (front->left)
			QueuePush(&qt, front->left);

		if (front->right)
			QueuePush(&qt, front->right);

		printf("%d ", front->data);
	}
	printf("\n");
	QueueDestory(&qt);
}

 

3.链式二叉树的功能函数

3.1 二叉树的节点个数

实现思想:

方法1.在调用函数的接口中,传一个计数变量指针过来,通过递归对个数进行累积

方法2.利用函数递归本身进行计数,累加后作为返回值返回一个二叉树的个数

注意:不推荐使用静态局部变量,或者全局变量,这样在多线程中,造成线程安全问题

静态局部变量哪怕在递归中写成 static int count = 0; 这个初始化只会在这个程序中这个递归第一次运行时才会初始化,并且后续运行中不再进行任何初始化,并且这个变量存在于静态区,在多线程中多个线程调用会导致出现线程安全问题

全局变量虽然随时可以重置,但是如果在多线程中可能也会出现线程安全问题

实现代码(这里使用后序遍历进行实现):

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

    使用后序遍历,走到这一步自身肯定不为空,自身就已经计数1
    把左右子树统计个数加上自身就是这颗树的节点数量
    return BinaryTreeSize(root->left) + BinaryTreeSize(root->right) + 1;
}

3.2 二叉树叶子节点个数

实现思想:

在前序遍历的方法下进行变形,如果这个根节点的度不为0,那这个节点就不是叶子节点,则对它的左右子树进行访问,并将接下来的每个子树依次当作根节点进行判断其是否是叶子节点

访问到NULL,则返回0,访问到度为0的节点则返回1,并将结果相加后返回

实现代码:

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

    如果这个节点的度为0,也就是左右子树都是空,则返回1
    if(root->left == NULL && root->right == NULL)
        return 1;
    
    左右子树计算结果相加
    return BinaryTreeLeafSize(root->left) + BinaryTreeLeafSize(root->right);
}

3.3 二叉树第k层节点个数

实现思想:

求第K层节点就很简单了,递归时进行递减,只要k==1 并且第k层的根节点不等于空 则为一个有效个数

实现代码:

二叉树第k层节点个数 
int BinaryTreeLevelKSize(BTNode* root, int k)
{
	如果root==NULL 返回0;
	if (root == NULL)
		return 0;

	走到这一步代表root不是空,判断是否是第k层,如果是那就返回1;
	k不用额外判断会不会小于等于0 因为k每次是递减1,只要节点不等于空,迟早会等于1
	if (k == 1)
		return 1;

	如果都不满足,那就继续走
	return BinaryTreeLevelKSize(root->left, k - 1) + BinaryTreeLevelKSize(root->right, k - 1);
}

3.4 二叉树查找值为x的节点

实现思想:

走到每个根节点时都进行判断,如果这就是值为x的节点,则结束遍历查找,直接返回该值的地址

实现代码:

二叉树查找值为x的节点 
BTNode* BinaryTreeFind(BTNode* root, BTDataType x)
{
	如果节点是空则返回空
	if (root == NULL)
		return NULL;

	if (root->data == x)
		return root;
	走到这一步则不是空,先从左子树里找,如果找到了直接返回结束程序不用再去右子树找
	BTNode* BTleft = BinaryTreeFind(root->left, x);
	if (BTleft != NULL)
	{
		return BTleft;
	}

	如果左子树找不到,再去右子树找
	BTNode* BTright = BinaryTreeFind(root->right, x);
	if (BTright != NULL)
	{
		return BTright;
	}

	如果都没找到 就返回空
	return NULL;
}

4.二叉树的创建和销毁

4.1 二叉树的构建

二叉树的构建其实是通过遍历数组创建

我现在要创建一个二叉树,给出一个char类型的数组,对其进行先序遍历构建

例如如下的先序遍历字符串: ABC##DE#G##F### 其中“#”表示的是空格,空格字符代表空树。

实现思想:

每次进入判断指针指向的数据是否为 '#' 如果是'#' 则代表是空树直接返回空

如果不是'#' 则malloc一个节点,并把指针指向的元素赋予节点的data

并对其左子树和右子树进行同样以上的步骤赋值

实现代码:

#include <stdio.h>
#include <stdlib.h>

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

//创建二叉树
BTNode* BinaryTreeCreate(char* a, int* pi)
{
    如果*pi是# 则代表空树
    if(a[*pi] == '#')
    {
        (*pi)++;
        return NULL;
    }

    走到这代表*pi肯定不是空树,则创建一个节点放入
    BTNode* root = (BTNode*)malloc(sizeof(BTNode));
    root->data = a[*pi];
    (*pi)++;

    此时数组中仍然可能还有其他数据,在数组走完之前
    对左右子树进行遍历构建并赋值给上一颗树的左右子树指针
    root->left = BinaryTreeCreate(a,pi);
    root->right = BinaryTreeCreate(a,pi);

    return root;
}


int main()
{
    char a[100];
    scanf("%s",a);
    
    int count = 0;
    BTNode* root = BinaryTreeCreate(a,&count);
    return 0;
}

4.2 二叉树的销毁

太容易实现了,直接上代码,自行体会~

二叉树销毁
void BinaryTreeDestory(BTNode* root)
{
	if (root == NULL)
		return;

	BinaryTreeDestory(root->left);
	BinaryTreeDestory(root->right);
	free(root);
}

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

实现思想:

用层序遍历,但是这次把空一起入进去,如果队列Pop到空节点时,则判队列是否为空,如果为空则是完全二叉树

如果不是空,则把队列中剩下的数据出完,这些数据中如果有非空节点则不是完全二叉树

实现代码:

判断二叉树是否是完全二叉树 
int BinaryTreeComplete(BTNode* root)
{
	Queue qt;
	QueueInit(&qt);

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

	while (!QueueEmpty(&qt))
	{
		如果front!= NULL 则继续push
		如果front == NULL 则出去判断
		BTNode* front = QueueFront(&qt);
		QueuePop(&qt);
		if (front == NULL)
			break;

		QueuePush(&qt, front->left);
		QueuePush(&qt, front->right);
	}

	判断队列是否为空
	while (!QueueEmpty(&qt))
	{
		走到这里肯定不为空,此时如果树是完全二叉树,队列中剩余的就全都会是空
		一旦出现非空,则这棵树不为完全二叉树
		BTNode* front = QueueFront(&qt);
		QueuePop(&qt);
		如果front为非空,返回false
		if (front)
		{
			QueueDestory(&qt);
			return false;
		}
	}

	走到这里则代表上一步已经把队列中剩余的元素都pop干净,并且都是NULL 这棵树则为完全二叉树
	QueueDestory(&qt);
	return true;
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值