【C语言数据结构】(四)树、二叉树、堆的结构及模拟实现

2 篇文章 0 订阅
2 篇文章 0 订阅

1.树的概念及结构

1.1 概念

是一种非线性的数据结构,它是由n(n>=0)个有限结点组成的一个具有层次关系的集合。它之所以被称为树是因为它看起来像一颗倒着的树,即根朝上而叶朝下。

特点:

  1. 有一个特殊的结点,称为根节点,根结点没有前驱结点
  2. 除根结点外,其余结点被分成M(M>0)个互不相交的集合T1、T2、…Tm,其中每一个集合Ti(1<=i<M)又是一颗结构与树类似的子树。每颗子树的根节点有且只有一个前驱,可以有0个或多个后继
  3. 因此,树是递归定义的

1.2 结构

在这里插入图片描述

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

1.3 树的表示

相比于线性结构,树的存储要表示则比较麻烦,实际中树有很多种表达方式,如:双亲表示法,孩子表示法,孩子兄弟表示法等等。这里就简单的了解其中最常用的孩子兄弟表示法。

typedef int DataType;
struct Node
{
 struct Node* _firstChild1; // 第一个孩子结点
 struct Node* _pNextBrother; // 指向其下一个兄弟结点
 DataType _data; // 结点中的数据域
};

树在实际中的运用:表示文件系统的目录树结构

2.二叉树概念及结构

2.1 概念

  • 一颗二叉树是结点的一个有限集合,该集合或者为空,或者是由一个根结点加上两棵分别成为左子树和右子树的二叉树组成。即二叉树分为两种:
    1.空树
    2.根结点+左子树+右子树
  • 二叉树的特点:
    每个结点最多有两棵子树,即二叉树不存在度大于2的结点;
    二叉树的子树有左右之分,其子树的次序不能颠倒,即二叉树是一颗有序树

2.2 结构

在这里插入图片描述

2.3 特殊的二叉树

  1. 满二叉树:一个二叉树,如果每一层的结点个数都达到最大值,这个二叉树就是满二叉树。也就是说除了最后一层的结点,其余结点都有左子树和右子树。如果该二叉树的层数为K,则结点总个数为(2^K)-1;
  2. 完全二叉树:完全二叉树是效率很高的数据结构,完全二叉树是由满二叉树引出来的。对于深度为K的,由n个结点的二叉树,当且仅当其每一个结点都与深度为K的满二叉树中编号从1至n的结点一一对应时称为完全二叉树。满二叉树是一种特殊的完全二叉树。
    在这里插入图片描述

2.4 二叉树的存储结构

二叉树一般可以使用两种结构存储,一种顺序结构,一种链式结构。

2.5 二叉树的性质

  1. 若规定根节点的层数为1,则一棵非空二叉树的第i层上最多有2^(i-1)个结点
  2. 若规定根节点的层数为1,则深度为h的二叉树的最大结点数是2^h-1
  3. 对任意一棵二叉树,如果度为0的结点(叶子结点)个数为n0,度为2的结点个数为n2,则有n0=n2+1
  4. 若规定根节点的层数为1,具有n个结点的满二叉树的深度h=log2(n+1)(2为底,n+1为对数)
  5. 对于具有n个结点的完全二叉树,如果按照从上至下从左至右的数组顺序对所有结点从0开始编号,则对于序号为i的结点有:
  • 若i>0,i位置结点的双亲序号:(i-1)/2;i=0,则i为根结点编号,无双亲结点
  • 若2i+1<n,左孩子序号:2i+1,2i+1>=n则无右孩子
  • 若2i+2<n,右孩子序号:2i+2,2i+2>=n则无右孩子

3.二叉树顺序结构及实现

