首页 > 其他分享 >二叉树进阶之二叉搜索树:一切的根源

二叉树进阶之二叉搜索树:一切的根源

时间:2024-08-17 21:53:53浏览次数:12  
标签:Node 进阶 cur nullptr 之二 right 二叉树 root left

前言:

在学完了简单的容器与C++面向对象的三大特性之后,我们首先接触的就是map与set两大容器,但是这两个容器底层实现的原理是什么呢?我们不而知,今天,主要来为学习map与set的底层原理而打好基础,而二叉搜索树,则是一切的开端......

一、二叉搜索树的定义与性质

1.1、什么是二叉搜索树:

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

1.2、二叉搜索树的性质:

  • 节点值关系: 对于每个节点 N,其左子树中所有节点的值都小于 N,右子树中所有节点的值都大于 N
  • 中序遍历的顺序: 对二叉搜索树进行中序遍历(左-根-右)时,节点的值按升序排列。
  • 平均时间复杂度: 在平均情况下,查找、插入和删除操作的时间复杂度都是 O(log n)。
  • 最坏时间复杂度: 在最坏情况下(树退化为链表),查找、插入和删除操作的时间复杂度是 O(n)。

二、二叉搜索树的模拟实现

初步了解性质之后,我们就根据这些特点与所学二叉树的知识模拟实现一个简单的二叉搜索树结构。首先我们创建一个头文件,取名为BSTree.h

1、节点结构

结点是组成一个二叉树的基础,我们要写一个二叉树,首先就是定义好一个的节点结构,由于是二叉树,所以一个结点会有两个子节点,我们分别以right与left表示左右子节点。随后就是值了,我们用val来表示(注意:一般来说会有key与key-val两种模型,分别象征之后的set与map两种容器,我们这里先以简单的key模型为例子)。

template<class T>
struct BSTNode
{
	BSTNode(const T&data=T())
		:_val(data)
		,left(nullptr)
		,right(nullptr)
	{}
	BSTNode<T>* right;
	BSTNode<T>* left;
	T _val;
};

再这样一串代码中,我们先定义一个结点结构,用之前学的模板知识将结点改造为一个模板。对与结点的默认构造,我们选择调用所存储数据类型的默认结构在初始化列表中给val值进行初始化,并且,给左右子节点的指针全部指向空。(data值直接调用的T这个数据类型自己的默认构造:包括内置类型与自定义类型)

2、二叉树的创建

随后,我们就在继续创建一个BSTree结构体模板,为了方便,先将结点进行重命名,随后将必要的构造函数,参数写上。一个BSTree参数只需要一个根节点就行。构造函数也只需要对根节点指针指向空就行。

template<class T>
class BSTree
{
	typedef BSTNode<T> Node;
public:
	BSTree()
		:_root(nullptr)
	{}
	
private:
	Node* _root;
};

3、二叉树的查找

在实现插入这个接口之前得先实现查找,因为要插入一个节点,我们就需要先找到相应的位置。

倘若找不到这个节点,就返回nullptr,找到了,就返回指向这个结点的指针。

Node* Find(const T& data)
{
	Node* cur = _root;
	while (cur)
	{
		if (cur->_val == data)
		{
			return cur;
		}
		else if (cur->_val > data)
		{
			cur = cur->right;
		}
		else
		{
			cur = cur->left;
		}
	}
	return nullptr;
}

4、二叉树的插入

接下来就是实现插入这个重要接口,由于二叉搜索树的独特性质,不能插入相同的元素,为了知道是否插入成功,我们需要给这个接口的返回值返回一个bool类型

