跳到主要内容

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

查看所有标签

· 阅读需 7 分钟

1、题干

在一个 106 x 106 的网格中,每个网格上方格的坐标为 (x, y)

现在从源方格 source = [sx, sy] 开始出发,意图赶往目标方格 target = [tx, ty] 。数组 blocked 是封锁的方格列表,其中每个 blocked[i] = [xi, yi] 表示坐标为 (xi, yi) 的方格是禁止通行的。

每次移动,都可以走到网格中在四个方向上相邻的方格,只要该方格 在给出的封锁列表 blocked 上。同时,不允许走出网格。

只有在可以通过一系列的移动从源方格 source 到达目标方格 target 时才返回 true。否则,返回 false

 

示例 1:

输入:blocked = [[0,1],[1,0]], source = [0,0], target = [0,2]
输出:false
解释:
从源方格无法到达目标方格,因为我们无法在网格中移动。
无法向北或者向东移动是因为方格禁止通行。
无法向南或者向西移动是因为不能走出网格。

示例 2:

输入:blocked = [], source = [0,0], target = [999999,999999]
输出:true
解释:
因为没有方格被封锁,所以一定可以到达目标方格。

 

提示:

  • 0 <= blocked.length <= 200
  • blocked[i].length == 2
  • 0 <= xi, yi < 106
  • source.length == target.length == 2
  • 0 <= sx, sy, tx, ty < 106
  • source != target
  • 题目数据保证 sourcetarget 不在封锁列表内

2、解题思路

迷宫的封锁列表blocked会形成围栏,假设从起点和终点分别进行深度遍历,遍历过程中设定步数最大值steps为围栏最大面积,若2条路径行进的步数都超过最大步数或者遍历过程中起点和终点都能访问到则表示起止点连通,则表示起止点连通,反之不连通。


