二叉搜索树(BST)和二叉平衡树(AVL)

本文详细介绍了二叉搜索树(BST)的基本操作,包括查找、插入和删除节点,以及二叉平衡树(AVL树)的概念,重点讨论了AVL树的高度平衡性质和旋转操作,如LL、RR、LR、RL旋转,确保查找、插入和删除的高效性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

二叉搜索树(BST)和二叉平衡树(AVL)

二叉搜索树(BST)

​ 在二叉搜索树(又称二叉排序树或者二叉查找树)中:

  • 若任意结点的左子树不空,则左子树上所有结点的值均不大于它的根结点的值;
  • 若任意结点的右子树不空,则右子树上所有结点的值均不小于它的根结点的值;
  • 任意结点的左、右子树也分别为二叉搜索树。
基本操作和代码实现
树的节点
class Node {
  Integer data;
  Node left;
  Node right;
  Node parent;
  public Node(Integer data, Node left, Node right, Node parent) {
    this.data = data;
    this.left = left;
    this.right = right;
    this.parent = parent;
  }
}
最小关键字

​ 最小关键字查询过程:根据二叉搜索树性质,左子树小于该节点。从根节点开始,一直找left指针,直到left指针为null。

public Node getMin(Node root) {
  if (root == null) {
    return null;
  }
  while (root.left != null) {
    root = root.left;
  }
  return root;
}
最大关键字

​ 最大关键字查询过程:根据二叉搜索树性质,右子树大于该节点。从根节点开始,一直找right指针,直到right指针为null。

public Node getMax(Node root) {
  if (root == null) {
    return null;
  }
  while (root.right != null) {
    root = root.right;
  }
  return root;
}
查找

​ 查找某关键字的过程为:

  • 树根开始查找,若根为null,则查找失败,返回NULL;
  • 如果根节点的值等于关键字,返回根节点,结束查找;
  • 如果根节点的值小于关键字,继续查找根节点的左子树;
  • 如果根节点的值大于关键字,继续查找根节点的右子树;
  • 如果查找到叶子节点仍不相等,关键字不存在,返回NULL,结束查找。
/**
     * 查找值为data的节点(递归)
     * @param root
     * @param data
     * @return
     */
public Node searchRec(Node root, int data) {
  if (root == null) {
    return null;
  }
  if (root.data > data) {
    return searchRec(root.left, data);
  } else if (root.data < data) {
    return searchRec(root.right, data);
  } else {
    return root;
  }
}
/**
     * 查找值为data的节点
     * @param root
     * @param data
     * @return
     */
public Node search(Node root, int data) {
  while (root != null) {
    if (root.data > data) {
      root = root.left;
    } else if (root.data < data) {
      root = root.right;
    } else {
      break;
    }
  }
  return root;
}
插入节点

插入节点的过程:

  • 新值v插入到一棵二叉搜索树T中,新建一个结点tmp;
  • 从根节点开始,如果值v大于根节点,查找其右子树;
  • 如果值v小于根节点,查找其左子树;
  • 当某个节点的左子树或右子树为null时,这个null的位置即为要插入节点tmp的位置。
  • 如果在查找过程中,发现值v等于某个节点的值,证明该值已经存在于树中,插入结束。
/**
     * BST插入节点(递归)
     *
     * @param root
     * @param insert
     * @return
     */
public Node insertRec(Node root, Node insert) {
  if (root == null) {
    root = insert;
  } else if (root.data > insert.data) {
    root.left = insertRec(root.left, insert);
  } else if (root.data < insert.data) {
    root.right = insertRec(root.right, insert);
  }
  return root;
}

/**
     * BST插入节点
     *
     * @param root
     * @param insert
     * @return
     */
public Node indert(Node root, Node insert) {
  if (root == null) {
    root = insert;
  } else {
    Node tmp = root;
    Node parent = null;//记录父节点
    while (tmp != null) {//定位插入的位置
      parent = tmp;
      if (insert.data > tmp.data) {
        tmp = tmp.right;
      } else {
        tmp = tmp.left;
      }
    }
    insert.parent = parent;
    if (parent.data > insert.data) {
      parent.left = insert;
    } else if (parent.data < insert.data) {
      parent.right = insert;
    }
    //如果 parent.data = insert.data,说明该节点已经在树中,不用重新插入
  }
  return root;
}

在插入新结点后,新结点总是作为一个新叶子结点而存在的。

删除某个节点

​ 删除和插入一样,会引起树的变化。要保证不破坏二叉搜索树的性质,删除后需要做一些调整。

