数据结构与算法系列17--二叉树

什么是二叉树?

二叉树是每个结点最多有两个子树的树结构,它是最常见的一种树结构。
在这里插入图片描述

一些基本概念

根节点:树的最顶端是节点,如图中的1节点
父节点:图中1就是2和3的父节点
子节点:相对的,2和3就是子节点
兄弟节点:在同一父节点下,像2和3就是兄弟节点
叶子节点:没有子节点的节点,像8,9
节点的高度:节点到叶子节点的最长路径
节点的深度:根节点到这个节点所经历的边的个数
节点的层数:节点的深度加1
树的高度:根节点的高度

什么是满二叉树?

除了叶子节点之外,每个节点都有左右两个子节点,这种二叉树就叫作满二叉树。

什么是完全二叉树?

叶子节点都在最底下两层,最后一层的叶子节点都靠左排列,并且除了最后一层,其他层的节点个数都要达到最大,这种二叉树叫作完全二叉树。

如何储存二叉树?

要储存二叉树,我们有两种方法,一种是基于指针或者引用的二叉链表储存法,一种是基于数组的顺序储存法。
二叉链表储存法:我们可以定义一个链表,每个节点除了储存数据外,还储存了指向左节点的指针和指向右节点的指针。我们只要拿到根节点,就可以通过左右节点的指针,将二叉树串起来。这是我们比较常用的一种储存二叉树的方式。
数组储存法:基于数组的顺序存储法。我们把根节点存储在下标 i=1 的位置,那左子节点存储在下标 2 * i=2 的位置,右子节点存储在 2 * i+1=3 的位置。以此类推。
需要注意的是,当二叉树是一个完全二叉树时,利用数组要比链表储存要省很大是内存,因为此时数组的连续储存空间是被充分利用的,而链表因为要储存额外的指针数据,所以耗费的内存要更大。我们常见的堆其实就是一棵完全二叉树,它的最常用的储存方式也就是数组。

二叉树的遍历

前序遍历:
对于树中的任意节点来说,我们先打印这个节点,然后打印它的左子树,最后打印它的右子树。
中序遍历:
对于树中的任意节点来说,我们先打印它的左子树,再打印它本身,最后打印右子树。
后序遍历:
对于树中的任意节点来说,我们先打印它的左子树,再打印它的右子树,最后打印这个节点本身。
实际上,二叉树的前、中、后序遍历就是一个递归的过程。
前序遍历的伪代码如下:

void preOrder(Node* root) {
  if (root == null) return;
  print root // 此处为伪代码,表示打印 root 节点
  preOrder(root->left);
  preOrder(root->right);
}

上面这三种方式的遍历的时间复杂度都是O(n)。

二叉查找树

二叉查找树是二叉树中最常用的一种类型,也叫二叉搜索树。它是为了实现快速查找而生的,不过,它也不仅仅支持快速一个数据,还支持快速插入,删除一个数据。那它是怎么做到这些呢?
这些依赖于它特殊的结构。二叉查找树要求,在树中的任意一个节点,其左子树的每一个节点的值,都要小于这个节点的值,而右子树每一个节点的值都要大于这个节点的值。

二叉查找树的查找操作

具体过程:我们先取根节点,如果根节点的值等于我们要查找的值,就直接返回。如果要查找的值比跟节点小,那就在左子树中递归查找,如果要查找的数据比根节点的值大,那就在右子树中递归查找。

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;
    }
  }
}

二叉查找树的插入操作

二叉查找树的插入操作的过程有点类似于查找操作,我们也是从根节点开始,依次比较要插入的数据和节点的大小关系,找到合适的位置插入。
如果要插入的数据比节点数据大,并且此时右子树为空,那就将数据插入到右子节点上,如果右子树不为空,那就递归遍历右子树,继续查找要插入的位置。同理,如果要查找的数据比当前节点小,并且此时左子树为空,那就将数据插入到左子节点上,如果不为空,那就递归遍历左子树,继续查找要插入的位置。

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;
    }
  }
}

二叉查找树的删除操作

删除操作相对比较复杂,针对要删除的子节点的个数不同,我们需要分三种情况来处理。
第一种情况是,如果要删除的节点没有子节点,我们只需要直接将父节点中,指向要删除节点的指针置为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;
}

二叉查找树的时间复杂度

二叉查找树的时间复杂度其实都跟树的高度成正比,也就是O(height)。
而如何求树的高度呢?
-只有一个根结点时,二叉树高度为 1

  • 只有左子树时,二叉树深度为左子树高度加 1
  • 只有右子树时,二叉树深度为右子树高度加 1
  • 同时存在左右子树时,二叉树深度为左右子树中高度最大者加 1
    最坏情况下它的时间复杂度是O(n),为什么呢?因为如果做频繁的插入操作,有可能它的结构会退化成一个链表。
    而他最好的时间复杂度是O(logn)。

猜你喜欢

转载自blog.csdn.net/qq_34493908/article/details/84205984