一、链表
1.1 链表的概念以及结构
链表是一种物理上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针依次链接实现的。
逻辑结构是为了方便理解想象出来的,物理结构是实际内存中真实的存储方式。
链表的逻辑结构:

链表的物理结构:

注:链表在逻辑结构上是连续的,但在物理结构上并不一定连续。并且链表的每个节点一般都是用malloc从堆区里申请出来的。
1.2 链表的分类
实际中链表的结构非常多样,不同的组合共有8种:
1.单向或双向

2.带头节点或不带头节点

3.循环或不循环

最常用的两种结构:

1.3 无头单向不循环链表的接口实现
结构:
cpp
typedef int SLTDataType;
typedef struct SListNode
{
SLTDataType data; //节点的数据域,用于存放数据
struct SListNode* next; //节点的指针域,用于存放指向下一个节点的指针
}SLTNode; //对结构体的重命名
链表的打印:

cpp
//链表的打印
void SLTPrint(SLTNode* phead)
{
SLTNode* cur = phead;
while(cur != NULL)
{
printf("%d->", cur->data);
cur = cur->next;
}
printf("NULL\n");
}
注:此处的cur是一个临时的结构体指针变量,用于存储结构体的地址,cur=cur->next相当于把cur指向的下一个节点的地址赋给cur这个临时变量,进行访问下一个节点。
动态申请一个节点:
cpp
//动态申请一个节点
SLTNode* BuySLTNode(SLTDataType x)
{
SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
if (newnode == NULL)
{
perror("malloc fail");
return NULL;
}
newnode->data = x;
newnode->next = NULL;
return newnode;
}
链表头插:

cpp
void SLTPushFront(SLTNode** pphead, SLTDataType x);
cpp
//链表头插
void SLTPushFront(SLTNode** pphead, SLTDataType x)
{
assert(pphead);
SLTNode* newnode = BuySLTNode(x);
newnode->next = *pphead;
*pphead = newnode;
}
注:链表的头插需要改变的是结构体指针phead,因此在传参时需要传入结构体指针的地址。
链表尾插:

cpp
void SLTPushBack(SLTNode** pphead, SLTDataType x);
cpp
//链表尾插
void SLTPushBack(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;
}
}
注:尾插空链表时需要改变的是phead,是一个结构体指针类型,因此传参需要传结构体指针的指针pphead。当尾插的链表不是空链表时,尾插改变的是尾节点的next,改变的是一个结构体类型,只需要结构体指针就行。
链表头删:

cpp
void SLTPopFront(SLTNode** pphead);
cpp
//链表头删
void SLTPopFront(SLTNode** pphead)
{
assert(pphead);
//当链表为空,不能继续进行删除操作
assert(*pphead);
//链表不为空
SLTNode* del = *pphead;
*pphead = del->next;
free(del);
del = NULL;
}
链表尾删:

cpp
void SLTPopBack(SLTNode** pphead);
cpp
//链表尾删
void SLTPopBack(SLTNode** pphead)
{
assert(pphead);
//当链表为空,不能继续进行删除操作
assert(*pphead);
//当链表只剩下一个节点
if ((*pphead)->next == NULL)
{
free(*pphead);
*pphead = NULL;
}
else
{
//当链表还剩多个节点
//先找到尾节点的前一个节点
SLTNode* prev = *pphead;
while (prev->next->next != NULL)
{
prev = prev->next;
}
free(prev->next);
prev->next = NULL;
}
}
链表查找:

cpp
SLTNode* SLTFind(SLTNode* phead, SLTDataType x);
cpp
//链表查找
SLTNode* SLTFind(SLTNode* phead, SLTDataType x)
{
SLTNode* pos = phead;
//找到了返回节点的地址,找不到返回NULL
while (pos)
{
if (pos->data != x)
{
pos = pos->next;
}
else
{
return pos;
}
}
return NULL;
}
链表在pos位置之前插入x:
情况1:pos刚好是第一个节点

情况2:pos不是第一个节点

