leetcode刷题——数组篇

112 阅读5分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第11天

 一、数组篇

leetcode-88--合并两个有序数组

Question:给你两个有序整数数组 nums1(m个元素) 和 nums2(n个元素),请你将 nums2 合并到 nums1 中,使 num1 成为一个有序数组。(假设 nums1 **的空间大小等于 m + n

示例:

    输入:nums1 = [1,2,3,0,0,0], m = 3, nums2 = [2,5,6], n = 3
输出:[1,2,2,3,5,6]

Method 1: 直接合并后排序

比较直观的做法是先将 nums2 插到 num1 的末尾, 然后排序。这里我们注意 nums1 = [1,2,3,0,0,0],数组有多余空间,只是元素没有占满

const merge = function (nums1, m, nums2, n) {
    if (n === 0) return;    // 被合并数组没有元素直接返回
    nums1.splice(m, nums1.length - m, ...nums2);    // 先将 nums2 放在 nums1 后面
    // 也可如下:
    // for (let i = 0; i <= n; i++) {
    //     nums1[m + i] = nums2[i];
    // }
    nums1.sort((a, b) => a - b);    // 递增顺序排序
};

note: 针对上面解法回顾一下两个知识点

  • Array.prototype.splice():通过删除或替换现有元素或者原地添加新的元素来修改数组,并以数组形式返回被修改的内容(会改变原数组)。这里是用 nums2 的元素来替换 nums1 多余的数组空间(上面用0表示)。
  • Array.prototype.sort():用原地算法对数组的元素进行排序,并返回数组。默认排序顺序是在将元素转换为字符串,然后比较它们的UTF-16代码单元值序列时构建的。

解法类似 merge sort (归并排序),来看看相关的思路:

merge 的过程可以是先比较两个数组的头元素,然后将较小的推到最终的数组中,并将其从原数组中出队。不断循环直到两个数组都为空。

// 这里代码赘余为了方便理解
const myMerge = function (nums1, nums2) {
    let arr = [];
    while (nums1.length || nums2.length) {
        // 考虑一下边缘情况
        if (nums1.length === 0) {
            arr.push(nums2.shift());
            continue;
        }
        if (nums2.length === 0) {
            arr.push(nums1.shift());
            continue;
        }
        // 取出两个数组的头部元素
        const a = nums1[0];
        const b = nums2[0];
        // 比较大小
        if (a > b) {
            arr.push(nums2.shift());
        } else {
            arr.push(nums1.shift());
        }
    }
    return arr;
};

但是题目要求的是“原地修改”,即返回的是数组 nums1

Method 2: 双指针法

Think1:基于上面的 merge sort ,如果都从数组头部开始比较,需要将 nums1 的前 m 个元素放到一个辅助数组中,避免写指针写入的干扰(空间复杂度为O(m))

Think2:从后往前比较,将比较出来的较大的那个数放在 nums1 最后的那个空余空间(时间复杂度为O(m+n),空间复杂度为O(1))

// Think2
const merge = function (nums1, m, nums2, n) {
    let cur = m + n - 1;   // 定义写指针,指向当前用于填入元素的位置,初始化指向 nums1 的末尾
    while (cur >= 0) {
        if (n === 0) return;
        // 边缘情况处理
        if (m < 1) {
            nums1[cur--] = nums2[--n];
            continue;
        }
        if (n < 1) {
            nums1[current--] = nums1[--m];
            continue;
        }
        // 具体处理
        if (nums1[m - 1] > nums2[n - 1]) {
            nums1[current--] = nums1[--m];
        } else {
            nums1[current--] = nums2[--n];
        }
    }
}

// 更好的处理
const merge = function (nums1, m, nums2, n) {
    // 定义三个指针
    let len1 = m - 1,
        len2 = n - 1,
        len = m + n - 1
    while (len2 >= 0) { // nums2 有元素时
        if (len1 < 0) { // nums1 没有元素时
            nums1[len--] = nums2[len2--]
            continue
        }
        nums1[len--] = nums1[len1] >= nums2[len2] ? nums1[len1--] : nums2[len2--]
    }
};

leetcode-1--两数之和

Question:给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出 和为目标值 的那 两个 整数,并返回它们的数组下标

示例:

    输入:nums = [2,7,11,15], target = 9
输出:[0,1]
解释:因为 nums[0] + nums[1] == 9 ,返回 [0, 1] 

Method 1:暴力枚举

使用两层循环,外层循环计算当前元素与 target 之间的差值,内层循环寻找该差值,若找到该差值,则返回两个元素的下标(时间复杂度为 O(n^2),空间复杂度为 O(1))

var twoSum = function (nums, target) {
    const len = nums.length;
    for (let i = 0; i < len; i++) {
        let preItem = nums[i];
        // j = i + 1 的目的是减少重复计算和避免两个元素下标相同
        for (let j = i + 1; j < len; j++) {
            let reaItem = nums[j];
            if (preItem + reaItem === target)
                return [i, j];
        }
    }
    return []; // 没有符合条件的两个数,返回 []
};

Method 2:一层 for 循环寻找差值

使用一层循环,每次循环执行一次 indexOf 判断数组中是否有差值(时间复杂度O(n))

const twoSum = (nums, target) => {
    for (let i = 0; i < nums.length; i++) {
          let diff= nums.indexOf(target - nums[i]);
          if (diff!= -1 && diff!= i) {  // 不存在差值会返回 -1,且题目要求不能出现同一元素
             return [i, diff]
          }
    }
    return []; // 没有符合条件的两个数,返回 []
}

note:

  • Array.prototype.indexOf():返回在数组中可以找到一个给定元素的第一个索引,如果不存在,则返回 -1

Method 3:HashMap法

增加一个 Map 记录已经遍历过的数字及其对应的索引值:这样当遍历一个新数字的时候就去 Map 里查询 target 与该数的差值是否已经在前面的数字中出现过。如果出现过,则直接拿到这个出现过的数的索引和当前遍历的数的数组下标并返回(时间复杂度O(n),空间复杂度为 O(1))

const twoSum = (nums, target) => {
    // 1. 构造 Map 数据结构
    const map = new Map(); // 存储方式 {key, index}
    // 2. 遍历数组
    for (let i = 0; i < nums.length; i++) {
        // 2.1 如果找到 target - nums[i] 的值
        let diff = target - nums[i];
        if (map.has(diff)) { // 判断 map 中是否有这个 diff 值(key)
            // 拿到 key 对应的下标(index)及数组元素下标(i),并返回
            return [map.get(diff), i];
        } else {
            // 2.2 如果没找到则把[该数组元素值 key,该元素在原数组的下标值 index]存入 map
            map.set(nums[i], i);
        }
    }
    return []; // 没有符合条件的两个数,返回 []
};

Note:

  • 一个函数要么有返回值,要么没有返回值,如果混合在一起可能出问题,如调试会出错。所以上述方法最后加上了 return [];。
  • Map 数据结构

leetcode-15--三数之和

Question:给你一个包含 n 个整数的数组 nums,判断 nums 中是否存在三个元素 a,b,c ,使得 a + b + c = 0 ?请你找出所有和为 0 且不重复的三元组。

示例 1:

输入:nums = [-1,0,1,2,-1,-4]
输出:[[-1,-1,2],[-1,0,1]]

Method 1:暴力破解

暴力破解,需要使用三层 for 循环,时间复杂度达到了O(n^3),效率太低,这里不作考虑(除非你实在没办法啦哈哈哈哈!)

Method 2:HashMap法

这个方法实际上是对三层 for 循环的优化,通过 Map 存储记录 + 2层 for 循环实现,时间复杂度可以降到 O(n^2)。

思路转换:回忆一下上一题的两数之和,解法非常相似。而利用一下数学知识,a + b + c = 0 不就是求 a + b = c 吗?(这里 -c 也看成 c)

const threeSum = function (nums) {
    if (nums == null || nums.length < 3) return []; // 判断数组元素个数是否满足要求
    // 初始化两个辅助变量
    let res = [];
    let hashMap = new Map(); // 作 Map 记录
    // 1、先将数组顺序排序
    nums.sort((a, b) => a - b); 
    // 2、第一层 for 循环拿到第一个数
    for (let i = 0; i < nums.length; i++) {
        let first = nums[i];
        // 3、第二层 for 循环(第二个数 nums[j])
        for (let j = i + 1; j < nums.length; j++) {
            // 3.1、拿到第三个数 c = 0 - nums[j] - first
            let third = 0 - nums[j] - first;
            if (hashMap.has(third)) {
                res.push([first, nums[j], third]);
            }
            hashMap.set(nums[j], j);
        }
        // 第二层循环完一次记得清楚这层循环的 map 记录,再进行下一轮双重 for 循环
        hashMap.clear();
    }
    return res;
}

来看看结果:出现了重复三元组

    输入:[-1,0,1,2,-1,-4]

    输出:[[-1,-1,2],[-1,0,1],[-1,1,0],[-1,2,-1],[-1,0,1],[-1,1,0],[-1,2,-1],[0,1,-1]]

    预期结果:[[-1,-1,2],[-1,0,1]]

进一步优化,做去重处理:

const threeSum = function (nums) {
    if (nums == null || nums.length < 3) return []; // 判断数组元素个数是否满足要求
    // 初始化辅助数组
    let res = [];
    // 1、先将数组原地升序
    nums.sort((a, b) => a - b);
    // 2、第一层 for 循环拿到第一个数
    for (let i = 0; i < nums.length; i++) {
        if (nums[i] > 0) break; // 前面已经升序,如果三元组的第一个元素大于0,则三数之和必不等于0,直接退出循环
        if (i > 0 && nums[i] === nums[i - 1]) continue;   // 去除重复情况
        // 这里就回到 两数之和 问题
        let first = nums[i];
        let hashMap = new Map(); // 作 Map 记录(这次放到这里啦!!!)
        // 3、第二层 for 循环(第二个数 nums[j])
        for (let j = i + 1; j < nums.length; j++) {
            // 3.1、拿到第三个数 c = 0 - nums[j] - first
            let third = 0 - nums[j] - first;
            // 去除重复情况
            if (res.length) {
                let [a, b, c] = res[res.length - 1];
                if (b === nums[j] && c === third) continue;
            }
            // 4、判断 Map 中是否有第三个数的记录
            if (hashMap.has(third)) {
                res.push([first, nums[j], third]); // 有的话直接返回三元组
            }
            hashMap.set(nums[j], j); // 没有就把第二个数记录到 Map
        }
    }
    return res;
}

note:关于循环建立新的 Map 结构和清空原 Map 结构数据的性能问题探讨

注意观察上面两个解法建立 Map 的位置,因为每次进行完一轮第二层所有的 for 循环(即第一轮 for 循环结束),都需要保证有一个为空的 Map,不然最后会返回重复的三元组(如原解法的输出和预期结果对比)。所以原解法我们在 for 循环开始前建立 Map后,第一轮 for 循环结束前必须把这个 Map 清空。而去重的解法我把建立 Map的操作放在第一层 for 循环内,这样就不用做清空 Map 的操作了(代码看起来减少了,好像还更让人读懂代码执行顺序了鸭!)。但是这样导致一个问题----每次执行第一层的 for 循环都要新建一个 Map 结构,这个操作的性能是比只需要新建一个 Map 再进行清空要高的多的(注意:这里我们可是每次都只需要清空一个三元组就行了哦!)。所以还是别省代码了哈哈哈哈!!

For more:其实这里的需求使用 Set 数据结构就行啦,可以节省更多内存,比用 Map 性能更好哦!

Method 3:双指针法

外层循环:使用 i 遍历数组

内层循环:使用双指针(left,right),寻找符合三数之和为0的元素

双指针移动避免重复:左右指针同时向内收缩

  • 左指针在保证left < right的前提下,一直右移,直到指向不重复的元素
  • 右指针在保证left < right的前提下,一直左移,直到指向不重复的元素
const threeSum = function (nums) {
    if (nums == null || nums.length < 3) return [];
    let res = [];
    let len = nums.length
    nums.sort((a, b) => a - b);
    for (let i = 0; i < len - 2; i++) { // 这里是 len-2 哦!
        if (nums[i] > 0) break;
        if (i > 0 && nums[i] === nums[i - 1]) continue;
        let left = i + 1; // 左指针
        let right = len - 1; // 右指针
        while (left < right) {
            let sum = nums[i] + nums[left] + nums[right];
            if (sum < 0) { // 三数和小于0,则左指针右移
                left++;
            } else if (sum > 0) { // 三数和大于0,则右指针左移
                right--;
            } else {
                res.push([nums[i], nums[left], nums[right]]);
                // 去重:因为数组已经升序,重复的元素是紧贴在一起的,为了避免重复把指针移动到最后一个重复元素的位置
                while (nums[left] === nums[left + 1]) {
                    left++;
                };
                while (nums[right] === nums[right - 1]) {
                    right--;
                };
                // 更新指针位置(同时向内收缩:因为 left right 已经计算过一次是否满足条件,要移动到新的位置上哦!
                left++;
                right--;
                continue;
            }
        }
    }
    return res;
}