Java数据结构:ArrayList与顺序表1

一、List介绍

在集合框架中,List是一个接口,继承自Collection

Collection也是一个接口,

该接口中规范了后序容器中常用的一些方法,具体如下所示:

Iterable也是一个接口,表示实现该接口的类是可以逐个元素进行遍历的,具体如下:

站在数据结构的角度来看,List就是一个线性表,即n个具有相同类型元素的有限序列,在该序列上可以执行增删改查以及变量等操作。

List中常见的方法介绍

常用的方法:

方法解释
boolean add(E e)尾插 e
void add(int index,E element)将 e 插入到 index 位置
boolean addAll(Collection<? extends E> c)尾插 c 中的元素
E remove(int index)删除 index 位置元素
boolean remove(Object o)删除遇到的第一个 o
E get(int index)获取下标 index 位置元素
E set(int index, E element)将下标 index 位置元素设置为 element
void clear()清空
boolean contains(Object o)判断 o 是否在线性表中
int indexOf(Object o)返回第一个 o 所在下标
int lastIndexOf(Object o)返回最后一个 o 的下标
List<E> subList(int fromIndex, int toIndex)截取部分 list

List的使用

注意List是个接口,并不能直接用来实例化

如果要使用,必须去实例化List的实现类。在集合框架中,ArrayListLinkedList都实现了List接口。

二、ArrayList与顺序表

线性表

线性表(linear list)是n个具有相同特性的数据元素的有限序列。 线性表是一种在实际中广泛使用的数据结 构,常见的线性表:顺序表、链表、栈、队列...

线性表在逻辑上是线性结构,也就说是连续的一条直线。但是在物理结构上并不一定是连续的,线性表在物理上存储时,通常以数组和链式结构的形式存储。

顺序表

顺序表是用一段物理地址连续的存储单元依次存储数据元素的线性结构,一般情况下采用数组存储。在数组上完成 数据的增删查改

接口的实现(模拟实现ArrayList类中的增删查改方法)

模拟实现ArrayList顺序表中的增删查改方法,思路:

首先创建一个MyArrayList类,表示自身实现一个类,这个类中包含ArrayList中的方法,再写一个接口IList,让MyArrayList类实现这个接口,然后实现这个接口的抽象方法(即为增删查改方法)。

1.IList接口的完整框架

在这个接口中,有增删查改等抽象方法,例如add方法,它表示的是从数组的末尾增加元素,以及add方法的重载,表示的是从指定的位置插入元素等。

public interface IList {
    //获取顺序表长度
    int size();
    //新增元素,默认在数组末尾新增
    void add(int date);
    //在pos位置新增元素
    void add(int pos,int date);
    //顺序表已满
    boolean isFull();
    //顺序表增容
    void grow();
    //返回元素的下标
    int indexOf(int toFind);
    //获取pos位置元素
    int get(int pos);
    //顺序表为空
    boolean isEmpty();
    //修改pos位置元素
    void set(int pos,int value);
    //删除pos位置元素
    void remove(int toRemove);
    //清空顺序表
    void clear();

    //打印顺序表,注意:该方法并不是顺序表中的方法,为了方便看测试结果给出的
    void display();
}

2.MyArrayList类的完整框架

我们知道,顺序表一般情况下采用数组存储,那么我们就需要一个数组成员变量(这里为int类型),还需要一个usedSize成员变量,表示数组元素的有效个数;接着定义一个静态的成员变量DEFAULT_CAPACITY,它表示 规定的数组默认容量大小 ,最后写一个无参数构造方法,在这个构造方法中,初始化数组的默认容量,即将静态的成员变量DEFAULT_CAPACITY的值 作为这个数组的默认容量。不要忘记,MyArrayList类实现了IList接口,因此,必须重写接口中的抽象方法。

public class MyArrayList implements IList {
    public int[] array;
    public int usedSize;//未初始化默认值为0

    public static final int DEFAULT_CAPACITY = 10;//规定数组的默认容量大小   
    
