top19
给定一个链表,将倒数第 n 个结点删除。
解法一
删除一个结点,无非是遍历链表找到那个结点前边的结点,然后改变下指向就好了。但由于它是链表,它的长度我们并不知道,我们得先遍历一遍得到它的长度,之后用长度减去 n 就是要删除的结点的位置,然后遍历到结点的前一个位置就好了。
publicListNoderemoveNthFromEnd(ListNodehead, intn) { intlen=0; ListNodeh=head; while (h!=null) { h=h.next; len++; } //长度等于 1 ,再删除一个结点就为 null 了if (len==1) { returnnull; } intrm_node_index=len-n; //如果删除的是头结点if (rm_node_index==0) { returnhead.next; } //找到被删除结点的前一个结点h=head; for (inti=0; i<rm_node_index-1; i++) { h=h.next; } //改变指向h.next=h.next.next; returnhead; }
时间复杂度:假设链表长度是 L ,那么就第一个循环是 L 次,第二个循环是 L - n 次,总共 2L - n 次,所以时间复杂度就是 O(L)。
空间复杂度:O(1)。
我们看到如果长度等于 1 和删除头结点的时候需要单独判断,其实我们只需要在 head 前边加一个空节点,就可以避免单独判断。
publicListNoderemoveNthFromEnd(ListNodehead, intn) { ListNodedummy=newListNode(0); dummy.next=head; intlength=0; ListNodefirst=head; while (first!=null) { length++; first=first.next; } length-=n; first=dummy; while (length>0) { length--; first=first.next; } first.next=first.next.next; returndummy.next; }
解法二 遍历一次链表
上边我们遍历链表进行了两次,我们如何只遍历一次呢。
看了 leetcode 的讲解。
想象一下,两个人进行 100m 赛跑,假设他们的速度相同。开始的时候,第一个人就在第二个人前边 10m ,这样当第一个人跑到终点的时候,第二个人相距第一个人依旧是 10m ,也就是离终点 10m。
对比于链表,我们设定两个指针,先让第一个指针遍历 n 步,然后再让它俩同时开始遍历,这样的话,当第一个指针到头的时候,第二个指针就离第一个指针有 n 的距离,所以第二个指针的位置就刚好是倒数第 n 个结点。
publicListNoderemoveNthFromEnd(ListNodehead, intn) { ListNodedummy=newListNode(0); dummy.next=head; ListNodefirst=dummy; ListNodesecond=dummy; //第一个指针先移动 n 步for (inti=1; i<=n+1; i++) { first=first.next; } //第一个指针到达终点停止遍历while (first!=null) { first=first.next; second=second.next; } second.next=second.next.next; returndummy.next; }
时间复杂度:
第一个指针从 0 到 n ,然后「第一个指针再从 n 到结束」和「第二个指针从 0 到倒数第 n 个结点的位置」同时进行。
而解法一无非是先从 0 到 结束,然后从 0 到倒数第 n 个结点的位置。
所以其实它们语句执行的次数其实是一样的,从 0 到倒数第 n 个结点的位置都被遍历了 2 次,所以总共也是 2L - n 次。只不过这个解法把解法一的两次循环合并了一下,使得第二个指针看起来是顺便遍历,想法很 nice。
所以本质上,它们其实是一样的,时间复杂度依旧是 O(n)。
空间复杂度:O(1)。
总
利用两个指针先固定间隔,然后同时遍历,真的是很妙!另外室友的想法也很棒,哈哈哈哈哈