双向链表

双向链表是链表的一种,它的每个节点中都有两个指针,分别指向上一个节点和下一个节点,所以,从双向链表中的任意一个节点开始,都可以很方便的访问他的前驱节点和后继节点。如下图为双向链表的结构图:
link (1).jpg
双向链表与普通链表的区别在于,在普通链表中,一个节点只有一个指向下一个节点的指针域,而在双向链表中,有两个指针域,一个指向下一个节点,另一个指向上一个节点,如上图所示。

实现双向链表

定义节点类

双向链表中的节点只是比普通链表中的节点多了一个 prev 指针,我们的双向链表中的节点类可以继承自普通链表的节点类:

  1. class DoublyNode {
  2. constructor(element) {
  3. this.element = element;
  4. this.prev = null;
  5. this.next = null;
  6. }
  7. }

定义双向链表类

DoublyLinkList 类是一种特殊的 LInkList 类,我们只需要扩展

  1. import { defaultEquals } from './utils';
  2. class DoublyLinkList {
  3. constructor(equalsFn = defaultEquals) {
  4. // 链表内部的函数,用于比较链表中的元素是否相等
  5. this.equalsFn = equalsFn;
  6. // 存储双向链表中的元素个数
  7. this.count = 0;
  8. // 保存头节点(注意:head 永远指向头节点)
  9. this.head = null;
  10. // 保存尾结点(注意:tail 永远指向尾结点)
  11. this.tail = null;
  12. }
  13. }

接下来,我们为双向链表定义一些方法:

append(element) 向双向链表尾部添加一个新元素
insert(element, index) 向双向链表的任意位置插入新元素
getElementAt(index) 返回双向链表中特定位置的元素,若元素不存在,返回 undefined
remove(element) 从双向链表中移除一个元素
removeAt(index) 从双向链表的特定位置移除一个元素
removeHead() 移除首节点
removeTail() 移除尾结点
indexOf(element) 返回指定元素在双向链表中的索引,若没有该元素则返回 -1
head() 返回首节点
tail() 返回尾结点
size() 返回双向链表包含的元素个数,即返回链表的长度
isEmpty() 判断双向链表是否为空
clear() 清空双向链表
toString() 返回表示整个双向链表的字符串
inverseToString() 返回双向链表从头到尾的字符串

append 向双向链表尾部添加一个新元素

向双向链表尾部添加一个新元素时,和向普通链表尾部添加新元素一样,也可能有两中情景:

  • 双向链表为空,添加的是第一个元素
  • 双向链表不为空,向其追加元素

    1. append(element) {
    2. const node = new DoublyNode();
    3. // 双向链表为空,添加的是第一个元素
    4. if (this.head == null) {
    5. // 让 head 指向新创建的节点
    6. this.head = node;
    7. // 让 tail 属性指向新创建的节点 tail 属性永远指向双向链表的最后一个元素
    8. this.tail = node;
    9. } else {
    10. // 双向链表不为空,向双向链表尾部添加元素
    11. // 将尾部元素的 next 指针指向新节点,新节点就变成了新的尾结点
    12. this.tail.next = node;
    13. // 将新创建的节点的 prev 指针指向旧的尾结点
    14. node.prev = this.tail;
    15. // tail 属性永远是链表最后一个元素的引用
    16. this.tail = node;
    17. }
    18. this.count++;
    19. }

    我们来分析一下这两种情景:

第一种情景:向空的双向链表添加一个元素。当我们创建一个双向链表实例时,head 默认会指向 null,也就意味着此时的双向链表是一个空链表,我们是在向双向链表添加第一个元素,因此我们要做的就是让 head 指向这个新建的节点 node,同时也要把 tail 指向 node,即此时的 node 节点既是首节点也是尾结点。

  1. // 双向链表为空,添加的是第一个元素
  2. if (this.head == null) {
  3. // 让 head 指向新创建的节点
  4. this.head = node;
  5. // 让 tail 属性指向新创建的节点 tail 属性永远指向双向链表的最后一个元素
  6. this.tail = node;
  7. }