bool Insert(const T& data)
{
	if (_root == nullptr)//如果是一个空树,就创建一个结点随后返回真,并把_root指向新创建的结点
	{
		_root = new Node(data);
		return true;
	}
	Node* cur == _root;
	Node* parent = nullptr;//记录cur的父节点
	while (cur)
	{
		parent = cur;
		if (cur->_val > data)
		{
			cur = cur->left;
		}
		else if (cur->_val < data)
		{
			cur = cur->right;
		}
		else
		{
			return false;
		}
	}
	cur = new Node(data);
	if (cur->_val > parent->_val)
	{
		parent->right = cur;
	}
	else
	{
		parent->left = cur;
	}
	return true;
}

5、二叉树的删除

在二叉搜索树中,删除节点的操作是最复杂的。删除操作需要保持二叉搜索树的性质,即在删除一个节点后,仍然能够保持左子树的所有节点值小于当前节点值,右子树的所有节点值大于当前节点值。如果我们想要删除的节点为叶子,那还比较轻松,但如果我们想要删除的节点仍然拥有子节点,那就麻烦许多了。

日常我们有两种思路解决,二者大同小异,一个是寻找左子树的最最右节点,一个是找右子树的最左节点,这里我就以右子树的最左节点为例。

bool Erase(const T& data)
{
	Node* parent = nullptr;
	Node* cur = _root;
	while (cur)//查找是否有这个节点
	{
		if (cur->_val > data)
		{
			parent = cur;
			cur = cur->left;
		}
		else if (cur->_val < data)
		{
			parent = cur;
			cur = cur->right;
		}
		else
		{
			break;
		}
	}

	if (cur == nullptr)//没有这个节点直接返回
	{
		return false;
	}

	if (cur->left == nullptr)//左为空或者右为空,或者都为空的情况下
	{
		if (cur == _root)
		{
			_root = _root->right;
		}
		if (cur == parent->left)
		{
			parent->left = cur->right;
		}
		else  
		{
			parent->right = cur->right;
		}
		delete cur;
		cur = nullptr;
		return true;
	}

	if (cur->right == nullptr)
	{
		if (cur == _root)
		{
			_root = _root->left;
		}
	
		if (cur == parent->left)
		{
			parent->left = cur->left;
		}
		else
		{
			parent->right = cur->left;
		}
		delete cur;
		cur = nullptr;
		return true;
	}

	Node* pparent = cur;//两个子节点都在的情况下,需要找到右子树的最小节点,将两个的值进行交换,再删除右子树的最小节点,此时又是一个0/1子树的选择情况
	Node* pcur = cur->right;
	while (pcur->left)
	{
		pparent = pcur;
		pcur = pcur->left;
	}
	cur->_val = pcur->_val;

	Node* p = pcur->right;
	if (pparent->left == pcur)
	{
		pparent->left = p;
	}
	else
	{
		pparent->right = p;
	}
	delete pcur;
	pcur = nullptr;
	return true;
	
}

大概的思路就是先找到要删除的节点,随后判断这个节点有几个非空子节点,0和1个空节点是一样的处理方法,二两个非空节点就需要用右子树的最小值或者左子树的最大值来替换,随后删除替换了值的那个节点。

6、二叉树的遍历

由于在外部,我们拿不到根节点,那么我们要怎么遍历一遍呢?这里就需要用到我们的套层艺术了!

首先我们先在private中写一个中序遍历的接口:

void order(Node*root)
{
	if (root == nullptr)
	{
		return;
	}
	order(root->left);
	cout << root->_val << " ";
	order(root->right);
}

随后,我们在publci中写一个无参的_oeder函数来调用order就行了。

void _order()
{
	order(_root);
	cout<< endl;
}

我们写一个简单的用例测试一下

#include<iostream>
using namespace std;
#include"BSTree.h"

int main()
{
	
	BSTree<int>Tree;
	Tree.Insert(6);
	Tree.Insert(3);
	Tree.Insert(2);
	Tree.Insert(1);
	Tree.Insert(4);
	Tree.Insert(7);
	Tree.Insert(9);
	Tree.Insert(8);
	Tree.Insert(5);
	Tree.Insert(10);
	Tree._order();



	for (int i = 1; i <= 10; ++i)
	{
		Tree.Erase(i);
		Tree._order();

	}
	Tree._order();

	return 0;
}

