跳到主要内容

131 篇博文 含有标签「数组」

查看所有标签

· 阅读需 6 分钟

1、题干

给你一个整数数组 arr ,你一开始在数组的第一个元素处(下标为 0)。

每一步,你可以从下标 i 跳到下标 i + 1i - 1 或者 j

  • i + 1 需满足:i + 1 < arr.length
  • i - 1 需满足:i - 1 >= 0
  • j 需满足:arr[i] == arr[j] 且 i != j

请你返回到达数组最后一个元素的下标处所需的 最少操作次数 。

注意:任何时候你都不能跳到数组外面。

 

示例 1:

输入:arr = [100,-23,-23,404,100,23,23,23,3,404]
输出:3
解释:那你需要跳跃 3 次,下标依次为 0 --> 4 --> 3 --> 9 。下标 9 为数组的最后一个元素的下标。

示例 2:

输入:arr = [7]
输出:0
解释:一开始就在最后一个元素处,所以你不需要跳跃。

示例 3:

输入:arr = [7,6,9,6,9,6,9,7]
输出:1
解释:你可以直接从下标 0 处跳到下标 7 处,也就是数组的最后一个元素处。

 

提示:

  • 1 <= arr.length <= 5 * 104
  • -108 <= arr[i] <= 108

DFS解法写自闭了,简单写下BFS和动态规划的思路和代码

2、解法1-BFS

BFS解法需要注意各种边界条件以及剪枝优化,不然很可能超时、内存溢出。剪枝的方法有这些:

  • 跳过相邻且值相同的节点,只留头尾,中间部分没有意义
  • 跳过已访问过的节点
  • BFS的当前层级超过到达该节点的最小步数

3、代码

var minJumps = function (arr) {
const n = arr.length;

// 记录数字对应的索引以及最小步数
const map = new Map();
for (let i = 0; i < n; i++) {
if (arr[i] === arr[i + 1] && arr[i] === arr[i - 1]) continue;
map.get(arr[i]) ? map.get(arr[i])[0].push(i) : map.set(arr[i], [[i], Infinity]);
}

const visited = new Set();
let queue = [0], level = 0, res = Infinity;

while (queue.length) {
const set = new Set();
if (level >= res) break;
for (const i of queue) {
if (visited.has(i)) continue;
visited.add(i);

if (i === n - 1) res = Math.min(res, level);
if (i >= 1) set.add(i - 1);
if (i <= n - 2) set.add(i + 1);

const [idxs, min] = map.get(arr[i]);
if (level + 1 > min) continue;

map.get(arr[i])[1] = Math.min(min, level + 1);
for (const x of idxs) {
if (x !== i) set.add(x);
}
}
queue = [...set];
level++;
}

return res;
};

4、执行结果

  • 执行用时: 180 ms
  • 内存消耗: 62.3 MB

5、解法2-动态规划

实际上这道题不能用动态规划,因为违反了无后效性,其实带来的问题就是不能一次性规划出结果,这个问题没啥好办法,多规几次就出来了(两次动态规划结果一致则认为已经是最优解)。

6、代码

var minJumps = function (arr) {
const n = arr.length;

// 记录数字对应的索引
const map = new Map();
for (let i = 0; i < n; i++) {
map.get(arr[i]) ? map.get(arr[i])[0].push(i) : map.set(arr[i], [[i], Infinity]);
}

// DP处理
const dp = new Array(n).fill(Infinity);
dp[0] = 0;

function updateSame(i) {
const [idxs, min] = map.get(arr[i]);
if (idxs.length < 2 || dp[i] >= min) return;
map.get(arr[i])[1] = dp[i];
for (let j = 0; j < idxs.length; j++) {
dp[idxs[j]] = Math.min(dp[i] + 1, dp[idxs[j]]);
}
}

function runDp() {
for (let i = 0; i < n; i++) {
if (i > 0) dp[i - 1] = Math.min(dp[i] + 1, dp[i - 1]), updateSame(i - 1);
if (i < n - 1) dp[i + 1] = Math.min(dp[i] + 1, dp[i + 1]), updateSame(i + 1);

updateSame(i);
}
}

while (1) {
runDp();
const r1 = dp.toString();
runDp();
const r2 = dp.toString();
if (r1 === r2) return dp[n - 1];
}
};

7、执行结果

  • 执行用时: 380 ms
  • 内存消耗: 65.1 MB

8、解法3-DFS

各种处理和优化,最终还是没能AC,直接贴代码吧,如果有大神看到希望指点下🙏

