leetcode-链表总结

leetcode-237-删除链表中的节点(delete node in a linkedlist)-java
单向链表中,如果给定链表中的一个节点,不给这个之前的,想要影响之前的,只能把自己改变。
比如要删掉自己,只能把自己变成下一个

leetcode-19-删除链表的倒数第N个节点(remove nth node from end of list)-java
首先,如果要删除单链表的头结点,直接head=head.next
如果要删除除了头结点之外的节点,pre.next=now.next即可

然后,如果一些本来需要两遍扫描但要求一遍的,可以考虑双指针算法,一个在前,一个在后,之后两个同时行动

leetcode-206-反转链表(reverse linked list)-java
反转链表 a 变为b
将a的每个head 变为b的head,每次再去掉a的head,即可
a 1 2 3 4 5
a的1 2 3 4 5 依次变为b的头,最后为 5 4 3 2 1(一个链表 头在最左,尾在最右,将a的头放在b的最左)

也可以设置通过保存pre,now,next的节点,将now.next=pre,遍历一遍即可
直接让next关系逐个向前

leetcode-21-合并两个有序链表(merge two sorted lists)-java
逐次比较两个链表的头,小的变为结果集的尾部,两者指针皆向前移动一步
而且可以在开始时,创建一个新的节点,将剩余的变成new.next,最后返回new.next即可(而不是先找到最小的节点)

leetcode-234-回文链表(palindrome linked list)-java
如果要让链表首尾比较,可以先把前半部分翻转,再把前后比较(可以用快慢指针,使用快慢指针得到中点,而且慢指针同时将链表反转)
注意,如果要让空间o©,时间o(n),记住翻转的方法

leetcode-141-环形链表(linked list cycle)-java
如果遇到链表是否循环,而且不要额外空间
可用双指针
1 快慢指针,一个走2,一个走1,遇到则循环
2 一个前一个后,将走过的指针的next改为head或者自己

leetcode-2-两数相加(add two numbers)-java
发现两个链表数字的顺序是倒着的,与做加法的顺序一样,可以类似加法,直接沿着链表做加法
链表对应结点相加时增加前一个结点的进位,并保存下一个结点的进位;
两个链表长度不一致时,要处理较长链表剩余的高位和进位计算的值
如果最高位计算时还产生进位,则还需要添加一个额外结点

leetcode-328-奇偶链表(odd even linkedlist)-java
双指针,设置奇数的头尾,偶数的头尾,在一次遍历中,如果该为奇数位,则奇数的尾.next=now,偶数亦然,最后奇数尾.next=偶数头,偶数尾.next=null
将奇节点放在一个链表里,偶链表放在另一个链表里。然后把偶链表接在奇链表的尾部
每次奇数.next=偶数.next,偶数.next=奇数.next 1.next=2.next=3 2.next=3.next=4

leetcode-160- 相交链表(intersection of two linked list)-java
首先有一点非常重要,相交节点后的所有节点 (包括相交节点) 都是两个链表共享的。
我们将两个链表的右端对齐,长度较大的链表 的左端的 多出来的节点 肯定不是相交节点,所以我们不考虑左端的这一部分的节点
使用双指针算法,先遍历一遍两个链表的长度,再将两个指针指向两个链表头部,移动长的链表的指针,指向相同距离的位置,再两个指针一起走,如果指针相遇,则得到相同节点
或者
我们需要做的事情是,让两个链表从同距离末尾同等距离的位置开始遍历。这个位置只能是较短链表的头结点位置。
为此,我们必须消除两个链表的长度差
指针 pA 指向 A 链表,指针 pB 指向 B 链表,依次往后遍历
如果 pA 到了末尾,则 pA = headB 继续遍历
如果 pB 到了末尾,则 pB = headA 继续遍历
比较长的链表指针指向较短链表head时,长度差就消除了
如此,只需要将最短链表遍历两次即可找到位置
即假设有长度A和B,A长,
双指针从A跑完,pA跑了长度A,到B的headB
pB从B跑,B跑完跑了B,再到A的headA,跑了A-B,
此时pA在B的headB,pB在A的A-B处,是相同的位置。

