【数据结构】一步到胃,键值对版二叉搜索树


title: 【数据结构】一步到胃,键值对版二叉搜索树
date: 2024-07-27 08:14:12
tags: 数据结构 二叉树 搜索树
cover: https://picbed0521.oss-cn-shanghai.aliyuncs.com/blogpic/202409230835315.png


原文见我的网站: www.supdriver.top

什么是二叉搜索树

二叉搜索树的定义是一颗二叉树的所有节点满足:根的左右孩子存在时,满足 左孩子 < 根 < 右孩子

递归定义则是:

  1. 左子树的根 < < 右子树的根
  2. 左子树是二叉搜索树,右子树是二叉搜索树

写一个验证二叉搜索树的函数

Leetecode题目链接🔗

封装一个二叉树类

文件布置

  • BSTree.h用于声明和实现BSTree
  • test.cpp用于测试

头文件

BSTree.h

#include <iostream>
#include <utility>

test.cpp

#include "BSTree.h"

命名空间

namespace key_value

这里使用key_value作为命名空间,表示这是键值表示的搜索二叉树

定义节点

二叉树的节点用于储存键值对左右指针,并提供默认构造函数,使用初始化列表初始化成员变量

template<class K,class V>
class BSTNode
{
public:
    BSTNode(const K& key = K(), const V& value = V())
        :_left(nullptr)
        , _right(nullptr)
        , _key(key)
        , _value(value)
    {}
    BSTNode<K, V>* _left;//指向左子树
    BSTNode<K, V>* _right;//指向右子树
    K _key;//储存键
    V _value;//储存值
};

封装二叉搜索树

Binary Search Tree,这里用简称BSTree封装

基本架构

鉴于该类的接口基本依赖于成员变量,所以先组织好成员变量

作为一颗二叉树类,成员变量仅需一个指向根的指针即可

再次之前先用typedef定义一个节点类出来用于简化代码

最后提供一个默认构造函数_root初始化为nullptr

	template<class K,class V>
	class BSTree
	{
		typedef BSTNode<K, V> Node;//使用typedef简化代码
	public:
		BSTree() :_root(nullptr) {}//提供默认构造函数
	private:
		Node* _root;//指向根节点的指针作为成员变量
	};

insert函数

准备好后第一个接口就是insert,用于构建搜索二叉树

这里需要考虑的情况有

  • 空树时的插入
  • 插入的key已存在
  • 一般情况下成功的插入
public:
bool insert(const K& key, const V& value)
{
    if (_root == nullptr)//空树
    {
        _root = new Node(key, value);
        return true;
    }

    Node* cur = _root;
    Node* parent = nullptr;
    while (cur)
    {
        if (key < cur->_key)//key比当前节点小,往左子树走
        {
            parent = cur;
            cur = cur->_left;
        }
        else if (key > cur->_key)//往右子树走
        {
            parent = cur;
            cur = cur->_right;
        }
        else
        {
            return false;//key已存在,插入失败
        }
    }
    //此时cur为nullptr, parent为cur的结点
    if (key < parent->_key)parent->_left = new Node(key, value);
    else parent->_right = new Node(key, value);
    return true;//成功插入
}

in_order函数

使用此函数前序遍历打印二叉树来验证其满足搜索树的性质

这里使用递归打印,所以要借助_in_order子函数

public:
    void in_order()
    {
        _in_order(_root);//传入根结点
        std::cout << std::endl;
    }
protected:
    void _in_order(Node* root)
    {
        if (root == nullptr) return;
        _in_order(root->_left);//先访问左结点
        std::cout << root->_value << " " << std::endl;//再访问根结点
        _in_order(root->_right);//最后访问右结点
    }

然后写一段测试代码测试性质

test.cpp

#include "BSTree.h"
#include <vector>
using namespace std;
int main()
{
	vector<int> arr({ 2,3,1,5,4,6,8,7 });//准备待插入的键值对
	key_value::BSTree<int, int> bst;
	for (int i = 0; i < arr.size(); ++i)
	{
		bst.insert(arr[i],arr[i]);//这里使键值相同,方便观察
	}

	bst.in_order();//测试

	return 0;
}

find函数

可以用find函数查找对应key的结点。

同时观察可知,控制curparent的移动的代码段和前面的函数很像,所以给find函数分出来一个_find子函数,并使它返回pair<Node*,Node*>,将这两个指针返回利于其它函数对_find的回调

同时为了简化代码,继续用typedef封装类型

public:
    typedef std::pair< Node*, Node* > PNN;//简化代码
    bool find(const K& key)
    {
        return _find(key).first != nullptr;//检查是否找到
    }
