深入浅出数据结构之数组

简介: 深入浅出数据结构之数组

深入浅出数据结构之数组

数据结构是计算机存储、组织数据的方式

常用的数据结构有数组、链表、栈、队列、散列表(哈希表)、堆、树、图等

熟悉数据结构能够帮助我们在平常工作中更好的使用

本篇文章将围绕数组深入浅出的解析数组的特点、适用场景以及设计如何实现数组

数组的特点

存储元素内存连续

数组是在内存中连续存储多个元素的结构,元素能够通过数组的下标(索引)进行访问

数组的下标从0开始,比如图中创建了一个长度为5(连续空间)的数组,其中从下标0开始存储元素1、2、3、4

数组中存储什么元素,由什么类型的数组决定,比如我规定创建int类型的数组,那么数组中元素类型就都应该是int,并且数组中每个元素占4Byte空间

image.png

随机访问

由于数组内存空间是连续的且元素类型大小相同,这能让数组做到对每个元素的"随机访问"

比如我知道这个int数组存储真实数据的物理空间偏移量位置offset为500,则下标为0的元素就是从offset=500开始存储的,则下标为3的元素就是从offset=500+(3 * 4)=512 开始的

插入/删除时可能需要挪动其他元素

在数组中寻找某个关键元素时,我们可以选择将数组遍历(从头到尾)查找一边,这样平均时间复杂度就变成O(n)其中n为数据规模,随着数组中元素的增多耗时将会上升

在数组中插入某个元素时,如果插入的是数组末尾则时间复杂度不高为常量级别O(1),如果插入的是数组中间,由于空间连续,还需要将后续所有元素向后挪动一位,平均时间复杂度为O(n)

比如图中将元素0插入到下标2的位置

image.png

删除同理,比如将元素1删除时其他元素向前挪动

image.png

扩容与缩容

当发生添加操作时且数组已经被占满了就会发生扩容,需要创建一个更大的数组来装载元素,由于数组的特点,创建新数组后还需要将老数组所有元素迁移到新数组中

扩容大小是需要权衡的,当数组太小且后续还有添加操作时,扩容太小将会导致后续又进行扩容;当数组太大且后续无添加操作时,扩容太大可能导致浪费空间(图中是从长度为5 扩容为长度为10)

image.png

频繁删除元素后,数组可能存在大量空余空间,这时候如果想避免浪费空间就能进行缩容,以此来节约空间,缩容流程与扩容相似,也是新创建数组再对元素做迁移,也会额外耗时

适用场景

刚刚分析过数组的特点,熟悉特点的同学已经能够从特点分析出数组适用于哪些场景了

数组是空间连续的,支持随机访问的,访问数组中某个元素的时间成本都相同,这很适用于想查找某个下标位置上元素是什么的场景

对数组中最后一个元素进行添加/删除时,时间复杂度是常量级别的,这适用于频繁在数组尾部添加/删除的场景

数组的扩容/缩容会额外耗费时间,如果操作时就知道数据大小,可以在创建数组时就进行初始化大小,避免扩容

设计实现数组

设计数组时,需要考虑用变量来记录数组的一些信息

考虑到通用性,可以使用泛型来实现各种类型的对象数组,可能需要一个size变量来记录数组中元素的数量

 public class ListArray<T> {
     private int size = 0;
     private Object[] element;
 }

考虑到要给用户提供初始化大小,还需要INITSIZE变量来记录初始化数组长度

 private final int INITSIZE = 5;
 ?
 ListArray() {
     element = new Object[INITSIZE];
 }
 ?
 ListArray(int elements) {
     if (elements <= 0) element = new Object[INITSIZE];
     else element = new Object[INITSIZE];
 }

当遇到不理想的情况时返回一个特殊的常量ERROR

 private final int ERROR = -1;

提供一些基础能够使用数组的方法等等

查看元素数量

     public int size() {
         return size;
     }

判断数组是否为空

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

查找元素

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

判断数组中是否包含某个元素

     public boolean contains(T element) {
         return indexOf(element) != ERROR;
     }

获取下标位置的元素,查询前还要判断所给下标是否越界

     //时间复杂度:O(1)
     public T get(int index) {
         checkIndex1(index);
         return (T) element[index];
     }
 ?
     private void checkIndex1(int index) {
         if (index < 0 || index >= size) throw new ArrayIndexOutOfBoundsException("您输入的下标数有误");
     }

将某位置的下标元素替换为新元素,并返回旧元素

     //设置对象,返回的是旧对象
     //时间复杂度:O(1)
     public T set(int index,T elements) {
         checkIndex1(index);
         T oldElement = (T) element[index];
         element[index] = elements;
         return oldElement;
     }

