链表的理论基础

在这篇文章关于链表,你该了解这些!中,介绍了如下几点:

  • 链表的种类主要为:单链表,双链表,循环链表
  • 链表的存储方式:链表的节点在内存中是分散存储的,通过指针连在一起。
  • 链表是如何进行增删改查的。
  • 数组和链表在不同场景下的性能分析。

可以说把链表基础的知识都概括了,但又不像教科书那样的繁琐

链表经典题目

虚拟头结点

讲解了链表操作中一个非常总要的技巧:虚拟头节点。

每次对应头结点的情况都要单独处理,所以使用虚拟头结点的技巧,就可以解决这个问题

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

/*** 方法:哨兵节点。哨兵节点将被用于伪头。初始化两个指针 curr 和 prev 指向当前节点和前继节点。** 算法:万一头节点==val,所以需要有一个虚拟的节点来辅助遍历。* 初始化哨兵节点为 ListNode(0) 且设置 sentinel.next = head。* 初始化两个指针 curr 和 prev 指向当前节点和前继节点。* 当 curr != nullptr;*    比较当前节点和要删除的节点:*        若当前节点就是要删除的节点:则 prev.next = curr.next。*        否则设 prve = curr。*    遍历下一个元素:curr = curr.next。* 返回 sentinel.next。
*/
class Solution {public ListNode removeElements(ListNode head, int val) {ListNode sentinel = new ListNode(0);sentinel.next = head;ListNode prev = sentinel, curr = head;while (curr != null) {if (curr.val == val) {prev.next = curr.next;}else {prev = curr;}curr = curr.next;}return sentinel.next;}}

复杂度分析

  • 时间复杂度:O(N),只遍历了一次。
  • 空间复杂度:O(1)。

链表的基本操作

在链表:一道题目考察了常见的五个操作!中,我们通设计链表把链表常见的五个操作练习了一遍。

这是练习链表基础操作的非常好的一道题目,考察了:

  • 获取链表第index个节点的数值
  • 在链表的最前面插入一个节点
  • 在链表的最后面插入一个节点
  • 在链表第index个节点前面插入一个节点
  • 删除链表的第index个节点的数值

707-设计链表的实现

