第 11 章:树结构实际应用(二)

简介: 第 11 章:树结构实际应用

4.3、二叉排序树思路分析

4.3.1、添加子节点

假设待添加的节点为 node ,当前遍历的节点为 curNode,编码思路如下:

  • 待添加节点 node 的值与当前节点 curNode 的值比较
  • node.value < curNode.value:需要将 node 节点安排在 curNode 节点的左边,左移当前节点指针:curNode = curNode.left ,为下次判断做准备
  • node.value >= curNode.value:需要将 node 节点安排在 curNode 节点的右边,右移当前节点指针:curNode = curNode.right ,为下次判断做准备
  • 重复执行上述操作即可

何时停止递归?以下两个条件均可以让递归停止

  • 待添加节点 node 的值比当前节点 curNode 的值小,并且 curNode 没有左节点
  • node.value < curNode.value && curNode.left ==null

待添加节点 node 的值比当前节点 curNode 的值大(或相等),并且 curNode 没有右节点

  • node.value >= curNode.value && curNode.right==null
    总结:新添加的节点都会沉到最下面去,成为一个叶子节点

4.3.2、查找子节点

查找目标子节点:假设目标节点的值为 value ,当前遍历的节点为 curNode ,编码思路如下

目标值 value 与当前节点值 curNode.value 进行比较

  • value == curNode.value:证明当前节点就是要找的节点,直接返回 curNode
  • value < curNode.value:证明要找的节点在 curNode 左边,左移当前节点指针:curNode = curNode.left,并继续执行上述比较操作
  • value >= curNode.value:证明要找的节点在 curNode 右边,右移当前节点指针:curNode = curNode.right,并继续执行上述比较操作

何时停止递归?

  • 当前节点值与目标值相等:curNode.value == value,证明当前节点就是要找的节点,直接返回 curNode
  • 或者当前节点为空:curNode == null,即证明没找到,返回 null

查找目标子节点的父节点(删除节点时需要查找目标节点的父节点):假设目标节点的值为 value ,当前遍历的节点为 parentNode ,parentNode 表示目标节点的父节点,编码思路如下


已找到目标节点的父节点:

  • 如果:parentNode.left != null && value = parentNode.left.value,则说明parent.left为目标节点,parent为目标节点的父节点
  • 如果:parentNode.right != null && value = parentNode.right.value,则说明parent.right为目标节点,parent为目标节点的父节点

还未找到目标节点的父节点,判断目标值 value 与当前节点值 parentNode.value 的大小

  • value < this.value && this.left != null:目标值在当前节点的左边,并且当前节点还有左节点,则将当前节点指针左移 parentNode = parentNode.left,继续寻找目标节点
  • value >= this.value && this.left != null:目标值在当前节点的右边,并且当前节点还有右节点,则将当前节点指针右移 parentNode = parentNode.right,继续寻找目标节点

何时停止递归?两个递归停止条件,满足其中一个即可

  • value = parentNode.left.value || value = parentNode.right.value :找到目标节点的父节点,则直接返回 parentNode
  • 找不到目标节点的父节点:往左找找不到,往右找也找不到,

4.3.3、删除子节点

再来一遍:

  • 单链表能不能实现自删除?不能!
  • 单链表想要删除需要怎么操作?找到其父节点!!!

假设目标节点的值为 value ,根节点为 root ,首先根据 value 值找到目标节点 targetNode ,再找到目标节点的父节点 parentNode

  • 如果targetNode == null,说明没有找到目标节点,直接滚蛋
  • 如果 targetNode != null && root.left == null && root.right == null,说明只有根节点既是目标节点,删除根节点即可
  • 否则就是下面三种复杂的情况咯:

第一种情况:待删除的节点为叶子节点,直接删除该叶子节点即可

  • 怎么样才算是叶子节点?targetNode.left == null && targetNode.right == null:左右节点都为空

怎么删除?

  • 如果parentNode.left != null && parentNode.left.value == value:即待删除的节点是 parentNode 的左子节点,则删除 parentNode 的左节点:parentNode.left = null;
  • 如果 parentNode.right!= null && parentNode.right.value == value:即待删除的节点是 parentNode 的右子节点,则删除 parentNode 的右节点:parentNode.right= null;

