首页 > 编程语言 >C++——二叉树(进阶)

C++——二叉树(进阶)

时间:2024-11-03 16:44:54浏览次数:3  
标签:Node return 进阶 nullptr C++ 二叉树 key root cur

1.二叉搜索树

1.1概念

二叉搜索树又称 二叉排序树,它或是 一棵空树,又或是具有 以下性质的二叉树:
若它的左子树不为空,则左子树上所有节点的值都小于根节点的值 若它的右子树不为空,则右子树上所有节点的值都大于根节点的值 它的左右子树也分别为二叉搜索树
可以观察到,对这棵树进行 中序遍历的话,可以得到一串升序的数据 如果左子树放大的数据,右子树放小的数据,那么中序遍历就是降序了

1.2操作

1.2.1二叉搜索树的查找

一、从根开始比较,查找,比根大则往右边走查找,比根小则往左边走查找。 二、最多查找高度次,如果走到空,还没找到,则这个值不存在。

1.2.2二叉搜索树的插入

一、树为空树,则直接新增节点,赋值给root指针 二、树不为空树,按二叉搜索树的性质查找插入位置,插入新节点

1.2.3二叉搜索树的删除

首先要查找所删除的元素 是否在二叉搜索树中,如果不存在,则返回 如果存在,则要删除的结点可以分为 下面四种情况
一、要删除的结点无孩子结点 二、要删除的结点只有左孩子结点 三、要删除的结点只有右孩子结点 四、要删除的结点有左、右孩子结点 这种情况是最难处理的,因为它不能直接托孤了,待删除的结点有两个孩子结点 此时最好使用 替换法,找一个可以轻松脱身的结点,与待删除结点交换,然后再删除节点
综上,待删除结点有4种情况,但实际上,情况一可以与情况二或者三合并起来,因为叶子节点可以当作 左为空,也可以当作 右为空,所以真正的删除过程如下:
情况二:直接删除该结点,使被删除节点的 双亲结点指向被删除结点的左孩子结点 情况三:直接删除该结点,使被删除节点的 双亲结点指向被删除结点的右孩子结点 情况四:在它的右子树中寻找中序下的第一个结点(关键码最小),用它的值填补到被删除结点中,再来处理该结点的删除问题,使用替换法删除结点

1.3实现

1.3.1树的结点

template<class K>
struct BSTreeNode
{
	BSTreeNode<K>* _left;
	BSTreeNode<K>* _right;
	K _key;

	BSTreeNode(const K& key)
		:_left(nullptr)
		,_right(nullptr)
		,_key(key)
	{}
};

这里一般不使用T,而使用K,因为这个数据结构的值要参与数据比较,一般把这个值叫做关键字 key

1.3.2插入

//BinarySearchTree.h

#pragma once
#include<iostream>
using namespace std;

//这里不写命名空间是因为,所写的东西与库中的命名不冲突


template<class K>
struct BSTreeNode
{
	BSTreeNode<K>* _left;
	BSTreeNode<K>* _right;
	K _key;

	BSTreeNode(const K& key)
		:_left(nullptr)
		,_right(nullptr)
		,_key(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* parent = nullptr;
		Node* cur = _root;  //cur是一个局部变量


		//cur走到空的位置就可以完成插入了
		while (cur)
		{
			parent = cur;

			if (key > cur->_key)//插入的值比cur指向的_key大
			{
				cur = cur->_right;
			}
			else if (key < cur->_key)
			{
				cur = cur->_left;
			}


			//默认情况下,搜索二叉数是不允许有重复值的
			else
			{
				return false;
				//所以如果插入的值在树中已经存在了
				//就return false
			}
		}

		cur = new Node(key);

		//判断cur是连接 在parent的左边还是右边
		if (key > parent->_key)
		{
			parent->_right = cur;
		}

		else
		{
			parent->_left = cur;
		}

		return true;
	}


private:
	Node* _root = nullptr;
};
1.3.2.1测试
//Test.cpp

#include"BinarySearchTree.h"

int main()
{
	int a[] = { 8, 3, 1, 10, 6, 4, 7, 14, 13 };
	BSTree<int> bt;
	for (auto e : a)
	{
		bt.Insert(e);
	}

	return 0;
}

1.3.3中序遍历

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

public:

	void InOrder(Node* root)//中序遍历
	{
		if (root == nullptr)
			return;

		InOrder(root->_left);
		cout << root->_key << " ";
		InOrder(root->_right);
	}

