【链表】(三) 经典问题

目录

一、反转链表 - 引入

二、反转链表

2.1 题目要求

2.2 解决过程

三、移除链表元素

3.1 题目要求

3.2 解决过程

四、奇偶链表

4.1 题目要求

4.2 解决过程

五、回文链表

5.1 题目要求

5.2 解决过程

六、小结 - 链表经典问题


一、反转链表 - 引入

参考文献:https://leetcode-cn.com/explore/learn/card/linked-list/195/classic-problems/749/


二、反转链表

2.1 题目要求

2.2 解决过程

注意事项

本题应返回经修改的单链表的头节点 head,而不是什么都不返回或返回别的 (尽管题目并未明说...)。

个人实现

法一:双指针如第一节所述,按原始顺序迭代节点,并将它们逐个移动到链表头部。空间复杂度 O(1),时间复杂度 O(n)。

2020/07/11 - 76.37%

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

class Solution:
    def reverseList(self, head: ListNode) -> ListNode:
        if not head:  # 空链表
            return
        
        front = head       # 靠前/左节点 - 总为原链表的头节点
        rear = front.next  # 靠后/右节点 - 作为 front 的后驱节点用于移动
        while rear:
            # 移动 - rear 移动到 head 前成为新 head (注意移动次序正确)
            front.next = rear.next  # 第一个指向第三个
            rear.next = head        # 第二个指向第一个
            head = rear             # 头节点重指定为当前首个节点
            # 更新
            rear = front.next       # rear 重指定为 front 的后驱节点
        return head

相关解说 (思想一致)

其他实现

法一:双指针迭代。此方法比个人实现还要简洁,原理如下所示:

class Solution(object):
    def reverseList(self, head):
        pre = None  # 靠前/左节点
        cur = head  # 靠后/右节点
        while cur:
            tmp = cur.next  # 记录当前节点的下一个节点
            cur.next = pre  # 然后将当前节点指向pre
            pre = cur  # pre 和 cur节点各后移一位
            cur = tmp
        return pre	

2020/07/11 - 99.95% - 最优之一 

法二:递归。递归方法通常 写法简洁,但难免相应地更加 抽象费解,需要多画图和理解。原理如下所示:

class Solution(object):
    def reverseList(self, head):
        # 递归终止条件
        if not (head and head.next):  # if (not head) or (not head.next):
            return head
        # 此 cur 为最后一个节点
        cur = self.reverseList(head.next)  
        # 假设链表 1->2->3->4->5, 那么此时 cur 就是 5
        # 而 head 是 4, head 的下一个是 5,下下一个为空
        # 所以 head.next.next 就是 5->4
        head.next.next = head  # 反向引用
        # 防止链表循环,需将 head.next 设为空
        head.next = None  # 被引用节点指向 None
        # 每层递归函数都返回 cur, 即最后一个节点
        return cur

2020/07/11 - 99.90% - 最优之一

参考文献

https://leetcode-cn.com/explore/learn/card/linked-list/195/classic-problems/750/

https://leetcode-cn.com/problems/fan-zhuan-lian-biao-lcof/solution/fan-zhuan-lian-biao-yi-dong-de-shuang-zhi-zhen-jia/

https://leetcode-cn.com/problems/fan-zhuan-lian-biao-lcof/solution/dong-hua-yan-shi-duo-chong-jie-fa-206-fan-zhuan-li/


三、移除链表元素

3.1 题目要求

3.2 解决过程

注意事项

老规矩,结果返回经处理的新链表头节点即可。

个人实现

法一:哑节点/哨兵节点 + 双指针迭代。思想很直白,遍历链表找到目标节点即通过跳跃引用间接删除。同时为避免对头部节点等特殊情况特殊处理,使用哑节点/哨兵节 dummy 点作为伪头,使各节点操作一致。此外,注意需返回新链表的头节点,结果 应返回 dummy.next 而非 head。因为当头节点也是待删除节点时,即便“删去”了头节点,变量 head 仍保存了其引用,此时结果返回 head 只会得到最初的一个头节点,而非新链表。空间复杂度 O(1),时间复杂度 O(n)。

2020/07/11 - 99.95% - 最优

class Solution:
    def removeElements(self, head: ListNode, val: int) -> ListNode:
        dummy = ListNode("h")   # 哑节点/哨兵节点, 使各节点能够得到一致处理
        dummy.next = head       # 作为伪头
        
        slow = dummy            # 慢指针
        fast = head             # 快指针 (不用 dummy.next 是因为可能链表为空)
        while fast:
            if fast.val == val:
                slow.next = fast.next  # 跳跃引用实现删除
            else:
                slow = slow.next       # 否则慢指针移动一步
            fast = fast.next    # 不论如何快指针要移动一步
        return dummy.next       # 不能 return head, 例如特殊情况 [1], 1

法二:单指针迭代 + 特殊情况特殊处理,稍麻烦些。

2020/07/11 - 99.80% - 次优

