从C语言到C++_24(二叉搜索树)概念+完整代码实现+笔试题

news2024/11/26 22:33:09

目录

1. 二叉搜索树(BinarySearchTree)

1.1 二叉搜索树的优势和劣势

二叉搜索树的时间复杂度:O(N)

1.2 二叉搜索树的改良

2. 二叉搜索树的实现

2.1 二叉搜索树的定义

2.2 二叉搜索树的插入

2.3 二叉搜索树的查找

2.4 二叉搜索树的删除

2.5 二叉搜索树的查找(递归)

2.6 二叉搜索树的插入(递归)

2.7 二叉搜索树的删除(递归)

2.8 析构和拷贝构造和赋值

3. 搜索二叉树的应用

3.1 K 模型

3.2 KV 模型

4. 笔试选择题

答案:

5. 完整代码:

本章完。


此篇算是用C++讲高阶数据结构第一篇,在C++完结之前高阶数据结构内容都放在④⑤两个专栏,

等后面C++完结还会学图和算法的内容。

先讲二叉搜索树是因为讲解 map 和 set 的特性需要二叉搜索树做铺垫,理解搜索二叉树有助于更好地理解 map 和 set 的特性。第二个原因是为了后期讲解查找效率极高的平衡搜索二叉树,随后再讲完红黑树,我们就可以模拟实现 map 和 set 了。

1. 二叉搜索树(BinarySearchTree)

概念:搜索二叉树(二叉搜索树)又称为二叉排序树,它或者是一颗空树,

或者是具有以下性质的二叉树:

  • 若其左子树不是空,则左子树上所有节点的值都小于根结点的值
  • 若其右子树不是空,则右子树上所有结点的值都大于根结点的值
  • 其左右子树必须都是二叉搜索树

至于叫它 "搜索二叉树",还是 "二叉搜索树",都是可以的,就是叫搜索二叉树得英文缩写不太好。

结论:任意一个子树都需要满足,左子树的值 < 根 < 右子树的值,才能构成二叉搜索树。 

1.1 二叉搜索树的优势和劣势

既然叫搜索二叉树,它肯定是用来搜索的,当满足搜索二叉树时你将可以快速地查找任意的值。

举个例子: 查找7

放到以前我们如果不用二分查找,可能会选择用暴力的方式去从头到尾遍历一遍。

但现在学了搜索二叉树,我们就可以轻松找到这个7了,7 比 8(根节点) 小,

根据搜索二叉树的性质,它必然不会出现在右子树 (右边大) ...

搜索二叉树查找一个值的最坏情况,也只是查找高度次。

二叉搜索树的时间复杂度:O(N)

上面的例子会让人误以为搜索二叉树的增删查改的时间复杂度是O(logN),

但实际上是O(N),因为这棵树是有可能会 蜕化 的,极端情况下会蜕化成一个 "单边树" 

比如按有序插入:

最差情况:二叉搜索树退化为单边树(或类似单边),其平均比较次数为:O(N)

最优情况:二叉搜索树为完全二叉树(或接近完全二叉树),其平均比较次数为:O(logN)

对于时间复杂度的分析我们要做一个悲观主义者,根据最差情况去定时间复杂度:O(N)

1.2 二叉搜索树的改良

果搜索二叉树蜕化成了单边树,其性能也就失去了,能否进行改进让它保持性能?

如何做到不论按照上面次序插入关键码,二叉搜索树的性能均能达到最优?

搜索二叉树由于控制不了极端情况,与 O(logN)失之交臂,但平衡二叉搜索树做到了。

严格意义上来说满二叉树才是O(logN)完全二叉树是接近O(logN)。

而平衡搜索二叉树维持左右两边均匀程度,让它接近完全二叉树,从而让效率趋近O(logN)。

后面我们学的各种树(AV树,红黑树)可以说都是二叉搜索树的改良。

2. 二叉搜索树的实现

2.1 二叉搜索树的定义

此时我们的BinarySearchTree就缩写成BSTree了。

这里我们用模板,模板参数我们给了一个K,表示 key 的意思(模板参数并非一定要用 T)。

template<class K>
class BSTreeNode
{
public:
	BSTreeNode(const K& key)
		:_left(nullptr)
		, _right(nullptr)
		, _key(key)
	{}

	BSTreeNode<K>* _left;
	BSTreeNode<K>* _right;
	K _key;
};

下面我们来定义整个树,BSTreeNode<K> 有些长了,我们不如将其 typedef 成 Node 。

这里我们构造函数都没必要写,它自己生成的就够用了:

template<class K>
class BSTree
{
	typedef BSTreeNode<K> Node;

protected:
	Node* _root = nullptr;
};

2.2 二叉搜索树的插入

二叉搜索树的插入是会“去重”的。

我们先来实现最简单的插入操作:

  • 如果树为空,则直接新增结点,赋值给 root 指针。
  • 如果树不为空,按二叉搜索树性质查找插入位置,插入新节点。

Insert 的实现我们可以用递归,也可以用非递归,这一块递归比非递归更难理解。

秉着先难后易的态度,我们先讲比较难理解的非递归版本

Step1:首先检查是否有根结点 _root,如果没有我们就 new 一个结点出来作为根结点。
此时插入成功,返回 true。

Step2:插入就需要找到插入位置,我们定义一个 cur 变量,从根节点开始,
根据搜索二叉树 性质,将 cur 结点的值与插入的值  进行大小比较。

如果插入的值大于当前结点值,则将 cur 结点向右移动 cur=cur->_right ;
如果插入的值小于当前节点值,就将 cur 结点向左移动 cur=cur->_left。

值得注意的是,我们还需要额外记录一下 cur 的父结点,

因为你不知道什么时候会碰到nullptr结束。

并且当我们找到插入位置后,仅仅 new 上一个新结点给 cur 是完成不了插入操作的!

因为直接这么做 cur 也只是一个局部变量而已,你需要 cur 跟上一层(cur 的父亲)相链接才行!

为了能找到上一层,所以我们还需要额外定义一个 prev 变量来记录 cur 的父结点,

在我们更换 cur 结点时记录父结点的位置 prev=cur 即可。

当然了,还有一种插入失败的情况,就是判断大小时出现等于的情况,返回 false 即可。

(重复的值是不允许插入的,默认情况是不允许冗余的!但是也有针对这个的变形,后续再说)

