目录

  • 为什么数组下标从0开始?
  • 数组定义
    • 为什么这么下定义?
    • 定义数组的三种方式
  • 从 ArrayList 源码看数组增删改查

    • 初始化
    • 增加
    • 删除
    • 修改
    • 查找
  • 数组和容器

    • 数组时间复杂度
    • 数组插入,删除优化
    • 容器替代数组?
  • 字节高频算法题:移动零
  • 算法发散

❝没有最完美的数据结构,只有最合适的数据结构。

为什么数组下标从0开始?

这个问题上大学第一课C语言的时候我就疑惑,没有接触过计算机之前,数数都是从1开始的呀,一只羊两只羊三只羊,别睡着了。

参考原因如下:

  1. 高级语言爷爷级的C语言,就是从0开始,后面发展的语言都是沿用这个,降低学习成本;但是发展了这么多年,几个更加新的语言,Python等支持负数下标;
  2. 下标代表偏移量,

推导得到第i个元素地址公式:

a[i]_address = first_address + i * data_type_size

如果从1开始,推导得到第i个元素地址公式: a[i]_address = first_address + (i-1) * data_type_size

即多了一次-1操作,对于CPU来说,就是多了一次减法指令

数组定义

数组是一种线性表结构,它用一组连续的内存空间,来存储一组具有相同类型的数据。

「线性表」:具有像线一样性质的表。即线性表上的数据只有前后关系,数组,链表,队列,栈这样的都是前后关系的线性表结构,树和图这样的前后左右都有关系的即是非线性表结构。

为什么这么下定义?

一般下定义都是留下了最精炼的字来概括内容,就像一部好的电影没有一句废话,下面来分析一下数组定义。

「连续」:正是因为连续的内存空间,所以我们能推算出每个元素的地址,假设一个数组有五个元素,起始地址为00,那么后面元素地址一次为01,02,03,04,别人一问你第五个元素地址,你立马可以告诉她是04,这正是因为数组的内存空间是一段连续的空间。

然而如果这五个元素存放在链表里,那么你就不能立马告诉别人第五个元素的地址是04了,你要先找到第一个元素取得第二个元素的地址,然后取得第三个元素的地址,一直找下去找到最后一个元素,就是因为链表存储的空间不是连续的,链表元素里面除了数据本身还需要多存放下一个元素的地址,通过这种方式来找下一个元素,如果要同时知道链表前后是谁就需要双链表了。

注:正是因为数组需要连续的内存空间,所以定义数组的时候都需要指定数组的初始大小,要不然会报错。JAVA容器类ArrayList底层是Object[]数组实现的,数组指定的初始大小在JDK1.8之前是10,JDK1.8时候变成了0。

「相同类型」:试想一下,你一个数组,一会儿放个int类型,一会儿放个long类型,那么上面提到的内存连续也拯救不了你。你让计算机咋搞呢,int类型占四个字节,long类型占八个字节(64位操作系统下),计算机是把四个字节看成一个元素,还是八个字节当做一个元素呢,要知道所需存储空间不同地址不同呀,即使你内存连续都不能根据下标统一寻址了。

「因此,数组两大特性:」连续内存空间,相同类型元素。数组一切的一切,都是基于这两个的,基于这两大特性,数组实现了最大的优点:随机存取,我们很多时候使用数组都是贪图这个优点。

定义数组的三种方式

初始化数组主要分为静态初始化动态初始化,无论哪一种都需要指定数组大小:

静态初始化:定义数组时候,开辟空间的同时设置内容,一次性初始化完成;

动态初始化:数组先开辟空间,再使用索引进行内容赋值;

从 ArrayList 源码看数组增删改查

感觉纯粹看数组的增删改挺无趣的,我们每个人只要静下心来都可以实现数组的增删改查,极客算法里面通过看JAVA的 ArrayList 源码的方式来看数组增删改,我觉得挺不错的:

一来可以看看设计者们怎么封装的,感受感受优秀代码设计;

