单链表
单链表由0个或多个节点构成,每个节点都是一个结构体,
该结构体 包括 存储的数据和下一个节点的地址。
这里节点1为头节点,节点3为尾节点.
节点的地址不一定连续,但当前节点通过访问自身结构体的内容可以找到下一个节点的地址.
因此知道头节点后,就可以顺藤摸瓜找到后面节点的数据。
单链表的实现
不能像顺序表一样直接定义单链表的结构体,而是要定义节点的结构体
因为单链表的特点是“按需申请空间”,即需要一个数据申请一块空间。
定义节点的结构体
typedef int SLTDataType;
typedef struct SingleListNode //单链表的节点
{
SLTDataType data; //存储的数据
struct SingleListNode* next;//下一个节点的地址
}SLTNode;
构成一个单链表最简单的方法
void testSLTPrint()
{
//先分别malloc三个节点的空间,这里n1、n2、n3都是结构体指针,通过结构体指针可以修改结构体的内容
SLTNode* n1 = (SLTNode*)malloc(sizeof(SLTNode));
assert(n1);
SLTNode* n2 = (SLTNode*)malloc(sizeof(SLTNode));
assert(n2);
SLTNode* n3 = (SLTNode*)malloc(sizeof(SLTNode));
assert(n3);
//分别给这三个节点存值
n1->data = 1;
n2->data = 2;
n3->data = 3;
//再把他们连接起来
n1->next = n2;
n2->next = n3;
n3->next = NULL;
}
此时头节点是n1指向的结构体。
打印单链表的数据
传单链表的头节点指针,才能找到后面的全部节点.若单链表为空,则他的头节点指针为NULL.
//phead是局部变量,函数内部局部变量的改变不会影响外面的参数
void SLTPrint(SLTNode* phead)
{
while (phead != NULL)
{
printf("%d->", phead->data);
phead = phead->next;
}
printf("NULL\n");
}
单链表的增删查改
尾插与尾删 ( 时间复杂度均为O(N) )
前言:要在函数内部改变外面的int类型,就要传int*,解引用改变
要在函数内部改变外面的int*类型,就要传int**,解引用改变
尾插有两种情况:
链表为空,传过来的头节点指针为NULL,此时我们需要申请节点,改变头节点指针phead。
链表不为空,此时我们要申请节点接在节点3的后面,即改变节点3结构体里的next,改变结构体。
如果传的是头节点指针,可以顺利找到节点3结构体的地址,从而修改结构体;
然而我们还可能需要改变头节点指针phead,所以应该传头节点指针的指针,即pphead.
void SLTPushBack(SLTNode** pphead,SLTDataType x)//单链表的尾插
{
//二级指针一定不为空
assert(pphead);
//按需申请节点
SLTNode* tmp = (SLTNode*)malloc(sizeof(SLTNode));
assert(tmp);
tmp->data = x;
tmp->next = NULL;
//若头节点指针为空,则可以解引用二级指针改变头节点指针
if (*pphead == NULL)
{
*pphead = tmp;
}
else
{
SLTNode* cur = *pphead;
while (cur->next != NULL)//找尾节点
{
cur = cur->next;
}
cur->next = tmp;//改变尾节点的next
}
}
尾删也有两种情况:
1 链表只有一个节点,把最后一个节点删除后,要把头节点指针置为NULL,即改变头节点指针。
2 链表有多个节点,找到尾节点的前一个节点后,先把尾节点的空间free掉,再把新尾节点的next置为NULL。
void SLTPopBack(SLTNode** pphead)
{
assert(pphead && *pphead);//节点为空删不了
if ((*pphead)->next == NULL)//只有一个节点
{
free(*pphead);
*pphead = NULL;
}
else//多个节点
{
SLTNode* cur = *pphead;
//前面已经知道cur->next开始一定不为空
while (cur->next->next != NULL)//找尾节点的前一个节点
{
cur = cur->next;
}
//此时cur为尾节点的前一个节点
free(cur->next);//释放掉尾节点的空间
cur->next = NULL;//把新尾节点的next置NULL
}
}
头插和头删(时间复杂度为O(1))
头插和头删一定会改变头节点的指针,要传二级指针。
头插的两种情况:
1 链表为空,申请新节点,使头节点指针指向该节点;
2 链表不为空
void SLTPushFront(SLTNode** pphead, SLTDataType x)//头插
{
assert(pphead);
//申请新节点
SLTNode* tmp = (SLTNode*)malloc(sizeof(SLTNode));
assert(tmp);
tmp->data = x;
if (*pphead == NULL)//链表为空,直接使头节点指针指向新节点,同时新节点的next置NULL
{
tmp->next = NULL;
*pphead = tmp;
}
else//链表不为空
{
tmp->next = *pphead;
*pphead = tmp;
}
}
头删的两种情况:
1 只有一个节点,删除后头节点指针置NULL;
2 多个节点
void SLTPopFront(SLTNode** pphead)//头删
{
assert(pphead && *pphead);
SLTNode* next = (*pphead)->next;//保留第二个节点的地址
SLTNode* phead = *pphead;//第一个节点的地址
free(phead);//把第一个节点释放掉
*pphead = next;//头节点改成第二个节点
}
查找与修改
查找不会修改单链表的数据,只需传头指针遍历单链表
SLTNode* SLTFind(SLTNode* phead, SLTDataType x)//通过数据查找,返回节点的指针
{
SLTNode* cur = phead;
while (cur != NULL)
{
if (cur->data == x)
return cur;
else
cur = cur->next;
}
return NULL;
//还可以顺便修改
SLTNode* pos = SLTFind(phead, 1);
if(pos != NULL)
pos->data = 55;
在任意位置之前插入
与查找的函数互相配合,先找到插入的位置,再传给该函数
有2种情况:
1 头插,直接调用即可;
2
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)//在pos位置之前插入
{
assert(pos && pphead);
SLTNode* tmp = (SLTNode*)malloc(sizeof(SLTNode));
assert(tmp);
tmp->data = x;
SLTNode* cur = *pphead;
if (cur == pos)
{
//头插
SLTPushFront(pphead, x);
}
else
{
while (cur->next != pos)//找pos的前一个节点
{
cur = cur->next;
}
//此时cur为pos的前一个节点的指针
cur->next = tmp;
tmp->next = pos;
}
}
删除任意位置的节点
void SLTErase(SLTNode** pphead, SLTNode* pos)//删除pos位置的节点
{
assert(pphead && *pphead && pos);
if (pos == *pphead)//头删
{
SLTNode* next = (*pphead)->next;
free(*pphead);
*pphead = next;
}
else
{
SLTNode* cur = *pphead;
while (cur->next != pos)//找到pos的前一个节点指针
{
cur = cur->next;
}
cur->next = pos->next;
free(pos);
pos = NULL;
}
}