第二种情景:向一个不为空的双向链表添加新元素。要向双向链表的尾部添加一个新元素,首先需要找到最后一个元素。在双向链表中,tail 属性永远都是对双向链表最后一个元素的引用,因此我们可以很方便的找到双向链表中的最后一个元素。然后将 tail 的 next 指针指向要添加到双向链表的新节点,再将 tail 属性指向新节点,因为 tail 属性永远都是指向双向链表中的最后一个元素。

  1. else {
  2. // 双向链表不为空,向双向链表尾部添加元素
  3. // 将尾部元素的 next 指针指向新节点,新节点就变成了新的尾结点
  4. this.tail.next = node;
  5. // 将新创建的节点的 prev 指针指向旧的尾结点
  6. node.prev = this.tail;
  7. // tail 属性永远是链表最后一个元素的引用
  8. this.tail = node;
  9. }

insert 向双向链表的任意位置插入新元素

insert 方法向双向链表的任意位置插入一个新元素。向双向链表中插入新元素时,需要考虑四种情景:

  • 插入的位置不合法,元素插入失败
  • 在双向链表的起点位置插入新元素
  • 在双向链表的尾部插入新元素
  • 在双向链表的中间插入新元素 ```javascript insert(element, index) { // 插入的位置合法,向双向链表中插入新元素 if (index >=0 && index <= this.count) {

    const node = new DoublyNode(element); let current = this.head;

    if (index === 0) {

    1. // 在双向链表的头部插入元素
    2. if (this.head == null) { // 双向链表为空链表
    3. // 此时添加的 node 节点既是头节点也是尾节点
    4. this.head = node;
    5. this.tail = node;
    6. } else { // 双向链表不为空,此时添加的 node 将会成为 头节点
    7. // 将 node 节点的 next 指针指向当前的头节点
    8. node.next = this.head;
    9. // 将当前的头节点的 prev 指针指向 node 节点
    10. this.head.prev = node;
    11. // 将 head 指向 node,node成为新的头节点
    12. this.head = node;
    13. }

    } else if (index === this.count) {

    1. // 在双向链表的尾部插入元素,此时添加的 node 将会成为 尾节点
    2. current = this.tail;
    3. // 将当前的尾节点的 next 指针指向 node 节点
    4. current.next = node;
    5. // 将 node 节点的 prev 指针指向当前的尾节点
    6. node.prev = current;
    7. // 将 tail 属性指向 node,node成为新的尾节点
    8. this.tail = node;

    } else {

    1. // 在双向链表的其他位置插入元素
    2. // 首先获取插入位置的前一个节点
    3. const previous = this.getElementAt(index - 1);
    4. // 插入位置的后一个节点
    5. current = previous.next;
    6. // 将 node 节点插入 previous 和 current 之间
    7. // 则将 node 的 next 指针指向 current,prev 指针指向 previous
    8. node.next = current;
    9. node.prev = previous;
    10. // node 节点插入后,previous 的 next 指针指向 node 节点
    11. previous.next = node;
    12. // current(插入位置的后一个节点) 的 prev 指针指向新插入的 node 节点
    13. current.prev = node;

    }

    // 新节点插入后,双向链表的长度增加 1 this.count++; // 返回 true,表示元素成功插入到双向链表中 return true; }

    // 插入的位置不合法,返回false,表示元素没有添加到双向链表中 return false;

}

  1. 下面我们逐一分析这四种情景:
  2. 第一种情景:插入的位置不合法。由于我们是根据位置(索引)来插入元素,因此在插入前需要先检查越界值,如果越界了,就返回 false,表示元素没有插入到双向链表中。
  3. ```javascript
  4. if (index >=0 && index <= this.count) {
  5. }
  6. // 插入的位置不合法,返回false,表示元素没有添加到双向链表中
  7. return false;

