leetcode 题解(线性表-数组篇二)c++解法

这篇博客主要介绍了LeetCode中与数组相关的九道题目,包括接雨水、旋转图像、爬楼梯等,提供了C++的解法。解法涵盖了动态规划、双指针、递归等多种算法思想,旨在帮助读者理解和解决数组问题。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

参考来源:https://github.com/soulmachine/leetcode

目录

1.接雨水(困难)

2.旋转图像(中等)

3.爬楼梯(中等)

4.格雷编码(中等)

5.矩阵置零(中等)

6.加油站(中等)

7.分发糖果(困难)

8.只出现一次的数字(简单)

9.只出现一次的数字 II(中等)

一 数组

1.接雨水(困难)

题目描述:

给定 n 个非负整数表示每个宽度为 1 的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。

上面是由数组 [0,1,0,2,1,0,1,3,2,1,2,1] 表示的高度图,在这种情况下,可以接 6 个单位的雨水(蓝色部分表示雨水)。 感谢 Marcos 贡献此图。

输入输出样例:

输入: [0,1,0,2,1,0,1,3,2,1,2,1]
输出: 6

解法详解:

代码:

class Solution {
public:
    int trap(vector<int>& height) {
        int n = height.size();
        int* maxLeft = new int[n]();
        int* maxRight = new int[n]();
        int i;
        for(i = 1;i < n - 1;i++){
            maxLeft[i] = max(maxLeft[i - 1],height[i - 1]);
            maxRight[n - 1 - i] = max(maxRight[n - i],height[n - i]);
        }
        int sum = 0;
        for(i = 1;i < n - 1;i++){
            int h = min(maxLeft[i],maxRight[i]);
            if(h > height[i]){
                sum += h - height[i];
            }
        }
        return sum;
    }
};

思路:

该解法很好地利用了木桶短板效应:木桶能装的最大水取决于最短木板的高度。在这道题中,逐个分析了每个柱子处能装的最大水量,对于每一格的柱子,显然能装的最大水量取决于两侧的柱子,因此需要分别求出该点左侧的柱子与右侧的柱子的最大高度,然后根据木桶效应取他们两个最小值,如果所取值大于此处柱子的高度,则这个柱子储水量为所取值减柱子的高度。

代码:

class Solution {
public:
    int trap(const vector<int>& A) {
        const int n = A.size();
        int max = 0; 
        for (int i = 0; i < n; i++)
            if (A[i] > A[max]) max = i;
        int water = 0;
        for (int i = 0, peak = 0; i < max; i++)
            if (A[i] > peak) peak = A[i];
            else water += peak - A[i];
        for (int i = n - 1, top = 0; i > max; i--)
            if (A[i] > top) top = A[i];
            else water += top - A[i];
        return water;
    }
};

思路:

这种做法的思路是上一个做法的改进版,一开始遍历整个数组,得到最高柱子所在的位置 i ,然后分成两边,对于最高柱子左侧的所有柱子,他们右侧最高柱子就是i,因此我们遍历一遍左侧的柱子,用一个变量储存这些柱子的左侧柱子最高的高度,还是用最高高度减当前柱子高度为储水量。对于最高柱子的右侧同理。

相比于之前的思路,这个思路节约了O(n)的空间复杂度,用最高柱子把柱子分成两部分,解决了遍历过程中无法同时得到左侧以及右侧柱子最大高度的问题(前一个思路是用数组来存)。

代码:

class Solution {
public:
    int trap(const vector<int>& A) {
        const int n = A.size();
        stack<pair<int, int>> s;
        int water = 0;
        for (int i = 0; i < n; ++i) {
            int height = 0;
            while (!s.empty()) { 
                int bar = s.top().first;
                int pos = s.top().second;
                water += (min(bar, A[i]) - height) * (i - pos - 1);
                height = bar;
                if (A[i] < bar) 
                    break;
                else
                    s.pop(); 
            }
            s.push(make_pair(A[i], i));
        }
        return water;
    }
};

思路:

该思路与之前两个思路有所不同,这个解法是从从左往右遍历,用一个栈储存柱子高度信息,当每次把当前柱子放入栈之前,先考虑当前柱子是否大于等于栈顶柱子的高度。

是的话,就把栈里所有高度小于当前柱子高度的柱子弹出并计算两个柱子之间的储水量(储水宽度为两柱子相隔距离,储水高度为两柱子高度取小值减去上一个被弹出柱子的高度),直至栈顶柱子大于当前柱子为止。

最后再压入当前柱子。

仔细思考这种做法,可以发现它与之前两种做法大有不同,不在是计算每个柱子的储水量,最后求和。而是不断计算当前柱子与左侧柱子储水量,不断求和。可以发现,每次遍历到新的柱子时,此时栈里的元素总是一个严格递减序列(看思路第二段),所以如果当前柱子高度还是小于栈顶柱子,就直接压入,这是栈里依旧是一个严格递减序列,如果当前柱子高度大于等于栈顶柱子,则弹出比当前柱子高度高或者相等的这些柱子,求他们之前的储水量,由于之前说了栈里是个严格递减序列,所以用height变量来储存上一次栈顶柱子高度(这个高度肯定小于这次栈顶柱子高度),用柱子高度差,乘上宽度,就等于储水量。

2.旋转图像(中等)

题目描述:

给定一个 × n 的二维矩阵表示一个图像。

将图像顺时针旋转 90 度。

说明:

你必须在原地旋转图像,这意味着你需要直接修改输入的二维矩阵。请不要使用另一个矩阵来旋转图像。

输入输出样例:

给定 matrix = 
[
  [1,2,3],
  [4,5,6],
  [7,8,9]
],

原地旋转输入矩阵,使其变为:
[
  [7,4,1],
  [8,5,2],
  [9,6,3]
]

解法详解:

代码:

class Solution {
public:
    void rotate(vector<vector<int>>& matrix) {
        int i,j;
        int row = matrix.size();
        if(row == 0){
            return;
        }
        for(i = 0;i < row;i++){
            for(j = 0;j < row - 1 - i;j++){
                int temp = matrix[i][j];
                matrix[i][j] = matrix[row - 1 - j][row - 1 - i];
                matrix[row - 1 - j][row - 1 - i] = temp;
            }
        }
        for(i = 0;i < row / 2;i++){
            for(j = 0;j < row;j++){
                int temp = matrix[i][j];
                matrix[i][j] = matrix[row - 1 - i][j];
                matrix[row - 1 - i][j] = temp;
            }
        }
    }
};

思路:

一般人的思路是根据数学上的旋转公式暴力求解,但是算法会变得很复杂。上述思路另辟蹊径,先将矩阵沿着对角线对折,接着沿着水平中线对折。

3.爬楼梯(中等)

题目描述:

假设你正在爬楼梯。需要 n 阶你才能到达楼顶。

每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?

注意:给定 n 是一个正整数。

输入输出样例:

输入: 2
输出: 2
解释: 有两种方法可以爬到楼顶。
1.  1 阶 + 1 阶
2.  2 阶

 解法详解:

代码:

class Solution {
public:
    int climbStairs(int n) {
        if(n == 1){
            return 1;
        }
        int* kind = new int[n]();
        kind[0] = 1;
        kind[1] = 2;
        if(n <= 2){
            return kind[n - 1];
        }
        int j;
        for(j = 2;j < n;j++){
            kind[j] = kind[j - 1] + kind[j - 2];
        }
        return kind[n - 1];
    }
}

思路:

爬楼梯法类似于斐波那契数列,用迭代方法可以O(n)迭代求解出答案。

4.格雷编码(中等)

题目描述:

格雷编码是一个二进制数字系统,在该系统中,两个连续的数值仅有一个位数的差异。

给定一个代表编码总位数的非负整数 n,打印其格雷编码序列。格雷编码序列必须以 0 开头。

输入输出样例:

输入: 2
输出: [0,1,3,2]
解释:
00 - 0
01 - 1
11 - 3
10 - 2