输出结果没有问题:

7、二叉树的析构销毁

在上面的代码中,存在一个内存泄漏的问题,就是我们所有的节点都是自己new出来的,所以也需要我们自己手动去销毁,自动构建出来的析构函数自然就不行,所以必须要自己写析构函数。所以我们可以实现一个destroy函数,后序递归销毁二叉树,随后让析构函数调用destroy函数就行。

void Destroy(Node* root)
{
    if (root)
    {
        Destroy(root->left);
        Destroy(root->right);
        delete root;
    }
}

	~BSTree()
	{
		destroy(_root);
	}

8、由key模型向key-val模型的转化

接下来我们去实现一下两种模型之间的转化:

其实,二者并无太大区别,在插入销毁查找中判断仍然使用key值判断只不过是把以往的单独的T改成了T,K,把之前的_val改成_key。

只不过我们需要实现一个额外的构造函数,这个构造函数需要进行深拷贝来实现,也就是说我们要进行递归对这个二叉树进行深拷贝。

	BSTree(const BSTree<T,K>&t)
	{
		_root=Copy(t._root);
	}
	Node* Copy(Node* root)
	{
		if (root == nullptr)
		{
			return nullptr;
		}

		Node* newRoot = new Node(root->_key, root->_val);
		newRoot->_left = Copy(root->_left);
		newRoot->_right = Copy(root->_right);

		return newRoot;
	}

key模型整体的代码如下:

#pragma once

template<class T>
struct BSTNode
{
	BSTNode(const T&data=T())
		:_val(data)
		,left(nullptr)
		,right(nullptr)
	{}
	BSTNode<T>* right;
	BSTNode<T>* left;
	T _val;
};

template<class T>
class BSTree
{
	typedef BSTNode<T> Node;
public:
	BSTree()
		:_root(nullptr)
	{}

	~BSTree()
	{
		destroy(_root);
	}
	Node* Find(const T& data)
	{
		Node* cur = _root;
		while (cur)
		{
			if (cur->_val == data)
			{
				return cur;
			}
			else if (cur->_val > data)
			{
				cur = cur->left;
			}
			else
			{
				cur = cur->right;
			}
		}
		return nullptr;
	}

	bool Insert(const T& data)
	{
		if (_root == nullptr)//如果是一个空树,就创建一个结点随后返回真,并把_root指向新创建的结点
		{
			_root = new Node(data);
			return true;
		}
		Node* cur = _root;
		Node* parent = nullptr;//记录cur的父节点
		while (cur)
		{
			parent = cur;
			if (cur->_val > data)
			{
				cur = cur->left;
			}
			else if (cur->_val < data)
			{
				cur = cur->right;
			}
			else
			{
				return false;
			}
		}
		cur = new Node(data);
		if (cur->_val > parent->_val)
		{
			parent->right = cur;
		}
		else
		{
			parent->left = cur;
		}
		return true;
	}

	bool Erase(const T& data)
	{
		Node* parent = nullptr;
		Node* cur = _root;
		while (cur)//查找是否有这个节点
		{
			if (cur->_val > data)
			{
				parent = cur;
				cur = cur->left;
			}
			else if (cur->_val < data)
			{
				parent = cur;
				cur = cur->right;
			}
			else
			{
				break;
			}
		}

		if (cur == nullptr)//没有这个节点直接返回
		{
			return false;
		}

		if (cur->left == nullptr)//左为空或者右为空,或者都为空的情况下
		{
			if (cur == _root)
			{
				_root = _root->right;
			}
			else if (cur == parent->left)
			{
				parent->left = cur->right;
			}
			else  
			{
				parent->right = cur->right;
			}
			delete cur;
			cur = nullptr;
			return true;
		}

		if (cur->right == nullptr)
		{
			if (cur == _root)
			{
				_root = _root->left;
			}
			else if (cur == parent->left)
			{
				parent->left = cur->left;
			}
			else
			{
				parent->right = cur->left;
			}
			delete cur;
			cur = nullptr;
			return true;
		}

		Node* pparent = cur;//两个子节点都在的情况下,需要找到右子树的最小节点,将两个的值进行交换,再删除右子树的最小节点,此时又是一个0/1子树的选择情况
		Node* pcur = cur->right;
		while (pcur->left)
		{
			pparent = pcur;
			pcur = pcur->left;
		}
		cur->_val = pcur->_val;

		Node* p = pcur->right;
		if (pparent->left == pcur)
		{
			pparent->left = p;
		}
		else
		{
			pparent->right = p;
		}
		delete pcur;
		pcur = nullptr;
		return true;
		
	}

