链表的概念
链表是由一系列非连续节点组成的线性表,链表的组成单元是节点,每个节点都有数据域和引用域,节点之间就是通过引用域联系起来,链表的第一个节点是头结点,最后一个节点是尾节点,我们可以通过头结点逐个找到它之后的节点,一个节点总是指向下一个节点,故链表是线性的。内存空间包括堆和栈,链表在它们之中的存储方式是:头结点的引用放在栈中,链表的其余部分都放在堆中(我们可以这样理解堆和栈,栈是一种线性的存储形式,而堆中数据的存储是没有规律的,就像是在一个空间中摆法物品,它们不一定是整齐摆放的,堆中数据之间是通过引用来建立联系的)。链表是通过引用联系起来的,那么它就会有很多种实现形式,比如单链表、双向链表、循环链表,它们的含义通过名称不难理解。
泛型
链表中可以存储任意类型的数据,如果我们在定义链表的时候以某种具体的类来定义,那么就只能以这种数据类型来输入,这是很不灵活的。这里我们可以用到泛型,什么是泛型?泛型就是当你在写这段代码的时候不确定输入的参数是什么类型,就可以使用泛型,一般我们用 < T > 的形式来表示,表明我们这个类支持泛型,或者说我们这个类中某个变量的类型我们不确定,需要根据输入参数的类型来决定。当一个方法有传入参数的时候就可以定义泛型。在泛型表示的链表中我们就是用Node< T >表示节点类。需要注意的是:泛型表示java的基本数据类型,它只是一个符号,泛型的存在使得程序有很大的灵活性。
单链表的实现思路
我们先写一个节点类,节点的包括数据域和引用域,我们就定义这两个属性,其他的我们可以交给链表的定义中去完成。
我们再写一个链表类,定义它的头结点,尾节点,链表的长度这些属性。关于头结点,有两种实现思路,一种是头结点存储数据,另一种是头结点仅仅存储下一个节点的地址而不存储数据。我们这里讨论不存储数据的头结点实现的链表。size表示链表的长度,这里头结点不参与计算,我们在定义链表的时候自动生成了头结点且此时size为0,index是我们在索引节点时用到的角标,同样不包括头结点,第一个有数据的节点index=0,下一个index=1,以此类推。
添加,删除,查询,反转
1、添加:在链表末尾加上一个节点,让之前的最后一个节点指向这个新节点,然后把新节点的应用域赋为null,再把它作为尾节点。这里一个细节我觉得值得一提,java中的对象是存储在堆中的,程序对它们的调用其实是通过对于这个对象的存储位置的引用来实现的,在程序中我们打印一个对象,会输出一段字符,它表示的就是一串地址,我们在链表类中定义了Node 类的tail节点对象,当我插入一个新的节点时,我们需要tail=newNode,这是为什么呢?因为我们需要把新的节点的地址赋给tail,这样我们就可以通过tail对象来访问最后一个节点了,归根结底,对象是通过地址的引用来找到的。
2、删除:删除的时候我们要分删除第一个节点,中间的节点,最后一个节点这三种情况来讨论。对于第一个节点,我们只要把头结点指向当前节点的下一个节点再把第一个节点的指针域赋为空即可,对于之间的节点,我们需要把当前节点的前一个节点指向当前节点的下一个节点,再把当前节点的指针域赋为空,这步操作的作用是可以断开这个节点和原链表的连接,使得这个废弃的节点能够被GC清除。最后一个节点就直接把倒数第二个节点的引用域赋为空并把它变成尾节点即可。
3、查询:思路就是通过头结点去递归寻找子节点,控制循环的次数就可以获得需要的对象引用。
4、反转:反转的思路是,从最后一个节点开始操作,把倒数第三个节点对倒数第二个节点的引用赋给最后一个,就实现了最后一个节点指向它前一个节点了,同时我们需要断开倒数第二个节点对下一个节点的引用。从后往前,直到实现引用域的反转,原先的第一个节点就是尾节点了,头结点需要重新定义,在第一次操作的时候需要使得它获得到最后一个节点的引用。在循环结束后,我们在把这个节点作为新的头结点。这种思路是不改变数据域,只调整引用域。当然也可以重新定义一个新的链表,实现反向赋值,也可以实现反转,不过这样就不是原链表了。
代码实现
1、节点类:
public class Node<T>{ //定义一个节点类,对象用泛型来表示,泛型类用T表示
public T data; //数据域
public Node<T> next; //引用域
}
2、链表类:
public final class Linked<T> { //泛型类,此链表支持泛型输入
//此链表头结点不存数据,数据放在下一个开始的节点中,头结点不参与索引,我们这里的第一个节点不包括头结点,是指有数据的节点。
public int size=0; //节点数量
public Node<T> head;//定义头结点
public Node<T> tail;//定义尾节点
/**
* 链表的构造方法
*/
public Linked (){
Node<T> firstNode = new Node<T>(); //创建链表的时候先创建头结点
firstNode.data =null;
firstNode.next =null;
head=firstNode; //把此节点作为头结点
tail=firstNode; //把此节点作为尾节点
}
/**
* 添加元素
* @param data
*/
public void add(T data)
{
Node<T> node = new Node<T>();
node.data=data;
node.next=null;
if(size>0)
{
Node<T> preNode = getPreNode(size);//获取前一个节点
preNode.next= node; //使前一个节点指向当前节点
tail=node; //把当前节点作为尾节点
size++; //长度加一
}
else if(size==0)
{
head.next= node; //使头结点指向当前节点
tail=node; //把当前节点作为尾节点
size++; //长度加一
}
}
/**
* 获取指定索引的前一个节点
*/
public Node<T> getPreNode(int index){
Node<T> preNode = new Node<T>();
preNode = head.next;
for(int i=0; i<index-1;i++) //从头结点开始循环查找节点
{
preNode=preNode.next ;
}
return preNode; //返回此节点
}
/**
* 删除指定索引处的节点
* 返回被删除节点的数据
* @param index
*/
public T deleteNode(int index)
{
if(index<0&&index>size)
{
return null;
}
else if (index ==0) //删除第一个节点(头结点)后的那个节点
{
T data = head.next.data;
head.next=head.next.next;//使头结点指向删除节点的下一个节点
head.next.next=null;//指针赋空,可以被GC清除删除的元素
size--;
return data;
}
else if(index==size-1)//删除最后一个节点
{
Node<T> preNode = getPreNode(index);//获取前一个节点
preNode.next = null;//使前一个节点指向null
T data = preNode.next.data; //保存数据
tail = preNode; //前一节点设为尾节点
size--;
return data;
}
else
{
Node<T> preNode = getPreNode(index);//获取前一个节点
Node<T> theNode = preNode.next;//当前节点
T data = theNode.data; //保存数据
preNode.next = theNode.next;//使前一个节点指向当前节点的下一个节点
theNode.next=null;//当前节点指针赋为空
size--;
return data;
}
}
/**
* 查找指定索引的节点
*/
public T search(int index)
{
if(index<0&&index>size-1) //在范围外,返回null
{
return null;
}
else if(index==0)
{
return head.next.data; //返回第一个节点的数据(头结点的下一个节点)
}
else
{
Node<T> preNode = getPreNode(index); //获取前一个节点
T data =preNode.next.data; //返回索引节点的数据
return data;
}
}
/**
* 链表反转
*/
public void reversal(){
Node<T> newHeadNode = new Node<T>();//定义一个节点,之后作为新的头结点
newHeadNode.data=null; //数据域为空
for(int i = size ; i>2 ; i-- )
{
if(i==size) //第一次操作时使新节点指向最后一个节点
{
newHeadNode.next=getPreNode(size-1).next;
getPreNode(i).next=getPreNode(i-2).next; //循环使得后一个节点指向前一个节点
getPreNode(i-1).next=null; //断开向前一个节点至后一个节点的引用
}
else
{
getPreNode(i).next=getPreNode(i-2).next; //循环使得后一个节点指向前一个节点
getPreNode(i-1).next=null; //断开向前一个节点至后一个节点的引用
}
}
getPreNode(2).next=head.next; //使得第二个节点指向第一个节点(头结点不算入节点数)
getPreNode(1).next=null; //断开引用
tail=head.next;//原先的第一个节点变成尾节点
head.next=null;//断开原头结点
head=newHeadNode;//把新的节点变为头结点
}
}
测试
一、测试添加数据:
public static void main(String[] args)
{
Linked<String> linked = new Linked<>();
linked.add("3");//index=0
linked.add("ss");//index=1
linked.add("dfdf");
linked.add("ddd");
linked.add("345");
linked.add("222");
for(int i=0; i<linked.size;i++)
{
System.out.println(linked.search(i));
}
}
}
输出:
3
ss
dfdf
ddd
345
222
二、测试删除数据:
public static void main(String[] args)
{
Linked<String> linked = new Linked<>();
linked.add("3");//index=0
linked.add("ss");//index=1
linked.add("dfdf");
linked.add("ddd");
linked.add("345");
linked.add("222");
linked.deleteNode(3); //删除ddd
for(int i=0; i<linked.size;i++)
{
System.out.println(linked.search(i));
}
}
}
输出:
3
ss
dfdf
345
222
三、测试反转链表:
public class Test {
public static void main(String[] args)
{
Linked<String> linked = new Linked<>();
linked.add("3");//index=0
linked.add("ss");//index=1
linked.add("dfdf");
linked.add("ddd");
linked.add("345");
linked.add("222");
for(int i=0; i<linked.size;i++)
{
System.out.println(linked.search(i));
}
System.out.println("进行反转:");
linked.reversal();//链表反转的方法
for(int i=0; i<linked.size;i++)
{
System.out.println(linked.search(i));
}
}
}
输出:
3
ss
dfdf
ddd
345
222
进行反转:
222
345
ddd
dfdf
ss
3