【数据结构】树的基本概念及存储

前言:树是数据结构中很大的一个概念,可以分为二叉树、红黑树等等分支。一般用于优化我们的搜索和实现一些排序比如堆排序等等

1.树的基本概念

树是一种非线性的数据结构,由n(>= 0)个结点组成而且具有一定的层序的结构。最上面只有一个结点被称为根结点,根结点下面有很多个分支,各个分支又有各自的分支。看名字也知道这个数据结构有点像一颗树,但这颗树应该是倒着的更像:

如果有数字和符号抽象一下就长这样:

其中结点1没有任何的前驱结点,也是树的开始所以被称之为根结点。整个结构被称之为树,但是我们又可以将2节点视为下面结点(5、6结点)的根结点,这个树被称之为子树,子树之间又可以分为更小规模的子树。所以可以知道树是一个由递归定义的数据结构

子树之间不能有交集,除了根结点外每个结点有且仅有一个父结点,所以一个树如何有N个节点那它就会有N - 1 条边,像下面的结构就是非树的结构:

1.1树的相关概念

我们以下面这幅图作为例子来讲解树的各个相关概念,比较重要的概念我会加粗:

(1)结点的度:一个结点下面有几个子树,比如结点5的度为3;

(2)叶结点或者终端结点:度为0的结点比如上面的2、7、8、14、15、10、11、12、13;

(3)非终端结点或分支结点:度不为0的结点

(4)双亲结点或者父结点:如果一个结点有子结点,那它就是这个子结点的父结点

(5)孩子结点或子结点:与父结点相对,比如3是7的父结点那7就是3的子结点;

(6)兄弟结点:有形同的父结点;

(7)树的度:一颗树中结点的度其中最大的那个,比如上面的图中树的度为5;

(8)结点的层次:一般以根节点为第一层,根的子结点为第二层,比如10结点在第3层

(9)树的深度或者高度:树中结点的最大层次比如上面的图高度为4;

(10)堂兄弟结点:处于同一层的结点互相之间;

(11)结点的祖先:从该结点到根结点所经的所有分支结点;

(12)子孙:以该点为根节点的子树下面的结点都称之为该点的子孙;

(13) 森林:有N(>0)的数组成的集合

树的概念虽然有很多,但是只有几个常常用到的就是我加粗的那几个而已其实联想一下我们现实的家族关系都很容易理解


1.2有序树和无序树

(1)有序树:结点的子树按照从左往右的顺序严格排列,不能更改。
(2)无序树:结点的子树之间没有顺序,可以更改。
像我们后面学习的二叉树就是一个有序树,需要区分左右孩子所以不能随意的更改


1.3有根树与无根树

(1)有根树:根节点是固定

(2)无根树:根节点不固定,谁都可以是根结点。

树有根无根主要会影响树的存储,我们见到的树一般都是由根数,如果是无根树的话因为我们没法确定哪个是父结点,哪个是子结点我们只知道它们之间有一条边。所以在存储时要存 a 有一个结点b也有一个结点a;


2.树的表示与存储

树是一个比较复杂的数据结构,这就注定了它的存储会比较的复杂,网络上有很多树的表示法和存储的方式,比如:双亲表示法、孩子表示法、孩子双亲表示法、孩子兄弟表示法

有这么多的表示法我这里就只介绍孩子兄弟的表示法的概念,代码实现会在后面二叉树那里给实现了。因为孩子表示法比较简单所以我这里也简单介绍下和简单实现下代码


2.1孩子兄弟表示法

孩子兄弟表示法又被称为左孩子右兄弟表示法,这个方法可以将一个普通的树以二叉树的方式来进行存储,在这个表示法中我们需要维护两个指针

cpp 复制代码
struct TreeNode {
    ElemType vla;
    TreeNode *Leftchild;   // 第一个孩子
    TreeNode *Rrigtsibling;  // 下一个兄弟
};

一个指向这个结点的第一个孩子,一个指向该结点在树中的下一个兄弟:

这张图由AI制作因为我感觉它画得比我好看多了。。。

后面我会在二叉树那里实现代码


2.2孩子表示法

我们一般用的都是孩子兄弟表示法,孩子表示法我一般都只在刷刷算法题的时候用用

孩子表示法就是把每个结点的孩子信息给存起来,无根树的情况上面有所过了我就不赘述了,这里就介绍两个存储的方法

假如我这里有一颗树,一共有n个结点,编号分别为1到n

2.2.1利用STL中的vector数组存储:

vector是C++的STL中为我们提供的变长数组,我们可以创建N个变长数组:

cpp 复制代码
vector<int> edges[N];

其中edges[i]就存储着i号结点所连接的结点:

cpp 复制代码
#include <iostream>
#include <vector>
using namespace std;

const int N = 1e5 + 10;
int n;
vector<int> edges[N];

int main()
{
	cin >> n;
	for (int i = 1; i < n; i++)
	{
		int a, b; cin >> a >> b;
		edges[a].push_back(b);
		edges[b].push_back(a);
	}
	return 0;
}

2.2.2链式向前星

这个顾名思义就是利用链表的形式来存储孩子的信息,我这里就使用数组来模拟链表来存储了:

cpp 复制代码
#include <iostream>
using namespace std;

const int N = 1e5 + 10;
int e[N * 2], ne[N * 2], id;
int h[N];
int n;

void add(int a, int b)
{
	id++;
	e[id] = a;
	ne[id] = h[a];
	h[a] = id;
}

int main()
{
	cin >> n;
	for (int i = 1; i < n; i++)
	{
		int a, b; cin >> a >> b;
		add(a, b), add(b, a);
	}
	
	
	return 0; 
}

