剑指offer(I) 续

206 阅读17分钟

本文中题解及图片部分参考自leetcode题解区,著作权归原作者所有。此文章仅作个人学习记录之用。

题目56(II)

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

  • 1 <= nums.length <= 10000
  • 1 <= nums[i] < 2^31

解1

class Solution {
public:
    int singleNumber(vector<int>& nums) {
        vector<int> ans(32,0);
        int i;
        for(int x : nums){
            i = 31;
            while(x){
                if(x & 1){
                    ans[i]++;
                }
                x >>= 1;
                i--;
            }
        }

        i = 0;
        int base = 1;
        for(vector<int>:: reverse_iterator a = ans.rbegin(); a != ans.rend() - 1; a++){
            *a %= 3;
            i += *a * base;
            base <<= 1;
        }
        return i;
    }
};

image.png 开一个32位的数组记录每一位上1的出现次数即可。
注意到给的nums的范围是正数,所以符号位就不用考虑了。所以base才定义为int,循环范围也是ans.rend() - 1,而非通常的ans.rend()。如果是当成无符号数处理,处理ans[0]时位权就是2312^{31},会爆int。 如果存在负数的话还应该根据ans[0]是0还是1决定对计算的结果是否取负数。

解2

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

和解1的思路相同,统计各位1的出现次数。但做法不一样,借助了有限状态自动机。

各二进制位的 位运算规则 相同 ,因此只需考虑一位即可。如下图所示,对于所有数字中的某二进制位 1 的个数,存在 3 种状态,即对 3 余数为 0, 1, 2 。

若输入二进制位 1 ,则状态按照以下顺序转换;
01200 \rightarrow 1 \rightarrow 2 \rightarrow 0 \rightarrow \cdots
若输入二进制位 0 ,则状态不变。

image.png

如下图所示,由于二进制只能表示 0, 10,1 ,因此需要使用两个二进制位来表示 33 个状态。设此两位分别为 two , one ,则状态转换变为: 0001100000 \rightarrow 01 \rightarrow 10 \rightarrow 00 \rightarrow \cdots

image.png

计算 one 方法:

设当前状态为 two one ,此时输入二进制位 n 。如下图所示,通过对状态表的情况拆分,可推出 one 的计算方法为:

if two == 0:
  if n == 0:
    one = one
  if n == 1:
    one = ~one
if two == 1:
    one = 0

引入 异或运算 ,可将以上拆分简化为:

if two == 0:
    one = one ^ n
if two == 1:
    one = 0

引入 与运算 ,可继续简化为:

one = one ^ n & ~two

image.png

计算 two 方法:

由于是先计算 one ,因此应在新 one 的基础上计算 two 。 如下图所示,修改为新 one 后,得到了新的状态图。观察发现,可以使用同样的方法计算 two ,即:

two = two ^ n & ~one

image.png

返回值:

以上是对数字的二进制中 “一位” 的分析,而 int 类型的其他 31 位具有相同的运算规则,因此可将以上公式直接套用在 32 位数上。

遍历完所有数字后,各二进制位都处于状态 00 和状态 01 (取决于 “只出现一次的数字” 的各二进制位是 1 还是 0 ),而此两状态是由 one 来记录的(此两状态下 two 恒为 0 ),因此返回 one 即可。

题目39

数组中有一个数字出现的次数超过数组长度的一半,请找出这个数字。 你可以假设数组是非空的,并且给定的数组总是存在多数元素。

解1

class Solution {
public:
    int majorityElement(vector<int>& nums) {
        unordered_map<int, int> m;
        int max = 0, ans;
        for(int i : nums){
            m[i]++;
            if(m[i] > max){
                ans = i;
                max = m[i];
            }
        }
        return ans;
    }
};

就遍历数组,用哈希表记录每个数字的出现次数,返回最大的。

解2

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

该数字出现次数超过数组一半,则排完序数组正中间的元素就是所求。

题目66

给定一个数组 A[0,1,,n1]A[0,1,…,n-1],请构建一个数组 B[0,1,,n1]B[0,1,…,n-1],其中 B[i]B[i] 的值是数组 A 中除了下标 i 以外的元素的积, 即 B[i]=A[0]×A[1]××A[i1]×A[i+1]××A[n1]B[i]=A[0]×A[1]×\cdots×A[i-1]×A[i+1]×\cdots×A[n-1]。不能使用除法。

class Solution {
public:
    vector<int> constructArr(vector<int>& a) {
        int res = 1, n = a.size();
        vector<int> ans(n, 1);
        for(int i = 0; i < n - 1; i++){ //a[0]~a[i]的乘积
            res *= a[i];
            ans[i + 1] = res;
        }
        res = 1;
        for(int i = n - 1; i > 0; i--){ //a[i]~a[n-1]的乘积
            res *= a[i];
            ans[i - 1] *= res;
        }
        return ans;
    }
};

由于不能使用除法,本来可以只遍历一次获取所有元素的积,再除以对应元素得到答案。
现在要从前往后、从后往前分别遍历一次,分别获取a[0]a[i]a[0]\sim a[i]的乘积、a[i]a[n1]a[i]\sim a[n-1]的乘积
ans[i]=(a[0]×a[1]××a[i1])(a[i+1]××a[n1])ans[i]=(a[0]×a[1]×\cdots×a[i-1]) * (a[i+1]×\cdots×a[n-1])

题目14(I)

给你一根长度为 n 的绳子,请把绳子剪成整数长度的 m 段(m、n都是整数,n>1并且m>1),每段绳子的长度记为 k[0],k[1]k[m1]k[0],k[1]\cdots k[m-1] 。请问 k[0]k[1]k[m1]k[0]*k[1]*\cdots *k[m-1] 可能的最大乘积是多少?例如,当绳子的长度是8时,我们把它剪成长度分别为2、3、3的三段,此时得到的最大乘积是18。其中 2 <= n <= 58

class Solution {
public:
    int cuttingRope(int n) {
        if(n <= 3) return n - 1;
        switch(n % 3){
            case 0: return pow(3, n / 3);
            case 1: return pow(3, n / 3 - 1) * 4;
            case 2: return pow(3, n / 3) * 2;    
        }
        return 0;
    }
};

纯数学解法:
n=x1+x2++xkn = x_1 + x_2 + \cdots + x_k ,即求 max(x1x2xk)max(x_1x_2\cdots x_k)

