侧边栏壁纸
博主头像
孔子说JAVA博主等级

成功只是一只沦落在鸡窝里的鹰,成功永远属于自信且有毅力的人!

  • 累计撰写 297 篇文章
  • 累计创建 134 个标签
  • 累计收到 4 条评论

目 录CONTENT

文章目录

二叉查找树(Binary Search Tree)的查找、插入及删除操作实战

孔子说JAVA
2022-07-26 / 0 评论 / 0 点赞 / 65 阅读 / 16,554 字 / 正在检测是否收录...

二叉查找树也叫二叉搜索树,是二叉树中最常用的一种类型。它不仅仅支持快速查找一个数据,还支持快速插入、删除一个数据。而所有这些都依赖于二叉查找树的特殊结构。二叉查找树要求,在树中的任意一个节点,其左子树中的每个节点的值,都要小于这个节点的值,而右子树节点的值都大于这个节点的值。

image-1658746320891

1、相关概念

1.1 树(Tree)

树的定义:树为N个结点构成的有限集合。树中有一个称为“根(Root)”的特殊结点,其余结点可分为若干个互不相交的树,称为原来结点的“子树”。

image-1658746429116

这里的每个元素都叫做“节点”,用来连接相邻节点之间的关系称为“父子关系”。如下图,A 节点就是 B 节点的父节点,B 节点是 A 节点的子节点。B、C、D 这三个节点的父节点是同一个节点,所以它们之间互称为兄弟节点。我们把没有父节点的节点叫做根节点,也就是图中的节点 E。我们把没有子节点的节点叫做叶子节点或者叶节点,比如图中的 G、H、I、J、K、L 都是叶子节点。

image-1658746507604

还有几个比较相似的概念:高度(Height)、深度(Depth)、层(Level)。它们的定义是这样的:

  • 节点的高度 = 节点到叶子节点的最长路径(边数)。
  • 节点的深度 = 根节点到这个节点所经历的边的个数。
  • 节点的层数 = 节点的深度 + 1。
  • 树的高度 = 根节点的高度。

image-1658746654315

小技巧:高度从下至上从0开始计算, 深度从上至下从0开始计算,层数从上至下从1开始计算

1.2 二叉树(Binary Tree)

二叉树,顾名思义,每个节点最多有两个“叉”,也就是两个子节点,分别是左子节点和右子节点。不过,二叉树并不要求每个节点都有两个子节点,有的节点只有左子节点,有的节点只有右子节点。下面这几个都是二叉树。以此类推,你可以想象一下四叉树、八叉树长什么样子。

image-1658746757096

  • 这个图里面,有两个比较特殊的二叉树,分别是编号 2 和编号 3 这两个。

满二叉树(是完全二叉树的一种特殊情况)

编号 2 的二叉树中,叶子节点全都在最底层,除了叶子节点之外,每个节点都有左右两个子节点,这种二叉树就叫做满二叉树。

完全二叉树

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

1.3 完全二叉树定义的由来

满二叉树很好理解,也很好识别,但是完全二叉树,很多人可能就分不清楚了。下面是几个完全二叉树和非完全二叉树的例子,大家可以对比看看。

image-1658748135987

完全二叉树定义,目的就是为了方便进行数组形式的存储。

如何表示(或者存储)一棵二叉树?

想要存储一棵二叉树,有两种方法:一种是基于指针或者引用的二叉链式存储法,一种是基于数组的顺序存储法。

1)链式存储法

image-1658748286726

从图中可以很清楚地看到,每个节点有三个属性,其中一个属性存储数据,另外两个属性分别存储指向左右子节点的指针。我们只要拎住根节点,就可以通过左右子节点的指针,把整棵树都串起来。这种存储方式我们比较常用。大部分二叉树代码都是通过这种结构来实现的。

2)顺序存储法

image-1658748348341

如图,我们把根节点存储在下标 i = 1 的位置,那左子节点存储在下标 2 * i = 2 的位置,右子节点存储在 2 * i + 1 = 3 的位置。以此类推,B 节点的左子节点存储在 2 * i = 2 * 2 = 4 的位置,右子节点存储在 2 * i + 1 = 2 * 2 + 1 = 5 的位置。

