第四话·数据结构必看之·单链表就这?
Posted kikokingzz
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了第四话·数据结构必看之·单链表就这?相关的知识,希望对你有一定的参考价值。
🌕写在前面
- 🍊博客主页:kikoking的江湖背景
- 🎉欢迎关注🔎点赞👍收藏⭐️留言📝
- 🌟本文由 kikokingzz 原创,CSDN首发!
- 📆首发时间:🌹2021年11月29日🌹
- 🆕最新更新时间:🎄2021年11月29日🎄
- ✉️坚持和努力一定能换来诗与远方!
- 🙏作者水平很有限,如果发现错误,请留言轰炸哦!万分感谢感谢感谢!
- 前文简介:
- 第一话·彻底搞清数据结构中的·逻辑结构&存储结构
- 第二话·彻底搞懂数据结构之·算法复杂度
- 第三话·408必看顺序表之·人生不是“顺序表”
目录
✅7.在指定pos位置前插入(略麻烦)·SListInsertFront
✅ 8.在指定pos位置后插入·SListInsertAfter
🐉:在了解本节课的单链表之前,让我先回顾一下上节课的顺序表
顺序表的缺陷:
- 动态增容有线性消耗
- 伴随一定程度的空间浪费
- 插入删除需要挪动数据
🐉:链表对顺序表的这些缺陷进行了改进
链表:
- 空间按需索取
- 插入删除数据不需要挪动数据,只需修改指针
- 不需要使用地址连续的存储单元——逻辑结构和物理结构不一致
🐉:那么对于一种数据结构,我们要按照什么样的顺序对其进行理解呢?
L:要从它的逻辑结构-->物理结构(存储结构)-->执行的操作来进行分析
🐉:小子,没想到之前的顺序表的经历让你一下子成熟了这么多!分析的很全面!
🐉:那么接下来让我们挨个对其进行分析
🔥逻辑结构
单链表的逻辑结构是线性表,也就是说其在逻辑上是线性结构,也就是一条直线
L:那么其和存储结构有什么关系呢?
🐉:俗话说的好,运算的定义是针对逻辑结构的,运算的实现是针对存储结构的
L:好深奥啊!什么运算定义,运算实现??
🐉:我们举个例子,来解释一下啥是逻辑一对一的关系:
·题目:将一串打乱的6个英文字母按字母顺序排列(c d e a b f )🐉:运算定义简单来说就是:你在脑海里打算怎么对这6个英文字母排序呢,总的来说就只有以下四种方法:
显然,符合我们要求的排列方式的最优选择就是线性结构
✨✨✨我是分割线✨✨✨
🔥存储结构:单链表
概念:它是指通过一组任意的存储单元来存储线性表中的数据元素,为了建立数据元素之间的线性关系,对每个链表结点,除存放元素自身的信息外,还需要存放一个指向其后继的指针
单链表结点结构如下图所示:
数据域:存储数据元素信息的域
指针域:存储直接后继元素位置的域
单链表就是依靠每个结点的指针域将线性表的数据元素按逻辑次序连接在一起
由于使用带头结点的链表十分简单操作,因此下文选择了较为困难的不带头结点的单向链表作为操作案例。
✨✨✨我是分割线✨✨✨
🔥头指针与头节点
🍊头指针
头指针是链表中第一个结点的存储位置,整个链表的存取从头指针开始,之后的每个结点其实都是上一个后继指针指向的位置;最后一个结点指向为“空”(通常用NULL或“^“来表示)
🍊头结点
在单链表的第一个结点前再设置一个结点,称为头结点。头结点的数据域可以不存储任何信息,头结点的指针域指向第一个结点的指针
🍊头指针与头结点的异同
✨✨✨我是分割线✨✨✨
🔥带头与不带头结点
🍊不带头结点
🍊带头结点
✨✨✨我是分割线✨✨✨
下文为不带头结点的单链表!!!因为如果不带头结点的你掌握了,带头结点的随便秒杀
✅1.创建一个结点·SLN
typedef int SLDataType; typedef struct SListNode SLDataType data;//数据域,存放数据 struct SListNode* next;//指向下一个结点的指针 //指针指向的类型是 struct SListNode这个结点 SLN;//用SLN来代替这个结构体
假设p是指向线性表第i个元素的指针
结点a(i) p->data 的值是 结点a(i)的一个数据元素 p->next 的值是 一个指针,结点a(i)的后继结点,指向第i+1个元素,即结点a(i+1)
✨✨✨我是分割线✨✨✨
✅2.输出单链表(遍历单链表)·SListPrint
因为单链表的元素离散地分布在存储空间中,因此不能直接找到表中的某个特定结点,输出单链表时需要从表头开始遍历,依次打印输出
void SListPrint(SLN* plist)//输出单链表 //本质是遍历单链表 SLN* cur = plist;//将头指针赋给cur while (cur)//只有cur为真才向下遍历 printf("%d->", cur->data);//输出数据域 cur = cur->next;//cur指向下一个结点 printf("NULL\\n");//最后一个结点指向NULL
✨✨✨我是分割线✨✨✨
✅3.动态申请一个结点·BuySListNode
因为后续的插入操作都需要新建一个结点,所以写成一个函数模块,可以方便后续的调用。
SLN* BuySListNode(SLDataType x)//创建一个数据域为x的新结点 SLN* node = (SLN*)malloc(sizeof(SLN));//申请一个SLN类型的结点空间 node->data = x; node->next = NULL; return node; //返回类型为一个指向SLN结构体类型的指针 //该指针指向创建的新结点
✨✨✨我是分割线✨✨✨
🙋🏻如何区分传二级指针还是一级指针
1.如果要更改传入指针自身的值----->需要传入二级指针
改变实参int变量的值 ————>传int* void swap(int* x, int* y); int main() int a = 1; int b = 2; swap(&a, &b); 相对的,改变int* 变量的值 ————>传int** void swap(int** x, int** y); int main() int* a; int* b; swap(&a, &b);
2.对于无头单链表。只要第一个结点会改变(即传入指针的值要改变)就需要传二级指针
注意:带头结点的单链表可以不用传二级指针,因为传入指针指向的头结点是不会变
✨✨✨我是分割线✨✨✨
✅4.单链表尾插·SListPushBack
由于我们写的无头节点的单链表,因此我们需要考虑空指针的情况,当原有链表没有数据元素时,此时头指针指向NULL,我们需要改变头指针的值,因此需要使用二级指针接收头指针地址,来改变头指针的值
void SListPushBack(SLN** pplist, SLDataType x) SLN* newnode = BuySListNode(x);//使用创建结点函数,用newnode来接收 if (*pplist == NULL) *pplist = newnode;//将头指针值改为新结点地址 else SLN* tail = *pplist;//将头指针赋值给tail while (tail->next != NULL) tail = tail->next;//遍历找尾 tail->next = newnode;//原尾结点指向新结点
调试:
✨✨✨我是分割线✨✨✨
✅5.单链表头插·SListPushFront
由于我们写的无头节点的单链表,而且是头插,因此每头插入一个结点,都要改变一次头指针的值,因此要使用二级指针接收头指针的地址,进而改变头指针的值
void SListPushFront(SLN** pplist, SLDataType x) SLN* newnode = BuySListNode(x);//动态申请一个新结点 newnode->next = *pplist;//新结点指向头结点指向的位置 *pplist = newnode;//将头结点指向新结点
调试:
✨✨✨我是分割线✨✨✨
✅6. 单链表查找·SListFind
查找指定元素在链表中的位置pos
SLN* SListFind(SLN* plist, SLDataType x)//函数返回一个指向被查找结点位置的指针 SLN* cur = plist; while (cur)//只有不是空链表,才进行查找过程 if (cur->data == x) return cur;//找到就返回这个结点的指针 cur = cur->next;//挨个向后遍历查找 return NULL;
查找调试
✨✨✨我是分割线✨✨✨
✅7.在指定pos位置前插入(略麻烦)·SListInsertFront
void SListInsertFront(SLN* pos,SLDataType x) assert(pos);//断言一下,防止pos为空指针 SLN* newnode = BuySListNode(x);//动态申请一个结点 newnode->next = pos->next;//将新结点指向pos的后结点 pos->next = newnode;//将pos指向新结点 int tmp = pos->data;//使用临时变量交换pos和newnode结点的数据 pos->data = newnode->data; newnode->data = tmp;
调试
✨✨✨我是分割线✨✨✨
✅ 8.在指定pos位置后插入·SListInsertAfter
可见步骤7和步骤8最后实现的结果是一样的,但是在pos后插入会更方便
void SListInsertAfter(SLN* pos, SLDataType x) SLN* newnode = BuySListNode(x);//动态申请一个结点 newnode->next = pos->next;//新结点指向pos指向的结点 pos->next = newnode;//pos指向新结点
调试
✨✨✨我是分割线✨✨✨
✅9.尾删·SListPopBack
尾删考虑的三种情况
1.头指针为空 ---->直接返回
2.只有一个结点---->free后,要将头指针置空(改变了头指针的值----->要使用二级指针接收)
3.有很多结点---->free掉最后结点,将倒数第二个结点的next置为NULL
void SListPopBack(SLN** pplist) if (*pplist == NULL) return;//头指针为空,直接返回 else if ((*pplist)->next == NULL) free(*pplist);//把第一个结点free *pplist = NULL;//将头指针的值置为空 else SLN* tail = *pplist; SLN* prev = NULL; while (tail->next != NULL) prev = tail; tail = tail->next; free(tail);//free掉尾结点 tail = NULL;//将尾指针的值置为空 prev->next = NULL;//将倒数第二个结点的next置为NULL
调试:
✨✨✨我是分割线✨✨✨
✅10.头删·SListPopBack
由下图可见,头删只需要考虑两种情况:
1.头指针为空----->直接return
2.将头指针的值改为第一个结点的next(即改变了头指针的值,需要用二级指针接收头指针地址)
void SListPopFront(SLN** pplist) if (*pplist == NULL) return; else SLN* cur = (*pplist)->next;//声明一个临时变量指向第一个结点的后继 free(*pplist);//free掉第一个结点 *pplist = cur;//头指针指向临时变量指向的结点
调试:
✨✨✨我是分割线✨✨✨
📄题目1
在一个无头单链表的某一个结点的前面插入一个值x,如何插入 (不告诉你头指针!)
解法:向后插入,将两值交换
✨✨✨我是分割线✨✨✨
✅11.指定位置后删·SListEraseAfter
void SListEraseAfter(SLN* pos) assert(pos);//断言一下,pos为空则不向下执行 if (pos->next == NULL) return;//pos为最后一个结点,删空气 else SLN* next = pos->next;//将pos后结点的地址给next pos->next = next->next; free(next); //free掉next指向的结点 next = NULL;//将next置空是一个好习惯
调试
✨✨✨我是分割线✨✨✨
✅12.指定位置前删·SListEraseFront
void SListEraseFront(SLN** pplist,SLN* pos) assert(pos); assert(*pplist); if (*pplist == pos) return;//pos处于第一个结点,无法前删 SLN* cur = *pplist; SLN* prev = NULL; if (cur->next == pos)//删除的位置是第一个结点,需要改变头指针的值 *pplist = pos; free(cur); cur = NULL; else//其余情况,用双指针来进行连接和free while (cur->next != pos) prev = cur; cur = cur->next; prev->next = pos; free(cur); cur = NULL;
调试
✨✨✨我是分割线✨✨✨
📄习题2.移除链表元素
思路分析:
🍅常规法
struct ListNode* removeElements(struct ListNode* head, int val) //为什么不使用二级指针?这里的头指针的值不是改变了吗? //使用了一个返回值 struct ListNode* ,因此函数内部改变的头结点的值在函数外也能得到 struct ListNode* cur = head; struct ListNode* prev =NULL ; while(cur) if(cur->val==val) struct ListNode* next=cur->next; if(prev==NULL)//cur是头 free(cur); head=next; cur=next; else prev->next=next; free(cur); cur=next; else prev=cur;//当prev不是NULL时,首元素必然不是val cur=cur->next; return head;
注意:为什么不使用二级指针?这里的头结点的值不是改变了吗?
使用了一个返回值 struct ListNode* ,因此函数内部改变的头结点的值在函数外也能得到
🍅哨兵法
如果我们添加一个哨兵位,那么就不需要根据首元素是否为val分类讨论了
(首元素是否为val)-------->本质是:prev 的空指针问题
struct ListNode* reverseList(struct ListNode* head) struct ListNode* cur=head , *newHead=NULL; while(cur) struct ListNode* next=cur->next; cur->next=newHead; newHead=cur; cur=next; return newHead;
✨✨✨我是分割线✨✨✨
📄习题3.反转链表
🍅思路一:直接使用三个指针翻转
1->2->3->4->NULL 通过3个指针n1\\n2\\n3进行翻转 n1 n2 n3 NULL 1->2->3->4->NULL n1 n2 n3 NULL<-1 2->3->4->NULL n1 n2 n3 NULL<-1<-2 3->4->NULL n1 n2 n3 NULL<-1<-2<-3 4->NULL n1 n2 n3 NULL<-1<-2<-3<-4 NULL
struct ListNode* reverseList(struct ListNode* head) if(head==NULL||head->next==NULL)//空表,或者只有一个结点的时候 return head; struct ListNode* n1=NULL, *n2=head, *n3=head->next; while(n2) n2->next=n1;//翻转 n1=n2;//迭代 n2=n3; if(n3==NULL)//防止空指针的情况 break; n3=n3->next; return n1;
🍅思路二:头插法
注意这里头插不创建新结点
cur next 1 ->2 ->3 ->4->NULL newHead==NULL 每次取原链表中的结点头插到新结点 需要两个指针,一个用来头插,一个用来标识下一个结点 cur->newHead 1->NULL cur=newhead -----step2----- cur next 1->2 ->3->4->NULL cur->newHead->NULL 2->1->NULL cur=newHead
struct ListNode* reverseList(struct ListNode* head) struct ListNode* cur=head , *newHead=NULL; while(cur) struct ListNode* next=cur->next; cur->next=newHead; newHead=cur; cur=next; return newHead;
以上是关于第四话·数据结构必看之·单链表就这?的主要内容,如果未能解决你的问题,请参考以下文章