private:
	Node* _root = nullptr;
};
1.3.3.1测试时的问题
int main()
{
	int a[] = { 8, 3, 1, 10, 6, 4, 7, 14, 13 };
	BSTree<int> bt;
	for (auto e : a)
	{
		bt.Insert(e);
	}

	bt.InOrder();
	//调用中序遍历要传递根结点,但这里不容易拿到
	//二叉树的递归需要有参数

	return 0;
}
1.3.3.2解决方式一

写一个GetNode函数,把_root返回

1.3.3.3解决方式二

改写中序遍历的实现方式

C++中通常不使用方式一

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

public:

	void _InOrder(Node* root)//中序遍历
	{
		if (root == nullptr)
			return;

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

	//类外面拿不到根结点,但是类里面可以使用
	void InOrder()
	{
		_InOrder(_root);
	}

private:
	Node* _root = nullptr;
};

//测试成功
int main()
{
	int a[] = { 8, 3, 1, 10, 6, 4, 7, 14, 13 };
	BSTree<int> bt;
	for (auto e : a)
	{
		bt.Insert(e);
	}

	bt.InOrder();

	return 0;
}

C++中的递归通常都是这样去写的

1.3.4查找

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

public:

	bool Find(const K& key)
	{
		Node* cur = _root;
		while (cur)
		{
			if (key > _root->_key)
			{
				cur = cur->_right;
			}

			else if (key < _root->_key)
			{
				cur = cur->_left;
			}

			else
			{
				return true;
			}

		}

		return false;
	}

private:
	Node* _root = nullptr;
};

1.3.5删除

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

public:

	bool Erase(const K& key)
	{
		//首先找到待删除的结点,同时要找到它的父亲
		Node* parent = nullptr;
		Node* cur = _root;

		while (cur)
		{//查找的过程

			if (key > cur->_key)
			{
				parent = cur;
				cur = cur->_right;
			}

			else if (key < cur->_key)
			{
				parent = cur;
				cur = cur->_left;
			}

			//找到了待删除结点:cur
			else
			{
				//开始删除

				//1.情况三:左为空
				if (cur->_left == nullptr)
				{//先判断待删除节点是不是根节点
					if (cur == _root)
					{
						_root = cur->_right;
					}
					
					else
					{//判断父亲的哪个指针指向cur
						if (parent->_left == cur)
						{
							parent->_left = cur->_right;
						}
						else
						{
							parent->_right = cur->_right;
						}
					}

				}

				//2.情况二:右为空
				else if (cur->_right == nullptr)
				{
					if (cur == _root)
					{
						_root = cur->_left;
					}

					else
					{//判断父亲的哪个指针指向cur
						if (parent->_left == cur)
						{
							parent->_left = cur->_left;
						}
						else
						{
							parent->_right = cur->_left;
						}
					}
				}

				//3.情况四:左、右都不为空
				else 
				{//默认找右树的最小节点来替换
					//Node* parent = nullptr;
					Node* parent = cur;
					Node* subLeft = cur->_right;
					while (subLeft->_left)
					{
						parent = subLeft;
						subLeft = subLeft->_left;
					}
					
					swap(cur->_key, subLeft->_key);

					//替换完之后,就要删除节点了
					//注意:这里不能去复用函数来删除最左节点
					//因为此时它已经不是二叉搜索树了,很可能根本就找不到最左节点
					
					if (parent->_left == subLeft)
					{
						parent->_left = subLeft->_right;
					}

					else
					{
						parent->_right = subLeft->_right;
					}
				}
				return true;
			}//找到了待删除结点:cur

			
		}//while结尾

		//没有找到
		return false;
	}

private:
	Node* _root = nullptr;
};


int main()
{
	int a[] = { 8, 3, 1, 10, 6, 4, 7, 14, 13 };
	BSTree<int> bt;
	for (auto e : a)
	{
		bt.Insert(e);
	}

	bt.InOrder();
	
	bt.Erase(14);
	bt.InOrder();

	bt.Erase(3);
	bt.InOrder();

	bt.Erase(8);
	bt.InOrder();

	for (auto e : a)
	{
		bt.Erase(e);
		bt.InOrder();
	}

	return 0;
}
1.3.5.1注意事项

一、

二、

我们已经实现了二叉搜索树的增、删、查,那么二叉搜索树的修改,也需要实现吗?

