56
22/6/9 1 第4第 第第 第第第第第第第第第第第第第第第第第第 第第第第第第第第第第 第第第第第第第第第第第第第第第第第第第第第第第第 第第第第第第第第第第第第第第第第 第第第第第第第第第第第第第第第 第第第第第第第 第第第第第第第第第第第第第第第第第第第第第 第第第第第第第第第第第第第

第 4 章 链表

Embed Size (px)

DESCRIPTION

第 4 章 链表. 本章主题: 有序表的链式存储 应用 教学目的: 掌握有序表链式存储方法和基本操作 教学重点: 链表的基本操作 教学难点: 多项式与稀疏矩阵的链表实现. 引入:线性表的两种基本存储结构:. 第 1 种:顺序存储结构:顺序表如数组. 顺序表具有以下两个基本特点: (1) 线性表的所有元素所占的存储空间是连续的。 (2) 表中各数据元素在存储空间中是按逻辑顺序依次存放的。 顺序存储结构不足之处: 1)数据元素最大个数需预先确定,使得高级程序语言编译系 统需预先分配相应的存储空间; - PowerPoint PPT Presentation

Citation preview

Page 1: 第 4 章    链表

23/4/19 1

第 4 章 链表

本章主题:本章主题:有序表的链式存储应用有序表的链式存储应用

教学目的:教学目的:掌握有序表链式存储方法和基本操作掌握有序表链式存储方法和基本操作

教学重点:教学重点:链表的基本操作链表的基本操作

教学难点:教学难点:多项式与稀疏矩阵的链表实现多项式与稀疏矩阵的链表实现

Page 2: 第 4 章    链表

23/4/19 2

引入:线性表的两种基本存储结构:引入:线性表的两种基本存储结构:

第第 11 种:顺序存储结构:顺序表如数组种:顺序存储结构:顺序表如数组•顺序表具有以下两个基本特点:顺序表具有以下两个基本特点: (1) (1) 线性表的所有元素所占的存储空间是连续的。线性表的所有元素所占的存储空间是连续的。 (2) (2) 表中各数据元素在存储空间中是按逻辑顺序依次存放的。表中各数据元素在存储空间中是按逻辑顺序依次存放的。

•顺序存储结构不足之处: 1 )数据元素最大个数需预先确定,使得高级程序语言编译系 统需预先分配相应的存储空间; 2 )插入与删除运算的效率很低。为了保持线性表中的元素顺 序在插入操作和删除操作时需移动大量数据。 3 )存储空间不便于扩充。

Page 3: 第 4 章    链表

23/4/19 3

线性表实现举例 1 :typedef int ElemType; /* ElemType 定义成 int 类型 */

typedef struct{/* 结构体 */ElemType data[MAXSIZE] ; /* 定义 int 类型的数组 , ElemType 即为 int */int length;

}SqList; // 取名

// 创建 n 个元素的列表 SqListvoid 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++;

}}

Page 4: 第 4 章    链表

23/4/19 4

完成下列基本操作的算法代码完成下列基本操作的算法代码//1 //1 创建列表创建列表 SqListSqList

void Creat_SqList(SqList *L , int n);void Creat_SqList(SqList *L , int n);

//2 //2 打印输出打印输出 SqListSqList

void Print_SqList(SqList *L);void Print_SqList(SqList *L);

//3 //3 在某一位置插入新的元素在某一位置插入新的元素 ......

void Insert_SqList(SqList *L,int i,ElemType x);void Insert_SqList(SqList *L,int i,ElemType x);

//4 //4 删除某一位置的元素删除某一位置的元素

void Delete_SqList(SqList *L,int i);void Delete_SqList(SqList *L,int i);

//5 //5 找某个元素是不是在找某个元素是不是在 SqListSqList 若在返回其位置若在返回其位置 .... 不在返回不在返回 -1-1

int Locate_SqList(SqList *L,ElemType x);int Locate_SqList(SqList *L,ElemType x);

