ListTraverse 是数据结构中一个非常基础且重要的操作,它指的是按照一定的顺序,访问链表(或列表)中的每一个节点,并对每个节点执行指定的操作(例如打印数据、查找、修改等)。
由于 C 语言本身没有内置的链表数据类型,ListTraverse 通常是我们自己实现的链表结构的一部分。
下面我将从以下几个方面进行讲解:
- 核心思想
- 实现步骤
- 完整代码示例
- 进阶:使用函数指针
核心思想
链表是由一个个节点通过指针连接而成的,每个节点通常包含两部分:
- 数据域:存储实际的数据。
- 指针域:指向下一个节点的地址。
ListTraverse 的核心思想就是 从头节点开始,顺着每个节点的 next 指针一路走下去,直到最后一个节点(其 next 指针为 NULL)。
整个过程就像一条火车,火车头是 head,每一节车厢是一个节点,车厢之间的连接是 next 指针,终点站是 NULL。
实现步骤
实现 ListTraverse 非常简单,主要分为以下几步:
- 获取链表的头节点指针:这是遍历的起点。
- 定义一个“游标”指针:通常命名为
current或p,并将其初始化为头节点指针。 - 进入循环:使用一个
while循环,只要游标指针current不为NULL,就继续循环。 - 访问节点:在循环内部,对当前游标指针
current所指向的节点进行操作(打印其数据)。 - 移动游标:操作完成后,将游标指针
current更新为current->next,即指向下一个节点。 - 循环结束:当
current变为NULL时,说明已经遍历到链表的末尾,循环结束。
完整代码示例
下面是一个完整的、可运行的 C 语言示例,它定义了一个简单的链表,并实现了 ListTraverse 函数来打印链表中的所有数据。
1 定义节点和链表结构
#include <stdio.h>
#include <stdlib.h>
// 1. 定义链表节点结构体
typedef struct Node {
int data; // 数据域,这里用 int 类型
struct Node *next; // 指针域,指向下一个节点
} Node;
// 为了方便,我们定义一个链表结构体,包含头指针和长度
typedef struct {
Node *head;
int length;
} LinkedList;
2 辅助函数(创建和初始化)
// 创建一个新节点
Node* createNode(int value) {
Node *newNode = (Node*)malloc(sizeof(Node));
if (newNode == NULL) {
printf("内存分配失败!\n");
exit(1);
}
newNode->data = value;
newNode->next = NULL;
return newNode;
}
// 初始化链表
void initList(LinkedList *list) {
list->head = NULL;
list->length = 0;
}
3 核心函数:ListTraverse
这是本次讲解的重点。
/**
* @brief 遍历链表并打印每个节点的数据
* @param list 要遍历的链表
*/
void ListTraverse(LinkedList *list) {
// 1. 定义游标指针,并初始化为头节点
Node *current = list->head;
printf("开始遍历链表: ");
// 2. 循环条件:只要 current 不为 NULL
while (current != NULL) {
// 3. 访问当前节点(例如打印数据)
printf("%d -> ", current->data);
// 4. 移动游标到下一个节点
current = current->next;
}
printf("NULL\n"); // 打印链表结束的标志
}
4 其他辅助函数(添加节点)
为了演示 ListTraverse,我们需要一个往链表里添加数据的方法。
// 在链表头部添加节点(头插法)
void insertAtHead(LinkedList *list, int value) {
Node *newNode = createNode(value);
newNode->next = list->head; // 新节点的 next 指向原来的头节点
list->head = newNode; // 更新头节点为新节点
list->length++;
}
5 main 函数(测试)
int main() {
// 创建并初始化一个链表
LinkedList myList;
initList(&myList);
// 向链表中添加一些数据
insertAtHead(&myList, 30);
insertAtHead(&myList, 20);
insertAtHead(&myList, 10);
printf("当前链表长度: %d\n", myList.length);
// 调用 ListTraverse 函数来打印整个链表
ListTraverse(&myList);
return 0;
}
6 运行结果
将以上代码编译并运行,你会得到如下输出:
当前链表长度: 3
开始遍历链表: 10 -> 20 -> 30 -> NULL
进阶:使用函数指针
上面的 ListTraverse 功能比较固定,只能打印数据,如果我们想让它更通用,比如既能打印,又能进行数据统计,该怎么办?
这时,函数指针 就派上用场了,我们可以将 ListTraverse 的行为参数化,让调用者决定对每个节点做什么操作。
1 定义操作函数类型
我们定义一个函数指针类型,这个类型的函数接收一个 int(节点数据)作为参数,没有返回值。
// 定义一个函数指针类型,它指向一个接受int参数、无返回值的函数 typedef void (*VisitFunc)(int);
2 改进后的 ListTraverse
我们修改 ListTraverse 函数,让它接收一个函数指针作为参数。
/**
* @brief 遍历链表并对每个节点执行指定的操作
* @param list 要遍历的链表
* @param visit 一个函数指针,指向对节点数据的操作函数
*/
void ListTraverseWithFunc(LinkedList *list, VisitFunc visit) {
Node *current = list->head;
while (current != NULL) {
// 调用传入的函数,并传入当前节点的数据
visit(current->data);
current = current->next;
}
printf("NULL\n");
}
3 定义不同的操作函数
我们可以定义多个不同的操作函数,然后传递给 ListTraverseWithFunc。
// 操作函数1:打印数据
void printData(int data) {
printf("%d -> ", data);
}
// 操作函数2:判断数据是否大于15
void checkGreaterThan15(int data) {
if (data > 15) {
printf("%d (大于15) ", data);
}
}
4 在 main 函数中测试
int main() {
LinkedList myList;
initList(&myList);
insertAtHead(&myList, 30);
insertAtHead(&myList, 20);
insertAtHead(&myList, 10);
printf("--- 使用函数指针的遍历示例 ---\n");
// 场景1:打印所有数据
printf("场景1: 打印所有数据\n");
ListTraverseWithFunc(&myList, printData);
// 输出: 10 -> 20 -> 30 -> NULL
printf("\n场景2: 筛选并打印大于15的数据\n");
ListTraverseWithFunc(&myList, checkGreaterThan15);
// 输出: 20 (大于15) 30 (大于15) NULL
return 0;
}
5 运行结果
--- 使用函数指针的遍历示例 ---
场景1: 打印所有数据
10 -> 20 -> 30 -> NULL
场景2: 筛选并打印大于15的数据
20 (大于15) 30 (大于15) NULL
使用函数指针,ListTraverse 的功能变得极其灵活和强大,这是 C 语言中实现回调机制的经典方式。
| 特性 | 简单 ListTraverse |
带函数指针的 ListTraverse |
|---|---|---|
| 核心思想 | 从头到尾,顺着 next 指针访问。 |
从头到尾,顺着 next 指针访问,但行为由外部函数决定。 |
| 实现方式 | 在函数内部直接处理数据(如 printf)。 |
在函数内部调用一个外部传入的函数指针。 |
| 优点 | 简单直观,适合固定操作。 | 高灵活性、可重用性,符合“开闭原则”(对扩展开放,对修改关闭)。 |
| 缺点 | 功能固定,无法复用。 | 需要额外定义操作函数,对初学者可能稍显复杂。 |
| 适用场景 | 简单脚本、一次性打印。 | 需要对链表进行多种操作(打印、查找、修改、统计等)的复杂程序。 |
ListTraverse 是链表操作的基础,掌握其基本实现后,强烈建议学习并使用函数指针版本来提升代码的健壮性和可扩展性。
