树
二叉树
基本操作
初始化二叉树
插入与删除节点
遍历
层序遍历
前序、中序、后序遍历
数组表示
完美二叉树
任意二叉树
优缺点
二叉搜索树
基本操作
查找节点
插入节点
删除节点
中序遍历有序
/* 二叉树节点类 */
class TreeNode {
int val; // 节点值
TreeNode left; // 左子节点引用
TreeNode right; // 右子节点引用
TreeNode(int x) { val = x; }
}
在二叉树中,除叶节点外,其他所有节点都包含子节点和非空子树 。
// 初始化节点
TreeNode a = new TreeNode(10);
TreeNode b = new TreeNode(20);
TreeNode c = new TreeNode(30);
TreeNode d = new TreeNode(40);
TreeNode e = new TreeNode(50);
// 构建节点之间的引用(指针)
a.left = b; // a 的左子节点是 b
a.right = c; // a 的右子节点是 c
b.left = d; // b 的左子节点是 d
b.right = e; // b 的右子节点是 e
// 初始化节点
TreeNode x = new TreeNode(1);
TreeNode y = new TreeNode(2);
TreeNode z = new TreeNode(3);
TreeNode w = new TreeNode(4);
// 在 x -> y 中间插入节点 w
x.left = w; // 将 w 作为 x 的左子节点
w.left = y; // 将 y 作为 w 的左子节点
// 删除节点 w
x.left = y; // 直接将 x 的左子节点指向 y,删除了 w
/* 层序遍历 */
List levelOrderTraversal(TreeNode root) {
// 初始化队列,加入根节点
Queue queue = new LinkedList<>();
if (root != null) {
queue.add(root);
}
// 初始化一个列表,用于保存遍历序列
List result = new ArrayList<>();
while (!queue.isEmpty()) {
TreeNode currentNode = queue.poll(); // 队列出队
result.add(currentNode.val); // 保存节点值
// 如果有左子节点,左子节点入队
if (currentNode.left != null) {
queue.offer(currentNode.left);
}
// 如果有右子节点,右子节点入队
if (currentNode.right != null) {
queue.offer(currentNode.right);
}
}
return result; // 返回层序遍历的结果
}
在这个示例中,创建了一个方法 levelOrderTraversal
来进行二叉树的层序遍历。
while
循环,当队列不为空时,出队当前节点,将其值添加到结果列表中。这种实现方式可以有效地遍历整棵树,并返回层序遍历的节点值列表。
import java.util.ArrayList;
import java.util.List;
public class BinaryTreeTraversal {
List list = new ArrayList<>(); // 用于保存遍历结果
/* 前序遍历 */
void preOrder(TreeNode root) {
if (root == null)
return;
// 访问优先级:根节点 -> 左子树 -> 右子树
list.add(root.val); // 访问根节点
preOrder(root.left); // 递归访问左子树
preOrder(root.right); // 递归访问右子树
}
/* 中序遍历 */
void inOrder(TreeNode root) {
if (root == null)
return;
// 访问优先级:左子树 -> 根节点 -> 右子树
inOrder(root.left); // 递归访问左子树
list.add(root.val); // 访问根节点
inOrder(root.right); // 递归访问右子树
}
/* 后序遍历 */
void postOrder(TreeNode root) {
if (root == null)
return;
// 访问优先级:左子树 -> 右子树 -> 根节点
postOrder(root.left); // 递归访问左子树
postOrder(root.right); // 递归访问右子树
list.add(root.val); // 访问根节点
}
// 获取遍历结果
List getTraversalResult() {
return list;
}
}
在这个示例中,我们定义了一个 BinaryTreeTraversal
类,其中包含前序遍历、中序遍历和后序遍历的方法。每个遍历方法的实现都遵循相应的访问顺序:
使用 list
来保存遍历结果,你可以调用相应的遍历方法后,通过 getTraversalResult()
方法获取最终的遍历结果。
将所有节点按照层序遍历的顺序存储在一个数组中,则每个节点都对应唯一的数组索引。
若某节点的索引为 , 则该节点的左子节点索引为 2 + 1 ,右子节点索引为 2 + 2 。
import java.util.ArrayList;
import java.util.List;
/* 数组表示下的二叉树类 */
class ArrayBinaryTree {
private List tree; // 存储树节点的数组
/* 构造方法 */
public ArrayBinaryTree(List arr) {
tree = new ArrayList<>(arr);
}
/* 返回树的大小 */
public int size() {
return tree.size();
}
/* 获取索引为 i 节点的值 */
public Integer val(int i) {
// 若索引越界,则返回 null,代表空位
if (i < 0 || i >= size()) {
return null;
}
return tree.get(i);
}
/* 获取索引为 i 节点的左子节点的索引 */
public Integer left(int i) {
return 2 * i + 1;
}
/* 获取索引为 i 节点的右子节点的索引 */
public Integer right(int i) {
return 2 * i + 2;
}
/* 获取索引为 i 节点的父节点的索引 */
public Integer parent(int i) {
return (i - 1) / 2;
}
/* 层序遍历 */
public List levelOrder() {
List res = new ArrayList<>();
// 直接遍历数组
for (int i = 0; i < size(); i++) {
if (val(i) != null) {
res.add(val(i));
}
}
return res;
}
/* 深度优先遍历 */
private void dfs(Integer i, String order, List res) {
// 若为空位,则返回
if (val(i) == null) {
return;
}
// 前序遍历
if ("pre".equals(order)) {
res.add(val(i));
}
dfs(left(i), order, res); // 递归左子树
// 中序遍历
if ("in".equals(order)) {
res.add(val(i));
}
dfs(right(i), order, res); // 递归右子树
// 后序遍历
if ("post".equals(order)) {
res.add(val(i));
}
}
/* 前序遍历 */
public List preOrder() {
List res = new ArrayList<>();
dfs(0, "pre", res); // 从根节点开始遍历
return res;
}
/* 中序遍历 */
public List inOrder() {
List res = new ArrayList<>();
dfs(0, "in", res); // 从根节点开始遍历
return res;
}
/* 后序遍历 */
public List postOrder() {
List res = new ArrayList<>();
dfs(0, "post", res); // 从根节点开始遍历
return res;
}
}
在这个实现中:
ArrayList
来存储树的节点。dfs
来完成。这个类可以用于表示和操作基于数组的二叉树。可以向构造函数传递一个整数列表作为树的节点,从而构建出相应的二叉树结构。
优点:
简洁性:
节省空间:
访问速度快:
容易实现层序遍历:
缺点:
空间浪费:
动态性差:
不灵活:
对树的性质有限制:
综上所述,数组表示法在某些情况下非常有效,尤其是对于完全二叉树,但在处理其他类型的二叉树时可能并不是最佳选择。选择使用何种表示方法通常取决于具体应用场景和二叉树的结构特点。
/* 查找节点 */
TreeNode search(int num) {
TreeNode currentNode = root; // 从根节点开始查找
// 循环查找,直到找到目标节点或达到叶节点
while (currentNode != null) {
// 目标节点在当前节点的右子树中
if (currentNode.val < num) {
currentNode = currentNode.right; // 移动到右子节点
}
// 目标节点在当前节点的左子树中
else if (currentNode.val > num) {
currentNode = currentNode.left; // 移动到左子节点
}
// 找到目标节点,跳出循环
else {
return currentNode; // 返回找到的节点
}
}
// 若未找到目标节点,返回 null
return null;
}
在这个示例中,search
方法在二叉搜索树中查找值为 num
的节点:
while
循环来遍历树,直到找到目标节点或到达叶节点(currentNode
为 null
)。
num
,则移动到右子树。num
,则移动到左子树。null
,表示目标值不存在于树中。这种方法的时间复杂度是 O(h),其中 h 是树的高度。对于平衡树,平均复杂度较低,但在极端情况下(如退化为链表),可能达到 O(n)。
/* 插入节点 */
void insert(int num) {
// 若树为空,则初始化根节点
if (root == null) {
root = new TreeNode(num);
return;
}
TreeNode currentNode = root; // 当前节点
TreeNode parentNode = null; // 记录父节点
// 循环查找,直到找到插入位置
while (currentNode != null) {
// 找到重复节点,直接返回
if (currentNode.val == num) {
return; // 不插入重复值
}
parentNode = currentNode; // 更新父节点
// 插入位置在 currentNode 的右子树中
if (currentNode.val < num) {
currentNode = currentNode.right;
}
// 插入位置在 currentNode 的左子树中
else {
currentNode = currentNode.left;
}
}
// 创建新节点
TreeNode newNode = new TreeNode(num);
// 将新节点插入父节点的正确位置
if (parentNode.val < num) {
parentNode.right = newNode; // 插入到右子节点
} else {
parentNode.left = newNode; // 插入到左子节点
}
}
在这个示例中,insert
方法的工作流程为:
while
循环遍历树,直到找到合适的插入位置或找到重复的节点。currentNode
和其父节点 parentNode
,更新 parentNode
在每一步中。newNode
。这种插入操作在二叉搜索树的平均情况下时间复杂度为 O(h),h 是树的高度。对于平衡的树结构,平均时间复杂度相对较低,但在极端情况下(例如树形不平衡时),可能达到 O(n)。
/* 删除节点 */
void remove(int num) {
// 若树为空,直接提前返回
if (root == null) {
return;
}
TreeNode currentNode = root;
TreeNode parentNode = null;
// 循环查找待删除节点
while (currentNode != null) {
// 找到待删除节点,跳出循环
if (currentNode.val == num) {
break;
}
parentNode = currentNode;
// 待删除节点在 currentNode 的右子树中
if (currentNode.val < num) {
currentNode = currentNode.right;
}
// 待删除节点在 currentNode 的左子树中
else {
currentNode = currentNode.left;
}
}
// 若无待删除节点,则直接返回
if (currentNode == null) {
return;
}
// 子节点数量 = 0 or 1
if (currentNode.left == null || currentNode.right == null) {
// 当子节点数量 = 0 / 1 时, child = null / 该子节点
TreeNode child = (currentNode.left != null) ? currentNode.left : currentNode.right;
// 删除节点 currentNode
if (currentNode != root) {
if (parentNode.left == currentNode) {
parentNode.left = child; // 将父节点的左指针指向 child
} else {
parentNode.right = child; // 将父节点的右指针指向 child
}
} else {
// 若删除节点为根节点,则重新指定根节点
root = child;
}
}
// 子节点数量 = 2
else {
// 获取中序遍历中 currentNode 的下一个节点(右子树最左边的节点)
TreeNode tmp = currentNode.right;
while (tmp.left != null) {
tmp = tmp.left;
}
// 用 tmp 的值替换 currentNode 的值
currentNode.val = tmp.val;
// 递归删除 tmp 节点
remove(tmp.val);
}
}
代码说明:
查找节点:
while
循环遍历树,从根节点开始,查找待删除的节点 currentNode
,同时记录其父节点 parentNode
。处理节点未找到:
currentNode
为 null
),则直接返回。处理子节点数量为 0 或 1 的情况:
currentNode
的左子树或右子树为空,将其子节点(如果存在)直接连接到父节点的相应位置。处理子节点数量为 2 的情况:
currentNode
的中序后继(右子树中最左边的节点)。currentNode
,然后递归调用 remove
,删除中序后继节点。注意事项:
二叉 搜索树的中序遍历序列是升序的 。
文章记录了学习Krahets的《Hello 算法》的轨迹,代码均使用Java语言,原书支持 Python、C++、Java、C#、Go、Swift、JavaScript、TypeScript、Dart、 Rust、C 和 Zig 等语言。
教程链接:krahets/hello-algo: 《Hello 算法》:动画图解、一键运行的数据结构与算法教程。支持 Python, Java, C++, C, C#, JS, Go, Swift, Rust, Ruby, Kotlin, TS, Dart 代码。简体版和繁体版同步更新,English version ongoing (github.com)https://github.com/krahets/hello-algo