【代码随想录 | day03】(JavaScript)链表理论基础&203.移除链表元素、707.设计链表、206.反转链表

  • 链表理论基础
  • 203.移除链表元素、707.设计链表、206.反转链表

建议: 本题最关键是要理解 虚拟头结点的使用技巧,这个对链表题目很重要。

链表基本知识

什么是链表?

  • 链表是一种通过指针串联在一起的线性结构,每一个节点由两部分组成,一个是数据域一个是指针域(存放指向下一个节点的指针),最后一个节点指针域指向null空指针的意思)。
  • 链接的入口节点称为链表的头结点也就是head

链表特点?

  1. NodeList是一中类数组对象,用于保存一组有序的节点
  2. 可以通过方括号来访问NodeList的值,他有item()方法与length属性。
  3. 他并不是Array的实列,没有数组对象的方法。

为什么会用到链表?

数组不总是最佳的数据结构,因为,在很多编程语言中,数组的长度都是固定的,如果数组已被数据填满,再要加入新的元素是非常困难的。而且,对于数组的删除和添加操作,通常需要将数组中的其他元素向前或者向后平移,这些操作也是十分繁琐的。

然而,JS中数组却不存在上述问题,主要是因为他们被实现了成了对象,但是与其他语言相比(比如C或Java),那么它的效率会低很多。


203.移除链表元素

题目链接:https://leetcode.cn/problems/remove-linked-list-elements/submissions/

思路:

  • 首先要判断头结点是不是空的,如果是空的,编译就会报错。同时头结点指向的下一个节点也不能为空。
    while(head != null)
  • ⭐️想要删除target,那么就要对它的上一个节点(current)做一些操作
  • 原链表中,current的next➡️target,但是现在要删除target,那么只需要将current.next ➡️target.next 。
    就相当于:current.next➡️current.next.next

如果想要删除的节点刚好是头结点,头结点找不到其上一个节点进行操作,应该怎么办?

需要单独写一段逻辑来处理移除头结点的情况。这里就涉及如下链表操作的两种方式:

原链表操作

直接使用原来的链表来进行删除操作。

  • 使用原来的链表来进行移除。
  • 其实只要将头结点向后移动一位就可以,这样就从链表中移除了一个头结点。
  • 要记得将原来的头结点从内存中删除。
虚拟头结点

设置一个虚拟头结点在进行删除操作。

JavaScript中可以使用:const ret = new ListNode(0, head);

设置一个虚拟头结点,这样原链表的所有节点就都可以按照统一的方式进行移除了。

最后呢在题目中,return 头结点的时候,别忘了 return dummyNode->next;, 这才是新的头结点

// 设置一个虚拟头结点来解决
var removeElements = function(head, val) {
    const ret = new ListNode(0, head); // 虚拟头结点
    let current = ret;  
    while(current.next) { // 当下一个节点存在的时候
        if(current.next.val == val) {
            current.next = current.next.next;
            continue; 
        }
        current = current.next;
    }
    return ret.next;
    // return current.next
};

问:当链表为[7, 7, 7, 7],val=7时,该怎么考虑?

答:(当程序运行到continue; 语句时,会终止当前的这一次循环,进入到下一次循环中。)注意代码中第八行中的continue; 就是以防cur.next.next.val的值仍然为val的情况,所以还需要检查一遍。

有个问题昂,为什么这里一定要返回 ret.next 呢?
在这里插入图片描述

答:ret是创建出来的虚拟头结点,直接返回 ret.next 就能打印整条链表。但是现在需要对链表进行操作,所以必须设置一个current,用current = ret,从头开始对一个个节点进行检查和操作。

创建虚拟头结点的目的是为了方便对头结点进行操作 不用虚拟头结点也可以 但是就需要对头结点进行特殊处理了


707.设计链表

题目链接:https://leetcode.cn/problems/design-linked-list/solution/

操作链表注意点:

在遍历链表的时候,要定义一个指针(定义临时指针 cur = dummyHead)来遍历,而不是直接操作。因为操作完链表之后,要返回头结点。如果上来就操作头结点,那么头结点的值都改变了。

看到题目时,首先让我懵的就是这一块,属实是基础不够扎实了。

使用函数表达式创建了函数,类名建议首字母大写,结合后面的var obj = new MyLinkedList(),知道了MyLinkedList是一个类,obj就是这个类的实例。

在这里插入图片描述

