概述
对数组进行分析,目标如下
- 线性表的概念
- 数组的存储结构
- 数组查询,插入,删除操作的特点及对应的时间复杂度
- 刷题(盛最多水的容器)
线性表
在数据结构中,数据的逻辑结构分为线性结构
和非线性结构
线性结构: n个数据元素有序集合,特征如下:
- 集合中必存在唯一的一个
第一个元素
- 集合中必存在唯一的一个
最后的元素
- 除最后元素之外,其它数据元素均有唯一的
后继
- 除第一元素之外,其它数据元素均有唯一的
前驱
数据结构中线性结构
指的是数据元素之间存在着一对一
的线性关系的数据结构,这个线性并不是说一定是直线,常见的线性数据结构有:数组
(一维),链表
,栈
,队列
;表现形式如下:
相对应于线性结构,非线性结构
的逻辑特征是一个结点元素可能对应多个直接前驱和多个后驱,比如: 树
,图
,堆
等,如下图
数组基础
概念和结构
数组(Array
)是一种用连续的内存空间
存储相同数据类型
数据的线性数据结构
int[] array = new int[]{10,20,30}
内存结构中如下图
数组的表示方式:使用下标
来获取数组元素数据
操作平台是如何根据下标
来找到对应元素的内存地址
?
拿一个长度为
10
的数组来举例 ,int[] a = new int[10],在下面图中,计算机给数组分配了一块连续的空间,100-139,其中内存的起始地址为baseAddress=100
计算机给每个内存单元都分配了一个地址,通过地址来访问其数据,因此访问数组中某个元素时,首先要经过一个寻址公式
计算要访问的元素,在内存中的地址:
a[i] = baseAddress + i * dataTypeSize
其中dataTypeSize
代表数组中元素类型的大小,在这个例子中,存储的是int
型的数据,因此dataTypeSize=4
个字节
下标为什么从0
开始而不是1
呢?
个人想来,从数组存储的内存模型上来看,下标
最确切的定义应是偏移(offset)
,如果用array
来表示数组的首地址,array[0]
就是偏移为0的位置,也就是首地址,array[k]
就表示偏移k
个typeSize
的位置,所以计算array[k]
的内存地址用这个公式
array[k]_address = baseAddress + k * typeSize
如果下标从1
开始,那么计算array[k]
的内存地址会变成
array[k]_adress = baseAddress + (k-1) * typeSize
对比两个公式,不难发现从数组下标1
开始去访问数组元素,对于 cpu
来说,多了一次减法指令
另一个原因,c
语言设计者使用0
开始作为数组的下标,后来的高级语言沿用了这一设计(也有从1开始的)
数组的特点
查询O(1)
数组元素的访问是通过下标来访问的(不是遍历),计算机通过数组的首地址和寻址公式能够很快速的找到想要访问的元素
public class Demo {public static void main(String[] args) {int[] array = new int[]{10, 20, 30};System.out.println("打印:" + test(array, 1));}public static int test(int[] a, int i) {return a[i];}
}
代码的执行次数并不会随着数组的数据规模大小变化而变化,是常数级的,所以查询单个数据操作的时间复杂度为O(1)
插入删除O(n)
数组是一段连续
的内存空间,因此为了保证数组的连续性会使用数组的插入和删除的效率变的很低
数据插入
假设数组的长度为n
,现在需要将一个数据插入到数组中第k
个位置,为了将第k
个位置腾出来给新的数据,需要将第k~n
这部分的元素都顺序的往后挪一位,如下图所示:
插入操作对应的时间复杂度是多少呢?
最好的情况下是
O(1)
,最坏的情况下是O(n)
,平均情况下的时间复杂度是O(n)
数据删除
同数据插入可得:如果要删除第k
个位置的数据,为了内存的连续性,也需要搬移数据,不然中间就会出现空洞,内存就不连续了,时间复杂度仍然为O(n)
如果要提高数据删除的效率呢?
在某些特殊场景下,并不一定非得追求数组中数据的连续性;如果将多次删除操作集中在一起执行,删除的效率是不是会提高很多
example:数组a[6]中存了6个元素:a1,a2,a3,a4,a5,a6,现在要依次删除a1,a2这两个元素
为了避免a3,a4,a5,a6这几个数据被搬移两次,可以先记录下已经删除的数据,每次的删除操作并不是真正的移数据,只是记录数据 已经被删除,当数组没有更多的空间存储时,再触发执行一次真正的删除操作,这样就大大减少了删除操作导致的数据搬移
这种思想,就是jvm
标记清除垃圾回收算法的核心
思想
刷题
盛最多水的容器
盛最多水的容器
给定一个长度为 n 的整数数组 height 。有 n 条垂线,第 i 条线的两个端点是 (i, 0) 和 (i, height[i]) 。
找出其中的两条线,使得它们与 x 轴共同构成的容器可以容纳最多的水。
返回容器可以储存的最大水量。
说明:你不能倾斜容器。
第 i
条线的两个端点是 (i, 0)
和 (i, height[i])
两个端点的距离(高度)即数组下标i
的值
暴力美学,朴素解法
public class Demo {public static void main(String[] args) {System.out.println(maxArea(new int[]{1, 8, 6, 2, 5, 4, 8, 3, 7}));}public static int maxArea(int[] height) {int max = 0;for (int i = 0; i < height.length; i++) {for (int j = i + 1; j < height.length; j++) {// 比较两条线,哪个低(得出宽高算面积)int lineHeight = Math.min(height[i], height[j]);int width = j - i;max = Math.max(max, width * lineHeight);}}return max;}
}
双指针,速度快
由示例1
图中所示,要面积最大,要么x
轴要宽,要么 y
轴要高,所以,开始x
轴最大,算面积,后面比较两上y
轴,y
轴相对较小的继续向前或向后移动,这就是双指针
public class Demo {public static void main(String[] args) {System.out.println(maxArea2(new int[]{1, 8, 6, 2, 5, 4, 8, 3, 7}));}public static int maxArea2(int[] height) {int max = 0;// 第一步 , x轴距离最大,算其面积int i = 0;int j = height.length - 1;while (i != j) {// i = j 已无计算的必要int area = Math.min(height[i], height[j]) * (j - i);if (height[i] < height[j]) {// 如果前指针小于后面的指针,即 height[i] 的值小于 height[j] ,则移动 i ,寻找更大的 y 轴i++;} else {// 反过来一样j--;}max = Math.max(area, max);}return max;}
}
结束
数组至此就结事了,如有问题欢迎评论!