链表数据结构实现

链表的基本概念

链表=可变长的对象数组,属于动态对象数组范畴。
对象数组有哪些问题呢?
在这里插入图片描述
正因为如此现在如果要想让其可以编写出便于维护的代码,那么就需要实现一个动态对象数组,就可以使用链表完成。
但是现在如果想要实现动态的对象数组,要考虑两个问题:

  • 为了适应所有的开发要求,此对象数组要求可以保存所有的数据类型,那么一定首选Object类型;
  • 为了可以保存多个数据,需要采用引用的方式来进行保存,但是数据本身是不可能保存顺序的,所以需要有一个可以负责保存顺序的类来包装这个数据。
    在这里插入图片描述
    通过以上的分析就可以得出如下的结论:
  • 保存数据为了方便使用Object;
  • 数据本身不包含有先后的逻辑关系,所以将数据封装在一个Node类,负责关系的维护。
    范例:定义如下一个类
class Node{//表示定义节点
	private Object data;//要保存的数据
	private Node next;//保存下一个节点
	public Node(Object data){
		this.data=data;
	}
	public void setNext(Node next){//设置节点
		this.next=next;
	}
	public Node getNext(){//取得节点
		return this.next;
	}
}
public Object getData(){
		return this.data;
	}

完成了节点之后就可以进行节点的基本使用了。
在这里插入图片描述
范例:采用循环的方式操作节点

public class Hello{
	public static void main(String args[]){
		//1. 定义各自独立的操作节点
		Node root = new Node("火车头");
		Node n1 = new Node("车厢1");
		Node n2 = new Node("车厢2");
		//2.设置彼此间的关系
		root.setNext(n1);
		n1.setNext(n2);
		//3.输出
		Node currentNode = root;//从根节点开始取出数据
		while(currentNode!=null){//当前有节点
			System.out.println(currentNode.getData());//取数据
			currentNode = currentNode.getNext();//下一个节点
		}
	}
}

以上的操作如果使用循环并不方便。最好的做法是递归调用。
范例:利用递归的方式实现内容的取得

public class Hello{
	public static void main(String args[]){
		//1. 定义各自独立的操作节点
		Node root = new Node("火车头");
		Node n1 = new Node("车厢1");
		Node n2 = new Node("车厢2");
		//2.设置彼此间的关系
		root.setNext(n1);
		n1.setNext(n2);
		print(root);
	}
	public static void print(Node node){
		if(node==null){
			return;//结束方法调用
		}
		System.out.println(node.getData());
		print(node.getNext());
	}
}

链表的实现关键就是Node类,Node类要保存数据与下一个节点。

链表开发入门(实现的基本原理)

在这里插入图片描述
现在需要一个负责所有的Node的关系匹配的类,用户只需要通过这个类保存或取得数据即可,那么就可以编写一个Link类完成。
范例:基本结构

class Link{//表示一个链表操作类,利用此类来隐藏Node的节点匹配
	public void add(Object obj){//向链表中追加数据
		
	}
	public void print(){//输出链表中的全部数据
		
	}
}
public class Hello{
	public static void main(String args[]){
		Link all = new Link();
		all.add("商品1");
		all.add("商品2");
		all.add("商品3");
		all.print();
	}
}

用户不关心Node,用户只关心通过Link操作完成后可以取得数据。
在这里插入图片描述
范例:完善程序

