本文将主要介绍链表的概念和结构,并实现最常用的两种链表:无头单向非循环链表 和 带头双向循环链表。
目录
一、链表的概念及其结构
概念:链表是一种物理存储结构上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的 。
注意:
1.从上图可看出,链式结构在逻辑上是连续的,但在物理上不一定连续。
2.现实中的结点一般都是从堆上申请出来的。
3.从堆上申请的空间,是按照一定的策略来分配的,两次申请的空间可能连续也可能不连续。
二、链表的分类
实际中链表的结构非常多样,以下情况组合起来就有8种链表结构:
1. 单向或者双向
2. 带头或者不带头
3. 循环或者非循环
虽然有这么多的链表的结构,但是我们实际中最常用还是两种结构:
无头单向非循环链表:
带头双向循环链表:
1. 无头单向非循环链表:
结构简单,一般不会单独用来存数据。实际中更多是作为其他数据结构的子结构,如哈希桶、图的邻接表等等。另外这种结构在笔试面试中出现很多。
2. 带头双向循环链表:
结构最复杂,一般用在单独存储数据。实际中使用的链表数据结构,都是带头双向循环链表。另外这个结构虽然结构复杂,但是使用代码实现以后会发现结构会带来很多优势,实现反而简单了。
三、链表的实现
1.无头单向非循环链表:
链表节点定义:
typedef int SLTDateType;//假设int为该链表的数据类型
typedef struct SListNode
{
SLTDateType data;//存放链表数据
struct SListNode* next;//指向下一个节点的指针
}SListNode;//重命名简化
需要实现的接口:
// 动态申请一个节点
SListNode* BuySListNode(SLTDateType x);
// 单链表打印
void SListPrint(SListNode* phead);
// 单链表尾插
void SListPushBack(SListNode** pphead, SLTDateType x);
// 单链表的头插
void SListPushFront(SListNode** pphead, SLTDateType x);
// 单链表的尾删
void SListPopBack(SListNode** pphead);
// 单链表头删
void SListPopFront(SListNode** pphead);
// 单链表查找
SListNode* SListFind(SListNode* phead, SLTDateType x);
// 单链表在pos位置之前插入x
void SListInsert(SListNode* pos, SLTDateType x);
// 单链表删除pos位置之前的值
void SListErase(SListNode* pos);
// 单链表在pos位置之后插入x
void SListInsertAfter(SListNode* pos, SLTDateType x);
// 单链表删除pos位置之后的值
void SListEraseAfter(SListNode* pos);
// 单链表销毁
void SListDestory(SListNode* phead);
我们分模块来实现上述接口:
① SeqList.h 文件:存放头文件的包含、顺序表结构体和函数的声明。
② SeqList.c 文件:存放函数的定义,来实现单链表的各个功能。
③ test.c 文件: 用于单链表的测试。
1.动态节点的申请
SListNode* BuySListNode(SLTDateType x)//申请一个新的节点
{
SListNode* newnode = (SListNode*)malloc(sizeof(SListNode));
assert(newnode);//判断是否申请成功
newnode->data = x;//存入新节点的数据
newnode->next = NULL;//下一个节点为空
return newnode;
}
2.单链表打印
打印单链表,定义一个指针cur,遍历链表并记录当前位置,当cur->next 为 NULL 时,结束遍历。
void SListPrint(SListNode* phead)
{
SListNode* cur = phead; //从头节点开始
while (cur != NULL)
{
printf("%d -> ", cur->data);//打印链表数据
cur = cur->next;
}
printf("NULL\n");
}
3.单链表的头插
创建一个新的节点,让新节点的指针指向原头节点,并将新的节点修改为头节点即可。
注意:要修改传入的参数指针plist,需要参数的地址。或者返回新的头指针。
void SListPushFront(SListNode** pphead, SLTDateType x)
{
assert(pphead);
SListNode* newnode = BuySListNode(x);//申请一个新的节点
newnode->next = *pphead;//将新节点的指针指向原来的头节点
*pphead = newnode;//将新节点设置为头节点
}
4.单链表的尾插
创建一个新的节点,找到链表中的尾节点,让尾节点的指针指向新节点,将新节点的指针置空。
void SListPushBack(SListNode** pphead, SLTDateType x)
{
assert(pphead);
SListNode* newnode = BuySListNode(x);//申请一个新节点
if (*pphead == NULL)//判断原链表是否为空
{
*pphead = newnode;//原链表为空,新链表为头节点
}
else
{
SListNode* tail = *pphead;
while (tail->next != NULL)//遍历链表找尾节点
{
tail = tail->next;
}
tail->next = newnode;//新节点在申请时,指针已经置空
}
}
5.单链表的头删
让头指针指向下一个节点,并释放掉原来的头节点。
void SListPopFront(SListNode** pphead)
{
assert(pphead);
assert(*pphead);//确保原链表和指针参数不为空
SListNode* next = (*pphead)->next;
free(*pphead);
*pphead = next;
}
6.单链表的尾删
遍历链表,找到链表最后节点的前一个节点,释放最后一个节点,并将最后一个节点的前一个接的的指针置空。
void SListPopBack(SListNode** pphead)
{
assert(pphead);
assert(*pphead);//确保原链表和指针参数不为空
if ((*pphead)->next == NULL)//如果链表只有一个节点
{
free(*pphead);
*pphead = NULL;
}
else
{
SListNode* tail = *pphead;
while (tail->next->next != NULL)//寻找最后节点的前一个节点
//如果链表只有一个节点,会造成越界访问
{
tail = tail->next;
}
free(tail->next);
tail->next = NULL;
}
}
7.单链表的查找
遍历链表,查找目标数据,如果找到了返回该节点地址,没找到则返回空指针。
SListNode* SListFind(SListNode* pphead, SLTDateType x)
{
SListNode* cur = pphead;
while (cur)
{
if (cur->data == x)
{
return cur;
}
cur = cur->next;
}
return NULL;
}
8.在指定位置插入节点
首先找到指定位置之前的节点,创建一个新的节点,让新的节点的指针指向指定的节点,让指定的节点之前的节点指向新节点即可。
void SListInsert(SListNode** pphead, SListNode* pos, SLTDateType x)
{
assert(pphead);
assert(pos);
if (*pphead == pos)//当置指定位置为头节点时,此时为头插
{
SListPushFront(pphead, x);
}
else
{
SListNode* prev = *pphead;
while (prev->next != pos)//寻找指定位置的前一个节点
{
prev = prev->next;
}
SListNode* newnode = BuySListNode(x);
prev->next = newnode;
newnode->next = pos;
}
}
9.在指定位置删除节点
首先找到指定位置之前的节点,让指定节点前一个节点的指针指向指定节点后一个节点,再释放掉指定节点即可。
void SListErase(SListNode** pphead, SListNode* pos)
{
assert(pphead);
assert(pos);
if (pos == *pphead)//pos为头节点时,为头删
{
*pphead = (*pphead)->next;
free(pos);
}
else
{
SListNode* prev = *pphead;
while (prev->next != pos)//寻找指定位置的前一个节点
{
prev = prev->next;
}
prev->next = pos->next;//让指定位置的前一个节点的指针指向指定位置的后一个节点
free(pos);
}
}
10.在指定位置之后插入节点
创建一个新的节点,让新节点的指针 指向 指定节点 指向的节点,再让指定节点 指向的新创建的节点即可。修改指定位置之后的节点,不需要考虑是否会改变头指针的情况,因此不需要使用二级指针。
void SListInsertAfter(SListNode* pos, SLTDateType x)
{
assert(pos);
SListNode* newnode = BuySListNode(x);
newnode = pos->next;
pos->next = newnode;
}
11.在指定位置之后删除节点
记录指定节点的地址,让指定节点之前的节点指向指定位置之后的节点,再结束指定节点。
void SListEraseAfter(SListNode* pos)
{
assert(pos);
assert(pos->next);//避免指定位置之后为空指针
SListNode* del = pos->next;
pos->next = pos->next->next;
free(del);
}
12.单链表的销毁
遍历链表,逐个节点释放。
void SListDestory(SListNode* phead)
{
assert(phead);
SListNode* cur = phead;
while (cur)
{
SListNode* next = cur->next;
free(cur);
cur = next;
}
}
2.带头双向循环链表(图片演示):
链表节点定义:
这里的头节点为哨兵位,仅作为链表的头节点,不存储有效的数据。
typedef int LTDataType;//假设链表数据类型
typedef struct ListNode
{
struct ListNode* next;
struct ListNode* prev;
LTDataType data;
}LTNode;
需要实现的接口:
//初始化链表
LTNode* ListInit();
//尾插
void ListPushBack(LTNode* phead, LTDataType x);
//头插
void ListPushFront(LTNode* phead, LTDataType x);
//头删
void ListPopFront(LTNode* phead);
//尾删
void ListPopBack(LTNode* phead);
//打印
void ListPrint(LTNode* phead);
//在指定位置插入数据
void ListInsert(LTNode* pos, LTDataType x);
//在指定位置删除数据
void ListErase(LTNode* pos);
//查找
LTNode* ListFind(LTNode* plist, LTDataType x);
//销毁
void ListDestory(LTNode* phead);
1.初始化
初始化创建一个不存储数据的哨兵位(如下图):
LTNode* BuyListNode(LTDataType x)//创建一个新节点
{
LTNode* node = (LTNode*)malloc(sizeof(LTNode));
if (node == NULL)//判断节点是否开辟成功
{
perror("BuyListNode::malloc");
exit(-1);
}
node->next = NULL;
node->prev = NULL;
node->data = x;
return node;
}
LTNode* ListInit()
{
LTNode* phead = BuyListNode(-1);//创建一个哨兵位头节点
phead->next = phead;
phead->prev = phead;
return phead;
}
2.指定位置插入数据(头插、尾插)
void ListInsert(LTNode* pos, LTDataType x)
{
assert(pos);
LTNode* newnode = BuyListNode(x);//创建一个新节点
LTNode* prev = pos->prev;
prev->next = newnode;
newnode->next = pos;
pos->prev = newnode;
newnode->prev = prev;
}
由于存在哨兵位,头插如下图:
即 在指定位置head->next ,插入一个数据
因此直接对指定位置插入函数进行复用即可。
void ListPushFront(LTNode* phead, LTDataType x)
{
assert(phead);
ListInsert(phead->next, x);
}
尾插:
循环链表首尾相连,在head之前插入即可。
void ListPushBack(LTNode* phead, LTDataType x)
{
assert(phead);
ListInsert(phead, x);
}
3.指定位置删除数据(头删、尾删)
改变指针,释放pos即可。
void ListErase(LTNode* pos)
{
assert(pos);
assert(pos->next != pos);//除哨兵位外,链表至少有一个节点
LTNode* prev = pos->prev;
LTNode* next = pos->next;
prev->next = next;
next->prev = prev;
free(pos);
}
头删:
即删除head->next 的节点
void ListPopFront(LTNode* phead)
{
ListErase(phead->next);
}
尾删:
即删除head->prev 的节点
void ListPopBack(LTNode* phead)
{
ListErase(phead->prev);
}
4.打印链表
定义指针cur遍历链表,直到回到头节点。
void ListPrint(LTNode* phead)
{
assert(phead);
LTNode* cur = phead->next;
while (cur != phead)
{
printf("%d ", cur->data);
cur = cur->next;
}
printf("\n");
}
5.销毁链表
遍历链表,逐个删除,最后释放头节点。
void ListDestory(LTNode* phead)
{
assert(phead);
LTNode* cur = phead->next;
while (cur != phead)
{
LTNode*next = cur->next;
ListErase(cur);
cur = next;
}
free(phead);
}