三、链表_JavaScript数据结构与算法

链表

链表是多个元素组成的列表。元素存储不连续,用 next 指针连在一起

数组 VS 链表

  • 数组:增删首尾元素时往往需要移动元素

  • 链表:增删非首尾元素,不需要移动元素,只需要更改 next 的指向

    插入一个节点效率很高,只需修改它前面的节点,使其指向新加入的节点

    删除一个节点,将待删除的元素

JavaScript 中没有链表,可以使用 Object 模拟链表

const a = { val: 'a' }
const b = { val: 'b' }
const c = { val: 'c' }
const d = { val: 'd' }
a.next = b
b.next = c
c.next = d

// 遍历链表
let p = a
while (p) {
  console.log(p.val)
  p = p.next
}

// 插入
const e = { val: 'e' }
c.next = e
e.next = d

// 删除
c.next = d

237. 删除链表中的节点

237. 删除链表中的节点

输入输出

输入:head = [4,5,1,9], node = 5
输出:[4,1,9]

输入:head = [4,5,1,9], node = 1
输出:[4,5,9]

输入:head = [1,2,3,4], node = 3
输出:[1,2,4]

输入:head = [0,1], node = 0
输出:[1]

输入:head = [-3,5,-99], node = -3
输出:[5,-99]

题解

  • 无法直接获取被删除节点的上个节点(链表的节点只会指向下个节点),将被删除节点转移到下个节点
  • 将被删节点的值改为下个节点的值
/**
 * Definition for singly-linked list.
 * function ListNode(val) {
 *     this.val = val;
 *     this.next = null;
 * }
 */

var deleteNode = function (node) {
  node.val = node.next.val
  node.next = node.next.next
}

复杂度:

  • 时间复杂度:O(1)
  • 空间复杂度:O(1)

206. 反转链表

206. 反转链表

输入输出

输入:head = [1,2,3,4,5]
输出:[5,4,3,2,1]

输入:head = [1,2]
输出:[2,1]

输入:head = []
输出:[]

迭代

  • 反转两个节点:将 n+1next 指向 n
  • 反转多个节点:双指针遍历链表,重复上述操作

