力扣第一题:两数之和。
题目描述:
一个函数,接收两个参数,一个数组、一个目标数,求出数组中两个元素之和等于目标数的两个数(必有一个有效答案)。
一、暴力
两层遍历,找到答案。
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,这算是优化吧?
时间复杂度 ,空间复杂度 。
二、HashMap
思考:哪一步耗时?
答案:找差的过程。
对于每一个元素,都要在另外的数组元素中找出值等于 target - nums[i]
,这是最耗时的步骤。那么怎么解决?
数据结构的存在就是为了在某些场景做出某些动作获得更高的收益。那么这种场景用什么数据结构合适?
先了解一下哈希表(散列表)。这是一种根据键而直接访问在内存存储位置的数据结构,也就是说,通过一个函数f(key)
计算出值在数组中哪个位置,来加快查找速度。这个函数f()
称为散列函数,存放记录的数组称为散列表。
🌰 你想要找到某个微信好友(键),第一步打开微信通讯录(建立哈希表),第二步点击右侧字母找到好友名首字母(建立一个从名字到首字母的散列函数),第三步在这个首字母列表中查找好友。
问题又来了,进行存储的是一个数组,那么多个姓氏首字母相同怎么处理?这也就是哈希冲突。多个键通过散列函数计算出来的哈希值相同,那么可以使用拉链法解决这个问题。所谓拉链法也就是存放记录的数组并不直接存储值,而是存储一个链表的头部,在之后的每次添加中,都追加到链表末尾,从而解决了冲突的产生。整个流程也就是通过散列函数计算键的散列值,根据散列值找到数组相应下标,追加到下标中的链表。
这里还能有其他优化,比如说链表的查找需要遍历,如果链表存储了太多元素,那效率也不行。所以有拉链法变种,不使用链表,而是使用树,比如说公众号标题常客红黑树?还有一种解决方法是开放寻址法。整体思路就是当哈希地址冲突时(坑被占了),那就以这个哈希地址1为基础再生成一个哈希地址2,如果2还是被占了,就再以1为基础产生另一个哈希地址,直到有坑位为止。但开放寻址的问题就是每个哈希地址可能都有关联的下一个下下个,所以删除要慎重,一般是软删除。
这么来看,好似JS中的对象与哈希表非常像。确实,JS的对象是一种Hash结构的数据结构,但不是Hash表,它是一种字符串 - 值的映射关系,而哈希表是一种值 - 值的映射关系。所以ES6提供了一个更完善的Map数据结构来在JS中使用哈希表。
🌰 数组是什么?数组在正经的语言中的实现大概都是一种线性存储的数据结构,不仅是逻辑上的更是物理上的。元素在内存中一一排列开来,所以定义的时候都要说明一下大小,免得开辟一个太大的空间,浪费。数组能够进行随机存储,是因为在数组中所有的元素都是相同的数据类型,那么也就占用相同的物理空间,只需要通过下标计算下偏移量即可读取数组元素。
回忆一下JS的数组,它是数组吗?还是只是叫“数组”。
V8 源码为证:
上面注释翻译一下大概就是: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!!!比暴力法就快了这么一点?
分析下复杂度:时间复杂度是 ,空间复杂度也是 。空间换时间,但这时间也提升不多啊。
三、原生时刻
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,炸了。
好了,就寝了。还有更多写法。比如什么先排个序啊,先过滤一下啦,啥乱七八糟的。开心就好。