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

284 阅读9分钟
  • 链表理论基础
  • 203.移除链表元素、707.设计链表、206.反转链表

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

链表基本知识

什么是链表?

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

链表特点?

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

为什么会用到链表?

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

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


203.移除链表元素

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

思路:

  • 首先要判断头结点是不是空的,如果是空的,编译就会报错。同时头结点指向的下一个节点也不能为空。 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 呢?1.png

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

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


707.设计链表

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

操作链表注意点:

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

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

使用函数表达式创建了函数,类名建议首字母大写,结合后面的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 个节点

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

3.png

 输入["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 的操作。 day3.jpg

在定义新的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--;
     };

力扣中别人的解法

原文链接:leetcode.cn/problems/de…

 // 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.反转链表

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

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

双指针写法

定义两个指针,一个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)