百度云下载:https://pan.baidu.com/s/1mWuKxN45BuhgHlC8xBjQWw
第1章 绪论
一、基础知识题
1.1 简述下列概念
数据,数据元素,数据类型,数据结构,逻辑结构,存储结构,算法。
【解答】数据是信息的载体,是描述客观事物的数、字符,以及所有能输入到计算机中并被计算机程序识别和处理的符号的集合。
数据元素是数据的基本单位。在不同的条件下,数据元素又可称为元素、结点、顶点、记录等。
数据类型是对数据的取值范围、数据元素之间的结构以及允许施加操作的一种总体描述。每一种计算机程序设计语言都定义有自己的数据类型。
“数据结构”这一术语有两种含义,一是作为一门课程的名称;二是作为一个科学的概念。作为科学概念,目前尚无公认定义,一般认为,讨论数据结构要包括三个方面,一是数据的逻辑结构,二是数据的存储结构,三是对数据进行的操作(运算)。而数据类型是值的集合和操作的集合,可以看作是已实现了的数据结构,后者是前者的一种简化情况。
数据的逻辑结构反映数据元素之间的逻辑关系(即数据元素之间的关联方式或“邻接关系”),数据的存储结构是数据结构在计算机中的表示,包括数据元素的表示及其关系的表示。数据的运算是对数据定义的一组操作,运算是定义在逻辑结构上的,和存储结构无关,而运算的实现则依赖于存储结构。
数据结构在计算机中的表示称为物理结构,又称存储结构。是逻辑结构在存储器中的映像,包括数据元素的表示和关系的表示。逻辑结构与计算机无关。
算法是对特定问题求解步骤的一种描述,是指令的有限序列。其中每一条指令表示一个或多个操作。一个算法应该具有下列特性:有穷性、确定性、可行性、输入和输出。
-
- 数据的逻辑结构分哪几种,为什么说逻辑结构是数据组织的主要方面?
【解答】数据的逻辑结构分为线性结构和非线性结构。(也可以分为集合、线性结构、树形结构和图形即网状结构)。
逻辑结构是数据组织的某种“本质性”的东西:
(1)逻辑结构与数据元素本身的形式、内容无关。
(2)逻辑结构与数据元素的相对位置无关。
(3)逻辑结构与所含数据元素的个数无关。
1.3 试举一个数据结构的例子,叙述其逻辑结构、存储结构、运算三方面的内容。
【解答】学生成绩表,逻辑结构是线性结构,可以顺序存储(也可以链式存储),运算可以有插入、删除、查询,等等。
1.4 简述算法的五个特性,对算法设计的要求。
【解答】算法的五个特性是:有穷性、确定性、可行性、零至多个输入和一至多个输出。
对算法设计的要求:正确性,易读性,健壮性,和高的时空间效率(运算速度快,存储空间小)。
1.5 设n是正整数,求下列程序段中带@记号的语句的执行次数。
(1)i=1;k=0; (2) i=1;j=0;
while(i<n) while(i+j<=n)
{k=k+50*i; i++; @ {if(i>j)j++; @
} else i++; } @
(3)x=y=0; (4)x=91;y=100;
for(i=0;i<n;i++) @ while(y>0)
for(j=0;j<n;j++) @ if(x>100)
{x++; @ {x=x-10; y--; @
for(k=0;k<n;k++) @ }
y++; @ else x++; @
}
【解答】(1)n-1
(2)i= én/2ù ,j=ën/2û
(3)n+1, n(n+1), n2,(n+1)n2, n3
(4)100, 1000
1.6 有实现同一功能的两个算法A1和A2,其中A1的时间复杂度为Tl=O(2n),A2的时间复杂度为T2=O(n2),仅就时间复杂度而言,请具体分析这两个算法哪一个好。
【解答】对算法A1和A2的时间复杂度T1和T2取对数,得nlog2和2logn。显然,当n<4时,算法A1好于A2;当n=4时,两个算法时间复杂度相同;当n>4时,算法A2好于A1。
1.7 选择题:算法分析的目的是( )
A、找出数据结构的合理性 B、研究算法中的输入和输出的关系
C、分析算法的效率以求改进 D、分析算法的易懂性和文档特点
【解答】C
二、算法设计题
1.8 已知输入x,y,z三个不相等的整数,设计一个“高效”算法,使得这三个数按从小到大输出。“高效”的含义是用最少的元素比较次数、元素移动次数和输出次数。
【算法1.8】
void Best()
{//按序输出三个整数的优化算法
int a,b,c,t;
scanf(“%d%d%d”,&a,&b,&c);
if(a>b)
{t=a; a=b; b=t:} //a和b已正序
if(b>c)
{t=c; c=b; //c已到位
if(a>t) {b=a; a=t;} //a和b已正序
else b=t;
}
printf(“%d,%d,%d\n”,a,b,c);
//最佳2次比较,无移动;最差3次比较,7个赋值
}
1.9 在数组A[n]中查找值为k的元素,若找到则输出其位置i(1≤i≤n),否则输出0作为标志。设计算法求解此问题,并分析在最坏情况下的时间复杂度。
【题目分析】从后向前查找,若找到与k值相同的元素则返回其位置,否则返回0。
【算法1.9】
int Search(ElemType A[n+1], ElemType k)
{i=n;
while(i>=1)&&(A[i]!=k)) i--;
if(i>=1) return i;
else return 0;
}
当查找不成功时,总的比较次数为n+1次,所以最坏情况下时间复杂度为O(n)。
在学过第 9 章 “查找”后,可优化以上算法:设“监视哨”。算法如下:
int Search(ElemType A[n+1], ElemType k)
{i=n; A[0]=k;
while(A[i]!=k) i--;
return i;
}
研究表明,当n>=1000时,算法效率提高50%。
第2章 线性表
一、基础知识题
2.1 试述头指针、头结点、元素结点、首元结点的区别,说明头指针和头结点的作
【解答】指向链表第一个结点(或为头结点或为首元结点)的指针称为头指针。“头指针”具有标识一个链表的作用,所以经常用头指针代表链表的名字,如链表L既是指链表的名字是L,也是指链表的第一个结点的地址存储在指针变量L中,头指针为“NULL”则表示指针变量L没指向任何链表。
有时,我们在整个线性链表的第一个元素结点之前加入一个结点,称为头结点,它的数据域可以不存储任何信息(当然,作为“副产品”,头结点的数据域也可能做监视哨或存放线性表的长度等附加信息),指针域中存放的是第一个数据结点的地址,空表时为空。 “头结点”的加入,使插入和删除等操作方便、统一。
元素结点即是数据结点,至少包括元素自身信息和其后继元素的地址两个域。
首元结点是指链表中第一个数据元素的结点;为了操作方便,通常在链表的首元结点之前附设一个结点,称为头结点。
2.2分析顺序存储结构和链式存储结构的优缺点,说明何时应该利用何种结构。
【解答】①从空间上来看,当线性表的长度变化较大,难以估计其规模时,选用动态的链表作为存储结构比较合适。由于链表除了需要设置数据域外,还要额外设置指针域,因此当线性表长度变化不大,易于事先确定规模时,为了节约存储空间,宜采用顺序存储结构。
②从时间上看,顺序表具有按元素序号随机访问的特点,在顺序表中按序号访问数据元素的时间复杂度为O(1);而链表中按序号访问的时间复杂度为O(n)。所以如果经常按序号访问数据元素,使用顺序表优于链表。
在顺序表中做插入删除操作时,平均移动大约表中一半的元素,因此n较大时顺序表的插入和删除效率低。在链表中作插入、删除,虽然也要找插入位置,但操作主要是比较操作。从这个角度考虑显然链表优于顺序表。
总之,两种存储结构各有长短,选择那一种存储结构,由实际问题中的主要因素决定。
2.3 分析在顺序存储结构下插入和删除结点时平均需要移动多少个结点。
【解答】平均移动表中大约一半的结点。插入操作平均移动个结点,删除操作平均移动个结点。具体移动的次数取决于表长和插入、删除的结点的位置。
2.4 为什么在单循环链表中常使用尾指针,若只设头指针,插入元素的时间复杂度如何?
【解答】单循环链表中无论设置尾指针还是头指针都可以遍历到表中任一个结点。设置尾指针时,若在表尾进行插入元素或删除第一元素,操作可在O(1)时间内完成;若只设置头指针,表尾进行插入或删除操作,需要遍历整个链表,时间复杂度为O(n)。
2.5 在单链表、双链表、单循环链表中,若知道指针p指向某结点,能否删除该结点,时间复杂度如何?
【解答:】以上三种链表中,若知道指针p指向某结点,都能删除该结点。
双链表删除p所指向的结点的时间复杂度为O(1),而单链表和单循环链表上删除p所指向的结点的时间复杂度均为O(n)。
2.6 下面算法的功能是什么?
LinkedList Unknown(LinkedList la)
{LNode *q,*p;
if(la && la->next)
{q=la; la=la->next; p=la;
while(p->next) p=p->next;
p->next=q; q->next=null;
}
return la;
}
【解答】将首元结点删除并插入到表尾(设链表长度大于1)。
2.7 选择题:在循环双链表的*p结点之后插入*s结点的操作是( )
A、p->next=s; s->prior=p; p->next->prior=s; s->next=p->next;
B、p->next=s; p->next->prior=s; s->prior=p; s->next=p->next;
C、s->prior=p; s->next=p->next; p->next:=s; p->next->prior=s;
D、s->prior=p; s>next=p>next; p>next->prior =s; p->next=s;
【解答】D
2.8 选择题:若某线性表最常用的操作是存取任一指定序号的元素和在最后进行插入和删除运算,则利用( )存储方式最节省时间。
A.顺序表 B.双链表 C.带头结点的双循环链表 D.单循环链表
【解答】A
二、算法设计题
2.9 设ha和hb分别是两个带头结点的非递减有序单链表的头指针, 试设计算法, 将这两个有序链表合并成一个非递增有序的单链表。要求使用原链表空间,表中无重复数据。
【题目分析】因为两链表已按元素值非递减次序排列,将其合并时,均从第一个结点起进行比较,将小的链入链表中,同时后移链表工作指针,若遇值相同的元素,则删除之。该问题要求结果链表按元素值非递增次序排列,故在合并的同时,将链表结点逆置。
【算法2.9】
LinkedList Union(LinkedList ha,hb)
∥ha,hb分别是带头结点的两个单链表的头指针,链表中的元素值按非递减有序
∥本算法将两链表合并成一个按元素值非递增有序的单链表,并删除重复元素
{ pa=ha->next; ∥pa是链表ha的工作指针
pb=hb->next; ∥pb是链表hb的工作指针
ha->next=null; ∥ha作结果链表的头指针,先将结果链表初始化为空
while(pa!=null && pb!=null) ∥当两链表均不为空时作
{while(pa->next && pa->data==pa->next->data)
{u=pa->next; pa->next=u->next; free(u)}∥删除pa链表中的重复元素
while(pb->next && pb->data==pb->next->data)
{u=pb->next; pb->next=u->next; free(u)}∥删除pb链表中的重复元素
if(pa->data<pb->data)
{r=pa->next; ∥将pa 的后继结点暂存于r
pa->next=ha->next; ∥将pa结点链于结果表中,同时逆置
ha->next=pa;
pa=r; ∥恢复pa为当前待比较结点
}
else if(pb->data<pa->data)
{r=pb->next; ∥ 将pb 的后继结点暂存于r
pb->next=ha->next; ∥将pb结点链于结果表中,同时逆置
ha->next=pb;
pb=r; ∥恢复pb为当前待比较结点
}
else{u=pb;pb=pb->next;free(u)}∥删除链表pb和pa中的重复元素
}// while(pa!=null && pb!=null)
if(pa) pb=pa; ∥避免再对pa写下面的while语句
while(pb!=null) ∥将尚未到尾的表逆置到结果表中
{r=pb->next; pb->next=ha->next; ha->next=pb; pb=r; }
return ha
}∥算法Union结束
-
- 设la是一个双向循环链表,其表中元素递增有序。试写一算法插入元素x,使表中元素依然递增有序。
【问题分析】双向链表的插入与单链表类似,不同之处是需要修改双向指针。
【算法2.10】
DLinkedList DInsert(DLinkedList la, ElemType x)
∥在递增有序的双向循环链表la中插入元素x,使表中元素依然递增有序
{p=la->next; ∥p指向第一元素
la->data=MaxElemType;∥MaxElemType是和x同类型的机器最大值,用做监视哨
while(p->data<x) ∥寻找插入位置
p=p->next ;
s=(DLNode *)malloc(sizeof(DLNode));∥申请结点空间
s->data=x;
s->prior=p->prior; s->next=p; ∥将插入结点链入链表
p->prior->next=s; p->prior=s;
}
-
- 设p指向头指针为la的单链表中某结点,试编写算法,删除结点*p的直接前驱结点。
【题目分析】设*p是单链表中某结点,删除结点*p的直接前驱结点,要找到*p的前驱结点的前驱*pre。进行如下操作:u=pre->next; pre->next=u->next;free(u);
【算法2.11】
LinkedList LinkedListDel(LinkedList la,LNode *p)
{∥删除单链表la上的结点*p的直接前驱结点,假定*p存在
pre=la;
if(pre-next==p)
printf(“*p是链表第一结点,无前驱\n”) ; exit(0) ; }
while(pre->next->next !=p)
pre=pre->next;
u=pre->next; pre->next=u->next; free(u);
return(la);
}
-
- 设计一算法,将一个用循环链表表示的稀疏多项式分解成两个多项式,使这两个多项式各自仅有奇次幂或偶次幂项,并要求利用原链表中的结点空间来构造这两个链表。
【题目分析 】设循环链表表示的多项式的结点结构为:
typedef struct node
{int power; ∥幂
float coef; ∥系数
ElemType other; ∥其他信息
struct node *next; ∥指向后继的指针
}PNode,*PolyLinkedList;
则可以从第一个结点开始,根据结点的幂是奇数或偶数而将其插入到奇次幂或偶次幂项的链表中。假定用原链表保存偶次幂,要为奇次幂的链表生成一个表头,为了保持链表中结点的原来顺序,用一个指针指向奇次幂链表的表尾。注意链表分解时不能“断链”。
【算法2.12】
void PolyDis(PolyLinkedList poly)
∥将poly表示的多项式链表分解为各含奇次幂或偶次幂项的两个循环链表
{PolyLinkedList poly2=(PolyLinkedList)malloc(sizeof(PNode));
∥poly2表示只含奇次幂的多项式
r2=poly2; ∥r2是只含奇次幂的多项式链表的尾指针
r1=poly; ∥r1是只含偶次幂的多项式链表当前结点的前驱结点的指针
p=poly->next; ∥链表带头结点,p指向第一个元素
while(p!=poly)
if(p->power % 2)∥处理奇次幂
{r=p->next; ∥暂存后继
r2->next=p; ∥结点链入奇次幂链表
r2=p; ∥尾指针后移
p=r; ∥恢复当前待处理结点
}
else ∥处理偶次幂
{r1->next=p; r1=p; p=p->next;}
}
r->next=poly2; r1->next=poly; ∥构成循环链表
}∥PolyDis
-
- 以带头结点的双向链表表示的线性表L=(a1,a2,…,an),试写一时间复杂度为O(n)的算法,将L改造为L=(a1,a3,…,an,…,a4,a2)。
【题目分析】分析结果链表,易见链表中位置是奇数的结点保持原顺序,而位置是偶数的结点移到奇数结点之后,且以与原来相反的顺序存放。因此,可从链表第一个结点开始处理,位置是奇数的结点保留不动,位置是偶数的结点插入到链表尾部,并用一指针指向链表尾,以便对偶数结点“尾插入”。
【算法2.13】
DLinkedList DInvert(DLinkedList L)
∥将双向循环链表L位置是偶数的结点逆置插入到链表尾部
{p=L->next; ∥p指向第一元素
Q=p->prior; ∥Q指向最后一个元素
pre=L ; ∥pre指向链表中位置为奇数的结点的前驱
r=L ; ∥r指向链表中偶数结点的尾结点
i=0 ; ∥i记录结点序号
while(p != Q) ∥寻找插入位置
{i++ ;
if(i%2) ∥处理序号为奇数的结点
{p->prior=pre ;pre->next=p ;pre=p; p=p->next;}
else ∥处理序号为偶数的结点
{u=p ; ∥记住当前结点
p=p->next ;∥p指向下个待处理结点
u->prior=r->prior; ∥以下4个语句将结点插入链表尾
u->next=r;
r->prior->next=u;
r->prior=u;
r=u; ∥指向新的表尾
} ∥else
} ∥while
} ∥结束算法
-
- 设单向链表的头指针为head,试设计算法,将链表按递增的顺序就地排序。
【题目分析】本题中的“就地排序”,可理解为不另辟空间,这里利用直接插入原则把链表整理成递增有序链表。
【算法2.14】
LinkedList LinkListInsertSort(LinkedList head)
∥利用直接插入原则将带头结点的单链表head整理成递增的有序链表
{if(head->next!=null) ∥链表不为空表
{p=head->next->next; ∥p指向第一结点的后继
head->next->next=null;∥第一元素有序,然后从第二元素起依次插入
while(p!=null)
{r=p->next; ∥暂存p的后继
q=head;
while(q->next && q->next->data<p->data)
q=q->next;∥查找插入位置
p->next=q->next; ∥将p结点链入链表
q->next=p;
p=r;
}
}
}
-
- 已知递增有序的三个单链表分别代表集合A,B和C,设计算法实现A=A∪(B∩C),并使结果链表仍保持递增。要求算法的时间复杂度为O(|A|+|B|+|C|)。其中,|A|为集合A的元素个数。
【题目分析】本题首先求B和C的交集,即求B和C中的共有元素,再与A求并集,同时删除重复元素,以保持结果A递增。
【算法2.15】
LinkedList union(LinkedList A,B,C)
∥A、B和C均是带头结点的递增有序的单链表,本算法实现A=A∪(B∩C)
∥结果表A保持递增有序
{pa=A->next;pb=B->next;pc=C->next;∥设置三个工作指针
pre=A; ∥pre指向结果链表中当前待合并结点的前驱
A->data=MaxElemType;∥同类型元素最大值,起监视哨作用
while(pa || pb && pc)
{while(pb && pc)
if(pb->data<pc->data) pb=pb->next;
else if(pb->data>pc->data) pc=pc->next;
else break; ∥B表和C表有公共元素
if(pb && pc)
{while(pa && pa->data<pb->data)∥先将A中小于B,C公共元素部分链入
{pre->next=pa;pre=pa;pa=pa->next;}
if(pre->data!=pb->data)
{pre->next=pb;pre=pb;pb=pb->next;pc=pc->next;}
else{pb=pb->next;pc=pc->next;}∥A中已有B,C公共元素
}
}∥ while(pa||pb&&pc)
if(pa) pre->next=pa; ∥当B,C无公共元素,将A中剩余链入
else pre->next=null; ∥A已到尾,B,C也无公共元素,
}∥算法Union结束
-
- 顺序表la与lb非递减有序,顺序表空间足够大。试设计一种高效算法,将lb中元素合到la中,使新的la的元素仍保持非递减有序。高效指最大限度地避免移动元素。
【题目分析】顺序存储结构的线性表的插入,其时间复杂度为O(n),平均移动近一半的元素。线性表la和lb合并时,若从第一个元素开始比较,一定会造成元素后移,这不符合本题“高效算法”的要求。应从线性表的最后一个元素开始比较,大者放到最终位置上。设两线性表的长度各为m和n ,则结果表的最后一个元素应在m+n位置上。这样从后向前,直到第一个元素为止。
【算法2.16】
SeqList Union(SeqList la, SeqList lb)
∥算法将顺序存储的非递减有序表la和lb中的lb合并到la中,la仍非递减有序
{ m=la.last;n=lb.last;∥m,n分别为线性表la和lb的长度
k=m+n-1; ∥k为结果线性表的工作指针(下标)
i=m-1;j=n-1; ∥i,j分别为线性表la和lb的工作指针(下标)
while(i>=0 && j>=0)
if(la.data[i]>=lb.data[j]) la.data[k--]=la.data[i--];
else la.data[k--]=lb.data[j--];
while(j>=0) la.data[k--]=lb.data[j--];
la.last=m+n;
return la;
}
【算法讨论】算法中数据移动是主要操作。在最佳情况下(lb的最小元素大于la的最大元素),仅将lb的n个元素移(拷贝)到la中,时间复杂度为O(n),最差情况,la的所有元素都要移动,时间复杂度为O(m+n)。因数据合并到la中,所以在退出第一个while循环后,只需要一个while循环,处理lb中剩余元素。第二个循环只有在lb有剩余元素时才执行,而在la有剩余元素时不执行。本算法 “最大限度的避免移动元素”,是“一种高效算法”。
-
- 已知非空线性链表由head指出,试写一算法,将链表中数据域值最小的那个结点移到链表的最前面。要求:不得额外申请新的链结点。
【题目分析】 本题首先要查找最小值结点。将其移到链表最前面,实质上是将该结点从链表上摘下(不是删除并回收空间),再插入到链表的最前面。
【算法2.17】
LinkedList Delinsert(LinkedList head)
∥本算法将非空线性链表head中数据域值最小的那个结点移到链表的最前面
{p=head->next;∥p是链表的工作指针
pre=head; ∥pre指向链表中数据域最小值结点的前驱
q=p; ∥q指向数据域最小值结点,初始假定是第一结点
while(p->next)
{if(p->next->data<q->data){pre=p;q=p->next;} ∥找到新的最小值结点
p=p->next;
}
if(q!=head->next) ∥若最小值是第一元素结点,则不需再操作
{pre->next=q->next; ∥将最小值结点从链表上摘下
q->next=head->next; ∥将q结点插到链表最前面
head->next=q;
}
}∥Delinsert
-
- 设la是带头结点的非循环双向链表的指针,其结点中除有prior,data和next外,还有一访问频度域freq,其值在链表初始使用时为0。当在链表中进行ListLocate(la,x)运算时,若查找失败,则在表尾插入值为x的结点;若查找成功,值为x的结点的freq值增1,并要求链表按freq域值非增(递减)的顺序排列,且最近访问的结点排在频度相同的结点的后面,使频繁访问的结点总是靠近表头。试编写符合上述要求的ListLocate(la,x)运算的算法,返回找到结点的指针。
【题目分析】首先在双向链表中查找数据值为x的结点,查到后,将结点从链表上摘下,然后再顺结点的前驱链查找该结点的位置。
【算法2.18】
DLinkList ListLocate(DLinkedList L,ElemType x)
∥ L是带头结点的按访问频度递减的双向链表,本算法先查找数据x
∥查找成功时结点的访问频度域增1,最后将该结点按频度递减插入链表中
{DLinkList p=L->next,q=L; ∥p为L表的工作指针,q为p的前驱,用于查找插入位置
while(p && p->data!=x) {q=p;p=p->next;}∥ 查找值为x的结点
if(!p) {printf(“不存在所查结点,现插入之\n”);
s=(DNode *)malloc(sizeof(DNode));
s->data=x; s->freq=0; ∥插入值为x的结点
s->next=p; s->prior=q;
q->next=s; p->prior=s; p=s; ∥返回p结点;
}
else { p->freq++; ∥ 令元素值为x的结点的freq域加1
p->next->prior=p->prior; ∥ 将p结点从链表上摘下
p->prior->next=p->next;
q=p->prior; ∥ 以下查找p结点的插入位置
while(q !=L && q->freq<p->freq) q=q->prior;
p->next=q->next; q->next->prior=p;∥ 将p结点插入
p->prior=q; q->next=p;
}
return(p); ∥返回值为x的结点的指针
} ∥ 算法结束
-
- 三个带头结点的线性链表la、lb和lc中的结点均依元素值自小至大非递减排列(可能存在两个以上值相同的结点),编写算法对la表进行如下操作:使操作后的la中仅留下三个表中均包含的数据元素的结点,且没有值相同的结点,并释放所有无用结点。限定算法的时间复杂度为O(m+n+p),其中m、n和p分别为三个表的长度。
【题目分析】 留下三个链表中公共数据,首先查找两表la和lb中公共数据,再去lc中找有无该数据。要消除重复元素,应记住前驱,要求时间复杂度O(m+n+p),在查找每个链表时,指针不能回溯。
【算法2.19】
LinkedList lcommon(LinkedList la,lb,lc)
∥本算法使la表留下la、lb和lc三个非递减有序表共同结点,无重复元素
{pa=la->next;pb=lb->next;pc=lc->next;
∥pa,pb和pc分别是la,lb和lc三个表的工作指针
pre=la;
la->data=MaxElemType ∥pre是la表当前结点的前驱结点的指针,头结点作监视哨
while(pa && pb && pc) ∥当la,lb和lc表均不空时,查找三表共同元素
{while(pa&&pa->data==pre->data)
{u=pa; pa=pa->next; free(u);}//删la中相同元素
while(pb && pc)
if(pb->data<pc->data)pb=pb->next; ∥结点元素值小时,后移指针
else if(pb->data>pc->data)pc=pc->next;
else break ;∥处理lb和lc表元素值相等的结点
if(pb && pc)
{while(pa && pa->data<pc->data){u=pa;pa=pa->next;free(u);}
if(pa && pa->data>pc->data){pb=pb->next; pc=pc->next;}
else if(pa && pa->data==pc->data)∥pc,pa和pb对应结点元素值相等
{pre->next=pa;pre=pa;pa=pa->next;∥将新结点链入la表
pb=pb->next;pc=pc->next; ∥链表的工作指针后移
}∥pc,pa和pb对应结点元素值相等
} }∥while(pa && pb && pc)
pre->next=null; ∥置新la表表尾
while(pa!=null) ∥删除原la表剩余元素。
{u=pa;pa=pa->next;free(u);}
}∥算法结束
【算法讨论】 算法中la表、lb表和lc表均从头到尾(严格说lb、lc中最多一个到尾)遍历一遍,算法时间复杂度符合O(m+n+p)。算法主要由while(pa && pb && pc)控制。三表有一个到尾则结束循环。要注意头结点的监视哨的作用,否则第一个结点要特殊处理。算法最后要给新la表置结尾标记,同时若原la表没到尾,还应释放剩余结点所占的存储空间。本算法并未释放lb和lc中的结点。
第3章 栈和队列
一、基础知识题
-
- 有五个数依次进栈:1,2,3,4,5。在各种出栈的序列中,以3,4先出的序列有哪几个。(3在4之前出栈)。
【解答】34215 ,34251, 34521
铁路进行列车调度时, 常把站台设计成栈式结构,若进站的六辆列车顺序为:1,2,3,4,5,6, 那么是否能够得到435612, 325641, 154623和135426的出站序列, 如果不能,说明为什么不能; 如果能, 说明如何得到(即写出"进栈"或"出栈"的序列)。
【解答】输入序列为123456,不能得出435612和154623。不能得到435612的理由是,输出序列最后两元素是12,因为前面4个元素(4356)得到后,栈中元素剩12,且2在栈顶,不可能让栈底元素1在栈顶元素2之前出栈。不能得到154623的理由类似,当栈中元素只剩23,且3在栈顶,2不可能先于3出栈。
得到325641的过程如下:1 2 3顺序入栈,32出栈,得到部分输出序列32;然后45入栈,5出栈,部分输出序列变为325;接着6入栈并退栈,部分输出序列变为3256;最后41退栈,得最终结果325641。
得到135426的过程如下:1入栈并出栈,得到部分输出序列1;然后2和3入栈,3出栈,部分输出序列变为13;接着4和5入栈,5,4和2依次出栈,部分输出序列变为13542;最后6入栈并退栈,得最终结果135426。
-
- 若用一个大小为6的数组来实现循环队列,且当前rear和front的值分别为0和3,当从队列中删除一个元素,再加入两个元素后,rear和front的值分别为多少?
【解答】2和 4
-
- 设栈S和队列Q的初始状态为空,元素e1,e2,e3,e4,e5和e6依次通过栈S,一个元素出栈后即进队列Q,若6个元素出队的序列是e3,e5,e4,e6,e2,e1,则栈S的容量至少应该是多少?
【解答】 4
-
- 循环队列的优点是什么,如何判断“空”和“满”。
【解答】循环队列解决了常规用0--m-1的数组表示队列时出现的“假溢出”(即队列未满但不能入队)。在循环队列中,我们仍用队头指针等于队尾指针表示队空,而用牺牲一个单元的办法表示队满:即当队尾指针加1(求模)等于队头指针时,表示队列满。也有通过设标记以及用一个队头或队尾指针加上队列中元素个数来区分队列的“空”和“满”的。
-
- 设长度为n的链队列用单循环链表表示,若只设头指针,则入队和出队的时间如何?若只设尾指针呢?
【解答】若只设头指针,则入队的时间为O(n),出队的时间为O(1)。若只设尾指针,则入队和出队的时间均为O(1)。
-
- 指出下面程序段的功能是什么?
-
- void demo1(SeqStack S)
-
- 指出下面程序段的功能是什么?
{int i,arr[64],n=0;
while(!StackEmpty(S)) arr[n++]=Pop(S);
for(i=0;i<n;i++) Push(S,arr[i]);
}
【解答】程序段的功能是实现了栈中元素的逆置。
-
-
-
- void demo2(SeqStack S,int m)∥设栈中元素类型为int型
-
-
{int x;SeqStack T;
StackInit(T);
while(!StackEmpty(S))
if((x=Pop(S))!=m) Push(T,x);
while(!(StackEmpty(T)) {x=Pop(T); Push(S,x);}
}
【解答】程序段的功能是删除了栈中值为m的元素。
-
-
-
- void demo3(SeQueue Q,int m)∥设队列中元素类型为int型
-
-
{int x;SeqStack S;
StackInit(S);
while(!QueueEmpty(Q)){x=QueueOut(Q); Push(S,x);}
while(!StackEmpty(S)){x=Pop(s); QueueIn(Q,x);}
}
【解答】程序段的功能是实现了队列中元素的逆置。
-
- 试将下列递推过程改写为递归过程。
void ditui(int n)
{i=n;
while(i>0) printf(i--);
}
【解答】void digui(int n)
{if(n>0){printf(n);
digui(n-1);
}
}
-
- 写出下列中缀表达式的后缀表达式:
(1)A*B*C (2)(A+B)*C-D (3)A*B+C/(D-E) (4)(A+B)*D+E/(F+A*D)+C
解答】
(1)ABC**
(2)AB+C*D-
(3)AB*CDE-/+
(4)AB+D*EFAD*+/+C+
-
- 选择题:循环队列存储在数组A[0..m]中,则入队时的操作为( )。
A. rear=rear+1 B. rear=(rear+1) % (m-1)
C. rear=(rear+1) % m D. rear=(rear+1) % (m+1)
【解答】D
3.11 选择题:4个园盘的Hahoi塔,总的移动次数为( )。
A.7 B. 8 C.15 D.16
【解答】C
3.12选择题:允许对队列进行的操作有( )。
A. 对队列中的元素排序 B. 取出最近进队的元素
C. 在队头元素之前插入元素 D. 删除队头元素
【解答】D
二、算法设计题
3.13 利用栈的基本操作,编写求栈中元素个数的算法。
【题目分析】 将栈值元素出栈,出栈时计数,直至栈空。
【算法3.13】
int StackLength(Stack S)
{//求栈中元素个数
int n=0;
while(!StackEmpty(S)
{n++; Pop(S);
}
return n;
}
【算法讨论】:若要求统计完元素个数后,不能破坏原来栈,则在计数时,将原栈导入另一临时栈,计数完毕,再将临时栈倒入原栈中。
int StackLength(Stack S)
{//求栈中元素个数
int n=0;
Stack T;
StackInit(T); //初始化临时栈T
while(!StackEmpty(S)
{n++; Push(T,Pop(S));
}
while(!StackEmpty(T)
{Push(S,Pop(T));
}
return n;
}
-
- 双向栈S是在一个数组空间V[m]内实现的两个栈,栈底分别处于数组空间的两端。试为此双向栈设计栈初始化Init(S)、入栈Push(S,i,x)、出栈Pop(S,i)算法,其中i为0或1,用以指示栈号。
[题目分析]两栈共享向量空间,将两栈栈底设在向量两端,初始时,s1栈顶指针为-1,s2栈顶为m。两栈顶指针相邻时为栈满。两栈顶相向、迎面增长,栈顶指针指向栈顶元素。
【算法3.14】
#define ElemType int ∥假设元素类型为整型
typedef struct
{ElemType V[m]; ∥栈空间
int top[2]; ∥top为两个栈顶指针的数组
}stk;
stk S; ∥S是如上定义的结构类型变量,为全局变量
- 栈初始化
int Init()
{S.top[0]=-1;
S.top[1]=m;
return 1; //初始化成功
}
- 入栈操作:
int push(stk S ,int i,int x)
∥i为栈号,i=0表示左栈,i=1为右栈,x是入栈元素。入栈成功返回1,失败返回0
{if(i<0||i>1){printf(“栈号输入不对\n”);exit(0);}
if(S.top[1]-S.top[0]==1) {printf(“栈已满\n”);return(0);}
switch(i)
{case 0: S.V[++S.top[0]]=x; return(1); break;
case 1: S.V[--S.top[1]]=x; return(1);
}
}∥push
- 退栈操作
ElemType pop(stk S,int i)
∥退栈。i代表栈号,i=0时为左栈,i=1时为右栈。退栈成功时返回退栈元素
∥否则返回-1
{if(i<0 || i>1){printf(“栈号输入错误\n”);exit(0);}
switch(i)
{case 0: if(S.top[0]==-1) {printf(“栈空\n”);return(-1);}
else return(S.V[S.top[0]--]);
case 1: if(S.top[1]==m {printf(“栈空\n”); return(-1);}
else return(S.V[S.top[1]++]);
}∥switch }∥算法结束
- 判断栈空
int Empty();
{return (S.top[0]==-1 && S.top[1]==m);
}
[算法讨论] 请注意算法中两栈入栈和退栈时的栈顶指针的计算。s1(左栈)是通常意义下的栈,而s2(右栈)入栈操作时,其栈顶指针左移(减1),退栈时,栈顶指针右移(加1)。
-
- 设以数组Q[m]存放循环队列中的元素,同时设置一个标志tag,以tag=0和tag=1来区别在队头指针(front)和队尾指针(rear)相等时,队列状态为“空”还是“不空”。试编写相应的入队(QueueIn)和出队(QueueOut)算法。
【算法3.15】
- 初始化
SeQueue QueueInit(SeQueue Q)
{//初始化队列
Q.front=Q.rear=0; Q.tag=0;
return Q;
}
(2) 入队
SeQueue QueueIn(SeQueue Q,int e)
{//入队列
if((Q.tag==1) && (Q.rear==Q.front)) printf("队列已满\n");
else {Q.rear=(Q.rear+1) % m;
Q.data[Q.rear]=e;
if(Q.tag==0) Q.tag=1; //队列已不空
}
return Q;
}
(3)出队
ElemType QueueOut(SeQueue Q)
{//出队列
if(Q.tag==0) { printf("队列为空\n"); exit(0);}
else
{Q.front=(Q.front+1) % m;
e=Q.data[Q.front];
if(Q.front==Q.rear) Q.tag=0; //空队列
}
return(e);
}
-
- 假设用变量rear和length分别指示循环队列中队尾元素的位置和内含元素的个数。试给出此循环队列的定义,并写出相应的入队(QueueIn)和出队(QueueOut)算法。
【算法3.16】
(1)循环队列的定义
typedef struct
{ElemType Q[m]; ∥ 循环队列占m个存储单元
int rear,length; ∥ rear指向队尾元素,length为元素个数
}SeQueue;
(2) 初始化
SeQueue QueueInit(SeQueue cq)
∥cq为循环队列,本算法进行队列初始化
{ cq.rear=0; cq.length=0; return cq;
}
(3) 入队
SeQueue QueueIn(SeQueue cq,ElemType x)
∥cq是以如上定义的循环队列,本算法将元素x入队
{if(cq.length==m) return(0); ∥ 队满
else {cq.rear=(cq.rear+1) % m; ∥ 计算插入元素位置
cq.Q[cq.rear]=x; ∥ 将元素x入队列
cq.length++; ∥ 修改队列长度
}
return (cq);
}
-
- 出队
ElemType QueueOut(SeQueue cq)
∥ cq是以如上定义的循环队列,本算法是出队算法,且返回出队元素
{if(cq.length==0) return(0); ∥ 队空
else { int front=(cq.rear-cq.length+1+m) % m;∥ 出队元素位置
cq.length--; ∥ 修改队列长度
return (cq.Q[front]); ∥ 返回对头元素
}
}
-
- 已知Ackerman函数定义如下:
Akm(m,n)=
试写出递归和非递归算法。
【算法3.17】
- 递归算法
int Ack(int m,n)
{if(m==0) return(n+1);
else if(m!=0 && n==0) return(Ack(m-1,1));
else return(Ack(m-1,Ack(m,m-1));
}
- 非递归算法
int Ackerman( int m, int n)
{int akm[m][n];
int i,j;
for(j=0;j<n;j++) akm[0][j];=j+1;
for(i=1;i<m;i++)
{akm[i][0]=akm[i-1][1];
for(j=1;j<n;j++)
akm[i][j]=akm[i-1][akm[i][j-1]];
}
return(akm[m][n]);
}
-
- 假设称正读和反读都相同的字符序列为“回文”,例如,“abba“和“abccba”是“回文”,“abcde”和“ababab”则不是“回文“,试写一个算法,判别读入的一个以@为结束符的字符序列是否是“回文”。
【题目分析】将字符串前一半入栈,然后,栈中元素和字符串后一半进行比较。即将第一个出栈元素和后一半串中第一个字符比较,若相等,则再出栈一个元素与后一个字符比较,……,直至栈空,结论为字符序列是回文。在出栈元素与串中字符比较不等时,结论字符序列不是回文。
【算法3.18】
int sympthy(char str[],char s[])
{ int i=0,j,n;
while (str[i]!=‘\0’) i++; // 查字符个数
if(i==1){printf(“字符串是回文\n”);exit(0);}
n=i;
for(i=0;i<n/2;i++) s[i]=str[i]; //前一半字符入栈
j=i;
if(n%2==0) i++; //若n为偶数,
else i=i+2; //若n为奇数
while (i<n && str[i] == s[j]) //比较字符串是否是回文
{i++; j--;}
if (i==n) printf(“字符串是回文\n”);
else printf(“字符串不是回文\n”);
}
}
-
- 设整数序列a1,a2,…,an,给出求解最大值的递归程序。
【算法3.19】
int MaxValue (int a[],int n)
∥设n个整数存于数组a中,本算法求解其最大值
{if(n==1) max=a[1];
else if a[n]>MaxValue(a,n-1) max=a[n];
else max=MaxValue(a,n-1);
return(max);
-
- 已知栈的三个运算定义如下:
PUSH(ST,x):元素x入ST栈;
POP(ST,x):ST栈顶元素出栈并赋给变量x;
Sempty(ST):判ST栈是否为空。
利用栈的上述运算来实现队列的三个运算:
QueueIn:插入一个元素入队列;
QueueOut:删除一个元素出队列;
QueueEmpty:判队列为空。
【题目分析】栈的特点是后进先出,队列的特点是先进先出。所以,需要用两个栈s1和s2模拟一个队列的操作:s1作输入栈,逐个元素压栈,以此模拟队列元素的入队;出队时,将栈s1退栈并逐个压入栈s2中,s1中最先入栈的元素,在s2中处于栈顶,s2退栈,相当于队列的出队,实现了先进先出。显然,只有栈s2为空且s1也为空,才算是队列空。
【算法3.20】
- 入队
int QueueIn(stack s1,ElemType x)
∥s1是容量为n的栈,栈中元素类型是ElemType
∥本算法将x入栈,若入栈成功返回1,否则返回0
{if(top1==n && !Sempty(s2)) ∥top1是栈s1的栈顶指针,是全局变量
{printf(“栈满”);return(0);} ∥s1满s2非空,这时s1不能再入栈
if(top1==n && Sempty(s2)) ∥若s2为空,先将s1退栈,元素再压栈到s2
{while(!Sempty(s1))
{POP(s1,x);PUSH(s2,x);}
PUSH(s1,x); return(1); ∥x入栈,实现了队列元素的入队
}
(2)出队
void QueueOut(stack s2,s1)
∥s2是输出栈,本算法将s2栈顶元素退栈,实现队列元素的出队
{if(!Sempty(s2)) ∥栈s2不空,则直接出队
{POP(s2,x); printf(“出队元素为”,x); }
else ∥处理s2空栈
if(Sempty(s1))
{printf(“队列空”);exit(0);}∥若输入栈也为空,则判定队空
else ∥先将栈s1倒入s2中,再作出队操作
{while(!Sempty(s1)) {POP(s1,x);PUSH(s2,x);}
POP(s2,x); ∥s2退栈相当队列出队
printf(“出队元素\n”,x);
}
}
(3)判栈空
int QueueEmpty ()
{∥本算法判用栈s1和s2模拟的队列是否为空
if(Sempty(s1) && Sempty(s2)) return(1);∥队列空
else return(0); ∥队列不空
}
[算法讨论]算法中假定栈s1和栈s2容量相同。出队从栈s2出,当s2为空时,若s1不空,则将s1倒入s2再出栈。入队在s1,当s1满后,若s2空,则将s1倒入s2,之后再入队。因此队列的容量为两栈容量之和。元素从栈s1倒入s2,必须在s2空的情况下才能进行,即在要求出队操作时,若s2空,则不论s1元素多少(只要不空),就要全部倒入s2中。
第 4 章 串
一、基础知识题
4.1 简述下列每对术语的区别:
空串和空格串; 串常量与串变量;主串和子串;串变量的名字和串变量的值;静态分配的顺序串与动态分配的顺序串。
【解答】 不含任何字符的串称为空串,其长度为0。仅含有空格字符的串称为空格串,其长度为串中空格字符的个数。空格符可用来分割一般的字符,便于人们识别和阅读,但计算串长时应包括这些空格符。空串在串处理中可作为任意串的子串。
用引号(数据结构教学中通常用单引号,而C语言中用双引号)括起来的字符序列称为串常量,其串值是常量。串值可以变化的量称为串变量。
串中任意个连续的字符组成的子序列被称为该串的子串。包含子串的串又被称为该子串的主串。子串在主串中第一次出现的第一个字符的位置称子串在主串中的位置。
串变量与其它变量一样,要用名字引用其值,串变量的名字也是标识符,串变量的值可以修改。
串的存储也有静态存储和动态存储两种。静态存储指用一维数组,通常一个字符占用一个字节,需要静态定义串的长度,具有顺序存储结构的优缺点。若需要在程序执行过程中,动态地改变串的长度,则可以利用标准函数malloc()和free()动态地分配或释放存储单元,提高存储资源的利用率。在C语言中,动态分配和回收的存储单元都来自于一个被称之为“堆”的自由存储区,故该方法可称为堆分配存储。类型定义如下所示:
typedef struct
{ char *str;
int length;
}HString;
4.2设有串S=’good’,T=’I︼am︼a︼student’,R=’!’,求:
(1)StringConcat(T,R) (2)SubString(T,8,7)
(3)StringLength(T) (4)Index(T, ’a’)
(5)StringInsert(T,8,S)
(6)Replace(T, SubString(T,8,7), ’teacher’)
【解答】
(1) StringConcat(T,R)=’I︼am︼a︼student!’
(2) SubString(T,8,7)=’student’
(3) StringLength(T)=14
(4) Index(T, ’a’)=3
(5) StringInsert(T,8,S)=’I︼am︼a︼goodstudent’
(6) Replace(T, SubString(T,8,7),’teacher’)= ’I︼am︼a︼teacher’
4.3若串S1=‘ABCDEFG’, S2=‘9898’ ,S3=‘###’,S4=‘012345’,执行
concat(replace(S1,substr(S1,length(S2),length(S3)),S3),substr(S4,index(S2,‘8’),length(S2)))
操作的结果是什么?
【解答】
concat(replace(S1,substr(S1,length(S2),length(S3)),S3),substr(S4,index(S2,‘8’),length(S2)))
= concat(replace(S1,substr(S1,4,3),S3),substr(S4,2,4))
= concat(replace(S1,’DEF’,S3),’1234’)
= concat(‘ABC###G’,’1234’)
= ‘ABC###G1234’
4.4 设S为一个长度为n的字符串,其中的字符各不相同,则S中的互异的非平凡子串(非空且不同于S本身)的个数是多少?
【解答】长度为n的字符串中互异的非平凡子串(非空且不同于S本身)的个数计算如下:
长度为1的子串有n个,长度为2的子串有n-1个,长度为3的子串有n-2个,……,长度为n-1的子串有2个,长度为n的子串有1个(按题意要求这个子串就是S本身,不能计算在总数内)。故子串个数为:(2+n)*(n-1)/2
4.5 KMP算法(字符串匹配算法)较Brute(朴素的字符串匹配)算法有哪些改进?
【解答】KMP算法的最大特点是主串指针不回溯,在整个匹配过程中,对主串从头到尾扫描一遍,对于处理存储在外存上的大文件是非常有效的。
虽然Brute(朴素的字符串匹配)算法的时间复杂度是O(n*m),但在一般情况下,其实际的执行时间近似于O(n+m),因此至今仍被采用。KMP算法仅当主串与模式间存在许多“部分匹配”的情况下才显得比Brute(朴素的字符串匹配)算法快得多。
4.6 求串’ababaaababaa’ 的next函数值。
【解答】
j | 1 2 3 4 5 6 7 8 9 10 11 12 |
t串 | a b a b a a a b a b a a |
next[j] | 0 1 1 2 3 4 2 2 3 4 5 6 |
4.7 模式串t=’abcaabbcaabdab’,求模式串的next和nextval函数的值。
【解答】
j | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
t串 | a b c a a b b c a a b d a b |
next[j] | 0 1 1 1 2 2 3 1 1 2 2 3 1 2 |
nextval[j] | 0 1 1 0 2 1 3 1 0 2 1 3 0 1 |
4.8 对S=’aabcbabcaabcaaba’,T=’bca’,画出以T为模式串,S为目标串的匹配过程。
【解答】
↓i=1 第一趟匹配: a a b c b a b c a a b c a a b a b ↑j=1 ↓i=2 第二趟匹配: a b a b c a b c a c b a b b c ↑j=2 ↓i=3 第三趟匹配: a b a b c a b c a c b a b b ↑j=1 ↓i=7 第四趟匹配: a b a b c a b c a c b a b b c a (匹配成功) ↑j=4 |
4.9选择题:下面关于串的的叙述中,哪一个是不正确的?
A.串是字符的有限序列
B.空串是由空格构成的串
C.模式匹配是串的一种重要运算
D.串既可以采用顺序存储,也可以采用链式存储
【解答】 B
4.10选择题:串是一种特殊的线性表,下面哪个叙述体现了这种特殊性?
A.数据元素是一个字符 B. 可以顺序存储
C. 数据元素可以是多个字符 D. 可以链接存储
【解答】A
二、算法设计题
4.11试写出用单链表表示的字符串结点类型的定义,并依次实现它的计算串长度、串赋值、判断两串相等、求子串、两串连接、求子串在串中位置的6个函数。要求每个字符串结点中只存放一个字符。
【算法4.11】
单链表结点的类型定义如下:
typedef struct Node
{char data;
struct Node *next;
}LNode,*LinkedString;
(1) 计算串长度
int StringLength(LinkedString L)
{∥求带头结点的用单链表表示的字符串的长度
p=L->next; ∥p指向串中第一个字符结点
j=0; ∥计数器初始化
while(p)
{j++; p=p->next;} ∥计数器加1,指针后移
return j;
}
(2) 串赋值
LinkedString StringAssign(LinkedString L)
{//字符串赋值
LNode *s,*p,*r;
s=(char *)malloc(sizeof(char));
s->next=null; //头结点
r=s; //r记住尾结点
L=L->next; //串中第一字符
while(L)
{p=(char *)malloc(sizeof(char));
p->data=L->data; //赋值
p->next=r->next; //插入链表中
r->next=p;
r=p; //指向新的尾结点
L=L->next;
}
return s;
}
(3) 判断两串相等
int StringEqual(LinkedString s, LinkedString q)
{//判断字符串的相等
LNode *p=s->next,*r=q->next;
while(p && r)
if(p->data==r->data)
{p=p->next; r=r->next;}
else return 0;
if(!p && !r)
return 1;
else
return 0;
}
(4) 求子串
LinkedString Substring(LinkedString S, int start, int i)
{//求串S从start开始的i个字符所组成的子串
LNode *sub,*p,*r,*s=S->next;
int j=1;
if(start<1 || i<0) {printf(“参数错误\n”); exit(0);}
sub=(char *)malloc(sizeof(char));
sub->next=null; //头结点
r=sub;
while(s!=null && j<start)//查找起始结点
{s=s->next; j++;}
if(s==null)
{printf(“起始位置太大\n”); exit(0);}
else
{j=0;
while(s!=null && j<i)//若i太大,则截取到串尾
{p=(char *)malloc(sizeof(char));
p->data=s->data;
p->next=r->next;
r->next=p;
r=p;
j++;
}
}
return sub;
} ∥算法结束
(5) 两串连接
LinkedString Concat(LinkedString S, LinkedString T)
{∥求串S和串T的连接,返回结果串
LNode *p=S;
while(p->next!=null) //查找串尾
p=p->next;
p->next=T->next;
free(T); //释放头结点
return S;
}
(6) 求子串在主串中位置
int Index(LinkedString S, LinkedString T)
{//求子串在主串中的位置,成功时返回其在主串的位序,否则返回0表示失败
int i=1;j=1; ∥i记主串当前结点,j记子串在主串中的位序
p=S->next; ∥p是每趟匹配时S中的起始结点的指针
q=S->next; ∥q是S中的工作指针
r=T->next; ∥r是T中的工作指针
while(q && r)
if(q->data==r->data)
{q=q->next;r=r->next; i++;} ∥对应字母相等,指针后移
else ∥失配时,S起始结点后移,T从首字符结点开始
{i++;
j=i; ∥j记子串在主串中的位序
q=p->next;p=p->next; r=T->next;
}
if(r==null)return (j);
∥j是子串在主串的位序,p是子串在主串第一字符结点的指针
else return 0; ∥T并未在S中出现
} ∥算法结束
-
- 用顺序结构存储的串S,编写算法删除S中第i个字符开始的j个字符。
【题目分析】我们使用教材中定义的串的顺序存储结构。
【算法4.12】
void StringDelete(STring S,int i,int j)
{//在顺序存储的串S中删除自第i个字符开始的j个字符
if(i<1 || i>S.curlen || j<0) {printf(“参数错误\n”); exit(0);}
if(i+j-1>S.curlen) //若j太大,则从第i个字符起,删除到串尾
j=S.curlen-i+1;
for(ii=i+j-1;ii<S.curlen;ii++)
S.ch[i++]=S.ch[ii];
S.curlen-=j;
}
-
- 输入一个字符串,内有数字和非数字字符,如:ak123x456 17960?302gef4563,将其中连续的数字作为一个整体,依次存放到一数组a中,例如123放入a[0],456放入a[1],…… 。编写算法统计其共有多少个整数,并输出这些整数。
【题目分析】在一个字符串内,统计含多少整数的问题,核心是如何将数从字符串中分离出来。从左到右扫描字符串,初次碰到数字字符时,作为一个整数的开始。然后进行拼数,即将连续出现的数字字符拼成一个整数,直到碰到非数字字符为止,一个整数拼完,存入数组,再准备下一整数,如此下去,直至整个字符串扫描到结束。
【算法4.13】
int CountInt()
∥ 从键盘输入字符串,连续的数字字符算作一个整数,统计其中整数的个数
{char ch; int i=0,a[]; ∥ 整数存储到数组a,i记整数个数
scanf(“%c”,&ch); ∥ 从左到右读入字符串
while(ch!=‘#’) ∥‘#’是字符串结束标记
if(ch>=’0’&& ch<=’9’) ∥ 是数字字符
{num=0; ∥ 数初始化
while(ch>=’0’&& ch<=’9’)∥ 拼数
{num=num*10+‘ch’-‘0’;
scanf(“%c”,&ch);
}
a[i++]=num;
if(ch!=‘#’) ∥ 若拼数中输入了‘#’,则不再输入
scanf(“%c”,&ch);
}
else scanf(“%c”,&ch); ∥输入非数字且非#时,继续输入字符
printf(“共有%d个整数,它们是:\n”,i);
for(j=0;j<i;j++)
{printf(“%6d”,a[j]);
if((j+1)%10==0)printf(“\n”);} ∥ 每10个数输出在一行上
}∥CountInt
[算法讨论]假定字符串中的数均不超过32767,否则,需用长整型数组及变量。
-
- 输入一文本行(最多80个字符),编写算法求某一个不包含空格的子串在文本行中出现的次数。
【题目分析】本题是模式匹配问题。将文本行看作主串,先用子串定位函数确定子串在主串中的位置,如子串在主串中,则计数,并将子串后的部分主串当作新主串,继续查子串是否在主串中,如此下去,直到子串不在主串中为止。下面用串的基本操作编写求解本题的算法。并给出子串定位函数Index。
【算法4.14】
int Index(String S, String T)
{∥子串定位函数,成功返回子串T的第一字符在S中的位置,否则返回0
n=StringLength(S); m=StringLength(T);
i=1;
while(i<=n-m+1) ∥当i加上T的长度超过串S的长度时结束
{StringAssign(sub,SubString(S,i,m));∥取字串sub
if(StringEqual(sub,T)!=0) i++;
else return i ;
}∥while
return 0; ∥S中不存在与T相等的子串
}∥Index
int NumberofSub(String main, String sub)
{∥某一个不包含空格的子串在文本行中出现的次数
int i=0,j;
int n=StringLength(main), m=StringLength(sub);
j=Index(main,sub);
while(j!=0)
{i++;
StringAssign(main,SubString(main,j+m,n-(j+m)+1); ∥新主串
n=StringLength(main); ∥求新主串长度
j=Index(main,sub);
}
return i;
}
-
- 函数void insert(char*s,char*t,int pos)表示将字符串t插入到字符串s中,插入位置为pos。请编写实现该函数的算法。假设分配给字符串s的空间足够让字符串t插入。
【题目分析】本题是字符串的插入问题,要求在字符串s的pos位置,插入字符串t。首先应查找字符串s的pos位置,将第pos个字符到字符串s尾的子串向后移动字符串t的长度,然后将字符串t复制到字符串s的第pos位置后。
对插入位置pos要验证其合法性,小于1或大于串s的长度均为非法,因题目假设给字符串s的空间足够大,故对插入不必判溢出。
【算法4.15】
void insert(char *s,char *t,int pos)
∥将字符串t插入字符串s的第pos个位置。
{int i=1,x=0;
char *p=s,*q=t; ∥p,q分别为字符串s和t的工作指针
if(pos<1) {printf(“pos参数位置非法\n”);exit(0);}
while(*p!=’\0’&& i<pos) {p++;i++;} ∥查pos位置
∥若pos小于串s长度,则查到pos位置时,i=pos。
if(*p == '\0') {printf("%d位置大于字符串s的长度\n",pos);exit(0);}
else ∥查找字符串的尾
while(*p!= '\0')
{p++; i++;} ∥查到尾时,i为字符‘\0’的下标,p也指向‘\0’
while(*q!= '\0')
{q++; x++; } ∥查找字符串t的长度x,循环结束时q指向'\0'
for(j=i;j>=pos ;j--)
{*(p+x)=*p; p--;} ∥串s的pos后的子串右移,空出串t的位置
q--; ∥指针q回退到串t的最后一个字符
for(j=1;j<=x;j++) *p--=*q--; ∥将t串插入到s的pos位置上
【算法讨论】 串s的结束标记('\0')也后移了,而串t的结尾标记不应插入到s中。
-
- 设S=“S1S2…Sn”是一个长为N的字符串,存放在一个数组中,编写算法将S改造之后输出:
(1)将S的所有第偶数个字符按照其原来的下标从大到小的次序放在S的后半部分;
(2)将S的所有第奇数个字符按照其原来的下标从小到大的次序放在S的前半部分;
例如:S=‘ABCDEFGHIJKL’,则改造后的S为‘ACEGIKLJHFDB’。
【题目分析】对读入的字符串的第奇数个字符,直接放在数组前面,对第偶数个字符,先入栈,到读字符串结束,再将栈中字符出栈,送入数组中。
【算法4.16】
void ReArrangeString()
∥将字符串的第偶数个字符放在串的后半部分,第奇数个字符放在前半部分
{char ch,s[],stk[]; ∥s和stk是字符数组(表示字符串)和字符栈
int i=1,j; ∥i和j字符串和字符栈指针
while((ch=getchar())!=’#’)
s[i++]=ch; ∥读入字符串,’#’是字符串结束标志
s[i]=’\0’; ∥字符数组中字符串结束标志
i=1;j=1;
while(s[i]) ∥改造字符串
{if(i%2==0)
stk[i/2]=s[i];
else
s[j++]=s[i];
i++;
}∥while
i--; i=i/2; ∥i先从’\0’后退,然后其含义是第偶数字符的个数
while(i>0)
s[j++]=stk[i--] ∥将第偶数个字符逆序填入原字符数组
}
第5章 数组和广义表
一、基础知识题
5.1 已知二维数组A[3][5],其每个元素占3个存储单元,并且A[0][0]的存储地址为1200。求元素A[1][3]的存储地址(分别对以行序和列序为主序存储进行讨论),该数组共占用多少个存储单元?
【解答】按照以行序为主序存储公式:
LOC(i,j)=LOC(c1,c2)+[(i-c1)*(d2-c2+1)+(j-c2)]*L
在C语言中有:LOC(i,j)=LOC(0,0)+(i*(d2+1)+j)*L
则: LOC(A[1][3])=1200+(1*5+3)*3=1224 (按行序存储)
LOC(A[1][3])=1200+(3*3+1)*3=1230 (按列序存储)
-
- 有一个10阶的对称矩阵A,采用压缩存储方式以行序为主序存储,A[1][1]为第一元素,其存储地址为1,每个元素占一个地址空间,求A[7][5]和A[5][6]的地址。
【解答】按照公式:
LOC(A[7][5])=7(7-1)/2+5 = 26
LOC(A[5][6])=LOC(A[6][5])=6(6-1)/2+5=20
-
- 设有一个二维数组A[m][n],设A[0][0]存放位置在644,A[2][2]存放位置在676,每个元素占一个空间,问A[3][3]存放在什么位置?
【解答】因为A[0][0]存放位置在644,A[2][2]存放位置在676,每个元素占一个空间,说明一行有15个元素(算法:(676-2-644)/2)。A[3][3]存放位置是692。
5.4 二维数组A[9][10]的元素都是6个字符组成的串,请回答下列问题:
(1)存放A至少需要( )个字节;
(2)A的第7列和第4行共占( )个字节;
(3)若A按行存放,元素A[7][4]的起始地址与A按列存放时哪一个元素的起始地址一致。
【解答】按照题5.1给出的公式:
(1)存放A需要9*10*6=540个字节
(2)A的第7列和第4行共占(9+10-1)*6=108个字节
(3)LOC(A[7][4])= LOC(A[0][0])+[7*10+4]*L (按行序存储)
LOC(A[i][j])= LOC(A[0][0])+[j*9+i]*L(按列序存储,0<=i<=8,0<=j<=9)所以,i=2,j=8。
即元素A[7][4]的起始地址与A按列存放时A[2][8]的起始地址一致。
5.5 将一个A[1..100,1..100]的三对角矩阵,按行优先存入一维数组B[1..m]中,试确定m的值,并求A中元素A[77,78](即元素下标i=77,j=78)在B数组中的位置K。
【解答】三对角矩阵共3n-2个元素,存入B[1..3n-2]中,元素在一维数组B中的下标k和元素在矩阵中的下标i和j的对应关系为:
k = 3(i-1) (主对角线左下角,即i=j+1)
k = 3(i-1)+1 (主对角线上,即i=j)
k = 3(i-1)+2 (主对角线上,即i=j-1)
由以上三式,得
k=2(i-1)+j (1£i,j£n; 1£k£3n-2)
故A[77,78](即元素下标i=77,j=78)在B数组中的位置为k=2(77-1)+78=230。
-
- 设有对称矩阵A[n][n],将其上三角元素逐行存于数组B[0..m-1]中,使得B[k]=a[i,j]且k=f1(i)+f2(j)+c。试推导出函数f1,f2和常数c。
【解答】上三角矩阵第一行(下标为0)有n个元素,下标i-1行有n-(i-1)个元素,第一行(下标0)到下标i-1行是梯形,而第i行上第j个元素(即aij)是第i行上第j-i+1个元素,故元素Aij在一维数组中的存储位置(下标k)为:
k=(n+(n-(i-1)))i/2+(j-i+1)=(2n-i+1)i/2+j-i+1
将上面的等式进一步整理为:
k=(n-1/2)i-i2/2+j+1,
则得f1(i)=(n-1/2)i-i2/2,f2(j)=j,c=1。
-
- 设A和B均为下三角矩阵,每一个都有n行。因此在下三角区域中各有n(n+1)/2个元素。另设有一个二维数组C,它有n行n+1列。试设计一个方案,将两个矩阵A和B中的下三角区域元素存放于同一个C中。要求将A的下三角区域中的元素存放于C的下三角区域中,B的下三角区域中的元素转置后存放于C的上三角区域中。并给出计算A的矩阵元素aij和B的矩阵元素bij在C中的存放位置下标的公式。
【解答】
void UnionTrans(int A[][],int B[][],int C[][],int n)
∥本算法将n阶方阵的下三角矩阵A和B置于C中,B要逆置
{for(i=0;i<n;i++)
{for(j=0;j<=i;j++) C[i][j]=A[i][j];
for(j=i+1;j<=n;j++) C[i][j]=B[j-1][i];
}
}
A的矩阵元素aij和B的矩阵元素bij在C中的存放位置下标的公式:
A[i][j]=C[i][j]; { 0<=i<n, 0<=j<=i}
B[i][j]=C[j][i+1]; { 0<=i<n, i<=j<=n}
-
- 什么是广义表?请简述广义表和线性表的主要区别。
【解答】广义表是零至多个元素的有限序列,广义表中的元素可以是原子,也可以是子表。从“元素的有限序列”角度看,广义表满足线性结构的特性:在非空线性结构中,只有一个称为“第一个”的元素,只有一个称为“最后一个”的元素,第一元素有后继而没有前驱,最后一个元素有前驱而没有后继,其余每个元素有唯一前驱和唯一后继。从这个意义上说,广义表属于线性结构。当广义表中的元素都是原子时,广义表就蜕变为线性表。
-
- 求广义表D=(a,(b,(),c),((d),e))的长度和深度。
【解答】3和3
-
- 设广义表L=((),()),试问GetHead(L),GetTail(L)的值,求L的长度和深度各为多少?
【解答】GetHead(L)和GetTail(L)的值分别是()和(())。
L的长度和深度都是2。
-
- 求下列广义表运算的结果:
-
- GetHead ((p,h,w))
- GetTail((b,k,p,h))
- GetHead(GetTail(((a,b),(c,d))))
- GetTail(GetHead(((a,b),(c,d))))
【解答】(1) GetHead ((p,h,w))=p
(2) GetTail((b,k,p,h))=(k,p,h)
(3)GetHead(GetTail(((a,b),(c,d)))= GetHead(((c,d)))=(c,d)
(4) GetTail(GetHead(((a,b),(c,d))))=GetTail((a,b))=(b)
5.12 利用广义表的GetHead和GetTail操作写出函数表达式,把以下各题中的单元素banana从广义表中分离出来:
(1) (apple, pear, banana, orange)
(2) ((apple, pear), (banana, orange))
(3) ((((apple))), ((pear)), (banana), orange)
(4) (apple, (pear, (banana), orange))
【解答】
(1) GetHead(GetTail(GetTail((apple,pear,banana,orange))))
(2) GetHead(GetHead(GetTail((apple,pear),(banana,orange)))))
(3) GetHead(GetHead(GetTail(GetTail(((((apple))),((pear)),(banana),orange))))
(4) GetHead(GetHead(GetTail(GetHead(GetTail((apple,(pear,(banana),orange)))))))
5.13 画出下列广义表的两种存储结构图
(1) ((),A,(B,(C,D)),(E,F))
(2) ((),a,((b,c), (), d), ((e)))
(3) (((a)),(b),c,(a),(((d,e))))
(4) (((b,c),d),(a),((a),((b,c),d)))
【解答】(1)广义表的第一种存储结构的理论基础是,非空广义表可唯一分解成表头和表尾两部分,而由表头和表尾可唯一构成一个广义表。这种存储结构中,原子和表采用不同的结点结构(“异构”,即结点域个数不同)。原子结点两个域:标志域tag=0表示原子结点,域atom表示原子的值;子表结点三个域:tag=1表示子表,hp和tp分别是指向表头和表尾的指针。在画存储结构时,对非空广义表不断进行表头和表尾的分解,表头可以是原子,也可以是子表,而表尾一定是表(包括空表)。下面是本题的第一种存储结构图。
(1) ((),A,(B,(C,D)),(E,F))
广义表的第二种存储结构的理论基础是,非空广义表最高层元素间具有逻辑关系:第一个元素无前驱有后继,最后一个元素无后继有前驱,其余元素有唯一前驱和唯一后继。有人将这种结构看作扩充线性结构。这种存储结构中,原子和表均采用三个域的结点结构(“同构”)。结点中都有一个指针域tp指向后继结点。原子结点中还包括标志域tag=0和原子值域atom;子表结点还包括标志域tag=1和指向子表的指针hp。在画存储结构时,从左往右一个元素一个元素的画,直至最后一个元素。下面是本题的第二种存储结构图。
(1) ((),A,(B,(C,D)),(E,F))
5.14 选择题:对矩阵压缩存储是为了:
A.方便运算 B.方便存储 C.提高运算速度 D.减少存储空间
【解答】D
5.15选择题:下面说法不正确的是:
A. 广义表的表头总是一个广义表 B. 广义表的表尾总是一个广义表
C. 广义表难以用顺序存储结构 D. 广义表可以是一个多层次的结构
【解答】A
二、算法设计题
5.16 设矩阵A中的某一个元素A[i][j]是第i行中的最小值,而又是第j列中的最大值,则称A[i][j]为矩阵中的一个鞍点,请写出一个可确定该鞍点位置的算法(如果这个鞍点存在),并给出算法的时间复杂度。
【题目分析】 寻找马鞍点最直接的方法,是在一行中找出一个最小值元素,然后检查该元素是否是元素所在列的最大元素,如是,则输出一个马鞍点,时间复杂度是O(m*(m+n)).本算法使用两个辅助数组max和min,存放每列中最大值元素的行号和每行中最小值元素的列号,时间复杂度为O(m*n+m),但比较次数比前种算法会增加,也多使用向量空间。
【算法5.16】
int m=10, n=10;
void Saddle(int A[m][n])
∥A是m*n的矩阵,本算法求矩阵A中的马鞍点
{int i,j, max[n]={0}, ∥max数组存放各列最大值元素的行号,初始化为行号0
min[m]={0}; ∥min数组存放各行最小值元素的列号,初始化为列号0
for(i=0;i<m;i++) ∥选各行最小值元素和各列最大值元素.
for(j=0;j<n;j++)
{if(A[max[j]][j]<A[i][j]) max[j]=i; ∥修改第j列最大元素的行号
if(A[i][min[i]]>A[i][j]) min[i]=j; ∥修改第i行最小元素的列号
}
for(i=0;i<m;i++)
{j=min[i]; ∥第i行最小元素的列号
if(i==max[j])
printf(“A[%d][%d]是马鞍点,元素值是%d”,i,j,A[i][j]);∥是马鞍点
}
}∥ Saddle
-
- 设稀疏矩阵用三元组表示,编写算法将两个稀疏矩阵相加,结果矩阵仍用三元组表示。
【题目分析】设稀疏矩阵为A和B,算法的基本思想是:依次扫描A和B的行号和列号: 若A的当前项的行号小于B的当前项的行号,则将A的当前项存入C中;若A的当前项的行号大于B的当前项的行号,则将B的当前项存入C中;若A的当前项的行号等于B的当前项的行号,则比较其列号,将列号较小的当前项存入C中;如果行号与列号都相等,且对应的元素值相加后不为0,则将该非零元素存入C中。
【算法5.17】
void matadd(TSMatrix A, TSMatrix B, TSMatrix C)
{∥采用三元组顺序表方式存储,实现稀疏矩阵相加
C.m=A.m; C.n=A.n; C.len=0; ∥结果矩阵C初始化
i=0; j=0;
if(A.len || B.len)
{k=0;
while(i<A.len && j<B.len)
if(A.data[i].row<B.data[j].row) ∥A的行号小于B的行号
{C.data[k].row=A.data[i].row;
C.data[k].col=A.data[i].col;
C.data[k++].e=A.data[i++].e;
}
else if(A.data[i].row>B.data[j].row) ∥A的行号大于B的行号
{C.data[k].row=B.data[j].row;
C.data[k].col=B.data[j].col;
C.data[k++].e=B.data[j++].e;
}
else ∥A的行号等于B的行号
if(A.data[i].col<B.data[j].col) ∥A的列号小于B的列号
{C.data[k].row=A.data[i].row;
C.data[k].col=A.data[i].col;
C.data[k++].e=A.data[i++].e;
}
else if(A.data[i].col>B.data[j].col)∥A的列号大于B的列号
{C.data[k].row=B.data[j].row;
C.data[k].col=B.data[j].col;
C.data[k++].e=B.data[j++].e;
}
else ∥A的行、列号分别等于B的行、列号
{num=A.data[i++].e+B.data[j++].e;∥对应元素相加
if(num!=0) ∥和不为0,则存入C中
{C.data[k].row=A.data[i].row;
C.data[k].col=A.data[i].col;
C.data[k++].e=num;
}
} ∥else A的行、列号分别等于B的行、列号
while(i<A.len) ∥A的元素还没有扫描完
{C.data[k].row=A.data[i].row;
C.data[k].col=A.data[i].col;
C.data[k++].e=A.data[i++].e;
}
while(j<B.len) ∥B的元素还没有扫描完
{C.data[k].row=B.data[j].row;
C.data[k].col=B.data[j].col;
C.data[k++].e=B.data[j++].e;
}
C.len=k;
return C;
}
-
- 三对角矩阵 A[n][n],将其三条对角线上的元素逐行地存储到向量B[0..3n-3]中,使得B[k]=aij,写一算法求三对角矩阵在这种压缩存储表示下的转置矩阵。
【题目分析】对角矩阵逐行存储到B[0..3n-3]中时,三对角线上元素的坐标满足|j-i|<=1。
【算法5.18】
void compress(int A[n][n],int B[ ],int n)
{//三对角矩阵A[0..n-1,0..n-1],将三条对角线上的元素逐行存放于数组B中
k=0;
for(i=0;i<n;i++)
{if(i==0) //第一行(行下标0)上两个元素
for(j=i;j<=i+1;j++)//其余每行上三个元素
B[k++]=A[i][j];
else if(i==n-1) //最后一行(行下标n-1)上两个元素
for(j=i-1;j<=i;j++)
B[k++]=A[i][j];
else
for(j=i-1;j<=i+1;j++)
B[k++]=A[i][j];
}
(2)已知k求i,j时,则下标的对应关系是:
i=(k+1)/3; (0≤k≤3n-3) ∥ (k+1)/3取小于(k+1)/3的最大整数
void uncompress(int B[ ],int A[n][n],int n)
{//由数组B[0..3n-3]确定三对角矩阵A[0..n-1,0..n-1]
for(i=0;i<n;i++) //矩阵初始化
for(j=0;j<n;j++)
A[i][j]=0;
for(k=0;k<=3*n-3;k++)
{i=(k+1)/3;
if(k+1)%3==0) j=i-1;
else if(k%3==0) j=i;
else if(k-1)%3==0) j=i+1;
A[j][i]=B[k]; //转置
}
}
-
- 设A[1..100]是一个记录构成的数组,B[1..100]是一个整数数组,其值介于1至100之间,现要求按B[1..100]的内容调整A中记录的次序,比如当B[1]=ll时,则要求将A[1]的内容调整到A[11]中去。规定可使用的附加空间为O(1)。
【题目分析】题目要求按B数组内容调整A数组中记录的次序,可以从i=1开始,检查是否B[i]=i。如是,则A[i]恰为正确位置,不需再调;否则,B[i]=k≠i,则将A[i]和A[k]对调,B[i]和B[k]对调,直到B[i]=i为止。
【算法5.19】
void CountSort(rectype A[],int B[])
∥A是100个记录的数组,B是整型数组,本算法利用数组B对A进行计数排序
{int i,j,n=100;
i=1;
while(i<=n)
{if(B[i]!=i) ∥若B[i]=i则A[i]正好在自己的位置上,则不需要调整
{ j=i;
while(B[j]!=i)
{ k=B[j]; B[j]=B[k]; B[k]=k;∥ B[j]和B[k]交换
r0=A[j];A[j]=A[k]; A[k]=r0;∥r0是数组A的元素类型,A[j]和A[k]交换
}
i++;
} ∥完成了一个小循环,第i个已经安排好
}∥算法结束
-
- 请编写递归算法,逆转广义表中的数据元素。例如:将广义表:(a,((b,c),()),(((d),e),f))逆转为:((f,(e,(d))),((),(c,b)),a)。
【题目分析】采用广义表的第二种存储结构(扩充的线性链表),类似于单链表的逆置。
【算法5.20】
Void GListInvert(GList p,GList t)
//将广义表p逆置为广义表t
{GList q=null;
while(p)
{if(p->tag!=0) //若是字表,则递归逆置
{m=p->val.hp;
GListInvert(m,n) ;
p->val.hp=n;
};
r=p->tp; //保留后继
p->tp=q;
q=p;
p=r //恢复当前待处理结点
}
t=q;
}
第6章 树和二叉树
一、基础知识题
6.1设树T的度为4,其中度为1,2,3和4的结点个数分别为4,2,1,1,求树T中的叶子数。
【解答】 设度为m的树中度为0,1,2,…,m的结点数分别为n0, n1, n2,…, nm,结点总数为n,分枝数为B,则下面二式成立
n= n0+n1+n2+…+nm (1)
n=B+1= n1+2n2 +…+mnm+1 (2)
由(1)和(2)得叶子结点数n0=1+
即: n0=1+(1-1)*4+(2-1)*2+(3-1)*1+(4-1)*1=8
6.2一棵完全二叉树上有1001个结点,求叶子结点的个数。
【解答】因为在任意二叉树中度为2 的结点数n2和叶子结点数n0有如下关系:n2=n0-1,所以设二叉树的结点数为n, 度为1的结点数为n1,则
n= n0+ n1+ n2
n=2n0+n1-1
1002=2n0+n1
由于在完全二叉树中,度为1的结点数n1至多为1,叶子数n0是整数。本题中度为1的结点数n1只能是0,故叶子结点的个数n0为501.
6.3 一棵124个叶结点的完全二叉树,最多有多少个结点。
【解答】由公式n=2n0+n1-1,当n1为1时,结点数达到最多248个。
6.4.一棵完全二叉树有500个结点,请问该完全二叉树有多少个叶子结点?有多少个度为1的结点?有多少个度为2的结点?如果完全二叉树有501个结点,结果如何?请写出推导过程。
【解答】由公式n=2n0+n1-1,带入具体数得,500=2n0+n1-1,叶子数是整数,度为1的结点数只能为1,故叶子数为250,度为2的结点数是249。
若完全二叉树有501个结点,则叶子数251,度为2的结点数是250,度为1的结点数为0。
6.5 某二叉树有20个叶子结点,有30个结点仅有一个孩子,则该二叉树的总结点数是多少。
【解答】由公式n=2n0+n1-1,得该二叉树的总结点数是69。
6.6 求一棵具有1025个结点的二叉树的高h。
【解答】该二叉树最高为1025(单支树),最低高为11。因为210-1<1025<211-1,故1025个结点的完全二叉树高11。
6.7 一棵二叉树高度为h,所有结点的度或为0,或为2,则这棵二叉树最少有多少结点。
【解答】第一层只一个根结点,其余各层都两个结点,这棵二叉树最少结点数是2h-1。
6.8将有关二叉树的概念推广到三叉树,则一棵有244个结点的完全三叉树的高度是多少。
【解答】设含n个结点的完全三叉树的高度为h,则有
<n≤
<n≤
<2n<
本题n=244, 故h=6。
6.9 对二叉树的结点从1开始进行连续编号,要求每个结点的编号大于其左、右孩子的编号,同一结点的左、右孩子中,其左孩子的编号小于其右孩子的编号,是采用何种次序的遍历实现编号的。
【解答】后序遍历二叉树,因为后序遍历顺序为左子树-右子树-根结点。
6.10 高度为h(h>0)的满二叉树对应的森林由多少棵树构成。
【解答】因为在二叉树转换为森林时,二叉树的根结点,根结点的右子女,右子女的右子女,……,都是树的根,所以,高度为h(h>0)的满二叉树对应的森林由h棵树构成。
6.11 某二叉树结点的中序序列为BDAECF,后序序列为DBEFCA,则该二叉树对应的森林包括几棵树?
【解答】3棵树。(本题不需画出完整的二叉树,更不需要画出森林,只需画出二叉树的右子树就可求解。)
6.12 对任意一棵树,设它有n个结点,这n个结点的度数之和为多少?
【解答】n-1。度数其实就是分支个数。根结点无分支所指,其余结点有且只有一个分支所指。
6.13 一棵左子树为空的二叉树在先序线索化后,其中空的链域的个数是多少?
【解答】对二叉树线索化时,只有空链域才可加线索。一棵左子树为空的二叉树在先序线索化时,根结点的左链为空,应加上指向前驱的线索,但根结点无前驱,故该链域为空。同样分析知道最后遍历的结点的右链域为空。故一棵左子树为空的二叉树在先序线索化后,其中空的链域的个数是2个。
6.14 一棵左、右子树均不空的二叉树在先序线索化后,其中空的链域的个数是多少?
【解答】1个。
6.15 设B是由森林F变换得的二叉树。若F中有n个非终端结点,则B中右指针域为空的结点有几个?
【解答】森林中任何一个非终端结点在转换成二叉树时,其第一个子女结点成为该非终端结点的左子女,其余子女结点成为刚生成的左子女结点的右子女,右子女结点的右子女,……,最右子女结点的右链域为空。照此分析,n个非终端结点在转换后,其子女结点中共有n个空链域。另外,森林中各棵树的根结点可以看做互为兄弟,转换成二叉树后也产生1个空链域。因此,本题的答案是n+1。
6.16 试分别找出满足以下条件的所有二叉树:
(1) 二叉树的前序序列与中序序列相同;
(2) 二叉树的中序序列与后序序列相同;
(3) 二叉树的前序序列与后序序列相同;
(4) 二叉树的前序序列与层次序列相同;
(5) 二叉树的前序、中序与后序序列均相同。
【解答】前序遍历二叉树的顺序是“根—左子树—右子树”,中序遍历的顺序是“左子树—根—右子树”,后序遍历顺序是:“左子树—右子树―根",根据以上原则,本题解答如下:
若前序序列与中序序列相同,则或为空树,或为任一结点至多只有右子树的二叉树。
若中序序列与后序序列相同,则或为空树,或为任一结点至多只有左子树的二叉树。
若前序序列与后序序列相同,则或为空树,或为只有根结点的二叉树。
若二叉树的前序、中序与后序序列均相同,则或为空树,或为只有根结点的二叉树。
6.17 已知一棵二叉树的前序遍历的结果是ABECDFGHIJ,中序遍历的结果是EBCDAFHIGJ,试画出这棵二叉树,对二叉树进行中序线索化,并将该二叉树转换为森林。
B |
A |
J |
C |
E |
D |
G |
F |
H |
I |
n |
u |
l |
l |
n |
u |
l |
l |
|
|
|
|
【解答】
6.18 已知一棵二叉树的后序遍历序列为EICBGAHDF,同时知道该二叉树的中序遍历序列为CEIFGBADH,试画出该二叉树。
|
F |
E |
C |
I |
A |
B |
G |
D |
H |
【解答】
6.19设二叉树中每个结点均用一个字母表示,若一个结点的左子树或右子树为空,用#表示,现前序遍历二叉树,访问的结点序列为ABD##C#E##F##,写出中序和后序遍历二叉树时结点的访问序列。
【解答】中序遍历二叉树时结点的访问序列:#D#B#C#E#A#F#
后序遍历二叉树时结点的访问序列。##D###ECB##FA
6.20有n个结点的k叉树(k≥2)用k叉链表表示时,有多少个空指针?
【解答】k叉树(k≥2)用k叉链表表示时,每个结点有k个指针,除根结点没有指针指向外,其余每个结点都有一个指针指向,故空指针的个数为:
nk-(n-1)=n(k-1)+1
6.21 一棵高度为h的满k叉树有如下性质:根结点所在层次为0;第h层上的结点都是叶子结点;其余各层上每个结点都有k棵非空子树,如果按层次自顶向下,同一层自左向右,顺序从1开始对全部结点进行编号,试问:
(1)各层的结点个数是多少?
(2)编号为i的结点的双亲结点(若存在)的编号是多少?
(3)编号为i的结点的第m个孩子结点(若存在)的编号是多少?
(4)编号为i的结点有右兄弟的条件是什么?其右兄弟结点的编号是多少?
【解答】
(1)kl(l为层数,按题意,根结点为0层)
(2)因为该树每层上均有kl个结点,从根开始编号为1,则结点i的从右向左数第2个孩子的结点编号为ki。设n 为结点i的子女,则关系式(i-1)k+2<=n<=ik+1成立,因i是整数,故结点i的双亲的编号为ë(i-2)/kû+1。
(3) 结点i(i>1)的前一结点编号为i-1(其最右边子女编号是(i-1)*k+1),故结点 i的第 m个孩子的编号是(i-1)*k+1+m。
(4) 根据以上分析,结点i有右兄弟的条件是,它不是双亲的从右数的第一子女,即 (i-1)%k!=0,其右兄弟编号是i+1。
6.22.证明任一结点个数为n(n>0) 的二叉树的高度至少为ë(logn)û+1。
【解答】最低高度二叉树的特点是,除最下层结点个数不满外,其余各层的结点数都应达到各层的最大值。设n个结点的二叉树的最低高度是h,则n应满足2h-1≦n≦2h-1关系式。解此不等式,并考虑h是整数,则有h=ëlognû+1,即任一结点个数为n 的二叉树的高度至少为O(logn)。
6.23 已知A[1..N]是一棵顺序存储的完全二叉树,如何求出A[i]和A[j]的最近的共同祖先?
【解答】根据顺序存储的完全二叉树的性质,编号为i的结点的双亲的编号是ëi/2û,故A[i]和A[j]的最近公共祖先可如下求出:
while(i/2!=j/2)
if(i>j) i=i/2;
else j=j/2;
退出while后,若i/2=0,则最近公共祖先为根结点,否则最近公共祖先是i/2。
6.24已知一棵满二叉树的结点个数为20到40之间的素数,此二叉树的叶子结点有多少个?
【解答】结点个数在20到40的满二叉树且结点数是素数的数是31,其叶子数是16。
6.25求含有n个结点、采用顺序存储结构的完全二叉树中的序号最小的叶子结点的下标。要求写出简要步骤。
【解答】根据完全二叉树的性质,最后一个结点(编号为n)的双亲结点的编号是ën/2û,这是最后一个分枝结点,在它之后是第一个终端(叶子)结点,故序号最小的叶子结点的下标是ën/2û+1。
6.26 试证明:同一棵二叉树的所有叶子结点,在前序序列、中序序列以及后序序列中都按相同的相对位置出现(即先后顺序相同),例如前序abc,后序bca,中序bac。
【证明】前序遍历是“根-左-右”,中序遍历是“左-根-右”,后序遍历是“左-右-根”。三种遍历中只是访问“根”结点的时机不同,对左右子树均是按左右顺序来遍历的,因此所有叶子都按相同的相对位置出现。
6.27设具有四个结点的二叉树的前序遍历序列为abcd;S为长度等于4的由a,b,c,d排列构成的字符序列,若任取S作为上述算法的中序遍历序列,试问是否一定能构造出相应的二叉树,为什么?试列出具有四个结点二叉树的全部形态及相应的中序遍历序列。
【解答】若前序序列是abcd,并非由这四个字母的任意组合(4!=24)都能构造出二叉树。因为以abcd为输入序列,通过栈只能得到1/(n+1)*2n!/(n!*n!)=14 种,即以abcd为前序序列的二叉树的数目是14。任取以abcd作为中序遍历序列,并不全能与前序的abcd序列构成二叉树。例如:若取中序序列dcab就不能。
该14棵二叉树的形态及中序序列略。
6.28已知某二叉树的每个结点,要么其左、右子树皆为空,要么其左、右子树皆不空。又知该二叉树的前序序列为:JFDBACEHXIK;后序序列为:ACBEDXIHFKJ。请给出该二叉树的中序序列,并画出相应的二叉树树形。
【解答】一般说来,仅仅知道二叉树的前序遍历序列和后序遍历序列并不能确定这棵二叉树,因为并不知道左子树和右子树两部分各有多少个结点。但本题有特殊性,即每个结点“要么其左、右子树皆为空,要么其左、右子树皆不空”。具体说,前序序列的第一个结点是二叉树的根,若该结点后再无其它结点,则二叉树只有根结点;否则,该结点必有左右子树,且根结点后的第一个结点就是“左子树的根”。到后序序列中查找这个“左子树的根”,它将后序序列分成左右两部分:左部分(包括所查到的“左子树的根结点”)是二叉树的左子树(可能为空),右部分(除去最后的根结点)则是右子树(可能为空)。这样,在确定根结点后,就可以将后序遍历序列(从而也将前序遍历序列)分成左子树和右子树两部分了。
本题中,先看前序遍历序列,第一个结点是J,所以J是二叉树的根,J后面还有结点,说明J有左、右子树,J后面的F必是左子树的根。到后序遍历序列中找到F,F将后序遍历序列分成两部分:左面ACBEDXIH,说明FACBEDXIH是根J的左子树;右面K(K的右面J已知是根),说明K是根J的右子树。这样,问题就转化为“以前序序列FDBACEHXI和后序序列ACBEDXIHF去构造根J的左子树”,以及“以前序序列K和后序序列K去构造根J的右子树”了。如此构造下去,所构造的二叉树如下。易见,中序序列为ABCDEFXHIJK。
J |
F |
D |
A |
H |
K |
X |
E |
C |
B |
I |
6.29 已知一个森林的先序序列和后序序列如下,请构造出该森林。
先序序列:ABCDEFGHIJKLMNO
后序序列:CDEBFHIJGAMLONK
【解答】森林的先序序列和后序序列对应其转换的二叉树的先序序列和中序序列,应先据此构造二叉树,再构造出森林。
F |
A |
E |
B |
D |
C |
J |
H |
G |
I |
K |
O |
N |
L |
MG |
6.30 画出同时满足下列两条件的两棵不同的二叉树。
(1)按先根序遍历二叉树顺序为ABCDE。
(2)高度为5其对应的树(森林)的高度最大为4。
【解答】
A |
B |
D |
C |
E |
|
|
A |
B |
C |
D |
E |
6.31用一维数组存放的一棵完全二叉树;ABCDEFGHIJKL。请写出后序遍历该二叉树的结点访问序列。
【解答】后序遍历该二叉树的结点访问序列为:DECGHFBKJLIA
6.32一棵二叉树的先序、中序和后序序列如下,其中有部分未标出,试构造出该二叉树。
先序序列为: C D E G H I K
中序序列为:C B F A _ J K I G
后序序列为: E F D B J I H A
【解答】
|
A |
B |
C |
E |
D |
G |
I |
H |
F |
J |
K |
6.33设树形T在后根次序下的结点排列和各结点相应的度数如下:
后根次序:BDEFCGJKILHA
次 数:000030002024
请画出T的树形结构图。
【解答】在树在后根遍历次序下,根结点在最后,任何结点的子树的所有结点都直接排在该结点之前。例如,挨着根结点的是根结点的最右边的子女。每棵子树的所有结点都聚集在一起,中间不会插入其它结点,也不会丢掉任何结点。
按照这种理论解答本题,在遍历次序中从右到左分析,A是根,它有4个子女,H是它的最右边的子女(第4子女)。H有2个子女,L是H的最右边的子女,L无子女,故I是H的第1子女。I又有2个子女:K和J,二者均无子女。由此推断出下一个结点G是根结点A的第3子女。……,继续构造,直至最左面的结点B,结果树形如下:
I |
C |
A |
B |
G |
E |
D |
F |
K |
J |
L |
H |
6.34 若森林共有n个结点和b条边(b<n),则该森林中有多少棵树。
【解答】n-b
森林的n个结点开始可看作是n个连通分量,加入一条边将减少一个连通分量。因为树可以定义为无环的图,故加入b条边将减少b个连通分量,因而n个结点b条边的森林有n-b棵树。
6.35 求高度为k的完全二叉树至少有多少个叶结点?
【解答】当高度为k的完全二叉树的第k层只有一个结点时,结点数达到最少,这也是高度为k的完全二叉树具有的最少叶结点数,我们知道,第k-1层有2k-2-1个叶结点,第k层只有一个叶结点,但这个结点的双亲已不再是叶子结点,故高度为k的完全二叉树至少有2k-2-1个叶结点。
6.36 某通信电文由A、B、C、D、E、F六个字符组成,它们在电文中出现的次数分别是16,5,9,3,20,1。试画出其哈夫曼树并确定其对应的哈夫曼编码。
【解答】
A |
|
|
|
D |
F |
B |
C |
|
E |
|
对应的哈夫曼编码: A—10, B—1101, C—111, D—11001, E—0, F—11000
二、算法设计题
6.37以二叉链表作为存储结构,设计算法求出二叉树T中度为0、度为1和度为2的结点数。
【题目分析】结点计数可以在遍历中解决。根据“访问根结点”在“递归遍历左子树”和“递归遍历右子树”中位置的不同,而有前序、后序和中序遍历。
【算法6.37】
int n2,n1,n0; ∥设置三个全局变量,分别记度为2,1和叶子结点的个数
void Count(BiTree t)
{if(t)
{if(t->lchild && t->rchild) n2++;
else if(t->lchild && !t->rchild || !t->lchild && t->rchild) n1++;
else n0++;
if(t->lchild!=null) Count(t->lchild);
if(t->rchild!=null) Count(t->rchild);
}∥Count
6.38一棵n个结点的完全二叉树存放在二叉树的顺序存储结构中,试编写非递归算法对该树进行先序遍历。
【题目分析】二叉树的顺序存储是按完全二叉树的顺序存储格式,双亲与子女结点下标间有确定关系。顺序存储结构的二叉树用结点下标大于n(完全二叉树)或0(对一般二叉树的“虚结点”)判空。本题是完全二叉树。
【算法6.38】
void PreOrder(ElemType bt[],int n)
∥对以顺序结构存储的完全二叉树bt进行前序遍历
{int i=1,top=0,s[]; ∥top是栈s的栈顶指针,栈容量足够大
while(i<=n || top>0)
{while(i<=n)
{printf(bt[i]); ∥访问根结点;
if(2*i+1<=n) s[++top]=2*i+1; ∥右子女的下标位置进栈
i=2*i; ∥沿左子女向下
}
if(top>0) i=s[top--]; ∥取出栈顶元素
}∥while
}∥结束PreOrder
6.39以二叉链表作为存储结构的二叉树,按后序遍历时输出的结点顺序为a1, a2,…,an。试编写一算法,要求输出后序序列的逆序an,an-1 …,a2 ,a1 。
【题目分析】二叉树后序遍历是按“左子树-右子树-根结点”的顺序遍历二叉树,根据题意,若将遍历顺序改为“根结点-右子树-左子树”,就可以实现题目要求。
【算法6.39】
void PostOrder(BiTree bt)
//对二叉树bt进行先右后左的“先根”遍历
{if(bt)
{printf(bt->data); //访问根结点
PostOrder(bt->rchild); //先根遍历右子树
PostOrder(bt->lchild); //先根遍历左子树
}
}
6.40以二叉链表作为存储结构,设计算法交换二叉树中所有结点的左、右子树。
【算法6.40】
void exchange(BiTree bt)
∥将二叉树bt所有结点的左右子树交换
{if(bt){BiTree s;
s=bt->lchild; bt->lchild=bt->rchild; bt->rchild=s; ∥左右子女交换
exchange(bt->lchild); ∥交换左子树上所有结点的左右子树
exchange(bt->rchild); ∥交换右子树上所有结点的左右子树
}∥if }∥结束
【算法讨论】将上述算法中两个递归调用语句放在前面,将交换语句放在最后,则是以后序遍历方式交换所有结点的左右子树。中序遍历方式不适合本题。
6.41以二叉链表为存储结构,写出在二叉树中求值为x的结点在树中层次数的算法。
[题目分析] 按层次遍历,设一队列Q,用front和rear分别指向队头和队尾元素,last指向各层最右结点位置。
【算法6.41】
int Level_x(BiTree bt)∥求值为x的结点在树中层次数
{if(bt!=null)
{int front=0,last=1,rear=1,level=1;∥level记层次数
BiTree Q[];Q[1]=bt;∥根结点入队
while(front<=last)
{bt=Q[++front];
if(bt->data==x)
{printf(“%3d\n”,level); return level;}∥值为x的结点在树中层次数
if(bt->lchild!=null) Q[++rear]=bt->lchild;∥左子女入队列
if(bt->rchild!=null) Q[++rear]=bt->rchild;∥右子女入队列
if(front==last) {last=rear; level++; }∥本层最后一个结点已处理完
}
}
}∥算法结束
6.42已知深度为h的二叉树以一维数组作为存储结构。试编写算法求该二叉树中叶子的个数。
【题目分析】按完全二叉树形式顺序存储二叉树时,无元素的位置要当作“虚结点”。设虚结点取二叉树结点以外的值(这里设为0)。设结点序号为i,则当i<=(2h-1)/2时,若其2i和2i+1位置为虚结点,则i为叶子结点;当i>(2h-1)/2时,若i位置不是虚结点,则必为叶子结点。
【算法6.42】
int Leaves(int BT[],int n)
∥计算深度为h以一维数组BT作为存储结构的二叉树的叶子结点数,n为数组长度
{int num=0; ∥记叶子结点数
for(i=0;i<n;i++)
if(BT[i]!=0)
{if(i<=n/2)
{if(BT[2*i]==0 && 2*i+1<=n && BT[2*i+1]==0) num++;}
∥若结点无孩子,则是叶子
else if(BT[i]!=0) num++; ∥存储在数组后一半的元素是叶子结点
}
return num;
}∥结束Leaves
6.43已知二叉树以一维数组作为存储结构。试编写算法求下标为i和j的两个结点的最近共同祖先结点的值。
【题目分析】二叉树顺序存储,是按完全二叉树的格式存储,利用完全二叉树双亲结点与子女结点编号间的关系,求下标为i和j的两结点的双亲,双亲的双亲,等等,直至找到最近的公共祖先。
【算法6.43】
void Ancestor(ElemType bt[],int n,i,j,)
∥求顺序存储在bt[1..n]的二叉树中下标为i和j的两个结点的最近公共祖先结点
{if(i<1 || j<1) {printf(“参数错误\n”);exit(0);};
if(i==j)
{if(i==1) {printf(“所查结点为根结点,无祖先\n”);exit(0);};
else {printf (“结点的最近公共祖先是%d,值是%d”,i/2,A[i/2]);exit(0)}
}
while(i!=j)
if(i>j) i=i/2; ∥下标为i的结点的双亲结点的下标
else j=j/2; ∥下标为j的结点的双亲结点的下标
printf(“所查结点的最近公共祖先的下标是%d,值是%d”,i,A[i]);
∥设元素类型整型。
}∥ Ancestor
6.44已知一棵完全二叉树顺序存储于向量s[1..n]中,试编写算法由此顺序存储结构建立该二叉树的二叉链表。
【算法6.44】
BiTree Creat(ElemType A[],int i)
∥n个结点的完全二叉树存于一维数组A中,本算法建立二叉链表表示的完全二叉树
{BiTree tree;
if(i<=n)
{tree=(BiTree)malloc(sizeof(BiNode));
tree->data=A[i];
if(2*i>n) tree->lchild=null;
else tree->lchild=Creat(A,2*i);
if(2*i+1>n) tree->rchild=null;
else tree->rchild=Creat(A,2*i+1);
}
return (tree);
}∥Creat
【算法讨论】初始调用时,i=1。
6.45编写算法判别给定二叉树是否为完全二叉树。
【题目分析】判定是否是完全二叉树,可以使用队列,在遍历中利用完全二叉树“若某结点无左子女就不应有右子女”的原则进行判断。具体说,在层次遍历时,若碰到一个空指针后,在遍历结束前又碰到结点,则结论为该二叉树不是完全二叉树。
【算法6.45】
int JudgeComplete(BiTree bt)
∥判断二叉树是否是完全二叉树,如是,返回1,否则,返回0
{int tag=0; ∥出现空指针时,置tag=1
BiTree p=bt, Q[]; ∥ Q是队列,元素是二叉树结点指针,容量足够大
if(p==null) return (1);
QueueInit(Q); QueueIn(Q,p); ∥初始化队列,根结点指针入队
while(!QueueEmpty(Q))
{p=QueueOut(Q); ∥出队
if(p->lchild && !tag) QueueIn(Q,p->lchild);∥左子女入队
else if(p->lchild) return 0; ∥前边已有结点空,本结点不空
else tag=1; ∥首次出现结点为空
if(p->rchild && !tag) QueueIn(Q,p->rchild);∥右子女入队
else if(p->rchild) return 0;
else tag=1;
} ∥while
return 1; } ∥JudgeComplete
[算法讨论]完全二叉树证明还有其它方法。判断时易犯的错误是证明其左子树和右子树都是完全二叉树,由此推出整棵二叉树必是完全二叉树的错误结论。
6.46设树以双亲表示法存储,编写计算树的深度的算法。
【题目分析】以双亲表示法作树的存储结构,对每一结点,找其双亲,双亲的双亲,直至(根)结点,就可求出每一结点的层次,取其结点的最大层次就是树的深度。
【算法6.46】
int Depth(Ptree t)
∥求以双亲表示法为存储结构的树的深度
{int maxdepth=0;
for(i=1;i<=t.n;i++)
{temp=0; f=i;
while(f>0)
{temp++; f=t.nodes[f].parent; } ∥ 深度加1,并取新的双亲
if(temp>maxdepth) maxdepth=temp; ∥最大深度更新
}
return(maxdepth);∥返回树的深度
} ∥结束Depth
6.47已知在二叉树中,*root为根结点,*p和*q为二叉树中两个结点,试编写求距离它们最近的共同祖先的算法。
【题目分析】后序遍历最后访问根结点,即在递归算法中,根是压在栈底的。采用后序非递归遍历,栈中存放二叉树结点的指针,当访问到某结点时,栈中所有元素均为该结点的祖先。本题要找p和q 的最近共同祖先结点r ,不失一般性,设p在q的左边。后序遍历必然先遍历到结点p,栈中元素均为p的祖先。将栈拷入另一辅助栈中。再继续遍历到结点q时,将栈中元素从栈顶开始逐个到辅助栈中去匹配,第一个匹配(即相等)的元素就是结点p 和q的最近公共祖先。
【算法6.47】
先设二叉树的结点结构为:
typedef struct
{BiTree t;
int tag; ∥tag=0表示结点的左子女已访问,tag=1为右子女已访问
}stack;
stack s[],s1[];∥栈,容量足够大
BiTree Ancestor(BiTree ROOT,p,q,r)
∥求二叉树上结点p和q的最近的共同祖先结点r
{top=0; bt=ROOT;
while(bt!=null ||top>0)
{while(bt!=null && bt!=p && bt!=q) ∥结点入栈
{s[++top].t=bt;
s[top].tag=0;
bt=bt->lchild;
} ∥沿左分枝向下
if(bt==p)
∥不失一般性,假定p在q的左侧,遇结点p时,栈中元素均为p的祖先结点
for(i=1;i<=top;i++) {s1[i]=s[i]; top1=top;}
∥将栈s的元素转入辅助栈s1保存, top1记住栈顶
if(bt==q) ∥找到q 结点
for(i=top;i>0;i--) ∥将栈中元素的树结点到s1去匹配
{pp=s[i].t;
for(j=top1;j>0;j--)
if(s1[j].t==pp){printf(“共同的祖先已找到\n”);return (pp);}
}
while(top!=0 && s[top].tag==1) top--; ∥退栈
if(top!=0){s[top].tag=1;bt=s[top].t->rchild;}∥沿右分枝向下遍历
}∥结束while(bt!=null ||top>0)
return(null);∥q、p无公共祖先
}∥结束Ancestor
6.48以二叉链表作为存储结构,设计按层次遍历二叉树的算法。
【算法6.48】
void Level(BiTree bt) ∥层次遍历二叉树
{if(bt)
{QueueInit(Q); ∥Q是以二叉树结点指针为元素的队列
QueueIn(Q,bt);
while(!QueueEmpty(Q))
{p=QueueOut(Q); ∥出队
printf(p->data); ∥访问结点
if(p->lchild) QueueIn(Q,p->lchild); ∥非空左子女入队
if(p->rchild) QueueIn(Q,p->rchild); ∥非空右子女入队
}
}∥if(bt)
}
6.49编写算法查找二叉链表中数据域值为x的结点(假定各结点的数据域值各不相同),并打印出x所有祖先的数据域值。
【题目分析】后序遍历最后访问根结点,当访问到值为x的结点时,栈中所有元素均为该结点的祖先。
【算法6.49】
void Search(BiTree bt,ElemType x)
∥在二叉树bt中,查找值为x的结点,并打印其所有祖先
{typedef struct
{BiTree t;
int tag; ∥tag=0表示左子女被访问,tag=1右子女被访问
}stack;
stack s[]; ∥栈容量足够大
top=0;
while(bt!=null||top>0)
{while(bt!=null && bt->data!=x) ∥结点入栈
{s[++top].t=bt; s[top].tag=0; bt=bt->lchild;} ∥沿左分枝向下
if(bt->data==x)
{printf(“所查结点的所有祖先结点的值为:\n”);∥找到x
for(i=1;i<=top;i++)
printf(s[i].t->data); return;
} ∥输出祖先值后结束
while(top!=0 && s[top].tag==1) top--; ∥退栈(空遍历)
if(top!=0)
{s[top].tag=1;bt=s[top].t->rchild;} ∥沿右分枝向下遍历
}∥ while(bt!=null||top>0) }结束search
6.50设计这样的二叉树,用它可以表示父子、夫妻和兄弟三种关系,并编写一个查找任意父亲结点的所有儿子结点的过程。
【题目分析】用二叉树表示出父子,夫妻和兄弟三种关系,可以用根结点表示父(祖先),根结点的左子女表示妻,妻的右子女表示子。这种二叉树可以看成类似树的孩子兄弟链表表示法;根结点是父,根无右子女,左子女表示妻,妻的右子女(右子女的右子女等)均可看成兄弟(即父的所有儿子),兄弟结点又成为新的父,其左子女是兄弟(父的儿子)妻,妻的右子女(右子女的右子女等)又为儿子的儿子等等。首先递归查找某父亲结点,若查找成功,则其左子女是妻,妻的右子女及右子女的右子女等均为父亲的儿子。
【算法6.50】
BiTree Search(BiTree t,ElemType father)
∥在二叉树上查找值为father的结点
{int tag=0;
if(t==null) p=null; ∥二叉树上无father结点
else if(t->data==father)
{tag=1; p=t;}∥查找成功
else{p=Search(t->lchild,father); if(tag==0)p=Search(t->rchild,father);}
return p;
}∥结束Search
void PrintSons(BiTree t,ElemType x) ∥在二叉树上查找结点值为x的所有的儿子
{p=Serach(t,x); ∥在二叉树t上查找父结点x
if(p && p->lchild) ∥存在父结点,且有妻
{q=p->lchild; q=q->rchild; ∥先指向其妻结点,再找到第一个儿子
while(q!=null)
{printf(q->data); q=q->rchild;}∥输出父的所有儿子
}
}∥结束PrintSons
6.51 编写递归算法判定两棵二叉树是否相等。
【题目分析】首先判断二叉树的根是否相等,如是,再判断其左、右子树是否相等。
【算法6.51】
int BTEqual(BiTree t, BiTree x)
{∥判定二叉树t和二叉树x是否相等
if(!t && !x) return true;
if(t && x && t->data==x->data && BTEqual(t->lchild,x->lchild) && BTEqual(t->rchild,x->rchild) return ture;
else return false;
}
6.52已知一棵高度为K具有n个结点的二叉树,按顺序方式存储,编写将树中最大序号叶子结点的祖先结点全部打印输出的算法。
【题目分析】二叉树中最大序号的叶子结点,是在顺序存储方式下编号最大的结点
【算法6.52】
void Ancesstor(ElemType bt[])
∥打印最大序号叶子结点的全部祖先
{c=m; ∥m=2K-1
while(bt[c]==0) c--;∥找最大序号叶子结点,该结点存储时在最后
f=c/2; ∥c的双亲结点f
while(f!=0) ∥从结点c的双亲结点直到根结点,路径上所有结点均为祖先结点
{printf(bt[f]); f=f/2; }∥逆序输出,最老的祖先最后输出
}∥结束
6.53设二叉树以二叉链表作为存储结构,编写算法对二叉树进行非递归的中序遍历。
【算法6.53】
void InOrder(BiTree bt)
∥对二叉树进行非递归中序遍历
{BiTree s[],p=bt; ∥s是元素为二叉树结点指针的栈,容量足够大
int top=0;
while(p || top>0)
{while(p) {s[++top]=p; bt=p->lchild;} ∥沿左子树向下
if(top>0)
{p=s[top--]; printf(p->data); p=p->rchild;} ∥退栈,访问,转右子树
}
}∥结束
【算法讨论】若将访问语句printf(p->data),放到语句s[++top]=p的前面,则是前序遍历的非递归算法。
6.54设T是一棵满二叉树,写一个把T的后序遍历序列转换为先序遍历序列的递归算法。
【题目分析】对一般二叉树,仅根据一个先序(或中序、或后序)遍历,不能确定另一个遍历序列。但由于满二叉树“任一结点的左右子树均含有数量相等的结点”,根据此性质,可将任一遍历序列转为另一遍历序列。
【算法6.54】
void PostToPre(ElemType post[],pre[],int l1,h1,l2,h2)
∥将满二叉树的后序序列转为先序序列,l1,h1,l2,h2是序列初始和最后结点的下标。
{if(h1>=l1)
{pre[l2]=post[h1]; ∥根结点
visit(pre[l2]); ∥访问根结点
half=(h1-l1)/2; ∥左或右子树的结点数
PostToPre(post,pre,l1,l1+half-1,l2+1,l2+half)
∥将左子树后序序列转为先序序列
PostToPre(post,pre,l1+half,h1-1,l2+half+1,h2)
∥将右子树后序序列转为先序序列
}
}∥ PostToPre t
6.55若二叉树BT的每个结点,其左、右子树都为空,或者其左、右子树都不空,这种二叉树有时称为“严格二叉树”。由“严格二叉树”的前序序列和后序序列可以唯一确定该二叉树。写出根据这种二叉树的前序序列和后序序列确定该二叉树的递归算法。
【问题分析】前序序列的第一个结点是二叉树的根,若该结点后再无其它结点,则该结点是叶子;否则,该结点必有左、右子树,且根结点后的第一个结点就是左子树的根。到后序序列中查找这个左子树的根,它将后序序列分成左、右两部分:左部分(包括所查到的结点)是二叉树的左子树(可能为空),右部分(除去最后的根结点)则是右子树(可能为空)。这样,在确定根结点后,可递归确定左、右子树。
【算法6.55】
void creat(BiTree *BT,char pre[n],char post[n],int l1,int h1,int l2,int h2)
∥由严格二叉树的前序序列pre(l1:h1)和后序序列post(l2:h2)建立二叉树
{ BiTree p=*BT;
if(l1<=h1)
{p=(BiNode *)malloc(sizeof(BiNode));
p->data=pre[l1];∥前序序列的第一个元素是根
if(l1==h1) {p->lchild=p->rchild=null; }∥叶子结点
else ∥分支结点
{for(int i=l2;i<=h2;i++) ∥到后序序列中查左子树的根
if(post[i]==pre[l1+1] break;
L=i-l2+1; ∥左子树结点数
creat(&(p->lchild), pre,post,l1+1,l1+L,l2,i);
creat(&(p->rchild), pre,post,l1+L+1,h1,i+1,h2-1);
}∥else
}∥if
}∥结束
6.56编写一个递归算法,利用叶结点中空的右链指针域rchild,将所有叶结点自左至右链接成一个单链表,算法返回最左叶结点的地址(链头)。
【题目分析】叶子结点只有在遍历中才能知道,这里使用中序递归遍历。设置前驱结点指针pre,初始为空。第一个叶子结点由指针head指向,遍历到叶子结点时,就将它前驱的rchild指针指向它,最后叶子结点的rchild为空。
【算法6.56】
LinkedList head,pre=null; ∥全局变量
LinkedList InOrder(BiTree bt)
∥中序遍历二叉树bt,将叶子结点从左到右链成一个单链表,表头指针为head
{if(bt){InOrder(bt->lchild); ∥中序遍历左子树
if(bt->lchild==null && bt->rchild==null)∥叶子结点
if(pre==null) {head=bt; pre=bt;} ∥处理第一个叶子结点
else{pre->rchild=bt; pre=bt; } ∥将叶子结点链入链表
InOrder(bt->rchild); ∥中序遍历右子树
}
pre->rchild=null; ∥设置链表尾
return(head);
} ∥InOrder
6.57已知有一棵二叉链表表示的二叉树,编写算法,输出从根结点到叶子结点的最长一枝上的所有结点。
【题目分析】后序遍历时栈中保留当前结点的祖先的信息,用一变量保存栈的最高栈顶指针,每当退栈时,栈顶指针大于已保存的最高栈顶指针的值时,则将该栈倒入辅助栈中,辅助栈始终保存最长路径长度上的结点,直至后序遍历完毕,则辅助栈中内容即为所求。
【算法6.57】
void LongestPath(BiTree bt)
∥求二叉树从根结点到叶子结点的最长一枝上的所有结点
{BiTree p=bt,l[],s[];
∥l, s是栈,元素是二叉树结点指针,l中保留当前最长路径中的结点
int i,top=0,tag[],longest=0;
while(p || top>0)
{while(p){s[++top]=p;tag[top]=0; p=p->lchild;}∥沿左分枝向下
if(tag[top]==1) ∥当前结点的右分枝已遍历
{if(!s[top]->lchild && !s[top]->rchild)
∥只有到叶子结点时,才查看路径长度
if(top>longest)
{for(i=1;i<=top;i++) l[i]=s[i];
longest=top;
top--;
}
}∥保留当前最长路径到l栈,记住最高栈顶指针,退栈
while(top>0 && tag[top]==1) top--; ∥退栈
if(top>0)
{tag[top]=1; p=s[top].rchild;} ∥沿右子分枝向下
}∥while(p!=null||top>0)
}∥结束LongestPath
6.58试编写一算法对二叉树进行前序线索化。
【题目分析】线索化是在遍历中完成的,因此,对于二叉树进行前序、中序、后序遍历,在“访问根结点”处进行加线索的改造,就可实现前序,中序和后序的线索化。
【算法6.58】
BiThrTree pre=null;∥设置前驱
void PreOrderThreat(BiThrTree BT)
∥对以线索链表为存储结构的二叉树BT进行前序线索化
{if(BT!=null)
{if(BT->lchild==null){BT->ltag=1; BT->lchild=pre;}∥设置左线索
if(pre!=null && pre->rtag==1) pre->rchild=BT; ∥设置前驱的右线索;
if(BT->rchild==null) BT->rtag=1; ∥为建立右链作准备
pre=BT; ∥前驱后移
if(BT->ltag==0) PreOrderThreat(BT->lchild); ∥左子树前序线索化
PreOrderThreat(BT->rchild); ∥右子树前序线索化
}∥if(BT!=null) }∥结束PreOrderThreat
6.59试编写一算法对二叉树进行中序线索化。
【算法6.59】
BiThrTree pre==null;
void InOrderThreat(BiThrTree T)∥对二叉树进行中序线索化
{if(T)
{InOrderThreat(T->lchild); ∥左子树中序线索化
if(T->lchild==null){T->ltag=1; T->lchild=pre;} ∥左线索为pre
if(pre!=null && pre->rtag==1) pre->rchild=T;} ∥给前驱加后继线索
if(T->rchild==null) T->rtag=1; ∥置右标记,为右线索作准备
pre=BT; ∥前驱指针后移
InOrderThreat(T->rchild); ∥右子树中序线索化
}∥if
}∥结束InOrderThreat
6.60设p指向前序线索二叉树t的某结点,编写算法求*p结点的后继结点。
【题目分析】在前序线索二叉树中,求*p结点的后继结点,若*p结点有左子女,则左子女是其后继结点;若*p结点无左子女而有右子女,则右子女是其后继;若*p结点无左、右子女,线索p->rchild指向其后继。
【算法6.60】
BiThrTree PreorderNext(BiThrTree p)
{if (p->ltag==0) 结点有左子女
return(p->lchild); 结点的左子女为其前序后继
else
return(p->rchild);p->rchild为其前序后继
} PreorderNext
6.61设p指向后序线索二叉树t的某结点,编写算法求*p结点的前驱结点。
【题目分析】在后序线索二叉树中,求*p结点的前驱结点,若*p结点有右子女,则右子女是其前驱结点;若*p结点无右子女而有左子女,则左子女是其前驱;若*p结点既无右子女又无左子女,线索p->lchild指向其前驱。
【算法6.61】
BiThrTree PostorderPre(BiThrTree p)
{ if (p->rtag==0) 结点有右子女
return(p->rchild); 结点的右子女为其后序前驱
else
return(p->lchild) ; p->lchild为其后序前驱
} PreorderPre
6.62设计算法求中序线索二叉树中指针P所指结点的前驱结点的指针。
【题目分析】中序线索二叉树中,指针P所指结点的前驱结点的特征是:若p->ltag=1,p->lchild指向其前驱,否则,P的左子树上按中序遍历的最后结点是其中序前驱。
【算法6.62】
BiThrTree InPre(BiThrTree T, BiThrTree p)
∥在中序线索树T中,查找给定结点p的中序前驱
{if(p->ltag==1)q=p->lchild; ∥若p的左标志为1,用其左指针指向前驱
else {q=p->lchild;
while(q->rtag==0)
q=q->rchild; ∥p的前驱为其左子树中最右下的结点
}
return (q);
}∥结束InPre
6.63设计算法求中序线索二叉树中指针P所指结点的后继结点的指针。
【题目分析】中序线索二叉树中,指针P所指结点的后继结点的特征是:若p->rtag=1,p->rchild指向其后继,否则,P的右子树上按中序遍历的第一个结点是其中序后继。
【算法6.63】
BiThrTree InSucc(BiThrTree T, BiThrTree p)
∥在中序线索二叉树T中,查找给定结点p的中序后继
{if(p->rtag==1)
q=p->rchild; ∥若p的右标志为1,用其右指针指向后继
else {q=p->rchild;
while(q->ltag==0)
q=q->lchild; ∥p的后继为其右子树中最左下的结点
}
return (q);
}∥结束InSucc
第 7 章 图
一、基础知识题
7.1设无向图的顶点个数为n,则该图最多有多少条边?
【解答】n(n-1)/2
7.2一个n个顶点的连通无向图,其边的个数至少为多少?
【解答】n-1
7.3要连通具有n个顶点的有向图,至少需要多少条弧?
【解答】n
7.4 n个顶点的完全有向图含有弧的数目是多少?
【解答】n(n-1)
7.5一个有n个顶点的无向图,最少有多少个连通分量,最多有多少个连通分量。
【解答】1, n
7.6图的BFS生成树的树高要小于等于同图DFS生成树的树高,对吗?
【解答】对
7.7无向图G=(V,E),其中:V={a,b,c,d,e,f},E={(a,b),(a,e),(a,c),(b,e),(c,f),(f,d),(e,d)},写出对该图从顶点a出发进行深度优先遍历可能得到的全部顶点序列。
【解答】abedfc, acfdeb, aebdfc, aedfcb
7.8 在图采用邻接表存储时,求最小生成树的 Prim 算法的时间复杂度是多少?
【解答】O(n+e)
7.9若一个具有n个顶点,e条边的无向图是一个森林,则该森林中必有多少棵树?
【解答】n-e
7.10 n个顶点的无向图的邻接矩阵至少有多少非零元素?
【解答】0
7.11证明:具有n个顶点和多于n-1条边的无向连通图G一定不是树。
【证明】具有n个顶点n-1条边的无向连通图是自由树,即没有确定根结点的树,每个结点均可当根。若边数多于n-1条,因一条边要连接两个结点,则必因加上这一条边而使两个结点多了一条通路,即形成回路。形成回路的连通图不再是树。
7.12证明对有向图顶点适当编号,使其邻接矩阵为下三角形且主对角线为全零的充要条件是该图是无环图。
【证明】该有向图顶点编号的规律是让弧尾顶点的编号大于弧头顶点的编号。由于不允许从某顶点发出并回到自身顶点的弧,所以邻接矩阵主对角元素均为0。先证明该命题的充分条件。由于弧尾顶点的编号均大于弧头顶点的编号,在邻接矩阵中,非零元素(A[i][j]=1)自然是落到下三角矩阵中;命题的必要条件是要使上三角为0,则不允许出现弧头顶点编号大于弧尾顶点编号的弧,否则,就必然存在环路。(对该类有向无环图顶点编号,应按顶点出度的大小进行顺序编号。)
7.13设G=(V,E)以邻接表存储,如图所示,试画出从顶点1出发所得到的深度优先和广度优先生成树。
习题7.13 的图
【解答】深度优先生成树
1 |
2 |
3 |
4 |
5 |
宽度优先生成树:
1 |
2 |
3 |
4 |
5 |
7.14 已知一个图的顶点集V和边集E分别为:
V={0,1,2,3,4,5,6,7};
E={<0,2>,<1,3>,<1,4>,<2,4>,<2,5>,<3,6>,<3,7>,<4,7>,<4,8>,<5,7>,<6,7>,<7,8>};
若存储它采用邻接表,并且每个顶点邻接表中的边结点都是按照顶点序号从小到大的次序链接的,则按教材中介绍的进行拓扑排序的算法,写出得到的拓扑序列。
【解答】1-3-6-0-2-5-4-7-8
7.15一带权无向图的邻接矩阵如下图 ,试画出它的一棵最小生成树。
习题7.15 的图
【解答】设顶点集合为{1,2,3,4,5,6},
由下边的逻辑图可以看出,在{1,2,3}和{4,5,6}回路中,
各任选两条边,加上(2,4),则可构成9棵不同的最小生成树。
1 |
2 |
1 |
1 |
1 |
1 |
1 |
3 |
1 |
2 |
3 |
4 |
5 |
6 |
7.16如图所示是一带权有向图的邻接表法存储表示。其中出边表中的每个结点均含有三个字段,依次为边的另一个顶点在顶点表中的序号、边上的权值和指向下一个边结点的指针。试求:
(1).该带权有向图的图形;
(2).从顶点V1为起点的广度优先遍历的顶点序列及对应的生成树;
(3).以顶点V1为起点的深度优先遍历生成树;
(4).由顶点V1到顶点V3的最短路径。
习题7.16 的图
33 |
36 |
25 |
18 |
10 |
29 |
38 |
30 |
42 |
1 |
4 |
3 |
2 |
6 |
5 |
【解答】(1)
(2)V1,V2,V4,V6,V3,V5
1 |
2 |
4 |
6 |
3 |
5 |
- 顶点集合V(G)={V1,V2,V3,V4,V5,V6}
边的集合 E(G)={<V1,V2>,<V2,V3>,<V1,V4>,<V4,V5>,<V5,V6>}
- V1到V3最短路径为67: (V1—V4—V3)
迭代 | 集合 S
| 选择 顶点 | D[ ] |
D[2] D[3] D[4] D[5] D[6] | |||
初值 | { v1} |
| 33 ∞ 29 ∞ 25 |
1 | {v1, v6} | v6 | 33 ∞ 29 ∞ 25 |
2 | { v1, v6, v4} | v4 | 33 67 29 71 |
3 | { v1, v6, v4, v2} | v2 | 33 67 71 |
4 | {v1,v6, v4, v2,v3} | v3 | 67 71 |
5 | {v1,v6,v4, v2,v3,v5} | v | 71 |
7.17已知一有向网的邻接矩阵如下,如需在其中一个顶点建立娱乐中心,要求该顶点距其它各顶点的最长往返路程最短,相同条件下总的往返路程越短越好,问娱乐中心应选址何处?给出解题过程。
习题7.17 的图
【解答】下面用FLOYD算法求出任意两顶点的最短路径(如图A(6)所示)。题目要求娱乐中心“距其它各结点的最长往返路程最短”,结点V1,V3,V5和V6最长往返路径最短都是9。按着“相同条件下总的往返路径越短越好”,选顶点V5,总的往返路径是34。
A(0)= A(1)= A(2)= A(3)= A(4)= A(5)= A(6)=
7.18求出图中顶点1到其余各顶点的最短路径。
习题7.18的图 |
【解答】本表中DIST中各列最下方的数字是顶点1到顶点的最短通路。
所选顶点 | S(已确定最短路径 的顶点集合) | T(尚未确定最短 路径的顶点集合) | DIST | ||||||
[2] | [3] | [4] | [5] | [6] | [7] | [8] | |||
初态 | {1} | {2,3,4,5,6,7,8} | 30 | ¥ | 60 | 10 | ¥ | ¥ | ¥ |
5 | {1,5} | {2,3,4,6,7,8} | 25 | ¥ | 60 | 10 | 17 | ¥ | ¥ |
6 | {1,5,6} | {2,3,4,7,8 } | 20 | 33 | 60 |
| 17 | 25 | ¥ |
2 | {1,5,6,2} | {3,4,7,8} | 20 | 33 | 60 |
|
| 25 | ¥ |
7 | {1,5,6,2,7} | {3,4,8} |
| 31 | 28 |
|
| 25 | 35 |
4 | {1,5,6,2,7,4} | {3,8} |
| 31 | 28 |
|
|
| 35 |
3 | {1,5,6,2,7,4,3} | {5,8} |
| 31 |
|
|
|
| 35 |
8 | {1,5,6,2,7,4,3,8} | {8} |
|
|
|
|
|
| 35 |
顶点1到其它顶点的最短路径依次是20,31,28,10,17,25,35。按Dijkstra算法所选顶点依次是5,6,2,7,4,3,8。
7.19对图示的AOE网络,计算各活动弧的e(ai)和l(ai)的函数值,各事件(顶点)的ve(vi)和vl (vi)的函数值,列出各条关键路径。
【解答】
顶点 | α | A | B | C | D | E | F | G | H | W |
Ve(i) | 0 | 1 | 6 | 3 | 4 | 24 | 13 | 39 | 22 | 52 |
Vl(i) | 0 | 29 | 24 | 3 | 7 | 31 | 13 | 39 | 22 | 52 |
活动 | a1 | a2 | a3 | a4 | a5 | a6 | a7 | a8 | a9 | a10 | a11 | a12 | a13 | a14 | a15 | a16 | a17 |
e(i) | 0 | 0 | 0 | 0 | 1 | 6 | 6 | 3 | 3 | 4 | 24 | 13 | 13 | 13 | 39 | 22 | 22 |
l(i) | 28 | 18 | 0 | 3 | 29 | 24 | 31 | 34 | 3 | 7 | 31 | 20 | 36 | 13 | 39 | 22 | 40 |
a |
C |
F |
H |
G |
W |
,长52。 |
关键路径是:
活动与顶点的对照表:a1<α,A> a2<α,B> a3<α,C> a4<α,D> a5<A,E> a6<B,E> a7<B,W> a8<C,G>
a9<C,F> a10<D,F> a11<E,G> a12<F,E> a13<F,W> a14<F,H> a15<G,W> a16<H,G> a17<H,W>
-
- 利用弗洛伊德算法,写出如图所示相应的带权邻接矩阵的变化。
3 |
2 |
1 |
4 |
5 |
9 |
6 |
8 |
2 |
3 |
1 |
10 |
|
【解答】A0=A1=A2=A3= A4=
二、算法设计题
7.21 设无向图G有n个顶点,m条边。试编写用邻接表存储该图的算法。
【算法7.21】
void CreatGraph (AdjList g)∥建立有n个顶点和m 条边的无向图的邻接表存储结构
{int n,m;
scanf("%d%d",&n,&m);
for(i=0,i<n;i++) ∥输入顶点信息,建立顶点向量
{scanf(&g[i].vertex); g[i].firstarc=null;}
for(k=0;k<m;k++) ∥输入边信息
{scanf(&v1,&v2);∥输入两个顶点
i=GraphLocateVertex (g,v1); j=GraphLocateVertex (g,v2);∥顶点定位
p=(ArcNode *)malloc(sizeof(ArcNode));∥申请边结点
p->adjvex=j; p->next=g[i].firstarc; g[i].firstarc=p; ∥将边结点链入
p=(ArcNode *)malloc(sizeof(ArcNode));
p->adjvex=i; p->next=g[j].firstarc; g[j].frstarc=p;
}∥for
}∥算法CreatGraph结束
-
- 知有向图有n个顶点,请编写算法,根据用户输入的偶对建立该有向图的邻接表。
【算法7.22】
void CreatAdjList(AdjList g)∥建立有向图的邻接表存储结构
{int n;
scanf("%d",&n);
for(i=0;i<n;j++)
{scanf(&g[i].vertex); g[i].firstarc=null;}∥输入顶点信息,下标从0开始
scanf(&v1,.&v2);
while(v1 && v2)∥题目要求两顶点之一为0表示结束
{i=GraphLocateVertex(g,v1);
p=(ArcNode*)malloc(sizeof(ArcNode));
p->adjvex=j; p->next=g[i].firstarc; g[i].firstarc=p;
scanf(&v1,&v2);
}∥while
}
-
- 设有向图G有n个点(用1,2,…,n表示),e条边,写一算法根据G的邻接表生成G的反向邻接表,要求算法时间复杂性为O(n+e)。
【算法7.24】
void InvertAdjList(AdjList gin,gout)
∥将有向图的出度邻接表改为按入度建立的逆邻接表
{for(i=0;i<n;i++)∥设有向图有n个顶点,建逆邻接表的顶点向量
{gin[i].vertex=gout[i].vertex; gin[i].firstarc=null; }
for(i=0;i<n;i++) ∥邻接表转为逆邻接表。
{p=gout[i].firstarc;∥取指向邻接点的指针
while(p!=null)
{j=p->adjvex;
s=(ArcNode *)malloc(sizeof(ArcNode));∥申请结点空间
s->adjvex=i; s->next=gin[j].firstarc; gin[j].firstarc=s;
p=p->next;∥下一个邻接点。
}∥while
}∥for
}
-
- 写出从图的邻接表表示转换成邻接矩阵表示的算法。
【算法7.25】
void AdjListToAdjMatrix(AdjList gl, AdjMatrix gm)
∥将图的邻接表表示转换为邻接矩阵表示
{for(i=0;i<n;i++) ∥设图有n个顶点,邻接矩阵初始化
for(j=0;j<n;j++) gm[i][j]=0;
for(i=0;i<n;i++) ∥取第一个邻接点,填邻接矩阵元素值,并求下一个邻接点
{p=gl[i].firstarc;
while(p!=null)
{gm[i][p->adjvex]=1;p=p->next; }
}∥for
}∥算法结束
-
- 试写出把图的邻接矩阵表示转换为邻接表表示的算法。
【算法7.26】
void AdjMatrixToAdjList(AdjMatrix gm, AdjList gl)
∥将图的邻接矩阵表示转换为邻接表表示
{for(i=0;i<n;i++) ∥邻接表表头向量初始化。
{scanf(&gl[i].vertex); gl[i].firstarc=null;}
for(i=0;i<n;i++)
for(j=0;j<n;j++)
if(gm[i][j]==1)
{p=(ArcNode *)malloc(sizeof(ArcNode)) ;∥申请结点空间
p->adjvex=j;∥顶点I的邻接点是j
p->next=gl[i].firstarc;
gl[i].firstarc=p; ∥链入顶点i的邻接点链表中
}∥if
}∥end
-
- 试编写建立有n个顶点,m条边且以邻接多重表为存储结构表示的无向图的算法。
【算法7.27】
void CreatMGraph(AdjMulist g)
∥建立有n个顶点m条边的无向图的邻接多重表的存储结构
{int n,m;
scanf("%d%d",&n,&m);
for(i=0,i<n;i++) ∥建立顶点向量
{scanf(&g[i].vertex); g[i].firstedge=null;}
for(k=0;k<m;k++) ∥建立边结点
{scanf(&v1,&v2);
i=GraphLocateVertex(g,v1); j=GraphLocateVertex(g,v2);
p=(ENode *)malloc(sizeof(ENode));
p->ivex=i; p->jvex=j;
p->ilink=g[i].firstedge; p->jlink=g[j].firstedge;
g[i].firstedge=p; g[j].firstedge=p;
}∥for
}∥算法结束
7.28 已知某有向图(n个结点)的邻接表,求该图各结点的入度数。
【题目分析】在有向图的邻接表存储结构中求顶点的入度,需要遍历整个邻接表。
【算法7.28】
void Indegree(AdjList g)∥求以邻接表为存储结构的n个顶点有向图的各顶点入度
{for(i=0;i<n;j++)
{num=0;∥入度初始为0
for(j=0;j<n;j++) ∥遍历整个邻接表,求一个顶点的入度
if(i!=j)
{p=g[j].firstarc;
while(p)
{if(p->adjvex==i) num++; p=p->next; }
}
printf(“顶点%d的入度为:%d\n”,g[i].vexdata,num); ∥设顶点数据为整型
}
}
7.29 已知无向图G=(V,E),给出求图G的连通分量个数的算法。
【题目分析】使用图的遍历可以求出图的连通分量。进入dfs或bfs一次,就可以访问到图的一个连通分量的所有顶点。
【算法7.29】
void dfs (v)
{visited[v]=1; printf (“%3d”,v); ∥输出连通分量的顶点。
p=g[v].firstarc;
while(p!=null)
{if(visited[p->adjvex==0]) dfs(p->adjvex);
p=p->next;
}∥while
}∥ dfs
void Count()
∥求图中连通分量的个数
{int k=0 ; static AdjList g ;
for(i=0;i<n;i++ ) ∥设无向图g有n个结点
if(visited[i]==0) { printf ("\n第%d个连通分量:\n",++k); dfs(i);}∥if
}∥Count
【算法讨论】算法中visited[]数组是全程变量,每个连通分量的顶点集按遍历顺序输出。这里设顶点信息就是顶点编号,否则应取其g[i].vertex分量输出。
7.30 已知无向图采用邻接表存储方式,试写出删除边(i,j)的算法。
【算法7.30】
void DeletEdge(AdjList g,int i,j)
∥在用邻接表方式存储的无向图g中,删除边(i,j)
{p=g[i].firstarc; pre=null; ∥删顶点i 的边结点(i,j),pre是前驱指针
while(p)
if(p->adjvex==j)
{if(pre==null)g[i].firstarc=p->next;
else pre->next=p->next;
free(p); ∥释放空间
}
else {pre=p; p=p->next;} ∥沿链表继续查找
p=g[j].firstarc; pre=null; ∥删顶点j 的边结点(j,i)
while(p)
if(p->adjvex==i)
{if(pre==null)g[j].firstarc=p->next;
else pre->next=p->next;
free(p); ∥释放空间
}
else {pre=p; p=p->next;} ∥沿链表继续查找
}∥ DeletEdge
【算法讨论】 算法中假定给的i,j 均存在,否则应检查其合法性。若未给顶点编号,而给出顶点信息,则先用顶点定位函数求出其在邻接表顶点向量中的下标i和j。
7.31 假设有向图以邻接表存储,试编写算法删除弧<Vi,Vj>的算法。
【算法7.31】
void DeleteArc(AdjList g,vertype vi,vj)
∥删除以邻接表存储的有向图g的一条弧<vi,vj>,假定顶点vi和vj存在
{i=GraphLocateVertex(g,vi);
j=GraphLocateVertex(g,vj); ∥顶点定位
p=g[i].firstarc; pre=null;
while(p)
if(p->adjvex==j)
{if(pre==null) g[i].firstarc=p->next;
else pre->next=p->next;
free(p);
}∥释放结点空间
else {pre=p; p=p->next;}
}∥结束
-
- 设计一个算法利用遍历图的方法判别一个有向图G中是否存在从顶点Vi到Vj的长度为k的简单路径,假设有向图采用邻接表存储结构。
【题目分析】本题利用深度优先递归的搜索方法判断有向图G的顶点i到j是否存在长度为k的简单路径,先找到i的第一个邻接点m,再从m出发递归的求是否存在m到j的长度为k-1的简单路径。
【算法7.32】
int existpathlen(AlGraph G,int i,int j,int k)
{//判断邻接表方式存储的有向图G的顶点i到j是否存在长度为k的简单路径
if(i==j&&k==0) return 1; //找到了一条路径,且长度符合要求
else if(k>0)
{visited[i]=1;
for(p=G.vertices[i].firstarc;p;p=p->next)
{m=p->adjvex;
if(!visited[m])
if(existpathlen(G,m,j,k-1)) return 1; //剩余路径长度减一
}
visited[i]=0; //本题允许曾经被访问过的结点出现在另一条路径中
}
return 0; //没找到
}
-
- 设有向图G采用邻接矩阵存储,编写算法求出G中顶点i到顶点j的不含回路的、长度为k的路径数。
【算法7.32】
int GetPathNum(AdjMatrix GA,int i,int j,int k,int n)
{//求邻接矩阵方式存储的有向图G的顶点i到j之间长度为k的简单路径条数
//n为顶点个数
if(i==j&&k==0) return 1; //找到了一条路径,且长度符合要求
else if(k>0)
{sum=0; //sum表示通过本结点的路径数
visited[i]=1;
for(k=0;k<n;k++)
{if(GA[i][k]!=0 && !visited[k])
sum+=GetPathNum(GA,k,j,k-1,n) //剩余路径长度减一
}
visited[i]=0; //本题允许曾经被访问过的结点出现在另一条路径中
}
return sum;
}
-
- 设计算法求出以邻接表存储的有向图G中由顶点u到v的所有的简单路径。
【算法7.34】
void AllSPdfs(AdjList g,vertype u,vertype v)
∥求有向图g中顶点u到顶点v的所有简单路径
{ int top=0,s[];
s[++top]=u; visited[u]=1;
while(top>0 || p)
{p=g[s[top]].firstarc; ∥第一个邻接点
while(p!=null && visited[p->adjvex]==1)
p=p->next; ∥下一个访问邻接点表
if(p==null) top--; ∥退栈
else {i=p->adjvex; ∥取邻接点(编号)
if(i==v) ∥找到从u到v的一条简单路径,输出
{for(k=1;k<=top;k++)
printf( "%3d",s[k]);
printf( "%3d\n",v);
}∥if
else { visited[i]=1; s[++top]=i; } ∥else深度优先遍历
}∥else
}∥while
}∥ AllSPdfs
7.35 以邻接表作存储结构,编写拓扑排序的算法。
【算法7.35】
7.36 试写一算法,判断以邻接表方式存储的有向图中是否存在由顶点Vi到顶点Vj的路径(i<>j)。
【题目分析】从Vi深度优先遍历,若在未退出深度优先遍历时遍历到Vj,说明Vi间Vj存在路径
【算法7.36】
int visited[n]; ∥设有向图有n个顶点
int Pathitoj(AdjList g,int Vi,int Vj)
∥ 判断以邻接表方式存储的有向图中是否存在由顶点Vi到顶点Vj的路径
{if(Vi==Vj) return 1; ∥Vi到顶点Vj存在路径
else{visited[Vi]=1;
for(p=g[Vi].firstarc;p;p=p->next)
{k=p->adjvex;
if(!visited[k] && Pathitoj(g,k, Vj)) return 1;
}∥for
return 0; ∥∥Vi到顶点Vj不存在路径
}∥else
}∥结束
【算法讨论】若顶点vi和vj 不是编号,必须先用顶点定位函数,查出其在邻接表顶点向量中的下标。下面再对本题用非递归算法求解如下。
【算法7.36.1】
int Connectij (AdjList g , vertype Vi , Vj )
∥判断n个顶点以邻接表表示的有向图g中,顶点 Vi 各Vj 是否有路径,有则返回1,否则返回0
{for(i=1;i<n;i++) visited[i]=0;∥访问标记数组初始化
i=GraphLocateVertex(g, Vi); ∥顶点定位,不考虑 Vi或 Vj不在图中的情况
j=GraphLocateVertex(g, Vj);
int stack[],top=0;stack[++top]=i;
while(top>0)
{k=stack[top--]; p=g[k].firstarc;
while(p && visited[p->adjvex]==1)
p=p->next;∥查第k个链表中第一个未访问的弧结点
if(p==null) top--;
else {i=p->adjvex;
if(i==j) return(1); ∥顶点Vi和Vj 间有路径
else {visited[i]=1; stack[++top]=i;}
}∥else
}while
return(0); ∥顶点Vi和Vj间无通路
}
7.37
7.38已知n个顶点的有向图,用邻接矩阵表示,编写函数,计算每对顶点之间的最短路径。
本题用FLOYD算法直接求解如下:
【算法7.38】
void ShortPath_FLOYD(AdjMatrix g)
∥求具有n个顶点的有向图每对顶点间的最短路径
{AdjMatrix length; ∥length[i][j]存放顶点vi到vj的最短路径长度。
for(i=1;i<=n;i++)
for(j=1;j<=n;j++) length[i][j]=g[i][j]; ∥初始化。
for(k=1;k<=n;k++)
for(i=1;i<=n;i++)
for(j=1;j<=n;j++)
if(length[i][k]+length[k][j]<length[i][j])
length[i][j]=length[i][k]+length[k][j];
}∥算法结束
7.39设计算法求距离顶点V0的最短路径长度(以弧数为单位)为K的所有顶点,要求尽可能地节省时间。
【题目分析】 本题应用宽度优先遍历求解。若以v0作生成树的根为第1层,则距顶点v0最短路径长度为K的顶点均在第K+1层。可用队列存放顶点,将遍历访问顶点的操作改为入队操作。队列中设头尾指针f和r,用level表示层数。
【算法7.39】
void bfs_K(graph g,int v0,K)
∥输出无向连通图g中距顶点v0最短路径长度为K的顶点
{int Q[]; ∥Q为顶点队列,容量足够大
int f=0,r=0,t=0; ∥f和r分别为队头和队尾指针,t指向当前层最后顶点
int level=0,flag=0;∥层数和访问成功标记
visited[v0]=1; ∥设v0为根
Q[++r]=v0; t=r; level=1; ∥v0入队
while(f<r && level<=K+1)
{v=Q[++f];
w=GraphFirstAdj(g,v);
while(w!=0) ∥w!=0 表示邻接点存在
{if(visited[w]==0)
{Q[++r]=w; visited[w]=1;∥邻接点入队列
if(level==K+1){ printf("距顶点v0最短路径为k的顶点%d ",w); flag=1;}
}∥if
w=GraphNextAdj(g ,v ,w);
}∥while(w!=0)
if(f==t) {level++;t=r; }
∥当前层处理完,修改层数,t指向下一层最后一个顶点
}∥while(f<r && level<=K+1)
if(flag==0) printf( "图中无距v0顶点最短路径为%d的顶点。\n",K);
}∥算法结束
[算法讨论]本题亦可采取另一个算法。由于在生成树中结点的层数等于其双亲层次数加1,故可设顶点和层次数2个队列,其入队和出队操作同步,其核心语句段如下:
QueueInit(Q1) ; QueueInit(Q2); ∥Q1和Q2是顶点和顶点所在层次数的队列
visited[v0]=1; ∥访问数组初始化,置v0被访问标记
level=1; flag=0; ∥是否有层次为K的顶点的标志
QueueIn(Q1,v0); QueueIn(Q2,level); ∥顶点和层数入队列
while(!empty(Q1) && level<=K+1)
{v=QueueOut(Q1); level=QueueOut(Q2);∥顶点和层数出队
w=GraphFirstAdj(g,v0);
while(w!=0) ∥邻接点存在
{if(visited[w]==0)
if(level==K+1)
{printf("距离顶点v0最短路径长度为K的顶点是%d\n",w);
visited[w]=1; flag=1; QueueIn(Q1 ,w); QueueIn(Q2,level+1); }
w=GraphNextAdj(g ,v ,w);
}∥while(w!=0)
}∥while(!empty(Q1) && level<K+1)
if(flag==0) printf( "图中无距v0顶点最短路径为%d的顶点。\n",K);
7.40 设有n(n>0)个顶点的无向连通图G, 可以邻接矩阵Anxn存储,由于邻接矩阵的对称性,只将其下三角顺序存储在数组S中。请编写对以数组S存储的图G进行宽度优先遍历的算法。
【题目分析】 由宽度优先遍历的定义,首先访问任一顶点,然后访问该顶点的未曾访问的邻接点,如此下去,直至全部顶点访问完成。在邻接矩阵中,第i行非零元素都是第i个顶点的邻接点,而在压缩存储下,找某顶点的邻接点要遍历整个数组。矩阵元素的下标i和j和其在一维数组S中的序号k的关系:
由
得 和j=k-i(i+1)/2
在一维数组中,只有非零元素才是顶点。为简单计,当访问完一个顶点后,就将其在一维数组中的位序置零;由于是图的遍历,当全部顶点访问完后就直接结束算法。
【算法7.40】
#define n 用户图的顶点数
#define m n(n+1)/2
int nodes=0, visited[n]={0}; ∥顶点计数和访问标志数组
void index(int k,*i,*j)
∥由非零元素在一维数组S中的序号k计算其在邻接矩阵中的下标i和j
{*i= é(-3+sqrt(9+8*k))/2ù; *j=k-(*i)*(*i+1)/2
}
void Tri_bfs(int v)
∥对下三角存储的无向连通图作宽度优先遍历,v是遍历的开始顶点
{nodes++; QueueInit(Q); QueueIn(Q,v);
printf(v); visited[v]=1; ∥初始化
while(!QueueEmpty(Q) && nodes<=n)
{v=QueueOut(Q);
for(k=0; k<m; k++)
if(s[k]!=0)
{index(k,&i,&j); ∥求非零元素在邻接矩阵中的下标
if(i==v || j==v) ∥顶点i或j是顶点v
if(i==v) ∥顶点i是顶点v
{if(visited[j]==0) ∥顶点j是顶点v的邻接点,且尚未访问
{nodes++; printf(j); visited[j]=1; s[k]=0; QueueIn(Q,j);}
}
else ∥顶点j是顶点v
{if(visited[i]==0) ∥顶点i是顶点v的邻接点,且尚未访问
{nodes++; printf(i); visited[i]=1; s[k]=0; QueueIn(Q,i);}
}∥
}∥index(k,&i,&j)
}∥∥while(!QueueEmpty(Q) && nodes<=n)
}∥算法结束
[算法讨论]对于连通图,进入BFS一次就可访问完图的全部顶点;对于非连通图,进入BFS一次就可访问完图的一个连通分量。若要遍历全部顶点,在调用BFS的函数中加入语句:
for(vi=0; vi<n;vi++)
if(visited[vi]==0) Tri_bfs(vi);
第8章 动态存储结构
-
- 在伙伴系统中的伙伴是指任意两块大小相同、位置相邻的内存块。这种说法对吗?
【解答】不对。只有同一内存块分裂的两块才互称伙伴。
8.2最佳适配法与最先适配法相比,前者容易增加闲置空间的碎片。这种说法对吗?
【解答】对。
8.3设内存中可利用空间已连成一个单链表,对用户的存储空间需求,一般有哪三种分配策略?
【解答】
首次拟合法;从链表头指针开始查找,找到第一个大于等于所需空间的结点即分配。
最佳拟合法:链表结点大小增序排列,找到第一个大于等于所需空间的结点即分配。
最差拟合法:链表结点大小逆序排列,总从第一个结点开始分配,将分配后结点所剩空间插入到链表适当位置。
首次拟合法适合事先不知道请求分配和释放信息的情况,分配时需查询,释放时插在表头。 最佳拟合法适用于请求分配内存大小范围较宽的系统,释放时容易产生存储量很小难以利用的内存碎片,同时保留那些很大的内存块以备将来可能发生的大内存量的需求,分配与回收均需查询。 最差拟合法适合请求分配内存大小范围较窄的系统,分配时不查询,回收时查询,以便插入适当位置。
8.4计算起始二进制地址为011011110000,长度为4(十进制)的块的伙伴地址是多少?
【解答】011011110100
8.5地址为(1664)10大小为(128)10的存储块的伙伴地址是什么?
地址为(2816)10大小为(64)10的存储块的伙伴地址是什么?
【解答】
(1)buddy(1664,7)=1664-128=1536
(2)buddy(2816,6)=2816+64=2880
8.6 试叙述动态存储分配伙伴系统的基本思想,它和边界标识法不同点是什么?
【解答】动态存储分配伙伴系统的基本思想是:在伙伴系统中,无论占用块或空闲块,其大小均为2的k(k为≥0的正整数)次幂。若内存容量为2m,则空闲块大小只能是20,21,22,…,2m。由同一大块分裂而得的两个小块互称“伙伴空间”,如内存大小为210的块分裂成两个大小为29的块。只有两个“伙伴空间”才能合并成一个大空间。
起始地址为p,大小为2k的内存块,其伙伴的起始地址为:
边界标识法在每块的首尾均有“占用”/“空闲”标志,空闲块合并方便。伙伴系统算法简单,速度快,但只有互为伙伴的两个空闲块才可合并,因而易产生虽空闲但不能归并的碎片。
8.7已知一个大小为512个字长的存储,假设先后有6个用户申请大小分别为23,45,52,100,11和19的存储空间,然后再顺序释放大小为45,52,11的占用块。假设以伙伴系统实现动态存储管理。
(1) 画出可利用空间表的初始状态。
(2) 画出为6个用户分配所需要的存储空间后可利用空间表的状态以及每个用户所得到的存储块的起始地址。
(3) 画出在回收3个占用块之后可利用空间表的状态。
【解答】因为512=29,可利用空间表的初始状态图如8-1所示。
当用户申请大小为23的内存块时,因24<23<=25,但没有大小为25的块,只有大小为29的块,故将29的块分裂成两个大小为28的块,其中大小为28的一块挂到可利用空间表上,另一块再分裂成两个大小为27的块。又将其中大小为27的一块挂到可利用空间表上,另一块再分裂成两个大小为26的块,一块26的块挂到可利用空间表上,另一块分裂成两个大小为25的块,其中一块挂到可利用空间表上,另一块分给用户(地址0—31)。如此下去,最后每个用户得到的存储空间的起始地址如图8-2, 6个用户分配所需要的存储空间后可利用空间表的状态如图8-3。
在回收时,因为给申请45的用户分配了26,其伙伴地址是0,在占用中,不能合并,只能挂到可利用空间表上。在回收大小为52的占用块时,其伙伴地址是192,也在占用。回收大小为11的占用块时,其伙伴地址是48,可以合并为大小25的块, 挂到可利用空间表上。回收3个占用块之后可利用空间表的状态如图8-4。
存储大小 | 起始地址 |
23 | 0 |
45 | 64 |
52 | 128 |
100 | 256 |
11 | 32 |
19 | 192 |
图8-2 图8-1
(注:在图8.3和图8.4画上了占用块,从原理上,只有空闲块才出现在“可利用空间表”中。)
图8-3 图8-4
8.9下图所示的伙伴系统中,回收两块首地址分别为768及128,大小为27的存储块,请画出回收后该伙伴系统的状态图。
【解答】因为768 % 27+1=0,所以768和768+27=896互为伙伴, 伙伴合并后,首址为768,块大小为28。因为768 % 28+1=28,所以,所以首址768大小为28的块和首址512大小为28的块合并,成为首址512大小为29的空闲块。因为128 % 27+1=27,其伙伴地址为128-27=0, 将其插入可利用空间表中。回收后该伙伴系统的状态图如下。
第9章 集合
一、基础知识题
9.1 若对长度均为n的有序的顺序表和无序的顺序表分别进行顺序查找,试在下列三种情况下分别讨论二者在等概率情况下平均查找长度是否相同?
(1)查找不成功,即表中没有和关键字K相等的记录;
(2)查找成功,且表中只有一个和关键字K相等的记录;
(3)查找成功,且表中有多个和关键字K相等的记录,要求计算有多少个和关键字K相等的记录。
【解答】
(1)平均查找长度不相同。在有序的顺序表中查找时,在n+1(小于任何一个和大于第n)个位置均可能失败;查找无序的顺序表时,查找失败都是在第n+1个位置,其平均查找长度是n+1。
(2)平均查找长度相同。在n个位置上均可能成功。
(3)平均查找长度不相同。前者在某个位置上(1<=i<=n)查找成功时,和关键字K相等的记录是连续的,而后者要查找完顺序表的全部记录。
9.2 在查找和排序算法中,监视哨的作用是什么?
【解答】监视哨的作用是免去查找过程中每次都要检测整个表是否查找完毕,提高了查找效率。
9.3 用分块查找法,有2000项的表分成多少块最理想?每块的理想长度是多少?若每块长度为25 ,平均查找长度是多少?
【解答】分成45块,每块的理想长度为45(最后一块长20)。若每块长25,则平均查找长度为ASL=(80+1)/2+(25+1)/2=53.5(顺序查找确定块),或ASL=19(折半查找确定块,块内确定元素)。
-
- 用不同的输入顺序输入n个关键字,可能构造出的二叉排序树具有多少种不同形态?
【解答】
-
- 证明若二叉排序树中的一个结点存在两个孩子,则它的中序后继结点没有左孩子,中序前驱结点没有右孩子。
【证明】根据中序遍历的定义,该结点的中序后继是其右子树上按中序遍历的第一个结点,即右子树上值最小的结点,即叶子结点或仅有右子树的结点,它们都没有左孩子;而其中序前驱是其左子树上按中序遍历的最后个结点,即左子树上值最大的结点,即叶子结点或仅有左子树的结点,没有右孩子。命题得证。
-
- 对于一个高度为h的AVL树,其最少结点数是多少?反之,对于一个有n个结点的AVL树, 其最大高度是多少? 最小高度是多少?
- 【解答】设以Nh表示深度为h的AVL树中含有的最少结点数。显然,N0=0,N1 =1,N2 =2,且Nh = Nh-1 + Nh-2 +1(h≥2)。这个关系与斐波那契序列类似,用归纳法可以证明:当h≥0时,Nh = Fh+2 -1,而Fh约等于Φh/(其中Φ=(1+)/2),则Nh约等于Φh+2/-1(即高度为h的AVL树具有的最少结点数),这也就是有n个结点的AVL树的最大高度。有n个结点的AVL树的 最小高度是。
-
- 试推导含有12个结点的平衡二叉树的最大深度,并画出一棵这样的树。
【解答】深度为n的AVL树中的最少结点数为
所以12=,有=13,求得n+2=7(Fibonacci数列第一项的值假设为1,对应于二叉树表示有一个结点的二叉树的深度为1),所以n=5。
可表示为如下图所示的AVL树:
|
|
|
|
|
-
- 假定有n个关键字,它们具有相同的哈希函数值,用线性探测方法把这n个关键字存入到哈希表中要做多少次探测?
【解答】n个关键字都是同义词,因此,用线性探测法将第一个关键字存入时不会发生冲突,对其余关键字存入时都会发生冲突,所以探测的次数应为次。
-
- 建立一棵具有13个结点的判定树,并求其成功和不成功的平均查找长度值各为多少。
【解答】
3 |
10 |
1 |
5 |
12 |
4 |
2 |
6 |
8 |
11 |
9 |
13 |
7 |
查找成功时的平均查找长度为:
查找不成功时的平均查找长度为:
-
- 二叉排序树中关键字互不相同,则其中关键字最小值结点无左孩子,关键字最大值结点无右孩子,此命题是否正确?最小值结点和最大值结点一定是叶子吗?一个新结点总是插在二叉排序树的某叶子上吗?
【解答】对二叉排序树进行中序遍历可以得到结点的有序序列,中序遍历的第一个结点是最小值结点,中序遍历的最后一个结点是最大值结点。题目给出二叉排序树中关键字互不相同,则其中最小值结点必无左孩子,最大值结点必无右孩子,此命题是正确的。
最小值结点和最大值结点不一定是叶子结点。一个新结点不一定总是插在二叉排序树的某叶子上,但插入后一定是叶子结点。
-
- 回答问题并填空
(1)散列表存储的基本思想是什么?
(2)散列表存储中解决碰撞的基本方法有哪些?其基本思想是什么?
(3)用线性探查法解决碰撞时,如何处理被删除的结点?为什么?
【解答】
(1)散列表存储的基本思想是用关键字的值决定数据元素的存储地址。
(2)散列表存储中解决碰撞的基本方法:
① 开放定址法
形成地址序列的公式是:Hi=(H(key)+di)% m,其中m是表长,di是增量。根据di取法不同,又分为三种:
a.di =1,2,…,m-1 称为线性探测再散列,其特点是逐个探测表空间,只要散列表中有空闲空间,就可解决碰撞,缺点是容易造成“聚集”,即不是同义词的关键字争夺同一散列地址。
b.di =12,-12,22,-22,… ,±k2(k≤m/2) 称为二次探测再散列,它减少了聚集,但不容易探测到全部表空间,只有当表长为形如4j+3(j为整数)的素数时才有可能。
c.di =伪随机数序列,称为随机探测再散列。
② 再散列法 Hi=RHi(key) i=1,2,…,k,是不同的散列函数,即在同义词产生碰撞时,用另一散列函数计算散列地址,直到解决碰撞。该方法不易产生“聚集”,但增加了计算时间。
③ 链地址法 将关键字为同义词的记录存储在同一链表中,散列表地址区间用H[0..m-1]表示,数组元素初始值为空指针。凡散列地址为i(0≤i≤m-1)的记录均插在以H[i]为头指针的链表中。这种解决方法中数据元素个数不受表长限制,插入和删除操作方便,但增加了指针的空间开销。这种散列表常称为开散列表,因为数据元素个数不受表长限制。而①中的散列表称闭散列表,含义是元素个数受表长限制。
④ 建立公共溢出区 设H[0..m-1]为基本表,凡关键字为同义词的记录,都填入溢出区O[0..m-1]。
(3) 用线性探查法解决碰撞时,要在被删除结点的散列地址处作标记,不能物理的删除。否则,中断了查找通路。
-
- 如何衡量哈希函数的优劣?简要叙述哈希表技术中的冲突概念,并指出三种解决冲突的方法。
【解答】评价哈希函数优劣的因素有:能否将关键字均匀影射到哈希空间上,有无好的解决冲突的方法,计算哈希函数是否简单高效。由于哈希函数是压缩映像,冲突难以避免。
解决冲突的方法见上面9.12题。
-
- 设有一组关键字{9,01,23,14,55,20,84,27},采用哈希函数:H(key)=key % 7 ,表长为10,用开放地址法的二次探测再散列方法Hi=(H(key)+di) % 10(di=12,22,32,…,)解决冲突。要求:对该关键字序列构造哈希表,并计算查找成功的平均查找长度。
【解答】
散列地址 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
关键字 | 14 | 01 | 9 | 23 | 84 | 27 | 55 | 20 |
|
|
比较次数 | 1 | 1 | 1 | 2 | 3 | 4 | 1 | 2 |
|
|
平均查找长度:ASLsucc=(1+1+1+2+3+4+1+2)/8=15/8
以关键字27为例:H(27)=27%7=6(冲突) H1=(6+1)%10=7(冲突)
H2=(6+22)%10=0(冲突) H3=(6+33)%10=5 所以比较了4次。
-
- 对下面的关键字集合{30,15,21,40,25,26,36,37},若查找表的装填因子为0.8,采用线性探测再散列方法解决冲突。要求:
(1)设计哈希函数;
(2)画出哈希表;
(3)计算查找成功和查找失败的平均查找长度。
【解答】由于装填因子为0.8,关键字有8个,所以表长为8/0.8=10。
(1)用除留余数法,哈希函数为H(key)=key % 7
(2)
散列地址 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
关键字 | 21 | 15 | 30 | 36 | 25 | 40 | 26 | 37 |
|
|
比较次数 | 1 | 1 | 1 | 3 | 1 | 1 | 2 | 6 |
|
|
(3)计算查找失败时的平均查找长度,必须计算不在表中的关键字,当其哈希地址为i(0≤i≤m-1)时的查找次数。本例中m=10。故查找失败和查找成功时的平均查找长度分别为:
ASLunsucc=(9+8+7+6+5+4+3+2+1+1)/10=4.6 ASLsucc =16/8=2
-
- 设哈希函数H(k)=3K % 11,散列地址空间为0~10,对关键字序列(32,13,49,24,38,21,4,12)按下述两种解决冲突的方法构造哈希表:
(1)线性探测再散列
(2)链地址法,
并分别求出等概率下查找成功时和查找失败时的平均查找长度。
【解答】.(1)
散列地址 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
关键字 | 13 | 22 |
| 53 | 1 |
| 41 | 67 | 46 |
| 51 |
| 30 |
比较次数 | 1 | 1 |
| 1 | 2 |
| 1 | 2 | 1 |
| 1 |
| 1 |
(2)装填因子=9/13=0.7
(3)ASLsucc=11/9
(4)ASLunsucc=29/13
-
- 已知长度为l2的表{Jan,Feb,Mar,Apr,May,June,July,Aug,Sep,Oct,Nov,Dec}
(1) 试按表中元素的次序依次插入一棵初始为空的二叉排序树,并求在等概率情况下查找成功的平均查找长度。
(2) 用表中元素构造一棵最佳二叉排序树,求在等概率的情况下查找成功的平均查找长度。
(3) 按表中元素顺序构造一棵AVL树,并求其在等概率情况下查找成功的平均查找长度。
【解答】
(1)ASLsuss =(1*1+2*2+3*3+4*3+5*2+6*1)/12=42/12
(2)ASLsuss =(1*1+2*2+4*3+5*4)/12=37/12
(3)ASLsuss =(1*1+2*2+4*3+4*4+5*1)/12=38/12
-
- 假定对有序表:(3,4,5,7,24,30,42,54,63,72,87,95)进行折半查找,试回答下列问题:
(1) 画出描述折半查找过程的判定树;
(2) 若查找元素54,需依次与哪些元素比较?
(3) 若查找元素90,需依次与哪些元素比较?
(4) 假定每个元素的查找概率相等,求查找成功时的平均查找长度。
【解答】
-
- 直接在二叉排序树中查找关键字K与在中序遍历输出的有序序列中查找关键字K,其效率是否相同?输入关键字有序序列来构造一棵二叉排序树,然后对此树进行查找,其效率如何?为什么?
【解答】在二叉排序树上查找关键字K,走了一条从根结点至多到叶子的路径,时间复杂度是O(logn),而在中序遍历输出的序列中查找关键字K,时间复杂度是O(n)。按序输入建立的二叉排序树,蜕变为单枝树,其平均查找长度是(n+1)/2,时间复杂度也是O(n)。
-
- 设二叉排序树中关键字由1到1000的整数组成,现要查找关键字为363的结点,下述关键字序列哪一个不可能是在二叉排序树中查到的序列?说明原因。
(1)51,250,501,390,320,340,382,363
(2)24,877,125,342,501,623,421,363
【解答】序列(2)不可能是二叉排序树中查到363的序列。查到501后,因363<501,后面应出现小于501的数,但序列中出现了623,故不可能。
-
- 用关键字1,2,3,4的四个结点(1)能构造出几种不同的二叉排序树?其中(2)最优查找树有几种?(3)AVL树有几种?(4)完全二叉树有几种?试画出这些二叉排序树。
【解答】(1)本题的本质是给定中序序列1、2、3、4,有几种不同的二叉排序树,即该中序序列相当多少不同的前序序列,这是树的计数问题。设中序序列中元素数为n,则二叉数的数目为1/(n+1)C2nn,这里n=4,故有14种。图示如下:
(2)最优查找树有4种,图中⑽ ⑾ ⑿ ⒀
(3)AVL树也有4种,图中⑽ ⑾ ⑿ ⒀
(4)完全二叉树有1种,图中⑽
-
- 简要叙述B-树与B+树的区别?
【解答】m阶的B+树和B-树主要区别有三:
(1)有n棵子树的结点中含有n(B-树中n-1)个关键字;
(2)B+树叶子结点包含了全部关键字信息,及指向含关键字记录的指针,且叶子结点本身依关键字大小自小到大顺序链接;
(3)B+树的非终端结点可以看成是索引部分,结点中只含其子树(根结点)中最大(或最小)关键字。B+树的查找既可以顺序查找,也可以随机查找,B-树只能随机查找。
-
- 包括n个关键字的m阶B-树在一次检索中最多涉及多少个结点?(要求写出推导过程。)
【解答】本题等价于“含有n个关键字的m阶B-树的最大高度是多少”?一次检索中最多走一条从根到叶子的路径,由于根结点至少有两棵子树,其余每个(除叶子)结点至少有ém/2ù棵子树,则第三层至少有ém/2ù*2个结点,第l+1层至少有2*ém/2ùl-1个结点。设B-树深度为l+1,即第l+1层是叶子结点,叶子结点数是n+1(下面推导),故有n+1≥2*ém/2ùl-1,即l≤logém/2ù()+1。
【注】 推导B-树中叶子结点数s与关键字数n的关系式:s=n+1
设B-树某结点的子树数为Ci,则该结点的关键字数Ni=Ci-1。对于有k个结点的B-树,有
∑Ni=∑(Ci-1)=∑Ci-k(1≤i≤k) ……(1)
因为B树上的关键字数,即∑Ni=n (1≤i≤k) ……(2)
而B-树上的子树数可这样计算:每个结点(除根结点)都是一棵子树,设叶子(子树)数为s;则
∑Ci=(k-1)+s (1≤i≤k) ……(3)
综合(1)(2)(3)式,有s=n+1。证毕。
-
- 设有一棵空的3阶B树,依次插入关键字30,20,10,40,80,58,47,50,29,22,56,98,99,请画出该树。
【解答】
-
- 含9个叶子结点的3阶B-树中至少有多少个非叶子结点?含10个叶子结点的3阶B-树中至多有多少个非叶子结点?
【解答】含9个叶子结点的3阶B-树至少有4个非叶子结点,当每个非叶子结点均含3棵子树,第三层是叶子结点时就是这种情况。当4层3阶B-树有10个叶子结点时,非叶子结点达到最大值8个:其中第一层1个,第二层2个,第三层5个。
-
- 选择题:对顺序表进行二分查找时,要求顺序表必须:
A.以顺序方式存储 B.以顺序方式存储,且数据元素有序
C.以链接方式存储 D.以链接方式存储,且数据元素有序
【解答】B
-
- 选择题:下列二叉排序树中查找效率最高的是:
A.平衡二叉树 B.二叉查找树
C.没有左子树的二叉排序树 D.没有右子树的二叉排序树
【解答】A
二、算法设计题
-
- 元素集合已存入整型数组A[1..n]中,试写出依次取A中各值A[i](1<=i<=n)构造一棵二叉排序树T的非递归算法。
【题目分析】本题以非递归形式建立二叉排序树
【算法9.28】
void CSBT(BSTree T,ElemType A[],int n)
∥以存储在数组K中的n个关键字,建立一棵初始为空的二叉排序树
{for(i=1;i≤n;i++)
{p=T;f=null;∥初始调用CSBT时,T=null
while(p!=null)
if(p->data<A[i])
{f=p;p=p->rchild; } ∥ f是p的双亲
else if(p->data>A[i]){f=p;p=p->lchild;}
s=(BSTree)malloc(sizeof (BiNode)); ∥ 申请结点空间
s->data=A[i];s->lchild=null;s->rchild=null;
if(f==null)T=s; ∥根结点
else if(s->data<f->data)f->lchild=s;∥左子女
else f->rchild=s;∥右子树根结点的值大于根结点的值
}∥算法结束
-
- 编写删除二叉排序树中值是X的结点的算法。要求删除结点后仍然是二叉排序树,并且高度没有增长。
【题目分析】在二叉排序树上删除结点,首先要查找该结点。查找成功后,若该结点无左子树,则可直接将其右子树的根结点接到其双亲结点上;若该结点有左子树,则用其左子树中按中序遍历的最后一个结点代替该结点,从而不增加树的高度。
【算法9.29】
void Delete(BSTree bst, keytype X)
∥在二叉排序树bst上,删除其关键字为X的结点。
{BSTree f,p=bst;
while(p && p->key!=X) ∥查找值为X的结点
if(p->key>X) {f=p; p=p->lchild;}
else {f=p; p=p->rchild;}
if(p==null) {printf(“无关键字为X的结点\n”); exit(0);}
if {p->lchild==null} ∥被删结点无左子树
if(f->lchild==p) f->lchild=p->rchild;∥将被删结点的右子树接到其双亲上
else f->rchild=p->rchild;
else {q=p; s=p->lchild; ∥被删结点有左子树
while(s->rchild !=null) ∥查左子树中最右下的结点(中序最后结点)
{q=s; s=s->rchild;}
p->key=s->key; ∥结点值用其左子树最右下的结点的值代替
if(q==p)
p->lchild=s->lchild;∥被删结点左子树的根结点无右子女
else q->rchild=s->lchild; ∥s是被删结点左子树中序序列最后一个结点
free(s);
}∥else
}∥算法结束
-
- 假设二叉排序树的各个元素值均不相同,设计一个递归算法按递减次序打印各元素的值。
【题目分析】按着“右子树-根结点-左子树”遍历二叉排序树,并输出结点的值。
【算法9.30】
void InOrder(BSTree bt)∥按递减次序输出二叉排序树结点的值
{BiTree s[],p=bt; ∥s是元素为二叉树结点指针的栈,容量足够大
int top=0;
while(p || top>0)
{while(p)
{s[++top]=p; bt=p->rchild;} ∥沿右子树向下
if(top>0)
{p=s[top--]; printf(p->data); p=p->lchild;}
}
}∥结束
以下是递归输出,算法思想是一样的。
void InvertOrder(BSTree bt)∥按递减次序输出二叉排序树结点的值
{BiTree p=bt;
if(p)
{InOrder(bt->rchild); ∥中序遍历右子树
printf(p->data); ∥访问根结点
InOrder(bt->lchild); ∥中序遍历左子树
}∥if
}∥结束
-
- 设记录R1,R2,…,Rn按关键字值从小到大顺序存储在数组r[1..n]中,在r[n+1]处设立一个监视哨,其关键字值为+∞; 试写一查找给定关键字k 的算法;并画出此查找过程的判定树,求出在等概率情况下查找成功时的平均查找长度。
【算法9.31】
int Search(rectype r[],int n,keytype k)
∥在n个关键字从小到大排列的顺序表中,查找关键字为k的结点
{r[n+1].key=k; ∥在高端设置监视哨
i=1;
while(r[i].key<k) i++;
return i%(n+1);
}∥算法search结束
查找过程的判定树是单枝树,限于篇幅不再画出。本题中虽然表按关键字有序,但进行顺序查找,查找成功的平均查找长度亦为(n+1)/2。
-
- 在二叉排序树中查找值为X的结点,若找到,则记数(count)加1;否则,作为一个新结点插入树中,插入后仍为二叉排序树,写出其递归和非递归算法(要求给出结点的定义)。
【算法9.32】
typedef struct node
{ElemType data;
int count;
struct node *llink,*rlink;
}BiTNode,*BSTree;
void Search_InsertX(BSTree t,ElemType X)
∥在二叉排序树t中查找值为X的结点,若查到,则其结点的count域值增1
∥否则,将其插入到二叉排序树中
{p=t;
while(p!=null && p->data!=X) ∥查找值为X的结点,f指向当前结点的双亲
{f=p;
if(p->data<X) p=p->rlink; else p=p->llink;
}
if(!p) ∥ 无值为x的结点,插入之
{p=(BiTNode *)malloc(sizeof (BiTNode));
p->data=X;p->llink=null;p->rlink=null;p->count=0;
if(t==null) t=p; ∥若初始为空树,则插入结点为根结点
else if(f->data>X)
f->llink=p;
else f->rlink=p;
}
else p->count++;∥ 查询成功,值域为X的结点的count增1
}∥ Search_InsertX
-
- 假设一棵平衡二叉树的每个结点都标明了平衡因子b,试设计一个算法,求平衡二叉树的高度。
【题目分析】 因为二叉树各结点已标明了平衡因子b,故从根结点开始记树的层次。根结点的层次为1,每下一层,层次加1,直到层数最大的叶子结点,这就是平衡二叉树的高度。当结点的平衡因子b为0时,任选左、右一分枝向下查找,若b不为0,则沿左(当b=1时)或右(当b=-1时)子树向下查找。
【算法9.33】
int Height(AVLTree t)∥ 求平衡二叉树t的高度
{level=0;p=t;
while(p)
{level++; ∥ 树的高度增1
if(p->bf<0)p=p->rchild;∥bf=-1 沿右分枝向下
else p=p->lchild; ∥bf>=0 沿左分枝向下
}∥while
return (level);∥平衡二叉树的高度
} ∥算法结束
-
- 设二叉排序树的存储结构为:
typedef struct node
{ElemType key;int size;struct node *lchild, *rchild,*parents;}node,*BiTree;
一个结点*x的size域的值是以该结点为根的子树中结点的总数(包括*x本身)。例如,下图中x所指结点的size值为4。设树高为h,试写一时间为O(h)的算法Rank(BiTree T,node *x),返回x所指结点在二叉排序树T的中序序列里的排序序号,即:求x^结点是根为T的二叉排序树中第几个最小元素。例如,下图x所指结点是树T中第11个最小元素。
7/2 |
22/1 |
14/1 |
12/1 |
21/4 |
14/7 |
19/2 |
16/2 |
20/1 |
10/4 |
17/12 |
9/1 |
T |
x |
key |
size |
【题目分析】 因为T是二叉排序树,则可利用二叉排序树的性质,从根往下查找结点*x。若T的左子树为空,则其中序序号为1,否则为T->lchild->size+1。设T的中序序号为r,其左子女p的中序序号和右子女q的中序序号分别为r-p->rchild->size-1和r+q->lchild->size+1。
【算法9.34】
int Rank(tree T,node *x)
∥ 在二叉排序树T上求结点x的中序序号
{if(T->lchild) r=T->lchild->size+1;∥根结点有左子树时的中序序号
else r=1; ∥根结点无左子树时中序序号为1
while(T)
if(T->key>x->key) ∥到左子树去查找
{T=T->lchild;
if(T)
{if(T->rchild) r=r-T->rchild->size-1;else r=r-1}
}
else if(T->key<x->key)∥到右子树去查找
{T=T->rchild;
if(T)
{if(T->lchild)r=r+T->lchild->size+1;else r=r+1;}
}
else return (r);∥返回*x结点的中序序号
return (0); ∥T树上无x结点。
}∥结束算法Rank
算法2:本题的另一种解法是设r是以*x为根的中序序号。初始时,若x的左子树为空,r=1;否则,r=x->lchild->size+1。利用结点的双亲域,上溯至根结点,即可求得*x的中序序号。
int Rank(tree T,node *x)∥在二叉排序树T上,求结点*x的中序序号
{if(x->lchild)r=x->lchild->size+1;else r=1;∥*x的这个序号是暂时的
p=x; ∥p要上溯至根结点T,求出*x的中序序号
while(p!=T)
{if(p==p->parents->rchild) ∥p是其双亲的右子女,
{if(p->parents->lchild==null) r++; ∥p结点的双亲排在p结点的前面
else r=r+p->parent->lchild->size+1;∥双亲及左子树均排在p前面
}
p=p->parents;
}∥while
return (r);
}∥Rank
-
- 已知某哈希表HT的装填因子小于1,哈希函数H(key)为关键字的第一个字母在字母表中的序号。
- 处理冲突的方法为线性探测再散列。编写按第一个字母的顺序输出哈希表中所有关键字的算法。
- 处理冲突的方法为链地址法。编写一个计算在等概率情况下查找不成功的平均查找长度的算法。
【题目分析】 本题未直接给出哈希表表长,但已给出装填因子小于1,且哈希函数H(k)为关键字第一个字母在字母表中的序号,字母‘A’的序号为1,表长可设为n(n≥27),而链地址法中,表长26。查找不成功是指碰到空指针为止(另一种观点是空指针不计算比较次数)。
【算法9.35】
(1)void Print(rectype h[])∥按关键字第一个字母在字母表中的顺序输出各关键字
{int i,j;
for(i=1;i≤26;i++) ∥ 哈希地址1到26
{j=1;printf(“\n”);
while(h[j]!=null) ∥ 设哈希表初始值为null
{if(ord(h[j])==i)∥ ord()取关键字第一字母在字母表中的序号
printf(“%s”,h[j]);
j=(j+1)% n;
}∥while
}∥for
}∥end
(2)int ASLHash(rectype h[])∥链地址解决冲突的哈希表查找不成功时平均查找长度
{int i,j;count=0; ∥记查找不成功的总的次数
LinkedList p;
for(i=1;i≤26;i++)
{p=h[i];j=1;∥按我们约定,查找不成功指到空指针为止。
while(p!=null){j++;p=p->next;}
count+=j;
}
return (count/26.0);
}
【注】值得指出,对用拉链法求查找失败时的平均查找长度有两种观点。其一,认为比较到空指针算失败。例如,若本题哈希地址h[i]( 1≤i≤26)为空指针,则认为比较1次失败;若哈希地址h[i]( 1≤i≤26)为非空指针,例如,h[i](1≤i≤26)链表中只有一个结点,则认为比较2次后失败,我们持这种观点。还有另一种观点:他们认为只有和关键字比较才计算比较次数,而和空指针比较不计算比较次数。照这种观点,上面两种情况失败时的比较次数分别为0和1。
-
- 有一个100*100的稀疏矩阵,其中1%的元素为非零元素,现要求用哈希表作存储结构。
(1)请设计一个哈希表
(2)请写一个对你所设计的哈希表中给定行值和列值存取矩阵元素的算法;并对算法所需时间和用一维数组(每个分量存放一个非零元素的行值、列值和元素值)作存储结构时存取元素的算法进行比较。
【题目分析】非零元素个数是100,负载因子取0.8,表长125左右,取表长p为127,散列地址为0到126。哈希函数用H(k)=(3*i+2*j) % 127,i,j为行值和列值。
【算法9.36】
#define m 127
typedef struct {int i,j;ElemType v;}triple;
void CreatHT(triple H[m])∥生成稀疏矩阵的哈希表,表中元素值初始化为0
{for(k=0;k<100;k++)
{scanf(&i,&j,&val);∥设元素值为整型
h=(3*i+2*j)% m; ∥计算哈希地址
while(HT[h].v!=0)) h=(h+1) % m; ∥线性探测哈希地址
HT[h].i=i;HT[h].j=j;HT[h].v=val;∥非零元素存入哈希表
}∥for }∥算法CreatHT结束
ElemType Search(triple HT[m],int i,int j)
∥在哈希表中查找下标为i,j的非零元素,查找成功返回非零元素,否则返回零值
{int h=(3*i+2*j) % m;
while((HT[h].i!=i || HT[h].j!=j) && HT[h].v!=0) h=(h+1)% m;
return (HT[h].v);
}∥Search
第10章 排序
-
- 基础知识题
-
- 基本概念:内排序,外排序,稳定排序,不稳定排序,顺串,败者树,最佳归并树。
【解答】
⑴内排序和外排序 若整个排序过程不需要访问外存便能完成,则称此类排序问题为内部排序(简称内排序);反之,若参加排序的记录数量很大,整个序列的排序过程不可能在内存中完成,则称此类排序问题为外部排序(简称外排序)。内部排序适用于记录个数不多的文件,不需要访问外存,而外部排序适用于记录很多的大文件,整个排序过程需要在内外存之间多次交换数据才能得到排序的结果。
⑵稳定排序和不稳定排序 假设待排序记录中有关键字Ki=Kj(i≠j),且在排序前的序列中Ri领先于Rj。经过排序后,Ri与Rj的相对次序保持不变(即Ri仍领先于Rj),则称这种排序方法是稳定的,否则称之为不稳定的。
⑶顺串 外部排序通常经过两个独立的阶段完成。第一阶段,根据内存大小,每次把文件中一部分记录读入内存,用有效的内部排序方法(如快速排序、堆排序等)将其排成有序段,这有序段又称顺串或归并段。
⑷败者树 败者树是为提高外部排序的效率而采用的,是由参加比赛的n个元素作叶子结点而得到的完全二叉树。每个非叶(双亲)结点中存放的是两个子结点中的败者数据,而让胜者去参加更高一级的比赛。另外,还需增加一个结点,即结点0,存放比赛的全局获胜者。
⑸最佳归并树 在外部排序的多路平衡归并的k叉树中,为了提高效率减少对外存的读写次数,按哈夫曼树构造的k叉树称最佳归并树。这棵树中只有度为0和度为k的结点。若用m表示归并段个数,用nk表示度为k的个数,若(m-1)%(k-1)=0,则不需增加虚段,否则应附加k-(m-1)%(k-1)-1个虚段(即第一个k路归并使用(m-1)%(k-1)+1个归并段)。
-
- 设待排序的关键字序列为(15, 21, 6, 30, 23, 6′, 20, 17), 试分别写出使用以下排序方法每趟排序后的结果。并说明做了多少次比较。
(1) 直接插入排序 (2) 希尔排序(增量为5,2,1) (3) 起泡排序
(4) 快速排序 (5) 直接选择排序 (6) 锦标赛排序
(7) 堆排序 (8) 二路归并排序 (9) 基数排序
【解答】
(1) 直接插入排序
初始关键字序列: 15,21,6,30,23,6′,20,17
第一趟直接插入排序:【15,21】
第二趟直接插入排序:【6,15,21】
第三趟直接插入排序:【6,15,21,30】
第四趟直接插入排序:【6,15,21,23,30】
第五趟直接插入排序:【6,6′,15,21,23,30】
第六趟直接插入排序:【6,6′,15,20,21,23,30】
第七趟直接插入排序:【6,6′,15,17,20,21,23,30】
(2) 希尔排序(增量为5,2,1)
初始关键字序列: 15,21,6,30,23,6′,20,17
第一趟希尔排序: 6′,20,6,30,23,15,21,17
第二趟希尔排序: 6′,15,6,17,21,20,23,30
第三趟希尔排序: 6′,6,15,17,20,21,23,30
(3) 起泡排序
初始关键字序列:15,21,6,30,23,6′,20,17
第一趟起泡排序:15,6,21,23,6′,20,17,30
第二趟起泡排序:6,15,21,6′,20,17,23,30
第三趟起泡排序:6,15,6′,20,17,21,23,30
第四趟起泡排序:6,6′,15,17,20,21,30,23
第五趟起泡排序:6,6′,15,17,20,21,30,23
(4) 快速排序
初始关键字序列: 15,21,6,30,23,6′,20,17
第一趟快速排序: 【6′,6】15【30,23,21,20,17】
第二趟快速排序: 6′,6, 15【17,23,21,20】30
第三趟快速排序: 6′,6, 15,17【23,21,20】30
第四趟快速排序: 6′,6, 15,17,【20,21】23,30
第五趟快速排序: 6,6′,15,17,20,21,30,23
(5) 直接选择排序
初始关键字序列: 15,21,6,30,23,6′,20,17
第一趟直接选择排序: 6,21,15,30,23,6′,20,17
第二趟直接选择排序: 6,6′,15,30,23,21,20,17
第三趟直接选择排序: 6,6′,15,30,23,21,20,17
第四趟直接选择排序: 6,6′,15,17,23,21,20,30
第五趟直接选择排序: 6,6′,15,17,20,21,23,30
第六趟直接选择排序: 6,6′,15,17,20,21,23,30
第七趟直接选择排序: 6,6′,15,17,20,21,23,30
(6) 锦标赛排序
初始关键字序列: 15,21,6,30,23,6′,20,17
6 |
6 |
6’ |
15 |
6 |
6’ |
17 |
15 |
21 |
6 |
30 |
23 |
6’ |
20 |
17 |
6’ |
15 |
6’ |
15 |
30
|
6’ |
17 |
15 |
21 |
∞
|
30 |
23 |
6’ |
20 |
17 |
15 |
15 |
23 |
15 |
30
|
23 |
17 |
15 |
21 |
∞
|
30 |
23 |
∞
|
20 |
17 |
锦标赛排序的基本思想是:首先对n个待排序记录的关键字进行两两比较,从中选出én/2ù个较小者再两两比较,直到选出关键字最小的记录为止,此为一趟排序。我们将一趟选出的关键字最小的记录称为“冠军”,而“亚军”是从与“冠军”比较失败的记录中找出,具体做法为:输出“冠军”后,将(冠军)叶子结点关键字改为最大,继续进行锦标赛排序,直到选出关键字次小的记录为止,如此循环直到输出全部有序序列。上面给出了排在前三个的记录,详细过程略。
(7) 堆排序
初始关键字序列:15,21,6,30,23,6′,20,17
初始堆: 6,17,6’,21,23,15,20,30
第一次调堆: 6’,17,15, 21,23,30,20,【6】
第二次调堆: 15,17,20,21,23,30,【6’,6】
第三次调堆: 17,21,20,30,23,【15,6’,6】
第四次调堆: 20,21,23,30,【17,15,6’,6】
第五次调堆: 21,30,23,【20,17,15,6’,6】
第六次调堆: 23,30,【21,20,17,15,6’,6】
第七次调堆: 30,【23,21,20,17,15,6’,6】
堆排序结果调堆:【30,23,21,20,17,15,6’,6】
(8) 二路归并排序
初始关键字序列: 15,21,6,30,23,6′,20,17
二路归并排序结果:15,17,20,21,23,30,6,6’
final↑ ↑first
(9) 基数排序
初始关键字序列:p→15→21→6→30→23→6′→20→17
第一次分配得到:
B[0].f→30→20←B[0].e
B[1].f→21←B[1].e
B[3].f→23←B[3].e
B[5].f→15←B[5].e
B[6].f→6→6’←B[6].e
B[7].f→17←B[7].e
第一次收集得到:
p→30→20→21→23→15→6→6’→17
第二次分配得到
B[0].f→6→6’←B[0].e
B[1].f→15→17←B[1].e
B[2].f→20→21→23←B[5].e
B[3].f→30←B[3].e
第二次收集得到
p→6→6’→15→17→20→21→23→30
基数排序结果:6,6′,15,17,20,21,23,30
-
- 在各种排序方法中,哪些是稳定的?哪些是不稳定的?并为每一种不稳定的排序方法举出一个不稳定的实例。
【解答】见下表:
排序方法 | 平均时间 | 最坏情况 | 辅助空间 | 稳定性 | 不稳定排序举例 |
直接插入排序 | O(n2) | O(n2) | O(1) | 稳定 |
|
折半插入排序 | O(n2) | O(n2) | O(1) | 稳定 |
|
二路插入排序 | O(n2) | O(n2) | O(n) | 稳定 |
|
表插入排序 | O(n2) | O(n2) | O(1) | 稳定 |
|
起泡排序 | O(n2) | O(n2) | O(1) | 稳定 |
|
直接选择排序 | O(n2) | O(n2) | O(1) | 不稳定 | 2,2’,1 |
希尔排序 | O(n1.3) | O(n1.3) | O(1) | 不稳定 | 3,2,2’,1(d=2,d=1) |
快速排序 | O(nlog2n) | O(n2) | O(log2n) | 不稳定 | 2,2’,1 |
堆排序 | O(nlog2n) | O(nlog2n) | O(1) | 不稳定 | 2,1,1’(极大堆) |
2-路归并排序 | O(nlog2n) | O(nlog2n) | O(n) | 稳定 |
|
基数排序 | O(d*(rd+n)) | O(d*(rd+n)) | O (rd ) | 稳定 |
|
-
- 在执行某种排序算法的过程中出现了排序码朝着最终排序序列相反的方向移动,从而认为该排序算法是不稳定的,这种说法对吗?为什么?
【解答】这种说法不对。因为排序的不稳定性是指两个关键字值相同的元素的相对次序在排序前、后发生了变化,而题中叙述和排序中稳定性的定义无关,所以此说法不对。对4,3,2,1起泡排序就可否定本题结论。
-
- 在堆排序、快速排序和归并排序方法中:
(1)若只从存储空间考虑,则应首先选取哪种排序,其次选取哪种排序,最后选取哪种排序?
(2)若只从排序结果的稳定性考虑,则应选取哪种排序方法?
(3)若只从平均情况下排序最快考虑,则应选取哪种排序方法?
(4)若只从最坏情况下排序最快并且要节省内存考虑,则应选取哪种排序方法?
【解答】
(1)堆排序,快速排序,归并排序
(2)归并排序
(3)快速排序
(4)堆排序
-
- 设要求从大到小排序。问在什么情况下冒泡排序算法关键字交换的次数为最多。
【解答】对冒泡算法而言,初始序列为反序时交换次数最多。若要求从大到小排序,则表现为初始是上升序时关键字交换的次数为最多。
-
- 快速排序的最大递归深度是多少?最小递归深度是多少?
【解答】设待排序记录的个数为n,则快速排序的最小递归深度为ëlog2nû+1,最大递归深度n。
-
- 我们知道,对于n个元素组成的顺序表进行快速排序时,所需进行的比较次数与这n个元素的初始排序有关。问:
(1) 当n=7时,在最好情况下需进行多少次比较?请说明理由。
(2) 当n=7时,给出一个最好情况的初始排序的实例。
(3) 当n=7时,在最坏情况下需进行多少次比较?请说明理由。
(4) 当n=7时,给出一个最坏情况的初始排序的实例。
【解答】
(1) 在最好情况下,每次划分能得到两个长度相等的子文件。假设文件的长度n=2k-1,那么第一遍划分得到两个长度均为ën/2û的子文件,第二遍划分得到4个长度均为ën/4û的子文件,以此类推,总共进行k=log2(n+1)遍划分,各子文件的长度均为1,排序完毕。当n=7时,k=3,在最好情况下,第一遍需比较6次,第二遍分别对两个子文件(长度均为3,k=2)进行排序,各需2次,共10次即可。
(2) 在最好情况下快速排序的原始序列实例:4,1,3,2,6,5,7。
(3) 在最坏情况下,若每次用来划分的记录的关键字具有最大(或最小)值,那么只能得到左(或右)子文件,其长度比原长度少1。因此,若原文件中的记录按关键字递减次序排列,而要求排序后按递增次序排列时,快速排序的效率与冒泡排序相同,其时间复杂度为O(n2)。所以当n=7时,最坏情况下的比较次数为21次。
(4) 在最坏情况下快速排序的初始序列实例: 7,6,5,4,3,2,1,要求按递增排序。
-
- 判断下面的每个结点序列是否表示一个堆,如果不是堆,请把它调整成堆。
-
- 100,90,80,60,85,75,20,25,10,70,65,50
- 100,70,50,20,90,75,60,25,10,85,65,80
-
- 判断下面的每个结点序列是否表示一个堆,如果不是堆,请把它调整成堆。
【解答】
(1) 是堆
(2) 不是堆。 调成大堆: 100,90,80,25,85,75,60,20,10,70,65,50
-
- 在多关键字排序时,LSD和MSD两种方法的特点是什么?
【解答】
最高位优先(MSD)法:先对最高位关键字K0进行排序,将序列分成若干子序列,每个子序列中的记录都具有相同的K0值,然后,分别就每个子序列对关键字K1进行排序,按K1值不同再分成若干更小的子序列,……,依次重复,直至最后对最低位关键字排序完成,将所有子序列依次连接在一起,成为一个有序子序列。
最低位优先(LSD)法:先对最低位关键字Kd-1进行排序,然后对高一级关键字Kd-2进行排序,依次重复,直至对最高位关键字K0排序后便成为一个有序序列。进行排序时,不必分成子序列,对每个关键字都是整个序列参加排序,但对Ki (0<=i<d-1)排序时,只能用稳定的排序方法。另一方面,按LSD进行排序时,可以不通过关键字比较实现排序,而是通过若干次“分配”和“收集”来实现排序。
-
- 给出如下关键字序列321,156,57,46,28,7,331,33,34,63试按链式基数排序方法,列出一趟分配和收集的过程。
【解答】
按LSD法 →321→156→57→46→28→7→331→33→34→63
分配 [0] [1] [2] [3] [4] [5] [6] [7] [8] [9]
321 33 34 156 57 28
331 63 46 7
收集 →321→331→33→63→34→156→46→57→7→28
-
- 奇偶交换排序如下所述:对于初始序列A[1],A[2],…,A[n],第一趟对所有奇数i(1<=i<n),将A[i]和A[i+1]进行比较,若A[i]>A[i+1],则将两者交换;第二趟对所有偶数i(2<=i<n),将A[i]和A[i+1]进行比较,若A[i]>A[i+1],则将两者交换;第三趟对所有奇数i(1<=i<n);第四趟对所有偶数i(2<=i<n),…,依次类推直至到整个序列有序为止。
(1) 分析这种排序方法的结束条件。
(2) 写出用这种排序方法对35,70,33,65,24,21,33进行排序时,每一趟的结果。
【解答】
- 排序结束条件为,连续的第奇数趟排序和第偶数趟排序都没有交换。
第一趟奇数:35,70,33,65,21,24,33
第二趟偶数:35,33,70,21,65,24,33
第三趟奇数:33,35,21,70,24,65,33
第四趟偶数:33,21,35,24,70,33,65
第五趟奇数:21,33,24,35,33,70,65
第六趟偶数:21,24,33,33,35,65,70
第七趟奇数:21,24,33,33,35,65,70(无交换)
第八趟偶数:21,24,33,33,35,65,70(无交换) 结束
-
- 设某文件经内排序后得到100个初始归并段(初始顺串),若使用多路归并排序算法,并要求三趟归并完成排序,问归并路数最少为多少?
【解答】 设归并路数为k,归并趟数为s,则s=élogk100ù,因élogk100ù=3 ,且k为整数,故k=5,即最少5路归并可以完成排序。
-
- 证明:置换-选择排序法产生的初始归并段的长度至少为m。(m是所用缓冲区的长度)。
【证明】 由置换选择排序思想,第一个归并段中第一个元素是缓冲区中最小的元素,以后每选一个元素都不应小于前一个选出的元素,故当产生第一个归并段时(即初始归并段),缓冲区中m个元素中除最小元素之外,其他m-1个元素均大于第一个选出的元素,即使以后读入元素均小于输出元素时,初始归并段中也至少能有原有的m个元素。证毕。
-
- 设有11个长度(即包含记录的个数)不同的初始归并段,它们所包含的记录个数分别为25,40,16,38,77,64,53,88,9,48,98。试根据它们做4路平衡归并,要求:
(1)指出总的归并趟数;
(2)构造最佳归并树;
(3)根据最佳归并树计算每一趟及总的读记录数。
【解答】
因为(11-1)%(4-1)=1,所以加“虚段”,第一次由两个段合并。
25 |
9 |
16 |
40 |
48 |
53 |
25 |
77 |
128 |
242 |
64 |
38 |
88 |
556 |
98 |
(1)三趟归并
(2)最佳归并树如图
(3)设每次读写一个记录
第一趟50次读写
总的读写次数:2052
[(9+16)*3+(25+38+40)*2+
(48+53+64+77)*2+(88+98)]*2
-
- 对输入文件(101,51,19,61,3,71,31,17,19,100,55,20,9,30,50,6,90),当k=6时,使用置换-选择算法,写出建立的初始败者树及生成的初始归并段。
【解答】
初始败者树
初始归并段: R1:3,19,31,51,61,71,100,101
2 |
5 |
71 |
4 |
0 |
3 |
1 |
3 |
1 |
61 |
1 |
19 |
1 |
51 |
1 |
101 |
1 |
1 |
R2:9,17,19,20,30,50,55,90
R3:6
-
- 选择题:下面给出的四种排序方法中,排序过程中的比较次数与排序方法无关的是。
A.选择排序法 B. 插入排序法 C. 快速排序法 D. 堆排序法
【解答】A
-
- 选择题:一个排序算法的时间复杂度与以下哪项有关。
A.排序算法的稳定性 B. 所需比较关键字的次数
C. 所采用的存诸结构 D. 所需辅助存诸空间的大小
【解答】B
二、算法设计题
-
- 请编写一个算法,在基于单链表表示的关键字序列上进行简单选择排序。
【算法10.19】
void LinkedListSelectSort(pointer head);
∥本算法一趟找出一个关键字最小的结点,其数据和当前结点进行交换
{p=head->next;
while(p)
{q=p->next; r=p; ∥设r是指向关键字最小的结点的指针
while(q!=null)
{if(q->data<r->data) r=q;
q=q->next;
}
if (r!=p) r->data<-->p->data;
p=p->next; ∥选下一个最小元素
}
【算法讨论】本算法只交换两个结点的数据,若要交换结点,则须记下当前结点和最小结点的前驱指针
-
- 设单链表头结点指针为L,结点数据为整型。试写出对链表L按“直接插入方法”排序的算法。
【算法10.20】
void LinkInserSort(LinkedList L)
//本算法对单链表L按“直接插入方法”进行排序
{ p=L->next->next; //链表至少一个结点,p初始指向链表中第二结点(若存在)
L->next->next=null; //初始假定第一个记录有序
while(p!=null)
{q=p->next; //q指向p的后继结点}
s=L;
while(s->next!=null && s->next->data<p->data)
s=s->next; //向后找插入位置
p->next=s->next;
s->next=p; //插入结点
p=q; //恢复p指向当前结点
}
}
-
- 试设计一个双向冒泡排序算法,即在排序过程中交替改变扫描方向。
【算法10.21】
void BubbleSort2(int a[],int n) ∥相邻两趟向相反方向起泡的冒泡排序算法
{change=1;low=0;high=n-1; ∥冒泡的上下界
while(low<high && change)
{change=0; ∥设不发生交换
for(i=low;i<high;i++) ∥气泡上浮,大元素下沉(向右)
if(a[i]>a[i+1])
{a[i]<-->a[i+1];change=1;} ∥有交换,修改标志change
high--; ∥修改上界
for(i=high;i>low;i--) ∥气泡下沉,小元素上浮(向左)
if(a[i]<a[i-1]){a[i]<-->a[i-1];change=1;}
low++; ∥修改下界
}∥while }∥BubbleSort2
-
- 写出快速排序的非递归算法。
【算法10.22】
void QuickSort(rectype r[n+1]; int n)
{∥ 对r[1..n]进行快速排序的非递归算法
typedef struct{int low,high; }node
node s[n+1];∥栈,容量足够大
int quickpass(rectype r[],int,int); ∥ 函数声明
int top=1; s[top].low=1; s[top].high=n;
while(top>0)
{ss=s[top].low; tt=s[top].high; top--;
if(ss<tt)
{k=quickpass(r,ss,tt);
if(k-ss>1) {s[++top].low=ss; s[top].high=k-1;}
if(tt-k>1) {s[++top].low=k+1; s[top].high=tt;}
}
} ∥ 算法结束
int quickpass(rectype r[];int s,t)
{i=s; j=t; rp=r[i]; x=r[i].key;
while (i<j)
{while(i<j && x<=r[j].key) j--;
if(i<j) r[i++]=r[j];
while(i<j && x>=r[j].key) i++;
if(i<j) r[j--]=r[i];;
}
r[i]=rp;
return (i);
} ∥ 一次划分算法结束
[算法讨论]可对以上算法进行两点改进:一是在一次划分后,先处理较短部分,较长的子序列进栈;二是用“三者取中法”改善快速排序在最坏情况下的性能。下面是部分语句片段:
int top=1; s[top].low=1; s[top].high=n;
ss=s[top].low; tt=s[top].high; top--; flag=true;
while(flag || top>0)
{k=quickpass(r,ss,tt);
if(k-ss>tt-k) ∥ 一趟排序后分割成左右两部分
{if(k-ss>1) ∥ 左部子序列长度大于右部,左部进栈
{s[++top].low=ss; s[top].high=k-1; }
if(tt-k>1) ss=k+1; ∥ 右部短的直接处理
else flag=false; ∥ 右部处理完,需退栈
}
else if(tt-k>1) ∥右部子序列长度大于左部,右部进栈
{s[++top].low=k+1; s[top].high=tt; }
if(k-ss>1) tt=k-1 ∥ 左部短的直接处理
else flag=false ∥ 左部处理完,需退栈
}
if(!flag && top>0) ∥退栈
{ss=s[top].low; tt=s[top].high; top--; flag=true;}
} ∥ end of while(flag || top>0)
} ∥ 算法结束
int quickpass(rectype r[];int s,t)
∥ 用“三者取中法”进行快速排序的一次划分
{ int i=s, j=t, mid=(s+t)/2;
rectype tmp;
if(r[i].key>r[mid].key) {tmp=r[i];r[i]=r[mid];r[mid]=tmp }
if(r[mid].key>r[j].key)
{tmp=r[j];r[j]=r[mid];
if(tmp>r[i]) r[mid]=tmp; else {r[mid]=r[i];r[i]=tmp }
}
{tmp=r[i];r[i]=r[mid];r[mid]=tmp }
∥ 三者取中:最佳2次比较3次赋值;最差3次比较10次赋值
rp=r[i]; x=r[i].key;
while (i<j)
{while(i<j && x<=r[j].key) j--;
if(i<j) r[i++]=r[j];
while(i<j && x>=r[j].key) i++;
if(i<j) r[j--]=r[i];;
]
r[i]=rp;
return (i);
} ∥ 一次划分算法结束
-
- 假设由1000个关键字为小于10000的整数的记录序列,请设计一种排序方法,要求以尽可能少的比较次数和移动次数实现排序,并按你的设计编写算法。
【题目分析】设关键字小于10000的整数的记录序列存于数组中,再设容量为10000的临时整数数组,按整数的大小直接放入下标为该整数的数组单元中,然后对该数组进行整理存回原容量为1000的数组中。
【算法10.23】
void intsort(int R[],int n)
{//关键字小于10000的1000个整数存于数组R中,本算法对整数进行排序
int R1[10000]={0}; //初始化为0
for(i=0;i<1000;i++)
R1[R[i]]=R[i];
for(i=0,k=0;i<10000;i++)
if(R1[i]!=0)
R[k++]=R1[i];
}
-
- 荷兰国旗问题:设有一个仅有红、白、蓝 三种颜色的条块组成的条块序列。编写一个时间复杂度为O(n)的算法,使得这些条块按红、白、蓝的顺序排好,即排成荷兰国旗图案
【题目分析】设用整型数组R表示荷兰国旗,元素值1、2和3分别表示红、白和篮色。再设整型变量i,j和k,排序结束后R[1..i-1]表示红色,R[i..j-1]表示白色,R[j..n]表示篮色。i,j和k的初始值分别是1,1和n。
【算法10.24】
void DutchFlag(int R[],int n)
∥对红、白、篮三种颜色的条块,经排序形成荷兰国旗
{int i=1,j=1,k=n; ∥指针初始化,j到k是待排序元素
while(j<=k)
if(r[j]==1) ∥红色
{r[i]<-->r[j]; i++;j++; }
else if(r[j]==2) j++; ∥白色
else {r[j]<-->r[k]; k--;} ∥兰色
}
[算法讨论]象将元素值为正数、负数和零排序成前面都是负数,接着是零,最后是正数的排序,以及字母字符、数字字符和其它字符的排序等,都属于这类荷兰国旗问题。排序后,红、白和蓝色的元素个数分别为i-1,j-i,n-j+1。
-
- 已知记录序列a[1..n]中的关键字各不相同,可按如下所述实现计数排序:另设数组c[1..n],对每个记录a[i],统计序列中关键字比它小的记录个数存于c[i],则c[i]=0的记录必为关键字最小的记录,然后依c[i]值的大小对a中记录进行重新排列,编写算法实现上述排序方法。
【算法10.25】
void CountSort(rectype r[],int n)
{//对r[1..n]进行计数排序
c[1..n] =0; ∥c数组初始化,元素值指其在r中的位置
for(i=1;i<n;i++) ∥一趟比较选出大小,给数组 c 赋值
for(j=i+1;j<=n;j++)
if(r[i].key>r[j].key)
c[i]++; else c[j]++;
i=1;
while(i<n) ∥若c[i]+1= i,则r[i] 正好是第i个元素;否则,需要调整
{if(c[i]+1!=i)
{j=i; rc=r[i];
while(c[j]+1!=i)
{k=c[j]+1;rt=r[k]; //暂时保存r[k]/
r[k]=rc; j=c[k]; //取下一 j 值
c[k]=k-1; //第k个已排好
rc=rt
}
r[i]=rc; c[i]=i-1; //完成了一个小循环,第i个已排好
}//if
i=i+1
}// while
上述调整也可用如下逻辑简单但效率低下的算法:
c[1..n]= c[1..n]+1 {c数组元素值指其在r中的位置。
while( i<n )
{ while( c[i]!=i)
{j=c[i]; c[i]ß à c[j]; r[i]ß à r[j]}
i=i+1;
}
-
- 若待排序序列用单链表存储,试给出其快速排序算法。
[题目分析]快速排序的思想是以第一个元素作“枢轴”,通过一趟的比较,将枢轴元素放在其排序的最终位置,使它左面的元素都小于等于它,而它右面的元素都大于等于它,从而再对其左右两部分递归进行快速排序。在链表中实现快速排序也必须使用这一原则。
【算法10.26】
void LinkQuickSort(LinkedList start,end)
∥对单链表start进行快速排序,end是链表的尾指针,初始调用时为null
{LinkedList start1,end1,end2,p;
int flag=0; ∥tag是是否结束排序的标志
If(start==null || start==end) return; ∥空表或只有一个结点
start1=end1=start; ∥start1和end1是右一半链表头结点的前驱和尾结点的指针
if(end1!=end) flag=1;
p=start->next; ∥p为工作指针
while(flag) ∥进行一趟快速排序
{if(p->data<start->data) ∥结点插入前一半
{end1->next=p->next; ∥保留后继结点
if(p==end) flag=0; ∥一趟快速排序结束
if(start==end1)
end0=p;∥end0是遍历遇到的第一个小于枢轴的结点,将为前半的尾结点
p->next=start; start=p;∥修改左半部链表头指针
p=end1->next; ∥恢复当前待处理结点
}
else ∥处理右半部链表
{if(p==end) flag=0; ∥已到链表尾
end1->next=p; end1=p; ;∥end1和p是前驱和后继关系
p=p->next;
}∥else }∥while
LinkQuickSort(start,end0); ∥对枢轴元素最终位置前的单链表快速排序
LinkQuickSort(start1->next,end1);∥对枢轴元素最终位置后的单链表快速排序
}∥LinkQuickSort
-
- 在数组A[0..n-1]中存放有n个不同的整数,其值均在1到n之间。写出一个函数或过程,将A中的n个数从大到小排序后存入B[0..n-1]数组中,要求算法的时间复杂度为O(n)。
【题目分析】因为n个值不同且大小在1到n之间的整数,要求逆序放入另一数组,只要逐个取出放到适当位置即可。即值为i(1<=i<=n)的元素就是数组下标为n-i的元素。
【算法10.27】
void ReArrange(int A[], int B[],int n)
{for(i=0;i<n;i++)
B[n-A[i]]=A[i];
}
第11章 文件
一、基础知识题
11.1名词解释:索引文件, 索引顺序文件,ISAM文件,VSAM文件,散列文件,倒排文件。
【解答】先介绍文件的概念:文件是由大量性质相同的记录组成的集合,按记录类型不同可分为操作系统文件和数据库文件。
文件的基本组织方式有顺序组织、索引组织、散列组织和链组织。文件的存储结构可以采用将基本组织结合的方法,常用的结构有顺序结构、索引结构、散列结构。
(1) 顺序结构,相应文件为顺序文件,其记录按存入文件的先后次序顺序存放。顺序文件本质上就是顺序表。若逻辑上相邻的两个记录在存储位置上相邻,则为连续文件;若记录之间以指针相链接,则称为串联文件。顺序文件只能顺序存取,要更新某个记录,必须复制整个文件。顺序文件连续存取的速度快,主要适用于顺序存取,批量修改的情况。
(2) 带索引的结构,相应文件为索引文件。索引文件包括索引表和数据表,索引表中的索引项包括数据表中数据的关键字和相应地址,索引表有序,其物理顺序体现了文件的逻辑次序,实现了文件的线性结构。索引文件只能是磁盘文件,既能顺序存取,又能随机存取。
(3) 散列结构,也称计算寻址结构,相应文件称为散列文件,其记录是根据关键字值经散列函数计算确定其地址,存取速度快,不需索引,节省存储空间。不能顺序存取,只能随机存取。
其它文件均由以上文件派生而得。
文件采用何种存储结构应综合考虑各种因素,如:存储介质类型、记录的类型、大小和关键字的数目以及对文件作何种操作。
索引文件:在主文件外,再建立索引表指示关键字及其物理记录的地址间一一对应关系。这种由索引表和主文件一起构成的文件称为索引文件。索引表依关键字有序。主文件若按关键字有序称为索引顺序文件,否则称为索引非顺序文件(通常简称索引文件)。索引顺序文件因主文件有序,一般用稀疏索引,占用空间较少。
ISAM文件:ISAM是专为磁盘存取设计的文件组织方式。即使主文件关键字有序,但因磁盘是以盘组、柱面和磁道(盘面)三级地址存取的设备,因此通常对磁盘上的数据文件建立盘组、柱面和磁道(盘面)三级索引。在ISAM文件上检索记录时,先从主索引(柱面索引的索引)找到相应柱面索引。再从柱面索引找到记录所在柱面的磁道索引,最后从磁道索引找到记录所在磁道的第一个记录的位置,由此出发在该磁道上进行顺序查找直到查到为止;反之,若找遍该磁道而未找到所查记录,则文件中无此记录。
VSAM文件:VSAM文件采用B+树动态索引结构,文件只有控制区间和控制区域等逻辑存储单位,与外存储器中柱面、磁道等具体存储单位没有必然联系。VSAM文件结构包括索引集、顺序集和数据集三部分,记录存于数据集中,顺序集和索引集构成B+树,作为文件的索引部分可实现顺链查找和从根结点开始的随机查找。
散列文件:散列文件也称直接存取文件,根据关键字的散列函数值和处理冲突的方法,将记录散列到外存上。这种文件组织只适用于像磁盘那样的直接存取设备,其优点是文件随机存放,记录不必排序,插入、删除方便,存取速度快,无需索引区,节省存储空间。缺点是散列文件不能顺序存取,且只限于简单查询。经多次插入、删除后,文件结构变得不合理,需重组文件,这很费时。
倒排文件:倒排文件是一种多关键字的文件,主数据文件按关键字顺序构成串联文件,并建立主关键字索引。对次关键字也建立索引,该索引称为倒排表。倒排表包括两项,一项是次关键字,另一项是具有同一次关键字值的记录的物理记录号(若数据文件非串联文件,而是索引顺序文件—如ISAM,则倒排表中存放记录的主关键字而不是物理记录号)。倒排表作索引的优点是索引记录快,缺点是维护困难。在同一索引表中,不同的关键字其记录数不同,各倒排表的长度不同,同一倒排表中各项长度也不相等。
11.2什么是文件的逻辑记录和物理记录? 它们有什么区别与联系?
【解答】文件的逻辑结构是用户或程序员能够看见的数据组织形式,是用户对数据的表示和存取方式。文件的各记录间也存在逻辑关系,可以把文件看作一种线性结构。即记录间满足唯一直接前驱和唯一直接后继的关系。
文件的物理结构是数据的物理表示和组织。文件的逻辑结构着眼于用户使用方便,而物理结构需考虑节约存储空间和减少存取时间。存储记录的方式依据实际需要及设备的特性的差异而不同:一个物理记录可以存放一条或多条逻辑记录;多个物理记录也可以表示一个逻辑记录。
11.3索引顺序存取方法(ISAM)中,主文件已按关键字排序,为何还需要主关键字索引?
【解答】
ISAM是专为磁盘存取设计的文件组织方式。即使主文件关键字有序,但因磁盘是以盘组、柱面和磁道(盘面)三级地址存取的设备,因此通常对磁盘上的数据文件建立盘组、柱面和磁道(盘面)三级索引。在ISAM文件上检索记录时,先从主索引(柱面索引的索引)找到相应柱面索引。再从柱面索引找到记录所在柱面的磁道索引,最后从磁道索引找到记录所在磁道的第一个记录的位置,由此出发在该磁道上进行顺序查找直到查到为止;反之,若找遍该磁道而未找到所查记录,则文件中无此记录。
11.4 分析ISAM文件和VSAM文件的应用场合、优缺点等。
【解答】ISAM是一种专为磁盘存取设计的文件组织形式,采用静态索引结构,对磁盘上的数据文件建立盘组、柱面、磁道三级索引。ISAM文件中记录按关键字顺序存放,插入记录时需移动记录并将同一磁道上最后的一个记录移至溢出区,同时修改磁道索引项,删除记录只需在存储位置作标记,不需移动记录和修改指针。经过多次插入和删除记录后,文件结构变得不合理,需周期整理ISAM文件。
VSAM文件采用B+树动态索引结构,文件只有控制区间和控制区域等逻辑存储单位,与外存储器中柱面、磁道等具体存储单位没有必然联系。VSAM文件结构包括索引集、顺序集和数据集三部分,记录存于数据集中,顺序集和索引集构成B+树,作为文件的索引部分可实现顺链查找和从根结点开始的随机查找。
与ISAM文件相比,VSAM文件有如下优点:动态分配和释放存储空间,不需对文件进行重组;能保持较高的查找效率,且查找先后插入记录所需时间相同。因此,基于B+树的VSAM文件通常作为大型索引顺序文件的标准组织。
11.5一个ISAM文件除了主索引外,还包括哪两级索引?
【解答】ISAM文件有三级索引:磁盘组、柱面和磁盘,柱面索引存放在某个柱面上,若柱面索引较大,占多个磁道时,可建立柱面索引的索引—主索引。故本题中所指的两级索引是盘组和磁道。
11.6 为什么在倒排文件组织中,实际记录中的关键字域可删除以节约空间?而在多重表结构中这样做为什么要牺牲性能?
【解答】因倒排文件组织中,倒排表有关键字值及同一关键字值的记录的所有物理记录号,可方便地查询具有同一关键字值的所有记录;而多重表文件中次关键字索引结构不同,删除关键字域后查询性能受到影响。
11.7 简单比较文件的多重表和倒排表组织方式各自特点。
【解答】多重表文件是把索引与链接结合而形成的组织方式。记录按主关键字顺序构成一个串联文件,建立主关键字的索引(主索引)。对每一次关键字建立次关键字索引,具有同一关键字的记录构成一个链表。主索引为非稠密索引,次索引为稠密索引,每个索引项包括次关键字,头指针和链表长度。多重表文件易于编程,也易于插入,但删除繁锁。需在各次关键字链表中删除。
倒排文件是一种多关键字的文件,主数据文件按关键字顺序构成串联文件,并建立主关键字索引。对次关键字也建立索引,该索引称为倒排表。倒排表包括两项,一项是次关键字,另一项是具有同一次关键字值的记录的物理记录号(若数据文件非串联文件,而是索引顺序文件—如ISAM,则倒排表中存放记录的主关键字而不是物理记录号)。倒排表作索引的优点是索引记录快,缺点是维护困难。在同一索引表中,不同的关键字其记录数不同,各倒排表的长度不同,同一倒排表中各项长度也不相等。
11.8 组织待检索文件的倒排表的优点是什么?
【解答】倒排表作索引的优点是索引记录快,因为从次关键字值直接找到各相关记录的物理记录号,倒排因此而得名(因通常的查询是从关键字查到记录)。在插入和删除记录时,倒排表随之修改,倒排表中具有相同次关键字的记录号是有序的。
11.9 为什么文件的倒排表比多重表组织方式节省空间?
【解答】排表有两项,一是次关键字值,二是具有相同次关键字值的物理记录号,这些记录号有序且顺序存储,不使用多重表中的指针链接,因而节省了空间。
11.10 试比较顺序文件,索引非顺序文件,索引顺序文件,散列文件的存储代价,检索,插入,删除记录时的优点和缺点。
【解答】(1)顺序文件只能顺序查找,优点是批量检索速度快,不适于单个记录的检索。顺序文件不能象顺序表那样插入、删除和修改,因文件中的记录不能象向量空间中的元素那样“移动”,只能通过复制整个文件实现上述操作。
(2)索引非顺序文件适合随机存取,不适合顺序存取,因主关键字未排序,若顺序存取会引起磁头频繁移动。索引顺序文件是最常用的文件组织,因主文件有序,既可顺序存取也可随机存取。索引非顺序文件是稠密索引,可以“预查找”,索引顺序文件是稀疏索引,不能“预查找”,但由于索引占空间较少,管理要求低,提高了索引的查找速度。
(3)散列文件也称直接存取文件,根据关键字的散列函数值和处理冲突的方法,将记录散列到外存上。这种文件组织只适用于像磁盘那样的直接存取设备,其优点是文件随机存放,记录不必排序,插入、删除方便,存取速度快,无需索引区,节省存储空间。缺点是散列文件不能顺序存取,且只限于简单查询。经多次插入、删除后,文件结构不合理,需重组文件,这很费时。
11.11某一文件有15个记录,关键字分别为285,070,923,597,512,262,015,076,157,208,337,613,117,390,362。桶的容量m=3,桶数b=7,用除留余数法构造哈希函数H(key)=key % 7,试构造其散列文件。
147 | 812 | 357 |
|
589 | 197 |
| ∧ |
723 | 226 | 072 |
|
136 | 213 |
| ∧ |
207 |
|
| ∧ |
096 |
|
| ∧ |
048 | 881 | 118 | ∧ |
364 ∧ |
205 ∧ |
桶编号 基桶 溢出桶 |
0 1 2 3 4 5 6 |