leetcode572的几种优化方案
前言:前几天面试的时候遇到了这题,因为是去年刷的这题,最后只记得一个hash,靠着这个提示和面试官的一些提示最终勉强做出来。
几种一般的解法
- 简单深度搜索,判断每一个节点所对应的子树是否结构和目标树相同。
- 将树的结构用字符串替代(参考树的序列化等方法),然后进行字符串的比较
- 将树转换为一个唯一的hash数字,通过比较数字的相同来判断两个树是否结构相同。
其他信息
- 树的节点个数
- 树的深度
- 如何处理hash冲突
因为面试的时候比较紧张,就只将第三个信息加进来了。所以最终的代码是这样的:
typedef unsigned long long hash_type;
struct node{
int val;
node* left;
node* right;
};
bool isSameTree(node* root1, node* root2)
{
if(root1 != nullptr && root2 != nullptr){
return isSameTree(root1->left, root2->left)
&& isSameTree(root1->right, root2->right);
}else if(root1 == nullptr && root2 == nullptr){
return true;
}else{
return false;
}
}
hash_type getTreeHash(node* root)
{
if(!root){
return 0;
}
return getTreeHash(root->left)*5+7+getTreeHash(root->right)*11;
}
hash_type dfs_subtree(node* root, hash_type aimValue, bool& find, node* root2)
{
if(find){
return 0;
}
if(!root){
return 0;
}
hash_type leftValue = dfs_subtree(root->left, aimValue, find, root2);
hash_type rightValue = dfs_subtree(root->right, aimValue, find, root2);
hash_type value = leftValue*5+7+rightValue*11;
if(value == aimValue && !find){
find = isSameTree(root, root2);
}
return value;
}
bool isSubtree(node* root1, node* root2)
{
if(!root2){
return true;
}
if(!root1){
return false;
}
hash_type root2HashValue = getTreeHash(root2);
bool find = false;
dfs_subtree(root1, root2HashValue, find, root2);
return find;
}
面试管针对以上代码的评价:
- dfs_subtree函数将两个功能合在了一起
个人理解:按照leetcode的题解,将其功能进行分离,最终也可以达到目的,事实上,我在创建getTreeHash的函数的时候也是想着将功能进行分离,但是后续考虑到针对每一个节点要么多次计算,要么用hashMap进行记录,时间和空间复杂度都会变高。故没采用。(但是没和面试官说明,功能分离的代码也自行查看leetcode题解,这里不做说明) - 我最初写的版本没有isSameTree这个函数,所以会出现hash冲突的情况。在面试管的提示下添加了这个函数,但是因为之前针对find的修改只是变true。增加这个函数之后就可以出现true变成false的情况,所以需要在进行dfs判断两个树的结构是否相同之前需要判断是否已经找到了。也即将判断条件改成if(value == aimValue && !find)。
个人说明:后面面试官问我学到了什么,当时答的比较一般,只是简单说明针对非修改参数设置const,针对要修改的参数都要在修改前进行确认。实际上后续思考了一下,其实是针对某一个模块的修改,需要站在上一层架构或者逻辑进行思考确认其能否兼容该修改,如果不能兼容,则需要修改上一层的逻辑,然后递归向上层进行重新思考。 - 面试管说以上代码他已经感觉没什么问题了。但是实际上,还有一些可以改进的点。
a) 计算hash的数值应该整合成一个函数,避免后续修改hash的计算方式需要修改多个地方
b) 未考虑节点的个数和深度等信息,可以进行剪枝来进行加快判断
c) 因为hash数值是粗判断,可以使用int数值才记录hash数值(int的计算比unsigned long long要快)
typedef int hash_type;
struct node{
int val;
node* left;
node* right;
};
struct hash_node{
hash_type hashValue; // 记录hash数值
int depth; // 记录树的深度
int nodeCnt; // 记录树的节点个数
hash_node():hahs_Value(0),depth(0),nodeCnt(0){}
};
bool isSameTree(node* root1, node* root2)
{
if(root1 != nullptr && root2 != nullptr){
return isSameTree(root1->left, root2->left)
&& isSameTree(root1->right, root2->right);
}else if(root1 == nullptr && root2 == nullptr){
return true;
}else{
return false;
}
}
hash_type CalHashValue(const hash_type& left, const hash_type& right)
{
// 记得题解的话也可以按照答案计算hash数值
return left*101+71+right*301;
}
hash_node getTreeHash(node* root)
{
hash_node res;
if(root){
hash_node&& leftNode = getTreeHash(root->left);
hash_node&& rightNode = getTreeHash(root->right);
// 以下三行的功能应该整合成一个函数
res.hashValue = CalHashValue(leftNode.hashValue, rightNode.hashValue);
res.depth = max(leftNode.depth, rightNode.depth)+1;
res.nodeCnt = (leftNode.nodeCnt + rightNode.nodeCnt)+1;
}
return res;
}
hash_node dfs_subtree(node* root, const hash_node& aimValue, bool& find, node* root2)
{
// 多处return也可以按照redis的实现用goto来进行优化
// 主要是针对编译代码的一个优化,一般不用考虑,并且有的大佬也不喜欢goto
hash_node res;
if(find || !root){
return res;
}
hash_node&& leftNode = dfs_subtree(root->left, aimValue, find, root2);
hash_node&& rightNode = dfs_subtree(root->right, aimValue, find, root2);
res.depth = max(leftNode.depth, rightNode.depth)+1;
res.nodeCnt = (leftNode.nodeCnt + rightNode.nodeCnt)+1;
// 通过对深度和高度的判断来进行剪枝
// 可以针对左树特别大,右树很小的情况进行的一个剪枝优化
if(res.depth > aimValue.depth || res.nodeCnt > aimValue.nodeCnt){
return res;
}
res.hashValue = CalHashValue(leftNode.hashValue, rightNode.hashValue);
if(res == aimValue && !find){
find = isSameTree(root, root2);
}
return res;
}
bool isSubtree(node* root1, node* root2)
{
if(!root2){
return true;
}
if(!root1){
return false;
}
hash_node&& root2HashNode = getTreeHash(root2);
bool find = false;
dfs_subtree(root1, root2HashValue, find, root2);
return find;
}
该代码主要是针对左树特别大的情况下的一种优化。因为该实现仍需要遍历,从下往上算,高度小于等于目标树高度的子树。所以当两边的树相差不大的时候,该实现会因为引进了新的参数,计算速度反而会下降。但是当左树特别大的时候,该优化就可以将高度过高的节点的计算从hash计算改成加法计算,所以树的高度相差越大,优化效果越明显。(当然,该实现可以询问是否需要)