	void _order()
	{
		order(_root);
		cout<< endl;
	}
private:
	void order(Node*root)
	{
		if (root == nullptr)
		{
			return;
		}
		order(root->left);
		cout << root->_val << " ";
		order(root->right);
	}

	void Destroy(Node* root)
	{
		if (root)
		{
			Destroy(root->left);
			Destroy(root->right);
			delete root;
		}
	}

	Node* _root;
};

key-val的模型代码如下:


template<class T, class K>
struct BSTNode
{
	BSTNode(const T& key = T(), const K& val=K())
		:_key(key)
		,_val(val)
		, left(nullptr)
		, right(nullptr)
	{}
	BSTNode<T,K>* right;
	BSTNode<T,K>* left;
	T _key;
	K _val;
};

template<class T,class K>
class BSTree
{
	typedef BSTNode<T,K> Node;
public:
	BSTree() = default;
	BSTree(const BSTree<T,K>&t)
	{
		_root=Copy(t._root);
	}
	Node* Copy(Node* root)
	{
		if (root == nullptr)
		{
			return nullptr;
		}

		Node* newRoot = new Node(root->_key, root->_val);
		newRoot->_left = Copy(root->_left);
		newRoot->_right = Copy(root->_right);

		return newRoot;
	}

	~BSTree()
	{
		Destroy(_root);
	}
	Node* Find(const T& key)
	{
		Node* cur = _root;
		while (cur)
		{
			if (cur->_key == key)
			{
				return cur;
			}
			else if (cur->_key > key)
			{
				cur = cur->left;
			}
			else
			{
				cur = cur->right;
			}
		}
		return nullptr;
	}

	bool Insert(const T& key,const K&val)
	{
		if (_root == nullptr)//如果是一个空树,就创建一个结点随后返回真,并把_root指向新创建的结点
		{
			_root = new Node(key, val);
			return true;
		}
		Node* cur = _root;
		Node* parent = nullptr;//记录cur的父节点
		while (cur)
		{
			parent = cur;
			if (cur->_key > key)
			{
				cur = cur->left;
			}
			else if (cur->_key < key)
			{
				cur = cur->right;
			}
			else
			{
				return false;
			}
		}
		cur = new Node(key,val);
		if (cur->_key > parent->_key)
		{
			parent->right = cur;
		}
		else
		{
			parent->left = cur;
		}
		return true;
	}