//6 //6 反向函数 反向函数 void Reverse_SqList(SqList *L);void Reverse_SqList(SqList *L);

Page 5: 第 4 章    链表

23/4/19 5

【例 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

)3()1()21(

22

nnii

n

i

n

i

即最坏情况下移动数据的时间复杂性能为 O(n2)。

Page 6: 第 4 章    链表

23/4/19 6

第第 22 种 线性表的链式存储种 线性表的链式存储 为避免大量结点的移动,引入指针的线性表的链式存储结构,简称

链表 (Linked List) 。dat a next

数据域 指针域

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-14-1

Page 7: 第 4 章    链表

23/4/19 7

4.2 单向链表 单向链表的每个结点只有一个指向后继的指针。即:访问数据元素只能由链表头依次到链表尾,而不能做逆向访问。又称线性链表,这是一种最简单的链表。

对于空链表,头结点的指针域为空( NULL )图中用∧表示

图 4-2( a )为带头结点的空链 (b) 为带头结点的单链表

head

head

a1 a2 a3 ∧

( a) 空链表

b) ( 非空链表

Page 8: 第 4 章    链表

23/4/19 8

例 4-1[以 at 结尾的单词表 ] 建立单词链表的结构 :

typedef struct list_node *list_pointer;

typedef struct list_node{

char data[4];

list_pointer link;

};

1. 建立一个新的空表: list_pointer ptr=NULL;

2. 判空: #define is_empty(ptr) (!(ptr))

3. 创建一个新节点: ptr= (list_pointer)malloc(sizeof(list_node));

4. 添加单词 bat 入表 : strcpy(ptr->data,”bat”); ptr->link= NULL;

b a t \0 b a t \0 NULLNULL

ptrptr

First nodeFirst node | ptr->data | ptr->link || ptr->data | ptr->link |

Page 9: 第 4 章    链表

23/4/19 9

例 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);5. 表的输出: void print_list(list_pointer ptr);

Page 10: 第 4 章    链表

23/4/19 10

链表的基本操作实现 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;

Page 11: 第 4 章    链表

23/4/19 11

由于链表是一种动态管理的存储结构,每个结点需动态产生。1 .初始化链表 initlist(L) 的实现 建立一个空的带头结点的单链表。空链表的表长为 0 ,在这种情况下,

链表中没有元素结点。但应有一个头结点,其指针域为空。

void initlist(LinkList *L)

{ *L=(LNode *)malloc(sizeof(LNode));

(*L)->next=NULL;

}

在函数调用时,指针 L 指向的内容发生了变化,为使得调用函数中头指针变量 head 获得头结点地址,需传递头指针变量的地址给initlist() 函数,而函数中定义二级指针变量 L 接受该地址值,从而返回改变后的值。

Page 12: 第 4 章    链表

23/4/19 12

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); }

Page 13: 第 4 章    链表

23/4/19 13

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; }

Page 14: 第 4 章    链表

23/4/19 14

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; }

Page 15: 第 4 章    链表

23/4/19 15

5 .链表的插入算法 inselem(L,i,x) 的实现 单链表结点的插入是利用修改结点指针域的值,使其

指向新的链接位置来完成的插入操作,而无需移动任何元素。

假定在链表中值为 ai的结点之前插入一个新结点,要完成这种插入必须首先找到所插位置的前一个结点,再进行插入。假设指针 q 指向待插位置的前驱结点,指针 s 指向新结点,则完成该操作的过程如图 4-4所示。

Page 16: 第 4 章    链表

23/4/19 16

图 4-4 p 前插入 s 结点过程示意图

s=( LNode *) mal l oc( si zeof ( LNode) );s- >dat a=x;

( b) s x申请新结点 ,数据域置

xs

heada1 a2

. . . ai - 1

p

. . .ai an∧

xs

heada1 a2 ×. . . ai - 1

p

. . .ai an ∧

② ①

( a)找到插入位置

s- >next =q- >nextq- >next =s

( c) s修改指针域,将新结点 插入

