前面向大家介绍了顺序表以及它的实现,今天我们再来向大家介绍链表中的单链表。
1.链表的概念和结构
1.1 链表的概念
链表是一种在物理结构上非连续,非顺序的一种存储结构。链表中的数据的逻辑结构是由链表中的指针链接起来的。
1.2 链表的结构
链表的结构与火车相似。
火车是由一节一节的车厢构成的,并且各个车厢之间是相互独立的,且每个车厢都有属于自己的锁。假设火车上车厢的门都是锁上的状态,每个门都要对应的锁来开门,那我们如何快速的从第一个车厢走到最后一个车箱呢?
答案很简单,我们只要把下一节的车厢的钥匙放在上一节车厢就行了。
链表也是如此,车厢对应到链表中就是节点。
所以链表是由一个个节点组成的,每个节点由存储的数据和指向下一个节点的指针组成的。
为什么需要指针呢?
因为链表在物理结构上是不连续的,由于连表中的节点的地址是由计算机随机分配的,我们并不能清楚的知道各个节点的具体位置,这时候就需要指针了。通过指针我们就能知道每个节点的位置。
上图就是一个单链表的结构,plist是一个指向第一个节点的指针,往后看会发现,每一个节点都会包含了下一个节点的指针。
2.单链表的实现
单链表的实现,我们依然通过三个文件来实现,为SList.h,SList.c和test.c
2.1 单链表的创建
我们通过前面顺序表就可以很快写出以下代码
typedef int SLDataType;
struct SListNode
{SLDataType data;struct SListNode* next;
}SList;
2.2 单链表的初始化
链表是由一个一个的节点组成的,所以我们要为节点申请空间,会用到malloc函数。
void test01()
{//手动初始化SLTNode* node1 = (SLTNode*)malloc(sizeof(SLTNode));node1->data = 1;SLTNode* node2 = (SLTNode*)malloc(sizeof(SLTNode));node2->data = 1;SLTNode* node3 = (SLTNode*)malloc(sizeof(SLTNode));node3->data = 1;SLTNode* node4 = (SLTNode*)malloc(sizeof(SLTNode));node4->data = 1;node1->next = node2;node2->next = node3;node3->next = node4;node4->next = NULL;SLTNode* plist = node1;
}
为了方便观察,我们先写一个打印链表的函数。
SLPrint(SLTNode* phead)
{SLTNode* pur = phead;while (pur){printf("%d->", pur->data);pur = pur->next;}printf("NULL\n");
}
解释以上代码
pur是一个指向第一个节点的指针,我们知道最后一个节点中的指针为NULL,pur在不断的变换为next的值,直到pur的值为NULL就跳出循环。
运行代码
我们就发现单链表初始化成功了。
但是上面的代码是我们动手来实现链表的初始化的,但这样写代码的效率就会降低,我们一般都会通过函数来实现,这就涉及到了链表数据的插入。
2.3 数据的插入
数据的插入方式我们分为尾插和头插的两种。
2.3.1 尾插
尾插,顾名思义就是从链表中的尾部插入一个新的节点。
上图就是尾插的形式。
思路分析
既然我们要插入一个新的节点,我们就要为新的节点申请空间,为了方便,我们同样把申请空间的操作包装成一个函数。
//申请空间
SLTNode* SLBuySpace(SLDataType x)
{SLTNode* new = (SLTNode*)malloc(sizeof(SLTNode));//判断空间是否申请成功if (new == NULL){perror("malloc fail");exit(1);//退出程序}//到这空间申请成功new->data = x;new->next = NULL;return new;
}
接着,既然要实现尾插,我们就要找到链表的尾巴,然后才能插上新的节点。
//找尾巴
SLTNode* ptail = *pphead;
while (ptail->next)
{ptail = ptail->next;
}
ptail->next = newnode;
尾插的总代码
//尾插
void SLPushBack(SLTNode** pphead, SLDataType x)
{assert(pphead);SLTNode* newnode = SLBuySpace(x);if (*pphead == NULL){//链表为空*pphead = newnode;}else{//链表不为空//找尾巴SLTNode* ptail = *pphead;while (ptail->next){ptail = ptail->next;}ptail->next = newnode;}}
需要注意的是,我们这里的形参是一个二级指针,因为我们要将原来phead指针指向的内容进行改变,如果我们单独将指针的值传过来,通过前面的学习,我们传值时,形参的改变是不会影响实参的,所以我们要将指针的地址传过来,通过地址修改实参的值。
运行代码
还需注意的是,我们要将链表为空和不为空分为两种情况处理,如果我们只考虑到链表不为空的情况,则当我们一开始处理的链表为空时,就不会进入循环,为空,ptial->next就无法进行解引用。
2.3.1 头插
头插,也就是将一个新的节点插入链表的头部,使新的节点称为第一个节点。
代码实现
//头插
void SLPushHead(SLTNode** pphead, SLDataType x)
{assert(pphead);//为新节点申请空间SLTNode* newnode = SLBuySpace(x);newnode->next = *pphead;*pphead = newnode;
}
头插的代码很简单,但是最后要让newnode成为新的phead,不要漏掉*pphead=newnode。
运行代码
2.4 数据的删除
2.4.1 尾删
尾删就是将链表中的最后一个节点删除掉。
思路分析
尾删我们就要找到链表的尾巴,并将其释放掉,但注意的是,当我们将最后一个节点释放掉之后,前一个节点中的next就会变成野指针,所以我们也要找到最后一个节点的前一个节点,并将其next指针赋值为NULL。
尾删前如下图
尾删后如下图
代码实现
void SLPopBack(SLTNode** pphead)
{assert(pphead && *pphead);//链表不能为空SLTNode* prev = *pphead;SLTNode* ptail = *pphead;while (ptail->next){prev = ptail;ptail = ptail->next;}//这里,prev和ptail找到free(ptail);prev->next = NULL;
}
运行代码
2.4.2 头删
头删就是将链表中的第一个节点删点。
代码实现
//头删
void SLPopHead(SLTNode** pphead)
{assert(pphead && *pphead);SLTNode* next = (*pphead)->next;free(*pphead);*pphead = next;//将下个节点变为新的节点
}
我们需要把下一个节点变为新的头节点,所以我们创建一个next先将下一个节点的地址存储起来。
2.5 查找数据
查找数据很简单,只需遍历链表,并返回存储要查询数据节点的地址,如没由,就返回NULL。
代码实现
SLTNode* SLDataFind(SLTNode* phead, SLDataType x)
{assert(phead);SLTNode* pcur = phead;//遍历链表while (pcur){if (pcur->data == x){return pcur;}pcur = pcur->next;}return NULL;
}
运行代码
2.6 在指定位置之前插入数据
在指定位置之前插入数据,会影响到插入数据前一个节点的·指针,所以我们要找到插入位置的前一个节点。如下图
代码实现
void SLAddPos(SLTNode** pphead, SLTNode* pos, SLDataType x)
{assert(pphead);assert(pos);SLTNode* newnode = SLBuySpace(x);if (*pphead == pos){//链表为空//头插SLPushHead(pphead, x);}else{//链表不为空//找位置pos前面的节点SLTNode* prev = *pphead;while (prev->next != pos){prev = prev->next;}newnode->next = pos;prev->next = newnode;}
}
2.7 在指定位置之后插入数据
在指定位置之后插入数据就很简单,这个操作会影响插入位置的后一个指针,因为我们可以通过插入位置来找到插入位置的后一个节点,不在需要遍历链表。
我们只需将pos->next指向newnode,让newnode->next指向pos->next。
代码实现
void SLAddBack(SLTNode* pos, SLDataType x)
{assert(pos);//为插入节点申请空间SLTNode* newnode = SLBuySpace(x);SLTNode* next = pos->next;pos->next = newnode;newnode->next = next;
}
这里我们要先将pos->next保存下来,因为后面的pos->next会发生改变,而我们要让newnode->next指向原来的pos->next;
2.8 删除pos节点的数据
当我们删除pos节点时,会影响到pos前后两个节点的指针,所以,我们首先要找到pos前后的两个节点。
代码实现
void SLErasePos(SLTNode** pphead, SLTNode* pos)
{assert(pphead && *pphead);assert(pos);if (pos == *pphead){//只有一个节点//头删SLPopHead(pphead);}else{SLTNode* next = pos->next;//找prevSLTNode* prev = *pphead;while (prev->next != pos){prev = prev->next;}free(pos);pos = NULL;prev->next = next;}}
注意事项:我们要先将pos->next的值先存储起来,因为前面的pos就会被释放掉了,最后就找不到pos->next了。
我们还要分情况讨论,当链表中只有一个节点时,那就是头删操作了,直接调用头删的函数就行了。
运行代码
2.9 删除pos后的节点
思路分析
既然要删除pos后的节点,首先链表就不能为空,pos->next也不能为空。
注意,将pos后面的节点释放掉了之后,此时pos->next就是野指针来,要注意将pos->next置为空。
代码实现
void SLEraseAfter(SLTNode* pos) //pos->data==1
{assert(pos && pos->next);//要删除的节点SLTNode* del = pos->next;pos->next = del->next;free(del);del = NULL;
}
2.10 销毁链表
销毁链表就一个一个销毁就行了。
代码实现
void SLBreak(SLTNode** pphead)
{SLTNode* pcur = *pphead;while (pcur){SLTNode* next = pcur->next;free(pcur);pcur = next;}*pphead = NULL;
}
感谢观看。