leetcode-23-合并K个排序链表-java
优先队列能传入的参数有index,ListNode,val,可以只传入ListNode,因为它包含了val,并且还有next,能够定位当前节点和下一个节点。如果传入index,可以用Pair<val,index>。
注意:如果要使用堆排序,可以使用优先级队列,PriorityQueue

leetcode-148-排序链表-java
解法1(成功,5ms,较快)
采用自顶向下的归并排序算法,速度O(nlogn),空间O(logn)
通过递归实现链表归并排序,有以下两个环节:
分割 split环节: 找到当前链表中点,并从中点将链表断开(以便在下次递归 cut 时,链表片段拥有正确边界);
找到中点 now后,执行 now.next = Null将链表切断。
递归分割时,输入当前链表左端点 head 和中心节点 now的下一个节点 next(因为链表是从 now切断的)。
split递归终止条件: 当head.next = = Null时,说明只有一个节点了,直接返回此节点。
合并 merge 环节: 将两个排序链表合并,转化为一个排序链表。
双指针法合并,建立辅助ListNode h 作为头部。
设置两指针 left, right 分别指向两链表头部,比较两指针处节点值大小,由小到大加入合并链表头部,指针交替前进,直至添加完两个链表。
返回辅助ListNode h 作为头部的下个节点 h.next。
时间复杂度 O(l + r),l, r 分别代表两个链表长度。
当题目输入的 head == Null时,直接返回Null。

解法2(别人的)

采用自底向上的归并排序算法,速度O(nlogn),空间O(1)

对于非递归的归并排序,需要使用迭代的方式替换cut环节:
我们知道,cut环节本质上是通过二分法得到链表最小节点单元,再通过多轮合并得到排序结果。
每一轮合并merge操作针对的单元都有固定长度intv,例如:
第一轮合并时intv = 1,即将整个链表切分为多个长度为1的单元,并按顺序两两排序合并,合并完成的已排序单元长度为2。
第二轮合并时intv = 2,即将整个链表切分为多个长度为2的单元,并按顺序两两排序合并,合并完成已排序单元长度为4。
以此类推,直到单元长度intv >= 链表长度,代表已经排序完成。
根据以上推论,我们可以仅根据intv计算每个单元边界,并完成链表的每轮排序合并,例如:
当intv = 1时,将链表第1和第2节点排序合并,第3和第4节点排序合并,……。
当intv = 2时,将链表第1-2和第3-4节点排序合并,第5-6和第7-8节点排序合并,……。
当intv = 4时,将链表第1-4和第5-8节点排序合并,第9-12和第13-16节点排序合并,……。

模拟上述的多轮排序合并:
统计链表长度length,用于通过判断intv < length判定是否完成排序;
额外声明一个节点res,作为头部后面接整个链表,用于:
intv *= 2即切换到下一轮合并时,可通过res.next找到链表头部h;
执行排序合并时,需要一个辅助节点作为头部,而res则作为链表头部排序合并时的辅助头部pre;后面的合并排序可以将上次合并排序的尾部tail用做辅助节点。
在每轮intv下的合并流程:
根据intv找到合并单元1和单元2的头部h1, h2。由于链表长度可能不是2^n,需要考虑边界条件:
在找h2过程中,如果链表剩余元素个数少于intv,则无需合并环节,直接break,执行下一轮合并;
若h2存在,但以h2为头部的剩余元素个数少于intv,也执行合并环节,h2单元的长度为c2 = intv - i。
合并长度为c1, c2的h1, h2链表,其中:
合并完后,需要修改新的合并单元的尾部pre指针指向下一个合并单元头部h。(在寻找h1, h2环节中,h指针已经被移动到下一个单元头部)
合并单元尾部同时也作为下次合并的辅助头部pre。
当h == None,代表此轮intv合并完成,跳出。
每轮合并完成后将单元长度×2,切换到下轮合并:intv *= 2。

