基本定义

在计算机科学中,双向链表又被称为双链表,也属于链表,但是他每个节点都有两个指针,指向他前一个节点和后一个节点。我们学习单向链表的时候会发现,我们只能从前端遍历到尾端或者是尾端遍历到前端,因为单向链表正如其名:单向,所以说我们可以轻松的得到下一个节点,但是想回到上一个节点是很难的。但是双向链表就不一样的,他每个节点不单单保存着他下一个节点,还保存着他上一个节点,对于操作的方便程度是单向链表无法比拟的。当然,它占用的内存也会大一些

双向链表常见的方法

  1. append:向链表后端新增一个节点
  2. insert:在指定位置插入一个节点
  3. get:返回指定位置的节点
  4. indexOf:返回指定节点在链表的索引,找不到则返回-1
  5. update:修改某个位置的节点
  6. removeAt:移除指定位置的节点
  7. remove:移除链表后端的节点
  8. isEmpty:判断链表有没有节点,有则返回true,反之返回false
  9. size:返回链表的节点个数
  10. toString:输出链表的节点,因为实际上我们只需要节点的value,所以需要封装个方法打印出所有的value
  11. forwarrdString:从前端遍历到后端,打印出所有节点的value
  12. backwordString:从后端遍历到前端,打印出所有节点的value
  13. getHead:返回链表前端的节点
  14. getTail:返回链表后端的节点

在js中实现双向链表

前置工作

  1. class Node {
  2. constructor(data) {
  3. //当前节点的value
  4. this.data = data;
  5. //保存上一个节点信息
  6. this.prev = null;
  7. //保存下一个节点信息
  8. this.next = null;
  9. }
  10. }
  11. class DoubleList {
  12. constructor() {
  13. //头部节点
  14. this.head = null;
  15. //尾部节点
  16. this.tail = null;
  17. //链表长度
  18. this.length = 0;
  19. }
  20. }

先封装个节点类和双向链表类,每一个节点我们都是实例化一个Node类保存到链表的。

以下所有方法都在DoubleList类中实现

实现append方法

  1. // append方法:在链表后端插入一个节点
  2. append(data) {
  3. const node = new Node(data);
  4. //分情况处理:判断是否是第一个节点
  5. if (this.length === 0) {
  6. this.head = node;
  7. this.tail = node;
  8. } else {
  9. let current = this.head;
  10. while (current.next) {
  11. current = current.next;
  12. }
  13. node.prev = current;
  14. current.next = node;
  15. this.tail = node;
  16. }
  17. this.length++;
  18. }

append方法比较简单,只是在处理节点的时候需要考虑到上一个节点的保存,别忘记在新增后需要将length加1

实现 insert方法

  1. // insert方法:在指定位置插入元素
  2. insert(position, data) {
  3. if (position < 0 || position > this.length) return false;
  4. const node = new Node(data);
  5. let current = this.head;
  6. // 先判断插入第一个元素的情况
  7. if (position == 0) {
  8. this.head = node;
  9. node.next = current;
  10. current.prev = node;
  11. } else if (position === this.length) {
  12. //判断插入的是最后一个节点的情况
  13. while (current.next) {
  14. current = current.next;
  15. }
  16. current.next = node;
  17. node.prev = current;
  18. this.tail = node;
  19. } else {
  20. //在中间的情况
  21. let index = 0;
  22. let prive = null;
  23. while (index < position) {
  24. prive = current;
  25. current = current.next;
  26. index++;
  27. }
  28. prive.next = node;
  29. node.prev = prive;
  30. current.prev = node;
  31. node.next = current;
  32. }
  33. this.length++;
  34. return true;
  35. }

insert方法就比较麻烦,分的情况也比较多,首先需要考虑插入的是第一个节点的情况,先看图:

05_双向链表 - 图1

我们可以看到默认情况双向链表的结构,head会指向第一个节点,tail会指向最后一个节点,然后每个节点有个next属性指向下一个节点,也有一个prev属性指向上一个节点

05_双向链表 - 图2