第二种情况:待删除的节点只有一颗子树,直接将其子树接在 parentNode 左边或右边即可

  • 怎么判断节点只有一颗子树?targetNode.left 和 targetNode.right 中有且仅有一个为 null

怎么删除?四种情况

如果 targetNode 只有左子结点,则证明子树挂在 targetNode 的左边,现在来看看 target 挂在 parentNode 的哪边?


  • 如果 target 挂在 parentNode 的左边,直接将 target 的子树挂在 parentNode 的左边:parentNode.left = target.left
  • 如果 target 挂在 parentNode 的右边,直接将 target 的子树挂在 parentNode 的右边:parentNode.right = target.left

如果 targetNode 只有右子结点,则证明子树挂在 targetNode 的右边,现在来看看 target 挂在 parentNode 的哪边?


  • 如果 target 挂在 parentNode 的左边,直接将 target 的子树挂在 parentNode 的左边:parentNode.left = target.right
  • 如果 target 挂在 parentNode 的右边,直接将 target 的子树挂在 parentNode 的右边:parentNode.right = target.right
  • 以上逻辑有个 Bug ~~~ 当待删除的节点为根节点时 ,parentNode == null,这时候我们直接用根节点 root 来操作即可

第三种情况:待删除的节点具有两棵颗子树

  • 从 targetNode 的左子树种找到值最大的节点(一直往右遍历),或者从从 targetNode 的右树种找到值最小的节点(一直往左遍历),假设最小值为 temp ,最小值所在的节点为 minNode
  • 此时 minNode 肯定为叶子节点,删除 minNode 节点
  • targetNode.value 设置为 temp ,这样以 targetNode 根节点的子树又是一棵二叉排序树

4.4、二叉排序树代码

4.4.1、树节点的定义

  • 树节点的定义
//创建Node结点
class Node {
  int value;
  Node left;
  Node right;
  public Node(int value) {
    this.value = value;
  }
  @Override
  public String toString() {
    return "Node [value=" + value + "]";
  }
  // 中序遍历
  public void infixOrder() {
    if (this.left != null) {
      this.left.infixOrder();
    }
    System.out.println(this);
    if (this.right != null) {
      this.right.infixOrder();
    }
  }
  // 添加结点的方法
  // 递归的形式添加结点,注意需要满足二叉排序树的要求
  public void add(Node node) {
    if (node == null) {
      return;
    }
    // 判断传入的结点的值,和当前子树根结点值的关系
    if (node.value < this.value) {
      // 如果当前结点左子结点为null
      if (this.left == null) {
        this.left = node;
      } else {
        // 递归的向左子树添加
        this.left.add(node);
      }
    } else { // 添加的结点的值大于 当前结点的值
      if (this.right == null) {
        this.right = node;
      } else {
        // 递归的向右子树添加
        this.right.add(node);
      }
    }
  }
  // 查找要删除的结点
  /**
   * 
   * @param value 希望删除的结点的值
   * @return 如果找到返回该结点,否则返回null
   */
  public Node search(int value) {
    if (value == this.value) { // 找到就是该结点
      return this;
    } else if (value < this.value) {// 如果查找的值小于当前结点,向左子树递归查找
      // 如果左子结点为空
      if (this.left == null) {
        return null;
      }
      return this.left.search(value);
    } else { // 如果查找的值不小于当前结点,向右子树递归查找
      if (this.right == null) {
        return null;
      }
      return this.right.search(value);
    }
  }
  // 查找要删除结点的父结点
  /**
   * 
   * @param value 要找到的结点的值
   * @return 返回的是要删除的结点的父结点,如果没有就返回null
   */
  public Node searchParent(int value) {
    // 如果当前结点就是要删除的结点的父结点,就返回
    if ((this.left != null && this.left.value == value) || (this.right != null && this.right.value == value)) {
      return this;
    } else {
      // 如果查找的值小于当前结点的值, 并且当前结点的左子结点不为空
      if (value < this.value && this.left != null) {
        return this.left.searchParent(value); // 向左子树递归查找
      } else if (value >= this.value && this.right != null) {
        return this.right.searchParent(value); // 向右子树递归查找
      } else {
        return null; // 没有找到父结点
      }
    }
  }
}


