难度标识:
⭐:简单,⭐⭐:中等,⭐⭐⭐:困难。
tips:这里的难度不是根据LeetCode难度定义的,而是根据我解题之后体验到题目的复杂度定义的。
1.最大子数组和 ⭐
思路
这题是标准的Kadane算法的应用,这是一种动态规划的算法,Kadane算法是一种高效解决最大子数组和问题的方法。其核心思想是迭代遍历数组,同时保存两个累计值:当前连续子数组的最大和和全局最大子数组和。
-
对于数组中的每一个位置
i,计算在包含nums[i]的情况下,最大的子数组和是多少。这个和可以是仅包含nums[i],或者是nums[i]加上前一个位置的最大和(即max_current + nums[i])。选择这两者中较大的一个作为当前位置的最大和。 -
这种方式确保了,对于数组中的每一个位置,我们都知道包含这个位置的子数组中可能获得的最大和。
-
通过这种方式,我们可以在遍历数组的过程中,持续追踪到目前为止找到的最大和。
代码
var maxSubArray = function (nums) {
let maxSum = nums[0], res = nums[0];
for (let i = 1; i < nums.length; i++) {
maxSum = Math.max(nums[i], maxSum + nums[i])
res = Math.max(res, maxSum)
}
return res
};
2.合并区间 ⭐
思路
这题可以首先将数组根据子数组的第一个元素排序,然后使用快慢双指针的方法进行求解。
-
排序: 首先,根据区间的起始端点对所有区间进行排序。这样,相似或者可能重叠的区间会在数组中相邻出现。
-
快慢双指针:定义一个慢指针,一个快指针,快指针用来寻找子数组之间开始端点与上一个子数组之间的结束端点关系。
-
如果一个子数组的起始端点小于等于上一个子数组的结束端点,那么这两个区间肯定重叠,使用slow指针保存包含这个重叠的区间。
-
如果一个子数组的起始端点大于上一个子数组的结束端点,那么这两个区间肯定不重叠,使用slow指针直接保存这个区间。
- slow指针保存的就是最后需要的所有区间,我们通过
slice方法对原数组截取即可,返回我们需要的区间数组。
代码
var merge = function (intervals) {
intervals.sort((a, b) => a[0] - b[0])
let slow = 0, fast = 0;
while (fast < intervals.length) {
if (intervals[fast][0] <= intervals[slow][1]) {
intervals[slow][1] = Math.max(intervals[slow][1], intervals[fast][1])
} else {
slow++
intervals[slow] = intervals[fast]
}
fast++
}
return intervals.slice(0, slow + 1)
};
3.轮转数组 ⭐
思路
思路1:
-
1.首先处理一下k,k如果大于数组长度的话,我们得让k除以数组长度,然后得到得余数就是需要向右轮转位置的多少了,小于的时候其实不用处理,用
k = k%nums.length即可 -
2.直接截取后k个元素,然后插入到数组最前面,这其实就完成了向右轮转,代码见下面代码中的思路1代码。
思路2:
这题后面有条 你可以使用空间复杂度为 O(1) 的 原地 算法解决这个问题吗? 上面这个方法虽然简单,但是空间复杂度是 O(k),这是由于 splice 方法返回一个包含 k 个元素的新数组。上面已经给我们提示了,原地算法,就是直接操作原来的数组来进行修改,不使用额外的非常量空间。那我们可以使用左右双指针的方法让数组做反转。
-
反转整个数组。
-
反转数组的前 k % n 个元素。
-
反转数组的剩余元素。
这个三次反转方法的核心原理是:两次反转操作的组合效果实际上等同于一次旋转操作。
考虑一个简单的例子,有一个数组
[1,2,3,4],我们要向右旋转2个位置,那我们想要得到的结果就是[3,4,1,2],如果我们只是简单地将前2个数字反转得到[2,1,3,4],然后再反转后2个数字得到[2,1,4,3],这并不是我们想要的结果。但是,如果我们先反转整个数组,得到[4,3,2,1],然后按照上述方式操作,我们就会得到正确的旋转结果[3,4,1,2]。通过这种方式,我们可以利用简单的数组反转操作来实现复杂的数组旋转操作。
代码
思路1代码:
var rotate = function (nums, k) {
k %= nums.length
return nums.unshift(...nums.splice(nums.length - k, k))
};
思路2代码:
var rotate = function (nums, k) {
const n = nums.length
k %= nums.length
reverse(nums, 0, n - 1)
reverse(nums, 0, k-1)
reverse(nums, k, n - 1)
};
function reverse(nums, left, right) {
while (left < right) {
[nums[left], nums[right]] = [nums[right], nums[left]]
left++;
right--
}
}
4.除自身以外数组的乘积 ⭐⭐
思路
这题可以使用左右双指针解决。
-
拿一个左指针,从左往右遍历数组,并把每个元素左边的乘积和记录到一个新数组的同样位置。
-
再拿一个右指针,从右往左遍历数组,并把每个元素右边的乘积记录到右指针中并乘以刚才同样位置的左边乘积的和,那这样我们就得到的每个元素左边和右边所有元素的和,排除了它自身。
这个方法其实就是以当前元素为中心,去找它左右两边的乘积和,也是左右双指针的一种应用,思路只不过是从中间量变。做的时候从两边往中间就行了。
代码
var productExceptSelf = function (nums) {
const n = nums.length
const answer = [1]
let left = 1
for (let i = 1; i < n; i++) {
left *= nums[i - 1]
answer[i] = left
}
let right = 1
for (let i = n - 2; i >= 0; i--) {
right *= nums[i + 1]
answer[i] *= right
}
return answer
};
5.缺失的第一个正数 ⭐⭐
思路
思路1
首先这题如果我们不考虑题目的 请你实现时间复杂度为 O(n) 并且只使用常数级别额外空间的解决方案。 这个条件,那是不是很简单,直接拿一个哈希表记录数组中的元素,然后从1开始去哈希表中找存不存在,不存在,那最小整数就是这个数。代码见下面思路1代码,但是这个方法的空间复杂度是 O(n),因为你使用了一个集合。
思路2
如果要想空间复杂度达到 O(1),那么我们只能使用原数组来进行哈希表计数标识。
就地置换:核心思想是尝试将每个数字放在其应该在的位置上。例如,数字1应该在nums[0]的位置,数字2应该在nums[1]的位置,以此类推。对于任意数字nums[i],它应该在i-1的位置。如果数组是[3,4,-1,1],我们希望它看起来是[1,-1,3,4]。这意味着数字nums[i]应该在位置i-1。代码见下面的思路2代码。
代码
思路1代码
var firstMissingPositive = function (nums) {
const set = new Set(nums)
let i = 1
while (true) {
if (!set.has(i)) {
return i
}
i++
}
};
思路2代码
var firstMissingPositive = function (nums) {
const n = nums.length
for (let i = 0; i < n; i++) {
if (nums[i] < 1) {
nums[i] = n + 1
}
}
for (let i = 0; i < n; i++) {
const num = Math.abs(nums[i])
if (num < n + 1) {
nums[num - 1] = -Math.abs(nums[num - 1])
}
}
for (let i = 0; i < n; i++) {
if (nums[i] > 0) {
return i + 1
}
}
return n + 1
};