C++ 二叉搜索树

目录

​编辑

0.前言

1.二叉搜索树的概念

1.1 二叉树的基本性质

1.2 二叉搜索树的性质

1.3 二叉搜索树的示例

2.二叉搜索树的操作

2.1查找节点

2.2插入节点

2.3删除节点

2.4遍历节点

3.二叉搜索树的实现

4.二叉搜索树的应用

4.1K模型

4.2KV模型

5.二叉搜索树性能分析

5.1 时间复杂度

5.2 空间复杂度

5.3 平衡二叉搜索树

6.结语


(图像由AI生成) 

0.前言

在之前的C语言系列博客中,我们已经介绍了二叉树的基本概念。二叉树是一种常见的数据结构,其每个节点最多有两个子节点,分别称为左子节点和右子节点。今天,我们将进一步探讨一种特殊的二叉树——二叉搜索树(Binary Search Tree,简称BST)。BST在计算机科学中有着广泛的应用,尤其是在需要快速查找、插入和删除操作的场景中。

1.二叉搜索树的概念

二叉搜索树(Binary Search Tree,简称BST)是一种特殊的二叉树,它在树节点的存储和操作上遵循特定的性质,使得查找、插入和删除操作更加高效。具体而言,二叉搜索树具有以下几个主要性质:

1.1 二叉树的基本性质

在讨论二叉搜索树之前,首先回顾一下二叉树的基本性质:

  • 每个节点最多有两个子节点,分别称为左子节点和右子节点。
  • 每个节点可以有零个、一或两个子节点。

1.2 二叉搜索树的性质

二叉搜索树在二叉树的基础上增加了以下性质:

  • 左子树的性质:对于二叉搜索树中的任意一个节点,其左子树中的所有节点的值都小于该节点的值。
  • 右子树的性质:对于二叉搜索树中的任意一个节点,其右子树中的所有节点的值都大于该节点的值。
  • 递归性质:每个节点的左子树和右子树也都是二叉搜索树。

这三条性质保证了二叉搜索树的有序性,使得在树中进行查找、插入和删除操作时可以利用二分查找的思想,从而提高操作效率。

1.3 二叉搜索树的示例

举一个简单的例子来说明二叉搜索树的性质:

        10
       /  \
      5    15
     / \   / \
    3   7 12  18

在这个二叉搜索树中:

  • 根节点的值是10。
  • 根节点的左子树中的节点值均小于10,右子树中的节点值均大于10。
  • 对于节点5,其左子树中的节点值均小于5,右子树中的节点值均大于5。同样,对于节点15,其左子树中的节点值均小于15,右子树中的节点值均大于15。
  • 这种性质在树中的每个节点处都成立。

2.二叉搜索树的操作

2.1查找节点

查找节点操作的目的是在树中找到一个与给定值匹配的节点。查找操作从根节点开始,根据节点的值与目标值的比较结果,决定在左子树或右子树中继续查找,直到找到目标节点或到达树的叶节点。

查找操作的步骤如下:

  1. 从根节点开始,初始化当前节点为根节点。
  2. 比较当前节点的数据与目标值:
    • 如果相等,则找到了目标节点。
    • 如果目标值小于当前节点的数据,则移动到左子树继续查找。
    • 如果目标值大于当前节点的数据,则移动到右子树继续查找。
  3. 重复上述步骤,直到找到目标节点或当前节点为空。

下面是查找节点操作的代码实现:

template<class T>
class BSTree
{
private:
    BSTNode<T>* root;
public:
    typedef BSTNode<T> Node;
    typedef BSTNode<T>* PNode;

    // 查找节点函数,根据给定的数据查找节点,返回指向该节点的指针
    PNode find(const T& data) const
    {
        PNode p = root;  // 从根节点开始查找
        while (p)
        {
            if (data == p->data)  // 找到匹配节点
                return p;
            else if (data < p->data)  // 数据小于当前节点数据,查找左子树
                p = p->left;
            else  // 数据大于当前节点数据,查找右子树
                p = p->right;
        }
        return nullptr;  // 未找到匹配节点,返回 nullptr
    }
};

