LeetCode之路:104. Maximum Depth of Binary Tree

来源:互联网 发布:2000年流行网络歌曲 编辑:程序博客网 时间:2024/05/16 07:29

一、引言

废话不多说,又是下一道题的战斗了:

Given a binary tree, find its maximum depth.

The maximum depth is the number of nodes along the longest path from the root node down to the farthest leaf node.

其实这道题,光看到名字就知道要考察什么了。Depth of Binary Tree

其实本人对于树的相关概念忘得也是差不多了,这里相当于从零开始分析,如何去求指定二叉树的高度。

但是问题首先,我们先来看看,LeetCode 是如何声明并且制作一个二叉树的。

二、LeetCode 二叉树的制作

哈哈,这又是一个与题意无关的话题了,但是谁叫 LeetCode 做的那么好呢:

LeetCode 二叉树展示效果

做题细心的同学,也许会发现这里,当我们输入以下的二叉树结点数据时,把鼠标移到输入的数组之间,就会自动浮现这传数据所代码的二叉树展示图:

二叉树数据

那么,LeetCode 是如何将这串数据识别为二叉树的呢?

先来看看 LeetCode 对于二叉树结点的定义:

/** * Definition for a binary tree node. * struct TreeNode { *     int val; *     TreeNode *left; *     TreeNode *right; *     TreeNode(int x) : val(x), left(NULL), right(NULL) {} * }; */

TreeNode 是一个结点类,其中 val 是结点的值,也就是我们输入的二叉树数据数组的内容; leftright 都是结点指向左子结点和右子结点的指针;最下面一行可能有些同学不理解,这里是 TreeNode 的构造函数(struct 与 class 的异同自己百度自己总结)。

看到这里我们就明白了,我们输入了一个代表着各个结点值的数组,如果为 null ,则代表当前结点不存在;而这个数组的元素,与二叉树从上到下,从左到右的顺序一一对应:

二叉树数组与图的对应关系

这里,我们就可以一目了然的得知这个二叉树的数据与图的关系了。

那么,问题来了,我们即使写好了这么一个获取到二叉树的高度的函数,该如何测试它呢?

换句话说,我们需要自己实现一个制作二叉树的类,让它得到了类似

{ “0”, “0”, “0”, “0”, “null”, “null”, “0”, “null”, “null”, “null”, “0” }

这样的一个数组,能够转化为我们需要的以 TreeNode 为结点的二叉树的存储结构。

那么,让我们开始吧!

  1. 我们首先将获取到的上述的数据,转换为能够轻松识别的整型数组,比如将 null 转化为 -1之类的比较方便比较的数值

  2. 我们动态创建一个指定大小(跟参数数据数组一样大小)的TreeNode 指针的数组,并且分配空间

  3. 我们遍历这个动态创建的数组,根据根结点到孩子结点的逻辑思路,将参数数据数组的值 set 进去

思路非常简单,这里请参看我写的代码:

// make Treeclass BinaryTree {public:    TreeNode* makeBinaryTree(vector<string> nodelist) {        // 将形如 [1,null,2] 的字符串数组        // 转化为 [1, -1, 2] 的整型数组        // 以 -1 标记当前节点为空值        vector<int> nodeData;        for (auto str : nodelist) {            if (str == "null") {                nodeData.push_back(-1);            } else {                int node = stoi(str);                nodeData.push_back(node);            }        }        // 制造一个满足上述条件的树        TreeNode *nodeHead = new TreeNode[nodeData.size()]();        vector<TreeNode*> tree;        TreeNode *q = nodeHead + nodeData.size();        for (TreeNode *p = nodeHead; p < nodeHead + nodeData.size(); ++p) {            if (nodeData[p - nodeHead] == -1) {                p->left = nullptr;                p->right = nullptr;                p->val = 0;            } else {                // 左子结点为当前结点编号的 2 倍                if (2 * (p - nodeHead + 1) <= nodeData.size()) {                    p->left = nodeHead + 2 * (p - nodeHead + 1) - 1;                } else {                    p->left = nullptr;                }                // 右子结点为当前结点编号的 2 倍加 1                if (2 * (p - nodeHead + 1) + 1 <= nodeData.size()) {                    p->right = nodeHead + 2 * (p - nodeHead + 1) - 1 + 1;                } else {                    p->right = nullptr;                }                p->val = nodeData[p - nodeHead];            }            tree.push_back(p);        }        // test make tree is right or not        vector<int> testTree;        for (TreeNode *p = nodeHead; p < nodeHead + nodeData.size(); ++p) {            testTree.push_back(p->val);            int leftVal = 0;            int rightVal = 0;            if (p->left != nullptr) {                leftVal = p->left->val;            }             if (p->right != nullptr) {                rightVal = p->right->val;            }            cout << "node " << p->val << " 's left child is " << leftVal <<                " , right child is " << rightVal << endl;        }        return nodeHead;    }};

这里有几个细节值得一谈:

std::stoi() 方法的使用,使 std::string 类型的值转换为 int 类型;

若令根结点值为 1,那么左子结点的值是父结点值的 2 倍,右子结点的值是父结点值的 2倍加 1 ,用此方式来设置子结点的值;

最后测试这个程序,测试输入数据:

{ “1”, “2”, “3”, “4”, “null”, “null”, “7”, “null”, “null”, “null”, “11” }

输出结果为:

