js之数据结构

一,为什么要学习数据结构

前端开发的进阶。高级工程师的象征。

了解数据结构可以在开发中有效的利用合适的数据结构来处理代码,可以合理的利用内存、性能等,同时可以处理一些复杂的逻辑。

二,数据结构分类

js中自带的最简单的数据结构:数组Array、集合Set

类似数组的基础的数据结构:栈、队列

带有节点的数据结构:链表、树、图

依赖哈希函数的数据结构:哈希表(字典、散列)

三,数组Array

结构

js的数组是最简单的一种线性表的数据结构,他是用一组连续的空间来存储数据的。

特点

1,他的访问是高效的,可以通过索引来访问

2,他的插入和删除是低效的,因为插入或者删除一个值后,要将后面的所有的值都向前/向后移动一个位置

四,集合Set

结构

ES6新增的Set是一个无序的,没有重复值的数据集合,他类似堆栈和队列数据结构。也可以与数学中的集合相对比(二者类似)

特点

1,,Set采用了自己独特的算法判断两个值是否相等,而非===。可以用在对特定列表执行比较和判断是否相等

2,Set的存储空间占用比Array要少,同时Array和Set的转换也比较简单,因此可以用到 Array 的场合可以优先考虑一下 Set

3,支持类似数学中的集合操作求交集、并集、差集、判断子集

交集:A and B共同并集:A + B所有差集:only in A或only in B

五,栈(Stack)

先进后出(LIFO)有序集合。新的元素和待删除的元素都在栈顶,老的元素都在栈底。

(类似于叠起来的一堆书,要想拿最底下的一本书,需要将上面的书一个一个拿掉)

js中可以利用数组的push和pop方法来模拟实现一个栈:

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

    // 入栈
    set(item) {
        this.items.push(item);
    }

    // 出栈
    remove() {
        this.items.pop();
    }

    // 获取栈顶元素
    get() {
        return this.items[this.items.lnegth];
    }

    size() {
        return this.items.length;
    }

    // ... ...
};

// 调用栈
let s = new Stack();
s.push('hello');
s.push('world');
console.log(s.get());
s.remove();
console.log(s.get());

六,队列(Queue)

与栈相反,是先进先出(FIFO)的有序集合。(比如打饭的排队)

在js中同样可以利用数组的原生方法(push()、shift())来模拟实现。

针对默认的队列也增加了两个修改版本的队列。

优先队列

元素添加和移除的时候设置优先级。(比如医院排队,孕妇和老人优先通道...)

循环队列

为了充分利用空间,克服"假溢出"现象的方法,将队列的首尾相连,想象成一个圆环。(例如队列一共10个元素,访问索引11,即为访问第一个元素)

七,链表(Linked List)

与数组类似,也是一个有序的元素集合,不同的是链表的元素在内存中的存储并不是连续的,每个元素都由当前的节点和下一个节点的引用(指针)组成。

特点

1,在链表中插入和删除数据常量的时间,因为只更改指针即可;而在数据中插入或删除数据需要线性时间,因为后现的所有元素都需要移位。

2,数组访问一个元素可以直接访问到任何位置的元素;而链表访问一个元素,需要从头开始遍历,直到找到要找的元素

总结:

综合实际情况,访问数据更多可以用数据结构,操作数据(插入删除)更多,可以用链表

js同样可以通过用object设置current、next等引用属性值,来模拟实现一个链表。

// eg:追加一个元素
append(element) {
    const node = new Node(element)
    let current = null
    if (this.head === null) { // 如果链表还没有第一个元素,则当前元素赋值为第一个元素head
        this.head = node;
    } else {
        current = this.head; // 从头遍历
        while(current.next) {
            current = current.next; //一直找到最后一个元素(没有指向下一个元素的指针next)
        }
        current.next = node; // 赋值为下一个元素
        }
    this.length++;
}

// eg:在指定位置插入一个元素
insert(position, element) {
    if (0 < position <= this.items.length) {
        const node = new Node(element); // 要插入的元素节点
        let current = this.head;  // 设置当前的元素
        let previous = null; // 设置上一个元素(从头遍历)
        let index = 0;
        if (index === 0) { // 如果在第一个位置插入,则直接赋值为head元素
            this.head = node;
        } else {
            while (i++ < position) { // 循环遍历到指定位置
                privious = current; // 将当前的值赋值给上一个元素
                current = current.next; // 将当前值的下一个元素赋值给当前元素
            }
            // 找到了目标位置,修改目标位置的上一个元素和下一个元素的引用
            node.next = current; // 将当前节点的下一个元素引用赋值为当前元素
            privious.next = node; // 将当前节点赋值给上一个元素的next引用
        }
        this.length++;
        return true;
    }
    return false;
}