class Node{//表示定义节点
	private Object data;//要保存的数据
	private Node next;//保存下一个节点
	public Node(Object data){
		this.data=data;
	}
	public void setNext(Node next){//设置节点
		this.next=next;
	}
	public Node getNext(){//取得节点
		return this.next;
	}
	public Object getData(){
		return this.data;
	}
	//第一次调用:this=Link.root
	//第二次调用:this=Link.root.next
	//第三次调用:this=Link.root.next.next
	public void addNode(Node newNode){
		if(this.next==null){//当前节点之后没有节点
			this.next=newNode;
		}else{//如果现在当前节点后有节点
			this.next.addNode(newNode);
		}
	}
	//第一次调用:this.Link.root
	//第二次调用:this.Link.root.next
	public void printNode(){
		System.out.println(this.data);//当前节点数据
		if(this.next!=null){//还有后续节点
			this.next.printNode();
		}
	}
}
class Link{//表示一个链表操作类,利用此类来隐藏Node的节点匹配
	private Node root;//需要一个根元素
	public void add(Object obj){//向链表中追加数据
		//将要操作的数据包装为Node类对象,这样才可以进行先后关系的排列
		Node newNode = new Node(obj);
		//现在没有根节点
		if(this.root==null){//this出现在Link类,表示Link类的当前对象
			this.root=newNode;//将第一个节点作为根节点
		}else{//根节点存在了,交由Node类处理
			this.root.addNode(newNode);//由根节点负责调用
		}
	}
	public void print(){//输出链表中的全部数据
		if(this.root!=null){//现在有数据
			this.root.printNode();//输出节点数据
		}
	}
}
public class Hello{
	public static void main(String args[]){
		Link all = new Link();
		all.add("商品1");
		all.add("商品2");
		all.add("商品3");
		all.print();
	}
}

此时的代码就实现了链表的基本操作,整个过程中,用户不关心Node的处理,只关心数据的保存和输出。

开发可用链表

以上的代码只能够说是基本的链表结构形式,但是从另外一方面,以上的代码给我们提供了链表的实现思路,那么如何才能够设计一个好的链表呢?
链表的实现必须依靠Node类,但是整个的过程之中,用户不需要操作Node。而且通过代码可以发现,Node类里的操作有其特定的需要。但是这个时候Node类写在了外面,表示用户可以操作Node类的对象,现在的问题在于:如何可以让Node类只为Link类服务,但是又不让其被其他类所访问,自然想到可以使用内部类来完成,而且内部类的好处在于:可以与外部类进行私有属性的访问。
范例:合理的结构规划

class LinkImpl{//外部类只关心此类
	private class Node{//使用私有内部类,防止外部类使用此类
		private Object data;//要保存的数据
		private Node next;//保存下一个节点
		public Node(Object data){
			this.data=data;
		}
	}
	//****************************************************************
	private Node root;//根元素
}

要开发程序就一定要创建出自己的开发标准,一旦说到标准,就应该想到使用接口来完成。

interface Link{
}
class LinkImpl implements Link{//外部类只关心此类
	private class Node{//使用私有内部类,防止外部类使用此类
		private Object data;//要保存的数据
		private Node next;//保存下一个节点
		public Node(Object data){
			this.data=data;
		}
	}
	//****************************************************************
	private Node root;//根元素

}
在随后完善代码的过程中,除了功能的实现之外,实际上也属于接口功能的完善。

实现数据增加操作:public void add(Object data)

  1. 需要在接口里面定义好数据增加的操作方法;
 interface Link{
	public void add(Object data);//数据增加
}
  1. 进行代码的实现,同样实现的过程之中LinkImpl类只关心根节点,而具体的子节点的排列都交由Node类负责处理;
  • 在Link类中实现add()方法
public void add(Object data){//向链表中追加数据
		if(data==null){//没有要增加的数据
			return;//结束调用
		}
		Node newNode = new Node(data);//创建新的节点
		if(this.root==null){
			this.root=newNode;
		}else{//应该交由Node类负责处理
			this.root.addNode(newNode);
		}
	}
  • 在Node类中进行数据的追加操作;
public void addNode(Node newNode){
			if(this.next==null){
				this.next=newNode;
			}else{
				this.next.addNode(newNode);
			}
		}

此时的代码实现与基本的代码实现是完全一样的。

取得保存元素个数:public int size()

每一个Link接口的对象都要保存各自的内容,所以为了方便控制保存个数,可以增加一个Link类的属性,用此属性在数据成功追加之后实现自增操作。

  • 在Link类中定义一个count属性,默认值为0;
private int count = 0;
  • 当数据已经成功添加完毕之后,实现计数的统计;
