数据结构与算法 - 链表

172 阅读1分钟

前言

你应该已经使用过数组了,push, pop,shift,unshift,这些方法能够很方便的让我们在末尾插入,末尾弹出,头部弹出,头部插入,但为了保证你在操作之后,所有的数据都维持一个正确的下标【如你在头部插入一个数据,原来的所有下标都要往后移动一位】,有些操作会很费性能。

举个例子

let len = 1e5;
let arr = [];
let t1 = performance.now();
for (let i = 0; i < len; i++) {
    // 末尾插入
    arr.push(i);
}

let t2 = performance.now();
for (let i = 0; i < len; i++) {
    // 头部弹出
    arr.shift();
}
let t3 = performance.now();

// 在我电脑上的结果是push: 1.899999976158142ms shift: 390.10000002384186ms
console.log(`push: ${t2 - t1}ms shift: ${t3 - t2}ms`);

结果显示,shift的时间,远超push,且在数据量更大时,这个差距也会更大,可见,数组在某些方面存在一定的局限性,下面我们再看一个题目

一个题目

有一个日志系统,数据会一直增加,而我只需要保存最近的100W条数据,数据量超100W时,保存最新数据,而舍弃最旧的数据,按照数组的写法,会是下面这个样子

let logs = [];
let limit = 1e6;

function addLog(data) {
    if (logs.length >= limit) {
        logs.shift();
    }
    logs.push(data);
}

这份代码就会出现我们上面提到的问题,分析一下,可以发现,在这个例子中,我们只关心两个量:数据总量【是否超过限制】和数据的顺序关系【那个数据在前,那个数据在后】,对比数组,还可以总结一点,就是:我们并不需要下标。
尝试一下实现这样一个数据结构。

实现

let logs = {
    len: 0,
    front: null,
    end: null
}
let limit = 1e6;

function addLog(data) {
    let newVal = {
        before: null,
        after: null,
        data: data
    }
    
    if (!logs.len) {
        // 没有数据时,设置头和尾
        logs.front = newVal;
        logs.end = newVal;
    } else {
        // 有数据时,在尾部插入数据
        logs.end.after = newVal;
        newVal.before = logs.end;
        logs.end = newVal;
    }
    logs.len++;
    
    // 数据超过限制,删除头部的数据
    if (logs.len > limit) {
        let newFront = logs.front.after;
        newFront.before = null;
        logs.front = newFront;
        logs.len--;
    }
}

总结

上面的代码,就是链表的核心:一个单纯的链式结构,只关心谁连着谁或者被谁连着
较数组的优势: 增加或删除数据
较数组的劣势: 没法直接通过下标快速获取数据

ps:

  • 以上实现并非标准的链表,在某些时候甚至还有bug【limit为0时没有清空front和end操作】
  • 标准的链表代码,网上太多了,这里就不贴了

下一章

栈,队列