一、概念
二叉树是常用的一种数据结构。二叉树是一种树形结构,其中每个节点最多有两个子节点,通常被称为左子节点和右子节点。二叉树可以是空树(没有任何节点),也可以只有一个根节点或多个节点。
二、二叉树遍历
遍历是对二叉树的一种基本运算,目的是按一定的规则和顺序访问二叉树中的所有节点,且每个节点仅被访问一次。常见的遍历算法分为深度遍历和广度(层序)遍历。其中深度遍历有三种:前序遍历、中序遍历、后序遍历。遍历算法的实现通常通过递归或迭代的方式实现。
题干:
给定一个二叉树的根节点root,对二叉树进行遍历
定义好的二叉树TreeNode对象结构
/**
* Definition for a binary tree node.
*/
public class TreeNode {
int val;
TreeNode left;
TreeNode right;
TreeNode() {}
TreeNode(int val) { this.val = val; }
TreeNode(int val, TreeNode left, TreeNode right) {
this.val = val;
this.left = left;
this.right = right;
}
}
1、前序遍历
前序遍历是从根节点开始对二叉树结构进行遍历的,通过根节点再按照左子节点和右子节点对二叉树进行顺序访问遍历的。这种按照根节点 -> 左子树 -> 右子树顺序遍历二叉树称为前序遍历。
输入:[1, 2, 3, 4, 5, 6, 7]
输出:[1, 2, 4, 5, 3, 6, 7]
1. 递归遍历
class Solution {
public List<Integer> preorderTraversal(TreeNode root) {
List<Integer> list = new ArrayList<>();
//递归算法
preorder(root, list);
return list;
}
//递归算法
public void preorder(TreeNode root, List<Integer> list){
if(root == null){
return;
}
//获取根节点数据
list.add(root.val);
//递归遍历左子树节点
preorder(root.left, list);
//递归遍历右子树节点
preorder(root.right, list);
}
}
复杂度分析:
时间复杂度:O(n),其中n为二叉树节点的个数。二叉树的遍历中每个节点都会被访问一次且只会访问一次。
空间复杂度:O(n),为递归过程中栈的开销,平均情况下为 O(logn),最坏情况下树呈现链状,为 O(n)。
2. 迭代遍历
class Solution {
public List<Integer> preorderTraversal(TreeNode root) {
List<Integer> list = new ArrayList<>();
//迭代算法
preorderStack(root, list);
return list;
}
//通过栈的先进后出特性,将root节点放到栈中,对二叉树从遍历
public void preorderStack(TreeNode root, List<Integer> list){
Deque<TreeNode> stack = new LinkedList<>();
while(root != null || stack.isEmpty()){
//root不为null时,将root节点和左子树节点放到栈中
while(root != null){
//获取根节点数据
list.add(root.val);
//将root节点放入栈中
stack.push(root);
//处理root左子树节点
root = root.left;
}
//root为null,获取栈中root节点
root = stack.pop();
//处理root右子树节点
root = root.right;
}
}
}
复杂度分析:
时间复杂度:O(n),其中n为二叉树节点的个数。二叉树的遍历中每个节点都会被访问一次且只会访问一次。
空间复杂度:O(n),为迭代过程中显式栈的开销,平均情况下为 O(logn),最坏情况下树呈现链状,为 O(n)。
2、中序遍历
按照访问左子树——根节点——右子树的方式遍历这棵树,而在访问左子树或者右子树的时候,我们按照同样的方式遍历,直到遍历完整棵树。这种按照左子树 -> 根节点 -> 右子树顺序遍历二叉树被称为中序遍历。
输入:[1, 2, 3, 4, 5, 6, 7]
输出:[4, 2, 5, 1, 6, 3, 7]
1. 递归遍历
class Solution {
public List<Integer> inorderTraversal(TreeNode root) {
List<Integer> list = new ArrayList<>();
//递归算法
inorder(root, list);
return list;
}
//递归算法
public void inorder(TreeNode root, List<Integer> list){
if(root == null){
return;
}
//递归遍历处理左子树
inorder(root.left, list);
//获取根节点的数据
list.add(root.val);
//递归遍历处理右子树
inorder(root.right, list);
}
}
复杂度分析:
时间复杂度:O(n),其中n为二叉树节点的个数。二叉树的遍历中每个节点都会被访问一次且只会访问一次。
空间复杂度:O(n),空间复杂度取决于递归的栈深度,而栈深度在二叉树为一个链的情况下会达到O(n)的级别。
2. 迭代遍历
class Solution {
public List<Integer> inorderTraversal(TreeNode root) {
List<Integer> list = new ArrayList<>();
//迭代算法
inorderStack(root, list);
return list;
}
//通过栈的先进后出特性,将root节点放到栈中,对二叉树从遍历
public void inorderStack(TreeNode root, List<Integer> list){
//定义一个栈用于存放二叉树
Deque<TreeNode> stack = new LinkedList<>();
while(root != null || stack.isEmpty()){
//root不为null时,将root节点和左子树节点放到栈中
while(root != null){
//将root节点放入栈中
stack.push(root);
//处理root左子树节点
root = root.left;
}
//root为null,获取栈中root节点
root = stack.pop();
//获取根节点数据
list.add(root.val);
//处理root右子树节点
root = root.right;
}
}
}
复杂度分析:
时间复杂度:O(n),其中n为二叉树节点的个数。二叉树的遍历中每个节点都会被访问一次且只会访问一次。
空间复杂度:O(n),空间复杂度取决于递归的栈深度,而栈深度在二叉树为一个链的情况下会达到O(n)的级别。
3、后序遍历
按照访问左子树——右子树——根节点的方式遍历这棵树,而在访问左子树或者右子树的时候,我们按照同样的方式遍历,直到遍历完整棵树。这种按照左子树 -> 右子树 -> 根节点顺序遍历二叉树被称为后序遍历。
输入:[1, 2, 3, 4, 5, 6, 7]
输出:[4, 5, 2, 6, 7, 3, 1]
1. 递归遍历
class Solution {
public List<Integer> postorderTraversal(TreeNode root) {
List<Integer> list = new ArrayList<>();
//递归算法
postorder(root, list);
return list;
}
//递归算法
public void postorder(TreeNode root, List<Integer> list){
if(root == null){
return;
}
//递归遍历处理左子树
postorder(root.left, list);
//递归遍历处理右子树
postorder(root.right, list);
//获取根节点的数据
list.add(root.val);
}
}
复杂度分析:
时间复杂度:O(n),其中n为二叉树节点的个数。二叉树的遍历中每个节点都会被访问一次且只会访问一次。
空间复杂度:O(n),为递归过程中栈的开销,平均情况下为 O(logn),最坏情况下树呈现链状,为 O(n)。
2. 迭代遍历
class Solution {
public List<Integer> postorderTraversal(TreeNode root) {
List<Integer> list = new ArrayList<>();
//迭代算法
postorderStack(root, list);
return list;
}
//通过栈的先进后出特性,将root节点放到栈中,对二叉树从遍历
public void postorderStack(TreeNode root, List<Integer> list){
//定义一个栈用于存放二叉树
Deque<TreeNode> stack = new LinkedList<>();
//用于判断当前处理右子树与上次处理的二叉树是否相同,避免再处理一次
TreeNode pre = null;
while(root != null || stack.isEmpty()){
//root不为null时,将root节点和左子树节点放到栈中
while(root != null){
//将root节点放入栈中
stack.push(root);
//处理root左子树节点
root = root.left;
}
//root为null,获取栈中root节点
root = stack.pop();
//右子树为null 或 右子树 与 上次处理的pre 子树相等 不需在处理一遍, 取root节点值
if(root.right == null || root.right == pre){
//获取根节点数据
list.add(root.val);
//将当前root节点赋值给pre 用于标记
pre = root;
//将root重置为null,再次出栈遍历处理
root = null;
}else{
//右子树不为null时,需将右子树入栈处理
stack.push(root);
//处理root右子树节点
root = root.right;
}
}
}
}
复杂度分析:
时间复杂度:O(n),其中n为二叉树节点的个数。二叉树的遍历中每个节点都会被访问一次且只会访问一次。
空间复杂度:O(n),为递归过程中栈的开销,平均情况下为 O(logn),最坏情况下树呈现链状,为 O(n)。
4、层序遍历(广度遍历)
按照访问从上到下,从左到右一层一层的方式遍历这棵树,直到遍历完整棵树。这种按照从上到下,从左到右顺序遍历二叉树被称为层序(广度)遍历。
输入:[1, 2, 3, 4, 5, 6, 7]
输出:[[1], [2, 3], [4, 5, 6, 7]]
1. 递归遍历
class Solution {
public List<List<Integer>> levelOrder(TreeNode root) {
List<List<Integer>> list = new ArrayList<>();
//将root根节点当成第0层
order(root, 0, list);
return list;
}
//递归解法
public void order(TreeNode root, int level, List<List<Integer>> list){
if(root == null){
return;
}
//对当list集合包含的层数等于level层数时
if(list.size() == level){
//新添加一个集合用于存储当前层级的节点元素
list.add(new ArrayList<Integer>());
}
//将节点的数据加入level层数的集合中
list.get(level).add(root.val);
if(root.left != null){
//左子节点不为空时 递归处理左子结点
order(root.left, level + 1, list);
}
if(root.right != null){
//右子节点不为空时 递归处理右子结点
order(root.right, level + 1, list);
}
}
}
复杂度分析:
时间复杂度:O(n),其中n为二叉树节点的个数。二叉树的遍历中每个节点都会被访问一次且只会访问一次。
空间复杂度:O(n),空间复杂度取决于递归的栈深度,而栈深度在二叉树为一个链的情况下会达到O(n)的级别。
2. 迭代遍历
class Solution {
public List<List<Integer>> levelOrder(TreeNode root) {
List<List<Integer>> list = new ArrayList<>();
orderStack(root, list);
return list;
}
//迭代 利用队列先进先出特点将节点放入队列中 在按照节点获取数据
public void orderStack(TreeNode root, List<List<Integer>> list){
if(root == null){
return;
}
//创建一个队列linkedList实现。利用队列FIFO数据结构实现遍历
Queue<TreeNode> queue = new LinkedList<>();
//将根节点加入队列,作为遍历的起点
queue.offer(root);
//利用while循环遍历队列,直到队列为空,每次循环代表遍历树的一层
while(!queue.isEmpty()){
//存储当前层所有节点值
List<Integer> levels = new ArrayList<>();
//获取当前队列大小,即当前层所有节点数
int currentLevelSize = queue.size();
//从1开始遍历队列中节点
for(int i = 1; i <= currentLevelSize; i++){
//通过poll方法按照顺序获取队列头部的节点
TreeNode node = queue.poll();
//将节点值添加到数组中
levels.add(node.val);
//判断当前节点是否存在左子结点
if(node.left != null){
//将左子结点放到队列中便于下次while循环中处理
queue.offer(node.left);
}
//判断当前节点是否存在右子结点
if(node.right != null){
//将右子结点放到队列中便于下次while循环中处理
queue.offer(node.right);
}
}
//将当前层级中节点值放入集合中
list.add(levels);
}
}
}
复杂度分析:
时间复杂度:O(n),其中n为二叉树节点的个数。二叉树的遍历中每个节点都会被访问一次且只会访问一次。
空间复杂度:O(n),空间复杂度取决于递归的栈深度,而栈深度在二叉树为一个链的情况下会达到O(n)的级别。
三、总结
二叉树作为一种重要的数据结构,在计算机科学中具有广泛的应用。了解二叉树的原理和算法对于深入学习数据结构和算法设计具有重要意义。通过掌握二叉树的遍历操作,可以高效地解决许多实际问题。