顺序表是存在一些固有的缺陷的:
其中链表就可以很好的解决上面的问题。
链表是一种物理存储结构上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的 。
数据结构中:
实际中链表的结构非常多样,以下情况组合起来就有8种链表结构:
注:
链表的定义
typedef int SLTDataType;
typedef struct SListNode
{
SLTDataType data; // int val;
struct SListNode* next; //注意这个地方不能使用SLTNode* next;这个地方还没有typedef出来就开始使用SLTNode,编译器找定义时只会向上找,然而上面并没有SLTNode。
}SLTNode;
链表的打印
链表的打印需要将头节点的地址进行传参,从指向头节点位置的指针开始依次循环遍历直到遇到NULL为止停止打印节点的数据
void SListPrint(SLTNode* phead)
{
SLTNode* cur = phead;
while (cur != NULL)
{
printf("%d->", cur->data);
cur = cur->next;
}
printf("NULL\n");
}
链表的尾插
链表的尾插首先通过循环的方式找到链表中的最后一个节点,接着要创建一个新节点,节点里面存放尾插的数据,最后将新节点的地址存放到最后一个节点中从而完成连接的过程。这时是最常见的一种情况,还有一种是当链表为空时(没有一个节点,头指针存放NULL),需要创建一个新节点并把新节点的地址存放到头指针中即可。
SLTNode* BuySListNode(SLTDataType x)
{
SLTNode* node = (SLTNode*)malloc(sizeof(SLTNode));
if (node == NULL)
{
printf("malloc fail\n");
exit(-1);
}
node->data = x;
node->next = NULL;
return node;
}
void SListPushBack(SLTNode** pphead, SLTDataType x)
{
assert(pphead);//头指针可能为空,但是头指针的地址是不可能为空的
if (*pphead == NULL)
{
SLTNode* newnode = BuySListNode(x);
*pphead = newnode;
}
else
{
SLTNode* tail = *pphead;
while (tail->next != NULL)
{
tail = tail->next;
}
SLTNode* newnode = BuySListNode(x);
tail->next = newnode;
}
}
注意:
链表的头插
链表的头插只需要创建一个新节点,将头指针内存放的第一个节点的地址存储到新结点中,再将新节点的地址存放到头指针中(其中这两步顺序不能颠倒),从而完成头插的连接。注意当链表为空时这个头插不会出现问题。
void SListPushFront(SLTNode** pphead, SLTDataType x)
{
assert(pphead);
SLTNode* newnode = BuySListNode(x);
newnode->next = *pphead;
*pphead = newnode;
}
链表的尾删
链表尾删的方式有两种:
注:
//第一种:
void SListPopBack(SLTNode** pphead)
{
// 检查参数是否传错
assert(pphead);
// 没有节点断言报错 -- 处理比较激进
// 无节点
assert(*pphead);
// 一个节点
if ((*pphead)->next == NULL)
{
free(*pphead);
*pphead = NULL;
}
// 多个节点
else
{
SLTNode* tail = *pphead;
while (tail->next->next != NULL)
{
tail = tail->next;
}
free(tail);
tail->next = NULL;
}
}
//第二种:
void SListPopBack(SLTNode** pphead)
{
// 参数传错
assert(pphead);
// 无节点
// 温和处理
if (*pphead == NULL)
{
return;
}
// 一个节点
if ((*pphead)->next == NULL)
{
free(*pphead);
*pphead = NULL;
}
// 多个节点
else
{
SLTNode* prev = NULL;
SLTNode* tail = *pphead;
while (tail->next != NULL)
{
prev = tail;
tail = tail->next;
}
free(tail);
prev->next = NULL;
}
}
//第三种:
void SListPopBack(SLTNode** pphead)
{
// 参数传错
assert(pphead);
// 无个节点
// 链表为空了,还在调用尾删
assert(*pphead);
// 一个节点
if ((*pphead)->next == NULL)
{
free(*pphead);
*pphead = NULL;
}
// 多个节点
else
{
SLTNode* prev = NULL;
SLTNode* tail = *pphead;
while (tail->next != NULL)
{
prev = tail;
tail = tail->next;
}
free(tail);
prev->next = NULL;
}
}
//第四种:
void SListPopBack(SLTNode** pphead)
{
// 参数传错
assert(pphead);
// 无节点
// 温和处理
if (*pphead == NULL)
{
return;
}
// 一个节点
if ((*pphead)->next == NULL)
{
free(*pphead);
*pphead = NULL;
}
// 多个节点
else
{
SLTNode* prev = NULL;
SLTNode* tail = *pphead;
while (tail->next != NULL)
{
prev = tail;
tail = tail->next;
}
free(tail);
prev->next = NULL;
}
}
注:pphead的意义是能够在增删的函数中改变外面的plist
链表的头删
链表的头删首先判断头指针的地址是否为空,其次再处理链表中无节点、一个节点、多个节点的情况即可。
注:
//第一种:
void SListPopFront(SLTNode** pphead)
{
// plist一定有地址,所以pphead不为空
assert(pphead);
// 链表为空
assert(*pphead);
// 只有一个节点
if ((*pphead)->next == NULL)
{
free(*pphead);
*pphead = NULL;
}
// 多个节点
else
{
SLTNode* next = (*pphead)->next;
free(*pphead);
(*pphead) = next;
}
}
//第二种:
void SListPopFront(SLTNode** pphead)
{
// plist一定有地址,所以pphead不为空
assert(pphead);
//链表为空
assert(*pphead);
//只有一个节点
//多个节点
SLTNode* next = (*pphead)->next;
free(*pphead);
(*pphead) = next;
}
链表中节点的个数
可以通过循环的方式(终止条件是当前节点的地址是否为空)求链表中节点的个数。
注:phead不需要进行断言,因为phead有可能为空(链表为空)空链表可以求节点个数
int SListSize(SLTNode* phead)
{
int size = 0;
SLTNode* cur = phead;
while (cur)
{
size++;
cur = cur->next;
}
return size;
}
链表判空
链表的判空只需对头指针进行判空即可,如果头指针为空返回true,否则返回false。
注:C语言中用bool类型需要引头文件stdbool.h
//第一种:
bool SListEmpty(SLTNode* phead)
{
return phead == NULL;
}
//第二种:
bool SListEmpty(SLTNode* phead)
{
if (phead == NULL)
{
return true;
}
else
{
return false;
}
}
//第三种:
bool SListEmpty(SLTNode* phead)
{
return phead == NULL ? true : false;
}
链表的查找
链表的查找是通过循环遍历的方式进行查找,如果找到了返回该节点的地址,否则返回NULL。
SLTNode* SListFind(SLTNode* phead, SLTDataType x)
{
SLTNode* cur = phead;
while (cur)
{
if (cur->data == x)
{
return cur;
}
else
{
cur = cur->next;
}
}
return NULL;
}
链表的插入
链表的前插入分两种情况处理:头插、后面插入。这里注意后面插入时首先需要找到pos位置之前的节点的位置(通过循环的方式找到pos位置之前的节点的位置),其次创建要插入的新节点,最后进行插入使其新节点的next指向pos位置的节点(next存储pos的值)并使前一个节点的next指向新节点(next存储新节点的地址)即可。
// 在pos位置之前去插入x
void SListInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{
assert(pphead);
assert(pos);
// 头插
if (*pphead == pos)
{
SListPushFront(pphead, x);
}
// 后面插入
else
{
// 找到pos位置的前一个节点
SLTNode* prev = *pphead;
while (prev->next != pos)
{
prev = prev->next;
}
SLTNode* newnode = BuySListNode(x);
newnode->next = pos;
prev->next = newnode;
}
}
链表的后插入首先要创建一个新节点,其次使其新节点的next指向pos位置上的节点的下一个节点,最后将pos位置上节点的next指向新节点。
// 在pos位置后面去插入x
void SListInsertAfter(SLTNode* pos, SLTDataType x)
{
assert(pos);
//注意插入顺序,不可颠倒
SLTNode* newnode = BuySListNode(x);
newnode->next = pos->next;
pos->next = newnode;
}
注:单链表不适合在某个位置之前插入,因为需要找前一个位置。单链表适合在某个位置之后插入。
链表的删除
链表删除当前位置的节点分两种情况:头删、后面删除。这里注意后面删除首先需要找当前位置节点的前一个节点,其次使其前一个节点的next指向当前位置节点的后一个节点,最后再将pos位置上的节点释放掉。
// 删除pos位置的值
void SListErase(SLTNode** pphead, SLTNode* pos)
{
assert(pphead);
assert(pos);
// 头删
if (pos == *pphead)
{
SListPopFront(pphead);
}
// 后面节点删除
else
{
// 找到pos位置的前一个节点
SLTNode* prev = *pphead;
while (prev->next != pos)
{
prev = prev->next;
}
prev->next = pos->next;
free(pos);
pos = NULL;
}
}
链表删除当前位置的之后的节点,只需将当前位置的节点的next指向当前位置的节点的下一个节点的下一个节点即可,不过在此之前需要对当前位置的节点的下一个节点进行保存避免找不到该节点。最后再将该节点释放掉即可。
void SListEraseAfter(SLTNode* pos)
{
assert(pos);
assert(pos->next);
SLTNode* next = pos->next;
pos->next = next->next;
free(next);
next = NULL;
}
链表的销毁
链表的销毁是将所有节点释放掉,通过循环的方式对链表进行销毁。这里注意释放节点时需要对下一个节点的地址进行保存避免找不到下一个节点的地址,最后需要将头指针置空即可。
注:节点的释放会引发一些问题会把这块节点的使用权还给操作系统并且指向这块空间上的值会被置成随机值
void SListDestory(SLTNode** pphead)
{
assert(pphead);
SLTNode* cur = *pphead;
while (cur)
{
SLTNode* next = cur->next;
free(cur);
cur = next;
}
*pphead = NULL;
}
链表的定义
typedef int LTDataType;
typedef struct ListNode
{
struct ListNode* next;
struct ListNode* prev;
LTDataType data;
}LTNode;
链表的初始化
链表初始化首先要创建一个哨兵位的头节点,其次将哨兵位的头节点的前驱和后继都指向该头节点即可
LTNode* BuyListNode(LTDataType x)
{
LTNode* node = (LTNode*)malloc(sizeof(LTNode));
if (node == NULL)
{
printf("malloc fail\n");
exit(-1);
}
node->next = NULL;
node->prev = NULL;
node->data = x;
return node;
}
//第一种:
void ListInit(LTNode** pphead)
{
*pphead = BuyListNode(-1);
(*pphead)->next = *pphead;
(*pphead)->prev = *pphead;
}
//第二种:
LTNode* ListInit()
{
LTNode* phead = BuyListNode(0);
phead->next = phead;
phead->prev = phead;
return phead;
}
链表的打印
链表打印通过循环的方式从头节点的下一个节点开始进行打印节点(带有哨兵位的头节点是无效节点,不存储有效数据),注意当循环遍历到头节点时结束循环。
void ListPrint(LTNode* phead)
{
assert(phead);
LTNode* cur = phead->next;
while (cur != phead)
{
printf("%d ", cur->data);
cur = cur->next;
}
printf("\n");
}
链表的尾插
链表尾插首先通过头节点的前驱找到尾节点的地址,其次创建新节点,然后修改头节点、新节点、尾节点三者的连接关系即可
注:链表尾插的过程中没有修改头指针
//第一种:
void ListPushBack(LTNode* phead, LTDataType x)
{
assert(phead);
LTNode* tail = phead->prev;
LTNode* newnode = BuyListNode(x);
tail->next = newnode;
newnode->prev = tail;
newnode->next = phead;
phead->prev = newnode;
}
//第二种:
//利用函数复用只需将头节点的指针传给链表的插入函数(相当于在头节点之前插入(链表的尾插))即可。
void ListPushBack(LTNode* phead, LTDataType x)
{
assert(phead);
ListInsert(phead, x);
}
注:时间复杂度为O(1)
链表的头插
利用函数复用只需将头节点的后继传给链表的插入函数(相当于在头节点的后继所指向的节点之前插入(链表的头插))即可。
void ListPushFront(LTNode* phead, LTDataType x)
{
assert(phead);
ListInsert(phead->next, x);
}
补充:C语言中复用函数是指在编程过程中重复使用代码段的一种方法。每个程序中都存在很多相似的或者相同的代码段,把这些相同的代码段抽取出来,形成一个独立的函数就叫做复用函数。复用函数的好处在于可以有效提高程序的可维护性、可读性,提高代码的可复用性,还可以明显减少程序的开发时间,简化程序的结构。
链表的尾删
链表尾删通过头节点的前驱找到尾节点的地址,再通过尾节点的前驱找到尾节点的前一个节点的地址,然后删除尾节点,最后完成头节点和尾节点的前一个节点的连接关系即可。
//第一种:
void ListPopBack(LTNode* phead)
{
assert(phead);
assert(!ListEmpty(phead));
LTNode* tail = phead->prev;
LTNode* tailPrev = tail->prev;
free(tail);
tailPrev->next = phead;
phead->prev = tailPrev;
}
//第二种:
//利用函数复用只需将头节点的前驱传给链表的删除函数(相当于在头节点的前驱所指向的节点进行删除(链表的尾删))即可。
void ListPopBack(LTNode* phead)
{
assert(phead);
assert(!ListEmpty(phead));
ListErase(phead->prev);
}
链表的判空
链表的判空通过判断头节点的后继是否是本身如果是返回真否则返回假。
bool ListEmpty(LTNode* phead)
{
assert(phead);
return phead->next == phead;
}
链表中有效节点的个数
求链表中有效节点的个数通过循环遍历的方式实现即可。
size_t ListSize(LTNode* phead)
{
assert(phead);
size_t n = 0;
LTNode* cur = phead->next;
while (cur != phead)
{
++n;
cur = cur->next;
}
return n;
}
链表的头删
利用函数复用只需将头节点的后继传给链表的删除函数(相当于在头节点的后继所指向的节点进行删除(链表的头删))即可。
void ListPopFront(LTNode* phead)
{
assert(phead);
assert(!ListEmpty(phead));
ListErase(phead->next);
}
链表的插入(在某个位置之前插入节点)
链表的插入首先通过pos位置上节点的前驱找到前一个节点,再创建一个新节点,最后将pos位置上的节点、新节点、pos位置的节点的前一个节点三个节点完成连接即可。
void ListInsert(LTNode* pos, LTDataType x)
{
assert(pos);
LTNode* newnode = BuyListNode(x);
LTNode* prev = pos->prev;
// prev newnode pos
prev->next = newnode;
newnode->prev = prev;
newnode->next = pos;
pos->prev = newnode;
}
链表的删除(删除某个位置的节点)
链表的删除首先找到pos位置上的节点的前一个节点,其次再找到pos位置上的节点的后一个节点,然后释放pos位置上的节点,最后将pos位置上的节点的前一个节点和pos位置上的节点的后一个节点进行连接即可。
// 删除pos位置
void ListErase(LTNode* pos)
{
assert(pos);
LTNode* prev = pos->prev;
LTNode* next = pos->next;
free(pos);
pos = NULL;
prev->next = next;
next->prev = prev;
}
链表的查找
链表的查找是通过循环遍历的方式进行查找,如果找到了返回该节点的地址,否则返回NULL。
LTNode* ListFind(LTNode* phead, LTDataType x)
{
LTNode* cur = phead->next;
while (cur != phead)
{
if (cur->data == x)
{
return cur;
}
cur = cur->next;
}
return NULL;
}
链表的销毁
链表的销毁是将所有节点释放掉(包括带哨兵位的头节点),通过循环的方式对链表进行销毁。这里注意释放节点时需要对下一个节点的地址进行保存避免找不到下一个节点的地址(最后需要将头指针置空)即可。
注:节点的释放会引发一些问题会把这块节点的使用权还给操作系统并且指向这块空间上的值会被置成随机值
//第一种:
void ListDestroy(LTNode* phead)
{
LTNode* cur = phead->next;
while (cur != phead)
{
LTNode* next = cur->next;
free(cur);
//cur = NULL;
cur = next;
}
free(phead);
//phead = NULL;
// 这里其实置空不置空都可以的,因为出了函数作用域,没人能访问phead
// 其次就是phead形参的置空,也不会影响外面的实参
}
//第二种:
void ListDestroy(LTNode** pphead)
{
LTNode* cur = (*pphead)->next;
while (cur != *pphead)
{
LTNode* next = cur->next;
free(cur);
cur = next;
}
free(*pphead);
*pphead = NULL;
}
注:链表销毁函数的参数可以用一级指针也可以用二级指针。参数用一级指针,外面用了以后实参存在野指针的问题。但是这个函数保持了接口的一致性。参数用二级指针,虽然解决了野指针问题但是从接口设计的角度有点混乱。
不同点 | 顺序表 | 链表 |
---|---|---|
存储空间上 | 物理上一定连续 | 逻辑上连续,但物理上不一定连续 |
随机访问(用下标访问) | 支持O(1) | 不支持:O(N) |
任意位置插入或者删除元素 | 可能需要搬移元素,效率低O(N) | 只需修改指针指向,效率O(1) |
插入 | 动态顺序表,空间不够时需要扩容 | 没有容量的概念(按需申请空间) |
应用场景 | 元素高效存储+频繁访问 | 任意位置插入和删除频繁 |
缓存利用率(缓存命中率) | 高 | 低 |
注:
存储体系结构以及局部原理性。