406-BST树讲解大全(上)

在这里插入图片描述

BST树讲解大全

BST树称作二叉搜索树(Binary Search Tree)或者二叉排序树(Binary Sort Tree),它或者是一棵空树;或者是具有下列性质的二叉树:
1、若左子树不为空,则左子树上所有节点的值均小于它的根节点的值
2、若右子树不为空,则右子树上所有节点的值均大于它的根节点的值
3、左右子树也分别满足二叉搜索树性质

特点:每一个节点都满足 左孩子的值(不为空)< 父节点的值 <右孩子的值(不为空)

在这里插入图片描述
类似于二分查找算法。
在这棵树上,我们找一个元素,最多找4次。跟58比较,比58大的往右边找,再跟67比较,比67小的往左边走,跟62比较,比62小的没有了,比62大的往右边走,跟64比较,跟64还不相等了,只能说明这个元素不存在。

在这里插入图片描述
二分搜索的时间复杂度:log(n)
在这里插入图片描述

二叉树的定义

//BST树代码实现
template<typename T, typename Comp = less<T>>
class BSTree
{
public:
	//初始化根节点和函数对象+lambda表达式
	BSTree(Comp comp = Comp())
		:root_(nullptr)
		, comp_(comp)
	{}
	//二叉树节点定义
	struct Node
	{
		Node(T data = T())
			:data_(data)
			, left_(nullptr)
			, right_(nullptr)
		{}
		T data_;//数据域
		Node* left_;//左孩子域
		Node* right_;//右孩子域
	};
	Node* root_; // 指向BST树的根节点
	Comp comp_; // 定义一个函数对象
};

BST树的插入

在树上,每个节点都有2个指针域,一个指针指向它的左孩子,一个指针指向它的右孩子。

我们会定义一个root指针,指向根节点。
如果没有根节点,root=nullptr;

BST树如果为空,root—>新生成的节点(即根节点)
BST树如果不为空, 从根节点开始比较,找到合适的位置,生成新的节点,并把节点的地址写入父节点相应的地址域当中。
在这里插入图片描述
假设我们要把12插入上图的BST树中,首先,得从根节点开始,我们拿12和58对比,12比58小,就到24,12比24小,就到0,12比0大,就到5,12比5大,就看5的右孩子了,5的右孩子为空,就插在5的右孩子位置了。生成一个节点,连接在5的右孩子位置。也就是让5这个节点的右孩子记录一下新生成节点12的地址。
在这里插入图片描述

非递归插入

//非递归插入操作
void n_insert(const T& val)
{
	//树为空,生成根节点
	if (root_ == nullptr)
	{
		root_ = new Node(val);//指向新生成的节点 
		return;
	}

	//搜索合适的插入位置,记录父节点的位置
	//因为下面的循环出去之后,cur为空的! 
	Node* parent = nullptr;//父指针 
	Node* cur = root_;//当前指针 
	while (cur != nullptr)
	{
		if (cur->data_ == val)//和当前节点比较 
		{
			//不插入元素相同的值
			return;
		}
		else if (comp_(cur->data_, val))//当前的数据小于要插入的数据 
		{
			parent = cur;
			cur = cur->right_;
		}
		else//当前的数据大于要插入的数据
		{
			parent = cur;
			cur = cur->left_;
		}
	}

	//把新节点插入到parent节点的孩子上
	if (comp_(val, parent->data_))//要插入的数据小于节点的数据 
	{
		parent->left_ = new Node(val);
	}
	else//要插入的数据大于节点的数据 
	{
		parent->right_ = new Node(val);
	}
}

递归
在这里插入图片描述
从根节点开始查询,肯定是要找到叶子节点的空位置,要用递归,传参肯定要传入节点和值。
递归是有回溯的。
在这里插入图片描述
在回溯的过程中,把新生成的节点的地址返回给父节点去。

//递归插入操作
void insert(const T& val)
{
	root_ = insert(root_, val);
}

//递归插入操作实现
Node* insert(Node* node, const T& val)
{
	if (node == nullptr)
	{
		//递归结束,找到插入val的位置,生成新节点并返回其节点地址
		return new Node(val);
	}

	if (node->data_ == val)
	{
		return node;//值相同,就不插入
	}
	else if (comp_(node->data_, val))//当前节点的值小于要插入的值
	{
		node->right_ = insert(node->right_, val);//因为有返回,可以写入父节点的地址上
	}
	else//当前节点的值大于要插入的值
	{
		node->left_ = insert(node->left_, val);//因为有返回,可以写入父节点的地址上
	}
	return node;
}

BST树的删除

