560 likes | 668 Views
第 4 章 链表. 本章主题: 有序表的链式存储 应用 教学目的: 掌握有序表链式存储方法和基本操作 教学重点: 链表的基本操作 教学难点: 多项式与稀疏矩阵的链表实现. 引入:线性表的两种基本存储结构:. 第 1 种:顺序存储结构:顺序表如数组. 顺序表具有以下两个基本特点: (1) 线性表的所有元素所占的存储空间是连续的。 (2) 表中各数据元素在存储空间中是按逻辑顺序依次存放的。 顺序存储结构不足之处: 1)数据元素最大个数需预先确定,使得高级程序语言编译系 统需预先分配相应的存储空间;
E N D
第4章 链表 本章主题:有序表的链式存储应用 教学目的:掌握有序表链式存储方法和基本操作 教学重点:链表的基本操作 教学难点:多项式与稀疏矩阵的链表实现
引入:线性表的两种基本存储结构: 第1种:顺序存储结构:顺序表如数组 • 顺序表具有以下两个基本特点: • (1) 线性表的所有元素所占的存储空间是连续的。 • (2) 表中各数据元素在存储空间中是按逻辑顺序依次存放的。 • 顺序存储结构不足之处: • 1)数据元素最大个数需预先确定,使得高级程序语言编译系 • 统需预先分配相应的存储空间; • 2)插入与删除运算的效率很低。为了保持线性表中的元素顺 • 序在插入操作和删除操作时需移动大量数据。 • 3)存储空间不便于扩充。
线性表实现举例1: typedef int ElemType; /* ElemType 定义成int 类型*/ typedef struct{/*结构体*/ ElemType data[MAXSIZE] ; /*定义int 类型的数组, ElemType即为int */ int length; }SqList; //取名 // 创建n个元素的列表SqList void Creat_SqList(SqList *L , int n){ int i; L->length =n; i=0; printf("\ninput %d data : ",n); while(i<n){ scanf("%d",&L->data[i]); i++; } }
完成下列基本操作的算法代码 //1 创建列表SqList void Creat_SqList(SqList *L , int n); //2 打印输出SqList void Print_SqList(SqList *L); //3 在某一位置插入新的元素... void Insert_SqList(SqList *L,int i,ElemType x); //4 删除某一位置的元素 void Delete_SqList(SqList *L,int i); //5 找某个元素是不是在SqList若在返回其位置..不在返回-1 int Locate_SqList(SqList *L,ElemType x); //6 反向函数 void Reverse_SqList(SqList *L);
即最坏情况下移动数据的时间复杂性能为O(n2)。即最坏情况下移动数据的时间复杂性能为O(n2)。 【例2】将顺序表(a1,a2,a3,…,an)重新排列为以a1为界的两部分:a1前面的值均比a1小,a1后面的值都比a1大(这里假设数据元素的类型具有可比性,可设为整型)。 【算法描述】:从第二个元素到最后一个元素,逐一向后扫描: if(ai >a1) 不必改变它与a1之间的位置,继续比较下一个; if(ai <a1) 表明它应该在a1的前面,此时将它前面的元素依次向下移动一个位置,然后将它置入最上方。 算法中,外循环执行n-1次,内循环中元素的移动次数与当前数据的大小有关,当第i个元素小于a1时,要移动i-1个元素,再加上前面结点的保存及置入,共移动i-1+2次,在最坏情况下,a1后面的结点都小于a1,故总的移动次数为:
第2种 线性表的链式存储 为避免大量结点的移动,引入指针的线性表的链式存储结构,简称链表(Linked List)。 图4-1 4.1 指针 4.1.1指针的危险性:使用类型的强制转换 int *pi,i; pi=(int *)malloc(sizeof(int)); 4.1.2 动态存储分配 malloc函数 float *pf; pf=(float *)malloc(sizeof(float)); free(pf);//释放指针
4.2 单向链表 单向链表的每个结点只有一个指向后继的指针。即:访问数据元素只能由链表头依次到链表尾,而不能做逆向访问。又称线性链表,这是一种最简单的链表。 对于空链表,头结点的指针域为空(NULL)图中用∧表示 图4-2(a)为带头结点的空链 (b)为带头结点的单链表
First node | ptr->data | ptr->link | b a t \0 NULL ptr 例4-1[以at结尾的单词表] 建立单词链表的结构: typedef struct list_node *list_pointer; typedef struct list_node{ char data[4]; list_pointer link; }; • 建立一个新的空表:list_pointer ptr=NULL; • 判空:#define is_empty(ptr) (!(ptr)) • 创建一个新节点:ptr= (list_pointer)malloc(sizeof(list_node)); • 添加单词bat入表: strcpy(ptr->data,”bat”); ptr->link= NULL;
例4-2[2节点链表] P91-92代码实现e4-2.c typedef struct list_node *list_pointer; typedef struct list_node{ int data; list_pointer link; }list_node; 1. 建立一个2节点的表:list_pointer create2(); 2. 判满:#define is_full (ptr) (!(ptr)) 3. 表前端插入: void insert(list_point *ptr,list_pointer node) 4. 表的删除(p92): void delete(list_pointer *ptr, list_pointer trail, list_pointer node); • 表的输出: void print_list(list_pointer ptr);
链表的基本操作实现2 #define ElemType int typedef struct LNode { ElemType data; struct LNode *next; } Lnode,*LinkList; Lnode *p; p->data=ai p->next->data=ai+1 typedef struct list_node *list_pointer; typedef struct list_node{ int data; list_pointer link; } list_node;
由于链表是一种动态管理的存储结构,每个结点需动态产生。由于链表是一种动态管理的存储结构,每个结点需动态产生。 1.初始化链表initlist(L)的实现 建立一个空的带头结点的单链表。空链表的表长为0,在这种情况下,链表中没有元素结点。但应有一个头结点,其指针域为空。 void initlist(LinkList *L) { *L=(LNode *)malloc(sizeof(LNode)); (*L)->next=NULL; } 在函数调用时,指针L指向的内容发生了变化,为使得调用函数中头指针变量head获得头结点地址,需传递头指针变量的地址给initlist()函数,而函数中定义二级指针变量L接受该地址值,从而返回改变后的值。
2.求线性表长度Getlen(L)的实现 设计思路:设置一个初值为0的计数器变量和一个跟踪链表结点的指针p。初始时p指向链表中的第一个结点,然后顺着next域依次指向每个结点,每指向一个结点计数器变量加1。当p为0时,结束该过程。其时间复杂度为O(n)。 int Getlen(LinkList L) { int num=0; LNode *p; p=L->next; while(p!=NULL) { num++; p=p->next; } return(num); }
3.按序号取元素Getelem(L,i)的实现 对单链表中的结点只能顺序存取,即访问前一个结点后才能接着访问后一个结点。所以要访问单链表中第i个元素值,必须从头指针开始遍历链表,依次访问每个结点,直到访问到第i个结点为止。同顺序表一样,也需注意存取的位置是否有效。 LNode *Getelem(LinkList L,int i) { LNode *p; int pos=1; p=L->next; if(i<1 || i>Getlen(L)) exit(1); while(pos<i) { p=p->next; pos++; } return p; }
4.查找运算locate(L,x)的实现 设计思路:设置一个跟踪链表结点的指针p,初始时p指向链表中的第一个结点,然后顺着next域依次指向每个结点,每指向一个结点就判断其值是否等于x,若是则返回该结点地址。否则继续往后搜索,直到p为0,表示链表中无此元素,返回NULL。其时间复杂度为O(n)。 LNode *Locate(LinkList L,int x) { LNode *p; p=L->next; while(p!=NULL && p->data!=x) p=p->next; return p; }
5.链表的插入算法inselem(L,i,x)的实现 单链表结点的插入是利用修改结点指针域的值,使其指向新的链接位置来完成的插入操作,而无需移动任何元素。 假定在链表中值为ai的结点之前插入一个新结点,要完成这种插入必须首先找到所插位置的前一个结点,再进行插入。假设指针q指向待插位置的前驱结点,指针s指向新结点,则完成该操作的过程如图4-4所示。
图 4-4 p前插入s结点过程示意图 上述指针进行相互赋值的语句顺序不能颠倒。
void Inselem(LinkList L,int i,int x) { LNode *p,*q,*s; int pos=1; p=L; if(i<1 || i>Getlen(L)+1) exit(1); s=(LNode *)malloc(sizeof(LNode)); s->data=x; while(pos<=i) { q=p;p=p->next; pos++; } s->next=q->next; q->next=s; }
若传给函数的是待插入结点的地址,可编写如下的算法:若传给函数的是待插入结点的地址,可编写如下的算法: void Insnode(LinkList L,int i,LNode *s) { LNode *p,*q; int pos=1; if(i<1 || i>Getlen(L)+1) exit(1); p=L; while(pos<=i) { q=p;p=p->next; pos++; } s->next=q->next; q->next=s; }
6.链表的删除运算delelem(L,i)的实现 要删除链表中第i个结点,首先在单链表中找到删除位置前一个结点,并用 指针q指向它,指针p指向要删除的结点。将*q的指针域修改为待删除结点*p 的后继结点的地址。删除后的结点需动态的释放。对照p92 4-4表的删除算法 void Delelem(LinkList L,int i) { int pos=1; LNode *q=L,*p; if(i<1||i>Getlen(L))exit(1); while(pos<i) { q=q->next; pos++; } p=q->next; q->next=p->next; free(p); } void delete (list_pointer *ptr, list_pointer trail, list_pointer node) { if(trail) trail->link=node->link; else *ptr=(*ptr)->link; free(node); }
在插入和删除算法中,都是先查询确定操作位置,然后再进行插入和删除操作。所以其时间复杂度均为O(n)。另外在算法中实行插入和删除操作时没有移动元素的位置,只是修改了指针的指向,所以采用链表存储方式要比顺序存储方式的效率高。在插入和删除算法中,都是先查询确定操作位置,然后再进行插入和删除操作。所以其时间复杂度均为O(n)。另外在算法中实行插入和删除操作时没有移动元素的位置,只是修改了指针的指向,所以采用链表存储方式要比顺序存储方式的效率高。 7.链表元素输出运算displist(L)的实现 从第一个结点开始,顺着指针链依次访问每一个结点并输出。 void displist(LinkList L) { LNode *p; p=L->next; while(p!=NULL) { printf("%4d",p->data); p=p->next; } printf("\n"); }
【例4-】利用前面定义的基本运算函数,编写将一已知的单链表H倒置的程序。如图4-6的操作。(a)为倒置前,(b)为倒置后。【例4-】利用前面定义的基本运算函数,编写将一已知的单链表H倒置的程序。如图4-6的操作。(a)为倒置前,(b)为倒置后。 【算法基本思路】不另外增加新结点,而只是修改原结点的指针。设置指针 p ,令其指向head->next,并将head->next置空,然后依次将p所指链表的结点顺序摘下,插入到head之后即可。 图4-6 单链表的倒置
4-17具体算法如下: void reverse(LinkList L) { LNode *p,*q; if(L->next!=NULL) //至少两个元素 { p=L->next; L->next=NULL; while(p!=NULL) { q=p; p=p->next; Insnode(L,1,q); } } } main() { LNode *head,*p; int i,x; initlist(&head); for(i=1;i<=5;i++) { scanf("%d",&x); Inselem(head,i,x); } reverse(head); displist(head); } 该算法只是对链表顺序扫描一遍即完成了倒置,所以时间复杂性为O(n)。
4.3 栈的链式存储结构及其基本运算的实现 1.栈的链式存储结构 栈的链式实现是以链表作为栈的存储结构,并在这种存储结构上实现栈的基本运算。栈的链式实现称为链栈 链栈结构示意图
链栈的C语言描述如下: typedef struct snode { //定义链栈结点类型 ElemType data; struct snode *next; }StackNode; typedef struct{ StatckNode *top;//栈顶指针 }linkstack; 链栈的几种状态 :
2.基本运算在链式存储结构的实现 • 栈初始化 LinkSTACK InitStack(LinkSTACK *s) • { • s=(LinkSTACK*)malloc(sizeof(LinkSTACK)); • s->top=NULL; • }
//入栈运算 void push(linkstack *s,elemtype x) { StatckNode *p=(StatckNode*)malloc(sizeof(StatckNode)); p->data = x; p->next = s->top; s->top = p; } //判断栈是否为空 int empty(linkstack *s) { if(s->top == NULL) { return 1; } return 0; }
出栈运算 elemtype pop(linkstack *s) { elemtype x; StatckNode *p = s->top; if(empty(s)) { printf("Underflow!!\n"); exit (0); } x=p->data; s->top = p->next; free(p); return x; }
取栈顶元素 elemtype gettop(linkstack *s) { if(empty(s)) { printf("Underflow!!\n"); exit (0); } return s->top->data; }
4.3.2 队列的链式存储结构及其基本运算的实现 1. 队列的链式存储结构 队列的链式存储结构简称为链队。它实际上是一个同时带有首指针和尾指针的单链表。头指针指向表头结点,而尾指针则指向队尾元素。 链队结构示意图
链队的数据类型定义如下 : typedef struct qnode{ //链队结点的类型 ElemType data; struct qnode *next; }QTYPE; typedef struct qptr{ //链队指针类型 QTYPE *front,*rear; }SQUEUE; SQUEUE LQ;
2.基本运算链队的实现 • 队列初始化 void InitQueue(SQUEUE *LQ) { QTYPE *p; p=(QTYPE *)malloc(sizeof(QTYPE)); p->next=NULL; LQ->front= LQ->rear=p; }
入队列 int EnQueue(SQUEUE *LQ,ElemType x) { QTYPE *s; s=(QTYPE *)malloc(sizeof(QTYPE)); s->data=x; s->next=LQ->rear->next; LQ->rear->next=s; LQ->rear=s; return 1; } • 判队空 • int Empty(SQUEUE *LQ) • { return(LQ->front==LQ->rear?1:0);}
出队列 int OutQueue(SQUEUE *LQ,ElemType *x) { QTYPE *p; if(Empty(LQ)){ printf("\n Queue is free!"); return 0;} p=LQ->front->next; *x=p->data; LQ->front->next=p->next; if(LQ->front->next==NULL) LQ->rear=LQ->front; free(p); return 1; }
取队头元素 int GetHead(SQUEUE *LQ,ElemType *x) { if(Empty(LQ)){ printf("\n Queue is free!"); return 0; } *x=LQ->front->next->data; return 1; }
4.4 多项式 4.4.1 多项式的单向链表示 4.4.2 多项式加法 4.4.3 多项式删除 4.4.4 多项式的循环链表示
4. 4 线性表的应用——一元多项式计算 4.4.1 一元多项式表示 链式存储结构的典型应用之一是在高等数学的多项式方面。本节主要讨论采用链表结构表示的一元多项式的操作处理。在数学上,一个一元多项式Pn(x) 可以表示为 : Pn(x)=a0+a1x+a2x2+…+anxn (最多有n+1项) aixi是多项式的第i项(0≤i≤n)。其中ai为系数,x为自变量,i为指数。多项式中有n+1个系数,而且是线性排列。 一个多项式由多个aixi (1≤i≤m)项组成,每个多项式项采用以下结点存储:
其中,coef数据域存放序数ci;expn数据域存放指数ei;next域是一个链域,指向下一个结点。由此,一个多项式可以表示成由这些结点链接而成的单链表(假设该单链表是带头结点的单链表)。其中,coef数据域存放序数ci;expn数据域存放指数ei;next域是一个链域,指向下一个结点。由此,一个多项式可以表示成由这些结点链接而成的单链表(假设该单链表是带头结点的单链表)。 在计算机中,多项式可用一个线性表listP来表示:listP=(p0,p1,p2…,pn)。但这种表示无法分清每一项的系数和指数。所以可以采用另一种表示一元多项式的方法:listP={(a0,e0),(a1,e1),(a2,e2),…,(an,en) }。在这种线性表描述中,各个结点包括两个数据域,对应的类型描述为:
typedef struct node { double coef; //系数为双精度型 int expn; //指数为正整型 struct node *next; //指针域 }polynode; 在顺序存储结构中,采用基类型为polynode的数组表示多项式中的各项。如p[i].coef和p[i].expn分别表示多项式中第i项的系数和指数。但多项式中,其中一些项的系数会为0。如多项式A(x)=a0+a1x+a2x2+ a6x6+ a9x9+a15x15 中包括16项,其中只有6项系数不为0。顺序存储结构可以使多项式相加算法变得简单。但是,当多项式中存在大量的零系数时,这种表示方式就会浪费大量的存储空间。为了有效利用存储空间,可用链式存储结构表示多项式。 在链式存储结构中,多项式中每一个非零项构成链表中的一个结点,而对于系数为零的项则不需要表示。 (注意:表示多项式的链表应该是有序链表)
4.4.2 一元多项式相加 假设用单链表表示多项式:A(x)=12+7x+8x10+5x17 ,B(x)=8x+15x7-6x10,头指针Ah与Bh分别指向这两个链表,如图2-21所示: 对两个多项式进行相加运算,其结果为C(x)= 12+15x+15 x7+2x10+5x17。如图4.42所示。 图4-4-1 合并以前的链表 图 4-4-2 合并以后的链表
对两个一元多项式进行相加操作的运算规则是:假设指针qa和qb分别指向多项式A(x)和B(x)中当前进行比较的某个结点,则需比较两个结点数据域的指数项,有三种情况: (1) 指针qa所指结点的指数值<指针qb所指结点的指数值时,则保留qa指针所指向的结点,qa指针后移; (2) 指针qa所指结点的指数值>指针qb所指结点的指数值时,则将qb指针所指向的结点插入到qa所指结点前,qb指针后移; (3) 指针qa所指结点的指数值=指针qb所指结点的指数值时,将两个结点中的系数相加。若和不为零,则修改qa所指结点的系数值,同时释放qb所指结点;反之,从多项式A (x)的链表中删除相应结点,并释放指针qa和qb所指结点。
多项式相加算法: struct polynode *add_poly(struct polynode *Ah,struct polynode *Bh) { struct polynode *qa,*qb,*s,*r,*Ch; qa=Ah->next;qb=Bh->next; //qa和qb分别指向两个链表的第一结点 r=qa;Ch=Ah; //将链表Ah作为相加后的结果链表 while(qa!=NULL&&qb!=NULL) //两链表均非空 { if(qa->exp==qb->exp) //两者指数值相等 { x=qa->coef+qb->coef; if(x!=0) { qa->coef=x;r->next=qa;r=qa; s=qb++;free(s);qa++; } //相加后系数不为零时 else{s=qa++;free(s);s=qb++;free(s);} } //相加后系数为零时 else if(qa->exp<qb->exp) { r->next=qa;r=qa;qa++;} //多项式Ah的指数值小 else{r->next=qb;r=qb;qb++;}//多项式Bh的指数值小 }
if(qa==NULL) r->next=qb; else r->next=qa; //链接Ah或Bh中的剩余结点 return (Ch); } 上述算法的时间复杂性为O(n)。采用相同的方法还可以完成多项式的其他运算。
4.5 链表的其他操作 • 翻转单链表 4-17(参考§4-2) • 串接单链表 • 循环链表的操作 4.6 等价关系(选学) 4.7 稀疏矩阵的链表存储(自学)
图4-8 双向链表结点结构图 单链表只有一个指向后继的指针来表示结点间的逻辑关系。故从任一结点开始找其后继结点很方便,但要找前驱结点则比较困难。双向链式是用两个指针表示结点间的逻辑关系。即增加了一个指向其直接前驱的指针域,这样形成的链表有两条不同方向的链,前驱和后继,因此称为双链表。在双链表中,根据已知结点查找其直接前驱结点可以和查找其直接后继结点一样方便。这里也仅讨论带头结点的双链表。仍假设数据元素的类型为ElemType。 4.8 双向链表 双向链表结点的定义如下: typedef struct DNode{ ElemType data; struct DNode *prior; struct DNode *next; }Dnode,*DuLinkList; 结点的结构如图4-8所示。
双向链表中,每个结点都有一个指向直接前驱结点和一个指向直接后继结点的指针。链表中第一个结点的前驱指针和最后一个结点的后继指针可以为空,不做任何指向,这是简单的双向链表。 图4-16 带头结点的双向链表 在图4-16中,如果某指针变量p指向了一个结点,则通过该结点的指针p可以直接访问它的后继结点,即由指针p->next所指向的结点;也可以直接访问它的前驱结点,由指针p->prior指出。这样在需要查找前驱的操作中,就不必再从头开始遍历整个链表。这种结构极大地简化了某些操作。
结点的插入过程如图4-17所双向链表示: 注意:在图4-17中,关键语句指针操作序列既不是唯一也不是任意的。操作①必须在操作③之前完成,否则*p的前驱结点就丢掉了。 图4-17 双链表插入结点示意图
图4-18 双链表的删除结点示意图在双向链表中找到删除位置的前一个结点,由p指向它,q指向要删除的结点。删除操作如下:①将*p的next域改为指向待删结点*q的后继结点;②若*q不是指向最后的结点,则将*q之后结点的prior域指向*p。 注意:在双向链表中进行插入和删除时,对指针的修改需要同时修改结点的前驱指针和后继指针的指向。
【解】算法思路:扫描双向链表DL,查找链表DL的第i个位置并在第i个位置之后插入元素x。【解】算法思路:扫描双向链表DL,查找链表DL的第i个位置并在第i个位置之后插入元素x。 status ListInsert_DuL(DuLinkList DL,int i, ElemType x) { Dnode *s; if(!p=get_linklist(DL,i)) //p指向第i个元素结点, return ERROR; //p=NULL表示第i个元素不存在 if(!(s=(DulinkListmalloc(sizeof(Dnode)))) return OVERFLOW; s->data=x; //为新元素建结点 s->next=p->next; //插入 s->prior=p; p->next->prior=s; p->next=s return OK; }