基础介绍

树的介绍

  • 无向无环图。
  • 由节点构成,节点之间具有相互关系,储存对应元素。
    • 节点索具有的子节点(子树)个数称为
    • 为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),我们就可以得到根节点到任意一个节点的距离,从而得到距离之和。
那么,接下来我们应该怎么从中知道其他的节点呢?

img

这样我们首先通过第一次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)
  • 接着我们通过二分查找的方式进行。假设我们深度为 h,则我们需要查找 2h12h1 的范围内的节点。
  • 怎么索引到具体的呢?假设我们有一棵树 [1,2,...,15],则 h=4 那么我们需要二分查找的空间为 [8,15]
  • 观察我们的二进制,当我们需要查找12的时候,我们有 1\textbr100, 而我们进行查找的方式是右(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;
}
};
posted @   木木ちゃん  阅读(4)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 记一次.NET内存居高不下排查解决与启示
· DeepSeek 开源周回顾「GitHub 热点速览」
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
点击右上角即可分享
微信分享提示