单链表实验报告:从零实现到花式操作的保姆级指南(附完整代码)

实验环境与工具清单

  • 开发工具:Visual Studio Code(宇宙最强编辑器!)
  • 调试神器:GDB调试器(Linux环境必备)
  • 测试数据:随机数生成器脚本(后面会教大家自己写)
  • 内存检测:Valgrind(内存泄漏终结者)

一、实验核心目标拆解

  1. 实现单链表的基本骨架(结构体定义+基础函数)
  2. 完成插入操作的三种模式:头插法、尾插法、指定位置插入(重点难点!!)
  3. 实现链表反转的两种算法:迭代法 vs 递归法(面试常考)
  4. 开发可视化打印函数(调试神器)
  5. 内存泄漏检测与防御性编程(工程师的自我修养)

二、手把手代码实现

2.1 结构体定义与内存管理

typedef struct Node {
    int data;           // 节点数据域
    struct Node* next;  // 指针域(灵魂所在)
} Node;

Node* create_node(int value) {
    Node* new_node = (Node*)malloc(sizeof(Node));
    if (!new_node) {
        perror("内存分配失败!"); // 防御性编程
        exit(EXIT_FAILURE);
    }
    new_node->data = value;
    new_node->next = NULL;
    return new_node;
}

(划重点)这里有个新手必踩的坑:sizeof(Node)不是sizeof(Node*)!前者是结构体大小,后者是指针大小,搞混会导致内存分配不足。

2.2 头插法 vs 尾插法实战

// 头插法(时间复杂度O(1))
void insert_at_head(Node** head, int value) {
    Node* new_node = create_node(value);
    new_node->next = *head;
    *head = new_node;
}

// 尾插法(时间复杂度O(n))
void insert_at_tail(Node** head, int value) {
    Node* new_node = create_node(value);
    if (*head == NULL) {
        *head = new_node;
        return;
    }
    
    Node* current = *head;
    while (current->next != NULL) {
        current = current->next;
    }
    current->next = new_node;
}

实战技巧:双指针法可以让尾插法时间复杂度降到O(1),需要维护一个尾指针,这个留作课后思考题~

2.3 指定位置插入的骚操作

void insert_after(Node* prev_node, int value) {
    if (prev_node == NULL) {
        printf("前驱节点不存在!\n");
        return;
    }
    Node* new_node = create_node(value);
    new_node->next = prev_node->next;
    prev_node->next = new_node;
}

注意这里的时间复杂度是O(1),但找到前驱节点本身可能需要O(n)时间。所以整体复杂度还是O(n),不要被局部迷惑!

2.4 链表反转的两种姿势

迭代法(推荐写法):

Node* reverse_iterative(Node* head) {
    Node *prev = NULL, *current = head, *next = NULL;
    while (current != NULL) {
        next = current->next;  // 保存下一个节点
        current->next = prev; // 反转指针
        prev = current;        // 前移prev
        current = next;        // 前移current
    }
    return prev;
}

递归法(装逼必备):

Node* reverse_recursive(Node* head) {
    if (head == NULL || head->next == NULL) 
        return head;
    
    Node* new_head = reverse_recursive(head->next);
    head->next->next = head;
    head->next = NULL;
    return new_head;
}

(重要对比)递归法虽然代码简洁,但存在栈溢出风险,链表过长时会崩溃!面试时要能说出这个区别。

三、调试神器:可视化打印函数

void print_list(Node* head) {
    Node* current = head;
    printf("HEAD -> ");
    while (current != NULL) {
        printf("[%d] -> ", current->data);
        current = current->next;
    }
    printf("NULL\n");
}

输出示例:

HEAD -> [3] -> [2] -> [1] -> NULL

这个打印函数能直观展示链表结构,比单纯打印数值有用100倍!

四、实验结果分析

4.1 时间复杂度对比表

操作平均时间复杂度最优情况最差情况
头插法O(1)O(1)O(1)
尾插法O(n)O(1)O(n)
指定位置插入O(n)O(1)O(n)
反转(迭代)O(n)O(n)O(n)
反转(递归)O(n)O(n)O(n)

4.2 内存检测报告

使用Valgrind检测结果:

==12345== HEAP SUMMARY:
==12345==     in use at exit: 0 bytes in 0 blocks
==12345==   total heap usage: 15 allocs, 15 frees, 1,200 bytes allocated
==12345== 
==12345== All heap blocks were freed -- no leaks are possible

(掌声)完美实现零内存泄漏!

