数据结构——C++实现二叉搜索树,前中后序、层序迭代遍历配合仿函数

2023-09-13 13:33:51

通过介绍二叉搜索树,到实现最基础的二叉树模型,四种迭代遍历方式。

结点模型

template<class Type>
class binary_tree
{
	/* 二叉树是由多个结点组成的,所以定义一个内部的结点类用于构建树 */
	class BTNode
	{
		/* 不允许无参构造,因为编译器会对m_val采用默认构造,如果是int类型会导致随机值,可能造成问题 */
		BTNode() = delete;
	public:
		/* 防止隐式类型转换 */
		explicit BTNode(const Type& _val) : m_val(_val), m_left(nullptr), m_right(nullptr) {}

		Type m_val;
		BTNode* m_left;
		BTNode* m_right;
	};
	
	/* 创建结点,如果new失败则抛异常,需要在调用的时候捕获 */
	static BTNode* create_node(const Type& _val)
	{
		BTNode* newNode = new BTNode(_val);
		if (newNode == nullptr)
		{
			throw std::exception("create_node failed");
		}
		return newNode;
	}
	

二叉树遍历

前序遍历

/* 
		前中后须遍历,统一使用标记法,使得三种遍历代码结构相似,和层序也有点相似
		如果用第一种方法,前序会跟后序相似,但是中序会有差别 
		
		同时通过传入function对象,使得遍历拿到结点后可以执行自定义操作
		返回值为bool是为了确认是否继续往后遍历,比如find,找到后应该返回true,表示不再往后遍历的,树中保证值是唯一的
	*/
	static void pre_order(BTNode* root, std::function<bool(BTNode*)> func)
	{
		/* 方法一 */
		//if (root == nullptr)
		//	return;

		//std::stack<BTNode*> st;
		//st.push(root);

		//while (!st.empty())
		//{
		//	BTNode* node = st.top();
		//	st.pop();

		//	if (node->m_right)
		//	{
		//		st.push(node->m_right);
		//	}
		//	if (node->m_left)
		//	{
		//		st.push(node->m_left);
		//	}

		//	/* 返回true时不继续操作 */
		//	if (func(node))
		//	{
		//		return;
		//	}
		//}

		/* 方法二:标记法 */
		if (root == nullptr)
			return;
		
		/* 思想:栈模拟递归,为了解决遍历和操作的次序不一致,使用标记法,在要处理的结点后面放一个空 */
		std::stack<BTNode*> st;
		st.push(root);
		while (!st.empty())
		{
			auto node = st.top();
			if (node != nullptr)
			{
				/* 先将结点从栈中拿出,用来访问左右,之后再按顺序插入 */
				st.pop();

				/* 中左右,因此入栈顺序要是右左中 */
				if (node->m_right)
					st.push(node->m_right);

				if (node->m_left)
					st.push(node->m_left);

				/* 访问过,但是没处理,用空来标识 */
				st.push(node);
				st.push(nullptr);
			}
			else
			{
				/* 弹掉nullptr */
				st.pop();

				node = st.top();
				st.pop();

				func(node);
			}
		}
	}

中序遍历

	static void in_order(BTNode* root, std::function<bool(BTNode*)> func)
	{
		/* 方法一 */
		//std::stack<BTNode*> st;
		//BTNode* cur = root;
		//while (!st.empty() || cur)
		//{
		//	if (cur == nullptr)
		//	{
		//		/* 拿到根 */
		//		cur = st.top();
		//		st.pop();
		//		func(cur);
		//		cur = cur->m_right;
		//	}
		//	else
		//	{
		//		st.push(cur);
		//		cur = cur->m_left;
		//	}
		//}

		/* 方法二:标记法 */
		if (root == nullptr)
			return;

		std::stack<BTNode*> st;
		st.push(root);
		while (!st.empty())
		{
			auto node = st.top();
			if (node != nullptr)
			{
				/* 先将结点从栈中拿出,用来访问左右,之后再按顺序插入 */
				st.pop();

				/* 左中右,因此入栈顺序要是右中左 */
				if (node->m_right)
					st.push(node->m_right);

				/* 访问过,但是没处理,用空来标识 */
				st.push(node);
				st.push(nullptr);

				if (node->m_left)
					st.push(node->m_left);
			}
			else
			{
				st.pop();

				node = st.top();
				st.pop();

				func(node);
			}
		}
	}

后序遍历