由均值不等式:x1x2xk(nk)kx_1\,x_2\cdots \,x_k \leq (\frac{n}{k}) ^ k,当且仅当 x1=x2==xkx_1 = x_2 = \cdots = x_k 时取等

f(x)=(nx)x(n2,x1)f(x) = (\frac{n}{x}) ^ x(n\ge 2, x \geq 1),求导得 f(x)=f(x)×(lnn1lnx)f'(x) = f(x) \times (lnn - 1 - lnx)

显然 f(x)>0f(x) > 0 ,令f(x)=0f'(x) = 0x=ne x = \frac{n}{e}

由于 kk 为正整数,考虑 k=n2k = \lfloor\frac{n}{2}\rfloork=n3k = \lfloor\frac{n}{3}\rfloor

f(n2)=2n2<3n3=f(n3)f(\frac{n}{2}) = 2 ^ \frac{n}{2} < 3 ^ \frac{n}{3} = f(\frac{n}{3}),故 k=n3k = \lfloor\frac{n}{3}\rfloor

所以应该按3米一段切分,那么对于n3\lfloor\frac{n}{3}\rfloor段切完后剩余的部分有三种情况:

  • n % 3 = 0无需操作
  • n % 3 = 2 若最后一段绳子长度为 2 ;则保留,不再拆为 1+1 。
  • n % 3 = 1若最后一段绳子长度为 1 ;则应把一份 3 + 1 替换为 2 + 2,因为 2×2>3×12 \times 2 > 3 \times 1

还要处理 n3n \leq 3 的情况: n=3n = 3 时拆分为1+21 + 2n=2n = 2 时拆分为1+11 + 1

题目57(II)

输入一个正整数 target ,输出所有和为 target 的连续正整数序列(至少含有两个数)。 序列内的数字由小到大排列,不同序列按照首个数字从小到大排列。

class Solution {
public:
    vector<vector<int>> findContinuousSequence(int target) {
        vector<vector<int>> ans;
        vector<int> res;
        int l = 1, r = 2;
        while(l < r && r <= target / 2 + 1){
            int sum = (l + r) * (r - l + 1) / 2;
            if(sum == target){
                res.clear();
                for(int i = l; i <= r; i++){
                    res.push_back(i);
                }
                ans.push_back(res);
                l++;
            }
            if(sum < target){
                r++;
            }
            if(sum > target){
                l++;
            }
        }
        return ans;
    }
};

该方法是在暴力枚举以i开头的序列上的一个优化。
从[1,2]开始,计算[l, r]的区间和,如果小于target则 r++,大于target则 l++,等于则放入ans。
比起暴力搜索好处在于若 [l, r] 已经等于target了,那么 [l+1, r]、[l+2, r] ··· 都没有必要,直接从 [l+1, r+1]开始就可以了。

题目62

0,1,···,n-1这n个数字排成一个圆圈,从数字0开始,每次从这个圆圈里删除第m个数字(删除后从下一个数字开始计数)。求出这个圆圈里剩下的最后一个数字。

例如,0、1、2、3、4这5个数字组成一个圆圈,从数字0开始每次删除第3个数字,则删除的前4个数字依次是2、0、4、1,因此最后剩下的数字是3。

解1

class Solution {
public:
    int lastRemaining(int n, int m) {
        if(n == 1) return 0;
        return (m + lastRemaining(n - 1, m)) % n;
    }
};

约瑟夫环是一个典型的递归问题,我们定义 f(n,m)f(n, m) 表示从下标 0 开始,共 n 个元素,删除第 m 个元素的结果,返回值是最后剩下的元素下标。
结束条件显然是 n=1n = 1 时,此时只有一个元素,返回下标0。

主要是递推关系怎么找。

f(n1,m)=y f(n - 1, m) = y,这表示从下标 0 开始, n1n - 1 个元素时会剩下下标 yy 的元素;那如果我初始从下标 ii 开始选择,显然最后会剩下下标为(i+y)%(n1)(i + y)\,\%\,(n - 1)的元素。

对于 f(n,m)f(n, m),从下标 0 开始,第一轮显然会删除下标为 (m1)%n(m - 1)\, \% \,n的元素,然后就变成了剩下 n1n - 1 个元素,但不是从 0 开始,而是从下标 m%nm \,\%\, n 开始,那么最后会剩下下标为(m%n+y)%n(m \, \% \,n+ y)\,\%\,n的元素。

所以递推公式为:f(n,m)=(m+f(n1,m))%nf(n, m) = (m + f(n - 1, m))\,\%\,n

解2

class Solution {
public:
    int lastRemaining(int n, int m) {
        // if(n == 1) return 0;
        // return (m + lastRemaining(n - 1, m)) % n;

        int ans = 0;
        for(int i = 2; i <= n; i++){
            ans = (m + ans) % i;
        }
        return ans;
    }
};

把递归改成循环可以节省栈空间

题目29

输入一个矩阵,按照从外向里以顺时针的顺序依次打印出每一个数字。

class Solution {
public:
    vector<int> spiralOrder(vector<vector<int>>& matrix) {
        int line = matrix.size();
        vector<int> ans;
        if(line == 0) return ans;
        int column = matrix[0].size(), i = 0, j = 0, count = 0;

        while(ans.size() < column * line){
            if(i == j && i * 2 + 1 == line){
                ans.push_back(matrix[i][j]);
                j++;
            }
            while(j != column - count - 1 && ans.size() < column * line){
                ans.push_back(matrix[i][j++]);
            }
            while(i != line - count - 1 && ans.size() < column * line){
                ans.push_back(matrix[i++][j]);
            }
            while(j > count && ans.size() < column * line){
                ans.push_back(matrix[i][j--]);
            }
            while(i > count && ans.size() < column * line){
                ans.push_back(matrix[i--][j]);
            }
            j++;
            i++;
            count++;
        }
        return ans;
    }
};

首先应该考虑空矩阵的特殊情况,在取得line之后应该先判断,不为空才能取column。

这就是纯模拟题,i,j控制输出位置,左下右上依次输出。例:

1   2  3 | 4 
——
5 | 6  7   8 
           ——
9 | 10 11  12