第二种情景:在双向链表的第一个位置(起点) 插入一个新元素。如果双向链表为空,我们只需要把 head 和 tail 属性都指向这个新节点即可,此时的 node 节点既是头节点也是尾结点。如果双向链表不为空,我们就将新加节点 node 的 next 指针指向双向链表中当前的头节点 head,然后将 head 的 prev 指针指向新加节点 node,最后将 head 属性指向 node 节点,新插入的 node 节点就成为了新的头节点。

  1. if (index === 0) {
  2. // 在双向链表的头部插入元素
  3. if (this.head == null) { // 双向链表为空链表
  4. // 此时添加的 node 节点既是头节点也是尾节点
  5. this.head = node;
  6. this.tail = node;
  7. } else { // 双向链表不为空,此时添加的 node 将会成为 头节点
  8. // 将 node 节点的 next 指针指向当前的头节点
  9. node.next = this.head;
  10. // 将当前的头节点的 prev 指针指向 node 节点
  11. this.head.prev = node;
  12. // 将 head 指向 node,node成为新的头节点
  13. this.head = node;
  14. }
  15. }

第三种情景:在双向链表的尾部添加一个新元素。在这种情景下,我们需要把最后一个节点的 next 指针指向 node 节点,然后把 node 节点的 prev 指针指向双向链表中当前的尾结点,最后将 tail 属性指向 node 节点,这样新添加的node节点就成为了新的尾结点了。

  1. else if (index === this.count) {
  2. // 在双向链表的尾部插入元素,此时添加的 node 将会成为 尾节点
  3. current = this.tail;
  4. // 将当前的尾节点的 next 指针指向 node 节点
  5. current.next = node;
  6. // 将 node 节点的 prev 指针指向当前的尾节点
  7. node.prev = current;
  8. // 将 tail 属性指向 node,node成为新的尾节点
  9. this.tail = node;
  10. }

第四种情景:在链表的中间插入一个新元素。在这种情景下,我们需要迭代双向链表找到要插入的位置。调用 getElementAt 方法找到插入位置的前一个节点 previous,然后将 插入位置的下一个节点 previous.next 赋值给 current 变量。我们现在要做的就是在 previous 和 current 之间插入新节点,首先将新插入的节点 node 的next 指针指向 current ,previous 的 next 指针指向 node 节点,然后再将 current 的 prev 指针指向 node 节点,node 节点的 prev 指针指向 previous ,这样,新节点 node 就插入了 previous 和 current 之间。

  1. else {
  2. // 在双向链表的其他位置插入元素
  3. // 首先获取插入位置的前一个节点
  4. const previous = this.getElementAt(index - 1);
  5. // 插入位置的后一个节点
  6. current = previous.next;
  7. // 将 node 节点插入 previous 和 current 之间
  8. // 则将 node 的 next 指针指向 current,prev 指针指向 previous
  9. node.next = current;
  10. node.prev = previous;
  11. // node 节点插入后,previous 的 next 指针指向 node 节点
  12. previous.next = node;
  13. // current(插入位置的后一个节点) 的 prev 指针指向新插入的 node 节点
  14. current.prev = node;
  15. }

getElementAt() 返回双向链表中特定位置的元素

getElementAt 方法返回双向链表中特定位置的元素,若元素不存在,返回 undefined

  1. getElementAt(index) {
  2. // 传入的位置合法,迭代双向链表查找目标位置的元素
  3. if (index >= 0 && index <= this.count) {
  4. // 从第一个元素迭代双向链表
  5. let node = this.head;
  6. for (let i = 0; i < index && node != null; i++) {
  7. // 迭代整个双向链表直到目标 index,结束循环时,此时的 node 元素是目标index 的前一个元素
  8. // 因此需要使用 node.next 来获取目标 index 的元素
  9. node = node.next;
  10. }
  11. return node;
  12. }
  13. // 传入的位置不合法,即在双向链表中找不到该位置的节点,返回 undefined
  14. return undefined;
  15. }

