【js - 算法】链表篇

258 阅读5分钟

链表

前言

本文(链表)涉及算法:迭代、递归、双指针、哈希集合

上一篇文章 js算法数组篇 感兴趣的同学可以去学习下

链表概念

通过指针串联在一起的线性结构 链表类型:

  1. 单链表:一个节点由两部分组成,一个是数据,一个是存放指向下一节点的指针,最后一个指针指向 null
  2. 双链表:双链表有两个指针,一个指向下一个节点,一个指向上一个节点
  3. 环形链表:顾名思义,就是链表首位相连

链表存储方式:

数组在内存中是连续分布的,链表则不是,链表通过指针域的指针链接在内存中的各个节点,散乱的分布在内存的某个地址上,取决于操作系统的内存管理

移除链表元素

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

leetcode 链接

  1. 递归:时间复杂度 O(n)
const removeElements = (cur, val) => {
  if (cur === null) return null; // 如果为 null 直接返回
  cur.next = removeElements(cur.next, val);
  return cur.val === val ? cur.next : cur; // 如果当前值等于 目标值,则跳过
};
  1. 迭代:设置虚拟头节点(防止第一个节点即需要删除),如果下一个节点不为 null 并且等于 给定值,则删除下一个节点;时间复杂度 O(n)
const removeElements = (cur, val) => {
  let dummyHead = new ListNode(0); // 创建虚拟头节点
  dummyHead.next = cur;
  let temp = dummyHead;
  while (temp.next !== null) {
    if (temp.next.val === val) {
      temp.next = temp.next.next; // 如果等于给定值,跳到下下个
    } else {
      temp = temp.next; // 否则继续向下走
    }
  }
  return dummyHead.next; // 注意设置了虚拟头节点,需要去除(设置了这个,就不需要考虑头节点等于给定值的情况)
};

反转链表

题目:给你单链表的头节点 head ,请你反转链表,并返回反转后的链表。 leetcode 题目

  1. 迭代:时间复杂度 O(n)
const reverseList = (head) => {
  let pre = null;
  let cur = head;
  while (cur) {
    let next = cur.next; // 保存操作
    cur.next = pre; // 翻转操作
    pre = cur;
    cur = next;
  }
  return pre;
};

链表相交

题目:给你两个单链表的头节点 headA 和 headB ,请你找出并返回两个单链表相交的起始节点。如果两个链表没有交点,返回 null 。 leetcode 链接

  1. 哈希集合:时间复杂度 O(m+n)(m、n 分别为 headA、headB 链表的长度);空间复杂度 O(m)(m 为 headA 的长度)
const getIntersectionNode = (headA, headB) => {
  let map = new Map();
  let temp = headA;
  while (temp) {
    // 遍历,将值加入 map 中
    map.set(temp, temp);
    temp = temp.next;
  }
  temp = headB; // 重新赋值,遍历
  while (temp) {
    if (map.has(temp)) {
      // 如果找到相同节点,则返回
      return temp;
    }
    // 没找到,继续遍历
    temp = temp.next;
  }
  return null;
};
  1. 双指针:时间复杂度 O(m+n);空间复杂度为 O(1)

情况一:两链表相交

  • 链表 headA 和 headB 长度为 m 和 n;headA 不相交部分长度为 a,headB 不相交部分长度为 b,它们相交的部分长度为 c
  • 所以 a + c = m;b + c = n
  • 如果 a = b,那么它们会同时到达相交的地方
  • 如果 a 和 b 不相等,他们会各自遍历一遍链表,不会同时遍历完
  • 重点来了,遍历完之后,将 pA 指向 headB 的头节点,pB 指向 headA 的头节点,继续遍历;指针 pA 移动 a + c + b 次后;pB 移动 b + c + a 次后,此时它们会同时到达相交的节点

情况二:链表不相交

  • 如果 m = n,同时到达尾部,为 null
  • 如果 m 和 n 不相等,各自走完 m + n 次节点,到达尾部,指向 null
const getIntersectionNode = (headA, headB) => {
  if (headA === null || headB === null) {
    return null;
  }
  let pA = headA,
    pB = headB;
  while (pA !== pB) {
    pA = pA === null ? headB : pA.next; // 重点,要进行重新指向,进行另一条链表的遍历
    pB = pB === null ? headA : pB.next;
  }
  return pA;
};

环形链表

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

  1. 哈希集合:时间复杂度 O(n);空间复杂度 O(n)

遍历所有节点,并且用 哈希存起来,判断是否已经访问过,思路很简单

const hasCycle = (head) => {
  let map = new Map();
  while (head) {
    if (map.has(head)) {
      // 判断是否已经访问过
      return true;
    }
    map.set(head, head); // 访问过的存入哈希表
    head = head.next;
  }
  return false;
};
  1. 双指针(快慢指针):时间复杂度 O(n);空间复杂度 O(1)

快指针一次走两步,慢指针一次一步,如果有环就会相遇

const hasCycle = (head) => {
  if (head === null || head.next === null) {
    return false;
  }
  let slow = head,
    fast = head.next; // 设置快慢指针
  while (slow !== fast) {
    if (fast === null || fast.next === null) {
      // 快指针走在前面,只需要考虑快指针是否存在
      return false;
    }
    slow = slow.next; // 一次一步
    fast = fast.next.next; // 一次两步
  }
  return true;
};

两两交换链表中的节点

题目:给你一个链表,两两交换其中相邻的节点,并返回交换后链表的头节点。你必须在不修改节点内部的值的情况下完成本题(即,只能进行节点交换)。

leetcode 链接

  1. 递归:时间复杂度 O(n)
  • head 表示头节点,新的链表的第二节点
  • newHead 表示新的链表的头节点,原始链表的第二节点
  • 所以原始链表中,剩下节点的头节点即为 newHead.next
  • 其余节点进行两两交换 head.next = swapPairs(newHead.next)
const swapPairs = (head) => {
  if (head === null || head.next === null) {
    // 递归结束条件
    return head;
  }
  const newHead = head.next;
  head.next = swapPairs(newHead.next);
  newHead.next = head;
  return newHead;
};

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

题目:给你一个链表,删除链表的倒数第 n 个结点,并且返回链表的头结点。

leetcode 链接

  1. 双指针:时间复杂度 O(n)

如果要删除倒数第 n 个节点,让 fast 移动 n 步,然后让 fast 和 slow 同时移动,直到 fast 指向链表末尾。删掉 slow 所指向的下一个节点就可以了,不难写出以下代码

var removeNthFromEnd = function (head, n) {
  let dummyHead = new ListNode(0); // 创建虚拟头节点(在移除链表元素中提到过作用)
  dummyHead.next = head;
  let slow = dummyHead,
    fast = dummyHead;
  while (n--) fast = fast.next; // fast 先走 n 步
  while (fast.next !== null) {
    // 让 fast 走到结尾,slow也跟着向下走
    fast = fast.next;
    slow = slow.next;
  }
  slow.next = slow.next.next; // 删除 slow 的下一个节点即可
  return dummyHead.next; // 注意设置的虚拟头节点
};