关键语句:

q

q

上述指针进行相互赋值的语句顺序不能颠倒。

Page 17: 第 4 章    链表

23/4/19 17

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; }

Page 18: 第 4 章    链表

23/4/19 18

若传给函数的是待插入结点的地址,可编写如下的算法: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; }

Page 19: 第 4 章    链表

23/4/19 19

6 .链表的删除运算 delelem(L,i) 的实现 要删除链表中第 i 个结点,首先在单链表中找到删除位置前一个结点,并用指针 q 指向它,指针 p 指向要删除的结点。将 *q 的指针域修改为待删除结点 *p

的后继结点的地址。删除后的结点需动态的释放。对照 p92 4-4 表的删除算法

void delete (list_pointer *ptr, list_pointer trail, list_pointer node)

{ if(trail)

trail->link=node->link;

else

*ptr=(*ptr)->link;

free(node);

}

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);

}

Page 20: 第 4 章    链表

23/4/19 20

x=p- >dat a;

( b) x返回被删除结点数据

x

p

heada1 a2

. . . ai - 1

q

. . .ai an ∧

( a) p找到删除位置

heada1 . . .

( c) p修改指针域,将结点 删除

q

ai - 1

p

ai. . . an ∧ai +1

②关键语句: q- >next =p- >next;

f r ee( p)

图 4-5 线性链表的删除过程示意图

Page 21: 第 4 章    链表

23/4/19 21

在插入和删除算法中,都是先查询确定操作位置,然后再进行插入和删除操作。所以其时间复杂度均为 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");}

Page 22: 第 4 章    链表

23/4/19 22

【例 4- 】利用前面定义的基本运算函数,编写将一已知的单链表H 倒置的程序。如图 4-6 的操作。 (a) 为倒置前, (b) 为倒置后。

【算法基本思路】不另外增加新结点,而只是修改原结点的指针。设置指针 p ,令其指向 head->next ,并将 head->next 置空,然后依次将 p 所指链表的结点顺序摘下,插入到 head 之后即可。

heada1 a2

. . . ai - 1. . .ai an ∧

headan an- 1

. . . ai. . .ai - 1 a1 ∧

( a)倒置前

( b)倒置后

图 4-6 单链表的倒置

Page 23: 第 4 章    链表

23/4/19 23

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-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);

}

}

}

Page 24: 第 4 章    链表

23/4/19 24

4.3 4.3 栈的栈的链式链式存储结构及其基本运算的实现存储结构及其基本运算的实现 1.1.栈的链式存储结构栈的链式存储结构

栈的链式实现是以链表作为栈的存储结构,并在这种存储结构上实现栈栈的链式实现是以链表作为栈的存储结构,并在这种存储结构上实现栈的基本运算。栈的链式实现称为链栈的基本运算。栈的链式实现称为链栈

topA B C

栈顶 栈底

^

data next

链栈结构示意图链栈结构示意图

Page 25: 第 4 章    链表

23/4/19 25

链栈的几种状态 链栈的几种状态 ::

^

(a) top->next=NULL表示空栈 (b) A,B两个元素顺序入栈 (c) B元素出栈

top A

^

top

^

top

A

B

链栈的链栈的 CC 语言描述如下:语言描述如下: typedef struct snode { typedef struct snode {

//// 定义链栈结点类型定义链栈结点类型 ElemType data;ElemType data;

struct snode *next;struct snode *next;

}StackNode;}StackNode;

typedef struct{typedef struct{

StatckNode *top;//StatckNode *top;// 栈顶指针栈顶指针

}linkstack;}linkstack;

Page 26: 第 4 章    链表

23/4/19 26

22.基本运算在链式存储结构的实现.基本运算在链式存储结构的实现 栈初始化栈初始化 LinkSTACK InitStack(LinkSTACK *s)LinkSTACK InitStack(LinkSTACK *s)

{ {

s=(s=(LinkSTACKLinkSTACK*)malloc(sizeof(LinkSTACK));*)malloc(sizeof(LinkSTACK));

s->top=NULL;s->top=NULL;

}}

