Java数据结构(三)——顺序表与ArrayList

顺序表

前置知识

顺序表线性表的一种(底层是数组),另一种是链表,说到线性表,就得了解 List接口,站在数据结构的角度上,List接口就是线性表,即n个具有相同类型元素的有限序列,在该序列上可以执行增删查改以及变量等操作。

List接口继承自Collection接口,Collection接口规范了后序容器常用的一些方法,例如求元素个数size()、添加新元素add(E)/addAll(Collection<? extends E>)等等,如下图:

在这里插入图片描述

了解完Collection接口的方法,再来看一下List接口的方法,有很多,只截取一部分:
在这里插入图片描述

很多是吧,我们列举常用的,包括接下来要讲的ArrayList类,常用方法也是这几个:

  1. boolean add(E e):尾插e
  2. void add(int index, E element):将 e 插入到index位置
  3. boolean addAll(Collection<? extends E> c):尾插 c 中的所有元素(参数列表的形式我们会在泛型进阶讲解),这里的参数可以接受任何类型的集合,只要这个集合的元素类型是 E 或其子类。
  4. E remove(int index):删除 index 位置的元素
  5. boolean remove(Object o):删除遇到的第一个 o
  6. E get(int index):获取 index 下标位置元素,
  7. E set(int index, E element):将 index 下标位置元素设置为 element
  8. void clear():清空容器
  9. boolean contains(Object o):判断 o 是否在线性表中
  10. int indexOf(Object o):返回第一个 o 所在的下标
  11. int lastindexOf(Object o):返回最后一个 o 所在的下标
  12. List<E> subList(int fromIndex, int toIndex):截取下标区间 [fromIndex, int toIndex] 的元素

【List的使用】

List是一个接口,并不能直接用来实例化,使用时必须实例化List的实现类。 在集合框架中,ArrayListLinkedList都实现了List接口,而ArrayList就是本篇要介绍的主角。


顺序表是用一段物理地址连续的存储单元依次存储数据元素的线性结构,一般情况下采用数组存储。在数组上完成数据的增删查改。在集合框架中,对应 ArrayList。不过要注意的是,这个数组只能依次存储,不能跳跃式的存储,比如,添加新的元素,不能跳过一个下标,存储在后面,必须依次存储。

我们先看ArrayList类的部分源码:

在这里插入图片描述

  • ArrayList是以泛型方式实现的,是一个泛型类,注意实例化
  • ArrayList实现了RandomAccess接口,表明ArrayList支持随机访问
  • ArrayList实现了Cloneable接口,表明ArrayList是可以clone
  • ArrayList实现了Serializable接口,表明ArrayList是支持序列化的
  • 另外,ArrayList不是线程安全的,在单线程下可以使用,在多线程中可以选择VectorCopyOnWriteArrayList
  • ArrayList底层是一段连续的空间,并且可以动态扩容,是一个动态类型的顺序表

ArrayList的构造

ArrayList的构造方法有三个:

  1. ArrayList():无参数构造方法
  2. ArrayList(Collection<? extends E> c):利用其他实现了Collection接口的容器构造
  3. ArrayList(int initialCapacity):指定顺序表初始容量的构造方法
        public static void main(String[] args) {
            
            ArrayList<Integer> l1 = new ArrayList<>();//无参构造
            System.out.println(l1);
            
            ArrayList<Integer> l2 = new ArrayList<>(10);//指定初始容量构造
            l2.add(1);
            l2.add(2);
            System.out.println(l2);
            
            ArrayList<Integer> l3 = new ArrayList<>(l2);//使用l2构造
            System.out.println(l3);
        }

在这里插入图片描述

  • 如上代码,我们使用了其他ArrayList对象l2作为参数构造了l3,即将l2的所有元素作为l3初始数据

当然我们可以定义List接口的引用接收ArrayList对象:

            List<Integer> list = new ArrayList<>();
  • 优点:向上转型,从而可以实现多态
  • 缺点:无法调用ArrayList特有的方法,只能调用ArrayList中重写List接口的方法

ArrayList的常用方法

