【数据结构与算法篇】一文详解数据结构之二叉树

一 . 树的概念

    树是一种非线性的数据结构,它是由n(n>=0)个有限结点组成一
个具有层次关系的集合。把它叫做树是因为它看起来像一棵倒挂的树,
也就是说它是根朝上,而叶朝下的。
  • 根结点: 树中的从上开始的第一个节点, 是树中的特殊节点
    • 根节点没有前驱结点
    • 有至少一个或者n个后继节点
  • 其余结点: 除根节点之外的节点 。 它们被分成M(M>0)个互不相交的集合T1、T2、……、Tm,其中每一个集合Ti(1<= i
    <= m)又是一棵结构与树类似的子树。
  • 每棵子树的根结点
    • 有且只有一个前驱(所有子树的根节点)
    • 可以有0个或多个后继
  • 因此,树是递归定义的
    • 递归 : 复杂问题拆解成多个类似的小问题进行求解。
  • 注意:树形结构中,子树之间不能有交集,否则就不是树形结构

二 . 相关术语

在这里插入图片描述

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

三 . 树的表示

  • 树有很多种表示方式, 例如:
    • 双亲表示法
    • 孩子表示法
    • 孩子双亲表示法
    • 孩子兄弟表示法等
  • 孩子兄弟表示法是最常用的一种表示法, 介绍如下:
  • 树中的任意一个节点的组成
    • 值域 : 存储数据
    • 孩子节点 : 指向其第一个孩子节点
    • 兄弟节点 : 指向它的下一个兄弟节点
typedef int DataType
struct TreeNode
{
	struct TreeNode* firstChild1;   // 指向其第一个孩子节点
	struct TreeNode* nextBrother;  // 指向其下一个兄弟节点
	DataType _data;              // 节点中的数据域
}

左孩子右兄弟表示法 : 指向左边第一个孩子节点(子节点), 指向右边第一个兄弟节点

高度为h的完全二叉树 : 前h-1层为满的, 第h层不一定满, 但是第h层一定是从左到右连续的

四 . 什么是二叉树

一棵二叉树是节点的有限集合

  • 节点可以为空, 此时为空树
  • 可以只有一个节点, 该节点被称为根节点
  • 也可以由根与子树构成(左子树和右子树, 子树可为空)

在这里插入图片描述

1> 二叉树的特性

  • 二叉树的度最大为2
  • 每个父节点所拥有子节点的个数为 1或2
  • 不存在子节点的节点是二叉树中的叶节点
  • 二叉树的子树有左右之分, 不可以颠倒。 因此二叉树也被称为有序树

对于任意的二叉树都是由以下情况复合而成的
在这里插入图片描述

2> 特殊的二叉树

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

3> 二叉树的性质

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

五 . 二叉树的存储结构

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

1> 二叉树的顺序存储

  • 顺序结构存储就是使用数组来存储
  • 一般使用数组只适合表示完全二叉树,因为非完全二叉树会有空间的浪费
  • 在现实中使用中 只有数据结构 - 堆(完全二叉树)才会使用数组来存储
  • 二叉树的顺序存储在物理上通过数组来实现,在逻辑上是一颗二叉树的结构
  • 完全二叉树的顺序存储:
    完全二叉树的顺序存储
  • 非完全二叉树的顺序存储:
    在这里插入图片描述

2> 二叉树的链式存储

二叉树的链式存储结构是指,用链表来表示一棵二叉树,即用链来指示元素的逻辑关系。 通常的方法是:

  • 链表中每个结点由三个域组成,数据域和左右指针域
  • 左右指针分别用来指向该结点的左孩子节点和右孩子节点
  • 链式结构又分为二叉链和三叉链,在本篇文章中讲述的是二叉链
  • 二叉链:
    • 在这里插入图片描述
  • 三叉链:
    • 在这里插入图片描述

六 . 堆

1> 什么是堆

完全二叉树的顺序存储结构被称之为堆
堆分为大堆和小堆:

  • 大堆 : 父节点 >= 子节点 (指的是节点中存储的值)
  • 小堆 : 父节点 <= 子节点 (指的是节点中存储的值)

2> 用堆存储数据

堆是一种数据结构, 可以看作是一种容器, 用来存放数据, 若要将数据存入堆中, 那么要通过以下步骤:

    1. 将数据按照完全二叉树的顺序存储方式存入到一维数组中,
  • 2 . 调整数组中元素的关系, 必须满足 父节点 >=(<=) 子节点
  • 3 . 当数组中的元素满足在二叉树中 父节点 >= 子节点时, 这是相当于将数据存入大堆中
  • 4 . 如果满足 父节点 <= 子节点, 这是相当于将数据存入小堆中。
  • 堆示例图:
    • 在这里插入图片描述

3> 堆的Cpp代码实现