实际应用中,在此基础上链表又细分了双向链表和循环链表。

双向链表:就是每个节点同时又包含一上一个元素的引用。他的优势是可以支持倒着迭代链表

循环链表:他与普通链表的唯一区别就是最后一个元素的next不是null,而是指向第一个元素head循环链表同时也支持单项链表和双向链表

八,树(Tree)

概念:

树是一种非顺序的数据结构,类似于数组,但是他的父节点同时有多个子节点的引用。js中类似的树场景有html标签、原型继承、React组件。生活中的例子有公司组织架构图、族谱等。

细分:

二叉树

一种特殊的树结构,他的节点最多有2个子节点(左侧子节点、右侧子节点)。二叉树可以帮我们写出更高效的插入、删除、查询元素的算法,在计算机方面应用比较广泛。

二叉搜索树

属于二叉树的一种。左侧子节点的值必须<=父节点的值;右侧子节点的值必须>父节点的值

注意

在树中,节点称为“键”,而不是“项”。

树的遍历:

通常我们要访问树的某一个节点就需要通过遍历先找到节点。这里的遍历类别可以分为水平方向和垂直方向两大类。遍历方式可以分为中序遍历、先序遍历和后序遍历3种方式。

 中序:按照从小到大的顺序访问所有节点左子节点>父节点>右子节点

 先序:先访问根节点,再访问叶子节点父节点>左子节点>右子节点

 后序:先访问叶子节点,再访问根节点左子节点>右子节点>根节点

垂直方向(DPF-深度遍历):深度遍历通常使用递归算法。

// eg: 递归实现先序遍历
function preOrderTree(callback) {
    const orders = (node, callback) => {
        callback(node); // 先执行父节点
        orders(node.left, callback); // 递归执行左子节点
        orders(node.right, callback); // 递归执行右子节点
    }
    orders(this.root, callback); // 开始执行递归函数
}

水平方向(BFT-广度遍历):广度遍历,例如树的形状如果大于深度,通常使用迭代算法。这需要一个队列来追踪每次迭代的所有子节点。(迭代遍历可以利用js的数组来模拟实现)

// eg: 迭代实现先序遍历
function preOrderTree(root, res = []) {
    const stack = []; // 定义一个空栈
    if (root) {
        stack.push(root); // 如果根节点不为空,则压入栈,然后进入循环
    }

    // 先序遍历的顺序是中、左、右;因此入栈的顺序便是右、左、中
    while(stack.length) {
        const node = stack.pop(); // 取出栈顶元素
        if(!node) {
            // 栈顶元素为空,就取出前一个元素值放入数组,进行下一次循环
            res.push(stack.pop().val);
            continue;
        }

        // 如果栈顶元素不为空,就先压入栈右子节点
        if (node.right) stack.push(node.right);
        // 再压入栈左子节点
        if (node.left) stack.push(node.left);
        // 再压入栈当前节点
        stack.push(node);
        // 最后压入空节点,为了让叶子节点成为下一次循环的中间节点,保证先入栈中节点
        stack.push(null);
    };
    
    return res; // 直到stack栈为空,结束循环,返回数组res
}

九,图(Graph)

概念

图是网络结构的抽象模型,图是由一组边连接的节点。如果一个树可以自由的拥有多个父节点,那么这个树也变成了图。

特点

任何二元关系都可以用图来表示,一个多对多的关系。(比如各种社交网络、互联网本身、人的大脑等等)

相关概念:

相邻顶点:由一条边连接一起的顶点称为相邻顶点。

顶点的度:一个顶点的度是他相邻顶点的数量(比如A点和B、C、D三个顶点相邻,则A的度是3)

有向图和无向图:图可以是无向的(边没有方向)和有向的(无向图)

加权图与未加权图:大部分的图都是未加权图。如果边被赋予了权值,则为加权图

图的实现方法

图的数据结构有很多种实现方法,具体取决于要解决的问题类型。常见的有邻接矩阵,邻接表,关联矩阵

    邻接矩阵:(比较常见)通常用二维数组表示两个节点是否相邻,比如i和j节点相邻,则arr[i][j] === 1不相邻,则arr[i][j] === 0;但对于稀疏的图(非强链接)就会有很多0冗余浪费内存。

    邻接表:(大多数问题下很常用)由每个顶点和当前顶点的相邻顶点组成(可以用数组、链表、字典来表示)。

    关联矩阵:(用在顶点数量比边多的情况)矩阵的行表示顶点,列表示边。如果顶点 v 是边 e 的入射点,则 array[v][e] === 1; 否则,array [v][e] === 0

js模拟图的实现大概思路如下:

class Graph{
    constructor() {
        this.vertList = []; // 存放数组第一维度的顶点列表(可以知道一共有多少个顶点)
        this.adjList = new Dictionary(); // 假设定义了一个字典类,实例化一个字典列表
    }

