数据结构与算法 — 进一步认识数组

数组

提到数组,相信大家的都不陌生,毕竟每个编程语言都会有它的影子。

数组是最基础的数据结构,尽管数组看起来非常的基础简单,但这个基础的数据结构要掌握其精髓,也不是那么简单事。


开门见山

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

这个定义有几个关键词,也是数组的精髓所在。下面就从这几个关键词进一步理解数组。

第一个是线性表。顾名思义,线性表的特征就是数据排成像一条线一样的结构。每个线性表的数据最多只有前和后两个方向。除了数组,链表、队列、栈等数据结构也是线性表结构。

举个栗子,糖葫芦串就与线性表的特征非常相似。糖葫芦(数据)串成在一条直线的竹签,并且每个糖葫芦(数据)最多只有前和后两个方向。

第二个是连续的内存空间和相同的类型的数据。因为这两个条件的限制,数组有了非常重要的特性:随机访问元素,随机访问元素的时间复杂度为O(1)。但有利必有弊,这两个条件的限制导致数据在进行插入和删除一个数据的时候,为了保证数据的连续性,就需要做数据的搬移操作。


随机访问

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

我们拿一个长度为5的int类型的数组int a[5],来举例子。在我们定义这个数组时,计算机会给数组int a[5],分配了一块连续的内存空间。

假设,数组int a[5]内存块的首地址为base_address=100,那么

  • a[0]的地址就是100(首地址)
  • a[2]的地址就是104
  • a[3]的地址就是108
  • a[3]的地址就是112
  • a[4]的地址就是116

计算机是通过访内存地址,来访问内存中存储的数据。那么,当计算机要随机访问数组中的某个元素时,会通过下面这条寻址公式,计算出对应元素的内存地址,从而通过内存地址访问数据。

a[i]_address = base_address + i * data_type_size

a[i]_address表示对应数组下标的内存地址,data_type_size表示数组存储的数据类型的大小,数组int a[5]。存储的是5个int类型的数据,它的data_type_size就为4个字节。

二位数组的寻址公式,假设二位数组的维度是m*n,则公式为:

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


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

要先解答这个问题时,我们试想假设数组下标从1开始,a[1]表示数组的首地址,那么计算机的寻址公式就会变成为:

a[index]_address = base_address + (i - 1) * data_type_size

对比数组下标从0开始和设数组下标从1开始的寻址公式,我们不难看出,从1开始编号,每次随机访问数组元素都多了一次减法运算,对于CPU来说,就是多了一次减法指令。

更何况数组是非常基础的数据结构,使用频率非常的高,所以效率优化必须要做到极致。所以为了减少CPU的一次减法指令,数组选择了从0开始编号,而不是从1开始。

二位数组

以上是从计算机寻址公式角度分析的,当然其实还有历史等原因。


数组的插入和删除过程

前面提到对于数组的定义,数组为了保持内存数据的连续性,就会导致插入和删除这两个操作比比较低效。接下来通过代码来阐述为什么导致低效呢?又有哪些方法改进?

插入操作过程

插入操作对于数据的不同的场景和不同的插入位置,时间复杂度都略有不同。接下来以数组的数据是有序和没有规律的两种场景分析插入操作。

不管什么场景,如果在数组的末尾插入元素,那么就非常简单,不需要搬移数据,直接将元素放入到数组的末尾,这时空间复杂度就为O(1)。

如果在数组的开头或中间插入数据呢?这时可以根据场景的不同,采用不同的方式。

如果数组的数据是有序(从小到大或从大到小),在第k位置插入一个新的元素时,就必须把k之后的数据往后移动一位,此时最坏时间复杂度是O(n)。

如果数组的数据没有任何规律,那么在第k位置插入一个新的元素时,先将旧的第k位置的数据搬移到数据末尾,在把新的元素数据直接放入到第k位置。那么在这种特定场景下,在第k个位置插入一个元素的时间复杂度就为O(1)。

一图胜千言,我们以图的方式展现数组的数据是有序和没有规律场景的插入元素的过程。

删除操作过程

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

如果删除数组末尾的数据,则时间复杂度为O(1);如果删除开头的数据,因需把k位置之后的数据往前搬移一位,那么时间复杂度就为O(n)。

一图胜千言,我们以图的方式展现数组删除操作。


代码实战数组插入、删除和查询

本例子,以数组的数据是有序(数据是从小到大的顺序)的场景,实现数组的插入、删除和查询操作

先用结构体定义数组的属性,分别有数组的长度、被占用的个数和数组指针。

struct Array_t
{
    int length; // 数组长度
    int used;   // 被占用的个数
    int *arr;   // 数组地址
};

创建数组:

根据结构体设定的数组长度,创建对应连续空间并且相同类型的数组

void alloc(struct Array_t *array)
{
    array->arr = (int *)malloc(array->length * sizeof(int));
}

插入过程

  1. 判断数组占用个数是否超过数组长度
  2. 遍历数组,找到待插入新元素的下标idx
  3. 如果找到插入元素的下标不是末尾位置,则需要将idx数据依次往后搬移一位
  4. 在idx下标插入新元素,并将数组占用个数+1
