单链表的插入和合并以及双链表的删除

一、单链表核心操作:逆置、合并、删除

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):

  1. 删除头节点p == head);
  2. 删除尾节点p == tail);
  3. 删除中间节点(既不是头也不是尾)。
二、删除的核心步骤(通用逻辑)

无论哪种场景,删除的核心思路是:

  1. 让待删节点p前驱节点 指向p的后继节点;
  2. 让待删节点p后继节点 指向p的前驱节点;
  3. 释放p的内存(避免内存泄漏)。
三、分场景详细讲解(附代码示例)

先定义双向链表结构体:

复制代码
typedef struct Node{
    int data;
    struct Node*pre;
    struct Node*next;
}node, *ll;
场景 1:删除中间节点
  • 条件:p->pre != NULLp->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 == headp->pre == NULL);

  • 操作:更新链表头指针为原头节点的后继,同时让新头节点的pre置空;

  • 代码:

    // 假设待删节点p是头节点
    head = p->next; // 头指针后移到原头节点的后继
    if(head != NULL){ // 若删除后链表非空
    head->pre = NULL; // 新头节点的前驱置空
    } else { // 若删除后链表为空(只有头节点)
    tail = NULL; // 尾指针也置空
    }
    free(p);
    p = NULL;

场景 3:删除尾节点
  • 条件:p == tailp->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;
}
相关推荐
薛定e的猫咪1 小时前
【NeurIPS 2023】多目标强化学习算法工具库-MORL-Baselines
人工智能·算法·机器学习
Tisfy2 小时前
LeetCode 3507.移除最小数对使数组有序 I:纯模拟
算法·leetcode·题解·模拟·数组
努力学算法的蒟蒻2 小时前
day63(1.22)——leetcode面试经典150
算法·leetcode·面试
永远都不秃头的程序员(互关)2 小时前
【决策树深度探索(一)】从零搭建:机器学习的“智慧之树”——决策树分类算法!
算法·决策树·机器学习
程序员-King.2 小时前
day161—动态规划—最长递增子序列(LeetCode-300)
算法·leetcode·深度优先·动态规划·递归
西柚小萌新2 小时前
【计算机视觉CV:目标检测】--3.算法原理(SPPNet、Fast R-CNN、Faster R-CNN)
算法·目标检测·计算机视觉
高频交易dragon2 小时前
Hawkes LOB Market从论文到生产
人工智能·算法·金融
_OP_CHEN2 小时前
【算法基础篇】(五十)扩展中国剩余定理(EXCRT)深度精讲:突破模数互质限制
c++·算法·蓝桥杯·数论·同余方程·扩展欧几里得算法·acm/icpc
福楠2 小时前
C++ STL | set、multiset
c语言·开发语言·数据结构·c++·算法