跟着labuladong猛猛刷题(持续更新)

Day 1:

  1. 两个栈可以实现队列,一个队列就可实现一个栈。

    两个栈实现队列的时候,只有在出队列时 ,如果 “出栈”栈为空,需要有 入栈栈到出栈栈的转移操作。

    一个队列实现栈时,每次入栈都需要将队列进行“翻转”(重新全部入队列,最终将队尾移动到对头)。

  2. 遇到对时间复杂度有要求的就空间换时间。

  3. INT_MAX 可用来表示正无穷,INT_MIN表示负无穷。

  4. 倒序一个链表,优先想到栈(先入后出!)

  5. 判断链表有无环,快慢双指针。 画图能够迅速帮你找到解决思路!!!!杨老师!谢谢你 画图列方程

  6. 由于链表经常要用前一个值操作后一个值,所以经常要考虑边界值。

  7. 对于深度克隆,比如随机链表复制,无向图复制,要想到哈希表。

Day 2:

  1. 链表一定要考虑边界值啊!!第2次了。链表操作A->next的时候一定要考虑A是否为空;
  2. 非递减数列两数求和,别想那么多,先给大窗口,然后缩小,
  3. 双指针,快慢双指针,左右双指针(中心扩张,两边收缩)
  4. 如果有正负的情况,千万不要自作从聪明,将某个值设为0,不然比大小时候会出现bug。
  5. 如果说写出的代码分为好多种情况,那么思路一定是错了。
  6. 树的层序遍历需要用到队列。当需要记录层数时,需要用到额外的变量,并用到for循环
  7. 动态规划问题的第一个性质:重叠子问题 。 设置备忘录。一般是哈希表。数组也可以。如果可以看出数组长度不会超过某个最大值,就可以用数组。但这是个稀疏数组。

Day 3:

  1. 回溯代码框架,设最后返回的集合是res,res里面的元素是方案(路径),路径中为节点,第一步我们要搞清,方案是什么,路径是什么,节点又是什么。(比如n皇后问题,方案是整个棋盘,选择是在第row行选择第col个点)所以backtrace的参数就是(棋盘,row)

  2. 回溯算法:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    void backtrace(状态参数)
    {
    if(设置结束条件)
    {
    res.push_back(路径);
    return
    }
    //路径

    for(int )
    {
    //排除不合法选择
    if(!is_valid())
    continue;

    选择();
    进入下一层
    退选择
    }

    }

  3. 其实想想看,回溯算法和动态规划是不是有点像呢?我们在动态规划系列文章中多次强调,动态规划的三个需要明确的点就是「状态」「选择」和「base case」,是不是就对应着走过的「路径」,当前的「选择列表」和「结束条件」?

  4. 组合问题,给定数组自带顺序,且答案中[1,2] [2,1]算是相同结果的,甚至可以不用is_valid;因为仔细想一下,比如选择到2,那么1 一定选过了 ,3一定没选过。(自行理解其中奥义很简单)。

  5. 求 有重复数据的子集 要先排序。然后

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    for(int i = start;i<nums.size();i++)
    {
    //因为和上一个相同的话, 下一个back_trace(nums,k,i+1)会得到相同的结果
    if (i > start && nums[i] == nums[i - 1])
    continue;

    subresult.push_back(nums[i]);
    back_trace(nums,k,i+1);
    subresult.pop_back();
    }
  6. BFS: 将问题抽象为一幅图 , 找到节点 ,以及节点的邻居。那么算法的流程就是从初始节点开始。比如开密码锁,每个节点就是 密码锁当前的数字,那么邻居就是,转动一位得到的数字。

Day 4 :

  1. 搞不定边界就尽量不要使用双重二分。
  2. 检查是不是break和continue 用混了!!!!
  3. 动态寻找最大值最小值的时候要初始化一个结果!不能只声明。
  4. 集合之间对比,要想到unordered_map

