234-链表-回文链表

栈 | 递归 | 链表 | 双指针

逃课做法

好的,让我们一起来详细解析一下这个判断单链表是否为回文链表的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;
};

这段代码通过构造两个字符串 ab 来判断链表是否为回文。下面我们逐行解释代码的含义。

1. 初始化变量 ab

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 拼接字符串 ab
a = a + head.val;
b = head.val + b;
  • a = a + head.val; 将当前节点的值 head.val 添加到字符串 a 的末尾。这保持了链表的原始顺序。

    例如,对于链表 1 → 2 → 3

    • 第一个节点 1a 变成 "1"
    • 第二个节点 2a 变成 "12"
    • 第三个节点 3a 变成 "123"
  • b = head.val + b; 将当前节点的值 head.val 添加到字符串 b 的开头。这实现了链表值的逆序。

    例如,对于链表 1 → 2 → 3

    • 第一个节点 1b 变成 "1"
    • 第二个节点 2b 变成 "21"
    • 第三个节点 3b 变成 "321"
2.3 移动到下一个节点
head = head.next;

这一步将 head 指向下一个节点,继续遍历整个链表。

3. 比较字符串 ab

return a === b;

在遍历完成后,我们将 ab 进行了比较。如果两者相等,说明链表是回文的;否则,不是回文。

4. 完整的代码逻辑

综合以上步骤,代码的逻辑如下:

  1. 初始化两个空字符串 ab
  2. 遍历整个链表,将节点值按顺序添加到 a 中,并按逆序添加到 b 中。
  3. 比较 ab,如果相等,返回 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)组成。每个节点包含两个部分:

  1. 值(Value):存储节点的数据。
  2. 指针(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;
}

此循环会一直进行,直到 headnull,表示已经遍历完所有节点。

字符串拼接

在JavaScript中,字符串是不可变的。每次对字符串进行拼接操作,实际上会创建一个新的字符串。

在代码中:

a = a + head.val;
b = head.val + b;

这两行代码分别将当前节点的值添加到字符串 a 的末尾和字符串 b 的开头。

回文的判断

回文的核心在于正序和逆序是否相同。通过构造两个字符串,一个按顺序拼接,一个按逆序拼接,然后比较这两个字符串是否相等,可以有效地判断是否为回文。

总结

通过这段代码,我们使用了如下算法思想:

  1. 遍历链表:从头到尾依次访问每个节点。
  2. 构造正序和逆序字符串:分别记录链表的值的正序和逆序。
  3. 比较字符串:通过比较正序和逆序字符串,判断链表是否为回文。

这种方法的时间复杂度是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; // 如果循环结束,说明是回文
};

逻辑分解

  1. 初始化数组

    const vals = [];
    

    这里我们创建一个空数组 vals,用于存储链表中的节点值。

  2. 遍历链表

    while (head) {
        vals.push(head.val);
        head = head.next;
    }
    
    • 这个 while 循环用于遍历整个链表,直到 headnull
    • 在每次迭代中,我们将当前节点的值 head.val 添加到数组 vals 中,然后将 head 移动到下一个节点。

    例如,对于链表 1 → 2 → 3 → 2 → 1,经过这个循环,vals 将会变为 [1, 2, 3, 2, 1]

  3. 双指针初始化

    let start = 0, end = vals.length - 1;
    
    • start 指向数组的开头,end 指向数组的末尾。我们将使用这两个指针来比较数组中的元素。
  4. 双指针比较

    while (start < end) {
        if (vals[start] !== vals[end]) {
            return false;
        }
        start++;
        end--;
    }
    
    • 在这个 while 循环中,我们检查 start 指针指向的值和 end 指针指向的值是否相等。
    • 如果发现不相等的情况,立即返回 false,说明链表不是回文。
    • 如果相等,则两个指针分别向中间移动,继续比较。
  5. 返回结果

    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
};

逻辑分解

  1. 边界条件检查

    if (head == null || head.next == null) {
        return true;
    }
    
    • 如果链表为空 (head == null) 或者只有一个节点 (head.next == null),直接返回 true,因为它们都可以视为回文。
  2. 初始化指针

    let fast = head;  // 快指针
    let slow = head;  // 慢指针
    let prev;         // 用于保存慢指针的前一个节点
    
    • fastslow 都初始化为链表的头节点。fast 每次移动两步,slow 每次移动一步。
  3. 快慢指针遍历链表

    while (fast && fast.next) {
        prev = slow;       // 保存当前慢指针位置
        slow = slow.next;  // 慢指针走一步
        fast = fast.next.next; // 快指针走两步
    }
    
    • fastfast.next 不为 null 时,继续遍历。
    • 在每次迭代中,我们先保存 slow 的位置到 prev,然后移动 slowfast 指针。
    • 当循环结束时,slow 指向链表的中间节点。
  4. 断开链表

    prev.next = null; 
    
    • 将前半段链表的尾部 prev.next 设置为 null,将链表分为两个部分:前半段和后半段。
  5. 翻转后半段链表

    let head2 = null;  // 翻转后的链表头
    while (slow) {
        const tmp = slow.next; // 保存当前节点的下一个节点
        slow.next = head2;     // 翻转当前节点的指针
        head2 = slow;          // 更新翻转后的链表头
        slow = tmp;           // 继续处理下一个节点
    }
    
    • 使用一个新的指针 head2 来存储翻转后的链表。
    • 在每次迭代中,先保存当前节点的下一个节点 tmp,然后将当前节点的 next 指向 head2(翻转指针),最后更新 head2slow
  6. 比对前半段和翻转后的后半段

    while (head && head2) {
        if (head.val !== head2.val) {  // 如果不相等,返回 false
            return false;
        }
        head = head.next;   // 移动前半段指针
        head2 = head2.next; // 移动后半段指针
    }
    
    • 使用两个指针 headhead2 分别指向前半段和后半段的头部,逐个比较节点的值。
    • 如果发现有不相等的情况,立即返回 false
  7. 返回结果

    return true; // 如果没有发现不相等的情况,返回 true
    
    • 如果所有节点都相等,返回 true,表示链表是回文的。

举例说明

让我们通过一个具体的例子来理解这个方法的工作过程。

示例链表1 → 2 → 3 → 2 → 1

  1. 初始化

    • fastslow 都指向 1
  2. 快慢指针遍历

    • 第一次迭代:
      • prev 指向 1slow 移动到 2fast 移动到 3
    • 第二次迭代:
      • prev 指向 2slow 移动到 3fast 移动到 1(链表末尾)。
    • 第三次迭代:
      • prev 指向 3slow 移动到 2fast 移动到 null(结束循环)。

    此时,slow 指向 2prev 指向 3,链表被断开为:

    • 前半段:1 → 2 → 3
    • 后半段:2 → 1
  3. 翻转后半段链表

    • slow 开始(即指向 2),翻转后的链表变为 1 → 2
    • head2 最终指向 1,后半段链表的逆序为 1 → 2
  4. 比对

    • 比较 head(指向 1)和 head2(指向 1),相等。
    • 比较 head(指向 2)和 head2(指向 2),相等。
    • 比较结束,返回 true

时间复杂度与空间复杂度

  • 时间复杂度O(n),其中 n 是链表的长度。我们需要遍历链表两次:一次用于找到中间节点并断开链表,另一次用于翻转后半段并比较。
  • 空间复杂度O(1),只使用了常量级的额外空间,不需要额外的数组或数据结构来存储链表的值。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值