如何在BST树中删除节点?
在这里插入图片描述
我们把你一删除,应该把你的孩子连接到你的父亲的孩子域上!

1、如果删除的是叶子节点,就没有孩子,所以,删除叶节点只需要把它的父节点的相对应的孩子地址域置为空就可以了。
2、如果删除只有1个孩子的节点,我们把69删除了,其实就是把78节点的地址写到69的父节点67的右孩子域就可以了。即:把要删除的节点的孩子节点地址写到其父节点的对应的孩子域中。
3、如果删除的节点有2个孩子。
找待删除的节点的前驱节点(或者后继节点),用前驱或者后继节点的值,把待删除节点的值覆盖掉,然后直接删除前驱或者后继节点就可以了。

前驱节点:当前节点的左子树中值最大的节点。
后继节点:当前节点的右子树中值最小的节点。
前驱节点比如说67,67的左子树中值最大,大的值都在左子树的右边,所以,67的前驱节点就是从其左孩子开始,一直往右走,走到头,走到头的那个节点就是它的前驱节点。67的前驱节点是64。
后继节点比如说67,往右走,从右孩子开始,一直往左走,走到头的那个节点就是后继节点。67的后继节点是69。

前驱节点或者后继节点最多有1个孩子或者没有孩子。

如果我们要删除67节点,67节点有2个孩子,我们就把67的前驱节点找到:64节点,把67节点的值改为64,然后删除原来的64节点就可以了。

如果删除下图所示的root根节点
在这里插入图片描述
它的孩子就成为了新的根节点
在这里插入图片描述
非递归删除操作

//非递归删除操作
void n_remove(const T& val)
{
	//如果树是空的,就直接返回
	if (root_ == nullptr)
	{
		return;
	}

	//搜索待删除节点
	Node* parent = nullptr;
	//因为找到待删除节点之后,要到他的父节点写东西,写空,或者待删节点的孩子 
	Node* cur = root_;
	while (cur != nullptr)//遍历 
	{
		if (cur->data_ == val)
		{
			break;//找到待删除节点
		}
		else if (comp_(cur->data_, val))//当前节点的值小于要删除的节点的值 
		{
			parent = cur;
			cur = cur->right_;
		}
		else//当前节点的值大于要删除的节点的值 
		{
			parent = cur;
			cur = cur->left_;
		}
	}

	//如果没找到待删除节点
	if (cur == nullptr)
	{
		return;
	}

	//先处理情况3 =》 删除前驱节点(归结成情况1、2)
	if (cur->left_ != nullptr && cur->right_ != nullptr)
	{
		parent = cur;
		Node* pre = cur->left_;//指向要删除节点的左孩子 
		while (pre->right_ != nullptr)//一直往右走 
		{
			parent = pre;
			pre = pre->right_;
		}
		cur->data_ = pre->data_;//把前驱节点的值赋值给要删除的节点的值 
		cur = pre; //让cur指向前驱节点,转化成情况1,2
	}

	//cur指向删除节点,parent指向其父节点,同一处理情况1或者2
	Node* child = cur->left_;
	if (child == nullptr)//就是没有叶子 
	{
		child = cur->right_;//指向哪都是空 
	}

	if (parent == nullptr) //特殊情况 表示删除的是根节点,根节点没有parent
	{
		root_ = child;
	}
	else
	{
		//把待删除节点的孩子(nullptr或者不空)写入其父节点相应地址域中
		if (parent->left_ == cur)
		{
			parent->left_ = child;
		}
		else
		{
			parent->right_ = child;
		}
	}

	delete cur;//删除当前节点
}

递归删除操作
在这里插入图片描述

删除之后,在回溯的时候,得把删除节点的孩子给删除节点的父节点返回回去。
如果删除的节点没有孩子,就给父节点返回空。
如果删除的节点是有1个孩子,比如说,删除62,得把64节点地址返回给67的孩子域
如果删除的节点有2个孩子,我们就找待删除节点的前驱节点,把前驱节点的值替代待删除节点的值,然后把前驱节点删除。

//递归删除操作
void remove(const T& val)
{
	root_ = remove(root_, val);
}