leetcode-138-复制带随机指针的链表-java
我们通过扭曲原来的链表,并将每个拷贝节点都放在原来对应节点的旁边。这种旧节点和新节点交错的方法让我们可以在不需要额外空间的情况下解决这个问题。让我们看看这个算法如何工作
遍历原来的链表并拷贝每一个节点,将拷贝节点放在原来节点的旁边,创造出一个旧节点和新节点交错的链表。
如你所见,我们只是用了原来节点的值拷贝出新的节点。原节点 next 指向的都是新创造出来的节点。
cloned_node.next = original_node.next
original_node.next = cloned_node
迭代这个新旧节点交错的链表,并用旧节点的 random 指针去更新对应新节点的 random 指针。比方说, B 的 random 指针指向 A ,意味着 B’ 的 random 指针指向 A’ 。
现在 random 指针已经被赋值给正确的节点, next 指针也需要被正确赋值,以便将新的节点正确链接同时将旧节点重新正确链接。

leetcode-146-LRU缓存机制-java
LRU 缓存机制可以通过哈希表辅以双向链表实现,我们用一个哈希表和一个双向链表维护所有在缓存中的键值对。
双向链表按照被使用的顺序存储了这些键值对,靠近头部的键值对是最近使用的,而靠近尾部的键值对是最久未使用的。
哈希表即为普通的哈希映射(HashMap),通过缓存数据的键映射到其在双向链表中的位置。

这样以来,我们首先使用哈希表进行定位,找出缓存项在双向链表中的位置,随后将其移动到双向链表的头部,即可在 O(1)的时间内完成 get 或者 put 操作。具体的方法如下:
对于 get 操作,首先判断 key 是否存在:
如果 key 不存在,则返回 −1;
如果 key 存在,则 key 对应的节点是最近被使用的节点。通过哈希表定位到该节点在双向链表中的位置,并将其移动到双向链表的头部,最后返回该节点的值。

对于 put 操作,首先判断 key 是否存在:
如果 key 不存在,使用 key 和 value 创建一个新的节点,在双向链表的头部添加该节点,并将 key 和该节点添加进哈希表中。然后判断双向链表的节点数是否超出容量,如果超出容量,则删除双向链表的尾部节点,并删除哈希表中对应的项;
如果 key 存在,则与 get 操作类似,先通过哈希表定位,再将对应的节点的值更新为 value,并将该节点移到双向链表的头部。

上述各项操作中,访问哈希表的时间复杂度为 O(1),在双向链表的头部添加节点、在双向链表的尾部删除节点的复杂度也为 O(1)。而将一个节点移到双向链表的头部,可以分成「删除该节点」和「在双向链表的头部添加节点」两步操作,都可以在 O(1)时间内完成。
在双向链表的实现中,使用一个伪头部(dummy head)和伪尾部(dummy tail)标记界限,这样在添加节点和删除节点的时候就不需要检查相邻的节点是否存在。

leetcode-142-环形链表 II-java
使用双指针算法,一个slow指针,一个fast指针,一个每次1步,一个每次两步,链表如果循环,分为开始的不循环部分not和循环的circle,slow走了x的距离,fast走了2x的距离,slow与fast相遇时,fast比slow多走了circle的距离,x+circle=2x,所以slow走的距离就是circle。
然后两个指针都到head,fast先走circle步,然后fast和slow每次都走一步,如果slow走了not距离,fast则总共走了now+circle距离,完成一次大循环,重新来到刚开始循环的地方,返回这个节点即可。
注意:当not远大于circle时,第一步算出来的circle会是真是circle的k倍,但是不影响第二步,一个走了not,一个走了now+k*circle,下一步还是刚开始循环的地方

