前端算法之路-链表实战篇之leecode奥力给

703 阅读10分钟

本文已参加「新人创作礼」活动,一起开启掘金创作之路。

进入正文

上一遍文章一节介绍了链表的基础知识,不了解的可以看上一篇文章。俗话说光说不练假把式,这一篇我们与实际相结合,来个leecode链表题实战

141. 环形链表

首先讲一下环形链表

如何判断是否是循环链表:首先设置一个快指针(走两步),一个慢指针(走一步),两个这个指针相遇时,这个链表就是环形链表,否则就不是

如下图

循环链表.gif

代码

/**
 * Definition for singly-linked list.
 * function ListNode(val) {
 *     this.val = val;
 *     this.next = null;
 * }
 */

/**
 * @param {ListNode} head
 * @return {boolean}
 */
var hasCycle = function(head) {
    //首先判断传入是否有值,如果没有值,直接返回false
    if(!head) return false
    //设置一个快指针,一个慢指针,如果两个指针有相等的时候,那么这个链表必有环
    let slow=head,fast=head;
    while(fast.next&&fast.next.next){
        //慢指针走一步
        slow=slow.next;
        //快指针走两步
        fast=fast.next.next;
        //判断两个指针是否相等
        if(slow===fast){
            return true
        }
    }
    return false
};

142. 环形链表 II

此题需要返回链表开始入环的第一个节点,那如何获取入环的第一个节点呢?就是在快慢指针相等的情况下,头指针和慢指针一起移动,两个指针相遇的节点,就是入环的第一个节点

循环链表2.gif

/**
 * Definition for singly-linked list.
 * function ListNode(val) {
 *     this.val = val;
 *     this.next = null;
 * }
 */

/**
 * @param {ListNode} head
 * @return {ListNode}
 */
var detectCycle = function(head) {
    if(!head) return null;
    let slow=head,fast=head;
    while(fast.next&&fast.next.next){
        slow=slow.next;
        fast=fast.next.next;
        if(fast===slow){
            //当快慢指针相等的时候,头节点到入环节点的距离和慢指针到入环节点的距离相等
            let cur=head;
            while(slow!==cur){
                slow=slow.next;
                cur=cur.next
            }
            return cur
        }
    }
    return null
};

206. 反转链表

这题求的是反转之后的链表,比较简单,就是相当于修改每个节点指针,让它指向上一个。

反转链表.gif

/**
 * Definition for singly-linked list.
 * function ListNode(val, next) {
 *     this.val = (val===undefined ? 0 : val)
 *     this.next = (next===undefined ? null : next)
 * }
 */
/**
 * @param {ListNode} head
 * @return {ListNode}
 */
var reverseList = function(head) {
    if(!head) return null;
    //创建个虚拟指针,一个指向头节点的指针
    let pre=null,cur=head;
    while(cur){
        //下面代码拆分
        // let next=cur.next; 将cur的指向赋值,防止之后数据丢失
        // cur.next=pre; 将cur的指向改为上一个节点,反转脸变
        // pre=cur; 将上一个节点向后走一步
        // cur=next; 将当前节点向后走一步,然后继续反转链表
        [cur.next,pre,cur]=[pre,cur,cur.next]
    }
    return pre
};

92. 反转链表 II

这个题和上一个一样,只不过多了一个区间值,所以我们需要先找到这个区间值,再做上面反转的操作

/**
 * Definition for singly-linked list.
 * function ListNode(val, next) {
 *     this.val = (val===undefined ? 0 : val)
 *     this.next = (next===undefined ? null : next)
 * }
 */
/**
 * @param {ListNode} head
 * @param {number} left
 * @param {number} right
 * @return {ListNode}
 */
var reverseBetween = function(head, left, right) {
    if(!head) return null;
    //创建两个虚拟指针,以及要反转的节点个数
    let ret=new ListNode(-1,head),temp=ret,count=right-left+1;
    //将指针移动到第一个需要反转前一个的位置
    while(--left){
        ret=ret.next
    }
    //传入反转的节点
    ret.next=reverse(ret.next,count);
    return temp.next
};

function reverse(head,count){
    let pre=null,cur=head;
    //在左右区间反转
    while(count--){
        [cur.next,pre,cur]=[pre,cur,cur.next]
    }
    //让需要反转的最开始第一个节点(反转后是最后一个节点),和剩下的数据链接上 head是2,cur是5  2->5
    head.next=cur;
    return pre
}

25. K 个一组翻转链表

