学习JavaScript数据结构与算法(四)— 链表

205 阅读7分钟

前言

本人是一个刚入行的菜鸡前端程序员,写这个文章的目的只是为了记录自己学习的笔记与成果,如有不足请大家多多指点。
我们在前面已经学习了数组这种数据结构。数组(也可以称为列表)是一种非常简单的存储数据序列的数据结构。本篇我们一起学习如何实现和使用链表这种动态的数据结构,我们可以从中随意添加或移除项,它会按需进行扩容。

链表数据结构

链表存储有序的元素集合,链表中的元素在内存中并不是联系放置的。每个元素由一个存储元素本身的节点和一个指向下一元素的引用(称为指针或链接)组成。

相对于数组,链表在添加或移除元素的时候不需要移动其他元素。链表需要指针,在数组中我们可以直接访问任何位置的任何元素,而要想访问链表中间的一个元素,则需要从起点(表头)开始迭代链表直到找到所需要的元素。

创建链表

要表示链表中的第一个以及其他元素,我们需要一个助手类,叫做Node类。Node类表示我们想要添加到链表中的项。它包含一个 element 属性表示要加入链表元素的值;以及一个 next 属性指向链表中下一个元素的指针。

创建node类
class Node {
 constructor(element) {
     this.element = element;
     this.next = undefined //链表的最后一个节点的下一个元素始终是 undefined或null
 }   
}
创建链表类

我们要实现一个名为 indexOf 的方法,它使我们能够在链表中找到一个特定的元素。要比较链表中的元素是否相等,我们需要使用一个内部调用的函数。名为 equalsFn 。

class LinkedList {
    constructor() {
        this.count = 0; // 存储链表中的元素数量
        this.head = undefined; // 表示第一个元素的引用
        this.equalsFn = function (a,b) {
            return a === b;
        }
    }
}

链表中常用的方法

push(element) - 向链表尾部添加一个新元素

向 LinkedList 对象尾部添加一个元素时,可能有两种场景:链表为空,添加的是第一个元素;链表不为空,向其追加元素

push(element) {
    const node = new Node(element);
    let current; 
    if (this.head == null) { // 当head为null或undefined,向链表中添加第一项
        this.head = node;
    } else {
        current = this.head;
        while(current.next != null) { // 获取到链表的最后一项
            current = current.next; 
        }
        current.next = node;
    }
    this.count++
}
removeAt(index) - 从链表中移除元素

实现 removeAt 需要考虑两种情况,第一种是从特定的位置移除一个元素(removeAt(index)),第二种是根据元素的值移除元素(reomve())。
我们先实现第一种,从特定的位置移除一个元素。这种情况也存在两种场景:第一种是移除第一个元素,第二种是移除第一个元素之外的其他元素。

removeAt(index) {
    if(index >= 0 && index < this.count) {
        let current = this.head;
        if(index === 0) { // 移除第一项
            this.head = current.next;
        } else {
            let previous;
            for(let i = 0; i < index; i++) {
                previous = current;
                //将previous 与 current 的下一项链接起来: 跳过 current,从而移除它
                current = current.next;
            }
            previous.next = current.next;
        }
        this.count--;
        return current.element; // 返回移除的值
    }
    return undefined; // 如果index 不是有效位置,返回 undefined 
}
循环迭代链表直到目标位置 - getElementAt()

在reomoveAt(idex)中,我们需要迭代整个链表直到达到我们的目标索引 index 。循环到目标 index 的代码片段在 LinkedList 类的方法中很常见。因此我们可以重构代码,将这部分的逻辑独立为单独的方法。

getElementAt(index) {
    if (index >= 0 && index <= this.count) {
        let current = this.head;
        for (let i = 0; i < index; i++) {
            current = current.next // 循环结束时,current 元素将是 index 位置的元素引用
        }
        return current;
    }
    return undefined;
}
利用 getElementAt() 重构 removeAt(index)
removeAt(index) {
    if(index >= 0 && index < this.count ) {
        let current = this.head;
        if( index === 0 ) {
            this.head = current.next;
        } else {
            const previous = this.getElementAt(index-1) // previous 将是 index-1 位置的元素 
            current = previous.next 
            previous.next = current.next
        }
        this.count--;   
        return current.element;
    }
    return undefined;
}
insert() - 在任意位置插入元素
inset(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.getElenemtAt(index -1); // previous 是需要添加节点的位置的前一个位置
            const current = previous.next;
            node.next = current;
            previous.next = node;
        }
        this.count++; 
        return true; // 添加成功返回 true
    }
    return false; // index 是无效位置,添加失败
}
indexOf() - 返回一个元素的位置

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