4.4.2、二叉排序树的定义

  • 二叉排序树的定义
//创建二叉排序树
class BinarySortTree {
  private Node root;
  public Node getRoot() {
    return root;
  }
  // 添加结点的方法
  public void add(Node node) {
    if (root == null) {
      root = node;// 如果root为空则直接让root指向node
    } else {
      root.add(node);
    }
  }
  // 中序遍历
  public void infixOrder() {
    if (root != null) {
      root.infixOrder();
    } else {
      System.out.println("二叉排序树为空,不能遍历");
    }
  }
  // 查找要删除的结点
  public Node search(int value) {
    if (root == null) {
      return null;
    } else {
      return root.search(value);
    }
  }
  // 查找父结点
  public Node searchParent(int value) {
    if (root == null) {
      return null;
    } else {
      return root.searchParent(value);
    }
  }
  // 删除结点
  public void delNode(int value) {
    if (root == null) {
      return;
    } else {
      // 1.需求先去找到要删除的结点 targetNode
      Node targetNode = search(value);
      // 如果没有找到要删除的结点
      if (targetNode == null) {
        return;
      }
      // 如果我们发现当前这颗二叉排序树只有一个结点
      if (root.left == null && root.right == null) {
        root = null;
        return;
      }
      // 去找到targetNode的父结点
      Node parent = searchParent(value);
      // 如果要删除的结点是叶子结点
      if (targetNode.left == null && targetNode.right == null) {
        // 判断targetNode 是父结点的左子结点,还是右子结点
        if (parent.left != null && parent.left.value == value) { // 是左子结点
          parent.left = null;
        } else if (parent.right != null && parent.right.value == value) {// 是右子结点
          parent.right = null;
        }
      } else if (targetNode.left != null && targetNode.right != null) { // 删除有两颗子树的节点
        int minVal = delRightTreeMin(targetNode.right);
        targetNode.value = minVal;
      } else { // 删除只有一颗子树的结点
        // 如果要删除的结点有左子结点
        if (targetNode.left != null) {
          if (parent != null) {
            // 如果 targetNode 是 parent 的左子结点
            if (parent.left.value == value) {
              parent.left = targetNode.left;
            } else { // targetNode 是 parent 的右子结点
              parent.right = targetNode.left;
            }
          } else {
            root = targetNode.left;
          }
        } else { // 如果要删除的结点有右子结点
          if (parent != null) {
            // 如果 targetNode 是 parent 的左子结点
            if (parent.left.value == value) {
              parent.left = targetNode.right;
            } else { // 如果 targetNode 是 parent 的右子结点
              parent.right = targetNode.right;
            }
          } else {
            root = targetNode.right;
          }
        }
      }
    }
  }
  // 编写方法:
  // 1. 返回的 以node 为根结点的二叉排序树的最小结点的值
  // 2. 删除node 为根结点的二叉排序树的最小结点
  /**
   * 
   * @param node 传入的结点(当做二叉排序树的根结点)
   * @return 返回的 以node 为根结点的二叉排序树的最小结点的值
   */
  public int delRightTreeMin(Node node) {
    Node target = node;
    // 循环的查找左子节点,就会找到最小值
    while (target.left != null) {
      target = target.left;
    }
    // 这时 target就指向了最小结点
    // 删除最小结点(该节点肯定是左叶子节点)
    delNode(target.value);
    return target.value;
  }
}

4.4.3、代码测试

  • 代码
public static void main(String[] args) {
    int[] arr = { 7, 3, 10, 12, 5, 1, 9, 2 };
    BinarySortTree binarySortTree = new BinarySortTree();
    // 循环的添加结点到二叉排序树
    for (int i = 0; i < arr.length; i++) {
        binarySortTree.add(new Node(arr[i]));
    }
    // 中序遍历二叉排序树
    System.out.println("中序遍历二叉排序树~");
    binarySortTree.infixOrder(); // 1, 3, 5, 7, 9, 10, 12
    // 测试一下删除叶子结点
    binarySortTree.delNode(5);
    binarySortTree.delNode(10);
    System.out.println("删除结点后");
    binarySortTree.infixOrder();
}
  • 程序运行结果