Step3:插入!new 一个新结点给 cur,此时 cur 只是一个局部变量,必须要和父亲链接,

此时应该链接父亲的左边,还是链接父亲的右边?我们不知道,所以我们需要再做一个比较:

如果父节点的值大于插入的值,则将 cur 链接到父亲左边 prev->_left=cur;

反之将 cur 链接到父亲右边  prev->_right=cur。

最后,插入成功返回 true。

insert 代码:

	bool Insert(const K& key)
	{
		if (_root == nullptr)
		{
			_root = new Node(key);
			return true;
		}

		Node* prev = nullptr;
		Node* cur = _root;
		while (cur != nullptr) // 找到要插入的位置
		{
			if (key < cur->_key) // 要插入的值比当前值小
			{
				prev = cur; // 记录cur,等下cur更新就是cur的父亲
				cur = cur->_left; // 到左边插入
			}
			else if (key > cur->_key)
			{
				prev = cur;
				cur = cur->_right;
			}
			else
			{
				return false; // 相等,插入失败
			}
		}

		cur = new Node(key); // 走到这,cur就是要插入的位置
		if (key < prev->_key) // 如果key比cur的父亲小
		{
			prev->_left = cur; // 插入到父亲的左孩子
		}
		else
		{
			prev->_right = cur;
		}
		return true;
	}

再写一个中序遍历来测试一下插入的效果:

	void InOrder(Node* root)
	{
		if (root == nullptr)
		{
			return;
		}
		InOrder(root->_left);
		cout << root->_key << " ";
		InOrder(root->_right);
	}

模拟出一个测试用例:

void TestBSTree1() 
{
	BSTree<int> t;
	int a[] = { 8, 3, 1, 10, 6, 4, 7, 14, 13 };
	for (const auto& e : a) 
	{
		t.Insert(e);
	}
	t.InOrder();  // 没法传根
}

此时会出现一个问题,因为根是私有的,我们没办法把根传过去。

此时我们可以选择在类内部写一个成员函数 GetRoot 去取根,但是这里我们可以选择这么做:

干脆将刚才我们实现的中序设为 protected 保护,然后再写一个 InOrder 放在公有的区域。

这就是在类内访问 _root 了,没有什么问题。

如此一来我们在类外就可以直接调用 InOrder,并且也不需要传递参数了。

BinarySearchTree.h:

#pragma once

#include <iostream>
using namespace std;

template<class K>
class BSTreeNode
{
public:
	BSTreeNode(const K& key)
		:_left(nullptr)
		, _right(nullptr)
		, _key(key)
	{}

	BSTreeNode<K>* _left;
	BSTreeNode<K>* _right;
	K _key;
};

template<class K>
class BSTree
{
	typedef BSTreeNode<K> Node;

public:
	bool Insert(const K& key)
	{
		if (_root == nullptr)
		{
			_root = new Node(key);
			return true;
		}

		Node* prev = nullptr;
		Node* cur = _root;
		while (cur != nullptr) // 找到要插入的位置
		{
			if (key < cur->_key) // 要插入的值比当前值小
			{
				prev = cur; // 记录cur,等下cur更新就是cur的父亲
				cur = cur->_left; // 到左边插入
			}
			else if (key > cur->_key)
			{
				prev = cur;
				cur = cur->_right;
			}
			else
			{
				return false; // 相等,插入失败
			}
		}

		cur = new Node(key); // 走到这,cur就是要插入的位置
		if (key < prev->_key) // 如果key比cur的父亲小
		{
			prev->_left = cur; // 插入到父亲的左孩子
		}
		else
		{
			prev->_right = cur;
		}
		return true;
	}

	void InOrder()
	{
		_InOrder(_root);
		cout << endl;
	}

protected:
	void _InOrder(Node* root)
	{
		if (root == nullptr)
		{
			return;
		}
		_InOrder(root->_left);
		cout << root->_key << " ";
		_InOrder(root->_right);
	}

	Node* _root = nullptr;
};

Test.c:

#include "BinarySearchTree.h"

void TestBSTree1() 
{
	BSTree<int> t;
	int arr[] = { 8, 3, 1, 10, 2, 2, 3, 6, 4, 7, 14, 13 };
	for (const auto& e : arr)
	{
		t.Insert(e);
	}
	t.InOrder();
}

int main()
{
	TestBSTree1();

	return 0;
}

2.3 二叉搜索树的查找

二叉搜索树的查找 Find 实现很容易,用和刚才一样的思路,从根结点开始查找。 

从根开始,如果要查找的值大于 cur 目前的值,则让 cur 往右走,反之往左走。

当查找得值与 cur 的值相等时则说明找到了,返回 true。

当 cur 触及到空(while 循环结束)则说明找不到,返回 false。

代码:

	bool Find(const K& key)
	{
		Node* cur = _root;
		while (cur != nullptr)
		{
			if (key < cur->_key)
			{
				cur = cur->_left;
			}
			else if (key > cur->_key)
			{
				cur = cur->_right;
			}
			else
			{
				return true;
			}
		}
		return false;
	}

2.4 二叉搜索树的删除

搜索二叉树删除的实现是有很有难度的。

没有孩子或者只有一个孩子,可以直接删除,孩子托管给父亲。

两个还是没办法给父亲,父亲养不了这么多孩子,但是可以找个人替代父亲养孩子。

当然,也不能随便找,找的人必须仍然维持搜索二叉树的性质,这是原则。

必须比左边的大,比右边的小。所以在家族中找是最合适的。

找左子树的最大值结点,或者右子树的最小值结点。

首先要查找元素是否在二叉搜索树中,如果不存在,则返回。

如果存在,那么删除的结点可能分为下面四种情况:

a. 要删除的结点无孩子结点
b. 要删除的结点只有左孩子结点
c. 要删除的结点只有右孩子结点
d. 要删除的结点有左孩子结点也有右孩子结点

看起来有待删除节点有 4 种情况,但实际上 a 和 b,或 a 和 c 可以合并。

因此,真正的删除过程如下:

我们还是定义一个 cur 变量,定义一个prev为空,后面是cur的父亲。

先找到要删除的结点,然后分下面三种情况

① 该结点无左孩子

如果要删除下面这颗二叉树的 10 节点和 4 节点:

当 cur 找到 10 结点后,如果左侧为空情况如下:

若该结点为 _root,直接让 _root 等于它的右孩子结点。
对于删除 10 结点:若 cur是右孩子,则令其父亲的右孩子指向其右孩子 (如图所示)
对于删除 4 结点:若 cur是左孩子,则令其父亲的左孩子指向其右孩子(如图所示)
最后删除 cur 结点

② 该结点无右孩子

如果要删除 14 结点,删除逻辑和删除左孩子是类似的:

③ 该结点有左右两个孩子

如果删除的结点有左右两个孩子,我们就在它的右子树中寻找中序的第一个结点。

即与右子树的最小值进行替换,当然也可以选择左子树的最大值进行替换。

例子:比如下面这颗子树,我们要删除 3 结点:

 如果该结点有两个孩子,则采用如下替换法:

该结点和右子树的最小值或左子树的最大值进行值的替换,然后删除替换后的结点。

这里我们采用与右子树的最小值进行替换。

Erase代码:

	bool Erase(const K& key)
	{
		Node* cur = _root;
		Node* father = nullptr;
		while (cur) // 找到要删除的结点
		{
			if (key < cur->_key)
			{
				father = cur;
				cur = cur->_left;
			}
			else if (key > cur->_key)
			{
				father = cur;
				cur = cur->_right;
			}
			else // 找到后开始删除,分三种情况
			{
				if (cur->_left == nullptr) // ①该结点无左孩子
				{
					if (cur == _root)
					{
						_root = cur->_right;
					}
					else
					{
						if (cur == father->_left)
						{
							father->_left = cur->_right;
						}
						else //(cur == father->_right)
						{
							father->_right = cur->_right;
						}
					}
					delete cur;
					cur = nullptr;
				}
				else if (cur->_right == nullptr) //  ②该结点无右孩子
				{
					if (cur == _root)
					{
						_root = cur->_left;
					}
					else
					{
						if (cur == father->_left)
						{
							father->_left = cur->_left;
						}
						else //(cur == father->_left)
						{
							father->_right = cur->_left;
						}
					}
					delete cur;
					cur = nullptr;
				}
				else // ③有两个结点,替换法删除
				{
					Node* MinNode = cur->_right;
					Node* MinParNode = cur;
					while (MinNode->_left) // 找右子树的最小
					{
						MinParNode = MinNode;
						MinNode = MinNode->_left;
					}
					swap(cur->_key, MinNode->_key); // 找到后交换

					if(MinParNode->_right == MinNode) // 链接父亲结点,这步易漏
					{
						MinParNode->_right = MinNode->_right;
					}
					else
					{
						MinParNode->_left = MinNode->_right;
					}
					delete MinNode;
					MinNode = nullptr;
				}
				return true;
			}
		}
		return false;
	}

Test.c:

#include "BinarySearchTree.h"

void TestBSTree1() 
{
	BSTree<int> t;
	int arr[] = { 8, 3, 1, 10, 2, 2, 3, 6, 4, 7, 14, 13 };
	for (const auto& e : arr)
	{
		t.Insert(e);
	}
	t.InOrder();

	t.Erase(8);
	t.Erase(3);
	t.InOrder();
	for (const auto& e : arr)
	{
		t.Erase(e);
	}
	t.InOrder();
}

int main()
{
	TestBSTree1();

	return 0;
}

2.5 二叉搜索树的查找(递归)

二叉搜索树的递归查找很简单,因为外面不能传根,这里像InOrder一样封装起来

	bool FindR(const K& key)
	{
		return _FindR(_root, key);
	}

	bool _FindR(Node* root, const K& key)
	{
		if (root == nullptr)
		{
			return false;
		}
		if (key < root->_key)
		{
			_FindR(root->_left, key);
		}
		else if (key > root->_key)
		{
			_FindR(root->_right, key);
		}
		else
		{
			return true;
		}
	}

2.6 二叉搜索树的插入(递归)

二叉搜索树的递归插入如果这样写是插入不了的:

	bool InsertR(const K& key)
	{
		return _InsertR(_root, key);
	}

	bool _InsertR(Node* root, const K& key)
	{
		if (root == nullptr)
		{
			root = new Node(key);
			return true;
		}

		if (key < root->_key)
		{
			return _InsertR(root->_left, key);
		}
		else if (key > root->_key)
		{
			return _InsertR(root->_right, key);
		}
		else
		{
			return false;
		}
	}

因为递归到最后一步的时候,root只是一个局部变量,根本插入不了数据。

可以一步一步的把父亲传下来,但是这里有一个神之一手:加引用:

	bool InsertR(const K& key)
	{
		return _InsertR(_root, key);
	}

	bool _InsertR(Node*& root, const K& key)
	{
		if (root == nullptr)
		{
			root = new Node(key);
			return true;
		}

		if (key < root->_key)
		{
			return _InsertR(root->_left, key);
		}
		else if (key > root->_key)
		{
			return _InsertR(root->_right, key);
		}
		else
		{
			return false;
		}
	}

这里的引用最后一步才起作用,它是空,但它也是上一层传下来的别名。

给给root,就把父亲链接起来了,可以用二级指针,但是用引用很方便。

2.7 二叉搜索树的删除(递归)

这里的递归删除和上面的递归插入一样,也用到了非常巧妙的引用:

	bool EraseR(const K& key)
	{
		return _EraseR(_root, key);
	}

	bool _EraseR(Node*& root, const K& key)
	{
		if (root == nullptr)
		{
			return false;
		}

		if (root->_key < key)
		{
			return _EraseR(root->_right, key);
		}
		else if (root->_key > key)
		{
			return _EraseR(root->_left, key);
		}
		else // 找到要删除的结点,开始删除
		{
			Node* del = root;
			if (root->_left == nullptr) // 这里就体现了引用的神之一手,根本不用判断父亲
			{
				root = root->_right;
			}
			else if (root->_right == nullptr)
			{
				root = root->_left;
			}
			else // 找右树的最左节点替换删除
			{
				Node* MinNode = root->_right;
				while (MinNode->_left)
				{
					MinNode = MinNode->_left;
				}
				swap(root->_key, MinNode->_key);
				//return EraseR(key);  错的
				return _EraseR(root->_right, key);
			}
			delete del;
			return true;
		}
	}

Test.c:

#include "BinarySearchTree.h"

