链表是数据结构中比较基础也是比较重要的类型之一,那么有了数组,为什么我们还需要链表呢!或者说设计链表这种数据结构的初衷在哪里?
这是因为,在我们使用数组的时候,需要预先设定目标群体的个数,也即数组容量的大小,然而实时情况下我们目标的个数我们是不确定的,因此我们总是要把数组的容量设置的很大,这样以来就浪费了很多的空间。另外,数组在进行插入操作和删除操作的时候,在插入或者删除制定元素之后,我们往往需要进行循环移位,这增加了我们的线性开销。
正是由于以上的两种主要原因,链表被设计出来用于一般表的操作。为了避免上面描述数组的两种弊端,我们希望链表有一下的特点
1 可以灵活的扩展自己的长度。
2 存储地址不连续,删除或者插入操作的时候不需要循环移位。
要实现以上两个特点,我们需既要保证每个节点的独立性,又要保存相邻两个节点的联系。
为此,链表一般被设计为下面的形式。
Node--->Node---->Node
链表是由一个一个的节点组成的,可以方便和自由的插入未知个Node,前一个节点中用指针保存着下一个节点的位置,这样以来便顺利的完成了我们对链表的两点期望,但是唯一的缺点是增加了额外的空间消耗。
————————————————————————————————————————————————————————————————————————————
链表的定义:
链表的定义一般使用结构体,在看《数据结构与算法分析》这本书的时候发现,书中频繁的使用typedef的关键字,结果真的很棒不仅保持的代码的整洁程度,也让我们在下面的编码过程中少见了很多烦人的指针(当然指针还是一直存在的)。所以这里也借用了书中的定义方法。
struct Node; typedef struct Node* PtrNode; typedef PtrNode Position; typedef PtrNode List; struct Node{ int Value; PtrNode Next; };
下面接着书写一个建立链表的函数,输入每个节点的值,直到这个值是-1的时候函数结束。
在这个里面,我以前一直搞不明白为什么需要定义三个Node *,现在终于了解了,最终还是复习了指针的内容明白的,这里说一下指针实现链表对指针的操作很频繁,需要比较扎实的掌握了指针之后,在来看链表会轻松很多。在下面的一段程序里,我分别定义了head/p/tmp这三个指向节点结构体的指针,head的主要作用就像一个传销头目,他会主动联系上一个下线p,然后他就什么也不干了,p接着去发展一个又一个的下线tmp,结果一串以head为首的链表就出来了。
起先,我总觉得有了head,为什么还要p,这是因为如果直接使用head去指向下一个节点,head的位置也是不断在移动的,即它永远处于链表的尾端,这样当我们返回链表的时候,其实是空值。所以,我们需要p这个中转环节。(其实,这种做法在指针中非常普遍,大部分有返回指针类型的函数中,都会首先定义一个指针变量来保存函数的传入的参数,而不是对参数直接进行操作)。
/* 函数功能:创建一个链表 函数描述:每次输入一个新的整数,即把新增加一个节点存放该整数, 当输入的整数为-1时,函数结束。 */ List create() { int n=0; Position p,head,tmp; head=NULL; tmp=malloc(sizeof(struct Node)); if(tmp==NULL) { printf("tmp malloc failed!\n"); return NULL; } else { p=tmp; printf("please input the first node's message!\n"); scanf("%d",&(tmp->Value)); } while(tmp->Value!=-1) { n+=1; if(n==1) { head=p; tmp->Next=NULL; } else { p->Next=tmp; } p=tmp; tmp=malloc(sizeof(struct Node)); printf("please input the %d node!\n",n+1); scanf("%d",&(tmp->Value)); } p->Next=NULL; free(tmp); //free函数free掉的只是申请的空间,但是指针还是依然存在的。 tmp=NULL; return head; }
接下来,在写一个删除链表节点的函数,输入一个整数然后遍历链表节点,当链表节点的值与该整数相等的时候,即把该节点删除。
在完成这个函数首先一定要把这个过程思考清楚,不可否认我之前是一个上来就敲代码的人,看了《剑指offer》感觉这种习惯是程序员的大忌,甚至还想写一篇博客,名字都想好了《程序员的自我修养之思考在前,代码在后》。其实想想也是,我们写程序的目的是为了解决问题,而不是为了简单的写程序,纯粹的让程序跑起来大概只会在上学那会存在吧!真实的程序开发中需要考虑几乎所有 能想到的实际问题,所以无论程序再下,一要学会先思考清楚,再下笔写程序。
关于这个函数,我们要想到的是:
1 如果链表为空,我们该怎么做,当然是直接返回。
2 如果要删除的元素为头节点该怎么办?
3 如果要删除的元素为尾节点该怎么办?
当注意到以上三个部分,我们的程序就可能避免掉了输入链表为空,程序直接崩溃的现象,也可以避免删除元素值为头节点时删不掉的尴尬。我们的程序就有了一定的鲁棒性。
下面着重考虑链表的删除的实现:
list: Node_a->Node_b->Node_c->Node_d;
list tmp p
-------> tmp->Next=p->Next;
list: Node_a->Node_b----------->Node_d
free(p)
假设我们要删除的节点为上图的Node_c;假设我们能够找到Node_c的前一个位置tmp和被删除节点位置p的话;这个时候我们只需要执行tmp->Next=p->Next即可。
只要完成上面的分析以及考虑到各种情况,我们完成下面的代码就水到渠成了。
/* 函数功能:删除链表中指定值的节点(如果存在多个,只删除第一个) 本例中输入一个整数,删除链表节点值为这个整数的节点。 */ List DeleteNode(List list) { Position p,tmp; int value; if(list==NULL) { printf("The list is null,function return!\n"); return NULL; } else { printf("please input the delete Node's value:\n"); scanf("%d",&value); } p=list; if(p->Value==value) { list=p->Next; free(p); p=NULL; return list; } while(p!=NULL&&p->Value!=value) { tmp=p; p=p->Next; } if(p->Value==value) { if(p->Next!=NULL){ tmp->Next=p->Next; } else { tmp->Next=NULL; } free(p); p=NULL; } return list; }
关于链表的使用场景分析:
链表在程序开发中用到的频率还是非常高的,所以在高级语言中往往会对链表进行一些实现,比如STL中list以及Java中也有类似的东西。在目前的服务器端开发,主要运用链表来接收一些从数据中取出来的数据进行处理。
即使你不知道链表的底层实现,仍然可以成功的运用STL里面的现成的东西。但是作为一个学习者,我觉得会使用和从底层掌握仍然是两个不同的概念,linux之父说:“talk is less,show you code”。
以下的程序,用链表模拟了一个电话通讯录的功能,包括添加联系人,查找联系人,以及删除联系人。
PS:关于鲁棒性,程序中最大的危险是使用了gets这个函数,目前先保留使用gets,等待找到工作之后在做进一步的程序完善。
/************************************************************************** Programe: This is a phone list write by list The programe is just prictise for list Author: heat nan Mail:[email protected] Data:2015/07/27 **************************************************************************/ #include<stdio.h> #include<string.h> #include<stdlib.h> #define N 25 #define M 15 struct node; typedef struct node* p_node; typedef p_node List; typedef p_node Position; typedef struct node** PList; struct node{ char name[N]; char number[M]; Position next; }; int JudgeNameExist(List list,char* name); void AddPerson(PList list); void PrintList(List list); List FindPerson(List list); List FindPersonByName(List list,char* name); int AddPersonByName(PList list,List node); int DeletePersonByName(PList list,char* name); void DeletePerson(PList list); int main() { List list=NULL; Position p; char cmd[100]; while(1) { printf(" MAIN \n"); printf(" ******* 1 add a person *******\n"); printf(" ******* 2 show the phone list *******\n"); printf(" ******* 3 find from phone list *******\n"); printf(" ******* 4 delete from phone list *******\n\n\n"); printf("Please input the cmd number:\n"); gets(cmd); switch(cmd[0]) { case '1': AddPerson(&list); break; case '2': PrintList(list); break; case '3': FindPerson(list); break; case '4': DeletePerson(&list); break; default: printf("wrong cmd!\n"); break; } } return 0; } /* Function:判断要添加的联系人名称是否已经存在于电话簿中. Input: List 电话列表,name 要添加的联系人的姓名. Return: 已经存在返回1,不存在返回0. */ int JudgeNameExist(List list,char* name) { if(FindPersonByName(list,name)!=NULL) return 1; else return 0; } /* Function:根据输入的姓名查找联系人的信息节点 Input: 要输入的电话列表list,姓名name Return: 返回查找到的节点 */ List FindPersonByName(List list,char* name) { while(list!=NULL) { if(strcmp(list->name,name)==0) break; list=list->next; } return list; } /* Function:根据姓名添加新的联系人到联系人列表 Input: 指向联系人列表地址的指针, 新用户节点 Return: 添加成功返回1,添加失败返回0 */ int AddPersonByName(PList list,List node) { if(node==NULL) { printf("the node is NULL!\n"); return 0; } if(*list==NULL) { *list=node; return 1; } List pHead=*list; while(pHead->next!=NULL) pHead=pHead->next; pHead->next=node; return 1; } void AddPerson(PList list) { Position tmp; Position p_head; tmp=(struct node*)malloc(sizeof(struct node)); char name[N]; char number[M]; if(tmp==NULL) { printf("malloc the tmp node failed in function add person!\n"); } else { printf("please input the name:\n"); gets(name); printf("please input the number:\n"); gets(number); strcpy(tmp->name,name); strcpy(tmp->number,number); tmp->next=NULL; } if(JudgeNameExist(*list,name)==1) { free(tmp); printf("the name have already exist!\n"); return; } AddPersonByName(list,tmp); } /* Function: 打印联系人列表 Input: 联系人列表 */ void PrintList(List list) { Position show; show=list; if(show==NULL) { return ; } printf("Now,we print the phone list:\n"); while(show!=NULL) { printf("Name:%s Number:%s\n",show->name,show->number); show=show->next; } } List FindPerson(List list) { char name[N]; Position pHead=list; printf("please input the name you will find:\n"); gets(name); Position node=FindPersonByName(list,name); if(node!=NULL) printf("find success! name-> %s number-> %s\n",node->name,node->number); else printf("find failed!\n"); return node; } /* Function:根据姓名删除联系人 Input: 指向联系人地址的指针,联系人姓名 Output: 删除成功返回1,失败返回0 */ int DeletePersonByName(PList list,char* name) { if(*list==NULL||name==NULL) return 0; List pHead=*list; if(strcmp(pHead->name,name)==0) { *list=pHead->next; free(pHead); pHead->next==NULL; return 0; } List tmp=pHead->next; while(tmp!=NULL) { if(strcmp(tmp->name,name)==0) { pHead->next=tmp->next; free(tmp); tmp->next=NULL; return 1; } pHead=tmp; tmp=tmp->next; } return 0; } void DeletePerson(PList list) { List pHead=*list; if(pHead==NULL) { printf("there is no person you can delet\n"); return ; } char name[N]; printf("please input the name:\n"); gets(name); DeletePersonByName(list,name); }
————————————————————————————————————————————————————————
*************************************************************************************************************************************************
————————————————————————————————————————————————————————
链表概述
链表是一种常见的重要的数据结构。它是动态地进行存储分配的一种结构。它可以根据需要开辟内存单元。链表有一个“头指针”变量,以head表示,它存放一个地址。该地址指向一个元素。链表中每一个元素称为“结点”,每个结点都应包括两个部分:一为用户需要用的实际数据,二为下一个结点的地址。因此,head指向第一个元素:第一个元素又指向第二个元素;……,直到最后一个元素,该元素不再指向其它元素,它称为“表尾”,它的地址部分放一个“NULL”(表示“空地址”),链表到此结束。
链表的各类操作包括:学习单向链表的创建、删除、 插入(无序、有序)、输出、 排序(选择、插入、冒泡)、反序等等。
单向链表的图示:
---->[NULL]
head
图1:空链表
---->[p1]---->[p2]...---->[pn]---->[NULL]
head p1->next p2->next pn->next
图2:有N个节点的链表
创建n个节点的链表的函数为:
#include "stdlib.h" #include "stdio.h" #define NULL 0 #define LEN sizeof(struct student) struct student { int num; //学号 float score; //分数,其他信息可以继续在下面增加字段 struct student *next; //指向下一节点的指针 }; int n; //节点总数 /* ========================== 功能:创建n个节点的链表 返回:指向链表表头的指针 ========================== */ struct student *Create() { struct student *head; //头节点 struct student *p1 = NULL; //p1保存创建的新节点的地址 struct student *p2 = NULL; //p2保存原链表最后一个节点的地址 n = 0; //创建前链表的节点总数为0:空链表 p1 = (struct student *) malloc (LEN); //开辟一个新节点 p2 = p1; //如果节点开辟成功,则p2先把它的指针保存下来以备后用 if(p1==NULL) //节点开辟不成功 { printf ("\nCann't create it, try it again in a moment!\n"); return NULL; } else //节点开辟成功 { head = NULL; //开始head指向NULL printf ("Please input %d node -- num,score: ", n + 1); scanf ("%d %f", &(p1->num), &(p1->score)); //录入数据 } while(p1->num != 0) //只要学号不为0,就继续录入下一个节点 { n += 1; //节点总数增加1个 if(n == 1) //如果节点总数是1,则head指向刚创建的节点p1 { head = p1; p2->next = NULL; //此时的p2就是p1,也就是p1->next指向NULL。 } else { p2->next = p1; //指向上次下面刚刚开辟的新节点 } p2 = p1; //把p1的地址给p2保留,然后p1产生新的节点 p1 = (struct student *) malloc (LEN); printf ("Please input %d node -- num,score: ", n + 1); scanf ("%d %f", &(p1->num), &(p1->score)); } p2->next = NULL; //此句就是根据单向链表的最后一个节点要指向NULL free(p1); //p1->num为0的时候跳出了while循环,并且释放p1 p1 = NULL; //特别不要忘记把释放的变量清空置为NULL,否则就变成"野指针",即地址不确定的指针 return head; //返回创建链表的头指针 }
输出链表中节点的函数为:
/* =========================== 功能:输出节点 返回: void =========================== */ void Print(struct student *head) { struct student *p; printf ("\nNow , These %d records are:\n", n); p = head; if(head != NULL) //只要不是空链表,就输出链表中所有节点 { printf("head is %o\n", head); //输出头指针指向的地址 do { /* 输出相应的值:当前节点地址、各字段值、当前节点的下一节点地址。 这样输出便于读者形象看到一个单向链表在计算机中的存储结构,和我们 设计的图示是一模一样的。 */ printf ("%o %d %5.1f %o\n", p, p->num, p->score, p->next); p = p->next; //移到下一个节点 } while (p != NULL); } }
单向链表的删除图示:
---->[NULL]
head
图3:空链表
从图3可知,空链表显然不能删除
---->[1]---->[2]...---->[n]---->[NULL](原链表)
head 1->next 2->next n->next
---->[2]...---->[n]---->[NULL](删除后链表)
head 2->next n->next
图4:有N个节点的链表,删除第一个节点
结合原链表和删除后的链表,就很容易写出相应的代码。操作方法如下:
1、你要明白head就是第1个节点,head->next就是第2个节点;
2、删除后head指向第2个节点,就是让head=head->next,OK这样就行了。
---->[1]---->[2]---->[3]...---->[n]---->[NULL](原链表)
head 1->next 2->next 3->next n->next
---->[1]---->[3]...---->[n]---->[NULL](删除后链表)
head 1->next 3->next n->next
图5:有N个节点的链表,删除中间一个(这里图示删除第2个)
结合原链表和删除后的链表,就很容易写出相应的代码。操作方法如下:
1、你要明白head就是第1个节点,1->next就是第2个节点,2->next就是第3个节点;
2、删除后2,1指向第3个节点,就是让1->next=2->next。
删除指定学号的节点的函数为:
/* ========================== 功能:删除指定节点 (此例中是删除指定学号的节点) 返回:指向链表表头的指针 ========================== */ struct student *Del (struct student *head, int num) { struct student *p1; //p1保存当前需要检查的节点的地址 struct student *p2; //p2保存当前检查过的节点的地址 if (head == NULL) //是空链表(结合图3理解) { printf ("\nList is null!\n"); return head; } //定位要删除的节点 p1 = head; while (p1->num != num && p1->next != NULL) //p1指向的节点不是所要查找的,并且它不是最后一个节点,就继续往下找 { p2 = p1; //保存当前节点的地址 p1 = p1->next; //后移一个节点 } if(p1->num==num) //找到了。(结合图4、5理解) { if (p1 == head) //如果要删除的节点是第一个节点 { head = p1->next; //头指针指向第一个节点的后一个节点,也就是第二个节点。这样第一个节点就不在链表中,即删除 } else //如果是其它节点,则让原来指向当前节点的指针,指向它的下一个节点,完成删除 { p2->next = p1->next; } free (p1); //释放当前节点 p1 = NULL; printf ("\ndelete %ld success!\n", num); n -= 1; //节点总数减1个 } else //没有找到 { printf ("\n%ld not been found!\n", num); } return head; }
单向链表的插入图示:
---->[NULL](原链表)
head
---->[1]---->[NULL](插入后的链表)
head 1->next
图7 空链表插入一个节点
结合原链表和插入后的链表,就很容易写出相应的代码。操作方法如下:
1、你要明白空链表head指向NULL就是head=NULL;
2、插入后head指向第1个节点,就是让head=1,1->next=NULL,OK这样就行了。
---->[1]---->[2]---->[3]...---->[n]---->[NULL](原链表)
head 1->next 2->next 3->next n->next
---->[1]---->[2]---->[x]---->[3]...---->[n]---->[NULL](插入后的链表)
head 1->next 2->next x->next 3->next n->next
图8:有N个节点的链表,插入一个节点(这里图示插入第2个后面)
结合原链表和插入后的链表,就很容易写出相应的代码。操作方法如下:
1、你要明白原1->next就是节点2,2->next就是节点3;
2、插入后x指向第3个节点,2指向x,就是让x->next=2->next,1->next=x。
插入指定节点的后面的函数为:
/* ========================== 功能:插入指定节点的后面 (此例中是指定学号的节点) 返回:指向链表表头的指针 ========================== */ struct student *Insert (struct student *head, int num, struct student *node) { struct student *p1; //p1保存当前需要检查的节点的地址 if (head == NULL) //(结合图示7理解) { head = node; node->next = NULL; n += 1; return head; } p1 = head; while(p1->num != num && p1->next != NULL) //p1指向的节点不是所要查找的,并且它不是最后一个节点,继续往下找 { p1 = p1->next; //后移一个节点 } if (p1->num==num) //找到了(结合图示8理解) { node->next = p1->next; //显然node的下一节点是原p1的next p1->next = node; //插入后,原p1的下一节点就是要插入的node n += 1; //节点总数增加1个 } else { printf ("\n%ld not been found!\n", num); } return head; }
单向链表的反序图示:
---->[1]---->[2]---->[3]...---->[n]---->[NULL](原链表)
head 1->next 2->next 3->next n->next
[NULL]<----[1]<----[2]<----[3]<----...[n]<----(反序后的链表)
1->next 2->next 3->next n->next head
图9:有N个节点的链表反序
结合原链表和插入后的链表,就很容易写出相应的代码。操作方法如下:
1、我们需要一个读原链表的指针p2,存反序链表的p1=NULL(刚好最后一个节点的next为NULL),还有一个临时存储变量p;
2、p2在原链表中读出一个节点,我们就把它放到p1中,p就是用来处理节点放置顺序的问题;
3、比如,现在我们取得一个2,为了我们继续往下取节点,我们必须保存它的next值,由原链表可知p=2->next;
4、然后由反序后的链表可知,反序后2->next要指向1,则2->next=1;
5、好了,现在已经反序一个节点,接着处理下一个节点就需要保存此时的信息:
p1变成刚刚加入的2,即p1=2;p2要变成它的下一节点,就是上面我们保存的p,即p2=p。
反序链表的函数为:
/* ========================== 功能:反序节点 (链表的头变成链表的尾,链表的尾变成头) 返回:指向链表表头的指针 ========================== */ struct student *Reverse (struct student *head) { struct student *p; //临时存储 struct student *p1; //存储返回结果 struct student *p2; //源结果节点一个一个取 p1 = NULL; //开始颠倒时,已颠倒的部分为空 p2 = head; //p2指向链表的头节点 while(p2 != NULL) { p = p2->next; p2->next = p1; p1 = p2; p2 = p; } head = p1; return head; }
对链表进行选择排序的基本思想就是反复从还未排好序的那些节点中,选出键值(就是用它排序的字段,我们取学号num为键值)最小的节点,依次重新组合成一个链表。
我认为写链表这类程序,关键是理解:head存储的是第一个节点的地址,head->next存储的是第二个节点的地址;任意一个节点p的地址,只能通过它前一个节点的next来求得。
单向链表的选择排序图示:
---->[1]---->[3]---->[2]...---->[n]---->[NULL](原链表)
head 1->next 3->next 2->next n->next
---->[NULL](空链表)
first
tail
---->[1]---->[2]---->[3]...---->[n]---->[NULL](排序后链表)
first 1->next 2->next 3->next tail->next
图10:有N个节点的链表选择排序
1、先在原链表中找最小的,找到一个后就把它放到另一个空的链表中;
2、空链表中安放第一个进来的节点,产生一个有序链表,并且让它在原链表中分离出来(此时要注意原链表中出来的是第一个节点还是中间其它节点);
3、继续在原链表中找下一个最小的,找到后把它放入有序链表的尾指针的next,然后它变成其尾指针;
对链表进行选择排序的函数为:
========================== */ struct student *SelectSort (struct student *head) { struct student *first; //排列后有序链的表头指针 struct student *tail; //排列后有序链的表尾指针 struct student *p_min; //保留键值更小的节点的前驱节点的指针 struct student *min; //存储最小节点 struct student *p; //当前比较的节点 first = NULL; while(head != NULL) //在链表中找键值最小的节点 { //注意:这里for语句就是体现选择排序思想的地方 for (p = head, min = head; p->next != NULL; p = p->next) //循环遍历链表中的节点,找出此时最小的节点 { if (p->next->num < min->num) //找到一个比当前min小的节点 { p_min = p; //保存找到节点的前驱节点:显然p->next的前驱节点是p min = p->next; //保存键值更小的节点 } } //上面for语句结束后,就要做两件事;一是把它放入有序链表中;二是根据相应的条件判断,安排它离开原来的链表 //第一件事 if (first == NULL) //如果有序链表目前还是一个空链表 { first = min; //第一次找到键值最小的节点 tail = min; //注意:尾指针让它指向最后的一个节点 } else //有序链表中已经有节点 { tail->next = min; //把刚找到的最小节点放到最后,即让尾指针的next指向它 tail = min; //尾指针也要指向它 } //第二件事 if (min == head) //如果找到的最小节点就是第一个节点 { head = head->next; //显然让head指向原head->next,即第二个节点,就OK } else //如果不是第一个节点 { p_min->next = min->next; //前次最小节点的next指向当前min的next,这样就让min离开了原链表 } } if (first != NULL) //循环结束得到有序链表first { tail->next = NULL; //单向链表的最后一个节点的next应该指向NULL } head = first; return head; }
对链表进行直接插入排序的基本思想就是假设链表的前面n-1个节点是已经按键值(就是用它排序的字段,我们取学号num为键值)排好序的,对于节点n在这个序列中找插入位置,使得n插入后新序列仍然有序。按照这种思想,依次对链表从头到尾执行一遍,就可以使无序链表变为有序链表。
单向链表的直接插入排序图示:
---->[1]---->[3]---->[2]...---->[n]---->[NULL](原链表)
head 1->next 3->next 2->next n->next
---->[1]---->[NULL](从原链表中取第1个节点作为只有一个节点的有序链表)
head
图11
---->[3]---->[2]...---->[n]---->[NULL](原链表剩下用于直接插入排序的节点)
first 3->next 2->next n->next
图12
---->[1]---->[2]---->[3]...---->[n]---->[NULL](排序后链表)
head 1->next 2->next 3->next n->next
图13:有N个节点的链表直接插入排序
1、先在原链表中以第一个节点为一个有序链表,其余节点为待定节点。
2、从图12链表中取节点,到图11链表中定位插入。
3、上面图示虽说画了两条链表,其实只有一条链表。在排序中,实质只增加了一个用于指向剩下需要排序节点的头指针first罢了。
这一点请读者务必搞清楚,要不然就可能认为它和上面的选择排序法一样了。
对链表进行直接插入排序的函数为:
/* ========================== 功能:直接插入排序(由小到大) 返回:指向链表表头的指针 ========================== */ struct student *InsertSort (struct student *head) { struct student *first; //为原链表剩下用于直接插入排序的节点头指针 struct student *t; //临时指针变量:插入节点 struct student *p,*q; //临时指针变量 first = head->next; //原链表剩下用于直接插入排序的节点链表:可根据图12来理解 head->next = NULL; //只含有一个节点的链表的有序链表:可根据图11来理解 while(first != NULL) //遍历剩下无序的链表 { //注意:这里for语句就是体现直接插入排序思想的地方 for (t = first, q = head; ((q != NULL) && (q->num < t->num)); p = q, q = q->next); //无序节点在有序链表中找插入的位置 //退出for循环,就是找到了插入的位置,应该将t节点插入到p节点之后,q节点之前 //注意:按道理来说,这句话可以放到下面注释了的那个位置也应该对的,但是就是不能。原因:你若理解了上面的第3条,就知道了 //下面的插入就是将t节点即是first节点插入到p节点之后,已经改变了first节点,所以first节点应该在被修改之前往后移动,不能放到下面注释的位置上去 first = first->next; //无序链表中的节点离开,以便它插入到有序链表中 if (q == head) //插在第一个节点之前 { head = t; } else //p是q的前驱 { p->next = t; } t->next = q; //完成插入动作 //first = first->next; } return head; }
对链表进行冒泡排序的基本思想就是对当前还未排好序的范围内的全部节点,自上而下对相邻的两个节点依次进行比较和调整,让键值(就是用它排 序的字段,我们取学号num为键值)较大的节点往下沉,键值较小的往上冒。即:每当两相邻的节点比较后发现它们的排序与排序要求相反时,就将它们互换。
单向链表的冒泡排序图示:
---->[1]---->[3]---->[2]...---->[n]---->[NULL](原链表)
head 1->next 3->next 2->next n->next
---->[1]---->[2]---->[3]...---->[n]---->[NULL](排序后链表)
head 1->next 2->next 3->next n->next
图14:有N个节点的链表冒泡排序
任意两个相邻节点p、q位置互换图示:
假设p1->next指向p,那么显然p1->next->next就指向q,
p1->next->next->next就指向q的后继节点,我们用p2保存
p1->next->next指针。即:p2=p1->next->next,则有:
[ ]---->[p]---------->[q]---->[ ](排序前)
p1->next p1->next->next p2->next
图15
[ ]---->[q]---------->[p]---->[ ](排序后)
图16
1、排序后q节点指向p节点,在调整指向之前,我们要保存原p的指向节点地址,即:p2=p1->next->next;
2、顺着这一步一步往下推,排序后图16中p1->next->next要指的是p2->next,所以p1->next->next=p2->next;
3、在图15中p2->next原是q发出来的指向,排序后图16中q的指向要变为指向p的,而原来p1->next是指向p的,所以p2->next=p1->next;
4、在图15中p1->next原是指向p的,排序后图16中p1->next要指向q,原来p1->next->next(即p2)是指向q的,所以p1->next=p2;
5、至此,我们完成了相邻两节点的顺序交换。
6、下面的程序描述改进了一点就是记录了每次最后一次节点下沉的位置,这样我们不必每次都从头到尾的扫描,只需要扫描到记录点为止。 因为后面的都已经是排好序的了。
对链表进行冒泡排序的函数为:
/* ========================== 功能:冒泡排序(由小到大) 返回:指向链表表头的指针 ========================== */ struct student *BubbleSort (struct student *head) { struct student *endpt; //控制循环比较 struct student *p; //临时指针变量 struct student *p1,*p2; p1 = (struct student *) malloc (LEN); p1->next = head; //注意理解:我们增加一个节点,放在第一个节点的前面,主要是为了便于比较。因为第一个节点没有前驱,我们不能交换地址 head = p1; //让head指向p1节点,排序完成后,我们再把p1节点释放掉 for (endpt = NULL; endpt != head; endpt = p) //结合第6点理解 { for (p = p1 = head; p1->next->next != endpt; p1 = p1->next) { if (p1->next->num > p1->next->next->num) //如果前面的节点键值比后面节点的键值大,则交换 { p2 = p1->next->next; //结合第1点理解 p1->next->next = p2->next; //结合第2点理解 p2->next = p1->next; //结合第3点理解 p1->next = p2; //结合第4点理解 p = p1->next->next; //结合第6点理解 } } } p1 = head; //把p1的信息去掉 head = head->next; //让head指向排序后的第一个节点 free (p1); //释放p1 p1 = NULL; //p1置为NULL,保证不产生“野指针”,即地址不确定的指针变量 return head; }
有序链表插入节点示意图:
---->[NULL](空有序链表)
head
图18:空有序链表(空有序链表好解决,直接让head指向它就是了。)
以下讨论不为空的有序链表。
---->[1]---->[2]---->[3]...---->[n]---->[NULL](有序链表)
head 1->next 2->next 3->next n->next
图18:有N个节点的有序链表
插入node节点的位置有两种情况:一是第一个节点前,二是其它节点前或后。
---->[node]---->[1]---->[2]---->[3]...---->[n]---->[NULL]
head node->next 1->next 2->next 3->next n->next
图19:node节点插在第一个节点前
---->[1]---->[2]---->[3]...---->[node]...---->[n]---->[NULL]
head 1->next 2->next 3->next node->next n->next
插入有序链表的函数为:
/* ========================== 功能:插入有序链表的某个节点的后面(从小到大) 返回:指向链表表头的指针 ========================== */ struct student *SortInsert (struct student *head, struct student *node) { struct student *p; //p保存当前需要检查的节点的地址 struct student *t; //临时指针变量 if (head == NULL) //处理空的有序链表 { head = node; node->next = NULL; n += 1; //插入完毕,节点总数加 return head; } p = head; //有序链表不为空 while(p->num < node->num && p != NULL) //p指向的节点的学号比插入节点的学号小,并且它不等于NULL { t = p; //保存当前节点的前驱,以便后面判断后处理 p = p->next; //后移一个节点 } if (p == head) //刚好插入第一个节点之前 { node->next = p; head = node; } else //插入其它节点之后 { t->next = node; //把node节点加进去 node->next = p; } n += 1; //插入完毕,节点总数加1 return head; }
综上所述,链表的各类操作函数的完整代码如下:
#include "stdlib.h" #include "stdio.h" #define NULL 0 #define LEN sizeof(struct student) struct student { int num; //学号 float score; //分数,其他信息可以继续在下面增加字段 struct student *next; //指向下一节点的指针 }; int n; //节点总数 /* ========================== 功能:创建n个节点的链表 返回:指向链表表头的指针 ========================== */ struct student *Create() { struct student *head; //头节点 struct student *p1 = NULL; //p1保存创建的新节点的地址 struct student *p2 = NULL; //p2保存原链表最后一个节点的地址 n = 0; //创建前链表的节点总数为0:空链表 p1 = (struct student *) malloc (LEN); //开辟一个新节点 p2 = p1; //如果节点开辟成功,则p2先把它的指针保存下来以备后用 if(p1==NULL) //节点开辟不成功 { printf ("\nCann't create it, try it again in a moment!\n"); return NULL; } else //节点开辟成功 { head = NULL; //开始head指向NULL printf ("Please input %d node -- num,score: ", n + 1); scanf ("%d %f", &(p1->num), &(p1->score)); //录入数据 } while(p1->num != 0) //只要学号不为0,就继续录入下一个节点 { n += 1; //节点总数增加1个 if(n == 1) //如果节点总数是1,则head指向刚创建的节点p1 { head = p1; p2->next = NULL; //此时的p2就是p1,也就是p1->next指向NULL。 } else { p2->next = p1; //指向上次下面刚刚开辟的新节点 } p2 = p1; //把p1的地址给p2保留,然后p1产生新的节点 p1 = (struct student *) malloc (LEN); printf ("Please input %d node -- num,score: ", n + 1); scanf ("%d %f", &(p1->num), &(p1->score)); } p2->next = NULL; //此句就是根据单向链表的最后一个节点要指向NULL free(p1); //p1->num为0的时候跳出了while循环,并且释放p1 p1 = NULL; //特别不要忘记把释放的变量清空置为NULL,否则就变成"野指针",即地址不确定的指针 return head; //返回创建链表的头指针 } /* =========================== 功能:输出节点 返回: void =========================== */ void Print(struct student *head) { struct student *p; printf ("\nNow , These %d records are:\n", n); p = head; if(head != NULL) //只要不是空链表,就输出链表中所有节点 { printf("head is %o\n", head); //输出头指针指向的地址 do { /* 输出相应的值:当前节点地址、各字段值、当前节点的下一节点地址。 这样输出便于读者形象看到一个单向链表在计算机中的存储结构,和我们 设计的图示是一模一样的。 */ printf ("%o %d %5.1f %o\n", p, p->num, p->score, p->next); p = p->next; //移到下一个节点 } while (p != NULL); } } /* ========================== 功能:删除指定节点 (此例中是删除指定学号的节点) 返回:指向链表表头的指针 ========================== */ struct student *Del (struct student *head, int num) { struct student *p1; //p1保存当前需要检查的节点的地址 struct student *p2; //p2保存当前检查过的节点的地址 if (head == NULL) //是空链表(结合图3理解) { printf ("\nList is null!\n"); return head; } //定位要删除的节点 p1 = head; while (p1->num != num && p1->next != NULL) //p1指向的节点不是所要查找的,并且它不是最后一个节点,就继续往下找 { p2 = p1; //保存当前节点的地址 p1 = p1->next; //后移一个节点 } if(p1->num==num) //找到了。(结合图4、5理解) { if (p1 == head) //如果要删除的节点是第一个节点 { head = p1->next; //头指针指向第一个节点的后一个节点,也就是第二个节点。这样第一个节点就不在链表中,即删除 } else //如果是其它节点,则让原来指向当前节点的指针,指向它的下一个节点,完成删除 { p2->next = p1->next; } free (p1); //释放当前节点 p1 = NULL; printf ("\ndelete %ld success!\n", num); n -= 1; //节点总数减1个 } else //没有找到 { printf ("\n%ld not been found!\n", num); } return head; } //销毁链表 void DestroyList(struct student *head) { struct student *p; if(head==NULL) return 0; while(head) { p=head->next; free(head); head=p; } return 1; } /* ========================== 功能:插入指定节点的后面 (此例中是指定学号的节点) 返回:指向链表表头的指针 ========================== */ struct student *Insert (struct student *head, int num, struct student *node) { struct student *p1; //p1保存当前需要检查的节点的地址 if (head == NULL) //(结合图示7理解) { head = node; node->next = NULL; n += 1; return head; } p1 = head; while(p1->num != num && p1->next != NULL) //p1指向的节点不是所要查找的,并且它不是最后一个节点,继续往下找 { p1 = p1->next; //后移一个节点 } if (p1->num==num) //找到了(结合图示8理解) { node->next = p1->next; //显然node的下一节点是原p1的next p1->next = node; //插入后,原p1的下一节点就是要插入的node n += 1; //节点总数增加1个 } else { printf ("\n%ld not been found!\n", num); } return head; } /* ========================== 功能:反序节点 (链表的头变成链表的尾,链表的尾变成头) 返回:指向链表表头的指针 ========================== */ struct student *Reverse (struct student *head) { struct student *p; //临时存储 struct student *p1; //存储返回结果 struct student *p2; //源结果节点一个一个取 p1 = NULL; //开始颠倒时,已颠倒的部分为空 p2 = head; //p2指向链表的头节点 while(p2 != NULL) { p = p2->next; p2->next = p1; p1 = p2; p2 = p; } head = p1; return head; } /* ========================== 功能:选择排序(由小到大) 返回:指向链表表头的指针 ========================== */ struct student *SelectSort (struct student *head) { struct student *first; //排列后有序链的表头指针 struct student *tail; //排列后有序链的表尾指针 struct student *p_min; //保留键值更小的节点的前驱节点的指针 struct student *min; //存储最小节点 struct student *p; //当前比较的节点 first = NULL; while(head != NULL) //在链表中找键值最小的节点 { //注意:这里for语句就是体现选择排序思想的地方 for (p = head, min = head; p->next != NULL; p = p->next) //循环遍历链表中的节点,找出此时最小的节点 { if (p->next->num < min->num) //找到一个比当前min小的节点 { p_min = p; //保存找到节点的前驱节点:显然p->next的前驱节点是p min = p->next; //保存键值更小的节点 } } //上面for语句结束后,就要做两件事;一是把它放入有序链表中;二是根据相应的条件判断,安排它离开原来的链表 //第一件事 if (first == NULL) //如果有序链表目前还是一个空链表 { first = min; //第一次找到键值最小的节点 tail = min; //注意:尾指针让它指向最后的一个节点 } else //有序链表中已经有节点 { tail->next = min; //把刚找到的最小节点放到最后,即让尾指针的next指向它 tail = min; //尾指针也要指向它 } //第二件事 if (min == head) //如果找到的最小节点就是第一个节点 { head = head->next; //显然让head指向原head->next,即第二个节点,就OK } else //如果不是第一个节点 { p_min->next = min->next; //前次最小节点的next指向当前min的next,这样就让min离开了原链表 } } if (first != NULL) //循环结束得到有序链表first { tail->next = NULL; //单向链表的最后一个节点的next应该指向NULL } head = first; return head; } /* ========================== 功能:直接插入排序(由小到大) 返回:指向链表表头的指针 ========================== */ struct student *InsertSort (struct student *head) { struct student *first; //为原链表剩下用于直接插入排序的节点头指针 struct student *t; //临时指针变量:插入节点 struct student *p,*q; //临时指针变量 first = head->next; //原链表剩下用于直接插入排序的节点链表:可根据图12来理解 head->next = NULL; //只含有一个节点的链表的有序链表:可根据图11来理解 while(first != NULL) //遍历剩下无序的链表 { //注意:这里for语句就是体现直接插入排序思想的地方 for (t = first, q = head; ((q != NULL) && (q->num < t->num)); p = q, q = q->next); //无序节点在有序链表中找插入的位置 //退出for循环,就是找到了插入的位置,应该将t节点插入到p节点之后,q节点之前 //注意:按道理来说,这句话可以放到下面注释了的那个位置也应该对的,但是就是不能。原因:你若理解了上面的第3条,就知道了 //下面的插入就是将t节点即是first节点插入到p节点之后,已经改变了first节点,所以first节点应该在被修改之前往后移动,不能放到下面注释的位置上去 first = first->next; //无序链表中的节点离开,以便它插入到有序链表中 if (q == head) //插在第一个节点之前 { head = t; } else //p是q的前驱 { p->next = t; } t->next = q; //完成插入动作 //first = first->next; } return head; } /* ========================== 功能:冒泡排序(由小到大) 返回:指向链表表头的指针 ========================== */ struct student *BubbleSort (struct student *head) { struct student *endpt; //控制循环比较 struct student *p; //临时指针变量 struct student *p1,*p2; p1 = (struct student *) malloc (LEN); p1->next = head; //注意理解:我们增加一个节点,放在第一个节点的前面,主要是为了便于比较。因为第一个节点没有前驱,我们不能交换地址 head = p1; //让head指向p1节点,排序完成后,我们再把p1节点释放掉 for (endpt = NULL; endpt != head; endpt = p) //结合第6点理解 { for (p = p1 = head; p1->next->next != endpt; p1 = p1->next) { if (p1->next->num > p1->next->next->num) //如果前面的节点键值比后面节点的键值大,则交换 { p2 = p1->next->next; //结合第1点理解 p1->next->next = p2->next; //结合第2点理解 p2->next = p1->next; //结合第3点理解 p1->next = p2; //结合第4点理解 p = p1->next->next; //结合第6点理解 } } } p1 = head; //把p1的信息去掉 head = head->next; //让head指向排序后的第一个节点 free (p1); //释放p1 p1 = NULL; //p1置为NULL,保证不产生“野指针”,即地址不确定的指针变量 return head; } /* ========================== 功能:插入有序链表的某个节点的后面(从小到大) 返回:指向链表表头的指针 ========================== */ struct student *SortInsert (struct student *head, struct student *node) { struct student *p; //p保存当前需要检查的节点的地址 struct student *t; //临时指针变量 if (head == NULL) //处理空的有序链表 { head = node; node->next = NULL; n += 1; //插入完毕,节点总数加 return head; } p = head; //有序链表不为空 while(p->num < node->num && p != NULL) //p指向的节点的学号比插入节点的学号小,并且它不等于NULL { t = p; //保存当前节点的前驱,以便后面判断后处理 p = p->next; //后移一个节点 } if (p == head) //刚好插入第一个节点之前 { node->next = p; head = node; } else //插入其它节点之后 { t->next = node; //把node节点加进去 node->next = p; } n += 1; //插入完毕,节点总数加1 return head; } /* 以上函数的测试程序: 提示:根据测试函数的不同注释相应的程序段,这也是一种测试方法。 */ int main(void) { struct student *head; struct student *stu; int thenumber; // 测试Create()、Print() head = Create(); Print(head); //测试Del() printf("\nWhich one delete: "); scanf("%d",&thenumber); head = Del(head,thenumber); Print(head); //测试Insert() stu = (struct student *)malloc(LEN); printf("\nPlease input insert node -- num,score: "); scanf("%d %f",&stu->num,&stu->score); printf("\nInsert behind num: "); scanf("%d",&thenumber); head = Insert(head,thenumber,stu); Print(head); //测试Reverse() printf("\nReverse the LinkList: \n"); head = Reverse(head); Print(head); //测试SelectSort() printf("\nSelectSort the LinkList: \n"); head = SelectSort(head); Print(head); //测试InsertSort() printf("\nInsertSort the LinkList: \n"); head = InsertSort(head); Print(head); //测试BubbleSort() printf("\nBubbleSort the LinkList: \n"); head = BubbleSort(head); Print(head); printf("\nSortInsert the LinkList: \n"); //测试SortInsert():上面创建链表,输入节点时请注意学号num从小到大的顺序 stu = (struct student *)malloc(LEN); printf("\nPlease input insert node -- num,score: "); scanf("%d %f",&stu->num,&stu->score); head = SortInsert(head,stu); Print(head); //销毁链表 DestroyList(head); printf ("\n"); system ("pause"); }