JavaScript数据结构与算法 - 链表

1. 链表介绍

  • 链表存储有序的元素集合
  • 链表中的元素在内存中并不连续放置
  • 每个元素由一个存储元素本身的节点和一个指向下一个元素的引用(也称指针或链接)组成
    在这里插入图片描述

与数组相比:

  • 链表在添加或移除时不需要移动其他元素
  • 链表使用需要指针
  • 在数组中可以直接访问任何位置的任何元素,而链表中想访问链表中间的任一元素,需要从表头开始迭代链表直到找到所需的元素

2. 单链表

2.1 创建链表

// 作为默认的相等性比较函数
function defaultEquals(a, b) {
    return a === b;
}

// 助手类,表示链表中的第一个及其他元素
class Node {
    constructor(element, next) {
        // 要添加到链表中的项
        this.element = element;
        // 指向链表中下一个元素的指针
        this.next = next;
    }
}

class LinkedList {
    // 可以自行传入用于比较两个 JavaScript 对象或值是否相等的自定义函数。
    // 如果没有传入这个自定义函数,该数据结构将使用defaultEquals函数作为默认的相等性比较函数
    constructor(equalsFn = defaultEquals) {
        // count属性,用来存储链表中的元素数量
        this.count = 0;
        // 保存第一个元素的引用
        this.head = undefined;
        // 要比较链表中元素是否相等,需要使用一个内部调用的函数 equalsFn
        this.equalsFn = equalsFn;
    }
}

LinkedList类的方法:

  • push(element):向链表尾部添加一个新元素
  • insert(element, position):向链表的特定位置插入一个新元素
  • getElementAt(index):返回链表中特定位置的元素。如果链表中不存在这样一个元素,返回undefined
  • remove(element):从链表种移除一个元素
  • indexOf(element):返回元素在链表中的索引。如果链表种没有该元素就返回-1
  • removeAt(position):从链表的特定位置移除一个元素
  • isEmpty():如果链表中不包含任何元素,返回true,如果链表长度大于0,返回false
  • size():返回链表包含的元素个数,与数组的length属性类似
  • toString():返回整个链表的字符串。由于列表项使用了Node类,需要重写继承自JavaScript对象默认的toString方法,让其只输出元素的值

2.2 向链表尾部添加元素

两种场景:

  1. 链表为空,添加第一个元素
    在这里插入图片描述
  2. 链表不为空,向其追加元素
    在这里插入图片描述
push(element) {
    // 把element作为值传入,创建Node项
    const node = new Node(element);
    let current;
    // 向空数组添加一个元素
    if (this.head == null) {
        // 让head指针指向node元素
        this.head = node;
    } else { // 追加元素
        // 只有第一个元素的引用
        current = this.head;
        // 获取最后一项
        while (current.next != null) {
            current = current.next;
        }
        // 将其next赋为新元素,建立链接
        current.next = node;
    }
    // 递增链表长度
    this.count++;
}

链表最后一个节点的下一个元素始终是undefined或null。


2.3 从链表中移除元素

从特定位置移除一个元素(removeAt)。存在两个场景:

  1. 移除第一个元素
  2. 移除除第一个元素之外的其他元素
    在这里插入图片描述
removeAt(index) {
    // 检查越界值
    if (index >= 0 && index < this.count) { // 需要得到移除的index的位置,先验证该index是有效的
        // 用current创建一个对链表中的第一个元素的引用
        let current = this.head;
        // 移除第一项
        if (index === 0) {
            // 移除第一项,就是让head指向列表的第二个元素
            this.head = current.next;
        } else { // 移除链表中最后一个或中间某个元素
            // 对当前元素的前一个元素的引用
            let previous;
            // 迭代链表的节点,直到达到目标位置
            for (let i = 0; i < index; i++) {
                previous = current;
                // 对循环列表当前元素的引用
                current = current.next;
            }
            // 将previous与current的下一项链接起来:跳过current,从而移除它。当前节点会被丢弃在计算机内存内,等着被垃圾回收器清除
            previous.next = current.next;
        }
        this.count--;
        return current.element;
    }
    return undefined;
}

2.4 循环迭代链表到目标位置

getElementAt(index) {
    if (index >= 0 && index <= this.count) {
        // 初始化node变量,从链表的第一个元素head开始
        let node = this.head;
        for (let i = 0; i < index && node != null; i++) {
            node = node.next;
        }
        return node;
    }
    return undefined;
}

  • 重构removeAt方法

使用getElementAt方法重构remove方法来移除元素。

remove(index) {
    if (index >= 0 && index < this.count) {
        let current = this.head;
        if (index === 0) {
            this.head = current.next;
        } else {
            // 用getElementAt重构remove方法
            const previous = this.getElementAt(index - 1);
            current = previous.next;
            previous.next = current.next;
        }
        this.count--;
        return current.element;
    }
    return undefined;
}

