搜索二叉树实现和讲解包含递归和非递归写法!(超详细解读+源代码)

介绍

二叉搜索树又称二叉排序树,它或者是一棵空树,或者是具有以下性质的二叉树:
若它的左子树不为空,则左子树上所有节点的值都小于根节点的值
若它的右子树不为空,则右子树上所有节点的值都大于根节点的值
它的左右子树也分别为二叉搜索树
图1二叉搜索树示例

基本操作实现

构造函数

普通构造函数用编译期生成的就够用,我们主要实现拷贝构造函数,方便后续开发。需要注意的是:当我们自主实现一个构造函数,无论实现的是拷贝构造函数还是普通构造函数,编译器都不会帮我们再去默认生成一个构造函数(编译器生成的默认构造函数只是针对自定义类型进行初始化处理,对于自定义类型需要自己实现构造函数),此时如果我们还需要用编译器默认生成的构造函数就需要用到关键字== default == ,这是c+11之后版本才支持的。

BSTree() = default;//默认构造函数
	//拷贝构造函数
	BSTree(const BSTree<T>&t)
	{
		_proot = CopyTree(t._proot);
	}

	// 拷贝构造函数 现代写法
	BSTree<T>& operator=(BSTree<T> t)
	{
		swap(_proot, t._proot);
		return *this;
	}

析构函数

析构函数就是释放资源 直接调用Destroy

~BSTree()
	{
		Destroy_Tree(_proot);
		_proot = nullptr;
	} 

插入函数

插入有两种实现方法,分别是递归写法和非递归写法,下面想详细介绍两种写法的实现。

非递归写法

实现逻辑图:

1.插入的树为空树:直接插入
图2空树插入节点-非递归
代码实现:

if (_proot == nullptr)
		{
			_proot = new Node(data);
			return true;
		}

2.非空树插入:
以之前给的示例为例,要插入的是10;
图3 非空插入节点--非递归
在非空树中,我们首先要找到插入的位置,根据二叉搜索树的性质,比根大的在右边,比根小的在左边,进行查找操作。代码实现如下:

		pNode cur = _proot;
		pNode parent =nullptr;
		//找到要插入的位置
		while (cur)
		{
			if (cur->_data<data)
			{
				parent = cur;
				cur = cur->_pright;
			}
			else if (cur->_data>data)
			{
				parent = cur;
				cur = cur->_pleft;
			}
			else
				return false;
		}

循环结束后,cur的位置就是待插入的位置,注意在此版本实现中规定不允许有相同的结点出现在二叉树,如有特殊需求可以对代码判断进行相应修改。
找到插入位置后就是进行插入操作,代码如下:

cur = new Node(data);
		if (parent->_data<data)
		{
			parent->_pright = cur;
			
		}
		else 
		{
			parent->_pleft = cur;
		}
		return true;

判断data插入哪个位置,是根节点的左边还是右边,而后插入!

递归写法

思路:传根的引用作为参数

逻辑实现

1.空树,直接插入
图4 空树插入--递归写法
2.非空插入:
图5 非空插入函数--递归写法
注意这里,在找到插入位置后,此时的root是一个空结点,同时root也是上一个结点(父节点12)的右结点,直接把data的值构造成结点就能实现插入。不需要像非递归版本一样创建一个父节点手动链接,因为插入结点就是父节点的子节点的引用,本身就是链接在一起的,只需赋值就好!

插入函数总结

无论是非递归写法还是递归写法,都各有优点,逻辑实现上,非递归的写法更让人容易理解和接受,而递归写法就比较巧妙用了引用传参,更加简洁实现。

删除函数

非递归写法

