《C语言中线性表、顺序表与链表的教学规划》
![]()
🌟
各位朋友好,我是maomi_9526!🌍 若想栽一棵树,最早是在十年前,其次就是当下的时候!
🚀 今日来钻研C语言的相关知识。
👍 要是觉得这篇文章有帮助,欢迎您进行点赞、收藏并分享给更多人哦
目录
- 教学目标
- 教学重点与难点
- 教学方法
- 教学大纲
- 线性表(Linear List)
- 线性表的定义
- 线性表的基本特性
- 线性表的常见实现方式
- 顺序存储(顺序表)
- 链式存储(链表)
- 顺序表与链表的比较
- 顺序表操作
- 链表基本操作
- 顺序表(Sequence Table)
- 静态顺序表与动态顺序表
- 顺序表 vs 链表
- 对比分析
- 课堂练习
- 作业设计
- 教学总结
教学目标
- 领会线性表的逻辑构造以及其在物理层面的实现途径。
- 熟练掌握顺序表和链表的实现原理以及核心操作方法。
- 能够借助代码来实现顺序表和链表的基础功能。
- 剖析顺序表与链表在性能方面的不同之处以及各自适用的场景。
教学重点与难点
- 重点:顺序表的动态扩充过程、链表的指针运用方式、时间复杂度的分析方法。
- 难点:链表节点的插入和删除逻辑、顺序表与链表在性能上的权衡要点。
教学方法
理论讲解结合代码演示、对比分析以及课堂练习相结合的方式。
教学大纲
1. 线性表(Linear List)
1.1 线性表的定义
线性表是由若干个具备相同特性的元素所构成的有限序列,元素之间存在清晰的前后关系。每一个元素都拥有唯一的前驱和后继元素。线性表能够采用顺序存储或者链式存储的方式。
– 逻辑结构:线性表在逻辑上是按顺序排列的,意味着元素之间是依次排列的,每个元素都有着明确的前后关联。
– 物理结构:线性表的元素在计算机内存中的存储方式,既可以是连续的(顺序表),也可以是不连续的(链表)。
1.2 线性表的基本特性
- 元素的顺序性:线性表中元素的顺序关系决定了元素之间的前后依赖关系,一般我们按照从第一个元素到最后一个元素的顺序来进行访问。
- 唯一性:每个元素在序列里有唯一的前驱和后继元素,只有第一个元素没有前驱,最后一个元素没有后继。
- 有限性:线性表包含一个有限的元素集合,并且元素的数量是固定的。
1.3 线性表的常见实现方式
线性表存在两种常见的物理存储形式:顺序存储和链式存储。
1.3.1 顺序存储(顺序表)
顺序存储借助一段连续的内存空间来存储数据,通常利用数组来实现。数据元素在内存中的地址是连续的,所以能够通过索引直接进行访问。
– 优点:
– 高效的随机访问:能够通过索引直接访问任意位置的元素,时间复杂度为 O(1)。
– 空间效率高:在内存中是连续存储的,相对而言能够更高效地利用内存。
– 缺点:
– 插入和删除效率较低:在顺序表中间插入或者删除元素时,需要移动大量的元素,时间复杂度为 O(N)。
– 固定容量问题:顺序表的容量一旦确定,后期难以动态调整空间,需要运用动态扩容,但扩容可能会造成空间浪费。
1.3.2 链式存储(链表)
链表运用非连续的内存空间来存储数据,每个元素包含一个数据域以及一个指向下一个元素的指针(或者同时包含指向前后节点的指针),所以数据元素的地址不一定是连续的。
– 优点:
– 插入和删除效率高:链表仅需调整指针就能完成插入和删除操作,时间复杂度为 O(1)(无需移动元素)。
– 动态内存分配:链表不需要预先定义大小,内存空间能够依据实际需求动态分配,避免了空间浪费的情况。
– 缺点:
– 随机访问效率低:访问链表中的元素需要从头节点开始依次遍历,时间复杂度为 O(N)。
– 指针开销:每个节点需要额外的指针空间来存储前驱或后继元素。
1.4 顺序表与链表的比较
特性 | 顺序表 | 链表 |
---|---|---|
存储方式 | 使用连续的内存块(数组)存储元素 | 使用不连续的内存块(通过指针链接) |
随机访问 | O(1) | O(N) |
插入/删除操作 | O(N)(需要移动元素) | O(1)(只需修改指针) |
扩容/缩容 | 动态扩容(可能浪费空间) | 不需要扩容,按需分配空间 |
内存空间使用 | 存储空间固定,可能造成浪费 | 每个节点动态分配内存 |
应用场景 | 适合频繁访问的场景 | 适合频繁插入和删除的场景 |
内存开销 | 低,只有存储元素的数据 | 较高,每个节点还需要存储指针 |
1.5 顺序表操作
- 初始化:构建一个顺序表,设定初始的容量大小。
- 插入:把一个元素插入到指定的位置,这一过程涉及到元素的移动。
- 删除:删除指定位置的元素,同样涉及到元素的移动。
- 查找:依据索引来访问元素。
- 扩容:当顺序表的空间不够时,需要进行扩容操作,并将现有的数据复制到新的数组中。
代码示例(插入与删除):
// 在位置index插入元素
void insert(DynamicArray* arr, int index, int value) {
if (index < 0 || index > arr->size) {
printf("Index out of bounds\n");
return;
}
if (arr->size == arr->capacity) {
resize(arr, 2 * arr->capacity);
}
// 元素后移
for (int i = arr->size; i > index; i--) {
arr->array[i] = arr->array[i - 1];
}
arr->array[index] = value;
arr->size++;
}
// 删除位置index的元素
void delete(DynamicArray* arr, int index) {
if (index < 0 || index >= arr->size) {
printf("Index out of bounds\n");
return;
}
// 元素前移
for (int i = index; i < arr->size - 1; i++) {
arr->array[i] = arr->array[i + 1];
}
arr->size--;
}
1.6 链表基本操作
- 初始化:构建一个空链表,一般会使用虚拟头节点(dummy node)来简化操作。
- 插入:在指定的位置插入一个元素,需要修改指针的链接关系。
- 删除:删除指定位置的元素,同样要修改指针的链接关系。
- 查找:遍历链表来查找指定的元素。
- 反转:把链表的元素顺序进行反转,需要调整指针的指向。
代码示例(单向链表):
#include <stdio.h>
#include <stdlib.h>
struct ListNode {
int val;
struct ListNode* next;
};
// 创建链表的头插法
struct ListNode* create_linked_list_head(int* values, int size) {
struct ListNode* dummy = (struct ListNode*)malloc(sizeof(struct ListNode));
dummy->next = NULL;
for (int i = 0; i < size; i++) {
struct ListNode* new_node = (struct ListNode*)malloc(sizeof(struct ListNode));
new_node->val = values[i];
new_node->next = dummy->next;
dummy->next = new_node;
}
return dummy->next;
}
// 创建链表的尾插法
struct ListNode* create_linked_list_tail(int* values, int size) {
struct ListNode* dummy = (struct ListNode*)malloc(sizeof(struct ListNode));
struct ListNode* tail = dummy;
for (int i = 0; i < size; i++) {
struct ListNode* new_node = (struct ListNode*)malloc(sizeof(struct ListNode));
new_node->val = values[i];
tail->next = new_node;
tail = tail->next;
}
return dummy->next;
}
void print_linked_list(struct ListNode* head) {
struct ListNode* current = head;
while (current != NULL) {
printf("%d ", current->val);
current = current->next;
}
printf("\n");
}
int main() {
int values[] = {1, 2, 3};
struct ListNode* head = create_linked_list_tail(values, 3);
print_linked_list(head); // 输出:1 2 3
return 0;
}
代码示例(反转链表、删除节点):
// 反转链表
struct ListNode* reverse_linked_list(struct ListNode* head) {
struct ListNode* prev = NULL;
struct ListNode* curr = head;
while (curr != NULL) {
struct ListNode* next_node = curr->next;
curr->next = prev;
prev = curr;
curr = next_node;
}
return prev;
}
// 删除链表中所有值为val的节点
struct ListNode* remove_elements(struct ListNode* head, int val) {
struct ListNode* dummy = (struct ListNode*)malloc(sizeof(struct ListNode));
dummy->next = head;
struct ListNode* current = dummy;
while (current->next != NULL) {
if (current->next->val == val) {
struct ListNode* temp = current->next;
current->next = temp->next;
free(temp);
} else {
current = current->next;
}
}
return dummy->next;
}
6.代码示例(C语言):
#include <stdio.h>
#define MAX_SIZE 10
// 顺序表(数组实现)
int linear_list[MAX_SIZE] = {1, 2, 3, 4, 5};
// 链表(链式存储)
struct Node {
int data;
struct Node* next;
};
void print_linear_list() {
for (int i = 0; i < 5; i++) {
printf("%d ", linear_list[i]);
}
printf("\n");
}
void print_linked_list(struct Node* head) {
struct Node* current = head;
while (current != NULL) {
printf("%d ", current->data);
current = current->next;
}
printf("\n");
}
int main() {
print_linear_list(); // 输出线性表
return 0;
}
2. 顺序表(Sequence Table)
2.1 静态顺序表与动态顺序表
- 静态顺序表(Static Sequence Table):
- 定义:静态顺序表是运用固定大小的数组来存储数据元素。它的大小在编译的时候就已经确定,运行过程中不能进行更改。
- 特点:
- 大小固定,一旦对数组的大小进行了定义,就没办法动态地进行调整。
- 存储元素的内存地址在编译的时候就被分配好了,内存空间是连续的。
- 适用于数据量已知且不会频繁变化的场景。
- 优点:
- 存储效率高,访问元素的时间复杂度为O(1)。
- 缺点:
- 固定的容量有可能会导致内存浪费或者空间不足的情况。
- 插入和删除元素的时候可能会产生大量的数据搬移,时间复杂度为O(N)。
示例(C语言):
#include <stdio.h>
#define MAX_SIZE 10
int static_array[MAX_SIZE] = {1, 2, 3, 4, 5};
void print_static_array() {
for (int i = 0; i < 5; i++) {
printf("%d ", static_array[i]);
}
printf("\n");
}
int main() {
print_static_array(); // 输出:1 2 3 4 5
return 0;
}
- 动态顺序表(Dynamic Sequence Table):
- 定义:动态顺序表是一个支持动态扩容的数组。当数组的存储空间不够的时候,系统会自动分配更大的内存空间,并且将原有的元素复制到新的数组中。
- 特点:
- 容量是可以动态调整的,通常会根据需求将数组的容量进行扩展(比如扩容为原来的2倍)。
- 数据的内存地址有可能会发生变化,因为重新分配了更大的内存空间。
- 适用于数据量不确定或者需要经常变化的场景。
- 优点:
- 容量能够动态增长,不会造成内存空间的浪费。
- 避免了固定容量数组出现空间不足的问题。
- 缺点:
- 扩容操作可能会导致性能下降,尤其是在频繁扩容的时候,需要进行内存的重新分配和数据的搬移。
示例(C语言实现动态顺序表):
#include <stdio.h>
#include <stdlib.h>
typedef struct {
int* array; // 动态分配的数组
int size; // 当前元素个数
int capacity; // 数组容量
} DynamicArray;
// 初始化动态数组
void init(DynamicArray* arr) {
arr->capacity = 2; // 初始容量为2
arr->size = 0;
arr->array = (int*)malloc(arr->capacity * sizeof(int));
}
// 扩容:将数组容量翻倍
void resize(DynamicArray* arr, int new_capacity) {
arr->array = (int*)realloc(arr->array, new_capacity * sizeof(int));
arr->capacity = new_capacity;
}
// 向动态数组中添加元素
void append(DynamicArray* arr, int value) {
if (arr->size == arr->capacity) {
// 扩容
resize(arr, 2 * arr->capacity);
}
arr->array[arr->size] = value;
arr->size++;
}
// 打印数组内容
void print_array(DynamicArray* arr) {
for (int i = 0; i < arr->size; i++) {
printf("%d ", arr->array[i]);
}
printf("\n");
}
// 释放动态数组内存
void free_array(DynamicArray* arr) {
free(arr->array);
}
int main() {
DynamicArray arr;
init(&arr);
// 添加元素
append(&arr, 1);
append(&arr, 2);
append(&arr, 3); // 扩容时容量变为4
append(&arr, 4);
printf("动态顺序表的内容:\n");
print_array(&arr); // 输出:1 2 3 4
free_array(&arr); // 释放内存
return 0;
}
顺序表与链表对比分析
- 静态顺序表:内存空间固定,适用于数据量明确的情形,访问效率高,但插入和删除操作或许需要大量的数据搬移,并且扩容较为困难。
- 动态顺序表:内存空间可扩展,适用于数据量不确定或变化较大的场景,动态扩容规避了固定容量引发的问题,但在扩容时可能会影响性能。
特性 | 顺序表 | 链表 |
---|---|---|
存储方式 | 使用连续的内存块(数组)存储元素 | 使用不连续的内存块(通过指针链接) |
随机访问 | O(1) | O(N) |
插入/删除操作 | O(N)(需要移动元素) | O(1)(只需修改指针) |
空间管理 | 动态扩容可能浪费空间 | 按需分配,无空间浪费 |
缓存局部性 | 高( |