复杂度分析

  • 大O复杂度表示法

    常见的有O(1), O(n), O(logn), O(nlogn)

    时间复杂度除了大O表示法外,还有以下情况
  • 最好情况时间复杂度
  • 最坏情况时间复杂度
  • 平均情况时间复杂度
  • 均摊时间复杂度

代码执行效率分析

大多数情况下,代码执行的效率可以采用时间复杂度分析,但是大O表示法通常会省略常数。

但是在工业级实现中,真正的执行效率通常会考虑多方面:

  • 时间复杂度
  • 空间复杂度
  • 缓存友好
  • 指令条数
  • 性能退化(如哈希冲突解决方案等)

以上是理论层级,对于前端开发来说,如果你开发的是基础库会被广泛使用时,只是基于理论的分析是不够的。

此时就可以借助一些基准库来对你的模块进行测试: benchmark.js

数据结构

1. 数组

  • 特点: 连续内存,支持下标访问,缓存友好,时间复杂度O(1)
  • 深入: 从JS继承链可以知道在JS中的Array数组继承于Object,在某些情况下数组会退化为哈希表结构[Dictionary]。
  • 思考: 考虑如下情况,浏览器会怎样存储该数组?
let myArray = [10000];
myArray[0] = 0;
myArray[9999] = 1;

2. 链表

  • 特点: 非连续内存,查找元素时间复杂度最坏情况O(n),最好情况O(1)。
  1. 单向链表
  2. 双向链表
  • 深入: 判断链表是否有环形?
  • 使用一个步进为1,和一个额外的步进为2的参数,如果在遍历完之前相遇,则有环。

3. 栈

  • 特点: 先进后出,后进先出,底层结构可以为数组或链表。
  • 场景: 函数调用时对当前执行环境进行入栈操作,调用完成后出栈恢复调用前上下文。
  • 深入: 递归调用时连续入栈操作,如果递归层级过深造成堆栈溢出。

    比如AngularJS中的脏值检查嵌套深度上线为10。
  • 思考: 浏览器的前进后退功能可否用栈实现?

    使前进栈和后退栈的双栈结构

4. 队列

  • 特点: 先进先出,底层结构可以为数组或链表。
  1. 无阻塞队列

    基于链表
  2. 阻塞队列

    基于数组的循环队列

    深入: JS中的Macro队列和Micro队列

5. 跳表

  • 底层为链表,为了增加查找效率,在链表基础上,以(2/3/4/../n)个节点增加一层查找链表

  • 特点: '支持区间查找',支持动态数据结构,需要额外的存储空间来存储索引链节点

  • 时间复杂度: 基于链表的查找时间复杂度由跳表层高决定

  • 场景: 如Redis中的有序集合

6. 散列表

  • 底层为数组结构,使用散列函数计算key将其映射到数组中的下标位置。
  • 散列函数要求:
  1. 散列值为非负整数
  2. key1 = key2, 则 hash(key1) = hash(key2)
  3. key1 != key2, 则 hash(key1) != hash(key2)
  • 常见的哈希算法有: MD5, SHA, CRC等。
  • 散列冲突:
  1. 开放寻址

    线性探测:散列冲突时,依次往后查找空闲

    双重散列: 使用多个散列函数,如果第一个散列值被占用,则用第二个散列值
  2. 链表法

    散列冲突时,在一级数据后链接一个链表结构。该链表可以是单/双链,或树形链表
  • 装载因子: 整个数组中已经被使用的位置/总位置
  • 动态扩容: 当装载因子达到某个限定值(如:0.75)时,对整个底层数组进行扩容

    注: 工业级实现中,动态扩容并不是一次完成的。

    一次完成的动态扩容会造成性能瓶颈,一般是在装载因子达到设定值时,申请一个新的数组。在后续的每次操作时,将一个数据从原数组搬到新数组,直到原数组为空。

JavaScript 对象数据结构

  1. 我在另一篇文章中有将到JS中的对象在底层存储的时候会有好几种模式,下面我们结合源码来详细分析一下