我们会发现head指向value为4的节点,然后value为4的节点的next指向value为1的节点,然后节点value为1的节点的prev指向value为4的节点,也就是他上一个节点

当插入最后也是同理,value为3的节点的next属性指向value为4的节点,value为4的节点的prev属性指向value为3的节点,然后继续将tail指向最后一个节点,也就是value为4的节点。

实现get方法

  1. // get方法:获取指定位置的元素
  2. get(position) {
  3. if (this.length / 2 > position) {
  4. let index = 0;
  5. let current = this.head;
  6. while (index < position) {
  7. current = current.next;
  8. index++;
  9. }
  10. return current.data;
  11. } else {
  12. let index = 0;
  13. let current = this.tail;
  14. while (index < this.length - position - 1) {
  15. current = current.prev;
  16. index++;
  17. }
  18. return current.data;
  19. }
  20. }

get方法就比较简单,和单向链表相似,值得注意的是,如果我们链表中有大量数据,那么无论什么时候都从头找到尾明显不合适,所以可以判断一下需要找的元素的位置偏向前面还是偏向后面,决定我们从前开始找还是从后开始找,有点类似于二分查找,不过我们只分一次,这也是单向链表做不到的

实现indexOf方法

  1. // indexOf方法: 返回元素的索引,没有锗元素就返回-1
  2. indexOf(data) {
  3. let index = 0;
  4. let current = this.head;
  5. while (current) {
  6. if (current.data === data) {
  7. return index;
  8. } else {
  9. current = current.next;
  10. index++;
  11. }
  12. }
  13. return -1;
  14. }

实现update方法

  1. // update方法: 更新方法
  2. update(position, data) {
  3. if (position < 0 || position >= this.length) return false;
  4. let index = 0;
  5. let current = this.head;
  6. while (index < position) {
  7. current = current.next;
  8. index++;
  9. }
  10. current.data = data;
  11. return true;
  12. }

实现removeAt方法

  1. // removeAt方法:移除特定位置的元素
  2. removeAt(position) {
  3. if (position < 0 || position >= this.length) return false;
  4. let index = 0;
  5. let current = this.head;
  6. let prive = null;
  7. if (this.length === 1) {
  8. this.head = null;
  9. this.tail = null;
  10. return true;
  11. }
  12. //一样先判断在移除第一个节点的情况
  13. if (position === 0) {
  14. this.head = current.next;
  15. current.next.prev = this.head;
  16. } else if (position === this.length - 1) {
  17. //判断移除最后一个节点的情况
  18. this.tail.prev.next = null;
  19. this.tail = this.tail.prev;
  20. } else {
  21. //移除中间节点的情况
  22. while (index < position) {
  23. prive = current;
  24. current = current.next;
  25. index++;
  26. }
  27. prive.next = current.next;
  28. current.next.prev = prive;
  29. return true;
  30. }
  31. this.legnth--;
  32. }

removeAt方法稍微复杂一点点,大部分情况都是在考虑节点引用的问题,在移除最后一个节点的时候,也要注意移除顺序,如果我们选择现在将tail指向最后一个节点的上一个,那么我们找到倒数第二个节点的时候就找不到了,因为我们拿不到最后一个节点了,所以需要先将最后一个节点的上一个节点的next先置为空,然后在处理tail的指向。

实现remove方法

  1. // remove方法:移除某个元素
  2. remove(data) {
  3. const index = this.indexOf(data);
  4. return this.removeAt(index);
  5. }

先调用indexOf方法找到移除元素的索引,然后调用removeAt方法将索引传递进去接口

实现isEmpty方法

  1. // isEmpty方法: 判断链表是否为空,是则返回true,否则返回false
  2. isEmpty() {
  3. return this.legnth > 0 ? true : false;
  4. }

实现size方法

  1. // size方法: 返回链表的长度
  2. size() {
  3. return this.length;
  4. }

实现getHead方法

  1. // getHead方法: 返回列表前端元素
  2. getHead() {
  3. return this.head.data;
  4. }

实现getTail方法

  1. // getTail方法: 返回列表后端元素
  2. getTail() {
  3. return this.tail.data;
  4. }