数据结构与算法之 —— 哈希表

611

0x01 基础概念

  1. ES5:Object、Array

  2. ES6:Map、Set

  3. 没有的:dictionary、list、linkedlist、doublelinkedlist、queue、stack、hash

1.1 兼容性

  1. PC 浏览器兼容性为 IE11+

  2. 移动端浏览器要好一些

  3. nodeJs 中兼容性最好,从 0.x 版本中已经支持

1.2 对比

大家更多的会拿 Map 与 Object、Set 与 Array 进行对比,本次会着重介绍他们之间的区别。

0x02 Set

2.1 概念

  1. 数学中的集合:集合是一组不同的对象的集;

  2. 数据结构中的集合:集合是由一组无序且唯一的项组成,使用了与有限集合相同的数学概念,可以把它想象为一个既没有重复元素,也没有顺序概念的数组;

  3. JavaScript 中的集合:在 ECMAScript6 中新增了 Set 类;

2.2 基本方法

2.3 常见操作 

 Set 可以很容易地实现并集(Union)、交集(Intersect)和差集(Difference) 

2.3.1 交集(Intersect)

读作:“A交B”(或“B交A”)

2.3.2 并集(Union)

读作:“A并B”(或“B并A”)

2.3.3 差集(Difference)

读作:B关于A的相对补集

let a = new Set([1, 2, 3]);
let b = new Set([4, 3, 2]);

// 并集
let union = new Set([...a, ...b]);

// Set {1, 2, 3, 4}

// 交集
let intersect = new Set([...a].filter(x => b.has(x)));
// set {2, 3}

// B关于A的相对补集
let difference = new Set([...a].filter(x => !b.has(x)));
// Set {1}

2.4 Set 与 Array 的转换

let arr = [3, 5, 2, 2, 5, 5];

// 数组 => set
let mySet = new Set(arr);

// set => 数组
let myArr = [...mySet];         // 方法一
let myArr2 = Array.from(mySet)  // 方法二

// 经过上面 2 次转换,可以返回去重的数组

0x03 Map

3.1 基本方法

必知方法

3.2 Map 与 Object 的区别

具体区别可以关注:MDN - Objects 和 maps 的比较,主要区别有:

  1. 键的冲突

  2. 键的类型

  3. 键的顺序

  4. Size

  5. 迭代

  6. 性能

本次着重讲键的类型、键的顺序、性能。

3.2.1 键的类型

代码🌰1:

const myObject = Object.create(null, { 
    m: {value: function() {}, enumerable: true}, 
    "2": {value: "2", enumerable: true}, 
    "b": {value: "b", enumerable: true}, 
    0: {value: 0, enumerable: true}, 
    "1": {value: "1", enumerable: true}, 
    "a": {value: "a", enumerable: true}, 
});

3.2.2 键的顺序

Object 键的顺序

1. 类整数的 key,按照升序返回 (and strings like "1" that parse as ints)

2. 字符串 key 按照插入的顺序 (ES2015 guarantees this and all browsers comply)

3. Symbols 按照插入的顺序 (ES2015 guarantees this and all browsers comply)

参考文章

💡 温馨提示:

  1. 不要分开使用 Object.keys() 与 Object.values(),可能会引发键值对儿,对应不上

  2. Object.keys, Object.entries, Object.values, for..in 这些方法,在 Object 的迭代上,顺序都不可靠

  3. 不要相信 Object 的顺序,可以使用 Map 或 Array

3.2.3 性能差别

Object和ES 6的map主要都是用的哈希,那么它们运行效率比较起来怎么样呢?

// 如以下代码,初始化一个map和一个object:
var map = new Map();
var obj = {};
var size = 100000;

for(var i = 0; i < size; i++){
    map.set('key' + i , i);
    obj['key' + i] = i;
}

// 再准备一个keys数组,用来存储查找的key,如下,分成三种情况分别测验:1. keys都能查到;2. 只有一半的keys能找到;3. 全部的keys都查找不到:

var keys = [];
for(var i = 0; i < 100; i++){
    //1. keys都找得到
    keys.push('key' + parseInt(Math.random() * size)); 
    //2. 约一半的keys找不到
    //keys.push('key' + parseInt(Math.random() * size * 2)); 
    //3. 全部的keys都找不到
    //keys.push('key' + parseInt(Math.random() * size) + size);
}