3.1 概念及结构

  • 顺序结构存储就是使用数组来存储,一般使用数组只适合表示完全二叉树,因为不是完全二叉树会有空间的浪费。而现实中使用只有堆才会使用数组来存储。二叉树顺序存储在物理上是一个数组,在逻辑上是一棵二叉树
    在这里插入图片描述
  • 普通的二叉树是不适合用数组来存储的,因为可能会存在大量的空间浪费。而完全二叉树更适合使用顺序结构存储。现实中我们通常把堆(一种完全二叉树)使用顺序结构来存储,需要注意的是这里的堆和操作系统虚拟进程地址空间中的堆是两回事,一个是数据结构,一个是操作系统中管理内存的一块区域分段。

3.3 堆的概念

如果有一个关键码的集合K={k0,k1,k2,…,kn-1},把它的所有元素按完全二叉树的顺序存储方式存储在一个一维数组中,并满足:ki<=k2i+1且ki<=k2i+2(ki>=k2i+1且ki>=k2i+2),i=0,1,2…,则称为小堆(或大堆)。将根结点最大的堆称为最大堆或大根堆,根结点最小的堆叫做最小堆或小根堆。

  • 堆的性质:
  1. 堆中某个结点的值总是不大于或不小于其双亲结点的值
  2. 堆总是一棵完全二叉树

3.4 堆的实现

3.4.1 堆向下调整算法

现在我们给出一个数组,逻辑上看做一颗完全二叉树。我们通过从根节点开始的向下调整算法可以把它调整成一个小堆。向下调整算法有一个前提:左右子树必须是一个堆,才能调整。

3.4.2 堆的创建

我们给出的一个数组,这个数组逻辑上可以看做一颗完全二叉树,但是还不是一个堆,现在我们通过算法,把它构建成一个堆。根节点左右子树不是堆,我们怎么调整呢?我们从倒数的第一个非叶子节点的子树开始调整,一直调整到根节点的树,就可以调整成堆。(递归思想)

3.4.3 堆的插入

先将待插入元素插入到数组的尾上,再进行向上调整算法,直到满足堆。

3.4.4 堆的删除

堆的删除是删除堆顶元素,它是堆中最大(大堆)或最小的元素(小堆)。首先将堆顶元素与数据根最后一个结点交换,然后删除数组最后一个数据,再对根结点进行向下调整算法即可完成。

3.4.5 堆排序

利用了堆删除的思想,每次将堆中最大(大堆)或最小的元素(小堆)交换到最后,直至整个数组有序。

#pragma once

typedef int DataType;
typedef int(*PCOM)(int left, int right);

typedef struct Heap
{
	DataType* array;
	int capacity;
	int size;
	PCOM pCom;
}Heap;

//建小堆参数
int Less(int left, int right);
//建大堆参数
int Greater(int left, int right);
//向下调整方法
void AdjustDown(Heap* hp, int i);
//堆的构建
void HeapInit(Heap* hp, DataType* array, int size, PCOM pCom);
//容量检查
int CheckCap(Heap* hp);
//向上调整
void AdjustUp(Heap* hp, int i);
//堆的插入
void HeapInsert(Heap* hp, DataType data);
//堆的删除
void HeapErase(Heap* hp);
//取堆顶的数据
DataType HeapTop(Heap* hp);
//堆的数据个数
int HeapSize(Heap* hp);
//堆的判空
int IsHeapEmpty(Heap* hp);
//堆的销毁
void HeapDestroy(Heap* hp);

//对数组进行堆排序
void HeapSort(int* array, int size);

代码实现:

#include "Heap.h"
#include <stdio.h>
#include <assert.h>
#include <malloc.h>

int Less(int left, int right)
{
	return left < right;
}
int Greater(int left, int right)
{
	return left > right;
}

void Swap(int* left, int* right)
{
	int temp = *left;
	*left = *right;
	*right = temp;
}