Day 5:

  1. 股票问题因为给的数组不是有序的,所以不能使用双指针法。
  2. 动态规划超出时间限制就要反思是否有重叠子问题,设置备忘录解决。
  3. 自顶向下的动归是递归,有可能会遇到重复子问题,自底向上的动归是迭代,但是空间复杂度高一点。
  4. 如果动态规划的开始和末尾相互影响。就要分出情况。
  5. vector.end() 并不是最后一个元素的指针 而是最后一个元素的下一个指针
  6. 回溯法毕竟是穷举,拥有极高的时间复杂度。如果有其他解法尽量不要用穷举。
  7. 不该超出时间限制的时候超出时间限制,检查起始条件 ,终止条件,以及变化条件。

Day 6

  1. 使用双指针时要确保指针移动啊。
  2. 差分数组要多一位(其实也没必要,得看情况)。

Day 7

  1. 二分搜索时候左右边界问题别搞错了啊。 左边界查看left==num.size() 右边界查看left-1<0;
  2. 二分搜索如果没找到不返回特殊值的话,左右边界还是很好写的。
  3. 转换思路才能二分搜索。比如给定天数,让你求运输船最小承载。这个不好算,那就转换成给出承载,返回最小天数。这就可以用二分了。这个转变思路非常巧妙。
  4. 无论是升序降序 左边界返回left 右边界返回left-1;

Day 8

  1. vector erase和遍历数组没有区别,时间复杂度很高。易超出时间限制
  2. 排序的 比较函数 大于为真是降序 小于为真是升序
  3. unordered_set 是不支持随机存取的。所以要用unordered_map 与 vector实现随机读取;
  4. 要分清出子序列子串的区别,字串必须连续,子序列不必连续;
  5. 都tm什么时候了还在 犯”=” 和 “==”的错误。现在还非常容易犯小错误

Day 9

  1. 树的遍历中,如果遍历结果包含着空指针,那么不需要有别的遍历辅助,也能够确定一棵树。
  2. stoi()可以把字符串转化成为数字。 to_string 可以将数字转化为字符串
  3. 可以用 map用于关联字符串与节点, set用于检测字符串重复。

Day 10

  1. 涉及到区间和 那就一定会有前缀和数组!! 而对区间进行统一操作,那就一定会有差分数组?
  2. 归并排序的一个重要思想,凡是已经在同一部分里的,就一定发生过关系了。只有和另一部分的没有发生过关系。(非常重要的思想)
  3. 而且如果无序很难,但是发现有序很好做,那么可以去想想归并排序, 或者看到题目中有 数组的性质是, 当i<j
  4. 前缀和 要思考 有没有
  5. BST 的中序遍历结果是有序的(升序)。 可以说遇到二叉搜索树,大概率是中序遍历

Day 11

  1. 快速排序是 树的前序遍历 归并排序是树的后序遍历

  2. 快速排序为了达到稳定的快速,数组要是乱序。函数shuffle() 可以随机打乱

  3. 快速排序中

    1
    2
    3
    4
    5
    6
     while(cur<=right && nums[cur]>=nums[p]) cur++;//自己体会
    //而且需要特殊的交换操作
    swap(nums[p],nums[cur]);
    swap(nums[p+1],nums[cur]);// 交换后这
    p+=1;
    cur = p+1;
  4. srand(time(0)); 生成随机树种子

  5. 堆是完全二叉树 ,然后再满足父节点大于等于 或小于等于子节点。所以插入的时候,先插入满足完全二叉树的位置,在满足第二个要求。删除的时候 先交换 再下降。

  6. 完全二叉树用数组实现非常方便。 节点 i 的左子节点 2i 右子节点2i+1

  7. 优先级队列定义比较函数必须是静态成员函数的?

  8. 判断完全二叉树是否是满二叉树,左节点一直找下去,右节点一直找下去,看看高度是否相同。

  9. if 后面如果不是一句话 一定不要忘了加花括号啊

