代码随想录算法训练营记录

今日内容:

20.有效的括号

其实这题跟最后一个逆波兰表达式有关,最后一题是逆波兰表达式求值,但是根据中缀表达式生成逆波兰表达式的算法里就会用到栈来处理中缀中的括号问题。

所以一个栈直接秒了,思路打开,碰到左括号别傻傻push左括号,而得push右括号,这样就可以直接判断top()了,而不用碰到右括号的时候再来个转换。

没错,我这次就push的左括号,碰到右括号的时候还用ASCII码去算对应的左括号值

代码

class Solution {
public:
    bool isValid(string s) {
        if(s.size() % 2 != 0) return false;
        stack<char> b;
        for(int i = 0;i < s.size();i++) {
            if(s[i] == '(') b.push(')');
            else if(s[i] == '[') b.push(']');
            else if(s[i] == '{') b.push('}');
            else if(b.empty() || s[i] != b.top()) return false;
            else b.pop();
        }
        return b.empty();
    }
};

1047.删除字符串中所有相邻重复项

如果不告诉用栈做的话,貌似还挺复杂的,不过用栈就很简单了

压栈前判断栈顶是不是重复,重复就pop,不重复就push,建议从尾到头遍历s,这样全pop出来时顺序还是对的。

代码

class Solution {
public:
    string removeDuplicates(string s) {
        stack<char> st;
        int i = s.size() - 1;
        for(;i >= 0;i--) {
            if(st.empty() || st.top() != s[i]) st.push(s[i]);
            else st.pop();
        }
        string ans;
        for(i = 0;!st.empty();i++) {
            ans.push_back(st.top());
            st.pop();
        }
        return ans;
    }
};

105.逆波兰表达式求值

笔者大一下的Qt课设就是写一个大数计算器,对这逆波兰表达式还是比较熟悉,有了式子,求值就比较简单了,这个题还确保了int不炸。

代码

class Solution {
public:
    int evalRPN(vector<string>& tokens) {
        stack<int> st;
        for(int i = 0;i < tokens.size();i++) {
            if(tokens[i] == "+" || tokens[i] == "-" || tokens[i] == "*" || tokens[i] == "/") {
                int num1 = st.top();
                st.pop();
                int num2 = st.top();
                st.pop();
                if(tokens[i] == "+") st.push(num1 + num2);
                if(tokens[i] == "-") st.push(num2 - num1);
                if(tokens[i] == "*") st.push(num1 * num2);
                if(tokens[i] == "/") st.push(num2 / num1);
            }
            else
                st.push(stoi(tokens[i]));
        }
        int res = st.top();
        st.pop();
        return res;
    }
};

第13天

今日内容:

239.滑动窗口最大值

考验对于priority_queue数据结构的了解和掌握程度,不过不能当API选手,得知道怎么手写堆,不求随手手撕出大小顶堆,但是得知道大概写法。

关于priority_queue的感性理解

在lc上看见了评论区大佬,关于priority_queue的比喻描述很形象,特引用至此:

单调队列真是一种让人感到五味杂陈的数据结构,它的维护过程更是如此…就拿此题来说,队头最大,往队尾方向单调…有机会站在队头的老大永远心狠手辣,当它从队尾杀进去的时候,如果它发现这里面没一个够自己打的,它会毫无人性地屠城,把原先队里的人头全部丢出去,转身建立起自己的政权,野心勃勃地准备开创一个新的王朝…这时候,它的人格竟发生了一百八十度大反转,它变成了一位胸怀宽广的慈父!它热情地请那些新来的“小个子”们入住自己的王国…然而,这些小个子似乎天性都是一样的——嫉妒心强,倘若见到比自己还小的居然更早入住王国,它们会心狠手辣地找一个夜晚把它们通通干掉,好让自己享受更大的“蛋糕”;当然,遇到比自己强大的,它们也没辙,乖乖夹起尾巴做人。像这样的暗杀事件每天都在上演,虽然王国里日益笼罩上白色恐怖,但是好在没有后来者强大到足以干翻国王,江山还算能稳住。直到有一天,闯进来了一位真正厉害的角色,就像当年打江山的国王一样,手段狠辣,野心膨胀,于是又是大屠城…历史总是轮回的。

似乎没办法贴评论的链接,去description下找吧,应该挺靠前的。

叛军屠城 = 遇到新最值,全弹出; 慈悲为怀 = 后续小值有序堆在队头之后;

代码