补充知识点:

  • 首先JS连class关键字都没有,怎么办呢?用函数代替,JS中最不缺的就是函数,函数不仅能够执行普通功能,还能当class使用。
  • 当做类用的函数本身也是一个函数,而且他就是默认的构造函数。
  • constructor() 方法是类的构造函数(默认方法),用于传递参数,返回实例对象,通过 new 命令生成对象实例时,自动调用该方法。如果没有显示定义, 类内部会自动给我们创建一个constructor()

但是这个类没有构造函数,而且看题目,本身也没有用作构造函数。

做题思路

我们设计链表包含两个类,一个是 LinkNode 类用来表示节点,另一个事 MyLinkedList 类提供插入节点、删除节点等一些操作。

在链表类中实现这些功能:

  1. get(index):获取链表中第 index 个节点的值。如果索引无效,则返回-1。
  2. addAtHead(val):在链表的第一个元素之前添加一个值为 val 的节点。插入后,新节点将成为链表的第一个节点。
  3. addAtTail(val):将值为 val 的节点追加到链表的最后一个元素。
  4. addAtIndex(index,val):在链表中的第 index 个节点之前添加值为 val 的节点。如果 index 等于链表的长度,则该节点将附加到链表的末尾。如果 index 大于链表长度,则不会插入节点。如果index小于0,则在头部插入节点。
  5. deleteAtIndex(index):如果索引 index 有效,则删除链表中的第 index 个节点

控制台中的输入输出是什么意思

在这里插入图片描述

输入["MyLinkedList","addAtHead","addAtTail","addAtIndex","get","deleteAtIndex","get"] [[],[1],[3],[1,2],[1],[1],[1]]

输出
[null,null,null,null,2,null,3]

由于get函数才会返回值,所以输出中,这个数组中的2和3是返回的链表节点的值。其他的null都是依次对应了"MyLinkedList",“addAtHead”,“addAtTail”,“addAtIndex”,"deleteAtIndex"这些方法的操作,且没有值返回。

Carl老师的解法解读:

原文链接:代码随想录 (programmercarl.com)

创建实例对象linkedList时,需要知道链表长度,有没有首尾。所以,MyLinkedList 类中要有这三个属性。

getNode方法中,首先考虑索引的有效性。之后,需要创建虚拟头结点

  • 注意:index >= this._size
  • 注意:index-- >= 0
class LinkNode {
    constructor(val, next) {
        this.val = val;
        this.next = next;
    }
}

 // 单链表 储存头尾节点 和 节点数量
var MyLinkedList = function() {
    this._size = 0;
    this._tail = null;
    this._head = null;
};

MyLinkedList.prototype.getNode = function(index) {
    if(index < 0 || index >= this._size) return null;
    // 创建虚拟头节点
    let cur = new LinkNode(0, this._head);
    // 0 -> head
    while(index-- >= 0) {  
        cur = cur.next;
    }
    return cur;
};
get

用到了上面定义的getNode方法,这个方法可以获取到索引为index的节点。

  • 注意:返回的是this.getNode(index).val,而不是getNode(index).val
MyLinkedList.prototype.get = function(index) {
    if(index < 0 || index >= this._size) return -1;
    // 获取当前节点
    return this.getNode(index).val;
};
addAtHead和 addAtTail

在插入头结点时,就显示了使用虚拟头结点带来的便利了。因为在头部有一个虚拟节点,只需要在这两个节点之前插入一个节点,就能完成 addAtHead 的操作。

在定义新的node时,默认node下一个指向是null

**注意点:**首先需要将新的节点指向头结点,然后再让虚拟节点指向新的节点

MyLinkedList.prototype.addAtHead = function(val) {
    // 创建新节点,值为val,指针指向了头部
    const node = new LinkNode(val, this._head); 
    // this._head指针始终要指向头结点  所以头结点变更后它也需要变更指向的位置
    this._head = node;
    this._size++; // 链表长度发生变化
    if(!this._tail) {
        this._tail = node;
    }
};

MyLinkedList.prototype.addAtTail = function(val) {
    const node = new LinkNode(val, null);
    this._size++;
    if(this._tail) {
        this._tail.next = node;
        this._tail = node;
        return;
    }
    this._tail = node;
    this._head = node;
};

尾部插入时,当前遍历节点current一定要指向尾部节点(也就是说,current.next != null时就要一直遍历下去),然后next指向new Node

addAtIndex

一定要保证第n个节点是current.next 而不是current

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;
    }
    // 获取目标节点的上一个的节点
    const node = this.getNode(index - 1);
    node.next = new LinkNode(val, node.next);
    this._size++;
};
deleteAtIndex
    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;
        }
        // 获取目标节点的上一个的节点
        const node = this.getNode(index - 1);    
        node.next = node.next.next;
        // 处理尾节点
        if(index === this._size - 1) {
            this._tail = node;
        }
        this._size--;
    };
