image.png
相信大家都知道猴子捞月的故事,一只猴子抓住另一只猴子,这就是一个典型的链表,每只猴子都是一个节点。

链表的定义

链表是一种物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。链表由一系列节点组成,节点可以在运行时动态生成。下面是一个简单的链表结构示意图:link.jpg
图一

关于链表,我们需要知道几个重要的概念:

1、节点

节点包含两部分,一部分是存储数据元素的数据域,一部分是存储指向下一个节点的指针域。上图蓝色的部分就是数据域,绿色部分就是指针域,它们一起共同构成一个节点。一个节点包含一个 data 属性,该属性表示要加入链表的元素的值;以及一个 next 属性,该属性是指向链表中下一个元素的指针。这个节点可以使用如下的方式定义和使用:

  1. class Node {
  2. constructor(data, next) {
  3. this.data = data;
  4. this.next = next;
  5. }
  6. }
  7. const node1 = new Node(1);
  8. const node2 = new Node(2);
  9. const node3 = new Node(3);
  10. node1.next = node2;
  11. node2.next = node3;
  12. console.log(node1.data);
  13. console.log(node1.next.data);
  14. console.log(node1.next.next.data)

2、首尾节点

链表中的第一个节点是首节点,最后一个节点是尾结点

3、有头链表和无头链表

无头链表
无头链表是指第一个节点既有数据域,又有指针域,第一个节点既是首节点又是头结点,如 图一 就是一个简单的无头链表

有头链表
有头链表是指第一个节点只有指针域,而没有数据域,如下图:
link (1).jpg

实现链表

定义链表类

我们首先定义一个 LinkList 链表类:

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

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

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

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

向链表尾部添加一个元素时,可能有两种情景:

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

    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. // head 永远指向头节点
    11. current = this.head;
    12. // 从头节点开始,循环访问链表,找到最后一项(即尾节点),在尾节点后面追加新节点
    13. while(current.next !== null) {
    14. // 获得 尾节点
    15. current = current.next;
    16. }
    17. // 将尾节点的 next 指针指向新创建的节点
    18. current.next = node;
    19. }
    20. // 链表长度加 1
    21. this.count++;
    22. // 返回 true,表示添加节点成功
    23. return true;
    24. }

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

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

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

第二种情景:向一个不为空的链表添加元素。要向链表的尾部添加一个元素,首先需要找到最后一个元素。我们只有第一个元素的引用,因此需要循环访问列表,直到找到最后一项。为此,我们需要一个指向链表中 current 项的变量。
在循环访问链表的过程中,当 current.next 为 undefined 或null 时,我们就只到已经到达链表尾部了。然后要做的就是让当前(也就是最后一个)元素 current 的 next 指针指向要添加到链表的新节点。

  1. else {
  2. // 链表不为空,向链表尾部追加元素
  3. // head 永远指向头节点
  4. current = this.head;
  5. // 从头节点开始,循环访问链表,找到最后一项(即尾节点),在尾节点后面追加新节点
  6. while(current.next !== null) {
  7. // 获得 尾节点
  8. current = current.next;
  9. }
  10. // 将尾节点的 next 指针指向新创建的节点
  11. current.next = node;
  12. }

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

insert 方法可以在链表的任意位置插入一个新元素。向链表中插入新元素时,我们需要考虑三种情景:

  • 插入的位置不合法,元素插入失败
  • 在链表的起点插入新元素
  • 在链表的中间或尾部添加新元素

    1. insert(element, index) {
    2. // 插入的位置合法,将元素插入到链表中
    3. if (index >= 0 && index <= this.count) {
    4. const node = new Node(element);
    5. // 在链表的第一个位置添加
    6. if (index === 0) {
    7. // current 变量是对链表中第一个元素的引用
    8. const current = this.head;
    9. // 将新添加的节点的 next 指针指向当前的第一个元素,也就是将当前的第一个元素变为是链表中的第二个元素
    10. node.next = current;
    11. // 将 head 指向 node,即链表的第一个元素是新添加的节点
    12. this.head = node;
    13. } else {
    14. // 在链表的中间或尾部添加新元素
    15. // 想要插入新节点的位置的前一个节点
    16. const previous = this.getElementAt(index - 1);
    17. // 想要插入新节点的位置的后一个节点
    18. const current = previous.next;
    19. // 在 previous 和 current 之间插入新节点
    20. // 将 新节点的 next 指针 指向 current
    21. node.next = current;
    22. // 将 前一个节点的 next 指针指向 新节点
    23. previous.next = node;
    24. }
    25. // 更新链表的长度
    26. this.count++;
    27. // 返回 true,表示元素插入成功
    28. return true
    29. }
    30. // 插入的位置不合法,返回 false,表示元素没有添加到链表中
    31. return false;
    32. }

    下面,我们来分析一下这三种情景:
    第一种情景:插入的位置不合法。由于我们是根据位置 (索引) 来插入元素,因此需要检查越界值,如果越界了,就返回 false,表示元素没有添加到链表中。

