一文搞定二叉树
树
二叉树
基本操作
初始化二叉树
插入与删除节点
遍历
层序遍历
前序、中序、后序遍历
数组表示
完美二叉树
任意二叉树
优缺点
二叉搜索树
基本操作
查找节点
插入节点
删除节点
中序遍历有序
二叉树
/* 二叉树节点类 */
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<Integer> levelOrderTraversal(TreeNode root) {
// 初始化队列,加入根节点
Queue<TreeNode> queue = new LinkedList<>();
if (root != null) {
queue.add(root);
}
// 初始化一个列表,用于保存遍历序列
List<Integer> 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<Integer> 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<Integer> getTraversalResult() {
return list;
}
}
在这个示例中,我们定义了一个 BinaryTreeTraversal
类,其中包含前序遍历、中序遍历和后序遍历的方法。每个遍历方法的实现都遵循相应的访问顺序:
- 前序遍历:先访问根节点,再访问左子树,最后访问右子树。
- 中序遍历:先访问左子树,再访问根节点,最后访问右子树。
- 后序遍历:先访问左子树,再访问右子树,最后访问根节点。
使用 list
来保存遍历结果,你可以调用相应的遍历方法后,通过 getTraversalResult()
方法获取最终的遍历结果。
数组表示
完美二叉树
将所有节点按照层序遍历的顺序存储在一个数组中,则每个节点都对应唯一的数组索引。
若某节点的索引为 𝑖 , 则该节点的左子节点索引为 2𝑖 + 1 ,右子节点索引为 2𝑖 + 2 。
任意二叉树
import java.util.ArrayList;
import java.util.List;
/* 数组表示下的二叉树类 */
class ArrayBinaryTree {
private List<Integer> tree; // 存储树节点的数组
/* 构造方法 */
public ArrayBinaryTree(List<Integer> 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<Integer> levelOrder() {
List<Integer> 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<Integer> 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<Integer> preOrder() {
List<Integer> res = new ArrayList<>();
dfs(0, "pre", res); // 从根节点开始遍历
return res;
}
/* 中序遍历 */
public List<Integer> inOrder() {
List<Integer> res = new ArrayList<>();
dfs(0, "in", res); // 从根节点开始遍历
return res;
}
/* 后序遍历 */
public List<Integer> postOrder() {
List<Integer> res = new ArrayList<>();
dfs(0, "post", res); // 从根节点开始遍历
return res;
}
}
在这个实现中:
- 构造方法:初始化一个
ArrayList
来存储树的节点。 - 大小、值获取、索引计算:提供了一系列方法以获取树的节点数量、指定索引的节点的值,以及计算父节点和子节点的索引。
- 遍历方法:实现了层序遍历、前序遍历、中序遍历和后序遍历。每种遍历都通过递归深度优先遍历方法
dfs
来完成。
这个类可以用于表示和操作基于数组的二叉树。可以向构造函数传递一个整数列表作为树的节点,从而构建出相应的二叉树结构。
优缺点
优点:
-
简洁性:
- 数组表示法可以直接使用下标来访问父节点和子节点,代码实现简单、直观。
-
节省空间:
- 在完全二叉树(或满二叉树)中,使用数组表示法可以有效利用空间,避免了指针的额外开销。
-
访问速度快:
- 数组在内存中是连续存储的,访问元素时使用下标可以在 O(1) 时间内完成,效率高。
-
容易实现层序遍历:
- 由于节点的排列是有序的,层序遍历可以直接通过数组的索引来实现,而不需要额外的队列或栈结构。
缺点:
-
空间浪费:
- 在节点较稀疏的情况下(例如不完全二叉树),数组会留有大量空位,导致空间浪费。
-
动态性差:
- 当二叉树结构发生变化(如插入或删除节点)时,数组的大小是固定的,可能需要重新分配内存和复制整个数组,效率低。
-
不灵活:
- 对于非完全二叉树,使用数组表示法处理节点的插入和删除变得复杂,不如链式表示法灵活。
-
对树的性质有限制:
- 数组表示法通常适用于完全二叉树或满二叉树,对于普通的二叉树,使用链式表示法更加高效和适用。
综上所述,数组表示法在某些情况下非常有效,尤其是对于完全二叉树,但在处理其他类型的二叉树时可能并不是最佳选择。选择使用何种表示方法通常取决于具体应用场景和二叉树的结构特点。
二叉搜索树
基本操作
查找节点
/* 查找节点 */
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
,删除中序后继节点。
- 找到
注意事项:
- 此实现假设输入的数值是唯一的,并且树是二叉搜索树。确保在删除操作后,树的性质仍然保持有效。
- 此方法的时间复杂度为 O(h),其中 h 是树的高度。对于平衡树,平均情况下较低,但在极端不平衡的情况下,复杂度可能达到 O(n)。
中序遍历有序
二叉 搜索树的中序遍历序列是升序的 。