class Solution {
public:
    vector<int> maxSlidingWindow(vector<int>& nums, int k) {
        priority_queue<pair<int, int>> pq;//存pair,得带上值得下标方便确定是不是该出队
        vector<int> ans;
        for(int i = 0;i < nums.size();i++) {
            if(pq.empty()) pq.push(pair(nums[i], i));//空的直接存
            else if(nums[i] >= pq.top().first) {//新最值,全出队
                while(!pq.empty()) pq.pop();
                pq.push(pair(nums[i], i));
            }
            else if(pq.size() >= k) {//已满
                pq.push(pair(nums[i], i));
                while(pq.top().second + k <= i)//出队已经晚了的,注意得是while
                    pq.pop();
            }
            else pq.push(pair(nums[i], i));
            if(i + 1 >= k) ans.push_back(pq.top().first);
        }
        return ans;
    }
};

347.前k个高频元素

感觉思路比较简单暴力,用map来记“值-频率”,然后根据“频率数组”建堆排序来降低时间复杂度到O(nlogn)以下。总之是先记再排序。

不过carl的反其道而行很巧妙,采用小根堆,这样就可以简单地根据队列盈满来出队队头,官解也是小根堆,不过没有简单出队,而是判断当前的和队头的哪个更小,如果队头更小才出队。

对于pq自定义排序标准的语法不了解,是看过之后才写的,对于这道题

priority_queue的模板类型参数有三,1.要存的类型;2.要存的类型的vector;3.自定义比较方法所在类,自定义比较需重载()运算符

代码

class Solution {
public:
    class cmp{
    public:
        bool operator()(const pair<int, int> &a, const pair<int, int> &b) {
            return a.second > b.second;
        }
    };
    vector<int> topKFrequent(vector<int>& nums, int k) {
        //用map记录值-频率,通过优先队列或者堆来对前k个频率排序,最后输出前k个元素
        unordered_map<int, int> map;
        for(int i : nums) map[i]++;

        priority_queue<pair<int, int>, vector<pair<int, int>>, cmp> pq;
        for(auto it : map) {
            pq.push(it);
            if(pq.size() > k) pq.pop();
        }
        vector<int> ans;
        while (k--){
            int t = pq.top().first;
            ans.push_back(t);
            pq.pop();
        }
        return ans;
    }
};

day14

今日内容:

  • 递归遍历
  • 迭代遍历
  • 统一迭代

三道例题:

前序遍历二叉树
中序遍历二叉树
后续遍历二叉树

递归遍历

太过简单,skip

迭代遍历(非统一版)

使用栈模拟递归过程:

前序就是先访问当前节点值,然后压栈右左孩子

vector<int> preorderTraversal(TreeNode* root) {
    if(root == nullptr) return {};
    stack<TreeNode*> st;
    TreeNode * cur = root;
    st.push(cur);
    vector<int> ans;
    while(!st.empty()) {
        cur = st.top();
        st.pop();
        ans.push_back(cur->val);
        if(cur->right) st.push(cur->right);
        if(cur->left) st.push(cur->left);
    }
    return ans;
}

中序就是先存所有左节点,直到遇null再出栈栈顶,访问值后压栈右节点(压栈的所有节点均不为空)

vector<int> inorderTraversal(TreeNode* root) {
    if(!root) return {};
    stack<TreeNode*> st;
    vector<int> ans;
    TreeNode * cur = root;
    while(!st.empty() || cur != nullptr) {
        if(cur != nullptr) {
            st.push(cur);
            cur = cur->left;
        }
        else {
            cur = st.top();
            st.pop();
            ans.push_back(cur->val);
            cur = cur->right;
        }
    }
    return ans;
}

后序比较讨巧,左右中倒序就是中右左,把前序的压栈顺序调换,最后翻转结果就行,就不贴代码了

统一迭代遍历

形式统一的迭代遍历,主要思想是压栈null标记下一个节点需要访问,这样写出来的代码在压栈部分就可以只调换顺序实现三种遍历

个人感觉比较好理解,最好记住写法,下面以后序为例给出代码

vector<int> postorderTraversal(TreeNode* root) {
    vector<int> ans;
    stack<TreeNode *> st;
    TreeNode * cur = root;
    if(cur != nullptr)st.push(cur);
    while(!st.empty()) {
        cur = st.top();
        if(cur != nullptr) {
            st.pop();

            st.push(cur);
            st.push(nullptr);//中

            if(cur->right) st.push(cur->right);//右

            if(cur->left) st.push(cur->left);//左
        }
        else {
            st.pop();
            cur = st.top();
            ans.push_back(cur->val);
            st.pop();
        }
    }
    return ans;
}

day 15

今日内容:

- 102.层序遍历
- 226.翻转二叉树
- 101.对称二叉树

层序遍历

思路就是用队列记录逐层,这样顺序不会变。进入一层时最好记录队列初长度,然后根据长度遍历该层,避免根据队列是否空而判断该层是否遍历结束,便于即时将子节点入队

