算法-链表

359 阅读7分钟

什么是链表

链表:由一组零散的内存块透过指针连接而成,每一个块中必须包含当前节点内容以及后继指针。 常见的链表类型有单链表、双链表以及循环链表。

单链表

  • 每个节点存储有当前数据和下一个节点的内存地址
  • 最后一个节点next为null

image.png

单链表的基本组成:

  • 生成新节点
  • 获取链表
  • 查询是否存在指定节点
  • 新增节点
  • 插入节点到指定节点前
  • 移除节点
  • 节点大小

基本结构代码

class LinkedList {
  constructor() {
    // 头节点
    this.head = null
    // 链表长度
    this.length = 0
  }
  // 生成新节点
  createNode(val) {
    return {
      element: val,
      next: null
    }
  }
  // 获取链表
  getList() {
    return this.head
  }
  // 查询节点
  search(element) { }
  // 新增节点
  append(element) { }
  // 插入节点到指定位置
  insert(position, element) { }
  // 移除节点
  remove(element) { }
  isEmpty() {
    return this.length === 0
  }
  size() {
    return this.length
  }
}

新增方法append

新增分为两种情况

  • 链表为空时

image.png

  • 链表不为空时

image.png

思路

  • 链表为空时,将head指向新节点,链表长度加一
  • 链表不为空时,遍历节点,直到最后一个节点,将最后一个节点的next指向新节点,链表长度加一

代码实现

  // 新增节点
  append(element) {
    const node = this.createNode(element)
    // 空链表,head指向新节点
    if(this.isEmpty()) {
      this.head = node 
    } else {
      let p = this.head
      // 寻找最后一个节点
      while(p.next) {
        p = p.next
      }
      p.next = node
    }
    this.length +=1
  }

使用

  const list = new LinkedList()
  list.append('233')
  list.append('666')

查询链表中是否存在指定值search

思路

  • 链表为空,直接返回false
  • 链表不为空,遍历链表
    • 判断节点值是否等于指定值,如果等于,返回true;否则移动到下一个节点,继续查询
  • 查询完整个列表,都没有找到,返回false

代码实现

  // 查询节点
  search(element) {
    // 空链表
    if(this.isEmpty()) {
      return false
    }
    // 非空链表
    let p = this.head
    while(p) {
      // 查询到,返回true
      if(p.element === element) {
        return true
      }
      p = p.next
    }
    // 查询整个链表,没有找到
    return false
  }

指定位置插入节点insert

image.png

  • position0时,插入节点的next指向headhead指向插入节点,更新链表长度
  • position满足0 < position <= 链表长度条件时,遍历节点
    • 直到遍历节点索引和position相等,插入节点的next指向索引对应的节点,索引前一个节点的next指向插入节点, 更新链表长度

代码实现

  // 插入节点到指定位置
  insert(position, element) {
    const node = this.createNode(element)
    if(position === 0) {
      node.next = this.head
      this.head = node
      this.length +=1
    }
    if(position > 0 && position <= this.size()) {
      let prev = this.head
      let cur = this.head
      const size = this.size()
      for (let i = 0; i <= size; i++) {
        if(i === position) {
          node.next = cur
          prev.next = node
          this.length +=1
          break;
        }
        // 指针后移
        prev = cur
        cur = cur.next
      }
    }
  }

移除节点remove

情况一:

image.png

情况二:

image.png

  • 空链表,直接返回
  • 链表不为空,
    • 如果移除第一个节点,把head指向head.next,更新链表长度,结束函数
    • 不是移除第一个节点,遍历节点
      • 判断当前节点值与移除值是否相等,如果相等,前一个节点的next指向当前节点的next

代码实现

  // 移除节点
  remove(element) {
    if(this.isEmpty()) {
      return
    }
    // 如果移除第一个节点
    if(this.head.element === element) {
      this.head = this.head.next
      this.length--
      return
    }
    // 指向前一个节点
    let prev = this.head
    let cur = this.head
    let isFind = false
    while(cur && !isFind) {
      // 判断是否查询到指定值
      if(cur.element === element) {
        isFind = true
        // 前一个节点next指向当前节点的next,跳过当前节点,达到移除效果
        const next = cur.next
        prev.next = next
        // 更新链表长度
        this.length--
      } else {
        prev = cur
        cur = cur.next
      }
    }
  }

完整单链表代码

