二叉树基本概念与实现(c语言)

二叉树

1.树的概念

1.1树的相关概念

​ 树是一种非线性的数据结构,它是由n(n>=0)个有限节点组成一个具有层次关系的集合。因为整体的逻辑结构像一棵倒挂的树,因此称之为树。

  • 树有一个特殊的节点,称为根节点,根节点没有前驱节点
  • 除根节点外,其余节点被分成M(M>0)个互不相交的集合,其中每个集合又是一棵结构类似树的子树。即每棵子树的根节点有且只有一个前驱,可以有0个或多个后续
  • 因此,树是递归定义的

树的结构

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

  • 子树是不相交的;
  • 除了根节点外,每个节点有且仅有一个父节点
  • 一棵N个节点的树有N-1条边
1.2树的基本概念

请添加图片描述

节点的度:一个节点的度含有的子树的个数称为该节点的度;例如上图:A的度为3,H的度为4

叶节点或终端节点:度为0的节点;例如上图:I、F、G、N、O、P、K、Q、R、M节点为叶节点

分支节点或非终端节点:度不为0的节点;例如上图:B、E、C、H、J、L节点为分支节点

父节点:若一个节点含有子节点,则这个节点称为其子节点的父节点;例如上图:A是B的父节点

子节点:一个节点含有的子树的根节点称为该节点的子节点;例如上图:B是A的子节点

兄弟节点:具有相同父节点的节点互称为兄弟节点;例如上图:B、C、D是兄弟节点

树的度:一棵树中,最大的节点的度称为树的度;例如上图:树的度是4

节点的层次:从根节点开始定义,根节点为第一层,根的子节点为第二层,以此类推;

树的高度或深度:树种节点的最大层次;例如上图:树的高度为5

堂兄弟节点:双亲在同一层的节点互为堂兄弟节点;例如上图:F、G互为堂兄弟节点

节点的祖先:从根到该节点所经分支上的所有节点;例如上图:A是所有节点的祖先

子孙:以某节点为根的子树中任一节点都称为该节点的子孙;例如上图:所有节点都是A的子孙

森林:由m(m>0)棵互不相交的树的集合称为森林;

1.3树的表示

​ 树有很多种表示方式如:双亲表示法、孩子表示法、孩子双亲表示法和孩子兄弟表示法等。在这里我们简单了解其中最常用的孩子兄弟表示法。

typedef int TreeDataType;
struct TreeNode
{
    TreeDataType val;				//节点中的数据
    struct TreeNode* FirstChild;	//指向第一个子节点
    struct TreeNode* NextBrother;	//指向下一个兄弟节点
};

请添加图片描述

2.二叉树的概念与结构

2.1概念

一颗二叉树是节点的一个有限集合,该集合:

  • 或者为空
  • 由一个根节点加上两棵别称为左子树和右子树的二叉树组成

即:

  1. 二叉树不存在度大于2的节点
  2. 二叉树的子树有左右之分,次序不能颠倒,因此二叉树也称为有序树

注意:对于任意二叉树都是由以下几种情况复合组成:

  1. 空树
  2. 只有根节点
  3. 只有左子树
  4. 只有右子树
  5. 左右子树均存在

请添加图片描述

2.2特殊的二叉树
  1. 满二叉树:一个二叉树的每一层节点数都达到最大值,则这个二叉树是满二叉树。也就是说,如果一个二叉树的层数为K,且节点总数是 2 K − 1 2^K-1 2K1,则它就是满二叉树。
  2. 完全二叉树:如果一个二叉树的层数为K,那么它的节点数量在[ 2 ( K − 1 ) 2^{(K-1)} 2(K1), 2 K − 1 2^K-1 2K1]的域内,并且每个节点按层次依此连续存放,则它就是完全二叉树。满二叉树是一种特殊的完全二叉树。

请添加图片描述

