🌟 各位看官好,我是 maomi_9526!
🌍 种一棵树最好是十年前,其次是现在!
🚀 今天来学习C语言的相关知识。
👍 如果觉得这篇文章有帮助,欢迎您一键三连,分享给更多人哦
目录
[1. 线性表(Linear List)](#1. 线性表(Linear List))
[1. 线性表的定义](#1. 线性表的定义)
[2. 线性表的基本特性](#2. 线性表的基本特性)
[3. 线性表的常见实现方式](#3. 线性表的常见实现方式)
[3.1 顺序存储(顺序表)](#3.1 顺序存储(顺序表))
[3.2 链式存储(链表)](#3.2 链式存储(链表))
[4. 顺序表与链表的比较](#4. 顺序表与链表的比较)
[5. 顺序表操作](#5. 顺序表操作)
[2. 顺序表(Sequence Table)](#2. 顺序表(Sequence Table))
[2.1 静态顺序表与动态顺序表](#2.1 静态顺序表与动态顺序表)
[3. 顺序表 vs 链表](#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)。
-
对比单向链表和双向链表在删除尾节点时的时间复杂度差异。
-
教学总结
通过代码实现和对比分析,学生应掌握以下内容:
-
顺序表和链表的实现原理及适用场景。
-
动态扩容的策略与时间复杂度分析。
-
链表指针操作的逻辑与常见算法。