//https://github.com/v8/v8/blob/master/src/objects/js-objects.h
// 我们先来看JS中object的定义,继承自JSReceiver
// Line 278
class JSObject : public JSReceiver {
//省略...
} // Line 26
// 接下来再看,JSReceiver继承自HeapObject,并且有几个重要属性
// JSReceiver includes types on which properties can be defined, i.e.,
// JSObject and JSProxy.
class JSReceiver : public HeapObject {
public:
NEVER_READ_ONLY_SPACE
// Returns true if there is no slow (ie, dictionary) backing store.
// 是否有快速属性模式
inline bool HasFastProperties() const; // Returns the properties array backing store if it exists.
// Otherwise, returns an empty_property_array when there's a Smi (hash code) or an empty_fixed_array for a fast properties map.
// 属性数组
inline PropertyArray property_array() const; // Gets slow properties for non-global objects.
// 字典属性
inline NameDictionary property_dictionary() const; // Sets the properties backing store and makes sure any existing hash is moved
// to the new properties store. To clear out the properties store, pass in the
// empty_fixed_array(), the hash will be maintained in this case as well.
void SetProperties(HeapObject properties); // There are five possible values for the properties offset.
// 1) EmptyFixedArray/EmptyPropertyDictionary - This is the standard
// placeholder.
//
// 2) Smi - This is the hash code of the object.
//
// 3) PropertyArray - This is similar to a FixedArray but stores
// the hash code of the object in its length field. This is a fast
// backing store.
//
// 4) NameDictionary - This is the dictionary-mode backing store.
//
// 4) GlobalDictionary - This is the backing store for the
// GlobalObject. // 初始化属性
inline void initialize_properties();
  1. 由上可知对象有快速属性和字典属性两种模式,快速属性由数组存储,字典属性采用hash表存储。
  2. 快速属性这里不深入,我们接下来看看NameDictionary的底层结构
// https://github.com/v8/v8/blob/master/src/objects/dictionary.h
// 我们先来看看继承链
// Line 202
class V8_EXPORT_PRIVATE NameDictionary : public BaseNameDictionary<NameDictionary, NameDictionaryShape>{}
// Line 128
class EXPORT_TEMPLATE_DECLARE(V8_EXPORT_PRIVATE) BaseNameDictionary : public Dictionary<Derived, Shape> {}
// Line 26
class EXPORT_TEMPLATE_DECLARE(V8_EXPORT_PRIVATE) Dictionary : public HashTable<Derived, Shape> {} // 由上可知继承自HashTable,我们来看HashTable的定义
// https://github.com/v8/v8/blob/master/src/objects/hash-table.h
// 并且在文件开头的注释已经很详细了 // HashTable is a subclass of FixedArray that implements a hash table that uses open addressing and quadratic probing.
*重要: hash表使用数组为基础数据,并在其上实现了开放寻址和二次探测
// In order for the quadratic probing to work, elements that have not yet been used and elements that have been deleted are distinguished.
// Probing continues when deleted elements are encountered and stops when unused elements are encountered.
* 为了使二次探测工作正常,未使用/被删除的元素将被标记删除而不是直接删除
// - Elements with key == undefined have not been used yet.
// - Elements with key == the_hole have been deleted. // 以下会被使用的hash表派生类
// Line 292
template <typename Derived, typename Shape>
class EXPORT_TEMPLATE_DECLARE(V8_EXPORT_PRIVATE) ObjectHashTableBase
: public HashTable<Derived, Shape> {} 接下来我们看看V8在实现hash表时的几个重要行为和参数
// https://github.com/v8/v8/blob/master/src/objects/objects.cc 1. 扩容
// Line 7590
// 下面源代码是新增一个元素后的逻辑
ObjectHashTableBase<Derived, Shape>::Put(Isolate* isolate, Handle<Derived> table, Handle<Object> key, Handle<Object> value, int32_t hash) {
int entry = table->FindEntry(roots, key, hash);
// Key is already in table, just overwrite value.
// key已经存在,覆盖值
if (entry != kNotFound) {
table->set(Derived::EntryToValueIndex(entry), *value);
return table;
} // 如果33%以上元素都被删除了,考虑重新hash
// Rehash if more than 33% of the entries are deleted entries.
// TODO(jochen): Consider to shrink the fixed array in place.
if ((table->NumberOfDeletedElements() << 1) > table->NumberOfElements()) {
table->Rehash(roots);
}
// If we're out of luck, we didn't get a GC recently, and so rehashing isn't enough to avoid a crash.
// 如果没有足够的预估空位,按二倍大小进行重新hash
if (!table->HasSufficientCapacityToAdd(1)) {
int nof = table->NumberOfElements() + 1;
int capacity = ObjectHashTable::ComputeCapacity(nof * 2);
if (capacity > ObjectHashTable::kMaxCapacity) {
for (size_t i = 0; i < 2; ++i) {
isolate->heap()->CollectAllGarbage(
Heap::kNoGCFlags, GarbageCollectionReason::kFullHashtable);
}
table->Rehash(roots);
}
} // Line 6583.
// 下面是计算预估空位的逻辑
HashTable<Derived, Shape>::EnsureCapacity(Isolate* isolate, Handle<Derived> table, int n, AllocationType allocation) {
if (table->HasSufficientCapacityToAdd(n)) return table;
int capacity = table->Capacity();
int new_nof = table->NumberOfElements() + n;
const int kMinCapacityForPretenure = 256;
bool should_pretenure = allocation == AllocationType::kOld ||
((capacity > kMinCapacityForPretenure) &&
!Heap::InYoungGeneration(*table));
Handle<Derived> new_table = HashTable::New(
isolate, new_nof,
should_pretenure ? AllocationType::kOld : AllocationType::kYoung); table->Rehash(ReadOnlyRoots(isolate), *new_table);
return new_table;
} 2. 收缩
// Line 6622
HashTable<Derived, Shape>::Shrink(Isolate* isolate,
Handle<Derived> table,
int additionalCapacity) {
int capacity = table->Capacity();
int nof = table->NumberOfElements(); // Shrink to fit the number of elements if only a quarter of the capacity is filled with elements.
// 当只有1/4的装载量时,进行收缩
if (nof > (capacity >> 2)) return table;
// Allocate a new dictionary with room for at least the current number of
// elements + {additionalCapacity}. The allocation method will make sure that
// there is extra room in the dictionary for additions. Don't go lower than
// room for {kMinShrinkCapacity} elements.
int at_least_room_for = nof + additionalCapacity;
int new_capacity = ComputeCapacity(at_least_room_for);
if (new_capacity < Derived::kMinShrinkCapacity) return table;
if (new_capacity == capacity) return table; const int kMinCapacityForPretenure = 256;
bool pretenure = (at_least_room_for > kMinCapacityForPretenure) &&
!Heap::InYoungGeneration(*table);
Handle<Derived> new_table =
HashTable::New(isolate, new_capacity,
pretenure ? AllocationType::kOld : AllocationType::kYoung,
USE_CUSTOM_MINIMUM_CAPACITY); table->Rehash(ReadOnlyRoots(isolate), *new_table);
return new_table;
}

Web高级 JavaScript中的数据结构的更多相关文章

  1. Web高级 JavaScript中的算法

    算法 排序算法 稳定排序 待排序序列中相等元素在排序完成后,原有先后顺序不变. 非稳定排序 有序度 待排序序列中有序关系的元素对个数. 逆序度 1. 插入排序 遍历有序数组,对比待插入的元素大小,找到 ...

  2. JavaScript中的数据结构及实战系列

    本系列主要是讲解JavaScript中的数据结构及在实际项目中遇到的地方 JavaScript中的数据结构及实战系列(1):队列 JavaScript中的数据结构及实战系列(2):栈

  3. javascript中的数据结构

    Javascript中的关键字   abstract     continue      finally      instanceof      private       this boolean ...

  4. JavaScript中常见数据结构

    数据结构 栈:一种遵从先进后出 (LIFO) 原则的有序集合:新添加的或待删除的元素都保存在栈的末尾,称作栈顶,另一端为栈底.在栈里,新元素都靠近栈顶,旧元素都接近栈底. 队列:与上相反,一种遵循先进 ...

  5. JavaScript中的数据结构及实战系列(1):队列

    开题 张三丰教无忌太极剑法: 还记得吗? 全都记得. 现在呢? 已经忘却了一小半. 啊,已经忘了一大半. 不坏不坏,忘得真快,那么现在呢? 已经全都忘了,忘得干干净净. 好了,你上吧. 长时间写前端代 ...

  6. JavaScript中的数据结构及实战系列(2):栈

    开题: 不冒任何险,什么都不做,什么也不会有,什么也不是. 本文目录 栈介绍: JavaScript实现栈: 栈的应用: 栈介绍: 和队列一样,栈也是一种表结构,但是和队列的"先进先出&qu ...

  7. 在Object-C中学习数据结构与算法之排序算法

    笔者在学习数据结构与算法时,尝试着将排序算法以动画的形式呈现出来更加方便理解记忆,本文配合Demo 在Object-C中学习数据结构与算法之排序算法阅读更佳. 目录 选择排序 冒泡排序 插入排序 快速 ...

  8. 一篇文章把你带入到JavaScript中的闭包与高级函数

    在JavaScript中,函数是一等公民.JavaScript是一门面向对象的编程语言,但是同时也有很多函数式编程的特性,如Lambda表达式,闭包,高阶函数等,函数式编程时一种编程范式. funct ...

  9. 掌握javascript中的最基础数据结构-----数组

    这是一篇<数据结构与算法javascript描述>的读书笔记.主要梳理了关于数组的知识.部分内容及源码来自原作. 书中第一章介绍了如何配置javascript运行环境:javascript ...

随机推荐

  1. VB数据集

    dim Re as recordset dim rs1 as recordsetre.movelast '移动到数据表的最后re.movefirst '移动到数据表的最前re.movenext '移动 ...

  2. 1118. Birds in Forest (25)

    Some scientists took pictures of thousands of birds in a forest. Assume that all the birds appear in ...

  3. Linux中查看某 个软件的安装路径

    本人qq群也有许多的技术文档,希望可以为你提供一些帮助(非技术的勿加). QQ群:   281442983 (点击链接加入群:http://jq.qq.com/?_wv=1027&k=29Lo ...

  4. 造个自己的Vue的UI组件库类似Element

    前言 随着前端的三大框架的出现,组件化的思想越来越流行,出现许多组件库.它能够帮助开发者节省时间提高效率, 如React的Ant-design,Vue的iView,Element等,它们的功能已经很完 ...

  5. 2.k8s资源清单

    一.常见资源对象 常见的资源对象:(包括但不仅限于) l  Workload: Pod,ReplicaSet,Deployment,StatefulSet,DaemonSet,Job,Cronjob ...

  6. RPA走专有云还是公共云?阿里云RPA公共云给出了这样几组数据…

    自从近年国外UIPATH.AUTOMATION ANYWHERE.BLUE PRISM等RPA企业的崛起,Robotic Process Automation(RPA)走进了人们视野.它以节省成本.提 ...

  7. Windows上安装Apache

    1.下载 (1)进入Apache官网http://httpd.apache.org— (2)点击Download (3)点击Files for Microsoft Windows (4)点击Apach ...

  8. Redis-cli相关命令

    > flushdb > SELECT OK []> GET db_number (nil) []> []> flushdb OK []> SELECT OK > ...

  9. 设计模式学习笔记——Adapter 适配器模式

    适配器设计模式的适应场景: 一般情况是上端固定,下端固定,下端功能不满足或跟上端不协调,使用适配器重新包一层(继承适配器接口,以满足上端需求,继承下层类,以调用方法),使下端代码能满足上端需求(欺骗, ...

  10. 解决axios异步问题