var minJumps = function (arr) {
const n = arr.length;

// 记录数字对应的索引以及最小步数
const map = new Map();
for (let i = 0; i < n; i++) {
if (arr[i] === arr[i + 1] && arr[i] === arr[i - 1]) continue;
map.get(arr[i]) ? map.get(arr[i])[0].push(i) : map.set(arr[i], [
[i], Infinity
]);
}

let res = Infinity;

function dfs(i, path) {
if (i === n - 1) return res = Math.min(res, path.size);
if (i < 0 || i > n - 1 || path.has(i)) return;

const [idxs, min] = map.get(arr[i]);
if (path.size >= min || path.size + 1 >= res) return;

path.add(i);

for (let j = 0; idxs.length > 1 && j < idxs.length; j++) {
if (i !== idxs[j]) dfs(idxs[j], path);
}

dfs(i - 1, path);
dfs(i + 1, path);

map.get(arr[i])[1] = Math.min(map.get(arr[i])[1], path.size);
path.delete(i);
}

return dfs(0, new Set()), res;
};

· 阅读需 4 分钟

1、题干

给你一个整数数组 nums 和一个整数 k ,判断数组中是否存在两个 不同的索引 i 和 j ,满足 nums[i] == nums[j]abs(i - j) <= k 。如果存在,返回 true ;否则,返回 false

 

示例 1:

输入:nums = [1,2,3,1], k = 3
输出:true

示例 2:

输入:nums = [1,0,1,1], k = 1
输出:true

示例 3:

输入:nums = [1,2,3,1,2,3], k = 2
输出:false

 

 

提示:

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

2、解法1-哈希映射

遍历数组所有元素,并将当前数字和索引作为键值存入哈希映射 map 中,如果当前数字 nums[i] 已存在于 map 中且其索引与当前索引的差值不大于 k 则返回 true,遍历结束仍未找到符合条件的数字则返回 false


3、复杂度

  • 时间复杂度:O(n)O(n)
  • 空间复杂度:O(n)O(n)

4、代码

var containsNearbyDuplicate = function (nums, k) {
const map = new Map();
for (let i = 0; i < nums.length; i++) {
if (map.has(nums[i]) && i - map.get(nums[i]) <= k) return true;
map.set(nums[i], i);
}
return false;
};
class Solution:
def containsNearbyDuplicate(self, nums: List[int], k: int) -> bool:
map = {}
for i in range(len(nums)):
if(nums[i] in map and i - map[nums[i]] <= k):
return True
map[nums[i]] = i
return False

5、解法2-滑动窗口+哈希集合

遍历数组所有元素,使用哈希集合 set 作为滑动窗口容器存储数组中连续的 k 个元素,索引 i 大于 k 之后需要剔除滑动窗口容器的首个元素 nums[i - k - 1] 以保证其长度不超过 k,如果当前元素已存在于哈希集合则说明二者索引差值不大于 k 应返回 true,遍历结束仍未找到符合条件的数字则返回 false

解法1的优势在于更容易想到和理解。解法2的优势在于只需要存储值而不需要存储键值对,因此空间占用会更少;另外当 knums.length 小的情况下,解法2的空间复杂度会更小且不受 nums.length 变大而影响。


6、复杂度

  • 时间复杂度:O(n)O(n)
  • 空间复杂度:O(k)O(k)

7、代码

var containsNearbyDuplicate = function (nums, k) {
const set = new Set();
for (let i = 0; i < nums.length; i++) {
if (i > k) set.delete(nums[i - k - 1]);
if (set.has(nums[i])) return true;
set.add(nums[i]);
}
return false;
};
class Solution:
def containsNearbyDuplicate(self, nums: List[int], k: int) -> bool:
s = set()
for i in range(len(nums)):
if(i > k):
s.remove(nums[i - k - 1])
if(nums[i] in s):
return True
s.add(nums[i])
return False

· 阅读需 3 分钟

1、题干

给定一个整数数组 nums 和一个整数 k ,请返回其中出现频率前 k 高的元素。可以按 任意顺序 返回答案。

 

示例 1:

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

示例 2:

输入: nums = [1], k = 1
输出: [1]

 

提示:

  • 1 <= nums.length <= 105
  • k 的取值范围是 [1, 数组中不相同的元素的个数]
  • 题目数据保证答案唯一,换句话说,数组中前 k 个高频元素的集合是唯一的

 

进阶:所设计算法的时间复杂度 必须 优于 O(n log n) ,其中 n 是数组大小。

 

注意:本题与主站 347 题相同:https://leetcode-cn.com/problems/top-k-frequent-elements/

2、解法1-API排序

先使用哈希映射 map 对所有整数进行计数,然后将 map 中的键值对存入数组并使用排序API按数量降序排序,最后返回前 k 个数字


3、代码

var topKFrequent = function (nums, k) {
const map = new Map();
for (const n of nums) map.set(n, 1 + (map.get(n) || 0));

const arr = [];
for (const kv of map) arr.push(kv);
return arr.sort((a, b) => b[1] - a[1]).slice(0, k).map(a => a[0]);
};

