(链表的具体相关操作请参考本博客文章中的链表一类)
链表(Linked list)是一种常见的基础数据结构,是一种线性表,是一种物理存储单元上非连续、非顺序的存储结构。链表由一系列结点(链表中每一个元素称为结点)组成,结点可以在运行时动态生成。每个结点包括存储数据元素的数据域和存储下一个结点地址的指针域两个部分。 相比于线性表顺序结构,操作复杂。数据元素的逻辑顺序也是通过链表中的指针链接次序实现的。使用链表结构可以克服数组链表需要预先知道数据大小的缺点,链表结构可以充分利用计算机内存空间,实现灵活的内存动态管理。但是链表失去了数组随机读取的优点,同时链表由于增加了结点的指针域,空间开销比较大。在计算机科学中,链表作为一种基础的数据结构可以用来生成其它类型的数据结构。链表通常由一连串节点组成,每个节点包含任意的实例数据(data fields)和一或两个用来指向上一个/或下一个节点的位置的链接("links")。链表最明显的好处就是,常规数组排列关联项目的方式可能不同于这些数据项目在记忆体或磁盘上顺序,数据的存取往往要在不同的排列顺序中转换。而链表是一种自我指示数据类型,因为它包含指向另一个相同类型的数据的指针(链接)。链表允许插入和移除表上任意位置上的节点,但是不允许随机存取。链表有很多种不同的类型:单向链表,双向链表以及循环链表。链表可以在多种编程语言中实现。像Lisp和Scheme这样的语言的内建数据类型中就包含了链表的存取和操作。
特点
线性表的链式存储表示的特点是用一组任意的存储单元存储线性表的数据元素(这组存储单元可以是连续的,也可以是不连续的)。因此,为了表示每个数据元素与其直接后继数据元素 之间的逻辑关系,对数据元素 来说,除了存储其本身的信息之外,还需存储一个指示其直接后继的信息(即直接后继的存储位置)。由这两部分信息组成一个"结点"(如概述旁的图所示),表示线性表中一个数据元素。线性表的链式存储表示,有一个缺点就是要找一个数,必须要从头开始找起,十分麻烦。
图7-3是单链表的结构:
基本操作:
1)建立:
顺序建链表:
struct node *head; struct node *creat(int n) { int i; struct node *tail,*p; head=(struct node *)malloc(sizeof(struct node)); head->next=NULL; tail=head; for(i=0;i<n;i++) { p=(struct node *)malloc(sizeof(struct node)); scanf("%d",&p->data); p->next=NULL; tail->next=p; tail=p; } return(head); };
逆序建链表:
struct node *head; struct node *creat(int n) { int i; struct node *p; head=(struct node *)malloc(sizeof(struct node)); head->next=NULL; for(i=0;i<n;i++) { p=(struct node *)malloc(sizeof(struct node)); scanf("%d",&p->data); p->next=head->next; head->next=p; } return(head); };
2)查找:
从链表中删除一个节点有三种情况,即删除链表头节点、删除链表的中
间节点、删除链表的尾节点。题目给出的是学生姓名,则应在链表中从头到尾依此查找各节
点,并与各节点的学生姓名比较,若相同,则查找成功,否则,找不到节点。由于删除的节
点可能在链表的头,会对链表的头指针造成丢失,所以定义删除节点的函数的返回值定义为
返回结构体类型的指针。
struct node *delet(head,pstr)//以he a d 为头指针,删除pstr所在节点 struct node *head; char *pstr; { struct node *temp,*p; temp = head;// 链表的头指针 if (head==NULL) //链表为空 printf(" List is null! "); else //非空表 { temp = head ; while (strcmp(temp->str,pstr)!=0&&temp->next!=NULL)// 若节点的字符串与输入字符串不同,并且未到链表尾 { p=temp; temp = temp->next; // 跟踪链表的增长,即指针后移 } if(strcmp(temp->str,pstr)==0 ) //找到字符串 { if(temp==head) { //表头节点 printf("delete string :%s ",temp->str); head = head->next; free (temp) ; //释放被删节点 } else { p->next=temp->next; //表中节点 printf("delete string :%s ",temp->str); free( temp ) ; } } else printf(" no find string! ");//没找 到要删除的字符串 } return(head) ; //返回表头指针* }
3. 链表的插入
首先定义链表的结构:
struct
{
int num; /*学生学号* /
char str[20]; /*姓名* /
struct node *next;
} ;
在建立的单链表中,插入节点有三种情况,如图7 - 5所示。
插入的节点可以在表头、表中或表尾。假定我们按照以学号为顺序建立链表,则插入的节点依次与表中节点相比较,找到插入位置。由于插入的节点可能在链表的头,会对链表的头指针造成修改,所以定义插入节点的函数的返回值定义为返回结构体类型的指针。节点的
插入函数如下:
struct node *insert(head,pstr,n) //插入学号为n、姓名为p s t r 的节点 struct node *head; //链表的头指针 char *pstr; int n; { struct node *p1,*p2,*p3; p1=(struct node*)malloc(sizeof(struct node)); 分配//一个新节点 strcpy(p1->str,pstr); // 写入节点的姓名字串 p1->num=n; // 学号 p2=head; if (head==NULL) // 空表 { head=p1; p1->next=NULL;//新节点插入表头 } else { //非空表 while(n>p2->num&&p2->next!=NULL) //输入的学号小于节点的学号,并且未到表尾 { p3=p2; p2=p2->next; //跟踪链表增长 } if (n<=p2->num) //找到插入位置 if (head==p2) // 插入位置在表头 { head=p1; p1->next=p2; } else { //插入位置在表中 p3->next=p1; p1->next=p2; } else { //插入位置在表尾 p2->next=p1; p1->next=NULL; } } return ( head ) ; // 返回链表的头指针 }
循环链表是与单链表一样,是一种链式的存储结构,所不同的是,循环链表的最后一个结点的指针是指向该循环链表的第一个结点或者表头结点,从而构成一个环形的链。
循环链表的运算与单链表的运算基本一致。所不同的有以下几点:
1、在建立一个循环链表时,必须使其最后一个结点的指针指向表头结点,而不是象单链表那样置为NULL。此种情况还使用于在最后一个结点后插入一个新的结点。
2、在判断是否到表尾时,是判断该结点链域的值是否是表头结点,当链域值等于表头指针时,说明已到表尾。而非象单链表那样判断链域值是否为NULL。
双向链表
双向链表其实是单链表的改进。
当我们对单链表进行操作时,有时你要对某个结点的直接前驱进行操作时,又必须从表头开始查找。这是由单链表结点的结构所限制的。因为单链表每个结点只有一个存储直接后继结点地址的链域,那么能不能定义一个既有存储直接后继结点地址的链域,又有存储直接前驱结点地址的链域的这样一个双链域结点结构呢?这就是双向链表。
在双向链表中,结点除含有数据域外,还有两个链域,一个存储直接后继结点地址,一般称之为右链域;一个存储直接前驱结点地址,一般称之为左链域。
应用举例概述
约瑟夫环问题:已知n个人(以编号1,2,3...n分别表示)围坐在一张圆桌周围。从编号为k的人开始报数,数到m的那个人出列;他的下一个人又从1开始报数,数到m的那个人又出列;依此规律重复下去,直到圆桌周围的人全部出列。例如:n = 9,k = 1,m = 5。
参考代码
#include<stdio.h> #include<malloc.h> #defineN41 #defineM5 typedef struct node*link; struct node { int item; link next; }; link NODE(intitem,linknext) { linkt=malloc(sizeof*t); t->item=item; t->next=next; return t; } int main(void) { int i; link t=NODE(1,NULL); t->next=t; for(i=2; i<=N; i++) t=t->next=NODE(i,t->next); while(t!=t->next) { for(i=1; i<M; i++) t=t->next; t->next=t->next->next; } printf("%d ",t->item); return0; }
其他相关结语与个人总结
C语言是学习数据结构的很好的学习工具。理解了C中用结构体描述数据结构,那么对于理解其C++描述,Java描述都就轻而易举了!链表的提出主要在于顺序存储中的插入和删除的时间复杂度是线性时间的,而链表的操作则可以是常数时间的复杂度。对于链表的插入与删除操作,个人做了一点总结,适用于各种链表如下:
插入操作处理顺序:中间节点的逻辑,后节点逻辑,前节点逻辑。按照这个顺序处理可以完成任何链表的插入操作。
删除操作的处理顺序:前节点逻辑,后节点逻辑,中间节点逻辑。
按照此顺序可以处理任何链表的删除操作。
如果不存在其中的某个节点略过即可。
上面的总结,大家可以看到一个现象,就是插入的顺序和删除的顺序恰好是相反的,很有意思!