【刷题笔记day4】链表进阶题目练习

LC24 两两一组反转链表

给定一个链表,两两交换其中相邻的节点,并返回交换后的链表。
你不能只是单纯的改变节点内部的值,而是需要实际的进行节点交换。
例如:Input: head = [1,2,3,4] → Output: [2,1,4,3]

本题有两种解法:迭代法和递归法。

迭代法

这个方法涉及比较多的指针操作,很容易绕晕,但在开始绕之前,需要明白以下知识点:

  • 要想对相邻节点A和B进行反转,我们需要一个指针指向A的上一个节点。这是开始反转操作的大前提。
  • 链表节点数可能是奇数个或偶数个,这个条件直接影响循环终止条件(详见代码)

现在来看代码(借用代码随想录的图以方便理解)

class Solution:
     def swapPairs(self, head: Optional[ListNode]) -> Optional[ListNode]:
         """
         迭代法
         """
         if not head:
             return
         dummy = ListNode(next = head)
         cur = dummy
        
         while cur.next and cur.next.next:
             # 首先用指针分别指向 target pair 的前一个节点和后一个节点
             temp1 = cur.next
             temp2 = cur.next.next.next
             # 以下三步反转 target pair
             # 与上图的顺序有差别
             cur.next = cur.next.next  # “步骤一”
             temp1.next = temp2        # “步骤三”
             cur.next.next = temp1     # “步骤二”
             # 移动cur,为下一次循环做好准备
             cur = cur.next.next
            
         return dummy.next

递归法

掌握本题的递归解法更有助于解答下一题: LC25 K个一组反转链表

class Solution:
    def swapPairs(self, head: Optional[ListNode]) -> Optional[ListNode]:
        """
        递归法
        """
        if not head or not head.next:
            # 如果奇数个节点,传入的head是None
            # 如果是偶数个,传入的head.next是None
            return head
        
        first = head
        second = head.next
        others = head.next.next
        
        # 先反转前两个元素
        second.next = first
        # 利用递归定义,将剩下链表节点两两反转,接到后面
        first.next = self.swapPairs(others)
        # 现在整个链表已经成功反转,返回新的头节点
        return second

通过对比两种解法可以发现,迭代法是“自底向上”地找出答案;而递归法是“自顶向下”地反推,相信我们都能想到数学归纳法:

转自 从“数学归纳法”到理解“递归算法”!

  1. 证明基本情况(通常是N = 1 的时候)是否成立。 证明对于N=1成立。我们只需要先从最小的自然数开始证明。这一步通常非常简单。关键是证明第二步。
  2. 证明N > 1 时,假设 N - 1 成立,那么对于N成立(N为任意大于1的自然数)。
    这一步并不是直接证明的,而是假设N-1成立,利用这个结论推出N是成立的。如果能够推出的话,就可以说:对于所有的自然数都成立。因为证明了对1成立,那么对2成立,对3也成立。那么就证明了对所有自然数都成立。

所以要对原链表(假设有6个节点)两两一组反转,在反转1号和2号之前,要先确保3号到6号已经反转好了,在反转3号4号前,要确保5号6号已经反转好了。上面的递归函数中的first.next需要接住的就是当前问题的子问题的答案,也就是我上一句话描述的过程。

拓展题: LC25 K个一组反转链表

给你链表的头节点 head ,每 k 个节点一组进行翻转,请你返回修改后的链表。
k 是一个正整数,它的值小于或等于链表的长度。如果节点总数不是 k 的整数倍,那么请将最后剩余的节点保持原有顺序。
你不能只是单纯的改变节点内部的值,而是需要实际进行节点交换。

# Definition for singly-linked list.
# class ListNode:
#     def __init__(self, val=0, next=None):
#         self.val = val
#         self.next = next
class Solution:
    def reverseKGroup(self, head: Optional[ListNode], k: int) -> Optional[ListNode]:
        # base case
        if head == None: 
            return None
            
        # 确定反转区间,a, b是反转区间的头尾节点
        a, b = head, head
        for i in range(k):
            # 如果区间不足k个,则保持原有顺序返回
            if (b == None): return head
            b = b.next
            
        # 反转区间
        newHead = self.reverse(a, b)
        # 递归到下一层
        a.next = self.reverseKGroup(b, k)
        return newHead
    
    # 迭代法反转子链表
    def reverse(self, a: Optional[ListNode], b: Optional[ListNode]) -> Optional[ListNode]:
        pre = None
        cur = a
        nxt = a
        
        while cur != b:
            nxt = cur.next
            cur.next = pre
            pre = cur
            cur = nxt
        return pre

LC19 删除链表倒数第N个节点

给你一个链表,删除链表的倒数第 n 个结点,并且返回链表的头结点。
进阶:你能尝试使用一趟扫描实现吗?

对于单链表,要一趟扫描找到倒数第n个节点,需要借助快慢双指针技巧,来实现两个循环才能达到的效果。

