代码随想录刷题day3丨链表理论基础,203.移除链表元素,707.设计链表,206.反转链表
1.链表理论基础
- 文章链接:https://programmercarl.com/%E9%93%BE%E8%A1%A8%E7%90%86%E8%AE%BA%E5%9F%BA%E7%A1%80.html
1.1链表
-
概述:链表是一种通过指针串联在一起的线性结构,每一个节点由两部分组成,一个是数据域一个是指针域(存放指向下一个节点的指针),最后一个节点的指针域指向null(空指针的意思)
-
链表的入口节点称为链表的头结点也就是head
-
链表的类型
-
单链表:上面说的就是单链表
-
双链表:每一个节点有两个指针域,一个指向下一个节点,一个指向上一个节点。既可以向前查询也可以向后查询。
-
循环链表:链表首尾相连。可以用来解决约瑟夫环问题。
-
约瑟夫问题:
-
约瑟夫问题是个著名的问题:N个人围成一圈,第一个人从1开始报数,报M的将被杀掉,下一个人接着从1开始报。如此反复,最后剩下一个,求最后的胜利者。
例如只有三个人,把他们叫做A、B、C,他们围成一圈,从A开始报数,假设报2的人被杀掉。首先A开始报数,他报1。侥幸逃过一劫。
然后轮到B报数,他报2。非常惨,他被杀了
C接着从1开始报数
接着轮到A报数,他报2。也被杀死了。
最终胜利者是C -
解决方案:
-
暴力解法:N个人看作是N个链表节点,节点1指向节点2,节点2指向节点3,……,节点N-1指向节点N,节点N指向节点1,这样就形成了一个环。然后从节点1开始1、2、3……往下报数,每报到M,就把那个节点从环上删除。下一个节点接着从1开始报数。最终链表仅剩一个节点。它就是最终的胜利者。
-
公式法:
-
为了方便导出递推公式,重新定义问题: N个人编号为1,2,……,N,依次报数,每报到M时,杀掉那个人,求最后胜利者的编号?
-
递推公式:f(N,M)=(f(N−1,M)+M)%N
-
f(N,M)表示,N个人报数,每报到M时杀掉那个人,最终胜利者的编号
-
f(N−1,M)表示,N-1个人报数,每报到M时杀掉那个人,最终胜利者的编号
-
-
-
-
-
-
-
-
链表的存储方式
- 链表是通过指针域的指针链接在内存中各个节点。所以链表中的节点在内存中不是连续分布的 ,而是散乱分布在内存中的某地址上,分配机制取决于操作系统的内存管理。
1.2链表的操作
1.2.1删除节点
-
删除D节点,如图所示:只要将C节点的next指针 指向E节点就可以了。
1.2.2添加节点
-
添加F节点,如图所示:
1.3性能分析
插入/删除(时间复杂度) | 查询(时间复杂度) | 适用场景 | |
---|---|---|---|
数组 | O(n) | O(1) | 数据量固定,频繁查询,较少增删 |
链表 | O(1) | O(n) | 数据量不固定,频繁增删,较少查询 |
- 数组在定义的时候,长度就是固定的,如果想改动数组的长度,就需要重新定义一个新的数组。
- 链表的长度可以是不固定的,并且可以动态增删, 适合数据量不固定,频繁增删,较少查询的场景。
1.4定义链表节点
public class ListNode {
// 结点的值
int val;
// 下一个结点
ListNode next;
// 节点的构造函数(无参)
public ListNode() {
}
// 节点的构造函数(有一个参数)
public ListNode(int val) {
this.val = val;
}
// 节点的构造函数(有两个参数)
public ListNode(int val, ListNode next) {
this.val = val;
this.next = next;
}
}
2.题目
2.1移除链表元素
-
题目链接:https://leetcode.cn/problems/remove-linked-list-elements/description/
-
视频讲解:https://www.bilibili.com/video/BV18B4y1s7R9/?vd_source=1651d2836ebdf1a7446e95eebf87f882
-
文档讲解:https://programmercarl.com/0203.%E7%A7%BB%E9%99%A4%E9%93%BE%E8%A1%A8%E5%85%83%E7%B4%A0.html
-
解题思路:
-
法一:直接使用原来的链表来进行删除操作。
-
移除头结点和移除其他节点的操作是不一样的,因为链表的其他节点都是通过前一个节点来移除当前节点,而头结点没有前一个节点。
-
所以头结点如何移除呢,其实只要将头结点向后移动一位就可以,这样就从链表中移除了一个头结点。
-
举例:
- 移除头结点
-
移除其它节点
-
-
法二:设置一个虚拟头结点在进行删除操作。这样原链表的所有节点就都可以按照统一的方式进行移除了。
- 最后呢在题目中,return 头结点的时候,别忘了
return dummyNode->next;
, 这才是新的头结点
- 最后呢在题目中,return 头结点的时候,别忘了
-
-
代码:
-
用原来的链表操作:
//单链表 /** * Definition for singly-linked list. * public class ListNode { * int val; * ListNode next; * ListNode() {} * ListNode(int val) { this.val = val; } * ListNode(int val, ListNode next) { this.val = val; this.next = next; } * } */ class Solution { public ListNode removeElements(ListNode head, int val) { while (head != null && head.val == val) { head = head.next; } if (head == null) { return head; } ListNode cur = head; while (cur != null && cur.next != null) { if (cur.next.val == val) { cur.next = cur.next.next; } else { cur = cur.next; } } return head; } }
-
设置一个虚拟头节点:
/** * Definition for singly-linked list. * public class ListNode { * int val; * ListNode next; * ListNode() {} * ListNode(int val) { this.val = val; } * ListNode(int val, ListNode next) { this.val = val; this.next = next; } * } */ class Solution { public ListNode removeElements(ListNode head, int val) { // 设置一个虚拟的头结点 ListNode dummy = new ListNode(); dummy.next = head; ListNode cur = dummy; while(cur.next != null){ if(cur.next.val == val){ cur.next = cur.next.next; }else{ cur = cur.next; } } return dummy.next; } }
-
-
总结:做题时最好画下草图,方便理解。关键在于理解指针如何移动,定义动态指针cur实现遍历操作。使用虚拟头结点会更方便。
2.2设计链表
-
题目链接:https://leetcode.cn/problems/design-linked-list/description/
-
视频讲解:https://www.bilibili.com/video/BV1FU4y1X7WD/?vd_source=1651d2836ebdf1a7446e95eebf87f882
-
文档讲解:https://programmercarl.com/0707.%E8%AE%BE%E8%AE%A1%E9%93%BE%E8%A1%A8.html
-
解题思路:
- 题目大致意思分为以下几个:(n从0开始)
- 获取第n个节点的值(第0个节点就是head节点)
- 头部插入节点
- 尾部插入节点
- 第n个节点前插入节点
- 删除第n个节点
- 该题统一使用虚拟头结点的方式
- 题目大致意思分为以下几个:(n从0开始)
-
代码:
//单链表 //时间复杂度: 涉及 index 的相关操作为 O(index), 其余为 O(1) //空间复杂度: O(n) class ListNode{ int val; ListNode next; ListNode(){ } ListNode(int val){ this.val = val; } } class MyLinkedList { //size存储链表元素的个数 int size; //虚拟头节点 ListNode dummyhead; //初始化链表 public MyLinkedList() { size = 0; dummyhead = new ListNode(); } //获取第index个节点的数值,注意index是从0开始的,第0个节点就是头结点 public int get(int index) { //如果index非法,返回 -1 if(index < 0 || index >= size){ return -1; } ListNode cur = dummyhead; for(int i = 0;i <= index;i++){ cur = cur.next; } return cur.val; } public void addAtHead(int val) { ListNode newNode = new ListNode(val); newNode.next = dummyhead.next; dummyhead.next = newNode; size++; } public void addAtTail(int val) { ListNode newNode1 = new ListNode(val); ListNode cur = dummyhead; while(cur.next != null){ cur = cur.next; } cur.next = newNode1; size++; } public void addAtIndex(int index, int val) { if(index < 0 || index > size){ return; } ListNode newNode2 = new ListNode(val); ListNode cur = dummyhead; for(int i = 0;i < index;i++){ cur = cur.next; } newNode2.next = cur.next; cur.next = newNode2; size++; } public void deleteAtIndex(int index) { if(index < 0 || index >= size){ return; } ListNode cur = dummyhead; for(int i = 0;i < index;i++){ cur = cur.next; } cur.next = cur.next.next; size--; } }
-
总结:
- cur 的位置非常重要
2.3反转链表
-
题目链接:https://leetcode.cn/problems/reverse-linked-list/description/
-
视频讲解:https://www.bilibili.com/video/BV1nB4y1i7eL/?vd_source=1651d2836ebdf1a7446e95eebf87f882
-
文档讲解:https://programmercarl.com/0206.%E7%BF%BB%E8%BD%AC%E9%93%BE%E8%A1%A8.html
-
解题思路:双指针解法(建议)或者递归法
-
如果再定义一个新的链表,实现链表元素的反转,其实这是对内存空间的浪费。
-
其实只需要改变链表的next指针的指向,直接将链表反转 ,而不用重新定义一个新的链表,如图所示:
-
之前链表的头节点是元素1, 反转之后头结点就是元素5 ,这里并没有添加或者删除节点,仅仅是改变next指针的方向。
-
步骤:
- 首先定义一个cur指针,指向头结点,再定义一个pre指针,初始化为null。
- 然后就要开始反转了,首先要把 cur->next 节点用tmp指针保存一下,也就是保存一下这个节点。
- 为什么要保存一下这个节点呢,因为接下来要改变 cur->next 的指向了,将cur->next 指向pre ,此时已经反转了第一个节点了。
- 接下来,就是循环走如下代码逻辑了,继续移动pre和cur指针。
- 最后,cur 指针已经指向了null,循环结束,链表也反转完毕了。 此时我们return pre指针就可以了,pre指针就指向了新的头结点。
-
-
代码:
-
双指针解法
/** * Definition for singly-linked list. * public class ListNode { * int val; * ListNode next; * ListNode() {} * ListNode(int val) { this.val = val; } * ListNode(int val, ListNode next) { this.val = val; this.next = next; } * } */ //时间复杂度: O(n) //空间复杂度: O(1) class Solution { public ListNode reverseList(ListNode head) { ListNode cur = head; ListNode pre = null; ListNode temp = null; while(cur != null){ temp = cur.next; cur.next = pre; pre = cur; cur = temp; } return pre; } }
-
递归法
//时间复杂度: O(n) //空间复杂度: O(n) /** * Definition for singly-linked list. * public class ListNode { * int val; * ListNode next; * ListNode() {} * ListNode(int val) { this.val = val; } * ListNode(int val, ListNode next) { this.val = val; this.next = next; } * } */ class Solution { public ListNode reverseList(ListNode head) { return reverse(null,head); } private ListNode reverse(ListNode pre,ListNode cur){ if(cur == null){ return pre; } ListNode temp = null; temp = cur.next;// 先保存下一个节点 cur.next = pre;// 反转 // 更新prev、cur位置 // prev = cur; // cur = temp; return reverse(cur,temp); } }
-
-
总结:
-
temp 作为中间量保存 cur
-
终止条件就是 cur= null时
-
递归法相对抽象一些,但是其实和双指针法是一样的逻辑,同样是当cur为空的时候循环结束,不断将cur指向pre的过程。
-
分析总结图
-