void TestBSTree1() 
{
	BSTree<int> t;
	int arr[] = { 8, 3, 1, 10, 2, 2, 3, 6, 4, 7, 14, 13 };
	for (const auto& e : arr)
	{
		t.Insert(e);
	}
	t.InOrder();

	t.Erase(8);
	t.Erase(3);
	t.InOrder();
	for (const auto& e : arr)
	{
		t.Erase(e);
	}
	t.InOrder();
}

void TestBSTree2()
{
	BSTree<int> t;
	int arr[] = { 8, 3, 1, 10, 2, 2, 3, 6, 4, 7, 14, 13 };
	for (const auto& e : arr)
	{
		t.InsertR(e);
	}
	t.InOrder();

	t.EraseR(8);
	t.EraseR(3);
	t.EraseR(2);
	t.InOrder();
	cout << t.Find(10) << endl;
	cout << t.Find(100) << endl;
	cout << t.FindR(10) << endl;
	cout << t.FindR(100) << endl;
	for (const auto& e : arr)
	{
		t.EraseR(e);
	}
	t.InOrder();
}

int main()
{
	TestBSTree2();

	return 0;
}

2.8 析构和拷贝构造和赋值

Test.c: 默认生成的:

#include "BinarySearchTree.h"

void TestBSTree3()
{
	BSTree<int> t;
	int arr[] = { 8, 3, 1, 10, 2, 2, 3, 6, 4, 7, 14, 13 };
	for (const auto& e : arr)
	{
		t.InsertR(e);
	}
	t.InOrder();

	BSTree<int> copy = t;
	copy.InOrder();
}

int main()
{
	TestBSTree3();

	return 0;
}

这里也是浅拷贝的问题,指向的是同一颗树,没崩只是没写析构,写下析构:

	~BSTree()
	{
		_Destory(_root);
	}

protected:
	void _Destory(Node*& root) // 加引用下面的置空就起作用了
	{
		if (root == nullptr)
		{
			return;
		}
		_Destory(root->_left);
		_Destory(root->_right);
		delete root;
		root = nullptr;
	}

再运行刚才测试,程序崩溃:

 这时我们就应该自己写拷贝构造了:

	BSTree(const BSTree<K>& t)
	{
		_root = _Copy(t._root);
	}

protected:
	Node* _Copy(Node* root)
	{
		if (root == nullptr)
		{
			return nullptr;
		}

		Node* CopyRoot = new Node(root->_key); // 中序递归链接左右子树
		CopyRoot->_left = _Copy(root->_left);
		CopyRoot->_right = _Copy(root->_right);
		return CopyRoot;
	}

这时运行就会报错:

错误(活动)    E0291    类 "BSTree<int>" 不存在默认构造函数

这时写个默认的拷贝构造:

	BSTree(const BSTree<K>& t)
	{
		_root = _Copy(t._root);
	}

	//BSTree() // 这样写也行,但是下面是C++11的用法
	//{}
	BSTree() = default; // C++11的关键字,强制编译器生成默认的构造

protected:
	Node* _Copy(Node* root)
	{
		if (root == nullptr)
		{
			return nullptr;
		}

		Node* CopyRoot = new Node(root->_key); // 中序递归链接左右子树
		CopyRoot->_left = _Copy(root->_left);
		CopyRoot->_right = _Copy(root->_right);
		return CopyRoot;
	}

运行程序:

 写了拷贝构造我们就可以直接用现代写法写一个赋值:

	BSTree<K> operator=(BSTree<K> t)
	{
		swap(_root, t._root);
		return *this;
	}

测试:

void TestBSTree3()
{
	BSTree<int> t;
	int arr[] = { 8, 3, 1, 10, 2, 2, 3, 6, 4, 7, 14, 13 };
	for (const auto& e : arr)
	{
		t.InsertR(e);
	}
	t.InOrder();

	BSTree<int> copy = t;
	copy.InOrder();

	BSTree<int> t2;
	t2.Insert(3);
	t2.Insert(5);
	t2.Insert(4);
	copy = t2;
	t2.InOrder();
	copy.InOrder();
}

3. 搜索二叉树的应用

3.1 K 模型

K模型,即只有 key 作为关键码,我们上面写的就是K模型,

结构中只需存储 key 即可,关键码就是需要搜索到的值。

举个例子:对于单词 word,我们需要判断该单词是否拼写正确

以单词集合中的每个单词作为 key,构建一个搜索二叉树。
在二叉树中检索该单词是否存在,存在则拼写正确,不存在则拼写错误。
 

3.2 KV 模型

KV模型,每一个关键码 key,都有与之对应的值 Value,即 <Key, Value> 的键值对。

这就像 Python 中的 dict 字典类型一样,key 和 value 对应。

这在生活中也是非常常见的,比如英汉词典就是英文与中文的对应关系,通过英文可以快读检索到对应的中文,英文单词也可以与其对应的中文构建出一种键值对:

<string, string>   即  <word, chinese>


再比如统计水果次数,就构建出了一种键值对:

<string, int>   即  <水果, count>

直接放用于测试的代码:

//二叉搜索树的KV结构
namespace KeyValue
{
	//定义两个类模板参数K、V
	template<class K, class V>
	class BSTreeNode
	{
	public:
		BSTreeNode(const K& key, const V& value)
			:_left(nullptr)
			, _right(nullptr)
			, _key(key)
			, _value(value)
		{}

		BSTreeNode<K, V>* _left;
		BSTreeNode<K, V>* _right;
		K _key; //存放了两个类型的数据,相比较与K模型
		V _value;
	};

	//同样的,定义两个类模板参数K、V
	//搜索二叉树依旧是按照K的数据进行排序,和V无关
	template<class K, class V>
	class BSTree
	{
		typedef BSTreeNode<K, V> Node;
	public:
		bool Insert(const K& key, const V& value)
		{
			if (_root == nullptr)
			{
				_root = new Node(key, value);
				return true;
			}

			Node* parent = nullptr;
			Node* cur = _root;
			while (cur)
			{
				if (cur->_key < key)
				{
					parent = cur;
					cur = cur->_right;
				}
				else if (cur->_key > key)
				{
					parent = cur;
					cur = cur->_left;
				}
				else
				{
					return false;
				}
			}

			cur = new Node(key, value);
			if (parent->_key < key)
			{
				parent->_right = cur;
			}
			else
			{
				parent->_left = cur;
			}

			return true;
		}

