[数据结构与算法] 线性表之数组详解

一、什么是数组

1.1 定义

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

1.2 数组特性

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

  2. 连续的内存空间、相同类型的数据:因此可以随机访问,但为了保证内存的连续性,需要做大量的数据搬移工作,使增删等操作变得非常低效。

  3. 线性存储,程序执行效率高。

1.3 时间复杂度

插入元素:O(n),所插入位置后面的元素都要向后移动。

删除元素:O(n),所删除位置后面的元素都要向前移动。

访问元素:O(1),使用索引访问。

查找元素:与使用查找算法有关,如顺序查找、二分查找、插值查找等。

1.4 空间复杂度

数组只用来存储指定类型的数据,所以存储n个数据的空间复杂度为O(n)。

二、基本操作

2.1 插入数据

数组插入数据,特定场景,特殊对待,如下:

  • 数组需要保持有序:将插入位置k后面的数据元素依次后移,空出要插入的位置,再将新元素插入。O(n)

  • 数组不需要保持有序:将插入位置的旧元素移到最后,再在k处插入新元素。或直接将新元素放在最后。O(1)

2.2 删除数据

数组删除数据,特定场景,特殊对待,如下:

  • 考虑内存连续性

    • 数组需要保持有序:删除k位置元素后,将后面的元素依次前移一个位置。O(n)

    • 数组不需要保持有序:用最后一个元素覆盖要删除的元素。O(1)

  • 不考虑内存连续

    • 标记删除法,将要删除的元素标记(没有真正的删除),当数组空间不够时,一次性删除。但会造成内存不连续,如下图:

在这里插入图片描述

(一)存有abcdef的数组,将bce标记为删除(注意:此时数组的长度仍为6),此时想在数组末尾存入g

(二)因空间不够,将没有删除的元素df依次移动到a后面a[1]、a[2]的位置,并将数组长度标记为3(注意:此时a[3]、a[4]、a[5]的内存中仍存有def,只是不再计入数组长度)

(三)将g存入a[3]的位置。

这里澄清两个概念,数组空间数组长度

  • 数组空间:数组所占内存空间,不随插入或删除元素而改变,除非申请空间
  • 数组长度:数组所存储的数据元素个数,随插入或删除元素而改变

2.3 随机访问

以存储整型数据为例,

在这里插入图片描述

对于数组int a[],其地址为a,第一个数组元素的地址和数组的地址相等,第二个数据元素的地址为a+4(由于存储整型,所以加4),依次类推,每往后移动一个数据元素,地址加4,可见只要知道了数组的首地址,就知道了数组中每一个元素的地址,所以利用数组下标可以实现数组元素的随机访问。

随机访问寻址方法:a[i]_address = base_address + i * data_type_size

其中,base_address 为数组首地址,data_type_size 为每个元素的大小,依数据类型确定。

代码验证一下:

#include <stdio.h>

int main()
{
    int a[] = {0, 1, 2, 3};

    printf("   a.address: %p\n", a);
    for (int i = 0; i < 4; i++)
    { 
	printf("a[%d].address: %p\n", i, &a[i]);
    }
}

打印结果为:

   a.address: 0x7ffd46f51c50
a[0].address: 0x7ffd46f51c50
a[1].address: 0x7ffd46f51c54
a[2].address: 0x7ffd46f51c58
a[3].address: 0x7ffd46f51c5c

对于二维数组(多维数组),依然是线性存储的,寻址方式依然是通过元素下标。

二维数组寻址:对于 m * n 的二维数组, a [ i ] [ j ] ( i < m , j < n ) a [ i ][ j ] (i < m, j < n) a[i][j](i<m,j<n)的地址为:

a[i][j]_address = base_address + ( i * n + j) * data_type_size

2.4 查找数据

这里注意,查找和随机访问是不一样的,随机访问是给出索引(位置)返回数据元素的值,而查找一般是给出数据元素的值返回它的索引(位置)。

所以,数组查找操作的时间复杂度不是O(1),要看使用的查找算法,比如二分查找,时间复杂度为 O(logn)。

三、其他关于数组的问题

3.1 数组越界问题

数组申请的内存是有限的,如果访问的内存不属于数组,就会发生数组越界问题。

int main(int argc, char* argv[])
{
    int i = 0;
    int arr[3] = {0};
    for(; i<=3; i++)
    {
        arr[i] = 0;
        printf("hello world!\n");
    }
    return 0;
}

这一段程序数组越界了,输出结果和内存地址分配方式有关。

函数体内的局部变量存在栈上,且是连续压栈。在Linux进程的内存布局中,栈区在高地址空间,从高向低增长。变量iarr在相邻地址,在内存分配时,如果按照内存地址递减的方式进行分配,则iarr的地址大,那么arr越界正好访问到i。当然,前提是iarr元素同类型,否则那段代码仍是未决行为。这时无限循环输出hello world。

如果按照内存地址递增的方式进行分配,则iarr的地址小,那么arr越界正好访问不到i,这时只输出4次hello world。

3.2 数组下标为什么从0开始?

从数组存储的内存模型上来看,下标最准确的定义应该是偏移(offset)。第一个元素的偏移为0,第i个元素的偏移为i。但如果从1开始,第i个元素的偏移就为i-1,多了一次减法运算,这将降低CPU的运算效率。(对于底层算法开发,效率优化要做到极致。)

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

万俟淋曦

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值