链表及其有关操作
链表的定义:
首先先让我们来看看什么是链表:
链表是一种物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的,链表由一系列结点(链表中每一个元素称为结点)组成,结点可以在运行时动态生成。
由上面我们不难发现,链表就是一系列零散的点,通过指针作为引线,在内存中串起来的一组数据。还有一个值得我们注意的是,链表是非顺序的存储结构,与我们的数组不同,虽然两者都是一组数据,但是数组在内存中是连续存储的。
然后还有一个值得注意的点是,链表中的元素是动态生成的,也就是说,我们不再用栈区的内存来存储数据,我们会用类似于malloc的函数取堆区开辟内存存储数据。
链表的分类:
然后我们来了解一下有哪几种链表:
1.单链表,顾名思义,就是有且仅有一条引线(不能分叉)来将零散点串起来的链表,并且每相邻的两个链表都有相同的顺序,即一个串一个,所有结点和边形成一条单向有向通路。
2.双向链表:双向链表,就是里面的每个存储结点有一个指向前面元素的指针和指向后面元素的指针,即两个存储元素间是互通的,互相可达的。
3.循环链表:所谓循环,就是末尾与起点相连,形成闭合回路,循环链表就是最后一个结点可以指向链表最开始的位置:
单链表的构成
单链表由一连串的结点构成,而节点要的有两个性质:1.存储数据,2.存放指向下一个结点的指针,所以我们不难总结出,单链表中一个节点,分为两个部分,一部分是存放数据的(数据域),一部分是存放指针(指针域):
链表的结点概念:
1.首节点:顾名思义,就是链表中第一个存储数据的结点。
2.头结点:头节点和首结点不同,头节点不算入这个链表的结点,它不存放数据,它只存放指向首结点的指针。
3.尾结点:尾结点就是最后一个结点
4.头指针:头指针就是指向头节点的指针
5.尾指针:尾指针就是尾结点的指针域存放的指针,一般在单链表中为NULL
有了上面的认识,我们可以先初步写出每个结点的类型:
typedef struct Dot
{
int data;//数据
struct Dot* Pnext;//指针
}Node ,*Pnode;
这里第一部分的data表示存放的数据,而第二部分的Pnext表示指向下一个结点的指针,因为下一个结点和它本身类型一样,所以下一个结点的类型也是struct Dot。为了方便,我将这个结构体类型重命名为Node,而将指向这个结构体的指针类型命名为Pnode
单链表的创建
首先,如何创建一个单链表,回顾前面链表的构成,一个链表由一连串的结点串起来,那怎么确定这串链表的位置呢?就像你怎么拎起来一串珠子,当然是拿起它第一个举起来,那么那串珠子自然都被你拎起来了。
我们创建一个单链表的思想也类似,要想找到这个结点,首先要创建一个头节点,要创造头节点,就不可避免去创造一个指向头节点的头指针,作为我们“拉动”这个链表的引线
Pnode Phead = NULL;
Phead = (Pnode)malloc(sizeof(Node));
if (Phead == NULL)
{
printf("Fail Creat!\n");
exit(-1);
}
然后我们要想,接下来要怎么做?是不是要创建新的结点,并且将每一个结点的数据存放进去,并且要将一个个结点挂在前一个结点上面,即让前一个结点指针域的指针指向后一个节点,按照这个思路,我们可以先创造新结点并存入数据:
//len是创造链表结点个数
for (int i = 0; i < len; i++)
{
//输入每一个结点存放的数据
int data = 0;
printf("请输入第%d个结点的数据:>\n",i+1);
scanf("%d", &data);
//创造一个新节点
Pnode Pnew = NULL;
Pnew = (Pnode)malloc(sizeof(Node));
if (Pnew == NULL)
{
printf("Fail creat!\n");
exit(-1);
}
}
接下来我们只需要将新的结点挂在前一个结点上就行了,但问题来了,前一个结点怎么确定呢?于是我们又创立一个新的指针变量Pend,指向当前链表最后一个结点:
//链表长度
int len = 0;
printf("请输入链表长度:>\n");
scanf("%d", &len);
//创造头指针
Pnode Phead = NULL;
//当前尾结点
Pnode Pend = NULL;
Phead = (Pnode)malloc(sizeof(Node));
Pend = Phead;
//没有下面的语句,当len=0时,就是野指针了
Phead->Pnext = NULL;
if (Phead == NULL)
{
printf("Fail Creat!\n");
exit(-1);
}
这里要注意一下,我们要将头结点Phead的指针域置位空指针,万一不创造结点,即len=0,那么后续进行操作Phead->Pnext就是操作野指针了
有了上面这些,我们最后只需要将新节点挂到当前最后一个结点上面就OK了
//当前结点存入数据
Pnew->data = data;
//前一个结点指针指向这个节点
Pend->Pnext = Pnew;
//此结点当前为尾结点,指针域置空
Pnew->Pnext = NULL;
//尾结点置为当前结点
Pend = Pnew;
单链表的遍历
当我们创建完一个链表后,我们不能确定自己创建的链表是否存在问题,而最好的判断方法就是将链表里的每一个结点的元素拿出来一遍,然后输出到屏幕。而依照链表的顺序,依次访问每一个结点就叫做链表的遍历。
下面我们来尝试写一下怎么遍历我们的链表:
首先,我们想要访问每一个结点,自然需要一个指针变量,这个指针变量指向我们当前访问的结点,然后按照单链表的顺序,这个指针变量也会一直走,指向下一个结点,直到访问到最后一个结点。
这里有两个问题,1.什么时候是访问到最后一个结点。2.指针变量如何指向下一个结点。
首先我们来解决第2个问题,如何将指针前移。我们知道,当指针指向当前结点A时,那么指针指向的节点的指针域里的指针指向下一个结点,所以我们只需要将其指针域里的指针值赋给遍历指针即可。
Pnow = Pnow->Pnext;
然后我们来看第一个问题,什么时候停止,因为我们知道,尾结点的指针域指向空,所以我们可以以此来判断是否已经到达尾结点。下面我们看看如何实现:
Pnode Pnow = NULL;
Pnow = Phead->Pnext;
while(Pnow!=NULL)
{
printf("%d ",Pnow->data);
Pnow = Pnow->Pnext;
}
这里的判断也可以有另一种情况:
Pnode Pnow = NULL;
//Pnow = Phead->Pnext;
while(Pnow->Pnext!=NULL)
{
Pnow = Pnow->Pnext;
printf("%d ",Pnow->data);
}
两者的不同却决于Pnow一开始的位置不同。
单链表的长度
如何计算单链表的长度,这个问题其实和遍历链表类似,我们只需将遍历中的每一次输出改为变量自增,最后就可以得出结果。
//计算链表长度
int flag = 0;
Pnode Pnow = NULL;
Pnow = Phead->Pnext;
while (Pnow != NULL)
{
++flag;
Pnow = Pnow->Pnext;
}
单链表非空的判断
链表判断非空,实际上就是看头结点的指针域是否为NULL,这个没什么好说的。
//判断链表是否为空
if (NULL == Phead->Pnext)
printf("Empty List!\n");
else
printf("Not Empty List!\n");
单链表结点的插入
首先,我们来设想一下,如果你正要插入一个长长要按序号排的队伍中,你会怎么做,是不是要找到你自己的序号对应的位置,然后找到你前面的人,跟着你前面的人,然后你现在后面的人是不是就是原来是不是就是跟在你前面的人后面,现在跟在你后面了?
根据这个思路,我们首先确定要插入一个结点,需要哪些信息:
1.你自己的序号,也就是要插入的位置
2.你自己,对应的就是新的结点
3.队伍,对应着链表
所以,我们先确定这三样:
void insert_list(Pnode Phead)//头结点,队伍
{
int data = 0,loct=0;
printf("请输入要插入的数据:>\n");//你自己
scanf("%d", &data);
printf("请输入要插入哪个结点前:>\n");//你的序号
scanf("%d", &loct);
}
接下来就是如何找到你的位置的问题,在一个队伍中,你要找到自己的序号,是不是一个一个数前面的人,直到找到你的序号,同理我们也采用这样的方法来找:
//找到对应的位置
Pnode Pnow = NULL;
Pnow = Phead;
int i = 0;
while (Pnow != NULL && i < loct - 1)
{
Pnow = Pnow->Pnext;
++i;
}
这里while循环第一个判断条件的意义是:你一直找,找到队尾,都找不到你的序号。第二个条件就是:你要找到你前面的人,他指向的后面就是你的位置,所以找loct-1次。
这里我们要注意,当你找完了,都找不到你的序号,例如:你是7号,但是队伍就3个人,所以无论如何你都找不到你前面的6号(当然,你要是4号是可以直接插到3后面),此时已经到达队伍尾端了。
所以下面要判断你能否插入到这个队伍中:
//判断需要插入的结点位置是否已经越界
if (Pnow == NULL || i > loct-1)
{
printf("Fail!\n");
return;
}
我感觉这里的i>loct-1可以有效防止插入位置为负数。
下面一步就是插入了,就是断开原来前后的关系,将你拼进去而已:
//插入
Pnode Ptmp = NULL;
Pnode Pnew = (Pnode)malloc(sizeof(Node));
if (Pnew == NULL )
{
printf("Fail create!\n");
return;
}
//新结点的数据
Pnew->data = data;
//原来结点指向后面的指针保存起来
Ptmp = Pnow->Pnext ;
//新结点放到原结点后面
Pnow->Pnext = Pnew;
//新结点指向原结点后面
Pnew->Pnext = Ptmp;
单链表结点的删除
理解了单链表结点的插入,那删除自然不成问题,因为它们两者本质都是找到一个特定的位置,然后对这个位置进行相应操作:
这里的删除操作,主要是找到你要删除的结点,将指向此结点的前一个结点的指针,改为指向要删除的结点的后一个结点即可。
//删除
Pnode tmp = NULL;
tmp=Pnow->Pnext;
Pnow->Pnext = Pnow->Pnext->Pnext;
free(tmp);
tmp = NULL;
printf("删除成功\n");
单链表的排序
最后的最后,自然就是单链表如何排序啦,我们知道,我们经常在数组中对数组元素进行排序,我们常用有选择、冒泡等排序方法,类似的,我们可以将这种排序思路利用到链表中。
下面以选择排序的为例:
我们计划从链表中选择一个结点,然后取这个结点后面的每一个元素与这个结点比对,然后将比它小的都和它进行交换,按照这个思路,我们需要两个1指针,一个指向当前结点,一个指向以后的结点,所以类似的,我们可以创建两个指针变量:
Pnode Pnow = NULL;
Pnode P = NULL;
然后就是选择如何进行前移,类似的,我们只需要将这个指针原本指向的结点的下一个结点的地址赋给这个指针即可,而下一个结点的地址就是这个结点的指针域存放的指针所指向的位置,所以我们可以写出选择比较的代码,如下:
for (Pnow = Phead->Pnext; Pnow != NULL; Pnow = Pnow->Pnext)
{
for (P = Pnow->Pnext; P != NULL; P = P->Pnext)
{
if (Pnow->data > P->data)
{
int tmp=0;
tmp = Pnow->data;
Pnow->data = P->data;
P->data = tmp;
}
}
}