数据结构——线性表的实现

数据结构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.自己编写
  1. 首先我们先明确我们的目的是什么?我们的目的是内存空间物尽其用,并且给我们数据有一个充足的存放空间。
  2. 既然是动态数组,我们面对上面两种情况进行一个新数组的编写,将旧数组的值拷贝进入我们新数组,好像是可行的。
  3. 那么旧数组会去哪里?鉴于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)顺序表方法的操作的时间复杂度

对于下列的操作

  1. 清空
  2. 获取顺序表中的元素个数
  3. 查找顺序表中某一个位置的元素
  4. 向线性表最后加入元素
    操作时间复杂度都为O(1)

对于增加、删除还有按值查找的操作要分情况回答

  1. 删除
    最好的情况:
    删除最后一位元素,那么这时的时间复杂度为O(1);
    最坏的情况:
    删除第一位元素,所有元素前移一位,那么这时候时间复杂度就会为O(n);
    那么平均而言,O(n*(n+1)/2),然后根据规则来讲,要忽略常数,与最高项相乘的常数还有幂次小于最高次的项。最后我们可以得出删除操作的时间复杂度为O(n)
  2. 插入
    最好的情况:
    在最后一位元素插入,所有元素不需要移动,那么这时的时间复杂度为O(1);
    最坏的情况:
    在第一位元素插入,所有元素后移一位,那么这时候时间复杂度就会为O(n);
    那么平均而言,O(n*(n+1)/2),最后我们可以得出插入操作的时间复杂度为O(n)
  3. 按值查找
    最好的情况:
    在第一位元素找到,那么这时的时间复杂度为O(1);
    最坏的情况:
    在最后一位元素找到,或者找不到,那么这时候时间复杂度就会为O(n);
    那么平均而言,O(n*(n+1)/2),最后我们可以得出按值查找操作的时间复杂度为O(n)

总结

本文,我主要讲述了简单的数据结构——顺序表的实现方式,还有对于现成API的有点理解。在这一次编写的过程,我也是发现原来顺序表的遍历是可以通过迭代器来实现的。也是一次抛离现成API的进行的数据结构的编写。希望接下来再接再励,如果本文对你有帮助或者本文有什么错误的地方,希望大家能指正我的错误。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值