【LeetCode】面试经典 150 题 Day 2

189.轮转数组icon-default.png?t=O83Ahttps://leetcode.cn/problems/rotate-array/?envType=study-plan-v2&envId=top-interview-150

189.轮转数组

给定一个整数数组 nums,将数组中的元素向右轮转 k 个位置,其中 k 是非负数。

示例 1:

输入: nums = [1,2,3,4,5,6,7], k = 3
输出: [5,6,7,1,2,3,4]
解释:
向右轮转 1 步: [7,1,2,3,4,5,6]
向右轮转 2 步: [6,7,1,2,3,4,5]
向右轮转 3 步: [5,6,7,1,2,3,4]

示例 2:

输入:nums = [-1,-100,3,99], k = 2
输出:[3,99,-1,-100]
解释: 
向右轮转 1 步: [99,-1,-100,3]
向右轮转 2 步: [3,99,-1,-100]

提示:

  • 1 <= nums.length <= 105
  • -231 <= nums[i] <= 231 - 1
  • 0 <= k <= 105

进阶:

  • 尽可能想出更多的解决方案,至少有 三种 不同的方法可以解决这个问题。
  • 你可以使用空间复杂度为 O(1) 的 原地 算法解决这个问题吗?

原题解析

题目要求给定一个整数数组 nums,将数组中的元素向右轮转 k 个位置,其中 k 是非负数。例如,给定数组 [1, 2, 3, 4, 5, 6, 7] 和 k = 3,将数组的元素向右移动 3 个位置,输出结果为 [5, 6, 7, 1, 2, 3, 4]。

关键在于:

  • 数组元素右移:最后 k 个元素移到数组前面,前面的元素依次右移。

  • 考虑数组的循环性质:当 k 超过数组长度时,移动会出现循环,因此需要计算 k 对数组长度 n 取模,即 k = k % n。

代码

class Solution {
public:
    void rotate(vector<int>& nums, int k) {
        int n=nums.size();
        k=k%n;
        if(k==0)return;

        reverse(nums,0,n-1);
        reverse(nums,0,k-1);
        reverse(nums,k,n-1);
        
    }

private:
    void reverse(vector<int>&nums,int start,int end){
        while(start<end){
            swap(nums[start],nums[end]);
            start++;
            end--;
        }
    }
};

算法逻辑

  1. 问题的转化:为了有效地实现数组的右移,算法通过“反转”技巧来简化操作。我们可以将问题分解为三次反转操作:

    • 反转整个数组。

    • 反转前 k 个元素。

    • 反转剩余的元素。

  2. 反转步骤的逻辑

    • 第一步:通过反转整个数组,将需要右移到前面的元素移到前面,但它们的顺序是反的。

    • 第二步:对前 k 个元素进行反转,恢复它们的顺序。

    • 第三步:对剩余的元素进行反转,恢复它们的顺序。

    这种反转方法能够保证在不使用额外空间的情况下完成轮转。

数学原理

设数组 nums 的长度为 n,我们将问题分为三次反转操作:

  1. 整体反转:首先,将数组的所有元素反转,即对于数组 nums[0, 1, ..., n-1],其顺序变为 nums[n-1, ..., 0]。

  2. 前 k 个元素反转:将反转后的数组的前 k 个元素再次反转。反转 nums[0, k-1],恢复这部分的正确顺序。

  3. 后 ( n - k ) 个元素反转:将反转后的数组的剩余部分,即 nums[k, n-1],反转回正确顺序。

数学公式表达:

设初始数组为 A = [a_1, a_2, ..., a_n],经过反转操作后:

  • 第一次反转:A' = [a_n, a_{n-1}, ..., a_1]

  • 第二次反转:对 A' 的前 k 个元素 [a_n, ..., a_{n-k+1}]反转,得到 [a_{n-k+1}, ..., a_n]

  • 第三次反转:对剩余的后 (n - k) 个元素[a_{n-k}, ..., a_1] 反转,得到[a_1, ..., a_{n-k}]

最后的结果即为[a_{n-k+1}, ..., a_n, a_1, ..., a_{n-k}],与向右轮转 k 次的结果一致。


