今天来讲一下Java中比较重要的链表,这部分比较长,但非常重要,由于例子很多为了防止混乱我会将完整的代码贴在文章末尾。
链表的本质是一个动态的对象数组,它可以实现若干个对象的存储。
1.链表的基本定义
在实际开发中对象数组是一项非常实用的技术,并且利用其可以描述出“多”方的概念:假如一个人有多本书,那么我们如果创建了一个人的类,在这个类里面一定会有一个对象数组保存书的信息,但是传统的对象数组依赖于数组的概念,数组中最大的一个缺点是:长度是固定的。正是因为如此所以在我们的开发过程中,传统的数组引用是非常有限的,基本都是数组的接收以及循环处理,如果想要进行灵活的数据保存,那么就必须自己来实现结构。
传统对象数组的开发依赖于角标(索引)的控制,比如我们new一个数组num[10],那么处理对象的时候就要使用角标,比如给num[0]赋值,删除num[1]中的数据等等,那么这样想实现内容的动态维护难度太高了,而且复杂度极高。所以现在我们就发现,如果对于一成不变的数据,可以使用传统数组来实现,但是对于随时可能变化的数据来说就必须实现一个可以动态扩充的对象数组。
举个例子,假如我们现在要记录我们在超市购买的东西,一共有五个东西:那么我们买了123之后,如果突然不想要2了;或者买了124之后已经出了超市了,又想折回去再买一个2;或者本来打算买五个东西,买完了又突然上架了第六个,发现还需要买一个6。这样的情况反映在传统数组中处理起来是比较麻烦的,传统数组面对这些情况有缺陷的,我们最好可以找到一个动态的数组来处理数据,这就是链表的作用。
链表的本质就是利用引用的逻辑关系来实现类似数组的数据处理操作,以一种保存“多”方数据的形式来实现与数组类似的功能。
我们可以类比一下:链表就像一列火车,有火车头和车厢,而每节车厢是怎样连接的呢?是通过车厢与车厢之间的连接处来连接的,而每节车厢都可能与前后两节车厢相连,那么就需要每节车厢都具有一个可以连接后面车厢的结构与一个让前面车厢连接的结构,这就是简单的链表结构。
通过分析可以发现,如果需要实现链表,那么就需要有一个公共的结构,这个结构可以实现数据的保存以及下一个数据的连接指向,为了描述这样的结构,我们应该可以把每一个储存结构当作一个节点来看,那么我们就可以准备一个节点类,但是这个节点类里面可以保存各种数据的类型,以及保存下一个节点的指向,那么我们就可以使用泛型来给这个类:
root | next→ | node<T> | 数据 | next→ | node<T> | 数据 | next→ | node<T> | 数据 | next→ | null |
![](https://img-blog.csdnimg.cn/20200219143547335.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzM5NzMyODY3,size_16,color_FFFFFF,t_70)
这就是我们现有的结构了,其中【[note<T>] [数据] [ →]】这就是我们上面说的一个公共的结构,它可以实现数据的保存,也可以实现对下个结构的连接。
虽然已经清楚了需要通过Node节点来进行数据的保存,但是毕竟里面需要牵扯到接待您的引用处理关系,那么这个节点应该由谁来控制,由用户吗?当然是不会的用户只需要进行对于数据的操作就可以了,所以我们需要一个专门的类来处理节点的引用关系的配置。为什么不能让用户自己来控制呢?我们来举个例子:
如果需要用户自己控制,那我们就首先要写个类来定义这些关系:
class Node<E>{
private E data; //节点中的数据
private Node next; //下一个节点,也是Node类型
public Node(E data) {
this.data = data;
}
public E getData() {
return this.data;
}
public void setNext(Node<E> next) {
this.next = next;
}
public Node getNext() {
return this.next;
}
}
然后我们就要自己手动的来控制这些关系,比如我们拿火车举个例子:
public class LinkDemo {
public static void main( String args[] ){
Node<String> n1 = new Node<String> ("火车头");
Node<String> n2 = new Node<String> ("车厢一号");
Node<String> n3 = new Node<String> ("车厢二号");
Node<String> n4 = new Node<String> ("车厢三号");
Node<String> n5 = new Node<String> ("车厢四号");
n1.setNext(n2);
n2.setNext(n3);
n3.setNext(n4);
n4.setNext(n5);
print(n1);
}
public static void print(Node<?> node) {
if(node != null) {
System.out.println(node.getData());
print(node.getNext());
}
}
}
我们自己费了好大的劲来让这些东西放到自己的位置上,还得专门写个类来输出,当然输出结果是没有问题的:
火车头
车厢一号
车厢二号
车厢三号
车厢四号
可是如果这样操作的话,我们还不如去操作数组。作为用户,只需要关心数据的存储与获取,不需要自己去控制数据如何存储与如何获取,所以在我们的主类与Node类之间还应该有一个链表操作类,由这个类去处理这些存储与获取背后的工作,主类只需要关注数据的存储与获取就可以了;而由于用户只与链表操作类有关系,那么Node类就不需要出现在明面上了,我们把它作为内部类来只为链表操作类服务;同时链表操作应该有一个标准,所以链表操作类应该有一个接口ILink,而它也就成了LinkImpl类;这就是一个完整的链表操作的结构。
2.数据的保存
听过之前的分析可以发现,进行链表的操作过程中为了避免转型的异常应该使用泛型,同时也应该设计一个链表的执行标准接口;同时具体实现该接口的时候还应该通过Node类做出节点的关系处理:
interface ILink<E> { //设置泛型避免安全隐患
public void add(E e);
}
class LinkImpl<E> implements ILink<E>{
private class Node { //保存节点与数据的关系
private E data; //保存的数据
private Node next; //保存的下一个节点
public Node(E data) { //有数据的情况下才有意义
this.data = data;
}
}
//-----------------以下为Link类中定义结构---------------------
}
在我们现在所定义的程序之中并没有set()和get()方法,是因为内部类的私有属性也方便外部类直接访问。
现在我们要开始写Link类中的定义结构,首先我们要获取数据,那么数据应该怎样被获取呢?在没有任何数据的时候,我们需要给这个程序一个root属性,也就是我们常说的根节点,这个root就相当于火车中的火车头,不管有多少节车厢都得连接在火车头后面。
而对于root应该是什么类型呢?也应该是Node类型,这样才能进行对下一个节点的增加,同时在数据增加的时候根节点必须是第一个对象;在增加的时候,我们要明确的是各个数据之间本身是不具有关联特性的,只有Node类才有,所以要想实现关联处理就必须将数据包装在Node类之中。我们这样来写Link类中的结构:
class LinkImpl<E> implements ILink<E>{
private class Node { // 保存节点与数据的关系
private E data; // 保存的数据
private Node next; // 保存的下一个节点
public Node(E data) { // 有数据的情况下才有意义
this.data = data;
}
}
//-----------------以下为Link类中定义结构---------------------
private Node root; // 保存根元素
//-----------------以下为Link类中定义结构---------------------
public void add(E e) {
if(e == null) { // 保存的数据为空
return; // 方法直接结束
}//数据之间本身是不具有关联特性的,只有Node类才有
//所以要想实现关联处理就必须将数据包装在Node类之中。
Node newNode = new Node(e); // 创建一个新的节点
if(this.root == null) { // 现在没有根节点
this.root = newNode;
}else { // 根节点存在
this.root.next = newNode; // 下一个应该是新节点
}
}
}
这样看起来实现了root根节点的实现,也同时实现了对root后面的节点的增加,但是这样写的话有一个非常严重的问题:root以及后面的一个节点使成功增加的,但是到了第二个节点的时候并不会增加,而是会替换掉root后面的节点,所以我们要改变程序的代码,其实在调用的时候能够准确地找到节点的位置:
class LinkImpl<E> implements ILink<E>{
private class Node { // 保存节点与数据的关系
private E data; // 保存的数据
private Node next; // 保存的下一个节点
public Node(E data) { // 有数据的情况下才有意义
this.data = data;
}
public void addNode(Node newNode) { // 保存新的节点数据
// 第一次调用的时候没有节点,this = LinkImpl.root
// 如果不是空的,第二次调用的时候就是this = LinkImpl.root.next
// 同理,第三次为this = LinkImpl.root.next.next
// 第四次...第五次...直到找到下一个节点为空的节点
if(this.next == null) { // 如果当前节点的下一个节点为空
this.next = newNode; // 就表示可以增加新的节点
}else { // 如果不是空的
this.next.addNode(newNode); // 就将当前节点放到下一个节点处查看
}
}
}
//-----------------以下为Link类中定义结构---------------------
private Node root; // 保存根元素
//-----------------以下为Link类中定义结构---------------------
public void add(E e) {
if(e == null) { // 保存的数据为空
return; // 方法直接结束
}//数据之间本身是不具有关联特性的,只有Node类才有
//所以要想实现关联处理就必须将数据包装在Node类之中。
Node newNode = new Node(e); // 创建一个新的节点
if(this.root == null) { // 现在没有根节点
this.root = newNode;
}else { // 根节点存在
this.root.addNode(newNode);; // 下一个应该是新节点
}
}
}
在Node类中添加一个新的方法addNode(),来查找写一个节点为空的节点,以便于我们存储节点,然后将根节点直接使用addNode方法调用,这样就能保证每个新增的节点都是在上一个节点的后面了。最后我们使用完整代码来添加几个数据看一下:
interface ILink<E> { // 设置泛型避免安全隐患
public void add(E e);
}
class LinkImpl<E> implements ILink<E>{
private class Node { // 保存节点与数据的关系
private E data; // 保存的数据
private Node next; // 保存的下一个节点
public Node(E data) { // 有数据的情况下才有意义
this.data = data;
}
public void addNode(Node newNode) { // 保存新的节点数据
// 第一次调用的时候没有节点,this = LinkImpl.root
// 如果不是空的,第二次调用的时候就是this = LinkImpl.root.next
// 同理,第三次为this = LinkImpl.root.next.next
// 第四次...第五次...直到找到下一个节点为空的节点
if(this.next == null) { // 如果当前节点的下一个节点为空
this.next = newNode; // 就表示可以增加新的节点
}else { // 如果不是空的
this.next.addNode(newNode); // 就将当前节点放到下一个节点处查看
}
}
}
//-----------------以下为Link类中定义结构---------------------
private Node root; // 保存根元素
//-----------------以下为Link类中定义结构---------------------
public void add(E e) {
if(e == null) { // 保存的数据为空
return; // 方法直接结束
}//数据之间本身是不具有关联特性的,只有Node类才有
//所以要想实现关联处理就必须将数据包装在Node类之中。
Node newNode = new Node(e); // 创建一个新的节点
if(this.root == null) { // 现在没有根节点
this.root = newNode;
}else { // 根节点存在
this.root.addNode(newNode);; // 下一个应该是新节点
}
}
}
public class LinkDemo {
public static void main( String args[] ){
ILink<String> link = new LinkImpl<String>();
link.add("火车头、");
link.add("车厢一号、");
link.add("车厢二号、");
link.add("车厢三号、");
}
}
编译之后没有问题,虽然现在还没有写打印数据的代码,但是这已经是一个链表的结构了。在这个结构中,LinkImpl类只是负责数据与根节点的处理,而所有后续节点的处理全部是由Node类来处理的,这就使得数据可以一直增加,这就是链表的数据结构。
3.获取数据长度
在链表之中往往需要有保存大量的数据,那么这些数据往往需要进行数据的个数的统计操作,所以应该在LinkImpl子类中添加有数据统计信息,同适当增加或删除数据时都对其个数进行修改,于是我们来看一下怎样实现。
在Link接口中追加一个获取数据的方法:
interface ILink<E> { // 设置泛型避免安全隐患
public void add(E e); // 增加数据
public int size(); // 获取数据个数
}
在LinkImpl子类中增加一个统计个数的属性:
//-----------------以下为Link类中定义结构---------------------
private Node root; // 保存根元素
private int count; // 保存数据个数
如何增加呢?在add()方法中进行数据的个数追加:
//-----------------以下为Link类中定义结构---------------------
public void add(E e) {
if(e == null) { // 保存的数据为空
return; // 方法直接结束
}//数据之间本身是不具有关联特性的,只有Node类才有
//所以要想实现关联处理就必须将数据包装在Node类之中。
Node newNode = new Node(e); // 创建一个新的节点
if(this.root == null) { // 现在没有根节点
this.root = newNode;
}else { // 根节点存在
this.root.addNode(newNode);; // 下一个应该是新节点
}
this.count ++;
}
同时要在LinkImpl子类中覆写返回count的个数:
public int size() {
return this.count;
}
现在我们来看一下有没有成功:
public class LinkDemo {
public static void main( String args[] ){
ILink<String> link = new LinkImpl<String>();
System.out.println("增加之前:" + link.size());
link.add("火车头、");
link.add("车厢一号、");
link.add("车厢二号、");
link.add("车厢三号、");
System.out.println("增加之后:" + link.size());
}
}
编译通过,运行结果如下:
增加之前:0
增加之后:4
虽然这只是一个辅助功能,但这个辅助功能是有它存在的意义的。
4.空集合的判断
链表中可以保存若干个数据,如果现在链表中没有提供数据,就可以说它是一个空的集合,那么就需要有一个空的判断。
在ILink接口中追加有判断方法:
public boolean isEmpty(); // 判断是否是空集合
在LinkImpl子类中提供覆写:
public boolean isEmpty() {
//return this.root == null;
return this.count == 0;
}
这里要注意,判断根节点或者数据长度对其判断本质是一样的。接下来我们使用一下:
public class LinkDemo {
public static void main( String args[] ){
ILink<String> link = new LinkImpl<String>();
System.out.println("增加之前:" + link.size() + "、是否为空:" + link.isEmpty());
link.add("火车头、");
link.add("车厢一号、");
link.add("车厢二号、");
link.add("车厢三号、");
System.out.println("增加之后:" + link.size() + "、是否为空:" + link.isEmpty());
}
编译通过,运行结果如下:
增加之前:0、是否为空:true
增加之后:4、是否为空:false
5.返回集合数据
链表本身就是属于动态对象数组,既然是一个对象数组,那就应该可以把所有的数据以数组的形式返回来,可以定义一个toArray()的方法,但是这个方法只能返回Object类型的数组。
现在我们来分析一下如何返回数据:首先以这个代码为例,代码中一共有四个数据,那么这四个数据在哪里体现?在count上体现,因为数据存储后count == 4,所以数组长度也应该等于数据的个数:4,也就是count,在这里要明白返回的数组是可以根据count的变化来动态开辟的。
然后我们会发现,如果要在数组中储存数据,第一个存储的应该是root.data,接下来应该是root.next.data,以此类推进行存储,同时也作为一个数组需要使用下标来进行索引,所以使用foot来进行索引,同时要注意到这个下标来对所有节点进行控制,所以应该定义在LinkImpl子类中。
接下来我们再来看一下如何实现,首先在ILink接口中追加新的方法:
public Object [] toArray(); // 将集合数据以数组形式返回
在LinkImpl子类中增加两个属性:
//-----------------以下为Link类中定义结构---------------------
private Node root; // 保存根元素
private int count; // 保存数据个数
private int foot; // 操作数组的下标
private Object[] returnData; // 返回的数据保存
这里为什么把返回的数据保存定义成为一个属性呢?因为在返回的时候要进行递归调用,而递归调用时数据要在Node类中处理,而返回的数组要在LinkImpl子类中处理,所以数据处理过程中要用到数组,而将数组定义为外部类中的私有属性,这个时候Node类就可以直接使用。
在LinkImpl子类中覆写方法:
public Object[] toArray() {
if(this.isEmpty()) { // 如果现在是一个空集合
return null; // 现在就没有数据
}
// 如果现在不是空集合
this.foot = 0; // 角标清零
this.returnData = new Object[this.count]; // 根据已有的长度动态开辟数组
// 利用Node类进行递归数据获取
return this.returnData; // 返回数组数据
}
这里要注意中间还有一段是要利用Node类进行递归数据获取:
public void toArrayNode() {
// 将当前数据存储到开辟的数组中
LinkImpl.this.returnData[LinkImpl.this.foot ++] = this.data;
if(this.next != null) { // 还有下一个数据
// 第一次调用:this = LinkImpl.root
// 第二次调用:this = LinkImpl.next.root
// 第三次调用:this = LinkImpl.next.next.root
// 以此类推...
this.next.toArrayNode();
}
}
然后补全这段代码:
this.root.toArrayNode();// 利用Node类进行递归数据获取
现在来看一下能不能实现:
public class LinkDemo {
public static void main( String args[] ){
ILink<String> link = new LinkImpl<String>();
System.out.println("增加之前:" + link.size() + "、是否为空:" + link.isEmpty());
link.add("火车头、");
link.add("车厢一号、");
link.add("车厢二号、");
link.add("车厢三号、");
System.out.println("增加之后:" + link.size() + "、是否为空:" + link.isEmpty());
Object result[] = link.toArray();
for(Object obj : result) {
System.out.print(obj);
}
}
}
编译通过,运行结果如下:
增加之前:0、是否为空:true
增加之后:4、是否为空:false
火车头、车厢一号、车厢二号、车厢三号、
集合的数据一般返回的时候都是以对象数组的形式返回的,虽然可以使用规定类型返回,但是不建议大家这样去做,因为返回的类型往往都是Object类型的。
6.根据索引取得数据
现在我们可以返回一个数组来获取数据,但是如果想要获取某一个数据的话又该怎么办呢,比如现在有四个数据,我就想获取第二个该怎么实现呢?大家可能会想到利用刚才的数组直接通过下标来打印出来,这个办法是可行的,但不是完美的,这样就引出了我们的下一个需求,通过下标索引来获取数据。
首先在IL接口中追加新的方法:
public E get(int index); // 通过索引获取数据
然后再LinkImpl的子类中覆写:
public E get(int index) {
if(index >= this.count) { // 索引应该在指定的范围之内
return null;
} // 索引获取的数据应该在Node类中完成
this.foot = 0; // 重置索引的下标
}
这时我们应该在Node类中追加一个根据索引获取数据的处理:
public E getNode(int index) {
if(LinkImpl.this.foot ++ == index) { // 如果索引相同
return this.data; // 返回当前数据
}else {
return this.next.getNode(index); // 继续进行下一次匹配
}
}
处理完成后在get()方法中添加:
return this.root.getNode(index);
接下来我们实现一下:
public class LinkDemo {
public static void main( String args[] ){
ILink<String> link = new LinkImpl<String>();
System.out.println("增加之前:" + link.size() + "、是否为空:" + link.isEmpty());
link.add("火车头、");
link.add("车厢一号、");
link.add("车厢二号、");
link.add("车厢三号、");
System.out.println("增加之后:" + link.size() + "、是否为空:" + link.isEmpty());
Object result[] = link.toArray();
for(Object obj : result) {
System.out.print(obj);
}
System.out.println("\n----------通过索引数据获取----------");
System.out.println(link.get(0));
System.out.println(link.get(2));
System.out.println(link.get(5));
}
}
编译通过,运行结果如下:
增加之前:0、是否为空:true
增加之后:4、是否为空:false
火车头、车厢一号、车厢二号、车厢三号、
----------通过索引数据获取----------
火车头、
车厢二号、
null
可以看到如果下标处没有数据,那么就会返回null,这个比数组更加完善;同时要了解到,数组获取一个数据的时间复杂度为 1 ,而链表获取一个数据的时间复杂度为 n。
7.通过索引修改指定的数据
现在我们已经可以通过索引来获取数据了,既然能够获取那就能够修改。
首先在ILink接口中追加新的方法:
public void set(int index, E data); // 通过索引修改数据
同时在Node类中应该提供数据修改的处理支持:
public void setNode(int index, E data) {
if(LinkImpl.this.foot ++ == index) { // 如果索引相同
this.data = data; // 修改当前数据
}else {
this.next.setNode(index, data); // 继续进行下一次匹配
}
}
在LinkImpl子类中进行覆写:
public void set(int index, E data) {
if(index >= this.count) { // 索引应该在指定的范围之内
return; // 方法结束
} // 索引获取的数据应该在Node类中完成
this.foot = 0; // 重置索引的下标
this.root.setNode(index, data); // 修改数据
}
现在我们来实现一下:
public class LinkDemo {
public static void main( String args[] ){
ILink<String> link = new LinkImpl<String>();
System.out.println("增加之前:" + link.size() + "、是否为空:" + link.isEmpty());
link.add("火车头、");
link.add("车厢一号、");
link.add("车厢二号、");
link.add("车厢三号、");
link.set(2, "没有车厢、");
System.out.println("增加之后:" + link.size() + "、是否为空:" + link.isEmpty());
Object result[] = link.toArray();
for(Object obj : result) {
System.out.print(obj);
}
System.out.println("\n----------通过索引数据获取----------");
System.out.println(link.get(0));
System.out.println(link.get(2));
System.out.println(link.get(5));
}
}
编译通过,运行结果如下:
增加之前:0、是否为空:true
增加之后:4、是否为空:false
火车头、车厢一号、没有车厢、车厢三号、
----------通过索引数据获取----------
火车头、
没有车厢、
null
这种操作的时间复杂度也是 n,因为要进行数据的遍历递归处理。
8.判断指定数据是否存在
在一个集合中往往存在大量的数据,有时候需要判断某个数据是否存在,这时候就需要使用equals()方法来比较实现判断。
首先在ILink接口中追加新的方法:
public boolean contains(E data); // 判断数据是否存在
同时在Node类中进行判断:
public boolean containsNode(E data) {
if(this.data.equals(data)) { // 对象比较
return true;
}else { // 有两种情况
if(this.next == null) { // 如果后面没有了
return false;
}else {
return this.next.containsNode(data); //继续判断
}
}
}
在LinkImpl子类中进行覆写:
public boolean contains(E data) {
if(data == null) { // 如果没有数据
return false;
}
return this.root.containsNode(data); // 交给Node类判断
}
现在我们来实现一下:
public class LinkDemo {
public static void main( String args[] ){
ILink<String> link = new LinkImpl<String>();
System.out.println("增加之前:" + link.size() + "、是否为空:" + link.isEmpty());
link.add("火车头、");
link.add("车厢一号、");
link.add("车厢二号、");
link.add("车厢三号、");
link.set(2, "没有车厢、");
System.out.println("增加之后:" + link.size() + "、是否为空:" + link.isEmpty());
Object result[] = link.toArray();
for(Object obj : result) {
System.out.print(obj);
}
System.out.println("\n----------通过索引数据获取----------");
System.out.println(link.get(0));
System.out.println(link.get(2));
System.out.println(link.get(5));
System.out.println("\n----------数据判断----------");
System.out.println(link.contains("车厢三号、"));
System.out.println(link.contains("车厢"));
}
}
编译通过,运行结果如下:
增加之前:0、是否为空:true
增加之后:4、是否为空:false
火车头、车厢一号、没有车厢、车厢三号、
----------通过索引数据获取----------
火车头、
没有车厢、
null
----------数据判断----------
true
false
由于整个链表没有空数据的存在,所以在判断的时候直接使用每一个节点的数据发出equals()方法的调用就可以了,正如在代码中contains()方法首先就进行了判断是否为空数据传入,或者可以直接在containsNode()方法中将if(this.data.equals(data))变为if(data.equals(this.data)),这样也可以保证数据的完善处理。
9.数据删除
数据的删除指的是可以从集合里面删除掉指定的一个数据内容,也就是说此时传递的是数据内容,那么要进行删除操作依然需要进行对象比较,但是对于集合数据的删除有两种情况:
![](https://img-blog.csdnimg.cn/20200219143547335.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzM5NzMyODY3,size_16,color_FFFFFF,t_70)
- 删除根结点:如果删除的是根节点,那么root后的节点应该成为root,root的next应该转到原本root.next节点上的next,这样就成功实现了删除根节点。
删除根节点 - 删除节点:如果要删除节点,首先要知道删除哪个节点,然后把删除节点的previous节点的next转到当前节点的next上。
删除节点
同时我们要知道,如果删除root节点,由于LinkImpl与根节点有关,所以应该在LinkImpl类中完成;如果删除节点,应该由Node类负责。
首先在ILink接口中追加新的删除方法:
public void remove(E e); // 数据删除
再LinkImpl子类中实现根节点的判断处理:
public void remove(E data) {
if(this.contains(data)) { // 判断数据是否存在
if(this.root.data.equals(data)) { // 根节点为要删除节点
this.root = this.root.next;
}
this.count --;
}
}
这样就可以实现根节点的删除了,我们实现一下:
public class LinkDemo {
public static void main( String args[] ){
ILink<String> link = new LinkImpl<String>();
System.out.println("增加之前:" + link.size() + "、是否为空:" + link.isEmpty());
link.add("火车头、");
link.add("车厢一号、");
link.add("车厢二号、");
link.add("车厢三号、");
link.set(2, "没有车厢、");
link.remove("火车头、");
System.out.println("增加之后:" + link.size() + "、是否为空:" + link.isEmpty());
Object result[] = link.toArray();
for(Object obj : result) {
System.out.print(obj);
}
System.out.println("\n----------通过索引数据获取----------");
System.out.println(link.get(0));
System.out.println(link.get(2));
System.out.println(link.get(5));
System.out.println("\n----------数据判断----------");
System.out.println(link.contains("车厢三号、"));
System.out.println(link.contains("车厢"));
}
}
编译通过,运行结果为:
增加之前:0、是否为空:true
增加之后:3、是否为空:false
车厢一号、没有车厢、车厢三号、
----------通过索引数据获取----------
车厢一号、
车厢三号、
null
----------数据判断----------
true
false
如果现在根节点不是要删除的节点,那么我们就要开始向下进行判断,由于我们删除节点时需要知道上一个节点,所以我们还要在Node类中追加删除处理:
public void removeNode(Node previous, E data) {
if(this.data.equals(data)) { // 当前节点是要删除的节点
previous.next = this.next; // 当前节点的上一个节点的next给当前节点的next
}else { // 如果不是
if(this.next != null) { // 如果还有下一个节点
this.next.removeNode(this, data);
}
}
}
同时要注意到,因为根节点已经判断完成了,所以我们要从下一个节点开始判断,完善LinkImpl子类中的代码:
public void remove(E data) {
if(this.contains(data)) { // 判断数据是否存在
if(this.root.data.equals(data)) { // 根节点为要删除节点
this.root = this.root.next;
}else { // 根节点不是,交给Node来处理
this.root.next.removeNode(this.root, data);
}
this.count --;
}
}
再来实现一下:
public class LinkDemo {
public static void main( String args[] ){
ILink<String> link = new LinkImpl<String>();
System.out.println("增加之前:" + link.size() + "、是否为空:" + link.isEmpty());
link.add("火车头、");
link.add("车厢一号、");
link.add("车厢二号、");
link.add("车厢三号、");
link.set(2, "没有车厢、");
link.remove("火车头、");
link.remove("车厢一号、");
System.out.println("增加之后:" + link.size() + "、是否为空:" + link.isEmpty());
Object result[] = link.toArray();
for(Object obj : result) {
System.out.print(obj);
}
System.out.println("\n----------通过索引数据获取----------");
System.out.println(link.get(0));
System.out.println(link.get(2));
System.out.println(link.get(5));
System.out.println("\n----------数据判断----------");
System.out.println(link.contains("车厢三号、"));
System.out.println(link.contains("车厢"));
}
}
编译通过,运行结果如下:
增加之前:0、是否为空:true
增加之后:2、是否为空:false
没有车厢、车厢三号、
----------通过索引数据获取----------
没有车厢、
null
null
----------数据判断----------
true
false
删除的逻辑就依靠的是引用的改变完成的。
10.清空链表
有些时候需要链表数据的整体清空处理,这个时候就可以这个直接根据根元素来控制,因为只要root节点被设置为NULL,那么后面的节点就都不存在了。
首先在ILink接口中追加处理方法:
public void clean(); // 清空集合
在LinkImpl子类中进行覆写:
public void clean() {
this.root = null; // 后续的节点都没了
this.count = 0; // 个数清零
}
这样就可以实现清空了,我们来实现一下:
public class LinkDemo {
public static void main( String args[] ){
ILink<String> link = new LinkImpl<String>();
System.out.println("增加之前:" + link.size() + "、是否为空:" + link.isEmpty());
link.add("火车头、");
link.add("车厢一号、");
link.add("车厢二号、");
link.add("车厢三号、");
link.set(2, "没有车厢、");
link.remove("火车头、");
link.remove("车厢一号、");
link.clean();
System.out.println("增加之后:" + link.size() + "、是否为空:" + link.isEmpty());
Object result[] = link.toArray();
for(Object obj : result) {
System.out.print(obj);
}
System.out.println("\n----------通过索引数据获取----------");
System.out.println(link.get(0));
System.out.println(link.get(2));
System.out.println(link.get(5));
System.out.println("\n----------数据判断----------");
System.out.println(link.contains("车厢三号、"));
System.out.println(link.contains("车厢"));
}
}
这里要注意因为现在clean()之后root为null了,所以我们要完善代码,分别是:
if(result != null) {
for(Object obj : result) {
System.out.print(obj);
}
}
public boolean contains(E data) {
if((data == null) || (this.root == null)) { // 如果没有数据
return false;
}
return this.root.containsNode(data); // 交给Node类判断
}
这两个地方需要增添判断,运行结果如下:
增加之前:0、是否为空:true
增加之后:0、是否为空:true
----------通过索引数据获取----------
null
null
null
----------数据判断----------
false
false
这就是链表的全部功能,这只是一个最简单最基础的单向链表实现,现在我们将完整代码贴上来:
interface ILink<E> { // 设置泛型避免安全隐患
public void add(E e); // 增加数据
public int size(); // 获取数据个数
public boolean isEmpty(); // 判断是否是空集合
public Object [] toArray(); // 将集合数据以数组形式返回
public E get(int index); // 通过索引获取数据
public void set(int index, E data); // 通过索引修改数据
public boolean contains(E data); // 判断数据是否存在
public void remove(E data); // 数据删除
public void clean(); // 清空集合
}
class LinkImpl<E> implements ILink<E>{
private class Node { // 保存节点与数据的关系
private E data; // 保存的数据
private Node next; // 保存的下一个节点
public Node(E data) { // 有数据的情况下才有意义
this.data = data;
}
public void addNode(Node newNode) { // 保存新的节点数据
// 第一次调用的时候没有节点,this = LinkImpl.root
// 如果不是空的,第二次调用的时候就是this = LinkImpl.root.next
// 同理,第三次为this = LinkImpl.root.next.next
// 第四次...第五次...直到找到下一个节点为空的节点
if(this.next == null) { // 如果当前节点的下一个节点为空
this.next = newNode; // 就表示可以增加新的节点
}else { // 如果不是空的
this.next.addNode(newNode); // 就将当前节点放到下一个节点处查看
}
}
public void toArrayNode() {
// 将当前数据存储到开辟的数组中
LinkImpl.this.returnData[LinkImpl.this.foot ++] = this.data;
if(this.next != null) { // 还有下一个数据
// 第一次调用:this = LinkImpl.root
// 第二次调用:this = LinkImpl.next.root
// 第三次调用:this = LinkImpl.next.next.root
// 以此类推...
this.next.toArrayNode();
}
}
public E getNode(int index) {
if(LinkImpl.this.foot ++ == index) { // 如果索引相同
return this.data; // 返回当前数据
}else {
return this.next.getNode(index); // 继续进行下一次匹配
}
}
public void setNode(int index, E data) {
if(LinkImpl.this.foot ++ == index) { // 如果索引相同
this.data = data; // 修改当前数据
}else {
this.next.setNode(index, data); // 继续进行下一次匹配
}
}
public boolean containsNode(E data) {
if(this.data.equals(data)) { // 对象比较
return true;
}else { // 有两种情况
if(this.next == null) { // 如果后面没有了
return false;
}else {
return this.next.containsNode(data); //继续判断
}
}
}
public void removeNode(Node previous, E data) {
if(this.data.equals(data)) { // 当前节点是要删除的节点
previous.next = this.next; // 当前节点的上一个节点的next给当前节点的next
}else { // 如果不是
if(this.next != null) { // 如果还有下一个节点
this.next.removeNode(this, data);
}
}
}
}
//-----------------以下为Link类中定义结构-----------------
private Node root; // 保存根元素
private int count; // 保存数据个数
private int foot; // 操作数组的下标
private Object[] returnData; // 返回的数据保存
//-----------------以下为Link类中定义结构-----------------
public void add(E e) {
if(e == null) { // 保存的数据为空
return; // 方法直接结束
}//数据之间本身是不具有关联特性的,只有Node类才有
//所以要想实现关联处理就必须将数据包装在Node类之中。
Node newNode = new Node(e); // 创建一个新的节点
if(this.root == null) { // 现在没有根节点
this.root = newNode;
}else { // 根节点存在
this.root.addNode(newNode);; // 下一个应该是新节点
}
this.count ++;
}
public int size() {
return this.count;
}
public boolean isEmpty() {
//return this.root == null;
return this.count == 0;
}
public Object[] toArray() {
if(this.isEmpty()) { // 如果现在是一个空集合
return null; // 现在就没有数据
}
// 如果现在不是空集合
this.foot = 0; // 角标清零
this.returnData = new Object[this.count]; // 根据已有的长度动态开辟数组
this.root.toArrayNode();// 利用Node类进行递归数据获取
return this.returnData; // 返回数组数据
}
public E get(int index) {
if(index >= this.count) { // 索引应该在指定的范围之内
return null;
} // 索引获取的数据应该在Node类中完成
this.foot = 0; // 重置索引的下标
return this.root.getNode(index);
}
public void set(int index, E data) {
if(index >= this.count) { // 索引应该在指定的范围之内
return; // 方法结束
} // 索引获取的数据应该在Node类中完成
this.foot = 0; // 重置索引的下标
this.root.setNode(index, data); // 修改数据
}
public boolean contains(E data) {
if((data == null) || (this.root == null)) { // 如果没有数据
return false;
}
return this.root.containsNode(data); // 交给Node类判断
}
public void remove(E data) {
if(this.contains(data)) { // 判断数据是否存在
if(this.root.data.equals(data)) { // 根节点为要删除节点
this.root = this.root.next;
}else { // 根节点不是,交给Node来处理
this.root.next.removeNode(this.root, data);
}
this.count --;
}
}
public void clean() {
this.root = null; // 后续的节点都没了
this.count = 0; // 个数清零
}
}
public class LinkDemo {
public static void main( String args[] ){
ILink<String> link = new LinkImpl<String>();
System.out.println("增加之前:" + link.size() + "、是否为空:" + link.isEmpty());
link.add("火车头、");
link.add("车厢一号、");
link.add("车厢二号、");
link.add("车厢三号、");
link.set(2, "没有车厢、");
link.remove("火车头、");
link.remove("车厢一号、");
link.clean();
System.out.println("增加之后:" + link.size() + "、是否为空:" + link.isEmpty());
Object result[] = link.toArray();
if(result != null) {
for(Object obj : result) {
System.out.print(obj);
}
}
System.out.println("\n----------通过索引数据获取----------");
System.out.println(link.get(0));
System.out.println(link.get(2));
System.out.println(link.get(5));
System.out.println("\n----------数据判断----------");
System.out.println(link.contains("车厢三号、"));
System.out.println(link.contains("车厢"));
}
}
链表是Java中非常重要的一个板块,需要很好的掌握它。同时这篇文章也是Java系列的一个小结尾,Java还有很多高级特性比如:线程与进程、线程的同步与死锁、“生产者—消费者”模型、比较器、类库、对象序列化、反射、Map、JDBC等等很多的特性都还没有整理,以后会慢慢整理,但不是接下来的这段时间,接下来的这段时间我会将C语言中的数据结构慢慢整理一遍,我们下次见👋