代码分析:
我们首先会对传入的 index 参数做合法性检查,如果传入的位置是不合法的参数,我们会返回 undefined,因为这个位置在双向链表中不存在。

然后从双向链表的第一个节点开始,迭代整个链表直到目标index,当结束循环时,node 就是我们要查找的目标节点。

remove 从双向链表中移除一个元素

  1. remove(element) {
  2. const index = this.indexOf(element);
  3. return this.removeAt(index)
  4. }

removeAt 从双向链表的特定位置移除一个元素

removeAt 方法从双向链表的特定位置移除一个元素。我们需要处理四种情景:

  • 移除的位置不合法,元素移除失败
  • 移除双向链表中的第一个元素
  • 移除双向链表的最后一个元素
  • 移除双向链表中间的元素
  1. removeAt(index) {
  2. if (index >= 0 && index < this.count) {
  3. // current 变量是对双向链表中第一个节点的引用
  4. let current = this.head;
  5. if (index === 0) {
  6. // 删除双向链表的头部元素
  7. // 删除头部元素,把 head 指向第二个节点即可
  8. this.head = this.head.next;
  9. if (this.count === 1) {
  10. // 链表只有一个节点时,该节点的 prev 指针和 next 指针都为 null
  11. // 如果链表中只有一个节点,删除了头部节点,就变成了空的双向链表
  12. // 因此需要将 tail 设为 null
  13. this.tail = null;
  14. } else {
  15. // 将新的头部节点的 prev 指针设为 null
  16. this.head.prev = null;
  17. }
  18. } else if (index === this.count - 1) {
  19. // 删除双向链表的尾部元素
  20. current = this.tail;
  21. // tail 是对最后一个元素的引用,删除最后一个元素,直接将 tail 的引用更新为倒数第二个元素即可
  22. this.tail = current.prev;
  23. // 将新的尾部元素的 next 指针设为 null
  24. this.tail.next = null;
  25. } else {
  26. // 删除双向链表的中间元素
  27. // current 变量就是要删除的元素
  28. current = this.getElementAt(index);
  29. // 要删除元素的前一个元素
  30. const previous = current.prev;
  31. // 将 previous 的 next 指针指向要删除元素的下一个元素
  32. previous.next = current.next;
  33. // 将 要删除元素的下一个元素的 prev 指针指向 previous
  34. current.next.prev = previous;
  35. }
  36. this.count--;
  37. return current.element;
  38. }
  39. return undefined;
  40. }

接下来,我们分析一下这四种情景:

第一种情景:移除的位置不合法。由于 removeAt 方法是根据位置 (index) 移除双向链表中的元素,因此我们首先需要对 index 进行有效性检查。从 0(包括 0 )到双向链表的长度(count - 1)都是有效的位置。如果 index 不是有效的位置,就返回 undefined,即没有从链表中移除元素。

第二种情景:移除双向链表中的第一个元素。要移除双向链表中的第一个元素,我们把 head 的引用改为双向链表中的第二个元素(current.next)即可,同时还要将链表中的第二个元素的 prev 指针设为null,因为双向链表的第一个元素的 prev 指针永远都为 null(或 undefined)。如果双向链表中只有一个元素,移除了双向链表的第一个元素(也是最后一个元素),那么双向链表就为空了,此时我们还需要将 tail 设为 null。

  1. if (index === 0) {
  2. // 删除双向链表的头部元素
  3. // 删除头部元素,把 head 指向第二个节点即可
  4. this.head = this.head.next;
  5. if (this.count === 1) {
  6. // 链表只有一个节点时,该节点的 prev 指针和 next 指针都为 null
  7. // 如果链表中只有一个节点,删除了头部节点,就变成了空的双向链表
  8. // 因此需要将 tail 设为 null
  9. this.tail = null;
  10. } else {
  11. // 将新的头部节点的 prev 指针设为 null
  12. this.head.prev = null;
  13. }
  14. }

