链表part02:24. 两两交换链表中的节点 、19.删除链表的倒数第N个节点、 链表相交 、142.环形链表II

之前写的现在看起来很啰嗦,更新为python的 

甚至都不需要用临时nxt记录断开前的next指向,我们顺序写的好,一次就能把pre first 和second的指向搞定

pre指向 second ,first 指向second的下一个 (这一步最关键,second.next现在已经被重新指向了,第三步我们改second.next的方向就没有任何问题了) ,secode 指向first节点。 然后更新pre和cur节点。 

# Definition for singly-linked list.
# class ListNode:
#     def __init__(self, val=0, next=None):
#         self.val = val
#         self.next = next
class Solution:
    def swapPairs(self, head: Optional[ListNode]) -> Optional[ListNode]:
        dummy = ListNode(-1,head)
        pre_node = dummy
        cur_node =head
        
        while cur_node  and cur_node.next :

             # 获取需要交换的两个节点
            first_node=cur_node
            second_node =cur_node.next

            # --- 开始交换 ---
            # 1. 将前一个节点的 next 指向第二个节点 (修正)
            pre_node.next =first_node.next  #pre_node.next =second_node
            # 2. 将第一个节点的 next 指向第二个节点的下一个节点
            first_node.next=second_node.next 
            # 3. 将第二个节点的 next 指向第一个节点,完成交换
            second_node.next=first_node

            # --- 更新指针,为下一次循环做准备 ---
            # 1. 更新 pre_node 为交换后的第一个节点 (现在是 pair 的末尾)
            pre_node =first_node
            # 2. 更新 cur_node 为下一对需要交换的第一个节点
            cur_node=first_node.next
        return dummy.next 
    
    
    
    def swapPairs(self, head: Optional[ListNode]) -> Optional[ListNode]:
        # 创建哑节点,简化边界处理
        # Create a dummy node to simplify edge cases.
        dummy = ListNode(0, head)
        # pre_node 指向要交换的节点对之前的那个节点
        # pre_node points to the node before the pair to be swapped.
        pre_node = dummy
        # first_node 指向当前要处理的节点对的第一个节点,作为循环的主要控制变量
        # first_node points to the first node of the current pair to process, acting as the main loop control variable.
        first_node = head

        # 循环条件:只要 first_node 和它后面的节点 (构成一对) 都存在
        # Loop condition: As long as first_node and the node after it (forming a pair) both exist.
        while first_node and first_node.next:
            # second_node 是当前要处理的节点对的第二个节点
            # second_node is the second node of the current pair to process.
            second_node = first_node.next

            # --- 开始交换 ---
            # 1. pre_node 指向 second_node
            #    Link pre_node to second_node.
            pre_node.next = second_node
            # 2. first_node 指向 second_node 原来的下一个节点
            #    Link first_node to the original node after second_node.
            first_node.next = second_node.next
            # 3. second_node 指向 first_node
            #    Link second_node back to first_node.
            second_node.next = first_node
            # --- 交换结束 ---

            # --- 更新指针,为下一次循环做准备 ---
            # 1. 更新 pre_node 为交换后的第一个节点 (即原来的 first_node)
            #    Update pre_node to the node that was originally first_node (now the end of the swapped pair).
            pre_node = first_node
            # 2. 更新 first_node 为下一对需要交换的第一个节点
            #    Update first_node to the start of the next pair.
            first_node = first_node.next # 注意:此时 first_node.next 已经是下一对的起点了

        # 返回哑节点的下一个节点,即新链表的头
        # Return the node after the dummy node, which is the new head of the list.
        return dummy.next
        
           

            


  24. 两两交换链表中的节点    

草图:

这道题目需要注意的是2个节点进行交换,分为奇偶数情况。 

  • 要操作2个节点的互换,得找到第一个节点的前一个节点作为cur节点。 这样指针才能成功指向第一个节点的后面2个节点。 目的是更换之前,能成功用临时变量把相关节点都保存下来,这样在互换后还能找到节点。 
  • 如上图所示, 节点1和节点2要更换,就得在节点1之前设置一个cur节点,此时是一个虚拟节点dummy。 dummy节点指向2节点,2节点指向1节点,1节点指向3节点。即:cur->2->1->3->4->null ;进行3、4的互换时候, 需要把cur节点移动到节点3 前面,也就是cur=节点1。
  • 我常犯的错误是:直接让 cur.next =cur.next.next; 这样就把原来dummy-->节点1的指引链断了,节点1无法找到。 所以一定要在重新指引前,做好节点1的储存:temp=cur.next;  同理,节点2指向节点1后,节点2->3的链条断裂。 我们需要做好节点3的储存temp1=cur.next.next;
  • 遍历循环时候,这2个顺序不能写反,否则会出现空指针异常。

