写在前面:
今天来看一道不怎么难的题,给大家放松一下。放松的同时也希望和大家一起回顾一下 “链表” Linked List 的一些基本知识和使用方法。
链表是面试里常考察的题型之一,他和 array 最大的不同之处在于他 更好的延展性,比数组,甚至是动态数组对于不元素大小的头尾增删效率更加,因为不需要 对内存空间长度的重新分配。与之带来的缺点就是从全局角度来说的 “长度不可见”,“位置不可见”,所有的长度和位置都依赖于我们按照链表的方向进行遍历,使得一般 Array 的解题思路用在链表上一般都会 TLE
今天这道题目虽然用遍历方法很好解决,也不会 TLE,但是我们可以以这道简单题目为跳板,更好的学习和了解一些在链表中可以使用的 shortcut,那就让我们开始吧!
题目介绍:
题目信息:
- 题目链接:https://leetcode.com/problems/remove-nth-node-from-end-of-list/description/
- 题目类型:Linked List, Two Pointer
- 题目来源:Google 高频面试题
- 题目难度:Medium (Actually,这道题放现在应该是道 Easy)
题目问题:
- 给定一个以数组表示的单链表:
- 找到从后往前数的第 n 个数,并将他删除
- 返回删除后链表
题目想法:
In place 改动:
虽然说创造一个新的 List head,并且不断 copy 原有的元素并删除指定元素可以更加轻松的完成本道题目的书写(不用 handle 因为删除 linked list 所导致的 segmentation fault 问题),但是额外的空间是非常不 efficient的,各位码友在面试的时候能 inplace 修改做出来就不要用 额外空间,会很减分的
Brute Force:
因为是 singly linked list,所以我们不知道他有多长,也不知道他的末尾在哪里。大家应该很容易想到一种暴力解法:
- 即我先遍历一遍 linked list,同时记录下长度 L 和最后一点
- 然后再次遍历,数着 L - n 的地方停下,并在这个地方做修改
- Runtime:O(L + L - n) = O(2L - n) = O(L) --> 取决于长度
- Space: O(1)
这种方法是可以通过测试的,但是显然是一种很笨的方法。如果是面试大厂的同学们需要做的更好,才能在面试中脱颖而出~~
因为 single linked list 决定了我们只能从这一个方向遍历,并且也只能遍历全部才能知道具体的长度和位置,那我们可以怎么样在刚刚的基础上进行优化呢?
计算偏移量:
聪明的小伙伴可能已经发现:无论是什么情况,我们想要的那个点和 list 末尾节点中间都差 n
因为我们的题目是要删除 从后往前数的 第 n 个点,那换个思路,他和末尾点的偏移量就是 n。再平移回起点,只需要我们在起始点地方就已经做好起始点的偏移量,等到起始点的偏移量先到达末尾之后,我们当时的起始点不就是我们想要的那个元素吗?
如果使用双指针,一个点指向起始点后 n+1 个,另一个指向原点,那当其中一个点到达终点以后,另一个点 K 就会在 需要被删除的目标点前一个,K->next = K->next->next 就可以删除掉目标点了。这种方法巧妙的利用了我们所需要的距离差恒定的思想,将一个绝对位置问题改变成了相对位置,这样我们就不再需要第一遍的遍历了。
题目解法:
- 定义 Dummy 起点
- Dummy -> next 为 head
- 定义 fast, slow pointer 为 dummy 位置
- 遍历 n+1 次:
- 将 fast 向前推 1
- 遍历直到 fast 变成 nullptr:
- fast 向前推 1
- slow 向前推 1
- slow->next 进行删除
- 返回 dummy->next 也就是 head
题目代码:
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode() : val(0), next(nullptr) {}
* ListNode(int x) : val(x), next(nullptr) {}
* ListNode(int x, ListNode *next) : val(x), next(next) {}
* };
*/
class Solution {
public:
ListNode* removeNthFromEnd(ListNode* head, int n) {
ListNode* dummy = new ListNode();
dummy->next = head;
ListNode* fast = dummy;
ListNode* slow = dummy;
// we want to iterate to make n gap, so that when we reach, we delete the next one
for(int i = 0; i <= n; i++){
fast = fast -> next;
}
//iterate through until fast reach the end, then we find the target at the slow
while(fast != nullptr){
fast = fast->next;
slow = slow->next;
}
ListNode* toFree = slow->next;
slow->next = toFree->next;
delete toFree;
return dummy->next;
}
};
- Runtime:O(L-n) = O(L)
- Space: O(1)
- Remark: 虽然在 Big O 角度,两个方法 Runtime 在相同量级,但实际执行的时候我们优化的方法能讲效率提升很多,尤其是是当linked list 长度非常大的时候。同时,优化后的方法也是面试中面试官更愿意看到的解答