🌟 各位看官好,我是maomi_9526!
🌍 种一棵树最好是十年前,其次是现在!
🚀 今天来学习C语言的相关知识。
👍 如果觉得这篇文章有帮助,欢迎您一键三连,分享给更多人哦
目录
教学目标
教学重点与难点
教学方法
教学大纲
1. 线性表(Linear List)
1. 线性表的定义
2. 线性表的基本特性
3. 线性表的常见实现方式
3.1 顺序存储(顺序表)
3.2 链式存储(链表)
4. 顺序表与链表的比较
5. 顺序表操作
6.链表基本操作
2. 顺序表(Sequence Table)
2.1 静态顺序表与动态顺序表
3. 顺序表 vs 链表
对比分析
课堂练习
作业设计
教学总结
教学目标
-
理解线性表的逻辑结构和物理实现方式。
-
掌握顺序表和链表的实现原理及核心操作。
-
能够通过代码实现顺序表和链表的基本功能。
-
分析顺序表与链表的性能差异及适用场景。
教学重点与难点
-
重点:顺序表的动态增容、链表的指针操作、时间复杂度分析。
-
难点:链表节点的插入与删除逻辑、顺序表与链表的性能权衡。
教学方法
-
理论讲解 + 代码演示 + 对比分析 + 课堂练习
教学大纲
1. 线性表(Linear List)
1. 线性表的定义
-
线性表:线性表是由n个具有相同特性的元素组成的有限序列,元素之间有明确的前后关系。每个元素有唯一的前驱和后继元素。线性表可以是顺序存储或链式存储。
-
逻辑结构:线性表是逻辑上的顺序排列,指的是元素之间的关系是依次排列的,每个元素都有一个明确的前后关系。
-
物理结构:线性表的元素在计算机内存中的存储方式,可以是连续的(顺序表)或者不连续的(链表)。
-
2. 线性表的基本特性
-
元素的顺序性:线性表中元素的顺序关系决定了元素之间的前后依赖关系,通常我们按从第一个元素到最后一个元素的顺序进行访问。
-
唯一性:每个元素在序列中有唯一的前驱和后继元素,只有第一个元素没有前驱,最后一个元素没有后继。
-
有限性:线性表包含一个有限的元素集合,且元素数量固定。
3. 线性表的常见实现方式
线性表有两种常见的物理存储方式:顺序存储和链式存储。
3.1 顺序存储(顺序表)
顺序存储使用一段连续的内存空间存储数据,通常使用数组来实现。数据元素在内存中的地址是连续的,因此可以通过索引来直接访问。
-
优点:
-
高效的随机访问:可以通过索引直接访问任意位置的元素,时间复杂度为 O(1)。
-
空间效率高:在内存中是连续存储,相对来说能够更高效地利用内存。
-
-
缺点:
-
插入和删除效率较低:在顺序表中间插入或删除元素时,需要移动大量的元素,时间复杂度为 O(N)。
-
固定容量问题:顺序表的容量一旦固定,后期无法动态调整空间,需要使用动态扩容,但扩容可能导致空间浪费。
-
3.2 链式存储(链表)
链表使用非连续的内存空间存储数据,每个元素包含一个数据域和一个指向下一个元素的指针(或者同时包含指向前后节点的指针),因此数据元素的地址不一定是连续的。
-
优点:
-
插入和删除效率高:链表只需要调整指针即可完成插入和删除操作,时间复杂度为 O(1)(不需要移动元素)。
-
动态内存分配:链表不需要预先定义大小,内存空间可以根据实际需求动态分配,避免了空间浪费问题。
-
-
缺点:
-
随机访问效率低:访问链表中的元素需要从头节点开始依次遍历,时间复杂度为 O(N)。
-
指针开销:每个节点需要额外的指针空间存储前驱或后继元素。
-
4. 顺序表与链表的比较
顺序表和链表是两种常见的线性表实现方式,它们的优缺点适用于不同的应用场景。
特性 | 顺序表 | 链表 |
---|---|---|
存储方式 | 使用连续的内存块(数组)存储元素 | 使用不连续的内存块(通过指针链接) |
随机访问 | O(1) | O(N) |
插入/删除操作 | O(N)(需要移动元素) | O(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--;
}
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; }
-
-
静态顺序表:内存空间是固定的,适用于数据量确定的情况,访问效率高,但插入和删除操作可能需要大量的数据搬移,且扩容困难。
-
动态顺序表:内存空间是可扩展的,适用于数据量不确定或变化较大的情况,动态扩容避免了固定容量带来的问题,但在扩容时可能会影响性能。
3. 顺序表 vs 链表
对比分析
特性 | 顺序表 | 链表 |
---|---|---|
存储方式 | 连续内存 | 离散内存,通过指针链接 |
随机访问 | O(1) | O(N) |
插入/删除 | O(N)(需移动元素) | O(1)(只需调整指针) |
空间管理 | 动态扩容可能浪费空间 | 按需分配,无空间浪费 |
缓存局部性 | 高(连续存储) | 低(节点分散) |
课堂练习
-
顺序表练习:实现一个函数,删除顺序表中所有等于给定值的元素。
-
链表练习:合并两个有序链表,返回新的有序链表头节点。
作业设计
-
编码题:
-
实现动态顺序表的缩容功能(当元素数量小于容量的1/4时,容量减半)。
-
实现双向链表的插入和删除操作。
-
-
分析题:
-
分析顺序表动态扩容均摊时间复杂度为何是O(1)。
-
对比单向链表和双向链表在删除尾节点时的时间复杂度差异。
-
教学总结
通过代码实现和对比分析,学生应掌握以下内容:
-
顺序表和链表的实现原理及适用场景。
-
动态扩容的策略与时间复杂度分析。
-
链表指针操作的逻辑与常见算法。