//递归删除操作实现
Node* remove(Node* node, const T& val)
{
	if (node == nullptr)//没找到就返回空,本来就是空,返回空
		return nullptr;

	if (node->data_ == val)//找到待删除节点
	{
		//处理情况3
		if (node->left_ != nullptr && node->right_ != nullptr)//待删除节点有2个孩子
		{
			//找前驱节点
			Node* pre = node->left_;
			while (pre->right_ != nullptr)
			{
				pre = pre->right_;
			}
			node->data_ = pre->data_;//把前驱节点的值覆盖待删除节点的值
			//通过递归直接删除前驱节点 
			node->left_ = remove(node->left_, pre->data_);
		}
		else//处理情况1和情况2
		{
			if (node->left_ != nullptr)//待删除的节点只有左孩子
			{
				//删除节点以后,把非空的左孩子返回,回溯时更新其父节点地址域
				Node* left = node->left_;
				delete node;
				return left;
			}
			else if (node->right_ != nullptr)//待删除的节点只有右孩子
			{
				//删除节点以后,把非空的右孩子返回,回溯时更新其父节点地址域
				Node* right = node->right_;
				delete node;
				return right;
			}
			else//删除的是没有孩子的节点  叶子节点
			{
				delete node;
				return nullptr;//回溯时更新其父节点地址域为nullptr
			}
		}
	}
	else if (comp_(node->data_, val))//当前节点的值小于要删除节点的值
	{
		node->right_ = remove(node->right_, val);
	}
	else//当前节点的值大于要删除节点的值
	{
		node->left_ = remove(node->left_, val);
	}
	return node;//把当前节点返回给父节点,更新父节点相应的地址域
}

BST树的查询

从根节点开始比较,比根节点小,往左走,比根节点大,往右走。
如果走到叶子节点上还没找到元素,就说明不存在!
非递归方式

//非递归查询操作
bool n_query(const T& val)
{
	Node* cur = root_;//指向根节点 
	while (cur != nullptr)
	{
		if (cur->data_ == val)//找到了 
		{
			return true;
		}
		else if (comp_(cur->data_, val))//当前节点的值小于要查询的节点的值 
		{
			cur = cur->right_;
		}
		else//当前节点的值大于要查询的节点的值 
		{
			cur = cur->left_;
		}
	}
	return false;
}

递归查询

// 递归查询操作
bool query(const T& val)
{
	return nullptr != query(root_, val);
}
//递归查询操作实现
Node* query(Node* node, const T& val)
{
	if (node == nullptr)
		return nullptr;

	if (node->data_ == val)//找到了
	{
		return node;
	}
	else if (comp_(node->data_, val))//当前节点的值小于要查询的值
	{
		return query(node->right_, val);
	}
	else//当前节点的值大于要查询的值
	{
		return query(node->left_, val);
	}
}

BST树的前中后序遍历

在这里插入图片描述
层序遍历:从根节点开始,一层一层的遍历,从上到下,从左到右依次遍历
58,24,67,0,34,62,69,5,41,64,78

我们把每一个节点看作V,它的左孩子看作L,它的右孩子看作R
规定L总是出现在R之前。

所以,我们有3种组合方式:
VLR (前序遍历)
LVR (中序遍历)
LRV (后序遍历)
根据V的位置,我们就知道它是什么样的遍历。

V代表当前节点的输出操作。L代表访问该节点的左孩子,不输出,R代表访问该节点的右孩子,不输出。

前序遍历
58,24,0,5,34,41,67,62,64,69,78
在每一个节点上都是以VLR的方式访问节点。
第一个是V,我们输出的是58,然后是L,我们走到58的左边,也就是24,又是以VLR的方式访问节点,V就是当前节点24了,我们输出24,V操作完该到L了,L就是访问24的左孩子,也就是0,到了0,又是以VLR的方式访问节点,V就是当前节点0了,我们输出0,V操作完改到L了,L就是访问0的左孩子,为空,完了,执行R操作,就改访问0的右孩子了,就到5了,又是以VLR的方式访问5节点,V操作,输出5,然后执行L操作,访问5的左孩子,L操作空,R操作空,就往上回退。回退到0,0这个节点的VLR都访问完了,再回退到24,24的V和L访问过了,该执行R操作了,访问24的右孩子34,34又是以VLR的方式,V操作输出34,然后执行L操作,34没有左孩子,然后执行R操作,访问到34的右孩子41,到了41,又是以VLR的方式访问,输出41,41的左右孩子都为空,回退到34,34已结执行完VLR了,再回退到24,24已结执行完VLR了,再回退到58,58的V和L访问了,接下来访问58的R,访问到67了,又是以VLR的方式访问,V操作输出67,然后执行L操作,访问67的左孩子,访问到62,又是以VLR的操作,输出62,62的左孩子为空,访问62的右孩子,访问到64,执行VLR,V操作输出64,64的左右孩子为空,回退到62节点,62执行完VLR了,回退到67,执行67的R操作,访问到69,又是以VLR的方式,输出69,其左孩子为空,看其右孩子,访问到其右孩子78,78执行V操作输出78,78的左右孩子都为空。回退到69。就完了。

