面试经典150题刷题记录

数组部分

1. 合并两个有序的子数组 ------ 倒序双指针避免覆盖

88. 合并两个有序数组

给你两个按 非递减顺序 排列的整数数组 nums1nums2,另有两个整数 mn ,分别表示 nums1nums2 中的元素数目。

请你 合并 nums2nums1 中,使合并后的数组同样按 非递减顺序 排列。

注意: 最终,合并后数组不应由函数返回,而是存储在数组 nums1 中。为了应对这种情况,nums1 的初始长度为 m + n,其中前 m 个元素表示应合并的元素,后 n 个元素为 0 ,应忽略。nums2 的长度为 n

参考题解:. - 力扣(LeetCode)

java 复制代码
class Solution {
    
    public void merge(int[] nums1, int m, int[] nums2, int n) {
        /* 
            倒序双指针法:从尾部开始,从而避免覆盖问题 
            m,n 分别表示 nums1 和 nums2 的元素个数
        */  
        int p1 = m - 1; // 指向第一个数组的最后一个元素
        int p2 = n - 1; // 指向第二个元素数组的最后一个元素
        int insert_position = m + n - 1; // 指向要插入的位置
        while (p2 >= 0) { // nums2 还有要合并的元素
            // 如果 p1 < 0,那么走 else 分支,把 nums2 合并到 nums1 中
            if (p1 >= 0 && nums1[p1] > nums2[p2]) {
                nums1[insert_position--] = nums1[p1--]; // 填入 nums1[p1]
            } else {
                nums1[insert_position--] = nums2[p2--]; // 填入 nums2[p1]
            }
        }
    }
}

2. 移除元素 ------ 数组末尾元素交换法

27. 移除元素

给你一个数组 nums和一个值 val,你需要 原地 移除所有数值等于 val的元素。元素的顺序可能发生改变。然后返回 nums 中与 val 不同的元素的数量。

假设 nums 中不等于 val 的元素数量为 k,要通过此题,您需要执行以下操作:

  • 更改 nums 数组,使 nums 的前 k 个元素包含不等于 val 的元素。nums 的其余元素和 nums 的大小并不重要。
  • 返回 k
java 复制代码
class Solution {
    public int removeElement(int[] nums, int val) {
        /**
            idea & steps:
                1. 从左往右遍历数组nums,,
                2. 如果与val相同,则与数组最后一个元素交换,
                3. 数组长度 -- 
                4. 注:如果相同了则继续遍历当前元素;(因为当前元素是末尾交换过来的元素,不一定不等于val)
                5. 结束的条件:当前判断的位置与更改后的数组位置相同
         */
        int cur = 0;  // 起始指针
        int rear = nums.length - 1; // 终止指针
        while(cur <= rear) {
            if(nums[cur] == val) {
                nums[cur] = nums[rear];
                rear --;
            }else {
                cur ++;
            }
        }
        return cur;
    }
}

3. 删除有序数组中的重复项

26. 删除有序数组中的重复项

给你一个 非严格递增排列 的数组 nums ,请你**原地** 删除重复出现的元素,使每个元素 只出现一次 ,返回删除后数组的新长度。元素的 相对顺序 应该保持 一致 。然后返回 nums 中唯一元素的个数。

考虑 nums 的唯一元素的数量为 k ,你需要做以下事情确保你的题解可以被通过:

  • 更改数组 nums ,使 nums 的前 k 个元素包含唯一元素,并按照它们最初在 nums 中出现的顺序排列。nums 的其余元素与 nums 的大小不重要。
  • 返回 k
java 复制代码
class Solution {
    public int removeDuplicates(int[] nums) {
        /**
            idea & steps
                1. 利用递增的特性,那么相同元素一定是在挨在一起的
                2. 原地删除
                如果后面的元素与前面的元素相同,则为重复项,不保留
                如果后面的元素与前面的元素不相同,则为重复项,不保留
                用指针 k 指向保留的元素要填入的下标
                // 最后的前k个元素包含唯一元素
         */ 
        int k = 1; // 记录存放元素的位置
        for(int i=1; i < nums.length; i++) {
            if(nums[i] != nums[i-1]) {
                nums[k++] = nums[i];
            }
        }
        return k;
    }
}

