现在位置: 首页 > C 语言数据结构与算法 > 正文

数据结构 - 数组

数组(Array)是最基础、也是最常用的数据结构之一。

数组是一组类型相同、在内存中连续存放的元素集合。


数组的定义与内存布局

在 C 语言中,数组声明后会在内存中分配一块连续的空间,每个元素可以通过下标直接访问。

数组内存布局示意图

上图展示了一个包含 5 个 int 元素的数组在内存中的布局。假设起始地址为 0x1000,每个 int 占 4 字节,则:

  • arr[0] 位于地址 0x1000
  • arr[1] 位于地址 0x1004(0x1000 + 4)
  • arr[i] 位于地址 0x1000 + i × 4

正是由于这种连续存储的特性,数组可以通过 起始地址 + 偏移量 的方式在 O(1) 时间内访问任意元素。

一维数组

实例

#include <stdio.h>

int main() {
    /* 声明并初始化一个一维数组 */
    int arr[5] = {10, 20, 30, 40, 50};

    /* 遍历访问每个元素 */
    printf("数组元素: ");
    for (int i = 0; i < 5; i++) {
        printf("%d ", arr[i]);
    }
    printf("\n");  /* 输出: 数组元素: 10 20 30 40 50 */

    /* 通过索引直接访问,时间复杂度 O(1) */
    printf("arr[2] = %d\n", arr[2]);  /* 输出: 30 */

    return 0;
}

多维数组

C 语言支持多维数组,最常见的是二维数组,常用于表示矩阵等表格数据。

二维数组在内存中仍然是按行连续存储的(行优先存储),只是通过行列下标的计算将其映射为二维视图。

实例

#include <stdio.h>

int main() {
    /* 声明 3 行 4 列的二维数组 */
    int matrix[3][4] = {
        {1,  2,  3,  4},
        {5,  6,  7,  8},
        {9, 10, 11, 12}
    };

    /* 遍历二维数组:外层循环遍历行,内层循环遍历列 */
    printf("二维数组 (3×4 矩阵):\n");
    for (int i = 0; i < 3; i++) {        /* i 为行索引 */
        for (int j = 0; j < 4; j++) {    /* j 为列索引 */
            printf("%2d ", matrix[i][j]);
        }
        printf("\n");
    }

    /* 访问特定元素:matrix[行][列] */
    printf("matrix[1][2] = %d\n", matrix[1][2]);  /* 输出: 7 */
    return 0;
}

二维数组的内存地址计算公式:元素地址 = 起始地址 + (i × 列数 + j) × sizeof(元素类型)。其中 i 为行索引,j 为列索引。


数组的基本操作

数组插入和删除操作流程

上图展示了数组插入和删除操作的核心步骤:插入时需要将目标位置之后的元素依次后移以腾出空间;删除时需要将目标位置之后的元素依次前移以填补空缺。

遍历

实例

#include <stdio.h>

/* 遍历并打印数组所有元素
   时间复杂度:O(n),n 为数组长度 */

void traverse(int arr[], int n) {
    printf("遍历结果: ");
    for (int i = 0; i < n; i++) {
        printf("%d ", arr[i]);
    }
    printf("\n");
}

int main() {
    int arr[] = {15, 28, 43, 56, 71};
    int n = sizeof(arr) / sizeof(arr[0]);  /* 计算数组长度 */
    traverse(arr, n);
    return 0;
}

插入

实例

#include <stdio.h>

/* 在数组的指定位置插入元素
   参数:arr 数组, n 当前元素个数, pos 插入位置(0-based),
         value 插入值, capacity 数组总容量
   时间复杂度:O(n),最坏情况需要移动 n 个元素
   返回值:插入后的元素个数,容量不足返回 -1 */

int insert(int arr[], int n, int pos, int value, int capacity) {
    if (n >= capacity) {
        printf("数组已满,无法插入\n");
        return -1;
    }
    if (pos < 0 || pos > n) {
        printf("插入位置无效\n");
        return -1;
    }

    /* 从后向前,将 pos 及之后的元素依次后移一位 */
    for (int i = n; i > pos; i--) {
        arr[i] = arr[i - 1];
    }
    arr[pos] = value;  /* 在新位置写入数据 */
    return n + 1;      /* 元素个数加 1 */
}