二叉搜索树是不能随意修改的,因为修改之后,就很难保证这棵树依旧是二叉搜索树了。

1.4递归实现二叉搜索树

二叉搜索树本身是一个递归结构,但当前的实现,我们使用的是循环。

实际上还可以写一个递归版本的二叉搜索树

1.4.1查找

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

public:

	bool FindR(const K& key)
	{
		return _FindR(_root,key);
	}
private:
//要实现递归,都要在里面多套一层

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

		//比当前的值大,就转换成在当前节点的右子树去搜索
		if (key > root->_key)
		{
			return _FindR(root->_right, key);
		}

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

		else
		{
			return true;
		}
	}

private:
	Node* _root = nullptr;
};

1.4.2插入

1.4.2.1方式一
template<class K>
class BSTree
{
	typedef BSTreeNode<K> Node;

public:

	bool InsertR(const K& key)
	{
		if (_root == nullptr)
		{
			_root = new Node(key);
			return true;
		}
		return _InsertR(_root, key,nullptr);
	}
private:

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

		if (key > root->_key)
		{
			return _InsertR(root->_right, key,root);
		}

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

		else
		{
			return false;
		}
	}

private:
	Node* _root = nullptr;
};
1.4.2.1方式二:巧妙地使用引用
template<class K>
class BSTree
{
	typedef BSTreeNode<K> Node;

public:

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

private:

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

		if (key > root->_key)
		{
			return _InsertR(root->_right, key);
		}

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

		else
		{
			return false;
		}
	}

private:
	Node* _root = nullptr;
};

既然递归实现时,可以巧妙地使用引用来达成目的。

那么之前的循环实现的插入函数是不是也可以使用引用呢?

答案是不可以的。C++的引用不能改变指向

比如起初是A的引用,那么之后就不能再变成B的引用、C的引用了,

    int a = 0;
	int& b = a;//给a取别名为b
	int& c = a;
	int& d = b;//给别名取别名
 
	int x = 1;
	b = x;//这里是赋值
    //b已经是a的引用了,就不能再更改了

递归时可以更改是因为,虽然都叫做root,但是每个栈帧中都是一个新的引用,不同作用域可以定义同名变量,每次都定义了一个新的引用,只是说前几次的引用没有起到作用,最后一次的引用才起到了作用。

1.4.3删除

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

public:

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

private:

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

		//比它大,转换成在右树删除
		if (key > root->_key)
		{
			return _EraseR(root->_right, key);
		}

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

		else
		{//找到了节点,开始删除
			if (root->_left == nullptr)
			{
				//使用引用传递参数,写起来就会很舒服
				//不必再去判断是父亲的左还是右
				Node* del = root;
				root = root->_right;
				delete del;

				return true;
			}

			else if (root->_right == nullptr)
			{
				Node* del = root;
				root = root->_left;
				delete del;

				return true;
			}

			else//左右都不为空
			{
				//这里还要去找替代节点
				Node* subLeft = root->_right;
				while (subLeft->_left)
				{
					subLeft = subLeft->_left;
				}

				swap(root->_key, subLeft->_key);

				//转换成在子树中,去递归删除
				//在子树中,删除的节点一定 不是 左右都不为空的节点
				return _EraseR(root->_right,key);
			}
		}

	}

private:
	Node* _root = nullptr;
};

画出递归展开图,有助于理解

1.5完善搜索树的实现

1.5.1析构函数

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

public:

	//不可能说对析构函数递归
	//因为递归一定有一个特点:递归一定要控制参数
	//由参数的变化来控制这棵树
	~BSTree()
	{
		Destroy(_root);
	}
	//不写析构释放,是会产生内存泄露的,但是不会报错

private:

	void Destroy(Node*& root)//注意auto是不能作为参数的
	{
		if (root == nullptr)
		{
			return;
		}

		//后序遍历
		Destroy(root->_left);
		Destroy(root->_right);
		delete root;

		root = nullptr;
		//直接置空是无用的,这里不影响实参
		//之前的实现,通常使用二级指针
		//但现在更好的办法是使用引用传参
	}

private:
	Node* _root = nullptr;
};

Destroy函数的参数换成Node& root可以吗?

~BSTree()
{
	Destroy(*_root);
}
//如果_root是空呢?所以这样实现纯纯是自找麻烦

void Destroy(Node& root)
{
}

1.5.2拷贝构造

