链表操作实例
该实例为:不断输入学生信息(学生姓名,性别,年龄,分数)后,打印输出所有的结果。其中用到了链表的初始化,创建,长度计算,增加节点,倒序链表内容等。这里简单介绍几个重要的链表操作函数。
github下载地址:https://github.com/Edroid/student_linklist
一 简要了解
先简单了解一下链表的基础知识
什么是链表,书上给出的定义是:链表由一系列节点(链表中每一个元素称为节点)组成,节点可以在运行时动态生成。每个节点包括两个部分:一个是存储
数据元素
的数据域,另一个是存储下一个节点地址的指针域。如图:
我们首先定义一个结构体类型 link_note_t
typedef student_t Item;
typedef struct link_node_s
{
Item item;
struct link_node_s *next;
} link_node_t; /*--- end of struct link_node_s ---*/
下面我们先来弄清楚几个变量
link_node_t head; //head为结构体变量,其存放的是结构体内容,我们假设其地址为0x0001
link_node_t *head1; //head1 为结构体指针变量,其存放的是结构体的地址(0x0001),假设其本身地址为0x0008
link_node_t **head2; //head2为结构体指针的指针,其存放的是结构体指针变量的地址(0x0008),假设其本身地址为0x000b。
二,链表操作实例
1.统计链表长度,即链表中节点的个数。
int list_item_count(link_node_t *head) //传入形参为指向结构体的指针
{
int count = 0;
link_node_t *pnode = head;
if(!head) //异常处理:如果链表为空,直接返回0
return 0;
while(pnode != NULL)//链表不为空时,计算并返回count的值
{
++count;
pnode = pnode->next;
}
return count;
}
2.增加节点
向链表中添加新的节点,该其指针域的指针指向的是新的结构体变量
int list_add_item(link_node_t **head, Item item) //传入形参为指向节点的指针和新的结构体变量中的内容
{
link_node_t *pnew; //定义新的结构体指针用来指向新的结构体
link_node_t *pnode = *head; //定义一个结构体指针用来找到尾节点
pnew = (link_node_t *)malloc(sizeof(*pnew));//开辟新的内存空间用来存放新的节点
if(pnew == NULL)
return -1;
memset(pnew, 0, sizeof(*pnew));//清空pnew,Memset 用来对一段内存空间全部设置为某个字符,一般用在对定义的字符串进行初始化为‘ ’或‘/0’;
pnew->item = item;
if(NULL == *head) //如果第一个节点为空,则将头节点作为新的节点
{
*head = pnew;
}
else //否则,找到链表中最后一个节点并设置其为新的节点
{
while(pnode->next != NULL)
{
pnode = pnode->next;
}
pnode->next = pnew;
}
return 0;
}
3.对链表中节点使用函数
void list_traverse(link_node_t *head, void (*pfunc)(Item item)) //传入形参为节点和函数指针
{
link_node_t *pnode = head;
while(pnode != NULL)
{
pfunc(pnode->item);
pnode = pnode->next;
}
}
说明:void (*pfunc)(Item item)为函数指针,该函数返回值为空,函数形参为结构体Item中的item
4.倒序链表中的内容
void link_reverse(link_node_t **head)
{
link_node_t *revsd_ptr; /* Last one time reversed linker pointer*/
link_node_t *cur_ptr; /* Currently need reverse node pointer*/
link_node_t *next_ptr; /* Next need revers node pointer*/
revsd_ptr = NULL;
cur_ptr = *head;
while( cur_ptr != NULL)
{
/* first circulating second circulating
* Step1:
* revsd
*/ cur next revsd cur next
* | | | | |
* NULL A -> B -> C -> D -> E NULL <- A B -> C -> D -> E
next_ptr = cur_ptr->next;
/* Step2:
* revsd cur next revsd cur next
* | | | | |
* NULL <- A B -> C -> D -> E NULL <- A <- B C -> D -> E
*/
cur_ptr->next = revsd_ptr;
/* Step3:
* revsd cur/next revsd cur/next
* | | | |
* NULL <- A B -> C -> D -> E NULL <- A <- B C -> D -> E
*/
revsd_ptr = cur_ptr;
cur_ptr = next_ptr;
}
5.销毁链表
int list_destroy(link_node_t **head)
{
link_node_t *psave;
while( *head )//当节点不为NULL时
{
psave = (*head)->next;
free(*head); //释放内存
*head = psave;
}
}
关于什么时候形参为节点(link_note_t *head),什么时候形参为节点地址(link_note_t **head) 的问题:
刚开始学链表的时候最难理解的地方就是形参的确定,不妨对比一下经典的swap函数
void swap(int *a, int *b)
{
int c;
c = *a;
*a = *b;
*b =*a;
}
int main(void)
{
int a = 2,b = 3;
swap(&a,&b);
printf("%d,%d\n",a,b)
}
只有给swap传入a, b 的地址的时候才能真正改变其内容本身,如果只是传入变量a,b本身,swap函数中改变的是形参也就是swap中的局部变量,并没有改变a,b本身的内容。对比一下就会发现,其实链表的操作也是一样的,如果涉及到链表本身内容的改变,我们需要传入节点的地址,即 link_note_t **head,如果不改变链表内容本身,则传入节点 link_note_t *head。
本人能力有限,以上只是个人学习链表之后的一个总结和看法,难免有错误之处。若发现错误欢迎指正,感激不尽。