单链表

链表

「链表 linked list」是一种线性数据结构,其中的每个元素都是一个节点对象,各个节点通过"引用"相连接。引用记录了下一个节点的内存地址,通过它可以从当前节点访问到下一个节点。

链表由于其结构的不同可以分为:单链表、双向链表、循环链表等等,其中最基础的就是单链表了。

本章主要列举一些有关单链表的LeetCode题目,从而掌握单链表题目中的一些常用知识点和操作。

单链表

单链表中每个节点形如:

ts 复制代码
type ListNode={
    val:object,
    next:ListNode,
}

节点与节点之间使用next连接,由于其单向性以及存储地址的不连续性,我们在访问第i个节点时都需要从头遍历,时间复杂度为 <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( n ) O(n) </math>O(n),但是其增加和删除操作的时间复杂度为 <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( 1 ) O(1) </math>O(1)。

由于单链表是一个比较简单的数据结构,你可以在这里了解更多。

基础题目

LeetCode-203.移除链表元素

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

这道题很简单,就是删除目标结点。首先分析一下如何删除结点,如图:

我们需要删除红色节点,那么我们需要知道哪些信息? 答:要知道被删除节点的前一个节点。

需要怎么做? 答:假设要删除节点的前一个节点为cur,只需要cur.next = cur.next.next即可;

那如果头节点就是我们要删除的节点呢? 答:可以在删除其他节点前,用head = head.next来删除前面连续的目标节点。

这些问题都解决了,就可以开始写代码了:

js 复制代码
var removeElements = function (head, val) {
  //删除前面连续的目标节点
  while (head) {
    if (head.val === val) {
      head = head.next;
    } else {
      break;
    }
  }
  //链表是否为空
  if (!head) return head; 
  //删除后续节点
  let cur = head;
  while (cur.next) {
    if (cur.next.val === val) {
      cur.next = cur.next.next;
      continue;
    }
    cur = cur.next;
  }
  return head;
};

这样虽然AC了,但是还不够优雅,因为你需要特殊去处理头节点为目标节点的情况。

为此,我们可以统一操作,引入哨兵(sentinel)节点,如图:

这样链表中的所有节点都有了指向它的节点,这样操作更加统一,也减少了我们需要考虑的边界情况:

js 复制代码
var removeElements = function (head, val) {
  const sentinel = new ListNode(0, head);
  let cur = sentinel;
  while (cur.next) {
    if (cur.next.val === val) {
      cur.next = cur.next.next;
      continue;
    }
    cur = cur.next;
  }
  return sentinel.next;
};

LeetCode-206.反转链表

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

对于链表1-->2-->3-->4 经过翻转变为4-->3-->2-->1;

对于链表题目,我十分推荐画图模拟分析:

如图所示,我们反转链表时一定需要两个节点的信息,从而让cur指向pre,但是你会发现,节点2和节点3断开了,我们再也没办法找到下一个节点了,所以cur的下一个节点也是我们需要的,那么重新梳理一下流程:

如上图所示:

  1. cur.next存为temp
  2. cur.next = pre
  3. pre.next = null
  4. 移动到下一个位置:pre = cur; cur = temp;

emmmm.... 步骤3中为什么指向null呢?后面的操作也需要如此嘛,梳理一下发现只有对于第一遍反转时才需要指向null,对于这种因为头节点而产生的副作用,我们都去考虑引入哨兵节点 ,如果引入了我们就不再需要步骤3了,最后只要令哨兵节点为null即可。

但是,这里令哨兵节点为null不可行,说是链表中存在环,希望大家能够帮我解答一下👍。

因此,由于代码中不存在修改pre.next的操作,因此直接令prenull即可。

js 复制代码
var reverseList = function (head) {
  let pre = null;
  let cur = head;
  //对于只有0个或一个节点时
  if (!cur || !cur.next) { return head; }

  while (cur) {
    let temp = cur.next;
    cur.next = pre;
    //更新位置
    pre = cur;
    cur = temp;
  }

  return pre;
};

LeetCode-24.两两交换链表中的节点

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

对于链表1-->2-->3-->4 经过翻转变为2-->1-->4-->3; 对于链表1-->2-->3 经过翻转变为2-->1-->3;

不说了,看图:

我们需要的节点是需要交换的两个节点的前一个节点(需要交换3,4,则需要1,3),那对于头节点又会出现边界情况,那就引入哨兵节点,代码如下:

js 复制代码
var swapPairs = function (head) {
  let sentinel = new ListNode(0, head);
  let pre = sentinel;
  let cur = pre.next;
  //头节点为空
  if (!cur) return head;

  //根据节点数量在奇数和偶数时,最后cur指针位置不同来决定while条件
  while (cur && cur.next) {
    pre.next = cur.next;
    cur.next = cur.next.next;
    pre.next.next = cur;
    //更新位置
    pre = pre.next.next;
    cur = pre.next;
  }
  return sentinel.next;
};

LeetCode-19.删除链表的倒数第 N 个结点

这道题又是一个删除链表节点的题,这道题的技巧在于如何找到链表倒数第N个节点的前驱节点。由于链表是单向的,因此我们无法回退。

但是,我们只需要两个指针,两个指针之间相距N步,那么当快指针移动到尾部时,慢指针移动到了倒数第N-1个节点了。

