知识点速查

中序与前序或后序联合构造二叉树

这算是经典题目了吧,本科算法课上学到树这一节时的必做题,题目本身没什么难度,但这里比较有意思的是考虑代码的简洁与优雅程度。

首先,来讲一下算法思路。很简单,把树的构造看作是一个递归的过程。对于中序与前序,我们都知道中序序列是先遍历左子树,然后遍历根结点,再遍历右子树。而前序序列是先遍历根结点,然后遍历左子树,再遍历右子树。

那么怎么把它转化成一个递归问题呢? 我们知道左子树也是通过左子树的中序序列和它的前序序列构造,右子树同理,那么只要在输入的中序序列中找到左子树的中序序列、在输入的前序序列中找到左子树的前序序列,那么就可以转化成与原问题相同的问题。而这一点是可以轻松做到的,通过前序的遍历顺序可以知道,它第一个遍历的一定是根结点,我们直接拿着这个根结点去中序序列中查找,找到根结点的位置,这样中序序列就被一分为二了,前半部是左子树的中序序列,后半部分是右子树的中序序列。同时,我们知道了左子树的大小(即其中结点的数量)。又因为前序序列是按“中左右”的顺序遍历,跳过“中”之前理所当然是“左”了,于是左子树的前序序列就是当前位置前进一位(跳过根结点)的位置开始,一直数左子树大小个元素。

这样,我们就有了左子树的中序序列和前序序列。同理,我们找到右子树的中序序列与前序序列。这时,问题就变成了非常简单的三步:

  1. 构造左子树与右子树
  2. 创建根结点
  3. 连接根结点与左子树和右子树

按这个思路,理想中的代码应该是:

1
2
3
4
5
6
7
8
TreeNode *build(inorder, preorder) {
    // trivial handlings...
    // ...
    root = new TreeNode(root_val);
    root->left = build(inorder_left_tree, preorder_left_tree);
    root->right = build(inorder_right_tree, preorder_right_tree);
    return root;
}

在 Python 中这种风格能轻易地通过切片做到,然而 C++ 中却不支持这种操作。从官方题解来看,为了顺利递归不得不再定义一个辅助函数,这个辅助函数有多至 6 个参数,包括中序序列、前序序列、中序序列的范围(两个参数)、前序序列的范围。这种写法看上去不够简洁,直观上能想到的改进方法是定义一个聚合类:

1
2
3
4
5
template<typename Iter>
struct OrderRange {
  Iter beg;
  Iter end;
};

然后辅助函数只用接收两个这样的 OrderRange 。但这样是行不通的,因为在递归的过程中总要对范围的有效性进行限制,比如在范围往前移动时,我们要限制它不能超过原来的序列末尾。这么来看,只有两个 OrderRange 是不够的,再增加参数个数明显违背了一开始追求简洁的初心。

我翻了翻 2020 年提交的代码,里面为了简洁程度居然牺牲了大量的空间和时间:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
class Solution {
public:
    TreeNode* buildTree(vector<int>& preorder, vector<int>& inorder) {
        if (preorder.empty()) return nullptr;
        int leftsize = ranges::find(inorder, preorder[0]) - inorder.begin();
        // 直接复制到一个新的 vector 里
        vector<int> preorder_left (preorder.begin()+1, preorder.begin()+1+leftsize);
        vector<int> preorder_right (preorder.begin()+1+leftsize, preorder.end());
        vector<int> inorder_left (inorder.begin(), inorder.begin()+leftsize);
        vector<int> inorder_right (inorder.begin()+leftsize+1, inorder.end());
        TreeNode* left = buildTree(preorder_left, inorder_left);
        TreeNode* right = buildTree(preorder_right, inorder_right);
        return new TreeNode(preorder[0], left, right);
    }
};

