循环链表

循环链表是另一种形式的链式存储结构。它的特点是表中最后一个节点的 next 指针指向头节点,整个链表形成一个环。如下图:
circle-link.jpg
循环链表与链表 (普通链表) 的唯一区别在于,链表的最后一个节点的 next 指针指向的是 undefined,而循环链表的最后一个节点的 next 指针指向的是头节点。

实现循环链表

定义节点类

  1. class Node {
  2. constructor(element) {
  3. this.element = element;
  4. this.next = undefined;
  5. }
  6. }

定义循环链表类

  1. function defaultEquals(a, b) {
  2. return a === b;
  3. }
  4. class CircularLinkedList {
  5. constructor(equalsFn = defaultEquals) {
  6. this.count = 0;
  7. this.head = undefined;
  8. this.equalsFn = qeualsFn;
  9. }
  10. }

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

下面,我们逐一实现这些方法:

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

向循环链表尾部添加一个新元素时,有两种情景:

  • 循环链表为空,添加的是第一个yuans
  • 循环链表不为空,向其追加元素
    1. append(element) {
    2. const node = new Node(element);
    3. let current = null;
    4. // 循环链表为空,添加的是第一个节点
    5. if (this.head == null) {
    6. // 让 head 指向新添加的节点
    7. this.head = node;
    8. } else {
    9. // 循环链表不为空,在循环链表尾部添加新节点
    10. current = this.getElementAt(this.size() - 1);
    11. // 将当前的尾节点的 next 指针指向node,node变成新的尾节点
    12. current.next = node;
    13. }
    14. // 将尾节点的 next 指针指向头节点,形成一个环
    15. node.next = this.head;
    16. this.count++;
    17. }

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

第一种情景:向空的循环链表添加一个元素。当我们创建一个循环链表实例时,head 默认指向 undefined,也就意味着此时的循环链表是一个空链表,我们是在空的循环链表添加第一个元素,因此我们要做的就是让 head 指向这个新添加的节点 node,同时要把新节点的 next 指针指向 head,也就是指向自己。

  1. if (this.head == null) {
  2. // 让 head 指向新添加的节点
  3. this.head = node;
  4. }
  5. // 将尾节点的 next 指针指向头节点,形成一个环
  6. node.next = this.head;

第二种情景:循环链表不为空,向其追加元素。向一个不为空的循环链表添加一个新元素,首先需要找到最后一个元素。我们使用 getElementAt 方法获取循环链表的最后一个元素,然后将它的 next 指针指向新添加的节点,同时将新节点的 next 指针指向头节点。

  1. else {
  2. // 循环链表不为空,在循环链表尾部添加新节点
  3. current = this.getElementAt(this.size() - 1);
  4. // 将当前的尾节点的 next 指针指向node,node变成新的尾节点
  5. current.next = node;
  6. }
  7. // 将尾节点的 next 指针指向头节点,形成一个环
  8. node.next = this.head;

insert 向循环链表的特定位置插入一个新元素