从[0,0]开始,由于line = 3,column = 4,那么每次横向输出3个,纵向输出2个,该例中为:
[1,2,3] [4,8] [12,11,10] [9,5] [6,7]
内层的4个while就是这个作用,count用来控制每轮输出的个数,因为内圈肯定比外圈个数少

开头的 if 作用是当进行最后一轮输出时,如果仍然按照四个while会不满足循环条件直接退出导致错误,最后一轮的元素位置是与line和column有关的,补上这个才对。

题目31

输入两个整数序列,第一个序列表示栈的压入顺序,请判断第二个序列是否为该栈的弹出顺序。假设压入栈的所有数字均不相等。例如,序列 {1,2,3,4,5} 是某栈的压栈序列,序列 {4,5,3,2,1} 是该压栈序列对应的一个弹出序列,但 {4,3,5,1,2} 就不可能是该压栈序列的弹出序列。

解1

class Solution {
public:
    bool validateStackSequences(vector<int>& pushed, vector<int>& popped) {
        int push = 0, pop = 0;
        stack<int> st;
        while(pop < popped.size()){
            while((st.empty() || st.top() != popped[pop]) && push < pushed.size()){
                st.push(pushed[push++]);
            }
            if(st.top() == popped[pop]){
                st.pop();
                pop++;
            }
            else{
                if(push == pushed.size()) return false;
            }
        }
        return true;
    } 
};

模拟整个过程,当栈空或栈顶不是待弹出元素 且 仍有可压栈元素则压栈
栈顶就是要弹出元素则弹出
若已无元素可压栈,且栈顶不是待弹出元素。返回false
整个流程能走完(栈空了)返回true

题目20

请实现一个函数用来判断字符串是否表示数值(包括整数和小数)。

数值(按顺序)可以分成以下几个部分:

若干空格
一个 小数 或者 整数
(可选)一个 'e' 或 'E' ,后面跟着一个 整数
若干空格

小数(按顺序)可以分成以下几个部分:

(可选)一个符号字符('+' 或 '-')
下述格式之一:
至少一位数字,后面跟着一个点 '.'
至少一位数字,后面跟着一个点 '.' ,后面再跟着至少一位数字
一个点 '.' ,后面跟着至少一位数字

整数(按顺序)可以分成以下几个部分:

(可选)一个符号字符('+' 或 '-')
至少一位数字

部分数值列举如下:

["+100", "5e2", "-123", "3.1416", "-1E-16", "0123"]

部分非数值列举如下:

["12e", "1a3.14", "1.2.3", "+-5", "12e+5.4"]  

class Solution {
public:
    bool isNumber(string s) {
        int n = s.length(), state = 0, i = 0;
        while(i < n){
            switch(state){
                case 0: 
                    if(s[i] == ' '){
                        i++;
                        state = 1;
                    }
                    else if(s[i] == '+' || s[i] == '-'){
                        i++;
                        state = 2;       
                    }
                    else if(s[i] >= '0' && s[i] <= '9'){
                        i++;
                        state = 3;
                    }
                    else if(s[i] == '.'){
                        i++;
                        state = 4;
                    }
                    else{
                        return false;
                    }
                    break;
                case 1:
                    if(s[i] == '+' || s[i] == '-'){
                        i++;
                        state = 2;
                    }
                    else if(s[i] == '.'){
                        i++;
                        state = 4;
                    }
                    else if(s[i] >= '0' && s[i] <= '9'){
                        i++;
                        state = 3;
                    }
                    else if(s[i] == ' '){
                        i++;
                        state = 1;
                    }
                    else{
                        return false;
                    }
                    break;
                case 2:
                    if(s[i] >= '0' && s[i] <= '9'){
                        i++;
                        state = 3;
                    }
                    else if(s[i] == '.'){
                        i++;
                        state = 4;
                    }
                    else{
                        return false;
                    }
                    break;
                case 3:
                    if(s[i] == ' '){
                        i++;
                        state = 11;
                    }
                    else if(s[i] == '.'){
                        i++;
                        state = 5;
                    }
                    else if(s[i] >= '0' && s[i] <= '9'){
                        i++;
                        state = 3;
                    }
                    else if(s[i] == 'e' || s[i] == 'E'){
                        i++;
                        state = 8;
                    }
                    else{
                        return false;
                    }
                    break;
                case 4:
                    if(s[i] >= '0' && s[i] <= '9'){
                        i++;
                        state = 7;
                    }
                    else{
                        return false;
                    }
                    break;
                case 5:
                    if(s[i] == ' '){
                        i++;
                        state = 11;
                    }
                    else if(s[i] >= '0' && s[i] <= '9'){
                        i++;
                        state = 6;
                    }
                    else if(s[i] == 'e' || s[i] == 'E'){
                        i++;
                        state = 8;
                    }
                    else{
                        return false;
                    }
                    break;
                case 6:
                    if(s[i] == ' '){
                        i++;
                        state = 11;
                    }
                    else if(s[i] >= '0' && s[i] <= '9'){
                        i++;
                        state = 6;
                    }
                    else if(s[i] == 'e' || s[i] == 'E'){
                        i++;
                        state = 8;
                    }
                    else{
                        return false;
                    }
                    break;
                case 7:
                    if(s[i] == ' '){
                        i++;
                        state = 11;
                    }
                    else if(s[i] >= '0' && s[i] <= '9'){
                        i++;
                        state = 7;
                    }
                    else if(s[i] == 'e' || s[i] == 'E'){
                        i++;
                        state = 8;
                    }
                    else{
                        return false;
                    }
                    break;
                case 8:
                    if(s[i] == '+' || s[i] == '-'){
                        i++;
                        state = 9;
                    }
                    else if(s[i] >= '0' && s[i] <= '9'){
                        i++;
                        state = 10;
                    }
                    else{
                        return false;
                    }
                    break;
                case 9:
                    if(s[i] >= '0' && s[i] <= '9'){
                        i++;
                        state = 10;
                    }
                    else{
                        return false;
                    }
                    break;
                case 10:
                    if(s[i] == ' '){
                        i++;
                        state = 11;
                    }
                    else if(s[i] >= '0' && s[i] <= '9'){
                        i++;
                        state = 10;
                    }
                    else{
                        return false;
                    }
                    break;
                case 11:
                    if(s[i] == ' '){
                        i++;
                        state = 11;
                    }
                    else{
                        return false;
                    }
                    break;
            }
        }
        if(state == 3 || state == 5 || state == 6 || state == 7 || state == 10 || state == 11)
            return true;
        else
            return false;
    }
};