中序遍历二叉排序树~
Node [value=1]
Node [value=2]
Node [value=3]
Node [value=5]
Node [value=7]
Node [value=9]
Node [value=10]
Node [value=12]
删除结点后
Node [value=1]
Node [value=2]
Node [value=3]
Node [value=7]
Node [value=9]
Node [value=12]

4.5、课后练习

  • 如果我们从左子树找到最大的结点, 然后前面的思路完成.

5、平衡二叉树(AVL 树)

5.1、二叉排序树的问题

看一个案例(说明二叉排序树可能的问题),给你一个数列{ 1,2,3,4,5,6 } ,要求创建一颗二叉排序树(BST),并分析问题所在

  • 左子树全部为空,从形式上看,更像一个单链表
  • 插入速度没有影响
  • 查询速度明显降低(因为需要依次比较),不能发挥BST 的优势,因为每次还需要比较左子,其查询速度比单链表还慢

  • 解决方案-平衡二叉树(AVL)

5.2、平衡二叉树基本介绍

  • 平衡二叉树也叫平衡二叉搜索树(Self-balancing binary search tree)又被称为AVL树, 可以保证查询效率较高。
  • 平衡二叉树具有以下特点:它是一 棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。
  • 平衡二叉树的常用实现方法有红黑树、AVL、替罪羊树、Treap、伸展树等。
  • 注意:平衡二叉树一定是二叉排序树!!!
  • 举例说明,看看下面哪些AVL树, 为什么?

5.3、平衡二叉树思路分析

5.3.1、计算子树高度

其实计算子树高度这个递归还挺难理解的,我想了想,可以这样来理解:

  • left == null ? 0 : left.height()是求左子树的高度
  • right == null ? 0 : right.height()是求右子树的高度
  • 所以如上两个表达式取最大值,即为当前子树的高度
// 返回 以该结点为根结点的树的高度
public int height() {
    return Math.max(left == null ? 0 : left.height(), right == null ? 0 : right.height()) + 1;
}


  • 画了个图来说明计算子树高度的递归顺序和递归回溯过程

5.3.2、左旋转

  • 问题:当插入8 时,rightHeight() - leftHeight() > 1成立,此时,不再是一颗 AVL树了

怎么处理–进行左旋转(就是降低右子树的高度)

创建一个新的节点 newNode (以4这个值创建),创建一个新的节点,值等于当前根节点的值

把新节点的左子树设置了当前节点的左子树:newNode.left = left

把新节点的右子树设置为当前节点的右子树的左子树:newNode.right =right.left;

把当前节点的值换为右子节点的值:value=right.value;

把当前节点的右子树设置成右子树的右子树:right=right.right;

把当前节点的左子树设置为新节点:left=newNode;

  • 想想为啥是上面的的步骤?
  • 插入节点 8 后,整棵树不再是 AVL 树,节点 4 的右子树高度 > 节点 4 的左子树高度,需要进行左旋
  • 问题来了:什么是左旋?怎么进行左旋?还是以下面的图为例:不就是把节点 6 往上提,把根节点 4 往左下沉,然后再把节点 5 挂在节点 4 的右边


5.3.3、右旋转

  • 问题:当插入6 时,leftHeight() - rightHeight() > 1成立,此时,不再是一颗 AVL树了
  • 怎么处理–进行右旋转(就是降低左子树的高度) 这里是将 9 这个节点,通过右旋转,到右子树
  • 创建一个新的节点 newNode (以10这个值创建) ,创建一个新的节点,值等于当前根节点的值
  • 把新节点的右子树设置了当前节点的右子树:newNode.right = right;
  • 把新节点的左子树设置为当前节点的左子树的右子树:newNode.left =left.right;
  • 把当前节点的值换为左子节点的值:value=left.value;
  • 把当前节点的左子树设置成左子树的左子树:left=left.left;
  • 把当前节点的右子树设置为新节点:right=newNode;

想想为啥是上面的的步骤?

  • 插入节点 6 后,整棵树不再是 AVL 树,节点 10 的左子树高度 > 节点 10 的右子树高度,需要进行右旋
  • 问题来了:什么是右旋?怎么进行右旋?还是以下面的图为例:不就是把节点 8 往上提,把根节点 10 往右下沉,然后把节点 9 挂在节点 10 的左边