    public MyArrayList() {
        this.array = new MyArrayList[DEFAULT_CAPACITY ];//初始化数组,规定数组的默认容量
    } 
    
    //重写接口中的抽象方法
    public void size() {
    
    }
    public int add(int date) {

    }
    ……………………
} 

现在,类和接口的整体框架已经说明完毕,现在,详细说明如何重写抽象方法

[1] size方法:获取顺序表长度

获取顺序表的长度,即获取的是顺序表中有效元素个数的长度,那么直接返回usedSize的值即可:

public int size() {
    return this.usedSize;
}
[2] add方法:从数组的末尾新增元素

从数组的末尾开始新增元素,也就是说从数组的最后一个有效元素个数后面开始增加,那么也就是usedSize的位置处开始增加,每次增加完成之后,有效元素个数就会增加一个,即usedSize++;注意:在进行增加操作之前,必须对数组是否已经满了isFull()进行判断,如果满了,就必须增容grow(),然后在进行新增操作。如果没满,那就直接进行新增操作。

(2.1) isFull方法:顺序表已满

这个方法的返回值类型为boolean,如果有效元素个数usedSize和数组的长度array.lenght相等,那么就代表这个数组已经满了,就会返回 true ,然后进行增容操作;如果不相等,就会返回 false ,无需进行增容操作,直接新增元素。

public boolean isFull() {
    return this.usedSize == array.lenght;
}
(2.2) grow方法:顺序表增容

如果在 isFull方法 中,判断数组已满,那么就必须对数组进行增容操作:前面我们学习Arrays类的时候,知道这个类中有一个方法—— copyOf方法,它的作用是用于复制数组并指定新数组长度;

那么我们可以利用这个方法来进行增容操作,即利用copyOf方法复制原数组并指定这个新数组的长度是原数组长度的2倍,最后将这个新数组赋值给原数组array,这样就表示增容成功。

pubilc void grow() {
    this.array = Arrays.copyOf(this.array,this.array.lenght * 2);
}
(2.3) add方法完整代码
public void add(int date) {
    if(isFull()) { //如果已满,返回true,执行增容操作
        grow();//增容
    }
    this.array[this.usedSize] = date;//在末尾处新增元素
    this.usedSize++;//有效个数增加
}
[3] add方法重载:在指定位置pos新增元素

依然需要对数组进行判满操作,看看是否需要增容,然后再进行指定位置新增元素操作:首先需要做的是 挪动元素,即想要在pos位置插入新元素,需要将原来的pos位置以及pos位置之后的元素往后挪动(注意要从后往前挪动),然后才可以将新元素插入到pos位置,最后让usedSize++。

注意:我们在挪动之前还需要判断pos位置是否合法,即pos不能小于0,且pos位置不能大于usedSize有效元素个数(usedSize位置可以进行插入),我们写一个checkPos方法,专门判断pos位置合法性,那么当检测到pos位置不合法时,具体该如何判断?

———— 使用 异常 知识

我们写一个异常类 PosIllegalException ,它继承于 RunTimeException 类,如果pos位置不合法,它会在运行时报异常错误;我们让checkPos方法throws声明这个可能发生的异常,然后在方法内部,pos不合法时throw抛出异常 ;最后我们在add方法内部,如果pos位置真的不合法,那么就 try-catch 捕获并处理这个异常。

(3.1) PosIllegalException 异常类
public class PosIllegalException extends RunTimeException {
    public PosIllegalException() {
        super();
    }
    public PosIllegalException(String message) {
        super(message);
    }
}
(3.2) checkPos方法:判断pos位置合法性

这个方法可以设置为私有方法private,因为这个方法只是为了判断合法性,只是在MyArrayList类内部使用即可,调用者调用的话没有任何意义。