/*
 *  插入新元素
 *  参数1:Array_t数组结构体指针
 *  参数2:新元素的值
 *  返回:成功返回插入的数组下标,失败返回-1
 */
int insertElem(struct Array_t *array, int elem)
{
    // 当数组被占用数大于等于数组长度时,说明数组所有下标都已存放数据了,无法在进行插入
    if (array->used >= array->length)
    {
        std::cout << "ERROR: array size is full, can't insert " << elem << " elem." << std::endl;
        return -1;
    }

    int idx = 0;

    // 遍历数组,找到大于新元素elem的下标idx
    for (idx = 0; idx < array->used; idx++)
    {
        // 如果找到数组元素的值大于新元素elem的值,则退出
        if (array->arr[idx] > elem)
        {
            break;
        }
    }

    // 如果插入的下标的位置不是在末尾,则需要把idx之后的
    // 数据依次往后搬移一位,空出下标为idx的元素待后续插入
    if (idx < array->used)
    {
        // 将idx之后的数据依次往后搬移一位
        memmove(&array->arr[idx + 1], &array->arr[idx], (array->used - idx) * sizeof(int));
    }

    // 插入元素
    array->arr[idx] = elem;
    // 被占用数自增
    array->used++;

    // 成功返回插入的数组下标
    return idx;
}

删除过程

  1. 判断待删除的下标是否合法
  2. 将待删除idx下标之后的数据往前搬移一位
/*
 *  删除新元素
 *  参数1:Array_t数组结构体指针
 *  参数2:删除元素的数组下标位置
 *  返回:成功返回0,失败返回-1
 */
int deleteElem(struct Array_t *array, int idx)
{
    // 判断下标位置是否合法
    if (idx < 0 || idx >= array->used)
    {
        std::cout << "ERROR:idx[" << idx << "] not in the range of arrays." << std::endl;
        return -1;
    }

    // 将idx下标之后的数据往前搬移一位
    memmove(&array->arr[idx], &array->arr[idx + 1], (array->used - idx - 1) * sizeof(int));

    // 数组占用个数减1
    array->used--;

    return 0;
}

查询下标

遍历数组,查询元素值的下标,若找到则返回数组元素;没找到则报错提示

/*
 *  查询元素下标
 *  参数1:Array_t数组结构体指针
 *  参数2:元素值
 *  返回:成功返回元素下标,失败返回-1
 */
int search(struct Array_t *array, int elem)
{
    int idx = 0;

    // 遍历数组
    for (idx = 0; idx < array->used; idx++)
    {
        // 找到与查询的元素值相同的数组元素,则返回元素下标
        if (array->arr[idx] == elem)
        {
            return idx;
        }

        // 如果数组元素大于新元素,说明未找到此数组下标, 则提前报错退出
        // 因为本例子的数组是有序从小到大的
        if (array->arr[idx] > elem)
        {
            break;
        }
    }

    // 遍历完,说明未找到此数组下标,则报错退出
    std::cout << "ERROR: No search to this" << elem << " elem." << std::endl;

    return -1;
}

打印数组

输出数组的每个元素

void dump(struct Array_t *array)
{
    int idx = 0;

    for (idx = 0; idx < array->used; idx++)
    {
        std::cout << "INFO: array[" << idx << "] : " << array->arr[idx] << std::endl;
    }
}

main函数

创建长度为3,类型为int的数组,并对数组插入元素、删除元素、查询元素和打印元素。

int main()
{
    struct Array_t array = {3, 0, NULL};

    int idx = 0;

    std::cout << "alloc array length: " << array.length << " size: " << array.length * sizeof(int) << std::endl;
    alloc(&array);
    if (!array.arr)
        return -1;

    std::cout << "insert 1 elem" << std::endl;
    insertElem(&array, 1);

    std::cout << "insert 0 elem" << std::endl;
    insertElem(&array, 0);

    std::cout << "insert 2 elem" << std::endl;
    insertElem(&array, 2);

    dump(&array);

    idx = search(&array, 1);
    std::cout << "1 elem  is at position " << idx << std::endl;

    idx = search(&array, 2);
    std::cout << "2 elem  is at position " << idx << std::endl;

    std::cout << "delect position [2] elem " << std::endl;
    deleteElem(&array, 2);

    dump(&array);

    return 0;
}

运行结果:

[[email protected] array]# ./array
alloc array length: 3 size: 12
insert 1 elem
insert 0 elem
insert 2 elem
INFO: array[0] : 0
INFO: array[1] : 1
INFO: array[2] : 2
1 elem  is at position 1
2 elem  is at position 2
delect position [2] elem
INFO: array[0] : 0
INFO: array[1] : 1

小结

数组是最基础、最简单的数据结构。数组用一块连续的内存空间,来存储相同类型的一组数据,最大的特点就是随机访问元素,并且时间复杂度为O(1)。但是插入、删除操作也因此比较低效,时间复杂度为O(n)。

声明:本文参考极客时间—数据结构与算法部分内容。

原文地址:https://www.cnblogs.com/xiaolincoding/p/11564454.html

时间: 2024-10-17 03:55:21

数据结构与算法 — 进一步认识数组的相关文章