2.3二叉树的性质
  1. 若规定根节点的层数为1,则一棵非空二叉树的第i层上最多有 2 ( i − 1 ) 2^{(i - 1)} 2(i1)个节点
  2. 若规定根节点的层数为1,则深度为h的二叉树的最大节点数为 2 h − 1 2^h - 1 2h1
  3. 对于任何一棵二叉树,如果度为0其叶节点个数为 n 0 n_0 n0,度为2的分支节点个数为 n 2 n_2 n2,则有 n 0 = n 2 + 1 n_0=n_2+1 n0=n2+1
  4. 若规定跟家电的层数为1,具有n个节点的满二叉树的深度为 h = l o g 2 ( n + 1 ) h=log_2(n+1) h=log2(n+1)
  5. 对于具有n个节点的完全二叉树,如果按照从上至下从左至右的数组顺序对所有节点从0开始编号,则对于序号为 i i i的节点有:
    • i > 0 i>0 i>0, i i i位置节点的父节点的序号为 ( i − 1 ) / 2 (i-1)/2 (i1)/2;若 i = 0 i=0 i=0 i i i为根节点编号,无双亲节点
    • 2 i + 1 < n 2i+1<n 2i+1<n,则左孩子序号为 2 i + 1 2i+1 2i+1 2 i + 1 > = n 2i+1>=n 2i+1>=n则无左孩子
    • 2 i + 2 < n 2i+2<n 2i+2<n,则有孩子序号为 2 i + 2 2i+2 2i+2 2 i + 2 > = n 2i+2>=n 2i+2>=n则无右孩子

3.二叉树顺序结构

​ 二叉树一般有两种结构存储,一种为顺序结构,一种为链式结构。以下将讲解顺序结构的二叉树

3.1堆

