【4】js链表的实现-JavaScript学习数据结构

161 阅读4分钟

链表基本概念

链表是和数组非常类似的一种基本数据结构,但链表在内从中的存储格子是连续的,二者在性能上各有所长。数组访问元素为O(1),但是数组在插入和删除操作的时候比较麻烦,因为需要其他元素移位置。链表的访问是从头到尾的,但是链表在插入和删除上有巨大的优势。除了对数据的随机访 问,链表几乎可以用在任何可以使用一维数组的情况中。如果需要随机访问,数组仍然是更好的选择。

链表的每个节点需要两个格子,第一个用于保存数据,第二个保存着链表里的下一结点的内存地址

大多数语言中数组的长度是固定的,所以当数组已被数据填满时,再要加入新的元素就会非常困难。虽然JavaScript中的数组不存在这个问题,但JavaScript 中数组的主要问题是,它们被实现成了对象,与其他语言(比如 C++ 和 Java) 的数组相比,效率很低(请参考 Crockford 那本书的第 6 章)(注:此处没去研究)。

数组的插入元素、删除元素过程

数组基本操作的复杂度

因为数组在内存中的格子必须是连续的,因此,数组中不能有空的元素,必须全部填满。这导致了数组在进行插入、删除的时候,虽然插入和删除本身只需要一步操作,但是插入[n]时,n右侧元素必须全部右移才能留出空位n;删除操作也是一样,删除[n]处的元素后会留一个空位,n右侧的必须全部左移才能填满。在插入和删除上,链表的优势就比较明显。

要标识出链表的起始节点却有点麻烦,许多链表的实现都在链表最前面有一个特殊节
点,叫做头节点。

链表插入元素、删除元素过程

可以看出来,链表在插入和删除过程中操作非常简单。插入只需要把一条链条断开,中间接入新元素的左右端即可;删除元素只需要把要删除的元素左右的链直接连起来,这样被删除元素就脱离链表了。不过要注意的是,链表的访问必须是从头开始,因此要插入cookies元素,必须先访问到eggs元素,而这个步骤是O(n)。这样似乎链表和数组的性能是差不多的,甚至数组还要更好?如下图分析。

数组与链表的性能

链表相对于数组,高效地遍历单个列表并删除其中多个元素,是链表的亮点之一。对于多个元素的插入和删除,链表从头到尾走一遍就行了,而数组每次只能操作一个元素,因为其必须保证数组是填满后才能再次操作。

用类实现链表

在用JavaScript的类实现链表之前,要知道链表的基本操作包括:

  • push(element):在链尾添加元素
  • insert(element,position):链表的某个位置插入
  • getElementAt(index):返回链表中特定位置的元素
  • remove(element):从链表中移除一个元素
  • indexOf(element):返回元素在链表中的索引
  • removeAt(position):从链表的特定位置移除一个元素
  • isEmpty()
  • size()
  • getHead()
  • toString()
class Node {
    constructor(element) {
        this.element = element;
        this.next = undefined;
    }
}

class List {
    constructor() {
        this.count = 0;
        this.element = undefined;
    }
    //向链尾加入元素
    push(element) {
        let newNode = new Node(element);
        let currNode;

        if (this.head == null) {
            this.head = newNode;
        } else {
            currNode = this.head;
            while (currNode.next != null) {
                currNode = currNode.next;
            }
            currNode.next = newNode;
        }
        this.count++;
    }

    //移除元素:链中的第几个
    removeAt(index) {
        if (index >= 0 && index < this.count) {
            let currNode = this.head;

            if (index === 0) {
                this.head = currNode.next;
            } else {
                let preNode;
                for (let i = 0; i < index; i++) {
                    preNode = currNode;
                    currNode = currNode.next;
                }
                preNode.next = currNode.next;
            }
            this.count--;
            return currNode.element;
        }
        return undefined;
    }

    //移除元素:移除某个值
    remove(element) {
        const index = this.indexOf(element);
        this.removeAt(index);
    }

    //任意位置插入一个元素
    insert(element, index) {
        if (index >= 0 && index < this.count) {
            const newNode = new Node();
            if (index === 0) {
                const currNode = this.head;
                newNode.next = currNode;
                this.head = newNode;
            } else {
                const preNode = getElementAt(index - 1);
                const currNode = preNode.next;
                newNode.next = currNode;
                preNode.next = newNode;
            }
            this.count++;
            return true;
        }
        return false;
    }

    //获取某位置的元素
    getElementAt(index) {
        if (index >= 0 && index < this.count) {
            let currNode = this.head;
            for (let i = 0; i < index; i++) {
                currNode = currNode.next;
            }
            return currNode.element;
        }
        return undefined;

    }

    //获取某个值的位置
    indexOf(element) {
        let currNode = this.head;
        let result = -1;
        let index = 0;
        while (currNode != null) {
            if (currNode.element === element) {
                result = index;
                break;
            }
            currNode = currNode.next
            index++;
        }
        return result;
    }

    //有几个元素
    size() {
        return this.count;
    }

    //检查是否为空
    isEmpty() {
        return this.size() === 0;
    }

    getHead() {
        return this.head;
    }

    //输出所有的链表元素,供测试使用
    showItems() {
        let currNode = this.head;
        let result = [];
        while (currNode != null) {
            result.push(currNode.element);
            currNode = currNode.next;
        }
        return result;
    }

}

上面写的方法方法比较多,实际上很多方法都是重复的,如size()isEmpty(),链表中元素个数为0,自然就是isEmpty = true。在比如,上面的删除包括两种方式,一种是根据index(在链中第几个位置),另外一种是根据element(元素的具体值),实际上通过indexOf(),即找到某个element在链中的位置,那么对于删除方法,index和element都是没有差别的。

在具体实现链表的时候,有一些注意事项:

  • while循环中的条件是currNode != null,还是currNode.next != null
  • 条件判断xx != null的含义,与!== 的区别,为什么是null,而不写undefined

其他链表

链表有很多不同种类,最普通的链表是单向的,也就是上面说介绍的那种,只能从头单位单向遍历。

双向链表

在双向链表中,链接是双向的,A节点的next指向B,同时B的previous又是指向A。

在单向链表中,如果迭代时错过了要找的元素,就需要回到起点,重新开始迭代。这是双向链表的一个优势。

双向链表示意图

class DoublyNode extends Node {
    constructor(element, next, previous) {
       super(element, next);
       this.prevous = previous;
    }
}

class DoblyList extends Link {
   ...
}

双向链表的具体实现上,各种方法与单向链表是类似的。注意的是,在绑定A和B的两个节点关系时,要同时说明A的next是B,B的previous是A。在这一步中,单向链表只用说明A的next是B即可,主要是这一点上的区别。

//在A和B节点中插入C节点 A=C=B
const preNode = this.getElementAt(index-1); //前一个节点A
currNode = preNode.next;  //现在的节点B(即将要变成C的下一个)
const newNode = DoublyNode(element)  //创建新节点C

//单向链表绑定A,顺序
newNode.next = currNode;
preNode.next = newNode;

//反向绑定
currNode.previous = newNode;
newNode.previous = preNode;

循环链表

循环链表可以像链表一样只有单向引用,也可以像双向链表一样有双向引用。循环链表和链 表之间唯一的区别在于,最后一个元素指向下一个元素的指针(tail.next)不是引用 undefined,而是指向第一个元素(head),如下图所示:

单向循环链表

双向循环链表

参考资料