数组部分
1. 合并两个有序的子数组 ------ 倒序双指针避免覆盖
给你两个按 非递减顺序 排列的整数数组 nums1
和 nums2
,另有两个整数 m
和 n
,分别表示 nums1
和 nums2
中的元素数目。
请你 合并 nums2
到 nums1
中,使合并后的数组同样按 非递减顺序 排列。
注意: 最终,合并后数组不应由函数返回,而是存储在数组 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. 移除元素 ------ 数组末尾元素交换法
给你一个数组 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. 删除有序数组中的重复项
给你一个 非严格递增排列 的数组 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位
给你一个有序数组 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)
给定一个大小为 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. 轮转数组 ------ 反转三次
给定一个整数数组 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. 买卖股票的最佳规划 ------ 贪心/动态规划
给定一个数组 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. 买卖股票的最佳时机 Ⅱ
给你一个整数数组 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. 跳跃游戏 ------ 贪心(注意代码的写法)
给你一个非负整数数组 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. 跳跃游戏Ⅱ------(明确什么时候要跳下一步,以及怎样跳步数最少)
给定一个长度为 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]
。
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)复杂度的删除与访问(尾元素替代法)
实现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)
给你一个整数数组 nums
,返回 数组 answer
,其中 answer[i]
等于 nums
中除 nums[i]
之外其余各元素的乘积 。
题目数据 保证 数组 nums
之中任意元素的全部前缀元素和后缀的乘积都在 32 位 整数范围内。
请 不要使用除法, 且在 O(n)
时间复杂度内完成此题。
本题的难点在于不可以使用除法: 且需要在O(1) 的额外空间复杂度内完成这个题目
进阶: 你可以在 O(1)
的额外空间复杂度内完成这个题目吗?( 出于对空间复杂度分析的目的,输出数组 不被视为额外空间。)
参考题解:. - 力扣(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;
}
}