算法笔记 - [数据结构之线性表结构<上>]

写在前面:本文为我的读书笔记,其间不免有一些我的不成熟观点,也不免有一些错误,慎之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策略算法
相关文章
相关标签/搜索