通过举例来阐述单向链表

43 阅读1分钟

js 中是没有内置链表这种数据结构的,所以我们需要通过模拟来实现链表。如果不清楚链表定义的可以移步这儿

实现单向链表

用一张图来描述下单向链表 image.png

链表的重要特点就是他的 next 指针,因为链表访问元素是通过 next 指针去一个个寻找。

<script>
/* 数组:数组的创建通常需要申请一段连续的内存空间(一整块的内存)并且大小是固定的(大多数编程语言数组都是固定的),所以当当前数组不能满足容量需求时,需要扩容.(一般情况下是申请一个更大的数组,比如2倍.然后将原数组中的元素复制讨去)。扩容就很消耗性能
而且在数组开头或中间位置插入数据的成本很高,需要进行大量元素的位移.
尽管我们已经学过的JavaScript的Array类方法可以帮我们做这些事,但背后的原理依然是这样。

链表:类似于数组  非连续、非顺序的存储结构   链表类似于火车:有一个火车头,火车头会连接一个节点,节点上有乘客(类似于数据),并且这个节点会连接下一个节点,以此类推.

要存储多个元素,另外一个选择就是链表.
但不同于数组,链表中的元素在内存中 不 必 是  连续的空间.
链表的每个元素由一个存储元素本身的节点和一个指向下一个元素的引用(有些语言称为指针或者连接)组成.

相对于数组,链表有一些优点:
内存空间不是必须连续的.可以充分利用计算机的内存实现灵活的内存动态管理口链表不必在创建时就确定大小,并且大小可以无限的延伸下去.
链表在插入和删除数据时,时间复杂度可以达到O(1).相对数组效率高很多.

相对于数组,链表有一些缺点:
链表访问任何一个位置的元素时,都需要    从 头 开 始    访问.(无法跳过第一个元素访问任何一个元素).直到找到对应的元素.  
*/



