简述
树是一种非常重要的数据结构,它是计算机科学中经常使用的一种数据结构,被广泛应用于算法设计和软件开发中。在本文中,我们将详细介绍树的基本概念、树的遍历方式、树的实现方式以及树的应用场景。在数据结构中树主要包括:1.二叉树2. AVL树3. 红黑树4. B树5. B+树6. Trie7哈夫曼树8. 树堆9. 树状数组10. KD树11. 线段树12. 树链剖分13. 树套树14. 字典树15. 并查集树16. LCA(最近公共祖先)树17. 分治树18. 范围树19. 二叉搜索树20. 线索二叉树21. 哈希树22. 二项树23. Fibonacci树24. Treap树25. Splay树26. 左偏树27. 完全二叉树28. 满二叉树29. 三叉树30. 四叉树31. 八叉树32. 哈夫曼编码树。本文只分析二叉树与二叉搜索树,其它在后续文章分析。
一、树的基本概念
树是由若干个节点组成的一种数据结构,其中第一个节点称为根节点
,其他节点被称为子节点
。每个节点可以有零个或多个子节点,如果一个节点没有子节点,则称其为叶子节点
。树可以被看作是一个由节点和边组成的图,其中每个节点表示一个对象
,每条边表示节点之间的关系。每个节点最多只有一个父节点。树的根节点是没有父节点的节点,它是整个树的起点。
下面是一棵树的例子:
在树的结构中,有几个重要的概念:
- 节点(Node)
节点是树中的基本单位,每个节点可以有零个或多个子节点。一个节点最多只能有一个父节点,但可以有多个子节点。树中没有父节点的节点被称为根节点
,没有子节点的节点被称为叶节点(Leaf Node)
。
:::: column
::: column-left
:::
::: column-right
:::
::::
如上左图,节点1为根节点,2,3,4为根节点的子节点,5,6,7,8没有子节点称为叶子节点。其中右图的不能称为树,因为树中子节点有且只有一个父节点,而右图中6节点有两个父节点
。
- 边(Edge)
边是树中连接节点的基本单位
。在一棵树中,每个节点都与它的父节点之间有一条边。一棵树中的边必须满足以下条件:
- 每个节点最多只有一条边连接它和它的父节点;
- 根节点没有父节点,它是整个树的起点;
- 叶节点没有子节点,它是整个树的终点。
:::: column
::: column-left
:::
::: column-right
:::
::::
如上左图,有7条边,右图不符合树的规格6处有两条边链接2节点。
- 树的高度(Height)与深度(Depth)
需要注意的是,高度和深度是不同的概念。树的高度
是根节点到最深叶子节点
的路径长度
,而深度
是某个节点到根节点的节点数量
。因此,在一棵树中,高度和深度的值是不相等的。根节点的深度为0
在这棵树中,从根节点 A 到最深的叶子节点 F 的路径是 A -> B -> D -> F,因此它的高度为 3。
树的深度指的是从某个节点到根节点的路径上的节点数
。例如,在上面的树中,节点 F 的深度为 4,因为从 F 到根节点 A 的路径上有 4 个节点(F -> D -> B -> A)。
树的高度和深度也影响着树的性质和算法的时间复杂度。例如,对于一棵平衡二叉树,它的高度为 log(n),其中 n 表示树中节点的总数,因此在这样的树中进行查找、插入和删除操作的时间复杂度都是 O(log n)
。而对于一棵高度为 n 的链表,它的操作时间复杂度则会退化为 O(n)
。
- 子树(Subtree)
子树是指以某个节点为根
的子树,它包含了该节点的所有子孙节点。例如,在下面这棵树中,以节点 A 为根的子树包含了 A、B、C、D、E 和 F 这些节点。
在实际应用中,我们经常需要遍历树中的子树,以完成各种操作。例如,我们可以使用前序遍历来遍历一棵树的所有子树。在前序遍历中,我们先遍历根节点,然后依次遍历左子树和右子树。因此,如果我们要遍历以某个节点为根的子树,可以按照前序遍历
的方式依次访问该节点的左子树和右子树。
以节点 B 为根的子树的前序遍历结果:
B -> D -> F -> E
这个结果的意思是,我们先访问节点 B,然后依次遍历它的左子树 D 和右子树 E。在 D 的左子树中,我们继续遍历节点 F。这样一直遍历下去,直到遍历完该子树中的所有节点。
- 祖先节点(Ancestor)和后代节点(Descendant)
祖先节点是指某个节点的所有父亲节点,以及这些父亲节点的父亲节点,以此类推,直到根节点,例如,在下面这棵树中,节点 B 的祖先节点是节点 A,节点 A 的祖先节点是空集
。
后代节点是指某个节点的所有子孙节点,以及这些子孙节点的子孙节点,以此类推,直到叶子节点。例如,在上面这棵树中,节点 A 的后代节点包括节点 B、C、D 和 E。
在实际应用中,我们经常需要在树中查找某个节点的祖先节点或后代节点,以完成各种操作。例如,我们可以使用递归算法
来查找某个节点的所有祖先节点。具体做法是,首先判断当前节点是否为根节点,如果是,则返回空集;否则,将当前节点的父亲节点加入结果集中,并递归查找当前节点的父亲节点的祖先节点,直到递归到根节点为止。
二、树的遍历方式
树的遍历方式是指按照一定的顺序依次访问树的每个节点,可以分为深度优先遍历
和广度优先遍历
两种方式。
- 深度优先遍历
深度优先遍历是指从根节点开始,先访问某个节点的所有子节点,再递归地访问每个子节点的子节点,直到所有节点都被访问完毕。深度优先遍历可以分为先序遍历
、中序遍历
和后序遍历
三种方式.
先序遍历
先序遍历是一种递归遍历
方法,可以按照节点的先后顺序遍历树中的所有节点。在先序遍历中,我们首先访问根节点,然后按照从左到右的顺序访问其所有子树。以下是一个示例树:
对于这棵树,它的先序遍历顺序是 A-B-D-E-C。以下是一个以节点 A 为例的先序遍历示例代码:
void preOrder(Node* node)
{
if (node == nullptr)
{
return;
}
cout << node->val << " ";
preOrder(node->left);
preOrder(node->right);
}
这个代码的意思是,如果当前节点为空,则直接返回;否则,首先输出当前节点的值,然后递归遍历当前节点的左子树和右子树。这样一来,我们就可以实现树的先序遍历了。
简单分析一下上面的递归过程:
:::: column
::: column-left
:::
::: column-right
:::
::::
(1)假如传入的参数是根节点root由于root不是空节点所以打印根节点的数据为
A
(2)接着进入递归传入的参数为根节点的左节点,那么此时的preOrder(root)这段代码相当于被挂起了,如上左图中preOrder(root)相当于被暂停了.
(3)第一次递归,参数是root->left,root->left不为空,程序继续往下走,此时打印的是根节点的左子的数值
B
,接着再次进入递归函数,传入的参数root->left->left,所以preOrder(root->left)也被暂停了(4)第二次递归,参数是root->left,root->left不为空,程序继续往下走,此时打印的是根节点的左子的数值
B
,接着再次进入递归函数,传入的参数root->left->left,所以preOrder(root->left)也被暂停了(5)第三次递归,参数是root->left->left为子节点的左节点,同理此节点也不为空,所以打印该值为D,继续进入递归,参数为root->left->left->left
(6)第四次递归,参数为root->left->left->left该节点为空,所以返回
(1)第1次递归返回,如上图,此时D从暂停变为运行,继续向下运行 preOrder(root->->left->left->right),进行一次递归,参数为root->left->left->right,显然D没有右子节点,返回,继续运行,这会跳出底四次递归返回到节点B的暂停处。
(2)第2次递归返回,如上图,B从暂停变为运行,继续向下运行 preOrder(root->left->right)进行一次递归,发现有节点E,打印,再进行一次递归参数为root->left->right->right,这个节点为空返回,继续运行,跳出底三次递归回到节点A。preOrder(root->right),即使根节点的有节点,打印值为C,继续递归preOrder(root->right->right),递归参数为根节点右子的右子节点,方向没有节点,返回,最后发现程序运行完了,遍历结束
中序遍历
在中序遍历中,我们按照从左到右的顺序访问每个节点的左子树
,然后访问节点本身
,最后再访问节点的右子树
。以下是一个示例树:
具体做法是,首先递归遍历当前节点的左子树,然后输出当前节点的值,最后递归遍历当前节点的右子树。对于这棵树,它的中序遍历顺序是 D-B-E-A-C。
以下是一个以节点 A 为例的中序遍历示例代码:
void inOrder(Node* node) {
if (node == nullptr) {
return;
}
inOrder(node->left);
cout << node->val << " ";
inOrder(node->right);
}
这个代码的意思是,如果当前节点为空,则直接返回;否则,首先递归遍历当前节点的左子树,然后输出当前节点的值
,最后递归遍历当前节点的右子树。这样一来,我们就可以实现树的中序遍历了。
后序遍历
在后序遍历中,我们按照从左到右的顺序先访问每个节点的左子树
和右子树
,最后再访问节点本身。对于这棵树,它的后序遍历顺序是 D-E-B-C-A。以下是一个以节点 A 为例的后序遍历示例代码:
void postOrder(Node* node)
{
if (node == nullptr)
{
return;
}
postOrder(node->left);
postOrder(node->right);
cout << node->val << " ";
}
这个代码的意思是,如果当前节点为空,则直接返回;否则,首先递归遍历当前节点的左子树,然后递归遍历当前节点的右子树,最后输出当前节点的值。这样一来,我们就可以实现树的后序遍历了。
- 广度优先遍历
广度优先遍历是指按照从上到下、从左到右的顺序逐层遍历树的节点。也称作层次遍历。广度优先遍历需要借助一个队列来实现。对于上面的例子,广度优先遍历的结果为: A-B-C-D-E
void bfs(Node* root)
{
queue<Node*> q;
q.push(root);
while (!q.empty())
{
Node* node = q.front();
q.pop();
cout << node->val << " ";
if (node->left != nullptr)
{
q.push(node->left);
}
if (node->right != nullptr)
{
q.push(node->right);
}
}
}
首先将根节点 A 入队,然后进入循环。在循环中,每次取出队头
元素,输出该元素的值,并将其所有子节点入队。这样一来,我们就可以实现树的广度优先遍历了。
三、树的应用
操作系统中的进程管理
在操作系统中,进程可以通过树结构来管理。每个进程都有一个父进程,如果这个进程还有子进程,那么这些子进程就是这个进程的子节点。
文件系统中的目录结构
在文件系统中,目录可以通过树结构来组织。每个目录都有一个父目录,如果这个目录还有子目录,那么这些子目录就是这个目录的子节点。
编译器中的语法分析
在编译器中,语法可以通过树结构来表示。语法树是指由语法规则构成的树形结构,它可以将程序的语法结构表示为一个树形结构,方便进行语法分析。
二叉树
一、引言
在数据结构中,二叉树是一种特殊的树结构,每个节点最多只能有两个子节点,分别称为左子节点和右子节点。二叉树通常用于实现搜索和排序算法
,同时也可以用于存储表达式
和计算表达式
的值等应用场景。
二、二叉树的基本概念
二叉树有以下特点:
每个节点最多有两个子节点;左子节点必须在右子节点之前插入;可以为空。
二叉树有以下几种类型:
:::: column
::: column-left
:::
::: column-right
:::
::::
完全二叉树:除了最后一层节点可以不满,其他层节点都必须是满的,最后一层的节点从左到右依次排列,如上左图就是完全二叉树
满二叉树:除了叶子节点,每个节点都有两个子节点。
三、满二叉树遍历
如上右图在上面例子中,根节点是A,它有两个子节点B和C。节点B又有两个子节点D和E,节点C有节点F、G。对满二叉树实现遍历。代码如下,其实就是前面所提到的深度遍历
// 定义满二叉树节点结构体
struct TreeNode
{
int val;
TreeNode* left;
TreeNode* right;
TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
};
// 前序遍历:A->B->D->E->C->F->G
void preorderTraversal(TreeNode* root) {
if (root == nullptr) return;
cout << root->val << " ";
preorderTraversal(root->left);
preorderTraversal(root->right);
}
// 中序遍历:D->B->E->A->F->C->G
void inorderTraversal(TreeNode* root) {
if (root == nullptr) return;
inorderTraversal(root->left);
cout << root->val << " ";
inorderTraversal(root->right);
}
// 后序遍历:D->E->B->F->G->C->A
void postorderTraversal(TreeNode* root) {
if (root == nullptr) return;
postorderTraversal(root->left);
postorderTraversal(root->right);
cout << root->val << " ";
}
四、二叉树的实现方式
二叉树的实现方式有两种:链式存储
和数组存储
。
链式存储
链式存储是指使用指针来实现二叉树的存储。每个节点都包含三个信息:数据、左子节点和右子节点。通过指针可以实现节点之间的连接。
#include <iostream>
using namespace std;
// 定义二叉树节点结构体
struct TreeNode
{
int val;
TreeNode* left;
TreeNode* right;
// 构造函数
TreeNode(int x) : val(x), left(NULL), right(NULL) {}
};
// 定义二叉树类
class BinaryTree
{
public:
// 构造函数
BinaryTree()
{
root = NULL;
}
// 析构函数
~BinaryTree()
{
destroyTree(root);
}
// 插入节点
void insert(int key)
{
insert(root, key);
}
// 前序遍历
void preorderTraversal()
{
preorderTraversal(root);
cout << endl;
}
// 中序遍历
void inorderTraversal()
{
inorderTraversal(root);
cout << endl;
}
// 后序遍历
void postorderTraversal()
{
postorderTraversal(root);
cout << endl;
}
private:
// 根节点指针
TreeNode* root;
// 插入节点的递归函数
void insert(TreeNode*& node, int key)
{
if (node == NULL)
{
node = new TreeNode(key);
return;
}
if (key < node->val)
{
insert(node->left, key);
}
else
{
insert(node->right, key);
}
}
// 销毁二叉树的递归函数
void destroyTree(TreeNode*& node)
{
if (node != NULL)
{
destroyTree(node->left);
destroyTree(node->right);
delete node;
node = NULL;
}
}
// 前序遍历的递归函数
void preorderTraversal(TreeNode* node)
{
if (node != NULL)
{
cout << node->val << " ";
preorderTraversal(node->left);
preorderTraversal(node->right);
}
}
// 中序遍历的递归函数
void inorderTraversal(TreeNode* node)
{
if (node != NULL)
{
inorderTraversal(node->left);
cout << node->val << " ";
inorderTraversal(node->right);
}
}
// 后序遍历的递归函数
void postorderTraversal(TreeNode* node)
{
if (node != NULL)
{
postorderTraversal(node->left);
postorderTraversal(node->right);
cout << node->val << " ";
}
}
};
int main()
{
BinaryTree tree;
tree.insert(5);
tree.insert(3);
tree.insert(7);
tree.insert(2);
tree.insert(4);
tree.insert(6);
tree.insert(8);
cout << "前序遍历:";
tree.preorderTraversal();
cout << "中序遍历:";
tree.inorderTraversal();
cout << "后序遍历:";
tree.postorderTraversal();
return 0;
}
输出
前序遍历:5 3 2 4 7 6 8
中序遍历:2 3 4 5 6 7 8
后序遍历:2 4 3 6 8 7 5
有上面的输出可以知道,该树的形状如下图
在这个实现中,节点类中包含节点的值,以及左右子节点的指针,二叉树类中包含根节点的指针以及一系列操作二叉树的方法。其中,insert()方法使用递归来插入节点,preorder()、inorder()、postorder()方法也都使用递归实现了树的前序、中序、后序遍历,删除的操作。最后在main()函数中创建了一个二叉树对象,并对其进行了插入节点和遍历操作的测试。
数组存储形式实现的二叉树
在完全二叉树中,将二叉树按照从上到下、从左到右的顺序依次编号,那么可以用一维数组来表示二叉树。具体的,假设二叉树的根节点的编号为1,那么对于二叉树中的任意一个节点i,其左子节点的编号为2i+1
,右子节点的编号为2i+2
,其父节点的编号为(i-1)/2
。这种表示方式有一个显著的优势就是,我们可以很方便地使用数组的形式来存储二叉树的节点信息,通过数组的下标关系
,我们可以快速地定位到对应的节点,因此对于一些需要大量访问节点的操作,使用数组实现的二叉树比较高效。
假设我们有以下一棵二叉树:将二叉树按照完全二叉树的形式排列,即从上到下,从左到右依次排列节点,空节点也要占位,如下所示:
我们可以使用一个一维数组来存储这棵二叉树,数组下标从0开始,从上到下,从左到右的顺序依次存储节点,空节点也占据位置,如下所示:其中,-1表示该位置为空节点
index: 0 1 2 3 4 5 6 7 8 9
value: 1 2 3 4 5 6 7 8 9 -1
对于任意一个节点,设其在数组中的下标为i,它的左子节点在数组中的下标为2i+1
,右子节点在数组中的下标为2i+2
,其父节点在数组中的下标为(i-1)/2
。如下所示:
- 根节点1,其左节点为2下标为20+1=1;右子节点为节点3,其下标为20+2=2;。
- 节点2的左子节点为节点4,其下标为21+1=3;右子节点为节点5,其下标为21+2=4;父节点为节点1,其下标为(1-1)/2=0。
- 节点3的左子节点为节点6,其下标为22+1=5;右子节点为节点7,其下标为22+2=6;父节点为节点1,其下标为(2-1)/2=0。
- 节点4的左子节点为节点8,其下标为23+1=7;右子节点为节点9,其下标为23+2=8;父节点为节点2,其下标为(4-1)/2=1。
又如,我们想要在数组中插入以下值:4、2、7、1、3、6、9。
第一步,我们将4作为根节点插入数组的第一个位置:
[4, , , , , , , ]
第二步,我们将2作为第一个节点的左侧子节点插入数组的第二个位置:
[4, 2, , , , , , ]
第三步,我们将7作为第一个节点的右侧子节点插入数组的第三个位置:
[4, 2, 7, , , , , ]
第四步,我们将1作为2的左侧子节点插入数组的第四个位置:
[4, 2, 7, 1, , , , ]
第五步,我们将3作为2的右侧子节点插入数组的第五个位置:
[4, 2, 7, 1, 3, , , ]
第六步,我们将6作为7的左侧子节点插入数组的第六个位置:
[4, 2, 7, 1, 3, 6, , ]
第七步,我们将9作为7的右侧子节点插入数组的第七个位置:
[4, 2, 7, 1, 3, 6, 9, ]
数组实现二叉树的代码如下:
#include <iostream>
#define MAX_SIZE 100
using namespace std;
// 定义节点结构体
struct Node
{
int data;
};
// 定义二叉树类
class BinaryTree
{
private:
Node* tree[MAX_SIZE];
int size = 0;
public:
BinaryTree()
{
for (int i = 0; i < MAX_SIZE; i++)
{
tree[i] = nullptr; // 初始化为空指针
}
}
// 插入节点
void insert(int data)
{
if (size == MAX_SIZE)
{
cout << "Tree is full!" << endl;
return;
}
Node* node = new Node();
node->data = data;
tree[size] = node;
size++;
}
// 前序遍历
void preOrder(int index)
{
if (index >= size || tree[index] == nullptr)
{
return;
}
cout << tree[index]->data << " ";
preOrder(2 * index + 1); // 遍历左子树
preOrder(2 * index + 2); // 遍历右子树
}
// 中序遍历
void inOrder(int index)
{
if (index >= size || tree[index] == nullptr)
{
return;
}
inOrder(2 * index + 1); // 遍历左子树
cout << tree[index]->data << " ";
inOrder(2 * index + 2); // 遍历右子树
}
// 后序遍历
void postOrder(int index)
{
if (index >= size || tree[index] == nullptr)
{
return;
}
postOrder(2 * index + 1); // 遍历左子树
postOrder(2 * index + 2); // 遍历右子树
cout << tree[index]->data << " ";
}
};
// 主函数测试
int main()
{
BinaryTree tree;
tree.insert(1);
tree.insert(2);
tree.insert(3);
tree.insert(4);
tree.insert(5);
tree.insert(6);
tree.insert(7);
cout << "PreOrder: ";
tree.preOrder(0);
cout << endl;
cout << "InOrder: ";
tree.inOrder(0);
cout << endl;
cout << "PostOrder: ";
tree.postOrder(0);
cout << endl;
return 0;
}
输出结果
PreOrder: 1 2 4 5 3 6 7
InOrder: 4 2 5 1 6 3 7
PostOrder: 4 5 2 6 7 3 1
从上面输出,我们可知例子的二叉树如下图所示
上述代码中,使用数组 tree 存储二叉树节点,根节点存储在 tree[0]
,第
i
i
i 个节点的左子节点和右子节点分别存储在 tree[2*i+1]
和 tree[2*i+2]
。插入节点时,将新节点插入到数组末尾,遍历时按照上述规则访问节点即可实现前序遍历、中序遍历、后序遍历。
二叉搜索树
一、简述
二叉搜索树(Binary Search Tree,BST)是一种常用的数据结构,它是一棵二叉树,其中每个节点都包含一个可比较的键(key)和一个对应的值(value)。对于每个节点,它的左子树中的所有键都小于该节点的键,而右子树中的所有键都大于该节点的键。这个特性使得查找、插入和删除操作都可以在O(log n)的时间复杂度内完成,因此BST被广泛应用于需要高效地动态查找、插入和删除数据的场景中。
BST的实现方式有两种:链式存储
和数组存储
。链式存储使用指针将节点连接在一起,数组存储使用数组来表示整棵树。
下面我们来看一下链式存储实现BST的示意图:
从图中可以看出,BST具有以下几个特点:
每个节点有一个键值(key)和一个对应的值(value)。
左子树中的所有键值都小于当前节点的键值,右子树中的所有键值都大于当前节点的键值。
每个节点最多有两个子节点,分别称为左子节点和右子节点。所有叶子节点的值都为NULL或空指针。
二、二叉搜索树的操作
二叉搜索树常见的操作主要又节点查找、节点插入、节点删除、树的最大高度等操作。
下面是二叉搜索树实现与遍历的代码
#include <iostream>
using namespace std;
// 定义二叉搜索树的结点
struct TreeNode
{
int val;
TreeNode *left;
TreeNode *right;
TreeNode(int x) : val(x), left(NULL), right(NULL) {}
};
class BinarySearchTree
{
private:
TreeNode *root;
// 向二叉搜索树中插入结点
TreeNode* insert(TreeNode* node, int val)
{
if (node == NULL)
{
node = new TreeNode(val);
}
else if (val < node->val)
{
node->left = insert(node->left, val);
}
else
{
node->right = insert(node->right, val);
}
return node;
}
// 前序遍历二叉搜索树
void preOrder(TreeNode* node)
{
if (node != NULL)
{
cout << node->val << " ";
preOrder(node->left);
preOrder(node->right);
}
}
// 中序遍历二叉搜索树
void inOrder(TreeNode* node)
{
if (node != NULL)
{
inOrder(node->left);
cout << node->val << " ";
inOrder(node->right);
}
}
// 后序遍历二叉搜索树
void postOrder(TreeNode* node)
{
if (node != NULL)
{
postOrder(node->left);
postOrder(node->right);
cout << node->val << " ";
}
}
public:
BinarySearchTree()
{
root = NULL;
}
// 向二叉搜索树中插入结点
void insert(int val)
{
root = insert(root, val);
}
// 前序遍历二叉搜索树
void preOrder()
{
preOrder(root);
cout << endl;
}
// 中序遍历二叉搜索树
void inOrder()
{
inOrder(root);
cout << endl;
}
// 后序遍历二叉搜索树
void postOrder()
{
postOrder(root);
cout << endl;
}
};
int main()
{
BinarySearchTree bst;
// 向二叉搜索树中插入结点
bst.insert(5);
bst.insert(2);
bst.insert(7);
bst.insert(1);
bst.insert(3);
// 前序遍历二叉搜索树
bst.preOrder(); // 5 2 1 3 7
// 中序遍历二叉搜索树
bst.inOrder(); // 1 2 3 5 7
// 后序遍历二叉搜索树
bst.postOrder(); // 1 3 2 7 5
return 0;
}
输出结果
前序遍历: 5 2 1 3 7
中序遍历: 1 2 3 5 7
后序遍历: 1 3 2 7 5
插入解析:
首先,在main函数里面创建一个二叉搜索树的对象,个创建的时候比较注意的是BinarySearchTree(),构造函数会讲root[根节点]初始化为空,也就是空树。
接着,插入一个节点数值为5,bst.inster(5)在类中其实是调用了root = insert(root, val);这个函数一个是根节点,现在为空,另一个是5。
然后,因为root==NULL,所以在 insert(root, val)函数中第一个if条件满足,会新创建一个new TreeNode(val)节点,节点数值为5,其实就是root从root->NULL转变了root指向了一个地址假如是200,也就是说现在这个树是非空的;返回根节点root[node]
在main函数中继续插入一个节点数值为2,那么进入insert(root, val)函数,val=2,root=200,就是根节点地址。由于root非空所以,只有第二个if满足。 node->left = insert(node->left, val);这里是个递归的函数,传入的参数为root->left=NULL,vlaue=2,递归就是挂起,我讲这一刻定义为(1)-left时刻。递归重新进入insert(root, val),这里的参数是root->left,val=2
此时,root->left==NULL,所以新建一个节点,该节点指向root->left,假设root->left的地址为150,这里的root->left的值为2.条件语句执行完了,通过return node,返回此时的node地址为150,返回就是返回到(1)-left时刻。
最后,root->left==NULL,所以新建一个节点,该节点指向root->left,假设root->left的地址为150,这里的root->left的值为2.条件语句执行完了,通过return node,返回此时的node地址为150,返回就是返回到(1)-left时刻。所以,此时root->left=150,跳出if语句,return node,这里的node就是root.实现了左子的插入
对于左子的左子也就是root->left->left如何插入?也很简单,先是进入else if (val < node->val) ,会使用node->left = insert(node->left, val);,node->left 进入第一次递归,root->left!=NULL,所以会进入第一次递归中的else if (val < node->val),此次传入的参数就是root->left->left,进入第二次递归,root->left->left为NULL,所以会创建根节点的左孙节点。其它的节点类推。
节点查找
节点查找操作通过比较节点值与目标值的大小,向左或向右移动节点指针进行查找,如果找到了与目标值相等的节点,则返回该节点指针;如果没有找到,则返回空指针。这里也是递归实现的,参看上面的分析过程。
Node* search(Node* root, int value)
{
if (root == nullptr || root->val == value)
{
return root;
}
if (value < root->val)
{
return search(root->left, value);
}
else
{
return search(root->right, value);
}
}
节点删除
节点删除操作相对比较复杂,需要分三种情况讨论:
如上图如果删除的节点是叶子节点:如上所示的1、8、11、14、18节点直接删除即可。
如果删除的节点只有一个子节点,那么删除后的节点需要子节点替换上。如上图删除3后,5节点的左节点要指向1,又如删除20节点后,17的右节点要指向18.
如果删除的节点有两个子节点,子节点中又有很多子节点的该怎么办?如上图的删除5节点后,直接给3,7中任意一个继承?
如果给3继承,那么4位置与7位置必然会发生冲突。而且无论怎么调整4,7的位置都会破坏搜索二叉树的平衡:左边比右边小这个属性。
:::: column
::: column-left
:::
::: column-right
:::
::::
在3的子节点树中,只有4节点比较适合5的位置。而且不会破坏原来的平衡
:::: column
::: column-left
:::
::: column-right
:::
::::
同理,如果想让右边节点继承的话,只要找出7节点的子树中的最小值,与5节点替换。那么就只有6节点比较符合。综合上面讨论结果,得出删除节点的代码如下。
#include <iostream>
using namespace std;
struct Node
{
int key;
Node* left;
Node* right;
Node(int k) : key(k), left(nullptr), right(nullptr) {}
};
class BST
{
private:
Node* root;
Node* insert(Node* node, int key)
{
if (node == nullptr)
{
node = new Node(key);
}
else if (key < node->key)
{
node->left = insert(node->left, key);
}
else if (key > node->key)
{
node->right = insert(node->right, key);
}
return node;
}
Node* findMin(Node* node)
{
while (node->left != nullptr)
{
node = node->left;
}
return node;
}
Node* remove(Node* node, int key)
{
if (node == nullptr)
{
return node;
}
if (key < node->key)
{
node->left = remove(node->left, key);
}
else if (key > node->key)
{
node->right = remove(node->right, key);
}
else
{
if (node->left == nullptr)
{
Node* temp = node->right;
delete node;
return temp;
}
else if (node->right == nullptr)
{
Node* temp = node->left;
delete node;
return temp;
}
Node* temp = findMin(node->right);
node->key = temp->key;
node->right = remove(node->right, temp->key);
}
return node;
}
public:
BST() : root(nullptr) {}
void insert(int key)
{
root = insert(root, key);
}
void remove(int key)
{
root = remove(root, key);
}
void inorderTraversal(Node* node)
{
if (node != nullptr)
{
inorderTraversal(node->left);
cout << node->key << " ";
inorderTraversal(node->right);
}
}
void inorderTraversal()
{
inorderTraversal(root);
cout << endl;
}
};
int main()
{
BST bst;
bst.insert(50);
bst.insert(30);
bst.insert(70);
bst.insert(20);
bst.insert(40);
bst.insert(60);
bst.insert(80);
bst.inorderTraversal(); // 20 30 40 50 60 70 80
bst.remove(20);
bst.remove(30);
bst.remove(70);
bst.inorderTraversal(); // 40 50 60 80
return 0;
}
输出
插入后:20 30 40 50 60 70 80
删除后:40 50 60 80
代码插入后与删除后的二叉搜索树结构如下图
如上右图,当想要删除70这个节点时候,如果采用将50->70拆散,70->80,70->60,这些链接拆散,新增50->60,60->80链接,这让会使得代码十分是复杂。为此,我们可以采用下面的优化方式。
如上图,直接将60节点的值赋给原来70这个节点,将60节点删除,这种做法是很简单快捷的。
查找某个子树中最小的节点。
Node* minValueNode(Node* node)
{
Node* current = node;
while (current && current->left != nullptr)
{
current = current->left;
}
return current;
}
以下是查找某个子树中最大值节点的C++代码:
Node* maxValueNode(Node* node) {
Node* current = node;
while (current && current->right) {
current = current->right;
}
return current;
}
这个函数接受一个节点指针作为参数,并返回这个节点子树中的最大值节点指针。它首先将指针移动到右侧节点,直到到达最右侧节点(即最大值节点),然后将其返回。
检测树的最大高度
#include <iostream>
using namespace std;
// 定义二叉树的节点结构体
struct TreeNode
{
int val;
TreeNode* left;
TreeNode* right;
TreeNode(int x) : val(x), left(NULL), right(NULL) {}
};
// 插入节点到二叉搜索树中
TreeNode* insertNode(TreeNode* root, int val)
{
if (!root)
{
return new TreeNode(val);
}
if (val < root->val)
{
root->left = insertNode(root->left, val);
}
else
{
root->right = insertNode(root->right, val);
}
return root;
}
// 计算二叉搜索树的最大高度
int getMaxHeight(TreeNode* root)
{
if (!root)
{
return 0;
}
int leftHeight = getMaxHeight(root->left);
int rightHeight = getMaxHeight(root->right);
return max(leftHeight, rightHeight) + 1;
}
int main()
{
// 构建二叉搜索树
TreeNode* root = NULL;
root = insertNode(root, 3);
insertNode(root, 9);
insertNode(root, 20);
insertNode(root, 15);
insertNode(root, 7);
// 计算最大高度并输出
cout << "Max Height: " << getMaxHeight(root) << endl;
return 0;
}
以上代码树的结构如下
输出
Max Height: 4
在这个示例代码中,我们首先定义了一个TreeNode结构体,表示二叉树的节点,包括节点的值、左右子树指针。接着,我们实现了一个insertNode函数,用于将一个节点插入到二叉搜索树中。最后,我们实现了一个getMaxHeight函数,用于计算二叉搜索树的最大高度。在getMaxHeight函数中,我们使用递归的方式遍历二叉树,计算左子树和右子树的高度,并取其中较大的值,最后加上1,即为整个树的最大高度。
在main函数中,我们构建了一个二叉搜索树,并调用getMaxHeight函数计算最大高度,然后将结果输出。
二叉搜索树的遍历
#include <iostream>
using namespace std;
// 定义二叉搜索树节点结构体
struct TreeNode
{
int val;
TreeNode* left;
TreeNode* right;
TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
};
// 前序遍历
void preorderTraversal(TreeNode* root)
{
if (root == nullptr) return;
cout << root->val << " ";
preorderTraversal(root->left);
preorderTraversal(root->right);
}
// 中序遍历
void inorderTraversal(TreeNode* root)
{
if (root == nullptr) return;
inorderTraversal(root->left);
cout << root->val << " ";
inorderTraversal(root->right);
}
// 后序遍历
void postorderTraversal(TreeNode* root)
{
if (root == nullptr) return;
postorderTraversal(root->left);
postorderTraversal(root->right);
cout << root->val << " ";
}
// 主函数
int main()
{
TreeNode* root = new TreeNode(4);
root->left = new TreeNode(2);
root->right = new TreeNode(6);
root->left->left = new TreeNode(1);
root->left->right = new TreeNode(3);
root->right->left = new TreeNode(5);
root->right->right = new TreeNode(7);
cout << "前序遍历:";
preorderTraversal(root);
cout << endl;
cout << "中序遍历:";
inorderTraversal(root);
cout << endl;
cout << "后序遍历:";
postorderTraversal(root);
cout << endl;
return 0;
}
其中,主函数中的代码是一个例子,用于构建以下二叉搜索树:
运行程序后,输出结果如下:
前序遍历:4 2 1 3 6 5 7
中序遍历:1 2 3 4 5 6 7
后序遍历:1 3 2 5 7 6 4
以上三种遍历方式是二叉树的常用遍历方法,也是面试中常被考察的知识点。需要注意的是,三种遍历方式的时间复杂度均为O(n),其中n为二叉树中节点的个数。
图
一、简述
在计算机科学中,图是一种非常重要的数据结构,用于描述物理和抽象对象之间的关系。图数据结构可以用来解决很多问题,例如路由算法、社交网络分析、搜索引擎等。在本篇博客中,我们将介绍C++中的图数据结构,包括图的定义、表示、遍历和常见算法。
:::: column
::: column-left 50%
:::
::: column-right 36%
:::
前面我们介绍了,二叉搜索树,二叉搜索树是一种层次结构
的数据结构,即是父节点最多能有两个节点。而实际应用用,往往有些数据父节点下有多个子节点或者一个节点可能有两个以及以上的父节点,如上左图,这种数据结构被称之为为图
二、图的定义
:::: column
::: column-left 70%
:::
::: column-right 65%
:::
图由两个基本元素组成:节点和边。节点也被称为顶点,通常用数字或字符串来表示。边连接两个节点,并且可以有权重或方向。如果边没有权重或方向,则称其为无向无权图
,如果边具有方向,则称其为有向图
。如果边具有权重,则称其为带权图
。
无向无权图
如上左图,它由一组节点(也称为顶点)和一组边组成,每条边连接两个节点,并且没有权重或距离
的概念。由于是无向的,因此边可以从一个节点流向另一个节点,也可以反过来。例如可以实现1->3的流向也可以实现3->1的流向.在无向无权图中,我们通常用邻接表
或邻接矩阵
来表示节点之间的连接关系。后面会分析邻接表与邻接矩阵。
有向带权图
它由一组节点(也称为顶点)和一组有向边组成,每条有向边连接两个节点,并且具有权重或距离的概念。有向边表示一个节点可以到达另一个节点,但反过来不一定成立。权重可以是任何数字,例如整数、浮点数等等。有向带权图可以用来表示有方向的距离、代价、时间等等。如上右图,7->3 的权重为150,但是3->7是错误的。甚至可以简单理解为一张航班的价格表,其中权重就是价格。7->3需要150元。也可以用邻接表或邻接矩阵来表示节点之间的连接关系和权重
三、图的属性
图(Graph)是计算机科学中一种重要的数据结构,它由一组节点(Vertex,也叫顶点)和连接这些节点的边(Edge)构成。图可以用来表示各种复杂的关系和连接,比如社交网络中的用户之间的关系、路网中的道路和交通规划等。在C++中,图数据结构可以使用邻接矩阵或邻接表表示,本文将主要介绍邻接表表示法。
基本属性
节点(Vertex)
节点是图中的基本单位,也叫做顶点。在C++中,我们通常用一个整数来表示一个节点,这个整数被称为节点的标识符(Identifier)。标识符可以是任何类型,只要它们可以唯一地标识节点即可。
:::: column
::: column-left 40%
:::
::: column-right 60%
:::
边(Edge)
边是图中连接节点的基本单位。一条边可以连接两个节点,也可以连接一个节点和自己(自环边)
,如上右图A节点有自环边。边可以带有权重(Weight),表示节点之间的距离或者其他类型的信息。如上左图。
连通图和非连通图
:::: column
::: column-left 70%
:::
::: column-right 70%
:::
连通图(Connected Graph)中,任意两个节点之间都存在至少一条路径,即任意两个节点之间都是可达的。
非连通图(Disconnected Graph)中,存在节点之间没有路径连接的情况。如上左图,D与C之间是非连通的
四、图的表示方式
:::: column
::: column-left 70%
:::
::: column-right 60%
:::
边列表(Edge List)
对于上面这一副无向无权的图该如何存储它?这种图最主要的就是要保存两个节点之间的关系。比如A与C、A与B都是邻近节点。G与A之间不是邻近的关系.设计的代码需要有两列表:一是存储该图的所有节点,二是存储节点之前的边关系。如上右图所示.下面是实现一个边列表的代码例子
#include <iostream>
#include <vector>
using namespace std;
struct Edge
{
int from;
int to;
int weight;
Edge(int f, int t, int w): from(f), to(t), weight(w) {}
};
int main()
{
// 创建一个包含5个节点的无向图
int n = 5;
vector<Edge> edges = {Edge(0, 1, 2), Edge(0, 3, 1), Edge(1, 2, 3), Edge(2, 3, 2), Edge(3, 4, 5)};
// 输出边列表中的每一条边
for (int i = 0; i < edges.size(); i++) {
cout << edges[i].from << " --" << edges[i].weight << "-- " << edges[i].to << endl;
}
return 0;
}
在这个示例中,我们首先定义了一个结构体Edge,它包含了一条边的起点、终点和权重。然后我们创建了一个包含5个节点的无向图,使用一个vector来存储图中的所有边。最后我们遍历了vector中的所有边,并输出每条边的起点、终点和权重信息。
边列表的存储方式简单明了,但是只适合处理边比较少的稠密图。在处理边比较多的稀疏图时,边列表的存储方式会浪费大量的空间。对于一个的图来说,要判断两点之间是否有联系需要遍历所有的节点时间复杂度是(n x m),空间复杂度可能是o(nxm)的.那么还有其他方式比较节省空间的?
邻接矩阵
:::: column
::: column-left 60%
:::
::: column-right 50%
A | B | C | D | E | F | G | H | |
---|---|---|---|---|---|---|---|---|
A | 0 | 1 | 1 | 1 | 0 | 0 | 0 | 0 |
B | 1 | 0 | 0 | 0 | 1 | 1 | 0 | 0 |
C | 1 | 0 | 0 | 0 | 0 | 0 | 1 | 0 |
D | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 1 |
E | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 1 |
F | 0 | 1 | 1 | 0 | 0 | 0 | 0 | 1 |
G | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 1 |
H | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 0 |
::: |
在邻接矩阵中,每个节点对应矩阵的一行和一列
。矩阵中的每个元素表示相应的两个节点之间是否存在边
。如果存在边,则元素值为1
,否则为0
。在这个例子中,邻接矩阵中的第一行第二列元素为1,表示节点A和节点B之间存在一条边。,如果上图是有权的图,那只需要将二维数组中1的值存储为具体的权重值即可。
下面是邻接矩阵实现的代码
#include <iostream>
using namespace std;
const int MAX_SIZE = 100;
class Graph
{
private:
int V; // 顶点数
int E; // 边数
int matrix[MAX_SIZE][MAX_SIZE]; // 邻接矩阵
public:
Graph(int v)
{
V = v;
E = 0;
// 初始化矩阵
for (int i = 0; i < V; i++)
{
for (int j = 0; j < V; j++)
{
matrix[i][j] = 0;
}
}
}
void addEdge(int v, int w)
{
// 添加边 v -> w
matrix[v][w] = 1;
E++;
}
void removeEdge(int v, int w)
{
// 删除边 v -> w
matrix[v][w] = 0;
E--;
}
int numVertices()
{
return V;
}
int numEdges()
{
return E;
}
bool isAdjacent(int v, int w)
{
// 判断顶点 v 和 w 是否邻接
return matrix[v][w] == 1;
}
void printMatrix()
{
// 打印邻接矩阵
for (int i = 0; i < V; i++)
{
for (int j = 0; j < V; j++)
{
cout << matrix[i][j] << " ";
}
cout << endl;
}
}
};
int main()
{
// 创建一个有 5 个顶点的图
Graph g(5);
// 添加一些边
g.addEdge(0, 1);
g.addEdge(0, 2);
g.addEdge(1, 2);
g.addEdge(2, 3);
g.addEdge(3, 4);
// 打印邻接矩阵
g.printMatrix();
// 输出图的顶点和边数
cout << "Number of vertices: " << g.numVertices() << endl;
cout << "Number of edges: " << g.numEdges() << endl;
// 判断两个顶点是否邻接
cout << "Vertex 0 and vertex 1 are ";
if (!g.isAdjacent(0, 1))
{
cout << "not ";
}
cout << "adjacent." << endl;
cout << "Vertex 1 and vertex 2 are ";
if (!g.isAdjacent(1, 2))
{
cout << "not ";
}
cout << "adjacent." << endl;
// 删除一些边
g.removeEdge(0, 1);
g.removeEdge(2, 3);
// 打印邻接矩阵
g.printMatrix();
// 输出图的顶点和边数
cout << "Number of vertices: " << g.numVertices() << endl;
cout << "Number of edges: " << g.numEdges() << endl;
return 0;
}
0 1 1 0 0
0 0 1 0 0
0 0 0 1 0
0 0 0 0 1
0 0 0 0 0
Number of vertices: 5
Number of edges: 5
Vertex 0 and vertex 1 are adjacent.
Vertex 1 and vertex 2 are adjacent.
0 0 1 0 00 0 1 0 0
0 0 0 0 0
0 0 0 0 1
0 0 0 0 0
Number of vertices: 5
Number of edges: 3
这种表示图的方式比边列表方式有什么好处?
比如当要寻找A与D之间是否有关系的时候,直接查找A的那一行寻找标记1的位置是否有D,就可以实现。那么时间复杂度就可以从O(n X M )直接变为O(M),如果在利用哈希表的进行优化甚至可以实现O(1)的时间复杂度.但是这种方式,空间复杂度依旧是O(n x m),而且表格中存在这大量的0这种节点需要遍历,显然这些0不是我们需要的。如果我们直接存为1的边信息,再做成一张表。当给出的节点再这张1
的表中找不到,那么证明这两个节点之间没有任何的关系,反之则是有关系。
邻接表(Adjacency List)
邻接表是一种用于表示图的数据结构,它使用链表存储每个节点的邻居节点。具体来说,对于一个有n个节点的无向图,邻接表会创建一个包含n个链表的数组,数组的每个位置对应着一个节点。每个节点的链表中包含了与它相邻的所有节点。最简单的方式就是创建两个列表:一是用来保存节点、二是用来存储节点的边关系。对于这两个列表我们可以使用数组来存储。
依旧是之前的例子,为了方便后面的存储,每个节点使用数据来表示。如0节点,他的连邻节点有1、2、3那么在右图直接存的就是1,2,3的节点。相比临接矩阵我们忽略了大量的非邻接关系的存储。
如何存储上面右图的这个表?
方法一:定义8个指针数组例如这里定义 int *arry[8],每个指针的数组的元素是不一样的。例如:arry[0] = new int node[3].就是A节点中有邻接关系的节点有三个。将1,2,3这三个数据值分别存储在arr[0]指向的数组中。这种方式的空间复杂度为O(e),显然空间复杂度比邻接矩阵低。那么时间复杂度?,假如要查找3是否在0节点的邻近,最坏的情况就是直接遍历所有的arry[0]的数组,这种情况时间复杂度是O(n)。如果arry[0]对应的数组是按照从小到大排序的,那么时间复杂度就是O(logn)下面是实现邻接表的代码
#include <iostream>
#include <vector>
using namespace std;
// 邻接表中的边
struct Edge {
int to; // 目标节点
int weight; // 权重
Edge(int t, int w) : to(t), weight(w) {}
};
class Graph {
public:
Graph(int n) : nodes(n), edges(0) {
for (int i = 0; i < n; i++) {
nodes[i] = edges.size();
}
}
// 添加一条从 from 到 to 权重为 weight 的边
void addEdge(int from, int to, int weight) {
edges.push_back(Edge(to, weight));
}
// 输出邻接表
void print() {
for (int i = 0; i < nodes.size() - 1; i++) {
cout << "Node " << i << ": ";
for (int j = nodes[i]; j < nodes[i+1]; j++) {
Edge e = edges[j];
cout << e.to << "(" << e.weight << ") ";
}
cout << endl;
}
cout << "Node " << nodes.size() - 1 << ": " << endl;
}
private:
vector<int> nodes; // 存储每个节点在 edges 中的起始位置
vector<Edge> edges; // 存储邻接表中的所有边
};
int main() {
Graph g(5);
g.addEdge(0, 1, 2);
g.addEdge(0, 3, 1);
g.addEdge(1, 2, 3);
g.addEdge(2, 3, 2);
g.addEdge(2, 4, 1);
g.addEdge(3, 4, 4);
g.print();
return 0;
}
输出:Node 0: 1(2) 3(1)
Node 1: 2(3)
Node 2: 3(2) 4(1)
Node 3: 4(4)
Node 4:
在 print() 方法中,我们遍历每个节点 i,并输出其对应的邻接边。具体来说,我们从 nodes[i] 开始,直到 nodes[i+1],输出从节点 i 出发的所有邻接边。在本例中,我们创建了一个 5 节点的图,并添加了 6 条边。最后我们调用 print() 方法输出邻接表。
邻接表与邻接矩阵的删除与插入操作
:::: column
::: column-left 60%
:::
::: column-right 50%
A | B | C | D | E | F | G | H | |
---|---|---|---|---|---|---|---|---|
A | 0 | 1 | 1 | 1 | 0 | 0 | 1 | 0 |
B | 1 | 0 | 0 | 0 | 1 | 1 | 0 | 0 |
C | 1 | 0 | 0 | 0 | 0 | 0 | 1 | 0 |
D | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 1 |
E | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 1 |
F | 0 | 1 | 1 | 0 | 0 | 0 | 0 | 0 |
G | 1 | 0 | 1 | 0 | 0 | 0 | 0 | 1 |
H | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 0 |
::: |
如上图添加A-G直接的关系并且删除了F-H之间的关系。对于邻接矩阵很简单,直接在邻接表里面直接将对应修改的节点从0->1与1-0即可
邻接矩阵就比较麻烦了,因为是数组形式存储,插入删除需要再找一个新的数组,重新调整大小,将数组中所有的元素搬到新的数组中去。过程十分的复杂,时间复杂度高。既然能用数组存储邻接表,那么链表其实也是可以的。链表的形式或许对于频繁的插入删除操作更适合。下面是链表实现的邻接表的代码实现
#include <iostream>
#include <vector>
using namespace std;
// 邻接表中的边
struct Edge {
int to; // 目标节点
int weight; // 权重
Edge* next; // 下一条边
Edge(int t, int w) : to(t), weight(w), next(nullptr) {}
};
// 邻接表中的节点
struct Node {
int id; // 节点编号
Edge* firstEdge; // 该节点的第一条边
Node(int i) : id(i), firstEdge(nullptr) {}
};
class Graph {
public:
Graph(int n) {
for (int i = 0; i < n; i++) {
nodes.push_back(new Node(i));
}
}
~Graph() {
for (int i = 0; i < nodes.size(); i++) {
Edge* e = nodes[i]->firstEdge;
while (e != nullptr) {
Edge* tmp = e;
e = e->next;
delete tmp;
}
delete nodes[i];
}
}
// 添加一条从 from 到 to 权重为 weight 的边
void addEdge(int from, int to, int weight) {
Edge* e = new Edge(to, weight);
e->next = nodes[from]->firstEdge;
nodes[from]->firstEdge = e;
}
// 输出邻接表
void print() {
for (int i = 0; i < nodes.size(); i++) {
cout << "Node " << nodes[i]->id << ": ";
Edge* e = nodes[i]->firstEdge;
while (e != nullptr) {
cout << e->to << "(" << e->weight << ") ";
e = e->next;
}
cout << endl;
}
}
private:
vector<Node*> nodes; // 存储所有节点和它们的邻接边
};
int main() {
Graph g(5);
g.addEdge(0, 1, 2);
g.addEdge(0, 3, 1);
g.addEdge(1, 2, 3);
g.addEdge(2, 3, 2);
g.addEdge(2, 4, 1);
g.addEdge(3, 4, 4);
g.print();
return 0;
}
输出
Node 0: 3(1) 1(2)
Node 1: 2(3)
Node 2: 4(1) 3(2)
Node 3: 4(4)
Node 4:
在本例中,我们使用链表实现邻接表。具体来说,我们使用 Node 类来表示每个节点,并将每个节点的第一条邻接边存储在一个指针中。我们使用 Edge 类来表示每个边,其中包含目标节点、权重和下一条边的指针