1 链表的概述
链表就是用锁链连接起来的,锁链指的是指针,表指的是存放数据的节点。链表是由若干个节点组成,各个节点的结构完全类似,都是由有效数据和指针两部分组成,有效数据区用来存储有效数据信息,而指针用来指向链表的前一个或者后一个节点,链表就是利用指针将各个节点进行串联起来的链式存储的线性表。
链表与数组的比较
链表的优点是操作灵活,插入删除效率很高,但是缺点是需要额外分配存放节点地址的空间,而且操作繁琐;
数组的优点是操作简单,易于理解,而且不需要开辟额外的空间,但是在数组中间进行插入和删除的操作效率低。
2 单链表
2.1 单链表的结构
单链表的创建到使用的步骤:
(1)创建空的单链表,比如可以定义一个creat_node()函数来创建第一个节点;
(2)操作单链表(增、删、改、查、排),比如插入操作,定义一个insert_tail()函数来向链表(或者节点)的后面追加新节点;
(3)销毁单链表,比如定义一个destory_list()函数,用于销毁链表。
2.2 单链表的节点构成
在C语言中的构建方法就是定义一个结构体
struct node
{
int data;
struct node *pnext;
};
结构体中的两个元素分别是节点的有效数据和指针。将数据定义为int类型,结构体中的指针为struct node * 类型,pnext指针指向的是下一节点空间。定义的结构体类型,本身并没有变量生成,也不占用内存。
使用堆内存创建一个节点
形成链表的特点:必须需要多少就有多少,必须可以随意删除和释放。
(1)申请堆内存并检查申请内存是否成功,申请的空间大小为节点结构体类型规定的大小,刚申请的新内存就是一个新节点;
(2)清零刚申请的堆内存空间;
(3)向新节点写入有效数据;
(4)初始化指针为NULL。
伪代码的实现:
struct node * creat_node(int data)
{
struct node *p =(struct node*)malloc(sizeof (struct node));//申请一个节点的空间大小
if(NULL == p)//检查申请内存是否成功
{
printf ("malloc errror.\n");
return NULL;
}
bzero(p, sizeof(struct node ));//将 sizeof(struct node )个字节清0
p->data = data;
p->pnext = NULL;
return p;
}
链表的头指针
头指针并不是节点,而是一个普通指针变量,占4个字节,头指针的类型是struct node *类型,所以它能指向链表的节点。
2.3 构建一个简单的链表
(1)定义头指针
(2)创建一个节点,并将头指针指向一个节点
(3)接着创建节点,并将创建的节点从前一个节点的尾部插入进来
(4)以此类推,需要多少数据便创建多少个节点,最终形成链表
2.4 从单链表尾部插入节点
(1)找到链表的最后一个节点;
(2)将新的节点和原来的最后一个节点连接起来。
void insert_tail(struct node *ph, struct node *new)
{
struct node *p = ph;
while (NULL != p->pnext)
{
p = p->pnext;
}
p->pnext = new;
}
函数的参数为链表的头指针ph和要插入的新节点的首地址。第一步的while循环用来找到最后一个节点的首地址,第二步的作用是将新节点和原来的最后一个节点连接起来。
构建一个简单的链表
#include <stdio.h>
#include <strings.h>
#include <stdlib.h>
struct node//构建单链表节点
{
int data;
struct node *pnext;
};
struct node *create_node(int data);
void insert_tail(struct node *ph, struct node *new);
int main(void)
{
struct node *pheader = create_node(1);
insert_tail(pheader, create_node(2));
insert_tail(pheader, create_node(3));
printf("node1 data: %d.\n",pheader->data);
printf("node2 data: %d.\n",pheader->pnext->data);
printf("node3 data: %d.\n",pheader->pnext->pnext->data);
return 0;
}
头节点
头节点有两个特点:
(1)紧跟在头指针后面
(2)头节点的数据部分是空的(或者存链表的节点数),指针部分指向第一个有效节点
2.5 从单链表头部插入节点
(1)新节点的pnext指向原来原来的第一个节点的首地址,即新节点和原来的第一个节点相连;
(2)头节点的pnext指向新节点的首地址,即头节点和新节点相连。
简单地来讲就是先连接尾巴,再连接头部。
伪代码实现如下:
void insert_head(struct node *ph, struct node *new)
{
new -> pnext = ph -> pnext;
ph -> pnext = new;
}
这两个步骤的顺序不可交换,若将头节点pnext指针指向新节点的首地址,当我们想要执行第一步时原来的第一个有效节点的地址已经丢失了,第一步就做不下去了。
箭头非指向
在C语言中,箭头->是用指针的方式来访问结构体中的某个成员,链表中节点的连接过程和程序中的箭头没有关系,链表中的节点是通过指针指向来连接的,编程中表现为给指针变量赋值,实质是把后一个节点的首地址赋值给前一个节点的pnext元素。
2.6 遍历单链表
遍历的方法是,从头指针+头节点开始,顺着链表连接指针一次访问链表的各个节点,取出当前访问节点的数据,然后再访问洗衣歌节点,知道最后一节点结束返回。
遍历过程分析
(1)指针p访问第一个有效节点并判断此节点是否是尾节点,取出数据,指针p移动到下一个节点;
(2)判断当前节点是否是尾节点,取出数据,移动到下一个节点;
(3)判断当前节点是否是尾节点,若是,取出数据,停止遍历。
代码分析
//遍历单链表,ph为指向单链表的头指针,将遍历的节点数据打印出来
void_list_for_each_1(struct node *ph)
{
struct node *p = ph -> pnext;//p直接走到第一个节点
printf("---------begin----------\n");
while (NULL != p -> pnext)//判断是否为最后一个节点
{
printf ("node data:%d.\n",p -> data);
p = p -> pnext;//走到下一个节点,也就是循环增量
}
printf ("node data:%d.\n",p -> data);
printf("---------end----------\n");
}
结束while循环后还要打印一次p -> data是因为当p走到最后一个节点时,p -> pnext已经等于NULL,不会进入循环体,因此尾节点的data并不会打印出来。
void_list_for_each_2(struct node *ph)
{
struct node *p = ph ;
printf("---------begin----------\n");
while (NULL != p -> pnext)
{
printf ("node data:%d.\n",p -> data);
}
printf("---------end----------\n");
}
若链表中没有头节点不能使用该遍历算法,因为会漏掉第一个节点的有效数据。
2.7 删除单链表的节点
情况一:
删除的节点不是尾节点
(1)把删除节点的前一个节点的pnext指针指向待删除节点的后一节点的首地址(这个节点从链表中摘除);
(2)对这个被摘除的节点进行free操作,释放内存
情况二:
删除的节点是尾节点
(1)把待删除尾节点的前一节点的pnext指针指向NULL(原来的尾节点前面的一个节点变成新的尾节点);
(2)对摘除的节点进行free操作,释放内存。
为什么要释放内存
程序在遍历链表后就结束返回,还没释放的内存会被自动释放。如果删除节点中没有free,整个程序中频繁的添加或者删除节点,会出现吃内存的现象。
代码实现
从链表中ph中删除节点,待删除的节点的特征是数据区等于data
int delete_node (struct *ph, int data)
{
struct node *p = ph;//指向当前节点
struct node *pprev = NULL;//指向当前节点的前一节点
while (NULL != p -> pnext)//遍历,走到尾节点退出循环
{
pprev = p;//跟随p移动,指向p的前一个节点
p = p -> pnext;//走到下一个节点
if (p -> data = data)//走到要删除的节点
{
if (NULL == p -> pnext)//尾节点
{
pprev -> pnext = NULL;//摘除尾节点
free(p);//释放摘除的节点的内存
}
else;
{
pprev -> pnext = p -> pnext;//摘除要删除的节点
free (p); //释放摘除的节点的内存
}
return 0;//删除节点成功,函数返回
}
}
printf("没有需要删除的节点.\n");
return -1;
}
2.8 单链表的逆序
首先遍历原链表,然后将原链表的头指针和头节点作为新链表的头指针和头节点,原链表中的有效节点挨个一次取出,采用头插入法插入新的链表中即可。链表逆序=遍历+头插入。
代码实现
将ph指向的链表逆序
int reverse_node (struct node *ph)
{
struct node *p = ph -> pnext;//p指向第一个有效节点
struct node *pback ;//保存当前节点的后一节点地址
//当链表没有有效节点或者只有一个有效节点时,逆序不用做任何操作
if ((NULL = p) || (NULL == p -> pnext))
return;
while (NULL != p -> pnext)//遍历
{
pback = p -> pnext;//保存p节点后面一个节点地址
if (p == ph -> pnext)//原链表第一个有效节点
{
p -> pnext = NULL;//头插入之尾部连接
else//原链表的非第一个有效节点
{
p -> pnext = ph -> pnext;//头插入之尾部连接
}
p -> pnext = p;//头插入之头部连接
p = pback;//指针p走到下一个节点
}
insert_head(ph,p);
}
原链表中第一个有效节点逆序后变成了尾节点,它的pnext指针指向NULL。pback指针的作用是,在把当前遍历到的节点头插入到新的链表之前,保存下一个节点的地址,否则在当前节点插入新链表后,下一节点的地址就会丢失。
在遍历到最后一个节点时,尾节点不满足while循环条件,因此要在循环结束后手动将尾节点头插入到新链表中。
2 双链表
双链表是有两个遍历方向的链表。
单链表=有效数据+指针(指针指向后一个节点)
双链表=有效数据+两个指针(分别指向前一个节点和后一个节点)
2.1 双链表插入节点
2.1.1 双链表尾部插入节点
//insert_tail(待插入的链表,新节点)
void insert_tail(struct node *ph, struct node *new)
{
//第一步:找到链表的尾节点
struct node *p = ph;
while ( NULL != p -> pnext)
{
p = p -> pnext;
}
//第二步:将新节点接到链表的尾节点后面成为新的尾节点
//(1)原来的尾节点的pnext指针指向新节点的首地址
//(2)新节点的pprev指针指向原来的尾节点的首地址
p -> pnext = new;
new ->pprev = p;
}
2.1.2 双链表头部插入节点
void insert_tail(struct node *ph, struct node *new)
{
new -> pnext = ph -> pnext;//新节点的next指针指向原来的节点1的地址
if (NULL != ph -> pnext)//节点1的prev指向新节点地址
ph -> pnext -> pprev = new;
ph -> pnext = new;//头节点的next指针指向新节点的地址
new -> pprev = ph;//新节点的prev指针指向头节点的地址
}
2.2 遍历双链表
正向遍历
struct node *list_for_each(struct node *ph)
{
struct node *p = ph ;
while (NULL == p )
{
return NULL;
}
while (NULL != p -> pnext)
{
p = p -> pnext;
printf ("data=%d.\n",p -> data);
}
return p;
}
正向遍历和单链表遍历相同,此处不再赘述过程。
逆向遍历
#include<stdio.h>
#include<stdlib.h>
struct node
{
int data;
struct node *pprev;
struct node *pnext;
};
void list_for_each_reverse(struct node *ptail)
{
struct node *p = ptail;
while (NULL != p -> pprev)
{
printf ("data=%d.\n",p -> data);
p = p -> pprev
}
}
int main (void)
{
struct node *pheader = create_node(0);
insert_tail(pheader,create_node(11));
insert_tail(pheader,create_node(12));
insert_tail(pheader,create_node(13));
printf("正向遍历:\n");
struct node *ptail = list_for_each(pheader)
printf("反向遍历:\n");
list_for_each_reverse(ptail);
return 0;
}
list_for_each_reverse()函数接收一个尾节点的指针作为参数,进行逆向遍历,逻辑和正向遍历差不多,通过p = p -> pprev来向前移动,依次访问节点。main函数创建了一个包含三个有效节点的链表,然后正向、逆向遍历。
2.3 删除双链表的节点
情况一:
要删除尾节点
需要断开(1)和(2)这两条链接,然后释放free(p)就完成了尾节点的删除。用p->pprev->pnext=NULL;这条语句就断开了图中的链接(1)。用p->pprev=NULL断开图中链接(2),因为最终要释放尾节点,所以第二条语句可以省略。
情况二:
要删除的不是尾节点
要删除节点1就要断开(1)(2)(3)(4)这四条指针的链接,然后释放free(p)
p->pprev->pnext=p->pnext;前一个节点的pnext指向一个节点的首地址;
p->pprev=NULL;断开连接(2);
p->pnext=NULL;断开连接(3);
p->pnext->pprev=p->pprev后一个节点的pprev指向前一个节点的首地址;
同样因为释放free(p),所以第二步和第三步可以省略。
代码实现
int delete_node(struct node *ph, int data)
{
struct node *p = ph;
if(NULL == p)
return -1;
while (NULL != p -> pnext)
{
p = p -> pnext;
if (p -> data == data)
{
if (NULL == p -> pnext)
{
p -> pnext ->pnext = NULL;
}
else
{
p -> pprev -> pnext = p -> pnext;
p -> pnext-> pprev = p -> pprev;
}
free (p);
return 0;
}
}
printf ("未找到要删除的节点.\n")
return -1;
}