个人觉得路径总和系列问题是DFS中比较经典的提醒,整合到了一篇文章,感兴趣可以看一下:传送门
二叉树的中序遍历可以说是DFS的基础问题,但是其三种解法难度递增,个人觉得对于理解DFS的本质有一定的帮助,笔者将其三种解法整合到了一篇文章:传送门
LeetCode100-相同的树Easy
经典DFS编程,两个树同步进行就好了~
class Solution {
public:
bool isSameTree(TreeNode* p, TreeNode* q) {
if(p==nullptr&&q==nullptr) return true;
if(!(p!=nullptr&&q!=nullptr)) return false;
return p->val==q->val&&isSameTree(p->left,q->left)&&isSameTree(p->right,q->right);
}
};
LeetCode98-验证二叉搜索树Middle
解法一:根据二叉搜索树的特性可知,其中序遍历结果为一个递增数组,我们可以中序遍历二叉树,同时检查新保存的数据是否满足递增。
(注意在便利的同时就可以进行判断,这样节省掉额外遍历结果数组的时间)
class Solution
{
public:
bool isValidBST(TreeNode *root)
{
vector<int> nums;
stack<TreeNode* > stk;
while(root!=nullptr||!stk.empty())
{
while(root!=nullptr)
{
stk.push(root);
root=root->left;
}
root=stk.top();
if(nums.size()&&root->val<=nums.back()) return false;//遍历同时检查数组
nums.push_back(root->val);
root=root->right;
stk.pop();
}
return true;
}
};
解法二:DFS每个节点,判断当前节点是否满足二叉搜索树的条件,对于每一个节点来说,他都要满足在一个上下限内,这里要考虑到子节点不单单要考虑直接的父节点,还要考虑更上层的根节点。
使用两个变量用于限定范围。
(最开始考虑DFS编程时,思路是对于每个节点我去看它的左右子节点,但是这样会出现针对左右子树我们要采取不同的策略,可以完成题目要求,但是编程过于冗余)
class Solution
{
public:
bool isValidBST(TreeNode *root)
{
return DFS(root, -__LONG_MAX__, __LONG_MAX__);
}
bool DFS(TreeNode *root, long upper, long lower)
{
if (root == nullptr)
return true;
return root->val < lower && root->val > upper && DFS(root->left, upper, root->val) && DFS(root->right, root->val, lower);
}
};
LeetCode110-平衡二叉树Middle
解法一:自顶向下-常规DFS思路
递归的访问每个子树,同时使用递归的方法求出其左右子树高度,然后进行判断;即递归嵌套递归使用。
时间复杂度O(n^2)(对于高度存在了很多的重复计算)
空间复杂度O(n)
class Solution
{
public:
bool isBalanced(TreeNode *root)
{
if (root == nullptr)
return true;
return abs(dfs(root->left) - dfs(root->right)) <= 1 && isBalanced(root->left) && isBalanced(root->right);
//递归的对每个节点判断左右子树高度差是否满足条件
}
int dfs(TreeNode *root) //求每个节点的高度
{
if (root == nullptr)
return 0;
return max(dfs(root->left), dfs(root->right)) + 1;
}
};
解法二:自底向上的递归(类比于后序遍历的思路)
递归的求每个节点左右子树的高度,同时判断,在返回值中体现其是否满足要求。
时间复杂度O(n)
空间复杂度O(n)
class Solution
{
public:
bool isBalanced(TreeNode *root)
{
if (dfs(root) == -1)
return false;
return true;
}
int dfs(TreeNode *root)
{
if (root == nullptr)
return 0;
int leftHeight;
int rightHeight;
if ((leftHeight = dfs(root->left)) == -1 || (rightHeight = dfs(root->right)) == -1 || abs(leftHeight - rightHeight) > 1)
//在判断过程中赋值可以在左子树不满足时不需要计算对应右子树,节省一点时间
{
return -1; //-1代表当前子树不符合要求
}
return max(leftHeight, rightHeight) + 1; //返回符合要求子树的高度
}
};
LeetCode99-恢复二叉搜索树Middle
两种解法共通点:我们通过中序遍历结果是否保持递增找出被交换的数字所在位置,每次找到一对递减的数对,要考虑到交换节点的结果是产生一对或两对数对,第一对数字一定包含第一个应该被交换的节点,但寻找第二个节点要通过继续向后遍历看是否有第二对数对。
解法一:利用二叉搜索树中序遍历有序递增的特性,遍历整个树,然后遍历保存路径的数组中找到应该被交换的两个点。
时空复杂度均为O(N)
class Solution
{
public:
void recoverTree(TreeNode *root)
{
//利用二叉搜索树的中序遍历有序特性
vector<TreeNode *> nums;
DFS(root, nums);
TreeNode *p = nullptr, *q = nullptr;
for (int i = 0; i < nums.size() - 1; i++)
{
//交换两个节点,在中序遍历中产生一对或两对递减数对
//当只有一对时说明交换的时相邻节点
//两对对应交换的节点之间存在一定的间隔
//我们记录第一对数对,如果存在第二对,那就更新第二个点,反之直接输出
if (nums[i]->val >= nums[i + 1]->val)
{
if (p == nullptr)
p = nums[i];
}
if (nums[i]->val >= nums[i + 1]->val)
{
q = nums[i + 1];
}
}
if (q == nullptr)
q = p + 1;
int temp = p->val;
p->val = q->val;
q->val = temp;
}
void DFS(TreeNode *root, vector<TreeNode *> &nums)
{
if (root == nullptr)
return;
DFS(root->left, nums);
nums.push_back(root);
DFS(root->right, nums);
}
};
解法二:Morris中序遍历
关于Morris算法具体思路与实现可见笔者另一篇文章-传送门
本题中只需要根据找交换点的特性更改一部分代码即可。
时间复杂度O(N)(实际上是2N)
空间复杂度O(1)
class Solution
{
public:
void recoverTree(TreeNode *root)
{
TreeNode *PreTreeNode = nullptr, *a = nullptr, *b = nullptr;
// a,b分别用于保存两个要交换的节点,pre用于保存当前节点前一个节点,用于比较
TreeNode *predecessor = nullptr;
while (root != nullptr)
{
if (root->left != nullptr)
{
// predecessor 节点就是当前 root 节点向左走一步,然后一直向右走至无法走为止
predecessor = root->left;
while (predecessor->right != nullptr && predecessor->right != root)
//找到左子树中最右的节点,用predecessor标记
{
predecessor = predecessor->right;
}
if (predecessor->right == nullptr)
//说明当前是用右指针建立父子连接的过程
// 让 predecessor 的右指针指向 root,继续遍历左子树
{
predecessor->right = root;
root = root->left;
}
else
//当前已建立好连接,处于获取val值求res的过程
// 说明左子树已经访问完了,我们需要断开链接
{
//直接在遍历的程序上将每次记录改为判断是否符合二叉搜索树规律
if (PreTreeNode != nullptr)
{
if (root->val <= PreTreeNode->val)
{
if (a == nullptr)
a = PreTreeNode;
b = root;
}
else
{
PreTreeNode = root;
}
}
else
PreTreeNode = root;
predecessor->right = nullptr;
//为了恢复树的结构,如果只是为了求便利的结果实际上这一步可有可无,不影响遍历的结果
// leetcode测试中注释掉这行代码会报错,猜测可能是因为更改了树的结构
//而测试用例是通过恢复这一树结构重新填值来改变的
root = root->right;
}
}
// 如果没有左孩子,则直接访问右孩子
else
{
if (PreTreeNode != nullptr)
{
if (root->val <= PreTreeNode->val)
{
if (a == nullptr)
a = PreTreeNode;
b = root;
}
else
{
PreTreeNode = root;
}
}
else
PreTreeNode = root;
root = root->right;
}
}
swap(a->val, b->val);
}
};
DFS一般采用递归的形式,使用栈结构先进后出的特性,核心在于状态回溯。
DFS本身的思路其实很简单,只要一直关注“深度优先”四个字进行思考很容易都可以得到一种编程解法,但是很多时候时空消耗都不尽人意,要与其他的方法进行适当的结合…
大概就先写这么多,最近忙着别的事情每天就只是靠每日一题养老有些懈怠,后面会选择新的模块继续大量刷题的!