第三种情景:移除双向链表的最后一个元素。因为我们已经有对双向链表最后一个元素的引用,因此我们可以很方便地移除最后一个元素。我们只需将 tail 的引用指向双向链表中倒数第二个元素即可,既然 tail 指向了倒数第二个元素,我们只需要将 next 指针设为 null 就可以了。这样,双向链表中的最后一个元素自然就被移除了。

  1. else if (index === this.count - 1) {
  2. // 删除双向链表的尾部元素
  3. current = this.tail;
  4. // tail 是对最后一个元素的引用,删除最后一个元素,直接将 tail 的引用更新为倒数第二个元素即可
  5. this.tail = current.prev;
  6. // 将新的尾部元素的 next 指针设为 null
  7. this.tail.next = null;
  8. }

第四种情景:从双向链表中间移除一个元素。首先需要迭代双向链表,直到找到要移除的位置。我们将要移除的元素赋值给 current 变量,那么要移除元素的上一个元素就是 previous = current.prev,要移除元素的下一个元素就是
current.next,我们只需要将 previous 的 next 指针指向 current.next,current.next 的 prev 指针指向 previous ,就可以断开与要移除元素的连接,从而将要移除元素移除掉。

  1. else {
  2. // 删除双向链表的中间元素
  3. // current 变量就是要删除的元素
  4. current = this.getElementAt(index);
  5. // 要删除元素的前一个元素
  6. const previous = current.prev;
  7. // 将 previous 的 next 指针指向要删除元素的下一个元素
  8. previous.next = current.next;
  9. // 将 要删除元素的下一个元素的 prev 指针指向 previous
  10. current.next.prev = previous;
  11. }

removeHead 移除首节点

  1. removeHead() {
  2. if (this.head == null) {
  3. return undefined;
  4. }
  5. const current = this.head;
  6. // 删除头部元素,把 head 指向第二个节点即可
  7. this.head = this.head.next;
  8. if (this.count === 1) {
  9. // 链表只有一个节点时,该节点的 prev 指针和 next 指针都为 null
  10. // 如果链表中只有一个节点,删除了头部节点,就变成了空的双向链表
  11. // 因此需要将 tail 设为 null
  12. this.tail = null;
  13. } else {
  14. // 将新的头部节点的 prev 指针设为 null
  15. this.head.prev = null;
  16. }
  17. return current.element;
  18. }

如果双向链表只有一个节点,移除首节点,双向链表就为空了,此时除了要将 head 设为 null,还需要将 tail 也设为 null;
如果双向链表中不止一个节点,移除首节点,只需将 head 指向双向链表的第二个节点,然后将第二个节点的 prev 指针设为 null 就可以了。

removeTail 移除尾结点

  1. removeTail() {
  2. // 空链表
  3. if (this.head == null) {
  4. return undefined;
  5. }
  6. // current 变量是对尾结点的引用
  7. const current = this.tail;
  8. // tail 是对最后一个元素的引用,删除最后一个元素,直接将 tail 的引用更新为倒数第二个元素即可
  9. this.tail = current.prev;
  10. // 将新的尾部元素的 next 指针设为 null
  11. this.tail.next = null;
  12. return current.element;
  13. }

因为 tail 属性是对双向链表的最后一个节点的引用,要移除尾节点,直接将 tail 的引用更新为双向链表的倒数第二个节点,并将倒数第二个节点的 next 指针设为 null 即可。

indexOf 返回指定元素在双向链表中的索引

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

  1. indexOf(element) {
  2. // current 变量是对双向链表中第一个元素(即首节点)的引用
  3. let current = this.head;
  4. // 迭代链表,从 head(索引为 0)开始
  5. let index = 0;
  6. while(current != null) {
  7. // 比较当前节点的元素与目标元素是否相等,若相等,当前节点就是我们要找的节点,返回当前节点的位置
  8. if (this.equalsFn(element, current.element)) {
  9. return index;
  10. }
  11. // 不是我们要找的节点,继续迭代下一个节点
  12. index++;
  13. current = current.next;
  14. }
  15. // 双向链表为空或者迭代到双向链表尾部,没有找到目标元素,返回 -1
  16. return -1
  17. }

