想知道你的浏览器是否设为无痕浏览呢?那样,你的浏览器就不会有历史记录了👀
咳咳😅,事实上浏览器的历史记录可以使用单向链表来存储用户的浏览记录。每个节点代表一个访问的网页,通过指针连接起来,可以方便地回溯和导航浏览历史。除此之外,生活上还有许多关于链表的应用实例:联系人列表、wechat或者qq用户好友、火车车厢连接。所以,今天的主角是:链表
你们好,我是Benmao
本专栏永久免费,持续更新,喜欢可以收藏,讨厌可以吐槽。
在此专栏我讲不断发表目前学习的数据结构与算法相关笔记,所以不是教程。
用到的语言是Java语言。
此外,向大家推荐学习Java的一个好帮手 笨猫编程手册,遇到有关Java的知识点可以进行查找,对数据结构与算法感兴趣的,可以收藏本栏目
目录
链表引入
单链表介绍
当谈到单链表时,我们通常指的是单向链表(Singly Linked List),它是一种常见的数据结构,用于存储和操作数据。它由一系列节点组成,每个节点包含一个数据元素和一个指向下一个节点的指针。链表的头节点是第一个节点,尾节点是最后一个节点,尾节点的指针指向null,表示链表的结束。
1) 链表是以节点的方式来存储,是链式存诸
2) 每个节点包含对应的值或其他属性, next属性: 指向下一个节点的地址,
3) 链表的各个节点不一定是连续存储4)链表分带头节点的链表和没有头节点的,根据实际的需求来确定
单向链表的优点是插入和删除节点的时间复杂度为O(1),因为只需要修改指针的指向。然而,访问链表中的特定节点的时间复杂度为O(n),因为需要从头节点开始遍历链表,单链表的节点在内存中可以是分散的,因此在访问链表时,我们需要通过指针来跳转到下一个节点。这也是单链表相对于数组的一个区别,数组的元素在内存中是连续存储的。因此在学习链表或者做一些关于链表的算法题时,基本上会使用辅助变量来完成链表的遍历操作,对于链表很熟悉的小伙伴,对pre、curr、next分别作为当前节点上一个节点、当前节点、当前节点的下一个节点不会感到陌生吧,那么接下来就是实现单链表的思路
单链表结构实现思路和代码
1. 定义节点属性
创建一个节点类,包含数据元素和指向下一个节点的指针。节点类可以包含构造函数和其他必要的方法。为了更加直观地感受,除了no(节点不能重复的值)和next,格外地添加了一个属性name和nickname来代表创建节点的每一个对应的名称
//定义BenmaoNode类,每一个BenmaoNode对象就是一个节点
class BenmaoNode{
public int no;//不能重复,用于测验
public String name;
public String nickname;
public BenmaoNode next;//指向下一个节点
//节点构造器
public BenmaoNode(int no, String name, String nickname) {
this.no = no;
this.name = name;
this.nickname = nickname;
}
@Override
public String toString() {
return "BenmaoNode{" +
"no=" + no +
", name='" + name + '\'' +
", nickname='" + nickname + '\'' +
'}';
}
}
2. 初始化头节点
新建一个BenmaoLinkedList类,用于实现对链表节点的一些操作。我们先定义一个头节点,不存放具体的数据,后面的遍历操作会更加方便。
private BenmaoNode head = new BenmaoNode(0, "","");
//因为遍历是不使用head,也可以head==null,但之后创建链表时需要判断head是否为null
3. 添加节点
首先,找到当前链表的最后一个节点。从头节点开始,通过遍历链表,将指针移动到最后一个节点。然后,将最后一个节点的next
指针指向新的节点。这样,新节点就被成功添加到了链表的末尾。当我们遍历的时候需要一个辅助变量来完成指针移动的操作,让链表的head的引入赋值给一个节点即可。
//添加节点到单向链表
//当不考虑编号的顺序时
//1.找到当前链表的最后节点
//2.将最后这个节点的next,指向新的节点
private void add(BenmaoNode node){
//因为head节点不能动,因此需要一个辅助节点
BenmaoNode temp = head;
//遍历链表,找到最后一个节点
while (true){
//什么时候到最后? 那就是temp.next==null
if(temp.next==null){
break;
}
//还不是最后一个节点,就将temp后移
temp=temp.next;
}
//循环结束后,temp已经到达最后一个节点,把最后一个节点的next==null改为next==node
temp.next=node;
}
4. 遍历链表
同样地,若是对上一段代码理解透彻,对于遍历,只需要一个赋值变量。此外,需要注意的是,在开始遍历之前,需要判断一下链表是否为空。
如果链表为空:head.next==null为true
public void listAll(){
//判断链表数据为空,只有head一个无意义的节点
if(head.next==null){
System.out.println("链表为空");
return;
}
//因为head节点不能动,因此需要一个辅助节点
BenmaoNode temp = head.next;
while (true){
//判断是否到链表最后
if(temp==null){
break;
}
//输出节点信息
System.out.println(temp.toString());
//将next后移
temp=temp.next;
}
}
5. 测试链表结构
在main方法中,依次创建几个节点,把节点串起来(add方法) 就形成了链表。接下来测试一下所写的代码吧
//创建四个节点
BenmaoNode node = new BenmaoNode(1, "Benmao", "笨猫");
BenmaoNode node1 = new BenmaoNode(2, "LBQ", "宝宝");
BenmaoNode node2 = new BenmaoNode(3, "WHY", "IMG");
BenmaoNode node3 = new BenmaoNode(4, "DDG", "DOG");
//把四个节点连起来成为链表
BenmaoLinkedList list = new BenmaoLinkedList();
list.add(node);
list.add(node1);
list.add(node2);
list.add(node3);
//显示链表
list.listAll();
单链表的增、删、改
1. 单链表的增加
咦,不对吧,之前不是已经写了添加链表节点的方法了吗?难道博主在水博客?
当然不是,既然有这句话那么一定另有作用。是的,在上文的确我们写过添加链表节点的方法,但是细心的小伙伴就就可以发现,no似乎没有发挥任何作用。所有接下来,将学习如何根据no的大小来添加链表,这样就成为了一个有序链表
那么思路是什么?
--首先,创建一个辅助节点temp,并将其初始化为头节点。 --遍历链表,找到合适的插入位置。比较当前节点的下一个节点的编号与新节点的编号,直到找到第一个大于新节点编号的节点,或者到达链表的末尾。 --将新节点的next指针指向当前节点的下一个节点,将当前节点的next指针指向新节点。这样,新节点就被成功插入到链表中。 --需要注意的是,在添加节点时,如果发现已经有此节点编号,就不能添加了,因为no是不可重复的。我们可以定义一个flag变量来判断此节点是否存在,只有不存在时才可以添加
//TODO:按顺序添加节点的方式
public void addByNo(BenmaoNode node){
//头节点不能动,通过一个辅助节点来帮助找到添加的位置
BenmaoNode temp = head;
boolean flag = false;//添加的编号是否存在,默认false
while (true){
if(temp.next==null){//到链表最后
break;
}
if(temp.next.no> node.no){//位置找到了,就在temp后面插入
break;
} else if (temp.next.no==node.no) {
flag = true;//编号存在
break;
}
temp = temp.next;//后移
}
//判断flag,如果存在(true),就不能添加
if(flag){
System.out.println("节点编号存在,无法添加");
}else {
//插入到链表中
node.next = temp.next;
temp.next = node;
}
}
2. 单链表的删除
想要删除链表的一个节点,需要找到待删除的节点,因此和单链表的添加类似,如果没有找到此节点一直遍历到链表末尾,那么就不会删除任何一个节点
--首先,创建一个辅助节点temp,并将其初始化为头节点。 --遍历链表,找到需要删除的节点的前一个节点。比较当前节点的下一个节点的编号与目标节点的编号,直到找到目标节点或者到达链表的末尾。 --如果找到目标节点,将前一个节点的next指针指向目标节点的下一个节点,从而跳过目标节点,实现删除操作。
那么只需要遍历当前节点的下一个节点,意思就是说只需要判断当前节点的no和目标节点的no,因为一旦找到需要删除的节点,只需要把需要删除的节点的上一个节点的next指向需要删除的节点的next即可(可能听起来比较绕,如果这篇博客有人看有人反馈的话我就画图)
temp.next.no==no而不是temp.no==no
//TODO:根据no删除节点
public void delNode(int no){
BenmaoNode temp = head;
boolean flag = false;//是否找到需要删除的节点
while (true){
if (temp.next==null){//到达链表的最后
break;
}
if(temp.next.no==no){
flag=true;
break;
}
temp=temp.next;
}
//判断是否找到
if(flag){
temp.next=temp.next.next;
}else {
System.out.printf("要删除的%d节点不存在", no);
}
}
3. 单链表的更改
如果想要修改链表某个节点的name或者nickname,节点的编号no
不能被修改(不用多说了吧,no不可重复),否则会被视为添加新节点。和遍历一样,遍历找到了直接修改属性即可。需要注意的是,如果传递的node的no在链表中找不到,将不会对链表进行任何操作
--首先,创建一个辅助节点temp,并将其初始化为头节点的下一个节点。 --遍历链表,找到需要修改的节点。比较当前节点的编号与目标节点的编号,直到找到目标节点或者到达链表的末尾。 --如果找到目标节点,将目标节点的数据字段更新为新节点的数据字段。
//TODO:完成修改节点的信息,根据no编号来修改,但是no不能修改,否则就是添加节点
public void update(BenmaoNode node){//根据node的no来修改
//是否为空
if(head.next == null){
System.out.println("链表为空");
return;
}
BenmaoNode temp = head.next;
boolean flag = false;//表示是否找到该节点
while (true){
if(temp == null){
break;//快遍历结束了
}
if(temp.no == node.no){
//找到了
flag = true;
break;
}
temp = temp.next;
}
//判断是否找到需要修改的节点
if(flag){
temp.name= node.name;;
temp.nickname = node.nickname;
}else{
System.out.printf("没有找到编号为%d的节点\n", node.no);
}
}
链表扩展
1. 反转链表——迭代
接下来就是关于链表的一些算法的操作,在写算法题中,经常会遇见链表的反转,可能反转所有节点,可能反转部分节点。本篇博客将反转所有节点来讲解反转链表
思路:需要三个变量,先定义一个赋值节点node用于遍历链表,遍历时需要改变当前节点的next改为上一个节点,因此需要其外两个变量res和next分别作为遍历节点的上一个节点、遍历节点的下一个节点,它们分别用于记录上一个节点的地址在和记录下一个节点的地址值,因此可以实现遍历时链表不会断而且改变链表方向的操作
//TODO:实现反转链表
public BenmaoNode reserveNode(BenmaoNode node){
BenmaoNode res = null;//上一个节点
BenmaoNode curr = node;//当前节点
BenmaoNode next = null;//保存下一个节点
while (curr!=null){
next=curr.next;
curr.next=res;
res=curr;
curr=next;
}
return res;
}
2. 反转链表——递归
递归算法后面专栏更新,目前当作参考
//TODO:递归倒序打印链表
public void printNode(BenmaoNode node){
if(node==null){
return;
}
printNode(node.next);
System.out.println(node);
}