(剑指OFFER面试题34)二叉树中和为某一值的路径

题目描述

输入一颗二叉树和一个整数,打印出二叉树中结点值的和为输入整数的所有路径。路径定义为从树的根结点开始往下一直到叶结点所经过的结点形成一条路径。
例如下图,输入的二叉树和整数22,则打印出两条路径,第一条路径包含节点10、12;第二条路径包含节点10、5和7 。
二叉树
一般的数据结构和算法的教材都很少介绍树的路径,因此,理解有点麻烦,大家可以想到哈弗曼树的权值,有点类似。
以上图的树来进行分析。由于路径是从根节点出发到叶节点,也就是说路径总是以根节点为起始点,因此我们首先需要遍历根节点。在树的前序、中序、后序3中遍历方式中,只有前序遍历是首先访问根节点的。
按照前序遍历的顺序遍历上图的树可以看出,在本体的二叉树节点中没有指向父节点的指针,当访问节点5的时候,我们是不知道前面经过了哪些节点的,除非我们把经过的路径上的节点全部记录下来。每访问一个节点,我们都把当前节点添加到路径中去。当到达节点5时,路径中包含两个节点,他们的值分别是10和5 。接下来遍历到节点4,我们把这个节点也添加到路径中。这个时候已经到达叶节点,但是路径上3个节点的值之和是19,这个和不等于输入的值22,因此不是符合要求的路径。
我们接着遍历其他的节点。在遍历下一个节点之前,先要从节点4回到节点5,再去遍历节点5的右子节点7。值得注意的是,当回到节点5的时候,由于节点4已经不再前往节点7的路径上了,所以我们需要把节点4从路径中删除。接下来访问节点7的时候,再把该节点添加到路径中。此时路径中的3个节点10、5、7之和刚刚好是22,是一条符合要求的路径。
最后我们遍历一下节点12,在遍历这个节点之前,需要先经过节点5回到节点10。同样,每次当从子节点回到父节点的时候,我们都需要在路径上删除子节点。最后在从节点10到节点12的时候,路径上的两个节点的值之和也是22,因此,这也是一条符合要求的路径。
上述过程可通过下列表格列出过程:
访问过程
分析完前面具体的例子之后,我们就能找到一些规律了:当用前序遍历的方式访问到某一节点时,我们把该节点添加到路径上,并累加该节点的值。如果该节点为叶节点,并且路径中节点值的和刚刚好等于输入的整数,则当前路径符合要求,然后打印该路径。如果当前节点不是叶节点,那么继续访问它的子节点。当前节点访问结束之后,递归函数将自动回到它的父节点。因此,我们在函数退出之前需要在路径上删除当前节点并减去当前节点的值,以确保返回父节点时路径刚好是从根节点到父节点。
根据上述分析,可以得出储存容器需要用到数据结构的栈,对于递归的调用就是一个压栈和出栈的过程。

还有一个需要注意到的问题是,有人会想:当我在没有遍历到叶节点的时候路径的和值就和期待值一样了,那我是不是可以就直接返回到上一层进行下一个遍历了,答案是否定的,因为题目并没有说明节点值的范围,那么有可能会存在负数,所以我们必须遍历完直到叶节点,然后判断是否满足条件。

以下为参考代码,有递归和非递归两个方法:

using System;
using System.Collections.Generic;

namespace 二叉树中和为某一值的路径
{
    public class TreeNode
    {
        public int val;
        public TreeNode left;
        public TreeNode right;
        public TreeNode(int x)
        {
            val = x;
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            TreeNode root = null;
            root = CreateTree(root);

            Solution s = new Solution();
            List<List<int>> list= s.FindPath2(root, 22);
            s.PrintPath(list);
        }

        private static TreeNode CreateTree(TreeNode root)
        {
            root = new TreeNode(10)
            {
                left = new TreeNode(5),
                right = new TreeNode(12)
            };
            root.left.left = new TreeNode(4);
            root.left.right = new TreeNode(7);

            return root;
        }
    }

    class Solution
    {
        /// <summary>
        /// 用带记忆的DFS来解决
        /// 有人会在Solution里面设置两个集合,这样传参的时候就不需要传引用,而是直接调用全局变量就行了,这是空间换时间,在做题时,可以先问清楚需求,如果要求时间优先,那么就空间换时间,把集合定义成全局变量,如果要求空间优先,那么就时间换空间,在调用函数时传引用
        /// </summary>
        /// <param name="root">根节点</param>
        /// <param name="expectNumber">期待值</param>
        /// <returns>返回路径的集合</returns>
        public List<List<int>> FindPath1(TreeNode root, int expectNumber)
        {
            List<List<int>> list = new List<List<int>>();
            List<int> path = new List<int>();

            //如果根节点不为空,那么就遍历树的所有节点查找是否有符合要求的路径
            if (root != null)
                DFS(root,expectNumber,ref list,ref path);

            return list;
        }

