《恋上数据结构与算法》第1季:动态数组原理实现(图文并茂,一文带你了解ArrayList底层实现)

news2025/7/17 20:13:50

动态数组原理实现

  • 一、数组(Array)
  • 二、动态数组
  • 三、动态数组的设计
  • 四、动态数组的实现
    • 1. 添加元素
    • 2. 数组扩容
    • 3. 删除元素
    • 4. 数组缩容
    • 5. 清空元素
    • 6. 修改元素
    • 7. 查询元素
    • 8. 插入元素
    • 9. 查看元素位置
    • 10. 是否包含某个元素
    • 11. 元素的数量
    • 12. 数组是否为空
    • 13. 动态数组打印
  • 附上动态数组源码

一、数组(Array)

  • 数组是一种顺序存储的线性表,所有元素的内存地址都是连续的。
int[] array = new int[]{11,22,33};

在这里插入图片描述

在很多编程语言中,数组有个致命的缺点,无法动态修改容量;
实际开发中,我们希望数组的容量是动态变化的。

二、动态数组

  1. 可以通过数组实现一个动态数组,动态数组的容量是动态变化的
  2. 可以对动态数组进行增删改查等操作
// 元素的数量
int size(); 
// 是否为空
boolean isEmpty();
// 是否包含某个元素
boolean contains(E element); 
// 添加元素到最后面
void add(E element); 
// 返回index位置对应的元素
E get(int index); 
// 设置index位置的元素
E set(int index, E element); 
// 往index位置添加元素
void add(int index, E element); 
// 删除index位置对应的元素 
E remove(int index); 
// 查看元素的位置
int indexOf(E element); 
// 清除所有元素
void clear(); 

三、动态数组的设计

  • 创建类 ArrayList 如下图,创建 size 属性来管理数组中元素的个数,创建 elements 属性来管理存取的数据
    在这里插入图片描述
public class ArrayList<E> {
	private int size;
	private E[] elements;
}
  • 添加初始化方法,创建 elements 数组,并指定 elements 默认的容量
public class ArrayList<E> {
	private int size;
	private E[] elements;
	// 设置elements数组默认的初始化空间
	private static final int CAPACITY_DEFAULT = 10;
	
	public ArrayList(int capacity) {
		capacity = capacity < CAPACITY_DEFAULT ? CAPACITY_DEFAULT : capacity;
		elements = (E[]) new Object[capacity];
	}
	// 默认情况
	public ArrayList() {
		this(CAPACITY_DEFAULT);
	}
}

四、动态数组的实现

1. 添加元素

  1. 我们知道,每当数组添加新元素时,都会在数组最后一个元素的后面添加新元素;
  2. 这个 新元素需要添加到的索引 等于 当前数组元素的个数,在ArrayList 中 size 属性就是 当前数组元素的个数,所以就是将新元素添加到数组的 size 位置上,然后 size1
    在这里插入图片描述
public void add(E element) {
	elements[size] = element;
	size++;
}

2. 数组扩容

  1. 由于数组elements最大的容量只有10, 所以当数组存满元素时, 就需要对数组进行扩容
  2. 因为数组是无法动态扩容的,所以需要创建一个新的数组,这个数组的容量要比之前数组的容量大
  3. 然后在将原数组中的元素存放到新数组中, 这样就实现了数组的扩容
    在这里插入图片描述
// 扩容
private void ensureCapacity(){
    // 获取数组当前容量(长度)
    int oldLength = elements.length;
    // 如果 当前存储的元素个数(size) < 当前数组容量,直接返回
    if(size < oldLength){
        return;
    }
    // 新数组的容量为原数组容量的1.5倍
    int newCapacity = oldLength + (oldLength >> 1);
    // 创建新数组
    E[] newElements = (E[]) new Object[newCapacity];
    //原数组中的元素存储到新数组中
    for(int i = 0; i < size; i++){
        newElements[i] = elements[i];
    }
    // 引用新数组(新数组的索引要指向旧数组的索引)
    elements = newElements;
}
  1. 此时,add 方法的实现如下,在添加新元素之前,先判断是否需要扩容
