链表这个东西不是很简单,多次重复学习,看视频敲代码才终于变成自己的知识。
本次讲解的是单向链表,只能通过后面节点找到前面节点,无法通过前面节点找到后面节点。
后续会对双向链表,双向循环链表进行讲解。
文章目录
Ⅰ.无空头的链表
链 表 添 加 { 头 添 加 + 头 删 除 应 用 场 景 − − − 栈 头 添 加 + 尾 删 除 应 用 场 景 − − − 队 列 尾 添 加 + 头 删 除 应 用 场 景 − − − 队 列 链表添加\left\{ \begin{array}{rcl} 头添加+头删除 & &应用场景---栈 \\ 头添加+尾删除& &应用场景---队列\\ 尾添加+头删除& &应用场景---队列 \end{array} \right. 链表添加⎩⎨⎧头添加+头删除头添加+尾删除尾添加+头删除应用场景−−−栈应用场景−−−队列应用场景−−−队列
1.添加节点,尾添加
#include<stdio.h>
#include<stdlib.h>
//有空头
//节点结构体
struct Node
{
int a;
struct Node* pNext;
};
//一个节点记着头(用来遍历链表),一个记着尾,头尾指针都为空,说明链表里没有节点
struct Node* g_pHead = NULL;
struct Node* g_pEnd = NULL;
//创建链表。在链表中增加一个数据
//尾添加
void AddListToTail(int a)
{
//创建一个节点
//在vs里malloc会自动进行数据类型转换,但是在别的编译器里,不写强制类型转换,该malloc会报错。
struct Node* pTemp = (struct Node*)malloc(sizeof(struct Node));
pTemp->a = a;
pTemp->pNext = NULL;//这里很重要,保证最后一个节点指向空,遍历时作为遍历退出条件
//将新节点链接到链表上----分两种情况:
//1.链表里还没有节点,链表添加第一个节点时,头节点和尾节点指向同一位置
if (NULL == g_pHead || NULL == g_pEnd)//其实这里只需要一个判断就够了,不需要 || 因为其中有一个为空就说明该链表为空
{
g_pHead= pTemp;
g_pEnd = pTemp;
}
//2.链表从第二个节点开始,头节点就不需要再变化了,只需要尾节点添加就行了
else
{
//由于指针的性质,两个指针指向同一位置时,通过其中一个指针就可以改变所指位置的值,自然指向该位置的另一个指针所指的值就变了。
//这里就是通过g_pEnd->pNext = pTemp;语句改变g_pEnd所指节点成员pNext的值,而g_pEnd是单独的一个节点,它便按照规则也就是下一个节点,指向尾节点。
g_pEnd->pNext = pTemp;//前一个节点的next连接到新的节点
g_pEnd = pTemp;
}
//由于g_pEnd = pTemp;在两个分支里都有,所以可以写成如下形式
/*
if (NULL == g_pHead || NULL == g_pEnd)//其实这里只需要一个判断就够了,不需要 || 因为其中有一个为空就说明该链表为空
{
g_pEnd = pTemp;
}
else
{
g_pEnd->pNext = pTemp;
}
g_pEnd = pTemp;
*/
}
(1)划重点
根据指针的性质,两个指针指向同一位置时,通过其中一个指针就可以改变所指位置的值,自然指向该位置的另一个指针所指的值就变了。
链表尾添加就是通过这一性质完成的。
g_pEnd->pNext = pTemp;
g_pEnd = pTemp;
(2)测试
int main()
{
AddListToTail(1);
AddListToTail(2);
AddListToTail(3);
AddListToTail(4);
g_pHead;
system("pause");
return 0;
}
打断点查看链表添加情况,可以看到数据逐一添加到头节点后面。最先添加的数据在前面,最后添加的数据在后面,和队列的性质一样。
(3)使用链表添加数组数据
int a[10] = { 0,1,2,3,4,5,6,7,8,9 };
for (int i = 0; i < sizeof(a)/sizeof(a[0]); i++)
{
AddListToTail(a[i]);
}
2.添加数据,头添加
//创建链表。在链表中增加一个数据
//头添加
void AddListToHead(int a)
{
//创建一个节点
struct Node* pTemp = (struct Node*)malloc(sizeof(struct Node));
//节点数据赋值
pTemp->a = a;
pTemp->pNext = NULL;//这里很重要,保证第一个点指向空,遍历时作为遍历退出条件
//将新健节点链接到链表上----分两种情况:
//1.链表里还没有节点,链表添加第一个节点时,头节点和尾节点指向同一位置
if (NULL == g_pHead || NULL == g_pEnd)
{
g_pEnd = pTemp;
g_pHead = pTemp;
}
//2.链表从第二个节点开始,尾节点就不需要再变化了,只需要新节点pTemp指向头节点,头节点再更新指向新节点
else
{
//注意,下面这样写是错误的,这样写是假的头添加,相当于只是给头尾指针换了个名字
//尾添加是尾节点的next指向新节点,头添加是新节点的next指向头
//这是头添加和尾添加最大的区别
/*
g_pHead->pNext = pTemp;
g_pHead = pTemp;
*/
//正确写法
//新节点的next指向头
pTemp->pNext = g_pHead;
g_pHead = pTemp;
}
//由于g_pHead = pTemp;在两个分支里都有,所以可以写成如下形式
/*
if (NULL == g_pHead || NULL == g_pEnd)
{
g_pEnd = pTemp;
}
else
{
pTemp->pNext = g_pHead;
}
g_pHead = pTemp;
*/
}
(1)划重点
注意,下面这样写是错误的,这样写是假的头添加,相当于只是给头尾指针换了个名字。当添加第二个及其后续节点时,尾添加是尾节点的next指向新节点,头添加是新节点的next指向头,这是头添加和尾添加最大的区别。
错误写法
g_pHead->pNext = pTemp;
g_pHead = pTemp;
正确写法
pTemp->pNext = g_pHead;
g_pHead = pTemp;
(2)测试
int a[10] = { 0,1,2,3,4,5,6,7,8,9 };
for (int i = 0; i < sizeof(a) / sizeof(a[0]); i++)
{
AddListToHead(a[i]);
}
打断点查看链表添加情况,可以看到数据逐一添加到链表中,最先添加的数据在底部,而最后添加的数据在顶部,和栈的性质一样。
3.遍历链表
遍历查询数据
void ScanList()
{
//操作链表时,头指针不能动,头指针动了,链表的头就找不到了。
//建立一个新指针用来遍历
struct Node* pTemp = g_pHead;
while (pTemp != NULL)//非空链表不遍历
{
printf("%d\n",pTemp->a);
pTemp = pTemp->pNext;
}
}
注意这里的判断条件也可以写成while (pTemp),不会出错,最好写成while (pTemp != NULL),一般只有bool型变量这样写—while(b) 以及while(! b)
(1)测试
(2)划重点
节点数据赋值前的预操作
pTemp->a = a;
pTemp->pNext = NULL;//这里赋值为NULL很重要
-
对于尾添加,保证最后一个节点指向空,遍历时作为遍历退出条件
-
对于头添加,保证第一个点指向空,遍历时作为遍历退出条件
如果没有进行上述操作,会出现如下错误,出现了野指针,这是不被允许的。
4.查询指定节点
(1)代码
struct Node* SelectNode(int a)
{
struct Node* pTemp = g_pHead;
while (pTemp != NULL)
{
if (a == pTemp->a)
{
return pTemp;
}
pTemp = pTemp->pNext;
}
return NULL;
}
(2)测试
该节点存在
该节点不存在
5.链表释放
(1)基础知识
本例子使用了
struct Node* pTemp = g_pHead;
与之前的
struct Node* pTemp = (struct Node*)malloc(sizeof(struct Node));
相比较,提出了本问题:什么时候定义指针需要用malloc分配内存,什么时候不需要。弄清楚这一点对彻底理解清楚程序很有必要。首先需要了解一下几点:
- C、C++中,内存模型分为栈和堆,这两种模型内存的方式是不同的。
- 在栈中存放的变量是由系统自动管理的,在函数结束后系统会自动释放,不需要人为的任何操作。
- 在堆中存放的是用户自己管理的内存,手动分配的,malloc建立,系统不会在函数体执行结束后自动释放,需要用户手动通过free函数释放。
- 当需要对分配的空间进行自己的管理和释放时,需要实用malloc,或者分配的空间再函数结束后还需要存在。
下面列举一个学习链表初期容易犯的错误:
在释放某一节点前,需要再定义一个节点(不需要malloc)记住要删除的节点,然后pTemp指向pTemp的下一节点,否则会出错,出现访问错误的问题。
(2)链表释放代码
//链表清空
//数组释放,只需要释放首地址,该数组就全部释放掉了
//链表释放需要循环遍历释放每一个节点
void FreeList()
{
//记录头
struct Node* pTemp = g_pHead;
while (pTemp != NULL)//非空链表不遍历
{
struct Node* pt = pTemp;//定义一个节点记住pTemp,待删除
pTemp = pTemp->pNext;//pTemp指向下一个节点
free(pt);
}
//头尾清空,方便下一次创建
g_pHead = NULL;
g_pEnd = NULL;
}
6.插入节点
(1)代码
void AddListRand(int index, int a)
{
//链表为空
if (NULL == g_pHead)
{
printf("链表没有指定节点");
return;
}
//找位置
struct Node* pt = SelectNode(index);
if (NULL == pt)
{
printf("没有指定节点\n");
return;
}
//有此节点
//给a创建节点
struct Node* pTemp = (struct Node*)malloc(sizeof(struct Node));
//节点成员赋值
pTemp->a = a;
pTemp->pNext = NULL;
//链接到链表上
if (pt == g_pEnd)
{
g_pEnd->pNext = pTemp;
g_pEnd = pTemp;
}
else
{
//先连
pTemp->pNext = pt->pNext;
//后断
pt->pNext = pTemp;
}
}
(2)测试
7.删除节点
1.头删除
代码
//头删除
void DeleteListHead()
{
//链表检测
if (NULL == g_pHead)
{
printf("链表为NULL,无需释放\n");
return;
}
//记住旧的头
struct Node* pt = g_pHead;
//头的下一个节点变成新的头
g_pHead = g_pHead->pNext;
//释放旧的头
free(pt);
}
测试
2.尾删除
划重点
要先找到尾节点的前一个节点,否则直接删除了尾节点,g_pEnd就无法再容易的找到旧的尾节点的前一个节点。
代码
void DeleteListTail()
{
//链表检测
if (NULL == g_pHead)
{
printf("链表为NULL,无需释放\n");
return;
}
//链表不为空
//链表有几个节点,一个节点和多个节点的情况不一样
//链表只有一个节点
if (g_pHead == g_pEnd)
{
free(g_pEnd);
//头尾一定要赋空值
g_pHead = NULL;
g_pEnd = NULL;
}
else
{
//找到尾巴前一个节点
struct Node* pTemp = g_pHead;
while (pTemp->pNext != g_pEnd)
{
pTemp = pTemp->pNext;
}
//找到了,删除尾巴
//释放尾节点
free(g_pEnd);//释放g_pEnd所指区域的空间
//尾巴前移
g_pEnd = pTemp;//g_pEnd作为一个单独的变量还是可以继续使用的
//尾巴的next指针赋空值NULL
g_pEnd->pNext = NULL;
}
}
测试
3.指定节点删除
注意先判断链表是否为空,还要判断该数据对应的节点是否存在。考虑要周全,要分三种情况,(1)链表有一个节点,(2)两个节点,(3)两个以上节点。两个节点时要判断要删除的节点是头节点还是尾节点;三个以上节点删除时还多一种情况,判断要删除的节点是中间节点的情况。
代码
//指定节点删除
void DeleteListRand(int a)
{
//链表检测
if (NULL == g_pHead)
{
printf("链表为NULL,无需释放\n");
return;
}
//链表有该元素
struct Node* pTemp = SelectNode(a);
if (NULL == pTemp)
{
printf("查无此节点\n");
return;
}
//找到了该节点,分为三种情况
//1.链表中只有一个节点
if (g_pHead == g_pEnd)
{
DeleteListHead();
}
//2.链表中有两个节点
else if (g_pHead->pNext == g_pEnd)
{
//分为两种情况
//1.要删除的是头节点
if (g_pHead == pTemp)
{
DeleteListHead();
}
//2.要删除的是尾节点
else
{
DeleteListTail();
}
}
//3.链表中有两个以上的节点
else
{
//分为三种情况
//1.要删除的是头节点
if (g_pHead == pTemp)
{
DeleteListHead();
}
//2.要删除的是尾节点
else if(g_pEnd == pTemp)
{
DeleteListTail();
}
//删除除了头尾节点以外的节点
else
{
//找到要删除节点Ptemp的前一个节点
struct Node* pt = g_pHead;
while (pt->pNext != pTemp)
{
pt = pt->pNext;
}
//找到了
pt->pNext = pTemp->pNext;
free(pTemp);
}
}
}
测试
Ⅱ.有空头的链表
1.尾添加
(1)代码
#include <stdio.h>
#include <stdlib.h>
//节点结构体
struct Node
{
int a;
struct Node* pNext;
};
//头尾指针定义
struct Node* g_pHead = NULL;
struct Node* g_pEnd = NULL;
//尾添加
void AddListTail(int a)
{
//创建一个节点
struct Node* pTemp = (struct Node*)malloc(sizeof(struct Node));
//节点成员赋值
pTemp->a = a;
pTemp->pNext = NULL;
//链接
//与无空头的链表相比,不需要再单独考虑头节点了
g_pEnd->pNext = pTemp;
g_pEnd = pTemp;
}
(2)测试
int main(void)
{
//链表空头
//有空头初始化,也简化了链表操作时对头节点的单独考虑,也就不需要再单独考虑头节点了
g_pHead = (struct Node*)malloc(sizeof(struct Node));
g_pHead->pNext = NULL;//空头成员不用管
g_pEnd = g_pHead;
AddListTail(1);
AddListTail(2);
AddListTail(3);
AddListTail(4);
return 0;
}
划重点
有空头初始化,也简化了链表操作时对头节点的单独考虑,也就不需要再单独考虑头节点了
g_pHead = (struct Node*)malloc(sizeof(struct Node));
g_pHead->pNext = NULL;//空头成员不用管
g_pEnd = g_pHead;
测试结果
2.头添加
(1)将空头链表初始化和创建节点进行封装
这样做是为了简化代码,看起来更加简洁
//空头链表初始化
void InitListHead()
{
//链表空头
//有空头初始化,也简化了链表操作时对头节点的单独考虑,也就不需要再单独考虑头节点了
g_pHead = (struct Node*)malloc(sizeof(struct Node));
g_pHead->pNext = NULL;//空头成员不用管
g_pEnd = g_pHead;
}
//创建节点
struct Node* CreatNode(int a)
{
//创建一个节点
struct Node* pTemp = (struct Node*)malloc(sizeof(struct Node));
if (NULL == pTemp)
{
printf("内存不足\n");
return NULL;
}
//节点成员赋值
pTemp->a = a;
pTemp->pNext = NULL;
return pTemp;
}
(2)头添加代码
//头添加
void AddListHead(int a)
{
struct Node* pTemp = CreatNode(a);
if (NULL == pTemp)
{
return;
}
//链接
pTemp->pNext = g_pHead->pNext;
g_pHead->pNext = pTemp;
}
(3)测试
代码
int main(void)
{
//空头链表初始化
InitListHead();
AddListHead(0);
AddListHead(1);
AddListHead(2);
AddListHead(3);
g_pHead;
return 0;
}
测试结果