​ 寻找要删除的节点的过程和查询过程类似。找到的节点有一下情况:

  1. 为叶子节点,待删除的节点没有子节点。直接删除该节点,并修改父节点指向该节点的指针为null。

  2. 待删除的节点只有左孩子。

    2.1 如果该节点为父节点的左孩子,将父节点的左指针指向该节点的左孩子即可;

    2.2 如果该节点为父节点的右孩子,将父节点的右指针指向该节点的左孩子即可;

  3. 待删除的节点只有右孩子。

    3.1 如果该节点为父节点的左孩子,将父节点的左指针指向该节点的右孩子即可;

    3.2 如果该节点为父节点的右孩子,将父节点的右指针指向该节点的右孩子即可;

  4. 待删除的节点有左孩子和右孩子。

    过程:

    a. 找到右子树的最小值;

    b. 将右子树的最小值的数据赋值给待删除节点的数据;

    c. 然后删除最小值位置的那个节点。

public void delete(Node root, Node del) {
  if (root == null) {
    return;
  }
  Node tmp = null;
  while (root != null) {//定位需要删除的节点
    if (root.data > del.data) {
      tmp = root;//记录父节点
      root = root.left;
    } else if (root.data < del.data) {
      tmp = root;//记录父节点
      root = root.right;
    } else {//此时。root即位要删除的节点
      if (root.left == null && root.right == null) {//<1>待删除的节点没有子节点
        if (tmp == null) {//删除的为树的根节点
          root = null;
        } else {
          if (tmp.left == root) {
            tmp.left = null;
          } else if (tmp.right == root) {
            tmp.right = null;
          }
        }
      } else if (root.right == null && root.left != null) {//<2>待删除的节点只有左孩子
        if (tmp == null) {//删除的为树的根节点
          root = root.left;
        } else {
          if (tmp.left == root) {//待删除的节点为父节点的左孩子
            tmp.left = root.left;
          } else if (tmp.right == root) {//待删除的节点为父节点的右孩子
            tmp.right = root.left;
          }
        }
      } else if (root.right != null && root.left == null) {//<3>待删除的节点只有右孩子
        if (tmp == null) {//删除的为树的根节点
          root = root.right;
        } else {
          if (tmp.right == root) {//待删除的节点为父节点的右孩子
            tmp.right = root.right;
          } else if (tmp.left == root) {//待删除的节点为父节点的左孩子
            tmp.left = root.right;
          }
        }
      } else {//<4>待删除的节点有左孩子和右孩子
        /**
                     * 方法:
                     * a.找到右子树的最小值;
                     * b.将右子树的最小值的数据赋值给待删除节点的数据;
                     * c.然后删除最小值位置的那个节点。
                     */
        Node rightMin = getMin(root.right);//右子树的最小值的节点
        Node rightMinParent = rightMin.parent;//右子树的最小值的节点的父节点
        root.data = rightMin.data;
        if (rightMinParent.left == rightMin) {
          rightMinParent.left = null;
        } else if (rightMinParent.right == rightMin) {
          rightMinParent.right = null;
        }
      }
    }
    break;//删除之后结束循环
  }
}

二叉平衡树(AVL)

​ 二叉平衡树(AVL树),任意节点对应的两颗子树的最大高度差为1,因此它被称为高度平衡树。它或者是一棵空树,或者是具有下列性质的树:

  • 具备二叉排序树的所有性质;
  • 左子树和右子树深度差的绝对值不超过1;
  • 左子树和右子树都是二叉平衡树

AVL树的查找、插入和删除在平均和最坏情况下都是O(logn)。

基本操作和代码实现
AVL树的节点
  • data为节点的值
  • left节点的左孩子
  • right节点的右孩子
  • Height高度
class Node<T> {
  T data;
  Node<T> left;
  Node<T> right;
  int height;
  public Node(T data, Node<T> left, Node<T> right) {
    this.data = data;
    this.left = left;
    this.right = right;
    this.height = 1;//初始话高度为1,当树为空时,高度为0。
  }
}
树的高度

​ 树的高度为最大层次。即空的二叉树的高度是0,非空树的高度等于它的最大层次。

/**
     * 获取树的高度
     * @param root
     * @return
     */
public int height(Node root) {
  if (root != null) {
    return root.height;
  } else {
    return 0;
  }
}
旋转

​ 在AVL树进行插入或者删除操作后,可能导致AVL树失去平衡(左子树的高度和右子树的高度相差大于1)。

在这里插入图片描述

上图中的四颗失去平衡的AVL树中,依次是:LL、LR、RL、RR。

LL:也称"左左",插入或删除一个节点后,根节点的左子树的左子树还有非空子节点,导致"根的左子树的高度"比"根的右子树的高度"大2,导致AVL树失去了平衡。

LR:也称"左右",插入或删除一个节点后,根节点的左子树的右子树还有非空子节点,导致"根的左子树的高度"比"根的右子树的高度"大2,导致AVL树失去了平衡。