js 复制代码
var removeNthFromEnd = function (head, n) {
  let fast = head;
  let slow = head;
  for (let i = 0; i < n; i++) {
    fast = fast.next;
  }
  //如果删除的是第一个元素,即倒数第size个,那么fast前移size步会指向null
  if (!fast) return head.next;

  while (fast.next) {
    fast = fast.next;
    slow = slow.next;
  }

  slow.next = slow.next.next;
  return head;
};

考虑特殊情况:如果链表长度为3,要删除倒数第三个,那么我们fast指针向前3步就会指向null了,那么后续while循环就会报错,所以需要进行特判,又是头节点。上哨兵试试:

js 复制代码
//哨兵
var removeNthFromEnd = function (head, n) {
  const sentinel = new ListNode(0, head);
  let fast = sentinel;
  let slow = sentinel;
  for (let i = 0; i < n; i++) {
    fast = fast.next;
  }

  while (fast.next) {
    fast = fast.next;
    slow = slow.next;
  }

  slow.next = slow.next.next;
  //因为头节点可能被删除了,因为返回哨兵节点的next
  return sentinel.next;
};

LeetCode-160.相交链表

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

假设链表A长度为 5 ,链表B长度为 6 ,那么相交点一定后五个节点中,我们只要得到两个链表的长度差offset,然后再让两个指针分别遍历短链表A的头部开始,长链表B的第offset个开始逐一比较即可。(注意此处比较的是节点是否相同,而不是节点的val

js 复制代码
var getIntersectionNode = function (headA, headB) {
  let lengthA = 0, lengthB = 0;
  let cur = headA;
  while (cur) {
    lengthA++;
    cur = cur.next;
  }
  cur = headB;
  while (cur) {
    lengthB++;
    cur = cur.next;
  }

  let offset = Math.abs(lengthA - lengthB);
  let longList, shorList;
  if (lengthA > lengthB) {
    longList = headA;
    shorList = headB
  } else {
    longList = headB;
    shorList = headA;
  }

  for (let i = 0; i < offset; i++) {
    longList = longList.next;
  }

  while (longList && shorList) {
    if (longList === shorList) return longList;
    longList = longList.next;
    shorList = shorList.next;
  }

  return null;
};

LeetCode-142.环形链表 II

给定一个链表的头节点 head ,返回链表开始入环的第一个节点。 如果链表无环,则返回 null

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

这个题中有两个问题:

  1. 判断是否有环?
    • 快指针每次前移两步,慢指针每次移动一步,那么快指针一定先入环,当慢指针入环时快指针此时在环中的某个节点处,此时,快指针相对于慢指针每次前移一格,因此一定会追上慢指针,那么当快慢指针相遇时就可以判断是否有环了。
  2. 入口节点如何求?
    • 如下图所示:

其中慢指针走了x+y 为什么不是 x+y+n(y+z)

因为当慢指针入环后,一定在他未走完一圈时被快指针碰到,试想,如果慢指针走了一圈,那么快指针就走了两圈,那么快指针一定在某处超过慢指针,又因为快指针相对于慢指针每次向前一步,因此不可能越过慢指针,只能是相遇后超过,因此证明了慢指针一定在入环的第一圈被快指针相遇。

然后如图中公式,我们就能用一个指针指向head,另一个指向相遇节点,共同前进,当相遇时即为环的入口了。

js 复制代码
var detectCycle = function (head) {
  let fast = head;
  let slow = head;
  //快慢指针判断是否有环
  while (fast && fast.next && fast.next.next) {
    fast = fast.next.next;
    slow = slow.next;
    if (fast === slow) break;
  }
  if (!(fast && fast.next && fast.next.next))
    return null;
  //有环
  let res = head;
  while (res !== slow) {
    res = res.next;
    slow = slow.next;
  }
  return res;
};

总结

  • 使用哨兵节点(虚拟头节点)统一链表操作,减少判断头节点可能会出现的情况。
  • 大多数情况下,我们要操作某个节点都需要他的前驱节点,例如24题中,我们需要的不是3,4而是1,3。
  • 对于相交链表、环形链表这种题目,还是要多做多积累
相关推荐
qq_589568104 分钟前
Echarts的高级使用,动画,交互api
前端·javascript·echarts
游是水里的游5 分钟前
【算法day19】回溯:分割与子集问题
算法
不想当程序猿_6 分钟前
【蓝桥杯每日一题】分糖果——DFS
c++·算法·蓝桥杯·深度优先
南城花随雪。25 分钟前
单片机:实现FFT快速傅里叶变换算法(附带源码)
单片机·嵌入式硬件·算法
dundunmm40 分钟前
机器学习之scikit-learn(简称 sklearn)
python·算法·机器学习·scikit-learn·sklearn·分类算法
古希腊掌管学习的神41 分钟前
[机器学习]sklearn入门指南(1)
人工智能·python·算法·机器学习·sklearn
波音彬要多做42 分钟前
41 stack类与queue类
开发语言·数据结构·c++·学习·算法
暴富的Tdy2 小时前
【CryptoJS库AES加密】
前端·javascript·vue.js
℘团子এ2 小时前
js和html中,将Excel文件渲染在页面上
javascript·html·excel