数组

注:该博客大量参考(抄袭)了极客时间上王争老师的《算法和数据结构之美》课程的课程文稿,侵删

1. 数组

数组(Array)是一种线性表数据结构。它用一组连续的内存空间,来存储一组具有相同类型的数据

数组支持随机访问。然而对数组进行删除、插入确实非常低效的,为了保证连续性,就需要做大量的数据搬移工作

1.1 数组是如何实现根据下标随机访问数组元素的?

计算机会给每一个内存单位分配一个地址,计算机通过地址来访问内存中的数据。当计算机需要随机访问数组中的某个元素时,它会根据寻址公式,计算出该元素存储的内存地址:

address_i = address_0 + i * typeSize

以一段代码来说明

public static void main(String[] args) {
        int[] arr = new int[10];
        System.out.println(arr);
    }
// output
// [I@77459877

[代表数组,I代表int类型,@分隔符,后面内存地址,也就是说array是一种引用类型,它记录着起始地址(base_address/address_0)、数据类型(int 4个byte),也就是说 data_type_size是4个字节,通过下标访问时,通过算出对应的地址,就可以访问内存中的数据了

根据随机访问的特点,数组这种结构适合大量查找的业务场景

「数组支持随机访问,根据下标随机访问的时间复杂度为O(1)

1.2 低效的“插入”和“删除”

  • 「插入」

    • 有序数组:

      假设一个数组的长度是n,现在需要将一个数据插入到数组中的第k个位置,为了把第k个位置腾出来,需要把后面的k~n都往后移一位。在最好的情况下,即插入到最后一个位置,时间复杂度是O(1)。在最坏的情况下,插入第一个位置,O(n)。平均情况下,(1+2+...n)/n = O(n)

    • 无序数组

      在这种情况下,数组知识被当做一个存储数据的集合。直接将第k位的数据搬移到数组元素的最后,然后把新的元素放在第k个位置,O(1)

  • 「删除」

    跟插入数据类似,如果要删除第k个位置的数据,为了内存的连续性,也需要搬移数据,不然中间就会出现空洞,内存就不连续了

    时间复杂度和插入的时候分析一样,但是在某些特殊场景下,我们并不一定非得追求数组中数据的连续性。可以将多次删除操作集中在一起执行

    记录下已经删除的数据,每次删除操作并不是真正地搬移数据,只是记录数据已经被删除。当数组没有空间存储数据时,再触发执行一次真正的删除操作,这样就大大减少了删除操作导致的数据搬移。

1. 3 容器能否完全替代数组

ArrayList相较于Array,有以下优势

  1. 可以将很多数组的操作细节封装起来,比如数组的插入、删除数据时需要搬移其他数据等
public static void main(String[] args) {
        int[] arr = new int[]{1, 2, 3, 4, 5};
        System.out.println(Arrays.toString(arr));
        insert(2, 10, arr);
        System.out.println(Arrays.toString(arr));
    }

    static void insert(int idx, int elem, int[] arr) {
        arr[idx] = elem;
    }
// output
// [1, 2, 3, 4, 5]
// [1, 2, 10, 4, 5]
public static void main(String[] args) {
        ArrayList<Integer> arrayList = new ArrayList<>(Arrays.asList(1, 2, 3, 4, 5));
        System.out.println(arrayList.toString());
        arrayList.add(2, 10);
        System.out.println(arrayList.toString());
    }
// output
// [1, 2, 3, 4, 5]
// [1, 2, 10, 3, 4, 5]
  1. 支持动态扩容,每次存储空间不够的时候,他都会将空间自动扩容为1.5倍大小

但是扩容操作涉及内存申请和数据搬移,是比较耗时的,所以,如果实现能确定需要存储的数据大小,最好在创建ArrayList的时候事先指定数据大小

1.4 为什么大多数编程语言中,数组要从0开始,而不是从1开始?

因为“下标”最确切的定义是“偏移(offset)”。从0开始,计算第i个元素的内存地址只需要用以下公式

\[address_i = address_0 + i * typeSize \]

如果从1开始,计算公式就变为了

\[address_i = address_1 = (i - 1)*typeSize \]

可以看到,从1开始编号,每次随机访问数组元素都多了一次减法运算,对于CPU来说,就是多了一次减法指令

1.5 总结

数组是用一块连续的内存空间,来存储相同类型的一组数据,最大的特点就是支持随机访问。但是插入、删除操作也因此变得低效,平均时间复杂度为O(n)。在平时的业务开发中,我们可以直接使用编程语言提供的容器类,但是,如果是特别底层的开发,直接使用数组可能会更加合适

posted @ 2020-08-14 18:28  宗吾先生  阅读(139)  评论(0编辑  收藏  举报