Web高級 JavaScript中的數據結構

複雜度分析

  • 大O複雜度表示法
    常見的有O(1), O(n), O(logn), O(nlogn)
    時間複雜度除了大O表示法外,還有如下狀況
  • 最好狀況時間複雜度
  • 最壞狀況時間複雜度
  • 平均狀況時間複雜度
  • 均攤時間複雜度

代碼執行效率分析

大多數狀況下,代碼執行的效率能夠採用時間複雜度分析,可是大O表示法一般會省略常數。
可是在工業級實現中,真正的執行效率一般會考慮多方面:javascript

  • 時間複雜度
  • 空間複雜度
  • 緩存友好
  • 指令條數
  • 性能退化(如哈希衝突解決方案等)

以上是理論層級,對於前端開發來講,若是你開發的是基礎庫會被普遍使用時,只是基於理論的分析是不夠的。
此時就能夠藉助一些基準庫來對你的模塊進行測試: 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)個節點增長一層查找鏈表java

  • 特色: '支持區間查找',支持動態數據結構,須要額外的存儲空間來存儲索引鏈節點
  • 時間複雜度: 基於鏈表的查找時間複雜度由跳錶層高決定
  • 場景: 如Redis中的有序集合git

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;
}
相關文章
相關標籤/搜索