4、复杂度

  • 时间复杂度:O(nlogn)O(nlogn)
  • 空间复杂度:O(n)O(n)

5、解法2-桶排序

先使用哈希映射 map 对所有整数进行计数,然后将 map 中的键值对存入按数量存入桶中,最后返回前 k 个数字。其时间复杂度为 O(n)O(n),解决了进阶问题。


6、代码

var topKFrequent = function (nums, k) {
const map = new Map();
for (const n of nums) map.set(n, 1 + (map.get(n) || 0));

const buckets = new Array(nums.length + 1);
for (const [n, v] of map) !buckets[v] ? buckets[v] = [n] : buckets[v].push(n);

const res = [];
for (let i = buckets.length - 1; i > -1; i--) {
if (res.length >= k) break;
if (buckets[i]) res.push(...buckets[i]);
}

return res.length > k ? res.slice(0, k) : res;
};

7、复杂度

  • 时间复杂度:O(n)O(n)
  • 空间复杂度:O(n)O(n)

· 阅读需 3 分钟

1、题干

给定一个整数数组 nums 和一个整数 k ,请返回其中出现频率前 k 高的元素。可以按 任意顺序 返回答案。

 

示例 1:

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

示例 2:

输入: nums = [1], k = 1
输出: [1]

 

提示:

  • 1 <= nums.length <= 105
  • k 的取值范围是 [1, 数组中不相同的元素的个数]
  • 题目数据保证答案唯一,换句话说,数组中前 k 个高频元素的集合是唯一的

 

进阶:所设计算法的时间复杂度 必须 优于 O(n log n) ,其中 n 是数组大小。

 

注意:本题与主站 347 题相同:https://leetcode-cn.com/problems/top-k-frequent-elements/

2、解法1-API排序

先使用哈希映射 map 对所有整数进行计数,然后将 map 中的键值对存入数组并使用排序API按数量降序排序,最后返回前 k 个数字


3、代码

var topKFrequent = function (nums, k) {
const map = new Map();
for (const n of nums) map.set(n, 1 + (map.get(n) || 0));

const arr = [];
for (const kv of map) arr.push(kv);
return arr.sort((a, b) => b[1] - a[1]).slice(0, k).map(a => a[0]);
};

4、复杂度

  • 时间复杂度:O(nlogn)O(nlogn)
  • 空间复杂度:O(n)O(n)

5、解法2-桶排序

先使用哈希映射 map 对所有整数进行计数,然后将 map 中的键值对存入按数量存入桶中,最后返回前 k 个数字。其时间复杂度为 O(n)O(n),解决了进阶问题。


6、代码

var topKFrequent = function (nums, k) {
const map = new Map();
for (const n of nums) map.set(n, 1 + (map.get(n) || 0));

const buckets = new Array(nums.length + 1);
for (const [n, v] of map) !buckets[v] ? buckets[v] = [n] : buckets[v].push(n);

const res = [];
for (let i = buckets.length - 1; i > -1; i--) {
if (res.length >= k) break;
if (buckets[i]) res.push(...buckets[i]);
}

return res.length > k ? res.slice(0, k) : res;
};

7、复杂度

  • 时间复杂度:O(n)O(n)
  • 空间复杂度:O(n)O(n)

· 阅读需 8 分钟

1、题干

给你一个下标从 0 开始的二维整数数组 questions ,其中 questions[i] = [pointsi, brainpoweri] 。

这个数组表示一场考试里的一系列题目,你需要 按顺序 (也就是从问题 0 开始依次解决),针对每个问题选择 解决 或者 跳过 操作。解决问题 i 将让你 获得  pointsi 的分数,但是你将 无法 解决接下来的 brainpoweri 个问题(即只能跳过接下来的 brainpoweri 个问题)。如果你跳过问题 i ,你可以对下一个问题决定使用哪种操作。

  • 比方说,给你 questions = [[3, 2], [4, 3], [4, 4], [2, 5]] :
    • 如果问题 0 被解决了, 那么你可以获得 3 分,但你不能解决问题 1 和 2 。
    • 如果你跳过问题 0 ,且解决问题 1 ,你将获得 4 分但是不能解决问题 2 和 3 。

请你返回这场考试里你能获得的 最高 分数。

 

示例 1:

输入:questions = [[3,2],[4,3],[4,4],[2,5]]
输出:5
解释:解决问题 0 和 3 得到最高分。
- 解决问题 0 :获得 3 分,但接下来 2 个问题都不能解决。
- 不能解决问题 1 和 2
- 解决问题 3 :获得 2 分
总得分为:3 + 2 = 5 。没有别的办法获得 5 分或者多于 5 分。