如果节点 X 存储在数组中下标为 i 的位置,下标为 2 * i 的位置存储的就是左子节点,下标为 2 * i + 1 的位置存储的就是右子节点。反过来,下标为 i/2 的位置存储就是它的父节点。通过这种方式,我们只要知道根节点存储的位置(一般情况下,为了方便计算子节点,根节点会存储在下标为 1 的位置),这样就可以通过下标计算,把整棵树都串起来。

上例是一棵完全二叉树,所以仅仅“浪费”了一个下标为 0 的存储位置(可以从0开始编号)。如果是非完全二叉树,其实会浪费比较多的数组存储空间。看下例:

image-1658748501491

所以,如果某棵二叉树是一棵完全二叉树,那用数组存储无疑是最节省内存的一种方式。因为数组的存储方式并不需要像链式存储法那样,要存储额外的左右子节点的指针。这就是为什么完全二叉树要求最后一层的子节点都靠左的原因。

二叉树的每个节点最多有两个子节点,分别是左子节点和右子节点。二叉树中,有两种比较特殊的树,分别是满二叉树和完全二叉树。满二叉树又是完全二叉树的一种特殊情况。

  • 堆其实就是一种完全二叉树,最常用的存储方式就是数组。
  • 二叉树既可以用链式存储,也可以用数组顺序存储。数组顺序存储的方式比较适合完全二叉树,其他类型的二叉树用数组存储会比较浪费存储空间。

2、二叉树的遍历

二叉树通过何种方式将所有节点都遍历打印出来呢?经典的方法有三种,前序遍历、中序遍历和后序遍历。其中,前、中、后序,表示的是节点与它的左右子树节点遍历打印的先后顺序。

  • 前序遍历:对于树中的任意节点来说,先打印这个节点,然后再打印它的左子树,最后打印它的右子树。

  • 中序遍历:对于树中的任意节点来说,先打印它的左子树,然后再打印它本身,最后打印它的右子树。

  • 后序遍历:对于树中的任意节点来说,先打印它的左子树,然后再打印它的右子树,最后打印这个节点本身。

image-1658748664633

从上图可以看出,每个节点最多会被访问两次,所以遍历操作的时间复杂度,跟节点的个数 n 成正比,也就是说二叉树遍历的时间复杂度是 O(n)。

二叉树的前、中、后序遍历实际上就是一个递归的过程。而写递归代码的关键,就是看能不能写出递推公式,这里的关键就是,如果要解决问题 A,就假设子问题 B、C 已经解决,然后再来看如何利用 B、C 来解决 A。所以,我们可以把前、中、后序遍历的递推公式都写出来。

//前序遍历的递推公式
preOrder(r)=print r->preOrder(r->left)->preOrder(r->right)
//中序遍历的递推公式
inOrder(r)=print inOrder(r->left)->r->inOrder(r->right)
//后序遍历的递推公式
postOrder(r)=print postOrder(r->left)->postOrder(r->right)

先序遍历代码:

/**
 * 先序遍历(先根遍历):先访问根节点,再访问左右子节点
 */
public void preOrder() {
    System.out.print("先序遍历(先根遍历): ");
    preOrder(root);
    System.out.println();
}

private void preOrder(TreeNode node) {
    if (node != null) {
        System.out.print(node.data + " ");
        preOrder(node.left);
        preOrder(node.right);
    }
}

中序遍历代码:

/**
 * 中序遍历=:先访问左子节点,再访问根节点,最后访问右子节点
 */
public void inOrder() {
    System.out.print("中序遍历: ");
    inOrder(root);
    System.out.println();
}

private void inOrder(TreeNode node) {
    if (node != null) {
        inOrder(node.left);
        System.out.print(node.data + " ");
        inOrder(node.right);
    }
}

后序遍历代码:

/**
 * 后序遍历:先访问左子节点,再访问右子节点,最后访问根节点
 */
public void postOrder() {
    System.out.print("后序遍历: ");
    postOrder(root);
    System.out.println();
}

private void postOrder(TreeNode node) {
    if (node != null) {
        postOrder(node.left);
        postOrder(node.right);
        System.out.print(node.data + " ");
    }
}

3、二叉查找树(Binary Search Tree)

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

image-1658746320891

3.1 二叉查找树的查找操作

