目录
- 常见的查找数据结构和算法介绍
- 二叉查找树
- 资料
- 收获
一、常见的查找数据结构和算法介绍
1.1 链表(顺序查找)
针对少量的、无规则的数据,可以采用链表进行顺序查找
从头到尾依次逐个查找,直到找到所要的数据或搜索完整个数据序列。时间复杂度是O(n)
它的优点是插入比较快,但是查找比较慢。
1.2 有序数组(二分查找)
针对有序数组,可以采用二分查找法(折半查找法)
基本原理:
首先讲要查找的元素月数组的中间元素比较
定义三个遍历,最小值,最大值,中间值,其中中间值=(最小值+最大值)/2
如果key小于中间值,把最大值的下标移动到中间值的前一个
如果key大于中间值,把最小值的下标移动到中间值的后一个。
如果key和中间元素相等,匹配成功,结束查找。
一般情况下二分查找比顺序查找快很多,是很多实际应用中经常被采用。
有序数组的二分查找法的不足:插入操作非常慢,需要后插入位置后的都要向后移动一位。
1.3 二叉查找树
那么是否有同时保证查找、插入、删除操作效率都比较高的算法和数据结构呐?
要满足插入的高效性,首先能想到的是链表,但是链表无法使用 查找时高效的二分查找法。而二叉查找树将二分查找的效率和链表的灵活性结合起来。也是这篇我们学习实践的重点。
还有对二叉树进行优化的平衡二叉查找树以及散列表,我们将在后面几篇进行学习实践。
图片来源于《算法》
二、二叉查找树(BST)
通过上面一小节,我们了解到二叉查找树,结合了链表的灵活性以及数组的二分查找的高效性。这一小节,我们来了解二叉查找树数据结构,以及创建(插入)、查找(遍历)、删除过程。
先来看下二叉查找树的数据结构
二叉查找树使用的数据结构由结点
组成,每个结点只能有一个父结点(跟结点除外),每个结点包含两个链接(也可以为空)分别指向左子结点和右子结点。
我们先来定义二叉查找树一个结点的结构体
template
struct BSTNode
{
BSTNode(const T& key = T())
: _left(nullptr)
, _right(nullptr)
, _key(key)
{}
BSTNode* _left;
BSTNode* _right;
T _key;
};
2.1 插入(创建)
二叉查找树采用二叉树的中序遍历的方式进行创建插入。
如果树是空的,就返回一个包含该键值对的新结点,称为根结点。
如果被查找的键小于根结点的键,在左子树中插入该键,否则在右子树中插入。
具体实现如下
bool insertNode(const T& key)
{
//如果树为空,直接创建一个新结点进行插入
if (_root == nullptr)
{
_root = new Node(key);
return true;
}
//查找要插入的位置,从根结点作为比较的起始点
Node* cur = _root;
Node* parent = nullptr;
//通过下面这个while循环找到要插入的位置。也可以使用递归函数实现
while (cur)
{
//通过parent记录要出入结点的位置
parent = cur;
//要插入的值比当前结点值小,在其左子结点树上继续查找
if (key < cur->_key)
{
cur = cur->_left;
}
//否则,在其右子结点树上继续查找
else if (key >= cur->_key)
{
cur = cur->_right;
}
}
//插入元素
//生成新的结点
cur = new Node(key);
//和父节点进行比较,决定插入在左结点还是右结点
if (key < parent->_key)
{
parent->_left = cur;
}
else
{
parent->_right = cur;
}
return true;
}
2.2 查找(遍历)
从根结点开始查找,
具体实现如下
Node* find(const T& key)
{
Node* cur = _root;
//也可以改为递归的方式
//运用了高效的二分查找
while (cur)
{
if (cur->_key == key)
{
return cur;
}
else if (key < cur->_key)
{
cur = cur->_left;
}
else
{
cur = cur->_right;
}
}
return nullptr;
}
2.3 删除
删除结点分为以下三种情况
- 删除叶子结点、
- 删除有一个子树的结点;
- 删除有两个子树的结点。
这个比较复杂些。我们先把流程搞清楚,然后在通过代码实现
删除叶子结点
- 找到要删除的叶子结点 targetNode
- 找到targetNode的父节点parentNode
- 确定targetNode是parentNode的左子结点还是右子结点
- 进行删除 左子结点: parentNode.left = nullptr; 右子节点 parentNode.right = nullptr;
删除有一个子树的结点
这种情况要考虑,该结点的子结点是左子结点还是右子结点。以及该结点是其父节点的左子结点还是右子结点。具体步骤如下:
找到要删除的叶子结点 targetNode (同删除叶子结点第一步)
找到targetNode的父节点parentNode (同删除叶子结点第一步)
确定targetNode的子结点是左子结点还是右子结点
确定targetNode的是其parentNode结点的左子结点还是右子结点
-
如果targetNode的子结点是左子结点
如果targetNode是其父节点parentNode的左子结点
parentNode.left = targetNode.left如果targetNode是其父结点parentNode的右子结点
parentNode.right = targetNode.left -
如果targetNode的子结点是右子结点
如果targetNode是其父节点parentNode的左子结点
parentNode.left = targetNode.right如果targetNode是其父结点parentNode的右子结点
parentNode.right = targetNode.right
删除有两个子树的结点
删除拥有两个子树的结点后,左子树和右子树又该如何和原结点建立关系呐? 具体步骤如下
- 找到要删除的叶子结点 targetNode (同删除叶子结点第一步)
- 找到targetNode的父节点parentNode (同删除叶子结点第一步)
- 从tartgetNode的右子树中找到最小的结点(或者从targetNode的左子树中找到最大的结点)
- 用一个临时变量tmp存储该上一步找到结点的值
- 删除第3步找到的结点
- 把targetNode.key = tmp,即不改变链表的指向,只需要改变当前要删除结点的值即可。
梳理清楚上面的步骤后,我们看下通过代码如何实现结点的删除
bool delete(const T& key)
{
//如果树为空,删除失败
if (_root == nullptr)
{
return false;
}
//查找key在树中的位置
//targetNode的位置以及parentNode的位置
Node* cur = _root;
Node* parent = nullptr;
while (cur)
{
if (key == cur->_key)
{
break;
}
else if (key < cur->_key)
{
parent = cur;
cur = cur->_left;
}
else
{
parent = cur;
cur = cur->_right;
}
}
//遍历了整棵树,如果key不在树中,无法删除
if (cur == nullptr)
{
return false;
}
//如果在树中找到了key,进行删除结点,要分三种情况:
//1.该结点是叶子结点
//2.该结点只有左子结点或者右子结点
//3.该结点左右子树都存在
//情况1
if (cur->_left == nullptr && cur->_right == nullptr)
{
if(cur == parent->_left)
{
parent->_left = nullptr;
}else
{
parent->_right = nullptr;
}
}
//情况2
else if (cur->_left == nullptr)
{
//情况2.1:
//只有根结点和根的右孩子,此时要删除的结点正好是树的根
if (cur == _root)
{
_root = cur->_right;
}
else
{
//或该结点不是树的根
if (cur == parent->_left)
{
parent->_left = cur->_right;
}
else
{
parent->_right = cur->_right;
}
}
}
//情况2.2:
else if (cur->_right == nullptr)
{
if (cur == _root)
{
_root = cur->_left;
}
else
{
if (cur == parent->_right)
{
parent->_right = cur->_left;
}
else
{
parent->_left = cur->_left;
}
}
}
else
{
//当前结点左右孩子都存在,直接删除不好删,可以在其子树中找一个替代结点,比如找其左子树中的最大结点,即左子树中最右侧的结点,或者找其右子树中最小的结点,即右子树中最小的结点。替换结点找到后,将替代结点中的值交给待删除结点,转换成删除替代结点。
if (cur->_left != nullptr || cur->_right != nullptr)
{
//找右子树中最小的结点替换待删除的结点
Node* repalce = cur->_right;
parent = cur;
while (repalce->_left)
{
parent = repalce;
repalce = repalce->_left;
}
cur->_key = repalce->_key;
if (repalce == parent->_left)
{
parent->_left = repalce->_right;
}
else
{
parent->_right = repalce->_right;
}
delete repalce;
repalce = nullptr;
}
return true;
}
return false;
}
五、资料
《算法》
[【老九学堂】二分查找法] : https://www.bilibili.com/video/BV1LJ411X76n?from=search&seid=16165089659396424420
[【C语言描述】《数据结构和算法》(小甲鱼)-二叉排序树] : https://www.bilibili.com/video/BV1jW411K7yg?p=76
[尚硅谷Java数据结构与java算法(Java数据结构与算法)] : https://www.bilibili.com/video/BV1E4411H73v?p=132
[二叉搜索树的实现(C++)] : https://blog.csdn.net/tangya3158613488/article/details/89390232
六、 收获
- 了解查找算法几种方式(无序链表顺序查找、有序数组二分查找、二分查找树、平衡二叉查找树、散列表)
- 学习实践了二分查找树的新增结点、查找结点、以及删除结点的逻辑和实现。
感谢你的阅读
下一篇我们继续学习实践平衡二叉查找树,欢迎关注公众号“音视频开发之旅”,一起学习成长。
欢迎交流