Page 27: 第 4 章    链表

23/4/19 27

//// 入栈运算入栈运算 void push(linkstack *s,elemtype x)void push(linkstack *s,elemtype x)

{ { StatckNode *p=(StatckNode*)malloc(sizeof(StatckNode));StatckNode *p=(StatckNode*)malloc(sizeof(StatckNode));

p->data = x;p->data = x;

p->next = s->top;p->next = s->top;

s->top = p;s->top = p;

}}

//// 判断栈是否为空判断栈是否为空int empty(linkstack *s)int empty(linkstack *s)

{{ if(s->top == NULL)if(s->top == NULL)

{ return 1;{ return 1;

}}

return 0; }return 0; }

Page 28: 第 4 章    链表

23/4/19 28

出栈运算出栈运算

elemtype pop(linkstack *s)elemtype pop(linkstack *s)

{ { elemtype x;elemtype x;

StatckNode *p = s->top;StatckNode *p = s->top;

if(empty(s))if(empty(s))

{ printf("Underflow!!\n"); exit (0);{ printf("Underflow!!\n"); exit (0);

}}

x=p->data;x=p->data;

s->top = p->next;s->top = p->next;

free(p);free(p);

return x;return x;

}}

Page 29: 第 4 章    链表

23/4/19 29

取栈顶元素取栈顶元素

elemtype gettop(linkstack *s)elemtype gettop(linkstack *s)

{ {

if(empty(s))if(empty(s))

{{

printf("Underflow!!\n");printf("Underflow!!\n");

exit (0);exit (0);

}}

return s->top->data;return s->top->data;

}}

Page 30: 第 4 章    链表

23/4/19 30

4.34.3.2 .2 队列的链式存储结构及其基本运算的实现队列的链式存储结构及其基本运算的实现

1. 1. 队列队列的链式存储结构的链式存储结构

队列的链式存储结构简称为队列的链式存储结构简称为链队链队。它实际上是一个同时带。它实际上是一个同时带有首指针和尾指针的单链表。头指针指向表头结点,而尾指有首指针和尾指针的单链表。头指针指向表头结点,而尾指针则指向队尾元素。针则指向队尾元素。

链队结构示意图链队结构示意图

... ^front

rear

data next

Page 31: 第 4 章    链表

23/4/19 31

链队的数据类型定义如下 :链队的数据类型定义如下 :

typedef struct qnode{ //typedef struct qnode{ // 链队结点的类型链队结点的类型

ElemType data;ElemType data;

struct qnode *next;struct qnode *next;

}QTYPE;}QTYPE;

typedef struct qptr{ //typedef struct qptr{ // 链队指针类型链队指针类型

QTYPE *front,*rear;QTYPE *front,*rear;

}SQUEUE;}SQUEUE;

SQUEUE LQ;SQUEUE LQ;

Page 32: 第 4 章    链表

23/4/19 32

链队运算指针变化情况 链队运算指针变化情况

(a) 队空

^front

rear

LQ

(b) 元素A入队后

^front

rear

LQA

(c) 元素B入队后

^front

rear

LQA B

(d) 元素A出队

^A B

front

rear

LQ

Page 33: 第 4 章    链表

23/4/19 33

22.基本运算.基本运算链链队的实现 队的实现 队列初始化队列初始化

void InitQueue(SQUEUE *LQ)void InitQueue(SQUEUE *LQ)

{ QTYPE *p;{ QTYPE *p;

p=(QTYPE *)malloc(sizeof(QTYPE));p=(QTYPE *)malloc(sizeof(QTYPE));

p->next=NULL;p->next=NULL;

LQ->front= LQ->rear=p;LQ->front= LQ->rear=p;

}}

Page 34: 第 4 章    链表

23/4/19 34

入队列入队列 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);}

Page 35: 第 4 章    链表

23/4/19 35

出队列出队列

