剑指 Offer 刷题笔记 2

目录

剑指 Offer 刷题笔记 2

第 16 天 排序(简单)

剑指 Offer 45. 把数组排成最小的数

一开始我还在想类似基数排序的方法

比如我会想

30

3

34

5

9

这个顺序,会不会是先排每个数的最高的位数,然后再排第二高的位数

如果某个数没有第二个位数,那么就复制前面的位数

比如

30

3(3)

34

5(5)

9(9)

之后想到百位数啊千位数啊,我就晕了

之后看题解才知道还可以直接通过两两之间的拼接结果的比较,来得到整个序列的最小拼接结果

感觉这应该是要用离散数学证明……?不懂

一开始比较 x+yy+x 我是这么写的

class Solution {
public:
    int plus(int x, int y){
        int tmp = y;
        while(tmp != 0){
            tmp = tmp/10;
            x = x*10;
        }
        x = x + y;
        return x;
    }

    string minNumber(vector<int>& nums) {
        sort(nums.begin(),nums.end(),[this](int a ,int b){
            return plus(a, b) < plus(b, a);
        });

        string str = "";

        for(auto it = nums.begin(); it != nums.end(); it++){
            str.append(to_string(*it));    
        }

        return str;
    }
};

虽然对于简单的例子能行,但是对于大数来说会溢出

执行结果:
执行出错
Line 7: Char 18: runtime error: signed integer overflow: 999999997 * 10 cannot be represented in type 'int' (solution.cpp)
SUMMARY: UndefinedBehaviorSanitizer: undefined-behavior prog_joined.cpp:17:18
最后执行的输入:
[999999998,999999997,999999999]

最后还是复制了别人的题解

class Solution {
public:
    string minNumber(vector<int>& nums) {
        vector<string> strs;
        for(int i = 0; i < nums.size(); i++)
            strs.push_back(to_string(nums[i]));
        quickSort(strs, 0, strs.size() - 1);
        string res;
        for(string s : strs)
            res.append(s);
        return res;
    }
private:
    void quickSort(vector<string>& strs, int l, int r) {
        if(l >= r) return;
        int i = l, j = r;
        while(i < j) {
            while(strs[j] + strs[l] >= strs[l] + strs[j] && i < j) j--;
            while(strs[i] + strs[l] <= strs[l] + strs[i] && i < j) i++;
            swap(strs[i], strs[j]);
        }
        swap(strs[i], strs[l]);
        quickSort(strs, l, i - 1);
        quickSort(strs, i + 1, r);
    }
};

规则是,如果 x + y < y + x 代表 x < y 这样就遵循了传统的从小到大的排列

而这样排最终就会排出最小的数

这个快排是一开始取了 pivot 为 strs[l] 也就是取第一个数

快排这个最后的第一个元素与 i 指向的元素交换(j 也一样吧,因为最后 i == j),感觉很神奇,但是又很合理,合理的在于因为是以枢轴为比较标准,那么枢轴最后就是应该放在中间的

而且还很合理的是,最后 i == j 时,i 指向的一定是小于等于枢轴的点,所以和枢轴交换不会破坏顺序

剑指 Offer 61. 扑克牌中的顺子

一开始我只想着一个大小王只能补一次

没有想到连续的大小王来补空位

class Solution {
public:
    bool isStraight(vector<int>& nums) {
        sort(nums.begin(), nums.end());

        int zero_count = 0;
        int prev = 0;
        for(int num : nums){
            if(num == 0) zero_count++;
            else{
                // 如果是第一个位置
                if(prev == 0) prev = num;
                // 如果上一个位置和这个位置之间是连续的
                else if(prev + 1 == num) prev = num;
                // 如果上一个位置和这个位置之间不是连续的
                else{
                    // 如果还有大小王,尝试使用大小王来补位置
                    if(zero_count != 0){
                        // 如果大小王补的位置也续不上
                        if(prev + 2 == num){
                            zero_count--;
                            prev = num;
                        }
                        // 如果续不上
                        else{
                            return false;
                        }
                    }
                    // 如果没有大小王,返回假
                    else{
                        return false;
                    }
                }
            }
        }

        return true;
    }
};

所以之后我觉得我的思路应该有问题

应该是排序之后从第一个牌开始,执行五次,扫完了牌可以提前终止

class Solution {
public:
    bool isStraight(vector<int>& nums) {
        sort(nums.begin(), nums.end());

        int zero_count = 0;
        int prev_num = 0;
        int curr_idx = 0;

        // 统计 0 的数量
        for(int i = 0; i < 5; i++){
            if(nums[i] == 0) zero_count++;
            else{
                curr_idx = i;
                break;
            }
        }

        prev_num = nums[curr_idx++];
        for(int i = 0; i < 5; i++){
            if(curr_idx == 5) break;

            if(prev_num + 1 == nums[curr_idx]){
                prev_num++;
                curr_idx++;
            }
            else{
                // 有大小王可用
                if(zero_count != 0){
                    zero_count--;
                    prev_num++;
                }
                else{
                    return false;
                }
            }
        }

        return true;
    }
};

第 17 天 排序(中等)

剑指 Offer 40. 最小的 k 个数

这道题确实很常见……在好多面经都能看到

快排不用排序整个数组——快选

一开始我是用快排写的,但是这样写出来会堆栈溢出

class Solution {
public:
    vector<int> getLeastNumbers(vector<int>& arr, int k) {
        vector<int> res;
        quickSort(arr, 0, arr.size()-1);
        for(int i = 0; i < k; i++){
            res.push_back(arr[i]);
        }
        return res;
    }
private:
    void quickSort(vector<int>& arr, int left, int right){
        if(left >= right) return;
        int i = left, j = right;
        while(i < j){
            while(arr[j] >= arr[left] && i < j) j--;
            while(arr[i] <= arr[left] && i < j) i++;
            swap(arr[i], arr[j]);
        }
        swap(arr[i], arr[left]);
        quickSort(arr, left, i-1);
        quickSort(arr, i+1, right);
    }
};

之后才发现别人都是提前终止的。

那我一开始想的就是说一直分,每一次只分左边的子区间,因为是取最小的 k 个数,左边的子区间是取小数

一直分到下一次两个子区间的长度小于 k 的时候,这个时候分完这两个子区间,就不用再继续分了

之后在写的时候确实发现,不一定能够一直分左边的子区间,比如如果要传入一个接近末尾的 k 的话,那么按照我的思路的话,就要一直区分两个区间了,那跟原来的还是没有区别

这个时候我再看别人的,发现别人的还是更进一步,就是说我现在举了一个接近末尾的 k 的例子,那么我就应该是一直区分右区间,如果是接近开头的 k,那就应该一直区分左区间,那么就是应该有一个东西告诉我我现在这个 k 的情况是接近哪边,我才好知道我该区分那个区间

这样的话,每次区分都只是一次 partition,最多区分 logn

然后他用什么判断呢,就用 partition 最后返回的中点作为与 k 的比较,真的很强,把所有信息都用到了

然后我才知道别人把这个叫作快选

使用递归

class Solution {
public:
    vector<int> getLeastNumbers(vector<int>& arr, int k) {
        vector<int> res;
        quickSort(arr, 0, arr.size() - 1, k);
        for (int i = 0; i < k; i++) {
            res.push_back(arr[i]);
        }
        return res;
    }
private:
    void quickSort(vector<int>& arr, int left, int right, int k) {
        int i = partition(arr, left, right);
        if (i == k-1)
            return;
        // 右区间还没排好序
        else if (i < k-1) {
            quickSort(arr, i + 1, right, k);
        }
        // 左区间还没排好序
        else {
            quickSort(arr, left, i - 1, k);
        }
    }

    int partition(vector<int>& arr, int left, int right) {
        if (left >= right) return right;
        int i = left, j = right;
        while (i < j) {
            while (arr[j] >= arr[left] && i < j) j--;
            while (arr[i] <= arr[left] && i < j) i++;
            swap(arr[i], arr[j]);
        }
        // 这里用 i 或者 j 没有区别,因为最后 i == j
        swap(arr[j], arr[left]);
        return j;
    }
};

不使用递归

class Solution {
public:
    vector<int> getLeastNumbers(vector<int>& arr, int k) {
        vector<int> res;
        quickSort(arr, 0, arr.size() - 1, k);
        for (int i = 0; i < k; i++) {
            res.push_back(arr[i]);
        }
        return res;
    }
private:
    void quickSort(vector<int>& arr, int left, int right, int k) {
        int i = 0;
        while(i != k-1){
            i = partition(arr, left, right);
            // 右区间还没排好序
            if(i < k-1)
                left = i+1;
            // 左区间还没排好序
            else
                right = i-1;
        }
    }

    int partition(vector<int>& arr, int left, int right) {
        if (left >= right) return right;
        int i = left, j = right;
        while (i < j) {
            while (arr[j] >= arr[left] && i < j) j--;
            while (arr[i] <= arr[left] && i < j) i++;
            swap(arr[i], arr[j]);
        }
        // 这里用 i 或者 j 没有区别,因为最后 i == j
        swap(arr[j], arr[left]);
        return j;
    }
};
基数排序

在数据的值有一个较小的上限时可以使用基数排序,确实简单

class Solution {
public:
    vector<int> getLeastNumbers(vector<int>& arr, int k) {
        vector<int> nums(10001, 0);
        vector<int> result;
        for(int idx : arr){
            nums[idx]++;
        }
        for(int i = 0; i<nums.size();i++){
            if(nums[i]>0){
                if(result.size() == k){
                    return result;
                }
                result.push_back(i);
                nums[i]--;
                i--;
            }
        }

        return result;
    }
};
堆排

一开始假设不知道要用大根堆还是小根堆

但是知道的方法,遍历数组 arr,对其元素执行 push 操作。每次 push 后,检查 size,若 size > k,则执行 pop 操作

所以从这个 pop 就可以发现,我每次居然是要丢掉顶部元素,也就是说,如果是大根堆,我丢掉的 n - k 个元素都比堆中剩下的元素大,如果是小根堆,我丢掉的 n - k 个元素都比堆中剩下的元素小

那么这里就很明显了,取 k 个最小的数使用大根堆,取 k 个最大的数使用小根堆

那么对于这题,还有一个小小的优化方法,如果当前数字不小于堆顶元素,数字可以直接丢掉,不入堆

class Solution {
public:
    vector<int> getLeastNumbers(vector<int>& arr, int k) {
        priority_queue<int> max_heap;
        vector<int> result;

        if(k == 0) return result;
        
        for (int num : arr) {
            // 如果要入堆,查看当前堆中元素的数量,小于
            if (max_heap.size() == k) {
                if (num < max_heap.top()) {
                    // 如果当前数字不小于堆顶元素,数字可以直接丢掉,不入堆
                    max_heap.push(num);
                    max_heap.pop();
                }
            }
            else {
                max_heap.push(num);
            }
        }

        int tmp;
        for (int i = max_heap.size(); i > 0; i--) {
            tmp = max_heap.top();
            max_heap.pop();
            result.push_back(tmp);
        }

        return result;
    }
};
BFPRT

nlogn 是任何排序算法的最低耗时

看了别人的讲解,看上去好复杂……

https://zhuanlan.zhihu.com/p/291206708

剑指 Offer 41. 数据流中的中位数

一开始尝试了一下最简单的插入一次就排序一次,果然时间超了

class MedianFinder {
private:
    vector<int> nums;
public:
    /** initialize your data structure here. */
    MedianFinder() {

    }
    
    void addNum(int num) {
        nums.push_back(num);
        sort(nums.begin(), nums.end());
    }
    
    double findMedian() {
        int count = nums.size();
        if(count % 2 == 1)
            return nums[count/2];
        else
            return (double)(nums[count/2-1] + nums[count/2])/2.0;
    }
};

/**
 * Your MedianFinder object will be instantiated and called as such:
 * MedianFinder* obj = new MedianFinder();
 * obj->addNum(num);
 * double param_2 = obj->findMedian();
 */

想想也合理,每排一次是 nlogn,要排 n 次,那就是 n^2logn

之后看了一眼题解就想到了两个堆

其实我之前是想到了的

但是我在想的时候我发现我不知道怎么调整这两个堆

在瞥一眼题解的时候看到了奇数偶数之类的,然后我就想到了,可以在两个堆的元素数目相差过大的时候调整

那么每一次插入都是堆的插入,堆的数目为 n/2,插入耗时 log(n/2)

n 次插入,算法总的时间复杂度是 nlogn

class MedianFinder {
private:
    // 左区间 大根堆
    priority_queue<int> max_queue;
    // 右区间 小根堆
    priority_queue<int, vector<int>, greater<int>> min_queue;
public:
    /** initialize your data structure here. */
    MedianFinder() {

    }

    void addNum(int num) {
        // 找到当前的中位数
        double curr_middle = findMedian();

        // 如果小于等于这个中位数,就插入左区间,反之插入右区间
        if (num <= curr_middle)
            max_queue.push(num);
        else
            min_queue.push(num);

        // 两个堆各自的元素数目之差不超过 1
        int count1 = max_queue.size();
        int count2 = min_queue.size();

        int tmp;
        if (count1 - count2 == 2) {
            tmp = max_queue.top();
            max_queue.pop();
            min_queue.push(tmp);
        }
        else if (count2 - count1 == 2) {
            tmp = min_queue.top();
            min_queue.pop();
            max_queue.push(tmp);
        }
    }

