Day 3 第二章 链表part01
- 今日任务
- 链表理论基础; 203.移除链表元素; 707.设计链表; 206.反转链表
链表理论基础
- 建议:了解一下链接基础,以及链表和数组的区别
- 文章链接:https://programmercarl.com/%E9%93%BE%E8%A1%A8%E7%90%86%E8%AE%BA%E5%9F%BA%E7%A1%80.html
1. 什么是链表
- 链表是以节点的方式来存储,是链式存储
- 每个节点包含data域,next域(指向下一个节点)
- 链表的各个节点不一定是连续存储
- 链表分带头节点的链表和不带头节点的列表(根据实际的需求来确定)
链表是一种通过指针串联在一起的线性结构,每一个节点由两部分组成,一个是数据域一个是指针域(存放指向下一个节点的指针),最后一个节点的指针域指向null(空指针的意思)。
链表的入口节点称为链表的头结点也就是head。
逻辑结构如图所示:
2. 链表的类型
单链表
- 单链表中的指针域只能指向节点的下一个节点。
双链表
- 双链表:每一个节点有两个指针域,一个指向下一个节点,一个指向上一个节点。
- 双链表 既可以向前查询也可以向后查询。
如图所示:
循环链表
- 循环链表,顾名思义,就是链表首尾相连。
- 循环链表可以用来解决约瑟夫环问题。
3. 链表的存储方式
- 数组是在内存中是连续分布的,但是链表在内存中可不是连续分布的。
- 链表是通过指针域的指针链接在内存中各个节点。
- 所以链表中的节点在内存中不是连续分布的 ,而是散乱分布在内存中的某地址上,分配机制取决于操作系统的内存管理。
这个链表起始节点为2, 终止节点为7, 各个节点分布在内存的不同地址空间上,通过指针串联在一起。
4. 链表的定义
这是因为平时在刷leetcode的时候,链表的节点都默认定义好了,直接用就行了,所以同学们都没有注意到链表的节点是如何定义的。
而在面试的时候,一旦要自己手写链表,就写的错漏百出。
import java.util.List;
//ListNode代表一个节点
class ListNode{
public int val;
public ListNode next;
//构造函数
public ListNode(int a){
this.val = a;
}
}
//创建链表
public class MyLinkedList {
public ListNode head;//链表的头
//方法一:枚举法(直接进行val的赋值以及对next的初始化)
//不用对最后一个节点的next进行赋值,因为next是引用类型,不赋值则默认为null。
public void creatList() {
ListNode listNode1 = new ListNode(11);
ListNode listNode2 = new ListNode(22);
ListNode listNode3 = new ListNode(33);
ListNode listNode4 = new ListNode(44);
ListNode listNode5 = new ListNode(55);
this.head = listNode1;
listNode1.next = listNode2;
listNode2.next = listNode3;
listNode3.next = listNode4;
listNode4.next = listNode5;
}
//方法二:头插法
/*头插法是指在链表的头节点的位置插入一个新节点,定义一个node表示该节点
然后就是对node的next进行赋值,用node.next = this.head即可完成
(注意:head应指向新节点)*/
public void addFirst(int data) {
ListNode node = new ListNode(data);
node.next = this.head;
this.head = node;
/*if(this.head == null){
this.head = node;
}else{
node.next = this.head;
this.head = node;
}*/
}
//方法三:尾插法
/*尾插法是指在链表的尾节点的位置插入一个新节点,定义一个node表示该节点
然后就是对原来最后一个节点的next进行赋值
先将head移动至原来最后一个节点,用head.next = node进行赋值
(注意:如果链表不为空,需要定义cur来代替head)*/
public void addLast(int data) {
ListNode node = new ListNode(data);
if (this.head == null) {
this.head = node;
} else {
ListNode cur = this.head;
while (cur.next != null) {
cur = cur.next;
}
cur.next = node;
}
}
//打印顺序表
/*认识了链表的结构,我们可以知道,节点与节点之间通过next产生联系。
并且我们已将创建了head,即头节点的地址,通过head的移动来实现链表的打印。
注意:为了使head一直存在且有意义,
我们在display()函数中定义一个cur:ListNode cur = this.head;来替代head。
对于head的移动,可用head = head.next来实现*/
public void display() {
ListNode cur = this.head;
while (cur != null) {
System.out.print(cur.val + " ");
cur = cur.next;
}
System.out.println();
}
//查找是否包含关键字key是否在单链表当中
//查找key,可以利用head移动,实现对于key的查找(**注意**:同样要定义一个cur来代替head)
public boolean contains(int key) {
ListNode cur = this.head;
while (cur != null) {
if (cur.val == key) {
return true;
}
cur = cur.next;
}
return false;
}
//得到单链表的长度
//定义计数器count = 0,通过head的移动来判断链表长度(**注意**:同样要定义一个cur来代替head)
public int Size() {
int count = 0;
ListNode cur = this.head;
while (cur != null) {
count++;
cur = cur.next;
}
return count;
}
//找到index位置的前一位置的地址
public ListNode findIndex(int index) {
ListNode cur = head.next;
while (index - 1 != 0) {
cur = cur.next;
index--;
}
return cur;
}
//任意位置插入,第一个数据节点为0号下标
public void addIndex(int index, int data) {
if (index < 0 || index > Size()) {
return;
}
if (index == 0) { //相当于头插法
addFirst(data);
return;
}
if (index == Size()) { //相当于尾插法
addLast(data);
return;
}
ListNode cur = findIndex(index);//找到index位置前一位置的地址
ListNode node = new ListNode(data);//初始化node
node.next = cur.next;
cur.next = node;
}
//找到key的前驱(前一节点)
public ListNode searchPrev(int key) {
ListNode cur = this.head;
while (cur.next != null) {
if (cur.next.val == key) {
return cur;
}
cur = cur.next;
}
return null;
}
//删除第一次出现关键字为key的节点
public void remove(int key) {
if (this.head == null) {
return;
}
if (this.head.val == key) {
this.head = this.head.next;
return;
}
ListNode cur = searchPrev(key);
if (cur == null) {
return; //没有要删除的节点
}
ListNode del = cur.next;//定义要删除的节点
cur.next = del.next;
}
//清空链表
public void clear() {
while (this.head != null) {
ListNode curNext = this.head.next;
this.head.next = null;
this.head = curNext;
}
}
}
5. 链表的操作
删除节点
删除D节点,如图所示:
只要将C节点的next指针 指向E节点就可以了。
那有同学说了,D节点不是依然存留在内存里么?只不过是没有在这个链表里而已。
是这样的,所以在C++里最好是再手动释放这个D节点,释放这块内存。
其他语言例如Java、Python,就有自己的内存回收机制,就不用自己手动释放了。
添加节点
如图所示:
可以看出链表的增添和删除都是O(1)操作,也不会影响到其他节点。
但是要注意,要是删除第五个节点,需要从头节点查找到第四个节点通过next指针进行删除操作,查找的时间复杂度是O(n)。
6. 链表 & 数组
再把链表的特性和数组的特性进行一个对比,如图所示:
数组在定义的时候,长度就是固定的,如果想改动数组的长度,就需要重新定义一个新的数组。
链表的长度可以是不固定的,并且可以动态增删, 适合数据量不固定,频繁增删,较少查询的场景。
203.移除链表元素
- 建议: 本题最关键是要理解 虚拟头结点的使用技巧,这个对链表题目很重要。
- 题目链接:https://leetcode.cn/problems/remove-linked-list-elements/
- 文章讲解:https://programmercarl.com/0203.%E7%A7%BB%E9%99%A4%E9%93%BE%E8%A1%A8%E5%85%83%E7%B4%A0.html
- 视频讲解:https://www.bilibili.com/video/BV18B4y1s7R9/
1. 直接使用原来的链表来进行删除操作。
- 需要判断删除的节点是否是头节点
- 是头节点把头节点的指针向后移一位就可
- 不是头节点只需要把前一个节点的指针指向下一个节点
public ListNode removeElements1(int val) {
while(head != null && head.val == val){ //首先判断链表不为空,要删除的是不是头节点
head = head.next; //如果要删除的是头节点则指向下一个
}
ListNode temp = head;
while(temp != null && temp.next != null){ //操作判断是否要删除的不是temp是temp.next,所以要保证两个都不为空
if(temp.next.val == val){ //要持续判断temp后面的需不需要删除,所以不能一删完就将temp后移
temp.next = temp.next.next;
}else{
temp = temp.next; //直到temp.next不等于val再后移
}
}
return head;
}
2. 设置一个虚拟头结点再进行删除操作。
- 好处:代码比较统一
public ListNode removeElements2(int val) {
ListNode dummyHead = new ListNode();
dummyHead.next = head;
ListNode temp = dummyHead;
while(temp != null && temp.next != null){ //操作判断是否要删除的不是temp是temp.next,所以要保证两个都不为空
if(temp.next.val == val){ //要持续判断temp后面的需不需要删除,所以不能一删完就将temp后移
temp.next = temp.next.next;
}else{
temp = temp.next; //直到temp.next不等于val再后移
}
}
return dummyHead.next; //注意返回的不是head,head可能已经被删了
}
3. 通过输入创建链表
思路:先创建一个数据再一个一个添加
707.设计链表
- 建议: 这是一道考察 链表综合操作的题目,不算容易,可以练一练 使用虚拟头结点
- 题目链接:https://leetcode.cn/problems/design-linked-list/
- 视频讲解:https://www.bilibili.com/video/BV1FU4y1X7WD/
- 文章讲解:https://programmercarl.com/0707.%E8%AE%BE%E8%AE%A1%E9%93%BE%E8%A1%A8.html
先设计链表:选择单链表/双链表;确定链表固定属性(eg:长度,头尾结点)
//单链表
class SingleLinkedList707 {
int size; //表示链表元素的个数
Node head; //虚拟头节点
public SingleLinkedList707() {
size = 0;
head = new Node();
}
//获取第index个节点的数值,注意index是从0开始的,第0个节点就是头结点
public int get(int index) {
//注意 index = size 也越界
if(index < 0 || index >= size){
return -1;
}
Node curr = head;
//包含一个虚拟头节点,所以查找index+1个节点
for(int i = 0; i <= index; i++){
curr = curr.next;
}
return curr.val;
}
public void addAtHead(int val) {
addAtIndex(0,val);
}
public void addAtTail(int val) {
addAtIndex(size,val);
}
// 在第 index 个节点之前插入一个新节点,例如index小于等于0,那么新插入的节点为链表的新头节点。
// 如果 index 等于链表的长度,则说明是新插入的节点为链表的尾结点
// 如果 index 大于链表的长度,则返回空
public void addAtIndex(int index, int val) {
//先判断非法情况,
if(index > size){
return;
}
if(index < 0){
index = 0;
}
//把不需要插入的情况排除掉后,先增加链表的长度
size++;
//找到要插入节点的前驱
Node temp = head;
for(int i = 0; i < index; i++){
temp = temp.next;
}
//插入节点
Node toAdd = new Node(val);
toAdd.next = temp.next;
temp.next = toAdd;
}
//删除第index个节点
public void deleteAtIndex(int index) {
if(index < 0 || index > size){
return;
}
if(index == 0){
head = head.next;
}
Node temp = head;
for(int i = 0; i < index; i++){
temp = temp.next;
}
temp.next = temp.next.next;
}
}
class Node{
int val;
Node next, prev;
Node() {}
Node(int val) { this.val = val; }
Node(int val, Node next){
this.val = val;
this.next = next;
}
public String toString(){
return "ListNode:val="+val;
}
}
206.反转链表
- 建议先看我的视频讲解,视频讲解中对 反转链表需要注意的点讲的很清晰了,看完之后大家的疑惑基本都解决了。
- 题目链接:https://leetcode.cn/problems/reverse-linked-list/
- 视频讲解:https://www.bilibili.com/video/BV1nB4y1i7eL
- 文章讲解:https://programmercarl.com/0206.%E7%BF%BB%E8%BD%AC%E9%93%BE%E8%A1%A8.html
1. 双指针法
public static ListNode reverseList(ListNode head) {
ListNode cur, pre, temp;
cur = head;
pre = null;
while(cur != null){
temp = cur.next;
cur.next = pre;
pre = cur;
cur = temp;
}
return pre;
}
2. 递归法
public ListNode reverseList(ListNode head) {
return reverse(head, null);
}
public ListNode reverse(ListNode cur, ListNode pre){
ListNode temp;
if(cur == null) return pre;
temp = cur.next;
cur.next = pre;
return reverse(temp, cur);
}