2.2插入节点

插入节点操作的目的是在树中添加一个新节点。插入操作也从根节点开始,根据新节点的值与当前节点的值进行比较,决定将新节点插入到左子树或右子树中,直到找到一个合适的叶节点位置。

插入操作的步骤如下:

  1. 从根节点开始,初始化当前节点和父节点为根节点及其父节点(最初为空)。
  2. 比较当前节点的数据与新节点的值:
    • 如果新节点的值小于当前节点的数据,则移动到左子树继续查找插入位置。
    • 如果新节点的值大于当前节点的数据,则移动到右子树继续查找插入位置。
  3. 重复上述步骤,直到当前节点为空,即找到了插入位置。
  4. 根据父节点与新节点的值关系,决定将新节点插入为父节点的左子节点或右子节点。

下面是插入节点操作的代码实现:

template<class T>
class BSTree
{
private:
    BSTNode<T>* root;
public:
    typedef BSTNode<T> Node;
    typedef BSTNode<T>* PNode;

    // 插入节点函数,插入成功返回 true,否则返回 false
    bool insert(const T& data)
    {
        PNode p = root;  // 从根节点开始查找插入位置
        PNode pp = nullptr;  // 记录父节点指针
        while (p)
        {
            pp = p;
            if (data == p->data)  // 数据已存在,插入失败
                return false;
            else if (data < p->data)  // 数据小于当前节点数据,查找左子树
                p = p->left;
            else  // 数据大于当前节点数据,查找右子树
                p = p->right;
        }
        PNode newNode = new Node(data);  // 创建新节点
        if (pp == nullptr)  // 树为空,新节点为根节点
            root = newNode;
        else if (data < pp->data)  // 新节点为父节点的左子节点
            pp->left = newNode;
        else  // 新节点为父节点的右子节点
            pp->right = newNode;
        return true;  // 插入成功
    }
};

2.3删除节点

删除节点操作的目的是从树中移除一个指定值的节点。删除操作相对复杂一些,因为需要考虑删除节点后如何保持树的有序性。删除操作包括三种情况:

  1. 删除叶节点:直接删除该节点。
  2. 删除只有一个子节点的节点:用其子节点替代该节点。
  3. 删除有两个子节点的节点:找到该节点的中序后继(右子树中最小的节点)或中序前驱(左子树中最大的节点),用其替代该节点,然后删除该中序后继或中序前驱节点。

删除操作的步骤如下:

  1. 从根节点开始查找要删除的节点及其父节点。
  2. 根据要删除节点的子节点情况执行相应的删除操作:
    • 如果是叶节点,直接删除。
    • 如果只有一个子节点,用其子节点替代该节点。
    • 如果有两个子节点,用其右子树的最小节点替代该节点,然后删除该最小节点。

下面是删除节点操作的代码实现:

template<class T>
class BSTree
{
private:
    BSTNode<T>* root;
public:
    typedef BSTNode<T> Node;
    typedef BSTNode<T>* PNode;

    // 删除节点函数,删除成功返回 true,否则返回 false
    bool erase(const T& data)
    {
        PNode p = root;  // 从根节点开始查找要删除的节点
        PNode pp = nullptr;  // 记录父节点指针

        // 查找要删除的节点
        while (p && p->data != data)
        {
            pp = p;
            if (data < p->data)  // 数据小于当前节点数据,查找左子树
                p = p->left;
            else  // 数据大于当前节点数据,查找右子树
                p = p->right;
        }

        if (p == nullptr)  // 未找到要删除的节点
            return false;

        // 要删除的节点有两个子节点
        if (p->left && p->right)
        {
            PNode minP = p->right;  // 找到右子树中的最小节点
            PNode minPP = p;  // 记录最小节点的父节点指针
            while (minP->left)
            {
                minPP = minP;
                minP = minP->left;
            }
            p->data = minP->data;  // 用最小节点的数据替换要删除节点的数据
            p = minP;  // 重新标记要删除的节点
            pp = minPP;  // 更新父节点指针
        }

        // 要删除的节点是叶子节点或者仅有一个子节点
        PNode child = nullptr;  // 记录要删除节点的子节点指针
        if (p->left)
            child = p->left;
        else if (p->right)
            child = p->right;

        if (pp == nullptr)  // 要删除的节点是根节点
            root = child;
        else if (pp->left == p)  // 要删除的节点是父节点的左子节点
            pp->left = child;
        else  // 要删除的节点是父节点的右子节点
            pp->right = child;

        delete p;  // 释放要删除的节点
        return true;  // 删除成功
    }
};