protected:

    PNN _find(const K& key)//返回PNN用于简化其它接口
    {
        if (_root == nullptr) return {nullptr,nullptr};
        Node* cur = _root;
        Node* parent = nullptr;
        while (cur)
        {
            if (key < cur->_key)//key比当前节点小,往左子树走
            {
                parent = cur;
                cur = cur->_left;
            }
            else if (key > cur->_key)//往右子树走
            {
                parent = cur;
                cur = cur->_right;
            }
            else
            {
                //找到key
                return { cur,parent };
            }
        }
        //没找到key,cur为nullptr
        return { nullptr,parent };
    }
重写insert函数
    bool insert(const K& key, const V& value)
    {
        if (_root == nullptr)//空树
        {
            _root = new Node(key, value);
            return true;
        }
        //====修改的部分====
        PNN pnn = _find(key);

        Node* cur = pnn.first;
        Node* parent = pnn.second;

        if (cur != nullptr)//该key已存在,插入失败
        {
            return false;
        }
        //================
        
        //此时cur为nullptr, parent为cur的结点
        if (key < parent->_key)parent->_left = new Node(key, value);
        else parent->_right = new Node(key, value);
        return true;//成功插入
    }

erase函数

这里也可以复用_find来方便地删除结点

这里要考虑的情况有:

  • 树为空
  • 删除最后一个结点
  • 删除根节点
  • 左子树为空(包括叶子结点)
  • 右子树为空
  • 删除一般的结点

当树有>=2个结点,且要删除非叶子结点时,要考虑结点替换,否则二叉树会断掉,这里一般两种策略,取左子树的最右结点(最大结点),或取右子树的最左结点(最小结点)

public:
    bool erase(const K& key)
    {
        if (_root == nullptr)return false;//空树无法删除
        if (_root->_key == key && _root->_left == nullptr && _root->_right == nullptr)//删除最后一个结点
        {
            delete _root;
            _root = nullptr;
        }
        PNN pnn = _find(key);
        Node* cur = pnn.first;
        Node* parent = pnn.second;
        if (cur == nullptr) return false;//没找到该结点

        //下面的cur必不为空
        if(cur->_left == nullptr)
        {
            if (cur == _root)//为根节点时要替换根节点
            {
                Node* right = _root->_right;
                delete _root;
                _root = right;
            }
            //没有左子树,则直接移除结点,右子树替代原结点
            if (cur == parent->_left) parent->_left = cur->_right;
            else parent->_right = cur->_right;
            delete cur;
        }
        else if (cur->_right == nullptr)
        {
            if (cur == _root)
            {
                Node* left = _root->_left;
                delete _root;
                _root = left;
            }
            //直接过继左子树
            if (cur == parent->_left)parent->_left = cur->_left;
            else parent->_right = cur->_left;
        }
        else 
        {
            //找到左子树的最右结点
            Node* _cur = cur->_left;
            Node* _parent = cur;
            while (_cur->_right != nullptr)
            {
                _parent = _cur;
                _cur = _cur->_right;
            }
            //移走结点
            if (_cur == _parent->_left) _parent->_left = nullptr;
            else _parent->_right = nullptr;
            //获取cur的左右结点,必须再移走结点后,否则可能出现指向自己的结点
            _cur->_left = cur->_left;
            _cur->_right = cur->_right;
            //代替cur的位置
            if (cur == _root)//删除根结点时
            {
                delete _root;
                _root = _cur;
            }
            else//删除普通结点时
            {
                if (cur == parent->_left) parent->_left = _cur;
                else parent->_right = _cur;
                delete cur;
            }

        }
        return true;
    }

拷贝构造

利用二叉树的性质,可以再构建个copy子函数来递归拷贝

public:
    BSTree(const BSTree<K>& t)
    {
        _root = Copy(t._root);
    }
protected:
    Node* copy(Node* root)
    {
        if (root == nullptr) return nullptr;//递归出口
        Node* pnode = new Node(root->_key, root->_value);//复制结点
        pnode->_left = copy(root->_left);
        pnode->_right = copy(root->_right);
        return pnode;
    }

析构函数

这里也用destroy子函数来递归地后序遍历依次删除各个结点

public:
    ~BSTree()
    {
        destroy(_root);
    }
protected:
    void destroy(Node* root)
    {
        if (root == nullptr) return;
        destroy(root->_left);
        destroy(root->_right);
        delete root;
    }

至此,一个基本的二叉搜索树已封装完成

实现的功能有

  • 构建二叉搜索树
  • 拷贝复制二叉树
  • key值查找
  • key值删除

完整代码

#pragma once
#include <iostream>
#include <utility>

namespace key_value
{
	template<class K,class V>
	class BSTNode
	{
	public:
		BSTNode(const K& key = K(), const V& value = V())
			:_left(nullptr)
			, _right(nullptr)
			, _key(key)
			, _value(value)
		{}
		BSTNode<K, V>* _left;//指向左子树
		BSTNode<K, V>* _right;//指向右子树
		K _key;//储存键
		V _value;//储存值
	};

