数据结构基础篇-线性表-数组

原文地址
数组可以说使我们编程中最常见的数据结构了,在代码中随处可见。我们可以很容易使用类似int[] ints = new int[4];这样的语法构建一个整数数组,那么你了解数组底层的原理吗?接下来我们就来结合数组的特点分析一下数组的原理,并实现一个简单的数组来帮助大家理解的原理。

数组的特点

高效的随机访问特性,使数组能够根据下标以O(1)的时间复杂度随机访问数组中的任意元素(请注意这里随机访问不是查询哦)。
相应的,为满足随机访问的特性,需要作出一些牺牲,对数组的元素做一些限制:

  1. 数组需要占用一块连续的物理内存
  2. 数组中元素的类型必须是一致的

只有满足了这两点,才能实现随机访问的特性。

数组如何实现随机访问的

由于数组分配在连续的内存中,我们可以很容易的通过起始地址 + 元素偏移量来定位具体的数组元素的内存地址,然后通过内存地址获取该地址保存的元素:

  1. 数组起始内存地址,即为数组分配的连续内存的起始地址
  2. 数组元素偏移量,即为该元素的下标 * 元素类型的长度
    所以我们得出以下公式:
下标为i的元素的内存地址 = 数组起始内存地址 + 元素类型长度 * i
复制代码

可以看到,数组根据下标访问元素时,只需要两次数值计算和一次内存读取操作,非常的高效。

数组的优点和缺点

我们先讨论优点:

  1. 随机访问特性能保证快速访问(包括修改)对应下标的数组元素,这项操作的代价很小(O(1))。
  2. CPU缓冲行的特性能够加速数组的访问,当然缓冲行不是我们这篇文章讨论的重点,我们可以简单的认为,CPU是对数组友好的,当我们访问一个数组元素时,CPU会机智的把这个元素后面的好多个元素一起给你,这样在你访问下一个元素时可能就不需要再去内存中读取了。

那么数组有什么缺点呢:

  1. 数组在初始化的时候必须指定容量,这就很不灵活,如果分配的太大,就会浪费空间,如果分配的太小,就需要动态扩容,而数组的扩容需要重新分配一块更大的空间,然后拷贝元素,很显然,这是一项O(n)时间复杂度的操作。
  2. 数组必须分配连续的内存,如果内存碎片过多,可能会导致内存分配失败。

这里需要补充一点: 理论上,数组的插入和删除操作因为涉及到元素的移动,所以时间复杂度是O(n)的,要差于链表在已知节点情况下的插入和删除操作,数组和链表的查询操作都需要遍历匹配,所以时间复杂度都是O(n)的。
但是事实上,得益于CPU的缓冲行机制,数组的连续存储优势会体现的非常明显,在大部分情况下(元素数量未达到一定规模的时候),数组的的插入、删除、查询都要快于链表,并且具备链表所没有的随机访问特性。

实现一个简单的int数组

理解了数组的原理,我们来动手实现一个int型数组,这里使用java实现,借助了Unsafe类来做内存分配。
源码可以查看我的github

public class IntArray {

    // 借助 sun.misc.Unsafe 直接分配内存
    private static final Unsafe UNSAFE = UnsafeUtil.getUnsafe();
    
    // int类型占用4个字节(byte)
    // 下标i的内存地址偏移量 == i * 4 == i << 2
    private static final int INDEX_SHIFT = 2;

    private final int length;

    // 该数组的内存起始地址
    private final long memoryAddress;

    /**
     * new int[length]
     */
    public IntArray(int length) {
        if (length < 0) {
            throw new IllegalArgumentException(String.valueOf(length));
        }

        this.length = length;
        // 分配一块连续的内存,保存这块内存的起始地址
        memoryAddress = UNSAFE.allocateMemory(length << INDEX_SHIFT);
        // 初始化这块内存
        UNSAFE.setMemory(memoryAddress, length << INDEX_SHIFT, (byte) 0);
    }

    /**
     * array[index] = data
     */
    public void set(int index, int data) {
        ensureIndexRange(index);
        // 根据 起始地址+数组下标*类型长度 计算出内存地址,然后将data保存到该地址
        UNSAFE.putInt(memoryAddress + (index << INDEX_SHIFT), data);
    }

    /**
     * array[index]
     */
    public int get(int index) {
        ensureIndexRange(index);
        // 根据 起始地址+数组下标*类型长度 计算出内存地址,然后获取该地址的int值
        return UNSAFE.getInt(memoryAddress + (index << INDEX_SHIFT));
    }

    /**
     * array.length
     */
    public int length() {
        return length;
    }

    private void ensureIndexRange(int index) {
        if (index < 0 || index >= length) {
            throw new IndexOutOfBoundsException(String.valueOf(index));
        }
    }

}复制代码

转载于:https://juejin.im/post/5c99f79f5188252df268f9ab

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值