数据结构与算法系列——链表系列题目(1)

533 阅读3分钟

这是我参与更文挑战的第9天,活动详情查看: 更文挑战

前言

链表这个数据结构,在我们前端开发中用的比较少,但是抵不住源码中有这样的数据结构。如果你对链表中的基本操作你都不是很熟悉的话。你很难看懂源码中的一些方法。本篇文章还是结合Leecode带你领略链表中的奥秘。

定义

链表是一种物理存储单元上非连续、非顺序的存储结构数据元素的逻辑顺序是通过链表中的指针链接次序实现的。链表由一系列结点(链表中每一个元素称为结点)组成,结点可以在运行时动态生成。每个结点包括两个部分:一个是存储数据元素的数据域,另一个是存储下一个结点地址的指针域。 相比于线性表顺序结构,操作复杂。由于不必须按顺序存储,链表在插入的时候可以达到O(1)的复杂度,比另一种线性表顺序表快得多,但是查找一个节点或者访问特定编号的节点则需要O(n)的时间,而线性表和顺序表相应的时间复杂度分别是O(logn)和O(1)。

实现js版本的链表

// 单个链表节点
class ListNode {
  constructor(value, next) {
    this.value = value || 0
    this.next = next || null
  }
}

// 所有链表的节点
class LinkList {
  constructor(arr) {
    this.list = LinkList.arr2List(arr)
  }
  
  // 数组转链表的一个方法
  static arr2List(arr) {
    if (!Array.isArray(arr) && !arr.length) {
      throw 'errr'
    }
    let head = new ListNode(arr[0])
    let current = head
    arr.slice(1).forEach((item) => {
      let node = new ListNode(item)
      current.next = node
      current = node
    })
    return head
  }
}

上面这段主要是定义了链表节点属性和链表,以及一个链表上一个静态方法: 数组转链表。

链表的遍历

forEach() {
    let current = this.list
    let arr = []
    // 一直遍历节点
    while (current) {
      arr.push(current.value)
      current = current.next
    }
    return arr
  }

链表的遍历,其实就是不断取next节点。

插入一个链表节点

// k 表示位置 node 节点
  insert(k, node) {
    let current = this.list
    let i = 0
    while (current) {
      if (i === k) {
        let behind = current.next
        current.next = node
        node.next = behind
      }
      current = current.next
      i++
    }
    return this.list
  }

在某个位置插入链表节点, 就当前链表的节点next 指向后一个, k位置的下一个节点执向当前节点。

反转链表

reverse() {
  let cur = this.list
  let pre = null
  // 遍历当前链表
  while (cur !== null) {
    // 将当前指针指向前一个  并且 得记住当前后面的节点
    let temp = cur.next
    // 将当前节点指向上一个节点
    cur.next = pre
    // 改变上一个节点
    pre = cur
    cur = temp
  }
  return pre
}

链表的反转就是定义一个节点,不断改变节点的指向。

判断是不是回文链表

// 判断当前链表是不是回文链表
  isParionList() {
    let current = this.list
    let res = []
    while (current) {
      res.push(current.val)
      current = current.next
    }
    // 利用数组reverse 方法
    return res.reverse().toString() == res.toString()
  }

利用数组reverse去判断,虽然比较蠢,但是也是实现了。

求两个链表相交的地方

 static getIntersectionNode(headA, headB) {
    // 用一个集合去存储每个节点
    const set = new Set()
    let current = headA
    while (current !== null) {
      set.add(current)
      current = current.next
    }
    current = headB
    while (current !== null) {
      // 如果存在节点相同 就返回就好
      if (set.has(current)) {
        return current
      }
      current = current.next
    }
    return current
  }

总结

至此,我们对链表这个数据结构有了一定的认知,由于其非连续内存的特性导致链表非常适用于频繁插入、删除的场景,而不见长于读取场景,这跟数组的特性恰好形成互补,所以现在也可以回到题目中的问题了,链表的特性与数组互补,各有所长,而且链表由于指针的存在可以形成环形链表,在特定场景也非常有用,因此链表的存在是很有必要的。