void AdjustDown(Heap* hp, int i)//若该结点的左右子树都满足堆的性质,则用此方法将该树调整为堆
{
	assert(hp);
	int parent = i;
	int child = 2 * parent + 1;//i必有左孩子
	while (child < hp->size)
	{
		if (child + 1 < hp->size && hp->pCom(hp->array[child + 1], hp->array[child]))
			child += 1;
		if (hp->pCom(hp->array[child], hp->array[parent]))
		{
			Swap(&hp->array[parent], &hp->array[child]);
			parent = child;
			child = 2 * parent + 1;
		}
		else
			return;
	}
}

void HeapInit(Heap* hp, DataType* array, int size, PCOM pCom)
{
	assert(hp);
	//1.将外部数组元素传入"堆"中
	hp->array = (DataType*)malloc(sizeof(DataType)*size);
	if (NULL == hp->array)
	{
		assert(0);
		return;
	}
	hp->capacity = size;
	for (int i = 0; i < size; i++)
	{
		hp->array[i] = array[i];
	}
	hp->size = size;
	hp->pCom = pCom;
	//2.调整连续空间上的元素使其满足堆的性质
	int root = 0;
	//从倒数第一个非叶结点开始,向前对每一个分支结点向下调整,直到根节点
	for (root = ((size - 1) - 1) / 2; root >= 0; root--)
	{
		AdjustDown(hp, root);
	}
}

int CheckCap(Heap* hp)
{
	assert(hp);
	if (hp->size != hp->capacity)
		return 1;
	DataType* arr = (DataType*)realloc(hp->array, sizeof(DataType)*(hp->capacity) * 2);
	if (NULL == arr)
	{
		assert(0);
		return 0;
	}
	hp->array = arr;
	hp->capacity *= 2;
	return 1;
}

void AdjustUp(Heap* hp, int i)
{
	assert(hp);
	int child = i;
	int parent = (child - 1) / 2;
	while (child)
	{
		if (hp->pCom(hp->array[child], hp->array[parent]))
		{
			Swap(&hp->array[child], &hp->array[parent]);
			child = parent;
			parent = (child - 1) / 2;
		}
		else
			return;
	}
}

void HeapInsert(Heap* hp, DataType data)
{
	assert(hp);
	//检查是否需要扩容
	if (CheckCap(hp))
	{
		hp->array[hp->size++] = data;
		//将新元素向最后一个位置插入,再向上调整使其满足堆的性质(向上调整)
		AdjustUp(hp, hp->size - 1);
	}
	else
	{
		printf("Error!!\n");
		assert(0);
	}
}

void HeapErase(Heap* hp)
{
	assert(hp);
	if (IsHeapEmpty(hp))
	{
		assert(0);
		return;
	}
	//将堆顶元素与最后一个元素交换,hp->size--,再向下调整
	Swap(&hp->array[0], &hp->array[hp->size - 1]);
	hp->size--;
	AdjustDown(hp, 0);
}

DataType HeapTop(Heap* hp)
{
	assert(hp);
	if (IsHeapEmpty(hp))
	{
		printf("NULL!!\n");
		return -1;
	}
	return hp->array[0];
}

int HeapSize(Heap* hp)
{
	assert(hp);
	return hp->size;
}

int IsHeapEmpty(Heap* hp)
{
	assert(hp);
	return 0 == hp->size;
}

void HeapDestroy(Heap* hp)
{
	assert(hp);
	free(hp->array);
}



void Adjust(int* array, int size, int parent)
{
	int child = parent * 2 + 1;
	//假设排升序,则建大堆
	while (child < size)
	{
		if (child + 1 < size&&array[child + 1] < array[child])
			child += 1;
		if (array[parent] > array[child])
		{
			Swap(&array[parent], &array[child]);
			parent = child;
			child = parent * 2 + 1;
		}
		else
			return;
	}
}

//堆排序
void HeapSort(int* array, int size)
{
	//1.建堆
	for (int root = ((size - 1) - 1) / 2; root >= 0; root--)
	{
		Adjust(array, size, root);
	}
	//2.利用堆删除的思想排序
	while (size>1)
	{
		Swap(&array[0], &array[size - 1]);
		size--;
		Adjust(array, size, 0);
	}
}

