本文代码均采用C语言编写~~
本文歌单:底线——山止川行/庄淇文29
1.单链表
1.1链表的概念
概念:链表是一种 物理存储结构上非连续 、非顺序的存储结构,数据元素的 逻辑顺序 是通过链表中的指针链接 次序实现的 。
1.2链表的必要性
在上一篇文章(传送门)的最后,我们提到了顺序表在数据存储时的两个缺陷——扩容消耗和高时间复杂度。为此,数据结构中专门引进了链表来解决这两个问题。
本文会聚焦于结构最简单的链表——单链表来进行详细讲解。
1.3单链表结构
单链表的结构是由一个数据域(data)和一个后驱指针域(next)组成,如图示:
结构体定义,如图示:
typedef struct SingleListNode {
int data;
struct SingleListNode* Next;
}SingleListNode;
1.4单链表解决什么问题?
从单链表的结构可以看出,其主要解决的问题重心在于扩容消耗。单链表的按照节点串联,完全避免了因为堆区空间不够大而产生的异地扩容消耗(malloc系函数的特点),每次开辟的空间只要节点大小即可,减小了对空间的要求。
2.单链表函数接口实现
这个板块图形出现的地址为了好理解,我没有采用真实运行地址,只是形象化,请大家不要过于纠结~~
2.1函数列表展示
//创建新的节点
SingleListNode* BuyNewNode(int c);
//打印单链表
void SingleListPrint(SingleListNode* phead);
//尾插
void SingleListPushBack(SingleListNode** pphead, int c);
//头插
void SingleListPushFront(SingleListNode** pphead, int c);
//尾删
void SingleListPopBack(SingleListNode** pphead);
//头删
void SingleListPopFront(SingleListNode** pphead);
//查找
SingleListNode* SIngleListFind(SingleListNode* phead, int c);
//pos位置前面数据插入
void SingleListInsert(SingleListNode** pphead, SingleListNode* pos, int c);
//pos删除
void SingleListErase(SingleListNode** pphead, SingleListNode* pos);
下面是各函数实现代码演示
2.2创建新节点
这是一个在插入函数中所公用的一段代码,单独列出来方便引用
//创建新的节点
SingleListNode* BuyNewNode(int c) {
SingleListNode* newnode = (SingleListNode*)malloc(sizeof(SingleListNode));
assert(newnode);
newnode->data = c;
newnode->Next = NULL;
}
2.3打印单链表
单链表的打印和顺序的打印有一定的差别,物理结构上的不连续注定只能通过指针的访问来获得对应数据,这时候对于访问越界就要充分地进行考虑。结构体指针cur的最终指向只能最后一个节点的地址,而不是NULL。
//打印单链表
void SingleListPrint(SingleListNode* phead) {
SingleListNode* cur = phead;
while (cur != NULL) {
printf("%d->", cur->data);
cur = cur->Next;
}
printf("NULL\n");
}
2.4尾插
在单链表最后补上一个新节点有两点注意
第一点,新节点(newnode)的指针域必须为NULL,强调这是尾节点,也是在创建新节点时我们将Next置为NULL的原因之一。
第二点,原来的尾节点的指针域从NULL变为新尾节点的地址,这点是容易遗忘的。
如图示:
代码:
//尾插
void SingleListPushBack(SingleListNode** pphead, int c) {
assert(pphead);
SingleListNode* newnode = BuyNewNode(c);
//空链表的尾插
if (*pphead == NULL) {
*pphead = newnode;
}
else {
//寻找尾节点
SingleListNode* tail = *pphead;
while (tail->Next != NULL) {
tail = tail->Next;
}
tail->Next = newnode;
}
}
2.5头插
头插唯一的注意点就是在于判断一级指针*pphead的非空判定上(虽然不是特别重要)
如图示:
代码:
//头插
void SingleListPushFront(SingleListNode** pphead, int c) {
assert(pphead);
assert(*pphead != NULL);
SingleListNode* newnode = BuyNewNode(c);
newnode->Next = *pphead;
*pphead = newnode;
}
2.6尾删
尾删有两种方法推荐
第一种:双指针
双指针的方法是使用tail和tailPrev两个指针去找到尾节点和倒数第二个节点,释放tail指向的空间,将tailPrev的指针域置为NULL,完成尾删,如图示:
代码:
//尾删
void SingleListPopBack(SingleListNode** pphead) {
assert(*pphead);
if ((*pphead)->Next == NULL) {
free(*pphead);
(*pphead) = NULL;
}
else {
//方法一:双指针
SingleListNode* tailPrev = NULL;
SingleListNode* tail = (*pphead);
while (tail->Next != NULL) {
tailPrev = tail;
tail = tail->Next;
}
free(tail);
tailPrev->Next = NULL;
}
}
第二种:结构体访问连用
这个方法的精髓在于一句代码上,即:
while (tail->Next->Next != NULL)
具体代码大家细细品味一下吧
//尾删
void SingleListPopBack(SingleListNode** pphead) {
assert(*pphead);
if ((*pphead)->Next == NULL) {
free(*pphead);
(*pphead) = NULL;
}
else {
//方法二:指针的连用
SingleListNode* tail = (*pphead);
while (tail->Next->Next != NULL) {
tail = tail->Next;
}
free(tail->Next);
tail->Next = NULL;
}
}
2.7头删
头删的注意点在于最后要将一级指针*pphead要置为第二个节点地址后再释放空间,这个非常重要!
代码:
//头删
void SingleListPopFront(SingleListNode** pphead) {
assert(pphead);
assert(*pphead != NULL);
SingleListNode* next = (*pphead)->Next;
free(*pphead);
*pphead = next;
}
2.8查找
//查找
SingleListNode* SIngleListFind(SingleListNode* phead, int c) {
SingleListNode* cur = phead;
while (cur->data != c) {
if (cur->Next == NULL) {
return NULL;
}
cur = cur->Next;
}
return cur;
}
2.9增删操作统一函数
统一函数的思路均已在上面代码中出现,这里不再赘述了,大家自己看看就行。
2.9.1pos位置前查找
//pos前插,后插不做演示
void SingleListInsert(SingleListNode** pphead, SingleListNode* pos, int c) {
assert(pphead);
assert(pos);
if (pos == *pphead) {
SingleListPushFront(pphead, c);
}
else {
//找前面数据的地址
SingleListNode* prev = *pphead;
while (prev->Next != pos) {
prev = prev->Next;
}
SingleListNode* newnode = BuyNewNode(c);
prev->Next = newnode;
newnode->Next = pos;
}
}
2.9.2pos删除
//pos删除
void SingleListErase(SingleListNode** pphead, SingleListNode* pos) {
assert(pphead);
assert(pos);
if (*pphead == pos) {
SingleListPopFront(pphead);
}
else{
SingleListNode* prev = *pphead;
while (prev->Next != pos) {
prev = prev->Next;
}
prev->Next = pos->Next;
free(pos);
}
}
3.单链表的缺点
单链表在解决顺序表的扩容消耗的问题之后,其时间复杂度的问题还是没有得到解决,甚至遇到了比顺序表更难以解决的问题:
1.单链表的删除一直是O(n),和顺序表一样。
2.单链表的访问只可以单向前进,没有回头的机会,访问前面数据要新的指针寻找。
对于以上的问题,接下来将引入经过完善后的双向链表和特殊结构链表将会在各自对应应用场景下发挥作用。