林秀栋的技术博客

前端数据结构(上)

文章来源

简介

八大常见的数据结构

  1. 数组:Array
  2. 堆栈:Stack
  3. 队列:Queue
  4. 链表:Linked Lists
  5. 树:Trees
  6. 图:Graphs
  7. 字典树:Trie
  8. 散列表(哈希表):Hash Tables

上面的结构,在较高的层次上,可合为三种类型:

  1. 堆栈和队列是类似于数组的结构,仅在项目的插入和删除方式上有所不同。
  2. 链表,树,和图 结构的节点是引用到其他节点。
  3. 散列表依赖于散列函数来保存和定位数据。

在复杂性方面:

在效率方面:

数组:Array

数组是最简单的数据结构。 贴一张每个函数都运行10,000次迭代:

10,000个随机密钥在10,000个对象的数组中查找的执行效率对比图:

[
  {
    id: "key0",
    content: "I ate pizza 0 times"
  },
  {
    id: "key1",
    content: "I ate pizza 1 times"
  },
  {
    id: "key2",
    content: "I ate pizza 2 times"
  },
  ...
]

["key284", "key958", "key23", "key625", "key83", "key9", ... ]

01

for… in为何这么慢?

for… in语法令人难以置信的缓慢。在测试中就已经比正常情况下慢近9倍的循环。

这是因为for … in语法是第一个能够迭代对象键的JavaScript语句。

循环对象键({})与在数组([])上进行循环不同,

因为引擎会执行一些额外的工作来跟踪已经迭代的属性。

堆栈:Stack

02

堆栈是元素的集合,可以在顶部添加项目,我们有几个实际的堆栈示例:

浏览器历史记录 撤消操作 递归以及其它。

两句话解释堆栈:

  1. 两个原则操作:push和pop。Push 将元素添加到数组的顶部,而Pop将它们从同一位置删除。
  2. 遵循”Last In,First Out”,即:LIFO,后进先出。

堆栈的实现。

请注意,下方例子中,我们可以颠倒堆栈的顺序:底部变为顶部,顶部变为底部。

因此,我们可以分别使用数组unshift和shift方法代替push和pop。

class Stack {
    constructor(...items) {
        this.reverse = false;
        this.stack = [...items];
    }

    push(...items) {
        return this.reverse
            ? this.stack.unshift(...items)
            : this.stack.push(...items);
    }
    
    pop() {
        return this.reverse ? this.stack.shift() : this.stack.pop();
    }
}

const stack = new Stack(4, 5);
stack.reverse = true;
console.log(stack.push(1, 2, 3) === 5) // true
console.log(stack.stack === [1, 2, 3, 4, 5]) // true

队列:Queue

在计算机科学中,一个队列(queue)是一种特殊类型的抽象数据类型或集合。集合中的实体按顺序保存。

03

在前端开发中,最著名的队列使用当属浏览器/NodeJs中 关于宏任务与微任务。

在后端领域,用得最广泛的就是消息队列:Message queue:如RabbitMQ、ActiveMQ等。

以编程思想而言,Queue可以用两句话描述:

  1. 只是具有两个主要操作的数组:unshift和pop。
  2. 遵循”Fist In,first out”即:FIFO,先进先出。

04

队列的实现

请注意,下方例子中,我们可以颠倒堆队列的顺序。

因此,我们可以分别使用数组unshift和shift方法代替push和pop。

class Queue {
    constructor(...items) {
        this.reverse = false;
        this.queue = [...items];
    }

    enqueue(...items) {
        return this.reverse
            ? this.queue.push(...items)
            : this.queue.unshift(...items);
    }

    dequeue() {
        return this.reverse ? this.queue.shift() : this.queue.pop();
    }
}

链表:Linked Lists

与数组一样,链表是按顺序存储数据元素。

链表不是保留索引,而是指向其他元素。

05

第一个节点称为头部(head),而最后一个节点称为尾部(tail)。

单链表与双向链表:

链表的优点:

链表的应用场景:

06

单链表实现

单链表的操作核心有:

