LeetCode 1. 两数之和

·  阅读 514

力扣第一题:两数之和

题目描述:

一个函数,接收两个参数,一个数组、一个目标数,求出数组中两个元素之和等于目标数的两个数(必有一个有效答案)。

一、暴力

两层遍历,找到答案。

function twoSum(nums: number[], target: number): number[] {
    let result = []
    for (let i = 0; i < nums.length; i++) {
        for (let j = i + 1; j < nums.length; j++) {
            if (nums[i] + nums[j] === target) {
                result.push(i);
                result.push(j);
            }
        }
    }
    return result;
};
复制代码

这样写可以,但太暴力,跑了 132ms,注意题目,每个测试用例只有一个有效答案,如果在一开始就找到了,那后续的遍历都是无意义的。针对这个条件做出优化。

function twoSum(nums: number[], target: number): number[] {
    let result = []
    for (let i = 0; i < nums.length; i++) {
        for (let j = i + 1; j < nums.length; j++) {
            if (nums[i] + nums[j] === target) {
                result.push(i);
                result.push(j);
                return result;
            }
        }
    }
};
复制代码

改动一行代码,现在跑了 88ms,这算是优化吧?

时间复杂度 O(N2)O(N^2),空间复杂度 O(1)O(1)

二、HashMap

思考:哪一步耗时?

答案:找差的过程。

对于每一个元素,都要在另外的数组元素中找出值等于 target - nums[i],这是最耗时的步骤。那么怎么解决?

数据结构的存在就是为了在某些场景做出某些动作获得更高的收益。那么这种场景用什么数据结构合适?

先了解一下哈希表(散列表)。这是一种根据键而直接访问在内存存储位置的数据结构,也就是说,通过一个函数f(key)计算出值在数组中哪个位置,来加快查找速度。这个函数f()称为散列函数,存放记录的数组称为散列表。

🌰 你想要找到某个微信好友(键),第一步打开微信通讯录(建立哈希表),第二步点击右侧字母找到好友名首字母(建立一个从名字到首字母的散列函数),第三步在这个首字母列表中查找好友。

问题又来了,进行存储的是一个数组,那么多个姓氏首字母相同怎么处理?这也就是哈希冲突。多个键通过散列函数计算出来的哈希值相同,那么可以使用拉链法解决这个问题。所谓拉链法也就是存放记录的数组并不直接存储值,而是存储一个链表的头部,在之后的每次添加中,都追加到链表末尾,从而解决了冲突的产生。整个流程也就是通过散列函数计算键的散列值,根据散列值找到数组相应下标,追加到下标中的链表。

这里还能有其他优化,比如说链表的查找需要遍历,如果链表存储了太多元素,那效率也不行。所以有拉链法变种,不使用链表,而是使用树,比如说公众号标题常客红黑树?还有一种解决方法是开放寻址法。整体思路就是当哈希地址冲突时(坑被占了),那就以这个哈希地址1为基础再生成一个哈希地址2,如果2还是被占了,就再以1为基础产生另一个哈希地址,直到有坑位为止。但开放寻址的问题就是每个哈希地址可能都有关联的下一个下下个,所以删除要慎重,一般是软删除。

这么来看,好似JS中的对象与哈希表非常像。确实,JS的对象是一种Hash结构的数据结构,但不是Hash表,它是一种字符串 - 值的映射关系,而哈希表是一种值 - 值的映射关系。所以ES6提供了一个更完善的Map数据结构来在JS中使用哈希表。

🌰 数组是什么?数组在正经的语言中的实现大概都是一种线性存储的数据结构,不仅是逻辑上的更是物理上的。元素在内存中一一排列开来,所以定义的时候都要说明一下大小,免得开辟一个太大的空间,浪费。数组能够进行随机存储,是因为在数组中所有的元素都是相同的数据类型,那么也就占用相同的物理空间,只需要通过下标计算下偏移量即可读取数组元素。

回忆一下JS的数组,它是数组吗?还是只是叫“数组”。

V8 源码为证:

image.png

上面注释翻译一下大概就是:JSArray描述的是JavaScript的数组对象,可以有两种存储模式:

  • 快:使用FixedArray类型并且 length <= element.length()
  • 慢:以数字为键的哈希表

FixedArray是啥,我们不知道,但是我们知道JSObject按照这个起名风格是JavaScript object对象,也就是数组继承了对象。所以是不是可以认为一个数组实际上是:

const arr = [1,2,3];
// 翻译为
const arr = {
    '0': 1,
    '1': 2,
    '2': 3,
}
复制代码

所以这也是为什么arr['0']也能进行元素访问的原因?

跑题了,放题解:

function twoSum(nums: number[], target: number): number[] {
    let map = new Map();
    for (let i = 0; i < nums.length; i++) {
        if (map.has(target - nums[i])) {
            return [map.get(target - nums[i]), i];
        } else {
            map.set(nums[i], i)
        }
    }
};
复制代码

嗯,不错,运行时长86ms。WTF!!!比暴力法就快了这么一点?

分析下复杂度:时间复杂度是 O(N)O(N),空间复杂度也是 O(N)O(N)。空间换时间,但这时间也提升不多啊。

三、原生时刻

function twoSum(nums: number[], target: number): number[] {
    for (let i = 0; i < nums.length; ++i) {
        const result = target - nums[i];
        const order = nums.indexOf(result, i+1);
        if (order !== -1) {
            return [i, order]
        }
    }
};
复制代码

122ms,也不差。

function twoSum(nums: number[], target: number): number[] {
    let dic = {}
    for (let i = 0; i < nums.length; ++i) {
        if (target - nums[i] in dic) {
            return [dic[target-nums[i]], i]
        }
        dic[nums[i]] = i
    }
};
复制代码

80ms,一般般。

function twoSum(nums: number[], target: number): number[] {
    for (let i = 0; i < nums.length; ++i) {
        if (nums.lastIndexOf(target - nums[i]) !== i && nums.lastIndexOf(target - nums[i]) >= 0) {
            return [i, nums.lastIndexOf(target - nums[i])]
        }
    }
};
复制代码

484ms,炸了。

好了,就寝了。还有更多写法。比如什么先排个序啊,先过滤一下啦,啥乱七八糟的。开心就好。

分类:
前端
标签:
分类:
前端
标签:
收藏成功!
已添加到「」, 点击更改