vector<vector<int>> levelOrder(TreeNode* root) {
    vector<vector<int>> ans;
    vector<int> temp;
    if(root == nullptr) return {};
    TreeNode * cur = root;
    queue<TreeNode*> q;
    q.push(cur);
    while(!q.empty()) {
        int size = q.size();
        for (int i = 0; i < size; i++) {
            cur = q.front();
            q.pop();
            temp.push_back(cur->val);
            if (cur->left) q.push(cur->left);
            if (cur->right) q.push(cur->right);
        }
        ans.push_back(temp);
        temp.clear();
    }
    return ans;
}

翻转二叉树

递归版比较简单,太过简单,所以skip

迭代版就看作在遍历,而且是前序遍历那种单循环,出栈后把左右节点交换,然后压栈左右节点继续就行,给出循环部分的核心代码本来就是核心代码模式,又再核心……

while(!st.empty()) {
    TreeNode* node = st.top();              // 中
    st.pop();
    swap(node->left, node->right);
    if(node->right) st.push(node->right);   // 右
    if(node->left) st.push(node->left);     // 左
}

对称二叉树

递归版很简单,写一个辅助函数判断左右节点是不是相等,是树就接着递归,然后从根开始对每一个分支节点的左右孩子判断就行
代码没写,偷了个懒😜

迭代版要难一点,仅限于手写层面,思路不难

迭代需要用队列或者栈等来存,但是不是按左右顺序挨个入队,而是左右对应交替入队,这样方便判断是否相等

下附carl的漂亮含注释代码,carl原文链接

class Solution {
public:
    bool isSymmetric(TreeNode* root) {
        if (root == NULL) return true;
        queue<TreeNode*> que;
        que.push(root->left);   // 将左子树头结点加入队列
        que.push(root->right);  // 将右子树头结点加入队列
        
        while (!que.empty()) {  // 接下来就要判断这两个树是否相互翻转
            TreeNode* leftNode = que.front(); que.pop();
            TreeNode* rightNode = que.front(); que.pop();
            if (!leftNode && !rightNode) {  // 左节点为空、右节点为空,此时说明是对称的
                continue;
            }

            // 左右一个节点不为空,或者都不为空但数值不相同,返回false
            if ((!leftNode || !rightNode || (leftNode->val != rightNode->val))) {
                return false;
            }
            que.push(leftNode->left);   // 加入左节点左孩子
            que.push(rightNode->right); // 加入右节点右孩子
            que.push(leftNode->right);  // 加入左节点右孩子
            que.push(rightNode->left);  // 加入右节点左孩子
        }
        return true;
    }
};

day16

今日内容:

- 104.二叉树的最大深度
- 559.n叉树的最大深度
- 111.二叉树的最小深度
- 222.完全二叉树的节点个数

树的最大深度

最大深度指从根到所有节点的长度中最长的那一个,换言之就是要找离根最远的节点然后返回到它的长度。

用dfs和bfs都行,分别代表递归前后序遍历和层序遍历,对于n叉树而言,仅仅是多比较几次而已,改写难度不大

下附对于n叉树的bfs遍历

int maxDepth(Node* root) {
    queue<Node *> q;
    if(root) q.push(root);
    int ans = 0;
    while(!q.empty()) {
        int size = q.size();
        for(int i = 0;i < size;i++) {
            Node * cur = q.front();
            q.pop();
            for(int j = 0;j < cur->children.size();j++) {
                q.push(cur->children[j]);
            }
        }
        ans++;
    }
    return ans;
}

二叉树的最小深度

最小深度需要注意,是从根到最近的叶子节点的距离,叶子节点指没有左右孩子的节点

所以在遍历时需要注意结束条件,对于层序遍历则判断当前节点是否是叶子,如果是就维护最小深度

对于递归遍历则根据子节点个数来分类处理,如果左右双全或双无,则直接递归;如果只有一个,就单独递归

下附递归遍历代码:

int minDepth(TreeNode* root) {
    if(!root) return 0;
    
    int left = minDepth(root->left);
    int right = minDepth(root->right);

    if(root->left == nullptr && root->right) {
        return 1 + right;
    }
    if(root->right == nullptr && root->left) {
        return 1 + left;
    }
    return 1 + min(left, right);
}

留坑,lc的最快执行代码中在最后return前把root的左右都指null,意义不明,但是就是快,没想出来为什么

完全二叉树的节点个数

用普通二叉树的遍历当然能做,只是不太好,还是用好完全二叉树的特性:非底层全满,底层从左往右堆