4. 删除有序数组中的重复项 Ⅱ - 比较前k位

80. 删除有序数组中的重复项 II

给你一个有序数组 nums ,请你**原地** 删除重复出现的元素,使得出现次数超过两次的元素只出现两次 ,返回删除后数组的新长度。

不要使用额外的数组空间,你必须在 原地修改输入数组并在使用 O(1) 额外空间的条件下完成。

java 复制代码
class Solution {
    public int removeDuplicates(int[] nums) {
        /**
            目标:使得出现超过两次的元素只出现两次,返回删除后数组的新长度
            注:出现1次的元素,仍然进行保留

            ===> 推广到一般情况:将保留2位 修改为 保留k位
            idea & steps: 
                1. 前k个元素直接保存
                2. 后面的元素进行遍历,将其与前k个元素比较,因为是有序的,故只需要与 nums[u-k]进行比较即可
                    只有不同才会进行保留;

            // 保留k位只用把代码中的2修改为k即可,或者封装为一个函数,传入参数k
        */
        int insert_pos = 0; // 记录当前插入的位置
        for(int num : nums) {
            // 注意这里是 insert_pos - 2 而不是 i-2; insert_pos表示当前元素要插入的位置
            // 即可以理解为要插入的元素
            // 如果要插入的元素 与 该位置前面两个元素都相同,则不能插入,继续遍历下一个元素
            if(insert_pos < 2 || nums[insert_pos - 2] != num){
                nums[insert_pos ++] = num;
            } // 前两个元素直接保留
            // printArray(nums);
        }
        return insert_pos;
    }
    // public void printArray(int[] nums) {
    //     for(int num : nums) 
    //         System.out.print(num + " ");
    //     System.out.println();
    // }
}

5. 多数元素-摩尔投票法【正负抵消】

参考题解:. - 力扣(LeetCode)

169. 多数元素

给定一个大小为 n的数组 nums ,返回其中的多数元素。多数元素是指在数组中出现次数 大于 ⌊ n/2 ⌋ 的元素。

你可以假设数组是非空的,并且给定的数组总是存在多数元素。

java 复制代码
class Solution {
    public int majorityElement(int[] nums) {
        /**
            返回大于 n/2 的元素
            比如 n = 8 , 则返回大于等于5个元素的
            比如 n = 7, 则返回大于等于4个元素的
            尝试设计实践复杂度为 O(n);  不可以进行排序了,因为快排的时间复杂度为 O(nlogn)
            遍历一遍进行解决 
        */

        // 摩尔投票法:核心理念为票数正负抵消。此方法时间与空间复杂度分别为 O(N) 和 O(1)
        // 众数 与 非众数 进行一对一抵消,剩下的就是众数了

        int x = 0; // 众数
        int votes = 0; // 初始投票为0,vote表示众数的票;每次为vote为0时,则重新选择一个众数,因为之前的众数已经被抵消完了
        for(var num : nums) {
            // if(votes == 0) {
            //     x = num; // 当前num为新的众数
            //     votes ++;
            // }else{
            //     if(num == x) {
            //         votes ++;
            //     }else {
            //         votes --;
            //     }
            // }
            if(votes == 0)  x = num;
            votes += num == x ? 1 : -1;
        }

        // 验证环节
        // 验证 x 是否为众数
        // for (int num : nums)
        //     if (num == x) count++;
        // return count > nums.length / 2 ? x : 0; // 当无众数时返回 0
        return x;
    }
}

6. 轮转数组 ------ 反转三次

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]

7. 买卖股票的最佳规划 ------ 贪心/动态规划

121. 买卖股票的最佳时机

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

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

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

