• 沒有找到結果。

数据结构(用C语言描述) - 万水书苑-出版资源网

N/A
N/A
Protected

Academic year: 2021

Share "数据结构(用C语言描述) - 万水书苑-出版资源网"

Copied!
31
0
0

加載中.... (立即查看全文)

全文

(1)第. 2. 章 线性表. 学习要点       . 线性表的定义及特征 顺序表的存储结构 顺序表的插入、删除、查找算法及其时间性能 单链表的存储结构 单链表的插入、删除、查找算法及其时间性能 顺序表和单链表的比较 循环链表和双链表的存储方法. 引言 线性表(Linear List)是最简单、最基本和最常用的一种线性结构。它有两种存储方法: 顺序存储和链式存储,它的主要基本操作是插入、删除和检索等。 案例:给出一组元素进行排序,给出一组无序元素,要求任取其中一个元素,以这个元 素为参考,将所有元素分成比这个元素大的排在一边,比这个元素小的排在另一边。 又给出二组有序元素进行保持仍然是有序的合并。 问题:在要求采用的辅助存储空间最小的情况下,应该采用哪种结构来实现排序?采用 顺序存储结构和采用链式存储结构在时间性能上有什么区别?. 2.1 线性表的定义及逻辑结构 线性表是一种线性结构。线性结构的特点是数据元素之间是一种线性关系,数据元素“一 个接一个地排列”。在一个线性表中数据元素的类型是相同的,或者说线性表是由同一类型的 数据元素构成的线性结构。在实际问题中线性表的例子很多,如学生情况信息表是一个线性表, 表中数据元素的类型为学生类型; 一个字符串也是一个线性表,表中数据元素的类型为字符 型;等等。 综上所述,线性表的定义如下: 线性表是具有相同数据类型的 n(n≥0)个数据元素的有限序列,通常记为: (a1,a2,… ai-1,ai,ai+1,…an).

(2) 第 2 章 线性表. 13. 其中 n 为表长,n=0 时称为空表。 表中相邻元素之间存在着顺序关系。将 ai-1 称为 ai 的直接前趋,ai+1 称为 a i 的直接后 继。就是说:对于 ai,当 i=2,...,n 时,有且仅有一个直接前趋 a i-1.,当 i=1,2,...,n-1 时,有且仅有一个直接后继 a i+1,而 a 1 是表中第一个元素,它没有前趋,a n 是最后一个元 素,无后继。 ai 为序号为 i 的数据元素(i=1,2,…,n),通常我们将它的数据类型抽象为 datatype,datatype 根据具体问题而定,如在学生情况信息表中,它是用户自定 义的学生类型;在字符串中,它是字符型;等等。. 2.2 线性表的基本操作 在第 1 章中提到,数据结构的运算是定义在逻辑结构层次上的,而运算的具体实现是建 立在存储结构上的,因此下面定义的线性表的基本运算作为逻辑结构的一部分,每个操作的具 体实现只有在确定了线性表的存储结构之后才能完成。 线性表上的基本操作有: (1)线性表初始化:Init_List(L) 初始条件:表 L 不存在。 操作结果:构造一个空的线性表。 (2)求线性表的长度:Length_List(L) 初始条件:表 L 存在。 操作结果:返回线性表中的所含元素的个数。 (3)取表元:Get_List(L,i) 初始条件:表 L 存在且 1<=i<=Length_List(L)。 操作结果:返回线性表 L 中的第i个元素的值或地址。 (4)按值查找:Locate_List(L,x),x是给定的一个数据元素。 初始条件:线性表 L 存在。 操作结果:在表 L 中查找值为x的数据元素,其结果返回在 L 中首次出现的值为x的那 个元素的序号或地址,称为查找成功;否则,在 L 中未找到值为x的数据元素,返回一特殊 值表示查找失败。 (5)插入操作:Insert_List(L,i,x) 初始条件:线性表 L 存在,插入位置正确(1<=i<=n+1,n 为插入前的表长)。 操作结果:在线性表 L 的第 i 个位置上插入一个值为 x 的新元素,这样使原序号为 i, i+1, ..., n 的数据元素的序号变为 i+1,i+2, ..., n+1,插入后表长=原表长+1。 (6)删除操作:Delete_List(L,i) 初始条件:线性表 L 存在,1<=i<=n。 操作结果:在线性表 L 中删除序号为 i 的数据元素,删除后使序号为 i+1, i+2,..., n 的元.

(3) 14. 数据结构 (用 C 语言描述). 素变为序号为 i, i+1,...,n-1,新表长=原表长-1。 (1)某数据结构上的基本运算,不是它的全部运算,而是一些常用的基本运算。 (2)每一个基本运算在实现时可能根据不同的存储结构派生出一系列相关的运 算来,如线性表的查找在链式存储结构中还会有按序号查找; 再如插入运算,也 可能是将新元素x插入到适当位置上等。 (3)在上述各操作中定义的线性表L仅仅是一个抽象在逻辑结构层次的线性 表,未涉及到它的存储结构,因此每个操作在逻辑结构层次上尚不能用具体的 某种程序语言写出具体的算法,而算法的实现只有在存储结构确立之后。. 2.3 线性表的顺序存储结构. 2.3.1. 顺序表. 线性表的顺序存储是指在内存中用地址连续的一块存储空间顺序存放线性表的各元素, 用这种存储形式存储的线性表称为顺序表(Sequential List)。因为内存中的地址空间是线性的, 因此用物理上的相邻实现数据元素之间的逻辑相邻关系是既简单,又自然的。如图 2.1 所示, 设 a1 的存储地址为 Loc(a1),每个数据元素占 d 个存储地址,则第 i 个数据元素的地址为: Loc(ai)=Loc(a1)+(i-1)*d 1<=i<=n 这就是说,只要知道顺序表首地址和每个数据元素所占地址单元的个数,就可求出第 i 个数据元素的地址,这也是顺序表具有按数据元素的序号随机存取的特点。. data. 0. 1. …. a1. a2. …. ai-1. i-1. i. …. n-1. ai. ai+1. …. an. ... MAXSIZE1-1 …. last 图 2.1 线性表的顺序存储示意图. 在程序设计语言中,一维数组在内存中占用的存储空间是一组连续的存储区域,因此用 一维数组来表示顺序表的数据存储区域是再合适不过的。考虑到线性表的运算有插入、删除等, 即表长是可变的,因此数组的容量需设计得足够大,设用:data[MAXSIZE]来表示,其中 MAXSIZE 是一个根据实际问题定义的足够大的整数,线性表中的数据从 data[0] 开始依次顺 序存放,但当前线性表中的实际元素个数可能未达到 MAXSIZE 多个,因此需用一个变量 last 记录当前线性表中最后一个元素在数组中的位置,即 last 起一个指针的作用,始终指向线性 表中最后一个元素。表空时,last=-1。这种存储思想的具体描述可以是多样的。例如可以是: datatype data[MAXSIZE]; int last;.