示例 2:

输入:questions = [[1,1],[2,2],[3,3],[4,4],[5,5]]
输出:7
解释:解决问题 1 和 4 得到最高分。
- 跳过问题 0
- 解决问题 1 :获得 2 分,但接下来 2 个问题都不能解决。
- 不能解决问题 2 和 3
- 解决问题 4 :获得 5 分
总得分为:2 + 5 = 7 。没有别的办法获得 7 分或者多于 7 分。

 

提示:

  • 1 <= questions.length <= 105
  • questions[i].length == 2
  • 1 <= pointsi, brainpoweri <= 105

动态规划没学明白,周赛坑了一小时没写出来,可能我智力有问题吧

13375743a4d4704fbb667f6be417fd85.gif

2、解法1-记忆化搜索

  • 每个问题都有解决和跳过两种选择
  • 对于任意问题 ii 如果解决则得分叠加下一道可解的题即 dfs(i + 1 + questions[i][1]) + questions[i][0])
  • 如果跳过则得分与下一道题相同即 dfs(i + 1)
  • 题目要求最高分数因此取二者的最大值,如此深度遍历下去直至结束
  • 由于每个问题都有2种状态,因此时间复杂度接近 O(2n)O(2^n),为了避免超时使用哈希表记录已搜索过的题目后续直接返回

一般动态规划的题都能用暴力穷举(DFS、BFS、回溯等)解决,如果实在想不到动态规划的解题思路,可以尝试暴力穷举+记忆化。


3、复杂度

  • 时间复杂度:不使用记忆化时为 O(2n)O(2^n),使用记忆化后无法确定
  • 空间复杂度:O(n)O(n)

4、代码

var mostPoints = function (questions) {
const n = questions.length, visited = new Map();
function dfs(idx) {
if (idx > n - 1) return 0;
if (visited.has(idx)) return visited.get(idx);
const [score, skip] = questions[idx];
return visited.set(idx, Math.max(dfs(idx + 1), dfs(idx + 1 + skip) + score)).get(idx);
}
return dfs(0);
};

5、执行结果

执行用时: 360 ms 内存消耗: 91.8 MB


6、解法2-顺序DP

  • 对于任意问题 ii 而言 dp[i]dp[i] 表示完成 [0,i][0,i] 题能获得的最高分,另外每个问题都有解决和跳过两种选择
  • 如果解决问题 ii 则其得分 dp[i]+questions[i][0]dp[i] + questions[i][0] 可以转移给下一道可解的题 jj 使用(其中 j=questions[i][1]+i+1j = questions[i][1] + i + 1),转移时 dp[j]dp[j] 取二者最大值即 dp[j]=Math.max(dp[i]+questions[i][0],dp[j])dp[j] = Math.max(dp[i] + questions[i][0], dp[j])
  • 如果跳过问题 ii 则其得分 dp[i]dp[i] 可以转移给下一道题 i+1i + 1 使用,转移时 dp[i+1]dp[i + 1] 取二者最大值即 dp[i+1]=Math.max(dp[i],dp[i+1]);dp[i + 1] = Math.max(dp[i], dp[i + 1]);

注意:\ 一般动态规划按以上思路遍历下去最终返回 dpdp 数组末尾元素即可,但这里会出现一种特殊情况:最高分在 dpdp 数组中的索引可能大于 questions.lengthquestions.length。 例如用例:[[25,1],[44,3],[91,1],[12,3],[93,3]] 的长度为5,而最高分出现在 dp[8]dp[8]

因此使用变量 max 在动态规划过程中记录所有得分中的最高分并返回,最后AC了。

所以最后注意动态规划过程中处理好最高得分数组越界的问题就能AC


7、复杂度

  • 时间复杂度:O(n)O(n)
  • 空间复杂度:O(n)O(n)

8、代码

var mostPoints = function (questions) {
const n = questions.length, dp = new Array(n).fill(0);
let max = 0;
for (let i = 0; i < n; i++) {
dp[i + 1] = Math.max(dp[i], dp[i + 1] || 0);
const j = questions[i][1] + i + 1;
dp[j] = Math.max(dp[i] + questions[i][0], dp[j] || 0);
max = Math.max(dp[i + 1], dp[j], max);
}
return max;
};

9、执行结果

执行用时: 280 ms 内存消耗: 69.8 MB


10、解法3-逆序DP

看了些资料才发现,逆序DP也是一种解题方法,并不是特例

  • 逆序DP中状态转移是相反的,即将靠后问题的得分转移给前面的问题,对于任意问题 ii 而言 dp[i]dp[i] 表示完成 [i,n1][i,n-1] 题能获得的最高分
  • 如果选择跳过问题 ii,则将下一题得分 dp[i+1]dp[i + 1] 转移给 dp[i]dp[i]
  • 如果选择解决问题 ii,则将下一可解题得分 dp[questions[i][1]+i+1]dp[questions[i][1] + i + 1] 转移给 dp[i]dp[i],并叠加当前题得分 questions[i][0]questions[i][0]
  • 依次处理所有问题,最后返回 dp[0]dp[0]