就是一个纯模拟题,这种题典型的就是用有限状态自动机模拟整个判断流程。
难点并不在代码,而是自动机的状态和转移容易漏。
我就把所有结束状态接收 e/E 和空格的转移漏掉了,还有连续多个空格的转移也漏了

状态机如图:

image.png

黄色的为结束状态

题目67

写一个函数 StrToInt,实现把字符串转换成整数这个功能。不能使用 atoi 或者其他类似的库函数。

首先,该函数会根据需要丢弃无用的开头空格字符,直到寻找到第一个非空格的字符为止。

当我们寻找到的第一个非空字符为正或者负号时,则将该符号与之后面尽可能多的连续数字组合起来,作为该整数的正负号;假如第一个非空字符是数字,则直接将其与之后连续的数字字符组合起来,形成整数。

该字符串除了有效的整数部分之后也可能会存在多余的字符,这些字符可以被忽略,它们对于函数不应该造成影响。

注意:假如该字符串中的第一个非空格字符不是一个有效整数字符、字符串为空或字符串仅包含空白字符时,则你的函数不需要进行转换。

在任何情况下,若函数不能进行有效的转换时,请返回 0。

说明:

假设我们的环境只能存储 32 位大小的有符号整数,那么其数值范围为 [−231, 231 12^{31},  2^{31} − 1]。如果数值超过这个范围,请返回  INT_MAX (231 12^{31} − 1) 或 INT_MIN (−2312^{31}) 。

class Solution {
public:
    int strToInt(string str) {
        int i = 0, ans = 0;
        long long tmp;
        bool isNegative = false;
        while(str[i] == ' '){
            i++;
        }
        if(str[i] == '-' || str[i] == '+'){
            if(str[i] == '-')
                isNegative = true;
            i++;
        }
        if(str[i] < '0' || str[i] > '9')
            return 0;
        while(i < str.length() && str[i] >= '0' && str[i] <= '9'){
            tmp = (long long)ans * 10 + (str[i] - '0');
            if(isNegative) tmp = -tmp;
            if(tmp <= -2147483648){
                return -2147483648;
            }
            else if(tmp >= 2147483647){
                return 2147483647;
            }
            else{
                ans = isNegative ? -tmp : tmp;
                i++;
            }
        }
        return isNegative ? -ans : ans;
    }
};

isNegative用来记录正负

注意tmp定义为long long防止越界,在处理过程中先把结果存在tmp中,如果不越界再放进ans里,越界就直接返回了

遗漏特殊情况:多个正负号、int边界值

题目59(I)

给定一个数组 nums 和滑动窗口的大小 k,请找出所有滑动窗口里的最大值。

示例:

输入: nums = [1,3,-1,-3,5,3,6,7], 和 k = 3
输出: [3,3,5,5,6,7] 
解释: 

  滑动窗口的位置                最大值
---------------               -----
[1  3  -1] -3  5  3  6  7       3
 1 [3  -1  -3] 5  3  6  7       3
 1  3 [-1  -3  5] 3  6  7       5
 1  3  -1 [-3  5  3] 6  7       5
 1  3  -1  -3 [5  3  6] 7       6
 1  3  -1  -3  5 [3  6  7]      7

解1

class Solution {
public:
    vector<int> maxSlidingWindow(vector<int>& nums, int k) {
        int n = nums.size();
        if(n == 0) return vector<int>();
        vector<int> ans(n - k + 1);
        pair<int, int> max{-1, INT_MIN}; //<下标,最值>
        for(int i = 0; i < n - k + 1; i++){
            if(max.first == -1 || max.first < i){ //没有可用的就只能搜索
                max.second = INT_MIN; //从头搜索得重置max
                for(int j = i; j < i + k; j++){
                    if(nums[j] > max.second){
                        max.first = j;
                        max.second = nums[j];
                    }
                }
            }
            else{ //有可用的只需要和新增的最后一个元素比较即可
                if(max.second <= nums[i + k - 1]){
                    max.first = i + k - 1;
                    max.second = nums[i + k - 1];
                }
            }
            ans[i] = max.second;            
        }
        return ans;
    }
};

就是最朴素的想法。在暴力遍历的基础上优化一点点,记录每次窗口的最大值,由于相邻窗口只有最后一个元素是新增的,在上一轮的最大值在当前窗口中的情况下只需要和最后一个元素比较即可。如果不在那就只能暴力搜索。
一般来说是比暴力好的,但是最坏情况下(数组降序排列)退化为和暴力一样。

解2

class Solution {
public:
    vector<int> maxSlidingWindow(vector<int>& nums, int k) {
        int n = nums.size();
        if(n == 0) return vector<int>();
        vector<int> ans;
        deque<int> q;

        for(int i = 0; i < k; i++){
            while(!q.empty() && nums[q.back()] <= nums[i]){
                q.pop_back();
            }
            q.push_back(i); //队列里放的下标,不是值,要不没法判断队首是否越过窗口
        }
        ans.push_back(nums[q.front()]);

        for(int i = k; i < n; i++){
            while(!q.empty() && nums[q.back()] <= nums[i]){
                q.pop_back();
            }
            q.push_back(i);
            while(q.front() <= i - k){
                q.pop_front();
            }
            ans.push_back(nums[q.front()]);
        }
        return ans;
    }
};

我们需要一个队列,这个队列放进去窗口里的元素,然后随着窗口的移动,队列也一进一出,每次移动之后,队列告诉我们里面的最大值是什么。

其实队列没有必要维护窗口里的所有元素,只需要维护有可能成为窗口里最大值的元素就可以了,同时保证队里里的元素数值是由大到小的。

那么这个维护元素单调递减的队列就叫做单调队列,即单调递减或单调递增的队列。

不要以为实现单调队列就是 对窗口里面的数进行排序,如果排序的话,那和优先级队列又有什么区别了呢。

设计单调队列的时候,pop和push操作要保持如下规则:

push(value):如果push的元素value大于队尾元素的数值,那么就将队尾的元素弹出,直到push元素的数值小于等于队尾元素的数值为止
pop(value):每push一个元素就应该从队首pop一个已不在当前窗口范围内的元素
保持如上规则,每次窗口移动的时候,只要问que.front()就可以返回当前窗口的最大值。