ArrayList类中的常用方法与上文列举的List接口中常用方法一致,这里仅给出演示代码,一些小的注意问题在注释给出:

    public static void main(String[] args) {
        ArrayList<Integer> list1 = new ArrayList<>();
        ArrayList<Integer> list2 = new ArrayList<>();

        list1.add(1);
        list1.add(2);
        list1.add(3);
        list1.add(4);
        list1.add(5);
        list1.remove(2);//删除下标为2位置的元素
        list1.remove(Integer.valueOf(5));//删除值为5的元素
        System.out.println(list1);//打印结果为[1, 2, 4]
        list2.add(6);
        list2.add(7);
        list2.add(8);
        list1.addAll(list2);//将list2中的所有元素尾插到list1中
        System.out.println(list1);//打印结果为[1, 2, 4, 6, 7, 8]
        list1.set(0, 100);//将0下标位置设置为100
        System.out.println(list1.get(0));//打印0下标位置的元素100
//        list1.add(100, 100);   error,报错!因为顺序表只允许连续存储,不允许跳跃式存储
        System.out.println(list1.contains(100));//100在表中,打印true
        List<Integer> list3 = list1.subList(0, 3);//截取[0, 3)下标位置的元素,返回一个List<Integer>的对象,用list3接收
        System.out.println(list3);//打印结果是[100, 2, 4]
    }

ArrayList的遍历

前面的演示代码中我们使用System.out.println(对象的引用);的方式打印表中的数据,这是因为ArrayList类中重写了toString方法

现在我们介绍三种ArrayList的遍历方法:forfor-each以及迭代器

直接看代码:

   public static void main(String[] args) {
        ArrayList<Integer> list = new ArrayList<>();
        list.add(1);
        list.add(2);
        list.add(3);
        list.add(4);
        list.add(5);

        //for循环 + 下标遍历
        System.out.println("=====for循环=====");
        for (int i = 0; i < list.size(); i++) {
            System.out.print(list.get(i) + " ");
        }
        System.out.println();

        //for-each循环
        System.out.println("=====for-each循环=====");
        for (int x : list) {
            System.out.print(x + " ");
        }
        System.out.println();
        for(Integer x : list) {
            System.out.print(x + " ");
        }
        System.out.println();

        //使用迭代器
        System.out.println("=====使用迭代器=====");
        Iterator<Integer> it = list.iterator();
        while(it.hasNext()) {
            System.out.print(it.next() + " ");
        }
        System.out.println();

        ListIterator<Integer> lIt = list.listIterator();
        while(lIt.hasNext()) {
            System.out.print(lIt.next() + " ");
        }
        System.out.println();

        System.out.println("=====逆序输出=====");
        ListIterator<Integer> lIt1 = list.listIterator(list.size());
        while(lIt1.hasPrevious()) {
            System.out.print(lIt1.previous() + " ");
        }
    }

在这里插入图片描述

  • for循环:利用方法size()get()即可
  • for-each:以整型为例,:左边可以是Integer包装类也可以是int(自动拆箱)
  • 迭代器IteratorListIterator都是接口,ListIterator接口继承了Iterator接口,使用时调用指定方法即可
  • 对于拓展的逆序输出,ListIterator可以做到,但是Iterator没有相关的方法

ArrayList的扩容机制

抛出几个问题:使用ArrayList的空构造方法实例化的顺序表初始容量是多少?向顺序表中添加数据时,如果表满了,怎么扩容?

对于这几个问题,我们要观察ArrayList的源码:

在这里插入图片描述

对于ArrayList实现的接口以及表示的含义前面已经介绍过了,直接看定义的成员变量:

  • serialVersionUID是Java序列化机制中的一个特殊字段,用于标识类的版本

  • DEFAULT_CAPACITY是默认的初始化容量,为10

  • EMPTY_ELEMENTDATADEFAULTCAPACITY_EMPTY_ELEMENTDATA

    共同点

    • 它们都是用来表示空ArrayList实例的静态final常量。
    • 它们都是Object类型的数组。
    • 它们都被声明为private,只在ArrayList内部使用。

    区别

    • EMPTY_ELEMENTDATA是一个真正的空数组(长度为0),而DEFAULTCAPACITY_EMPTY_ELEMENTDATA的长度等于DEFAULT_CAPACITY
    • 当添加第一个元素时,如果使用的是EMPTY_ELEMENTDATA,则需要创建一个新的数组并设置其大小为DEFAULT_CAPACITY;如果使用的是DEFAULTCAPACITY_EMPTY_ELEMENTDATA,则可以直接在原数组上进行调整。
    • EMPTY_ELEMENTDATA用于普通的空ArrayList实例,而DEFAULTCAPACITY_EMPTY_ELEMENTDATA用于那些初始容量已经设置为默认容量的空ArrayList实例。
  • elementData:即顺序表,存储顺序表的元素

  • size:当前顺序表的有效数据数量


接着我们看一下三个构造方法:

在这里插入图片描述

  • 初始化顺序表容量的构造方法:当传入的初始化容量为0时,将EMPTY_ELEMENTDATA赋值给顺序表elementData
  • 无参构造方法:无参构造方法初始容量其实设置为了默认容量,所以赋值DEFAULTCAPACITY_EMPTY_ELEMENTDATA
  • 利用其他Collection的构造方法:先将集合c转化为Object类型的数组,如果数组不为空,判断c的类是否为ArrayList,为真,直接赋值;为假,则通过copyOf创建新数组并拷贝,用新数组赋值;如果数组为空,直接赋值EMPTY_ELEMENTDATA