		//查找只和数据_key有关,与数据_value无关
		Node* Find(const K& key)
		{
			Node* cur = _root;
			while (cur)
			{
				if (cur->_key < key)
				{
					cur = cur->_right;
				}
				else if (cur->_key > key)
				{
					cur = cur->_left;
				}
				else
				{
					return cur;
				}
			}

			return nullptr;
		}

		//删除只和数据_key有关,与数据_value无关
		bool Erase(const K& key)
		{
			//... 和K模型一样
			return true;
		}

		void InOrder()
		{
			_InOrder(_root);
			cout << endl;
		}
	protected:
		void _InOrder(Node* root)
		{
			if (root == nullptr)
			{
				return;
			}

			_InOrder(root->_left);
			cout << root->_key << ":" << root->_value << endl;
			_InOrder(root->_right);
		}

		Node* _root = nullptr;
	};

	void TestBSTree1()
	{
		BSTree<string, string> dict; // 字典树,如果所有单词都在里面就能很准确查找
		dict.Insert("sort", "排序");
		dict.Insert("left", "左边");
		dict.Insert("right", "右边");
		dict.Insert("string", "字符串");
		dict.Insert("insert", "插入");
		string str;
		while (cin >> str) // Crtl+z+换行结束,或者Crtl+c结束
		{
			BSTreeNode<string, string>* ret = dict.Find(str);
			if (ret)
			{
				cout << "对应的中文:" << ret->_value << endl;
			}
			else
			{
				cout << "对应的中文->无此单词" << endl;
			}
		}
	}

	void TestBSTree2() // 统计水果出现的次数
	{
		string arr[] = { "香蕉", "苹果", "香蕉", "草莓", "香蕉", "苹果", "苹果", "苹果" };

		BSTree<string, int> countTree;
		for (auto& str : arr)
		{
			//BSTreeNode<string, int>* ret = countTree.Find(str);
			auto ret = countTree.Find(str);
			if (ret)
			{
				ret->_value++;
			}
			else
			{
				countTree.Insert(str, 1);
			}
		}

		countTree.InOrder();
	}
}

4. 笔试选择题

1. 关于二叉搜索树特性说法错误的是( )

A.二叉搜索树最左侧的节点一定是最小的

B.二叉搜索树最右侧的节点一定是最大的

C.对二叉搜索树进行中序遍历,一定能够得到一个有序序列

D.二叉搜索树的查找效率为O(log_2N)

2. 下面的哪个序列可能是二叉搜索树中序遍历的结果? ( )

A.73 8 2 9 4 11

B. 2 3 4 7 8 9 11

C.11 2 9 3 8 4 7

D.以上均可

3. 将整数序列(7-2-4-6-3-1-5)按所示顺序构建一棵二叉排序树a(亦称二叉搜索树),之后将整数8按照二叉排序树规则插入树a中,请问插入之后的树a中序遍历结果是( ) 

A.1-2-3-4-5-6-7-8

B.7-2-1-4-3-6-5-8

C.1-3-5-2-4-6-7-8

D.1-3-5-6-4-2-8-7

E.7-2-8-1-4-3-6-5

F.5-6-3-4-1-2-7-8

4. 下面关于二叉搜索树正确的说法是( )

A.待删除节点有左子树和右子树时,只能使用左子树的最大值节点替换待删除节点

B.给定一棵二叉搜索树的前序和中序遍率历结果,无法确定这棵二叉搜索树

C.给定一棵二叉搜索树,根据节点值大小排序所需时间复杂度是线性的

D.给定一棵二叉搜索树,可以在线性时间复杂度内转化为平衡二叉搜索树

答案:

1. D

二叉搜索树的概念:

  二叉搜索树又称二叉排序树,它或者是一棵空树,或者是具有以下性质的二叉树:

   1. 若它的左子树不为空,则左子树上所有节点的值都小于根节点的值

   2. 若它的右子树不为空,则右子树上所有节点的值都大于根节点的值

   3. 它的左右子树也分别为二叉搜索树

  从概念中可以得出以下性质:

   1. 二叉搜索树中最左侧节点一定是最小的,最右侧节点一定是最大的

   2. 对二叉搜索树进行中序遍历,可以得到一个有序的序列

  A ,B,C:正确

  D:错误,二叉搜索树最差情况下会退化为单支树,因此:其查找的效率为O(N)

2. B

二叉搜索树的特性:如果对二叉搜索树进行中序遍历,可以得到有序的序列

3. A

插入之后的树仍旧是二叉搜索树,因此只要是有序的结果则正确,而有序的结果只有A

4. C

 A:错误,当待删除节点的左右子树均存在时,既可以在左子树中找一个最大的节点作为替代节 点,也可以在右子树中找一个最小的节点作为替代节点,左右子树中都可以找替代节点

  B:错误,根据前序遍历和中序遍历,是可以确定一棵树的结构,使用两个遍历结果确定树的结构, 其中有一个遍历结果必须要是中序遍历结果。

  C:正确,二叉搜索树遍历一遍,就可以得到一个有序序列,因此,时间复杂度为O(N)

  D:错误,这里面还需要牵扯到旋转等其他操作,时间复杂度不是线性的

5. 完整代码:

#pragma once

#include <iostream>
#include <algorithm>
using namespace std;

template<class K>
class BSTreeNode
{
public:
	BSTreeNode(const K& key)
		:_left(nullptr)
		, _right(nullptr)
		, _key(key)
	{}

	BSTreeNode<K>* _left;
	BSTreeNode<K>* _right;
	K _key;
};

template<class K>
class BSTree
{
	typedef BSTreeNode<K> Node;

public:
	bool Insert(const K& key)
	{
		if (_root == nullptr)
		{
			_root = new Node(key);
			return true;
		}

		Node* prev = nullptr;
		Node* cur = _root;
		while (cur != nullptr) // 找到要插入的位置
		{
			if (key < cur->_key) // 要插入的值比当前值小
			{
				prev = cur; // 记录cur,等下cur更新就是cur的父亲
				cur = cur->_left; // 到左边插入
			}
			else if (key > cur->_key)
			{
				prev = cur;
				cur = cur->_right;
			}
			else
			{
				return false; // 相等,插入失败
			}
		}

		cur = new Node(key); // 走到这,cur就是要插入的位置
		if (key < prev->_key) // 如果key比cur的父亲小
		{
			prev->_left = cur; // 插入到父亲的左孩子
		}
		else
		{
			prev->_right = cur;
		}
		return true;
	}