简而言之就是快指针先前进n步,然后慢指针和快指针保持相对静止,二者都做匀速运动直到快指针运动到最后一个节点时,慢指针就指向了倒数第n个节点。

那么要删除这个节点,我们需要一个虚拟头节点以便返回最终结果,还需要让慢指针最终运行到倒数第n+1个节点,也就是倒数第n个节点的前一个节点,这样方便进行删除操作。

# Definition for singly-linked list.
# class ListNode:
#     def __init__(self, val=0, next=None):
#         self.val = val
#         self.next = next
class Solution:
    def removeNthFromEnd(self, head: Optional[ListNode], n: int) -> Optional[ListNode]:
        dummy = ListNode(next=head)
        p1, p2 = dummy, dummy
        # p1 前进n + 1步
        for i in range(n + 1):
            p1 = p1.next
            
        # 结束循环时,控制p2指向要删除节点的上一个节点
        while p1 != None:
            p1 = p1.next
            p2 = p2.next
        p2.next = p2.next.next
        return dummy.next

LC160 相交链表

给你两个单链表的头节点 headA 和 headB ,请你找出并返回两个单链表相交的起始节点。如果两个链表没有交点,返回 null

两个链表长度不一怎么办?分别把各自接到对方的后面去,就形成了逻辑上两条长度一样的链表。这样子当从头开始遍历的时候,两指针相等之时就可以证明两条链表相交。

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

class Solution:
    def getIntersectionNode(self, headA: ListNode, headB: ListNode) -> Optional[ListNode]:
        p1, p2 = headA, headB
        while p1 != p2:
            if p1 != None:
                p1 = p1.next
            else:
            	# 遍历到尾部了,从另一条链表的头部继续
                p1 = headB
            if p2 != None:
                p2 = p2.next
            else:
            	# 遍历到尾部了,从另一条链表的头部继续
                p2 = headA
                
        # 假如没有交点,p1, p2 最后都会为None,循环退出
        return p1

LC142 环形链表II

给定一个链表,返回链表开始入环的第一个节点。 如果链表无环,则返回 null。

本题是LC141 环形链表的进阶版,后者只需要判断链表中是否有环,属于本题解答的一部分,所以合并过来。

本题的思路是快慢双指针,二者同时出发,但快指针一次运动两步,慢指针一次运动一步,假如存在环,那么快指针一定会追上慢指针(想起来中学的1000米考试😅)这就是LC141的解法。

但本题还需要确定环的起点,这个就需要一点数学功底了。先放结论:

当快慢指针第一次相遇时,让快指针重新从起点出发,但此时二者速度相同,那么二者再次相遇地点就是环的起点