这题的意思也是反转链表,但是会给你个规定的值k,比如k等于2,就是两两反转,等于3就是三个反转

所以步骤是

1、先循环,在k个值内反转链表

  • 如果长度不够k个,那么直接返回
  • 如果够k个,就进行反转 2、然后循环移动k个,下一组k反转
/**
 * Definition for singly-linked list.
 * function ListNode(val, next) {
 *     this.val = (val===undefined ? 0 : val)
 *     this.next = (next===undefined ? null : next)
 * }
 */
/**
 * @param {ListNode} head
 * @param {number} k
 * @return {ListNode}
 */
var reverseKGroup = function(head, k) {
    if(!head) return null;
    //创建两个虚拟节点
    let ret=new ListNode(-1,head),temp=ret;
    while(ret){
        //传节点和k进行反转
        ret.next=reverse(ret.next,k);
        //因为k个节点已经处理过;所以ret往后移动k个
        for(let i =0;i<k&&ret;i++){
            ret=ret.next
        }
        //如果ret为空 中断循环
        if(!ret) break
    }
    return temp.next
};

function reverse(head,k){
    let pre=head,cur=head,con=k;
    //先循环k次看节点够不够k个
    while(--k&&pre){
        pre=pre.next
    }
    //如果pre为空,说明节点不够k个,直接返回
    if(!pre) return head;
    //反转k个节点
    while(con--){
        [cur.next,pre,cur]=[pre,cur,cur.next]
    }
    head.next=cur;
    return pre
}

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

上一章说过,删除节点就是将需要删除节点的前一歌的指针,指向删除节点后一个,此题要求删除倒数n个节点,所以我们需要先设置一个节点,将它移动到删除位置之前

所以步骤是

  1. 先创建一个节点a,让它移动n步;
  2. 然后创建一个虚拟头节点b,指针指向头节点(将b设置为虚拟头节点是防止删除的节点时头节点)
  3. a和b同时移动,当a为null时,说明b节点在删除节点的前一位
  4. 修改b节点的指针指向

删除第k个节点.gif

/**
 * Definition for singly-linked list.
 * function ListNode(val, next) {
 *     this.val = (val===undefined ? 0 : val)
 *     this.next = (next===undefined ? null : next)
 * }
 */
/**
 * @param {ListNode} head
 * @param {number} n
 * @return {ListNode}
 */
var removeNthFromEnd = function(head, n) {
    if(!head) return null;
    //创建一个虚拟头节点
    let ret=new ListNode(-1,head),pre=ret,cur=head;
    //复制的头节点往后移动n步->3
    while(n--){
        cur=cur.next
    }
    //当cur不为空时,虚拟头节点和头节点一起向后移动,当cur为空,说明虚拟头节点在删除节点的前一个
    while(cur){
        pre=pre.next;
        cur=cur.next
    }
    //将虚拟节点的指针指向虚拟节点的下下个
    pre.next=pre.next.next;
    return ret.next
};

83. 删除排序链表中的重复元素

因为链表是升序的,所以只需要循环两个节点互相比较值

步骤:

  1. 设置两个节点,一个在头节点a,另一个在头节点下一个的位置b
  2. 因为b节点在后,所以退出循环只需要b为空
  3. 判断两个节点值是否相等
  4. 当有重复元素时,
    • a节点的next指向b节点的next
    • 然后a,b节点向后移动,进入下一次循环
  5. 当没有重复元素时,a和b向后移动,进入下一次循环
/**
 * Definition for singly-linked list.
 * function ListNode(val, next) {
 *     this.val = (val===undefined ? 0 : val)
 *     this.next = (next===undefined ? null : next)
 * }
 */
/**
 * @param {ListNode} head
 * @return {ListNode}
 */
var deleteDuplicates = function(head) {
    if(!head) return null;
    //设置两个节点方便比较
    let slow=head,fast=head.next;
    //当后一个节点不为空,一直循环判断是否有重复元素
    while(fast){
        //当有重复元素的时候
        if(slow.val===fast.val){
            //慢节点的下一个指向快节点的下一个,删除快节点
            slow.next=fast.next;
        }else{
            //当无重复元素,慢节点向后移动继续比较
            slow=slow.next;
        }
        //快节点无论是否有重复元素都需要向后移动
        //1、有重复元素时,删除了重复元素所以快节点需要向后移动,与慢节点再比较
        //2、当没有重复元素,快慢节点后会向后移动进行新比较
        fast=fast.next
    }
    return head
};

82. 删除排序链表中的重复元素 II

