链表的定义与使用
- 链表的基本概念
- 链表开发入门(实现的基本原理)
- 开发可用链表
- 实现数据增加操作:public void add(Object data)
- 取得保存元素个数:public int size()
- 判断是否为空集合:public boolean isEmpty()
- 数据查询:public boolean contains(Object data)
- 根据索引取得数据:public Object get(int Index)
- 修改数据:public void set(int index,Obect obj)
- 数据删除: public void remove(Object obj)
- 清空链表:public void clear()
- 返回数据:public Object [] toArray()
- 总结
链表的基本概念
链表=可变长的对象数组,属于动态对象数组范畴。
对象数组有哪些问题呢?
正因为如此现在如果要想让其可以编写出便于维护的代码,那么就需要实现一个动态对象数组,就可以使用链表完成。
但是现在如果想要实现动态的对象数组,要考虑两个问题:
- 为了适应所有的开发要求,此对象数组要求可以保存所有的数据类型,那么一定首选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)
- 需要在接口里面定义好数据增加的操作方法;
interface Link{
public void add(Object data);//数据增加
}
- 进行代码的实现,同样实现的过程之中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类来负责处理。
- 在Link接口里面创建一个新的方法:
interface Link{
public void add(Object data);//数据增加
public int size();//取得保存元素的个数
public boolean isEmpty();//判断是否为空集合
public boolean contains(Object obj);//判断是否有指定元素
}
- 在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)
链表本身属于动态的对象数组,所以数组本身一定会提供有根据索引取得数据的操作支持,那么在链表中也可以定义与之类似的方法。
- 修改LinkImpl类,为其增加一个foot的属性,之所以将foot属性定义在LinkImpl类之中是为了方便几个Node类共同进行属性的操作使用的,同时内部类可以方便的访问外部类中的私有成员;
private int foot = 0;//操作的索引脚标
- 在Link接口里面首先定义出新的操作方法
public Object get(int index);//根据索引取得内容,索引从0开始
- 在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()只需要用新的数据更新已有节点数据即可。
- 在Link接口中定义新的方法:
public void set(int index,Object obj);
- 修改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)
链表的数据删除就是节点的删除操作,此过程中需要考虑:要删除的是根节点还是子节点问题。
- 删除根节点: Link处理
- 删除子节点:Node处理
- 在Link接口里定义新的方法
public void remove(Object data);//删除数据
- 修改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。
- 在Link接口里面定义新的操作方法:
public void clear();
- 直接在LinkImpl类中修改清空操作:
public void clear(){
this.root=null;
this.count=0;
System.gc();//回收内存空间
}
实际上这样的代码存在内存释放问题。调用gc()方法回收内存。
返回数据:public Object [] toArray()
链表即动态对象数组,操作链表中的数据最好的做法就是将其转换为对象数组返回,所以这个时候需要针对数据做递归处理。
- 在Link中定义返回数组的方法
public Object[] toArray();
- 修改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 | 方法名称 | 类型 | 描述 |
---|---|---|---|
1 | public void add(Object data) | 普通 | 向集合追加数据 |
2 | public int size() | 普通 | 取得集合中保存的元素个数 |
3 | public boolean isEmpty() | 普通 | 判断是否为空集合 |
4 | public boolean contains(Object data) | 普通 | 判断是否有指定的元素,需要equals()支持 |
5 | public Object get(int Index) | 普通 | 根据索引取得指定数据 |
6 | public void set(int index,Obect obj) | 普通 | 修改指定索引位置上的数字 |
7 | public void remove(Object obj) | 普通 | 数据删除操作,需要equals()支持 |
8 | public void clear() | 普通 | 清空链表 |
9 | public Object [] toArray() | 普通 | 链表转换为对象数组 |