	static void post_order(BTNode* root, std::function<bool(BTNode*)> func)
	{
		if (root == nullptr)
			return;

		std::stack<BTNode*> st;
		st.push(root);
		while (!st.empty())
		{
			auto node = st.top();
			if (node != nullptr)
			{
				/* 先将结点从栈中拿出,用来访问左右,之后再按顺序插入 */
				st.pop();

				/* 访问过,但是没处理,用空来标识 */
				st.push(node);
				st.push(nullptr);

				/* 左右中,因此入栈顺序要是中右左 */
				if (node->m_right)
					st.push(node->m_right);

				if (node->m_left)
					st.push(node->m_left);
			}
			else
			{
				st.pop();

				node = st.top();
				st.pop();

				func(node);
			}
		}
	}

层序遍历

	static void level_order(BTNode* root, std::function<bool(BTNode*)> func)
	{
		/* 
			广度优先遍历
			每次一个结点入队,要访问时出队,并且将该结点的左右结点入队
			因为访问和操作次序要一致,因此不能用栈,栈是模拟递归的
		*/

		if (root == nullptr)
			return;

		std::queue<BTNode*> q;
		q.push(root);
		BTNode* cur = root;

		while (!q.empty())
		{
			cur = q.front();
			q.pop();

			func(cur);

			if(cur->m_left)
				q.push(cur->m_left);

			if (cur->m_right)
				q.push(cur->m_right);
		}
	}

二分遍历

	static BTNode* binary_search(BTNode* root, const Type& _val)
	{
		BTNode* cur = root;
		BTNode* parent = nullptr;
		while (cur)
		{
			if (cur->m_val == _val)
				return cur;

			parent = cur;
			if (cur->m_val > _val)
			{
				cur = cur->m_left;
			}
			else if (cur->m_val < _val)
			{
				cur = cur->m_right;
			}
		}
		return nullptr;
	}

增删查改实现

public:
	using iterator = BTNode * ;
	
	binary_tree() : m_root(nullptr) {}
	explicit binary_tree(const Type& _val) : m_root(create_node(_val)) {}
	~binary_tree()
	{
		/* 遍历每一个结点逐一析构,return false表示不要提前结束 */
		pre_order(m_root, [](BTNode* node) {delete node; return false; });
	}

	void insert(const Type& _val)
	{
		BTNode* newNode = nullptr;
		try
		{
			newNode = create_node(_val);
		}
		catch (const std::exception& exp)
		{
			std::cout << exp.what() << std::endl;
			return;
		}
		
		/* 目前为空树 */
		if (m_root == nullptr)
		{
			m_root = newNode;
			return;
		}

		BTNode* cur = m_root;
		BTNode* parent = nullptr;
		while (cur)
		{
			if (cur->m_val == _val)
				return;

			parent = cur;
			if (cur->m_val > _val)
			{
				cur = cur->m_left;
			}
			else if (cur->m_val < _val)
			{
				cur = cur->m_right;
			}
		}
		cur = newNode;
		
		if (parent->m_val > _val)
			parent->m_left = cur;
		else
			parent->m_right = cur;
	}