2.4遍历节点

遍历节点是指按照一定的顺序访问树中的所有节点。常见的遍历方法包括前序遍历、中序遍历和后序遍历。在二叉搜索树中,中序遍历尤为重要,因为它可以按从小到大的顺序输出树中的所有节点值。

  • 前序遍历:先访问根节点,然后访问左子树,最后访问右子树。
  • 中序遍历:先访问左子树,然后访问根节点,最后访问右子树。
  • 后序遍历:先访问左子树,然后访问右子树,最后访问根节点。

下面是中序遍历操作的代码实现:

template<class T>
class BSTree
{
private:
    BSTNode<T>* root;
public:
    typedef BSTNode<T> Node;
    typedef BSTNode<T>* PNode;

    // 中序遍历函数,递归遍历树的节点并输出节点数据
    void inOrder(PNode p) const
    {
        if (p)
        {
            inOrder(p->left);  // 递归遍历左子树
            cout << p->data << " ";  // 输出节点数据
            inOrder(p->right);  // 递归遍历右子树
        }
    }
};

为了遍历整棵树,可以在主函数中调用 inOrder 函数,并传入根节点:

int main() {
    Key::BSTree<int> tree;
    tree.insert(5);
    tree.insert(3);
    tree.insert(7);
    tree.insert(2);
    tree.insert(4);
    tree.insert(6);
    tree.insert(8);

    cout << "Inorder traversal: ";
    tree.inOrder(tree.getRoot());  // 调用中序遍历
    cout << endl;

    return 0;
}

 输出结果:

Inorder traversal: 2 3 4 5 6 7 8

3.二叉搜索树的实现

下面是二叉搜索树(K模型)的基本实现代码:

namespace Key {
    // 定义模板结构体 BSTNode,用于表示二叉搜索树的节点
	template<class T>
	struct BSTNode
	{
		T data;  // 节点存储的数据
		BSTNode<T>* left;  // 指向左子节点的指针
		BSTNode<T>* right;  // 指向右子节点的指针
		
		// 构造函数,初始化节点数据和左右子节点指针
		BSTNode(const T& data = T()) : data(data), left(nullptr), right(nullptr) {}
	};

    // 定义模板类 BSTree,用于表示二叉搜索树
	template<class T>
	class BSTree
	{
	private:
		BSTNode<T>* root;  // 指向树根节点的指针

	public:
		typedef BSTNode<T> Node;  // 定义 Node 类型为 BSTNode<T>
		typedef BSTNode<T>* PNode;  // 定义 PNode 类型为指向 BSTNode<T> 的指针
		
		// 构造函数,初始化树根节点为 nullptr
		BSTree() :root(nullptr) {}
		// 析构函数
		~BSTree() {}

		// 获取树根节点
		PNode getRoot() const { return root; }

		// 中序遍历函数,递归遍历树的节点并输出节点数据
		void inOrder(PNode p) const
		{
			if (p)
			{
				inOrder(p->left);  // 递归遍历左子树
				cout << p->data << " ";  // 输出节点数据
				inOrder(p->right);  // 递归遍历右子树
			}
		}