测试制作数组类是否正确

看到这里,发现结果与 LeetCode 上的二叉树展示图一致,完成了二叉树高度函数的测试环境代码。

三、递归:编程方法中的印象画派

其实我们之前花了那么久的时间来完成制作二叉树的任务是非常值得的,有助于我们很快编写出二叉树的高度求法,因为这里已经对于二叉树的结点的定义非常熟悉了。

这里,当我看到这道题的时候,我脑海里第一个想到的就是 :

递归

对,就是递归。

那么什么是递归?一个自身调用自身的函数?一个总让人觉得陌生的函数?

其实递归并不像我们印象中那么可怕,递归函数在一方面来说,其实有助于我们更加清晰宏观的了解整个函数的运行逻辑:

如果把编程比作画画,那么递归函数就是当之无愧的印象画派;

我们凑近了去看,往往被它粗糙的用墨放荡的线条所迷乱;

而当我们放弃对其上下求索,端一杯咖啡悠闲品味的时候,却在不经意间看到了它给我们勾勒出的那一幅绚烂的意境;

这就是我所理解的递归。

那么,一个递归函数需要有这两个特征:

  1. 必须不停地接近解

  2. 必须有结束条件

这里,让我们分析这个情景:

我们拿到了一个二叉树的根结点,我们找到它的左子结点和右子结点;

我们拿到了左子结点,我们找到它的左子结点和右子结点;

我们不停的更替当前结点,我们不停的比较当前结点是否为空,然后我们返回上一个当前结点,我们继续继续又继续着…

What a mess!

我不会写代码了!怎么这么乱啊~~~

别急,写递归是做一幅印象画,让我们隔远了看:

我们拿到了一个结点;

我们查看它是否为空,为空则计算当前记录中的高度值;
如果不为空,我们拿这个结点的左子结点当作当前结点;
我们继续拿这个结点的右子结点当作当前结点;

于是,我们不停的在第一步和第二步中循环,代码已出:

class Solution {public:    int maxDepth(TreeNode* root) {        static int max;        static stack<int> farthest_path;        if (root != nullptr) {            farthest_path.push(root->val);        } else {            max = max > farthest_path.size() ? max : farthest_path.size();            return max;        }        maxDepth(root->left);        maxDepth(root->right);        farthest_path.pop();        return max;    }};

值得注意的是,这里声明了两个静态变量用于存储当前的状态:

其中 max 用来存储当前的高度值;

farthest_path 用来存储当前的遍历路径。

至此,问题解决!

四、等等…提交 LeetCode 未通过!

让我们拿着上面的代码,复制粘贴,点击 submit solution,我们期待着 Accept,期待着点开 more detail 查看结果信息:

wrong answer

居然 Wrong Answer 了!

不可能啊,怎么会有错误呢?让我们看看这个错误的 case :

error case

看上去好像也没有什么特别的,拷贝到本机 IDE 上仔细调试,好像也没有问题。

难道是 static 变量的问题?

让我们尝试下将 static 变量替换为类的私有变量试试:

// my simlified solution , move out static variable// runtime = 6 msclass Solution {public:    int maxDepth(TreeNode* root) {        if (root != nullptr) {            farthest_path.push(root->val);        } else {            max = max > farthest_path.size() ? max : farthest_path.size();            return max;        }        maxDepth(root->left);        maxDepth(root->right);        farthest_path.pop();        return max;    }private:    int max;    stack<int> farthest_path;};

Accept!

我们不禁要问,为什么呢?

原因这里我也没有研究出来,猜测与 OJ 测试中,使用的是多条 case ,使用 static 变量导致了一些全局区域的问题,这里我们不细细钻研,毕竟与题目无关了。

吃一堑长一智,以后在 LeetCode 上做题,尽量避免 static 变量。

五、记得要看看最高票代码哦

程序员永远不能失去对于代码整洁性和简洁性的极致追求:

class Solution {public:    int maxDepth(TreeNode *root) {        return root == NULL ? 0 : max(maxDepth(root->left), maxDepth(root->right)) + 1;    }};

这是一行充满了魔力的代码,让我看了之后不由得震撼:

这才是一幅完美的印象画!

max 函数检查当前结点的左子结点和右子结点中最高的一个返回,然后依次递归,实在是:

perfect

这里,这段代码的作者还提供了一段广度优先遍历的代码,使用到了std::queue,思路比较新颖,虽略显复杂,但是 runtime 与上个代码一样:

class Solution {public:    int maxDepth(TreeNode *root)    {        if (root == NULL)            return 0;        int res = 0;        queue<TreeNode *> q;        q.push(root);        while (!q.empty())        {            ++res;            for (int i = 0, n = q.size(); i < n; ++i)            {                TreeNode *p = q.front();                q.pop();                if (p->left != NULL)                    q.push(p->left);                if (p->right != NULL)                    q.push(p->right);            }        }        return res;    }};

其实不管如何,都是需要不停的接近我们的解,然后限定条件返回。

六、总结

一道小小的题,依然还是做了那么久:

看到了 LeetCode 实现那么完美的二叉树制作,于是也手动完成了一个;

看到了 static 变量导致的 case 过不了的问题,思考了很久,还是决定有待研究;

看到了最高票答案的一行代码,深深被震撼之余,更加坚定了对于代码整洁性和简洁性的追求;

依然还在路上,I love this world!

0 0