1 尾插和头插
首先我们自己先定义一个结构体类型:
typedef struct SListNode { SLTDateType data; struct SListNode* next; }SListNode;
尾插的具体实现:
SListNode* SLCreatNode(x) { SListNode* tmp = (SListNode*)malloc(sizeof(SListNode)); if (tmp != NULL) { tmp->data = x; tmp->next = NULL; return tmp; } } void SLPushBack(SListNode** pNode, SLTDateType x) { SListNode* newnode=SLCreatNode(x); if (*pNode == NULL) { *pNode = newnode; } else { SListNode* tail = *pNode; while (tail->next) { tail = tail->next; } tail->next = newnode; } }
因为头插和尾插都需要malloc出空间,所以为了代码的简练我们就分装了一个函数SLCreatNode来完成空间的动态开辟。尾插中值得注意的小细节是当*pNode为NULL的时候要单独处理,不然执行tail->next就会出错。
头插的具体实现:
void SLPushFront(SListNode** pNode, SLTDateType x) { SListNode* newnode = SLCreatNode(x); newnode->next = *pNode; *pNode = newnode; }
头插比较简单,这里就不多讲了。为了能够看见具体效果,我们可以实现一个打印接口。
具体代码:
void SListPrint(SListNode* pNode) { SListNode* cur = pNode; while (cur) { printf("%d->", cur->data); cur = cur->next; } printf("NULL"); }
现在我们可以来看结果了:
通过效果图可以看出尾插和头插都是正确的。
2 尾删和头删
尾删的具体代码:
void SLPopBack(SListNode** pNode) { //methon1: /*if (*pNode == NULL) { return; }*/ //method2: assert(*pNode != NULL);//(结点为空) if (((*pNode)->next) == NULL) //(一个结点) { free(*pNode); *pNode = NULL; } //method1: /*else { SListNode* prev = NULL; SListNode* tail = *pNode; while (tail->next) { prev = tail; tail = tail->next; } free(tail); tail = NULL; prev->next = NULL; }*/ //method2: else//(多个结点) { SListNode* tail = *pNode; while (tail->next->next) { tail = tail->next; } free(tail->next); tail->next = NULL; } }
尾删的细节处理比较多,细节有时候没有处理好就会导致程序挂掉。我们首先来看当1 结点为空的情况:处理这种情况一般有两种方法
温柔版:if (*pNode == NULL)
{
return;
}
暴力版:assert(*pNode != NULL);
具体哪种方法可以凭借自己喜好选择。
2 结点为多个:
第一种方法是不创建额外的临时变量:判断tail->next->next是否为空,为空就停止,然后free掉tail->next,再将tail->next置为NULL,第二种方法是创建临时变量prev,具体代码可以参照上面。其实无论是哪一种方法,都是要记住要free掉的空间的上一个地址,通过这个地址将其改为NULL。
3 结点为一个:
当结点为一个的时候无论采取上面的哪一种方法都会造成对NULL的访问,这样肯定是行不通的,所以结点为一个的时候就要单独处理。
头删的具体代码:
void SLPopFront(SListNode** pNode) { if (*pNode==NULL) { return; } //一个结点与多个结点的情况可以同时处理 else { SListNode* head = (*pNode)->next; free(*pNode); *pNode = head; } }
整体思路与尾删差不多,有些不同的是当结点为一个或者多个的时候可以同时处理。
来看一下结果:
如果再尾删一个的话:
由于尾删我们采取的是暴力的方式,编译器会直接给我们报出错误在哪一行。
3 查找(查增和查删)
首先来看一看查找的具体代码:
SListNode* SListFind(SListNode* pNode, SLTDateType x) { SListNode* cur = pNode; while (cur) { if (cur->data == x) { return cur; } else { cur = cur->next; } } return NULL; }
为什么查找的返回值要用SListNode*呢?
这样的好处是为了方便与查增与查删一起使用,并且还很方便修改值:
例如:
这样我们就将3改为了我们想要的300,那如果我们想要在3的前面或者后面插入一个数呢?
如果是想在3的前面插入一个数,这个时候我们就必须一个一个从头遍历,找到要插入的pos位的上一位地址,还要分类讨论第一个位置是否为要查找的位置,代码量也比较大,这也是单链表的缺陷。
在pos位前插入代码具体实现:
void SListInsert(SListNode** pNode, SListNode* pos, SLTDateType x) { SListNode* newnode = SLCreatNode(x); if (*pNode == pos) { newnode->next = *pNode; *pNode = newnode; } else { SListNode* posProve = *pNode; while (posProve->next != pos) { posProve = posProve->next; } posProve->next = newnode; newnode->next = pos; } }
其中需要注意的是当第一个元素就是查找元素的时候,这个就要分开判断,这种情况就相当于头插。
来看看这种查找后插入的效果:
那如果是直接后插呢?
直接上代码:
void SLInsertAfter(SListNode* pos, SLTDateType x) { if (pos != NULL) { SListNode* newnode = SLCreatNode(x); newnode->next = pos->next; pos->next = newnode; } }
结果展示:
这样在后面插入的话代码量会少很多。
删除pos位的具体代码:
void SListErase(SListNode** pNode, SListNode* pos) { assert(pNode); if (*pNode == pos)//头删 { *pNode = pos->next; free(pos); } else { SListNode* posPrev = *pNode; while (posPrev->next != pos) { posPrev = posPrev->next; } posPrev->next = pos->next; free(pos); pos = NULL; } }
要想删除pos位,就必须知道前一位的地址,所以只能从头开始遍历,而当第一位就是pos位的时候,就相当于头删,所以就要分开讨论,具体代码可以参考上面。
结果展示:
其实单链表进行删除pos位是比较麻烦的,要从头开始遍历,但是如果只是想删除pos位后面的一位就很容易,而且可以不用传入plist的地址。
具体代码实现:
void SLiEraseAfter(SListNode* pos) { assert(pos->next); SListNode* posNext = pos->next; pos->next = posNext->next; free(posNext); posNext = NULL; }
效果图:
4 释放
既然空间是动态开辟的,那么当不用该空间时就要还给操作系统,那么直接free掉*pNode不就好了吗?如果直接free掉*pNode会导致后面空间的地址丢失,那不就造成内存泄漏了吗,所以这样肯定是行不通的,正确的做法应该是用一个临时指针来保存当前地址。
具体代码:
void SListDestroy(SListNode** pNode) { SListNode* cur = *pNode; while (cur) { SListNode* Next = cur->next; free(cur); cur = NULL; cur = Next; } *pNode = NULL; }
好了,今天的分享就到这里了,如果该文对你有帮助的话能不能3连支持一下博主呢?