【数据结构与算法01】数组

数组是应用最广泛的数据存储结构.它被植入到大部分的编程语言中,由于数组十分易懂,所以在这里就不赘述,主要附上两端代码,一个是普通的数组,另一个是有序数组.有序数组是按关键字升序(或降序)排列的,这种排列使快速查找数据项成为可能,即可以使用二分查找. 普通数组的Java代码: public class GeneralArray { private int[] a; private int size; //数组的大小 private int nElem; //数组中有多少项 public Gener

数据结构与算法-字符串输出数组中的最大值

输出数组a中的最大值及其下标 #include<stdio.h> #define N 5 int main() { int a[N],i,max,t; for(i=0;i<N;i++) scanf("%d",&a[i]); max=a[0]; //把数组的第一个数赋值给max,此时对应的下标为0 t=0; for(i=1;i<N;i++) //从数组的第二个数开始判断,max是否是最大值 if(max<a[i]){ //不是最大值,就把该值赋值给m

数据结构与算法之有序数组(2)——in dart

本文比第一篇,采用了类实现.增加了运算符重载等功能.本来有序数组是不能修改某个位置的值的,因为这样会打破数组的有序性:但为了演示,保留了修改的方法,但为此增加了排序. 1 import 'dart:math' show Random; 2 3 final _rnd = Random(); 4 final _seed = 100; 5 6 class OrderedArray { 7 List<int> _array; 8 int _realLength; 9 10 OrderedArray(i

[数据结构与算法] : 栈的数组实现

头文件 1 typedef int ElementType; 2 3 #ifndef _STACK_AR_ 4 #define _STACK_AR_ 5 6 struct StackRecord; 7 typedef struct StackRecord *Stack; 8 9 int IsEmpty(Stack S); 10 int IsFull(Stack S); 11 Stack CreateStack(int MaxElements); 12 void DisposeStack(Stac

数据结构与算法实例(数组实现)

数据结构与算法实例分析-数组 ★数组是一种最简单的数据结构,它占据一块连续的内存并且顺序存储数据,所以我们需要首先指定数组的大小 ★数组的空间效率不是很好,会有空闲的区域没有得到充分的应用 ★时间复杂度为O(1); ★数组一旦被定义,它的维度和维界就不会再改变,因此除了结构的初始化和销毁之外,数组就只有存取和修改元素值得操作 1.数组的存储结构 2.基本操作 ⑴.建造空间并进行初始化:struct triple triple_init(int v1,int v2,int v3); ⑵.释放已开辟

大数据就是这么任性第一季数据结构和算法(一线经验、权威资料、知识新鲜、实践性强、全程源码)

这门课程是针对大数据工程师和云计算工程师的基础课程,同时也是所有计算机专业人士必须掌握的一门课程. 如果不掌握数据结构和算法,你将难以掌握高效.专业的数据处理手段,更难以从容应对复杂的大数据处理场景. 请思考以下问题: 1.社交网站(如微博.facebook)中,人与人的关系是海量数据,你如何研究和处理此问题? 2.数据库的索引作用是什么?为什么利用哈希.B+树和堆表等数据结构来组织索引? 3.为什么Linux的虚拟内存管理模块,使用红黑树来处理VMA的查找? 4.为什么搜索引擎可以在毫秒级返回

数据结构与算法系列四(单链表)

1.引子 1.1.为什么要学习数据结构与算法? 有人说,数据结构与算法,计算机网络,与操作系统都一样,脱离日常开发,除了面试这辈子可能都用不到呀! 有人说,我是做业务开发的,只要熟练API,熟练框架,熟练各种中间件,写的代码不也能“飞”起来吗? 于是问题来了:为什么还要学习数据结构与算法呢? #理由一: 面试的时候,千万不要被数据结构与算法拖了后腿 #理由二: 你真的愿意做一辈子CRUD Boy吗 #理由三: 不想写出开源框架,中间件的工程师,不是好厨子 1.2.如何系统化学习数据结构与算法?

数据结构与算法系列研究四——数组和广义表

稀疏矩阵的十字链表实现和转置 一.数组和广义表的定义 数组的定义1:一个 N 维数组是受 N 组线性关系约束的线性表.           二维数组的逻辑结构可形式地描述为:           2_ARRAY(D,R)              其中 D={aij} | i=0,1,...,b1-1; j=0,1,...,b2-1;aij∈D0}              R={Row,Col}              Row={<aij,ai,j+1>|0<=i<=b1-1;

《数据结构、算法与应用》8.(顺序查找数组中第一个出现指定元素的位置)

最近在读<数据结构.算法与应用>这本书,把书上的习题总结一下,用自己的方法来实现了这些题,可能在效率,编码等方面存在着很多的问题,也可能是错误的实现,如果大家在看这本书的时候有更优更好的方法来实现,还请大家多多留言交流多多指正,谢谢 8. 从左至右检查数组a[0:n-1]中的元素,以查找雨x相等的那些元素.如果找到一个元素与x相等,则函数返回x第一次出现所在的位置.如果在数组中没有找到这样的元素,函数则返回-1. // // main.cpp // Test_08 // // Created