数据结构初阶|数组:最基础却最不可或缺的“数据容器”
文章目录
前言
在数据结构的世界里,有这样一种存在——它简单到入门即会,却核心到贯穿整个编程生涯;它是复杂数据结构(树、图、哈希表)的基础,也是日常开发中最常用的“数据载体”,它就是「数组(Array)」。
很多新手刚接触编程时,其实已经不知不觉用过数组:比如用Python存一组学生成绩、用Java定义一个整数列表,本质上都是在使用数组。但多数人只停留在“会用”的层面,却不懂它的底层逻辑、优缺点和适用场景,导致写出来的代码看似能运行,却在效率上大打折扣。
今天这篇博客,专门拆解「数组」的核心知识点,从“是什么、底层怎么存、优缺点、适用场景,到新手避坑”,全程用通俗语言+简单示例,哪怕是零基础,也能彻底吃透数组,真正做到“会用、会选、会优化”。
一、先搞懂:数组到底是什么?
官方定义很抽象:数组是由相同数据类型的元素,按连续的内存地址存储组成的线性表。其实不用死记硬背,我们用一个生活中的例子就能秒懂——数组就像“一排整齐的抽屉”。
假设你有5个抽屉,从左到右依次编号0、1、2、3、4(注意:数组的下标大多从0开始,这是新手最容易踩的第一个坑),每个抽屉里只能放一个相同类型的“物品”(比如都是整数、都是字符串),不能混放。你要找某个抽屉里的物品,只要知道它的编号(下标),就能直接打开抽屉拿到;但如果想在两个抽屉中间加一个新抽屉,就需要把后面所有的抽屉都往后挪,才能腾出位置——这就是数组的核心本质。
再举一个编程中的简单示例(Python):
# 定义一个存储学生成绩的数组(Python中list本质是动态数组)
scores = [90, 85, 95, 80, 75]
# 访问下标为2的元素(第3个成绩)
print(scores[2]) # 输出:95
这段代码里,scores就是一个数组,里面的每个元素都是整数(相同类型),我们通过下标2,就能直接访问到95这个元素,这就是数组最核心的优势——快速访问。
二、底层逻辑:为什么数组能“快速访问”?
新手最疑惑的一点:同样是存数据,为什么数组查询起来比链表快?答案就藏在「连续内存存储」和「下标计算」里。
当我们定义一个数组时,计算机都会做两件事:
-
分配一块「连续的内存空间」:比如我们定义一个int类型的数组(int占4个字节),长度为5,计算机就会分配5×4=20个连续的字节,用来存储这5个整数,不会出现内存碎片化的情况;
-
记录数组的「起始内存地址」和「元素类型大小」:假设数组的起始地址是1000,那么每个元素的地址都能通过公式计算出来:元素地址 = 起始地址 + 下标 × 元素类型大小。
举个具体的例子(int数组,起始地址1000):
-
下标0的元素地址:1000 + 0×4 = 1000
-
下标2的元素地址:1000 + 2×4 = 1008
-
下标4的元素地址:1000 + 4×4 = 1016
也就是说,计算机不需要遍历所有元素,只要通过公式计算,就能瞬间找到目标元素的内存地址,直接读取数据——这就是数组「随机访问效率极高」的底层原因,其时间复杂度是O(1)(无论数组有多少个元素,访问某个元素的时间都是固定的)。
这里要区分两个概念:「随机访问」和「顺序访问」:
-
随机访问:直接通过下标找到元素(数组专属优势);
-
顺序访问:从第一个元素开始,一个个遍历,直到找到目标(比如链表)。
三、数组的优缺点:没有完美的结构,只有合适的场景
数组虽然基础,但并不是万能的,它的优缺点非常鲜明,掌握这些,才能在开发中正确选择使用数组。
1. 核心优点
-
随机访问效率极高:如前文所说,通过下标计算地址,瞬间访问元素,时间复杂度O(1),这是数组最核心的优势,也是它无法被其他基础结构替代的原因;
-
内存利用率高:数组是连续存储的,不需要额外存储“连接信息”(比如链表的指针域),只需要存储元素本身,内存开销小;
-
实现简单、操作便捷:几乎所有编程语言都原生支持数组,定义和使用都非常简单,无需额外封装复杂的逻辑。
2. 明显缺点
-
插入、删除效率低:因为数组是连续存储的,一旦在中间插入或删除元素,后面所有的元素都需要依次移动,来保证内存的连续性。比如在长度为5的数组中间插入一个元素,需要移动3个元素;数组越长,移动的元素越多,时间复杂度是O(n)(n是数组长度);
-
长度固定(静态数组):传统的静态数组(比如C语言中的数组),初始化时必须指定长度,一旦定义,长度就无法修改。如果数据量超过数组长度,就会出现溢出;如果数据量不足,又会浪费内存;
-
只能存储相同类型的元素:数组的所有元素必须是同一种数据类型,不能同时存储整数、字符串、布尔值等(Python中的list是动态数组,看似可以存不同类型,本质上是对底层数组的封装,并非原生数组的特性)。
补充:静态数组 vs 动态数组
新手很容易混淆这两个概念,这里简单区分一下,避免踩坑:
-
静态数组:长度固定,初始化时指定,无法动态扩容/缩容(如C语言:int arr[5] = {1,2,3,4,5});
-
动态数组:长度可以动态变化,底层还是静态数组,当数组存满时,会自动分配一块更大的连续内存,将原数组的元素复制过去(如Python的list、Java的ArrayList)。
动态数组解决了静态数组“长度固定”的问题,但插入、删除效率低的缺点依然存在——因为本质上还是基于连续内存存储的。
四、数组的适用场景:什么时候该用数组?
结合数组的优缺点,我们可以明确它的适用场景,核心原则是:数据量固定/变化少、需要频繁查询、很少插入/删除。
举几个日常开发和学习中的常见场景:
-
存储固定数量的同类数据:比如存班级30个学生的成绩、存一周7天的气温、存一个用户的5个常用地址——这些数据量固定,且需要频繁查询(比如查某天的气温、某个学生的成绩);
-
需要快速随机访问的场景:比如实现一个简单的排行榜(存前10名的分数,通过下标快速获取第3名、第5名)、存验证码的6位数字(通过下标快速读取每一位);
-
作为复杂数据结构的基础:比如哈希表的底层实现(开放地址法)、堆排序的底层存储、字符串的底层实现(本质是字符数组)——这些复杂结构,都依赖数组的连续存储和快速访问特性。
反例:如果需要频繁插入、删除数据(比如聊天记录、购物车商品),就不适合用数组,应该选择链表等更合适的数据结构。
五、新手必避的5个数组坑
数组虽然简单,但新手很容易在细节上踩坑,总结了5个最常见的坑,避开这些,你的数组使用会更规范、更高效。
-
下标越界(最常见):数组的下标从0开始,最大下标是“数组长度-1”。比如长度为5的数组,下标最大是4,如果你访问下标5,就会出现下标越界错误(Python中是IndexError,Java中是ArrayIndexOutOfBoundsException);
-
混淆静态数组和动态数组:比如在C语言中,误以为静态数组可以直接扩容,导致出现内存溢出;在Python中,误以为list是原生数组,随意存不同类型的元素,影响代码效率;
-
频繁插入/删除时用数组:比如用数组实现购物车,频繁添加、删除商品,导致代码效率极低——这种场景,链表或集合(Set)会更合适;
-
忽略数组的“相同类型”特性:比如在Java中,定义int类型数组,却试图存入字符串,导致编译错误;即使是Python的list,也不建议混存不同类型,会增加内存开销;
-
误以为数组一定比其他结构好:数组只是基础结构,没有绝对的优势,选择结构的核心是“适配场景”——比如小数据量、频繁删改,链表比数组更高效。
六、新手练习:3个简单数组实操(附C/C++/Python代码)
学数据结构,光看概念不够,实操一遍,比背10遍理论都管用。这里给新手准备了3个简单的数组实操练习,分别用C/C++/Python实现(Python的list是动态数组,上手简单),跟着写一遍,就能快速掌握数组的基本操作。
练习1:数组遍历(打印所有元素)
C语言:
# 定义一个int类型静态数组(长度5,初始化元素)
int arr[] = {10, 20, 30, 40, 50};
// 计算数组长度(元素个数 = 数组总字节数 / 单个元素字节数)
int len = sizeof(arr) / sizeof(arr[0]);
// 遍历方式1:for循环(推荐,简洁)
for (int i = 0; i < len; i++) {
printf("%d ", arr[i]); // 输出:10 20 30 40 50
}
printf("\n"); // 换行,区分两种遍历结果
// 遍历方式2:下标遍历(明确使用下标,适配需要下标操作的场景)
for (int i = 0; i < len; i++) {
printf("下标%d:%d\n", i, arr[i]);
}
C++:
# 定义一个int类型静态数组(长度5,初始化元素),C++兼容该定义方式
int arr[] = {10, 20, 30, 40, 50};
// 计算数组长度(C++可沿用该方法,与C语言一致:总字节数 / 单个元素字节数)
int len = sizeof(arr) / sizeof(arr[0]);
// C++输出必备头文件和命名空间(新手可直接复制,避免cout报错)
#include <iostream>
using namespace std;
// 遍历方式1:for循环(推荐,简洁),循环语法与C基本一致
for (int i = 0; i < len; i++) {
cout << arr[i] << " "; // C++用cout输出,替代C语言的printf,输出:10 20 30 40 50
}
cout << endl; // C++用endl换行,替代C语言的printf("\n"),区分两种遍历结果
// 遍历方式2:下标遍历(明确使用下标,适配需要下标操作的场景)
for (int i = 0; i < len; i++) {
cout << "下标" << i << ":" << arr[i] << endl; // 统一用cout输出,贴合C++语法
}
Python:
# 定义一个存储整数的数组(Python中list本质是动态数组,无需指定长度)
arr = [10, 20, 30, 40, 50]
# Python无需手动计算数组长度,直接用len()函数获取(简洁高效)
len_arr = len(arr)
# 遍历方式1:for循环(推荐,简洁,直接遍历数组元素)
for num in arr:
print(num, end=" ") # 用print输出,end=" "保证元素同行显示,输出:10 20 30 40 50
print() # 换行,区分两种遍历结果,替代C++的endl
# 遍历方式2:下标遍历(明确使用下标,适配需要下标操作的场景)
# 用range(len_arr)获取下标范围,对应C++的for循环下标逻辑
for i in range(len_arr):
print(f"下标{i}:{arr[i]}") # 用f-string格式化输出,贴合Python简洁语法
练习2:数组查找(找到目标元素的下标)
C语言:
# 数组查找函数:参数为数组、数组长度、目标值,返回目标下标(未找到返回-1)
int find_index(int arr[], int len, int target) {
// 遍历数组,逐个对比元素
for (int i = 0; i < len; i++) {
if (arr[i] == target) {
return i; // 找到目标元素,返回当前下标
}
}
return -1; // 遍历结束未找到,返回-1
}
// 主函数测试(新手可直接复制运行)
int main() {
int arr[] = {15, 25, 35, 45, 55}; // 定义待查找的数组
int target = 35; // 定义目标元素
int len = sizeof(arr) / sizeof(arr[0]); // 计算数组长度
// 调用查找函数,接收返回的下标
int index = find_index(arr, len, target);
printf("%d", index); // 输出:2(目标元素35的下标为2)
return 0;
}
C++:
# 数组查找函数:参数为数组、数组长度、目标值,返回目标下标(未找到返回-1)
// C++兼容数组参数传递方式,函数逻辑与原逻辑完全一致
int find_index(int arr[], int len, int target) {
// 遍历数组,逐个对比元素
for (int i = 0; i < len; i++) {
if (arr[i] == target) {
return i; // 找到目标元素,返回当前下标
}
}
return -1; // 遍历结束未找到,返回-1
}
// 主函数测试(新手可直接复制运行,标准C++语法)
#include <iostream> // C++输出必备头文件,替代C语言的stdio.h
using namespace std; // 简化cout使用,避免频繁写std::cout
int main() {
int arr[] = {15, 25, 35, 45, 55}; // 定义待查找的数组
int target = 35; // 定义目标元素
int len = sizeof(arr) / sizeof(arr[0]); // 计算数组长度,C++可沿用该方法
// 调用查找函数,接收返回的下标
int index = find_index(arr, len, target);
cout << index; // C++用cout输出,替代C语言的printf,输出:2(目标元素35的下标为2)
return 0;
}
Python:
# 数组查找函数:参数为数组、目标值,返回目标下标(未找到返回-1)
# Python无需手动传递数组长度,用len()函数直接获取,更简洁
def find_index(arr, target):
# 遍历数组,逐个对比元素(enumerate获取下标和对应元素,适配原逻辑)
for i, num in enumerate(arr):
if num == target:
return i # 找到目标元素,返回当前下标
return -1 # 遍历结束未找到,返回-1
# 测试代码(新手可直接复制运行,贴合Python简洁语法)
arr = [15, 25, 35, 45, 55] # 定义待查找的数组(Python list本质是动态数组)
target = 35 # 定义目标元素
# 调用查找函数,接收返回的下标
index = find_index(arr, target)
print(index) # 用Python的print输出,替代C++的cout,输出:2(目标元素35的下标为2)
练习3:数组插入(在指定下标插入元素)
C语言:
# 数组插入函数:C语言静态数组长度固定,插入需手动移动元素,参数为原数组、原长度、插入下标、插入值、接收结果的新数组
// 注意:C语言静态数组无法直接扩容,此处用新数组接收插入结果(长度=原长度+1)
void insert_element(int old_arr[], int old_len, int index, int value, int new_arr[]) {
// 1. 判断插入下标是否合法(0 ≤ 下标 ≤ 原数组长度,下标=原长度表示插在末尾)
if (index < 0 || index > old_len) {
printf("下标越界,插入失败\n");
// 下标非法时,将原数组元素复制到新数组,保持原数据不变
for (int i = 0; i < old_len; i++) {
new_arr[i] = old_arr[i];
}
return;
}
// 2. 复制原数组元素到新数组,插入位置前的元素直接复制
for (int i = 0; i < index; i++) {
new_arr[i] = old_arr[i];
}
// 3. 在指定下标插入目标元素
new_arr[index] = value;
// 4. 复制插入位置后的元素(需向后移动一位)
for (int i = index; i < old_len; i++) {
new_arr[i+1] = old_arr[i];
}
}
// 主函数测试(新手可直接复制运行)
int main() {
int old_arr[] = {1, 2, 3, 4}; // 原数组(静态数组,长度4)
int old_len = sizeof(old_arr) / sizeof(old_arr[0]); // 计算原数组长度
int new_len = old_len + 1; // 插入后新数组长度=原长度+1
int new_arr[new_len]; // 定义新数组,用于接收插入结果
int insert_index = 2; // 插入下标(对应原需求“下标2”)
int insert_value = 5; // 插入的元素值
// 调用插入函数,执行插入操作
insert_element(old_arr, old_len, insert_index, insert_value, new_arr);
// 打印插入后的新数组(输出:1 2 5 3 4)
for (int i = 0; i < new_len; i++) {
printf("%d ", new_arr[i]);
}
return 0;
}
C++:
# 数组插入函数:C++兼容静态数组操作,同时使用C++标准输出,参数与原逻辑一致
// 注意:C++静态数组同样无法直接扩容,此处用新数组接收插入结果(长度=原长度+1)
void insert_element(int old_arr[], int old_len, int index, int value, int new_arr[]) {
// 1. 判断插入下标是否合法(0 ≤ 下标 ≤ 原数组长度,下标=原长度表示插在末尾)
if (index < 0 || index > old_len) {
cout << "下标越界,插入失败" << endl; // C++用cout输出,endl换行,替代printf
// 下标非法时,将原数组元素复制到新数组,保持原数据不变
for (int i = 0; i < old_len; i++) {
new_arr[i] = old_arr[i];
}
return;
}
// 2. 复制原数组元素到新数组,插入位置前的元素直接复制
for (int i = 0; i < index; i++) {
new_arr[i] = old_arr[i];
}
// 3. 在指定下标插入目标元素
new_arr[index] = value;
// 4. 复制插入位置后的元素(需向后移动一位)
for (int i = index; i < old_len; i++) {
new_arr[i+1] = old_arr[i];
}
}
// 主函数测试(新手可直接复制运行,标准C++语法)
#include <iostream> // C++输出必备头文件,替代C语言的stdio.h
using namespace std; // 简化cout使用,避免频繁写std::cout
int main() {
int old_arr[] = {1, 2, 3, 4}; // 原数组(静态数组,长度4)
int old_len = sizeof(old_arr) / sizeof(old_arr[0]); // 计算原数组长度,C++可沿用该方法
int new_len = old_len + 1; // 插入后新数组长度=原长度+1
int new_arr[new_len]; // 定义新数组,用于接收插入结果
int insert_index = 2; // 插入下标(对应原需求“下标2”)
int insert_value = 5; // 插入的元素值
// 调用插入函数,执行插入操作,逻辑与原C语言代码完全一致
insert_element(old_arr, old_len, insert_index, insert_value, new_arr);
// 打印插入后的新数组(输出:1 2 5 3 4),用cout替代printf
for (int i = 0; i < new_len; i++) {
cout << new_arr[i] << " ";
}
return 0;
}
Python:
# 数组插入函数:适配Python动态数组(list)特性,无需手动扩容,参数为数组、插入下标、插入值
# 注意:Python的list是动态数组,插入时会自动移动元素、扩容,无需手动定义新数组
def insert_element(arr, index, value):
# 1. 判断插入下标是否合法(0 ≤ 下标 ≤ 数组长度,下标=数组长度表示插在末尾)
if index < 0 or index > len(arr):
print("下标越界,插入失败") # Python用print输出,贴合前文练习语法
return arr # 下标非法时,返回原数组,保持数据不变
# 2. 在指定下标插入目标元素(Python list的insert方法,底层自动移动元素)
# 无需手动复制元素、扩容,简化操作,贴合Python简洁特性
arr.insert(index, value)
return arr
# 测试代码(新手可直接复制运行,与前两个练习语法、风格统一)
arr = [1, 2, 3, 4] # 原数组(Python list本质是动态数组,无需指定长度)
insert_index = 2 # 插入下标(对应原需求“下标2”)
insert_value = 5 # 插入的元素值
# 调用插入函数,执行插入操作,接收返回的新数组
new_arr = insert_element(arr, insert_index, insert_value)
# 打印插入后的新数组(输出:1 2 5 3 4),与前文练习输出格式一致
print(" ".join(map(str, new_arr))) # 格式化输出,保证元素同行显示
总结
数组是数据结构的“入门基石”,它看似简单,却承载着“连续存储”“随机访问”的核心思想——这些思想,会贯穿你后续学习所有复杂数据结构的过程。
学习数组,不用追求“精通底层源码”,重点是理解它的核心逻辑、优缺点和适用场景,能在合适的场景中正确使用数组,能完成简单的实操练习,就是初阶阶段的胜利。
下一篇,我们会聊数组的“好搭档”——链表,看看它是如何解决数组“插入删除慢”的问题的。如果这篇博客对你有帮助,欢迎点赞收藏~ 也可以在评论区留言,说说你使用数组时遇到的坑,我们一起交流进步!
更多推荐
所有评论(0)