// 然后进行查找,重复多次,并打印时间:
var count = 100000;
console.time("map time");
for(var i = 0; i < count; i++){
    for(var j = 0; j < keys.length; j++){
        let x = map.get(keys[j]);
    }
}
console.timeEnd("map time");
console.time("obj time");
for(var i = 0; i < count; i++){
    for(var j = 0; j < keys.length; j++){
        let x = obj[keys[j]];
    }
}
console.timeEnd("obj time");

1. 总体上 map 和 object 差别不大,可以按需使用;

2. 在 keys 找不到的情况下,map 效率明显高于 object;

3. 建议能用 map 的时候尽量用 map,object 还有迭代顺序和原型查找的问题;

0x04 Set 与 Map 的对比

必知方法对比

0x05 哈希表

HashMap vs TreeMap

HashSet vs TreeSet

​每种数据结构的时间复杂度

5.1 各类语言的实现

  1. Python:dict 字典 hashmap

  2. Java、C#、C++:HashMap、TreeMap 需要指定

  3. JavaScript:“一切皆对象?”

💡 再回顾下上面说到的 Object.values() 中的顺序问题:

var data = {
    name: "yin",
    age: 18,
    "-school-": "high school",
    1: "Monday",
    2: "Thuesday",
    "3": "Wednesday"
};

../../v8/src/runtime/http://runtime-literals.cc 105 boilerplate obj:
0x3930221af3a9: [JS_OBJECT_TYPE]
- map = 0x6712e19cc41 [FastProperties]
- prototype = 0x27d71d20f19
- elements = 0x2e1e1a56b579 <FixedArray[19]> [FAST_HOLEY_ELEMENTS]
- properties = 0x2c2a4d782241 <FixedArray[0]> {
#name: 0x3930221aec51 <String[3]: yin> (data field at offset 0)
#age: 18 (data field at offset 1)
#-school-: 0x3930221aecb1 <String[11]: high school> (data field at offset 2)
}
- elements = {
0: 0x2c2a4d782351 <the hole>
1: 0x3930221aecf9 <String[6]: Monday>
2: 0x3930221aed39 <String[8]: Thuesday>
3: 0x3930221aed79 <String[9]: Wednesday>
4-18: 0x2c2a4d782351 <the hole>
}

5.2 基本概念

哈希表中元素是由哈希函数确定的。将数据元素的关键字K作为自变量,通过一定的函数关系(称为哈希函数),计算出的值,即为该元素的存储地址。表示为:Addr = HashFuction(key)

构造一个哈希表之前需要解决两个主要问题:

  1. 构造一个合适的哈希函数

  2. 冲突的处理

5.3 哈希函数

5.3.1 哈希函数的类型

  1. 加法Hash

  2. 位运算Hash

  3. 乘法Hash

  4. 除法Hash

  5. 查表Hash

  6. 混合Hash

  7. 数组Hash

5.3.2 哈希函数的实现

5.3.3 哈希函数的应用

场景:CMP 中的 AB Test 实现原理

5.4 哈希碰撞

如:0 ~ 14 分在 A 桶,15~ 30 分在 B 桶,对于 117406、406117 这两个人会永远落在 A

备注:实际中咱们公司的 AB test 没有再取模,而是直接使用 crc32 之后的最后一位,会有 0 ~ 9 十位数字, 进行的分桶策略。上图只是为了讲解下常用的方法。

0x06 推荐练习

常见于判断Array、String 中相同元素计数

6.1 解题套路

6.1.1 Set

熟练利用 Set 的特性,进行集合的交集、并集、差集、子集的运算

6.1.2 Map

169. 多数元素

// 169. 多数元素
// [2,2,1,1,1,2,2] => 2

var majorityElement = function(nums) {
    // [2,2,1,1,1,2,2]
    let i;
    let length = nums.length;
    let myMap = new Map();
    for(i = 0; i < length; i++){
        if(myMap.has(nums[i])){
            myMap.set(nums[i], myMap.get(nums[i]) + 1); // 常用方法:计数
        } else {
            myMap.set(nums[i], 1)
        }
    }

    for (let [key, value] of myMap.entries()) {
        if (value > length / 2) return key;
    }
};

0x07 参考

  1. Map - JavaScript | MDN

  2. Set - JavaScript | MDN

  3. ES6 入门教程

  4. 从Chrome源码看JS Object的实现

  5. hash 函数

  6. V8 - Most object types in the V8 JavaScript are described in this file

  7. stackoverflow - Do Object.keys() and Object.values() methods return arrays that preserve the same order