4.二叉树链式结构及实现

4.1 二叉树链式结构的遍历

遍历是指沿着某条搜索路线,依次对树中每个结点均做一次且仅做一次访问。访问结点时所做的操作依赖于具体的应用问题。遍历是二叉树上最重要的运算之一,是二叉树上进行其他运算的基础。

前序/中序/后序的递归结构遍历:根据访问结点操作发生的位置来命名

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

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

  3. LRN:后序遍历(Postorder Traversal)——访问根结点的操作发生在遍历其左右子树之后。(左右根)
    由于被访问的结点必是某子树的根,所以N(Node)、L(Left subtree)和R(Right subtree)又可解释为根、根的左子树和根的右子树。NLR、LNR和LRN分别又称为先根遍历、中根遍历和后根遍历。

  4. 层序遍历:除了先序遍历、中序遍历、后序遍历外,还可以对二叉树进行层序遍历。设二叉树的根节点所在层数为1,层序遍历就是从所在二叉树的根节点出发,首先访问第一层的树根节点,然后从左到右访问第2层上的节点,接着是第三层的节点,以此类推,自上而下,自左至右逐层访问树的结点的过程。在C语言中,我们需要借助队列的数据结构来模拟实现。

#pragma once

typedef int Datatype;

typedef struct BTNode
{
	Datatype data;
	struct BTNode* Left;
	struct BTNode* Right;
}BTNode;

//通过前序遍历的数组构建二叉树
BTNode* BinTreeCreate(Datatype* arr, int size, Datatype invalid);
//创建二叉树辅助方法
BTNode* _BinTreeCreate(Datatype* arr, int size, Datatype invalid, int* index);
//二叉树前序遍历
void PreOrder(BTNode* root);
//二叉树中序遍历
void InOrder(BTNode* root);
//二叉树后序遍历
void PostOrder(BTNode* root);
//层序遍历
void LevelOrder(BTNode* root);
//二叉树节点个数
int BinTreeSize(BTNode* root);
//二叉树叶子节点个数
int BinaryTreeLeafSize(BTNode* root);
//二叉树第k层节点个数
int BinaryTreeLevelKSize(BTNode* root,int k);
//二叉树的高度
int BinaryTreeHeight(BTNode* root);
//二叉树查找值为x的节点
BTNode* BinaryTreeFind(BTNode* root,Datatype x);
//判断二叉树是否为完全二叉树
int BinaryTreeComplete(BTNode* root);
//二叉树销毁
void BTDestroy(BTNode** root);

代码实现:
在判断二叉树是否为完全二叉树的方法中使用到了队列的实现代码,可参考我的博客:栈和队列

#include <stdio.h>
#include <malloc.h>
#include <assert.h>
#include "BTree.h"
#include "Queue.h"

BTNode* BuyBinTreeNode(Datatype data)
{
	BTNode* NewNode = (BTNode*)malloc(sizeof(BTNode));
	if (NULL == NewNode)
	{
		assert(0);
		return NULL;
	}
	NewNode->data = data;
	NewNode->Left = NewNode->Right = NULL;
	return NewNode;
}

BTNode* _BinTreeCreate(Datatype* arr, int size, Datatype invalid, int* index)
{
	BTNode* root = NULL;
	if (*index < size&&invalid != arr[*index])
	{
		root = BuyBinTreeNode(arr[*index]);
		(*index)++;
		root->Left = _BinTreeCreate(arr, size, invalid, index);
		(*index)++;
		root->Right = _BinTreeCreate(arr, size, invalid, index);
	}
	return root;
}

BTNode* BinTreeCreate(Datatype* arr, int size, Datatype invalid)
{
	int index = 0;
	return _BinTreeCreate(arr, size, invalid, &index);
}

