一、链表基础理论
链表是一种通过指针串联在一起的线性结构,每一个节点由两部分组成,一个是数据域一个是指针域(存放指向下一个节点的指针),最后一个节点的指针域指向null(空指针的意思)。
链表的入口节点称为链表的头结点也就是head。如图所示:
链表的类型
链表分为:单链表、双链表、循环链表
单链表:
刚才说的这种链表就是单链表
双链表:
单链表中的指针域只能指向节点的下一个节点。
双链表:每一个节点有两个指针域,一个指向下一个节点,一个指向上一个节点。
双链表 既可以向前查询也可以向后查询。
循环链表:
循环链表,顾名思义,就是链表首尾相连。
循环链表可以用来解决约瑟夫环问题。
链表的存储方式
链表是通过指针域的指针链接在内存中各个节点。
所以链表中的节点在内存中不是连续分布的 ,而是散乱分布在内存中的某地址上,分配机制取决于操作系统的内存管理。
如下图:这个链表起始节点为2, 终止节点为7, 各个节点分布在内存的不同地址空间上,通过指针串联在一起
链表的定义
平时在刷leetcode的时候,链表的节点都默认定义好了,直接用就行了;
而在面试的时候,一旦要自己手写链表,就写的错漏百出。
// 定义一个单链表
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;
}
}
链表的操作
删除节点:
删除D节点,如图所示:
只要将C节点的next指针 指向E节点就可以了。
那么此时D节点不是依然存留在内存里么?只不过是没有在这个链表里而已。
是这样的,所以在C++里最好是再手动释放这个D节点,释放这块内存。
其他语言例如Java、Python,就有自己的内存回收机制,就不用自己手动释放了。
添加节点:
可以看出链表的增添和删除都是O(1)操作,也不会影响到其他节点。
注意,要是删除第五个节点,需要从头节点查找到第四个节点通过next指针进行删除操作,查找的时间复杂度是O(n)。
性能分析
数组在定义的时候,长度就是固定的,如果想改动数组的长度,就需要重新定义一个新的数组。
链表的长度可以是不固定的,并且可以动态增删, 适合数据量不固定,频繁增删,较少查询的场景。
二、移除链表元素
题目:
给你一个链表的头节点 head 和一个整数 val ,请你删除链表中所有满足 Node.val == val 的节点,并返回 新的头节点 。
题意:删除链表中等于给定值 val 的所有节点。
示例1:
输入:head = [1,2,6,3,4,5,6], val = 6 输出:[1,2,3,4,5] |
示例2:
输入:head = [], val = 1 输出:[] |
示例3:
输入:head = [7,7,7,7], val = 7 输出:[] |
提示:
- 列表的节点数目在范围[0,104]内
- 1 <= Node.val <= 50
- 0 <= val <= 50
解题思路分析
不采用虚拟头节点的方式:
要删除C节点,就让前一个节点直接指向要删除节点的下一个节点;
如果要删除头节点A,头节点没有前一个节点,需要直接把头指针指向下一个节点作为新的头节点即可;
删除元素操作:
删除头节点:
- 首先需要判断删除的是不是头节点;头节点一定要不为空,因为接下来要取出头节点的值,如果为空的话就是在操作空指针了,编译会报错;
- 注意,这里不能用if判断,因为if只能判断一次;如果指向的新的头节点依然是要删除的目标元素,那么还需要继续删除头节点的操作;
- 如果头节点不为空,且他的值等于要删除的目标值,那么直接将头指针head指向下一个节点即可删除该头节点
删除非头节点:
- 定义一个指针来找要删除的非头节点元素
- 判断非头节点元素是否为空,避免操作空指针
- 如果找到要删除的元素节点,就让该节点的前一个节点指向该节点的后一个节点
- 如果没找到就往后移动指针继续找,直到指针指向空,就代表移动到了最后一个节点
代码讲解:
//删除头节点元素
//首先要判断删除的是不是头节点,同时还需要判断头结点是否为空
//注意,这里不能用if判断,因为if只能判断一次;如果指向的新的头节点依然是要删除的目标元素,那么还需要继续删除头节点的操作
while(head != NULL && head.val == target) {
//如果头节点不为空,且他的值等于要删除的目标值,那么直接将头指针head指向下一个节点即可删除该头节点
head = head.next;
}
//删除非头节点元素(经过上面的循环,此时已经确定当前的head.val != val了)
//定义一个指向head的指针current,因为删除元素需要让他的前一个节点指向他的后一个节点,如果current直接指向头节点后一个节点的话,单向链表就找不到他的前一个节点了;此处的逻辑是:如果current.next的值是要删除的,那么直接让current指向current.next.next即可
ListNode cur = head;
while(cur != NULL && cur.next != NULL) {
if(cur.next.val == target){
cur.next = cur.next.next;
}else{
cur = cur.next; //如果不相等就让cur继续往下走,判断下一个cur.next是否等于target
}
}
return head; //返回操作完删除以后的链表头节点指针
由此可以看出删除头节点和删除非头节点的操作是不一样的,这样的话删除节点的方式是不统一的;
有没有可以让删除节点操作统一的方法?
有,就是 虚拟头节点 的方法
采用虚拟头节点方式:
我们可以在最前面加一个虚拟头节点,将删除头节点的操作和删除非头节点的操作统一起来;
这样我们再去删除头节点的话,实际操作和删除非头节点是一样的:
代码讲解:
//首先创建一个新节点,作为虚拟头节点
ListNode dummy = new ListNode(-1, head);
//定义一个临时指针用来遍历链表
ListNode cur = dummy; //这个临时指针要指向虚拟头节点,理由同样是需要找到要删除节点的前一个节点
while(cur.next != NULL) {
if(cur.next.val == target) {
cur.next = cur.next.next;
}else{
cur = cur.next;
}
}
return dummy.next; //虚拟头节点的下一个才是新链表的头节点,不能return head,因为head指向的头节点可能已经被删掉了
题解:
不采用虚拟头节点:
- 时间复杂度 O(n)
- 空间复杂度 O(1)
采用虚拟头节点:
- 时间复杂度 O(n)
- 空间复杂度 O(1)
注意:这里不要忘记判断头指针为空的情况;(如果是空链表就直接返回head)