​ 堆是一种完全二叉树,我们通常将堆使用二叉树的顺序结构的数组来存储。如果有一个关键码的集合K={ k 0 , k 1 , k 2 , . . . , k n − 1 k_0, k_1, k_2, ..., k_{n-1} k0,k1,k2,...,kn1},把它的所有元素按完全二叉树的顺序结构方式存储在一个一维数组中,并满足: K i < = K 2 ∗ i + 1 K_i <= K_{2*i+1} Ki<=K2i+1 K i < = K 2 ∗ i + 2 K_i <= K_{2*i+2} Ki<=K2i+2(或 K i > = K 2 ∗ i + 1 K_i >= K_{2*i+1} Ki>=K2i+1 K i > = K 2 ∗ i + 2 K_i >= K_{2*i+2} Ki>=K2i+2 i = 0 , 1 , 2 , . . . i = 0, 1, 2,... i=0,1,2,...,则称为小堆(或大堆)。将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆。

堆的性质:

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

请添加图片描述

3.2堆的实现
3.2.1堆的基本接口
typedef int HeapDataType;

typedef struct Heap
{
	HeapDataType* a;//数据
	int size;		//堆有效数据大小
	int capacity;	//堆容量
}HP;

//堆的初始化和销毁
void HeapInit(HP* php);
void HeapInitArray(HP* php, int* a, int size);
void HeapDsetroy(HP* php);

//向上调整
void AdjustUp(HeapDataType* a, int child);
//插入数据
void HeapPush(HP* php, HeapDataType x);
//向下调整
void AdjustDown(HeapDataType* a, int size,int parent);
//删除数据
void HeapPop(HP* php);
//堆数据判空
bool HeapEmpty(HP* php);
//获取堆顶数据
HeapDataType HeapTop(HP* php);
//获取堆大小
int HeapSize(HP* php);
3.2.2堆的初始化

​ 堆与顺序表的结构很相似,因此可以像顺序表那样初始化堆。

//初始化堆
void HeapInit(HP* php)
{
	assert(php);

	HeapDataType* ptr = (HeapDataType*)malloc(sizeof(HeapDataType) * 4);
	if (ptr == NULL)
	{
		perror("malloc fail");
		return;
	}

	php->a = ptr;
	php->capacity = 4;	//初始的堆容量
	php->size = 0;		//初始的有效数据值
}
3.2.3堆的调整算法

​ 在创建堆之前,我需要讲述一个向下调整的算法。在任给的一组数组中,需要保证数组是以大根堆\小根堆存储的才能建堆并且插入或删除数据。例如,左右子树都是大根堆,此时插入数据在根的位置上,可以依次对比根下的左孩子和右孩子,对插入的数据的位置进行调整,使数组满足二叉树的结构。

向下调整主要的点是如何找到父节点的左右孩子的位置

因此,我们可以利用二叉树的性质:

  • 左孩子 = 父节点 × 2 + 1
  • 右孩子 = 父节点 × 2 + 2

比较父节点和孩子数据的大小来进行排序,已达到向下调整的效果

//向下调整(左右子树都是大堆/小堆)
void AdjustDown(HeapDataType* a, int size, int parent)
{
	int child = parent * 2 + 1;//左孩子
    //只要孩子节点还在堆内就可以循环
    //当孩子节点大与有效数据的值时,即已经调整完毕
	while (child < size)
	{
        //这里的比较是让左右孩子作比较
		//在构建大根堆的情况下,若右孩子比左孩子大,则只需要++child指向右孩子即可
        //同时可以控制两数比较的方式创建大根堆或者创建小根堆
		if (child + 1 < size && a[child] < a[child + 1])//<:建大堆 >:建小堆
		{
			child++;
		}

        //比较
        //若孩子比父的数据大,则两数交换,否则退出循环,即二叉树已成堆
        //交换后需要讲父节点指向原本交换的孩子的位置上,即寻找孩子的孩子
        //同时可以控制两数比较的方式创建大根堆或者创建小根堆
		if (a[parent] < a[child])//<:建大堆 >:建小堆
		{
			Swap(&a[parent], &a[child]);
			parent = child;			//新的指向父节点的值
			child = parent * 2 + 1;	//指向上述父节点的左孩子的值
		}
		else
		{
			break;
		}
	}
}

​ 有向下调整算法,自然也有向上调整算法的思想。简单来说就是将最后一位数据与其父节点进行对比调整,令数据能够调整到适当的位置。

向上调整主要通过寻找指向父节点的位置

因此像向下调整一样利用二叉树的性质:

  • 父节点 = (孩子节点 - 1)/ 2

比较父节点与孩子的数据的大小并进行调整,以达到向上调整的效果

//向上调整(除了child位置,前面数据构成堆)
void AdjustUp(HeapDataType* a, int child)
{
	int parent = (child - 1) / 2;//指向父节点
    //只要指向待调整的数据的值(即child)大于0就可以循环
    //当child <= 0时,说明child指向的数据为二叉树的根
	while (child > 0)
	{
        //比较
        //以创建大根堆为例,若孩子的值大于父节点的值
        //则两数交换,更新新的指向父节点和孩子节点的值
        //同时可以控制两数比较的方式创建大根堆或者创建小根堆
		if (a[child] > a[parent])//>:建大堆  <:建小堆
		{
			Swap(&a[child], &a[parent]);
			child = parent;				//新的指向孩子的值
			parent = (child - 1) / 2;	//新的指向上述孩子的父节点的值
		}
		else
		{
			break;
		}
	}
}
3.2.4堆的插入

​ 在上面讲述了向下和向上调整的算法,接下来就可以插入数据到堆内了。

//插入数据
void HeapPush(HP* php, HeapDataType x)
{
	assert(php);
	
    //检查扩容
    //当堆内数据存满后需要对堆进行扩容处理
	if (php->size == php->capacity)
	{
		HeapDataType* ptr = (HeapDataType*)realloc(php->a, sizeof(HeapDataType) * php->capacity * 2);
		if (ptr == NULL)
		{
			perror("realloc fail");
			return;
		}
		php->a = ptr;
		php->capacity *= 2;	//扩容后,容量为原来的2倍
	}

    //插入数据
	php->a[php->size] = x;
	php->size++;
	
    //向上调整
	AdjustUp(php->a, php->size - 1);//size-1是因为上面的size在数据插入后多加了一次
}

这里解释以下为什么要使用向上调整进行插入

类似顺序表的插入都是尾插较为高效,只需要访问数组的size下标然后再++即可完成一次数据的插入,而且使用尾插的方式可以保证向上调整的条件,即除了child位置,前面数据构成堆,如果在插入时对其进行排序的话,整个过程就会变得非常麻烦,且效率低下。

3.2.5堆的删除

​ 堆的删除非常有趣,它只能删除堆顶的数据。整体的做法是将堆顶的数据与最后一个数据交换,然后删除数组最后一个数据,在进行向下调整。

//删除堆顶数据
void HeapPop(HP* php)
{
	assert(php);
	assert(php->size);
	
    //交换堆顶和数组最后的数据
	Swap(&php->a[0], &php->a[php->size - 1]);
    //删除最后一个数据
	php->size--;
	//向下调整
    //因为最后一个数据被放到了堆顶,所以从0位置开始向下调整
    //交换后的数组满足向下调整的条件,即左右子树都是大堆/小堆
	AdjustDown(php->a, php->size, 0);
}
3.2.6堆的创建

​ 了解了堆的基本操作之后,我们就可以对任给的一组数据连续的数组进行建堆了。

//建堆初始化
void HeapInitArray(HP* php, int* a, int size)
{
	assert(php);

	HeapDataType* ptr = (HeapDataType*)malloc(sizeof(HeapDataType) * size);
	if (ptr == NULL)
	{
		perror("malloc fail");
		return;
	}

	php->capacity = size;
	php->size = size;

	//建堆
    //这里的操作类似堆的插入
	for (int i = (size - 2) / 2; i >= 0; i--)
	{
		AdjustDown(php->a, php->size, i);
	}
}
3.2.7堆的其他接口

​ 由于剩下的接口较为简单,小编在此直接将代码展示出来供大家参考。

//销毁堆
void HeapDsetroy(HP* php)
{
	assert(php);

	free(php->a);
}

//堆的判空
bool HeapEmpty(HP* php)
{
	assert(php);

	return php->size == 0;
}

//堆顶数据
HeapDataType HeapTop(HP* php)
{
	assert(php);

	return php->a[0];
}

//堆的数据个数
int HeapSize(HP* php)
{
	assert(php);

	return php->size;
}

4.二叉树链式结构

4.1二叉树的遍历

​ 二叉树的遍历是按照某中特定的规则,依此对二叉树中的节点进行相应的操作,并且每个节点都只操作一次。二叉树的遍历有以下几种:

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

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

//前序遍历-- 根->左子树->右子树
void PreOrder(BTNode* root)
{
	if (root == NULL)
	{
		//printf("NULL ");
		return;
	}
	printf("%d ", root->val);
	PreOrder(root->left);
	PreOrder(root->right);
}

//中序遍历-- 左子树->根->右子树
void InOrder(BTNode* root)
{
	if (root == NULL)
	{
		//printf("NULL ");
		return;
	}
	InOrder(root->left);
	printf("%d ", root->val);
	InOrder(root->right);
}

//后序遍历-- 左子树->右子树->根
void PostOrder(BTNode* root)
{
	if (root == NULL)
	{
		//printf("NULL ");
		return;
	}
	PostOrder(root->left);
	PostOrder(root->right);
	printf("%d ", root->val);
}

​ 还有一种遍历为层序遍历,层序遍历的实现需要借助队列来进行,因此这里只简单为大家提供代码作为参考,如果想读懂下面的代码的话还需要理解队列的知识哦。

//层序遍历
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->val);

		//带入根的左右子树
		if (front->left)
			QueuePush(&q, front->left);

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

	QueueDestroy(&q);
}
4.2二叉树的创建和销毁
typedef char BTDataType;