class Node {
  constructor(data) {
    this.data = data
    this.next = null
  }
}

class LinkedList {
  constructor() {
    this.head = null
    this.tail = null
    // 长度非必要
    this.length = 0
  }
  push(data) {
    // 创建一个新节点
    const node = new Node(data)
    // 检查头部是否为空
    if (this.head === null) {
      this.head = node
      this.tail = node
    } 
    this.tail.next = node
    this.tail = node
    this.length++
  }
  pop(){
    // 先检查链表是否为空
    if(this.isEmpty()) {
      return null
    } 
    // 如果长度为1
    if (this.head === this.tail) {
      this.head = null
      this.tail = null
      this.length--
      return this.tail
    }
    let node = this.tail
    let currentNode = this.head
    let penultimate
    
    while (currentNode) {
      if (currentNode.next === this.tail) {
        penultimate = currentNode
        break
      }
      currentNode = currentNode.next
    }
    
    penultimate.next = null
    this.tail = penultimate
    this.length --
    return node
  }
  
  get(index){
    // 处理边界条件
    if (index === 0) {
      return this.head
    }
    if (index < 0 || index > this.length) {
      return null
    }
    
    let currentNode = this.head
    let i = 0
    while(i < index) {
      i++
      currentNode = currentNode.next
    }
    return currentNode
    
  }
  delete(index){
    let currentNode = this.head
    
    if (index === 0) {
      let deletedNode
      currentNode.next = this.head
      deletedNode = currentNode
      this.length--
      
      return deletedNode
    }
    
    if (index < 0 || index > this.length) {
      return null
    }
    
    let i = 0
    let previous
    
    while(i < index) {
      i++
      previous = currentNode
      currentNode = currentNode.next
    }
    previous.next = currentNode.next
    this.length--
    return currentNode
  }
  
  isEmpty() {
    return this.length === 0
  }
  print() {
    const list = []
    let currentNode = this.head
    while(currentNode){
      list.push(currentNode.data)
      currentNode = currentNode.next
    }
    return list.join(' => ')
  }
}

// 测试一下
const l = new LinkedList()

// 添加节点
const values = ['A', 'B', 'C']
values.forEach(value => l.push(value))

console.log(l)
console.log(l.pop())
console.log(l.get(1))
console.log(l.isEmpty())
console.log(l.print())

结果:

07

双向链表实现

双向链表的设计

类似于单链表,双向链表由一系列节点组成。每个节点包含一些数据以及指向列表中下一个节点的指针和指向前一个节点的指针。这是JavaScript中的简单表示:

一个简单的双向链表

08

class Node {
    constructor(data) {
        // data 包含链表项应存储的值
        this.data = data;
        // next 是指向列表中下一项的指针
        this.next = null;
        // prev 是指向列表中上一项的指针
        this.prev = null;
    }
}

class DoublyLinkedList {
    constructor() {
        this.head = null;
        this.tail = null;
    }
    // 各种操作方法
    // 在链表的尾部添加节点
    append( item ) {
        let node = new Node( item );
        if(!this.head) {
            this.head = node;
            this.tail = node;
        } else {
            node.prev = this.tail;
            this.tail.next = node;
            this.tail = node
        }
    }

    // 在链表的指定位置添加节点
    appendAt( pos, item ) {
        let current = this.head;
        let counter = 1;
        let node = new Node( item );
        if( pos == 0 ) {
            this.head.prev = node
            node.next = this.head
            this.head = node
        } else {
            while(current) {
            current = current.next;
            if( counter == pos ) {
                node.prev = current.prev
                current.prev.next = node
                node.next = current
                current.prev = node
            }
            counter++
            }
        }
    }

    // 在链表的尾部删除节点
    remove( item ) {
        let current = this.head;
        while( current ) {
            if( current.data === item ) {
            if( current == this.head && current == this.tail ) {
                this.head = null;
                this.tail = null;
            } else if ( current == this.head ) {
                this.head = this.head.next
                this.head.prev = null
            } else if ( current == this.tail ) {
                this.tail = this.tail.prev;
                this.tail.next = null;
            } else {
                current.prev.next = current.next;
                current.next.prev = current.prev;
            }
        }
        current = current.next
        }
    }