//创建一个链表构造函数(链表类)
class Linked {
    constructor(data) {
        this.length = 0; // 链表总长度
        this.head = null; // 头 也就是第一项 
    };
    // 在创建一个构造链表数据的函数 类也可以 es5的构造函数也行(这样就不用手写return) 这里使用es6 
    #Node(data) {
        return {
            data, // 新增的数据
            next: null  // 指向下一个数据的指针(最后一个节点的指针 就指向null(如果只有一个 那么第一个就是最后一个))
        }
    };
    add(data) {
        // 1.创建新节点
        const newNode = this.#Node(data); // 私有方法 实现
        // const newNode = new Node(data); // 外部类 实现


        // 2.判断当前添加的节点是不是第一个节点
        if (this.length === 0) {
            //链表为空的时候 就是第一个
            this.head = newNode;
        } else {
            //有数据的时候 不是第一个  链表每次查找只能从第一个查找
            let current = this.head; // 只要是查找 就从第一个开始
            // while 循环会一直循环代码块,只要指定的条件为 true
            while (current.next) {
                current = current.next; // 只要有下一个 就把当前的指针替换成下一个
            }
            //current.next为null while 循环结束 说明 找到了最后一个  因为current.next 不存在了 就把新数据加到尾巴上
            current.next = newNode;
        }
        // 3.更新长度
        this.length += 1;
    };

    // toString()
    toString(data) {
        let current = this.head;
        let result = "";
        while (current) {
            result += current.data + "";
            current = current.next;
        }
        return result;
    };


    // insert 既然是插入 肯定要指定一个插入的位置 即position
    insert(position, data) {
        // 1. 边界判断
        if (position < 0 || position > this.length) return;
        // 2.创建新元素
        const newNode = this.#Node(data);
        // 3.添加
        // 添加到第一个
        if (position === 0) {
            newNode.next = this.head; // 新元素成为第一个之后  原来的head就成了第二个
            this.head = newNode;  // 新元素成了 head
        } else {
            //添加到其他位置
            let index = 0,
                prev = null,
                current = this.head; // 老规矩 只不过是从头再来
            while (index++ < position) {
                /*
              思路:"天", "行", "健" ,"君", "子"   是一个链表,他们对应的下标(index)分别是0,1,2,3,4我们现在往第4个位置插入新元素(即‘君’字后面) 
              重点:这个时候,我们需要在 ‘君’ 和 ‘子’的中间 插入新元素。那么 根据链表的feture,新元素的next指针应该指向 ‘子’  ,并且  ‘君’ 的next指针需要指向新元素!!!  也就是新元素承上启下,隔断‘君’ 和 ‘子’原来的next指针
              那么 我们需要同时保存两个变量   一个是上一个变量的值(‘君’ 元素 ),一个是当前元素
              为什么需要当前元素:不用一个变量来保存那个值(我们需要插入的新元素后面的值你不留着 后面怎么链接起来呢???)都丢失了后半段都找不到了好吗?
              为什么需要上一个变量的值:你成功的把 newNode 的 next指针 指向了后面的元素,但是人家原来的链接并没有断开啊,兄弟!! ‘君’ 原来就指向 ‘子’,现在 newNode 横插一脚,横刀夺爱,抢夺了‘子’,  newNode 的 next指针 指向了‘子’,但是‘子’的前任 (‘君’) 还留着链接‘子’的钥匙,就问你怕不怕!!  所以 newNode 不仅对‘子’ 建立链接,还要砍断‘君’ 和 ‘子’的链接。  很简单:你保存上一个元素(‘君’),你把他的 next指针 插道你身上不久完事了吗  她的锅  你来背  从此关上了 ‘君’ 直接通向 ‘子’ 的大门。做啥事都先通过你的把关。
              */
                prev = current; // 保存前任的信息
                current = current.next; // 挨次挨次赋值 就像接受安检一样
            }
            prev.next = newNode; // 阻断 前任对她的联系,转到自己身上 严格把控
            newNode.next = current; // 建立自己的联系,宣布主权
            /* 上面的情况即使你插入的是末尾 也没有问题  next指针 就是指向null */
        }
        // 4. length 更新
        this.length++;
    };


    // get
    get(position) {
        // 边界判断
        if (position < 0 || position >= this.length)
            return "你获取了个寂寞!";
        let index = 0,
            current = this.head;
        while (index++ < position) {
            current = current.next;
        }
        return current.data;
    };
    // indexOf
    indexOf(data) {
        let current = this.head;
        let index = 0;
        // 找得到
        while (current) {
            if (current.data === data) {
                return index;
            }
            current = current.next;
            index++;
        }
        // 找不到
        return -1;
    };
    // update
    update(position, data) {
        if (position < 0 || position >= this.length) return false;
        let index = 0,
            current = this.head;
        //  找到元素
        while (index++ < position) {
            current = current.next;
        }
        // 改变值
        current.data = data;
        return true;
    };
    // removeAt
    removeAt(position) {
        if (position < 0 || position >= this.length) return null;
        let index = 0,
            current = this.head,
            prev = null;
        // 判断是不是第一个
        if (position === 0) {
            this.head = this.head.next; // 删除得事第一个 直接干掉  老皇帝死了 儿子继承皇位 带上帽子(head)继续统领群臣
            return current.data
        } else {
            while (index++ < position) {
                prev = current;
                current = current.next;
            }
            console.log('prev',prev);
            console.log('current',current);
            
            
            // 上一个 的指针 直接链接到 下下个数据  当前数据就被孤立了  js对于没有引用的数据会回收
            prev.next = current.next;
            return current.data
        }
        this.length -= 1;
    };

    // remove
    remove(data) {
        const position = this.indexOf(data);
        return this.removeAt(position);
    };
    // isEmpty  判断链表是否为空
    isEmpty() {
        return this.length === 0;
    };
    // size     查看链表的长度
    size() {
        return this.length;
    };
}


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


const myLinkedList = new Linked();
myLinkedList.add("天");
myLinkedList.add("行");
myLinkedList.add("健");
myLinkedList.add("君");
myLinkedList.add("子");
myLinkedList.add("以");
myLinkedList.add("自");
myLinkedList.add("强");
myLinkedList.add("不");
myLinkedList.add("息");
myLinkedList.insert(0, "<<易传>>:");
myLinkedList.insert(4, ",");
myLinkedList.insert(12, "。地势坤");
console.log('myLinkedList', myLinkedList);
console.log("myLinkedList.toString", myLinkedList.toString());
console.log("myLinkedList.length", myLinkedList.length);
console.log("myLinkedList.get", myLinkedList.get(-1));
console.log("myLinkedList.get", myLinkedList.get(100));
console.log("myLinkedList.get", myLinkedList.get(0));
console.log("myLinkedList.indexOf", myLinkedList.indexOf("君"));
console.log("myLinkedList.indexOf", myLinkedList.indexOf('22'));
console.log("myLinkedList.update", myLinkedList.update(0, "xx"));
console.log("myLinkedList.removeAt", myLinkedList.removeAt(0));
console.log("myLinkedList.removeAt", myLinkedList.removeAt(10));
console.log("myLinkedList.insert", myLinkedList.toString());
</script>