题目59(II)

请定义一个队列并实现函数 max_value 得到队列里的最大值,要求函数max_value、push_back 和 pop_front 的均摊时间复杂度都是O(1)。

若队列为空,pop_front 和 max_value 需要返回 -1

class MaxQueue {
    queue<int> que;
    deque<int> deq;
public:
    MaxQueue() {
    }
    
    int max_value() {
        if (deq.empty())
            return -1;
        return deq.front();
    }
    
    void push_back(int value) {
        while (!deq.empty() && deq.back() < value) {
            deq.pop_back();
        }
        deq.push_back(value);
        que.push(value);
    }
    
    int pop_front() {
        if (que.empty())
            return -1;
        int ans = que.front();
        if (ans == deq.front()) {
            deq.pop_front();
        }
        que.pop();
        return ans;
    }
};

题目37

请实现两个函数,分别用来序列化和反序列化二叉树。

你需要设计一个算法来实现二叉树的序列化与反序列化。这里不限定你的序列 / 反序列化算法执行逻辑,你只需要保证一个二叉树可以被序列化为一个字符串并且将这个字符串反序列化为原始的树结构。

class Codec {
public:
    void rserialize(TreeNode* root, string& str) {
        if (root == nullptr) {
            str += "None,";
        } else {
            str += to_string(root->val) + ",";
            rserialize(root->left, str);
            rserialize(root->right, str);
        }
    }

    string serialize(TreeNode* root) {
        string ret;
        rserialize(root, ret);
        return ret;
    }

    TreeNode* rdeserialize(list<string>& dataArray) {
        if (dataArray.front() == "None") {
            dataArray.erase(dataArray.begin());
            return nullptr;
        }

        TreeNode* root = new TreeNode(stoi(dataArray.front()));
        dataArray.erase(dataArray.begin());
        root->left = rdeserialize(dataArray);
        root->right = rdeserialize(dataArray);
        return root;
    }

    TreeNode* deserialize(string data) {
        list<string> dataArray;
        string str;
        for (auto& ch : data) {
            if (ch == ',') {
                dataArray.push_back(str);
                str.clear();
            } else {
                str.push_back(ch);
            }
        }
        if (!str.empty()) {
            dataArray.push_back(str);
            str.clear();
        }
        return rdeserialize(dataArray);
    }
};

序列化好说,遍历就行了。
注意一般只给出一个遍历序列时是无法唯一确定一棵树的,我们要实现反序列化,就要在遍历序列中标记好空节点。注意节点的val可没说只有一位数或者都是正数,所以节点之间要加分隔符才能区分开。

反序列化时首先处理遍历序列,去掉分隔符依次放进列表。递归地反序列化就行,每建立一个节点就删掉其遍历记录即可保证每次传递的都是当前子树的遍历序列。

题目38

输入一个字符串,打印出该字符串中字符的所有排列。
你可以以任意顺序返回这个字符串数组,但里面不能有重复元素。

解1

class Solution {
public:
    unordered_set<string> ans;

    void DFS(string& s, int i, vector<bool>& visit, string tmp){
        if(i == s.size()){
            ans.insert(tmp);
            return;
        }
        for(int j = 0; j < visit.size(); j++){
            if(!visit[j]){
                visit[j] = true;
                DFS(s, i + 1, visit, tmp + s[j]);
                visit[j] = false; //回溯
            }
        }
    }

    vector<string> permutation(string s) {
        vector<bool> visit(s.length(), false);
        DFS(s, 0, visit, "");

        vector<string> res;
        for(string s : ans){
            res.push_back(s);
        }
        return res;
    }
};

求全排列是典型的DFS问题,递归选择每一位的可能字符,用visit数组记录用过的字符。
记得DFS和回溯一般成对出现,不要忘记回溯。
题目要求去重,解1先把结果放进unordered_set自动保证去重,再转移进vector返回结果。还有优化的空间。

解2

class Solution {
public:
    vector<string> ans;

    void DFS(string& s, int i, vector<bool>& visit, string tmp){
        if(i == s.size()){
            ans.push_back(tmp);
            return;
        }
        for(int j = 0; j < visit.size(); j++){
            if(j > 0 && s[j] == s[j - 1] && !visit[j - 1]){
                continue;
            }
            if(!visit[j]){
                visit[j] = true;
                DFS(s, i + 1, visit, tmp + s[j]);
                visit[j] = false;
            }
        }
    }

    vector<string> permutation(string s) {
        vector<bool> visit(s.length(), false);
        sort(s.begin(), s.end());
        DFS(s, 0, visit, "");

        return ans;
    }
};

在递归过程中做一次判断保证重复的就直接剪枝,结果直接放进vector即可。
思路就是把s先排序,同一个位置上只放同一个字母的第一个。
我漏了!visit[j - 1]这个条件,要求只是同一个位置上不再用同一个字母DFS,如果不判断前一个未访问,在下一个位置就也把这个字母跳过了,导致结果少了。

题目19

请实现一个函数用来匹配包含 '.' 和 '*' 的正则表达式。模式中的字符 '.' 表示任意一个字符,而 '*' 表示它前面的字符可以出现任意次(含0次)。在本题中,匹配是指字符串的所有字符匹配整个模式。例如,字符串"aaa"与模式"a.a"和"ab*ac*a"匹配,但与"aa.a"和"ab*a"均不匹配。

class Solution {
public:
    bool isMatch(string s, string p) {
        int slen = s.size(), plen = p.size();
        vector<vector<bool>> dp(slen + 1, vector<bool>(plen + 1, false));
        //dp[i][j]表示s[0]~s[i-1]p[0]~p[j-1]是否匹配,0行表示s为空,0列表示p为空

        dp[0][0] = true;
        for(int i = 0; i <= slen; i++){
            for(int j = 1; j <= plen; j++){                
                //p[j-1] == '*'
                if(p[j - 1] == '*' && ((j > 1 && dp[i][j - 2]) || (i > 0 && j > 1 && dp[i - 1][j] && (s[i - 1] == p[j - 2] || p[j - 2] == '.')))){ 
                    dp[i][j] = true;
                    continue;
                }

                //p[j-1] == '.'
                if(p[j - 1] == '.' && i > 0 && dp[i - 1][j - 1]){
                    dp[i][j] = true;
                    continue;
                }

                //p[j-1]是普通字符
                if(i > 0 && dp[i - 1][j - 1] && s[i - 1] == p[j - 1]){ 
                    dp[i][j] = true;
                }
            }
        }
        return dp[slen][plen];
    }
};