根据二叉查找树的性质,其左子树中的每个节点的值,都要小于这个节点的值,而右子树节点的值都大于这个节点的值。所以在二叉查找树中查找一个节点的操作就相对比较简单,我们先取根节点,如果它等于我们要查找的数据,那就返回。如果要查找的数据比根节点的值小,那就在左子树中递归(或迭代)查找;如果要查找的数据比根节点的值大,那就在右子树中递归(或迭代)查找。如下图所示:

image-1658749115625

基于递归实现

public TreeNode find(int value) {
    return findInternal(value, root);
}
 
private TreeNode findInternal(int value, TreeNode node) {
    //递归终止条件
    if (node == null) return null;
    if (node.data == value) return node;
    if (node.data > value) {
        return findInternal(value, node.left);
    } else {
        return findInternal(value, node.right);
    }
}

基于非递归的实现

public TreeNode find1(int value) {
    TreeNode node = root;
    //终止条件
    while (node != null && node.data != value) {
        if (node.data > value) {
            node = node.left;
        } else {
            node = node.right;
        }
    }
    return node;
}

3.2 二叉查找树的插入操作

二叉查找树的插入过程有点类似查找操作。新插入的数据一般都是在叶子节点上,所以只需要从根节点开始,依次比较要插入的数据和节点的大小关系。

  • 如果树中找到节点的值等于待插入的节点,则直接返回。
  • 如果要插入的数据比节点的数据大,并且节点的右子树为空,就将新数据直接插到右子节点的位置。
  • 如果要插入的数据比节点的数据大,并且节点的右子树不为空,就再递归遍历右子树,直到找到合适的插入位置。
  • 如果要插入的数据比节点的数值小,并且节点的左子树为空,就将新数据插入到左子节点的位置。
  • 如果要插入的数据比节点的数值小,并且节点的左子树不为空,就再递归遍历左子树,直到找到合适的插入位置。

image-1658749146811

public void insert(int value) {
    //树还没有初始化
    if (root == null) {
        root = new TreeNode(value);
        return;
    }
    TreeNode p = root;
    while (p != null) {
        //存在,不需要插入
        if (p.data == value) return;
        //往 p 的右子树查找
        if (value > p.data) {
            //p 的 右子树为空
            if (p.right == null) {
                p.right = new TreeNode(value);
                return;
            }
            p = p.right;
        }
 
        //往p的左子树查找
        if (value < p.data) {
            //p的左子树为空
            if (p.left == null) {
                p.left = new TreeNode(value);
                return;
            }
            //否则继续在p的左子树中查找
            p = p.left;
        }
    }
}

3.3 二叉查找树的删除操作

二叉查找树的查找、插入操作都比较简单,相对来说删除操作就比较复杂了。针对要删除节点的子节点个数的不同,我们需要分三种情况来处理。

3.3.1 要删除的节点没有子节点

如果要删除的节点没有子节点,我们只需要直接将父节点中,指向要删除节点的指针置为 null。比如图中的删除节点 55。

3.3.2 要删除的节点只有一个子节点

如果要删除的节点只有一个子节点(只有左子节点或者右子节点),我们只需要更新父节点中,指向要删除节点的指针,让它指向要删除节点的子节点就可以了。比如图中的删除节点 13。

3.3.3 要删除的节点有两个子节点

如果要删除的节点有两个子节点,这就比较复杂了。我们需要找到这个节点的右子树中的最小节点,把它替换到要删除的节点上。然后再删除掉这个最小节点,因为最小节点肯定没有左子节点(如果有左子结点,那就不是最小节点了),所以,我们可以应用上面两条规则来删除这个最小节点。比如图中的删除节点 18。

image-1658749167559

public void delete(int value) {
    //p 表示指向要删除的节点,初始化指向跟结点
    TreeNode p = root;
    //pp 表示指向p的父节点
    TreeNode pp = null;
    while (p != null && p.data != value) {
        pp = p;
        if (value > p.data) p = p.right;
        else p = p.left;
    }
 
    //没有找到节点p
    if (p == null) {
        return;
    }
 
    //要删除的节点有两个子节点
    if (p.left != null && p.right != null) {
        TreeNode minP = p.right;
        TreeNode minPP = p;
        while (minP.left != null) {
            minPP = minP;
            minP = minP.left;
        }
        //值替换
        p.data = minP.data;
        //下面变为删除minP了
        p = minP;
        pp = minPP;
    }
 
    //删除结点是叶子节点或者仅有一个子节点
    TreeNode child;
    if (p.left != null) {
        child = p.left;
    } else if (p.right != null) {
        child = p.right;
    } else child = null;
 
    //删除跟结点
    if (pp == null) root = child;
    else if (pp.left == p) pp.left = child;
    else pp.right = child;
}