cpp
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x);
cpp
//在pos位置之前插入
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{
assert(pphead);
assert(pos); //pos不能传空指针
SLTNode* newnode = BuySLTNode(x);
//当pos是第一个节点时
if (*pphead == pos)
{
*pphead = newnode;
newnode->next = pos;
}
else
{
//当pos不是第一个节点时,需要先找到pos的前一个节点
SLTNode* cur = *pphead;
while (cur->next != pos)
{
cur = cur->next;
}
cur->next = newnode;
newnode->next = pos;
}
}
链表在pos位置后面插入x:

cpp
void SLTInsertAfter(SLTNode* pos, SLTDataType x);
cpp
//在pos位置后面插入x
void SLTInsertAfter(SLTNode* pos, SLTDataType x)
{
assert(pos);
SLTNode* newnode = BuySLTNode(x);
newnode->next = pos->next;
pos->next = newnode;
}
注:此处必须先把newnode->next设置为pos->next,如果先把pos->next设置为newnode的话,会出现循环链表的情况。
如下:

链表删除pos位置的值:
情况1:pos刚好是第一个节点

情况2:pos不是第一个节点

cpp
void SLTErase(SLTNode** pphead, SLTNode* pos);
cpp
//删除pos位置的值
void SLTErase(SLTNode** pphead, SLTNode* pos)
{
assert(pphead);
assert(pos);
//情况1:如果pos是第一个节点
if (*pphead == pos)
{
*pphead = pos->next;
free(pos);
pos = NULL;
}
else
{
//情况2:pos不是第一个节点
//先找到pos的前一个节点
SLTNode* cur = *pphead;
while (cur->next != pos)
{
cur = cur->next;
}
cur->next = pos->next;
free(pos);
pos = NULL;
}
}
链表删除pos位置后面的值:

cpp
void SLTEraseAfter(SLTNode* pos);
cpp
//删除pos位置后面的值
void SLTEraseAfter(SLTNode* pos)
{
assert(pos);
assert(pos->next); //pos的下一个值不能为NULL
SLTNode* del = pos->next;
pos->next = del->next;
free(del);
del = NULL;
}
单链表的销毁:

cpp
void SLTDestroy(SLTNode** pphead);
cpp
//单链表销毁
void SLTDestroy(SLTNode** pphead)
{
assert(pphead);
SLTNode* cur = *pphead;
while (cur)
{
SLTNode* next = cur->next;
free(cur);
cur = next;
}
*pphead = NULL;
}
1.4 带头双向循环链表的接口实现
带头双向循环链表的结构:

cpp
typedef int LTDataType;
typedef struct LTNode
{
struct LTNode* prev; //指向前一个节点的指针
struct LTNode* next; //指向下一个节点的指针
LTDataType data; //用于保持节点的数据
}LTNode; //对结构体的重命名
初始化:

cpp
LTNode* LTInit();
cpp
LTNode* LTInit()
{
LTNode* phead = (LTNode*)malloc(sizeof(LTNode));
if (phead == NULL)
{
perror("malloc fail");
return NULL;
}
phead->next = phead;
phead->prev = phead;
return phead;
}
打印:

cpp
void LTPrint(LTNode* phead);
cpp
//打印
void LTPrint(LTNode* phead)
{
assert(phead);
LTNode* cur = phead->next;
printf("哨兵位<==>");
while (cur != phead)
{
printf("%d<==>", cur->data);
cur = cur->next;
}
printf("\n");
}
申请一个节点:

cpp
//申请一个节点
LTNode* LTBuyNode(LTDataType x)
{
LTNode* newnode = (LTNode*)malloc(sizeof(LTNode));
if (newnode == NULL)
{
perror("malloc fail");
return NULL;
}
newnode->data = x;
newnode->next = newnode;
newnode->prev = newnode;
return newnode;
}
尾插:

cpp
void LTPushBack(LTNode* phead, LTDataType x);
cpp
//尾插
void LTPushBack(LTNode* phead, LTDataType x)
{
assert(phead);//双向循环链表必定会有一个头结点head,头结点的指针不可能为空
LTNode* newnode = LTBuyNode(x);
//找尾
LTNode* tail = phead->prev;
//newnode和尾链接
tail->next = newnode;
newnode->prev = tail;
//newnode和头链接
newnode->next = phead;
phead->prev = newnode;
}
头插:


cpp
void LTPushFront(LTNode* phead, LTDataType x);
cpp
//头插
void LTPushFront(LTNode* phead, LTDataType x)
{
assert(phead);
LTNode* newnode = LTBuyNode(x);
//保存头节点的下一个节点next
LTNode* next = phead->next;
//链接头结点和newnode
phead->next = newnode;
newnode->prev = phead;
//链接newnode和next
newnode->next = next;
next->prev = newnode;
}
尾删:

cpp
void LTPopBack(LTNode* phead);
cpp
//尾删
void LTPopBack(LTNode* phead)
{
assert(phead);
assert(phead->next != phead); //链表的有效节点个数不能为0
//先找到尾节点的前一个节点
LTNode* tail = phead->prev;
LTNode* tailPrev = tail->prev;
//链接tailPrev和phead
tailPrev->next = phead;
phead->prev = tailPrev;
//释放尾节点
free(tail);
}
头删:

cpp
void LTPopFront(LTNode* phead);
cpp
//头删
void LTPopFront(LTNode* phead)
{
assert(phead);
assert(phead->next != phead); //链表的有效节点个数不能为0
//先找第一个节点和第二个节点
LTNode* first = phead->next;
LTNode* second = first->next;
//链接头结点和第二个节点
phead->next = second;
second->prev = phead;
//释放第一个节点
free(first);
}
查找:
cpp
LTNode* LTFind(LTNode* phead, LTDataType x);
cpp
//查找
LTNode* LTFind(LTNode* phead,LTDataType x)
{
assert(phead);
//找到就返回节点的地址,找不到返回NULL
LTNode* cur = phead->next;
while (cur != phead)
{
if (cur->data == x)
{
return cur;
}
cur = cur->next;
}
return NULL;
}
注:查找同时也可以代表修改。
在pos位置的前面插入:


cpp
void LTInsert(LTNode* pos, LTDataType x);
cpp
//在pos前面插入
void LTInsert(LTNode* pos, LTDataType x)
{
assert(pos); //确保pos不能为空
//记录pos的前一个节点
LTNode* posPrev = pos->prev;
LTNode* newnode = LTBuyNode(x);
//链接posPrev和newnode
posPrev->next = newnode;
newnode->prev = posPrev;
//链接pos和newnode
newnode->next = pos;
pos->prev = newnode;
}
删除pos位置的值:

cpp
void LTErase(LTNode* pos);
cpp
//删除pos位置的值
void LTErase(LTNode* pos)
{
assert(pos);
//保存pos的前一个节点和后一个节点
LTNode* posPrev = pos->prev;
LTNode* posNext = pos->next;
//链接posPrev和posNext
posPrev->next = posNext;
posNext->prev = posPrev;
//释放pos
free(pos);
}
注:pos的值也不能等于phead,这一点可以进行另外的判断。
销毁:
cpp
void LTDestroy(LTNode* phead);
cpp
//销毁
void LTDestroy(LTNode* phead)
{
assert(phead);
LTNode* cur = phead->next;
while (cur != phead)
{
LTNode* next = cur->next;
free(cur);
cur = next;
}
free(phead);
}
注:销毁需要使用者在外部将phead置空,和free的使用方法相似。
双向带头循环链表的头插、尾插和头删、尾删可以复用Insert和Erase。
二、顺序表和链表的区别
|--------------|---------------------------------------------------------------------------|--------------------|
| 不同点 | 顺序表 | 链表 |
| 存储空间上 | 一块连续的物理空间 | 逻辑上连续,但在物理空间上不一定连续 |
| 随机访问 | 支持 | 不支持 |
| 任意位置的插入或删除元素 | 可能需要挪动元素,效率为O(N) | 只需要修改指针的指向 |
| 插入 | 动态顺序表空间不够时需要扩容,扩容会有两个缺点1.扩容一般是以2倍的形式进行扩,会有扩容所需要的代价;2.扩容后的插入数据一般还会伴随着空间的浪费 | 按需申请空间 |
| 应用场景 | 元素高效存储+频繁访问 | 频繁在任意位置插入删除 |
| 缓存利用率 | CPU高速缓存的缓存命中率会更高 | CPU高速缓存的缓存命中率会更低 |
有关CPU缓存相关的详细文章: