好耶,开始学链表了。尽管单向链表内容比较简单,对C语言一无所知的蒟蒻还是决定写下来,以帮自己捋清逻辑并供复习使用。
因为是初学者,代码码风很丑,希望大家多多包涵(抱拳),欢迎各路大神的指点!
感谢来自PKU信科的dalao—Lucario的拨冗指点。
简介
链表,顾名思义,是一种链状的结构。类比以前用来建图的前向星算法就比较好理解了。而实际上单向链表的结构比一般的图更加简单,它既不存在双向边和环,也不存在分支,只有一条长长的主链,即:除首尾节点外,每一个节点的出度和入度都是1 (虽然这个性质似乎没啥实用价值23333) 。
我们打个比方。假如现在有一间很大的教室,每一列有很多很多的位置。这让80岁高龄的lgg老师非常头疼,他无法记清楚每个同学的位置,而调皮的同学们显然是不会自己乖乖坐在自己座位上的(事实上同学们是无法自己坐到座位上的)。为了能够成功地给(和)同学们上(扯)课(淡),lgg老师想了一个好办法,他选了一个科代表(神仙glf)并让他坐在第一排,这样lgg老师就知道了坐在第一排的是谁,然后让每个人都记住自己的后桌是谁,而坐在最后一排的同学(蒟蒻lry)则记住自己后面是墙就可以了。现在每次收作业的时候就从科代表开始,每个人把作业交给lgg老师后就扯一嗓子,喊下一个同学上来交。这种方法lgg老师很满意,因为他只需要记住第一排的是glf神仙,其他的事情他就不用管了。
一.链表的优点
根据链表的图示我们可以发现,链表与一维数组非常相似。在检索功能上,一维数组的遍历显然更好写一些;但是在一个动态的过程中,链表不仅可以实现内存的动态分配,在修改维护上也更加优秀。举个例子,当前链长100,假如要在2号节点和3号节点之间插入一个新的节点,我们需要将数组开为101,再把三号节点及其后面所有节点全部向后平移一位。而链表却只需要简单地删边和加边。在上个例子中,可以认为从三号同学开始,后面每个同学的相对位置是没有变的,不需要修改。链表这种结构就是充分利用了这一点,优化了效率。
二.定义节点的数据类型
typedef struct node{
int val;//每个节点的权值
struct node *next; //下一个节点的地址
}linkedlist;
这个好像没啥可说的
三.链表的创建Create
node *Createlist(int n){
node *head,*x,*end;
/*
蒟蒻的写法中并没有为head单独malloc,而是只用一个指针储存头结点的地址
现在主流一点的写法是专门malloc一个虚点来作为head,但它的val并没有被赋值
这样的写法在insert等函数中比较方便,不需特判插入点是否会作为新的头节点
*/
head=(node *)malloc(sizeof(node));
scanf("%d",&head->val);
end=head;
for(int i=1;i<=n-1;i++){
x=(node *)malloc(sizeof(node));
scanf("%d",&x->val);
end->next=x;
end=x;
}
end->next=NULL;//注意别忘了这一步,它是我们遍历的边界,也是后面递归的边界
return head;
}
由于n是传递的参数,所以其内存分配是动态的。这个Createlist 函数是node*类型的函数,它返回的head就是链表中第一个节点的地址。所以主函数里我们应该写为:
node *head=Createlist(n);
四.遍历
之前已经提过,遍历的边界应该是指针指向NULL的时候。
node *pre=head;
while(pre!=NULL){
printf("%d ",pre->val);
pre=pre->next;
}
五.插入新节点 insert
插入一个新节点作为新的第index号节点
node* insertpoint(node *list,int index){
if(index==1){
node *newpoint=(node *)malloc(sizeof(node));
newpoint->next=list;newpoint->val=114514;
list=newpoint;
return list;
}
else{
node *newpoint=(node *)malloc(sizeof(node));
newpoint->val=114514;
node *t,*last=list;
for(int i=1;i<=index-2;i++){
last=last->next;
if(last==NULL){
printf("Error\n");
return list;
}
}
t=last->next;
newpoint->next=t;
last->next=newpoint;
}
return list;
}
main函数中:
int index=read();
list=insertpoint(list,index);
六.删除节点 deletepoint
删除链表中第index号节点
node *deletepoint(node *list,int index){
if(index==1){
node *newlist=list->next;
free(list);
return newlist;
}
else{
node *t,*last=list;
for(int i=1;i<=index-2;i++){
last=last->next;
if(last==NULL){
printf("Error\n");
return list;
}
}
t=last->next;
last->next=t->next;
free(t);
return list;
}
main函数中:
int index=read();
list=deletepoint(list,index);
七.链表的反序 reverse
反序是链表中最重要的操作之一。
递归写法
递归思想是当我处理某节点的时候,我先保证后续节点形成的子链是我已经反序处理好的。对整个链表的反序就可以拆分为两个子问题:处理当前节点,以及处理后面的子链。这个时候应该return 什么东西呢?我们回到上一个例子。现在lgg老师怕坐在最后一排的同学上课摸鱼摸得太多,决定让坐在后面的同学坐到前面来。那么反序过后,现在lgg老师需要知道的是什么呢?他需要知道以前坐最后一排的是哪位同学(lry,危)。所以我们从最后一排开始,不仅要反过来记住自己前面的是哪个同学,还得从后往前传话,把摸鱼者是lry的消息告诉lgg老师。
node *reverse(node *head){
if(head==NULL||head->next==NULL){
return head;
}
node* newhead=reverse(head->next);
head->next->next=head;
head->next=NULL;
return newhead;
}
传话给了glf神仙处后,我们还需要让lgg老师重新记忆一下(因为lgg老师不是一个节点,递归返回只会返回到glf神仙)。所以主函数应该这样写:
head=reverse(head);