public void add(E element){
    // 添加新元素之前,先判断是否需要扩容
    ensureCapacity();
    elements[size] = element;
    size++;
}

3. 删除元素

  1. 删除元素,实际上就是去掉 指定位置的元素,并将后面的元素 向前移动
  2. 如下图,当删除索引为 2 的元素,只需要将后面的元素向前面移动,然后去掉最后一个元素,size1 即可。
    在这里插入图片描述
public E remove(int index){
    // 取出需要删除的元素
    E element = elements[index];
    // 通过循环,将index后面的所有元素向前移动一位
    for(int i = index; i < size - 1; i++){
        elements[i] = elements[i+1];
    }
    // 删除最后一个元素
    elements[--size] = null;
    // 将删除的元素返回
    return element;
}
  • 注意:删除元素时传入的索引不能越界,即 不能小于0,也不能大于等于size
  • 所以,我们在删除元素之前需要先进行索引检查
private void rangCheck(int index){
    if(index < 0 || index >= size){
        throw new IndexOutOfBoundsException("Index:" + index + ", Size:" + size);
    }
}
  1. 此时,remove方法实现如下
public E remove(int index){
    // 判断索引是否越界
    rangCheck(index);
    // 取出需要删除的元素
    E element = elements[index];
    // 通过循环,将index后面的所有元素向前移动一位
    for(int i = index; i < size - 1; i++){
        elements[i] = elements[i+1];
    }
    // 删除最后一个元素
    elements[--size] = null;
    // 将删除的元素返回
    return element;
}

4. 数组缩容

  1. 当数组中的元素删除后,数组剩余的空间可能会很大,这样就会造成内存的浪费
  2. 所以,当数组中元素删除后,我们需要对数组进行缩容
  3. 实现方法类似于扩容,当数组中容量小于某个值时,创建新的数组,然后将原有的数组中的元素存入新数组即可
// 缩容
private void trim(){
    // 获取当前数组的容量(长度)
    int capacity = elements.length;
    // 当size大于等于当前容量的一半,或者当前容量已经小于默认容量(10)时,就直接返回
    if (size >= capacity >> 1 || capacity < CAPACITY_DEFAULT){
        return;
    }
    // 计算新的容量 = 原来容量的一半
    int newCapacity = capacity >> 1;
    // 创建新数组
    E[] newElements = (E[]) new Object[newCapacity];
    // 将原数组元素存入新数组
    for(int i = 0; i < size; i++){
        newElements[i] = elements[i];
    }
    // 引用新数组(新数组的索引要指向旧数组的索引)
    elements = newElements;
}
  • 每次删除元素后,判断是否需要缩容
public E remove(int index){
    // 判断索引是否越界
    rangCheck(index);
    // 取出需要删除的元素
    E element = elements[index];
    // 通过循环,将index后面的所有元素向前移动一位
    for(int i = index; i < size - 1; i++){
        elements[i] = elements[i+1];
    }
    // 删除最后一个元素
    elements[--size] = null;
    // 判断数组是否需要缩容
    trim();
    // 将删除的元素返回
    return element;
}

5. 清空元素

  • 清空元素,只需要将所有的元素置为 null,然后 size 置为 0
public void clear(){
    // 清空存储的数据
    for(int i = 0; i < size; i++){
        elements[i] = null;
    }
    // 将size置为0
    size = 0;
}

6. 修改元素

  • 修改元素时,只需要将原有位置的元素替换掉即可,只是需要注意一下索引是否越界
public E set(int index, E element){
    // 判断索引是否越界
    rangCheck(index);
    // 取出被替代元素
    E oldElement = elements[index];
    // 替换新元素
    elements[index] = element;
    // 返回被替换的元素
    return oldElement;
}

7. 查询元素

  • 查询元素,只需要将指定索引的元素返回,注意判断索引是否越界即可
public E get(int index){
    rangCheck(index);
    return elements[index];
}

8. 插入元素

  1. 插入元素类似于删除元素,只需要将插入位置后面的元素向后移动即可
  2. 注意:需要从后向前移动元素,如果从前向后移动元素,那么会进行元素覆盖,最后出错
    在这里插入图片描述