注意点

  • 最开始认为围栏的最大面积是(n2)2(\dfrac{n}{2}) ^ 2,是一块正方形区域。实际官解给出的是n(n1)2\dfrac{n(n-1)}{2},是一块等腰直角三角形区域,其中nblocked数组长度。具体实现时没看官解,限制的是n22\dfrac{n^2}{2},实际上围栏最大面积略偏高不影响结果,因为在不被围的情况下可以走的步数肯定远超这个值。这个最大面积的推导和精确计算可以说有点复杂了,答题时被用例28教育了一番。
  • 遍历过程中如果起止两个点都能访问到则表示起止点连通(用例[[0,3],[1,0],[1,1],[1,2],[1,3]] [0,0] [0,2]
  • 走过的坐标使用哈希表记录,以免重复遍历导致死循环
  • JS若使用递归实现在执行用例28时会出现栈溢出(本地溢出,力扣判题程序未溢出,后面补充了递归DFS代码)

3、代码-非递归

var isEscapePossible = function (blocked, source, target) {
const hash = (x, y) => 1123 * x + y;
const maxSteps = blocked.length ** 2 / 2, dirs = [[0, 1], [0, -1], [1, 0], [-1, 0]];
const blockSet = blocked.reduce((a, c) => a.add(hash(c[0], c[1])), new Set());

function move(s, t, steps, visited) {
const stack = [s];
while (stack.length) {
const [x, y] = stack.pop();
if (x < 0 || x >= 1e6 || y < 0 || y >= 1e6 || blockSet.has(hash(x, y)) || visited.has(hash(x, y))) continue;
if (--steps <= 0 || (x === t[0] && y === t[1])) return true;
visited.add(hash(x, y));
for (const d of dirs) stack.push([x + d[0], y + d[1]]);
}
return false;
}

return move(source, target, maxSteps, new Set()) && move(target, source, maxSteps, new Set());
};

4、执行结果

执行用时: 156 ms 内存消耗: 58.8 MB


5、代码-递归

var isEscapePossible = function (blocked, source, target) {
const hash = (x, y) => 1123 * x + y;
const maxSteps = blocked.length ** 2 / 2, dirs = [[0, 1], [0, -1], [1, 0], [-1, 0]];
const blockSet = blocked.reduce((a, c) => a.add(hash(c[0], c[1])), new Set());

function dfs(x, y, steps, visited, t) {
if (x < 0 || x >= 1e6 || y < 0 || y >= 1e6 || blockSet.has(hash(x, y)) || visited.has(hash(x, y))) return false;
if (--steps <= 0 || (x === t[0] && y === t[1])) return true;
visited.add(hash(x, y));
return dirs.reduce((a, c) => a || dfs(x + c[0], y + c[1], steps, visited, t), false);
}

return dfs(...source, maxSteps, new Set(), target) && dfs(...target, maxSteps, new Set(), source);
};

6、执行结果

执行用时: 120 ms 内存消耗: 47.8 MB

1.png


7、优化点

  • 问题:最开始哈希表保存的坐标是字符串拼接而来,但是字符串消耗空间更多、操作耗时更多,所以执行结果的时长和内存偏高
  • 优化:不用字符串做key,使用质数乘法把坐标转成唯一数字,使用该唯一数字做key
  • 结果:优化后两种代码执行结果用时减半(240ms 到 120ms),内存消耗也大幅减少(57.3 MB 到 47.8 MB),递归DFS代码执行结果破双百

字符串做key比较容易理解和实现且不会冲突,但消耗时间空间较多;而数字做key的好处在于消耗时间空间较少,但麻烦点在于实现时要注意避免key冲突的问题。

· 阅读需 4 分钟

1、题干

LeetCode 设计了一款新式键盘,正在测试其可用性。测试人员将会点击一系列键(总计 n 个),每次一个。

给你一个长度为 n 的字符串 keysPressed ,其中 keysPressed[i] 表示测试序列中第 i 个被按下的键。releaseTimes 是一个升序排列的列表,其中 releaseTimes[i] 表示松开第 i 个键的时间。字符串和数组的 下标都从 0 开始 。第 0 个键在时间为 0 时被按下,接下来每个键都 恰好 在前一个键松开时被按下。

测试人员想要找出按键 持续时间最长 的键。第 i 次按键的持续时间为 releaseTimes[i] - releaseTimes[i - 1] ,第 0 次按键的持续时间为 releaseTimes[0]

注意,测试期间,同一个键可以在不同时刻被多次按下,而每次的持续时间都可能不同。

请返回单次按键 持续时间最长 的键,如果有多个这样的键,则返回 按字母顺序排列最大 的那个键。

 

示例 1:

输入:releaseTimes = [9,29,49,50], keysPressed = "cbcd"
输出:"c"
解释:按键顺序和持续时间如下:
按下 'c' ,持续时间 9(时间 0 按下,时间 9 松开)
按下 'b' ,持续时间 29 - 9 = 20(松开上一个键的时间 9 按下,时间 29 松开)
按下 'c' ,持续时间 49 - 29 = 20(松开上一个键的时间 29 按下,时间 49 松开)
按下 'd' ,持续时间 50 - 49 = 1(松开上一个键的时间 49 按下,时间 50 松开)
按键持续时间最长的键是 'b' 和 'c'(第二次按下时),持续时间都是 20
'c' 按字母顺序排列比 'b' 大,所以答案是 'c'

示例 2:

输入:releaseTimes = [12,23,36,46,62], keysPressed = "spuda"
输出:"a"
解释:按键顺序和持续时间如下:
按下 's' ,持续时间 12
按下 'p' ,持续时间 23 - 12 = 11
按下 'u' ,持续时间 36 - 23 = 13
按下 'd' ,持续时间 46 - 36 = 10
按下 'a' ,持续时间 62 - 46 = 16
按键持续时间最长的键是 'a' ,持续时间 16

 

提示:

  • releaseTimes.length == n
  • keysPressed.length == n
  • 2 <= n <= 1000
  • 1 <= releaseTimes[i] <= 109
  • releaseTimes[i] < releaseTimes[i+1]
  • keysPressed 仅由小写英文字母组成

2、解题思路

根据题意遍历所有按键keysPressed,若按键时长releaseTimes[i] - (releaseTimes[i - 1] || 0)超过以往最大时长maxTime或等于以往最大时长且该按键字典序更大,则将该时长记为最大时长maxTime,该按键记为持续时间最长的键key,最终返回key即可。

3、代码

var slowestKey = function (releaseTimes, keysPressed) {
let maxTime = 0, key = keysPressed[0];
for (let i = 0; i < keysPressed.length; i++) {
const time = releaseTimes[i] - (releaseTimes[i - 1] || 0);
if (time > maxTime || (time === maxTime && keysPressed[i] > key)) maxTime = time, key = keysPressed[i];
}
return key;
};

4、复杂度

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

5、执行结果

image.png

· 阅读需 3 分钟

1、题干

给定一个二叉搜索树的 根节点 root 和一个整数 k , 请判断该二叉搜索树中是否存在两个节点它们的值之和等于 k 。假设二叉搜索树中节点的值均唯一。

 

示例 1:

输入: root = [8,6,10,5,7,9,11], k = 12
输出: true
解释: 节点 5 和节点 7 之和等于 12

示例 2:

输入: root = [8,6,10,5,7,9,11], k = 22
输出: false
解释: 不存在两个节点值之和为 22 的节点

 

提示:

  • 二叉树的节点个数的范围是  [1, 104].
  • -104 <= Node.val <= 104
  • root 为二叉搜索树
  • -105 <= k <= 105

 

注意:本题与主站 653 题相同: https://leetcode-cn.com/problems/two-sum-iv-input-is-a-bst/

2、解法1

DFS遍历+哈希查找:DFS遍历所有节点node,在哈希表set中查找是否存在k - node.val,若存在则返回true否则继续遍历直至结束则返回false

3、代码

var findTarget = function (root, k) {
const set = new Set();
function dfs(node) {
if (!node) return false;
if (set.has(k - node.val)) return true;
set.add(node.val);
return dfs(node.left) || dfs(node.right);
}
return dfs(root);
};

4、复杂度

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

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

5、执行结果

1.png


6、解法2

DFS遍历+DFS二分查找:DFS遍历所有节点node,利用BST特性进行DFS二分查找是否存在k - node.val,若存在则返回true否则继续遍历直至结束则返回false

这里二分搜索的时间复杂度受到二叉树平衡性的影响,最坏情况下二叉树可能退化成链表,导致搜索时间复杂度为O(n)O(n),总体时间复杂度为O(n2)O(n^2)

7、代码

var findTarget = function (root, k) {
function find(node, num) {
if (!node) return false;
if (num === node.val) return true;
return num > node.val ? find(node.right, num) : find(node.left, num);
}

function findSum(node, num) {
if (!node) return false;
return (num !== 2 * node.val && find(root, num - node.val)) || findSum(node.right, num) || findSum(node.left, num);
}

return findSum(root, k);
};

8、复杂度

时间复杂度:平衡二叉搜索树的情况下为O(nlogn)O(nlogn) ,非平衡二叉搜索树的情况下超过O(nlogn)O(nlogn),最差为O(n2)O(n^2)

空间复杂度:O(1)O(1)

9、执行结果

1.png

· 阅读需 3 分钟

1、题干

给你一个下标从 0 开始的一维整数数组 original 和两个整数 m 和  n 。你需要使用 original 中 所有 元素创建一个 m 行 n 列的二维数组。

original 中下标从 0 到 n - 1 (都 包含 )的元素构成二维数组的第一行,下标从 n 到 2 * n - 1 (都 包含 )的元素构成二维数组的第二行,依此类推。

请你根据上述过程返回一个 m x n 的二维数组。如果无法构成这样的二维数组,请你返回一个空的二维数组。

 

示例 1:

输入:original = [1,2,3,4], m = 2, n = 2
输出:[[1,2],[3,4]]
解释:
构造出的二维数组应该包含 2 行 2 列。
original 中第一个 n=2 的部分为 [1,2] ,构成二维数组的第一行。
original 中第二个 n=2 的部分为 [3,4] ,构成二维数组的第二行。

示例 2:

输入:original = [1,2,3], m = 1, n = 3
输出:[[1,2,3]]
解释:
构造出的二维数组应该包含 1 行 3 列。
将 original 中所有三个元素放入第一行中,构成要求的二维数组。

示例 3:

输入:original = [1,2], m = 1, n = 1
输出:[]
解释:
original 中有 2 个元素。
无法将 2 个元素放入到一个 1x1 的二维数组中,所以返回一个空的二维数组。

示例 4:

输入:original = [3], m = 1, n = 2
输出:[]
解释:
original 中只有 1 个元素。
无法将 1 个元素放满一个 1x2 的二维数组,所以返回一个空的二维数组。

 

提示:

  • 1 <= original.length <= 5 * 104
  • 1 <= original[i] <= 105
  • 1 <= m, n <= 4 * 104

2、解题思路

考察数组基本操作

3、代码

var construct2DArray = function (original, m, n) {
return original.length !== m * n ? [] : new Array(m).fill(0).map((v, i) => {
return original.slice(n * i, n * i + n);
});
};

4、执行结果

1.png

· 阅读需 4 分钟

1、题干

Alice 手中有一把牌,她想要重新排列这些牌,分成若干组,使每一组的牌数都是 groupSize ,并且由 groupSize 张连续的牌组成。

给你一个整数数组 hand 其中 hand[i] 是写在第 i 张牌上的数值。如果她可能重新排列这些牌,返回 true ;否则,返回 false

 

    示例 1:

    输入:hand = [1,2,3,6,2,3,4,7,8], groupSize = 3
    输出:true
    解释:Alice 手中的牌可以被重新排列为 [1,2,3],[2,3,4],[6,7,8]

    示例 2:

    输入:hand = [1,2,3,4,5], groupSize = 4
    输出:false
    解释:Alice 手中的牌无法被重新排列成几个大小为 4 的组。

     

    提示:

    • 1 <= hand.length <= 104
    • 0 <= hand[i] <= 109
    • 1 <= groupSize <= hand.length

     

    注意:此题目与 1296 重复:https://leetcode-cn.com/problems/divide-array-in-sets-of-k-consecutive-numbers/

    2、解法1

    先对原数组升序排序,然后进行双指针嵌套遍历,外层遍历找顺子起点,内层遍历找顺子后续元素,被找到的元素置为-1即寻找其他顺子元素时直接略过,若无法找到完整顺子则返回false

    3、代码

    var isNStraightHand = function (hand, groupSize) {
    hand.sort((a, b) => a - b);
    for (let i = 0; i < hand.length; i++) {
    if (hand[i] < 0) continue;
    let count = 0;
    for (let j = i + 1; j < hand.length && count !== groupSize - 1; j++) {
    if (hand[j] - hand[i] === count + 1) count++, hand[j] = -1;
    }
    if (count !== groupSize - 1) return false;
    }
    return true;
    };

    代码逻辑简要说明

    • 外层遍历:从下标i=0开始遍历数组内的元素,如果当前元素不为负数则作为顺子起点
    • 内层遍历:从下标j=i+1(即顺子起点的下一个元素)开始遍历数组内的元素,寻找顺子后续元素
      • 对已找到的元素计数,计数变量记为count
      • 若当前元素与顺子起点的差值等于count+1,则说明该元素是顺子的后续元素
    • 内层遍历结束,若找到的元素数量不等于groupSize - 1,说明顺子不存在应返回false

    4、执行结果

    1.png

    5、解法2

    先对原数组升序排序,然后对数组内数字进行哈希表计数(数字作key数量作value),然后遍历数组所有数字,以数量大于1的数字作为顺子起点,寻找顺子后续元素,若无法找到完整顺子则返回false

    6、代码

    var isNStraightHand = function (hand, groupSize) {
    hand.sort((a, b) => a - b);
    const map = hand.reduce((acc, n) => acc.set(n, (acc.get(n) || 0) + 1), new Map());
    for (const n of hand) {
    if (!map.get(n)) continue;
    for (let i = 0; i < groupSize; i++) {
    if (!map.get(n + i)) return false;
    else map.set(n + i, map.get(n + i) - 1);
    }
    }
    return true;
    };

    7、执行结果

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

    · 阅读需 2 分钟

    1、题干

    给你一个 下标从 0 开始 的整数数组 nums ,返回满足下述条件的 不同 四元组 (a, b, c, d)数目

    • nums[a] + nums[b] + nums[c] == nums[d] ,且
    • a < b < c < d

     

    示例 1:

    输入:nums = [1,2,3,6]
    输出:1
    解释:满足要求的唯一一个四元组是 (0, 1, 2, 3) 因为 1 + 2 + 3 == 6 。

    示例 2:

    输入:nums = [3,3,6,4,5]
    输出:0
    解释:[3,3,6,4,5] 中不存在满足要求的四元组。

    示例 3:

    输入:nums = [1,1,1,3,5]
    输出:4
    解释:满足要求的 4 个四元组如下:
    - (0, 1, 2, 3): 1 + 1 + 1 == 3
    - (0, 1, 3, 4): 1 + 1 + 3 == 5
    - (0, 2, 3, 4): 1 + 1 + 3 == 5
    - (1, 2, 3, 4): 1 + 1 + 3 == 5

     

    提示:

    • 4 <= nums.length <= 50
    • 1 <= nums[i] <= 100

    2、解题思路

    等式可转换成nums[a] + nums[b] == nums[d] - nums[c],先用哈希表存储一边的结果,再遍历计算另一边的结果是否存在于哈希表。

    3、代码

    var countQuadruplets = function (nums) {
    const dict = new Map();
    for (let i = 0; i < nums.length; i++) {
    for (let j = i + 1; j < nums.length; j++) {
    if (!dict.has(nums[i] + nums[j])) dict.set(nums[i] + nums[j], []);
    dict.get(nums[i] + nums[j]).push(j);
    }
    }

    let res = 0;
    for (let i = 2; i < nums.length; i++) {
    for (let j = i + 1; j < nums.length; j++) {
    if (!dict.has(nums[j] - nums[i])) continue;
    res += dict.get(nums[j] - nums[i]).reduce((acc, cur) => cur < i ? acc + 1 : acc, 0);
    }
    }

    return res;
    };

    4、执行结果

    1.png

    · 阅读需 4 分钟

    1、题干

    给你一个 不含重复 单词的字符串数组 words ,请你找出并返回 words 中的所有 连接词

    连接词 定义为:一个完全由给定数组中的至少两个较短单词(不一定是不同的两个单词)组成的字符串。

     

    示例 1:

    输入:words = ["cat","cats","catsdogcats","dog","dogcatsdog","hippopotamuses","rat","ratcatdogcat"]
    输出:["catsdogcats","dogcatsdog","ratcatdogcat"]
    解释:"catsdogcats" 由 "cats", "dog" 和 "cats" 组成;
    "dogcatsdog" 由 "dog", "cats" 和 "dog" 组成;
    "ratcatdogcat" 由 "rat", "cat", "dog" 和 "cat" 组成。

    示例 2:

    输入:words = ["cat","dog","catdog"]
    输出:["catdog"]

     

    提示:

    • 1 <= words.length <= 104
    • 1 <= words[i].length <= 30
    • words[i] 仅由小写英文字母组成。 
    • words 中的所有字符串都是 唯一 的。
    • 1 <= sum(words[i].length) <= 105

    常规解法用字典树处理,详情可以看官解。 补充:这个解法有漏洞,调整用例肯定还会超时

    2、解题思路

    这里采用一种非常规解法,创建哈希集合set并加入所有单词,然后遍历所有单词并判断该单词是否由set中的元素组成,判断过程是利用递归对每个单词进行分段,如果set包含所有分段后的子串则说明其是连接词。

    然后写出了下面这段代码,结果通过用例43/44,最后一个用例超时。主要是因为这个用例的特殊性,导致递归检查函数的执行次数暴涨。

    var findAllConcatenatedWordsInADict = function (words) {
    const set = new Set(words);
    if (set.has("")) set.delete("");

    function dfs(word, start) {
    let s = '';
    for (let i = start; i < word.length; i++) {
    s += word[i];
    if (set.has(s) && dfs(word, i + 1)) return true;
    }
    return set.has(s) && start;
    }

    return words.reduce((acc, cur) => (dfs(cur, 0) && acc.push(cur), acc), []);
    };

    由于该用例的特殊性,尝试添加预检策略绕过一些特殊用例:

    • 1、字符串数组按长度升序排序,由于长单词肯定由短单词组成,意味着如果前面的短单词无法组合出当前单词的特征,就说明其不是连接词
    • 2、预检函数,取出单词中所有连续的两个字符(最短连接子串),检查更短的单词是否包含或能组合出这个最短连接子串,如果为否则说明其不是连接词

    3、完整代码

    var findAllConcatenatedWordsInADict = function (words) {
    words.sort((a, b) => a.length - b.length);
    const set = new Set(words);
    if (set.has('')) set.delete('');

    function dfs(word, start) {
    let s = '';
    for (let i = start; i < word.length; i++) {
    s += word[i];
    if (set.has(s) && dfs(word, i + 1)) return true;
    }
    return set.has(s) && start;
    }

    const headSet = new Set(), tailSet = new Set(), compSet = new Set();
    function preCheck(word) {
    let found = true, comps = [];
    for (let i = 0; i < word.length - 1; i++) {
    const s = word[i] + word[i + 1];
    if (!compSet.has(s) && !(tailSet.has(s[0]) && headSet.has(s[1]))) found = false;
    comps.push(s);
    }
    headSet.add(word[0]), tailSet.add(word[word.length - 1]);
    for (const c of comps) compSet.add(c);
    return found;
    }

    return words.reduce((acc, cur) => {
    if (!preCheck(cur)) return acc;
    if (dfs(cur, 0)) acc.push(cur);
    return acc;
    }, []);
    };

    4、执行结果

    1.png

    · 阅读需 5 分钟

    1、题干

    在社交媒体网站上有 n 个用户。给你一个整数数组 ages ,其中 ages[i] 是第 i 个用户的年龄。

    如果下述任意一个条件为真,那么用户 x 将不会向用户 yx != y)发送好友请求:

    • ages[y] <= 0.5 * ages[x] + 7
    • ages[y] > ages[x]
    • ages[y] > 100 && ages[x] < 100

    否则,x 将会向 y 发送一条好友请求。

    注意,如果 xy 发送一条好友请求,y 不必也向 x 发送一条好友请求。另外,用户不会向自己发送好友请求。

    返回在该社交媒体网站上产生的好友请求总数。

     

    示例 1:

    输入:ages = [16,16]
    输出:2
    解释:2 人互发好友请求。

    示例 2:

    输入:ages = [16,17,18]
    输出:2
    解释:产生的好友请求为 17 -> 16 ,18 -> 17 。

    示例 3:

    输入:ages = [20,30,100,110,120]
    输出:3
    解释:产生的好友请求为 110 -> 100 ,120 -> 110 ,120 -> 100 。

     

    提示:

    • n == ages.length
    • 1 <= n <= 2 * 104
    • 1 <= ages[i] <= 120

    2、题目分析

    • 交友年龄限制的限制条件转换后,可以理解为:对于任意年龄ages[x],交友范围的区间是(0.5 * ages[x] + 7,ages[x]]
    • 由交友范围还可推断出,年龄15岁以下的用户交友范围区间是空集,因此后续编码时可以直接排除15岁以下的数据。

    3、解法1-计数排序

    对年龄使用计数排序,遍历所有年龄age,累计并更新交友范围区间(0.5 * age + 7,age]内的人数preCount,然后对最终结果res累加当前年龄的交友请求数(preCount - 1) * counts[age]

    4、代码

    var numFriendRequests = function (ages) {
    const counts = new Array(121).fill(0);
    for (const age of ages) if (age > 14) counts[age]++;
    let preCount = 0, res = 0;
    for (let age = 15; age < counts.length; age++) {
    if (age % 2 === 0) preCount -= counts[((age - 1) / 2 + 8) >> 0];
    preCount += counts[age];
    res += (preCount - 1) * counts[age];
    }
    return res;
    };

    代码简要说明

    • if (age % 2 === 0) preCount -= counts[((age - 1) / 2 + 8) >> 0]
      • 对于任意年龄ageage-1而言,交友年龄下限分别为counts[(age / 2 + 8) >> 0]counts[((age - 1) / 2 + 8) >> 0]
      • age为奇数时这两个交友年龄下限相同,age为偶数时交友年龄下限比age-1大1
      • 因此age为偶数时,需要减去age-1的交友下限人数counts[((age - 1) / 2 + 8) >> 0]
    • preCount += counts[age]
      • 加上当前年龄用户的数量
    • res += (preCount - 1) * counts[age]
      • 当前年龄人数为counts[age],他们会给交友范围内除自己外的所有人preCount - 1发请求
      • 因此有res += (preCount - 1) * counts[age]

    5、执行结果

    1.png


    6、解法2-二分查找

    先对所有年龄ages进行降序排序,然后遍历所有年龄,对当前年龄的交友年龄下限进行二分查找,累计所有交友请求数。其中,遇到相同年龄时直接跳过并累计其数量same,当年龄不再相同时对最终结果累加same * (same - 1)

    该方法存在较多边界条件,处理不当很容易报错,不建议使用该方法实现

    7、代码

    var numFriendRequests = function (ages) {
    ages.sort((a, b) => b - a);
    let same = 1;
    return ages.reduce((acc, cur, i) => {
    if (cur === ages[i + 1]) same++;
    if (cur <= 14 || (cur === ages[i + 1] && i !== ages.length - 1)) return acc;

    let l = i + 1, r = ages.length - 1;
    while (l < r) {
    const m = Math.floor((l + r) / 2);
    ages[m] <= cur / 2 + 7 ? (r = m - 1) : (l = m + 1);
    }

    acc += same * (same - 1 + Math.max(0, r - i - (ages[r] > cur / 2 + 7 ? 0 : 1)));
    return (same = 1), acc;
    }, 0);
    };

    8、执行结果

    执行用时:124 ms 内存消耗:47.3 MB

    · 阅读需 6 分钟

    1、题干

    有一棵特殊的苹果树,一连 n 天,每天都可以长出若干个苹果。在第 i 天,树上会长出 apples[i] 个苹果,这些苹果将会在 days[i] 天后(也就是说,第 i + days[i] 天时)腐烂,变得无法食用。也可能有那么几天,树上不会长出新的苹果,此时用 apples[i] == 0days[i] == 0 表示。

    你打算每天 最多 吃一个苹果来保证营养均衡。注意,你可以在这 n 天之后继续吃苹果。

    给你两个长度为 n 的整数数组 daysapples ,返回你可以吃掉的苹果的最大数目

     

    示例 1:

    输入:apples = [1,2,3,5,2], days = [3,2,1,4,2]
    输出:7
    解释:你可以吃掉 7 个苹果:
    - 第一天,你吃掉第一天长出来的苹果。
    - 第二天,你吃掉一个第二天长出来的苹果。
    - 第三天,你吃掉一个第二天长出来的苹果。过了这一天,第三天长出来的苹果就已经腐烂了。
    - 第四天到第七天,你吃的都是第四天长出来的苹果。

    示例 2:

    输入:apples = [3,0,0,0,0,2], days = [3,0,0,0,0,2]
    输出:5
    解释:你可以吃掉 5 个苹果:
    - 第一天到第三天,你吃的都是第一天长出来的苹果。
    - 第四天和第五天不吃苹果。
    - 第六天和第七天,你吃的都是第六天长出来的苹果。

     

    提示:

    • apples.length == n
    • days.length == n
    • 1 <= n <= 2 * 104
    • 0 <= apples[i], days[i] <= 2 * 104
    • 只有在 apples[i] = 0 时,days[i] = 0 才成立

    又是优先队列,JS没有优先队列,又是被折磨的一天。。。 这道题跟课程表 III类似,可以看看我之前的题解,也是计数排序

    2、执行结果

    1.png

    3、解题思路

    • 总体思路:使用贪心策略,在尚未腐烂的苹果中优先选择保质期最小的苹果。
    • 关键思路:基于计数排序求保质期最小的苹果,用数组存储苹果数量和保质期,数组的下标表示保质期、值表示该日期对应的苹果数量;找保质期最小的苹果只需要以上一个最小保质期为起点,遍历数组找到下标最小且值不为0的元素。
    • 时间复杂度:由于遍历的起点是上一个最小的保质期,因此总体实现出来的时间复杂度是 O(n)O(n),其中 nn 是最大保质期。

    下面说明代码实现细节:

    • 计数排序容器记为freshArr,其下标表示保质期、值表示该保质期之前的苹果数量
    • 最小保质期记为minDay,用于记录有苹果剩余的最小保质期
    • 可以吃掉苹果的最大数量记为res,即最终结果
    • 接下来以最大保质期为限制逐天遍历,天数记为i
    • 在第i天时,如果i没有超过n且过期天数大于0(i < days.length && days[i] > 0),进行以下操作
      • 在计数排序容器freshArr中按保质期i + days[i] - 1累加苹果数量
      • 更新最小保质期minDay = Math.min(minDay, i + days[i] - 1)
    • 接下来只要找到有苹果剩余的最小保质期即可
      • 先确保最小保质期在今天之后,即minDay = Math.max(minDay, i)
      • 然后在计数排序容器freshArr中遍历,找到有苹果剩余的最小保质期
    • 接下来更新最终结果和计数排序容器freshArr中的苹果数量
      • 注意更新之前确定该保质期有苹果剩余,即if (freshArr[minDay]) res++, freshArr[minDay]--
    • 最后就得到可以吃掉苹果的最大数量res

    4、代码

    var eatenApples = function (apples, days) {
    let res = 0, minDay = days[0] - 1;
    const freshArr = new Array(days.length).fill(0);
    for (let i = 0; i < freshArr.length; i++) {
    if (i < days.length && days[i] > 0) {
    freshArr[i + days[i] - 1] = (freshArr[i + days[i] - 1] || 0) + apples[i];
    minDay = Math.min(minDay, i + days[i] - 1);
    }
    minDay = Math.max(minDay, i);
    while (minDay < freshArr.length && !freshArr[minDay]) minDay++;
    if (freshArr[minDay]) res++, freshArr[minDay]--;
    }
    return res;
    };

    · 阅读需 3 分钟

    1、题干

    给你一个大小为 m x n 的矩阵 board 表示甲板,其中,每个单元格可以是一艘战舰 'X' 或者是一个空位 '.' ,返回在甲板 board 上放置的 战舰 的数量。

    战舰 只能水平或者垂直放置在 board 上。换句话说,战舰只能按 1 x k1 行,k 列)或 k x 1k 行,1 列)的形状建造,其中 k 可以是任意大小。两艘战舰之间至少有一个水平或垂直的空位分隔 (即没有相邻的战舰)。

     

    示例 1:

    输入:board = [["X",".",".","X"],[".",".",".","X"],[".",".",".","X"]]
    输出:2

    示例 2:

    输入:board = [["."]]
    输出:0

     

    提示:

    • m == board.length
    • n == board[i].length
    • 1 <= m, n <= 200
    • board[i][j]'.''X'

     

    进阶:你可以实现一次扫描算法,并只使用 O(1) 额外空间,并且不修改 board 的值来解决这个问题吗?

    执行结果

    1.png

    解法1-寻找船头

    由于战舰只能水平或者垂直放置在board上,那就意味着船头位置的左边和上边必然是空位。因此只要双循环遍历整个board矩阵,遇到船头则战舰数量加一。

    同理可以寻找船尾,船尾的右边和下边必然是空位。

    代码

    一行强迫症写法

    var countBattleships = function (board) {
    return board.reduce((acc, cur, i) => acc + cur.filter((c, j) => c === 'X' && board[i][j - 1] !== 'X' && (!i || board[i - 1][j] !== 'X')).length, 0);
    };

    常规写法

    var countBattleships = function (board) {
    let res = 0;

    for (let i = 0; i < board.length; i++) {
    for (let j = 0; j < board[0].length; j++) {
    if (board[i][j] == 'X' && board[i][j - 1] !== 'X' && (!i || board[i - 1][j] !== 'X')) res++;
    }
    }

    return res;
    };

    解法2-DFS

    双循环遍历整个board矩阵,遇到战舰则战舰数量加一,然后深度遍历战舰所占位置,把遍历过的战舰位置修改成空位避免重复遍历。

    代码

    var countBattleships = function (board) {
    let res = 0;
    function dfs(i, j) {
    if (i < 0 || j < 0 || i >= board.length || j >= board[0].length || board[i][j] !== 'X') return;

    board[i][j] = '.';
    dfs(i + 1, j);
    dfs(i - 1, j);
    dfs(i, j + 1);
    dfs(i, j - 1);
    }

    for (let i = 0; i < board.length; i++) {
    for (let j = 0; j < board[0].length; j++) {
    if (board[i][j] === 'X') dfs(i, j), res++;
    }
    }

    return res;
    };