int main() {
    int arr[10] = {10, 20, 30, 40};  /* 容量为 10,当前有 4 个元素 */
    int n = 4;

    printf("插入前: "); for (int i = 0; i < n; i++) printf("%d ", arr[i]);
    printf("\n");  /* 输出: 插入前: 10 20 30 40 */

    n = insert(arr, n, 2, 25, 10);  /* 在索引 2 处插入 25 */

    printf("插入后: "); for (int i = 0; i < n; i++) printf("%d ", arr[i]);
    printf("\n");  /* 输出: 插入后: 10 20 25 30 40 */
    return 0;
}

删除

实例

#include <stdio.h>

/* 删除数组指定位置的元素
   参数:arr 数组, n 当前元素个数, pos 删除位置(0-based)
   时间复杂度:O(n),最坏情况需要移动 n 个元素
   返回值:删除后的元素个数 */

int delete(int arr[], int n, int pos) {
    if (n <= 0) {
        printf("数组为空,无法删除\n");
        return 0;
    }
    if (pos < 0 || pos >= n) {
        printf("删除位置无效\n");
        return n;
    }

    /* 从前向后,将 pos 之后的元素依次前移一位 */
    for (int i = pos; i < n - 1; i++) {
        arr[i] = arr[i + 1];
    }
    return n - 1;  /* 元素个数减 1 */
}

int main() {
    int arr[10] = {10, 20, 30, 40, 50};
    int n = 5;

    printf("删除前: "); for (int i = 0; i < n; i++) printf("%d ", arr[i]);
    printf("\n");  /* 输出: 删除前: 10 20 30 40 50 */

    n = delete(arr, n, 2);  /* 删除索引 2 的元素 (30) */

    printf("删除后: "); for (int i = 0; i < n; i++) printf("%d ", arr[i]);
    printf("\n");  /* 输出: 删除后: 10 20 40 50 */
    return 0;
}

查找

在一个无序数组中查找目标值,最直接的方法是遍历整个数组逐一比较,这称为线性查找。

实例

#include <stdio.h>

/* 线性查找:在数组中搜索目标值
   时间复杂度:O(n),最坏情况需要遍历整个数组
   返回值:目标元素的索引,未找到返回 -1 */

int search(int arr[], int n, int target) {
    for (int i = 0; i < n; i++) {
        if (arr[i] == target) {
            return i;  /* 找到,返回索引 */
        }
    }
    return -1;  /* 未找到 */
}

int main() {
    int arr[] = {43, 17, 89, 25, 61};
    int n = 5;

    int idx = search(arr, n, 89);
    if (idx != -1) {
        printf("找到目标值 89,索引为 %d\n", idx);  /* 输出: 找到目标值 89,索引为 2 */
    } else {
        printf("未找到目标值\n");
    }
    return 0;
}

数组的优缺点

优点说明
O(1) 随机访问通过下标可以在常数时间内访问任意元素,这是数组最大的优势
内存连续连续存储有利于 CPU 缓存命中(空间局部性),提升访问效率
实现简单语法直观,是大多数编程语言的原生支持类型
缺点说明
大小固定C 语言中的静态数组在编译时就确定了大小,无法动态扩容
插入/删除慢在数组中间插入或删除元素时,需要移动大量元素,时间复杂度 O(n)
空间浪费如果声明的数组过大而实际使用较少,会造成内存浪费

正是这些局限性,催生了下一章将要学习的更加灵活的动态结构——链表


操作复杂度总结

操作时间复杂度说明
按索引访问O(1)直接计算地址
在末尾插入O(1)不需要移动元素
在头部/中间插入O(n)需要移动后续元素
删除末尾元素O(1)不需要移动元素
删除头部/中间元素O(n)需要移动后续元素
线性查找O(n)逐个比较
二分查找(有序数组)O(log n)要求数组已排序