		// 查找节点函数,根据给定的数据查找节点,返回指向该节点的指针
		PNode find(const T& data) const
		{
			PNode p = root;  // 从根节点开始查找
			while (p)
			{
				if (data == p->data)  // 找到匹配节点
					return p;
				else if (data < p->data)  // 数据小于当前节点数据,查找左子树
					p = p->left;
				else  // 数据大于当前节点数据,查找右子树
					p = p->right;
			}
			return nullptr;  // 未找到匹配节点,返回 nullptr
		}

		// 插入节点函数,插入成功返回 true,否则返回 false
		bool insert(const T& data)
		{
			PNode p = root;  // 从根节点开始查找插入位置
			PNode pp = nullptr;  // 记录父节点指针
			while (p)
			{
				pp = p;
				if (data == p->data)  // 数据已存在,插入失败
					return false;
				else if (data < p->data)  // 数据小于当前节点数据,查找左子树
					p = p->left;
				else  // 数据大于当前节点数据,查找右子树
					p = p->right;
			}
			PNode newNode = new Node(data);  // 创建新节点
			if (pp == nullptr)  // 树为空,新节点为根节点
				root = newNode;
			else if (data < pp->data)  // 新节点为父节点的左子节点
				pp->left = newNode;
			else  // 新节点为父节点的右子节点
				pp->right = newNode;
			return true;  // 插入成功
		}

		// 删除节点函数,删除成功返回 true,否则返回 false
		bool erase(const T& data)
		{
			PNode p = root;  // 从根节点开始查找要删除的节点
			PNode pp = nullptr;  // 记录父节点指针

			// 查找要删除的节点
			while (p && p->data != data)
			{
				pp = p;
				if (data < p->data)  // 数据小于当前节点数据,查找左子树
					p = p->left;
				else  // 数据大于当前节点数据,查找右子树
					p = p->right;
			}

			if (p == nullptr)  // 未找到要删除的节点
				return false;

			// 要删除的节点有两个子节点
			if (p->left && p->right)
			{
				PNode minP = p->right;  // 找到右子树中的最小节点
				PNode minPP = p;  // 记录最小节点的父节点指针
				while (minP->left)
				{
					minPP = minP;
					minP = minP->left;
				}
				p->data = minP->data;  // 用最小节点的数据替换要删除节点的数据
				p = minP;  // 重新标记要删除的节点
				pp = minPP;  // 更新父节点指针
			}

			// 要删除的节点是叶子节点或者仅有一个子节点
			PNode child = nullptr;  // 记录要删除节点的子节点指针
			if (p->left)
				child = p->left;
			else if (p->right)
				child = p->right;

			if (pp == nullptr)  // 要删除的节点是根节点
				root = child;
			else if (pp->left == p)  // 要删除的节点是父节点的左子节点
				pp->left = child;
			else  // 要删除的节点是父节点的右子节点
				pp->right = child;

			delete p;  // 释放要删除的节点
			return true;  // 删除成功
		}
	};
}

4.二叉搜索树的应用

二叉搜索树(BST)在计算机科学中有着广泛的应用,特别是在需要快速查找、插入和删除操作的场景中。根据应用需求的不同,二叉搜索树可以有多种变体,常见的包括K模型和KV模型。

4.1K模型

K模型是一种基于二叉搜索树的数据存储模型,在这种模型中,树的节点仅存储键(key),而不存储对应的值。K模型适用于只需要键而不需要存储对应值的场景。例如,在实现集合(Set)数据结构时,K模型是一种常见的选择。K模型的代码参考3.二叉搜索树的实现

应用场景

  1. 集合(Set):K模型可以用来实现集合数据结构,集合中的每个元素都是唯一的,常见的操作包括插入、删除和查找元素。
  2. 字典树(Trie):在一些字典树的变体中,可以使用K模型的二叉搜索树来存储单词的前缀,进行快速的前缀查找。

特点

  • 存储简单:每个节点只存储一个键,节省了空间。
  • 查找效率高:由于二叉搜索树的有序性,查找操作可以在O(log n)时间内完成(理想情况下)。