这样写确实是有点麻烦,主要是list有点耗时,这里就当看个乐得了工程中也一般不用孩子表示法


2.3孩子表示法的遍历

树因为是一个非线性的数据结构,所以遍历也不太像之前那样方便,我这里提供了两种遍历方式分别为深度优先遍历(DFS)和宽度优先遍历(DFS)

2.3.1深度优先遍历(DFS)

因为树这个数据结构本来就是由递归定义的,所以我们同样可以使用递归的方式来遍历它。深度优先遍历有点一条路走到黑的感觉,因此我们可以先创建一个布尔类型的数组st来记录这个结点是否被遍历过来防止递归死循环;

用vector存储:

cpp 复制代码
#include <iostream>
#include <vector>
using namespace std;
const int N = 1e5 + 10;
int n;
vector<int> edges[N];
bool st[N];//标记被遍历过的

void dfs(int u)
{
	cout << u << ' ';
	st[u] = true;
	for (auto e : edges[u])
	{
		if (!st[e])
		{
			dfs(e);
		}
	}
}

int main()
{
	cin >> n;
	for (int i = 1; i < n; i++)
	{
		int a, b; cin >> a >> b;
		edges[a].push_back(b);
		edges[b].push_back(a);		
	}
	//深度优先搜索
	dfs(1);
	return 0;
}

用链式向前星存储:

cpp 复制代码
#include <iostream>

using namespace std;

const int N = 1e5 + 10;
int n;
int h[N], e[N * 2], ne[N * 2], id;
bool st[N];

void dfs(int u)
{
	cout << u << ' ';
	st[u] = true;
	for (int i = h[u]; i; i = ne[i])
	{
		int v = e[i];
		if (!st[v])
		{
			dfs(v);
		}
	}
}

void add(int a, int b)
{
	id++;
	e[id] = b;
	
	ne[id] = h[a];
	h[a] = id;
}

int main()
{
	cin >> n;
	for (int i = 1; i < n; i++)
	{
		int a, b; cin >> a >> b;
		add(a, b); add(b, a);
	}
	dfs(1);
	return 0;
}

2.3.3宽度优先遍历(DFS)

宽度优先遍历就有点像涟漪扩散的过程我们需要借助队列来完成这个过程,从树的第一次也就是根节点那一层开始。首先让根结点进入队列,然后根结点出队让根节点的子结点依次进来:

接着再让第一个子结点出队让2的子结点进来:

这样就可以一层层的遍历整棵树

用vector存储:

cpp 复制代码
#include <iostream>
#include <queue>
#include <vector>
using namespace std;
const int N = 1e5 + 10;
int n;
bool st[N];
vector<int> edges[N];

void bfs()
{
	queue<int> q;
	q.push(1);
	st[1] = true;
	while (q.size())
	{
		int v = q.front();
		q.pop();
		cout << v << ' ';
		for (int e : edges[v])
		{
			if (!st[e])
			{
				q.push(e);
				st[e] = true;
			}
		}
	}
}

int main()
{
	cin >> n;
	for (int i = 1; i < n; i++)
	{
		int a, b; cin >> a >> b;
		edges[a].push_back(b);
		edges[b].push_back(a);
	}
	
	bfs();
	return 0;
}

用链式向前星的方式存储:

cpp 复制代码
#include <iostream>
#include <queue>
using namespace std;
const int N = 1e5 + 10;
int n;
int h[N], e[N * 2], ne[N * 2], id;
bool st[N];

void add(int a, int b)
{
	id++;
	e[id] = b;
	
	ne[id] = h[a];
	h[a] = id;
}

void bfs()
{
	queue<int> q;
	q.push(1);
	st[1] = true;
	while (q.size())
	{
		int v = q.front(); q.pop();
		cout << v << ' ';
		for (int i = h[v]; i; i = ne[i])
		{
			int t = e[i];
			if (!st[t])
			{
				q.push(t);
				st[t] = true;
			}
		}
	}
}

int main()
{
	cin >> n;
	for (int i = 1; i < n; i++)
	{
		int a, b; cin >> a >> b;
		add(a, b); add(b, a);
	}
	
	bfs();
	return 0;
}

本文比较重要的地方是第一章和了解一下兄弟表示法其他的我顺带就介绍了一下

相关推荐
一江寒逸2 小时前
数据结构与算法之美:串(字符串)——从基础操作到KMP模式匹配,吃透面试最高频的字符串考点
数据结构·面试·职场和发展
王老师青少年编程2 小时前
csp信奥赛C++高频考点专项训练之贪心算法 --【区间贪心】:种树
c++·算法·贪心·csp·信奥赛·区间贪心·种树
hi_ro_a2 小时前
C++ 哈希表封装 unordered_map /unordered_set
数据结构·c++·算法·哈希算法
c++之路2 小时前
C++ 动态内存
java·jvm·c++
pluviophile_s6 小时前
第18讲:⾃定义类型:结构体
c语言·笔记
Jasmine_llq6 小时前
《B4447 [GESP202512 二级] 环保能量球》
数据结构·算法·数学公式计算(核心)·整数除法算法·多组数据循环处理·输入输出算法·简单模拟算法
老唐7776 小时前
常见经典十大大机器学习算法分类与总结
人工智能·深度学习·神经网络·学习·算法·机器学习·ai
烟雨孤舟7 小时前
python 基础学习文档
学习
菜鸟丁小真7 小时前
LeetCode hot100 -73.矩阵置零
数据结构·leetcode·矩阵·知识点总结