文章目录
数据结构之链表学习
随着学习的不断深入,开始学习数据结构这一门课程,链表是数据结构中关键的学习节点之一。在本篇文章中,我们会初步介绍链表以及链表的构造方法,了解其相关的功能,并以代码和图片的方式为大家展示,不足之处望大家给予指正,共同进步。
一、什么是链表
语法说明:链表实际上是一种物理存储结构上非连续存储结构,数据元素的逻辑顺序是通过链表中的引用链接次序实现的。
语法上理解起来相对比较吃力,生活中可以举个例子来说明一下。大家可以依照火车来想象一下链表🧷
火车都是由一节一节的车厢组成的,从而形成了一个完整的列车。链表也是如此,是由一个一个节点组成。
实际上链表的结构非常多,组合起来有8种的链表结构,在本篇我们主要介绍的是➡️单向不带头非循环链表⬅️
我们先画一个大致的图来简单介绍链表
这是一个单向不带头非循环链表,由四个节点组成,每个节点中分为2个部分,并且都是带值的,这些值代表的又是什么意思呢?
val是用来存储数据的
Next是用来存储下一个节点的地址
我们将0x12的位置的节点定义为head,也就是头节点。那么 head=0x12 head.val = 10 head.next = 0x66
需要先了解这些数值是什么意思,才能在后面的链表中理解其中的含义。
二、创建一个链表
简单的介绍链表之后,我们来建立一个链表,并以代码的方式来呈现。
在建立一个链表之前我们需要建立一个静态内部类,在生成链表相关的对象时,可以不依赖于外部的对象。不加也是可以,看自己的项目需求。
public class MySingleList {
static class ListNode{//静态内部类
public int val;//存储的数据
public ListNode next;//存储下一个节点的地址,目前是不知道下一个节点是什么
public ListNode(int val){//构造方法
this.val=val;
}
}
public ListNode head;//代表当前链表的头节点的引用,所以是一个节点型的对象
//使用穷举的方式创建一个链表
public void createLink(){
ListNode listNode1 = new ListNode(12);
ListNode listNode2 = new ListNode(45);
ListNode listNode3 = new ListNode(23);
ListNode listNode4 = new ListNode(90);
//利用链式存储的方式将各个节点串联在一起,形成一个完整的链表
listNode1.next = listNode2;
listNode2.next = listNode3;
listNode3.next = listNode4;
head = listNode1;
}
以上代码需要注意几点
- 我们定义了val和next之后,并没有直接给定值,是因为我们还不知道下一个节点中存储的是什么,类似于我们已经定义好了框架,具体给的值还需要重新放入。
- 此时的头节点已经定义,但是我们还不知道head应该指向哪一个节点。
- val的值需要重写构造方法,便于给成员赋值。虽然编译器会自动提供一个不带参数的构造方法给我们,但是还是需要重写一个属于我们自己的构造方法,代码需要一定的严谨性。
完成了定义之后,就需要给val赋值了,我们分别给四个节点listnode1 listnode2 listnode3 listnode4 的val赋值,12 45 23 90。如下图
这是一个不完整的链表,并没有完全串联起来。这个时候就需要用到next了,我们在文章的开头说过,next是用来存储下一个节点的地址的。
每一个listNode都有属于自己的地址,我们通过赋值的方式从第二个节点开始,将自己的地址赋值给前一个节点的next;最后再将listNode1赋值给head,从而我们形成了一个完整的链表。
完整的链表结构⬇️
通过调试的方法也能看到目前是已经将4个节点都串联起来了,最后一个节点的next赋值为null,也就是空的意思,我们的链表是非循环的,所以需要赋值为null。
三、 链表的构造方法
遍历链表
既然已经创建了一个链表,我们现在遍历打印出来。这里有2个很重要的条件,需要我们注意。
- 遍历链表的时候我们第一步肯定是先打印节点1的val,也就是head.val;遍历就需要往后走,head应该怎么往后走?
- 遍历肯定是需要用到循环,那我的循环条件是什么?
比方说我的头节点现在需要往后遍历了,那我现在的语法就是head=head.next,类似于++;那循环条件是什么呢?我们再看下链表的结构图
最后一张图,head已经来到了最后null,代表head.next== null,无法再打印出结果了,也就是head.next的位置,链表已经遍历完了。那循环的条件应该就是head.next!=null。
我们实现一个遍历链表
//方法
建立一个链表
public void createLink(){
//目前只有val有值,next目前没有值
ListNode listNode1 = new ListNode(12);
ListNode listNode2 = new ListNode(45);
ListNode listNode3 = new ListNode(23);
ListNode listNode4 = new ListNode(90);
//利用链式存储的方式将各个节点串联在一起,形成一个完整的链表
listNode1.next = listNode2;
listNode2.next = listNode3;
listNode3.next = listNode4;
head = listNode1;
}
打印一个链表
public void display{
while(head.next!=null){
System.out.print(head.val+" ");
}
System.out.println();
}
//测试
public static void main(String[] args) {
MySingleList mySingleList=new MySingleList();
mySingleList.createLink();
mySingleList.display();
}
运行结果
少了一个90,为什么会少了一个90,答案就在循环条件中。我们对比2个循环条件
一个是head不等于空,一个是head.next不等于空,这二者有什么区别吗?有的
循环条件如果是head.next!=null 意味着我的head如果想要遍历数组的话,当来到最后一个节点是,发现最后一个节点的地址空的,循环就终止了,无法打印最后一个节点的val也就是90这个值。
所以我们循环条件需要改成head!=null,换成head!=null之后,来到最后一个节点发现head=0x77为空吗?不为空,OK打印90,那么就可以将链表4个val都打印出来。
public void display{
while(head!=null){
System.out.print(head.val+" ");
}
System.out.println();
}
运行结果
看起来基本上都正常了,我们尝试再打印一次看看,检查一下代码
public static void main(String[] args) {
MySingleList mySingleList=new MySingleList();
mySingleList.createLink();
System.out.println("first display");
mySingleList.display();
System.out.println("second display");
mySingleList.display();
}
运行结果
我链表呢?哪去了?我不道啊
问题就在于这个head,我的head已经随着遍历数组变为空了,head已经被jvm回收了,已经找不到,但是不行啊,我后面还得用呢。
能不能找个人替我代head去跑呢,我给钱。哈哈这里是开句玩笑,不过确实可以找一个替代值,替代head去遍历链表。
public void display(){
ListNode cur = head;//滴滴代跑
while (cur != null) {
System.out.print(cur.val+" ");
cur = cur.next;
}
System.out.println();
}
我们创建一个链表类型的值cur,替代head去遍历数组,这样一来,head的指向不会消失,我的链表也可以正常打印,想打印几次都没问题
光是一个链表打印我们就已经踩了很多坑了,数据结构真的是一门非常有趣(折磨)的课程
综上所述我们一定要理解的地方就是循环遍历的条件
如果你想把链表全部遍历完要用head!=null
如果你想把链表全部遍历完要用head!=null
如果你想把链表全部遍历完要用head!=null
如果你想遍历到链表的尾巴要用head.next!=null
如果你想遍历到链表的尾巴要用head.next!=null
如果你想遍历到链表的尾巴要用head.next!=null
查找是否包含关键字key在链表中
查找+是否 我们可以从中提取到关键信息,查找肯定是用循环,是否的返回值是true or false
那么就需要用到boolean来修饰我的查找方法
public boolean contains(int key){//此时参数是int ,因为找的值是val不是next
ListNode cur =head;//和遍历链表一样,使用滴滴代跑
int count=0;
while(cur!=null){//那么我想找到关键字肯定是需要将整个数组遍历完
if(head.val==key){
return true;//如果找到了就返回true
}
cur=cur.next;//没找到并不是立马返回false,而是继续往后找
}
return false;//实在找不到了才返回true
}
比方说我想找23,cur开始往后找,注意这里已经不是head来遍历了,我们找了滴滴代跑了。
cur=cur.next就是相当于cur往后++
如果我们要找的值,链表里面不存在。比方说我想要找到80
得到链表的长度
得到链表的长度很好理解,就是将整个链表遍历一边,利用计数器的方式记录长度
public int size(){
int count=0;
ListNode cur=head;
while(head!=null){//如果想要将链表遍历到底,就需要这个条件
count++;
cur=cur.next;//相当于++
}
return count;
}
//测试
public static void main(String[] args) {
MySingleList mySingleList=new MySingleList();
mySingleList.createLink();
System.out.print("链表的长度是:");
System.out.println(mySingleList.size());
}
运行结果
头插法和尾插法
头插法
头插法就是讲一个节点插入头节点前面,代替头节点成为新的头节点
头插法需要注意两点
- 你得有节点
- 插入节点你得把后面的节点给绑住
node就是我需要插入的节点,那绑住是什么意思呢?
下面的图片就是我想要实现的效果,绑住的意思是代插入的节点必须和原先的头节点产生联系,我只需要将head赋值给代插入的节点node的next,然后再将head的赋值给head,从而就形成了新的头节点
也就是
node.next=head;
node=head;
这样一来就实现了头插法,代码如下⬇️
public void addFirst(int data){
ListNode listNode = new ListNode(data);
listNode.next = head;
head = listNode;
}
public static void main(String[] args) {
MySingleList mySingleList=new MySingleList();
System.out.println("测试头插");
mySingleList.addFirst(1);
mySingleList.addFirst(2);
mySingleList.addFirst(3);
mySingleList.addFirst(4);
mySingleList.display();
}
没错,只有这几行代码
ListNode listNode = new ListNode(data) 这里因为是定义一个代插入的节点所以是链表类型。
打印出来是倒序的,因为每次都是头插意味着我每次插入的节点都是新的头节点,所以是倒序。
那如果我的节点是空的呢?我只有一个val,没有next,还能插入吗?
可以
尾插法
尾插法讲究的是一个将节点插入到链表的尾部,成为新的尾部节点
那既然是插到尾部,那就需要找到尾巴,怎么找呢?这次就需要用到cur.next ! = null
这次我们需要判断头节点是否为空了,如果是为空,就直接将节点插入
如果不为空,找到尾巴节点,再将节点插入
public void addLast(int data){
ListNode listnode =new ListNode(data);
if(head == null){//如果头节点为空,直接插入
head=ListNode;
return;
}
ListNode cur =head;//滴滴代跑
while(cur.next!=null){//这里是一直遍历到尾巴节点
cur=cur.next;
}
cur.next=ListNode;//如果找到了尾巴节点,
//就代插入节点直接给尾巴的next,形成一个新的尾部节点
}
测试
public static void main(String[] args) {
MySingleList mySingleList=new MySingleList();
System.out.println("测试尾插");
mySingleList.addLast(1);
mySingleList.addLast(2);
mySingleList.addLast(3);
mySingleList.addLast(4);
mySingleList.display();
}
运行结果
和头插不同的是,尾插每次插入都是形成新的尾部节点,所以打印出来是顺序的。
在任意位置插入元素
在任意位置插入我想插入的节点
链表本身是没有索引的
现在我想要把node这个节点插入到我的2位置上,这个应该怎么实现呢?假设现在cur现在是指向head头节点,index是我的2位置。我怎么才能找到需要插入的位置,实际上我只需要找到index-1的位置,找到index-1的位置后把节点插入即可。如果inde=0,就是头插法。如果是末尾位置就是尾插法。
所以我们先实现一个找到代插入节点位置的前一个位置也就是index-1的位置
private ListNode findIndexSubOne(int index){
ListNode cur=head;
int count=0;
while(count!=index-1){//说明还没有走到index-1的位置
cur =cur.next;//这里还是一样相当于cur++
count++;
}
return cur;//此时就是index-1的位置的地址
}
OK,那如果在循环条件下,我找到了代插入节点的前一个位置。我们设想一下,如果我的index位置是不合法的如果是0位置,或者位置超过了链表的长度呢?这个时候就需要抛出异常。所以我们还需要加上一个异常
private void checkIndex(int index){
if(index<0 || index>size()){//如果index的位置不合法
throw new ListIndexOfException("Index位置不合法");//此时就需要抛出异常
}
}
新建一个异常
public class ListIndexOfException extends RuntimeException{
public ListIndexOfException() {
}
public ListIndexOfException(String message) {
super(message);
}
}
现在已经解决后顾之忧了,到了插入的时候了,其实和之前一样,插入就是让这个节点和前一个节点和后一个节点产生联系,将代插入的节点的地址赋值给前一个节点的next,将代插入节点的next赋值给后一个节点。
最后实现的效果就是如上所示
public void addIndex(int index,int data)throws ListIndexOfException{
checkIndex(index);
if(index==0){
addFirst(data);//如果是头节点就是头插
return;
}
if(index==size()){
addLast(data);//尾巴节点就是尾插
return;
}
ListNode cur=findIndexSubOne(index);//拿到index-1位置的地址了
ListNode node=new ListNode(data);//这是我们新建的代插入的节点
//真正插入就只需要2段代码
node.next=cur.next;//先绑后面
cur.next=node;
}
运行结果
这样我们就实现了在2位置上插入一个新的节点,前提是需要找到index-1的位置
删除出现的第一个关键字key的节点
删除一个节点并不是意味着需要把这个节点的val和next都删除,而是绕开它,和在任意位置插入节点一样,我们需要找到待删除节点的前一个位置
如上图所示,我需要删除del这个位置的节点,那我就需要让cur走到这个节点的前一个位置,改变next的值
还是和之前一样,我需要找到前一个节点的位置
直接跳过del这个节点,详细的操作通过代码的方式为大家讲解
private ListNode searchPrev(int key){
//当cur.next==null的时候
//说明没有你需要的节点
ListNode cur=head;//滴滴代跑
while(cur.next!=null){
if(cur.next.val==key){
return cur;
}
cur=cur.next;
}
return null;//代表没有你要删除的节点
}
结合上图看下如果我是要删除del这个节点,它的地址是0x66,val为23那么我就需要找到前一个节点的位置,就是是cur走到的位置,如果这找到了,返回。如果没找到继续找,实在找不到返回null。
幸运的是我们找到了,返回之后我们开始准备删除了,但是删除之前也需要看下头节点是否为空或者头节点是否我需要删除的位置
public void remove(int key) {
if(head == null){//如果头节点为空,代表一个节点都没有,直接返回
return ;
}
if(head.val == key){
head=head.next;//发现头节点确实是我需要删除的,直接将下一个节点的地址赋值给头节点,实现跳过。
return;
}
ListNode cur = searchPrev(key);
if(cur == null){//没有你需要删除的节点
return;
}
//我现在已经找到需要删除节点的位置了,我就讲这个节点的地址给前一个节点的next
ListNode del=cur.next;
//并且我将待删除节点的next赋值给前一个节点的next
cur.next=del.next;
实现了跳转
}
真正删除的代码其实也就是这两段,只不过我们需要注意避免踩坑。
删除所有关键字为key的节点
我目前需要删除的是所有关键字为12的节点,那么问题来了,我该怎么删除
这个时候就需要用到前驱节点prev,cur继续用来找需要删除的节点,二者是紧密相连的。
让prev指向头节点,让cur直接指向下一个节点
如上图所示,我现在要删除12,一共有两个12。
如果头节点是空,那么直接返回
目前我cur的val是等于12的就相当于找到了,直接删除
删除的方式是什么呢,把待删除节点的next赋值给前驱节点的next,直接跳过
prev不动,因为我们还不知道下一个节点是否还是不是我们需要删除的
类似于上图,我已经将待删除的节点的next赋值给前驱节点的next了,但是后面还有一个12,cur++往后走一步了,这个时候prev不能动。
还有一种情况就是我我已经删除第一个12了,但是下一个节点的val是45,这个时候就需要进入else语句了
进入else语句之后,prev就可以直接来到cur的位置
来到第二个12这里,OK那么和刚才一样,继续删除。
public void removeAllKey(int key){
if(head==null){
return ;
}
ListNode prev=head;
ListNode cur=head.next;
while(cur!=null){//条件还是一样,将整个链表遍历结束
if(cur.val == key){//找到了,删除
prev.next=cur.next;
cur=cur.next;
}else{
prev=cur;
cur=cur.next;
}
}
if(head.val == key){
head=head.next;
}
}
测试
public static void main(String[] args) {
MySingleList mySingleList=new MySingleList();
mySingleList.addLast(1);
mySingleList.addLast(2);
mySingleList.addLast(2);
mySingleList.addLast(1);
mySingleList.addLast(4);
mySingleList.removeAllKey(1);
mySingleList.display();
}
最后我们还要注意一点,如果是我的头节点就是待删除的位置呢,这个时候怎么办?
如果说我们的头节点就是待删除的位置,val=12,那么直接将head.next赋值给head就好。
运行结果
清除所有节点
清除所有节点就是将所有的链表回收,只需要将头节点回收就可以了,就像火车一样,火车头没有了,后面的车厢就是再多,也开不起来。
public void clear(){
head = null;
}
运行结果
总结
这篇文章只是初步了解了一下链表以及其构造方法,后续会给大家写一些关于链表面试题的文章,文章有不足或者错误之处,希望大家指正。