【链表题】——回文链表

回文链表

力扣234. 回文链表
判断回文链表请添加图片描述

如果是,返回 true ;否则,返回 false 。

思路1:直接反转整个链表,与原链表对比

这个思路最容易想到,但是时空复杂度高。如果直接反转整个链表然后与原链表对比的方法,虽然在反转过程中只使用了几个额外的指针变量,但反转后的链表确实占用了与原链表相同的空间。

所以这种方法的空间复杂度不是 O ( 1 ) O(1) O(1),而是 O ( n ) O(n) O(n),其中n是链表的长度,因为存储了反转后的整个链表。

一、时间复杂度分析

反转链表和对比两个链表都需要遍历整个链表一次,总的时间复杂度为 O ( n ) + O ( n ) = O ( n ) O(n)+O(n)=O(n) O(n)+O(n)=O(n)

二、空间复杂度分析

在反转链表过程中,生成了一个与原链表长度相同的反转后的链表,所以空间复杂度为 O ( n ) O(n) O(n)

三、 Python 代码及分析

# Definition for singly-linked list.
# class ListNode:
#     def __init__(self, val=0, next=None):
#         self.val = val
#         self.next = next

class Solution:
    def isPalindrome(self, head):
        # 迭代法反转链表
        prev = None
        curr = head
        while curr:
            next_node = curr.next
            curr.next = prev
            prev = curr
            curr = next_node

        # 对比反转后的链表和原链表
        reversed_head = prev
        original_head = head
        is_palindrome = True
        while original_head and reversed_head:
            if original_head.val!= reversed_head.val:
                is_palindrome = False
                break
            original_head = original_head.next
            reversed_head = reversed_head.next

        # 再次反转链表恢复原状(可选)
        prev = None
        curr = reversed_head
        while curr:
            next_node = curr.next
            curr.next = prev
            prev = curr
            curr = next_node

        return is_palindrome

运行结果:解答错误 79 / 93 个通过的测试用例

在这个实现中,反转链表后占用了额外的空间来存储反转后的链表,所以空间复杂度为 O ( n ) O(n) O(n)。而时间复杂度仍然是 O ( n ) O(n) O(n),因为需要遍历链表两次,一次用于反转,一次用于对比。

思路二:递归法反转再比较呢?

递归的思路反转原始链表再对比来判断是否为回文链表,可以这样分析:

一、时间复杂度

  1. 反转链表部分:
    • 在递归反转链表的过程中,每个节点都会被访问一次,因此反转链表的时间复杂度为 O ( n ) O(n) O(n),其中 n 是链表的长度。
  2. 对比部分:
    • 同样,对比两个链表也需要遍历整个链表一次,时间复杂度为 O ( n ) O(n) O(n)
    • 总体时间复杂度为 O ( n ) + O ( n ) = O ( n ) O(n)+O(n)=O(n) O(n)+O(n)=O(n)

二、空间复杂度

  1. 递归调用栈:
    • 递归反转链表时,每一次递归调用都会在栈上占用一定的空间。在最坏情况下,递归的深度为链表的长度 n,因此递归调用栈的空间复杂度为 O ( n ) O(n) O(n)
  2. 对比部分:
    • 对比两个链表时不需要额外的空间与输入规模相关的空间。

所以总体空间复杂度为 O ( n ) O(n) O(n)

下面是一个用递归反转链表并对比判断回文链表的 Python 代码示例:

# Definition for singly-linked list.
# class ListNode:
#     def __init__(self, val=0, next=None):
#         self.val = val
#         self.next = next

class Solution:
    def isPalindrome(self, head):
        self.original_head = head
        reversed_head = self.reverseListRecursive(head)
        is_palindrome = True
        while head and reversed_head:
            if head.val!= reversed_head.val:
                is_palindrome = False
                break
            head = head.next
            reversed_head = reversed_head.next
        return is_palindrome
    
    def reverseListRecursive(self, head):
        if not head or not head.next:
            return head
        new_head = self.reverseListRecursive(head.next)
        head.next.next = head
        head.next = None
        return new_head

上面的代码有个错误!
对比阶段!
这个问题是因为在比较的时候,没有考虑到原始链表和反转后的链表在比较过程中需要同时移动,而不是只移动一个链表。修改后的代码如下:

# Definition for singly-linked list.
# class ListNode:
#     def __init__(self, val=0, next=None):
#         self.val = val
#         self.next = next

class Solution:
    def isPalindrome(self, head):
        self.original_head = head
        reversed_head = self.reverseListRecursive(head)
        is_palindrome = True
        while reversed_head:
            if head.val!= reversed_head.val:
                is_palindrome = False
                break
            head = head.next
            reversed_head = reversed_head.next
        return is_palindrome
    
    def reverseListRecursive(self, head):
        if not head or not head.next:
            return head
        new_head = self.reverseListRecursive(head.next)
        head.next.next = head
        head.next = None
        return new_head