图解:

  • 全栈潇晨——方法2.迭代法

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BlSQTw1n-1643104522630)(https://gitee.com/lilyn/pic/raw/master/jslearn-img/206quan.gif)]

  • 在遍历列表时,需要将当前节点(curr)的 next 指针指向上一个节点(prev),之后移动 prevcurr 为下一个循环做准备

/**
 * Definition for singly-linked list.
 * function ListNode(val, next) {
 *     this.val = (val===undefined ? 0 : val)
 *     this.next = (next===undefined ? null : next)
 * }
 */

/* 
prev -> curr
null -> 0 -> 1 -> 2
prev <- curr
null <- 0 <- 1 <- 2
*/
var reverseList = function (head) {
  let curr = head
  let prev = null
  while (curr !== null) {
    const tmp = curr.next
    // 当前节点的next指向上一个节点(完成逆序)
    curr.next = prev
    // 为下一个循环做准备
    prev = curr
    curr = tmp
  }
  return prev
}

简化版:

var reverseList = function (head) {
  let [prev, curr] = [null, head]
  while (curr !== null) {
    [curr.next, prev, curr] = [prev, curr, curr.next]
  }
  return prev
}

复杂度:

  • 时间复杂度:O(n)n 为链表节点数
  • 空间复杂度:O(1)

递归

var reverseList = function (head) {
  if (head == null || head.next == null) return head
  let next = head.next // next节点,反转后是最后一个节点
  const reverseHead = reverseList(head.next)
  head.next = null // 裁减 head
  next.next = head // 尾接
  return reverseHead
}

复杂度:

  • 时间复杂度:O(n),共 n-1 层递归
  • 空间复杂度:O(n),共 n-1 层递归

2. 两数相加

2. 两数相加

输入输出

给你两个 非空 的链表,表示两个非负的整数。它们每位数字都是按照 逆序 的方式存储的,并且每个节点只能存储 一位 数字。请你将两个数相加,并以相同形式返回一个表示和的链表

输入:l1 = [2,4,3], l2 = [5,6,4]
输出:[7,0,8]
解释:342 + 465 = 807.

输入:l1 = [0], l2 = [0]
输出:[0]

输入:l1 = [9,9,9,9,9,9,9], l2 = [9,9,9,9]
输出:[8,9,9,9,0,0,0,1]

题解

  • 新建一个空链表
  • 遍历被相加的两个链表,模拟相加操作,将 个位数 追加到新链表上,将 十位数 留到下一位去相加
/**
 * Definition for singly-linked list.
 * function ListNode(val, next) {
 *     this.val = (val===undefined ? 0 : val)
 *     this.next = (next===undefined ? null : next)
 * }
 */

var addTwoNumbers = function (l1, l2) {
  const l3 = new ListNode(0)
  let p1 = l1
  let p2 = l2
  let p3 = l3
  let carry = 0
  while (p1 || p2) {
    // 有可能链表是一长一短
    const v1 = p1 ? p1.val : 0
    const v2 = p2 ? p2.val : 0
    // 两链表节点相加再进位
    const val = v1 + v2 + carry
    carry = Math.floor(val / 10)
    p3.next = new ListNode(val % 10)
    if (p1) p1 = p1.next
    if (p2) p2 = p2.next
    p3 = p3.next
  }
  // 判断最后一位节点是否有进位
  if (carry) {
    p3.next = new ListNode(carry)
  }
  return l3.next
}

复杂度:

  • 时间复杂度:O(n)n 为较长链表的长度
  • 空间复杂度:O(n)n 为较长链表的最大值

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

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

输入输出

输入:head = [1,1,2]
输出:[1,2]

输入:head = [1,1,2,3,3]
输出:[1,2,3]

题解

  • 因为链表是有序的,所以重复元素一定相邻
  • 遍历链表,如果发现当前元素和下个元素值相同,就删除下个元素值
  • 遍历结束后,返回原链表的头部
/**
 * Definition for singly-linked list.
 * function ListNode(val, next) {
 *     this.val = (val===undefined ? 0 : val)
 *     this.next = (next===undefined ? null : next)
 * }
 */

var deleteDuplicates = function (head) {
  let p = head
  while (p) {
    if (p.val === p.next.val) {
      p.next = p.next.next
    } else {
      p = p.next
    }
  }
  return head
}

复杂度:

  • 时间复杂度:O(n)n 为链表的长度
  • 空间复杂度:O(1)

141. 环形链表

141. 环形链表

输入输出

给你一个链表的头节点 head ,判断链表中是否有环。如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。如果 pos 是 -1,则在该链表中没有环

输入:head = [3,2,0,-4], pos = 1
输出:true
解释:链表中有一个环,其尾部连接到第二个节点。

输入:head = [1,2], pos = 0
输出:true
解释:链表中有一个环,其尾部连接到第一个节点。

输入:head = [1], pos = -1
输出:false
解释:链表中没有环。

题解

  • 两个人在圆形操场上的起点同时起跑,速度快的人一定会超过速度慢的人一圈
  • 用一快一慢两个指针遍历链表,如果指针能够相逢,那么链表就有圈
/**
 * Definition for singly-linked list.
 * function ListNode(val) {
 *     this.val = val;
 *     this.next = null;
 * }
 */

var hasCycle = function (head) {
  let p1 = head
  let p2 = head
  while (p1 && p2 && p2.next) {
    p1 = p1.next
    p2 = p2.next.next
    if (p1 === p2) {
      return true
    }
  }
  return false
}

复杂度:

  • 时间复杂度:O(n)
  • 空间复杂度:O(1)

原型链

  • 原型链本质是链表
  • 原型链上的节点是各种原型对象,比如:Function.prototypeObject.prototype
  • 原型链通过 __proto__ 属性连接各种原型对象

原型链 除了对象其它都是先指向自己类型的原型对象再指向 Object 的原型对象

  • obj -> Object.prototype -> null
  • func -> Fuction.prototype -> Object.prototype -> null
  • arr -> Array.prototype -> Object.prototype -> null
const obj = {}
const func = () => {}
const arr = []

/* watch */
obj.__proto__ === Object.prototype
func.__proto__ === Fuction.prototype
func.__proto__.__proto__ === Object.prototype
arr.__proto__ === Array.prototype
arr.__proto__.__proto__ === Object.prototype

原型链知识点

  • 如果 A 沿着原型链找到 B.prototype,那么 A instanceof B === true

    这个实际调用的是 B[Symbol.hasInstanceof](A)

  • 如果 A 对象上没有找到 x 属性,那么会沿着原型链找 x 属性

面试题分析

instanceof 的原理,并用代码实现

解法:

  • 遍历 A 的原型链,如果找到 B.prototype,返回 true,否则返回 false
const instanceOf = (obj, Ctor) => {
  let p = obj
  while (p) {
    if (p === Ctor.prototype) return true
    p = p.__proto__
  }
  return false
}

推荐解法:

  • 检测数据类型步骤跳过
function instance_of(obj, Ctor) {
  // 先检测是否有Symbol.hasInstance这个属性
  if (typeof Ctor[Symbol.hasInstance] === 'function') return Ctor[Symbol.hasInstance](obj)

  // 最后才会按照原型链进行处理
  let prototype = Object.getPrototypeOf(obj)
  while (prototype) {
    if (prototype === Ctor.prototype) return true
    prototype = Object.getPrototypeOf(prototype)
  }
  return false
}

说出下列输出结果

var foo = {},
  F = function () {}
Object.prototype.a = 'value a'
Function.prototype.b = 'value b'

console.log(foo.a) // value a
console.log(foo.b) // 

console.log(F.a) // value a
console.log(F.b) // value b

解法:

  • 明确 fooF 变量的原型链,沿着原型链找 a 属性和 b 属性

前端与链表

使用链表指针获取 JSON 的节点值

const json = {
  a: { b: { c: 1 } },
  d: { e: 2 },
}

const path = ['a', 'b', 'c']
let p = json
path.forEach(k => {
  p = p[k]
})
console.log(p) // 1

234. 回文链表

234. 回文链表

输入输出

输入:head = [1,2,2,1]
输出:true

输入:head = [1,2]
输出:false

数组双指针

  • 将值赋值到数组中后用双指针法
/**
 * Definition for singly-linked list.
 * function ListNode(val, next) {
 *     this.val = (val===undefined ? 0 : val)
 *     this.next = (next===undefined ? null : next)
 * }
 */

var isPalindrome = function (head) {
  const arr = []
  while (head !== null) {
    arr.push(head.val)
    head = head.next
  }
  for (let i = 0, j = arr.length - 1; i < j; i++, j--) {
    if (arr[i] !== arr[j]) return false
  }
  return true
}

复杂度:

  • 时间复杂度:O(n)n 为链表的元素个数
  • 空间复杂度:O(n)n 为链表的元素个数

快慢指针

将链表的后半部分反转,然后将前半部分和后半部分进行比较,比较完成后再将链表恢复原样

  1. 找到前半部分链表的尾节点

    可以使用 快慢指针 找到链表的中间:慢指针一次走一步,快指针一次走两步,快慢指针同时出发,当快指针移动到链表的末尾,慢指针恰好到链表的中间

  2. 反转后半部分链表

  3. 判断是否回文

var isPalindrome = function (head) {
  if (head === null) return true
  // 快慢指针,让慢指针指向链表后半部分的开头
  let fast = head
  let slow = head
  // 反转指针
  let prev = null
  let curr = head
  while (fast !== null && fast.next !== null) {
    // 快慢指针,获得链表中部
    fast = fast.next.next
    slow = slow.next
    // 反转指针,反转前半部分指针
    curr.next = prev
    prev = curr
    curr = slow
  }
  /* 
  如果节点为奇数个
  原链表 1->3->5->3->1->null
  快指针 1     2     3
  慢指针 1  2  3 

  如果节点为偶数个
  原链表 1->3->3->1->null
  快指针 1     2      3
  慢指针 1  2  3 
  */
  if (fast !== null) {
    slow = slow.next
  }
  while (prev !== null && slow !== null) {
    if (slow.val !== prev.val) return false
    slow = slow.next
    prev = prev.next
  }
  return true
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值