	bool Find(const K& key)
	{
		Node* cur = _root;
		while (cur != nullptr)
		{
			if (key < cur->_key)
			{
				cur = cur->_left;
			}
			else if (key > cur->_key)
			{
				cur = cur->_right;
			}
			else
			{
				return true;
			}
		}
		return false;
	}

	bool Erase(const K& key)
	{
		Node* cur = _root;
		Node* father = nullptr;
		while (cur) // 找到要删除的结点
		{
			if (key < cur->_key)
			{
				father = cur;
				cur = cur->_left;
			}
			else if (key > cur->_key)
			{
				father = cur;
				cur = cur->_right;
			}
			else // 找到后开始删除,分三种情况
			{
				if (cur->_left == nullptr) // ①该结点无左孩子
				{
					if (cur == _root)
					{
						_root = cur->_right;
					}
					else
					{
						if (cur == father->_left)
						{
							father->_left = cur->_right;
						}
						else //(cur == father->_right)
						{
							father->_right = cur->_right;
						}
					}
					delete cur;
					cur = nullptr;
				}
				else if (cur->_right == nullptr) //  ②该结点无右孩子
				{
					if (cur == _root)
					{
						_root = cur->_left;
					}
					else
					{
						if (cur == father->_left)
						{
							father->_left = cur->_left;
						}
						else //(cur == father->_left)
						{
							father->_right = cur->_left;
						}
					}
					delete cur;
					cur = nullptr;
				}
				else // ③有两个结点,替换法删除
				{
					Node* MinNode = cur->_right;
					Node* MinParNode = cur;
					while (MinNode->_left) // 找右子树的最小
					{
						MinParNode = MinNode;
						MinNode = MinNode->_left;
					}
					swap(cur->_key, MinNode->_key); // 找到后交换

					if(MinParNode->_right == MinNode) // 链接父亲结点,这步易漏
					{
						MinParNode->_right = MinNode->_right;
					}
					else
					{
						MinParNode->_left = MinNode->_right;
					}
					delete MinNode;
					MinNode = nullptr;
				}
				return true;
			}
		}
		return false;
	}

	void InOrder()
	{
		_InOrder(_root);
		cout << endl;
	}

	bool FindR(const K& key)
	{
		return _FindR(_root, key);
	}

	bool InsertR(const K& key)
	{
		return _InsertR(_root, key);
	}

	bool EraseR(const K& key)
	{
		return _EraseR(_root, key);
	}

	~BSTree()
	{
		_Destory(_root);
	}

	BSTree(const BSTree<K>& t)
	{
		_root = _Copy(t._root);
	}

	//BSTree() // 这样写也行,但是下面是C++11的用法
	//{}
	BSTree() = default; // C++11的关键字,强制编译器生成默认的构造

	BSTree<K> operator=(BSTree<K> t)
	{
		swap(_root, t._root);
		return *this;
	}

protected:
	Node* _Copy(Node* root)
	{
		if (root == nullptr)
		{
			return nullptr;
		}

		Node* CopyRoot = new Node(root->_key); // 中序递归链接左右子树
		CopyRoot->_left = _Copy(root->_left);
		CopyRoot->_right = _Copy(root->_right);
		return CopyRoot;
	}

	void _Destory(Node*& root) // 加引用下面的置空就起作用了
	{
		if (root == nullptr)
		{
			return;
		}
		_Destory(root->_left);
		_Destory(root->_right);
		delete root;
		root = nullptr;
	}

	bool _FindR(Node* root, const K& key)
	{
		if (root == nullptr)
		{
			return false;
		}
		if (key < root->_key)
		{
			_FindR(root->_left, key);
		}
		else if (key > root->_key)
		{
			_FindR(root->_right, key);
		}
		else
		{
			return true;
		}
	}

	bool _InsertR(Node*& root, const K& key)
	{
		if (root == nullptr)
		{
			root = new Node(key);
			return true;
		}

		if (key < root->_key)
		{
			return _InsertR(root->_left, key);
		}
		else if (key > root->_key)
		{
			return _InsertR(root->_right, key);
		}
		else
		{
			return false;
		}
	}

	bool _EraseR(Node*& root, const K& key)
	{
		if (root == nullptr)
		{
			return false;
		}

		if (root->_key < key)
		{
			return _EraseR(root->_right, key);
		}
		else if (root->_key > key)
		{
			return _EraseR(root->_left, key);
		}
		else // 找到要删除的结点,开始删除
		{
			Node* del = root;
			if (root->_left == nullptr) // 这里就体现了引用的神之一手,根本不用判断父亲
			{
				root = root->_right;
			}
			else if (root->_right == nullptr)
			{
				root = root->_left;
			}
			else // 找右树的最左节点替换删除
			{
				Node* MinNode = root->_right;
				while (MinNode->_left)
				{
					MinNode = MinNode->_left;
				}
				swap(root->_key, MinNode->_key);
				//return EraseR(key);  错的
				return _EraseR(root->_right, key);
			}
			delete del;
			return true;
		}
	}

	void _InOrder(Node* root)
	{
		if (root == nullptr)
		{
			return;
		}
		_InOrder(root->_left);
		cout << root->_key << " ";
		_InOrder(root->_right);
	}

	Node* _root = nullptr;
};

//二叉搜索树的KV结构
namespace KeyValue
{
	//定义两个类模板参数K、V
	template<class K, class V>
	class BSTreeNode
	{
	public:
		BSTreeNode(const K& key, const V& value)
			:_left(nullptr)
			, _right(nullptr)
			, _key(key)
			, _value(value)
		{}

		BSTreeNode<K, V>* _left;
		BSTreeNode<K, V>* _right;
		K _key; //存放了两个类型的数据,相比较与K模型
		V _value;
	};

	//同样的,定义两个类模板参数K、V
	//搜索二叉树依旧是按照K的数据进行排序,和V无关
	template<class K, class V>
	class BSTree
	{
		typedef BSTreeNode<K, V> Node;
	public:
		bool Insert(const K& key, const V& value)
		{
			if (_root == nullptr)
			{
				_root = new Node(key, value);
				return true;
			}

			Node* parent = nullptr;
			Node* cur = _root;
			while (cur)
			{
				if (cur->_key < key)
				{
					parent = cur;
					cur = cur->_right;
				}
				else if (cur->_key > key)
				{
					parent = cur;
					cur = cur->_left;
				}
				else
				{
					return false;
				}
			}

			cur = new Node(key, value);
			if (parent->_key < key)
			{
				parent->_right = cur;
			}
			else
			{
				parent->_left = cur;
			}

			return true;
		}