11、复杂度

  • 时间复杂度:O(n)O(n)
  • 空间复杂度:O(n)O(n)

12、代码

var mostPoints = function (questions) {
const n = questions.length, dp = new Array(n).fill(0);
for (let i = n - 1; i > -1; i--) {
dp[i] = Math.max(dp[i + 1] || 0, questions[i][0] + (dp[questions[i][1] + i + 1] || 0));
}
return dp[0];
};

13、执行结果

执行用时: 176 ms 内存消耗: 65 MB

· 阅读需 8 分钟

1、题干

给定两个以 非递减顺序排列 的整数数组 nums1 nums2 , 以及一个整数 k 

定义一对值 (u,v),其中第一个元素来自 nums1,第二个元素来自 nums2 

请找到和最小的 k 个数对 (u1,v1),  (u2,v2)  ...  (uk,vk) 。

 

示例 1:

输入: nums1 = [1,7,11], nums2 = [2,4,6], k = 3
输出: [1,2],[1,4],[1,6]
解释: 返回序列中的前 3 对数:
[1,2],[1,4],[1,6],[7,2],[7,4],[11,2],[7,6],[11,4],[11,6]

示例 2:

输入: nums1 = [1,1,2], nums2 = [1,2,3], k = 2
输出: [1,1],[1,1]
解释: 返回序列中的前 2 对数:
  [1,1],[1,1],[1,2],[2,1],[1,2],[2,2],[1,3],[1,3],[2,3]

示例 3:

输入: nums1 = [1,2], nums2 = [3], k = 3 
输出: [1,3],[2,3]
解释: 也可能序列中所有的数对都被返回:[1,3],[2,3]

 

提示:

  • 1 <= nums1.length, nums2.length <= 105
  • -109 <= nums1[i], nums2[i] <= 109
  • nums1nums2 均为 升序排列
  • 1 <= k <= 104
  • k <= nums1.length * nums2.length

最近发现,力扣的JS环境支持最小优先队列 MinPriorityQueue 和 最大优先队列 MaxPriorityQueue,直接使用即可,无需引入或特殊配置,其 API 和相关信息可以参考datastructures-js


2、解法1-优先队列

  • 使用最小优先队列 MinPriorityQueue 存储 nums1 中所有元素下标与 nums20 下标构成的数对
  • 然后取队头下标数对进行遍历,遍历时以 nums2 的下标步进
  • 若队头下标数对所对应元素之和比当前数对之和更小则将当前下标入队并中断遍历,反之将当前数对推入结果数组 res
  • 重复前两步遍历直至结束

3、代码

var kSmallestPairs = function (nums1, nums2, k) {
const res = [];
const pq = new MinPriorityQueue({ compare: (a, b) => nums1[a[0]] + nums2[a[1]] - (nums1[b[0]] + nums2[b[1]]) });

for (let i = 0; i < nums1.length; i++) pq.enqueue([i, 0]);

while (res.length < k && pq.size()) {
let [i, j] = pq.dequeue();

for (; res.length < k && j < nums2.length; j++) {
// 队头的数对和更小
const [i1, j1] = pq.front() || [];
if (pq.size() && nums1[i1] + nums2[j1] < nums1[i] + nums2[j]) {
pq.enqueue([i, j]);
break;
}

// 队头的数对和相等或更大
res.push([nums1[i], nums2[j]]);
}
}

return res;
};

4、执行结果

执行用时: 160 ms 内存消耗: 61.7 MB


5、解法1-优化

  • 问题1:因为是求 TopK,所以优先队列没必要存储超过 k 个元素,这是导致时间空间过高的主要原因
  • 问题2:既然是求 TopK,队列初始时已加入 k 个元素,后续只要在队列中排序和轮替所有数据即可;而上述逻辑是一边遍历一边与队头元素比较,虽然不影响时间和空间,但是代码和逻辑都更复杂
  • 优化1:优先队列添加初始数据时,只需要加入 Math.min(k, nums1.length) 个元素即可
  • 优化2:while 循环那段代码的逻辑改成:每次取队头下标对应的数对推入结果数组 res,并且将下一对未入队的下标 [i, j + 1] 加入队列中

6、复杂度

  • 时间复杂度:O(klogk)O(klogk)
  • 空间复杂度:O(k)O(k)

7、代码