5.3.4、双旋转

  • 问题分析
    如下不平衡二叉树满足右旋条件,根节点 10 的左子树高度 > 根节点的右子树高度
  • 但不巧的是:根节点 10 的左子树的右子树高度 > 根节点 10 的左子树的左子树高度,那么在进行右旋后,还是棵不平衡二叉树
  • 那不就是因为节点 7 的右子树太长了,进行右旋后,挂到右边去会导致整棵树的右子树过高

怎么解决?

  • 目标:把节点 7 的右子树的高度降低,即对节点 7 进行左旋
  • 我先把以节点 7 为根节点的树搞成 AVL 树(对节点 7 进行左旋),再对节点 10 进行右旋,就行啦~
  • 即先对当前结点的左节点进行左旋转,再对当前结点进行右旋转的操作即可

编码思路:假设当前节点为 curNode

如果 curNode.rightHeight() - curNode.leftHeight() > 1:需进行左旋
如果 curNode.left.leftHeight() > curNode.left.rightHeight()

  • 先对curNode.left进行右旋:curNode.left.rightRotate()
  • 再对curNode进行左旋:curNode.leftRotate()
  • 否则直接进行左旋:curNode.leftRotate()

如果curNode.leftHeight() - curNode.rightHeight() > 1:需进行右旋

如果 curNode.left.rightHeight() > curNode.left.leftHeight()

  • 先对curNode.left进行左旋:curNode.left.leftHeight()
  • 再对 curNode 进行右旋:curNode.rightRotate()
  • 否则直接进行右旋:curNode.rightRotate()

  • 图有点难看,有时间再重新画吧。。。

5.4、平衡二叉树代码

5.4.1、树节点的定义

// 创建Node结点
class Node {
  int value;
  Node left;
  Node right;
  public Node(int value) {
    this.value = value;
  }
  // 返回左子树的高度
  public int leftHeight() {
    if (left == null) {
      return 0;
    }
    return left.height();
  }
  // 返回右子树的高度
  public int rightHeight() {
    if (right == null) {
      return 0;
    }
    return right.height();
  }
  // 返回 以该结点为根结点的树的高度
  public int height() {
    return Math.max(left == null ? 0 : left.height(), right == null ? 0 : right.height()) + 1;
  }
  // 左旋转方法
  private void leftRotate() {
    // 创建新的结点,以当前根结点的值
    Node newNode = new Node(value);
    // 把新的结点的左子树设置成当前结点的左子树
    newNode.left = left;
    // 把新的结点的右子树设置成当前结点的右子树的左子树
    newNode.right = right.left;
    // 把当前结点的值替换成右子结点的值
    value = right.value;
    // 把当前结点的右子树设置成当前结点右子树的右子树
    right = right.right;
    // 把当前结点的左子树(左子结点)设置成新的结点
    left = newNode;
  }
  // 右旋转
  private void rightRotate() {
    Node newNode = new Node(value);
    newNode.right = right;
    newNode.left = left.right;
    value = left.value;
    left = left.left;
    right = newNode;
  }
  @Override
  public String toString() {
    return "Node [value=" + value + "]";
  }
  // 添加结点的方法
  // 递归的形式添加结点,注意需要满足二叉排序树的要求
  public void add(Node node) {
    if (node == null) {
      return;
    }
    // 判断传入的结点的值,和当前子树的根结点的值关系
    if (node.value < this.value) {
      // 如果当前结点左子结点为null
      if (this.left == null) {
        this.left = node;
      } else {
        // 递归的向左子树添加
        this.left.add(node);
      }
    } else { // 添加的结点的值大于 当前结点的值
      if (this.right == null) {
        this.right = node;
      } else {
        // 递归的向右子树添加
        this.right.add(node);
      }
    }
    // 当添加完一个结点后,如果: (右子树的高度-左子树的高度) > 1 , 左旋转
    if (rightHeight() - leftHeight() > 1) {
      // 如果它的右子树的左子树的高度大于它的右子树的右子树的高度
      if (right != null && right.leftHeight() > right.rightHeight()) {
        // 先对右子结点进行右旋转
        right.rightRotate();
        // 然后在对当前结点进行左旋转
        leftRotate(); // 左旋转..
      } else {
        // 直接进行左旋转即可
        leftRotate();
      }
      return; // 必须要!!!
    }
    // 当添加完一个结点后,如果 (左子树的高度 - 右子树的高度) > 1, 右旋转
    if (leftHeight() - rightHeight() > 1) {
      // 如果它的左子树的右子树高度大于它的左子树的高度
      if (left != null && left.rightHeight() > left.leftHeight()) {
        // 先对当前结点的左结点(左子树)->左旋转
        left.leftRotate();
        // 再对当前结点进行右旋转
        rightRotate();
      } else {
        // 直接进行右旋转即可
        rightRotate();
      }
    }
  }
  // 中序遍历
  public void infixOrder() {
    if (this.left != null) {
      this.left.infixOrder();
    }
    System.out.println(this);
    if (this.right != null) {
      this.right.infixOrder();
    }
  }
}

