在学习链表的时候,有一些很重要的概念,之前一直没有弄清楚,后来经师父点拨,现在终于有点顿悟了,这些关键的点在百度上都没有人提及,却是我一直很疑惑的一点,所以总结在这里,以免日后遗忘.
在链表中我们基本的操作主要是创建链表,插入链表和删除打印链表等。但是他们是如何运作的,即使看再多的代码,没有理解也是白搭
首先要了解:
- 链表主要是由节点经由内部指针一环扣一环的连接构成;每个节点是由结构体构成;每个结构体中包含自身的数据和指向下一个节点的指针。
typedef struct student
{
char Score;
struct student *next;
}Chain_Node;
- 对于链表来说,最重要的是一定要抓住链表的头(还有尾),因为我们对链表的操作一般都是遍历链表。如果没有头则无法遍历链表。所以我们最好在全局变量时,定义一个头指针和尾指针,这样就不会在函数执行完后给释放掉。
Chain_Node *Student_pHead,*Student_pTail;//结构指针,指向结构的初始地址
-
在了解链表的存在是为了在应用数据的时候更加的便捷,因而在创建,插入和删除及其他操作时并不是像百度里面一样,随便写一个就可以。节点的插入、删除及其他操作必须依存条件而存在。这点特别重要,也是我看别人程序时蒙蒙的源头,因为我发现别人程序写的都不一样,却又找不着他们的规律。下面我来仔细的聊聊创建链表的过程,自然就可以明了。
-
创建链表,插入链表和删除链表并不是可以分开的步骤。虽然我们是用不同的函数写不同的作用。但是实际上他们的关系却是紧密联系在一起的。
首先我们在对链表有任何动作之前的第一步一定是创建链表,要不然链表都没有创建,还何谈插入,删除等步骤。但是在创建链表时我又犯了一个错误,因为看到别人写的链表里面都会进行加入节点的过程,有的加到头,有的加到尾,实际上这些都不是创建过程,而是创建链表之后的下一步过程,而且在不同的条件下会有不同的函数。
因而真正意味着创建链表的步骤只有一点,就是给第一个节点分配空间,若分配成功,则把定义的全局变量的头Student_pHead和尾指针Student_pTail 都指向新开辟的节点,然后让尾的指向下一个的指针指向空,这就完成了第一个节点的创建过程。(在分配内存的这个步骤一定要判断系统是否有没有分配内存成功,如果成功了才进行后面的操作,否则就直接退出函数)。
Chain_Node *Creat_new_list(char *new_node)
{
Chain_Node *p_new;//在栈中生成了指针
p_new=(Chain_Node *)malloc(sizeof(Chain_Node));//在堆中分配了内存节点,当把该节点的地址赋给p_new的时候,就是使得栈中的指针指向了堆中分配的内存地址
if(p_new==NULL) return p_new;
p_new->Name=new_node[0];
p_new->Score=new_node[1];
/*其中下面的if是指的创建的过程,但是下面的else并不是创建的过程,而是在创建之后对链表第二个及后面节点的操作,而这些操作都是遵循一定的条件进行的操作,该条件就是指的是生产的要求。*/
if(Student_pHead==NULL)//当头指针指向空的时候,代表还是一个空链表的时候。创建过程就是指的对第一个节点的操作,使得第一个节点即是头结点也是尾节点。使尾节点的指针指向空。
{
Student_pHead=p_new;//使得头指针指向新创建节点的内存地址
Student_pTail=p_new;//使得尾指针指向新创建节点的内存地址,不过p_new里面是有东西的,有Name, Score及指针。
Student_pTail->next=NULL;//上面的两句已经使得头和尾指针指向了具体的内存地址,因为这两个头尾指针就是实实在在的了。因而要把尾里面的指针指向空。
}
else
{
//这里是写得在第二个节点后的条件操作程序
}
return Student_pHead;
}
上面的有个特别重要的观念要一直抓住,就是对于指针变量的指向及存在问题:
申请的全局变量Chain_Node *Student_pHead,Student_pTail;
是存在于静态区;malloc()函数分配的内存存在于堆区;在函数中的临时变量指针p_new存在于栈区。而使用下面的语句使得他们联系在了一起,请见下面列表:
p_new=(Chain_Node )malloc(sizeof(Chain_Node));//在堆中分配了内存节点,当把该节点的地址赋给p_new的时候,就是使得栈中的指针指向了堆中分配的内存地址,而Student_pHead=p_new;Student_pTail=p_new; Student_pTail->next=NULL; 使得Student_pHead指针和* Student_pTail指针从全局变量区指向了与p_new指向的同一个地方,因而把两者结合起来了。
而p_new指针在被使用完释放的时候就没有了。而全局变量Student_pHead指针和 Student_pTail指针则不会被释放,一直指向堆,这样就使得链表头和尾都保留下来了。
- 除了上面第4条的创建过程之后其他的过程(即else的部分)在百度上写的并不是创建过程,他们创建的链表都是有条件的链表,我们不能只是看了后照搬。因为我们自己写程序的时候条件不同,会导致出不同的程序。
条件到底指的是什么呢?就是指的在创建第一个节点之后,后面的节点该怎么操作的原理问题?即我链表创建出来的作用到底是什么?
下面我分几个例子进行说明:
5.1.1. 如果第二个节点的分数比第一个学生的分数高,则把第二个节点放在第一个节点的后面,同时把第二个节点作为尾节点,第一个节点作为头节点。否则第二个节点就放在第一个节点前面作为头节点。
5.1.2. 如果第三个节点的分数比前面两个节点高,则放置为尾节点,如果第三个节点比前面两个节点的分数都低,则放置为头节点。如果第三个节点比其中一个节点小,比另一个节点高,则作为中间的节点。
5.1.3. 在第三个节点之后的节点都是一样的操作需要进行比较才能放置,这个其实在第二个节点之后都是一个插入的过程而不是创建的过程。
5.2. 假若我想建立一条链表,就是每来一个新的节点,我都置为头节点。则我只需要每次一来一个新的节点的时候,建立一个临时变量。把原来的头(第一个节点)赋值给临时变量。在把新的节点赋值给头。再使得新的头的结构指针指向原来的头。这个就建立了连接。
Chain_Node *Creat_new_list(char *new_node)
{
Chain_Node *p_new,*p_head;//在栈中生成了指针
p_new=(Chain_Node *)malloc(sizeof(Chain_Node));
if(p_newNULL) return p_new;
p_new->Name=new_node[0];
if(Student_pHeadNULL)//创建过程
{
Student_pHead=p_new;
Student_pTail=p_new;
Student_pTail->next=NULL;
}
else//这里是写得在第二个节点后的条件操作程序:把新来的信息添加到链表的头,新的节点更新为链表的头
{
p_head=Student_pHead;//这个是把原来的头赋给临时变量
Student_pHead=p_new;//把新的节点赋值给头(这就是为什么上一句要先把原来的头给赋值给临时变量,因为我们要把新的节点赋给头,所以原来的头要先保存)
Student_pHead->next=p_head;//把新的头的指针指向原来的头。这就完成了步骤。
}
return Student_pHead;//返回头
}
5.3 假若我想建立一条链表,就是每来一个新的节点,我都放在最后作为尾节点。则我只需要每次一来一个新的节点的时候进行如下步骤:
- 把原来的尾指针指向新的节点;
- 把新的节点作为尾;
- 再把尾指向空NULL即可。
Chain_Node *Creat_new_list(char *new_node)
{
Chain_Node *p_new;//在栈中生成了新的节点
p_new=(Chain_Node *)malloc(sizeof(Chain_Node));
if(p_newNULL) return p_new;
p_new->Name=new_node[0];
if(Student_pHeadNULL)//创建过程
{
Student_pHead=p_new;
Student_pTail=p_new;
Student_pTail->next=NULL;
}
else//这里是写得在第二个节点后的条件操作程序:把新来的信息添加到链表的头,新的节点更新为链表的头
{
Student_pTail->next=p_new;//这个是把原来的尾指针指向新的节点
Student_pTail=p_new;//因为我们是要把节点加入到最后作为尾节点,则需把新的节点赋值为尾指针。
Student_pTail->next=NULL;//把新的尾指向空。
}
return Student_pHead;//返回头
}
总结:综合上面的5.1-5.3可知,我们有不同的条件,则会有不同的程序,因而不能一概而论。这个过程应该就是一个插入的过程。5.1是插入头节点;5.2是根据情况遍历链表,插入节点(这个我单独来写)。5.3是插入尾节点。该用哪个程序得视不同的要求来写。
- 现在我来写专门的插入函数。以5.2为例,我以学生的分数来编链表,使得链表根据分数从小到大进行排列。这个例子就是链表的精华所在,只要把这个弄清楚了,链表也就这么回事。
方案一、首先来理一理我们写这个程序的思路步骤:
思路:主要是在创建了第一个节点之后,后面加入节点的时候需要按照分数从小到大进行排序。
● 如果每次新加入的节点的分数比头节点的分数还要小,则需要把新的节点的指针指向头节点,把新的节点变为头节点;
● 如果每次新加入的节点的分数比尾节点的分数还要大,则需要把原来旧的尾节点指针指向新的节点;
● 如果每次新加入的节点的分数比头结点小,比尾节点大就需要仔细考虑怎么实现了。假如之前已经输入了 40,80, 90,100然后我新加入的节点分数为85,则按照理论来说,85应该插入80,90的中间。所以在最开始时需要与相邻的两个节点的分数进行比较,得到比前一个大比后一个小就插入在他们的中间。这里最关键的一点就是怎么与相邻的两点进行比较?要解决这个问题,主要是要会设置临时结构体指针变量。目前设置为Chain_Node *p_new,*p_tmp,*p_previous。
1.*p_new:指向的是分配新内存的新的节点。
2.*p_tmp/*p_previous:临时变量,他们的作用需要看下面的框架图来理解:
第一步:首先把头节点Student_pHead赋给p_tmp临时结构体变量
第二步:判断p_tmp如果是空的话,代表连一个节点都没有创建,则不执行while循环。如果p_tmp不为空,则代表至少创建了一个节点。进入while()循环;进入该while循环主要是判断新节点该插入哪个位置。有疑问的话直接运行程序就会清楚了。
完整程序如下:
Chain_Node *Creat_new_list(char n)
{
char i,n;
Chain_Node *p_new,p_tmp,p_previous;//在栈中生成了指针,其中p_tmp和p_previous是临时指针,中间变量
for(i=0;i<n;i++)
{
p_new=(Chain_Node *)malloc(sizeof(Chain_Node));//在堆中分配了内存节点,当把该节点的地址赋给p_new的时候,就是使得栈中的指针指向了堆中分配的内存地址
if(p_newNULL) return p_new;
//输入学生的姓名和学分
printf (“Please input Student’s Name , score: \n”);
scanf("%d", &(p_new->Score)); //录入数据
/其中下面的if是指的创建的过程,但是下面的else并不是创建的过程,而是在创建之后对链表第二个及后面节点的操作,
而这些操作都是遵循一定的条件进行的操作,该条件就是指的是生产的要求。/
if(Student_pHeadNULL)
{
Student_pHead=p_new;
Student_pTail=p_new;
Student_pTail->next=NULL;
}
/*这里是写得在第二个节点的条件操作程序,每次进来一个节点就用分数进行比较,如果分数比前一个节点大比后一个节点小就放在两者中间,
如果新插入的节点分数比头结点小就放置在原来的头节点之前作为头节点。如果分数比尾节点的分数还要大就放置在原来的尾节点后面作为尾节点。
*/
else
{
p_tmp=Student_pHead;//先把头节点赋给临时变量p_tmp,主要是为了遍历链表
while(p_tmp)
{
if(p_new->Score<=p_tmp->Score)//如果新加的节点分数小于头节点,则把新的节点更新为头节点
{
if(p_tmpStudent_pHead)
{
p_new->next=Student_pHead;//新的节点指向原来的头
Student_pHead=p_new;//把新的节点变为新的头节点
break;
}
else
{
p_previous->next=p_new;
p_new->next=p_tmp;
break;
}
}
else
{
if(p_tmp->nextNULL)
{
p_tmp->next=p_new;
p_new->next=NULL;
break;
}
}
p_previous=p_tmp;
p_tmp=p_tmp->next;
}
}
}
return Student_pHead;
}