前言
从二叉搜索树开始,到下一节的AVL树以及下下节的B树,都适合于字典描述。以前和字典相关的数据结构是线性结构跳表和散列,这一章到了树形结构。
二叉树的删除方法要特别注意,涉及到四个指针在树上游荡,需要理清每步的逻辑关系。
二叉搜索树
为什么使用二叉搜索树?
和其它每种数据结构一样,都有其存在的意义,优势及劣势。二叉搜索树也要与同为字典描述的跳表和散列进行对比。
查找(平均、最坏) | 插入 | 删除 | |
---|---|---|---|
跳表 | Θ(logn)、Θ(n) | Θ(logn)、Θ(n) | Θ(logn)、Θ(n) |
散列 | Θ(1)、Θ(n) | Θ(1)、Θ(n) | Θ(1)、Θ(n) |
二叉搜索树 | Θ(logn)、Θ(n) | Θ(logn)、Θ(n) | Θ(logn)、Θ(n) |
平衡搜索树 | Θ(logn)、Θ(logn) | Θ(logn)、Θ(logn) | Θ(logn)、Θ(logn) |
可见在增删查领域二叉搜索树媲美跳表,但是不及散列在理想状况下的Θ(1)。
二叉搜索树的特殊形式,平衡二叉搜索树有着最稳定的增删查时间复杂度,它是我们下一节要学习的数据结构。在学习之前,我们要先学习二叉搜索树的基本操作。
其实在实际应用领域,是平衡二叉树在大显身手。平衡二叉树的优势还不止这些,
比如有个新的需求: 按关键字的升序输出字典元素。
时间复杂度 | |
---|---|
散列 | O(nlogn) |
跳表 | Θ(n) |
平衡搜索树 | Θ(n) |
这时散列的表现较差,平衡二叉搜索树仍能有良好的时间性能。
在学习平衡二叉搜索树之前,我们实现实现二叉搜索树。
二叉搜索树实现
类
template <class K, class E>
class BSTree : public linkedBinaryTree<K,E>
{
pair<const K,E>* find(const K& theKey) const;
void insert(const pair<const K, E>& thePair);
void erase(const K& theKey);
void ascend(){linkedBinaryTree<K,E> :: inOrderOutput();}
};
搜索
template<class K ,class E>
pair<const K, E>* BSTree :: find(const K& theKey) const
{
binaryTreeNode<pair<const K, const E>> *p = root;
while(p != NULL)
{
if(theKey < p->data.first)
{
p = p->leftChild;
}else if(theKey > p->data.first)
{
p = p->rightChild;
}else
{
return &p->data;
}
}
return NULL;
}
插入
template<class K , class E>
void BSTree :: insert(const pair<const K , E>& thePair)
{
binaryTreeNode<pair<const K, const E>> *p = root;
binaryTreeNode<pair<const K, const E>> *pp = NULL;
while(p != NULL)
{
pp = p;
if(thePair.first < p->data.first)
{
p = p->leftChild;
}else if(thePair.first > p->data.first)
{
p = p->rightChild;
}else
{
p->data.second = thePair.second;
return;
}
}
binaryTreeNode<pair<const K, const E>> *newNode
= new binaryTreeNode<pair<const K, const E>>(thePair);
if(root != NULL)
{
if(thePair.first > pp->data.first)
pp->rightChild = newNode;
else
pp->leftChild = newNode;
}
else
{
root = newNode;
}
treeSize++;
}
删除
删除是二叉搜索树最复杂的操作,强烈建议读者自己画两个二叉搜索树,标识算法中所要用到的节点,捋一遍代码,明确各指针的作用。有时间的话,我会更新二叉搜索树删除方法的图解。
/*
删除的方法体中可分为四个部分,选择使用左子树的最大节点替换
1. 搜索要删除的节点的key,并记录该节点和父节点。
2. 如果要删除的节点有两个孩子,进行搜索找到左子树最大节点,并记录该节点和父节点。
3. 调整四个指针的位置,使要删除的节点最多只剩一个孩子
4. 要删除的节点至多有一个孩子,删除节点,将孩子节点和父节点对接。
*/
template <class K, class E>
void bsTree<K,E> :: erase(const K& theKey)
{
// 第一部分
binaryTreeNode<pair<const K, const E>> *p = root;
binaryTreeNode<pair<const K, const E>> *pp = NULL;
while(p != NULL)
{
pp = p;
if(theKey < p->data.first)
{
p = p->leftChild;
}else if(theKey > p->data.first)
{
p = p->rightChild;
}else
{
return;
}
}
if(p != NULL)
return;
//第二部分
if(p->leftChild != NULL && p->rightChild != NULL)
{
binaryTreeNode<pair<const K, const E>> *s = p->leftChild;
binaryTreeNode<pair<const K, const E>> *pp = p;
while(s->rightChild != NULL)
{
ps = s;
s = s->rightCHild;
}
// 第三部分
//将左子树找到的节点s移到p,因为const Key,所以我们
// 直接新建一节点,其左右子树指向p的左右子树。
binaryTreeNode<pair<const K, E>> *q =
new binaryTreeNode<pair<const K, E>>(s->data, p-rightChild,p->rightChild);
if(pp == NULL)
root = q;
else if (p == pp->rightChild)
pp->rightChild = q;
else
pp->leftChild = q;
if(ps == p)
pp = q; // 此时不能等于ps,ps还在原来的树上,q是替换后的新树。
else
pp = ps;
delete p; // 建立新节点会删除需要删除的节点,但是子树多了一需要删除的节点,即s
p = s; // 把s当作至多只有一个孩子节点的p,继续往下进行。
}
// 第四部分,可直接适用于只有一个节点或无孩子的情况。
binaryTreeNode<pair<const K, E>> *c;
if(p->leftChild != NULL)
c = p->leftChild;
else
c = p->rightChild; // 此时包含了p无孩子的情况
//删除p
if(p == root)
root = c;
else
{
if(p == pp->leftChild)
pp->leftChild = c;
else
pp->rightChild = c;
}
treeSize--;
delete p;
}