int OutQueue(SQUEUE *LQ,ElemType *x)int OutQueue(SQUEUE *LQ,ElemType *x)

{ QTYPE *p;{ QTYPE *p;

if(Empty(LQ)){if(Empty(LQ)){

printf("\n Queue is free!");printf("\n Queue is free!");

return 0;}return 0;}

p=LQ->front->next;p=LQ->front->next;

*x=p->data;*x=p->data;

LQ->front->next=p->next;LQ->front->next=p->next;

if(LQ->front->next==NULL) LQ->rear=LQ->front; if(LQ->front->next==NULL) LQ->rear=LQ->front;

free(p);free(p);

return 1;return 1;

}}

Page 36: 第 4 章    链表

23/4/19 36

取队头元素取队头元素

int GetHead(SQUEUE *LQ,ElemType *x)

{ if(Empty(LQ)){

printf("\n Queue is free!");

return 0;

}

*x=LQ->front->next->data;

return 1;

}

Page 37: 第 4 章    链表

23/4/19 37

4.4 多项式4.4.1 多项式的单向链表示4.4.2 多项式加法4.4.3 多项式删除4.4.4 多项式的循环链表示

Page 38: 第 4 章    链表

23/4/19 38

4. 4 4. 4 线性表的应用线性表的应用 ———— 一元多项式计算一元多项式计算

4.4.1 一元多项式表示 链式存储结构的典型应用之一是在高等数学的多项式方面。本节主要讨论采用链表结构表示的一元多项式的操作处理。在数学上,一个一元多项式Pn(x) 可以表示为 :

Pn(x)=a0+a1x+a2x2+…+anx

n ( 最多有 n+1项 )

aixi 是多项式的第 i项( 0≤i≤n)。其中 ai 为系数, x 为自变量, i 为指

数。多项式中有 n+1 个系数,而且是线性排列。

一个多项式由多个 aixi (1≤i≤m) 项组成,每个多项式项采用以下结点存储:

coef expn next

Page 39: 第 4 章    链表

23/4/19 39

coef expn next

其中, coef 数据域存放序数 ci; expn 数据域存放指数 ei;

next 域是一个链域,指向下一个结点。由此,一个多项式可以表示成由这些结点链接而成的单链表 (假设该单链表是带头结点的单链表 ) 。

在计算机中,多项式可用一个线性表 listP来表示:listP=( p0, p1, p2…, pn)。但这种表示无法分清每一项的系数和指数。所以可以采用另一种表示一元多项式的方法:listP={( a0, e0), (a1, e1), (a2, e2) …, ,

(an, en) } 。在这种线性表描述中,各个结点包括两个数据域,对应的类型描述为:

Page 40: 第 4 章    链表

23/4/19 40

typedef struct node{ double coef; //系数为双精度型 int expn; // 指数为正整型 struct node *next; // 指针域}polynode;

在顺序存储结构中,采用基类型为 polynode 的数组表示多项式中的各项。如 p[i].coef和 p[i].expn 分别表示多项式中第 i项的系数和指数。但多项式中,其中一些项的系数会为 0 。如多项式 A(x)=a0+a1x+a2x

2+ a6x6+

a9x9+a15x

15 中包括 16项,其中只有 6项系数不为 0 。顺序存储结构可以使多项式相加算法变得简单。但是,当多项式中存在大量的零系数时,这种表示方式就会浪费大量的存储空间。为了有效利用存储空间,可用链式存储结构表示多项式。 在链式存储结构中,多项式中每一个非零项构成链表中的一个结点,而对于系数为零的项则不需要表示。

(注意:表示多项式的链表应该是有序链表)

Page 41: 第 4 章    链表

23/4/19 41

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 所示。12 0 7 1 8 10 5 17Ah

8 1 15 7 6 0Bh ∧

图 4-4-1 合并以前的链表

15 1 15 7 2 10 5 17 ∧012

Ch

图 4-4-2 合并以后的链表

Page 42: 第 4 章    链表

23/4/19 42

对两个一元多项式进行相加操作的运算规则是:假设指针 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 所指结点。

Page 43: 第 4 章    链表

23/4/19 43

多项式相加算法: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 的指数值小 }

