深入链表操作:C语言中的链表中间节点查找与合并
目录
链表的基本结构
尾插法建立链表
打印链表
找到链表中间节点
原地逆置链表
合并两个链表
主函数
示例运行
输出示例
总结
关键点回顾
在数据结构的学习中,链表是一种非常重要的线性结构。它的动态特性使得在插入和删除操作时比数组更为高效。今天,我们将探讨如何在链表中找到中间节点,并将链表的后半部分逆置后与前半部分合并。通过这篇博客,你将更深入地理解链表的实现和操作。
链表的基本结构
在C语言中,链表的基本结构由节点(Node)构成,每个节点包含数据部分和指向下一个节点的指针。我们首先定义一个链表节点的结构体:
typedef int ElemType;
typedef struct node {
int data;
struct node *next;
} Node;
这里,Node
结构体包含了数据和指向下一个节点的指针。
尾插法建立链表
我们使用尾插法来创建链表。以下是尾插法的实现:
void list_tail_insert(Node *&L) {
L = (Node *) malloc(sizeof(Node));
L->next = NULL;
ElemType x;
scanf("%d", &x);
Node *s, *r = L; // s 用来指向申请的新结点 r 始终指向链表尾部
while (x != 9999) {
s = (Node *) malloc(sizeof(Node)); // 为新结点申请空间
s->data = x;
r->next = s; // 新结点给尾结点的next指针
r = s; // r 要指向新的尾部
scanf("%d", &x);
}
r->next = NULL; // 让尾结点的 next 为 Null
}
在这个函数中,我们创建一个头节点,并通过循环读取用户输入的数据,直到输入9999为止。每次读取到一个新数据时,我们都会创建一个新节点,并将其插入到链表的尾部。
打印链表
为了验证我们的链表操作是否成功,我们需要一个函数来打印链表的内容:
void print_list(Node *L) {
L = L->next; // 跳过头结点
while (L != NULL) {
printf("%d ", L->data);
L = L->next;
}
printf("\n");
}
这个函数从头节点的下一个节点开始遍历链表,打印每个节点的数据。
找到链表中间节点
我们实现了一个函数来找到链表的中间节点,并将后半部分链表设置为新的链表L2
:
void find_middle(Node *L, Node *&L2) {
L2 = (Node *) malloc(sizeof(Node)); // 第二条链表的头节点
Node *p_cur, *p_pre; // 双指针法
p_pre = p_cur = L->next;
while (p_cur) {
p_cur = p_cur->next;
if (NULL == p_cur) {
break;
}
p_cur = p_cur->next;
if (NULL == p_cur) { // 为了使得偶数个 p_pre 依然指向中间节点
break;
}
p_pre = p_pre->next;
}
L2->next = p_pre->next; // L2 头节点指向后面的一半链表
p_pre->next = NULL; // 前一半链表的最后一个节点 next 要为 NULL
}
在这个函数中,我们使用双指针法来找到链表的中间节点。p_cur
每次移动两步,而p_pre
每次移动一步。当p_cur
到达链表末尾时,p_pre
正好指向中间节点。我们将后半部分链表的头节点指向L2
。
原地逆置链表
接下来,我们实现一个函数来逆置链表L2
:
void reverse(Node *L2) {
Node *r, *s, *t;
r = L2->next;
if (NULL == r) {
return; // 链表为空
}
s = r->next;
if (NULL == s) {
return; // 链表只有一个节点
}
t = s->next;
while (t) {
s->next = r; // 原地逆置
r = s; // 更新指针
s = t;
t = t->next; // 移动到下一个节点
}
s->next = r; // 最后一个节点指向原链表的头
L2->next->next = NULL; // 逆置后,链表第一个节点的 next 要为 NULL
L2->next = s; // s 是链表的第一个节点,L2 指向它
}
在这个reverse
函数中,我们使用三个指针r
、s
和t
来实现链表的原地逆置。r
指向当前逆置后的链表的头,s
指向当前正在处理的节点,t
指向下一个节点。通过不断更新这些指针,我们可以将链表的顺序反转。
合并两个链表
最后,我们实现一个函数来合并两个链表L
和L2
:
void merge(Node *L, Node *L2) {
Node *p_cur, *p, *q;
p_cur = L->next; // p_cur 始终指向组合后的链表的链表尾
p = p_cur->next; // p 指向 L 的第二个节点
q = L2->next; // q 指向 L2 第一个节点
while (p != NULL && q != NULL) {
p_cur->next = q; // 将 L2 的节点连接到合并链表
q = q->next; // q 指向下一个
p_cur = p_cur->next; // 更新合并链表的尾部
p_cur->next = p; // 将 L 的节点连接到合并链表
p = p->next; // p 指向下一个
p_cur = p_cur->next; // 更新合并链表的尾部
}
// 处理剩余的节点
if (p != NULL) {
p_cur->next = p; // 如果 L 还有剩余节点,连接到合并链表
}
if (q != NULL) {
p_cur->next = q; // 如果 L2 还有剩余节点,连接到合并链表
}
}
在这个merge
函数中,我们通过指针p_cur
、p
和q
来合并两个链表。p_cur
始终指向合并后的链表的尾部,p
指向链表L
的当前节点,q
指向链表L2
的当前节点。我们交替将两个链表的节点连接到合并链表中,直到一个链表遍历完。
主函数
最后,我们在main
函数中调用这些操作,构建链表并进行合并操作:
int main() {
Node *L; // 链表头 是结构体指针类型
list_tail_insert(L); // 尾插法输入数据
print_list(L); // 打印链表
// 寻找中间节点 并返回第二条链表
Node *L2 = NULL;
find_middle(L, L2); // 将后半部分链表设置为 L2
printf("-------------------------------------\n");
print_list(L); // 打印前半部分链表
reverse(L2); // 逆置后半部分链表
print_list(L2); // 打印逆置后的后半部分链表
printf("-------------------------------------\n");
merge(L, L2); // 合并两个链表
free(L2); // 释放 L2 的内存
print_list(L); // 打印合并后的链表
return 0;
}
在main
函数中,我们首先定义一个链表L
,然后调用list_tail_insert
函数来填充链表。接着,我们使用print_list
函数打印链表的内容,以验证插入操作的正确性。
示例运行
假设用户输入以下数据来创建链表:
1
2
3
4
5
9999
在这种情况下,链表的初始状态为:1 -> 2 -> 3 -> 4 -> 5
。接着,我们进行以下操作:
- 找到中间节点并将后半部分链表设置为
L2
,此时L2
将包含4 -> 5
。 - 逆置
L2
,结果为5 -> 4
。 - 合并
L
和L2
,最终链表将变为:`1 ->2 -> 3 -> 5 -> 4
输出示例
假设用户输入了上述数据,程序的输出将类似于:
1 2 3 4 5
-------------------------------------
1 2 3
5 4
-------------------------------------
1 2 3 5 4
在这个输出中,我们可以看到:
- 初始链表:用户输入的链表为
1 2 3 4 5
。 - 分割后的链表:在找到中间节点后,前半部分链表为
1 2 3
,后半部分链表L2
为4 5
,经过逆置后变为5 4
。 - 合并后的链表:最终合并后的链表为
1 2 3 5 4
,展示了合并操作的结果。
总结
通过这篇博客,我们深入探讨了链表的基本操作,包括尾插法、打印链表、找到中间节点、逆置链表和合并两个链表。链表的灵活性使得它在许多应用中都非常有用,尤其是在需要频繁插入和删除操作的场景中。
关键点回顾
- 双指针法:使用双指针法有效地找到链表的中间节点,避免了多次遍历。
- 原地逆置:通过指针操作实现链表的原地逆置,节省了额外的空间。
- 合并操作:交替合并两个链表,确保合并后的链表保持顺序。
希望这篇文章能帮助你更好地理解链表的实现和操作。如果你有任何问题或想法,欢迎在评论区留言讨论!链表的世界充满了可能性,继续探索吧!