所以完全二叉树的左右子树深度肯定是一样的,如果不一样,那么再递归,直到递归到完全二叉树或者细粒度足够小时的空节点

代码贴的carl的,原文链接:代码随想录 | 完全二叉树的节点个数

int countNodes(TreeNode* root) {
    if (root == nullptr) return 0;
    TreeNode* left = root->left;
    TreeNode* right = root->right;
    int leftDepth = 0, rightDepth = 0; // 这里初始为0是有目的的,为了下面求指数方便
    while (left) {  // 求左子树深度
        left = left->left;
        leftDepth++;
    }
    while (right) { // 求右子树深度
        right = right->right;
        rightDepth++;
    }
    if (leftDepth == rightDepth) {
        return (2 << leftDepth) - 1; // 注意(2<<1) 相当于2^2,所以leftDepth初始为0
    }
    return countNodes(root->left) + countNodes(root->right) + 1;
}

day 17

今日内容:

● 110.平衡二叉树
● 257. 二叉树的所有路径
● 404.左叶子之和

平衡二叉树

只是判断平衡二叉树,比较简单,按规范化思路来吧,避免一会有感觉秒了,一会没感觉卡了

递归结束条件:如果左子树不是平衡二叉树 或者 右子树不是平衡二叉树 或者 左右子树深度差距大于1

递归操作:判断左子树是不是平衡二叉树,判断右子树是不是平衡二叉树,获取左右子树深度

参数及返回值:根节点 + 是否合法的bool值

原创AC代码:

bool isBalanced(TreeNode* root) {
    if(root == nullptr) return true;//空视作平衡
    if(isBalanced(root->left) && isBalanced(root->right)) {//左右都是
        return abs(getDepth(root->left) - getDepth(root->right)) <= 1;//左右深度是否匹配
    }
    else return false;
}
int getDepth(TreeNode * root) {
    if(root == nullptr) return 0;//空树深度0
    int ans = 1;
    return max(getDepth(root->left), getDepth(root->right)) + 1;//左右子树最大深度加自己
}

这个时间复杂度较大,O(n^2),对每一个节点都要单独求深度然后判断,自顶向下

自底向上做法:

bool isBalanced(TreeNode* root) {
    if(root == nullptr) return true;
    return getDepth(root) != -1;
}
int getDepth(TreeNode * root) {
    if(root == nullptr) return 0;
    int left = getDepth(root->left);
    if(left == -1) return -1;//只要一个子树不平衡,整个树就不平衡
    int right = getDepth(root->right);
    if(right == -1) return -1;
    return abs(left - right) <= 1 ? max(left, right) + 1 : -1;
}

二叉树的所有路径

递归三步:

  1. 参数&返回值

无需返回值,参数有根节点和存路径和答案的数组

  1. 递归终止条件

遇到叶节点

  1. 递归逻辑

没遇到就接着往里插

比较简单,贴代码:

class Solution {
public:
    vector<string> ans;
    vector<string> binaryTreePaths(TreeNode* root) {
        if(!root) return {};
        string line;
        traversal(root, line);
        return ans;
    }
    void traversal(TreeNode * root, string s) {
        s += to_string(root->val);
        if(!root->left && !root->right) ans.push_back(s);
        else {
            s += "->";
            if(root->left) traversal(root->left, s);
            if(root->right) traversal(root->right, s);
        }
    }
};

迭代写法

用一个栈存节点,一个栈存目前已经走过的路径
注意push根节点和其他节点的差异

class Solution {
public:
    vector<string> binaryTreePaths(TreeNode* root) {
        vector<string> ans;
        if(!root) return ans;
        stack<string> path;
        stack<TreeNode *> st;
        st.push(root);
        path.push(to_string(root->val));//only value
        while(!st.empty()) {
            TreeNode * cur = st.top(); st.pop();
            string tem = path.top(); path.pop();

            if(cur->left) {
                st.push(cur->left);
                path.push(tem + "->" + to_string(cur->left->val));//insert the next value
            }
            if(cur->right) {
                st.push(cur->right);
                path.push(tem + "->" + to_string(cur->right->val));
            }
            if(!cur->left && !cur->right) {//no next value
                ans.push_back(tem);
            }
        }
        return ans;
    }
};

左叶子之和

需要注意,单独一个根节点不能称作左叶子,只是叶子,但不左

class Solution {
public:
    int sumOfLeftLeaves(TreeNode* root) {
        if(!root) return 0;
        int ans = 0;
        if(root->left && !root->left->left && !root->left->right) {
            ans = root->left->val;
        }
        return ans + sumOfLeftLeaves(root->left) + sumOfLeftLeaves(root->right);
    }
};

这个递归看得有点懵,后面再来仔细理解一下吧

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值