2.5 在任意位置插入元素

  • 在链表第一个位置添加元素:
    current元素是对链表中第一个元素的引用。将node.next的值设为current(链表中第一个元素),此时head和node.next都指向current,接下去将head的引用改为node,添加成功。
  • 在链表最后一个位置添加元素:
    previous是对最后一个元素的引用,current将为undefined。此时node.next指向current,previous.next指向node,添加成功。
  • 在链表中间添加元素:
    将新元素node插入previous和current之间,先将node.next的值指向current,再把previous.next的值设为node,添加成功。
insert(element, index) {
    if (index >= 0 && index <= this.count) {
        const node = new Node(element);
        // 在第一个位置添加
        if (index === 0) {
            // 对第一个元素的引用
            const current = this.head;
            node.next = current;
            this.head = node;
        } else {
            // 迭代数组找到目标位置
            const previous = this.getElementAt(index - 1);
            const current = previous.next;
            node.next = current;
            previous.next = node;
        }
        this.count++;
        return true;
    }
    // 越界了就返回false
    return false;
}

2.6 返回一个元素的位置

indexOf()方法接收一个元素的值,日过在链表中找到了它,就返回元素的位置,否则返回-1。

// 返回一个元素的位置
indexOf(element) {
    let current = this.head;
    for (let i = 0; i < this.count && current != null; i++) {
        // 验证current节点的元素和目标元素是否相等
        if (this.equalsFn(element, current.element)) {
            // 如果当前元素是要寻找的值,返回它的位置;否则迭代下一个节点
            return i;
        }
        current = current.next;
    }
    return -1;
}

2.7 从链表中移除元素

// 移除元素
remove(element) {
    const index = this.indexOf(element);
    return this.removeAt(index);
}

2.8 判空、链表元素个数、获取第一个元素

// 返回链表的元素个数
size() {
	return this.count;
}
// 链表中没有元素返回true,否则返回false
isEmpty() {
	return this.size() === 0;
}
// 获取类的第一个元素
getHead() {
	return this.head;
}

2.9 将LinkedList对象转换成一个字符串

toString() {
    // 链表为空,返回一个空字符串
    if (this.head == null) {
        return '';
    }
    // 用链表第一个元素的值来初始化方法最后返回的字符串
    let objString = `${this.head.element}`;
    let current = this.head.next;
    // 迭代其他元素,将元素值添加到字符串上
    // 如果链表只有一个元素,current != null不会执行
    for (let i = 1; i < this.size() && current != null; i++) {
        objString = `${objString}, ${current.element}`;
        current = current.next;
    }
    return objString;
}

3. 双向链表

在这里插入图片描述

3.1 创建链表

优势:单向链表中,如果迭代中错过了要找的元素,要回到起点重新开始迭代。

在单链表的基础上:

import LinkedList from '单链表文件路径';

export default class DoublyLinkedList extends LinkedList {
	...
}

实现 DoublyLinkedList类:

// DoublyNode扩展了Node类,可以继承element和next属性
class DoublyNode extends Node {
    constructor(element, next, prev) {
        // 使用了继承所以需要在DoublyNode类的构造函数中调用Node的构造函数
        super(element, next);
        this.prev = prev; // 新增
    }
}
// 扩展LinkedList类
class DoublyLinkedList extends LinkedList {
    constructor(equalsFn = defaultEquals) {
        // 调用LinkedList的构造函数,他会初始化equalsFn、const和head属性
        super(equalsFn);
        // 保存对链表最后一个元素的引用
        this.tail = undefined; // 新增
    }
}

3.2 在任意位置插入新元素

区别:单向链表只要控制一个next指针,双向链表要同时控制next和prev两个指针。

insert(element, index) {
    if (index >= 0 && index <= this.count) {
        const node = new DoublyNode(element);
        let current = this.head;
        // 场景一:在双向链表的第一个位置插入一个新元素
        if (index === 0) {
            // 如果双向链表为空,将head和tail指向新节点
            if (this.head == null) { // 新增
                this.head = node;
                thia.tail = node;
            } else { // 不为空
                node.next = this.head;
                current.prev = node; // 新增
                this.head = node;
            }
        }
        // 场景二:在双向链表最后添加新元素
        else if (index === this.count) { // 最后一项,新增
            current = this.tail;
            current.next = node;
            node.prev = current;
            this.tail = node;
        }
        // 场景三:在双向链表中间插入新元素
        else {
            // 迭代链表,找到位置
            const previous = this.getElementAt(index - 1);
            current = previous.next;
            node.next = current;
            previous.next = node;
            current.prev = node; // 新增
            node.prev = previous; // 新增
        }
        this.count++;
        return true;
    }
    return false;
}

对insert和remove两个方法的改进:
在在结果为否的情况下,可以把元素插入双向链表的尾部。
如position大于length/2的时候,可以从尾部开始迭代。


3.3 从任意位置移除元素