indexOf(element) {
    let current = this.head;
    for(let i = 0; i < this.count && current != null ; i++) {
        if(this.equalsFn(element, current.elemnt)) {
            return i;
        }
        current = current.next;
    }
    return -1; 
}
remove() - 从链表中移除元素

创建完 indexOf() 方法之后,我们就可以来实现前面没有实现的移除元素的第二种情况了,remove()。

remove(element) {
    const index = this.indexOf(element);
    return this.removeAt(index);
}

我们已经有了一个用来移除给定位置元素的方法(removeAt)。因为我们有了 indexOf 能够找到元素的位置,就可以很轻松的实现删除指定元素的方法了。

isEmpty、size、getHead 方法
  • size - 链表元素的个数
size () {
    return this.count;
}
  • isEmpty - 链表是否为空
isEmpty () {
    return this.size() === 0;
}
  • getHead - 获取表头
getHead() {
    return this.head;
}
toString - 将链表转换为字符串
toString() {
    if (this.head == null) {
        return '';
    } 
    let objString = `${this.head.element}`;
    let current = this.head.next;
    for (let i = 1; i < this.size() && current != null; i++) {
        objString = `${objString},${current.element`;
        current = current.next;
    }
    return objString;
}

双向链表

链表有多种不同的类型,现在我们介绍另一种类型的链表,双向链表。
双向链表和普通链表的区别在于,在链表中,一个节点只有链向下一个节点的链接;而在双向链表中,链接时双向的:一个链向下一个元素,另一个链向前一个元素。

先从实现 DoublyLinkedList 类所需的变动开始。

   class DoublyNode extends Node {
        constructor(element, next, prev) {
            super(element,next);
            this.prev = prev; 
        }
   }
    
   defaultEquals = function (a,b) {
        return a === b;
   }
    
   class DoublyLinkedList extents LinkedList {
        constructor(equalsFn = defaultEquals) {
           super(equalsFn);
           this.tail = undefined; // 链表的最后一个元素  
       }
   }
在任意位置插入新元素 - insert()

向双向链表中插入一个心元素跟单向链表非常类似。区别在于,链表只要控制一个 next 指针,而双向链表中则要同时控制 next 和 prev 这两个指针。

insert (element, index) {
    if(index >= 0 && index <= this.count) {
        const node = new DoublyNode(element);
        let current = this.head;
        if(index === 0) { // 添加到第一项
            if (this.head == null) {
                this.head = node;
                this.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;
}
从任意位置移除元素 - removeAt()

从双向链表中移除元素跟链表非常类似。唯一的区别就是,还需要设置前一个位置的指针。

removeAt(index) {
    if(index >= 0 && index < this.count) {
        let current = this.head;
        if(index === 0) {
            this.head = current.next;
            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.next = current.next;
            current.next.prev = prevous;
        }
        this.count--;
        return current.element; // 返回被删除的元素
    }
    return undefined;
}

有序链表

有序链表是保持元素有序的链表结构,除了使用排序算法之外,我们还可以将元素插入到正确的位置来保证链表的有序性。 先来声明 SortedLikedList 类。

const Compare = {
    LESS_THAN: -1,
    BIGGER_THAN: 1
}

function defaultCompare(a,b) {
    if(a === b) {
        return 0;
    }
    return a < b ? Compare.LESS_THAN : Compart.BIGGER_THAN;
}

class SortedLinkedList extends LinkedList {
    constructor(equalsFn = defaultEquals, compareFn = defaultCompare) {
        super(equalsFn);
        this.compareFn = compareFn
    }
}
有序插入元素
// 由于我们不想允许在任何位置插入元素,我们要给 index 参数设置一个默认值
insert(element, index = 0) {
    if(this.isEmpty()) {
        return super.insert(element,0);
    }
    let current = this.head;
    let i = 0;
    for ( ; i < this.size() && current ; i++) {
        const comp = this.compareFn(elemet, current.element);
        if(comp === Compare.LESS_THAN ) {
            return i; //获取到需要插入的正确位置 
        }
        current = current.next
    }
    return super.insert(element,i)
}

总结

本篇介绍了链表这种数据结构,以及其变体:双向链表、循环链表、和有序链表。链表相比数组最重要的优点就是无须移动链表中的元素,就能轻松地添加和移除元素。当需要添加和移除很多元素时,最好的选择就是链表,而非数组。
在下一篇,我们将一起学习一种存储唯一元素的数据结构 - 集合。