js链表实战

407 阅读7分钟

「这是我参与2022首次更文挑战的第14天,活动详情查看:2022首次更文挑战」。

上一篇文章 写给前端开发的链表介绍(js) 中,介绍了链表,这篇文章就来实战一下,用 js 实现一个链表,再做几道 leetcode。

编码链表的一些小技巧

编码链表和编码数组是完全不一样的,这里有些小技巧,非常有用,链表类题目很多都是用这些小技巧,只有亲自写过才能体会。

  • 循环链表使用 while,每次把当前节点指向当前节点的 next, 就向后面移动了一个节点。
while(head) {
  do something...
  head = head.next
}
  • 设定哨兵节点,哨兵节点的 next 指向当前链表,这样不仅可以少写一些边界条件的判断,也能很好地操作第一个节点。
const cur = {              // 哨兵 -> 1 -> 2 ->3
    next: head
}
return cur.next            // 返回 哨兵.next
  • 设定临时链表,把当前链表赋值给一个临时变量,这样就可以随便操作临时变量,因为引用类型指向同一地址,操作临时链表,原链表也会变化。最后返回的时候返回原链表。
比如:
head:  1 -> 2 -> 3

let temp = head
temp = temp.next                     // 随便操作 temp
temp.val = 100

console.log(temp)    100 -> 3
console.log(head)    1 -> 100 -> 3   // head也会跟着变化

return head

js实现一个链表

JS 中的链表,是以嵌套的对象的形式来实现的

{
  // 数据域
  val: 1,
  // 指针域,指向下一个结点
  next: {
      val:2,
      next: ...
  }
} 

当然,我们也能自己写构造函数,把功能实现全面一点。

目标功能

  • 新增节点
  • 查找某一节点
  • 查找某一节点的前一个节点
  • 插入节点
  • 删除节点
  • 打印链表元素
  • 获取链表长度

定义两个类,一个 Node 类来表示节点,一个 LinkedList 类来实现上述方法。

编码

class Node {                        // 节点构造函数
  constructor (val) {
    this.val = val
    this.next = null
  }
}

class LinkNodeList {               // 链表构造函数
  constructor () {
    this.head = null
    this.length = 0
  }
  
  // 新增节点
  append (val) {
    const node = new Node(val)
    let temp = this.head
    if (this.head) {              // 如果有头节点,就把链表最后一个节点指向要创建的node节点
      while (temp.next) {
        temp = temp.next
      }
      temp.next = node 
    } else {                     // 如果没有头节点,就把要创建的node节点赋值给head
      this.head = node
    }
    this.length++
  }
    
  // 查找节点
  find (val) {
    let temp = this.head
    while (temp.val !== val) {          // 找不到就继续循环
      temp = temp.next
    }
    return temp                         // 找到了就返回节点             
  }
  
  // 查找某一节点的前一个节点
  findPre (val) {
    let temp = this.head
    while (!temp.next && temp.next.val !== val) {    // 当前节点的 next 去和 val 做比较
      temp = temp.next
    }
    return temp
  }
  
  // 插入节点
  insert (val, insertVal) {
    const newNode = new Node(insertVal)
    const temp = this.find(val)
    newNode.next = temp.next
    temp.next = newNode
    this.length++
  }
  
  // 删除节点
  delete (val) {
    const deleteNode = this.find(val)
    this.findPre(deleteNode).next = deleteNode.next
    this.length--
  }
  
  // 打印整个链表
  print () {
    let temp = this.head
    let res = ''
    if (this.head) {
      while (temp) {
        res += `${temp.val} -> `
        temp = temp.next
      }
      console.log('打印链表 :>> ', res.slice(0, res.length - 4))
    } else {
      console.log('链表为空!')
    }
  }
  
  // 获取链表长度
  getLength () {
    return this.length
  }
}

测试一下:

const l1 = new LinkNodeList()  // 定义一个链表

l1.append(1)    
l1.append(2)
l1.append(3)
l1.append(4)                   // 添加 1,2,3,4

l1.insert(2, 100)              // 在 2 后面添加 100
l1.delete(2)                   // 删除 2

l1.print()                     // 打印链表

image.png

用js实现这么一个链表,可以很好地体会链表里插入和删除节点时,节点之间的指向问题。

leetcode实战

203. 移除链表元素

真题描述:给你一个链表的头节点 head 和一个整数 val ,请你删除链表中所有满足 Node.val == val 的节点,并返回 新的头节点 。

image.png

迭代法

循环链表,遇到和 val 值相同的就删除这个节点,充分利用哨兵节点和临时链表的技巧,代码如下:

const removeElements = function (head, val) {
  const cur = {                            // 哨兵节点
    next: head
  }             
  let temp = cur                           // 临时变量
  while (temp.next) {
    if (temp.next.val === val) {           
      temp.next = temp.next.next           // 如果相等,就删除链表元素
    } else {
      temp = temp.next                     // 否则就一直循环
    }
  }
  return cur.next                          // 最后返回哨兵节点的next
}

