#所想
都说万事开头难,想了好多天像记录一下,但就是一直在找理由去推迟,倒不如就从现在开始吧。我文笔不好,说话也总是思路不清,就当是日记,记录一下。
之前看了很多优秀的博主通过博客分享自己的生活,分享自己的感受以及学习经历,以至于我也产生了这种冲动想通过这种方法去记录一下自己的生活,自己的感悟。
先说一下博主本人的情况吧,本人目前已经大四了,在沈阳的一所双非学校,也到了秋招的时间,就想着把之前学的知识都复习一下,再做个项目准备秋招。在此之前呢,是找了沈阳的一个公司进行实习,进行运维工作,第一次进行工作,过程很艰难,但自己赚了前,也买了喜欢的switch,不用靠家里的生活费,总的来说还是很开心的。实习期间有很多收获,有很多感受,但工作内容都是重复的,不需要什么技术的活,以至于现在感觉什么都不会了,感觉把之前学到的知识都忘干净了,为此这些天一直在进行复习。
复习期间呢,接触了俗称八股触发器的WebServer项目,跟着csdn博主:JehanRio 进行学习的,看了这位优秀的博主的博客,也想要以此记录一下。现在呢则有点悔恨自己没早点发现这种方式像优秀的博主一样从大一就开始记录。我现在已经大四,总想着时间已经来不及了,没有空再去写博客,写学习内容,便一推再推。
最近有重温了一边我最喜欢的电视剧《士兵突击》,我喜欢看B站UP主:木鱼水心 的解说,关于《士兵突击》我已经看了好多遍了,但是每当我有些焦虑,有些低迷的时候,我都会想去再看一次,王宝强饰演的许三多总是给予我很多力量,我也想成为一个像他一样不焦虑,能耐得住寂寞的人,在许三多所待的七连改编之后,所有的队友都已经离开,只剩许三多一人独自守着连队,这一守就是半年之久,在这半年的时间里,许三多一个人做内务、一个人出早操,一个人开班会、一个人唱军歌,过着机械化、程序化的生活,到最后一向看不上许三多的连长也说出了:我认识一个人,他每做一件小事儿的时候,他都像救命稻草一样抓着,有一天我一看,嚯!好家伙!他抱着的是已经让我仰望的参天大树了。我也要努力像次一样,争取把每件小事做好,预期焦虑时间来不级,没空写博客,倒不如从现在开始就开始。
哎,自己的一点感悟吧,最近总是很多想法,可能是浏览了过多招聘信息吧,通过这种方式记录一下,倾诉一下,也是很舒服的。希望自己能坚持写博客,坚持学习,保持清醒,保持想象。
#树的常用算法
说了很多,下面就正式开始吧,第一次分享,就刚好从要复习的树的常用算法开始吧。顺便在这里说下,下文都是我的理解,理解可能有误,如果想好好学习算法,我只推荐B站UP主:左程云——左神,直接搜索名字就能找到。
树的结构定义
struct TreeNode
{
int val;
TreeNode *left; // 左孩子
TreeNode *right; // 右孩子
TreeNode(int v) : val(v), left(nullptr), right(nullptr) {}
};
1、树的先序、中序、后序遍历
递归方式:
//树的遍历
void Order(TreeNode *root)
{
if (root == nullptr)
{
return;
}
cout << root->val << endl; // 先序遍历
Order(root->left);
cout << root->val << endl; // 中序遍历
Order(root->right);
cout << root->val << endl; // 后序遍历
}
非递归方式:
//先序遍历
void preOrder(TreeNode* root)
{
if (root == nullptr)
{
return;
}
stack<TreeNode*> stk;
stk.push(root); // 根节点入栈
while(!stk.empty())
{
TreeNode *node = stk.top();
stk.pop(); // 保存当前栈顶节点并弹出打印
cout<< node->val <<' ';
//注意这里,要先向栈中压入右孩子,再压入左孩子
if (node->right != nullptr)
{
stk.push(node->right);
}
if (node->left != nullptr)
{
stk.push(node->left);
}
}
}
先序遍历为何要先向栈中压入右孩子,在压入左孩子:
栈的特性是先入后出、后入先出,因此后放放入入的左孩子会先出栈打印,先放入的右孩子会后出栈,以此来保持 先序遍历的特点:根左右。
先序遍历说完了之后先不讲中序遍历,先讲一下后续遍历。我们想根据先序遍历的入栈方式,即右孩子先入栈,左孩子后入栈,我们便得到了根左右的打印方式,对于后续遍历,我们要是按照左右的方式入栈,就会得到右左的出栈顺序,先打印根节点,以此得到的顺序为根右左 ,此时我们要是再准备一个栈,将根右左进栈,再弹出,是不是就有了左右根的打印顺序,即后续遍历。
// 后续遍历
void posOrder(TreeNode *root)
{
if (root == nullptr)
{
return;
}
stack<TreeNode *> s1;
stack<TreeNode *> s2;
s1.push(root);
while (!s1.empty())
{
TreeNode *node = s1.top();
s1.pop();
s2.push(node); // 将s1弹出的节点先不打印,而是压入s2中
if (node->left != nullptr)
{
s1.push(node->left);
}
if (node->right != nullptr)
{
s2.push(node->right);
}
}
while (!s2.empty()) // 树的全部节点已经放入s2中,打印后得到后序遍历
{
TreeNode *cur = s2.top();
s2.pop();
cout << cur->val << ' ';
}
}
最后则是中序遍历,也是我感觉最难的,我会尽我最大努力讲,若实在听不懂还是建议去找左神的视频。
// 中序遍历
void InOrder(TreeNode *root)
{
if (root == nullptr)
{
return;
}
stack<TreeNode *> s1;
while (!s1.empty() || root != nullptr)
{
if (root != nullptr) // 首先将此时根节点的所有左孩子全部先压入栈中
{
s1.push(root);
root = root->left;
}
else /*没有左孩子后,则弹出一个节点,即此时子树的根节点,也是其父亲的左孩子,弹出打印,
去看此时子树的右孩子,将遍历右孩子树的全部左孩子,以此类推实现中序遍历*/
{
root = s1.top();
s1.pop();
cout << root->val << ' ';
root = root->right;
}
}
}
我尽我可能的说了,总的来说脑子里要有这个打印过程,确实不好理解,还是看左神的讲解更好。
2、树的层序遍历
层序遍历也即二叉树的宽度优先遍历,就是一层一层的遍历,为了实现层序遍历,需要用的的容器则是队列,因为队列先进先出的特性,将左孩子先放进去就先打印左孩子,右孩子后进去就后打印右孩子。
// 层序遍历
void levelOrder(TreeNode *root)
{
if (root == nullptr)
{
return;
}
queue<TreeNode *> que;
que.push(root);
while (!que.empty())
{
root = que.front();
que.pop();
cout << root->val << endl;
if (root->left != nullptr)
{
que.push(root->left);
}
if (root->right != nullptr)
{
que.push(root->right);
}
}
}
3、求二叉树最宽的层有多少节点
对于树的问题,大多数都可以用树形dp去解决问题,但是我想分享不使用树形dp去解决本问题的方法。
依次去遍历树的每一层,同时还有一个变量去记录本层节点数量,在上一次层我们就已经设置好了当前层的结束节点位置,当遍历本层到结束节点后,比较本层宽度与最大宽度,将下一层的结束节点赋给curend,因为马上要遍历下一层了,依次遍历完就得到了最大宽度。
int maxWidth(TreeNode *head)
{
queue<TreeNode *> que; // 树的遍历队列
que.push(head);
TreeNode *curend = head; // 当前层的结束节点
TreeNode *nextend = nullptr; // 下一层的结束节点
int maxWidth = 0; // 最大宽度
int curWidth = 0; // 当前层宽度
while (!que.empty())
{
TreeNode *cur = que.front();
if (cur->left != nullptr)
{
nextend = cur->left;
que.push(cur->left);
}
if (cur->right != nullptr)
{
nextend = cur->right;
que.push(cur->right);
}
curWidth++;
if (cur == curend)
{
maxWidth = max(curWidth, maxWidth);
curend = nextend;
curWidth = 0;
}
que.pop();
}
return maxWidth;
}
4、判断树是否是完全二叉树
重点是对下面这行代码的理解:
leaf && (l != nullptr || r !=nullptr) || l == nullptr && r != nullptr
leaf 标志了是否在碰到了左右不双全的节点,初始为false,若果碰到了则置为true。
leaf变为true的情况一般是遍历到了最后一层,出现了左或右孩子为空的情况。
leaf && (l != nullptr || r !=nullptr) : 若之前碰到了左右不双全的节点并且又碰到了左孩子不为空或右孩子不为空的情况,则返回false。
l == nullptr && r != nullptr :这个较好理解,左孩子为空,右孩子不为空,那么一定不是完全二叉树。
bool isCBT(TreeNode *head)
{
queue<TreeNode *> que;
bool leaf = false; // 标志:是否碰到了左右不双全的节点
TreeNode *l = nullptr;
TreeNode *r = nullptr;
que.push(head);
while(!que.empty())
{
head = que.front();que.pop();
l = head->left;
r = head->right;
if (leaf && (l != nullptr || r !=nullptr) || l == nullptr && r != nullptr)
{
return false;
}
if (l != nullptr)
{
que.push(l);
}
if (r != nullptr)
{
que.push(r);
}
if (l == nullptr || r == nullptr)
{
leaf = true;
}
}
return true;
}
关于树的常用算法还有很多很多,例如二叉树的序列化、反序列化、二叉树与多叉树的转化等等,就先聊到这吧,可以观看左神视频学习更多。我就说了两个我认为很具有启发性的两个问题,关于什么是启发性,我也说不上来,就是感觉。
最后的最后我想在感叹一点,输出内容怎么这么累啊,自己学起来倒没感觉那么累,越来越敬佩那些大佬们了,希望自己能坚持多输出一些吧。
一起加油!!!!!!!!!!!