代码随想录算法训练营第三天|链表part01

114 阅读5分钟

链表

单链表:

定义链表节点:

class LinkNode {
    constructor(val, next) {
        this.val = val;
        this.next = next;
    }
}

定义链表:

var MyLinkedList = function() {
    this._size = 0;
    this._head = null;
};

双向链表:

203.移除链表元素

题目链接:leetcode.cn/problems/re…

第一想法

单向链表无法通过下标找到元素,需要从头节点开始一个一个依次访问

如果当前节点的data == val

则上一个节点的next 指向 当前节点的next

思路

这里实际上需要考虑两种情况,第一种是删除的节点是头节点,第二种是删除的节点非头节点

可以单独分开来考虑,也可以使用一个虚拟头节点,这样就可以保证所有会被删除的节点都不是头节点

最后返回的时候去掉虚拟头节点就行

这里用第二种方法比较方便,JS代码如下:

/**
 * 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} val
 * @return {ListNode}
 */
var removeElements = function(head, val) {
    let cur = new ListNode(0,head);
    let ret = cur;
    while(ret.next){
        if(ret.next.val == val){
            ret.next = ret.next.next;
            continue;
        }
        ret = ret.next;
    }
    return cur.next;
};

这里要注意continue,即如果该节点需要删除,则删除该节点后需要跳出循环,不能执行ret = ret.next这一步

这道题给的输入输出很适合用第二种方法

总结

这一题一开始有点不理解,算法和输入输出的关系,也就是对于JS中的链表有点没搞清楚

然后发现JS中,链表是用对象实现的,即链表的一般形式:

const list = {
    head: {
        value: 6
        next: {
            value: 10                                             
            next: {
                value: 12
                next: {
                    value: 3
                    next: null    
                    }
                }
            }
        }
    }
};

707.设计链表

题目链接:leetcode.cn/problems/de…

第一想法

JS实现链表不是很熟,第一想法还是懵逼

先看了一下题解

这一题写了整整4个小时,好难

卡哥的答案的JS部分看的好难受,最后干脆自己写了

思路

一开始自己直接上手写,写了写去全是错误,有太多的边界情况需要考虑,写起来越写越乱

看了卡哥的视频后发现,使用虚拟头结点可以使问题变得简单一些

这样就不需要把对头节点的操作单独考虑,也不需要单独考虑index = 0或者链表为空这些特殊情况

  • 初始化链表和节点

    ​
    var MyLinkedList = function() {
        this._size = 0;
        this._head = null;
    ​
    };
    ​
    function ListNode(val, next) {
        this.val = val;
        this.next = next;
    };
    
  • 获取index对应元素的值

    MyLinkedList.prototype.get = function(index) {
        // 注意index的有效范围
        if(index < 0 || index >= this._size){
            return -1;
        }
        // 这里建立一个虚拟头结点
        let dummyhead = new ListNode(0,this._head);
        let cur = dummyhead.next;
        while(index-- > 0){
            cur = cur.next;
        }
        return cur.val;
    };
    
  • 头部插入节点

    这里必须注意插入节点的顺序

    应该建立新节点和后一个节点的连接,再建立新节点和前一个节点的连接

    MyLinkedList.prototype.addAtHead = function(val) {
        
        const node = new ListNode(val, this._head);
        this._head = node;
        this._size++;
    };
  • 尾部插入节点

    当前的指针必须指向链表中最后一个元素

    创建虚拟头指针进行移动,移动到链表的最后一个元素上

    MyLinkedList.prototype.addAtTail = function(val) {
        let cur = new ListNode(0, this._head);
        const node = new ListNode(val,null)
        this._size++;
        if(this._head){
            while(cur.next !== null){
                cur = cur.next;
        }
            // 注意这里,如果cur.next 是null,那么无法赋值
            cur.next = node;
            return;
        }
        this._head = node;
    };
    

    这里有一个大坑,也是让我反复写不出来的原因

    如果此时链表是空的,cur.next 就是 null

    在JS语法中,null是一个独立的类型,这里给它赋值一个对象会出问题

    所以必须把链表是空的这种情况单独拿出来分析

  • 第n个节点前插入节点

    将指针移动到第n个节点的上一个节点

    注意边界上的考虑,一般考虑边界时可以举一个极端的例子

    这里的边界情况,实际上如果index = 链表长度时,并不需要单独考虑

    MyLinkedList.prototype.addAtIndex = function(index, val) {
        
        if(index > this._size) return;
        if(index <= 0) {
            this.addAtHead(val);
            return;
        }
        if(index === this._size) {
            this.addAtTail(val);
            return;
            }
    ​
        let cur = new ListNode(0, this._head);
        const node = new ListNode(val);
        while(index-- > 0){
            cur = cur.next;
        }
        node.next = cur.next;
        cur.next = node;
        this._size++;
    };
    

    这里和上面同理

    对于cur.next = null 的情况,都单独写出来讨论

  • 删除节点

    这里第n个节点必须是cur.next,即此时指针指向的应该是待删除节点的前一个节点

    MyLinkedList.prototype.deleteAtIndex = function(index) {
        if(index < 0 || index >= this._size){
            return;
        }
        let cur = new ListNode(0, this._head);
            if(index == 0){
            this._head = this._head.next;
            this._size--;
            return;
        }
            while(index-- > 0){
            cur = cur.next;
            }
            cur.next = cur.next.next;
            this._size--;
       
     };
    

    这里如果index = 0,则cur的改变对原链表并没有影响

    可是如果index 不为0,则cur发生移动,改变cur的指向对原数组又有影响了

    因此需要把index为0的情况单独讨论

总结

哪怕对着卡哥的视频一步步写,还是执行出问题,写了整整两天

自己又不会用vscode调试,又卡住了

这题卡的实在是太难受了,又不会debug,代码看着完全没有问题,就是跑不成功

最后发现问题,貌似是JS中无法把一个对象赋值给null,导致之前的分类完全错误,还是必须去分情况考虑

这道题需要好好研究研究

206.反转链表

题目链接:leetcode.cn/problems/re…

第一想法

定义一个新链表,新链表的next = 旧链表的prev(用双向链表了)

思路

1.双指针解法

image-20230318215615845

这里使用两个指针,cur指向头节点,pre指向其上一个节点

令cur.next = pre即可

为了使cur和pre可以方便移动,这里引入一个第三方变量temp

代码如下:

var reverseList = function(head) {
    let cur = head;
    let pre = null;
    let temp = null;
    while(cur != null){
        // 注意这里的相互赋值
        temp = cur.next;
        cur.next = pre;
        pre = cur;
        cur = temp;
    }
    return pre;
    
};

循环的结束条件是cur指向null,即此时pre是头结点

将pre返回即可

2.递归解法

递归写法是基于双指针写法的原理的

代码如下:

var reverseList = function(head) {
   return reverse(head,null);
    
};
var reverse = function(cur,pre){
    if(cur == null){
        return pre;
    }
    let temp = cur.next;
    cur.next = pre;
    return reverse(temp,cur);
​
}

总结

这部分不难,主要是这个双指针的思想需要记住

还有就是这个赋值的操作,搞清楚赋值的先后顺序,这个比较容易搞混

递归感觉自己还不是特别熟,需要再多练练