对解法1的优化,可以发现解法1内,第一步相遇时,slow走了circle的距离,恰好是第二步里fast要先走的距离。
就是第一步里相遇的地方,就是第二步里fast先走完后的位置,只需slow节点指向head,然后两个指针一起走即可
该方法为弗洛伊德算法,具体见 https://leetcode-cn.com/problems/linked-list-cycle-ii/solution/huan-xing-lian-biao-ii-by-leetcode/

剑指offer-5-从尾到头打印链表-java
倒序输出链表有2个方法:
1 如果不改变原有链表,可以使用栈,依次压入后,弹出
2 改变原有链表,将原有链表进行倒序,然后从头开始读

剑指offer-13-O(1)时间删除链表结点-java
如果删除节点是头结点,返回头结点的next
如果删除节点是中间结点,删除节点的值设置为next的值,然后删除节点的next指向next的next
如果删除节点是尾节点,从头开始遍历,设置删除节点的前一个节点的next为null

剑指offer-15-链表中倒数第K个结点-java
注意:
1、输入Head指针为Null。由于代码会试图访问空指针指向的内存,程序会崩溃。
2、输入以Head为头结点的链表的结点总数少于k。由于在for循环中会在链表向前走k-1步,仍然会由于空指针造成崩溃。
3、输入的参数k为0.或负数,同样会造成程序的崩溃。

定义两个指针。第一个指针从链表的头指针开始遍历向前走k-1。第二个指针保持不动;从第k步开始,第二个指针也开化寺从链表的头指针开始遍历。由于两个指针的距离保持在k-1,当第一个(走在前面的)指针到达链表的尾指结点时,第二个指针正好是倒数第k个结点。

剑指offer-16-反转链表-java
对于一个链表,我们只能从头往后遍历,如果要反转,我们需要更改当前节点的next域指向前一个节点,此时链表断开,为了能继续修改下一个节点的next域,我们还要维护下一个节点。

剑指offer-17-合并两个排序的链表-java
建立一个新的节点作为head,返回结果时,返回head.next,tail一开始为head
当l1和l2都不为空时,进行循环,不断设置tail.next,同时更新tail,l1,l2
当l1和l2有一个为空时,tail.next为不为空的链表

剑指offer-26-复杂链表的复制-java
第一步:让仍然是根据原始链表的每个结点N创建对应的N’。不过我们把N’链接在N的后面。
第二步:设置复制出来的结点的m_pSibling。原始链表上的A的m_pSibling指向结点C,那么其对应复制出来的A’是A的m_pNext指向的结点,同样C’也是C的m_pNext指向的结点。即A’ = A.next,A’.m_pSibling = A.m_pSibling.next;故像这样就可以把每个结点的m_pSibling设置完毕。
第三步:将这个长链表拆分成两个链表:把奇数位置的结点用m_pNext链接起来就是原始链表,把偶数位置的结点用m_pNext链接起来就是复制出来的链表。

剑指offer-37-两个链表的第一个公共节点-java
方法1
首先有一点非常重要,相交节点后的所有节点 (包括相交节点) 都是两个链表共享的。
我们将两个链表的右端对齐,长度较大的链表 的左端的 多出来的节点 肯定不是相交节点,所以我们不考虑左端的这一部分的节点
使用双指针算法,先遍历一遍两个链表的长度,再将两个指针指向两个链表头部,移动长的链表的指针,指向相同距离的位置,再两个指针一起走,如果指针相遇,则得到相同节点

方法2
其实思路和解法1一样,都是双指针,等指针在两个链表的相同位置后,开始比较
我们需要做的事情是,让两个链表从同距离末尾同等距离的位置开始遍历。这个位置只能是较短链表的头结点位置。
为此,我们必须消除两个链表的长度差

指针 pA 指向 A 链表,指针 pB 指向 B 链表,依次往后遍历
如果 pA 到了末尾,则 pA = headB 继续遍历
如果 pB 到了末尾,则 pB = headA 继续遍历
比较长的链表指针指向较短链表head时,长度差就消除了
如此,只需要将最短链表遍历两次即可找到位置

