链表介绍
链表是一种非连续、非顺序的存储结构,其功能与数组相近,但是数组在初始化时会确定大小,而链表则是在这上面做出了改进,只要内存中还有空间,链表就可以一直存储数据。下面是链表的一个示意图。
从示意图中,我们可以大致了解链表的结构。首先可以看到,链表是由一个个结点(LinkNode)构成的,结点和结点之间使用指针(next)链接,而除了首结点(head)只存在指向下一个结点的指针外,其余的结点都可以存放数据。
代码分析
-
结点类(LinkNode)
在介绍中我们了解到,链表由结点(LinkNode)构成,所以需要编写结点类,注意这里采用的类型是泛型<E>,这样就可以直接操作多种类型的数据,也免去了强制类型转换的烦恼。
class LinkNode<E> {
E data;//存放数据
LinkNode<E> next;//指向下一个结点的指针
public LinkNode() {//无参构造方法
next=null;
}
public LinkNode(E data) {//带参构造方法
this.data = data;
next=null;
}
}
-
方法类( LinkListClass)
有了一个个结点,我们还需要一个类存放将所有的结点链接的方法和链表其他的运用方法,这个类取名叫 LinkListClass<E>,同样,采用了泛型。同时,该类中要建立头结点head,注意这里不能在建表的方法中建立头结点,否则head就是局部变量,建表完后就被释放,无法再通过head找到链表。
public class LinkListClass<E> {
LinkNode<E> head;
public LinkListClass() {//构造方法
head=new LinkNode<E>();
head.next=null;
}
}
头插法:
建表方法通常有两种,一种是头插法,即每一次都在head结点后插入,将之前的结点向后移动一格,具体代码如下:
public void CreateListF(E[] a) {//数组a接受存放在链表中的数据
LinkNode<E> s;//每次新建一个结点
for (int i = 0; i < a.length; i++) {//将数组a中所有的元素放入链表
s = new LinkNode<E>(a[i]);
s.next = head.next;//head.next原先是null,插入新结点后,该结点后是null
head.next = s;//head.next变成该结点
}
}
建表结果如下:
尾插法:
尾插法和头插法有所不同,需要找到最后一个结点进行插入,但是每次都从第一个开始找十分麻烦,所以会使用一个结点t来标记尾结点。
public void CreateListR(E[] a){
LinkNode<E> s;//新结点,用于存放数据
LinkNode<E> t;//由于是在链表末尾插入,t结点用于标记末尾结点,时刻变化
t=head;//未插入时,head结点就是最后一个结点
for(int i=0;i<a.length;i++) {//遍历数组a
s=new LinkNode<E>(a[i]);
t.next=s;//尾结点为t,t.next就是插入的结点s
t=s;//此时尾结点变为s
}
t.next=null;//所有结点插入完毕,不需要再记录尾结点
}
建表结果如下:
可见,虽然头插法和尾插法接收的数据都是{1,2,3,4,5},但用头插法建表后的存放数据的顺序是{5,4,3,2,1},尾插法的数据仍旧是{1,2,3,4,5}。
查找结点(注意后面很多方法中都会使用该方法):
private LinkNode<E> geti(int i){
LinkNode<E> p=head;//头节点
int j=-1;//注意链表的序号和数组一样是从0开始的,所以j是-1不是0(head结点不算在序号中,为-1)
while (j<i){//直到找到第i个,即j=i
j++;
p=p.next;//p变成指向的下一个结点
}
return p;//将需要查找的第i个结点返回
}
添加元素:
public void Add(E e){//一次添加一个元素e
LinkNode<E> s=new LinkNode<E>(e);//创建s结点并将数据e放入
LinkNode<E> p=head;
while (p.next!=null){//一直找到最后一个结点
p=p.next;
}
p.next=s;//最后一个结点后面是s结点
}
计算长度:
public int size(){
LinkNode<E> p=head;//从头节点开始
int cnt=0;//计数
while (p.next!=null){//直到最后一个结点
cnt++;
p=p.next;
}
return cnt;
}
缩小长度:
public void Setsize(int nlen){//nlen为需要的长度
int len=size();//使用到了前面计算长度的方法size
if(nlen<0 || nlen>len ){//nlen小于o或比实际长度大,则报错
throw new IllegalArgumentException("设置长度:n不在有效范围内");
}
if(nlen==len){//长度相同,不需要缩小
return;
}
LinkNode<E> p=geti(nlen-1);//找到序号为nlen-1的结点,此时长度为nlen
p.next=null;//将其与下一个结点的链接断开
}
找到序号为i的结点中的元素:
public E GetElem(int i){//i为要查找的链表序号
int len=size();//使用前面的size方法
if(i<0 || i>len-1){//判断是否出错
throw new IllegalArgumentException("查找:位置i不在有效范围内");
}
LinkNode<E> p=geti(i);//使用前面查找第i个结点的方法
return p.data;//返回其中数据
}
改变序号为i的结点中的数据:
public void SetElem(int i,E e){
if(i<0 || i>size()-1){//判断是否出错
throw new IllegalArgumentException("查找:位置i不在有效范围内");
}
LinkNode<E> p=geti(i);//使用前面查找结点的方法geti
p.data=e;//将数据修改
}
查找数据为e的结点序号:
public int GetNo(E e){
int j=0;
LinkNode<E> p=head.next;//直接从序号为0的结点开始
while (p!=null && !p.data.equals(e)){//不是最后一个结点且没有找到元素e是继续循环
j++;
p=p.next;
}
if(p==null){//没有找到元素e返回-1
return -1;
}
else {//找到返回1
return j;
}
}
交换两个结点中的元素:
public void swap(int i,int j){
LinkNode<E> p=geti(i);//找到序号为i的元素
LinkNode<E> q=geti(j);//找到序号为j的元素
E tmp=p.data;//使用tmp进行交换
p.data=q.data;
q.data=tmp;
}
在指定位置插入结点:
public void Insert(int i,E e){//i为要插入的位置,e为插入结点中的元素
if(i<0 || i>size()){//判断是否错误
throw new IllegalArgumentException("插入:位置i不在有效范围内");
}
LinkNode<E> s=new LinkNode<E>(e);//创建结点
LinkNode<E> p=geti((i-1));//找到序号为i的元素的前一位p结点
s.next=p.next;//新结点的后一位为i结点
p.next=s;//p结点的后一位为新结点
}
删除指定序号的元素:
public void Delete(int i){
if(i<0 || i>size()-1){//判断序号i是否错误
throw new IllegalArgumentException("删除:位置i不在有效范围内");
}
LinkNode <E> p=geti(i-1);//找到序号i的前一结点p
p.next=p.next.next;//前一结点的下一位改为下下位
}
总结:
链表的运用范围非常的广泛,想要运用好也需要一定的理解。