package list;
/*
* 顺序表类
* 顺序表是随机存取结构
* 插入和删除操作效率很低
*/
public class SeqList extends Object {
protected Object[] element; // 创建泛型数组
protected int n; // 数组长度,因为这两个类型都是相互关系的,所以要用保护成员,防止被更改
/*
* 构造函数1,固定长度
*/
public SeqList(int length) {
this.element = new Object[length]; // 构造长度为length的空表
this.n = 0;
}
/*
* 构造函数2,默认长度,构造方法重载
*/
public SeqList() {
this(64); // 调用本类已经声明的指定参数列表的构造方法,也就是上面那个构造函数1
}
/*
* 构造函数3,由values数组提供元素
*/
public SeqList(T[] values) {
this(values.length); // 创建容量为values.length的空表
for (int i = 0; i < values.length; i++) {
this.element[i] = values[i]; // 对象引用赋值
}
this.n = element.length;
}
/*
* 判断表是否为空
*/
public boolean isEmpty() {
return this.n == 0;
}
/*
* 查询顺序表的大小
*/
public int size() {
return this.n;
}
/*
* 获取第i个元素
*/
public T get(int i) {
if (i >= 0 && i < this.n) { // 防止溢出
return (T) this.element[i]; // 传递对象引用
}
return null; // 溢出发生时,返回空,会抛出空对象异常
}
/*
* 将顺序表第i个元素设置为x
*/
public void set(int i, T x) {
if (x == null) // 如果x为空,则抛出空异常
throw new NullPointerException("x==null");
if (i >= 0 && i < this.n) // 如果i没有溢出,则开始执行赋值语句
this.element[i] = x;
// 如果有溢出,抛出越界异常
else
throw new java.lang.IndexOutOfBoundsException(i + "");
}
/*
* 描述属性值,即将所有元素都转换为字符串返回
*/
public String toString() {
String str = this.getClass().getName() + "("; // 返回类名
if (this.n > 0)
str += this.element[0].toString(); // 添加元素
for (int i = 1; i < this.n; i++)
str += "," + this.element[i].toString();
return str + ")";
}
/*
* 插入某个元素,时间复杂度为O(n),上面那些语句都是为了寻找当前x要插入的位置i这里的插入是指在第i个元素之后插入,而不是插入到第i位上,因为数组上有0位
*/
public int insert(int i, T x) {
if (x == null) // 如果要插入的元素为空,要抛出异常
throw new NullPointerException("x==null");
if (i < 0)
i = 0; // 如果i是溢出的,要进行容错
if (i > this.n)
i = this.n; // 这也是容错
Object[] source = this.element;
if (this.n == element.length) { // 如果数组满了,要进行扩展空间,并复制到新数组中
this.element = new Object[source.length * 2]; // 申请新的空间
for (int j = 0; j < i; j++) { // 进行复制,仅仅复制第i位之前的,因为之后要后移
this.element[j] = source[j];
}
}
for (int j = this.n - 1; j >= i; j--) { // 第i个元素之后依次向后移动一位
this.element[j + 1] = source[j];
}
this.element[i] = x; // 插入x元素
this.n++; // 更改插入后的长度
return i; // 返回x的序号,这个可以随意改
}
/*
* 顺序表末尾插入,成员方法重载
*/
public int insert(T x) {
return this.insert(this.n, x);
}
/*
* 移除顺序表第i个元素,花费时间主要用于移动元素,在等概率情况下,删除一个元素 平均移动n/2个元素,时间复杂度为O(n)
*/
public T remove(int i) {
if (this.n > 0 && i >= 0 && i < this.n) { // 首先判断是否符合正常逻辑
T old = (T) this.element[i];
for (int j = i; j < this.n - 1; j++) // 第i个元素之后依次向前覆盖一位
this.element[j] = this.element[j + 1];
this.element[this.n - 1] = null; // 设置数组元素最后一位为空,释放原引用实例
// 感觉应该是这个实例没有引用指向它时,就会被回收
this.n--; // 总长度更新
return old; // 这个返回是为了判断是否溢出的问题
}
return null; // 如果输入的i不符合条件,即溢出,返回空对象
}
/*
* 删除线性表中所有元素
*/
public void clear() {
this.n = 0; // 设置长度为0,未释放数组空间
}
/*
* 顺序表查找操作,这里用到了T类的equals(Object)方法,运行时多态
* 也就是这里的比较会随着你的T类类型的改变而改变对比方法,比如你的T是String,
* 那么比较时,就会调用String.equals()方法,如果是Integer,则会调用Interger的方法
* 运行时,可以有多个状态(自己的理解,不一定对,后期会不断更新)
*/
public int search(T key) {
for (int i = 0; i < this.n; i++) {
if (key.equals(this.element[i])) // 遍历寻找
return i;
}
return -1; // 当空表或者没有找到时
}
/*
* 顺序表的比较相等,两种情况 主要步骤是equals(Object),时间复杂度为O(n)
*/
public boolean equals(Object obj) {
if (this == obj) { // 若this和obj引用同一个顺序表实例,则相等
return true;
}
if (obj instanceof SeqList) { // 若两者引用的实例不同,SeqList是所有SeqList的父类
SeqList list = (SeqList) obj; // 赋值,然后进行比较
if (this.n == list.n) { // 如果长度相同,执行下一步比较
for (int i = 0; i < this.n; i++) // 只要有一个元素不相同,就表明不相等
if (!(this.get(i).equals(list.get(i))))
return false;
return true; // 到了这一步表明,上面两个条件都符合,相等
}
}
return false; // 如果上面两个情况都不属于,那么肯定不等
}
public static void main(String[] args) {
// TODO Auto-generated method stub
SeqList list = new SeqList(10);
list.insert(0, "aa");
list.insert("bb");
System.out.println("表长度:" + list.size());
if (list.search("cc") == -1)
System.out.println("该表中没有cc元素");
else
System.out.println("该表中存在cc元素");
System.out.println(list.toString());
list.set(0, "111");
System.out.println(list.toString());
list.clear();
System.out.println(list.toString());
}
}
(1)上面顺序表的实现主要依据数组,而数组(Array)存储具有相同数据类型的元素集合,每个存储单元的地址是连续的,所以计算第i个元素地址所需要的时间是一个常量(只需要知道数组首地址,然后加上每个元素的长度*i,即可),所以时间复杂度是O(1),与元素序号i无关
(2)随机存取结构:存取任何元素的时间复杂度是O(1)的数据结构(所以数组是随机存取结构,当然顺序表也是)
因为顺序表在建立是就已经确定了其长度(也就是数组的长度),所以当你的顺序表使用的数组容量不够时,解决数据溢出的办法是:申请另一个更大容量的数组,并进行数组元素复制,这样就扩充了顺序表的容量(感觉很麻烦)
声明SeqList
注意:Java语言规定,T的实际参数必须是类,不能是int,char等基本数据类型,如果需要表示基本数据类型,则必须采用基本数据类型包装类,比如Integer、Character等。
还有当你创建实例时,规定好了T的类型,那么list表中所有元素都必须是这个类型,之外的类型会报错
大家可以注意到,上面SeqList类中,仅有两个全局变量:element和n,而且还被定义为了protect类型,这是为了保护这两个变量不被其他程序改变(这两个变量对于整个顺序表至关重要,不能有任何改变,要隐藏起来,除了子类,其他任何程序都没有权限改变),可以说是透明的!
这里采用了三个构造方法,多次采用了重载(调用本类已经定义好了的构造方法,要用到this()函数),这里重载其实实现了多态性(根据你输入不同参数的状态,而调用不同的构造方法),使得程序更加灵活
其实java类提供了一个默认的无参数的构造方法(默认调用super()),但是如果一个类声明了构造方法时,Java就不会再提供默认构造方法了
类的析构方法(destructor):用于释放实例并执行特定操作(也就是说当你的顺序表实例没有用了,就可以释放以节省空间,在释放的时候,可以执行某些特定操作——当然这些操作你是可以设定的,在析构方法中)
注意,一个类只能有一个析构方法,不能重载
java约定的析构方法声明如下:
public void finalize() //析构方法
其实在java语言中,java本身可以自动释放不再使用的存储空间(通过垃圾回收机制),所以一般不需要我们调用析构方法来释放存储空间,当然如果你一定要执行一下析构方法(或者是某种特殊需要),也可以通过重写上面的析构方法来实现
网上关于析构方法的解释很多,这里就不再一一列举了(主要是占用篇幅太多,这里只是启发式地讲解一下)
这个问题是新手经常忽略的一个问题,如果是一般运行出错概率很小,也可以不考虑(但是从编程本身考虑,不建议只以运行成功为目的),但是运行成功只是编程的一个方面,我们还要考虑安全和维护等多方面的问题,按照一个前辈的说法:你这样编写程序,只是5K级的水平(这里5K是工资。。。)
一个程序的成功要考虑多个方面,第一当然是运行成功了,运行成功以后,你还要多方面测试(如果不同输入,会不会出现错误呢),其次要进行重构(使你的程序更加简单,更加易懂,这是高级程序员的必修课),当然还有很多其他方面的这里就不再一一说了(主要是没有词了。。。。)
好了,该说一下正题了,在SeqList类的get(i)、set(i,x)方法中,如果需要i,超出了范围(本来应该是0<=i ①:方法返回错误信息,get(i)返回null(也就是当你在执行get方法时,发现 i 溢出了,那么你就将返回值设为nul)表示操作不成功,当你get(i)返回null时,再调用其他方法,java就会抛出NullPointerException空对象异常。 ②:抛出异常,当你察觉到 i 溢出时,也不用管什么返回值了,直接抛出序号越界异常IndexOutOfBoundsException,将需要信息传递给调用者(上面set方法就是用的这种解决方案,当然一般新手是不习惯与这种写法的,因为这个越界异常很难记,可以采用上面的方法) 这里说一下运行时多态问题,Java支持运行时多态,也就是根据你运行的状态,在运行时Java可以自己确定执行哪一个方法(如果该方法有重载时),还是举上面那个构造方法的例子吧,有多个构造方法(重载),当你创建对象时,Java会根据你的参数的不同,而选择不同的构造方法来执行 顺序表的插入和删除都是相当麻烦的(要不是复习一遍数据结构,我一般不会用顺序表这种结构),它的时间复杂度都是O(n)——这里指的是没插入一个元素或者删除一个元素的时间复杂度! 如果插入到最前面,则需要移动n个元素,如果插入到末尾,则需要移动0位,那么插入一个元素的平均移动次数为: 如上图所示,Pi表示每个插入位置被选中的概率,然后乘上每个位置对应的移动次数,这就是总的期望次数了,这里设置选中的插入元素概率相同,也就说Pi = 1/(n+1),这里n+1是因为插入位置要比元素个数多1 ,将pi提出去,然后就可以计算0+1+2+3.。。。+n了,最后算出来为:n/2,所以时间复杂度就是O(n )了 直接看代码吧: 上面这个图讲解的非常清楚,两个引用变量,只要一个发生变化另外一个就会发生变化! 注意:浅拷贝根本就没有为另一个数组申请空间,两者是共用一个数组的! 直接上代码: 上面这个例子,我称为半深层拷贝(没有这个词哈,我自己创的),因为它只完成了一半(申请了一个数组空间),但是最后赋值的时候,仍然是对象引用赋值,没有创建新对象,可以参考如下图所示: 上图就是上面代码的形象化描述,虽然插入和删除后面的元素并不会影响listb,但是如果改变其中一个元素就会影响对方对象了。 代码如下:解决办法有两个:
(7)运行时多态(说的简单一些,主要是启发式的,不懂可以再查阅相关资料)
4、顺序表的插入操作
其计算步骤:
5、顺序表的深拷贝和浅拷贝问题(下一次再更新完毕)
(1)浅拷贝(通常是有错误的)
public SeqList(SeqList
(2)深拷贝
public SeqList(SeqList list) { //半深层拷贝
this.n = list.n;
this.element = new Object[list.element.length]; // 申请一个数组
for (int i = 0; i < list.n; i++) {
this.element[i] = list.element[i]; //对象引用赋值,但是没有创建新对象
}
}
真正的深拷贝应该如下图所示:
参考书籍:《数据结构(java版)》叶核亚,有不懂的,可以再看一下这本书
public SeqList(SeqList