private void checkPos(int pos) throws PosIllegalException { //声明异常
    if(pos < 0 || pos > usedSize) { //pos位置不合法
        throw new PosIllegalException("pos位置不合法"); //抛出异常
    }
}
(3.3) add方法重载完整代码
public void add(int pos,int date) {
    //捕获并处理异常
    //在try中存放的是可能出现异常的代码,即判断pos合法性、判满、增容、挪动并新增元素这些代码写在这里面
    try {
        checkPos(pos);
        if(isFull()) {
            grow();
        }
        //挪动元素
        for(int i = usedSize-1;i >= pos; i--) { //这里从最后一个有效元素开始遍历,即从后往前,这样才不会覆盖掉原有的元素
            array[i+1] = array[i];
        }
        array[pos] = date;
        usedSize++;
    }catch(PosIllegalException e) { //try中捕获了出现异常的代码,这里处理异常
        e.printStackTrace();
    }
}
[4] indexOf方法:返回元素的下标

遍历数组,如果能够在数组中找到与要找的元素相同的,就返回该元素在数组中对应的下标;否则就返回-1。

public int indexOf(int toFind) {
    for(int i = 0;i < usedSize; i++) {
        if(array[i] == toFind) {
            return i;
        }
    }
    return -1;
}
[5] get方法:获取pos位置的元素

依然需要判断pos位置的合法性,然后再进行获取操作:直接返回pos位置的元素即可,如果pos位置不合法,就返回-1。

注意:此时的checkPos方法与前面的不同,此时pos不能小于0,且pos也不能大于等于usedSize,因为数组中的元素对应的下标是0~usedSize-1。除此之外,我们还需要对数组进行判空操作,即如果数组的有效元素个数等于0,数组为空时,就无法获取指定元素,就会报异常,此过程中,我们需要一个新的checkPos方法,一个isEmpty方法,表示顺序表为空;一个EmptyException异常类以及一个声明这个异常类的checkEmpty方法。

(5.1) checkPos2方法:判断pos位置合法性
private void checkPos2(int pos) throws PosIllegalException {
    if(pos < 0 || pos >= usedSize) {  //这里变成 >=
        throw new PosIllegalException("pos位置不合法");
    }
}
(5.2) EmptyException异常类
public class EmptyException extends RunTimeException {
    public EmptyException() {
        super();
    }
    public EmptyException(String message) {
        super(message);
    }
}
(5.3) isEmpty方法:顺序表为空

这个方法的返回值设置为boolean,如果有效元素个数usedSize等于0,返回true,则说明数组为空。

public boolean isEmpty() {
    return this.usedSize == 0;
}
(5.4) checkEmpty方法:判断顺序表是否为空
private void checkEmpty() throws EmptyException {
    if(isEmpty()) {
        throw new EmptyException("顺序表为空");
    }
}
(5.5) get方法完整代码
public int get(int pos) {
    try {
        checkEmpty();
        checkPos2(pos);
        
        return array[pos];
    }catch(PosIllegalException e) {
        e.printStackTrace();
    }catch(EmptyException e) {
        e.printStackTrace();
    }

    return -1;//如果pos位置不合法或者顺序表为空,处理完异常后,就会返回-1
} 
[6] set方法:修改pos位置元素

依然需要进行判空和判断pos位置合法性,然后再进行修改操作:直接将pos位置的元素修改成想要的元素即可。

public void set(int pos,int value) {
    try {
        checkEmpty();
        checkPos2(pos);
        
        array[pos] = value;
    }catch(PosIllegalException e) {
        e.printStackTrace();
    }catch(EmptyException e) {
        e.printStackTrace();
    }
}
[7] remove方法:删除pos位置元素

首先判断顺序表是否为空,使用我们前面已经实现好的indexOf方法,来获取要删除的元素toRemove在数组中的下标位置pos,如果pos为-1,即数组中没有这个元素,则直接返回;如果不是-1,那么就从pos位置开始挪动元素:将数组中的元素一个个往前挪动,达到覆盖掉pos位置原来的元素的目的,即实现了元素的删除,最后记得usedSize--。

