算法筆記 - [數據結構之線性表結構<上>]

寫在前面:本文爲我的讀書筆記,其間不免有一些我的不成熟觀點,也不免有一些錯誤,慎之javascript

前文連接:
算法筆記 -【複雜度分析】
算法筆記 -【複雜度分析<續>】java

何爲線性表?
線性表就是數據排成像一條線同樣的結構。每一個線性表上的數據最多隻有前、後兩個方向
常見的線性表結構的數據結構有:node

  • 數組
  • 鏈表
  • 隊列

下面一一作一下簡單的總結算法

數組

概念

數組是一種線性表數據結構,使用內存中一組連續的空間存儲相同數據類型的數據
注意其概念中線性表、連續、相同數據類型
盜個圖:
timg.jpg
鑑於數組的特性,數組適合哪些操做或者適合哪些場景呢?chrome

數組的優點

隨機訪問

數組在內存中就是一段連續的空間,並且每一項的數據類型相同也就意味着每一項所佔用的內存空間同樣,這樣要訪問數組的某一項i就能夠很容易得經過公式 base_address + data_type_size * i 計算出內存地址,直接讀取,經過複雜度分析的方法對整個訪問過程進行復雜度分析可知:數組訪問的複雜度爲O(1)
注:這裏的訪問是相似a[i]這樣對數組項的訪問取值,並不是從數組中查找某一項編程

數組的劣勢

插入操做

設想咱們在數組a的第k個位置插入一項x,來分析一下
最好狀況:k爲數組末尾,則直接插入便可,複雜度爲O(1)
最壞狀況:k爲數組首位,則須要將數組原有的n個元素向後挪動一位,而後將x插入,複雜度爲O(n)
通常狀況:根據指望平均複雜度的分析方法,很容易得出複雜度爲:O(n)segmentfault

