前戏
以下内容是基于不带头单向非循环的单链表.希望能清楚这一点.
大家都写过顺序表, 不难发现顺序表的删除和增加一个元素很麻烦,每次删除和增加一个元素都需要挪动一部分数据.那么问题来了,有没有一个数据结构删除和增加元素是不用挪动数据的?
答案是肯定的,单链表就来了. 说起single 不由让人想起一首歌, single dog, single dog, single dog the way~
上面说到不带头单向非循环,可能有些兄弟不懂什么意思,里面涵盖三个条件.下面依次来解释.
不带头
链表中每个节点都可以当头, 头节点是不确定的.0x12也可以,后面的节点都可以当头节点.假如头节点是确定的,就确定了整个单链表.后面涉及了代码就可以理解头节点的重要性了.
单向
所谓单向,指的是单指向,每个节点用next连接且只有前一个的next指向下个节点,上图就是单向的.
非循环
最后一个节点的next指向了第一个节点的地址,形成了一个闭环.这是循环,反之则是非循环.
创建链表
我采用的是穷举法,将每个节点定义出来,然后每个节点通过next连接,最后将note1视为头节点.注意只是定义一个变量当作头,这个头随时都可以改变的.
static class Note{
public int val;
public Note next;
public Note(int val){
this.val = val;
}
}
Note head;
public void createNote(){
Note note1 = new Note(21);
Note note2 = new Note(21);
Note note3 = new Note(28);
Note note4 = new Note(21);
note1.next = note2;
note2.next = note3;
note3.next = note4;
head = note1;
}
打印链表
首先定义一个变量cur指向head,为什么我要这么写呢? 因为我们研究的是不带头的链表,假如用head去遍历,到最后head就不知道指向哪里了,找不到head,所以我们这里就伪造一个head,让它去遍历整个链表.
那么问题来了,什么时候整个链表走完了?当cur.next == null时,链表走完了吗?
并没有走完,当前cur的val也要打印出来.cur.next == null 当做while的条件,还有一个问题,就是当head为空时,就会报空指针异常.所以以下代码中while的条件就改成了cur != null.这样就可以遍历完整个链表了.
public void display(){
Note cur = head;
while(cur != null){
System.out.print(cur.val + " ");
cur = cur.next;
}
System.out.println();
}
链表头插
这里要注意的一点就是先绑定后面的节点,然后head指向note.
public void addFirst(int data){
Note note = new Note(data);
note.next = head;
head = note;
}
链表尾插
抛开上面的代码不说,while的条件为什么是cur.next != null ?而头插法while的条件是cur != null,我们做事情不要太死了,要学会变通,看下图,当循环结束时,cur是在最后一个节点上面的,这个时候就找到了尾节点,然后将尾节点连接新增的节点就行了.
当利用cur.next 作为条件时,势必要考虑cur是否为空的问题,也就是head是否为空,也就是while循环上面的代码.当head为空时,也就是说明当前链表是空链表.新增的节点既是头也是尾,head直接更新为新增节点就行了.
public void addLast(int data){
Note note = new Note(data);
if(this.head == null){
this.head = note;
return;
}
Note cur = head;
while(cur.next !=null){
cur = cur.next;
}
cur.next = note;
}
任意位置插入
我们一上一个数据结构做铺垫,涉及元素的位置,首先就要考虑该位置是否合法.也就是第一个if语句.
需要注意的是,数据结构中,不能隔着位置插入元素,每种数据结构都一样.
在任意位置新增节点,无非就是三种情况,第一点: 在第一个位置插入,第二点: 在链表的最后插入,第三点: 在链表中插入. 第一点和第二点就是上面的头插和尾插,我们要插入一个元素,首先要找到要插入的位置,然后将节点插入,通过findCur函数找到要插入的位置,然后进行插入节点操作.
需要注意的点还是先绑定后面的节点,然后绑定前面的节点,假如位置替换,cur.next = note, note.next= cur.next. 乍一看没什么问题,推理一下就能发现问题了,cur.next的指向已经发生改变了,指向了note,而cur之前的next找不到了,就是这么神奇,所以说要先绑定后面的节点,然后绑定前面的节点.
还有就是这个方法需要考虑head 为空的情况吗? 答案是不需要,当head为空时,链表的长度为0, 链表为0的情况,上面的if语句就已经处理过了,所以不用考虑head为null的情况了.
public boolean addIndex(int index,int data) throws InputException{
if(index < 0 || index > size()){
throw new InputException("输入的位置非法");
}
if(index == 0){
addFirst(data);
return true;
}else if(size() == index){
addLast(data);
return true;
}
Note note = new Note(data);
Note cur = findCur(index);
note.next = cur.next;
cur.next = note;
return true;
}
public Note findCur(int index){
Note cur = this.head;
while(index - 1 != 0){
cur = cur.next;
index--;
}
return cur;
}
查找是否包含关键字key
这个没啥好说的,就是通过cur去遍历整个链表,找到了返回true,没找到就返回false.方法的开头判断一下链表是否为空就行了.
public boolean contains(int key){
if(this.head == null){
return false;
}
Note cur = this.head;
while(cur != null){
if(cur.val == key){
return true;
}
cur = cur.next;
}
return false;
}
删除第一次出现的关键字key
注意:这里是叫我们删除第一次出现的关键字,假如链表是这样的:1,2,3,3假如要删除的是三,最后删除之后的链表是1, 2, 3.只是删除了第一次出现的3, 而后面的3并没有删除.
要注意的第二点: 所谓的节点删除,并不是被内存回收了,而是没有被当前链表中的节点指向了,但是我们在创建链表的时候,还有一个变量指向被删除的节点,那有人就要问了,有变量指向的节点会被jvm中的垃圾回收机制回收吗?会被回收.
当程序结束时或者节点长时间不被使用时,都会被回收.
这里的删除节点的思路也很简单,首先要找到要删除的节点,然后将节点删除.可能有一行代码,比较难理解就是 cur.next = cur.next.next. 如下图,假如要删除val为三的这个节点的,cur.next指向的下一个节点的地址,下一个节点的地址的next指向的又是下一个节点的地址,当cur.next指向cur.next.next时,其实就是cur下一个节点指向由原来的0x02变成了0x03了.
public void remove(int key) throws EmptyException{
if(this.head == null){
throw new EmptyException("链表为空");
}
if(this.head.val == key){
this.head = head.next;
return;
}
Note cur = findCurOfKey(key);
if(cur == null){
System.out.println("链表中没有要删除的元素");
}
cur.next = cur.next.next;
}
public Note findCurOfKey(int key){
Note cur = this.head;
while(cur.next != null){
if(cur.next.val == key){
return cur;
}
cur = cur.next;
}
return null;
}
删除所有值为key的节点
这里用到了一个常用的算法来解决这个问题.前后指针法, 我利用动态图像来表达删除的整个过程.
假如我们要删除的是33这个节点,cur为空循环结束.
可以看到下面的动态图,当要删除的节点是head节点时,利用如下while循环是无法删除的,head节点要单独删除,那为什么要放在while之后?
假如放在while之前删除的话,万一head后面还有一个要删除的节点,还是删除不了.因为head又到后面去了,所以将head之后的节点删除完了之后,然后单独删除head节点.
public void removeAllKey(int key){
if(this.head == null){
throw new EmptyException("链表为空");
}
Note cur = this.head.next;
Note prev = this.head;
while(cur != null){
if(cur.val == key){
prev.next = cur.next;
cur = cur.next;
}else {
prev = cur;
cur = cur.next;
}
}
if(this.head.val == key){
head = head.next;
return;
}
}
得到单链表的长度
这个代码没什么好说的,比较容易.我第一次写的时候,还判断链表是否为空,其实不需要判断.
当head = null时,while循环进不去,最后返回conter,这时候的conter的值还是初始化值 0 .
public int size(){
int conter = 0;
Note cur = this.head;
while(cur != null){
cur = cur.next;
conter++;
}
return conter;
}
总结
有一个经验丰富的程序员跟我说,写链表的时候,先绑定后面的节点,然后绑定当前节点.
以上就是非循环不带头单链表的所有内容啦,如果对你有帮助的话,不要忘记点赞关注收藏哦!!!