手撕单链表(单向,不循环,不带头结点)的基本操作

𝙉𝙞𝙘𝙚!!👏🏻‧✧̣̥̇‧✦👏🏻‧✧̣̥̇‧✦ 👏🏻‧✧̣̥̇:Solitary-walk

      ⸝⋆   ━━━┓
     - 个性标签 - :来于“云”的“羽球人”。 Talk is cheap. Show me the code
┗━━━━━━━  ➴ ⷯ

本人座右铭 :   欲达高峰,必忍其痛;欲戴王冠,必承其重。

👑💎💎👑💎💎👑 
💎💎💎自💎💎💎
💎💎💎信💎💎💎
👑💎💎 💎💎👑    希望在看完我的此篇博客后可以对你有帮助哟

👑👑💎💎💎👑👑   此外,希望各位大佬们在看完后,可以互赞互关一下,看到必回
👑👑👑💎👑👑👑

 目录:

前言:对于单链表的基本操作重在考验大家对C语言指针的底子

一:传值传参区别

二:尾插

三:头插

四:尾删

五:头删

六:指定数据的查找

七:指定位置之前的删除

八:指定位置之后的删除

九:任意位置之前的插入

十:任意位置之后的插入

结语


 一:传值传参区别

这里就拿一个比较经典的问题来引入吧!

   写一个函数实现2个数 的交换

 

对于刚刚接触编程的铁子们,对这个结果 可能存在很大的疑惑

不慌不忙,接下来我慢慢给大家解释 

int a = 1, b = 2;
    int* p = &a;
    *p = 3;

想必大家对这个代码应该不会很陌生吧。

此时我们对指针p进行解引用拿到的就是变量 a 

也就是说,此时我们通过借助指针实现了对a   的改变

同理,这里我在调用Swap( )这个函数的时候,是不是进行传地址就可以实现对2个数的交换?

话不多说,接下来我们代码实现

 

 是滴,此时确实实现了2个数的交换

分析:

1)传参的本质:形参是对实参的一份临时拷贝,对形参的临时修改不会影响实参

2)所以说:当需要对变量进行改变的时候,我们就需要传对应的地址就可以

如何理解“ 传对应的地址”

比如说:

      改变int 类型的变量,这时就需要传int*的指针(地址)    

     改变int *类型的变量,这时就需要传int**的指针(地址)    

     改变结构体类型的变量,这时就需要传结构体的指针(地址)

 二:尾插

分析:

1)首先为要插入进来的数据开辟结点

2)链表不为空的时候:注意此时要改变的是结构体

         首先 先找到尾结点( 链表最后一个结点的next为空)

         其次进行尾插

3)链表为空:注意此时改变的是结构体类型的指针(头节点为空)

         直接进行插入即可

草图如下:

对非空的链表插入前:

 插入后:

   接下来可是重头戏,好好看,一不仔细,就错失了,那可就不好理解了,避免这个“瓜”没有吃到,反而懊恼不已

 1)开辟结点为插入的数据:
因为之后插入需要频繁开辟结点,所以这里写成了一个函数
SLNode* BuyNode(DataType x)
{SLNode* newnode = (SLNode*)malloc(sizeof(SLNode));if (newnode == NULL){perror("malloc fail\n");return NULL;}// 对开辟的结点进行初始化newnode->data = x;newnode->next = NULL;return newnode;
}
2)找尾结点:这里就依次遍历即可

注意啦:看看这样写对不???

SLNode* ptail = phead;
    while (ptail)
    {
        ptail = ptail->next;
    }
    ptail->next = newnode;

NO,NO,NO

乍一看,看不出啥问题,这就是“码图”结合了

这里以逻辑结构(人类的思维,为了形象化的理解)来解释

逻辑图:

 

   当ptail这个指针指向3这个结点的时候,是不为空的,所以就继续进入我的循环里,此时的尾结点就变成了NULL这个对应当结点,下面在进行插入自然也是不能把新的结点和我原来链表进行有效的连接起来

找尾结点的正确代码:

 while (ptail->next){ptail = ptail->next;}ptail->next = newnode;

 3)当链表为空的时候我们发现以上代码不可取:

因为此时实参plist  传给我的phead这个形参就是一份临时拷贝,我对phead(结构体指针)改变不影响plist(结构体指针)的变化

有了前面那个传值传参的引入,想必大家此时应该有了见解了吧

没错!

就是实参传结构体指针的地址,形参用二级指针

 对应完整代码:

void SLPushBack(SLNode* *phead, DataType x)
{/*1:开辟结点2:判读是否为空的链表3:非空:找到尾结点;此时改变的是结构体,需要传结构体的地址4:为空:直接插入:  因为改变的是结构体类型的指针,所有需要传结构体类型的指针的地址,涉及到二级指针*/SLNode* newnode = BuyNode(x);if (*phead == NULL)  //为空{newnode->next = *phead;// 对*phead解引用 就是plist这个实参*phead = newnode;return;}//非空/*  找尾结点:errSLNode* ptail = phead;while (ptail){ptail = ptail->next;}ptail->next = newnode;*/SLNode* ptail = *phead;while (ptail->next)  //找尾结点{ptail = ptail->next;}ptail->next = newnode;/*当链表为空的时候,以上代码有问题为空的时候需要对头节点进行改变,注意头节点是结构体类型指针所以需要传地址*/}

 三:头插

分析:

1) 首先为插入数据开辟结点

2)因为此时改变的是结构体指针(plist),所以需要传入结构体指针的地址

void SLPushFront(SLNode** phead, DataType x)
{/*1:为x开辟结点2:更新头节点3:因为改变的是结构体类型的指针,所有需要传结构体类型的指针的地址传参的本质是:拷贝:形参是对实参的一份临时拷贝,对形参的修改不会影响我实参的变化*/SLNode* newnode = BuyNode(x);newnode->next = *phead;// 对*phead解引用 就是plist这个实参*phead = newnode;
}

四:尾删

分析:

1)首先判断链表是否为空;为空不需删除

2)其次:判断链表是否为一个结点;因为此时改变的头节点(结构体指针);那就涉及到了传结构体指针的地址

3)最后就是多个结点的情况:

      先找尾结点

       删除尾结点

 1)先从正常情况说起(多个结点)

 找尾结点:这里需要找到尾结点的前一个结点,避免free(ptail)时找不到新的尾结点

 尾删后:

SLNode* ptail = *phead;SLNode* pre =* phead;while (ptail->next)  //找尾结点{pre = ptail;//保存尾结点的前一个结点ptail = ptail->next;}free(ptail);ptail = pre;//尾结点更新ptail->next = NULL;//不要忘了置空
2)只有一个结点

注意这里需要传入结构体指针的地址

if ((*phead)->next == NULL)  //一个结点,注意*与->优先级{free(*phead);*phead = NULL;return;}
 3)判空

直接暴力检查即可:

assert(*phead);

 对应完整代码:

void SLPopBack(SLNode** phead)
{/*1:判断是否为空2:判断是否为一个结点:因为此时改变的是头节点(结构体指针)3:找到尾结点,此时尾结点的前一个结点成为新的结点4:  *phead 就是头指针  plist*///为空:assert(*phead);if ((*phead)->next == NULL)  //一个结点,注意*与->优先级{free(*phead);*phead = NULL;return;}// 非空SLNode* ptail = *phead;SLNode* pre =* phead;while (ptail->next)  //找尾结点{pre = ptail;//保存尾结点的前一个结点ptail = ptail->next;}free(ptail);ptail = pre;//尾结点更新ptail->next = NULL;//不要忘了置空}

五:头删

相信有了前面的尾删,我们对头删那便是轻轻松拿捏了

1)判空

2)非空

 1)判空

assert(*phead);  //直接暴力检查

2)非空:  删除头节点之前需要保存一下

对应完整代码:

void SLPopFront(SLNode** phead)
{/*1:判是否为空 2:非空:删除头节点之前需要保存一下第二个结点*/assert(*phead);//为空SLNode* psec = (*phead)->next;//保存第二个结点free(*phead);*phead = psec;//更新}
六:指定数据查找

1:若是当前数据存在,则返回对应的结点;否则返回NULL

2:依次遍历

相信有了前面的基础,我们对这个区区查找的代码轻轻松拿下

SLNode* SLFind(SLNode* phead, DataType x)
{/*若是找到返回该节点循环遍历*/SLNode* pcur = phead;while (pcur){if (pcur->data == x)return pcur;//返回节点elsepcur = pcur->next;//更新}return NULL;
}
七:指定位置之前的删除 

分析:

假设对pos这个位置之前的进行删除

1:pos若是为头节点,则不需要删除

2:pos为第二个结点,其实就是进行头删的操作,注意此时改变的是头节点(结构体指针),所以需要传二级指针

3:正常情况:找到pos前一个结点

 1:pos为头节点

直接暴力断言,就像当你作业还没有写完,但你依然再玩游戏此时你的父亲突然过来问你,作业写完了吗,你回答到:没有。你父亲直接就是一顿说,此时你就乖乖去写作业了

assert(*phead != pos);

2:pos 为第二个结点

if ((*phead)->next == pos)//pos为第二个结点
    {
        free(*phead);
        *phead = pos;//pos是新的头节点
    }

3:正常情况
SLNode* pre = *phead;while (pre->next->next != pos){pre = pre->next;}free(pre->next);pre->next = pos;

对应完整代码: 

void SLEarseBefore(SLNode** phead, SLNode* pos)
{/*1:pos为头节点是不可以删除的2:pos为第二个结点,此时要删除的是头节点,改变的是结构体指针(phead),需要二级指针3:正常情况,找到pos前一个结点*/assert(pos != *phead);//保证pos不为头节点if ((*phead)->next == pos)//pos为第二个结点{free(*phead);*phead = pos;//pos是新的头节点}else{SLNode* pre = *phead;while (pre->next->next != pos){pre = pre->next;}free(pre->next);pre->next = pos;}
}
八:指定位置之后的删除

分析:假设要删除的位置是pos

    1:pos为最后一个结点;没有必要删除
    2:pos不为最后一个结点

对应代码:

void SLEarseAfter(SLNode** phead, SLNode* pos)
{/*1:pos为最后一个结点;没有必要删除2:pos不为最后一个结点*/assert(pos->next != NULL);//暴力判断是否为最后一个SLNode* del = pos->next;pos->next = del->next;free(del);del = NULL;
}
九:任意位置之前的插入

假设任意位置为pos

1:pos为头节点:可以借助头插的函数进行,注意此时改变的是头节点(结构体指针),要传结构体指针的地址

2:pos不是头节点:需要找到pos的前一个结点

3:为要插入的数据开辟结点

 对应代码:

void SLInsertBefore(SLNode** phead, SLNode* pos, DataType x)
{/**1:开辟结点2:找到pos前面的结点(pos不是头节点)3:pos是头节点此时变成头插*/SLNode* newnode = BuyNode(x);if (pos == *phead){SLPushFront(phead, x);return;}else{SLNode* pre = *phead;while (pre->next != pos){pre = pre->next;}//插入pre->next = newnode;newnode->next = pos;}
}
十:任意位置之后的插入

分析:假设此位置是pos

1:开辟结点

2:保存pos后面的那个结点SLNode* p =  pos->next

3: 直接插入

void SLInsertAfter(SLNode* phead, SLNode* pos, DataType x)
{/*1:开辟结点2:保存一下pos后面的那结点(否则会连不上)3:直接插入*/SLNode* newnode = BuyNode(x);SLNode* p = pos->next;//保存pos后的结点//插入pos->next = newnode;newnode->next = p;
}

 整个单链表完整代码:

SList.h
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>typedef int DataType;
typedef struct SListNode
{DataType data;//数据域struct SListNode* next;//指针域
}SLNode;void SLPrint(SLNode* phead);
void SLPushFront(SLNode** phead, DataType x);
void SLPushBack(SLNode** phead, DataType x);
void SLPopBack(SLNode** phead);
void SLPopFront(SLNode** phead); 
SLNode* SLFind(SLNode* phead, DataType x);//对指定数据进行查找
void SLModify(SLNode* phead, SLNode*pos,DataType x);void SLInsertBefore(SLNode** phead, SLNode* pos, DataType x);//在指定数据之前插入
void SLInsertAfter(SLNode* phead, SLNode* pos, DataType x);//在指定数据之前插入void SLEarseBefore(SLNode** phead, SLNode* pos);//任意位置之前的删除
void SLEarseAfter(SLNode** phead, SLNode* pos);//任意位置之后的删除
SList.c
#define _CRT_SECURE_NO_WARNINGS 1
#include"SList.h"void SLPrint(SLNode* phead)
{SLNode* pcur = phead;while (pcur){printf("%d->", pcur->data);pcur = pcur->next;//更新}printf("NULL\n");
}
SLNode* BuyNode(DataType x)
{SLNode* newnode = (SLNode*)malloc(sizeof(SLNode));if (newnode == NULL){perror("malloc fail\n");return NULL;}// 对开辟的结点进行初始化newnode->data = x;newnode->next = NULL;return newnode;
}
void SLPushFront(SLNode** phead, DataType x)
{/*1:为x开辟结点2:更新头节点3:因为改变的是结构体类型的指针,所有需要传结构体类型的指针的地址传参的本质是:拷贝:形参是对实参的一份临时拷贝,对形参的修改不会影响我实参的变化*/SLNode* newnode = BuyNode(x);newnode->next = *phead;// 对*phead解引用 就是plist这个实参*phead = newnode;
}
void SLPushBack(SLNode* *phead, DataType x)
{/*1:开辟结点2:判读是否为空的链表3:非空:找到尾结点;此时改变的是结构体,需要传结构体的地址4:为空:直接插入:  因为改变的是结构体类型的指针,所有需要传结构体类型的指针的地址,涉及到二级指针*/SLNode* newnode = BuyNode(x);if (*phead == NULL)  //为空{newnode->next = *phead;// 对*phead解引用 就是plist这个实参*phead = newnode;return;}//非空/*  找尾结点:errSLNode* ptail = phead;while (ptail){ptail = ptail->next;}ptail->next = newnode;*/SLNode* ptail = *phead;while (ptail->next)  //找尾结点{ptail = ptail->next;}ptail->next = newnode;/*当链表为空的时候,以上代码有问题为空的时候需要对头节点进行改变,注意头节点是结构体类型指针所以需要传地址*/}
void SLPopBack(SLNode** phead)
{/*1:判断是否为空2:判断是否为一个结点:因为此时改变的是头节点(结构体指针)3:找到尾结点,此时尾结点的前一个结点成为新的结点4:  *phead 就是头指针  plist*///为空:assert(*phead);if ((*phead)->next == NULL)  //一个结点,注意*与->优先级{free(*phead);*phead = NULL;return;}// 非空SLNode* ptail = *phead;SLNode* pre =* phead;while (ptail->next)  //找尾结点{pre = ptail;//保存尾结点的前一个结点ptail = ptail->next;}free(ptail);ptail = pre;//尾结点更新ptail->next = NULL;//不用忘了置空//对一个与多个节点的操作可以合并//只要找到倒数第二个结点就可以/*SLNode* ptail = *phead;while (ptail->next->next){ptail = ptail->next;}free(ptail->next);ptail->next = NULL;*/}
void SLPopFront(SLNode** phead)
{/*1:判是否为空 2:非空:删除头节点之前需要保存一下第二个结点*/assert(*phead);//为空SLNode* psec = (*phead)->next;//保存第二个结点free(*phead);*phead = psec;//更新
}
SLNode* SLFind(SLNode* phead, DataType x)
{/*若是找到返回该节点循环遍历*/SLNode* pcur = phead;while (pcur){if (pcur->data == x)return pcur;//返回节点elsepcur = pcur->next;//更新}return NULL;
}
void SLInsertBefore(SLNode** phead, SLNode* pos, DataType x)
{/**1:开辟结点2:找到pos前面的结点(pos不是头节点)3:pos是头节点此时变成头插*/SLNode* newnode = BuyNode(x);if (pos == *phead){SLPushFront(phead, x);return;}else{SLNode* pre = *phead;while (pre->next != pos){pre = pre->next;}//插入pre->next = newnode;newnode->next = pos;}
}
void SLInsertAfter(SLNode* phead, SLNode* pos, DataType x)
{/*1:开辟结点2:保存一下pos后面的那结点(否则会连不上)3:直接插入*/SLNode* newnode = BuyNode(x);SLNode* p = pos->next;//保存pos后的结点//插入pos->next = newnode;newnode->next = p;
}
void SLEarseBefore(SLNode** phead, SLNode* pos)
{/*1:pos为头节点是不可以删除的2:pos为第二个结点,此时要删除的是头节点,改变的是结构体指针(phead),需要二级指针3:正常情况,找到pos前一个结点*/assert(pos != *phead);//保证pos不为头节点if ((*phead)->next == pos)//pos为第二个结点{free(*phead);*phead = pos;//pos是新的头节点}else{SLNode* pre = *phead;while (pre->next->next != pos){pre = pre->next;}free(pre->next);pre->next = pos;SLNode* pre = *phead;while (pre->next->next != pos){pre = pre->next;}free(pre->next);pre->next = pos;}
}
void SLEarseAfter(SLNode** phead, SLNode* pos)
{/*1:pos为最后一个结点;没有必要删除2:pos不为最后一个结点*/assert(pos->next != NULL);//暴力判断是否为最后一个SLNode* del = pos->next;pos->next = del->next;free(del);del = NULL;
}
void SLModify(SLNode* phead, SLNode* pos, DataType x)
{assert(phead);pos->data = x;}
test.c

#define _CRT_SECURE_NO_WARNINGS 1
#include"SList.h"void TestPush()
{SLNode* plist = NULL;//SLPushFront(&plist,1);//SLPushFront(&plist,2);//SLPushFront(&plist,3);//SLPrint(plist);SLPushBack(&plist, 4);SLPushBack(&plist, 5);SLPrint(plist);}
void TestPop()
{SLNode* plist = NULL;SLPushBack(&plist, 4);SLPushBack(&plist, 5);SLPushBack(&plist, 6);SLPushBack(&plist, 7);SLPrint(plist);SLNode* pos = SLFind(plist, 7);if(pos)//避免pos为空SLInsertAfter(plist, pos, 44);SLPrint(plist);/*SLPopBack(&plist);SLPrint(plist);SLPopBack(&plist);*//*SLPopFront(&plist);SLPrint(plist);SLPopFront(&plist);SLPrint(plist);*/}
void TestEarse()
{SLNode* plist = NULL;SLPushBack(&plist, 4);SLPushBack(&plist, 5);SLPushBack(&plist, 6);SLPushBack(&plist, 7);SLPrint(plist);SLNode* pos = SLFind(plist, 7);if (pos)//避免pos为空//SLEarseAfter(&plist, pos);SLModify(plist, pos, 77);SLPrint(plist);
}void Swap(int *x, int* y)
{int tmp = *x;//中间变量*x = *y;*y = tmp;
}
int main()
{TestEarse();return 0;/*总结:1:是指针不一定必须断言,是否断言取决于你的操作2:在函数外面改变变量,需要传地址,想改变谁,就传对应类型的地址3:对于链表这块一定注意自己要改变的是结构体还是结构体指针???因为这决定了传的地址类型不一样4:找*/
}

结语:

对于初学单链表的小白来讲(比如我本人,哈哈哈),这个理解起来确实不是那么顺手,其实重点在指针和结构体的掌握。当然自己也需要反复的体会其中的奥妙,

都看到这里了,屏幕前的你,咱一波关注走起呗,你的支持是我不懈的动力,蟹蟹

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/337639.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【详解:两数之和三数之和四数之和】

本文讲解两数之和&#xff0c;三数之和和四数之和这三道经典双指针类型题。会在详解题目的同时给出AC代码【这三个题目从前往后是循序渐进的&#xff0c;非常巧妙】 目录 1、查找总价格为目标值的两个商品 2、三数之和 3、力扣18. 四数之和 1、查找总价格为目标值的两个商品…

SCS模型(径流曲线法)概述

目录 1.介绍&#xff1a;2.计算公式&#xff1a;参考文献&#xff1a;小结&#xff1a; 1.介绍&#xff1a; SCS模型&#xff08;径流曲线法&#xff09;是由美国农业部水土保持局(Soil Conservation Service) 基于经验提出&#xff0c;最初用于预测在农业用地小型流域降雨所累…

Django 框架添加管理员,完成对普通用户信息管理

前情回顾&#xff1a;Django框架 完成用户登录注册 文章目录 1.创建管理员2.完善管理员功能2.1增加管理员登录功能2.2完善展示用户信息功能2.3完善修改用户信息功能2.4完善删除用户信息功能 1.创建管理员 一般管理员都是直接指定&#xff0c;不开放页面注册&#xff0c;可以直…

【数据库系统概论】期末复习1

试述数据、数据库、数据库系统、数据库管理系统的概念。试述文件系统与数据库系统的区别和联系。试述数据库系统的特点。数据库管理系统的主要功能有哪些&#xff1f;试述数据库系统三级模式结构&#xff0c;这种结构的优点是什么&#xff1f;什么叫数据与程序的物理独立性&…

2024-01-03 无重叠区间

435. 无重叠区间 思路&#xff1a;和最少数量引爆气球的箭的思路基本都是一致了&#xff01;贪心就是比较左边的值是否大于下一个右边的值 class Solution:def eraseOverlapIntervals(self, points: List[List[int]]) -> int:points.sort(keylambda x: (x[0], x[1]))# 比较…

基于JAVA+SpringBoot的高校学术报告系统

✌全网粉丝20W,csdn特邀作者、博客专家、CSDN新星计划导师、java领域优质创作者,博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌ &#x1f345;文末获取项目下载方式&#x1f345; 一、项目背景介绍&#xff1a; 智慧高校学术报告系统…

系统添加深色模式实现方案

业务需求,夜间看系统太刺眼,要求添加夜间模式 效果如下: 依赖如下: 参考了官方解决方案,尝试后没有有效的解决. 官方解决方案 后续打算换框架,发现antdesign pro vue版本的暗黑模式禁用了. ant design pro 预览地址 思路: 引入andesign 暗黑模式的样式 , 手动修改自定义类…

VLAN原理与配置

0x00 前言 本节主要记录VLAN相关的内容。 传统以太网的缺点 广播域越大&#xff0c;产生的网络安全问题&#xff0c;垃圾流量问题越严重。 什么是VLAN&#xff1f; Virtual Local Area NetWork 虚拟局域网技术。 VLAN的特点是什么 一个VLAN就是一个广播域&#xff0c;在…

AWS CodeCommit创建git库

问题 在AWS云国际站上面使用CodeCommit服务创建git代码库。这里假设本地已经安装好git&#xff0c;ssh等工具&#xff0c;并且本地已经创建好相关公私钥文件&#xff0c;熟悉git和ssh通用配置文件&#xff0c;这里不会介绍windows和pc上面的ssh和git配置文件差异&#xff0c;需…

RocketMQ 消费重试

消费者出现异常&#xff0c;消费某条消息失败时&#xff0c; Apache RocketMQ 会根据消费重试策略重新投递该消息进行故障恢复。本文介绍消费重试机制的原理、版本兼容性和使用建议。 一、应用场景​ Apache RocketMQ 的消费重试主要解决的是业务处理逻辑失败导致的消费完整性…

内裤洗衣机有用吗?五款小型洗衣机全自动推荐

随着内衣洗衣机的流行&#xff0c;很多小伙伴在纠结该不该入手一款内衣洗衣机&#xff0c;专门来洗一些贴身衣物&#xff0c;答案是非常有必要的&#xff0c;因为我们现在市面上的大型洗衣机只能做清洁&#xff0c;无法对我们的贴身衣物进行一个高强度的清洁&#xff0c;而小小…

ubuntu查看内存使用情况命令

命令简介 在Ubuntu系统中&#xff0c;可以使用终端命令来查看电脑的内存使用情况。打开终端并输入以下命令&#xff1a; free -h 该命令可用于查看系统中内存的总量、已使用的内存、空闲的内存及缓冲区使用的内存。其中“-h”选项用于以人类可读的格式显示内存大小。执行该命…