dp[i][0] 表示 第i天持有该股票的最大现金

dp[i][1] 表示第i天不持有该股票的最大现金

java 复制代码
class Solution {
public:
    int maxProfit(vector<int>& prices) {
        /*
            // 即求当前元素与后面元素的最大差值
            法一: 暴力查找 O(n^2)
            法二:贪心思想:取左边的最小值,取右边最大值 O(n) O(1)
            法二:动态规划:假设 dp[i] 表示第i天可获得的最大利润
            如果第i天卖出了股票 dp[i]
            如果第i天不卖出股票又怎么样
        */
        return method4(prices);
    }

private: 
    int method2(vector<int>& prices) {
                // 法二: 贪心思想
        //    比如 7 1 5 3 6 4
        //    最小值为1,取到之后拿右边的值减它,记录减结果之后的最大利润
        // 7 100 1 20 
        // 先取的是7 然后记录了最值 93,到1之后在记录最大值也只有19,相当于每个元素都取了最小的情况
        int low = INT_MAX; // 记录左边的最小镇
        int ans = 0; // 记录最后结果
        int n = prices.size();
        for(int i=0; i < n; i++) { // 遍历数组
            low = min(prices[i],low); // 左边的最小
            ans = max(ans, prices[i] - low); // 取最大区间的利润
        }
        return ans;
    }

    int method3(vector<int>& prices) {
        /* 法三:动态规划 
        1. dp数组的含义
            dp[i][0] 表示第i天持有股票所得最多现金;
            dp[i][1] 表示第i天不持有股票所得最多现金;
                注意持有的意思并不是说在这一天买入,也可以在之前买入,只不过一直没有卖出
        2. dp公式
            第i天持有股票所得最多现金,应该等于第i-1天持有股票的最大利润 或者 是今天买入时的现金最大值
            dp[i][0] = max(dp[i-1][0] , -prices[i]);
            第i天未持有股票所得最多现金,应该等于第i-1天未持有股票所得最多现金 或者 是今天卖出时的现金最大值
            而今天卖出现金就应该是第i-1天持有的最大值 + prices[i] (今天卖出说明前一天是持有的)
            dp[i][1] = max(dp[i-1][1], dp[i-1][0] + prices[i])
        3. 初始化
            dp[0][0] : 此时持有股票就是一定买入了; dp[0][0] = -prices[0]
            dp[0][1] : 第一天不持有就是不买 dp[0][1] = 0;
        4. 遍历顺序
            从左到右,从前到后
        */
        int n = prices.size();
        // 第一个参数为外层vector的大小,第二个参数它创建了一个大小为 2 的一维整数向量
        // ,这个一维向量会作为外层向量的每个元素的初始值。
        vector<vector<int>> dp(n, vector<int>(2));
        dp[0][0] = -prices[0];
        dp[0][1] = 0;
        for(int i=1; i< n ; i++) {
            dp[i][0] = max(dp[i-1][0] , -prices[i]);
            dp[i][1] = max(dp[i-1][1], dp[i-1][0] + prices[i]);
        }
        return dp[n-1][1]; // 一直要考虑到最后一天,故最大值为到最后一天的不持有股票的结果
    }

    // 法3改进:节省空间
    int method4(vector<int>& prices) {
        int n = prices.size();
        vector<vector<int>> dp(2, vector<int>(2));
        dp[0][0] = -prices[0];
        dp[0][1] = 0;
        for(int i=1; i< n ; i++) {
            dp[i%2][0] = max(dp[(i-1)%2][0] , -prices[i]);
            dp[i%2][1] = max(dp[(i-1)%2][1], dp[(i-1)%2][0] + prices[i]);
        }
        return dp[(n - 1) % 2][1]; // 一直要考虑到最后一天,故最大值为到最后一天的不持有股票的结果
    }
};

8. 买卖股票的最佳时机 Ⅱ

122. 买卖股票的最佳时机 II

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

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

返回 你能获得的 最大 利润