二来可以熟悉熟悉源码,更加清楚天天用的 ArrayList 底层实现,可以看到有什么值得平时注意的。

以下都是基于JDK1.8,选取ArrayList是因为这个我们平时用的最多。

初始化

public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, Serializable {// 序列化idprivate static final long serialVersionUID = 8683452581122892189L;// 默认初始的容量private static final int DEFAULT_CAPACITY = 10;// 一个空对象private static final Object[] EMPTY_ELEMENTDATA = new Object[0];// 一个空对象,如果使用默认构造函数创建,则默认对象内容默认是该值private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = new Object[0];// 当前数据对象存放地方,transient表明当前对象不参与序列化transient Object[] elementData;// 当前数组长度private int size;// 数组最大长度private static final int MAX_ARRAY_SIZE = 2147483639;// 方法开始
}
复制代码

默认构造函数:

public ArrayList() {this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;/** 也就是实现了 Object[] elementData;elementData = new Object[0] ,即new了一个空的对象数组,数组长度是0 **/}
复制代码

增加

ArrayList 添加了四种添加方法:

  • add(E element)
  • add(int i , E element)
  • addAll(Collection)
  • add(int index, E element)

数组末尾追加元素 add(E element)

public boolean add(E e) {ensureCapacityInternal(size + 1);  // Increments modCount!!elementData[size++] = e;return true;}
复制代码

ensureCapacityInternal() 确保添加的元素有地方存储,size+1,默认size为0,+1保证数组下标为size+1这个地方可以存储新元素,下面的 elementData[size++] = e 进行新的元素追加到数组并且上面的保证使其赋值不会数组越界;

private void ensureCapacityInternal(int minCapacity) {ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));}private static int calculateCapacity(Object[] elementData, int minCapacity) {if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {return Math.max(DEFAULT_CAPACITY, minCapacity);}return minCapacity;}private void ensureExplicitCapacity(int minCapacity) {modCount++;// overflow-conscious codeif (minCapacity - elementData.length > 0)grow(minCapacity);}
复制代码

minCapacity 为增加元素时所需最小长度数组容量大小;

下面第一次add时候,将当前elementData数组的长度用 Math.max 变为10,即第一次add时候 将数组长度 minCapacity 变为默认初始容量10;(jdk1.8以前都是直接初始化的时候指定this(10)直接指定默认容量大小)

非第一次add的时候,minCapacity 为原数组的长度+1:

如果所需的最小长度大于了现有数组长度,那么现在的数组容量肯定是不够的,需要进行扩容;

modCount 是从 abstractList 里面继承过来的值,用于迭代器Iterator的操作次数记录;

private void grow(int minCapacity) {// overflow-conscious codeint oldCapacity = elementData.length;// 右移运算符等价于除以2,如果第一次是10,扩容之后的大小是15int newCapacity = oldCapacity + (oldCapacity >> 1);if (newCapacity - minCapacity < 0)newCapacity = minCapacity;// 考虑边界问题,数组最大容量为2的31次方,int为四个字节,每个字节8位if (newCapacity - MAX_ARRAY_SIZE > 0)newCapacity = hugeCapacity(minCapacity);// minCapacity is usually close to size, so this is a win:elementData = Arrays.copyOf(elementData, newCapacity);}private static int hugeCapacity(int minCapacity) {if (minCapacity < 0) // overflowthrow new OutOfMemoryError();return (minCapacity > MAX_ARRAY_SIZE) ?Integer.MAX_VALUE :MAX_ARRAY_SIZE;}
复制代码

扩容,如果添加元素所需最小容量minCapacity(即当前的数组已使用空间(size)加1)大于数组长度,则增大数组容量,扩大为原来的1.5倍。(右移一位相当于除以2)

数组最大容量为2的31次方,数组长度length属性是int,int为四个字节,每个字节8位,2G内存,没有人会丧心病狂搞这么大数组吧!

public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) {@SuppressWarnings("unchecked")T[] copy = ((Object)newType == (Object)Object[].class)? (T[]) new Object[newLength]: (T[]) Array.newInstance(newType.getComponentType(), newLength);System.arraycopy(original, 0, copy, 0,Math.min(original.length, newLength));return copy;}public static native void arraycopy(Object src,  int  srcPos,Object dest, int destPos,int length);
复制代码

Arrays.copyOf追踪下去代码,确保有足够的容量之后,使用System.arraycopy 将旧数组拷贝到新的数组.

数组中间插入一个元素

public void add(int index, E element) {// 判断index 是否有效rangeCheckForAdd(index);// 计数+1,并确认当前数组长度是否足够,和上面的追加一样ensureCapacityInternal(size + 1);  // Increments modCount!!System.arraycopy(elementData, index, elementData, index + 1,size - index); // 将index 后面的数据都往后移一位elementData[index] = element; // 设置目标数据size++;}private void rangeCheckForAdd(int index) {if (index > size || index < 0)throw new IndexOutOfBoundsException(outOfBoundsMsg(index));}
复制代码

需要插入的位置(index)后面的元素统统往后移动一位,然后将新值插入。

整个插入过程:

  1. 确保数插入的位置小于等于当前数组长度,并且不小于0,否则抛出异常;
  2. 确保数组此数组能放得下新的数据 所需长度minCapacity=size+1;
  3. 修改次数(modCount)标识自增1,如果当前数组所需长度大于当前的数组长度,则调用grow方法,增长数组;
  4. grow方法会将当前数组的长度变为原来容量的1.5倍;
  5. 确保有足够的容量之后,使用System.arraycopy 将需要插入的位置(index)后面的元素统统往后移动一位;
  6. 将新的数据内容存放到数组的指定位置(index)上;

删除

ArrayList 中提供了 五种删除数据的方式:

  • remove(int i)
  • remove(E element)
  • removeRange(int start,int end)
  • clear()
  • removeAll(Collection c)
public E remove(int index) {// 判断索引是否有效,范围检查rangeCheck(index); modCount++;// 获取对应数据E oldValue = elementData(index);  // 判断删除数据位置int numMoved = size - index - 1; // 如果删除数据不是最后一位,则需要移动数组// 先将index后面的元素往前面移动一位(调用System.arraycooy实现)if (numMoved > 0) System.arraycopy(elementData, index+1, elementData, index, numMoved);// 然后将最后一个元素置空,进行垃圾回收elementData[--size] = null; return oldValue;}
复制代码

修改

这个简单,需要改哪个,直接 data[index] = 4 重新赋值就可以

查找

数组支持随机访问,根据下标随机访问的时间复杂度为O(1)。

但是这并不代表数组的查找时间复杂度是O(1),即使是排好序的数组,你用二分查找,时间复杂度也是O(logn),这是两个概念。

数组和容器

数组时间复杂度

如果在数组的末尾插入元素,那就不需要移动数据了,这时的时间复杂度为O(1)。

但如果在数组的开头插入元素,那所有的数据都需要依次往后移动一位,所以 最坏时间复杂度是O(n)。

因为我们在每个位置插入元素的概率是一样的,所以平均情况时间复杂度为(1+2+...n)/n=O(n)。

数组插入,删除优化

上面数组的插入和删除效率是很低的,正是因为数组是连续的空间内存,而插入和删除的时候改变了数组的空间内存,为了维护连续的内存空间所以要进行数组元素的移动

具有这个特性,就要维护他,比如红黑树具有查找快速的特点,插入和删除的时候就必须要通过各种左旋右旋操作来维护红黑树的平衡,其实是一样的道理。

插入优化

如果数组中的数据是有序的,我们在某个位置插入一个新的元素时,就必须按照刚才的方法搬移插入位置之后的数据。

但是,我们开发中,如果数组中存储的数据并没有任何规律,数组只是被当作一个存储数据的集合。在这种情况下,如果要将某个数据插入到第i个位置,为了避免大规模的数据搬移,还有一个简单高效的办法就是,直接将第i位的数据搬移到数组元素的最后,把新的元素直接放入第i个位置(具体如下图)。

利用这种处理技巧,在特定场景下,在第i个位置插入一个元素的时间复杂度立即降为了O(1),快排就用到了这个处理思想。

删除优化(标记清除算法)

标记清除算法 是JVM垃圾回收里面用到的核心算法,具体的可以看公众号《阿甘的码路》里面,有关垃圾回收机制相关的文章。

如果数组中数据不要求连续的情况下,我们将多次删除操作集中在一起执行,只做标记清除工作而不进行真正的删除,然后统一进行删除,删除的效率会提高很多不用进行数据多次的搬迁。

容器替代数组?

容器优点:

  1. 动态扩容,程序员很舒服只需要一直add就好了不需要管数组大小是否足够
  2. 封装了很多细节,API丰富,将下标操作转化为英文add,remove等人类语言

容器缺点: 装箱拆箱有一定的性能损耗

数组优点:

  1. 多维数组直接用数组表示更加直观,如 int[3][4] arrList<List<Integer>>?
  2. 普通业务开发容器足够,底层开发例如网络框架这种对性能优化极致追求的代码用数组还是比较高效的。

字节高频算法题:移动零

审题: 保持非零元素相对顺序,指的是元素在数组里面的相对顺序,而不是让保证元素相对大小。

思路:

  1. 遍历,遇0删除,列表最后添加0

这里使用的Python的 api 还是很方便的,代码也很清晰明了,思路简单。JAVA就做不到这样add然后remove,集合的实现方式不一样,不信的话可以进行实现,你会发现有很多报错。

缺点: 空间复杂度很高,每次remove其实都需要移动此元素后面所有的元素。

  1. 两次遍历

创建两个指针i和j,第一次遍历的时候指针j用来记录当前有多少非0元素。即遍历的时候每遇到一个非0元素就将其往数组左边挪,第一次遍历完后,j指针的下标就指向了最后一个非0元素下标。

第二次遍历的时候,起始位置就从j开始到结束,将剩下的这段区域内的元素全部置为0。

时间复杂度:O(n)

空间复杂度:O(1)

  1. 最优解

在原数组上面进行操作,所有的非0元素往前移动,0自然在后面了

  • j记录要填入的非零元素位置,遇到非0元素就挪动到j位置上;
  • 遍历整个数组,遇到nums[i]==0 时候不处理;如果非0的时候,则把nums[i]的非0元素和nums[j]上的0元素互换,调换位置
  • j始终指向的是下一个非0元素;

很抽象,移动零最优解图解如下:

省了一次遍历,借鉴了快排的思路:

快排:快速排序首先要确定一个待分割的元素做中间点x,然后把所有小于等于x的元素放到x的左边,大于x的元素放到其右边;

移动零:我们可以用0当做这个中间点,把不等于0(注意题目没说不能有负数)的放到中间点的左边,等于0的放到其右边。

  1. 从右往左开始遍历,所有的0元素往后移动,和上面的思路一样,读者如果可以根据上面的解法自己实现这种方式,那么这道算法算是理解了。

arraylist转int数组_五千字的数组拓展,面试官对我竖起大拇指喊停相关推荐

  1. list 查找_五千字长文带你学习 二分查找算法

    点击上方"与你一起学算法",选择"星标"公众号 重磅干货,第一时间送达 二分查找的思想 提及二分查找算法,我想大部分人都不陌生,就算不是学计算机的,基本上也都使 ...

  2. 2W五千字的C++基础知识整理汇总

    最近发现一篇两万五千字的C++基础知识大汇总,可把我高兴坏了.白嫖,必须白嫖! 来源:高效程序员 一 .从"hello world" 入门C++! C++总览简介 C++ 是一种静 ...

  3. 计算机论文一千五,1.论文字数不够,正文要五千字,至少增加1000,但不要抄袭.DOC...

    PAGE 1.论文字数不够,正文要五千字,至少增加1000,但不要抄袭 2.格式不对.格式要求及例子我给你附在后面,照葫芦画瓢,每一部分都要有,格式都要对. 请3日内改好,已经来不及了.4月末系统关闭 ...

  4. 三万五千字长文!让你懂透编译原理(六)——第六章 属性文法和语法制导翻译

    三万五千字长文!让你懂透编译原理(六)--第六章 属性文法和语法制导翻译 长文预警 系列文章传送门: 万字长文+独家思维导图!让你懂透编译原理(一)--第一章 引论 万字长文!让你懂透编译原理(二)- ...

  5. 转发——————《黄金问题3---一万五千字教你如何全面爱护你的膝盖》(正文+1楼回复必看)...

    <黄金问题3---一万五千字教你如何全面爱护你的膝盖>(正文+1楼回复必看)  由 mathiasych 发表在 虎扑篮球· 健身和运动健康 http://bbs.hupu.com/fit ...

  6. 基于Android的漫画阅读器App设计与实现,安卓、MySQL、Java、Andriod Studio,前台用户+后台管理,完美运行,有一万五千字论文。

    基于Android的漫画阅读器App设计与实现,安卓.MySQL.Java.Andriod Studio,前台用户+后台管理,完美运行,有一万五千字论文. 用户模块功能: 引导页:首先进入APP之后会 ...

  7. 干货 | 五千字长文带你快速入门FlinkSQL

    本文已收录github:https://github.com/BigDataScholar/TheKingOfBigData,里面有大数据高频考点,Java一线大厂面试题资源,上百本免费电子书籍,作者 ...

  8. 一万五千字讲清Java当中Collection下的接口

    文章目录 Collection接口 List接口 ArrayList集合 ArrayList集合的常用方法和底层源码 Vector集合 LinkedList集合 LinkedList集合的底层源码 Q ...

  9. 从事测试的第6年 , 开工第二天五千字总结..我不平凡的2021

    大家好,今天开工第二天!我打算给自己做个总结. 我:一个混过大厂,待过创业公司,爱给开发提Bug的测试一枚.2021年,是工作的第六年,也是具有突破性成长的一年,一直挺喜欢 6 这个数字,果然不负期望 ...

最新文章

  1. c++ const 类型检查
  2. uaa 授权_使用UAA OAuth2授权服务器–客户端和资源
  3. java cunstructor_参加JavaScript面试,必须搞懂的问题(推荐)
  4. 空间说说html,空间说说大全(共100句)
  5. Python3中的yield from语法
  6. JS - this,call,apply
  7. win7中计算机被改为了句号,WIN7中中文输入法快捷键无法修改的解决方法.doc
  8. python编程课第七课_Python 第7课 if的介绍和使用
  9. esp8266 继电器接线图_如何使用ESP8266和Android控制继电器
  10. Sqlmap常用命令总结及注入实战(Access、mysql)
  11. 智能工厂具体的名词解释
  12. 如何获取vs code中插件Waka Time的API key
  13. 2022 年杭电多校第八场补题记录
  14. FTP与TFTP介绍
  15. 国内开源社区有哪些 ?|GitCode
  16. 我们都没有迎来决赛---Leo读《不是孙振耀写的职场感言》(1)
  17. Python扩展包:Unofficial Windows Binaries for Python Extension Packages
  18. IFrame语法小全 --使用iframe内的函数
  19. Xcode升级到14.3 archieve打包失败
  20. FCPX插件:CineFlare Tunnels(20个静态和深度隧道过渡和效果)

热门文章

  1. ACE_Proactor网络通信示例
  2. python自定义函数详解_Python自定义函数
  3. STM32F4启动流程分析
  4. Codeforces Round #482 (Div. 2)
  5. 安装windows系统时遇到的大坑——鼠标键盘没反应
  6. 简单Unity时间架构设计(克洛诺斯之匙)
  7. 私有云Opetstack的创建与运用
  8. 安全设置IIS的15个方法
  9. widcomm BT search place
  10. 单列索引和联合索引,有什么区别?