    double findMedian() {
        int count1 = max_queue.size();
        int count2 = min_queue.size();

        if (count1 == 0 && count2 == 0) return 0.0;

        if (count1 == count2)
            return ((double)max_queue.top() + (double)min_queue.top()) / 2.0;
        else if (count1 > count2)
            return (double)max_queue.top();
        else
            return (double)min_queue.top();
    }
};

/**
 * Your MedianFinder object will be instantiated and called as such:
 * MedianFinder* obj = new MedianFinder();
 * obj->addNum(num);
 * double param_2 = obj->findMedian();
 */

第 18 天 搜索与回溯算法(中等)

剑指 Offer 55 - I. 二叉树的深度

一开始用层序遍历做的

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode(int x) : val(x), left(NULL), right(NULL) {}
 * };
 */
class Solution {
public:
    int maxDepth(TreeNode* root) {
        int depth = 0;
        if(root == nullptr) return depth;

        queue<TreeNode*> nodeQueue;
        nodeQueue.push(root);

        TreeNode *tmp;

        while(nodeQueue.size() != 0){
            depth++;
            // 这个时候又要用到初始化为 size 的操作了
            for(int i = nodeQueue.size(); i > 0; i--){
                tmp = nodeQueue.front();
                nodeQueue.pop();

                if(tmp->left != nullptr) nodeQueue.push(tmp->left);
                if(tmp->right != nullptr) nodeQueue.push(tmp->right);
            }
        }

        return depth;
    }
};

但是时间复杂度不是很好

估计是因为这样会遍历整个树,时间复杂度是 o(n)

看了别人的递归版本的,时间复杂度降了好多

class Solution {
public:
    int maxDepth(TreeNode* root) {
        return root ? 1 + max(maxDepth(root->left), maxDepth(root->right)) : 0;
    }
};

感觉学到了……就是说在递归和非递归的总次数一样的时候,递归如果能够在每一次执行做更少的事(比如这里就不用队列的入队出队),那么反而时间复杂度可能更好

剑指 Offer 55 - II. 平衡二叉树

一开始我还想着 return isBalanced(root->left) && isBalanced(root->right) 这种递归呢

但是后面一想,如果死板地用地话,还不知道终止条件怎么写

然后之后就想到还是需要每一个结点的高度来判断

如果从上到下递归 isBalanced 每次执行都要得到结点的高度的话,那就会有很多重复的计算,就比如说第一层的 A 要算自己的高度,要遍历第二第三层的孩子,第二层的 B 要算自己的高度,就要遍历第三层的孩子,这样的话,第三层就被计算了两次

所以果然还是不能用 isBalanced 递归,那么在之前想的过程中,就会想到算高度是一个递归的过程,如果他是从下到上的话,那就只需要遍历整个树一遍,就一定能遇到可能的不平衡的情况,所以在这个算高度的过程中顺便就算了是否是不平衡,是最好的

然后就是可以设置一个提前终止条件,节省时间

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode(int x) : val(x), left(NULL), right(NULL) {}
 * };
 */
class Solution {
private:
    bool flag = true;
public:
    bool isBalanced(TreeNode* root) {
        getHeight(root);
        return flag;
    }

    int getHeight(TreeNode *root){
        if(root == nullptr) return 0;
        if(flag == false) return 0;

        int height1 = getHeight(root->left);
        int height2 = getHeight(root->right);

        if(abs(height1-height2) >= 2) flag = false;

        return max(height1, height2) + 1;
    }
};

第 19 天 搜索与回溯算法(中等)

剑指 Offer 64. 求1+2+…+n

一开始我就是简单的递归,不知道他的难点在哪里

class Solution {
public:
    int sumNums(int n) {
        if(n == 1) return 1;
        int sum = sumNums(n-1);
        return sum + n;
    }
};

后来才发现原来是我也不能用 if

那这样的话我就不知道怎么做了……

看了题解才知道,原来 A&&B, A||B 的原理是先执行 A,然后判断整个表达式是否已经确定,如果已经确定就不执行 B,然后 B 可以是一个可执行的式子

class Solution {
public:
    int sumNums(int n) {
        n && (n += sumNums(n-1));
        return n;
    }
};


// 作者:LeetCode-Solution
// 链接:https://leetcode.cn/problems/qiu-12n-lcof/solution/qiu-12n-by-leetcode-solution/
// 来源:力扣(LeetCode)
// 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

这里就是在 n 不为 0 的时候会执行后面的 n += sumNums(n-1)

快速乘

之后看到快速乘法

主要是把一个数拆成二进制数

A = 2 i 1 + 2 i 2 + . . . + 2 i n A = 2^{i_1} + 2^{i_2} + ... + 2^{i_n} A=2i1+2i2+...+2in

然后 A ∗ B A*B AB 就等于

( 2 i 1 + 2 i 2 + . . . + 2 i n ) ∗ B (2^{i_1} + 2^{i_2} + ... + 2^{i_n})*B (2i1+2i2+...+2in)B

而一个数乘 2 的若干次方就相当于移位

所以 A ∗ B A*B AB 就等于

B < < i 1 + B < < i 2 + . . . + B < < i n B<<i_1 + B<<i_2 + ... + B<<i_n B<<i1+B<<i2+...+B<<in

那么写成算法就是,对 A A A 不断向右移位,同时 B B B 也向左移位,如果当前的 A A A 的最后一位是 1,那么就让结果加上当前的 B B B

class Solution {
public:
    int sumNums(int n) {
        // n * (n+1)/2
        int sum = 0;
        int A = n, B = n + 1;
        while(A){
            if(A & 1){
                sum += B;
            }
            A >>= 1;
            B <<= 1;
        }

        sum /= 2;

        return sum;
    }
};

然后这个还是要循环,为了不循环就要把它拆开

拆开的具体方法是,查看他最多会循环多少次,然后就写出最大次数的循环体在代码中

剑指 Offer 68 - I. 二叉搜索树的最近公共祖先

瞬间想到快慢指针那一类型的题

那些题都是,两个指针同步走,然后如果一个指针遇到了结尾,两个指针就都停下来,这个时候可以保存到两个指针之间的距离

然后两指针再从头开始,上一轮中落后的指针走间隔的步数,另一个指针不动

这个时候走完了间隔的步数,两个指针就应该指向相同的位置

而二叉树看上去是树,但是找父节点这个操作是唯一性的,所以如果一路上只有找父节点这个操作的话,跟链表中根据 next 走没有区别

这就要求给出一个结点能够直接找到它的父节点……对于一般的二叉树,他还确实没有保存父节点这个数据噢

那就只能放弃这个直接利用父节点信息的方法

那么这个时候肯定是要自己得到父节点信息

能得到这个信息的方式就是遍历二叉树了

那么既然都在遍历二叉树了,就不必要再说“先遍历一遍二叉树,遍历的时候设置父节点信息,然后再使用利用父节点信息的双指针算法”

而是在遍历的过程中就可以发现待求结点的父子关系了

然后就想到了递归

因为遍历的时候也没办法保证我可以顺序发现待求结点啊啥的

然后递归也能遍历到整个树的结点,只是没有常规的先中后序遍历那样有顺序信息

要用递归的话就是使用分治的思想

对最顶层的树 lowestCommonAncestor(root, p, q) 得到的是要求的公共祖先

对不包含 p 或 q 的树 lowestCommonAncestor(root, p, q) 得到的应该是这个树的根

对只包含 p 或者只包含 q 的树 lowestCommonAncestor(root, p, q) 得到的也是这个树的根

对既包含 p 也包含 q 的树 lowestCommonAncestor(root, p, q) 得到的就是 p 和 q 的最近公共祖先

那么这里就看出来,如果树 T1 包含 p q,然后假设 T1 是 p 和 q 的最近公共祖先

那么假设 T1 是 T2 的子树,那么 lowestCommonAncestor(T2, p, q) 应该返回 T1

之后我就感觉有点不会了

于是我先写了

TreeNode *anc1 = lowestCommonAncestor(root->left, p, q);

TreeNode *anc2 = lowestCommonAncestor(root->right, p, q);

考虑 anc1 都 anc2 都有值的情况,这就说明左右各有一个 p 或 q

那么自己就是最近公共祖先

那么 anc 没有值的情况应该就是对应该子树没有 p 或 q,之前想错了

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode(int x) : val(x), left(NULL), right(NULL) {}
 * };
 */
class Solution {
public:
    TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
        if(root == nullptr)
            return nullptr;

        if(root == p || root == q)
            return root;
        
        TreeNode *anc1 = lowestCommonAncestor(root->left, p, q);
        TreeNode *anc2 = lowestCommonAncestor(root->right, p, q);

        if(anc1 != nullptr && anc2 != nullptr)
            return root;
        else if(anc1 != nullptr)
            return anc1;
        else if(anc2 != nullptr)
            return anc2;
        else
            return nullptr;
    }
};

第 20 天 分治算法(中等)

剑指 Offer 07. 重建二叉树

一开始我用的是传统的分治

如果在先序中找到一个点,那么这个点要对应到中序的某个区间中

在这个中序的这个区间,从 left 开始找,一定能找到先序的这个点在中序的 left 和 right 之间,假设落在 middle

然后先序的点的序号++,中序的区间分治,拆成 (left, middle-1), (middle+1, right)

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode(int x) : val(x), left(NULL), right(NULL) {}
 * };
 */
class Solution {
private:
    int preorder_pointer = 0;

    TreeNode* buildSubTree(vector<int>& preorder, vector<int>& inorder, int left, int right){
        if(left > right) return nullptr;

        int root_idx = left;
        while(inorder[root_idx] != preorder[preorder_pointer] &&
            root_idx < inorder.size()){
            root_idx++;
        }

        preorder_pointer++;

        TreeNode *root = new TreeNode(inorder[root_idx]);
        root->left = buildSubTree(preorder, inorder, left, root_idx-1);
        root->right = buildSubTree(preorder, inorder, root_idx+1, right);

        return root;
    }
public:
    TreeNode* buildTree(vector<int>& preorder, vector<int>& inorder) {
        if(preorder.size() == 0 || inorder.size() == 0) return nullptr;

        TreeNode *root;
        root = buildSubTree(preorder, inorder, 0, inorder.size()-1);

        return root;
    }
};
执行用时:44 ms, 在所有 C++ 提交中击败了12.96% 的用户
内存消耗:24.4 MB, 在所有 C++ 提交中击败了86.06% 的用户

然后看了迭代法

迭代法的本质是

若这颗树是一颗只有左子树的树,相当于一条单链表,那么中序遍历和先序遍历的结果是反过来的

利用栈来逆序存放,一旦遍历到最左下的地方,就开始弹出栈,若过程中栈顶弹出的和中序遍历从左往右的不相等,则说明不是单链表,而是多了个右孩子插在了最后一个弹出的结点的右边

https://leetcode.cn/problems/zhong-jian-er-cha-shu-lcof/solution/mian-shi-ti-07-zhong-jian-er-cha-shu-by-leetcode-s/1312660

例如

        3
       / \
      9  20
     /  /  \
    8  15   7
   / \
  5  10
 /
4

先序和中序遍历序列

preorder = [3, 9, 8, 5, 4, 10, 20, 15, 7]
inorder = [4, 5, 8, 10, 9, 3, 15, 20, 7]

我们用一个栈和一个指针辅助进行二叉树的构造。初始时栈中存放了根节点(前序遍历的第一个节点),指针指向中序遍历的第一个节点;

我们依次枚举前序遍历中除了第一个节点以外的每个节点。如果 index 恰好指向栈顶节点,那么我们不断地弹出栈顶节点并向右移动 index,并将当前节点作为最后一个弹出的节点的右儿子;如果 index 和栈顶节点不同,我们将当前节点作为栈顶节点的左儿子;

无论是哪一种情况,我们最后都将当前的节点入栈。

作者:LeetCode-Solution
链接:https://leetcode.cn/problems/zhong-jian-er-cha-shu-lcof/solution/mian-shi-ti-07-zhong-jian-er-cha-shu-by-leetcode-s/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

剑指 Offer 16. 数值的整数次方

快速幂,之前写题遇见过

主要是要注意特殊情况

class Solution {
public:
    double myPow(double x, int n) {
        if(n == 0)
            return 1.0;

        double res = 1;

        if(n < 0){
            if(x == 0)
                return DBL_MAX;
            else{
                x = 1.0/x;
                // 如果 n 是最大负数,先令 res = x,相当于 n 减 1
                // 再对 n 取相反数,这时就不会溢出
                if(n == 0x80000000){
                    res = x;
                    n -= -1;
                }
                n = -n;
            }
        }

        while(n != 1){
            if(n&1){
                res *= x;
            }
            n >>= 1;
            x *= x;
        }
        return x * res;
    }
};

剑指 Offer 33. 二叉搜索树的后序遍历序列

对于给定的 vector 的给定区间,每一次都先取最后的一个值,这个结点就是这个区间的根节点

然后想要划分左右子树,就以这个根节点为基准,一路向左找,找到第一个比根节点小的值

这个值的序号为 middle,那么左子树的区间是 (left, middle) 右子树的区间是 (middle+1, right-1)

然后再看左子树的区间有没有错,因为我划分区间是找第一个比根节点小的值嘛,那你这个左区间是不是所有结点都比根节点小呢?都比根节点小,才符合二叉搜索树的定义

之后递归分治

