头图

🧑‍💻JavaScript算法与数据结构-HowieCong

务必要熟悉JavaScript使用再来学!

一、两数求和——Map

原题:

给定一个整数数组 nums 和一个目标值 target,请你在该数组中找出和为目标值的那 两个 整数,并返回他们的数组下标。你可以假设每种输入只会对应一个答案。但是,你不能重复利用这个数组中同样的元素。

示例

给定 nums = [2, 7, 11, 15], target = 9\
因为 nums[0] + nums[1] = 2 + 7 = 9 所以返回 [0, 1]

(1)暴力解题思路

  • 暴力的解法:两层循环来遍历同一个数组;第一层循环遍历的值为a,第二层循环遍历的值为b;若a+b=目标值,那么a和b对应的数组下标就是我们想要的答案
  • 暴力的反思:

    • 发现代码两层循环了,能不能用空间换时间,把它优化一层循环
    • 两层循环基本意味着O(n^2)的复杂度,这个复杂度容易导致算法超时,如果没超时,写了两层遍历,也会在面试时划分与候选人的区别

(2)Map——空间换时间

结论:几乎所有的求和问题,都可以转化为求差问题
  • 在遍历数组的过程中,增加一个Map来记录已经遍历过的数字,及其对应的索引值,然后每遍历到一个新数字的时候,都回到Map里面查询目标数与该数的差值是否已经在前面的数字中出现过了,若出现了,答案已经显示,没必要再往下走了
  • Eg:以nums = [2,7,11,15]这个数组为例,来模拟下思路

    • 第一次遍历到2,此时Map为空
    • 以2为key,索引0为value作存储,继续往下走;遇到了7
    • 计算targetnum和7的差值为2,去Map中检索2这个key,发现是之前出现过的值
    • 那么2和7的索引组合就是这道题的答案
  • Map编码实现
    const twoSum = function(nums,targer){
        // 用对象模拟map的能力
        const diffs = {}
        // 缓存数组长度
        const len = nums.length
        // 遍历数组
        for(let i = 0; i < len; i++){
            // 判断当前值对应的target差值是否存在(是否已经遍历过)
            if(diffs[target - nums[i]] !== undefined){
                // 若有对应差值,那么答案正确!
                return[diffs[target - nums[i]],i]
            }
            // 若没有对应差值,则记录当前值
            diff[nums[i]]=i
        }
    };

二、强大的双指针

(1)合并两个有序数组

原题

给你两个有序整数数组 nums1 和 nums2,请你将 nums2 合并到 nums1 中,使 nums1 成为一个有序数组。\
说明: 初始化 nums1 和 nums2 的元素数量分别为 m 和 n 。 你可以假设 nums1 有足够的空间(空间大小大于或等于 m + n)来保存 nums2 中的元素。

示例

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

[1,2,2,3,5,6]

1.思路分析
  • 标准解法

    • 首先定义两个指针,各指向两个数组生效部分的尾部
    • 每次只对指针所指的元素进行比较,取其中较大的元素,把它从nums的末尾往前补充

为什么要从后向前补充?

因为要所有值合并到nums1里,所以我们这里可以把nums1看作一个容器,这个容器不为空,而是前面几个坑有内容,如果我们从前往后补充,就没法直接往对应的坑位赋值了(会产生值覆盖)

nums1的有效部分和nums2不一定是一样长的,还得考虑其中一个提前到头的这种情况:

  • 如果提前遍历完的是nums的有效部分,剩下的是nums2,那么就意味着nums1的头部空出来,直接把nums2补到nums1前面即可
  • 如果提前遍历完的是nums2,剩下的是nums1,由于容器本身就是nums,此时就不做任何额外的操作了
2.编码实现
const merge = function(nums1,m,nums2,n){
    // 初始化两个指针的动向,初始化nums1尾部索引k
    let i = m - 1, j = n - 1,k= m + n - 1
    // 当两个数组都没有遍历完时,指针同步移动
    while(i >= 0 && j >= 0){
        // 取较大的值,从末尾往前填补
        if(nums1[i] >= num2[j]){
            nums1[k] = nums1[i]
            i--
            j--
        }else{
            nums1[k] = nums2[j]
            j--
            k--
        }
    }
    
    // nums2留下的情况,特殊处理
    while(j > 0){
        nums1[k] = nums2[j]
        k--
        j--
    }
}

(2)三数求和问题

原题:

给你一个包含 n 个整数的数组 nums,判断 nums 中是否存在三个元素 a,b,c ,使得 a + b + c = 0 ?请你找出所有满足条件且不重复的三元组。\
注意:答案中不可以包含重复的三元组。

示例:

给定数组 nums = [-1, 0, 1, 2, -1, -4], 满足要求的三元组集合为: [ [-1, 0, 1], [-1, -1, 2] ]

1.思路分析
  • 可以把求和问题变成求差问题,固定其中一个数,在剩下的数寻找是否由两个数和这个固定数相加是等于0的
  • 双指针使用场景 => 1.空间换时间 2.可以帮我们降低问题的复杂度
  • 双指针用在设计求和、比大小类的题目时,大前提往往是:该数组必须有序。否则双指针根本无法帮助我们缩小定位的范围
  • 第一步就是将数组排序:
nums = nums.sort((a,b) =>{
    return a- b
})
  • 第二步就是对数组进行遍历,每次遍历到哪个数字,就固定哪个数字,然后把左指针指向该数字后面的一个坑里的数字,把右指针指向数组末尾,让左右指针从起点开始,从中间前进

image.png

  • 每次指针移动一次位置,计算以下两个指针指向数字之和加上固定的那个数之后,是否等于0,如果是那么我们就得到了一个目标组合;否则,分两种情况来看:

    • 相加之和大于0,说明右侧的数偏大了,右指针左移
    • 相加之和小于0,说明左侧的数偏小了,左指针右移
  • 这个数组在题目要求了“不重复的三元组”,还需要做一个重复元素的跳过处理
2.编码实现
const threeSum = function(nums){
    // 用于存放结果数组
    let res = []
    // 给nums排序
    nums = nums.sort((a,b) =>{
        return a - b
    })
    // 缓存数组长度
    cosnt len = nums.length
    // 注意我们遍历到倒数第三个数就足够了,因为左右指针会遍历后面两个数
    for(let i = 0; i < len - 2; i++){
        // 左指针j
        let j = i + 1
        // 右指针k
        let k = len - 1
        // 如果遇到重复的数字,则跳过
        if(i > 0 && nums[i] === nums[i-1]){
            continue
        }
        while(j < k){
            // 三数之和小于0,左指针前进
            if(nums[i] + nums[j] + nums[k] < 0){
                j++
                // 处理左指针元素重复的情况
                while(j < k && nums[j] === num[j + 1]){
                    j++
                }
            }else if(nums[i] + nums[j] + nums[k] > 0){
                // 三数之和大于0,右指针后退
                k--
                //处理右指针重复的情况
                while(j < k && nums[j] === num[k + 1]){
                    k--
                }
            }else{
                // 得到目标数字组合,推入结果数组
                res.push(nums[i],nums[j],nums[k])
                // 左右指针一起前进
                j++
                k--
                // 若左指针元素重复,跳过
                while(j < k && nums[j] === num[j - 1]){
                    j++
                }
                // 若右指针元素重复,跳过
                while(j < k && nums[k] === num[k + 1]){
                    k--
                }
            }
        }
    }
    //  返回结果数组
    return res
}

(3)双指针法中的“对撞指针”法

在上面的题,左右指针一起从两边往中间位置相互靠近,这样的特殊双指针形态,被称为“对撞指针”
  • 关键字——有序和数组
  • 如果普通双指针走不通,立刻要想到对撞指针
  • 即使没有“有序”,应该手动进行排序尝试——没有条件则创造条件
  • 对撞指针可以帮助我们缩小问题的范围,这点在上面的题可以看出,节省了计算时间,降低了问题本身的复杂度,做题的速度也快起来了

❓其他

1. 疑问与作者HowieCong声明

  • 如有疑问、出错的知识,请及时点击下方链接添加作者HowieCong的其中一种联系方式或发送邮件到下方邮箱告知作者HowieCong
  • 若想让作者更新哪些方面的技术文章或补充更多知识在这篇文章,请及时点击下方链接添加里面其中一种联系方式或发送邮件到下方邮箱告知作者HowieCong
  • 声明:作者HowieCong目前只是一个前端开发小菜鸟,写文章的初衷只是全面提高自身能力和见识;如果对此篇文章喜欢或能帮助到你,麻烦给作者HowieCong点个关注/给这篇文章点个赞/收藏这篇文章/在评论区留下你的想法吧,欢迎大家来交流!

2. 作者社交媒体/邮箱-HowieCong


HowieCong
2 声望0 粉丝

大前端开发 => AI 小菜鸡!虚心好学!欢迎一起交流!