删除从后往前第n个结点

我的思路:

递归至链表尾,然后向前增加至n,然后删除该结点。后面发现没那么简单。


我刚开始写的程序如下:
当我检测n在栈中的值我发现用print函数后n输出都是9,我想起函数每次调用都会将n值存入栈而这个值是不变的,例如n=10,出栈后n=9,每个出栈的n都等于9
然后我看leetcode上有个人思路跟我的一样,然而他用了一个helper函数

public ListNode deleteNth(ListNode head,int n){
		if(head==null||head.next==null) return head;
		deleteNth(head.next,n);
		n--;
		if(n==0)head = head.next;
		return head;
	}
<img src="https://img-blog.youkuaiyun.com/20151227152814401?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQv/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center" width="400" height="300" alt="" />
</pre><pre name="code" class="java"><pre name=<span style="font-family: Arial, Helvetica, sans-serif;">"code" class="java"> public class Solution{</span>
    public ListNode removeNthFromEnd(ListNode head, int n) {
        ListNode cur = head;
        int count = helper(cur, 0, n);
        if (count+1 == n) head = head.next;//删除头节点此时头节点的索引count=n-1
        return head; 
}

    public int helper(ListNode head, int count, int target) {
        if (head == null || head.next == null) {
            return 0;
        }
        count = helper(head.next, count, target);//这里count保存的值会递增,因为值是在函数递归完成后将值赋给count
        count++;
        if (count == target) {   //0<count<n
            head = head.next;
            head.next = head.next.next;//删除非头节点结点
        }
        return count;
    }
}



循环思路:

设计两个指针p1,p2然后使p1往前指向第n+1个结点(因为p2最后指向null而不是最后一个节点),如果p1这时为空,说明n大于等于链表长度,此时应当删除头节点!如果不是,就同时将两指针向后移动,直到p1==null时,此时p2指向离最后一个结点n+1处,删除指定节点只需要:p2.next = p2.next.next!!(因为p2 = p2.next无法删除结点)

代码如下:

/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode(int x) { val = x; }
 * }
 */
