• JavaScript数据结构之链表


    1 数组与链表的优缺点

    链表和数组一样,都可以用于存储一系列的元素,但是链表和数组的实现机制完全不同。

    一般情况下,要存储多个元素,数组可能是最常用的数据结构。但是使用数组存储有一些缺点:

    • 数组的创建需要申请一段连续的内存空间(一整块的内存),并且大小是固定的,所以当当前数组不能满足容量需求时,就需要扩容(一般情况下会申请一个更大的数组,比如2倍,然后将原数组中的元素复制过去)。
    • 在数组的开头或中间位置插入数据的成本很高,需要进行大量元素的位移。

    存储多个元素的另一个选择就是链表,但是不同于数组,链表中的元素在内存中不必是连续的空间,链表的每个元素由一个存储元素本身的节点指向下一个元素的引用(指针)组成。

    那么和数组相比,链表有一些优势:

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

    链表也有一些缺点:

    • 链表访问任何一个元素的位置时,都需要从头开始访问,并且无法跳过第一个元素访问任何一个元素
    • 链表无法通过下标直接访问元素,需要从头一个个访问,直到找到对应的元素

    2 什么是链表

    链表的每个元素由一个存储元素本身的节点指向下一个元素的引用(指针)组成。

    它类似于火车,火车头就是头节点,火车车厢之间的连接类似于指针,火车上的乘客类似于数据。
    在这里插入图片描述
    接下来我们根据它的特性来手动封装一个链表。

    3 封装链表结构

    首先我们来封装一个链表类LindedList,用来表示我们的链表结构。LindedList类中应该有两个属性,链表的头节点head和链表的长度length

    function LinkedList() {
        this.head = null; // 初始指向null
        this.length = 0; // 初始链表长度为0
    }
    
    • 1
    • 2
    • 3
    • 4

    LindedList类内部有一个ListNode类,用于创建节点,创建节点时应该为节点传入数据data,并且该节点有指向下一个节点的指针。

    function LinkedList() {
        this.head = null; // 初始指向null
        this.length = 0; // 初始链表长度为0
    
        function ListNode(data) {
            this.data = data;
            this.next = null;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    到这里链表的基础结构就完成了,接下来我们来封装链表的方法。

    4 向链表尾部添加一个新的项

    向链表尾部添加一个新的节点,有两种情况:

    • 当链表本身为空时,头节点就是新添加的节点
    • 当链表不为空时,让链表最后一个节点指向新添加的节点

    根据这个思路,我们可以实现append方法如下:

    LinkedList.prototype.append = function (data) { // data是新节点的值
        let node = new ListNode(data); // 创建新的节点
        if (this.length === 0) { // 如果链表为空,则新节点就是头节点
            this.head = node;
        } else { // 如果链表不为空,新节点添加到链表尾部
            let current = this.head; // 将current指向头节点
            // 链表无法直接访问到最后的节点,只能通过一次次遍历来访问
            while (current.next) { // 当达到最后一个节点时,循环结束
                // 当下一个节点存在时,就让current指针移动到下一个节点上
                current = current.next;
            }
            // 最后一个节点指向新节点
            current.next = node;
        }
        this.length += 1; // 链表的长度+1
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    5 向链表某个位置插入一个新的项

    在链表的任意位置插入节点时,也分为两种情况:

    • 当插入到第一个位置时,新节点变成了头节点,那么新节点要指向原来的头节点,属性head也应该变成新节点
    • 当插入到其他位置时,首先通过循环找到该位置,同时保存上一个节点和下一个节点,然后将上一个节点指向新节点,新节点指向下一个节点

    插入insert方法代码实现如下:

    // position为节点要插入的位置,data为节点的值
    LinkedList.prototype.insert = function (position, data) {
        // 对position进行越界判断,当该值小于0或者大于链表长度时,不能进行插入操作
        if (position <= 0 || position > this.length) return false;
        let node = new ListNode(data); // 创建新节点
        if (position === 0) { // 如果节点要插入第一个位置
            node.next = this.head; // 新节点指向原来的头节点
            this.head = node; // 头节点修改为新节点
        } else {
            let previous = null; // 指向前一个位置
            let current = this.head; // 指向下一个位置
            let index = 1; // 记录循环的位置
            // 循环结束,previous和current之间就是插入的节点
            while (index < position) {
                previous = current;
                current = current.next;
                index++;
            }
            previous.next = node; // 在正确的位置插入元素
            node.next = current;
        }
        this.length += 1; // 长度加1
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    6 获取对应位置的元素

    获取某个位置上的元素,也要通过循环链表来找到当前元素,get方法实现如下:

    LinkedList.prototype.get = function (position) {
        // 越界判断,如果位置小于0或者大于链表长度,不能获取到元素
        if (position <= 0 || position > this.length) return null;
        let index = 1; // 记录当前位置
        let current = this.head; // current指向头节点
        // 循环结束,current指向该位置上的节点
        while (index < position) {
            current = current.next;
            index++;
        }
        return current.data;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    7 获取元素在链表中的索引

    获取索引时,要循环遍历链表,将链表的每一个节点值都和给定的值比较,如果相等返回当前节点的位置,如果没找到,则返回-1。indexOf方法实现如下:

    // 获取某个元素的位置
    LinkedList.prototype.indexOf = function (data) {
        let current = this.head;
        let index = 1; // 记录元素的位置
        while (current) { // 循环遍历链表
            if (current.data === data) { // 如果当前节点的值等于元素的值
                return index; // 返回位置
            } else { // 如果不等于,继续循环
                current = current.next;
                index++;
            }
        }
        return -1; // 循环结束了,说明没找到
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    8 修改某个位置的元素

    修改某个位置的元素时,循环遍历链表,找到给定的位置,修改该位置上元素的值。update方法实现如下:

    // 修改某个位置的元素
    LinkedList.prototype.update = function (position, data) {
        // 越界判断
        if (position <= 0 || position > this.length) return false;
        let current = this.head;
        let index = 1;
        while (index < position) {
            current = current.next;
            index++;
        }
        current.data = data; // 修改数据
        return true;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    9 从链表中删除某位置节点

    删除某个位置上的节点分为两种情况:

    • 删除第一个位置上的节点时,要将第一个位置上的节点指向null,并且第二个位置上的节点成为头节点
    • 删除其他位置上的节点,循环找到该位置,同时记录该节点上一个节,将上一个节点指向该位置的下一个节点

    删除某位置节点removeAt方法实现如下:

    LinkedList.prototype.removeAt = function (position) {
        if (position <= 0 || position > this.length) return false; // 越界判断
        let current = this.head;
        if (position === 1) { // 如果删除第一个位置上的节点(头节点)
            this.head = this.head.next;
        } else { // 删除其他位置的节点
            let index = 1; // 记录当前位置
            let previous = null;
            while (index < position) {
                previous = current;
                current = current.next;
                index++;
            }
            // 上一个节点指向当前元素的下一个节点
            previous.next = current.next;
        }
        this.length--;
        return current; // 返回被删除的节点
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    10 全部代码

    function LinkedList() {
        this.head = null; // 初始指向null
        this.length = 0; // 初始链表长度为0
    
        function ListNode(data) { // 创建新节点类
            this.data = data;
            this.next = null;
        }
    
        // 添加元素
        LinkedList.prototype.append = function (data) { // data是新节点的值
            let node = new ListNode(data); // 创建新的节点
            if (this.length === 0) { // 如果链表为空,则新节点就是头节点
                this.head = node;
            } else { // 如果链表不为空,新节点添加到链表尾部
                let current = this.head; // 将current指向头节点
                // 链表无法直接访问到最后的节点,只能通过一次次遍历来访问
                while (current.next) { // 当达到最后一个节点时,循环结束
                    // 当下一个节点存在时,就让current指针移动到下一个节点上
                    current = current.next;
                }
                // 最后一个节点指向新节点
                current.next = node;
            }
            this.length += 1; // 链表的长度+1
        }
    
        // 插入元素
        // position为节点要插入的位置,data为节点的值
        LinkedList.prototype.insert = function (position, data) {
            // 对position进行越界判断,当该值小于0或者大于链表长度时,不能进行插入操作
            if (position <= 0 || position > this.length) return false;
            let node = new ListNode(data); // 创建新节点
            if (position === 0) { // 如果节点要插入第一个位置
                node.next = this.head; // 新节点指向原来的头节点
                this.head = node; // 头节点修改为新节点
            } else {
                let previous = null; // 指向前一个位置
                let current = this.head; // 指向下一个位置
                let index = 1; // 记录循环的位置
                // 循环结束,previous和current之间就是插入的节点
                while (index < position) {
                    previous = current;
                    current = current.next;
                    index++;
                }
                previous.next = node; // 在正确的位置插入元素
                node.next = current;
            }
            this.length += 1; // 长度加1
        }
    
        // 获取某个位置上的元素
        LinkedList.prototype.get = function (position) {
            // 越界判断,如果位置小于0或者大于链表长度,不能获取到元素
            if (position <= 0 || position > this.length) return null;
            let index = 1; // 记录当前位置
            let current = this.head; // current指向头节点
            // 循环结束,current指向该位置上的节点
            while (index < position) {
                current = current.next;
                index++;
            }
            return current.data;
        }
    
        // 获取某个元素的位置
        LinkedList.prototype.indexOf = function (data) {
            let current = this.head;
            let index = 1; // 记录元素的位置
            while (current) { // 循环遍历链表
                if (current.data === data) { // 如果当前节点的值等于元素的值
                    return index; // 返回位置
                } else { // 如果不等于,继续循环
                    current = current.next;
                    index++;
                }
            }
            return -1; // 循环结束了,说明没找到
        }
    
        // 修改某个位置的元素
        LinkedList.prototype.update = function (position, data) {
            // 越界判断
            if (position <= 0 || position > this.length) return false;
            let current = this.head;
            let index = 1;
            while (index < position) {
                current = current.next;
                index++;
            }
            current.data = data; // 修改数据
            return true;
        }
    
        LinkedList.prototype.removeAt = function (position) {
            if (position <= 0 || position > this.length) return false; // 越界判断
            let current = this.head;
            if (position === 1) { // 如果删除第一个位置上的节点(头节点)
                this.head = this.head.next;
            } else { // 删除其他位置的节点
                let index = 1; // 记录当前位置
                let previous = null;
                while (index < position) {
                    previous = current;
                    current = current.next;
                    index++;
                }
                // 上一个节点指向当前元素的下一个节点
                previous.next = current.next;
            }
            this.length--;
            return current; // 返回被删除的节点
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
  • 相关阅读:
    SonarQube系列-架构与外部集成
    ES 8.x 向量检索性能测试 & 把向量检索性能提升100倍!
    简谈设计模式之建造者模式
    [JS] 网络请求相关
    东软云HIS医疗管理系统——技术栈【SpringBoot+Vue+MySQL+MyBatis】
    【数据结构】二叉树、堆
    【实例项目:基于多设计模式下的日志系统(同步&异步)】
    c++ 深度拷贝和浅拷贝
    操作系统相关
    2023-09-10 LeetCode每日一题(课程表 II)
  • 原文地址:https://blog.csdn.net/m0_46612221/article/details/123254663