性能分析

  1. 时间复杂度:每次反转操作的时间复杂度为 O(n),三次反转操作的总时间复杂度为 O(n) ,其中 n 是数组的长度。

  2. 空间复杂度:算法没有使用额外的存储空间,只使用了常数级的辅助变量,因此空间复杂度为 O(1) 。

相较于使用辅助数组的方法(空间复杂度 O(n)),该算法通过原地反转操作实现了最优的空间利用。

示例解释

示例 1

输入:nums = [1, 2, 3, 4, 5, 6, 7], k = 3。

  1. 第一步:反转整个数组 [1, 2, 3, 4, 5, 6, 7],得到 [7, 6, 5, 4, 3, 2, 1]。

  2. 第二步:反转前 k = 3 个元素 [7, 6, 5],得到 [5, 6, 7, 4, 3, 2, 1]。

  3. 第三步:反转剩余的元素 [4, 3, 2, 1],得到 [5, 6, 7, 1, 2, 3, 4]。

最终结果为 [5, 6, 7, 1, 2, 3, 4]。

示例 2

输入:nums = [-1, -100, 3, 99], k = 2。

  1. 第一步:反转整个数组 [-1, -100, 3, 99],得到 [99, 3, -100, -1]。

  2. 第二步:反转前 k = 2 个元素 [99, 3],得到 [3, 99, -100, -1]。

  3. 第三步:反转剩余的元素 [-100, -1],得到 [3, 99, -1, -100]。

最终结果为 [3, 99, -1, -100]。

121.买股票最佳时机icon-default.png?t=O83Ahttps://leetcode.cn/problems/best-time-to-buy-and-sell-stock/?envType=study-plan-v2&envId=top-interview-150

121.买股票最佳时机

给定一个数组 prices ,它的第 i 个元素 prices[i] 表示一支给定股票第 i 天的价格。

你只能选择 某一天 买入这只股票,并选择在 未来的某一个不同的日子 卖出该股票。设计一个算法来计算你所能获取的最大利润。

返回你可以从这笔交易中获取的最大利润。如果你不能获取任何利润,返回 0 。

示例 1:

输入:[7,1,5,3,6,4]
输出:5
解释:在第 2 天(股票价格 = 1)的时候买入,在第 5 天(股票价格 = 6)的时候卖出,最大利润 = 6-1 = 5 。
     注意利润不能是 7-1 = 6, 因为卖出价格需要大于买入价格;同时,你不能在买入前卖出股票。

示例 2:

输入:prices = [7,6,4,3,1]
输出:0
解释:在这种情况下, 没有交易完成, 所以最大利润为 0。

提示:

  • 1 <= prices.length <= 105
  • 0 <= prices[i] <= 104

原题解析

题目要求给定一个股票价格数组 prices,其中 prices[i] 表示第 i 天的股票价格。你需要选择一天买入股票,并在之后的某一天卖出,以获得最大的利润。如果没有任何盈利的机会,则返回 0。

需要注意的是,买入的时间必须在卖出时间之前,因此不能在过去的时间卖出股票。

代码

class Solution {
public:
    int maxProfit(vector<int>& prices) {
        if(prices.empty())return 0;

        int min_price=prices[0];
        int max_profit=0;

        for(int i=1;i<prices.size();i++){
            if(prices[i]<min_price){
                min_price=prices[i];
            }

            int profit=prices[i]-min_price;

            if(profit>max_profit){
                max_profit=profit;
            }
        }

        return max_profit;
    }
};

算法逻辑

该算法通过遍历 prices 数组,实时追踪到当前为止的最低买入价格 min_price,并计算在当前价格下卖出的潜在利润。具体步骤如下:

  1. 初始化

    • min_price:记录遍历过程中的最低买入价格,初始化为 prices[0]。

    • max_profit:记录当前最大的利润,初始为 0。

  2. 遍历数组

    • 每次遍历时,将当前价格 prices[i] 与 min_price 进行比较,更新 min_price 为较小的值。

    • 计算在当前价格 prices[i] 卖出时的利润 profit = prices[i] - min_price,并将其与当前最大利润 max_profit 比较,更新 max_profit。

  3. 返回结果

    • 遍历结束后,返回最大利润 max_profit。若没有盈利机会,则 max_profit 保持为 0。

数学原理

