文章目录
刷题记录2
1047.删除字符串中的所有相邻重复项
cpp
class Solution {
public:
string removeDuplicates(string s) {
string res = "";
for(char a : s)
{
if(res.empty() || res.back() != a) res.push_back(a);
else if(res.back() == a) res.pop_back();
}
return res;
}
};
像这种消消乐类型的题目都是用到的栈这种数据结构来解决问题,而这里呢是将string来代替栈,就不需要用到stack容器,如果使用stack容器还需要将容器内的元素重新遍历赋值到string中,然后还需要一次reverse反转,如果直接使用string作为栈就节省了代码和时间。这里在解题的时候遇到了一个空间性能上的一个问题,就是我将res.push_back(a)改成了res = res+a这种写法会导致内存超限,并且耗时将近500ms。我就在像这是为什么呢?两个操作明明都是在字符串尾部添加字符a,为什么空间和时间性能就相差这么大呢?通过搜索得知:res = res+a这种操作是会重新生成一个string临时变量,先将原本的res拷贝到临时变量中再添加上a再返回给res,这个操作又是在循环当中,假如题目中给出的s十分长,那么就相当于是On^2的时间复杂度了,再加上每个循环开辟一个临时变量,所以空间消耗也十分大。对于res.push_back来说它仅仅只是在res的基础上在后面添加字符a,这样要比运算符+要节省空间和时间很多。所以在遇到对字符串进行添加或删除时最好用push_back和pop_back。其他相关函数front(), back(), append()。
150.逆波兰表达式求值
cpp
class Solution {
public:
int evalRPN(vector<string>& tokens) {
stack<int> s;
for(int i = 0; i < tokens.size(); i++)
{
if(tokens[i] == "+")
{
int num1 = s.top();
s.pop();
s.top() += num1;
}
else if(tokens[i] == "-")
{
int num1 = s.top();
s.pop();
s.top() -= num1;
}
else if(tokens[i] == "*")
{
int num1 = s.top();
s.pop();
s.top() *= num1;
}
else if(tokens[i] == "/")
{
int num1 = s.top();
s.pop();
s.top() /= num1;
}
else s.push(stoi(tokens[i]));
}
return s.top();
}
};
这一题其实只要读懂题目的意思就行了,然后按照题目给的要求编程就行了,关键是要明白这个题要用栈来写,看到提示:遇到数字则入栈,遇到运算符则取出栈顶两个数字进行计算,并将结果压入栈中,这里着重强调一下一个数字字符串转换数字的方法:int num = stoi("12345");但是呢,好像int num = "12345"也是能实现的,都记住吧,技多不压身。。。
239.滑动窗口最大值
cpp
class Solution {
public:
vector<int> maxSlidingWindow(vector<int>& nums, int k) {
vector<int> ans;
deque<int> dq;
for(int i = 0; i < nums.size(); i++)
{
//入
while(!dq.empty() && nums[i] >= nums[dq.back()]) dq.pop_back();
dq.push_back(i);
//出
if(i-dq.front() >= k) dq.pop_front();
//记录数据
if(i >= k-1) ans.push_back(nums[dq.front()]);
}
return ans;
}
};
以前绝对遇到过这种题!我记得当时我单纯用for循环嵌套都写不出来着,现在至少能用for循环嵌套能够写出来了,当然如果暴力的话怎么可能解决力扣的困难题呢,果然不出意外的运行超时了。当然有提升是肯定的,加油!
这里用到的数据结构是单调队列,做这么多题下来好像确实没怎么写过队列的题目。这里用到的思想是维护一个单调的队列来记录nums[i]的值,这里题目求的是滑动窗口的最大值,那么我们就需要维护一个单调递减的一个队列,将最大的元素一直停留在队列的首位。首先是入队操作,循环判断队尾的元素是不是小于要插入进来的元素,如果尾元素小于要插入的元素的话就不断地将尾元素剔除,目的就在于使这个队列是递减的。然后就是出操作:插入元素后通过下标的循环不变量i-dq.front() >= k时就需要将首元素剔除掉。最后是记录数据操作,由于不同测试样例的窗口大小不同,所以前k个数据插入的时候是不需要记录数据的,所以当下标i=k-1的时候才开始记录结果。
347.前k个高频元素
144.二叉树前序遍历(145、94后序、中序)
递归法:
cpp
class Solution {
public:
void preorder(TreeNode* root, vector<int>& res)
{
if(root == nullptr) return;
res.push_back(root->val);
preorder(root->left, res);
preorder(root->right, res);
}
vector<int> preorderTraversal(TreeNode* root) {
vector<int> res;
preorder(root, res);
return res;
}
};
中序和后序的递归方法还是很简单的,就是分别将res.push_back(root->val);放到两个递归函数的中间和后面,这样就分别达到了中序遍历和后序遍历的效果。
迭代法:
前序
cpp
class Solution {
public:
vector<int> preorderTraversal(TreeNode* root) {
stack<TreeNode*> s;
vector<int> res;
if(root == nullptr) return {};
s.push(root);
while(!s.empty())
{
TreeNode* temp = s.top();
s.pop();
res.push_back(temp->val);
if(temp->right) s.push(temp->right);
if(temp->left) s.push(temp->left);
}
return res;
}
};
首先回顾前序遍历,它的递归方式是中左右。这样就不难写出前序遍历的递归写法。题目中的提升中给出要写出其迭代写法,最开始还是没有思路的,甚至想到的是用队列这种数据结构,但是经过脑袋一次思考之后回想起用队列的话是实现树的层序遍历,这并不能达到前中后序遍历的效果。看了一眼题解发现用的是栈,而且在使用栈的时候也是有需要注意的点:首先是入栈要判断树是否为空树,再就是入栈顺序,这里是右子树先入栈,左子树后入栈,由于栈是先入后出,所以是先右后左,还有就是入栈前需要对左右节点进行判空操作,如果为空那么就不能入栈,如果入栈了的话会导致空指针异常。
后序
cpp
class Solution {
public:
vector<int> postorderTraversal(TreeNode* root) {
stack<TreeNode*> s;
vector<int> res;
if(root == nullptr) return {};
s.push(root);
while(!s.empty())
{
TreeNode* temp = s.top();
res.push_back(temp->val);
s.pop();
if(temp->left) s.push(temp->left);
if(temp->right) s.push(temp->right);
}
reverse(res.begin(), res.end());
return res;
}
};
最开始想借着前序遍历的写法来写后序遍历,结果还是卡住了,卡尔给出的方法也实在是妙啊,前序遍历是中左右,而后序遍历是左右中,它采用的方法就是在指针入栈方面进行了改动,先将左右指针入栈的顺序颠倒了一下,就使结果变成了中右左,然后将res数组进行翻转就变成了最后的左右中,我天,这是人能想出来的办法吗555,真的佩服!
中序
cpp
class Solution {
public:
vector<int> inorderTraversal(TreeNode* root) {
stack<TreeNode*> s;
vector<int> res;
TreeNode* cur = root;
while(!s.empty() || cur != nullptr)
{
if(cur != nullptr)
{
s.push(cur);
cur = cur->left;
}
else
{
cur = s.top();
s.pop();
res.push_back(cur->val);
cur = cur->right;
}
}
return res;
}
};
中序和其他两个差别还是很大的,主要难点在于用栈来模拟递归的逻辑这一点第一次来写确实有点难写,这方面还是有点薄弱,虽说考试不可能考迭代法写二叉树遍历、但这也是考试和以后面试的两手准备吧!
102.二叉树的层序遍历
cpp
class Solution {
public:
vector<vector<int>> levelOrder(TreeNode* root) {
deque<TreeNode*> dq;
vector<vector<int>> res;
if(root == nullptr) return {};
dq.push_back(root);
while(!dq.empty())
{
int n = dq.size();
vector<int> temp;
for(int i = 0; i < n; i++)
{
TreeNode* tmp = dq.front();
dq.pop_front();
temp.push_back(tmp->val);
if(tmp->left) dq.push_back(tmp->left);
if(tmp->right) dq.push_back(tmp->right);
}
res.push_back(temp);
}
return res;
}
};
这也是第二次做这道题了,唉又没做出来,还是忘得快啊,这才过了一个多月的题,就忘得一干二净了,当时在思考的时候还是想到的是以前自己的思路,就是没有回忆到以前题解的思路。这一题的层序遍历与其他不同点就在于每层的数据是一个一维数组,然后最后的结果是一个二维数组。其实感觉自己也是把题目想复杂化了,其实就是层多少个元素就将每层的元素传入到临时的一维数组中,多少个元素直接用dq.size()就能获取到。唉,给我最大的感觉就是还得回头看,重点的确不是在于多,还是在于你要掌握这个方法。
226.翻转二叉树
cpp
class Solution {
public:
void reverse(TreeNode* root)
{
if(root == nullptr) return;
swap(root->left, root->right);
if(root->left) reverse(root->left);
if(root->right) reverse(root->right);
}
TreeNode* invertTree(TreeNode* root) {
reverse(root);
return root;
}
};
最开始我想着能不能取巧只交换节点的值是否能够达到效果,答案是不能,不能被题干中的示例给迷惑了,可能出现问题的情况是一个节点可能只有左右子树中的一个,这样就不能够通过交换左右节点的值来达到交换的效果,会出现空指针异常的错误。所以这里只能够用交换指针来达到目的,最开始我也对交换完指针之后是否会对后续递归产生问题产生疑惑,其实是不会的,无论是左右节点哪个先递归,最终目的是让所有其左右节点指针互换就行,所以只需要关注遍历整个树,这样就能达到我们最后想要的效果。
101.对称二叉树
cpp
class Solution {
public:
bool isSame(TreeNode* p, TreeNode* q)
{
//这一行包含了很多条件:
//1、左空右非空返回false
//2、左非空右空返回false
//3、左右都空返回true
if(p == nullptr || q == nullptr) return p == q;
//左右都非空
bool outside = isSame(p->left, q->right);
bool inside = isSame(p->right, q->left);
bool val = p->val == q->val;
return val && outside && inside;
}
bool isSymmetric(TreeNode* root) {
return isSame(root->left, root->right);
}
};
打开这一道题显示是1个月前做的题,现在一看,感觉跟新题没什么差别,唉还是没有思路。感觉前面跟灵神学做的算法题还是过于草率了,慢慢看出差别了,灵神讲题更注重于代码简洁和方法的灵活上面,这对于我们这种普通人来说接受起来只有那种昙花一现的效果,并不能将方法牢记于心,卡尔讲题确确实实落实在了干货上面,落在了基础上面,这才是真正的好讲师啊!
对于这一次提交的代码,我结合了灵神和卡尔的思路。首先对于递归退出的条件我只用一行解决了,但那一行代表的含义是丰富的,具体请看代码注释,再就是左右都非空的情况,这里就要用到后序遍历,为什么用后序遍历?卡尔给出的解释是:由于每一个节点都需要其孩子的真假信息才能判断当前节点的真假信息,需要将孩子的信息回溯到当前节点综合判断,所以用到的是后序遍历,解决二叉树问题无非就是四种遍历方式,但是具体采用哪一种一定要根据题意来采用,不同的遍历方式对于不同的题目产生的答案有可能不同,有时候一个题可以采用多种遍历方式,而有些题仅能采用某一种方式解题,这就是我之前刷题所遗漏的地方。最后解释一下val那一行,先进行的 == 运算,==运算的结果返回到val中,那一行也能写成bool val = p->val == q->val ? 1 : 0;还有就是return返回的值需要val、outside和inside综合判断真价值
这一道题给了我很大的感触吧,刷题确实不能图快,有时回过头来做的题其实又是一个新题。。。不能一个劲的追求简洁,扎实才是王道啊!
104.二叉树的最大深度
cpp
class Solution {
public:
int func(TreeNode* root)
{
if(root == nullptr) return 0;
int leftNum = func(root->left);
int rightNum = func(root->right);
return max(leftNum, rightNum)+1;
}
int maxDepth(TreeNode* root) {
return func(root);
}
};
这里给出二叉树的两个定义:深度:指节点到根节点的距离,根节点的深度为1;高度:节点到叶子节点的距离,叶子节点的高度为1。所以求深度最好是用前序遍历采用迭代方法比较好,但是写的话会比较麻烦,这里采用的是后序遍历,为什么会是后序遍历呢?题干求的是最大深度,那么是不是可以理解成求二叉树的最大高度呢?求最大高度也就是求根节点的高度,这样由叶子节点一步一步往上传递就用到的是后序遍历。
111.二叉树的最小深度
cpp
class Solution {
public:
int minDepth(TreeNode* root) {
if(root == nullptr) return 0;
int leftnum = minDepth(root->left);
int rightnum = minDepth(root->right);
if(root->left == nullptr && root->right != nullptr) return 1+rightnum;
else if(root->right == nullptr && root->left != nullptr) return 1+leftnum;
else return min(leftnum, rightnum)+1;
}
};
整体上与前面一道题是相同的解法,也是用到的后序遍历的方法,但是再返回值上有一点不同,需要对二叉树的各种情况进行讨论,先是左空右不空的情况--->返回根节点右子树的最小深度+1、右空左不空与前面上一个情况相反,最后是都不空的情况,直接返回根节点的最小深度。
结合前面的对称二叉树的题目,我发现其实在一般情况你写完提交后不能通过全部样例时就需要考虑二叉树的几个特殊情况:左空右不空、左不空右空、左右都不空、左右都空。
222.完全二叉树的节点个数
方法一:
cpp
class Solution {
public:
int countNodes(TreeNode* root) {
if(root == nullptr) return 0;
return countNodes(root->left) + countNodes(root->right)+1;
}
};
方法二:
cpp
class Solution {
public:
int countNodes(TreeNode* root) {
if(root == nullptr) return 0;
TreeNode* p_left = root->left, *p_right = root->right;
int leftDepth = 1, rightDepth = 1;
while(p_left)
{
leftDepth++;
p_left = p_left->left;
}
while(p_right)
{
rightDepth++;
p_right = p_right->right;
}
if(leftDepth == rightDepth) return pow(2, leftDepth)-1;
int leftNum = countNodes(root->left);
int rightNum = countNodes(root->right);
return leftNum+rightNum+1;
}
};
方法一用的普通的后序遍历求完全二叉树的个数,因为遍历了整棵树所以时间复杂度是On的,而方法二用到了题干给出的完全二叉树的性质,利用完全二叉树的性质使时间复杂度降低了。具体的逻辑:在递归到每个节点的时候写一段代码来判断以当前节点为根节点的二叉树是否为完全二叉树,如果是,直接将2^深度-1返回给上一级,如果不是那么就继续后序遍历该树。如何判断一个子树是否为完全二叉树?--->由于题目中给出的树一定是完全二叉树,那么我们只需要从当前节点定义两个指针,分别一直往左迭代和往右迭代直到指为空,迭代的过程中用两个int型的变量记录各自的深度,最后判断两个深度是否相等,如果相等那么这个子树就是完全二叉树,反之则不是。