int main()
{
	int a[] = { 8, 3, 1, 10, 6, 4, 7, 14, 13 };
	BSTree<int> bt;
	for (auto e : a)
	{
		bt.InsertR(e);
	}

	bt.InOrder();

	BSTree<int> copy(bt);
	//没有写拷贝构造,这里就是浅拷贝,会崩溃
	copy.InOrder();

	return 0;
}

如何书写呢?

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

public:
	BSTree(const BSTree<K>& t)
	{
		//t._root = Copy(_root);
		_root = Copy(t._root);
	}

private:

	Node* Copy(Node* root)
	{
		if (root == nullptr)
		{
			return nullptr;
		}

		//前序遍历
		Node* newRoot = new Node(root->_key);
		newRoot->_left = Copy(root->_left);
		newRoot->_right = Copy(root->_right);

		return newRoot;
	}

private:
	Node* _root = nullptr;
};

标签:Node,return,进阶,nullptr,C++,二叉树,key,root,cur
From: https://blog.csdn.net/2301_80342122/article/details/143430815

相关文章

  • 老鹰抓小鸡C++
    题目描述热热和乎乎等n位小朋友玩老鹰抓小鸡的游戏,狐狸老师当老鹰,排在第一位的小朋友当“母鸡”,其他4小朋友当“小鸡”。但是“母鸡”很辛苦,所以过一段时间“母鸡”需要排到队伍最后成为“小鸡”,让第二位小朋友当“母鸡”······试编一程序,模拟k次位置变化的过程。初始位......
  • 有序数列合并C++
    将两个数列合并为一个数列,并从小到大排序题目描述输入两个升序排列的序列,将两个序列合并为一个有序序列并输出。输入描述输入包含三行,第一行包含两个正整数n,m(1≤n,m≤100),用空格分隔。n表示第二行第一个升序序列中数字的个数,m表示第三行第二个升序序列中数字的个数。......
  • C++ STL常用容器之set
    文章目录一、集合set二、所需的头文件三、基本访问操作3.1插入元素3.2删除元素3.3查找元素3.4其他函数四、无序集合unordered_set五、multiset六、unordered_multiset七、使用set容器八、map与set的区别一、集合setset称为集合,是一个内部自动有序且不含重复元素......
  • c++11(下篇)
    提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档文章目录前言一、可变参数模板4.1基本语法及原理4.2包扩展4.3empalce系列接⼝二、lambda2.1.仿函数2.2.lambda表达式语法2.3.捕捉列表2.3lambda原理三.新的类功能3.1默认的移动构造和移动赋值3.2......
  • 学生管理系统(C++)
    #include<iostream>#include<vector>#include<fstream>#include<string>#include<sstream>#include<algorithm>usingnamespacestd;structStudent{std::stringname;intid;intage;......
  • 【NOI】C++函数入门二(自定义函数)
    文章目录前言一、概念1.导入1.1首先什么是函数呢?2.函数分类3.为什么要定义函数呢?4.函数结构5.函数使用注意事项二、例题讲解问题:1137-纯粹素数问题:1258-求一个三位数问题:1140-亲密数对问题:1149-回文数个数三、总结四、感谢前言在这一章节中,我们将深入探......
  • C++学习笔记
    一、从C转向C++1.1、使用const和inline而不#define使用constconstintm=10;intn=m;上述代码在C中,编译器会先到m的内存中取出数据,再赋值给n;但在C++中,会直接将10赋值给n。C++的常量更类似于#define,是一个替换过程。#define经常不被认为是语言的一部分。define本......
  • 华为OD机试-E卷,100分 - 最小的调整次数特异性双端队列Java & Python& JS & C++ & C
    最新华为OD机试题目描述有一个特异性的双端队列,该队列可以从头部或尾部添加数据,但是只能从头部移出数据。小A依次执行2n个指令往队列中添加数据和移出数据。其中n个指令是添加数据(可能从头部添加、也可能从尾部添加),依次添加1到n;n个指令是移出数据。现在要求移除数据的顺......
  • 华为OD机试-E卷100分 -货币单位换算Java & Python& JS & C++ & C
    最新华为OD机试题目描述记账本上记录了若干条多国货币金额,需要转换成人民币分(fen),汇总后输出。每行记录一条金额,金额带有货币单位,格式为数字+单位,可能是单独元,或者单独分,或者元与分的组合。要求将这些货币全部换算成人民币分(fen)后进行汇总,汇总结果仅保留整数,小数部分舍弃......