第2章 线性表 2.1 线性表的基本概念 2.2 线性表的顺序存储 2.3 线性表的链式存储 2.4 线性表的应用 2.5 有序表 本章小结
2.1 线性表的基本概念 2.1.1 线性表的定义 2.1.2 线性表的运算
2.1.1 线性表的定义 线性表是具有相同特性的数据元素的一个有限序列。该序列中所含元素的个数叫做线性表的长度,用n表示,n≥0。 当n=0时,表示线性表是一个空表,即表中不包含任何元素。设序列中第i(i表示位序)个元素为ai(1≤i≤n)。 线性表的一般表示为: (a1,a2,…ai,ai+1,…,an)
其中a1为第一个元素,又称做表头元素,a2为第二个元素,an为最后一个元素,又称做表尾元素。 例如,在线性表 (1,4,3,2,8,10) 中,1为表头元素,10为表尾元素。
2.1.2 线性表的运算 线性表的基本运算如下: (1) 初始化线性表InitList(&L):构造一个空的线性表L。 (2) 销毁线性表DestroyList(&L):释放线性表L占用的内存空间。
(3) 判线性表是否为空表ListEmpty(L):若L为空表,则返回真,否则返回假。 (4) 求线性表的长度ListLength(L):返回L中元素个数。 (5) 输出线性表DispList(L):当线性表L不为空时,顺序显示L中各结点的值域。 (6) 求线性表L中指定位置的某个数据元素GetElem(L,i,&e):用e返回L中第 i(1≤i≤ListLength(L))个元素的值。
(7) 定位查找LocateElem(L,e):返回L中第1个值域与e相等的位序。若这样的元素不存在,则返回值为0。 (8) 插入数据元素ListInsert(&L,i,e):在L的第i(1≤i≤ListLength(L)+1)个元素之前插入新的元素e,L的长度增1。 (9) 删除数据元素ListDelete(&L,i,&e):删除L的第i(1≤i≤ListLength(L))个元素,并用e返回其值,L的长度减1。
例2.1 假设有两个集合 A 和 B 分别用两个线性表 LA 和 LB 表示,即线性表中的数据元素即为集合中的成员。编写一个算法求一个新的集合C=A∪B,即将两个集合的并集放在线性表LC中。 解:本算法思想是:先初始化线性表LC,将LA的所有元素复制到LC中,然后扫描线性表LB,若LB的当前元素不在线性表LA中,则将其插入到LC中。算法如下:
void unionList(List LA,List LB,List &LC) { int lena,lenc,i; ElemType e; InitList(LC); for (i=1;i<=ListLength(LA);i++) /*将LA的所有元素插入到Lc中*/ { GetElem(LA,i,e); ListInsert(LC,i,e); } lena=ListLength(LA); /*求线性表的长度*/ lenb=ListLength(LB);
for (i=1;i<=lenb;i++) { GetElem(LB,i,e); /*取LB中第i个数据元素赋给e*/ if (!LocateElem(LA,e)) ListInsert(LC,++lena,e); /*LA中不存在和e相同者,则插入到LC中*/ }
由于LocateElem(LA,e)运算的时间复杂度为O(ListLength(LA)),所以本算法的时间复杂度为: O(ListLength(LA)*ListLength(LB))。
2.2 线性表的顺序存储 2.2.1 线性表的顺序存储—顺序表 2.2.2 顺序表基本运算的实现
2.2.1 线性表的顺序存储—顺序表 线性表的顺序存储结构就是:把线性表中的所有元素按照其逻辑顺序依次存储到从计算机存储器中指定存储位置开始的一块连续的存储空间中。 这样,线性表中第一个元素的存储位置就是指定的存储位置,第i+1个元素(1≤i≤n-1)的存储位置紧接在第i个元素的存储位置的后面。
假定线性表的元素类型为ElemType,则每个元素所占用存储空间大小(即字节数)为sizeof(ElemType),整个线性表所占用存储空间的大小为: n*sizeof(ElemType) 其中,n表示线性表的长度。
顺序表示意图
在定义一个线性表的顺序存储类型时,需要定义一个数组来存储线线表中的所有元素和定义一个整型变量来存储线性表的长度。 假定数组用data[MaxSize]表示,长度整型变量用length表示,并采用结构体类型表示,则元素类型为通用类型标识符ElemType的线性表的顺序存储类型可描述如下:
其中,data成员存放元素,length成员存放线性表的实际长度。 typedef struct { ElemType data[MaxSize]; int length; } SqList; /*顺序表类型*/ 其中,data成员存放元素,length成员存放线性表的实际长度。 说明:由于C/C++中数组的下标从0开始,线性表的第i个元素ai存放顺序表的第i-1位置上。为了清楚,我们ai在逻辑序列中的位置称为逻辑位序,在顺序表中的位置称为物理位序。
2.2.2 顺序表基本运算的实现 一旦采用顺序表存储结构,我们就可以用C/C++语言实现线性表的各种基本运算。为了方便,假设ElemType为char类型,使用如下自定义类型语句: typedef char ElemType;
其方法是将给定的含有n个元素的数组的每个元素依次放入到顺序表中,并将n赋给顺序表的长度成员。算法如下: 建立顺序表 其方法是将给定的含有n个元素的数组的每个元素依次放入到顺序表中,并将n赋给顺序表的长度成员。算法如下: void CreateList(SqList *&L,ElemType a[],int n) /*建立顺序表*/ { int i; for (i=0;i<n;i++) L->data[i]=a[i]; L->length=n; }
该运算的结果是构造一个空的线性表L。实际上只需将length成员设置为0即可。 顺序表基本运算算法 (1) 初始化线性表InitList(L) 该运算的结果是构造一个空的线性表L。实际上只需将length成员设置为0即可。 void InitList(SqList *&L) //引用型指针 { L=(SqList *)malloc(sizeof(SqList)); /*分配存放线性表的空间*/ L->length=0; } 本算法的时间复杂度为O(1)。
(2) 销毁线性表DestroyList(L) 该运算的结果是释放线性表L占用的内存空间。 void DestroyList(SqList *&L) { free(L); } 本算法的时间复杂度为O(1)。
(3) 判定是否为空表ListEmpty(L) 该运算返回一个值表示L是否为空表。若L为空表,则返回1,否则返回0。 int ListEmpty(SqList *L) { return(L->length==0); } 本算法的时间复杂度为O(1)。
(4) 求线性表的长度ListLength(L) 该运算返回顺序表L的长度。实际上只需返回length成员的值即可。 int ListLength(SqList *L) { return(L->length); } 本算法的时间复杂度为O(1)。
该运算当线性表L不为空时,顺序显示L中各元素的值。 (5) 输出线性表DispList(L) 该运算当线性表L不为空时,顺序显示L中各元素的值。 void DispList(SqList *L) { int i; if (ListEmpty(L)) return; for (i=0;i<L->length;i++) printf("%c",L->data[i]); printf("\n"); }
本算法中基本运算为for循环中的printf语句,故时间复杂度为: O(L->length)或O(n)
(6) 求某个数据元素值GetElem(L,i,e) 该运算返回L中第 i(1≤i≤ListLength(L))个元素的值,存放在e中。 int GetElem(SqList *L,int i,ElemType &e) { if (i<1 || i>L->length) return 0; e=L->data[i-1]; return 1; } 本算法的时间复杂度为O(1)。
(7) 按元素值查找LocateElem(L,e) 该运算顺序查找第1个值域与e相等的元素的位序。若这样的元素不存在,则返回值为0。 int LocateElem(SqList *L, ElemType e) { int i=0; while (i<L->length && L->data[i]!=e) i++; if (i>=L->length) return 0; else return i+1; }
本算法中基本运算为while循环中的i++语句,故时间复杂度为: O(L->length)或O(n)
(8) 插入数据元素ListInsert(L,i,e) 该运算在顺序表L的第i个位置(1≤i≤ListLength(L)+1)上插入新的元素e。 思路:如果i值不正确,则显示相应错误信息;否则将顺序表原来第i个元素及以后元素均后移一个位置,腾出一个空位置插入新元素,顺序表长度增1。
int ListInsert(SqList *&L,int i,ElemType e) { int j; if (i<1 || i>L->length+1) return 0; i--; /*将顺序表逻辑位序转化为elem下标即物理位序*/ for (j=L->length;j>i;j--) L->data[j]=L->data[j-1]; /*将data[i]及后面元素后移一个位置*/ L->data[i]=e; L->length++; /*顺序表长度增1*/ return 1; }
对于本算法来说,元素移动的次数不仅与表长L 对于本算法来说,元素移动的次数不仅与表长L.length=n有关,而且与插入位置i有关:当i=n+1时,移动次数为0;当i=1时,移动次数为n,达到最大值。在线性表sq中共有n+1个可以插入元素的地方。假设pi(= )是在第i个位置上插入一个元素的概率,则在长度为n的线性表中插入一个元素时所需移动元素的平均次数为: 因此插入算法的平均时间复杂度为O(n)。
(9) 删除数据元素ListDelete(L,i,e) 删除顺序表L中的第i(1≤i≤ListLength(L))个元素。 思路:如果i值不正确,则显示相应错误信息;否则将线性表第i个元素以后元素均向前移动一个位置,这样覆盖了原来的第i个元素,达到删除该元素的目的,最后顺序表长度减1。
int ListDelete(SqList *&L,int i,ElemType &e) { int j; if (i<1 || i>L->length) return 0; i--; /*将顺序表逻辑位序转化为elem下标即物理位序*/ e=L->data[i]; for (j=i;j<L->length-1;j++) L->data[j]=L->data[j+1]; /*将data[i]之后的元素前移一个位置*/ L->length--; /*顺序表长度减1*/ return 1; }
对于本算法来说,元素移动的次数也与表长n和删除元素的位置i有关:当i=n时,移动次数为0;当i=1时,移动次数为n-1。在线性表sq中共有n个元素可以被删除。假设pi(pi= )是删除第i个位置上元素的概率,则在长度为n的线性表中删除一个元素时所需移动元素的平均次数为: = 因此删除算法的平均时间复杂度为O(n)。
例2.2 设计一个算法,将x插入到一个有序(从小到大排序)的线性表(顺序存储结构即顺序表)的适当位置上,并保持线性表的有序性。 解:先通过比较在顺序表L中找到存放x的位置i,然后将x插入到L.data[i]中,最后将顺序表的长度增1。
void Insert(SqList *&L,ElemType x) { int i=0,j; while (i<L->length && L->data[i]<x) i++; for (j=L->length-1;j>=i;j--) L->data[j+1]=L->data[j]; L->data[i]=x; L->length++; } 查找插入位置 元素后移一个位置 L[0] L[1] … L[i] L[i+1]… L[n-1]
例2.3 设计一个算法,将两个元素有序(从小到大)的顺序表合并成一个有序顺序表。 思路:将两个顺序表进行二路归并。例如: 顺序表p:1,3,5 ←i扫描p 顺序表q:2,4,10,20 ←j扫描q 归并到顺序表r中 ← k记录r中元素个数 1(i=0) 2(j=0) 将1(i=1)插入r(k=1) 3(i=1) 2(j=0) 将2(j=1)插入r(k=2) 3(i=1) 4(j=1) 将3(i=2)插入r(k=3) 5(i=2) 4(j=1) 将4(j=2)插入r(k=4) 5(i=2) 10(j=2) 将5(j=3)插入r(k=5) 将q中余下元素插入r中。
SqList *merge(SqList *p, SqList *q) { SqList *r; int i=0,j=0,k=0; r=(SqList *)malloc(sizeof(SqList)); while (i<p->length && j<q->length) { if (p->data[i]<q-> data[j]) { r-> data[k]=p-> data[i]; i++;k++; } else { r-> data[k]=q-> data[j]; j++;k++;
while (i<p->length) { r->data[k]=p->data[i]; i++;k++; } while (j<q->length) { r->data[k]=q->data[j]; j++;k++; r->length=k; /*或p->length+q->length*/ return(r);
例2.4 已知长度为n的线性表A采用顺序存储结构,编写一个时间复杂度为O(n)、空间复杂度为O(1)的算法,该算法删除线性表中所有值为item的数据元素。 解:用k记录顺序表A中等于item的元素个数,边扫描A边统计k,并将不为item的元素前移k个位置,最后修改A的长度。对应的算法如下:
void delnode1(SqList &A,ElemType item) { int k=0,i; /*k记录值不等于item的元素个数*/ for (i=0;i<A.length;i++) if (A.data[i]!=item) { A.data[k]=A.data[i]; k++; /*不等于item的元素增1*/ } A.length=k; /*顺序表A的长度等于k*/ 算法1:类似于建顺序表
void delnode2(SqList &A,ElemType item) { int k=0,i=0; /*k记录值等于item的元素个数*/ while (i<A.length) { if (A.data[i]==item) k++; else A.data[i-k]=A.data[i]; /*当前元素前移k个位置*/ i++; } A.length=A.length-k; /*顺序表A的长度递减*/ 算法2
上述算法中只有一个while循环,时间复杂度为O(n)。算法中只用了i,k两个临时变量,空间复杂度为O(1)。
2.3 线性表的链式存储 2.3.1 线性表的链式存储—链表 2.3.2 单链表基本运算的实现 2.3.3 双链表 2.3.4 循环链表 2.3.5 静态链表
2.3.1 线性表的链式存储—链表 在链式存储中,每个存储结点不仅包含有所存元素本身的信息(称之为数据域),而且包含有元素之间逻辑关系的信息,即前驱结点包含有后继结点的地址信息,这称为指针域,这样可以通过前驱结点的指针域方便地找到后继结点的位置,提高数据查找速度。 一般地,每个结点有一个或多个这样的指针域。若一个结点中的某个指针域不需要任何结点,则仅它的值为空,用常量NULL表示。
由于顺序表中的每个元素至多只有一个前驱元素和一个后继元素,即数据元素之间是一对一的逻辑关系,所以当进行链式存储时,一种最简单也最常用的方法是: 在每个结点中除包含有数据域外,只设置一个指针域,用以指向其后继结点,这样构成的链接表称为线性单向链接表,简称单链表;
在线性表的链式存储中,为了便于插入和删除算法的实现,每个链表带有一个头结点,并通过头结点的指针惟一标识该链表。 带头结点单链表示意图
在单链表中,由于每个结点只包含有一个指向后继结点的指针,所以当访问过一个结点后,只能接着访问它的后继结点,而无法访问它的前驱结点。
另一种可以采用的方法是:在每个结点中除包含有数值域外,设置有两个指针域,分别用以指向其前驱结点和后继结点,这样构成的链接表称之为线性双向链接表,简称双链表。
带头结点的双链表示意图
双链表的特点 在双链表中,由于每个结点既包含有一个指向后继结点的指针,又包含有一个指向前驱结点的指针,所以当访问过一个结点后,既可以依次向后访问每一个结点,也可以依次向前访问每一个结点。
在单链表中,假定每个结点类型用LinkList表示,它应包括存储元素的数据域,这里用data表示,其类型用通用类型标识符ElemType表示,还包括存储后继元素位置的指针域,这里用next表示。 typedef struct LNode /*定义单链表结点类型*/ { ElemType data; struct LNode *next; /*指向后继结点*/ } LinkList;
2.3.2 单链表基本运算的实现 1. 建立单链表 先考虑如何建立单链表。假设我们通过一个含有n个数据的数组来建立单链表。建立单链表的常用方法有如下两种: (1) 头插法建表 该方法从一个空表开始,读取字符数组a中的字符,生成新结点,将读取的数据存放到新结点的数据域中,然后将新结点插入到当前链表的表头上,直到结束为止。采用头插法建表的算法如下:
void CreateListF(LinkList *&L,ElemType a[],int n) { LinkList *s;int i; L=(LinkList *)malloc(sizeof(LinkList)); /*创建头结点*/ L->next=NULL; for (i=0;i<n;i++) { s=(LinkList *)malloc(sizeof(LinkList)); /*创建新结点*/ s->data=a[i]; s->next=L->next; /*将*s插在原开始结点之前,头结点之后*/ L->next=s; }
a d c b a d a c d a b c d a 采用头插法建立单链表的过程 head i=0 i=1 i=2 i=3 head 第1步:建头结点 head ∧ i=0 i=1 i=2 i=3 第2步:i=0,新建a结点,插入到头结点之后 head a ∧ 第3步:i=1,新建d结点,插入到头结点之后 head d a ∧ 第4步:i=2,新建c结点,插入到头结点之后 head c d a ∧ 第5步:i=3,新建b结点,插入到头结点之后 head b c d a ∧
(2) 尾插法建表 头插法建立链表虽然算法简单,但生成的链表中结点的次序和原数组元素的顺序相反。若希望两者次序一致,可采用尾插法建立。该方法是将新结点插到当前链表的表尾上,为此必须增加一个尾指针r,使其始终指向当前链表的尾结点。采用尾插法建表的算法如下:
void CreateListR(LinkList *&L,ElemType a[],int n) { LinkList *s,*r;int i; L=(LinkList *)malloc(sizeof(LinkList)); /*创建头结点*/ r=L; /*r始终指向终端结点,开始时指向头结点*/ for (i=0;i<n;i++) { s=(LinkList *)malloc(sizeof(LinkList)); /*创建新结点*/ s->data=a[i];r->next=s; /*将*s插入*r之后*/ r=s; } r->next=NULL; /*终端结点next域置为NULL*/
采用尾插法建立单链表的过程 a d c b i=0 i=1 i=2 i=3 head a d c b ∧ b 头结点
2. 插入结点运算 插入运算是将值为x的新结点插入到单链表的第i个结点的位置上。先在单链表中找到第i-1个结点,再在其后插入新结点。 单链表插入结点的过程如下图所示。
插入结点示意图
3. 删除结点运算 删除运算是将单链表的第i个结点删去。先在单链表中找到第i-1个结点,再删除其后的结点。删除单链表结点的过程如下图所示。
删除结点示意图
该运算建立一个空的单链表,即创建一个头结点。 void InitList(LinkList *&L) 4. 线性表基本运算实现 (1) 初始化线性表InitList(L) 该运算建立一个空的单链表,即创建一个头结点。 void InitList(LinkList *&L) { L=(LinkList *)malloc(sizeof(LinkList)); /*创建头结点*/ L->next=NULL; }
(2) 销毁线性表DestroyList(L) void DestroyList(LinkList *&L) { LinkList *p=L,*q=p->next; while (q!=NULL) { free(p); p=q;q=p->next; } free(p);
(3) 判线性表是否为空表ListEmpty(L) 若单链表L没有数据结点,则返回真,否则返回假。 int ListEmpty(LinkList *L) { return(L->next==NULL); }
(4) 求线性表的长度ListLength(L) int ListLength(LinkList *L) { LinkList *p=L;int i=0; while (p->next!=NULL) { i++; p=p->next; } return(i);
逐一扫描单链表L的每个数据结点,并显示各结点的data域值。 (5) 输出线性表DispList(L) 逐一扫描单链表L的每个数据结点,并显示各结点的data域值。 void DispList(LinkList *L) { LinkList *p=L->next; while (p!=NULL) { printf("%c",p->data); p=p->next; } printf("\n");
(6)求线性表L中指定位置的某个数据元素GetElem(L,i,&e) 思路:在单链表L中从头开始找到第 i个结点,若存在第i个数据结点,则将其data域值赋给变量e。
int GetElem(LinkList *L,int i,ElemType &e) { int j=0; LinkList *p=L; while (j<i && p!=NULL) { j++; p=p->next; } if (p==NULL) return 0; /*不存在第i个数据结点*/ else /*存在第i个数据结点*/ { e=p->data; return 1;
(7) 按元素值查找LocateElem(L,e) int LocateElem(LinkList *L,ElemType e) { LinkList *p=L->next;int n=1; while (p!=NULL && p->data!=e) { p=p->next; n++; } if (p==NULL) return(0); else return(n); }
(8) 插入数据元素ListInsert(&L,i,e) 思路:先在单链表L中找到第i-1个结点*p,若存在这样的结点,将值为e的结点*s插入到其后。 int ListInsert(LinkList *&L,int i,ElemType e) { int j=0; LinkList *p=L,*s; while (j<i-1 && p!=NULL) /*查找第i-1个结点*/ { j++; p=p->next; }
if (p==NULL) return 0; /*未找到位序为i-1的结点*/ else /*找到位序为i-1的结点*p*/ { s=(LinkList *)malloc(sizeof(LinkList)); /*创建新结点*s*/ s->data=e; s->next=p->next; /*将*s插入到*p之后*/ p->next=s; return 1; }
(9) 删除数据元素ListDelete(&L,i,&e) 思路:先在单链表L中找到第i-1个结点*p,若存在这样的结点,且也存在后继结点,则删除该后继结点。 int ListDelete(LinkList *&L,int i,ElemType &e) { int j=0; LinkList *p=L,*q; while (j<i-1 && p!=NULL) /*查找第i-1个结点*/ { j++; p=p->next; }
if (p==NULL) return 0; /*未找到位序为i-1的结点*/ else /*找到位序为i-1的结点*p*/ { q=p->next; /*q指向要删除的结点*/ if (q==NULL) return 0; /*若不存在第i个结点,返回0*/ p->next=q->next; /*从单链表中删除*q结点*/ free(q); /*释放*q结点*/ return 1; }
例2.5 设C={a1,b1,a2,b2,…,an,bn}为一线性表,采用带头结点的hc单链表存放,编写一个算法,将其拆分为两个线性表,使得: A={a1,a2,…,an},B={b1,b2,…,bn}
解: 设拆分后的两个线性表都用带头结点的单链表存放。 先建立两个头结点*ha和*hb,它们用于存放拆分后的线性表A和B,ra和rb分别指向这两个单链表的表尾,用p指针扫描单链表hc,将当前结点*p链到ha未尾,p沿next域下移一个结点,若不为空,则当前结点*p链到hb未尾,p沿next域下移一个结点,如此这样,直到p为空。最后将两个尾结点的next域置空。 对应算法如下:
void fun(LinkList *hc, LinkList *&ha, LinkList *&hb) { LinkList *p=hc->next,*ra,*rb; ha=hc; /*ha的头结点利用hc的头结点*/ ra=ha; /*ra始终指向ha的末尾结点*/ hb=(LinkList *)malloc(sizeof(LinkList)); /*创建hb头结点*/ rb=hb; /*rb始终指向hb的末尾结点*/
while (p!=NULL) { ra->next=p;ra=p; /*将*p链到ha单链表未尾*/ p=p->next; if (p!=NULL) { rb->next=p; rb=p; /*将*p链到hb单链表未尾*/ } ra->next=rb->next=NULL; /*两个尾结点的next域置空*/
本算法实际上是采用尾插法建立两个新表。所以,尾插法建表算法是很多类似习题的基础!
例2.6 有一个带头结点的单链表head,其ElemType类型为char,设计一个算法使其元素递增有序。 解:若原单链表中有一个或以上的数据结点,先构造只含一个数据结点的有序表(只含一个数据结点的单链表一定是有序表)。扫描原单链表余下的结点*p(直到p==NULL为止),在有序表中通过比较找插入*p的前驱结点*q,然后将*p插入到*q之后(这里实际上采用的是直接插入排序方法)。
void Sort(LinkList *&head) { LinkList *p=head->next,*q,*r; if (p!=NULL) /*head有一个或以上的数据结点*/ { r=p->next; /*r保存*p结点后继结点的指针*/ p->next=NULL; /*构造只含一个数据结点的有序表*/ p=r; while (p!=NULL) { r=p->next; /*r保存*p结点后继结点的指针*/
q=head; while (q->next!=NULL && q->next->data<p->data) q=q->next; /*在有序表中找插入*p的前驱结点*q*/ p->next=q->next; /*将*p插入到*q之后*/ q->next=p; p=r; /*扫描原单链表余下的结点*/ }
对于双链表,采用类似于单链表的类型定义,其DLinkList类型的定义如下: 2.3.3 双链表 对于双链表,采用类似于单链表的类型定义,其DLinkList类型的定义如下: typedef struct DNode /*定义双链表结点类型*/ { ElemType data; struct DNode *prior; /*指向前驱结点*/ struct DNode *next; /*指向后继结点*/ } DLinkList;
在双链表中,有些操作如求长度、取元素值和查找元素等操作算法与单链表中相应算法是相同的,这里不多讨论。但在单链表中,进行结点插入和删除时涉及到前后结点的一个指针域的变化。而在双链表中,结点的插入和删除操作涉及到前后结点的两个指针域的变化。
双链表中插入结点示意图
归纳起来,在双链表中p所指的结点之后插入一个*s结点。其操作语句描述为: s->next=p->next;/*将*s插入到*p之后*/ p->next->prior=s; s->prior=p; p->next=s;
在双链表中删除一个结点的过程如右图所示: 删除结点示意图
归纳起来,删除双链表L中*p结点的后续结点。其操作语句描述为: p->next=q->next; q->next->prior=p;
2.3.4 循环链表 循环链表是另一种形式的链式存储结构。它的特点是表中最后一个结点的指针域不再是空,而是指向表头结点,整个链表形成一个环。由此,从表中任一结点出发均可找到链表中其他结点。
带头结点的循环单链表和循环双链表的示意图
例2.7 编写出判断带头结点的双向循环链表L是否对称相等的算法。 解:p从左向右扫描L,q从右向左扫描L,若对应数据结点的data域不相等,则退出循环,否则继续比较,直到p与q相等或p的下一个结点为*q为止。对应算法如下:
int Equeal(DLinkList *L) { int same=1; DLinkList *p=L->next; /*p指向第一个数据结点*/ DLinkList *q=L->prior; /*q指向最后数据结点*/ while (same==1) if (p->data!=q->data) same=0; else { if (p==q) break; /*数据结点为奇数的情况*/ q=q->prior; if (p==q) break; /*数据结点为偶数的情况*/ p=p->next; } return same;
2.3.5 静态链表 静态链表借用一维数组来描述线性链表。数组中的一个分量表示一个结点,同时使用游标(指示器cur即为伪指针)代替指针以指示结点在数组中的相对位置。数组中的第0个分量可以看成头结点,其指针域指示静态链表的第一个结点。 这种存储结构仍然需要预先分配一个较大空间,但是在进行线性表的插入和删除操作时不需要移动元素,仅需要修改“指针”,因此仍然具有链式存储结构的主要优点。
下图给出了一个静态链表的示例。图(a)是一个修改之前的静态链表,图(b)是删除数据元素“陈华”之后的静态链表,图(c)插入数据元素“王华”之后的静态链表,图中用阴影表示修改的游标。
2.4 线性表的应用 计算任意两个表的简单自然连接过程讨论线性表的应用。假设有两个表A和B,分别是m1行、n1列和m2行、n2列,它们简单自然连接结果C=A B,其中i表示表A中列号,j表示表B中的列号,C为A和B的笛卡儿积中满足指定连接条件的所有记录组,该连接条件为表A的第i列与表B的第j列相等。例如: i=j
C=A B的计算结果如下: 3=1
由于每个表的行数不确定,为此,用单链表作为表的存储结构,每行作为一个数据结点。另外,每行中的数据个数也是不确定的,但由于提供随机查找行中的数据,所以每行的数据采用顺序存储结构,这里用长度为MaxCol的数组存储每行的数据。因此,该单链表中数据结点类型定义如下: #define MaxCol 10 /*最大列数*/ typedef struct Node1 /*定义数据结点类型*/ { ElemType data[MaxCol]; struct Node1 *next; /*指向后继数据结点*/ } DList;
另外,需要指定每个表的行数和列数,为此将单链表的头结点类型定义如下: typedef struct Node2 /*定义头结点类型*/ { int Row,Col; /*行数和列数*/ DList *next; /*指向第一个数据结点*/ } HList; 采用尾插法建表方法创建单链表,用户先输入表的行数和列数,然后输入各行的数据,为了简便,假设表中数据为int型,因此定义: typedef int ElemType; 对应的建表算法如下:
void create(HList *&h) { int i,j; DList *r,*s; h=(HList *)malloc(sizeof(HList));h->next=NULL; printf("表的行数,列数:"); scanf("%d%d",&h->Row,&h->Col); for (i=0;i<h->Row;i++) { printf(" 第%d行:",i+1); s=(DList *)malloc(sizeof(DList)); for (j=0;j<h->Col;j++) scanf("%d",&s->data[j]); if (h->next==NULL) h->next=s; else r->next=s; r=s; /*r始终指向最后一个数据结点*/ } r->next=NULL; 采用尾插法建表
对应的输出表的算法如下: void display(HList *h) { int j; DList *p=h->next; while (p!=NULL) { for (j=0;j<h->Col;j++) printf("%4d",p->data[j]); printf("\n"); p=p->next; }
为了实现两个表h1和h2的简单自然连接,先要输入两个表连接的列序号f1和f2,然后扫描单链表h1,对于h1的每个结点,从头至尾扫描单链表h2,若自然连接条件成立,即h1的当前结点*p和h2的当前结点*q满足: p->data[f1-1]==q->data[f2-1] 则在新建单链表h中添加一个新结点。 新建的单链表h也是采用尾插法建表方法创建的。 实现两个表h1和h2的简单自然连接并生成结果h的算法如下:
void link(HList *h1,HList *h2,HList *&h) { int f1,f2,i;DList *p=h1->next,*q,*s,*r; printf("连接字段是:第1个表位序,第2个表位序:"); scanf("%d%d",&f1,&f2); h=(HList *)malloc(sizeof(HList)); h->Row=0; h->Col=h1->Col+h2->Col; h->next=NULL; while (p!=NULL) { q=h2->next;
while (q!=NULL) { if (p->data[f1-1]==q->data[f2-1]) /*对应字段值相等*/ { s=(DList *)malloc(sizeof(DList)); /*创建一个数据结点*/ for (i=0;i<h1->Col;i++) /*复制表1的当前行*/ s->data[i]=p->data[i]; for (i=0;i<h2->Col;i++) s->data[h1->Col+i]=q->data[i];/*复制表2的当前行*/ if (h->next==NULL) h->next=s; else r->next=s; r=s; /*r始终指向最后数据结点*/ h->Row++; /*表行数增1*/ } q=q->next; /*表2下移一个记录*/ p=p->next; /*表1下移一个记录*/ r->next=NULL;/*表尾结点next域置空*/ 尾插法建表
2.5 有序表 所谓有序表,是指这样的线性表,其中所有元素以递增或递减方式排列,并规定有序表中不存在元素值相同的元素。在这里仍以顺序表进行存储。 其中只有ListInsert()基本运算与前面的顺序表对应的运算有所差异,其余都是相同的。有序表的ListInsert()运算对应的算法如下:
int ListInsert(SqList &L,ElemType e) { int i=0,j; while (i<L.length && L.data[i]<e) i++; if (L.data[i]==e) return 0; for (j=ListLength(L);j>i;j--) /*将data[i]及后面元素后移一个位置*/ L.data[j]=L.data[j-1]; L.data[i]=e; L.length++; /*顺序表长度增1*/ return 1; }
本章小结 本章的基本学习要点如下: (1) 理解线性表的逻辑结构特性。 (2) 深入掌握线性表的两种存储方法,即顺序表和链表。体会这两种存储结构之间的差异。 (3) 重点掌握顺序表和链表上各种基本运算的实现。 (4) 综合运用线性表这种数据结构解决一些复杂的实际问题。
练习题2 习题2、3和4。 上机实验题 题6和7。