数据结构 — 二叉树

目录

一、树概念及结构

1.  树的概念

2. 树的相关概念

3. 树的表示

4. 树在实际中的运用(表示文件系统的目录树结构)

二、二叉树概念及结构

 1.二叉树概念

 2. 特殊的二叉树

 3. 二叉树的性质

 4. 二叉树的存储结构

1. 顺序存储

2. 链式存储

三、二叉树的顺序结构及实现

 1. 二叉树的顺序结构

 2. 堆的概念及结构

 3. 堆的实现

  3.1 堆的结构

  3.2 堆的创建

  3.3 向上调整建堆

  3.4 删除堆顶数据

  3.5 向下调整

  3.6 堆的其他函数

 4.建堆时间复杂度 

 5. 堆的应用

  5.1 堆排序

  5.2 TOP-K问题

  6. 二叉树链式结构的实现

   6.1 二叉树的结构

   6.2 二叉树的遍历

   6.2 构建二叉树

   6.3 二叉树的节点个数

   6.4 查找节点

   6.5 销毁二叉树

   6.6 层序遍历

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


一、树概念及结构

 1.  树的概念

  1. 树是一种非线性的数据结构,它是由nn>=0)个有限结点组成一个具有层次关系的集合。把它叫做树是因为它看起来像一棵倒挂的树,也就是说它是根朝上,而叶朝下的
  2. 有一个特殊的结点,称为根结点,根节点没有前驱结点,除根节点外,其余结点被分成M(M>0)个互不相交的集合T1T2……Tm,其中每一个集合Ti(1<= i <= m)又是一棵结构与树类似的子树。每棵子树的根结点有且只有一个前驱,可以有0个或多个后继 ,因此,树是递归定义的。

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

 2. 树的相关概念

  1. 节点的度:一个节点含有的子树的个数称为该节点的度; 如上图:A的为6
  2. 叶节点或终端节点:度为0的节点称为叶节点; 如上图:BCHI...等节点为叶节点
  3. 非终端节点或分支节点:度不为0的节点; 如上图:DEFG...等节点为分支节点
  4. 双亲节点或父节点:若一个节点含有子节点,则这个节点称为其子节点的父节点; 如上图:AB的父节点
  5. 孩子节点或子节点:一个节点含有的子树的根节点称为该节点的子节点; 如上图:BA的孩子节点
  6. 兄弟节点:具有相同父节点的节点互称为兄弟节点; 如上图:BC是兄弟节点
  7. 树的度:一棵树中,最大的节点的度称为树的度; 如上图:树的度为6
  8. 节点的层次:从根开始定义起,根为第1层,根的子节点为第2层,以此类推;
  9. 树的高度或深度:树中节点的最大层次; 如上图:树的高度为4
  10. 堂兄弟节点:双亲在同一层的节点互为堂兄弟;如上图:HI互为兄弟节点
  11. 节点的祖先:从根到该节点所经分支上的所有节点;如上图:A是所有节点的祖先
  12. 子孙:以某节点为根的子树中任一节点都称为该节点的子孙。如上图:所有节点都是A的子孙
  13. 森林:由mm>0)棵互不相交的树的集合称为森林;

3. 树的表示

树结构相对线性表就比较复杂了,要存储表示起来就比较麻烦了, 既然保存值域,也要保存结点和结点之间 的关系 ,实际中树有很多种表示方式如:双亲表示法,孩子表示法、孩子双亲表示法以及孩子兄弟表示法等。我们这里就简单的了解其中最常用的孩子兄弟表示法
typedef int DataType;
struct Node
{
     struct Node* _firstChild1; // 第一个孩子结点
     struct Node* _pNextBrother; // 指向其下一个兄弟结点
     DataType _data; // 结点中的数据域
};

4. 树在实际中的运用(表示文件系统的目录树结构)

 


二、二叉树概念及结构

 1.二叉树概念

一棵二叉树是结点的一个有限集合,该集合 :
1. 或者为空
2. 由一个根节点加上两棵别称为左子树和右子树的二叉树组成

 从上图可以看出:

1. 二叉树不存在度大于 2 的结点
2. 二叉树的子树有左右之分,次序不能颠倒,因此二叉树是有序树
注意:对于任意的二叉树都是由以下几种情况复合而成的:

2. 特殊的二叉树