    // 添加顶点
    addVert(v) {
        this.vertList.push(v); // push到顶点列表中
        this.adjList.set(v, []); // 字典中对该顶点设置空的相邻顶点列表
    }
    
    // 添加边(就是关联两个相邻顶点)
    addEdge(pointA, pointB) {
        this.adjList.get(pointA).push(pointB); // A顶点的相邻顶点列表中加入B顶点
        this.adjList.get(pointB).push(pointA); // B顶点的相邻顶点列表中加入A顶点
    }

    // 打印所有顶点的关系
    toString() {
        this.vertList.reduce((prevV, currV, index) => {
            // 当前顶点的相邻顶点字典列表
            const currAdjList = this.adjList.get(currV);
            // 为相邻顶点列表的reduce循环设置初始值为当前顶点
            const initAdVal = `${prevV}\n ${currV} => `;
            // 遍历并获取相邻顶点列表的各个顶点
            currAdjList.reduce((prevAd, currAd, indexAd) => {
                return `${prevAd}${currAd} `;
            }, initAdVal);
        });
    }
}

// 调用
const graph = new Graph();
['A', 'B', 'C', 'D', 'E'].forEach((item) => {
    graph.addVert(item); // 添加顶点
});
graph.addEdge('A', 'B'); // 为A顶点添加边
graph.addEdge('A', 'C');
graph.addEdge('A', 'D');
graph.addEdge('C', 'D');
graph.addEdge('C', 'E');

console.log(graph.toString());
/**
 * 输出
 * A => B C D
 * C => D E
 */

图的遍历

图和树的遍历类似,都有BFS(广度)和DFS(深度)。

具体的实践如果用到可自行学习网上参考资料。

十,哈希表(Hash Table)

概念:

理解:哈希表是类似于字典的一种结构,使用key、value的形式存储数据。每个键值对的存储位置由哈希函数来决定。

js原生的Set集合其实也属于哈希表的一种。其他哈希表还有字典(映射)、散列。js中的Map数据结构和Object都是哈希表的一种实现。

原理

哈希表是基于数组实现的。而哈希表是利用哈希函数将一个单词转换成了哈希化之后的数字,而把这个数字做成类似数组的下标。而我们访问一个值,比如名字,实际上这个对象同时也存储了下标的值,因此就可以通过这个名字直接找到对应的下标,从而访问数据。

特点:

优点

1,哈希表是为了解决数组的一些缺点来实现的数据结构。比如数组在已知下标的索引可以快速检索到值,但是如果下标未知(比如只知道员工的姓名,来查员工的信息),数组的性能就不及哈希表。

2,对于删除、插入数据,数组的效率也比较低,但是哈希表进行插入、删除等操作效率就非常高

缺点

1,哈希表的key值不能重复

2,数据是无序的,不能被遍历。

3,不能快速的找到最大、最小值

4,空间利用率不高,底层使用的数组可能并不是每个单元都被利用了

分类:

字典

如果说集合是{value: value}的形式存储,而字典是{key: value}的形式存储。js中的object对象就是典型的一个字典的实现。

散列

HashTable 类,也叫 HashMap 类。他提供了一个散列算法, 用来快速的获取一个值,实现方式就如上述原理描述,不需要遍历。

js模拟实现字典:和封装一个对象类基本一致。

js模拟实现散列:js中模拟散列HashTable类,就可以封装一个静态函数作为散列函数,将一个字符串转换成一个唯一的数值索引。注意的是在实现中删除元素,不要实际去从数组中删除,因为会造成元素移动。

class HashTable{
    
    constructor() {
        this.table = [];
    }

    // 散列函数
    static hashCodeClum(key) {
        let hash = 0;
        for (let codePoint of key) {
            hash += codePoint.charCodeAt()
        }
        return hash % 37;
    }

    // 添加和修改元素
    put(key, value) {
        const position = HashTable.hashCodeClum(key); // 通过散列函数获取索引
        this.table[position] = value;
    }

    // 获取元素
    get(key) {
        const position = HashTable.hashCodeClum(key);
        return this.table[position];
    }

    // 删除元素 
    remove(key) {
        const position = HashTable.hashCodeClum(key);
        this.table[position] = undefined;
    }
}

散列表和散列集合

散列表与散列一样,而散列集合是一个集合构成,也是使用的散列函数。

如果出现相同的散列值,就需要解决这种冲突通,常有两种方式:

分离链接(为散列表的每一个位置创建一个链表并将元素存储在里面

线性探查(插入一个元素时,如果索引为 index 的位置已经被占据了,就尝试 index+1的位置,以此类推

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

妍思码匠

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值