顺序线性表
单链式线性表
双链式顺序表
循环链表及其应用
线性表(Linear List)是一种最常见也很重要的数据结构。类似于代数学中的向量的概念,线性表是由一组有序的数据组成。一般采用的描述方式为:
(a0, a2, …, an-1)
其中n表示线性表的有效长度,其中元素序号为0—n-1。这里所说的“有效长度”指的是线性表中实际使用的元素的个数,相对于线性表的最大允许长度。ai是元素的抽象表示,可以是表示不同的含义的变量。
若线性表中没有任何元素,及有效长度为0时,我们称之为空表,记为()。线性中的第一个元素称之为表头,通常还将线性表的最后一个元素成为表尾。在对线性表进行元素访问时使用其下标作为索引。例如,访问第i个元素时,可以通过下标i-1。将元素的下标称为元素的“指示”(注意,这里的“指示”为名词),当前可访问的元素的指示用curr表示,可以通过改变curr来访问线性表中的任意元素。
线性表的数据结构和实现方式并不固定,可以通过结合数组和其它参数的方式实现,即“顺序表”方式,也可以通过动态“链表”的方式实现。顺序表的特点在于动态性小,在创建线性表时就确定了线性表的最大长度,这避免了动态内存申请的开销,但是申请“足够长”的线性表通常会引起资源浪费。链表具有很好的动态性,即可以可以随时增长或缩短线性表,使用自由度很好,但是也付出了资源申请和释放带来的代价。
在设计数据结构时我们需要充分考虑数据结构的封装性,在这里线性表的封装性体现在对表中元素的访问和操作。上面我们已经讨论到,线性表中的元素可以通过curr指向来访问,但是如果直接修改对象的curr是不符合面向对象的封装性的。我们可以考虑将curr封装到数据结构中并对用户透明化。用户只可以通过获取当前元素的函数,如getCurrVal()来获取当前元素项,并通过函数来设置curr,如next()函数将curr设置为当前元素的下一个元素位置,prev()函数将curr设置为前一个元素的位置,goFirst()函数将curr设置为线性表的表头。
对线性表的操作通常包括元素的添加、插入、删除等。在创建线性表时,首先创建一个空表,并在空表的基础上逐个添加元素,同时更新有效元素的个数。为了保证线性表中元素的有效性和有序性,在进行插入元素时应该不影响已存在的元素,删除元素时应及时处理删除所带来的空缺位置,如图:
基于上面的考虑,线性表的抽象数据类型(ADT)设计如下:
/*
* 线性链表接口设计
*/
interface List {
public void insert (ElemItem elem); // 在当前位置插入元素
public ElemItem remove(); // 删除并返回当前元素
public void append(ElemItem elem); // 在当前线性表尾部添加元素
public void clear(); // 清空整个线性链表
public void goFirst(); // 将当前位置赋值为第一个位置
public int next(); // 设置当前位置为下一位置并返回位置
public int prev(); // 设置当前位置为前一位置并返回位置
public void setCurrVal(ElemItem elem); // 设置当前位置的元素项
public ElemItem getCurrVal(); // 获取当前位置的元素项
public int getSize(); // 获取当前线性表的有效元素个数
public int getTotalSize(); // 获取线性表的最大尺寸
public boolean inList(); // 当前位置是否在链表中
public void printList(); // 打印当前线性表中的有效元素
}
图中接口函数描述了对线性表的各种操作,其中insert函数在当前位置插入新的元素项,remove函数将当前位置的元素项删除。append函数在线性表的尾部添加元素项,这通常用于线性表的创建。goFirst函数将当前位置调整至线性表的表头位置,prev函数和next函数分别将当前位置前移和后移。当前位置的元素项的获取和修改分别通过getCurrVal和setCurrVal函数实现。此外还有线性表中元素的打印显示printList等。
需要注意的是,这里的线性表中元素项是上一节中设计的ElemItem对象,也就是说这样的线性表中的元素可以是任意的类型,并且不同的个元素项可以是不同的类型,这样的设计使得线性表具有更大的使用自由度。
可以发现,在线性表的接口设计中我们只能通过函数的方式来操作或访问线性表中的元素项,这充分体现了面向对象的封装性。
顺序表实现线性表ADT
顺序表(sequential list)实际上是利用数组和一些参数来实现线性表所定义的功能。在顺序表创建时,我们需要同时声明顺序表的最大长度,即这里的数组的长度。这里的数组与我们通常所说的数组实际上是完全相同的,其元素的是ElemItem类型。顺序表的表头即为数组的第一个元素,对当前指向的调整也就是数组下标的调整。此外,顺序表中所能容纳的元素的个数也被数组的长度限制着。
向当前位置插入新的元素项的操作也受着数组结构的制约。一方面,在数组中元素已经填满时顺序表中将不能再插入(insert)或添加(append)新的元素;另一方面,在插入和删除顺序表中当前元素时需要对数组进行移位操作从而保证元素的有序性和连贯性,如图。
可以发现,在进行插入、删除时操作时间开销将是O(n)的复杂度,其中n表示顺序表的长度,在下面的比较中将会发现,这这个开销是比较大的。
除了在上一节中线性表接口中设计的函数之外,我们设计的顺序表类还包括数组、当前元素指示curr和当前有效元素个数currSize这三个私有(private)参数,这些参数不能被类的成员以外的函数调用。
import List.List;
// 用顺序表实现线性表SequentialList.java
class SequentialList implements List{
private ElemItem[] SeqList;
private int listSize; // 顺序表的大小
private int currSize; // 当前表中有效元素个数
private int curr; // 当前位置
private void init( int mSize){ // 根据顺序表大小初始化
curr = 0; // 当前位置为第一个
listSize = mSize; // 顺序表的大小
currSize = 0; // 当前有效元素个数
SeqList = new ElemItem[mSize]; // 创建空间
}
SequentialList(){
init(0); // 以大小为0初始化
}
SequentialList( int mSize){
init(mSize); // 以大小为mSize初始化
}
public void insert(ElemItem elem) {
if (currSize > listSize){
return;
}
for( int i = currSize - 1; i >= curr; i--){
SeqList[i + 1] = SeqList[i];
}
SeqList[curr] = elem;
currSize++;
}
public ElemItem remove() {
if(0 == currSize)
return null;
ElemItem for_ret = SeqList[curr]; // 首先提取当前元素
if(curr == currSize - 1){ // 当前位置是表的尾部
curr--;
currSize--;
return for_ret;
}
// 将当前位置以后的元素依次向前移
for( int c = curr; c < currSize - 1; c++){
SeqList[c] = SeqList[c + 1];
}
currSize--; // 当前有效元素个数减1
// curr--; // 当前位置前移1
return for_ret;
}
public void goFirst(){
if (0 == listSize){
System.out.println("当前顺序表是空的");
return;
}
curr = 0;
}
public void append(ElemItem elem) {
if (0 == listSize){
System.out.println("当前顺序表是空的");
return;
}
else if(curr >= listSize){
System.out.println("当前顺序表已经满了");
return;
}
else{
// 将元素插入到末尾同时将当前个数增加1
SeqList[currSize++] = elem;
}
}
public void clear() {
if (0 == listSize){
System.out.println("当前顺序表是空的");
return;
}
else{
currSize = 0; // 将当前有效元素个数设为0
curr = 0; // 将当前位置设为顺序表的头部
}
}
public int next() {
if(curr < listSize - 1)
curr++;
return curr;
}
public int prev() {
if(curr >= 1)
curr--;
return curr;
}
public void setCurrVal(ElemItem elem) {
SeqList[curr].elem = elem;
}
public ElemItem getCurrVal() {
return SeqList[curr];
}
public int getSize() {
return currSize;
}
public int getTotalSize(){
return listSize;
}
public boolean inList() {
return (curr >= 0) && (curr < currSize);
}
public void printList(){
if (0 == listSize){
System.out.println("当前顺序表是空的");
return;
}
else{
System.out.println("当前顺序表中的元素:");
for( int c = 0; c < currSize - 1; c++){
System.out.print(SeqList[c].getElem()+", ");
}
System.out.println(SeqList[currSize - 1].getElem()+".");
}
}
}
在SequentialList类中不带参数的构造函数实际上是没有意义的,它构造了一个空表。下面通过一些使用实例来描述顺序表的使用方法。
import Element.ElemItem;
/**
* 顺序表的使用实例,ExampleSequentialList.java
*/
public class ExampleSequentialList {
public static void main(String args[]){
// 创建最大长度为10的顺序表
SequentialList sList = new SequentialList(10);
ElemItem<Integer> e1;
// 将0~9添加到该顺序表,注意顺序表的当前位置一直是表头
for( int i = 0; i < sList.getTotalSize(); i++){
e1 = new ElemItem<Integer>(i);
sList.append(e1);
// sList.next();
}
sList.printList(); // 打印所有元素
// 将当前位置向后移三位
sList.next();
sList.next();
sList.next();
System.out.println("当前位置的元素项:" + sList.getCurrVal().getElem());
// 删除两个元素
sList.remove();
sList.remove();
System.out.println("删除两个元素后顺序表中共有"
+ sList.getSize() + "项:");
sList.printList(); // 打印所有元素
System.out.println("当前位置的元素项:" + sList.getCurrVal().getElem());
// 在当前位置再插入两个元素
sList.insert( new ElemItem<Double>(1.11));
sList.printList();
sList.insert( new ElemItem<String>("java"));
sList.printList();
}
}
在本中,首先创建一个最大长度为10的顺序表(0, 2, …, 9),元素类型为整数型,这通过添加元素的append函数实现,此时顺序表的当前位置还指向表头。接着将当前位置向后移动三次,并打印显示当前位置元素的大小。在此基础上连续删除两个元素,并打印显示删除后的顺序表中的所有元素及表中的有效元素的个数。此后,在当前位置插入两个与当前顺序表中元素类型不同的连个新元素,分别是双精度类型元素1.11和字符串类型元素”java”,并打印显示此时顺序表中的所有元素。
示例程序的运行结果如下:
当前顺序表中的元素:
0, 1, 2, 3, 4, 5, 6, 7, 8, 9.
当前位置的元素项:3
删除两个元素后顺序表中共有8项:
当前顺序表中的元素:
0, 1, 2, 5, 6, 7, 8, 9.
当前位置的元素项:5
当前顺序表中的元素:
0, 1, 2, 1.11, 5, 6, 7, 8, 9.
当前顺序表中的元素:
0, 1, 2, java, 1.11, 5, 6, 7, 8, 9.
链表实现线性表ADT
链表是一种动态的线性表实现方式,即在创建链表时不需要设定链表的最大长度,可以动态的添加新的元素,但需要动态申请内存。
与顺序表不同的是链表并不是利用数组来组织元素项的。链表中的每个元素项又进行了一层“包装”,元素项是节点的成员变量,而节点是链表的基本组成元素。
具体来说,链表是有一串链在一起的节点组成,除了元素项之外每一个节点还拥有一个可以链接到其它节点的指针。最常见的链表是单链表,其中每个节点包含一个指向下一个节点的指针。若链表中的节点同时包含指向前一个节点的指针和指向后一个节点的指针,这样的链表称为双链表。这里我们将指向前一个节点的指针成为“前向指针”,指向下一个节点的指针称为“后向指针”。本节内容主要讨论单链表以及其设计方法。
单链表节点
根据上面的讨论,若链表中的节点只含有指向下一个节点的指针则成为单链表,其节点成为单链表节点(singly linked node)。根据这一性质我们可以在单链表节点类中设计指向下一个节点的“后向指针”。当然,单链表节点包含存放具体数据的元素项,如果元素项为空,则该节点成为“哑节点”。如图描述了单链表节点,其设计如下:
import Element.ElemItem;
/**
* 单链表节点的数据类型,包括节点数数据部分、指向另一个节点的指向
* SingleNode.java
*/
public class SingleNode {
private ElemItem elem; // 私有的元素项
private SingleNode next; // 私有的指向下一个节点的指针
public SingleNode(ElemItem elem, SingleNode next){ // 构造函数
this.elem = elem;
this.next = next;
}
public SingleNode(){ // 默认的构造函数
this.elem = null;
this.next = null;
}
public ElemItem getElem(){ // 获取元素项
return elem;
}
public void setElem(ElemItem new_elem){ // 重新设置元素项
this.elem = new_elem;
}
public SingleNode getNext(){ // 获取下一个节点
return next;
}
public void setNext(SingleNode new_next){ // 重新设定下一个节点
this.next = new_next;
}
}
可以发现,单链表节点类中除了获取和设置节点元素的函数getElem和setElem之外还有获取和设置节点的下一个节点的函数getNext和setNext。
在此基础上可以设计出链表类来实现线性表的接口。跟上一节中顺序表类的设计类似,除了线性表接口中所定义的函数之外,链表类还将定义了三个私有的成员,即表示链表表头的head,表示当前节点的curr和当前单链表中的元素的个数。注意,这里链表的当前有效元素的个数与链表的“最大长度”应该是相同的。
由于链表具有动态性,在插入和删除节点时不需要像顺序表中那样要移动部分元素的位置。图描述了向链表中插入和删除的过程:
在链表中插入新节点时步骤:
首先,待删除的节点(当前节点)的前一个节点的后向指针重置为待删除节点的下一节点;
第二步,将curr更新为被删除的节点的下一节点,即上图中将curr更新为Data4节点。
或许读者可能会产生疑问,上图中的Data3该怎么处理?接触过C/C++的读者都知道,在程序中创建的内存空间需要及时释放,否则会导致内存泄漏。在上面的讨论中我们没有考虑释放Data3的内存,而只是将其闲置不管,这会带来类似C/C++中的内存泄漏吗?事实上Java编译器智能地位用户考虑了这块闲置的内存,即使用户不释放它,Java编译器的垃圾回收器(garbage collector)将自动将其回收,不会造成内存泄漏。
基于这样的考虑,单链表类的一种设计方法如下:
import Element.ElemItem;
/**
* 单链表的一种设计方法,SingleLink.java
*/
public class SingleLink implements List{
private SingleNode head; // 链表的表头
private SingleNode curr; // 链表的当前位置
private int currSize; // 当前的链表长度
public SingleLink(){ // 构造函数
head = null;
curr = null;
currSize = 0;
}
public void insert(ElemItem elem) {
SingleNode forInsertNode = new SingleNode(elem, curr);
if(curr == head){ // 当前位置在表头,则直接修改表头
head = forInsertNode;
currSize++;
return;
}
SingleNode preNode = head; // 用于寻找当前节点的前一个节点
while(preNode.getNext() != curr){ // 从头开始循环寻找前一个节点
preNode = preNode.getNext();
}
preNode.setNext(forInsertNode); // 将curr的前向节点的下一个节点
curr = forInsertNode;
currSize++;
}
public ElemItem remove() {
ElemItem forReturn = curr.getElem();
// 若当前指向位置为头部,则直接将头往后移,并将当前位置往后移
if(curr == head){
head = head.getNext();
curr = curr.getNext();
currSize--; // 当前链表大小减1
return forReturn;
}
SingleNode preNode = head; // 用于寻找当前节点的前一个节点
while(preNode.getNext() != curr){ // 从头开始循环寻找前一个节点
preNode = preNode.getNext();
}
preNode.setNext(curr.getNext()); // 首先重新设定前一节点的下一节点
// 当前位置为链表尾部时,则当前节点置为前一节点
if(curr.getNext() == null)curr = preNode;
// 当前位置不是链表尾部时,则当前节点置为后一节点
else curr = curr.getNext();
currSize--;
return forReturn;
}
public void append(ElemItem elem) {
if ( null == head){ // 当前表为空表
// 当前链表为空时将新建的节点赋值给表头
head = new SingleNode(elem, null);
curr = head;
return;
}
SingleNode tail = head;
// 位置后移直至到链表的尾部
while(tail != null &&tail.getNext() != null){
tail = tail.getNext();
}
// 当前链表不为空时,将新建节点赋值为表尾的下一节点
tail.setNext( new SingleNode(elem, null));
currSize++;
}
public void clear() {
head = null; // 将表头和当前位置都指向空
curr = null;
currSize = 0;
}
public void goFirst() {
curr = head;
}
public int next() {
if(curr != null &&
curr.getNext() != null)curr = curr.getNext();
return 0;
}
public int prev() {
if(curr == head){
return -1;
}
SingleNode preNode = head;
while(preNode.getNext() != curr){
preNode = preNode.getNext();
}
curr = preNode;
return 0;
}
public void setCurrVal(ElemItem elem) {
if(curr == null) return;
curr.setElem(elem);
}
public ElemItem getCurrVal() {
if(curr == null) return null;
return curr.getElem();
}
public int getSize() {
return currSize;
}
public int getTotalSize() {
return currSize;
}
public boolean inList() {
return curr == null;
}
public void printList() {
SingleNode ptr = head;
if(ptr == null)
System.out.println("当前链表为空!");
System.out.println("当前链表中元素为:");
while(ptr.getNext() != null){
System.out.print(ptr.getElem().getElem() + ", ");
ptr= ptr.getNext();
}
System.out.println(ptr.getElem().getElem() + ".");
}
}
可以发现,在创建单链表时不需要指定链表的长度,并可以动态地添加和插入新的节点元素而没有个数的限制。但是由于单链表节点没有前向指针,在调整当前元素指向前移的操作时,需要从表头开始一次向后寻找当前节点的前一个节点,判断的标准是它的下一个节点为当前节点。更为严重的问题是,在进行插入操作的步骤2和删除操作的步骤1,也需要这样的寻找前一个节点的过程,如图:
图 定位当前节点的前一节点的过程
这样的操作复杂度为O(n),其中n表示当前链表的长度。
除此之外,在向单链表中添加(append)新元素时,需定位当前链表的最后一个节点,这也需要O(n)的复杂度。
通过简单的比较我们可以发现,这中单链表设计方式与之前的顺序表相比除了元素的动态添加和插入之外没有带来任何优越性,甚至还会引发性能的下降。
高效的单链表设计
我们可以从上面的分析总结出前一节中设计的单链表的劣性主要原因在于:
1. 前一个节点的指针无法直接获取;
2. 当前链表的最后一个元素需要定位。
针对这两个缘由,我们可以在原先的设计基础上进行改进。既然单链表节点具有指向下一个节点的指针,那么可以将curr重新定义为指向当前节点的前一个节点,而利用curr.getNext()来指向当前节点。重要的是沿着这样的思路我们可以设计出高效的链表数据结构,并且在插入和删除节点时避免从表头开始逐个向后寻找当前节点的前一个节点,如图。
很明显,当前节点的前一节点的指针就是curr,这样第一个问题就解决了。不过这里需要考虑的一个问题是,既然当前节点是通过前一节点的向后指针来表示的,那么链表的第一个元素有谁来指向?事实上只能额外引入一个节点来指向链表的第一个节点。我们将这个额外引入的节点定义为链表的表头,其元素项为空(null)的“哑节点”。
当然,这种结构在调整当前元素指向前移的操作时(即将上图中curr前移的操作)从表头开始寻找的过程无法避免。此外,为了避免添加新的元素时寻找表尾的过程,我们可以在链表中增加一个私有成员变量tail来指向表尾。图中描述了对这种新的链表的几个操作:
图 单链表的插过程
在链表中插入新节点时步骤:
首先,设定待插入的节点的后向指针为当前节点,即curr的后一节点,如上图,将Data5节点的后向指针设定为Data3;
第二步,将当curr的后向指针修改为待插入的新节点,及上图中将Data2的后向指针修改为Data5;
单链表删除节点的过程
在链表中插入新节点时步骤:
将curr的后向指针修改为当前节点(待删除节点)的下一节点,如上图,将Data2节点的后向指针修改为Data4。
跟上一节的插入和删除过程相比,这里显得高校很多。
改进的高效单链表类如下:
import Element.ElemItem;
/**
* 高效的链表类,在低效的链表类基础上做改进,SingleLink2.java
*/
public class SingleLink2 implements List{
private SingleNode head; // 链表的表头
private SingleNode tail; // 链表的表尾
private SingleNode curr; // 链表的当前位置
private int currSize; // 当前的链表长度
public SingleLink2(){ // 构造函数
head = new SingleNode( null, null);
curr = head; // curr指向的是当前节点的前驱节点
tail = head;
currSize = 0;
}
/* (non-Javadoc)
* @see List.List#insert(Element.ElemItem)
*/
public void insert(ElemItem elem) {
// 用当前节点作为后驱节点、elem构造新的节点,并作为新的当前节点
curr.setNext( new SingleNode(elem, curr.getNext()));
if(curr == tail){
tail = tail.getNext();
}
currSize++;
}
public ElemItem remove() {
// 无删除对象
if(curr == null || curr.getNext() == null) return null;
// 当前位置的元素项
ElemItem forReturn = curr.getNext().getElem();
if(curr.getNext() == tail) // 若要删除的是表尾,则特殊对待
tail = curr;
curr.setNext(curr.getNext().getNext());
currSize--;
return forReturn;
}
public void append(ElemItem elem) {
tail.setNext( new SingleNode(elem, null)); // 在表尾添加元素项
tail = tail.getNext(); // 将表尾位置向后移
currSize++;
}
public void clear() {
head = new SingleNode( null, null); // 将表头赋值为空节点
curr = head;
tail = head; // 还原为初始状态
}
public void goFirst() {
curr = head;
}
public int next() {
if(curr != null && curr.getNext() != null) curr = curr.getNext();
return 0;
}
public int prev() {
if(curr == null || curr == head){
return -1;
}
else{
SingleNode pre = head;
while(pre.getNext() != curr)
pre = pre.getNext();
curr = pre;
}
return 0;
}
public void setCurrVal(ElemItem elem) {
if(curr != null && curr.getNext() != null)
curr.getNext().setElem(elem);
}
public ElemItem getCurrVal() {
if(curr != null && curr.getNext() != null)
return curr.getNext().getElem();
return null;
}
public int getSize() {
return currSize;
}
public int getTotalSize() {
return currSize;
}
public boolean inList() {
return curr == null || curr.getNext() == null;
}
public void printList() {
SingleNode ptr = head;
while(ptr.getNext() != tail){
System.out.print(ptr.getNext().getElem().getElem()
+ ", ");
ptr = ptr.getNext();
}
System.out.println(tail.getElem().getElem() + ".");
}
}
可在这个链表类中插入和删除节点的操作复杂度为O(1),但是由于新增了成员变量tail,在链表中插入和删除时需要对当前指向为tail的情况做特殊考虑。例如,如果删除的节点是表尾节点,则需要将tail指向向前移动。此外,本链表类中的改进依然没有降低当前指向前移的复杂度,该操作的复杂度依然是O(n)。
性能比较
通过对前两节中的两种链表的性能比较,我们可以发现,第二种设计方式在第一种的基础上新增了一个空表头,并且当前节点实际上是通过前一节点的后向链接访问的。这虽然付出了额外的空间代价(一个节点空间),但是极大地降低了操作复杂度。对链表添加表尾成员变量实际上也是传统的链表数据结构中普遍采用的方法。
双链表
在前面两小节中我们分析并设计出了一个比较高效的链表数据结构,但是依然存在的问题是链表的当前指针前移操作的复杂度比较高,需要通过从表头开始向后逐一判断。双链表则是一种由具有前向指针的节点组成的链表,其当前指向的前移操作复杂度为O(1)。在设计双链表时我们首先需要设计同时具有前向指针和后向指针的双链表节点。
双链表节点类的定义如下:
import Element.ElemItem;
/**
* 双链表节点的数据类型,包括节点数数据部分、指向另一个节点的多项
*/
public class DoubleNode {
private ElemItem elem; // 私有的元素项
private DoubleNode next; // 私有的指向下一个节点的指针
private DoubleNode prev; // 私有的指向前一个节点的指针
public DoubleNode(ElemItem elem,
DoubleNode next, DoubleNode prev){ // 构造函数
this.elem = elem;
this.next = next;
this.prev = prev;
}
public DoubleNode(){ // 默认的构造函数
this.elem = null;
this.next = null;
this.prev = null;
}
public ElemItem getElem(){ // 获取元素项
return elem;
}
public void setElem(ElemItem new_elem){ // 重新设置元素项
this.elem = new_elem;
}
public DoubleNode getNext(){ // 获取下一个节点
return next;
}
public DoubleNode getPrev(){ // 获取前一个节点
return prev;
}
public void setNext(DoubleNode new_next){ // 重新设定下一个节点
this.next = new_next;
}
public void setPrev(DoubleNode new_prev){ // 重新设定前一个结点
this.prev = new_prev;
}
}
由于节点增加了前向指针,在链表中添加、插入和删除节点时都需要考虑前向指针的更新,如图所示
图 双链表的插入
双链表插入过程:
首先,定义新节点,如上图Data5对应节点,使其前向指针设置为当前节点(插入节点之前的当前节点)的前一个节点,即curr位置(Data2),其后向指针设置为当前节点,即curr的后一个节点;定义节点后将curr的后向指针重置为这个新定义的节点;
第二步,对插入节点之前的当前节点(Data3)的前向指针进行重置,如上图,将Data3节点的前向指针改为Data5对应的节点。
循环链表
循环链表是单链表或双链表的一种简单推广,即在单链表或双链表的基础上将链表的首尾链接,同时保留原链表的表头head和表尾tail,如图所示。
循环链表:
这里我们设计的双链表将基于前面内容中的高效单链表数据结构进行设计,考虑到表头节点的特殊性我们对双链表的一些操作做如下的限制:
对插入和删除操作时需要注意的是不能将表头head删除,不能在head和tail两者直接相连处(5处)插入节点(这样的操作由添加appand完成)。这两种情况都发生在curr与tail相同的时候,在函数实现时需要考虑到这一点。此外,当curr与tail相同时应禁止重新设置当前节点的元素项,因为这时的当前节点实际上是表头head,其元素项只能为空,不能重置。
根据以上的讨论结合双链表数据结构,我们设计训话链表如下:
import Element.ElemItem;
/**
* 循环链表类,CycleLink.java
*
*/
public class CycleLink implements List{
private DoubleNode head; // 循环链表的表头
private DoubleNode tail; // 循环链表的表尾
private DoubleNode curr; // 循环链表的当前位置
private int currSize; // 当前的循环链表长度
public CycleLink(){ // 构造函数
head = new DoubleNode( null, null, null); // 哑节点
tail = head;
curr = head;
currSize = 0;
}
public void insert(ElemItem elem) {
if(curr == tail) return; // 不能插入
// 用当前节点作为后驱节点、curr作为前去节点、elem作为元素构造新的 // 节点,并作为新的当前节点
curr.setNext( new DoubleNode(elem, curr.getNext(), curr));
// 插入前当前节点的前向节点改为新节点
curr.getNext().getNext().setPrev(curr.getNext());
currSize++;
}
public ElemItem remove() {
// 无删除对象
if(curr == null || curr.getNext() == null) return null;
// 若curr==tail,此时curr.getNext()指向head,不能直接删除head
if(curr == tail) return null; // 不能删除
// 当前位置的元素项
ElemItem forReturn = curr.getNext().getElem();
// 若要删除的是表尾,则特殊对待
if(curr.getNext() == tail) tail = curr;
// 下面两行执行删除节点
curr.setNext(curr.getNext().getNext());
curr.getNext().setPrev(curr);
currSize--;
return forReturn;
}
public void append(ElemItem elem) {
// 在表尾添加元素项
tail.setNext( new DoubleNode(elem, head, tail)); tail = tail.getNext(); // 将表尾位置向后移
head.setPrev(tail); // 表头的前向节点为表尾节点
currSize++;
}
public void clear() {
// 将表头赋值为空节点
head = new DoubleNode( null, null, null);
curr = head;
tail = head; // 还原为初始状态
}
public void goFirst() {
curr = head;
}
public int next() {
if(curr != null && curr.getNext() != null)
curr = curr.getNext();
return 0;
}
public int prev() {
curr = curr.getPrev();
return 0;
}
public void setCurrVal(ElemItem elem) {
// 则直接返回
if(curr == tail ) return;
if(curr != null && curr.getNext() != null)
curr.getNext().setElem(elem);
}
public ElemItem getCurrVal() {
if(curr != null && curr.getNext() != null)
return curr.getNext().getElem();
return null;
}
public int getSize() {
return currSize;
}
public int getTotalSize() {
return currSize;
}
public boolean inList() {
return curr == null || curr.getNext() == null;
}
public void printList() {
DoubleNode ptr = head;
while(ptr.getNext() != tail){
System.out.print(
ptr.getNext().getElem().getElem()+ ", ");
ptr = ptr.getNext();
}
System.out.println(tail.getElem().getElem() + ".");
}
}
循环链表的一个简单应用
这里的循环链表确切地说应该称为循环双链表,在实际应用中使用得很多。这里举一个例子来说明循环链表的使用。
问题描述:10只猴子竞选大王,它们的编号为0~9。首先围成一圈,然后从0号开始1至3报数,报到3的猴子将失去竞选权,最后一只拥有竞选权的猴子将成为大王。
问题抽象:用大小为10的循环链表来表示10只猴子围成的圈;将当前节点指针curr往后循环移动,移动一次则判断当前节点是否为哑节点。若不是,则计数器加一,否则继续后移且计数不变。当计数器加到3时将对应的节点设为哑节点,计数器清零。如此往复10次,将得到留下来的猴子。流程图如下:
代码如下:
import Element.ElemItem;
/** *
* 猴子选大王的问题,循环链表的应用ExamlpeCycleLonk.java
*/
public class ExampleCycleLink {
public static void main(String args[]){
int num = 10; // 猴子的个数
int count = 0; // 猴子报数的计数器
// 建立循环链表来存放猴子的序号
CycleLink cyLink = new CycleLink();
// 项链表中添加序号
for( int i = 0; i < num; i++){
cyLink.append( new ElemItem<Integer>(i));
}
System.out.println("1\t2\tout");
cyLink.goFirst(); // 从第一只猴子开始
// 用于缓存猴子的状态:元素项为null表示该猴子出列了
ElemItem e;
// 每轮1至3报数都将出列一只猴子,所以一共num轮报数
for( int i = 0; i < num; i++){
count = 0;
System.out.println("--------------------");
while( true){
// 获取当前猴子的信息
e = cyLink.getCurrVal();
if(e != null){ // 该猴子依然具有竞选权
count++;
if(count == 3) break; // 报数结束
System.out.print(cyLink.getCurrVal().getElem() + "\t");
cyLink.next(); // 报数没有结束的时候继续
}
else{
cyLink.next(); // 当前猴子没有竞选权,跳过
}
}
System.out.println(cyLink.getCurrVal().getElem());
cyLink.setCurrVal( null); // 更新当前报数是3的猴子的状态
cyLink.next();
}
}
}
运行结果:
报数1报数2出列
--------------------
012
--------------------
345
--------------------
678
--------------------
901
--------------------
346
--------------------
790
--------------------
347
--------------------
934
--------------------
939
--------------------
333
可以看出最后失去竞选权的标号为3的猴子。