代码分析:

从双向链表中查找某个节点的位置,需要对双向链表进行迭代。我们从第一个元素 head (索引 0) 开始,一直到 current 为 null为止,也就是迭代到双向链表的尾部。

在每次迭代时,我们都会验证 current 节点的元素和目标元素是否相等,如果相等,当前位置的元素就是我们要找的元素,返回该元素的位置。如果不是我们要找的元素,则继续迭代下一个节点。

如果双向链表为空或者迭代到了双向链表的尾部,都没有找到我们想要的元素,就返回 -1。

head 返回首节点

  1. head() {
  2. return this.head;
  3. }

tail 返回尾结点

  1. tail() {
  2. return this.tail;
  3. }

size 返回双向链表包含的元素个数

size 方法返回双向链表的节点个数,即返回双向链表的长度。DoublyLinkList 是我们自己构建的类,其长度是我们使用 count 变量来控制的,因此返回双向链表的长度,就是返回 count 变量。

  1. size() {
  2. return this.count;
  3. }

isEmpty 判断双向链表是否为空

isEmpty 方法判断双向链表是否为空。如果双向链表中没有元素,isEmpty 就会返回 true,否则返回 false

  1. isEmpty() {
  2. return this.size() === 0
  3. }

clear 清空双向链表

  1. clear() {
  2. this.head = null;
  3. this.tail = null;
  4. this.count = 0;
  5. }

toString() 返回表示整个双向链表的字符串

toString 方法将 DoublyLinkList 对象转换成一个字符串,然后返回双向链表内容的字符串。

  1. toString() {
  2. if (this.head == null) {
  3. return '';
  4. }
  5. let objString = `${this.head.element}`;
  6. let current = this.head.next;
  7. while(current != null) {
  8. objString = `${objString}, ${current.element}`;
  9. current = current.next;
  10. }
  11. return objString;
  12. }

代码分析:

首先,如果双向链表为空,我们就返回一个空字符串;

如果双向链表不为空,我们首先用双向链表第一个元素的值来初始化方法返回的字符串objString。然后迭双向代链表的所有其它元素,将元素值添加到字符串 objString 上;

当迭代完双向链表后,返回双向链表内容的字符串。

inverseToString() 返回双向链表从尾到头的字符串

inverseToString 方法将 DoublyLinkList 对象转换成一个字符串,然后返回双向链表从尾到头的字符串。

  1. inverseToString() {
  2. if (this.tail != null) {
  3. return ''
  4. }
  5. let objString = `${this.tail.element}`;
  6. let previous = this.tail.prev;
  7. while(previous != null) {
  8. objString = `${objString}, ${previous.element}`;
  9. previous = previous.prev;
  10. }
  11. return
  12. }

代码分析:

首先,如果双向链表为空,我们就返回一个空字符串;

如果双向链表不为空,我们首先用双向链表的尾部元素的值来初始化方法返回的字符串objString。然后从尾部开始往前迭双向代链表的所有其它元素,将元素值添加到字符串 objString 上;

当迭代完双向链表后,返回双向链表内容的字符串。

