爆砍数据结构 —— 二叉树大杂烩

文章目录

  • 一、对二叉树的基本认识
    • 1.1、概念
    • 1.2、特殊二叉树
    • 1.3、二叉树的基本性质
    • 1.4、二叉树的存储结构
  • 二、二叉树的顺序结构以及实现
    • 2.1、二叉树的顺序结构
    • 2.2、堆的概念及结构
    • 2.3、堆的实现
      • 2.3.1、向下调整算法
      • 2.3.2、堆的创建
      • 2.3.3、堆的时间复杂度
      • 2.3.4、堆的代码实现
    • 2.4、堆的应用
      • 2.4.1堆排序
      • 2.4.2、TOP-K问题
  • 三、二叉树链式结构的实现
    • 3.1、二叉树的遍历
      • 3.1.1、前序遍历的实现
      • 3.1.2、中序遍历的实现
      • 3.1.3、后序遍历的实现
      • 3.1.4、层序遍历
    • 3.2、二叉树的一些附加功能
  • 四、总结

程序员和上帝打赌要开发出更大更好——傻瓜都会用的软件。而上帝却总能创造出更大更傻的傻瓜。所以,上帝总能赢。——Anon

一、对二叉树的基本认识

1.1、概念

二叉树是结点的一个有限集合,我们可以来看下图:
爆砍数据结构 —— 二叉树大杂烩_第1张图片

从上图我们很容易可以发现两点:

1、二叉树不存在超过度大于2的结点

2、二叉树的子树有左右之分,次序是不能颠倒的,所以我们可以将二叉树称为有序树

1.2、特殊二叉树

对于特殊二叉树而言,我们一般分为两种

  1. 满二叉树:树如其名,存在一个二叉树,每一层的结点数都达到了最大值,我们就称这个二叉树为满二叉树。通俗来讲,如果一个二叉树的层数为K,并且结点总数为2的K次方-1,则他就是满二叉树’
  2. 完全二叉树:完全二叉树和满二叉树有不小的渊源,或者可以说完全二叉树就是由满二叉树引出来的。对于深度为K,有n个结点的二叉树,当且仅当其每一个结点都与深度为K的满二叉树中编号为1到n的结点一一对应时,我们称之为完全二叉树,需要注意的是满二叉树是特殊的完全二叉树。

1.3、二叉树的基本性质

二叉树的性质有很多,我们在这里就总结比较常见且实用的几种

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

1.4、二叉树的存储结构

一般来说,我们将二叉树分为两种存储结构,一种是顺序存储结构,一种是链式结构

  1. 顺序存储

顺序结构存储是使用数组来存储,但是一般只有在表示完全二叉树的时候我们才会使用数组,这是因为不是完全二叉树会有空间的浪费。而在现实中只有堆才会使用数组来存储,这些都是后话,我们在后面会单独对堆进行讲解。总之通俗来讲,二叉树的顺序存储在物理上是一个数组,在逻辑上是一颗二叉树。
爆砍数据结构 —— 二叉树大杂烩_第2张图片

  1. 链式存储

链式存储结构是指用链表来表示一棵二叉树,也就是用来表示元素的逻辑关系。通常的方法是链表中每个结点由三个域组成,分别是数据域和左右指针域。左右指针分别左右指针分别用来给出该结点左孩子和右孩子所在的链结点的存储地址 。链式结构又分为二叉链和三叉链,三叉链暂时我们不会接触,等到对数据结构有更加深入的理解后我们再进行学习。
爆砍数据结构 —— 二叉树大杂烩_第3张图片

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

2.1、二叉树的顺序结构

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

爆砍数据结构 —— 二叉树大杂烩_第4张图片

2.2、堆的概念及结构

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

NOTE:

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

爆砍数据结构 —— 二叉树大杂烩_第5张图片

2.3、堆的实现

2.3.1、向下调整算法

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

爆砍数据结构 —— 二叉树大杂烩_第6张图片

2.3.2、堆的创建

下面我们给出一个数组,这个数组逻辑上可以看做一颗完全二叉树。但这仍然不是一个堆,我们要通过一个算法,把他构建成一个堆。那么现在左右子树不是堆,要怎么进行调整呢?这里我们应该从倒数的第一个非叶子节点的子树开始调整,一直调整到根节点的树,就可以调整成堆了。

爆砍数据结构 —— 二叉树大杂烩_第7张图片

2.3.3、堆的时间复杂度

因为堆是完全二叉树,而满二叉树也是完全二叉树,此处为了简化使用满二叉树来证明(因为时间复杂度本身就不是一个精确的值,所以多几个节点并不会影响最终结果):

爆砍数据结构 —— 二叉树大杂烩_第8张图片

看起来很麻烦,其实逻辑还是比较简单的,这里运用到了高中数列的错位相减的知识,我们可以很容易得出建堆的时间复杂度是O(N)

2.3.4、堆的代码实现

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