分析:

与第7题唯一不同的就是在dp[i][0]的处理上,因为第7题中只能买一次股票,所以持有股票时的现金一定是 -prices[i]; 但是在本题中可以买卖多次股票,即在买当前股票时可能之前就有了股票的消费,故如果在第i天买入,那dp[i][0] 就应该等于 dp[i-1][1] - prices[i] ; (前一天不持有股票的现金减去今天买股票的钱)

java 复制代码
class Solution {
    public int maxProfit(int[] prices) {
        /**
            动态规划思想:
                1. dp含义:
                    dp[i][0] 表示第i天持有该股票的现金
                    dp[i][1] 表示第i天不持有股票的现金
                2. 状态方程
                    dp[i][0] 有两种情况:case1: 第i-1天就持有 dp[i-1][0]
                             case2: 在第i天买入 dp[i-1][1] - prices[i]
                    dp[i][1] 两种情况:case1 : 第i天没有卖出,故为前面不持有 dp[i-1][1]
                                    case2: 第i天卖出,故为前一天持有的钱 + prices[i] 即 dp[i-1][0]+prices[i]
                3. 初始值
                    dp[0][0] = -prices[0];
                    dp[0][1] = 0;
                4. 遍历顺序:从前到后
         */
        int len = prices.length;
        int[][] dp = new int[len][2];
        dp[0][0] = -prices[0];
        dp[0][1] = 0;
        for(int i=1; i < len; i++) {
            dp[i][0] = Math.max(dp[i-1][0], dp[i-1][1] - prices[i]);
            dp[i][1] = Math.max(dp[i-1][1], dp[i-1][0] + prices[i]);
        }
        return dp[len-1][1];
    }
}

9. 跳跃游戏 ------ 贪心(注意代码的写法)

55. 跳跃游戏

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

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

示例 1:

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

其实跳几步无所谓,关键在于可跳的覆盖范围!

不一定非要明确一次究竟跳几步,每次取最大的跳跃步数,这个就是可以跳跃的覆盖范围。

这个范围内,别管是怎么跳的,反正一定可以跳过来。

那么这个问题就转化为跳跃覆盖范围究竟可不可以覆盖到终点!

每次移动取最大跳跃步数(得到最大的覆盖范围),每移动一个单位,就更新最大覆盖范围。

贪心算法局部最优解:每次取最大跳跃步数(取最大覆盖范围),整体最优解:最后得到整体最大覆盖范围,看是否能到终点

java 复制代码
class Solution {
    public boolean canJump(int[] nums) {
        int cover = 0;
        // 只在可跳的范围内进行变化// 初始时下标为0
        for(int i=0; i <= cover; i++) {
            cover = Math.max(i + nums[i], cover); // 更新覆盖范围
            if(cover >= nums.length - 1) return true; // 可以跳出去
        }
        return false;
    }
}

10. 跳跃游戏Ⅱ------(明确什么时候要跳下一步,以及怎样跳步数最少)

45. 跳跃游戏 II

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

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

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

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

java 复制代码
class Solution {
    public int jump(int[] nums) {
        /**
            本题要计算最少步数,那么就要想清楚什么时候步数才一定要加一呢?,以及怎么走呢
                只有在当前的最大覆盖范围不能覆盖到终点时需要步数加1
                而下一步的覆盖范围要是最大的,才可以得到最少的跳跃次数;
                不用关注具体的跳跃方法,因为求的只是一个次数问题

            所以真正解题的时候,要从覆盖范围出发,不管怎么跳,覆盖范围内一定是可以跳到的,以最小的步数增加覆盖范围,覆盖范围一旦覆盖了终点,得到的就是最少步数!
        这里需要统计两个覆盖范围,当前这一步的最大覆盖和下一步最大覆盖。
        */

        // 比如:2 3 1 1 4 开始可至2的位置,然后一直走2之后,找到这个范围内最大下标可至结果,故而返回2次
        int curDistance = 0; // 记录当前最远下标 // 并不是每次都要更新这个,只要在达不到时才更新
        int nextDistance = 0;  // 记录下一步覆盖最远距离下标
        int ans = 0; 
        int len = nums.length;
        if(len == 1) return 0;
        for(int i=0; i < len; i++) {
            nextDistance = Math.max(nextDistance, nums[i] + i);  // 更新下一步覆盖最远距离下标
            if(i == curDistance) { // 这样才算正真找到了下一次的最远下标,故更新
                ans ++;  // 走下一步
                curDistance = nextDistance; // 继续往下走
                if(curDistance >= len - 1) return ans;
            }
        }
        return ans;
    }
}