(4) 第 2 章 线性表. 15. 这样表示的顺序表如图 2.1 所示。表长为 last+1,数据元素分别存放在 data[0]到 data[last] 中。这样使用简单方便,但有时不便管理。 从结构性上考虑,通常将 data 和 last 封装成一个结构作为顺序表的类型: typedef struct { datatype data[MAXSIZE]; int last; } SeqList;. 定义一个顺序表:SeqList L ; 这样表示的线性表如图 2.2(a)所示。表长=L.last+1,线性表中的数据元素 a1 至 an 分别 存放在 L.data[0]至 L.data[L.last]中。 由于我们后面的算法用 C 语言描述,根据 C 语言中的一些规则,有时定义一个指向 SeqList 类型的指针更为方便: SeqList *L; L 是一个指针变量,线性表的存储空间通过 L=malloc(sizeof(SeqList)) 操作来获得。 L 中存放的是顺序表的地址,这样表示的线性表如图 2.2(b)所示。表长表示为(*L).last 或 L->last+1,线性表的存储区域为 L->data,线性表中数据元素的存储空间为: L->data[0] ~ L->data[L->last]。 在以后的算法中多用这种方法表示,读者在读算法时应注意相关数据结构的类型说明。 L.data. L->data. 0. a1. 0. a1. 1. a2. 1. a2. 2. a3. 2. a3. 3. a4. 3. a4. 4. a5. 4. a5. L.last→5. a6. L->last→5. a6. 6. 6 …. …. MAXSIZE-1. MAXSIZE-1. (a). (b) 图 2.2 线性表的顺序存储示意图. 2.3.2. 顺序表上基本运算的实现. 1.顺序表的初始化 顺序表的初始化即构造一个空表,这对表是一个加工型的运算,因此将 L 设为指针参数,.

