138. 复制带随机指针的链表
给定一个链表,每个节点包含一个额外增加的随机指针,该指针可以指向链表中的任何节点或空节点。
要求返回这个链表的深拷贝。
示例:
输入:
{“KaTeX parse error: Expected '}', got 'EOF' at end of input: …:"1","next": {"id”:“2”,“next”:null,“random”:
{“KaTeX parse error: Expected 'EOF', got '}' at position 9: ref":"2"}̲,"val":2},"rand…ref”:“2”},“val”:1}
解释:
节点 1 的值是 1,它的下一个指针和随机指针都指向节点 2 。
节点 2 的值是 2,它的下一个指针指向 null,随机指针指向它自己。
提示:你必须返回给定头的拷贝作为对克隆列表的引用。
思路一:
首先,我们来看一下有向链表:
在上图中,对于一个节点,它的 next 指针指向链表中的下一个节点。 next 指针是通常有向链表中有的部分且将所有节点 链接 起来。图中有趣的一点,也是这题有趣的一点在于 random 指针,正如名字所示,它可以指向链表中的任一节点也可以为空。
方法 1:回溯
想法
回溯算法的第一想法是将链表想象成一张图。链表中每个节点都有 2 个指针(图中的边)。因为随机指针给图结构添加了随机性,所以我们可能会访问相同的节点多次,这样就形成了环。
上图中,我们可以看到随机指针指向了前一个节点,因此成环。我们需要考虑这种环的实现。
此方法中,我们只需要遍历整个图并拷贝它。拷贝的意思是每当遇到一个新的未访问过的节点,你都需要创造一个新的节点。遍历按照深度优先进行。我们需要在回溯的过程中记录已经访问过的节点,否则因为随机指针的存在我们可能会产生死循环。
算法
1.从头指针开始遍历整个图。
我们将链表看做一张图。下图对应的是上面的有向链表的例子,Head 是图的出发节点。
2.当我们遍历到某个点时,如果我们已经有了当前节点的一个拷贝,我们不需要重复进行拷贝。
3.如果我们还没拷贝过当前节点,我们创造一个新的节点,并把该节点放到已访问字典中,即:
visited_dictionary[current_node] = cloned_node_for_current_node.
4.我们针对两种情况进行回溯调用:一个顺着 random 指针调用,另一个沿着 next 指针调用。步骤 1 中将 random 和 next 指针分别红红色和蓝色标注。然后我们分别对两个指针进行函数递归调用:
cloned_node_for_current_node.next = copyRandomList(current_node.next);
cloned_node_for_current_node.random=copyRandomList(current_node.random);
复杂度分析
时间复杂度:O(N) ,其中 N是链表中节点的数目。
空间复杂度:O(N)。如果我们仔细分析,我们需要维护一个回溯的栈,同时也需要记录已经被深拷贝过的节点,也就是维护一个已访问字典。渐进时间复杂度为 O(N)。
方法 2: O(N)空间的迭代
想法
迭代算法不需要将链表视为一个图。当我们在迭代链表时,我们只需要为random指针和next指针指向的未访问过节点创造新的节点并赋值即可。
算法
1.从 head 节点开始遍历链表。下图中,我们首先创造新的 head 拷贝节点。拷贝的节点如下图虚线所示。实现中,我们将该新建节点的引用也放入已访问字典中。
2.random 指针
如果当前节点 i的 random 指针只想一个节点 j且节点 j已经被拷贝过,我们将直接使用已访问字典中该节点的引用而不会新建节点。
如果当前节点 i的 random 指针只想的节点 j还没有被拷贝过,我们就对 j节点创建对应的新节点,并把它放入已访问节点字典中。
下图中,A 的 random 指针指向的节点 C。前图中可以看出,节点 C还没有被访问过,所以我们创造一个拷贝的 C’节点与之对应,并将它添加到已访问字典中。
3.next 指针
如果当前节点 ii 的 next 指针指向的节点 jj 在已访问字典中已有拷贝,我们直接使用它的拷贝节点。
如果当前节点 ii 的next 指针指向的节点 jj 还没有被访问过,我们创建一个对应节点的拷贝,并放入已访问字典。
下图中,A节点的next 指针指向节点B。节点B在前面的图中还没有被访问过,因此我们创造一个新的拷贝 B’节点,并放入已访问字典中。
4.我们重复步骤 2 和步骤 3 ,直到我们到达链表的结尾。
下图中,节点B的 random 指针指向的节点 A已经被访问过了,因此在步骤 2 中,我们不会创建新的拷贝,只将节点 B’的 random 指针指向克隆节点 A’。
同样的,节点B的next 指针指向的节点C 已经访问过,因此在步骤 3 中,我们不会创建新的拷贝,而直接将 B’的 next 指针指向已经存在的拷贝节点 C’。
复杂度分析
时间复杂度:O(N)。因为我们需要将原链表逐一遍历。
空间复杂度:O(N)。 我们需要维护一个字典,保存旧的节点和新的节点的对应。因此总共需要 N个节点,需要 O(N)的空间复杂度。
方法 3:O(1)空间的迭代
想法
与上面提到的维护一个旧节点和新节点对应的字典不同,我们通过扭曲原来的链表,并将每个拷贝节点都放在原来对应节点的旁边。这种旧节点和新节点交错的方法让我们可以在不需要额外空间的情况下解决这个问题。让我们看看这个算法如何工作
算法
- 遍历原来的链表并拷贝每一个节点,将拷贝节点放在原来节点的旁边,创造出一个旧节点和新节点交错的链表。
如你所见,我们只是用了原来节点的值拷贝出新的节点。原节点 next 指向的都是新创造出来的节点。
cloned_node.next = original_node.next
original_node.next = cloned_node
2.迭代这个新旧节点交错的链表,并用旧节点的 random 指针去更新对应新节点的 random 指针。比方说, B 的 random 指针指向 A ,意味着 B’ 的 random 指针指向 A’ 。
3.现在random指针已经被赋值给正确的节点,next指针也需要被正确赋值,以便将新的节点正确链接同时将旧节点重新正确链接。复杂度分析
• 时间复杂度:O(N)
• 空间复杂度:O(1)
作者:LeetCode
链接:https://leetcode-cn.com/problems/copy-list-with-random-pointer/solution/fu-zhi-dai-sui-ji-zhi-zhen-de-lian-biao-by-leetcod/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
思路二:
想法一哈希
借助哈希保存节点信息。
代码
时间复杂度:O(n)
空间复杂度:O(n)
想法二:原地复制
- 复制节点,同时将复制节点链接到原节点后面,如A->B->C 变为 A->A’->B->B’->C->C’。
- 设置节点random值。
-
将复制链表从原链表分离。
代码
时间复杂度:O(n)
空间复杂度:O(1)
参考链接:https://leetcode-cn.com/problems/copy-list-with-random-pointer/solution/138-fu-zhi-dai-sui-ji-zhi-zhen-de-lian-biao-ha-xi-/
我的:
/*
// Definition for a Node.
class Node {
public:
int val;
Node* next;
Node* random;
Node(int _val) {
val = _val;
next = NULL;
random = NULL;
}
};
*/
class Solution {
public:
Node* copyRandomList(Node* head)
{
if(head == nullptr)
{
return head;
}
Node *node = head;
//1.将复制节点添加到原节点后面
while(node != nullptr)
{
Node *copy = new Node(node -> val,nullptr,nullptr);
copy -> next = node -> next;
node -> next = copy;
node = copy -> next;
}
//2.复制random节点
node = head;
while(node != nullptr)
{
if(node -> random != nullptr)
{
node -> next -> random = node -> random -> next;
}
node = node -> next -> next;
}
//3.分离链表
node = head;
Node *newHead = head -> next;
Node *newNode = newHead;
while(node != nullptr)
{
node -> next = node -> next -> next;
if(newNode -> next != nullptr)
{
newNode -> next = newNode -> next -> next;
}
node = node -> next;
newNode = newNode -> next;
}
return newHead;
}
};
19. 删除链表的倒数第N个节点
给定一个链表,删除链表的倒数第n个节点,并且返回链表的头结点。
示例:
给定一个链表: 1->2->3->4->5, 和 n = 2.
当删除了倒数第二个节点后,链表变为 1->2->3->5.
说明:
给定的n保证是有效的。
进阶:
你能尝试使用一趟扫描实现吗?
思路一:
本文适用于初学者。它介绍了以下内容:链表的遍历和删除其末尾的第 n 个元素。
方法一:两次遍历算法
思路
我们注意到这个问题可以容易地简化成另一个问题:删除从列表开头数起的第(L−n+1) 个结点,其中 L是列表的长度。只要我们找到列表的长度 L,这个问题就很容易解决。
算法
首先我们将添加一个哑结点作为辅助,该结点位于列表头部。哑结点用来简化某些极端情况,例如列表中只含有一个结点,或需要删除列表的头部。在第一次遍历中,我们找出列表的长度L。然后设置一个指向哑结点的指针,并移动它遍历列表,直至它到达第 (L−n) 个结点那里。我们把第 (L−n) 个结点的 next 指针重新链接至第 (L - n + 2)个结点,完成这个算法。
图 1. 删除列表中的第 L - n + 1 个元素
复杂度分析
时间复杂度: O(L),该算法对列表进行了两次遍历,首先计算了列表的长度 L其次找到第 (L−n)个结点。操作执行了2L-n步,时间复杂度为 O(L)。
空间复杂度:O(1),我们只用了常量级的额外空间。
方法二:一次遍历算法
算法
上述算法可以优化为只使用一次遍历。我们可以使用两个指针而不是一个指针。第一个指针从列表的开头向前移动 n+1步,而第二个指针将从列表的开头出发。现在,这两个指针被 n个结点分开。我们通过同时移动两个指针向前来保持这个恒定的间隔,直到第一个指针到达最后一个结点。此时第二个指针将指向从最后一个结点数起的第 n个结点。我们重新链接第二个指针所引用的结点的 next 指针指向该结点的下下个结点。
图 2. 删除链表的倒数第 N 个元素
复杂度分析
时间复杂度:O(L),该算法对含有 L 个结点的列表进行了一次遍历。因此时间复杂度为 O(L)。
空间复杂度:O(1),我们只用了常量级的额外空间。
作者:LeetCode
链接:https://leetcode-cn.com/problems/remove-nth-node-from-end-of-list/solution/shan-chu-lian-biao-de-dao-shu-di-nge-jie-dian-by-l/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
思路二:
采取双重遍历肯定是可以解决问题的,但题目要求我们一次遍历解决问题,那我们的思路得发散一下。
我们可以设想假设设定了双指针 p 和 q 的话,当 q 指向末尾的 NULL,p 与 q 之间相隔的元素个数为 n 时,那么删除掉 p 的下一个指针就完成了要求。
- 设置虚拟节点 dummyHead 指向 head
- 设定双指针 p 和 q,初始都指向虚拟节点 dummyHead
- 移动 q,直到 p 与 q 之间相隔的元素个数为 n
- 同时移动 p 与 q,直到 q 指向的为 NULL
-
将 p 的下一个节点指向下下个节点
代码实现
作者:MisterBooo
链接:https://leetcode-cn.com/problems/remove-nth-node-from-end-of-list/solution/dong-hua-tu-jie-leetcode-di-19-hao-wen-ti-shan-chu/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
我的:
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode(int x) : val(x), next(NULL) {}
* };
*/
class Solution {
public:
ListNode* removeNthFromEnd(ListNode* head, int n)
{
ListNode* dummyHead = new ListNode(0);
dummyHead -> next = head;
ListNode* p = dummyHead;
ListNode* q = dummyHead;
for(int i = 0;i<n + 1 ; i++)
{
p = p -> next;
}
while(p)
{
p = p -> next;
q = q -> next;
}
q -> next = q -> next -> next;
return dummyHead -> next;
}
};
2. 两数相加
给出两个非空的链表用来表示两个非负的整数。其中,它们各自的位数是按照逆序的方式存储的,并且它们的每个节点只能存储一位数字。
如果,我们将这两个数相加起来,则会返回一个新的链表来表示它们的和。
您可以假设除了数字 0 之外,这两个数都不会以 0开头。
示例:
输入:(2 -> 4 -> 3) + (5 -> 6 -> 4)
输出:7 -> 0 -> 8
原因:342 + 465 = 807
思路二:
解法1:
整体思路:
将长度较短的链表在末尾补零使得两个连表长度相等,再一个一个元素对其相加(考虑进位)
具体步骤:
- 获取两个链表所对应的长度
- 在较短的链表末尾补零
-
对齐相加考虑进位
实现代码:
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode(int x) : val(x), next(NULL) {}
* };
*/
class Solution {
public:
ListNode* addTwoNumbers(ListNode* l1, ListNode* l2) {
int len1=1;//记录l1的长度
int len2=1;//记录l2的长度
ListNode* p=l1;
ListNode* q=l2;
while(p->next!=NULL)//获取l1的长度
{
len1++;
p=p->next;
}
while(q->next!=NULL)//获取l2的长度
{
len2++;
q=q->next;
}
if(len1>len2)//l1较长,在l2末尾补零
{
for(int i=1;i<=len1-len2;i++)
{
q->next=new ListNode(0);
q=q->next;
}
}
else//l2较长,在l1末尾补零
{
for(int i=1;i<=len2-len1;i++)
{
p->next=new ListNode(0);
p=p->next;
}
}
p=l1;
q=l2;
bool count=false;//记录进位
ListNode* l3=new ListNode(-1);//存放结果的链表
ListNode* w=l3;//l3的移动指针
int i=0;//记录相加结果
while(p!=NULL&&q!=NULL)
{
i=count+p->val+q->val;
w->next=new ListNode(i%10);
count=i>=10?true:false;
w=w->next;
p=p->next;
q=q->next;
}
if(count)//若最后还有进位
{
w->next=new ListNode(1);
w=w->next;
}
return l3->next;
}
};
解法2:
整体思路:
不对齐补零,若链表不为空则用sum(代表每个位的和的结果)加上,考虑进位。
实现代码:
class Solution {
public:
ListNode* addTwoNumbers(ListNode* l1, ListNode* l2) {
ListNode* head=new ListNode(-1);//存放结果的链表
ListNode* h=head;//移动指针
int sum=0;//每个位的加和结果
bool carry=false;//进位标志
while(l1!=NULL||l2!=NULL)
{
sum=0;
if(l1!=NULL)
{
sum+=l1->val;
l1=l1->next;
}
if(l2!=NULL)
{
sum+=l2->val;
l2=l2->next;
}
if(carry)
sum++;
h->next=new ListNode(sum%10);
h=h->next;
carry=sum>=10?true:false;
}
if(carry)
{
h->next=new ListNode(1);
}
return head->next;
}
};
作者:chenlele
链接:https://leetcode-cn.com/problems/add-two-numbers/solution/liang-shu-xiang-jia-by-gpe3dbjds1/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
我的:
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode(int x) : val(x), next(NULL) {}
* };
*/
class Solution {
public:
ListNode* addTwoNumbers(ListNode* l1, ListNode* l2)
{
ListNode* head = new ListNode(-1);
ListNode* h = head;
bool carry = false;
int sum = 0;
while(l1 != NULL || l2 != NULL)
{
sum = 0;
if(l1)
{
sum = sum + l1 -> val;
l1 = l1 -> next;
}
if(l2)
{
sum = sum + l2->val;
l2 = l2 -> next;
}
if(carry)
{
sum = sum + 1;
}
h -> next = new ListNode(sum%10);
h = h -> next;
carry = sum >= 10? true:false;
}
if(carry)
{
h -> next = new ListNode(1);
}
return head -> next;
}
};