//前序遍历
void PreOrder(BTNode* root)
{
	if (root)
	{
		printf("%d ", root->data);
		PreOrder(root->Left);
		PreOrder(root->Right);
	}
}

//中序遍历
void InOrder(BTNode* root)
{
	if (root)
	{
		InOrder(root->Left);
		printf("%d ", root->data);
		InOrder(root->Right);
	}
}

//后序遍历
void PostOrder(BTNode* root)
{
	if (root)
	{
		PostOrder(root->Left);
		PostOrder(root->Right);
		printf("%d ", root->data);
	}
}

//层序遍历
void LevelOrder(BTNode* root)
{
	Queue q;
	if (NULL == root)
		return;

	QueueInit(&q);
	QueuePush(&q, root);
	while (!IsQueueEmpty(&q))
	{
		BTNode* cur = (BTNode*)QueueFront(&q);
		printf("%d ", cur->data);
		if (cur->Left)
			QueuePush(&q, cur->Left);
		if (cur->Right)
			QueuePush(&q, cur->Right);
		QueuePop(&q);
	}
	QueueDestroy(&q);
}

//求二叉树结点的个数
int BinTreeSize(BTNode* root)
{
	if (NULL == root)
		return 0;

	return BinTreeSize(root->Left) + BinTreeSize(root->Right) + 1;
}

//求二叉树中叶子结点的个数
int BinaryTreeLeafSize(BTNode* root)
{
	if (NULL == root)
		return 0;
	if (NULL == root->Left&&NULL == root->Right)
		return 1;
	return BinaryTreeLeafSize(root->Left) + BinaryTreeLeafSize(root->Right);
}

//求第K层结点的个数
int BinaryTreeLevelKSize(BTNode* root, int k)
{
	if (NULL == root || k <= 0)
		return 0;
	if (1 == k)
		return 1;
	return BinaryTreeLevelKSize(root->Left, k - 1) + BinaryTreeLevelKSize(root->Right, k - 1);
}

//求二叉树的高度
int BinaryTreeHeight(BTNode* root)
{
	if (NULL == root)
		return 0;
	return 1 + (BinaryTreeHeight(root->Left) > BinaryTreeHeight(root->Right) ? BinaryTreeHeight(root->Left) : BinaryTreeHeight(root->Right));
}

//二叉树的查找
BTNode* BinaryTreeFind(BTNode* root, Datatype x)
{
	BTNode* ret = NULL;

	if (NULL == root)
		return NULL;

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

	if (ret = BinaryTreeFind(root->Left, x))
		return ret;

	return BinaryTreeFind(root->Right, x);
}

//二叉树的销毁
void BTDestroy(BTNode** root)
{
	if (*root)
	{
		BTDestroy(&(*root)->Left);
		BTDestroy(&(*root)->Right);
		free(*root);
		*root = NULL;
	}
}

//判断是否为完全二叉树
int BinaryTreeComplete(BTNode* root)
{
	if (NULL == root)
	{
		return 1;
	}
	Queue q;
	QueueInit(&q);
	BTNode* cur = NULL;
	QueuePush(&q, root);
	int flag = 0;
	while (!IsQueueEmpty(&q))
	{
		cur = QueueFront(&q);
		if (flag)
		{
			if (cur->Left || cur->Right)
			{
				QueueDestroy(&q);
				return 0;
			}
		}
		else
		{
			if (cur->Left && cur->Right)
			{
				QueuePush(&q, cur->Left);
				QueuePush(&q, cur->Right);
			}
			else if (cur->Left)
			{
				QueuePush(&q, cur->Left);
				flag = 1;
			}
			else if (cur->Right)
			{
				QueueDestroy(&q);
				return 0;
			}
			else
			{
				flag = 1;
			}
		}
		QueuePop(&q);
	}
	QueueDestroy(&q);
	return 1;
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值