-
今天在LeetCode碰到一道链表的算法题,Remove Nth Node From End of List。题目难度不大。首先还是看一下题目:
-
题目描述:对于一个给定的链表,请删除从链表尾部倒数第n个节点,并返回链表头指针。
-
题目分析:假设链表长度为k,删除从链表尾部倒数第n个节点,也就是顺序的第k-n+1个节点,将第k-n个节点的next指向第k-n+1个节点指向的下一个节点。那么剩下需要做的就只有找到第k-n个节点。由此就产生了两种不同的思路。
-
解法一:首先遍历链表获得长度k,然后在从头遍历查找到第k-n个节点,修改k-n的next指向,然后返回链表有指针。
链表遍历获取长度,代码如下:
const getListLength = (head) => { let count = 0; while (head) { ++count; head = head.next; } return count }然后查找整个节点,当删除的节点为链表的头部和尾部时需要特殊处理,代码实现如下:
const removeNthFromEnd = function(head, n) { let headp = head; let len = getListLength(head); //当n等于链表长度,也就是删除链表头部 if (n === len) { headp = head.next; return headp; } //当n为1时,删除链表尾部 if (n === 1 && len === 1) { return null; } len -= n; while (len > 1) { --len; head = head.next; } //修改节点next指向 const temp = head.next.next; head.next = temp; return headp; };上面的代码虽然能够解决问题,但是看上去总是显得拙劣,解题思路不够整齐和巧妙。那么是否可以找到一个解法将所有情况统一处理呢,不再去处理上述两种页数情况,答案是yes。通过给链表添加一个头指针,是特殊情况也纳入统一解法中。代码实现如下:
const removeNthFromEnd = function(head,n){ const dummy = ListNode(0); dummy.next = head; let first = dummy; let len = getListLength(head); len -= n; while(len > 0){ first = first.next; --len; } first.next = first.next.next; return dummy.next; }通过给链表添加一个头指针,避免掉单独处理边界情况,这种方法值得学习!
-
解法二: 采用双指针,第两个指针相差n个节点,这样在一次遍历后后面的指针就指向了第k-n个节点。
const removeNthFromEnd = function(head, n) { const dummy = ListNode(0, null); dummy.next = head; let first = dummy; let second = dummy; for (let i = 0; i <= n; i++) { first = first.next; } while (first !== null) { first = first.next; second = second.next; } second.next = second.next.next; return dummy.next; } -
总结:第一种解法对链表遍历了两次,遍历k+k-n次节点,时间复杂度o(k),第二种解法采用双指针,需要遍历k次。第一次确定first指向的节点,进行了n+1次遍历,然后在后续再次进行了k-n-1次遍历,因此总的遍历次数只有k次,相对于第一种减少了k-n次,如果k是一个大数,那么在指向效率上则会出现明显差距。另外通过给链表添加头指针将问题简化,特殊情况一般化,使得问题的解决思路更统一。