        /// <summary>
        /// 深度遍历,查找是否有路径满足
        /// </summary>
        /// <param name="root">当前的节点</param>
        /// <param name="sum">与期待值的差</param>
        /// <param name="list">保存路径的集合</param>
        /// <param name="path">路径</param>
        private void DFS(TreeNode root,int sum,ref List<List<int>> list,ref List<int> path)
        {
            //把当前节点添加进集合
            path.Add(root.val);
            //如果当前节点为叶节点,且和与期待值相等,则把路径添加到路径集合中
            if (root.left == null && root.right == null)
            {
                //这里因为path的元素会随着递归的改变而发生变化,所以在储存到路径集合list里时需要开辟新的空间储存路径值再保存
                if (sum - root.val == 0)
                    list.Add(new List<int>(path));
                    //list.Add(path);       这样是不可行的,在退出FindPath函数后,两个集合都会为空,在主函数中将无法调用
                path.Remove(root.val);
                return;
            }
            //如果当前节点有子节点,那么递归进入子节点,传入的期待值必须减去当前节点值
            if (root.left != null)
                DFS(root.left, sum - root.val, ref list, ref path);
            if (root.right != null)
                DFS(root.right, sum - root.val, ref list, ref path);
            //当遍历完所有子节点回到上一级函数的时候,需要把当前的节点移出集合
            path.Remove(root.val);
        }

        //非递归实现的路径查找
        /// <summary>
        /// 来源:牛客网
        /// 思路:
        /// 1.按先序遍历把当前节点cur的左孩子依次入栈同时保存当前节点,每次更新当前路径的和sum;
        /// 2.判断当前节点是否是叶子节点以及sum是否等于expectNumber,如果是,把当前路径放入路径集合中;
        /// 3.遇到叶子节点cur更新为NULL,此时看栈顶元素,如果把栈顶元素保存在lastNode变量中,同时弹出栈顶元素,当前路径中栈顶元素弹出,sum减掉栈顶元素;
        /// 4.如果步骤3中的栈顶元素的右孩子存在且右孩子之前没有遍历过,当前节点cur更新为栈顶的右孩子,此时改变cur=NULL的情况。
        /// </summary>
        /// <param name="root"></param>
        /// <param name="expectNumber"></param>
        /// <returns></returns>
        public List<List<int>> FindPath2(TreeNode root,int expectNumber)
        {
            List<List<int>> list = new List<List<int>>();
            if (root == null)
                return list;

            Stack<TreeNode> s = new Stack<TreeNode>();
            s.Push(root);                                   //把根节点压入栈中

            List<int> curpath = new List<int>();            //当前路径
            int sum = 0;                                    //当前路径和
            TreeNode curNode = root;                        //当前节点
            TreeNode lastNode = null;                       //上一个节点

            while (s.Count > 0)
            {
                if (curNode == null)
                {
                    TreeNode pNode = s.Peek();
                    if (pNode.right != null && lastNode != pNode.right)
                    {
                        curNode = pNode.right;
                    }
                    else
                    {
                        lastNode = pNode;
                        s.Pop();
                        curpath.RemoveAt(curpath.Count - 1);                //移除最后一位
                        sum -= pNode.val;
                    }
                }
                else
                {
                    if (curNode != s.Peek())
                        s.Push(curNode);
                    sum += curNode.val;
                    curpath.Add(curNode.val);
                    if (curNode.left == null && curNode.right == null && sum == expectNumber)
                    {
                        list.Add(new List<int>(curpath));
                    }
                    curNode = curNode.left;
                }
            }
            return list;
        }

        /// <summary>
        /// 打印所有的路径
        /// </summary>
        /// <param name="list">路径集合</param>
        public void PrintPath(List<List<int>> list)
        {
            foreach(List<int> item1 in list)
            {
                foreach(int item2 in item1)
                {
                    Console.Write(item2 + "\t");
                }
                Console.WriteLine();
            }
            Console.WriteLine();
        }
    }
}

猜你喜欢

转载自blog.csdn.net/qq_33575542/article/details/80786926