typedef struct BinaryTreeNode
{
	BTDataType val;
	struct BinaryTreeNode* left;
	struct BinaryTreeNode* right;
}BTNode;
//通过前序遍历的数组构建二叉树
BTNode* BinaryTreeCreate(BTDataType* a, int* pi)//这里的pi指的是访问数组下标的值
{
	assert(*pi >= 0);
	
    //这里的‘#’代表NULL指针
    //当访问到NULL指针时,即递归的退出条件
	//同时需要对pi指针指向的值++,以便下一次递归能正确访问到下一个数据
	if (a[*pi] == '#')
	{
		(*pi)++;
		return NULL;
	}

	BTNode* root = (BTNode*)malloc(sizeof(BTNode));
	if (root == NULL)
	{
		perror("malloc fail");
		return NULL;
	}
	root->val = a[*pi];
	printf("%c ", a[*pi]);
	(*pi)++;
	if (a[*pi])
	{
        //对根的左子树和右子树进行链接
		root->left = BinaryTreeCreate(a, pi);
		root->right = BinaryTreeCreate(a, pi);
	}

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

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

	free(root);
}
4.3二叉树常用的接口
4.3.1二叉树节点个数
//二叉树节点个数
int BinaryTreeSize(BTNode* root)
{
	if (root == NULL)
		return 0;

	return BinaryTreeSize(root->left) + BinaryTreeSize(root->right) + 1;
}
4.3.2二叉树叶子节点个数
//二叉树叶子节点个数
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);
}
4.3.3二叉树第k层节点个数
int BinaryTreeLevelKSize(BTNode* root, int k)
{
	assert(k > 0);

	if (root == NULL)
		return 0;

	if (k == 1)
		return 1;

	return BinaryTreeLevelKSize(root->left, k - 1) 
			+ BinaryTreeLevelKSize(root->right, k - 1);
}
4.3.4二叉树查找值为x的节点
//二叉树查找值为x的节点
BTNode* BinaryTreeFind(BTNode* root, BTDataType x)
{
	if (root == NULL)
		return NULL;

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

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

	BTNode* right = BinaryTreeFind(root->right, x);
	if (right)
		return right;

	return NULL;
}
  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值