【算法和数据结构】二叉树

概述

树 是一种经常用到的数据结构,用来模拟具有树状结构性质的数据集合。

树里的每一个节点有一个值和一个包含所有子节点的列表。
从图的观点来看,树也可视为一个拥有N 个节点和N-1 条边的一个有向无环图。

二叉树是一种更为典型的树状结构。
如它名字所描述的那样,二叉树是每个节点最多有两个子树的树结构,通常子树被称作“左子树”和“右子树”。

完成后,你将:
1、掌握树和二叉树的概念
2、熟悉不同的遍历方法
3、运用递归解决二叉树相关题目

树的遍历

树和二叉树的基本概念。

本章学习、理解和区分树的遍历方法
能够运用递归方法解决树的为前序遍历、中序遍历和后序遍历问题
能用运用迭代方法解决树的为前序遍历、中序遍历和后序遍历问题
能用运用广度优先搜索解决树的层序遍历问题

树的遍历介绍

前序遍历
前序遍历首先访问根节点,然后遍历左子树,最后遍历右子树。
root - left - right
请看下面的例子:
在这里插入图片描述
中序遍历
中序遍历是先遍历左子树,然后访问根节点,然后遍历右子树。
left - root - right

通常来说,对于二叉搜索树,我们可以通过中序遍历得到一个递增的有序序列
我们将在另一张卡片(数据结构介绍 – 二叉搜索树)中再次提及。

让我们一起来看树的中序遍历:
在这里插入图片描述
后序遍历
后序遍历是先遍历左子树,然后遍历右子树,最后访问树的根节点。
left - right - root
我们一起来看后序遍历的动画演示:
在这里插入图片描述

值得注意的是,当你删除树中的节点时,删除过程将按照后序遍历的顺序进行。
也就是说,当你删除一个节点时,你将首先删除它的左节点和它的右边的节点,然后再删除节点本身。

在这里插入图片描述
您可以使用中序遍历轻松找出原始表达式。
但是程序处理这个表达式时并不容易,因为你必须检查操作的优先级。

如果你想对这棵树进行后序遍历,使用栈来处理表达式会变得更加容易。
每遇到一个操作符,就可以从栈中弹出栈顶的两个元素,计算并将结果返回到栈中。

递归和迭代
请练习文章后面习题中的三种遍历方法。
您可以通过递归或迭代方法实现算法,并比较它们之间的差异。

二叉树的前序遍历

递归算法 & 迭代算法

递归算法
使用前序遍历的特性:root-left-right
时间复杂度:O(n),其中 n是二叉树的节点数。每一个节点恰好被遍历一次。
空间复杂度:O(n),为迭代过程中显式栈的开销,平均情况下为 O(log n),最坏情况下树呈现链状,为 O(n)

迭代算法
使用stack来实现前序遍历
先将左节点加入list并push到栈
右节点,通过pop出,已经被add过的根节点的方式来获得。
时间复杂度:O(n),其中 n是二叉树的节点数。每一个节点恰好被遍历一次。
空间复杂度:O(n),为迭代过程中显式栈的开销,平均情况下为 O(log n),最坏情况下树呈现链状,为 O(n)

//树节点的基本数据结构
public class TreeNode
{
    public int val;
    public TreeNode left;
    public TreeNode right;
    public TreeNode(int val = 0, TreeNode left = null, TreeNode right = null)
    {
        this.val = val;
        this.left = left;
        this.right = right;
    }
}
//144.二叉树前序遍历:递归方式
public IList<int> PreorderTraversal_1(TreeNode root)
{
    IList<int> list = new List<int>();
    if (root == null) return list;
    Preorder(root,list);
    return list;
}
//递归方法
private void Preorder(TreeNode root, IList<int> list)
{
    list.Add(root.val);
    if (root.left != null) Preorder(root.left,list);
    if (root.right != null) Preorder(root.right,list);
}

//144.二叉树前序遍历:迭代方式
public IList<int> PreorderTraversal_2(TreeNode root)
{
    IList<int> list = new List<int>();
    if (root == null) return list;
    Stack<TreeNode> stack = new Stack<TreeNode>();
    TreeNode curNode = root; //当前节点
    while (stack.Count!=0 || curNode !=null) 
    {
        while (curNode!=null)
        {
            list.Add(curNode.val);
            stack.Push(curNode); //压入栈
            curNode = curNode.left;
        }
        //当前为左节点为空节点
        curNode = stack.Pop(); //把上一个根节点压出栈
        curNode = curNode.right; //获取右节点
    }
    return list;
}

二叉树的中序遍历

递归算法 & 迭代算法

递归算法
使用中序遍历的特性:left-root-right
时间复杂度:O(n),其中 n是二叉树的节点数。每一个节点恰好被遍历一次。
空间复杂度:O(n),为迭代过程中显式栈的开销,平均情况下为 O(log n),最坏情况下树呈现链状,为 O(n)