1. 满二叉树 :一个二叉树,如果每一个层的结点数都达到最大值,则这个二叉树就是满二叉树。也就是 说,如果一个二叉树的层数为K ,且结点总数是 (2^k)-1,则它就是满二叉树。
2. 完全二叉树 :完全二叉树是效率很高的数据结构,完全二叉树是由满二叉树而引出来的。对于深度为 K的,有n 个结点的二叉树,当且仅当其每一个结点都与深度为 K 的满二叉树中编号从 1 n 的结点一一对应时称之为完全二叉树。 要注意的是满二叉树是一种特殊的完全二叉树。

 3. 二叉树的性质

1. 若规定根节点的层数为 1 ,则一棵非空二叉树的 i 层上最多有 2^(i-1) 个结点.
2. 若规定根节点的层数为 1 ,则 深度为 h的二叉树的最大结点数是 (2^h)-1.
3. 对任何一棵二叉树 , 如果度为 0其叶结点个数为n₀, 度为2的分支结点个数为n₂ ,则有 n₀=n₂ +1
4. 若规定根节点的层数为 1 ,具有 n个结点的满二叉树的深度h=㏒₂(n+1) . (ps:㏒₂(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 否则无右孩子

 4. 二叉树的存储结构

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

1. 顺序存储

        顺序结构存储就是使用数组来存储 ,一般使用数组 只适合表示完全二叉树 ,因为不是完全二叉树会有空间的浪费。而现实中使用中只有堆才会使用数组来存储,二叉树顺 序存储在物理上是一个数组,在逻辑上是一颗二叉树。

2. 链式存储

        二叉树的链式存储结构是指,用链表来表示一棵二叉树,即用链来指示元素的逻辑关系。 通常的方法是链表中每个结点由三个域组成,数据域和左右指针域,左右指针分别用来给出该结点左孩子和右孩子所在的链结点的存储地址 。链式结构又分为二叉链和三叉链.

typedef int BTDataType;
// 二叉链
struct BinaryTreeNode
{
     struct BinTreeNode* _pLeft; // 指向当前节点左孩子
     struct BinTreeNode* _pRight; // 指向当前节点右孩子
     BTDataType _data; // 当前节点值域
}
// 三叉链
struct BinaryTreeNode
{
     struct BinTreeNode* _pParent; // 指向当前节点的双亲
     struct BinTreeNode* _pLeft; // 指向当前节点左孩子
     struct BinTreeNode* _pRight; // 指向当前节点右孩子
     BTDataType _data; // 当前节点值域
};

三、二叉树的顺序结构及实现

 1. 二叉树的顺序结构

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

 2. 堆的概念及结构

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

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

 3. 堆的实现

  3.1 堆的结构

typedef int HpDataType;
typedef struct Heap
{
	HpDataType* _a;
	int _size;
	int _capacity;
}Heap;

  3.2 堆的创建

        堆的构建这里我直接复用一下堆的初始化和插入,插入的里面我用的是向上调整建堆,这里不管是建大堆还是小堆,自己进行控制即可。

//堆的初始化
void HeapInit(Heap* hp)
{
	assert(hp);
	hp->_a = NULL;
	hp->_capacity = hp->_size = 0;
}
// 堆的构建
void HeapCreate(Heap* hp, HpDataType* a, int n)
{
	assert(hp);
	HeapInit(hp);

	for(int i = 0;i < n;++i)
	{
		HeapPush(hp, a[i]);
	}
}

//插入
void HeapPush(Heap* hp, HpDataType x)
{
	assert(hp);
	if (hp->_capacity == hp->_size)
	{
		HpDataType newcapacity = hp->_capacity == 0 ? 4 : hp->_capacity * 2;
		HpDataType* tmp = (HpDataType*)realloc(hp->_a, sizeof(HpDataType)* newcapacity);
		if (tmp == NULL)
		{
			perror("malloc fail");
			exit(-1);
		}
		
		hp->_a = tmp;
		hp->_capacity = newcapacity;
	}
	hp->_a[hp->_size] = x;
	hp->_size++;
	Adjustup(hp->_a,hp->_size - 1);//向上调整建堆
}

3.3 向上调整建堆

        因为从末尾插入一个数据以后,可能会让这个堆的节后出问题,所以每插入一个数据要进行一次调整。

        假如是建小堆,从最后的位置开始走,最后的下标一定是孩子,那么如果孩子位置的值比父亲位置的值小,就应该交换他们两的值,然后继续往上走。

//向上调整
void Adjustup(HpDataType* a,int child)
{
	int parent = (child - 1) / 2;

	while (child > 0)
	{
		if (a[child] < a[parent])//小堆
		{
			swap(&a[child], &a[parent]);
			child = parent;
			parent = (child - 1) / 2;
		}
		else
		{
			break;
		}
	}

}

  3.4 删除堆顶数据

        删除堆顶的数据不能直接把第一个数据删除掉,如果直接删除堆的节后就会出现问题,后面还需要重新建堆,费时费力。所以首先将末尾的数据和堆顶的数据进行交换,然后size--,数据就删除掉了,此时堆顶的数据是交换后的,不符合堆的结构,再把堆顶的数据向下调整,调整然后删除数据才算完成。

//删除堆顶数据
void HeapPop(Heap* hp)
{
	assert(hp);
	if (!HeapEmpty(hp))
	{
		swap(&hp->_a[0], &hp->_a[hp->_size - 1]);
		hp->_size--;
		Adjustdown(hp->_a,hp->_size,0);
	}
}

  3.5 向下调整

        还是以建小堆为例,先看左右孩子谁小,谁小就交换谁。孩子比父亲小,交换而后继续往下走,直到满足堆的结构,调整结束。

//向下调整
void Adjustdown(HpDataType* a,int n, int parent)
{
	int child = parent * 2 + 1;
	while (child < n)
	{
		if ( child + 1< n && a[child] > a[child + 1])
		{
			++child;
		}
		if (a[child] < a[parent])
		{
			swap(&a[child], &a[parent]);
			parent = child;
			child = parent * 2 + 1;
		}
		else
		{
			break;
		}
	}
}

  3.6 堆的其他函数

//显示堆的数据
void HeapPrint(Heap* hp)
{
	assert(hp);
	for(int i = 0;i < hp->_size;++i)
	{
		printf("%d ", hp->_a[i]);
	}
	printf("\n");
}
//交换
void swap(HpDataType* q1, HpDataType* q2)
{
	HpDataType tmp = *q1;
	*q1 = *q2;
	*q2 = tmp;
}
//判断是否为空
bool HeapEmpty(Heap* hp)
{
	assert(hp);
	return hp->_size == 0;
}
//获取堆顶的数据
HpDataType HeapTop(Heap* hp)
{
	assert(hp);
	return hp->_a[0];
}
//堆的数据个数
int HeapSize(Heap* hp)
{
	assert(hp);
	return hp->_size;
}
//堆的销毁
void HeapDestry(Heap* hp)
{
	assert(hp);
	free(hp->_a);
	hp->_capacity = hp->_size = 0;
}

 4.建堆时间复杂度 

        因为堆是完全二叉树,而满二叉树也是完全二叉树,此处为了简化使用满二叉树来证明( 时间复杂度本来看的就是近似值,多几个节点不影响最终结果)

由此得出:建堆的时间复杂度为 O(N)

 5. 堆的应用

  5.1 堆排序

        堆排序即利用堆的思想来进行排序,总共分为两个步骤:
1. 建堆
升序:建大堆
降序:建小堆
2. 利用堆删除思想来进行排序
        以升序为例,先把大堆建好,此时堆顶(也就是下标为0的元素)的数据一定是最大的,将堆顶数据和最后位置(也就是下标为n-1的元素)的数据进行交换,交换完成后,最大的数就在最后的位置,那么就不用去动这个数据了(size--)。而后将堆顶的数据进行向下调整,调整完成后,堆顶就是次大的了,而后继续交换,再次进行调整,直到排好顺序。

// 对数组进行堆排序
void HeapSort(int* a, int n)
{
	//升序建大堆,降序建小堆
	for (int i = (n-2)/2; i >= 0; --i)
	{
		Adjustdown(a, n, i);
	}
	//先选出最大的或最小的,交换到数组尾部,向下调整后再选出次大或次小的再次交换到尾部位置的前一个位置
	for (int i = 1; i < n; ++i)
	{
		swap(&a[0], &a[n - i]);
		Adjustdown(a, n - i, 0);
	}
}

  5.2 TOP-K问题

        TOP-K问题:即求数据结合中前 K 个最大的元素或者最小的元素,一般情况下数据量都比较大 比如:专业前 10 名、世界 500 强、富豪榜、游戏中前 100 的活跃玩家等。
        对于Top-K 问题,能想到的最简单直接的方式就是排序,但是:如果数据量非常大,排序就不太可取了 ( 可能数据都不能一下子全部加载到内存中) 。最佳的方式就是用堆来解决,基本思路如下:
1. 用数据集合中前 K 个元素来建堆
k 个最大的元素,则建小堆,
k 个最小的元素,则建大堆。
2. 用剩余的 N-K 个元素依次与堆顶元素来比较,不满足则替换堆顶元素
将剩余 N-K 个元素依次与堆顶元素比完之后,堆中剩余的 K 个元素就是所求的前 K 个最小或者最大的元素。
        以 前K个最大为例,假设n很大,k很小,从文件里读取数据,先将文件里的前k个数据插入到数组里,然后建小堆。建完堆以后依次读取数据,每读一个就与堆顶的数据进行比较,如果比堆顶的数据大,那么就替代掉它,然后向下调整,直到把n个数据读完,此时队里的数据就是前K个对打的元素,但是堆里的数据不能保证是有序的,可以选完之后再排序,效率会高很多。
//找n个数里最大或最小的前K个
void PrintTopK(int* a, int n, int k)
{
	const char* filename = "test1.txt";
	FILE* fout = fopen(filename, "r");
	if (fout == NULL)
	{
		perror("fopen fail");
		return;
	}
	
	
	for(int i = 0;i<k;++i)
	{
		fscanf(fout, "%d", &a[i]);
		
	}
	for (int i = (k - 2) / 2; i >= 0; --i)
	{
		Adjustdown(a, k, i);
	}
	int ret = 0;
	while (fscanf(fout, "%d", &ret) != EOF)
	{
		if (ret > a[0])
		{
			a[0] = ret;
			Adjustdown(a, k, 0);
		}
	}

	//free(a);
	fclose(fout);
}
//往文件里写数据
void WriteData(const char* filename, int n)
{
	FILE* fin = fopen(filename, "w");
	if (fin == NULL)
	{
		perror("fopen fail");
		return;
	}
	srand(time(NULL));

	while (n--)
	{
		fprintf(fin, "%d ", rand());
	}


	fclose(fin);
}

  6. 二叉树链式结构的实现

   6.1 二叉树的结构

这里再回顾一下二叉树的概念:

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

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

//符号和结构的定义
typedef char BTDataType;
typedef struct BinaryTreeNode
{
	BTDataType data;
	struct BinaryTreeNode* left;
	struct BinaryTreeNode* right;
}BTNode;

   6.2 二叉树的遍历

二叉树的遍历分为四种:前序,中序,后序以及层序。

        二叉树结构,最简单的方式就是遍历。二叉树遍历 是按照某种特定的规则,依次对二叉 树中的节点进行相应的操作,并且每个节点只操作一次 。访问结点所做的操作依赖于具体的应用问题。 遍历是二叉树上最重要的运算之一,也是二叉树上进行其它运算的基础。
二叉树每种遍历的顺序:
前序:根,左子树,右子树;
中序:左子树,根,右子树;
后序:左子树,右子树,根;
层序:第一层,第二层,第N层,(从左往右)
这里层序遍历放到后面单独说,前中后序遍历很相似,我就只拿前序遍历进行讲解。
前序遍历的递归图解:
首先访问根节点1,不为空就继续往下走,然后访问它的左子树,访问它的左子树时会重新调用函数,加一层函数栈帧,此时在2这个节点栈帧中,2是1的左子树同时也是根,不为空输出2,继续往下走,到3这个节点的栈帧,不为空然后输出3,进入3的左子树,为空返回上一层(也就是3这个节点的函数栈帧),3的左为空返回,进入3的右,3的右也为空,然后返回。此时3这个节点的函数栈帧运行完毕返回(是以2的左返回),2的左返回,进入2的右,2的右为空返回空,2的函数栈帧运行完毕返回,此时1的左子树运行完毕,进入1的右子树,还是以刚才的调用继续往下走,直到1这个根节点的函数栈帧运行完毕,返回了以后,前序遍历就算完成了。

// 二叉树前序遍历
void BTPrevOrder(BTNode* root)
{
	if (root == NULL)
	{
		printf("NULL ");
		return;
	}
	printf("%c ", root->_Data);
	BTPrevOrder(root->_Left);
	BTPrevOrder(root->_Right);
}
// 二叉树中序遍历
void BTInOrder(BTNode* root)
{
	if (root == NULL)
	{
		printf("NULL ");
		return;
	}
	BTPrevOrder(root->_Left);
	printf("%d ", root->_Data);
	BTPrevOrder(root->_Right);
}
// 二叉树后序遍历
void BTPostOrder(BTNode* root)
{
	if (root == NULL)
	{
		printf("NULL ");
		return;
	}
	BTPrevOrder(root->_Left);
	BTPrevOrder(root->_Right);
	printf("%d ", root->_Data);
}

 6.2 构建二叉树

        这里节点的数据用的是字符,数字也一样,只需更改自定义的类型即可。用前序遍历去构建二叉树,用#代表NULL,将数组a里的内容依次赋值到每个节点上。

// 通过前序遍历的数组"ABD##E#H##CF##G##"构建二叉树
BTNode* BinaryTreeCreate(BTDataType* a, int* pi)
{
	if (a[*pi] == '#')
	{
		(*pi)++;
		return NULL;
	}
	BTNode* root = (BTNode*)malloc(sizeof(BTNode));
	if (root == NULL)
	{
		perror("malloc fail");
		return NULL;
	}
	
	root->_Data = a[(*pi)++];
	root->_Left = BinaryTreeCreate(a, pi);
	root->_Right = BinaryTreeCreate(a, pi);

	return root;
}

  6.3 二叉树的节点个数

// 二叉树节点个数
int BTSize(BTNode* root)
{
	if (root == NULL)
		return 0;
	return BTSize(root->_Left) + BTSize(root->_Right) + 1;
}
// 二叉树叶子节点个数
int BTLeafSize(BTNode* root)
{
	if (root == NULL)
		return 0;
	int leSize = BTLeafSize(root->_Left);
	int riSize = BTLeafSize(root->_Right);

	return leSize > riSize ? leSize + 1 : riSize + 1;
}
// 二叉树第k层节点个数
int BTLevelKSize(BTNode* root, int k)
{	
	assert(k > 0);
	if (root == NULL)
		return 0;
	if (k == 1)
		return 1;
	return BTLevelKSize(root->_Left, k - 1) + BTLevelKSize(root->_Right, k - 1);
}

  6.4 查找节点

        这里如果找到了,直接一层一层返回当前节点即可,如果左子树找不到,右子树也找不到需要返回空。

// 二叉树查找值为x的节点
BTNode* BinaryTreeFind(BTNode* root, BTDataType x)
{
	if (root == NULL)
		return NULL;
	if (root->_Data == x)
	{
		return root;
	}
	BTNode* lret = BinaryTreeFind(root->_Left, x);
	if (lret != NULL)
		return lret;
	BTNode* rret = BinaryTreeFind(root->_Right, x);
	if (rret != NULL)
		return rret;
	return NULL;
}

  6.5 销毁二叉树

// 二叉树销毁
void BinaryTreeDestory(BTNode* root)
{
	if (root == NULL)
		return;
	BinaryTreeDestory(root->_Left);
	BinaryTreeDestory(root->_Right);
	free(root);

}

  6.6 层序遍历

        这里需要用到队列,那么就需要参考之前的代码咯。

// 层序遍历
void BTLevelOrder(BTNode* root)
{
	Queue q;
	QueueInit(&q);//队列初始化
	if (root)
		QueuePush(&q, root);//入队列
	while (!QueueEmpty(&q))
	{
		BTNode* front = QueueFront(&q);//获取队头
		QueuePop(&q);//出队列
		printf("%c ", front->_Data);

		if(front->_Left)
			QueuePush(&q, front->_Left);
		if (front->_Right)
			QueuePush(&q, front->_Right);
	}
	printf("\n");

	QueueDestory(&q);//销毁队列
}

这里用队列的代码需要注意将二叉树的结构放到队列的头文件中,不然编译报错过不了。

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

        就是以层序遍历的方法遍历二叉树,先入根,根不为空就出队列,同时让它的左右子树也入队列,一直遍历直到遇到第一个空,那么它的后面也都必须是空,否则就不是完全二叉树。

// 判断二叉树是否是完全二叉树
int BTComplete(BTNode* root)
{
	Queue q;
	QueueInit(&q);
	if (root)
		QueuePush(&q, root);
	while (!QueueEmpty(&q))
	{
		BTNode* front = QueueFront(&q);
		QueuePop(&q);
		if (front == NULL)
		{
			break;
		}
			QueuePush(&q, front->_Left);
			QueuePush(&q, front->_Right);
	}
	while (!QueueEmpty(&q))
	{
		BTNode* front = QueueFront(&q);
		QueuePop(&q);
		if (front != NULL)
		{
			QueueDestory(&q);
			return false;
		}
	}
	QueueDestory(&q);
	return true;
}

完整代码:Binary/Binary · 晚风不及你的笑/作业库 - 码云 - 开源中国 (gitee.com)

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

晚风不及你的笑427

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

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

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

打赏作者

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

抵扣说明:

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

余额充值