/*** 定义一个单链表:找到目标ListNode的前一个ListNode复杂度分析时间复杂度:addAtHead: O(1)addAtInder,get,deleteAtIndex: O(k),其中 k 指的是元素的索引。addAtTail:O(N),其中 N 指的是链表的元素个数。空间复杂度:所有的操作都是 O(1)*/
public class ListNode{int val;ListNode next;ListNode(int x){val = x;next = null;}
}class MyLinkedList {int size;ListNode head;  // sentinel node as pseudo-head/** Initialize your data structure here. */public MyLinkedList() {size = 0;head = new ListNode(0);}/** Get the value of the index-th node in the linked list. If the index is invalid, return -1. */public int get(int index) {// if index is invalidif (index < 0 || index >= size){return -1;}ListNode curr = head;// index steps needed// to move from sentinel node to wanted indexfor (int i = 0; i < index + 1 ;i++){curr = curr.next;}return curr.val;}/** Add a node of value val before the first element of the linked list. After the insertion, the new node will be the first node of the linked list. */public void addAtHead(int val) {addAtIndex(0,val);}/** Append a node of value val to the last element of the linked list. */public void addAtTail(int val) {addAtIndex(size,val);}/** Add a node of value val before the index-th node in the linked list. If index equals to the length of linked list, the node will be appended to the end of linked list. If index is greater than the length, the node will not be inserted. */public void addAtIndex(int index, int val) {// If index is greater than the length,// the node will not be inserted.if (index > size) return;// [so weird] If index is negative,// the node will be inserted at the head of the list.if (index < 0) index = 0;++size;// find predecessor of the node to be addedListNode pred = head;for (int i = 0; i < index; i++) {pred = pred.next;}// node to be addedListNode toAdd = new ListNode(val);// insertion itselftoAdd.next = pred.next;pred.next = toAdd;}/** Delete the index-th node in the linked list, if the index is valid. */public void deleteAtIndex(int index) {// if the index is invalid, do nothingif (index < 0 || index >= size) return;size--;// find predecessor of the node to be deletedListNode pred = head;for(int i = 0; i < index; ++i){pred = pred.next;}// delete pred.nextpred.next = pred.next.next;}
}
/*** 双向链表,主要找前后的两个ListNode复杂度分析时间复杂度:addAtHead,addAtTail:O(1)get,addAtIndex,delete:O(min(k,N−k)),其中 k 指的是元素的索引。空间复杂度:所有的操作都是 O(1)*/
public class ListNode{int val;ListNode next;ListNode prev;ListNode(int x){val = x;}
}class MyLinkedList {int size;ListNode head,tail; // sentinel nodes as pseudo-head and pseudo-tail/** Initialize your data structure here. */public MyLinkedList() {size = 0;ListNode head = new ListNode(0);ListNode tail = new ListNode(0);head.next = tail;tail.prev = head;}/** Get the value of the index-th node in the linked list. If the index is invalid, return -1. */public int get(int index) {if (index < 0 || index >= size )return -1;// choose the fastest way: to move from the head// or to move from the tailListNode curr = head;//双向查找,类似于二分法if (index + 1 < size - index){for (int i = 0; i < index + 1; i++) {curr = curr.next;}else{curr = tail;for (int i = 0; i < size - index ; i++) {curr = curr.prev;}}return curr.val;}/** Add a node of value val before the first element of the linked list. After the insertion, the new node will be the first node of the linked list. */public void addAtHead(int val) {ListNode pred = head,succ = head.next;++size;ListNode toAdd = new ListNode(val);toAdd.prev = pred;toAdd.next = succ;pred.next = toAdd;succ.prev = toAdd;}/** Append a node of value val to the last element of the linked list. */public void addAtTail(int val) {ListNode succ = tail, pred = tail.prev;++size;ListNode toAdd = new ListNode(val);toAdd.prev = pred;toAdd.next = succ;succ.prev = toAdd;pred.next = toAdd;}/** Add a node of value val before the index-th node in the linked list. If index equals to the length of linked list, the node will be appended to the end of linked list. If index is greater than the length, the node will not be inserted. */public void addAtIndex(int index, int val) {// If index is greater than the length,// the node will not be inserted.if (index > size) return;// [so weird] If index is negative,// the node will be inserted at the head of the list.if (index < 0) index = 0;// find predecessor and successor of the node to be addedListNode pred, succ;if (index < size - index){pred = head;for(int i = 0; i < index; ++i) pred = pred.next;succ = pred.next;}else {succ = tail;for (int i = 0; i < size - index; ++i) succ = succ.prev;pred = succ.prev;}// insertion itself++size;ListNode toAdd = new ListNode(val);toAdd.prev = pred;toAdd.next = succ;pred.next = toAdd;succ.prev = toAdd;}}/** Delete the index-th node in the linked list, if the index is valid. */public void deleteAtIndex(int index) {// if the index is invalid, do nothingif (index < 0 || index >= size) return;// find predecessor and successor of the node to be deletedListNode pred, succ;if (index < size - index) {pred = head;for(int i = 0; i < index; ++i) pred = pred.next;succ = pred.next.next;}else {succ = tail;for (int i = 0; i < size - index - 1; ++i) succ = succ.prev;pred = succ.prev.prev;}--size;pred.next = succ;succ.prev = pred;}
}

反转链表

在链表:听说过两天反转链表又写不出来了?中,讲解了如何反转链表。

206-反转一个单链表。

/**方法一:迭代 : 假设链表为 1→2→3→∅,我们想要把它改成 ∅←1←2←3。在遍历链表时,将当前节点的 next 指针改为指向前一个节点。由于节点没有引用其前一个节点,因此必须事先存储其前一个节点。在更改引用之前,还需要存储后一个节点。最后返回新的头引用*/
class Solution {public ListNode reverseList(ListNode head) {ListNode pre = null;ListNode curr = head;while (curr != null){ListNode temp = curr.next;curr.next = pre;pre = curr;curr = temp;}return pre;}
}

复杂度分析

  • 时间复杂度:O(n),其中 n是链表的长度。需要遍历链表一次。
  • 空间复杂度:O(1)。

环形链表

在链表:环找到了,那入口呢?中,讲解了在链表如何找环,以及如何找环的入口位置。

这道题目可以说是链表的比较难的题目了。

很多同学关注的问题是:为什么一定会相遇,快指针就不能跳过慢指针么?

可以确定如下两点:

  • fast指针一定先进入环中,如果fast 指针和slow指针相遇的话,一定是在环中相遇,这是毋庸置疑的。
  • fast和slow都进入环里之后,fast相对于slow来说,fast是一个节点一个节点的靠近slow的,注意是相对运动,所以fast一定可以和slow重合