class Solution {
public:
    bool verifyPostorder(vector<int>& postorder) {
        return isPostorder(postorder, 0, postorder.size() - 1);
    }
private:
    bool isPostorder(vector<int>& postorder, int left, int right) {
        if (left >= right) return true;

        int middle = right - 1;
        // middle >= left 先执行,如果为假就不执行后面的
        // 所以不会由于下标错误发生栈溢出
        while (middle >= left && postorder[middle] > postorder[right]) {
            middle--;
        }
			
        int tmp = middle;

        while (tmp >= left) {
            if (postorder[tmp] > postorder[right])
                return false;
            tmp--;
        }

        return isPostorder(postorder, left, middle) && isPostorder(postorder, middle + 1, right - 1);
    }
};

把后序遍历倒过来,变为类似 根 右 左 的顺序 再使用辅助栈

对于每一个区间,也是第一个是根,然后向右一直找,直到找到第一个小于根的,记为 middle 那么 (middle, right) 就是

第 21 天 位运算(简单)

剑指 Offer 15. 二进制中 1 的个数

一开始我就是简单的循环遍历

class Solution {
public:
    int hammingWeight(uint32_t n) {
        int count = 0;
        for(int i = 0; i < 32; i++){
            if(n&1) count++;
            n >>= 1;
        }
        return count;
    }
};

看到题解才知道,居然还能有一个方法只跟 1 有关

在这里插入图片描述

class Solution {
public:
    int hammingWeight(uint32_t n) {
        int count = 0;
        while(n != 0){
            count++;
            n = n&(n-1);
        }
        return count;
    }
};

剑指 Offer 65. 不用加减乘除做加法

就是设计一个加法器

sum = a1 + b1 + cin; 表示当前要看的两个数的对应两个位上的数字相加

sum & 2 表示取和的第二位,也就是进位

(sum & 2)>>1 表示将这个进位的值放到第一位,方便参与运算

sum & 1 表示求和的第一位,也就是结果的当前位的数字

(sum & 1) << i 表示要将这个求和的结果放到结果的第 i 位上

result | ((sum & 1) << i) 就是具体怎么放的,是用或运算放的

class Solution {
public:
    int add(int a, int b) {
        int cin = 0, a1 = 0, b1 = 0, sum = 0, result = 0;
        for (int i = 0; i < 32; i++) {
            a1 = a & 1;
            b1 = b & 1;
            sum = a1 + b1 + cin;
            cin = (sum & 2)>>1;
            result = result | ((sum & 1) << i);

            a >>= 1;
            b >>= 1;
        }
        return result;
    }
};

第 22 天 位运算(中等)

剑指 Offer 56 - I. 数组中数字出现的次数

原来位运算考虑的是位运算……或者这么说有点怪,就是我以前只是会按位或,按位与

我在想这题的时候,我也是在想,出现两次和出现一次的区别就在于,出现两次的话,在遍历的过程中可以两两消去

然后我就想用累加来获取这个信息,不断累加……但是没有消去的过程

之后看题解才发现,是使用异或

两个相同的数的异或等于 0,两个不相同的数的异或等于 1

然后异或运算是满足交换律的,也就是说 a XOR b XOR c = a XOR c XOR b

那么就可以把所有重复的数字交换到前面,计算得到 00…00,然后得到的就是 a XOR b,a 和 b 是两个只出现一次的数字

然后就要看这个 a XOR b 的性质是什么,才能在第二轮中找出来

然后我也没有看出来这个 a XOR b 有什么特别的

主要是因为我脑子还是想的是用这个 a XOR b 遍历序列,可能会和每一个元素做一些操作,然后会有啥啥啥结果

看了题解发现不是,他题解首先就明确了,如果能够把数组中的元素分为两组,那么并且 a 和 b 在不同组,假设 a 在组 1,那么对组 1 所有元素合在一起异或,因为组内其他元素肯定是两两重复的,所以最后的异或结果是 a;b 同理

所以他首先确定了,要把 a 和 b 分到不同的两组

然后再用这个 a XOR b 来判断怎么给 a 和 b 分组

然后他就发现了,取 a XOR b 的第 i 位并且如果这一位 = 1,那么如果 a 中第 i 位 = 1,b 中的第 i 位就 = 0,反之亦然,这就用 a XOR b 把 a 和 b 区分开了

那么再看其他元素能不能也用 a XOR b 的第 i 位区分呢?其实其他元素使用 a XOR b 的第 i 位来分不一定要有一些意义,只要确保相同的元素在这个标准下都能被分到一组就行了,而不需要管分到不同组的有什么意义。显然,相同的元素在第 i 位上有相同的值,所以可以用 a XOR b 的第 i 位 = 1 来区分

至此就结束了

class Solution {
public:
    vector<int> singleNumbers(vector<int>& nums) {
        // 数组中所有数的异或 = a XOR b
        // a, b 是只出现一次的数
        int xor_all = 0;
        for(int num : nums){
            xor_all ^= num;
        }

        // a XOR b 中从小到大第一个 = 1 的位
        int pos = 0;
        for(; pos < 31; pos++){
            if(xor_all >> pos & 1) break;
        }

        int a = 0, b = 0;
        for(int num : nums){
            if(num >> pos & 1) a ^= num;
            else b ^= num;
        }

        return vector<int>({a, b});
    }
};

剑指 Offer 56 - II. 数组中数字出现的次数 II

上一题是除了两个数只出现了一次之外,其他的数都出现了两次

现在这个是在一个数组 nums 中除一个数字只出现一次之外,其他数字都出现了三次。请找出那个只出现一次的数字。

我一开始还想着对数组遍历一遍,得到 a1^a2^...^an

结果中的第 i 位为 1 表示 a1, a2, …, an 的第 i 位中有奇数个不同的,或者奇数个相同的……?

之后看题解才懂,原来统计也可以用在位运算

主要因为统计里面有求余,这就是和位运算统一的点

比如这里就是,对每一个位上 1 出现的次数做统计,并且 mod3

比如 3 4 3 3

二进制:0011 0100 0011 0011

每一位上的 1 累加:0133

每一位 mod3:0100

具体怎么统计每一位上的 1 的出现次数呢,常见的就是用一个数组来存了,因为是 32 长度,所以空间复杂度是 o(1)

一般到这里就可以了

class Solution {
public:
    int singleNumber(vector<int>& nums) {
        // 数组中所有数的异或 = a1 XOR a2 XOR ... XOR an
        // a1, a2, ..., an 之间互不相等
        vector<int> count(32, 0);
        for(int num : nums){
            for(int i = 0; i < 32; i++){
                count[i] += (num>>i)&1;
            }
        }

        int ans = 0;
        for(int i = 0; i < 32; i++){
            // mod3 之后得到的一定是 0 或 1
            count[i] = count[i]%3;
            ans |= (count[i]<<i);
        }

        return ans;
    }
};

但是还有更快的方法。这里统计 nums 数组第 i 个元素的第 j 位上是否是 1,使用的是循环遍历,看上去还是不够简洁

如果有一种方法只用位运算就好了

有限状态自动机

那么题解进一步给出的方法就是,对每一位,求出一个状态图,然后把这个状态图化简为一个位运算式子

他那里叫做 有限状态自动机

我感觉是,任何一个状态图,最终都能化简成一个位运算式子,只是如果状态图很复杂,那么位运算式子就会很长,如果状态图本身有一点规律,那么位运算式子可能会简洁

状态图就是,定义输入,当前状态,输出状态

先看 32 位数中的某一位作为输入

输入当前状态输出状态
00000
00101
01010
10001
10110
11000

如果只这么看的话,就和单纯的用数组来统计没有区别了

