链表结构简介
链表结构是一种用比较特殊的数据结构类型,它也是线性数据结构中的一种,但是与栈结构等线性数据结构不同,它的内部结构并不是一个简单的存储空间,而是一个带有指向性质的单元。要理解链表结构要弄清楚两个问题,第一个就是链表结构的存储方式是怎样的,第二个就是链表结构的模型该如何描述。
先说第一个问题,链表结构的存储方式。链表结构对数据存储的时候并不是简单的将这个数据存放到一个指定的空间中,而是将数据存储到一个属于链表结构的节点当中。这个节点就是链表的一个组成部分,在这个节点中,不仅存储了这个元素还存储了下一个元素或者上一个元素的地址,通过这些储存的地址就可以找到当前元素的上一个元素或者下一个元素。
了解完链表结构的存储方式,那么链表结构的模型就比较好建立了。最简单的模型就是链子,不论是铁链还是狗链都能帮助我们理解链表结构。因为链表结构就相当于是用一根无形的绳子将内存中的数据串了起来,就像是一根链子一样,所以称之为链表结构。但是要注意的是,虽然我们可以将链表结构想象成一根铁链,但是在实际内存中,链表结构中相邻的两个元素在内存中并不是相邻的。只是通过上一个节点中存储的地址能找到下一个节点中存储的元素而已。
综上,链表结构是由许多节点组成的,每一个节点中都应包含数据部分和地址部分两个部分。其中数据部分用来存储该节点的实际数据,而地址部分用来存储上一个或者下一个节点的地址。
链表结构可以分为单向链表、双向链表和双向循环链表三种,其中单向链表和双向链表是比较常见的链表结构,双向循环链表不太常见,简单了解即可。
单向链表
单向链表是链表结构中最为简单的一类,它的特点是链表中节点的链接方向是单向的,也就是说访问链表中的元素时只能从头开始一个一个的寻找,不能直接锁定指定元素的位置,也不能从尾向前访问元素。故能够判断出单向链表的结构中节点存储的内容应该为节点存储的元素以及下一个节点的地址。
了解了单向链表的结构以及存储模式,就可以创建单向链表的具体对象了。目前,在java中是找不到一个合适的结构来描述链表结构的,因此需要自行创建链表实现类。考虑到单向链表和双向链表的除了在存储结构上纯在些许差异之外其余并无太多不同,并且它们都应该具有对内部储存元素进行操作的方法,因此为了提高代码的复用性,可以将链表结构中的方法抽象出来创建一个接口,这样在实现单向链表或者双向链表类的时候只需要实现接口中的方法就能够实现对应的操作方法了。大大降低了代码的复杂程度。根据链表结构中应该具备的操作元素的方法,创建了以下接口:
package com.data.demo;
/**
* 基于链表结构存取元素的API定义
* @param <E>
*/
public interface MyList <E>{
void add (E element);
E get (int index);
int size();
E remove(int index);
}
此接口中定义了四个抽象方法,对应了添加元素、获取元素、获取元素个数以及删除元素四个功能。
创建完链表结构的接口类,就可以创建一个单向链表类来实现这个接口类了。由于向单向链表中添加元素之前并不能确定添加的元素类型,因此定义的单向链表类应该是一个泛型类,它当中的需要参数的方法应该为泛型方法,这也说名了上面定义的接口为泛型接口的原因。
在单向链表结构中应该具备这样一个部分,这个部分是用来描述节点内容的。而且很容易发现,节点功能只会在单向链表类中被使用,在其它类中不会被使用。这个描述符合内部类的特征,因此可以在单向链表类中定义一个内部类来存储节点信息。将这个内部类定义为内部泛型类,它的名称为Node,在Node类型中item变量用来存储当前节点的元素,定义一个变量next用来指向下一个节点对象的地址,并且还要提供一个全参构造方法。具体实现如演示代码所示。
以上定义了单向链表中的用于描述节点的结构,接下来就需要对单向链表中的具体方法做出实现了。
实现添加元素的方法
结合上文所说,单向链表访问元素必须从头节点开始,不能从尾节点或者链表当中的任意节点开始,因此在实现单向链表中的具体方法之前需要确定单向链表头节点的位置。因此最简单的方法就是定义一个私有的Node类型的成员变量来存储单向链表中头节点的位置。这里将这个变量定义为 head。此外,操作元素的方法中还涉及了元素个数的变动,也应该添加一个私有变量来记录单向链表中的元素个数变动,此处为size。
随后实现单行链表中添加元素的方法add。这个方法的作用是将指定元素添加到单向链表结构中,根据对单向链表结构的描述,这个方法可以分为三步实现。第一步,创建节点,将传入方法的元素添加到创建的节点当中;第二步,找到尾节点;第三步,实现节点挂接。
这三步中比较难理解的是第二步和第三步。第二步找到尾节点的原因是添加元素是将元素添加到单向链表的尾节点之后,因此需要先找到尾节点。而在单向链表中,访问元素是从头节点开始的,所以要找到尾节点就得从头节点开始不断向下访问节点,直到访问的节点不再指向下一个元素为止,就找到了尾节点。也就是说,当next的地址为null时,尾节点就找到了。根据描述,第二步应该用一个死循环来实现,具体实现如演示代码中的getTail方法所示。这里要注意,循环结构中的this.node = head;的意思是将head的值赋给一个新的变量node,这样做的目的是因为真正头节点的位置是不能改变的。其次循环实现是通过移动node指向的地址来实现的。如果不满足要求,就像node.next也就是下一个节点的地址传给node,直到node的地址为空。
第三步实现节点的挂接就稍微好理解点了,因为是在尾节点添加元素,所以新添加的元素成为了尾节点,原来的尾节点不再是尾节点,所以原来节点中指向下一个节点的地址不应为空,而应是新添加的节点。这就是节点挂接的简单描述,具体实现为
if(tail == null){
this.head = node;
}else{
tail.next = node;
}
其中tail == null代表没有尾节点,即整个单向链表均为空,这时添加的节点就是头节点,即只需要将添加的节点赋给头节点即可。在拥有尾节点的情况下用代码tail.next = node;即可将尾节点指向的下一个节点指向添加的节点,节点挂接的操作就完成了。当然,添加有元素会使得单向链表中的元素个数发生变动,因此还需要对元素变动进行记录。至此,添加元素的方法就完成了。
实现获取元素的方法
获取元素的方法是getNode,这个方法需要传入一个int类型的参数作为元素索引,通过索引来获取元素。这里可能会有人提出一个疑问,链表结构存在索引吗?为什么能够通过索引来过去元素?我们先暂时将这个问题搁浅,实现获取元素的方法之后自然就清楚了。
由于getNode方法是通过索引来获取指定位置的元素的,所以在获取元素之前要先检查给定的索引是否合法。因为编写的单向链表类中的元素是从0这个索引开始的,所以索引的范围为[0,size),即最小为0,最大为元素个数减1。通过单向链表实现的接口能发现,要实现的四个方法中不止getNaoe方法用到了元素索引,所以对索引合法性的判断最好编写成一个独立的方法。在演示代码中体现为checkIndex方法。在这个方法中,如果索引合法则会执行获取索引所指向的元素,如果不合法则会抛出索引异常的的提示。
当给定的索引合法之后就可以获取索引指向的元素了,那么其实可以发现,单向链表并没有索引,那么这个索引哪儿来的呢?其实这里要联系到添加元素到单向链表的操作,可以发现,添加元素时提供的方法时从尾节点添加的,也就是说这个单向链表中的元素有一个默认顺序,这个顺序就是添加元素的顺序。那么,如果把元素添加的元素作为单向链表中元素的“虚假的索引”就可以通过这个“虚假的索引”来获取单向链表中的元素了。这个想法在getNode这个方法得到了充分的体现。
private Node getNode(int index){
Node node = this.head;
for(int i = 0;i < index;i++){
node = node.next;
}
return node;
}
在方法getNode方法的代码中,由于单向链表只能从头节点开始访问元素的限制,所以先将头节点赋给一个新的变量node,随后通过循环结构来访问单向链表中的元素。根据上面描述的原理,获取索引为index的元素等价于获取第index个添加到单向链表中的元素,所以只要将索引index作为循环是否终止的判断条件即可获取到指定索引的元素。如此,先前搁浅的问题便得到了解决,同时获取指定位置的元素的方法也得到了实现。
删除指定索引位置的元素
删除元素的方法为remove,这个方法仍然需要传入一个索引,此外它还会返回删除的元素的值。由于这个方法有一个索引,所以需要调用方法checkIndex来检验传入索引的合法性。此外也要获取到当前索引指向的节点,以方便后续操作以及返回当前节点中的元素。
接下来阐述单向链表中删除元素的索引。通过上面的描述知道,单向链表中的元素是通过节点中存储的地址值来链接在一起的,那么如果能够将某个元素上的这个中链接关系删除,就能将这个元素从这个单向链表中移除。所以这里涉及到三个操作,移除上一个节点指向当前节点的地址值,将上一个节点中存储的地址值指向当前节点的下一个节点以及将当前节点存储的地址值置空。以上的这三个操作就是在单项链表中删除指定元素的具体操作,但是在具体操作时又可以分为删除的元素是否为头节点的情况,如果删除的元素时头节点,则只需要将下一个元素赋给头节点,随后将要删除的节点存储的地址置空即可。而当要删除的元素不是头节点时,上面的三个操作就缺一不可了。以上的描述体现在代码中具体为:
if(this.head == node){
this.head = node.next;
}else{
Node<E> temp = this.head;
for (int i = 0; i < index-1; i++) {
temp = temp.next;
}
temp.next = node.next;
}
node.next = null;
在这个代码中要注意的是获取前一个节点只需要索引减一即可。然后就是注意中间变量temp的合理运用。当然由于删除了元素,所以记录元素个数的变量也要发生相应的变化。
获取元素个数的方法
获取元素个数的方法极为简单,因为在添加元素和删除元素的操作中都实现了元素个数的变化,并且对元素个数的变化进行了相应记录。所以在这个方法中只需要将记录元素个数的变量size返回即可。
演示代码
实现了上述的所有方法,接下来要对其进行验证,所以开辟一个main方法,在main方法中实例化一个单向链表类并测试以上的四个方法。具体实现如下所示:
package com.data.demo;
/**
*基于单项链表实现元素存取的容器类
* @param <E>
*/
public class MySinglyLinkedList<E> implements MyList<E> {
//定义单向链表中的节点对象
class Node<E>{
private E item;//存储元素
private Node next;//存储下一个节点对象的地址
Node(E item,Node next){
this.item = item;
this.next = next;
}
}
private Node head;//存放链表的头节点
private int size;//记录元素个数
//添加元素
@Override
public void add(E element) {
//创建节点
Node<E> node = new Node<>(element,null);
//找到尾节点
Node tail = getTail();
//节点挂接
if(tail == null){
this.head = node;
}else{
tail.next = node;
}
//记录元素个数
this.size++;
}
//寻找尾节点的方法
private Node getTail(){
//判断头节点是否存在
if(this.head == null){
return null;
}
Node node = this.head;
while(true){
if(node.next == null)
break;
node = node.next;
}
return node;
}
//获取元素
@Override
public E get(int index) {
//校验index的合法性
this.checkIndex(index);
//根据位置获取节点元素
Node<E> node = this.getNode(index);
//返回节点中的元素
return node.item;
}
/**
* 校验index合法性的方法
* @return
*/
private void checkIndex(int index){
if(!(index < this.size && index >= 0)){
throw new IndexOutOfBoundsException("Index:"+index+"Size:"+this.size);
}
}
/**
* 根据位置获取节点
* @return
*/
private Node getNode(int index){
Node node = this.head;
for(int i = 0;i < index;i++){
node = node.next;
}
return node;
}
//获取元素个数
@Override
public int size() {
return this.size;
}
//删除元素
@Override
public E remove(int index) {
//校验index的合法性
this.checkIndex(index);
//根据位置找到节点对象
Node<E> node = getNode(index);
//获取该节点对象中的元素
E item = node.item;
//将该节点对象从单向链表中删除
//判断该节点是否为头节点
if(this.head == node){
this.head = node.next;
}else{
Node<E> temp = this.head;
for (int i = 0; i < index-1; i++) {
temp = temp.next;
}
temp.next = node.next;
}
node.next = null;
//记录元素个数
this.size--;
//返回该元素
return item;
}
public static void main(String[] args) {
MySinglyLinkedList<String> mySinglyLinkedList = new MySinglyLinkedList<>();
mySinglyLinkedList.add("a");
mySinglyLinkedList.add("b");
mySinglyLinkedList.add("c");
mySinglyLinkedList.add("d");
mySinglyLinkedList.add("e");
System.out.println(mySinglyLinkedList.size());
System.out.println(mySinglyLinkedList.remove(2));
for (int i = 0; i < mySinglyLinkedList.size; i++) {
System.out.println(mySinglyLinkedList.get(i));
}
}
}