不懂为什么要用dp,先讲一下dp的做法吧

  • 定义dp数组及下标含义:dp[i][j]表示s[0]~s[i-1]p[0]~p[j-1]是否匹配,0行表示s为空,0列表示p为空
  • 寻找递推公式:s[0]~s[i-1]p[0]~p[j-1]是否匹配,根据p[j-1]可以分成三种情况:
    • 1.p[j-1] = '*'
      • 最简单的情况是s[i-1] = p[j-3],如图:即dp[i][j-2] = true,必定匹配成功
      image.png
      • 另一种情况是dp[i-1][j] = true,即s[0]~s[i-2]与p[0]~p[j-1]匹配
      image.png 此时只要p[j-2]s[i-1]相等,或p[j-2] = '.'即匹配成功
    • 2.p[j-1] = '.':因为'.'可以匹配任意字符,只要dp[i-1][j-1] = true即可
    • 3.p[j-1]是普通字符:那么前面匹配成功dp[i-1][j-1] = true且当前位s与p一样即可s[i - 1] == p[j - 1]
  • 初始化:两个都是空串匹配成功dp[0][0] = true,p空s非空必定失败(所以j从1开始遍历的),s空p非空需要走上面的判断流程
  • 遍历顺序:i 从0开始,j 从1开始
  • 注意点:每次判断都要谨防下标越界,所以代码里每一处判断都额外判断了下标;每一种条件判断成功后直接continue,不然还要走后面的流程,使运行变慢

题目49

我们把只包含质因子 2、3 和 5 的数称作丑数(Ugly Number)。求按从小到大的顺序的第 n 个丑数。
说明: 

  1. 1 是丑数。
  2. n 不超过1690。

解1

class Solution {
public:
    int nthUglyNumber(int n) {
        unordered_set<int> s; //去重
        priority_queue<int, vector<int>, greater<int>> pq; //小顶堆

        pq.push(1);
        s.insert(1);
        int ans = 1;
        while(n--){
            ans = pq.top();
            pq.pop();
            if(ans <= INT_MAX / 2 && s.find(2 * ans) == s.end()){
                pq.push(2 * ans);
                s.insert(2 * ans);
            }
            if(ans <= INT_MAX / 3 && s.find(3 * ans) == s.end()){
                pq.push(3 * ans);
                s.insert(3 * ans);
            }
            if(ans <= INT_MAX / 5 && s.find(5 * ans) == s.end()){
                pq.push(5 * ans);
                s.insert(5 * ans);
            }
        }
        return ans;
    }
};

要先明白最重要的一点:根据丑数的定义,若x为丑数,则2x,3x,5x均为丑数。且所有的丑数都可以根据这个逻辑生成,无需遍历所有整数判断。

使用一个小顶堆存放所有可能的丑数,依次弹出堆顶,弹出n次后即为答案。
由于在乘2、3、5的过程中会重复生成一些数字,所以要先去重再进堆。
乘法过程中还有可能爆int,题目给的条件 n1690n \leq1690 就是防止爆int的,所以如果乘了之后爆int就不用算了,它肯定不在答案范围内,因此在乘法之前先判断。

解2

class Solution {
public:
    int nthUglyNumber(int n) {
        vector<int> dp(n + 1);
        dp[0] = 0, dp[1] = 1;
        int p2 = 1, p3 = 1, p5 = 1;
        for(int i = 2; i <= n; i++){
            int n2 = dp[p2] * 2, n3 = dp[p3] * 3, n5 = dp[p5] * 5;
            dp[i] = min(min(n3, n2), n5);
            if(dp[i] == n2) p2++;
            if(dp[i] == n3) p3++;
            if(dp[i] == n5) p5++;
        }
        return dp[n];
    }
};

定义数组 dp,其中 dp[i] 表示从小到大的第 i 个丑数,第 n 个丑数即为 dp[n]。 由于最小的丑数是 1,因此 dp[1] = 1

例如 n = 10, primes = [2, 3, 5]。 打印出丑数列表:1, 2, 3, 4, 5, 6, 8, 9, 10, 12

首先一定要知道,后面的丑数一定由前面的丑数乘以2,或者乘以3,或者乘以5得来。例如,8, 9, 10, 12一定是1, 2, 3, 4, 5, 6乘以2, 3, 5三个质数中的某一个得到。

这样的话我们的解题思路就是:从第一个丑数开始,一个个数丑数,并确保数出来的丑数是递增的,直到数到第n个丑数,得到答案。那么问题就是如何递增地数丑数?

观察上面的例子,假如我们用1, 2, 3, 4, 5, 6去形成后面的丑数,我们可以将1, 2, 3, 4, 5, 6分别乘以2, 3, 5,这样得到一共6*3=18个新丑数,这18个数中最小的一个就是下一个丑数。也就是说1, 2, 3, 4, 5, 6中的每一个丑数都有一次机会与2相乘,一次机会与3相乘,一次机会与5相乘(一共有18次机会形成18个新丑数),来得到更大的一个丑数。

这样就可以用三个指针,

pointer2, 指向1, 2, 3, 4, 5, 6中,还没使用乘2机会的丑数的位置且该指针的前一位已经使用完了乘以2的机会。
pointer3, 指向1, 2, 3, 4, 5, 6中,还没使用乘3机会的丑数的位置且该指针的前一位已经使用完了乘以3的机会。
pointer5, 指向1, 2, 3, 4, 5, 6中,还没使用乘5机会的丑数的位置且该指针的前一位已经使用完了乘以5的机会。

下一次寻找丑数时,则对这三个位置分别尝试使用一次乘2机会,乘3机会,乘5机会,看看哪个最小,最小的那个就是下一个丑数。最后,那个得到下一个丑数的指针位置加一,因为它对应的那次乘法使用完了。

这里需要注意下去重的问题,如果某次寻找丑数,有多个指针的结果相同且被选中了,这些指针都应该加一,这样可以确保一个数字只被数一次。

题目60

把n个骰子扔在地上,所有骰子朝上一面的点数之和为s。输入n,打印出s的所有可能的值出现的概率。