LR:也称"右左",插入或删除一个节点后,根节点的右子树的左子树还有非空子节点,导致"根的右子树的高度"比"根的左子树的高度"大2,导致AVL树失去了平衡。

RR:也称"右右",插入或删除一个节点后,根节点的右子树的右子树还有非空子节点,导致"根的右子树的高度"比"根的左子树的高度"大2,导致AVL树失去了平衡。

上述四种情况可采用对应的旋转方法来使其恢复平衡。

LL旋转

在这里插入图片描述

​ 上图中左边的失去平衡的LL树,经过LL旋转变成了右边的AVL树。

​ LL旋转是围绕"失去平衡的AVL根节点"进行的,也就是上图中的"8",将"4"变为根节点,"8"变为"4"的右子树,"4"的右子树变为"8"的左子树。

public Node LLRotation(Node k2) {
  //上图中8为k2,4为k1
  Node k1 = k2.left;
  k2.left = k1.right;
  k1.right = k2;
  k2.height = Math.max(height(k2.left), height(k2.right)) + 1;
  k1.height = Math.max(height(k1.left), k2.height) + 1;
  return k1;
}
RR旋转

在这里插入图片描述

​ RR旋转是与LL旋转对称的情况。

public Node RRRotation(Node k1) {
  //8为k1,12为k2
  Node k2 = k1.right;
  k1.right = k2.left;
  k2.left = k1;
  k1.height = Math.max(height(k1.left), height(k1.right)) + 1;
  k2.height = Math.max(height(k2.right), k1.height) + 1;
  return k2;
}
LR旋转

​ 对于LR失衡的情况,要经过两次旋转才能让AVL树恢复平衡。

在这里插入图片描述

​ 第一次旋转是围绕"4"进行RR旋转,第二次旋转是围绕"6"进行LL转转。

public Node LRRotation(Node k3){
  //上图中4为k1,8为k3,6为k2
  k3.left = RRRotation(k3.left);
  return LLRotation(k3);
}
RL旋转

​ RL旋转与LR旋转的情况对称。

在这里插入图片描述

​ 第一次旋转是围绕"12"的LL旋转,第二次旋转是围绕"10"的RR旋转。

public Node RLRotation(Node k1){
  k1.right = LLRotation(k1.right);
  return RRRotation(k1);
}
插入节点

​ 将值data插入到树root中。

public Node insert(Node root, int data) {
  if (root == null) {
    return new Node(data, null, null);
  } else {
    if (data < root.data) {//插入左子树
      root.left = insert(root.left, data);
      if (height(root.left) - height(root.right) == 2) {//需要进行旋转调整
        if (data < root.left.data) {
          root = LLRotation(root);
        } else {
          root = LRRotation(root);
        }
      }
    } else if (data > root.data) {//插入右子树
      root.right = insert(root.right, data);
      if (height(root.right) - height(root.left) == 2) {//需要进行旋转调整
        if (data > root.right.data) {
          root = RRRotation(root);
        } else {
          root = RLRotation(root);
        }
      }
    } else {//重复添加
    }
  }
  root.height = Math.max(height(root.left), height(root.right)) + 1;
  return root;
}
删除节点

​ 删除树中值为data的节点。

public Node delete(Node root, int data) {
  if (root == null) {
    return null;
  }
  if (data < root.data) {//待删除的节点在左子树中
    root.left = delete(root.left, data);
    if (height(root.right) - height(root.left) == 2) {//树失去平衡,需要调整
      Node tmp = root.right;
      if (tmp != null) {
        if (height(tmp.left) > height(tmp.right)) {
          root = RLRotation(root);
        } else {
          root = RRRotation(root);
        }
      }
    }
  } else if (data > root.data) {//待删除的节点在右子树中
    root.right = delete(root.right, data);
    Node tmp = root.left;
    if (tmp != null) {
      if (height(tmp.right) > height(tmp.left)) {
        root = LRRotation(root);
      } else {
        root = LLRotation(root);
      }
    }
  } else {//找到了待删除的节点
    if (root.left == null && root.right == null) {//待删除的节点为叶子节点
      root = null;
    } else if (root.left != null && root.right == null) {//待删除的节点只有左子树
      root = root.left;
      root.height = Math.max(height(root.left), height(root.right)) + 1;
    } else if (root.left == null && root.right != null) {//待删除的节点只有右子树
      root = root.right;
      root.height = Math.max(height(root.right), height(root.left)) + 1;
    } else {//待删除待节点左右子树均不为null
      Node leftMax = root.left;
      while (leftMax.right != null) {//左子树中的最大值
        leftMax = leftMax.right;
      }
      //将该最大节点的值赋值给root
      root.data = leftMax.data;
      //删除该最大节点
      root.left = delete(root.left, leftMax.data);
    }
  }
  return root;
}

例子

POJ1577

POJ2418

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值