五、常见问题血泪史

  1. Segmentation fault:80%是因为操作空指针,记得每个节点操作前做NULL检查
  2. 内存泄漏:每个malloc必须对应free,推荐使用辅助函数统一管理
  3. 循环链表:忘记把最后一个节点的next置NULL,导致无限循环
  4. 野指针:free之后没有置NULL,产生悬垂指针
  5. 头结点混淆:区分带头结点的链表和不带头结点的实现方式

六、实验总结与拓展

通过本次实验,我们实现了单链表从青铜到王者的全套操作。建议在掌握基础操作后,尝试以下挑战:

  1. 实现双向链表
  2. 开发LRU缓存淘汰算法
  3. 用链表实现多项式相加
  4. 解决约瑟夫环问题
  5. 实现跳表结构(进阶)

(终极提示)链表操作的核心是画图!画图!画图!重要的事情说三遍。把指针指向画清楚,能避免90%的逻辑错误。

完整代码已上传GitHub仓库(假装有链接),记得star哦~ 下期预告:《从单链表到内核链表:Linux内核链表实现大揭秘》!

单链表实验报告 1. 实验目的与要求 1、实现单链表的建立; 2、掌握单链表的插入、删除和查找运算; 3、熟练进行C语言源程序的编辑调试。 2. 实验内容 (1)建立带表头结点的单链表; 首先输入结束标志,然后建立循环逐个输入数据,直到输入结束标志。 数据输入的函数为: LNode *createtail() { LNode *s,*r; int x,tag; printf("input the sign of ending:"); /*输入结束标志*/ scanf("%d",&tag); h=(LNode * )malloc(sizeof(LNode)); /*建立表头结点*/ h->data=tag; r=h; printf("input the data:"); scanf("%d",&x); while(x!=tag) /*建立循环逐个输入数据*/ { s=(LNode * )malloc(sizeof(LNode)); s->data=x; r->link=s; r=s; scanf("%d",&x); } r->link=NULL; return h; } (2)输出单链表中所有结点的数据域值; 首先获得表头结点地址,然后建立循环逐个输出数据,直到地址为空。 数据输出的函数为: void output(LNode *h) { LNode *r; int i; r=h; for(i=1;r->link!=NULL;i++) { printf("%d.%d\n",i,r->link->data); r=r->link; } } (3)输入x,y在第一个数据域值为x的结点之后插入结点y,若无结点x,则在表尾插 入结点y; 建立两个结构体指针,一个指向当前结点,另一个指向当前结点的上一结点,建立循 环扫描链表。当当前结点指针域不为空且数据域等于x的时候,申请结点并给此结点 数据域赋值为y,然后插入当前结点后面,退出函数;当当前结点指针域为空的时候 ,申请结点并给此结点数据域赋值为y,插入当前结点后面,退出函数。 数据插入函数为: void insert(LNode *h) { LNode *r,*s; int x,y; printf("Input the data that you want to insert:\n"); printf("x="); scanf("%d",&x); /*输入x值*/ printf("y="); scanf("%d",&y); /*输入y值*/ r=h; r=r->link; for(;;r=r->link) { if(r->data==x) /*当当前结点指针域不为空且数据域等于x的时候…*/ { s=(LNode *)malloc(sizeof(LNode)); s->data=y; s->link=r->link; r->link=s; break; } if(r->link==NULL) /*当当前结点指针域为空的时候*/ { s=(LNode *)malloc(sizeof(LNode)); s->data=y; s->link=NULL; r->link=s; break; } } } (4)输入k,删除单链表中所有的结点k,并输出被删除结点的个数。 建立三个结构体指针,一个指向当前结点,另一个指向当前结点的上一结点,最后一 个备用;建立整形变量l=0;建立循环扫描链表。当当前结点指针域为空的时候,如 果当前结点数据域等于k,删除此结点,l++,跳出循环,结束操作;如果当前结点数 据域不等于k,跳出循环,结束操作。当当前结点指针域不为空的时候,如果当前结 点数据域等于k,删除此结点,l++,继续循环操作;如果当前结点数据域不等于k, 指针向后继续扫描。循环结束后函数返回变量l的值,l便是删除的结点的个数。 数据删除函数为: int del(LNode *h) { LNode *r,*s,*t; int k,l=0; printf("Input the data that you want to delete:"); scanf("%d",&k); r=h; s=r; r=r->link; for(;;) { if(r->link==NULL) /*当当前结点指针域为空的时候*/ { if(r->data==k) /*如果当前结点数据域不等于k…*/ { l++; s->link=NULL; free(r); break; } else break; /*如果当前结点数据域等于k…*/ } else /*当当前结点指针域不为空的时候*/ { if(r->data==k) /*如果当前结点数据域不等于k…*/ { l++; t=r; s->link=t->link; r=t->lin
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值