迭代算法
使用stack来实现中序遍历
现将左节点PUSH到栈里,获取下一个左节点
如果左节点为空,就将栈里POP出,赋值给list,然后获取到Right右节点,一次迭代
时间复杂度:O(n),其中 n是二叉树的节点数。每一个节点恰好被遍历一次。
空间复杂度:O(n),空间复杂度取决于栈深度,而栈深度在二叉树为一条链的情况下会达到 O(n) 的级别。

//94. 二叉树的中序遍历:递归算法
public IList<int> InorderTraversal_1(TreeNode root)
{
    IList<int> list = new List<int>();
    Inorder(root, list);
    return list;
}

private void Inorder(TreeNode root, IList<int> list)
{
    if (root == null) return; //空节点直接返回
    Inorder(root.left, list);
    list.Add(root.val);
    Inorder(root.right, list);
}

//94. 二叉树的中序遍历:迭代算法
public IList<int> InorderTraversal_2(TreeNode root)
{
    IList<int> list = new List<int>();
    Stack<TreeNode> stack = new Stack<TreeNode>();
    TreeNode curNode = root;
    while (stack.Any() || curNode != null)
    {
        while (curNode != null)
        {
            stack.Push(curNode); //压入栈
            curNode = curNode.left; //左节点
        }
        root = stack.Pop();
        list.Add(root.val); //出栈第一个
        curNode = root.right; //右节点
    }
    return list;
}

二叉树的后序遍历

递归算法 & 迭代算法

时间复杂度空间复杂度都是O(n)

 //145. 二叉树的后序遍历 :递归
public IList<int> PostorderTraversal_1(TreeNode root)
{
    IList<int> list = new List<int>();
    Postorder(root, list);
    return list;
}

private void Postorder(TreeNode root, IList<int> list)
{
    if (root == null) return; //空节点直接返回
    Postorder(root.left, list);
    Postorder(root.right, list);
    list.Add(root.val);
}

//145. 二叉树的后序遍历 :迭代
public IList<int> PostorderTraversal_2(TreeNode root)
{
    IList<int> list = new List<int>();
    Stack<TreeNode> stack = new Stack<TreeNode>();
    TreeNode curNode = root;
    TreeNode preNode = null; //代表此刻左节点
    while (stack.Any() || curNode != null)
    {
        while (curNode != null)
        {
            stack.Push(curNode);
            curNode = curNode.left; //判断还有没有左节点
        }
        curNode = stack.Pop(); //此节点没有左节点或者节点置空了,压出上一个赋值
        if (curNode.right == null || curNode.right == preNode) //当前没有右节点,右节点为null
        {
            list.Add(curNode.val); //把节点添加list
            preNode = curNode; //把当前节点给preNode
            curNode = null; //当前根节点置空
        }
        else //当前有右节点,不为空,更节点再次入栈
        {
            stack.Push(curNode); //重新入栈
            curNode = curNode.right;
        }
    }
    return list;
}

总结

二叉树前中后序的递归版本属于easy题,而迭代版本通常是medium甚至是hard。

在做迭代版本之前,我建议大家先问问各类“遍历”算法的本质是什么?是最后输出的那一串有序的数字吗?数字的顺序是对的,遍历算法就是对的吗?

个人认为,以上问题的答案都应该是:否。“遍历”的本质是对内存的有序访问,失去了访问顺序,即便你用各种数据结构恢复了这个次序,遍历本身也显得毫无意义。常见的后序遍历写法中有一种已经出现在评论区了——它的思想也很简单,大家做过单词串翻转吗?

String in = “it is a word”
String out = “word a is it”

这个问题有一种很优雅的写法是先reverse(in),然后再逐词翻转——“两次反转”,最后得到的就是正确顺序。

回到二叉树后序遍历,你也可以利用这种思想,利用双向链表的addFirst,对外部次序和内含次序进行同时翻转,同样会得到一种非常”优雅”的解法,结构简单明晰,甚至还有点好背(狗头)。但是,它并没有真正实现“遍历”——仔细看会发现,该算法其实在使用一种异构的前序遍历:“中->右->左”,而非传统意义上的“中->左->右”,而这种异构也正是他的第一次反转。而第二次反转就在输出序列上。

所以本质上,这是一个“前序遍历”,而不是所谓的“后序遍历”。只有当你的各个节点以“左->右->中”的次序依次出现在迭代的loop当中时,它才是真正的后序遍历,就像官解那样。贴个我个人的版本。

class Solution {
    public List<Integer> postorderTraversal(TreeNode root) {
        List<Integer> ans = new ArrayList<>();
        Stack<TreeNode> s = new Stack<>();
        Set<TreeNode> seen = new HashSet<>();
        while (root != null || !s.isEmpty()) {
            if (root == null && seen.contains(s.peek())) {
                ans.add(s.pop().val);
            } else if (root == null) {
                seen.add(s.peek());
                root = s.peek().right;
            } else {
                s.push(root);
                root = root.left;
            }
        }
        return ans;
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值