「这是我参与2022首次更文挑战的第4天,活动详情查看:2022首次更文挑战」
树(Tree)是一种很有趣的数据结构,它既能像链表那样快速的插入和删除,又能像有序数组那样快速查找。树的种类很多,本节将记录一种特殊的树————二叉树(Binary Tree)。二叉树的每个节点最多只能有两个子节点,通常称为左子节点和右子节点。如果一个二叉树的每个节点的左子节点的关键字值小于该节点,右子节点的关键字值大于等于该节点,那么这种二叉树也称为二叉搜索树(Binary Search Tree,BST),本节主要关注BST。
相关术语
查看一个BST例子:
- 路径:从一个节点走到另一个节点,经过的节点顺序就称为路径;
- 根:树的顶端节点称为根,一个数只能有一个根节点,并且从根节点到任意子节点只能有一条路径;
- 父节点:每个节点(除了根)都有一条边向上连接到另一个节点,这个节点就是下面节点的父节点;
- 子节点:每个节点(除了叶子节点)都有一条或两条边向下连接其他节点,下面这些节点就是当前节点的子节点。子节点分为左子节点和右子节点;
- 叶节点:没有子节点的节点称为叶子节点,或叶节点;
- 关键字:节点中的数据,比如上图中的数值。
操作BST
在操作BST前,我们先用代码定义一个BST的骨架:
/** BST */
public class BinarySearchTree {
/** 根节点 */
private Node root;
/** 节点 */
static class Node {
/** 关键字 */
int key;
/** 额外携带的数据 */
String value;
/** 左子节点 */
Node leftChild;
/** 右子节点 */
Node rightChild;
public Node(int key, String value) {
this.key = key;
this.value = value;
}
}
}
下面的这些操作都以这个BST为例:
插入
假如我们需要插入一个key为88的节点,需要经过如下步骤:
- 从根节点出发,88比72大,所以走右子节点82路径;
- 88比82大,所以走右子节点90路径;
- 88比90小,所以走左子节点87路径;
- 88比87大,并且87的右子节点为空,所以我们最终把88作为87的右子节点插入树中。
当key重复时,可以选择覆盖或者忽略,这由业务决定。
上述过程动态图如下所示:
Java代码实现如下:
/** BST */
public class BinarySearchTree {
/** 根节点 */
private Node root;
/** 节点 */
static class Node {
/** 关键字 */
int key;
/** 额外携带的数据 */
String value;
/** 左子节点 */
Node leftChild;
/** 右子节点 */
Node rightChild;
public Node(int key, String value) {
this.key = key;
this.value = value;
}
}
/** 插入 */
public void insert(int key, String value) {
// 创建一个新节点
Node newNode = new Node(key, value);
if (this.root == null) {
// 如果根为null,则这个新节点就是根
root = newNode;
} else {
// 如果跟不为null,则从根开始搜索插入位置
Node currentNode = root;
// 用于暂存父节点
Node parentNode;
while (true) {
// 父节点设置为当前节点
parentNode = currentNode;
if (key < currentNode.key) {
currentNode = currentNode.leftChild;
if (currentNode == null) {
// 如果key小于当前节点key,并且当前节点的左子节点为空,则将新节点
// 设置为当前节点(父节点暂存对象)的左子节点,退出循环
parentNode.leftChild = newNode;
return;
}
} else if (key > currentNode.key) {
currentNode = currentNode.rightChild;
if (currentNode == null) {
// 如果key大于当前节点key,并且当前节点的右子节点为空,则将新节点
// 设置为当前节点(父节点暂存对象)的又子节点,退出循环
parentNode.rightChild = newNode;
return;
}
} else {
// 如果key等于当前节点key,则将value覆盖当前节点value
currentNode.value = newNode.value;
return;
}
}
}
}
}
编写测试程序:
public class BinarySearchTreeTest {
public static void main(String[] args) {
BinarySearchTree bst = new BinarySearchTree();
Arrays.asList(72, 57, 82, 30, 63, 79, 90, 27, 40, 62, 67, 80, 87, 48).forEach(key -> {
String value = "我是key为" + key + "的value";
bst.insert(key, value);
});
}
}
以debug的方式运行程序,查看bst结构:
bst结构和上图一致,有兴趣可以自己验证。
查找
假如我们需要查找key为67的节点,需要经过如下步骤:
- 从根节点出发,67比72小,所以走左子节点57路径;
- 67比57大,所以走右子节点63路径;
- 67比63大,所以走右子节点67路径;
- 67等于67,找到目标节点,退出;
- 如果搜索直到叶子节点都没找到,则返回空。
上述过程动态图如下所示:
Java代码实现如下:
/** BST */
public class BinarySearchTree {
/** 根节点 */
private Node root;
/** 节点 */
static class Node {
/** 关键字 */
int key;
/** 额外携带的数据 */
String value;
/** 左子节点 */
Node leftChild;
/** 右子节点 */
Node rightChild;
public Node(int key, String value) {
this.key = key;
this.value = value;
}
}
/** 查找 */
public Node find(int key) {
// 从根节点开始查找
Node currentNode = root;
// 当前节点的key不等于被查找的key时
while (currentNode.key != key) {
if (key < currentNode.key) {
// 如果key值小于当前节点key,则查找左子节点
currentNode = currentNode.leftChild;
} else {
// 如果key值大于等于当前节点key,则查找右子节点
currentNode = currentNode.rightChild;
}
// 如果当前节点为null,说明查到叶子节点了,仍没查到目标key,则直接返回null
if (currentNode == null) {
return null;
}
}
// 返回当前节点(退出while循环要么key相等,要么没找到,结果为null)
return currentNode;
}
}
编写测试程序:
public class BinarySearchTreeTest {
public static void main(String[] args) {
BinarySearchTree bst = new BinarySearchTree();
Arrays.asList(72, 57, 82, 30, 63, 79, 90, 27, 40, 62, 67, 80, 87, 48).forEach(key -> {
String value = "我是key为" + key + "的value";
bst.insert(key, value);
});
System.out.println(bst.find(87).value);
bst.insert(87, "hello world");
System.out.println(bst.find(87).value);
}
}
输出如下: 我是key为87的value hello world