链表创建的两种方法
引言
首先讲述了我自己在动态实现数据结构的时候遇到的大坑结构体指针问题,随后就是头插法和尾插法的介绍。
结构体指针
引言
首先需要讲述的是一个让我改了一天半的bug。实际上半天的时候就已经发现了二级指针的问题,但是并没有深究,以为是自己对指针运用不够熟练,就略微重温了一下指针的比较浅层的相关内容,然后第二天的两个程序链栈和链队列都出现了段错误和垃圾值问题,改了半天才发现不仅仅是二级指针的问题,主要其实是结构体指针的问题。在此,对这个坑进行记录,补充知识,以防再犯错。
正文
结构体指针在常见的基础数据结构如:动态数组、链表、栈、队列、树、图中都是很常见的。
关于结构体指针也很简单,就是一个指针,它所指向的类型是结构体类型,通过该指针可以访问结构体变量的内部成员如(*p).age访问结构体变量的age成员,其中 *p=结构体变量。
我们通常通过这种方法在主函数与其他函数之间进行传址操作。通过结构体指针改变结构体变量成员的值,但是有时候并不会注意到它并不能改变结构体变量本身的值,这种错误常发生在动态分配内存上。拿链栈举例,我们试图创建一个链栈,对指向栈顶的指针的初始化的init函数中,我们需要对指向栈顶指针分配内存,此时我们可能会传入在主函数里定义好的头指针。
void init_(Stacklink front)
{
front = (Stacklink)malloc(sizeof(StackNode));
front->pNext = NULL;
}
上面这段代码看似没有任何错误,实际上这样并不能改变改变front的地址,我们想要改变front值应该通过传指针的指针,即二级指针。下面是一个简单例子来证明。
#include <stdio.h>
#include <malloc.h>
typedef struct student{
int age;
int num;
} stu, *p_stu;
void change(p_stu ps)
{
ps = (p_stu)malloc(sizeof(stu));
printf("address in change(),the value of ps:%p\n", ps);
}
int main()
{
p_stu ps = NULL;
printf("address in main(),before changing the value of ps:%p\n", ps);
change(ps);
printf("address in main(),after changing the value of ps:%p\n", ps);
}
结果如下
而通过传结构体指针的指针得到下面结果
#include<stdio.h>
#include<malloc.h>
typedef struct student{
int age;
int num;
} stu, *p_stu;
void change(p_stu* pps)
{
*pps = (p_stu)malloc(sizeof(stu));
printf("address in change(),the value of ps:%p\n", pps);
printf("address in change(),the value of ps:%p\n", *pps);
}
int main()
{
p_stu ps = NULL;
printf("address in main(),the value of ps:%p\n", ps);
change(&ps);
printf("address in main(),after changing the value of ps:%p\n", ps);
}
结果如下
可以发现传入二级指针,成功实现了对结构体指针变量的地址分配。
所以如果想改变结构体指针的地址应该传入二级指针->结构体指针的指针。
头插法
依据上面说到的结构体指针问题,为了增加可读性,这里创建链表都用返回值来实现而不是二级指针
头插法:顾名思义,在头节点后面插入新的节点,每个新产生的节点都会被接在头节点后面。
接下来给出关于头插法的图例解释。
代码中关键的步骤在图中给出了,如果能理解,头插法创建链表就很简单了
#include <stdio.h>
#include <malloc.h>
#include <stdbool.h>
#include <windows.h>
typedef struct NODE{
int data;
struct NODE* pNext;
}node; //定义链表节点的类型
node* create_node(int size); //创建链表
void print_node(node* pHead); //打印链表
int main()
{
node *pHead;
int length;
printf("please put the length of list:");
scanf("%d", &length);
pHead = create_node(length);
print_node(pHead);
}
node* create_node(int size)
{
int i,val;
node *phead = (node *)malloc(sizeof(node)); //创建头节点的“分身”,利用“分身”创建一个链表之后返回分身地址,pHead就构成了一个链表的头节点
phead->pNext = NULL; //要记得置NULL,一开始写的时候忘记了,导致出现了段错误
for (i = 0;i<size; i++) //根据输入的length创建新节点
{
node *pNew = (node *)malloc(sizeof(node));
scanf("%d", &val);
pNew->data = val;
pNew->pNext = phead->pNext; //先让新节点指针指向头节点后继节点
phead->pNext = pNew; //再让头节点指向新节点
}
return phead;
}
void print_node(node *pHead)
{
printf("It's the time to print node.\n");
node *q = pHead->pNext;
while (q != NULL)
{
printf("%d\n", q->data);
q = q->pNext;
}
q = NULL;
}
代码输出如下
可以注意到,这里输出的序列是输入序列的逆序,原因是因为从头指针开始遍历,而每次插在头节点后面的结点是最后创建的结点,符合先进后出,由此我们可以想到链栈应该是用头插法创建链表了。
同时写这段代码的时候,又开始犯创建之后指针不初始化的错误了,导致尾节点的指针域是个野指针,循环无法结束。关于指针的错误真的是很常见而且很难找,所以建议大家都建立指针之后先根据要求置NULL,再进行操作。
尾插法
尾插法:每个新节点都插在表尾,因此叫做尾插法。
#include <stdio.h>
#include <malloc.h>
#include <stdbool.h>
#include <windows.h>
typedef struct NODE{
int data;
struct NODE* pNext;
}node; //定义链表节点的类型
node* create_node(int size); //创建链表
void print_node(node* pHead); //打印链表
int main()
{
node *pHead;
int length;
printf("please put the length of list:");
scanf("%d", &length);
pHead = create_node(length);
print_node(pHead);
}
node* create_node(int size)
{
int i,val;
node *phead = (node *)malloc(sizeof(node)); //创建头节点的“分身”,利用“分身”创建一个链表之后返回分身地址,pHead就构成了一个链表的头节点
phead->pNext = NULL;
node *p = phead;
for (i = 0;i<size; i++) //根据输入的length创建新节点
{
node *pNew = (node *)malloc(sizeof(node));
scanf("%d", &val);
pNew->data = val;
p->pNext = pNew; //先让新节点指针指向头节点后继节点
p = pNew; //再让头节点指向新节点
}
p->pNext = NULL;//尾节点要置NULL否则野指针循环无法结束
return phead;
}
void print_node(node *pHead)
{
printf("It's the time to print node.\n");
node *q = pHead->pNext;
while (q != NULL)
{
printf("%d\n", q->data);
q = q->pNext;
}
q = NULL;
}
可以发现这里的链表输出是正序的,符合先进先出,可以用来构造链式队列。
总结
1.头插法是利用头节点的指针域始终指向头节点的后继节点的特点,来对新插入的节点完成前驱后继节点的设置,链表内数据顺序是与输入顺序逆序的。
2.尾插法是头节点二号分身来控制新节点的插入位置,链表内数据顺序是正序的。
3.依据尾插法,当我们需要对整个链表进行操作的时候通常创建一个新的节点,让它与头节点相等,来实现遍历。