目录
单链表的概念及结构
概念
顺序表可以随机读写表中的任意一个元素,因为它的物理结构是连续的,所以能用首地址加下标的方式进行随机读写,但是插入和删除操作需要移动大量元素。
而链式结构存储的线性表,不需要使用地址连续的存储单元,也就是不要求逻辑上相邻的元素在物理结构上也相邻,它是通过指针建立起数据元素之间的逻辑关系。
因此链表能按需申请释放空间,插入和删除操作不需要移动元素,只需要修改结点里指针变量的指向就可以,但也会失去顺序表可随机读写的优点。
结构
我们知道单链表的逻辑结构是线性的,物理结构是非线性的。但是,单链表是怎么实现物理结构是非线性的呢?物理结构又是怎样的非线性呢?
为了建立起数据元素之间在逻辑上的线性关系,对每一个链表结点,除存放元素自身的数据外,还需要存放一个指向其后继结点的指针。单链表的结点结构如下图所示。
为了明白单链表的物理结构是怎样的非线性,我们先创建几个结点进行连接生成一个单链表。
int main()
{
SLNode* n1 = BuySLNode(1);
SLNode* n2 = BuySLNode(2);
SLNode* n3 = BuySLNode(3);
SLNode* n4 = BuySLNode(4);
SLNode* n5 = BuySLNode(5);
n1->next = n2;
n2->next = n3;
n3->next = n4;
n4->next = n5;
SLNode* pList = n1; // 头指针:始终指向链表的第一个结点。
return 0;
}
通过几个结点的连接,我们所生成的单链表大概是这样的:
我们再通过监视窗口,来查看每个结点的地址和每个结点中next的值:
我们可以清楚的看到,每一个结点中next的值是它逻辑上下一个结点的地址。而被pList指向的是单链表中第一个结点,结点中next指向NULL表示该结点是单链表的最后一个结点。
单链表的接口实现
打印SListPrint
关于单链表的打印,我们要清楚的是:我们如何判断链表的结束?我们知道单链表的最后一个结点中next为NULL,所以我们会想到,以next是否为空来判断:
void SListPrint(SLNode* phead)
{
SLNode* curr = phead;
while (curr->next!= NULL)
{
printf("%d->", curr->data);
curr = curr->next;
}
printf("NULL\n");
}
如果这样来打印的话,有两个问题:1.最后一个结点我们没有遍历到。2.当单链表为空(链表为空属于正常状态)时,程序会出错,因为会对空指针解引用。
所以我们需要更改一下判断条件:判断curr是否为空。这样最后一个结点也会遍历到,并且当链表为空时,也会正常打印出NULL。
尾插SListPushBack
在进行尾插的时候分为两种情况:1.当链表为空时。2.当链表不为空时。
当链表不为空时,我们只需要找到最后一个结点,让最后一个结点的next指向新结点。
// 当链表不为空时,需要找到最后一个结点(尾结点),将尾结点的next指向新结点 SLNode* tail = *pphead; while (tail->next != NULL) { tail = tail->next; } // 此时的tail刚好指向最后一个结点 tail->next = BuySLNode(x);
当链表为空时,我们插入第一个结点,需要改变头指针的指向,使头指针指向新结点。所以在传参的时候,我们要传头指针的地址才能改变头指针的指向。这就是为什么在进行单链表的一些操作的时候需要用到二级指针的原因。
if (*pphead == NULL) // 当单链表为空时,直接将头指针指向新结点 { *pphead = BuySLNode(x); }
头插SListPushFront
头插的情况也有两种:1.链表为空。2.链表非空。但是在具体操作的时候,两种情况的连接方法是一样的。
如果让1与2的连接顺序相反,则需要创建一个next变量来保存pList的指向。
尾删SListPopBack
尾删就是将最后一个结点释放掉,并将原来最后一个结点的前一个结点中的next值改为NULL使之变为最后一个结点。
尾删分为几种情况:1.当链表为空。2.当链表只有一个结点。3.当链表有多个结点。
1.当链表为空时,不需要删除。
2.当链表只有一个结点时,直接将剩下的最后一个结点释放掉,并让头指针指向NULL。
assert(pphead); // 头指针的地址不可能为空
assert(*pphead); // 如果单链表为空时,不需要删除直接报错
if ((*pphead)->next == NULL) // 单链表只有一个结点的情况
{
free(*pphead);
*pphead = NULL;
}
3.当链表有多个结点时,则需要找到最后一个结点以及最后一个结点的前一个结点,将最后一个结点释放掉,并将前一个结点的next改为NULL。
进行这种情况的操作有两种方法。
1.
// 1. 前后指针 SLNode* tailPrev = NULL; SLNode* tail = *pphead; while (tail->next != NULL) { tailPrev = tail; tail = tail->next; } free(tail); tail = NULL; tailPrev->next= tail; // tailPrev->next=NULL;
2.
// 2. next->next SLNode* tail = *pphead; while (tail->next->next!=NULL) { tail = tail->next; } free(tail->next); tail->next = NULL;
头删SListPopFront
头删就是将存储元素的第一个结点释放掉。
查找SListFind
查找的操作就是将单链表遍历一遍就行,找到了就返回对于结点的地址,没有找到就返回空指针。
插入SListInsert
单链表的插入一般有pos位置之前插入和pos位置之后插入,这次我们写的是在pos位置之前插入。进行pos位置之前插入,需要找到pos位置前的结点,将pos位置前的结点中next指向改为新结点的地址,再将新结点中next指向改为pos位置。
其实这样的插入效率并不高,一般的单链表都是在pos位置之后插入,避免需要找pos位置前的结点。
还有一点,如果需要使用大量的插入,尾插,尾删操作,我们并不会使用单链表,而是使用双向链表。
删除SListErase
单链表的删除操作也有两种情况,一种是删除pos位置的结点,还有一种是删除pos位置之后的结点。因为删除pos位置之后的结点不需要找pos位置之前的结点,而删除pos位置的结点需要找pos位置之前的结点。
这次实现的是删除pos位置的结点,但当我们学习了带头双向链表这些操作的效率就会好很多。