向循环链表中插入一个新元素,我们需要考虑三种情景:

  • 插入的位置不合法,元素插入失败
  • 在循环链表的第一个位置(头部)插入新元素
  • 在循环链表的中间或尾部插入新元素

    1. insert(element, index) {
    2. // 插入的位置合法,将元素插入到循环链表中
    3. if (index >= 0 && index <= this.count) {
    4. const node = new Node(element);
    5. // current 变量是对头节点的引用
    6. let current = this.head;
    7. if (index === 0) {
    8. // 在循环链表的第一个位置插入新元素
    9. if (this.head == null) {
    10. // 在空循环链表的第一个位置插入新元素
    11. // 将 head 指向新添加的节点
    12. this.head = node;
    13. // 将新添加节点的 next 指针指向 head,即指向自己
    14. node.next = this.head;
    15. } else {
    16. // 在非空循环链表的第一个位置插入新元素
    17. // 将新添加节点 node 的 next 指针指向现在的 head 引用的头节点(current 变量)
    18. node.next = current;
    19. // 获取循环链表中最后一个节点
    20. current = this.getElementAt(this.size());
    21. // 将 head 指向新添加的 node 节点,即新的头节点
    22. this.head = node;
    23. // 此时的 current 变量是循环链表最后一个节点的引用
    24. // 将最后一个节点的 next 指针指向 head,即指向头节点
    25. current.next = this.head;
    26. }
    27. } else {
    28. // 在循环链表的中间或尾部插入新元素
    29. // 新节点插入位置的前一个节点
    30. const previous = this.getElementAt(index - 1);
    31. // 将新插入元素的 next 指针指向插入位置的下一个节点
    32. node.next = previous.next;
    33. // 将新节点插入位置的前一个节点的 next 指针指向新插入的节点
    34. previous.next = node;
    35. }
    36. // 更新循环链表的长度
    37. this.count++;
    38. // 返回 true ,表示元素插入成功
    39. return true;
    40. }
    41. // 插入的位置不合法,返回false,表示元素没有添加到循环链表中
    42. return false;
    43. }

    下面,我们来分析一下这三种情景:

第一种情景:插入的位置不合法。由于我们是根据位置(索引)插入元素,因此在插入前需要先检查越界值,如果越界了,就返回false,表示元素没有插入到循环链表中。

第二种情景:在循环链表的第一个位置(头部)插入新元素。
第一种情况,在空链表的第一个位置插入新元素,将 head 赋值为新添加的元素,并将新添加元素的 next 指针指向 head。在这种情况下,新添加的元素即是头节点也是尾节点,因此它的 next 指针指向自己。
第二种情况,在一个非空循环链表的第一个位置插入一个新元素。我们首先将新插入节点的 next 指针指向当前的头节点(head),新插入节点变成了新的头节点,然后将循环链表的最后一个节点的 next 指针指向新的头节点,即指向新插入的节点。

第三种情景:在循环链表的中间或尾部插入新元素。我们首先找到插入位置的前一个节点 previous,然后将 previous 的 next 指针指向新插入的节点,再将新插入节点的next指针指向插入位置的后一个节点,这样,新节点就插入成功了。

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 从循环链表的特定位置移除一个元素

从循环链表的特定位置移除一个元素时, 我们需要考虑三种情景:

  • 移除的位置不合法,移除移除失败
  • 移除循环链表的第一个元素
  • 移除循环链表的最后一个或中间的元素

    1. removeAt(index) {
    2. //
    3. if (index >= 0 && index < this.count) {
    4. // current 变量是对循环链表第一个元素(即首节点)的引用
    5. let current = this.head;
    6. // 移除的是循环链表中的第一个元素
    7. if (index === 0) {
    8. // 从只有一个元素的循环链表中删除一个元素
    9. if (this.size() === 1) {
    10. // 将 head 赋值为 undefined 即可
    11. this.head = undefined;
    12. } else {
    13. // 从非空循环链表移除第一个元素
    14. // 保存当前 head 元素的引用
    15. const removed = this.head;
    16. // 循环链表中最后一个元素
    17. current = this.getElementAt(this.size() - 1);
    18. // 将 head 指向第二个元素
    19. this.head = this.head.next;
    20. // 将最后一个元素是 next 指针指向新的 head
    21. current.next = this.head;
    22. // 更新 current 变量为要移除的第一个元素
    23. current = removed;
    24. }
    25. } else {
    26. // 移除位置的前一个节点
    27. const previous = this.getElementAt(index - 1);
    28. // 要移除的元素
    29. current = previous.next;
    30. // 将移除位置的前一个节点的 next 指针指向移除位置的下一个节点
    31. previous.next = current.next;
    32. }
    33. // 更新循环链表的长度
    34. this.count--;
    35. // 返回移除节点的数据域
    36. return current.element;
    37. }
    38. // 传入的 index 不是有效位置,返回 undefined,表示没有从循环链表中移除元素
    39. return undefined;
    40. }

    下面,我们来分析一下这三种情景:

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

