在这篇文章中,我们解释了从单链表末端移除第N个节点的两种方法。我们可以在O(N)时间内通过单次/一次遍历完成这个任务。
目录:
- 链接列表的介绍
- 方法1:对关联列表进行两次遍历
- 方法2:一次遍历列表,一次通过
你应该了解的类似问题:
这类似于Leetcode问题19。从列表的末端删除第N个节点。
链接列表简介
链接列表是一种动态的线性数据结构,其中的元素形成了一个节点序列,每个节点都使用指针链接到另一个节点上。
这种数据结构被认为可以从任何位置进行有效的插入和删除,但是访问时间很昂贵。
有4种类型的链表,每种都有自己的用途和应用,它们是:单链表、双链表、循环链表和双循环链表。
你可以在本帖末尾提供的链接中阅读更多关于这些的内容。
在这里,我们将讨论从单链表的末端删除第n个节点,但首先我们需要讨论单链表的结构,以获得一些关于如何执行该操作的洞察力。
单链表的结构。
头部节点表示列表的开始。
一个节点包含数据字段和指向下一个节点的指针,称为next。
单链列表的最后一个节点next的指针指向null,表示它是标志着列表结束的节点。
在我们开始解决这个问题之前,我们需要了解删除链表中的一个节点的实际操作。
下面是这些程序的图示。
删除第一个节点
- 使头部指针指向头部之后的下一个节点
- 释放头部节点的内存
删除中间节点
- 遍历到待删除节点之前的前一个节点
- 使前一个节点的下一个指针指向要删除的节点之后的节点,该节点不包括被删除的节点
- 释放被删除节点的内存
删除最后一个节点
- 遍历到链表的倒数第二个节点
- 使其下一个指针指向空,表示列表的结束
- 释放被删除节点的内存
考虑到这一点,我们可以从列表的末端删除第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
算法:
- 创建一个临时节点,指向列表的头部。
- 在第一遍中获得列表的长度。
- 设置指针到临时节点,并在列表中移动,直到我们到达(长度-n)节点,即我们需要删除的节点之前的节点。
- 使(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
算法:
- 创建两个指针,快指针指向第(n-1)个节点,慢指针指向第0个节点
- 通过链接列表更新这两个指针
- 当循环结束时,fast.next == null,fast将指向最后一个节点,slow将在最后一个节点的(n-1)个节点
- 将slow的指针设置为next的next
- 返回没有删除元素的列表头部
代码:
// 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),因为没有使用额外的空间。
问题:
- 为什么我们在双通道方法中使用快指针和慢指针?
- 你如何比较链表和数组,每种数据结构的优点和缺点是什么,你能找到一个比另一个更好的情况吗?