Page 44: 第 4 章    链表

23/4/19 44

if(qa==NULL) r->next=qb; else r->next=qa; // 链接 Ah或 Bh 中的剩余结

点return (Ch);}

上述算法的时间复杂性为 O(n) 。采用相同的方法还可以完成多项式的其他运算。

Page 45: 第 4 章    链表

23/4/19 45

4.5 链表的其他操作• 翻转单链表 4-17 (参考 §4-2)

• 串接单链表• 循环链表的操作

4.6 等价关系(选学)

4.7 稀疏矩阵的链表存储(自学)

Page 46: 第 4 章    链表

23/4/19 46

4.8 双向链表

双向链表结点的定义如下: typedef struct DNode{ ElemType data; struct DNode *prior; struct DNode *next; }Dnode,*DuLinkList;结点的结构如图 4-8 所示。

单链表只有一个指向后继的指针来表示结点间的逻辑关系。故从任一结点开始找其后继结点很方便,但要找前驱结点则比较困难。双向链式是用两个指针表示结点间的逻辑关系。即增加了一个指向其直接前驱的指针域,这样形成的链表有两条不同方向的链,前驱和后继,因此称为双链表。在双链表中,根据已知结点查找其直接前驱结点可以和查找其直接后继结点一样方便。这里也仅讨论带头结点的双链表。仍假设数据元素的类型为ElemType。

图 4-8 双向链表结点结构图

pr i or dat a next

Page 47: 第 4 章    链表

23/4/19 47

∧ a1 a2∧an

. . .

∧ ∧ ( a)空双向链表

b)( 非空双向链表

head

head

图 4-16 带头结点的双向链表

在图 4-16 中,如果某指针变量 p 指向了一个结点,则通过该结点的指针 p 可以直接访问它的后继结点,即由指针 p->next 所指向的结点;也可以直接访问它的前驱结点,由指针 p->prior 指出。这样在需要查找前驱的操作中,就不必再从头开始遍历整个链表。这种结构极大地简化了某些操作。

双向链表中,每个结点都有一个指向直接前驱结点和一个指向直接后继结点的指针。链表中第一个结点的前驱指针和最后一个结点的后继指针可以为空,不做任何指向,这是简单的双向链表。

Page 48: 第 4 章    链表

23/4/19 48

结点的插入过程如图 4-17 所双向链表示:

注意:在图 4-17 中,关键语句指针操作序列既不是唯一也不是任意的。操作①必须在操作③之前完成,否则 *p 的前驱结点就丢掉了。

ai - 1

x

s- >dat a=x

s

ai

p

ai - 1

s

ai

p

( a)插入前的状态

x

×① ④ ③ ②

( b)插入过程

关键语句:① s- >pr i or =p- >pr i or ;② s- >next =p;③ p- >pr i or =s;④ s- >pr i or - >next =s;

图 4-17 双链表插入结点示意图

Page 49: 第 4 章    链表

23/4/19 49

② q- >next - >pr i or =p;

. . .. . . ai - 1 ai

q

ai +1

ai - 1 ai

q

ai +1

( a)删除前状态

( b)删除过程

. . .. . .

× ×

关键语句:① p- >next =q- >next ;

③ f r ee( q)

p

p

图 4-18 双链表的删除结点示意图在双向链表中找到删除位置的前一个结点,由 p 指向它, q 指向要删除的结点。删除操作如下:①将 *p的 next

域改为指向待删结点 *q 的后继结点;②若 *q 不是指向最后的结点,则将*q 之后结点的 prior 域指向 *p。

注意:在双向链表中进行插入和删除时,对指针的修改需要同时修改结点的前驱指针和后继指针的指向。

Page 50: 第 4 章    链表

23/4/19 50

【解】算法思路:扫描双向链表 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;

}

Page 51: 第 4 章    链表