11. H指数

12. O(1) 时间插入、删除和获取随机元素 ------ 数据结构的运用;list如何同时实现O(1)复杂度的删除与访问(尾元素替代法)

380. O(1) 时间插入、删除和获取随机元素

实现RandomizedSet 类:

  • RandomizedSet() 初始化 RandomizedSet 对象
  • bool insert(int val) 当元素 val 不存在时,向集合中插入该项,并返回 true ;否则,返回 false
  • bool remove(int val) 当元素 val 存在时,从集合中移除该项,并返回 true ;否则,返回 false
  • int getRandom() 随机返回现有集合中的一项(测试用例保证调用此方法时集合中至少存在一个元素)。每个元素应该有 相同的概率 被返回。

你必须实现类的所有函数,并满足每个函数的 平均 时间复杂度为 O(1)

java 复制代码
// O(1)的时间复杂度,所以选择使用哈希表来实现操作
// 而随机数的生成则可以随机生成一个下标,可以使用list同时来存储每个元素,这样在生成随机数时可以利用list的随机存取功能
// Map(val, index) 第一个元素为具体值,第二个元素为其对应下标(在list中)
class RandomizedSet {
    Random random;
    HashMap<Integer, Integer> map;
    List<Integer> list;
    int size;


    public RandomizedSet() { 
        random = new Random();
        map = new HashMap<>();
        list = new ArrayList<>();
        size = 0;
    }
    
    public boolean insert(int val) { 
        if(map.containsKey(val)){ // 哈希表中含有这个键
            return false;
        }
        map.put(val, size++);  // value为其下标
        list.add(val);
        return true;
    }
    
    // 在list中删除指定元素索引的操作,为了实现O(1)复杂度,所以选择用最后一个元素来代替要删除的元素
    // 然后改为删除最后一个元素即可;类似堆的删除
    public boolean remove(int val) {
        if(!map.containsKey(val)){
            return false;
        }
        int index = map.get(val); // 得到这个值的索引
        int last = list.get(size - 1); //  

        list.set(index, last); // 用最后一个元素来填充list中删除的元素,从而达到一个O(1)的删除效果 
        map.put(last, index); // 更新map

        map.remove(val);      
        list.remove(--size);
        return true;
    }
    
    public int getRandom() { // 利用list进行随机数的生成
        int index = random.nextInt(list.size());
        return list.get(index);
    }
}

13. 除自身以外数组的乘积 ------ 前后缀分解法

前后缀分解法题单:. - 力扣(LeetCode)

238. 除自身以外数组的乘积

给你一个整数数组 nums,返回 数组 answer ,其中 answer[i] 等于 nums 中除 nums[i] 之外其余各元素的乘积 。

题目数据 保证 数组 nums之中任意元素的全部前缀元素和后缀的乘积都在 32 位 整数范围内。

不要使用除法, 且在 O(n) 时间复杂度内完成此题。

本题的难点在于不可以使用除法: 且需要在O(1) 的额外空间复杂度内完成这个题目

进阶: 你可以在 O(1) 的额外空间复杂度内完成这个题目吗?( 出于对空间复杂度分析的目的,输出数组 不被视为额外空间。)

参考题解:. - 力扣(LeetCode)

. - 力扣(LeetCode)

