1. 链表的基本概念
- 数组的特点
- 缺点
(1)数组的大小不够灵活,其元素个数在定义时必须确定。若定义太小则不够用时无法改变数组大小,若定义太大则会导致内存浪费。
(2)对于元素的增删操作效率太低。当堆数组中某个元素进行增删时,其后面元素必须跟着整体后移或前移; - 优点
(1)读写操作的访问速度快。因为其在内存中连续分布,读写其他元素时只需偏移指针即可;
(2)占用内存小,每个元素只占用本身的内存空间。链表则需要下一节点的地址。
- 链表的组成结构
- 链表由一个或多个节点组成,每个节点包含数据域和指针域两部分。
(1)数据域存储数据;
(2)指针域存储下一个节点的地址;
- 链表的优点
(1)链表中可以在任何两节点之间进行增删,而不影响其他节点;
(2)节点的增删只需要将上一个节点指针域指向新节点,新节点指针域指向下一节点即可。
- 节点的表示方法
struct LinkNode
{
int num; //数据域
struct LinkNode* next; //指针域
}
2. 链表的分类
- 链表的分类
- 动态链表和静态链表
(1)动态链表分配在堆区
struct LinkNode
{
int num;
struct LinkNode* next;
};
/*创建静态链表*/
void test01()
{
//1.初始化节点
struct LinkNode node1 = { 10, NULL };
struct LinkNode node2 = { 20, NULL };
struct LinkNode node3 = { 30, NULL };
struct LinkNode node4 = { 40, NULL };
struct LinkNode node5 = { 50, NULL };
//2.创建节点之间关系
node1.next = &node2;
node2.next = &node3;
node3.next = &node4;
node4.next = &node5;
//3.遍历节点
struct LinkNode* pCurrent = &node1;
while (NULL != pCurrent)
{
printf("%d\n", pCurrent->num);
pCurrent = pCurrent->next;
}
}
int main(int argc, char* argv[])
{
test01();
return 0;
}
(2)静态链表分配在栈区
/*创建动态链表*/
void test02()
{
//1.初始化节点
struct LinkNode* node1 = malloc(sizeof(struct LinkNode));
struct LinkNode* node2 = malloc(sizeof(struct LinkNode));
struct LinkNode* node3 = malloc(sizeof(struct LinkNode));
struct LinkNode* node4 = malloc(sizeof(struct LinkNode));
struct LinkNode* node5 = malloc(sizeof(struct LinkNode));
//2.建立节点之间联系
node1->num = 10;
node1->next = node2;
node2->num = 20;
node2->next = node3;
node3->num = 30;
node3->next = node4;
node4->num = 40;
node4->next = node5;
node5->num = 50;
node5->next = NULL;
//3.遍历节点
struct LinkNode* pCurrent = node1;
while (NULL != pCurrent)
{
printf("%d\n", pCurrent->num);
pCurrent = pCurrent->next;
}
}
int main(int argc, char* argv[])
{
test02();
return 0;
}
- 单向链表、双向链表、单向循环链表、双向循环链表
(1)单向链表:只指向下一节点地址;
(2)双向链表:指向前后两节点地址;
(3)单向循环链表:尾节点指向首节点地址;
(4)双向循环列表:首尾节点互指向地址。
- 带头节点的链表
- 特点
(1)带头节点的链表头节点永远固定。当在链表头部增加新节点时,避免重新为链表赋予新的头节点地址;
(2)头节点只维护指针域而不维护数据域,因此头节点的数据是无意义的。
2. 链表的设计
2.1 链表的原理
- 目的:用户可以通过提供的接口来操作链表,并且无法对链表的本身信息进行修改。即对链表的操作进行封装。
- 创建一个链表的结构体
(1)成员包括一个链表的头结点;
(2)该链表的节点长度。
/*定义链表结构体*/
typedef struct LinkList
{
LinkNode pHeader;
int m_Size;
}LinkList;
- 创建一个节点的节点的结构体
(1)数据域为用户的未知数据类型的地址;
(2)指针域为下一节点地址;
/*定义节点结构体*/
typedef struct LinkNode
{
void* data;
struct LinkNode* next;
}LinkNode;
- 对链表的类型进行封装,使用户无法得知传入的具体数据类型;
typedef void* LList;
2.2 链表的初始化
- 申请链表的内存空间;
- 初始化链表以及其内部的节点;
- 返回链表的地址。
/*初始化链表*/
LList init_LinkList()
{
//1.申请内存空间
LinkList* list = malloc(sizeof(LinkList));
if (NULL == list)
{
return NULL;
}
//2.初始化链表及节点
list->pHeader.data = 0;
list->pHeader.next = NULL;
list->m_Size = 0;
//3.返回链表地址
return list;
}
2.3 链表的插入
- 将用户传入的被封装的数据类型恢复;
- 判断插入的未知是否合法,若不合法则进行尾插;
- 插入新节点
(1)建立新节点;
(2)定义辅助指针来指定插入未知
(3)插入新节点,建立节点之间的联系
(4)更新链表的大小信息
/*插入链表*/
void insert_LinkList(LList p_list, int pos, int p_data)
{
if(NULL == p_list)
{
return;
}
//0.回复数据类型
LinkList* list = p_list;
//1.判断插入位置是否合法
if (pos < 0 || pos >= list->m_Size)
{ //1.1不合法则进行尾插
pos = list->m_Size;
}
//2.插入新节点
//2.1建立新节点
LinkNode* newNode = malloc(sizeof(LinkNode));
newNode->data = p_data;
newNode->next = NULL;
//2.2定义辅助指针确定插入位置
LinkNode* pCurrent = &list->pHeader;
for (int i = 0; i < pos; i++)
{
pCurrent = pCurrent->next;
}
//2.3插入新节点
newNode->next = pCurrent->next;
pCurrent->next = newNode;
//3.更新链表信息
list->m_Size++;
}
2.4 链表的遍历
- 将封装的数据结构解封;
- 定义辅助指针指向当前节点
- 通过循环遍历链表的结点
(1)由于用户的数据类型无法确定,因此通过回调函数对遍历的数据传输到用户手中。
/*遍历链表*/
void foreach_LinkList(LList p_list, void(*Cmp)(void*))
{
if (NULL == p_list)
{
return;
}
//0.恢复数据结构
LinkList* list = p_list;
//1.定义辅助指针指向当前节点
LinkNode* pCurrent = list->pHeader.next;
//2.遍历链表
for (int i = 0; i < list->m_Size; i++)
{
Cmp(pCurrent->data);
pCurrent = pCurrent->next;
}
}
2.5 节点的删除
2.5.1 按位置删除
- 判断删除位置是否合法,不合法直接返回;
- 定义辅助指针,确定删除的位置;
- 删除节点
(1)定义辅助指针记录要删除节点的指针域;
(2)释放要删除节点的内存空间;
(3)将上一节点指针域指向下一结点
4.更新链表信息;
/*删除节点*/
void deleteByPos_LinkList(LList p_list, int pos)
{
if (NULL == p_list)
{
return;
}
LinkList* list = p_list;
//1.判断删除位置是否合法
if (pos < 0 || pos > list->m_Size - 1)
{
return;
}
//2.定义辅助指针,确定删除的位置
LinkNode* pCurrent = &list->pHeader;
for (int i = 0; i < pos; i++)
{
pCurrent = pCurrent->next;
}
//3. 删除结点
//3.1定义辅助指针记录要删除节点的信息
LinkNode* pDel = pCurrent->next;
pCurrent->next = pDel->next;
//3.2释放要删除的节点
free(pDel);
pDel = NULL;
//4.更新链表信息
list->m_Size--;
}
2.5.2 按位置删除
- 定义辅助指针指向上一节点和当前节点
- 遍历节点查找要删除的节点;
(1)根据回调函数,让用户实现如何确定判断要删除节点的条件;
(2)若当前节点并非要删除的节点,继续遍历;
(3)若当前节点使要删除的节点,删除节点停止遍历; - 删除节点
(1)将上一节点指针域指向下一节点;
(2)释放当前节点的指针域;
(3)停止遍历;
(4)更新链表信息;
void deleteByVal_LinkList(LList p_list, void* p_data, int (*Comp)(void*, void*))
{
if (NULL == p_list)
{
return;
}
LinkList* list = p_list;
//1.定义辅助指针指向当前和上一节点
LinkNode* pPrev = &list->pHeader;
LinkNode* pCurrent = pPrev->next;
//2.循环遍历查找要删除的点
for (int i = 0; i < list->m_Size; i++)
{
//2.1若当前节点为要删除的点
if (Comp(pCurrent->data, p_data))
{
//2.2释放当前内存,更新链表信息,并停止查找
pPrev->next = pCurrent->next;
free(pCurrent);
pCurrent = NULL;
list->m_Size--;
break;
}
//2.2若当前节点不是要删除的点
//继续遍历
pPrev = pCurrent;
pCurrent = pCurrent->next;
}
}
2.6 清空链表
- 定义辅助指针指向当前节点;
- 遍历链表逐个释放节点
(1)定义辅助指针指向当前节点的指针域;
(2)释放当前节点内存;
(3)继续遍历下一个节点; - 将头节点的指针域指向空
- 更新链表信息大小;
/*清空链表*/
void clear_LinkList(LList p_list)
{
if (NULL == p_list)
{
return;
}
LinkList* list = p_list;
//1.定义辅助指针
LinkNode* pCurrent = list->pHeader.next;
//2.遍历逐个释放节点
for (int i = 0; i < list->m_Size; i++)
{
//2.1定义复制指针记录当前节点的指针域
LinkNode* pClr = pCurrent->next;
//2.2释放当前节点内存
free(pCurrent);
//2.4继续释放下一节点
pCurrent = pClr;
}
list->pHeader.next = NULL;
list->m_Size = 0;
}
2.7 销毁链表
- 清空链表;
- 释放链表结构体
*销毁链表*/
void destory_LinkList(LList p_list)
{
if (NULL == p_list)
{
return;
}
//1.清空链表;
clear_LinkList(p_list);
//2.释放链表结构体
free(p_list);
p_list = NULL;
}
2.8 链表的反转
- 定义三个辅助指针指向上一节点、当前节点、下一节点;
- 遍历节点,将上一节点的地址赋给当前节点的指针域;
- 更新头结点。
void reverse_LinkList(LinkNode* pHeader)
{
if (NULL == pHeader)
{
return;
}
//1.定义辅助指针
LinkNode* pPrev = NULL;
LinkNode* pCurrent = pHeader->next;
LinkNode* pNext = NULL;
while(NULL != pCurrent)
{
//2.1pNext指向下一节点;
pNext = pCurrent->next;
//2.2当前节点指针域指向上一个节点
pCurrent->next = pPrev;
//移动辅助指针
pPrev = pCurrent;
pCurrent = pNext
}
}