二叉搜索树的删除
首先查找元素是否在二叉搜索树中,如果不存在,则返回;
代码实现:


		//如果树是空 删除失败
		if (_proot == nullptr)
		{
			return false;
		}
		//先找点
		Node* pcur = _proot;
		Node* parent = nullptr;
		while (pcur)
		{
		if (data < pcur->_data)
			{
				parent = pcur;
				pcur = pcur->_pleft;
			}
			else if(data>pcur->_data)
			{
				parent = pcur;
				pcur = pcur->_pright;
			}
			else//找到了

否则要删除的结点可能分下面四种情况:
a. 要删除的结点无孩子结点
b. 要删除的结点只有左孩子结点
c. 要删除的结点只有右孩子结点
d. 要删除的结点有左、右孩子结点
看起来有待删除节点有4中情况,实际情况a可以与情况b或者c合并起来,因为bc清空都适用于a因此真正的删除过程如下:
情况b:删除该结点且使被删除节点的双亲结点指向被删除节点的左孩子结点
图6 左节点为空
代码实现:

//第一种情况 pcur节点左节点或者右节点==nullptr 这种情况也适用于pcur为叶子节点的情况
if (pcur->_pleft == nullptr)
{ 
	
	if (pcur == _proot)//如果删除的是根节点 
	{
		_proot = pcur->_pright;
	}
	else
	{
		if (pcur == parent->_pleft)//父亲左节点的左结点为空 父左结点指向子右结点
		{
			parent->_pleft = pcur->_pright;
		}
		else //父亲的右结点的左节点为空 父右指向子右
		{
			parent->_pright = pcur->_pright;
		}
	}
	delete pcur;

		}
		else if(pcur->_pright==nullptr)
		{
			if (pcur == _proot) //删除根节点
			{
				_proot = pcur->_pleft;
			}
			else
			{
				if (pcur == parent->_pright)//pcur在父节点的右边 
				{
					parent->_pright = pcur->_pleft;
				}
				else
				{
					parent->_pleft = pcur->_pleft;
				}
			}
			delete pcur;
		
		}

情况c:删除该结点且使被删除节点的双亲结点指向被删除结点的右孩子结点
情况d:找一个替代结点 可以选择**左子树中最大结点或者右子树中最小结点,**来替换以保证搜索二叉树的基本结构不变! 这里我采用左子树中最大结点来替换

	else 
	{
		pNode ret_parent = pcur;
		pNode ret = pcur->_pleft;//左子树最大值节点替换
		while (ret->_pright)
		{
			ret_parent = ret;
			ret = ret->_pright;
		}
		swap(pcur->_data, ret->_data);
		if (ret_parent->_pleft == ret)
		{
			ret_parent->_pleft = ret->_pleft;
		}
		else
		{
			ret_parent->_pright = ret->_pright;
		}
		delete ret;
	}

递归写法

递归写法核心还是用引用传参

bool _Rec_Erase(pNode& root,const T& data)
	{
		if (root == nullptr)
		{
			return false;
		}
		if (root->_data > data)
		{
			return _Rec_Erase(root->_pleft, data);
		}
		else if (root->_data < data)
		{
			return _Rec_Erase(root->_pright, data);
		}
		else// 找到了待删节点
		{
			pNode del = root;//用一个节点记录待删节点地址,否则后续操作会出现内存泄漏
				//只有右边节点
				if (root->_pleft == nullptr)
				{
					root = root->_pright;
				}
				//只有右边节点
				else if (root->_pright == nullptr)
				{
					root = root->_pleft;
				}
				//两边都有节点 用替代法 这里用左节点最大值
				else
				{
					Node* maxLeft = root->_pleft;
					while (maxLeft->_pright)
					{
						maxLeft = maxLeft->_pright;
					}
					swap(root->_data, maxLeft->_data);
					//交换完成之后递归调用删除函数,注意这里可以递归
					//前面非递归写法不可以递归的原因 前面的函数交换完成之后
					//根节点是整个树的根节点,而交换之后树的结构被破环了,
					//根本删除不到交换后的节点,而这里给的是交换完成后,子树的根做为参数
					//树的结构没有被破坏
					return _Rec_Erase(root->_pleft,data);
				}
			delete del; //释放资源	
			return true;
		}

总结

插入和删除操作都必须先查找,查找效率代表了二叉搜索树中各个操作的性能。
对有n个结点的二叉搜索树,若每个元素查找的概率相等,则二叉搜索树平均查找长度是结点在二叉搜索树的
深度的函数,即结点越深,则比较次数越多。
但对于同一个关键码集合,如果各关键码插入的次序不同,可能得到不同结构的二叉搜索树:
图7不同结构的二叉搜索树
赋值度分析
源码:
BsTree.h


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

//搜索二叉树
template<class T>
struct BSTreeNode
{
	//构造函数   初始化列表
	BSTreeNode(const T& data = T())
		:_pright(nullptr), _pleft(nullptr), _data(data)
	{}
	BSTreeNode<T>* _pright;
	BSTreeNode<T>* _pleft;
	T _data;
};
template <class T>
class BSTree
{
	typedef BSTreeNode<T>Node; //封装一下结点
	typedef Node* pNode;//封装结点指针
public:

	//插入函数 非递归版本
	bool Insert(const T& data)
	{
		if (_proot == nullptr)
		{
			_proot = new Node(data);
			return true;
		}
		pNode cur = _proot;
		pNode parent = nullptr;
		//找到要插入的位置
		while (cur)
		{
			if (cur->_data < data)
			{
				parent = cur;
				cur = cur->_pright;
			}
			else if (cur->_data > data)
			{
				parent = cur;
				cur = cur->_pleft;
			}
			else
				return false;
		}
		cur = new Node(data);
		if (parent->_data < data)
		{
			parent->_pright = cur;

		}
		else
		{
			parent->_pleft = cur;
		}
		return true;
	}
	//插入函数的递归版本
	bool Rec_Insert(const T& data)
	{
		return _Rec_Insert(_proot, data);
	}

	//删除函数 非递归版本
	bool Erase(const T& data)
	{
		//如果树是空 删除失败
		if (_proot == nullptr)
		{
			return false;
		}
		//先找点
		Node* pcur = _proot;
		Node* parent = nullptr;
		while (pcur)
		{
			if (data < pcur->_data)
			{
				parent = pcur;
				pcur = pcur->_pleft;
			}
			else if (data > pcur->_data)
			{
				parent = pcur;
				pcur = pcur->_pright;
			}
			else//找到了
			{
				//第一种情况 pcur节点左节点或者右节点==nullptr 这种情况也适用于pcur为叶子节点的情况
				if (pcur->_pleft == nullptr)
				{

					if (pcur == _proot)//如果删除的是根节点 
					{
						_proot = pcur->_pright;
					}
					else
					{
						if (pcur == parent->_pleft)//父亲左节点的左结点为空 父左结点指向子右结点
						{
							parent->_pleft = pcur->_pright;
						}
						else //父亲的右结点的左节点为空 父右指向子右
						{
							parent->_pright = pcur->_pright;
						}
					}
					delete pcur;

				}
				else if (pcur->_pright == nullptr)
				{
					if (pcur == _proot) //删除根节点
					{
						_proot = pcur->_pleft;
					}
					else
					{
						if (pcur == parent->_pright)//pcur在父节点的右边 
						{
							parent->_pright = pcur->_pleft;
						}
						else
						{
							parent->_pleft = pcur->_pleft;
						}
					}
					delete pcur;

				}

				//第二种情况:当前节点左右孩子都存在,
				//直接删除不好删除,可以在其子树中找一个替代结点
				else
				{
					pNode ret_parent = pcur;
					pNode ret = pcur->_pleft;//左子树最大值节点替换
					while (ret->_pright)
					{
						ret_parent = ret;
						ret = ret->_pright;
					}
					swap(pcur->_data, ret->_data);
					if (ret_parent->_pleft == ret)
					{
						ret_parent->_pleft = ret->_pleft;
					}
					else
					{
						ret_parent->_pright = ret->_pright;
					}
					delete ret;
				}
				return true;
			}
		}
		return false;

	}
	//删除函数 递归版本
	bool Rec_Erase(const T& data)
	{
		return _Rec_Erase(_proot, data);

	}
	//清空二叉树
	void Destroy_Tree(pNode root)
	{
		if (root == nullptr)
			return;
		Destroy_Tree(root->_pleft);
		Destroy_Tree(root->_pright);
		delete root;
	}
	//查找是否存在
	bool Find(const T& data)
	{
		pNode cur = _proot;
		while (cur)
		{
			if (cur->_data < data)
			{
				cur = cur->_pright;
			}
			else if (cur->_data > data)
			{
				cur = cur->_pleft;
			}
			else
			{
				return true;
			}
		}
		return false;
	}
	//析构函数
	~BSTree()
	{
		Destroy_Tree(_proot);
		_proot = nullptr;
	}
	//构造函数注释
	/* 强制编译器自己生成构造 默认情况下,当代码中出现一个构造函数的时候
	 无论是拷贝构造还是普通的构造函数,编译器都不会生成默认的构造函数了
	如果还要使用默认的构造函数(只针对内置类型有效),就要用default关键字
	 注意的是 default关键字是C++11版本之后的才有*/
	BSTree() = default;
	//拷贝构造函数
	BSTree(const BSTree<T>& t)
	{
		_proot = CopyTree(t._proot);
	}

	// 拷贝构造函数 现代写法
	BSTree<T>& operator=(BSTree<T> t)
	{
		swap(_proot, t._proot);
		return *this;
	}
	//中序遍历 要传根节点参数 封装一下 
	void InOrder()
	{
		_InOrder(_proot);
		cout << endl;
	}

private:
	pNode _proot;
	//中序打印函数
	void _InOrder(pNode root)
	{
		if (root == nullptr)
			return;

		_InOrder(root->_pleft);
		cout << root->_data << " ";

		_InOrder(root->_pright);
	}
	//递归版本的插入函数 
	bool _Rec_Insert(pNode& proot, const T& data)
	{
		if (proot == nullptr)//找到插入位置 
		{
			proot = new Node(data);
			return true;
		}
		if (proot->_data > data) //在节点左边插入
		{
			return _Rec_Insert(proot->_pleft, data);
		}
		else if (proot->_data < data)//在节点右边插入
		{
			return _Rec_Insert(proot->_pright, data);
		}
		return false;
	}
	//删除函数递归版本
	bool _Rec_Erase(pNode& root, const T& data)
	{
		if (root == nullptr)
		{
			return false;
		}
		if (root->_data > data)
		{
			return _Rec_Erase(root->_pleft, data);
		}
		else if (root->_data < data)
		{
			return _Rec_Erase(root->_pright, data);
		}
		else// 找到了待删节点
		{
			pNode del = root;//用一个节点记录待删节点地址,否则后续操作会出现内存泄漏
				//只有右边节点
			if (root->_pleft == nullptr)
			{
				root = root->_pright;
			}
			//只有右边节点
			else if (root->_pright == nullptr)
			{
				root = root->_pleft;
			}
			//两边都有节点 用替代法 这里用左节点最大值
			else
			{
				Node* maxLeft = root->_pleft;
				while (maxLeft->_pright)
				{
					maxLeft = maxLeft->_pright;
				}
				swap(root->_data, maxLeft->_data);
				//交换完成之后递归调用删除函数,注意这里可以递归
				//前面非递归写法不可以递归的原因 前面的函数交换完成之后
				//根节点是整个树的根节点,而交换之后树的结构被破环了,
				//根本删除不到交换后的节点,而这里给的是交换完成后,子树的根做为参数
				//树的结构没有被破坏
				return _Rec_Erase(root->_pleft, data);
			}
			delete del; //释放资源	
			return true;
		}


	}

};


Test.cpp


#include"BSTree.h"
void InsertTest()
{
	BSTree<int> t;
	int a[] = { 8, 3, 1, 10, 6, 4, 7, 14, 13 };
	for (auto e : a)
	{
		t.Insert(e);
	}
	t.Rec_Insert(5);
	t.InOrder();
}

void EraseTest()
{
	BSTree<int> t;
	int a[] = { 8,7,4,5,15,10,9,12,20 };
	for (auto e : a)
	{
		t.Insert(e);
	}
	t.InOrder();

	//删除
	t.Rec_Erase(15);
	t.InOrder();
	t.Rec_Erase(8);
	t.InOrder();

	//测试Find
	cout << "Find:::" << endl;
	bool flag = t.Find(8);
	cout << flag << endl;
	bool flag2 = t.Find(12);
	cout << flag2 << endl;
}
int main()
{
	EraseTest();
	return 0;
}

代码命名规则注释:代码中变量或者函数前 带“_’下划线的表示类的私有变量/函数。
水平有限,如有错误,敬请批评指正。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
下面是用 Python 语言实现非递归创建二叉树非递归遍历的代码示例: ```python class TreeNode: def __init__(self, val): self.val = val self.left = None self.right = None def createTree(nums): if not nums: return None root = TreeNode(nums[0]) stack = [root] i = 1 while i < len(nums): node = TreeNode(nums[i]) if not stack: break if stack[-1].left is None: stack[-1].left = node else: stack[-1].right = node stack.pop() stack.append(node) i += 1 return root def preorderTraversal(root): if not root: return [] result = [] stack = [root] while stack: node = stack.pop() result.append(node.val) if node.right: stack.append(node.right) if node.left: stack.append(node.left) return result def inorderTraversal(root): if not root: return [] result = [] stack = [] node = root while stack or node: while node: stack.append(node) node = node.left node = stack.pop() result.append(node.val) node = node.right return result ``` 这里我们用到了栈来实现非递归遍历。在创建二叉树时,我们用一个栈来保存节点,如果当前节点的左子树为空,就将其作为左子树;否则,看是否有右兄弟节点,如果有,就将当前节点弹出栈,并将其右兄弟节点作为新的当前节点;如果没有,就继续将新节点作为左子树。 在先序遍历中,我们将根节点压入栈中,然后弹出栈顶元素,将其值加入结果数组中,再将其右子节点、左子节点依次压入栈中。在中序遍历中,我们需要一直将左子节点压入栈中,直到没有左子节点了,就取出栈顶元素,将其值加入结果数组中,并将当前节点设置为其右子节点,继续进行遍历。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值