// 堆的构建
void HeapCreate(Heap* hp, HPDataType* a, int n);
// 堆的销毁
void HeapDestory(Heap* hp);
// 堆的插入
void HeapPush(Heap* hp, HPDataType x);
// 堆的删除
void HeapPop(Heap* hp);
// 取堆顶的数据
HPDataType HeapTop(Heap* hp);
// 堆的数据个数
int HeapSize(Heap* hp);
// 堆的判空
int HeapEmpty(Heap* hp);

2.4、堆的应用

2.4.1堆排序

堆排序是利用堆的思想来进行排序,一个有两个步骤:

1、建堆

  • 建大堆:升序
  • 建小堆:降序

2、利用堆排序思想来进行排序

建堆和堆删除都用到了向下调整,因为掌握了向下调整,就可以完成堆排序。

爆砍数据结构 —— 二叉树大杂烩_第9张图片

2.4.2、TOP-K问题

什么是 Top K 问题? 简单来说就是在一堆数据里面找到前 K 大(当然也可以是前 K 小)的数。 这个问题也是十分经典的算法问题,不论是面试中还是实际开发中,都非常典型。

对于Top-K问题,能想到的最简单直接的方式就是排序,但是:如果数据量非常大,排序就不太可取了(可能
数据都不能一下子全部加载到内存中)。最佳的方式就是用堆来解决,基本思路如下:

  1. 用数据集合中前K个元素来建堆
  • 前k个最大的元素,则建小堆
  • 前k个最小的元素,则建大堆
  1. 用剩余的N-K个元素依次与堆顶元素来比较,不满足则替换堆顶元素

将剩余N-K个元素依次与堆顶元素比完之后,堆中剩余的K个元素就是所求的前K个最小或者最大的元素。

这里我们就不给出详细代码,在后续的博客会另外讲这个问题的多种实现方法。

三、二叉树链式结构的实现

3.1、二叉树的遍历

学习二叉树的结构,最简单的方法就是遍历,二叉树遍历就是按照某种特定的规则,依次对二叉树中的节点进行相应的操作,并且每一个节点只操作一次。访问结点所做的操作要看具体需要我们解决的是什么问题。遍历是二叉树最重要的运算之一,也是在二叉树上进行其他运算的基础。

二叉树的遍历一般有:前序/中序/后序的递归遍历结构:

遍历顺序:

  • 前序:根节点 —— 左子树 —— 右子树
  • 中序:左子树 —— 根节点 —— 右子树
  • 后序:左子树 —— 右子树 —— 根节点

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

// 二叉树前序遍历
void PreOrder(BTNode* root);
// 二叉树中序遍历
void InOrder(BTNode* root);
// 二叉树后序遍历
void PostOrder(BTNode* root);

3.1.1、前序遍历的实现

前序遍历递归图解:
爆砍数据结构 —— 二叉树大杂烩_第10张图片

代码实现:

void BinaryTreePrevOrder(BTNode* root)
{
	if (root == NULL)
	{
		printf("NULL");
		return;
	}

	printf("%d ", root->data);
	BinaryTreePrevOrder(root->_left);
	BinaryTreePrevOrder(root->_right);
}

3.1.2、中序遍历的实现

中序遍历在实现的逻辑上其实和前序遍历没有任何差别,只需要调换下根和左子树的遍历顺序即可

void BinaryTreeInOrder(BTNode* root)
{
	if (root == NULL)
	{
		printf("NULL ");
		return;
	}

	BinaryTreeInOrder(root->_left);
	printf("%c ", root->data);
	BinaryTreeInOrder(root->_right);
}

3.1.3、后序遍历的实现

后序遍历也是如此,只需要改变一下遍历顺序即可

void BinaryTreePostOrder(BTNode* root)
{
	if (root == NULL)
	{
		printf("NULL");
		return;
	}

	BinaryTreePostOrder(root->_left);
	BinaryTreePostOrder(root->_right);
	printf("%d ", root->data);
}

3.1.4、层序遍历

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

爆砍数据结构 —— 二叉树大杂烩_第11张图片

代码实现如下:

void BinaryTreeLevelOrder(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");

	QueueDestroy(&q);
}

3.2、二叉树的一些附加功能

这些功能运用到的基本都是前面的知识以及简单的逻辑,在这里就不一一介绍了,附上代码供各位参考;

// 二叉树节点个数
int BinaryTreeSize(BTNode* root)
{
	return root == NULL ? 0 :
		BinaryTreeSize(root->_left) + BinaryTreeSize(root->_right) + 1;
}
// 二叉树叶子节点个数
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);
}
// 二叉树第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);
}

四、总结

总的来说,二叉树是数据结构中非常重要的一个部分,所含的内容也是比较冗杂的,我认为需要一定量的代码练习来加以巩固,大家可以自行去leetcode和牛客上找相应的题目练练手,希望本文对大家二叉树的入门有所帮助!

你可能感兴趣的:(数据结构,数据结构,c语言)