其实还可以用一种比较简单的思路,删除节点并不是真正的删除,节点不是真的删除,而是打一个标记。但是这样有一个缺陷,如果删除的节点过多,会占用过多的内存空间,而且查找,和插入等的需要遍历的元素也会相应的增多。

  • public boolean deleted;

当然删除的元素只是被打一个标记,再插入时如果元素相同,可以重复使用该位置,比如插入元素 3 ,此时打一个标记,deleted = true,过段时间后再插入元素 3 ,则可简单的把标记deleted = false即可。

3.4 二叉查找树的其他操作

除了插入、删除、查找操作之外,二叉查找树中还可以支持快速地查找最大节点和最小节点、前驱节点和后继节点。二叉查找树除了支持上面几个操作之外,还有一个重要的特性,就是中序遍历二叉查找树,可以输出有序的数据序列,时间复杂度是 O(n),非常高效。因此,二叉查找树也叫作二叉排序树。

  • 根据二叉查找树的性质,对于任何一个节点,其左子树的节点都比其小,右子树的节点都比其大,所以需要寻找最小的节点,只需要不断的往左子树查找,直到某一个节点的左子树为空,或者是到达叶子节点。
public TreeNode findMin() {
   if (root == null) {
      return null;
   }
   TreeNode node = root;
   while (node.left != null) {
      node = node.left;
   }
   return node;
}
  • 快速查找最大节点与最小节点是类似的,快速查找最大节点就是不断的往右进行查找,直到每一个节点的左子树为空,或者是到达叶子节点
public TreeNode findMax() {
   if (root == null) {
      return null;
   }
   TreeNode node = root;
   while (node.right != null) {
      node = node.right;
   }
   return node;
}

4、支持重复数据的二叉查找树

前面讲二叉查找树的时候,我们默认树中节点存储的都是数字。很多时候,在实际的软件开发中,我们在二叉查找树中存储的,是一个包含很多字段的对象。我们利用对象的某个字段作为键值(key)来构建二叉查找树。我们把对象中的其他字段叫作卫星数据。

前面我们讲的二叉查找树的操作,针对的都是不存在键值相同的情况。那如果存储的两个对象键值相同,这种情况该怎么处理呢?我这里有两种解决方法。

第一种方法比较容易。二叉查找树中每一个节点不仅会存储一个数据,因此我们通过链表和支持动态扩容的数组等数据结构,把值相同的数据都存储在同一个节点上。有点类似与散列表的拉链冲突解决办法。

第二种方法比较不好理解,不过更加优雅。每个节点仍然只存储一个数据。在查找插入位置的过程中,如果碰到一个节点的值,与要插入数据的值相同,我们就将这个要插入的数据放到这个节点的右子树,也就是说,把这个新插入的数据当作大于这个节点的值来处理。

image-1658749244916

当要查找数据的时候,遇到值相同的节点,我们并不停止查找操作,而是继续在右子树中查找,直到遇到叶子节点,才停止。这样就可以把键值等于要查找值的所有节点都找出来。

image-1658749254236

对于删除操作,我们也需要先查找到每个要删除的节点,然后再按前面讲的删除操作的方法,依次删除。

image-1658749264941

5、二叉查找树的时间复杂度分析

现在,我们来分析一下,二叉查找树的插入、删除、查找操作的时间复杂度。实际上,二叉查找树的形态各式各样。比如这个图中,对于同一组数据,我们构造了三种二叉查找树。它们的查找、插入、删除操作的执行效率都是不一样的。图中第一种二叉查找树,根节点的左右子树极度不平衡,已经退化成了链表,所以查找的时间复杂度就变成了 O(n)。

image-1658749284430