第二种情景:移除循环链表的第一个元素。
第一种情况,从只有一个元素的循环链表中移除一个元素,我们只需要将 head 赋值为 undefined 即可。
第二种情况,从一个非空的循环链表中移除第一个元素。在这种情况下,head 的指向会发生改变,我们需要修改最后一个节点的 next 指针的指向。因此,我们首先保存现在的 head 元素的引用,它将会从循环链表中移除。然后获取循环链表中最后一个节点,将 head 指向循环链表中的第二个元素,也就是新的 head,然后将最后一个节点的 next 指针指向新的head。这样,就将原来的 第一个元素移除了。

第三种情景:移除循环链表的最后一个或中间的元素。我们首先获取到移除位置的前一个节点 previous,通过 previous.next 获得要移除的元素,然后将 previous 的 next 指针指向移除位置的后一个节点,即可将我们想要移除的元素从循环链表中移除了。

removeHead 移除首节点

  1. removeHead() {
  2. if (this.isEmpty()) {
  3. return undefined;
  4. }
  5. let current = this.head;
  6. // 从只有一个元素的循环链表中删除一个元素
  7. if (this.size() === 1) {
  8. // 将 head 赋值为 undefined 即可
  9. this.head = undefined;
  10. } else {
  11. // 从非空循环链表移除第一个元素
  12. // 保存当前 head 元素的引用
  13. const removed = this.head;
  14. // 循环链表中最后一个元素
  15. current = this.getElementAt(this.size() - 1);
  16. // 将 head 指向第二个元素
  17. this.head = this.head.next;
  18. // 将最后一个元素是 next 指针指向新的 head
  19. current.next = this.head;
  20. // 更新 current 变量为要移除的第一个元素
  21. current = removed;
  22. }
  23. this.count--;
  24. return current.element;
  25. }

removeTail 移除尾节点

  1. removeTail() {
  2. if (this.isEmpty()) {
  3. return undefined;
  4. }
  5. // 尾节点的前一个节点
  6. const previous = this.getElementAt(this.size() - 1);
  7. // 要移除的尾节点
  8. const current = previous.next;
  9. // 将尾节点的前一个节点的 next 指针指向 head (current.next 指向的是 head)
  10. previous.next = current.next;
  11. this.count--;
  12. return current.element
  13. }

移除尾节点,首先需要找到尾节点的前一个节点 previous,然后通过 previous.next 获取到尾节点,并将尾节点缓存下来,接着将 previous 的 next 指针指向 head (尾节点的next指针指向的是 head),这样,尾节点就从循环链表中移除了。

indexOf 返回指定元素在循环链表中的索引

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

代码分析:
从循环链表中查找某个节点的位置,就需要对循环链表进行迭代。我们从 head(索引 0)开始,一直到循环链表长度(count 变量)为止,也就是迭代到循环链表的尾部。

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

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

head 查看首节点

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

tail 查看尾结点

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

size 返回循环链表包含的元素个数

size 方法返回循环链表的节点个数,即返回循环链表的长度。CircularLinkedList 是我们自己构建的类,链表的长度是我们使用 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.count = 0;
  4. }

toString 返回表示整个循环链表的字符串

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

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

代码分析:
首先,如果循环链表为空,我们就返回一个空字符串;

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

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

