赞
踩
进入链表,链表根据单/双向,是否循环,是否有哨兵位可分成多种,我们只重点选择几个解析,触类旁通,这篇文章的单链表没有带哨兵位的单链表,实现会有很多困难,也正是因为这种结构有问题,所以很容易取得出题者的青睐,单链表实现函数的参数开始使用二级指针,亲自画图成为理解单链表断言、结构的最快方法。
typedef int SLTDateType;
typedef struct SListNode
{
SLTDateType data;
struct SListNode* next;
}SListNode;
先观察代码,单链表的单个节点就是一个结构体,结构体中存储了下一个节点的地址,上一个节点通过地址可以找到下一个节点,这样就形成了单链表的链式结构,而前后两个节点的内存地址不一定是连续的,就造就了链表逻辑上连续,物理上非连续的性质。
#pragma once // slist.h #include<stdio.h> #include<stdlib.h> #include<assert.h> typedef int SLTDateType; typedef struct SListNode { SLTDateType data; struct SListNode* next; }SListNode; // 动态申请一个节点 SListNode* BuySListNode(SLTDateType x); // 单链表打印 void SListPrint(SListNode* plist); // 单链表尾插 void SListPushBack(SListNode** pplist, SLTDateType x); // 单链表的头插 void SListPushFront(SListNode** pplist, SLTDateType x); // 单链表的尾删 void SListPopBack(SListNode** pplist); // 单链表头删 void SListPopFront(SListNode** pplist); // 单链表查找 SListNode* SListFind(SListNode* plist, SLTDateType x); // 单链表在pos位置之后插入x // 分析思考为什么不在pos位置之前插入? void SListInsertAfter(SListNode* pos, SLTDateType x); // 单链表删除pos位置之后的值 // 分析思考为什么不删除pos位置? void SListEraseAfter(SListNode* pos); // 单链表的销毁 void SListDestroy(SListNode* plist); //pos位置之前插入 void SListInsertbefore(SListNode** pplist, SListNode* pos, SLTDateType x);
当我们储存单链表的头节点的地址(用A/B),通过该地址访问结构体的成员(next),反复循环直到next指向NULL(链表尾),我们可以得到整条链表,所以当我们想创建一个空的单链表,我们只需要创建一个储存节点地址的指针(A/B)(它指向NULL,意为空)即可。(下图结构体指针均指向链表头)
单链表的头插头删比顺序表便捷的多,回顾顺序表的头插头删需要移动整个数组,单链表头插只改变要插入的新节点的成员(next),使其指向头节点,并改变储存头节点地址的指针Head(换新头)即可,见下图。
void SListPushFront(SListNode** pplist, SLTDateType x)
{
assert(pplist);
//assert(*pplist);
SListNode* newNode = BuySListNode(x);
newNode->next = *pplist;
*pplist = newNode;
}
这里值得注意的是:1.我们传了指向头节点的指针的指针(二级指针)给函数,这样我们才可以在函数中通过解引用改变指向头节点的指针(也就是换新头操作)。
2.我们不要断言*pplist,因为它是可以指向NULL的(此时链表为空,插入时链表可以为空),而二级指针pplist却不能为空,且容易对类型为SListNode**的NULL指针进行访问。(如下图)
因为单链表的每个节点都是一个结构体,所以我们要插入新数据(新节点)的时候,我们需要创建一个新的结构体,许多插入都需要创建新节点,于是将节点创造封装成函数(BuySListNode)
SListNode* BuySListNode(SLTDateType x)//x为你要创造的新节点的值data
{
SListNode* newNode = (SListNode*)malloc(sizeof(SListNode));
if (newNode == NULL)
{
perror("BuySListNode malloc fail");
return NULL;
}
newNode->data = x;
newNode->next = NULL;
return newNode;
}
创造新节点时不要忘记将新节点的值初始化!
这里或许有人会问:我在BuySListNode函数中直接创造一个结构体,然后用&返回它的地址不是一样创造了一个新节点吗?不要忘记在函数中创造的临时变量有自己的作用域,当函数结束时,临时变量会被销毁(内存还给操作系统),这时我们再访问该地址就会造成内存非法访问。
void SListPopFront(SListNode** pplist)
{
assert(*pplist);
SListNode* del = *pplist;
*pplist = (* pplist)->next;
free(del);
}
头删逻辑类似,也需要换头。
注意:1.不要忘记对删除的节点进行内存释放(free),不然会造成内存泄漏,请不要说什么程序结束还是会释放,公司项目程序可不敢轻易结束。
2.删除中 * pplist 是不能为空的,我们不能对空链表进行删除,所以需要断言 *pplist。
3.我们需要先用 SListNode *类型的变量将要删除的头保存,先进行换头,不然释放空间后我们无法再找到头节点的下一个节点。
单链表想要尾删尾插,我们需要得到尾指针的地址,以至于我们需要从头遍历整个链表来找到它,回顾顺序表的尾删尾插很容易找到尾,可知单链表和顺序表各有优势,顺序表尾操作高效,单链表头操作高效。
上图为找尾过程的循环判断条件:当cur(tail)的next为NULL时,跳出循环。
void SListPushBack(SListNode** pplist, SLTDateType x) { assert(pplist); SListNode* newNode = BuySListNode(x); if (*pplist == NULL) { *pplist = newNode; } else { //找尾 SListNode* tail = *pplist;//tail就是cur while (tail->next)//找尾过程 { tail = tail->next; } tail->next = newNode; } }
注意:1.链表为空时,头插操作可以套用正常头插的逻辑,因为NULL也可被指向(作为节点的next),而尾插时我们需要找尾,我们需要对尾指针进行成员访问(->),而NULL是不可以被访问的,所以我们需要加一个判断,当空链表头插时,直接将新节点的地址给指向链表头的指针*pplist(也就是Head指针)。
2.在进行完尾插操作时,不要忘记给尾指针的next置空(NULL)。
3.不要直接让*pplist去亲自找尾,因为这是二级指针的解引用,对它改变将直接影响Head的值。
尾删逻辑类似,以tail->next->next为循环条件可以不用保存要删的尾指针的前一个指针,直接释放tail->next。
void SListPopBack(SListNode** pplist)
{
//assert(pplist);
assert(*pplist);
SListNode* tail = *pplist;
while (tail->next->next)
{
tail = tail->next;
}
/*SListNode* del = tail->next;
tail->next = NULL;
free(del);*/
free(tail->next);
tail->next = NULL;
}
注意:断言*pplist,链表为空时不存在删除。
void SListPrint(SListNode* plist)
{
//assert(plist);
SListNode* cur = plist;
while (cur)
{
printf("%d->", cur->data);
cur = cur->next;
}
printf("NULL\n");
}
链表的打印无需对指向头指针的Head指针进行改动,所以我们只要传一级指针就足以访问节点的成员,不过虽然是一级指针,对plist的改动不会反应到实际参数,我们还是尽量不要用函数参数亲自遍历链表,这样可以提升代码的可读性。
从链表中找到你想要的值,并返回该值所在的节点的地址。
SListNode* SListFind(SListNode* plist, SLTDateType x) { assert(plist); SListNode* cur = plist; while (cur) { if (cur->data != x) { cur = cur->next; } else { return cur; } } //return NULL; }
注意:空链表无法查找,如果你认为可以查找,也可以把 “return NULL;” 打开,将assert(plist)断言给注释,全凭你的个人意愿。
指定位置插入删除应和SListFind函数配合使用。
在链表中指定位置(pos)的后面插入新节点。
void SListInsertAfter(SListNode* pos, SLTDateType x)//x为你想要插入的新节点的data
{
assert(pos);
SListNode* newNode = BuySListNode(x);
newNode->next = pos->next;
pos->next = newNode;
}
注意:pos位置不能为NULL(无法访问)。
在指定位置之前插入情况要稍微复杂一点,我们需要找到pos位置的前一个位置的地址,所以我们需要把头节点的地址给prev,让它从头开始遍历直到prev的next是pos,但是这就有衍生出了一个新的问题:当pos位置就是头节点,就相当于我们需要头插,而头插函数需要二级指针,所以该函数需要传头节点的二级指针。
void SListInsertbefore(SListNode** pplist,SListNode* pos, SLTDateType x) { //assert(pos); //assert(pplist); //assert(*pplist); SListNode* prev = *pplist; if (prev == pos) { SListPushFront(pplist,x); } while (prev->next!=pos)//从头开始遍历直到prev的next是pos { prev = prev->next; } SListNode* newNode = BuySListNode(x); prev->next=newNode; newNode->next = pos; }
注意:我们指定位置的插入一般需要和SListFind函数进行配合使用,pos位置就一定会在链表中存在,所以我们无需断言,当函数出错(pos位置不在链表中会有对NULL访问的错误,思考下为什么?),证明使用者没有正确使用函数,成年人应该为自己的行为买单。
void SListEraseAfter(SListNode* pos)
{
assert(pos);
assert(pos->next);
SListNode* del = pos->next;
/*if (del == NULL)
{
return;
}*/
pos->next = pos->next->next;
free(del);
}
注意:1.当pos位置的下一个为NULL我们可以选择温柔结束(return返回),也可以暴力断言assert(pos->next)。
2.因为=运算符先算‘=’右边的式子,我们可以用pos->next->next找到pos后2个节点的地址来赋值给pos的next变量(pos->next),而不会使pos->next地址丢失,导致找不到pos->next->next。
void SListDestroy(SListNode* plist)
{
//assert(plist);
SListNode* cur = plist;
SListNode* del = plist;
while (cur)
{
cur = cur->next;
free(del);
del = cur;
}
//plist = NULL;
}
注意:1.链表为空也可销毁(销了相当于没销),为空时plist=cur=NULL,不会进入循环。
2.销毁链表后对指向头节点的指针Head(plist)置空十分有必要,这样可以有效避免非法访问,但是销毁函数将置空工作交给使用者,所以传递了一级指针,肯定不是懒,只是想让使用者清楚的记得这个链表已经被你亲手毁了:)。
#pragma once // slist.h #include<stdio.h> #include<stdlib.h> #include<assert.h> typedef int SLTDateType; typedef struct SListNode { SLTDateType data; struct SListNode* next; }SListNode; // 动态申请一个节点 SListNode* BuySListNode(SLTDateType x); // 单链表打印 void SListPrint(SListNode* plist); // 单链表尾插 void SListPushBack(SListNode** pplist, SLTDateType x); // 单链表的头插 void SListPushFront(SListNode** pplist, SLTDateType x); // 单链表的尾删 void SListPopBack(SListNode** pplist); // 单链表头删 void SListPopFront(SListNode** pplist); // 单链表查找 SListNode* SListFind(SListNode* plist, SLTDateType x); // 单链表在pos位置之后插入x // 分析思考为什么不在pos位置之前插入? void SListInsertAfter(SListNode* pos, SLTDateType x); // 单链表删除pos位置之后的值 // 分析思考为什么不删除pos位置? void SListEraseAfter(SListNode* pos); // 单链表的销毁 void SListDestroy(SListNode* plist); //pos位置之前插入 void SListInsertbefore(SListNode** pplist, SListNode* pos, SLTDateType x);
#define _CRT_SECURE_NO_WARNINGS 1 #include"slist.h" 我的节点申请 //SListNode* BuySListNode(SLTDateType x) //{ // SListNode s; // s.data = x; // return &s; //} //标准的节点申请 SListNode* BuySListNode(SLTDateType x) { SListNode* newNode = (SListNode*)malloc(sizeof(SListNode)); if (newNode == NULL) { perror("BuySListNode malloc fail"); return; } newNode->data = x; newNode->next = NULL; return newNode; } //对新节点的申请是在BuySListNode函数中的!函数中的变量内存在栈区中(临时变量),函数结束就会被销毁,如果此时返回了临时变量的地址 //很容易导致对已经释放的空间进行访问!! //值得注意的是,对于所有malloc开辟的空间,我们都要防止开辟失败的情况,所以当开辟失败,我们打印错误信息并且结束程序。 //malloc函数所需要包含的头文件是"stdlib",没有包含会报错访问异常。 // 我的单链表打印 //标准单链表打印 void SListPrint(SListNode* plist) { //assert(plist); SListNode* cur = plist; while (cur) { printf("%d->", cur->data); cur = cur->next; } printf("NULL\n"); } //虽然说对传入函数的变量(形参)进行改动对原变量(实参)没有影响,但是函数对指针变量返回是常有的,因此最好用临时的变量代替来 //进行遍历或者其他操作。(禁止不伦不类) //注意!!!就算是空链表,也是可以打印的,因此该函数不应该对传来的指针变量进行(assert)断言; 我的单链表尾插 //void SListPushBack(SListNode** pplist, SLTDateType x) //{ // assert(pplist); // SListNode * cur = *pplist; // SListNode * newNode=BuySListNode(x); // if (cur == NULL) // { // cur = newNode; // pplist = &cur; // } // else // { // while (cur->next) // { // cur = cur->next; // } // cur->next = newNode; // } //} //标准单链表尾插 void SListPushBack(SListNode** pplist, SLTDateType x) { assert(pplist); SListNode* newNode = BuySListNode(x); if (*pplist == NULL) { *pplist = newNode; } else { //找尾 SListNode* tail = *pplist; while (tail->next) { tail = tail->next; } tail->next = newNode; } } //这里传二级指针的原因是:当链表为空时结构体指针指向的是空(NULL),此时需要改变链表的头指针使其成为新节点的地址,我们需要保存这个地址 //以便于找到我们链表头的位置,所以传递二级指针变量。 //也正是因为这个原因,二级指针不可能为空,所以我们断言二级指针pplist,在它为空时报错。 //在链表中需要特别注意区分临时指针变量和函数参数指针。 // 我的单链表的头插 //void SListPushFront(SListNode** pplist, SLTDateType x) //{ // assert(pplist); // SListNode * newNode = BuySListNode(x); // if (*pplist == NULL) // { // *pplist = newNode; // } // else // { // newNode->next = *pplist; // *pplist = newNode; // } //} //标准单链表头插 void SListPushFront(SListNode** pplist, SLTDateType x) { assert(pplist); SListNode* newNode = BuySListNode(x); newNode->next = *pplist; *pplist = newNode; } //头插操作与链表的大小无关,注意改变表头就好! // 我的单链表的尾删 //标准单链表尾删 void SListPopBack(SListNode** pplist) { //assert(pplist); assert(*pplist); SListNode* tail = *pplist; while (tail->next->next) { tail = tail->next; } /*SListNode* del = tail->next; tail->next = NULL; free(del);*/ free(tail->next); tail->next = NULL; } //我们断言*pplist,因为当它为空指针时(即链表为空时),链表不存在删除。 // 我的单链表头删 //标准单链表头删 void SListPopFront(SListNode** pplist) { assert(*pplist); SListNode* del = *pplist; *pplist = (* pplist)->next; free(del); } //这里可以不对del指针进行置空,因为它是临时变量,出函数还是会被销毁。 // 我的单链表查找 //标准单链表查找 SListNode* SListFind(SListNode* plist, SLTDateType x) { assert(plist); SListNode* cur = plist; while (cur) { if (cur->data != x) { cur = cur->next; } else { return cur; } } //return NULL; } //这里加了plist断言(不允许链表为空),其实可以不加断言,直接return空指针。 // 我的单链表在pos位置之后插入x // 标准单链表在pos位置之后插入x // 分析思考为什么不在pos位置之前插入? void SListInsertAfter(SListNode* pos, SLTDateType x) { assert(pos); SListNode* newNode = BuySListNode(x); newNode->next = pos->next; pos->next = newNode; } //在pos位置之前插入需要知道pos之前一个节点的地址,需要遍历链表,时间复杂度为O(N),效率太低。 //当pos位置的下一个为NULL时同样适用 //在pos位置之前插入 void SListInsertbefore(SListNode** pplist,SListNode* pos, SLTDateType x) { //assert(pos); //assert(pplist); //assert(*pplist); SListNode* prev = *pplist; if (prev == pos) { void SListPushFront(pplist,x); } while (prev->next!=pos) { prev = prev->next; } SListNode* newNode = BuySListNode(x); prev->next=newNode; newNode->next = pos; } // 我的单链表删除pos位置之后的值 // 标准单链表删除pos位置之后的值 // 分析思考为什么不删除pos位置? void SListEraseAfter(SListNode* pos) { assert(pos); assert(pos->next); SListNode* del = pos->next; /*if (del == NULL) { return; }*/ pos->next = pos->next->next; free(del); } //与在pos位置之前插入同理。 //当pos位置的下一个为NULL我们可以选择温柔结束(return返回),也可以暴力断言(assert)pos->next。 //因为=运算符先算‘=’右边的式子,我们可以用pos->next->next找到pos后2个节点的地址来赋值给pos的next变量(pos->next), //而不会使pos->next地址丢失,导致找不到pos->next->next。 // 我的单链表的销毁 void SListDestroy(SListNode* plist) { //assert(plist); SListNode* cur = plist; SListNode* del = plist; while (cur) { cur = cur->next; free(del); del = cur; } //plist = NULL; } //注意:链表销毁之后不要再对链表进行打印,因为打印函数SListPrint同样是对链表的头节点地址进行访问, //就算是把plist置空(NULL)也不可以,因为它们都只是通过传递地址进行的访问的函数,你无法改变实参的数据。
本文章为作者的笔记和心得记录,顺便进行知识分享,有任何错误请评论指点:)。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。