即假设有长度A和B,A长,
双指针从A跑完,pA跑了长度A,到B的headB
pB从B跑,B跑完跑了B,再到A的headA,跑了A-B,
此时pA在B的headB,pB在A的A-B处,是相同的位置。

剑指offer-45-圆圈中最后剩下的数字-java
解法1(成功)
既然题目中有一个数字圆圈,很自然的想法就是用个数据结构来模拟这个圆圈。在常用的数据结构中,我们很容易的想到环形链表。我们可以创建一个共有n个结点的环形链表,然后每次都从这个链表中删除第m个结点

解法2(别人的)

定义函数f(n,m),表示每次在n个数字0,1,…,n-1中每次删除第m个数字最后剩下的数字。
在n个数字中,第一个被删除的数字是(m%n)-1,我们把这个数字记为K. 在删除掉第一个元素K后,剩下的n-1个数字就是0,1,2,…,k-1,k+1,…,n-1,并且下一次删除从K+1开始计数。那么,在下一次计数的时候其实就相当于在这样一个序列中遍历:K+1,…,n-1,0, 1,… , K-1 。这个序列和前一个序列其实是一样的,不一样的是我们把它的顺序修改了一下而已,但是删除元素时遍历顺序是一样的。故经过若干次删除后剩下的数字和前一个序列也应该是一样的。我们把后一个序列每次删除第m个数字最后剩下的数字记为f’(n-1,m),至于为什么记为f’(n-1,m)你看到后面就懂了。那么现在我们最起码可以确定的是f(n,m)=f’(n-1,m)。

我们再来看分析这个序列:k+1,…,n-1,0,1,…,k-1 。我们将这个序列做一个映射,映射结果是形成一个从0到n-2的序列:
k+1 -> 0
k+2 -> 1

n-1 -> n-k-2
0 -> n-k-1
1 -> n-k

k-1 -> n-2
f’(n-1,m) f(n-1,m)
我们定义映射为p,那么p(x) = (x-k-1)%n 。 它表示如果映射前的数字是x,那么映射后的数字是(x-k-1)%n。该映射的逆映射是p-1(x)=(x+k+1)%n。既然要掌握这个方法,就要彻底搞懂,下面跟着我一起证明一遍:

证明:
令y = p(x),即 y = (x-k-1)%n
则有 y = (x-k-1) +t1n,t1属于整数,且0<= y <n
< —> x = y - t1n + k + 1
<----> x = (y+k+1) + t2n ,即 y = (x+k+1) + tn,故p-1(x) = (x+k+1) %n
证明完毕。

现在,我们发现经过映射之后的n-1个数字是不是和原先的n个数字形式上是一样的?只不过少看一个数字n-1而已。那么,对0,1,…,n-2这n-1个数字,排成一个圆圈,从数字0开始每次删除第m个数,剩下的数字是不是可以表示成f(n-1,m)?! 现在有没有发现我们之前为什么要定义那么序列为 f’(n-1,m)? 这是要建立两次删除之间的联系!就是说原始的n个元素,在删除第一个元素k之后,按理说初始序列已经被打乱了,没有规则了;但是我们通过一个映射关系,让序列重新排列成初始序列的形式。这样只要我们找到这样的映射关系,求出两次操作之间的函数关系(迭代规律)就将问题转化成了递归问题。而递归问题的出口很好确定,当n=1时,序列只有一个元素:0,f(1,m)就是这个0!
既然有了映射关系,下面我们求两次迭代操作之间的关系,即如何由f(n-1,m)求得f(n,m)。

求解:
因为f(n,m) = f’(n-1,m),且f’(n-1,m) = (f(n-1,m)+k+1)%n,故f(n,m) = (f(n-1,m)+k+1)%n。 又因为 k = (m%n)-1,带入f(n,m) = (f(n-1,m)+k+1)%n,得:f(n,m) = (f(n-1,m)+m)%n。
因此,当n=1时,f(n,m) = 0
当n>1时,f(n,m) = [f(n-1,m)+m]%n

