大家好呀,今天我们来认识一下单链表。
单链表
目录
对链表的认识
相比于顺序表,链表就相当于是申请了一个个内存块,用指针来进行访问,这一个个内存块也被称作一个个节点。节点与节点之间是没有关联的。就好比我申请了4个内存块,我要创建链表,那么我就在第一个内存块里存放第二个内存块的地址,第二个存放第三个的,依次内推。当然,链表的尾部也可以存放头部的地址,那就是循环链表了。我们今天先来认识一下单链表。
对于链表来说插入和删除就比较简单了,我若想在头部插入,那么我可以让我想插入的节点存放原来的头节点的地址。在中间插入时,让上一个节点存放该节点的地址,该节点存放原来的下一个节点的地址。删除也是同理,只需要改变指针的指向就可以了,不需要挪动数据。
创建单链表
那么接下来我们就来创建单链表。
首要的便是结构体存放数据,对于单链表,只需要元素和指针(指针当然得是结构体指针)。代码如下:
typedef struct SListNode { int val; struct SListNode* next; }SLNode;
与顺序表一样,单链表的功能也有头插,头删;尾插,尾删;在任意位置插入和删除和查找位置。在编写单链表功能之前,我们先编写打印函数练习下手感。
如何来编写打印函数呢,既然有指针,在非NULL的情况下,把phead(头节点)的地址给一个变量,让其循着指针依次遍历就可以了,达到尾节点指向的NULL结束。代码如下:
void SLPrint(SLNode* phead) { SLNode* cur = phead; while (cur != NULL) { printf("%d ", cur->val); cur = cur->next; } }
头插,尾插
我们先来编写尾插函数。
既然要尾插,那么找到尾部是必须的。如何找尾呢?我们定义一个指针变量tail,把头节点的地址赋给它。当tail的next指向空的时候,tail所指向的就是尾节点。
既然要尾插,那么就需要创建新的节点。要创建一个新节点,那么就用malloc来开辟空间。开辟成功进行赋值,开辟失败进行提醒,退出。代码如下:
//创建新节点 SLNode* CreateNode(SLNDataType x) { SLNode* newnode = (SLNode*)malloc(sizeof(SLNode)); if (newnode == NULL) { perror("malloc fail"); exit(-1); } newnode->next = NULL; newnode->val = x; return newnode; }
我们再来考虑尾插,尾插有两种情况:一是链表为空,二是链表不为空。如果链表不为空让原来的尾节点指向该节点,让该节点的next指向空,尾插结束。 代码如下:
void SLPushBack(SLNode* phead, SLNDataType x) { SLNode* tail = phead; while (tail->next != NULL) { tail = tail->next; } SLNode* newnode = CreateNode(x); tail->next = newnode; }
如果链表为空,这时我们就会发现,先要改变phead,就需要二级指针,因此我们就对传参进行修改。代码如下:
void SLPushBack(SLNode** pphead, SLNDataType x) { SLNode* newnode = CreateNode(x); if (*pphead == NULL) { *pphead = newnode; } }
修改后的整体代码如下:
void SLPushBack(SLNode** pphead, SLNDataType x) { SLNode* newnode = CreateNode(x); if (*pphead == NULL) { *pphead = newnode; } else { SLNode* tail = *pphead; while (tail->next != NULL) { tail = tail->next; } tail->next = newnode; } }
接下来就是头插。
有了尾插的经验。我们首先进行考虑,头插是不是需要二级指针来进行改变呢?很明显这是需要的,头插时需要把原来头部位置的地址给新的头部,那么就需要二级指针来进行接收,我们改变参数。编写完成后改变头节点。
当然此时我们就会思考一个问题,头插是否需要考虑链表为空的问题呢?其实是不需要的,因为在头插时,是将头节点指向原来的头节点,之后再将原来的头节点进行替换就不需要考虑对空指针的解引用的问题。代码如下:
void SLPushFront(SLNode** pphead, SLNDataType x) { SLNode* newnode = CreateNode(x); newnode->next = *pphead; *pphead = newnode; }
头删,尾删
接下来就是头删和尾删。
我们先来编写尾删。
尾删又该如何考虑呢?那么是不是把尾部位置直接free掉就可以了?当然不是,如果直接free,那么前一个节点就会变成野指针。所以最好用快慢指针进行标记。
比如创建prev和tail两个快慢指针,当tail的next指向空的时候,那么tail就在尾部的位置,prev就在前一个节点。这时我们再把tail的位置释放,让prev位置的指针指向NULL,编写完成。当然,这时我们又得考虑链表为空或者只有一个节点的情况,为空的话就没必要删除节点了,进行检查。代码如下:
void SLPopBack(SLNode** pphead) { SLNode* prev = NULL; SLNode* tail = *pphead; while (tail->next != NULL) { prev = tail; tail = tail->next; } free(tail); tail = NULL; prev->next = NULL; }
那么继续来考虑只有一个节点的情况。
如果只有一个节点那么tail的next就指向空了,这时prev->next就没有了意义。因此需要重新进行判断。只有一个节点的话,删掉并制空就可以了。代码如下:
void SLPopBack(SLNode** pphead) { assert(*pphead); if ((*pphead)->next == NULL) { free(*pphead); *pphead = NULL; } }
整体代码如下:
//尾删 void SLPopBack(SLNode** pphead) { assert(*pphead); if ((*pphead)->next == NULL) { free(*pphead); *pphead = NULL; } else { SLNode* prev = NULL; SLNode* tail = *pphead; while (tail->next != NULL) { prev = tail; tail = tail->next; } free(tail); tail = NULL; prev->next = NULL; } }
之后便是头删。
头删的思路比较简单,将头节点的地址先保存,让头节点指向下一个然后释放保存地址的空间就可以了。代码如下:
void SLPopFront(SLNode** pphead) { assert(*pphead); SLNode* prev = (*pphead)->next; free(pphead); pphead = NULL; }
在任意位置前插入和删除
首先来编写在任意位置进行插入。
既然在任意位置进行插入,那么自然就包括头插,中间插入。但不推荐尾插,因为是在任意位置前进行插入。头插我们直接调用头插函数即可。在中间进行插入的话首先得找到上一个节点,让上一个节点指向该节点,该节点指向下一个节点。
紧接着考虑如何进行传参,**pphead铁定是不会变的,但不妨碍不嫌事大者会传个NULL过去,因此先来断言一下。接下来就来考虑指定位置的pos,为了和stl库对齐,pos还是写成结构体指针,当然也是为了方便进行查找。那么pos可能会为空吗?是可能的,如果在*pphead和pos都为空,那么情况就变成了头插。但要是一个为空另一个不为空程序就会崩掉,因此我们得进行断言。我们按照思路编写代码。代码如下:
void SLTInsert(SLNode** pphead, SLNode* pos, SLNDataType x) { assert(pphead); assert((!(*pphead) && !pos) || (pos && *pphead)); //头插 if (*pphead == pos) { SLPushFront(pphead, x); } //中间插入 else { SLNode* prev = *pphead; while (prev->next != pos) { prev = prev->next; } SLNode* newnode = CreateNode(x); prev->next = newnode; newnode->next = pos; } }
接下来就是在任意位置前删除。那么这段代码该如何进行编写呢?其实思路都是大差不差的,找到前一个节点,让他指向删除节点的下一个节点。之后再对删除节点进行释放就可以了。接着我们对特殊情况进行考虑,若是链表只有一个节点的话,那么pos的next就会指向空了。因此首先得进行判断,若是只有一个节点,那么直接调用头删函数。代码如下:
void SLTErase(SLNode** pphead, SLNode* pos) { assert(pphead); assert(*pphead); assert(pos); if (*pphead == pos) { SLPopFront(pphead); } else { SLNode* prev = *pphead; while (prev->next != pos) { prev = prev->next; } prev->next = pos->next; free(pos); pos = NULL; } }
查找
查找函数比较简单,只需要遍历一遍,找到时返回该节点的地址就可以了,没找到返回NULL。当然,为空不需要查找,需要判断。代码如下:
SLNode* SLTFind(SLNode* phead, SLNDataType x) { assert(phead); SLNode* cur = phead; while (cur != NULL) { if (cur->val == x) { return cur; } else { cur = cur->next; } } return NULL; }
完整代码
list.h
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
typedef int SLNDataType;
typedef struct SListNode
{
int val;
struct SListNode* next;
}SLNode;
//遍历
void SLPrint(SLNode* phead);
//尾插
void SLPushBack(SLNode** pphead, SLNDataType x);
//头插
void SLPushFront(SLNode** pphead, SLNDataType x);
//尾删
void SLPopBack(SLNode** pphead);
//头删
void SLPopFront(SLNode** pphead);
//任意位置之前插入
void SLTInsert(SLNode** pphead, SLNode* pos, SLNDataType x);
//任意位置删除
void SLTErase(SLNode** pphead, SLNode* pos);
//查找
SLNode* SLTFind(SLNode* phead, SLNDataType x);
list.c
#include"list.h"
//遍历
void SLPrint(SLNode* phead)
{
SLNode* cur = phead;
while (cur != NULL)
{
printf("%d-> ", cur->val);
cur = cur->next;
}
}
//创建新节点
SLNode* CreateNode(SLNDataType x)
{
SLNode* newnode = (SLNode*)malloc(sizeof(SLNode));
if (newnode == NULL)
{
perror("malloc fail");
exit(-1);
}
newnode->next = NULL;
newnode->val = x;
return newnode;
}
//尾插
void SLPushBack(SLNode** pphead, SLNDataType x)
{
SLNode* newnode = CreateNode(x);
if (*pphead == NULL)
{
*pphead = newnode;
}
else
{
SLNode* tail = *pphead;
while (tail->next != NULL)
{
tail = tail->next;
}
tail->next = newnode;
}
}
//头插
void SLPushFront(SLNode** pphead, SLNDataType x)
{
SLNode* newnode = CreateNode(x);
newnode->next = *pphead;
*pphead = newnode;
}
//尾删
void SLPopBack(SLNode** pphead)
{
assert(*pphead);
if ((*pphead)->next == NULL)
{
free(*pphead);
*pphead = NULL;
}
else
{
SLNode* prev = NULL;
SLNode* tail = *pphead;
while (tail->next != NULL)
{
prev = tail;
tail = tail->next;
}
free(tail);
tail = NULL;
prev->next = NULL;
}
}
//头删
void SLPopFront(SLNode** pphead)
{
assert(*pphead);
SLNode* prev = (*pphead)->next;
free(pphead);
pphead = NULL;
}
//任意位置之前插入
void SLTInsert(SLNode** pphead, SLNode* pos, SLNDataType x)
{
assert(pphead);
assert((!(*pphead) && !pos) || (pos && *pphead));
//头插
if (*pphead == pos)
{
SLPushFront(pphead, x);
}
//中间插入
else
{
SLNode* prev = *pphead;
while (prev->next != pos)
{
prev = prev->next;
}
SLNode* newnode = CreateNode(x);
prev->next = newnode;
newnode->next = pos;
}
}
//任意位置删除
void SLTErase(SLNode** pphead, SLNode* pos)
{
assert(pphead);
assert(*pphead);
assert(pos);
if (*pphead == pos)
{
SLPopFront(pphead);
}
else
{
SLNode* prev = *pphead;
while (prev->next != pos)
{
prev = prev->next;
}
prev->next = pos->next;
free(pos);
pos = NULL;
}
}
//查找
SLNode* SLTFind(SLNode* phead, SLNDataType x)
{
assert(phead);
SLNode* cur = phead;
while (cur != NULL)
{
if (cur->val == x)
{
return cur;
}
else
{
cur = cur->next;
}
}
return NULL;
}
test.c
#include"list.h"
void Test1()
{
SLNode* plist = NULL;
//调用函数进行测试
SLPushBack(&plist, 1);
SLPushBack(&plist, 2);
}
int main()
{
Test1();
return 0;
}
小结
单链表就结束啦,单链表的编写的坑有点多。因此需要进行再三考虑,特别是在对任意位置插入时对pos和pphead为空的检查判定。好啦,单链表就到这里啦。另外祝大家新年快乐!