前文
昨天刚写完了顺序表的部分,今天加更给单链表也给上传一下,我觉得写代码理清各种事件的逻辑是很有成就感的,也希望我能继续把这个事情给做下去,努力做好
认识单链表
单链表也是线性表的表达方式之一,和数组不同的是,单链表里面的元素之间,内存不是连续的,是通过指针指向下一个元素。在单链表中,基本组成成分是节点,节点里面分成两个部分,一个是数据域,还有一个是指针域,指向下一个节点的位置。
#include <stdio.h>
typedef int Elemtype;
typedef struct SLlist{
Elemtype Data; //存放数据域
struct SLlist* next; //存放指针域
}SLlist;
正如代码中写的那样,将线性表封装在一个结构体里,里面的成员可以存放它的数据,还有一个结构体指针,这个结构体指针就是用来指向下一个节点的地址。
在线性表的使用中,有几个很重要,容易混淆的概念:头指针,头节点,尾指针,尾节点。
其中头节点是这个链表里面的第一个节点,里面存放了数据和下一个节点的地址,而头指针是一个结构体类型的指针,虽然形式上和节点有些与类似,但实际上里面存放的是头节点的地址,它不是这个链表的节点,只不过是在告诉人们,从这里指向了头节点。而尾节点则是链表的最后一个节点,通过设置尾节点的指针域,指向NULL或者头节点,可以构成一个普通的单链表或者循环单链表,尾指针则是指向尾节点的一个指针,和头指针作用基本一致,里面存放的是尾节点的地址。
懂了这些,接下来就开始进一步学习链表的操作吧
链表的基本操作
链表的建立
链表和顺序表不同的一点是,在每一次加入新节点时,都要申请一份内存。
#include <stdio.h>
#include <stdlib.h>
typedef int Elemtype;
typedef struct SLlist{
Elemtype Data;
struct SLlist* next;
}SLlist;
//建立链表
SLlist* Create_List(Elemtype value){
SLlist* New_List = (SLlist*)malloc(sizeof(SLlist));
if(New_List == NULL){
printf("获取内存失败\r\n");
return NULL;
}
New_List->Data = value;
New_List->next = NULL;
return New_List; //返回新节点的地址
}
在这里,我们通过malloc来申请一份内存,来存放新节点,在这里这个New_List既是一个新节点,也是这个新节点的地址,因为我们时通过它来存放了申请内存的首地址,而且这个指针还是结构体指针类型,里面有成员可以存放数据和下一个节点的地址NULL
所以我们可以明白,每个节点的名字都存放着自己这个节点的地址
在我们每一次插入新节点时,都要调用这个函数
链表的插入之尾插法
链表的插入有两种形式,一种是头插法,一种是尾插法。在这里我先讲尾插法,因为更符合生活现象。
尾插法中利用了两个指针,头指针和尾指针,将节点按照先后顺序依次插入在前一个节点的后面
//尾插法,新节点永远插在后面
SLlist* Tail_list(int n, Elemtype value[])
//参数:n是指插入节点数目;value是节点的数据,以数组的形式传参,一次插入多个节点
{
SLlist* head = NULL;
//头指针,头指针指向头节点,也就是第一个节点
SLlist* tail = NULL;
//尾指针,尾指针指向尾节点,也就是最后一个节点,在尾插法中尾节点是必须要有的
for (int i = 0; i < n; i++) {
//创建新节点
SLlist* newNode = Create_List(value[i]);
if (head == NULL) //如果插入的是第一个节点
{
head = newNode;
tail = newNode;
}
else
{
tail->next = newNode;
//通过 tail 里保存的地址,找到前一个节点
//然后把前一个节点的next字段写成newNode的地址
tail = newNode; //更新尾指针,存放的是地址
}
}
return head; //返回的是头指针,也就是头结点的地址
}
按照这样,如果插入的是第一个节点,原链表里面没有数据,那么得到的就是类似下图