4.2KV模型

KV模型是一种扩展的二叉搜索树模型,在这种模型中,树的节点不仅存储键(key),还存储对应的值(value)。KV模型适用于键值对数据存储的场景,例如在实现映射(Map)数据结构时,KV模型非常有用。

应用场景

  1. 映射(Map):KV模型可以用来实现映射数据结构,其中每个键(key)都对应一个值(value),常见的操作包括插入键值对、删除键值对和查找键对应的值。
  2. 数据库索引:在数据库中,KV模型的二叉搜索树可以用来实现索引结构,加快数据的查找和检索速度。

特点

  • 键值存储:每个节点存储一个键值对,能够存储和检索更丰富的信息。
  • 灵活性高:可以在同一棵树中存储不同类型的键值对,适用于多种数据存储需求。

下面是KV模型的二叉搜索树的代码实现:

namespace KeyValue {
	template<class K, class V>
	struct BSTNode
	{
		K key;  // 节点存储的键
		V value;  // 节点存储的值
		BSTNode<K, V>* left;  // 指向左子节点的指针
		BSTNode<K, V>* right;  // 指向右子节点的指针

		// 构造函数,初始化节点键和值以及左右子节点指针
		BSTNode(const K& key = K(), const V& value = V()) : key(key), value(value), left(nullptr), right(nullptr) {}
	};

	template<class K, class V>
	class BSTree
	{
		typedef BSTNode<K, V> Node;  // 定义 Node 类型为 BSTNode<K, V>
		typedef BSTNode<K, V>* PNode;  // 定义 PNode 类型为指向 BSTNode<K, V> 的指针
	private:
		PNode root;  // 指向树根节点的指针
	public:
		BSTree() :root(nullptr) {}  // 构造函数,初始化树根节点为 nullptr
		~BSTree() {}  // 析构函数

		PNode getRoot() const { return root; }  // 获取树根节点

		// 中序遍历函数,递归遍历树的节点并输出节点键和值
		void inOrder(PNode p) const
		{
			if (p)
			{
				inOrder(p->left);  // 递归遍历左子树
				cout << p->key << "->" << p->value << " ";  // 输出节点键和值
				inOrder(p->right);  // 递归遍历右子树
			}
		}

		// 查找节点函数,根据给定的键查找节点,返回指向该节点的指针
		PNode find(const K& key) const
		{
			PNode p = root;  // 从根节点开始查找
			while (p)
			{
				if (key == p->key)  // 找到匹配节点
					return p;
				else if (key < p->key)  // 键小于当前节点键,查找左子树
					p = p->left;
				else  // 键大于当前节点键,查找右子树
					p = p->right;
			}
			return nullptr;  // 未找到匹配节点,返回 nullptr
		}

		// 插入节点函数,插入成功返回 true,否则返回 false
		bool insert(const K& key, const V& value)
		{
			PNode p = root;  // 从根节点开始查找插入位置
			PNode pp = nullptr;  // 记录父节点指针
			while (p)
			{
				pp = p;
				if (key == p->key)  // 键已存在,插入失败
					return false;
				else if (key < p->key)  // 键小于当前节点键,查找左子树
					p = p->left;
				else  // 键大于当前节点键,查找右子树
					p = p->right;
			}
			PNode newNode = new Node(key, value);  // 创建新节点
			if (pp == nullptr)  // 树为空,新节点为根节点
				root = newNode;
			else if (key < pp->key)  // 新节点为父节点的左子节点
				pp->left = newNode;
			else  // 新节点为父节点的右子节点
				pp->right = newNode;
			return true;  // 插入成功
		}

