JavaScript 单向链表和双向链表的实现

58 阅读3分钟

1、链表简介

链表(Linked List)是一种常见的线性数据结构,它由一系列节点组成,每个节点包含两部分:数据和指向下一个节点的指针。

链表相对于数组而言,具有插入和删除操作高效、内存利用率高等优点。但由于链表中的元素不是连续存储的,因此在访问元素时需要从头节点开始遍历,导致访问效率较低。

本文将介绍如何在 JavaScript 中实现单向链表和双向链表。

2、单向链表

2.1 单向链表的定义

单向链表是最简单的链表类型。每个节点包含数据和一个指向下一个节点的指针。链表的最后一个节点指向 null。这种链表的特点是只能从头部向尾部遍历。

2.2 单向链表的节点类

首先,我们定义一个节点类 Node,用于表示链表中的每个节点。

class Node {
    constructor(data) {
        this.data = data; // 节点数据
        this.next = null; // 指向下一个节点的指针
    }
}

2.3 单向链表类

接下来,我们定义一个链表类 SinglyLinkedList,用于管理节点。

class SinglyLinkedList {
    constructor() {
        this.head = null; // 链表头
        this.size = 0;    // 链表大小
    }

    // 在链表末尾添加节点
    append(data) {
        const newNode = new Node(data);
        if (!this.head) {
            this.head = newNode;
        } else {
            let current = this.head;
            while (current.next) {
                current = current.next;
            }
            current.next = newNode;
        }
        this.size++;
    }

    // 在指定位置插入节点
    insertAt(data, index) {
        if (index < 0 || index > this.size) {
            return; // 索引无效
        }
        const newNode = new Node(data);
        if (index === 0) {
            newNode.next = this.head;
            this.head = newNode;
        } else {
            let current = this.head;
            let previous;
            let count = 0;
            while (count < index) {
                previous = current;
                current = current.next;
                count++;
            }
            newNode.next = current;
            previous.next = newNode;
        }
        this.size++;
    }

    // 删除指定位置的节点
    removeAt(index) {
        if (index < 0 || index >= this.size) {
            return null; // 索引无效
        }
        let current = this.head;
        if (index === 0) {
            this.head = current.next;
        } else {
            let previous;
            let count = 0;
            while (count < index) {
                previous = current;
                current = current.next;
                count++;
            }
            previous.next = current.next;
        }
        this.size--;
        return current.data;
    }

    // 打印链表
    print() {
        let current = this.head;
        let result = '';
        while (current) {
            result += current.data + ' -> ';
            current = current.next;
        }
        console.log(result + 'null');
    }
}

2.4 单向链表的使用示例

const list = new SinglyLinkedList();
list.append(10);
list.append(20);
list.append(30);
list.print(); // 10 -> 20 -> 30 -> null

list.insertAt(15, 1);
list.print(); // 10 -> 15 -> 20 -> 30 -> null

list.removeAt(2);
list.print(); // 10 -> 15 -> 30 -> null

3、双向链表

3.1 双向链表的定义

双向链表是每个节点都有两个指针,一个指向下一个节点,一个指向前一个节点。这种链表的特点是可以从头部向尾部遍历,也可以从尾部向头部遍历。

3.2 双向链表的节点类

我们定义一个双向节点类 DNode

class DNode {
    constructor(data) {
        this.data = data; // 节点数据
        this.next = null; // 指向下一个节点的指针
        this.prev = null; // 指向前一个节点的指针
    }
}

3.3 双向链表类

接下来,我们定义一个双向链表类 DoublyLinkedList

class DoublyLinkedList {
    constructor() {
        this.head = null; // 链表头
        this.tail = null; // 链表尾
        this.size = 0;    // 链表大小
    }

    // 在链表末尾添加节点
    append(data) {
        const newNode = new DNode(data);
        if (!this.head) {
            this.head = newNode;
            this.tail = newNode;
        } else {
            this.tail.next = newNode;
            newNode.prev = this.tail;
            this.tail = newNode;
        }
        this.size++;
    }

    // 在指定位置插入节点
    insertAt(data, index) {
        if (index < 0 || index > this.size) {
            return; // 索引无效
        }
        const newNode = new DNode(data);
        if (index === 0) {
            if (!this.head) {
                this.head = newNode;
                this.tail = newNode;
            } else {
                newNode.next = this.head;
                this.head.prev = newNode;
                this.head = newNode;
            }
        } else if (index === this.size) {
            this.append(data);
            return;
        } else {
            let current = this.head;
            let count = 0;
            while (count < index) {
                current = current.next;
                count++;
            }
            newNode.next = current;
            newNode.prev = current.prev;
            current.prev.next = newNode;
            current.prev = newNode;
        }
        this.size++;
    }

    // 删除指定位置的节点
    removeAt(index) {
        if (index < 0 || index >= this.size) {
            return null; // 索引无效
        }
        let current = this.head;
        if (index === 0) {
            this.head = current.next;
            if (this.head) {
                this.head.prev = null;
            } else {
                this.tail = null;
            }
        } else {
            let count = 0;
            while (count < index) {
                current = current.next;
                count++;
            }
            if (current.next) {
                current.next.prev = current.prev;
            } else {
                this.tail = current.prev;
            }
            if (current.prev) {
                current.prev.next = current.next;
            }
        }
        this.size--;
        return current.data;
    }

    // 打印链表
    headPrint() {
        let current = this.head;
        let result = '';
        while (current) {
            result += current.data + ' <-> ';
            current = current.next;
        }
        console.log(result + 'null');
    }

    // 打印链表
    tailPrint() {
        let current = this.tail;
        let result = '';
        while (current) {
            result += current.data + ' <-> ';
            current = current.prev;
        }
        console.log(result + 'null');
    }
}

3.4 双向链表的使用示例

const dList = new DoublyLinkedList();
dList.append(10);
dList.append(20);
dList.append(30);
dList.headPrint(); // 10 <-> 20 <-> 30 <-> null
dList.tailPrint(); // 30 <-> 20 <-> 10 <-> null

dList.insertAt(15, 1);
dList.headPrint(); // 10 <-> 15 <-> 20 <-> 30 <-> null
dList.tailPrint(); // 30 <-> 20 <-> 15 <-> 10 <-> null

dList.removeAt(2);
dList.headPrint(); // 10 <-> 15 <-> 30 <-> null
dList.tailPrint(); // 30 <-> 15 <-> 10 <-> null

4、总结

  • 单向链表:适用于需要频繁插入和删除操作的场景,时间复杂度为O(1)。
  • 双向链表:既可以从头遍历到尾,又可以从尾遍历到头,适用于需要双向访问的场景。

希望本文能帮助你理解链表的基本概念和实现方法。通过实践,你可以进一步掌握链表的使用和优化。