若是數組是一個有序的集合,那必須按照以上方法進行插入,可是若是數組只是做爲一個存儲集合,是無序的,則能夠按照這樣的方法:
a中第k個元素移動到末尾,而後將x放在位置k,這樣能夠大大下降複雜度(此時複雜度爲O(1)api

刪除操做

設想咱們刪除數組a的第k個位置的元素
最好狀況:k爲數組末尾,則直接刪除,複雜度爲O(1)
最壞狀況:k爲數組首位,則須要將數組原有的n個元素向前挪動一位,複雜度爲O(n)
通常狀況:根據指望平均複雜度的分析方法,很容易得出複雜度爲:O(n)數組

若是某些場景不追求數組的連續性,則能夠先記錄下a中哪些項已被刪除,作一個標記,並不真正刪除,這樣就不須要對數組的元素進行搬移,直到數組的空間不足或者須要使用到被標記刪除的空間才進行真正刪除,將以前被標記的項刪除,並將數組項移動到正確位置,這樣就能夠將屢次刪除/搬移數組項的操做合併到一次進行操做,從而能夠提高性能瀏覽器

(上面提到的優化過程是否是很像v8引擎的標記清除的策略?)

javascript中的「數組」

前文已對數組作了淺顯介紹,但具體到javascript中呢?
javascript中的數組跟其餘編程語言的數組是同樣的嗎?

如下內容多爲其餘文章參考和測試,由於沒有結合引擎源碼(並且不一樣引擎的實現存在差別),因此不甚嚴謹和權威

javascript數組的概念

數組是值的集合,能夠是動態的,能夠是稀疏的,是javascript對象的特殊形式,數組元素能夠是任意類型(參考《javascript權威指南》)

其實javascript數組除了名字叫數組好像和「真正」的數組(或者說其餘語言的數組)沒什麼關係。

對於javascript開發者來講,瞭解常規數組有什麼意義呢?
其實很好理解,javascript代碼最終是要引擎實現的,瞭解常規數組的優/劣勢,對於優化javascript代碼有很重要的意義

v8引擎中javascript數組的實現

其實叫這個標題很慌,由於並無真正詳細得閱讀過v8源碼,不論如何,市面上很是多的參考文章結合實際測試仍是頗有參考意義的

引擎建立並處理js數組有三種模式

  1. Fast Elements 模式(快速模式)
  2. Fast Holey Elements 模式(快速空洞模式)
  3. Dictionary Elements 模式(字典模式)

引擎默認使用Fast Elements模式構建數組,這種模式最快速,
若是數組中存在空值(稀疏數組),將轉爲Fast Holey Elements模式,該模式下沒有賦值的位置將被存儲一個特殊的值,這樣在訪問該位置時將獲得undefined,
若是數組中保存的值爲特殊類型的值(如對象等),將轉爲Dictionary Elements模式
以上三種模式的特色在如下會有介紹

Fast Elements 模式

對於新建立的新數組,引擎會默認使用該模式,該模式由c數組實現,性能最好,該模式下引擎將爲數組分配連續的內存空間
固定長度大小數組、存儲相同類型值、索引大小不很大(具體與引擎實現有關)時引擎將以此種模式建立數組

Fast Holey Elements 模式

此模式適合於數組中只有某些索引存有 元素,而其餘的索引都沒有賦值的狀況。在 Fast Holey Elements 模式下,沒有賦值的數組索引將會存 儲一個特殊的值,這樣在訪問這些位置時就能夠獲得 undefined。可是 Fast Holey Elements 一樣會動態分配連 續的存儲空間,分配空間的大小由最大的索引值決定。

Dictionary Elements 模式

該模式下數組實際上就是使用 Hash 方式存儲。此方式最適合於存儲稀疏數組,它不用開闢大塊連續的存儲空 間,節省了內存,可是因爲須要維護這樣一個 Hash-Table,其存儲特定值的時間開銷通常要比 Fast Elements 模式大不少。
一種由 Fast Elements 轉換爲 Dictionary Elements 的典型狀況是對數組賦值時使用遠超當前數組大小的索引值,這時候要對數組分配大量空間則將可能形成存儲空間的浪費。在Fast Elements 模式下,若是數組內存佔用量過大,數組將直接轉化爲 Dictionary Elements 模式

測試實例

固定大小數組與非固定大小數組

// 非固定數組長度
function trendArray() {
    var tempArray = new Array();
    var i;
    for(i=0; i<100; i++){
        tempArray[i]= 1;
    }
}

// 固定數組長度
function fixedArray() {
    var tempArray = new Array(100);
    var i;
    for(i=0; i<100; i++){
        tempArray[i]= 1;
    }
}

對兩段代碼分別作100,0000次循環

console.time('trendArray')
for(let j = 0; j < 1000000; j++) {
    trendArray()
}
console.timeEnd('trendArray')

console.time('fixedArray')
for(let j = 0; j < 1000000; j++) {
    fixedArray()
}
console.timeEnd('fixedArray')
瀏覽器 非固定長度 固定長度
chrome(76.0.3809.132) 400ms左右 130ms左右
firefox(71.0 ) 930ms左右 690ms左右

測試能夠看出固定長度的數組在耗時上是頗有優點的,由於指定大小後,引擎不用頻繁更新數組的長度,減小了沒必要要的內存操做
對於非固定長度的數組進行一點改動

function trendArray1() {
    var tempArray = new Array();
    tempArray[99] = 1
    var i;
    for(i=0; i<100; i++){
        tempArray[i]= 1;
    }
}

// 測試
console.time('trendArray1')
for(let j = 0; j < 1000000; j++) {
    trendArray1()
}
console.timeEnd('trendArray1')

對非固定長度的數組在遍歷賦值以前作了一步操做:tempArray[99] = 1,最終測試結果:

瀏覽器 非固定長度 非固定長度,中途賦值 固定長度
chrome(76.0.3809.132) 400ms左右 140ms左右 130ms左右
firefox(71.0 ) 930ms左右 690ms左右 690ms左右

其實從測試中仍是可以推測出一些東西的:
固定長度的數組,引擎在建立數組的時候就會爲其分配一段連續的內存空間,以後每次只進行賦值操做就行;
非固定長度數組,引擎不清楚未來數組長度,可能避免內存空間浪費,將不對其分配連續內存空間,以後每次賦值,都會進行內存空間的拓展,形成性能的降低
非固定長度數組在指望末尾賦值,引擎一開始不清楚數組長度,而後進行末尾賦值以後會爲其分配一段連續的內存空間,以後的賦值操做便再也不進行空間拓展,至關於將空間拓展合併到一次完成
其實不管以上結論是否真實準確,從測試中也能夠得出結論:最好固定數組長度,這樣實打實能提高部分性能,另外不妨用複雜度分析的方法分析一下以上三種狀況引擎處理的複雜度

數組or對象
通常狀況下數組是不會有負數值索引的,但在js中,你甚至能夠爲數組添加負數值索引,但並不推薦這麼作,由於若是爲數組添加賦值索引,引擎會將其按照對象來進行處理,這樣引擎就沒法利用數組的特色進行線性存儲

function positiveArray() {
    var tempArray = new Array(5);
    var i;
    for(i = 0; i < 5; i++) {
        tempArray[i]= 1;
    }
}

function negativeArray() {
    var tempArray = new Array(5);
    var i;
    for(i = 0; i > -5; i--) {
        tempArray[i]= 1;
    }
}

一樣循環100,0000次

console.time('positiveArray')
for(let j = 0; j < 1000000; j++) {
    positiveArray()
}
console.timeEnd('positiveArray')

console.time('negativeArray')
for(let j = 0; j < 1000000; j++) {
    negativeArray()
}
console.timeEnd('negativeArray')

測試結果

瀏覽器 負數值索引 正數值索引
chrome(76.0.3809.132) 1000ms左右 20ms左右
firefox(71.0 ) 1000ms左右 300ms左右

結果顯而易見,因此避免負數值索引的使用
稀疏數組的代價

function continusArray() {
    var tempArray = [];
    var index = 0;
    for(var i = 0; i < 5; i++){
        tempArray[index]= 1;
        index +=1;
    }
}

function discontinuousArray() {
    var tempArray = [];
    var index = 0;
    for(var i = 0; i < 5; i++){
        tempArray[index]= 1;
        index +=20;
    }
}

一樣循環100,0000次

console.time('continusArray')
for(let j = 0; j < 1000000; j++) {
    continusArray()
}
console.timeEnd('continusArray')

console.time('discontinuousArray')
for(let j = 0; j < 1000000; j++) {
    discontinuousArray()
}
console.timeEnd('discontinuousArray')

測試結果:

瀏覽器 連續數組 不連續數組
chrome(76.0.3809.132) 40ms左右 170ms左右
firefox(71.0 ) 210ms左右 650ms左右

引擎須要對不連續的數組分配更多的內存空間,並且須要對「空洞」作特殊處理,這些都是額外耗時的操做,也會帶來性能損失

總結

引擎對js數組的實現是有不少優化的,整體來講是一個降級的過程:

  1. 固定長度、相同類型項的數組最接近c的實現,性能最好
  2. 稀疏數組會帶來必定性能損失
  3. 數組來講引擎會優先爲其分配連續的內存空間,相對對象的非連續存儲來講帶來性能提高

有了以上結論,在代碼實現上面就能夠針對性得進行優化。

鏈表

前一小結簡單介紹過數組,鏈表實際上是和數組相似得一種線性數據結構,可是它在內存中不是一段連續得存儲空間,它經過「指針」將不連續得一塊塊內存空間串聯起來存儲數據,上一個鏈表項會攜帶一個指針信息指向下一個鏈表項得內存地址;
鏈表示意圖:

鏈表的特色

在介紹數組的時候就有總結過數組的特色,從某種意義上來講鏈表和數組存在互補關係

鏈表的優勢

插入/刪除
鏈表的插入/刪除只須要改變一下鏈表項的指向便可,不會像數組那樣涉及到數據的搬運,其複雜度爲O(1)

隨機訪問
由於鏈表在內存中的保存並非一段連續的內存空間,並且每一項所佔用的空間也不固定,因此沒法像數組那樣計算出某一項的內存地址去直接訪問,因此訪問鏈表的某一項只能從頭開始一個個遍歷,其複雜度爲O(n)

鏈表在js中的實現

鏈表元素

class Node {
    constructor(val) {
        this.element = val // 數據
        this.next = null // 指針
    }
}

鏈表類

class LinkedList {
    constructor() {
        this._length = 0
        this.head = null
    }
}

指定位置後插入

insert(pos, element) {
    // 檢查越界
    if (pos < 0 || pos > this._length) return false
    const node = new Node(element)
    let index = 0, current = this.head, previous
    if (pos === 0) {
      node.next = current
      this.head = node
    }
    else {
      while (index < pos) {
        previous = current
        current = current.next
        index++
      }
      previous.next = node
      node.next = current
    }
    
    this._length += 1
    return true
  }

尾部插入

append(element) {
    return this.insert(this._length, element)
}

頭部插入

prepend(element) {
    const node = new Node(element)
    if (!this.head) {
      this.head = node
    } else {
      node.next = this.head
      this.head = node
    }
    this._length += 1
    return true
  }

刪除指定位置項

delete(pos) {
    // 檢查越界
    if (pos < 0 || pos >= this._length) return null
    let index = 0, current = this.head, previous
    // 移除第一項
    if (pos === 0) {
      this.head = current.next
      return current
    }
    while (index < pos) {
      previous = current
      current = current.next
      index += 1
    }
    previous.next = current.next
    this._length -= 1
    return current
  }

刪除頭部

shift() {
    if (!this.head) return null
    return this.delete(0)
}

刪除尾部

pop() {
    return this.delete(this._length)
}

刪除指定項

remove(element) {
    const index = this.indexOf(element)
    return this.delete(index)
}

返回鏈表項索引

indexOf(element) {
    if (!element) return -1
    let index = 0, current = this.head
    while(current) {
        if (current.element === element) return index
        index += 1
        current = current.next
    }
    return -1
}

判斷鏈表是否爲空

isEmpty() {
    return this._length === 0
}

返回鏈表長度

size() {
    return this._length
}

返回鏈表頭部元素

getHead() {
    return this.head
}

完整代碼

class Node {
  constructor(val) {
    this.element = val // 數據
    this.next = null // 指針
  }
}

class LinkedList {
  constructor() {
    this._length = 0
    this.head = null
  }
  insert(pos, element) {
    // 檢查越界
    if (pos < 0 || pos > this._length) return false
    const node = new Node(element)
    let index = 0, current = this.head, previous
    if (pos === 0) {
      node.next = current
      this.head = node
    }
    else {
      while (index < pos) {
        previous = current
        current = current.next
        index++
      }
      previous.next = node
      node.next = current
    }
    
    this._length += 1
    return true
  }
  append(element) {
    return this.insert(this._length, element)
  }
  prepend(element) {
    const node = new Node(element)
    if (!this.head) {
      this.head = node
    } else {
      node.next = this.head
      this.head = node
    }
    this._length += 1
    return true
  }
  delete(pos) {
    // 檢查越界
    if (pos < 0 || pos >= this._length) return null
    let index = 0, current = this.head, previous
    // 移除第一項
    if (pos === 0) {
      this.head = current.next
      return current
    }
    while (index < pos) {
      previous = current
      current = current.next
      index += 1
    }
    previous.next = current.next
    this._length -= 1
    return current
  }
  shift() {
    if (!this.head) return null
    return this.delete(0)
  }
  pop() {
    return this.delete(this._length - 1)
  }
  remove(element) {
    const index = this.indexOf(element)
    return this.delete(index)
  }
  indexOf(element) {
    if (!element) return -1
    let index = 0, current = this.head
    while (current) {
      if (current.element === element) return index
      index += 1
      current = current.next
    }
    return -1
  }
  isEmpty() {
    return this._length === 0
  }
  size() {
    return this._length
  }
  getHead() {
    return this.head
  }
}

以上即爲以javascript爲基礎實現的簡單的單向鏈表結構,固然還有雙向鏈表結構、循環鏈表結構,顧名思義,就是鏈表項相對單向多了向前的指向,首位相連。
那在javascript平常項目開發中有哪些應用場景呢?或者說鏈表結構適合進行哪些算法呢?

鏈表的一些簡單應用場景

在應用中常常會有相似:「歷史搜索」,「最近使用」等這樣的列表,這些列表通常都會有一個長度限制,那怎麼實現這樣的列表呢?

緩存算法

  1. FIFO(First In First Out)先進先出算法
  2. LFU(Least Frequently Used)最少使用算法
  3. LRU(Least Recently Used)最近最少使用算法

使用單向鏈表實現LRU算法

思路

根本徹底沒有必要記住這些算法的名字,只須要根據字面意思理解其算法思想便可,回到最初的問題,怎麼實現「最近使用」這樣的功能呢?

  1. 設想有一個鏈表q維護「最近使用」這樣的內容列表,其限制長度爲s,越靠近尾部的訪問時間越早則優先級越低,
  2. 當有一個新的數據x被訪問,此時遍歷q,若是x已經在q中保存,則將x移動到q的首部,若是x沒有在q中,則插入到q首部
  3. 在進行新數據x插入時檢查鏈表長度是否已經超出限制limit,若是超出限制,則將尾部項刪除
實現
class LRUList {
  constructor(limit = 10) {
    this._list = new LinkedList()
    this._limit = limit // 長度限制
  }
  add(element) {
    const size = this._list.size()
    if (this._list.indexOf(element) > -1) {
      this._list.remove(element)
    }
    else if (size === this._limit) { // 若是已經滿了
      this._list.pop() // 移除末尾
    }
    this._list.prepend(element) // 將新元素添加至鏈表首部
  }
  delete(element) {
    return this._list.delete(element)
  }
  find(element) {
    return this._list.indexOf(element)
  }
}

是否是很簡單?

思考

其實發現不管限制有沒有滿,咱們訪問緩存x都須要遍歷鏈表,因此複雜度爲O(n),那能不能優化呢?(答案是確定的,後面再說)
緩存的操做無非就是訪問、插入、刪除這些,而這些操做數組也是支持的,因此LRU數組也是徹底能夠實現的,可能在js中因爲引擎的優化以及提供豐富的api可能數組實現起來更加便利,但基於鏈表的實現也展現了一種選擇

總結

主要介紹記錄了數組、鏈表這兩種數據結構,同時有數組在js中的獨有特色、鏈表在js中的實現和應用:

  • 數組是在使用一段連續固定長度內存保存的同類型的數據片斷
  • 數組優點在於隨機訪問,劣勢是插入/刪除操做
  • js的數組不一樣於其餘更底層的編程語言,它能夠保存不一樣類型的數據,沒必要固定長度,長度隨時可擴展
  • js引擎對js數組的實現是一個降級的過程,致使js的數組在內存中可能並非一段連續的空間
  • js中引擎會對一些特定的數組進行優化,在應用中應儘量這樣使用
  • 鏈表是非連續、不固定長度、能夠存儲不一樣數據類型的一種數據結構,它能夠充分利用內存中的「碎片」空間,js中的數組其實更加相似鏈表
  • 鏈表優點在於插入/刪除操做,劣勢是隨機訪問
  • 在js中實現了簡單的鏈表結構,同時基於這個鏈表實現LRU策略算法
相關文章
相關標籤/搜索