栈 | 递归 | 链表 | 双指针
逃课做法
好的,让我们一起来详细解析一下这个判断单链表是否为回文链表的JavaScript代码。我们将一步一步地理解代码的每个部分,并通过简单的例子帮助你更好地掌握其中的知识点和算法思想。
问题描述
回文链表 是指一个链表从前往后和从后往前的顺序是一样的。比如:
- 1 → 2 → 3 → 2 → 1 是回文链表。
- 1 → 2 → 3 不是回文链表。
我们的任务是编写一个函数 isPalindrome
,它接收一个单链表的头节点 head
,并判断这个链表是否是回文的。如果是,返回 true
;否则,返回 false
。
代码解析
让我们先看看提供的代码:
var isPalindrome = function(head) {
let a = '', b = '';
while (head != null) {
a = a + head.val;
b = head.val + b;
head = head.next;
}
return a === b;
};
这段代码通过构造两个字符串 a
和 b
来判断链表是否为回文。下面我们逐行解释代码的含义。
1. 初始化变量 a
和 b
let a = '', b = '';
a
用于存储链表节点值的顺序拼接结果。b
用于存储链表节点值的逆序拼接结果。
例如,对于链表 1 → 2 → 3 → 2 → 1
:
a
最终会是"12321"
。b
最终会是"12321"
,因为在每次拼接时,当前节点值被加在前面,实现了逆序。
2. 遍历链表
while (head != null) {
a = a + head.val;
b = head.val + b;
head = head.next;
}
2.1 条件 head != null
这是一个常见的遍历链表的条件。当 head
不为 null
时,表示还没有到达链表的末尾,可以继续遍历。
2.2 拼接字符串 a
和 b
a = a + head.val;
b = head.val + b;
-
a = a + head.val;
将当前节点的值head.val
添加到字符串a
的末尾。这保持了链表的原始顺序。例如,对于链表
1 → 2 → 3
:- 第一个节点
1
,a
变成"1"
。 - 第二个节点
2
,a
变成"12"
。 - 第三个节点
3
,a
变成"123"
。
- 第一个节点
-
b = head.val + b;
将当前节点的值head.val
添加到字符串b
的开头。这实现了链表值的逆序。例如,对于链表
1 → 2 → 3
:- 第一个节点
1
,b
变成"1"
。 - 第二个节点
2
,b
变成"21"
。 - 第三个节点
3
,b
变成"321"
。
- 第一个节点
2.3 移动到下一个节点
head = head.next;
这一步将 head
指向下一个节点,继续遍历整个链表。
3. 比较字符串 a
和 b
return a === b;
在遍历完成后,我们将 a
和 b
进行了比较。如果两者相等,说明链表是回文的;否则,不是回文。
4. 完整的代码逻辑
综合以上步骤,代码的逻辑如下:
- 初始化两个空字符串
a
和b
。 - 遍历整个链表,将节点值按顺序添加到
a
中,并按逆序添加到b
中。 - 比较
a
和b
,如果相等,返回true
,否则返回false
。
举例说明
让我们通过一个具体的例子来理解这个过程。
示例链表 1: 1 → 2 → 3 → 2 → 1
(回文链表)
- 初始化:
a = ""
,b = ""
- 第一节点
1
:a = "" + "1" = "1"
b = "1" + "" = "1"
- 移动到下一个节点
- 第二节点
2
:a = "1" + "2" = "12"
b = "2" + "1" = "21"
- 移动到下一个节点
- 第三节点
3
:a = "12" + "3" = "123"
b = "3" + "21" = "321"
- 移动到下一个节点
- 第四节点
2
:a = "123" + "2" = "1232"
b = "2" + "321" = "2321"
- 移动到下一个节点
- 第五节点
1
:a = "1232" + "1" = "12321"
b = "1" + "2321" = "12321"
- 移动到下一个节点(此时
head = null
,结束循环)
- 比较:
a = "12321"
,b = "12321"
,相等,返回true
示例链表 2: 1 → 2 → 3
(非回文链表)
- 初始化:
a = ""
,b = ""
- 第一节点
1
:a = "" + "1" = "1"
b = "1" + "" = "1"
- 移动到下一个节点
- 第二节点
2
:a = "1" + "2" = "12"
b = "2" + "1" = "21"
- 移动到下一个节点
- 第三节点
3
:a = "12" + "3" = "123"
b = "3" + "21" = "321"
- 移动到下一个节点(此时
head = null
,结束循环)
- 比较:
a = "123"
,b = "321"
,不相等,返回false
相关知识点
单链表(Singly Linked List)
单链表是一种基本的数据结构,由一系列节点(Node)组成。每个节点包含两个部分:
- 值(Value):存储节点的数据。
- 指针(Pointer):指向下一个节点的引用。
例如,链表 1 → 2 → 3
可以表示为:
Node1: val = 1, next -> Node2
Node2: val = 2, next -> Node3
Node3: val = 3, next -> null
链表遍历
遍历链表意味着从头节点开始,依次访问每个节点,直到到达链表的末尾(即 next
指针为 null
)。
在我们的代码中:
while (head != null) {
// 处理当前节点
head = head.next;
}
此循环会一直进行,直到 head
为 null
,表示已经遍历完所有节点。
字符串拼接
在JavaScript中,字符串是不可变的。每次对字符串进行拼接操作,实际上会创建一个新的字符串。
在代码中:
a = a + head.val;
b = head.val + b;
这两行代码分别将当前节点的值添加到字符串 a
的末尾和字符串 b
的开头。
回文的判断
回文的核心在于正序和逆序是否相同。通过构造两个字符串,一个按顺序拼接,一个按逆序拼接,然后比较这两个字符串是否相等,可以有效地判断是否为回文。
总结
通过这段代码,我们使用了如下算法思想:
- 遍历链表:从头到尾依次访问每个节点。
- 构造正序和逆序字符串:分别记录链表的值的正序和逆序。
- 比较字符串:通过比较正序和逆序字符串,判断链表是否为回文。
这种方法的时间复杂度是O(n),空间复杂度也是O(n),其中n是链表的长度。对于初学者来说,这种方法直观且易于理解,非常适合作为解决回文链表问题的入门方法。
希望通过以上详细的解释,你能更好地理解这段代码的工作原理和背后的算法思想。如果你有任何疑问,欢迎继续提问!
我们来看一下这个方法一的实现,它通过将链表的值转存到数组中,然后利用双指针来判断是否是回文链表。接下来,我们将逐步解析这段代码,理解其逻辑和背后的算法思想。
方法一:转成数组
代码解析
const isPalindrome = (head) => {
const vals = [];
// 遍历链表,将值放入数组中
while (head) {
vals.push(head.val);
head = head.next;
}
let start = 0, end = vals.length - 1; // 双指针初始化
// 使用双指针判断是否回文
while (start < end) {
if (vals[start] !== vals[end]) { // 如果不同,返回 false
return false;
}
start++; // 向中间移动
end--; // 向中间移动
}
return true; // 如果循环结束,说明是回文
};
逻辑分解
-
初始化数组:
const vals = [];
这里我们创建一个空数组
vals
,用于存储链表中的节点值。 -
遍历链表:
while (head) { vals.push(head.val); head = head.next; }
- 这个
while
循环用于遍历整个链表,直到head
为null
。 - 在每次迭代中,我们将当前节点的值
head.val
添加到数组vals
中,然后将head
移动到下一个节点。
例如,对于链表
1 → 2 → 3 → 2 → 1
,经过这个循环,vals
将会变为[1, 2, 3, 2, 1]
。 - 这个
-
双指针初始化:
let start = 0, end = vals.length - 1;
start
指向数组的开头,end
指向数组的末尾。我们将使用这两个指针来比较数组中的元素。
-
双指针比较:
while (start < end) { if (vals[start] !== vals[end]) { return false; } start++; end--; }
- 在这个
while
循环中,我们检查start
指针指向的值和end
指针指向的值是否相等。 - 如果发现不相等的情况,立即返回
false
,说明链表不是回文。 - 如果相等,则两个指针分别向中间移动,继续比较。
- 在这个
-
返回结果:
return true;
- 如果循环结束时没有返回
false
,则说明所有对应元素都相等,链表是回文,返回true
。
- 如果循环结束时没有返回
举例说明
让我们通过一个具体的例子来说明这个方法的工作过程。
示例链表:1 → 2 → 3 → 2 → 1
- 在遍历链表时,
vals
将被填充为[1, 2, 3, 2, 1]
。 - 初始化双指针:
start = 0
(指向1
)end = 4
(指向1
,即vals.length - 1
)
比较过程:
-
第一次比较:
vals[0]
(1)与vals[4]
(1)相等,指针移动:start = 1
(指向2
)end = 3
(指向2
)
-
第二次比较:
vals[1]
(2)与vals[3]
(2)相等,指针移动:start = 2
(指向3
)end = 2
(指向3
)
-
此时
start
不再小于end
,循环结束,返回true
。
时间复杂度与空间复杂度
- 时间复杂度:
O(n)
,其中n
是链表的长度。我们需要遍历链表一次来构建数组,然后又遍历一次数组来进行比较。 - 空间复杂度:
O(n)
,因为我们使用了额外的数组vals
来存储链表的值。
我们来详细分析这个使用快慢指针的方法来判断单链表是否为回文链表的实现。这个方法的优势在于它的空间复杂度为 (O(1)),只使用常量级的额外空间。接下来,我们逐步解析这段代码,理解其逻辑和实现原理。
方法二:快慢指针
代码解析
const isPalindrome = (head) => {
// 如果链表为空或只有一个节点,直接返回 true
if (head == null || head.next == null) {
return true;
}
let fast = head; // 快指针
let slow = head; // 慢指针
let prev; // 用于保存慢指针的前一个节点
// 快慢指针遍历链表
while (fast && fast.next) {
prev = slow; // 保存当前慢指针位置
slow = slow.next; // 慢指针走一步
fast = fast.next.next; // 快指针走两步
}
// 断开链表,将前半段和后半段分开
prev.next = null;
// 翻转后半段链表
let head2 = null; // 翻转后的链表头
while (slow) {
const tmp = slow.next; // 保存当前节点的下一个节点
slow.next = head2; // 翻转当前节点的指针
head2 = slow; // 更新翻转后的链表头
slow = tmp; // 继续处理下一个节点
}
// 比对前半段和翻转后的后半段
while (head && head2) {
if (head.val !== head2.val) { // 如果不相等,返回 false
return false;
}
head = head.next; // 移动前半段指针
head2 = head2.next; // 移动后半段指针
}
return true; // 如果没有发现不相等的情况,返回 true
};
逻辑分解
-
边界条件检查:
if (head == null || head.next == null) { return true; }
- 如果链表为空 (
head == null
) 或者只有一个节点 (head.next == null
),直接返回true
,因为它们都可以视为回文。
- 如果链表为空 (
-
初始化指针:
let fast = head; // 快指针 let slow = head; // 慢指针 let prev; // 用于保存慢指针的前一个节点
fast
和slow
都初始化为链表的头节点。fast
每次移动两步,slow
每次移动一步。
-
快慢指针遍历链表:
while (fast && fast.next) { prev = slow; // 保存当前慢指针位置 slow = slow.next; // 慢指针走一步 fast = fast.next.next; // 快指针走两步 }
- 当
fast
和fast.next
不为null
时,继续遍历。 - 在每次迭代中,我们先保存
slow
的位置到prev
,然后移动slow
和fast
指针。 - 当循环结束时,
slow
指向链表的中间节点。
- 当
-
断开链表:
prev.next = null;
- 将前半段链表的尾部
prev.next
设置为null
,将链表分为两个部分:前半段和后半段。
- 将前半段链表的尾部
-
翻转后半段链表:
let head2 = null; // 翻转后的链表头 while (slow) { const tmp = slow.next; // 保存当前节点的下一个节点 slow.next = head2; // 翻转当前节点的指针 head2 = slow; // 更新翻转后的链表头 slow = tmp; // 继续处理下一个节点 }
- 使用一个新的指针
head2
来存储翻转后的链表。 - 在每次迭代中,先保存当前节点的下一个节点
tmp
,然后将当前节点的next
指向head2
(翻转指针),最后更新head2
和slow
。
- 使用一个新的指针
-
比对前半段和翻转后的后半段:
while (head && head2) { if (head.val !== head2.val) { // 如果不相等,返回 false return false; } head = head.next; // 移动前半段指针 head2 = head2.next; // 移动后半段指针 }
- 使用两个指针
head
和head2
分别指向前半段和后半段的头部,逐个比较节点的值。 - 如果发现有不相等的情况,立即返回
false
。
- 使用两个指针
-
返回结果:
return true; // 如果没有发现不相等的情况,返回 true
- 如果所有节点都相等,返回
true
,表示链表是回文的。
- 如果所有节点都相等,返回
举例说明
让我们通过一个具体的例子来理解这个方法的工作过程。
示例链表:1 → 2 → 3 → 2 → 1
-
初始化:
fast
和slow
都指向1
。
-
快慢指针遍历:
- 第一次迭代:
prev
指向1
,slow
移动到2
,fast
移动到3
。
- 第二次迭代:
prev
指向2
,slow
移动到3
,fast
移动到1
(链表末尾)。
- 第三次迭代:
prev
指向3
,slow
移动到2
,fast
移动到null
(结束循环)。
此时,
slow
指向2
,prev
指向3
,链表被断开为:- 前半段:
1 → 2 → 3
- 后半段:
2 → 1
- 第一次迭代:
-
翻转后半段链表:
- 从
slow
开始(即指向2
),翻转后的链表变为1 → 2
。 head2
最终指向1
,后半段链表的逆序为1 → 2
。
- 从
-
比对:
- 比较
head
(指向1
)和head2
(指向1
),相等。 - 比较
head
(指向2
)和head2
(指向2
),相等。 - 比较结束,返回
true
。
- 比较
时间复杂度与空间复杂度
- 时间复杂度:
O(n)
,其中n
是链表的长度。我们需要遍历链表两次:一次用于找到中间节点并断开链表,另一次用于翻转后半段并比较。 - 空间复杂度:
O(1)
,只使用了常量级的额外空间,不需要额外的数组或数据结构来存储链表的值。