第二种情景:在链表的起点添加新元素。current 变量是对链表中当前第一个元素的引用,我们需要做的是把要插入元素的 next 指针指向 current ,也就是将链表中当前的第一个元素变成链表中的第二个元素;然后将 head 指向 node ,即此时链表的第一个元素是新添加的节点。

第三种情景:在链表中间或尾部添加一个元素。首先,我们需要迭代链表,找到目标位置。我们循环至 index - 1 的位置,也就是需要添加新节点位置的前一个位置。当我们循环至 index - 1 的位置时跳出循环,此时的 previous 就是对想要插入新元素的位置之前一个元素的引用,current 变量就是我们想要插入新元素的位置后面一个元素的引用。我们是要在 previous 和 current 之间添加新元素,因此,我们把 previous 的 next 指针指向新添加的元素 node,取代 current,然后将新添加节点的 next 指针指向 current,这样就完成了向链表中添加新元素。

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 从链表中移除元素

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. // 移除的是链表中的第一个元素
    6. if (index === 0) {
    7. // 将头节点指向链表的第二个元素即可
    8. this.head = current.next;
    9. } else {
    10. // previous 变量时对要移除节点的前一个节点的引用
    11. const previous = this.getElementAt(index);
    12. // current 变量是对要移除节点的引用
    13. current = previous.next;
    14. previous.next = current.next
    15. }
    16. this.count--;
    17. // 返回移除节点的数据域,即要移除的元素
    18. return current.data;
    19. }
    20. // 传入的index 参数不是有效位置,返回 undefined,即没有从链表中移除元素
    21. return undefined;
    22. }
    下面,我们来分析一下这三种情景:
    第一种情景:移除的位置不合法。由于 removeAt 方法是根据 index 移除链表中的元素,因此我们首先需要对 index 进行有效性检查。从 0(包括 0 )到链表的长度(count - 1)都是有效的位置。如果 index 不是有效的位置,就返回 undefined,即没有从链表中移除元素

第二种情景:移除链表的第一个元素。如果移除的是链表的第一个元素,我们只要将 head 指向链表的第二个元素即可,这样第一个元素自然就被移除了。

  1. // current 变量是对链表第一个元素(即头节点)的引用
  2. let current = this.head;
  3. // 移除的是链表中的第一个元素
  4. if (index === 0) {
  5. // 将头节点指向链表的第二个元素即可
  6. this.head = current.next;
  7. }

第三种情景:移除链表的最后一个或者中间某个元素。如果我们要移除的是链表的最后一个元素或者是中间的某个元素,我们需要迭代链表,直到到达目标位置。其中 current 变量是对所循环链表的当前节点的引用,previous 变量是对当前节点的前一个节点的引用。

在迭代到目标位置之后,current 变量就是我们要移除的节点。我们要移除当前的节点,要做的事情就是将 previous.next 和 current.next 连接起来。这样,当前节点就会被丢弃在计算机内存中,等待被垃圾收集器清除。

迭代查找目标位置的逻辑我们都统一封装在了 getElementAt 方法了,因此我们可以调用 getElementAt 方法来获得要移除节点的前一个节点。

  1. else {
  2. // previous 变量时对要移除节点的前一个节点的引用
  3. const previous = this.getElementAt(index);
  4. // current 变量是对要移除节点的引用
  5. current = previous.next;
  6. previous.next = current.next
  7. }

removeHead() 移除首节点

readmoveHead 方法移除的是链表的第一个元素,即首节点。

要移除首节点,我们要做的事情就是将 head 指向链表中的第二个节点,这样首节点就被丢弃在了计算机内存中,等待着垃圾收集器清除。

  1. removeHead() {
  2. //链表为空,返回 undefined,表示没有移除节点
  3. if (this.head == null) {
  4. return undefined;
  5. }
  6. // current 变量是对首节点的引用
  7. const current = this.head;
  8. // 移除首节点,就是将 head 指向第二个节点
  9. this.head = current.next;
  10. this.count--;
  11. return current.data;
  12. }

