leetcode刷题(javaScript)——数组相关场景题总结

数组只是一种数据结构,通常结合其他算法场景出现。这里总结几类在LeetCode刷题时,针对数组相关的场景题,可以使用以下技巧和方法:

  1. 双指针法

    • 快慢指针用于解决数组中的有序问题,如移除重复项、找出唯一元素等。
    • 左右指针用于解决数组中的对撞问题,如两数之和、接雨水等。
  2. 排序:对数组进行排序可以简化很多问题,如对数组进行排序后,可以更容易地解决部分排序问题。

  3. 哈希表:使用对象字面量或Map结构存储键值对,可以快速查找数组中的元素,常用于解决两数之和、最长连续序列等问题。

  4. 滑动窗口:对于找出连续子数组的问题,如连续子数组的最大和、最小覆盖子串等,滑动窗口是一个有效的技巧。

  5. 动态规划:对于需要考虑历史状态的问题,如最大子序和、最长递增子序列等,动态规划可以提供解决方案。

  6. 分治法:将大问题分解为小问题,分别解决后再合并结果,适用于如归并排序等场景。

  7. 贪心算法:在每一步选择中都采取在当前看来最好的选择,例如买卖股票的最佳时机等问题。

哈希表相关参考我的博客:leetcode刷题(javaScript)——字典哈希表相关场景题总结-CSDN博客

动态规划相关参考我的博客

leetcode刷题(javaScript)——动态规划相关场景题总结

递归相关参考我的博客

leetcode刷题(javaScript)——回溯、递归、dfs相关场景题总结-CSDN博客

双指针:快慢指针/左右指针

双指针法在LeetCode刷题中经常被用到,特别是在数组和字符串相关的问题中。双指针法在解决数组和字符串相关问题时,通常用于处理两个方向的遍历、查找、移动等操作。根据问题的特点,选择合适的双指针策略,可以有效地提高问题的解决效率。以下是一些常见的应用场景:

  1. 快慢指针

    • 移除数组中的重复元素,保留唯一元素。
    • 寻找数组中的某个特定值或范围。
  2. 左右指针

    • 在有序数组中查找两数之和、三数之和等。
    • 滑动窗口问题,如最小覆盖子串、最长无重复字符的子串等

26. 删除有序数组中的重复项

给你一个 非严格递增排列 的数组 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是数组长度
};

283. 移动零

给定一个数组 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;
        }
    }
};

977. 有序数组的平方

给你一个按 非递减顺序 排序的整数数组 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;
};

11. 盛最多水的容器

给定一个长度为 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;
};

15. 三数之和

给你一个整数数组 nums ,判断是否存在三元组 [nums[i], nums[j], nums[k]] 满足 i != ji != kj != 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;
};

滑动窗口

239. 滑动窗口最大值

给你一个整数数组 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;
};

 模拟过程

189. 轮转数组

给定一个整数数组 nums,将数组中的元素向右轮转 k 个位置,其中 k 是非负数。

思路:有多种方法可以实现数组向右旋转 k 个位置,下面列举其中两种常见的方法:

  1. 使用额外数组:

    • 创建一个额外的数组,将原数组中从倒数第 k 个元素到最后一个元素复制到额外数组的开头。
    • 将原数组中前 len-k 个元素复制到额外数组的剩余位置。
    • 将额外数组复制回原数组。
    • 时间复杂度为 O(n),空间复杂度为 O(n)。
  2. 使用翻转:

    • 翻转整个数组。
    • 翻转从索引 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]];
    }
}

59. 螺旋矩阵 II 