	bool Erase(const T& key)
	{
		Node* parent = nullptr;
		Node* cur = _root;
		while (cur)//查找是否有这个节点
		{
			if (cur->_key > key)
			{
				parent = cur;
				cur = cur->left;
			}
			else if (cur->_key < key)
			{
				parent = cur;
				cur = cur->right;
			}
			else
			{
				break;
			}
		}

		if (cur == nullptr)//没有这个节点直接返回
		{
			return false;
		}

		if (cur->left == nullptr)//左为空或者右为空,或者都为空的情况下
		{
			if (cur == _root)
			{
				_root = _root->right;
			}
			else if (cur == parent->left)
			{
				parent->left = cur->right;
			}
			else
			{
				parent->right = cur->right;
			}
			delete cur;
			cur = nullptr;
			return true;
		}

		if (cur->right == nullptr)
		{
			if (cur == _root)
			{
				_root = _root->left;
			}
			else if (cur == parent->left)
			{
				parent->left = cur->left;
			}
			else
			{
				parent->right = cur->left;
			}
			delete cur;
			cur = nullptr;
			return true;
		}

		Node* pparent = cur;//两个子节点都在的情况下,需要找到右子树的最小节点,将两个的值进行交换,再删除右子树的最小节点,此时又是一个0/1子树的选择情况
		Node* pcur = cur->right;
		while (pcur->left)
		{
			pparent = pcur;
			pcur = pcur->left;
		}
		cur->_key = pcur->_key;

		Node* p = pcur->right;
		if (pparent->left == pcur)
		{
			pparent->left = p;
		}
		else
		{
			pparent->right = p;
		}
		delete pcur;
		pcur = nullptr;
		return true;

	}

	void _order()
	{
		order(_root);
		cout << endl;
	}
private:
	void order(Node* root)
	{
		if (root == nullptr)
		{
			return;
		}
		order(root->left);
		cout << root->_key << " " << root->_val << " ";
		order(root->right);
	}

	void Destroy(Node* root)
	{
		if (root)
		{
			Destroy(root->left);
			Destroy(root->right);
			delete root;
		}
	}

	Node* _root;
};

c测试用例如下:

#include<iostream>
using namespace std;
#include"BSTree.h"


void TestBSTree()
{
	// 统计水果出现的次数
	string arr[] = { "苹果", "西瓜", "苹果", "西瓜", "苹果", "苹果", "西瓜",
   "苹果", "香蕉", "苹果", "香蕉" };
	BSTree<string, int> countTree;
	for (const auto& str : arr)
	{
		// 先查找水果在不在搜索树中
		// 1、不在,说明水果第一次出现,则插入<水果, 1>
		// 2、在,则查找到的节点中水果对应的次数++
		//BSTreeNode<string, int>* ret = countTree.Find(str);
		auto ret = countTree.Find(str);
		if (ret == NULL)
		{
			countTree.Insert(str, 1);
		}
		else
		{
			ret->_val++;
		}
	}
	countTree._order();
}

int main()
{
	
	BSTree<int,string>Tree;
	Tree.Insert(6, "six");
	Tree.Insert(3,"three");
	Tree.Insert(2,"two");
	Tree.Insert(1,"one");
	Tree.Insert(4,"four");
	Tree.Insert(7,"seven");
	Tree.Insert(9,"nine");
	Tree.Insert(8,"eight");
	Tree.Insert(5,"five");
	Tree.Insert(10,"ten");
	Tree._order();



	for (int i = 1; i <= 10; ++i)
	{
		Tree.Erase(i);
		Tree._order();

	}
	Tree._order();

	cout << endl;

	TestBSTree(); 

	return 0;
}

三.、小结

二叉搜索树是一种重要的数据结构,具有良好的查找、插入和删除性能。但它也存在潜在的不平衡问题,因此在实际应用中,常常需要通过自平衡的二叉搜索树(如AVL树、红黑树)来保证性能。

希望本文对基础的二叉搜索树的探索能对您产生帮助!

标签:Node,进阶,cur,nullptr,之二,right,二叉树,root,left
From: https://blog.csdn.net/htw250056/article/details/141281293