class Solution {
public:
    int singleNumber(vector<int>& nums) {
        // 数组中所有数的异或 = a1 XOR a2 XOR ... XOR an
        // a1, a2, ..., an 之间互不相等
        vector<int> count(32, 0);
        for(int num : nums){
            for(int i = 0; i < 32; i++){
                count[i] += (num>>i)&1;
                count[i] = count[i]%3;
            }
        }

现在要把这个状态图拆成能够用位运算表示的形式

而因为输入是 1 位,状态是 n 位,所以必须要做 n 套位运算

又因为状态是 n 位,为了方便将计算方法从 1 位拓展到 32 位并行,必须将状态字分为 n 个变量

这里具体为什么要拆,还是需要知道后面是怎么从 1 位拓展到 32 位的

假设我们已经知道了,拆了,然后状态图变为

输入当前状态 1当前状态 2输出状态 1输出状态 2
00000
00101
01010
10001
10110
11000

为了方便,换一个名字

输入twoonetwoone
00000
00101
01010
10001
10110
11000

然后就单看某一个状态,例如先看 one

输入oneone
000
011
000
101
110
100

但是一般是不能只看一个状态的,就是说,计算某个状态的变化还可能跟其他状态与有关

例如这里的 one 的倒数第一行和倒数第三行,输入和 one 的旧状态都是一样的,但是结果却不一样,这就要与 two 相关了

if(input == 0){
	one = one;
}
else{
	if(two == 0) one = one ^ input; // 与传统按位加法相同
	else one = 0; // mod3
}

然后继续化简

if(input == 0){
	one = one;
}
else{
	one = (~two) & (one ^ input);
}

一旦能够写出 if else,就可以把这个 if else 化简成位运算

然后继续化简下一层 if else

one = (input & ((~two) & (one ^ input))) | ((~input) & one);

再看 two

if(input == 0){
	two = two;
}
else{
	if(one == 0) two = 0; // 这是直接看的规律
	else two = 1; // 这是直接看的规律
}

第一层 if else 化简

if(input == 0){
	two = two;
}
else{
	two = one;
}

第二层 if else 化简

two = ((~input) & two)|(input & one);

最终写出来的像这样

class Solution {
public:
    int singleNumber(vector<int>& nums) {
        int one = 0, two = 0;
        for(int num : nums){
            one = (num & ((~two) & (one ^ num))) | ((~num) & one);
            two = ((~num) & two)|(num & one);
        }

        return one;
    }
};

虽然这是错的,因为我刚计算完新一轮的 one 就把 one 更新了,而我在推导 two 规则的时候不是根据新一轮的 one 来推导的,而是根据旧一轮的 one 来推导的

所以要是按照我的推导,我应该写成

class Solution {
public:
    int singleNumber(vector<int>& nums) {
        int one = 0, new_one = 0, two = 0;
        for(int num : nums){
            new_one = (num & ((~two) & (one ^ num))) | ((~num) & one);
            two = ((~num) & two)|(num & one);
            one = new_one;
        }

        return one;
    }
};

但是既然发现了这个问题,我可以在推导 two 的计算公式的时候就用新一轮的 one

if(input == 0){
	two = two;
}
else{
	two = ~(two ^ one); // 直接看的规律
}

化简

two = ((~input) & two)|(input & (~(two ^ one)));

最终写成

class Solution {
public:
    int singleNumber(vector<int>& nums) {
        int one = 0, two = 0;
        for(int num : nums){
            one = (num & ((~two) & (one ^ num))) | ((~num) & one);
            two = ((~num) & two)|(num & (~(two ^ one)));
        }

        return one;
    }
};

第 23 天 数学(简单)

剑指 Offer 39. 数组中出现次数超过一半的数字

最简单的就是排序之后取中间序号的元素

class Solution {
public:
    int majorityElement(vector<int>& nums) {
        sort(nums.begin(), nums.end());
        return nums[nums.size()/2];
    }
};

但是排序的最小时间复杂度是 o(nlogn)

应该是还可以降低的

我感觉这种算法就是,如果它的时间复杂度高了,说明他做了额外的事情,说明题目中的条件只需要做更少的事情就能捕捉到答案的信息

之后就是只利用出现的次数的信息

class Solution {
public:
    int majorityElement(vector<int>& nums) {
        if(nums.size() == 1)
            return nums[0];
        int half_len = nums.size()/2;
        unordered_map<int, int> map;
        for(int num : nums){
            if(map.find(num) != map.end()){
                map[num]++;
                if(map[num] > half_len){
                    return num;
                }
            }
            else{
                map.insert(pair<int, int>(num, 1));
            }
        } 
        return 0;
    }
};
摩尔投票法

就是设定第一次遇见的值存为 res,记录他的出现次数

如果下一次遇见的值跟我 res 不一样的话,那么计数值就减 1

如果一样的话就加 1

如果计数值减到 0,说明我下一次遇到值就相当于我第一次遇见值,存为 res

那么最后出现的次数超过一半的那个值肯定会成为 res,因为它的 count 至多是大于 len/2 的


//解法三:摩尔投票法
//也可以理解成混战极限一换一,不同的两者一旦遇见就同归于尽,最后活下来的值都是相同的,即要求的结果
//时间O(n),空间O(1)
class Solution {
public:
    int majorityElement(vector<int>& nums) {
        int res = 0, count = 0;
        for(int i = 0; i < nums.size(); i++){
            if(count == 0){
                res = nums[i];
                count++;
            }
            else
                res==nums[i] ? count++:count--;
        }
        return res;
    }
};

// https://leetcode.cn/problems/shu-zu-zhong-chu-xian-ci-shu-chao-guo-yi-ban-de-shu-zi-lcof/comments/280917

剑指 Offer 66. 构建乘积数组

看了题解才知道的

我们不必将所有数字的乘积除以给定索引处的数字得到相应的答案,而是利用索引左侧所有数字的乘积和右侧所有数字的乘积(即前缀与后缀)相乘得到答案。

class Solution {
public:
    vector<int> constructArr(vector<int>& a) {
        int len = a.size();
        if(len == 0) return vector<int>();
        if(len == 1) return vector<int>(1, 0);

        vector<int> left_sum(len, 1);
        vector<int> right_sum(len, 1);
        for(int i = 0; i < len-1; i++){
            left_sum[i+1] *= left_sum[i] * a[i];
            right_sum[len-2-i] *= right_sum[len-1-i] * a[len-1-i];
        }

        vector<int> b(len, 0);
        for(int i = 0; i < len; i++){
            b[i] = left_sum[i] * right_sum[i];
        }

        return b;
    }
};

之后还有进一步优化是不使用新数组放索引左侧所有数字的乘积和右侧所有数字的乘积

而是第一次 answer 放索引左侧所有数字的乘积

第 24 天 数学(中等)

剑指 Offer 14- I. 剪绳子

一开始我只是以为要取 m 段近似相等的长度

毕竟高中数学都知道 x+y = a, xy 的极值在哪里取到

class Solution {
public:
    int cuttingRope(int n) {
        // 假设已知要分 m 段,最大的乘积位于这 m 段的长度近似相等的时候
        // 而当 m = n 的时候乘积最小,等于 1
        // 因此 m 从 2 开始增大到 n-1,计算 m 段的乘积
        
        int ans = 1;

        int tmp = 0, res = 0;

        for(int m = 2; m < n; m++){
            tmp = pow(m, n/m);
            res = n%m;
            tmp = tmp*res;
            if(ans < tmp) ans = tmp;    
        }

        return ans;
    }
};

但是我之后发现我这个是不对的

比如 n = 10

m分段ans
22 2 2 2 232
33 3 3 127
44 4 232
55 525

但是正确解法是 3 3 4

那么他与我的方法就是相差了最后一段,我的最后一段是 n%m,他的最后一段是 n%m+m,于是我就尝试了一下多考虑他这种情况会怎么样

很 hack……直接过了……

class Solution {
public:
    int cuttingRope(int n) {
        // 假设已知要分 m 段,最大的乘积位于这 m 段的长度近似相等的时候
        // 而当 m = n 的时候乘积最小,等于 1
        // 因此 m 从 2 开始增大到 n-1,计算 m 段的乘积
        
        int ans = 1;

        int tmp = 0, res = 0;

        for(int m = 2; m < n; m++){
            tmp = pow(m, n/m);
            res = n%m;
            tmp = tmp*res;
            if(ans < tmp) ans = tmp; 

            // hack
            // 查看将 res 与一个 m 加在一起之后的结果
            if(n/m > 1){
                tmp = pow(m, n/m-1);
                res = n%m+m;
                tmp = tmp*res;
                if(ans < tmp) ans = tmp;  
            }  
        }

        return ans;
    }
};
执行用时:0 ms, 在所有 C++ 提交中击败了100.00% 的用户
内存消耗:6.1 MB, 在所有 C++ 提交中击败了26.58% 的用户

之后看了一下传统的数学方式,震惊了……居然是要写成函数……虽然很合理

然后就直接算到不会超过 4 之类的……神奇

还看到了别人直接找规律的……真神奇

在这里插入图片描述

剑指 Offer 57 - II. 和为s的连续正数序列

一开始用的笨方法枚举

class Solution {
public:
    vector<vector<int>> findContinuousSequence(int target) {
        // 找到的第一个规律是,最后一个连续正整数序列的最后一个值
        // 一定是 target/2 当 target 为偶数时;target/2+1,当 target 为奇数时
        // 第二个规律是,可以从 last 向后一直找,

        int last = target / 2 + target % 2;

        vector<vector<int>> ans;
        vector<int> tmp_vector;

        int tmp_sum = 0;

        while (last > 1) {
            tmp_vector.clear();
            tmp_sum = 0;

            for (int i = last; i > 0; i--) {
                tmp_sum += i;
                tmp_vector.insert(tmp_vector.begin(), i);
                if (tmp_sum == target) {
                    ans.insert(ans.begin(), tmp_vector);
                    break;
                }
                else if (tmp_sum > target) {
                    break;
                }
            }

            last--;
        }

        return ans;
    }
};

之后看题解才知道用求和公式解方程组

还有双指针……很强

剑指 Offer 62. 圆圈中最后剩下的数字

假设我们最好剩余的数字是N。

执行完“删除第三个元素”的操作后,N在新数组中的位置P的意义是什么? ——它表示,在新数组中,N前面有还有P个元素。那么,在当前数组中,N前面一定有“P+3”个元素。 明白了这一点即可开始倒推。

最后一轮:当前有1个元素。N的位置是0;

倒数第2轮:当前有2个元素。已知在执行完“删除第三个元素”后,N在新数组中的位置是0。则说明在本轮中N前面有0+3=3个元素,所以N的位置是3,然而本轮只有2个元素,所以N的实际位置是(0+3)%2=1;

倒数第3轮:当前有3个元素。已知在执行完“删除第三个元素”后,N’在新数组中的位置是1。说明此刻,N前面有1+3=4个元素,所以N的位置是4。而当前数组只有3个元素,故实际位置是(1+3)%3=1;

https://leetcode.cn/problems/yuan-quan-zhong-zui-hou-sheng-xia-de-shu-zi-lcof/solution/javajie-jue-yue-se-fu-huan-wen-ti-gao-su-ni-wei-sh/866241

class Solution {
public:
    int lastRemaining(int n, int m) {
        int pos = 0;
        // 如果 n = 1,那么直接返回 0
        // i 从 2 开始,表示第一次回溯是考虑到倒数第一和倒数第二个元素总共两个元素
        // 总共需要让 i = n,也就是回到没有删的情况
        for(int i = 2; i <= n; ++i){
            pos = (pos + m)%i;
        }

        return pos;
    }
};

第 25 天 模拟(中等)

剑指 Offer 29. 顺时针打印矩阵

很慢的模拟……设置了四个边界

class Solution {
private:
    int dir[4][2] = { {0, 1},{1, 0},{0, -1},{-1, 0} };
public:
    vector<int> spiralOrder(vector<vector<int>>& matrix) {
        vector<int> ans;

        if (matrix.size() == 0) return ans;

        int row_bound_up = -1, row_bound_bottom = matrix.size();
        int col_bound_left = -1, col_bound_right = matrix[0].size();

        int i = 0, j = 0;
        int dir_idx = 0;

        int count = matrix.size() * matrix[0].size();

        ans.push_back(matrix[i][j]);

        while (1) {
            if(ans.size() == count) break;

            if (row_bound_up == row_bound_bottom || col_bound_left == col_bound_right)
                break;

            if (j + dir[dir_idx][1] == col_bound_right && dir_idx == 0) {
                row_bound_up++;
                dir_idx++;
                continue;
            }
            if (i + dir[dir_idx][0] == row_bound_bottom && dir_idx == 1) {
                col_bound_right--;
                dir_idx++;
                continue;
            }
            if (j + dir[dir_idx][1] == col_bound_left && dir_idx == 2) {
                row_bound_bottom--;
                dir_idx++;
                continue;
            }
            if (i + dir[dir_idx][0] == row_bound_up && dir_idx == 3) {
                col_bound_left++;
                dir_idx = 0;
                continue;
            }

            i += dir[dir_idx][0];
            j += dir[dir_idx][1];

            ans.push_back(matrix[i][j]);
        }

        return ans;
    }
};

剑指 Offer 31. 栈的压入、弹出序列

一开始是模拟栈,每一次看 popped 当前的位置是否等于栈顶,如果相等,就出栈,然后看 popped 的下一个位置;如果不相等就继续入栈,直到 pushed 向量元素都入过栈

等到 pushed 向量元素都入过栈之后,查看 popped 当前位置是否在末尾,如果不在末尾,说明还有栈里面还有剩下的一些元素没有出栈,将他们与 popped 一一对应地出栈,如果出不了,则说明顺序不对

class Solution {
public:
    bool validateStackSequences(vector<int>& pushed, vector<int>& popped) {
        if (pushed.size() == 0) return true;

        int stk_idx = 0, pop_idx = 0;
        stack<int> stk;

        while (1) {
            if (stk.size() != 0){
                if (popped[pop_idx] == stk.top()) {
                    stk.pop();
                    pop_idx++;
                }
                else {
                    if (stk_idx >= pushed.size()) break;
                    stk.push(pushed[stk_idx]);
                    stk_idx++;
                }
            }
            else {
                if (stk_idx >= pushed.size()) break;
                stk.push(pushed[stk_idx]);
                stk_idx++;
            }
        }

        while (pop_idx < popped.size()) {
            if (popped[pop_idx] == stk.top()) {
                stk.pop();
                pop_idx++;
            }
            else {
                return false;
            }
        }

        return true;
    }
};

之后才发现,最后一次不用再与 popped 一一对应地出栈了,只要最后 stack 种还剩元素,就说明不对了

class Solution {
public:
    bool validateStackSequences(vector<int>& pushed, vector<int>& popped) {
        if (pushed.size() == 0) return true;

        int pop_idx = 0;
        stack<int> stk;

        for(int num : pushed){
            stk.push(num);
            while(stk.size() != 0){
                if(stk.top() == popped[pop_idx]){
                    stk.pop();
                    pop_idx++;
                }
                else{
                    break;
                }
            }
        }

        if(stk.size() == 0) return true;
        else return false;
    }
};

第 26 天 字符串(中等)

剑指 Offer 20. 表示数值的字符串

这个去除字符串首尾空位的函数还是要记一下的

string trim(string& s) {
    if (s.empty()) return s;
    s.erase(0, s.find_first_not_of(" "));
    s.erase(s.find_last_not_of(" ") + 1); // 只传入 pos,那就默认删除到 string 的末尾
    return s;
}

他这里可以直接处理边界情况,也就是输出的字符串如果首尾都没有空格会怎么样

可以打印一下他找到的位置

string trim(string& s) {
    if (s.empty()) return s;
    cout << s.find_first_not_of(" ") << endl;
    s.erase(0, s.find_first_not_of(" "));
    cout << s.find_last_not_of(" ") + 1 << endl;
    s.erase(s.find_last_not_of(" ") + 1); // 只传入 pos,那就默认删除到 string 的末尾
    return s;
}

如果输入 “abc”,这里会输出 0 3

也就是说 find_first_not_of 找不到的话就会返回 0,find_last_not_of 找不到的话就会返回 size()

再对 “abc" 使用 find_first_of("e") 输出 4294967295,可见他找不到就输出很大的值

然后就能写了

感觉与其写自动机,还不如写模拟……

class Solution {
private:
    void trim(string& s) {
        if (s.empty()) return;
        s.erase(0, s.find_first_not_of(" "));
        s.erase(s.find_last_not_of(" ") + 1); // 只传入 pos,那就默认删除到 string 的末尾
    }

    bool is_float(string& s) {
        if (s.empty()) return false;

        int idx = 0;
        int num_count = 0;

        if (s[idx] == '+' || s[idx] == '-') {
            idx++;
        }

        while (s[idx] != '.') {
            if (s[idx] >= '0' && s[idx] <= '9') {
                num_count++;
                idx++;

                // 如果找到末尾了还没找到 '.',说明这个字符串不是小数
                if (idx >= s.size()) return false;
            }
            // 如果当前字符不是数字,返回假
            else
                return false;
        }

        idx++;

        // 查看 '.' 后面的字符
        while (idx < s.size()) {
            if (s[idx] >= '0' && s[idx] <= '9') {
                num_count++;
                idx++;
            }
            // 如果当前字符不是数字,返回假
            else
                return false;
        }

        // 如果一路上没找到过数字,说明这个字符串不是小数
        if (num_count == 0) return false;

        return true;
    }

    bool is_int(string& s) {
        if (s.empty()) return false;

        int idx = 0;
        int num_count = 0;

        if (s[idx] == '+' || s[idx] == '-') {
            idx++;
        }

        // 查看字符
        while (idx < s.size()) {
            if (s[idx] >= '0' && s[idx] <= '9') {
                num_count++;
                idx++;
            }
            // 如果当前字符不是数字,返回假
            else
                return false;
        }

        // 如果一路上没找到过数字,说明这个字符串不是小数
        if (num_count == 0) return false;

        return true;
    }
public:
    bool isNumber(string s) {
        trim(s);
        string str1, str2;

        // 如果能找到 e 或 E
        int e_pos = min(s.find_first_of("e"), s.find_first_of("E"));
        if (e_pos < s.size()) {
            if (e_pos == s.size() - 1) return false; // e 或 E 在最后一位,错误
            str1 = s.substr(0, e_pos);
            str2 = s.substr(e_pos + 1, s.size() - e_pos - 1);
            return (is_float(str1) || is_int(str1)) && is_int(str2);
        }
        else {
            return is_float(s) || is_int(s);
        }
    }
};

剑指 Offer 67. 把字符串转换成整数

一开始我是这么判断溢出

// 连续数字
int num = 0, tmp = 0;
// 先判断 idx 是否在边界内,不在则不用执行后面的判断
while (idx < str.size() && str[idx] >= '0' && str[idx] <= '9') {
    num = str[idx] - '0';
    num *= flag;

    tmp = ans;

    ans *= 10;
    ans += num;

    // 如果溢出了
    if ((ans - num) / 10 != tmp) {
        if (flag == 1) return INT_MAX;
        else return INT_MIN;
    }

    idx++;
}

但是他在 ans *= 10; 这一步溢出的时候就报错了,可惜了

于是我觉得应该是预先判断的

// 连续数字
int num = 0, tmp = 0;
// 先判断 idx 是否在边界内,不在则不用执行后面的判断
while (idx < str.size() && str[idx] >= '0' && str[idx] <= '9') {
    num = str[idx] - '0';
    num *= flag;

    tmp = ans;

    // 如果乘 10 之后会溢出
    if (flag == 1) {
        if (ans > INT_MAX / 10) {
            return INT_MAX;
        }
    }
    else {
        if (ans < INT_MIN / 10) {
            return INT_MIN;
        }
    }

    ans *= 10;

    // 如果 + num 之后会溢出
    if (flag == 1) {
        if (ans > INT_MAX - num) {
            return INT_MAX;
        }
    }
    else {
        if (ans < INT_MIN - num) {
            return INT_MIN;
        }
    }

    ans += num;

    idx++;
}

还有它符号的问题,只需要第一个符号,如果有第二个符号就是错的

之后就是模拟

class Solution {
public:
    int strToInt(string str) {
        if (str.empty()) return 0;

        int idx = 0;
        bool has_find_flag = false;
        int flag = 1;

        int ans = 0;

        // 找到第一个数字或符号
        while (str[idx] < '0' || str[idx] > '9') {
            if (str[idx] == '+' || str[idx] == '-') {
                if (str[idx] == '+')
                    flag = 1;
                else
                    flag = -1;
                idx++;
                break;
            }
            // 找到空格,跳过
            else if (str[idx] == ' ') {
                idx++;
            }
            // 找到之外的字符,返回 0
            else {
                return 0;
            }

            // 如果找不到第一个数字,返回 0
            if (idx >= str.size()) return 0;
        }

        // 连续数字
        int num = 0;
        // 先判断 idx 是否在边界内,不在则不用执行后面的判断
        while (idx < str.size() && str[idx] >= '0' && str[idx] <= '9') {
            num = str[idx] - '0';
            num *= flag;

            // 如果乘 10 之后会溢出
            if (flag == 1) {
                if (ans > INT_MAX / 10) {
                    return INT_MAX;
                }
            }
            else {
                if (ans < INT_MIN / 10) {
                    return INT_MIN;
                }
            }

            ans *= 10;

            // 如果 + num 之后会溢出
            if (flag == 1) {
                if (ans > INT_MAX - num) {
                    return INT_MAX;
                }
            }
            else {
                if (ans < INT_MIN - num) {
                    return INT_MIN;
                }
            }

            ans += num;

            idx++;
        }

        return ans;
    }
};

第 27 天 栈与队列(困难)

剑指 Offer 59 - I. 滑动窗口的最大值

一开始用的是一个 vector 存储滑动窗口,滑动窗口先取 k 个数,然后排序,然后滑动窗口每一次移动都通过遍历找到要移出的元素,通过遍历找到要加入的元素要插入的位置

假设每一次删除和插入的平均位置是 n/2,那么每一次都要扫 n 次,那么总共是 n^2 的时间复杂度,所以超时了

class Solution {
public:
    vector<int> maxSlidingWindow(vector<int>& nums, int k) {
        vector<int> ans;
        int len = nums.size();

        if (len == 0) return ans;

        vector<int> window;

        // 1 <= k <= nums.size()
        for (int i = 0; i < k; ++i) {
            window.push_back(nums[i]);
        }

        sort(window.begin(), window.end());

        ans.push_back(window[k - 1]);

        for (int i = 0; i < len - k; ++i) {
            // 删掉离开窗口的

            for (int j = 0; j < k; ++j) {
                if (window[j] == nums[i]) {
                    window.erase(window.begin() + j);
                    break;
                }
            }

            // 加入进入窗口的

            // 如果窗口里面已经没有值了
            if (k - 1 == 0) {
                window.push_back(nums[k + i]);
            }
            // 如果窗口里面还有值
            else {
                for (int j = 0; j < k - 1; ++j) {
                    if (window[j] > nums[k + i]) {
                        window.insert(window.begin() + j, nums[k + i]);
                        break;
                    }
                    // 直到结尾都没找到比 nums[k + i] 大的,那么就把它插入到末尾
                    if (j == k - 2)
                        window.push_back(nums[k + i]);
                }
            }

            ans.push_back(window[k - 1]);
        }

        return ans;
    }
};

之后我本来想写一个二分查找,找第一个大于 nums[k+i] 的元素

然后发现这个事好像不太好找……?

之后看了题解才知道他是用优先队列,也就是堆

其实我也想过用堆,但是我想到,没有一个方法能够移除堆中指定的序号的元素,我就觉得似乎不能用堆

看到他的题解,我才知道,在滑动窗口移动的时候,不需要每移动一步,就移出一个加入一个

如果在每一步都及时地移出一个加入一个,虽然确实是可以及时地保证存储结构中的元素都是滑动窗口中的 k 个,但是没有必要

因为题目在每一步其实不要求你存储结构中的元素一定是 k 个,那么你尽力地保持 k 个其实没必要

又因为它其实要求的是最大值,那么只要保证每一次移动都能取到最大值

那么其实就是保证每一次移动的时候堆里面包含当前滑动窗口所覆盖的 k 个值,不能少了,但是可以多出来,但是多出来的部分不能包括历史的比当前堆顶更大的值

例如 1 2 3 4 1 1 1 窗口大小为 3

1 2 3 输出 3

2 3 4 输出 4

3 4 1 输出 4

4 1 1 输出 4

1 1 1 输出 1

如果不要求堆中的元素一定是 k 个,而是可以超过 k 个的话,那么简单一点想,一直加

1 2 3 堆中元素 1 2 3 输出 3

2 3 4 堆中元素 1 2 3 4 输出 4

3 4 1 堆中元素 1 2 3 4 1输出 4

4 1 1 堆中元素 1 2 3 4 1 1 输出 4

1 1 1 堆中元素 ?输出 1

之后就发现,在最大值 4 离开滑动窗口之后,堆中元素应该都是 1 才能输出为 1

这里就是需要不断移除堆顶

那怎么判断什么时候出,出几个?

观察发现,是当前堆中的最大值在窗口滑动之后离开了窗口,所以要调整堆,这个时候开始移除堆顶

那么什么时候结束呢,一直到堆中的最大值在滑动窗口中为止

那么怎么判断堆中的最大值在滑动窗口中?毕竟堆是无法根据 key 来找 index

那么接下来的神奇操作就是使用一个 pair 放到堆中,堆默认使用 pair 的 first 来排序

那么 pair 的 first 放元素的值,second 放元素在数组中的下标,直接读下标就能知道这个元素在不在滑动窗口中

class Solution {
public:
    vector<int> maxSlidingWindow(vector<int>& nums, int k) {
        vector<int> ans;
        int len = nums.size();

        if (len == 0) return ans;

        priority_queue<pair<int, int>> heap;

        // 初始将 k 个数加入 heap
        for (int i = 0; i < k; ++i) {
            heap.push(pair<int, int>(nums[i], i));
        }

        // 初始有一个堆顶加入 ans
        ans.push_back(heap.top().first);

        // 移动 len-k 次
        for (int i = 1; i <= len - k; ++i) {
            // 新加入滑动窗口的值入堆
            heap.push(pair<int, int>(nums[i+k-1], i+k-1));
            // 如果最大值因为不在滑动窗口内而出堆,那么一直出到最大值在滑动窗口内为止
            // 只用看序号的下界就行了,因为不可能超出上界
            // 因为一直出堆可能导致堆为空,所以先判空
            while (!heap.empty() && heap.top().second < i) {
                heap.pop();
            }
            
            ans.push_back(heap.top().first);
        }

        return ans;
    }
};
单调队列

堆的插入是 logn

最坏的情况,如果输入的数组是单调递增的,那么每一次都是把最大值放到堆的底部,然后需要经过 logn 的重排,总共是 nlogn 的时间复杂度

再狠一点,那就不要这个堆的重排的这个过程,因为这个过程本质上是对滑动窗口的排序,其实我们也不需要对滑动窗口的内部的排序,就是说,取最大值不一定需要排序

从直觉上来说,最大值是一个数与一堆数之间的关系,而排序是这一堆数内部之间两两都存在的关系,前者自然是后者的子集

那么就是说,现在要思考的是每次只维护一个最大值

然后我就看了题解……他的意思就是用一个双端队列,这个队列的首部始终是滑动窗口的最大值,尾部用于新加入值

然后他找到了这个一对多的关系:

由于我们需要求出的是滑动窗口的最大值,如果当前的滑动窗口中有两个下标 ii 和 jj,其中 ii 在 jj 的左侧(i<ji<j),并且 ii 对应的元素不大于 jj 对应的元素(nums[i]≤nums[j]nums[i]≤nums[j]),那么会发生什么呢?

当滑动窗口向右移动时,只要 ii 还在窗口中,那么 jj 一定也还在窗口中,这是 ii 在 jj 的左侧所保证的。因此,由于 nums[j]nums[j] 的存在,nums[i]nums[i] 一定不会是滑动窗口中的最大值了,我们可以将 nums[i]nums[i] 永久地移除。

作者:LeetCode-Solution
链接:https://leetcode.cn/problems/hua-dong-chuang-kou-de-zui-da-zhi-lcof/solution/hua-dong-chuang-kou-de-zui-da-zhi-by-lee-ymyo/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

按照这个关系,其实就是,将要从尾部进来一个新元素时,先将这个新元素与队列尾部的元素比较,如果新元素更大,说明旧元素用不到,那么就把旧元素出队

如果队列为空,那么直接入队

class Solution {
public:
    vector<int> maxSlidingWindow(vector<int>& nums, int k) {
        vector<int> ans;
        int len = nums.size();

        if (len == 0) return ans;

        deque<pair<int, int>> window;

        // 初始将 k 个数加入双端队列
        for (int i = 0; i < k; ++i) {
            // 一开始不需要判断队首的序号,因为一定不用出队首

            // 检查队尾元素是否小于新元素,如果小于,说明以后的最大值与这个队尾元素无关,所以将队尾元素出队
            // 如此一来,这个双端队列会变成一个单调递减的队列
            while (!window.empty() && window.back().first <= nums[i]) {
                window.pop_back();
            }

            window.push_back(pair<int, int>(nums[i], i));
        }

        // 初始的最大值加入 ans
        ans.push_back(window.front().first);

        // 移动 len-k 次
        for (int i = 0; i < len - k; ++i) {
            // 在滑动窗口滑动的时候可能要出队首,因此在这里检查队首的序号是否在滑动窗口之外
            // 既然要检查队首的序号,那么实际上就需要在双端队列中使用 pair,first 存数值,second 存序号
            while (!window.empty() && window.front().second < i+1) {
                window.pop_front();
            }

            // 检查队尾元素是否小于新元素,如果小于,说明以后的最大值与这个队尾元素无关,所以将队尾元素出队
            // 如此一来,这个双端队列会变成一个单调递减的队列
            while (!window.empty() && window.back().first <= nums[i+k]) {
                window.pop_back();
            }

            window.push_back(pair<int, int>(nums[i+k], i+k));
            ans.push_back(window.front().first);
        }

        return ans;
    }
};

剑指 Offer 59 - II. 队列的最大值

一开始我就是按照上一题的思路写单调队列,把每一次 pop_front 视为窗口一次移动

class MaxQueue {
private:
    queue<int> val_queue;
    deque<pair<int, int>> max_queue;
    int left_idx = 0;
    int curr_idx = 0;
public:
    MaxQueue() {

    }
    
    int max_value() {
        if(max_queue.empty()) return -1;

        return max_queue.front().first;
    }
    
    void push_back(int value) {
        val_queue.push(value);

        // 检查队尾元素是否小于新元素,如果小于,说明以后的最大值与这个队尾元素无关,所以将队尾元素出队
        while(!max_queue.empty() && max_queue.back().first <= value){
            max_queue.pop_back();
        }

        max_queue.push_back(pair<int, int>(value, curr_idx++));
    }
    
    int pop_front() {
        if(val_queue.empty()) return -1;

        int val = val_queue.front();
        val_queue.pop();

        // 出了一个元素,相当于窗口移动了一位
        left_idx++;
        // 将在窗口外的队首出队
        while(!max_queue.empty() && max_queue.front().second < left_idx){
            max_queue.pop_front();
        }

        return val;
    }
};

/**
 * Your MaxQueue object will be instantiated and called as such:
 * MaxQueue* obj = new MaxQueue();
 * int param_1 = obj->max_value();
 * obj->push_back(value);
 * int param_3 = obj->pop_front();
 */

但是我这里不知道为啥时间复杂度很低

执行用时:152 ms, 在所有 C++ 提交中击败了5.81% 的用户
内存消耗:47.9 MB, 在所有 C++ 提交中击败了13.24% 的用户

之后发现其实在出队的时候,判断出队的那个元素是不是当前最大值就好了,如果是当前最大值,就

class MaxQueue {
private:
    queue<int> val_queue;
    deque<int> max_queue;
public:
    MaxQueue() {

    }
    
    int max_value() {
        if(max_queue.empty()) return -1;

        return max_queue.front();
    }
    
    void push_back(int value) {
        val_queue.push(value);

        // 检查队尾元素是否小于新元素,如果小于,说明以后的最大值与这个队尾元素无关,所以将队尾元素出队
        while(!max_queue.empty() && max_queue.back() <= value){
            max_queue.pop_back();
        }

        max_queue.push_back(value);
    }
    
    int pop_front() {
        if(val_queue.empty()) return -1;

        int val = val_queue.front();
        val_queue.pop();

        // 如果出队的是当前最大值
        if(val == max_queue.front()){
            max_queue.pop_front();
        }

        return val;
    }
};

/**
 * Your MaxQueue object will be instantiated and called as such:
 * MaxQueue* obj = new MaxQueue();
 * int param_1 = obj->max_value();
 * obj->push_back(value);
 * int param_3 = obj->pop_front();
 */

第 28 天 搜索与回溯算法(困难)

剑指 Offer 37. 序列化二叉树

一开始我还以为可以用前序和中序来重建

之后想到 1 1 1 1 1 这种例子,果然不行,那只是在节点值互不相同的时候才能这么干

然后果然还是只能将二叉树转换为数组,然后在空的地方填 null

剩下的问题,我个人以为,就是怎么把一个可能不是完全二叉树的树记下来,得到他的层高啊之类的

因为我一开始想的是,肯定是层序遍历,但是层序遍历达不到那些为 null 的节点

于是就超时了

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode(int x) : val(x), left(NULL), right(NULL) {}
 * };
 */

using namespace std;

class Codec {
public:

    // Encodes a tree to a single string.
    string serialize(TreeNode* root) {
        string ans_str = "";
        queue<TreeNode*> que;

        que.push(root);

        int layer = 0;
        int null_count = 0;

        TreeNode* tmp_node = nullptr;
        int tmp_count = 0, target_count = 0;
        string tmp_str = "", tmp_layer_str = "";

        while (1) {
            null_count = 0;
            target_count = layer == 0 ? 1 : 2 << (layer-1);

            tmp_layer_str = "";

            // 这里用 que.size() 或者 target_count 都行
            for (int i = target_count; i > 0; --i) {
                tmp_node = que.front();
                que.pop();

                if (tmp_node == nullptr) null_count++;

                tmp_str = tmp_node == nullptr ? "n" : to_string(tmp_node->val);
                tmp_str += ",";

                tmp_layer_str += tmp_str;

                que.push(tmp_node == nullptr ? nullptr : tmp_node->left);
                que.push(tmp_node == nullptr ? nullptr : tmp_node->right);
            }

            if (null_count == target_count) {
                break;
            }

            ++layer;
            ans_str += tmp_layer_str;
        }

        return ans_str;
    }

    // Decodes your encoded data to tree.
    TreeNode* deserialize(string data) {
        int begin = 0, end = 0;
        int curr_idx = 0; // 当前节点在一维数组中的序号
        string tmp_substr = "";
        TreeNode* tmp_node = nullptr;

        vector<TreeNode*> nodes;
        nodes.push_back(nullptr);

        while (end < data.size()) {
            // 一直找直到找到 ','
            while (data[end] != ',') ++end;
            // 取上一次的结尾 + 1 与这一次的结尾,也就是上一次的 ',' 的位置 + 1 与这一次的 ',' 的位置之间的子串
            tmp_substr = data.substr(begin, end - begin);
            // 更新开头与结尾
            begin = end + 1;
            end = begin;
            // 如果为 'n' 说明这个位置的节点为 null,否则就是数字
            tmp_node = tmp_substr == "n" ? nullptr : new TreeNode(stoi(tmp_substr));
            // 不管当前节点是否是有值的,都加入 vector,保证 vector 的序号是与完全二叉树一一对应的
            nodes.push_back(tmp_node);
            // 更新当前节点的序号
            ++curr_idx;
            // 如果当前节点是有值的
            if (tmp_node != nullptr) {
                // 如果当前节点不是根节点
                if (curr_idx / 2 != 0) {
                    // 如果当前节点是父节点的左孩子
                    if (curr_idx % 2 == 0) nodes[curr_idx / 2]->left = tmp_node;
                    // 如果当前节点是父节点的右孩子
                    else nodes[curr_idx / 2]->right = tmp_node;
                }
            }
        }

        if (nodes.size() > 1) return nodes[1];
        else return nullptr;
    }
};

// Your Codec object will be instantiated and called as such:
// Codec codec;
// codec.deserialize(codec.serialize(root));

不过想想也合理,因为层可以很深,而树很稀疏,所以我这样写出完全二叉树的一维数组的长度是以 2 l a y e r 2^{layer} 2layer 增长的

假设最坏情况他每一层都只有一个节点,那么空间和时间复杂度都是 O ( 2 n ) O(2^n) O(2n)

之后看题解才发现,他真的只需要一个遍历就行了

然后这个遍历只需要是先序遍历也可以

这个递归重建的过程中,我一开始还以为,如果只知道一个遍历序列,就没有办法确定根左右的边界,就是说我以为没办法确定左子树和右子树的边界

之后看到他的代码才知道,他其实是可以通过 null 来判断什么时候到达边界的,到达边界就会停下来

比如