		//查找只和数据_key有关,与数据_value无关
		Node* Find(const K& key)
		{
			Node* cur = _root;
			while (cur)
			{
				if (cur->_key < key)
				{
					cur = cur->_right;
				}
				else if (cur->_key > key)
				{
					cur = cur->_left;
				}
				else
				{
					return cur;
				}
			}

			return nullptr;
		}

		//删除只和数据_key有关,与数据_value无关
		bool Erase(const K& key)
		{
			//... 和K模型一样
			return true;
		}

		void InOrder()
		{
			_InOrder(_root);
			cout << endl;
		}
	protected:
		void _InOrder(Node* root)
		{
			if (root == nullptr)
			{
				return;
			}

			_InOrder(root->_left);
			cout << root->_key << ":" << root->_value << endl;
			_InOrder(root->_right);
		}

		Node* _root = nullptr;
	};

	void TestBSTree1()
	{
		BSTree<string, string> dict; // 字典树,如果所有单词都在里面就能很准确查找
		dict.Insert("sort", "排序");
		dict.Insert("left", "左边");
		dict.Insert("right", "右边");
		dict.Insert("string", "字符串");
		dict.Insert("insert", "插入");
		string str;
		while (cin >> str) // Crtl+z+换行结束,或者Crtl+c结束
		{
			BSTreeNode<string, string>* ret = dict.Find(str);
			if (ret)
			{
				cout << "对应的中文:" << ret->_value << endl;
			}
			else
			{
				cout << "对应的中文->无此单词" << endl;
			}
		}
	}

	void TestBSTree2() // 统计水果出现的次数
	{
		string arr[] = { "香蕉", "苹果", "香蕉", "草莓", "香蕉", "苹果", "苹果", "苹果" };

		BSTree<string, int> countTree;
		for (auto& str : arr)
		{
			//BSTreeNode<string, int>* ret = countTree.Find(str);
			auto ret = countTree.Find(str);
			if (ret)
			{
				ret->_value++;
			}
			else
			{
				countTree.Insert(str, 1);
			}
		}

		countTree.InOrder();
	}
}
#include "BinarySearchTree.h"

void TestBSTree1() 
{
	BSTree<int> t;
	int arr[] = { 8, 3, 1, 10, 2, 2, 3, 6, 4, 7, 14, 13 };
	for (const auto& e : arr)
	{
		t.Insert(e);
	}
	t.InOrder();

	t.Erase(8);
	t.Erase(3);
	t.InOrder();
	for (const auto& e : arr)
	{
		t.Erase(e);
	}
	t.InOrder();
}

void TestBSTree2()
{
	BSTree<int> t;
	int arr[] = { 8, 3, 1, 10, 2, 2, 3, 6, 4, 7, 14, 13 };
	for (const auto& e : arr)
	{
		t.InsertR(e);
	}
	t.InOrder();

	t.EraseR(8);
	t.EraseR(3);
	t.EraseR(2);
	t.InOrder();
	cout << t.Find(10) << endl;
	cout << t.Find(100) << endl;
	cout << t.FindR(10) << endl;
	cout << t.FindR(100) << endl;
	for (const auto& e : arr)
	{
		t.EraseR(e);
	}
	t.InOrder();
}

void TestBSTree3()
{
	BSTree<int> t;
	int arr[] = { 8, 3, 1, 10, 2, 2, 3, 6, 4, 7, 14, 13 };
	for (const auto& e : arr)
	{
		t.InsertR(e);
	}
	t.InOrder();

	BSTree<int> copy = t;
	copy.InOrder();

	BSTree<int> t2;
	t2.Insert(3);
	t2.Insert(5);
	t2.Insert(4);
	copy = t2;
	t2.InOrder();
	copy.InOrder();
}

int main()
{
	//TestBSTree3();
	KeyValue::TestBSTree2();

	return 0;
}

本章完。

下一部分:树的OJ题,然后是map和set,再然后是AVL树和红黑树。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/724382.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

(汽车级)TMCS1101A3BQDRQ1、TMCS1101A3UQDRQ1隔离式霍尔效应电流传感器 6mA 8-SOIC

TMCS1101/TMCS1101-Q1精密隔离电流传感器是电隔离霍尔效应电流传感器&#xff0c;能够进行直流或交流电流测量&#xff0c;具有高精度、出色的线性度以及温度稳定性等特性。低漂移、温度补偿信号链在整个器件温度范围内具有<1.5%满量程误差。输入电流流经内部1.8mΩ导体&…

只需2行python代码,轻松将PDF转换成Word

1. pdf2docx功能 - 解析和创建页面布局- 页边距- 章节和分栏 (目前最多支持两栏布局)- 页眉和页脚 [TODO]- 解析和创建段落- OCR 文本 [TODO] - 水平&#xff08;从左到右&#xff09;或竖直&#xff08;自底向上&#xff09;方向文本- 字体样式例如字体、字号、粗/斜体、颜色…

什么是V2G充电技术?

据权威数据统计&#xff0c;截止2020年&#xff0c;我国纯电动汽车保有量已经达到400万辆。随着电动汽车保有量的快速增长&#xff0c;对电网会有一定的影响&#xff0c;电力容量需求增大&#xff0c;加剧用电峰谷差&#xff0c;对电网的冲击大&#xff0c;影响居民正常生活。这…

C#核心知识回顾——11.各数据集合的汇总区分、委托、事件

变量: 无符号 byte正8位 ushort正16位 uint正32位 ulong正64位 有符号 sbyte8位 short16位 int32位 long64位 浮点数 float double decimal 特殊 char bool string 复杂数据容器: 枚举enum 结构体struct 数组(一维、二维、交错) [] [,] [][] 类 数据集合&#xff1a;…

leetcode--杨辉三角(C、C++)

文章目录 1.C实现2.C实现 1.C实现 #include<stdio.h> #include<stdlib.h> #include<assert.h>int** generate(int n, int* rnum_row, int** rnum_row_ele) {//*rnum_row-->num_row&#xff1a;杨辉三角行数*rnum_row n;//*rnum_row_ele-->num_row_el…