public void remove(int toRemove) {
    try {
        checkEmpty();
        int pos = indexOf(toRemove);
        if(pos == -1) {
            return;
        }
        for(int i = pos;i < usedSize-1; i++) { //这里注意是i < usedSize-1,否则会越界
            array[i] = array[i+1];
        }
        usedSize--;
        
    }catch(EmptyException e) {
        e.printStackTrace();
    }
}
[8] clear方法:清空顺序表

想要清空顺序表,我们只需要将有效元素个数usedSize置为0即可,原因:

将usedSize设置为0后,顺序表在逻辑上就被清空了。因为后续的访问操作(如get、remove等)都会基于usedSize的值,而usedSize为0表示当前没有元素。

public void clear() {
    usedSize = 0;
}

如果数组中的元素是一个引用类型,则需要这样写:将地址置为null,防止内存泄露

public void clear() {
    for(int i = 0;i < usedSize; i++) {
        array[i] = null;
    }
}

但是此时我们在数组中存放的是基本数据类型int类型,数组中没有引用类型,因此只需直接将其置为0即可。

[9] display方法:打印顺序表

该方法并不是顺序表中的方法,只是为了方便看测试结果给出的

public void display() {
    for(int i = 0;i < usedSize; i++) {
        System.out.print(array[i] + " ");
    }
}

到这里,所有的增删查改方法都已经实现完成,可以去实例化一个MyArrayList类对象去测试一下其中的add等方法。

示例:

public class TestMyArrayList {
    public static void main(String[] args) {
        MyArrayList myArrayList = new MyArrayList();
        
        myArrayList.add(1);
        myArrayList.add(2);
        myArrayList.add(3);
        myArrayList.add(4);
      
        myArrayList.add(3,15);
        System.out.println(myArrayList.indexOf(2));
        System.out.println(myArrayList.get(1));   

        myArrayList.set(3,150);
        myArrayList.display();
        
        myArrayList.remove(150);
        myArrayList.display();

        myArrayList.clear();
        myArrayList.display;
    }
}

三、ArrayList介绍

Java中,有自身的ArrayList类。

在集合框架中,ArrayList是一个普通的类,实现了List接口。

我们看看ArrayList的源码:

说明:

  • 1. ArrayList是以泛型方式实现的,使用时必须要先实例化
  • 2. ArrayList实现了RandomAccess接口,表明ArrayList支持随机访问
  • 3. ArrayList实现了Cloneable接口,表明ArrayList是可以clone的
  • 4. ArrayList实现了Serializable接口,表明ArrayList是支持序列化的
  • 5. 和Vector不同,ArrayList不是线程安全的,在单线程下可以使用,在多线程中可以选择Vector或者 CopyOnWriteArrayList
  • 6. ArrayList底层是一段连续的空间,并且可以动态扩容,是一个动态类型的顺序表

示例:我们可以通过实例化一个ArrayList泛型类,通过它的引用去访问它自带的add方法等:

public class TestArrayList {
    public static void main(String[] args) {
        ArrayList<Integer> list = new ArrayList<>();
        list.add(10);
        list.add(11);
        list.add(12);
        
        list.add(1,999);
        
        System.out.println(list);//可直接打印,因为ArrayList类重写了toString方法
    }
}

运行结果:

如果使用clear方法,那么顺序表就为空:

除了可以像上面所说的方式实现ArrayList的实例化:

ArrayList<Integer> list = new ArrayList<>();

意思是通过list这个引用,可以调用当前类的所有可以被调用的方法。

还有另一个方法,我们在前面说过ArrayList实现类List接口,因此,可以通过向上转型的方式实例化ArrayList对象:

List<Integer> list = new ArrayList<>();

意思是只要实现List这个接口的,都能实现向上转型,不过要注意,通过这个引用只能调用LIst接口中包含的方法,而不能调用ArrayLIst类中的方法。

至于要用哪一种实例化方式,则要根据实际情况定。

关于ArrayList类的其他细节,请看下一篇文章。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值