public void add(int index, E element){
    // 从后向前的顺序,将元素向后移动
    for(int i = size - 1; i >= index; i++){
        elements[i + 1] = elements[i];
    }
    // 插入元素
    elements[index] = element;
    // size+1
    size++;
}
  1. 需要注意的是,插入元素的索引也不能越界,不过不同于删除和设置元素时,插入的位置可以是所有元素的最后,即size的位置
private void rangCheckForAdd(int index){
    // 当索引小于0 或 大于 size时,抛出异常
    if(index < 0 || index > size){
        throw new IndexOutOfBoundsException("Index:" + index + ", Size:" + size);
    }
}
  • 此时,add 方法如下
public void add(int index, E element){
    // 检查索引是否越界
    rangCheckForAdd(index);
    // 从后向前的顺序,将元素向后移动
    for(int i = size - 1; i >= index; i++){
        elements[i + 1] = elements[i];
    }
    // 插入元素
    elements[index] = element;
    // size+1
    size++;
}

9. 查看元素位置

  1. 可以通过循环,查找元素在数组中的位置
  2. 注意:数组中可以存储null,而null是不能调用equals方法的,所以需要对传入的元素进行判断,如果查找的元素是null,需要单独处理
  3. 当元素存在时,返回索引,否则返回变量 ELEMENT_NOT_FOUND 的值
public int indexOf(E element){
    if(element == null){
        for(int i = 0; i < size; i--){
            if(elements[i] == null){
                return i;
            }
        }
    }else{
        for(int i = 0; i < size; i++){
            if(element.equals(elements[i])){
                return i;
            }
        }
    }
    return ELEMENT_NOT_FOUND;
}

10. 是否包含某个元素

  • 当元素存在时,只需要判断索引是否等于 ELEMENT_NOT_FOUND 即可
public boolean contains(E element){
    // 查看元素的索引是否为ELEMENT_NOT_FOUND
    return indexOf(element) != ELEMENT_NOT_FOUND;
}

11. 元素的数量

  • 数组元素的数量,就是size的值
public int size(){
    return size;
}

12. 数组是否为空

  • 判断 size 的值是否为 0 即可
public boolean isEmpty(){
    return size == 0;
}

13. 动态数组打印

  • 可以重写 toString 方法,来打印 ArrayList 中的元素
@Override
public String toString() {
    StringBuilder string = new StringBuilder();
    string.append("size= ").append(size).append(", [");
    for(int i = 0; i < size; i ++){
        if(i != 0){
            string.append(",");
        }
        string.append(elements[i]);
    }
    string.append("]");
    return string.toString();
}

附上动态数组源码

package com.aimoyudett;

/**
 * @Author: 爱摸鱼的TT~
 * @Description: 自己实现ArrayList接口类
 * @Date Created in 2022-11-18 21:40
 * @Modified By:
 */
public class ArrayList<E> {

    private int size;// 元素的个数
    private E[] elements;// 数组对象
    // 设置elements数组默认的初始化空间
    private static final int CAPACITY_DEFAULT = 10;
    private static final int ELEMENT_NOT_FOUND = -1;

    // 无参构造方法
    public ArrayList(){
        this(CAPACITY_DEFAULT);
    }

    // 有参构造方法
    public ArrayList(int capacity){
        capacity = capacity < CAPACITY_DEFAULT ? CAPACITY_DEFAULT : capacity;
        // 创建长度为capacity的数组
        elements = (E[]) new Object[capacity];
    }

    public void add(E element){
        // 添加新元素之前,先判断是否需要扩容
        ensureCapacity();
        elements[size] = element;
        size++;
    }

    public E remove(int index){
        // 判断索引是否越界
        rangCheck(index);
        // 取出需要删除的元素
        E element = elements[index];
        // 通过循环,将index后面的所有元素向前移动一位
        for(int i = index; i < size - 1; i++){
            elements[i] = elements[i+1];
        }
        // 删除最后一个元素
        elements[--size] = null;
        // 判断数组是否需要缩容
        trim();
        // 将删除的元素返回
        return element;
    }

    public void add(int index, E element){
        // 检查索引是否越界
        rangCheckForAdd(index);
        // 从后向前的顺序,将元素向后移动
        for(int i = size - 1; i >= index; i--){
            elements[i + 1] = elements[i];
        }
        // 插入元素
        elements[index] = element;
        // size+1
        size++;
    }

