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


    活动地址:CSDN21天学习挑战赛

    一、什么是数组

    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 
    
    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]);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    打印结果为:

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

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

    二维数组寻址:对于 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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

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

    函数体内的局部变量存在栈上,且是连续压栈。在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的运算效率。(对于底层算法开发,效率优化要做到极致。)

  • 相关阅读:
    企业微信机器人对接GPT
    【干货】VS2017简介、编译、启动单项目和启动多项目
    图片上怎么加文字?看完就你知道了
    5-FITC,5-FITC(isomer I),5-异硫氰酸荧光素,5-Flourescein iso-thiocyanate
    [NOIP2014 提高组] 生活大爆炸版石头剪刀布
    给新入坑的小伙伴们的郑氏Java上路指南
    6-5 头插法创建单链表(C) 分数 10
    [云原生] 二进制安装K8S(中)部署网络插件和DNS
    如何判断一个点在地图上?如何判断一个点在多边形内?
    「学习笔记」平衡树基础:Splay 和 Treap
  • 原文地址:https://blog.csdn.net/maizousidemao/article/details/126321843