谈谈数组

一、数组是如何实现O(1)级别的下标随机访问

我们平时经常说数组适合用于查找,不适合增删,是因为数组查找的时间复杂度低,为O(1)。其实这句话的表述不对,数组查找一个数的时间复杂度最多是O(logn),而所谓的时间复杂度为O(1)实际上指的是根据数组下标对数组进行的随机访问。那么,数组是如何实现对下标的随机访问的呢?
当我们创建一个数组对象,并且规定了数组的存储类型、初始化大小,那么操作系统便会根据这些条件在内存中开辟固定的、连续的空间给这个数组对象,数组对象的地址便是该数组首个元素的地址。
举个例子,假设创建一个数组

int[] arr = new int[5];

该数组对象存储类型是整型变量,每个变量占4个字节,初始化大小为5,所以这个数组对象占整个连续内存大小为20字节。再假设arr[0]的地址为100,那么随机访问一个下标为i的数组元素,其地址计算公式为:

arr[i]_address = arr[0]_address + i * dataTypeSize

所以,现在要访问arr[3],先计算其在内存中的地址为112,这样就可以直接访问到arr[3]的值了,整个过程的时间复杂度就是上面那个计算公式,为O(1)。
下面再来思考一个问题,为什么数组的下标要从0开始呢?(不是绝对的,但是大部分情况是这样的)
先假设如果不为0,比如为1,那么在进行下标随机访问的时候回事什么样子呢?这时候下标为i的数组元素其地址计算公式为:

arr[i]_address = arr[1]_address + (i -1)* dataTypeSize

与上面唯一的差别就是多了一个减一,也就是说CPU要多执行一次减一的计数操作。作为非常基础且底层的数据结构应在性能上达到极致,能够避免的操作就绝不多做,这就是为什么数组的下标要从0开始了。

二、java多维数组内存连续性问题

第二个要讨论的是java维数组在内存中的地址连续性问题。首先要指明的是,数组的连续性在一维数组中是绝对的,但是在多维数组中便不是绝对的,java多维数组在内存中是分块连续的。
什么是分块连续的呢?以二维数组为例,二维数组保存的是各个一维数组首个元素的引用,一维数组保存的是一连串内存地址相连的元素。如下图所示:
在这里插入图片描述写个简单的例子:

public class Test1{
	public static void main(String args[]){
		int[][] arr = new int[5][3];
		printAddress(arr);
	}
	public static void printAddress(int[][] arr){
		for(int i = 0; i < arr.length;i++)
			System.out.println(arr[i]);
	}
}

结果输出为:

[I@4554617c
[I@74a14482
[I@1540e19d
[I@677327b6
[I@14ae5a5

很明显证明了每个一维数组之间是不连续的。
再写个例子,以不同的方式遍历二维数组,第一种是按行遍历,第二种是按列遍历。如果二维数组是全连续的,那么两种遍历方式的耗时应该是一致的,如果有差,就证明了二维数组不是全连续的,而是分块连续的。

public class Test2{
	private static int SIZE = 9999;

    public static void main(String[] args){ ;
        int[][] arr1 = new int[SIZE][SIZE];
        long currTime=System.currentTimeMillis();
        travelByRow(arr1);
        System.out.println("Total time in ByRow : "+(System.currentTimeMillis()-currTime)+" ms");

        int[][] arr2 = new int[SIZE][SIZE];
        currTime=System.currentTimeMillis();
        travelByCol(arr2);
        System.out.println("Total time in ByCol : "+(System.currentTimeMillis()-currTime)+" ms");
    }
    public static void travelByRow(int[][] arr){
        for(int i = 0; i < arr[0].length;i++){
            for(int j = 0;j < arr.length;j++){
                arr[j][i] = 1;
            }
        }
    }

    public static void travelByCol(int[][] arr){
        for(int i = 0; i < arr.length;i++){
            for(int j = 0;j < arr[0].length;j++)
                arr[i][j] = 1;
        }
    }
}

结果输出为:

Total time in ByRow : 108 ms
Total time in ByCol : 2489 ms

从结果中同时也说明了多维数组不同的遍历方式其性能不不一致。

三、数组及其实现容器的对比

java中有许多容器都是由数组实现的,比如ArrayList。ArrayList的好处就是它封装了数组的许多操作,大大减少了开发过程中的繁琐程度,同时还支持了自动扩容,这样开发人员值需要注重其功能的使用,而不需要去关心数组的具体操作,可以提高开发效率。但是,我们就可以不使用数组而改用容器了吗?不是的,虽然容器的操作简单粗暴省时省力(比如扩容过程),但是会损耗一点点性能。那么何时选择数组,何时选择容器,可以参考一下几点经验:

  1. 容器的存储类型必须是一个对象,不能存储基本数据类型,如果要存储,就要用到基本数据类型的包装类,但是使用的过程中会涉及装箱、拆箱的过程,对性能有所影响,若对性能要求没有特别的苛刻,可以选择容器。
  2. 如果预先知道数据的大小,就可以使用数组。
  3. 若要使用多维数组,还是用数组比较直观。
  4. 如果是做底层、架构搭建,追求极致的性能,减一使用基本的数据结构,比如数组;如果是做上层的开发,可以使用容器,提升开发效率。

本文参考王争的《数据结构与算法之美》,倾情推荐

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值