力扣中别人的解法

原文链接:https://leetcode.cn/problems/design-linked-list/solution/she-ji-lian-biao-by-leetcode-solution-abix/

// MyLinkedList类,提供了对链表进行操作的方法
var MyLinkedList = function() {
    this.size = 0;
    this.head = new LinkNode(0);  // 虚拟头结点
};

// 再定义一个LinkNode类,包含两个属性: val 用来保存节点上的数据,next 用来保存指向下一个节点的链接
function  LinkNode(val, next) {
    this.val = (val === undefined ? 0 : val)
    this.next = (next === undefined ? null : next)
}
get

首先要考虑 index 是否存在不合理的情况:index < 0 或者 index > 链表长度

MyLinkedList.prototype.get = function(index) {
    // 判断索引是否有效
	if (index < 0 || index >= this.size) {
        return -1;
    }
    
    let cur = this.head; // 定义临时指针
    for (let i = 0; i <= index; i++) {
        cur = cur.next;
    }
    return cur.val;
};
addAtIndex

实现 addAtHead(val) 和 addAtTail(val) 时,可以借助 addAtIndex(index, val) 来实现。所以先优先写出addAtIndex(index, val)。

MyLinkedList.prototype.addAtIndex = function(index, val) {
	if (index > this.size) {
        return;
    }
    index = Math.max(0, index);
    this.size++;
    let pred = this.head;
    for (let i = 0; i < index; i++) {
        pred = pred.next;
    }
    let toAdd = new LinkNode(val);
    toAdd.next = pred.next;
    pred.next = toAdd;
};
addAtHead和 addAtTail
MyLinkedList.prototype.addAtHead = function(val) {
    this.addAtIndex(0, val);
};

MyLinkedList.prototype.addAtTail = function(val) {
    this.addAtIndex(this.size, val);
};
deleteAtIndex
MyLinkedList.prototype.deleteAtIndex = function(index) {
	if (index < 0 || index >= this.size) {
        return;
    }
    this.size--;
    let pred = this.head;
    for (let i = 0; i < index; i++) {
        pred = pred.next;
    }
    pred.next = pred.next.next;
};

206.反转链表

题目链接:https://leetcode.cn/problems/reverse-linked-list/

可以用双指针解法、递归解法

双指针写法

定义两个指针,一个cur,一个pre。让cur指向head。

当cur指向null的时候,遍历就结束了

  1. while(cur) {
  2. temp = cur.next // 首先要把 cur.next 节点用tmp指针保存一下,也就是保存一下这个节点
  3. cur.next = pre // 要改变 cur.next 的指向了,将cur.next 指向pre
  4. pre = cur // pre 向前移动一格
  5. cur = temp }
  6. return pre 这个就是新链表的头结点了
var reverseList = function(head) {
    if(!head || !head.next) return head;
    let temp = null, pre = null, cur = head;
    while(cur) {
        temp = cur.next; // // 先保存当前节点的指针next
        cur.next = pre;
        pre = cur;
        cur = temp;
    }
    // temp = cur = null;
    return pre;
};

总结:代码中怎么链表是否为空,该怎么写?是 if(head.next = null) return null; ?

不不不,应该是if(!head || !head.next) return head;。如果链表为空,那么head就是null,那么!head就是True了,就能对空指针进行后续操作了

递归写法

递归的终止条件为链表没元素或者只有一个元素。

var reverse = function(pre, head) {
    if(!head) return pre;
    const temp = head.next;
    head.next = pre;
    pre = head
    return reverse(pre, temp);
}

var reverseList = function(head) {
    return reverse(null, head);
};

// 递归2
var reverse = function(head) {
    if(!head || !head.next) return head;
    // 从后往前翻
    const pre = reverse(head.next);
    head.next = pre.next;
    pre.next = head;
    return head;
}

var reverseList = function(head) {
    let cur = head;
    while(cur && cur.next) {
        cur = cur.next;
    }
    reverse(head);
    return cur;
};

参考文章

  1. 算法之链表(leetCode) - 掘金 (juejin.cn)
  2. javascript 中的nodeList理解_果果B的博客-CSDN博客
  3. javascript链表_木可生森的博客-CSDN博客
  4. JS中的算法与数据结构——链表(Linked-list) - 掘金 (juejin.cn)
  5. JS中的类很难吗? - 掘金 (juejin.cn)
  6. JavaScript class和function的区别 - 掘金 (juejin.cn)
  7. JS中的面向对象:prototype、_ _proto__与constructor · 前端进阶 (dennisgo.cn)
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值