1、本次的预计讲解的知识
1、本次的操作属于引用部分的加强的应用,所以在此部分有两点需要依赖:
·依赖于引用传递问题;
·this表示当前对象。
2、链表实现的基本模式;
3、开发并且使用可用链表。
2、具体内容(理解)
如果你对自己比较高一些,强烈建议多花一些时间吧链表的实现好好的弄一下,这个是为了后续的Java类集框架服务的。
2.1、链表的基本形式
链表是一种最为简单的诗句结构,它的主要目的是依靠引用关系来实现多个数据的保存,那么下面假设要保存的数据是字符串。
范例:定义一个Node类
·假设本次保存的数据是String型数据,同时拥有下一个的引用
//每一个链表实际上就是由多个结点多组成的 class Node {//定义一个结点 private String data ; //要保存的数据 private Node next ; //要保存的下一个结点 //每一个Node类的对象都必须保存有相应的数据 public Node (String data) { //必须有数据才有Node this.data = data ; } public Node getNext() { return this.next; } public void setNext(Node next) { this.next = next; } public String getData() { return this.data; } } |
以上只是专门负责保存结点关系的类,但是至于数怎么保存的关系,现在并不是由Node类进行。需要由其他类负责Node的关系匹配。
范例:使用第一种形式设置和取出数据
public class LinkDemo { public static void main(String[] args) { //第一步准出所有的数据 Node root = new Node("火车头") ; Node n1 = new Node("车厢A") ; Node n2 = new Node("车厢B") ; root.setNext(n1) ; n1.setNext(n2) ; //第二步:取出所有数据 Node currenrNode = root ; //当前有跟结点开始读取 while (currenrNode != null) { System.out.println(currenrNode.getData()) ; //将下一个结点设置为当前结点 currenrNode = currenrNode.getNext() ; } } } |
实际上以上的操作并不方便,最好的做方法还是应该使用递归操作完成。
范例:使用第二种方式设置和取得数据
public class LinkDemo { public static void main(String[] args) { //第一步准出所有的数据 Node root = new Node("火车头") ; Node n1 = new Node("车厢A") ; Node n2 = new Node("车厢B") ; root.setNext(n1) ; n1.setNext(n2) ; print(root) ; } public static void print (Node current) { if (current == null) {//递归的结束条件 return ;//结束方法 } System.out.println(current.getData()) ; print(current.getNext()); } } |
对于所有的结点由于并不知道具体的循环次数,所以只能够使用while循环,但是在结点操作中递归的操作要比直接使用while循环代码更加的直观。
疑问?整个过程实际上完成的功能就是一个设置数据和取得数据的过程。为什么需要Node?
由于数据本身不具备先后的关系,所以使用Node类来封装数据,同时利用Node类来指向下一个结点。
2.2、链表的基本雏形
通过分析发现:
·用户在操作的过程之中完全没有必要关心Node类是否存在;
·所有的结点的引用关系不应该由用户处理应该有一个专门的工具类来处理。
下面需要定义一个类开帮助客户端去隐藏所有的链表中给出的细节操作。
//每一个链表实际上就是由多个结点多组成的 class Node {//定义一个结点 private String data ; //要保存的数据 private Node next ; //要保存的下一个结点 //每一个Node类的对象都必须保存有相应的数据 public Node (String data) { //必须有数据才有Node this.data = data ; } public Node getNext() { return this.next; } public void setNext(Node next) { this.next = next; } public String getData() { return this.data; } //实现结点的添加 //第一次调用Link:this = Link.root //第二次调用Node:this = Link.root.next //第三次调用Node:this = Link.root.next.nxt public void addNode (Node newNode) { if (next == null) {//当前节点的下一个为null this.next = newNode ; } else { //当前节点之后还保存有结点 //当前结节点的下一个节点继续保存 this.next.addNode(newNode) ; } } //第一次调用Link:this = Link.root //第二次调用Node:this = Node.root.next //第三次调用Node:this = Link.root.next.nxt public void printNode () { System.out.println(this.data) ; //输出当前节点 if (this.next != null) { //现在还有下一个节点 this.next.printNode(); //输出先一个 } } } //需要进行Node类对象的关系处理 class Link {//负责数据设置和输出 private Node root ; //根节点 public void add(String data) {//增加数据 //为了可以设置数据的先后关系,所有将data包装在一个Node类对象里 Node newNode = new Node(data) ; //保存当前的时候,现在还没有根节点 if (root == null) { //一个链表只有一个根节点 this.root = newNode ; //将新的结点设置问根节点 } else { //随后增加的元素应该交由节点来决定 //从root结点之后找到合适的位置 this.root.addNode(newNode); } } public void print() {//输出数据 if (this.root != null) { root.printNode() ; } } } public class LinkDemo { public static void main(String[] args) { Link link = new Link() ; //由这个类负责所有的数据操作 link.add("Hello") ; //存放数据 link.add("WORLD") ; link.add("MLDN") ; link.add("WWW") ; link.print() ; //展示数据 } } |
通过以上的代码实际上可以发现链表的基本操作特点:
·客户端代码不用去关注具体的Node以及引用关系的细节,只关注与提供的Link类中支持的方法;
·Link类的主要功能是控制Node类对象的产生和根节点;
·Node类主要负责数据的保存以及引用关系的分配。
2.3开发可用链表
指的可以使用链表实现数据的增加、修改、删除、查询操作。
2.3.1、程序的基本结构
在开发具体的可用链表之前,首先必须明确一个道理:Node类负责所有的节点数据的保存以及节点关系的匹配,所以Node类不能单独去使用,而以上的实现里面Node是可以单独使用的,外部可以绕过Link类直接操作Node类,这样明显是没有任何意义存在的,所以下面必须修改设计结构,让Node类只能够被Link类使用。
这个使用内部类明显是一个最好的选择。内部类可以使用private定义,这样一个内部类只能够被一个外部类所使用,另外一点,内部类可以方便与外部类之间进行私有属性的直接访问。
范例:链表的开发结构
class Link { //链表类,外部能够看见的只有这一个类 //之所以定义在内部,主要是让且为Link服务 private class Node { //定义的节点类 private String data ;//保存数据 private String next ;//引用关系 public Node (String data) { this.data = data ; } } //===========以上为内部类============ private Node root ; } |
而后主要就是代码的填充,以及功能的完善。
2.3.2、数据增加:public void add (数据类型 变量)
如果进行新数据的增加,则应该有Link类负责结点对象的产生,并且有Link类维护根节点,所有的节点的关系匹配交给Node类处理。
class Link { //链表类,外部能够看见的只有这一个类 //之所以定义在内部,主要是让且为Link服务 private class Node { //定义的节点类 private String data ;//保存数据 private Node next ;//引用关系 public Node (String data) { this.data = data ; } public void addNode(Node newNode) { if (this.next == null) {//当前的下一个结点为空 this.next = newNode; } else {//向后继续保存 this.next.addNode(newNode) ; } } } //===========以上为内部类============ private Node root ; public void add(String data) {//不允许有空 if (data == null) { return ; } Node newNode = new Node(data) ;//要保存的数据 if (this.root == null) {//当前没有根节点 this.root = newNode ;//保存根节点 } else {//根节点存在,其它节点交给Node处理 this.root.addNode(newNode) ; }
} } public class LinkDemo { public static void main(String[] args) { Link all = new Link() ; all.add("Hello"); all.add("World"); all.add(null); } } |
此时使用的了一个不许为空的判断。但并不所有的链表不许为null。
2.3.3、取得取得保存元素个数:public int size()
既然每一个链表对象都只有一个root根元素,那么每一个链表就有自己的长度,可以在Link类里面设置一个count属性,随后每一次数据添加完成之后,可以进行个数的自增。
范例:修改Link.java类
·增加一个count属性
private int count = 0 ;//保存元素的个数 |
·在add方法里面增加数据的统计操作
public void addNode(Node newNode) { if (this.next == null) {//当前的下一个结点为空 this.next = newNode; } else {//向后继续保存 this.next.addNode(newNode) ; } this.cout++ ;//每一次保存完成后数据量加一 } |
·随后为Link类增加一个新的方法:size()
public int size() {//取得保存的数据量 return this.count ; } |
本程序null不会被保存。
2.3.4、判断是否是空链表:public boolean isEmpty()
空链表判断实际上可以通过两种方式完成:
·第一个:判断root有对象(是否为null);
·第二个:判断保存的数据量(count)。
范例:判断是否为空链表
public boolean isEmpty() { return this.count == 0 ; } |
本次是一个链表组成内部的剖析,所以一些简单的代码一定要清楚实现原理。
2.3.5、数据查询:public boolean cotains(数据类型 变量)
在链表之中一会保存有多个数据,那么基本的判断数据是否存在的方式,以:String为例。循环链表中的内容,并且与要查询的数据进行匹配(equals()),如果查找到了则返回true,否则返回false。
范例:修改Link
public boolean contains(String data) { //现在没有要查询的数据,根节点也不保存数据 if (data == null || this.root == null) { return false ;//没有查询结果 } return this.root.containsNode(data) ; } |
从根元素开始查询数据是否存在。
范例:在Node增加方法
public boolean containsNode(String data) { if (data.equals(this.data)) {//当前结点数据位要查询的数据 return true ; } else { //当前结点数据不满足查询要求 if (this.next != null) {//有后续节点 return this.next.containsNode(data) ; } else {//没有后续节点 return false ; } } } |
范例:定义测试程序
public class LinkDemo { public static void main(String[] args) { Link all = new Link() ; all.add("Hello"); all.add("World"); all.add(null); System.out.println(all.contains("Hello")) ; System.out.println(all.contains("ayou")) ; } } |
本次使用的是String型数据,所以判断数据的时候使用的是equals()方法,可是如果说现在要传递的是一个自定义对象呢?需要定义一个对象比较的方法(暂时将方法名称定义为compare())。
2.3.6、根据索引取得数据:public 数据类型 get(int index)
通过以上的代码测试可以发现,链表里面保存了多个String类的对象,在程序只有数组可以保存多个对象,现在使用的链表与链表数组相比较的话,优势就输没有长度限制,所以链表严格意义上来讲就是一个动态对象数组,那么也应该具备数组那样根据索引取得元素的功能。
由于是动态对象数组,所以数组中的每一个元素的索引的内容都一定是动态生成的。
范例:在Link类里面增加一个foot的属性,表示每一个Node元素的编号
private int foot = 0 ; |
范例:在一次查询的时候(一个链表有可能查询多次),那么foot应该在每一次查询是都从头开始,但是要记住,如果有内容,或者要查询的索引小于个数。
public String get(int index) { if (index > this.count) {//超过查过范围 return null ;//没有数据 } this.foot = 0 ;//表示从前往后查 return this.root.getNode(index) ;//将查询过程交给Node类处理 } |
范例:在Node类里面实现getNode方法,内部类和外部类可以方便的进行私有属性的互相访问。
public String getNode(int index) { //使用foot内容与要查询的索引进行比较 //随后将foot的内容自增,目的是为了下次查询方便 if (Link.this.foot ++ == index) {//为要查询的索引 return this.data ; } else {//现在应该继续向后判断 return this.next.getNode(index) ; } } |
这样的话就和数组的联系紧密了。
2.3.7、修改指定索引内容:public void set(int index,数据类型 变量)
修改数据和查询的区别不大,查询的时候当满足索引值的时候,只是进行了一个数据的返回,那么此处只需要将数据返回变为数据的重新赋值即可。
范例:在Link类里面增加set()方法
public void set(int index,String data) { if (index > this.count) { return ; } this.foot =0 ;//重新设置foot属性内容,作为索引出现 this.root.setNode(index, data) ;//交给Node设置内容 } |
范例:在Node类里面增加setNode()方法
public void setNode(int index, String data) { if (Link.this.foot++ == index) {//进行内容的修改 this.data = data ; } else { this.next.setNode(index, data); } } |
修改之后由于索引都是动态生成的,所以取出数据的时候没有任何的区别。
2.3.8、数据删除:public void remove(数据类型 变量)
对于删除数据而言,实际上要分为两种情况的:
·情况一:要上出的数据是根节点,则root应该变为“根节点.next”,Link类才关心根节点,所以此种情况要在Link类中进行处理;
·情况二:要删除的不是根节点,而是其它的普通节点,应该在Node进行处理,所以此处是从第二个节点开始判断的;
删除数据的最终形式:当前的结点上一个节点next = 当前节点的next,即:空出了当前节点。
范例:在Node类里面增加一个removeNode()方法,此方法专门负责处理费根节点删除
//要传递上一个节点以及要删除的数据 public void removeNode(Node previous, String data) { if (data.equals(this.data)) {//当前节点为要删除的节点 previous.next = this.next ;//空出当前节点 } else {//应该继续向后查询 this.next.removeNode(this, data); } } |
范例:在Link类里面增加根节点的判断
//第一次调用(Link),previous = Link.root、this = Link.root.next //第二次调用(Node),previous = Node.root.next、this = Node.root.next.next public void remove(String data) { if (this.contains(data)) {//主要功能判断数据是否存在 //要删除数据是否是根节点数据 //root是Node类的对象,此处直接访问了内部类的私有操作 if (data.equals(this.root.data)) { //这个为要删除的节点 this.root = this.root.next ; //空出当前根节点 } else {//不是根元素 //此时根元素已将判断过了,从第二个元素开始判断 this.root.next.removeNode(this.root, data); } count-- ;//个数要减少 } } |
删除是链表里面应该说是最麻烦的功能了。但是理解了删除就可以更好的理解this表示当前对象,以及引用关系的设置。
2.3.9、将链表变对象数组:public 数据类型 [] toArray()
任何情况下,不管什么样的类,都不能在类中使用输出语句,只要是想输出数据一定将数据返回到调用处进行输出,而由于链表属于动态对象数组,所以此处最好的做法是将链表以对象数组的形式返回。
通过以上的分析,最终Link类的toArray()方法一定要返回一个对象数组,并且这个对象数组也一定被Node类操作,那么这个对象数组最好定义在Link类的属性里面。
范例:修改Link类的定义
·增加一个返回的数组属性内容,之所以将其定义为属性,是因为内部类和外部类都可以访问
private String [] retArray ;//返回的数组 |
·增加toArray()方法
public String [] toArray() { if (this.root == null) { return ; } this.foot = 0; //需要脚标控制 this.retArray = new String[this.count] ;//根据保存内容开辟数组 this.root.toArrayNode() ;//交给Node处理 return this.retArray ; } |
范例:在Node类里面处理数组数据的保存
public void toArrayNode() { Link.this.retArray[Link.this.foot++] = this.data ; if (this.next != null) {//有后续元素 this.next.toArrayNode() ; } } |
实现的前提:内部类与外部类之间可以直接进行私有属性的访问。
链表数据变为对象数组取出是最为重要的功能。
2.4、使用链表
以上的给出的链表严格开讲不能够使用,而且意义也不大,因为它所能够操作的数据类型只有String,毕竟String所保留的数据比较少,所以可以采用自定类来进行链表的操作。
由于链表中要保存的对象需要实现contains()、remove()等功能,所以在类中要提供有对象比较方法的支持
范例:定义一个保存图书信息的类
class Book{ private String title ; private double price ; public Book(String title, double price) { this.title = title ; this.price = price ; } public String getInfo() { return "图书名称:" + this.title + ",价格:" + this.price ; } public boolean compare(Book book) { if (this == book) { return true ; } if (book == null) { return false ; } if (this.title.equals(book.title) && this.price == book.price) { return true ; } return false ; } } |
范例:修改链表实现
class Link { //链表类,外部能够看见的只有这一个类 //之所以定义在内部,主要是让且为Link服务 private class Node { //定义的节点类 private Book data ;//保存数据 private Node next ;//引用关系 public Node (Book data) { this.data = data ; } public void addNode(Node newNode) { if (this.next == null) {//当前的下一个结点为空 this.next = newNode; } else {//向后继续保存 this.next.addNode(newNode) ; } } //第一次调用(Link):this = Link.root //第二次调用(Node):this = Link.root.next public boolean containsNode(Book data) { if (data.compare(this.data)) {//当前结点数据位要查询的数据 return true ; } else { //当前结点数据不满足查询要求 if (this.next != null) {//有后续节点 return this.next.containsNode(data) ; } else {//没有后续节点 return false ; } } } public Book getNode(int index) { //使用foot内容与要查询的索引进行比较 //随后将foot的内容自增,目的是为了下次查询方便 if (Link.this.foot ++ == index) {//为要查询的索引 return this.data ; } else {//现在应该继续向后判断 return this.next.getNode(index) ; } } public void setNode(int index, Book data) { if (Link.this.foot++ == index) {//进行内容的修改 this.data = data ; } else { this.next.setNode(index, data); } } //第一次调用(Link),previous = Link.root、this = Link.root.next //第二次调用(Node),previous = Node.root.next、this = Node.root.next.next //要传递上一个节点以及要删除的数据 public void removeNode(Node previous, Book data) { if (data.equals(this.data)) {//当前节点为要删除的节点 previous.next = this.next ;//空出当前节点 } else {//应该继续向后查询 this.next.removeNode(this, data); } } //第一次调用(Link):this = Link.root; //第二次调用(toarrayNode):this = Link.root.next public void toArrayNode() { Link.this.retArray[Link.this.foot++] = this.data ; if (this.next != null) {//有后续元素 this.next.toArrayNode() ; } } } //===========以上为内部类============ private Node root ; private int count = 0 ;//保存元素的个数 private int foot = 0 ; private Book [] retArray ;//返回的数组 public void add(Book 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++ ;//每一次保存完成后数据量加一 } public int size() {//取得保存的数据量 return this.count ; } public boolean isEmpty() { return this.count == 0 ; } public Book get(int index) { if (index > this.count) {//超过查过范围 return null ;//没有数据 } this.foot = 0 ;//表示从前往后查 return this.root.getNode(index) ;//将查询过程交给Node类处理 } public boolean contains(Book data) { //现在没有要查询的数据,根节点也不保存数据 if (data == null || this.root == null) { return false ;//没有查询结果 } return this.root.containsNode(data) ; } public void set(int index,Book data) { if (index > this.count) { return ; } this.foot =0 ;//重新设置foot属性内容,作为索引出现 this.root.setNode(index, data) ;//交给Node设置内容 } public void remove(Book data) { if (this.contains(data)) {//主要功能判断数据是否存在 //要删除数据是否是根节点数据 //root是Node类的对象,此处直接访问了内部类的私有操作 if (data.equals(this.root.data)) { //这个为要删除的节点 this.root = this.root.next ; //空出当前根节点 } else {//不是根元素 //此时根元素已将判断过了,从第二个元素开始判断 this.root.next.removeNode(this.root, data); } count-- ;//个数要减少 } } public Book [] toArray() { if (this.root == null) { return null; } this.foot = 0; //需要脚标控制 this.retArray = new Book[this.count] ;//根据保存内容开辟数组 this.root.toArrayNode() ;//交给Node处理 return this.retArray ; } } |
范例:实现测试
public class LinkDemo { public static void main(String[] args) { Link all = new Link() ; all.add(new Book("Java开发",79.8)) ; all.add(new Book("JSP开发",69.8)) ; all.add(new Book("Oracle开发",59.8)) ; System.out.println("保存书的个数:" + all.size()) ; System.out.println(all.contains(new Book("Java开发",79.8))) ; all.remove(new Book("Oracle开发",59.8)); Book [] books = all.toArray(); for (int x=0; x<books.length; x++) { System.out.println(books[x].getInfo()) ; } } } |
虽然以上的代码麻烦(如果不让你写链表,只使用方法),那么可以发现此时的程序没有了长度的限制,在内存里面保存的,如果存放的太多了,那么你的程序会变慢
链表最好的使用就是横向替代掉对象数组。
3.5、在关系中使用链表
链表就是动态的对象数组,那么在之前进行数据表映射的时候(本次只以一对多为主),都会出现对象数组的概念,所以现在就可以利用链表来实现对象是数组的保存。
对于任何一个要使用链表的类而言,一定要提供有对比较的方法
public class LinkDemo { public static void main(String[] args) { //第一步:设置关系数据 //1、先准备好各自的独立对象 Provice pro = new Provice(1,"河北省"); City c1 = new City(1001,"唐山"); City c2 = new City(1002,"秦皇岛"); City c3 = new City(1003,"石家庄"); //2、设置关系 c1.setProvice(pro); //一个城市属于一个省份 c2.setProvice(pro); c3.setProvice(pro); pro.getCities().add(c1) ; pro.getCities().add(c2) ; pro.getCities().add(c3) ; //第二步:取出关系 System.out.println(pro.getInfo()) ; System.out.println("拥有的城市数量:" + pro.getCities().size()) ; pro.getCities().remove(c1); City[] c = pro.getCities().toArray() ; for (int x=0; x<c.length; x++) { System.out.println(c[x].getInfo()) ; } } } |
本程序不再受到数组长度的限制,但是新的问题:如果真按照这样的方式去编写会造成只要有一个简单Java类,那么就需要定义一个链表。
方法解决的是代码重复问题,但是以上并不属于代码的重复,属于数据类型的不统一,所以这个时候在之前所学到的知识不足以此类问题。
3、总结
1、链表本次讲解的只是一个最基础的单向链表;
2、请熟记以下的方法名称以及作用;
No. | 方法名称 | 类型 | 描述 |
1 | public void add (数据类型 变量) | 普通 | 向链表之中增加新的数据 |
2 | public int size() | 普通 | 取得链表中保存的元素个数 |
3 | public boolean isEmpty() | 普通 | 判断是否是空链表(size() == 0) |
4 | public boolean cotains(数据类型 变量) | 普通 | 判断某一数据是否存在 |
5 | public 数据类型 get(int index) | 普通 | 根据索引取得数据 |
6 | public void set(int index,数据类型 变量) | 普通 | 使用新的内容提换掉指定索引的旧内容 |
7 | public void remove(数据类型 变量) | 普通 | 删除指定数据,如果是对象则要进行对象比较 |
8 | public 数据类型 [] toArray() | 普通 | 将链表一对象数组的形式返回 |
3、本次讲解的链表是日后进行Java类集学习的先期的原理分析。