中序遍历:
从根节点开始,每一个节点都是以LVR的方式访问节点

0,5,24,34,41,58,62,64,67,69,78

后序遍历
从根节点开始,每一个节点都是以LRV的方式访问节点
5,0,41,34,24,64,62,78,69,67,58

递归的思路和上面所描述的一样。

我们看看前序遍历递归调用的图解:
每一个节点都是一次函数的开辟调用和销毁回退
node指向当前的节点
在这里插入图片描述

//递归前序遍历的实现 VLR
void preOrder(Node* node)
{
	if (node != nullptr)
	{
		cout << node->data_ << " "; //操作V输出当前节点值 
		preOrder(node->left_);//L操作,访问左孩子 
		preOrder(node->right_);//R操作,访问右孩子 
	}
}
//递归中序遍历的实现 LVR
void inOrder(Node* node)
{
	if (node != nullptr)
	{
		inOrder(node->left_);//L操作,访问左孩子 
		cout << node->data_ << " ";//操作V输出当前节点值 
		inOrder(node->right_);//R操作,访问右孩子 
	}
}
//递归后序遍历的实现 LRV
void postOrder(Node* node)
{
	if (node != nullptr)
	{
		postOrder(node->left_);//L操作,访问左孩子 
		postOrder(node->right_);//R操作,访问右孩子 
		cout << node->data_ << " "; //操作V输出当前节点值 
	}
}

层序遍历
在这里插入图片描述
从上到下,从左到右依次遍历。
58,24,67,0,34,62,69,5,41,64,78

有多少层就进行多少次递归遍历,
控制层数,当它到达哪个层,再进行打印

//递归层序遍历操作
void levelOrder()
{
	cout << "[递归]层序遍历:";
	int h = high();//树的层数
	for (int i = 0; i < h; ++i)
	{
		levelOrder(root_, i);
		//递归调用树的层数次,因为递归是深度的,每到一个层,就打印 
	}
	cout << endl;
}
//递归层序遍历的实现
void levelOrder(Node* node, int i)
{
	if (node == nullptr)
		return;

	if (i == 0)//到达要打印的层数 
	{
		cout << node->data_ << " ";
		return;
	}
	levelOrder(node->left_, i - 1);
	levelOrder(node->right_, i - 1);
}
//递归实现求二叉树层数 求以node为根节点的子树的高度并返回高度值
int high(Node* node)
{
	if (node == nullptr)
	{
		return 0;
	}
	int left = high(node->left_);//L,递归到最下边,然后回溯 
	int right = high(node->right_);//R
	return left > right ? left + 1 : right + 1;//V ,相当后序遍历 
}
//递归求二叉树节点个数的实现 求以node为根节点的树的节点总数,并返回
int number(Node* node)
{
	if (node == nullptr)
		return 0;
	int left = number(node->left_);//L
	int right = number(node->right_);//R
	return left + right + 1;//V  , 相当于后序遍历 
}

非递归的前 中 后 层 序遍历

在这里插入图片描述
非递归前序遍历
每一个节点是VLR的访问方式
我们从根节点开始,执行V操作,打印58,我们要把这些节点的右孩子存储起来,不然找完左孩子,找不到右孩子了。
我们定义一个栈
先压右边,然后压左边,
我访问58,我就先打印58,然后我把67压入栈,然后把24压入栈,然后把栈顶元素出栈,也就是24出栈,24打印之后,把它的右边入栈,也就是34,然后把它的左边:0入栈。然后0出栈,打印0,然后把0的右孩子5入栈,0的左孩子为空,就把5出栈,打印5,5的左右孩子为空,然后把34出栈,打印34,34有右孩子:41,把41入栈,34的左边为空,把41出栈,打印41,41的左右孩子为空,把67出栈,打印67,把67的右孩子69入栈,然后把67的左孩子62入栈 ,然后62出栈,打印62,然后把62的右孩子:64入栈,64的左孩子为空,出栈64,打印64,64的左右孩子为空,把69出栈,打印69,把69的右孩子78入栈,69的左孩子为空,把78出栈,打印78,结束了。

//非递归前序遍历操作
void n_preOrder()
{
	cout << "[非递归]前序遍历:";
	if (root_ == nullptr)
	{
		return;
	}

	stack<Node*> s;
	s.push(root_);//根节点入栈
	while (!s.empty())//如果栈不为空
	{
		Node* top = s.top();//获取栈顶元素
		s.pop();//出栈

		cout << top->data_ << " "; //执行V操作,打印当前节点的值

		if (top->right_ != nullptr)
		{
			s.push(top->right_);//执行R操作,右孩子入栈
		}

		if (top->left_ != nullptr)
		{
			s.push(top->left_);//执行L操作,左孩子入栈
		}
	}
	cout << endl;
}

