单向链表(单链表)的Java实现
最近被问到链表,是一个朋友和我讨论Java的时候说的。说实话,我学习编程的近一年时间里,学到的东西还是挺少的。语言是学了Java和C#,关于Web的学了一点Html+css+javascript。因为比较偏好,学习WinForm时比较认真,数据库操作也自己有所研究。但链表这个东西我还真没有学习和研究过,加上最近自己在看WPF,而课程也到了JSP了,比较紧。
但是我还是抽了一个晚上加半天的时间看了一下单向链表。并且使用Java试着写了一个实例出来。没有接触过链表的朋友可以作为参考,希望大家多提宝贵意见。
当然,我们首先解释一下什么是链表。就我所知,链表是一种数据结构,和数组同级。比如,Java中我们使用的ArrayList,其实现原理是数组。而LinkedList的实现原理就是链表了。我的老师说,链表在进行循环遍历时效率不高,但是插入和删除时优势明显。那么他有着愈十年的编程经验,我是相信的。不过不知道他是否是说双向链表,我们在此呢只对单向链表做一个了解。
链表(Chain本文所说链表均为单向链表,以下均简称单向链表)实际上是由节点(Node)组成的,一个链表拥有不定数量的节点。而向外暴露的只有一个头节点(Head),我们对链表的所有操作,都是直接或者间接地通过其头节点来进行的。
节点(Node)是由一个需要储存的对象及对下一个节点的引用组成的。也就是说,节点拥有两个成员:储存的对象、对下一个节点的引用。
这样说可能大家不是很明白,我贴一张图大家可能更容易理解。
那么大家可能清除了,为什么说有了头节点就可以操作所有节点。因为有着不断的引用嘛!
那么在链表里的属性大概就只有两个了:头节点和节点数量。当然,根据需要,我们通常需要更多的属性。
简单的链表可以写为下面的样子:
1 package myChain; 2 3 /** 4 * (单向)链表 5 * 6 * @author Johness 7 */ 8 public class PersonChain { 9 private PersonChainNode head; // 头节点 10 private int size; // 链表的实体(即节点的数量) 11 private int modCount; // 链表被操作的次数(备用) 12 13 /** 14 * 获得链表中节点数量 15 * 16 * @return 链表中节点数 17 */ 18 public int getSize() { 19 return this.size; 20 } 21 22 /** 23 * 添加节点的一般方法 24 * 25 * @param p 26 * 添加到链表节点的对象 由于实现细节,作为唯一标识,同一个编号的Person只能添加一次 27 */ 28 public void addNode(Person p) { 29 if (!contains(p.personNo)) { // 如果链表中没有该对象,则准备添加 30 if (head != null) { // 如果有头节点,则添加新节点作为头节点 31 head = new PersonChainNode((myChain.Person) p, head); 32 size++; 33 modCount++; 34 } else { // 如果没有头节点,则添加对象作为头节点 35 head = new PersonChainNode((myChain.Person) p, null); 36 size++; 37 modCount++; 38 } 39 } 40 } 41 }
以上的代码就是一般链表的骨架了。拥有两个重要属性。
那么做为能添加到链表的节点又该长什么样子呢?
我们可以写作如下:
1 package myChain; 2 3 /** 4 * (节点)实体,封装了'人'这个对象和下一个实体的引用 5 * 该实体将作为(单向)链表的节点 6 * @author Johness 7 */ 8 public class PersonChainNode { 9 Person person; // 人 10 PersonChainNode nextNode; // 该对象('人')保存的下一个对象的引用 11 12 // 获取当前实体对象('人') 13 public Person getPerson(){ 14 return this.person; 15 } 16 17 // 获取下一个实体 18 public PersonChainNode getNextNode(){ 19 return this.nextNode; 20 } 21 22 // 构造方法 23 public PersonChainNode (Person p,PersonChainNode ep){ 24 this.person = p; 25 this.nextNode = ep; 26 } 27 }
当然了,这只是个大概的样子。
那么我最后在把层次梳理一下:链表是由不定数量的节点连接(通过相互之间的引用)起来的,由于这种关系,在链表里我们只定义了头节点和节点数量。节点是由存储的对象及对下一个“节点”的引用封装而成。
在添加节点到链表中时,首先添加的节点后置后,新添加的节点作为头节点引用前一个添加的节点。
废话不多说,贴上我的例子,老师说我废话多……
(以下的例子较为简陋,大家不要笑话我哈)
1 package myChain; 2 3 /** 4 * '人' 类 5 * @author Johness 6 * @version 1.0 7 */ 8 public class Person { 9 String name; // 姓名 10 int age; // 年龄 11 int personNo; // 编号,用作唯一标识 12 13 // 带参构造方法 14 public Person(String name, int age, int personNo) { 15 this.name = name; 16 this.age = age; 17 this.personNo = personNo; 18 } 19 20 // 获取姓名 21 public String getName(){ 22 return this.name; 23 } 24 25 // 获取年龄 26 public int getAge(){ 27 return this.age; 28 } 29 30 // 获取编号 31 public int getPersonNo(){ 32 return this.personNo; 33 } 34 35 // 用于输出的信息 36 public String toString(){ 37 return "姓名:" + this.name + "\t年龄:" + this.age +"\t编号" + this.personNo; 38 } 39 }
1 package myChain; 2 3 /** 4 * (节点)实体,封装了'人'这个对象和下一个实体的引用 5 * 该实体将作为(单向)链表的节点 6 * @author Johness 7 */ 8 public class PersonChainNode { 9 Person person; // 人 10 PersonChainNode nextNode; // 该对象('人')保存的下一个对象的引用 11 12 // 获取当前实体对象('人') 13 public Person getPerson(){ 14 return this.person; 15 } 16 17 // 获取下一个实体 18 public PersonChainNode getNextEntity(){ 19 return this.nextNode; 20 } 21 22 // 构造方法 23 public PersonChainNode (Person p,PersonChainNode ep){ 24 this.person = p; 25 this.nextNode = ep; 26 } 27 28 // 构造方法 29 public PersonChainNode (Person p){ 30 this.person = p; 31 } 32 }
1 package myChain; 2 3 /** 4 * (单向)链表 5 * 6 * @author Johness 7 */ 8 public class PersonChain { 9 private PersonChainNode head; // 头节点 10 private int size; // 链表的实体(即节点的数量) 11 private int modCount; // 链表被操作的次数(备用) 12 13 /** 14 * 获得链表中节点数量 15 * 16 * @return 链表中节点数 17 */ 18 public int getSize() { 19 return this.size; 20 } 21 22 /** 23 * 添加节点的一般方法 24 * 25 * @param p 26 * 添加到链表节点的对象 由于实现细节,作为唯一标识,同一个编号的Person只能添加一次 27 */ 28 public void addNode(Person p) { 29 if (!contains(p.personNo)) { // 如果链表中没有该对象,则准备添加 30 if (head != null) { // 如果有头节点,则添加新节点作为头节点 31 head = new PersonChainNode((myChain.Person) p, head); 32 size++; 33 modCount++; 34 } else { // 如果没有头节点,则添加对象作为头节点 35 head = new PersonChainNode((myChain.Person) p, null); 36 size++; 37 modCount++; 38 } 39 } 40 } 41 42 /** 43 * 通过编号删除对象 44 * 45 * @param personNo 46 * 要删除对象的编号 47 */ 48 public void deleteNode(int personNo) { 49 if (size == 0) { // 如果当前链表节点数为零 50 return; 51 } 52 if (size == 1) { 53 // 如果只有一个节点并且正是需要删除的对象 54 if (head.person.personNo == personNo) { 55 head = null; 56 size = 0; 57 } 58 return; 59 } 60 // 如果不存在该对象编号 61 if (!contains(personNo)) { 62 return; 63 } 64 65 // 较为复杂的删除,定义整型保存被删除的节点的索引 66 //(删除和索引都是不存在的,这里只是一个说法) 67 int index = 0; 68 // 循环遍历,找到删除节点的索引 69 for (PersonChainNode p = head; p != null; p = p.nextNode) { 70 if (!(p.person.personNo == personNo)) { 71 index++; 72 } else { 73 break; 74 } 75 } 76 // 如果删除的是第一个节点(即头节点) 77 if (index == 0) { 78 head = new PersonChainNode(head.nextNode.person, 79 head.nextNode.nextNode); // 设置头节点后一个节点为新的头节点 80 size--; // 链表节点数减一 81 modCount++; // 链表被操作次数加一 82 return; 83 } 84 85 // 如果删除的节点不是第一个节点 86 // 循环遍历,找到被删除节点的前一个节点 87 // 其索引下标用count保存 88 int count = 0; 89 for (PersonChainNode p = head; p != null; p = p.nextNode) { 90 if (count == index - 1) { // 如果找到了被删除节点的前一个节点 91 if (index == size - 1) { // 如果被删除节点是最后一个节点 92 p.nextNode = null; // 将被删除节点的前一个节点的引用指向空引用 93 } else { // 如果被删除节点不是最后一个节点 94 p.nextNode = p.nextNode.nextNode; // 将被删除节点前一个节点对其引用指向被删除节点的下一个节点 95 } 96 size--; // 减一数量 97 modCount++; // 加一操作次数 98 return; 99 } 100 count++; // 没有找到,索引加一 101 } 102 } 103 104 /** 105 * 通过姓名查找对象 106 * 未实现 107 * @param name 108 * 对象姓名 109 * @return 对象数组,可能多个 110 */ 111 public Person[] searchNodeByPersonName(String name) { 112 return null; 113 } 114 115 /** 116 * 通过年龄查找对象 117 * 未实现 118 * @param age 119 * 对象年龄 120 * @return 对象数组,可能多个 121 */ 122 public Person[] searchPersonByAge(int age) { 123 return null; 124 } 125 126 /** 127 * 通过编号查找对象 128 * 由于编号是唯一标识,循环遍历查找并返回即可 129 * @param personNo 130 * 对象编号 131 * @return 查找到的对象或者null 132 */ 133 public Person searchNode(int personNo) { 134 Person p = null; 135 for (PersonChainNode pcn = head; pcn != null; pcn = pcn.nextNode) { 136 if (pcn.person.personNo == personNo) { 137 p = pcn.person; 138 } 139 } 140 return p; 141 } 142 143 /** 144 * 通过原对象修改及传入值修改该对象属性 145 * 146 * @param personNo 147 * 要修改的对象编号 148 * @param value 149 * 通过传入值的类型判断修改 只能修改姓名或年龄 150 */ 151 public void editNode(int personNo, Object value) { 152 // 通过作为唯一标识的编号查找到对象 153 Person target = searchNode(personNo); 154 if (target == null) { // 如果对象为null 155 return; 156 } 157 if (value == null) { // 如果传入参数为null 158 return; 159 } 160 // 如果传入参数为字符串类型 161 if (value.getClass() == java.lang.String.class) { 162 target.name = value.toString(); 163 return; 164 } 165 try { 166 // 如果传入参数为整型 167 target.age = Integer.parseInt(value.toString()); 168 return; 169 } catch (Exception ex) { 170 // 如果传入参数类型错误 171 return; 172 } 173 } 174 175 /** 176 * 通过对象编号打印对象 177 * 178 * @param personNo 179 * 对象编号 180 */ 181 public void printNode(int personNo) { 182 Person target = searchNode(personNo); 183 if (target == null) { 184 return; 185 } 186 System.out.println(target.toString()); 187 } 188 189 /** 190 * 判断指定对象是否存在链表中 191 * 192 * @param personNo 193 * 对象编号 194 * @return true表示存在该对象,false表示不存在该对象 195 */ 196 public boolean contains(int personNo) { 197 if (size != 0) { 198 for (PersonChainNode pcn = head; pcn != null; pcn = pcn.nextNode) { 199 if (pcn.person.personNo == personNo) { 200 return true; 201 } 202 } 203 } 204 return false; 205 } 206 207 // 排序方法 208 209 /** 210 * 通过姓名排序 211 */ 212 public void sortByPersonName() { 213 } 214 215 /** 216 * 通过年龄排序 217 */ 218 public void sortByPersonAge() { 219 } 220 221 /** 222 * 默认排序,通过编号排序 223 * 使用冒泡排序,增加了判断以提高效率 224 */ 225 public void sort() { 226 boolean jx = true; // 冒泡排序的简化方法 227 for (PersonChainNode pcn = head; pcn != null && jx; pcn = pcn.nextNode) { 228 jx = false; 229 for (PersonChainNode pc = head; pc != null && pc.nextNode != null; pc = pc.nextNode) { 230 if (pc.person.personNo > pc.nextNode.person.personNo) { 231 Person temp = pc.person; 232 pc.person = pc.nextNode.person; 233 pc.nextNode.person = temp; 234 jx = true; 235 } 236 } 237 } 238 } 239 240 /** 241 * 打印整个链表 242 */ 243 public void printAll() { 244 if (size != 0) { 245 for (PersonChainNode pcn = head; pcn != null; pcn = pcn.nextNode) { 246 System.out.println(pcn.person.toString()); 247 } 248 } 249 } 250 }
2012-04-11 21:33:32
那么实际上呢,我们在Java编程中使用的LinkedList就是在其内部维护着一个链表。
实际上的操作不外乎增删改查,不同的是由于高度封装,Jdk的LinkedList是使用索引来取得对象并进行操作的。
和以上我的例子是不尽相同的,因为我是安全按照自己的需求来做的,这样比较简单,实际上为了代码重用复用和扩展。Jdk内的链表节点不知道保存的信息,因此没有办法以除了索引之外的方式获取元素。
今天课程到了Java集合框架,老师略微提了一下单向不循环链表。
我将老师的举例改编一下,帮助大家理解:
老师需要找某一个同学,但是碰巧实在放假的时间内。同学们所处的位置都不一样。老师知道班长的手机号码,所以老师打电话给了班长,班长说他也不知道,但是他知道'我'的电话,他又打给我,我也不知道那位同学的地址,我又继续向下一个同学打电话,直到找到他。
那么在以上示例中,加入每一个同学都有另一个同学的电话(有且仅有一个)。
我们就可以说符合单向链表的环境了。大家可以理解记忆。
大家都知道,我们所创建的对象是保存在内存中的。数组是如此,链表也是如此。
但是数组是一个整体空间,所有元素共用。就比如教室内上课的同学们。教室的容量是固定的,同学可少不可多,老师若希望进行查询,能够一眼看出来。
链表和其节点是不同的存储位置,比如我们一个班毕业了。有的同学去了国外,有的去了北京。大家的位置不同,但是有一定联系的。
2012-04-23 19:16:20