这题要求删除所有的重复元素,所以还是需要两个节点比较

  1. 创建两个节点,一个虚拟头节点a,一个头节点b,进入循环,b为空,退出循环
  2. 如果a和b的下一个节点的值不相等,那么同时向后移动(比较下一个节点是因为防止节点a移动到重复元素)
  3. 如果相等,那么循环b节点向后移动,直到两个节点下一个值不想等,退出当前循环
  4. 修改a节点的指针指向b节点的下一个
  5. b节点向后移动进入新一轮循环

删除重复链表2.gif

/**
 * Definition for singly-linked list.
 * function ListNode(val, next) {
 *     this.val = (val===undefined ? 0 : val)
 *     this.next = (next===undefined ? null : next)
 * }
 */
/**
 * @param {ListNode} head
 * @return {ListNode}
 */
var deleteDuplicates = function(head) {
    if(!head) return null;
    //创建虚拟头节点
    let ret=new ListNode(-1,head),pre=ret,cur=head;
    //如果真实节点为空,说明链表走到了末尾
    while(cur&&cur.next){
        //如果虚拟节点的下一个和真实节点的下一个值不相等,两个节点一起向后移动
        if(pre.next.val!==cur.next.val){
            cur=cur.next;
            pre=pre.next
        }else{
            //如果相等,让cur向后移动,直到两个值不相等退出循环
            while(cur&&cur.next&&pre.next.val===cur.next.val){
                cur=cur.next
            }
            //pre节点的指针指向cur节点的指针指向,删除重复的节点
            pre.next=cur.next;
            //cur向后移动进行下一轮比较
            cur=cur.next
        }
    }
    return ret.next
};

86. 分隔链表

给定一个特定值x,比它小的在左边,大的放右边,且不能修改节点位置。

  1. 所以我们可以新建两个空链表,一个放小的,一个放大的
  2. 然后将两个链表拼接,就得到了分隔的链表
/**
 * Definition for singly-linked list.
 * function ListNode(val, next) {
 *     this.val = (val===undefined ? 0 : val)
 *     this.next = (next===undefined ? null : next)
 * }
 */
/**
 * @param {ListNode} head
 * @param {number} x
 * @return {ListNode}
 */
var partition = function(head, x) {
    if(!head) return null;
    //设置两个链表,一个存放比x小的,剩余存放到另一个
    let smallList=new ListNode(),bigList=new ListNode(),cur=head;
    //两个俩表分别设置两个指针
    let smallNode=smallList,bigNode=bigList;
    while(cur){
        //如果当前的值大于等于x;将当前节点放进大的链表里,并且将小链表的next为null,因为赋值后的next会带着原来指向
        if(cur.val>=x){
            bigNode.next=cur;
            bigNode=bigNode.next
            smallNode.next=null
        }else{
        //如果当前的值大于等于x;将当前节点放进小的链表里
            smallNode.next=cur;
            smallNode=smallNode.next
            bigNode.next=null
        }
        cur=cur.next
    }
    //两个链表拼接
    smallNode.next=bigList.next
    return smallList.next
};

138. 复制带随机指针的链表

这题的难点在于随机指针

  1. 我们可以在每个节点后面放一个复制的节点
  2. 修改原来节点和复制节点的指针和随机指针
  3. 然后将两个链表拆分

此题的图比较难画,所以代码有详细注释,如果还是不太明白的我之后再出图吧😄

/**
 * // Definition for a Node.
 * function Node(val, next, random) {
 *    this.val = val;
 *    this.next = next;
 *    this.random = random;
 * };
 */

/**
 * @param {Node} head
 * @return {Node}
 */
var copyRandomList = function(head) {
    if(!head) return null;
    //创建一个节点
    let real=head,clone;
    while(real){
        //创建一个克隆节点,放在当前节点的后面
        clone=new ListNode(real.val);
        clone.next=real.next;
        //复制当前节点的随机指针
        clone.random=real.random;
        //修改当前节点指向,修改完的head 应该是 原来的节点->复制的节点->原来的节点->复制的...->null
        real.next=clone;
        //当前节点节点向后移动进入下一次复制
        real=clone.next
    }
    //将clone重新复制头节点的下一个。也就是复制的第一个节点
    clone=head.next;
    while(clone){
        //修改复制节点的random指针,目前是指向原有节点,设置后指向复制节点(复制节点在原有节点后面)
        clone.random&&(clone.random=clone.random.next);
        //隔一位移动,修改的都是克隆的节点
        (clone=clone.next)&&(clone=clone.next)
    }
    real=clone=head.next;
    while(clone.next){
        //修正真实节点和克隆节点,拆分两个链表
        head.next=head.next.next;
        clone.next=clone.next.next;
        //头节点和克隆节点向后移动, 继续拆分
        head=head.next;
        clone=clone.next
    }
    //此时后一个真实节点指针还指向克隆节点,需要断开
    head.next=null
    return real
};