     1
      \
       2
      /
     3

那么先序序列化为 1 n 2 3 n

那么一开始取 1 然后直接对后面的 n 2 3 n 递归

那么怎么知道哪个是左子树哪个是右子树?

递归的方式是

1->left = fun(...)
1->right = fun(...)

其中 1->left = fun(...) 遇到 null 就终止了,然后之后 1->right = fun(...) 自然地就取到了剩下的 2 3 n

依次递归

class Codec {
public:

    // Encodes a tree to a single string.
    string serialize(TreeNode* root) {
        string str = "";

        rserialize(root, str);
        return str;
    }

    // 递归函数
    void rserialize(TreeNode* root, string& str) {
        if (root == nullptr) {
            str += "n,";
        }
        else {
            str += to_string(root->val);
            str += ",";
            rserialize(root->left, str);
            rserialize(root->right, str);
        }
    }

    // Decodes your encoded data to tree.
    TreeNode* deserialize(string data) {
        vector<string> str_vec;
        string tmp_str = "";

        for (char cha : data) {
            // 一直找逗号
            if (cha != ',') {
                tmp_str += cha;
            }
            // 找到逗号时将缓存存到 vector<string> 中
            else {
                // 存缓存
                str_vec.push_back(tmp_str);
                // 清空
                tmp_str = "";
            }
        }

        TreeNode* root = rdeserialize(str_vec);

        return root;
    }

