代码随想录算法训练营第四天 | 24. 两两交换链表中的节点、19. 删除链表的倒数第 N 个结点、面试题 02.07. 链表相交、142.环形链表II

118 阅读7分钟

24. 两两交换链表中的节点

文章链接

第一想法

题目要求是两两翻转节点,我的第一想法是双指针,通过指针的移动来不断修改链表,共有两个指针(pre和curr),pre指针指向两个节点翻转的前一个节点,curr指向两个节点翻转前的第一个节点.

image.png 代码如下:

type T=ListNode|null
function swapPairs(head: ListNode | null): ListNode | null {
 let vhead:T=new ListNode()
 vhead.next=head
 let pre:T=vhead
 let curr:T=vhead.next
 while(curr&&curr.next){ //这里需要保证curr和curr.next存在 curr用于判断偶数链表的终止条件,curr.next用于终止奇数链表的终止条件
     let node=curr.next//先把curr.next存起来
     pre.next=node//第一步
     curr.next=node.next//第二步
     node.next=curr//第三步
     //移动指针
     pre=curr
     curr=curr.next
 }
  return vhead.next
};

看完文章后的想法

文章的想法其实和第一想法是一致的,只不过代码随想录中只用到了一个指针,而我用到了两个指针,区别不大,但是也有所差异,因为翻转步骤不一样,代码随想录中的第二三步和我的二三步颠倒了,所以写代码时代码随想录中先存了三个步骤的节点,之后在翻转。其实意思是一样的,就不重复给出代码了.

思考

对于链表的题一定要把逻辑搞清楚,先干什么,后干什么都很重要。所以在做链表题时,如果不是对逻辑很清晰,可以通过画图来梳理逻辑,这样有助于帮助自己和他人理解。PS:虚拟节点真的很好用

19. 删除链表的倒数第 N 个结点

文章链接

第一想法

看到这道题我的第一想法是先算出链表有多长,然后找到删除节点的前一个节点,让next等于next.next。话不多说,上代码:

function removeNthFromEnd(head: ListNode | null, n: number): ListNode | null {
    let vhead:T=new ListNode()
    vhead.next=head
    let pre:T=head //用于计算链表的长度
    let count:number=0
    while(pre){
        count++
        pre=pre.next
    }
    pre=vhead//重新用pre指针来找要删除节点的前一个节点,这里等于vhead是因为直接找到前一个节点
    count-=n//减n是为了找到删除节点的前一个节点
    while(count--){
      pre=pre!.next//这里以及下面加了! 是为了防止编辑器提示,但是理论上来说pre不会是null的 不能用? 否则右侧的类型为ListNode|null|undefined 
    }
    pre!.next=pre!.next!.next
    return vhead.next
};

看完文章后的想法

一看代码随想录文章,就发现还是老生常谈的双指针,又是没想到,主体思路是有两个快慢指针,快指针先走n步(或N+1步,主要区别是终止条件一个是fast一个是fast.next),借用代码随想录中的图片:

image.png

image.png

image.png

image.png 代码如下(代码以提前走n步为例):

function removeNthFromEnd(head: ListNode | null, n: number): ListNode | null {
  let vhead:T=new ListNode()
  vhead.next=head
  let fast:T=vhead
  let slow:T=vhead
  while(n--){//提前走n步
      fast=fast!.next
  }
  while(fast!.next){//如果提前走n+1步 这里就应该是fast
      fast=fast!.next
      slow=slow!.next
  }
  slow!.next=slow!.next!.next
  return vhead.next
};

进阶递归写法:

function removeNthFromEnd(head: ListNode | null, n: number): ListNode | null {
    let vhead:T=new ListNode()
    vhead.next=head
    let count=0//用于计算回退到哪里了
    const foo:(node:T)=>void=(node:T)=>{
        if(node==null) return
        foo(node.next)
        count++//这里count++必须要在递归之后,递归之前没有意义
        if(count==n+1) node!.next=node!.next!.next//这里要是n+1,因为要回退到删除节点的前一个节点
    }
    foo(vhead)
    return vhead.next
};

思考

链表的题大多数都是用双指针可以解决的,看完文章后自己也用双指针的写法把题写出来了,唯一的不同就是要考虑清楚fast提前走几步和终止条件的关系。而在进阶写法中,count++一定要在递归之后,放在递归之前则count会是一个值且等于链表长度。

面试题 02.07. 链表相交

文章链接

第一想法

因为由题意可知,要判断节点是否相同,所以第一想法是模拟,第一层while来循环A,第二次while循环B,当A等于某个节点时,循环B依次比较节点是否相等,代码如下:

var getIntersectionNode = function(headA, headB) {
    let pre=headA
    let curr=headB
    while(pre){//当A等于某个节点时,循环B依次比较节点是否相等
        while(curr){
            if(pre==curr) return pre 
            curr=curr.next
        }
        pre=pre.next
        curr=headB
    }
    return null //如果没有找到相同节点就返回null
};

上方代码可以通过测试用例

看完文章后的想法

节点相等我忽略了一个重要条件,节点相同的话那么长度应该也是相同的。所以可以直接找到节点数目相同的时候再进行判断,代码如下:

var getIntersectionNode = function(headA, headB) {
    let pre=headA
    let curr=headB
    let lenA=0
    let lenB=0
    //获取链表长度
    while(pre){
        lenA++
        pre=pre.next
    }
    while(curr){
        lenB++
        curr=curr.next
    }
    //判断哪个链表长,且长的列表为pre 短的链表为curr
    let n=lenA-lenB
    pre=headA
    curr=headB
    if(lenA<lenB){
     n=lenB-lenA;
     [pre,curr]=[curr,pre]
    }
    //让长的链表的指针向后移动,知道指针指向的节点的长度和curr相同
    while(n--){
        pre=pre.next
    }
    while(pre){
        if(pre===curr) return pre
        pre=pre.next
        curr=curr.next
    }
    return null
};

思考

这道题还是挺简单的,但是做题的时候忽略了一个重要的细节,就是节点相同时长度也相同,所以我模拟的话这道题偶然通过了,链表的题多尝试用双指针解还是比较好的。

142.环形链表II

文章链接

第一想法

因为要判断是否有环,所以先把每走过的节点都标记一下,当走到有标记的节点之前,则是环的入口,如果节点走完都没有标记的节点的话,则说明无环,代码如下:

function detectCycle(head: ListNode | null): ListNode | null {
  let map=new Map() //用map来标记节点
  let pre:ListNode | null=head
  while(pre){
      if(map.has(pre)) return pre //判断是否已经走过这个节点
      map.set(pre,pre)
      pre=pre.next
  }
  return null //无环时输出
};

看完文章后的想法

只能说非常妙,考了非常多的数学知识。首先我们要确定是否有环,文章用的方法非常妙,用快指针一次走两个,慢指针一次走一个,如果存在环,则快慢指针一定会相遇(例如在操场跑步,快的人一定会把慢的人追上)如图: 代码随想录 找到环了,接下来该确定环的入口了,这里用到了大量的数学知识,如图:

image.png 慢指针走的路程为x+yx+y,快指针走的路程为x+y+n(z+y)x+y+n(z+y),相信大家对快指针走的路程没有问题,那么对于慢指针大家可能有些疑惑为什么是x+yx+y,刚开始我也有些疑惑,下面看代码随想录中的这几张图片: 当slow进入环时,fast也一定之前就进入了环,假设slow在环入口时,fast也在环入口

image.png 那么可以发现slow在环内的第一圈末尾时与fast相遇,假设slow进入环时,fast不在入口:

image.png 那么一定是fast先到环入口3,说明在此之前slow与fast以及相遇(一定会相遇,由物理知识可得,fast相对于slow一次走一个节点)。 OK,说完为什么相遇时slow走了x+yx+y,那么该说如何求解入口了,由于fast走的路程是slow的二倍:所以可得2(x+y)=x+y+n(z+y)2*(x+y)=x+y+n(z+y),化简为x+y=n(z+y)x+y=n(z+y),变形为x=(n1)(z+y)+zx=(n-1)(z+y)+z.由此可以得出,当快指针从相遇节点按一个一个节点走时,slow从head按一个一个节点走时,会在环入口相遇: 代码随想录 所以代码如下

type T=ListNode | null
function detectCycle(head: ListNode | null): ListNode | null {
    let fast:T=head
    let slow:T=head
    while(fast&&fast.next){ //求是否有环
        slow=slow!.next
        fast=fast.next.next
        if(slow==fast){ //求环入口
            slow=head
            while(slow!=fast){
                slow=slow!.next
                fast=fast!.next
            }
            return slow
        }
    }
    return null
};

思考

这道题对我来说很妙,用到了非常多的数学思想,对于双指针的用法又有了新的理解。只能说还是太年轻了.判断是否有环和环的入口的寻找看了文章好久才能梳理完,这道题得多加练习。

总结

今天四道题,花费时间4小时,前两道较为轻松,第三道虽然做出来了但是有个重要的条件没想到,第四题也做出来了,但是文章的思路确实妙,完全没想到文章的思路.