完整代码

  1. function defaultEquals(a, b) {
  2. return a === b;
  3. }
  4. class DoublyNode {
  5. constructor(element) {
  6. this.element = element;
  7. this.prev = null;
  8. this.next = null;
  9. }
  10. }
  11. class DoublyLinkList {
  12. constructor(equalsFn = defaultEquals) {
  13. this.equalsFn = equalsFn;
  14. this.count = 0;
  15. this.head = null;
  16. this.tail = null;
  17. }
  18. append(element) {
  19. const node = new DoublyNode(element);
  20. if (this.head == null) {
  21. this.head = node;
  22. this.tail = node;
  23. } else {
  24. this.tail.next = node;
  25. node.prev = this.tail;
  26. this.tail = node;
  27. }
  28. this.count++;
  29. }
  30. insert(element, index) {
  31. if (index >=0 && index <= this.count) {
  32. const node = new DoublyNode(element);
  33. let current = this.head;
  34. if (index === 0) {
  35. if (this.head == null) {
  36. this.head = node;
  37. this.tail = node;
  38. } else {
  39. node.next = this.head;
  40. this.head.prev = node;
  41. this.head = node;
  42. }
  43. } else if (index === this.count) {
  44. current = this.tail;
  45. current.next = node;
  46. node.prev = current;
  47. this.tail = node
  48. } else {
  49. const previous = this.getElementAt(index);
  50. current = previous.next;
  51. node.next = current;
  52. node.prev = previous;
  53. current.prev = node;
  54. previous.next = node;
  55. }
  56. this.count++;
  57. return true;
  58. }
  59. return false;
  60. }
  61. getElementAt(index) {
  62. if (index >= 0 && index <= this.count) {
  63. let node = this.head;
  64. for (let i = 0; i < index && node != null; i++) {
  65. node = node.next;
  66. }
  67. return node;
  68. }
  69. return undefined;
  70. }
  71. remove(element) {
  72. const index = this.indexOf(element);
  73. return this.removeAt(index);
  74. }
  75. removeAt(index) {
  76. if (index >= 0 && index < this.count) {
  77. let current = this.head;
  78. if (index === 0) {
  79. this.head = this.head.next;
  80. if (this.count === 1) {
  81. this.tail = null;
  82. } else {
  83. this.head.prev = null;
  84. }
  85. } else if (index === this.count - 1) {
  86. current = this.tail;
  87. this.tail = current.prev;
  88. this.tail.next = null;
  89. } else {
  90. current = this.getElementAt(index);
  91. const previous = current.prev;
  92. previous.next = current.next;
  93. current.next.prev = previous;
  94. }
  95. this.count--;
  96. return current.element;
  97. }
  98. return undefined;
  99. }
  100. removeHead() {
  101. if (this.head == null) {
  102. return undefined;
  103. }
  104. const current = this.head;
  105. this.head = this.head.next;
  106. if (this.count === 1) {
  107. this.tail = null;
  108. } else {
  109. this.head.prev = null;
  110. }
  111. return current.element;
  112. }
  113. removeTail() {
  114. if (this.head = null) {
  115. return undefined;
  116. }
  117. const current = this.tail;
  118. this.tail = current.prev;
  119. this.tail.next = null;
  120. return current.element;
  121. }
  122. indexOf(element) {
  123. let current = this.head;
  124. let index = 0;
  125. while(current != null) {
  126. if (this.equalsFn(element, current.element)) {
  127. return index;
  128. }
  129. index++;
  130. current = current.next;
  131. }
  132. return -1;
  133. }
  134. head() {
  135. return this.head;
  136. }
  137. tail() {
  138. return this.tail;
  139. }
  140. size() {
  141. return this.count;
  142. }
  143. isEmpty() {
  144. return this.size() === 0;
  145. }
  146. clear() {
  147. this.head = null;
  148. this.tail = null;
  149. this.count = 0;
  150. }
  151. toString() {
  152. if (this.head == null) {
  153. return '';
  154. }
  155. let objString = `${this.head.element}`;
  156. let current = this.head.next;
  157. while(current != null) {
  158. objString = `${objString}, ${current.element}`;
  159. current = current.next;
  160. }
  161. return objString;
  162. }
  163. inverseToString() {
  164. if (this.head == null) {
  165. return '';
  166. }
  167. let objString = `${this.tail.element}`;
  168. let previous = this.tail.prev;
  169. while(previous != null) {
  170. objString = `${objString}, ${previous.element}`;
  171. previous = previous.prev;
  172. }
  173. return objString;
  174. }
  175. }

双向链表的应用

基于双向链表实现栈

