表结构之顺序表

package list;

/*
 * 顺序表类
 * 顺序表是随机存取结构
 * 插入和删除操作效率很低
 */
public class SeqList<T> 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<T>的父类

			SeqList<T> list = (SeqList<T>) 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<String> list = new SeqList<String>(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、时间复杂度:

(1)上面顺序表的实现主要依据数组,而数组(Array)存储具有相同数据类型的元素集合,每个存储单元的地址是连续的,所以计算第i个元素地址所需要的时间是一个常量(只需要知道数组首地址,然后加上每个元素的长度*i,即可),所以时间复杂度是O(1),与元素序号i无关

(2)随机存取结构:存取任何元素的时间复杂度是O(1)的数据结构(所以数组是随机存取结构,当然顺序表也是)

2、顺序表的扩充

因为顺序表在建立是就已经确定了其长度(也就是数组的长度),所以当你的顺序表使用的数组容量不够时,解决数据溢出的办法是:申请另一个更大容量的数组,并进行数组元素复制,这样就扩充了顺序表的容量(感觉很麻烦)

3、顺序表设计说明

(1)泛型类(说简单也简单,说难也难)

声明SeqList<T>为泛型类(是为了方便),类型形式参数T称为泛型,T表示顺序表数据元素的数据类型,这个类型是不定的,只有当你创建实例时,才会被赋予真正的值(这个值可以自由拟定,可以是String,也可以说Integer)

注意:Java语言规定,T的实际参数必须是类,不能是int,char等基本数据类型,如果需要表示基本数据类型,则必须采用基本数据类型包装类,比如Integer、Character等。

还有当你创建实例时,规定好了T的类型,那么list表中所有元素都必须是这个类型,之外的类型会报错

(2)顺序表对象引用实例的存储结构(这个图还是很有用的,可以理解一下)


(3)隐藏成员变量:

大家可以注意到,上面SeqList类中,仅有两个全局变量:element和n,而且还被定义为了protect类型,这是为了保护这两个变量不被其他程序改变(这两个变量对于整个顺序表至关重要,不能有任何改变,要隐藏起来,除了子类,其他任何程序都没有权限改变),可以说是透明的!

(4)构造方法

这里采用了三个构造方法,多次采用了重载(调用本类已经定义好了的构造方法,要用到this()函数),这里重载其实实现了多态性(根据你输入不同参数的状态,而调用不同的构造方法),使得程序更加灵活

其实java类提供了一个默认的无参数的构造方法(默认调用super()),但是如果一个类声明了构造方法时,Java就不会再提供默认构造方法了

(5)析构方法(这个用的不多,理解的不是很清楚,大家不明白可以再自行再查阅)

类的析构方法(destructor):用于释放实例并执行特定操作(也就是说当你的顺序表实例没有用了,就可以释放以节省空间,在释放的时候,可以执行某些特定操作——当然这些操作你是可以设定的,在析构方法中)

注意,一个类只能有一个析构方法,不能重载

java约定的析构方法声明如下:

public void finalize()  //析构方法

其实在java语言中,java本身可以自动释放不再使用的存储空间(通过垃圾回收机制),所以一般不需要我们调用析构方法来释放存储空间,当然如果你一定要执行一下析构方法(或者是某种特殊需要),也可以通过重写上面的析构方法来实现

网上关于析构方法的解释很多,这里就不再一一列举了(主要是占用篇幅太多,这里只是启发式地讲解一下)

(6)存取操作,当指定元素需要不正确时的处理原则

这个问题是新手经常忽略的一个问题,如果是一般运行出错概率很小,也可以不考虑(但是从编程本身考虑,不建议只以运行成功为目的),但是运行成功只是编程的一个方面,我们还要考虑安全和维护等多方面的问题,按照一个前辈的说法:你这样编写程序,只是5K级的水平(这里5K是工资。。。)

一个程序的成功要考虑多个方面,第一当然是运行成功了,运行成功以后,你还要多方面测试(如果不同输入,会不会出现错误呢),其次要进行重构(使你的程序更加简单,更加易懂,这是高级程序员的必修课),当然还有很多其他方面的这里就不再一一说了(主要是没有词了。。。。)

好了,该说一下正题了,在SeqList类的get(i)、set(i,x)方法中,如果需要i,超出了范围(本来应该是0<=i<n),那么我们应该怎么办呢(或者说不能执行了,举个例子,你表中一共就有2个元素,它让你返回第6个元素,你怎么返回啊?)

解决办法有两个:

①:方法返回错误信息,get(i)返回null(也就是当你在执行get方法时,发现 i 溢出了,那么你就将返回值设为nul)表示操作不成功,当你get(i)返回null时,再调用其他方法,java就会抛出NullPointerException空对象异常。

②:抛出异常,当你察觉到 i  溢出时,也不用管什么返回值了,直接抛出序号越界异常IndexOutOfBoundsException,将需要信息传递给调用者(上面set方法就是用的这种解决方案,当然一般新手是不习惯与这种写法的,因为这个越界异常很难记,可以采用上面的方法)

(7)运行时多态(说的简单一些,主要是启发式的,不懂可以再查阅相关资料)

这里说一下运行时多态问题,Java支持运行时多态,也就是根据你运行的状态,在运行时Java可以自己确定执行哪一个方法(如果该方法有重载时),还是举上面那个构造方法的例子吧,有多个构造方法(重载),当你创建对象时,Java会根据你的参数的不同,而选择不同的构造方法来执行

4、顺序表的插入操作


顺序表的插入和删除都是相当麻烦的(要不是复习一遍数据结构,我一般不会用顺序表这种结构),它的时间复杂度都是O(n)——这里指的是没插入一个元素或者删除一个元素的时间复杂度!

其计算步骤:

如果插入到最前面,则需要移动n个元素,如果插入到末尾,则需要移动0位,那么插入一个元素的平均移动次数为:

如上图所示,Pi表示每个插入位置被选中的概率,然后乘上每个位置对应的移动次数,这就是总的期望次数了,这里设置选中的插入元素概率相同,也就说Pi = 1/(n+1),这里n+1是因为插入位置要比元素个数多1 ,将pi提出去,然后就可以计算0+1+2+3.。。。+n了,最后算出来为:n/2,所以时间复杂度就是O(n )了

5、顺序表的深拷贝和浅拷贝问题(下一次再更新完毕) 

(1)浅拷贝(通常是有错误的)

直接看代码吧:

public SeqList(SeqList<T> list) {
		this.n = list.n;
		this.element = list.element; //数组引用赋值,两个变量共用一个数组,错误
	}

上面这个图讲解的非常清楚,两个引用变量,只要一个发生变化另外一个就会发生变化!

注意:浅拷贝根本就没有为另一个数组申请空间,两者是共用一个数组的!

(2)深拷贝

直接上代码:

public SeqList(SeqList<? extends T> 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];  //对象引用赋值,但是没有创建新对象
		}
	}

上面这个例子,我称为半深层拷贝(没有这个词哈,我自己创的),因为它只完成了一半(申请了一个数组空间),但是最后赋值的时候,仍然是对象引用赋值,没有创建新对象,可以参考如下图所示:


上图就是上面代码的形象化描述,虽然插入和删除后面的元素并不会影响listb,但是如果改变其中一个元素就会影响对方对象了。

真正的深拷贝应该如下图所示:


代码如下:

public SeqList(SeqList<String> list) {
		this.n = list.n;
		this.element = new Object[list.element.length]; // 申请一个数组
		for (int i = 0; i < list.n; i++) {
			String a = (String) list.element[i];  //创建一个新的对象
			this.element[i] = a; // 直接赋值到最低层次
		}
	}
参考书籍:《数据结构(java版)》叶核亚,有不懂的,可以再看一下这本书

猜你喜欢

转载自blog.csdn.net/yuangan1529/article/details/80149329
今日推荐