到这里,我们解决了第一个问题并且拓展了一些,并考虑一个新的问题,这个问题将和最初的问题2一并解决:空构造方法实例化的表是个空数组,那么添加时怎么操作的?


add()方法入手:

在这里插入图片描述

如图第二个add调用了第一个add方法,而第一个和第三个add在表满时,均调用了一个grow()方法

在这里插入图片描述

然后,无参grow又调用了带有参数的grow方法

在这里插入图片描述

首先,将当前容量赋值给oldCapacity,如果if语句判断为假(oldCapacity <= 0 && elementData == DEFAULTCAPACITY_EMPTY_ELEMRNTDATA),执行else语句,即:当添加第一个元素时,如果使用的是无参构造方法,ArrayList会将内部数组的容量从0扩展到10,并赋值;如果if语句判断为真,则调用ArraysSupport.newLength方法计算新容量newCapacity,传入的第一个参数是旧容量,第二个参数是,增长的容量,即带参数的grow的参数与旧容量的差值(由于参数为size + 1,所以这里是1),第三个参数是旧容量的一半。

在这里插入图片描述

newLength方法接受三个参数:oldLength(旧数组长度)、minGrowth(最小增长量)和prefGrowth(首选增长量)。它首先计算首选长度prefLength,即旧数组长度加上minGrowthprefGrowth中的较大值。然后,如果首选长度在0到SOFT_MAX_ARRAY_LENGTH(数组的最大长度限制)之间,就返回这个首选长度;否则,调用hugeLength方法来处理较大的长度情况。

hugeLength方法也接受两个参数:oldLengthminGrowth。它首先计算最小所需长度minLength,即旧数组长度加上最小增长量。然后,如果minLength小于0(表示溢出),就抛出一个OutOfMemoryError异常,提示所需的数组长度过大。如果minLength小于等于SOFT_MAX_ARRAY_LENGTH,则返回SOFT_MAX_ARRAY_LENGTH;否则,返回minLength


【总结】

回归到最初的grow方法:

  1. 检测是否真正需要扩容,如果是调用grow准备扩容
  2. 预估需要库容的大小 初步预估按照1.5倍大小扩容 如果用户所需大小超过预估1.5倍大小,则按照用户所=需大小扩容,真正扩容之前检测是否能扩容成功,防止太大导致扩容失败
  3. 使用copyOf进行扩容
  4. 另外,如果使用的是无参构造方法,当添加第一个元素时,ArrayList会将内部数组的容量从0扩展到10,此后按照上3步

ArrayList的模拟实现

模拟实现一个ArrayList类是比较简单的,只需要掌握对数组的增删查改即可。

实现方式多种多样,能实现增删查改等业务即可,这里给出模拟实现的代码,感兴趣的可以看一下:

import java.util.Arrays;

public class MyArrayList {
    public int[] elem;
    public int capacity;
    public int usedSize;//0
    //默认容量
    private static final int DEFAULT_SIZE = 10;

    public MyArrayList() {
        this.elem = new int[DEFAULT_SIZE];
        this.capacity = 10;
    }

    /**
     * 打印顺序表:
     *   根据usedSize判断即可
     */
    public void display() {
        if(this.isEmpty()) {
            return;
        }
        for(int i = 0; i < this.usedSize; i++) {
            System.out.print(elem[i] + " ");
        }
        System.out.println();
    }

//     新增元素,默认在数组最后新增
    public void add(int data) {
        if(isFull()) {
            this.elem = Arrays.copyOf(this.elem, 2 * this.capacity);
        }
        this.elem[this.usedSize] = data;
        this.usedSize++;
    }

    /**
     * 判断当前的顺序表是不是满的!
     * @return true:满   false代表空
     */
    private boolean isFull() {
        if(this.usedSize == this.capacity) {
            return true;
        }else {
            return false;
        }
    }


    private boolean checkPosInAdd(int pos) {
        //在0位置添加可以,同时在最后位置也可以添加,但是不可以跳着增加
        if(pos >= 0 && pos <= this.usedSize) {
            return true;
        }else {
            throw new PosIllegalException("位置不合法!");
        }
    }

    // 在 pos 位置新增元素
    public void add(int pos, int data) throws PosIllegalException {
        if(isFull()) {
            this.elem = Arrays.copyOf(this.elem, 2 * this.capacity);
        }
        try{
            checkPosInAdd(pos);
            for(int i = this.usedSize; i > pos; i--) {
                this.elem[i] = this.elem[i - 1];
            }
            this.elem[pos] = data;
        }catch (PosIllegalException e) {
            System.out.println("输入的位置不合法!插入失败");
            e.printStackTrace();
        }
    }