var kSmallestPairs = function (nums1, nums2, k) {
const res = [];
const pq = new MinPriorityQueue({ compare: (a, b) => nums1[a[0]] + nums2[a[1]] - (nums1[b[0]] + nums2[b[1]]) });

for (let i = 0; i < Math.min(k, nums1.length); i++) pq.enqueue([i, 0]);

while (res.length < k && pq.size()) {
const [i, j] = pq.dequeue();
if (j + 1 < nums2.length) pq.enqueue([i, j + 1]);
res.push([nums1[i], nums2[j]]);
}

return res;
};

8、执行结果

1.png


9、解法2-单调栈

  • 思路跟优先队列的方式类似,使用递减栈 minStack 存储 nums1 中所有元素下标与 nums20 下标构成的数对,注意要倒序入栈以保证 minStack 必定是一个递减栈
  • 然后取栈顶下标数对进行遍历,遍历时以 nums2 的下标步进
  • 若栈顶下标数对所对应元素之和比当前数对之和更小则将当前下标入栈并中断遍历,反之将当前数对推入结果数组 res
  • 上一步的入栈不是压入栈顶,而是倒序遍历插入更大元素之后以保持 minStack 递减(此时二分查找再入栈无法降时间复杂度)
  • 重复前三步遍历直至结束

10、代码

var kSmallestPairs = function (nums1, nums2, k) {
const res = [], minStack = [];
for (let i = nums1.length - 1; i > -1; i--) minStack.push([i, 0]);

while (res.length < k && minStack.length) {
let [i, j] = minStack.pop();

for (; res.length < k && j < nums2.length; j++) {
// 栈顶的数对和相等或更大
const [i1, j1] = minStack[minStack.length - 1] || [];
if (!minStack.length || nums1[i1] + nums2[j1] >= nums1[i] + nums2[j]) {
res.push([nums1[i], nums2[j]]);
continue;
}

// 栈顶的数对和更小
if (nums1[minStack[0][0]] + nums2[minStack[0][1]] <= nums1[i] + nums2[j]) {
minStack.unshift([i, j]);
break;
}

for (let m = minStack.length - 1; m > -1; m--) {
const [i1, j1] = minStack[m];
if (nums1[i1] + nums2[j1] >= nums1[i] + nums2[j]) {
minStack.splice(m + 1, 0, [i, j]);
break;
}
}
break;
}
}

return res;
};

11、执行结果

执行用时: 152 ms 内存消耗: 60.4 MB


12、解法2-优化

问题和优化点都跟解法1的问题和优化点一样,单调栈 minStack 没必要存储超过 k 个元素,后续找最小的 k 对数字只要在单调栈中排序和轮替所有数据即可


13、复杂度

  • 时间复杂度:复杂度不稳定,介于 O(k)O(k)O(k2)O(k^2) 之间
  • 空间复杂度:O(k)O(k)

14、代码

var kSmallestPairs = function (nums1, nums2, k) {
const res = [], minStack = [];
for (let i = Math.min(k, nums1.length) - 1; i > -1; i--) minStack.push([i, 0]);

while (res.length < k && minStack.length) {
const [i, j] = minStack.pop();
res.push([nums1[i], nums2[j]]);

if (j + 1 >= nums2.length) continue;
const len = minStack.length;
for (let m = len - 1; m > -1; m--) {
const [i1, j1] = minStack[m];
if (nums1[i1] + nums2[j1] >= nums1[i] + nums2[j + 1]) {
minStack.splice(m + 1, 0, [i, j + 1]);
break;
}
}
if (len === minStack.length) minStack.unshift([i, j + 1]);
}

return res;
};

15、执行结果

执行用时: 120 m 内存消耗: 51.6 MB

· 阅读需 2 分钟

1、题干

给你一个整数数组 nums ,其中总是存在 唯一的 一个最大整数 。

请你找出数组中的最大元素并检查它是否 至少是数组中每个其他数字的两倍 。如果是,则返回 最大元素的下标 ,否则返回 -1

 

示例 1:

输入:nums = [3,6,1,0]
输出:1
解释:6 是最大的整数,对于数组中的其他整数,6 至少是数组中其他元素的两倍。6 的下标是 1 ,所以返回 1 。

示例 2:

输入:nums = [1,2,3,4]
输出:-1
解释:4 没有超过 3 的两倍大,所以返回 -1 。

 

提示:

  • 2 <= nums.length <= 50
  • 0 <= nums[i] <= 100
  • nums 中的最大元素是唯一的

2、解题思路

遍历两次,先找到最大数的序号idx,再查找是否存在乘2之后大于最大数的整数,若有则返回-1反之返回idx

3、代码

var dominantIndex = function (nums) {
const idx = nums.reduce((a, c, i) => c > nums[a] ? i : a, 0);
return nums.some((v, i) => i !== idx && 2 * v > nums[idx]) ? -1 : idx;
};

