要想理解链表,一定得理解指针。链表的本质就是指针。
关于指针的理解,小白可以看这篇文章,比较容易理解。https://blog.csdn.net/u014095878/article/details/104559879
要想学习指针,第一步,就是学会接受它,不要觉得它很难。第二步,要立体的去思考,要想着这个指针里面装了什么东西,而不是简单的给这个变量赋值。
创建单链表有四种情况:不带头节点的头插法,不带头节点的尾插法,带头节点的头插法,带头节点的尾插法。
看完下面我的分析。你就可以掌握单向链表的创建了。
先来看一个不带头节点的尾插法的实例:
#include <stdio.h>
#include <malloc.h>
#include<iostream>
using namespace std;
typedef int ElemType;
typedef struct Node_s //定义链表节点的结构体
{
ElemType data; //数据域
struct Node_s* next; //指针域
}Node, * LinkList; //Node相当于struct Node_s Linklist相当于struct Node_s *
int Output_L(LinkList head) //输出不带头节点的单链表的元素
{
if (!head)
return -1;
LinkList p;
p = head;
while (p != NULL)
{
printf("%d ", p->data);
p = p->next;
}
printf("\n");
return 0;
}
int main()
{
int i;
Node *head = NULL;
Node *tail = NULL;
Node *new_node = NULL;
for(i=0;i<5;i++)
{
new_node = (LinkList)malloc(sizeof(Node));
new_node->data = i+1;
new_node->next = NULL; //将即将要插入的节点打包(数据域放什么,指针域放什么)
if(head == NULL) //如果为空链表
{
head = new_node; //头指针指向第一个节点,头结点
tail = new_node; //尾指针指向第一个节点
}
else
{
tail->next = new_node; //尾指针指向的节点(上一个节点)的指针域指向新插入的节点
tail = new_node; //这两句不能反
}
cout << "i =" << i << ",tail = " << tail << endl;
}
Output_L(head);
}
刚接触这个很难去理解这个链表。我们知道链表是由一个结点一个结点串起来的。也知道一个结点有两部分组成,一个是存储数据元素的数据域,另一个是存储下一个结点地址的指针域。却不知道具体是是怎么实现的,尽管有一堆代码告诉我们实现的方法,我们却难以消化。
刚看到这个代码时,我只理解一句,new_node = (LinkList)malloc(sizeof(Node));这个是申请一块内存。
说说不理解的点。
1.head只赋值了一次,为什么后面打印输出是一个完整的单向链表?
2.为什么tail->next = new_node;赋值之后又赋值tail = new_node; ?
带着这两个疑问,我们重新梳理一下上面的代码。
Node *head = NULL;
Node *tail = NULL;
Node *new_node = NULL;
有3个变量,一个是head 头指针,一个是tail尾指针,一个是new_node不断的申请新内存的指针。
1.第一次进循环来,i=0;我们已经理解了new_node = (LinkList)malloc(sizeof(Node));每循环一次就申请一个新内存,也就新节点的地址;
2.new_node->data = i+1;给数据域赋值。i=0的时候,这个时候我们知道了第一个结点的地址是什么,地址里存放的数据域是什么,但是还不知道下一个结点的地址。
if(head == NULL) //如果为空链表
{
head = new_node; //头指针指向第一个节点,头结点
tail = new_node; //尾指针指向第一个节点
}
3.知道了第一个结点的地址,也就是头结点,赶紧把这个地址赋值给head(head = new_node;)。
我们疑惑的问题点到了,为什么只赋值一次?
因为,头结点赋值了之后,就不必再变了。后面变化的一直是tail尾指针,因为你不断的往后面插入数据。所以,尾指针是一直变化的。
这里我们也看到了,tail第一次跟head指向的地址一样(tail = new_node;)。
这个时候第一个结点的数据域知道了,但是指针域还不知道,head->next = null,tail->next = null,new_node->next = null,还不知道下一个地址在哪里。
4.紧接着,第二次进来,i= 1,new_node又申请了新的内存地址,即第二个结点。
new_node->data = i+1 =2;这个时候tail还是指向第一个地址(头结点),第一个地址存放的数据tail->data=1,tail->next=null.
所以这个时候,给tail->next赋值,tail->next = new_node,
把第一个结点的指针指向第二个结点,这个时候就串起来链表了。但是只有两个结点的链表。
后面是我们的第二个问题点,为什么又赋值tail = new_node; 因为后面还要继续往后面插入数据。
tail已经完成了第一次两个结点的串联,tail这个时候要移到第二个结点来,它要搬家了。
head头结点地址不变,tail从头结点地址移到第二个结点的地址,专业术语是,指向第二个结点的地址。
else
{
tail->next = new_node; //尾指针指向的节点(上一个节点)的指针域指向新插入的节点
tail = new_node; //这两句不能反
}
如果还不理解的,可以看看这段代码。我们知道指针就是存地址的嘛。
int i = 1,j=2;
int *p = &i;
cout<<"p的地址:"<<p<<endl;
p = &j;
cout<<"p的地址:"<<p<<endl;
指针p的地址从指向i的地址,到第二次指向j的地址。
tail从第一个结点的地址,指向第二个结点的地址。结点地址里存着数据data和一个地址,这个地址指向下一个结点的地址。
后面的循环,你可以自己在草稿纸里演算,演算成功了,你就掌握了这个链表的创建了。
如果你还想了解单链表的其他的不带头节点的头插法,带头节点的头插法,带头节点的尾插法,请参看下面这位博主的这篇文章
https://blog.csdn.net/TAlice/article/details/82112921。
感谢这位博主的介绍。感谢C/C++小白基础交流群里@雁的指导帮助。希望你们能完全掌握链表。