代码随想录算法训练营第三天 | 203. 移除链表元素、707. 设计链表、206. 反转链表

71 阅读4分钟

理论基础

文章推荐

定义链表

class ListNode{
    val:number
    next:null|ListNode
    constructor(val:number=0,next?:null|ListNode){
      this.val=val
      this.next=(next==undefined)?null:next
    }
}

203. 移除链表元素

文章链接

第一想法

看到这道题,我首先的想法就是把node==val的前一个指针指向node.next,原理图在代码随想录中可以找到,如下:

image.png 所以代码简单如下:

var removeElements = function(head, val) {
    let vhead=new ListNode(0) //设置虚拟头节点
    vhead.next=head
    let pre=vhead//pre是指针
    while(pre&&pre.next){
        if(pre.next.val==val){//判断当前节点的下一个节点的val是否等于val
            pre.next=pre.next.next //等于val就把pre的next指针指向pre.next.next
            continue//这里continue是进入下一个循环重新判断当前的pre.next
        } 
        pre=pre.next
    }
    return vhead.next
};
function removeElements(head: ListNode | null, val: number): ListNode | null {
    let vnode:ListNode=new ListNode()
    vnode.next=head
    let pre:ListNode=vnode
    while(pre&&pre.next){
        if(pre.next.val==val){
            pre.next=pre.next.next
            continue
        }
        pre=pre.next
    }
    return vnode.next
};

看完文章后的想法

阅读完文章后发现有两种思路,一种是直接在当前链表删除,另一种就是和我的第一想法一致,设置虚拟节点进行删除。下面代码为直接在链表删除:

function removeElements(head: ListNode | null, val: number): ListNode | null {
     while(head&&head.val===val) head=head.next //用于判断头节点是否等于val
     if(head===null)return head
     let pre:ListNode=head //指针
     let curr:ListNode|null=head.next//curr用于判断当前val是否等于val,这里的head.next类型是ListNode|null,所以curr的类型为ListNode|null,不加null这编译器中会报错
     while(curr){
         if(curr.val==val){
             pre.next=curr.next
             curr=pre.next
         }else{
             pre=curr
             curr=pre.next
         }
     }
     return head
};

思考

删除链表元素有两种方法,一种是直接删除链表,另一种是设置虚拟节点删除链表,两种方法都可以解决当前问题,但是个人习惯以及推荐使用第二种方法也就是设置虚拟头节点,这样写法的优势是代码格式统一,利于理解。

707. 设计链表

文章链接

第一想法

设计链表主要是根据链表的特性来设计,通过题目要求,需要设计5个方法,因为有很多的边界判断,所以设置count来控制边界,以及需要添加头结点,所以设置一个虚拟头结点最好,代码如下:

class MyLinkedList {
    vhead:ListNode
    count:number//设置count是为了方便边界判断
    constructor(head:ListNode) {
     this.vhead=new ListNode() //设置虚拟头结点
     this.vhead.next=head
     this.count=0
    }

    get(index: number): number {
        if(index+1>this.count) return -1 //边界判断 索引无效
      let pre:ListNode=this.vhead.next as ListNode //这里需要断言判断,因为next的类型为null|ListNode
      while(index--){
          pre=pre.next as ListNode //断言判断
      }
      return pre.val
    }

    addAtHead(val: number): void {
        let node:ListNode=new ListNode(val)
        node.next=this.vhead.next
        this.vhead.next=node
        this.count++
    }

    addAtTail(val: number): void {
        let node:ListNode=new ListNode(val)
        let pre:ListNode=this.vhead
        while(pre.next){
            pre=pre.next
        }
        pre.next=node
        this.count++
    }

    addAtIndex(index: number, val: number): void {
         let node:ListNode=new ListNode(val)
        if(index<=0){
          node.next=this.vhead.next
          this.vhead.next=node
          this.count++
        }else{
             let pre:ListNode|null=this.vhead
            while(index--){
                pre=pre.next 
                if(pre==null) return //如果pre==null说明index超过了链表的长度,所以不应该添加节点
            }
            node.next=pre.next
            pre.next=node
            this.count++
        }
    }

    deleteAtIndex(index: number): void {
        console.log(index)
        if(index<0||index+1>this.count) return //边界处理
        let pre:ListNode=this.vhead
        while(index--){
            pre=pre.next as ListNode
        }
        pre.next=(pre.next as ListNode).next
        this.count--
    }
}

看完文章后的想法

文章的总体思路和我的一致,但是有些细节比较好,文章和我一样都设置了头结点和size,但是也设置了尾节点tail,这样有利于直接找到头部节点和尾部节点。后面只展示其中一个我没想到的点,就是封装找Node节点的私有方法:

    //获取指定节点
    private getNode(index: number): ListNode {
        let curNode: ListNode = this.vhead;
        for (let i = 0; i <= index; i++) {
            // 理论上不会出现 null
            curNode = curNode.next!;//末尾表示curNode.next不会是null和undefined
        }
        return curNode;
    }

思考

这是一道锻炼链表非常不错的题,整体上个人是能够写出来的,但是也存在一些问题,一些边界处理没有处理好,导致力扣运行时出现了几次错误,总体上不难,但是个人还是缺少一些细节处理能力。

206. 反转链表

文章链接

第一想法

看到这道题目时,我的第一想法是先把链表中每个元素都拿出来放到一个数组中,然后从后向前循环这个数组,创建ListNode,拼接成一个新的翻转后的链表,代码如下:

function reverseList(head: ListNode | null): ListNode | null {
   let arr:number[]=[]
   let pre:ListNode|null=head
   while(pre){
       arr.push(pre.val)
       pre=pre.next
   }
   let vhead=new ListNode()
   let curr:ListNode=vhead
   for(let i=arr.length-1;i>=0;i--){
     let node=new ListNode(arr[i])
     curr.next=node
     curr=curr.next
   }
   return vhead.next
};

看完文章后的想法

emmm,上来卡哥就拒绝了我的这个想法,还是用老方法:双指针,这里也可以用递归写法,没想到(流汗),只需要把每个ListNode的指针改变就可以了,我第一想法就是有空间的浪费,性能不好,不值得推荐。

双指针法(图片来源于代码随想录): 代码随想录

type T=ListNode|null
function reverseList(head: ListNode | null): ListNode | null {
   let pre:T=null
   let curr:T=head
   while(curr){
       let node=curr.next
       curr.next=pre
       pre=curr//必须先移动pre
       curr=node 
   }
   return  pre
};

注意要一定先移动pre再移动curr

递归写法:

type T=ListNode|null
function reverseList(head: ListNode | null): ListNode | null {
    const foo=(pre:T,curr:T)=>{
       if(!curr) return pre
       let node=curr.next
       curr.next=pre
       return foo(curr,node)
    }    
   return foo(null,head)
};

思考

翻转链表写出来了但是思路不对,看了文章后有了新的方法,双指针法没想到,尝试了一下发现谁先移动要搞清楚,一定要先移动pre再移动curr。这是一个易错点。递归的方法一定要搞清逻辑是什么,才能把递归写好,要想清每一层递归完成什么事情,输入输出是什么,才能把递归写好。

今日总结

今天是链表的第一天,总共三道题,虽然学过链表但是对于链表的操作还是用的很少的,今天算是锻炼了自己的能力,双指针法没有想到很可惜.对于链表的题一定要把逻辑搞清,先后顺序搞明白才能写对代码。今日用时3小时。