4、执行结果

image.png

· 阅读需 3 分钟

1、题干

给你一个整数数组 nums 和两个整数 kt 。请你判断是否存在 两个不同下标 ij,使得 abs(nums[i] - nums[j]) <= t ,同时又满足 abs(i - j) <= k

如果存在则返回 true,不存在返回 false

 

示例 1:

输入:nums = [1,2,3,1], k = 3, t = 0
输出:true

示例 2:

输入:nums = [1,0,1,1], k = 1, t = 2
输出:true

示例 3:

输入:nums = [1,5,9,1,5,9], k = 2, t = 3
输出:false

 

提示:

  • 0 <= nums.length <= 2 * 104
  • -231 <= nums[i] <= 231 - 1
  • 0 <= k <= 104
  • 0 <= t <= 231 - 1

 

注意:本题与主站 220 题相同: https://leetcode-cn.com/problems/contains-duplicate-iii/

2、解法1-暴力解法

双层遍历,外层从头到尾遍历,内层遍历外层元素之后的 k 个元素,如果找到两个元素差值的绝对值小于等于 t 则结果为 true

注意:内层只需要遍历后 k 个元素,因为前 k 个元素在外层遍历时已经判断过

3、复杂度

  • 时间复杂度:O(nk)O(nk)
  • 空间复杂度:O(1)O(1)

4、代码

var containsNearbyAlmostDuplicate = function (nums, k, t) {
for (let i = 0; i < nums.length; i++) {
for (let j = i + 1; j <= i + k && j < nums.length; j++) {
if (Math.abs(nums[j] - nums[i]) <= t) return true;
}
}
return false;
};

5、执行结果

执行用时: 500 ms 内存消耗: 39.1 MB

6、解法2-桶排序

遍历过程中使用桶排序维护 k 个元素的有序集合,桶的大小设置为 t + 1 以保证同一个桶内出现两个元素 n1n2 时,必定有 abs(n1 - n2) <= t 即结果为true,另外如果相邻的桶内有数字且满足与当前数字的差值的绝对值小于等于 t 则结果为 true

7、复杂度

  • 时间复杂度:O(n)O(n)
  • 空间复杂度:O(k)O(k)

8、代码

var containsNearbyAlmostDuplicate = function (nums, k, t) {
const buckets = new Map();
const genIdx = (n) => ((n + 2147483648) / (t + 1)) >> 0;

for (let i = 0; i < nums.length; i++) {
if (i > k) buckets.delete(genIdx(nums[i - k - 1]));
const idx = genIdx(nums[i]);
if (buckets.has(idx)) return true;
if (buckets.has(idx - 1) && nums[i] - buckets.get(idx - 1) <= t) return true;
if (buckets.has(idx + 1) && buckets.get(idx + 1) - nums[i] <= t) return true;
buckets.set(idx, nums[i]);
}

return false;
};

9、执行结果

image.png

· 阅读需 3 分钟

1、题干

给你一个整数数组 nums 和两个整数 kt 。请你判断是否存在 两个不同下标 ij,使得 abs(nums[i] - nums[j]) <= t ,同时又满足 abs(i - j) <= k

如果存在则返回 true,不存在返回 false

 

示例 1:

输入:nums = [1,2,3,1], k = 3, t = 0
输出:true

示例 2:

输入:nums = [1,0,1,1], k = 1, t = 2
输出:true

示例 3:

输入:nums = [1,5,9,1,5,9], k = 2, t = 3
输出:false

 

提示:

  • 0 <= nums.length <= 2 * 104
  • -231 <= nums[i] <= 231 - 1
  • 0 <= k <= 104
  • 0 <= t <= 231 - 1

 

注意:本题与主站 220 题相同: https://leetcode-cn.com/problems/contains-duplicate-iii/

2、解法1-暴力解法

双层遍历,外层从头到尾遍历,内层遍历外层元素之后的 k 个元素,如果找到两个元素差值的绝对值小于等于 t 则结果为 true

注意:内层只需要遍历后 k 个元素,因为前 k 个元素在外层遍历时已经判断过

3、复杂度

  • 时间复杂度:O(nk)O(nk)
  • 空间复杂度:O(1)O(1)

4、代码

var containsNearbyAlmostDuplicate = function (nums, k, t) {
for (let i = 0; i < nums.length; i++) {
for (let j = i + 1; j <= i + k && j < nums.length; j++) {
if (Math.abs(nums[j] - nums[i]) <= t) return true;
}
}
return false;
};

5、执行结果

执行用时: 500 ms 内存消耗: 39.1 MB

6、解法2-桶排序

