文章目录
1. 二叉查找树简介
- 最常用的一种二叉树类型,也叫二叉搜索树
- 可以快速查找一个数据,还支持快速插入、删除一个数据
- 二叉查找树的结构
- 数中的任意一个节点,其左子树中的每个节点的值,都要小于这个结点的值
- 其右子树中的每个节点值,都要大于这个节点值
2. 二叉查找树的查找操作
- 要查找的数X,先取根节点
- 如果X和根节点相等,直接返回结果
- 如果X比根节点小,就到左子树递归查找
- 如果X比根节点大,就到右子树递归查找。
public class BinarySearchTree {
private Node tree;
public Node find(int data) {
Node p = tree;
while (p != null) {
if (data < p.data) p = p.left;
else if (data > p.data) p = p.right;
else return p;
}
return null;
}
public static class Node {
private int data;
private Node left;
private Node right;
public Node(int data) {
this.data = data;
}
}
}
3.二叉查找树的插入操作
- 类似查找操作,比较大小,找到合适的插入位置
- 插入的数为X,取根节点
- 如果X比根节点小,
- 如果左子树为空,则插入左子树
- 如果左子树不为空,则与左孩子比较,递归下去
- 如果X比根节点大,
- 如果右子树为空,则插入右子树
- 如果右子树不为空,则与右孩子比较,递归下去
public void insert(int data) {
if (tree == null) {
tree = new Node(data);
return;
}
Node p = tree;
while (p != null) {
if (data > p.data) {
if (p.right == null) {
p.right = new Node(data);
return;
}
p = p.right;
} else { // data < p.data
if (p.left == null) {
p.left = new Node(data);
return;
}
p = p.left;
}
}
}
4. 二叉查找树的删除操作
- 删除操作复杂,要按照删除节点的子节点的个数不同,我们需要分三种情况来处理
- 没有子节点,直接将其父节点指向该节点的指针设为null
- 删除节点有一个子节点,将父节点指向该节点的指针指向其子节点
- 删除的节点有两个子节点,比较复杂
- 找到这个节点的右子树的最小节点,并替换到要删除的节点
- 再删除这个最小节点
- 因为最小节点没有左子节点
public void delete(int data) {
Node p = tree; // p指向要删除的节点,初始化指向根节点
Node pp = null; // pp记录的是p的父节点
while (p != null && p.data != data) {
pp = p;
if (data > p.data) p = p.right;
else p = p.left;
}
if (p == null) return; // 没有找到
// 要删除的节点有两个子节点
if (p.left != null && p.right != null) { // 查找右子树中最小节点
Node minP = p.right;
Node minPP = p; // minPP表示minP的父节点
while (minP.left != null) {
minPP = minP;
minP = minP.left;
}
p.data = minP.data; // 将minP的数据替换到p中
p = minP; // 下面就变成了删除minP了
pp = minPP;
}
// 删除节点是叶子节点或者仅有一个子节点
Node child; // p的子节点
if (p.left != null) child = p.left;
else if (p.right != null) child = p.right;
else child = null;
if (pp == null) tree = child; // 删除的是根节点
else if (pp.left == p) pp.left = child;
else pp.right = child;
}
- 还有一种比较取巧的方式,就是不真正删除节点,只是标记已删除,这样占内存,但是删除操作简单,也没有增加插入、查找操作的实现难度
5. 二叉查找树的其他操作
- 快速查找最大节点-----直接向右遍历到底
- 最小节点----直接向左遍历到底
- 前驱节点----对其左子树进行向右遍历到底
- 后继节点----对其右子树进行向左遍历到底
- 中序遍历,直接输出有序序列,时间复杂度O(n),非常高效
5.1 重复数据二叉查找树
- 利用扩容机制,数组或者链表,将重复数据放在一个位置上
- 把重复数据当成比第一个数据大的情况,放在其右子树最左侧位置(右子树最小的位置)
6. 二叉查找树的时间复杂度
- 这里只考虑二叉查找树为完全二叉树(或满二叉树)
- 时间复杂度与树的高度成正比,就是O(height)
- 求树的高度n,第一层1,二层 2,k层 2k个
- 等比求和 ,求得高度,k = log n
- 时间复杂度 O(logn)
确定二叉树高度有两种思路:第一种是深度优先思想的递归,分别求左右子树的高度。当前节点的高度就是左右子树中较大的那个+1;第二种可以采用层次遍历的方式,每一层记录都记录下当前队列的长度,这个是队尾,每一层队头从0开始。然后每遍历一个元素,队头下标+1。直到队头下标等于队尾下标。这个时候表示当前层遍历完成。每一层刚开始遍历的时候,树的高度+1。最后队列为空,就能得到树的高度。
7.问题
散列表的插入、删除、查找操作的时间复杂度可以做到常量级的 O(1),非常高效。而二叉查找树在比较平衡的情况下,插入、删除、查找操作时间复杂度才是 O(logn),相对散列表,好像并没有什么优势,那我们为什么还要用二叉查找树呢
- 第一,散列表中的数据是无序存储的,如果要输出有序的数据,需要先进行排序。而对于二叉查找树来说,我们只需要中序遍历,就可以在 O(n) 的时间复杂度内,输出有序的数据序列。
- 第二,散列表扩容耗时很多,而且当遇到散列冲突时,性能不稳定,尽管二叉查找树的性能不稳定,但是在工程中,我们最常用的平衡二叉查找树的性能非常稳定,时间复杂度稳定在 O(logn)。
- 第三,笼统地来说,尽管散列表的查找等操作的时间复杂度是常量级的,但因为哈希冲突的存在,这个常量不一定比 logn 小,所以实际的查找速度可能不一定比 O(logn) 快。加上哈希函数的耗时,也不一定就比平衡二叉查找树的效率高。
- 第四,散列表的构造比二叉查找树要复杂,需要考虑的东西很多。比如散列函数的设计、冲突解决办法、扩容、缩容等。平衡二叉查找树只需要考虑平衡性这一个问题,而且这个问题的解决方案比较成熟、固定。
- 最后,为了避免过多的散列冲突,散列表装载因子不能太大,特别是基于开放寻址法解决冲突的散列表,不然会浪费一定的存储空间。
- 综合这几点,平衡二叉查找树在某些方面还是优于散列表的,所以,这两者的存在并不冲突。我们在实际的开发过程中,需要结合具体的需求来选择使用哪一个。
8. C++ 实现
# 二叉查找树
## 二叉树查找树的性质 (假设每个结点关键字都是整数,且互异)
1. 左子树的所有结点值均小于根结点值
2. 柚子树的所有结点值均大于根结点值
3. 左右子树都满足上述两个条件
## 插入过程
1. 若当前二叉树为空,则插入元素为根结点
2. 若插入的元素小于根结点值,则递归从根结点左子树找到可插入的位置
3. 若插入的元素大于根结点值,则递归从根结点的右子树找可插入的位置
## 删除过程
1. 待删除的结点Z为叶子结点,直接删除,修改父节点的指针为空
2. 待删除的结点Z为单支结点,让Z的子树与Z的父节点相连,删除结点Z
3. 待删除的结点Z左右子树都不为空 (L为Z的左结点,R为Z的右结点)
* 方式一:用L结点代替Z结点,走到L最右的结点,将R结点接入L最右
* 方式二:用R结点代替Z结点,走到R最左结点,将L结点接入
## 顺序遍历(二叉树中序遍历)
## 查找过程
1. 与根结点比较
* 和根结点相同大小,直接返回结果
* 比根结点小,递归左子树
* 比根结点大,递归右子树
//树结点数据结构
template<typedef T>
class BSTNode
{
public:
T _key; //关键字
BSTNode * _lchild;
BSTNode * _rchild;
BSTNode * _parent;
//构造函数
BSTNode(T key, BSTNode * lchild, BSTNode * rchild, BSTNode * parent):
_key(key), _lchild(lchild), _rchild(rchild), _parent(parent){};
};
//二叉搜索树数据结构
template<typedef T>
class BSTree
{
private:
BSTNode<T> * _root;
public:
Status insert(T key);
// Status remove(T key);
// Status travel(); //顺序遍历(二叉树中序遍历)
// Status search(T key);
private:
Status insert(BSTNode<T> * &root, T key);
// Status search(BSTNode<T> * &root, T key);
// Status remove(BSNode<T> * root, T key);};
// Status deleteNode(BSNode<T> * root);
};
template<typedef T>
Status BSTree<T>::insert(BSTNode<T> * &root, T key)
{
BSTNode<T> * cur = root;
if(NULL == root)
{
root->_key = T;
}
else if(cur->_key < root->_key)
{
cur = root->_lchild;
cur->_parent = root;
insert(root->_lchild, key);
}
else
{
cur = root->_rchild;
cur->_parent = root;
insert(root->_rchild, key);
}
}
template<typedef T>
Status BSTree<T>::insert(T key)
{
BSTNode<T> * cur = new BSTNode<T>(key, NULL, NULL, NULL);
if(NULL == cur) return OVERFLOW;
insert(_root, key);
}
template<typedef T>
Status BSTree<T>::search(BSNode<T> * root, T key)
{
if(NULL == root) return FALSE;
else if(key == root->_key) return TRUE;
else if(key < root->_key) return search(root->lchild, key);
else return search(root->rchild, key);
}
template<typedef T>
Status BSTree<T>::search(T key)
{
if(NULL == _root) return FALSE;
return search(_root, key);
}