【数据结构】二叉搜索树 C++ 简单实现:增删查改全攻略

目录

前言:

1、什么是二叉搜索树?

2、二叉搜索树性能分析

3、key类型二叉搜索树的实现

节点结构

类结构

3.1、插入

3.2、中序遍历

3.3、查找

3.4、删除

4、key_value类型二叉搜索树的实现

节点结构

类结构

4.1、构造函数

[4.1.1 默认构造](#4.1.1 默认构造)

[4.1.2 拷贝构造](#4.1.2 拷贝构造)

4.2、赋值重载

4.3、析构

4.4、插入

总结

前言:

今天这篇,我们就从'0'开始搭一棵 C++ 二叉搜索树:先定义节点结构,再实现最核心的'插入'和'查找',最后攻克最难的'删除'(包括叶子节点、单孩子节点、双孩子节点三种情况)。全程不跳步、不埋坑,写完后你不仅能运行代码,更能说清'每一步为什么这么做'。"

1、什么是二叉搜索树?

二叉搜索树又称二叉排序树,如图,它或者是一棵空树,或者是具有以下性质的二叉树:

左子树上所有节点的值都小于等于根结点的值;

右子树上所有节点的值都大于等于根结点的值;

它的左右子树也分别为二叉搜索树

***注意:***二叉搜索树中可以支持插入相等的值,也可以不支持插入相等的值,具体看使用场景定义,后续我 们学习map/set/multimap/multiset系列容器底层就是二叉搜索树,其中map/set不支持插入相等值,multimap/multiset支持插入相等值

2、二叉搜索树性能分析

在最优情况下,二叉搜索树为完全二叉树(或者接近完全二叉树),其高度为: log2 N

最差情况下,二叉搜索树退化为单支树(或者类似单支),其高度为: N

所以平均时间的时间复杂度为 O(log n),最差情况为 O(n)。

说明: 虽然二分查找也可以实现 O(log2 N) 级别的查找效率,但是二分查找有两大缺陷

  1. 需要存储在支持下标随机访问的结构中,并且有序。

  2. 插入和删除数据效率很低,因为存储在下标随机访问的结构中,插入和删除数据一般需要挪动数 据。

3、key类型二叉搜索树的实现

为什么是key类型二叉搜索树呢?因为我们后面要讲的set/multiset/map/multimap容器的底层数据结构就红黑树(Red-Black Tree) ------ 一种 "近似平衡" 的二叉搜索树(BST)。但不同的是,对于set/multiset容器,如下图,集合(Sets)是一种容器,它按照特定顺序存储唯一的元素。

节点结构

cpp 复制代码
template<class K>
struct BSTNode
{
	K _key;            // 存储数据
	BSTNode<K>* _left; // 指向左节点的指针
	BSTNode<K>* _right;// 指向右节点的指针

	BSTNode(const K& key = 0) //默认构造
		:_key(key)
		,_left(nullptr)
		,_right(nullptr)
	{ }
};

类结构

cpp 复制代码
template<class K>
class BSTree
{
	//typedef BSTNode<K> Node; // typedef对节点对象重命名
	using Node = BSTNode<K>; // 使用using重命名
public:
    // ...
private:
    Node* _root = nullptr; // 成员变量
};

***说明:***下面我们实现过程中默认为值不重复的情形

3.1、插入

思路:

(1)树为空,则直接新增结点,赋值给root指针

(2)树不空,按二叉搜索树性质,用cur指针遍历二叉搜索树,用一个parent指针记下父亲节点,要插入值key比当前节点大往右走,插入值比当前结点小往左走,直到找到空位置,插入新结点。

(4)确定要插入的节点在父亲节点的左边还是右边,即当_key < parent->_key则将新节点插入到parent的左边,反之则插入到右边。

(5)如果支持插入相等的值,插入值跟当前结点相等的值可以往右走,也可以往左走,找到空位置,插入新结点。但是,要确定相同的值要么都往左边走或者都往右边走。

cpp 复制代码
//-------------------------------代码实现-------------------------------------------------
bool Insert(const K& key)
{
	Node* node = new Node(key);

    // 处理空树
	if (_root == nullptr)
	{
		_root = node;
		return true;
	}
	Node* parent = nullptr; // 记下父亲节点的指针,方便最后连接新节点
	Node* cur = _root;

    // 遍历二叉搜索树
	while (cur)
	{
		if (key < cur->_key)
		{
			parent = cur;
			cur = cur->_left;
		}
		else if (key > cur->_key)
		{
			parent = cur;
			cur = cur->_right;
		}
		else
		{
			return false;
		}
	}

    // 确定新节点最后的插入位置
	if (key < parent->_key)
	{
		parent->_left = node;
	}
	else
	{
		parent->_right = node;
	}
}

3.2、中序遍历

根据二叉搜索树的特性,其中序遍历的结果恰好为一个有序序列(升序)

为了解决函数调用传参的问题,因为中序遍历的参数_root是成员变量,在类外面调用无法访问成员变量而无法传参,所以,用子函数方式来实现。

cpp 复制代码
// 中序遍历
void MidOrder()
{
	_MidOrder(_root);
}
// 子函数
void _MidOrder(Node* _root)
{
	if (_root == nullptr)
	{
		return;
	}
	_MidOrder(_root->_left);
	cout << _root->_key << " ";
	_MidOrder(_root->_right);
}

3.3、查找

思路:

(1)从根开始比较,查找val,val比根的值大则往右边走查找,val比根值小则往左边走查找。

(2)最多查找高度次,走到到空,还没找到,这个值不存在,返回false。

(3)如果不支持插入相等的值,找到x即可返回。

cpp 复制代码
bool Find(const K& val)
{
	Node* cur = _root;
    // 遍历查找
	while (cur)
	{
		if (val < cur->_key)
		{
			cur = cur->_left;
		}
		else if (val > cur->_key)
		{
			cur = cur->_right;
		}
		else if (val == cur->_key)
		{
			return true;
		}
		else
		{
			return false;
		}
	}
}

3.4、删除

思路:

(1)要删除节点N的位置有四种情况

a. 结点N左右孩子均为空 ;b. 结点N左孩子为空,右孩子结点不为空 ;c. 结点N右孩子位空,左孩子结点不为空 ;d. 结点N左右孩子结点均不为空。

(2)对于左右孩子都为空的情况,我们可以将其放在左孩子为空和右孩子为空的情况中处理。即将上面四种情况重新分为:左为空;右为空;左右都不为空三种。

(3)对于左为空,要删除节点N可能为根结点,则让该节点的右节点作为新的根结点;或者是父节点的左节点,则让父节点的_left指针指向节点N的右边;或者父节点的右节点,则让父节点的_right指针指向节点N的左边,右为空类似。

(4)对于左右都不为空,无法直接删除N结点,因为N的两个孩子无处安放,只能用替换法删除,找N左子树的值最大结点 R(最右结点)或者N右子树的值最小结点R(最左结点)替代N,因为这两个结点中任意一个,放到N的位置,都满足二叉搜索树的规则。替代N的意思就是N和R的两个结点的值交换,转而变成删除R结点,R结点符合情况2或情况3,可以直接删除。

cpp 复制代码
bool Earse(const K& val)
{
	Node* cur = _root;
	Node* parent = cur;

    // 遍历先找到N节点
	while (cur)
	{
		if (val < cur->_key)
		{
			parent = cur;
			cur = cur->_left;
		}
		else if (val > cur->_key)
		{
			parent = cur;
			cur = cur->_right;
		}
        // 找到N节点
		else
		{
			// 左为空
			if (cur->_left == nullptr)
			{
				// 根结点
				if (_root->_key == val)
				{
					_root = _root->_right;
				}
				else if (cur == parent->_left)
					parent->_left = cur->_right;
				else
					parent->_right = cur->_right;

				delete cur;
			}

			// 右为空
			else if (cur->_right == nullptr)
			{
				// 根结点
				if (_root->_key == val)
				{
					_root = _root->_left;
				}
				else if (cur == parent->_right)
					parent->_right = cur->_left;
				else
					parent->_left = cur->_left;

				delete cur;
			}

			// 左右孩子均不为空
			else
			{
				Node* replaceparent = cur;
				Node* replace = cur->_right;

                // 找左子树中最小值的节点R
				while (replace->_left)
				{
					replaceparent = replace;
					replace = replace->_left;
				}

                // 替换节点N与节点R的值
				cur->_key = replace->_key;
				if (replace == replaceparent->_left)
				{
					replaceparent->_left = replace->_right;
				}
				else
				{
					cur->_right = replace->_right;
				}

				delete replace;
			}
			return true;
		}
	}
	return false;
}

4、key_value类型二叉搜索树的实现

为什么还有key_value类型二叉搜索树呢?对于map/multimap容器,如下图,映射(Maps)是一种关联容器,它按照特定顺序存储由 "键值(key value)" 和 "映射值(mapped value)" 组合而成的元素。

节点结构

与key类型相比,只是多了个_value。

cpp 复制代码
template<class K, class V>
struct BSTNode
{
	K _key;
	V _value;
	BSTNode<K, V>* _left;
	BSTNode<K, V>* _right;

	BSTNode(const K& key = 0, const V& value = 0)
		:_key(key)
		, _value(value)
		, _left(nullptr)
		, _right(nullptr)
	{}
};

类结构

cpp 复制代码
template<class K, class V>
class BSTree
{
	using Node = BSTNode<K, V>;
public:
    // ...
private:
    Node* _root = nullptr;
};

***说明:***对于key_value类型我们实现一下其构造,析构等,除了插入操作与key类型有一点不同外,其他操作基本相同,所以我们不再重复解释。

4.1、构造函数

4.1.1 默认构造
cpp 复制代码
// C++11强制生成默认构造
BSTree() = default;
4.1.2 拷贝构造
cpp 复制代码
// 拷贝构造(深拷贝)
BSTree(const BSTree& tree)
{
	_root = copy(tree._root);
}

Node* copy(Node* root)
{
	if (root == nullptr)
	{
		return nullptr;
	}
	// 前序遍历(递归)
	Node* newroot = new Node(root->_key, root->_value);
	newroot->_left = copy(root->_left);
	newroot->_right = copy(root->_right);

	return newroot;
}

4.2、赋值重载

cpp 复制代码
// 赋值重载(现代写法)
BSTree& operator=(BSTree tree)
{
	swap(_root, tree._root);
	return *this;
}

4.3、析构

cpp 复制代码
// 析构函数
~BSTree()
{
	Destry(_root);
	_root = nullptr;
}

void Destry(Node* root)
{
	if (root == nullptr)
	{
		return;
	}
	// 后序遍历:防止根结点被释放而找不到左右孩子节点
	Destry(root->_left);
	Destry(root->_right);
	delete root;
}

4.4、插入

cpp 复制代码
bool Insert(const K& key, const V& value)
{
	Node* node = new Node(key, value);

	if (_root == nullptr)
	{
		_root = node;
		return true;
	}

	Node* parent = nullptr;
	Node* cur = _root;
	while (cur)
	{
		if (key < cur->_key)
		{
			parent = cur;
			cur = cur->_left;
		}
		else if (key > cur->_key)
		{
			parent = cur;
			cur = cur->_right;
		}
		else
		{
			return false;
		}
	}

	if (key < parent->_key)
	{
		parent->_left = node;
	}
	else
	{
		parent->_right = node;
	}
    return true;
}

总结

本文主要是简单实现了一下二叉搜索树的最基础的版本,与set/multiset/map/multimap底层的数据结构------平衡二叉搜索树有所不同,并没有过多关注底层实现细节,比如insert,Find返回值等。只是为了能够加深大家对二叉搜索树的理解,方便后面对平衡二叉树的理解。

相关推荐
小贾要学习2 小时前
编程中常见的排序算法
数据结构·c++·算法·排序算法
Yupureki2 小时前
从零开始的C++学习生活 4:类和对象(下)
c语言·数据结构·c++·学习
草莓熊Lotso2 小时前
揭开 C++ vector 底层面纱:从三指针模型到手写完整实现
开发语言·c++
还有几根头发呀2 小时前
[特殊字符] LeetCode 143 重排链表(Reorder List)详解
数据结构·链表
hui函数2 小时前
python全栈(基础篇)——day04:后端内容(字符编码+list与tuple+条件判断+实战演示+每日一题)
开发语言·数据结构·python·全栈
晨非辰5 小时前
《剑指Offer:单链表操作入门——从“头删”开始破解面试》
c语言·开发语言·数据结构·c++·笔记·算法·面试
啊我不会诶8 小时前
24ICPC成都站补题
数据结构·算法
仟千意9 小时前
数据结构:栈和队列
数据结构
渡我白衣9 小时前
list 与 forward_list:一场 STL 中的“链表哲学”之争
数据结构·c++·list