有了这个递推关系,是不是可以写代码了?可以由上而下的用递归,也可以由下而上的用迭代。递归在这里显然不存在子问题重复求解的问题,但是会有大量的堆栈操作,不如直接用迭代的方式。至于迭代方式的源码,上面已经给出了。 int last = 0; 是当n=1时,f(1,m)的值;后面的for循环就是自下而上的求解f(n,m)的值了。
这种思路非常复杂,但是代码尤其简洁,主要的时间都花在了分析和推导公式上了。该方法时间复杂度为O(n),空间复杂度是O(1),无论是时间复杂度和空间复杂度都要好于第一种方法

leetcode-024-两两交换链表中的节点
解法1(成功,0ms,很快)
将链表根据节点的位置分成链表1和链表2,原来的变成12121,然后再合并,2121

解法2(别人的)
可以通过递归的方式实现两两交换链表中的节点。
递归的终止条件是链表中没有节点,或者链表中只有一个节点,此时无法进行交换。

如果链表中至少有两个节点,则在两两交换链表中的节点之后,原始链表的头节点变成新的链表的第二个节点,原始链表的第二个节点变成新的链表的头节点。链表中的其余节点的两两交换可以递归地实现。在对链表中的其余节点递归地两两交换之后,更新节点之间的指针关系,即可完成整个链表的两两交换。

用 head 表示原始链表的头节点,新的链表的第二个节点,用 newHead 表示新的链表的头节点,原始链表的第二个节点,则原始链表中的其余节点的头节点是 newHead.next。令 head.next = swapPairs(newHead.next),表示将其余节点进行两两交换,交换后的新的头节点为 head 的下一个节点。然后令 newHead.next = head,即完成了所有节点的交换。最后返回新的链表的头节点 newHead。

解法3(别人的)
迭代
也可以通过迭代的方式实现两两交换链表中的节点。
创建哑结点 dummyHead,令 dummyHead.next = head。令 temp 表示当前到达的节点,初始时 temp = dummyHead。每次需要交换 temp 后面的两个节点。
如果 temp 的后面没有节点或者只有一个节点,则没有更多的节点需要交换,因此结束交换。否则,获得 temp 后面的两个节点 node1 和 node2,通过更新节点的指针关系实现两两交换节点。
具体而言,交换之前的节点关系是 temp -> node1 -> node2,交换之后的节点关系要变成 temp -> node2 -> node1,因此需要进行如下操作。

temp.next = node2
node1.next = node2.next
node2.next = node1

完成上述操作之后,节点关系即变成 temp -> node2 -> node1。再令 temp = node1,对链表中的其余节点进行两两交换,直到全部节点都被两两交换。
两两交换链表中的节点之后,新的链表的头节点是 dummyHead.next,返回新的链表的头节点即可。

leetcode-025-k个一组翻转链表
解法1(成功,0ms,极快)
步骤分解:
链表分区为已翻转部分+待翻转部分+未翻转部分
每次翻转前,要确定翻转链表的范围,这个必须通过 k 此循环来确定
需记录翻转链表前驱和后继,方便翻转完成后把已翻转部分和未翻转部分连接起来
初始需要两个变量 pre 和 end,pre 代表待翻转链表的前驱,end 代表待翻转链表的末尾
经过k此循环,end 到达末尾,记录待翻转链表的后继 next = end.next
翻转链表,然后将三部分链表连接起来,然后重置 pre 和 end 指针,然后进入下一次循环
特殊情况,当翻转部分长度不足 k 时,在定位 end 完成后,end==null,已经到达末尾,说明题目已完成,直接返回即可
时间复杂度为 O(n*K) 最好的情况为 O(n)最差的情况未 O(n^2)

leetcode-061-旋转链表
解法1(成功,0ms,极快)
先获取链表的长度,然后从头部移动length-k-1次,得到head,leftTail,rightHead,
然后再向后移动到null,得到rightTail。然后rigthtail.next指向head,leftTail.next指向null即可

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值