removeTail() 移除尾结点

  1. removeTail() {
  2. if (this.head == null) {
  3. return undefined;
  4. }
  5. const previous = this.getElementAt(this.size() - 1);
  6. const current = previous.next;
  7. previous.next = current.next;
  8. this.count--;
  9. return current.data;
  10. }

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

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

  1. indexOf(element) {
  2. // current 变量是对链表第一个元素(即首节点)的引用
  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.data)) {
  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. if (this.head == null) {
  3. return undefined;
  4. }
  5. return this.getElementAt(this.size());
  6. }

size 返回链表的节点个数

size 方法返回链表的节点个数,即返回链表的长度。LinkList 是我们自己构建的类,链表的长度是我们使用 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. // 将 head 置为 null,即可将链表清空
  3. this.head = null;
  4. this.count = 0;
  5. }

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

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

  1. toString() {
  2. // 链表为空,返回空字符串
  3. if (this.head == null) {
  4. return '';
  5. }
  6. let objString = `${this.head.data}`;
  7. let current = this.head.next;
  8. // 迭代链表,将将元素值添加到字符串上
  9. for(let i = 1; i < this.size() && current != null; i++) {
  10. objString = `${objString}, ${current.data}`;
  11. current = current.next;
  12. }
  13. return objString;
  14. }

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

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

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

完整代码

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

链表的应用

基于链表实现栈

在数据结构与算法学习之栈一文中,我们分别基于Object对象和数组实现了栈,现在,我们基于链表来实现栈。

  1. class Stack {
  2. constructor() {
  3. this.items = new LinkList();
  4. }
  5. // 添加元素到栈顶
  6. push(element) {
  7. return this.items.append(element);
  8. }
  9. // 从栈顶移除元素
  10. pop() {
  11. return this.items.removeTail();
  12. }
  13. // 查看栈顶元素
  14. peek() {
  15. return this.items.tail();
  16. }
  17. // 检查栈是否为空
  18. isEmpty() {
  19. return this.items.isEmpty();
  20. }
  21. // 清空栈
  22. clear() {
  23. this.items.clear()
  24. }
  25. // 返回栈里的元素个数
  26. size() {
  27. return this.items.size();
  28. }
  29. }

基于链表实现队列

在数据结构与算法学习之队列一文中,我们分别基于Object对象和数组实现了队列,现在,我们基于链表来实现队列。

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

翻转链表

迭代方式翻转链表
**
思路分析:
我们再考虑算法时,大多数情况是考虑边界情况会让问题变得简单,但边界情况往往不具备普适性,因此,我们也要尝试考虑中间的情况。
现在我们假设链表中间的某个节点为 currentNode,它的前一个节点为 prevNode,下一个节点为 nextNode。我们现在把思路聚焦到这个 currentNode 节点上,只考虑在这一个节点上翻转,将 currentNode 节点的 next 指针指向 prevNode:

  1. currentNode.next = prevNode;

只需要这简单的一个步骤就可以完成 currentNode 节点的翻转,对于头节点来说,它没有上一个节点,让 prevNode = null,表示它的上一个节点是一个空节点。

在遍历的过程中,每完成一个节点的翻转,都让 currentNode = nextNode,找到下一个需要翻转的节点,同时,prevNode 和 nextNode 也跟随 currentNode 一起向后滑动。

  1. // 迭代翻转链表
  2. const reverse_linkList_iterable = (head) => {
  3. if (!head) {
  4. return null;
  5. }
  6. let prevNode = null;
  7. let currentNode = head;
  8. while(currentNode) {
  9. const nextNode = currentNode.next; // 下一个节点
  10. currentNode.next = prevNode; // 将当前节点的 next 指针指向 prevNode,完成对当前节点进行翻转
  11. prevNode = currentNode; // prevNode 向后滑动
  12. currentNode = nextNode; // currentNode 向后滑动
  13. }
  14. // 最后要将 prevNode 返回,当循环结束时,prevNode 指向翻转前链表的最后一个节点
  15. return prevNode
  16. }
  1. const Node = function(data){
  2. this.data = data;
  3. this.next = null;
  4. }
  5. const node1 = new Node(1);
  6. const node2 = new Node(2);
  7. const node3 = new Node(3);
  8. const node4 = new Node(4);
  9. const node5 = new Node(5);
  10. node1.next = node2;
  11. node2.next = node3;
  12. node3.next = node4;
  13. node4.next = node5;
  14. function print(node) {
  15. var curr_node = node;
  16. while(curr_node){
  17. console.log(curr_node.data);
  18. curr_node = curr_node.next;
  19. }
  20. };
  21. print(reverse_iter(node1));