证明过程(来自代码随想录

那么相遇时: slow指针走过的节点数为: x + y, fast指针走过的节点数:x + y + n (y + z),n为fast指针在环内走了n圈才遇到slow指针, (y+z)为 一圈内节点的个数A。
因为fast指针是一步走两个节点,slow指针一步走一个节点, 所以 fast指针走过的节点数 = slow指针走过的节点数 * 2:
(x + y) * 2 = x + y + n (y + z)
两边消掉一个(x+y): x + y = n (y + z)
因为要找环形的入口,那么要求的是x,因为x表示 头结点到 环形入口节点的的距离。
所以要求x ,将x单独放在左面:x = n (y + z) - y ,
再从n(y+z)中提出一个 (y+z)来,整理公式之后为如下公式:x = (n - 1) (y + z) + z 注意这里n一定是大于等于1的,因为 fast指针至少要多走一圈才能相遇slow指针。
这个公式说明什么呢?
先拿n为1的情况来举例,意味着fast指针在环形里转了一圈之后,就遇到了 slow指针了。
当 n为1的时候,公式就化解为 x = z,
这就意味着,从头结点出发一个指针,从相遇节点 也出发一个指针,这两个指针每次只走一个节点, 那么当这两个指针相遇的时候就是 环形入口的节点。
也就是在相遇节点处,定义一个指针index1,在头结点处定一个指针index2。
让index1和index2同时移动,每次移动一个节点, 那么他们相遇的地方就是 环形入口的节点。

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

class Solution:
    def detectCycle(self, head: Optional[ListNode]) -> Optional[ListNode]:
        if head == None:
            return None
        slow, fast = head, head
        
        # 判断是否存在环
        while fast != None and fast.next != None:
            fast = fast.next.next
            slow = slow.next
            if slow == fast:
                break
        # 这里要明确是什么原因导致循环退出
        # 如果是fast遇到空指针,那就说明没有环
        if fast == None or fast.next == None:
            return None
            
        # 找环的起点
        fast = head
        while fast != slow:
            fast = fast.next
            slow = slow.next
        return slow

参考

如何K个一组反转链表

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
A:好的,下面是K - 双向链表练习题的解答。 题目描述: 设计一个数据结构,要求实现双向链表的基本操作:插入、删除、反转等,并实现一个方法向链表中添加一个元素。同时,该数据结构需要支持以下操作: 1. get(k):返回链表中第 k 个节点的值。如果 k 小于 1 或大于链表的长度,则返回 -1。 2. addAtHead(val):在链表的头部插入一个值为 val 的节点。 3. addAtTail(val):在链表的尾部插入一个值为 val 的节点。 4. addAtIndex(index,val):在链表中的第 index 个节点前插入一个值为 val 的节点。如果 index 等于链表的长度,则在链表的末尾添加该节点。如果 index 大于链表的长度,则不会插入。 5. deleteAtIndex(index):如果索引 index 有效,则删除链表中的第 index 个节点。 解题思路: 题目要求我们设计一个双向链表数据结构,并提供选定的一些操作。这是一个经典的链表问题,我们需要先设计链表节点的结构体,并在节点中保留前驱和后继指针,便于双向操作。然后,我们可以定义一个链表类,在该类中定义一些方法来实现链表的基本操作。 本题需要实现的方法包括: 1. get(k):遍历链表,找到第 k 个节点并返回该节点的值。 2. addAtHead(val):创建一个新节点,将该节点作为头节点,并更新头节点的前驱指针。 3. addAtTail(val):创建一个新节点,将该节点作为尾节点,并更新尾节点的后继指针。 4. addAtIndex(index,val):遍历链表,找到第 index - 1 个节点,创建一个新节点,并将其插入到该节点的后面。如果 index 为零,则将新节点插入到头部。如果 index 等于链表的长度,则将新节点插入到末尾。 5. deleteAtIndex(index):遍历链表,找到第 index - 1 个节点,并将其后继指针指向第 index + 1 个节点。如果 index 为零,则更新头节点。如果 index 等于链表的长度 - 1,则更新尾节点。 代码实现: 下面是基于C++的实现代码,其中Node是一个链表节点的结构体,List是链表类的定义: ```cpp #include<iostream> using namespace std; // 链表节点结构体 struct Node { int val; // 节点的值 Node* pre; // 前驱指针 Node* nxt; // 后继指针 Node(int _val):val(_val),pre(nullptr),nxt(nullptr){} // 构造函数 }; // 链表类 class List{ private: Node* head; // 头节点 Node* tail; // 尾节点 int size; // 链表长度 public: List():head(nullptr),tail(nullptr),size(0){} // 构造函数 int get(int k){ if(k < 1 || k > size) // 判断k是否合法 return -1; Node* p = head; for(int i=1; i<k; i++) // 遍历链表,找到第k个节点 p = p->nxt; return p->val; // 返回节点的值 } void addAtHead(int val){ Node* p = new Node(val); // 创建新节点 if(size == 0){ // 链表为空的情况 head = p; tail = p; }else{ // 链表非空的情况 p->nxt = head; // 插入节点 head->pre = p; head = p; } size++; // 更新链表长度 } void addAtTail(int val){ Node* p = new Node(val); // 创建新节点 if(size == 0){ // 链表为空的情况 head = p; tail = p; }else{ // 链表非空的情况 tail->nxt = p; // 插入节点 p->pre = tail; tail = p; } size++; // 更新链表长度 } void addAtIndex(int index, int val){ if(index > size) // index不合法,不插入 return; if(index <= 0) // 如果index小于等于0,插入到头部 addAtHead(val); else if(index == size) // 如果index等于size,插入到尾部 addAtTail(val); else{ // 如果index在链表中 Node* p = head; for(int i=1; i<index; i++) // 找到第index-1个节点 p = p->nxt; Node* q = new Node(val); // 创建新节点 q->nxt = p->nxt; // 插入节点 p->nxt->pre = q; p->nxt = q; q->pre = p; size++; // 更新链表长度 } } void deleteAtIndex(int index){ if(index < 0 || index >= size) // index不合法,不删除 return; if(index == 0){ // 如果要删除的是头节点 head = head->nxt; // 更新头节点 if(head == nullptr) // 如果链表为空,尾节点也需要更新 tail = nullptr; else head->pre = nullptr; }else if(index == size-1){ // 如果要删除的是尾节点 tail = tail->pre; // 更新尾节点 tail->nxt = nullptr; }else{ // 如果要删除的是中间节点 Node* p = head; for(int i=1; i<index; i++) // 找到第index-1个节点 p = p->nxt; p->nxt = p->nxt->nxt; // 删除节点 p->nxt->pre = p; } size--; // 更新链表长度 } }; int main(){ List l; l.addAtHead(1); l.addAtTail(3); l.addAtIndex(1,2); // 链表变为[1,2,3] cout<<l.get(1)<<" "; // 返回2 l.deleteAtIndex(1); // 现在链表是[1,3] cout<<l.get(1)<<" "; // 返回3 return 0; } ``` 总结: 双向链表实现相对较多的语言,相对单向链表更适合一些场景;比如说LUR Cache。对于双向链表的实现,我们需要注意节点间指针的指向关系,以及头节点和尾节点的处理。感兴趣的读者可以继续尝试其他链表问题,如链表的分割、链表的反转等。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值