如果fast是一次走三个节点,那么可能会跳过slow,因为相对于slow来说,fast是两个节点移动的。

确定有否有环比较容易,但是找到环的入口就不太容易了,需要点数学推理。

我在链表:环找到了,那入口呢?中给出了详细的推理,兼顾易懂和简洁了。

这是一位录友在评论区有一个疑问,感觉这个问题很不错,但评论区根本说不清楚,我就趁着总结篇,补充一下这个证明。

在推理过程中,为什么第一次在环中相遇,slow的 步数 是 x+y 而不是 x + 若干环的长度 + y 呢?

了解这个问题一定要先把文章链表:环找到了,那入口呢?看了,即文章中如下的地方:

首先slow进环的时候,fast一定是先进环来了。

如果slow进环入口,fast也在环入口,那么把这个环展开成直线,就是如下图的样子:

可以看出如果slow 和 fast同时在环入口开始走,一定会在环入口3相遇,slow走了一圈,fast走了两圈。

重点来了,slow进环的时候,fast一定是在环的任意一个位置,如图:

那么fast指针走到环入口3的时候,已经走了k + n 个节点,slow相应的应该走了(k + n) / 2 个节点。

因为k是小于n的(图中可以看出),所以(k + n) / 2 一定小于n。

也就是说slow一定没有走到环入口3,而fast已经到环入口3了

这说明什么呢?

在slow开始走的那一环已经和fast相遇了

那有同学又说了,为什么fast不能跳过去呢? 在刚刚已经说过一次了,fast相对于slow是一次移动一个节点,所以不可能跳过去

141-给定一个链表,判断链表中是否有环。

/*** 方法一:哈希表* 最容易想到的方法是遍历所有节点,每次遍历到一个节点时,判断该节点此前是否被访问过。* 我们可以使用哈希表来存储所有已经访问过的节点。* 每次我们到达一个节点,如果该节点已经存在于哈希表中,则说明该链表是环形链表,* 否则就将该节点加入哈希表中。重复这一过程,直到我们遍历完整个链表即可。**/
public class Solution {public boolean hasCycle(ListNode head) {Set<ListNode> set = new HashSet<ListNode>();while (head != null){if (!set.add(head)){return true;}head = head.next;}return false;}
}

复杂度分析

时间复杂度:O(N),其中 N 是链表中的节点数。最坏情况下我们需要遍历每个节点一次。

空间复杂度:O(N),其中 N 是链表中的节点数。主要为哈希表的开销,最坏情况下我们需要将每个节点插入到哈希表中一次。

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

/***方法一:哈希表* 我们遍历链表中的每个节点,并将它记录下来;一旦遇到了此前遍历过的节点,就可以判定链表中存在环。*  * 时间复杂度 O(N)*  * 空间复杂度 O(N)*/
public class Solution {public ListNode detectCycle(ListNode head) {ListNode pos = head;Set<ListNode> set = new HashSet<ListNode>();while (pos != null){if (set.contains(pos)){return pos;}else{set.add(pos);}pos = pos.next;}return null;}
}
/*** 方法二:快慢指针*我们使用两个指针,fast 与 slow。它们起始都位于链表的头部。* 随后,slow 指针每次向后移动一个位置,而 fast 指针向后移动两个位置。* 如果链表中存在环,则 fast 指针最终将再次与 slow 指针在环中相遇。** 需要构造两次相遇,第一次相遇是检查是否有环,第二次是找到环的入口。* 时间复杂度 O(N)* 空间复杂度 O(1)  我们只使用了 slow,fast,ptr 三个指针*/public class Solution {public ListNode detectCycle(ListNode head) {ListNode fast = head,slow = head;while (true){if (fast == null || fast.next == null) return null;fast.next = fast.next.next;slow = slow.next;if (fast == slow) break;}fast = head;while (slow != fast){slow = slow.next;fast = fast.next;}return fast;}
}