    public E get(int index){
        rangCheck(index);
        return elements[index];
    }

    public E set(int index, E element){
        // 判断索引是否越界
        rangCheck(index);
        // 取出被替代元素
        E oldElement = elements[index];
        // 替换新元素
        elements[index] = element;
        // 返回被替换的元素
        return oldElement;
    }

    public void clear(){
        // 清空存储的数据
        for(int i = 0; i < size; i++){
            elements[i] = null;
        }
        // 将size置为0
        size = 0;
    }

    public int indexOf(E element){
        if(element == null){
            for(int i = 0; i < size; i++){
                if(elements[i] == null){
                    return i;
                }
            }
        }else{
            for(int i = 0; i < size; i++){
                if(element.equals(elements[i])){
                    return i;
                }
            }
        }
        return ELEMENT_NOT_FOUND;
    }

    public boolean contains(E element){
        // 查看元素的索引是否为ELEMENT_NOT_FOUND
        return indexOf(element) != ELEMENT_NOT_FOUND;
    }

    public int size(){
        return size;
    }

    public boolean isEmpty(){
        return size == 0;
    }

    @Override
    public String toString() {
        StringBuilder string = new StringBuilder();
        string.append("size= ").append(size).append(", [");
        for(int i = 0; i < size; i ++){
            if(i != 0){
                string.append(",");
            }
            string.append(elements[i]);
        }
        string.append("]");
        return string.toString();
    }

    // 缩容
    private void trim(){
        // 获取当前数组的容量(长度)
        int capacity = elements.length;
        // 当size大于等于当前容量的一半,或者当前容量已经小于默认容量(10)时,就直接返回
        if (size >= capacity >> 1 || capacity < CAPACITY_DEFAULT){
            return;
        }
        // 计算新的容量 = 原来容量的一半
        int newCapacity = capacity >> 1;
        // 创建新数组
        E[] newElements = (E[]) new Object[newCapacity];
        // 将原数组元素存入新数组
        for(int i = 0; i < size; i++){
            newElements[i] = elements[i];
        }
        // 引用新数组(新数组的索引要指向旧数组的索引)
        elements = newElements;
    }

    private void rangCheckForAdd(int index){
        // 当索引小于0 或 大于 size时,抛出异常
        if(index < 0 || index > size){
            throw new IndexOutOfBoundsException("Index:" + index + ", Size:" + size);
        }
    }

    // 判断索引位置
    private void rangCheck(int index){
        if(index < 0 || index >= size){
            throw new IndexOutOfBoundsException("Index:" + index + ", Size:" + size);
        }
    }
    // 扩容
    private void ensureCapacity(){
        // 获取数组当前容量(长度)
        int oldLength = elements.length;
        // 如果 当前存储的元素个数(size) < 当前数组容量,直接返回
        if(size < oldLength){
            return;
        }
        // 新数组的容量为原数组容量的1.5倍
        int newCapacity = oldLength + (oldLength >> 1);
        // 创建新数组
        E[] newElements = (E[]) new Object[newCapacity];
        //原数组中的元素存储到新数组中
        for(int i = 0; i < size; i++){
            newElements[i] = elements[i];
        }
        // 引用新数组(新数组的索引要指向旧数组的索引)
        elements = newElements;
    }

