LeetCode 19. 删除链表的倒数第 N 个结点
LeetCode:https://leetcode-cn.com/problems/remove-nth-node-from-end-of-list/
题目描述
给你一个链表,删除链表的倒数第 n
个结点,并且返回链表的头结点。
进阶:你能尝试使用一趟扫描实现吗?
示例 1:
输入:head = [1,2,3,4,5], n = 2
输出:[1,2,3,5]
示例 2:
输入:head = [1], n = 1
输出:[]
示例 3:
输入:head = [1,2], n = 1
输出:[1]
提示:
链表中结点的数目为 sz
1 <= sz <= 30
0 <= Node.val <= 100
1 <= n <= sz
解题思路:
若链表为空,则无法删除
若删除链表头结点,则需要改动头结点起始位置
因为要删除倒数第n个节点,因此需要知道链表总长度(实际不需要,优化方案有说明)
解法1、两次遍历法,正序删除
/** * 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 first = head; int size = 0; if(head == null) return head; // 第一次遍历, 求出链表长度 while(first != null) { size++; first = first.next; } int index = size - n; // 顺序要删除的下标, 起始下标为0 int count = 0; // 对链表头开始遍历的元素个数 first = head; // 重置链表头(求链表长度已遍历过一遍) // 删除第一个元素 if(index == 0) { first = first.next; head = first; // 更改链表头 return head; } // 第二次遍历, 顺序删除链表第index个元素 // 1->2 3 4->5 删除倒数第3个元素 while(first != null) { count++; if(count == index) { first.next = first.next.next; break; } else { first = first.next; } } return head; } }
说明:
此算法遍历了两次链表,第一次求链表长度,第二次删除第index个节点
时间复杂度:O(n) ,空间复杂度:O(1) ,只用了常量级的额外空间。
解法2、两次遍历,引入了最顶临时节点,简化首节点判断
注意到这个问题可以容易地简化成另一个问题:删除从列表开头数起的第 (size - n + 1)个结点,其中 size 是列表的长度。只要我们找到列表的长度 size,这个问题就很容易解决。
1)我们将添加一个最顶临时结点作为辅助,该结点位于列表头部。最顶临时结点用来简化了某些极端情况,例如列表中只含有一个结点,或需要删除列表的头部。
2)在第一次遍历中,我们找出列表的长度 size。然后设置一个指向最顶临时结点的指针,并移动它遍历列表,直至它到达第 (size - n)个结点那里。我们把第 (size - n)个结点的 next
指针重新链接至第 (size - n + 2)个结点,完成这个算法。
/** * 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 first = head; int size = 0; ListNode top = new ListNode(0); // 增加了一个临时最顶节点 top.next = head; // 临时最顶节点指向head头, 解决删除头结点的问题 while(first != null) { size++; first = first.next; } int index = size - n; first = top; // 指向最顶临时节点 while(index-- > 0) { first = first.next; } first.next = first.next.next; // 最顶临时节点的next指向删除第index节点后的头结点 return top.next; // 返回删除后的头结点 } }
复杂度分析
时间复杂度:O(n) ,该算法对列表进行了两次遍历,首先计算了列表的长度 其次找到第 (size - n)个结点。 操作执行了(2*size - n)步,时间复杂度为 O(n)
空间复杂度:O(1) ,只用了常量级的额外空间。
解法3、一次遍历,快慢两个指针相差步长为n,精妙
上述算法可以优化为只使用一次遍历。我们可以使用两个指针而不是一个指针。
第一个指针从列表的开头向前移动 (n + 1) 步,而第二个指针将从列表的开头出发。
现在,这两个指针被 n 个结点分开。我们通过同时移动两个指针向前来保持这个恒定的间隔(解决了倒数第n个节点的问题,从链表尾部向前计算,相差为n,精妙吧),直到第一个指针到达最后一个结点。此时第二个指针将指向从最后一个结点数起的第 n 个结点(非常关键,解题的核心,结束条件)。
我们重新链接第二个指针所引用的结点的 next
指针指向该结点的下下个结点。
/** * 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 fast = head; // 快指针, 先遍历n个元素 ListNode slow = head; // 慢指针, 后开始遍历, 与fast同步 // fast 与 slow 距离为 n + 1 while(fast != null && n > 0) { fast = fast.next; n--; } // 1->2->3->4->5 n=2 1->2->3->5 while(fast != null && fast.next != null) { // fast不等于null, slow肯定也不等于null fast = fast.next; slow = slow.next; } if(fast == null) { // fast == null 是结束条件 head = head.next; } else { slow.next = slow.next.next; } return head; } }
复杂度分析
时间复杂度:O(n) ,该算法对含有 size 个结点的列表进行了一次遍历。因此时间复杂度为 O(n)
空间复杂度:O(1) ,我们只用了常量级的额外空间
解法4、一次遍历,删除倒数第n个元素,链表转数组存储,获取删除元素的前后元素,直观处理
核心思路:删除链表倒数第n个元素,即倒数第n个元素的前一个元素的next指向n元素的next
/** * 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 first = head; Map<Integer, ListNode> map = new HashMap<Integer, ListNode>(); int size = 0; int index = 0; while(first != null) { map.put(index++, first); first = first.next; } size = map.size(); if(size == 0) return null; // 1->2->3->4->5 n=2 1->2->3->5 if(n == size) { head = head.next; // 删除链表第一个元素, 需修改链表头 } else { map.get(size-n-1).next = map.get(size-n).next; // 倒数第n个元素的前一个元素next, 指向n的next } return head; }
解法5、一次遍历,删除倒数第n个元素,运用栈的特性:后进先出顺序
因为是删除链表的倒数第n个元素,逆向思维:即删除压栈后,先弹出的第n个元素
/** * 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 first = head; // 删除倒数第n个元素, 运用栈的后进先出特性, 来直观求解 Stack<ListNode> stack = new Stack<ListNode>(); while(first != null) { stack.push(first); first = first.next; } // 栈顶后入先出特性, 获取倒数的第n个元素 ListNode delNode = null; // 记录删除的元素 while(n-- > 0) { delNode = stack.pop(); } if(stack.size() > 0) { stack.pop().next = delNode.next; } else { // 栈为空, 即全部删除完了 head = head.next; } return head; }
解法6、一次遍历,删除倒数第n个元素,优化解法5:凡事用栈解决的,用递归也可解决
核心思想:跟算法5类似,采用递归压栈,再出栈,代码非常简洁、统一、完美
注意:递归函数的变量,要在函数外赋值全局变量,例如 int count = 0; 在递归函数外
/** * 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 { int count = 0; public ListNode removeNthFromEnd(ListNode head, int n) { // 2) 遇到最后一个元素为null, 则开始出栈, 返回 if(head == null) return null; // 1) 先把全部元素压栈(类似于算法5的Stack) // 1->2->3->4->5 n=2 1->2->3->5 head.next = removeNthFromEnd(head.next, n); // 3) 递归一次加1, 类似于解法5的压栈后就开始出栈, 出栈的第n个元素即是删除倒数第n个元素 if(n == ++count) return head.next; // 返回删除元素n的next return head; } }
晒一下成就:Java 执行用时 0ms,击败了 100%用户
总结
本文对一道算法题,用了六种解法,分别是:
1)两次遍历链表,把倒数第n个元素,转成了顺数第 size - n 个元素
2)优化算法1,新增了一个链表最顶部节点(假节点),解决删除头结点的判定问题
3)一次遍历,精妙设计快慢指针,二者相差为n个元素,fast快指针遍历到尾即结束
4)一次遍历,链表转数组,通过数组的下标直观进行链表的next链接
5)一次遍历,增加了Stack栈数据结构,把倒数第n个元素转成栈顶弹出第n个元素,更容易理解
6)一次遍历,凡是用栈能够实现的,大多也可以用递归函数实现(栈实现一般用深度递归队列也可实现)
刷算法题,注重数量的同时,更要注重质量,深入思考,不断优化递进,培养思维的深度
对一道题深度挖掘,举一反三,例如倒数第n个元素,用栈来实现,逆向思维,太美好啦
参考推荐:
Python 常用加密算法 base64, md5, sha1
版权所有: 本文系米扑博客原创、转载、摘录,或修订后发表,最后更新于 2021-02-05 18:21:56
侵权处理: 本个人博客,不盈利,若侵犯了您的作品权,请联系博主删除,莫恶意,索钱财,感谢!