		// 删除节点函数,删除成功返回 true,否则返回 false
		bool erase(const K& key)
		{
			PNode p = root;  // 从根节点开始查找要删除的节点
			PNode pp = nullptr;  // 记录父节点指针

			// 查找要删除的节点
			while (p && p->key != key)
			{
				pp = p;
				if (key < p->key)  // 键小于当前节点键,查找左子树
					p = p->left;
				else  // 键大于当前节点键,查找右子树
					p = p->right;
			}

			if (p == nullptr)  // 未找到要删除的节点
				return false;

			// 要删除的节点有两个子节点
			if (p->left && p->right)
			{
				PNode minP = p->right;  // 找到右子树中的最小节点
				PNode minPP = p;  // 记录最小节点的父节点指针
				while (minP->left)
				{
					minPP = minP;
					minP = minP->left;
				}
				p->key = minP->key;  // 用最小节点的键替换要删除节点的键
				p->value = minP->value;  // 用最小节点的值替换要删除节点的值
				p = minP;  // 重新标记要删除的节点
				pp = minPP;  // 更新父节点指针
			}

			// 要删除的节点是叶子节点或者仅有一个子节点
			PNode child = nullptr;  // 记录要删除节点的子节点指针
			if (p->left)
				child = p->left;
			else if (p->right)
				child = p->right;

			if (pp == nullptr)  // 要删除的节点是根节点
				root = child;
			else if (pp->left == p)  // 要删除的节点是父节点的左子节点
				pp->left = child;
			else  // 要删除的节点是父节点的右子节点
				pp->right = child;

			delete p;  // 释放要删除的节点
			return true;  // 删除成功
		}
	};
}

5.二叉搜索树性能分析

二叉搜索树(BST)的性能分析主要关注其时间复杂度和空间复杂度。BST的性能与树的结构密切相关,树的高度(深度)是影响其操作效率的关键因素。本文将从平均情况和最坏情况两个方面分析BST的性能。

5.1 时间复杂度

BST的主要操作包括查找、插入和删除。它们的时间复杂度主要取决于树的高度h。

平均情况

在理想情况下(即树是平衡的),树的高度h与节点数n之间的关系为h = O(log n)。在这种情况下,查找、插入和删除操作的平均时间复杂度为O(log n)。这种情况通常出现在随机插入节点的情况下。

最坏情况

在最坏情况下(即树退化为一个链表),树的高度h与节点数n之间的关系为h = O(n)。在这种情况下,查找、插入和删除操作的时间复杂度为O(n)。这种情况通常出现在顺序插入节点的情况下。

各操作的时间复杂度总结

  • 查找操作:从根节点开始,逐层查找目标节点,时间复杂度为O(h)。
  • 插入操作:从根节点开始,逐层查找插入位置,时间复杂度为O(h)。
  • 删除操作:从根节点开始,逐层查找目标节点,执行删除操作,时间复杂度为O(h)。

5.2 空间复杂度

BST的空间复杂度主要取决于存储节点所需的内存。每个节点需要存储数据、左子节点指针和右子节点指针。因此,BST的空间复杂度为O(n),其中n为节点数。

5.3 平衡二叉搜索树

为了避免BST退化为链表,常常使用平衡二叉搜索树(如红黑树和AVL树)。这些树通过在插入和删除操作后调整树的结构,保证树的高度保持在O(log n),从而确保查找、插入和删除操作的时间复杂度为O(log n)。

红黑树

红黑树是一种自平衡的二叉搜索树,每个节点包含一个额外的颜色位(红色或黑色)。通过遵循一组严格的规则,红黑树在插入和删除操作后保持树的平衡,使得其高度始终为O(log n)。

AVL树

AVL树是另一种自平衡的二叉搜索树,它通过维护每个节点的平衡因子(即左子树高度与右子树高度之差),在插入和删除操作后调整树的结构,确保树的高度保持在O(log n)。

6.结语

二叉搜索树是一种功能强大且高效的数据结构,广泛应用于各种查找、插入和删除操作中。通过理解其基本概念和操作,并借助平衡二叉搜索树等优化技术,可以在实际应用中有效地提升性能。希望本文的介绍能够帮助你更好地理解和应用二叉搜索树,为你的编程实践提供有力支持。

  • 11
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值