使用双向链表来实现栈,而不是链表,是因为相对于栈来说,我们会向链表尾部添加元素,也会从链表尾部移除元素,我们实现的双向链表有最后一个元素的引用,无需迭代就能获取它。双向链表可以直接获取尾部的元素,减少过程消耗,它的时间复杂度和原始的 Stack 实现相同,为 O(1)。

  1. class Stack {
  2. constructor() {
  3. this.items = new DoublyList();
  4. }
  5. // 添加元素到栈
  6. push(element) {
  7. this.items.append(element);
  8. }
  9. // 从栈顶移除元素
  10. pop() {
  11. if (this.isEmpty()) {
  12. return undefined;
  13. }
  14. return this.items.removeAt(this.size() - 1);
  15. }
  16. // 查看栈顶元素
  17. peek() {
  18. if (this.isEmpty()) {
  19. return undefined'
  20. }
  21. return this.items.tail();
  22. }
  23. // 检查栈是否为空
  24. isEmtpy() {
  25. return this.items.isEmpty();
  26. }
  27. // 清空栈
  28. clear() {
  29. this.items.clear()
  30. }
  31. // 返回栈里的元素个数
  32. size() {
  33. return this.items.size()
  34. }
  35. // 打印栈
  36. print() {
  37. return this.items.inverseToString()
  38. }
  39. }

基于双向链表实现队列

  1. class Queue {
  2. constructor() {
  3. this.items = new DoublyList();
  4. }
  5. // 入队列
  6. enqueue(element) {
  7. this.items.append(element);
  8. }
  9. // 出队列
  10. dequeue() {
  11. if (this.isEmpty()) {
  12. return undefined;
  13. }
  14. return this.items.removeHead();
  15. }
  16. // 返回队首
  17. head() {
  18. if (this.isEmpty()) {
  19. return undefined;
  20. }
  21. return this.items.head();
  22. }
  23. // 返回队尾
  24. tail() {
  25. if(this.isEmpty()) {
  26. return undefined;
  27. }
  28. return this.items.tail();
  29. }
  30. // 判断队列是否为空
  31. isEmpty() {
  32. return this.items.isEmpty();
  33. }
  34. // 清空队列
  35. clear() {
  36. this.items.clear();
  37. }
  38. // 返回队列长度
  39. size() {
  40. this.items.size()
  41. }
  42. // 打印队列
  43. print() {
  44. this.items.toString()
  45. }
  46. }

基于双向链表实现双端队列

  1. class Deque {
  2. constructor() {
  3. this.items = new DoublyList()
  4. }
  5. // 在双端队列的前端添加元素
  6. addFront(element) {
  7. this.items.append(element);
  8. }
  9. // 在双端队列的后端添加元素
  10. addBack(element) {
  11. this.items.insert(element, this.size() - 1);
  12. }
  13. // 从双端队列的前端移除第一个元素
  14. removeFront() {
  15. if (this.isEmpty()) {
  16. return undefined;
  17. }
  18. return this.items.removeHead();
  19. }
  20. // 从双端队列的后端移除一个元素
  21. removeBack() {
  22. if (this.isEmpty()) {
  23. return undefined;
  24. }
  25. return this.items.removeTail();
  26. }
  27. // 查看双端队列的前端的第一个元素
  28. peekFront() {
  29. if (this.isEmpty()) {
  30. return undefined;
  31. }
  32. return this.items.head();
  33. }
  34. // 查看双端队列的后端的第一个元素
  35. peekBack() {
  36. if (this.isEmpty()) {
  37. return this.items.tail()
  38. }
  39. }
  40. // 检查双端队列是否为空
  41. isEmpty() {
  42. return this.items.isEmpty();
  43. }
  44. // 清空双端队列
  45. clear() {
  46. return this.items.clear();
  47. }
  48. // 返回双端队列的长度
  49. size() {
  50. return this.items.size();
  51. }
  52. // 输出双端队列从队列前端到队列后端的字符串
  53. toString() {
  54. return this.items.toString();
  55. }
  56. }