class LinkedList {
  constructor() {
    // 头节点
    this.head = null
    // 链表长度
    this.length = 0
  }
  // 生成新节点
  createNode(val) {
    return {
      element: val,
      next: null
    }
  }
  // 获取链表
  getList() {
    return this.head
  }
  // 查询节点
  search(element) {
    // 空链表
    if(this.isEmpty()) {
      return false
    }
    // 非空链表
    let p = this.head
    while(p) {
      // 查询到,返回true
      if(p.element === element) {
        return true
      }
      p = p.next
    }
    // 查询整个链表,没有找到
    return false
  }
  // 新增节点
  append(element) {
    const node = this.createNode(element)
    // 空链表,head指向新节点
    if(this.isEmpty()) {
      this.head = node 
    } else {
      let p = this.head
      // 寻找最后一个节点
      while(p.next) {
        p = p.next
      }
      p.next = node
    }
    this.length +=1
  }
  // 插入节点到指定位置
  insert(position, element) {
    const node = this.createNode(element)
    if(position === 0) {
      node.next = this.head
      this.head = node
      this.length +=1
    }
    if(position > 0 && position <= this.size()) {
      let prev = this.head
      let cur = this.head
      const size = this.size()
      for (let i = 0; i <= size; i++) {
        if(i === position) {
          node.next = cur
          prev.next = node
          this.length +=1
          break;
        }
        // 指针后移
        prev = cur
        cur = cur.next
      }
    }
  }
  // 移除节点
  remove(element) {
    if(this.isEmpty()) {
      return
    }
    // 如果移除第一个节点
    if(this.head.element === element) {
      this.head = this.head.next
      this.length--
      return
    }
    // 指向前一个节点
    let prev = this.head
    let cur = this.head
    let isFind = false
    while(cur && !isFind) {
      // 判断是否查询到指定值
      if(cur.element === element) {
        isFind = true
        // 前一个节点next指向当前节点的next,跳过当前节点,达到移除效果
        const next = cur.next
        prev.next = next
        // 更新链表长度
        this.length--
      } else {
        prev = cur
        cur = cur.next
      }
    }
  }
  isEmpty() {
    return this.length === 0
  }
  size() {
    return this.length
  }
  print() {
    if(!this.isEmpty()) {
      const arr = []
      let p = this.head
      while(p) {
        arr.push(p.element)
        p = p.next
      }
      console.log(arr)
    }
  }
}

查找:从头节点开始查找,时间复杂度为 O(n)

插入或删除:在某一节点后插入或删除一个节点(后继节点)的时间复杂度为 O(1)

双向链表

双链表中的节点有两个指针,前驱指针和后继指针,新增了一个尾节点指针tail,永远指向最后一个节点

image.png

基本结构

  // 双向链表
  class DoublyLinkedList {
    constructor() {
      // 头节点
      this.head = null
      // 尾节点
      this.tail = null
      // 链表长度
      this.length = 0
    }
    // 生成新节点
    createNode(val) {
      return {
        element: val, // 节点值
        prev: null, // 前驱指针
        next: null // 后继指针
      }
    }
    // 获取链表
    getList() { return this.head }
    // 查询节点
    search(element) {}
    // 新增节点
    append(element) {}
    // 插入节点到指定位置
    insert(position, element) {}
    // 移除节点
    remove(element) {}
    isEmpty() { return this.length === 0 }
    size() { return this.length }
  }

新增节点到尾部append

链表为空时

image.png

链表不为空

image.png

思路

  • 创建新节点
  • 判断链表是否为空,如果为空,把head指向新节点,tail也指向新节点
  • 链表不为空,通过tail取出尾部节点,尾节点的next指向新节点, 最后把tail指向新节点
  • 链表长度加一

代码实现

  // 新增节点
  append(element) {
    const node = this.createNode(element)
    // 空链表
    if(!this.head) {
      this.head = node
      this.tail = node
    } else {
      // 取出尾部节点
      // 添加到新节点到链表中
      const cur = this.tail
      cur.next = node
      node.prev = cur
      // 更新tail指向
      this.tail = node
    }
    this.length++
  }

指定位置插入节点insert

在头部插入

image.png

在尾部插入

image.png

中间任意位置插入

image.png

插入位置position合法范围为:0 <= position <= 链表长度

