课程名称:《程序设计与数据结构》
学生班级:1623班
学生姓名:刘伟康
学生学号:20162330
实验时间:2017年9月25日—2017年10月1日
实验名称:线性结构
指导老师:娄嘉鹏、王志强老师
实验要求:
实验一 线性表的应用,实现和分析
- 1.Java中的线性表的测试:过程,问题;
2.Java中的线性表的应用:过程,问题;
3.顺序表的实现(伪代码,产品代码,测试代码):过程(分析,设计,实现,测试),问题;
4.链表的实现(伪代码,产品代码,测试代码):过程(分析,设计,实现,测试),问题;
5.Java ArrayList,LinkedList源码分析:过程,学到的技巧。
实验步骤及代码实现:
【代码托管1方法类】 【代码托管2单元测试】
1.线性结构-1:
ArrayList和LinkedList测试:
查看ArrayList和LinkedList的Java API帮助文档,参考:http://www.cnblogs.com/rocedu/p/4837092.html
用Junit对ArrayList和LinkedList的方法进行测试,要尽量覆盖正常情况,异常情况,边界情况。- ArrayList和LinkedList类中的基本操作:
add()
方法的两种用法:
List.add(0,111); //将指定的元素插入此列表中的指定位置(index,element)
List.add(20162330); //将指定的元素添加到此列表的尾部
clear()
方法:
for (int i = 0; i < size; i++)
elementData[i] = null; //将一个已经存在的表置成空表
size = 0;
isEmpty()
方法:(LinkedList类中没有)
public boolean isEmpty() {
return size == 0; //判断列表是否为空
}
get(i)
方法:(注意 i 的取值范围为 0 ≤ i ≤ length()-1
)
if (i < 0 || i > length - 1)
System.out.println("第" + i + "个元素不存在");
return List[i]; //读取并返回列表中第i个数据元素
remove()
方法:(注意 i 的取值范围为 0 ≤ i ≤ length()-1
)
if (i < 0 || i > length - 1) //移除此列表中指定位置上的元素
System.out.println("删除位置不合法");
for (int j = i; j < length - 1; j++)
List[j] = List[j + 1]; //元素前移
length--;
indexOf(x)
方法:
int i = 0;
while (i < length && !List[i].equals(x)) //依次查找
i++;
if (i < length)
return i; //返回此列表中首次出现的指定元素的索引
else
return -1; //列表中不包含此数据元素
contains()
方法:(与isEmpty()
方法返回值类型相同)
public static boolean Contains() {
return List.contains(20162330); //判断此列表中是否包含指定的元素
}
2.线性结构-2:
- 分别用Java的ArrayList和LinkedList实现有序线性表的合并:
aList,bList都是非递减线性表,合并后也是非递减
public static List<? extends Comparable> mergeSortedList(List<? extends Comparable> aList,
List<? extends Comparable> bList)
测试mergeSortedList的正确性,要尽量覆盖正常情况,异常情况,边界情况。
- 首先要理解什么是“非递减线性表”,包括递增或者相邻几项相等的线性表,合并之后要求达到一种归并排序的效果。我在理解了意思,有了自己的思路之后,使用一些循环和条件语句实现此算法,需要注意的只有两点:
(1) 两个线性表中的元素在归并时可能出现一个线性表中元素非空,而另一个线性表中元素为空的情况,这样继续归并就需要提前判断,不然就很可能出现越界异常。
(2) 使用Junit测试为了避免出现断言空指针测试失败的情况,需要将归并后的元素遍历为String类型后再断言,这样元素的期待值就不会以线性表的形式陈列。
我的归并排序算法如下:
for (int i = 0; ;) {
if (aList.isEmpty() && !bList.isEmpty()) { //提前判断每个线性表是否为空
for (int j = bList.size() - i + 1; j <= bList.size(); j++) { //添加非空的bList剩余的所有元素
List.add(bList.get(j));
bList.remove(j);
}
}
else if (bList.isEmpty() && !aList.isEmpty()) {
for (int j = aList.size() - i + 1; j <= aList.size(); j++) { //添加非空的aList剩余的所有元素
List.add(aList.get(j));
aList.remove(j);
}
}
else if (aList.isEmpty() && bList.isEmpty()) //元素排序结束
break;
else { //两个线性表都非空
if (aList.get(i).compareTo(bList.get(i)) == 0) { //使用Comparable接口类中的compareTo方法比较并添加元素
List.add(aList.get(i));
aList.remove(i);
List.add(bList.get(i));
bList.remove(i);
} else if (aList.get(i).compareTo(bList.get(i)) < 0) {
List.add(aList.get(i));
aList.remove(i);
}
else { //aList比bList中的第 i 个元素大
List.add(bList.get(i));
bList.remove(i);
}
}
}
return List; //返回已生成的线性表
}
**ArrayList合并线性表测试**:
**LinkedList合并线性表测试**:
3.线性结构-3:
参考Java Foundation 3rd 第15.6节,用数组实现线性表List,用JUnit或自己编写驱动类对自己实现的ArrayList进行测试。
- 用数组实现线性表明显需要对空间存储进行修改,无论使用静态方法还是动态方法定义数组,其存储空间都是固定的,所以这里有三种解决方法:
① 直接使用Object类定义一个固定大小的数组;
② 通过强制转型定义一个固定大小的数组:private T [] arr = (T [])new Object [num];
③ 当数组不能再存储线性表中的新元素时,创建一个更大的新数组来替换当前数组。
我采用了第一种方法,后面的两种方法后面才想到,还未实现。
private Object[] listSize = new Object[10]; //定义线性表及存储空间
private int length = 0; //线性表当前长度
采用方法一之后,定义类中的方法类型类似于第一个实验,这次我使用了 insert 方法在数组中插入新元素:
//在线性表的第 i 个数据元素之前插入一个值为 x 的数据元素
public void insert(int i, Object x) {
if (length == listSize.length)
System.out.println("线性表已满");
if (i < 0 || i > length)
System.out.println("插入位置不合法");
for (int j = length; j < i; j--)
listSize[j] = listSize[j - 1]; //元素后移
listSize[i] = x;
length++;
}
Junit测试:
4.线性结构-4:
参考Java Foundation 3rd 第15.7节,用链表实现线性表List,用JUnit或自己编写驱动类对自己实现的LinkedList进行测试。
首先介绍一下链表,因为教材中内容较少,所以我又查找了一些资料:
- 用链表实现线性表,这个实验相对较难,对于链表娄老师和王老师都讲过,我只知其意,至于怎么实现还有些困难,于是参考了相关资料中的代码,首先单链表是由若干个结点连接而成的。因此,要实现单链表,首先要设置结点类。结点类由 data数据域和 next指针域两部分构成:
public class Node {
private Object data; // 存放结点值
private Node next; //后继结点
public Node() {
this(null, null);
}
public Node(Object data) {
this(data, null);
}
public Node(Object data, Node next) {
this.data = data;
this.next = next;
}
public Object getData() {
return data;
}
public void setData(Object data) {
this.data = data;
}
public Node getNext() {
return next;
}
public void setNext(Node next) {
this.next = next;
}
}
对于单链表类,只需一个头指针就能唯一够标志它,对于单链表类,这里给出其中两个我已经理解的方法:
//求带头结点的单链表的长度
public int length() {
Node p = head.getNext(); //p指向首结点
int length = 0;
while (p != null) {
p = p.getNext();
length++;
}
return length;
}
//读取带头结点的链表中的第i结点
public Object get(int i) {
Node p = head.getNext();
int j = 0;
while (p != null && j < i) { //从首结点向后查找
p = p.getNext(); //指向后继结点
j++;
}
if (j > i || p == null) {
System.out.println("第" + i + "个元素不存在");
}
return p.getData();
}
对于链表的Junit测试,我有点“放水”地测试了比较简单的方法,原因是链表中一些方法的代码还未理解:
5.线性结构-5:
参考:http://www.cnblogs.com/rocedu/p/7483915.html
对Java的ArrayList,LinkedList按要求进行源码分析,并在实验报告中体现分析结果。- 源码分析,首先解压src源码包,找到ArrayList和LinkedList分析即可,其实我对源码分析的目的并不是很明确,源码中的大量注释基本能够解释其方法。总之,源码中的一些思想、添加注释的位置、代码的封装性等是值得学习的,我针对其中两个方法给出了自己的理解:
第一个是ArrayList中的trimToSize()
方法:
private static final Object[] EMPTY_ELEMENTDATA = {};
transient Object[] elementData;
private int size;
public void trimToSize() {
modCount++;
if (size < elementData.length) {
elementData = (size == 0) ? EMPTY_ELEMENTDATA : Arrays.copyOf(elementData, size);
}
}
// modCount这个属性是在其父类AbstractList中定义的,主要记录ArrayList的结构性变化次数,这里的trimToSize方法
// 的作用是调整适应列表的大小,如果size的值小于数据元素的长度,那么判断先size是否为零,这里使用三元运算符,
// 如果为零,则赋给数据元素空值,如果不为零调整size为合适大小.
第二个比较简单,是LinkedList中的 `indexOf()` 方法:
transient Object[] elementData;
private int size;
public int indexOf(Object o) {
if (o == null) {
for (int i = 0; i < size; i++)
if (elementData[i]==null)
return i;
} else {
for (int i = 0; i < size; i++)
if (o.equals(elementData[i]))
return i;
}
return -1;
}
// indexOf方法比较熟悉一点,可以遍历列表并返回首次出现此元素索引(即使该元素是值为空),
// 如果不包含(不存在此索引),则返回-1.
测试过程及遇到的问题:
1.第二个实验对两个线性表进行归并排序时,出现下标越界异常。"aList" 和 "bList" 我各分配了四个数的空间,为什么在归并到每个列表都只剩两个数的时候出现越界异常?
-
经过调试我发现当我传入的 i 自增到 2 时,
aList
和bList
各自都只剩下两个数的空间了,所以运行下一步时就会越界。原来我认为在线性表中remove了一个元素,后面元素的位置就不会再改变,但是事实上后面的数据元素会自动向前移,以至于在非空归并的时候下标越界,所以只需要将之前循环中的i++
去掉即可,线性表中的数据元素下标会随着之前元素的删除而自动前移。
2.在将线性表形式的数据元素转换成 String 类型的数据元素时,抛出"ClassCastException"。为什么这里的强转是多余的?
解决办法:(查找API并搜索)
查找之后才知道,在这里是因为将某一对象转换为某一子类时,如果该对象并非该子类的实例,就会抛出
ClassCastException
异常。就此例而言,前者n是String类型的,它的域要小于Object定义的get方法类,所以不能添加强转,只需要在后面添加空字符串就足够了,便可得到理想结果:
3."ArrayList类"和"LinkedList类"中除了方法之外还有什么不同?
- 解决办法:(搜索)
两个类在方法种类上差别较小,但是在使用上有着较大的差异,先来看ArrayList
的一些方法:
public boolean add(E e){
ensureCapacity(size+1);//确保内部数组有足够的空间
elementData[size++]=e;//将元素加入到数组的末尾,完成添加
return true;
}
public void ensureCapacity(int minCapacity){
modCount++;
int oldCapacity=elementData.length;
if(minCapacity>oldCapacity){ //如果数组容量不足,进行扩容
Object[] oldData=elementData;
int newCapacity=(oldCapacity*3)/2+1; //扩容到原始容量的1.5倍
if(newCapacitty<minCapacity) //如果新容量小于最小需要的容量,则使用最小需要的容量大小
newCapacity=minCapacity ; //进行扩容的数组复制
elementData=Arrays.copyof(elementData,newCapacity);
}
}
所以对于ArrayList
来说,在容量足够大的情况下,使用add方法的效率是很高的,而只有其对容量的需求超出数组的大小时,才会进行扩充容量。其中需要用到复制数组的System.arraycopy()
方法。
而对于LinkedList
来说,它的对应方法如下:
public boolean add(E e){
addBefore(e,header);//将元素增加到header的前面
return true;
}
private Entry<E> addBefore(E e,Entry<E> entry){
Entry<E> newEntry = new Entry<E>(e,entry,entry.previous);
newEntry.provious.next=newEntry;
newEntry.next.previous=newEntry;
size++;
modCount++;
return newEntry;
}
对于LinkeList
来说,它使用了链表的结构和存储方式,使用不需要维护容量的大小。所以它比ArrayList
具有一定的性能优势。但是,每次的元素的增加都需要新建一个Entry
对象,并进行更多的赋值操作。在频繁的系统调用中,难免会对性能产生影响。
同理,在插入、删除等方法内部两个类有着类似的区别。总的来说,使用ArrayList
虽然有时速度慢一些,但是有助于提高系统性能,而LinkedList
虽然有的时候更方便,更省时,但是其性能较差。
4.在进行源码分析时,经常会看到 "modCount" 变量,这个变量在不同方法中的作用是什么?
- 解决办法:(搜索)
modCount
主要用来记录ArrayList
或者LinkedList
类中结构性变化的次数。
例如:在ArrayList
的所有涉及结构变化的方法中都增加modCount
的值,包括:add()、remove()、addAll()、removeRange()及clear()方法。这些方法每调用一次,modCount
的值就加1,还有之前举过例子的trimToSize()
方法(ensureCapacity
方法等):
public void trimToSize() {
modCount++;
if (size < elementData.length) {
elementData = (size == 0) ? EMPTY_ELEMENTDATA : Arrays.copyOf(elementData, size);
}
}
【注】add()
及addAll()
方法的modCount
的值是在其中调用的ensureCapacity()
方法中增加的。
除此之外,modCount
还有另外的规避风险等作用。
分析总结:
本周的实验主要是对于线性结构的应用,同时也再次熟悉了Junit的使用。
本次实验的不足之处:
(1)我对于链表的代码还未完全理解;
(2)对于源码分析,我还不太明确其目的,但是我觉得可以从中学到一些以前异常出现的原因,并且可以让我们更深层次地添加注释,更规范地定义变量名称;
(3)实验要求“Junit测试要尽量覆盖正常情况,异常情况,边界情况。”这一点我做得不好,可能是因为缺少经验,测试覆盖的全面性还欠考虑。这次实验完成地有些匆忙,但是非常充实,这种感觉是清醒的,也是作为一个学生需要的。
PSP(Personal Software Process)时间统计:
-
步骤 耗时 百分比 需求分析 40min 10% 设计 40min 10% 代码实现 120min 30% 测试 100min 25% 分析总结 100min 25%