数据结构与算法---二叉查找树

1. 二叉查找树简介

  • 最常用的一种二叉树类型,也叫二叉搜索树
  • 可以快速查找一个数据,还支持快速插入、删除一个数据
  • 二叉查找树的结构
    • 数中的任意一个节点,其左子树中的每个节点的值,都要小于这个结点的值
    • 其右子树中的每个节点值,都要大于这个节点值
    • 在这里插入图片描述

2. 二叉查找树的查找操作

  1. 要查找的数X,先取根节点
  2. 如果X和根节点相等,直接返回结果
  3. 如果X比根节点小,就到左子树递归查找
  4. 如果X比根节点大,就到右子树递归查找。
  5. 在这里插入图片描述
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.二叉查找树的插入操作

  1. 类似查找操作,比较大小,找到合适的插入位置
  2. 插入的数为X,取根节点
  3. 如果X比根节点小,
    • 如果左子树为空,则插入左子树
    • 如果左子树不为空,则与左孩子比较,递归下去
  4. 如果X比根节点大,
    • 如果右子树为空,则插入右子树
    • 如果右子树不为空,则与右孩子比较,递归下去
  5. 在这里插入图片描述

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. 二叉查找树的删除操作

  1. 删除操作复杂,要按照删除节点的子节点的个数不同,我们需要分三种情况来处理
    • 没有子节点,直接将其父节点指向该节点的指针设为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;
}
  1. 还有一种比较取巧的方式,就是不真正删除节点,只是标记已删除,这样占内存,但是删除操作简单,也没有增加插入、查找操作的实现难度

5. 二叉查找树的其他操作

  • 快速查找最大节点-----直接向右遍历到底
  • 最小节点----直接向左遍历到底
  • 前驱节点----对其左子树进行向右遍历到底
  • 后继节点----对其右子树进行向左遍历到底
  • 中序遍历,直接输出有序序列,时间复杂度O(n),非常高效

5.1 重复数据二叉查找树

  1. 利用扩容机制,数组或者链表,将重复数据放在一个位置上
  2. 把重复数据当成比第一个数据大的情况,放在其右子树最左侧位置(右子树最小的位置)
  3. 在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

6. 二叉查找树的时间复杂度

  1. 这里只考虑二叉查找树为完全二叉树(或满二叉树)
  2. 时间复杂度与树的高度成正比,就是O(height)
  3. 求树的高度n,第一层1,二层 2,k层 2k个
  4. 等比求和 ,求得高度,k = log n
  5. 时间复杂度 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);
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值