我刚刚其实分析了一种最糟糕的情况,我们现在来分析一个最理想的情况,二叉查找树是一棵完全二叉树(或满二叉树)。这个时候,插入、删除、查找的时间复杂度是多少呢?

从我前面的例子、图,以及还有代码来看,不管操作是插入、删除还是查找,时间复杂度其实都跟树的高度成正比,也就是 O(height)

既然这样,现在问题就转变成另外一个了,也就是,如何求一棵包含 n 个节点的完全二叉树的高度?树的高度就等于最大层数减一,为了方便计算,我们转换成层来表示。从图中可以看出,包含 n 个节点的完全二叉树中,第一层包含 1 个节点,第二层包含 2 个节点,第三层包含 4 个节点,依次类推,下面一层节点个数是上一层的 2 倍,第 K 层包含的节点个数就是 2^(K-1)。

不过,对于完全二叉树来说,最后一层的节点个数有点儿不遵守上面的规律了。它包含的节点个数在 1 个到 2^(L-1) 个之间(我们假设最大层数是 L)。如果我们把每一层的节点个数加起来就是总的节点个数 n。也就是说,如果节点的个数是 n,那么 n 满足这样一个关系:

n >= 1+2+4+8+...+2^(L-2)+1

n <= 1+2+4+8+...+2^(L-2)+2^(L-1)

借助等比数列的求和公式,我们可以计算出,L 的范围是[log2(n+1), log2n +1]。完全二叉树的层数小于等于 log2n +1,也就是说,完全二叉树的高度小于等于 log2n

显然,极度不平衡的二叉查找树,它的查找性能肯定不能满足我们的需求。我们需要构建一种不管怎么删除、插入数据,在任何时候,都能保持任意节点左右子树都比较平衡的二叉查找树(平衡二叉查找树)。

二叉查找树,它支持快速地查找、插入、删除操作。

二叉查找树中,每个节点的值都大于左子树节点的值,小于右子树节点的值。不过,这只是针对没有重复数据的情况。对于存在重复数据的二叉查找树,我介绍了两种构建方法,一种是让每个节点存储多个值相同的数据;另一种是,每个节点中存储一个数据。针对这种情况,我们只需要稍加改造原来的插入、删除、查找操作即可。

在二叉查找树中,查找、插入、删除等很多操作的时间复杂度都跟树的高度成正比。两个极端情况的时间复杂度分别是 O(n) 和 O(logn),分别对应二叉树退化成链表的情况和完全二叉树。

6、二叉查找树和散列表

散列表的插入、删除、查找操作的时间复杂度可以做到常量级的 O(1),非常高效。而二叉查找树在比较平衡的情况下,插入、删除、查找操作时间复杂度才是 O(logn),相对散列表,好像并没有什么优势,那我们为什么还要用二叉查找树呢?我认为有下面几个原因:

  1. 散列表中的数据是无序存储的,如果要输出有序的数据,需要先进行排序。而对于二叉查找树来说,我们只需要中序遍历,就可以在 O(n) 的时间复杂度内,输出有序的数据序列。
  2. 散列表扩容耗时很多,而且当遇到散列冲突时,性能不稳定,尽管二叉查找树的性能不稳定,但是在工程中,我们最常用的平衡二叉查找树的性能非常稳定,时间复杂度稳定在 O(logn)。
  3. 笼统地来说,尽管散列表的查找等操作的时间复杂度是常量级的,但因为哈希冲突的存在,这个常量不一定比 logn 小,所以实际的查找速度可能不一定比 O(logn) 快。加上哈希函数的耗时,也不一定就比平衡二叉查找树的效率高。
  4. 散列表的构造比二叉查找树要复杂,需要考虑的东西很多。比如散列函数的设计、冲突解决办法、扩容、缩容等。平衡二叉查找树只需要考虑平衡性这一个问题,而且这个问题的解决方案比较成熟、固定。
  5. 最后,为了避免过多的散列冲突,散列表装载因子不能太大,特别是基于开放寻址法解决冲突的散列表,不然会浪费一定的存储空间。

综合这几点,平衡二叉查找树在某些方面还是优于散列表的,所以,这两者的存在并不冲突。我们在实际的开发过程中,需要结合具体的需求来选择使用哪一个。

7、完整代码实现

