作为(单)链表的升级版,我们通常接触的树都是二叉树(binary tree),即每个节点最多有 两个子节点;且除非题目说明,默认树中不存在循环结构。LeetCode 默认的树表示方法如下。
struct TreeNode {int val;TreeNode *left;TreeNode *right;TreeNode(int x) : val(x), left(NULL), right(NULL) {}
};
可以看出,其与链表的主要差别就是多了一个子节点的指针。
对于一些简单的递归题,某些 LeetCode 达人喜欢写 one-line code,即用一行代码解决问题, 把 if-else 判断语句压缩成问号冒号的形式。我们也会展示一些这样的代码,但是对于新手,笔者仍然建议您使用 if-else 判断语句。
在很多时候,树递归的写法与深度优先搜索的递归写法相同,因此本书不会区分二者。
104. 二叉树的最大深度
104. Maximum Depth of Binary Tree
给定一个二叉树,找出其最大深度。
二叉树的深度为根节点到最远叶子节点的最长路径上的节点数。
说明: 叶子节点是指没有子节点的节点。
/*** Definition for a binary tree node.* struct TreeNode {* int val;* TreeNode *left;* TreeNode *right;* TreeNode() : val(0), left(nullptr), right(nullptr) {}* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}* };*/
class Solution {
public:int maxDepth(TreeNode* root) {return root? 1 + max(maxDepth(root->left), maxDepth(root->right)): 0;}
};
110. 平衡二叉树
110. Balanced Binary Tree
给定一个二叉树,判断它是否是高度平衡的二叉树。
本题中,一棵高度平衡二叉树定义为:一个二叉树每个节点 的左右两个子树的高度差的绝对值不超过 1 。
解法类似于求树的最大深度,但有两个不同的地方:一是我们需要先处理子树的深度再进行 比较,二是如果我们在处理子树时发现其已经不平衡了,则可以返回一个-1,使得所有其长辈节 点可以避免多余的判断(本题的判断比较简单,做差后取绝对值即可;但如果此处是一个开销较 大的比较过程,则避免重复判断可以节省大量的计算时间)。
/*** Definition for a binary tree node.* struct TreeNode {* int val;* TreeNode *left;* TreeNode *right;* TreeNode() : val(0), left(nullptr), right(nullptr) {}* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}* };*/
class Solution {
public:bool isBalanced(TreeNode* root) {return helper(root) != -1;}int helper(TreeNode* root){if(!root){return 0;}int left = helper(root->left), right = helper(root->right);if(left == -1 || right == -1 || abs(left - right) > 1){return -1;}return 1 + max(left, right);}
};
543. 二叉树的直径
543. Diameter of Binary Tree
给定一棵二叉树,你需要计算它的直径长度。一棵二叉树的直径长度是任意两个结点路径长度中的最大值。这条路径可能穿过也可能不穿过根结点。
注意:两结点之间的路径长度是以它们之间边的数目表示。
同样的,我们可以利用递归来处理树。解题时要注意,在我们处理某个子树时,我们更新的 最长直径值和递归返回的值是不同的。这是因为待更新的最长直径值是经过该子树根节点的最长直径(即两侧长度);而函数返回值是以该子树根节点为端点的最长直径值(即一侧长度),使用这样的返回值才可以通过递归更新父节点的最长直径值)。
/*** Definition for a binary tree node.* struct TreeNode {* int val;* TreeNode *left;* TreeNode *right;* TreeNode() : val(0), left(nullptr), right(nullptr) {}* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}* };*/
class Solution {
public:// 主函数int diameterOfBinaryTree(TreeNode* root) {int diameter = 0;depth(root, diameter);return diameter;}// 辅函数 - 深度递归int depth(TreeNode* node, int& diameter){if(!node) return 0;int l = depth(node->left, diameter), r = depth(node->right, diameter);diameter = max(l + r, diameter); // 更新直径长度return 1 + max(l, r); // 返回深度}
};
437. 路径总和 III
437. Path Sum III
给定一个二叉树的根节点 root ,和一个整数 targetSum ,求该二叉树里节点值之和等于 targetSum 的 路径 的数目。
路径 不需要从根节点开始,也不需要在叶子节点结束,但是路径方向必须是向下的(只能从父节点到子节点)。
递归每个节点时,需要分情况考虑:(1)如果选取该节点加入路径,则之后必须继续加入连 续节点,或停止加入节点(2)如果不选取该节点加入路径,则对其左右节点进行重新进行考虑。 因此一个方便的方法是我们创建一个辅函数,专门用来计算连续加入节点的路径。
/*** Definition for a binary tree node.* struct TreeNode {* int val;* TreeNode *left;* TreeNode *right;* TreeNode() : val(0), left(nullptr), right(nullptr) {}* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}* };*/
class Solution {
public:// 主函数int pathSum(TreeNode* root, int targetSum) {if(!root) return 0;return pathSumStartWithRoot(root, targetSum) + pathSum(root->left, targetSum) + pathSum(root->right, targetSum);}// 辅函数 - 对当前节点连续加入节点,判断是否满足long long pathSumStartWithRoot(TreeNode* root, long long targetSum){if(!root) return 0;long long count = root->val == targetSum? 1: 0;count += pathSumStartWithRoot(root->left, targetSum - root->val);count += pathSumStartWithRoot(root->right, targetSum - root->val);return count;}
};
101. 对称二叉树
101. Symmetric Tree
给你一个二叉树的根节点 root
, 检查它是否轴对称。
判断一个树是否对称等价于判断左右子树是否对称。笔者一般习惯将判断两个子树是否相等 或对称类型的题的解法叫做“四步法”:(1)如果两个子树都为空指针,则它们相等或对称(2) 如果两个子树只有一个为空指针,则它们不相等或不对称(3)如果两个子树根节点的值不相等, 则它们不相等或不对称(4)根据相等或对称要求,进行递归处理。
/*** Definition for a binary tree node.* struct TreeNode {* int val;* TreeNode *left;* TreeNode *right;* TreeNode() : val(0), left(nullptr), right(nullptr) {}* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}* };*/
class Solution {
public:bool isSymmetric(TreeNode* root) {if(!root) return true;return helper(root->left, root->right);}bool helper(TreeNode* left, TreeNode* right){if(!left && !right) return true;if(!left || !right) return false;if(left->val != right->val) return false;return helper(left->left, right->right) && helper(left->right, right->left);}
};
1110. 删点成林
1110. Delete Nodes And Return Forest
给出二叉树的根节点 root,树上每个节点都有一个不同的值。
如果节点值在 to_delete 中出现,我们就把该节点从树上删去,最后得到一个森林(一些不相交的树构成的集合)。
返回森林中的每棵树。你可以按任意顺序组织答案。
这道题最主要需要注意的细节是如果通过递归处理原树,以及需要在什么时候断开指针。同 时,为了便于寻找待删除节点,可以建立一个哈希表方便查找。
/*** Definition for a binary tree node.* struct TreeNode {* int val;* TreeNode *left;* TreeNode *right;* TreeNode() : val(0), left(nullptr), right(nullptr) {}* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}* };*/
class Solution {
public:vector delNodes(TreeNode* root, vector& to_delete) {vectorforest;unordered_set dict(to_delete.begin(), to_delete.end());root = helper(root, dict, forest);// 自下而上最顶点的节点是处理不到,所以递归完后需要对最顶点的节点做处理 if(root){forest.push_back(root);}return forest;}TreeNode* helper(TreeNode* root, unordered_set &dict, vector &forest){if(!root) return root;// 先进行递归操作,目的是自下而上对树进行操作,即树的后序遍历root->left = helper(root->left, dict, forest);root->right = helper(root->right, dict, forest);// 如果存在一个节点的val值存在于to_delete数组中if(dict.count(root->val)){// 把当前的左右子树压入forest数组中if(root->left){forest.push_back(root->left);}if(root->right){forest.push_back(root->right);}root = nullptr; // 删除当前节点}return root;}};
我们可以使用广度优先搜索进行层次遍历。注意,不需要使用两个队列来分别存储当前层的 节点和下一层的节点,因为在开始遍历一层的节点时,当前队列中的节点数就是当前层的节点 数,只要控制遍历这么多节点数,就能保证这次遍历的都是当前层的节点。
637. 二叉树的层平均值
637. Average of Levels in Binary Tree
给定一个非空二叉树的根节点 root
, 以数组的形式返回每一层节点的平均值。与实际答案相差 10^-5
以内的答案可以被接受。
利用广度优先搜索,我们可以很方便地求取每层的平均值。
/*** Definition for a binary tree node.* struct TreeNode {* int val;* TreeNode *left;* TreeNode *right;* TreeNode() : val(0), left(nullptr), right(nullptr) {}* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}* };*/
class Solution {
public:vector averageOfLevels(TreeNode* root) {vector ans;if(!root) return ans;queue q;q.push(root);while(!q.empty()){int count = q.size();double sum = 0;for(int i=0; ival;if(node->left){q.push(node->left);}if(node->right){q.push(node->right);}}ans.push_back(sum / count);}return ans;}
};
前序遍历、中序遍历和后序遍历是三种利用深度优先搜索遍历二叉树的方式。它们是在对节 点访问的顺序有一点不同,其它完全相同。考虑如下一棵树:
前序遍历先遍历父结点,再遍历左结点,最后遍历右节点,我们得到的遍历顺序是 [1 2 4 5 3 6]。
void preorder(TreeNode *root){visit(root);preorder(root->left);preorder(root->right);
}
中序遍历先遍历左节点,再遍历父结点,最后遍历右节点,我们得到的遍历顺序是 [4 2 5 1 3 6]。
void inorder(TreeNode *root){inorder(root->left);visit(root);inorder(root->right);
}
后序遍历先遍历左节点,再遍历右结点,最后遍历父节点,我们得到的遍历顺序是 [4 5 2 6 3 1]。
void postorder(TreeNode *root){postorder(root->left);postorder(root->right); visit(root);
}
105. 从前序与中序遍历序列构造二叉树
105. Construct Binary Tree from Preorder and Inorder Traversal
给定两个整数数组 preorder 和 inorder ,其中 preorder 是二叉树的先序遍历, inorder 是同一棵树的中序遍历,请构造二叉树并返回其根节点。
对于任意一颗树而言,前序遍历的形式总是:
[ 根节点, [左子树的前序遍历结果], [右子树的前序遍历结果] ]
即根节点总是前序遍历中的第一个节点。而中序遍历的形式总是:
[ [左子树的中序遍历结果], 根节点, [右子树的中序遍历结果] ]
我们通过本题的样例讲解一下本题的思路。前序遍历的第一个节点是 4,意味着 4 是根节点。 我们在中序遍历结果里找到 4 这个节点,根据中序遍历的性质可以得出,4 在中序遍历数组位置 的左子数组为左子树,节点数为 1,对应的是前序排列数组里 4 之后的 1 个数字(9);4 在中序 遍历数组位置的右子数组为右子树,节点数为 3,对应的是前序排列数组里最后的 3 个数字。有了这些信息,我们就可以对左子树和右子树进行递归复原了。为了方便查找数字的位置,我们可以用哈希表预处理中序遍历的结果。
/*** Definition for a binary tree node.* struct TreeNode {* int val;* TreeNode *left;* TreeNode *right;* TreeNode() : val(0), left(nullptr), right(nullptr) {}* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}* };*/
class Solution {unordered_map index;
public:TreeNode* buildTree(vector& preorder, vector& inorder) {int n = preorder.size();// 构造哈希映射,帮助我们快速定位根节点for(int i=0; i& preorder, const vector& inorder, int preorder_left, int preorder_right, int inorder_left, int inorder_right){if(preorder_left > preorder_right){return nullptr;}// 前序遍历中的第一个节点就是根节点int preorder_root = preorder_left;// 在中序遍历中定位根节点int inorder_root = index[preorder[preorder_root]];// 先把根节点建立出来TreeNode* root = new TreeNode(preorder[preorder_root]);// 得到左子树中的节点数目int size_left_subtree = inorder_root - inorder_left;// 递归地构造左子树,并连接到根节点// 先序遍历中「从 左边界+1 开始的 size_left_subtree」个元素就对应了中序遍历中「从 左边界 开始到 根节点定位-1」的元素root->left = buildTreeHelper(preorder, inorder, preorder_left + 1, preorder_left + size_left_subtree, inorder_left, inorder_root - 1);// 递归地构造右子树,并连接到根节点// 先序遍历中「从 左边界+1+左子树节点数目 开始到 右边界」的元素就对应了中序遍历中「从 根节点定位+1 到 右边界」的元素root->right = buildTreeHelper(preorder, inorder, preorder_left + size_left_subtree + 1, preorder_right, inorder_root + 1, inorder_right);return root;}
};
144. 二叉树的前序遍历
144. Binary Tree Preorder Traversal
给你二叉树的根节点 root
,返回它节点值的 前序 遍历。
因为递归的本质是栈调用,因此我们可以通过栈来实现前序遍历。注意入栈的顺序。
递归写法:
/*** Definition for a binary tree node.* struct TreeNode {* int val;* TreeNode *left;* TreeNode *right;* TreeNode() : val(0), left(nullptr), right(nullptr) {}* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}* };*/
class Solution {
public:vector preorderTraversal(TreeNode* root) {vector ans;helper(root, ans);return ans;}void helper(TreeNode* root, vector& ans){if(!root){return;}ans.push_back(root->val);helper(root->left, ans);helper(root->right, ans);}
};
栈写法:
/*** Definition for a binary tree node.* struct TreeNode {* int val;* TreeNode *left;* TreeNode *right;* TreeNode() : val(0), left(nullptr), right(nullptr) {}* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}* };*/class Solution {
public:vector preorderTraversal(TreeNode* root) {vector ans;if(!root) return ans;stack s;s.push(root);while(!s.empty()){TreeNode* node = s.top();s.pop();ans.push_back(node->val);if(node->right){s.push(node->right);}if(node->left){s.push(node->left);}}return ans;}
};
二叉查找树(Binary Search Tree, BST)是一种特殊的二叉树:对于每个父节点,其左子树中 所有节点的值小于等于父结点的值,其右子树中所有节点的值大于等于父结点的值。因此对于一 个二叉查找树,我们可以在 O(log n) 的时间内查找一个值是否存在:从根节点开始,若当前节点 的值大于查找值则向左下走,若当前节点的值小于查找值则向右下走。同时因为二叉查找树是有 序的,对其中序遍历的结果即为排好序的数组。
一个二叉查找树的实现如下。
template
class BST{struct Node{T data;Node* left;Node* right;}Node* root;Node* makeEmpty(Node* t){if(t == NULL) return NULL;makeEmpty(t->left);makeEmpty(t->right);delete t;return NULL;}Node* insert(Node* t, T x){if(t == NULL){t = new Node;t->data = x;t->left = t->right = NULL;}else if(x < t->data){t->left = insert(t->left, x);}else if(x > t->data){t->right = insert(t->right, x);}return t;}Node* find(Node* t, T x){if(t == NULL) return NULL;if(x < t->data) return find(t->left, x);if(x > t->data) return find(t->right, x);return t;}Node* findMin(Node* t){if(t == NULL || t->left == NULL) return t;return findMin(t->left);}Node* findMax(Node* t){if(t == NULL || t->right == NULL) return t;return findMax(t->right);}Node* remove(Node* t ,T x){Node* temp;if(t == NULL){return NULL;}else if(x < t->data){t->left = remove(t->left, x);}else if(x > t->data){t->right = return(t->right, x);}else if(t->left && t->right){temp = findMin(t->right);t->data = temp->data;t->right = remove(t->right, t->data);}else{temp = t;if(t->left == NULL){t = t->right;}else if(t->right == NULL){t = t->left;}delete temp;}return t;}public:BST(): root(NULL){}~BST(){root = makeEmpty(root);}void insert(T x){insert(root, x);}void remove(T x){remove(root, x);}
};
99. 恢复二叉搜索树
99. Recover Binary Search Tree
给你二叉搜索树的根节点 root
,该树中的 恰好 两个节点的值被错误地交换。请在不改变其结构的情况下,恢复这棵树 。
我们可以使用中序遍历这个二叉查找树,同时设置一个 prev 指针,记录当前节点中序遍历 时的前节点。如果当前节点小于 prev 节点的值,说明需要调整次序。
有一个技巧是如果遍历整个序列过程中只出现了一次次序错误,说明就是这两个相邻节点需要被交换;如果出现了两次次序错误,那就需要交换这两个节点。
class Solution {
public:void recoverTree(TreeNode* root) {TreeNode *mistake1 = nullptr, *mistake2 = nullptr, *prev = nullptr;inorder(root, mistake1, mistake2, prev);if(mistake1 && mistake2){int temp = mistake1->val;mistake1->val = mistake2->val;mistake2->val = temp;}}void inorder(TreeNode* root, TreeNode* &mistake1, TreeNode* &mistake2, TreeNode* &prev){if(!root) return;if(root->left){inorder(root->left, mistake1, mistake2, prev);}if(prev && root->val < prev->val){if(!mistake1){mistake1 = prev;mistake2 = root;}else{mistake2 = root;}}prev = root;if(root->right){inorder(root->right, mistake1, mistake2, prev);}}
};
669. 修剪二叉搜索树
669. Trim a Binary Search Tree
给你二叉搜索树的根节点 root ,同时给定最小边界low 和最大边界 high。通过修剪二叉搜索树,使得所有节点的值在[low, high]中。修剪树 不应该 改变保留在树中的元素的相对结构 (即,如果没有被移除,原有的父代子代关系都应当保留)。 可以证明,存在 唯一的答案 。
所以结果应当返回修剪好的二叉搜索树的新的根节点。注意,根节点可能会根据给定的边界发生改变。
利用二叉查找树的大小关系,我们可以很容易地利用递归进行树的处理。
/*** Definition for a binary tree node.* struct TreeNode {* int val;* TreeNode *left;* TreeNode *right;* TreeNode() : val(0), left(nullptr), right(nullptr) {}* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}* };*/
class Solution {
public:TreeNode* trimBST(TreeNode* root, int low, int high) {if(!root){return root;}if(root->val > high){return trimBST(root->left, low, high);}if(root->val < low){return trimBST(root->right, low, high);}root->left = trimBST(root->left, low, high);root->right = trimBST(root->right, low, high);return root;}
};
字典树/前缀树(Trie)用于判断字符串是否存在或者是否具有某种字符串前缀。
为什么需要用字典树解决这类问题呢?假如我们有一个储存了近万个单词的字典,即使我们 使用哈希,在其中搜索一个单词的实际开销也是非常大的,且无法轻易支持搜索单词前缀。然而 由于一个英文单词的长度 n 通常在 10 以内,如果我们使用字典树,则可以在 O(n)——近似 O(1) 的时间内完成搜索,且额外开销非常小。
208. 实现 Trie (前缀树)
208. Implement Trie (Prefix Tree)
Trie(发音类似 "try")或者说 前缀树 是一种树形数据结构,用于高效地存储和检索字符串数据集中的键。这一数据结构有相当多的应用情景,例如自动补完和拼写检查。
请你实现 Trie 类:
Trie() 初始化前缀树对象。
void insert(String word) 向前缀树中插入字符串 word 。
boolean search(String word) 如果字符串 word 在前缀树中,返回 true(即,在检索之前已经插入);否则,返回 false 。
boolean startsWith(String prefix) 如果之前已经插入的字符串 word 的前缀之一为 prefix ,返回 true ;否则,返回 false 。
以下是字典树的典型实现方法。
class TrieNode{
public:TrieNode* childNode[26];bool isVal;TrieNode(): isVal(false) {for(int i=0; i<26; ++i){childNode[i] = nullptr;}}
};class Trie {TrieNode* root;
public:Trie(): root(new TrieNode()) {}// 向字典树插入一个词void insert(string word) {TrieNode* temp = root;for(int i=0; ichildNode[word[i] - 'a']){temp->childNode[word[i] - 'a'] = new TrieNode();}temp = temp->childNode[word[i] - 'a'];}temp->isVal = true;}// 判断字典树里是否有一个词bool search(string word) {TrieNode* temp = root;for(int i=0; ichildNode[word[i] - 'a'];}return temp? temp->isVal: false;}// 判断字典树是否有一个以词开始的前缀bool startsWith(string prefix) {TrieNode* temp = root;for(int i=0; ichildNode[prefix[i] - 'a'];}return temp;}
};/*** Your Trie object will be instantiated and called as such:* Trie* obj = new Trie();* obj->insert(word);* bool param_2 = obj->search(word);* bool param_3 = obj->startsWith(prefix);*/
欢迎大家共同学习和纠正指教
上一篇:电科大离散数学-4-二元关系