return(T)this.element[i]; //返回数组元素引用的对象,必须进行强制类型转换
returnnull;
}
其中,数组元素element[i]的类型是Object,实际引用实例的类型是T。
上述声明的调用语句如下:
SeqListlist=newSeqList(n); //顺序表元素类型是整数对象
Integeriobj=list.get(0); //Integer类对象iobj引用本类的实例
iobj.intValue() //iobj能够调用Integer类的成员方法
此时,iobj对象不仅能够调用Object类声明的toString()等方法,表现出运行时多态;还能够调用Integer类声明的成员方法。
也可调用语句如下:
SeqListlist=newSeqList(n);
Stringstr=list.get(0);
str.length() //str能够调用String类的成员方法
习2-5顺序表类以下声明有什么错误?
为什么?
publicbooleanequals(Objectobj) //比较两个顺序表是否相等
{
returnthis.n==list.n&&this.element==obj.element;
}
【答】在深拷贝的含义下,两个顺序表相等意味着:
两个顺序表长度相同且所有元素值相等。
而不是两个顺序表对象的所有成员变量值对应相等。
比较两个顺序表对象是否相等的多种情况如图2.2所示,方法实现见教材第30页。
图2.2比较两个顺序表对象是否相等
2.2.3排序顺序表
【思考题2-4】SeqList类声明以下方法,子类SortedSeqList是否可用?
为什么?
//返回将this与list所有元素合并连接的顺序表,不改变this和list
SeqListunion(SeqList
extendsT>list)
【答】①SeqList类声明以下成员方法:
publicvoidaddAll(SeqList
extendsT>list) //在this之后添加list所有元素,集合并。
见例2.5
publicSeqListunion(SeqList
extendsT>list)
{
SeqListresult=newSeqList(this); //深拷贝this,无法创建子类实例
result.addAll(list); //顺序表合并连接,尾插入
returnresult; //只能返回SeqList对象
}
%注意:
虽然union(list)方法与addAll(list)方法功能相同,但不能将它们重载如下,因为,参数列表相同而返回值不同,产生二义性,编译器不能根据方法的参数列表确定执行重载方法中的哪一个。
publicvoidaddAll(SeqList
extendsT>list)
publicSeqListaddAll(SeqList
extendsT>list) //语法错,参数列表相同时不能重载
②排序顺序表表示可重复的排序集合,元素按关键字大小排序。
SortedSeqList类继承SeqList类的以下成员方法:
publicvoidaddAll(SeqList
extendsT>list) //可用,参数list可引用SortedSeqList实例。
//其中,执行insert(x)方法运行时多态,当this引用子类实例时,执行排序顺序表按值插入
//合并结果this仍然是排序顺序表
publicSeqListunion(SeqList
extendsT>list) //算法正确,不可用。
希望返回排序顺序表
//因为,当this引用子类实例时,声明result和创建的仍然是SeqList实例,无法创建子类
//实例,就无法实现运行时多态,总是执行SeqList类的插入
因此,SortedSeqList类需要覆盖union(list)方法如下:
//覆盖,返回值类型不同但赋值相容。
能与父类实例运算
publicSortedSeqListunion(SeqList
extendsT>list)
{
SortedSeqListresult=newSortedSeqList(this); //创建子类实例。
只此一句不同
result.addAll(list); //继承父类方法,运行时多态,排序顺序表合并,按值插入
returnresult; //返回SortedSeqList对象
}
综上所述,SeqList类对于集合并操作的“有所为,有所不为”原则如下。
¤声明voidaddAll(list)方法,子类继承,运行时多态。
¤不声明SeqListunion(list)方法,因为,无法运行时多态。
一个类为一个功能只需提供一种实现,至于是否返回对象,由调用者决定,通过深拷贝和addAll(list)方法可得到。
2.3线性表的链式存储和实现
2.3.1单链表
1.单链表的结点
习2-6写出图2.3所示数据结构的声明。
图2.3一种数据结构
【答】table可声明为数组或顺序表,元素为结点或单链表,声明为以下4种之一:
Nodetable[]; //一维数组,元素为结点
SinglyListtable[]; //一维数组,元素为单链表
SeqList>table; //顺序表,元素为结点
SeqList>table; //顺序表,元素为单链表
【习题2-8】单链表结点类问题讨论。
①Node类声明构造方法
当一个类没有声明构造方法时,Java提供默认构造方法。
例如:
publicNode() //提供默认构造方法
{super(); //默认调用父类的无参数构造方法
}
当一个类声明了构造方法时,Java不再提供默认构造方法。
例如,当Node类声明了Node(Tdata,Nodenext)构造方法时,Java不再提供默认构造方法Node()。
如果Node类需要Node()构造方法,必须自己声明。
Java方法参数没有默认值。
例如,Node类可以声明以下构造方法:
publicNode(Tdata)
{this(data,null);
}
但不能将Node(Tdata,Nodenext)构造方法声明为如下形式:
publicNode(Tdata,Nodenext=null)
②Node类不需要声明析构方法
Node类从Object父类继承了析构方法,并且Java语言将自动释放不再使用的存储空间。
因此,Node类不需要声明析构方法。
③Node类不需要声明拷贝构造方法
Java不提供默认拷贝构造方法。
Node类如果声明拷贝构造方法以下:
publicNode(Nodep) //拷贝构造方法,复制p引用的结点
{this(p.data,p.next);
}
相当于如下:
publicNode(Nodep)
{this.data=p.data; //this.data引用p.data,对象引用赋值,两个结点引用同一个元素
this.next=p.next; //this.next指向p的后继结点,结点对象引用赋值,逻辑错误
}
由于Java的赋值=采用引用模型,执行结果如图2.4所示,this.next指向p的后继结点,并没有创建结点,只将p的后继结点作为当前结结点的后继结点,产生逻辑错误。
因此,Node类不能声明浅拷贝构造方法。
图2.4Node类浅拷贝
由于创建结点是单链表执行的操作,所以,Node类不需要声明拷贝构造方法。
④Node类可声明以下方法:
publicbooleanequals(Objectobj)//比较两个结点值是否相等,覆盖Object类的equals(obj)方法
{
returnobj==this||objinstanceofNode
>&&this.data.equals(((Node)obj).data);
}
⑤比较对象大小
Node是单链表和排序单链表的结点类。
单链表不需要比较元素大小,所以Node类不能声明实现Comparable接口。
排序单链表需要比较元素大小,以下排序单链表类声明,约定能够比较T对象大小。
//排序单链表类(升序),继承单链表类,T或T的某个祖先类实现Comparable接口
publicclassSortedSinglyListsuperT>>extendsSinglyList
2.单链表的基本操作
【思考题2-6】遍历单链表,如果将p=p.next语句写成p.next=p,结果会怎样?
画出示意图。
【答】语句p=p.next使p移动到后继结点,此时结点间的链接关系没有改变。
如果写成p.next=p,则使p.next指向p结点自己,改变了结点间的链接关系,并丢失后继结点,如图2.5所示,遍历算法也变成死循环。
图2.5p.next=p改变结点间的链接关系
【思考题2-7】设front指向非空单链表中的某个结点,在front结点之后插入p结点,执行以下语句结果会怎样?
画出示意图。
Nodep=newNode(x);
front.next=p; //①
p.next=front.next; //②
【答】②句相当于p.next=p;,产生错误,结点间的链接关系如图2.6所示。
图2.6插入语句次序错误
错误原因:
上述后两条语句次序颠倒。
对front.next赋值将改变结点间的链接关系,从而丢失后继结点地址。
因此,在改变结点链接关系时,应该先获得front.next的值,再对其赋值。
头插入存在同样问题。
3.带头结点的单链表
【习2.7】使用单链表求解Josephus环问题。
将例2.1程序中创建SeqList对象的语句替换为如下,其余代码不变,则使用单链表也可求解Josephus环问题。
SinglyListlist=newSinglyList();
上述程序虽然能够运行,但是,效率太低。
一是,每次循环调用单链表的size()和remove(i)方法,都要从头开始再次遍历单链表,时间复杂度都是O(n);再有,每次计数,欲删除第i个结点,需要再次查找使front指向第i个结点的前驱。