1 二叉搜索树的搜索
①遍历法
因为二叉搜索树的特性,可以采用简单的循环进行搜索,小则左,大则右。
这个简单的题目是以后很多算法题的根基,对二叉搜索树的定义与基本特性的考察。
class Solution {
public:
TreeNode* searchBST(TreeNode* root, int val) {
TreeNode* cur_node=root;
while(true)
{
if(cur_node->val==val)return cur_node;
//在左子树
else if(cur_node->val>val)
{
if(cur_node->left==nullptr)return cur_node->left;
else cur_node=cur_node->left;
}
else
{
if(cur_node->right==nullptr)return cur_node->right;
else cur_node=cur_node->right;
}
}
}
};
2 验证二叉搜索树
①定义法
利用二叉搜索树的特性,即根节点值大于左子树最大值,小于右子树最小值(不能使用大于左子节点+小于右子节点判断)
class Solution {
public:
class MVal
{
public:
int max_val;
int min_val;
bool flag;
MVal(int maxval,int minval,bool f)
{
max_val=maxval;
min_val=minval;
flag=f;
}
};
MVal isValidBSTAssist(TreeNode* root)
{
if(root->left==nullptr&&root->right==nullptr)
return MVal(root->val,root->val,true);
if(root->left&&root->right==nullptr)
{
MVal left=isValidBSTAssist(root->left);
if(left.flag && left.max_val<root->val)
left.flag=true;
else
left.flag=false;
left.max_val=max(left.max_val,root->val);
return left;
}
if(root->right&&root->left==nullptr)
{
MVal right=isValidBSTAssist(root->right);
if(right.flag && right.min_val>root->val)
right.flag=true;
else
right.flag=false;
right.min_val=min(right.min_val,root->val);
return right;
}
MVal left=isValidBSTAssist(root->left);
MVal right=isValidBSTAssist(root->right);
bool f1;
if(right.flag && right.min_val>root->val && left.flag && left.max_val<root->val)
f1=true;
else
f1=false;
int Max_Val=max(root->val,right.max_val);
int Min_Val=min(root->val,left.min_val);
return MVal(Max_Val,Min_Val,f1);
}
bool isValidBST(TreeNode* root) {
MVal result=isValidBSTAssist(root);
return result.flag;
}
};
②中序遍历递增法
事实上,二叉搜索树具有着较强的顺序规律,其中序遍历结果一定为递增序列,因此可以通过中序遍历结果进行判断。
class Solution {
public:
long max_val=-2147483649;
bool isValidBST(TreeNode* root) {
bool left=true;
bool right=true;
//左
if(root->left)left=isValidBST(root->left);
//中
if(root->val<=max_val)return false;
else
{
max_val=root->val;
}
//右
if(root->right)right=isValidBST(root->right);
return left&&right;
}
};
3 二叉搜索树的最小绝对差
①定义法
从最基础的思路出发,因为left.left < left.right< root < right.left< right,所以结果应该在左子树的最右边或右子树的最左边与root的差,借助这个思路可以递归求解。
class Solution {
public:
int getMinimumDifference(TreeNode* root) {
//因为left.left<left.right<root<right.left<right
//所以结果应该在左子树的最右边或右子树的最左边与root的差
int val,res,res_left,res_right;
TreeNode* cur_node;
//递归终止,叶节点
if(root->left==nullptr&&root->right==nullptr)return -1;
//只有左子树
else if(root->left&&root->right==nullptr)
{
cur_node=root->left;
while(cur_node)
{
val=cur_node->val;
cur_node=cur_node->right;
}
res=abs(root->val-val);
res_left=getMinimumDifference(root->left);
if(res_left==-1)return res;
else return min(res,res_left);
}
//只有右子树
else if(root->left==nullptr&&root->right)
{
cur_node=root->right;
while(cur_node)
{
val=cur_node->val;
cur_node=cur_node->left;
}
res=abs(root->val-val);
res_right=getMinimumDifference(root->right);
if(res_right==-1)return res;
else return min(res,res_right);
}
//左右子树都在
else
{
//左
cur_node=root->left;
while(cur_node)
{
val=cur_node->val;
cur_node=cur_node->right;
}
res=abs(root->val-val);
//右
cur_node=root->right;
while(cur_node)
{
val=cur_node->val;
cur_node=cur_node->left;
}
res=min(res,abs(root->val-val));
res_left=getMinimumDifference(root->left);
res_right=getMinimumDifference(root->right);
if(res_left==-1)res_left=res+1;
if(res_right==-1)res_right=res+1;
return min(res,min(res_left,res_right));
}
}
};
②中序遍历递增法
中序遍历递增,直接比较相邻元素即可,使用迭代法是最简单明了的,这里采用了递归算法,因此需要外置全局变量minDiff与pre。
class Solution {
public:
TreeNode* pre_node=nullptr;
int minAbsDiff=-1;
int getMinimumDifference(TreeNode* root) {
//中序遍历,单调递增
//终止条件
if(root==nullptr)return 0;
//左
getMinimumDifference(root->left);
//中
if(pre_node==nullptr)
{
pre_node=root;
}
else
{
if(minAbsDiff==-1)
{
minAbsDiff=abs(root->val-pre_node->val);
}
else
{
minAbsDiff=min(minAbsDiff,abs(root->val-pre_node->val));
}
pre_node=root;
}
//右
getMinimumDifference(root->right);
return minAbsDiff;
}
};
4 二叉搜索树中的众数
依然是套路题,中序遍历解决。
class Solution {
public:
int maxCount=0;
int count=0;
TreeNode* pre=nullptr;
vector<int> result;
//利用BST的中序遍历性质
//相等的数在中序遍历中会一致
void findModeAssist_search(TreeNode* root)
{
//递归终止
if(root==nullptr)return;
//左
findModeAssist_search(root->left);
//中
if(pre==nullptr)
{
count=1;
}
else if(pre->val==root->val)
{
count++;
}
else
{
count=1;
}
//更新节点
pre=root;
if(count==maxCount)result.push_back(root->val);
//右
if(count>maxCount)
{
result.clear();
maxCount=count;
result.push_back(root->val);
}
findModeAssist_search(root->right);
}
vector<int> findMode(TreeNode* root) {
findModeAssist_search(root);
return result;
}
};
5 二叉树的最近公共祖先
由于是普通二叉树,没有二叉搜索树的高度序列性,因此必须遍历整棵树才能获得最小公共祖先。
本道题可以使用后序遍历的思想进行处理,即左右子树的结果作为根节点的处理逻辑的基石。
一般而言,对于递归后的子树,返回的必定是节点、节点的最小祖先节点与空节点三种情况。
那么对于根节点而言,如果左右子树均非空节点,意味着自己就是最小的祖先节点,因为在自己之后,所有递归节点必然有至少一边是空节点。
如果根节点的左右子树只有一边是非空,那么存在两种情况:一为祖先节点依然在根节点之上,那么此时应该返回节点,而不是自身;二为祖先节点已经被找到,传来的非空节点即为结果,那么我们依然必须传递非空节点,而不能更改。
如果左右两边都为空节点,那自然是返回空节点作为路径错误的信号。
总而言之,本道题中,需要记住本道题中所返回的节点必然是空节点、答案、节点三种情况,在根节点并非答案的情况下,不能修改返回值导致判断错误。
class Solution {
public:
TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
//返回当前节点
if(root->val==p->val||root->val==q->val)return root;
TreeNode* left=nullptr;
TreeNode* right=nullptr;
if(root->left)left=lowestCommonAncestor(root->left,p,q);
if(root->right)right=lowestCommonAncestor(root->right,p,q);
//左右子树都非空,说明目前是最近公共祖先,且之后的回溯节点都是一边空一边非空
if(left&&right)return root;
//此时有两种可能,一旦成立就意味着right=nullptr
//1 正在寻找祖先的路上,left代表的只是非空
//2 已找到祖先,将结果返回的路上。left代表的是最终结果
if(left)return left;
//理论上应该与left一致,right不成立时的时候返回nullptr
//此处为省略写法,因为现在只有left=nullptr的情况了
//那么right代表三种可能,
//1 正确的子树在寻找祖先的路上,代表非空
//2 同left2,代表结果
//3 错误的子树,代表空
return right;
}
};
6 二叉搜索树的最近公共祖先
因为是二叉搜索树,所以直接利用其有序性进行判断即可。最近祖先节点要么是两个节点之一,那么是节点值介于两个节点之中。
class Solution {
public:
TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
TreeNode* cur_node=root;
//祖先
if(cur_node->val==p->val||cur_node->val==q->val)return cur_node;
if((p->val<cur_node->val)&&(q->val>cur_node->val))return cur_node;
if((p->val>cur_node->val)&&(q->val<cur_node->val))return cur_node;
//同方向,更改cur_node
if(p->val>cur_node->val&&q->val>cur_node->val)return lowestCommonAncestor(cur_node->right,p,q);
else return lowestCommonAncestor(cur_node->left,p,q);
}
};
7 二叉搜索树中的插入操作
二叉树的插入操作,必须注意维持二叉树的自身序列特征。利用定义一路查询到空节点(插入位置),并利用第二个指针记录其父节点,进行插入即可。
class Solution {
public:
TreeNode* insertIntoBST(TreeNode* root, int val) {
//特殊情况处理
if(root==nullptr)
{
root=new TreeNode(val);
return root;
}
TreeNode* parent=root;
TreeNode* cur_node=root;
while(cur_node)
{
parent=cur_node;
if(cur_node->val>val)cur_node=cur_node->left;
else cur_node=cur_node->right;
}
if(parent->val>val)parent->left=new TreeNode(val);
else parent->right=new TreeNode(val);
return root;
}
};
8 删除二叉搜索树中的节点
相比于插入节点的轻便,删除节点意味着对二叉搜索树的结构调整,需要分情况讨论。
1 删除叶节点是最简单的,直接断开删除节点与父节点连接即可。
2 对于单边节点也是相对容易的,只需要将子节点与父节点进行连接即可。
3 最为复杂的是双边节点,我们需要寻找一个节点替代删除节点的位置,以维持二叉搜索树的结构。一般而言有两个思路,由于max(left)< root< min(right),因此我们可以用左子树最大值 / 右子树最小值代替删除节点,而左子树最大值与右子树最小值必然是单边节点或叶节点,删除就容易多了。
这里采用右子树最小值。
class Solution {
public:
int findMin(TreeNode* root)
{
TreeNode* cur_node=root;
while(cur_node->left)
{
cur_node=cur_node->left;
}
return cur_node->val;
}
TreeNode* deleteNode(TreeNode* root, int key) {
//递归终止条件
if(root==nullptr)return nullptr;
if(key==root->val)
{
//叶节点
if(root->left==nullptr&&root->right==nullptr)
{
return nullptr;
}
//左子树
else if(root->left&&root->right==nullptr)
{
TreeNode* result=root->left;
return result;
}
//右子树
else if(root->right&&root->left==nullptr)
{
TreeNode* result=root->right;
return result;
}
//左右子树都在
else
{
int right_min=findMin(root->right);
root->val=right_min;
root->right=deleteNode(root->right,right_min);
return root;
}
}
else if(key>root->val)root->right=deleteNode(root->right,key);
else root->left=deleteNode(root->left,key);
return root;
}
};
9 修剪二叉搜索树
有两种思路思考这道题。
1 思考根节点是否需要删除。需要删除根节点时,由于二叉搜索树的有序性,必然有一边的子树也会被完全删除,因此直接返回另一边的递归结果即可;而不需要删除时,则是将根节点与左右子树递归结果进行连接。
2 利用二叉搜索树的有序性,寻找需要保留的区间。即根节点需要删除时,利用有序性,去修剪其中一边的子树,在递归的帮助下会自然地寻找到合适的区间。
class Solution {
public:
TreeNode* trimBST(TreeNode* root, int low, int high) {
//叶节点可以直接删除返回nullptr
//如果root需要删除的情况下,则root左右子树的返回结果至多只有一个不为nullptr
//因为一旦两边都不为nullptr,意味着low<left_return<root<right-return<high,那么root不应该删除
//因此,一旦root需要删除,只要返回左右子树修剪结果中不为nullptr的即可
if(root==nullptr)return nullptr;
//根节点需要删除的情况下
if(root->val<low||root->val>high)
{
TreeNode* left=trimBST(root->left,low,high);
TreeNode* right=trimBST(root->right,low,high);
if(left)return left;
else return right;
}
//根节点不需要修剪的情况下
//修剪左右子树即可
root->left=trimBST(root->left,low,high);
root->right=trimBST(root->right,low,high);
return root;
}
};
10 将有序数组转换为二叉搜索树
相比于中序遍历+后序遍历构造二叉树,在思考逻辑上二者完全一致,但本题有更加简单一些。
利用中序数组递增的特性,不断地二分数组,构造二叉树节点并连接即可。
class Solution {
public:
TreeNode* sortedArrayToBSTAssist(vector<int>& nums,int start,int end)
{
if(end<start)return nullptr;
int val_index=(start+end)/2;
TreeNode* root=new TreeNode(nums[val_index]);
if(end==start)return root;
root->left=sortedArrayToBSTAssist(nums,start,val_index-1);
root->right=sortedArrayToBSTAssist(nums,val_index+1,end);
return root;
}
TreeNode* sortedArrayToBST(vector<int>& nums) {
return sortedArrayToBSTAssist(nums,0,nums.size()-1);
}
};
11 把二叉搜索树转换为累加树
思考到累加树的顺序是从最大节点累加到最小节点,而二叉搜索树的中序遍历是升序序列,那么自然地,我们反向地进行中序遍历。每次的根节点赋值为右节点(累加结果)+根节点值,不断递归即可完成累加树的构造。
对sum=0的初始化,可以有效地对最大值的累加进行异常情况排除。
class Solution {
public:
int sum=0;
TreeNode* convertBST(TreeNode* root) {
//异常处理,也是递归终止条件
if(root==nullptr)return root;
//中序遍历思想
//但需要注意到,实际上的顺序为右-中-左,而非左-中-右
//右
convertBST(root->right);
//中
sum+=root->val;
root->val=sum;
//左
convertBST(root->left);
return root;
}
};
12 总结
老本啃完喽,同时也开学了,接下来要好好学习回溯、动态规划、双指针的内容。
至于树这一方面,还有很多AVL、B树、B+树、最小顶堆、红黑树等内容需要进一步深化学习,只能说慢慢来了。
另外GAN虽然不怎么吃设备要求,但DCGAN太tm难训练了,以前听过别人的强调,但自己亲自上手才发现这玩意的难度之大。生成器G和判别器D很难达到旗鼓相当的地步,不得不说判别这一个任务确实比生成要简单多了。
虽然在理论上还有着JK损失的问题啊,不如WGAN的损失设计之类,以及图片的数据分布的狭窄之类的,明天打算上传自己DCGAN的代码到github上,慢慢开始学习github管理自己的代码和实验成果。
吐槽一下:开学的前几门课都有点折磨,感觉很难学到自己想学的东西,本来打算白天刷题,晚上上课写这个MarkDown的,结果老师不让用电脑,真是绝了,下一次一定打印一些算法或论文过去看。
——2023.2.20