实际代码如下:

class Solution {
    public ListNode swapPairs(ListNode head) {
        ListNode dummyHead = new ListNode(0);
        dummyHead.next = head;
        ListNode cur = dummyHead;

        while (cur.next != null && cur.next.next != null) {
            ListNode temp = cur.next; // 保存第一个节点
            ListNode temp1 = cur.next.next.next; // 保存第二个节点后面的节点
            
            cur.next = cur.next.next; // 将当前指针的下一个指向第二个节点,dummy -> 节点2

            cur.next.next = temp; // 将第二个节点的下一个指向第一个节点,节点2 -> 节点1

            temp.next = temp1; // 将第一个节点的下一个指向原本第三个节点(即交换后的下一个部分),节点1 -> 节点3

            // 更新 cur 指针,移动到下一个需要处理的节点
            cur = cur.next.next;
        }

        return dummyHead.next;
    }
}

或者用更加见名知义的方式来写代码:

public ListNode swapPairs(ListNode head) {
        ListNode dummyHead= new ListNode(-1);
        dummyHead.next =head;
        ListNode cur=dummyHead;

        while(cur.next !=null && cur.next.next !=null ){
            ListNode firstNode =cur.next;
            ListNode secondeNode =cur.next.next;
            ListNode temp=cur.next.next.next;

            cur.next= secondeNode;//dummy-->节点2

            secondeNode.next=firstNode;//节点2-->节点1

            firstNode.next=temp;//节点1--->节点3

            //更新cur
            cur=cur.next.next;
        }

        return dummyHead.next;
    }

或者使用这样:

 public ListNode swapPairs(ListNode head) {
        ListNode dummyHead= new ListNode(0,head);
        ListNode cur=dummyHead;
        while(cur.next !=null && cur.next.next !=null ){
            ListNode firstNode =cur.next;
            ListNode secondeNode =cur.next.next;
            ListNode temp =secondeNode.next;
            secondeNode.next=firstNode;
            firstNode.next=temp;
            cur.next=secondeNode;
            cur =firstNode;
        }
        return dummyHead.next;
    }

或者使用这种: 我们选择从头到尾,一步步断开之前的链接关系,每断开一个就重新给予新的链接关系 ,这样的逻辑上会比较清晰好梳理。 

public ListNode swapPairs(ListNode head) {
        ListNode dummyHead= new ListNode(0,head);
        ListNode cur=dummyHead;
        while(cur.next !=null && cur.next.next !=null ){
            ListNode firstNode =cur.next;
            ListNode secondeNode =cur.next.next;
            //调整顺序之后,当前节点指向第二个节点
            cur.next= secondeNode;//dummy-->节点2
            //第一个节点指向第三个节点
            firstNode.next=secondeNode.next;
            //第二个节点指向第一个节点
            secondeNode.next=firstNode;//节点2-->节点1
            //更新cur
            cur=firstNode;
        }
        return dummyHead.next;
    }

或者我们使用递归的思路: 

 public ListNode swapPairs(ListNode head){
        if(head ==null || head.next ==null){
            return head;
        }
        //find next node 
        ListNode next =head.next;
        //use functinon swapPairs 
        ListNode newNode  =swapPairs(next.next );
        //change each other 
        next.next =head;
        head.next =newNode ;


        return next;
    }
public ListNode swapPairs(ListNode head) {
    // base case 退出条件
    if (head == null || head.next == null) {
        return head;
    }

    // 获取当前节点的下一个节点
    ListNode next = head.next;

    // 进行递归,处理 next 后面的链表
    ListNode newNode = swapPairs(next.next);

    // 交换当前两个节点
    next.next = head;
    head.next = newNode;

    // 返回新的头节点
    return next;
}

递归的压栈和弹栈过程

递归函数的执行过程可以想象为栈的操作,每次递归调用都将一个新函数调用压入栈中,而在递归完成后,函数会从栈中弹出并返回相应的值。

对于你的问题,我们可以一步一步来看递归过程中发生了什么,假设我们有一个链表 1 -> 2 -> 3 -> 4,递归调用过程如下:

第一次调用 swapPairs(1)
  1. 参数 head = 1,并且 head.next = 2 存在。
  2. 进入函数内部,next = head.next,即 next = 2
  3. 准备进行递归调用 swapPairs(next.next),即 swapPairs(3)
  4. 在此时,当前函数调用被压入栈中,并且控制权转移到递归调用 swapPairs(3)
第二次调用 swapPairs(3)
  1. 参数 head = 3,并且 head.next = 4 存在。
  2. 进入函数内部,next = head.next,即 next = 4
  3. 准备进行递归调用 swapPairs(next.next),即 swapPairs(null)
  4. 在此时,当前函数调用被压入栈中,并且控制权转移到递归调用 swapPairs(null)
第三次调用 swapPairs(null)
  1. 参数 head = null,由于 head == null,达到了递归的退出条件。
  2. 函数直接返回 null
  3. 这一返回操作相当于将栈顶的调用(第三次调用)从栈中弹出,并将返回值传递给它的上一层调用(也就是第二次调用)。

弹栈过程:回到第二次调用 swapPairs(3)

  1. 现在递归结束,控制权从第三次调用返回到了第二次调用。
  2. 在第二次调用中,原来保存的参数 head = 3 和 next = 4
  3. 第三次调用返回的值为 null,并赋值给 newNode,也就是说 在递归结束后,弹栈到第二次调用时,才知道 newNode = null

此时第二次调用的代码继续执行:

next.next = head; // 即 4 -> 3
head.next = newNode; // 即 3 -> null
  • next.next = head:将节点 4 的下一个节点设为 3
  • head.next = newNode:将节点 3 的下一个节点设为 null(因为 newNode 是 null)。
  • 最终返回 next,也就是节点 4,作为当前这次调用的返回值。

弹栈过程:回到第一次调用 swapPairs(1)

  1. 在第二次调用返回后,控制权回到了第一次调用。
  2. 在第一次调用中,原来的参数 head = 1 和 next = 2
  3. 第二次调用返回的值为 4,并赋值给 newNode,也就是说 在递归结束后,弹栈到第一次调用时,才知道 newNode = 4

然后继续执行第一次调用中的剩余代码:

next.next = head; // 即 2 -> 1
head.next = newNode; // 即 1 -> 4
  • next.next = head:将节点 2 的下一个节点设为 1,交换完成。
  • head.next = newNode:将节点 1 的下一个节点设为 4,连接后面的部分。
  • 最终返回 next,即节点 2,作为整个链表的新头节点。

总结

  • 压栈:每次递归调用都会将当前的执行状态(函数的局部变量、指针等)压入栈中,并将控制权交给下一次递归调用。
  • 递归返回值的确定:递归的返回值只有在递归调用结束并弹栈时才能被确定。在这个例子中,newNode = null 是在递归的最深层结束时确定的,然后在逐层弹栈的过程中逐步返回给上一层调用。
  • 你怎么知道递归返回 newNode = null
    • 只有在递归调用结束并开始返回时,函数才会从栈中弹出,递归的返回值才会被赋值给上一层调用的 newNode 变量。
    • 因此,newNode = null 是在第三次调用返回到第二次调用(即弹栈)时,第二次调用的上下文才获得了这个返回值。

所以,递归的返回值只有在递归结束后,在弹栈的过程中逐层传递给上一个递归调用。你在第二次调用中得到 newNode = null 是在第三次递归结束并返回的时候才知道的,而不是在一开始递归压栈的时候就能知道。

19.删除链表的倒数第N个节点

这道题目是需要用到双指针的逻辑去处理问题 。 我们为了让指针能正确定位到倒数n这位置,

需要设置一个n的距离,当fast 和slow距离为n,并且fast在链表的末尾,那么slow就自然在倒数第n个位置了。

基于此,我们需要调整一下,因为slow要定位在删除节点前一个位置,才能进行正确的删除处理。

所以,fast和slow距离为n+1后,同时移动2个指针,直到fast到null位置。 

/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode() {}
 *     ListNode(int val) { this.val = val; }
 *     ListNode(int val, ListNode next) { this.val = val; this.next = next; }
 * }
 */
class Solution {
    public ListNode removeNthFromEnd(ListNode head, int n) {
        ListNode dummyNode =new ListNode(0,head);
        ListNode  fast =dummyNode;
        ListNode  slow =dummyNode;
        for(int i=0;i<=n;i++){
            if(fast ==null){
                return null;
            }
            fast =fast.next;
        }
        while(fast !=null){
            fast=fast.next;
            slow =slow.next;
        }

        slow.next=slow.next.next;
        return dummyNode.next;
    }
}

 以下的代码逻辑上有点问题,这个处理fast为的null思路不恰当。

