二叉树是计算机科学中一种重要的数据结构,用于模拟具有树状结构的数据集合。每个节点最多有两个子节点,通常称之为“左子节点”和“右子节点”。二叉树在算法设计、数据库、搜索引擎等领域有着广泛的应用。
二叉树的基本概念
- 节点(node):树的基本单元,包含数据项和指向其子节点的指针。
- 根节点(root node):树顶部的节点。
- 父节点和子节点:在树中,如果节点A的直接下级是节点B,则节点A是节点B的父节点,节点B是节点A的子节点。
- 叶节点(leaf):没有子节点的节点。
- 子树(subtree):每个节点和它的后代构成的树称为原树的子树。
- 边(edge):连接两个节点的线段,即节点引用(指针)。
- 节点的度(degree):节点的子节点的数量。在二叉树中,度的取值范围是 0、1、2 。
- 节点的深度(depth):从根节点到该节点所经过的边的数量。
- 节点的高度(height):从节点到叶子节点的最长路径的边数。
- 树的高度:根节点的高度。
二叉树的类型
-
完全二叉树:除了最后一层外,每层都是满的,并且最后一层的节点都尽可能地集中在左侧。
-
满二叉树:除了叶子节点外,每个节点都有两个子节点。
-
平衡二叉树(AVL树):任何节点的两个子树的高度差不超过1。
插入与删除节点
与链表类似,在二叉树中插入与删除节点可以通过修改指针来实现
二叉树的遍历
遍历是指按照某种路径规则,访问树中每个节点一次且仅一次的过程。二叉树的遍历分为四种:前序、中序、后序和层序遍历。
- 前序遍历:先访问根节点,然后递归地前序遍历左子树,最后递归地前序遍历右子树。
- 中序遍历:先递归地中序遍历左子树,然后访问根节点,最后递归地中序遍历右子树。
- 后序遍历:先递归地后序遍历左子树,然后递归地后序遍历右子树,最后访问根节点。
- 层序遍历:按照树的层次从上到下访问树中的节点。
示例:二叉树的实现与遍历
以下是一个简单的Java实现,展示了如何定义一个二叉树节点以及如何进行前序遍历、中序遍历和后序遍历。
public class BinaryTree<E> {
// 前序遍历
public void preOrderTraversal(TreeNode<E> node) {
if (node == null) return;
System.out.print(node.data + " ");
preOrderTraversal(node.left);
preOrderTraversal(node.right);
}
// 中序遍历
public void inOrderTraversal(TreeNode<E> node) {
if (node == null) return;
inOrderTraversal(node.left);
System.out.print(node.data + " ");
inOrderTraversal(node.right);
}
// 后序遍历
public void postOrderTraversal(TreeNode<E> node) {
if (node == null) return;
postOrderTraversal(node.left);
postOrderTraversal(node.right);
System.out.print(node.data + " ");
}
private static class TreeNode<E> {
E data;
TreeNode<E> left, right;
public TreeNode(E data) {
this.data = data;
}
}
public static void main(String[] args) {
// 构建二叉树
// 1
// / \
// 2 3
// / \ / \
// 4 5 6 7
TreeNode<Integer> root = new TreeNode<>(1);
root.left = new TreeNode<>(2);
root.right = new TreeNode<>(3);
root.left.left = new TreeNode<Integer>(4);
root.left.right = new TreeNode<Integer>(5);
root.right.left = new TreeNode<>(6);
root.right.right = new TreeNode<>(7);
BinaryTree binaryTree = new BinaryTree();
// 前序遍历输出
System.out.println("Preorder traversal: ");
binaryTree.preOrderTraversal(root);
System.out.println();
// 中序遍历输出
System.out.println("Inorder traversal: ");
binaryTree.inOrderTraversal(root);
System.out.println();
// 后续遍历输出
System.out.println("Postorder traversal: ");
binaryTree.postOrderTraversal(root);
System.out.println();
}
}
在上面的代码中,我们首先定义了一个TreeNode
类,代表二叉树的节点。每个节点包含一个整数值data
和两个指向其左右子节点的引用left
和right
。
然后,我们创建了一个BinaryTree
类,它包含三种遍历方法:前序遍历(preOrderTraversal
)、中序遍历(inOrderTraversal
)和后序遍历(postOrderTraversal
)。每种遍历方法都接受一个TreeNode
类型的参数,这个参数是当前正在访问的节点。
最后,在main
方法中,我们创建了一个简单的二叉树实例,并分别对其执行前序、中序和后序遍历,并打印出每个节点的值。
输出结果将会是:
Preorder traversal:
1 2 4 5 3 6 7
Inorder traversal:
4 2 5 1 6 3 7
Postorder traversal:
4 5 2 6 7 3 1
层序遍历
层序遍历通常使用队列来实现。在Java中,我们可以使用LinkedList
作为队列。以下是层序遍历的示例代码:
import java.util.LinkedList;
import java.util.Queue;
public class BinaryTree {
// ... 其他遍历方法 ...
// 层序遍历
public void levelOrderTraversal(TreeNode<E> root) {
if (root == null) {
return;
}
Queue<TreeNode<E>> queue = new LinkedList<>();
// LinkedList.offer() 方法用于在队列末尾添加元素,它总是返回 true,表示添加操作总是成功的。
queue.offer(root);
System.out.println();
while (!queue.isEmpty()) {
// LinkedList.poll() 方法用于移除并返回队列的头部元素。如果队列为空,它返回 null 而不是抛出异常。
TreeNode<E> node = queue.poll();
System.out.print(node.data + " ");
if (node.left != null) {
queue.offer(node.left);
}
if (node.right != null) {
queue.offer(node.right);
}
}
}
public static void main(String[] args) {
// ... 构建二叉树和其他遍历 ...
// 层序遍历输出
System.out.println("Level order traversal: ");
binaryTree.levelOrderTraversal(root);
System.out.println();
}
}
层序遍历的输出结果将会是:
Level order traversal:
1 2 3 4 5 6 7
在二叉树的层序遍历中,我们首先将根节点放入队列。然后,只要队列不为空,我们就从队列中取出一个节点,打印其值,接着如果存在左子节点或右子节点,将它们加入队列。如此循环,直至队列为空,层序遍历就完成了。
我们将依次展示三种特殊二叉树:完全二叉树、满二叉树和平衡二叉树(AVL树)的相关代码。
完全二叉树
完全二叉树是指除了最后一层之外,每一层都被完全填满,且最后一层的所有节点都尽可能地集中在左侧。这里,我们使用一个数组来表示完全二叉树,数组中的每个索引对应二叉树中的一个节点。
完全二叉树的表示和检查
public class CompleteBinaryTree {
// 使用数组来表示完全二叉树,arr[i]表示节点i的值
private int[] arr;
public CompleteBinaryTree(int[] arr) {
this.arr = arr;
}
// 检查给定数组是否表示一个完全二叉树
public boolean isComplete() {
int i = 0;
while (i < arr.length) {
// 如果左子节点的索引超出数组范围,则不是完全二叉树
if (2 * i + 1 < arr.length) {
// 如果右子节点的索引超出数组范围,则当前节点必须是最后一个节点
if (2 * i + 2 < arr.length) {
i++;
} else {
return i == arr.length - 1;
}
} else {
break;
}
}
return true;
}
// 省略其他方法...
}
满二叉树
满二叉树是指除了叶子节点之外,每个节点都有两个子节点。在满二叉树中,节点总数与树的高度有确定的数学关系:如果树的高度是h
,那么节点总数是2^h - 1
。
满二叉树的表示和检查
public class FullBinaryTree {
// 使用TreeNode类表示二叉树的节点
public static class TreeNode {
int val;
TreeNode left;
TreeNode right;
TreeNode(int x) {
val = x;
}
}
// 检查一个二叉树是否是满二叉树
public boolean isFull(TreeNode root) {
if (root == null) {
return true;
}
// 如果一个节点有一个孩子而没有另一个孩子,则它不可能是满的
if ((root.left == null && root.right != null) || (root.left != null && root.right == null)) {
return false;
}
// 递归检查左右子树
return isFull(root.left) && isFull(root.right);
}
// 省略其他方法...
}
平衡二叉树(AVL树)
AVL树是一种自平衡的二叉查找树,它的每个节点的左右子树的高度最多相差1。为了维持这种平衡,插入和删除操作可能会涉及到树的旋转。
AVL树的节点定义与基本旋转操作
public class AVLTree {
// 定义AVL树的节点
public static class AVLTreeNode {
int val;
int height; // 节点的高度
AVLTreeNode left;
AVLTreeNode right;
AVLTreeNode(int x) {
val = x;
height = 1; // 新节点的高度默认为1
}
}
// 获取节点的高度
private int height(AVLTreeNode node) {
return node == null ? 0 : node.height;
}
// 计算节点的平衡因子
private int getBalanceFactor(AVLTreeNode node) {
if (node == null) {
return 0;
}
return height(node.left) - height(node.right);
}
// 右旋转
private AVLTreeNode rightRotate(AVLTreeNode y) {
AVLTreeNode x = y.left;
AVLTreeNode T2```java
// T2 = x的右子树
y.left = x.right;
x.right = y;
// 更新y和x的高度
y.height = Math.max(height(y.left), height(y.right)) + 1;
x.height = Math.max(height(x.left), height(x.right)) + 1;
// 返回新的根节点
return x;
}
// 左旋转
private AVLTreeNode leftRotate(AVLTreeNode x) {
AVLTreeNode y = x.right;
AVLTreeNode T2 = y.left;
// 旋转操作
y.left = x;
x.right = T2;
// 更新高度
x.height = Math.max(height(x.left), height(x.right)) + 1;
y.height = Math.max(height(y.left), height(y.right)) + 1;
// 返回新的根节点
return y;
}
// 重新平衡树
private AVLTreeNode rebalance(AVLTreeNode node) {
// 更新节点高度
node.height = Math.max(height(node.left), height(node.right)) + 1;
int balanceFactor = getBalanceFactor(node);
// 如果这个节点变得不平衡,那么有四种情况
// 左左 - 右旋转
if (balanceFactor > 1 && getBalanceFactor(node.left) >= 0) {
return rightRotate(node);
}
// 左右 - 左右旋转
if (balanceFactor > 1 && getBalanceFactor(node.left) < 0) {
node.left = leftRotate(node.left);
return rightRotate(node);
}
// 右右 - 左旋转
if (balanceFactor < -1 && getBalanceFactor(node.right) <= 0) {
return leftRotate(node);
}
// 右左 - 右左旋转
if (balanceFactor < -1 && getBalanceFactor(node.right) > 0) {
node.right = rightRotate(node.right);
return leftRotate(node);
}
// 没有失衡,不需要旋转
return node;
}
// 插入节点到AVL树中,并返回插入后的新树的根
public AVLTreeNode insert(AVLTreeNode node, int val) {
if (node == null) {
return new AVLTreeNode(val);
}
if (val < node.val) {
node.left = insert(node.left, val);
} else if (val > node.val) {
node.right = insert(node.right, val);
} else {
// 不允许有相同的值(已经存在的值)
return node;
}
// 重新平衡AVL树并返回结果
return rebalance(node);
}
// 省略删除操作和其他辅助函数...
}
在上面的代码中,我们定义了一个AVL树的节点类AVLTreeNode
,并且实现了四个主要操作:获取节点的高度height
,计算平衡因子getBalanceFactor
,右旋转rightRotate
和左旋转leftRotate
。这些旋转操作是为了重新平衡树,特别是在插入或删除节点后。
我们还实现了一个rebalance
方法,它会根据平衡因子来判断一个节点是否平衡,并执行相应的旋转操作以保持AVL树的平衡性。
最后,我们提供了insert
方法,它允许在AVL树中插入一个新值。当插入一个值时,我们可能需要进行一系列的旋转操作来保证树的平衡。删除操作也是类似的,但由于其复杂性,在这里我们没有展示。
请注意,这里的代码只展示了AVL树的基本框架和核心功能。在实际应用中,我们需要更加完善的实现,包括错误处理、删除功能以及更多的辅助方法。