思路

  • 判断position是否为合法范围,如果不是,直接返回false,插入失败
    • 插入位置有效
      • position=0,头部位置插入,新节点next指向head.next,再把head.prev指向新节点,最后head指向新节点,链表长度加一,返回插入成功
      • position=链表长度,尾部位置插入,新节点prev指向尾部节点,尾部节点next指向新节点,tail指向新节点
      • position=k,中间任意位置插入,遍历链表
        • 直到节点索引等于k, 新节点.next指向cur节点,cur.prev指向新节点,再让新节点.prev指向前一个节点,前一个节点prev指向新节点,最后返回插入成功

代码实现

  // 插入节点到指定位置
  insert(position, element) {
    const node = this.createNode(element)

    // 判断position合法性
    if(position >= 0 && position <= this.length) {
      // 插入头部
      if(position === 0) {
        const cur = this.head
        node.next = cur
        cur.prev = node
        this.head = node
      } else if(position === this.length) {
        // 插入尾部
        const cur = this.tail
        node.prev = cur
        cur.next = node
        this.tail = node
      } else {
        // 插入中间任意位置
        let cur = this.head
        let prev = this.head
        const size = this.size()
        for (let i = 0; i < size; i++) {
          if(position === i) {
            node.next = cur
            cur.prev = node
            node.prev = prev
            prev.next = node
            break
          } else {
            prev = cur
            cur = cur.next
          }
        }
      }
      this.length++
      return true
    }
    return false
  }

移除指定位置的节点remove

image.png

思路

  • 链表为空,直接返回null
  • 链表不为空,判断position是否符合合法范围: position >= 0 && position < 链表长度
  • 在合法范围内,遍历链表,直到指定位置链表,移除节点:
    • position=0, 记录头节点,head指向头结点的next
    • position=数组长度-1,记录尾节点,取出尾节点的前驱节点,前驱节点next指向nulltail指向前驱节点
    • 找到索引等于position的节点,记录被移除的节点,取出移除节点的后继节点, 后继节点.prev指向prev节点prev节点next指向后继节点
    • 链表长度减少一,返回被移除的结点
  • 不在合法范围内,返回null

代码实现

  // 移除指定位置的节点
  removeAt(position) {
    // 空链表
    if(this.length === 0) {
      return null
    }
    // 被移除的节点
    let removeNode = null
    if(position >=0 && position < this.length) {
      // 移除头节点
      if(position === 0) {
        const removeNode = this.head
        this.head = removeNode.next
      } else if(position === this.length-1) {
        // 移除尾节点
        const removeNode = this.tail
        const prev = removeNode.prev
        prev.next = null
        this.tail = prev
      } else {
        // 移除中间位置节点
        let cur = this.head
        let prev = this.head
        let length = this.length
        for (let i = 0; i < length; i++) {
          if(position === i) {
            removeNode = cur
            const nextNode = removeNode.next
            nextNode.prev = prev
            prev.next = nextNode
            break
          } else {
            prev = cur
            cur = cur.next
          }
        }
      }
      this.length--
      return removeNode
    }
    // 没有找到要移除的元素
    return null
  }

循环单链表

循环单链表是一种特殊的单链表,它和单链表的唯一区别是:单链表的尾节点指向的是 NULL,而循环单链表的尾节点指向的是头节点,这就形成了一个首尾相连的环:

image.png

合并两个有序链表

将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。

示例

输入:l1 = [1,2,4], l2 = [1,3,4]
输出:[1,1,2,3,4,4]

思路

  • 从链表开头开始,比较l1.vall2.val大小
    • 如果l1.val小于等于l2.val,此时最小值就是l1.val,第二小值在l1.nextl2中,进行递归查找
    • 如果l1.val大于l2.val,此时最小值就是l2.val,第二小值在l2.nextl1.val中,进行递归查找
    • 直到递归到 l1 l2 均为 null
    • 当递归到任意链表为 null ,直接将 next 指向另一条链表,不需要继续递归

代码实现

/**
 * @param {ListNode} l1
 * @param {ListNode} l2
 * @return {ListNode}
 */
const mergeTwoLists = function(l1, l2) {
  // 节点为null,直接返回另外一条链表
  if(!l1) {
    return l2
  }
  if(!l2) {
    return l1
  }
  if(l1.val <= l2.val) {
    // 最小值为l1,继续递归找第二小的值,在l1.next和l2中查找
    l1.next = mergeTwoLists(l1.next, l2)
    // 返回链表
    return l1
  } else {
    // 最小值为l2,继续递归找第二小的值,在l2.next和l1中查找
    l2.next = mergeTwoLists(l2.next, l1)
    return l2
  }
}

