数据结构

147 阅读3分钟

数组

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

线性表:线性表就是数据排成像一条线一样的结构。每个线性表上的数据最多只有前和后两个方向。其实除了数组,链表、队列、栈等也是线性表结构。

非线性表:比如二叉树、堆、图等。之所以叫非线性,是因为,在非线性表中,数据之间并不是简单的前后关系。

连续的内存空间和相同类型的数据:正是这两个限制条件才使得数组具有“随机访问”的特性,但是这使得数据的删除和插入操作变的非常低效,当我们要删除和插入一个数据时,为了保持数据的连续性,就要做大量的数据搬移操作。

  • 时间复杂度

    1.删除:O(n);

    2.插入:O(n);

    3.根据下标随机访问:O(1)

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

从数组存储的内存模型上来看,“下标”最确切的定义应该是“偏移(offset)”。前面也 讲到,如果用 a 来表示数组的首地址,a[0] 就是偏移为 0 的位置,也就是首地址,a[k] 就 表示偏移 k 个 type_size 的位置,所以计算 a[k] 的内存地址只需要用这个公式:

a[k]_address = base_address + k * type_size

但是,如果数组从 1 开始计数,那我们计算数组元素 a[k] 的内存地址就会变为:

a[k]_address = base_address + (k-1)*type_size

对比两个公式,从 1 开始编号,每次随机访问数组元素都多了一次减法运 算,对于 CPU 来说,就是多了一次减法指令。

链表

链表:它不需要一组连续的内存空间,它通过“指针”将一组零散的内存块串联起来使用

  • 链表的分类

    1.单链表:尾节点指向空地址null

    2.循环链表:尾节点的指针指向链表的头节点

    3.双链表:它支持两个方向,每个结点不止有一个后继指针 next 指向后面的结点,还有一个前驱指针 prev 指向前面的结点,双向链表需要额外的两个空间来存储后继结点和前驱结点的地址。

  • 链表的时间复杂度

    1.删除:O(1);

    2.插入: O(1);

    3.根据下标随机访问:O(n);

数组VS链表

1.缓存方面:数组简单易用,在实现上使用的是连续的内存空间,可以借助CPU的缓存机制,预读数组中的数据,所以访问效率更高,而链表在内存中并不是连续存储的,所以对CPU缓存不友好,没办法有效预读。

2.动态扩容:数组的缺点是大小固定,一经声明就要占用整块连续内存空间。如果声明的数组过大,系统 可能没有足够的连续内存空间分配给它,导致“内存不足(out of memory)”。如果声 明的数组过小,则可能出现不够用的情况。这时只能再申请一个更大的内存空间,把原数组 拷贝进去,非常费时。链表本身没有大小的限制,天然地支持动态扩容。

3.内存:如果你的代码对内存的使用非常苛刻,那数组就更适合你。因为链表中的每个结 点都需要消耗额外的存储空间去存储一份指向下一个结点的指针,所以内存消耗会翻倍。而 且,对链表进行频繁的插入、删除操作,还会导致频繁的内存申请和释放,容易造成内存碎 片。