浅谈二叉树,平衡二叉树,红黑树以及java简单实现
二叉树的相关介绍
二叉树(Binary Tree)
- 二叉树是一种每个节点最多只有两个子节点的树结构。
- 每个节点最多有两个子节点,称为左子节点和右子节点。
- 二叉树的节点可以为空(null)。
- 二叉树常用于实现搜索算法和排序算法,例如二叉搜索树和堆等数据结构。
二叉树 的第 i 层至多拥有 2^(i-1)
个节点,深度为 k 的二叉树至多总共有 2^(k+1)-1
个节点(满二叉树的情况),至少有 2^(k) 个节点
基于二叉查找树的这种特点,在查找某个节点的时候,可以采取类似于二分查找的思想,快速找到某个节点。n 个节点的二叉查找树,正常的情况下,查找的时间复杂度为 O(logN)。之所以说是正常情况下,是因为二叉查找树有可能出现一种极端的情况
这种情况也是满足二叉查找树的条件,然而,此时的二叉查找树已经近似退化为一条链表,这样的二叉查找树的查找时间复杂度顿时变成了 O(n)。由此必须防止这种情况发生,为了解决这个问题,于是引申出了平衡二叉树
优点:有序 缺点:极端条件下会退化成链表,降低查找效率
实际应用场景:
- 表达式树:用于表示和计算数学表达式,便于进行求值和运算。
- 二叉搜索树(Binary Search Tree):用于快速查找和插入数据,常见于数据库索引和字典实现等。
- 哈夫曼树(Huffman Tree):用于数据压缩,通过构建最优二叉树来实现数据的编码和解码。
平衡二叉树(Balanced Binary Tree)
- 平衡二叉树是一种二叉树,其左右子树的高度差不超过某个固定的值(如1)。
- 平衡二叉树的目的是保持树的高度平衡,以提高搜索、插入和删除等操作的效率。
- 平衡二叉树的常见实现包括AVL树和红黑树。
AVL树也规定了左结点小于根节点,右结点大于根节点。并且还规定了左子树和右子树的高度差不得超过1。这样保证了它不会成为线性的链表。 AVL树的查找稳定,查找、插入、删除的时间复杂度都为O(logN) 但是由于要维持自身的平衡,所以进行插入和删除结点操作的时候,需要对结点进行频繁的旋转
优点:有序,解决了BST会退化成线性结构的问题 缺点:进行插入和删除结点操作的时候,需要对结点进行频繁的旋转
实际应用场景:
- AVL树:一种自平衡的二叉搜索树,常用于数据库索引、集合类的实现等,保证插入和删除操作的时间复杂度为O(log N)。
- Treap树:结合了二叉搜索树和堆的特性,常用于排序和动态顺序统计等场景。
- 伸展树(Splay Tree):通过频繁访问的节点放到树的根节点来提高访问效率,常用于缓存系统和网络路由器等。
红黑树(Red-Black Tree)
由来
虽然平衡树解决了二叉查找树退化为近似链表的缺点,能够把查找时间控制在 O(logn),不过却不是最佳的,因为平衡树要求每个节点的左子树和右子树的高度差至多等于1,这个要求实在是太严了,导致每次进行插入/删除节点的时候,几乎都会破坏平衡树的第二个规则,进而都需要通过左旋和右旋来进行调整,使之再次成为一颗符合要求的平衡树
简介
红黑树是一种二叉查找树,但在每个节点增加一个存储位表示节点的颜色,可以是 red 或 black。通过对任何一条从根到叶子的路径上各个节点着色的方式的限制,红黑树确保没有一条路径会比其它路径长出两倍。它是一种弱平衡二叉树 (由于是若平衡,可以推出,相同的节点情况下,AVL 树的高度低于红黑树),相对于要求严格的 AVL 树来说,它的旋转次数变少,所以对于搜索、插入、删除操作多的情况下,我们就用红黑树。
特性
- 红黑树是一种自平衡的二叉查找树。
- 每个节点都有一个颜色属性,红色或黑色。
- 红黑树具有以下特性:
- 根节点是黑色的。
- 所有叶子节点(空节点)都是黑色的。
- 如果一个节点是红色的,则其两个子节点都是黑色的。
- 任意节点到其每个叶子节点的路径上,黑色节点的数量是相同的。
- 红黑树的平衡性质保证了树的高度近似于 log(N),其中 N 是节点数量。
优点
- 平衡性: 红黑树通过保持特定的平衡性质,能够保持树的高度相对较低,从而提供快速的插入、删除和搜索操作。其时间复杂度为 O(log N),其中 N 是树中节点的数量。
- 高效的查找: 红黑树的结构允许高效的查找操作,比线性数据结构(如数组)更快速,且具有稳定的查找性能。
- 有序性: 红黑树中的节点按照键值的顺序排列,使得它非常适用于需要有序遍历的场景,例如范围查询等。
- 广泛应用: 红黑树被广泛应用于各种数据结构和算法实现中,如集合类、关联容器、内存管理等。
缺点
- 相对复杂: 相比于普通的二叉搜索树,红黑树的实现和维护较为复杂,需要处理各种旋转和颜色变换等操作,这可能会增加代码的复杂性。
- 内存占用: 相比于其他树结构,红黑树需要额外的存储空间来存储节点的颜色信息,这可能导致额外的内存消耗。
- 插入和删除操作的调整: 红黑树的插入和删除操作需要进行节点的旋转和颜色调整,这可能导致算法的实现相对较复杂,且一些情况下需要进行多次调整。
- 不适合频繁修改: 如果需要频繁地插入和删除节点,红黑树的维护操作可能会带来较大的开销,因为平衡性的维护可能需要进行多次旋转和颜色调整
实际应用场景:
- STL(C++标准模板库)中的map和set的实现:红黑树常用于实现关联容器,提供高效的查找、插入和删除操作。
- Linux内核中的进程调度器(Completely Fair Scheduler):使用红黑树来管理进程的优先级和调度顺序。
- Java的TreeMap和TreeSet:底层采用红黑树实现,提供有序的键值存储和高效的查找。
简单代码实现
public class RedBlackTree<T extends Comparable<T>> {
private static final boolean RED = true;
private static final boolean BLACK = false;
private Node<T> root;
private class Node<T> {
T value; // 节点存储的值
Node<T> left; // 左子节点
Node<T> right; // 右子节点
boolean color; // 节点颜色(红色或黑色)
Node(T value, boolean color) {
this.value = value;
this.color = color;
}
}
// 判断节点的颜色
private boolean isRed(Node<T> node) {
if (node == null) {
return false;
}
return node.color == RED;
}
/**
* 执行左旋转操作,将指定节点的右子节点旋转为其父节点,返回旋转后的根节点。
*
* @param h 要执行左旋转的节点
* @return 旋转后的根节点
*/
private Node<T> rotateLeft(Node<T> h) {
Node<T> x = h.right;
h.right = x.left;
x.left = h;
x.color = h.color;
h.color = RED;
return x;
}
/**
* 执行右旋转操作,将指定节点的左子节点旋转为其父节点,返回旋转后的根节点。
*
* @param h 要执行右旋转的节点
* @return 旋转后的根节点
*/
private Node<T> rotateRight(Node<T> h) {
Node<T> x = h.left;
h.left = x.right;
x.right = h;
x.color = h.color;
h.color = RED;
return x;
}
/**
* 执行颜色翻转操作,将指定节点及其左右子节点的颜色进行交换。
* 要求指定节点的左右子节点必须存在且颜色均为黑色。
*
* @param h 指定节点
*/
private void flipColors(Node<T> h) {
h.color = RED;
h.left.color = BLACK;
h.right.color = BLACK;
}
/**
* 插入节点
*
* @param value 要插入的节点值
*/
public void insert(T value) {
root = insert(root, value);
root.color = BLACK;
}
private Node<T> insert(Node<T> node, T value) {
if (node == null) {
// 如果当前节点为空,说明已到达插入位置,创建新节点并返回
return new Node<>(value, RED);
}
int cmp = value.compareTo(node.value);
if (cmp < 0) {
// 如果要插入的值小于当前节点值,向左子树递归插入
node.left = insert(node.left, value);
} else if (cmp > 0) {
// 如果要插入的值大于当前节点值,向右子树递归插入
node.right = insert(node.right, value);
} else {
// 如果要插入的值等于当前节点值,更新当前节点的值
node.value = value;
}
// 红黑树的插入修复操作
if (isRed(node.right) && !isRed(node.left)) {
// 如果当前节点的右子节点是红色而左子节点是黑色,进行左旋转
node = rotateLeft(node);
}
if (isRed(node.left) && isRed(node.left.left)) {
// 如果当前节点的左子节点是红色且其左子节点也是红色,进行右旋转
node = rotateRight(node);
}
if (isRed(node.left) && isRed(node.right)) {
// 如果当前节点的左子节点和右子节点都是红色,进行颜色翻转
flipColors(node);
}
return node;
}
/**
* 删除节点
*
* @param value 要删除的节点值
*/
public void delete(T value) {
if (!contains(value)) {
return;
}
if (!isRed(root.left) && !isRed(root.right)) {
root.color = RED;
}
root = delete(root, value);
if (root != null) {
root.color = BLACK;
}
}
/**
* 在以指定节点为根的子树中删除指定值的节点,并返回删除后的根节点。
*
* @param node 子树的根节点
* @param value 要删除的节点值
* @return 删除后的根节点
*/
private Node<T> delete(Node<T> node, T value) {
if (value.compareTo(node.value) < 0) {
if (!isRed(node.left) && !isRed(node.left.left)) {
// 如果当前节点的左子节点和左子节点的左子节点都是黑色,进行移动操作
node = moveRedLeft(node);
}
// 向左子树递归删除
node.left = delete(node.left, value);
} else {
if (isRed(node.left)) {
// 如果当前节点的左子节点是红色,进行右旋转
node = rotateRight(node);
}
if (value.compareTo(node.value) == 0 && node.right == null) {
// 如果找到了要删除的节点,并且当前节点的右子节点为空,直接删除
return null;
}
if (!isRed(node.right) && !isRed(node.right.left)) {
// 如果当前节点的右子节点和右子节点的左子节点都是黑色,进行移动操作
node = moveRedRight(node);
}
if (value.compareTo(node.value) == 0) {
// 找到了要删除的节点
Node<T> min = findMin(node.right);
node.value = min.value;
node.right = deleteMin(node.right);
} else {
// 向右子树递归删除
node.right = delete(node.right, value);
}
}
return balance(node);
}
/**
* 对当前节点进行红色翻转,并进行左旋转操作。
* 如果当前节点的右子节点的左子节点是红色,则进行相应的右旋转和左旋转操作后,进行颜色翻转。
*
* @param node 当前节点
* @return 旋转后的节点
*/
private Node<T> moveRedLeft(Node<T> node) {
flipColors(node); // 红色翻转
if (isRed(node.right.left)) {
node.right = rotateRight(node.right); // 右旋转
node = rotateLeft(node); // 左旋转
flipColors(node); // 颜色翻转
}
return node;
}
/**
* 对当前节点进行红色翻转,并进行右旋转操作。
* 如果当前节点的左子节点的左子节点是红色,则进行右旋转操作后,进行颜色翻转。
*
* @param node 当前节点
* @return 旋转后的节点
*/
private Node<T> moveRedRight(Node<T> node) {
flipColors(node); // 红色翻转
if (isRed(node.left.left)) {
node = rotateRight(node); // 右旋转
flipColors(node); // 颜色翻转
}
return node;
}
/**
* 删除以当前节点为根的子树中的最小节点,并返回删除节点后的根节点。
*
* @param node 当前节点
* @return 删除节点后的根节点
*/
private Node<T> deleteMin(Node<T> node) {
if (node.left == null) {
// 如果左子节点为空,说明已经到达最小节点,返回 null
return null;
}
if (!isRed(node.left) && !isRed(node.left.left)) {
// 如果当前节点的左子节点和左子节点的左子节点都是黑色,进行移动操作
node = moveRedLeft(node);
}
// 递归删除左子树中的最小节点
node.left = deleteMin(node.left);
return balance(node);
}
/**
* 查找以当前节点为根的子树中的最小节点。
*
* @param node 当前节点
* @return 最小节点
*/
private Node<T> findMin(Node<T> node) {
if (node.left == null) {
// 如果左子节点为空,说明已经到达最小节点,返回当前节点
return node;
}
// 递归查找左子树中的最小节点
return findMin(node.left);
}
/**
* 对当前节点进行颜色调整,保持红黑树的平衡性。
*
* @param node 当前节点
* @return 调整后的节点
*/
private Node<T> balance(Node<T> node) {
if (isRed(node.right)) {
// 如果当前节点的右子节点是红色,进行左旋转
node = rotateLeft(node);
}
if (isRed(node.left) && isRed(node.left.left)) {
// 如果当前节点的左子节点和左子节点的左子节点都是红色,进行右旋转
node = rotateRight(node);
}
if (isRed(node.left) && isRed(node.right)) {
// 如果当前节点的左子节点和右子节点都是红色,进行颜色翻转
flipColors(node);
}
return node;
}
/**
* 查找节点
*
* @param value 要查找的节点值
* @return 如果节点存在返回 true,否则返回 false
*/
public boolean contains(T value) {
return contains(root, value);
}
private boolean contains(Node<T> node, T value) {
if (node == null) {
return false;
}
int cmp = value.compareTo(node.value);
if (cmp < 0) {
return contains(node.left, value);
} else if (cmp > 0) {
return contains(node.right, value);
} else {
return true;
}
}
}
二叉树的存储
链式存储结构(Linked Representation):
- 在链式存储结构中,每个节点由一个包含数据和指向左右子节点的指针(引用)的对象(或结构体)表示。
- 每个节点对象包含三个成员变量:数据、左子节点指针和右子节点指针。
- 通过指针的连接,可以形成一个动态的树结构,每个节点可以灵活地连接任意数量的子节点。
- 链式存储结构易于实现和操作,适用于任意形状和大小的二叉树。
简单代码实现
树节点对象
public class BinaryTreeNode<T> {
T val;
BinaryTreeNode<T> left;
BinaryTreeNode<T> right;
public BinaryTreeNode(T val) {
this.val = val;
this.left = null;
this.right = null;
}
}
树结构实现
public class BinaryTree<T extends Comparable<T>> {
private BinaryTreeNode<T> root;
public BinaryTree() {
root = null;
}
/**
* 插入节点
*/
public void insert(T val) {
root = insertHelper(root, val);
}
/**
* 递归地插入节点到二叉树中
*
* @param node 当前节点
* @param val 要插入的值
* @return 插入节点后的二叉树的根节点
*/
private BinaryTreeNode<T> insertHelper(BinaryTreeNode<T> node, T val) {
if (node == null) {
// 若当前节点为空,创建一个新节点并将其作为叶子节点插入到二叉树中
return new BinaryTreeNode<>(val);
}
if (val.compareTo(node.val) < 0) {
// 若插入值小于当前节点值,则插入到左子树
node.left = insertHelper(node.left, val);
} else if (val.compareTo(node.val) > 0) {
// 若插入值大于当前节点值,则插入到右子树
node.right = insertHelper(node.right, val);
}
return node;
}
/**
* 删除节点
*/
public void delete(T val) {
root = deleteHelper(root, val);
}
/**
* 递归地删除节点
*
* @param node 当前节点
* @param val 要删除的值
* @return 删除节点后的二叉树的根节点
*/
private BinaryTreeNode<T> deleteHelper(BinaryTreeNode<T> node, T val) {
if (node == null) {
return null;
}
if (val.compareTo(node.val) < 0) {
// 若要删除的值小于当前节点值,则继续在左子树中删除
node.left = deleteHelper(node.left, val);
} else if (val.compareTo(node.val) > 0) {
// 若要删除的值大于当前节点值,则继续在右子树中删除
node.right = deleteHelper(node.right, val);
} else {
// 找到目标节点
if (node.left == null && node.right == null) {
// 目标节点为叶子节点
return null;
} else if (node.left == null) {
// 目标节点只有右子节点
return node.right;
} else if (node.right == null) {
// 目标节点只有左子节点
return node.left;
} else {
// 目标节点有左右子节点
BinaryTreeNode<T> successor = findMin(node.right);
node.val = successor.val;
node.right = deleteHelper(node.right, successor.val);
}
}
return node;
}
/**
* 查找二叉树中的最小值节点
*
* @param node 当前节点
* @return 二叉树中的最小值节点
*/
private BinaryTreeNode<T> findMin(BinaryTreeNode<T> node) {
while (node.left != null) {
node = node.left;
}
return node;
}
/**
* 查找节点
*/
public BinaryTreeNode<T> search(T val) {
return searchHelper(root, val);
}
/**
* 递归地查找节点
*
* @param node 当前节点
* @param val 要查找的值
* @return 找到的节点或 null(若节点不存在)
*/
private BinaryTreeNode<T> searchHelper(BinaryTreeNode<T> node, T val) {
if (node == null || val.compareTo(node.val) == 0) {
return node;
}
if (val.compareTo(node.val) < 0) {
return searchHelper(node.left, val);
} else {
return searchHelper(node.right, val);
}
}
}
测试代码
BinaryTree<Integer> binaryTree = new BinaryTree<>();
// 插入节点
binaryTree.insert(5);
binaryTree.insert(3);
binaryTree.insert(7);
binaryTree.insert(2);
binaryTree.insert(4);
// 查找节点
BinaryTreeNode<Integer> node = binaryTree.search(3);
if (node != null) {
System.out.println("Found node with value 3");
} else {
System.out.println("Node with value 3 not found");
}
// 删除节点
binaryTree.delete(5);
node = binaryTree.search(5);
if (node != null) {
System.out.println("Found node with value 5");
} else {
System.out.println("Node with value 5 not found");
}
顺序存储结构(Array Representation):
- 在顺序存储结构中,使用一个数组来存储二叉树的节点,按照一定的规律将节点在数组中进行排列。
- 通常使用数组的索引来表示节点之间的关系。对于一个节点在数组中的索引为
i
,其左子节点的索引为2i+1
,右子节点的索引为2i+2
。 - 顺序存储结构将二叉树的节点按照层次顺序依次存储在数组中,从根节点开始,依次存储每一层的节点。
- 顺序存储结构节省了指针的存储空间,对于完全二叉树来说,利用数组的连续内存可以更高效地进行访问和遍历。
- 顺序存储结构适用于完全二叉树或近似完全二叉树,对于一般的二叉树,可能会浪费部分数组空间。
完全二叉树存储
非完全二叉树存储 (在数组中就会出现空隙,导致内存利用率降低)
简单代码实现
public class ArrayBinaryTree<T> {
private T[] array;
private int size;
/**
* 构造函数,初始化数组和大小
*
* @param capacity 数组容量
*/
public ArrayBinaryTree(int capacity) {
array = (T[]) new Object[capacity];
size = 0;
}
/**
* 插入节点
*
* @param data 要插入的节点值
*/
public void insert(T data) {
if (size == array.length) {
System.out.println("Binary tree is full, cannot insert new node.");
return;
}
array[size] = data;
size++;
}
/**
* 删除节点
*
* @param data 要删除的节点值
*/
public void delete(T data) {
int index = search(data);
if (index == -1) {
System.out.println("Node with value " + data + " not found.");
return;
}
// 将最后一个节点的值复制到要删除的节点位置,并将最后一个节点置为空
array[index] = array[size - 1];
array[size - 1] = null;
size--;
}
/**
* 查找节点
*
* @param data 要查找的节点值
* @return 节点值在数组中的索引,若未找到则返回 -1
*/
public int search(T data) {
for (int i = 0; i < size; i++) {
if (array[i].equals(data)) {
return i;
}
}
return -1;
}
/**
* 修改节点值
*
* @param oldData 要修改的节点旧值
* @param newData 要修改的节点新值
*/
public void update(T oldData, T newData) {
int index = search(oldData);
if (index == -1) {
System.out.println("Node with value " + oldData + " not found.");
return;
}
array[index] = newData;
}
/**
* 输出二叉树的内容
*/
public void print() {
for (int i = 0; i < size; i++) {
System.out.print(array[i] + " ");
}
System.out.println();
}
}
对比
存储方式 | 优点 | 缺点 | 适用场景 |
---|---|---|---|
链式存储 | - 灵活插入和删除节点 - 不受固定容量限制 | - 需要额外的指针空间 - 访问元素需要遍历链表 | - 需要频繁插入和删除节点的情况 - 不知道数据量大小的情况 |
顺序存储 | - 直接访问元素,效率高 | - 需要预先分配固定容量 - 插入和删除开销较大 | - 数据量已知且固定的情况 - 需要高效访问元素的情况 |
链式存储适用于需要频繁插入和删除节点的情况,由于链式存储不需要连续的内存空间,可以灵活地进行节点的插入和删除操作。链式存储的缺点是需要额外的指针空间,并且访问元素需要遍历链表。
顺序存储适用于数据量已知且固定的情况,由于顺序存储使用连续的内存空间存储元素,可以直接访问元素,因此访问效率较高。顺序存储的缺点是需要预先分配固定容量,如果数据量超过容量限制,则需要重新分配空间,并且插入和删除元素的开销较大。
根据具体的需求和场景,可以选择适合的存储方式。链式存储适合需要频繁插入和删除节点的情况,而顺序存储适合数据量已知且固定,需要高效访问元素的情况。
二叉树遍历
前序遍历(Preorder):
- 介绍:前序遍历是指先访问根节点,然后按照先左后右的顺序遍历左右子树。
- 使用场景:前序遍历可以用于打印表达式、构造二叉树、复制二叉树等。
- 优点:前序遍历是最自然、最直观的遍历方式,容易理解和实现。
- 缺点:前序遍历的输出顺序可能不符合某些特定需求。
- Java 代码实现:
class TreeNode {
int val;
TreeNode left;
TreeNode right;
public TreeNode(int val) {
this.val = val;
this.left = null;
this.right = null;
}
}
public class PreorderTraversal {
public void preorder(TreeNode root) {
if (root == null) {
return;
}
System.out.print(root.val + " "); // 先访问根节点
preorder(root.left); // 遍历左子树
preorder(root.right); // 遍历右子树
}
public static void main(String[] args) {
TreeNode root = new TreeNode(1);
root.left = new TreeNode(2);
root.right = new TreeNode(3);
root.left.left = new TreeNode(4);
root.left.right = new TreeNode(5);
PreorderTraversal traversal = new PreorderTraversal();
System.out.println("Preorder traversal:");
traversal.preorder(root);
}
}
中序遍历(Inorder):
- 介绍:中序遍历是指按照先左后根再右的顺序遍历左右子树,即从小到大顺序输出元素。
- 使用场景:中序遍历可以用于二叉搜索树的排序、表达式求值、二叉树的递增序列等。
- 优点:中序遍历可以按照从小到大的顺序输出有序二叉树的节点值。
- 缺点:对于一些特定需求,中序遍历的输出顺序可能不符合要求。
- Java 代码实现:
class TreeNode {
int val;
TreeNode left;
TreeNode right;
public TreeNode(int val) {
this.val = val;
this.left = null;
this.right = null;
}
}
public class InorderTraversal {
public void inorder(TreeNode root) {
if (root == null) {
return;
}
inorder(root.left); // 遍历左子树
System.out.print(root.val + " "); // 访问根节点
inorder(root.right); // 遍历右子树
}
public static void main(String[] args) {
TreeNode root = new TreeNode(4);
root.left = new TreeNode(2);
root.right = new TreeNode(6);
root.left.left = new TreeNode(1);
root.left.right = new TreeNode(3);
root.right.left = new TreeNode(5);
root.right.right = new TreeNode(7);
InorderTraversal traversal = new InorderTraversal();
System.out.println("Inorder traversal:");
traversal.inorder(root);
}
}
后序遍历(Postorder):
- 介绍:后序遍历是指按照先左后右再根的顺序遍历左右子树。
- 使用场景:后序遍历可以用于内存回收、文件系统的目录结构等。
- 优点:后序遍历在释放内存或处理文件系统目录结构时,能够确保子节点的操作已经完成。
- 缺点:后序遍历的输出顺序可能不符合某些特定需求。
class TreeNode {
int val;
TreeNode left;
TreeNode right;
public TreeNode(int val) {
this.val = val;
this.left = null;
this.right = null;
}
}
public class PostorderTraversal {
public void postorder(TreeNode root) {
if (root == null) {
return;
}
postorder(root.left); // 遍历左子树
postorder(root.right); // 遍历右子树
System.out.print(root.val + " "); // 访问根节点
}
public static void main(String[] args) {
TreeNode root = new TreeNode(4);
root.left = new TreeNode(2);
root.right = new TreeNode(6);
root.left.left = new TreeNode(1);
root.left.right = new TreeNode(3);
root.right.left = new TreeNode(5);
root.right.right = new TreeNode(7);
PostorderTraversal traversal = new PostorderTraversal();
System.out.println("Postorder traversal:");
traversal.postorder(root);
}
}
t val) {
this.val = val;
this.left = null;
this.right = null;
}
}
public class PostorderTraversal {
public void postorder(TreeNode root) {
if (root == null) {
return;
}
postorder(root.left); // 遍历左子树
postorder(root.right); // 遍历右子树
System.out.print(root.val + " "); // 访问根节点
}
public static void main(String[] args) {
TreeNode root = new TreeNode(4);
root.left = new TreeNode(2);
root.right = new TreeNode(6);
root.left.left = new TreeNode(1);
root.left.right = new TreeNode(3);
root.right.left = new TreeNode(5);
root.right.right = new TreeNode(7);
PostorderTraversal traversal = new PostorderTraversal();
System.out.println("Postorder traversal:");
traversal.postorder(root);
}
}