ArrayList与顺序表【附ArrayList扩容机制源码分析】

目录

一.线性表

 二、顺序表

2.1 简单模拟顺序表的实现

三、ArrayList简介

3.1 ArrayList的扩容机制(附源码分析)

四、使用示例:

4.1扑克牌

4.2.杨辉三角


一.线性表

线性表(linear list)是n个具有相同特性的数据元素的有限序列。 线性表是一种在实际中广泛使用的数据结构,常见的线性表:顺序表、链表、栈、队列...
线性表在逻辑上是线性结构,也就说是连续的一条直线。但是在物理结构上并不一定是连续的,线性表在物理上存储时,通常以数组和链式结构的形式存储。
ArrayList与顺序表【附ArrayList扩容机制源码分析】_第1张图片

 二、顺序表

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

2.1 简单模拟顺序表的实现

import java.util.ArrayList;
import java.util.Arrays;

public class MyArrayList {
    public int elem[];

    //记录当前有多少元素存入数组之中
    public int usedSize;
    public int Default_Capacity = 10;

    // 利用构造方法更好的完成对数组的初始化
    public MyArrayList(){
        this.elem = new int[Default_Capacity];
    }

    // 打印顺序表
    public void display() {
        for (int i = 0; i < this.usedSize ; i++) {
            System.out.print(elem[i]+" ");
        }
        System.out.println();
    }
    // 新增元素,默认在数组最后新增
    public void add(int data) {
        //在新增之前需要判断数组是否满
        if(isFull(elem)){
            this.elem = Arrays.copyOf(this.elem,2*elem.length);
        }
        elem[usedSize] = data;
        usedSize++;
    }
    public boolean isFull(int elem[]){
        return usedSize == elem.length;
    }
    private void checkAddPos(int pos){
        if(pos < 0 || pos > usedSize){
            throw new PosIndexNotLegalException("pos下标不合法");
        }
    }
    // 在 pos 位置新增元素
    public void add(int pos, int data) {
        //在新增元素之前,需要检查pos位置是否合法以及数组是否满
        try {
            checkAddPos(pos);
            if (isFull(elem)){
                elem = Arrays.copyOf(elem,2*elem.length);
            }
            for (int i = usedSize-1; i >= pos ; i--) {
                elem[i+1] = elem[i];
            }
            elem[pos] = data;
            usedSize++;

        }catch (PosIndexNotLegalException e){
            e.printStackTrace();
        }
    }
// 判定是否包含某个元素
    public boolean contains(int toFind) {
        for (int i = 0; i < usedSize; i++) {
            if (toFind == elem[i]){
                return true;
            }
        }
        return false;
    }
    // 查找某个元素对应的位置
    public int indexOf(int toFind) {
        for (int i = 0; i < usedSize ; i++) {
            if (toFind == elem[i]){
                return i;
            }
        }
        return -1;
    }
    public void checkPos(int pos){
       if(pos < 0 || pos >=usedSize ){
           throw new PosIndexNotLegalException("下标不合法");
       }
    }
    // 获取 pos 位置的元素
    public int get(int pos) {
        //同样在获取pos下标之前,需要判断下标是否合法
        checkPos(pos);
        return elem[pos];
    }
    // 给 pos 位置的元素设为 value
    public void set(int pos, int value) {
        checkPos(pos);
        elem[pos] = value;
    }
    //删除第一次出现的关键字key
    public void remove(int toRemove) {
        int index = indexOf(toRemove);
        if(index == -1){
            System.out.println("没有你要删除的数字");
            return;
        }
        for (int i = index; i < usedSize-1; i++) {
            elem[i] = elem[i+1];
        }
        usedSize--;
    }
    // 获取顺序表长度
    public int size() {
        return usedSize;
    }
    // 清空顺序表
    public void clear() {
        usedSize = 0;
    }
}

三、ArrayList简介

 在集合框架中,ArrayList是一个普通的类,实现了List接口,具体框架图如下:

ArrayList与顺序表【附ArrayList扩容机制源码分析】_第2张图片

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

3.1 ArrayList的扩容机制(附源码分析)

示例:

ArrayList与顺序表【附ArrayList扩容机制源码分析】_第3张图片

下面我们通过查看源码的方式来探究一下ArrayList的扩容机制:

 我们先来看看ArrayList无参的构造方法:

再来看看elementData和DEFAULTCAPACITY_EMPTY_ELEMENTDATA是什么:

 可以得知:在调用ArrayList构造方法(无参)创建对象时,此时给数组分配的空间大小为0.

✨✨

小插曲:或许有人会对EMPTY_ELEMENTDATA 和DEFAULTCAPACITY_EMPTY_ELEMENTDATA两个的存在感到疑惑,以下将通过原码来解答这一问题:

ArrayList与顺序表【附ArrayList扩容机制源码分析】_第4张图片

ArrayList与顺序表【附ArrayList扩容机制源码分析】_第5张图片

 分析:虽然这两个Object类的数组都被static final修饰,但是通过观察我们发现,两者的主要功能是区分,区分调用了哪个构造方法,调用无参的构造方法则是使用DEFAULTCAPACITY_EMPTY_ELEMENTDATA,而当调用有参的构造方法时,当传入的值为0时,就使用EMPTY_ELEMENTDATA。

✨✨

回到上一步,我们通过源码发现在默认容量其实为10

ArrayList与顺序表【附ArrayList扩容机制源码分析】_第6张图片

但是为什么显示的是0呢?下面我们通过查看add方法来进一步观察:

ArrayList与顺序表【附ArrayList扩容机制源码分析】_第7张图片

 因为是第一次实例化对象(调用无参构造方法),所以Size为0,即1作为参数(minCapacity)传入ensureCapacityInternal中:

 在ensureCapacityInternal中先调用了CalculateCapacity方法:

ArrayList与顺序表【附ArrayList扩容机制源码分析】_第8张图片

 我们发现:if(.....)中的语句与无参的构造方法中的语句一致

ArrayList与顺序表【附ArrayList扩容机制源码分析】_第9张图片

 于是返回DEFAULT_CAPACITY(10)和minCapacity(1)中的最大值,即返回10.

而10又作为ensureExplicitCapacity的参数,我们再来观察ensureExplicitCapacity的源码:

ArrayList与顺序表【附ArrayList扩容机制源码分析】_第10张图片

 minCapacity为10,而前面我们得知,elementData为空(长度为0),于是条件成立,执行grow方法:

ArrayList与顺序表【附ArrayList扩容机制源码分析】_第11张图片

分析: minCapacity为10,oldCapacity为0,而newCapacity = 0 + (0/2)= 0.

第一个if(...)语句中 newCapacity-minCapacity = 0 -10 < 0.

条件成立,于是newCapacity被赋值为10,

第二个if(...)语句中是让newCapacity-MAX_ARRAY_SIZE(可分配数组最大的大小),而MAX_ARRAY_SIZE为:ArrayList与顺序表【附ArrayList扩容机制源码分析】_第12张图片

显然条件不成立。 

于是elemData进行扩容(为10),这里也印证了为什么之前源码中DEFAULT_CAPACITY为10的原因。

因此我们可以得出结论,当创建一个ArrayLIst(调用无参构造)类时,被分配的数组大小为0,而使用add方法时,数组大小被扩容为10.

那大家一定很好奇,当10个元素都被放满的时候,该怎么处理?

这时add方法中的Size为10,minCapacity为11。 

ArrayList与顺序表【附ArrayList扩容机制源码分析】_第13张图片

 这时if(...)条件即不成立,calculateCapacity返回11

ArrayList与顺序表【附ArrayList扩容机制源码分析】_第14张图片

 minCapacity - elementData.length = 11- 10 > 0 

ArrayList与顺序表【附ArrayList扩容机制源码分析】_第15张图片

 进入grow方法:

ArrayList与顺序表【附ArrayList扩容机制源码分析】_第16张图片

 oldCapacity为10,newCapacity为10+10>>1 = 15.

于是我们得出结论:扩容为1.5倍的扩容

四、使用示例:

4.1扑克牌

import java.util.ArrayList;
import java.util.List;
import java.util.Random;


class Card{
    public int rank;
    public String suit;
    public Card(int rank,String suit){
        this.rank = rank;
        this.suit = suit;
    }

    @Override
    public String toString() {
        return " "+suit+" "+rank;
    }
}
public class CardDemo {
    public static final String[] suit = {"♥","♣","♦","♠"};

    public List buyDeskCard() {
        List cards = new ArrayList<>();
        for (int i = 1; i <= 13 ; i++) {
            for (int j = 0; j < 4; j++) {
                Card card = new Card(i,suit[j]);
                cards.add(card);
            }
        }
        return cards;

    }
    public void shuffle(List cards) {
        for (int i = cards.size()-1; i > 0 ; i--) {
            Random random = new Random();
            int index = random.nextInt(i);
            swap(cards,index,i);
        }
    }
    public void swap(List cards,int i,int j){
        Card tmp = cards.get(i);
        cards.set(i,cards.get(j));
        cards.set(j,tmp);
    }
    public List> deal(List cards){
        List hand1 = new ArrayList<>();
        List hand2 = new ArrayList<>();
        List hand3 = new ArrayList<>();

        List> hands = new ArrayList<>();
        hands.add(hand1);
        hands.add(hand2);
        hands.add(hand3);

        for (int i = 0; i < 5; i++) {
            for (int j = 0; j < 3; j++) {
                Card card = cards.remove(0);
                hands.get(j).add(card);
            }

        }
        return hands;

    }

}

测试代码:

import java.util.List;


public class Test2 {
    public static void main(String[] args) {
        CardDemo cardDemo = new CardDemo();
        List ret = cardDemo.buyDeskCard();
        cardDemo.shuffle(ret);
        System.out.println(ret);
        System.out.println("发牌");
        List> ret2 = cardDemo.deal(ret);
        for (int i = 0; i < ret2.size(); i++) {
            System.out.println("第"+i+"个人的牌: "+ret2.get(i));
        }
    }
}

运行结果:

4.2.杨辉三角

class Solution {
    public List> generate(int numRows) {
        //利用二维数组的思维解题,先创建两个顺序表
        List> ret = new ArrayList<>();
        List list = new ArrayList<>();
        //先将第一层构造完成
        list.add(1);
        ret.add(list);
        //构造后面层
        for(int i = 1;i < numRows;i++){
            List curRow = new ArrayList<>();//当前层
            curRow.add(1);
            List prevRow = ret.get(i-1);//前一层
           
            for(int j = 1;j < i;j++){
                int num = prevRow.get(j)+prevRow.get(j-1);
                curRow.add(num);
            }
         //在每层的其余元素构造完之后(除每行第一个和最后一个元素),将最后一个元素填入。
            curRow.add(1);
            ret.add(curRow);
        }
        return ret;
        
    }
}

该示例来自力扣——杨辉三角。 

总结:

当然顺序表也有诸多不是很好的地方:

1. 顺序表中间/头部的插入删除,时间复杂度为O(N)
2. 增容需要申请新空间,拷贝数据,释放旧空间。会有不小的消耗。
3. 增容一般是呈2倍的增长,势必会有一定的空间浪费。例如当前容量为1000,满了以后增容到2000,我们再继续插入了5个数据,后面没有数据插入了,那么就浪费了995个数据空间。

因此我们引入链表的概念来解决这类问题。

你可能感兴趣的:(数据结构,链表)