	template<class K,class V>
	class BSTree
	{
		typedef BSTNode<K, V> Node;//使用typedef简化代码
	public:
		BSTree() :_root(nullptr) {}//提供默认构造函数

		BSTree(const BSTree<K, V>& bst)
		{
			_root = copy(bst._root);
		}

		~BSTree()
		{
			destroy(_root);
		}

		bool insert(const K& key, const V& value)
		{
			if (_root == nullptr)//空树
			{
				_root = new Node(key, value);
				return true;
			}
			//====修改的部分====
			PNN pnn = _find(key);

			Node* cur = pnn.first;
			Node* parent = pnn.second;

			if (cur != nullptr)//该key已存在,插入失败
			{
				return false;
			}
			//================
			
			//此时cur为nullptr, parent为cur的结点
			if (key < parent->_key)parent->_left = new Node(key, value);
			else parent->_right = new Node(key, value);
			return true;//成功插入
		}

		void in_order()
		{
			_in_order(_root);//传入根结点
			std::cout << std::endl;
		}
	protected:
		void _in_order(Node* root)
		{
			if (root == nullptr) return;
			_in_order(root->_left);//先访问左结点
			std::cout << root->_value << " ";//再访问根结点
			_in_order(root->_right);//最后访问右结点
		}


	public:
		typedef std::pair< Node*, Node* > PNN;//简化代码
		bool find(const K& key)
		{
			return _find(key).first != nullptr;//检查是否找到
		}
	protected:
	
		PNN _find(const K& key)//返回PNN用于简化其它接口
		{
			if (_root == nullptr) return {nullptr,nullptr};
			Node* cur = _root;
			Node* parent = nullptr;
			while (cur)
			{
				if (key < cur->_key)//key比当前节点小,往左子树走
				{
					parent = cur;
					cur = cur->_left;
				}
				else if (key > cur->_key)//往右子树走
				{
					parent = cur;
					cur = cur->_right;
				}
				else
				{
					//找到key
					return { cur,parent };
				}
			}
			//没找到key,cur为nullptr
			return { nullptr,parent };
		}
	public:

		bool erase(const K& key)
		{
			if (_root == nullptr)return false;//空树无法删除
			if (_root->_key == key && _root->_left == nullptr && _root->_right == nullptr)//删除最后一个结点
			{
				delete _root;
				_root = nullptr;
			}
			PNN pnn = _find(key);
			Node* cur = pnn.first;
			Node* parent = pnn.second;
			if (cur == nullptr) return false;//没找到该结点

			//下面的cur必不为空
			if(cur->_left == nullptr)
			{
				if (cur == _root)//为根节点时要替换根节点
				{
					Node* right = _root->_right;
					delete _root;
					_root = right;
				}
				//没有左子树,则直接移除结点,右子树替代原结点
				if (cur == parent->_left) parent->_left = cur->_right;
				else parent->_right = cur->_right;
				delete cur;
			}
			else if (cur->_right == nullptr)
			{
				if (cur == _root)
				{
					Node* left = _root->_left;
					delete _root;
					_root = left;
				}
				//直接过继左子树
				if (cur == parent->_left)parent->_left = cur->_left;
				else parent->_right = cur->_left;
			}
			else 
			{
				//找到左子树的最右结点
				Node* _cur = cur->_left;
				Node* _parent = cur;
				while (_cur->_right != nullptr)
				{
					_parent = _cur;
					_cur = _cur->_right;
				}
				//移走结点
				if (_cur == _parent->_left) _parent->_left = nullptr;
				else _parent->_right = nullptr;
				//获取cur的左右结点,必须再移走结点后,否则可能出现指向自己的结点
				_cur->_left = cur->_left;
				_cur->_right = cur->_right;
				//代替cur的位置
				if (cur == _root)//删除根结点时
				{
					delete _root;
					_root = _cur;
				}
				else//删除普通结点时
				{
					if (cur == parent->_left) parent->_left = _cur;
					else parent->_right = _cur;
					delete cur;
				}

			}
			return true;
		}


	protected:
		Node* copy(Node* root)
		{
			if (root == nullptr) return nullptr;//递归出口
			Node* pnode = new Node(root->_key, root->_value);//复制结点
			pnode->_left = copy(root->_left);
			pnode->_right = copy(root->_right);
			return pnode;
		}

		void destroy(Node* root)
		{
			if (root == nullptr) return;
			destroy(root->_left);
			destroy(root->_right);
			delete root;
		}
	private:
		Node* _root;//指向根节点的指针作为成员变量
	};
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值