1 minute read

4/19

T20 有效的括号

重要反例:”(){}}{“ 说明不能左边栈、右边队列,还需保证顺序性,也就是左括号需要在遍历中先于对应的右括号出现。加上从做到右遍历字符串本身便是“老的优先”,或者说“先出现先匹配”的性质,所以不必额外建一个队列。直接遍历,根据即时性,只要右括号无法和当前栈顶左括号匹配或栈为空,直接返回false;若能匹配,pop栈顶,继续遍历。

class Solution {
public:
    bool isValid(string s) {
        stack<char> st;

        for (char c : s) {
            if (c == '(' || c == '[' || c == '{') {
                st.push(c);
            } else {
                if (st.empty()) return false;

                char top = st.top();
                if ((c == ')' && top != '(') ||
                    (c == ']' && top != '[') ||
                    (c == '}' && top != '{')) {
                    return false;
                }

                st.pop();
            }
        }

        return st.empty();
    }
};

T225 用队列实现栈

和昨天的题目不同,这一题没有平均时间复杂读为O(1)的解法,因为队列是先进先出的特性,无法实现倒转。注意:需要标记好谁是主队列。有趣的是,有个自旋方法可以实现仅用一个队列:

class MyStack {
public:
    queue<int> q;

    MyStack() {
        
    }
    
    void push(int x) {
        q.push(x);
        int n = q.size();
        while (n > 1) {
            q.push(q.front());
            q.pop();
            n--;
        }
    }
    
    int pop() {
        int res = q.front();
        q.pop();
        return res;
    }
    
    int top() {
        return q.front();
    }
    
    bool empty() {
        return q.empty();
    }
};

对比一、二个队列实现栈的时间复杂度:

方法一:两个队列

在 pop / top 时搬运

操作 时间复杂度
push O(1)
pop O(n)
top O(n)
empty O(1)

特点:

  • 入栈快
  • 出栈慢

方法二:一个队列

在 push 时旋转

操作 时间复杂度
push O(n)
pop O(1)
top O(1)
empty O(1)

可以发现不同解法适应不同操作分布概率的业务逻辑。当然,一个队列也可以选择push正常,在pop、top上进行自旋,这就和两个队列时间复杂度分布相似,而在空间上优化了复杂度。

注意:

queue<int> q1, q2;
swap(q1, q2);

这里面swap 做的不是“逐个元素交换”,而是交换内部指针/结构体。时间复杂度仍为O(1)


4/18

T54 替换数字(卡码网)

这道题之前遇到过,仅仅一两周前,但我对最优思路印象很弱。这说明,需要提高复习频率,优化记忆方式,或以练习、手写的方式去强化记忆。
以及:字符串插入是低效的,需要统一移动左右区域。能赋值就赋值。换个顺序(从最右侧开始)或许有新天地。

T151 已刷过,见之前

这周刷的专题是“双指针法”,前面常有所覆盖

……以下题目均在之前出现过,链表专题后期统一手写刷题,略。
跳至“栈与队列”专题

T232 用栈实现队列

核心:利用将一个栈内的一组数据倒入另一个栈内后恰好反向的特性
注:在平板上自己画图,应对多种情况,尤其是穿插不同元操作的实例(已完成)


4/17

T344 反转字符串

简单,略

T27 移除元素

这道题我一开始想的方法过于复杂:先排序,再左右指针,左边移到第一个val处,右边直接和左边开始一个个交换直到重合或左边不再是val。这个解法有三个问题:首先排序增加了时间复杂度,多乘以了一个logn;其次,我没有优化可以跳过右侧V的情况;最后,以下统计数目的代码会出现如下错误:

if (left < nums.size() && nums[left] != val) {
    return left + 1;
}
return left;
}

假如是 xxxxxxxxxxxVVVVV这样的排序后结构,x表示不等于V,左右指针在V区域中间某处相遇,此时的left前后都是V,不能保证等同于有效元素个数,因此须改写如下:

//计算前面非 val 的个数
int k = 0;
while (k < nums.size() && nums[k] != val) {
    k++;
}

return k;

最优解:

class Solution {
public:
    int removeElement(vector<int>& nums, int val) {
        int slow = 0; // 指向下一个要填的位置
        
        for (int fast = 0; fast < nums.size(); fast++) {
            if (nums[fast] != val) {
                nums[slow] = nums[fast];
                slow++;
            }
        }
        
        return slow;
    }
};

核心:
1、此题对元素内部顺序没有要求,因此无需排序
2、快指针经过的所有非V元素都已经赋值过慢指针对应的位置,因此不必担心慢指针覆盖非V元素——两个指针都是单调前进的
3、快指针遇到V即跳过,这样慢指针的每一个赋值都保证有效性,且快指针遍历完毕数组后,所有元素均已被考虑到


4/15(这两道题仔细复习KMP知识点后重做)

T28 找出字符串中第一个匹配项都下标【重要】

该题被归类为简单,因为暴力解法也能过。但这道题很重要,是KMP算法的核心应用。
注:复习KMP算法及其拓展应用

https://
www.bilibili.com/video/BV1PD4y1o7nd/?vd_source=2347c699c40e3a971470bde33b0f40de

https://www.bilibili.com/video/BV1M5411j7Xx/?vd_source=2347c699c40e3a971470bde33b0f40de

T459 重复的子字符串

误区:不能直接找首末最长匹配项,然后跳跃性遍历。

反例:
s = “abcabcabcabc”

最长前后缀:
“abcabcabc”

真正周期:
“abc”

关键:依旧KMP


4/13

T151 翻转字符串里的单词

【以下解法竟然不是O(1),而是O(n)的,记住:往开头插入(insert at front)是 O(n),而且通常会导致重新分配内存。本质上是在不断“构造新字符串”】

class Solution {
public:
    string reverseWords(string s) {
     //从右到左遍历字符串,如果一开始是空格,删除,直到遇见非空格。两个指针,一个指向当前单词开头,一个结尾
     //对于每个单词,直接拼接到s的开头,加一个空格
     //继续遍历,无视空格直到下一个单词
     //循环结束(s遍历完毕)   
    }
};