时间复杂度:O(n),其中 n 是链表的长度。
空间复杂度:O(1)

递归法

这道题也可以用递归来实现:

  • 终止条件:当前节点为空
  • 递归条件:链表的每个节点都调用递归函数
const removeElements = function (head, val) {
  if(!head) {
    return head
  }
  head.next = removeElements(head.next, val)
  return head.val === val ? head.next : head
}

时间复杂度:O(n),其中 n 是链表的长度。递归过程中需要遍历链表一次。
空间复杂度:O(n),其中 n 是链表的长度。空间复杂度主要取决于递归调用栈,最多不会超过 n 层。

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

真题描述:给定一个已排序的链表的头 head , 删除原始链表中所有重复数字的节点,只留下不同的数字 。返回 已排序的链表 。

image.png

这道题和上面那道类似,但是要复杂些,不过还是能用到前面总结的小技巧。

const deleteDuplicates = function (head) {
  const cur = {                                      // 设置哨兵节点
    next: head        
  }

  let temp = cur                                     // 设置临时链表

  while (temp.next && temp.next.next) {              // 当前节点后面两个节点都有值才会去判断,有没有重复的元素   
    if (temp.next.val === temp.next.next.val) {      // 如果有重复的元素
      const val = temp.next.val                      // 记录下这个重复的值
      while (temp.next && temp.next.val === val) {   // 反复排查这个重复的值,如果遇到重复的,都删除掉
        temp.next = temp.next.next
      }
    } else {                                          
      temp = temp.next                               // 没有重复的元素,就正常循环
    }
  }

  return cur.next                                   // 返回哨兵.next
}

时间复杂度:O(n),其中 n 是链表的长度。
空间复杂度:O(1)

141. 环形链表

真题描述:判断一个链表是否有环

这道经典面试题,一般有两种解法。

哈希表

用一个哈希表把走过的链表节点存起来,后面再遇到就说明有环。

js 里可以用 Set 和 Map 来存储节点。

const hasCycle = (head) => {
  const cache = new Set()       // 定义一个 Set
  while (head) {
    if (cache.has(head)) {      // 有值,说明走到了之前的节点上,有环
      return true
    }
    cache.add(head)             // 每走过一个节点都把节点存进 Set
    head = head.next            // 循环链表
  }
  return false                  // 链表循环完了都没走到之前的节点上,无环
}

时间复杂度:O(n),其中 n 是链表的长度。
空间复杂度:O(n),主要是哈希表的开销。

我第一次解题时尝试这么写,没有通过:

const hasCycle = (head) => {
  const obj = {}
  while (head) {
    if (obj[head]) {
      return true
    }
    obj[head] = head
    head = head.next
  }
  return false
}

因为我用 Object 来存,Object 的 key 只能是字符串,用 Object 存储链表节点(对象),都会自动转成这样的字符串。

image.png

es6 文档上讲述 Map 时也有相关说明:Map

JavaScript 的对象(Object),本质上是键值对的集合(Hash 结构),但是传统上只能用字符串当作键。这给它的使用带来了很大的限制。

这也是 Map 被设计出来的原因。

快慢指针

这题最巧妙的思路还是用快慢指针(双指针),说实话,不看题解,我是不可能想到这样的解题方法的。

快慢指针的思路是:定义两个指针,一个走得快,一个走得慢。

  • 如果链表有环,那么快指针一定能追上慢指针。
  • 如果链表没有环,那么快指针能走完这个链表。

就跟操场跑圈一样,跑得快的人可以套跑得慢的人圈。

代码如下:

const hasCycle = (head) => {
  let slow = head                 // 定义慢指针
  let fast = head                 // 定义快指针
  
  while (fast && fast.next) {
      slow = slow.next            // 慢指针一次走一步
      fast = fast.next.next       // 快指针一次走两步
      if (fast === slow) {        // 如果快指针追上了慢指针,有环
          return true
      }
  }
  return false                    // 循环结束,快指针走完了链表,无环
}

时间复杂度:O(n),其中 n 是链表的长度。
空间复杂度:O(1)

小结

链表的题做了一些之后,我直呼,都是套路啊,如果没写过直接让你来写,不可能会写的。

而如果写过几道类似的,其他的也可以融会贯通。

如果觉得上面几道题有难度,这篇文章里也有几道题,要相对简单一些,链接在这里,写给前端开发的链表介绍

往期算法相关文章

从 keep-alive 源码掌握 LRU Cache

写给前端开发的算法简介

树简介

写给算法初学者的分治法和快速排序(js)

散列表介绍

广度优先搜索

深度优先搜索

写给算法初学者的贪心算法