给你一个正整数 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正方形矩阵,我们可以模拟顺时针填充矩阵的过程。具体步骤如下:

  1. 初始化一个空的n x n矩阵matrix,并定义四个变量topbottomleft、``right`分别表示当前填充区域的上、下、左、右边界。

  2. 定义一个变量num表示当前要填充的数字,初始值为1。

  3. 不断循环填充矩阵,直到num达到n*n为止。在每一轮循环中,按照顺时针的顺序填充矩阵的上、右、下、左边界,同时更新边界值和num的值。

  4. 最终返回填充完成的矩阵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;
}

动态规划

55. 跳跃游戏

给你一个非负整数数组 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;
};

 45. 跳跃游戏 II

给定一个长度为 n0 索引整数数组 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];//返回最后一个元素,即为目标
};

哈希表结合数组

380. O(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()
 */

80. 删除有序数组中的重复项 II

给你一个有序数组 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;
};

12. 整数转罗马数字

罗马数字包含以下七种字符: I, V, X, LCD 和 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;
};

41. 缺失的第一个正数

给你一个未排序的整数数组 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)。怎么构建哈希表呢?

可以采用一种“原地哈希”的方法。

基本思想是:利用数组本身的索引来标记数字是否出现过。具体步骤如下:

  1. 首先遍历数组,将所有负数和0的数置为n+1。

  2. 然后再次遍历数组,将出现过的数字对应的索引位置的数置为负数。

  3. 最后再次遍历数组,找到第一个正数的索引加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;
};

前缀后缀和/积

238. 除自身以外数组的乘积

给你一个整数数组 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;
};

560. 和为 K 的子数组

给你一个整数数组 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出现了几次。

  1. 前缀和:首先,我们可以计算数组的一个前缀和数组,这样我们就可以快速得到任意一个子数组的和。前缀和数组prefixSum的第i个元素表示原数组从第一个元素到第i个元素的和。

  2. 哈希表:我们使用一个哈希表(用map对象)来存储前缀和的出现次数。键是前缀和,值是该前缀和出现的次数。

  3. 遍历数组:我们遍历数组,计算前缀和,并更新哈希表。对于每个前缀和,我们检查prefixSum[i] - k是否在哈希表中。如果存在,这意味着我们找到了一个子数组,其和为k。

  4. 统计子数组个数:如果prefixSum[i] - k在哈希表中,我们就增加结果计数。哈希表中prefixSum[i] - k的值表示有多少个前缀和等于prefixSum[i] - k,也就是有多少个子数组的和为k。

  5. 更新哈希表:每次计算新的前缀和时,我们更新哈希表,增加当前前缀和的计数。

  6. 边界条件:在开始遍历之前,我们先将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;
};

贪心算法

134. 加油站

在一条环路上有 n 个加油站,其中第 i 个加油站有汽油 gas[i] 升。

你有一辆油箱容量无限的的汽车,从第 i 个加油站开往第 i+1 个加油站需要消耗汽油 cost[i] 升。你从其中的一个加油站出发,开始时油箱为空。

给定两个整数数组 gascost ,如果你可以按顺序绕环路行驶一周,则返回出发时加油站的编号,否则返回 -1 。如果存在解,则 保证 它是 唯一 的。

思路:这个问题可以使用贪心算法来解决。具体步骤如下:

  1. 遍历每个加油站,计算每个加油站的净剩余汽油量(gas[i] - cost[i])。

  2. 定义两个变量:rest用于记录总的净剩余汽油量,curr用于记录当前的净剩余汽油量。

  3. 从第一个加油站开始遍历,如果当前净剩余汽油量小于0,则说明无法从当前加油站出发到达下一个加油站,需要将起始加油站设置为下一个加油站。

  4. 最后,如果总的净剩余汽油量大于等于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;//如果总剩余是负数,一定找不到,否则返回最后一次更新的起始点
};

135. 分发糖果

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;
};

单调栈

42. 接雨水

给定 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;
};

数组合并

88. 合并两个有序数组

给你两个按 非递减顺序 排列的整数数组 nums1 nums2,另有两个整数 mn ,分别表示 nums1nums2 中的元素数目。

请你 合并 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--];
    }
  };

56. 合并区间

以数组 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存起来。整个思路如下

  1. 对输入的区间数组intervals按照区间的起始位置进行排序。
  2. 初始化startend为排序后第一个区间的起始和结束位置。
  3. 遍历排序后的区间数组,从第二个区间开始,判断当前区间是否与前一个区间重叠。
  4. 如果重叠,更新end为当前区间和前一个区间结束位置的最大值。
  5. 如果不重叠,将前一个区间添加到结果数组res,并更新startend为当前区间的起始和结束位置。
  6. 遍历结束后,将最后一个区间添加到结果数组。
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;
};

  • 11
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

三月的一天

你的鼓励将是我前进的动力。

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值