AttributeError: 'NoneType' object has no attribute 'val'
       ^^^^^^^^
    if head.val!= reversed_head.val:
Line 8 in isPalindrome (Solution.py)
          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    ret = Solution().isPalindrome(param_1)
Line 42 in _driver (Solution.py)
    _driver()
Line 53 in <module> (Solution.py)

这个错误是因为在比较时,head可能已经为None,但仍然尝试访问其val属性。这通常发生在链表长度为奇数的情况下,当比较完中间节点后,继续比较时head已经到达链表末尾,为None

可以在比较循环中添加额外的条件检查来避免这个错误:

# Definition for singly-linked list.
# class ListNode:
#     def __init__(self, val=0, next=None):
#         self.val = val
#         self.next = next

class Solution:
    def isPalindrome(self, head):
        self.original_head = head
        reversed_head = self.reverseListRecursive(head)
        is_palindrome = True
        while reversed_head:
            if not head:
                break
            if head.val!= reversed_head.val:
                is_palindrome = False
                break
            head = head.next
            reversed_head = reversed_head.next
        return is_palindrome
    
    def reverseListRecursive(self, head):
        if not head or not head.next:
            return head
        new_head = self.reverseListRecursive(head.next)
        head.next.next = head
        head.next = None
        return new_head

在这个版本中,只在比较循环中检查反转后的链表是否为空,确保原始链表和反转后的链表同时移动并进行比较。
综上所述,使用递归反转链表再对比的方法,时间复杂度为 O ( n ) O(n) O(n),空间复杂度为 O ( n ) O(n) O(n)。这种方法虽然在实现上比较直观,但由于空间复杂度较高,在实际应用中可能不是最优的选择。

思路3:快慢指针找中点并反转后半部分链表的方法(时间复杂度 O(n),空间复杂度 O(1))

  1. 思路分析

    • 首先使用快慢指针找到链表的中点。快指针每次移动两步,慢指针每次移动一步,当快指针到达链表末尾时,慢指针就位于链表的中点。
    • 如果链表长度为奇数,慢指针正好指向中点节点;如果链表长度为偶数,慢指针指向中间两个节点的后一个。
    • 从慢指针的位置开始,反转后半部分链表。
    • 然后同时从链表的头部和反转后的后半部分的头部开始遍历,比较对应节点的值是否相等。如果有不相等的情况,则说明链表不是回文链表。
  2. 代码实现思路

    • 定义两个指针slowfast,初始都指向链表的头节点。
    • 使用一个循环,当fastfast.next都不为空时,slowfast分别向前移动一步和两步。
    • 找到中点后,使用迭代的方法反转后半部分链表,定义三个指针prevcurrnext_node,逐个改变节点的指针方向。
    • 最后同时遍历前半部分和反转后的后半部分链表,比较对应节点的值是否相等。
  3. Python 代码示例

# Definition for singly-linked list.
# class ListNode:
#     def __init__(self, val=0, next=None):
#         self.val = val
#         self.next = next

class Solution:
    def isPalindrome(self, head):
        slow = fast = head
        # 找到链表中点
        while fast and fast.next:
            slow = slow.next
            fast = fast.next.next
        # 反转后半部分链表
        prev = None
        curr = slow
        while curr:
            next_node = curr.next
            curr.next = prev
            prev = curr
            curr = next_node
        # 比较前半部分和反转后的后半部分
        first_half = head
        second_half = prev
        while second_half:
            if first_half.val!= second_half.val:
                return False
            first_half = first_half.next
            second_half = second_half.next
        return True

当链表长度为奇数时,slow确实会停在中点位置,但是不会导致重复对比。

原因如下:

  1. 在找到中点并反转后半部分链表后,比较过程是从原链表的头部(代表前半部分)和反转后的后半部分链表的头部开始比较。
  2. 对于奇数长度的链表,假设链表长度为2n + 1,前半部分有n个节点,后半部分也有n个节点(不包括中间节点)。
  3. 在比较过程中,当后半部分链表遍历完时,前半部分链表也正好遍历到中间节点的前一个节点,不会重复比较中间节点。

例如,对于链表1 -> 2 -> 3 -> 2 -> 1,长度为 5(奇数)。

  • 找到中点后,slow指向值为3的节点。
  • 反转后半部分链表后,得到1 -> 2 -> 3 <- 2 <- 1
  • 比较过程中,前半部分从值为1的节点开始,后半部分从值为1的节点开始,当后半部分遍历完两个节点后,前半部分也正好遍历到值为3的节点的前一个节点,不会重复比较中间节点3

你可能感兴趣的:(链表,数据结构,python,算法)