public void add(Object data){//向链表中追加数据
		if(data==null){//没有要增加的数据
			return;//结束调用
		}
		Node newNode = new Node(data);//创建新的节点
		if(this.root==null){
			this.root=newNode;
		}else{//应该交由Node类负责处理
			this.root.addNode(newNode);
		}
		this.count++;
	}

而在Link接口里面追加size()的方法,同时在LinkImpl子类里进行方法的覆写。

interface Link{
	public void add(Object data);//数据增加
	public int size();//取得保存元素的个数
}
public int size(){
		return this.count;
	}

此操作直接与最后的输出有关。

判断是否为空集合:public boolean isEmpty()

如果想要判断集合是否为空,有两种方式:长度为0,另外一个是判断根元素是否为null。
范例:在Link接口中追加一个新的方法:isEmpty

interface Link{
	public void add(Object data);//数据增加
	public int size();//取得保存元素的个数
	public boolean isEmpty();//判断是否为空集合
}

范例:在LinkImpl类中实现此方法

public boolean isEmpty(){
		return this.root==null;
	}

实际上此操作与size()几乎一脉相承。

数据查询:public boolean contains(Object data)

在这里插入图片描述
任何情况下,Link只负责与根元素有关的内容,而所有的其他元素的变更、查找、关系的匹配都应该交由Node类来负责处理。

  1. 在Link接口里面创建一个新的方法:
interface Link{
	public void add(Object data);//数据增加
	public int size();//取得保存元素的个数
	public boolean isEmpty();//判断是否为空集合
	public boolean contains(Object obj);//判断是否有指定元素
}
  1. 在LinkImpl子类里面要通过根元素开始调用查询,所有的查询交由Node类负责。
  • 在LinkImpl发出具体查询要求之前,必须保证有集合数据;
public boolean contains(Object data){
		if(this.root==null){//没有集合数据
			return false;
		}
		return this.root.containsNode(data);//根元素交给Node完成
	}
  • Node类中实现数据的查询
public boolean containsNode(Object data){
			if(this.data.equals(data)){//该节点数据符合查找数据
				return true;
			}else{//继续向下查找
				if(this.next!=null){//当前节点之后还有下一个节点
					return this.next.containsNode(data);
				}else{
					return false;
				}
			}
		}

这种查询的模式实质上也属于逐行的判断扫描。

根据索引取得数据:public Object get(int Index)

链表本身属于动态的对象数组,所以数组本身一定会提供有根据索引取得数据的操作支持,那么在链表中也可以定义与之类似的方法。
在这里插入图片描述
在这里插入图片描述

  1. 修改LinkImpl类,为其增加一个foot的属性,之所以将foot属性定义在LinkImpl类之中是为了方便几个Node类共同进行属性的操作使用的,同时内部类可以方便的访问外部类中的私有成员;
private int foot = 0;//操作的索引脚标
  1. 在Link接口里面首先定义出新的操作方法
public Object get(int index);//根据索引取得内容,索引从0开始
  1. 在LinkImpl类里面定义功能实现:
  • 在Node类中应该提供有一个getNode()方法,那么这个方法的功能是依次判断每一个索引值的操作形式。
public Object getNode(int index){//传递索引序号
			if(LinkImpl.this.foot++ ==index){//当前的索引为要查找的索引
				return this.data;//返回当前节点对象
			}else{
				return this.next.getNode(index);
			}
		}
  • 在Link类中实现get()方法;
public Object get(int index){
		if(index>=this.count){//索引不存在
			return null;
		}
		this.foot=0;//查询前执行一次清零操作
		return this.root.getNode(index);
	}

在这里插入图片描述

修改数据:public void set(int index,Obect obj)

与get()相比,set()方法依然需要进行循环判断,只不过get()索引判断成功之后会返回数据,而set()只需要用新的数据更新已有节点数据即可。

  1. 在Link接口中定义新的方法:
public void set(int index,Object obj);
  1. 修改LinkImpl子类,流程与get()差别不大;
  • 在Node类中追加一个setNode()方法