(5) 16. 数据结构 (用 C 语言描述). 首先动态分配存储空间,然后将表中 last 指针置为-1,表示表中没有数据元素。 算法实现: SeqList *init_SeqList( ) { SeqList *L; L=malloc(sizeof(SeqList)); L->last=-1; return L; }. 算法 2.1 设调用函数为主函数,主函数对初始化函数的调用如下: main() { SeqList *L; L=Init_SeqList(); }. 2.插入运算 线性表的插入是指在表的第 i 个位置上插入一个值为 x 的新元素,插入后使原表长为 n 的表:(a1,a2,...,ai-1,ai,ai+1,...,an) 变为表长为 n+1 的表:(a1,a2,...,ai-1,x,ai,ai+1,...,an)。 i 的取值范围为 1≤i≤n+1。顺序表中的插入如图 2.3 所示。 在顺序表上完成这一运算需通过以下步骤进行: (1)将 ai~an 顺序向下移动,为新元素让出位置。 (2)将 x 置入空出的第 i 个位置。 (3)修改 last 指针(相当于修改表长) ,使之仍指向最后一个元素。 算法实现: int Insert_SeqList(SeqList *L,int i,datatype x) { int j; if (L->last==MAXSIZE-1) { printf("表满"); return(-1); } //表空间已满,不能插入 if (i<1 || i>L->last+2) //检查插入位置的正确性 { printf("位置错");return(0); } for(j=L->last;j>=i-1;j--) L->data[j+1]=L->data[j]; //结点移动 L->data[i-1]=x; //新元素插入 L->last++; //last 仍指向最后元素 return (1); //插入成功,返回 }. 算法 2.2 (1)顺序表中数据区域有 MAXSIZE 个存储单元,在向顺序表中做插入时先检 查表空间是否满了,在表满的情况下不能再做插入,否则产生溢出错误。 (2)要检验插入位置的有效性,这里 i 的有效范围是:1≤i≤n+1,其中 n 为 原表长。 (3)注意数据的移动方向。.

(6) 第 2 章 线性表. 0. a1. a1. 1. a2. a2. …. …. i-2. ai-1. ai-1. i-1. ai. x. ai+1. ai+1. …. …. an. an. …. …. i. n-1. 17. MAXSIZE-1 插入前. 插入后. 图 2.3 顺序表中的插入. 性能分析: 插入算法的时间性能分析如下: 顺序表上的插入运算,时间主要消耗在数据的移动上,在第 i 个位置上插入 x,从 ai 到 an 都要向下移动一个位置,共需要移动 n-i+1 个元素,而 i 的取值范围为: 1≤i≤n+1,即有 n+1 个位置可以插入。设在第 i 个位置上做插入的概率为 Pi,则平均移 动数据元素的次数为: n 1. Ein .  p (n  i  1) i. i 1. 设:Pi=1/ (n+1),即为等概率情况,则: n 1. Ein .  i 1. n 1. pi (n  i  1) . 1 n (n  i  1)  n  1 i 1 2. . 这说明:在顺序表上做插入操作需移动表中一半的数据元素。显然时间复杂度为 O(n)。 3.删除运算 线性表的删除运算是指将表中第 i 个元素从线性表中去掉,删除后使原表长为 n 的线性 表:(a1,a2,...,ai-1,ai,ai+1,...,an) 变为表长为 n-1 的线性表:(a1,a2,...,ai-1,ai+1,...,an)。 i 的取值范围为:1≤i≤n。顺序表中的删除如图 2.4 所示。 顺序表上完成这一运算的步骤如下: (1)将 ai+1~an 顺序向上移动。 (2)修改 last 指针(相当于修改表长)使之仍指向最后一个元素。.

(7) 18. 数据结构 (用 C 语言描述). 0. a1. a1. 1. a2. a2. …. …. i-2. ai-1. ai-1. i-1. ai. ai+1. i. ai+1 … an. n-1. an …. …. 删除前. 删除后. MAXSIZE-1. 图 2.4 顺序表中的删除. 算法实现: int Delete_SeqList(SeqList *L;int i) { int j; if(i<1 || i>L->last+1) //检查空表及删除位置的合法性 { printf ("不存在第 i 个元素"); return(0); } for(j=i;j<=L->last;j++) L->data[j-1]=L->data[j]; //向上移动 L->last--; return(1);. //删除成功. }. 算法 2.3 (1)删除第 i 个元素,i 的取值为 1≤i≤n,否则第 i 个元素不存在,因此要检 查删除位置的有效性。 (2)当表空时不能做删除,因为表空时 L->last 的值为-1,条件(i<1 || i>L->last+1)也包括了对表空的检查。 (3)删除 ai 之后,该数据已不存在,如果需要,先取出 ai,再做删除。 性能分析: 与插入运算相同,其时间主要消耗在了移动表中元素上,删除第 i 个元素时,其后面的元 素 ai+1~an 都要向上移动一个位置,共移动了 n-i 个元素,所以平均移动数据元素的次数为: n. Ede .  p (n  i) i. i 1. 在等概率情况下,pi =1/ n,则:.

(8) 第 2 章 线性表 n. Ede . 1. n 1.  p (n  i)  n  (n  i)  i. i 1. i 1. 19. n 1 2. 这说明在顺序表上作删除运算时大约需要移动表中一半的元素,显然该算法的时间复杂 度为 O(n)。 4.按值查找 线性表中的按值查找是指在线性表中查找与给定值 x 相等的数据元素。在顺序表中完成 该运算最简单的方法是:从第一个元素 a1 起依次和 x 比较,直到找到一个与 x 相等的数据元 素,则返回它在顺序表中的存储下标或序号(二者差一);或者查遍整个表都没有找到与 x 相 等的元素,返回-1。 算法实现: int Location_SeqList(SeqList *L, datatype x) { int i=0; while(i<=L.last && L->data[i]!= x) i++; if (i>L->last) return -1; else return i; //返回的是存储位置 }. 算法 2.4 性能分析: 本算法的主要运算是比较。显然比较的次数与 x 在表中的位置有关,也与表长有关。当 a1=x 时,比较一次成功。当 an=x 时比较 n 次成功。平均比较次数为(n+1)/2,时间性能为 O(n)。. 2.3.3. 案例分析. 案例 1: 问题:将两个递增有序的顺序表 A、B 合并成一个递增有序的顺序表 C,并要求同样的数 据元素在新表中只出现一次。 算法分析: 两个顺序表按递增方式排列,在进行合并时,依次比较,哪个线性表中的元素值小就先 将这个元素复制到新的线性表中,若两个元素相等,则复制一个即可,一直到其中的一个线性 表中的元素全部复制到新表后,将另一个表中剩余的元素复制到新表中即可。 算法实现: #include<stdio.h> #include<stdlib.h> #include<alloc.h> struct SeqList {int *data; int last; int MaxSize; };.

(9) 20. 数据结构 (用 C 语言描述) typedef struct SeqList LIST; void InitSepList(LIST * L,int MAXNUM) //初始化顺序表 { if((L->data=(int *)malloc(MAXNUM *sizeof(int)))==NULL) { printf("分配内存错误!\n"); exit(1); } L->last=-1; L->MaxSize=MAXNUM; printf("表初始地址为%p\n",L->data); } int InsertList(LIST *L,int elem). //将元素插入顺序表. {int i; L->last++; if(L->last>L->MaxSize-1){printf("表满");return -1; } if(L->last==0)L->data[0]=elem; else { for(i=L->last;(i>=0)&&(L->data[i]>elem);i--) L->data[i+1]=L->data[i]; L->data[i+1]=elem; } return (0); } void OutputList(LIST *L) //输出顺序表中的元素 {int i; for(i=0;i<=L->last;i++) printf("%4d",L->data[i]); printf("\n"); } void merge(LIST *A,LIST *B,LIST *C) { int i,j,k; i=0;j=0;k=0; while ( i<=A->last && j<=B->last ) if (A->data[i]<B->data[j]) C->data[k++]=A->data[i++]; else C->data[k++]=B->data[j++]; while (i<=A->last ) C->data[k++]= A->data[i++]; while (j<=B->last ) C->data[k++]=B->data[j++]; C->last=k-1; }. //合并顺序表中的元素.

(10) 第 2 章 线性表 void main() {LIST L1,L2,L3; int r,op; InitSepList(&L1,20); InitSepList(&L2,20); InitSepList(&L3,40); while(1) { printf("选择操作:\n"); printf("1:第一个顺序表添加元素 2:第二个顺序表添加元素 printf("0:退出\n"); fflush(stdin); scanf("%d",&op); switch(op) { case 0: return; case 1: printf("输入要插入的元素:");. 21. 3:合并元素\n");. scanf("%d",&r); InsertList(&L1,r); printf("第一个顺序表为:"); OutputList(&L1); break; case 2: printf("输入要插入的元素:"); scanf("%d",&r); InsertList(&L2,r); printf("第二个顺序表为:"); OutputList(&L2); break; case 3: merge(&L1,&L2,&L3); printf("合并后的元素为:"); OutputList(&L3); break; } }. 算法 2.5 性能分析: 算法的时间性能是 O(m+n),其中 m 是 A 的表长,n 是 B 的表长。 案例 2: 问题:将顺序表 (a1,a2,...,an) 重新排列为以 ai 为界的两部分:ai 前面的值都比 ai 小, ai 后面的值都比 ai 大(这里假设数据元素的类型具有可比性,不妨设为整型),操作前后如图 2.5 所示。这一操作称为划分。ai 也称为基准。.

(11) 22. 数据结构 (用 C 语言描述). 25. 15. 30. 10. 20. 20. 60. 25. 10. 30. 35. 60. 15. 35. . . 划分前. 划分后 图 2.5 顺序表的划分. 算法分析: 划分的方法有多种,下面介绍的划分算法的思路简单,性能较差。 基本思路: 从第二个元素开始到最后一个元素,逐一向后扫描: (1)当前数据元素 aj 比 ai 大时,表明它已经在 ai 的后面,不必改变它与 ai 之间的位置, 继续比较下一个。 (2)当前结点若比 ai 小,则说明它应该在 ai 的前面,此时将它上面的元素都依次向下移 动一个位置,然后将它置入最上方。 算法实现: void part(SeqList *L) { int i,j; datatype x,y; x=L->data[0]; //将基准置入 x 中 for(i=1;i<=L->last;i++) if(L->data[i]<x) //当前元素小于基准 { y = L->data[i]; for(j=i-1;j>=0;j--) //移动 L->data[j+1]=L->data[j]; L->data[0]=y; } }. 算法 2.6 性能分析: 本算法中,有两重循环,外循环执行 n-1 次,内循环中移动元素的次数与当前数据的大小 有关,当第i个元素小于 ai 时,要移动它上面的 i-1 个元素,再加上当前结点的保存及置入, 所以移动 i-1+2 次,在最坏情况下,ai 后面的结点都小于 ai,故总的移动次数为:.

(12) 第 2 章 线性表 n. . n. (i  1  2) . i2.  (i  1)  i 2. 23. n * (n  3) 2. 2. 即最坏情况下移动数据时间性能为 O(n )。 这个算法简单但效率低,在第 8 章的快速排序中,我们将介绍另一种划分算法,它的性 能为 O(n)。 案例 3 问题:比较两个线性表的大小。两个线性表的比较依据下列方法:设 A、B 是两个线性表, 均用向量表示,表长分别为 m 和 n。A′和 B′分别为 A 和 B 中除去最大共同前缀后的子表。 例如,A=(x,y,y,z,x,z),B=(x,y,y,z,y,x,x,z),两表最大共同前缀为(x,y,y,z)。 则 A′=(x,z),B′=(y,x,x,z) 若 A′=B′=空表,则 A=B; 若 A′=空表且 B′≠空表,或两者均不空且 A′首元素小于 B′首元素,则 A<B; 否则 A>B。 算法分析: 首先找出 A、B 的最大共同前缀;然后求出 A′和 B′,之后再按比较规则进行比较,A>B 函 数返回 1;A=B 返回 0;A<B 返回-1。 算法实现: int compare( A,B,m,n) int A[],B[]; int m,n; { int i=0,j,AS[],BS[],ms=0,ns=0; while (A[i]==B[i]) i++; for (j=i;j<m;j++) { AS[j-i]=A[j];ms++; } for (j=i;j<n;j++) { BS[j-i]=B[j];ns++; }. //AS,BS 作为 A′,B′ //找最大共同前缀 //求 A′,ms 为 A′的长度 //求 B′,ms 为 B′的长度. if (ms==ns&&ms==0) return 0; else if (ms==0&&ns>0 || ms>0 && ns>0 && AS[0]<BS[0]) else return 1;. return –1;. }. 算法 2.7 性能分析: 算法的时间性能是 O(m+n)。. 2.4 线性表的链式存储结构 由于顺序表的存储特点是用物理上的相邻实现了逻辑上的相邻,它要求用连续的存储单 元顺序存储线性表中各元素,因此,对顺序表插入、删除时需要通过移动数据元素来实现,影 响了运行效率。本节介绍线性表链式存储结构,它不需要用地址连续的存储单元来实现。由于.

(13) 24. 数据结构 (用 C 语言描述). 不要求逻辑上相邻的两个数据元素物理上也相邻,是通过“链”建立数据元素之间的逻辑关系, 因而对线性表的插入、删除不需要移动数据元素。. 2.4.1. 单链表. 链表(Linked List)通过一组任意的存储单元来存储线性表中的数据元素,那么怎样表示 出数据元素之间的线性关系呢?为建立数据元素之间的线性关系,对每个数据元素 ai,除了存 放数据元素自身的信息 ai 之外,还需要和 ai 一起存放其后继 ai+1 所在的存储单元的地址, 这两部分信息组成一个“结点” ,结点的结构如图 2.6 所示,每个元素都是如此。存放数据元 素信息的称为数据域,存放其后继地址的称为指针域。因此,n 个元素的线性表通过每个结点 的指针域拉成了一个“链子”,称为链表。由于每个结点中只有一个指向后继的指针,因而称 其为单链表(Single Linked List)。 链表是由一个个结点构成的,结点定义如下: Data next typedef struct node { datatype data; struct node *next; } LNode,*LinkList;. 图 2.6 单链表结点结构. 定义头指针变量: LinkList. H;. 如图 2.7 所示是线性表 (a1,a2,a3,a4,a5,a6,a7,a8) 对应的单链表的链式存储结构示意图。. H. 160 首地址. 地址. 数据域. 指针域. 110. a5. 200. …. …. 150. a2. 190. 160. a1. 150. …. …. 190. a3. 210. 200. a6. 260. 210. a4. 110. … 240. …. a8. … 260. NULL .... a7. 图 2.7 链式存储结构. 240.

(14) 25. 第 2 章 线性表. 当然,必须将第一个结点的地址 160 放到一个指针变量(如 H)中,最后一个结点没有 后继,其指针域必需置空,表明此表到此结束,这样就可以从第一个结点的地址开始“顺藤摸 瓜”,找到每个结点。 作为线性表的一种存储结构,关键的是结点间的逻辑结构,而对每个结点的实际地址并 不关心,所以通常的单链表用图 2.8 所示的形式,而不用图 2.7 所示的形式表示。 通常用“头指针”来标识一个单链表,如单链表 L、单链表 H 等,是指某链表的第一个 结点的地址放在了指针变量 L、H 中,头指针为“NULL”则表示一个空表。 需要进一步指出的是:上面定义的 LNode 是结点的类型,LinkList 是指向 Lnode 类型结 点的指针类型。为了增强程序的可读性,通常将标识一个链表的头指针说明为 LinkList 类型的 变量,如 LinkList L ; 当 L 有定义时,值要么为 NULL,则表示一个空表;要么为第一个结点 的地址,即链表的头指针;将操作中用到指向某结点的指针变量说明为 LNode *类型,如 LNode *p;则语句: p=malloc(sizeof(LNode));. 完成了申请一块 Lnode 类型的存储单元的操作,并将其地址赋值给变量 p,如图 2.9 所示。P 所指的结点为*p,*p 的类型为 LNode 型,所以该结点的数据域为 (*p).data 或 p->data,指针 域为 (*p).next 或 p->next。free(p)则表示释放 p 所指的结点。 p->data H. a1. a2. …. an. 图 2.8 链表示意图. 2.4.2. ∧. p->next. P 图 2.9. 申请一个结点. 单链表上的基本运算. 1.建立单链表 (1)在链表的头部插入结点建立单链表。 链表与顺序表不同,它是一种动态管理的存储结构,链表中的每个结点占用的存储空间不 是预先分配的,而是运行时系统根据需求而生成的,因此建立单链表从空表开始,每读入一个 数据元素则申请一个结点,然后插在链表的头部,图 2.10 展现了线性表:(25,45,18,76,29)之 链表的建立过程,因为是在链表的头部插入,读入数据的顺序和线性表中的逻辑顺序是相反的。 算法实现: LinkList Creat_LinkList1( ) { LinkList L=NULL; //空表 Lnode *s; int x; //设数据元素的类型为 int scanf("%d",&x); while (x!=flag) { s=malloc(sizeof(LNode)); s->data=x;.

(15) 26. 数据结构 (用 C 语言描述) s->next=L; L=s; Scanf ("%d",&x); } return L; }. 算法 2.8 ∧. 29. 25. ∧. 45. 25. ∧. 18. 45. 25. ∧. 76. 18. 45. 25. ∧. 76. 18. 45. 25. ∧. 图 2.10 在头部插入建立单链表. (2)在单链表的尾部插入结点建立单链表。 头插入建立单链表简单,但读入数据元素的顺序与生成链表中元素的顺序是相反的,若 希望次序一致,则用尾插入的方法。因为每次是将新结点插入到链表的尾部,所以需加入一个 指针 r 用来始终指向链表中的尾结点,以便能够将新结点插入到链表的尾部,图 2.11 展现了 在链表的尾部插入结点建立链表的过程。 H=NULL r=NULL. //初始状态 r. H. 29. H. 29. 76. H. 29. 76. 18. H. 29. 76. 18. 45. H. 29. 76. 18. 45. r r r r 25. 图 2.11 在尾部插入建立单链表. 算法思路: 初始状态:头指针 H=NULL,尾指针 r=NULL; 按线性表中元素的顺序依次读入数据元 素,不是结束标志时,申请结点,将新结点插入到 r 所指结点的后面,然后 r 指向新结点(但.

(16) 27. 第 2 章 线性表 第一个结点有所不同,读者注意下面算法中的有关部分)。 算法实现: LinkList Creat_LinkList2( ) { LinkList L=NULL; Lnode *s,*r=NULL; int x; //设数据元素的类型为 int scanf("%d",&x); while (x!=flag) { s=malloc(sizeof(LNode)); s->data=x; if (L==NULL) L=s; //第一个结点的处理 else r->next=s; //其他结点的处理 r=s; //r 指向新的尾结点 scanf("%d",&x); } if ( r!=NULL) r->next=NULL; //对于非空表,最后结点的指针域放空指针 return L; }. 算法 2.9 在上面的算法中,第一个结点的处理和其他结点是不同的,原因是第一个结点加入时链 表为空,它没有直接前驱结点,它的地址就是整个链表的指针,需要放在链表的头指针变量中; 而其他结点有直接前驱结点,其地址放入直接前驱结点的指针域。 “第一个结点”的问题在很 多操作中都会遇到,如在链表中插入结点时,将结点插在第一个位置和其他位置是不同的,在 链表中删除结点时,删除第一个结点和其他结点的处理也是不同的,等等。 为了方便操作,有时在链表的头部加入一个“头结点”,头结点的类型与数据结点一致, 标识链表的头指针变量 L 中存放该结点的地址,这样即使是空表,头指针变量 L 也不为空了。 头结点的加入使得“第一个结点”的问题不再存在,也使得“空表”和“非空表”的处理成为 一致。 头结点的加入完全是为了运算的方便,它的数据域无定义,指针域中存放的是第一个数 据结点的地址,空表时为空。 图 2.12(a)、(b)分别是带头结点的单链表空表和非空表的示意图。 ∧. H (a). H. a1. a2. …. an. ∧. (b) 图 2.12 带头结点的单链表. 2.求表长 算法思路: 设一个移动指针 p 和计数器 j,初始化后,p 所指结点后面若还有结点,p 向后移动,计 数器加 1。 (1)设 L 是带头结点的单链表(线性表的长度不包括头结点)。.

(17) 28. 数据结构 (用 C 语言描述). 算法实现: int Length_LinkList1 (LinkList L) { Lnode. * p=L;. //p 指向头结点. int j=0; while (p->next) //p 所指的是第 j 个结点. { p=p->next; j++ } return j;. }. 算法 2.10(a) (2)设 L 是不带头结点的单链表。 算法实现: int Length_LinkList2 (LinkList L) { Lnode. * p=L;. int j; if (p==NULL). return 0; //空表的情况. j=1;. //在非空表的情况下,p 所指的是第一个结点. while (p->next ) { p=p->next; j++ } return j; }. 算法 2.10(b) 从上面两个算法中看到,不带头结点的单链表空表情况要单独处理,而带头结点之后则 不用了。 算法 2.10(a)、(b)的时间复杂度均为 O(n)。 在以后的算法中不加说明则认为单链表是带头结点的。 3.查找操作 (1)按序号查找 Get_Linklist(L,i)。 算法思路:从链表的第一个元素结点起,判断当前结点是否是第 i 个,若是,则返回该结 点的指针,否则继续后一个,到表结束为止。没有第 i 个结点时返回空。 算法实现: Lnode * Get_LinkList(LinkList L, Int i); //在单链表 L 中查找第 i 个元素结点,找到返回其指针,否则返回空 { Lnode * p=L; int j=0; while (p->next !=NULL && j<i ) { p=p->next; j++; } if (j==i) return p; else return NULL; }. 算法 2.11(a) (2)按值查找即定位 Locate_LinkList(L,x)。.

(18) 第 2 章 线性表. 29. 算法思路: 从链表的第一个元素结点起,判断当前结点其值是否等于 x,若是,返回该结点的指针, 否则继续后一个,到表结束为止。找不到时返回空。 算法实现: Lnode * Locate_LinkList( LinkList L, datatype x) //在单链表 L 中查找值为 x 的结点,找到后返回其指针,否则返回空 { Lnode * p=L->next; while ( p!=NULL && p->data != x) p=p->next; return p; }. 算法 2.11(b) 算法 2.1(a)、(b)的时间复杂度均为 O(n)。 4.插入操作 (1)后插结点:设 p 指向单链表中某结点,s 指向待插入的值为 x 的新结点,将*s 插入 到*p 的后面,插入示意图如图 2.13 所示。 操作如下: ①s->next=p->next; ②p->next=s; 两个指针的操作顺序不能交换。 (2)前插结点:设 p 指向链表中某结点,s 指向待插入的值为 x 的新结点,将*s 插入到 *p 的前面,插入示意图如图 2.14 所示,与后插不同的是:首先要找到*p 的前驱*q,然后再完 成在*q 之后插入*s,设单链表头指针为 L,操作如下: q=L; while (q->next!=p) q=q->next; s->next=q->next; q->next=s;. //找*p 的直接前驱. q. p. ① ×. × ② s. 图 2.13 在*p 之后插入*s. p. ③. ① s. ② 图 2.14 在*p 之前插入*s. 后插操作的时间复杂性为 O(1),前插操作因为要找 *p 的前驱,时间性能为 O(n);其实 更关注的是数据元素之间的逻辑关系,所以仍然可以将 *s 插入到 *p 的后面,然后将 p->data 与 s->data 交换即可,这样既满足了逻辑关系,也能使时间复杂性为 O(1)。.

(19) 30. 数据结构 (用 C 语言描述). (3)插入运算 Insert_LinkList(L,i,x)。 算法思路:①找到第 i-1 个结点;若存在继续②,否则结束; ②申请、填装新结点; ③将新结点插入。结束。 算法实现: int Insert_LinkList( LinkList L, int i, datatype x) //在单链表 L 的第 i 个位置上插入值为 x 的元素 { Lnode * p,*s; p=Get_LinkList(L,i-1); //查找第 i-1 个结点 if (p==NULL) { printf("参数 i 错");return 0; } //第 i-1 个不存在,不能插入 else { s=malloc(sizeof(LNode)); //申请、填装结点 s->data=x; s->next=p->next; //新结点插入在第 i-1 个结点的后面 p->next=s return 1; }. 算法 2.12 算法 2.12 的时间复杂度为 O(n)。 5.删除操作 (1)删除结点:设 p 指向单链表中某结点,删除*p。操作示意图如图 2.15 所示。 通过示意图可见,要实现对结点*p 的删除,首先 p q 要找到*p 的前驱结点*q,然后完成指针的操作即可。 指针的操作由下列语句实现: × q->next=p->next; free(p);. 显然找*p 前驱的时间复杂性为 O(n)。 若要删除*p 的后继结点(假设存在) ,则可以直接 完成: s=p->next; p->next=s->next; free(s);. 该操作的时间复杂性为 O(1)。 (2)删除运算:Del_LinkList(L,i)。 算法思路:①找到第 i-1 个结点;若存在继续②,否则结束; ②若存在第 i 个结点则继续③,否则结束; ③删除第 i 个结点,结束。 算法实现: int Del_LinkList(LinkList L,int i) //删除单链表 L 上的第 i 个数据结点 { LinkList. p,s;. 图 2.15 删除*p.

(20) 第 2 章 线性表 p=Get_LinkList(L,i-1);. 31. //查找第 i-1 个结点. if (p==NULL) { printf("第 i-1 个结点不存在");return -1; } else { if (p->next==NULL) { printf("第 i 个结点不存在");return 0; } else { s=p->next; //s 指向第 i 个结点 p->next=s->next; //从链表中删除 free(s); //释放*s return 1; }. 算法 2.13 算法 2.13 的时间复杂度为 O(n)。 (1)在单链表上插入、删除一个结点,必须知道其直接前驱结点。 (2)单链表不具有按序号随机访问的特点,只能从头指针开始一个个顺序进行。. 2.4.3. 循环链表. 对于单链表而言,最后一个结点的指针域是空指针,如果将该链表头指针置入该指针域, 使得链表头尾结点相连,就构成了单循环链表(Single Circular Linked List),如图 2.16 所示。 H. …. a1. an. (a)非空表. H. (b)空表. 图 2.16 带头结点的单循环链表. 在单循环链表上的操作基本上与非循环链表相同,只是将原来判断指针是否为 NULL 变 为是否是头指针而已,没有其他较大的变化。 对于单链表只能从头结点开始遍历整个链表,而对于单循环链表则可以从表中任意结点 开始遍历整个链表,不仅如此,有时对链表常做的操作是在表尾、表头进行,此时可以改变一 下链表的标识方法,不用头指针而用一个指向尾结点的指针 R 来标识,可以使操作效率得以 提高。 例如,对两个单循环链表 H1、H2 的连接操作,是将 H2 的第一个数据结点接到 H1 的尾 结点,如用头指针标识,则需要找到第一个链表的尾结点,其时间复杂性为 O(n),而链表若 用尾指针 R1、R2 来标识,则时间性能为 O(1)。操作如下: p= R1 –>next; //保存 R1 的头结点指针 R1->next=R2->next->next; //头尾连接 free(R2->next); //释放第二个表的头结点 R2->next=p; //组成循环链表. 这一过程如图 2.17 所示。.

(21) 32. 数据结构 (用 C 语言描述). p. R2. R1. a1. …. b1. an. …. bn. ×. × 图 2.17 两个用尾指针标识的单循环链表的连接. *2.4.4. 双向链表. 以上讨论的单链表的结点中,只有一个指向其后继结点的指针域 next,因此,若已知某结 点的指针为 p,其后继结点的指针则为 p->next,而找其前驱则只能从该链表的头指针开始, 顺着各结点的 next 域进行,也就是说,找后继的时间性能是 O(1),找前驱的时间性能是 O(n), 如果也希望找前驱的时间性能达到 O(1),则只能付出空间的代价:每个结点再加一个指向前 驱的指针域,结点的结构如图 2.18 所示,用这种结点组成的链表称为双向链表(Doubly Linked List)。 双向链表结点的定义如下: next prior data typedef struct dlnode { datatype data; struct dlnode *prior,*next; }DLNode,*DLinkList;. 图 2.18. 和单链表类似,双向链表通常也是用头指针标识,也可以带头结点和做成循环结构,图 2.19 是带头结点的双向循环链表示意图。显然通过某结点的指针 p,既可以直接得到其后继结 点的指针 p->next,也可以直接得到其前驱结点的指针 p->prior。这样,在有些操作中需要找前 驱时,无需再用循环。从下面的插入删除运算中可以看到这一点。 p H a2. a1. …. an. (a)非空表. H. (b)空表. 图 2.19 带头结点的双循环链表. 设 p 指向双向循环链表中的某一结点,即 p 中是该结点的指针,则 p->prior->next 表示的 是*p 结点之前驱结点的后继结点的指针,即与 p 相等;类似地,p->next->prior 表示的是*p 结 点之后继结点的前驱结点的指针,也与 p 相等,所以有以下等式: p->prior->next = p = p->next->prior 双向链表中结点的插入:设 p 指向双向链表中某结点,s 指向待插入的值为 x 的新结点, 将*s 插入到*p 的前面,插入示意图如图 2.20 所示。.

(22) 第 2 章 线性表. 33. p × ②. × ④. ①. ③. 图 2.20 双向链表中的结点插入. 操作如下: ①s->prior=p->prior; ②p->prior->next=s; ③s->next=p; ④p->prior=s; 指针操作的顺序不是唯一的,但也不是任意的,操作①必须要放到操作④的前面完成, 否则*p 的前驱结点的指针就丢掉了。如果读者把每条指针操作的涵义搞清楚,就不难理解了。 双向链表中结点的删除:设 p 指向双向链表中某结点,删除*p。操作示意图如图 2.21 所示。 ②. p ×. × ①. 图 2.21 双向链表中删除结点. 操作如下: ①p->prior->next=p->next; ②p->next->prior=p->prior; free(p);. *2.4.5. 静态链表. 如图 2.22 所示,规模较大的结构数组 sd[MAXSIZE]中有两个链表:链表 SL 是一个带头 结点的单链表,表示了线性表(a1, a2, a3, a4, a5);单链表 AV 是将当前 sd 中的空结点组成的链表。 数组 sd 的定义如下: #define MAXSIZE N //足够大的数 typedef struct {datatype data; int next; }SNode; //结点类型 SNode sd[MAXSIZE]; int SL,AV; //两个头指针变量.

(23) 34. 数据结构 (用 C 语言描述). 表 SL SL=0. 表 AV AV=6. data 0. next 4. 1. a4. 5. 2. a2. 3. 3. a3. 1. 4. a1. 2. 5. a5. -1. 6. 7. 7. 8. 8. 9. 9. 10. 10. 11. 11. -1. 图 2.22 静态链表. 链表的结点中也有数据域 data 和指针域 next,与前面所讲的链表中的指针不同的是,这 里的指针是结点的相对地址(数组的下标),称为静态指针(Static Pointer),这种链表称为静 态链表(Static Linked List),空指针用-1 表示,因为上面定义的数组中没有下标为-1 的单元。 在图 2.22 中,SL 是用户的线性表,AV 模拟的是系统存储池中空闲结点组成的链表。当 用户需要结点时,例如,向线性表中插入一个元素,需自己向 AV 申请,而不能用系统函数 malloc 来申请,相关的语句为: if(AV!=-1) { t=AV; AV=sd[AV].next; }. 所得到的结点地址(下标)存入 t 中;不难看出,当 AV 表非空时,摘下了第一个结点 给用户。当用户不再需要某个结点时,需通过该结点的相对地址 t 将它还给 AV,交给 AV 表 的结点链在了 AV 的头部,而不能调用系统的 free()函数。相关语句为: sd[t].next=AV; AV=t;. 2.4.6. 案例分析. 案例 4 问题:链表基本算法的实现。 算法分析:编写链表基本操作函数,实现下面的操作: (1)初始化一链表。 (2)在主函数中建立一选项菜单,使用户可选择进行插入、查找、删除或退出等操作。.

(24) 第 2 章 线性表. 35. (3)调用插入函数建立一个链表。 (4)查找用户从键盘输入的指定值的元素,如果该元素存在,返回该元素值在链表中的 第一个位置;否则返回指定值不存在的提示。 (5)查找用户从键盘输入的指定位置的元素,如果该位置合适,返回该位置的元素值; 否则返回指定位置不合理的提示。 (6)删除用户从键盘输入的指定值的元素,如果该元素值存在,返回删除成功的提示, 否则返回用户所输入元素值不存在的提示。 (7)删除用户从键盘输入的指定位置的元素,如果指定位置合适,返回删除成功的提示, 否则返回用户所输入的位置不合适的提示。 (8)遍历并输出顺序表中的元素。 (9)在进行插入、查找、删除等操作后,必须及时输出链表中的元素,便于观察操作结果。 算法实现: #include<stdio.h> #include<alloc.h> typedef struct LNode{ int data; struct LNode *next; }LNode,*LinkList; void InitLinkList(LinkList *p) { *p=NULL; } void InsertLinkList(LinkList *p,int elem,int j) { int i; LinkList u,q,r; u=malloc(sizeof(LNode)); u->data=elem; for(i=0,r=*p;i<j && r!=NULL;i++) { q=r; r=r->next; } if(r==*p) *p=u; else q->next=u; u->next=r; } int DeleteLinkList(LinkList *p,int elem) { LinkList q,r; q=*p; if(q->data==elem).

(25) 36. 数据结构 (用 C 语言描述) {*p=q->next; free(q); return 0; } for(;q->data!=elem && q!=NULL; r=q, q=q->next); if(q==NULL) return 1; if(q->data==elem) {r->next=q->next; free(q); return 0; } else return 1; } int FindLinkList(LinkList p,int elem) { int i; for( i=1;(p->data!=elem) && p!=NULL; p=p->next, i++); return(p==NULL)? -1:i; } void OutputLinkList(LinkList p) { LinkList q=p; printf("链表为:\n"); while(q!=NULL) { printf("%4d",q->data); q=q->next; } printf("\n"); } void main() { LinkList p; int op,i,j,r; InitLinkList(&p); while(1) { printf("选择操作 1:在指定位置插入元素 2:查找指定的元素\n"); printf("3:删除指定的元素 0:Exit\n"); fflush(stdin); scanf("%d",&op); switch(op) { case 0: return; case 1:.

(26) 第 2 章 线性表. 37. printf("输入要插入的元素值及位置:"); scanf("%d%d",&i,&j); InsertLinkList(&p,i,j); OutputLinkList(p); break; case 2: printf("输入要查找的元素值:"); scanf("%d",&i); r=FindLinkList(p,i); if(r>0) printf("所查找元素的位置为:[%d]\n",r); else printf("没找到\n"); printf("\n"); OutputLinkList(p); break; case 3: printf("输入要删除的元素值:"); scanf("%d",&i); r=DeleteLinkList(&p,i); if(r==1) printf("没找到元素!\n"); printf("\n"); if(r==0) { printf("删除成功!\n"); OutputLinkList(p); } break; } } }. 算法 2.14 案例 5 问题:已知单链表 H,写一算法将其倒置。即实现如图 2.23 所示的操作。 (a)为倒置前, (b)为倒置后。 H. 29. 76. 18. 45. 25. ∧. 18. 76. 29. ∧. (a) H. 25. 45 (b). 图 2.23 单链表的倒置.

(27) 38. 数据结构 (用 C 语言描述). 算法分析:依次取原链表中的每个结点,将其作为第一个结点插入新链表,指针 p 用来 指向当前结点,p 为空时结束。 算法实现: void reverse (Linklist H) { LNode *p; p=H->next; //p 指向第一个数据结点 H->next=NULL; //将原链表置为空表 H while (p) { q=p; p=p->next; q->next=H->next; //将当前结点插到头结点的后面 H->next=q; } }. 算法 2.15 该算法只是对链表顺序扫描一遍即完成了倒置,时间性能为 O(n)。 案例 6 问题:已知单链表 L,写一算法,删除其重复结点,即实现如图 2.24 所示的操作。图 2.24 (a)所示为删除前,(b)所示为删除后。 10. H. 15. 18. 10. 15. ∧. (a) 10. H. 15. 18. ∧. (b) 图 2.24. 删除重复结点. 算法分析: 用指针 p 指向第一个数据结点,从它的后继结点开始到表的结束,找与其值相同的结点 并删除之;p 指向下一个;依此类推,p 指向最后结点时算法结束。 算法实现: void pur_LinkList(LinkList H) { LNode *p,*q,*r; p=H->next; //p 指向第一个结点 if(p==NULL) return; while (p->next) { q=p; while (q->next) //从*p 的后继开始找重复结点 { if (q->next->data==p->data) { r=q->next; //找到重复结点,用 r 指向,删除*r q->next=r->next; free(r); } //if else q=q->next;.

(28) 第 2 章 线性表. 39. } //while(q->next) p=p->next; //p 指向下一个结点,继续 } //while(p->next) }. 算法 2.16 该算法的时间性能为 O(n2)。 *案例 7 问题:在带头结点的静态链表 SL 的第 i 个结点之前插入一个值为 x 的新结点。 算法分析: 可设静态链表的存储区域 sd 为全局变量,指针是结点为数组的下标。 算法实现: int Insert_SList( int SL, datatype x, int i) { int p,s; p=SL; j=0; while(sd[p].next!=-1 && j<i-1) {p=sd[p].next;j++;} //找第 i-1 个结点 if(j==i-1) { if(AV!=-1) //若 AV 表还有结点可用 {t=AV; AV=sd[AV].next; //申请、填装新结点 sd[t].data=x; sd[t].next=sd[p].next; sd[p].next=t; return 1;. //插入 //正常插入成功返回. } else{printf("存储池无结点");return 0;} //未申请到结点,插入失败 else{printf("插入的位置错误");return -1;} //插入位置不正确,插入失败 }. 算法 2.17 读者可将该算法和算法 2.12 进行比较,除了一些描述方法有些区别外,算法思路是相同的。. 2.5 顺序表和链表的比较. 1.线性表两种存储结构优缺点的比较 本章介绍了线性表的逻辑结构及它的两种存储结构:顺序表和链表。通过讨论可知两者 各有优缺点,顺序存储的优点有:  方法简单,各种高级语言中都有数组,容易实现。.

(29) 40. 数据结构 (用 C 语言描述). 不用为表示结点间的逻辑关系而增加额外的存储开销。  顺序表具有按元素序号随机访问的特点。 但它也有两个缺点:  在顺序表中做插入删除操作时,平均移动大约表中一半的元素,因此对 n 较大的顺序 表效率低。  需要预先分配足够大的存储空间,估计过大,可能会导致顺序表后部大量闲置;预先 分配过小,又会造成溢出。 . 2.存储结构的选择 链表的优缺点恰好与顺序表相反。在实际中怎样选取存储结构呢?通常有以下几点考虑: (1)基于存储的考虑。 顺序表的存储空间是静态分配的,在程序执行之前必须明确规定它的存储规模,也就是 说事先对"MAXSIZE"要有合适的设定,过大造成浪费,过小造成溢出。可见对线性表的长度 或存储规模难以估计时,不宜采用顺序表;链表不用事先估计存储规模,但链表的存储密度较 低,存储密度是指一个结点中数据元素所占的存储单元和整个结点所占的存储单元之比。显然 链式存储结构的存储密度是小于 1 的。 (2)基于运算的考虑。 在顺序表中按序号访问 ai 的时间性能为 O(1),而链表中按序号访问的时间性能为 O(n), 如果经常做的运算是按序号访问数据元素,则顺序表优于链表;而在顺序表中做插入、删除时 平均移动表中一半的元素,当数据元素的信息量较大且表较长时,这一点是不应忽视的;在链 表中作插入、删除,虽然也要找插入位置,但操作主要是比较操作,从这个角度考虑显然后者 优于前者。 (3)基于环境的考虑。 顺序表容易实现,任何高级语言中都有数组类型,链表的操作是基于指针的,相对来讲 前者简单些,也是用户考虑的一个因素。 总之,两种存储结构各有长短,选择哪一种由实际问题中的主要因素决定。通常“较稳 定”的线性表选择顺序存储,而频繁做插入删除即动态性较强的线性表宜选择链式存储。. 习题二. 一、问答题 1.顺序表和链表两种存储结构,各有哪些主要优缺点? 2.指出下列三个概念的区别:头指针、头结点、首结点,说明头指针和头结点的作用。 3.为什么在循环链表中设置尾指针比设置头指针更好?.

(30) 第 2 章 线性表. 41. 二、选择题 1.线性结构中的一个结点代表一个( )。 A.数据元素 B.数据 C.数据项 D.数据结构 2.对于顺序表的优缺点,以下说法错误的是( )。 A.可以随机存取表中的任一结点 B.无需为结点间的逻辑关系增加额外的存储空间 C.插入和删除运算比链式结构方便 D.需要预先分配连续的存储空间 3.若某线性表最常用的操作是在最后一个元素之后插入一个元素和删除第一个元素,则 采用( )存储方式最节省存储空间和运算时间。 A.双链表 B.仅有头指针的单循环链表 C.单链表 D.仅有尾指针的单循环链表 4.链表不具有的特点是( ) 。 A.不必事先分配存储空间 B.所需空间与线性表长度成正比 C.随机访问 D.插入删除时不需移动元素 5.对于单链表,以下说法错误的是( )。 A.NULL 为空指针,它不指向任何结点 B.所有数据通过指针的链接而组织成单链表 C.指针域用于存放一个指向本结点所含数据元素的直接后继所在结点的指针 D.数据域用于存储线性表的一个数据元素 6.循环链表的主要优点是( )。 A.从表中的任一结点出发都能扫描到整个链表 B.不再需要头指针 C.已知某结点的位置,可容易地找到它的直接前驱 D.指针可前后移动 7.从具有 n 个结点的单链表中查找值等于 X 的结点时,在查找成功的情况下,平均需比 较( )个结点。 A.n B.n/2 C.(n-1)/2 D.(n+1)/2. 三、填空题 1.线性表的存储结构有________和________两种存储方式。 2.对于一个具有 n 个结点的顺序表,在第 i(i≤n)个元素后插入一个新结点,需移动的 结点数为________个,时间复杂度为________。 3.线性表的链式存储结构的每一个结点需要包括两个部分:一部分用来存放元素的数据 信息,称为结点的________;另一部分用来存放元素的指向直接后继元素的地址信息,称为 ________。.

(31) 42. 数据结构 (用 C 语言描述). 4.在单链表中除首结点外,任意结点的存储位置都由________结点中的指针指示。 5.在单链表中,指针 P 所指结点的后面插入一个结点 S 的语句序列是________。 6.在单链表中,删除指针 P 所指结点后继结点的语句序列是________。 7.对于一个具有 n 个结点的单链表,在 p 所指的结点后插入一个新结点的时间复杂度为 ________;在给定值为 X 的结点后插入一个新结点的时间复杂度为________。 8.在双链表中,指针 P 所指结点前插入指针 S 所指的结点,需执行语句________。 9.线性表的元素长度为 6,LOCAL(a1)=1000,则 LOCAL(a3)=________。. 四、综合应用 1.利用原顺序表的存储单元把数据元素序列进行逆序运算,如原来顺序表 A 中的元素是: 1,2,3,4,5,6,逆序后为:6,5,4,3,2,1。 2.设一顺序表中的元素值递增有序。编写一算法,将元素X插到表中的适当位置,并保 持顺序表的有序性,分析算法的时间复杂度。 3.已知一个链表中的元素按值非递增有序排列,试编写一个算法,删除表中值相同的多 余的元素。 4.设有两个按元素递增有序存储的链表 A 和 B,编写一程序将 A 表和 B 表归并成一个 新的按元素值递减有序(元素非递增有序,允许值相同)的单链表 C。 5.设有两个按元素递增有序的线性表 A 和 B,编写一程序将 A 表和 B 表中的相同元素 归并到一个新的递增有序的线性表 C 中。(写出当线性表存储结构分别为顺序表和链表时算法 的实现方式) 6.设带头结点的单链表 L1 和 L2 中,分别存放着两个数据元素集合,编写算法判断集合 L1 是否是集合 L2 的子集,即判断集合 L1 中的数据元素是否都是集合中的数据元素。.

(32)

參考文獻

相關文件

【20150302】今天是 2016 年度第一次交读书报告,第一次拿到书单时,感

试题管理界面左侧,按照试卷结构罗列出了 HSK(一级)至 HSK(六

再以后,两个人频繁地约会,她发现他实际上是一个很好的男人,大度、细

1871—1946)和稍晚的塞梵利(F.Severi,1879—1961),他们主要的结果是代数曲面的分类.头一个

衡量一个地区或一个国家水资源的丰歉 程度的指标:

下面我们将用 这一点将一元函数的 Taylor 展开推广到多元函数.. 我们希望借助此给出多元函数

4、任给空间中某一点,及某一方向,过该定点且垂

下图是单脚支撑形式的示意图,支撑脚和地面的接触点 A 与前、后轮和地面之间 的接触点 B 、 C 共同构成三点支撑,在地面形 成△