1.基本概念:
- 顺序表:顺序存储的线性表。
- 链式表:链式存储的线性表,简称链表。
既然顺序存储中的数据因为挤在一起而导致需要成片移动,那很容易想到的解决方案是将数据离散地存储在不同内存块中,然后在用来指针将它们串起来。这种朴素的思路所形成的链式线性表,就是所谓的链表。
顺序表和链表在内存在的基本样态如下图所示:
2单向不循环链表:
单向不循环链表概念:
链表节点设计:
// 设计单向不循环链表的数据域
typedef struct data
{
int num;
char name[30];
} data_t;
// 设计链表的节点,此处标签不可省略,因为要设计节点的指针域
typedef struct node
{
data_t data; // 链表的数据域
struct node *next; // 链表的指针域
} node_t, *P_node_t;
链表节点初始化:
/// @brief 初始化链表节点
/// @param data 链表节点数据
/// @return 初始化完成的链表节点
P_node_t nodeInit(data_t *data)
{
// 申请一个新节点的内存空间
P_node_t new = (P_node_t)calloc(1, sizeof(node_t));
// 判断是否申请成功
if (new == NULL)
{
perror("CALLOC ERROR");
return NULL;
}
// 函数接口设计的巧妙之处:设计一个data_t *data接口
// 使初始化函数可以初始化头节点和节点,初始化头节点时
// 传入NULL,判断传入的data是否为空来分开初始化头节点和节点
if (data != NULL)
{
memcpy(&new->data, data, sizeof(data_t));
}
// 单向不循环,链表指针域初始化指向NULL
new->next = NULL;
return new;
}
链表节点头插:
/// @brief 将新节点头插到链表中
/// @param head 链表头节点
/// @param newNode 新节点
/// @return 头插完成的新链表
P_node_t addListHead(P_node_t head, P_node_t newNode)
{
if (head == NULL || newNode == NULL)
{
printf("链表头节点或者新节点为空.\n");
return head;
}
// 头插:需要考虑到链表中有无有效节点的情况
// 新节点的next指针先指向链表头节点之后的链表,防止链表头后面的节点丢失
newNode->next = head->next;
// 接着链表头节点的next指针指向新节点,头插完成
head->next = newNode;
return head;
}
链表节点尾插:
/// @brief 将新节点尾插到链表中
/// @param head 链表头节点
/// @param newNode 新节点
/// @return 头插完成的新链表
P_node_t addListTail(P_node_t head, P_node_t newNode)
{
if (head == NULL || newNode == NULL)
{
printf("链表头节点或者新节点为空.\n");
return head;
}
P_node_t tmp = NULL;
// 尾插:需要考虑到链表中有无有效节点的情况
// 通过循环使tmp指针遍历到链表尾
for (tmp = head; tmp->next != NULL; tmp = tmp->next)
;
// 新节点指向链表末尾
newNode->next = tmp->next;
// 链表末尾指向新节点
tmp->next = newNode;
// 返回链表头指针更新链表
return head;
}
链表节点有序插入:
/// @brief 将新节点有序插入链表
/// @param head 链表头节点
/// @param newNode 新节点
/// @return 插入新节点成功之后的新链表
P_node_t addListOrder(P_node_t head, P_node_t newNode)
{
if (head == NULL || newNode == NULL)
{
printf("链表头节点或者新节点为空.\n");
return head;
}
P_node_t tmp = NULL;
// 有序插入:需要考虑到链表中是否有有效节点的情况
for (tmp = head; tmp->next != NULL && tmp->next->data.num < newNode->data.num; tmp = tmp->next)
;
// 新节点先指向插入位置之后的链表,防止链表丢失
newNode->next = tmp->next;
// 接着插入位置前的链表指向新节点
tmp->next = newNode;
// 返回链表头指针更新链表
return head;
}
链表遍历:
/// @brief 遍历打印链表
/// @param head 链表头节点
void display(P_node_t head)
{
if (head->next==NULL)
{
printf("链表为空.\n");
return;
}
//for循环遍历链表,打印数据是从有效节点开始,所以tmp初值为head->next
for (P_node_t tmp = head->next; tmp != NULL; tmp = tmp->next)
{
printf("[%d]:%s\n", tmp->data.num, tmp->data.name);
}
return;
}
链表查找节点:
/// @brief 遍历查找链表找到匹配数据的节点并返回前一个节点地址
/// @param head 链表
/// @param findData 需要查找的数据
/// @return 查找成功返回匹配数据的节点的前一个节点的地址,失败返回NULL
P_node_t findPrevNode(P_node_t head,int findData)
{
P_node_t tmp = NULL;
//for遍历链表找到匹配数据的节点并返回前一个节点地址
for ( tmp = head;tmp->next!=NULL;tmp=tmp->next)
{
if (tmp->next->data.num == findData)
{
//设计成返回上一个节点地址是为了函数耦合性,不止可以用于查找数据,
//也可以用于删除修改数据这些。
return tmp;
}
}
return NULL;
}
链表修改节点数据:
/// @brief 修改指定数据节点的数据
/// @param head 链表
/// @return 修改完成后的链表
P_node_t reviseListNode(P_node_t head)
{
int reviseData;
P_node_t reviseNode = NULL;
printf("请输入要修改的数据.\n");
scanf("%d",&reviseData);
reviseNode = findNode(head,reviseData);
if (reviseNode !=NULL)
{
printf("请输入编号名字修改.\n");
scanf("%d%s",&reviseNode->next->data.num,reviseNode->next->data.name);
printf("修改成功.\n");
}else
{
printf("未查找到要修改的数据.\n");
}
return head;
}
链表剔除节点:
/// @brief 从链表中剔除要删除数据的节点
/// @param del 待剔除数据的节点的前驱节点
/// @return 被剔除的节点
P_node_t delNode(P_node_t head ,int delDataNum)
{
P_node_t delPrev = findPrevNode(head,delDataNum);
if (!delPrev)
{
return NULL;
}
P_node_t del = delPrev->next;
delPrev->next = del->next;
del->next = NULL;
return del;
}
链表节点选择排序:
/// @brief 选择排序单向不循环链表,使链表节点按节点数据从小到大排序
/// @param head 链表
/// @return 更新排序完成后的链表
P_node_t optionSort(P_node_t head)
{
/**
* 外循环控制选择排序的指针pos遍历链表每一个元素,第一次pos = head->next是从第一个有效节点开始的
* 之后pos=min->next是从排序完成后的元素的下一个元素开始的,因为如果正常的更新pos=pos->next那样子的话
* pos在min更新到pos的位置之后,如果pos=pos->next会跳过pos本来指向的节点,本来指向的节点就没有参加之后
* 的排序,而在正常选择排序中都是pos都是指向下标,下标不会变也不会移动,移动的是数据,而在链表这里,pos会跟随
* 着min在链表中的移动而改变位置,所以pos更新为排序完成的元素的之后,就不会跳过元素,不信你画图看看。
* 因为排序要考虑链表无有效节点的情况,所以循环判断条件为:pos!=NULL,接着min=pos,选择pos节点与后面的节点进行
* 大小比较,所以内循环初始条件为high=pos->next,从选择的pos节点之后开始遍历节点直到遍历完链表,如果遍历的节点
* 数据比min指向的节点的数据小,则更新min指向数据更小的节点,最后循环结束,min指向了最小数据的节点,将min节点
* 换到pos节点的位置,这样就排序好一个节点,下次排序则从min-next节点开始,直到遍历排序完所有的节点,选择排序完成。
*/
P_node_t min,pos,high,minPrev,posPrev;
//pos = head->next排序从有效节点开始;
// pos!=NULL考虑到链表中有无数据的情况并且遍历到最后一个节点结束;
// pos = min->next,pos更新为排序完成后的接着的节点
for (pos = head->next;pos!=NULL;pos = min->next)
{
//选择pos节点(即排序完的节点接着的未排序完的节点)作为排序节点
min = pos;
//high = pos->next;选择比较节点数据从选择的节点之后的节点开始
// high!=NULL;考虑到pos接着有无节点的情况,并且遍历到链表最后一个节点结束
// high = high->next遍历链表元素
for(high = pos->next;high!=NULL;high = high->next)
{
//判断数据大小
if (high->data.num <min->data.num)
{
//更新min指向数据更小的节点
min = high;
}
}
//跳出循环,此时min指向数据最小的节点
//考虑到一开始pos选择的节点是此次选择排序的最小数据的节点,pos原地不动不需要移动位置,
//同时也是加快代码效率,避免了节点重复卸下插入
if (min == pos)
{
continue;
}
//思路:此时把min节点移动到pos的位置
//移动min节点前需要先把min从链表卸下来,不然会影响到min之后的节点
//卸下min节点需要min节点的前驱节点
for(minPrev=head;minPrev->next!=min;minPrev=minPrev->next);
//min移动到pos位置,使用头插思路:将min节点头插到pos的前驱节点之后
for(posPrev=head;posPrev->next!=pos;posPrev=posPrev->next);
//min头插到pos的前驱节点之后
addListHead(posPrev,delNode(minPrev));
}
//返回更新排序完成后的链表
return head;
}
反转链表:
/// @brief 反转链表
/// @param head 链表
/// @return 返回反转后的链表头节点
P_node_t reverseList(P_node_t head)
{
// 链表无数据无需反转
if (head->next == NULL)
{
printf("链表中无数据,无需反转.\n");
return head;
}
// 链表只有一个数据无需反转
if (head->next->next == NULL)
{
printf("链表中只有一个数据,无需翻转.\n");
return head;
}
// pos和posNext来反转链表,tmp来遍历链表同时保存未反转的链表防止丢失
P_node_t pos, posNext, tmp;
// 反转从第一个元素开始
pos = head->next;
// posNext指向pos之后的节点
posNext = pos->next;
// tmp遍历未反转的链表节点
tmp = posNext->next;
// pos是反转前的第一个节点同时也是反转后的最后一个节点
// 因为是单向不循环链表,所以将反转后的最后一个节点指向NULL
pos->next = NULL;
// tmp=tmp->next遍历完链表的每个节点
for (;; tmp = tmp->next)
{
// posNext->next = pos;反转posNext指向pos
posNext->next = pos;
// 因为tmp==NULL时还要进行一次反转才能结束,所以tmp!=NULL的结束条件没有放在for循环中
if (tmp == NULL)
{
break;
}
// 更新pos和posNext指向下一个要反转的节点
pos = posNext;
posNext = tmp;
}
// 跳出循环,tmp==NULL遍历完链表每个节点
// posNext此时是反转前的最后一个节点同时也是反转后的第一个有效节点
// 头节点指向第一个有效节点
head->next = posNext;
// 返回反转后的链表头指针
return head;
}
链表销毁:
/// @brief 递归实现销毁单向不循环链表
/// @param head 链表节点
void destoryList(P_node_t head)
{
if (head->next == NULL)
{
free(head);
return ;
}
destoryList(head->next);
free(head);
return;
}
判断堆内存空间是否全部释放:
ubuntu下运行代码加上运行命令valgrind ./a.out ,如命令行提示未知命令,则需要先安装valgrind
单向不循环链表代码示例:
/**
* 单向不循环链表
*/
#include <stdio.h>
#include <stdbool.h>
#include <stdlib.h>
#include <string.h>
// 设计单向不循环链表的数据域
typedef struct data
{
int num;
char name[30];
} data_t;
// 设计链表的节点,此处标签不可省略,因为要设计节点的指针域
typedef struct node
{
data_t data; // 链表的数据域
struct node *next; // 链表的指针域
} node_t, *P_node_t;
/// @brief 初始化链表节点
/// @param data 链表节点数据
/// @return 初始化完成的链表节点
P_node_t nodeInit(data_t *data)
{
// 申请一个新节点的内存空间
P_node_t new = (P_node_t)calloc(1, sizeof(node_t));
// 判断是否申请成功
if (new == NULL)
{
perror("CALLOC ERROR");
return NULL;
}
// 函数接口设计的巧妙之处:设计一个data_t *data接口
// 使初始化函数可以初始化头节点和节点,初始化头节点时
// 传入NULL,判断传入的data是否为空来分开初始化头节点和节点
if (data != NULL)
{
memcpy(&new->data, data, sizeof(data_t));
}
// 单向不循环,链表指针域初始化指向NULL
new->next = NULL;
return new;
}
/// @brief 获得新数据
/// @return 新数据
data_t getData()
{
data_t newData = {0};
printf("请输入编号名字.\n");
while(scanf("%d%s",&newData.num,newData.name)!=2)
{
printf("输入错误,请重新输入.\n");
while(getchar()!='\n');
continue;
}
return newData;
}
/// @brief 将新节点头插到链表中
/// @param head 链表头节点
/// @param newNode 新节点
/// @return 头插完成的新链表
P_node_t addListHead(P_node_t head, P_node_t newNode)
{
if (head == NULL || newNode == NULL)
{
printf("链表头节点或者新节点为空.\n");
return head;
}
// 头插:需要考虑到链表中有无有效节点的情况
// 新节点的next指针先指向链表头节点之后的链表,防止链表头后面的节点丢失
newNode->next = head->next;
// 接着链表头节点的next指针指向新节点,头插完成
head->next = newNode;
return head;
}
/// @brief 将新节点尾插到链表中
/// @param head 链表头节点
/// @param newNode 新节点
/// @return 头插完成的新链表
P_node_t addListTail(P_node_t head, P_node_t newNode)
{
if (head == NULL || newNode == NULL)
{
printf("链表头节点或者新节点为空.\n");
return head;
}
P_node_t tmp = NULL;
// 尾插:需要考虑到链表中有无有效节点的情况
// 通过循环使tmp指针遍历到链表尾
for (tmp = head; tmp->next != NULL; tmp = tmp->next)
;
// 新节点指向链表末尾
newNode->next = tmp->next;
// 链表末尾指向新节点
tmp->next = newNode;
// 返回链表头指针更新链表
return head;
}
/// @brief 将新节点有序插入链表
/// @param head 链表头节点
/// @param newNode 新节点
/// @return 插入新节点成功之后的新链表
P_node_t addListOrder(P_node_t head, P_node_t newNode)
{
if (head == NULL || newNode == NULL)
{
printf("链表头节点或者新节点为空.\n");
return head;
}
P_node_t tmp = NULL;
// 有序插入:需要考虑到链表中是否有有效节点的情况
for (tmp = head; tmp->next != NULL && tmp->next->data.num < newNode->data.num; tmp = tmp->next)
;
// 新节点先指向插入位置之后的链表,防止链表丢失
newNode->next = tmp->next;
// 接着插入位置前的链表指向新节点
tmp->next = newNode;
// 返回链表头指针更新链表
return head;
}
/// @brief 遍历打印链表
/// @param head 链表头节点
void display(P_node_t head)
{
if (head->next == NULL)
{
printf("链表为空.\n");
return;
}
// for循环遍历链表,打印数据是从有效节点开始,所以tmp初值为head->next
for (P_node_t tmp = head->next; tmp != NULL; tmp = tmp->next)
{
printf("[%d]:%s\n", tmp->data.num, tmp->data.name);
}
return;
}
/// @brief 遍历查找链表找到匹配数据的节点并返回前一个节点地址
/// @param head 链表
/// @param findData 需要查找的数据
/// @return 查找成功返回匹配数据的节点的前一个节点的地址,失败返回NULL
P_node_t findPrevNode(P_node_t head, int findData)
{
P_node_t tmp = NULL;
// for遍历链表找到匹配数据的节点并返回前一个节点地址
for (tmp = head; tmp->next != NULL; tmp = tmp->next)
{
if (tmp->next->data.num == findData)
{
// 设计成返回上一个节点地址是为了函数耦合性,不止可以用于查找数据,
// 也可以用于删除修改数据这些。
return tmp;
}
}
return NULL;
}
/// @brief 修改指定数据节点的数据
/// @param head 链表
/// @return 修改完成后的链表
P_node_t reviseListNode(P_node_t head)
{
int reviseData;
P_node_t reviseNode = NULL;
printf("请输入要修改的数据.\n");
scanf("%d", &reviseData);
reviseNode = findPrevNode(head, reviseData);
if (reviseNode != NULL)
{
printf("请输入编号名字修改.\n");
scanf("%d%s", &reviseNode->next->data.num, reviseNode->next->data.name);
printf("修改成功.\n");
}
else
{
printf("未查找到要修改的数据.\n");
}
return head;
}
/// @brief 从链表中剔除要删除数据的节点
/// @param del 待剔除数据的节点的前驱节点
/// @return 被剔除的节点
P_node_t delNode(P_node_t head ,int delDataNum)
{
P_node_t delPrev = findPrevNode(head,delDataNum);
if (!delPrev)
{
return NULL;
}
P_node_t del = delPrev->next;
delPrev->next = del->next;
del->next = NULL;
return del;
}
/// @brief 选择排序单向不循环链表,使链表节点按节点数据从小到大排序
/// @param head 链表
/// @return 更新排序完成后的链表
P_node_t optionSort(P_node_t head)
{
/**
* 外循环控制选择排序的指针pos遍历链表每一个元素,第一次pos = head->next是从第一个有效节点开始的
* 之后pos=min->next是从排序完成后的元素的下一个元素开始的,因为如果正常的更新pos=pos->next那样子的话
* pos在min更新到pos的位置之后,如果pos=pos->next会跳过pos本来指向的节点,本来指向的节点就没有参加之后
* 的排序,而在正常选择排序中都是pos都是指向下标,下标不会变也不会移动,移动的是数据,而在链表这里,pos会跟随
* 着min在链表中的移动而改变位置,所以pos更新为排序完成的元素的之后,就不会跳过元素,不信你画图看看。
* 因为排序要考虑链表无有效节点的情况,所以循环判断条件为:pos!=NULL,接着min=pos,选择pos节点与后面的节点进行
* 大小比较,所以内循环初始条件为high=pos->next,从选择的pos节点之后开始遍历节点直到遍历完链表,如果遍历的节点
* 数据比min指向的节点的数据小,则更新min指向数据更小的节点,最后循环结束,min指向了最小数据的节点,将min节点
* 换到pos节点的位置,这样就排序好一个节点,下次排序则从min-next节点开始,直到遍历排序完所有的节点,选择排序完成。
*/
P_node_t min, pos, high, minPrev, posPrev;
// pos = head->next排序从有效节点开始;
// pos!=NULL考虑到链表中有无数据的情况并且遍历到最后一个节点结束;
// pos = min->next,pos更新为排序完成后的接着的节点
for (pos = head->next; pos != NULL; pos = min->next)
{
// 选择pos节点(即排序完的节点接着的未排序完的节点)作为排序节点
min = pos;
// high = pos->next;选择比较节点数据从选择的节点之后的节点开始
// high!=NULL;考虑到pos接着有无节点的情况,并且遍历到链表最后一个节点结束
// high = high->next遍历链表元素
for (high = pos->next; high != NULL; high = high->next)
{
// 判断数据大小
if (high->data.num < min->data.num)
{
// 更新min指向数据更小的节点
min = high;
}
}
// 跳出循环,此时min指向数据最小的节点
// 考虑到一开始pos选择的节点是此次选择排序的最小数据的节点,pos原地不动不需要移动位置,
// 同时也是加快代码效率,避免了节点重复卸下插入
if (min == pos)
{
continue;
}
// 思路:此时把min节点移动到pos的位置
// 移动min节点前需要先把min从链表卸下来,不然会影响到min之后的节点
// 卸下min节点需要min节点的前驱节点
for (minPrev = head; minPrev->next != min; minPrev = minPrev->next)
;
// min移动到pos位置,使用头插思路:将min节点头插到pos的前驱节点之后
for (posPrev = head; posPrev->next != pos; posPrev = posPrev->next)
;
//卸下min节点
minPrev->next = min->next ;
min->next = NULL;
// min头插到pos的前驱节点之后
addListHead(posPrev, min);
}
// 返回更新排序完成后的链表
return head;
}
/// @brief 递归实现销毁单向不循环链表
/// @param head 链表节点
void destoryList(P_node_t head)
{
if (head->next == NULL)
{
free(head);
return;
}
destoryList(head->next);
free(head);
return;
}
/// @brief 反转链表
/// @param head 链表
/// @return 返回反转后的链表头节点
P_node_t reverseList(P_node_t head)
{
// 链表无数据无需反转
if (head->next == NULL)
{
printf("链表中无数据,无需反转.\n");
return head;
}
// 链表只有一个数据无需反转
if (head->next->next == NULL)
{
printf("链表中只有一个数据,无需翻转.\n");
return head;
}
// pos和posNext来反转链表,tmp来遍历链表同时保存未反转的链表防止丢失
P_node_t pos, posNext, tmp;
// 反转从第一个元素开始
pos = head->next;
// posNext指向pos之后的节点
posNext = pos->next;
// tmp遍历未反转的链表节点
tmp = posNext->next;
// pos是反转前的第一个节点同时也是反转后的最后一个节点
// 因为是单向不循环链表,所以将反转后的最后一个节点指向NULL
pos->next = NULL;
// tmp=tmp->next遍历完链表的每个节点
for (;; tmp = tmp->next)
{
// posNext->next = pos;反转posNext指向pos
posNext->next = pos;
// 因为tmp==NULL时还要进行一次反转才能结束,所以tmp!=NULL的结束条件没有放在for循环中
if (tmp == NULL)
{
break;
}
// 更新pos和posNext指向下一个要反转的节点
pos = posNext;
posNext = tmp;
}
// 跳出循环,tmp==NULL遍历完链表每个节点
// posNext此时是反转前的最后一个节点同时也是反转后的第一个有效节点
// 头节点指向第一个有效节点
head->next = posNext;
// 返回反转后的链表头指针
return head;
}
int main(int argc, char const *argv[])
{
// 初始化链表头节点
// 定义一个头指针指向链表头节点
P_node_t head = nodeInit(NULL);
int n, optSort, result, findData, delData, overFlag = 0;
char menuOpt;
data_t newData;
P_node_t newNode = NULL, findResult = NULL, delResult = NULL;
while (1)
{
printf("请选择要进行的操作.\n");
printf("i.添加数据\ts.排序链表\td.显示链表\tf.查找数据\tg.修改数据\tx.删除数据\tr.翻转链表\tt.退出\n");
scanf("%c", &menuOpt);
switch (menuOpt)
{
case 'i':
printf("请输入要输入多少个数据.\n");
scanf("%d", &n);
printf("输入1.将数据头插到链表中\t2.将数据尾插到链表中\t3.将数据有序插入到链表中\n");
scanf("%d", &optSort);
for (int i = 0; i < n; i++)
{
// 获取数据
newData = getData();
// 初始化节点
newNode = nodeInit(&newData);
if (optSort == 1)
{
// 将节点头插添加到链表中
head = addListHead(head, newNode);
}
else if (optSort == 2)
{
// 将节点尾插插入到链表中
head = addListTail(head, newNode);
}
else if (optSort == 3)
{
// 将节点有序插入到链表中
head = addListOrder(head, newNode);
}
}
break;
case 's':
head = optionSort(head);
break;
case 'g':
head = reviseListNode(head);
break;
case 'x':
printf("请输入要删除的数据.\n");
scanf("%d", &delData);
delResult = delNode(head,delData);
if (delResult != NULL)
{
printf("删除成功.\n");
free(delResult);
}
else
{
printf("未找到要删除的数据.\n");
}
break;
case 'f':
printf("请输入要查找的数据.\n");
scanf("%d", &findData);
findResult = findPrevNode(head, findData);
if (findResult != NULL)
{
printf("查找成功.\n");
printf("查找的数据的节点地址为[%p]:%d:%s\n", findResult->next, findResult->next->data.num, findResult->data.name);
}
else
{
printf("未查找到数据.\n");
}
break;
case 'd':
display(head);
break;
case 'r':
head = reverseList(head);
printf("反转成功,反转后的链表为:\n");
display(head);
break;
case 't':
overFlag = 1;
break;
default:
printf("输入错误,请重新输入.\n");
break;
}
while (getchar() != '\n')
; // 清空缓冲区以防止影响下一次的%c输入
if (overFlag == 1)
{
break;
}
}
destoryList(head);
head = NULL; //避免野指针
return 0;
}
3无头单向不循环链表:
头节点的存在可以确保头指针head所指向的堆内存永远稳定不变。而无头节点的情况下没有该特性因此非常容易把链表的入口节点搞丢。
链表节点设计:
// 设计无头单向不循环链表节点数据域
typedef struct data
{
int num;
char name[30];
} data_t;
// 设计无头单向不循环链表节点
typedef struct node
{
data_t data; // 数据域
struct node *next; // 指针域
} node_t, *P_node_t;
判断链表是否为空:
/// @brief 判断无头单向不循环链表是否为空
/// @param head 链表头指针
/// @return 返回链表是否为空
bool isListEmpty(P_node_t head)
{
// 链表没有节点,head指向NULL
return (head == NULL);
}
链表节点初始化:
/// @brief 传入数据对链表节点指针域和数据域初始化
/// @param newData 链表节点数据域数据
/// @return 初始化完成的节点
P_node_t nodeInit(data_t newData)
{
// 在堆内存中申请新节点内存空间
P_node_t newNode = (P_node_t)calloc(1, sizeof(node_t));
// 判断新节点内存空间是否申请成功
if (newNode == NULL)
{
perror("CALLOC ERROR");
return NULL;
}
// 链表新节点指针域指向NULL
newNode->next = NULL;
// 初始化链表节点数据域
memcpy(&newNode->data, &newData, sizeof(data_t));
// 返回初始化完成的链表
return newNode;
}
链表节点头插:
/// @brief 将节点头插到链表
/// @param head 链表头指针
/// @param newNode 新节点
/// @return 返回头插完成的链表头指针
P_node_t addListHead(P_node_t head, P_node_t newNode)
{
// 头插要考虑到链表中是否有节点的情况
// 新节点先指向链表头指针指向的,防止后面的节点丢失
newNode->next = head;
// 链表头指针指向新节点
head = newNode;
// 返回更新完成的链表头指针
return head;
}
链表节点尾插:
/// @brief 将新节点尾插到链表中
/// @param head 链表头指针
/// @param newNode 新节点
/// @return 返回尾插完成的新链表头指针
P_node_t addListTail(P_node_t head, P_node_t newNode)
{
// 尾插要考虑到链表中是否有节点的情况
P_node_t tmp;
// 无头链表head指向第一个节点,从第一个节点开始遍历到最后一个节点
// 链表中可能没有节点,tmp->next就会段错误,设置tmp!=NULL当链表为空时跳出循环
// tmp->next!=NULL遍历到最后一个节点
for (tmp = head; tmp != NULL && tmp->next != NULL; tmp = tmp->next)
;
// 此时跳出循环有两种可能,一是链表为空,head即tmp指向NULL
// 判断是否是tmp指向NULL链表为空的情况
if (tmp == NULL)
{ // 注意:因为tmp和head都是指针,并且都指向NULL
// 所以像平常一样改变tmp的指向不能改变head的指向
// 所以这里直接改变head的指向而不是tmp
// 同样主函数的head和这里的head都是各自的指向,主函数head指向NULL
// 所以返回这里的head指向来改变主函数head的指向
head = newNode;
return head;
}
// 二是tmp遍历到最后一个节点
// newNode->next指向tmp的next指向的,其实就是指向NULL
newNode->next = tmp->next;
// tmp的next指向新节点
tmp->next = newNode;
// 返回尾插完成的链表头指针
// 因为是无头链表,head指向NULL,所以这里面的同名形参head指针
// 改变指向,无法改变主函数head的指向,所以要返回这里的head指向
// 来更新主函数head的指向
return head;
}
链表节点有序插入:
/// @brief 链表节点有序插入
/// @param head 链表头指针
/// @param newNode 新节点
/// @return 返回链表头指针更新有序插入后的链表
P_node_t addListOrder(P_node_t head, P_node_t newNode)
{
//tmp遍历链表节点,tmpPrev记录tmp的前一个节点位置即插入的位置
P_node_t tmp = NULL, tmpPrev = NULL;
//无头链表head无头节点,head指向第一个有效节点,所以tmp从head开始遍历
//
for (tmp = head; tmp != NULL && tmp->data.num < newNode->data.num; tmp = tmp->next)
{
tmpPrev = tmp;
}
//此时跳出循环有两种情况:
//一是:tmp==NULL
//1.链表无节点,head即tmp==NULL跳出循环
//2.链表节点的数据都比新节点小,tmp一直往后遍历直到tmp==NULL跳出循环
//二是:tmp遍历到节点数据比newNode数据大的节点跳出循环
//判断tmpPrev是否等于NULL,判断tmpPrev即tmp即head即链表是否为空
if (tmpPrev == NULL)
{
//新节点的next指向head指向的链表,防止链表丢失
newNode->next = head;
//头指针head指向新节点,新节点插入链表
head = newNode;
//返回head头指针更新链表
return head;
}
//tmpPrev不为空时,tmp寻找到合适的位置插入
else
{
//新节点的next指向插入位置之后的链表,防止链表丢失
newNode->next = tmp;
//插入位置的前驱节点的next指针指向新节点,新节点插入链表
tmpPrev->next = newNode;
}
//返回head指针更新链表
return head;
}
链表遍历:
/// @brief 遍历打印链表节点数据域
/// @param head 链表头指针
void displayList(P_node_t head)
{
if (isListEmpty(head))
{
printf("链表为空.\n");
return;
}
// 无头指针head指向第一个节点,所以tmp遍历从head开始
// tmp!=NULL遍历完链表每个节点
for (P_node_t tmp = head; tmp != NULL; tmp = tmp->next)
{
printf("[%d]:%s\n", tmp->data.num, tmp->data.name);
}
printf("\n");
return;
}
链表查找节点:
/// @brief 查找链表中节点的数据并返回数据匹配的节点的地址
/// @param head 链表头指针
/// @param findData 要查找的数据
/// @return 查找成功返回数据匹配的节点地址,失败返回NULL
P_node_t findListNode(P_node_t head,int findData)
{
//判断链表是否为空
if (isListEmpty(head))
{
printf("链表为空.\n");
return NULL;
}
P_node_t findNode = NULL,tmp = NULL;
//tmp遍历链表节点
for (tmp = head;tmp!=NULL;tmp=tmp->next)
{
if (tmp->data.num == findData)
{
findNode = tmp;
break;
}
}
//查找成功时findNode为数据匹配的节点地址,失败时为NULL
return findNode;
}
链表修改节点数据:
/// @brief 修改链表节点数据
/// @param head 链表头指针
/// @param reviseData 要修改的数据
/// @return 修改成功返回真,失败返回假
bool reviseListNode(P_node_t head,int reviseData)
{
P_node_t findNode = findListNode(head,reviseData);
if (findNode==NULL)
{
printf("未查找到要修改数据的节点.\n");
return false;
}else
{
printf("请输入要修改的名字编号.\n");
scanf("%d%s",&findNode->data.num,findNode->data.name);
return true;
}
}
链表删除节点:
/// @brief 删除链表中的节点并返回head更新链表头指针
/// @param head 链表头指针
/// @param delData 要删除节点的数据
/// @return 返回删除节点后的head更新链表头指针
P_node_t delListNode(P_node_t head,int delData)
{
//查找要删除数据的节点
P_node_t delNode = findListNode(head,delData),delNodePrev =NULL;
//判读是否有要删除数据的节点
if (!delNode)
{
printf("链表中未找到要删除的数据,删除失败.\n");
return head;
}
//要考虑到删除节点是头节点没有前驱节点的情况:
//并且这里改变head的指向还要返回head来更新主函数head的指向
if (delNode==head)
{
//改变头指针指向要删除头节点之后的链表
head = delNode->next;
//要删除节点指向NULL
delNode->next = NULL;
//返回head更新主函数头指针head的指向
return head;
}
//tmp遍历链表找到要删除节点的前驱节点
for(P_node_t tmp = head;tmp->next!=NULL;tmp= tmp->next)
{
if (tmp->next==delNode)
{
delNodePrev = tmp;
break;
}
}
//要删除节点的前驱节点的next指针指向要删除节点之后的链表
delNodePrev->next = delNode->next;
//要删除节点的next指向NULL
delNode->next = NULL;
//释放删除节点的堆内存空间
free(delNode);
//返回head来更新主函数链表头指针head的指向
return head;
}
链表销毁:
/// @brief 递归销毁链表
/// @param head 二级指针,主函数head的地址
void destoryList(P_node_t* head)
{
//递归结束条件
if (*head == NULL)
{
return;
}
//这里注意(*head)要用括号括起来,因为*优先级比->低
destoryList((*head)->next);
//释放节点
free(*head);
//使指针指向空,避免野指针
*head = NULL;
return;
}
判断堆内存空间是否全部释放:
无头单向不循环链表代码示例:
/**
* 无头单向不循环链表
*/
#include <stdio.h>
#include <stdbool.h>
#include <stdlib.h>
#include <string.h>
// 设计无头单向不循环链表节点数据域
typedef struct data
{
int num;
char name[30];
} data_t;
// 设计无头单向不循环链表节点
typedef struct node
{
data_t data; // 数据域
struct node *next; // 指针域
} node_t, *P_node_t;
/// @brief 获得新数据并返回新数据
/// @return 返回获得的新数据
data_t getData()
{
data_t newData = {0};
printf("请输入编号名字.\n");
while(scanf("%d%s",&newData.num,newData.name)!=2)
{
printf("输入错误,请重新输入.\n");
while(getchar()!='\n');
continue;
}
return newData;
}
/// @brief 判断无头单向不循环链表是否为空
/// @param head 链表头指针
/// @return 返回链表是否为空
bool isListEmpty(P_node_t head)
{
// 链表没有节点,head指向NULL
return (head == NULL);
}
/// @brief 传入数据对链表节点指针域和数据域初始化
/// @param newData 链表节点数据域数据
/// @return 初始化完成的节点
P_node_t nodeInit(data_t newData)
{
// 在堆内存中申请新节点内存空间
P_node_t newNode = (P_node_t)calloc(1, sizeof(node_t));
// 判断新节点内存空间是否申请成功
if (newNode == NULL)
{
perror("CALLOC ERROR");
return NULL;
}
// 链表新节点指针域指向NULL
newNode->next = NULL;
// 初始化链表节点数据域
memcpy(&newNode->data, &newData, sizeof(data_t));
// 返回初始化完成的链表
return newNode;
}
/// @brief 将节点头插到链表
/// @param head 链表头指针
/// @param newNode 新节点
/// @return 返回头插完成的链表头指针
P_node_t addListHead(P_node_t head, P_node_t newNode)
{
// 头插要考虑到链表中是否有节点的情况
// 新节点先指向链表头指针指向的,防止后面的节点丢失
newNode->next = head;
// 链表头指针指向新节点
head = newNode;
// 返回更新完成的链表头指针
return head;
}
/// @brief 将新节点尾插到链表中
/// @param head 链表头指针
/// @param newNode 新节点
/// @return 返回尾插完成的新链表头指针
P_node_t addListTail(P_node_t head, P_node_t newNode)
{
// 尾插要考虑到链表中是否有节点的情况
P_node_t tmp;
// 无头链表head指向第一个节点,从第一个节点开始遍历到最后一个节点
// 链表中可能没有节点,tmp->next就会段错误,设置tmp!=NULL当链表为空时跳出循环
// tmp->next!=NULL遍历到最后一个节点
for (tmp = head; tmp != NULL && tmp->next != NULL; tmp = tmp->next)
;
// 此时跳出循环有两种可能,一是链表为空,head即tmp指向NULL
// 判断是否是tmp指向NULL链表为空的情况
if (tmp == NULL)
{ // 注意:因为tmp和head都是指针,并且都指向NULL
// 所以像平常一样改变tmp的指向不能改变head的指向
// 所以这里直接改变head的指向而不是tmp
// 同样主函数的head和这里的head都是各自的指向,主函数head指向NULL
// 所以返回这里的head指向来改变主函数head的指向
head = newNode;
return head;
}
// 二是tmp遍历到最后一个节点
// newNode->next指向tmp的next指向的,其实就是指向NULL
newNode->next = tmp->next;
// tmp的next指向新节点
tmp->next = newNode;
// 返回尾插完成的链表头指针
// 因为是无头链表,head指向NULL,所以这里面的同名形参head指针
// 改变指向,无法改变主函数head的指向,所以要返回这里的head指向
// 来更新主函数head的指向
return head;
}
/// @brief 链表节点有序插入
/// @param head 链表头指针
/// @param newNode 新节点
/// @return 返回链表头指针更新有序插入后的链表
P_node_t addListOrder(P_node_t head, P_node_t newNode)
{
//tmp遍历链表节点,tmpPrev记录tmp的前一个节点位置即插入的位置
P_node_t tmp = NULL, tmpPrev = NULL;
//无头链表head无头节点,head指向第一个有效节点,所以tmp从head开始遍历
//
for (tmp = head; tmp != NULL && tmp->data.num < newNode->data.num; tmp = tmp->next)
{
tmpPrev = tmp;
}
//此时跳出循环有两种情况:
//一是:tmp==NULL
//二是:tmp遍历到节点数据比newNode数据大的节点跳出循环
//1.链表无节点,head即tmp==NULL跳出循环
//2.链表节点的数据都比新节点小,tmp一直往后遍历直到tmp==NULL跳出循环
//3.链表节点的数据都比新节点大,tmpPrev为NULL
//判断tmpPrev是否等于NULL,判断tmpPrev即tmp即head即链表是否为空
if (tmpPrev == NULL)
{
//新节点的next指向head指向的链表,防止链表丢失
newNode->next = head;
//头指针head指向新节点,新节点插入链表
head = newNode;
//返回head头指针更新链表
return head;
}
//tmpPrev不为空时,tmp寻找到合适的位置插入
else
{
//新节点的next指向插入位置之后的链表,防止链表丢失
newNode->next = tmp;
//插入位置的前驱节点的next指针指向新节点,新节点插入链表
tmpPrev->next = newNode;
}
//返回head指针更新链表
return head;
}
/// @brief 遍历打印链表节点数据域
/// @param head 链表头指针
void displayList(P_node_t head)
{
if (isListEmpty(head))
{
printf("链表为空.\n");
return;
}
// 无头指针head指向第一个节点,所以tmp遍历从head开始
// tmp!=NULL遍历完链表每个节点
for (P_node_t tmp = head; tmp != NULL; tmp = tmp->next)
{
printf("[%d]:%s\n", tmp->data.num, tmp->data.name);
}
printf("\n");
return;
}
/// @brief 查找链表中节点的数据并返回数据匹配的节点的地址
/// @param head 链表头指针
/// @param findData 要查找的数据
/// @return 查找成功返回数据匹配的节点地址,失败返回NULL
P_node_t findListNode(P_node_t head,int findData)
{
//判断链表是否为空
if (isListEmpty(head))
{
printf("链表为空.\n");
return NULL;
}
P_node_t findNode = NULL,tmp = NULL;
//tmp遍历链表节点
for (tmp = head;tmp!=NULL;tmp=tmp->next)
{
if (tmp->data.num == findData)
{
findNode = tmp;
break;
}
}
//查找成功时findNode为数据匹配的节点地址,失败时为NULL
return findNode;
}
/// @brief 修改链表节点数据
/// @param head 链表头指针
/// @param reviseData 要修改的数据
/// @return 修改成功返回真,失败返回假
bool reviseListNode(P_node_t head,int reviseData)
{
P_node_t findNode = findListNode(head,reviseData);
if (findNode==NULL)
{
printf("未查找到要修改数据的节点.\n");
return false;
}else
{
printf("请输入要修改的名字编号.\n");
scanf("%d%s",&findNode->data.num,findNode->data.name);
return true;
}
}
/// @brief 删除链表中的节点并返回head更新链表头指针
/// @param head 链表头指针
/// @param delData 要删除节点的数据
/// @return 返回删除节点后的head更新链表头指针
P_node_t delListNode(P_node_t head,int delData)
{
//查找要删除数据的节点
P_node_t delNode = findListNode(head,delData),delNodePrev =NULL;
//判读是否有要删除数据的节点
if (!delNode)
{
printf("链表中未找到要删除的数据,删除失败.\n");
return head;
}
//要考虑到删除节点是头节点没有前驱节点的情况:
//并且这里改变head的指向还要返回head来更新主函数head的指向
if (delNode==head)
{
//改变头指针指向要删除头节点之后的链表
head = delNode->next;
//要删除节点指向NULL
delNode->next = NULL;
//返回head更新主函数头指针head的指向
return head;
}
//tmp遍历链表找到要删除节点的前驱节点
for(P_node_t tmp = head;tmp->next!=NULL;tmp= tmp->next)
{
if (tmp->next==delNode)
{
delNodePrev = tmp;
break;
}
}
//要删除节点的前驱节点的next指针指向要删除节点之后的链表
delNodePrev->next = delNode->next;
//要删除节点的next指向NULL
delNode->next = NULL;
//释放删除节点的堆内存空间
free(delNode);
//返回head来更新主函数链表头指针head的指向
return head;
}
/// @brief 递归销毁链表
/// @param head 二级指针,主函数head的地址
void destoryList(P_node_t* head)
{
//递归结束条件
if (*head == NULL)
{
return;
}
//这里注意(*head)要用括号括起来,因为*优先级比->低
destoryList((*head)->next);
//释放节点
free(*head);
//使指针指向空,避免野指针
*head = NULL;
return;
}
int main(int argc, char const *argv[])
{
// 无头结点,head初始化指向NULL
P_node_t head = NULL, newNode = NULL;
char menuOpt;
int n, optInsert,findData,reviseData,delData,overFlag=0;
P_node_t findNode,delNode;
data_t newData;
while (1)
{
printf("请选择要进行的操作.\n");
printf("i.添加数据\td.显示链表\tf.查找数据\tg.修改数据\tx.删除数据\tt.退出\n");
scanf("%c", &menuOpt);
switch (menuOpt)
{
case 'i':
printf("请输入要输入几个数.\n");
scanf("%d", &n);
printf("请选择输入:\n1.头插\t2.尾插\t3.有序插入\n");
scanf("%d", &optInsert);
for (int i = 0; i < n; i++)
{
newData = getData();
newNode = nodeInit(newData);
if (optInsert == 1)
{
// 将节点头插添加到链表中
// 头插结束返回形参head的指向来改变链表head的指向
// 不然这里head永远指向NULL
head = addListHead(head, newNode);
}
else if (optInsert == 2)
{
// 将节点尾插添加到链表中
// 尾插结束返回形参head的指向来改变链表head的指向
// 不然这里head永远指向NULL
head = addListTail(head, newNode);
}
else if (optInsert == 3)
{
// 将节点有序插入到顺序表中
head = addListOrder(head, newNode);
}
}
break;
case 'f':
printf("请输入要查找的数据.\n");
scanf("%d",&findData);
findNode = findListNode(head,findData);
if (findNode==NULL)
{
printf("未查到数据.\n");
}else
{
printf("已查找到数据.\n数据在链表的地址为:%p:[%d]:%s\n",findNode,findNode->data.num,findNode->data.name);
}
break;
case 'g':
printf("请输入要修改的数据.\n");
scanf("%d",&reviseData);
if (reviseListNode(head,reviseData))
{
printf("修改成功!\n");
}else
{
printf("修改失败.\n");
}
break;
case 'x':
printf("请输入要删除的数据.\n");
scanf("%d",&delData);
head = delListNode(head,delData);
break;
case 'd':
displayList(head);
break;
case 't':
overFlag =1;
break;
default:
printf("输入错误,请重新输入.\n");
break;
}
if (overFlag==1)
{
break;
}
while (getchar() != '\n') //清空缓冲区防止影响下一次的%c输入
;
}
destoryList(&head);
return 0;
}
4.单向循环链表:
链表节点设计:
// 设计链表节点数据域
typedef struct data
{
int num;
char name[30];
} data_t;
// 设计链表节点
typedef struct node
{
data_t data; // 链表节点数据域
struct node *next; // 链表节点指针域
} node_t, *P_node_t;
链表节点初始化:
/// @brief 初始化单向循环链表节点
/// @param data 新数据指针
/// @return 初始化完成的新节点
P_node_t nodeInit(data_t *data)
{
// 为新节点在堆内存空间中申请内存空间
P_node_t newNode = (P_node_t)calloc(1, sizeof(node_t));
// 判断是否申请成功
if (newNode == NULL)
{
perror("CALLOC ERROR");
return NULL;
}
// 设计的巧妙之处:
// 设计函数参数为:data_t *data
// 传入NULL时:初始化链表头节点
// 传入不为空时:初始化链表有效节点
if (data != NULL)
{ // 初始化新节点的数据域
memcpy(&newNode->data, data, sizeof(data_t));
}
// 初始化新节点的指针域
newNode->next = newNode;
// 返回初始化完成的新节点
return newNode;
}
判断链表是否为空:
/// @brief 判断链表有无有效节点
/// @param head 链表头节点
/// @return 链表无有效节点返回真,有有效节点返回假
bool isListEmpty(P_node_t head)
{
return (head->next == head);
}
链表节点头插:
/// @brief 将新节点头插到链表中
/// @param head 链表头节点
/// @param newNode 新节点
/// @return 返回头插完成的链表头节点
P_node_t addListHead(P_node_t head, P_node_t newNode)
{
// 头插要考虑到链表有无有效节点的情况
// 新节点指向头节点之后的链表
newNode->next = head->next;
// 头节点指向新节点,新节点插入链表
head->next = newNode;
// 返回头插完成的链表头节点
return head;
}
链表节点尾插:
/// @brief 将新节点尾插到链表中
/// @param head 链表头节点
/// @param newNode 新节点
/// @return 返回链表头节点更新链表
P_node_t addListTail(P_node_t head, P_node_t newNode)
{
// 头插要考虑到链表中是否有有效节点的情况
P_node_t tmp;
// tmp遍历链表找到链表尾
for (tmp = head; tmp->next != head; tmp = tmp->next)
;
// 新节点的next指向tmp的next指向,即指向链表尾head
newNode->next = tmp->next;
// tmp的next指针指向新节点,链表插入新节点
tmp->next = newNode;
// 返回链表头节点更新链表
return head;
}
链表节点有序插入:
/// @brief 将节点有序插入到链表中
/// @param head 链表头节点
/// @param newNode 新节点
/// @return 返回有序插入后的链表头节点更新链表
P_node_t addListOrder(P_node_t head, P_node_t newNode)
{
P_node_t tmp = NULL;
// 有序插入要考虑到链表为空的情况,所以tmp从head头节点开始遍历
// tmp遍历到合适的位置插入
for (tmp = head; tmp->next != head && tmp->next->data.num < newNode->data.num; tmp = tmp->next)
;
// 新节点的next指向tmp的next,即插入位置之后的链表,防止后面的链表丢失
newNode->next = tmp->next;
// tmp的next指向新节点,新节点插入到链表中
tmp->next = newNode;
// 返回有序插入后的链表头节点更新链表
return head;
}
链表遍历:
/// @brief 遍历打印链表
/// @param head 链表头节点
void display(P_node_t head)
{
if (isListEmpty(head))
{
printf("链表为空.\n");
return;
}
// tmp遍历链表有效节点
for (P_node_t tmp = head->next; tmp != head; tmp = tmp->next)
{
printf("[%d]:%s\n", tmp->data.num, tmp->data.name);
}
return;
}
链表查找节点:
/// @brief 在链表中寻找匹配数据的节点并返回前驱节点的地址
/// @param head 链表头节点
/// @param findData 要查找的数据
/// @return 查找成功返回查找到的节点的前驱节点地址,失败返回NULL
P_node_t findListPrevNode(P_node_t head, int findData)
{
// 判断链表是否为空
if (isListEmpty(head))
{
printf("链表为空.\n");
return NULL;
}
P_node_t tmp = NULL, findNodePrev = NULL;
// tmp遍历链表节点
for (tmp = head; tmp->next != head; tmp = tmp->next)
{
if (tmp->next->data.num == findData)
{
findNodePrev = tmp;
break;
}
}
return findNodePrev;
}
修改链表节点数据:
/// @brief 修改链表节点数据
/// @param head 链表头节点
/// @param reviseData 要修改的节点的数据
/// @return 修改成功返回true,修改失败返回false
bool reviseListNode(P_node_t head, int reviseData)
{
P_node_t findNodePrev = findListNode(head, reviseData);
if (findNodePrev == NULL)
{
printf("链表中未查找到要修改的数据.\n");
return false;
}
printf("请输入要修改的编号名字.\n");
scanf("%d%s", &findNodePrev->next->data.num, findNodePrev->next->data.name);
printf("修改后的节点数据:[%d]:%s\n", findNodePrev->next->data.num, findNodePrev->next->data.name);
return true;
}
链表节点剔除:
/// @brief 剔除链表中的节点
/// @param head 链表头节点
/// @param delData 要剔除节点的数据
/// @return 返回剔除下来的节点
P_node_t delListNode(P_node_t head, int delData)
{
// 判断链表是否有有效节点
if (isListEmpty(head))
{
printf("链表为空,删除失败.\n");
return NULL;
}
P_node_t findNodePrev = findListNode(head, delData);
if (findNodePrev == NULL)
{
printf("链表中未找到要删除数据的节点.\n");
return NULL;
}
// findNodePrev为delNode的前驱节点
P_node_t delNode = findNodePrev->next;
// 要删除节点的前驱节点的next指针指向要剔除节点的后继节点
findNodePrev->next = delNode->next;
// 要剔除节点的next指针指向自己,delNode从链表中剔除
delNode->next = delNode;
// 返回剔除下来的节点
return delNode;
}
链表反转:
/// @brief 将链表反转
/// @param head 链表头节点
void reverseList(P_node_t head)
{
// 判断链表是否为空
if (isListEmpty(head))
{
printf("链表为空.\n");
return;
}
// posPrev为pos的前驱节点,方便pos的next指针反转指向前驱节点
// pos遍历链表的有效节点,反转链表的每个节点
// tmp为pos的后继节点,记录保存pos之后的链表,防止pos后的链表丢失
P_node_t posPrev = NULL, pos = NULL, tmp = NULL;
// pos遍历链表有效节点,所以posPrev从链表头节点开始,pos从链表head->next第一个有效节点开始
// pos==head时遍历完链表的每个节点
// posPrev = pos,pos = tmp;posPrev,pos,tmp指针往下遍历,其中posPrev要先向下遍历
// 因为pos=tmp会改变pos指向,所以要先将pos指向的节点地址赋给posPrev
for (posPrev = head, pos = posPrev->next; pos != head; posPrev = pos, pos = tmp)
{
// 在反转pos节点next指针指向前需要先将pos节点之后的链表保存下来
tmp = pos->next;
// pos的next指向前驱节点,反转pos节点指向
pos->next = posPrev;
}
// 此时pos指向head遍历完整个链表,posPrev指向反转前链表最后一个节点即反转后的第一个节点
// 链表反转后需要将头节点指向链表第一个节点
head->next = posPrev;
return;
}
链表插入排序:
/// @brief 插入排序链表
/// @param head 链表头节点
void insertSort(P_node_t head)
{
// 判断链表是否为空
if (isListEmpty(head))
{
printf("链表为空.\n");
return;
}
// outTmp外循环遍历链表每一个节点,inTmpPrev内循环遍历要插入节点之前的节点,inTmp为inTmpPrev后继节点
// insertNode存放指向要插入的节点,insertNodePrev为插入节点的前驱节点
P_node_t outTmp = NULL, inTmp = NULL, inTmpPrev = NULL, insertNode = NULL, insertNodePrev = NULL;
// 外循环遍历链表从有效节点开始,所以outTmp初始条件为head->next
// 当outTmp==head,outTmp遍历完整个链表,所以结束条件为outTmp!=head
for (outTmp = head->next; outTmp != head;)
{
// insertNode存放指向要插入的节点
insertNode = outTmp;
// 内循环进行每个insertNode要插入节点的插入位置寻找
// 插入需要前驱节点,所以初始条件从head头节点开始,结束条件为:遍历到要插入节点的前驱节点
for (inTmpPrev = head; inTmpPrev->next != insertNode; inTmpPrev = inTmpPrev->next)
{
// inTmpPrev遍历到合适的插入位置跳出循环
if (inTmpPrev->next->data.num > insertNode->data.num)
{
break;
}
}
// 此时跳出循环,有两种情况:
// 一是inTmpPrev遍历到要插入节点的前驱节点,即要插入节点前的链表都没有遍历到合适的位置插入
if (inTmpPrev->next == insertNode)
{
// 更新outTmp往下遍历
outTmp = outTmp->next;
// 此时要插入的节点在合适的位置无需插入,continue提高插入排序效率
continue;
}
// 插入前更新outTmp往下遍历
outTmp = outTmp->next;
// inTmp为inTmpPrev的后继节点
inTmp = inTmpPrev->next;
// 将insertNode节点插入前需要先将insertNode节点从链表中剔除下来,剔除insertNode节点需要用到其前驱节点
for (insertNodePrev = head; insertNodePrev->next != insertNode; insertNodePrev = insertNodePrev->next)
;
// 将insertNode从链表中剔除下来
// insertNode前驱节点的next指针指向insertNode的后继节点
insertNodePrev->next = insertNode->next;
// insertNode的next指针指向自己,将insertNode节点从链表剔除下来
insertNode->next = insertNode;
// 将insertNode插入到链表中,即inTmp节点和inTmpPrev节点之间
// insertNode的next指针指向inTmp
insertNode->next = inTmp;
// inTmpPrev的next指向insertNode
inTmpPrev->next = insertNode;
}
return;
}
链表销毁:
/// @brief 销毁链表,释放申请的堆内存空间
/// @param head 链表头节点
void destoryList(P_node_t head)
{
// tmp遍历链表有效节点并保存后面未释放链表
// tmpPrev指向待释放的节点
P_node_t tmp = NULL, tmpPrev = NULL;
// 销毁链表要考虑到链表有无有效节点的情况
// 释放节点空间从有效节点开始,最后再销毁头节点
for (tmp = head->next; tmp != head;)
{
// tmpPrev为tmp前驱节点
tmpPrev = tmp;
// tmp往后遍历,并在释放tmpPrev前保存后面链表
tmp = tmp->next;
// 释放tmpPrev
free(tmpPrev);
}
// 最后释放头节点
free(head);
return;
}
判断堆内存空间是否全部释放:
单向循环链表代码示例:
/**
* 单向循环链表
*/
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <string.h>
// 设计链表节点数据域
typedef struct data
{
int num;
char name[30];
} data_t;
// 设计链表节点
typedef struct node
{
data_t data; // 链表节点数据域
struct node *next; // 链表节点指针域
} node_t, *P_node_t;
/// @brief 初始化单向循环链表节点
/// @param data 新数据指针
/// @return 初始化完成的新节点
P_node_t nodeInit(data_t *data)
{
// 为新节点在堆内存空间中申请内存空间
P_node_t newNode = (P_node_t)calloc(1, sizeof(node_t));
// 判断是否申请成功
if (newNode == NULL)
{
perror("CALLOC ERROR");
return NULL;
}
// 设计的巧妙之处:
// 设计函数参数为:data_t *data
// 传入NULL时:初始化链表头节点
// 传入不为空时:初始化链表有效节点
if (data != NULL)
{ // 初始化新节点的数据域
memcpy(&newNode->data, data, sizeof(data_t));
}
// 初始化新节点的指针域
newNode->next = newNode;
// 返回初始化完成的新节点
return newNode;
}
/// @brief 判断链表有无有效节点
/// @param head 链表头节点
/// @return 链表无有效节点返回真,有有效节点返回假
bool isListEmpty(P_node_t head)
{
return (head->next == head);
}
/// @brief 获得新数据
/// @return 返回获得的新数据
data_t getData()
{
data_t newData = {0};
printf("请输入编号名字.\n");
while(scanf("%d%s",&newData.num,newData.name)!=2)
{
printf("输入错误,请重新输入.\n");
while(getchar()!='\n');
continue;
}
return newData;
}
/// @brief 将新节点头插到链表中
/// @param head 链表头节点
/// @param newNode 新节点
/// @return 返回头插完成的链表头节点
P_node_t addListHead(P_node_t head, P_node_t newNode)
{
// 头插要考虑到链表有无有效节点的情况
// 新节点指向头节点之后的链表
newNode->next = head->next;
// 头节点指向新节点,新节点插入链表
head->next = newNode;
// 返回头插完成的链表头节点
return head;
}
/// @brief 将新节点尾插到链表中
/// @param head 链表头节点
/// @param newNode 新节点
/// @return 返回链表头节点更新链表
P_node_t addListTail(P_node_t head, P_node_t newNode)
{
// 头插要考虑到链表中是否有有效节点的情况
P_node_t tmp;
// tmp遍历链表找到链表尾
for (tmp = head; tmp->next != head; tmp = tmp->next)
;
// 新节点的next指向tmp的next指向,即指向链表尾head
newNode->next = tmp->next;
// tmp的next指针指向新节点,链表插入新节点
tmp->next = newNode;
// 返回链表头节点更新链表
return head;
}
/// @brief 将节点有序插入到链表中
/// @param head 链表头节点
/// @param newNode 新节点
/// @return 返回有序插入后的链表头节点更新链表
P_node_t addListOrder(P_node_t head, P_node_t newNode)
{
P_node_t tmp = NULL;
// 有序插入要考虑到链表为空的情况,所以tmp从head头节点开始遍历
// tmp遍历到合适的位置插入
for (tmp = head; tmp->next != head && tmp->next->data.num < newNode->data.num; tmp = tmp->next)
;
// 新节点的next指向tmp的next,即插入位置之后的链表,防止后面的链表丢失
newNode->next = tmp->next;
// tmp的next指向新节点,新节点插入到链表中
tmp->next = newNode;
// 返回有序插入后的链表头节点更新链表
return head;
}
/// @brief 遍历打印链表
/// @param head 链表头节点
void display(P_node_t head)
{
if (isListEmpty(head))
{
printf("链表为空.\n");
return;
}
// tmp遍历链表有效节点
for (P_node_t tmp = head->next; tmp != head; tmp = tmp->next)
{
printf("[%d]:%s\n", tmp->data.num, tmp->data.name);
}
return;
}
/// @brief 在链表中寻找匹配数据的节点并返回前驱节点的地址
/// @param head 链表头节点
/// @param findData 要查找的数据
/// @return 查找成功返回查找到的节点的前驱节点地址,失败返回NULL
P_node_t findListPrevNode(P_node_t head, int findData)
{
// 判断链表是否为空
if (isListEmpty(head))
{
printf("链表为空.\n");
return NULL;
}
P_node_t tmp = NULL, findNodePrev = NULL;
// tmp遍历链表节点
for (tmp = head; tmp->next != head; tmp = tmp->next)
{
if (tmp->next->data.num == findData)
{
findNodePrev = tmp;
break;
}
}
return findNodePrev;
}
/// @brief 修改链表节点数据
/// @param head 链表头节点
/// @param reviseData 要修改的节点的数据
/// @return 修改成功返回true,修改失败返回false
bool reviseListNode(P_node_t head, int reviseData)
{
P_node_t findNodePrev = findListPrevNode(head, reviseData);
if (findNodePrev == NULL)
{
printf("链表中未查找到要修改的数据.\n");
return false;
}
printf("请输入要修改的编号名字.\n");
scanf("%d%s", &findNodePrev->next->data.num, findNodePrev->next->data.name);
printf("修改后的节点数据:[%d]:%s\n", findNodePrev->next->data.num, findNodePrev->next->data.name);
return true;
}
/// @brief 剔除链表中的节点
/// @param head 链表头节点
/// @param delData 要剔除节点的数据
/// @return 返回剔除下来的节点
P_node_t delListNode(P_node_t head, int delData)
{
// 判断链表是否有有效节点
if (isListEmpty(head))
{
printf("链表为空,删除失败.\n");
return NULL;
}
P_node_t findNodePrev = findListPrevNode(head, delData);
if (findNodePrev == NULL)
{
printf("链表中未找到要删除数据的节点.\n");
return NULL;
}
// findNodePrev为delNode的前驱节点
P_node_t delNode = findNodePrev->next;
// 要删除节点的前驱节点的next指针指向要剔除节点的后继节点
findNodePrev->next = delNode->next;
// 要剔除节点的next指针指向自己,delNode从链表中剔除
delNode->next = delNode;
// 返回剔除下来的节点
return delNode;
}
/// @brief 将链表反转
/// @param head 链表头节点
void reverseList(P_node_t head)
{
// 判断链表是否为空
if (isListEmpty(head))
{
printf("链表为空.\n");
return;
}
// posPrev为pos的前驱节点,方便pos的next指针反转指向前驱节点
// pos遍历链表的有效节点,反转链表的每个节点
// tmp为pos的后继节点,记录保存pos之后的链表,防止pos后的链表丢失
P_node_t posPrev = NULL, pos = NULL, tmp = NULL;
// pos遍历链表有效节点,所以posPrev从链表头节点开始,pos从链表head->next第一个有效节点开始
// pos==head时遍历完链表的每个节点
// posPrev = pos,pos = tmp;posPrev,pos,tmp指针往下遍历,其中posPrev要先向下遍历
// 因为pos=tmp会改变pos指向,所以要先将pos指向的节点地址赋给posPrev
for (posPrev = head, pos = posPrev->next; pos != head; posPrev = pos, pos = tmp)
{
// 在反转pos节点next指针指向前需要先将pos节点之后的链表保存下来
tmp = pos->next;
// pos的next指向前驱节点,反转pos节点指向
pos->next = posPrev;
}
// 此时pos指向head遍历完整个链表,posPrev指向反转前链表最后一个节点即反转后的第一个节点
// 链表反转后需要将头节点指向链表第一个节点
head->next = posPrev;
return;
}
/// @brief 插入排序链表
/// @param head 链表头节点
void insertSort(P_node_t head)
{
// 判断链表是否为空
if (isListEmpty(head))
{
printf("链表为空.\n");
return;
}
// outTmp外循环遍历链表每一个节点,inTmpPrev内循环遍历要插入节点之前的节点,inTmp为inTmpPrev后继节点
// insertNode存放指向要插入的节点,insertNodePrev为插入节点的前驱节点
P_node_t outTmp = NULL, inTmp = NULL, inTmpPrev = NULL, insertNode = NULL, insertNodePrev = NULL;
// 外循环遍历链表从有效节点开始,所以outTmp初始条件为head->next
// 当outTmp==head,outTmp遍历完整个链表,所以结束条件为outTmp!=head
for (outTmp = head->next; outTmp != head;)
{
// insertNode存放指向要插入的节点
insertNode = outTmp;
// 内循环进行每个insertNode要插入节点的插入位置寻找
// 插入需要前驱节点,所以初始条件从head头节点开始,结束条件为:遍历到要插入节点的前驱节点
for (inTmpPrev = head; inTmpPrev->next != insertNode; inTmpPrev = inTmpPrev->next)
{
// inTmpPrev遍历到合适的插入位置跳出循环
if (inTmpPrev->next->data.num > insertNode->data.num)
{
break;
}
}
// 此时跳出循环,有两种情况:
// 一是inTmpPrev遍历到要插入节点的前驱节点,即要插入节点前的链表都没有遍历到合适的位置插入
if (inTmpPrev->next == insertNode)
{
// 更新outTmp往下遍历
outTmp = outTmp->next;
// 此时要插入的节点在合适的位置无需插入,continue提高插入排序效率
continue;
}
// 插入前更新outTmp往下遍历
outTmp = outTmp->next;
// inTmp为inTmpPrev的后继节点
inTmp = inTmpPrev->next;
// 将insertNode节点插入前需要先将insertNode节点从链表中剔除下来,剔除insertNode节点需要用到其前驱节点
for (insertNodePrev = head; insertNodePrev->next != insertNode; insertNodePrev = insertNodePrev->next)
;
// 将insertNode从链表中剔除下来
// insertNode前驱节点的next指针指向insertNode的后继节点
insertNodePrev->next = insertNode->next;
// insertNode的next指针指向自己,将insertNode节点从链表剔除下来
insertNode->next = insertNode;
// 将insertNode插入到链表中,即inTmp节点和inTmpPrev节点之间
// insertNode的next指针指向inTmp
insertNode->next = inTmp;
// inTmpPrev的next指向insertNode
inTmpPrev->next = insertNode;
}
return;
}
/// @brief 销毁链表,释放申请的堆内存空间
/// @param head 链表头节点
void destoryList(P_node_t head)
{
// tmp遍历链表有效节点并保存后面未释放链表
// tmpPrev指向待释放的节点
P_node_t tmp = NULL, tmpPrev = NULL;
// 销毁链表要考虑到链表有无有效节点的情况
// 释放节点空间从有效节点开始,最后再销毁头节点
for (tmp = head->next; tmp != head;)
{
// tmpPrev为tmp前驱节点
tmpPrev = tmp;
// tmp往后遍历,并在释放tmpPrev前保存后面链表
tmp = tmp->next;
// 释放tmpPrev
free(tmpPrev);
}
// 最后释放头节点
free(head);
return;
}
int main(int argc, char const *argv[])
{
// 初始化链表头节点
P_node_t head = nodeInit(NULL);
char menuOpt;
int overFlag = 0, n, insertOpt, findData, reviseData, delData;
data_t newData;
P_node_t newNode = NULL, findNodePrev = NULL, delNode = NULL;
while (1)
{
printf("请选择要进行的操作.\n");
printf("i.添加数据\ts.排序链表\td.显示链表\tf.查找数据\tg.修改数据\tx.删除数据\tr.翻转链表\tt.退出\n");
scanf("%c", &menuOpt);
switch (menuOpt)
{
case 'i':
printf("请输入要输入几个数.\n");
scanf("%d", &n);
printf("输入1.将数据头插到链表中\t2.将数据尾插到链表中\t3.将数据有序插入到链表中\n");
scanf("%d", &insertOpt);
for (int i = 0; i < n; i++)
{
// 获取数据
newData = getData();
// 初始化节点
newNode = nodeInit(&newData);
if (insertOpt == 1)
{
// 将节点头插添加到链表中
head = addListHead(head, newNode);
}
else if (insertOpt == 2)
{
// 将节点尾插插入到链表中
head = addListTail(head, newNode);
}
else if (insertOpt == 3)
{
// 将节点有序插入到链表中
head = addListOrder(head, newNode);
}
}
break;
case 'd':
display(head);
break;
case 's':
insertSort(head);
break;
case 'f':
printf("请输入要查找的数据.\n");
scanf("%d", &findData);
findNodePrev = findListPrevNode(head, findData);
if (findNodePrev == NULL)
{
printf("查找失败.\n");
}
else
{
printf("查找成功,查找到的节点的地址为:%p:[%d]:%s\n", findNodePrev->next, findNodePrev->next->data.num, findNodePrev->next->data.name);
}
break;
case 'g':
printf("请输入要修改的数据.\n");
scanf("%d", &reviseData);
if (reviseListNode(head, reviseData))
{
printf("修改成功\n");
}
else
{
printf("修改失败\n");
}
break;
case 'x':
printf("请输入要删除的数据.\n");
scanf("%d", &delData);
delNode = delListNode(head, delData);
if (delNode)
{
// 释放剔除节点申请的堆内存空间
free(delNode);
printf("删除成功\n");
}
else
{
printf("删除失败\n");
}
break;
case 'r':
reverseList(head);
break;
case 't':
overFlag = 1;
break;
default:
printf("输入错误,请重新输入.\n");
break;
}
if (overFlag == 1)
{
break;
}
while (getchar() != '\n')
;
}
// 销毁链表
destoryList(head);
return 0;
}
5.结语
在本文中,我们探讨了单向链表的两种主要形式:无头单向不循环链表和单向循环链表。无头单向不循环链表以其简单而直观的结构,适合处理基本的数据存储和操作,而单向循环链表则提供了更高的灵活性,允许从任意节点开始遍历,适应某些特定应用场景。
单向链表作为一种基本的数据结构,具有动态性强、插入和删除操作方便等优点。在实际应用中,选择合适的链表类型能够显著提升程序的性能和可维护性。希望通过本文的介绍,能够为大家更深入地理解和使用单向链表提供帮助