removeAt(index) {
    if (index >= 0 && index < this.count) {
        let current = this.head;
        // 移除第一个元素
        if (index === 0) {
            this.head = current.next;
            // 如果只有一项,更新tail 新增
            if (this.count === 1) {
                this.tail = undefined;
            } else {
                this.head.prev = undefined;
            }
        }
        // 移除最后一个元素
        else if (index === this.count - 1) {
            current = this.tail;
            this.tail = current.prev;
            this.tail.next = undefined;
        }
        // 移除中间的某个元素
        else {
            // 迭代得到元素位置
            current = this.getElementAt(index);
            const previous = current.prev;
            // 将previous和current的下一项连接起来,跳过current
            previous.next = current.next;
            current.next.prev = previous; // 新增
        }
        this.count--;
        return current.element;
    }
    return undefined;
}

4. 循环链表

循环链表可以只有单向引用,也可以有双向引用。

最后一个元素指向下一个元素的指针不是undefined,是第一个元素head。

双向循环链表有指向head元素的tail.next和指向tail的head.prev。


4.1 创建CircularLinkedList类

// 不需要额外属性,直接扩展LinkedList类并覆盖需要改写的方法即可
class CircularLinkedList extends LinkedList {
    constructor(equalsFn = defaultEquals) {
        super(equalsFn);
    }
}

4.2 在任意位置插入元素

insert(element, index) {
    if (index >= 0 && index <= this.count) {
        const node = new Node(element);
        let current = this.head;
        // 场景一:在循环链表第一个位置插入新元素
        if (index === 0) {
            // 链表为空
            if (this.head == null) {
                // 将head赋值为新创建的元素
                this.head = node;
                // 将最后一个节点链接到head
                node.next = this.head; // 新增
            }
            // 链表不为空
            else {
                node.next = current;
                current = this.getElementAt(this.size());
                // 更新最后一个元素
                this.head = node;
                current.next = this.head; // 新增
            }
        }
        // 场景二:在链表中间插入元素
        else { // 这种场景没有变化
            const previous = this.getElementAt(index - 1);
            node.next = previous.next;
            previous.next = node;
        }
        this.count--;
        return true;
    }
    return false;
}

4.3 从任意位置移除元素

从循环链表中移除元素,只需要考虑修改循环链表的head元素的情况。

removeAt(index) {
    if (index >= 0 && index < this.count) {
        let current = this.head;
        if (index === 0) {
            if (this.size() === 1) {
                this.head = undefined;
            } else {
                const removed = this.head;
                current = this.getElementAt(this.size()); // 新增
                this.head = this.head.next;
                current.next = this.head;
                current = removed;
            }
        } else {
            // 不需要修改循环链表最后一个元素
            const previous = this.getElementAt(index - 1);
            current = previous.next;
            previous.next = current.next;
        }
        this.count--;
        return current.element;
    }
    return undefined;
}

5. 有序链表

有序链表是保持元素有序的链表结构。

将元素插入到正确的位置来保证链表的有序性。


5.1 创建SortedLinkedList类

function defaultCompare(a, b) {
  if (a === b) {
    return Compare.EQUALS;
  }
  return a < b ? Compare.LESS_THAN : Compare.BIGGER_THAN;
}
const Compare = {
    LESS_THAN: -1,
    BIGGER_THAN: 1
};
function defaultCompare(a, b) {
    // 如果元素有相同的引用,就返回0
    if (a === b) {
        return 0;
    }
    // 如果第一个元素小于第二个元素,返回-1,否则返回1
    return a < b ? Compare.LESS_THAN : Compare.BIGGER_THAN;
}
class SortedLinkedList extends LinkedList {
    constructor(enqualsFn = defaultEquals, compareFn = defaultCompare) {
        super(enqualsFn);
        // 该类需要一个用来比较元素的函数,所以需要声明compareFn
        this.compareFn = compareFn;
    }
}

如果用于比较的元素更加复杂,可以创建自定义的比较函数并将它传入SortedLinkedList类的构造函数中。


5.2 有序插入元素

覆盖insert方法:

// 不允许在任意位置插入元素,所以要给index参数设置一个默认值,以便直接调用list.insert(element)而无需传入参数
insert(element, index = 0) {
    if (this.isEmpty()) {
        return super.insert(element, 0);
    }
    // 链表不为空,会知道插入元素的正确位置,并调用LinkedList的insert方法,传入该位置保证链表的有序
    const pos = this.getIndexNextSortedElement(element);
    return super.insert(element, pos);
}
getIndexNextSortedElement(element) {
    let current = this.head;
    let i = 0;
    for (; i < this.size(); i++) {
        // 传入比较的数组
        const comp = this.compareFn(element, current.element);
        // 插入的元素小于current的元素时,就找到了插入的位置
        if (comp === Compare.LESS_THAN) {
            return i;
        }
        current = current.next;
    }
    // 返回有序链表的长度
    return i;
}

6. 创建StackLinkedList类

使用LinkedList类及其变种作为内部的数据结构来创建其他数据结构,如栈、队列、双向队列。

创建栈数据结构:

class StackLinkedList {
    constructor() {
        // 使用DoublyLinkedList来存储数据
        this.items = new DoublyLinkedList();
    }
    push(element) {
        this.items.push(element);
    }
    pop() {
        if (this.isEmpty()) {
            return undefined;
        }
        return this.items.removeAt(this.size() - 1);
    }
}
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值