	void erase(const Type& _val)
	{
		BTNode* cur = m_root;
		BTNode* parent = nullptr;
		while (cur)
		{
			if (cur->m_val == _val)
				break;

			parent = cur;
			if (cur->m_val > _val)
			{
				cur = cur->m_left;
			}
			else if (cur->m_val < _val)
			{
				cur = cur->m_right;
			}
		}

		if (cur == nullptr)
			return;

		/* 分三种情况:左为空,右为空,都为空 */
		BTNode* left = cur->m_left;
		BTNode* right = cur->m_right;
		if (left == nullptr)
		{
			if (cur == m_root)
			{
				m_root = cur->m_right;
			}
			if (parent->m_right == cur)
			{
				parent->m_right = cur->m_right;
			}
			else
			{
				parent->m_left = cur->m_right;
			}
			delete cur;
		}
		else if (right == nullptr)
		{
			if (cur == m_root)
			{
				m_root = cur->m_left;
			}
			if (parent->m_right == cur)
			{
				parent->m_right = cur->m_left;
			}
			else
			{
				parent->m_left = cur->m_left;
			}
			delete cur;
		}
		else
		{
			/* 
				左右都不为空,要找左树的最大结点或右树的最小结点
				比如找到左树的最大结点后,交换该结点和cur的值,之后重新链接,最后删除该最大结点
			*/
			/* 要记录目标结点的父结点,因为要判断目标结点是父结点的左还是右,才能正确链接 */
			auto minLeft = cur->m_left;
			auto minLeftParent = cur;

			while (minLeft->m_right)
			{
				minLeftParent = minLeft;
				minLeft = minLeft->m_right;
			}

			cur->m_val = minLeft->m_val;
			
			if (minLeftParent->m_left == minLeft)
			{
				minLeftParent->m_left = minLeft->m_left;
			}
			else
			{
				minLeftParent->m_right = minLeft->m_right;
			}
			delete minLeft;
		}
	}

	iterator find(const Type& _val)
	{
		return binary_search(m_root, _val);
	}

	iterator begin()
	{
		return m_root;
	}	

private:
	BTNode* m_root;
};

运算符重载

/* 重载流提取 */
friend std::ostream& operator<<(std::ostream& out, binary_tree& tree)
{
	auto print = [](BTNode* node) -> bool
	{
		std::cout << node->m_val << ' ';
		return false;
	};

	std::cout << "前序遍历:";
	tree.pre_order(tree.m_root, print);
	std::cout << std::endl;

	std::cout << "中序遍历:";
	tree.in_order(tree.m_root, print);
	std::cout << std::endl;

	std::cout << "后序遍历:";
	tree.post_order(tree.m_root, print);
	std::cout << std::endl;

	std::cout << "层序遍历:";
	tree.level_order(tree.m_root, print);
	std::cout << std::endl;

	return out;
}

测试代码

void tree_test()
{
	binary_tree<int> tree;
	std::vector<int> arr{ 8,3,10,1,6,14,4,7,13,0,16 };
	/*
		8 3 1 0 6 4 7 10 14 13 16

		0 1 3 4 6 7 8 10 13 14 16

		0 1 4 7 6 3 13 16 14 10 8
		
		8 3 10 1 6 14 0 4 7 13 16
	*/
	for (auto& num : arr)
	{
		tree.insert(num);
	}
	
	std::cout << tree << std::endl;

	tree.erase(6);
	std::cout << tree << std::endl;

	if (tree.find(8))
	{
		std::cout << "找到了\n";
	}

	tree.erase(8);

	if (tree.find(8))
	{
		std::cout << "找到了\n";
	}
	std::cout << tree << std::endl;
}

但是二叉搜索树有一个缺点,在遇到插入的值都是比树中所有值都大的结点时,会一直往右插入,从而形成一个单链表。因而失去了二叉树的优势。平衡二叉树(AVL树)可以解决这个问题。

更多推荐

LeetCode 753. 破解保险箱【欧拉回路,DFS】困难

本文属于「征服LeetCode」系列文章之一,这一系列正式开始于2021/08/12。由于LeetCode上部分题目有锁,本系列将至少持续到刷完所有无锁题之日为止;由于LeetCode还在不断地创建新题,本系列的终止日期可能是永远。在这一系列刷题文章中,我不仅会讲解多种解题思路及其优化,还会用多种编程语言实现题解,涉及

优化系统报错提示信息,提高人机交互(三)

