这两周做的两个项目都与链表操作有关,一开始接触链表的时候不是很理解,经过了两个项目的学习和琢磨对于链表的操作比以前理解的更透彻一点。
链表是链式存储的线性表。根据指针域的不同,链表分为单向链表、双向链表、循环链表。目前我只掌握了单向链表的原理和相关的操作,重点来说一下单向链表。
单向链表可以这样来理解,单向链表中的每个元素包含两个域,一个指针域,一个值域,这样的元素就是我们所说的节点,每个节点内包含一个指针,这个指针指向下一个节点,是链表中的“链”,这个链的作用就是把孤立的节点链接起来,最后一个节点的指针指向NULL。节点中另外一个域就是值域,值域就是这个节点的值,是这个节点的本质,比如,如果这个链表是学生成绩的链表,这个节点的值就是学生的姓名,学号,成绩等等,这个值不一定是一个可以有很多。节点中存放这些值的也是变量,可以是int型、float型、字符串、数组等等,如下图所示就是一个单项链表。
链表的主要操作有创建链表,插入新节点,删除结点,打印链表,查找链表中的节点。
创建链表分为两种方式,一种是从文件中读进来插入链表,一种是通过不断的从外部输入来创建链表。无论是那一种创建链表的方式,核心就是循环插入。循环就是从链表的头一直往下走,从头节点到下一个节点一直往后走。插入就是赋值,把节点的信息从外部输入或者是从文件中读出来赋值给节点内的变量。比如,先定义第1个节点,这个节点是占了内存的,然后将从外部输入的或者文件中读取的值赋值给节点中的变量,这样这个节点就是一个有内容的结点,是一个有意义的结点,这就是这个节点值域,然后再定义一个节点2,让节点1中的指针指向结点2,这样节点和节点2就通过这个指针链接起来了。这里要注意的是,节点2也是占内存的,但是目前节点2内没有存放有意义的值,同样的将新从外部的取得的值赋值给节点2,赋值只有节点2也是一个有意义有内容的节点,依次在创建节点3,这样一个链表就建成了,需要注意的是最后一个节点的指针指向空。
上边讲到的是链表操作的原理,实际操作的时候还有注意下边的问题,首先,这个链表的头怎么确定。第二,如果一个链表有100个节点,那就要定义100个节点,显然这个变量名是要循环使用的,怎么样保证不重复使用呢?第三,这个链表怎么向后挪。
这三个问题其实是有相关性的,这个链表的插入是往后进行的,但是往后进行的时候不能把前边的东西覆盖掉,就是头固定了之后,往后挪。举个简单的例子就像编绳子一样,绳子的的一头编好了,固定下来,只需要人的手向后边挪边编就可以了,到结尾打个结就可以了。这样理解之后,我们的思路就简单了。先定义一个头结点,这个头结点定义为全局变量,无论这个头节点中是否有内容,它都是固定的,也就是这个绳子的头固定了。头定下来之后,就需要“一双手”向后编绳子,这一双手就是两个结构体的指针p,q。从头节点开始编就是从头节点head开始插入数据,先将指针head赋值给指针p,那么修改对于p的操作就相当于是对于head的操作,而且,head不用动了,就定下来了。在head后插入节点,只需要将指针p向后挪就可以了,p是从头节点往后移动的,这样保证了已经插入的节点不会被覆盖掉。q节点才是真正要插入的节点,定义一个q节点,然后将要插入链表的值赋给q节点,q节点的指针指向空,这样q节点就是一个孤立独立的结点,让p结点的指针指向q,那么这个q节点就插入这个链表了,q节点继续定义新的孤立结点,存储外部数据,这样循环起来,链表就创建成功了。
总结来说就是,head头指针不动,q结构体指针是暂时存储外部数据,是个孤立的节点,p指针从head开始往后移动不断的将q节点插入到head为头的这个链表中。
具体代码如下:
先是节点结构的定义
typedef struct student * Student; static Student head; /*链表结点结构 *ID: 学生编号 *name: 学生姓名 *chgrade: 学生语文成绩 *mathgrade:学生数学成绩 *avegrade:学生平均成绩 *next: 下一个结点指针 * */ struct student { Student next; int ID; char name[MAX]; float chgrade; float mathgrade; float avegrade; };
int insert() { int ID; char name[MAX]; float chgrade; float mathgrade; float avegrade; int i; printf("Please input the new info (ID name chgrade mathgrade avegrade) : "); scanf("%d%s%f%f%f", &ID, &name, &chgrade, &mathgrade, &avegrade); printf("input %d %s %f %f %f successfully ", ID, name ,chgrade, mathgrade, avegrade); printf(" "); Student p, q; p = head; if(p != NULL) { while(p->next != NULL) { p = p->next; } } q = (Student)malloc(sizeof(struct student)); if(q == NULL) return -1; q->next = NULL; q->ID = ID; for(i = 0; i < MAX; i++) { q->name[i] = name[i]; } q->chgrade = chgrade; q->mathgrade = mathgrade; q->avegrade = avegrade; if(p == NULL) { head = q; return 1; } p->next = q; }
创建链表理解之后,对于删除节点和插入节点就很好理解了。都是要先遍历整个链表找到要删除的结点或者要插入的位置,从head开始,一个p指针向后移动,移动到每个结点的时候会判断结点的值,是否跟要删除的结点或者要插入的位置相等,如果等那就是找到了,如果不等,p指针继续向后挪,直到找到要删除的节点或者要插入的位置。找到之后就是删除和插入的操作了,删除节点的原理就是让要删除的结点的前一个节点的指针直接指向要删除结点的后一个节点,这样这个要删除的结点就不在链表里,成为了一个孤立的结点,释放这个节点就可以了。类似的,插入一个结点先创建一个孤立的节点,孤立节点的前边先插入链表就是让插入位置前的节点的指针指向该孤立节点,后边插入链表就是让孤立节点的指针指向插入位置后边的节点,这样插入就完成了。下面程序是按照ID删除节点
int del_ID(struct student *head, int ID) { struct student *p1; struct student *p2; if(head == NULL) { printf("no grades "); return head; } p1 = head; while(p1->ID != ID && p1->next != NULL) { p2 = p1; p1 = p1->next; } if(p1->ID == ID) { if(p1 == head) { head = p1->next; } else { p2->next = p1->next; } free(p1); p1 = NULL; printf("delete %d success ", ID); } else { printf("%d not been found! "); } return head; }
查找节点就更简单了,在插入和删除操作的时候提到了查找节点,那就是查找节点的办法,不同的是,查找节点操作,找到节点之后,只需要将查到节点的值打印出来就可以了。