LeetCode力扣刷题——指针三剑客之二:树
创始人
2024-04-10 03:50:25
0


一、数据结构介绍

        作为(单)链表的升级版,我们通常接触的树都是二叉树(binary tree),即每个节点最多有 两个子节点;且除非题目说明,默认树中不存在循环结构。LeetCode 默认的树表示方法如下。

struct TreeNode {int val;TreeNode *left;TreeNode *right;TreeNode(int x) : val(x), left(NULL), right(NULL) {}
};

        可以看出,其与链表的主要差别就是多了一个子节点的指针。


二、经典问题

1. 树的递归

        对于一些简单的递归题,某些 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;}};

2. 层次遍历

        我们可以使用广度优先搜索进行层次遍历。注意,不需要使用两个队列来分别存储当前层的 节点和下一层的节点,因为在开始遍历一层的节点时,当前队列中的节点数就是当前层的节点 数,只要控制遍历这么多节点数,就能保证这次遍历的都是当前层的节点。

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;}
};

2. 前中后序遍历

        前序遍历、中序遍历和后序遍历是三种利用深度优先搜索遍历二叉树的方式。它们是在对节 点访问的顺序有一点不同,其它完全相同。考虑如下一棵树:

         前序遍历先遍历父结点,再遍历左结点,最后遍历右节点,我们得到的遍历顺序是 [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;}
};

3. 二叉查找树

        二叉查找树(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;}
};

4. 字典树

        字典树/前缀树(Trie)用于判断字符串是否存在或者是否具有某种字符串前缀。

字典树,存储了单词 A、to、tea、ted、ten、i、in 和 inn,以及它们的频率

         为什么需要用字典树解决这类问题呢?假如我们有一个储存了近万个单词的字典,即使我们 使用哈希,在其中搜索一个单词的实际开销也是非常大的,且无法轻易支持搜索单词前缀。然而 由于一个英文单词的长度 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);*/

三、巩固练习


欢迎大家共同学习和纠正指教

相关内容

热门资讯

银河麒麟V10SP1高级服务器... 银河麒麟高级服务器操作系统简介: 银河麒麟高级服务器操作系统V10是针对企业级关键业务...
【NI Multisim 14...   目录 序言 一、工具栏 🍊1.“标准”工具栏 🍊 2.视图工具...
AWSECS:访问外部网络时出... 如果您在AWS ECS中部署了应用程序,并且该应用程序需要访问外部网络,但是无法正常访问,可能是因为...
不能访问光猫的的管理页面 光猫是现代家庭宽带网络的重要组成部分,它可以提供高速稳定的网络连接。但是,有时候我们会遇到不能访问光...
AWSElasticBeans... 在Dockerfile中手动配置nginx反向代理。例如,在Dockerfile中添加以下代码:FR...
Android|无法访问或保存... 这个问题可能是由于权限设置不正确导致的。您需要在应用程序清单文件中添加以下代码来请求适当的权限:此外...
月入8000+的steam搬砖... 大家好,我是阿阳 今天要给大家介绍的是 steam 游戏搬砖项目,目前...
​ToDesk 远程工具安装及... 目录 前言 ToDesk 优势 ToDesk 下载安装 ToDesk 功能展示 文件传输 设备链接 ...
北信源内网安全管理卸载 北信源内网安全管理是一款网络安全管理软件,主要用于保护内网安全。在日常使用过程中,卸载该软件是一种常...
AWS管理控制台菜单和权限 要在AWS管理控制台中创建菜单和权限,您可以使用AWS Identity and Access Ma...