Day 12

  1. 图结构表示方法,如果目的不是要迅速判断两节点之间是否有边,那还是使用邻接表方便一点。

  2. 拓扑排序 需要额外的数组记录节点的入度;

  3. int 类型的0 1 可以用逻辑运算符操作。

  4. 连通性使用 并查集

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    class UF {
    private:
    // 记录连通分量
    int count;
    // 节点 x 的父节点是 parent[x]
    vector<int> parent;


    int find_root(int x)
    {
    // 根节点的 parent[x] == x
    if (parent[x] != x) {
    parent[x] = find_root(parent[x]);
    }
    return parent[x];
    }
    public:
    /* 构造函数,n 为图的节点总数 */
    UF(int n)
    {
    // 一开始互不连通 联通分量等于节点数
    count = n;
    // 父节点指针初始指向自己
    //尺寸全部初始化为1
    parent.resize(n);
    for (int i = 0; i < n; i++)
    {
    parent[i] = i;
    }
    }
    /* 其他函数 */
    void union_(int p, int q)
    {
    int rootP = find_root(p);
    int rootQ = find_root(q);
    if (rootP == rootQ)
    return;
    // 将两棵树合并为一棵 以下二者选一即可
    {
    parent[rootQ] = rootP;
    parent[rootP] = rootQ;
    }
    count--; // 两个分量合二为一
    }

    int getcount() {
    return count;
    }

    bool connected(int p, int q)
    {
    int rootP = find_root(p);
    int rootQ = find_root(q);
    return rootP == rootQ;

    }
    };
  5. 并查集可以建立虚拟头部,从而排除异己。

  6. 在二位坐标中控制方向常用方法。

    1
    vector<vector<int>> dir = {{1,0},{0,1},{-1,0},{0,-1}} //对应下 右 上 左
  7. 类的后面要加;

  8. 只有判断无向图是否带环时才能用并查集。

  9. 克鲁斯卡尔算法 Kruskal 算法 就是无向有权图 按权重大小一次加边;

  10. 并查集的时间复杂度是0(1);

  11. 无向有权图的最小生成树算法 prim算法需要转换成图的邻接表格式。 Kruskal 不用 ,给的如果是边的集合 就krusal 如果是 graph prim。

Day 13

  1. dijkstra 算法: 优先级队列里存放边 因为这样可以通过边的权重来知晓

Day 14

  1. 前缀树

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    //力扣667
    class TrieNode {
    public:
    int val;
    vector<TrieNode *> next;
    TrieNode():val(0),next(26) {
    for (int i = 0; i < 26; ++i) {
    next[i] = nullptr;
    }
    }
    };

    class MapSum {
    private:
    TrieNode * root;
    unordered_map<string, int> cnt;
    public:
    MapSum() {
    root = new TrieNode();
    }

    void insert(string key, int val) {

    int delta = val;
    //如果之前存在 算出两数值差好做修改
    if (cnt.count(key)) {
    delta -= cnt[key];
    }
    cnt[key] = val;

    TrieNode * node = root;
    for (auto c : key) {
    if (node->next[c - 'a'] == nullptr) {
    node->next[c - 'a'] = new TrieNode();
    }
    node = node->next[c - 'a'];
    node->val += delta;
    }
    }

    int sum(string prefix) {
    TrieNode * node = root;
    for (auto c : prefix) {
    if (node->next[c - 'a'] == nullptr) {
    return 0;
    } else {
    node = node->next[c - 'a'];
    }
    }
    return node->val;
    }
    };
  2. 真不能上来直接写,一定要先想好各种情况。

Day 15

  1. 优先级队列设置升序降序,less代表降序 ,greater代表升序

  2. 单调栈,要把它想象成,个子高挡住个子矮的。从一个方向上望过去,是单调递增做递减的。

  3. 单调栈不一定存放数值,也可以存放索引,反正可以通过索引来找值。

  4. for循环一定要检查是i++ 还是 i—

  5. 单调栈 找比自己大的 那栈顶就是栈里最小的。好好理解一下。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    //力扣503
    class Solution {
    public:
    vector<int> nextGreaterElements(vector<int>& nums) {
    stack<int> st;
    int n = nums.size();

    vector<int> result(nums.size());

    for(int i =2*n-1;i>=0;i--)
    {
    //找到第一个大于自己的元素
    while(!st.empty() && st.top()<=nums[i%n]) st.pop();
    //没找到 那自己就是最大的
    if(st.empty()) result[i%n] = -1;
    //找到了保存结果
    else result[i%n] = st.top();
    //把自己放进去
    st.push( nums[i%n]);
    }

    return result;
    }
    };