对于业务比较复杂的接口,可能存在方法嵌套,每个方法都可能会报错,出现异常,那么需要把异常信息返回给接口调用者,如何实现呢?(1)捕获异常进行处理,不返回controller代码@AllArgsConstructor@RestController@Slf4jpublicclassDemoController{privat

Llama.cpp工具main使用手册

Llama.cpp提供的main工具允许你以简单有效的方式使用各种LLaMA语言模型。它专门设计用于与llama.cpp项目配合使用。推荐:用NSDT编辑器快速搭建可编程3D场景Llama.cpp的工具main提供简单的C/C++实现,具有可选的4位量化支持,可实现更快、更低的内存推理,并针对桌面CPU进行了优化。该程

【C++】构造函数初始化列表 ④ ( 构造函数 和 析构函数 调用顺序分析 )

文章目录一、构造函数和析构函数调用顺序说明1、构造函数调用顺序2、析构函数调用顺序3、拷贝构造函数也可以定义初始化列表二、构造函数和析构函数调用顺序代码分析1、构造函数调用顺序2、代码示例-构造/析构函数调用顺序分析构造函数初始化列表总结:初始化列表可以为类的成员变量提供初始值;初始化列表可以调用类的成员变量类型的构造

SpringBoot之静态资源规则与定制化

文章目录前言一、静态资源访问二、静态资源访问前缀三、webjar资源处理的默认规则四、welcome与favicon功能1.欢迎页支持欢迎页处理规则2.自定义Favicon五、补充总结前言本文主要介绍关于SpringBoot中Web开发的简单功能。一、静态资源访问只要静态资源放在类路径下:called/static(o

linux ssh 禁止指定用户通过ssh登录

Linux禁止用户或IP通过SSH登录限制用户SSH登录1.只允许指定用户进行登录(白名单):在/etc/ssh/sshd_config配置文件中设置AllowUsers选项,(配置完成需要重启SSHD服务)格式如下:AllowUsersaliyuntest@192.168.1.1#允许aliyun和从192.168.

洞察2023:中国心室辅助装置行业竞争格局及市场份额

本文核心数据:代表性企业排名;代表性企业优势分析等1、中国心室辅助装置行业竞争梯队人工心脏(ArtificialHeart,AH)是机械辅助类器械的代表,用于替代或辅助心脏泵血功能。按照功能可分为心室辅助装置(VentricularAssistDevice,VAD)、全人工心脏(TotalArtificialHeart

Linux系统之安装uptime-kuma服务器监控面板

Linux系统之安装uptime-kuma服务器监控面板一、uptime-kuma介绍1.1uptime-kuma简介1.2uptime-kuma特点二、本次实践环境介绍2.1环境规划2.2本次实践介绍2.3环境要求三、检查本地环境3.1检查本地操作系统版本3.2检查系统内核版本3.3检查系统是否安装Node.js四、

【计算机网络】IP协议第二讲(Mac帧、IP地址、碰撞检测、ARP协议介绍)

IP协议第二讲1.IP和Mac帧2.碰撞检测2.1介绍2.2如何减少碰撞发生2.3MTU2.4一些补充3.ARP协议3.1协议介绍3.2报文格式分析1.IP和Mac帧IP(InternetProtocol)和MAC(MediaAccessControl)帧是计算机网络中两个不同层次的概念,它们在网络通信中扮演不同的角色

sed简单使用

sed(StreamEditor)流编辑器,对标准输出或文件逐行进行处理语法格式第一种形式:stdout|sed[option]"patterncommand"第二种形式:sed[option]"patterncommand"filesed的选项选项含义-n只打印模式匹配行-e直接在命令行进行sed编辑,默认选项-f编

质数距离(C++筛素数模板题)

给定两个整数L和U,你需要在闭区间[L,U]内找到距离最接近的两个相邻质数C1和C2(即C2−C1是最小的),如果存在相同距离的其他相邻质数对,则输出第一对。同时,你还需要找到距离最远的两个相邻质数D1和D2(即D1−D2是最大的),如果存在相同距离的其他相邻质数对,则输出第一对。输入格式每行输入两个整数L和U,其中L

热文推荐