现在对 C++ 的认识进一步提高之后,我想出了一种还凑合的写法。我们都知道 C++ 有一个叫 Range 的第三方库,现在已经更新到了 range-v3 。 C++20里这个库已经被合并进了标准库 std 中。 Range 库在处理序列方面有强大的能力。如今的 range-v3 已经支持直接的切片操作(通过 ranges::slice_view 实现),但很遗憾标准库里并没有收录这部分的内容。于是,我转而使用 ranges::subrange 来实现。代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class Solution {
public:
    template<typename _It, typename _Se>
    TreeNode* build_recursive(std::ranges::subrange<_It, _Se> preorder, std::ranges::subrange<_It, _Se> inorder) {
        if (preorder.empty()) {
            return nullptr;
        }
        auto root_val = preorder.front(); // 根结点为前序序列的第一个元素
        auto it = std::ranges::find(inorder, root_val); // 在中序序列中找到根结点
        auto offset = std::ranges::distance(inorder.begin(), it);
        auto root = new TreeNode(root_val);
        // 构造左子树
        root->left = build_recursive(
            std::ranges::subrange{ std::ranges::next(preorder.begin(), 1, preorder.end()), std::ranges::next(preorder.begin(), 1 + offset, preorder.end()) },
            std::ranges::subrange{ inorder.begin(), std::ranges::next(inorder.begin(), offset, inorder.end()) });
        // 构造右子树
        root->right = build_recursive(
            std::ranges::subrange{ std::ranges::next(preorder.begin(), 1 + offset, preorder.end()), preorder.end() },
            std::ranges::subrange{ std::ranges::next(inorder.begin(), 1 + offset, inorder.end()), inorder.end() });
        return root;
    }
    TreeNode* buildTree(std::vector<int>& preorder, std::vector<int>& inorder) {
        return build_recursive(std::ranges::subrange(preorder), std::ranges::subrange(inorder));
    }
};

虽然也用到了辅助函数,但毕竟原函数里只用一行代码去调用辅助函数,也马马虎虎能称得上简洁。辅助函数是预想中的只接受两个序列的形式,内容也与上面描述的伪代码一致。

再来说说通过中序序列与后序序列构造二叉树。按照与上面差不多的思路,中序是“左中右”,后序是“左右中”,同样我们先找到“中”。可以知道根结点就是后序序列的最后一个元素,这时我们拿着最后一个元素去中序序列中找它的位置,找到后同样可以知道左子树的中序序列与右子树的中序序列。我们再用左子树中序序列的长度去切后序序列,得到左子树的后序序列与右子树的后序序列。这时,问题又转化成了它本身。这里不赘述,代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class Solution {
public:
    template<typename _It, typename _Se>
    TreeNode* build_recursive(std::ranges::subrange<_It, _Se> inorder, std::ranges::subrange<_It, _Se> postorder) {
        if (inorder.empty()) {
            return nullptr;
        }
        auto root_val = postorder.back(); // 根结节为后序序列的最后一个元素
        auto it = std::ranges::find(inorder, root_val); // 在中序序列中找到根结点
        auto offset = std::ranges::distance(inorder.begin(), it);
        TreeNode* root = new TreeNode(inorder[offset]);
        // 构造左子树
        root->left = build_recursive(
            std::ranges::subrange{ inorder.begin(), std::ranges::next(inorder.begin(), offset, inorder.end()) },
            std::ranges::subrange{ postorder.begin(), std::ranges::next(postorder.begin(), offset, postorder.end()) });
        // 构造右子树
        root->right = build_recursive(
            std::ranges::subrange{ std::ranges::next(inorder.begin(), offset + 1, inorder.end()), inorder.end() },
            std::ranges::subrange{ std::ranges::next(postorder.begin(), offset, postorder.end()), std::ranges::prev(postorder.end(), 1, postorder.begin()) });
        return root;
    }
    TreeNode* buildTree(std::vector<int>& inorder, std::vector<int>& postorder) {
        return build_recursive(std::ranges::subrange(inorder), std::ranges::subrange(postorder));
    }
};

提交结果:

知识点归纳

std::ranges::subrange 是一个子序列的类,它可以接受一个 range 这时子序列就是给定的序列全部,也可以接受两个迭代器,分别指向子序列的头部和子序列的 尾部下一个位置 (通常叫 setinel )。它的模板参数 _It 要 model 概念 std::input_or_output_iterator 这个概念规定了一个迭代器要满足的条件。而模板参数 _Se 要 model 概念 std::sentinel_for ,它约束了 sentinel 与迭代器类型之间的关系。

std::ranges::next 与 std::ranges::prev 可以帮助我们安全地移动迭代器,前提是要用它三个参数的重载形式,第一个参数给定要移动的迭代器,第二个参数给定移动的距离,第三个参数给定移动的边界。

参考列表