从单链表末端移除第N个节点的两种方法

269 阅读6分钟

在这篇文章中,我们解释了从单链表末端移除第N个节点的两种方法。我们可以在O(N)时间内通过单次/一次遍历完成这个任务。

目录:

  1. 链接列表的介绍
  2. 方法1:对关联列表进行两次遍历
  3. 方法2:一次遍历列表,一次通过

先决条件:单链表慢速快速指针技术

你应该了解的类似问题:

这类似于Leetcode问题19。从列表的末端删除第N个节点。

链接列表简介

链接列表是一种动态的线性数据结构,其中的元素形成了一个节点序列,每个节点都使用指针链接到另一个节点上。
这种数据结构被认为可以从任何位置进行有效的插入和删除,但是访问时间很昂贵。

有4种类型的链表,每种都有自己的用途和应用,它们是:单链表、双链表、循环链表和双循环链表。
你可以在本帖末尾提供的链接中阅读更多关于这些的内容。

在这里,我们将讨论从单链表的末端删除第n个节点,但首先我们需要讨论单链表的结构,以获得一些关于如何执行该操作的洞察力。

单链表的结构。

linkedlist.drawio-1-

头部节点表示列表的开始。
一个节点包含数据字段和指向下一个节点的指针,称为next。
单链列表的最后一个节点next的指针指向null,表示它是标志着列表结束的节点。

在我们开始解决这个问题之前,我们需要了解删除链表中的一个节点的实际操作。

下面是这些程序的图示。

删除第一个节点

  1. 使头部指针指向头部之后的下一个节点
  2. 释放头部节点的内存

deleteFromFront.drawio

删除中间节点

  1. 遍历到待删除节点之前的前一个节点
  2. 使前一个节点的下一个指针指向要删除的节点之后的节点,该节点不包括被删除的节点
  3. 释放被删除节点的内存

deletingMiddle.drawio

删除最后一个节点

  1. 遍历到链表的倒数第二个节点
  2. 使其下一个指针指向空,表示列表的结束
  3. 释放被删除节点的内存

deletend.drawio

考虑到这一点,我们可以从列表的末端删除第n个节点:

用程序解决这个问题:
输入:指针指向链接列表的头部,n表示从末尾开始的第n个元素。
输出:
指针指向链接列表的头部,没有从末尾开始的第n个元素。

我们可以用几种方法来解决这个问题,我将讨论两种方法。

方法1:对关联列表进行两次传递

在第一遍中,我们得到列表的长度。一旦我们得到了它,我们就可以将问题重组为试图从列表的开始删除(长度-n+1)个节点。
在第二遍中,我们从末端删除第n个节点。我们通过将(length - n)第n个节点的下一个指针链接到(length - n + 2)节点来实现。

例子:
List = [1, 4, 7, 9, 11]
n = 3

1 -> 4 -> 7 -> 9 -> 11

第一遍返回5。列表的长度。

我们想从开始就删除(5-3+1)=第3个节点,也就是7。

第二遍,让(5-3)第二节点的下一个节点指向(5-3+2)第四节点,我们将得到。

1 -> 4 -> 9 -> 11

算法:

  1. 创建一个临时节点,指向列表的头部。
  2. 在第一遍中获得列表的长度。
  3. 设置指针到临时节点,并在列表中移动,直到我们到达(长度-n)节点,即我们需要删除的节点之前的节点。
  4. 使(length - n)th 节点的下一个指针指向(length - n+2)节点。

代码:

// Part of iq.opengenus.org
ListNode *removeNthNodeFromEnd(ListNode *head, int n){
//Check wheather the list is empty
    if(!head)
        return head;
        
//First pass to get length of the list
    ListNode *temp = head;
    int len = 0;
    while(temp){
        len += 1;
        temp = temp->next;
    }
    
//Index of node to be removed
    int x = len - n;
    
// Initialize previous and current nodes
    ListNode *currNode = head;
    ListNode *prevNode = nullptr;

//Second pass to remove the node by shifting pointers.
    while(x > 0){
        prevNode = currNode;
        currNode = currNode->next;
        x -= 1;
    }

//Condition if the head node is to be deleted.
    if(!prevNode){
        temp = currNode;
        currNode = currNode->next;
        delete temp;
        head = currNode;
    }else{
        prevNode->next = currNode->next;
        delete currNode;
    }
    return head;
}

分析

这个过程需要线性时间,O(n) + O(n-x) = O(n)。分别获得列表的长度和转移指针。
O(1)空间,没有使用额外的空间。

方法2:一次通过列表,一次通过

我们首先创建两个指针,一个是快指针,另一个是慢指针,这两个指针相距n个节点。
也就是说,如果慢指针在列表的开头,那么快指针就在列表的n+1位置。
在这种情况下,当快指针到达列表的结尾时,慢指针就在列表结尾的第n个节点。
这时我们通过慢指针使下一个节点的指针指向next.next节点,也就是下一个节点后的节点。

例子:
列表 = [1, 4, 7, 9, 11]
n = 3

1 -> 4 -> 7 -> 9 -> 11

我们想删除7

1->4->7->9->11,慢速在4,快速在11。

在fast和slow之间有n个节点,
当fast到达终点时,slow指向从终点开始的第n个节点。
我们让slow(4)的next指向next.next(9),从而排除7。

结果列表将是:
1->4->9->11

算法:

  1. 创建两个指针,快指针指向第(n-1)个节点,慢指针指向第0个节点
  2. 通过链接列表更新这两个指针
  3. 当循环结束时,fast.next == null,fast将指向最后一个节点,slow将在最后一个节点的(n-1)个节点
  4. 将slow的指针设置为next的next
  5. 返回没有删除元素的列表头部

代码:

// Part of iq.opengenus.org
ListNode *removeNthNodeFromEnd(ListNode *head, int n){

//Initialize fast and slow pointers
    ListNode *fast = head, *slow = head;
    
//Give a headstart to fast pointer
    for(int i = 0; i < n; i++)
        fast = fast->next;
        
// If element to deleted is the head node
    if(!fast)
        return head->next;
        
//Move the pointers up to their respective positions
    while(fast->next){
        fast = fast->next;
        slow = slow->next;
    }
    
//Make next of slow to point to element after deleted element
    slow->next = slow->next->next;
    return head;
}

分析:

时间是线性的O(n) n是链表的长度。
空间是O(1),因为没有使用额外的空间。

问题:

  1. 为什么我们在双通道方法中使用快指针和慢指针?
  2. 你如何比较链表和数组,每种数据结构的优点和缺点是什么,你能找到一个比另一个更好的情况吗?

参考链接:

完整代码
链接列表 应用
链接列表