class Solution:
    def removeElements(self, head: ListNode, val: int) -> ListNode:
        if not head:  # 特殊情况 - 空链表
            return head

        while head and head.val == val:  # 特殊情况 - 要删除头节点
            head = head.next

        ptr = head  #单指针
        while ptr:
            if ptr.next and ptr.next.val == val:
                ptr.next = ptr.next.next
            else:
                ptr = ptr.next
        return head

官方实现与说明

# 官方实现 - 和个人实现基本一模一样
class Solution:
    def removeElements(self, head: ListNode, val: int) -> ListNode:
        sentinel = ListNode(0)
        sentinel.next = head
        
        prev, curr = sentinel, head
        while curr:
            if curr.val == val:
                prev.next = curr.next
            else:
                prev = curr
            curr = curr.next
        
        return sentinel.next

其他实现

法一:递归。这就好理解了,元素不是 val 的节点就保留,否则跳跃引用间接删除。如下所示:

class Solution:
    def removeElements(self, head: ListNode, val: int) -> ListNode:
        if not head:
            return None
        head.next = self.removeElements(head.next, val)
        if head.val == val:
            return head.next
        else:
            return head

参考文献

https://leetcode-cn.com/explore/learn/card/linked-list/195/classic-problems/752/

https://leetcode-cn.com/problems/remove-linked-list-elements/solution/yi-chu-lian-biao-yuan-su-by-leetcode/


四、奇偶链表

4.1 题目要求

4.2 解决过程

个人实现

法一双指针。如果不要求空间复杂度 O(1) (in-place),可以使用哈希表等额外空间轻易解决。而本方法则通过迭代链表,分别由 快指针 odd 慢指针 even 组成 奇数位链表偶数位链表,并在遍历至尾结点时,令 偶数位链表尾结点 指向 奇数位链表头节点 half 实现奇偶链表。空间复杂度 O(1),时间复杂度 O(n)。

2020/07/11 - 99.95% - 最优    

2020/11/13 - 100% (36ms) - 最佳

class Solution:
    def oddEvenList(self, head: ListNode) -> ListNode:
        if not head:  # null linked list
            return head
        
        odd = head  # odd pointer
        even = half = head.next  # even pointer and half pointer
        while even and even.next:
            # skip connection
            odd.next = odd.next.next 
            even.next = even.next.next
            # point to next node
            odd = odd.next
            even = even.next
            
        odd.next = half  # half connection
        return head

参考文献

https://leetcode-cn.com/explore/learn/card/linked-list/195/classic-problems/753/

https://leetcode-cn.com/problems/odd-even-linked-list/submissions/


五、回文链表

5.1 题目要求

5.2 解决过程

失败测试

最先试了一个计数器方法,然而这只能判断中点左右元素之和是否相等,而不能判断是否回文,因为无法比较顺序 (如 1->2->3->1->2 或 1->2->1->2 等)。虽然不能解题 (多种步长/间隔组合另当别论),但思想不错,不妨记录一下。空间复杂度 O(1),时间复杂度 O(n)。

class Solution:
    def isPalindrome(self, head: ListNode) -> bool:
        if not head:
            return True
        # 计算链表长度
        length = 0
        curr = head             # 第一次遍历指针
        while curr:
            length += 1
            curr = curr.next
        # 累加-累减判断回文与否
        counter = 0             # 计数器
        times = length // 2     # 半段长度
        ptr = head              # 第二次遍历指针
        # 前半段节点元素累加
        for _ in range(times):
            counter += ptr.val
            ptr = ptr.next
        # 中间节点元素跳过
        if length % 2 != 0:
            ptr = ptr.next
        # 后半段节点元素累减
        for _ in range(times):
            counter -= ptr.val
            ptr = ptr.next

        return counter == 0

个人实现

法一:遍历链表依次保存各节点元素,然后按通常方式判断回文字符串 (如逆序重排、双指针、递归等)。空间复杂度 O(n),时间复杂度 O(n)。

2020/10/23 - 98.65% (68ms)

class Solution:
    def isPalindrome(self, head: ListNode) -> bool:
        index = []  
        curr = head
        while curr:
            index.append(curr.val)  # 依次保存各节点元素
            curr = curr.next
        return True if index == index[::-1] else False  # 判断正序逆序是否一致

官方实现与说明

// C++ implementation
class Solution {
public:
    bool isPalindrome(ListNode* head) {
        vector<int> vals;
        while (head != nullptr) {
            vals.emplace_back(head->val);
            head = head->next;
        }
        for (int i = 0, j = (int)vals.size() - 1; i < j; ++i, --j) {
            if (vals[i] != vals[j]) {
                return false;
            }
        }
        return true;
    }
};
# Python implementation
class Solution:
    def isPalindrome(self, head: ListNode) -> bool:
        vals = []
        current_node = head
        while current_node is not None:
            vals.append(current_node.val)
            current_node = current_node.next
        return vals == vals[::-1]


# 伪代码
function print_values_in_reverse(ListNode head)
    if head is NOT null
        print_values_in_reverse(head.next)
        print head.val

