【数据结构】二叉搜索树

article/2025/8/25 1:59:31

目录

一.二叉搜索树规则

二.框架

三.实现

1.查找, 插入, 删除

迭代法实现

递归法实现

2.构造, 赋值

默认构造

拷贝构造

赋值运算符重载

3.中序遍历

四.时间复杂度及稳定性

五.key模型与key/value模型

1.key模型

2.key/value模型


一.二叉搜索树规则

二叉搜索树又称二叉排序树

二叉搜索树的中序遍历结果是递增的

规则:

一棵二叉树, 可以为空, 如果不为空, 则满足:

1.非空左子树所有节点的键值小于根节点的键值

2.非空右子树所有节点的键值大于根节点的键值

3.每个左子树都满足规则1, 每个右子树都满足规则2, 即左右子树都是搜索二叉树

4.不会出现重复的键值

二.框架

采用二叉链

1. 二叉搜索树主要支持增删查等功能, 不支持修改, 因为使用的主要场景是搜索

2. 二叉搜索树自带去掉重复键值的功能, 因为重复键值不会被插入

template<class T>
struct BinaryTreeNode
{BinaryTreeNode* _left;BinaryTreeNode* _right;T _val;BinaryTreeNode(const T& val):_left(nullptr),_right(nullptr),_val(val){}
};template<class T>
class BinarySearchTree
{
public:typedef BinaryTreeNode<T> BTNode;BinarySearchTree();//拷贝构造(前序遍历拷贝)BinarySearchTree(BinarySearchTree& tmp);//赋值运算符重载BinarySearchTree& operator=(BinarySearchTree tmp);//插入(迭代法)bool insert(const T& val);//插入(递归法)bool insertR(const T& val);//中序打印(增序列)void inoder();//查找(迭代法)bool find(const T& val);//查找(递归法)bool findR(const T& val);//删除(迭代法)bool erase(const T& val);//删除(递归法)bool eraseR(const T& val);
private:BTNode* _BinarySearchTreeCopy(BTNode* root);bool _insertR(BTNode*& root, const T& val);bool _findR(BTNode* root, const T& val);bool _eraseR(BTNode*& root, const T& val);void _inoder(BTNode* root);BTNode* _root = nullptr;
};

三.实现

1.查找, 插入, 删除

迭代法实现

查找

bool find(const T& val);

从根节点开始查找, 如果val大于根节点的键值, 去根的右边找; 如果val小于根节点的键值, 去根的左边找

如果最终没找到, 返回false; 如果最终找到了, 返回true

bool find(const T& val)
{BTNode* cur = _root;while (cur){if (cur->_val > val){cur = cur->_left;}else if (cur->_val < val){cur = cur->_right;}else{return true;}}return false;
}

插入

bool insert(const T& val);

从根节点开始遍历, 如果要插入的节点比根节点键值大, 遍历根节点的右子树; 如果要插入的节点比根节点键值小, 遍历根节点的左子树

如果遍历到键值与val相等的节点, 则说明该节点的键值已经存在, 不需要插入, 返回false

如果遍历到空节点, 则可以进行插入了, 插入时要注意插入的位置是上一个节点的左还是右, 所以需要记录上一个节点, 同时需要一个if...else判断, 插入之后返回true

注意一个额外情况:

插入之前这是一棵空树, 说明_root为空, 应该单独处理, 否则将会对prev这个空指针进行访问引发崩溃

bool insert(const T& val)
{//new出新节点BTNode* newnode = new BTNode(val);//如果是空树if (_root == nullptr){_root = newnode;return true;}//树不为空BTNode* cur = _root;BTNode* prev = nullptr;while (cur){if (cur->_val > val){prev = cur;cur = cur->_left;}else if (cur->_val < val){prev = cur;cur = cur->_right;}else{return false;}}if (prev->_val < val){prev->_right = newnode;}else if (prev->_val > val){prev->_left = newnode;}return true;
}

删除

bool erase(const T& val);

删除是二叉搜索树中最繁杂的一个接口, 很容易出错, 需要额外小心

删除的前半部分思想与查找相同, 也需要记录待删除节点的上一个节点

当找到要删除的节点之后, 开始删除

删除分为四种情况:

1.待删除节点的左右子树都为空

2.待删除节点的左子树为空

3.待删除节点的右子树为空

4.待删除节点的左右子树都不为空

情况1可以归类到情况2, 左右都为空认为是左为空右不为空去处理, 最终prev链接右不为空节点, 但实际链接的是空节点

