上节我们吧顺序表总结完毕,这节我们再来讲一下链表。
相对于顺序表来说,链表有什么优点:那我们想来说一下顺序表的缺点:
1.中间/头部位置的插入删除,需要挪动数据会导致程序效率低下。
2.增容会导致降低程序的运行效率
3.存在着一定的空间浪费
所以我们选择在使用链表来避免这样的缺点。
1.链表的概念及结构
概念:链表是一种物理存储结构上非连续非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。
链表就像这个小火车一样,是由一节节车厢(一个个节点)组成的 ,他的每个节点都是独立存在的,那我们怎么进行节点之间的联系呢?
那就是上一个节点,节点就是长这样的:
节点的组成主要有两个部分:当前节点要保存的数据 和 保存下一个节点的地址(指针变量)。图中指针变量 plist保存的是第一个节点的地址,我们称plist此时“指向”第一个节点,如果我们希望plist“指向”第二个节点时,只需要修改plist保存的内容为0x0012FFA0。
为什么还需要指针变量来保存下一个节点的位置?
链表中每个节点都是独立申请的(即需要插入数据时才去申请一块节点的空间),我们需要通过指针变量来保存下一个节点位置才能从当前节点找到下一个节点。
结合起那面学到的知识,我们可以给出每个节点对应的结构体代码:
我们先假设当前保存的节点是整型:
//其实也是可以不用假设是整形数据,我们可以typedef一下
typedef int SLDataType;//到时候用的时候就直接改一下int就可以了。
struct SListNode
{
SLDataType data; //节点数据
struct SListNode* next; //指针变量用来保存下一个节点的地址
//我们要思考一下为什么next指针的类型是struct SListNode*?因为next指针指向的是下一个节点,下一个结点的类型就是结构体类型
};
当我们想要保存一个整型数据时,实际是向操作系统申请了一块内存,这个内存不仅要保存整型数据,也需要保存下一个节点的地址(当下一个节点为空时保存的地址为空就是NULL)。当我们想要从第一个节点走到最后一个节点时,只需要在前一个节点拿上下一个节点的地址(下一个节点的钥匙)就可以了。
其实这串代码有很多可以改进的地方我们可以改成这样的:
//其实也是可以不用假设是整形数据,我们可以typedef一下
typedef int SLDataType;//到时候用的时候就直接改一下int就可以了。
//如果我们以后要用到这个结构体,但是他的名字特别复杂,那时候我们就要进行简单的命名
typedef struct SListNode
{
SLDataType data; //节点数据
struct SListNode* next; //指针变量用来保存下一个节点的地址
//我们要思考一下为什么next指针的类型是struct SListNode*?因为next指针指向的是下一个节点,下一个结点的类型就是结构体类型
}SLTNode;//这就是新名字
给定的链表结构中,如何实现节点从头到尾的打印?
2.单链表的实现
我们这一章的代码只讲:结构体的定义 链表的打印 申请空间函数的定义,还有链表的尾插
SList.h
#pragma once
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
//其实也是可以不用假设是整形数据,我们可以typedef一下
typedef int SLTDataType;//到时候用的时候就直接改一下int就可以了。
//如果我们以后要用到这个结构体,但是他的名字特别复杂,那时候我们就要进行简单的命名
typedef struct SListNode
{
SLTDataType data; //节点数据
struct SListNode* next; //指针变量用来保存下一个节点的地址
//我们要思考一下为什么next指针的类型是struct SListNode*?因为next指针指向的是下一个节点,下一个结点的类型就是结构体类型
}SLTNode;//这就是新名字
//接下来写一个非常简单的代码,就是链表的打印
void SLTPrint(SLTNode* phead);//定义一个指针(形参),指向链表的头节点,这里的phead就是人为取得名字
//尾插:尾插就是要先创建一个新节点,找到尾节点,再将尾节点和新节点连接起来
//
//申请空间函数
SLTNode* SLTBuyNode(SLTDataType x);//形参
void SLTPushBack(SLTNode** pphead, SLTDataType x);
SList.c
#include"SList.h"
void SLTPrint(SLTNode* phead)
{
SLTNode* pcur = phead;
while (pcur)//这里其实就是pcur != NULL
{
printf("%d->", pcur->data);
pcur = pcur->next;
}
printf("NULL");//这就是打印代码,在上面图片中有解释怎么进行理解。
}
SLTNode* SLTBuyNode(SLTDataType x)//形参
{
SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
if (newnode == NULL)//如果是申请不成功
{
perror("malloc fail!");//进行报错
exit(1);//并直接退出
}//下面是申请成功的情况
newnode->data = x;
newnode->next = NULL;
return newnode;//这里最后要返回新指针
}
//*pphead就是指向第一个结点的指针
void SLTPushBack(SLTNode** pphead, SLTDataType x) //改变外面的头指针要用二级指针 这就是形参用二级指针接收
{
SLTNode* newnode = SLTBuyNode(x);//实参
找尾
定义一个指针让他指向头节点
创建一个新节点
SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));//但是不管是头插还是尾插都需要不断申请空间,为了方便起见我们直接给封装成一个函数
//SLTNode* ptail = phead;
//while (ptail->next)//括号里就相当于ptail->next != NULL
//{
// ptail = ptail->next;
//}
出了这个循环,ptail指向的就是尾节点
//ptail->next = newnode;//现在才是完成尾插,但是我们有没有感觉有错误呢?
如果链表是一个空链表呢?phead指向的就是空,ptail指向的也是空,当创建了一个节点了之后,那再进行while循环里面的判断ptail->next != NULL,这一点就不成立了,
因为->就是对指针的解引用,但本身ptail就是空指针,所以不成立,所以我们要进行判断链表为不为空
assert(pphead);//*pphead其实是可以为空的。因为*pphead本身就是头节点plist,他是可以为空
//空链表和非空链表两种情况:
if (*pphead == NULL) //是判断相等,而不是赋值
{
*pphead = newnode;//如果为空链表的话,申请的新节点就是头节点
}
else
{
SLTNode* ptail = *pphead;
while (ptail->next)//括号里就相当于ptail->next != NULL
{
ptail = ptail->next;
}
//出了这个循环,ptail指向的就是尾节点
ptail->next = newnode;
}
//那这里还有没有问题?我们进行测试一下:
}
test.c
//我还要在test.c文件中进行测试还是要把SList.h文件加进来
#include"SList.h"
void SListTest01()//我们写上这样一串代码,来表示第一串测试
{
//在这里我们要定义第一个指针,也就是第一个节点
//链表是由一个一个的节点组成
//我们创建几个节点
//定义第一个节点,这里我要动态申请一块空间来存储节点里面的数据和指针
SLTNode* node1 = (SLTNode*)malloc(sizeof(SLTNode));//这里需要强制类型转化一下。
node1->data = 1;//node里面有两个数据一个是data一个是下一个结点的地址,但是现在我们还没有下一个节点所以不用着急,我们在创建几个节点就可以
SLTNode* node2 = (SLTNode*)malloc(sizeof(SLTNode));
node2->data = 2;
SLTNode* node3 = (SLTNode*)malloc(sizeof(SLTNode));
node3->data = 3;
SLTNode* node4 = (SLTNode*)malloc(sizeof(SLTNode));
node4->data = 4;
//将四个节点连接起来
node1->next = node2;
node2->next = node3;
node3->next = node4;
node4->next = NULL;
//调用链表的打印
SLTNode* plist = node1;//让plist作为实参传过去,看起来是有点多此一举,其实是避免node1被改变
SLTPrint(plist);//这里按说是要传第一个节点,但是如果不想传的话也没有关系的我们可以定义一个新指针
//我们再回到SList.c里面看看打印代码在那么写
}
SLTNode* SListTest02()
{
SLTNode* plist = NULL;//上来直接给一个空链表
SLTPushBack(&plist, 1);//调用尾插 ,这里为什么要进行取地址?因为我们要进行传址操作,所以要进行传地址,但是plist不是个指针变量嘛,为什么还要取地址?因为plist存的是别的变量的地址
//你如果想进行传址操作还是要传递自己的地址,所以要对指针取地址,那么形参就要用二级指针来进行接收了。
//并不是所有的传参都需要传址操作,就像上面的SLTPrint(plist);当函数需要修改传入的参数值时,需要传递参数的地址,以便函数内部可以直接修改原始数据。
//上面的就单纯的是调用,没有想要进行修改。
return plist;
}
int main()
{
//SListTest01();注释掉
//实际上我们在创建链表的时候不是一个一个的去创建节点,初始的时候是一个空列表,我们要通过插入方法,来为列表增加数据,我们不再使用手动的方式插入
//你链表定义在SListTest02();里面,出了函数就找不到了
SLTNode* plist = SListTest02(); //要以返回值的形式返回
SLTPrint(plist);//这时候我们打印一下,但是屏幕上只出来个NULL,但是我们已经给他尾插了个数了,但是为什么还为NULL呢?
return 0;
}
这三串代码的文件表达,满满干货我们从头开始看从SList.h开始看,有顺序知道,耗费大量心血,期望能帮到各位