23/4/19 51

4.8.2循环双链表 与循环单链表一样,也可以使用循环双链表。循环单链表和循环双链表可通过

尾结点找到头结点,也常作为编辑器的数据结构,尤其是循环双链表。 图 2-19是带头结点且有 n 个结点的循环双链表。

a1 a2 an. . .

( a)空双向循环链表

b)( 非空双向循环链表

head

head

图 2-19 带头结点且有 n 个结点的循环双链表

Page 52: 第 4 章    链表

23/4/19 52

在循环双向链表中,对于一些只涉及一个方向指针且存储结构不变的操作(如查找、求表长等),其算法实现与单链表相同。在进行插入或删除等结构变化的操作时,与双链表相同,必须同时修改两个方向上的指针,操作过程比单链表复杂。

【例 4-8 】编写从循环双向链表中删除第 i(i≥1) 个结点的算法。

【解】算法思想:扫描循环双向链表,查找第 i 个结点。若找到,修改其前趋结点的 next 域和后继结点的 prior 域,删除第 i 个( 1≤i≤ 表长)结点,返回OK;否则返回 ERROR 。当删除的结点是链表中的第一个结点时,除上述操作外,还需要修改头指针。

算法如下:

Page 53: 第 4 章    链表

23/4/19 53

status delete(DuLinklist head,int i,ElemType x)

{ // 从循环双向链表中删除第 i(i≥1) 个元素结点 Dnod *p; int j;

p=head;j=1;

while(p!=head && j<i) //寻找第 i 个元素结点 { p=p->next; ++j; }

if(p==head||j>i)return ERROR;// 链表中无第 i 个结点 x=p->data; // 否则,删除并用 x 返回其值 p->next->prior=p->prior;

p->prior->next=p->next;

if(i==1) head=head->next;//若 i=1, 修改头指针 head

return OK;

}

Page 54: 第 4 章    链表

23/4/19 54

链式存储结构克服了顺序存储结构的缺点 ,它的结点空间可以动态申请和释放;它的数据元素的逻辑次序靠结点的指针来指示,进行数据插入或删除时不需要移动数据元素。但是链式存储结构也有不足之处:①每个结点中的指针域需额外占用存储空间,当每个结点的数据域所占字节不多时,指针域所占存储空间的比重就显得很大;②链式存储结构是一种非随机存储结构。对任一结点的操作都要从指针链查找到该结点,这增加了算法的复杂度。

Page 55: 第 4 章    链表

23/4/19 55

本章小结本章小结

本章主要介绍的基本概念是本章主要介绍的基本概念是:

线性表线性表:一个线性表是 n≥0 个数据元素 a0, a1, a2 …, , an-

1 的有限序列。

线性表的顺序存储结构:在计算机中用一组地址连续的存储单元依次存储线性表的各个数据元素,称作线性表的顺序存储结构。

线性表的链式链式存储结构:线性表的链式存储结构就是用一组任意的存储单元——结点(可以是不连续的)存储线性表的数据元素。表中每一个数据元素都由存放数据元素值的数据域和存放直接前驱或直接后继的地址——指针域组成。

Page 56: 第 4 章    链表

23/4/19 56

循环链表循环链表:循环链表 (Circular Linked List) 是将单链表的最后一个结点的指针指向链表的表头结点,使整个链表形成环,从表中任一结点出发都可找到表中其它结点。

双向链表双向链表:双向链表中的每一个结点除了数据域外,还包含两个指针域:一个指针( next )指向该结点的后继结点,另一个指针( prior )指向它的前驱结点。

除基本概念以外,本章还介绍了:线性表的基本操作(初始化、插入、删除、存取、复制、合并)、线性表的顺序存储结构和链式存储结构的表示、一元多项式 Pn(x) 等。通过本章学习,要求读者不仅要了解基本概念,还要掌握顺序存储结构(初始化、插入操作、删除操作)、单链表(单链表的初始化、单链表的插入、单链表的删除)等操作。