1) 堆的初始化

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

	php->_arr = new HPDataType[4];
	php->capacity = 4;
	php->size = 0;
}

2) 堆的销毁

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

	if (!php)
	{
		return;
	}
	delete[] php->_arr;
	php->_arr = NULL;
	php->capacity = php->size = 0;
}

3) 堆的遍历

// 堆的遍历
void HPPrint(HP* php)
{
	for (int i = 0; i < php->size; i++)
	{
		cout << php->_arr[i] << " ";
	}
	cout << endl;
}

4) 向上(或向下)调整建大堆(或小堆)

小堆中向上调整
// 堆向上调整 (堆中插入或互换元素向上调整) (小堆)
void AdjustUpS(HPDataType* a, int child)
{
	while (child)
	{
		int parent = (child - 1) / 2;
		if (a[parent] < a[child])
			return;
		else
		{
			HPSwap(a[parent], a[child]);
			child = parent;
		}
	}
}
小堆中向下调整
// 堆向下调整 (堆中插入或互换元素向下调整) (小堆)
void AdjustDownS(HPDataType* a, int n, int parent)
{
	int child = parent * 2 + 1;
	while (child < n)
	{
		if (child + 1 < n && a[child + 1] < a[child])
		{
			++child;
		}
		if (a[child] < a[parent])
		{
			HPSwap(a[child], a[parent]);
			parent = child;
			child = parent * 2 + 1;
		}
		else
			break;
	}
}
大堆中向上调整
// 堆向上调整 (堆中插入或互换元素向上调整) (大堆)
void AdjustUpB(HPDataType* a, int child)
{
	while (child)
	{
		int parent = (child - 1) / 2;
		if (a[parent] > a[child])
			return;
		else
		{
			HPSwap(a[parent], a[child]);
			child = parent;
		}
	}
}
大堆中向下调整
// 堆向下调整 (堆中插入或互换元素向下调整) (大堆)
void AdjustDownB(HPDataType* a, int n, int parent)
{
	int child = parent * 2 + 1;
	while (child < n)
	{
		if (child + 1 < n && a[child + 1] > a[child])
		{
			++child;
		}
		if (a[child] > a[parent])
		{
			HPSwap(a[child], a[parent]);
			parent = child;
			child = parent * 2 + 1;
		}
		else
			break;
	}
}

5) 堆的插入

// 堆的插入 (小堆 插入元素向上调整)
void HPPush(HP* php, HPDataType val)
{
	assert(php);

	if (php->size == php->capacity)
	{
		HPDataType* newHP = new HPDataType[php->capacity * 2];
		for (int i = 0; i < php->size; i++)
		{
			newHP[i] = php->_arr[i];
		}
		delete[] php->_arr;
		php->_arr = newHP;
	}
	php->_arr[php->size] = val;
	++php->size;

	AdjustUpS(php->_arr, php->size - 1);       // 对新插入堆中的值 val 进行比较排序, 直至符合小堆为止
}

6) 堆删除

// 堆删除 (小堆, 删除 向上调整)
void HPPop(HP* php)
{
	assert(php);
	assert(php->size > 0);

	HPSwap(php->_arr[0], php->_arr[php->size - 1]);
	--php->size;

	AdjustDownS(php->_arr, php->size, 0);      // 再进行向下调整
}

7) 返回堆顶元素

// 返回堆顶元素
HPDataType HPTop(HP* php)
{
	assert(php);

	assert(php->size > 0);

	return php->_arr[0];
}

8) 返回堆中元素个数

// 返回堆中元素的个数
int HPSize(HP* php)
{
	assert(php);

	return php->size;
}

9) 判断堆是否为空

// 判断堆是否为空
bool HPEmpty(HP* php)
{
	return php->size == 0;
}

4> 堆排序

1) 排升序

排升序 : 将待排序序列排为升序, 步骤如下:

    1. 将待排序序列构造为一个大堆
    1. 利用堆顶元素, 也就是根元素是堆中最大值的特点, 将堆顶元素换至堆末尾
    1. 然后将新的堆顶元素进行向下调整
      注意:此时向下调整时, 原堆顶元素(被换至末尾的堆顶元素)不参与向下调整
    1. 之后再次进行如上步骤
    1. 直至待排序序列完全为升序

2) 排降序

派降序: 将待排序序列排为降序, 步骤如下:

    1. 将待排序序列构造为一个小堆
    1. 利用堆顶元素, 也就是根元素是堆中最小值的特点, 将堆顶元素换至堆末尾
    1. 然后将新的堆顶元素进行向下调整
      注意:此时向下调整时, 原堆顶元素(被换至末尾的堆顶元素)不参与向下调整
    1. 之后再次进行如上步骤
    1. 直至待排序序列完全为降序

5> TOP - K问题