java 复制代码
class Solution { 
    public int[] productExceptSelf(int[] nums) {
        // 方式一:未优化,O(n)  O(n)
        // return methods1(nums);

        // 方式二:O(n) O(1)
        return methodsOptimize(nums);
    }

    private int[] methods1(int[] nums) {
         /**
            1. 如果使用除法就是把所有元素乘起来,然后除以每个位置的元素即可。
            
            前后缀的思想:
                把当前nums[i]的左右两个部分分为前缀pre[i] 与 后缀suf[i]
                最后的结果即 pre[i] * suf[i] 
            如果算出了 nums[0] ~ nums[i-2] 的乘积 pre[i-1] 再乘上 nums[i-1] 即可得到 pre[i]
                即 pre[i] = pre[i-1] * nums[i-1];
            同理有 suf[i] = suf[i+1] * nums[i+1]
            初始值 pre[0] = suf[n - 1] = 1;
                算出 answer = pre[i] * suf[i]
         */
         // 1. 求出每个元素的前缀乘积 (类似前缀和的求和,从前往后相乘)
        int n = nums.length;
        int[] pre = new int[n];
        pre[0] = 1; // 初始化; 
        for(int i = 1; i < n; i++) {
            pre[i] = pre[i-1] * nums[i-1]; // eg: pre[1] = pre[0] * nums[0]; // pre[0] 是一个初值1,表示未开始乘
        }
        // 2. 求出每个元素的后缀;
        int[] suf = new int[n];
        suf[n - 1] = 1; 
        for(int i = n-2; i >=0 ;i--) {
            suf[i] = suf[i+1] * nums[i+1];
        }

        // 3. 将前缀乘积 与 后缀乘积 相乘从而得到每个元素的结果
        int[] ans = new int[n];
        for(int i=0 ; i<n; i++) {
            ans[i] = pre[i] * suf[i];
        }
        return ans;
    }


    private int[] methodsOptimize(int[] nums) {
        /**
            优化:先计算suf, 然后一边计算 pre, 一边把 pre直接乘到 suf[i] 中
            最后得到的 suf后缀乘积 即为 ans
        */
        int n = nums.length;
        int[] suf = new int[n];
        suf[n - 1] = 1; 
        for(int i=n-2; i >= 0; i--) {
            suf[i] = suf[i+1] * nums[i+1]; // 计算后缀
        }
        int pre = 1;
        for(int i = 0; i < n; i++) { // 计算前缀,但不存储,而是直接与suf进行运算
            suf[i] *= pre;
            pre = pre * nums[i]; // 更新pre 用pre代表每一层的前缀乘积
        }
        return suf;
    }
}

注:本文中的许多思路是参考leetcode题解以及代码随想录的,在这里只是记录自己的刷题过程以及用自己的方式表达出思路。

相关推荐
大数据编程之光1 小时前
Flink Standalone集群模式安装部署全攻略
java·大数据·开发语言·面试·flink
ifanatic3 小时前
[面试]-golang基础面试题总结
面试·职场和发展·golang
程序猿进阶4 小时前
堆外内存泄露排查经历
java·jvm·后端·面试·性能优化·oom·内存泄露
长风清留扬5 小时前
一篇文章了解何为 “大数据治理“ 理论与实践
大数据·数据库·面试·数据治理
jiao_mrswang5 小时前
leetcode-18-四数之和
算法·leetcode·职场和发展
Swift社区15 小时前
LeetCode - #139 单词拆分
算法·leetcode·职场和发展
Dong雨16 小时前
力扣hot100-->栈/单调栈
算法·leetcode·职场和发展
周三有雨17 小时前
【面试题系列Vue07】Vuex是什么?使用Vuex的好处有哪些?
前端·vue.js·面试·typescript
爱米的前端小笔记17 小时前
前端八股自学笔记分享—页面布局(二)
前端·笔记·学习·面试·求职招聘
好学近乎知o17 小时前
解决sql字符串
面试