判断链表是否有环

给你一个链表的头节点 head ,判断链表中是否有环。

如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。如果 pos 是 -1,则在该链表中没有环。注意:pos 不作为参数进行传递,仅仅是为了标识链表的实际情况。

如果链表中存在环,则返回 true 。 否则,返回 false

思路

  • 定义两个指针,一快一满。慢指针每次只移动一步,而快指针每次移动两步。
  • 初始时,慢指针在位置 head,而快指针在位置 head.next。这样一来,如果在移动的过程中,快指针反过来追上慢指针,就说明该链表为环形链表。否则快指针将到达链表尾部,该链表不为环形链表。

代码实现

/**
 * Definition for singly-linked list.
 * function ListNode(val) {
 *     this.val = val;
 *     this.next = null;
 * }
 */

/**
 * @param {ListNode} head
 * @return {boolean}
 */
const hasCycle = function(head) {
  // 形成环,至少需要2个节点
  if(head === null || head.next === null) {
    return false
  }
  // 初始设置快慢指针
  let slow = head
  let fast = head.next
  while(slow !== fast) {
    // 没有环
    if(fast === null || fast.next === null) {
      return false
    }
    slow = slow.next
    // 快指针每次移动两个位置
    fast = fast.next.next
  }
  return true
}

反转链表

给定单链表的头节点 head ,请反转链表,并返回反转后的链表的头节点。

image.png

输入: 1->2->3->4->5->NULL
输出: 5->4->3->2->1->NULL

进阶: 你可以迭代或递归地反转链表。你能否用两种方法解决这道题?

迭代法

image.png

思路: 遍历链表,每个节点的后继指针指向它的前一个节点

代码实现

/**
 * @param {ListNode} head
 * @return {ListNode}
 */
const reverseList = function(head) {
  // 如果是空链表或者只有一个节点
  if(!head || !head.next) {
    return head
  }
  let prev = null
  let cur = head
  while(cur) {
    // 临时存储curr的后继节点
    const nextNode = cur.next
    // 反转后继指针
    cur.next = prev
    // 移动到下一个节点
    prev = cur
    cur = nextNode
  }
  // 更新head为最后一个节点
  head = prev
  return head
}

链表的中间结点

给定一个带有头结点 head 的非空单链表,返回链表的中间结点。

如果有两个中间结点,则返回第二个中间结点。

示例1

输入:[1,2,3,4,5]
输出:此列表中的结点 3 (序列化形式:[3,4,5])
返回的结点值为 3 。 (测评系统对该结点序列化表述是 [3,4,5])。

注意,我们返回了一个 ListNode 类型的对象 ans,这样:
ans.val = 3, ans.next.val = 4, ans.next.next.val = 5, 以及 ans.next.next.next = NULL.

示例2

输入:[1,2,3,4,5,6]
输出:此列表中的结点 4 (序列化形式:[4,5,6])

由于该列表有两个中间结点,值分别为 34,我们返回第二个结点。

思路

  • 使用快慢指针,慢指针每次走一步,快指针每次走两步,当快指针到达尾部时,慢指针刚好到中间

代码实现

/**
 * @param {ListNode} head
 * @return {ListNode}
 */
const middleNode = function(head) {
  // 快慢指针
  let slow = head
  let fast = head
  // 当快指针到达终点时,慢指针刚好在中间
  while(fast && fast.next) {
    // 慢指针每次走一步
    // 快指针走两步
    slow = slow.next
    fast = fast.next.next
  }
  return slow
}

删除链表的倒数第n个结点

给定一个链表,删除链表的倒数第 n 个节点,并且返回链表的头结点。

示例:

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

示例2

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

说明: 给定的 n 保证是有效的。

方式一:数组法

  • 遍历一次链表,把每个结点放入数组中
  • 获取数组长度,通过长度可以计算出删除节点的前驱节点索引为length-n-1, 删除节点的索引为length-n, 再把前驱节点的next指向删除节点next,最后返回head
  • 如果length等于n,是移除头节点

代码实现

/**
 * @param {ListNode} head
 * @param {number} n
 * @return {ListNode}
 */
