搜索树(查找树)
搜索是一种很常见的需求,当数据较多时,如何找到目标对象时,即需要搜索查找。
那么如何快速找到目标呢,最朴素的思想就是暴力查找,一个一个对比。
显然,当数据量增大时,我们需要优化查找方法获取更快的查找速度,我们可以通过维护一个有序数组,从而实现二分查找,查找速度从O(n)降为O(longn)。
但是数组并不便于插入和删除,链表便成为了最好的选择。
既然是采用二分查找,那么不如直接按照查找的步骤维护一种数据结构,于是二叉搜索树出现了。
二叉搜索树
二叉搜索树的性质为:
若左子树不为空,则左子树上所有节点值都小于根结点值;
若右子树不为空,则右子树上所有节点值都大于根结点值;
左右子树依然具备二叉树的以上性质。
总结起来即是–左小右大。
二叉搜索树(Binary Search Tree)的实现
实现二叉搜索树的关键在于插入节点和删除节点。
对于插入节点,我们需要先从根节点开始为它找到一个空位,然后插入。
struct Node
{
Node(int val): _val(val), left(nullptr), right(nullptr)
{
}
Node* left;
Node* right;
int _val;
};
class BSTree
{
public:
BSTree():root(nullptr)
{}
void addNode(Node* node)
{
if(!root)
{
root = node;
return;
}
Node* tmp_node = root;
while(tmp_node)
{
if(node->_val == tmp_node->_val)
{
return;
}
else if(node->_val < tmp_node->_val)
{
if(!tmp_node->left)
tmp_node->left = node;
else
tmp_node = tmp_node->left;
}
else{
if(!tmp_node->right)
tmp_node->right = node;
else
tmp_node = tmp_node->right;
}
}
}
//层序输出
void print()
{
queue<Node*> node_q;
if(root)
node_q.push(root);
while(!node_q.empty())
{
Node* tmp_node = node_q.front();
node_q.pop();
printf("%d ", tmp_node->_val);
if(tmp_node->left)
node_q.push(tmp_node->left);
if(tmp_node->right)
node_q.push(tmp_node->right);
}
printf("\n");
}
public:
Node* root;
};
int main()
{
BSTree bsTree;
vector<int> num{5, 3, 9, 1, 6, 12};
for(auto n:num)
{
Node* cur_node = new Node(n);
bsTree.addNode(cur_node);
}
bsTree.print();
bsTree.addNode(new Node(4));
bsTree.print();
}
层序打印的结果:
对于删除节点,需要考虑:
1.该节点为叶子节点,即无子节点,直接删除即可;
2.该节点具有子节点,只有左子节点,则将左子节点的右子节点移动到删除位置,只有右子节点,将右子节点的左子节点移到删除位置;
3.同时具有左右子节点,可以选择左子树的最右节点移到删除位置,也可以选择右子树的最左;
//找到并删除
void deleteNode(int val)
{
Node* tmp_node = root;
Node* parent = nullptr;
while (tmp_node->_val != val)
{
parent = tmp_node;
if(val < tmp_node->_val)
tmp_node = tmp_node->left;
else
tmp_node = tmp_node->right;
}
if(tmp_node->left)
{
Node** back_node = &(tmp_node->left);
while((*back_node)->right)
{
back_node = &((*back_node)->right);
}
swap(tmp_node->_val, (*back_node)->_val);
*back_node = nullptr;
}
else if(tmp_node->right)
{
Node** back_node = &(tmp_node->right);
while((*back_node)->left)
{
back_node = &((*back_node)->left);
}
swap(tmp_node->_val, (*back_node)->_val);
*back_node = nullptr;
}
else{
if(parent == nullptr)
{
delete root;
root = nullptr;
return;
}
if(parent->left->_val == val)
{
delete parent->left;
parent->left = nullptr;
}
else{
delete parent->right;
parent->right = nullptr;
}
}
}
AVL树
简介
新事物的出现必然是为了解决旧事物的缺点,AVL树是在二叉搜索树的基础上进化而来的。
前面提到为了和二分查找的步骤一致,将链表转换为了二叉树,如果在创建二叉搜索树的时候,输入的序列是有序的,那么二叉搜索树也就退化为了链表,查找性能也将变为O(N)。
怎么防止这种情况发生,如果二叉搜索树在有退化为链表的趋势(左右子树高度差较大)时可以自行调整就好了。
于是,AVL树出现了,AVL树限定左右子树的高度差不能超过1,如果超过了1,就进行旋转操作从而自平衡。
在节点内部引入一个记录左右子树高度差的值,称为平衡因子(BF),在平衡情况下,其值在(-1,1)之间
当插入一个新节点时,向上更新平衡因子,如果平衡因子为2或-2,则需要旋转操作,重新平衡。
AVL的旋转
AVL树保持平衡的关键即是旋转操作,那么如何旋转呢?
分析不平衡的情况即可得知,不平衡的情况被归纳为四种,也就对应了四种旋转操作。
左单旋和右单旋
先看一种简单的情况,右侧持续插入节点,4节点的BF=-2,显然需要