    /*// 元素数量
    int size();

    // 是否为空
    boolean isEmpty();

    // 是否包含某个元素
    boolean contains(E element);

    // 添加元素到最后面
    void add(E element);

    // 返回index位置的元素
    E get(int index);

    // 设置index位置对应的元素
    E set(int index, E element);

    // 往index位置添加元素
    void add(int index, E element);

    // 删除index位置对应的元素
    E move(int index);

    // 查看元素的位置
    int indexOf(E element);

    // 清除所有元素
    void clear();*/
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/36323.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

win11的C/C++环境配置——基于MinGW-W64 GCC-8.1.0

首先给出MinGW-W64 GCC-8.1.0的下载地址&#xff1a;MinGW8.1.0 Win11下的C/C环境配置下载MinGW-W64 GCC-8.1.0添加bin文件和include文件到path变量中测试下载MinGW-W64 GCC-8.1.0 网页截图如下&#xff1a; 可以复制下载地址到迅雷中加速&#xff0c;下载完成后的文件如下&a…

MCE | “神药”二甲双胍后,糖尿病药物研究谁将是下一个顶流?

说到糖尿病药物&#xff0c;就不得不提一嘴“神药”二甲双胍&#xff0c;但除了二甲双胍&#xff0c;抗糖尿病药物的研究难道就没有点新玩意儿&#xff1f;当然有&#xff01; 糖尿病 (Diabetes) 是一种以高血糖为特征的慢性代谢病&#xff0c;是由于胰岛素分泌缺陷或者其生物…

美团闪购:闪电仓商户如狼似虎,传统商超便利店坐享其成?

近日&#xff0c;考研网红教师张雪峰一句“外卖员这个职业5-10年内可能会消失”再度登上热搜。 其实&#xff0c;他的这个推论&#xff0c;只是看到了目前外卖骑手的保有量&#xff0c;截至2021年&#xff0c;中国外卖骑手约1300万名。并没有看到炙手可热的“即时消费”新趋势&…

【Shell 脚本速成】05、Shell 运算详解

目录 一、赋值运算 二、算术运算[四则运算] 2.1 运算符与命令 2.2 整形运算 expr 命令&#xff1a;只能做整数运算&#xff0c;格式比较古板&#xff0c;运算符号两边注意空格 let命令&#xff1a;只能做整数运算&#xff0c;且运算元素必须是变量&#xff0c;无法直接对…

MySQL窗口函教-序号函数(row_number、rank、dense_rank)

MySQL窗门函教-序号函数&#xff08;row_number、rank、dense_rank&#xff09; 前言 mysql8.0中新增窗口函数&#xff08;开窗函数&#xff09; 窗口函数和普通聚合函数的区别 ①聚合函数是将多条记录聚合为一条&#xff1b;窗口函数是每条记录都会执行&#xff0c;有几条记…

代码源每日一题div1 区间和

区间和 - 题目 - Daimayuan Online Judge 题意&#xff1a; 思路&#xff1a; 根据前缀和的性质&#xff1a;当已知的前缀和区间是整个区间的划分时&#xff0c;才能求出整个区间的和 因为如果两个区间之间有交叉&#xff0c;交叉部分的和求不出来 因此&#xff0c;如果已知…

DeFi收益来源全面概述

去中心化金融一个主要的优势就是它对所有人开放&#xff0c;任何人在任何时间、任何地点都可以参与其中。这样一来&#xff0c;作为DeFi参与者就有机会获得在传统金融领域很难获得或根本不可能获得的收益。 加密货币的特性是开源的、无需许可的&#xff0c;这将DeFi变成了一个…

【Linux】进程创建/终止/等待/替换

目录 一、子进程的创建 1、fork函数的概念 2、如何理解fork拥有两个返回值 3、fork调用失败的场景 二、进程的终止 1、main函数返回值 1.1main函数的返回值的意义 1.2将错误码转化为错误信息 1.3查看进程的退出码 2、进程退出的情况 1、进程的正常退出与异常退出 2…

Principal branch

In mathematics, a principal branch is a function which selects one branch (“slice”) of a multi-valued function. Most often, this applies to functions defined on the complex plane. Contents1 Examples1.1 Trigonometric inverses1.2 Exponentiation to fraction…

255-261BFC,媒体的类型,媒体的特性,浏览器前缀,媒体查询,逻辑操作符,

◼ 有时候可能会看到有些CSS属性名前面带有:-o-、-xv-、-ms-、mso-、-moz-、-webkit- ◼ 官方文档专业术语叫做:vendor-specific extensions(供应商特定扩展) ◼ 为什么需要浏览器前缀了?  CSS属性刚开始并没有成为标准,浏览器为了防止后续会修改名字给新的属性添加了浏…

树莓派学习笔记(一)

树莓派学习笔记 笔记来自B站UP主【树小悉】的树莓派系列视频的听课笔记&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;适合新手入门&#xff0c;强烈推荐&#xff01;&#xff01;&#xff01; 关机命令 sudo poweroff 关闭电源sodo shutdown -h now 立刻关机sudp shut…

二、进程管理(四)经典同步互斥问题

目录 4.1生产者-消费者问题 4.1.1单类生产者-单类消费者问题 4.1.2多类生产者-多类消费者问题 4.1.3吸烟者问题 4.2读者-写者问题 4.3哲学家进餐问题 分析进程同步和互斥问题的三步&#xff1a; 关系分析&#xff1a;分析问题中的同步&#xff08;前驱关系&#xff09;、…

端口渗透篇:Java RMI 远程代码执行漏洞

转载https://cloud.tencent.com/developer/article/2149191 前言持续更新&#xff1a;整理下渗透测试工作中发现过的漏洞&#xff08;包含漏洞描述、漏洞等级、漏洞验证、修复建议&#xff09;&#xff0c;这里不深究漏洞产生的各种后利用或者绕过方式&#xff0c;漏洞验证过程…

【Python游戏】Python各大游戏合集(5):塔防游戏、飞机大战、连连看、打地鼠、记忆翻牌 | 附带源码

相关文件 关注小编&#xff0c;私信小编领取哟&#xff01; 当然别忘了一件三连哟~~ 公众号&#xff1a;Python日志 可以关注小编公众号&#xff0c;会不定时的发布一下Python小技巧&#xff0c;还有很多资源可以免费领取哟&#xff01;&#xff01; 源码领取&#xff1a;加P…

【高速数字化仪应用案例系列】虹科数字化仪在通信领域的应用

通信应用 随着国际社会要求以越来越快的速度传输更多信息&#xff0c;电子通信也在不断发展。模拟和数字技术用于无线、光纤和有线网络系统提供的点对点和广播通信。为了开发和维护这些系统&#xff0c;工程师需要能够测试和鉴定接收和传输的信号。需要减少信号损失或衰减&…

SpringBoot SpringBoot 原理篇 1 自动配置 1.2 bean 的加载方式【二】

SpringBoot 【黑马程序员SpringBoot2全套视频教程&#xff0c;springboot零基础到项目实战&#xff08;spring boot2完整版&#xff09;】 SpringBoot 原理篇 文章目录SpringBootSpringBoot 原理篇1 自动配置1.2 bean 的加载方式【二】1.2.1 第二种方式1 自动配置 1.2 bean …

idea 项目代码打包为jar包详解

目录前言一、将所有依赖和模块代码打包为一个jar二、只将模块代码打包为一个jar前言 假设我有如下简单 maven 项目 点击 File > Project Structure > Artifacts > 点击加号 > 选择JAR > 选择From modules with dependencies 一、将所有依赖和模块代码打包为一…

【树莓派不吃灰】命令篇⑨ 记录学习文件系统

目录1. 外部存储设备1.1 分区挂载1.2 查看磁盘信息&#xff08;包括未挂载磁盘&#xff09;1.2.1 mmcblk0p01.2.2 sda1&#xff08;额外了解&#xff09;1.3 查看UUID1.4 查看文件系统挂载情况2. 文件系统2.1 索引式文件系统2.1.1 EXT2 文件系统&#xff08;了解&#xff09;2.…

Android 序列化框架 Gson 原理分析,可以优化吗?

本文已收录到 AndroidFamily&#xff0c;技术和职场问题&#xff0c;请关注公众号 [彭旭锐] 提问。 前言 大家好&#xff0c;我是小彭。 Gson 是 Google 推出的 Java Json 解析库&#xff0c;具有接入成本低、使用便捷、功能扩展性良好等优点&#xff0c;想必大家都很熟悉了。…

ES6 入门教程 25 Module 的加载实现 25.4 循环加载

ES6 入门教程 ECMAScript 6 入门 作者&#xff1a;阮一峰 本文仅用于学习记录&#xff0c;不存在任何商业用途&#xff0c;如侵删 文章目录ES6 入门教程25 Module 的加载实现25.4 循环加载25.4.1 CommonJS 模块的加载原理25.4.2 CommonJS 模块的循环加载25.4.3 ES6 模块的循环加…