public void setNode(int index,Object data){
			if(LinkImpl.this.foot++ == index){
				this.data=data;//重新保存数据
			}else{
				this.next.setNode(index, data);
			}
		}
  • 在LinkImpl子类里面覆写set()方法,在set()方法编写的时候也需要针对给定的索引进行验证;
public void set(int index,Object data){
		if(index>=this.count){//索引不存在
			return;
		}
		this.foot=0;//查询前执行一次清零操作
		this.root.setNode(index,data);
	}

set()与get()方法实际上在使用时都有一个固定的条件:集合中的保存数据顺序应该为添加顺序。

数据删除: public void remove(Object obj)

链表的数据删除就是节点的删除操作,此过程中需要考虑:要删除的是根节点还是子节点问题。

  1. 删除根节点: Link处理
    在这里插入图片描述
  2. 删除子节点:Node处理
    在这里插入图片描述
  3. 在Link接口里定义新的方法
public void remove(Object data);//删除数据
  1. 修改LinkImpl操作
  • 在Node类中增加removeNode()方法;
//第一次:this=LinkImpl.root.next、previous=LinkImpl.root;
//第二次:this=LinkImpl.root.next.next、previous=LinkImpl.root.next;
public void removeNode(Node previous,Object data){
	if(this.data.equals(data)){//为当前要删除的节点
		previous.next=this.next;//空出当前节点
	}else{
		this.next.removeNode(this, data);
	}
}
  • 在Link类中增加新的操作
public void remove(Object data){
		if(this.contains(data)){//数据如果存在则删除
			if(this.root.data.equals(data)){//根元素为要删除的元素
				this.root=this.root.next;//第二个元素作为根元素
			}else{//不是根元素,根元素已经判断完了
				this.root.next.removeNode(this.root, data);
			}
			this.count--;
		}
	}

整个删除操作很好的体现了this的特征。

清空链表:public void clear()

当整个链表中的数据不用的时候,可以进行清空操作,最简单的做法是将root设置为null。

  1. 在Link接口里面定义新的操作方法:
public void clear();
  1. 直接在LinkImpl类中修改清空操作:
public void clear(){
		this.root=null;
		this.count=0;
		System.gc();//回收内存空间
	}

实际上这样的代码存在内存释放问题。调用gc()方法回收内存。

返回数据:public Object [] toArray()

链表即动态对象数组,操作链表中的数据最好的做法就是将其转换为对象数组返回,所以这个时候需要针对数据做递归处理。
在这里插入图片描述

  1. 在Link中定义返回数组的方法
public Object[] toArray();
  1. 修改LinkImpl子类。
  • 由于Node类需要操作链表数据读取,所以应该在LinkImpl子类里面提供有一个对象数组的属性。
public Object retData [] = null;
  • 在LinkImpl子类里面覆写toArray()方法,并且根据长度开辟数组空间。
public Object [] toArray(){
		if(this.root==null){//没有数据
			return null;
		}
		this.retData = new Object [this.count];
		this.foot=0;
		this.root.toArrayNode();
		return this.retData;
	}
  • 在Node中实现数据的保存操作
public void toArrayNode(){
			LinkImpl.this.retData[LinkImpl.this.foot++]=this.data;
			if(this.next!=null){
				this.next.toArrayNode();
			}
		}

以上设计都没有考虑过性能问题。

总结

1.以上只是简单的单向链表,要求清楚大概的原理;
2. 对于以下的方法一定要掌握。

No方法名称类型描述
1public void add(Object data)普通向集合追加数据
2public int size()普通取得集合中保存的元素个数
3public boolean isEmpty()普通判断是否为空集合
4public boolean contains(Object data)普通判断是否有指定的元素,需要equals()支持
5public Object get(int Index)普通根据索引取得指定数据
6public void set(int index,Obect obj)普通修改指定索引位置上的数字
7public void remove(Object obj)普通数据删除操作,需要equals()支持
8public void clear()普通清空链表
9public Object [] toArray()普通链表转换为对象数组
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值