    // 判定是否包含某个元素
    public boolean contains(int toFind) {
        if(isEmpty()) {
            return false;
        }
        for(int i = 0; i < this.usedSize; i++) {
            if(toFind == this.elem[i]) {
                return true;
            }
        }
        return false;
    }
    // 查找某个元素对应的位置
    public int indexOf(int toFind) {
        if(isEmpty()) {
            System.out.println("表为空!");
            return -1;
        }
        for (int i = 0; i < this.usedSize; i++) {
            if(toFind == this.elem[i]) {
                return i;
            }
        }
        return -1;
    }

    // 获取 pos 位置的元素
    public int get(int pos) throws PosIllegalException {
        if(isEmpty()) {
            System.out.println("表为空!返回值无效!");
            return -1;
        }
        try {
            checkPosInAdd(pos);
            return this.elem[pos];
        }catch (PosIllegalException e) {
            System.out.println("查找的位置不合法,返回值无效!");
            e.printStackTrace();
        }
        return -1;
    }

    private boolean isEmpty() {
        if(this.usedSize == 0) {
            return true;
        }else {
            return false;
        }
    }
    // 给 pos 位置的元素设为【更新为】 value
    public void set(int pos, int value) {
        if(isEmpty()) {
            System.out.println("表为空!");
            return;
        }else {
            try {
                checkPosInAdd(pos);
                this.elem[pos] = value;
            }catch (PosIllegalException e) {
                System.out.println("位置不合法!");
                e.printStackTrace();
            }
        }
    }

    /**
     * 删除第一次出现的关键字key
     * @param key
     */
    public void remove(int key) {
        if(isEmpty()) {
            System.out.println("表为空");
        }
        for(int i = 0; i < this.usedSize; i++) {
            if(this.elem[i] == key) {
                for(int j = i; j < this.usedSize - 1; j++) {
                    this.elem[j] = this.elem[j + 1];
                }
                this.usedSize--;
                return;
            }
        }
        System.out.println("没有找到关键字!");
        return;
    }

    // 获取顺序表长度
    public int size() {
        return this.usedSize;
    }

    // 清空顺序表
    public void clear() {
        this.usedSize = 0;
    }
}
public class PosIllegalException extends RuntimeException {
    public PosIllegalException() {

    }

    public PosIllegalException(String mes) {
        super(mes);
    }
}

ArrayList的相关练习

给定一个非负整数 numRows 生成「杨辉三角」的前 numRows 行。

class Solution {
    public List<List<Integer>> generate(int numRows) {
        //补充代码
    }
}

杨辉三角大家应该不陌生,如下:

1
1 1
1 2 1
1 3 3 1
… …

题目最大的特点是返回值类型:List<List<Integer>>,即返回一个实现了List接口的集合,其每一个元素也是这个集合的类型,这意味着集合中的每个元素代表杨辉三角的一行,每个元素又是一个集合,这个集合的每个元素是杨辉三角每一行的每个元素。我们刚刚学习了ArrayList,所以我们会使用ArrayList完成。

杨辉三角的第一行一定是一个1,每一行的第一个和最后一个元素一定是1,基于这一点,我们直接实现:

class Solution {
    public List<List<Integer>> generate(int numRows) {
        ArrayList<List<Integer>> list = new ArrayList<>(numRows);
        //第一行的1
        ArrayList<Integer> l1 = new ArrayList<>();
        l1.add(1);
        list.add(l1);
        //后续行
        for(int i = 1; i < numRows; i++) {
            ArrayList<Integer> lTmp = new ArrayList<>();
            lTmp.add(1);//第一个元素一定是1
            for(int j = 1; j < i; j++) {
                lTmp.add(list.get(i - 1).get(j - 1) + list.get(i - 1).get(j));
            }//每一行的中间元素
            lTmp.add(1);//最后一个元素一定是1
            list.add(lTmp);
        }
        return list;
    }
}

可能会出现以下报错(也是裤儿出现的问题):

Line 16: error: incompatible types: ArrayList<ArrayList<Integer>> cannot be converted to List<List<Integer>> return list;

因为:

ArrayList<ArrayList>ArrayList<List>之间的类型不兼容,不能直接进行互相转换。

在Java泛型中,类型擦除会导致泛型类型的具体信息在编译时被擦除,因此在运行时,泛型类型的实例并不知道它们的类型参数的具体类型。这意味着,尽管ArrayListList的子类,但在泛型类型中,它们被视为不同的类型。

原题链接:118. 杨辉三角 - 力扣(LeetCode)


  • 14
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值