你需要用一个浮点数数组返回答案,其中第 i 个元素代表这 n 个骰子所能掷出的点数集合中第 i 小的那个的概率。

解1(超时)

class Solution {
public:

    void DFS(int& n, vector<double>& ans, int i, int sum){
        if(i == n){
            ans[sum - n]++;
            return;
        } 
        for(int j = 0; j < 6; j++){
            DFS(n, ans, i + 1, sum + j + 1);
        }
    }

    vector<double> dicesProbability(int n) {
        vector<double> ans(5 * n + 1, 0);
        DFS(n, ans, 0, 0);
        for(double& i : ans){
            i /= pow(6, n);
        }
        return ans;
    }
};

暴力DFS n个骰子的所有可能,复杂度显然是O( 6n6^n ),超时

解2(DP)

class Solution {
public:
    vector<double> dicesProbability(int n) {
        vector<double> a(6, 1.0 / 6), b;
        vector<double>* pre = &a, *cur = &b;
        for(int i = 2; i <= n; i++){
            (*cur).clear();
            (*cur).resize(5 * i + 1, 0);        
            for(int j = 0; j < 5 * i - 4; j++){
                for(int k = j; k < j + 6; k++)
                    (*cur)[k] += (*pre)[j] / 6;
            }
            vector<double> *t = pre;
            pre = cur;
            cur = t;
        }
        return *pre;
    }
};

定义f(n)=f(n) = n 个骰子点数和的概率序列, f(n,x)=f(n,x) = n 个骰子点数和为 xx 的概率。

考虑f(n1)f(n-1)f(n)f(n)的关系:

当第 nn 个骰子的点数为 1 时,前 n1n - 1 个骰子的点数和应为 x1x - 1 ,方可组成点数和 xx ;同理,当此骰子为 2 时,前 n1n - 1 个骰子应为 x2x - 2 ;以此类推,直至此骰子点数为 6 。将这 6 种情况的概率相加,即可得到概率 f(n,x)f(n,x) 。递推公式如下所示:

f(n,x)=i=16f(n1,xi)×16f(n, x) = \sum\limits_{i=1}^6 f(n - 1, x - i) \times \frac{1}{6}

换言之,当我们遍历 f(n1)f(n-1) 中的每一个 f(n1,i)f(n - 1, i),它都对 f(n)f(n) 中的 f(n,i+1),f(n,i+2),,f(n,i+6)f(n, i + 1), \,f(n, i + 2), \,\cdots ,\, f(n, i + 6) 贡献了自己值的 16\frac{1}{6}

image.png

由此我们就可以根据f(n1)f(n-1) 得到 f(n)f(n) ,且 f(n)f(n) 只与 f(n1)f(n-1) 有关。

定义两个vector指针 pre 和 cur ,pre初始化为1个骰子的概率序列,由 pre 计算 cur 的概率序列(即 cur 比 pre 多了一个骰子),再交换pre 和 cur ,重复利用两个vector的空间即可继续操作。

题目17

输入数字 n,按顺序打印出从 1 到最大的 n 位十进制数。比如输入 3,则打印出 1、2、3 一直到最大的 3 位数 999。

class Solution {
public:
    void DFS(int n, string path, vector<string>& res, int cur){
        if(cur == n){
            res.push_back(path);
            return;
        }
        for(int i = 0; i < 10; i++){
            string tmp = " ";
            tmp[0] = '0' + i;
            path += tmp;
            DFS(n, path, res, cur + 1);
            path.erase(path.end() - 1);
        }
    } 

    vector<int> printNumbers(int n) {
        vector<int> ans;
        vector<string> res;
        DFS(n, "", res, 0);
        for(string i : res){
            ans.push_back(stoi(i));
        }
        ans.erase(ans.begin());
        return ans;
    }
};

本题实际上想考察的是大数问题,所以应该把结果用string生成,而不是其他任一种数据类型。
用string无非是把前导0去掉再返回
另外,c++11中stoi可以自动去掉前导0转为int

题目51

在数组中的两个数字,如果前面一个数字大于后面的数字,则这两个数字组成一个逆序对。输入一个数组,求出这个数组中的逆序对的总数。

解1(超时)

class Solution {
public:
    int merge_sort(vector<int>& nums, int l, int r){
        if(l >= r){
            return 0;
        }
        else{
            int lr = merge_sort(nums, l, (l + r) / 2), 
            rr =  merge_sort(nums, 1 + (l + r) / 2, r),
            i = l, j = (l + r) / 2 + 1, count = 0;
            vector<int> tmp;
            while(i <= (l + r) / 2 && j <= r){
                if(nums[i] <= nums[j]){
                    tmp.push_back(nums[i]);
                    i++;
                }
                else{
                    count += (l + r) / 2 - i + 1;
                    tmp.push_back(nums[j]);
                    j++;
                }
            }
            while(i <= (l + r) / 2){
                tmp.push_back(nums[i++]);
            }
            while(j <= r){
                tmp.push_back(nums[j++]);
            }

            for(i = 0, j = l; i < tmp.size() && j <= r; i++, j++){
                nums[j] = tmp[i];
            }
            return count + lr + rr;
        }        
    }

    int reversePairs(vector<int>& nums) {
        return merge_sort(nums, 0, nums.size() - 1);
    }
};

就是简单的归并,在合并两个有序子序列的部分超时了,如果提前int mid = (l + r) / 2,在循环中使用mid代替(l + r) / 2就不超时了。这种小问题在本题的大数据量面前就会造成大麻烦。

解2(官方解)

class Solution {
public:
    int mergeSort(vector<int>& nums, vector<int>& tmp, int l, int r) {
        if (l >= r) {
            return 0;
        }

        int mid = (l + r) / 2;
        int inv_count = mergeSort(nums, tmp, l, mid) + mergeSort(nums, tmp, mid + 1, r);
        int i = l, j = mid + 1, pos = l;
        while (i <= mid && j <= r) {
            if (nums[i] <= nums[j]) {
                tmp[pos] = nums[i];
                ++i;
                inv_count += (j - (mid + 1));
            }
            else {
                tmp[pos] = nums[j];
                ++j;
            }
            ++pos;
        }
        for (int k = i; k <= mid; ++k) {
            tmp[pos++] = nums[k];
            inv_count += (j - (mid + 1));
        }
        for (int k = j; k <= r; ++k) {
            tmp[pos++] = nums[k];
        }
        copy(tmp.begin() + l, tmp.begin() + r + 1, nums.begin() + l);
        return inv_count;
    }