/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode() {}
 *     ListNode(int val) { this.val = val; }
 *     ListNode(int val, ListNode next) { this.val = val; this.next = next; }
 * }
 */
class Solution {
    public ListNode removeNthFromEnd(ListNode head, int n) {
        ListNode dummyNode =new ListNode(0,head);
        ListNode  fast =dummyNode;
        ListNode  slow =dummyNode;
        n++; //先给n增加1,这样2个指针距离是n+1的距离了
        while(n-- && fast !=null){
            fast=fast.next;
        }
        while(fast !=null){
            fast=fast.next;
            slow =slow.next;
        }
        return dummyNode.next;
    }
}

02.07. 链表相交

这道题目是要找到交点,对于链表来讲,不是数值相等的点,而是看指针引用的内存地址是否是同一处来断定为交点。意味着交点之后,2个链表会自然共享一段链表逻辑。  所以我们一定是要对长度不一的2个链表进行对齐,并且是让他们尾部对齐。 长链表前面多出来的距离就是长链表自己独立的节点。  我们通过计算 gap = 长链表长度-短链表长度,获取这个差距。 然后对长链表进行遍历gap的次数,这样就能获取长链表舍去独立节点后的起点。  

step2: 现在2个链表都是对齐的状态,我们对2者进行遍历,就会逐一去进行对比。如果找到相等的就返回对应的节点,如果找不到,知道最后一个为null,停止循环,最后返出去的是null。 

对于2个链表的长度计算,我们可以通过设置头节点为cur,对cur遍历++,直到cur 为null,停止while循环。 

注意:我们循环结束后要重置curA和curB的节点,回复他们为头节点。 

具体的代码如下:

/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode(int x) {
 *         val = x;
 *         next = null;
 *     }
 * }
 */
public class Solution {
    public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
        int lenA =0;
        int lenB =0;
        ListNode curA= headA;
        ListNode curB =headB;
        while(curA !=null){
            lenA++;
            curA=curA.next;
        }
        while (curB !=null){
            lenB++;
            curB =curB.next;
        }
        // 计算完长度后,重置 curA 和 curB
        curA =headA;
        curB =headB;
        if(lenB>lenA){
            int temp =lenA;
            lenA=lenB;
            lenB = temp;
            ListNode tempNode = curA;
            curA =curB;
            curB = tempNode;
        }
        int gap =lenA-lenB;
        while(gap-->0){
            if(curA ==null){
                return null;
            }
            curA=curA.next;
        }
        while(curA !=null && curB !=null){
            if(curA==curB){
                return curA;
            }
            curA =curA.next;
            curB =curB.next;
        }

        return null;

    }
}

计算链表的长度我们可以写成for循环

for (curA = headA; curA != null; curA = curA.next) {
    lenA++;
}

还有一个快捷的方式:2个指针同时游走,第一个链表结束后继续走第二个链表。  2个指针走的路径长度是一致的。 

代码如下:

//方法二 复杂度O(m+n) 双指针 
    public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
        
        if(headA ==null || headB == null){
            return null;
        }

        ListNode pA =headA;
        ListNode pB =headB;
        while(pA !=pB){
            pA =(pA ==null)? headB:pA.next;
            pB =(pB ==null)? headA:pB.next;

        }
        return pA;
    }

142.环形链表II


代码如下:

/**
 * Definition for singly-linked list.
 * class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode(int x) {
 *         val = x;
 *         next = null;
 *     }
 * }
 */
public class Solution {
    public ListNode detectCycle(ListNode head) {
        //定义2个指针, 然后让他们走,一个跳2节点,一个跳1节点。 然后再定义他们相遇到某个节点 
        //然后再定义2个index ,index1 是快指针, index2是头节点。如果index1和index2 又一次相遇的话,中止
        //那么说明找到了头节点index2 ,
    ListNode fast =head;
    ListNode slow =head;
    while(fast !=null && fast.next !=null){
        fast=fast.next.next;
        slow =slow.next;
        
        if (fast ==slow){
            ListNode index1 =fast;
            ListNode index2 =head ;
            // x=z ,一个从链表的头出发,一个从相遇的位置出发,再次相遇的点就是入口位置。 
            while (index1 != index2){
                index1 =index1.next ;
                index2 =index2.next;
            }
            return index1;
        }
        
    } 
    return null;
    }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值