遍历过程中使用桶排序维护 k 个元素的有序集合,桶的大小设置为 t + 1 以保证同一个桶内出现两个元素 n1n2 时,必定有 abs(n1 - n2) <= t 即结果为true,另外如果相邻的桶内有数字且满足与当前数字的差值的绝对值小于等于 t 则结果为 true

7、复杂度

  • 时间复杂度:O(n)O(n)
  • 空间复杂度:O(k)O(k)

8、代码

var containsNearbyAlmostDuplicate = function (nums, k, t) {
const buckets = new Map();
const genIdx = (n) => ((n + 2147483648) / (t + 1)) >> 0;

for (let i = 0; i < nums.length; i++) {
if (i > k) buckets.delete(genIdx(nums[i - k - 1]));
const idx = genIdx(nums[i]);
if (buckets.has(idx)) return true;
if (buckets.has(idx - 1) && nums[i] - buckets.get(idx - 1) <= t) return true;
if (buckets.has(idx + 1) && buckets.get(idx + 1) - nums[i] <= t) return true;
buckets.set(idx, nums[i]);
}

return false;
};

9、执行结果

image.png

· 阅读需 5 分钟

1、题干

给你一个整数数组 nums ,判断这个数组中是否存在长度为 3 的递增子序列。

如果存在这样的三元组下标 (i, j, k) 且满足 i < j < k ,使得 nums[i] < nums[j] < nums[k] ,返回 true ;否则,返回 false

 

示例 1:

输入:nums = [1,2,3,4,5]
输出:true
解释:任何 i < j < k 的三元组都满足题意

示例 2:

输入:nums = [5,4,3,2,1]
输出:false
解释:不存在满足题意的三元组

示例 3:

输入:nums = [2,1,5,0,4,6]
输出:true
解释:三元组 (3, 4, 5) 满足题意,因为 nums[3] == 0 < nums[4] == 4 < nums[5] == 6

 

提示:

  • 1 <= nums.length <= 5 * 105
  • -231 <= nums[i] <= 231 - 1

 

进阶:你能实现时间复杂度为 O(n) ,空间复杂度为 O(1) 的解决方案吗?

2、解法1-求子问题

  • 问题是求递增的三元子序列,实际可以先求解子问题,即先确定递增的二元子序列再找第三个元素
  • 对于递增的二元子序列一定有这样一个规律:如果数列中存在递增二元子序列,那必定存在连续的递增二元子序列 (用反证法很容易推导)
  • 所以找第一对递增二元子序列只需要找到两个相邻且递增的元素即可,这里将他们记为 l1l2
  • 如果后续能找到大于当前 l2 的元素就可以结束查找返回 true
  • 如果后续无法找到大于当前 l2 的元素那就无法找到正解,因此需要不断用小于 l2 大于 l1 的数不断更新 l2
  • 另外 l1 的值限制了 l2 的下限也可能导致无法找到正解,因此需要找到更小的递增二元子序列 m1m2,其中 m1 < m2 <= l1m2在实际编码中不声明),如果 m1m2 都存在则更新 l1l2 的值

3、代码

var increasingTriplet = function (nums) {
let l1, l2, m1;
for (let i = 1; i < nums.length; i++) {
if (isNaN(l2) && nums[i - 1] < nums[i]) l1 = nums[i - 1], l2 = nums[i];
if (!isNaN(l2) && nums[i] > l2) return true;
if (!isNaN(l2) && l1 < nums[i]) l2 = nums[i];
if (!isNaN(m1) && nums[i] <= l1) l1 = m1, l2 = nums[i], m1 = undefined;
if (nums[i] < l1) m1 = nums[i];
}
return false;
};

这里实际也用到了贪心思想,代码进一步简化后会跟官解贪心类似,与官解的不同点在于利用了递增二元子序列的连续性


4、执行结果

执行用时: 84 ms 内存消耗: 51 MB


5、解法2-贪心

3个月前写的代码,跟官解的贪心解法类似,先确定前两个元素并初始化为 nums[0]Infinity,然后遍历后续元素并不断用更小的数更新前两个元素,最后如果存在第三个更大的数则表示存在递增三元子序列。

这种方式比较巧妙,用两个变量存储了两组递增二元子序列,中间两组子序列切换的过程实际不影响最终求解


6、代码

var increasingTriplet = function (nums) {
let [min, max] = [nums[0], Infinity];
for (let i = 1; i < nums.length; i++) {
if (nums[i] > max) return true;

if (nums[i] < min) min = nums[i];
if (nums[i] > min && nums[i] < max) max = nums[i];
}
return false;
};

7、执行结果

执行用时: 100 ms 内存消耗: 51.2 MB


这个题目还可以用回溯求解,但是时间复杂度比较高大概率会超时;另外题解区有二分的实现方案,这种方案具有通用性可以解决一类问题,非常值得学习。