leetcode系列-链表相关推荐

  1. leetcode系列-206.反转链表

    leetcode系列–第206题.反转链表 这道题很经典 给你单链表的头节点 head ,请你反转链表,并返回反转后的链表. 示例 1: 输入:head = [1,2,3,4,5] 输出:[5,4,3 ...

  2. leetcode系列--234.回文链表

    leetcode系列–第234题.回文链表 给你一个单链表的头节点 head ,请你判断该链表是否为回文链表.如果是,返回 true :否则,返回 false . 输入:head = [1,2,2,1 ...

  3. sv队列和动态数组的区别_Go 刷 LeetCode 系列:经典(7) 设计双端队列

    设计实现双端队列. 你的实现需要支持以下操作: MyCircularDeque(k):构造函数,双端队列的大小为k.insertFront():将一个元素添加到双端队列头部.如果操作成功返回 true ...

  4. 二叉搜索树的中序遍历为 递增序列_Go 刷 Leetcode 系列:恢复二叉搜索树

    二叉搜索树中的两个节点被错误地交换. 请在不改变其结构的情况下,恢复这棵树. 示例 1: 输入: [1,3,null,null,2] 1 / 3 \ 2输出: [3,1,null,null,2] 3 ...

  5. 二叉树和等于某值路径_Go刷LeetCode系列:二叉树(3)二叉树路径和

    给定一个二叉树和一个目标和,判断该树中是否存在根节点到叶子节点的路径,这条路径上所有节点值相加等于目标和. 说明: 叶子节点是指没有子节点的节点. 示例:  给定如下二叉树,以及目标和 sum = 2 ...

  6. leetcode系列-844.比较含退格的字符串

    leetcode系列–第844题.比较含退格的字符串 给定 s 和 t 两个字符串,当它们分别被输入到空白的文本编辑器后,如果两者相等,返回 true .# 代表退格字符.注意:如果对空文本输入退格字 ...

  7. leetcode系列--680.验证回文字符串 Ⅱ

    leetcode系列–第680题.验证回文字符串 Ⅱ 给定一个非空字符串 s,最多删除一个字符.判断是否能成为回文字符串. 输入: s = "abca" 输出: true 解释: ...

  8. leetcode系列-11.盛最多水的容器

    leetcode系列–第11题.盛最多水的容器 给定一个长度为 n 的整数数组 height .有 n 条垂线,第 i 条线的两个端点是 (i, 0) 和 (i, height[i]) .找出其中的两 ...

  9. leetcode系列-383.赎金信

    leetcode系列–第383题.赎金信 给你两个字符串:ransomNote 和 magazine ,判断 ransomNote 能不能由 magazine 里面的字符构成.如果可以,返回 true ...

最新文章

  1. MindSpore图像分类模型支持(Lite)
  2. hdoj5792 【树状数组】【未完待续】
  3. STM32 电机教程 30 - 无刷无感入门2
  4. JZOJ 5385. 【NOIP2017提高A组模拟9.23】Carry
  5. 记一次.net core 集成vue 实践
  6. 荷兰国旗问题(分三块)
  7. UE3 内存使用和分析
  8. java des 0填充方式_DES填充方式与初始向量IV的作用
  9. Word没有到一行自动换行如何解决
  10. Minitab 控制图
  11. JavaScript开发必备!这四款静态代码分析工具你了解吗
  12. 迪文串口屏TTL与主控板RS232电平信号转换方案
  13. Java输入小数和整数求COSx_嗖嗖移动大厅 源代码 Java初级小项目
  14. R语言ggplot2 | 如何自定义facet分面的坐标轴范围
  15. 验证微信号的正则表达式
  16. html中diy的背景怎么透明,自制复古几何无缝纹案背景_html/css_WEB-ITnose
  17. 研发效能——如何提高?
  18. choco 使用详解--window
  19. 要怎么在计算机里清除桌面内存,怎么清理运行内存占用_怎么清理电脑运行内存-win7之家...
  20. 在Matlab中将一幅图片的中心设置为x-y轴坐标原点

热门文章

  1. SAP FI-CO总账科目简析
  2. html盒子在始终在左侧,div盒子在一行方法(左中右结构 CSS布局)
  3. MySQL 错误【四】Value ‘0000-00-00 00:00:00’ can not be represented as java.sql.Timestamp
  4. Pell方程初识以及技巧性的求解Pell方程整数解(附带手推详细证明)
  5. 桌面文件突然不见了怎么恢复?
  6. JS的 验证组织机构的合法性
  7. 命名数据网络NDN中的概念小总结
  8. 2022-2028年全球及中国倍频器行业投资前景分析
  9. (29)Verilog实现倍频【方法二】
  10. 笔记本电脑能连接WiFi但浏览器无法打开网页的解决办法