对于给定的 n,其格雷编码序列并不唯一。
例如,[0,2,3,1] 也是一个有效的格雷编码序列。

00 - 0
10 - 2
11 - 3
01 - 1

解法详解:

代码 :

class Solution {
public:
    vector<int> grayCode(int n) {
        vector<int> result;
        result.reserve(1<<n);
        result.push_back(0);
        for (int i = 0; i < n; i++) {
            const int highest_bit = 1 << i;
            for (int j = result.size() - 1; j >= 0; j--) 
                result.push_back(highest_bit | result[j]);
        }
        return result;
    }
};

思路:

n比特的格雷码,可以递归地由n - 1位的格雷码得出,即若已知n - 1位格雷码,从n - 1位格雷码的最后一个数开始,反向遍历,把第n位设为1,其余位不变,填入序列中,便可以得到n位格雷码的序列,如下图所示(图来自参考来源):

5.矩阵置零(中等)

题目描述:

给定一个 m x n 的矩阵,如果一个元素为 0,则将其所在行和列的所有元素都设为 0。请使用原地算法

输入输出样例:

输入: 
[
  [1,1,1],
  [1,0,1],
  [1,1,1]
]
输出: 
[
  [1,0,1],
  [0,0,0],
  [1,0,1]
]

解法详解:

代码:

class Solution {
public:
    void setZeroes(vector<vector<int>>& matrix) {
        int row[matrix.size()] = {0};
        int column[matrix[0].size()] = {0};
        int i,j;
        for(i = 0;i<matrix.size();i++){
            for(j = 0;j<matrix[0].size();j++){
                if(matrix[i][j] == 0){
                    row[i] = 1;
                    column[j] = 1;
                }
            }
        }
        for(i = 0;i<matrix.size();i++){
            for(j = 0;j<matrix[0].size();j++){
                if(row[i] == 1|| column[j] == 1){
                    matrix[i][j] = 0;
                }
            }
        }
    }
};

思路:此解法的空间复杂度为O(m + n),用到了两个数组,一个用来记录需要置0的行,一个用来记录需要置0的列。最后遍历整个矩阵,如果这个元素所在的行或者列需要置零,就置为0。

代码:

class Solution {
public:
    void setZeroes(vector<vector<int> > &matrix) {
        const size_t m = matrix.size();
        const size_t n = matrix[0].size();
        bool row_has_zero = false; 
        bool col_has_zero = false; 
        for (size_t i = 0; i < n; i++)
            if (matrix[0][i] == 0) {
                row_has_zero = true;
                break;
            }
        for (size_t i = 0; i < m; i++)
            if (matrix[i][0] == 0) {
                col_has_zero = true;
                break;
            }
        for (size_t i = 1; i < m; i++)
            for (size_t j = 1; j < n; j++)
                if (matrix[i][j] == 0) {
                    matrix[0][j] = 0;
                    matrix[i][0] = 0;
                }
        for (size_t i = 1; i < m; i++)
            for (size_t j = 1; j < n; j++)
                if (matrix[i][0] == 0 || matrix[0][j] == 0)
                    matrix[i][j] = 0;
        if (row_has_zero)
            for (size_t i = 0; i < n; i++)
                matrix[0][i] = 0;
        if (col_has_zero)
            for (size_t i = 0; i < m; i++)
                matrix[i][0] = 0;
    }
};

思路:

此解法空间复杂度是O(1),巧妙地用矩阵的第一行以及第一列来储存需要置0的行以及列,节省了空间。

6.加油站(中等)

题目描述:

在一条环路上有 N 个加油站,其中第 i 个加油站有汽油 gas[i] 升。

你有一辆油箱容量无限的的汽车,从第 i 个加油站开往第 i+1 个加油站需要消耗汽油 cost[i] 升。你从其中的一个加油站出发,开始时油箱为空。

如果你可以绕环路行驶一周,则返回出发时加油站的编号,否则返回 -1。

说明: 

  • 如果题目有解,该答案即为唯一答案。
  • 输入数组均为非空数组,且长度相同。
  • 输入数组中的元素均为非负数。