    int reversePairs(vector<int>& nums) {
        int n = nums.size();
        vector<int> tmp(n);
        return mergeSort(nums, tmp, 0, n - 1);
    }
};

我看不出来和我的代码比哪里有明显的优化复杂度,但是用时比我的快得多。不理解

题目14(II)

给你一根长度为 n 的绳子,请把绳子剪成整数长度的 m 段(m、n都是整数,n>1并且m>1),每段绳子的长度记为 k[0],k[1]k[m1]k[0],k[1]\cdots k[m - 1] 。请问 k[0]×k[1]××k[m1]k[0]\times k[1]\times \cdots \times k[m - 1] 可能的最大乘积是多少?例如,当绳子的长度是8时,我们把它剪成长度分别为2、3、3的三段,此时得到的最大乘积是18。

答案需要取模 1e9+7(1000000007),如计算初始结果为:1000000008,请返回 1。

2 <= n <= 1000

class Solution {
public:

    long long my_pow(long long base, int exponent){ //快速幂求余
        long long res = 1;
        while(exponent){
            if(exponent & 1){
                res = res * base % 1000000007;
            }
            base = base * base % 1000000007;
            exponent >>= 1;
        }
        return res % 1000000007;
    }

    int cuttingRope(int n) {
        if(n <= 3){
            return n - 1;
        }
        switch(n % 3){
            case 0: return my_pow(3, n / 3);
            case 1: return my_pow(3, n / 3 - 1) * 4 % 1000000007;
            case 2: return my_pow(3, n / 3) * 2 % 1000000007;
        }
        return 0;
    }
};

本题与(I)的区别在于数据范围,本题的数据很大,所以涉及到对大数取余:仍然使用快速幂法

  • 快速幂模板一定要熟练:奇数次幂多乘一个base,循环base平方、指数 ÷\div 2的操作 根据(a*b)%p = (a%p * b%p) % p,加上取余之后,只需在每次平方时和最终结果里也取余即可

注意数据类型,虽然有取余,但也有可能乘积先爆了int再取余回到int,所以base和res都是long long

题目43

输入一个整数 n ,求1~n这n个整数的十进制表示中1出现的次数。

例如,输入12,1~12这些整数中包含1 的数字有1、10、11和12,1一共出现了5次。

class Solution {
public:
    int countDigitOne(int n) {
        int ans = 0, tmp;
        long long k = 1; //公式中的10的n次方部分
        while(n >= k){
            tmp = n % (k * 10); //记录不在完整循环中的部分
            ans += n / (k * 10) * k;
            if(tmp >= k  && tmp < k * 2){
                ans += tmp - k + 1;
            }
            else if(tmp >= k * 2){
                ans += k;
            }
            k *= 10;
        }
        return ans;
    }
};

根据题目要求,我们需要统计 [1, n] 范围内所有整数中,数字 1 出现的个数。由于 n 的范围最大为 2312^{31} - 1,它是一个 10 位整数,因此我们可以考虑枚举每一个数位,分别统计该数位上数字 1 出现的次数,最后将所有数位统计出的次数进行累加即可得到答案。

为了直观地叙述我们的算法,下面我们以「百位」进行举例,对于几个不同的 n 手动计算出答案,随后扩展到任意数位。

以 n = 1234567 为例,我们需要统计「百位」上数字 1 出现的次数。我们知道,对于从 0 开始每 1000 个数,「百位」上的数字 1 都会出现 100 次,即数的最后三位每 1000 个数都呈现 [000, 999] 的循环,其中的 [100, 199] 在「百位」上的数字为 1,共有 100 个。

n 拥有 1234 个这样的循环(即前四位0000 ~ 1233),每个循环「百位」上都出现了 100 次 1,这样就一共出现了 1234 × 100 次 1。如果使用公式表示,那么这部分出现的 11 的次数为: n1000×100\lfloor \frac{n}{1000} \rfloor \times 100

那么 n1000\lfloor \dfrac{n}{1000} \rfloor 就表示 n 拥有的完整的 [000, 999] 循环的数量。

对于剩余不在完整的循环中的部分(前四位1234),最后三位为 [000, 567],其中 567 可以用 nmod1000n \bmod 1000 表示,其中 mod 表示取模运算。记 n=nmod1000n' = n \bmod 1000,这一部分在「百位」上出现 1 的次数可以通过分类讨论得出:

n<100n' < 100 时,「百位」上不会出现 1;

100n<200100 \leq n' < 200 时,「百位」上出现 11 的范围为 [100, n'],所以出现了 n100+1n' - 100 + 1 次 1;

n200n' \geq 200 时,「百位」上出现了全部 100 次 1。

  • 注意代码里k必须取long long, k * 10也有可能爆int

题目44

数字以0123456789101112131415…的格式序列化到一个字符序列中。在这个序列中,第5位(从下标0开始计数)是5,第13位是1,第19位是4,等等。

请写一个函数,求任意第n位对应的数字。

class Solution {
public:
    int findNthDigit(int n) {
        if(n < 10) return n;
        vector<int> table;
        int base = 10;
        table.push_back(0);
        table.push_back(10);
        for(int i = 2; i < 9; i++){ 
            table.push_back(table[i - 1] + i * 9 * base);
            base *= 10;
        }

        int i = 0;
        for(i = 0; i < 9; i++){
            if(n < table[i] - 1){
                break;
            }
        }
        cout << i << endl;
        base = pow(10, i - 1);
        base += (n - table[i - 1]) / i;
        cout << base << endl;
        int count = (n - table[i - 1]) % i + 1;
        while(i - count){
            base /= 10;
            count++;
        }
        return base % 10;
    }
};

table[i]记录 i 位数在序列中占据的下标范围,是右开区间,即table[i]已经是 i+1 位数的范围了。
但是一位数下标却是闭区间[0,10],所以只能另外处理10以内的数字。

table完成后,遍历table找到n < table[i] - 1的一项,此项下标 i 就是要取的答案所在的数字的位数。然后从最小的 i 位数开始,每继续取i位,对应的数字就应该加1。如此就能准确找到要取的序列中的第 n 位对应的实际数字是多少。对 i 取余加一就是取该数字的第几位,取出来就是答案。