文章目录
1. 链表的理论基础
1.1 定义
- 链表是一种通过指针串联在一起的线性结构,每一个结点有两个部分组成,一个是数据域,一个是指针域(用来存放指向下一个结点的指针),最后一个结点的指针域指向null(空指针的意思)
- 链表的入口结点称为链表的头结点,也就是head
1.2 分类
1.2.1 单链表
单链表中的指针域只能指向结点的下一个结点
1.2.2 双链表
- 每一个结点有两个指针域,一个指向下一个节点,一个指向上一个结点
- 双链表既可以向前查询也可以向后查询
1.2.3 循环链表
- 链表首尾相连
- 可以用来解决约瑟夫环问题
1.3 链表的存储方式
链表是通过指针域的指针来连接在内存中的各个节点,所以链表中的结点在内存中不是连续分布的,而是散乱分布在内存中的某块地址上,分配机制取决于操作系统的内存管理
如图所示:
1.4 链表的定义
java定义链表节点的方式:
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;
}
}
1.5 链表的操作
1.5.1 删除
1.5.2 插入
1.6 链表的性能分析
插入/删除(时间复杂度) | 查询(时间复杂度) | 适用场景 | |
---|---|---|---|
数组 | O(n) | O(1) | 数据量固定,频繁查询,较少i增删 |
链表 | O(1) | O(n) | 数据量不固定,频繁增删,较少查询 |
2. 【203】移除链表元素
2.1 方法理论
以链表1 4 2 4为例,移除元素4
这种情况下,就是让结点的next指针直接指向下下一个结点就可以了
由于单链表的特殊性,只能指向下一个节点,刚刚删除的是链表中的第2个和第4个结点,如果删除的是头结点,那么就会涉及到以下两种链表的操作方式:
- 直接使用原链表来进行删除操作
- 设置一个虚拟头结点再进行删除操作
2.1.1 直接使用原链表来进行删除操作
移除头结点:只需将头指针向后移动移位即可
2.1.2 设置一个虚拟头结点再进行删除操作
这样的话就和移除链表中其他节点的方式统一了。
2.2 解题
/**
* 在原链表中删除元素
* 不添加虚拟头结点
* 时间复杂度O(n)
* 空间复杂度O(1)
* @param head
* @param val
* @return
*/
public static ListNode removeElements(ListNode head,int val){
//首先需要考虑头结点为不为空
if (head==null){
return head;
}
//头结点不为空,且头结点的值等于目标值,就把头指针向后移
while (head != null && head.val==val){
head=head.next;
}
//头结点的值不等于目标值
ListNode cur=head; //定义一个临时指针来遍历
while (cur!=null){
//cur的后继节点不为空,且后继节点的值等于目标值
while (cur.next!=null && cur.next.val==val){
cur.next=cur.next.next;
}
//cur的后继节点的值不等于目标值,cur指针就向后移动
cur=cur.next;
}
return head;
}
/**
* 构建虚拟头结点
* 时间复杂度O(n)
* 空间复杂度O(1)
* @param head
* @param val
* @return
*/
public static ListNode removeElements1(ListNode head,int val){
if (head==null){
return head;
}
//因为删除可能涉及到头结点,所以设置dummyHead结点,统一操作
ListNode dummyHead=new ListNode(-1,head);//把虚拟结点和原链表连接上
//删除元素必须要有前驱才行
ListNode pre=dummyHead;
ListNode cur=head;
while (cur!=null){
if (cur.val==val){
pre.next=cur.next;
}else {
/**
* 不等于目标值的话,cur需要向后移动,
* 那么在向后移动的过程中,找到了目标值,需要做删除操作,
* 那么此时需要找到其前驱元素
* 其前驱元素就暂存在pre中
*/
pre=cur;
}
cur=cur.next;//如果不等于目标值,则cur指针向后移动遍历
}
//使用虚拟头结点方法,最后返回的是dummyHead.next,而不是head
//因为head有可能被删除了
return dummyHead.next;
}
3.【707】设计链表
单链表
- 首先定义链表
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;
}
}
- 进行链表操作
public class MyLinkedList {
//存储链表元素的个数
int size;
//虚拟头结点
ListNode head;
//初始化MyLinkedList对象
public MyLinkedList() {
size=0;
head=new ListNode(0);
}
// 获取下标为index的结点的值 若下标无效,返回-1
public int get(int index) {
//index从0开始
//首先判断index是否合法
if (index<0 || index>=size){
return -1;
}
/**
* 定义一个临时指针来遍历
* 原因:若直接使用头结点来遍历的话,在遍历的过程中,头结点的值发生了变化
* 最后要返回头结点时,已不是原来的值
*/
ListNode curNode=head;
//包含一个虚拟头结点,所以查找的是第index+1个结点
for (int i=0;i<=index;i++){
curNode=curNode.next;
}
return curNode.val;
}
//将一个值为 val 的节点插入到链表中第一个元素之前。
// 在插入完成后,新节点会成为链表的第一个节点。
//也就是在index=0前插入节点
public void addAtHead(int val) {
addAtIndex(0,val);
}
//将一个值为 val 的节点追加到链表中作为链表的最后一个元素
public void addAtTail(int val) {
addAtIndex(size,val);
}
//将一个值为 val 的节点插入到链表中下标为 index 的节点之前。
// 如果 index 等于链表的长度,那么该节点会被追加到链表的末尾。
// 如果 index 比长度更大,该节点将 不会插入 到链表中
public void addAtIndex(int index, int val) {
if (index>size){
return;
}
if (index<0){
index=0;
}
size++;
//找到要插入节点的前驱 使用这个指针进行遍历
ListNode preNode=head;
for (int i=0;i<index;i++){
//在找到目标节点之前,一直遍历,最终在index-1处停止
preNode=preNode.next;
}
ListNode toAddNode=new ListNode(val);
//插入节点
toAddNode.next=preNode.next;
preNode.next=toAddNode;
}
//如果下标有效,则删除链表中下标为 index 的节点
public void deleteAtIndex(int index) {
//首先判断index是否合法
if (index<0 || index>=size){
return;
}
size--;
if (index==0){
head=head.next;
return;
}
//定义临时指针进行遍历
ListNode preNode=head;
for (int i=0;i<index;i++){
preNode=preNode.next;
}
//删除节点
preNode.next=preNode.next.next;
}
}
4.【206】反转链表
思路
如果再定义一个新的链表,实现反转,其实有点浪费内存空间
只需要改变链表的next指针的指向,就可以直接将链表反转,如图所示:
- 双指针法
- 首先定义一个cur指针用来遍历,指向头结点;再定义一个pre指针,初始化为null(因为头结点反转后会变为尾结点,为节点的指针为null)
- 接下来开始反转,注意要使用temp来保存一下cur.next结点,(因为接下来要改变cur.next的指向了,以防后续操作找不到此节点)
- 然后,循环走代码逻辑,继续移动pre和cur指针
- 最后,cur指针指向null,pre指向原链表的尾结点,也就是新链表的头结点,循环结束。此时可以return pre即可。
public ListNode reverseList(ListNode head){
ListNode preNode=null;
ListNode curNode=head;//临时指针来遍历
ListNode temp=null;
while (curNode!=null){
temp=curNode.next;//保存下一个节点
curNode.next=preNode;//指针反转
//指针向后移动
//两个指针移动顺序不能变
preNode=curNode;
curNode=temp;
}
return preNode;//最后preNode指向头结点
}