输入输出样例:

输入: 
gas  = [1,2,3,4,5]
cost = [3,4,5,1,2]

输出: 3

解释:
从 3 号加油站(索引为 3 处)出发,可获得 4 升汽油。此时油箱有 = 0 + 4 = 4 升汽油
开往 4 号加油站,此时油箱有 4 - 1 + 5 = 8 升汽油
开往 0 号加油站,此时油箱有 8 - 2 + 1 = 7 升汽油
开往 1 号加油站,此时油箱有 7 - 3 + 2 = 6 升汽油
开往 2 号加油站,此时油箱有 6 - 4 + 3 = 5 升汽油
开往 3 号加油站,你需要消耗 5 升汽油,正好足够你返回到 3 号加油站。
因此,3 可为起始索引。

解法详解:

代码:

class Solution {
public:
    int canCompleteCircuit(vector<int>& gas, vector<int>& cost) {
        int start = 0;
        int total = 0;
        int rest = 0;
        int i;
        for(i = 0;i < gas.size();i++){
            total += gas[i] - cost[i];
            rest += gas[i] - cost[i];
            if(rest < 0){
                rest = 0;
                start = i + 1;
            }
        }
        if(total < 0){
            return -1;
        }else{
            return start;
        }
    }
};

思路:

时间复杂度为O(n*n)的做法是把每个站作为起始点,依次判断可不可行,而上述做法是时间复杂度为O(n)的做法,依次遍历数组,用变量rest存储从出发点start出发到从i点出发向i+1时,汽油剩余量,如果发现rest小于0,即从i点到i+1汽油不足,这时无需要把起始点变为start + 1,而是直接将起始点变为i+1。

为什么可以直接置为i+1?也就是说从start + 1到 i 这些加油站,是不能作为初始点的。因为很显然,从start点出发,到i - 1点,这段期间rest是一直大于等于0的,因为在rest大于等于0的情况下,到达i点都会出现rest小于0的情况,更何况如果直接将start + 1到  i 取任意一点 j 作为起点(相当于从j - 1出发,rest = 0,也就是j - 1的区间在start点到i - 1点之间,并且rest = 0),则当车达到 i 时rest肯定也是小于0的。

如果换个角度看这道题,可以理解为一个长度为n的数组,数组的元素为gas[i] - cost[i],求是否有从数组里一点出发,其长度为1到n的n条连续序列之和(如果大于数组末尾,则从数组开头开始算)都是大于等于0的,也就是从这个数开始,不断叠加右边的数,且每次叠加的结果都是大于等于0的。

7.分发糖果(困难)

题目描述:

老师想给孩子们分发糖果,有 N 个孩子站成了一条直线,老师会根据每个孩子的表现,预先给他们评分。

你需要按照以下要求,帮助老师给这些孩子分发糖果:

  • 每个孩子至少分配到 1 个糖果。
  • 相邻的孩子中,评分高的孩子必须获得更多的糖果。

那么这样下来,老师至少需要准备多少颗糖果呢?

输入输出样例:

输入: [1,0,2]
输出: 5
解释: 你可以分别给这三个孩子分发 2、1、2 颗糖果。

解法详解:

代码:

class Solution {
public:
    int candy(vector<int>& ratings) {
        int i;
        int size = ratings.size();
        vector<int> candys(size,1);
        int candy = 2;
        for(i = 1;i < size;i++){
            if(ratings[i] > ratings[i - 1]){
                candys[i] = candy++;
            }else{
                candy = 2;
            }
        }  
        candy = 2;
        for(i = size -2;i >= 0;i--){
            if(ratings[i] > ratings[i + 1]){
                candys[i] = max(candys[i],candy++);
            }else{
                candy = 2;
            }
        }   
        int sum = accumulate(candys.begin() , candys.end() , 0);
        return sum;
    }
};

思路:

创建一个candys数组,代表每个孩子手中分到的糖果数,初始化每个孩子分到一个糖果先从左往右遍历,如果这个孩子的分数比左边孩子多,那么这个孩子分到的糖果比左边孩子分到的糖果多一个,否则,保持不变,还是一个糖果,一趟下来,candys数组中存在一些连续序列,这些孩子分数小于等于左边孩子分数,但他们分到的糖果数都是1,这是与题目不符的。

因此我们还需要依次从右往左的遍历处理这些有问题的连续序列,这些连续序列应该是递减的(排除相等的情况),可是应该从多少开始递减呢,应该取决于右边,所以我们需要一个从右往左的遍历,如果这个孩子的分数比右边孩子大,则糖果数加1,但如果这个孩子在第一轮遍历中已经给他赋值了(即他的糖果大于1),此时就要取之前给的糖果跟现在给的糖果的最大值(这样才能同时满足左右两边)。

可以发现前面两轮遍历中,我们都没讨论相等的情况,显然由题意,如果两个孩子分数相等。是没有限制条件的,因此我们就置最小值1就对了。

代码:

class Solution {
public:
    int candy(const vector<int>& ratings) {
        vector<int> f(ratings.size());
        int sum = 0;
        for (int i = 0; i < ratings.size(); ++i)
            sum += solve(ratings, f, i);
        return sum;
    }
    int solve(const vector<int>& ratings, vector<int>& f, int i) {
        if (f[i] == 0) {
            f[i] = 1;
            if (i > 0 && ratings[i] > ratings[i - 1])
                f[i] = max(f[i], solve(ratings, f, i - 1) + 1);
            if (i < ratings.size() - 1 && ratings[i] > ratings[i + 1])
                f[i] = max(f[i], solve(ratings, f, i + 1) + 1);
        }
        return f[i];
    }
};

思路:

该思路用动态规划的解法,通过备忘录方法储存每个孩子分到的糖果数,降低了时间复杂度。思路是先搜索备忘录,如果备忘录里有数,则返回这个孩子分到的糖果,如果没有数,则判断他是不是比左右两边孩子的分数大,如果是的话,继续递归求解左右孩子的糖果数。

8.只出现一次的数字(简单)

题目描述:

给定一个非空整数数组,除了某个元素只出现一次以外,其余每个元素均出现两次。找出那个只出现了一次的元素。

说明:

你的算法应该具有线性时间复杂度。 你可以不使用额外空间来实现吗?

输入输出样例:

输入: [2,2,1]
输出: 1

解法详解:

代码:

class Solution {
public:
    int singleNumber(vector<int>& nums) {
        int i,result = 0;
        for(i = 0;i<nums.size();i++){
            result = result ^ nums[i];
        }
        return result;
    }
};

思路:

由于只能用到线性时间复杂度,这种做法就比较取巧,由于异或运算符的特性,0与1异或是1,1与1异或是0,所以把数组里所有数异或起来,相同的数异或之后变成0,剩下的单数与0异或就剩下来了。

9.只出现一次的数字 II(中等)

题目描述:

给定一个非空整数数组,除了某个元素只出现一次以外,其余每个元素均出现了三次。找出那个只出现了一次的元素。

说明:

你的算法应该具有线性时间复杂度。 你可以不使用额外空间来实现吗?

输入输出样例:

输入: [2,2,3,2]
输出: 3

解法详解:

代码:

class Solution {
public:
    int singleNumber(vector<int>& nums) {
        vector<int> count(sizeof(int)*8,0);
        int size = nums.size();
        int i,j;
        for(i = 0;i < size;i++){
            for(j = 0;j < sizeof(int)*8;j++){
                count[j] += (nums[i]>>j) & 1;
                count[j] %= 3;
            }
        }
        int sum = 0;
        for(j = 0;j < sizeof(int)*8;j++){
            sum += (count[j]<<j);
        }
        return sum;
    }
};

思路:这种做法也有些技巧性,由于int的位数是固定的,比如32位(对于64位操作系统),对于数组里每个数,我们统计他们在对应位上1出现的次数,并且模3,得到的结果就是那唯一不重复的数。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值