将1归类到2之后, 大体分为三种情况

1.左子树为空

2.右子树为空

3.左右子树都为空

 

bool erase(const T& val)
{BTNode* cur = _root;BTNode* prev = nullptr;while (cur){if (cur->_val > val){prev = cur;cur = cur->_left;}else if (cur->_val < val){prev = cur;cur = cur->_right;}else{//开始删除if (cur->_left == nullptr)//左为空{if (prev == nullptr)//删除根节点_root = cur->_right;else{if (prev->_val < val)prev->_right = cur->_right;elseprev->_left = cur->_right;}delete cur;}else if(cur->_right == nullptr)//右为空{if (prev == nullptr)//删除根节点_root = cur->_left;else{if (prev->_val < val)prev->_right = cur->_left;elseprev->_left = cur->_left;}delete cur;}else//左右都不为空{//交换法删除, 交换cur和cur右子树的最小节点BTNode* minPrev = cur;//这里需要注意BTNode* min = cur->_right;while (min->_left){minPrev = min;min = min->_left;}swap(cur->_val, min->_val);if (min == minPrev->_left)minPrev->_left = min->_right;else//min == minPrev->_rightminPrev->_right = min->_right;delete min;}return true;}}return false;
}

递归法实现

在思想上: 与迭代法一致

在写代码的角度: 与迭代法相比, 非常简洁, 可读性更强

由于每次接收参数以BTNode*& root的方式去接收, 即可不用进行繁杂的左右判断

插入则直接new出节点赋值给root即可

删除则直接赋值root = root->_right或者root = root->_left(1,2的情况)

情况3直接复用_eraseR即可复用到情况1(左为空), 最终也是走了一个root = root->right

对于删除节点是根节点也不需要单独做处理

查找

bool findR(const T& val)
{return _findR(_root, val);
}bool _findR(BTNode* root, const T& val)
{if (root == nullptr){return false;}if (root->_val < val){return _findR(root->_right, val);}else if (root->_val > val){return _findR(root->_left, val);}else{return true;}
}

插入

bool insertR(const T& val)
{return _insertR(_root, val);
}bool _insertR(BTNode*& root, const T& val)
{if (root == nullptr){root = new BTNode(val);return true;}if (root->_val > val){return _insertR(root->_left, val);}else if (root->_val < val){return _insertR(root->_right, val);}else{return false;}
}

删除

bool eraseR(const T& val)
{return _eraseR(_root, val);
}bool _eraseR(BTNode*& root, const T& val)
{if (root == nullptr){return false;}if (root->_val > val){return _eraseR(root->_left, val);}else if (root->_val < val){return _eraseR(root->_right, val);}else{//开始删除if (root->_left == nullptr){BTNode* cur = root;root = root->_right;delete cur;return true;}else if (root->_right == nullptr){BTNode* cur = root;root = root->_left;delete cur;return true;}else{BTNode* cur = root->_right;while (cur->_left){cur = cur->_left;}swap(cur->_val, root->_val);//注意对_eraseR复用时传参一定是root->_right, 因为此时已经交换了节点//交换了节点, 对于root而言, 删除掉节点前, root不再是一棵二叉搜索树//而交换对root->_right没有影响, 以root->_right为根节点的子树仍是一棵二叉搜索树return _eraseR(root->_right, cur->_val);}}
}

2.构造, 赋值

默认构造

采用C++11的新写法

注意: 这种写法通常用于, 我们显示的写了其他构造函数, 编译器就不会自动生成默认构造了, 这时需要我们手动去写默认构造, C++11提供了这种便捷的方式, 但是有一点需要注意的是, 如果你再使用默认构造实例化对象时如果不在成员变量声明处加缺省值, 会导致初始化的成员变量是随机值, 所以Node* _root = nullptr必须这样声明成员变量

BinarySearchTree() = default;

拷贝构造

拷贝构造的参数必须写成引用, 避免死循环

使用递归遍历依次拷贝

注意: 对于二叉搜索树而言, 插入的顺序不同, 构建出的二叉搜索树的形状就不同

所以我们在拷贝时, 必须严格遵守原树的形状去拷贝

绝不可以先遍历再依次插入, 必须在遍历的同时进行构建, 且这里必须使用前序遍历

BinarySearchTree(BinarySearchTree& tmp)
{_root = _BinarySearchTreeCopy(tmp._root);
}BTNode* _BinarySearchTreeCopy(BTNode* root)
{if (root == nullptr){return nullptr;}BTNode* copynode = new BTNode(root->_val);copynode->_left = _BinarySearchTreeCopy(root->_left);copynode->_right = _BinarySearchTreeCopy(root->_right);return copynode;
}

赋值运算符重载

复用拷贝构造

BinarySearchTree& operator=(BinarySearchTree tmp)
{swap(tmp._root, _root);return *this;
}

3.中序遍历

void inoder()
{_inoder(this->_root);cout << endl;
}void _inoder(BTNode* root)
{if (root != nullptr){_inoder(root->_left);cout << root->_val << ' ';_inoder(root->_right);}
}

四.时间复杂度及稳定性

二叉搜索树是一种极其不稳定的数据结构

二叉搜索树的插入与删除都是在查找基础之上的操作, 所以它的查找效率代表了二叉搜索树的整体性能

查找效率:

时间复杂度是O(h) -- h是树的高度

最优查找效率: O(logN) -- 与完全二叉树非常类似

最坏查找效率: O(N) -- 单枝树

因为二叉搜索树的高度不定, 所以使其非常不稳定

五.key模型与key/value模型

1.key模型

key模型: 查找某个值在不在的场景, key不可被修改

2.key/value模型

key/value模型: 查找且匹配的场景, 根据key(键值)找到value, key不可被修改, value可以被修改


http://chatgpt.dhexx.cn/article/lbidCnWc.shtml

相关文章

数据结构——二叉搜索树详解

二叉搜索树 一、什么是二叉搜索树 二叉搜索树&#xff08;BST&#xff0c;Binary Search Tree&#xff09;&#xff0c;也称二叉排序树或二叉查找树。 二叉搜索树&#xff1a;一棵二叉树&#xff0c;可以为空&#xff1b;如果不为空&#xff0c;满足以下性质&#xff1a; 非空…

【数据结构】——二叉搜索树

目录 前言 二叉搜索树的概念 二叉搜索树的操作 树的节点实现 搜索树的基本结构 插入数据 查找 删除 拷贝构造函数 二叉搜索树的应用 前言 在c中的容器里map和set的学习需要二叉搜索树的铺垫&#xff0c;也为后边的的红黑树和AVL树做铺垫&#xff0c;也就是说&#xff0c;…

二叉搜索树(二叉排序树)

一.概念 二叉搜索树又称二叉排序树&#xff0c;具有以下性质&#xff1a; 若它的左子树不为空&#xff0c;则左子树上所有节点的值都小于根节点的值若它的右子树不为空&#xff0c;则右子树上所有节点的值都大于根节点的值它的左右子树也分别为二叉搜索树 注意&#xff1a;二…

二叉排序树(二叉查找树、二叉搜索树)(图解+完整代码)

目录 ⚽1.什么是二叉排序树 &#x1f3d0;2.构建二叉排序树 &#x1f3c0;3.二叉排序树的查找操作 &#x1f94e;4.二叉排序树的删除 &#x1f3b1;5.完整代码 ⚽1.什么是二叉排序树 我们直接看它的性质&#xff1a; 若它的左子树不空&#xff0c;则左子树上所有结点的值…

种树:二叉树、二叉搜索树、AVL树、红黑树、哈夫曼树、B树、树与森林

虽然今天不是植树节&#xff0c;但是我今天想种树。 文章目录 树&#xff0c;什么是树&#xff1f;二叉树定义二叉树的创建二叉树的前中后序遍历前序遍历&#xff1a;中序遍历后序遍历已知前序、中序遍历结果&#xff0c;还原二叉树已知后序、中序遍历结果&#xff0c;还原二叉…

java lrucache_Java LruCache 的使用及原理

概述 LRU (Least Recently Used) 的意思就是近期最少使用算法&#xff0c;它的核心思想就是会优先淘汰那些近期最少使用的缓存对象。 在我们日常开发中&#xff0c;UI 界面进行网络图片加载是很正常的一件事情&#xff0c;但是当界面上的图片过于多的时候&#xff0c;不可能每次…

LruCache和DiskLruCache

前言 Android中的三级缓存主要就是内存缓存和硬盘缓存。 Lru(least recently used)意为最近最少使用算法&#xff0c;核心思想就是当缓存满时&#xff0c;会优先淘汰最近最少使用的缓存对象。 LruCache的使用 在Android中可以直接使用LruCache&#xff0c;算法原理是&#xff1…

Android LruCache 缓存

使用场景 1、场景一&#xff1a;图片缓存利器。 可以规定缓存大小、有效避免OOM、自动移除队尾不用的图片缓存、避免HashMap各种问题。 2、场景二&#xff1a;通信缓存 从服务端需要获取数据&#xff0c;但是当访问的数据比较大&#xff0c;比较多&#xff0c;并且是重复数…

Java——LRUCache

概念 简单来说&#xff0c;由于我们的空间是有限的&#xff0c;所以发明了这个数据结构&#xff0c;当我们的空间不够添加新的元素时&#xff0c;就会删除最近最少使用的元素。 其底层逻辑通过哈希表和链表共同实现。哈希表中存储链表的每一个元素&#xff0c;方便进行元素的…

LruCache缓存

Lru算法&#xff1a; Lru 指的是“Least Recently Used-近期最少使用算法”。 1、那么LruCache到底是什么呢&#xff1f; LruCache 是对限定数量的缓存对象持有强引用的缓存&#xff0c;每一次缓存对象被访问&#xff0c;都会被移动到队列的头部。当有对象要被添加到已经达到数…

LruCache 源码解析

1. 概述 对于 Android 开发者&#xff0c;LruCache 肯定不陌生&#xff0c;几乎所有的图片缓存框架都会用到它来实现内存缓存等&#xff0c;可见 LruCache 在 Android 开发中的重要性。LRU 是 Least Recently Used 的缩写&#xff0c;近期最少使用的意思。当我们进行缓存的时候…

LruCache

LruCache这个类是通过Glide得知的&#xff0c;不过它是自己又基于LRU算法自己写了个LruCache工具类&#xff0c;不过基本原理类似&#xff0c;都是基于LRU算法实现的 1.来源 一般来说&#xff0c;缓存策略主要包含缓存的添加、获取和删除这三类操作。如何添加和获取缓存这个比…

Android基础-LruCache原理解析

一、Android中的缓存策略 一般来说&#xff0c;缓存策略主要包含缓存的添加、获取和删除这三类操作。如何添加和获取缓存这个比较好理解&#xff0c;那么为什么还要删除缓存呢&#xff1f;这是因为不管是内存缓存还是硬盘缓存&#xff0c;它们的缓存大小都是有限的。当缓存满了…

LRUCache详解

1.概念 LRU是Least Recently Used的缩写&#xff0c;意思是最近最少使用&#xff0c;它是一种Cache替换算法。 Cache的容量有限&#xff0c;因此当Cache的容量用完后&#xff0c;而又有新的内容需要添加进来时&#xff0c; 就需要挑选并舍弃原有的部分内容&#xff0c;从而腾出…

LRUCache 详解

LRU算法详解 一、什么是 LRU 算法 就是一种缓存淘汰策略。 计算机的缓存容量有限&#xff0c;如果缓存满了就要删除一些内容&#xff0c;给新内容腾位置。但问题是&#xff0c;删除哪些内容呢&#xff1f;我们肯定希望删掉哪些没什么用的缓存&#xff0c;而把有用的数据继续…

LRU Cache

前言 哈喽&#xff0c;各位小伙伴大家好&#xff0c;本章内容为大家介绍计算机当中为了提高数据相互传输时的效率而引进的一种重要设计结构叫做LRU Cache,下面将为大家详细介绍什么是LRU Cache,以及它是如何是实现的&#xff0c;如何提升效率的。 1.什么是LRU Cache? LRU是L…

uniapp日历原生插件

<template><!-- 打卡日历页面 --><view classall><view class"bar"><!-- 上一个月 --><view class"previous" click"handleCalendar(0)"><button class"barbtn">上一月</button><…

微信小程序使用日历插件

一&#xff0c;添加插件 1&#xff0c;在你的小程序关联的微信公众平台打开 设置》第三方服务》添加插件 2&#xff0c;直接AppID&#xff08;wx92c68dae5a8bb046&#xff09;搜索到该插件并申请授权&#xff0c;授权成功即可在小程序使用 二&#xff0c;小程序使用插件 app…

日历组件

日历组件&#xff1a; <template><div class"calendar" click.stop><div class"input-wrap"><inputtype"text"v-if"dateChangeSets":placeholder"placeholder"class"input dateChangeSets middle…

vue-calendar基于vue的日历插件

本文转载于https://www.cnblogs.com/zwhgithub/p/8005414.html vue-calendar-component 基于 vue 2.0 开发的轻量&#xff0c;高性能日历组件占用内存小&#xff0c;性能好&#xff0c;样式好看&#xff0c;可扩展性强原生 js 开发&#xff0c;没引入第三方库 效果 Install …