7.1 节点定义

public class TreeNode {
    public TreeNode left;
    public TreeNode right;
    public int data;

    public TreeNode() {
    }

    public TreeNode(int data) {
        this.data = data;
    }

    @Override
    public String toString() {
        return "TreeNode{" +
                "left=" + left +
                ", right=" + right +
                ", data=" + data +
                '}';
    }
}

7.2 二叉树操作

package com.kz.example.algorithm.find.treeNode;

import java.util.Stack;

/**
 * 二叉查找树
 */
public class BinarySearchTree {
    private TreeNode root;

    /**
     * 查找节点
     *
     * @param value 节点的值
     * @return
     */
    public TreeNode find(int value) {
        return findInternal(value, root);
    }

    /**
     * 查找节点
     *
     * @param value 节点的值
     * @param node  节点
     * @return
     */
    private TreeNode findInternal(int value, TreeNode node) {
        //递归终止条件
        if (node == null) return null;
        if (node.data == value) return node;
        if (node.data > value) {
            return findInternal(value, node.left);
        } else {
            return findInternal(value, node.right);
        }
    }

    /**
     * 查找节点
     *
     * @param value 节点的值
     * @return
     */
    public TreeNode find1(int value) {
        TreeNode node = root;
        //终止条件
        while (node != null && node.data != value) {
            if (node.data > value) {
                node = node.left;
            } else {
                node = node.right;
            }
        }
        return node;
    }

    /**
     * 插入节点
     *
     * @param value 节点的值
     */
    public void insert(int value) {
        //树还没有初始化
        if (root == null) {
            root = new TreeNode(value);
            return;
        }
        TreeNode p = root;
        while (p != null) {
            //存在,不需要插入
            if (p.data == value) return;
            //往 p 的右子树查找
            if (value > p.data) {
                //p 的 右子树为空
                if (p.right == null) {
                    p.right = new TreeNode(value);
                    return;
                }
                p = p.right;
            }

            //往p的左子树查找
            if (value < p.data) {
                //p的左子树为空
                if (p.left == null) {
                    p.left = new TreeNode(value);
                    return;
                }
                //否则继续在p的左子树中查找
                p = p.left;
            }
        }
    }

    /**
     * 删除节点
     *
     * @param value 节点的值
     */
    public void delete(int value) {
        //p 表示指向要删除的节点,初始化指向跟结点
        TreeNode p = root;
        //pp 表示指向p的父节点
        TreeNode pp = null;
        while (p != null && p.data != value) {
            pp = p;
            if (value > p.data) p = p.right;
            else p = p.left;
        }

        //没有找到节点p
        if (p == null) {
            return;
        }

        //要删除的节点有两个子节点
        if (p.left != null && p.right != null) {
            TreeNode minP = p.right;
            TreeNode minPP = p;
            while (minP.left != null) {
                minPP = minP;
                minP = minP.left;
            }
            //值替换
            p.data = minP.data;
            //下面变为删除minP了
            p = minP;
            pp = minPP;
        }

        //删除结点是叶子节点或者仅有一个子节点
        TreeNode child;
        if (p.left != null) {
            child = p.left;
        } else if (p.right != null) {
            child = p.right;
        } else child = null;

        //删除跟结点
        if (pp == null) root = child;
        else if (pp.left == p) pp.left = child;
        else pp.right = child;
    }

    /**
     * 查找最小值
     *
     * @return
     */
    public TreeNode findMin() {
        if (root == null) {
            return null;
        }
        TreeNode node = root;
        while (node.left != null) {
            node = node.left;
        }
        return node;
    }

    /**
     * 查找最大值
     *
     * @return
     */
    public TreeNode findMax() {
        if (root == null) {
            return null;
        }
        //一直往右找左节点
        TreeNode node = root;
        while (node.right != null) {
            node = node.right;
        }
        return node;
    }

    private void checkEmpty() {
        if (isEmpty()) {
            throw new RuntimeException("没有这样的节点");
        }
    }

    /**
     * 返回树中的节点数
     *
     * @return
     */
    public int size() {
        return size(root);
    }

    /**
     * 节点个数
     *
     * @param node
     * @return
     */
    private int size(TreeNode node) {
        if (node == null) {
            return 0;
        }
        //当前节点(1) + 左子树节点数 + 右子树节点数
        return 1 + size(node.left) + size(node.right);
    }