# Python implementation
class Solution:
    def isPalindrome(self, head: ListNode) -> bool:
        self.front_pointer = head  # self 是必要的

        def recursively_check(current_node=head):
            if current_node is not None:
                if not recursively_check(current_node.next):
                    return False
                if self.front_pointer.val != current_node.val:  # 前后对应元素对比
                    return False
                self.front_pointer = self.front_pointer.next  # 移向下一个对比元素
            return True
        
        return recursively_check()
# 简化表示, 加强理解!!!
class Solution:
    def isPalindrome(self, head: ListNode) -> bool:
        self.left = head
        
        def check(right=head):
            if right:
                if not check(right.next):  # 右指针先递归到最后一个, 再逐渐回溯
                    return False
                if right.val != self.left.val:  # 比较对应位置节点值
                    return False
                self.left = self.left.next  # 左指针后移一位
            return True  # 本次比较相等. 将结果反馈回上一层
        
        return check()

2020/10/23 - 52.17% (88ms) 

递归实现虽然简洁高效,但也相应地十分抽象费解,需要努力理解才行。 

// C++ implementation
class Solution {
public:
    bool isPalindrome(ListNode* head) {
        if (head == nullptr) {
            return true;
        }

        // 找到前半部分链表的尾节点并反转后半部分链表
        ListNode* firstHalfEnd = endOfFirstHalf(head);
        ListNode* secondHalfStart = reverseList(firstHalfEnd->next);

        // 判断是否回文
        ListNode* p1 = head;
        ListNode* p2 = secondHalfStart;
        bool result = true;
        while (result && p2 != nullptr) {
            if (p1->val != p2->val) {
                result = false;
            }
            p1 = p1->next;
            p2 = p2->next;
        }        

        // 还原链表并返回结果
        firstHalfEnd->next = reverseList(secondHalfStart);
        return result;
    }

    ListNode* reverseList(ListNode* head) {
        ListNode* prev = nullptr;
        ListNode* curr = head;
        while (curr != nullptr) {
            ListNode* nextTemp = curr->next;
            curr->next = prev;
            prev = curr;
            curr = nextTemp;
        }
        return prev;
    }

    ListNode* endOfFirstHalf(ListNode* head) {
        ListNode* fast = head;
        ListNode* slow = head;
        while (fast->next != nullptr && fast->next->next != nullptr) {
            fast = fast->next->next;
            slow = slow->next;
        }
        return slow;
    }
};
# 通过双指针定位链表中间节点, 不错的想法 (快指针走到末尾, 慢指针刚好到中间)
class Solution:
    def isPalindrome(self, head: ListNode) -> bool:
        if head is None:
            return True

        # Find the end of first half and reverse second half.
        first_half_end = self.end_of_first_half(head)  # 链表中间节点
        second_half_start = self.reverse_list(first_half_end.next)  # 链表后半部分反转

        # Check whether or not there's a palindrome.
        result = True
        first_position = head
        second_position = second_half_start
        while result and second_position is not None:
            if first_position.val != second_position.val:  # 从链表开头和中间处一一对比
                result = False
            first_position = first_position.next  # 指向下一个元素
            second_position = second_position.next  # 指向下一个元素

        # Restore the list and return the result.
        first_half_end.next = self.reverse_list(second_half_start)  # 恢复链表后半部分
        return result    

    def end_of_first_half(self, head):
        """ 通过快慢指针, 使慢指针指向链表中间节点引用 """
        fast = head
        slow = head
        while fast.next is not None and fast.next.next is not None:
            fast = fast.next.next
            slow = slow.next
        return slow

    def reverse_list(self, head):
        """ 反转链表 """
        previous = None
        current = head
        while current is not None:
            next_node = current.next
            current.next = previous
            previous = current
            current = next_node
        return previous

 当然,只反转前半个链表而不反转回去也可以,例如:

class Solution:
    def isPalindrome(self, head: ListNode) -> bool:
        slow = head
        fast = head
        pre = head
        prepre = None
        while fast and fast.next:
            #pre记录反转的前半个列表,slow一直是原表一步步走
            pre = slow
            slow = slow.next
            fast = fast.next.next

            pre.next = prepre
            prepre = pre

        if fast:#长度是奇数还是偶数对应不同情况
            slow = slow.next  
        
        while slow and pre:
            if slow.val != pre.val:
                return False
            slow = slow.next
            pre = pre.next
        return True 

参考文献

https://leetcode-cn.com/explore/learn/card/linked-list/195/classic-problems/754/

https://leetcode-cn.com/problems/palindrome-linked-list/solution/hui-wen-lian-biao-by-leetcode/

https://leetcode-cn.com/problems/palindrome-linked-list/solution/wo-de-kuai-man-zhi-zhen-du-cong-tou-kai-shi-gan-ju/247986


六、小结 - 链表经典问题

参考文献:https://leetcode-cn.com/explore/learn/card/linked-list/195/classic-problems/755/

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值