    // 在链表的指定位置删除节点
    removeAt( pos ) {
        let current = this.head;
        let counter = 1;
        if( pos == 0 ) {
            this.head = this.head.next;
            this.head.prev = null;
        } else {
            while( current ) {
                current = current.next
                if ( current == this.tail ) {
                    this.tail = this.tail.prev;
                    this.tail.next = null;
                } else if( counter == pos ) {
                    current.prev.next = current.next;
                    current.next.prev = current.prev;
                    break;
                }
                counter++;
            }
        }
    }

    // 翻转双向链表
    reverse(){
        let current = this.head;
        let prev = null;
        while( current ){
            let next = current.next
            current.next = prev
            current.prev = next
            prev = current
            current = next
        }
        this.tail = this.head
        this.head = prev
    }

    // 两节点间交换
    swap( nodeOne, nodeTwo ) {
        let current = this.head;
        let counter = 0;
        let firstNode;
        while( current !== null ) {
            if( counter == nodeOne ){
                firstNode = current;
            } else if( counter == nodeTwo ) {
                let temp = current.data;
                current.data = firstNode.data;
                firstNode.data = temp;
            }
            current = current.next;
            counter++;
        }
        return true
    }

    // 查询是否为空或长度
    length() {
        let current = this.head;
        let counter = 0;
        while( current !== null ) {
            counter++
            current = current.next
        }
        return counter;
    }
    isEmpty() {
        return this.length() < 1
    }

    // 遍历链表
    traverse( fn ) {
        let current = this.head;
        while( current !== null ) {
            fn(current)
            current = current.next;
        }
        return true;
    }

    // 查找节点的索引
    search( item ) {
        let current = this.head;
        let counter = 0;
        while( current ) {
            if( current.data == item ) {
                return counter
            }
            current = current.next
            counter++
        }
        return false;
    }
}

const doublyLinkedList = new DoublyLinkedList();

树:Tree

计算机中经常用到的一种非线性的数据结构——树(Tree),由于其存储的所有元素之间具有明显的层次特性,因此常被用来存储具有层级关系的数据,比如文件系统中的文件;也会被用来存储有序列表等。

树的分类

常见的树分类如下,其中我们掌握二叉搜索树即可。

树的应用

二叉树:Binary Search Tree

09

二叉树的遍历

按一定的规则和顺序走遍二叉树的所有结点,使每一个结点都被访问一次,而且只被访问一次,这个操作被称为树的遍历,是对树的一种最基本的运算。

由于二叉树是非线性结构,因此,树的遍历实质上是将二叉树的各个结点转换成为一个线性序列来表示。

按照根节点访问的顺序不同,二叉树的遍历分为以下三种:前序遍历,中序遍历,后序遍历;

前序遍历:Pre-Order

根节点->左子树->右子树

10

中序遍历:In-Order

左子树->根节点->右子树

11

后序遍历:Post-Order

左子树->右子树->根节点

12

二叉树的实现

class Node { 
    constructor(data) { 
        this.left = null
        this.right = null
        this.value = data
    }
}

class BST {
    constructor() {
        this.root = null
    }
    // 二叉树的各种操作
    // 插入新子节点/节点
    insertNode(root, newNode) {
        if (newNode.value < root.value) {
                // 先执行无左节点操作
                (!root.left)
                    ? root.left = newNode 
                    : this.insertNode(root.left, newNode)
            } else {
                (!root.right)
                    ? root.right = newNode 
                    : this.insertNode(root.right, newNode)
        }
    }
    insert(value) {
        let newNode = new Node(value)
        // 如果没有根节点
        if (!this.root) {
            this.root = newNode
        } else {
            this.insertNode(this.root, newNode)
        }
    }