完整代码

  1. // 元素是否相等比较函数
  2. function defaultEquals(a, b) {
  3. return a === b;
  4. }
  5. // 节点类
  6. class Node {
  7. constructor(element) {
  8. this.element = element;
  9. this.next = null;
  10. }
  11. }
  12. class CircularLinkedList {
  13. constructor(equalsFn = defaultEquals) {
  14. this.count = 0;
  15. this.head = undefined;
  16. this.equalsFn = quealsFn;
  17. }
  18. // 向循环链表尾部添加一个新元素
  19. append(element) {
  20. const node = new Node(element);
  21. let current = null;
  22. if (this.head == null) {
  23. this.head = node;
  24. } else {
  25. current = this.getElementAt(this.size() - 1);
  26. current.next = node;
  27. }
  28. node.next = this.head;
  29. this.count++;
  30. }
  31. // 向循环链表的特定位置插入一个新元素
  32. insert(element, index) {
  33. if (index >= 0 && index <= this.count) {
  34. const node = new Node(element);
  35. let current = this.head;
  36. if (index === 0) {
  37. if (this.head == null) {
  38. this.head = node;
  39. node.next = this.head;
  40. } else {
  41. node.next = current;
  42. current = this.getElementAt(this.size());
  43. this.head = node;
  44. current.next = this.head;
  45. }
  46. } else {
  47. const previous = this.getElementAt(this.size() - 1);
  48. node.next = previous.next;
  49. previous.next = node;
  50. }
  51. this.count++;
  52. return true;
  53. }
  54. return false;
  55. }
  56. // 返回循环链表中特定位置的元素
  57. getElementAt(index) {
  58. if (index >= 0 && index <= this.count) {
  59. let node = this.head;
  60. for (let i = 0; i < index && node != null; i++) {
  61. node = node.next;
  62. }
  63. return node;
  64. }
  65. return undefined;
  66. }
  67. // 从循环链表中移除一个元素
  68. remove(element) {
  69. const index = this.indexOf(element);
  70. return this.removeAt(index);
  71. }
  72. // 从循环链表的特定位置移除一个元素
  73. removeAt(index) {
  74. if (index >= 0 && index < this.count) {
  75. let current = this.head;
  76. if (index === 0) {
  77. if (this.size() === 1) {
  78. this.head = undefined;
  79. } else {
  80. const removed = this.head;
  81. current = this.getElementAt(this.size() - 1);
  82. this.head = this.head.next;
  83. current.next = this.head;
  84. current = removed;
  85. }
  86. } else {
  87. const previous = this.getElementAt(index - 1);
  88. current = previous.next;
  89. previous.next = current.next;
  90. }
  91. this.count--;
  92. return current.element;
  93. }
  94. return undefined;
  95. }
  96. // 移除首节点
  97. removeHead() {
  98. if (this.isEmpty()) {
  99. return undefined;
  100. }
  101. let current = this.head;
  102. if (this.size() === 1) {
  103. this.head = undefined;
  104. } else {
  105. const removed = this.head;
  106. current = this.getElementAt(this.size() -1);
  107. this.head = this.head.next;
  108. current.next = this.head;
  109. current = removed;
  110. }
  111. this.count--;
  112. return current.element;
  113. }
  114. // 移除尾节点
  115. removeTail() {
  116. if (this.isEmpty()) {
  117. return undefined;
  118. }
  119. const previous = this.getElementAt(this.size() -1);
  120. const current = previous.next;
  121. previous.next = current.next;
  122. this.count--;
  123. return current.element;
  124. }
  125. // 返回指定元素在循环链表中的索引
  126. indexOf() {
  127. let current = this.head;
  128. for (let i = 0; i < this.size() && current != null; i++) {
  129. if (this.equalsFn(element, current.element)) {
  130. return i;
  131. }
  132. current = current.next;
  133. }
  134. return -1;
  135. }
  136. // 查看首节点
  137. head() {
  138. return this.head;
  139. }
  140. // 查看尾节点
  141. tail() {
  142. return this.getElementAt(this.size())
  143. }
  144. // 返回循环链表包含的元素个数
  145. size() {
  146. return this.count;
  147. }
  148. // 判读循环链表是否为空
  149. isEmpty() {
  150. return this.size() === 0;
  151. }
  152. // 清空循环链表
  153. clear() {
  154. this.head = null;
  155. this.count = 0;
  156. }
  157. // 返回整个循环链表的字符串
  158. toString() {
  159. if (this.head == null) {
  160. return '';
  161. }
  162. let objString = `${this.head.element}`;
  163. let current = this.head.next;
  164. for (let i = 1; i < this.size && current != null; i++) {
  165. objString = `${objString}, ${current.element}`;
  166. current = current.next;
  167. }
  168. return objString;
  169. }
  170. }