数据结构与动态内存管理:单链表与文件I/O
单链表基础
单链表是一种简单的数据结构,用于在堆中存储可变数量的项。链表的最后一个节点的指针为
NULL
,表示链表结束。其节点结构定义如下:
#define NAME_SIZE 20 // Max number of characters in a name
/**
* A node in the linked list
*/
struct linkedList {
struct linkedList* next; // Next node
char name[NAME_SIZE]; // Name of the node
};
next
指针指向下一个节点(或为
NULL
),
name
数组最多存储 20 个字符。
单链表操作
-
添加节点
-
步骤:
- 创建新节点。
-
使新节点的
next指针指向当前链表的头节点。 - 更新链表头指针指向新节点。
- 代码示例:
-
步骤:
static void addName(void)
{
printf("Enter word to add: ");
char line[NAME_SIZE]; // Input line
if (fgets(line, sizeof(line), stdin) == NULL)
return;
if (line[strlen(line)-1] == '\n')
line[strlen(line)-1] = '\0';
// Get a new node.
struct linkedList* newNode = malloc(sizeof(*newNode));
strncpy(newNode->name, line, sizeof(newNode->name)-1);
newNode->name[sizeof(newNode->name)-1] = '\0';
newNode->next = theList;
theList = newNode;
}
-
打印链表
-
规则:从第一个节点开始,依次打印节点的
name,直到链表结束。 - 代码示例:
-
规则:从第一个节点开始,依次打印节点的
for (const struct linkedList* curNode = theList;
curNode != NULL;
curNode = curNode->next){
printf("%s, ", curNode->name);
}
-
删除节点
-
步骤:
- 遍历链表,找到要删除的节点。
- 若要删除的是头节点,更新链表头指针。
-
若不是头节点,将前一个节点的
next指针指向要删除节点的下一个节点。 - 释放要删除节点的内存。
- 代码示例:
-
步骤:
static void deleteWord(void)
{
printf("Enter word to delete: ");
char line[NAME_SIZE]; // Input line
if (fgets(line, sizeof(line), stdin) == NULL)
return;
if (line[strlen(line)-1] == '\n')
line[strlen(line)-1] = '\0';
struct linkedList* prevNode = NULL; // Pointer to previous node
for (struct linkedList* curNode = theList;
curNode != NULL;
curNode = curNode->next) {
if (strcmp(curNode->name, line) == 0) {
if (prevNode == NULL) {
theList = curNode->next;
} else {
prevNode->next = curNode->next;
}
free(curNode);
curNode = NULL;
return;
}
prevNode = curNode;
}
printf("WARNING: Node not found %s\n", line);
}
完整程序示例
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdbool.h>
#define NAME_SIZE 20 // Max number of characters in a name
/**
* A node in the linked list
*/
struct linkedList {
struct linkedList* next; // Next node
char name[NAME_SIZE]; // Name of the node
};
// The linked list of words
static struct linkedList* theList = NULL;
/**
* Add a name to the linked list.
*/
static void addName(void)
{
printf("Enter word to add: ");
char line[NAME_SIZE]; // Input line
if (fgets(line, sizeof(line), stdin) == NULL)
return;
if (line[strlen(line)-1] == '\n')
line[strlen(line)-1] = '\0';
// Get a new node.
struct linkedList* newNode = malloc(sizeof(*newNode));
strncpy(newNode->name, line, sizeof(newNode->name)-1);
newNode->name[sizeof(newNode->name)-1] = '\0';
newNode->next = theList;
theList = newNode;
}
/**
* Delete a word from the list.
*/
static void deleteWord(void)
{
printf("Enter word to delete: ");
char line[NAME_SIZE]; // Input line
if (fgets(line, sizeof(line), stdin) == NULL)
return;
if (line[strlen(line)-1] == '\n')
line[strlen(line)-1] = '\0';
struct linkedList* prevNode = NULL; // Pointer to the previous node
for (struct linkedList* curNode = theList;
curNode != NULL;
curNode = curNode->next) {
if (strcmp(curNode->name, line) == 0) {
if (prevNode == NULL) {
theList = curNode->next;
} else {
prevNode->next = curNode->next;
}
free(curNode);
curNode = NULL;
return;
}
prevNode = curNode;
}
printf("WARNING: Node not found %s\n", line);
}
/**
* Print the linked list.
*/
static void printList(void)
{
// Loop over each node in the list.
for (const struct linkedList* curNode = theList;
curNode != NULL;
curNode = curNode->next) {
printf("%s, ", curNode->name);
}
printf("\n");
}
int main()
{
while (true) {
printf("a-add, d-delete, p-print, q-quit: ");
char line[100]; // An input line
if (fgets(line, sizeof(line), stdin) == NULL)
break;
switch (line[0]) {
case 'a':
addName();
break;
case 'd':
deleteWord();
break;
case 'p':
printList();
break;
case 'q':
exit(8);
default:
printf(
"ERROR: Unknown command %c\n", line[0]);
break;
}
}
}
动态内存问题
-
内存泄漏
:内存分配后未释放,会导致程序不断消耗内存,最终耗尽系统资源。
- 示例:
{
int* dynamicArray; // A dynamic array
// Allocate 100 elements.
dynamicArray = malloc(sizeof(int) * 100);
}
-
使用已释放的指针
:释放指针后继续使用,可能导致随机结果或覆盖随机内存。
- 示例:
free(nodePtr);
nextPtr = nodePtr->Next; // Illegal
- 解决方法:释放指针后将其置为 `NULL`。
free(nodePtr);
nodePtr = NULL;
nextPtr = nodePtr->Next; // Crashes the program
-
越界写入
:向结构末尾之外写入数据,可能破坏随机内存。
- 示例:
int* theData; // An array of data
*theData = malloc(sizeof(*theData)*10);
theData[0] = 0;
theData[10] = 10; // Error
内存检测工具
-
Valgrind
:开源工具,可检测内存泄漏、越界写入、使用已释放指针和基于未初始化内存值做决策等问题。
-
使用方法:编译程序后,运行
valgrind --leak-check=full ./program_name。
-
使用方法:编译程序后,运行
-
GCC 地址 sanitizer
:编译时工具,可检测内存泄漏和越界写入。
-
使用方法:编译代码时添加
-fsanitize=address标志。
-
使用方法:编译代码时添加
C 语言 I/O 系统
-
printf 函数
-
基本格式:
printf(format-string, argument, ...) - 示例:
-
基本格式:
printf("Number: ->%d<-\n", 1234); // Prints ->1234<-
- 常用转换字符:
| 转换字符 | 参数类型 | 说明 |
|---|---|---|
%d
| 整数 |
char
和
short int
类型作为参数时会提升为
int
|
%c
| 字符 | 实际接收整数参数并打印为字符 |
%o
| 整数 | 以八进制打印 |
%x
| 整数 | 以十六进制打印 |
%f
| 双精度浮点数 |
float
参数作为参数时会提升为
double
|
%l
| 长整数 |
long int
类型需要单独的转换
|
- 编写 ASCII 表程序
/**
* Print ASCII character table (only printable characters).
*/
#include <stdio.h>
int main()
{
for (char curChar = ' '; curChar <= '~'; ++curChar) {
printf("Char: %c Decimal %3d Hex 0x%02x Octal 0%03o\n",
curChar, curChar, curChar, curChar);
}
return (0);
}
编程问题
- 修改单链表程序,使节点始终保持有序。
- 给定两个有序链表,编写函数返回公共节点列表。
- 将单链表程序改为双向链表,每个节点增加指向前一个节点的指针。
- 编写函数反转单链表的顺序。
- 编写函数去除链表中的重复节点。
流程图:单链表添加节点
graph TD;
A[开始] --> B[输入要添加的单词];
B --> C[创建新节点];
C --> D[复制单词到新节点的 name 数组];
D --> E[新节点的 next 指针指向当前链表头];
E --> F[更新链表头指针指向新节点];
F --> G[结束];
流程图:单链表删除节点
graph TD;
A[开始] --> B[输入要删除的单词];
B --> C[遍历链表查找目标节点];
C --> D{找到目标节点?};
D -- 是 --> E{目标节点是头节点?};
E -- 是 --> F[更新链表头指针];
E -- 否 --> G[前一个节点的 next 指针指向目标节点的下一个节点];
F --> H[释放目标节点内存];
G --> H;
H --> I[结束];
D -- 否 --> J[输出未找到节点警告];
J --> I;
数据结构与动态内存管理:单链表与文件I/O
解决编程问题的思路
1. 修改单链表程序,使节点始终保持有序
为了让单链表中的节点始终有序,在添加节点时,需要遍历链表找到合适的插入位置。具体步骤如下:
1. 输入要添加的单词。
2. 创建新节点并复制单词到新节点的
name
数组。
3. 遍历链表,找到第一个比新节点单词大的节点位置。
4. 如果找到的位置是头节点,更新链表头指针;否则,将新节点插入到该位置。
static void addNameSorted(void)
{
printf("Enter word to add: ");
char line[NAME_SIZE]; // Input line
if (fgets(line, sizeof(line), stdin) == NULL)
return;
if (line[strlen(line)-1] == '\n')
line[strlen(line)-1] = '\0';
// Get a new node.
struct linkedList* newNode = malloc(sizeof(*newNode));
strncpy(newNode->name, line, sizeof(newNode->name)-1);
newNode->name[sizeof(newNode->name)-1] = '\0';
struct linkedList* prevNode = NULL;
struct linkedList* curNode = theList;
while (curNode != NULL && strcmp(curNode->name, line) < 0) {
prevNode = curNode;
curNode = curNode->next;
}
if (prevNode == NULL) {
newNode->next = theList;
theList = newNode;
} else {
prevNode->next = newNode;
newNode->next = curNode;
}
}
2. 给定两个有序链表,编写函数返回公共节点列表
可以通过同时遍历两个链表,比较节点的
name
值来找出公共节点。具体步骤如下:
1. 初始化两个指针分别指向两个链表的头节点。
2. 比较两个指针指向的节点的
name
值:
- 如果相等,将该节点添加到结果链表中,并同时移动两个指针。
- 如果第一个链表的节点
name
值较小,移动第一个链表的指针。
- 如果第二个链表的节点
name
值较小,移动第二个链表的指针。
3. 重复步骤 2 直到其中一个链表遍历完。
struct linkedList* findCommonNodes(struct linkedList* list1, struct linkedList* list2) {
struct linkedList* result = NULL;
struct linkedList* tail = NULL;
while (list1 != NULL && list2 != NULL) {
int cmp = strcmp(list1->name, list2->name);
if (cmp == 0) {
struct linkedList* newNode = malloc(sizeof(*newNode));
strncpy(newNode->name, list1->name, sizeof(newNode->name)-1);
newNode->name[sizeof(newNode->name)-1] = '\0';
newNode->next = NULL;
if (result == NULL) {
result = newNode;
tail = newNode;
} else {
tail->next = newNode;
tail = newNode;
}
list1 = list1->next;
list2 = list2->next;
} else if (cmp < 0) {
list1 = list1->next;
} else {
list2 = list2->next;
}
}
return result;
}
3. 将单链表程序改为双向链表,每个节点增加指向前一个节点的指针
双向链表的节点结构需要增加一个指向前一个节点的指针
prev
。同时,在添加、删除和打印节点时,需要相应地处理
prev
指针。
#define NAME_SIZE 20 // Max number of characters in a name
/**
* A node in the doubly linked list
*/
struct doublyLinkedList {
struct doublyLinkedList* next; // Next node
struct doublyLinkedList* prev; // Previous node
char name[NAME_SIZE]; // Name of the node
};
// 添加节点
static void addNameDoubly(void)
{
printf("Enter word to add: ");
char line[NAME_SIZE]; // Input line
if (fgets(line, sizeof(line), stdin) == NULL)
return;
if (line[strlen(line)-1] == '\n')
line[strlen(line)-1] = '\0';
// Get a new node.
struct doublyLinkedList* newNode = malloc(sizeof(*newNode));
strncpy(newNode->name, line, sizeof(newNode->name)-1);
newNode->name[sizeof(newNode->name)-1] = '\0';
if (theList == NULL) {
newNode->next = NULL;
newNode->prev = NULL;
theList = newNode;
} else {
newNode->next = theList;
theList->prev = newNode;
newNode->prev = NULL;
theList = newNode;
}
}
// 删除节点
static void deleteWordDoubly(void)
{
printf("Enter word to delete: ");
char line[NAME_SIZE]; // Input line
if (fgets(line, sizeof(line), stdin) == NULL)
return;
if (line[strlen(line)-1] == '\n')
line[strlen(line)-1] = '\0';
struct doublyLinkedList* curNode = theList;
while (curNode != NULL) {
if (strcmp(curNode->name, line) == 0) {
if (curNode->prev == NULL) {
theList = curNode->next;
if (theList != NULL) {
theList->prev = NULL;
}
} else {
curNode->prev->next = curNode->next;
if (curNode->next != NULL) {
curNode->next->prev = curNode->prev;
}
}
free(curNode);
return;
}
curNode = curNode->next;
}
printf("WARNING: Node not found %s\n", line);
}
// 打印链表
static void printListDoubly(void)
{
struct doublyLinkedList* curNode = theList;
while (curNode != NULL) {
printf("%s, ", curNode->name);
curNode = curNode->next;
}
printf("\n");
}
4. 编写函数反转单链表的顺序
可以通过遍历链表,依次改变节点的
next
指针方向来实现链表反转。具体步骤如下:
1. 初始化三个指针:
prev
指向
NULL
,
cur
指向链表头,
next
指向
cur
的下一个节点。
2. 遍历链表,将
cur
的
next
指针指向
prev
,然后更新
prev
、
cur
和
next
指针。
3. 最后更新链表头指针为
prev
。
void reverseList(struct linkedList** head) {
struct linkedList* prev = NULL;
struct linkedList* cur = *head;
struct linkedList* next = NULL;
while (cur != NULL) {
next = cur->next;
cur->next = prev;
prev = cur;
cur = next;
}
*head = prev;
}
5. 编写函数去除链表中的重复节点
可以通过遍历链表,使用一个临时指针记录已经出现过的节点,对于重复的节点进行删除。具体步骤如下:
1. 初始化一个指针
cur
指向链表头。
2. 遍历链表,对于每个节点,使用另一个指针
runner
从该节点的下一个节点开始遍历,删除所有与该节点
name
相同的节点。
3. 移动
cur
指针到下一个节点,重复步骤 2 直到链表结束。
void removeDuplicates(struct linkedList* head) {
struct linkedList* cur = head;
while (cur != NULL) {
struct linkedList* runner = cur;
while (runner->next != NULL) {
if (strcmp(runner->next->name, cur->name) == 0) {
struct linkedList* temp = runner->next;
runner->next = runner->next->next;
free(temp);
} else {
runner = runner->next;
}
}
cur = cur->next;
}
}
总结
本文介绍了单链表的基本操作,包括添加、打印和删除节点,同时讨论了动态内存管理中常见的问题,如内存泄漏、使用已释放的指针和越界写入,并介绍了相应的检测工具。此外,还介绍了 C 语言的 I/O 系统中的
printf
函数和编写 ASCII 表的程序。最后,针对一些编程问题给出了解决思路和代码示例。通过这些内容,我们可以更好地理解和应用单链表和动态内存管理,提高编程能力。
流程图:反转单链表
graph TD;
A[开始] --> B[初始化 prev 为 NULL, cur 为链表头, next 为 NULL];
B --> C{cur 不为 NULL?};
C -- 是 --> D[next 指向 cur 的下一个节点];
D --> E[cur 的 next 指针指向 prev];
E --> F[prev 指向 cur];
F --> G[cur 指向 next];
G --> C;
C -- 否 --> H[更新链表头指针为 prev];
H --> I[结束];
流程图:去除链表重复节点
graph TD;
A[开始] --> B[cur 指向链表头];
B --> C{cur 不为 NULL?};
C -- 是 --> D[runner 指向 cur];
D --> E{runner 的下一个节点不为 NULL?};
E -- 是 --> F{runner 的下一个节点的 name 与 cur 的 name 相同?};
F -- 是 --> G[删除 runner 的下一个节点];
G --> E;
F -- 否 --> H[runner 指向 runner 的下一个节点];
H --> E;
E -- 否 --> I[cur 指向 cur 的下一个节点];
I --> C;
C -- 否 --> J[结束];
超级会员免费看

被折叠的 条评论
为什么被折叠?