5.4.2、平衡二叉树的定义

// 创建AVLTree
class AVLTree {
  private Node root;
  public Node getRoot() {
    return root;
  }
  // 添加结点的方法
  public void add(Node node) {
    if (root == null) {
      root = node;// 如果root为空则直接让root指向node
    } else {
      root.add(node);
    }
  }
  // 中序遍历
  public void infixOrder() {
    if (root != null) {
      root.infixOrder();
    } else {
      System.out.println("二叉排序树为空,不能遍历");
    }
  }
}

5.4.3、代码测试

  • 代码
public static void main(String[] args) {
    int[] arr = { 10, 11, 7, 6, 8, 9 };
    // 创建一个 AVLTree对象
    AVLTree avlTree = new AVLTree();
    // 添加结点
    for (int i = 0; i < arr.length; i++) {
        avlTree.add(new Node(arr[i]));
    }
    // 遍历
    System.out.println("中序遍历");
    avlTree.infixOrder();
    System.out.println("平衡处理后~~");
    System.out.println("树的高度=" + avlTree.getRoot().height()); // 3
    System.out.println("树的左子树高度=" + avlTree.getRoot().leftHeight()); // 2
    System.out.println("树的右子树高度=" + avlTree.getRoot().rightHeight()); // 2
    System.out.println("当前的根结点=" + avlTree.getRoot());// 8
    System.out.println("根节点的左结点=" + avlTree.getRoot().left);// 7
    System.out.println("根节点的右结点=" + avlTree.getRoot().right);// 10
}


  • 程序运行结果
中序遍历
Node [value=6]
Node [value=7]
Node [value=8]
Node [value=9]
Node [value=10]
Node [value=11]
平衡处理后~~
树的高度=3
树的左子树高度=2
树的右子树高度=2
当前的根结点=Node [value=8]
根节点的左结点=Node [value=7]
根节点的右结点=Node [value=10]
目录
相关文章
|
1月前
|
存储 算法 Unix
简单介绍一些其他的树
简单介绍一些其他的树
|
3月前
|
存储 数据处理 数据库
数据结构之B树、B+树和B*树
数据结构之B树、B+树和B*树
76 0
|
3月前
|
存储 算法 Java
【数据结构】树结构(B树、23树、B+树)
【数据结构】树结构(B树、23树、B+树)
44 0
【数据结构】树结构(B树、23树、B+树)
|
8月前
|
存储 缓存 关系型数据库
B树与B+树
B树与B+树
30 0
B树与B+树
|
8月前
|
存储 缓存 算法
B树,B-树,B+树和B*树
B树,B-树,B+树和B*树
系统发育树初步剖析
1. 什么是系统发育树 2. 如何看系统发育树并确定哪些物种最相关
156 0
|
存储 编解码 算法
第 11 章:树结构实际应用(一)
第 11 章:树结构实际应用
71 0
|
存储 关系型数据库 MySQL
B树和B+树的理解
B树和B+树的理解
B树和B+树的理解
|
存储
心里有点树 (一)
心里有点树 (一)
156 0
|
Java 容器
心里有点树 (二)
心里有点树 (二)
96 0

热门文章

最新文章