树
树
目录
基础介绍
树的介绍
- 无向无环图。
- 由节点构成,节点之间具有相互关系,储存对应元素。
- 节点索具有的子节点(子树)个数称为度。
- 度为0的节点称为叶子节点。
- 节点的子树的根节点称为双亲节点。相对应地子节点称为孩子节点。
- 整棵树的所有节点(除自己以外)的节点全部是孩子节点,则该节点为根节点。
- 双亲节点的层级(高度)比孩子节点小1。从根节点到叶子节点的最大层次称为树的高度。
- 森林是互不相交的树的集合。
二叉树
- 每个双亲节点最多只有两个孩子节点的树称为二叉树。子树具有左右之分。
- 每个节点(除叶子节点外)都有两个孩子节点的树称为满二叉树。
- 完全二叉树:自上而下,自左向右编号时,该二叉树存在的节点与同高度的满二叉树的对应节点编号相同时,称为完全二叉树。
- 二叉树的四种遍历方式:
- 先序遍历:根左右
- 中序遍历:左根右
- 后序遍历:左右根
- 层序遍历:从上到下,从左到右
- 线索二叉树:左孩子节点指向左子树的根节点或者前驱节点,右孩子节点指向右子树的根节点或者后继节点。根据遍历放手来确定前驱和后继节点。
刷题与解答
865.具有所有最深节点的最小子树
采用dfs方法进行。进行递归时,可以从尾部累计深度大小归回去。这样我们有:
/** * 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: using pti = pair<TreeNode *, int>; pti dfs(TreeNode * root) { // 递归到底部叶子节点返回。 if(root == nullptr) { return make_pair(root, 0); } pti leftTree = dfs(root -> left); pti rightTree = dfs(root -> right); // 左子树和右子树深度的大小比较,并在返回时增加深度。 if(leftTree.second > rightTree.second) return make_pair(leftTree.first, leftTree.second + 1); if(leftTree.second < rightTree.second) return make_pair(rightTree.first, rightTree.second + 1); // 相同情况下返回公共节点。 return make_pair(root, leftTree.second+1); } TreeNode* subtreeWithAllDeepest(TreeNode* root) { // 获取最终结果的节点。 return dfs(root).first; } };
834.树中距离之和
树上DP:换根DP
我们可以发现,通过第一次的深度优先搜索(DFS),我们就可以得到根节点到任意一个节点的距离,从而得到距离之和。
那么,接下来我们应该怎么从中知道其他的节点呢?
这样我们首先通过第一次dfs知道0根节点到所有子树的距离,再通过第二次dfs来进行“换根”操作,得到新的距离之和。
class Solution { public: vector<int> sumOfDistancesInTree(int n, vector<vector<int>>& edges) { vector<vector<int> > graph(n); // Build graph here. Undirected graph. for(int i = 0; i < edges.size(); i++) { graph[edges[i][0]].push_back(edges[i][1]); graph[edges[i][1]].push_back(edges[i][0]); } // Using dfs to gather the distance and add to all related. vector<int> vis(n,0); vector<int> distances(n,0); vector<int> subTreeNodes(n,1); // Tree DP: first, dfs the tree and find the distance and the sub tree of the node. std::function<void(int,int)> dfs1 = [&](int cur,int dist) -> void { vis[cur] = 1; distances[0] += dist; for(auto i: graph[cur]) { if(!vis[i]) { dfs1(i, dist+1); subTreeNodes[cur]+=subTreeNodes[i]; } } vis[cur] = 0; }; std::function<void(int)> dfs2 = [&](int cur) -> void { vis[cur] = 1; for(auto i: graph[cur]) { if(!vis[i]) { distances[i] = distances[cur] + n - subTreeNodes[i] * 2; dfs2(i); } } vis[cur] = 0; }; dfs1(0,0); dfs2(0); return distances; } };
优化1: 只需要保证不再访问父节点,作为树(无向无环图)可以不需要vis数组
仅传递上一个访问的节点即可。
class Solution { public: vector<int> sumOfDistancesInTree(int n, vector<vector<int>>& edges) { vector<vector<int> > graph(n); // Build graph here. Undirected graph. for(int i = 0; i < edges.size(); i++) { int x = edges[i][0], y = edges[i][1]; graph[x].push_back(y); graph[y].push_back(x); } // Using dfs to gather the distance and add to all related. vector<int> distances(n,0); vector<int> subTreeNodes(n,1); // Tree DP: first, dfs the tree and find the distance and the sub tree of the node. std::function<void(int,int,int)> dfs1 = [&](int cur, int pre, int dist) -> void { distances[0] += dist; for(auto i: graph[cur]) { if(i != pre) { dfs1(i, cur, dist+1); subTreeNodes[cur]+=subTreeNodes[i]; } } }; std::function<void(int,int)> dfs2 = [&](int cur, int pre) -> void { for(auto i: graph[cur]) { if(i != pre) { distances[i] = distances[cur] + n - subTreeNodes[i] * 2; dfs2(i, cur); } } }; dfs1(0,-1,0); dfs2(0,-1); return distances; } };
优化2: 采用lambda表达式和"deducing self"(C++23)方法
std::function
在调用时会分配内存空间,复制构造非引用对象,而且无法内联展开,而是调用虚函数,因此开销很大。
我们可以通过lambda表达式,通过通用引用(Universal reference)来传递lambba函数本身来实现递归。
... // Tree DP: first, dfs the tree and find the distance and the sub tree of the node. auto dfs1 = [&](auto&& dfs1, int cur, int pre, int dist) -> void { distances[0] += dist; for(auto i: graph[cur]) { if(i != pre) { dfs1(dfs1, i, cur, dist+1); subTreeNodes[cur]+=subTreeNodes[i]; } } }; auto dfs2 = [&](auto&& dfs2, int cur, int pre) -> void { for(auto i: graph[cur]) { if(i != pre) { distances[i] = distances[cur] + n - subTreeNodes[i] * 2; dfs2(dfs2, i, cur); } } }; dfs1(dfs1,0,-1,0); dfs2(dfs2,0,-1); ...
我们也可以通过推断自己(deducing this)方法来实现递归。(C++23)
... // Tree DP: first, dfs the tree and find the distance and the sub tree of the node. auto dfs1 = [&](this auto && self ,int cur, int pre, int dist) -> void { distances[0] += dist; for(auto i: graph[cur]) { if(i != pre) { self(i, cur, dist+1); subTreeNodes[cur]+=subTreeNodes[i]; } } }; auto dfs2 = [&](this auto && self,int cur, int pre) -> void { for(auto i: graph[cur]) { if(i != pre) { distances[i] = distances[cur] + n - subTreeNodes[i] * 2; self(i, cur); } } }; dfs1(0,-1,0); dfs2(0,-1); ...
1339.分裂二叉树的最大乘积
- 两次dfs进行优化。
- 第一次dfs,计算出总树的和。
- 第二次dfs,计算每个子树的节点和,利用基本不等式,寻找出与整棵树和的中值最接近的值,返回。
- 输出结果注意取模。
/** * 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 dfs(TreeNode * root) { int sum = root -> val; if(root -> left) { sum += dfs(root -> left); } if(root -> right) { sum += dfs(root -> right); } return sum; } int dfs2(TreeNode * root, int & sums ,int & best) { if(root == nullptr) return 0; // 空节点返回 int cur = dfs2(root -> left, sums, best) + dfs2(root -> right, sums, best) + root -> val; if(abs(sums - 2*best) > abs(sums - 2*cur)) best = cur; return cur; } int maxProduct(TreeNode* root) { int sumOfRoot = dfs(root); int best = sumOfRoot; dfs2(root, sumOfRoot, best); const int MOD = 1e9+7; return (long long)(sumOfRoot - best) * best % MOD; } };
- 最好不要写成
sum += dfs(left) + dfs(right)
因为递归很难被编译器优化导致大量时间消耗! - 分支的预测优化将会优于递归。
222.完全二叉树的节点个数
二分查找+位运算
- 首先通过dfs寻找到最大的深度。O(logn)
- 接着我们通过二分查找的方式进行。假设我们深度为
,则我们需要查找 到 的范围内的节点。 - 怎么索引到具体的呢?假设我们有一棵树 [1,2,...,15],则
那么我们需要二分查找的空间为 。 - 观察我们的二进制,当我们需要查找12的时候,我们有
, 而我们进行查找的方式是右(3)左(6)左(12)。 - 因此我们只需要采用位掩码(1 << (depth - 2))进行位检查,随后进行二分即可。
class Solution { int depth; public: void dfs(TreeNode * root, int cur) { depth = cur > depth ? cur : depth; if(root -> left) dfs(root -> left, cur + 1); } int countNodes(TreeNode* root) { // 1000 -> 1111 --> find middle = 12 -> 1100 & 8 & 4 & 2 -> check have -> left -> 13 15; if(!root) return 0; dfs(root, 1); int left = (1 << (depth - 1)), right = (1 << depth) - 1; int middle,ans; // Using Binary Search. while(left <= right) { middle = (left + right) >> 1; int bitMask = depth - 2 < 0 ? 0 : (1 << (depth - 2)); TreeNode * temp = root; while(bitMask) { if(!temp) break; if(bitMask & middle) { temp = temp -> right; } else { temp = temp -> left; } bitMask >>= 1; } if(temp) { left = middle + 1; ans = middle; } else right = middle - 1; } return ans; } };
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 记一次.NET内存居高不下排查解决与启示
· DeepSeek 开源周回顾「GitHub 热点速览」
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了