一、单链表核心操作:逆置、合并、删除
1. 就地逆置:把 "队伍" 反过来
单链表是 "单向通行" 的,只能顺着指针往后走,逆置就是把整个链表的顺序颠倒过来,还不能额外开新空间。
- 关键技巧:用三个 "跑腿指针"------Pre(前面的节点)、current(当前节点)、post(后面的节点)。
- 操作步骤:先记下 current 的下一个节点(post),避免后续找不到;再让 current "转身" 指向 Pre;接着三个指针一起往后挪,重复操作直到 current 走到头。最后 Pre 就是逆置后的新头节点,就像队伍掉头后,原来的队尾变成了队首。
代码示例(C 语言实现)
cpp
#include<stdio.h>
#include<stdlib.h>
// 定义单链表节点结构体:包含数据域和后继指针
typedef struct Node {
int data; // 节点存储的数值
struct Node *next; // 指向后继节点的指针
}*ll; // 用ll简化struct Node*类型名
int main() {
ll head=NULL; // 链表头指针,初始化为空(表示链表为空)
ll tail=NULL; // 链表尾指针,初始化为空(尾插法构建链表用)
int n; // 存储链表的节点个数
scanf("%d",&n);// 输入节点个数
// 第一步:尾插法构建单链表
for(int i=0;i<n;i++) {
// 动态分配内存创建新节点
ll temp=(ll)malloc(sizeof(struct Node));
int num;
scanf("%d",&num); // 输入当前节点要存储的数值
temp->data=num; // 给新节点赋值
temp->next=NULL; // 新节点的后继指针初始化为空
if (head==NULL) { // 处理第一个节点(链表为空时)
head=temp; // 头指针指向第一个节点
tail=temp; // 尾指针也指向第一个节点
}
else { // 处理后续节点(链表已有节点)
tail->next=temp; // 原尾节点的后继指向新节点
tail=temp; // 尾指针后移到新节点
}
}
// 第二步:就地逆置单链表(核心逻辑)
ll pre=NULL; // 记录当前节点的前驱节点,初始为空
ll post=NULL; // 暂存当前节点的后继节点,避免逆置后丢失
ll cur=head; // 遍历指针,从链表头开始
while (cur!=NULL) { // 遍历整个链表,直到cur指向空
post=cur->next; // 1. 先保存当前节点的后继(防止断链)
cur->next=pre; // 2. 逆置:当前节点指向它的前驱
pre=cur; // 3. 前驱指针后移(指向当前节点)
cur=post; // 4. 当前指针后移(指向之前保存的后继)
}
// 第三步:打印逆置后的链表
ll x=pre; // 逆置后pre指向新的链表头(原链表尾)
while (x!=NULL) {
printf("%d",x->data); // 打印当前节点的数值(注意末尾有一个空格)
if (x->next!=NULL) { // 若不是最后一个节点,额外打印一个空格(此处会导致多余空格)
printf(" ");
}
x=x->next; // 遍历下一个节点
}
// 打印完成后释放内存
ll temp;
while (pre != NULL) {
temp = pre;
pre = pre->next;
free(temp);
}
return 0;
}
2. 有序链表合并:把两个 "有序队伍" 合成一个
两个已经排好序的单链表,要合并成一个依然有序的链表,不用重新排序,效率更高。
- 关键技巧:用 "游动指针" 分别遍历两个链表,再用 "虚拟头节点" 记录合并后的链表(避免处理头节点为空的特殊情况)。
- 操作步骤:先比两个链表的头节点,谁小就作为合并链表的头;然后依次比较两个链表的当前节点,小的就接到合并链表的尾部,同时移动对应链表的指针;如果一个链表先遍历完,就把另一个链表剩下的部分直接接在后面,不用再比较。
代码示例(C 语言实现)
cpp
#include<stdio.h>
#include<stdlib.h>
// 定义单链表节点结构体:包含数据域和后继指针
typedef struct Node {
int data; // 节点存储的数值
struct Node *next; // 指向后继节点的指针
}node, *ll; // node等价于struct Node,ll简化struct Node*指针类型
int main() {
int n,m; // n:第一个有序链表的节点数;m:第二个有序链表的节点数
scanf("%d %d",&n,&m); // 输入两个链表的节点个数
// 初始化第一个有序链表的头、尾指针(初始为空)
ll head1=NULL;
ll tail1=NULL;
// 初始化第二个有序链表的头、尾指针(初始为空)
ll head2=NULL;
ll tail2=NULL;
// 第一步:构建第一个有序单链表(尾插法)
for(int i=0;i<n;i++) {
// 动态分配内存创建新节点
ll temp=(ll)malloc(sizeof(node));
int num;
scanf("%d",&num); // 输入当前节点的数值
temp->data=num; // 给新节点赋值
temp->next=NULL; // 新节点的后继指针初始化为空
if (head1==NULL) { // 处理第一个节点(链表为空)
head1=temp; // 头指针指向第一个节点
tail1=temp; // 尾指针也指向第一个节点
}
else { // 处理后续节点(链表已有节点)
tail1->next=temp; // 原尾节点的后继指向新节点
tail1=tail1->next; // 尾指针后移到新节点
}
}
// 第二步:构建第二个有序单链表(尾插法,逻辑同第一个链表)
for (int j=0;j<m;j++) {
// 动态分配内存创建新节点
ll tem=(ll)malloc(sizeof(node));
int num;
scanf("%d",&num); // 输入当前节点的数值
tem->data=num; // 给新节点赋值
tem->next=NULL; // 新节点的后继指针初始化为空
if (head2==NULL) { // 处理第一个节点(链表为空)
head2=tem; // 头指针指向第一个节点
tail2=tem; // 尾指针也指向第一个节点
}
else { // 处理后续节点(链表已有节点)
tail2->next=tem; // 原尾节点的后继指向新节点
tail2=tail2->next; // 尾指针后移到新节点
}
}
// 第三步:合并两个有序单链表(核心逻辑)
ll head=NULL; // 合并后新链表的头指针
ll tail=NULL; // 合并后新链表的尾指针
ll first=head1;// 遍历第一个链表的指针(初始指向head1)
ll second=head2;// 遍历第二个链表的指针(初始指向head2)
// 先确定合并链表的第一个节点(取两个链表头中较小的那个)
if (head1->data<=head2->data) {
head=head1; // 新链表头指向第一个链表的头
tail=head1; // 新链表尾也指向第一个链表的头
first=first->next; // 第一个链表的遍历指针后移
}
else {
head=head2; // 新链表头指向第二个链表的头
tail=head2; // 新链表尾也指向第二个链表的头
second=second->next;// 第二个链表的遍历指针后移
}
// 同时遍历两个链表,逐个取较小的节点加入合并链表
while (first&&second) { // 两个链表都未遍历完时循环
if (first->data<=second->data) { // 第一个链表当前节点更小
tail->next=first; // 合并链表尾节点指向该节点
tail=first; // 合并链表尾指针后移
first=first->next; // 第一个链表遍历指针后移
}
else { // 第二个链表当前节点更小
tail->next=second; // 合并链表尾节点指向该节点
tail=second; // 合并链表尾指针后移
second=second->next;// 第二个链表遍历指针后移
}
}
// 处理剩余节点:其中一个链表已遍历完,直接拼接另一个链表的剩余部分
while (first!=NULL||second!=NULL) {
if (first!=NULL) { // 第一个链表还有剩余节点
tail->next=first;
tail=first;
first=first->next;
}
else { // 第二个链表还有剩余节点
tail->next=second;
tail=second;
second=second->next;
}
}
// 第四步:打印合并后的有序链表
ll x=head; // 遍历指针从合并链表的头开始
while (x!=NULL) {
printf("%d",x->data); // 打印当前节点的数值
if (x->next!=NULL) { // 不是最后一个节点时,打印空格分隔
printf(" ");
}
x=x->next; // 遍历指针后移
}
return 0;
}
3. 节点删除:从 "队伍" 里拿掉一个人
删除单链表的某个节点,核心是 "不让后面的节点跟丢"。
- 关键技巧:找到要删除节点的前一个节点(Pre),让 Pre 直接指向要删除节点的下一个节点,相当于跳过了要删除的节点。
- 注意事项:一定要记得把删除的节点 "置空" 并释放内存,避免内存泄漏,也防止后续操作出错。
代码示例(C 语言实现)
#include <stdio.h>
#include <stdlib.h>
// 复用上面定义的ListNode、createNode、printList函数
// 删除单链表中指定值的节点
ListNode* deleteNode(ListNode *head, int val) {
// 处理头节点就是要删除的情况
if (head != NULL && head->val == val) {
ListNode *temp = head;
head = head->next;
free(temp); // 释放被删除节点的内存
return head;
}
// 找要删除节点的前一个节点
ListNode *pre = head;
ListNode *current = head->next;
while (current != NULL) {
if (current->val == val) {
pre->next = current->next; // 跳过要删除的节点
free(current); // 释放内存
break;
}
pre = current;
current = current->next;
}
return head;
}
// 测试节点删除
int main() {
// 构建链表 1 -> 2 -> 3 -> 4 -> NULL
ListNode *head = createNode(1);
head->next = createNode(2);
head->next->next = createNode(3);
head->next->next->next = createNode(4);
printf("原链表:");
printList(head);
// 删除值为3的节点
head = deleteNode(head, 3);
printf("删除值为3的节点后:");
printList(head);
// 释放内存
ListNode *temp;
while (head != NULL) {
temp = head;
head = head->next;
free(temp);
}
return 0;
}
二、双向链表核心操作:构建、查找、删除
双向链表比单链表更灵活,每个节点既有 "前指针"(指向前一个节点),又有 "后指针"(指向后一个节点),能往前也能往后走。
1. 构建:搭起 "双向通行" 的队伍
- 关键技巧:每个新节点都要和前后节点 "互留联系方式"。
- 操作步骤:先创建头节点,之后新增节点时,让当前链表的尾节点的后指针指向新节点,同时让新节点的前指针指向尾节点,再把尾节点更新为新节点,就像队伍末尾新来的人,既要拉住前面的人,也要让后面的人能找到自己。
代码示例:双向链表构建(尾插法)
cpp
#include<stdio.h>
#include<stdlib.h>
// 定义双向链表节点结构体:包含数据域、前驱指针、后继指针
typedef struct Node{
int data; // 节点存储的数值
struct Node*pre; // 指向前一个节点的前驱指针
struct Node*next; // 指向后一个节点的后继指针
}node, *ll; // node等价于struct Node,ll简化struct Node*指针类型
int main(){
int n,m; // n:双向链表的节点总数;m:需要查找的关键字个数
scanf("%d %d",&n,&m); // 输入节点数n和关键字数m
ll head=NULL; // 双向链表的头指针,初始为空(链表为空)
ll tail=NULL; // 双向链表的尾指针,初始为空(尾插法构建链表用)
// 第一步:尾插法构建双向链表
for(int i=0;i<n;i++){
int num;
scanf("%d",&num); // 输入当前节点要存储的数值
// 动态分配内存创建新的双向链表节点
ll temp=(ll) malloc(sizeof(node));
temp->data=num; // 给新节点赋值
temp->next=NULL; // 新节点的后继指针初始化为空
temp->pre=NULL; // 新节点的前驱指针初始化为空
if(head==NULL){ // 处理第一个节点(链表为空)
head=temp; // 头指针指向第一个节点
tail=temp; // 尾指针也指向第一个节点
}
else{ // 处理后续节点(链表已有节点)
tail->next=temp; // 原尾节点的后继指向新节点
temp->pre=tail; // 新节点的前驱指向原尾节点(双向关联)
tail=temp; // 尾指针后移到新节点
}
}
// 第二步:逐个处理m个关键字,查找并输出前驱/后继
for(int j=0;j<m;j++){
int k;
scanf("%d",&k); // 输入要查找的关键字
ll x;
x=head; // 遍历指针从链表头开始
// 遍历双向链表,查找目标关键字
while(x!=NULL){
if(x->data==k){ // 找到目标关键字对应的节点
// 输出前驱节点的数值(如果有前驱)
if(x->pre!=NULL){
printf("%d ",x->pre->data);
}
// 输出后继节点的数值(如果有后继)
if(x->next!=NULL){
printf("%d",x->next->data);
}
}
x=x->next; // 未找到则遍历下一个节点
}
printf("\n"); // 每个关键字的输出占一行
}
return 0;
};
2. 查找:在 "队伍" 里找特定的人
- 关键技巧:可以从头节点开始往后找,也能从尾节点往前找,哪个近就用哪个。
- 操作步骤:设定一个指针,从头部(或尾部)出发,依次比对节点的数据,找到目标数据就停止;如果要找的节点前后有数据,还能直接获取它的前后节点信息,比单链表查找更方便。
代码示例:双向链表查找
#include <stdio.h>
#include <stdlib.h>
// 复用上面定义的DListNode、createDNode、buildDList、printDList函数
// 查找双向链表中指定值的节点(从头往后找)
DListNode* findDNodeFromHead(DListNode *head, int val) {
DListNode *p = head;
while (p != NULL) {
if (p->val == val) {
printf("找到值为%d的节点\n", val);
return p; // 找到返回节点指针
}
p = p->next;
}
printf("未找到值为%d的节点\n", val);
return NULL; // 没找到返回NULL
}
// 查找双向链表中指定值的节点(从尾往前找,需先获取尾节点)
DListNode* findDNodeFromTail(DListNode *head, int val) {
// 先找到尾节点
DListNode *tail = head;
if (tail == NULL) return NULL;
while (tail->next != NULL) {
tail = tail->next;
}
// 从尾节点往前找
DListNode *p = tail;
while (p != NULL) {
if (p->val == val) {
printf("从尾部找到值为%d的节点\n", val);
return p;
}
p = p->prev;
}
printf("从尾部未找到值为%d的节点\n", val);
return NULL;
}
// 测试双向链表查找
int main() {
int arr[] = {1, 2, 3, 4, 5};
int len = sizeof(arr) / sizeof(arr[0]);
DListNode *dHead = buildDList(arr, len);
printf("双向链表:");
printDList(dHead);
// 从头查找值为3的节点
DListNode *target1 = findDNodeFromHead(dHead, 3);
if (target1 != NULL) {
// 打印找到节点的前驱和后继值(验证双向指针)
printf("值为3的节点,前驱:%d,后继:%d\n", target1->prev->val, target1->next->val);
}
// 从尾查找值为2的节点
DListNode *target2 = findDNodeFromTail(dHead, 2);
// 释放内存
DListNode *temp;
while (dHead != NULL) {
temp = dHead;
dHead = dHead->next;
free(temp);
}
return 0;
}
3. 删除:比单链表更 "省心" 的删除
双向链表删除节点不用特意找前一个节点,因为节点自己就知道前一个是谁。
- 操作步骤:找到要删除的节点(b),让它前一个节点(a)的后指针指向它后一个节点(c),再让后一个节点(c)的前指针指向它前一个节点(a),最后释放 b 的内存。相当于 a 和 c 直接拉手,跳过了 b,不用像单链表那样回头找 a。
一、删除操作的 3 种场景
双向链表删除节点主要分 3 种情况,需分别处理(假设待删节点为p):
- 删除头节点 (
p == head); - 删除尾节点 (
p == tail); - 删除中间节点(既不是头也不是尾)。
二、删除的核心步骤(通用逻辑)
无论哪种场景,删除的核心思路是:
- 让待删节点
p的前驱节点 指向p的后继节点; - 让待删节点
p的后继节点 指向p的前驱节点; - 释放
p的内存(避免内存泄漏)。
三、分场景详细讲解(附代码示例)
先定义双向链表结构体:
typedef struct Node{
int data;
struct Node*pre;
struct Node*next;
}node, *ll;
场景 1:删除中间节点
-
条件:
p->pre != NULL且p->next != NULL; -
操作:让
p的前驱节点的next指向p的后继,让p的后继节点的pre指向p的前驱; -
代码:
// 假设已找到待删节点p(中间节点)
p->pre->next = p->next; // 前驱节点的后继 → p的后继
p->next->pre = p->pre; // 后继节点的前驱 → p的前驱
free(p); // 释放节点内存
p = NULL; // 避免野指针
场景 2:删除头节点
-
条件:
p == head(p->pre == NULL); -
操作:更新链表头指针为原头节点的后继,同时让新头节点的
pre置空; -
代码:
// 假设待删节点p是头节点
head = p->next; // 头指针后移到原头节点的后继
if(head != NULL){ // 若删除后链表非空
head->pre = NULL; // 新头节点的前驱置空
} else { // 若删除后链表为空(只有头节点)
tail = NULL; // 尾指针也置空
}
free(p);
p = NULL;
场景 3:删除尾节点
-
条件:
p == tail(p->next == NULL); -
操作:更新链表尾指针为原尾节点的前驱,同时让新尾节点的
next置空; -
代码:
// 假设待删节点p是尾节点
tail = p->pre; // 尾指针前移到原尾节点的前驱
if(tail != NULL){ // 若删除后链表非空
tail->next = NULL; // 新尾节点的后继置空
} else { // 若删除后链表为空(只有尾节点)
head = NULL; // 头指针也置空
}
free(p);
p = NULL;
四、完整删除函数(整合所有场景)
结合 "查找节点 + 删除节点",写出一个完整删除函数:
// 功能:删除双向链表中值为key的节点
// 参数:head(链表头指针)、tail(链表尾指针)、key(要删除的关键字)
void deleteNode(ll *head, ll *tail, int key) {
ll p = *head;
// 1. 查找值为key的节点
while(p != NULL && p->data != key) {
p = p->next;
}
if(p == NULL) { // 未找到该节点
printf("未找到关键字%d,无需删除\n", key);
return;
}
// 2. 分场景删除节点
if(p->pre == NULL) { // 场景2:删除头节点
*head = p->next;
if(*head != NULL) {
(*head)->pre = NULL;
} else {
*tail = NULL;
}
} else if(p->next == NULL) { // 场景3:删除尾节点
*tail = p->pre;
if(*tail != NULL) {
(*tail)->next = NULL;
} else {
*head = NULL;
}
} else { // 场景1:删除中间节点
p->pre->next = p->next;
p->next->pre = p->pre;
}
// 3. 释放内存
free(p);
p = NULL;
printf("成功删除关键字%d\n", key);
}
五、使用示例(结合之前的双向链表构建示例代码代码)
cpp
int main() {
// 先构建双向链表(和之前的代码一致)
ll head=NULL, tail=NULL;
int n=5;
int nums[] = {1,2,3,4,5};
for(int i=0; i<n; i++) {
ll temp=(ll)malloc(sizeof(node));
temp->data = nums[i];
temp->pre=NULL;
temp->next=NULL;
if(head==NULL) {
head=temp;
tail=temp;
} else {
tail->next=temp;
temp->pre=tail;
tail=temp;
}
}
// 调用删除函数
deleteNode(&head, &tail, 3); // 删除中间节点3
deleteNode(&head, &tail, 1); // 删除头节点1
deleteNode(&head, &tail, 5); // 删除尾节点5
// 打印删除后的链表
ll x=head;
while(x!=NULL) {
printf("%d ", x->data);
x=x->next;
}
// 输出:2 4
return 0;
}