一、链表简介
1、链表与顺序表的区别
上一篇博客我介绍了顺序表,这次我们来认识认识链表!先来看看二者的区别:
顺序表:由于顺序表实际上是一个数组,因此它在物理上是连续的,逻辑上也是连续的!
链表 :链表实际上是由一块一块的内存组成,因此物理上表示连续的,但是在逻辑上是连续的!
链表的一个生动例子也就是我们日常出行可见的火车高铁!大家想想看:火车是不是有一节一节的车厢连接而成?在就很我们要学习的链表很相似了!链表也是由一块一块的内存连接而成!
如图:
链表是由一个一个的节点组织起来的,整体就叫做链表!
2、链表分类:
分类的几种标准:
单向 双向
带头 不带头
循环 非循环
根据以上标准,链表基本划分为八种:
单向带头循环、单向带头非循环、单向不带头循环、单向不带头非循环
双向带头循环、双向带头非循环、双向不带头循环、双向不带头非循环
全部讲解不太现实,也没有必要,我们只要认识两种比较重要的类型就行了!接下来我们主要认识的是:
单向不带头非循环、双向不带头非循环
3、通过画图认识几种类型:
单向不带头非循环:
如图:
1、每一个节点由两个域构成,分别是value和next,value存储这个节点的数据,next存储下一个节点的地址!
2、单向的意思是这个链表只有一个方向,即箭头方向
3、不带头表示表示这个链表没有头,而是指这个链表的头不固定!
如这个例子,现在这个头的地址为第一个节点的地址0x12,如果把第一个节点删除,那么这个头的地址也就修改为第二个节点的地址0x33
单向带头非循环:
与前面的单向不带头非循环对比,区别是这个类型带了一个固定的头,它永远不会变!
如图:
循环类型:
循环类型与非循环类型的区别在于,最后一个节点存储的地址是不是为null?
非循环:最后一个节点存储的地址为null
循环 :最后一个节点存储的地址为第一个节点的地址
如图:这个为循环类型
二、链表的实现
全部代码:
1、链表类代码:
public class MySingleLinkedList {
//链表是由一个个节点构成,因此我们可以抽象出一个节点类
class ListNode{
public int val;
public ListNode next;
//创建构造方法
public ListNode(int val){
this.val=val;
}
}
//链表的头节点
public ListNode head;
//创建一个小链表的方法:
public void create(){
ListNode node1=new ListNode(1);
ListNode node2=new ListNode(2);
ListNode node3=new ListNode(3);
ListNode node4=new ListNode(4);
//通过地址连接链表
node1.next=node2;
node2.next=node3;
node3.next=node4;
head=node1;
}
//打印链表的方法:
public void display(){
ListNode cur=head;
while(cur!=null){
System.out.print(cur.val+" ");
cur=cur.next;
}
}
//求链表长度的方法:
public int size(){
ListNode cur=head;
int count=0;
while(cur!=null){
count++;
cur=cur.next;
}
return count;
}
//头插的方法:
public void addFirst(int val){
ListNode node=new ListNode(val);
node.next=head;
head=node;
}
//尾插的方法:
public void addLast(int val){
ListNode node=new ListNode(val);
//如果head为空
if(head==null){
head=node;
return ;
}
//当head不为空
ListNode cur=head;
//找到尾节点
while(cur.next!=null){
cur=cur.next;
}
cur.next=node;
}
//任意位置插入的方法:
public void addIndex(int index,int val){
//检查index是否合法
try{
checkIndex(index);
}catch (IndexNotLeaglException e){
e.printStackTrace();
}
//index==0,相当于头插
if(index==0){
addFirst(val);
return;
}
//index=size,相当于尾插
if(index==size()){
addLast(val);
return;
}
//0<index<size
if(index>0&&index<size()){
ListNode node=new ListNode(val);
ListNode cur=findIndexSubOne(index);
node.next=cur.next;
cur.next=node;
}
}
//找到index的前一个位置的方法:
private ListNode findIndexSubOne(int index){
ListNode cur=head;
int count=0;
while(count!=index-1){
cur=cur.next;
count++;
}
return cur;
}
//检查index是否合法
private void checkIndex(int index){
if(index<0||index>size()){
throw new IndexNotLeaglException("index不合法!");
}
}
//查找是否包含某个关键字key的方法:
public boolean contains(int key){
ListNode cur=head;
while(cur!=null) {
if(cur.val==key){
return true;
}
cur=cur.next;
}
return false;
}
//删除链表中第一次出现关键字key的节点方法:
public void remove(int key){
//1、删除头节点
if(head==null){
return;
}
if(head.val==key){
head=head.next;
return;
}
//2、要删除的元素不在头节点
//先找到要删除的前一个节点
ListNode cur=head;
while(cur.next!=null){
if(cur.next.val==key){
break;
}
cur=cur.next;
}
//删除该节点
cur.next=cur.next.next;
}
//去除链表中所有相同的关键字
public void removeAllKey(int key){
if(head==null){
return;
}
//1、先去除头节点以外的与key相同值的节点
ListNode prev=head;//cur的前驱节点
ListNode cur=head.next;//寻找要去除的节点
//判断并且删除节点
while(cur!=null){
if(cur.val==key){
prev.next=cur.next;
cur=cur.next;
}else{
prev=cur;
cur=cur.next;
}
}
//2、判断头节点存储的内容是否为key
if(head.val==key){
head=head.next;
}
}
}
2、异常类代码:
public class IndexNotLeaglException extends RuntimeException{
IndexNotLeaglException(){
}
IndexNotLeaglException(String msg){
super(msg);
}
}
全部类文件:
代码讲解:
1、类文件
如图,为了实现链表,我们创建了两个类:MySingleLinkedList类和Test类
MySingleLinkedList类:主要是用来创建链表和实现链表的具体功能!
Test类:主要是用来测试和使用链表
2、创建节点类
我们知道,链表是由一个一个的节点构成的,每个节点由value(节点的内容)和next(下一个节点的地址)构成,那么在自己实现链表的过程中,我们就可以抽象出来一个节点类,包含两个成员变量val和next。
此时这个ListNode类定义在MySingleLinkedList类的内部,称为内部类!
另外,由于链表需要一个头,方便我们遍历链表,因此我们可以定义一个ListNode类型的引用变量head,用来存储第一个节点的地址!
public class MySingleLinkedList {
//链表是由一个个节点构成,因此我们可以抽象出一个节点类
class ListNode{
public int val;
public ListNode next;
//创建构造方法
public ListNode(int val){
this.val=val;
}
}
//链表的头节点
public ListNode head;
}
3、创建链表方法:
我们知道,链表的底层不像顺序表,它不是由数组构成,而是由一个一个的节点连接而成!
因此我们在创建链表的时候,需要实例化一个一个的ListNode类!这里我们可以实现一个方法,帮助我们创建一个小链表!
//创建一个小链表
public void create(){
ListNode node1=new ListNode(1);
ListNode node2=new ListNode(2);
ListNode node3=new ListNode(3);
ListNode node4=new ListNode(4);
//通过地址连接链表
node1.next=node2;
node2.next=node3;
node3.next=node4;
head=node1;
}
3、打印链表方法:
为了知道链表中存储了什么内容,我们可以实现一个打印链表的方法!
//打印链表方法:
public void display(){
ListNode cur=head;
while(cur!=null){
System.out.print(cur.val+" ");
cur=cur.next;
}
}
4、求链表长度方法:
//求链表长度
public int size(){
ListNode cur=head;
int count=0;
while(cur!=null){
count++;
cur=cur.next;
}
return count;
}
5、头部插入方法
在第一个节点之前插入一个新的节点:
首先,我们需要新实例化一个ListNode类node作为一个新的节点,此时的node.next
里面存储
插入前:
插入后:
//头插
public void addFirst(int val){
ListNode node=new ListNode(val);
node.next=head;
head=node;
}
6、尾部插入方法:
尾部插入方法分为两种情况讨论:链表head为空or链表head不为空
首先实例化一个节点类的对象node
1、当链表的head为空,我们只需要将head=node就行
2、当链表的head不为空,先找到尾节点,再将尾节点的next赋值为node
//尾插
public void addLast(int val){
ListNode node=new ListNode(val);
//如果head为空
if(head==null){
head=node;
return;
}
//当head不为空
ListNode cur=head;
//找到尾节点
while(cur.next!=null){
cur=cur.next;
}
cur.next=node;
}
7、任意位置插入方法:
相对于头部插入和尾部插入,这个任意位置插入的方法实现起来比较复杂,原因是,它自己本身也包含了头部插入和尾部插入功能!并且在此之上,还可以插入除了头部和尾部之间的任意位置!
首先,我们要弄清楚的一点是,链表本身不像顺序表,它是没有下标这个概念的,但是为了实现这个功能,我们可以人为地认为链表是有下标(index)的!
如图:
那么我们可以将其分为几种情况:
1、index==0,这时相当于头部插入
2、index==size(size表示链表节点的个数),这时相当于尾部插入
3、0<index<size,插入除头部和尾部的任意中间位置
4、index<0或index>size,下标不合法
1、index==0 和 2、index==size
这个时候调用头部插入方法和尾部插入方法即可
//index==0,相当于头插
if(index==0){
addFirst(val);
return;
}
//index=size,相当于尾插
if(index==size()){
addLast(val);
return;
}
3、0<index<size
这种情况下,我们分为两步走:
第一步:找到index位置的前一个位置,
先定义一个cur==head,然后移动cur,使cur找到index位置的前一个位置
//找到index的前一个位置的方法:
private ListNode findIndexSubOne(int index){
ListNode cur=head;
int count=0;
while(count!=index-1){
cur=cur.next;
count++;
}
return cur;
}
第二步:连接节点
连接节点只需要修改两个东西,以图为例子:
一个是新节点node的next,要存入原下标为2的节点的地址
一个是下标为1的节点的next,要修改为node的地址
修改之后:
//0<index<size
if(index>0&&index<size()){
ListNode node=new ListNode(val);
ListNode cur=findIndexSubOne(index);
node.next=cur.next;
cur.next=node;
}
4、index<0或index>size(index不合法)
这种情况下,我们可以在插入前检查下标,如果不合法,那就抛出异常!
首先新建一个类,定义一个异常类:
public class IndexNotLeaglException extends RuntimeException{
IndexNotLeaglException(){
}
IndexNotLeaglException(String msg){
super(msg);
}
}
然后在MySingleLinkedList类中实现一个检查下标的方法:
//检查index是否合法
private void checkIndex(int index){
if(index<0||index>size()){
throw new IndexNotLeaglException("index不合法!");
}
}
接下来就使用try……catch语句使用该异常类
任意插入方法的全部代码
//任意位置插入的方法:
public void add(int index,int val){
//检查index是否合法
try{
checkIndex(index);
}catch (IndexNotLeaglException e){
e.printStackTrace();
}
//index==0,相当于头插
if(index==0){
addFirst(val);
return;
}
//index=size,相当于尾插
if(index==size()){
addLast(val);
return;
}
//0<index<size
if(index>0&&index<size()){
ListNode node=new ListNode(val);
ListNode cur=findIndexSubOne(index);
node.next=cur.next;
cur.next=node;
}
}
//找到index的前一个位置的方法:
private ListNode findIndexSubOne(int index){
ListNode cur=head;
int count=0;
while(count!=index-1){
cur=cur.next;
count++;
}
return cur;
}
//检查index是否合法
private void checkIndex(int index){
if(index<0||index>size()){
throw new IndexNotLeaglException("index不合法!");
}
}
}
新创建的异常类型:
8、查找是否包含关键字key的方法:
查找链表中的每一个节点,如果链表中存在key,返回true,不存在key,返回false
//查找是否包含某个关键字key的方法:
public boolean contains(int key){
ListNode cur=head;
while(cur!=null) {
if(cur.val==key){
return true;
}
cur=cur.next;
}
return false;
}
9、删除第一次出现关键字为key的节点 的方法:
删除指定元素的节点分为2种情况:
1、删除的元素在头节点:
这种情况比较简单:只需要将链表的头修改为下一个链表的地址即可!
另外,如果head==null,不执行任何操作,直接return
//1、删除头节点
if(head==null){
return;
}
if(head.val==key){
head=head.next;
return;
}
2、要删除的元素不在头节点:
如果要删除指定元素所在的节点,我们可以先定义一个cur,让它找到要删除节点的前一个节点!
用cur.next节点的val来判断,cur的下一个节点存储的元素是不是我们要找的元素key,如果是,那么此时的cur正指向要删除节点的前一个节点!
注意,此时while循环的判断条件应该是(cur.next!=null)而不是(cur!=null)!
//先找到要删除的前一个节点
ListNode cur=head;
while(cur.next!=null){
if(cur.next.val==key){
break;
}
cur=cur.next;
}
//删除该节点
cur.next=cur.next.next;
}
让我们看看二者的区别:
1、cur!=null
当cur指向该链表的最后一个节点,由于cur!=null,因此进入循环。
我们就会发现,在循环体语句cur.next.val==key会报错!因为cur.next为null,因此也不会有cur.next.next(相当于null.next)
2 、cur.next!=null
当cur指向倒数第二个节点,上述的语法不会报错!
10、删除链表中相同的元素的方法:
//去除链表中所有相同的关键字
public void removeAllKey(int key){
if(head==null){
return;
}
//1、先去除头节点以外的与key相同值的节点
ListNode prev=head;//cur的前驱节点
ListNode cur=head.next;//寻找要去除的节点
//判断并且删除节点
while(cur!=null){
if(cur.val==key){
prev.next=cur.next;
cur=cur.next;
}else{
prev=cur;
cur=cur.next;
}
}
//2、判断头节点存储的内容是否为key
if(head.val==key){
head=head.next;
}
}