public class Solution {
    public ListNode reverseList(ListNode head) {
      /**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode(int x) { val = x; }
 * }
 */
 public class Solution{
    public ListNode removeNthFromEnd(ListNode head, int n) {
        ListNode p1 = head,p2 = head;
        while(n-->0){
            p1 = p1.next;
        }
        if(p1==null)return head.next;
        p1 = p1.next;
        while(p1!=null){
            p1 = p1.next;
            p2 = p2.next;
        }
        p2.next = p2.next.next;
        return head;
    }
}




<think>我们被要求删除链表中倒数第n个节点。根据引用资料,有两种常见方法:双指针法和计算链表长度法。 双指针法(快慢指针法): 1. 使用两个指针,让其中一个指针先移动n步(或n-1步,根据实现不同)。 2. 然后两个指针同时移动,直到快指针到达链表末尾。 3. 此时慢指针指向的位置就是倒数第n个节点的前一个节点(因为我们要删除节点,所以需要找到前一个节点)。 4. 注意处理特殊情况,比如删除头节点的情况(即当链表长度等于n时,需要删除头节点)。 计算链表长度法: 1. 遍历链表得到长度L。 2. 然后移动到第L-n-1个节点(因为要删除的是倒数第n个,即正数第L-n+1个,所以前一个节点是第L-n个,但索引从0开始,所以移动到第L-n-1个节点?注意:实际上我们遍历时从0开始计数,所以需要移动到第L-n-1个节点,然后删除下一个节点)。 3. 同样注意删除头节点的情况(即当L==n时,删除头节点)。 根据引用[4]的Java代码,它使用了双指针法,并且处理了两种情况: - 如果快指针在移动n步后变为null,说明要删除的是头节点。 - 否则,同时移动两个指针直到快指针到达最后一个节点(即快指针的next为null),此时慢指针指向要删除节点的前一个节点。 引用[2]的C++代码也是类似的双指针法,但它的处理略有不同:它使用了一个pre指针来记录慢指针的前一个节点(实际上在循环中,pre始终是b的前一个节点)。但是,当要删除的节点是头节点时,它直接调整头指针。 我们采用双指针法,并参考引用[4]的简洁实现: 步骤: 1. 初始化两个指针p和q,都指向头节点。 2. 将p向前移动n步(注意:这里移动n步,然后判断p是否为空,如果为空,说明要删除头节点)。 3. 如果p为空,说明要删除头节点,直接返回head.next。 4. 否则,让p和q同时移动,直到p的next为空(即p指向最后一个节点),此时q指向要删除节点的前一个节点。 5. 然后执行q.next = q.next.next。 但是注意,引用[4]的代码中,当p移动n步后,如果p不为空,则继续移动直到p.next为空。此时q指向要删除节点的前一个节点。因为: 初始:p和q都指向head p先移动n步,此时p指向第n个节点(从1开始计数,即正数第n个节点) 然后p和q同时移动,直到p.next为null,此时p指向最后一个节点(即第L个节点),而q移动了(L-n)步,指向第(L-n)个节点,即倒数第n+1个节点(因为倒数第n个节点前面有L-n-1个节点,所以第L-n个节点就是倒数第n个节点的前一个节点?实际上,倒数第n个节点是第L-n+1个节点,所以它的前一个节点是第L-n个节点,也就是q现在的位置)。因此,q.next就是倒数第n个节点,我们删除它。 但是,这里有一个边界:如果链表长度为n,那么p移动n步后,p会指向第n个节点(即最后一个节点)的下一个位置(null),所以我们在移动n步后判断p是否为null,如果是,则说明要删除头节点(因为此时倒数第n个节点就是头节点)。 另一种实现(使用虚拟头节点)可以避免处理头节点的特殊情况。引用[3]提到使用dummyNode虚节点,这样删除操作可以统一处理。 我们这里使用虚拟头节点的方法(推荐): 1. 创建一个虚拟头节点dummy,指向head。 2. 初始化两个指针slow和fast,都指向dummy。 3. 先将fast向前移动n+1步(这样slow和fast之间相隔n个节点)。 4. 然后同时移动slow和fast,直到fast为null(此时fast已经移出链表尾部)。 5. 此时slow指向要删除节点的前一个节点,执行slow.next = slow.next.next。 6. 返回dummy.next(因为头节点可能被删除,所以不能直接返回head,而应该返回dummy.next)。 为什么移动n+1步?因为我们要让slow指向被删除节点的前一个节点,所以当fast移动n+1步后,slow和fast之间正好间隔n个节点,这样当fast到达null时,slow指向的节点后面正好有n个节点(包括被删除节点),所以slow指向的是被删除节点的前一个节点。 步骤详解: dummy -> 1 -> 2 -> 3 -> 4 -> 5 -> null, n=2 初始化:slow=dummy, fast=dummy fast先移动n+1=3步:fast指向3(节点值为3) 然后同时移动slow和fast: slow移动到1,fast移动到4 slow移动到2,fast移动到5 slow移动到3,fast移动到null 此时slow指向3,而3的下一个节点4就是倒数第2个节点,所以删除4:slow.next = slow.next.next -> 3->5 这样,我们就不需要单独处理头节点的情况了(因为如果删除头节点,比如n=5,那么fast移动6步会指向null,此时slow指向dummy,删除dummy.next即头节点)。 因此,我们选择使用虚拟头节点的方法。 代码实现(使用JavaScript,因为引用[5]是TypeScript,但用户没有指定语言,我们选择一种通用语言,这里用JavaScript): 步骤: 1. 创建虚拟头节点dummy,并使其next指向head。 2. 初始化slow和fast指针,都指向dummy。 3. 将fast指针向前移动n+1步(注意:移动n+1步,需要确保不超出链表范围,但题目保证n有效,所以可以移动n+1步,如果链表长度小于n+1,则移动完n+1步后fast可能为null,但题目中n不大于链表长度,所以可以安全移动)。 4. 同时移动slow和fast,直到fast为null。 5. 此时slow指向要删除节点的前一个节点,修改slow.next指向。 6. 返回dummy.next。 注意:题目中n保证有效,且链表长度至少为1(如果n等于链表长度,则删除头节点)。 代码: ```javascript function ListNode(val, next) { this.val = (val===undefined ? 0 : val) this.next = (next===undefined ? null : next) } function removeNthFromEnd(head, n) { // 创建虚拟头节点 let dummy = new ListNode(0); dummy.next = head; let slow = dummy; let fast = dummy; // 将fast向前移动n+1步 for (let i = 0; i <= n; i++) { fast = fast.next; } // 同时移动slow和fast,直到fast为null while (fast !== null) { slow = slow.next; fast = fast.next; } // 删除节点 slow.next = slow.next.next; return dummy.next; } ``` 测试用例: 输入:head = [1,2,3,4,5], n=2 -> 输出:[1,2,3,5] 输入:head = [1], n=1 -> 输出:[] 注意:在删除头节点的情况下(如[1]删除倒数第1个节点),我们的dummy.next会指向null,所以返回[]。 但是,上述代码在移动n+1步时,如果链表长度小于n+1,则fast在移动过程中可能变为null(比如链表长度为n,那么移动n+1步时,在第n步后fast已经为null,再移动第n+1步就会出错)。因此,在移动n+1步的循环中,我们需要检查fast是否为null,如果为null就停止移动。 但是题目保证n是有效的(即1 ≤ n ≤ 链表长度),所以移动n+1步时,前n步是安全的,第n+1步可能会超出(因为链表长度可能为n,则移动n步后fast已经是null,再移动第n+1步就会报错)。因此,我们需要在循环中判断:如果fast为null,就跳出循环。 修改循环: ```javascript for (let i = 0; i <= n; i++) { if (fast === null) { // 如果已经到达链表末尾,跳出循环 break; } fast = fast.next; } ``` 然而,根据题目,n不会大于链表长度,所以移动n步后,fast可能指向null(当n等于链表长度时,移动n步后fast指向null,此时再移动一步就会出错)。因此,我们可以在移动n+1步时,先移动n步,然后判断是否已经为null,如果是,则不需要再移动。但为了逻辑清晰,我们可以在循环中判断。 但实际上,题目保证n有效(即n<=链表长度),所以移动n步后,fast指向第n个节点(从1开始)的下一个节点(即null),此时移动了n步后,fast已经为null,那么第n+1步就不需要移动了。所以我们在循环中判断,如果fast为null,就跳出循环。 但是,我们要求移动n+1步,如果移动过程中fast变为null,那么说明链表长度就是n,此时我们移动了n步(因为i从0到n-1,共n次移动)后fast为null,然后我们跳出循环,此时fast为null。那么接下来,我们执行同时移动slow和fast的循环(条件为fast不为null),这个循环不会执行。然后slow指向dummy,我们删除dummy.next(即头节点),这正是我们想要的。 所以修改后的代码: ```javascript function removeNthFromEnd(head, n) { let dummy = new ListNode(0); dummy.next = head; let slow = dummy; let fast = dummy; // 将fast向前移动n+1步 for (let i = 0; i <= n; i++) { if (fast === null) { break; // 链表长度不足n+1,但题目保证n有效,所以最多在移动n步后变为null,然后跳出 } fast = fast.next; } // 同时移动slow和fast,直到fast为null while (fast !== null) { slow = slow.next; fast = fast.next; } slow.next = slow.next.next; return dummy.next; } ``` 但是,我们也可以不检查,因为题目保证n有效,所以链表长度至少为n,所以移动n步后fast可能为null(当n等于链表长度时),但移动n+1步时,第n+1步会出错(因为第n步后fast已经是null)。因此,我们必须在循环中检查fast是否为null。 另一种写法是只移动n步,然后让fast和slow同时移动直到fast.next为null(这样slow指向被删除节点的前一个节点),但这样需要处理删除头节点的情况(当链表长度等于n时,移动n步后fast指向最后一个节点,此时fast.next为null,然后slow和fast同时移动0次,slow在dummy,然后删除头节点)。这样也可以,但使用虚拟头节点的方法中,我们通常移动n+1步是为了让两个指针间隔n个节点,这样当fast为null时,slow指向被删除节点的前一个节点。 我们也可以这样:先移动n步(而不是n+1步),然后同时移动slow和fast直到fast.next为null。这样slow指向被删除节点的前一个节点。代码如下: ```javascript function removeNthFromEnd(head, n) { let dummy = new ListNode(0); dummy.next = head; let slow = dummy; let fast = dummy; // 将fast向前移动n步 for (let i = 0; i < n; i++) { fast = fast.next; // 题目保证n有效,所以这里不会出现null } // 现在,同时移动slow和fast,直到fast.next为null while (fast.next !== null) { slow = slow.next; fast = fast.next; } slow.next = slow.next.next; return dummy.next; } ``` 这个版本更简洁,因为题目保证n有效,所以移动n步不会出现null(因为n<=链表长度,所以移动n步后,fast指向第n个节点,而不会是null)。然后我们移动slow和fast直到fast.next为null,此时slow指向被删除节点的前一个节点。 例如:链表[1,2,3,4,5],n=2 dummy->1->2->3->4->5 fast移动2步:fast指向2(节点值为2) 然后同时移动slow和fast直到fast.next为null: slow=dummy, fast=2 -> fast.next不为null,移动:slow=1, fast=3 slow=1, fast=3 -> fast.next不为null,移动:slow=2, fast=4 slow=2, fast=4 -> fast.next不为null,移动:slow=3, fast=5 fast.next为null,停止。 此时slow指向3,它的下一个节点是4(要删除的节点),删除4。 删除头节点的情况:链表[1],n=1 dummy->1 fast移动1步:fast指向1(节点值为1) 然后判断fast.next为null,所以不进入循环,此时slow在dummy,然后删除dummy.next(即1),返回dummy.next(即null)。 所以这个版本更简单,且不需要担心移动n+1步时出现null的问题。 因此,我们采用这种写法。 完整代码(JavaScript): 注意:题目给出的链表头节点是head,我们创建dummy节点指向head。 代码: ```javascript var removeNthFromEnd = function(head, n) { let dummy = new ListNode(0); dummy.next = head; let slow = dummy; let fast = dummy; // 先移动fast指针n步 for (let i = 0; i < n; i++) { fast = fast.next; } // 然后同时移动slow和fast,直到fast到达最后一个节点(即fast.next为null) while (fast.next !== null) { slow = slow.next; fast = fast.next; } // 此时slow指向要删除节点的前一个节点 slow.next = slow.next.next; return dummy.next; }; ``` 时间复杂度:O(L),L为链表长度,我们最多遍历链表两次(实际上一次循环移动n步,然后一次循环移动L-n步,总共L步)。 空间复杂度:O(1),只使用了常数空间。 我们也可以使用其他语言,但用户没有指定,这里用JavaScript作为示例。 现在,根据要求,我们需要生成相关问题。 相关问题: 1. 如何找到链表的中间节点? 2. 如何判断链表是否有环? 3. 如何反转链表? 4. 如何合并两个有序链表? 但是,根据当前问题,我们可以提出: 1. 如果链表长度未知,如何删除倒数第n个节点?(就是本题) 2. 如何找到链表的倒数第k个节点?(与删除类似,但不需要删除,只需返回节点) 3. 双指针法在链表中还有哪些应用? 4. 如何用递归方法删除倒数第n个节点? 因此,我们生成以下问题: §§相关问题§§ 1. 如何找到链表的倒数第k个节点? 2. 双指针法在链表中还有哪些应用场景? 3. 如何用递归方法删除链表的倒数第n个节点? 4. 如何判断链表是否有环?</think>要删除链表中倒数第 \(n\) 个节点,可以使用**双指针法**(快慢指针法),时间复杂度 \(O(L)\)(\(L\) 为链表长度),空间复杂度 \(O(1)\)。以下是详细步骤和代码实现: ### 方法步骤 1. **创建虚拟头节点** - 避免处理头节点删除的特殊情况。 - 例如:链表 `1->2->3->4->5`,删除倒数第 2 个节点时,虚拟节点指向头节点。 2. **初始化快慢指针** - `slow` 和 `fast` 均指向虚拟头节点。 3. **移动快指针 \(n\) 步** - 快指针先走 \(n\) 步,使快慢指针间距为 \(n\) 个节点。 - 例如:\(n=2\) 时,快指针指向节点 `2`。 4. **同时移动快慢指针** - 当快指针到达尾节点(`fast.next == null`)时,慢指针指向**待删除节点的前一个节点**。 - 例如:快指针到 `5` 时,慢指针指向 `3`(删除节点 `4` 的前驱)。 5. **删除目标节点** - 修改慢指针的 `next` 指向,跳过目标节点:`slow.next = slow.next.next`。 6. **返回虚拟头节点的下一个节点** - 返回 `dummy.next` 作为新链表的头节点。 ### 代码实现(JavaScript) ```javascript function ListNode(val, next) { this.val = (val === undefined ? 0 : val); this.next = (next === undefined ? null : next); } function removeNthFromEnd(head, n) { const dummy = new ListNode(0, head); // 创建虚拟头节点 let slow = dummy; let fast = dummy; // 快指针先走 n 步 for (let i = 0; i < n; i++) { fast = fast.next; } // 同时移动快慢指针,直到快指针到达尾节点 while (fast.next !== null) { slow = slow.next; fast = fast.next; } // 删除目标节点 slow.next = slow.next.next; return dummy.next; // 返回新头节点 } ``` ### 示例说明 - **输入**:`head = [1,2,3,4,5], n = 2` **输出**:`[1,2,3,5]` 过程: 1. 虚拟节点 `dummy(0)` → `1` → `2` → `3` → `4` → `5`。 2. 快指针先走 2 步:`fast` 指向 `2`。 3. 快慢指针同时移动:当 `fast` 到 `5` 时,`slow` 到 `3`。 4. 删除 `slow.next`(节点 `4`),链表变为 `1→2→3→5`。 - **输入**:`head = [1], n = 1` **输出**:`[]` 过程: 1. 虚拟节点 `dummy(0)` → `1`。 2. 快指针走 1 步:`fast` 指向 `1`。 3. `fast.next` 为 `null`,直接删除 `slow.next`(头节点),返回空链表。 ### 关键点 - **虚拟头节点**:简化头节点删除操作[^3]。 - **双指针间距**:快指针先走 \(n\) 步,保证慢指针定位到待删除节点的前驱[^4]。 - **边界处理**:当链表长度等于 \(n\) 时,删除头节点(由虚拟节点处理)[^5]。 ### 复杂度 - **时间**:\(O(L)\),只需遍历链表一次。 - **空间**:\(O(1)\),仅使用常数空间。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值