深度学习笔记之Transformer(五) Position Embedding铺垫:Word2vec

深度学习笔记之Transformer——Position Embedding铺垫&#xff1a;Word2vec 引言回顾&#xff1a;关于词特征表示的 One-hot \text{One-hot} One-hot编码目标函数构建关于语料库与任务目标似然函数构建 Word2vec \text{Word2vec} Word2vec模型结构重点总结 引言 在Transforme…

Zabbix“专家坐诊”第198期问答汇总

问题一 Q&#xff1a;请问一下&#xff0c;自带的思科SNMP交换机模板&#xff0c;怎么不监控down的接口&#xff1f; A1&#xff1a;这种一般在自动发现规则里加个过滤器&#xff0c;过滤出IFSTATUS匹配(1|3)的就能实现只发现up的端口了。 A2&#xff1a; 在“Items”页面&a…

Unity | 从建模到蒙皮动画的整个过程

目录 一、顶点数组、索引数组及UV数组 二、Mesh、MeshFilter、MeshRenderer及SkinnedMeshRenderer 1. Mesh 2. MeshFilter 3. MeshRenderer 4. MeshRenderer与SkinnedMeshRenderer&#xff08;蒙皮网格&#xff09; 三、Unity中相关组件 1. mesh和material 2. sharedM…

【微信小程序创作之路】- 小程序项目组成(初创小程序)

【微信小程序创作之路】- 小程序项目组成&#xff08;初创小程序&#xff09; 提示&#xff1a;第二章 初创小程序&#xff0c;讲解小程序项目组成 文章目录 【微信小程序创作之路】- 小程序项目组成&#xff08;初创小程序&#xff09;前言一、下载安装小程序微信开发者工具二…

Python multiprocessing 多进程

在multiporcessing中&#xff0c;通过新建Process对象创建进程&#xff0c;然后用start()方法调用它。Process与threading.Thread类似。如下是一个简单的例子&#xff1a; from multiprocessing import Processdef f(name):print(hello ,name)if __name__ __main__:p Proces…

2D和3D双管齐下才是王道?KAUST联合Snap、VGG提出单张图像三维重建的Magic123框架

在最近的AIGC社区中&#xff0c;3D视觉生成领域越来越受到广泛的关注&#xff0c;以神经辐射场&#xff08;NeRFs&#xff09; 为基础的深度渲染网络向大家展示了非常惊艳的三维效果。可是&#xff0c;NeRFs需要大量的多视角图片作为监督&#xff0c;因而从单张2D图像进行3D重建…

昆仑万维的“天工”能否改变人工智能应用的格局?

在人工智能领域&#xff0c;昆仑万维公司近日引发了广泛关注&#xff0c;发布了名为“天工”的大语言模型。这一新的技术突破引发了人们对于人工智能应用格局是否会被改变的讨论。随着昆仑万维进军大型语言模型市场&#xff0c;人们开始思考&#xff0c;“天工”能否成为人工智…

面部表情动画数据捕捉利器——AH表情捕捉头盔

在游戏制作、电视广告、影视动画制作中&#xff0c;逐渐开始采用面部捕捉头盔进行采集表情面部数据。广州虚拟动力推出的AH表情捕捉头盔&#xff0c;能够轻松创建实时逼真的面部动画&#xff0c;搭配广州虚拟动力的动作捕捉系统VDMocap Studio Plus&#xff0c;能够自动运算出表…

第八章:怎样远程连接+远程连接常用工具+Teamviewer详解

目录 一、什么是远程连接 二、为什么需要远程连接 三、常用的远程连接工具 四、Teamviewer 一、什么是远程连接 远程连接就是在远程连接另外一台计算机。当某台计算机开启了远程桌面连接功能后我们就可以在网络的另一端控制这台计算机了&#xff0c;通过远程桌面功能我们可…

基于Java校园二手物品交易平台设计实现(源码+lw+部署文档+讲解等)

博主介绍&#xff1a;✌全网粉丝30W,csdn特邀作者、博客专家、CSDN新星计划导师、Java领域优质创作者,博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌ &#x1f345;文末获取源码联系&#x1f345; &#x1f447;&#x1f3fb; 精彩专…

hive函数dayofweek的奇怪用法

hive函数dayofweek的奇怪用法 1.单独使用 select dayofweek(2023-07-03); 周一 2 select dayofweek(2023-07-04); 周二 3 select dayofweek(2023-07-05); 周三 4 select dayofweek(2023-07-06); 周四 5 select dayofweek(2023-07-07); 周五 6 select dayofweek(2023-07-08); …

VIO在ARM上的加速(2)- Neon

VIO在ARM上的加速&#xff1a; VIO在ARM上的加速&#xff08;1&#xff09;- ARM加速基础 VIO在ARM上的加速&#xff08;2&#xff09;- Neon VIO在ARM上的加速&#xff08;3&#xff09;- Neon在VIO中的应用 1 NEON的概述 ARM 处理器中使用的高级 SIMD 扩展的实现称为 NE…

【数据结构与算法】魔王语言解释(C/C++)

实践要求 1. 问题描述 有一个魔王总是使用自己的一种非常精炼而抽象的语言讲话&#xff0c;没有人能听懂。但他的语言是可以逐步解释成人能懂的语言的&#xff0c;因为他的语言是由以下两种形式的规则由人的语言逐步抽象上去的: 形式一 α → β 1 β 2 . . . β m \alpha \…

无线耳机推荐的品牌有哪些?八款无线蓝牙耳机推荐

无线蓝牙耳机无疑是当前最受欢迎的数码产品之一&#xff0c;平听闲暇时刻听听歌或者是运动健身&#xff0c;常常能看到蓝牙耳机的身影&#xff0c;作为一个热衷于听歌的精致boy&#xff0c;佩戴过的蓝牙耳机数不胜数&#xff0c;现在&#xff0c;除了手机品牌会开发无线蓝牙耳机…

西电_矩阵论_学习笔记

文章目录 【 第一章 线性空间 】【 第二章 范数 】【 第三章 矩阵函数 】【 第四章 矩阵分解 】【 第五章 矩阵特征值估计 】【 第六章 广义逆 】【 考试重点内容总结 】 这是博主2023春季西电所学矩阵论的思维导图&#xff08;软件是幕布&#xff09;&#xff0c;供大家参考&a…