非递归中序遍历:
每一个节点都是LVR的方式
定义一个栈
从根节点开始,58入栈,左孩子24入栈,24的左孩子0入栈,0没有左孩子,然后就是0出栈,打印0,然后就是右孩子5入栈,5如果有左孩子的话就是左孩子一直入栈,但是5没有左孩子,就是指向V了,5出栈,打印5,5也没有右孩子,24出栈,打印24,24有右孩子,34入栈,34没有左孩子,就34出栈,打印34,34有右孩子,41入栈,41没有左孩子,直接出栈,打印41,41没有右孩子,出栈58,打印58,58的右孩子67入栈,67有左孩子,62入栈,62没有左孩子,62出栈,打印62,62有右孩子,把64入栈,64没有左孩子,64出栈 ,打印64,64没有右孩子,出栈67,打印67,67有右孩子,69入栈,69没有左孩子,出栈69,打印69,69有右孩子,78入栈,78没有左孩子,出栈78,打印78,78没有右孩子,结束了。

//非递归中序遍历操作
void n_inOrder()
{
	cout << "[非递归]中序遍历:";
	if (root_ == nullptr)
	{
		return;
	}
	stack<Node*> s;
	Node* cur = root_;

	while (!s.empty() || cur != nullptr)
	{
		if (cur != nullptr)
		{
			s.push(cur);
			cur = cur->left_;//执行L操作
		}
		else
		{
			Node* top = s.top();
			s.pop();
			cout << top->data_ << " ";//执行V操作,打印当前节点的值
			cur = top->right_;//执行R操作
		}
	}

	cout << endl;
}

非递归后序遍历:
每一个节点都是LRV的方式访问

如果我们只使用1个栈,执行L,左边不断入栈,直到左边为空,出栈一个元素,相当于要执行V操作了,可是后序遍历现在是要执行R操作,不能访问V,要把右孩子全部处理完,才轮到V操作。这时候如何找到刚才弹出去的V?

我们可以改变一下思路,按VRL的方式访问,然后输出的结果按逆序。或者也可以把逆序的结果入栈,先入的后打印。
我们需要2个栈,1个栈用来VRL遍历,另1个栈用来存储结果。

//非递归后序遍历操作
void n_postOrder()
{
	cout << "[非递归]后序遍历:";
	if (root_ == nullptr)
	{
		return;
	}
	stack<Node*> s1;//执行VRL访问方式的栈
	stack<Node*> s2;//打印结果的栈
	s1.push(root_);//根节点入栈

	while (!s1.empty())//每一个节点按VRL的访问方式
	{
		Node* top = s1.top();
		s1.pop();

		s2.push(top);//操作V,要打印的元素入s2栈
		if (top->left_ != nullptr)
		{
			s1.push(top->left_);//要访问R之前先把L存进去
		}
		if (top->right_ != nullptr)
		{
			s1.push(top->right_);//执行R,把R存进去
		}
	}
	while (!s2.empty())
	{
		cout << s2.top()->data_ << " ";//打印元素
		s2.pop();
	}
	cout << endl;
}

非递归层序遍历
在这里插入图片描述

创建一个队列(队列是先进先出)
根节点58打印,然后58的左孩子不为空,入队,58的右孩子不为空,入队。
然后出队,打印24。
24的左孩子不为空,0入队,24的右孩子不为空,34入队。
然后67出队,打印67,67的左孩子不为空,62入队,67的右孩子不为空,69入队。
然后0出队,0的左孩子为空,0的右孩子不为空,5入队。
然后34出队,打印34,34的左孩子为空,34的右孩子不为空,41入队。
然后62出队,打印62,62的左孩子为空,62的右孩子不为空,64入队。
然后69出队,69的左孩子为空,69的右孩子不为空,78入队。
然后5出队。后面的都没有孩子了。就依次输出了。

//非递归层序遍历操作
void n_levelOrder()
{
	cout << "[非递归]层序遍历:";
	if (root_ == nullptr)
	{
		return;
	}
	queue<Node*> que;
	que.push(root_);
	while (!que.empty())//队列不为空
	{
		Node* front = que.front();//取队头
		que.pop();//出队

		cout << front->data_ << " ";//打印
		if (front->left_ != nullptr)
		{
			que.push(front->left_);//左孩子入队
		}
		if (front->right_ != nullptr)
		{
			que.push(front->right_);//右孩子入队
		}
	}
	cout << endl;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

林林林ZEYU

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值