const removeNthFromEnd = function(head, n) {
  // 定义栈存储节点
  let stack = []
  let p = head
  while(p) {
    stack.push(p)
    p = p.next
  }
  const length = stack.length
  // 移除头节点 
  if(n === length) {
    head = head.next
    return head
  }
  //  移除其它位置的,取出移除节点的前驱节点,前驱节点指向移除节点的后继
  let prevNode = stack[length-n-1]
  let curNode = stack[length-n]
  prevNode.next = curNode.next
  return head
}

方式二:双指针法

  • 定义firstsecond指针
  • second指针先走n
  • 判断second是否等于null,如果等于,说明是移除头节点,直接返回first.next
  • 移除头节点之后的节点
    • firstsecond指针同时移动,每次移动一个位置,直到second.nextnull,说明second指向尾节点, 此时first指向移除节点的前一个节点
    • first.next指向first.next.next,完成节点移除
    • 返回head

移除头节点

image.png

移除头节点之后的节点

image.png

image.png

代码实现

/**
 * @param {ListNode} head
 * @param {number} n
 * @return {ListNode}
 */
const removeNthFromEnd = function(head, n) {
  let first = head
  let second = head
  // 第2个指针先走n步
  while(n > 0) {
    second = second.next
    n--
  }
  // 移除头节点
  if(second === null) {
    head = first.next
    return head
  }
  // 移除头节点之后的节点
  // 两个指针一起移动,直到second到最后一个节点为止
  while(second.next) {
    first = first.next
    second = second.next
  }
  // 前驱节点指向后继的后继
  first.next = first.next.next
  return head
}

相交链表

给你两个单链表的头节点 headA 和 headB ,请你找出并返回两个单链表相交的起始节点。如果两个链表不存在相交节点,返回 null 。

图示两个链表在节点 c1 开始相交:

题目数据 保证 整个链式结构中不存在环。

注意,函数返回结果后,链表必须 保持其原始结构

示例 1:

输入:intersectVal = 8, listA = [4,1,8,4,5], listB = [5,6,1,8,4,5], skipA = 2, skipB = 3
输出:Intersected at '8'
解释:相交节点的值为 8 (注意,如果两个链表相交则不能为 0)。
从各自的表头开始算起,链表 A[4,1,8,4,5],链表 B[5,6,1,8,4,5]。
在 A 中,相交节点前有 2 个节点;在 B 中,相交节点前有 3 个节点。

示例2

输入:intersectVal = 0, listA = [2,6,4], listB = [1,5], skipA = 3, skipB = 2
输出:null
解释:从各自的表头开始算起,链表 A[2,6,4],链表 B[1,5]。
由于这两个链表不相交,所以 intersectVal 必须为 0,而 skipA 和 skipB 可以是任意值。
这两个链表不相交,因此返回 null 。

思路

  • 准备两个指针pApB,分别指向两链表头节点 headA , headB
  • 假设链表A长度为m,链表B长度为n,「两链表的公共尾部」的节点数量为c
  • 同步遍历链表AB
    • 如果指针pA先遍历完链表A, 把指向更改为headB,遍历链表B, 如果发生重合,一共走过步数为
    m+(n-c)
    
    • 如果指针pB先遍历完链表B, 把指向更改为headA,遍历链表A, 如果发生重合,一共走过步数为
    n+(m-c)
    
  • pApB相交,那么有以下公式成立
m+(n-c) = n+(m-c)

发生重合情况时有两种情况:

  • pApB指向同一个节点,c>0
  • pApB都为null, c=0

代码实现

/**
 * @param {ListNode} headA
 * @param {ListNode} headB
 * @return {ListNode}
 */
const getIntersectionNode = function(headA, headB) {
    let pA = headA
    let pB = headB
    // 如果相交,结束遍历,如果不相交,在遍历两个链表后,最后都是null
    while(pA !== pB) {
      // 没有遍历完,取出下一个节点; 否则移动到另外一个链表头继续遍历
      pA = pA === null ? headB : pA.next
      pB = pB === null ? headA : pB.next
    }
    return pA
}

图示

  • 初始状态

image.png

  • 同步遍历链表A和B,其中一条较短链表遍历完,移动指针到另外一条继续遍历,如下pB遍历完链表B,转向链表A的头部开始

image.png

  • pA遍历完链表A,转向链表B的头部开始

image.png

  • 最后在同一个节点相遇

image.png