递归方式翻转链表

递归的思想,精髓之处在于甩锅,你做不到的事情,让别人去做,等别人做完了,你在别人的基础上继续做。

甩锅一共分为四步:

  1. 明确函数的功能,既然是先让别人去做,那你得清楚地告诉他做什么。函数 reverse_linkList_Recursion(head) 完成的功能,是从 head 开始翻转链表,函数的返回值是翻转后的头节点。
  2. 正式甩锅,进行递归调用,就翻转链表而言,甩锅的方法如下:

    1. const newHead = reverse_linkList_Recursion(head.next);

    原本是翻转以 head 开头的链表,可是你不会啊,那就先让别人从 head.next 开始翻转链表,等他翻转完,得 到的 newHead 就是翻转后的头节点

  3. 根据别人的结果,计算自己的结果。第二步中,已经完成了从 head.next 开始翻转链表,现在,只需要把 head 连接到新链表上就可以了。新链表的尾结点是 head.next,执行 head.next.next = head,这样,head 就成了新链表的尾结点

    1. // 递归翻转链表
    2. function reverse_linkList_Recursion(head) {
    3. // head 为 null 时,即链表为空链表,直接返回 null
    4. if (!head) {
    5. return null;
    6. }
    7. // 从下一个节点开始进行翻转
    8. const newHead = reverse_linkList_Recursion(head.next);
    9. // 把当前节点连接到新链表上
    10. head.next.next = head;
    11. head.next = null;
    12. return newHead;
    13. }
  4. 找到递归终止的条件。函数最终要返回新链表的头结点,而新链表的头节点正是旧链表的尾结点,所以,遇到尾节点时,直接返回尾节点,这就是递归终止的条件。

    1. // 递归翻转链表
    2. function reverse_linkList_Recursion(head) {
    3. // head 为 null 时,即链表为空链表,直接返回 null
    4. if (!head) {
    5. return null
    6. }
    7. // 旧链表的尾结点,结束递归
    8. if (head.next == null) {
    9. return head;
    10. }
    11. // 从下一个节点开始进行翻转
    12. const newHead = reverse_linkList_Recursion(head.next);
    13. // 把当前节点连接到新链表上
    14. head.next.next = head;
    15. head.next = null;
    16. return newHead;
    17. }

    ```javascript const Node = function(data){ this.data = data; this.next = null; }

const node1 = new Node(1); const node2 = new Node(2); const node3 = new Node(3); const node4 = new Node(4); const node5 = new Node(5);

node1.next = node2; node2.next = node3; node3.next = node4; node4.next = node5;

function print(node) { var curr_node = node; while(curr_node){ console.log(curr_node.data); curr_node = curr_node.next; } };

print(reverse_linkList_Recursion(node1));

  1. <a name="kLCZz"></a>
  2. ### 从尾到头打印链表
  3. 当我们拿到一个链表时,我们得到的是头节点,只有头节点以后的节点被打印了,才能打印头节点,这不正是一个可以甩锅的事情么。
  4. 函数的功能,就是从 head 开始反向打印链表,但是现在不知道如何反向打印,于是甩锅,先从 head.next 开始反向打印,等这部分打印完了,再打印 head 节点。
  5. ```javascript
  6. function reverse_print_linkList(head) {
  7. if (head == null) {
  8. return
  9. } else {
  10. reverse_print_linkList(head.next)
  11. console.log(head.data)
  12. }
  13. }
  1. const Node = function(data){
  2. this.data = data;
  3. this.next = null;
  4. }
  5. const node1 = new Node(1);
  6. const node2 = new Node(2);
  7. const node3 = new Node(3);
  8. const node4 = new Node(4);
  9. const node5 = new Node(5);
  10. node1.next = node2;
  11. node2.next = node3;
  12. node3.next = node4;
  13. node4.next = node5;
  14. reverse_print_linkList(node1)