Java链表设计
链表
1,链表的实现
在实际开发之中对象数组是一项非常实用的技术,并且利用其可以描述出“多”方的概念,例如:一个人有多本书,则在人的类里面一定要提供有一个对象数组保存书的信息,但是传统的对象数组依赖于数组的概念,所以数组里面最大的缺点在于:长度是固定的,正是因为如此在实际开发之中,传统的数组应用是非常有限的(数组的接收与循环处理),如果想要实现灵活的数据保存,那么就必须自己来实现结构。
传统的对象数组的开发依赖于脚标(索引)的控制,如果想要实现内容的动态控制,那么难度太高了,而且复杂度攀升,所以现在就可以,对于一成不变的数据可以使用对象数组来实现,但是对于可能随时变化的数据就必须实现一个可以动态扩充的对象数组。
所谓的链表实质性的本质是利用引用的逻辑关系来实现类似于数组的数据处理操作,以保存多方数据的数组类似的功能
通过分析可得,如果想要实现链表处理,那么需要有一个公共的结构,这个结构可以实现数据的保存以及下一个数据的连接的指向,为了描述这样一个逻辑,可以把每个存储理解为一个节点类,所以此时应该准备出一个节点类出来,但是这个节点类里面可以保存各种类型类型的数据。
虽然已经清楚需要使用Node节点进行数据的保存,但是毕竟这里面需要牵扯到节点的引用处理关系,那么这个引用处理关系是由使用者来控制吗?这样肯定不可能,所以应该由一个专门的类来进行节点的引用关系的配置。
范例:直接操作node很麻烦
1 class Node<E>{ 2 private E data; 3 private Node next; 4 public Node(E data) { 5 this.data = data; 6 } 7 public E getData() { 8 return this.data; 9 } 10 public void setNext(Node<E> next) { 11 this.next = next; 12 } 13 public Node getNext() { 14 return this.next; 15 } 16 } 17 public class Main { 18 public static void main(String[] args) { 19 Node<String> n1=new Node<String>("火车头"); 20 Node<String> n2=new Node<String>("车厢一"); 21 Node<String> n3=new Node<String>("车厢二"); 22 Node<String> n4=new Node<String>("车厢三"); 23 Node<String> n5=new Node<String>("车厢四"); 24 25 n1.setNext(n2); 26 n2.setNext(n3); 27 n3.setNext(n4); 28 n4.setNext(n5); 29 30 print(n1); 31 // System.out.println("Hello World!"); 32 } 33 public static void print(Node<?> node){ 34 if (node!=null){//存在节点 35 System.out.println(node.getData()); 36 print(node.getNext());//递归调用 37 } 38 } 39 }
这样肯定不可能,所以应该由专门的类来进行节点的引用关系的配置。因为真实的使用者实际上关心的只是数据的存取与获取,所以现在应该对Node类进行包装处理。
2,数据增加
通过之前的分析可以发现在进行链表操作的过程之中为了避免转型的异常处理应该使用泛型,同时也应该设计一个链表的执行标准的接口,同时具体实现该接口的时候还应该通过Node类做出节点的关系描述。
范例:基本结构
1 interface ILink<E>{ 2 public void add(E e); 3 } 4 class LinkImpl<E> implements ILink<E>{ 5 private class Node{//保存节点的数据关系,【内部类形式】 6 private E data;//保存的数据 7 public Node(E data){//有数据的情况下才有意义 8 this.data=data; 9 } 10 } 11 //--------------以下为Link类中定义的结构--------- 12 }
范例:实现数据增加现在在所定义的Node类之中并没有出现setter和getter方法,是因为内部类中的私有属性也方便外部类直接访问。
1 interface ILink<E>{ 2 public void add(E e); 3 } 4 class LinkImpl<E> implements ILink<E>{ 5 private class Node{//保存节点的数据关系,【内部类形式】 6 private E data;//保存的数据 7 private Node next; 8 public Node(E data){//有数据的情况下才有意义 9 this.data=data; 10 } 11 //第一次调用:this = LinkImpl.root; 12 //第二次调用:this = LinkImpl.root.next; 13 //第三次调用:this = LinkImpl.root.next.next; 14 public void addNode(Node newNode){//保存新的node数据 15 if(this.next==null){//当前节点的下一个节点为空 16 this.next=newNode;//保存当前节点 17 }else{ 18 this.next.addNode(newNode); 19 } 20 } 21 } 22 //--------------以下为Link类中定义的成员--------- 23 private Node root;//保存根元素 24 //--------------以下为Link类中定义的方法--------- 25 26 @Override 27 public void add(E e) { 28 if(e==null){//保存的数据为空 29 return; 30 } 31 //数据本身不具有关联特性的,只有Node类有,那么要想实现关联处理就必须将数据封装在Node类中 32 Node newNode=new Node(e); 33 if(this.root==null){//现在没有根节点 34 this.root=newNode; 35 }else {//根节点存在 36 this.root.addNode(newNode);//将新节点保存在合适的位置 37 } 38 39 } 40 } 41 public class Main { 42 public static void main(String[] args) { 43 ILink<String> all=new LinkImpl<String>(); 44 all.add("wanyu"); 45 all.add("hello"); 46 all.add("world"); 47 all.add("great"); 48 } 49 50 }
Link类只是负责数据的操作与根节点的处理,而所有后续节点的处理全部都是由Node类负责完成的。
3,获取集合个数 public int size()
在链表中往往需要保存有大量的数据,那么这些数据往往需要进行数据个数的统计操作,所以应该在LinkImpl子类里面追加有数据统计信息,同时增加与删除时都应该对个数进行修改。
①在ILink接口里追加一个获取数据个数的方法:
1 interface ILink<E>{//设置泛型避免安全隐患 2 public void add(E e);//增加数据 3 public int size();//获取数据的个数 4 }
②在LinkImpl的子类中追加有一个个数统计的子类
1 private int count;
③在add()的方法里面进行数据个数的追加
1 @Override 2 public void add(E e) { 3 if(e==null){//保存的数据为空 4 return; 5 } 6 //数据本身不具有关联特性的,只有Node类有,那么要想实现关联处理就必须将数据封装在Node类中 7 Node newNode=new Node(e); 8 if(this.root==null){//现在没有根节点 9 this.root=newNode; 10 }else {//根节点存在 11 this.root.addNode(newNode);//将新节点保存在合适的位置 12 } 13 this.count++; 14 }
④在LinkImpl的子类中里面来返回数据的个数
1 public int size(){ 2 return this.count; 3 }
只是对于数据保存中的一个辅助功能。
范例:完整程序
1 interface ILink<E>{//设置泛型避免安全隐患 2 public void add(E e);//增加数据 3 public int size();//获取数据的个数 4 } 5 class LinkImpl<E> implements ILink<E>{ 6 private class Node{//保存节点的数据关系,【内部类形式】 7 private E data;//保存的数据 8 private Node next; 9 public Node(E data){//有数据的情况下才有意义 10 this.data=data; 11 } 12 //第一次调用:this = LinkImpl.root; 13 //第二次调用:this = LinkImpl.root.next; 14 //第三次调用:this = LinkImpl.root.next.next; 15 public void addNode(Node newNode){//保存新的node数据 16 if(this.next==null){//当前节点的下一个节点为空 17 this.next=newNode;//保存当前节点 18 }else{ 19 this.next.addNode(newNode); 20 } 21 } 22 } 23 //--------------以下为Link类中定义的成员--------- 24 private Node root;//保存根元素 25 private int count; 26 //--------------以下为Link类中定义的方法--------- 27 @Override 28 public void add(E e) { 29 if(e==null){//保存的数据为空 30 return; 31 } 32 //数据本身不具有关联特性的,只有Node类有,那么要想实现关联处理就必须将数据封装在Node类中 33 Node newNode=new Node(e); 34 if(this.root==null){//现在没有根节点 35 this.root=newNode; 36 }else {//根节点存在 37 this.root.addNode(newNode);//将新节点保存在合适的位置 38 } 39 this.count++; 40 } 41 public int size(){ 42 return this.count; 43 } 44 } 45 public class Main { 46 public static void main(String[] args) { 47 ILink<String> all=new LinkImpl<String>(); 48 System.out.println("【增加之前:】数据个数:"+all.size()); 49 all.add("wanyu"); 50 all.add("hello"); 51 all.add("world"); 52 all.add("great"); 53 System.out.println("【增加之后:】数据个数:"+all.size()); 54 } 55 }
4,空集合判断public boolean isEmpty()
链表里面可以保存有若干个数据,如果说现在链表还没有保存数据,则就表示是一个空集合。
①在ILink接口里面追加有判断方法:
1 public boolean isEmpty();//判断是否为空集合
使用根节点和个数本质一样。
②在LinkImpl子类里面覆写此方法;
1 public boolean isEmpty(){ 2 // return this.root==null; 3 return this.count==0; 4 }
5,返回集合数据 public Object[] toArray()
链表本身就属于一个动态对象数组,那么既然属于一个对象数组,就应该可以把所有的数据以数组的形式返回,那么这个时候就可以定义一个toArray()方法,但是这个时候的方法只能够返回Object型的数组。
①在ILink接口里面追加新的处理方法
1 public Object[] toArray();//将集合元素以数组的形式返回
②在LinkImpl子类里面追加有两个属性,
1 private int foo;//描述操作数组的脚标 2 private Object[] returnData;//返回的数据保存
③在Node类中根据递归获取数据
1 //第一次调用:this=LinkImpl.root 2 //第二次调用:this=LinkImpl.root.next 3 //第三次调用:this=LinkImpl.root.next.mext 4 public void toArrayNode(){ 5 LinkImpl.this.returnData [foo++]=this.data; 6 if (this.next!=null){//还有下一个数据 7 this.next.toArrayNode(); 8 } 9 }
④在进行数据返回的时候一定要首先判断是否为空集合;
1 @Override 2 public Object[] toArray() { 3 if(this.isEmpty()){//空集合 4 return null;//现在没有数据 5 } 6 this.foo=0;//脚标清零 7 this.returnData=new Object[this.count]; 8 9 //根据已有的数据长度开辟数组 10 this.root.toArrayNode();//利用Node类进行递归获取数据 11 return this.returnData; 12 }
集合的数据一般如果要返回肯定要以对象的形式返回。
6,根据索引取得数据public E get(int index)
链表可以向数组一样处理,所以也应该可以向数组一样进行索引数据的获取,再这样的情况下我们可以使用递归的形式来完成。
①在ILink接口中追加有新的方法
1 public E get(int index);//根据索引获取数据
②在Node类里面追加有根据索引获取数据的处理
1 @Override 2 public E get(int index) { 3 if (index>=this.count){//索引应该在指定的范围之内 4 return null; 5 }//索引数据的获取应该由Node类完成 6 this.foot=0;//重置索引的下标 7 return this.root.getNode(index); 8 }
这一特点和数组很相似,但是需要注意,数组获取一个数据的时间复杂度为1,而链表获取一个数据的时间复杂度为n。
7,链表(修改指定索引数据)
现在已经可以根据索引来获取指定的数据了,但是既然可以获取数据,那么也可以进行数据的修改。
①在ILink接口中追加新的方法
1 public void set(int index,E data);//修改索引数据
②在Node类之中应该提供有数据修改的处理支持
1 public void setNode(int index,E data){ 2 if(LinkImpl.this.foot++==index){//索引相同 3 this.data=data;//返回当前数据 4 }else{ 5 this.next.setNode(index,data); 6 } 7 }
③在LinkImpl中进行方法覆写
1 @Override 2 public void set(int index, E data) { 3 if (index>=this.count){//索引应该在指定的范围之内 4 return ;//方法结束 5 }//索引数据的获取应该由Node类完成 6 this.foot=0;//重置索引的下标 7 this.root.setNode(index,data);//修改数据 8 }
这种操作的时间复杂度也是N,因为也是需要进行数据的遍历处理
8,链表(判断数据是否存在)public boolean contains(E data)
在一个集合里面往往会保存有大量的数据,有些时候需要判断某个数据是否存在,这个时候就可以通过对象比较的模型(equals())进行判断。
①在ILink接口中追加判断方法
1 public boolean contains(E data);//判断数据是否存在
②在Node类中进行依次判断
1 public boolean containsNode(E data) { 2 if(this.data.equals(data)){//对象比较 3 return true; 4 }else { 5 if(this.next==null){//没有后续节点了 6 return false;//找不到数据 7 }else { 8 return this.next.containsNode(data); 9 } 10 } 11 }
③在LinkImpl子类里面实现此方法
1 @Override 2 public boolean contains(E data) { 3 if(data==null){ 4 return false; 5 } 6 return this.root.containsNode(data);//交给Node类判断 7 }
由于整个链表没有null数据的存在,所以整体的程序在判断的时候直接使用每个节点数据发出的equals()方法调用即可。
9,链表(数据删除)public void remove(E data)
数据的删除指的是可以从集合里面删除掉指定的一个数据内容,也就是说此时传递的是数据内容,那么如果要实现这种的删除操作,依然需要对象的比较的支持,但是对于集合数据的删除需要考虑两种情况:
·要删除的是根节点(LinkImpl与根节点有关,所以这个判断由根节点完成):
·要删除的不是根节点(由Node类负责):
①在ILink接口中追加新的方法;
1 public void remove(E e);//数据删除
②在LinkImpl子类里面实现根节点的判断;
1 @Override 2 public void remove(E data) { 3 if(this.contains(data)){//判断数据是否存在 4 if (this.root.data.equals(data)){//根节点为要删除节点 5 this.root=this.root.next;//根的下一个节点 6 } 7 } 8 }
③如果现在根节点并不是要删除的节点,那么就需要进行后续节点判断,但是请一定要记住,此时的根节点已经判断完成,在判断应该从下一个节点开始判断,在Node类中追加删除处理。
1 public void removeNode(Node previous,E data){ 2 if(this.data.equals(data)){ 3 previous.next=this.next;//空出当前节点 4 }else { 5 if(this.next!=null){//有后续节点 6 this.next.removeNode(this,data);//向后继续删除 7 } 8 } 9 }
④完善LinkImpl子类中的remove方法
1 @Override 2 public void remove(E data) { 3 if(this.contains(data)){//判断数据是否存在 4 if (this.root.data.equals(data)){//根节点为要删除节点 5 this.root=this.root.next;//根的下一个节点 6 }else {//交由Node类负责删除 7 this.root.next.removeNode(this.root,data); 8 } 9 this.count--; 10 } 11 }
删除的逻辑依靠的就是引用的改变处理完成的。
10,链表(清空数据)public void clean()
有些时候需要进行链表的整体清空处理,这个时候就可以直接根据根节点元素来进行控制,只要root设置为null,那么后续节点就都不存在了。
①在ILink接口中里面追加有清空处理方法
1 public void clean();//清空集合
②在LinkImpl的子类里面覆写方法
1 @Override 2 public void clean() { 3 this.root=null;//后续的所有节点都没了 4 this.count=0;//个数清零 5 }
这些就是链表的基本功能,当然这只是一个最简单、最基础的单向链表
11,综合实战:宠物商店
现在假设有一个宠物商店里面可以出售各种宠物,要求可以实现宠物的上架处理、下架处理,也可以根据我们的关键字查询出宠物的信息。
①应该定义出宠物的标准
1 interface Pet{//定义宠物的标准 2 public String getName();//获得名字 3 public String getColor();//获得颜色 4 }
②宠物商店应该以宠物的标准为主
1 class PetShop{//宠物商店 2 private ILink<Pet> allPets=new LinkImpl<Pet>();//保存多个宠物信息 3 public void add(Pet pet){//追加宠物,商品上架 4 this.allPets.add(pet);//集合中保存对象 5 } 6 public void delete(Pet pet){ 7 this.allPets.remove(pet);//集合中保存对象 8 } 9 public ILink<Pet> search(String keyword){ 10 ILink<Pet> searchResult=new LinkImpl<Pet>();//保存查询结果 11 Object[] result=this.allPets.toArray();//获取全部数据 12 if(result!=null){ 13 for(Object obj:result){ 14 Pet pet=(Pet) obj; 15 if(pet.getName().contains(keyword)||pet.getColor().contains(keyword)){ 16 searchResult.add(pet);//保存查询结果 17 } 18 } 19 } 20 return searchResult; 21 } 22 }
③根据宠物的标准来定义宠物的信息
1 class Cat implements Pet{//实现宠物标准 2 private String name; 3 private String color; 4 5 public Cat(String name, String color) { 6 this.name = name; 7 this.color = color; 8 } 9 10 @Override 11 public String getName() { 12 return name; 13 } 14 15 @Override 16 public String getColor() { 17 return color; 18 } 19 20 @Override 21 public boolean equals(Object obj) { 22 if(obj==null){ 23 return false; 24 } 25 if(!(obj instanceof Cat)){ 26 return false; 27 } 28 if (this==obj){ 29 return true; 30 } 31 Cat cat=(Cat) obj; 32 return this.name.equals(cat.name) && this.color.equals(cat.color); 33 } 34 35 @Override 36 public String toString() { 37 return "【宠物猫】名字:"+this.name+",颜色:"+this.color; 38 } 39 }
④实现宠物商店的操作
1 public class Main { 2 public static void main(String[] args) { 3 PetShop shop=new PetShop();//开店 4 shop.add((new Dog("黄斑狗","绿色"))); 5 shop.add((new Cat("小强猫","深绿色"))); 6 shop.add((new Cat("黄猫","深色"))); 7 shop.add((new Dog("黄狗","黄色"))); 8 shop.add((new Dog("斑点狗","灰色"))); 9 Object[] result=shop.search("黄").toArray(); 10 if(result!=null){ 11 for(Object temp:result){ 12 System.out.println(temp.toString()); 13 } 14 } 15 } 16 }
所有的程序开发都是以接口为标准进行开发的,这样在进行后期程序处理的时候就可以非常的灵活,只要符合标准的对象都可以保存。
12,综合实战:超市实战
①定义一个商品的标准
1 interface IGoods{//定义商品的标准 2 public String getName(); 3 public double getPrice(); 4 }
②定义购物车处理标准
1 interface IShopCar{//购物车的标准 2 public void add(IGoods goods);//添加商品信息 3 public void delete(IGoods goods);//添加商品信息 4 public Object[] getAll();//获得购物车中的全部商品信息 5 }
③定义购物车的实现类
1 class ShopCarImpl implements IShopCar{//购物车 2 private ILink<IGoods> allGoodses=new LinkImpl<IGoods>(); 3 public void add(IGoods goods){ 4 this.allGoodses.add(goods); 5 } 6 public void delete(IGoods goods){ 7 this.allGoodses.remove(goods); 8 } 9 public Object[] getAll(){ 10 return this.allGoodses.toArray(); 11 } 12 }
④定义收银台
1 class Cashier{//收银台 2 private IShopCar shopCar;//驼峰命名法 3 public Cashier(IShopCar shopCar) { 4 this.shopCar = shopCar; 5 } 6 public double allPrice(){//计算商品总价 7 double money=0.0; 8 Object[] result=this.shopCar.getAll(); 9 if(result!=null){ 10 for(Object obj:result){ 11 IGoods goods=(IGoods) obj; 12 money +=goods.getPrice(); 13 } 14 } 15 return money; 16 } 17 public int allCount(){//计算商品总个数 18 return this.shopCar.getAll().length; 19 } 20 }
⑤定义商品信息
1 class Book implements IGoods{ 2 private String name; 3 private double price; 4 public Book(String name, double price) { 5 this.name = name; 6 this.price = price; 7 } 8 @Override 9 public String getName() { 10 return this.name; 11 } 12 @Override 13 public double getPrice() { 14 return this.price; 15 } 16 public boolean equals(Object obj){ 17 if(obj==null){ 18 return false; 19 } 20 if(this==obj){ 21 return true; 22 } 23 if (!(obj instanceof Book)){ 24 return false; 25 } 26 Book book=(Book) obj; 27 return this.name.equals(book.name) && price==book.price; 28 } 29 }
⑥代码测试的编写
1 public class Main { 2 public static void main(String[] args) { 3 IShopCar car=new ShopCarImpl(); 4 car.add(new Book("Java开发",79.8)); 5 car.add(new Book("MySql开发",40.0)); 6 car.add(new Book("Spring开发",99.8)); 7 car.add(new Bag("黑色电脑包",201.0)); 8 Cashier cashier=new Cashier(car); 9 System.out.println("总价格:"+cashier.allPrice()); 10 System.out.println("总数量:"+cashier.allCount()); 11 Object[] result=car.getAll(); 12 if(result!=null){ 13 for(Object temp:result){ 14 System.out.println(temp.toString()); 15 } 16 } 17 } 18 }