    /**
     * 是否为空
     *
     * @return
     */
    public boolean isEmpty() {
        return root == null;
    }

    /**
     * 设置为空
     */
    public void makeEmpty() {
        root = null;
    }

    /**
     * 先序遍历(先根遍历):先访问根节点,再访问左右子节点
     */
    public void preOrder() {
        System.out.print("先序遍历(先根遍历): ");
        preOrder(root);
        System.out.println();
    }

    private void preOrder(TreeNode node) {
        if (node != null) {
            System.out.print(node.data + " ");
            preOrder(node.left);
            preOrder(node.right);
        }
    }

    /**
     * 中序遍历=:先访问左子节点,再访问根节点,最后访问右子节点
     */
    public void inOrder() {
        System.out.print("中序遍历: ");
        inOrder(root);
        System.out.println();
    }

    private void inOrder(TreeNode node) {
        if (node != null) {
            inOrder(node.left);
            System.out.print(node.data + " ");
            inOrder(node.right);
        }
    }

    /**
     * 后序遍历:先访问左子节点,再访问右子节点,最后访问根节点
     */
    public void postOrder() {
        System.out.print("后序遍历: ");
        postOrder(root);
        System.out.println();
    }

    private void postOrder(TreeNode node) {
        if (node != null) {
            postOrder(node.left);
            postOrder(node.right);
            System.out.print(node.data + " ");
        }
    }

    /**
     * 深度优先遍历,相当于先序遍历
     */
    public void depthOrder() {
        depthOrder(root);
        System.out.println();
    }

    /**
     * 和图的深度优先搜索类似
     *
     * @param node
     */
    private void depthOrder(TreeNode node) {
        if (node != null) {
            Stack<TreeNode> stack = new Stack<>();
            stack.push(node);
            while (!stack.isEmpty()) {
                TreeNode p = stack.pop();
                System.out.print(p.data + " ");
                //先push right,再push left 达到pop时先访问left的效果
                if (p.right != null) {
                    stack.push(p.right);
                }
                if (p.left != null) {
                    stack.push(p.left);
                }
            }
        }
    }

    /**
     * 删除二叉树中最小节点
     */
    public void removeMin() {
        root = removeMin(root);
    }

    /**
     * 删除node节点最小子树节点
     *
     * @param node
     * @return 新的子树根节点,而不是返回删除的节点
     */
    private TreeNode removeMin(TreeNode node) {
        //找到了最小左子树节点,返回其右子树
        //目的是让其父节点的left指向其右子树
        if (node.left == null) {
            return node.right;
        }
        //更新引用
        node.left = removeMin(node.left);
        return node;
    }

    public void printTree() {
        if (isEmpty()) {
            System.out.println("Empty tree");
        } else {
            print(root);
        }
    }

    private void print(TreeNode node) {
        System.out.println(node);
    }

    public static void main(String[] args) {
        BinarySearchTree tree = new BinarySearchTree();
        // 插入的值重复时会忽略
        tree.insert(1);
        tree.insert(1);
        tree.insert(3);
        tree.insert(2);
        tree.insert(1);
        tree.insert(4);
        tree.insert(5);
        int size = tree.size();
        System.out.println("节点个数 = " + size);
        tree.preOrder();
        tree.inOrder();
        tree.postOrder();
        TreeNode treeNode = tree.find(1);
        System.out.println(treeNode.toString());
        TreeNode min = tree.findMin();
        System.out.println("min = " + min.toString());
        TreeNode max = tree.findMax();
        System.out.println("max = " + max.toString());
        tree.delete(5);
        max = tree.findMax();
        System.out.println("max = " + max.toString());
    }
}

image-1658797267805

7.3 支持重复数据逇二叉树

package com.kz.example.algorithm.find.treeNode;

import java.util.ArrayList;
import java.util.List;

public class BinarySearchTreeRepeatData {
    private TreeNode root;