TOP-K问题:即求数据结合中前K个最大的元素或者最小的元素,一般情况下数据量都比较大。

  • 比如:专业前10名、世界500强、富豪榜、游戏中前100的活跃玩家等。
  • 对于Top-K问题,能想到的最简单直接的方式就是排序,但是:如果数据量非常大,排序就不太可取了(可能数据都不能一下子全部加载到内存中)。最佳的方式就是用堆来解决,基本思路如下:
    1. 用数据集合中前K个元素来建堆
      • 前k个最大的元素,则建小堆
      • 前k个最小的元素,则建大堆
    2. 用剩余的N-K个元素依次与堆顶元素来比较,不满足则替换堆顶元素
    3. 将剩余N-K个元素依次与堆顶元素比完之后,堆中剩余的K个元素就是所求的前K个最小或者最大的元素。

七 . 二叉树的链式存储结构

1> 链式二叉树的代码实现

1) 链式二叉树的四种遍历方式

前序遍历
// 链式二叉树的前序遍历
void PreOrder(BTNode* root)
{
	if (!root)
	{
		cout << " ";
		return;
	}
	cout << root->data << " ";
	PreOrder(root->left);
	PreOrder(root->right);
}
中序遍历
// 链式二叉树的中序遍历
void InOrder(BTNode* root)
{
	if (!root)
	{
		//cout << " ";
		return;
	}
	PreOrder(root->left);

	cout << root->data << " ";

	PreOrder(root->right);
}

后序遍历
// 链式二叉树的后序遍历
void EpilOgue(BTNode* root)
{
	if (!root)
	{
		//cout << " ";
		return;
	}
	PreOrder(root->left);
	PreOrder(root->right);

	cout << root->data << " ";
}
层序遍历

链式二叉树的层序遍历需要依靠队列这一种数据结构

// 链式二叉树的层序遍历
void Sequence(BTNode* root)
{
	queue<BTNode*> que;
	if (root)
	{
		que.push(root);
	}
	while (!que.empty())
	{
		BTNode* front = que.front();
		cout << front->data << " ";
		que.pop();

		if(front->left)
		    que.push(front->left);
		if(front->right)
		    que.push(front->right);
	}
	cout << endl;
}

2) 返回二叉树中节点的个数

返回二叉树中节点的总个数
// 返回链式二叉树中节点的总个数 (后序递归遍历)
int TreeSize(BTNode* root)
{
	// return root == NULL ? 0 : TreeSize(root->left) + TreeSize(root->right) + 1;
	if (!root)
	{
		return 0;
	}
	return TreeSize(root->left) + TreeSize(root->right) + 1;
}
返回二叉树中叶子节点的个数
// 返回链式二叉树中叶子节点的个数
int TreeLeafSize(BTNode* root)
{
	if (!root)
	{
		return 0;
	}
	if (root->left == NULL && root->right == NULL)
	{
		return 1;
	}
	return TreeLeafSize(root->left) + TreeLeafSize(root->right);
}
返回二叉树中第k层节点的总个数
// 返回链式二叉树第k层所有节点的个数 (非完全二叉树)
int TreeKLevel(BTNode* root, int k)
{
	assert(k > 0);

	if (!root)
		return 0;

	if (k == 1)
		return 1;

	return TreeKLevel(root->left, k - 1) + TreeKLevel(root->right, k - 1);
}

3) 二叉树中的查找

//  链式二叉树中查找值为val的节点 并返回其地址
BTNode* TreeFind(BTNode* root, BTDataType val)
{
	if (!root)
		return NULL;

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

	BTNode* ret = TreeFind(root->left, val);
	if (ret)
		return ret;

	ret = TreeFind(root->right, val);
	if (ret)
		return ret;

	return NULL;
}

4) 返回二叉树的高度

// 返回二叉树的高度(或者说 最大深度)
int TreeHight(BTNode* root)
{
	if (!root)
		return 0;

	int left = TreeHight(root->left);
	int right = TreeHight(root->right);

	return left >= right ? left + 1 : right + 1;
}

5) 判断是否为完全二叉树

// 判断是否为完全二叉树
int TreeComplete(BTNode* root)
{
	queue<BTNode*> q1;
	if (root)
		q1.push(root);

	while (!q1.empty())
	{
		BTNode* front = q1.front();
		if (!front)
			break;
		q1.push(front->left);
		q1.push(front->right);

		q1.pop();
	}

	while (!q1.empty())
	{
		if (q1.front())
			return false;
		q1.pop();
	}

	return true;
}

6) 二叉树的销毁

// 链式二叉树的销毁
void TreeDestory(BTNode** root)
{
	if (!(*root))
		return;

	TreeDestory(&((*root)->left));
	TreeDestory(&((*root)->right));

	delete *root;
	*root = NULL;
}
  • 4
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值