    // 移除子节点/节点
    removeNode(root, value) {
        if (!root) {
            return null
        }
        
        // 从该值小于根节点开始判断
        if (value < root.value) {
            root.left = this.removeNode(root.left, value)
            return root
        } else if (value > root.value) {
            root.right = tis.removeNode(root.right, value)
            return root
        } else {
            // 如果没有左右节点
            if (!root.left && !root.right) {
                root = null
                return root
            }
            
            // 存在左节点
            if (root.left) {
                root = root.left
                return root
            // 存在右节点
            } else if (root.right) {
                root = root.right
                return root
            }
            
            // 获取正确子节点的最小值以确保我们有有效的替换
            let minRight = this.findMinNode(root.right)
            root.value = minRight.value
            // 确保删除已替换的节点
            root.right = this.removeNode(root.right, minRight.value)
            return root
        }
    }
    remove(value) {
        if (!this.root) {
            return 'Tree is empty!'
        } else {
            this.removeNode(this.root, value)
        }
    }

    // 获取子节点的最小值
    findMinNode(root) {
        if (!root.left) {
            return root
        } else {
            return this.findMinNode(root.left)
        }
    }

    // 查找子节点/节点
    searchNode(root, value) {
        if (!root) {
            return null
        }
        
        if (value < root.value) {
            return this.searchNode(root.left, value)
        } else if (value > root.value) {
            return this.searchNode(root.right, value)
        }
        
        return root
    }
    search(value) {
        if (!this.root) {
            return 'Tree is empty'
        } else {
            return Boolean(this.searchNode(this.root, value))
        }
    }

    // Pre-Order:前序遍历
    preOrder(root) {
        if (!root) {
            return 'Tree is empty'
        } else {
            console.log(root.value)
            this.preOrder(root.left)
            this.preOrder(root.right)
        }
    }

    // In-Order:中序遍历
    inOrder(root) {
        if (!root) {
            return 'Tree is empty'
        } else {
            this.inOrder(root.left)
            console.log(root.value)
            this.inOrder(root.right)
        }
    }

    // Post-Order:后序遍历
    postOrder(root) {
        if (!root) {
            return 'Tree is empty'
        } else {
            this.postOrder(root.left)
            this.postOrder(root.right)
            console.log(root.value)
        }
    }
}

图:Graph

图是由具有边的节点集合组成的数据结构。图可以是定向的或不定向的。

图的应用

图用于不同的行业和领域:

图的构成

图表用于表示,查找,分析和优化元素(房屋,机场,位置,用户,文章等)之间的连接。

13

图的基本元素

符号和术语

有向图与无向图

有向图中,边具有方向。它们从一个节点转到另一个节点,并且无法通过该边返回到初始节点。

14

无向图中,边是无向的(它们没有特定的方向)。将无向边视为双向街道。您可以从一个节点转到另一个节点并返回相同的“路径”。

15

加权图

在加权图中,每条边都有一个与之相关的值(称为权重)。该值用于表示它们连接的节点之间的某种可量化关系。

例如:

16

稀疏图与密集图

当图中的边数接近最大边数时,图是密集的。

17

当图中的边数明显少于最大边数时,图是稀疏的。

18

循环

如果你按照图中的一系列连接,可能会找到一条路径,将你带回到同一节点。这就像“走在圈子里”,你走的路可以带你回到你的初始位置。

19

图的实现

我们将实现具有邻接列表的有向图。

class Graph {
    constructor() {
        this.AdjList = new Map();
    }
  
    // 基础操作方法
    // 添加顶点
    addVertex(vertex) {
        if (!this.AdjList.has(vertex)) {
            this.AdjList.set(vertex, []);
        } else {
            throw 'Already Exist!!!';
        }
    }

    // 添加边(Edge)
    addEdge(vertex, node) {
        // 向顶点添加边之前,必须验证该顶点是否存在。
        if (this.AdjList.has(vertex)) {
            // 确保添加的边尚不存在。
            if (this.AdjList.has(node)){
                let arr = this.AdjList.get(vertex);
                // 如果都通过,那么可以将边添加到顶点。
                if(!arr.includes(node)){
                    arr.push(node);
                }
            }else {
                throw `Can't add non-existing vertex ->'${node}'`;
            }
        } else {
            throw `You should add '${vertex}' first`;
        }
    }