707. 设计链表

恭喜你看到这里🎉,终于到了链表的最后一题,设计链表。来解释一下题目要求的方法

MyLinkedList linkedList = new MyLinkedList(); //定义链表
linkedList.addAtHead(1); //在链表头添加值
linkedList.addAtTail(3); //在链表尾添加值
linkedList.addAtIndex(1,2);   //链表变为1-> 2-> 3  在下标位置插入节点
linkedList.get(1);            //返回2  获取传入位置的节点值
linkedList.deleteAtIndex(1);  //现在链表是1-> 3 删除下标位置的节点值
linkedList.get(1);            //返回3 获取传入位置的节点值

经过上面题的训练,相信你对链表已经运用的炉火纯青啦,此题应该难不倒你,下面代码有详细注释,如果还是有疑问的地方评论区留言哦😯

function listNode(val,next){
    this.val=val;  //数据域
    this.next=!next?null:next //指针域
}

var MyLinkedList = function() {
    this.head=null; //头节点
    this.tail=null; //尾节点
    this.size=0 //链表长度
};

MyLinkedList.prototype.getNode=function(index){
    //判断index是否超出边界
    if(index<0||index>=this.size) return -1;
    let current=this.head;
    while(index--){
        current=current.next
    }
    return current
}
/** 
 * @param {number} index
 * @return {number}
 */
MyLinkedList.prototype.get = function(index) {
    //判断index是否超出边界
    if(index<0||index>=this.size) return -1;
    return this.getNode(index).val;
};

/** 
 * @param {number} val
 * @return {void}
 */
MyLinkedList.prototype.addAtHead = function(val) {
    //建立一个节点并且next指向head
    let node=new listNode(val,this.head);
    this.head=node;
    //判断最开始尾节点为空的情况,说明此链表只有一个节点,所以尾节点和头节点相同
    if(!this.tail){
        this.tail=this.head
    }
    this.size++
};

/** 
 * @param {number} val
 * @return {void}
 */
MyLinkedList.prototype.addAtTail = function(val) {
    let node=new listNode(val);
    this.size++
    //如果尾节点存在,那么先将添加的节点关联到尾节点,然后尾节点更新
    if(this.tail){
        this.tail.next=node; //添加节点关联尾节点
        this.tail=this.tail.next; //更新尾节点
        return
    }
    //尾节点不存在,说明头尾节点都为空
    this.head=node;
    this.tail=node
};

/** 
 * @param {number} index 
 * @param {number} val
 * @return {void}
 */
MyLinkedList.prototype.addAtIndex = function(index, val) {
    //判断边界
    if(index>this.size) return;
    //如果index<=0 相当于直接在头添加
    if(index<=0){
        this.addAtHead(val);
        return
    }
     //相当于直接在尾添加
    if(index===this.size){
        this.addAtTail(val)
        return
    }
    //获取添加位置的上一个节点
    let prev=this.getNode(index-1);
    let node=new listNode(val);
    //将要添加的节点指向上一个节点的指针指向
    node.next=prev.next;
    //将上一个节点的指针指向要添加的节点
    prev.next=node
    this.size++
};

/** 
 * @param {number} index
 * @return {void}
 */
MyLinkedList.prototype.deleteAtIndex = function(index) {
    //判断边界
    if(index<0||index>=this.size) return;
    //在头删除
    if(index===0){
        this.head=this.head.next;
        //该链表只有一个节点,头尾相等
        if(index===this.size-1){
            this.tail=this.head;
        }
        this.size--
        return
    }
    //获取删除位置的前一个节点
    let prev=this.getNode(index-1);
    //删除节点
    prev.next=prev.next.next;
    //判断删除尾节点的情况,需要更新尾节点
    if(index===this.size-1){
        this.tail=prev
    }
    this.size--
};

/**
 * Your MyLinkedList object will be instantiated and called as such:
 * var obj = new MyLinkedList()
 * var param_1 = obj.get(index)
 * obj.addAtHead(val)
 * obj.addAtTail(val)
 * obj.addAtIndex(index,val)
 * obj.deleteAtIndex(index)
 */

链表的基础:前端算法之路-链表