其中的椭圆形为插入的第一个节点,左右两边分别为头指针和尾指针,该节点指向NULL
如果继续插入第二个节点,此时,由于链表不为空,则插入的新节点在原节点后面
我们可以看到,首先,先将原来的节点指向新节点,通过tail->next = newnode;再将尾指针更新,指向新节点,使得尾指针存放的是新节点的地址。
以此类推
然后整个函数返回头指针,也就是头节点的地址。
链表插入之头插法
与尾插法不同,头插法不需要用到尾指针,它是将新插入的节点放到表头
SLlist* Head_list(int n, Elemtype value[])
{
SLlist* head = NULL; //头指针
for (int i = 0; i < n; i++) {
//创建新节点
SLlist* newNode = createNode(value[i]);
//核心
newNode->next = head;
head = newNode; //更新头节点
}
return head;
}
当然,我个人不是特别爱用这个,更多的是采用尾插法的形式。
遍历单链表
void List_printf(SLlist* head)
{
SLlist* current = head;
while(current != NULL){
printf("%d\r\n",current->Data);
current = current->next;
}
}
通过传入头节点,先判断链表是否为空,然打印节点的数据,再更新到下一个节点
链表中节点的删除
删除节点,有俩种方式,第一种,删除对应数据元素的第一个节点的;第二种,删除第i个节点。
在这里,我想先说明一下,我定义的链表的位置是从0开始的,也就是说,头节点位置是0,和数组类似。
删除节点,如果是在不是为头节点的情况下,那么,就是要将它的前节点和它的后节点连接起来,
再释放这个节点的内存;如果删除的是头节点,那就要改变头指针,将其指向下一个节点。这里就需要依靠二级指针的作用
第一种,删除对应数据元素的节点
void Delete_List(SLlist** head, Elemtype value) //传入的参数是二级指针
{
if(*head == NULL)//链表为空
{
printf("无法执行\r\n");
return;
}
SLlist* current = *head; //当前检查的节点
SLlist* prev = NULL; //上一个节点
while(current != NULL && current->Data != value)
{
prev = current;
current = current->next;
}
if(current == NULL) //没找到
{
printf("不存在该链表");
return;
}
if(prev == NULL) //删除的是头节点
{
//在这里才能体现二级指针的作用
*head = current->next;
free(current);
printf("删除成功\r\n");
return;
}
else //删除的是中间节点或者尾节点
{
prev->next = current->next;
free(current);
printf("删除成功\r\n");
return;
}
}
删除操作要考虑删除的节点是不是头节点,如果是,那么就要改变头节点,所以需要传入一个二级指针,有类似操作的还有在某个位置插入某节点。
第二种,删除第i个节点
void Delete_Index_List(SLlist** head, int i) //传入的参数是二级指针
{
if(*head == NULL || i < 0)//链表为空
{
printf("无法执行\r\n");
return;
}
int index = 0;
SLlist* current = *head; //当前检查的节点
SLlist* prev = NULL; //上一个节点
while(current != NULL && index != i)
{
prev = current;
current = current->next;
index++;
}
if(current == NULL) //没找到
{
printf("不存在该链表");
return;
}
if(index == 0) //删除的是头节点
{
//在这里才能体现二级指针的作用
*head = current->next;
free(current);
printf("删除成功\r\n");
return;
}
else //删除的是中间节点或者尾节点
{
prev->next = current->next;
free(current);
printf("删除成功\r\n");
return;
}
}
大体思路和第一种情况一样
总结一下删除节点的逻辑:
首先先判断一下这个节点是否为空(有时还要判断删除的位置是否正确),如果不是则继续,创造两个结构体指针,一个指向当前,一个指向现在之前(有时需要再写上一个数字,用于累计观察)
当前节点从头节点开始,而前节点从NULL开始
然后写上一个无限循环,来开始慢慢推进,直到当前节点为空或此时节点的数据域对应上,那就跳出循环
第一种可能,这个链表里面没有我要找的元素,所以推进到最后,当前节点变成了NULL
第二种可能,此时要删掉的节点是头节点,那么条件就是前一个节点为NULL(或者删除位置为0),然后语句体里面写上:更新头节点,释放当前节点。
第三种可能,此时要删掉的节点是中间节点或者尾节点,那么就要跳过当前节点,再释放当前节点
链表的插入
链表的插入要实现,也是和删除样,要考虑头节点,所以就需要二级指针作为入口
int Insert_List(SLlist** head, int pos, Elemtype value) {
// 检查位置是否合法
if (pos < 0) {
printf("位置不能为负数\r\n");
return 0; // 返回0表示失败
}
// 创建新节点
ListNode* newNode = createNode(value);
// 情况1:插在头部(pos == 0)
if (pos == 0) {
newNode->next = *head; //将新节点的指针域指向之前的头节点
*head = newNode; //更新头节点
printf("已插入 %d 到位置 %d\n", value, pos);
return 1; // 成功
}
// 情况2:插在中间或尾部
ListNode* current = *head;
int index = 0;
// 寻找第pos-1个节点(也就是插入位置的前一个)
while (current != NULL && index < pos - 1) {
current = current->next;
index++;
}
// 如果没走到指定位置(比如链表太短)
if (current == NULL) {
printf("位置 %d 超出链表范围!\n", pos);
free(newNode); // 记得释放未使用的节点
return 0;
}
// 此时current是第 pos-1 个节点
newNode->next = current->next; //新节点的指针域指向原来这个位置的节点
current->next = newNode; //第pos-1个节点的指针域更新,指向新节点
printf("已插入 %d 到位置 %d\n", value, pos);
return 1; // 成功
}
链表的插入逻辑:
首先判断插入的位置是否合法,如果合法则继续下一步,用一个结构体指针来接受这个新插入的节点
然后开始判断如果它插在头部(假设位置是0),那么就要将这个新节点的指针域指向之前的头节点,再更新头指针
假设位置不是0:用一个结构体指针来代替头节点,再用一个整数来计数,目的找到插入位置的前一个节点,进行无限循环慢慢推进
直到不存在该节点或者找到对应节点就退出
第一种没找到,此时current == NULL
第二种:找到了,此时将新节点的指针域指向原节点的地址;再将插入位置前一个节点的指针域更新,指向新节点
链表的查找
链表的查找和删除一样,也是要分成两种情况,第一种是根据数据值,来查找对应的第一个元素,第二种则是根据位置,查找第i个元素
第一种
//查找节点(返回节点指针)
SLlist* search(SLlist* head, Elemtype value)//传入头节点
{
SLlist* current = head;
while (current != NULL) {
if (current->Data == value) {
return current; //返回结点指针
}
current = current->next;
}
return NULL;
}
//查找节点(返回当前位置)
int searchIndex(SLlist* head, Elemtype value)
{
SLlist* current = head;
int index = 0;
while (current != NULL)
{
if (current->Data == value)
{
return index;
}
current = current->next;
index++;
}
return -1; //没找到
}
第二种
//查找节点(返回该节点的地址)
SLlist* Index_List(SLlist* head, int i)
{
ListNode* current = head;
int index = 0;
while (current != NULL)
{
if (index == i)
{
return current;
}
current = current->next;
index++;
}
return NULL; //没找到
}
链表排序
链表的排序比较难理解,我本人常用的方式有两种情况,第一种就是冒泡排序的改装;第二种则是
通过链表的特性直接改变指针域实现排序
设定我现在按照升序的方式来排序
第一种
void bubbleSortList(SLlist* head) {
if (head == NULL) return;
ListNode* end = NULL; // 每轮最大值冒泡到末尾,后续不用比较
while (head->next != end) { // 外层:控制轮数
ListNode* current = head;
while (current->next != end) { // 内层:相邻比较
if (current->Data > current->next->Data) {
// 交换数据
Elemtype temp = current->Data;
current->Data = current->next->Data;
current->next->Data = temp;
}
current = current->next;
}
end = current; // 缩小比较范围,则end及其后面的都是正常排好顺序的,end逐步向链表左边移动
}
}
第二种
SLlist* insertionSortList(SLlist* head) {
if (!head || !head->next)
return head;
SLlist dummy; // 虚拟头节点
dummy.next = NULL;
SLlist* current = head;
while (current != NULL) {
SLlist* next = current->next; // 保存下一个节点(防止断链)
// 在 dummy 链表中找插入位置
SLlist* prev = &dummy;
while (prev->next != NULL && prev->next->Data < current->Data) {
prev = prev->next;
}
// 插入 current 到 prev 之后
current->next = prev->next;
prev->next = current;
current = next; // 继续处理下一个
}
return dummy.next; //返回虚拟头节点的下一个节点,也就是我们需要的第一个节点
}
这里这个链表的排序我打算再重新开个下章节讲一下,就和下次的双链表一起写,我今天写这个感觉头有点充血了,人都写傻了,等下后面我看一下这里该怎么将好理解一点。
这个是我之前写的逻辑,可以先这么理解一下
总结
单链表的介绍和基本功能就讲的差不多了。由于作者学校这几天考试繁多,可能更新较慢,不过我会把它更的,这个我觉得可以
6247

被折叠的 条评论
为什么被折叠?