    // 打印图(Graph)
    print() {
        for (let [key, value] of this.AdjList) {
            console.log(key, value);
        }
    }

}

// 测试
let g = new Graph();
let arr = ['A', 'B', 'C', 'D', 'E', 'F'];
for (let i = 0; i < arr.length; i++) {
    g.addVertex(arr[i]);
}
g.addEdge('A', 'B');
g.addEdge('A', 'D');
g.addEdge('A', 'E');
g.addEdge('B', 'C');
g.addEdge('D', 'E');
g.addEdge('E', 'F');
g.addEdge('E', 'C');
g.addEdge('C', 'F');
g.print();
/* PRINTED */
// A [ 'B', 'D', 'E' ]
// B [ 'C' ]
// C [ 'F' ]
// D [ 'E' ]
// E [ 'F', 'C' ]
// F []

如果只创建顶点

let graph = new Graph();
graph.addVertex('A');
graph.addVertex('B');
graph.addVertex('C');
graph.addVertex('D');

// 结果
/* Map {
  'A' => [],
  'B' => [],
  'C' => [],
  'D' => []
} */

上面创建顶点的结果之所以都为空数组’[]’,是因为数组中需要储存边(Edge)的关系。 例如下图:

20

该图的Map将为:

Map {
  'A' => ['B', 'C', 'D'],
  // B没有任何指向
  'B' => [],
  'C' => ['B'],
  'D' => ['C']
}

广度优先算法(BFS)实现

一种利用队列实现的搜索算法。简单来说,其搜索过程和 “湖面丢进一块石头激起层层涟漪” 类似。从起点出发,对于每次出队列的点,都要遍历其四周的点。

21

该算法的具体步骤为:

循环内部:

visited = {
    'A': true,
    'B': true,
    'D': true,
    'E': true
}
q = ['B', 'D', 'E']

具体实现:

createVisitedObject(){
  let arr = {};
  for(let key of this.AdjList.keys()){
    arr[key] = false;
  }
  return arr;
}

bfs(startingNode){
  let visited = this.createVisitedObject();
  let q = [];

  visited[startingNode] = true;
  q.push(startingNode);

  while(q.length){
    let current = q.pop()
    console.log(current);

    let arr = this.AdjList.get(current);

    for(let elem of arr){
      if(!visited[elem]){
        visited[elem] = true;
        q.unshift(elem)
      }
    }

  }
}

深度优先算法(DFS)实现

一种利用递归实现的搜索算法。简单来说,其搜索过程和 “不撞南墙不回头” 类似。从起点出发,先把一个方向的点都遍历完才会改变方向。

22

具体步骤:

createVisitedObject(){
    let arr = {};
    for(let key of this.AdjList.keys()){
        arr[key] = false;
    }
    return arr;
}

dfs(startingNode){
    console.log('\nDFS')
    let visited = this.createVisitedObject();
    this.dfsHelper(startingNode, visited);
}

dfsHelper(startingNode, visited){
    visited[startingNode] = true;
    console.log(startingNode);

    let arr = this.AdjList.get(startingNode);

    for(let elem of arr){
        if(!visited[elem]){
            this.dfsHelper(elem, visited);
        }
    }
}

doesPathExist(firstNode, secondNode){
    let path = [];
    let visited = this.createVisitedObject();
    let q = [];
    visited[firstNode] = true;
    q.push(firstNode);
    while(q.length){
        let node = q.pop();
        path.push(node);
        let elements = this.AdjList.get(node);
        if(elements.includes(secondNode)){
            console.log(path.join('->'))
            return true;
        }else{
            for(let elem of elements){
                if(!visited[elem]){
                    visited[elem] = true;
                    q.unshift(elem);
                }
            }
        }
    }
    return false;
    // 多了个大括号
    }
}

BFS 的重点在于队列,而 DFS 的重点在于递归。这是它们的本质区别。