Leetcode 每日一题:Remove Nth Node From End of List

Leetcode 每日一题:Remove Nth Node From End of List_第1张图片

写在前面:

今天来看一道不怎么难的题,给大家放松一下。放松的同时也希望和大家一起回顾一下 “链表” 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)

题目问题:

  • 给定一个以数组表示的单链表:Leetcode 每日一题:Remove Nth Node From End of List_第2张图片
  • 找到从后往前数的第 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 长度非常大的时候。同时,优化后的方法也是面试中面试官更愿意看到的解答

你可能感兴趣的:(Leetcode,每日一题,leetcode,list,算法)