1.1
删除链表中等于给定值 val 的所有结点。
https://leetcode-cn.com/problems/remove-linked-list-elements/description/https://leetcode-cn.com/problems/remove-linked-list-elements/description/思路一:定义prev和cur一前一后两个指针,如果cur的值不是val,就让prev向后走,cur也向后走。当cur遇到val的时候,就让prev的next指向cur的next,再删除cur。删除后不能再通过cur=cur->next找下一个结点,有野指针问题。应该让cur指向prev的next。
按照此思路写完后代码是这样,但是发生了错误。
这里是因为如果第一个结点就是val,那么prev是空指针,会引发空指针的问题。那如果第一个结点就是我们要删除的值,就像头删一样,直接让head指向下一个结点,并删除第一个。
思路二:定义一个新链表,定义一个指针遍历链表,如果结点中的值不是val,就将它尾插到新的链表中。如果是val,保存下一个结点的地址,删除val,再将val移到新的位置。
每次尾插都要找到新链表的尾部,效率比较低,因此再定义一个尾指针。更具此思路写完提交后出了错误。
用上面例子走一遍代码,发现5尾插到了新的结点,6被释放了,但5的next仍然指向6。修改后仍然有错误。
问题在于如果刚开始的链表是空的,会导致tail->next发生空指针访问问题。所以遇到这种问题,直接返回空指针就好。
1.2
给定一个带有头结点 head 的非空单链表,返回链表的中间结点。如果有两个中间结点,则返回第二个中间结点。
https://leetcode-cn.com/problems/middle-of-the-linked-list/description/https://leetcode-cn.com/problems/middle-of-the-linked-list/description/思路一:常规思路认为先遍历一遍统计结点的个数,让个数除以二,再遍历刚刚除二后的步数就走到了中间值。但如果要求只遍历一遍呢?
思路二:定义两个指针,一个指针每次走两步,另一个指针每次走一步,单数个结点快的指针走到尾结点停止;双数个结点快的指针走到空停止。
1.3
输入一个链表,输出该链表中倒数第k个结点。
思路一:先遍历一遍求长度,然后再重头开始向后走长度减k个。那如果只能遍历一遍呢?
思路二:因为倒数第k个和最后一个的距离相差k-1,所以定义两个指针,让其中一个指针变量先走k-1步,这样就与另外一个指针的举例相差k-1,再让它们同时走,快的指针走到最后时慢的指针也就走到了倒数第k个。或者让其中一个指针变量先走k步,这样就与另外一个指针的举例相差k,再让它们同时走,快的指针走到空时慢的指针也就走到了倒数第k个。这里就以后一个举例。
如果链表本来就是NULL,那就直接返回。如果类似有3个值但求倒数第四个的情况,也不存在,这时候造成fast是空。
1.4
反转一个单链表。
https://leetcode-cn.com/problems/reverse-linked-list/description/https://leetcode-cn.com/problems/reverse-linked-list/description/思路一:翻指针,让每个结点的指针都指向前一个,在数组中可以定义前后两个指针进行反转,但单链表不能倒着走,我们也同样定义两个指针变量n1和n2,n1初始指向空,n2初始指向第一个结点,每次让n2的下一个指向n1。
但是这样1,2间就断了,不能再继续向后走了,所以再定义一个n3用来保存下一个结点,每次n2给n1,n3给n2,再让n3指向n3的下一个。
当n2为空时链表就反转完成了,但是这里如果让n3继续指向下一个会出现空指针问题,因此对n3加限制,n3不为空时才能往下走。如果链表本来就是空的,直接返回空。
思路二:遍历链表,将每个结点头插到新的链表中,每次头插更新新的头指针。每次头插如果只有一个指针遍历头插完后会找不到下一个结点,因此再定义一个新指针保留下一个。
1.5
将两个有序链表合并为一个新的有序链表并返回。新链表是通过拼接给定的两个链表的所有结点组成的。
思路一:两个链表中的值依次比较,取小的那个尾插到新链表,要尾插就需要有头指针和尾指针,头指针为了返回头结点,尾指针用来方便尾插。定义两个指针依次遍历比较,有一个走完就比较完成,剩下的直接连接到尾部即可。刚开始尾插时进行赋值,后面直接链接即可。如果链表本来是空的,返回另一个链表就行了。
思路二:定义一个哨兵位,这样可以省去判断空的问题。最后不要忘记内存泄漏的问题。
1.6
编写代码,以给定值x为基准将链表分割成两部分,所有小于x的结点排在大于或等于x的结点之前。
思路:将小于x的尾插到一个链表,将大于等于x的尾插到另一个链表,再将两个链表链接起来。这里的新链表带哨兵位,否则会有很多空指针的问题。尾插定义两个指针变量。
写完测试后有这样的问题
这是因为尾插改变的是前一个结点的指向,这样会造成死循环。所以还要记得置空。也不能直接返回phead,很有可能phead不是从小开始的,所以记得更新。
1.7
链表的回文结构。
https://www.nowcoder.com/practice/d281619e4b3e4a60a2cc66ea32855bfa?tpId=49&&tqId=29370&rp=1&ru=/activity/oj&qru=/ta/2016test/question-rankinghttps://www.nowcoder.com/practice/d281619e4b3e4a60a2cc66ea32855bfa?tpId=49&&tqId=29370&rp=1&ru=/activity/oj&qru=/ta/2016test/question-ranking思路:1.先找中间结点(偶数是后一个)2.从中间结点开始逆置一下后半段。3.前半段结点开始与后半段结点开始比较。
能不能整个逆置,用逆置后的链表和原链表比较?不能,因为这样会改变原来的链表,除非复制再比较。
1.8
输入两个链表,找出它们的第一个公共结点。
https://leetcode-cn.com/problems/intersection-of-two-linked-lists/description/https://leetcode-cn.com/problems/intersection-of-two-linked-lists/description/怎么判断两个链表相交呢?首先来看看链表相交是什么样子的。第二种相交是不存在的,因为一个结点的next不可能指向两个结点。
判断两个头指针不同的链表相交,其实看看它们尾结点的地址是不是一样就可以了。那如果两个链表相交怎么找第一个公共结点,可以拿第一个链表中的每个结点的地址和第二个链表中的每个地址比较一遍,但这样时间复杂度是O(N^2)。那还有什么办法,我们可以这样想,如果两个链表从同一个位置开始向后比较就能很快找到了。
但怎么知道长的链表位置从哪里开始,可以把两个链表的长度分别求出来,相减取绝对值,再让长的链表走相减后的步数就可以了。那如何判断哪个链表长哪个链表断?可以先假设一种情况,再判断是否处理。
1.9
给定一个链表,判断链表中是否有环。
https://leetcode-cn.com/problems/linked-list-cycle/description/https://leetcode-cn.com/problems/linked-list-cycle/description/什么是链表代环呢?就是说尾结点可以指向环中的任意一个位置,也可以指向自己。
思路:使用快慢指针,定义一个慢指针slow,定义一个快指针fast,slow每次走一步,fast每次走两步。如果环存在,slow走到中间时,fast开始进环,slow进环时,fast以及转了一会,这样就变成了一个fast追slow的追击问题,在环中某个位置就相遇了。有些人肯定想遍历链表,如果再次走到入环口的位置就代表有环,但问题关键是很难知道哪里是入环点,因此把这个问题转为追击问题。如果没有环,fast就走出去了。
这样代码就很容易实现了,但是这里面有这样几个问题。问题一:为什么slow走一步,fast走两步,他们会相遇?会不会错过?请证明。我们可以这样证明,假设slow开始进环时fast和slow的距离是N,N最小是0,最大是C-1。C代表圆的周长。
slow进环以后,fast开始追击slow,slow每次走一步,fast每次走两步,他们之间的距离就缩小1。
fast和slow的距离 | N | N-1 | N-2 | ...... | 1 | 0 |
当距离是零的时候就追上了。
问题二:slow走一步,fast走X(X>=3)步,他们会相遇吗?会不会错过?请证明。
还是假设slow开始进环时fast和slow的距离是N,先拿X=3举个例子,slow每次走1步,fast每次走3步,他们之间的距离就缩小2.
fast和slow的距离 | |||||||
N是偶数(N%2=0) | N | N-2 | N-4 | ...... | 4 | 2 | 0 |
N是奇数(N%2=1) | N | N-2 | N-4 | ...... | 3 | 1 | -1 |
当N是偶数的时候,距离为0就追上了。当N是奇数的时候,距离为-1时,表示错过了,相当于距离为C-1,进入了新的一轮的追击。
在新的一轮的追击里,如果C-1是偶数,就可以追上;如果C-1是奇数,就永远追不上了。
再拿X=4举个例子,slow每次走1步,fast每次走4步,他们之间的距离就缩小3。
fast和slow的距离 | |||||||
N%3=0 | N | N-3 | N-6 | ...... | 6 | 3 | 0 |
N%3=1 | N | N-3 | N-6 | ...... | 4 | 1 | -2 |
N%3=2 | N | N-3 | N-6 | ...... | 5 | 2 | -1 |
如果距离为0就追上了;如果距离为-2代表错过了,相当于距离为C-2,进入了新的一轮的追击。如果距离为-1代表错过了,相当于距离为C-1,进入了新的一轮的追击。然后还是分情况看。所以slow走一步,fast走x步根据这个思路判断就可以了。
1.10
给定一个链表,返回链表开始入环的第一个结点。 如果链表无环,则返回 NULL。
https://leetcode-cn.com/problems/linked-list-cycle-ii/description/https://leetcode-cn.com/problems/linked-list-cycle-ii/description/那如果链表有环如何知道环的入口点在哪里呢?
结合上一题,定义两个指针,slow每次走1步,fast每次走2步,他们一定会相遇。
当相遇的时候,假设起始点到入环点的距离是L,入环点到相遇点的距离是X(0<=X<C)(因为slow从入环开始到和fast相遇走的距离不可能超过一个圆环的长度;0是因为可能刚入环就相遇了),环的长度是C。此时slow从起始点开始到相遇点走过的距离是L+X;fast从起始点开始到相遇点走过的距离是L+N*C+X。(为什么有N*C,因为可能起始点到入环点的距离很长但环的长度很小,此时在slow进入环点时fast已经绕环很多圈)。fast走的距离时slow的2倍,这时就可以得到这样一个式子:2*(L+X)= L+N*C+X。经过化简得到L=N*C-X。也可以将式子写为L=(N-1)*C+C-X。式子告诉我们,从相遇点开始不管走多少圈都可以再走到相遇点,然后再后退或者少走X就是L的距离。其实也就可以得到一个结论:一个指针从相遇点开始走,一个指针从起始点开始走,他们一定会在入环点相遇。这样就找到了入环点。
还有这样一种方法:可以先找到相遇点,让相遇点和相遇点的下一个结点直接断开,也就是让相遇点的next指向空。然后将找入口点的问题转化为之前提到的找相交点的问题,这里找相遇点下一个结点和其实点的相交点。
1.11
给定一个链表,每个结点包含一个额外增加的随机指针,该指针可以指向链表中的任何结点或空结点。要求返回这个链表的深度拷贝。
复制链表非常好复制,遍历一遍尾插到新结点就可以了。但麻烦是新结点中的random该如何处理?
比如有些人可能这样想:原链表中7的random指向空,就让新链表中7的random指向空,这没有问题;那原链表中13的random指向7,就让新链表中13的random指向7。可这样合理吗?原链表13结点的random存的7的地址,新的链表中的结点都是malloc出来的,怎么知道7在哪里。有人这时候可能会这样说,遍历一遍新的链表,找到7,将7的结点的地址存到13的random中。可是如果遇到这种情况呢?
这里原链表中11结点的random指向最后一个7,在新链表中找11结点的random就找到第一个7了,这样与原来不相符,就出现错误了。可能到这里就能想到找相对地址:就比如说原链表和新链表都遍历到13结点了,新链表中13结点开始找random指向,此时保存原链表中13结点中random的地址,然后在原链表中计数遍历找与保存结点一样的地址,然后只需要在新链表中走计数步就可以找到13结点中random指向哪里。这样也可以找到,但非常麻烦,每一次都2N,N个结点走完时间复杂度会达到O(N^2)。
那如果要求优化到时间复杂度是O(N)呢?这里可以想到用空间换取时间:这里有几个链表就开几个数组,给原链表开一个数组存所有结点的地址,给新链表开一个数组存所有结点的地址。
这样有什么用呢?比如开始找新链表中13结点的random了,在旧链表中13结点的random里面存的地址是知道的,可以遍历给旧链表开的数组找到该地址的下标,然后再新链表中直接将给新链表开的数组的下标对应的地址存放到random里面即可。这样感觉稍微快了一些,但时间复杂度还是O(N^2)。
那怎么办呢?其实关键在于,我们找到原链表中13结点中random指向的结点后我们希望可以快速找到在新链表中拷贝的该节点,这样就直接可以让新链表中13结点的random很快指向正确的位置了。那怎么做呢?可以让拷贝的链表和原来的链表建立关联关系,拷贝后的结点连接到原来结点的后面。
这样做可以发现,拷贝结点中random所需的指向就是原结点中random指向的next。这样就可以很容易的链接完所有拷贝结点的random。
最后将拷贝结点都拆下来,连接到新链表,恢复原链表。
介绍完前面所说的,下面就可以描述一下代码该如何实现了:
先完成将拷贝结点插入到原结点后面,定义这样几个指针变量,遍历原链表,每次cur不为空时就拷贝原结点并改变链接关系。
然后再次遍历原链表,只要cur不指向空,就让新链表结点(cur的next)的random找到合适的位置。原链表结点的random指向空,新链表结点的random就直接指向空;原链表结点的random不指向空,新链表结点的random就指向原链表结点random的next。
最后恢复链表,尾插,改变链接关系。