前端算法系列(4):散列表和js引擎中的对象实现

611 阅读4分钟

概述

当对数组中的无序元素进行查找等操作时,需要对该数组进行遍历,即时间复杂度为o(n)。

散列表可以使用o(n)的空间复杂度存储数据,然后通过散列函数找出存储的位置,从而实现接近o(1)的时间复杂度进行curd。

js提供了两个具体的实现Map和Object,在具体问题中选哪个区别不大,具体区别可以参考objects_vs._maps

本文会讨论和语言无关的散列表和js中的实现。

散列表实现

具体可以参考这里

散列函数

通过数据得到在数组中存储位置的函数就是散列函数f(key),散列函数就是要将数据映射数组中的对应位置。

如果不同的值得到同一个哈希地址,这被称为哈希冲突,哈希冲突要尽量避免,但不能完全避免。

一个好的哈希函数应该对于每个值保存到每个位置上的概率是相等的,常用的构造哈希函数的方法包括

(1)除留取余法
取关键字被某个不大于哈希表长m的数p除后所得余数为哈希地址。即:f(key)=key % p, p≤m;

(2)直接定址法
指取关键字或关键字的某个线性函数值为哈希地址。即: f(key)=key 或者 f(key)=a*key+b

(3)数字分析法
假设关键字是以为基的数(如以10为基的十进制数),并且哈希表中可能出现的关键字都是事先知道的,则可以选取关键字的若干位数组成哈希表。

处理哈希冲突

如果发生了冲突,即,计算出来的位置已经保存了数据,当前数据应该保存在哪里?

(1)开放定址法
当发生地址冲突时,按照某种方法继续探测哈希表中的其他存储单元,直到找到空位置为止。
这里的某种方法比如不断+1,或者再用一个散列函数计算一次。

(2)建立公共溢出区
专门维护一个溢出表,存放位置被占的数据。

(3)链地址法 如果遇到冲突,就把当前元素插入到目标位置上的链表中,极端情况下,所有的数据会保存在同一个位置的链表中,查找复杂度为o(n)。

java中的hashMap就是使用的这种方式

如果单纯解决这种方式o(n)的极端情况可以使用avl或红黑树代替链表,时间复杂度降为o(lgn)。

js中hash表的实现

js作为一种动态语言,属性的保存位置只有在运行到特定代码时才知道,因此不能像java那样在对象创建时就明确各键值对的位置和分配空间大小。

js中的具体实现取决于各个js引擎(主流引擎实现方式类似),即使用了Shape和inline cache

在js中的对象是key到property attribute的映射,除了key对应的值以外还有其他attribute,比如[[Enumerable]]、[[Configurable]]等,因此这里要解决两个问题。

  • 怎么快速查找对应的属性值
  • 怎么减少数据冗余

Shape

js引擎为解决以上问题提供了Shape,其中保存了属性对应的attribute和保存的位置(offset)。

相同的对象共用同一个Shape,相同的标准即对应属性添加顺序相同。

如果在一个对象上另外添加属性,Shape就会生成transition chains,在这个链上的新Shape用来保存刚添加的属性。

image

Inline Caches

极端情况下,如果每次添加一个属性,每个属性的查找又变成了o(n),因此引入了inline cache。

每次调用一个函数,并将一个对象作为参数时会沿transition chains将对应属性的位置缓存起来,下一次直接读取,大大提高了访问速度。

相关例题

散列表的使用特别多,可以用来统计次数等,可以直接使用map或object,比如两数之和