一、链表的概念介绍
1.回顾顺序表:
1.1.当使用realloc申请新的空间时,会涉及->开辟新的空间->拷贝数据->释放原来的空间,这三个步骤会有不小的消耗。
1.2.扩容都是2倍增长,如果没有存储那么多数据就会导致一部分空间浪费,假如100个空间,扩容2倍到200个,但是一共插入的数据只有120个,就会导致有80个空间浪费。
所以,单链表就解决了顺序表的这些问题。
2.概念
链表是一种非连续、非顺序的存储结构,数据元素的逻辑顺序是通过指针链接次序而实现的。
单链表的特点:随机存储,顺序存取。
2.1.图中可以看出链式结构在逻辑上是连续的,但是在物理上不一定连续。
2.2.结点都是从堆上申请来的。
3.链表的分类
3.1.单向或双向
3.2.带头或不带头
3.3.循环或非循环
但实际中常用的只有两种:
1.
无头单向非循环链表:
结构简单
,一般不会单独用来存数据。实际中更多是作为
其他数据结
构的子结构
,如哈希、图的连接表等。另外这种结构在
笔试面试
中出现很多。
2. 带头双向循环链表:
结构最复杂
,一般用在单独存储数据。实际中使用的链表数据结构,都
是带头双向循环链表。另外这个结构虽然结构复杂,但是使用代码以后会发现结构会带来很多优势,实现反而简单了。后续会有讲解。
二、链表的接口实现
无头+单向+非循环链表。。。请往下看:
A:结构体定义
typedef int SLTDataType;
typedef struct SListNode
{
SLTDataType data;//结点存放数据
struct SListNode* next;//结点指向下一个结点
}SLTNode;
B:接口函数
修改结点值时为什么传入二级指针呢?而顺序表却是一级指针?
SLTNode* BuySLTNode(SLTDataType x);//初始化结点
void SListPrint(SLTNode* phead);//打印链表
void SListDestory(SLTNode** pphead);//销毁链表
void SListPushFront(SLTNode** pphead, SLTDataType x);//头插
void SListPopFront(SLTNode** pphead);//头删
void SListPushBack(SLTNode** pphead, SLTDataType x);//尾插
void SListPopBack(SLTNode** pphead);//尾删
SLTNode* SListFind(SLTNode* phead, SLTDataType x);//查找结点
void SListInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x);//按位置插入
void SListInsertAfter(SLTNode** pphead, SLTNode* pos, SLTDataType x);//位置后面插入
void SListErase(SLTNode** pphead, SLTNode* pos);
void SListEraseAfter(SLTNode* pos);
1:动态申请结点函数
只需要申请一个空间赋给newnode,参数x赋值给newnode作为结点值,newnode结点为最后一个结点了,再让newnode结点指向NULL,此为无头链表的申请结点。
SLTNode* BuySLTNode(SLTDataType x)
{
SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
if (newnode == NULL)
{
printf("malloc fail!");
exit(-1);
}
newnode->data = x;
newnode->next = NULL;
return newnode;
}
2:尾部插入函数
就是在链表的尾部插入,考虑链表是否为空,为空直接第一个结点就是尾插。
void SListPushBack(SLTNode** pphead, SLTDataType x)
{
assert(pphead);
SLTNode* newnode = BuySLTNode(x);
if (*pphead == NULL)
{
*pphead = newnode;
}
else
{
SLTNode* tail = *pphead;
while (tail->next != NULL)
{
tail = tail->next;
}
tail->next = newnode;
}
}
3:头部插入函数
void SListPushFront(SLTNode** pphead, SLTDataType x)
{
assert(pphead);
SLTNode* newnode = BuySLTNode(x);
newnode->next = *pphead;
*pphead = newnode;
}
4:尾部删除函数
void SListPopBack(SLTNode** pphead)
{
assert(pphead);
//assert(*pphead != NULL);
if (*pphead == NULL)
{
return;
}
if ((*pphead)->next == NULL)
{
free(*pphead);
*pphead = NULL;
}
else
{
SLTNode* tail = *pphead;
while (tail->next->next != NULL)
{
tail = tail->next;
}
free(tail->next);
tail->next = NULL;
}
}
5:头部删除函数
void SListPopFront(SLTNode** pphead)
{
assert(pphead);
if (*pphead == NULL)
{
exit(-1);
}
SLTNode* del = *pphead;
*pphead = (*pphead)->next;
free(del);
del = NULL;
}
6:插入函数--后
在pos之后插入,pos指向新的结点,新的结点要开空间赋值。
void SListInsertAfter(SLTNode* pos, SLTDataType x)
{
assert(pos);
SLTNode* newnode = BuySLTNode(x);
newnode->next = pos->next;
pos->next = newnode;
}
7: 插入函数--前
void SListInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{
assert(pphead);
assert(pos);
if (*pphead = pos)
{
SListPushFront(pphead, x);
}
else
{
SLTNode* prev = *pphead;
while (prev->next != pos)
{
prev = prev->next;
assert(prev);
}
SLTNode* newnode = BuySLTNode(x);
prev->next = newnode;
newnode->next = pos;
}
}
8:删除函数
void SListErase(SLTNode** pphead, SLTNode* pos)
{
assert(pphead);
assert(pos);
if (*pphead == pos)
{
SListPopFront(pphead);
}
else
{
SLTNode* prev = *pphead;
while(prev->next != pos)
{
prev = prev->next;
}
prev->next = pos->next;
free(pos);
}
}
9:删除函数--后
void SListEraseAfter(SLTNode* pos)
{
assert(pos);
assert(pos->next);
SLTNode* next = pos->next;
pos->next = next->next;
free(next);
}
10:查找函数
SLTNode* SListFind(SLTNode* phead, SLTDataType x)
{
SLTNode* cur = phead;
while (cur)
{
if (cur->data == x)
{
return cur;
}
cur = cur->next;
}
return NULL;
}
11:打印函数
void SListPrint(SLTNode* phead)
{
SLTNode* cur = phead;
while (cur)
{
printf("%d->", cur->data);
cur = cur->next;
}
printf("NULL\n");
}
12:销毁链表函数
void SListDestory(SLTNode** pphead)
{
assert(pphead);
SLTNode* cur = *pphead;
while (cur)
{
SLTNode* next = cur->next;
free(cur);
cur = next;
}
*pphead = NULL;
}
总结:
顺序表和无头单向链表都是各有优势和劣势。看实际当中需要什么,就用什么。