链表是个很有意思的东西,从这里开始,编程语言基础结束,数据结构开始了。
身为一个只学过C语言基础的非正式码农,从链表开始似乎是挺不错的,我这么觉得。
抽象的逻辑结构已经在计算机基础课上水过去了,现在能写一写的也只剩下代码了。
(好像又有抄书本的嫌疑呐)
链表顾名思义就是用链子穿起来的表,它有单向和双向之分。目前还在学习中,就先只讲讲单向的啦。
似乎是源于编程语言本身的特性,一般来说,构造链表都是通过诸如结构体,类等方式实现的,那么也就是说,链表的核心是结点,链是由每个结点对外派生的,或者说,链是结点的一部分。
(这正好与数组相反,数组是先有了链(连续内存空间和索引),才能往里塞东西)
这也让我知道,链表不是高级语言里自带的数据结构或者定义类型,需要自己去建立并管理使用。(至少C语言如此,以下的内容也全是建立在C语言上)
自己建立管理的话,那么大概就包括,生成链表,输出链表,插入结点,删除结点,删除链表,以及对链表数据进行各种运算等等。
首先是建立链表
先建立一个带有与结构体类型相同的指针的结构体(这个指针就是用来指向下一个结点的)
struct student { long num; char name[20]; char addr[50]; struct student *next; //This results in a chain of structs }; typedef struct student LIST;
(这里有一个转义,它可以提供很大的方便,C语言提供这么贴心的服务真是太好了)
然后是一个创建链表的函数
LIST *create_list() //This function creates a length-variable chain and returns the head pointer of the chain { LIST *h, *prev, *cur; int i, n; h = NULL; prev = NULL; printf("Please Input The quantity of Nodes: "); scanf("%d", &n); i = 0; while (i<n) { cur = (LIST *)malloc(sizeof(LIST)); //allocate memory space to initialize pointer 'cur' cur->next = NULL; if (h == NULL) h = cur; else prev->next = cur; printf("Please Input The Elements of Node %d:", (i + 1)); scanf("%d %s %s", &cur->num, cur->name, cur->addr); prev = cur; i++; } printf("Infomation Uploaded Successfully! "); return h; }
这里首先建立了三个指针 *h,*prev,*cur。我们之前创建的结构体本身只是一个多个数据的集合而已,这个函数让各个结点通过指针的方式连接起来,形成一条长链,*prev与*cur分别代表previous与current,即前一指针与当前指针,这个函数的流程就是,先构造出相关的结点,然后让*prev和*cur分别指向两个应当链接的结点,让它们连接上去。然后让这两个指针不停向后移动,这样整条链表就成了。而*h代表head,即头指针,指向了这个链表的第一个节点,正是有了它,我们才知道这个链表的存在,它可以说就是这个链表的证明,这个函数的返回值即为这个头指针,而以后对链表进行其他操作,都需要将此头指针作为引路人(实参)。
此处也反映了单向链表的一些性质,与数组相比,单向链表不存在索引,每一个结点仅仅向下联系,也就是说,如果你想访问某个深处的结点,必须从第一个结点开始,一个一个往下寻找,直到找到为止。
而具体的形成过程,用到了malloc函数,这个函数很深层次的东西我也尚未挖掘,在此,我对它的认识即是,它可以分配一块固定大小的内存空间(对于上面的代码则是一个LIST的空间),然后返回这块空间的头指针。与直接建立一个LIST类型的结构体相比,malloc的方式是动态的,它没有变量名,分配的内存空间也可以通过free的方式重新“充公”(后文会提到),并且由于没有变量名,可以直接将其套入循环,易于操作(想象一下如果把malloc换成新建一个结构体会怎样)。
然后就是赋值,成链,移位的过程。空间总是赋给*cur,然后由*cur传出去。
下一步是输出链表中的数据
void disp_list(LIST *h) { LIST *p = h; while (p != NULL) { printf("%-7d %-7s %-7s ", p->num, p->name, p->addr); p = p->next; } printf(" "); }
只需要一个简单的循环。
接下来时重点:插入结点
LIST *insert_node(LIST *h, LIST *s) //*h is the head pointer of the entire list //and *s is the pointer which points to the new node waiting for being inserted { LIST *prev, *cur; //initialize two pointer for the chain which points to the current node and the previous node cur = h; //first let 'cur' points to the head pointer prev = NULL; if (h == NULL) { h = s; s->next = NULL; } else { while ((s->num > cur->num) && (cur->next != NULL)) { prev = cur; cur = cur->next; } if (s->num <= cur->num) { if (h == cur) { h = s; s->next = cur; } else { prev->next = s; s->next = cur; } } else { cur->next = s; s->next = NULL; } } printf(" "); return h; }
两个参数,一个是待插入的链表头指针,另一个是指向待插入结点的指针
这里用到了一些判断,每一个结点中都有许多数据,其中存在一些索引性的,标记性的数据(毕竟并非数组,自带索引,所以只能自建)
在LIST中,num就是索引般的存在。
当然了,前面创立节点的过程是不包含对num合法性的检查的,这里的insert_node函数假定num是从小到大排列的,因此在进行实际运行时,输入的数据也应符合这个要求。
具体来说,就是逐个比较,直到找到一个比s->num更大的cur->num,然后,让prev->next由指向cur转为指向s,而s->next则指向cur,这样就完成了插入过程。
这时候链表相对于数组的优势就体现出来了。链表不仅大小长度是动态的,任意两个连续元素的链接也是动态的,这个“链接”本身可以任意进行操纵,而数组本身索引是固定的,实现插入操作需要移动从插入点到末尾的所有数据。
此外还有一些特殊情况,比如链表本身就是空的啦,插入点在链表头结点前或者末结点后等等,也都要分别考虑。
然后是删除结点
LIST *delete_node(LIST *h, int para)//*h is the head pointer of the list //and 'para' is the parameter to match the 'num' in every nodes to locate the particular node to be deleted { LIST *prev, *cur; prev = NULL; if (h == NULL) { printf("The Chain Is Empty And No Nodes Can Be Deleted. "); return NULL; } cur = h; while (cur->num != para&&cur->next != NULL) { prev = cur; cur = cur->next; } if (cur->num == para) { if (cur == h) h = cur->next; else prev->next = cur->next; free(cur); printf("Node Has Been Deleted. "); } else printf("No Node Matches The Given Parameter or Ordinal. "); return h; }
删除的过程与插入类似,需要注意的是,在断开某一结点与其他结点的联系的之后,应立刻对其进行free操作,释放内存空间。(在指针移位之后,被断开的结点就找不到啦)
而被删除结点的查找工作,也是通过num参数进行的。
最后来一个简单的应用吧
int main() { LIST *head,*p; //head pointer and the pointer which points to the nodes to be deleted int ordinal; //the parameter which will traversal the chain to find the particular node to delete head = create_list(); printf("Serial Name Address "); disp_list(head); printf("Please Input The Node Need To Be Inserted: "); p = (LIST*)malloc(sizeof(LIST)); scanf("%d %s %s", &p->num, p->name, p->addr); head = insert_node(head, p); printf("Serial Name Address "); disp_list(head); printf("Please Insert A Num Which Reflects On A Particular Node: "); scanf("%d", &ordinal); head = delete_node(head, ordinal); if (head != NULL) { printf("Serial Name Address "); disp_list(head); } else printf("No Data Remaining. "); }
下面再介绍一些简单的应用
①多项式求和
#include <stdio.h> #include <stdlib.h> struct item { int exp; float coef; //coefficient struct item *next; }; typedef struct item ITEM; ITEM* create_poly() //create items for a polynomial //without sorting, so polynomial ought to be input at descending arrangement //which means ,the exp must from large to small { ITEM *h = NULL, *prev=NULL, *cur=NULL; int ex; float co; int i = 1; printf("Please Input Coeffient and Exponential %d: ", i); scanf("%f %d", &co, &ex); while (co <-1e-6||co>1e-6)//Caution here!! { i++; cur = (ITEM*)malloc(sizeof(ITEM)); cur->exp = ex; cur->coef = co; cur->next = NULL; //make sure when loop is over,the 'next' of 'cur'(it is also the pointer to the last node) is NULL if (h == NULL) { h = cur; //first loop } else prev->next = cur; //later loop,let 'prev's next' points to this 'cur' prev = cur; //then, let 'cur' cover this 'prev', //and the 'next' of old 'prev' in previous line above now points to this new 'prev' //so the link was set up, and new 'cur' is coming at next loop. printf("Please Input Coeffient and Exponential %d: ", i); scanf("%f %d", &co, &ex); } return h; } void disp_poly(ITEM *h) //display a polynomial and make it similar as a polynomial in form { ITEM *p = h; printf("The Result Is: "); while (p != NULL) { if (p->exp == 0)//to prevent to print "Cx^0" { printf("%.2f", p->coef); } else printf("%fx^%d", p->coef, p->exp); p = p->next; //pointer pushing if (p != NULL) printf("+"); //judge whether to add an '+' after pushing pointer } printf(" "); } ITEM *add_poly(ITEM *poly_h1, ITEM *poly_h2) //Input 2 list and generate a new list and return its head pointer { ITEM *h_add = NULL, *prev_add = NULL, *cur_add = NULL, *p1, *p2; float c_add; int e_add; p1 = poly_h1; p2 = poly_h2; while (p1 != NULL&&p2 != NULL)//confirm when to end it { if (p1->exp > p2->exp) { c_add = p1->coef; e_add = p1->exp; p1 = p1->next; }//if exp1>exp2,then move p1 else if (p1->exp == p2->exp) //multiple 'if' is not the same as 'if,else if,else' chain //for multiple 'if' will continue judging after it satisfy former judgements //and the condition may change in the next judgement //while the 'if,else if,else' chain will only entry first satisfied judgement { c_add = p1->coef + p2->coef; e_add = p1->exp; // p1 = p1->next; p2 = p2->next; }//if exp1=exp2, then add them and move p1&p2 else { c_add = p2->coef; e_add = p2->exp; p2 = p2->next; } if (c_add<-1e-6 || c_add>1e-6) { cur_add = (ITEM*)malloc(sizeof(ITEM)); cur_add->coef = c_add; cur_add->exp = e_add; cur_add->next = NULL; if (h_add == NULL) h_add = cur_add; else prev_add->next = cur_add; prev_add = cur_add; } } while (p1 != NULL) //If one polynomial is over ,then the other will add to the result { c_add = p1->coef; e_add = p1->exp; p1 = p1->next; if (c_add<-1e-6 || c_add>1e-6) { cur_add = (ITEM*)malloc(sizeof(ITEM)); cur_add->coef = c_add; cur_add->exp = e_add; cur_add->next = NULL; if (h_add == NULL) //when p2 is NULL h_add = cur_add; else prev_add->next = cur_add; prev_add = cur_add; } } while (p2 != NULL) //If one polynomial is over ,then the other will add to the result { c_add = p2->coef; e_add = p2->exp; p2 = p2->next; if (c_add<-1e-6 || c_add>1e-6) { cur_add = (ITEM*)malloc(sizeof(ITEM)); cur_add->coef = c_add; cur_add->exp = e_add; cur_add->next = NULL; if (h_add == NULL) //when p2 is NULL h_add = cur_add; else prev_add->next = cur_add; prev_add = cur_add; } } return h_add; } void delete_poly(ITEM *poly_h) //delete nodes one by one from front { ITEM *cur_del, *prev_del = poly_h; while (prev_del != NULL) { cur_del = prev_del->next; free(prev_del); prev_del = cur_del; } } void main() { ITEM *poly1, *poly2, *poly_add; printf("Create The First Polynomial: "); poly1 = create_poly(); printf("Create The Second Polynomial: "); poly2 = create_poly(); poly_add = add_poly(poly1, poly2); disp_poly(poly_add); delete_poly(poly_add); }
以上是完整代码,其中有一些很有意思的注释,这也是我在实际写代码时发现的一些东西
首先是关于如何保证头指针指向正确,而链条又如何形成的。这里用了一个if判断,*h初始化是NULL(空)的,而cur在接收完第一次数据之后,就传给了*h,之后才开始prev对cur的链接,*h是否为NULL,就是判断cur传输方向的标准。而不管是进行了哪种传输,cur都要对prev进行覆盖,实现指针向后移动的过程,具体来说就是
cur<<data | h<<cur | prev<<cur |
cur<<data | prev->next<<cur | prev<<cur |
cur<<data | prev->next<<cur | prev<<cur |
cur<<data | prev->next<<cur | prev<<cur |
…… | …… | …… |
(从左到右,从上到下)
然后是关于多个条件互补的并列if语句和if-else if-else语句的区别(这是三个词,‘if’与‘else if’与‘else’,不是两个if-else),多个并列的if,在满足前面的某个if并且执行完相关语句后,会继续对后续if进行判断,而前面的if语句中可能存在使得后续if判断体结果改变的内容,这也就是说,有可能会进行多个判断,然后就不知道跑到哪里去了。而if-else if-else结构就不会有这个问题。以前在写嵌入式模块的代码时,还很少碰到这个问题,但在这里,本例中如果采用多个if会导致结果错误,在例②中,这种多个if的结构会直接导致程序崩溃。
②约瑟夫环问题
1 #include <stdio.h> 2 #include <stdlib.h> 3 4 struct Joseph 5 { 6 int num; 7 struct Joseph *next; 8 }; 9 typedef struct Joseph NODE; 10 11 NODE *create_joseph_circle(int length) 12 { 13 int i; 14 NODE *head = NULL, *prev = NULL, *cur = NULL; 15 for (i = 1; i < length+1; i++) 16 { 17 cur = (NODE*)malloc(sizeof(NODE)); 18 cur->num = i; 19 cur->next = NULL; 20 if (head == NULL) 21 head = cur; 22 else 23 prev->next = cur; 24 prev = cur; 25 } 26 cur->next = head; 27 return head; 28 } 29 30 int disp_joseph_circle(NODE *joseph_head) 31 { 32 NODE *prev=NULL, *cur; 33 int i = 1; 34 if (joseph_head == NULL) 35 { 36 printf("No Such Joseph Circle Exists. "); 37 return 0; 38 } 39 //prev=joseph_head; 40 //cur=prev->next; 41 cur = joseph_head; 42 while (cur->next !=NULL) 43 { 44 if (cur->next == cur&&i % 3 == 0) 45 { 46 printf("%d ", cur->num); 47 cur->next = NULL; 48 } 49 else if (i % 3 == 0) //@@@@@@@@@@@@ 50 { 51 printf("%d ", cur->num); 52 prev->next = cur->next; 53 free(cur); 54 cur = prev->next; 55 } 56 else 57 { 58 prev = cur; 59 cur = cur->next; 60 } 61 62 i++; 63 64 } 65 return 0; 66 free(cur); 67 } 68 69 void main() 70 { 71 int n = 13; 72 NODE * joseph; 73 joseph = create_joseph_circle(n); 74 disp_joseph_circle(joseph); 75 }
这个题目是这样的,13个人围城一圈,从第一个人开始报数,报到3退出圈子,按顺序输出圈子的序号
这种题目因为包含首尾相连和人员(结点)退出,用链表处理再合适不过
原先想的是直接在cur->next==cur的时候结束循环,后来发现,最后一个退出的人(13)无法输出,分析后发现,当只剩一个人时,他的数字可能不是3,因此他需要自己循环若干次,直到成为3的倍数为止,因此修改结束条件,将(cur->next==cur&&i%3==0)作为判断条件,然后就出现了严重bug,在输出13之后程序就报错了。通过调试发现,是cur最后成为了0xdddddddd,原因是当13输出后,cur->next已经为NULL,而在修改之前(即49行)没有else,导致继续进入if判断,导致cur成为了NULL,再次进入循环时,判断便出问题了。于是改为else if ,问题得以解决。