相关文章

  • c语言计算二叉树的带权路径长度之和(WPL)
    1.WPL:树中全部叶节点的带权路径之和2.代码中所画的树为:3.求上述WPL:WPL=0*1+1*2+1*3+2*4+2*5=234.主要代码为:intwpl(Node*ROOT,inthigh){ intn=0; if(ROOT!=NULL){ n=ROOT->weight*high; n=n+wpl(ROOT->lchild,high+1); n=n+wpl(ROOT->rchild,high+1); } r......
  • 【C++进阶学习】第十三弹——C++智能指针的深入解析
    前言:在C++编程中,内存管理是至关重要的一个环节。传统的手动内存管理方式容易导致内存泄漏、悬挂指针等问题。为了解决这些问题,C++引入了智能指针。本文将详细讲解C++中智能指针的概念、种类、使用方法以及注意事项。目录一、引言二、智能指针的原理及目的2.1智能指针......
  • Kettle PDI小白新手/进阶/必备 大数据基础之一数据清洗(ETL)基础进阶总结 1.6万字长文
    Kettle是一个开源的数据集成工具,主要用于ETL(抽取、转换、加载)过程。它的全名是PentahoDataIntegration(PDI),而Kettle是其早期的名字,Kettle在2006年被Pentaho收购后,正式更名为PentahoDataIntegration(PDI),因此现在更常被称为PDI。PDI仍然是Pentaho产品套件中的一个重要......
  • 数据结构与算法(六)二叉树
    二叉树是一种数据结构,广泛用于计算机科学和编程中。它具有以下几个重要特征:1.基本定义:①二叉树:每个节点最多有两个子节点,分别称为左子节点和右子节点。②节点:二叉树的基本单位,包含数据以及指向其子节点的指针。③根节点:二叉树的第一个节点,没有父节点。④叶子节点:没有子节点的......
  • HBase学习的第五天--HBase进阶结尾和phoenix开头
    HBase进阶下一、HBase的读写流程1.1 HBase读流程Hbase读取数据的流程:1)是由客户端发起读取数据的请求,首先会与zookeeper建立连接2)从zookeeper中获取一个hbase:meta表位置信息,被哪一个regionserver所管理着hbase:meta表:hbase的元数据表,在这个表中存储了自定义表相关的元......
  • Task3:进阶上分-实战优化
    part1:工具初探一ComfyUI应用场景探索初识ComfyUI什么是ComfyUIGUI是"GraphicalUserInterface"(图形用户界面)的缩写。简单来说,GUI就是你在电脑屏幕上看到的那种有图标、按钮和菜单的交互方式。 ComfyUI是GUI的一种,是基于节点工作的用户界面,主要用于操作图像的生......
  • HBase学习的第四天--HBase的进阶与API
    HBase进阶与API一、Hbaseshell1、Region信息观察创建表指定命名空间在创建表的时候可以选择创建到bigdata17这个namespace中,如何实现呢?使用这种格式即可:‘命名空间名称:表名’针对default这个命名空间,在使用的时候可以省略不写create'hbase01:t1','info'此时使用li......
  • PHP初级栈进阶篇
    小刘小刘,下雨不愁(收藏,关注不迷路)这里我会更新一些php进阶知识点,新手想再进一步可以有个方向,也有个知识图谱的普及当然本篇不止写技术 会涉及一些进阶路线我也是在这里积累,希望和同行者一起进步为后来者少走些弯路你说。。。咋就需要学这么多那前端go linux 分布......
  • 代码随想录算法训练营第十七天(一)| 654.最大二叉树 617.合并二叉树
    654.最大二叉树题目:给定一个不重复的整数数组 nums 。 最大二叉树 可以用下面的算法从 nums 递归地构建:创建一个根节点,其值为 nums 中的最大值。递归地在最大值 左边 的 子数组前缀上 构建左子树。递归地在最大值 右边 的 子数组后缀上 构建右子树。......
  • 二叉树的递归与非递归遍历:C++实现
    在数据结构的学习中,二叉树是一个非常重要的概念。遍历二叉树是理解和操作二叉树的基础。本文将介绍如何使用C++实现二叉树的递归和非递归遍历,包括前序、中序和后序遍历,并对每种遍历方法的原理进行简要介绍。二叉树节点定义首先,我们定义一个简单的二叉树节点结构:structTreeN......