前言
希望基于此系列文章能以轻松,简单的方式帮助大家学习并使用数据结构。
今天正式进入编程修仙第二期,炼气一层,数组(上),本来没准备分上下,但是发现光数组的基本介绍就写了不少,所以本篇将着重于数组的基本理念和思想方法,下篇将着重于数组的实际使用场景和容器类实现。
什么是数组
直接上定义,数组是一种线性数据结构,由相同类型的元素集合组成,存储在一段连续的内存空间中。
如何理解“存储在一段连续的内存空间“
- 内存是线性排列的一批存储单元,实际数据(和指令)就存储在存储单元中。单元有唯一编号,称为单元地址,单元地址从0 开始连续排列。如下图:
- 所以将数据“存储在一段连续的内存空间“,可以简单理解为将数据存放在上图连续的方格中,它们的单元地址将是连续的。如果我们将字母A,B,C按顺序存放进从单元地址3开始的连续内存空间,实际效果如下图:
- 同时存储单元有其存储空间,一般为一个字节,这也就意味着如果需要存储的数据大小超过一个字节,就需要用到多个存储单元。所以如果我们将三个int型数据1, 2, 3(一个int型数据占用4个字节)按顺序存放进单元地址3开始的连续内存空间,实际效果如下图:
以下皆为个人见解,仅供参考!
为什么要连续
举个生活中的例子,我有一整套《哈利波特》7本书,我现在把它们放到书架上(书架上可能有其它书),以后想看的时候再把它们拿下来。如果我们把7本书打散,随意放在书架任意位置,后续要找的时候,就得要一本本浏览书架上的书,看是不是自己想要的《哈利波特》。这样效率太低,更容易出错。
所以在这种情况下,绝大多数人都会选择把7本书放在书架同一区域,这样只要记住这个区域(如第二排第一本开始的7本),就可以快速找到他们。
数组其实也是相同的理念,不将数据随意存储在任意存储单元上,而是存储在一段连续的地址,这样根据第一个数据元素所在的首地址(对应书架第二排第一本)和数据占用存储单元数量(对应7本书)就可以快速找到所有的数据。
为什么要相同类型数据
生活举例
继续以7本哈利波特为例,如果我们将哈里波特第一部放在书架第二排第一本,并按第一部到第七部顺序连续放置,那如果我们要找到第五部哈利波特,可以直接拿走第二排第五本的书,而不用从第二排第一本开始一本本看。
明显“直接拿走第二排第五本的书”是种效率更高的方式,但这个方式的前提是存放和拿取的单位都是“本”,假如我们的场景扩大为在书架上放置多套书籍,如哈利波特,小学数学教材,初中语文教材和唐诗三百首,我们的需求变为直接拿取初二语文教材上册,那整个计算就会变得复杂,因为你需要先知道哈利波特一套是有7本,小学数学教材是10本,初二语文教材上册在初中语文教材中位于第5本,这样得出初二语文教材是在第22(1+7+10+5-1)本。
这样就带来了问题:计算书籍所在的位置变得困难,必须要先知道放在前面的书有哪些,他们分别有几本。
解决的方法也简单,那就是记录下每套书籍开始的位置(如哈里波特是在第二排第一本开始,小学数学教材是在第二排第八本开始),很明显这就回到了之前的“单独放置哈利波特”的场景:
已知初中语文教材在第二排第18本开始,初二语文教材上册在整个初中语文教材中位于第5本,所以直接取出第二排第22本(18+5-1)就是初二语文教材上册,效率超级加倍!
总结其中的逻辑是:将一个连续放置的书本区域,根据书本所属的作品集,拆分成多个区域,每个区域内都连续放置同一作品集的书本。通过这种方式,可基于书本在作品集中所处序号,快速定位书本实际在书架上的位置。
推导到计算机内存
如果直接推导的话,得到逻辑是:将一个连续的内存区域(线性表),根据存放数据类型的不同,拆分成多个区域(线性表),每个区域(线性表)内都连续存储相同类型的数据。
我们来验证下,如按顺序存入A,1,B,C,2,3(英文字母占用1个字节,数字为int型占用4个字节):
如要获取数据C,则需要分别计算A,1,B,C占用的空间,如下:
2 +1 + 4 + 1 = 8
A的首地址 2 + A占用1个存储单元 + 1占用4个存储单元 + B占用1个存储单元 = C的首地址8
这样实际上就仍然需要尝试遍历所有数据元素,才能找到需要的元素,无法实现根据元素在数组中的位置直接访问元素。
为了解决这个问题,所以我们按照数据类型进行划分,拆为两个数组,一个放英文字母(A,B,C),一个方int数字(1,2,3):
在这场景下,想要直接获取到放入的int数据 2,则根据公式计算它的首地址:
7 + (2 - 1) * 4 = 11
即直接获取从单元地址11开始的4个单元(11,12,13,14)数据就是之前放入的int数据 2 。
基于这个思路,我们可以得到公式:
需要查找的数据元素首地址 = 数组首地址(也是数组第一个元素首地址) + 数据类型大小 (长度)* 需要查找的数据元素在数组中的偏移量(序号-1)
这样就形成了根据数据元素在数组中的位置快速找到数据的方法。
回到问题
这也就解释了原来的问题,“为什么要相同类型数据”,这其实是为了达成根据元素在数组中的位置直接访问元素的能力(也就是数组最大特点随机访问),所作的前提定义。
需要查找的数据元素首地址 = 数组首地址(也是数组第一个元素首地址) + 数据类型大小 (长度)* 需要查找的数据元素在数组中的偏移量(序号-1)
但是通过公式可以看出,真正对直接计算元素的单元地址产生影响的是数据类型的大小 (长度),而不是数据类型本身。也就是说为了达成上文提到的“随机访问“的能力,前提条件只要数据类型的大小(长度)相同就可以。
那为什么还要限制相同数据类型,个人认为主要是在具体实现和使用上:相同数据类型,可以保证你在取出数据后可以直接、进行处理,否则得要先判断其类型,再选择处理方式,甚至会需要进行类型转换等运算,降低性能,增加程序复杂度和发生错误的可能。
数组的缺点
前面主要讲了数组的优点,接下来讲讲缺点:
插入操作效率低
为什么效率会低?这很好理解,如果要在一排书中间插入一本书,那得要先把后面的书往后挪一挪,留出空间,在放入新的书。在内存中也是如此,在数组中间插入一个数据,会需要移动它之后的所有数据,所以时间复杂度为O(n)。
如何解决?如果数组中的数据是有序排列的,如哈利波特1-7部,第2部必须放在1和3之间,所以只能移动后续元素,没有办法解决。但是如果数据是没有顺序的,那直接把新数据插入到末尾,或者只迁移插入位置上的数据,把原位置上已有的数据搬移到末尾,再把新的数据写入到该位子上,这样就可以避免大规模的数据迁移。
删除操作效率低
同插入类似,删除数组中某一个元素,还需要把后面的元素往前迁移。而且不幸的是,不管数据是有序的还是没有顺序的,删除都需要大规模数据迁移。所以只有尽可能少地进行删除操作,比如不删除而是将数据按业务逻辑置空,或者新建一个数组用来记录已删除元素序号,而不是真正删除原数组中的元素。
数组的使用
本系列都会用Python为例:
Python中针对数组有三种使用方式:
- 最常用的list,但是我们这次不以他为例,因为他可以存放不同数据类型(长度),我们把它在下期数组(下)中,讨论容器的时候再说
- Numpy中的array
- 为了样例简单,我们使用Python标准库中的array模块
import array
# 定义一个整数数组
arr = array.array('i', [1, 2, 3])
# 添加元素
arr.append(4)
# 访问元素
print(arr[0]) # 输出1
# 修改元素
arr[0] = 10
# 删除元素
del arr[0]
# 遍历数组
for i in arr:
print(i)
可以看出Python中的array是一个动态数组
结束
静态、动态数组,容器类实现和数组实际使用场景,计划放在下篇。
欢迎大家评论留言,本文只是我个人观点,如有不足或错误也清指出,大家一起学习~~