    // 递归函数
    TreeNode* rdeserialize(vector<string>& str_vec) {
        if (str_vec.size() == 0) return nullptr;
        // 如果当前查看的为 null
        if (str_vec[0] == "n") {
            // 清除已经检查过的 string
            str_vec.erase(str_vec.begin());
            return nullptr;
        }
        // 如果当前查看的节点有值
        TreeNode* tmp_node = new TreeNode(stoi(str_vec[0]));
        // 清除已经检查过的 string
        str_vec.erase(str_vec.begin());

        // 递归
        tmp_node->left = rdeserialize(str_vec);
        tmp_node->right = rdeserialize(str_vec);

        return tmp_node;
    }
};

其实这种方式是,嗯,让递归函数自己能够决定一个东西的边界,就很神奇,学到了

就,通常使用的前序、中序、后序、层序遍历记录的二叉树的信息不完整,即唯一的输出序列可能对应着多种二叉树可能性

但是你加了一个 null 在结果中,事情就不一样了


然后还看到了一个是使用括号的

T -> (T) num (T) | X

这个递归函数传入两个参数,带解析的字符串和当前当解析的位置 ptr,ptr 之前的位置是已经解析的,ptr 和 ptr 后面的字符串是待解析的
如果当前位置为 X 说明解析到了一棵空树,直接返回
否则当前位置一定是 (,对括号内部按照 (T) num (T) 的模式解析

作者:LeetCode-Solution
链接:https://leetcode.cn/problems/xu-lie-hua-er-cha-shu-lcof/solution/xu-lie-hua-er-cha-shu-by-leetcode-soluti-4duq/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

剑指 Offer 38. 字符串的排列

一开始用的模拟,然后超出内存限制了

class Solution {
public:
    vector<string> permutation(string s) {
        vector<string> ans;
        vector<pair<char, bool>> chars;

        for(char cha : s){
            chars.push_back(pair<char, bool>(cha, false));
        }

        rpermutation(ans, chars, "");

        return ans;
    }

    void rpermutation(vector<string>& ans, vector<pair<char, bool>>& chars, string prefix){
        if(chars.size() == 0) return;

        int used_count = 0;

        for(pair<char, bool> cha : chars){
            if(cha.second == false){
                cha.second = true;
                rpermutation(ans, chars, prefix + cha.first);
                cha.second = false;
            }
            else{
                used_count++;
            }
        }

        if(used_count == chars.size()){
            ans.push_back(prefix);
        }
    }
};

然后就必须要观察这个规律了

输入:s = “abc”
输出:[“abc”,“acb”,“bac”,“bca”,“cab”,“cba”]

我一开始的想法是给定 str.size() 个位置,然后把这些字符放到这些位置上

不知道可不可以用插入的方法

比如我顺序遍历 str,然后每取到一个字符都可以插入到现有的字符串的任意位置

比如一开始插 a

然后插 b,可以放到 a 的前面或者后面

那么就是 ans = [“ab”, “ba”]

然后再插 c

可以插到 ans 的各个字符串的各个位置

比如对于 “ab”,c 可以插三个位置,对于 “ba” 同理

这样的话我就不用递归了

于是写成

class Solution {
public:
    vector<string> permutation(string s) {
        vector<string> ans;
        string tmp1 = "", tmp2 = "";

        for(char cha : s){
            // 如果 ans 为空,只插入一个字符
            if(ans.size() == 0){
                tmp1.insert(tmp1.begin(), cha);
                ans.push_back(tmp1);
            }
            // 如果 ans 不为空
            else{
                // 对于 ans 中每一个字符串,使用 cha 插入到这个字符串的任意位置
                // 一开始取 ans.size(),之后就算 ans 追加了新的字符串,也与本轮无关
                for(int i = ans.size()-1; i >= 0; --i){
                    tmp1 = ans[i];
                    ans[i].insert(ans[i].begin(), cha);
                    for(int j = 1; j <= tmp1.size(); ++j){
                        tmp2 = tmp1;
                        tmp2.insert(tmp2.begin() + j, cha);
                        ans.push_back(tmp2);
                    }
                }
            }
        }

        return ans;
    }
};

但是我这个是全排列,对于重复字符会出错,会得到一些重复的顺序

输入:
"aab"
输出:
["baa","baa","aba","aab","aba","aab"]
预期结果:
["aba","aab","baa"]

对于这个例子

一开始 a

然后第二个 a 应该直接追加

本来是得到 [“aa”, “aa”],现在应该是得到 [“aa”]

然后再插 b

那如果是输入 ”abb“

前面的输入 [ba, ab]

现在插入 b 得到 [bba, bab, abb, abb, bba, bab]

我想了一会不知道怎么做……于是去看题解

发现原来题解第一个思路是我一开始想的方法

其实我一开始想的是对的

至于我一开始为什么会内存溢出,是因为我是用了 for(pair<char, bool> cha : chars){ 然后只对这个 cha 操作

实际上这里取到的 cha 相当于复制了一份,而不是取到地址,没注意到这个 C++ 特性hhh

修改之后就好了

class Solution {
public:
    vector<string> permutation(string s) {
        vector<string> ans;
        vector<pair<char, bool>> chars;

        for (char cha : s) {
            chars.push_back(pair<char, bool>(cha, false));
        }

        rpermutation(ans, chars, "");

        return ans;
    }

    void rpermutation(vector<string>& ans, vector<pair<char, bool>>& chars, string prefix) {
        if (chars.size() == 0) return;

        int used_count = 0;

        for (int i = 0; i < chars.size(); ++i) {
            if (chars[i].second == false) {
                chars[i].second = true;
                rpermutation(ans, chars, prefix + chars[i].first);
                chars[i].second = false;
            }
            else {
                used_count++;
            }
        }

        if (used_count == chars.size()) {
            ans.push_back(prefix);
        }
    }
};

然后他这个防止重复的思路就很神奇

具体地,我们只要在递归函数中设定一个规则,保证在填每一个空位的时候重复字符只会被填入一次。具体地,我们首先对原字符串排序,保证相同的字符都相邻,在递归函数中,我们限制每次填入的字符一定是这个字符所在重复字符集合中「从左往右第一个未被填入的字符」,即如下的判断条件:

作者:LeetCode-Solution
链接:https://leetcode.cn/problems/zi-fu-chuan-de-pai-lie-lcof/solution/zi-fu-chuan-de-pai-lie-by-leetcode-solut-hhvs/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

他会这么想主要是因为他对问题的描述是准确的

对于任意一个空位,如果存在重复的字符,该递归函数会将它们重复填上去并继续尝试导致最后答案的重复。

以 abb 为例子

   b  b
a 
   b  b
   
   a  b
b
   b  a

   a  b
b
   b  a

问题在于相同位置上填入了重复的字符

   b  b
a 
   (b)  b
   
   a  b
b
   b  a

   a  b
(b)
   b  a

所以某一个位置上不能填重复的字符,只能在剩下的不重复的字符中选,没得选就提前结束

那么怎么知道我有哪些重复字符?

既然我现在有一个 string,遍历一遍就得到一个 vector<char>,那么我只要排一遍序,就能让重复的数字相邻

然后我在遍历的时候我已经知道了哪些字符是用过的,所以一旦这个重复的相邻数字之中有一个是用过的,那么我就不用其他的重复数字

具体说就是:

一开始一个乱序的 bdbada 首先排成 aabbdd

然后 vector<char> 其实可以看成 aa bb dd

一开始,对这个 vector 扫一遍

i = 0,取到 'a',设置 chars[0].second = true,递归

然后直接往下走,找第一个不是 'a' 的字符,重复以上步骤

于是写成

class Solution {
public:
    vector<string> permutation(string s) {
        vector<string> ans;
        vector<pair<char, bool>> chars;

        for (char cha : s) {
            chars.push_back(pair<char, bool>(cha, false));
        }

        sort(chars.begin(), chars.end());

        rpermutation(ans, chars, "");

        return ans;
    }

    void rpermutation(vector<string>& ans, vector<pair<char, bool>>& chars, string prefix) {
        if (chars.size() == 0) return;

        // 回溯
        if (prefix.size() == chars.size()) {
            ans.push_back(prefix);
            return;
        }

        // old_cha 初始值为 '.' 表示无记录
        char old_cha = '.';
        for (int i = 0; i < chars.size(); ++i) {
            // 跳过重复字符
            // 经过 for 自带的 ++i 之后,再开始尝试跳过重复字符,以防止两个 ++i 互相影响
            while (i < chars.size() && chars[i].first == old_cha) {
                ++i;
            }
            // 如果当前指向的字符没用过
            if (i < chars.size() && chars[i].second == false) {
                // 将当前指向的字符标记为已使用
                chars[i].second = true;
                // 递归
                rpermutation(ans, chars, prefix + chars[i].first);
                // 将当前指向的字符回复为未使用
                chars[i].second = false;
                // 记录上一次的重复字符
                old_cha = chars[i].first;
            }
        }
    }
};
31. 下一个排列

他这个题还可以是找下一个排列。就是首先从小到大排,那么就是字典序最小,然后一直排,排到不能排,就是得到所有的排列

整数数组的一个 排列 就是将其所有成员以序列或线性顺序排列。

例如,arr = [1,2,3] ,以下这些都可以视作 arr 的排列:[1,2,3]、[1,3,2]、[3,1,2]、[2,3,1] 。

整数数组的 下一个排列 是指其整数的下一个字典序更大的排列。更正式地,如果数组的所有排列根据其字典顺序从小到大排列在一个容器中,那么数组的 下一个排列 就是在这个有序容器中排在它后面的那个排列。如果不存在下一个更大的排列,那么这个数组必须重排为字典序最小的排列(即,其元素按升序排列)。

例如,arr = [1,2,3] 的下一个排列是 [1,3,2] 。
类似地,arr = [2,3,1] 的下一个排列是 [3,1,2] 。
而 arr = [3,2,1] 的下一个排列是 [1,2,3] ,因为 [3,2,1] 不存在一个字典序更大的排列。

给你一个整数数组 nums ,找出 nums 的下一个排列。

必须 原地 修改,只允许使用额外常数空间。

来源:力扣(LeetCode)
链接:https://leetcode.cn/problems/next-permutation
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

一开始,我还以为就是简单的冒泡

class Solution {
public:
    void nextPermutation(vector<int>& nums) {
        // 1 2 3
        // 1 3 2
        // 3 1 2
        // 3 2 1
        // 1 2 3
        // 规律就是逐个把后面的大的数移到前面去
        // 也就是找到一个 (小数, 大数) 然后互换位置
        // 如果找不到,就 reverse

        for(int i = nums.size()-1; i > 0; --i){
            if(nums[i-1] < nums[i]){
                swap(nums[i-1], nums[i]);
                return;
            }
        }

        reverse(nums.begin(), nums.end());
    }
};

但是之后我发现我想错了

1 2 3

1 3 2

2 1 3

2 3 1

3 1 2

3 2 1

1 2 3

字典顺序,就是字母从 a 到 z,或者数字从小到大

那么对于数字来说,就是 [1 2 3] 代表 123,[1 3 2] 代表 132,123 的下一个是 132,然后是 2 开头的

再看别人的 4 个数的

1 2 3 4
1 2 4 3
1 3 2 4
1 3 4 2
1 4 2 3
1 4 3 2
2 1 3 4
2 1 4 3
2 3 1 4
2 3 4 1
2 4 1 3
2 4 3 1
3 1 2 4
3 1 4 2
3 2 1 4
3 2 4 1
3 4 1 2
3 4 2 1
4 1 2 3
4 1 3 2
4 2 1 3
4 2 3 1
4 3 1 2
4 3 2 1

题解:

注意到下一个排列总是比当前排列要大,除非该排列已经是最大的排列。我们希望找到一种方法,能够找到一个大于当前序列的新序列,且变大的幅度尽可能小。具体地:

  1. 我们需要将一个左边的「较小数」与一个右边的「较大数」交换,以能够让当前排列变大,从而得到下一个排列。

  2. 同时我们要让这个「较小数」尽量靠右,而「较大数」尽可能小。当交换完成后,「较大数」右边的数需要按照升序重新排列。这样可以在保证新排列大于原来排列的情况下,使变大的幅度尽可能小。

作者:LeetCode-Solution
链接:https://leetcode.cn/problems/next-permutation/solution/xia-yi-ge-pai-lie-by-leetcode-solution/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

最后我按照这个思路写的是

class Solution {
public:
    void nextPermutation(vector<int>& nums) {
        int left = -1;

        // 第一步:在整个数组中,从后往前找第一个 nums[i-1] < nums[i]
        // 这样可以使得用于交换的大数尽可能靠右
        // 如果没找到,说明是 nums[i-1] >= nums[i],那么第一次找到的时候,left 后面的数是递减的
        // 那么这里的 nums[i-1] 就是较小数
        for (int i = nums.size() - 1; i > 0; --i) {
            if (nums[i - 1] < nums[i]) {
                left = i - 1;
                break;
            }
        }

        // 如果没有找到 nums[i-1] < nums[i],直接反转整个数组
        if (left == -1) {
            reverse(nums.begin(), nums.end());
            return;
        }

        // 第二步:在数组的 [left, len] 中,从后往前找第一个 nums[left] < nums[j]
        // 这样可以使得用于交换的大数尽可能小
        // 同时,因为 nums[left] 之后的数是递减的,所以一定能够找到一个 nums[i] 满足 nums[left] < nums[j]
        // 最差的情况就是 j = left - 1
        for (int j = nums.size() - 1; j > left; --j) {
            if (nums[left] < nums[j]) {
                swap(nums[left], nums[j]);
                // 第三步,反转 [left+1, len]
                reverse(nums.begin() + left + 1, nums.end());
                break;
            }
        }
    }
};

然后回到这个题,复用获得下一个排列的函数

class Solution {
public:
    vector<string> permutation(string s) {
        vector<string> ans;
        vector<char> chars;

        for (char cha : s) {
            chars.push_back(cha);
        }

        sort(chars.begin(), chars.end());

        do {
            string tmp = "";
            for (char cha : chars) {
                tmp += cha;
            }
            ans.push_back(tmp);
        } while (nextPermutation(chars));

        return ans;
    }

    // 新增一个 bool 返回值,用于判断是否不能得到下一个排列
    bool nextPermutation(vector<char>& nums) {
        int left = -1;

        // 第一步:在整个数组中,从后往前找第一个 nums[i-1] < nums[i]
        // 这样可以使得用于交换的大数尽可能靠右
        // 如果没找到,说明是 nums[i-1] >= nums[i],那么第一次找到的时候,left 后面的数是递减的
        // 那么这里的 nums[i-1] 就是较小数
        for (int i = nums.size() - 1; i > 0; --i) {
            if (nums[i - 1] < nums[i]) {
                left = i - 1;
                break;
            }
        }

        // 如果没有找到 nums[i-1] < nums[i],已经没有下一个排列了
        if (left == -1) {
            return false;
        }

        // 第二步:在数组的 [left, len] 中,从后往前找第一个 nums[left] < nums[j]
        // 这样可以使得用于交换的大数尽可能小
        // 同时,因为 nums[left] 之后的数是递减的,所以一定能够找到一个 nums[i] 满足 nums[left] < nums[j]
        // 最差的情况就是 j = left - 1
        for (int j = nums.size() - 1; j > left; --j) {
            if (nums[left] < nums[j]) {
                swap(nums[left], nums[j]);
                // 第三步,反转 [left+1, len]
                reverse(nums.begin() + left + 1, nums.end());
                break;
            }
        }

        return true;
    }
};

第 29 天 动态规划(困难)

剑指 Offer 19. 正则表达式匹配

对于 '*',一开始我是写成直接的贪婪

class Solution {
public:
    bool isMatch(string s, string p) {
        int s_idx = 0, p_idx = 0;
        char curr_char = '/'; // 如果 '*' 出现在模式串的开头,那么使用一个绝对不会在源串中出现的字符来比较

        while(p_idx < p.size() && s_idx < s.size()){
            // 如果模式串当前的字符不为 '*'
            if(p[p_idx] != '*'){
                // 模式串当前的字符不为 '*',那么就记录下来,为了以后模式串的当前字符为 '*' 时用于比较
                curr_char = p[p_idx];
                // 如果当前两个字符不匹配,找下一个字符是否是 '*'
                if(!isCharMatch(s[s_idx], curr_char)){
                    // 如果下一个字符是 '*',那么 p 的指针++,进入下一个循环体
                    if(p_idx+1 < p.size() && p[p_idx+1] == '*'){
                        p_idx++;
                        continue;
                    }
                    // 如果没有下一个字符,或者下一个字符不是 '*',匹配失败
                    else{
                        return false;
                    }
                }
                // 如果当前两个字符匹配,两个指针都++,进入下一个循环体
                else{
                    s_idx++;
                    p_idx++;
                    continue;
                }
            }
            // 如果模式串当前的字符为 '*'
            else{
                // 如果不匹配,那么还可以跳过
                if(!isCharMatch(s[s_idx], curr_char)){
                    p_idx++;
                    continue;
                }
                // 如果匹配,那么贪婪匹配,只有源串的指针++,
                else{
                    s_idx++;
                    continue;
                }
            }
        }

        // 一般的情况下,如果匹配成功,双指针都应该走到末尾
        if(p_idx == p.size() && s_idx == s.size()) return true;
        // 但是对于这个应用场景,还有 '*' 作为模式串结尾的特殊例子
        // 这时,由于贪婪匹配的原则,源串的指针先达到末尾,而模式串的指针一直留在 '*'
        else{
            if(p_idx == p.size()-1 && p[p_idx] == '*'){
                return true;
            }
            else{
                return false;
            }
        }
    }

    bool isCharMatch(char a, char b){
        return b == '.' ? true : a == b;
    }
};

但是这样就过不了 '*' 不是尽量匹配的情况

输入:
"aaa"
"a*a"
输出:
false
预期结果:
true

主要是因为我是从前往后一个一个看模式串的,我不知道模式串的 '*' 后面有什么,所以我就想着,保守起见,还是尽量让 '*' 把能匹配的都匹配了

但是我想到我的字符串在失配的时候,也要在模式串向后看一位,看一下是不是 '*',因此我觉得让 '*' 向后看一位似乎也不是不能接受

就是逻辑又更加复杂了

class Solution {
public:
    bool isMatch(string s, string p) {
        int s_idx = 0, p_idx = 0;

        char curr_char = '/'; // 如果 '*' 出现在模式串的开头,那么使用一个绝对不会在源串中出现的字符来比较
        int match_count = 0; // 上一次 '*' 与源串匹配的次数

        while (p_idx < p.size() && s_idx < s.size()) {
            // 如果模式串当前的字符不为 '*'
            if (p[p_idx] != '*') {
                // 模式串当前的字符不为 '*',那么就记录下来,为了以后模式串的当前字符为 '*' 时用于比较
                curr_char = p[p_idx];
                // 如果当前两个字符不匹配,找下一个字符是否是 '*'
                if (!isCharMatch(s[s_idx], curr_char)) {
                    // 如果下一个字符是 '*',那么 p 的指针++,进入下一个循环体
                    if (p_idx + 1 < p.size() && p[p_idx + 1] == '*') {
                        p_idx++;
                        continue;
                    }
                    // 如果没有下一个字符,或者下一个字符不是 '*',匹配失败
                    else {
                        return false;
                    }
                }
                // 如果当前两个字符匹配,两个指针都++,进入下一个循环体
                else {
                    s_idx++;
                    p_idx++;
                    continue;
                }
            }
            // 如果模式串当前的字符为 '*'
            else {
                // 如果不匹配,那么还可以跳过
                if (!isCharMatch(s[s_idx], curr_char)) {
                    // 这里要决定跳过多少个
                    while (p_idx + 1 < p.size() && p[p_idx + 1] == curr_char && match_count > 0) {
                        p_idx++;
                        match_count--;
                    }

                    p_idx++;
                    match_count = 0;
                    continue;
                }
                // 如果匹配,那么贪婪匹配,只有源串的指针++
                // 但是也不能一直贪婪,因为可能出现:
                // 输入:
                // "aaa"
                // "a*a"
                // 输出:
                // false
                // 预期结果:
                // true
                // 因此还需要从 '*' 向后看
                // 我想到的方法是记录一下这个 '*' 匹配了多少次
                // 然后在 '*' 适配的时候向后看,模式串里面有多少个连续的 curr_char
                // 有一个就 match_count--,直到 match_count < 0 时匹配失败
                else {
                    s_idx++;
                    match_count++;
                    continue;
                }
            }
        }

        // 一般的情况下,如果匹配成功,双指针都应该走到末尾
        if (p_idx == p.size() && s_idx == s.size()) return true;
        // 如果 p 当前的字符是 '*' 可能还需要再来一次跳过
        else if (p_idx < p.size() && p[p_idx] == '*') {
            // 跳过
            bool has_jumped = false;
            while (p_idx + 1 < p.size() && p[p_idx + 1] == curr_char && match_count > 0) {
                has_jumped = true;
                p_idx++;
                match_count--;
            }
            if (has_jumped) p_idx++;
            if (p_idx == p.size() && s_idx == s.size()) return true;

            // 但是对于这个应用场景,还有 '*' 作为模式串结尾的特殊例子
            // 这时,由于贪婪匹配的原则,源串的指针先达到末尾,而模式串的指针一直留在 '*'
            if (p_idx == p.size() - 1 && p[p_idx] == '*') {
                return true;
            }
            else {
                return false;
            }
        }

        return false;
    }

    bool isCharMatch(char a, char b) {
        return b == '.' ? true : a == b;
    }
};

这样的话又过不了

输入:
"aaa"
"ab*a*c*a"
输出:
false
预期结果:
true

也就是说在一个 '*' 的过程中

果然模拟就是一个错误,

然后瞟了一眼是用动规

然后我一开始也想错了

他有一个例子是

输入:
s = "ab"
p = ".*"
输出: true

它的意思就是 '.' 这个东西可以和任意字符匹配,而不是我一开始以为的,如果匹配了 'a' 就不能匹配 'b' 这种

那我一开始就是想着

d p [ i ] [ j ] = { i f ( s [ i ] = = p [ j ] ) , { i f ( i − 1 < 0 ∣ ∣ j − 1 < 0 ) , d p [ i ] [ j ] = t r u e ; e l s e , d p [ i ] [ j ] = d p [ i − 1 ] [ j − 1 ] ; e l i f ( p [ j ] = = ∗ ) , { i f ( j − 1 < 0 ) , d p [ i ] [ j ] = f a l s e ; e l s e , d p [ i ] [ j ] = d p [ i ] [ j − 1 ] ; e l i f ( p [ j ] = = . ) , d p [ i ] [ j ] = t r u e ; e l s e d p [ i ] [ j ] = f a l s e ; dp[i][j] = \begin{cases} if (s[i] == p[j]), \begin{cases} if (i-1 < 0 || j-1 < 0), dp[i][j] = true; \\ else, dp[i][j] = dp[i-1][j-1]; \end{cases} \\ elif (p[j] == *), \begin{cases} if (j-1 < 0), dp[i][j] = false; \\ else, dp[i][j] = dp[i][j-1]; \end{cases} \\ elif (p[j] == .), dp[i][j] = true; \\ else dp[i][j] = false; \end{cases} dp[i][j]= if(s[i]==p[j]),{if(i1<0∣∣j1<0),dp[i][j]=true;else,dp[i][j]=dp[i1][j1];elif(p[j]==),{if(j1<0),dp[i][j]=false;else,dp[i][j]=dp[i][j1];elif(p[j]==.),dp[i][j]=true;elsedp[i][j]=false;

class Solution {
public:
    bool isMatch(string s, string p) {
        vector<vector<bool>> dp(s.size(), vector<bool>(p.size(), 0));

        for(int i = 0; i < s.size(); ++i){
            for(int j = 0; j < p.size(); ++j){
                if(s[i] == p[j]){
                    if(i-1<0 || j-1<0) dp[i][j] = true;
                    else dp[i][j] = dp[i-1][j-1];
                }
                else{
                    if(p[j] == '*'){
                        if(j-1<0) dp[i][j] = false;
                        else dp[i][j] = dp[i][j-1];
                    }
                    else if(p[j] == '.') dp[i][j] = true;
                    else dp[i][j] = 0;
                }
            }
        }

        return dp[s.size()-1][p.size()-1];
    }
};

但是这样的思路也不对……主要是因为,我还是觉得他是一个单项扫描模式串的过程……现在像这样做一个 dp 数组,比如对于

输入:
s = "aa"
p = "a"
输出: false

我这样想的 dp 会得到 dp[0][0], dp[0][1] 这样对于这两个元素的判断就相当于模式串的第一个 a 匹配了源串的两个位置,就很怪

之后去看了题解,发现题解想的更有逻辑……

就是说,dp[i][j] 表示字符串 s 的第 i 个元素是否能与字符串 p 的第 j 个元素匹配

然后判断 dp[i][j] 等于什么值,首先是,s[i]p[j] 匹配时,要考虑 p[j] 是什么

如果 p[j] != '*',按照双指针顺序前进的思路,那么 i 和 j 都应该是自增 1,那么在动规里面就是当前状态由之前的状态决定嘛,所以是 dp[i]j[j] = dp[i-1][j-1]

如果 p[j] == '*',按照双指针前进的思路,那么 i 前进一步,j 不动,在动规里面就是 dp[i]j[j] = dp[i-1][j]

s[i]p[j] 不匹配时,要考虑 p[j] 是什么

如果 p[j] != '*' 那么这一次的不匹配就是完完全全的不匹配,直接等于 false

如果 p[j] == '*' 那么这一次的不匹配可能是星号已经结束了它的任意次匹配,所以:

这一次的匹配值应该等于上一次的匹配值,而上一次的匹配值就是 dp[i][j-2]

这个上一次的匹配值


不对

我这里总是想错了,我其实要的是要是想着双指针一步步走的话,就会忽略一些细节

就是比如星号这种东西,其实他的出现就会让匹配由原来的一对一变成二对任意

就,其实,问题的性质已经变了,双指针这种一个一个移动的话,就,要考虑这种情况就会很难受

而在动规中的话,就是要两个两个考虑

但是其实所谓的两个两个考虑,也需要先知道当前的 p[j] 是否是 '*'

所以就是,首先先判断是否需要对模式串中的字符一组两个进行考虑,然后在考虑状态转移方程怎么写

所以方程应该先写为

d p [ i ] [ j ] = { i f ( p [ j ] ! = ∗ ) , . . . e l s e , . . . dp[i][j] = \begin{cases} if(p[j] != *), ... \\ else, ... \end{cases} dp[i][j]={if(p[j]!=),...else,...

因为显然传统的一对一比较好考虑,所以先考虑 p[j] 不为 '*' 的情况

这个时候要考虑的就是 s[i]p[j],因为是一对一嘛

那么就很简单,如果不匹配,那么直接 false;如果匹配,那么本次的匹配状态由上一次决定

上一次在这里就是 dp[i-1][j-1]

那么状态方程写为

d p [ i ] [ j ] = { i f ( p [ j ] ! = ∗ ) , { i f ( s [ i ] = = p [ j ] ) , d p [ i − 1 ] [ j − 1 ] e l s e , f a l s e e l s e , . . . dp[i][j] = \begin{cases} if(p[j] != *), \begin{cases} if(s[i] == p[j]), dp[i-1][j-1] \\ else, false \end{cases} \\ else, ... \end{cases} dp[i][j]= if(p[j]!=),{if(s[i]==p[j]),dp[i1][j1]else,falseelse,...

如果 p[j] == '*',那就要考虑模式串中两个字符作为一个组合

那么其实对比的是 s[i]p[j-1],判断它们是否相等

如果相等的话,说明正在处于星号连续匹配的过程中,不管是刚开始连续匹配还是已经连续匹配了一段时间了,那么本次的匹配状态应该由上一次决定

这里的上一次会有所不同,因为现在是处于连续匹配的过程中,所以上一步应该是 dp[i-1][j]

而如果不相等的话,说明星号连续匹配终止了,那么这个时候有两种情况,就是星号一次都没有匹配,以及星号已经匹配了若干次了

如果是星号一次都没有匹配,那么本次的状态由上一次的,进入两个字符一组之前的状态决定,也就是 dp[i][j-2]

如果是星号已经匹配了很多次了,那么本次的状态由上一次星号连续匹配的的状态决定……?


之后看了别人的题解,感觉自己还是想错了

在这里插入图片描述

对于如果 p[j] == '*',那就要考虑模式串中两个字符作为一个组合,这种情况

如果 s[i]p[j-1] 不相等的话,匹配终止,只需要本次的状态由上一次的,进入两个字符一组之前的状态决定,也就是 dp[i][j-2]

如果相等的话,他这里就会考虑多个跳跃

其实我一开始也不知道这个多个跳跃是用来干嘛的……之前我再看一遍才想到,他应该是这样的思路

就是其实如果 dp[i1][j1] = dp[i2][j2] 其实它的意思是 i1 到 i2,j1 到 j2 之间的东西都对不管是真是假的这个上一次的 dp[i2][j2] 的结果没有影响

所以就可以直接“跳”


然后他之后的那个总结的意思就是,在 s[i]p[j-1] 相等时,每一个星号是可以选择自己是继续连续匹配还是提前终止连续匹配

所以是 dp[i-1][j] || dp[i][j-2] 都可以

d p [ i ] [ j ] = { i f ( p [ j ] ! = ∗ ) , { i f ( s [ i ] = = p [ j ] ) , d p [ i − 1 ] [ j − 1 ] e l s e , f a l s e e l s e , { i f ( s [ i ] = = p [ j − 1 ] ) , d p [ i − 1 ] [ j ] ∣ ∣ d p [ i ] [ j − 2 ] e l s e , d p [ i ] [ j − 2 ] dp[i][j] = \begin{cases} if(p[j] != *), \begin{cases} if(s[i] == p[j]), dp[i-1][j-1] \\ else, false \end{cases} \\ else, \begin{cases} if(s[i] == p[j-1]), dp[i-1][j] || dp[i][j-2] \\ else, dp[i][j-2] \end{cases} \end{cases} dp[i][j]= if(p[j]!=),{if(s[i]==p[j]),dp[i1][j1]else,falseelse,{if(s[i]==p[j1]),dp[i1][j]∣∣dp[i][j2]else,dp[i][j2]

之后再考虑特殊字符 '.'

然后我就写成

class Solution {
public:
    bool isMatch(string s, string p) {
        vector<vector<bool>> dp(s.size(), vector<bool>(p.size(), 0));

        for(int i = 0; i < s.size(); ++i){
            for(int j = 0; j < p.size(); ++j){
                if(p[j] != '*'){
                    if(isCharMatch(s[i], p[j])){
                        dp[i][j] = get_dp(dp, i-1, j-1);
                    }
                    else dp[i][j] = false;
                }
                else{
                    if(isCharMatch(s[i], p[j-1])) dp[i][j] = get_dp(dp, i-1, j) || get_dp(dp, i, j-2);
                    else dp[i][j] = get_dp(dp, i, j-2);
                }
            }
        }

        return dp[s.size()-1][p.size()-1];
    }

    bool isCharMatch(char a, char b) {
        return b == '.' ? true : a == b;
    }

    bool get_dp(vector<vector<bool>>& dp, int i, int j){
        if(i < 0 || j < 0) return true;
        else return dp[i][j];
    }
};

这样写会错,我主要是把超过边界的值全部都设置为了 true

输入
"aa"
"a"
输出
true
预期结果
false

再看了别人的题解:

3.初始化:
      3.1 空的p 
          3.1.1 可以匹配空的s,dp[0][0]=true
          3.1.2 不可以匹配非空的s,dp[i][0]=false,i∈[1,m-1]
      3.2 空的s
          3.2.1 可以匹配空的s,dp[0][0]=true
          3.2.2 可能可以匹配非空的p,要经过计算如"a*b*c*"
      3.3 非空的p与非空的s,要经过计算

才发现别人考虑得很周全,就是空的 p 对应得情况和空的 s 对应的情况都考虑到了

以前的我是

s\p0(a)
0(a)
1(a)

要考虑边界的话

s\p01(a)
0x
1(a)x
2(a)xx

边界的话,如果要把考虑边界的代码和不考虑边界的代码放到一起,就会很复杂,我感觉

于是我分成了两个部分

于是

class Solution {
public:
    bool isMatch(string s, string p) {
        vector<vector<bool>> dp(s.size()+1, vector<bool>(p.size()+1, false));

        // tmp
        char first, second;

        // dp[0][0] 表示源串没有字符,模式串没有字符,匹配成功
        dp[0][0] = true;

        int i = 0, j = 0;
        
        // dp[0][1] 表示源串没有字符,模式串只有一个字符,一定匹配不成功

        // 初始化 i = 0 时每一列的值
        for(i = 0, j = 2; j <= p.size(); ++j){
            if(p[j-1] != '*'){
                dp[i][j] = false;
            }
            else{
                dp[i][j] = dp[i][j-2]; // 这里可能出现 true 哦
            }
        }

        // 初始化 j = 1 时每一行的值
        for(j = 1, i = 1; i <= s.size(); ++i){
            if(p[j-1] != '*'){
                if(isCharMatch(s[i-1], p[j-1])){
                    dp[i][j] = dp[i-1][j-1];
                }
                else dp[i][j] = false;
            }
            else{
                dp[i][j] = false;
            }
        }

        for(i = 1; i <= s.size(); ++i){
            for(j = 2; j <= p.size(); ++j){
                // 遍历中的 i 和 j 
                // 对应 s 和 p 中的 i-1 和 j-1
                // 对应 dp 中的 i 和 j
                if(p[j-1] != '*'){
                    if(isCharMatch(s[i-1], p[j-1])){
                        dp[i][j] = dp[i-1][j-1];
                    }
                    else dp[i][j] = false;
                }
                else{
                    if(isCharMatch(s[i-1], p[j-2])) dp[i][j] = dp[i-1][j] || dp[i][j-2];
                    else dp[i][j] = dp[i][j-2];
                }
            }
        }

        return dp[s.size()][p.size()];
    }

    bool isCharMatch(char a, char b) {
        return b == '.' ? true : a == b;
    }
};
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值