数据结构java版本
一、顺序表
前言
本文基于《数据结构及算法分析——Java语言描述》和《Java核心卷一》和黑马程序员相关课程及本人一些理解,简单介绍Java版本的线性表的基本实现,遍历,动态数组,还有ArrayList单链表的在上公司的源码。
一,顺序表的实现
1.顺序表的API设计
这里我借用黑马程序员课程中对API的设计图
首先我们要确定我们的存放数据的方式,因为数据结构的本质就是如何去存放数据,更为之方便算法实现,减少算法的时间复杂度和空间复杂度。
然后顺序表是一种数据不仅在物理层面上,而且在逻辑层面也是相邻的数据结构,所以我们会选择使用利用数组的方式来储存数据。而且任何数据结构都离不“增删查改”的操作,所以接下来对API的基本实现离不开这些。
2.顺序表的API具体实现
public class SequenceList {
//存储对象的数组
private T[] eles;
//当前线性表的长度
private int N;
//创造一个构造器,创造一个容量大小为capacity的SequenceList对象
SequenceList(int capacity){
}
//空置线性表
public void clean(){}
//判断线性表是否为空
public boolean isEmpty(){}
//获取线性表中元素个数
public int length(){}
//读取并返回线性表中第i个元素的数据
public T get (int i){}
//在线性表的第i个元素之前插入一个值为t的数据元素
public void insert(T t,int i){}
//向线性表的添加一个元素
public void add(T t){
//直接在线性表最后加入新元素就可以
eles[N++] = t;
}
//删除并返回线性表中的第i个数据元素
public T remove(int i){}
//返回线性表首次出现值为i的元素的位置序列,若不存在,则返回-1
public int indexOf(T t){}
我们现在已经创造SequenceList类了,并且已经在所有的方法中写入我们的所需要实现的方法了。
1)构造器的实现
//创造一个构造器,创造一个容量大小为capacity的SequenceList对象
SequenceList(int capacity){
//那么第一步我们需要创造一个数组
this.eles = new T[capacity];
}
首先我们先尝试这种创造数组的方法,这时候我们会发现idea会报错错误,这时候我们看看Java所报出的错误是什么?原来是不能直接实例化T,那么我们会发现T不是定义为泛型了吗?原因是我们没有对T这个类进行编写,是时候idea肯定会懵啊!你又没给我T这个类啊!你怎么实例化这个类!
所以正确的写法应该是
this.eles = (T[]) new Object[capacity];
这句话的意思首先先创造一个数组长度为capacity的类型为Object数组,因为我们可以知道Object是所有类的超类,所以不管怎么样,Object类一定是T的超类。再使用强制类型转化让数组只能接受T类型的数据。
2)方法的实现
1.空置顺序表
//空置线性表
public void clean(){
this.N = 0; //将线性表的长度变为0
}
为什么不需要重置数组所有元素呢?为什么这样写就行呢?因为我们对线性表的引用都是基于数组下标,而数组下标是基于N去实现的,而且Java内部中会有垃圾清理器,面对不要的数据会自动释放,而不用重新去重置所用数据。
2.判断线性表是否为空
public boolean isEmpty(){
if (this.N == 0)return true;
else return false;
}
借助线性表长度数据N来进行来判断现在线性表是否为空,还有更加简洁的写法
public boolean isEmpty(){
return this.N == 0;
}
3.获取线性表中元素个数
public int length(){
return N;
}
很简单的逻辑,这里就不加解释了。
4.读取并返回线性表中第i个元素的数据
public T get (int i){
if (N > i+1)return eles[i+1];
else return null;
}
因为是数组,所以数据的读取直接找到对应下标就可以返回了,唯一需要注意的点就是数组会不会越界的问题。
5.在线性表的第i个元素之前插入一个值为t的数据元素
public void insert(T t,int i){
++N;
for (int j = N-1; j >=i-1; j--) {
eles[j+1] = eles[j];
}
eles[i-1] = t;
}
插入操作需要先将i-1以及i-1之后的所有元素向后向移动一位,并且让N的值加一。为什么呢?如果在i处直接进行数据的直接插入,会将位置为i-1的数据给代替掉,所有需要进行数据的后移再进行数据的插入。
6.向线性表的添加一个元素
public void add(T t){
eles[N++] = t;
}
直接在线性表最后加入新元素并且使N的值加一就可以啦。
7.删除并返回线性表中的第i个数据元素
public T remove(int i){
for (int j = i+1; j < N-1; j++) {
eles[j] = eles[j++];
}
N = N-1;
return eles[i+1];
}
这个操作需要在位置i+1的地方进行操作,让后面的元素覆盖前面的元素,让线性表长度减一,和上面插入操作相似。
8.返回线性表首次出现值为i的元素的位置序列,若不存在,则返回-1
public int indexOf(T t){
for (int i = 0; i < N; i++) {
if (t.equals(eles[i]))
return i;
}
return -1;
}
调用了equals方法,对于这个方法具体的可以看我的这一篇文章
9.实现的全部代码
public class SequenceList {
//存储对象的数组
private T[] eles;
//当前线性表的长度
private int N;
//创造一个构造器,创造一个容量大小为capacity的SequenceList对象
SequenceList(int capacity){
/*
首先先创造一个数组长度为capacity的数组,用Object创造数组再强制类型转换,为什么要用这样呢?我们可以用T来创建数组
*/
// this.eles = new T[capacity];//我们可以看到idea会给我们报出T对象不能直接实例化的错误
this.eles = (T[]) new Object[capacity];
}
//空置线性表
public void clean(){
this.N = 0; //将线性表的长度变为0
}
//判断线性表是否为空
public boolean isEmpty(){
if (this.N == 0)return true;
else return false;
}
//获取线性表中元素个数
public int length(){
return N;
}
//读取并返回线性表中第i个元素的数据
public T get (int i){
//因为是数组,所以数据的读取直接找到对应下标就可以返回了
if (N > i+1)return eles[i+1];
else return null;
}
//在线性表的第i个元素之前插入一个值为t的数据元素
public void insert(T t,int i){
//插入操作需要先将i+1以及i+1之后的所有元素向后向移动一位
for (int j = N-1; j >=i-1; j--) {
eles[j+1] = eles[j];
}
eles[i-1] = t;
}
//向线性表的添加一个元素
public void add(T t){
//直接在线性表最后加入新元素就可以
eles[N++] = t;
}
//删除并返回线性表中的第i个数据元素
public T remove(int i){
//这个需要在位置i+1的地方进行操作,让后面的元素覆盖前面的元素
for (int j = i+1; j < N-1; j++) {
eles[j] = eles[j++];
}
//让线性表长度减一
N = N-1;
return eles[i+1];
}
//返回线性表首次出现值为i的元素的位置序列,若不存在,则返回-1
public int indexOf(T t){
for (int i = 0; i < N; i++) {
if (t.equals(eles[i]))
return i;
}
return -1;
}
3)线性表的遍历
1.迭代器的使用
如果想让线性表进行foreach循环,那么我们就需要使用迭代器,具体我们需要让我们的SequenceList类实现Iterable接口。
//先让SequenceList实现Iterable接口
public class SequenceList<T> implements Iterable<T>
//我们重写这个方法
public Iterator iterator() {
return null;
}
//我们会发现这个接口没有实现类
//所以接下来我们会定义一个内部类来实现这个Iterator接口
public class SIterable implements Iterator{
private int point;
public SIterable(){
this.point = 0;
}
@Override
public boolean hasNext() {
return point < N-1;
}
@Override
public Object next() {
return eles[++point];
}
这个重写方法作用应该显而易见吧,一个是判断是否还有后面的元素,一个是让下一个元素返回的。那么最后实现类那应该写成
@Override
public Iterator iterator() {
return new SIterable();
}
这样我们的迭代器就完成了,那么我们就可以在测试类中测试我们的程序了。
4)动态数组,以及ArrayList源码的理解
1.动态数组的定义
1、为什么会有动态数组这个概念?
原因是Java和c不同的点在于,c可以可以通过malloc函数动态地为数据分配内除空间,而java却没有这种功能,在《java核心卷1》第5章的第三节对这种特性就有所描述,原因是数组在编译时候就一定需要确定大小。为了减少对内存的浪费,所以提出了动态数组的概念。
2.如何实现动态数组的操作?
1.自己编写
- 首先我们先明确我们的目的是什么?我们的目的是内存空间物尽其用,并且给我们数据有一个充足的存放空间。
- 既然是动态数组,我们面对上面两种情况进行一个新数组的编写,将旧数组的值拷贝进入我们新数组,好像是可行的。
- 那么旧数组会去哪里?鉴于Java强大清除功能,当我们在后续操作中不再调用数组了,就会被清理了,所以我们无需理会不再使用的数组。
接下来就是我们的代码编写了
public void enlarrgeArray(int chargeSize){
//一个新数组指向原数组
T[] temp = eles;
eles = (T[]) new Object[chargeSize];
//将旧数组的值赋给新数组
for (int i = 0; i < N; i++) {
eles[i] = temp[i];
}
}
这个动态数组可以在在存储数组中进行判断使用,如果不够加进行缩容操作,如果
已经满了那就扩容。
2.使用ArrayList类
- 在《Java核心技术1》对ArrayList类的描述是一个采用类型参数的泛型类。
- 在《数据结构及算法分析——Java语言描述》中对ArrayList类提供了List ADT的一种可增长的数据实习。
接下来我们可以去idea中看看ArrayList类的源代码。
public <T> T[] toArray(T[] a) {
if (a.length < size)
// Make a new array of a's runtime type, but my contents:
return (T[]) Arrays.copyOf(elementData, size, a.getClass());
System.arraycopy(elementData, 0, a, 0, size);
if (a.length > size)
a[size] = null;
return a;
}
我们可以看到ArrayList类中源码中,也是编写了一个动态数组的操作,所以ArrayList类的底层就是动态数组。而且ArrayList类中具备增删查改的操作,所以在实际使用的时候我们可以直接调用ArrayList类下的方法。
5)顺序表方法的操作的时间复杂度
对于下列的操作
- 清空
- 获取顺序表中的元素个数
- 查找顺序表中某一个位置的元素
- 向线性表最后加入元素
操作时间复杂度都为O(1)
对于增加、删除还有按值查找的操作要分情况回答
- 删除
最好的情况:
删除最后一位元素,那么这时的时间复杂度为O(1);
最坏的情况:
删除第一位元素,所有元素前移一位,那么这时候时间复杂度就会为O(n);
那么平均而言,O(n*(n+1)/2),然后根据规则来讲,要忽略常数,与最高项相乘的常数还有幂次小于最高次的项。最后我们可以得出删除操作的时间复杂度为O(n) - 插入
最好的情况:
在最后一位元素插入,所有元素不需要移动,那么这时的时间复杂度为O(1);
最坏的情况:
在第一位元素插入,所有元素后移一位,那么这时候时间复杂度就会为O(n);
那么平均而言,O(n*(n+1)/2),最后我们可以得出插入操作的时间复杂度为O(n) - 按值查找
最好的情况:
在第一位元素找到,那么这时的时间复杂度为O(1);
最坏的情况:
在最后一位元素找到,或者找不到,那么这时候时间复杂度就会为O(n);
那么平均而言,O(n*(n+1)/2),最后我们可以得出按值查找操作的时间复杂度为O(n)
总结
本文,我主要讲述了简单的数据结构——顺序表的实现方式,还有对于现成API的有点理解。在这一次编写的过程,我也是发现原来顺序表的遍历是可以通过迭代器来实现的。也是一次抛离现成API的进行的数据结构的编写。希望接下来再接再励,如果本文对你有帮助或者本文有什么错误的地方,希望大家能指正我的错误。