二叉搜索树是一种特别有用的数据结构,AVL树,红黑树的原型都是二叉搜索树。本文将会对二叉搜索树进行初步介绍,从而入门二叉搜索树,为以后深入学习AVL树和红黑树打下基础。
二叉搜索树又称二叉排序树,它或者是一棵空树,或者是具有以下性质的二叉树:若它的左子树不为空,则左子树上所有节点的值都小于根节点的值。若它的右子树不为空,则右子树上所有节点的值都大于根节点的值,它的左右子树也分别为二叉搜索树。
根据二叉搜索的特性我们知道左子树都比根节点小,右子树都比根节点大。
因此我们中序遍历这颗树就是顺序遍历数据。C++中基于二叉搜索树实现的容器在遍历数据的时候都是采用中序遍历。
知道了二叉树搜索的特性,我们就来实现二叉搜索树。我们先把树的结构和节点结构定义出来。
template<class K>
struct Node
{
Node(const K& val)
:_left(nullptr),_right(nullptr),_key(val)
{
;
}
Node<K>* _left;
Node<K>*_right;
K _key;
};
template<class K>
class BST
{
public:
typedef Node<K> node;
private:
node* _root;
}
节点中key是用来存储数据的,leftl指针和righ指针是用来连接左右子树的,因为我们是用C++实现的,因此节点先定义出节点的结构体,之后在创建出二叉搜索树的结构体,这里为了方便就把节点类型重定义了一下。
类模板中Node只是类名,类型是Node< k >.
我们知道二叉搜索的特性,二叉搜索树主要是用来搜索的,因此一般来说二叉树中不允许出现重复的数据。根据二叉搜索树的特性,我们知道当插入的key比当前节点的key大就走右子树,如果插入的key比当前节点key小就走左子树,如果相等就插入失败。
bool Insert(const K& val)
{
if (_root == nullptr)
{
_root = new node(val);
return true;
}
node* cur = _root;
node* parent = nullptr;
while (cur)
{
if (cur->_key < val)
{
parent = cur;
cur = cur->_right;
}
else if (cur->_key > val)
{
parent = cur;
cur = cur->_left;
}
else
{
return false;
}
}
cur = new node(val);
if (val < parent->_key)
{
parent->_left = cur;
}
else
{
parent->_right = cur;
}
return true;
}
我们通过一个cur节点一路比较key值遍历,根据比较结果选择遍历路径走到空的时候,这个时候就该链接节点了,为了连接上节点,我们一路遍历的时候需要用一个paent节点记录cur的父节点,最后根据parents的key与插入的key的大小关系选择链接到parents的哪个左右指针上。
上述代码核心在于根据大小关系选择要插入的左右子树,while循环就是干这件事,插入的数据为了能够链接上,就需要parents节点。
递归实现插入数据
//注意这里的root参数是引用这是一个技巧
bool _InsertR(node*& root, const K& val)
{
if (root == nullptr)
{
root = new node(val);
return true;
}
if (val > root->_key)
{
_InsertR(root->_right, val);
}
else if (val < root->_key)
{
_InsertR(root->_left, val);
}
else
{
return false;
}
}
bool InsertR(const K&val)
{
return _InsertR(_root,val);
}
在使用的递归的时候,一般都是根据树的节点来访问树,在类中可以天然的访问到root节点,因此我们写个子函数,插入函数调用这个子函数即可。
要递归子树所以这个参数肯定是不能省略的,可能有人会说可以给个_root缺省值,但是这也是不行的,缺省值只能是常量和全局变量,而将_root设为缺省值需要用到this指针。所以这个子函数是最好的解决方法。
注意这里的参数是是引用,这样就能能把每个节点顺利链接上了,这是一个细节技巧
二叉搜索树的查找实现也很简单和插入类似,如果要查找的值比当前节点的key大就去当节点的右树去查找,如果要查找的值比当前节点的key小就去当前节点左树去查找。如果相等就说明找到了,这样不断比较遍历即可。
非递归实现二叉搜索树查找
bool Find(const K& val)
{
node* cur = _root;
while (cur)
{
if (cur->_key > val)
{
cur = cur->_left;
}
else if (cur->_key < val)
{
cur = cur->_right;
}
else
{
return true;
}
}
return false;
}
我们再来看看二叉搜索树的递归实现查找
bool _FindR( node*& root, const K& val)
{
if (root == nullptr)
{
return false;
}
if (root->_key == val)
{
return true;
}
else if (root->_key > val)
{
return _FindR(root->_left, val);
}
else
{
return _FindR(root->_right, val);
}
return false;
}
bool FindR(const K& val)
{
return _FindR(_root, val);
}
这里递归也很简单,val比当前节点的key小就递归左树查找,val比当前节点的key大就递归右树查找。
二叉搜索树删除节点的实现才是二叉树搜索最重要的点,这也是二叉搜索树接口中最难实现的一个点。我们来分析一下二叉搜索树删除节点的几种情况。
二叉树搜索节点的删除难点在于该节点可能有孩子节点,删除这个节点后,它的孩子节点应该怎么妥善处理才能删除后的树依然是一颗二叉搜索树。
其实,通过上面的分析删除节点可以分为两大类,该节点有只有一个孩子和该节点只有两个节点。该节点没有孩子都可以采用该节点只有一个孩子的处理方式,即托孤处理。如果当前节点是在它父节点的左树上,那么对于当前节点的父节点来说,唯一的孙子的节点一定是要链接在爷爷节点的左树上的。如果当前节点是在它父节点的右树上,那么对于当前节点的父节点来说,唯一的孙子的节点一定是要链接在爷爷节点的右树上的。
这就是托孤处理,让爷爷节点来管理孙子节点。
如果要删除的节点有两个节点怎么办呢?我们只用在该节点左右子树选择一个适合的节点的key替代要删除节点的key,之后在删除这个适合的节点节点即可。
这个合适的节点是要删除节点的左子树中的最右节点或者右子树中最左节点。因为该节点是最右节点或者最左节点因此它肯定是没有孩子节点,所以直接删除即可。不用在处理孩子节点了。这是一种替代删除法
代码示例
bool erase(const K& val)
{ //删除节点要先找到将要删除的节点
node* parent = nullptr;
node* cur = _root;
if (_root == nullptr)
{
//cout << "Tree NULL" << endl;
return false;
}
while (cur)
{
if (cur->_key > val)
{
parent = cur;
cur = cur->_left;
}
else if (cur->_key < val)
{
parent = cur;
cur = cur->_right;
}
else
{
//找到节点开始删除
//分为3种大情况 cur 左为空 右为空 或者左右都为空
//1.左为空
if (cur->_left == nullptr)
{ //如果删除的是根节点单独处理
if (cur == _root)
{
_root = cur->_right;
delete cur;
return true;
}
else
{
//判断cur是parent的右孩子还是右孩子
if (parent->_left == cur)
{
parent->_left = cur->_right;
delete cur;
return true;
}
else
{
parent->_right = cur->_right;
delete cur;
return true;
}
}
}
//2.右为空
else if (cur->_right == nullptr)
{ //1.cur为根节点单独判断
if (cur == _root)
{
_root = cur->_left;
delete cur;
return true;
}
else
{
if (parent->_left == cur)
{
parent->_left = cur->_left;
delete cur;
return true;
}
else
{
parent->_right = cur->_left;
delete cur;
return true;
}
}
}
//3.左右都不为空
else
{
//找当前节点的左树的最右节点或者右数的最左节点进行替换
/*注意不能这样写,
如果cur->_left就是左树的的最右节点就会有问题
node pmaxleft=nullptr;
* node maxleft=cur->_left;
*/
node* cur_parent = cur;
node* left_max = cur->_left;
while (left_max->_right)
{
cur_parent = left_max;
left_max = left_max->_right;
}
//替换key值
cur->_key = left_max->_key;
if (cur_parent->_left == left_max)
{
cur_parent->_left = left_max->_left;
}
else
{
cur_parent->_right = left_max->_left;
}
delete left_max;
return true;
}
}
}
return false;
}
先找到要删除的节点,在找节点的过程中需要记录该节点的父节点。找到以后在判断该节点的孩子节点哪一个为空,对于第一种情况:
左为空,我们需要链接右孩子,右为空,我们需要链接左孩子,在链接的时候还需要判断爷爷节点和父亲节点的左右关系,才能确定这个孙子节点和爷爷节点链接关系,这里需要单独处理一下参删除的节点是根节点的情况。
对于第二种情况:上述代码中我是找左树的最右节点来进行cur节点key值的替换。左树的最右节点是左子树中的最大节点,右树的最左节点就是右树的最小节点,这两个节点的key刚好符合根节点key的特性。这样删除之后这颗树依旧是一颗二叉搜索树。
那我们再来看看这个删除节点递归实现
递归实现删除节点
bool EraseR(const K& val)
{
return _EraseR(_root, val);
}
bool _EraseR(node*& root, const K& val)
{
if (root == nullptr)
{
//cout << "Tree NULL" << endl;
return false;
}
if (root->_key > val)
{
_EraseR(root->_left, val);
}
else if (root->_key < val)
{
_EraseR(root->_right, val);
}
else
{
node* del = root;
if (root->_left == nullptr)
{
root = root->_right;
}
else if (root->_right == nullptr)
{
root = root->_left;
}
else
{
node* maxleft = root->_left;
while (maxleft->_right)
{
maxleft = maxleft->_right;
}
swap(maxleft->_key, del->_key);
return _EraseR(root->_left, val);
}
delete del;
return true;
}
return false;
}
这里递归的时候引用起了很大的作用。
这里我们就不用parents指针了,就很方便。当要删除的节点左右孩子都不为空时,还是使用替换删除的方法,这里依旧是找左子树的最右节点的key值作为根节点新的key值,不过这这里是交换key值,之后在递归到要删除节点的左子树中进行删除,这样一定会遇到要删除的节点是左右孩子为空的情况,从而实现了正确的递归删除。
这里默认的构造函数直接将根节点初始化为nullptr即可,我们主要来实现一下这个拷贝构造。
BST(const BST<K>& t)
{
_root = Copy(t._root);
}
node* Copy( node* root)
{
if (root == nullptr)
{
return nullptr;
}
node* newroot = new node(root->_key);
newroot->_left = Copy(root->_left);
newroot->_right = Copy(root->_right);
return newroot;
}
BST()
:_root(nullptr)
{
}
这里拷贝构造调用capy函数直接递归复制创建一颗二叉树即可。
赋值重载
BST<K> operator=(const BST<K> t)
{
swap(_root, t._root);
return *this;
}
赋值重载的话还是和以前的方法我们直接使swap函数交换两棵树的根节点即可,这里swap函数是传值传参,会调用拷贝构造函数创建出一个t,并不会影响到这个实际的赋值对象。
析构函数
~BST()
{
Destroy(_root);
_root = nullptr;
}
void Destroy(node* root)
{
if (root == nullptr)
{
return;
}
Destroy(root->_left);
Destroy(root->_right);
delete root;
}
这里析构函数调用Destroy函数也是使用递归进行节点空间的释放,当然这里参数也可以设设置引用,在Destroy函数中将根节点置空,这里要注意需要采用后序遍历方式进行删除,也就是自底向上删除节点。
完整代码
#include
using namespace std;
template<class K>
struct Node
{
Node(const K& val)
:_left(nullptr),_right(nullptr),_key(val)
{
;
}
Node<K>* _left;
Node<K>*_right;
K _key;
};
template<class K>
class BST
{
public:
typedef Node<K> node;
bool Find(const K& val)
{
node* cur = _root;
while (cur)
{
if (cur->_key > val)
{
cur = cur->_left;
}
else if (cur->_key < val)
{
cur = cur->_right;
}
else
{
return true;
}
}
return false;
}
void _Inorder(node*root)
{
if (root == nullptr)
{
return;
}
_Inorder(root->_left);
cout << root->_key << " ";
_Inorder(root->_right);
}
void Inorder()
{
if (_root == nullptr)
{
cout << "Tree NULL" << endl;
return;
}
_Inorder(_root);
cout << endl;
}
bool Insert(const K& val)
{
if (_root == nullptr)
{
_root = new node(val);
return true;
}
node* cur = _root;
node* parent = nullptr;
while (cur)
{
if (cur->_key < val)
{
parent = cur;
cur = cur->_right;
}
else if (cur->_key > val)
{
parent = cur;
cur = cur->_left;
}
else
{
return false;
}
}
cur = new node(val);
if (val < parent->_key)
{
parent->_left = cur;
}
else
{
parent->_right = cur;
}
return true;
}
bool erase(const K& val)
{ //删除节点要先找到将要删除的节点
node* parent = nullptr;
node* cur = _root;
if (_root == nullptr)
{
//cout << "Tree NULL" << endl;
return false;
}
while (cur)
{
if (cur->_key > val)
{
parent = cur;
cur = cur->_left;
}
else if (cur->_key < val)
{
parent = cur;
cur = cur->_right;
}
else
{
//找到节点开始删除
//分为3种大情况 cur 左为空 右为空 或者左右都为空
//1.左为空
if (cur->_left == nullptr)
{ //如果删除的是根节点单独处理
if (cur == _root)
{
_root = cur->_right;
delete cur;
return true;
}
else
{
//判断cur是parent的右孩子还是右孩子
if (parent->_left == cur)
{
parent->_left = cur->_right;
delete cur;
return true;
}
else
{
parent->_right = cur->_right;
delete cur;
return true;
}
}
}
//2.右为空
else if (cur->_right == nullptr)
{ //1.cur为根节点单独判断
if (cur == _root)
{
_root = cur->_left;
delete cur;
return true;
}
else
{
if (parent->_left == cur)
{
parent->_left = cur->_left;
delete cur;
return true;
}
else
{
parent->_right = cur->_left;
delete cur;
return true;
}
}
}
//3.左右都不为空
else
{
//找当前节点的左树的最右节点或者右数的最左节点进行替换
/*注意不能这样写,
如果cur->_left就是左树的的最右节点就会有问题
node pmaxleft=nullptr;
* node maxleft=cur->_left;
*/
node* cur_parent = cur;
node* left_max = cur->_left;
while (left_max->_right)
{
cur_parent = left_max;
left_max = left_max->_right;
}
cur->_key = left_max->_key;
if (cur_parent->_left == left_max)
{
cur_parent->_left = left_max->_left;
}
else
{
cur_parent->_right = left_max->_left;
}
delete left_max;
return true;
}
}
}
return false;
}
//注意这里的root参数是引用这是一个技巧
bool _InsertR(node*& root, const K& val)
{
if (root == nullptr)
{
root = new node(val);
return true;
}
if (val > root->_key)
{
_InsertR(root->_right, val);
}
else if (val < root->_key)
{
_InsertR(root->_left, val);
}
else
{
return false;
}
}
bool InsertR(const K&val)
{
return _InsertR(_root,val);
}
bool _FindR( node*& root, const K& val)
{
if (root == nullptr)
{
return false;
}
if (root->_key == val)
{
return true;
}
else if (root->_key > val)
{
return _FindR(root->_left, val);
}
else
{
return _FindR(root->_right, val);
}
return false;
}
bool FindR(const K& val)
{
return _FindR(_root, val);
}
bool EraseR(const K& val)
{
return _EraseR(_root, val);
}
bool _EraseR(node*& root, const K& val)
{
if (root == nullptr)
{
//cout << "Tree NULL" << endl;
return false;
}
if (root->_key > val)
{
_EraseR(root->_left, val);
}
else if (root->_key < val)
{
_EraseR(root->_right, val);
}
else
{
node* del = root;
if (root->_left == nullptr)
{
root = root->_right;
}
else if (root->_right == nullptr)
{
root = root->_left;
}
else
{
node* maxleft = root->_left;
while (maxleft->_right)
{
maxleft = maxleft->_right;
}
swap(maxleft->_key, del->_key);
return _EraseR(root->_left, val);
}
delete del;
return true;
}
return false;
}
void Destroy(node* root)
{
if (root == nullptr)
{
return;
}
Destroy(root->_left);
Destroy(root->_right);
delete root;
}
~BST()
{
Destroy(_root);
_root = nullptr;
}
BST(const BST<K>& t)
{
_root = Copy(t._root);
}
BST<K> operator=(const BST<K> t)
{
swap(_root, t._root);
return *this;
}
node* Copy( node* root)
{
if (root == nullptr)
{
return nullptr;
}
node* newroot = new node(root->_key);
newroot->_left = Copy(root->_left);
newroot->_right = Copy(root->_right);
return newroot;
}
BST()
:_root(nullptr)
{
}
private:
node* _root=nullptr;
};
听名字就知道二叉搜索树的主要引用场景是用来搜索,这里二叉搜索树主要有两种模型。
1. K模型:K模型即只有key作为关键码,结构中只需要存储Key即可,关键码即为需要搜索到的值。比如:给一个单词word,判断该单词是否拼写正确,具体方式如下:以词库中所有单词集合中的每个单词作为key,构建一棵二叉搜索树,在二叉搜索树中检索该单词是否存在,存在则拼写正确,不存在则拼写错误。
这种模型就是用来处理在不在的问题。
2. KV模型:每一个关键码key,都有与之对应的值Value,即
的键值对。该种方式在现实生活中非常常见:比如英汉词典就是英文与中文的对应关系,通过英文可以快速找到与其对应的中文,英文单词与其对应的中文 就构成一种键值对;再比如统计单词次数,统计成功后,给定单词就可快速找到其出现的次数,单词与其出现次数就是 就构成一种键值对。 这种模型主要是用来通过一个值查找另一个值的。
namespace key_val
{
template<class K,class V>
struct Node
{
Node(const K& key,const V&val)
:_left(nullptr), _right(nullptr), _key(key),_value(val)
{
;
}
Node<K,V>* _left;
Node<K,V>* _right;
K _key;
V _value;
};
template<class K,class V>
class BST
{
public:
typedef Node<K,V> node;
node* Find(const K& key)
{
node* cur = _root;
while (cur)
{
if (cur->_key > key)
{
cur = cur->_left;
}
else if (cur->_key < key)
{
cur = cur->_right;
}
else
{
return cur;
}
}
return nullptr;
}
void _Inorder(node* root)
{
if (root == nullptr)
{
return;
}
_Inorder(root->_left);
cout << root->_key << " " << root->_value << endl;
_Inorder(root->_right);
}
void Inorder()
{
if (_root == nullptr)
{
cout << "Tree NULL" << endl;
return;
}
_Inorder(_root);
cout << endl;
}
bool Insert(const K& key,const V&val)
{
if (_root == nullptr)
{
_root = new node(key,val);
return true;
}
node* cur = _root;
node* parent = nullptr;
while (cur)
{
if (cur->_key < key)
{
parent = cur;
cur = cur->_right;
}
else if (cur->_key > key)
{
parent = cur;
cur = cur->_left;
}
else
{
return false;
}
}
cur = new node(key,val);
if (key < parent->_key)
{
parent->_left = cur;
}
else
{
parent->_right = cur;
}
return true;
}
bool erase(const K& val)
{ //删除节点要先找到将要删除的节点
node* parent = nullptr;
node* cur = _root;
if (_root == nullptr)
{
//cout << "Tree NULL" << endl;
return false;
}
while (cur)
{
if (cur->_key > val)
{
parent = cur;
cur = cur->_left;
}
else if (cur->_key < val)
{
parent = cur;
cur = cur->_right;
}
else
{
//找到节点开始删除
//分为3种大情况 cur 左为空 右为空 或者左右都为空
//1.左为空
if (cur->_left == nullptr)
{ //如果删除的是根节点单独处理
if (cur == _root)
{
_root = cur->_right;
delete cur;
return true;
}
else
{
//判断cur是parent的右孩子还是右孩子
if (parent->_left == cur)
{
parent->_left = cur->_right;
delete cur;
return true;
}
else
{
parent->_right = cur->_right;
delete cur;
return true;
}
}
}
//2.右为空
else if (cur->_right == nullptr)
{ //1.cur为根节点单独判断
if (cur == _root)
{
_root = cur->_left;
delete cur;
return true;
}
else
{
if (parent->_left == cur)
{
parent->_left = cur->_left;
delete cur;
return true;
}
else
{
parent->_right = cur->_left;
delete cur;
return true;
}
}
}
//3.左右都不为空
else
{
//找当前节点的左树的最右节点或者右数的最左节点进行替换
/*注意不能这样写,
如果cur->_left就是左树的的最右节点就会有问题
node pmaxleft=nullptr;
* node maxleft=cur->_left;
*/
node* cur_parent = cur;
node* left_max = cur->_left;
while (left_max->_right)
{
cur_parent = left_max;
left_max = left_max->_right;
}
cur->_key = left_max->_key;
if (cur_parent->_left == left_max)
{
cur_parent->_left = left_max->_left;
}
else
{
cur_parent->_right = left_max->_left;
}
delete left_max;
return true;
}
}
}
return false;
}
void Destroy(node* root)
{
if (root == nullptr)
{
return;
}
Destroy(root->_left);
Destroy(root->_right);
delete root;
}
~BST()
{
Destroy(_root);
_root = nullptr;
}
BST(const BST<K,V>& t)
{
_root = Copy(t._root);
}
BST<K,V> operator=(const BST<K,V> t)
{
swap(_root, t._root);
return *this;
}
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;
}
BST()
:_root(nullptr)
{
}
private:
node* _root;
};
}
在K模型的基础上加一个模板参数,在插入的时候除了插入key之外还插入了val,其他方面大致不变,Copy函数在new节点的时多需要对val值也初始化一下,还有这个node节点类的构造函数稍微修改一下即可。
#include"BinarySearchTree.h"
#include
#include
#include
void t1()
{
BST<int> tree;
vector<int>tem(8, 0);
srand((unsigned int)time(NULL));
for (int i = 1; i <= 8; i++)
{
int val = rand() % 20;
//tree.Insert(val);
tree.InsertR(val);
tem.push_back(val);
}
tree.Inorder();
for (int i = 0; i < 8; i++)
{
bool r1 = tree.Find(tem[i]);
bool r2 = tree.Find(tem[i] + 1);
cout << r1 << " " << r2 << " ";
}
cout << endl;
for (int i = 0; i < tem.size(); i++)
{
tree.erase(tem.back());
tree.Inorder();
tem.pop_back();
}
tree.Inorder();
cout << "t1" << endl;
}
void t2()
{
BST<int> tree;
vector<int>tem(8, 0);
srand((unsigned int)time(NULL));
for (int i = 1; i <= 8; i++)
{
int val = rand() % 20;
tree.InsertR(val);
tem.push_back(val);
}
tree.Inorder();
for (int i = 0; i < 8; i++)
{
bool r1 = tree.FindR(tem[i]);
bool r2 = tree.FindR(tem[i] + 1);
cout << r1 << " " << r2 << " ";
}
cout << endl;
for (int i = 0; i < tem.size(); i++)
{
tree.EraseR(tem.back());
tree.Inorder();
tem.pop_back();
}
tree.Inorder();
cout << "t2" << endl;
}
int main()
{
t1();
t2();
/*BSTt1;
t1.Insert(1);
t1.Insert(4);
t1.Insert(6);
t1.Insert(7);
t1.Insert(1);
BSTt2(t1);
t2.Inorder();*/
key_val::BST<string, string>t;
t.Insert("sort", "排序");
t.Insert("red", "红色");
t.Insert("big", "大");
t.Insert("end", "结束");
t.Insert("left", "左边");
t.Insert("right", "右边");
string s1;
while (cin >> s1)
{
auto ret= t.Find(s1);
if (ret)
{
cout << ret->_value << endl;
}
else
{
cout << "无此单词" << endl;
}
}
key_val::BST<string, int>t2;
string array[] = { "苹果","橘子","香蕉","苹果","梨","橙子","哈密瓜","橘子" };
for (string e : array)
{
auto ret = t2.Find(e);
if (ret == nullptr)
{
t2.Insert(e, 1);
}
else
{
ret->_value++;
}
}
t2.Inorder();
return 0;
}
这里简单演示了一下这个水果出现的次数和简单的单词互译。
插入和删除操作都必须先查找,查找效率代表了二叉搜索树中各个操作的性能。对有n个结点的二叉搜索树,若每个元素查找的概率相等,则二叉搜索树平均查找长度是结点在二叉搜索树的深度的函数,即结点越深,则比较次数越多。但对于同一个关键码集合,如果各关键码插入的次序不同,可能得到不同结构的二叉搜索树.
根据插入节点的大小关系,二叉搜索树可能出现不平衡的状态,这样二叉搜索树就会退化成单链表的形式。
因此最优情况下,二叉搜索树为完全二叉树(或者接近完全二叉树),其单次查找次数为:logN。最差情况下,二叉搜索树退化为单支树(或者类似单支),其单次查找次数为:N.
如果退化成单支树,二叉搜索树的性能就失去了,因此出现了AVL树以及红黑树,它们是平衡的二叉搜索树不会出现上述退化成单链表的情况,查找搜索效率很高。后续我将介绍这两种数据结构。
以上内容如有问题,欢迎指正!