赞
踩
数组只是一种数据结构,通常结合其他算法场景出现。这里总结几类在LeetCode刷题时,针对数组相关的场景题,可以使用以下技巧和方法:
双指针法:
- 快慢指针用于解决数组中的有序问题,如移除重复项、找出唯一元素等。
- 左右指针用于解决数组中的对撞问题,如两数之和、接雨水等。
排序:对数组进行排序可以简化很多问题,如对数组进行排序后,可以更容易地解决部分排序问题。
哈希表:使用对象字面量或Map结构存储键值对,可以快速查找数组中的元素,常用于解决两数之和、最长连续序列等问题。
滑动窗口:对于找出连续子数组的问题,如连续子数组的最大和、最小覆盖子串等,滑动窗口是一个有效的技巧。
动态规划:对于需要考虑历史状态的问题,如最大子序和、最长递增子序列等,动态规划可以提供解决方案。
分治法:将大问题分解为小问题,分别解决后再合并结果,适用于如归并排序等场景。
贪心算法:在每一步选择中都采取在当前看来最好的选择,例如买卖股票的最佳时机等问题。
哈希表相关参考我的博客:leetcode刷题(javaScript)——字典哈希表相关场景题总结-CSDN博客
动态规划相关参考我的博客
leetcode刷题(javaScript)——动态规划相关场景题总结
递归相关参考我的博客
双指针法在LeetCode刷题中经常被用到,特别是在数组和字符串相关的问题中。双指针法在解决数组和字符串相关问题时,通常用于处理两个方向的遍历、查找、移动等操作。根据问题的特点,选择合适的双指针策略,可以有效地提高问题的解决效率。以下是一些常见的应用场景:
快慢指针:
- 移除数组中的重复元素,保留唯一元素。
- 寻找数组中的某个特定值或范围。
左右指针:
- 在有序数组中查找两数之和、三数之和等。
- 滑动窗口问题,如最小覆盖子串、最长无重复字符的子串等
给你一个 非严格递增排列 的数组 nums
,请你 原地 删除重复出现的元素,使每个元素 只出现一次 ,返回删除后数组的新长度。元素的 相对顺序 应该保持 一致 。然后返回 nums
中唯一元素的个数。
考虑 nums
的唯一元素的数量为 k
,你需要做以下事情确保你的题解可以被通过:
nums
,使 nums
的前 k
个元素包含唯一元素,并按照它们最初在 nums
中出现的顺序排列。nums
的其余元素与 nums
的大小不重要。k
。思路:用快慢指针,慢指针pre在数组左侧,记录非重复值的index,快指针是循环遍历的i变量。快指针正常遍历,每走一个位置都和pre进行比较,如果快指针的元素和pre相同,pre不变;如果快指针和pre不同,pre向右走一个,通时保存快指针的值。一次循环结束,慢指针pre及之前的元素都是唯一的了。
- /**
- * @param {number[]} nums
- * @return {number}
- */
- var removeDuplicates = function(nums) {
- let pre=0;//左指针
- for(let i=1;i<nums.length;i++){//循环充当右指针
- if(nums[i]!=nums[pre]){//当右指针i找到唯一值,左指针向后移动,并记录右指针的值
- pre++;
- nums[pre]=nums[i];
- }
- }
- return pre+1;//pre最终的index+1是数组长度
- };
给定一个数组 nums
,编写一个函数将所有 0
移动到数组的末尾,同时保持非零元素的相对顺序。
请注意 ,必须在不复制数组的情况下原地对数组进行操作。
思路:用快慢指针,慢指针记录非0的最后一个index,快指针为i循环数组。当快指针走完时,如果快慢指针中间还有元素,将其置为0即可。如果快指针指向的元素为0,慢指针不更新,否则慢指针更新为快指针指向的值。
- /**
- * @param {number[]} nums
- * @return {void} Do not return anything, modify nums in-place instead.
- */
- var moveZeroes = function (nums) {
- //快慢指针,慢指针指向非0值,快指针遍历数组。最后快指针停止的时候,将快慢指针部分填充0
- let slow = 0;
- for (let i = 0; i < nums.length; i++) {
- if (nums[i] != 0) {
- nums[slow++] = nums[i];
- }
- }
- //如果没有0元素,slow应该等于nums.length。因为slow每次执行完都加1
- if (slow <= nums.length - 1) {
- for (let j = slow; j < nums.length; j++) {
- nums[j] = 0;
- }
- }
- };
给你一个按 非递减顺序 排序的整数数组 nums
,返回 每个数字的平方 组成的新数组,要求也按 非递减顺序 排序。
示例 1:
输入:nums = [-4,-1,0,3,10] 输出:[0,1,9,16,100] 解释:平方后,数组变为 [16,1,0,9,100] 排序后,数组变为 [0,1,9,16,100]
示例 2:
输入:nums = [-7,-3,2,3,11] 输出:[4,9,9,49,121]
提示:
1 <= nums.length <= 104
-104 <= nums[i] <= 104
nums
已按 非递减顺序 排序进阶:
O(n)
的算法解决本问题思路:这是一道双指针的技巧题了,不容易想出来。因为数组中还有负数,负数的平方和有可能大于正数的平方和,所以直接用平方和是无法排序的。
这里假设结果数组是从大到小获取的,也就是数组从头插法建立结果数组。那么每次从剩余数组中取出较大的元素,可以使用双指针来完成,根据左右指针的平方和取一个较大值,然后取值的指针走一步。
- /**
- * @param {number[]} nums
- * @return {number[]}
- */
- var sortedSquares = function (nums) {
- let result = [];
- let left = 0,
- right = nums.length - 1;
- while (left <= right) {
- const leftSquare = nums[left] * nums[left];
- const rightSquare = nums[right] * nums[right];
- if (leftSquare > rightSquare) {//左右指针选一个较大值
- result.unshift(leftSquare);
- left++;
- } else {
- result.unshift(rightSquare);
- right--;
- }
- }
- return result;
- };
给定一个长度为 n
的整数数组 height
。有 n
条垂线,第 i
条线的两个端点是 (i, 0)
和 (i, height[i])
。
找出其中的两条线,使得它们与 x
轴共同构成的容器可以容纳最多的水。
返回容器可以储存的最大水量。
说明:你不能倾斜容器。
思路:利用左右双指针,向中间靠拢计算水容量。如何得到过程中较大值,每次移动的时候移动那个高度较小的指针,这样指针就会去尝试找大的高度,而让较高的高度保留。计算过程中水容量的较大值max。最后返回max
- /**
- * @param {number[]} height
- * @return {number}
- */
- var maxArea = function (height) {
- let left = 0, right = height.length - 1;
- let cur, max = 0;
- while (left < right) {//左右指针,每次移动两者高度的最小值,保证获得较大的容量
- let leftVal = height[left];
- let rightVal = height[right];
- cur = Math.min(leftVal, rightVal) * (right - left);
- max = Math.max(cur, max);
- if (leftVal < rightVal) {//如果左侧高度小,移动左指针
- left++;
- } else {//如果相等或右侧高度小移动右指针
- right--;
- }
- }//遍历结束
- return max;
- };
给你一个整数数组 nums
,判断是否存在三元组 [nums[i], nums[j], nums[k]]
满足 i != j
、i != k
且 j != k
,同时还满足 nums[i] + nums[j] + nums[k] == 0
。请
你返回所有和为 0
且不重复的三元组。
注意:答案中不可以包含重复的三元组。
思路:这题老难了。看了解析才做出来。主体思路是先排序。然后对这个有序数组求解。
将三数之和拆成x+y+z。由于三数之和为0,那么必然有一个是非正数,即负数或0。遍历x,然后利用双指针从x后面的元素找y和z,这里不找两数之后,还是判断x+y+z是否为0,y从x后面第一个元素开始,z从数组最后一项开始。如果sum>0,说明z大了,z向左移,如果sum<0,说明y小了,y向右移。
那么难点来了,如何去重,这里x可能有重,y和z也有可能重复。面对这样一个数组,你如何跳过重复的x,y,z?
nums =[-2,0,0,2,2]
答案就是,移动y和z的时候,要移动下一个不相等的。
那x重复呢?
那你就跳过x,找不重复的。
- /**
- * @param {number[]} nums
- * @return {number[][]}
- */
- var threeSum = function (nums) {
- //将三数之和拆成x+y+z,假设x是负数,那么y+z一定是正数;相同的x只遍历一次
- nums = nums.sort((a, b) => a - b);//从小到大排序
- let res = [];//存放最终的数组
- let xIndex = 0, yIndex, zIndex;
- while (nums[xIndex] <= 0) {//只遍历负数和0,每次固定x,找y和z
- if (xIndex > 0 && nums[xIndex] === nums[xIndex - 1]) {//x去重,不要重复处理相同的x
- xIndex++;
- continue;
- }
- yIndex = xIndex + 1;
- zIndex = nums.length - 1;
- while (yIndex < zIndex) {
- let sum = nums[xIndex] + nums[yIndex] + nums[zIndex];//
- if (sum > 0) {
- zIndex--;
- } else if (sum < 0) {
- yIndex++;
- } else {//找到了目标x,y,z
- res.push([nums[xIndex], nums[yIndex], nums[zIndex]]);
- while (nums[yIndex] == nums[yIndex + 1]) {//y和z去重,否则nums =[-2,0,0,2,2]过不了
- yIndex++;
- }
- while (nums[zIndex] === nums[zIndex - 1]) {
- zIndex--;
- }
- yIndex++;
- zIndex--;
- }
- }
- xIndex++;
- }
- return res;
- };
给你一个整数数组 nums
,有一个大小为 k
的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k
个数字。滑动窗口每次只向右移动一位。
返回 滑动窗口中的最大值 。
思路:这是个困难题。但是诈一块思路很简单,就是用一个队列先进先出,每次求队列的最大值。但是题目没说,从无序队列求最大值会导致超时,n*k的时间复杂度。所以思路就变成了维护一个相对有序的队列。
怎么维护呢,首先,滑动窗口右侧的要加入的元素无论大小都要加入队列里,此时如果队列没有元素直接加入。如果队列有元素,当前加入的如果比队尾大,循环删除队尾,整个队列保持降序的状态。如果此时队头还有老的元素,判断队头的存放的数组下标是否不在当前的窗口中,如果不在,删除队头。每次队头最少删一个,最多删k-1个。在窗口移动的过程中将更新后的队头最大值存入结果数组中
- /**
- * @param {number[]} nums
- * @param {number} k
- * @return {number[]}
- */
- var maxSlidingWindow = function (nums, k) {
- let queue = [];//队头维护最大值,nums[i]推进去的时候如果比队尾大,循环删除小的队尾,推入后。判断队头是否需要移除
- let res = [];
- for (let i = 0; i < nums.length; i++) {
- while (queue.length && nums[i] >= nums[queue[queue.length - 1]]) {
- queue.pop();//当前入队的元素比队列尾部的大,删除队尾
- }
- queue.push(i);//当前下标入队
- if (queue[0] <= i - k) {//处理队头,如果队头不在窗口里,删除
- queue.shift();
- }
- if (i >= k - 1) {
- res.push(nums[queue[0]])
- }
- }
- return res;
- };
给定一个整数数组 nums
,将数组中的元素向右轮转 k
个位置,其中 k
是非负数。
思路:有多种方法可以实现数组向右旋转 k 个位置,下面列举其中两种常见的方法:
使用额外数组:
- 创建一个额外的数组,将原数组中从倒数第 k 个元素到最后一个元素复制到额外数组的开头。
- 将原数组中前 len-k 个元素复制到额外数组的剩余位置。
- 将额外数组复制回原数组。
- 时间复杂度为 O(n),空间复杂度为 O(n)。
使用翻转:
- 翻转整个数组。
- 翻转从索引 0 到 k-1 的子数组。
- 翻转从索引 k 到末尾的子数组。
- 时间复杂度为 O(n),空间复杂度为 O(1)。
这两种方法都可以实现数组向右旋转 k 个位置,但使用翻转的方法空间复杂度更低。因此,推荐使用翻转的方法来实现数组旋转。
这里注意k的值可能超过nums数组的长度,因此,需要对k进行nums取模。避免翻转多次。为了O(1)的空间复杂度,使用三次翻转,使得数组原地被修改,不引入额外空间。在js中我们可以使用数组方式,交换两个元素值。
- /**
- * @param {number[]} nums
- * @param {number} k
- * @return {void} Do not return anything, modify nums in-place instead.
- */
- var rotate = function (nums, k) {
- // 获取数组的长度。
- let len = nums.length;
- // 如果 k 大于数组长度,使用取模运算调整 k 的值。处理 k 大于数组元素数量时的情形。
- k = k % len;
- // 翻转整个数组,从索引 0 到 len-1。
- reverse(nums, 0, len - 1);
- reverse(nums, 0, k - 1);// 翻转数组的前 k 个元素。
- reverse(nums, k, len - 1);// 翻转从 k 到数组末尾的元素。
- };
- // 定义一个翻转函数,接收一个数组、一个开始索引和一个结束索引。
- function reverse(nums, start, end) {
- // 从 start 索引遍历到 start 和 end 索引中间的位置。
- // 中间位置使用 floor 函数计算,以正确处理奇数长度的数组。
- for (let i = start; i <= Math.floor((end + start) / 2); i++) {
- // 交换当前元素与从 end 索引开始对应的元素。这在原地有效地翻转了数组。
- [nums[i], nums[end - i + start]] = [nums[end - i + start], nums[i]];
- }
- }
给你一个正整数 n
,生成一个包含 1
到 n2
所有元素,且元素按顺时针顺序螺旋排列的 n x n
正方形矩阵 matrix
。
示例 1:
输入:n = 3 输出:[[1,2,3],[8,9,4],[7,6,5]]
示例 2:
输入:n = 1 输出:[[1]]
提示:
1 <= n <= 20
思路:生成一个按顺时针顺序螺旋排列的n x n正方形矩阵,我们可以模拟顺时针填充矩阵的过程。具体步骤如下:
初始化一个空的n x n矩阵
matrix
,并定义四个变量top
、bottom
、left
、``right`分别表示当前填充区域的上、下、左、右边界。定义一个变量
num
表示当前要填充的数字,初始值为1。不断循环填充矩阵,直到
num
达到n*n
为止。在每一轮循环中,按照顺时针的顺序填充矩阵的上、右、下、左边界,同时更新边界值和num
的值。最终返回填充完成的矩阵
matrix
。
- /**
- * @param {number} n
- * @return {number[][]}
- */
- function generateMatrix(n) {
- const matrix = Array.from(Array(n), () => Array(n).fill(0));
- let top = 0,
- bottom = n - 1,
- left = 0,
- right = n - 1;//表示当前填充区域的上、下、左、右边界。
- let num = 1;
- while (num <= n * n) {
- for (let i = left; i <= right; i++) {
- matrix[top][i] = num++;
- }
- top++;
- for (let i = top; i <= bottom; i++) {
- matrix[i][right] = num++;
- }
- right--;
- for (let i = right; i >= left; i--) {
- matrix[bottom][i] = num++;
- }
- bottom--;
- for (let i = bottom; i >= top; i--) {
- matrix[i][left]=num++;
- }
- left++;
- }
- return matrix;
- }
给你一个非负整数数组 nums
,你最初位于数组的 第一个下标 。数组中的每个元素代表你在该位置可以跳跃的最大长度。
判断你是否能够到达最后一个下标,如果可以,返回 true
;否则,返回 false
。
思路:通常我们在看到这种跳跃的题目,很容易想到动态规划,动态规划适用于爬楼梯这种,根据当前状态,有几种走法,根据走法从前面已有的信息推导当前状态。在这道题里,可以定义一个dp数组,dp[i]表示从0到i的任意节点能够跳到的最大数组下标。用dp数组记录一个最大值是不是大材小用?我们是不是可以直接使用一个max变量,每个index都重新计算一下。如果第i个元素是0,并且这个max没有超过i,是不是没法避免这个0值。后续的元素都到达不了。当然,如果这个0是数组最后一项就不用考虑。
- /**
- * @param {number[]} nums
- * @return {boolean}
- */
- var canJump = function (nums) {
- let maxToIndex = 0;//maxToIndex表示0-i之间的下标能到达最远的下标
- for (let i = 0; i < nums.length; i++) {
- if (nums[i] == 0 && maxToIndex <= i && i<nums.length-1) {//如果nums[i]是0,并且当前max没有超过i,并且不是最后一项
- return false;//返回false,结束
- } else {
- maxToIndex = Math.max(maxToIndex, i + nums[i]);//否则,更新max
- }
- }
- return true;
- };
给定一个长度为 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]
。
思路:我们看到,后一个的求解和前面的跳转是有关系的。考虑用动态规划做。动态规划的难点在于你怎么抽离一个公共的描述dp[i]的行为。在这个题里可以看出来,我们需要记录到达下标i能够跳跃的最少步数,那么dp[i]就描述为从节点0到达节点i经过的最少跳跃次数。那么难点来了,我们更新哪个节点呢?
假设除了节点0,后面的节点dp都初始化无限大。
每次访问节点i的时候,是不是可以更新节点i到i+nums[i]之间的跳转次数。
取dp[i]+1和节点本身的dp[j]值的最小值。因为dp[j]可能被计算过。
- /**
- * @param {number[]} nums
- * @return {number}
- */
- var jump = function (nums) {
- let dp = Array(nums.length).fill(Infinity);//使用动态规划dp[i]定义为从0跳到i最少跳跃步数
- dp[0] = 0;//初始化dp
- for (let i = 0; i < nums.length; i++) {//外循环i
- let jumpIndex = i + nums[i];//拿到覆盖范围
- for (let j = i + 1; j <= jumpIndex; j++) {//对覆盖范围内所有点重新计算dp
- dp[j] = Math.min(dp[i] + 1, dp[j]);//取新计算的最小值赋给当前dp[j]
- }
- }
- return dp[nums.length - 1];//返回最后一个元素,即为目标
- };
实现RandomizedSet
类:
RandomizedSet()
初始化 RandomizedSet
对象bool insert(int val)
当元素 val
不存在时,向集合中插入该项,并返回 true
;否则,返回 false
。bool remove(int val)
当元素 val
存在时,从集合中移除该项,并返回 true
;否则,返回 false
。int getRandom()
随机返回现有集合中的一项(测试用例保证调用此方法时集合中至少存在一个元素)。每个元素应该有 相同的概率 被返回。你必须实现类的所有函数,并满足每个函数的 平均 时间复杂度为 O(1)
。
思路:看到插入和删除时间复杂度为 O(1) 时,立马联想到使用哈希表,但哈希表无法在随机获取值的时候达到O(1) ,需要使用数组配合。因此同时使用哈希表和数组结构
在插入时,哈希表记录对应值所在的下标,并将值从数组队尾插入;
在删除时,获取值对应的数组下标,并用队尾元素覆盖写入对应下标,且重新给哈希表对应的记录更新下标;
在查找时,只要随机一个下标值访问数组即可
-
- var RandomizedSet = function () {
- this.map = new Map();//存储val和index
- this.arr = [];
- };
-
- /**
- * @param {number} val
- * @return {boolean}
- */
- RandomizedSet.prototype.insert = function (val) {
- if (this.map.has(val)) {
- return false;
- } else {
- this.map.set(val, this.arr.length);
- this.arr.push(val);
- return true;
- }
- };
-
- /**
- * @param {number} val
- * @return {boolean}
- */
- RandomizedSet.prototype.remove = function (val) {
- if (this.map.has(val)) {
- const index = this.map.get(val);
- this.arr[index] = this.arr[this.arr.length - 1];
- this.map.set(this.arr[index], index);
- this.arr.pop();
- this.map.delete(val);
- return true;
- } else {
- return false;
- }
- };
-
- /**
- * @return {number}
- */
- RandomizedSet.prototype.getRandom = function () {
- let randomIndex = Math.floor(Math.random() * this.arr.length);
- return this.arr[randomIndex];
- };
-
- /**
- * Your RandomizedSet object will be instantiated and called as such:
- * var obj = new RandomizedSet()
- * var param_1 = obj.insert(val)
- * var param_2 = obj.remove(val)
- * var param_3 = obj.getRandom()
- */
给你一个有序数组 nums
,请你 原地 删除重复出现的元素,使得出现次数超过两次的元素只出现两次 ,返回删除后数组的新长度。
不要使用额外的数组空间,你必须在 原地 修改输入数组 并在使用 O(1) 额外空间的条件下完成。
说明:
为什么返回数值是整数,但输出的答案是数组呢?
请注意,输入数组是以「引用」方式传递的,这意味着在函数里修改输入数组对于调用者是可见的。
思路:题目的数组还是有序,增加了一个限制,可以允许重复次数2。在上一题的基础上,增加一个计数器,统计重复的个数。pre和i不相等的时候正常给pre赋值。当两者相等的时候,如果计数器没有超过2,正常赋值。超过了就不移动pre了。相等的时候count++,不等的时候count=1
- var removeDuplicates = function (nums) {
- let pre = 0;//左指针
- let count = 1;//增加一个计算重复的count
- for (let i = 1; i < nums.length; i++) {
- if (nums[pre] != nums[i]) {//左右指针不相等
- pre++;
- nums[pre] = nums[i];
- count = 1;//不相等的时候count为1
- } else {//左右指针相等
- count++;
- if (count <= 2) {//count没有超过2
- pre++;//正常记录
- nums[pre] = nums[i];
- }//超过了,不处理
- }
- }
- return pre + 1;
- };
罗马数字包含以下七种字符: I
, V
, X
, L
,C
,D
和 M
。
字符 数值 I 1 V 5 X 10 L 50 C 100 D 500 M 1000
例如, 罗马数字 2 写做 II
,即为两个并列的 1。12 写做 XII
,即为 X
+ II
。 27 写做 XXVII
, 即为 XX
+ V
+ II
。
通常情况下,罗马数字中小的数字在大的数字的右边。但也存在特例,例如 4 不写做 IIII
,而是 IV
。数字 1 在数字 5 的左边,所表示的数等于大数 5 减小数 1 得到的数值 4 。同样地,数字 9 表示为 IX
。这个特殊的规则只适用于以下六种情况:
I
可以放在 V
(5) 和 X
(10) 的左边,来表示 4 和 9。X
可以放在 L
(50) 和 C
(100) 的左边,来表示 40 和 90。 C
可以放在 D
(500) 和 M
(1000) 的左边,来表示 400 和 900。给你一个整数,将其转为罗马数字。
思路:用哈希表存储键值对+使用相减法获取每步加入的字符。这里使用map构建哈希表,因为map
对象会维护键值对的插入顺序 。为什么要考虑顺序呢?因为相减法会尝试用最大值做减法,用被减数减它,每次相减可获得减数对应的val值。比如3200这个数,3200-1000,得到一个1000,用M表示,剩余2200,是不是还可以用1000减,得到MM,还有1200,在用1000减,得到MMM,剩余200,在尝试从剩余的数值中找比200小的第一个,100。用100减得到MMMC,最后MMMCC
- /**
- * @param {number} num
- * @return {string}
- */
- var intToRoman = function (num) {
- let map = new Map();
- let carry = 1;
- let str = '';
- //map存储键值对,在遍历的时候顺序和插入的顺序保持一致,大的值放前面
- map.set(1000, 'M');
- map.set(900, 'CM');
- map.set(500, 'D');
- map.set(400, 'CD');
- map.set(100, 'C');
- map.set(90, 'XC');
- map.set(50, 'L');
- map.set(40, 'XL');
- map.set(10, 'X');
- map.set(9, 'IX');
- map.set(5, 'V');
- map.set(4, 'IV');
- map.set(1, 'I');
- //用相减比取余简单一点。一直尝试用map中较高的值做减数
- map.forEach((val, key) => {
- while (num >= key) {//对每个key值如果可以作为减数,一直减,直到不符合,在尝试下一个key
- str += val;
- num -= key;
- }
- })
- return str;
- };
给你一个未排序的整数数组 nums
,请你找出其中没有出现的最小的正整数。
请你实现时间复杂度为 O(n)
并且只使用常数级别额外空间的解决方案。
思路:如果不考虑空间复杂度
求最小正整数,可以初始化最小正整数为1。只处理正数,不用管负数。那么用哈希表存储已遍历的元素。去哈希表里找是否有min,如果有,min+1,同时删除哈希表里的min,防止哈希过长。遍历nums结束,min值返回
- /**
- * @param {number[]} nums
- * @return {number}
- */
- var firstMissingPositive = function (nums) {
- let set = new Set();//用map存储已存在的值
- let min = 1;
- for (let i = 0; i < nums.length; i++) {
- if (nums[i] > 0) {//set只处理正数
- set.add(nums[i]);//先将结果入set
- while (set.has(min)) {//如果set里有min,说明min已经存在
- set.delete(min);//删除min,min+1,继续查找
- min++;
- }
- }
- }
- return min;
- };
思路:题目要求空间复杂度为O(1)。怎么构建哈希表呢?
可以采用一种“原地哈希”的方法。
基本思想是:利用数组本身的索引来标记数字是否出现过。具体步骤如下:
首先遍历数组,将所有负数和0的数置为n+1。
然后再次遍历数组,将出现过的数字对应的索引位置的数置为负数。
最后再次遍历数组,找到第一个正数的索引加1即为未出现的最小正整数。
在第二次遍历设置最小整数的时候用的是Math.abs(nums[i]),既不能更改负数的状态,同时还要使用当前nums[i]的正数值作为索引去更新其他值。
- /**
- * @param {number[]} nums
- * @return {number}
- */
- var firstMissingPositive = function (nums) {
- let n = nums.length;
- //原地hash
- for (let i = 0; i < nums.length; i++) {
- if (nums[i] <= 0) {
- nums[i] = n + 1;//设置负数都比n大
- }
- }
- //处理小于等于n的正数
- for (let i = 0; i < nums.length; i++) {
- let num = Math.abs(nums[i]);
- if (num <= n) {
- const index = num - 1;//用数组下标表示正数是否出现
- nums[index] = -Math.abs(nums[index]);//将已经出现的设为自身的负数,同时正数的值还可能后序遍历用到
- }
- }
- for (let i = 0; i < nums.length; i++) {
- if (nums[i] > 0) {
- return i + 1;
- }
- }
- return n + 1;
- };
给你一个整数数组 nums
,返回 数组 answer
,其中 answer[i]
等于 nums
中除 nums[i]
之外其余各元素的乘积 。
题目数据 保证 数组 nums
之中任意元素的全部前缀元素和后缀的乘积都在 32 位 整数范围内。
请 不要使用除法,且在 O(n)
时间复杂度内完成此题。
思路:这个题明确给出了不能用乘法,而且时间复杂度限制在o(n),然后还有提示前缀元素和后缀元素。如何利用前缀和后缀呢
对于要返回的数组arr,数组每一项arr[i]可以拆为两个部分的乘积,[0,i)的元素乘积,(i,len-1]的元素乘积。因此,先让arr[i]记录数组前缀积,一次遍历数组nums,所有的下标都能更新前缀积。之后计算后缀积,因为此时arr已经有前缀积了,所有后缀积需要用一个变量suf存储。从后往前所有元素都共享这个suf,arr[i]*这个前缀就可以了
- /**
- * @param {number[]} nums
- * @return {number[]}
- */
- var productExceptSelf = function (nums) {
- let arr = [];//返回的数组arr[i]表示除i以外元素的乘积,按照下标i将arr[i]=i之前的元素乘积*i之后的元素乘积
- // 两次for循环
- //第一次for循环求前缀积
- arr[0] = 1;
- let suf = 1;//后缀
- for (let i = 1; i < nums.length; i++) {
- arr[i] = arr[i - 1] * nums[i - 1];
- }
-
- //第二次for循环求后缀积
- for (let i = nums.length - 1; i >= 0; i--) {
- arr[i] = arr[i] * suf;
- suf *= nums[i];
- }
- return arr;
- };
-
给你一个整数数组 nums
和一个整数 k
,请你统计并返回 该数组中和为 k
的子数组的个数 。
子数组是数组中元素的连续非空序列。
思路:这个题一定要看题解。自己想很复杂。这道题目要求统计一个整数数组中和为特定值k的连续非空子数组的个数。解决这个问题的一个有效方法是使用前缀和与哈希表。
对于nums数组:
nums = [1, 0, 1, 0, 1] k = 2前缀和数组如下:
prefixSum = [0, 1, 1, 2, 2, 3]
加上第j个元素的前缀和为prefixSum[j],我现在想找以i为结束的子串数量有多少个。如何有i,prefixSum[i]表示0-i之间的和。那么有prefixSum[j]-prefixSum[i]=k;那我在节点j的时候怎么取找有多少个起始点i呢?假设我已经用map字典存放了前缀和,和出现的次数。那么我在找prefixSum[j]-k在map中有没有出现过,以及出现了几次,是不是就是i出现了几次。
前缀和:首先,我们可以计算数组的一个前缀和数组,这样我们就可以快速得到任意一个子数组的和。前缀和数组
prefixSum
的第i
个元素表示原数组从第一个元素到第i
个元素的和。哈希表:我们使用一个哈希表(用map对象)来存储前缀和的出现次数。键是前缀和,值是该前缀和出现的次数。
遍历数组:我们遍历数组,计算前缀和,并更新哈希表。对于每个前缀和,我们检查
prefixSum[i] - k
是否在哈希表中。如果存在,这意味着我们找到了一个子数组,其和为k。统计子数组个数:如果
prefixSum[i] - k
在哈希表中,我们就增加结果计数。哈希表中prefixSum[i] - k
的值表示有多少个前缀和等于prefixSum[i] - k
,也就是有多少个子数组的和为k。更新哈希表:每次计算新的前缀和时,我们更新哈希表,增加当前前缀和的计数。
边界条件:在开始遍历之前,我们先将
prefixSum[0]
设置为1,因为一个空的子数组的和为0。
- /**
- * @param {number[]} nums
- * @param {number} k
- * @return {number}
- */
- var subarraySum = function (nums, k) {
- //用map计算前缀和
- let map = new Map();
- map.set(0, 1);//表示前缀和为0的有一个
- let prefixSum = 0;
- let count = 0;
- for (let i = 0; i < nums.length; i++) {
- prefixSum += nums[i];//统计当前前缀和
- if (map.has(prefixSum - k)) {
- count += map.get(prefixSum - k);
- }
- map.set(prefixSum, map.has(prefixSum) ? map.get(prefixSum) + 1 : 1);//当前前缀和出现的次数
- }
- return count;
- };
在一条环路上有 n
个加油站,其中第 i
个加油站有汽油 gas[i]
升。
你有一辆油箱容量无限的的汽车,从第 i
个加油站开往第 i+1
个加油站需要消耗汽油 cost[i]
升。你从其中的一个加油站出发,开始时油箱为空。
给定两个整数数组 gas
和 cost
,如果你可以按顺序绕环路行驶一周,则返回出发时加油站的编号,否则返回 -1
。如果存在解,则 保证 它是 唯一 的。
思路:这个问题可以使用贪心算法来解决。具体步骤如下:
遍历每个加油站,计算每个加油站的净剩余汽油量(gas[i] - cost[i])。
定义两个变量:
rest
用于记录总的净剩余汽油量,curr
用于记录当前的净剩余汽油量。从第一个加油站开始遍历,如果当前净剩余汽油量小于0,则说明无法从当前加油站出发到达下一个加油站,需要将起始加油站设置为下一个加油站。
最后,如果总的净剩余汽油量大于等于0,则返回起始加油站的索引,否则返回-1。
- /**
- * @param {number[]} gas
- * @param {number[]} cost
- * @return {number}
- */
- var canCompleteCircuit = function (gas, cost) {
- let currGas = 0;
- let rest = 0; //记录总剩余,如果总的油量小于总消耗,一定没解
- let start = 0;
- for (let i = 0; i < gas.length; i++) {
- rest += gas[i] - cost[i];//记录此时总剩余
- currGas += gas[i] - cost[i];//记录当前汽油量
- if (currGas < 0) {//如果当前汽油是负数,更换起始点
- start = i + 1;
- currGas = 0;
- }
- }
- return rest >= 0 ? start : -1;//如果总剩余是负数,一定找不到,否则返回最后一次更新的起始点
- };
n
个孩子站成一排。给你一个整数数组 ratings
表示每个孩子的评分。
你需要按照以下要求,给这些孩子分发糖果:
1
个糖果。请你给每个孩子分发糖果,计算并返回需要准备的 最少糖果数目 。
思路:用一个数组存储第i个节点应该获取的糖果数量。默认最少有一个。
题目要求每个节点i,既要考虑它左侧的节点i-1,又要考虑右侧的节点i+1。这个时候不能同时考虑左右两个,因为会顾此失彼。这题的求解应该先考虑左边大或者右边大,在进行右边大或左边大的考虑。
假设我们先考虑右边比左边大的情况:
从左到右遍历,如果评分ratings[i]大于ratings[i-1],那么arr[i]=arr[i-1]+1,即在前一个糖果的基础上加1。否则arr[i]=1,不变。
遍历结束后,所有节点的右边比左边大的情况考虑结束。
接着考虑左边比右边大的情况:
这个时候我们需要以右侧为基准,不断找左侧孩子,因此,循环从右向左,如果当前评分ratings[i]大于ratings[i+1],第i个孩子的糖果数量arr[i]=arr[i+1]+1,但是如果这个值可能小于arr[i],为了两个都满足,取最大值。
arr[i]求解结束,第i个孩子的糖果数量固定了,更新sum值
- /**
- * @param {number[]} ratings
- * @return {number}
- */
- var candy = function (ratings) {
- let sum = 0;
- let arr = Array(ratings.length).fill(1);//初始化糖果数组
- for (let i = 1; i < ratings.length; i++) {//从前遍历,右侧比左侧大,右侧+1
- if (ratings[i] > ratings[i - 1]) {
- arr[i] = arr[i - 1] + 1;//右边比左边大的话,对应糖果数组比左边+1
- }
- }
- sum = arr[ratings.length - 1];
- for (let i = ratings.length - 2; i >= 0; i--) {//如果左边比右边大
- if (ratings[i] > ratings[i + 1]) {
- arr[i] = Math.max(arr[i + 1] + 1, arr[i]);
- }
- //二次遍历arr[i]存放的是最终糖果数量
- sum += arr[i];
- }
- return sum;
- };
给定 n
个非负整数表示每个宽度为 1
的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。
思路:使用单调递增栈,一旦发现添加的柱子高度大于栈头元素了,此时就出现凹槽了。然后计算横向的雨水量。凹槽内部的雨水通过横向计算累加得到。
凹槽的底部:取栈顶元素,将栈顶元素弹出,这个就是凹槽的底部
凹槽的高度:min(凹槽左边高度, 凹槽右边高度) - 凹槽自身高度
凹槽的宽度:凹槽右边的下标 - 凹槽左边的下标 - 1(因为只求中间宽度)
在求解时,先考虑凹槽时处理栈的情况要简单一下。同时在while循环求栈的时候,如果栈内没有元素可提前结束。
- /**
- * @param {number[]} height
- * @return {number}
- */
-
- var trap = function (height) {
- let stack = [];
- let total = 0; // 记录雨水量
-
- for (let i = 0; i < height.length; i++) {
- while (stack.length && height[i] > height[stack[stack.length - 1]]) {//出现凹槽循环求解栈内元素
- let curIndex = stack.pop();
- if (stack.length === 0) {//如果栈里没有数据,当前curIndex没有降水量
- break;
- }
- let leftIndex = stack[stack.length - 1];//拿到左边的高度
- let h = Math.min(height[leftIndex], height[i]) - height[curIndex];//和最右边取一个最小值,并减去自身的高度
- let w = i - leftIndex - 1;//计算宽度
- total += h * w;//累加当前的体积
- }
- stack.push(i);//处理结束,当前元素入栈
- }
-
- return total;
- };
给你两个按 非递减顺序 排列的整数数组 nums1
和 nums2
,另有两个整数 m
和 n
,分别表示 nums1
和 nums2
中的元素数目。
请你 合并 nums2
到 nums1
中,使合并后的数组同样按 非递减顺序 排列。
注意:最终,合并后数组不应由函数返回,而是存储在数组 nums1
中。为了应对这种情况,nums1
的初始长度为 m + n
,其中前 m
个元素表示应合并的元素,后 n
个元素为 0
,应忽略。nums2
的长度为 n
。
解题思路:从两个数组的最后两个元素进行比较,取最大值放第一个数组后面。考虑边界值,n=0时不用处理,m=0时将nums2数组值填充第一个
- /**
- * @param {number[]} nums1
- * @param {number} m
- * @param {number[]} nums2
- * @param {number} n
- * @return {void} Do not return anything, modify nums1 in-place instead.
- */
- var merge = function (nums1, m, nums2, n) {
- if (n == 0) return;
- //比较数据中最后一个元素,每次取最大值
- let i = m - 1,
- j = n - 1,
- k = m + n - 1;
- while (i >= 0 && j >= 0) {
- if (nums1[i] > nums2[j]) {
- nums1[k--] = nums1[i--];
- } else {
- nums1[k--] = nums2[j--];
- }
- }
- while (j >= 0) {
- nums1[k--] = nums2[j--];
- }
- };
以数组 intervals
表示若干个区间的集合,其中单个区间为 intervals[i] = [starti, endi]
。请你合并所有重叠的区间,并返回 一个不重叠的区间数组,该数组需恰好覆盖输入中的所有区间 。
示例 1:
输入:intervals = [[1,3],[2,6],[8,10],[15,18]] 输出:[[1,6],[8,10],[15,18]] 解释:区间 [1,3] 和 [2,6] 重叠, 将它们合并为 [1,6].
示例 2:
输入:intervals = [[1,4],[4,5]] 输出:[[1,5]] 解释:区间 [1,4] 和 [4,5] 可被视为重叠区间。
思路:没有技巧,纯手撕,根据测试用例看出bug在哪。注意这个用例
intervals =[[1,4],[2,3]]。输出是1,4。思路是将intervals按照首元素递增排列,可以借用sort实现。其次是合并逻辑,记录一个start和end。end是不确定的,只要end小与下一个start的时候,才将start,end存起来。整个思路如下
- 对输入的区间数组
intervals
按照区间的起始位置进行排序。- 初始化
start
和end
为排序后第一个区间的起始和结束位置。- 遍历排序后的区间数组,从第二个区间开始,判断当前区间是否与前一个区间重叠。
- 如果重叠,更新
end
为当前区间和前一个区间结束位置的最大值。- 如果不重叠,将前一个区间添加到结果数组
res
,并更新start
和end
为当前区间的起始和结束位置。- 遍历结束后,将最后一个区间添加到结果数组。
- var merge = function (intervals) {
- let res = []; // 存储最终合并后的区间
- intervals.sort((a, b) => a[0] - b[0]); // 对区间数组按区间的起始位置进行升序排序
- let [start, end] = intervals[0]; // 初始化start和end为第一个区间的起始和结束位置
-
- // 从第二个区间开始遍历
- for (let i = 1; i < intervals.length; i++) {
- const [curStart, curEnd] = intervals[i]; // 获取当前区间的起始和结束位置
- if (end >= curStart) { // 如果当前区间的起始位置小于等于上一个区间的结束位置,说明存在重叠
- end = Math.max(end, curEnd); // 更新结束位置为两个区间结束位置的最大值
- } else { // 如果没有重叠,将上一个区间添加到结果数组
- res.push([start, end]);
- start = curStart; // 更新start和end为当前区间的起始和结束位置
- end = curEnd;
- }
- }
-
- // 添加最后一个区间到结果数组
- res.push([start, end]);
- return res;
- };
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。