    /**
     * 查找符合条件的节点
     *
     * @param value 节点的值
     * @return
     */
    public List<TreeNode> find(int value) {
        List<TreeNode> listTreeNode = new ArrayList<>();
        TreeNode p = root;
        while (p != null) {
            //如果找到相等的,则加入到返回结果,同时继续去右子树查找
            if (p.data == value) {
                listTreeNode.add(p);
                p = p.right;
            } else if (p.data < value) {
                p = p.right;
            } else {
                p = p.left;
            }
        }
        return listTreeNode;
    }

    /**
     * 插入数据
     *
     * @param value 节点的值
     */
    public void insert(int value) {
        //树没有初始化
        if (root == null) {
            root = new TreeNode(value);
            return;
        }
        TreeNode p = root;
        while (p != null) {
            if (p.data <= value) {
                if (p.right == null) {
                    TreeNode newNode = new TreeNode(value);
                    p.right = newNode;
                    return;
                }
                p = p.right;
            } else {
                if (p.left == null) {
                    TreeNode newNode = new TreeNode(value);
                    p.left = newNode;
                    return;
                }
                p = p.left;
            }
        }
    }

    /**
     * 删除所有符合的节点
     *
     * @param data 节点的值
     */
    public void deleteNode(int data) {
        TreeNode p = this.root;
        TreeNode pParent = null;    // p 的父节点

        while (p != null) {
            if (p.data == data) {
                TreeNode pRe = delete(pParent, p);
                p = pRe;
            } else if (p.data < data) {
                pParent = p;
                p = p.right;
            } else {
                pParent = p;
                p = p.left;
            }
        }
    }

    /**
     * 删除指定节点
     *
     * @param pParent
     * @param p
     */
    public TreeNode delete(TreeNode pParent, TreeNode p) {
        int flag = 0;
        TreeNode pBak = p;

        // 要删除的节点有左右子节点
        if (p.left != null && p.right != null) {
            TreeNode minP = p.right;
            TreeNode minPP = p; // minP 的父节点


            while (minP.left != null) {
                minPP = minP;
                minP = minP.left;
            }

            p.data = minP.data; // 将 minP 的数据替换到 p 中

            /* 技巧:对右子树中最小的节点进行删除,
               这种情况跟要删除的节点只有一颗子树或者没有子树情况一样,
               所以这边将 minPP 赋值给 pParent,minP 赋值给 p,那么重复使用一段代码 */
            pParent = minPP;
            p = minP;

            flag = 1;
        }

        TreeNode child = null;
        // 要删除的节点只有左节点的情况
        if (p.left != null) {
            child = p.left;
        } else if (p.right != null) {   // 要删除的节点只有右子节点的情况
            child = p.right;
        } else {    // 要删除的节点左右子节点都无的情况
            child = null;
        }

        // 删除的是根节点的情况
        if (pParent == null) {
            this.root = child;
        }

        // 将 p 父节点的左/右子树重新指向
        if (pParent.left == p) {
            pParent.left = child;
        } else if (pParent.right == p) {
            pParent.right = child;
        }
        return flag == 1 ? pBak : child;
    }

    /**
     * 快速查找最小节点
     *
     * @return
     */
    public TreeNode findMin() {
        if (root == null) {
            return null;
        }
        //一直往左找左节点
        TreeNode node = root;
        while (node.left != null) {
            node = node.left;
        }
        return node;
    }

    /**
     * 快速查找最大节点
     *
     * @return
     */
    public TreeNode findMax() {
        if (root == null) {
            return null;
        }
        //一直往右找左节点
        TreeNode node = root;
        while (node.right != null) {
            node = node.right;
        }
        return node;
    }

    public static void main(String[] args) {
        BinarySearchTreeRepeatData repeatData = new BinarySearchTreeRepeatData();
        repeatData.insert(1);
        repeatData.insert(1);
        repeatData.insert(3);
        repeatData.insert(2);
        repeatData.insert(1);
        repeatData.insert(4);
        repeatData.insert(5);
        List<TreeNode> listTreeNode = repeatData.find(1);
        for(TreeNode node: listTreeNode) {
            System.out.println(node.toString());
        }
        TreeNode min = repeatData.findMin();
        System.out.println("min = "+min.toString());
        TreeNode max = repeatData.findMax();
        System.out.println("max = "+max.toString());
        repeatData.deleteNode(5);
        max = repeatData.findMax();
        System.out.println("max = "+max.toString());
    }
}
0

评论区