0、引例
思考:如何在n个动态的整数中搜索某个整数?(查看其是否存在)
1、二叉搜索树(Binary Search Tree)
1.1、概念
若一个二叉树满足:
- 任意一个节点的值都大于其左子树所有节点的值。
- 任意一个节点的值都小于其右子树所有节点的值。
则称此二叉树为“二叉搜索树”。
注:
- 二叉搜索树是二叉树的一种,是应用非常广泛的一种二叉树,英文简称为
BST
。
- 二叉搜索树又被称为:二叉查找树、二叉排序树。
1.2、特点
2、二叉搜索树的设计
2.1、类的结构设计
在编程之前,我们需要先对二叉搜索树类的结构进行设计。我们这里采样三叉链表存储二叉搜索树。
二叉搜索树类需要的成员主要包括:
**注:**内部类的成员可以不写访问权限。
**注:**因为二叉搜索树不需要预先分配内存,所以BinarySearchTree
不用写构造函数。
2.2、接口设计
- int size(); //元素的数量
- boolean isEmpty(); //是否为空
- void clear(); //清空所有元素
- void add(E element); //添加元素
- void remove(E element); //删除元素
- boolean contains(E element); //搜索(查找二叉搜索树中是否包含某元素)
注:
- 与动态数组的接口相比,二叉树的元素是没有索引(
index
)这一概念的。
- 用户处理(添加、删除、查找等)的是元素,他们是不关心二叉树内部节点结构的。
3、二叉搜索树的实现
3.1、编程实现
新建一个项目,在其中:
① 先新建一个名称为BinarySearchTree
(包名为:com.atangbiji
)的类,用来自己实现二叉搜索树;
② 再新建一个名称为Main
(包名为:com.atangbiji
)的类,用来调用(测试)自己封装的二叉搜索树。如下图所示:
③接口实现,我们这里直接考虑节点元素均为泛型的情况。
(1)定义私有成员变量
BinarySearchTree.java
文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| package com.atangbiji;
public class BinarySearchTree<E> {
private int size; private Node<E> root; private static class Node<E> { E element; Node<E> left; Node<E> right; Node<E> parent; public Node(E element, Node<E> parent) { this.element = element; this.parent = parent; } } }
|
**注:**由于一般情况下,某一节点的父节点一定存在,而其左节点或右节点可能不存在。因此,我们在设计内部静态节点类的构造函数时,只选择将父节点作为其形参。
(2)实现size()接口
BinarySearchTree.java
文件:
1 2 3 4 5 6 7 8
|
public int size() { return size; }
|
(3)实现isEmpty()接口
BinarySearchTree.java
文件:
1 2 3 4 5 6 7 8
|
public boolean isEmpty() { return size == 0; }
|
(4)实现clear()接口
BinarySearchTree.java
文件:
1 2 3 4 5 6 7 8 9
|
public void clear() { root = null; size = 0; }
|
(5)实现add(E element)接口
a、元素是否为空?
在添加元素前需要先检查元素是否为空。
BinarySearchTree.java
文件:
1 2 3 4 5 6 7 8 9 10
|
private void elementNotNullCheck(E element) { if (element == null) { throw new IllegalArgumentException("element must not null !"); } }
|
b、节点元素的比较
因为二叉搜索树的节点元素必须具备可比较性,所以我们需要自定义节点元素的比较函数。
- 当节点元素为数字等基本数据类型时,我们可以直接比较两者大小。
- 当节点元素为对象时,我们可以通过
Java
语言自带的Comparable
接口和Comparator
接口来自定义对象的比较逻辑。
实际应用中,我们可以使用如下三种方法来自定义节点元素对象的比较逻辑。其中:
①Comparable
接口法(方法一)主要处理节点元素数据类型相同,且只有一套比较逻辑的情况;
②Comparator
接口法(方法二)主要处理节点元素数据类型相同,但可能具有多套比较逻辑的情况;
③方法三则是方法一和方法二的完美结合,同时兼顾了节点元素可能具有一套或多套比较逻辑的情况,也是我们推荐使用的实现方法。
具体实现方法如下:
方法一:Comparable接口法
①新建一个Person
类,添加成员变量:age
、name
,按“Shift+Alt+S
”键,通过弹出菜单快速生成构造函数。
Person.java
文件:
1 2 3 4 5 6 7 8 9 10 11 12
| package com.atangbiji;
public class Person { private int age; private String name; public Person(int age, String name) { this.age = age; this.name = name; } }
|
②新建一个Comparable
接口类,在其中自定义一个int compareTo(E e)
接口函数,用作节点元素间的相互比较。
Comparable.java
文件:
1 2 3 4 5 6
| package com.atangbiji;
public interface Comparable<E> { int compareTo(E e); }
|
③在Person
类中实现Comparable
接口类中的int compareTo(E e)
接口函数。
我们这里假定年龄大的人所对应的节点元素大。
Person.java
文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| package com.atangbiji;
public class Person implements Comparable<Person>{ private int age; private String name; public Person(int age, String name) { this.age = age; this.name = name; }
@Override public int compareTo(Person e) { return age - e.age; } }
|
④在BinarySearchTree
类中继承Comparable
接口类。
BinarySearchTree.java
文件:
1 2 3 4
| package com.atangbiji; public class BinarySearchTree<E extends Comparable> { }
|
此时,我们便可以在节点元素的比较函数中调用int compareTo(E e)
接口函数了。
BinarySearchTree.java
文件:
1 2 3 4 5 6 7 8 9
|
private int compare(E e1,E e2) { return e1.compareTo(e2); }
|
需要说明的是:由于上述方法的比较逻辑是在Person
类内部实现的,所以无论你创建多少个二叉搜索树的对象,它们的Person
对象的比较逻辑都将是同一种逻辑,即:Person
对象只能是年龄大的节点元素大。
方法二:Comparator(比较器)接口法
对于节点数据类型相同的二叉搜索树,由于不同的二叉搜索树对象可能需要不同的比较逻辑(如:对于节点元素均为Person
对象的二叉搜索树,有的二叉搜索树假定年龄大的人所对应的节点元素大,而有的二叉搜索树假定年龄大的人所对应的节点元素小)。为了满足这种不同二叉搜索树对象间比较逻辑的个性化需求,我们可以让用户在Person
类外部使用不同的比较器来实现不同的比较逻辑。然后在新建二叉搜索树对象时,将相应的比较器作为形参传入,用来初始化对象。具体实现过程如下:
①新建一个Comparator
接口类,在其中自定义一个int compare(E e1, E e2)
接口函数,用作节点元素间的相互比较。
Comparator.java
文件:
1 2 3 4 5 6
| package com.atangbiji;
public interface Comparator<E> { int compare(E e1, E e2); }
|
②在BinarySearchTree
类中定义一个Comparator
私有变量,并通过一个有形参的构造函数对其初始化。
BinarySearchTree.java
文件:
1 2 3 4 5
| private Comparator<E> comparator;
public BinarySearchTree(Comparator<E> comparator) { this.comparator = comparator; }
|
③在Person
类中按“Shift+Alt+S
”键自动生成成员变量的get
和set
方法,供其它类调用。
Person.java
文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| public int getAge() { return age; }
public void setAge(int age) { this.age = age; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
|
④新建一个PersonComparator1
类,在其内部实现比较器逻辑:年龄大的人所对应的节点元素大。
PersonComparator1.java
文件:
1 2 3 4 5 6 7 8 9 10 11
| package com.atangbiji;
public class PersonComparator1 implements Comparator<Person> {
@Override public int compare(Person e1, Person e2) { return e1.getAge() - e2.getAge(); } }
|
⑤新建一个PersonComparator2
类,在其内部实现与PersonComparator1
相反的比较器逻辑:年龄大的人所对应的节点元素小。
PersonComparator2.java
文件:
1 2 3 4 5 6 7 8 9 10 11
| package com.atangbiji;
public class PersonComparator2 implements Comparator<Person> { @Override public int compare(Person e1, Person e2) { return e2.getAge() - e1.getAge(); } }
|
⑥此时BinarySearchTree
类不需要再继承Comparable
接口类。
BinarySearchTree.java
文件:
1 2 3
| public class BinarySearchTree<E>{ }
|
此时,我们便可以在节点元素的比较函数中通过Comparator
比较器调用int compare(E e1, E e2)
接口函数了。
BinarySearchTree.java
文件:
1 2 3 4 5 6 7 8 9
|
private int compare(E e1,E e2) { return comparator.compare(e1, e2); }
|
⑦这样在新建二叉搜索树对象时,我们便可以将自定义的比较逻辑(PersonComparator1
或PersonComparator2
)通过相应的Comparator
比较器作为形参传入。
Main.java
文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| package com.atangbiji;
public class Main { public static void main(String[] args) {
PersonComparator1 cmp1 = new PersonComparator1(); BinarySearchTree<Person> bst1 = new BinarySearchTree<>(cmp1); bst1.add(new Person(12, "Tom")); bst1.add(new Person(15, "Jim")); PersonComparator2 cmp2 = new PersonComparator2(); BinarySearchTree<Person> bst2 = new BinarySearchTree<>(cmp2); bst2.add(new Person(12, "Tom")); bst2.add(new Person(15, "Jim")); } }
|
通过这种方法,我们便实现了不同二叉搜索树对象间比较逻辑的个性化定制。
★ 方法三:将Comparable接口与Comparator接口完美结合
方法二虽然实现了不同二叉搜索树对象间比较逻辑的个性化定制,但它要求我们每次在创建二叉搜索树对象的同时都要传入一个自定义的Comparator
比较器。实际应用中,对于节点数据类型相同的二叉搜索树,虽然不同的二叉搜索树对象可能会出现不同的比较逻辑,但出现这种情况的概率很低。即:节点数据类型相同的二叉搜索树一般具有相同的(只有一套)比较逻辑。
因此,对于节点数据类型相同的二叉搜索树:
- 当它们的节点元素具有相同的比较逻辑时(大多数情况),我们希望在节点元素类内部使用
Comparable
接口实现比较逻辑,这样我们在新建二叉搜索树对象时就不用再传入比较器了;
- 当它们的节点元素具有多套比较逻辑时(极少数情况),我们在节点元素类外部使用
Comparator
比较器实现比较逻辑,并在新建二叉搜索树对象时将其作为形参传入。
为了让我们设计的二叉搜索树兼顾上述两种比较逻辑,我们将方法一中的Comparable
接口与方法二中的Comparator
接口综合起来,为用户提供两套构造函数(有形参和无形参)。
其中:
- 有形参的构造函数用来处理用户自定义节点元素的比较逻辑时的情况;
- 无形参的构造函数用来处理用户未定义节点元素的比较逻辑时的情况。
具体实现过程如下:
①在BinarySearchTree
类中再定义一个无形参的构造函数,用来处理用户未定义节点元素的比较逻辑时的情况。
BinarySearchTree.java
文件:
1 2 3 4
| public BinarySearchTree() { this(null); }
|
②考虑到当用户自定义节点元素的比较逻辑时,我们没必要让节点元素类(如:Person
)内部再实现Comparable
接口了。只有当用户未定义节点元素的比较逻辑时,我们才需要在节点元素类(如:Person
)内部实现Comparable
接口。因此,BinarySearchTree
类也不需要再继承Comparable
接口类。即:
BinarySearchTree.java
文件:
1 2 3
| public class BinarySearchTree<E>{ }
|
③在Person
对象类内部将默认的比较逻辑定义为:假定年龄大的人所对应的节点元素大。
Person.java
文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| package com.atangbiji;
public class Person implements Comparable<Person>{ private int age; private String name; public Person(int age, String name) { this.age = age; this.name = name; }
public int getAge() { return age; }
public void setAge(int age) { this.age = age; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
@Override public int compareTo(Person e) { return age - e.age; } }
|
④此时,我们便可以在节点元素的比较函数中根据创建二叉树搜索树时是否自定义节点元素的比较逻辑(comparator
是否为空)来分类处理。当用户自定义了节点元素的比较逻辑时,则在节点元素类外部使用Comparator
比较器实现比较逻辑;当用户未定义节点元素的比较逻辑时,则将节点元素强制转换为可比较的对象(Comparable
接口)。
BinarySearchTree.java
文件:
1 2 3 4 5 6 7 8 9 10 11 12 13
|
private int compare(E e1,E e2) { if (comparator != null) { return comparator.compare(e1, e2); } return ((Comparable<E>)e1).compareTo(e2); }
|
**注:**使用这种方法,当用户未定义节点元素的比较逻辑(即comparator == null
)时,若节点元素类(如:Person
)内部没有实现Comparable
接口,则Java
会抛出接口异常,如下图所示。
若出现此类异常,我们在节点元素类(如:Person
)内部实现Comparable
接口即可。
⑤由于Java
语言在java.util
包中自带了Comparable
与Comparator
接口类。因此,我们可以在项目中删除前面自定义的这两个接口类,并将它们分别替换为Java
自带的同名接口。即:在相应类中导入java.util.Comparator
接口类。
BinarySearchTree.java
文件:
1
| import java.util.Comparator;
|
PersonComparator1.java
文件:
1
| import java.util.Comparator;
|
PersonComparator2.java
文件:
1
| import java.util.Comparator;
|
⑥这样我们设计的二叉搜索树便可以同时满足用户自定义比较逻辑和未定义比较逻辑的情况。(当用户未定义时采用默认的比较逻辑)
Main.java
文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| package com.atangbiji;
public class Main { public static void main(String[] args) {
PersonComparator1 cmp1 = new PersonComparator1(); BinarySearchTree<Person> bst1 = new BinarySearchTree<>(cmp1); bst1.add(new Person(12, "Tom")); bst1.add(new Person(15, "Jim")); PersonComparator2 cmp2 = new PersonComparator2(); BinarySearchTree<Person> bst2 = new BinarySearchTree<>(cmp2); bst2.add(new Person(12, "Tom")); bst2.add(new Person(15, "Jim")); BinarySearchTree<Person> bst3 = new BinarySearchTree<>(); bst3.add(new Person(12, "Tom")); bst3.add(new Person(15, "Jim")); } }
|
注:当节点元素为Java
内置的很多数据类型(如:Integer
、Double
、String
等)时,我们之所以不需要自定义节点元素的比较逻辑是因为:它们在底层实现时已经帮我们实现了comparable
比较接口。
附:通过匿名类自定义比较逻辑
方法二和方法三中用户自定义的节点元素比较逻辑类是通过创建PersonComparator1
和PersonComparator2
类的方法实现的。实际应用中,为了简化代码,我们也可以通过创建匿名类的方法来实现自定义的比较逻辑。
Main.java
文件:
1 2 3 4 5 6 7 8 9 10 11
| BinarySearchTree<Person> bst4 = new BinarySearchTree<>(new Comparator<Person>() { @Override public int compare(Person arg0, Person arg1) { return arg0.getAge() - arg1.getAge(); } });
bst4.add(new Person(12, "Tom")); bst4.add(new Person(15, "Jim"));
|
c、值相等元素的处理
当新添加元素的值与树中某一节点的值相等时,我们可以选择直接返回(不处理),也可以选择让新元素覆盖旧的值。实际应用中,应根据具体需求自行处理。
由于添加的节点元素对象可能不完全相等。即:定义比较逻辑的成员变量的值相同,但其它成员变量的值不同的情况。如:依次添加两个“age
=12,name
=Tom
”、“age
=12,name
=Jack
”的Person
对象。为了保证各节点均为最新元素,我们这里选择让新元素覆盖旧的值。即:
BinarySearchTree.java
文件:
d、★在哪添加新元素?
通过分析我们可以发现:
注:
- 二叉搜索树新添加的节点一定会成为树的叶子节点,但它的父节点不一定是叶子节点。
- 当新添加元素的值与树中所有节点的值均不相等时,完成添加操作后,新添加的元素一定会成为树的叶子节点。新添加元素的左右子节点一定为空,即不存在任何子树。
- 永远不会出现将在树的中间断开,在其中插入新元素后,再分别将其与前面的父节点和后面的子树连接的情况。
BinarySearchTree.java
文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50
|
public void add(E element) { elementNotNullCheck(element); if (root == null) { root = new Node<>(element,null); size++; return; } Node<E> node = root; Node<E> parent = null; int cmp = 0; while (node != null) { cmp = compare(element, node.element); parent = node; if (cmp > 0) { node = node.right; }else if (cmp < 0) { node = node.left; }else { node.element = element; return; } } Node<E> newNode = new Node<>(element, parent); if (cmp > 0) { parent.right = newNode; } else if (cmp < 0) { parent.left = newNode; } size++; }
|
(6)获取前驱和后继节点
a、获取一个节点的前驱节点
前驱节点(predecessor):指中序遍历时的前一个节点。
注:
- 前驱节点的定义适用于所有的二叉树。
- 若二叉树是二叉搜索树,则前驱节点就是前一个比它小的节点。
**示例:**对于如下图所示的二叉搜索树,节点6
、13
、8
、7
、11
、9
、1
的前驱节点分别是:5
、12
、7
、6
、10
、8
、null
。
对于所有的二叉树,通过分析我们可以发现:
- 当
node.left != null
时,predecessor = node.left.right.right.right...
,直到找到node
左子树的最右边(right == null
)时终止,并返回该节点。
- 当
node.left == null && node.parent != null
时,predecessor = node.parent.parent.parent...
,直到找到node
是在哪个节点的右子树中(直到找到的节点在其父节点的右子树中)时终止,并返回该节点(的父节点)。
- 当
node.left == null && node.parent == null
时,该节点没有前驱节点。(如:没有左子树的根节点)
**注:**如果一个节点的度为2
,那么它的前驱节点的度只可能是1
和0
。
BinarySearchTree.java
文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| @SuppressWarnings("unused")
private Node<E> predecessor(Node<E> node) { if (node == null) return null; Node<E> p = node.left; if (p != null) { while (p.right != null) { p = p.right; } return p; } while (node.parent != null && node == node.parent.left) { node = node.parent; } return node.parent; }
|
b、获取一个节点的后继节点
后继节点(successor):指中序遍历时的后一个节点。
注:
- 后继节点的定义适用于所有的二叉树。
- 若二叉树是二叉搜索树,则后继节点就是后一个比它大的节点。
**示例:**对于如下图所示的二叉搜索树,节点1
、8
、4
、7
、6
、3
、11
的后继节点分别是2
、9
、5
、8
、7
、4
、null
。
对于所有的二叉树,通过分析我们可以发现:
- 当
node.right != null
时,successor = node.right.left.left.left...
,直到找到node
右子树的最左边(left == null
)时终止,并返回该节点。
- 当
node.right == null && node.parent != null
时,successor = node.parent.parent.parent...
,直到找到node
是在哪个节点的左子树中(直到找到的节点在其父节点的左子树中)时终止,并返回该节点(的父节点)。
- 当
node.right == null && node.parent == null
时,,该节点没有后继节点。(如:没有右子树的根节点)
**注:**如果一个节点的度为2
,那么它的后继节点的度只可能是1
和0
。
BinarySearchTree.java
文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| @SuppressWarnings("unused")
private Node<E> successor(Node<E> node) { if (node == null) return null; Node<E> p = node.right; if (p != null) { while (p.left != null) { p = p.left; } return p; } while (node.parent != null && node == node.parent.right) { node = node.parent; } return node.parent; }
|
(7)实现remove(E element)接口
a、删除叶子节点
如果待删除的节点是叶子节点,那么直接删除即可。具体地:
- 若待删除叶子节点是其父节点的左子节点(
node == node.parent.left
),则只需让该叶子节点的父节点的左子树为空(node.parent.left = null
)即可。
- 若待删除叶子节点是其父节点的右子节点(
node == node.parent.right
),则只需让该叶子节点的父节点的右子树为空(node.parent.right= null
)即可。
- 若待删除叶子节点的父节点为空(
node.parent == null
),则该节点为根节点,只需让root = null
即可。
b、删除度为1的节点
**如果待删除节点的度为1,那么要删除该节点只需:用其子节点替代该节点的位置即可。**具体地:
-
若待删除节点是其父节点的左子节点,则只需让:
node.child.parent = node.parent
;
node.parent.left = node.child
即可。
-
若待删除节点是其父节点的右子节点,则只需让:
node.child.parent = node.parent
;
node.parent.right = node.child
即可。
-
若待删除节点node
是根节点。则只需让:
root = node.child
;
node.child.parent = null
即可。
c、删除度为2的节点
为了不破坏二叉搜索树的性质,如果待删除节点的度为2,那么要删除该节点只需:
- 先用该节点的前驱(或后继)节点的值覆盖该节点的值(不是删除该节点,是值覆盖);
- 然后再删除该节点的原来的前驱(或后继)节点即可。
注:
- 如果一个节点的度为
2
,那么它的前驱和后继节点的度只可能是1
和0
。
- 删除度为
2
的待删除节点的前驱(或后继)节点,即转化为删除上述度为1
的节点或叶子节点。
- 当删除度为
2
的节点时,真正被删除的其实是该节点的前驱(或后继)节点。
- 二叉搜索树删除某一个节点后仍然是一颗二叉搜索树。
BinarySearchTree.java
文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79
|
public void remove(E element) { remove(node(element)); }
private Node<E> node(E element) { Node<E> node = root; while (node != null) { int cmp = compare(element, node.element); if (cmp == 0) return node; if (cmp > 0) { node = node.right; } else { node = node.left; } } return null; }
private void remove(Node<E> node) { if (node == null) return; size--; if (node.hasTwoChildren()) { Node<E> s = successor(node); node.element = s.element; node = s; } Node<E> replacement = node.left != null ? node.left : node.right; if (replacement != null) { replacement.parent = node.parent; if (node.parent == null) { root = replacement; } else if (node == node.parent.left) { node.parent.left = replacement; } else { node.parent.right = replacement; } } else if (node.parent == null) { root = null; } else { if (node == node.parent.left) { node.parent.left = null; } else { node.parent.right = null; } } }
|
**注:**用户处理(添加、删除、查找等)的是元素,他们是不关心二叉树内部节点结构的。
(8)实现contains(E element)接口
如:查找二叉搜索树中是否存在节点元素为4
的节点。
BinarySearchTree.java
文件:
1 2 3 4 5 6 7 8 9
|
public boolean contains(E element) { return node(element) != null; }
|
(9)打印二叉搜索树中的所有元素
为了更加直观地展示二叉(搜索)树,我们可以将二叉(搜索)树中的所有元素打印出来。
方法一:(中序)遍历打印法
在Java
中,当我们要打印某一个对象时,系统内部其实是通过调用toString()
方法来实现的。默认情况下,打印的是该对象的类名。为了打印二叉(搜索)树中的所有元素,我们只需重写(也称覆盖)toString()
方法,并在其中完成二叉(搜索)树的中序遍历和字符串的拼接工作即可。
注:Java
中字符串拼接建议使用StringBuilder
类。
BinarySearchTree.java
文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
|
@Override public String toString() { StringBuilder sb = new StringBuilder(); toString(root,sb,""); return sb.toString(); }
private void toString(Node<E> node, StringBuilder sb, String prefix) { if (node == null) return; toString(node.left, sb, prefix + "L---"); sb.append(prefix).append(node.element).append("\n"); toString(node.right, sb, prefix + "R---"); }
|
**注:**我们同样也可以采用其它的遍历方式来实现二叉(搜索)树的打印。
方法二:外部接口法(推荐)
我们这里推荐使用小码哥MJ
封装的二叉树打印接口(使用简单、显示更直观)(源码:https://github.com/CoderMJLee/BinaryTrees)。
使用它只需:
① 下载小码哥MJ封装的二叉树打印工具
②让BinaryTreeInfo
类实现BinaryTreeInfo
接口类。
BinarySearchTree.java
文件:
1 2 3
| public class BinarySearchTree<E> implements BinaryTreeInfo{
}
|
③在其内部重写相应的接口函数。
BinarySearchTree.java
文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
|
@Override public Object root() { return root; }
@Override public Object left(Object node) { return ((Node<E>)node).left; }
@Override public Object right(Object node) { return ((Node<E>)node).right; }
@Override public Object string(Object node) { return ((Node<E>)node).element; }
|
④这样,我们就可以直接调用MJ
封装的二叉树打印接口来打印二叉搜索树中的所有元素了。
main.java
文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
|
Integer data[] = new Integer[] { 7,4,2,1,3,5,9,8,11,10,12 }; BinarySearchTree<Integer> bst = new BinarySearchTree<>();
for(int i = 0; i < data.length; i++){ bst.add(data[i]); } System.out.println("使用MJ封装的接口打印二叉搜索树: \n"); BinaryTrees.println(bst);
|
**注:**当节点元素为自定义对象时,为了使打印结果更直观,我们还需要在自定义节点元素类(如:Person
类)中重写toString()
函数。
Person.java
文件:
1 2 3 4
| @Override public String toString() { return name + "(age="+ age +")"; }
|
3.2、接口测试
在Main
类中新建多个BinarySearchTree
对象,用来测试自己封装的二叉搜索树。这里仅以添加、删除、遍历节点元素等为例进行测试,其它接口的测试方法与此类似,这里不再赘述。
Main.java
文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241
| package com.atangbiji;
import java.util.Comparator;
import com.atangbiji.BinarySearchTree.Visitor; import com.atangbiji.printer.BinaryTrees;
public class Main { public static void main(String[] args) {
Integer data[] = new Integer[] { 7,4,2,1,3,5,9,8,11,10,12 }; BinarySearchTree<Integer> bst = new BinarySearchTree<>(); for(int i = 0; i < data.length; i++){ bst.add(data[i]); } System.out.println("一、节点元素为整数的二叉搜索树: \n"); System.out.println("1、采用(中序)遍历法打印二叉搜索树: \n"); System.out.println(bst); System.out.println("2、使用MJ封装的接口打印二叉搜索树: \n"); BinaryTrees.println(bst); System.out.println("二叉树高度(递归实现)为: " + bst.height() + "\n"); System.out.println("二叉树高度(层序遍历实现)为: " + bst.height2() + "\n");
PersonComparator1 cmp1 = new PersonComparator1(); BinarySearchTree<Person> bst1 = new BinarySearchTree<>(cmp1); bst1.add(new Person(12, "Tom")); bst1.add(new Person(15, "Jim")); bst1.add(new Person(10, "Lucy")); bst1.add(new Person(13, "ATang")); bst1.add(new Person(18, "Jan")); bst1.add(new Person(8, "Lily")); System.out.println("\n 二、节点元素为对象的二叉搜索树: "); System.out.println("\n 1、自定义比较逻辑1(Comparator接口法):假定年龄大的人所对应的节点元素大 \n"); BinaryTrees.println(bst1); PersonComparator2 cmp2 = new PersonComparator2(); BinarySearchTree<Person> bst2 = new BinarySearchTree<>(cmp2); bst2.add(new Person(12, "Tom")); bst2.add(new Person(15, "Jim")); bst2.add(new Person(15, "Jim")); bst2.add(new Person(10, "Lucy")); bst2.add(new Person(13, "ATang")); bst2.add(new Person(18, "Jan")); bst2.add(new Person(8, "Lily")); System.out.println("\n 2、自定义比较逻辑2(Comparator接口法):假定年龄大的人所对应的节点元素小 \n"); BinaryTrees.println(bst2); BinarySearchTree<Person> bst3 = new BinarySearchTree<>(); bst3.add(new Person(12, "Tom")); bst3.add(new Person(15, "Jim")); bst3.add(new Person(15, "Jim")); bst3.add(new Person(10, "Lucy")); bst3.add(new Person(13, "ATang")); bst3.add(new Person(18, "Jan")); bst3.add(new Person(8, "Lily")); System.out.println("\n 3、默认比较逻辑(Comparable接口法):假定年龄大的人所对应的节点元素大 \n"); BinaryTrees.println(bst3); BinarySearchTree<Person> bst4 = new BinarySearchTree<>(new Comparator<Person>() {
@Override public int compare(Person arg0, Person arg1) { return arg0.getAge() - arg1.getAge(); } }); bst4.add(new Person(12, "Tom")); bst4.add(new Person(15, "Jim")); bst4.add(new Person(15, "Jim")); bst4.add(new Person(10, "Lucy")); bst4.add(new Person(13, "ATang")); bst4.add(new Person(18, "Jan")); bst4.add(new Person(8, "Lily")); System.out.println("\n 4、通过匿名类自定义比较逻辑(Comparator接口法):假定年龄大的人所对应的节点元素大 \n"); BinaryTrees.println(bst4); System.out.println("\n" + "\n 三、遍历二叉搜索树(全部节点): "); System.out.println("\n 使用MJ封装的接口打印二叉搜索树: \n"); BinaryTrees.println(bst); System.out.println("\n" + "1、层序遍历二叉搜索树(全部节点):"); bst.levelOrder(new Visitor<Integer>() { @Override public boolean visit(Integer element) { System.out.print("_" + element + "; "); return false; } }); System.out.println("\n" + "2、前序遍历二叉搜索树(全部节点):"); bst.preorder(new Visitor<Integer>() {
@Override public boolean visit(Integer element) { System.out.print("_" + element + "; "); return false; } }); System.out.println("\n" + "3、中序遍历二叉搜索树(全部节点):"); bst.inorder(new Visitor<Integer>() {
@Override public boolean visit(Integer element) { System.out.print("_" + element + "; "); return false; } }); System.out.println("\n" + "4、后序遍历二叉搜索树(全部节点):"); bst.postorder(new Visitor<Integer>() {
@Override public boolean visit(Integer element) { System.out.print("_" + element + "; "); return false; } }); System.out.println("\n" + "\n 四、遍历二叉搜索树(部分节点): "); System.out.println("\n" + "使用MJ封装的接口打印二叉搜索树: \n"); BinaryTrees.println(bst); System.out.println("\n" + "1、层序遍历二叉搜索树(部分节点):"); bst.levelOrder(new Visitor<Integer>() { @Override public boolean visit(Integer element) { System.out.print("_" + element + "; "); return element == 8 ? true : false; } }); System.out.println("\n" + "2、前序遍历二叉搜索树(部分节点):"); bst.preorder(new Visitor<Integer>() {
@Override public boolean visit(Integer element) { System.out.print("_" + element + "; "); return element == 8 ? true : false; } }); System.out.println("\n" + "3、中序遍历二叉搜索树(部分节点):"); bst.inorder(new Visitor<Integer>() {
@Override public boolean visit(Integer element) { System.out.print("_" + element + "; "); return element == 8 ? true : false; } }); System.out.println("\n" + "4、后序遍历二叉搜索树(部分节点):"); bst.postorder(new Visitor<Integer>() {
@Override public boolean visit(Integer element) { System.out.print("_" + element + "; "); return element == 8 ? true : false; } });
System.out.println("\n" + "\n 五、删除二叉搜索树中的元素: "); System.out.println("\n" + "使用MJ封装的接口打印二叉搜索树(删除前): \n"); BinaryTrees.println(bst); System.out.println("\n" + "1、先删除元素值为5的叶子节点:"); bst.remove(5); System.out.println("\n" + "使用MJ封装的接口打印二叉搜索树(删除元素值为5的叶子节点后): \n"); BinaryTrees.println(bst); System.out.println("\n" + "2、在上述基础上,再删除元素值为4(度为1)的节点:"); bst.remove(4); System.out.println("\n" + "使用MJ封装的接口打印二叉搜索树(再删除元素值为4(度为1)的节点后): \n"); BinaryTrees.println(bst); System.out.println("\n" + "3、在上述基础上,再删除元素值为9(度为2)的节点:"); bst.remove(9); System.out.println("\n" + "使用MJ封装的接口打印二叉搜索树(再删除元素值为9(度为2)的节点后): \n"); BinaryTrees.println(bst); } }
|
运行该程序,输出结果为:
4、★ 遍历二叉搜索树
4.1、遍历方式
根据节点访问顺序的不同,二叉树常见的遍历方式有:前序遍历、中序遍历、后序遍历和层序遍历4种。
注:这里介绍的遍历方式适用于所有的二叉树。
(1)前序遍历(递归实现)
所谓前序遍历指的是:根节点放在前面遍历。其节点访问顺序为:①先访问根节点;②再前序遍历左子树;③最后前序遍历右子树。
**注:**步骤②与步骤③可交换。
BinarySearchTree.java
文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| public void preorderTraversal() { preorderTraversal(root); }
private void preorderTraversal(Node<E> node) { if (node == null) return; System.out.println(node.element); preorderTraversal(node.left); preorderTraversal(node.right); }
|
(2)中序遍历(递归实现)
所谓中序遍历指的是:根节点放在中间遍历。其节点访问顺序为:①先中序遍历左子树;②再访问根节点;③最后中序遍历右子树。
**注:**步骤①与步骤③可交换。
通过分析发现:若二叉搜索树采用上述中序遍历,则各节点按从小到大的顺序访问。
同理,若二叉搜索树按先中序遍历右子树、再访问根节点、最后中序遍历左子树的顺序遍历,则各节点按从大到小的顺序访问。
结论:二叉搜索树的中序遍历结果是**从小到大(升序)或从大到小(降序)**顺序访问的。
BinarySearchTree.java
文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| public void inorderTraversal() { inorderTraversal(root); }
private void inorderTraversal(Node<E> node) { if (node == null) return; inorderTraversal(node.left); System.out.println(node.element); inorderTraversal(node.right); }
|
(3)后续遍历(递归实现)
所谓后序遍历指的是:根节点放在后面遍历。其节点访问顺序为:①先后序遍历左子树;②再后序遍历右子树;③最后访问根节点。
**注:**步骤①与步骤②可交换。
BinarySearchTree.java
文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| public void postorderTraversal() { postorderTraversal(root); }
private void postorderTraversal(Node<E> node) { if (node == null) return; postorderTraversal(node.left); postorderTraversal(node.right); System.out.println(node.element); }
|
(4)层序遍历(队列实现)
- 访问顺序:从上往下、从左往右依次访问。
- 层序遍历不再是递归调用。
- 一般使用队列来实现。
**思路:**①将根节点入队;②循环执行如下操作,直到队列为空为止:a.将队头节点A出队,进行访问;b.将A的左子节点入队;c.将A的右子节点入队。
BinarySearchTree.java
文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
|
public void levelOrderTraversal() { if (root == null) return; Queue<Node<E>> queue = new LinkedList<>(); queue.offer(root); while (!queue.isEmpty()) { Node<E> node = queue.poll(); System.out.println(node.element); if (node.left != null) { queue.offer(node.left); } if (node.right != null) { queue.offer(node.right); } } }
|
注:上述层序遍历的代码要达应达到会默写的熟练程度。
4.2、遍历接口设计
4.2.1、编程实现
前面虽然已经实现了二叉树的前序、中序、后序和层序遍历,但它们内部只实现了打印各节点元素的功能。为了满足用户遍历时对节点元素的不同操作需求,我们可以参照“二叉搜索树节点元素的比较”的“Comparator
接口法”,在设计遍历接口时,将用户对节点元素的处理逻辑作为形参传入。这样用户便可以在调用遍历接口时在二叉搜索树类的外部自定义节点元素的处理逻辑了。具体实现过程如下:
①在BinarySearchTree
类内部自定义一个用户对节点元素的处理逻辑接口。
BinarySearchTree.java
文件:
1 2 3 4
| public static interface Visitor<E>{ void visit(E element); }
|
②新建一个二叉搜索树的层序遍历接口函数,并将其形参设计为用户自定义的对节点元素的处理逻辑接口。
BinarySearchTree.java
文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
|
public void levelOrder(Visitor<E> visitor) { if (root == null || visitor == null) return; Queue<Node<E>> queue = new LinkedList<>(); queue.offer(root); while (!queue.isEmpty()) { Node<E> node = queue.poll(); visitor.visit(node.element); if (node.left != null) { queue.offer(node.left); } if (node.right != null) { queue.offer(node.right); } } }
|
③类似地,我们可以对二叉搜索树的前序遍历、中序遍历和后序遍历接口进行设计。
BinarySearchTree.java
文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65
| public void preorder(Visitor<E> visitor) { preorder(root,visitor); }
private void preorder(Node<E> node, Visitor<E> visitor) { if (node == null || visitor == null) return; visitor.visit(node.element); preorder(node.left,visitor); preorder(node.right,visitor); }
public void inorder(Visitor<E> visitor) { inorder(root,visitor); }
private void inorder(Node<E> node, Visitor<E> visitor) { if (node == null || visitor == null) return; inorder(node.left,visitor); visitor.visit(node.element); inorder(node.right,visitor); }
public void postorder(Visitor<E> visitor) { postorder(root,visitor); }
private void postorder(Node<E> node, Visitor<E> visitor) { if (node == null || visitor == null) return; postorder(node.left,visitor); postorder(node.right,visitor); visitor.visit(node.element); }
|
4.2.2、遍历接口测试
此时,用户在调用遍历接口时,便可以在二叉搜索树类的外部自定义节点元素的处理逻辑,并将其作为形参传入接口函数了。
Main.java
文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70
| package com.atangbiji;
import com.atangbiji.BinarySearchTree.Visitor;
public class Main { public static void main(String[] args) {
Integer data[] = new Integer[] { 7,4,2,1,3,5,9,8,11,10,12 }; BinarySearchTree<Integer> bst = new BinarySearchTree<>(); for(int i = 0; i < data.length; i++){ bst.add(data[i]); } System.out.println("\n 使用MJ封装的接口打印二叉搜索树: \n"); BinaryTrees.println(bst); System.out.println("\n" + "遍历二叉搜索树(全部节点): \n"); System.out.println("\n" + "层序遍历二叉搜索树:"); bst.levelOrder(new Visitor<Integer>() { @Override public void visit(Integer element) { System.out.print("_" + element + "; "); } }); System.out.println("\n" + "前序遍历二叉搜索树:"); bst.preorder(new Visitor<Integer>() {
@Override public void visit(Integer element) { System.out.print("_" + element + "; "); } }); System.out.println("\n" + "中序遍历二叉搜索树:"); bst.inorder(new Visitor<Integer>() {
@Override public void visit(Integer element) { System.out.print("_" + element + "; "); } }); System.out.println("\n" + "后序遍历二叉搜索树:"); bst.postorder(new Visitor<Integer>() {
@Override public void visit(Integer element) { System.out.print("_" + element + "; "); } }); } }
|
运行该程序,各节点元素便按照自定义的处理逻辑:按**“_节点元素;”**的格式打印各元素。输出结果如下图所示:
4.3、增强遍历接口
4.3.1、编程实现
前面实现的遍历接口是把二叉树中所有元素都访问一遍。而实际项目中,我们可能只需要遍历二叉树的前面一部分节点。因此,为了提高程序性能,让用户可以随时停止遍历,我们需要对上述遍历接口进行功能增强。具体实现过程如下:
①在BinarySearchTree
类内部:
- 先将遍历接口修改成一个抽象类,然后再在该抽象类中添加一个成员变量
boolean isStop
用来记录是否在当前节点停止遍历。
- 然后再将用户对节点元素的处理逻辑接口修改成一个返回值是
boolean
类型的抽象函数。如果返回false
(默认),就继续遍历;如果返回true
,就停止遍历。
BinarySearchTree.java
文件:
1 2 3 4 5 6 7 8 9
| public static abstract class Visitor<E>{ boolean isStoped;
abstract boolean visit(E element); }
|
②在上述层序遍历接口的基础上,增加判断是否停止遍历功能。
BinarySearchTree.java
文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
|
public void levelOrder(Visitor<E> visitor) { if (root == null || visitor == null) return; Queue<Node<E>> queue = new LinkedList<>(); queue.offer(root); while (!queue.isEmpty()) { Node<E> node = queue.poll(); if (visitor.visit(node.element)) { return; }; if (node.left != null) { queue.offer(node.left); } if (node.right != null) { queue.offer(node.right); } } }
|
③类似地,对二叉搜索树的前序遍历、中序遍历和后序遍历接口进行修改。
- 在遍历之前判断是否继续本次遍历
- 在访问节点元素时记录下一次遍历的状态。
BinarySearchTree.java
文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69
| public void preorder(Visitor<E> visitor) { preorder(root,visitor); }
private void preorder(Node<E> node, Visitor<E> visitor) { if (node == null || visitor == null || visitor.isStoped) return; visitor.isStoped = visitor.visit(node.element); preorder(node.left,visitor); preorder(node.right,visitor); }
public void inorder(Visitor<E> visitor) { inorder(root,visitor); }
private void inorder(Node<E> node, Visitor<E> visitor) { if (node == null || visitor == null || visitor.isStoped) return; inorder(node.left,visitor); if (visitor.isStoped) return; visitor.isStoped = visitor.visit(node.element); inorder(node.right,visitor); }
public void postorder(Visitor<E> visitor) { postorder(root,visitor); }
private void postorder(Node<E> node, Visitor<E> visitor) { if (node == null || visitor == null || visitor.isStoped) return; postorder(node.left,visitor); postorder(node.right,visitor); if (visitor.isStoped) return; visitor.isStoped = visitor.visit(node.element); }
|
注:
- 在中序遍历中,如果在当前节点的左子树中遍历到了用户想要停止的节点,那么同样要停止递归;
- 在后序遍历中,如果在当前节点的左子树或右子树中遍历到了用户想要停止的节点,那么同样要停止递归。
4.3.2、增强遍历接口测试
此时,用户在调用遍历增强接口时,便可以在二叉搜索树类的外部自定义节点元素的处理逻辑,将其作为形参传入接口函数,并随时停止遍历了。
Main.java
文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78
| package com.atangbiji;
import com.atangbiji.BinarySearchTree.Visitor;
public class Main { public static void main(String[] args) {
Integer data[] = new Integer[] { 7,4,2,1,3,5,9,8,11,10,12 }; BinarySearchTree<Integer> bst = new BinarySearchTree<>(); for(int i = 0; i < data.length; i++){ bst.add(data[i]); } System.out.println("\n" + "使用MJ封装的接口打印二叉搜索树: \n"); BinaryTrees.println(bst); System.out.println("\n" + "遍历二叉搜索树(部分节点): \n"); System.out.println("\n" + "层序遍历二叉搜索树(部分节点):"); bst.levelOrder(new Visitor<Integer>() { @Override public boolean visit(Integer element) { System.out.print("_" + element + "; "); return element == 8 ? true : false; } }); System.out.println("\n" + "前序遍历二叉搜索树(部分节点):"); bst.preorder(new Visitor<Integer>() {
@Override public boolean visit(Integer element) { System.out.print("_" + element + "; "); return element == 8 ? true : false; } }); System.out.println("\n" + "中序遍历二叉搜索树(部分节点):"); bst.inorder(new Visitor<Integer>() {
@Override public boolean visit(Integer element) { System.out.print("_" + element + "; "); return element == 8 ? true : false; } }); System.out.println("\n" + "后序遍历二叉搜索树(部分节点):"); bst.postorder(new Visitor<Integer>() {
@Override public boolean visit(Integer element) { System.out.print("_" + element + "; "); return element == 8 ? true : false; } }); } }
|
运行该程序,各节点元素便按照自定义的处理逻辑:按**“_节点元素;”**的格式打印各元素。输出结果如下图所示:
附1:遍历的常见应用(补充接口)
Ⅰ、计算二叉树的高度
方法一:递归实现
思路:
- 当前节点的高度 = 当前节点左右子节点高度的较大者 + 1;
- 二叉树的高度 = 跟节点的高度。
BinarySearchTree.java
文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
|
public int height() { return height(root); }
private int height(Node<E> node) { if (node == null) return 0; return 1 + Math.max(height(node.left), height(node.right)); }
|
方法二:层序遍历实现
思路:
- 二叉树的高度 = 二叉树的层数 = 层序遍历时访问的层数;
- 当每一层节点全部出栈时,栈内元素刚好全部是下一层的所有节点。
BinarySearchTree.java
文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
|
public int height2() { if (root == null) return 0; int height = 0; int levelSize = 1; Queue<Node<E>> queue = new LinkedList<>(); queue.offer(root); while (!queue.isEmpty()) { Node<E> node = queue.poll(); levelSize--; if (node.left != null) { queue.offer(node.left); } if (node.right != null) { queue.offer(node.right); } if (levelSize == 0) { levelSize = queue.size(); height++; } } return height; }
|
Ⅱ、判断一棵树是否为完全二叉树
方法一:层序遍历实现方式1
**思路:**根据完全二叉树的定义可知:
- 如果树为空,返回
false
;
- 如果树不为空,开始层序遍历二叉树(用队列)
- 如果
node.left!=null && node.right!=null
,将node.left
、node.right
按顺序入队;
- 如果
node.left==null && node.right!=null
,返回false
;
- 如果
node.left!=null && node.right==null
或node.left==null && node.right==null
,那么后面遍历的节点应该都为叶子节点,才是完全二叉树 ;否则返回false
。
BinarySearchTree.java
文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
|
public boolean isComplete() { if (root == null) return false; Queue<Node<E>> queue = new LinkedList<>(); queue.offer(root); boolean leaf = false; while (!queue.isEmpty()) { Node<E> node = queue.poll(); if (leaf && !node.isLeaf()) return false; if (node.hasTwoChildren()) { queue.offer(node.left); queue.offer(node.right); } else if (node.left == null && node.right != null) { return false; } else { leaf = true; if (node.left != null) { queue.offer(node.left); } } } return true; }
|
**注:**为了使用方便,我们在内部静态节点类中封装了两个接口函数:一个用于判断该节点是否为叶子节点;一个用于判断该节点是否拥有两个子节点。
BinarySearchTree.java
文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| private static class Node<E> { E element; Node<E> left; Node<E> right; Node<E> parent; public Node(E element, Node<E> parent) { this.element = element; this.parent = parent; } public boolean isLeaf() { return left == null && right == null; } public boolean hasTwoChildren() { return left != null && right != null; } }
|
方法二:层序遍历实现方式2(推荐)
思路:
- 如果树为空,返回
false
;
- 如果树不为空,开始层序遍历二叉树(用队列)
- 如果
node.left!=null
,那么将node.left
入队;
- 如果
node.left==null && node.right!=null
,返回false
;
- 如果
node.right!=null
,那么将node.right
入队;
- 如果
node.right==null
,那么后面遍历的节点应该都为叶子节点,才是完全二叉树;否则返回false
。
BinarySearchTree.java
文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
|
public boolean isComplete2() { if (root == null) return false; Queue<Node<E>> queue = new LinkedList<>(); queue.offer(root); boolean leaf = false; while (!queue.isEmpty()) { Node<E> node = queue.poll(); if (leaf && !node.isLeaf()) return false; if (node.left != null) { queue.offer(node.left); } else if (node.right != null) { return false; } if (node.right != null) { queue.offer(node.right); } else { leaf = true; } } return true; }
|
Ⅲ、翻转二叉树
226. 翻转二叉树 - 力扣(LeetCode)
**思路:**遍历所有节点,把所有节点的左右子节点都交换即可。
方法一:使用前序遍历
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| public TreeNode invertTree(TreeNode root) { if (root == null) return root; TreeNode temp = root.left; root.left = root.right; root.right = temp; invertTree(root.left); invertTree(root.right);
return root; }
|
方法二:使用后序遍历
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| public TreeNode invertTree(TreeNode root) { if (root == null) return root; invertTree(root.left); invertTree(root.right); TreeNode temp = root.left; root.left = root.right; root.right = temp; return root; }
|
方法三:使用中序遍历
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| public TreeNode invertTree(TreeNode root) { if (root == null) return root; invertTree(root.left); TreeNode temp = root.left; root.left = root.right; root.right = temp; invertTree(root.left);
return root; }
|
方法四:使用层序遍历
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| public TreeNode invertTree(TreeNode root) { if (root == null) return root; Queue<TreeNode> queue = new LinkedList<>(); queue.offer(root); while (!queue.isEmpty()) { TreeNode node = queue.poll(); TreeNode temp = node.left; node.left = node.right; node.right = temp;
if (node.left != null) { queue.offer(node.left); }
if (node.right != null) { queue.offer(node.right); } }
return root; }
|
附2:根据遍历结果重构二叉树
结论:
-
如果已知**“前序遍历 + 中序遍历”的遍历结果,那么可以保证重构出唯一**的一棵二叉树。
-
如果已知**"后序遍历 + 中序遍历"的遍历结果,那么可以保证重构出唯一**的一棵二叉树。
-
如果已知**“前序遍历 + 后序遍历”**的遍历结果,那么:
- 若它是一颗棵真二叉树(即:任意节点的度只能为
0
或2
),则可以保证重构出唯一的一棵二叉树;
- 否则重构结果不唯一。(即:若存在度为
1
的节点,则重构结果不唯一)
**示例:**已知某二叉树的前序遍历结果为:4 2 1 3 6 5
;中序遍历结果:1 2 3 4 5 6
。那么该二叉树的结构一定为:
附:推荐一些神奇的网站
附:小码哥MJ封装的二叉树打印接口
点击下载附件
(本讲完,系列博文持续更新中…… )
参考文献:
[1] 《恋上数据结构与算法》,小码哥MJ
[2] 《数据结构与算法》,邓俊辉
如何获取源代码?
关注 “阿汤笔迹” 微信公众号,在后台回复关键词 “数据结构与算法设计与实现” ,即可获取项目源代码。
原文地址:http://www.atangbiji.com/2023/07/31/BinarySearchTree/
博主最新文章在个人博客 http://www.atangbiji.com/ 发布。