本问题可以归结为寻找数组中两个元素的差值最大的问题,且要求卖出的那一天对应的价格要晚于买入那一天的价格。因此,可以通过维护一个最小买入价格来计算每一天卖出时的潜在利润。

设 prices[i] 为第 i 天的股票价格,min_price 为当前为止最低的价格,profit 为在第 i 天卖出的利润,则:

profit = prices[i] - minprice

在遍历过程中,逐步更新 min_price 和 max_profit,通过动态调整来找到最大利润。

性能分析

  • 时间复杂度:该算法仅需遍历一次 prices 数组,每次计算涉及常数操作,因此时间复杂度为  O(n) ,其中 n 是数组 prices 的长度。

  • 空间复杂度:只使用了常数空间来存储 min_price 和 max_profit,因此空间复杂度为  O(1) 。

这是一种贪心算法,在遍历过程中实时更新最优解,并且只需一次遍历就能得出最大利润,因此是时间和空间效率都较优的算法。

示例解释

122.买股票最佳时机 IIicon-default.png?t=O83Ahttps://leetcode.cn/problems/best-time-to-buy-and-sell-stock-ii/?envType=study-plan-v2&envId=top-interview-150

122.买股票最佳时机 II

原题解析

给你一个整数数组 prices ,其中 prices[i] 表示某支股票第 i 天的价格。

在每一天,你可以决定是否购买和/或出售股票。你在任何时候 最多 只能持有 一股 股票。你也可以先购买,然后在 同一天 出售。

返回 你能获得的 最大 利润 。

示例 1:

输入:prices = [7,1,5,3,6,4]
输出:7
解释:在第 2 天(股票价格 = 1)的时候买入,在第 3 天(股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5 - 1 = 4。
随后,在第 4 天(股票价格 = 3)的时候买入,在第 5 天(股票价格 = 6)的时候卖出, 这笔交易所能获得利润 = 6 - 3 = 3。
最大总利润为 4 + 3 = 7 。

示例 2:

输入:prices = [1,2,3,4,5]
输出:4
解释:在第 1 天(股票价格 = 1)的时候买入,在第 5 天 (股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5 - 1 = 4。
最大总利润为 4 。

示例 3:

输入:prices = [7,6,4,3,1]
输出:0
解释:在这种情况下, 交易无法获得正利润,所以不参与交易可以获得最大利润,最大利润为 0。

提示:

  • 1 <= prices.length <= 3 * 104
  • 0 <= prices[i] <= 104

给定一个整数数组 prices,其中 prices[i] 表示某支股票在第 i 天的价格。你可以在任何一天购买和出售股票,但在任意时刻你最多只能持有一股股票,且可以在同一天买入并卖出。要求计算在所有可能的交易组合中,你能获得的最大利润。

本问题的核心在于找到所有可以产生利润的价格差。由于你可以在同一天买入和卖出,因此问题可以简化为寻找所有连续两天之间的价格上升部分,并将它们累加以得到最大利润。

代码

class Solution {
public:
    int maxProfit(vector<int>& prices) {
        int totalProfit=0;
        for(int i=0;i<prices.size()-1;i++){
            if(prices[i+1]>prices[i]){
                totalProfit+=prices[i+1]-prices[i];
            }
        }

        return totalProfit;
    }
};

算法逻辑

  1. 初始化一个变量 totalProfit 来记录总利润,初始值为 0。

  2. 遍历 prices 数组的每一对相邻元素 prices[i] 和 prices[i+1]:

    • 如果 prices[i+1] > prices[i],则将这两天的差价计入利润,即 totalProfit += prices[i+1] - prices[i]。

  3. 最终返回 totalProfit,即为最大利润。

数学原理

假设股票的价格变化是离散的时间序列 prices,我们的目标是从中找到最大收益。 对于每一天的价格,如果第二天的价格比今天高,那么可以选择在今天买入、明天卖出。 利润可以表示为: \text{profit} = \sum_{i=1}^{n-1} \max(0, \text{prices}[i] - \text{prices}[i-1]) 这里  n  是天数。对每一对相邻天数,我们只选择那些有正向价格差异的部分来累加利润。

性能分析

  • 时间复杂度:O(n),其中 n 是 prices 数组的长度。只需要遍历一次数组即可完成计算。

  • 空间复杂度:O(1),只使用了常数个额外空间。

示例解释

示例 1: 输入:prices = [7, 1, 5, 3, 6, 4]

  • 第 2 天买入(价格 1),第 3 天卖出(价格 5),利润为 5 - 1 = 4;

  • 第 4 天买入(价格 3),第 5 天卖出(价格 6),利润为 6 - 3 = 3;

  • 总利润为 4 + 3 = 7。 输出:7

示例 2: 输入:prices = [1, 2, 3, 4, 5]

  • 第 1 天买入(价格 1),第 5 天卖出(价格 5),利润为 5 - 1 = 4;

  • 总利润为 4。 输出:4

示例 3: 输入:prices = [7, 6, 4, 3, 1]

  • 所有天数价格均递减,没有任何可以产生正利润的交易,因此不参与交易,最大利润为 0。 输出:0

55.跳跃游戏icon-default.png?t=O83Ahttps://leetcode.cn/problems/jump-game/?envType=study-plan-v2&envId=top-interview-150

55.跳跃游戏

给你一个非负整数数组 nums ,你最初位于数组的 第一个下标 。数组中的每个元素代表你在该位置可以跳跃的最大长度。

判断你是否能够到达最后一个下标,如果可以,返回 true ;否则,返回 false 。

示例 1:

输入:nums = [2,3,1,1,4]
输出:true
解释:可以先跳 1 步,从下标 0 到达下标 1, 然后再从下标 1 跳 3 步到达最后一个下标。

示例 2:

输入:nums = [3,2,1,0,4]
输出:false
解释:无论怎样,总会到达下标为 3 的位置。但该下标的最大跳跃长度是 0 , 所以永远不可能到达最后一个下标。

提示:

  • 1 <= nums.length <= 104
  • 0 <= nums[i] <= 105

原题解析

给定一个非负整数数组 nums,你最初位于数组的第一个下标。数组中的每个元素表示你在该位置可以跳跃的最大长度。你需要判断是否能够从数组的第一个下标跳跃到最后一个下标。

本问题可以通过贪心算法来解决。每个位置 i 能跳到的最远位置是 i + nums[i],我们维护一个变量 maxReach 表示能到达的最远位置,遍历数组,若某个位置的索引 i 超过了当前 maxReach,说明无法到达这个位置,因此直接返回 false。如果能够遍历完所有可达位置且 maxReach 覆盖了最后一个下标,则返回 true。

代码

class Solution {
public:
    bool canJump(vector<int>& nums) {
        int maxReach=nums[0];
        for(int i=1;i<nums.size();i++){
            if(i>maxReach){
                return false;
            }
            maxReach=max(maxReach,i+nums[i]);
        }
        return maxReach>=nums.size()-1;
    }
};

算法逻辑

  1. 初始化 maxReach 为 nums[0],表示起始位置能跳到的最远距离。

  2. 遍历数组,索引 i 从 1 开始:

    • 如果当前索引 i 大于 maxReach,说明当前位置无法到达,返回 false。

    • 否则更新 maxReach 为 max(maxReach, i + nums[i]),即更新到达的最远位置。

  3. 遍历结束后,检查 maxReach 是否至少能覆盖数组的最后一个下标,若可以则返回 true。

数学原理

  • 假设当前在索引 i,那么在这个位置可以跳跃的最远距离是 i + nums[i]。通过不断更新当前能够到达的最远距离 maxReach,我们可以判断是否能到达目标。

  • 如果遍历过程中,某个位置的索引超过了 maxReach,则说明我们无法到达该位置,这时可以直接判断无法到达最后一个下标。

性能分析

  • 时间复杂度:O(n),其中 n 是 nums 数组的长度。我们只需遍历一次数组即可完成判断。

  • 空间复杂度:O(1),只使用了常数个额外空间。

示例解释

示例 1: 输入:nums = [2, 3, 1, 1, 4]

- 起始位置 0 可以跳跃 2 步,最远可到达位置是 0 + 2 = 2。
- 到达位置 1 后,可以跳跃 3 步,最远可以到达 1 + 3 = 4,此时已经能够到达最后一个下标。
  输出:true

示例 2:  输入:nums = [3, 2, 1, 0, 4]

- 起始位置 0 可以跳跃 3 步,最远可到达位置是 0 + 3 = 3。
- 但在到达位置 3 后,跳跃的最大长度为 0,无法继续前进到下标 4。
  输出:false

56.跳跃游戏 IIicon-default.png?t=O83Ahttps://leetcode.cn/problems/jump-game-ii/?envType=study-plan-v2&envId=top-interview-150

56.跳跃游戏 II

给定一个长度为 n 的 0 索引整数数组 nums。初始位置为 nums[0]

每个元素 nums[i] 表示从索引 i 向前跳转的最大长度。换句话说,如果你在 nums[i] 处,你可以跳转到任意 nums[i + j] 处:

  • 0 <= j <= nums[i] 
  • i + j < n

返回到达 nums[n - 1] 的最小跳跃次数。生成的测试用例可以到达 nums[n - 1]

示例 1:

输入: nums = [2,3,1,1,4]
输出: 2
解释: 跳到最后一个位置的最小跳跃数是 2。
     从下标为 0 跳到下标为 1 的位置,跳 1 步,然后跳 3 步到达数组的最后一个位置。

示例 2:

输入: nums = [2,3,0,1,4]
输出: 2

提示:

  • 1 <= nums.length <= 104
  • 0 <= nums[i] <= 1000
  • 题目保证可以到达 nums[n-1]

原题解析

给定一个非负整数数组 nums,数组中的每个元素表示你可以从当前索引跳跃的最大长度。初始位置在数组的第一个元素 nums[0],目标是到达数组的最后一个位置 nums[n-1],你需要求出最少的跳跃次数才能到达目标。

这个问题可以通过贪心算法解决。核心思想是在遍历数组的过程中,动态维护当前能够到达的最远位置 currentReach 和下一步能够到达的最远位置 nextReach。每当你到达当前能够跳跃的最远位置时,必须进行一次跳跃,同时更新 currentReach 为 nextReach。这种策略可以保证你使用最少的跳跃次数到达目标。

代码

class Solution {
public:
    int jump(vector<int>& nums) {
        int jumps=0,currentReach=0,nextReach=0;

        for(int i=0;i<nums.size()-1;i++){
            nextReach=max(nextReach,i+nums[i]);
            if(i==currentReach){
                jumps++;
                currentReach=nextReach;
            }
        }

        return jumps;
    }
};

算法逻辑

  1. 初始化跳跃次数 jumps = 0,当前能够到达的最远位置 currentReach = 0,下一步能够到达的最远位置 nextReach = 0。

  2. 遍历数组的每一个索引 i:

    • 更新 nextReach 为 max(nextReach, i + nums[i]),表示从当前位置可以跳到的最远位置。

    • 如果当前索引 i 等于 currentReach,说明需要进行一次跳跃,更新 currentReach = nextReach 并增加跳跃次数 jumps++。

  3. 当遍历完成时,返回 jumps,即为最小跳跃次数。

数学原理

假设当前在索引 i 处,nums[i] 表示你可以跳跃的最大距离。那么你可以跳跃的下一个位置是 i + j,其中 0 <= j <= nums[i],且跳跃不能超过数组的边界。 通过不断更新下一步可以跳跃的最远位置 nextReach,并在必要时增加跳跃次数,可以保证最终到达目标所需的最少跳跃次数。

跳跃过程可以总结为:

  1. 在遍历每个元素时,更新可以到达的最远位置 nextReach。

  2. 如果遍历到达了当前跳跃的最远位置 currentReach,则增加跳跃次数并更新当前的最远位置为 nextReach。

  3. 当 currentReach 能够覆盖数组的最后一个元素时,返回跳跃次数。

性能分析

  • 时间复杂度:O(n),其中 n 是数组 nums 的长度。我们只需要遍历一次数组即可计算出最小跳跃次数。

  • 空间复杂度:O(1),只使用了常数个额外空间来存储跳跃次数和最远可达位置。

示例解释

示例 1: 输入:nums = [2, 3, 1, 1, 4]

  • 从 0 跳到 1(跳 1 步),然后从 1 跳到 4(跳 3 步),总共 2 步。 输出:2

示例 2: 输入:nums = [2, 3, 0, 1, 4]

  • 从 0 跳到 1(跳 1 步),然后从 1 跳到 4(跳 3 步),总共 2 步。 输出:2

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值