添加元素,添加前需要检查扩容, 扩容中的迁移其实不需要遍历老数组迁移,可以使用APISystem.arraycopy();直接将老数组数据复制到新数组中

     /*
     添加对象到最后
     时间复杂度:
     最好:O(1) 在末尾添加元素
     最差:O(n) 扩容,移动元素
     平均:O(1)
     均摊:O(1) 连续多次复杂度低,出现个别复杂度高的情况(把复杂度高的均摊给复杂度低的)
     n是数据规模,这里数据规模是size(数组大小)
     */
     public void add(T element) {
         add(size, element);
     }
 ?
     /*
     往index位置添加对象
     时间复杂度:
     最好:O(1) 在末尾添加元素
     最差:O(n) 在头部添加元素,所有元素往后挪一位
     平均:O(n)
     n是数据规模,这里数据规模是size(数组大小)
     */
     public void add(int index, T elements) {
         checkIndex2(index);
         capacityExpansion(size + 1);
         for (int i = size; i > index; i--) {
             element[i] = element[i - 1];
         }
         element[index] = elements;
         size++;
     }
 ?
     //检查Index是否正确
     private void checkIndex2(int index) {
         if (index < 0 || index > size) throw new ArrayIndexOutOfBoundsException("您输入的下标数有误");
     }
 ?
     //扩容
     private void capacityExpansion(int capacity) {
         int oldExpansion = element.length;
         if (capacity <= oldExpansion) return;
 ?
         //(oldExpansion >> 1) = 0.5 oldExpansion
         int newExoansion = oldExpansion + (oldExpansion >> 1);
         Object[] newElement = new Object[newExoansion];
         for (int i = 0; i < size; i++) {
             newElement[i] = element[i];
         }
         element = newElement;
 ?
     }

其他方法不一一展示,数组的实现相对简单,不熟悉的同学可以去查看ArrayList源码或者手写一个简单的数组,这样能够大大加深自己对数组的理解

总结

本篇文章围绕数组,深入浅出的解析数组的特点、适用的场景以及设计数组思路与实现数组过程代码

数组是内存空间连续的结构,能够随机访问数组中的元素

在数组中间进行插入、删除需要挪动其他元素,这是额外的性能消耗,只有在数组末尾进行插入、删除才是最高效的

扩容与缩容会对数据进行复制,也是额外的性能消耗,如果知道操作数据规模就应该在初始化时设置,避免发生扩容缩容

在设计实现数组时,不仅要考虑到需要记录数组的信息,还要考虑到不合理的情况,对不合理的参数进行校验拦截


相关文章
【数据结构】数组、双链表代码实现
【数据结构】数组、双链表代码实现
|
4天前
|
算法 测试技术 C++
【数据结构】【双堆】【滑动窗口】3013. 将数组分成最小总代价的子数组 II
【数据结构】【双堆】【滑动窗口】3013. 将数组分成最小总代价的子数组 II
|
4天前
|
数据处理 C语言 C++
数据结构第四弹---数组相关OJ题
数据结构第四弹---数组相关OJ题
|
4天前
|
搜索推荐 算法 测试技术
数据结构排序——计数排序和排序总结(附上912. 排序数组讲解)
数据结构排序——计数排序和排序总结(附上912. 排序数组讲解)
30 0
|
4天前
|
存储 算法 Java
数据结构之数组
数据结构之数组
37 0
|
4天前
|
存储 算法 Java
数据结构与算法 数组和链表
数据结构与算法 数组和链表
12 0
|
4天前
|
算法 测试技术 C++
【栈 最小公倍数 最大公约数】2197. 替换数组中的非互质数
【栈 最小公倍数 最大公约数】2197. 替换数组中的非互质数
【栈 最小公倍数 最大公约数】2197. 替换数组中的非互质数
|
4天前
|
存储 算法
Leetcode 30天高效刷数据结构和算法 Day1 两数之和 —— 无序数组
给定一个无序整数数组和目标值,找出数组中和为目标值的两个数的下标。要求不重复且可按任意顺序返回。示例:输入nums = [2,7,11,15], target = 9,输出[0,1]。暴力解法时间复杂度O(n?),优化解法利用哈希表实现,时间复杂度O(n)。
22 0
|
4天前
|
算法
栈刷题记(二-用栈操作构建数组)
栈刷题记(二-用栈操作构建数组)
|
4天前
|
存储 算法 Serverless
【软件设计师备考 专题 】数据结构深度解析:从数组到图
【软件设计师备考 专题 】数据结构深度解析:从数组到图
60 0
http://www.vxiaotou.com