**本篇文章的目的:**让初学者理解数组和链表的核心概念、C语言实现方式,以及它们的优缺点和适用场景。
1. 为什么需要数组和链表?
想象你要管理一个班级的学生成绩:
- **场景1:**学生名单固定(如30人),且经常需要按学号快速查询成绩;
- **场景2:**学生名单频繁变动(如插班、退学),且需要经常在中间插入或删除成绩。
问题:如何高效存储和操作这些数据?
答案:
- **数组:**适合固定大小、随机访问的场景(如场景1)。
- **链表:**适合动态大小、频繁插入/删除的场景(如场景2)。
2. 数组(Array)------ 连续存储的"书架"
2.1 概念
2.1.1 定义
- 数组是一块连续的内存空间,用于存储相同类型的数据。
2.1.2 特点
- 随机访问快:通过下标(索引)直接访问元素(如scores[2]);
- 大小固定:创建时需指定长度(C语言中需手动管理内存);
- 插入/删除慢:在中间插入或删除元素时,需移动其他元素。
2.2 C语言实现
2.2.1 静态数组(大小固定)
#include <stdio.h>
int main() {
int scores[5] = {90, 85, 78, 92, 88}; // 定义一个长度为5的数组
print("第3位学生的成绩:%d\n", score[2]); // 输出78
return 0;
}
2.2.2 动态数组(大小可变,需手动管理内存)
#include <stdio.h>
#include <stdlib.h> // 包含malloc和free函数
int main() {
int n;
printf("输入学生人数:");
scanf("%d", &n);
int *scores = (int *)malloc(n * sizeof(int)); // 动态内存分配
if (scores = NULL) {
printf("内存分配失败!\n");
return 1;
}
// 输入成绩
for (int i = 0; i < n; i++) {
printf("输入第%d个学生的成绩:", i + 1);
scanf("%d", &scores[i]);
}
// 输出成绩
for (int i = 0; i < n; i++){
printf("第%d个学生的成绩:%d\n", i + 1, scores[i]);
}
free(scores); //释放内存
return 0;
}
2.3 数组的优缺点
| 优点 | 缺点 |
|---|---|
| 随机访问快(O(1)时间) | 大小固定,扩容成本高 |
| 内存连续,缓存友好 | 插入/删除需移动元素(O(n)) |
| 代码简单,易于实现 | 浪费空间(若分配过大) |
2.4 适用场景
- 数据大小已知且固定(如存储月份天数、棋盘格子)。
- 需要频繁按索引访问数据。
3. 链表(Linked List)------ 灵活串联的"便签绳"
3.1 概念
3.1.1 定义
- 链表由一系列节点组成,每个节点存储数据和指向下一个节点的指针。
3.1.2 特点
- 动态大小:无需预先分配固定空间,可随时插入/删除节点。
- 插入/删除快:只需修改指针,无需移动其他节点(O(1)时间、若已知位置)。
- 随机访问慢:需从头结点开始遍历(O(n)时间)。
3.2 C语言实现示例
3.2.1 单链表(插入、删除、遍历)
#include <stdio.h>
#include <stdlib.h>
typedef struct Node {
int data; // 数据域
struct Node *next // 指针域,指向下一个结点
}
// 在链表尾部插入结点
void appendNode(Node **head, int value) {
Node *newNode = (Node *)malloc(sizeof(Node));
newNode->data = valud;
newNode->next = NULL;
if (*head == NULL) {
*head = newNode;
} else {
Node *current = *head;
while (current->next != NULL) {
current = current->next;
}
current->next = newNode;
}
}
// 删除指定值的结点
void deleteNode(Node **head, int value) {
Node *temp = *head;
Node *prev = NULL;
if (temp != NULL && temp->data == value) {
*head = temp->next; // 删除头结点
free(temp);
return;
}
while(temp != NULL && temp->data != value) {
prev = temp;
temp = temp->next;
}
if(temp == NULL) return; //未找到
prev->next = temp->next;
free(temp);
}
// 遍历链表
void printfList(Node *head){
Node *current = head;
while (current != NULL) {
printf("%d -> ", current->data);
current = current->next;
}
printf("NULL\n");
}
int main() {
Node *head = NULL;
appendNode(&head, 10);
appendNode(&head, 20);
appendNode(&head, 30);
printfList(head); // 输出:10 -> 20 -> 30 -> NULL
deleteNode(&head, 20);
printfList(Head); // 输出:10 -> 30 -> NULL
return 0;
}
3.3 链表的优缺点
| 优点 | 缺点 |
|---|---|
| 动态大小,无需预先分配 | 随机访问慢(O(n)时间) |
| 插入/删除快(O(1)时间) | 额外空间存储指针(每个节点多4字节) |
| 不会浪费空间(按需分配) | 代码复杂,易出错(如指针操作) |
3.4 适用场景
- 数据大小未知或频繁变动(如操作系统中的任务队列)。
- 需要频繁在中间插入/删除数据(如音乐播放列表的顺序调整)。
4. 数组 VS 链表?(如何选择)
| 对比项 | 数组 | 链表 |
|---|---|---|
| 访问速度 | 快(直接索引) | 慢(需遍历) |
| 插入/删除速度 | 慢(需移动元素) | 快(修改指针) |
| 内存占用 | 连续内存,缓存友好 | 非连续内存,指针额外开销 |
| 实现难度 | 简单 | 较复杂(需管理指针) |
选择建议:
- 用数组:数据大小固定、需要频繁随机访问(如游戏中的地图数据)。
- 用链表:数据大小动态变化、需要频繁插入/删除(如浏览器历史记录)。
5. 拓展:动态数组和双向链表示例
5.1 实现一个动态数组:支持插入、删除、按索引访问元素。
#include <stdio.h>
#include <stdlib.h>
// 动态数组结构体
typedef struct {
int *data; // 存储数组元素的指针
int size; // 当前元素数量
int capacity; // 当前分配的容量
} DynamicArray;
// 初始化动态数组
DynamicArray* create_array(int initial_capacity) {
DynamicArray *arr = malloc(sizeof(DynamicArray));
arr->data = malloc(initial_capacity * sizeof(int));
arr->size = 0;
arr->capacity = initial_capacity > 0 ? initial_capacity : 10;
return arr;
}
// 调整数组容量
void resize_array(DynamicArray *arr, int new_capacity) {
int *new_data = realloc(arr->data, new_capacity * sizeof(int));
if (new_data) {
arr->data = new_data;
arr->capacity = new_capacity;
} else {
perror("Memory allocation failed");
exit(EXIT_FAILURE);
}
}
// 在指定位置插入元素
void insert_element(DynamicArray *arr, int index, int value) {
// 边界检查
if (index < 0 || index > arr->size) {
fprintf(stderr, "Error: Index out of bounds\n");
return;
}
// 扩容检查
if (arr->size == arr->capacity) {
resize_array(arr, arr->capacity * 2);
}
// 元素后移
for (int i = arr->size; i > index; i--) {
arr->data[i] = arr->data[i-1];
}
// 插入新元素
arr->data[index] = value;
arr->size++;
}
// 删除指定位置元素
void delete_element(DynamicArray *arr, int index) {
if (index < 0 || index >= arr->size) {
fprintf(stderr, "Error: Invalid delete index\n");
return;
}
// 元素前移
for (int i = index; i < arr->size - 1; i++) {
arr->data[i] = arr->data[i+1];
}
arr->size--;
arr->data[arr->size] = 0; // 清除旧值
// 缩容检查
if (arr->size > 0 && arr->size == arr->capacity / 4) {
resize_array(arr, arr->capacity / 2);
}
}
// 获取指定位置元素
int get_element(DynamicArray *arr, int index) {
if (index < 0 || index >= arr->size) {
fprintf(stderr, "Error: Invalid access index\n");
return -1; // 实际使用中应返回错误码
}
return arr->data[index];
}
// 释放动态数组内存
void free_array(DynamicArray *arr) {
free(arr->data);
free(arr);
}
// 测试代码
int main() {
printf("===== 动态数组测试 =====\n");
DynamicArray *arr = create_array(3);
// 测试插入
insert_element(arr, 0, 10);
insert_element(arr, 1, 20);
insert_element(arr, 0, 5); // 测试头部插入
printf("插入后数组: ");
for (int i = 0; i < arr->size; i++) {
printf("%d ", get_element(arr, i));
}
// 测试删除
delete_element(arr, 1); // 删除中间元素
printf("\n删除后数组: ");
for (int i = 0; i < arr->size; i++) {
printf("%d ", get_element(arr, i));
}
free_array(arr);
return 0;
}
5.2 实现双向链表:每个结点有prev和next指针,支持前后遍历。
#include <stdio.h>
#include <stdlib.h>
// 链表节点结构体
typedef struct Node {
int value;
struct Node *prev;
struct Node *next;
} Node;
// 双向链表结构体
typedef struct {
Node *head;
Node *tail;
int size;
} DoublyLinkedList;
// 创建新节点
Node* create_node(int value) {
Node *node = malloc(sizeof(Node));
node->value = value;
node->prev = NULL;
node->next = NULL;
return node;
}
// 初始化链表
DoublyLinkedList* create_list() {
DoublyLinkedList *list = malloc(sizeof(DoublyLinkedList));
list->head = NULL;
list->tail = NULL;
list->size = 0;
return list;
}
// 头部插入
void insert_head(DoublyLinkedList *list, int value) {
Node *node = create_node(value);
if (!list->head) {
list->head = list->tail = node;
} else {
node->next = list->head;
list->head->prev = node;
list->head = node;
}
list->size++;
}
// 尾部插入
void insert_tail(DoublyLinkedList *list, int value) {
Node *node = create_node(value);
if (!list->tail) {
list->head = list->tail = node;
} else {
node->prev = list->tail;
list->tail->next = node;
list->tail = node;
}
list->size++;
}
// 删除指定节点
void delete_node(DoublyLinkedList *list, Node *node) {
if (!node || list->size == 0) return;
if (node == list->head) {
list->head = node->next;
}
if (node == list->tail) {
list->tail = node->prev;
}
if (node->prev) node->prev->next = node->next;
if (node->next) node->next->prev = node->prev;
free(node);
list->size--;
}
// 双向遍历测试
void traverse_forward(DoublyLinkedList *list) {
printf("\n正向遍历: ");
Node *current = list->head;
while (current) {
printf("%d ", current->value);
current = current->next;
}
}
void traverse_backward(DoublyLinkedList *list) {
printf("\n反向遍历: ");
Node *current = list->tail;
while (current) {
printf("%d ", current->value);
current = current->prev;
}
}
// 释放链表内存
void free_list(DoublyLinkedList *list) {
Node *current = list->head;
while (current) {
Node *temp = current;
current = current->next;
free(temp);
}
free(list);
}
// 测试代码
int main() {
printf("\n\n===== 双向链表测试 =====\n");
DoublyLinkedList *list = create_list();
// 测试插入
insert_head(list, 30);
insert_tail(list, 40);
insert_tail(list, 50);
traverse_forward(list); // 30 40 50
traverse_backward(list); // 50 40 30
// 测试删除
Node *toDelete = list->head->next;
delete_node(list, toDelete);
traverse_forward(list); // 30 50
free_list(list);
return 0;
}