Data topic details | Data

数据结构课后习题 | Data

第 1 章 绪论

1. 简述数据与数据元素的关系与区别。

答:凡是能被计算机存储、加工的对象统称为数据,数据是一个集合。数据元素是数据的基本单位,是数据的个体。数据元素与数据之间的关系是元素与集合之间的关系。

2. 采用二元组表示的数据逻辑结构

采用二元组表示的数据逻辑结构 S=<D,R>,其中 D={a,b,···,i},R={r},r={<a,b>,<a,c>,<c,d>,<c,f>,<f,h>,<d,e>,<f,g>,<h,i>},问关系 r 是什么类型的逻辑结构?哪些结点是开始结点,哪些结点是终端结点?

答:该逻辑结构为树形结构,其中 a 结点没有前驱结点,它是开始结点,b、e、i 和 g、结点没有后继结点,它们都是终端结点。

3. 简述数据逻辑结构与存储结构的关系。

答:在数据结构中,逻辑结构与计算机无关,存储结构是数据元素之间的逻辑关系在计算机中的表示。存储结构不仅将逻辑结构中所有数据元素存储到计算机内存中,而且还要在内存中存储各数据元素间的逻辑关系。通常情况下,一种逻辑结构可以有多种存储结构,例如,线性结构可以采用顺序存储结构或链式存储结构表示。

4. 简述数据结构中运算描述和运算实现的异同。

答:运算描述是指逻辑结构施加的操作,而运算实现是指一个完成该运算功能的算法。它们的相同点是,运算描述和运算实现都能完成对数据的“处理”或某种特定的操作。不同点是,运算描述只是描述处理功能,不包括处理步骤和方法,而运算实现的核心则是设
计处理步骤。

5. 数据结构和数据类型有什么区别?

答:数据结构是相互之间存在一种或多种特定关系的数据元素的集合,一般包括三个
方面的内容,即数据的逻辑结构、存储结构和数据的运算。而数据类型是一个值的集合和
定义在这个值集上的一组运算的总称,如C语言中的short int数据类型是由-32768~32767
(16位机)的整数和+、-、*、/、%等运算符构成。

6. 在 C/C++中提供了引用运算符,简述其在算法描述中的主要作用。

答:在算法设计中,一个算法通常用一个或多个 C/C++函数来实现,在 C/C++函数之间传递参数时有两种情况,一是从实参到形参的单向值传递,二是实参和形参之间的双向值传递。对形参使用引用运算符,即在形参名前加上“&”,不仅可以实现实参和形参之间的双向值传递,而且使算法设计简单明晰。

7. 有以下用 C/C++语言描述的算法,说明其功能:

void fun(double &y,double x,int n)
{	y=x;
	while (n>1)
	{	y=y*x;
		n--;
	}
}

答:本算法的功能是计算 y= x n x^{n} xn

8. 用 C/C++语言描述下列算法,并给出算法的时间复杂度。

(1)求一个 n 阶整数数组的所有元素之和。
(2)对于输入的任意 3 个整数,将它们按从小到大的顺序输出。
(3)对于输入的任意 n 个整数,输出其中的最大和最小元素。

答:(1)算法如下:

int sum(int A[N][N],int n)
{	int i,j,s=0;
	for (i=0;i<n;i++)
		for (j=0;j<n;j++)
			s=s+A[i][j];
	return(s);
}

本算法的时间复杂度为 O( n 2 n^{2} n2)。
 
(2)算法如下:

void order(int a,int b,int c)
{	if (a>b)
	{	if (b>c)
			printf("%d,%d,%d\n",c,b,a);
		else if (a>c)
			printf("%d,%d,%d\n",b,c,a);
		else
			printf("%d,%d,%d\n",b,a,c);
	}
	else
	{	if (b>c)
		{	if (a>c)
				printf("%d,%d,%d\n",c,a,b);
			else
				printf("%d,%d,%d\n",a,c,b);
		}
		else printf("%d,%d,%d\n",a,b,c);
	}
}

本算法的时间复杂度为 O(1)。
 
(3)算法如下:

void maxmin(int A[],int n,int &max,int &min) 
{	int i;
	min=min=A[0];
	for (i=1;i<n;i++)
	{	if (A[i]>max) max=A[i];
		if (A[i]<min) min=A[i];
	}
}

本算法的时间复杂度为 O(n)。

9. 设 3 个表示算法频度的函数 f、g 和 h 分别为:

   f ( n ) = 100 n 3 + n 2 + 1000 f(n)=100n^{3}+n^{2}+1000 f(n)=100n3n2+1000
   g ( n ) = 25 n 3 + 5000 n 2 g(n)=25n^{3}+5000n^{2} g(n)=25n35000n2
   h ( n ) = n 1.5 + 5000 n l o g 2 n h(n)=n^{1.5}+5000nlog_2n h(n)=n1.55000nlog2n
   求它们对应的时间复杂度。

答: f ( n ) = 100 n 3 + n 2 + 1000 = O ( n 3 ) , g ( n ) = 25 n 3 + 5000 n 2 = O ( n 3 ) f(n)=100n^{3}+n^{2}+1000=O(n^{3}),g(n)=25n{3}+5000n^{2}=O(n^{3}) f(n)=100n3n2+1000=O(n3)g(n)=25n35000n2=O(n3)
n → ∞ 时 , n > l o g 2 n , 所 以 h ( n ) = n 1.5 + 5000 n l o g 2 n = O ( n 1.5 ) n \to \infty时, \sqrt{n}>log_2n,所以h(n)=n^{1.5}+5000nlog_2n=O(n^{1.5}) nn log2nh(n)=n1.55000nlog2n=O(n1.5)

10. 分析下面程序段中循环语句的执行次数。

int j=0,s=0,n=100;
do
{	j=j+1;
	s=s+10*j;
} while (j<n && s<n);

答: j j j=0,第 1 次循环: j j j=1, s s s=10。第 2 次循环: j j j=2, s s s=30。第 3 次循环: j j j=3, s s s=60。第 4 次循环: j j j=4, s s s=100。while 条件不再满足。所以,其中循环语句的执行次数为 4。

11. 设 n 为正整数,给出下列 3 个算法关于问题规模 n 的时间复杂度。

(1)算法 1

void fun1(int n)
{	i=1,k=100;
	while (i<=n)
	{	k=k+1;
		i+=2;
	}
}

(2)算法 2

void fun2(int b[]int n)
{	int i,j,k,x;
	for (i=0;i<n-1;i++)
	{	k=i;
		for (j=i+1;j<n;j++)
			if (b[k]>b[j]) k=j;
		x=b[i];b[i]=b[k];b[k]=x;
	}
}

(3)算法 3

void fun3(int n)
{	int i=0,s=0;
	while (s<=n)
	{	i++;
		s=s+i;
	}
}

答:(1)设 while 循环语句执行次数为 T(n),则:
i = T ( n ) + 1 ⩽ n i = T(n) + 1\leqslant n i=T(n)+1n,即 T ( n ) ⩽ ( n − 1 ) / 2 = O ( n ) T(n) \leqslant (n-1)/2=O(n) T(n)(n1)/2=O(n)
(2)算法中的基本运算语句是 if ( b [ k ] > b [ j ] ) k = j b[k] > b[j]) k=j b[k]>b[j])k=j,其执行次数 T ( n ) T(n) T(n)为:
T ( n ) = ∑ i = 0 n − 2 ∑ j = i + 1 n − 1 1 = ∑ i = 0 n − 2 ( n − i − 1 ) = n ( n − 1 ) 2 = O ( n 2 ) T(n)= \sum_{i=0}^{n-2} \sum_{j=i+1}^{n-1} 1 = \sum_{i=0}^{n-2} (n-i-1) = \frac{n(n-1)}{2} = O(n^{2}) T(n)=i=0n2j=i+1n11=i=0n2(ni1)=2n(n1)=O(n2)
(3)设 while 循环语句执行次数为 T(n),则:
s = 1 + 2 + ∗ ∗ ⋅ ⋅ ⋅ ∗ ∗ + T ( n ) = T ( n ) ( T ( n ) + 1 ) 2 ⩽ n s=1+2+**···**+T(n)=\frac{T(n)(T(n)+1)}{2} \leqslant n s=1+2++T(n)=2T(n)(T(n)+1)n,则 T(n)=O( n \sqrt n n )。

12. 有以下递归算法用于对数组 a[i…j]的元素进行归并排序:

void mergesort(int a[],int i,int j)
{	int m;
	if (i!=j)
	{	m=(i+j)/2;
		mergesort(a,i,m);
		mergesort(a,m+1,j);
		merge(a,i,j,m);
	}
}

求执行 mergesort( a , 0 , n − 1 a,0,n-1 a0n1)的时间复杂度。其中,merge( a , i , j , m a,i,j,m aijm)用于两个有序子序列 a [ i . . m ] a[i..m] a[i..m] a [ m + 1.. j ] a[m+1..j] a[m+1..j] 的合并,是非递归函数,它的时间复杂度为 O(合并的元素个数)。

答:设 mergesort( a , 0 , n − 1 a,0,n-1 a0n1)的执行时间为 T ( n ) T(n) T(n),分析得到以下递归关系:
T ( n ) = O ( 1 ) T(n)= O(1) T(n)=O(1)        n = 1 n=1 n=1
T ( n ) = 2 T ( n 2 ) + O ( n ) T(n)=2T( \frac{n}{2})+O(n) T(n)=2T(2n)+O(n)    n > 1 n>1 n>1
其中,O(n)为 merge()所需的时间,设为 cn(c 为常量)。因此:
T ( n ) = 2 T ( n 2 ) + c n = 2 ( 2 T ( n 2 2 ) + c n 2 ) + c n = 2 2 T ( n 2 2 ) + 2 c n = 2 3 T ( n 2 3 ) + 3 c n = 2 k T ( n 2 k + k c n ) = 2 k O ( 1 ) + k c n \begin{aligned} T(n) &=2T( \frac{n}{2} ) +cn=2(2T( \frac{n}{2^{2}} ) + \frac{cn}{2}) + cn = 2^{2}T( \frac{n}{2^{2}} ) + 2cn = 2^{3}T( \frac{n}{2^{3}} ) + 3cn \\ &=2^{k} T(\frac{n}{2^{k}} + kcn ) = 2^{k} O(1) + kcn \end{aligned} T(n)=2T(2n)+cn=2(2T(22n)+2cn)+cn=22T(22n)+2cn=23T(23n)+3cn=2kT(2kn+kcn)=2kO(1)+kcn
由于 n 2 k \frac{n}{2^{k}} 2kn 趋近于 1,则 k = l o g 2 n k=log_2n k=log2n。所以 T(n) = 2 l o g 2 n 2^{log_2n} 2log2n O(1) + c n l o g 2 n = n + c n l o g 2 n = O ( n l o g 2 n ) cnlog_2n = n + cnlog_2n = O(nlog_2n) cnlog2n=n+cnlog2n=O(nlog2n)

13. 描述一个集合的抽象数据类型 ASet,其中所有元素为正整数,集合的基本运算包括:

   (1)由整数数组 a[0…n-1]创建一个集合。
   (2)输出一个集合的所有元素。
   (3)判断一个元素是否在一个集合中。
   (4)求两个集合的并集。
   (5)求两个集合的差集。
   (6)求两个集合的交集。
   在此基础上设计集合的顺序存储结构,并实现各基本运算的算法。

答:抽象数据类型 ASet 的描述如下:
ADT ASet
{       数据对象: D D D = { d i d_{i} di | 0 ⩽ i ⩽ n \leqslant i \leqslant n in,n为一个正整数}
        数据关系:无。
        基本运算:
                  createset( &s,a,n):创建一个集合s;
                  dispset( s):输出集合s;
                  inset(s,e):判断元素e是否在集合s中。
                  void add(s1,s2,s3):s3=s1∪s2; //求集合的并集
                  void sub(s1,s2,s3):s3=s1-s2; //求集合的差集
                  void intersection(s1,s2,s3):s3=s1∩s2; //求集合的交集
}
 
设计集合的顺序存储结构类型如下:

typedef struct //集合结构体类型
{	int data[MaxSize]; //存放集合中的元素,其中 MaxSize 为常量
	int length; //存放集合中实际元素个数
} Set; //将集合结构体类型用一个新类型名 Set 表示

采用 Set 类型的变量存储一个集合。对应的基本运算算法设计如下:

void createset(Set &s,int a[],int n) //创建一个集合
{	int i;
	for (i=0;i<n;i++)
		s.data[i]=a[i];
	s.length=n;
}

void dispset(Set s) //输出一个集合
{	int i;
	for (i=0;i<s.length;i++)
		printf("%d ",s.data[i]);
	printf("\n");
}

bool inset(Set s,int e) //判断 e 是否在集合 s 中
{	int i;
	for (i=0;i<s.length;i++)
		if (s.data[i]==e)
			return true;
	return false;
}

void add(Set s1,Set s2,Set &s3) //求集合的并集
{	int i;
	for (i=0;i<s1.length;i++) //将集合 s1 的所有元素复制到 s3 中
		s3.data[i]=s1.data[i];
	s3.length=s1.length;
	for (i=0;i<s2.length;i++) //将 s2 中不在 s1 中出现的元素复制到 s3 中
		if (!inset(s1,s2.data[i]))
		{	s3.data[s3.length]=s2.data[i];
			s3.length++;
		}
}

void sub(Set s1,Set s2,Set &s3) //求集合的差集
{	int i;
	s3.length=0;
	for (i=0;i<s1.length;i++) //将 s1 中不出现在 s2 中的元素复制到 s3 中
		if (!inset(s2,s1.data[i]))
		{	s3.data[s3.length]=s1.data[i];
			s3.length++;
		}
}

void intersection(Set s1,Set s2,Set &s3) //求集合的交集
{	int i;
		s3.length=0;
		for (i=0;i<s1.length;i++) //将 s1 中出现在 s2 中的元素复制到 s3 中
			if (inset(s2,s1.data[i]))
			{	s3.data[s3.length]=s1.data[i];
				s3.length++;
			}
}

第 2 章 线性表

1. 简述线性表两种存储结构各自的主要特点。

答:线性表的两种存储结构分别是顺序存储结构和链式存储结构。顺序存储结构的主要特点如下:
   ① 数据元素中只有自身的数据域,没有关联指针域。因此,顺序存储结构的存储密度较大。
   ② 顺序存储结构需要分配一整块比较大存储空间,所以存储空间利用率较低。
   ③ 逻辑上相邻的两个元素在物理上也是相邻的,通过元素的逻辑序号可以直接其元素值,即具有随机存取特性。
   ④ 插入和删除操作会引起大量元素的移动。
   链式存储结构的主要特点如下:
   ① 数据结点中除自身的数据域,还有表示逻辑关系的指针域。因此,链式存储结构比顺序存储结构的存储密度小。
   ② 链式存储结构的每个结点是单独分配的,每个结点的存储空间相对较小,所以存储空间利用率较高。
   ③ 在逻辑上相邻的结点在物理上不一定相邻,因此不具有随机存取特性。
   ④ 插入和删除操作方便灵活,不必移动结点,只需修改结点中的指针域即可。

2. 简述单链表设置头结点的主要作用。

答:对单链表设置头结点的主要作用如下:
   ① 对于带头结点的单链表,在单链表的任何结点之前插入结点或删除结点,所要做的都是修改前一个结点的指针域,因为任何结点都有前驱结点(若单链表没有头结点,则首结点没有前驱结点,在其前插入结点和删除该结点时操作复杂些),所以算法设计方便。
   ② 对于带头结点的单链表,在表空时也存在一个头结点,因此空表与非空表的处理是一样的。

3. 假设某个含有n个元素的线性表有如下运算:

   Ⅰ. 查找序号为i(1≤i≤n)的元素
   Ⅱ. 查找第一个值为x的元素
   Ⅲ. 插入新元素作为第一个元素
   Ⅳ. 插入新元素作为最后一个元素
   Ⅴ. 插入第i(2≤i≤n)个元素
   Ⅵ. 删除第一个元素
   Ⅶ. 删除最后一个元素
   Ⅷ. 删除第i(2≤i≤n)个元素
   现设计该线性表的如下存储结构:
   ① 顺序表
   ② 带头结点的单链表
   ③ 带头结点的循环单链表
   ④ 不带头结点仅有尾结点指针标识的循环单链表
   ⑤ 带头结点的双链表
   ⑥ 带头结点的循环双链表
   指出各种存储结构中对应运算算法的时间复杂度。

答:各种存储结构对应运算的时间复杂度如表2.1所示。
表 2.1 各种存储结构对应运算的时间复杂度

-Ⅰ. 查找序号为i(1≤i≤n)的元素Ⅱ. 查找第一个值为x的元素Ⅲ. 插入新元素作为第一个元素Ⅳ. 插入新元素作为最后一个元素Ⅴ. 插入第i(2≤i≤n)个元素Ⅵ. 删除第一个元素Ⅶ. 删除最后一个元素Ⅷ. 删除第i(2≤i≤n)个元素
①顺序表O(1)O(n)O(n)O(1)O(n)O(n)O(1)O(n)
②带头结点的单链表O(n)O(n)O(1)O(n)O(n)O(1)O(n)O(n)
③带头结点的循环单链表O(n)O(n)O(1)O(n)O(n)O(1)O(n)O(n)
④不带头结点仅有尾结点指针标识的循环单链表O(n)O(n)O(1)O(1)O(n)O(1)O(n)O(n)
⑤带头结点的双链表O(n)O(n)O(1)O(n)O(n)O(1)O(n)O(n)
⑥带头结点的循环双链表O(n)O(n)O(1)O(1)O(n)O(1)O(1)O(n)

4. 对于顺序表 L,指出以下算法的功能。

void fun(SqList *&L)
{	int i,j=0;
	for (i=1;i<L->length;i++)
		if (L->data[i]>L->data[j])
			j=i;
	for (i=j;i<L->length-1;i++)
		L->data[i]=L->data[i+1];
	L->length--;
}

答:该算法的功能是在顺序表 L 中查找第一个值最大的元素,并删除该元素。

5. 对于顺序表 L,指出以下算法的功能。

void fun(SqList *&L,ElemType x)
{	int i,j=0;
	for (i=1;i<L->length;i++)
		if (L->data[i]<=L->data[j])
			j=i;
	for (i=L->length;i>j;i--)
		L->data[i]=L->data[i-1];
	L->data[j]=x;
	L->length++;
}

答:在顺序表 L 中查找最后一个值最小的元素,在该位置上插入一个值为 x 的元素。

6. 有人设计如下算法用于删除整数顺序表 L 中所有值在[x,y]范围内的元素,该算法显然不是高效的,请设计一个同样功能的高效算法。

void fun(SqList *&L,ElemType x)
{	int i,j;
	for (i=0;i<L->length;i++)
		if (L->data[i]>=x && L->data[i]<=y)
		{	for (j=i;j<L->length-1;j++)
				L->data[j]=L->data[j+1];
			L->length--;
		}
}

答:该算法在每次查找到 x 元素时,都通过移动来删除它,时间复杂度为 O( n 2 n^{2} n2),显然不是高效的算法。实现同样功能的算法如下:

void fun(SqList *&L,ElemType x,ElemType y)
{	int i,k=0;
	for (i=0;i<L->length;i++)
		if (!(L->data[i]>=x && L->data[i]<=y))
		{	L->data[k]=L->data[i];
			k++;
		}
	L->length=k;
}

  该算法(思路参见《教程》例 2.3 的解法一)的时间复杂度为 O(n),是一种高效的算法。

7. 设计一个算法,将元素x插入到一个有序(从小到大排序)顺序表的适当位置上,并保持有序性。

解:通过比较在顺序表 L 中找到插入 x 的位置 i,将该位置及后面的元素均后移一个位置,将 x 插入到位置 i 中,最后将 L 的长度增 1。对应的算法如下:

void Insert(SqList *&L,ElemType x)
{	int i=0,j;
	while (i<L->length && L->data[i]<x) i++;
	for (j=L->length-1;j>=i;j--)
		L->data[j+1]=L->data[j];
	L->data[i]=x;
	L->length++;
}

8. 假设一个顺序表 L 中所有元素为整数,设计一个算法调整该顺序表,使其中所有小于零的元素放在所有大于等于零的元素的前面。

解:先让 i、j 分别指向顺序表 L 的第一个元素和最后一个元素。当 i<j 时循环:i 从前向后扫描顺序表 L,找大于等于 0 的元素,j 从后向前扫描顺序表 L,找小于 0 的元素,当i<j 时将两元素交换(思路参见《教程》例 2.4 的解法一)。对应的算法如下:

void fun(SqList *&L)
{	int i=0,j=L->length-1;
	while (i<j)
	{	while (L->data[i]<0) i++;
		while (L->data[j]>=0) j--;
		if (i<j) //L->data[i]与 L->data[j]交换
			swap(L->data[i], L->data[j]);
	}
}

9. 对于不带头结点的单链表 L1,其结点类在为 LinkNode,指出以下算法的功能。

void fun1(LinkNode *&L1,LinkNode *&L2)
{	int n=0,i;
	LinkNode *p=L1;
	while (p!=NULL)
	{	n++;
		p=p->next;
	}
	p=L1;
	for (i=1;i<n/2;i++)
		p=p->next;
	L2=p->next;
	p->next=NULL;
}

答:对于含有 n n n 个结点的单链表 L 1 L1 L1,将 L 1 L1 L1 拆分成两个不带头结点的单链表 L 1 L1 L1 L 2 L2 L2,其中 L 1 L1 L1 含有原来的前 n n n/2 个结点, L 2 L2 L2 含有余下的结点。

10. 在结点类型为 DLinkNode 的双链表中,给出将 p 所指结点(非尾结点)与其后继结点交换的操作。

答:将 p p p 所指结点(非尾结点)与其后继结点交换的操作如下:

q=p->next; //q 指向结点 p 的后继结点
if (q->next!=NULL) //从链表中删除结点 p
	q->next->prior=p;
p->next=q->next;
p->prior->next=q; //将结点 q 插入到结点 p 的前面
q->prior=p->prior;
q->next=p;
p->prior=q;

11. 有一个线性表( a 1 , a 2 , … , a n a_{1},a_{2},…,a_{n} a1a2an),其中 n≥2,采用带头结点的单链表存储,头指针为 L L L,每个结点存放线性表中一个元素,结点类型为( d a t a , n e x t data,next datanext),现查找某个元素值等于 x x x 的结点指针,若不存在这样的结点返回 NULL。分别写出下面 3 种情况的查找语句。要求时间尽量少。

  (1)线性表中元素无序。
  (2)线性表中元素按递增有序。
  (3)线性表中元素按递减有序。

答:(1)元素无序时的查找语句如下:

p=L->next;
while (p!=NULL && p->data!=x)
p=p->next;
if (p==NULL) return NULL;
else return p;

(2)元素按递增有序时的查找语句如下:

p=L->next;
while (p!=NULL && p->data<x )
p=p->next;
if (p==NULL || p->data>x) return NULL;
else return p;

(3)元素按递减有序时的查找语句如下:

p=L->next;
while (p!=NULL && p->data>x)
p=p->next;
if (p==NULL || p->data<x) return NULL;
else return p;

12. 设计一个算法,将一个带头结点的数据域依次为 a 1 、 a 2 、 … 、 a n ( n ≥ 3 ) a_{1}、a_{2}、…、a_{n}(n≥3) a1a2ann3的单链表的所有结点逆置,即第一个结点的数据域变为 a n a_{n} an,第 2 个结点的数据域变为 a n − 1 a_{n-1} an1,…,尾结点的数据域为 a1。

解:首先让 p 指针指向首结点,将头结点的 next 域设置为空,表示新建的单链表为空表。用 p 扫描单链表的所有数据结点,将结点 p 采用头插法插入到新建的单链表中。对应的算法如下:

void Reverse(LinkNode *&L)
{	LinkNode *p=L->next,*q;
	L->next=NULL;
	while (p!=NULL) //扫描所有的结点
	{	q=p->next; //q 临时保存 p 结点的后继结点
		p->next=L->next; //总是将 p 结点作为首结点插入
		L->next=p;
		p=q; //让 p 指向下一个结点
	}
}

13. 一个线性表( a 1 , a 2 , … , a n a_{1},a_{2},…,a_{n} a1a2an)( n n n>3)采用带头结点的单链表 L L L 存储。设计一个高效算法求中间位置的元素(即序号为 [n/2] 的元素)。

解:让 p 、 q p、q pq 首先指向首结点,然后在 p 结点后面存在两个结点时循环:p 后移两个结点,q 后移一个结点。当循环结束后,q 指向的就是中间位置的结点,对应的算法如下:

ElemType Midnode(LinkNode *L)
{	LinkNode *p=L->next,*q=p;
	while (p->next!=NULL && p->next->next!=NULL)
	{	p=p->next->next;
		q=q->next;
	}
	return q->data;
}

14. 设计一个算法在带头结点的非空单链表 L 中第一个最大值结点(最大值结点可能有多个)之前插入一个值为 x 的结点。

解:先在单链表 L L L 中查找第一个最大值结点的前驱结点 m a x p r e maxpre maxpre,然后在其后面插入值为 x x x 的结点。对应的算法如下:

void Insertbeforex(LinkNode *&L,ElemType x)
{	LinkNode *p=L->next,*pre=L;
	LinkNode *maxp=p,*maxpre=L,*s;
	while (p!=NULL)
	{	if (maxp->data < p->data)
		{	maxp=p;
			maxpre=pre;
		}
		pre=p; p=p->next;
	}
	s=(LinkNode *)malloc(sizeof(LinkNode));
	s->data=x;
	s->next=maxpre->next;
	maxpre->next=s;
}

15. 设有一个带头结点的单链表 L,结点的结构为(data,next),其中 data 为整数元素,next 为后继结点的指针。设计一个算法,首先按递减次序输出该单链表中各结点的数据元素,然后释放所有结点占用的存储空间,并要求算法的空间复杂度为 O(1)。

解:先对单链表 L 的所有结点递减排序(思路参见《教程》例 2.8),再输出所有结点值,最后释放所有结点的空间。对应的算法如下:

void Sort(LinkNode *&L) //对单链表 L 递减排序
{	LinkNode *p,*q,*pre;
	p=L->next->next; //p 指向第 2 个数据结点
	L->next->next=NULL;
	while (p!=NULL)
	{	q=p->next;
		pre=L;
		while (pre->next!=NULL && pre->next->data>p->data)
		pre=pre->next;
		p->next=pre->next; //在结点 pre 之后插入 p 结点
		pre->next=p;
		p=q;
	}
}

void fun(LinkNode *&L) //完成本题的算法
{	printf("排序前单链表 L:");
	DispList(L); //调用基本运算算法
	Sort(L);
	printf("排序后单链表 L:");
	DispList(L); //调用基本运算算法
	printf("释放单链表 L\n");
	DestroyList(L); //调用基本运算算法
}

16. 设有一个双链表 h,每个结点中除有 p r i o r 、 d a t a prior、data priordata n e x t next next 三个域外,还有一个访问频度域 freq,在链表被起用之前,其值均初始化为零。每当进行 LocateNode( h , x h,x hx)运算时,令元素值为 x x x 的结点中 f r e q freq freq 域的值加 1,并调整表中结点的次序,使其按访问频度的递减序排列,以便使频繁访问的结点总是靠近表头。试写一符合上述要求的 LocateNode 运算的算法。

解:在 DLinkNode 类型的定义中添加整型 f r e q freq freq 域,给该域初始化为 0。在每次查找到一个结点 p p p 时,将其 f r e q freq freq 域增 1,再与它前面的一个结点 p r e pre pre 进行比较,若 p p p 结点的 f r e q freq freq 域值较大,则两者交换,如此找一个合适的位置。对应的算法如下:

bool LocateNode(DLinkNode *h,ElemType x)
{	DLinkNode *p=h->next,*pre;
	while (p!=NULL && p->data!=x)
		p=p->next; //找 data 域值为 x 的结点 p
	if (p==NULL) //未找到的情况
		return false;
	else //找到的情况
	{	p->freq++; //频度增 1
		pre=p->prior; //结点 pre 为结点 p 的前驱结点
		while (pre!=h && pre->freq<p->freq)
		{	p->prior=pre->prior;
			p->prior->next=p; //交换结点 p 和结点 pre 的位置
			pre->next=p->next;
			if (pre->next!=NULL) //若 p 结点不是尾结点时
			pre->next->prior=pre;
			p->next=pre;pre->prior=p;
			pre=p->prior; //q 重指向结点 p 的前驱结点
		}
		return true;
	}
}

17. 设 h a = ( a 1 , a 2 , … , a n ) ha=(a_{1},a_{2},…,a_{n}) ha=(a1a2an) h b = ( b 1 , b 2 , … , b m ) hb=(b_{1},b_{2}, …,b_{m}) hb=(b1b2bm) 是两个带头结点的循环单链表。设计一个算法将这两个表合并为带头结点的循环单链表 h c hc hc

解:先找到 ha 的尾结点 p,将结点 p 的 next 指向 hb 的首结点,再找到 hb 的尾结点 p,将其构成循环单链表。对应的算法如下:

void Merge(LinkNode *ha, LinkNode *hb, LinkNode *&hc)
{	LinkNode *p=ha->next;
	hc=ha;
	while (p->next!=ha) //找到 ha 的尾结点 p
		p=p->next;
	p->next=hb->next; //将结点 p 的 next 指向 hb 的首结点
	while (p->next!=hb)
		p=p->next; //找到 hb 的尾结点 p
	p->next=hc; //构成循环单链表
	free(hb); //释放 hb 单链表的头结点
}

18. 设两个非空线性表分别用带头结点的循环双链表 h a ha ha h b hb hb 表示。设计一个算法Insert( h a , h b , i ha,hb,i hahbi)。其功能是: i i i=0 时,将 h b hb hb 插入到 h a ha ha 的前面;当 i i i>0 时,将 h b hb hb 插入到 h a ha ha中第 i i i 个结点的后面;当 i i i 大于等于 h a ha ha 的长度时,将 h b hb hb 插入到 h a ha ha 的后面。

解:利用带头结点的循环双链表的特点设计的算法如下:

void Insert(DLinkNode *&ha, DLinkNode *&hb,int i)
{	DLinkNode *p=ha->next,*post;
	int lena=1,j;
	while (p->next!=ha) //求出 ha 的长度 lena
	{	lena++;
		p=p->next;
	}
	if (i==0) //将 hb 插入到 ha 的前面
	{	p=hb->prior; //p 指向 hb 的尾结点
		p->next=ha->next; //将结点 p 链到 ha 的首结点前面
		ha->next->prior=p;
		ha->next=hb->next;
		hb->next->prior=ha; //将 ha 头结点与 hb 的首结点链起来
	}
	else if (i<lena) //将 hb 插入到 ha 中间
	{	j=1;
		p=ha->next;
		while (j<i) //在 ha 中查找第 i 个结点 p
		{	p=p->next;
			j++;
		}
		post=p->next; //post 指向 p 结点的后继结点
		p->next=hb->next; //将 hb 的首结点作为 p 结点的后继结点
		hb->next->prior=p;
		hb->prior->next=post; //将 post 结点作为 hb 尾结点的后继结点
		post->prior=hb->prior;
	}
	else //将 hb 链到 ha 之后
	{	ha->prior->next=hb->next; //ha->prior 指向 ha 的尾结点
		hb->next->prior=ha->prior;
		hb->prior->next=ha;
		ha->prior=hb->prior;
	}
	free(hb); //释放 hb 头结点
}

19. 用带头结点的单链表表示整数集合,完成以下算法并分析时间复杂度:

(1)设计一个算法求两个集合A和B的并集运算即 C = A ∪ B C=A \cup B C=AB。要求不破坏原有的单链表 A A A B B B
(2)假设集合中的元素按递增排列,设计一个高效算法求两个集合 A A A B B B 的并集运算即 C = A ∪ B C=A \cup B C=AB 。要求不破坏原有的单链表 A A A B B B

解:(1)集合 A 、 B 、 C A、B、C ABC 分别用单链表 h a 、 h b 、 h c ha、hb、hc hahbhc 存储。采用尾插法创建单链表 h c hc hc,先将 h a ha ha 单链表中所有结点复制到 h c hc hc 中,然后扫描单链表 h b hb hb,将其中所有不属于 h a ha ha 的结点复制到 h c hc hc 中。对应的算法如下:

void Union1(LinkNode *ha,LinkNode *hb,LinkNode *&hc)
{	LinkNode *pa=ha->next,*pb=hb->next,*pc,*rc;
	hc=(LinkNode *)malloc(sizeof(LinkNode));
	rc=hc;
	while (pa!=NULL) //将 A 复制到 C 中
	{	pc=(LinkNode *)malloc(sizeof(LinkNode));
		pc->data=pa->data;
		rc->next=pc;
		rc=pc;
		pa=pa->next;
	}
	while (pb!=NULL) //将 B 中不属于 A 的元素复制到 C 中
	{	pa=ha->next;
		while (pa!=NULL && pa->data!=pb->data)
		pa=pa->next;
		if (pa==NULL) //pb->data 不在 A 中
		{	pc=(LinkNode *)malloc(sizeof(LinkNode));
			pc->data=pb->data;
			rc->next=pc;
			rc=pc;
		}
		pb=pb->next;
	}
	rc->next=NULL; //尾结点 next 域置为空
}

   本算法的时间复杂度为 O( m × n m×n m×n),其中 m、n 为单链表 ha 和 hb 中的数据结点个数。
 
(2)同样采用尾插法创建单链表 h c hc hc,并利用单链表的有序性,采用二路归并方法来提高算法效率。对应的算法如下:

void Union2(LinkNode *ha,LinkNode *hb,LinkNode *&hc)
{	LinkNode *pa=ha->next,*pb=hb->next,*pc,*rc;
	hc=(LinkNode *)malloc(sizeof(LinkNode));
	rc=hc;
	while (pa!=NULL && pb!=NULL)
	{	if (pa->data<pb->data) //将较小的结点 pa 复制到 hc 中
		{	pc=(LinkNode *)malloc(sizeof(LinkNode));
			pc->data=pa->data;
			rc->next=pc;
			rc=pc;
			pa=pa->next;
		}
		else if (pa->data>pb->data) //将较小的结点 pb 复制到 hc 中
		{	pc=(LinkNode *)malloc(sizeof(LinkNode));
			pc->data=pb->data;
			rc->next=pc;
			rc=pc;
			pb=pb->next;
		}
		else //相等的结点只复制一个到 hc 中
		{	pc=(LinkNode *)malloc(sizeof(LinkNode));
			pc->data=pa->data;
			rc->next=pc;
			rc=pc;
			pa=pa->next;
			pb=pb->next;
		}
	}
	if (pb!=NULL) pa=pb; //让 pa 指向没有扫描完的单链表结点
	while (pa!=NULL)
	{	pc=(LinkNode *)malloc(sizeof(LinkNode));
		pc->data=pa->data;
		rc->next=pc;
		rc=pc;
		pa=pa->next;
	}
	rc->next=NULL; //尾结点 next 域置为空
}

   本算法的时间复杂度为 O(m+n),其中 m、n 为单链表 ha 和 hb 中的数据结点个数。

20. 用带头结点的单链表表示整数集合,完成以下算法并分析时间复杂度:

(1)设计一个算法求两个集合A和B的差集运算即C=A-B。要求算法的空间复杂度为O(1),并释放单链表A和B中不需要的结点。
(2)并假设集合中的元素按递增排列,设计一个高效算法求两个集合A和B的差集运算即C=A-B。要求算法的空间复杂度为O(1),并释放单链表A和B中不需要的结点。

解:集合A、B、C分别用单链表ha、hb、hc存储。由于要求空间复杂度为O(1),不能采用复制方法,只能利用原来单链表中结点重组产生结果单链表。
  (1)将 ha 单链表中所有在 hb 中出现的结点删除,最后将 hb 中所有结点删除。对应的算法如下:

void Sub1(LinkNode *ha,LinkNode *hb,LinkNode *&hc)
{	LinkNode *prea=ha,*pa=ha->next,*pb,*p,*post;
	hc=ha; //将 ha 的头结点作为 hc 的头结点
	while (pa!=NULL) //删除 A 中属于 B 的结点
	{	pb=hb->next;
		while (pb!=NULL && pb->data!=pa->data)
		pb=pb->next;
		if (pb!=NULL) //pa->data 在 B 中,从 A 中删除结点 pa
		{	prea->next=pa->next;
			free(pa);
			pa=prea->next;
		}
		else
		{	prea=pa; //prea 和 pa 同步后移
			pa=pa->next;
		}
	}
	p=hb; post=hb->next; //释放 B 中所有结点
	while (post!=NULL)
	{	free(p);
		p=post;
		post=post->next;
	}
	free(p);
}

本算法的时间复杂度为 O(m×n),其中 m、n 为单链表 ha 和 hb 中的数据结点个数。
  (2)同样采用尾插法创建单链表 hc,并利用单链表的有序性,采用二路归并方法来提高算法效率,一边比较一边将不需要的结点删除。对应的算法如下:

void Sub2(LinkNode *ha,LinkNode *hb,LinkNode *&hc)
{	LinkNode *prea=ha,*pa=ha->next; //pa 扫描 ha,prea 是 pa 结点的前驱结点指针
	LinkNode *preb=hb,*pb=hb->next; //pb 扫描 hb,preb 是 pb 结点的前驱结点指针
	LinkNode *rc; //hc 的尾结点指针
	hc=ha; //ha 的头结点作为 hc 的头结点
	rc=hc;
	while (pa!=NULL && pb!=NULL)
	{	if (pa->data<pb->data) //将较小的结点 pa 链到 hc 之后
		{	rc->next=pa;
			rc=pa;
			prea=pa; //prea 和 p 同步后移
			pa=pa->next;
		}
		else if (pa->data>pb->data) //删除较大的结点 pb
		{	preb->next=pb->next;
			free(pb);
			pb=preb->next;
		}
		else //删除相等的 pa 结点和 pb 结点
		{	prea->next=pa->next;
			free(pa);
			pa=prea->next;
			preb->next=pb->next;
			free(pb);
			pb=preb->next;
		}
	}
	while (pb!=NULL) //删除 pb 余下的结点
	{	preb->next=pb->next;
		free(pb);
		pb=preb->next;
	}
	free(hb); //释放 hb 的头结点
	rc->next=NULL; //尾结点 next 域置为空
}

本算法的时间复杂度为 O(m+n),其中 m、n 为单链表 ha 和 hb 中的数据结点个数。


第 3 章 栈和队列

1. 有5个元素,其进栈次序为:ABCDE,在各种可能的出栈次序中,以元素CD最先出栈(即 C 第一个且 D 第二个出栈)的次序有哪几个?

答:要使 C 第一个且 D 第二个出栈,应是 A 进栈,B 进栈,C 进栈,C 出栈,D 进栈,D 出栈,之后可以有以下几种情况:
    (1)B出栈,A出栈,E进栈,E出栈,输出序列为CDBAE;
    (2)B出栈,E进栈,E出栈,A出栈,输出序列为CDBEA;
    (3)E进栈,E出栈,B出栈,A出栈,输出序列为CDEBA。
    所以可能的次序有:CDBAE、CDBEA、CDEBA。

2. 在一个算法中需要建立多个栈(假设 3 个栈或以上)时可以选用以下 3 种方案之一,试问这些方案之间相比各有什么优缺点?

    (1)分别用多个顺序存储空间建立多个独立的顺序栈。
    (2)多个栈共享一个顺序存储空间。
    (3)分别建立多个独立的链栈。

答:(1)优点是每个栈仅用一个顺序存储空间时,操作简单。缺点是分配空间小了,容易产生溢出,分配空间大了,容易造成浪费,各栈不能共享空间。
    (2)优点是多个栈仅用一个顺序存储空间,充分利用了存储空间,只有在整个存储空间都用完时才会产生溢出。缺点是当一个栈满时要向左、右查询有无空闲单元。如果有,则要移动元素和修改相关的栈底和栈顶指针。当接近栈满时,要查询空闲单元、移动元素和修改栈底、栈顶指针,这一过程计算复杂且十分耗时。
    (3)优点是多个链栈一般不考虑栈的溢出。缺点是栈中元素要以指针相链接,比顺序存储多占用了存储空间。

3. 在以下几种存储结构中,哪个最适合用作链栈?

    (1)带头结点的单链表
    (2)不带头结点的循环单链表
    (3)带头结点的双链表

答:栈中元素之间的逻辑关系属线性关系,可以采用单链表、循环单链表和双链表之一来存储,而栈的主要运算是进栈和出栈。
    当采用(1)时,前端作为栈顶,进栈和出栈运算的时间复杂度为 O(1)。
    当采用(2)时,前端作为栈顶,当进栈和出栈时,首结点都发生变化,还需要找到尾结点,通过修改其 next 域使其变为循环单链表,算法的时间复杂度为 O( n n n)。
    当采用(3)时,前端作为栈顶,进栈和出栈运算的时间复杂度为 O(1)。
    但单链表和双链表相比,其存储密度更高,所以本题中最适合用作链栈的是带头结点的单链表。

4. 简述以下算法的功能(假设 ElemType 为 int 类型):

void fun(ElemType a[],int n)
{	int i; ElemType e;
	SqStack *st1,*st2;
	InitStack(st1);
	InitStack(st2);
	for (i=0;i<n;i++)
		if (a[i]%2==1)
			Push(st1,a[i]);
		else
			Push(st2,a[i]);
	i=0;
	while (!StackEmpty(st1))
	{	Pop(st1,e);
		a[i++]=e;
	}
	while (!StackEmpty(st2))
	{	Pop(st2,e);
		a[i++]=e;
	}
	DestroyStack(st1);
	DestroyStack(st2);
}

答:算法的执行步骤如下:
    (1)扫描数组 a,将所有奇数进到 st1 栈中,将所有偶数进到 st2 栈中。
    (2)先将 st1 的所有元素(奇数元素)退栈,并放到数组 a 中并覆盖原有位置的元素;
再将 st2 的所有元素(偶数元素)退栈,并放到数组 a 中并覆盖原有位置的元素。
    (3)销毁两个栈 st1 和 st2。
所以本算法的功能是,利用两个栈将数组 a 中所有的奇数元素放到所有偶数元素的前面。例如,ElemType a[]={1,2,3,4,5,6},执行算法后数组 a 改变为{5,3,1,6,4,2}。

5. 简述以下算法的功能(顺序栈的元素类型为 ElemType)。

void fun(SqStack *&st,ElemType x)
{	SqStack *tmps;
	ElemType e;
	InitStack(tmps);
	while(!StackEmpty(st))
	{	Pop(st,e);
		if(e!=x) Push(tmps,d);
	}
	while (!StackEmpty(tmps))
	{	Pop(tmps,e);
		Push(st,e);
	}
	DestroyStack(tmps);
}

答:算法的执行步骤如下:
    (1)建立一个临时栈 t m p s tmps tmps 并初始化。
    (2)退栈 s t st st 中所有元素,将不为 x x x 的元素进栈到 t m p s tmps tmps 中。
    (3)退栈 t m p s tmps tmps 中所有元素,并进栈到 s t st st 中。
    (4)销毁栈 t m p s tmps tmps
    所以本算法的功能是,如果栈 s t st st 中存在元素 x x x,将其从栈中清除。例如, s t st st 栈中从栈底到栈顶为 a 、 b 、 c 、 d 、 e a、b、c、d、e abcde,执行算法 fun( s t , ′ c ′ st,'c' stc)后, s t st st 栈中从栈底到栈顶为 a 、 b 、 d 、 e a、b、d、e abde

6. 简述以下算法的功能(栈 st 和队列 qu 的元素类型均为 ElemType)。

bool fun(SqQueue *&qu,int i)
{	ElemType e;
	int j=1;
	int n=(qu->rear-qu->front+MaxSize)%MaxSize;
	if (j<1 || j>n) return false;
	for (j=1;j<=n;j++)
	{	deQueue(qu,e);
		if (j!=i)
		enQueue(qu,e);
	}
	return true;
}

答:算法的执行步骤如下:
    (1)求出队列 q u qu qu 中的元素个数 n n n。参数 i i i 错误时返回假。
    (2) q u qu qu 出队共计 n n n 次,除了第 i i i 个出队的元素外,其他出队的元素立即进队。
    (3)返回真。
    所以本算法的功能是,删除 q u qu qu 中从队头开始的第 i i i 个元素。例如, q u qu qu 中从队头到队尾的元素是 a 、 b 、 c 、 d 、 e a、b、c、d、e abcde,执行算法 fun( q u , 2 qu,2 qu2)后,qu 中从队头到队尾的元素改变为 a 、 c 、 d 、 e a、c、d、e acde

7. 什么是环形队列?采用什么方法实现环形队列?

答:当用数组表示队列时,把数组看成是一个环形的,即令数组中第一个元素紧跟在最末一个单元之后,就形成一个环形队列。环形队列解决了非环形队列中出现的“假溢出”现象。
    通常采用逻辑上求余数的方法来实现环形队列,假设数组的大小为 n,当元素下标 i i i 增 1 时,采用 i = ( i + 1 ) i=(i+1) i=(i+1)% n n n 来实现。

8. 环形队列一定优于非环形队列吗?什么情况下使用非环形队列?

答:队列主要是用于保存中间数据,而且保存的数据满足先产生先处理的特点。非环形队列可能存在数据假溢出,即队列中还有空间,可是队满的条件却成立了,为此改为环形队列,这样克服了假溢出。但并不能说环形队列一定优于非环形队列,因为环形队列中出队元素的空间可能被后来进队的元素覆盖,如果算法要求在队列操作结束后利用进队的所有元素实现某种功能,这样环形队列就不适合了,这种情况下需要使用非环形队列,例如利用非环形队列求解迷宫路径就是这种情况。

9. 假设以 I 和 O 分别表示进栈和出栈操作,栈的初态和终栈均为空,进栈和出栈的操作序列可表示为仅由 I 和 O 组成的序列。

    (1)下面所示的序列中哪些是合法的?
        A.IOIIOIOO   B.IOOIOIIO   C.IIIOIOIO   D.IIIOOIOO
    (2)通过对(1)的分析,设计一个算法判定所给的操作序列是否合法。若合法返回真;否则返回假。(假设被判定的操作序列已存入一维数组中)。

解:(1)选项A、D均合法,而选项B、C不合法。因为在选项B中,先进栈一次,立即出栈3次,这会造成栈下溢。在选项C中共进栈5次,出栈3次,栈的终态不为空。
  (2)本题使用一个链栈来判断操作序列是否合法,其中 s t r str str 为存放操作序列的字符数组, n n n 为该数组的字符个数(这里的 ElemType 类型设定为 char)。对应的算法如下:

bool judge(char str[],int n)
{	int i=0; ElemType x;
	LinkStNode *ls;
	bool flag=true;
	InitStack(ls);
	while (i<n && flag)
	{	if (str[i]=='I') //进栈
		Push(ls,str[i]);
		else if (str[i]=='O') //出栈
		{	if (StackEmpty(ls))
				flag=false; //栈空时
			else
				Pop(ls,x);
		}
		else
			flag=false; //其他值无效
		i++;
	}
	if (!StackEmpty(ls)) flag=false;
	DestroyStack(ls);
	return flag;
}

10. 假设表达式中允许包含 3 种括号:圆括号、方括号和大括号。编写一个算法判断表达式中的括号是否正确配对。

解:设置一个栈 s t st st,扫描表达式 e x p exp exp,遇到 ‘(’、‘[’ 或 ‘{’,则将其进栈;遇到 ‘)’ ,若栈顶是 ‘(’,则继续处理,否则以不配对返回假;遇到 ‘]’,若栈顶是 ‘[’,则继续处理,否则以不配对返回假;遇到 ‘}’,若栈顶是 ‘{’,则继续处理,否则以不配对返回假。在 e x p exp exp 扫描完毕,若栈不空,则以不配对返回假;否则以括号配对返回真。本题算法如下:

bool Match(char exp[],int n)
{	LinkStNode *ls;
	InitStack(ls);
	int i=0;
	ElemType e;
	bool flag=true;
	while (i<n && flag)
	{	if (exp[i]=='(' || exp[i]=='[' || exp[i]=='{')
			Push(ls,exp[i]); //遇到'('、'['或'{',则将其进栈
		if (exp[i]==')') //遇到')',若栈顶是'(',则继续处理,否则以不配对返回
		{	if (GetTop(ls,e))
			{	if (e=='(') Pop(ls,e);
				else flag=false;
			}
			else flag=false;
		}
		if (exp[i]==']') //遇到']',若栈顶是'[',则继续处理,否则以不配对返回
		{	if (GetTop(ls,e))
			{	if (e=='[') Pop(ls,e);
				else flag=false;
			}
			else flag=false;
		}
		if (exp[i]=='}') //遇到'}',若栈顶是'{',则继续处理,否则以不配对返回
		{	if (GetTop(ls,e))
			{	if (e=='{') Pop(ls,e);
				else flag=false;
			}
			else flag=false;
		}
		i++;
	}
	if (!StackEmpty(ls)) flag=false; //若栈不空,则不配对
	DestroyStack(ls);
	return flag;
}

11. 设从键盘输入一序列的字符 a 1 、 a 2 、 ⋅ ⋅ ⋅ 、 a n a_{1}、a_{2}、···、a_{n} a1a2an。设计一个算法实现这样的功能:若 a i a_{i} ai 为数字字符, a i a_{i} ai 进队,若 a i a_{i} ai 为小写字母时,将队首元素出队,若 a i a_{i} ai 为其他字符,表示输入结束。要求使用环形队列。

解:先建立一个环形队列 qu,用 while 循环接收用户输入,若输入数字字符,将其进队;若输入小写字母,出队一个元素,并输出它;若为其他字符,则退出循环。本题算法如下:

void fun()
{ 	ElemType a,e;
	SqQueue *qu; //定义队列指针
	InitQueue(qu);
	while (true)
	{	printf("输入 a:");
		scanf("%s",&a);
		if (a>='0' && a<='9') //为数字字符
		{	if (!enQueue(qu,a))
				printf(" 队列满,不能进队\n");
		}
		else if (a>='a' && a<='z') //为小写字母
		{	if (!deQueue(qu,e))
				printf(" 队列空,不能出队\n");
			else
				printf(" 出队元素:%c\n",e);
		}
		else break; //为其他字符
		}
	DestroyQueue(qu);
}

12. 设计一个算法,将一个环形队列(容量为 n,元素下标从 0 到 n-1)的元素倒置。例如,图 3.2(a)中为倒置前的队列( n n n=10),图 3.2(b)中为倒置后的队列。

在这里插入图片描述

图 3.2 一个环形队列倒置前后的状态

解:使用一个临时栈 s t st st,先将 q u qu qu 队列中所有元素出队并将其进栈 s t st st,直到队列空为止。然后初始化队列 q u qu qu(队列清空),再出栈 s t st st 的所有元素并将其进队 q u qu qu,最后销毁栈 s t st st。对应的算法如下:

void Reverse(SqQueue *&qu)
{	ElemType e;
	SqStack *st;
	InitStack(st);
	while (!QueueEmpty(qu)) //队不空时,出队并进栈
	{	deQueue(qu,e);
		Push(st,e);
	}
	InitQueue(qu); //队列初始化
	while (!StackEmpty(st)) //栈不空时,出栈并将元素入队
	{	Pop(st,e);
		enQueue(qu,e);
	}
	DestroyStack(st);
}

13. 编写一个程序,输入 n n n(由用户输入)个 10 以内的数,每输入 i i i 0 ⩽ i ⩽ 9 0 \leqslant i \leqslant 9 0i9),就把它插入到第 i 号队列中。最后把 10 个队中非空队列,按队列号从小到大的顺序串接成一条链,并输出该链的所有元素。

解:建立一个队头指针数组 quh 和队尾指针数组 q u t qut qut q u h [ i ] quh[i] quh[i] q u t [ i ] qut[i] qut[i]表示 i i i 号( 0 ⩽ i ⩽ 9 0 \leqslant i \leqslant 9 0i9)队列的队头和队尾,先将它们所有元素置为 NULL。对于输入的 x,采用尾插法将其链到 x x x 号队列中。然后按 0~9 编号的顺序把这些队列中的结点构成一个不带头结点的单链表,其
首结点指针为 head。最后输出单链表 head 的所有结点值并释放所有结点。对应的程序如下:

#include <stdio.h>
#include <malloc.h>
#define MAXQNode 10 //队列的个数
typedef struct node {
   int data;
   struct node *next;
} QNode;

void Insert(QNode *quh[], QNode *qut[], int x) //将 x 插入到相应队列中
{
   QNode *s;
   s = (QNode *) malloc(sizeof(QNode)); //创建一个结点 s
   s->data = x;
   s->next = NULL;
   if (quh[x] == NULL) //x 号队列为空队时
   {
       quh[x] = s;
       qut[x] = s;
   } else //x 号队列不空队时
   {
       qut[x]->next = s; //将 s 结点链到 qut[x]所指结点之后
       qut[x] = s; //让 qut[x]仍指向尾结点
   }
}

void Create(QNode *quh[], QNode *qut[]) //根据用户输入创建队列
{
   int n, x, i;
   printf("n:");
   scanf("%d", &n);
   for (i = 0; i < n; i++) {
       do {
           printf("输入第%d 个数:", i + 1);
           scanf("%d", &x);
       } while (x < 0 || x > 10);
       Insert(quh, qut, x);
   }
}

void DestroyList(QNode *&head) //释放单链表
{
   QNode *pre = head, *p = pre->next;
   while (p != NULL) {
       free(pre);
       pre = p;
       p = p->next;
   }
   free(pre);
}

void DispList(QNode *head) //输出单链表的所有结点值 
{
   printf("\n 输出所有元素:");
   while (head != NULL) {
       printf("%d ", head->data);
       head = head->next;
   }
   printf("\n");
}

QNode *Link(QNode *quh[], QNode *qut[]) //将非空队列链接起来并输出
{
   QNode *head = NULL, *tail; //总链表的首结点指针和尾结点指针
   int i;
   for (i = 0; i < MAXQNode; i++) //扫描所有队列
       if (quh[i] != NULL) //i 号队列不空
       {
           if (head == NULL) //若 i 号队列为第一个非空队列
           {
               head = quh[i];
               tail = qut[i];
           } else //若 i 号队列不是第一个非空队列
           {
               tail->next = quh[i];
               tail = qut[i];
           }
       }
   tail->next = NULL;
   return head;
}

int main() {
   int i;
   QNode *head;
   QNode *quh[MAXQNode], *qut[MAXQNode]; //各队列的队头 quh 和队尾指针 qut
   for (i = 0; i < MAXQNode; i++)
       quh[i] = qut[i] = NULL; //置初值空
   Create(quh, qut); //建立队列
   head = Link(quh, qut); //链接各队列产生单链表
   DispList(head); //输出单链表
   DestroyList(head); //销毁单链表
   return 1;
}

第 4 章 串

1. 串是一种特殊的线性表,请从存储和运算两方面分析它的特殊之处。

答:从存储方面看,串中每个元素是单个字符,在设计串存储结构时可以每个存储单元或者结点只存储一个字符。从运算方面看,串有连接、判串相等、求子串和子串替换等基本运算,这是线性表的基本运算中所没有的。

2. 为什么模式匹配中,BF 算法是有回溯算法,而 KMP 算法是无回溯算法?

答:设目标串为 s s s,模式串为 t t t。在 BF 算法的匹配过程中,当 t [ j ] = s [ i ] t[j]=s[i] t[j]=s[i]时,置 i + + i++ i++ j + + j++ j++;当 t [ j ] ≠ s [ i ] t[j] \neq s[i] t[j]=s[i]时,置 i = i − j + 1 i=i-j+1 i=ij+1 j = 0 j=0 j=0。从中看到,一旦两字符不等,目标串指针 i i i 会回退,所以 BF 算法是有回溯算法。在 KMP 算法的匹配过程中,当 t [ j ] = s [ i ] t[j]=s[i] t[j]=s[i]时,置 i + + i++ i++ j + + j++ j++;当 t [ j ] ≠ s [ i ] t[j] \neq s[i] t[j]=s[i] 时, i i i 不变,置 j = n e x t [ j ] j=next[j] j=next[j]。从中看到,目标串指针 i i i 不会回退,只会保持位置不变或者向前推进,所以 KMP 算法是无回溯算法。

3. 在 KMP 算法中,计算模式串的 n e x t next next 时,当 j = 0 j=0 j=0 时,为什么要置 n e x t [ 0 ] = − 1 next[0]=-1 next[0]=1

答:当模式串中 t 0 t_{0} t0 字符与目标串中某字符 s i s_{i} si 比较不相等时,此时置 n e x t [ 0 ] = − 1 next[0]=-1 next[0]=1 表示模式串中已没有字符可与目标串的 s i s_{i} si 比较,目标串当前指针 i i i 应后移至下一个字符,再和模式串的 t 0 t_{0} t0 字符进行比较。

4. KMP 算法是简单模式匹配算法的改进,以目标串 s s s=" a a b a a a b c aabaaabc aabaaabc"、模式串 t t t=" a a a b c aaabc aaabc" 为例说明的 n e x t next next 的作用。

答:模式串 t=" a a a b c aaabc aaabc"的 n e x t next next 数组值如表 4.1 所示。

j j j01234
t [ j ] t[j] t[j] a a a a a a a a a b b b c c c
n e x t [ j ] next[j] next[j]-10120
表 4.1 模式串 t 对应的 next 数组

  从 i=0,j=0 开始,当两者对应字符相等时, i + + i++ i++ j + + j++ j++,直到 i i i=2, j j j=2 时对应字符不相等。如果是简单模式匹配,下次从 i i i=1, j j j=0 开始比较。
  KMP 算法已经获得了前面字符比较的部分匹配信息,即 s [ 0..1 ] = t [ 0..1 ] s[0..1]=t[0..1] s[0..1]=t[0..1],所以 s [ 0 ] = t [ 0 ] s[0]=t[0] s[0]=t[0],而 n e x t [ 2 ] = 1 next[2]=1 next[2]=1 表明 t [ 0 ] = t [ 1 ] t[0]=t[1] t[0]=t[1],所以有 s [ 0 ] = t [ 1 ] s[0]=t[1] s[0]=t[1],这说明下次不必从 i i i=1, j j j=0 开始比较,而只需保持 i i i=2 不变,让 i i i=2 和 j j j= n e x t [ j ] next[j] next[j]=1 的字符进行比较。
   i i i=2, j j j=1 的字符比较不相等,保持 i i i=2 不变,取 j j j= n e x t [ j ] next[j] next[j]=0。
   i i i=2, j j j=0 的字符比较不相等,保持 i i i=2 不变,取 j j j= n e x t [ j ] next[j] next[j]=-1。
  当 j j j=-1 时 i i i++、 j j j++,则 i i i=3, j j j=0,对应的字符均相等,一直比较到 j j j 超界,此时表示匹配成功,返回 3。
  从中看到, n e x t [ j ] next[j] next[j]保存了部分匹配的信息,用于提高匹配效率。由于是在模式串的 j j j 位置匹配失败的, n e x t next next 也称为失效函数或失配函数。

5. 给出以下模式串的 n e x t next next 值和 n e x t v a l nextval nextval 值:

  (1)ababaa
  (2)abaabaab

答:(1)求其 next 和 nextval 值如表 4.2 所示。

j012345
t[j]ababaa
next[j]-100123
nextval[j]-10-10-13
表 4.2 模式串"ababaa"对应的 next 数组

(2)求其 next 和 nextval 值如表 4.3 所示。

j01234567
t[j]abaabaab
next[j]-10011234
nextval[j]-10-110-110
表 4.3 模式串"abaabaab"对应的 next 数组

6. 设目标为 s=“abcaabbabcabaacbacba”,模式串 t=“abcabaa”。

  (1)计算模式串 t 的 nextval 数组。
  (2)不写算法,给出利用改进的KMP算法进行模式匹配的过程。
  (3)问总共进行了多少次字符比较?

解:(1)先计算next数组,在此基础上求nextval数组,如表4.4所示。

j0123456
t[j]abcabaa
next[j]-1000121
nextval[j]-100-1021
表 4.4 计算 next 数组和 nextval 数组
(2)改进的 KMP 算法进行模式匹配的过程如图 4.2 所示。
图 4.2 改进的 KMP 算法模式匹配的过程

(3)从上述匹配过程看出:第 1 趟到第 4 趟的字符比较次数分别是 5、3、1、7,所以总共进行了 16 次字符比较。

7. 有两个顺序串 s1 和 s2,设计一个算法求一个顺序串 s3,该串中的字符是 s1 和 s2中公共字符(即两个串都包含的字符)。

解:扫描 s 1 s1 s1,对于当前字符 s 1 s1 s1. d a t a [ i ] data[i] data[i],若它在 s 2 s2 s2 中出现,则将其加入到串 s 3 s3 s3 中。最后返回 s 3 s3 s3 串。对应的算法如下:

SqString CommChar(SqString s1,SqString s2)
{	SqString s3;
	int i,j,k=0;
	for (i=0;i<s1.length;i++)
	{	for (j=0;j<s2.length;j++)
			if (s2.data[j]==s1.data[i])
				break;
		if (j<s2.length) //s1.data[i]是公共字符
		{	s3.data[k]=s1.data[i];
			k++;
		}
	}
	s3.length=k;
	return s3;
}

8. 采用顺序结构存储串,设计一个实现串通配符匹配的算法 pattern_index(),其中的通配符只有‘?’,它可以和任一个字符匹配成功。例如,pattern_index("? r e re re"," t h e r e a r e there are thereare") 返回的结果是 2。

解:采用 BF 算法的穷举法的思路,只需要增加对‘?’字符的处理功能。对应的算法如下:

int index(SqString s,SqString t)
{	int i=0,j=0;
	while (i<s.length && j<t.length)
	{	if (s.data[i]==t.data[j] || t.data[j]=='?')
		{	i++;
			j++;
		}
		else
		{	i=i-j+1;
			j=0;
		}
	}
	if (j>=t.length)
		return(i-t.length);
	else
	return(-1);
}

9. 设计一个算法,在顺序串 s 中从后向前查找子串 t,即求 t 在 s 中最后一次出现的位置。

解:采用简单模式匹配算法。如果串 s s s 的长度小于串 t t t 的长度,直接返回 -1。然后 i i i s . l e n g t h − t . l e n g t h s.length - t.length s.lengtht.length 到 0 循环:再对于 i i i 的每次取值循环:置 j = i j=i j=i k = 0 k=0 k=0,若 s . d a t a [ j ] = = t . d a t a [ k ] s.data[j]==t.data[k] s.data[j]==t.data[k],则 j j j++, k k k++。循环中当 k = = t . l e n g t h k == t.length k==t.length 为真时,表示找到子串,返回物理下标 i i i。所有循环结束后都没有返回,表示串 t t t 不是串 s s s 的子串则返回-1。对应的算法如下:

int LastPos1(SqString s,SqString t)
{	int i,j,k;
	if (s.length-t.length<0)
	return -1;
	for (i=s.length-t.length;i>=0;i--)
	{	for (j=i,k=0;j<s.length && k<t.length && s.data[j]==t.data[k];j++,k++);
		if (k==t.length)
		return i;
	}
	return -1;
}

10. 设计一个算法,判断一个字符串 s 是否形如"序列 1@为序列 2"模式的字符序列,其中序列 1 和序列 2 都不含有’@'字符,且序列 2 是序列 1 的逆序列。例如" a + b a+b a+b@ b + a b+a b+a"属于该模式的字符序列,而"1+3@3-1"则不是。

解:建立一个临时栈 s t st st 并初始化为空,其元素为 char 类型。置匹配标志 f l a g flag flag 为 true。扫描顺序串 s s s 的字符,将’@‘之前的字符进栈。继续扫描顺序串 s s s 中’@'之后的字符,每扫描一个字符 e e e,退栈一个字符 x x x,若退栈时溢出或 e e e 不等于 x x x,则置 f l a g flag flag 为 false。循环结束后,若栈不空,置 f l a g flag flag 为 false。最后销毁栈 s t st st 并返回 f l a g flag flag。对应的算法如下:

bool symm(SqString s) {
    int i = 0;
    char e, x;
    bool flag = true;
    SqStack *st;
    InitStack(st);
    while (i < s.length) //将'@'之前的字符进栈
    {
        e = s.data[i];
        if (e != '@')
            Push(st, e);
        else
            break;
        i++;
    }
    i++; //跳过@字符
    while (i < s.length && flag) {
        e = s.data[i];
        if (!Pop(st, x)) flag = false;
        if (e != x) flag = false;
        i++;
    }
    if (!StackEmpty(st)) flag = false;
    DestroyStack(st);
    return flag;
}

11. 采用顺序结构存储串,设计一个算法求串 s s s 中出现的第一个最长重复子串的下标和长度。

解:采用简单模式匹配算法的思路,先给最长重复子串的起始下标 m a x i maxi maxi 和长度 m a x l e n maxlen maxlen 均赋值为 0。用 i i i 扫描串 s s s,对于当前字符 s i s_{i} si,判定其后是否有相同的字符,若有记为 s j s_{j} sj,再判定 s i + 1 s_{i+1} si+1 是否等于 s j + 1 s_{j+1} sj+1 s i + 2 s_{i+2} si+2 是否等于 s j + 2 s_{j+2} sj+2,…,直至找到一个不同的字符为止,即找到一个重复出现的子串,把其起始下标 i i i 与长度 l e n len len 记下来,将 l e n len len m a x l e n maxlen maxlen 相比较,保留较长的子串 m a x i maxi maxi m a x l e n maxlen maxlen。再从 s j + l e n s_{j+len} sj+len 之后查找重复子串。然后对于 s i + 1 s_{i+1} si+1之后的字符采用上述过程。循环结束后, m a x i maxi maxi m a x l e n maxlen maxlen 保存最长重复子串的起始下标与长度,将其复制到串 t t t 中。
  对应的算法如下:

void maxsubstr(SqString s, SqString &t) {
    int maxi = 0, maxlen = 0, len, i, j, k;
    i = 0;
    while (i < s.length) //从下标为 i 的字符开始
    {
        j = i + 1; //从 i 的下一个位置开始找重复子串
        while (j < s.length) {
            if (s.data[i] == s.data[j]) //找一个子串,其起始下标为 i,长度为 len
            {
                len = 1;
                for (k = 1; s.data[i + k] == s.data[j + k]; k++)
                    len++;
                if (len > maxlen) //将较大长度者赋给 maxi 与 maxlen
                {
                    maxi = i;
                    maxlen = len;
                }
                j += len;
            } else j++;
        }
        i++; //继续扫描第 i 字符之后的字符
    }
    t.length = maxlen; //将最长重复子串赋给 t
    for (i = 0; i < maxlen; i++)
        t.data[i] = s.data[maxi + i];
}

12. 用带头结点的单链表表示链串,每个结点存放一个字符。设计一个算法,将链串 s s s 中所有值为 x x x 的字符删除。要求算法的时间复杂度均为 O( n n n),空间复杂度为 O(1)。

解:让 p r e pre pre 指向链串头结点, p p p 指向首结点。当 p p p 不为空时循环:当 p p p -> d a t a data data== x x x 时,通过 p r e pre pre 结点删除 p p p 结点,再让 p p p 指向 p r e pre pre 结点的后继结点;否则让 p r e pre pre p p p 同步后移一个结点。对应的算法如下:

void deleteall(LinkStrNode *&s, char x) {
    LinkStrNode *pre = s, *p = s->next;
    while (p != NULL) {
        if (p->data == x) {
            pre->next = p->next;
            free(p);
            p = pre->next;
        } else {
            pre = p; //pre、p 同步后移
            p = p->next;
        }
    }
}

第 5 章 递归

1. 有以下递归函数:

void fun(int n)
{	if (n==1)
		printf("a:%d\n",n);
	else
	{	printf("b:%d\n",n);
		fun(n-1);
		printf("c:%d\n",n);
	}
}

分析调用 fun(5)的输出结果。

解:调用递归函数 fun(5)时,先递推到递归出口,然后求值。这里的递归出口语句是 printf(“a:%d\n”,n),递推时执行的语句是 printf(“b:%d\n”,n),求值时执行的语句是 printf(“c:%d\n”,n)。调用 fun(5) 的输出结果如下:
  b:5
  b:4
  b:3
  b:2
  a:1
  c:2
  c:3
  c:4
  c:5

2. 已知 A[0…n-1]为整数数组,设计一个递归算法求这 n 个元素的平均值。

解:设avg(A,i)返回A[0…i]共i+1个元素的平均值,则递归模型如下:
avg(A,i)=A[0]              当i=0
avg(A,i)=(avg(A,i-1)*i+A[i])/(i+1)   其他情况
对应的递归算法如下:

float avg(int A[],int i)
{	if (i==0)
		return(A[0]);
	else
		return((avg(A,i-1)*i+A[i])/(i+1));
}

A A A[ n n n]中 n n n 个元素平均值的调用方式为:avg( A A A n n n-1)。

3. 设计一个算法求正整数 n 的位数。

解:设 f ( n f(n f(n)为整数 n n n 的位数,其递归模型如下:
f ( n ) f(n) f(n)=1          当 n n n<10 时
f ( n ) = f ( n / 10 ) + 1 f(n)=f(n/10)+1 f(n)=f(n/10)+1    其他情况
对应的递归算法如下:

int fun(int n)
{	if (n<10)
		return 1;
	else
		return fun(n/10)+1;
}

4. 上楼可以一步上一阶,也可以一步上两阶。设计一个递归算法,计算共有多少种不同的走法。

解:设 f ( n ) f(n) f(n) 表示 n n n 阶楼梯的不同的走法数,显然 f ( 1 ) = 1 f(1)=1 f(1)=1 f ( 2 ) = 2 f(2)=2 f(2)=2(两阶有一步一步走和两步走 2 种走法)。 f ( n − 1 ) f(n-1) f(n1) 表示 n − 1 n-1 n1 阶楼梯的不同的走法数, f ( n − 2 ) f(n-2) f(n2) 表示 n − 2 n-2 n2 阶楼梯的不同的走法数,对于 n n n 阶楼梯,第 1 步上一阶有个 f ( n − 1 ) f(n-1) f(n1) 种走法,第 1 步上两阶有个 f ( n − 2 ) f(n-2) f(n2) 种走法,则 f ( n ) = f ( n − 1 ) + f ( n − 2 ) f(n)= f(n-1)+ f(n-2) f(n)=f(n1)+f(n2)。对应的递归算法如下:

int fun(int n)
{	if (n==1 || n==2)
		return n;
	else
		return fun(n-1)+fun(n-2);
}

5. 设计一个递归算法,利用顺序串的基本运算求串 s 的逆串。

解:经分析,求逆串的递归模型如下:
f ( s ) = s f(s) = s f(s)=s                                     若 s = Φ s=Φ s=Φ
f ( s ) = C o n c a t ( f ( S u b S t r ( s , 2 , S t r L e n g t h ( s ) − 1 ) ) f(s) = Concat(f(SubStr(s,2,StrLength(s)-1)) f(s)=Concat(f(SubStr(s2StrLength(s)1)),SubStr(s,1,1))   其他情况
  递归思路是:对于 s s s=“ s 1 s 2 ⋅ ⋅ ⋅ s n s_{1}s_{2}···s_{n} s1s2sn”的串,假设“ s 2 s 3 ⋅ ⋅ ⋅ s n s_{2}s_{3}···s_{n} s2s3sn”已求出其逆串即 f f f(SubStr( s s s,2,StrLength( s s s)-1)),再将 s 1 s_{1} s1(为 SubStr( s s s,1,1))单个字符构成的串连接到最后即得到 s s s 的逆串。对应的递归算法如下:

#include "sqstring.cpp" //顺序串的基本运算算法
SqString invert(SqString s)
{	SqString s1,s2;
	if (StrLength(s)>0)
	{	s1=invert(SubStr(s,2,StrLength(s)-1));
		s2=Concat(s1,SubStr(s,1,1));
	}
	else
		StrCopy(s2,s);
	return s2;
}

6. 设有一个不带表头结点的单链表 L L L,设计一个递归算法 count( L L L)求以 L L L 为首结点指针的单链表的结点个数。

解:对应的递归算法如下:

int count(LinkNode *L)
{	if (L==NULL)
		return 0;
	else
		return count(L->next)+1;
}

7. 设有一个不带表头结点的单链表 L L L,设计两个递归算法,traverse( L L L)正向输出单链表 L L L 的所有结点值,traverseR( L L L)反向输出单链表 L L L 的所有结点值。

解:对应的递归算法如下:

void traverse(LinkNode *L)
{	if (L==NULL) return;
	printf("%d ",L->data);
	traverse(L->next);
}

void traverseR(LinkNode *L)
{	if (L==NULL) return;
	traverseR(L->next);
	printf("%d ",L->data);
}

8. 设有一个不带表头结点的单链表 L L L,设计两个递归算法,del( L L L x x x)删除单链表 L L L 中第一个值为 x x x 的结点,delall( L L L x x x)删除单链表 L L L 中所有值为 x x x 的结点。

解:对应的递归算法如下:

void del(LinkNode *&L, ElemType x) {
    LinkNode *t;
    if (L == NULL) return;
    if (L->data == x) 
    {   t = L;
        L = L->next;
        free(t);
        return;
    }
    del(L->next, x);
}

void delall(LinkNode *&L, ElemType x) {
    LinkNode *t;
    if (L == NULL) return;
    if (L->data == x) {
        t = L;
        L = L->next;
        free(t);
    }
    delall(L->next, x);
}

9. 设有一个不带表头结点的单链表 L L L,设计两个递归算法,maxnode( L L L)返回单链表 L L L中最大结点值,minnodel( L L L)返回单链表 L L L 中最小结点值。

解:对应的递归算法如下:

ElemType maxnode(LinkNode *L) {
    ElemType max;
    if (L->next == NULL)
        return L->data;
    max = maxnode(L->next);
    if (max > L->data) return max;
    else return L->data;
}

ElemType minnode(LinkNode *L) {
    ElemType min;
    if (L->next == NULL)
        return L->data;
    min = minnode(L->next);
    if (min > L->data) return L->data;
    else return min;
}

10. 设计一个模式匹配算法,其中模板串 t t t 含有通配符 ‘*’ ,它可以和任意子串匹配。对于目标串 s s s,求其中匹配模板 t t t 的一个子串的位置(’*'不能出现在 t t t 的最开头和末尾)。

解:采用 BF 模式匹配的思路,当是 s [ i ] s[i] s[i] t [ j ] t[j] t[j]比较,而 t [ j ] t[j] t[j]为’*‘时,取出 s s s 中对应’*‘的字符之后的所有字符构成的字符串,即 SubStr( s s s i i i+2, s . l e n g t h s.length s.length- i i i-1),其中 i i i+2 是 s s s 中对应’*‘字符后面一个字符的逻辑序号。再取出 t t t 中’*'字符后面的所有字符构成的字符串,即SubStr( t t t j j j+2, t . l e n g t h t.length t.length- j j j-1),递归对它们进行匹配,若返回值大于-1,表示匹配成功,返回 i i i。否则返回-1。对应的递归算法如下:

#include "sqstring.cpp" //顺序串的基本运算算法
findpat(SqString s,SqString t)
{	int i=0,j=0,k;
	while (i<s.length && j<t.length)
	{	if (t.data[j]=='*')
		{	k=findpat(SubStr(s,i+2,s.length-i-1),SubStr(t,j+2,t.length-j-1));
			j++;
			if (k>-1)
				return i-1;
			else
				return -1;
		}
		else if (s.data[i]==t.data[j])
		{	i++;
			j++;
		}
		else
		{	i=i-j+1;
			j=0;
		}
	}
	if (j>=t.length)
		return i-1;
	else
		return -1;
}

第 6 章 数组和广义表

1. 如何理解数组是线性表的推广。

答:数组可以看成是线性表在下述含义上的扩展:线性表中的数据元素本身也是一个线性表。在 d d d d d d ⩾ \geqslant 1)维数组中的每个数据元素都受着 d d d 个关系的约束,在每个关系中,数据元素都有一个后继元素(除最后一个元素外)和一个前驱元素(除第一个元素外)。
  因此,这 d d d 个关系中的任一关系,就其单个关系而言,仍是线性关系。例如, m m m× n n n 的二维数组的形式化定义如下:
   A A A=( D D D R R R)
  其中:
   D D D = { a i j a_{ij} aij | 0 ⩽ i ⩽ m \leqslant i \leqslant m im - 1,0 ⩽ j ⩽ n \leqslant j \leqslant n jn - 1} //数据元素的集合
   R R R = { ROW,COL }
  ROW = { < a i , j a_{i,j} ai,j a i + 1 , j a_{i+1,j} ai+1,j> | 0 ⩽ i ⩽ m \leqslant i \leqslant m im - 2,0 ⩽ j ⩽ n \leqslant j \leqslant n jn - 1} //行关系
  COL = { < a i , j a_{i,j} ai,j a i , j + 1 a_{i,j+1} ai,j+1> | 0 ⩽ i ⩽ m \leqslant i \leqslant m im - 1,0 ⩽ j ⩽ n \leqslant j \leqslant n jn - 2} //列关系

2. 有三维数组 a[0…7,0…8,0…9]采用按行序优先存储,数组的起始地址是 1000,每个元素占用 2 个字节,试给出下面结果:

  (1)元素 a 1 , 6 , 8 a_{1,6,8} a1,6,8的起始地址。
  (2)数组 a a a 所占用的存储空间。

答:(1)LOC( a 1 , 6 , 8 a_{1,6,8} a1,6,8)=LOC( a 0 , 0 , 0 a_{0,0,0} a0,0,0) + [1×9×10+6×10+8]×2 = 1000+316 = 1316。
(2)数组 a a a 所占用存储空间 = 8×9×10×2=1440 字节。

3. 如果某个一维数组 A A A 的元素个数 n n n 很大,存在大量重复的元素,且所有元素值相同的元素紧挨在一起,请设计一种压缩存储方式使得存储空间更节省。

答:设数组的元素类型为 ElemType,采用一种结构体数组 B B B 来实现压缩存储,该结构体数组的元素类型如下:

struct
{	ElemType data; //元素值
	int length; //重复元素的个数
}

如数组 A[]={1,1,1,5,5,5,5,3,3,3,3,4,4,4,4,4,4},共有 17 个元素,对应的压缩存储 B B B 为:{1,3},{5,4},{3,4},{4,6}}。从中看出,如果重复元素越多,采用这种压缩存储方式越节省存储空间。

4. 一个 n 阶对称矩阵 A 采用压缩存储在一维数组 B 中,则 B 包含多少个元素?

答:通常 B 中包含 n 阶对称矩阵 A 的下三角和主对角线上的元素,其元素个数为 1 + 2 + ⋅ ⋅ ⋅ + n = n ( n + 1 ) 2 1+2+···+n= \frac{n(n+1)}{2} 1+2++n=2n(n+1) 。所以 B 包含 n ( n + 1 ) 2 \frac{n(n+1)}{2} 2n(n+1) 个元素。

5. 设 n n n× n n n 的上三角矩阵 A A A[0… n n n-1,0… n n n-1]已压缩到一维数组 B B B[0… m m m]中,若按列为主序存储,则 A A A[i][j]对应的 B B B 中存储位置 k k k 为多少,给出推导过程。

答:对于上三角部分或者主对角中的元素 A A A[ i i i][ j j j] ( i ⩽ j i \leqslant j ij),按列为主序存储时,前面有 0~ j j j-1 共 j j j 列,第 0 列有 1 个元素,第 1 列有 2 个元素,…,第 j j j-1 列有 j j j 个元素,所以这 j j j 列的元素个数=1+2+…+ j j j= j j j( j j j+1)/2;在第 j j j 列中, A A A[ i i i][ j j j]元素前有 A A A[0… i i i-1, j j j]共 i i i 个元素。所以 A A A[ i i i][ j j j]元素前有 j j j( j j j+1)/2+ i i i 个元素,而 B B B 的下标从 0 开始,所以 A A A[ i i i][ j j j]在 B B B 中的位置 k k k= j j j( j j j+1)/2+ i i i

6. 利用三元组存储任意稀疏数组 A 时,假设其中一个元素和一个整数占用的存储空间相同,问在什么条件下才能节省存储空间。

答:设稀疏矩阵 A A A t t t 个非零元素,加上行数 rows、列数 cols 和非零元素个数 nums (也算一个三元组),那么三元组顺序表的存储空间总数为 3( t t t+1),若用二维数组存储时占用存储空间总数为 m m m× n n n,只有当 3( t t t+1)< m m m× n n n t t t< m m m× n n n/3-1 时,采用三元组存储才能节省存
储空间。

7. 用十字链表存储一个有 k 个非 0 元素的 m×n 的稀疏矩阵,则其总的节点数为多少?

答:该十字链表有一个十字链表表头节点,MAX( m m m n n n)个行、列表头节点。另外,每个非 0 元素对应一个节点,即 k k k 个元素节点。所以共有 MAX( m m m n n n)+ k k k+1 个节点。

8. 求下列广义表运算的结果

(1) h e a d [ ( x , y , z ) ] head[(x,y,z)] head[(xyz)]
(2) t a i l [ ( ( a , b ) , ( x , y ) ) ] tail[((a,b),(x,y))] tail[((ab)(xy))]
  注意:为了清楚起见,在括号层次较多时,将 h e a d head head t a i l tail tail 的参数用中括号表示。例如 h e a d [ G ] head[G] head[G] t a i l [ G ] tail[G] tail[G] 分别表示求广义表 G G G 的表头和表尾。$

答:(1) h e a d [ ( x , y , z ) ] = x head[(x,y,z)]=x head[(xyz)]=x
  (2) t a i l [ ( ( a , b ) , ( x , y ) ) ] = ( ( x , y ) ) tail[((a,b),(x,y))]=((x,y)) tail[((ab)(xy))]=((xy))

9. 设定二维整数数组 B B B[0… m m m-1,0… n n n-1]的数据在行、列方向上都按从小到大的顺序排序,且整型变量 x x x 中的数据在 B B B 中存在。设计一个算法,找出一对满足 B B B[ i i i][ j j j]= x x x i i i j j j 值。要求比较次数不超过 m m m+ n n n

解:从二维数组 B B B 的右上角的元素开始比较。每次比较有三种可能的结果:若相等,则比较结束;若 x x x 大于右上角元素,则可断定二维数组的最上面一行肯定没有与 x x x 相等的数据,下次比较时搜索范围可减少一行;若 x x x 小于右上角元素,则可断定二维数组的最右面一列肯定不包含与 x x x 相等的数据,下次比较时可把最右一列剔除出搜索范围。这样,每次比较可使搜索范围减少一行或一列,最多经过 m m m+ n n n 次比较就可找到要求的与 x x x 相等的元素。对应的程序如下:

#include <stdio.h>
#define M 3 //行数常量
#define N 4 //列数常量

void Find(int B[M][N], int x, int &i, int &j) {
    i = 0;
    j = N - 1;
    while (B[i][j] != x)
        if (B[i][j] < x) i++;
        else j--;
}

int main() {
    int i, j, x = 11;
    int B[M][N] = {{1, 2,  3,  4},
                   {5, 6,  7,  8},
                   {9, 10, 11, 12}};
    Find(B, x, i, j);
    printf("B[%d][%d]=%d\n", i, j, x);
    return 1;
}

10. 设计一个算法,计算一个三元组表表示的稀疏矩阵的对角线元素之和。

解:对于稀疏矩阵三元组表 a a a,从 a . d a t a [ 0 ] a.data[0] a.data[0]开始查看,若其行号等于列号,表示是一个对角线上的元素,则进行累加,最后返回累加值。算法如下:

bool diagonal(TSMatrix a, ElemType &sum) {
    sum = 0;
    if (a.rows != a.cols) //行号不等于列号,返回 false
    {
        printf("不是对角矩阵\n");
        return false;
    }
    for (int i = 0; i < a.nums; i++)
        if (a.data[i].r == a.data[i].c) //行号等于列号
            sum += a.data[i].d;
    return true;
}

11. 设计一个算法 Same( g 1 g1 g1 g 2 g2 g2),判断两个广义表 g 1 g1 g1 g 2 g2 g2 是否相同。

解:判断广义表是否相同过程是,若 g 1 g1 g1 g 2 g2 g2 均为 NULL,则返回 true;若 g 1 g1 g1 g 2 g2 g2 中一个为 NULL,另一不为 NULL,则返回 false;若 g 1 g1 g1 g 2 g2 g2 均不为 NULL,若同为原子且原子值不相等,则返回 false,若同为原子且原子值相等,则返回 Same( g 1 g1 g1-> l i n k link link g 2 g2 g2-> l i n k link link),若同为子表,则返回 Same( g 1 g1 g1-> v a l . s u b l i s t val.sublist val.sublist g 2 g2 g2-> v a l . s u b l i s t val.sublist val.sublist) & Same( g 1 g1 g1-> l i n k link link g 2 g2 g2-> l i n k link link)的结果,若一个为原子另一个为子表,则返回 false。对应的算法如下:

bool Same(GLNode *g1, GLNode *g2) {
    if (g1 == NULL && g2 == NULL) //均为 NULL 的情况
        return true; //返回真
    else if (g1 == NULL || g2 == NULL) //一个为 NULL,另一不为 NULL 的情况
        return false; //返回假
    else //均不空的情况
    {
        if (g1->tag == 0 && g2->tag == 0) //均为原子的情况
        {
            if (g1->val.data != g2->val.data) //原子不相等
                return false; //返回假
            return (Same(g1->link, g2->link)); //返回兄弟比较的结果
        }
        else if (g1->tag == 1 && g2->tag == 1) //均为子表的情况
            return (Same(g1->val.sublist, g2->val.sublist)
                    & Same(g1->link, g2->link));
        else //一个为原子,另一为子表的情况
            return false; //返回假
    }
}

第 7 章 树和二叉树

1. 有一棵树的括号表示为 A(B,C(E,F(G)),D),回答下面的问题:

  (1)指出树的根结点。
  (2)指出棵树的所有叶子结点。
  (3)结点 C 的度是多少?
  (4)这棵树的度为多少?
  (5)这棵树的高度是多少?
  (6)结点 C 的孩子结点是哪些?
  (7)结点 C 的双亲结点是谁?

答:该树对应的树形表示如图 7.2 所示。
  (1)这棵树的根结点是 A。
  (2)这棵树的叶子结点是 B、E、G、D。
  (3)结点 C 的度是 2。
  (4)这棵树的度为 3。
  (5)这棵树的高度是 4。
  (6)结点 C 的孩子结点是 E、F。
  (7)结点 C 的双亲结点是 A。

7.2 一棵树

2. 若一棵度为 4 的树中度为 2、3、4 的结点个数分别为 3、2、2,则该树的叶子结点的个数是多少?

答:结点总数 n = n 0 + n 1 + n 2 + n 3 + n 4 n=n_{0}+n_{1}+n_{2}+n_{3}+n_{4} n=n0+n1+n2+n3+n4,又由于除根结点外,每个结点都对应一个分支,所以总的分支数等于 n n n-1。而一个度为 i i i(0 ⩽ i ⩽ \leqslant i \leqslant i 4)的结点的分支数为 i i i,所以有: 总 分 支 数 = n − 1 = 1 × n 1 + 2 × n 2 + 3 × n 3 + 4 × n 4 总分支数 = n-1=1×n_{1}+2×n_{2}+3×n_{3}+4×n_{4} =n1=1×n1+2×n2+3×n3+4×n4。综合两式得: n 0 = n 2 + 2 n 3 + 3 n 4 + 1 = 3 + 2 × 2 + 3 × 2 = 14 n_{0}=n_{2}+2n_{3}+3n_{4}+1=3+2×2+3×2=14 n0=n2+2n3+3n4+1=3+2×2+3×2=14

3. 为了实现以下各种功能,其中 x x x 结点表示该结点的位置,给出树的最适合的存储结构:

  (1)求 x 和 y 结点的最近祖先结点。
  (2)求 x 结点的所有子孙。
  (3)求根结点到 x 结点的路径。
  (4)求 x 结点的所有右边兄弟结点。
  (5)判断 x 结点是否是叶子结点。
  (6)求 x 结点的所有孩子。

答:(1)双亲存储结构。
  (2)孩子链存储结构。
  (3)双亲存储结构。
  (4)孩子兄弟链存储结构。
  (5)孩子链存储结构。
  (6)孩子链存储结构。

4. 设二叉树 b t bt bt 的一种存储结构如表 7.1 所示。其中, b t bt bt 为树根结点指针, l c h i l d lchild lchild r c h i l d rchild rchild 分别为结点的左、右孩子指针域,在这里使用结点编号作为指针域值,0 表示指针域值为空; d a t a data data 为结点的数据域。请完成下列各题:

  (1)画出二叉树 bt 的树形表示。
  (2)写出按先序、中序和后序遍历二叉树 bt 所得到的结点序列。
  (3)画出二叉树 bt 的后序线索树(不带头结点)。
在这里插入图片描述

表7.1 二叉树bt的一种存储结构

答:(1)二叉树 b t bt bt 的树形表示如图 7.3 所示。

图 7.3 二叉树 bt 的逻辑结构

(2)先序序列: a b c e d f h g i j abcedfhgij abcedfhgij
    中序序列: e c b h f d j i g a ecbhfdjiga ecbhfdjiga
    后序序列: e c h f j i g d b a echfjigdba echfjigdba
(3)二叉树 bt 的后序序列为 e c h f j i g d b a echfjigdba echfjigdba,则后序线索树如图 7.4 所示。

图 7.4 二叉树 bt 的后序线索化树

5. 含有 60 个叶子结点的二叉树的最小高度是多少?

答:在该二叉树中, n 0 = 60 n_{0}=60 n0=60 n 2 = n 0 − 1 = 59 n_{2}=n_{0}-1=59 n2=n01=59 n = n 0 + n 1 + n 2 = 119 + n 1 n=n_{0}+n_{1}+n_{2}=119+n_{1} n=n0+n1+n2=119+n1,当 n 1 = 0 n_{1}=0 n1=0 且为完全二叉树时高度最小,此时高度 h = log ⁡ 2 ( n + 1 ) = log ⁡ 2 120 = 7 h = \log2\left ( n+1\frac{}{} \right ) = \log_2 120 =7 h=log2(n+1)=log2120=7。所以含有 60 个叶子结点的二叉树的最小高度是 7。

6. 已知一棵完全二叉树的第 6 层(设根结点为第 1 层)有 8 个叶子结点,则该完全二叉树的结点个数最多是多少?最少是多少?

答:完全二叉树的叶子结点只能在最下面两层,所以结点最多的情况是第 6 层为倒数第 2 层,即 1~6 层构成一棵满二叉树,其结点总数为 2 6 − 1 = 63 2^6-1=63 261=63。其中第 6 层有 2 5 = 32 2^5=32 25=32 个结点,含 8 个叶子结点,则另外有 32 − 8 = 24 32-8=24 328=24 个非叶子结点,它们中每个结点有两个孩子结点(均为第 7 层的叶子结点),计为 48 个叶子结点。这样 最 多 的 结 点 个 数 = 63 + 48 = 111 最多的结点个数=63+48=111 =63+48=111
  结点最少的情况是第 6 层为最下层,即 1~5 层构成一棵满二叉树,其结点总数为 2 5 − 1 = 31 2^5-1=31 251=31,再加上第 6 层的结点,总计 31 + 8 = 39 31+8=39 31+8=39。这样最少的结点个数为 39。

7. 已知一棵满二叉树的结点个数为 20~40 之间,此二叉树的叶子结点有多少个?

答:一棵高度为 h 的满二叉树的结点个数为 2 h − 1 2^h-1 2h1,有:20 ⩽ 2 h − 1 ⩽ \leqslant 2^h-1 \leqslant 2h1 40。
  则 h = 5 h=5 h=5,满二叉树中叶子结点均集中在最底层,所以 叶 子 结 点 个 数 = 2 5 − 1 = 16 个 叶子结点个数=2^5-1=16 个 =251=16

8. 已知一棵二叉树的中序序列为 c b e d a h g i j f cbedahgijf cbedahgijf,后序序列为 c e d b h j i g f a cedbhjigfa cedbhjigfa,给出该二叉树树形表示。

答:该二叉树的构造过程和二叉树如图 7.5 所示。

图 7.5 二叉树的构造过程

9. 给定 5 个字符 a a a f f f,它们的权值集合 W W W={2,3,4,7,8,9},试构造关于 W W W 的一棵哈夫曼树,求其带权路径长度 W P L WPL WPL 和各个字符的哈夫曼树编码。

答 : 由 权 值 集 合 W W W 构 建 的 哈 夫 曼 树 如 图 7.6 所示。 其 带 权 路 径 长 度 W P L = ( 9 + 7 + 8 ) × 2 + 4 × 3 + ( 2 + 3 ) × 4 = 80 WPL=(9+7+8)×2+4×3+(2+3)×4=80 WPL=(9+7+8)×2+4×3+(2+3)×4=80
  各个字符的哈夫曼树编码: a : 0000 , b : 0001 , c : 001 , d : 10 , e : 11 , f : 01 a:0000,b:0001,c:001,d:10,e:11,f:01 a0000b0001c001d10e11f01

图 7.5 二叉树的构造过程
图7.6 一棵哈夫曼树

10. 假设二叉树中每个结点的值为单个字符,设计一个算法将一棵以二叉链方式存储的二叉树 b b b 转换成对应的顺序存储结构 a a a

解:设二叉树的顺序存储结构类型为 SqBTree,先将顺序存储结构 a a a 中所有元素置为 ‘#’(表示空结点)。将 b b b 转换成 a a a 的递归模型如下:
f ( b , a , i ) f(b,a,i) f(bai) = a [ i ] a[i] a[i]=’#’;             当 b b b=NULL
f ( b , a , i ) f(b,a,i) f(bai) = 由 b b b 结点 data 域值建立 a [ i ] a[i] a[i]元素; 其他情况
f ( b − > l c h i l d , a , 2 ∗ i ) f(b->lchild,a,2*i) f(b>lchilda2i)
f ( b − > r c h i l d , a , 2 ∗ i + 1 ) f(b->rchild,a,2*i+1) f(b>rchilda2i+1)
调用方式为: f ( b , a , 1 ) f(b,a,1) f(ba1) a a a 的下标从 1 开始)。对应的算法如下:

void Ctree(BTNode *b,SqBTree a,int i)
{	if (b!=NULL)
	{	a[i]=b->data;
		Ctree(b->lchild,a,2*i);
		Ctree(b->rchild,a,2*i+1);
	}
	else a[i]='#';
}

11. 假设二叉树中每个结点值为单个字符,采用顺序存储结构存储。设计一个算法,求二叉树 t t t 中的叶子结点个数。

解:用 i i i 遍历所有的结点,当 i i i 大于等于 MaxSize 时,返回 0。当 t [ i ] t[i] t[i]是空结点时返回 0;当 t [ i ] t[i] t[i] 是非空结点时,若它为叶子结点,num 增 1;否则递归调用 num1=LeftNode( t t t 2 ∗ i 2*i 2i) 求出左子树的叶子结点个数 num1,再递归调用 num2=LeftNode( t t t 2 ∗ i + 1 2*i+1 2i+1)求出右子树的叶子结点个数 num2,置 num+=num1+num2。最后返回 num。对应的算法如下:

int LeftNode(SqBTree t,int i)
{	//i 的初值为 1
	int num1,num2,num=0;
	if (i<MaxSize)
	{	if (t[i]!='#')
		{	if (t[2*i]=='#' && t[2*i+1]=='#')
				num++; //叶子结点个数增 1
			else
			{	num1=LeftNode(t,2*i);
				num2=LeftNode(t,2*i+1);
				num+=num1+num2;
			}
			return num;
		}
		else return 0;
	}
	else return 0;
}

12. 假设二叉树中每个结点值为单个字符,采用二叉链存储结构存储。设计一个算法计算一棵给定二叉树 b 中的所有单分支结点个数。

解:计算一棵二叉树的所有单分支结点个数的递归模型 f ( b ) f(b) f(b)如下:
f ( b ) = 0 f(b)=0 f(b)=0                     若 b=NULL
f ( b ) = f ( b → l c h i l d ) + f ( b → r c h i l d ) + 1 f(b)=f(b \to lchild)+f(b \to rchild)+1 f(b)=f(blchild)+f(brchild)+1   若 b 结点为单分支
f ( b ) = f ( b → l c h i l d ) + f ( b → r c h i l d ) f(b)=f(b \to lchild)+f(b \to rchild) f(b)=f(blchild)+f(brchild)     其他情况
对应的算法如下:

int SSonNodes(BTNode *b)
{	int num1,num2,n;
	if (b==NULL)
		return 0;
	else if ((b->lchild==NULL && b->rchild!=NULL) || (b->lchild!=NULL && b->rchild==NULL))
		n=1; //为单分支结点
	else
		n=0; //其他结点
	num1=SSonNodes(b->lchild); //递归求左子树中单分支结点数
	num2=SSonNodes(b->rchild); //递归求右子树中单分支结点数
	return (num1+num2+n);
}

上述算法采用的是先序遍历的思路。

13. 假设二叉树中每个结点值为单个字符,采用二叉链存储结构存储。设计一个算法求二叉树 b 中最小值的结点值。

解:设 f ( b , m i n ) f(b,min) f(bmin) 是在二叉树 b b b 中寻找最小结点值 m i n min min,其递归模型如下:
f ( b , m i n ) f(b,min) f(bmin) = 不做任何事件                   若 b b b=NULL
f ( b , m i n ) f(b,min) f(bmin) = 当 b − > d a t a < m i n b->data<min b>data<min 时置 m i n = b − > d a t a min=b->data min=b>data;   其他情况
f ( b − > l c h i l d , m i n ) ; f ( b − > r c h i l d , m i n ) ; f(b->lchild,min); f(b->rchild,min); f(b>lchildmin);f(b>rchildmin);
对应的算法如下:

void FindMinNode(BTNode *b,char &min)
{	if (b->data<min)
	min=b->data;
	FindMinNode(b->lchild,min); //在左子树中找最小结点值
	FindMinNode(b->rchild,min); //在右子树中找最小结点值
}

void MinNode(BTNode *b) //输出最小结点值
{	if (b!=NULL)
	{	char min=b->data;
		FindMinNode(b,min);
		printf("Min=%c\n",min);
	}
}

14. 假设二叉树中每个结点值为单个字符,采用二叉链存储结构存储。设计一个算法将二叉链 b 1 b1 b1 复制到二叉链 b 2 b2 b2 中。

解:当 b 1 b1 b1 为空时,置 b 2 b2 b2 为空树。当 b 1 b1 b1 不为空时,建立 b 2 b2 b2 结点( b 2 b2 b2 为根结点),置 b 2 − > d a t a = b 1 − > d a t a b2->data=b1->data b2>data=b1>data;递归调用 Copy( b 1 − > l c h i l d b1->lchild b1>lchild b 2 − > l c h i l d b2->lchild b2>lchild),由 b 1 b1 b1 的左子树建立 b 2 b2 b2 的左子树;递归调用 Copy( b 1 − > r c h i l d b1->rchild b1>rchild b 2 − > r c h i l d b2->rchild b2>rchild),由 b 1 b1 b1 的右子树建立 b 2 b2 b2 的右子树。对应的算法如下:

void Copy(BTNode *b1,BTNode *&b2)
{	if (b1==NULL)
		b2=NULL;
	else
	{	b2=(BTNode *)malloc(sizeof(BTNode));
		b2->data=b1->data;
		Copy(b1->lchild,b2->lchild);
		Copy(b1->rchild,b2->rchild);
	}
}

15. 假设二叉树中每个结点值为单个字符,采用二叉链存储结构存储。设计一个算法,求二叉树 b b b 中第 k k k 层上叶子结点个数。

解:采用先序遍历方法,当 b b b 为空时返回 0。置 n u m num num 为 0。若 b b b 不为空,当前结点的层次为 k k k,并且 b b b 为叶子结点,则 n u m num num 增 1,递归调用 n u m 1 num1 num1=LevelkCount( b − > l c h i l d , k , h + 1 b->lchild,k,h+1 b>lchildkh+1) 求出左子树中第 k k k 层的结点个数 n u m 1 num1 num1,递归调用 n u m 2 num2 num2=LevelkCount( b − > r c h i l d , k , h + 1 b->rchild,k,h+1 b>rchildkh+1)求出右子树中第 k k k 层的结点个数 n u m 2 num2 num2,置 n u m num num+= n u m 1 num1 num1+ n u m 2 num2 num2,最后返回 n u m num num。对应的算法如下:

int LevelkCount(BTNode *b,int k,int h)
{	//h 的初值为 1
	int num1,num2,num=0;
	if (b!=NULL)
	{	if (h==k && b->lchild==NULL && b->rchild==NULL)
		num++;
		num1=LevelkCount(b->lchild,k,h+1);
		num2=LevelkCount(b->rchild,k,h+1);
		num+=num1+num2;
		return num;
	}
	return 0;
}

int Levelkleft(BTNode *b,int k //返回二叉树 b 中第 k 层上叶子结点个数
{
	return LevelkCount(b,k,1);
}

16. 假设二叉树中每个结点值为单个字符,采用二叉链存储结构存储。设计一个算法,判断值为 x x x 的结点与值为 y y y 的结点是否互为兄弟,假设这样的结点值是唯一的。

解:采用先序遍历方法,当 b b b 为空时直接返回 false;否则,若当前结点 b b b 是双分支结点,且有两个互为兄弟的结点 x x x y y y,则返回 true;否则,递归调用 f l a g flag flag=Brother( b − > l c h i l d b->lchild b>lchild x x x y y y),求出 x x x y y y 在左子树中是否互为兄弟,若 f l a g flag flag 为 true,则返回 true;否则递归调用 Brother( b − > r c h i l d b->rchild b>rchild x x x y y y),求出 x x x y y y 在右子树中是否互为兄弟,并返回其结果。对应的算法如下:

bool Brother(BTNode *b,char x,char y)
{	bool flag;
	if (b==NULL)
		return false;
	else
	{	if (b->lchild!=NULL && b->rchild!=NULL)
		{	if ((b->lchild->data==x && b->rchild->data==y) || (b->lchild->data==y && b->rchild->data==x))
				return true;
		}
		flag=Brother(b->lchild,x,y);
		if (flag==true)
			return true;
		else
			return Brother(b->rchild,x,y);
	}
}

17. 假设二叉树中每个结点值为单个字符,采用二叉链存储结构存储。设计一个算法,采用先序遍历方法求二叉树 b 中值为 x 的结点的子孙,假设值为 x 的结点是唯一的。

解:设计 Output( p p p)算法输出以 p p p 为根结点的所有结点。首先在二叉树 b b b 中查找值为 x x x 的结点,当前 b b b 结点是这样的结点,调用 Output( b − > l c h i l d b->lchild b>lchild)输出其左子树中所有结点,调用 Output( b − > r c h i l d b->rchild b>rchild) 输出其右子树中所有结点,并返回;否则,递归调用 Child( b − > l c h i l d b->lchild b>lchild x x x) 在左子树中查找值为 x x x 的结点,递归调用 Child( b − > r c h i l d b->rchild b>rchild x x x)在右子树中查找值为 x x x 的结点。
对应的算法如下:

void Output(BTNode *p) //输出以 p 为根结点的子树
{
    if (p != NULL) {
        printf("%c ", p->data);
        Output(p->lchild);
        Output(p->rchild);
    }
}

void Child(BTNode *b, char x) //输出 x 结点的子孙
{
    if (b != NULL) {
        if (b->data == x) {
            if (b->lchild != NULL)
                Output(b->lchild);
            if (b->rchild != NULL)
                Output(b->rchild);
            return;
        }
        Child(b->lchild, x);
        Child(b->rchild, x);
    }
}

18. 假设二叉树采用二叉链存储结构,设计一个算法把二叉树 b 的左、右子树进行交换。要求不破坏原二叉树。并用相关数据进行测试。

解:交换二叉树的左、右子树的递归模型如下:
f ( b , t ) f(b,t) f(bt) = t t t=NULL           若 b b b=NULL
f ( b , t ) f(b,t) f(bt) = 复制根结点 b b b 产生结点 t t t;     其他情况
   f ( b − > l c h i l d , t 1 ) ; f ( b − > r c h i l d , t 2 ) ; f(b->lchild,t1); f(b->rchild,t2); f(b>lchildt1);f(b>rchildt2);
   t − > l c h i l d = t 2 ; t − > r c h i l d = t 1 t->lchild=t2; t->rchild=t1 t>lchild=t2;t>rchild=t1
对应的算法如下(算法返回左、右子树交换后的二叉树):

#include "btree.cpp" //二叉树基本运算算法
BTNode *Swap(BTNode *b) {
    BTNode *t, *t1, *t2;
    if (b == NULL)
        t = NULL;
    else {
        t = (BTNode *) malloc(sizeof(BTNode));
        t->data = b->data; //复制产生根结点 t
        t1 = Swap(b->lchild);
        t2 = Swap(b->rchild);
        t->lchild = t2;
        t->rchild = t1;
    }
    return t;
}

或者设计成如下算法(算法产生左、右子树交换后的二叉树 b 1 b1 b1):

void Swap1(BTNode *b, BTNode *&b1) {
    if (b == NULL)
        b1 = NULL;
    else {
        b1 = (BTNode *) malloc(sizeof(BTNode));
        b1->data = b->data; //复制产生根结点 b1
        Swap1(b->lchild, b1->rchild);
        Swap1(b->rchild, b1->lchild);
    }
}

设计如下主函数:

int main() {
    BTNode *b, *b1;
    CreateBTree(b, "A(B(D(,G)),C(E,F))");
    printf("交换前的二叉树:");
    DispBTree(b);
    printf("\n");
    b1 = Swap(b);
    printf("交换后的二叉树:");
    DispBTree(b1);
    printf("\n");
    DestroyBTree(b);
    DestroyBTree(b1);
    return 1;
}

程序执行结果如下:
交换前的二叉树: A(B(D(,G)),C(E,F))
交换后的二叉树: A(C(F,E),B(,D(G)))

19. 假设二叉树采用二叉链存储结构,设计一个算法判断一棵二叉树 b b b 的左、右子树是否同构。

解:判断二叉树 b1、b2 是否同构的递归模型如下:
f ( b 1 , b 2 ) f(b1,b2) f(b1b2)=true                  b 1 b1 b1= b 2 b2 b2=NULL
f ( b 1 , b 2 ) f(b1,b2) f(b1b2)=false                若 b 1 b1 b1 b 2 b2 b2 中有一个为空,另一个不为空
f ( b 1 , b 2 ) f(b1,b2) f(b1b2)= f ( b 1 − > l c h i l d , b 2 − > l c h i l d ) f(b1->lchild,b2->lchild) f(b1>lchildb2>lchild)   其他情况
      & f ( b 1 − > r c h i l d , b 2 − > r c h i l d ) f(b1->rchild,b2->rchild) f(b1>rchildb2>rchild)
对应的算法如下:

bool Symm(BTNode *b1, BTNode *b2) //判断二叉树 b1 和 b2 是否同构
{
    if (b1 == NULL && b2 == NULL)
        return true;
    else if (b1 == NULL || b2 == NULL)
        return false;
    else
        return (Symm(b1->lchild, b2->lchild) & Symm(b1->rchild, b2->rchild));
}

bool Symmtree(BTNode *b) //判断二叉树的左、右子树是否同构
{
    if (b == NULL)
        return true;
    else
        return Symm(b->lchild, b->rchild);
}

20. 假设二叉树以二叉链存储,设计一个算法,判断一棵二叉树 b b b 是否为完全二叉树。

解:根据完全二叉树的定义,对完全二叉树按照从上到下、从左到右的次序遍历(层次遍历)应该满足:
  (1)某结点没有左孩子,则一定无右孩子。
  (2)若某结点缺左或右孩子(一旦出现这种情况,置 b j bj bj=false),则其所有后继一定无孩子。
  若不满足上述任何一条,均不为完全二叉树( c m cm cm=true 表示是完全二叉树, c m cm cm=false 表
示不是完全二叉树)。对应的算法如下:

bool CompBTree(BTNode *b) {
    BTNode *Qu[MaxSize], *p; //定义一个队列,用于层次遍历
    int front = 0, rear = 0; //环形队列的队头队尾指针
    bool cm = true; //cm 为真表示二叉树为完全二叉树
    bool bj = true; //bj 为真表示到目前为止所有结点均有左右孩子
    if (b == NULL) return true; //空树当成特殊的完全二叉树
    rear++;
    Qu[rear] = b; //根结点进队
    while (front != rear) //队列不空
    {
        front = (front + 1) % MaxSize;
        p = Qu[front]; //出队结点 p
        if (p->lchild == NULL) //p 结点没有左孩子
        {
            bj = false; //出现结点 p 缺左孩子的情况
            if (p->rchild != NULL) //没有左孩子但有右孩子,违反(1),
                cm = false;
        } else //p 结点有左孩子
        {
            if (!bj) cm = false; //bj 为假而结点 p 还有左孩子,违反(2)
            rear = (rear + 1) % MaxSize;
            Qu[rear] = p->lchild; //左孩子进队
            if (p->rchild == NULL)
                bj = false; //出现结点 p 缺右孩子的情况
            else //p 有左右孩子,则继续判断
            {
                rear = (rear + 1) % MaxSize;
                Qu[rear] = p->rchild; //将 p 结点的右孩子进队
            }
        }
    }
    return cm;
}

第 8 章 图

1. 图G是一个非连通图,共有28条边,则该图至少有多少个顶点?

答:由于 G G G 是一个非连通图,在边数固定时,顶点数最少的情况是该图由两个连通分量构成,且其中之一只含一个顶点(没有边),另一个为完全无向图。设该完全无向图的顶点数为 n n n,其边数为 n ( n − 1 ) / 2 n(n-1)/2 n(n1)/2,即 n ( n − 1 ) / 2 = 28 n(n-1)/2=28 n(n1)/2=28,得 n = 8 n=8 n=8。所以,这样的非连通图至少有 1+8=9 个顶点。

2. 有一个如图 8.2(a)所示的有向图,给出其所有的强连通分量。

答:图中顶点 0、1、2 构成一个环,这个环一定是某个强连通分量的一部分。再考察顶点 3、4,它们到这个环中的顶点都有双向路径,所以将顶点 3、4加入。考察顶点 5、6,它们各自构成一个强连通分量。该有向图的强连通分量有 3 个,如图 8.2(b)所示。

图 8.2 一个有向图及其强连通分量

3. 对于稠密图和稀疏图,采用邻接矩阵和邻接表哪个更好些?

答:邻接矩阵适合于稠密图,因为邻接矩阵占用的存储空间与边数无关。邻接表适合于稀疏图,因为邻接表占用的存储空间与边数有关。

4. 对 n n n 个顶点的无向图和有向图(均为不带权图),采用邻接矩阵和邻接表表示时,如何求解以下问题:

  (1)图中有多少条边?
  (2)任意两个顶点i和j是否有边相连?
  (3)任意一个顶点的度是多少?

答:(1)对于邻接矩阵表示的无向图,图的边数等于邻接矩阵数组中为 1 的元素个数除以 2;对于邻接表表示的无向图,图中的边数等于边结点的个数除以 2。
   对于邻接矩阵表示的有向图,图中的边数等于邻接矩阵数组中为 1 的元素个数;对于邻接表表示的有向图,图中的边数等于边结点的个数。
  (2)对于邻接矩阵 g g g 表示的无向图,邻接矩阵数组元素 g . e d g e s [ i ] [ j ] g.edges[i][j] g.edges[i][j] 为 1 表示它们有边相连,否则为无边相连。对于邻接矩阵 g g g 表示的有向图,邻接矩阵数组元素 g . e d g e s [ i ] [ j ] g.edges[i][j] g.edges[i][j] 为1表示从顶点i到顶点 j j j 有边, g . e d g e s [ j ] [ i ] g.edges[j][i] g.edges[j][i] 为 1 表示从顶点 j j j 到顶点 i i i 有边。
   对于邻接表 G G G 表示的无向图,若从头结点 G → a d j l i s t [ i ] G \to adjlist[i] Gadjlist[i]的单链表中找到编号为 j j j 的边表结点,表示它们有边相连;否则为无边相连。对于邻接表 G G G 表示的有向图,若从头结点 G → a d j l i s t [ i ] G \to adjlist[i] Gadjlist[i] 的单链表中找到编号为j的边表结点,表示从顶点 i i i 到顶点 j j j 有边。若从头结点 G → a d j l i s t [ j ] G \to adjlist[j] Gadjlist[j] 的单链表中找到编号为i的边表结点,表示从顶点 j j j 到顶点 i i i 有边。
  (3)对于邻接矩阵表示的无向图,顶点i的度等于第 i i i 行中元素为 1 的个数;对于邻接矩阵表示的有向图,顶点i的出度等于第 i i i 行中元素为 1 的个数,入度等于第 i i i 列中元素为 1 的个数,顶点i度等于它们之和。
对于邻接表 G G G 表示的无向图,顶点 i i i 的度等于 G → a d j l i s t [ i ] G \to adjlist[i] Gadjlist[i] 为头结点的单链表中边表结点个数。
   对于邻接表 G G G 表示的有向图,顶点 i i i 的出度等于 G → a d j l i s t [ i ] G \to adjlist[i] Gadjlist[i] 为头结点的单链表中边表结点的个数;入度需要遍历所有的边结点,若 G → a d j l i s t [ j ] G \to adjlist[j] Gadjlist[j]为头结点的单链表中存在编号为 i i i 的边结点,则顶点i的入度增 1,顶点i的度等于入度和出度之和。

5. 对于如图 8.3 所示的一个无向图 G,给出以顶点 0 作为初始点的所有的深度优先遍历序列和广度优先遍历序列。

图 8.3 一个无向图 G

答:无向图 G 的所有的深度优先遍历序列如下:
0 1 4 5 2 3
0 1 5 4 2 3
0 1 4 5 3 2
0 1 5 4 3 2
0 2 1 4 5 3
0 2 1 5 4 3
0 2 3 1 4 5
0 2 3 1 5 4
0 3 1 4 5 2
0 3 1 5 4 2
0 3 2 1 4 5
0 3 2 1 5 4
 
无向图 G 所有的广度优先遍历序列如下:
0 1 2 3 4 5
0 1 2 3 5 4
0 1 3 2 4 5
0 1 3 2 5 4
0 2 1 3 4 5
0 2 1 3 5 4
0 2 3 1 4 5
0 2 3 1 5 4
0 3 1 2 4 5
0 3 1 2 5 4
0 3 2 1 4 5
0 3 2 1 5 4

6. 对于如图 8.4 所示的带权无向图,给出利用 Prim 算法(从顶点 0 开始构造)和 Kruskal 算法构造出的最小生成树的结果,要求结果按构造边的顺序列出。

答:利用普里姆算法从顶点0出发构造的最小生成树为:{(0,1),(0,3),(1,2),(2,5),(5,4)}。利用克鲁斯卡尔算法构造出的最小生成树为:{(0,1),(0,3),(1,2),(5,4),(2,5)}。

图 8.4 一个带权无向图 G

7. 对于一个顶点个数大于4的带权无向图,回答以下问题:

  (1)该图的最小生成树一定是唯一的吗?如何所有边的权都不相同,那么其最小生成树一定是唯一的吗?
  (2)如果该图的最小生成树不是唯一的,那么调用Prim算法和Kruskal算法构造出的最小生成树一定相同吗?
  (3)如果图中有且仅有两条权最小的边,它们一定出现在该图的所有的最小生成树中吗?简要说明回答的理由。
  (4)如果图中有且仅有3条权最小的边,它们一定出现在该图的所有的最小生成树中吗?简要说明回答的理由。

答:(1)该图的最小生成树不一定是唯一的。如何所有边的权都不相同,那么其最小生成树一定是唯一的。
  (2)若该图的最小生成树不是唯一的,那么调用 Prim 算法和 Kruskal 算法构造出的最小生成树不一定相同。
  (3)如果图中有且仅有两条权最小的边,它们一定会出现在该图的所有的最小生成树中。因为在采用Kruskal算法构造最小生成树时,首先选择这两条权最小的边加入,不会出现回路(严格的证明可以采用反证法)。
  (4)如果图中有且仅有3条权最小的边,它们不一定出现在该图的所有的最小生成树中。因为在采用 Kruskal 算法构造最小生成树时,选择这 3 条权最小的边加入时,有可能出现回路。例如,如图8.5所示的带权无向图,有 3 条边的权均为 1,它们一定不会同时都出现在其任何最小生成树中。

图 8.5 一个带权无向图

8. 对于如图8.6所示的带权有向图,采用Dijkstra算法求出从顶点0到其他各顶点的最短路径及其长度,要求给出求解过程。

图 8.6 一个带权有向图 G

答:采用 Dijkstra 算法求从顶点 0 到其他各顶点的最短路径及其长度的过程如下:
  (1) S = 0 S={0} S=0 d i s t [ 0..5 ] dist[0..5] dist[0..5]={ 0 , 1 , 5 , 2 , ∞ , ∞ 0,1,5,2,\infty ,\infty 0152 }, p a t h [ 0..5 ] path[0..5] path[0..5]={0,0,0,0,-1,-1}。选取最短路径长度的顶点 1。
  (2) S S S={ 0 , 1 0,1 01},调整顶点 1 到顶点 2、4 的最短路径长度, d i s t [ 0..5 ] dist[0..5] dist[0..5]={ 0 , 1 , 4 , 2 , 8 , ∞ 0,1,4,2,8,\infty 01428 }, p a t h [ 0..5 ] path[0..5] path[0..5]={ 0 , 0 , 1 , 0 , 1 , − 1 0,0,1,0,1,-1 001011}。选取最短路径长度的顶点 3。
  (3) S S S={ 0 , 1 , 3 0,1,3 013},调整顶点 3 到顶点 5 的最短路径长度, d i s t [ 0..5 ] dist[0..5] dist[0..5]={ 0 , 1 , 4 , 2 , 8 , 10 0,1,4,2,8,10 0142810}, p a t h [ 0..5 ] path[0..5] path[0..5]={ 0 , 0 , 1 , 0 , 1 , 3 0,0,1,0,1,3 001013}。选取最短路径长度的顶点 2。
  (4) S S S={ 0 , 1 , 3 , 2 0,1,3,2 0132},调整顶点 2 到顶点 5 的最短路径长度, d i s t [ 0..5 ] dist[0..5] dist[0..5]={ 0 , 1 , 4 , 2 , 8 , 10 0,1,4,2,8,10 0142810}, p a t h [ 0..5 ] path[0..5] path[0..5]={ 0 , 0 , 1 , 0 , 1 , 3 0,0,1,0,1,3 001013}。选取最短路径长度的顶点 4。
  (5) S S S={ 0 , 1 , 3 , 2 , 4 0,1,3,2,4 01324},调整顶点 4 到顶点 5 的最短路径长度, d i s t [ 0..5 ] dist[0..5] dist[0..5]={ 0 , 1 , 4 , 2 , 8 , 10 0,1,4,2,8,10 0142810}, p a t h [ 0..5 ] path[0..5] path[0..5]={ 0 , 0 , 1 , 0 , 1 , 3 0,0,1,0,1,3 001013}。选取最短路径长度的顶点 5。
  (6) S S S={ 0 , 1 , 3 , 2 , 4 , 5 0,1,3,2,4,5 013245},顶点 5 没有出边, d i s t [ 0..5 ] dist[0..5] dist[0..5]={ 0 , 1 , 4 , 2 , 8 , 10 0,1,4,2,8,10 0142810}, p a t h [ 0..5 ] path[0..5] path[0..5]={ 0 , 0 , 1 , 0 , 1 , 3 0,0,1,0,1,3 001013}。
 
   最终结果如下:
  从 0 到 1 的最短路径长度为:1,路径为:0,1
  从 0 到 2 的最短路径长度为:4,路径为:0,1,2
  从 0 到 3 的最短路径长度为:2,路径为:0,3
  从 0 到 4 的最短路径长度为:8,路径为:0,1,4
  从 0 到 5 的最短路径长度为:10,路径为:0,3,5

9. 对于一个带权连通图,可以采用 Prim 算法构造出从某个顶点 v v v 出发的最小生成树,问该最小生成树是否一定包含从顶点 v v v 到其他所有顶点的最短路径。如果回答是,请予以证明;如果回答不是,请给出反例。

答:不一定。例如,对于如图 8.7(a)所示带权连通图,从顶点 0 出发的最小生成树如 图8.7(b)所示,而从顶点 0 到顶点 2 的最短路径为 0 → 2 0 \to 2 02,而不是最小生成树中的 0 → 1 → 2 0 \to 1 \to 2 012

图 8.7 一个带权连通图将其最小生成树

10. 若只求带权有向图 G G G 中从顶点i到顶点j的最短路径,如何修改 Dijkstra 算法来实现这一功能?

答:修改 Dijkstra 算法为:从顶点 i i i 开始(以顶点 i i i 为源点),按 Dijkstra 算法思路不断地扩展顶点集 S,当扩展到顶点j时,算法结束,通过 path 回推出从顶点 i i i 到顶点 j j j 的最短路径。

11. Dijkstra 算法用于求单源最短路径,为了求一个图中所有顶点对之间的最短路径,可以以每个顶点作为源点调用 Dijkstra 算法,Floyd 算法和这种算法相比,有什么优势?

答:对于有 n n n 个顶点的图,求所有顶点对之间的最短路径,若调用 Dijkstra 算法 n n n 次,其时间复杂度为 O ( n 3 ) O(n^{3}) O(n3)。Floyd 算法的时间复杂度也是 O ( n 3 ) O(n^{3}) O(n3)。但 Floyd 算法更快,这是因为前者每次调用 Dijkstra 算法时都是独立执行的,路径比较中得到的信息没有共享,而 Floyd 算法中每考虑一个顶点时所得到的路径比较信息保存在 A A A 数组中,会用于下次路径比较,从而提高整体查找最短路径的效率。

12. 回答以下有关拓扑排序的问题:

  (1)给出如 图8.8 所示有向图的所有不同的拓扑序列。
  (2)什么样的有向图的拓扑序列是唯一的?
  (3)现要对一个有向图的所有顶点重新编号,使所有表示边的非 0 元素集中到邻接矩阵数组的上三角部分。根据什么顺序对顶点进行编号可以实现这个功能?

图 8.8 一个有向图

答:(1)该有向图的所有不同的拓扑序列有: a e b c d aebcd aebcd a b c e d abced abced a b e c d abecd abecd
  (2)这样的有向图的拓扑序列是唯一的:图中只有一个入度为 0 的顶点,在拓扑排序中每次输出一个顶点后都只有一个入度为 0 的顶点。
  (3)首先该对有向图进行拓扑排序,把所有顶点排在一个拓扑序列中。然后按该序列对所有顶点重新编号,使得每条有向边的起点编号小于终点编号,就可以把所有边集中到邻接矩阵数组的上三角部分。

13. 已知有 6 个顶点(顶点编号为 0~5)的带权有向图 G,其邻接矩阵数组 A 为上三角矩阵,按行为主序(行优先)保存在如下的一维数组中:

46 ∞ \infty ∞ \infty ∞ \infty 5 ∞ \infty ∞ \infty ∞ \infty 43 ∞ \infty ∞ \infty 33

  要求:
  (1)写出图 G G G 的邻接矩阵数组 A A A
  (2)画出带权有向图 G G G
  (3)求图 G G G 的关键路径,并计算该关键路径的长度。

答:(1)图G的邻接矩阵数组 A 如 图8.9 所示。
(2)有向带权图 G 如 图8.10 所示。
(3)图8.11 中粗线所标识的 4 个活动组成图 G G G 的关键路径。

A = [ 0 4 6 ∞ ∞ ∞ ∞ 0 5 ∞ ∞ ∞ ∞ ∞ 0 4 3 ∞ ∞ ∞ ∞ 0 ∞ 3 ∞ ∞ ∞ ∞ 0 3 ∞ ∞ ∞ ∞ ∞ 0 ] A= \begin{bmatrix} 0 & 4 & 6 & \infty & \infty & \infty \\ \infty & 0 & 5 & \infty & \infty & \infty \\ \infty & \infty & 0 & 4 & 3 & \infty \\ \infty & \infty & \infty & 0 & \infty & 3 \\ \infty & \infty & \infty & \infty & 0 & 3 \\ \infty & \infty & \infty & \infty & \infty & 0 \end{bmatrix} A=0406504030330

图 8.9 邻接矩阵 A
 
图 8.10 图 G
 
图 8.11 图 G 中的关键路径

14. 假设不带权有向图采用邻接矩阵 g 存储,设计实现以下功能的算法:

  (1)求出图中每个顶点的入度。
  (2)求出图中每个顶点的出度。
  (3)求出图中出度为0的顶点数。

解:利用邻接矩阵的特点和相关概念得到如下算法:

void InDs1(MatGraph g) //求出图 G 中每个顶点的入度
{
    int i, j, n;
    printf("各顶点入度:\n");
    for (j = 0; j < g.n; j++) {
        n = 0;
        for (i = 0; i < g.n; i++)
            if (g.edges[i][j] != 0)
 				n++; //n 累计入度数
        printf(" 顶点%d:%d\n", j, n);
    }
}

void OutDs1(MatGraph g) //求出图 G 中每个顶点的出度
{
    int i, j, n;
    printf("各顶点出度:\n");
    for (i = 0; i < g.n; i++) {
        n = 0;
        for (j = 0; j < g.n; j++)
            if (g.edges[i][j] != 0)
                n++; //n 累计出度数
        printf(" 顶点%d:%d\n", i, n);
    }
}

void ZeroOutDs1(MatGraph g) //求出图 G 中出度为 0 的顶点个数
{
    int i, j, n;
    printf("出度为 0 的顶点:");
    for (i = 0; i < g.n; i++) {
        n = 0;
        for (j = 0; j < g.n; j++)
            if (g.edges[i][j] != 0) //存在一条出边
                n++;
        if (n == 0)
            printf("%2d\n", i);
    }
    printf("\n");
}

15. 假设不带权有向图采用邻接表 G G G 存储,设计实现以下功能的算法:

  (1)求出图中每个顶点的入度。
  (2)求出图中每个顶点的出度。
  (3)求出图中出度为0的顶点数。

解:利用邻接表的特点和相关概念得到如下算法:

void InDs2(AdjGraph *G) //求出图 G 中每个顶点的入度
{
    ArcNode *p;
    int A[MAXV], i; //A 存放各顶点的入度
    for (i = 0; i < G->n; i++) //A 中元素置初值 0
        A[i] = 0;
    for (i = 0; i < G->n; i++) //扫描所有头结点
    {
        p = G->adjlist[i].firstarc;
        while (p != NULL) //扫描边结点
        {
            A[p->adjvex]++; //表示 i 到 p->adjvex 顶点有一条边
            p = p->nextarc;
        }
    }
    printf("各顶点入度:\n"); //输出各顶点的入度
    for (i = 0; i < G->n; i++)
        printf(" 顶点%d:%d\n", i, A[i]);
}

void OutDs2(AdjGraph *G) //求出图 G 中每个顶点的出度
{
    int i, n;
    ArcNode *p;
    printf("各顶点出度:\n");
    for (i = 0; i < G->n; i++) //扫描所有头结点
    {
        n = 0;
        p = G->adjlist[i].firstarc;
        while (p != NULL) //扫描边结点
        {
            n++; //累计出边的数
            p = p->nextarc;
        }
        printf(" 顶点%d:%d\n", i, n);
    }
}

void ZeroOutDs2(AdjGraph *G) //求出图 G 中出度为 0 的顶点数
{
    int i, n;
    ArcNode *p;
    printf("出度为 0 的顶点:");
    for (i = 0; i < G->n; i++) //扫描所有头结点
    {
        p = G->adjlist[i].firstarc;
        n = 0;
        while (p != NULL) //扫描边结点
        {
            n++; //累计出边的数
            p = p->nextarc;
        }
        if (n == 0) //输出出边数为 0 的顶点编号
            printf("%2d", i);
   }
    printf("\n");
}

16. 假设一个连通图采用邻接表作为存储结构,试设计一个算法,判断其中是否存在经过顶点 v v v 的回路。

解:从顶点 v v v 出发进行深度优先遍历,用 d d d 记录走过的路径长度,对每个访问的顶点设置标记为 1。若当前访问顶点 u u u,表示 v ⇒ u v \Rightarrow u vu 存在一条路径,如果顶点 u u u 的邻接点 w w w 等于 v v v 并且 d > 1 d>1 d>1,表示顶点 u u u v v v 有一条边,即构成经过顶点 v v v 的回路,如 图 8.12 所示。Cycle 算法中 h a s has has 是布尔值,初始调用时置为 false,执行后若为 true 表示存在经过顶点 v v v 的回路,否则表示没有相应的回路。

图 8.12 图中存在回路的示意图
 

对应的算法如下:

int visited[MAXV]; //全局变量数组
void Cycle(AdjGraph *G, int u, int v, int d, bool &has) { //调用时 has 置初值 false,d 为-1
    ArcNode *p;
    int w;
    visited[u] = 1;
    d++; //置已访问标记
    p = G->adjlist[u].firstarc; //p 指向顶点 u 的第一个邻接点
    while (p != NULL) {
        w = p->adjvex;
        if (visited[w] == 0) //若顶点 w 未访问,递归访问它
            Cycle(G, w, v, d, has); //从顶点 w 出发搜索
        else if (w == v && d > 1) //u 到 v 存在一条边且回路长度大于 1
        {
            has = true;
            return;
        }
        p = p->nextarc; //找下一个邻接点
    }
}

bool hasCycle(AdjGraph *G, int v) //判断连通图 G 中是否有经过顶点 v 的回路
{
    bool has = false;
    Cycle(G, v, v, -1, has); //从顶点 v 出发搜索
    return has;
}

17.假设图 G G G 采用邻接表存储,试设计一个算法,判断无向图 G G G 是否是一棵树。若是树,返回真;否则返回假。

解:一个无向图 G G G 是一棵树的条件是: G G G 必须是无回路的连通图或者是有 n − 1 n-1 n1 条边的连通图。这里采用后者作为判断条件,通过深度优先遍历图 G G G,并求出遍历过的顶点数 v n vn vn 和边数 e n en en,若 v n = = G → n vn==G \to n vn==Gn 成立(表示为连通图)且 e n = = 2 ( G → n − 1 ) en==2(G \to n-1) en==2(Gn1)(遍历边数为 2 ( G → n − 1 ) ) 2(G \to n-1)) 2(Gn1) 成立,则 G G G 为一棵树。对应的算法如下:

void DFS2(AdjGraph *G, int v, int &vn, int &en) { //深度优先遍历图 G,并求出遍历过的顶点数 vn 和边数 en
    ArcNode *p;
    visited[v] = 1;
    vn++; //遍历过的顶点数增 1
    p = G->adjlist[v].firstarc;
    while (p != NULL) {
        en++; //遍历过的边数增 1
        if (visited[p->adjvex] == 0)
            DFS2(G, p->adjvex, vn, en);
        p = p->nextarc;
    }
}

int IsTree(AdjGraph *G) //判断无向图 G 是否是一棵树
{
    int vn = 0, en = 0, i;
    for (i = 0; i < G->n; i++)
        visited[i] = 0;
    DFS2(G, 1, vn, en);
    if (vn == G->n && en == 2 * (G->n - 1))
        return 1; //遍历顶点为 G->n 个,遍历边数为 2(G->n-1),则为树
    else
        return 0;
}

18. 设有 5 地(0~4)之间架设有 6 座桥(A~F),如图 8.13 所示,设计一个算法,从某一地出发,经过每座桥恰巧一次,最后仍回到原地。

图 8.13 实地图
图 8.14 一个无向图 G

解:该实地图对应的一个无向图 G G G 如图 8.14 所示,本题变为从指定点 k k k 出发找经过所有 6 条边回到 k k k 顶点的路径,由于所有顶点的度均为偶数,可以找到这样的路径。对应的算法如下:

int vedge[MAXV][MAXV]; //边访问数组,vedge[i][j]表示(i,j)边是否访问过
void Traversal(AdjGraph *G, int u, int v, int k, int path[], int d)
//d 是到当前为止已走过的路径长度,调用时初值为-1
{
    int w, i;
    ArcNode *p;
    d++;
    path[d] = v; //(u,v)加入到 path 中
    vedge[u][v] = vedge[v][u] = 1; //(u,v)边已访问
    p = G->adjlist[v].firstarc; //p 指向顶点 v 的第一条边
    while (p != NULL) {
        w = p->adjvex; //(v,w)有一条边
        if (w == k && d == G->e - 1) //找到一个回路,输出之
        {
            printf(" %d->", k);
            for (i = 0; i <= d; i++)
                printf("%d->", path[i]);
            printf("%d\n", w);
        }
        if (vedge[v][w] == 0) //(v,w)未访问过,则递归访问之
            Traversal(G, v, w, k, path, d);
        p = p->nextarc; //找 v 的下一条边
    }
    vedge[u][v] = vedge[v][u] = 0; //恢复环境:使该边点可重新使用
}

void FindCPath(AdjGraph *G, int k) //输出经过顶点 k 和所有边的全部回路
{
    int path[MAXV];
    int i, j, v;
    ArcNode *p;
    for (i = 0; i < G->n; i++) //vedge 数组置初值
        for (j = 0; j < G->n; j++)
            if (i == j) vedge[i][j] = 1;
            else vedge[i][j] = 0;
    printf("经过顶点%d 的走过所有边的回路:\n", k);
    p = G->adjlist[k].firstarc;
    while (p != NULL) {
        v = p->adjvex;
        Traversal(G, k, v, k, path, -1);
        p = p->nextarc;
    }
}

设计如下主函数:

int main() {
    int v = 4;
    AdjGraph *G;
    int n = 5, e = 6;
    int A[MAXV][MAXV] = {{0, 1, 0, 0, 1},
                         {1, 0, 0, 0, 1},
                         {0, 0, 0, 1, 1},
                         {0, 0, 1, 0, 1},
                         {1, 1, 1, 1, 0}};
    CreateAdj(G, A, n, e);
    printf("图 G 的邻接表:\n");
    DispAdj(G); //输出邻接表
    FindCPath(G, v);
    printf("\n");
    DestroyAdj(G);
    return 1;
}

程序执行结果如下:
图 G 的邻接表:
   0 : 1 [ 1 ] → 4 [ 1 ] → ∧ 0: 1[1] \to 4[1]→∧ 0:1[1]4[1]
   1 : 0 [ 1 ] → 4 [ 1 ] → ∧ 1: 0[1] \to 4[1]→∧ 1:0[1]4[1]
   2 : 3 [ 1 ] → 4 [ 1 ] → ∧ 2: 3[1] \to 4[1]→∧ 2:3[1]4[1]
   3 : 2 [ 1 ] → 4 [ 1 ] → ∧ 3: 2[1] \to 4[1]→∧ 3:2[1]4[1]
   4 : 0 [ 1 ] → 1 [ 1 ] → 2 [ 1 ] → 3 [ 1 ] → ∧ 4: 0[1] \to 1[1] \to 2[1] \to 3[1] \to∧ 4:0[1]1[1]2[1]3[1]
经过顶点 4 的走过所有边的回路:
   4 → 0 → 1 → 4 → 2 → 3 → 4 4 \to 0 \to 1 \to 4 \to 2 \to 3 \to 4 4014234
   4 → 0 → 1 → 4 → 3 → 2 → 4 4 \to 0 \to 1 \to 4 \to 3 \to 2 \to 4 4014324
   4 → 1 → 0 → 4 → 2 → 3 → 4 4 \to 1 \to 0 \to 4 \to 2 \to 3 \to 4 4104234
   4 → 1 → 0 → 4 → 3 → 2 → 4 4 \to 1 \to 0 \to 4 \to 3 \to 2 \to 4 4104324
   4 → 2 → 3 → 4 → 1 → 0 → 4 4 \to 2 \to 3 \to 4 \to 1 \to 0 \to 4 4234104
   4 → 3 → 2 → 4 → 0 → 1 → 4 4 \to 3 \to 2 \to 4 \to 0 \to 1 \to 4 4324014
   4 → 3 → 2 → 4 → 1 → 0 → 4 4 \to 3 \to 2 \to 4 \to 1 \to 0 \to 4 4324104

19. 设不带权无向图 G 采用邻接表表示,设计一个算法求源点 i 到其余各顶点的最短路径。

解:利用广度优先遍历的思想,求 i i i j j j 两顶点间的最短路径转化为求从 i i i j j j 的层数,为此设计一个 l e v e l [ ] level[] level[] 数组记录每个顶点的层次。对应的算法如下:

void ShortPath(AdjGraph *G, int i) {
    int qu[MAXV], level[MAXV];
    int front = 0, rear = 0, k, lev; //lev 保存从 i 到访问顶点的层数
    ArcNode *p;
    visited[i] = 1;
    rear++;
    qu[rear] = i;
    level[rear] = 0; //顶点 i 已访问,将其进队
    while (front != rear) //队非空则执行
    {
        front = (front + 1) % MAXV;
        k = qu[front]; //出队
        lev = level[front];
        if (k != i)
            printf(" 顶点%d 到顶点%d 的最短距离是:%d\n", i, k, lev);
        p = G->adjlist[k].firstarc; //取 k 的边表头指针
        while (p != NULL) //依次搜索邻接点
        {
            if (visited[p->adjvex] == 0) //若未访问过
            {
                visited[p->adjvex] = 1;
                rear = (rear + 1) % MAXV;
                qu[rear] = p->adjvex; //访问过的邻接点进队
                level[rear] = lev + 1;
            }
            p = p->nextarc; //找顶点 i 的下一邻接点
        }
    }
}

设计如下主函数:

int main() {
    AdjGraph *G;
    int n = 5, e = 8;
    int A[MAXV][MAXV] = {{0, 1, 0, 1, 1},
                         {1, 0, 1, 1, 0},
                         {0, 1, 0, 1, 1},
                         {1, 1, 1, 0, 1},
                         {1, 0, 1, 1, 0}};
    CreateAdj(G, A, n, e); //创建《教程》图 8.1(a)的邻接表
    printf("图 G 的邻接表:\n");
    DispAdj(G); //输出邻接表
    for (int i = 0; i < n; i++)
        visited[i] = 0;
    printf("顶点 1 到其他各顶点的最短距离如下:\n");
    ShortPath(G, 1);
    return 1;
}

程序的执行结果如下:
图 G 的邻接表:
   0 : 1 [ 1 ] → 3 [ 1 ] → 4 [ 1 ] → ∧ 0: 1[1] \to 3[1] \to 4[1] \to∧ 0:1[1]3[1]4[1]
   1 : 0 [ 1 ] → 2 [ 1 ] → 3 [ 1 ] → ∧ 1: 0[1] \to 2[1] \to 3[1] \to∧ 1:0[1]2[1]3[1]
   2 : 1 [ 1 ] → 3 [ 1 ] → 4 [ 1 ] → ∧ 2: 1[1] \to 3[1] \to 4[1] \to∧ 2:1[1]3[1]4[1]
   3 : 0 [ 1 ] → 1 [ 1 ] → 2 [ 1 ] → 4 [ 1 ] → ∧ 3: 0[1] \to 1[1] \to 2[1] \to 4[1] \to∧ 3:0[1]1[1]2[1]4[1]
   4 : 0 [ 1 ] → 2 [ 1 ] → 3 [ 1 ] → ∧ 4: 0[1] \to 2[1] \to 3[1] \to∧ 4:0[1]2[1]3[1]
顶点 1 到其他各顶点的最短距离如下:
   顶点 1 到顶点 0 的最短距离是:1
   顶点 1 到顶点 2 的最短距离是:1
   顶点 1 到顶点 3 的最短距离是:1
   顶点 1 到顶点 4 的最短距离是:2

20. 对于一个带权有向图,设计一个算法输出从顶点 i i i 到顶点 j j j 的所有路径及其路径长度。调用该算法求出《教程》图 8.35 中顶点 0 到顶点 3 的所有路径及其长度。

解:采用回溯的深度优先遍历方法。增加一个形参 length 表示路径长度,其初始值为 0。当从顶点 u 出发,设置 visited[u]=1,当找到一个没有访问过的邻接点 w,就从 w 出发递归查找,其路径长度 length 增加<u,w>边的权值。当找到终点 v,就输出一条路径。通过设置 visited[u]=0 回溯查找所有的路径。对应的算法如下:

int visited[MAXV];

void findpath(AdjGraph *G, int u, int v, int path[], int d, int length) { //d 表示 path 中顶点个数,初始为 0;length 表示路径长度,初始为 0
    int w, i;
    ArcNode *p;
    path[d] = u;
    d++; //顶点 u 加入到路径中,d 增 1
    visited[u] = 1; //置已访问标记
    if (u == v && d > 0) //找到一条路径则输出
    {
        printf(" 路径长度:%d, 路径:", length);
        for (i = 0; i < d; i++)
            printf("%2d", path[i]);
        printf("\n");
    }
    p = G->adjlist[u].firstarc; //p 指向顶点 u 的第一个邻接点
    while (p != NULL) {
        w = p->adjvex; //w 为顶点 u 的邻接点
        if (visited[w] == 0) //若 w 顶点未访问,递归访问它
            findpath(G, w, v, path, d, p->weight + length);
        p = p->nextarc; //p 指向顶点 u 的下一个邻接点
    }
    visited[u] = 0; //恢复环境,使该顶点可重新使用
}

设计如下主函数求《教程》图 8.35 中顶点 0 到顶点 3 的所有路径及其长度:

int main() {
    AdjGraph *G;
    int A[MAXV][MAXV] = {
            {0,   4,   6,   6,   INF, INF, INF},
            {INF, 0,   1,   INF, 7,   INF, INF},
            {INF, INF, 0,   INF, 6,   4,   INF},
            {INF, INF, 2,   0,   INF, 5,   INF},
            {INF, INF, INF, INF, 0,   INF, 6},
            {INF, INF, INF, INF, 1,   0,   8},
            {INF, INF, INF, INF, INF, INF, 0}};
    int n = 7, e = 12;
    CreateAdj(G, A, n, e); //创建《教程》中图 8.35 的邻接表
    printf("图 G 的邻接表:\n");
    DispAdj(G); //输出邻接表
    int u = 0, v = 5;
    int path[MAXV];
    printf("从%d->%d 的所有路径:\n", u, v);
    findpath(G, u, v, path, 0, 0);
    DestroyAdj(G);
    return 1;
}

上述程序执行结果如下:
图 G 的邻接表:
   0 : 1 [ 4 ] → 2 [ 6 ] → 3 [ 6 ] → ∧ 0: 1[4] \to 2[6] \to 3[6] \to ∧ 0:1[4]2[6]3[6]
   1 : 2 [ 1 ] → 4 [ 7 ] → ∧ 1: 2[1] \to 4[7] \to ∧ 1:2[1]4[7]
   2 : 4 [ 6 ] → 5 [ 4 ] → ∧ 2: 4[6] \to 5[4] \to ∧ 2:4[6]5[4]
   3 : 2 [ 2 ] → 5 [ 5 ] → ∧ 3: 2[2] \to 5[5] \to ∧ 3:2[2]5[5]
   4 : 6 [ 6 ] → ∧ 4: 6[6] \to ∧ 4:6[6]
   5 : 4 [ 1 ] → 6 [ 8 ] → ∧ 5: 4[1] \to 6[8] \to ∧ 5:4[1]6[8]
   6 : ∧ 6: ∧ 6:
从 0->5 的所有路径:
  路径长度:9, 路径: 0 1 2 5
  路径长度:10, 路径:0 2 5
  路径长度:12, 路径:0 3 2 5
  路径长度:11, 路径:0 3 5

第 9 章 查找

1. 设有 5 个数据 d o 、 f o r 、 i f 、 r e p e a t 、 w h i l e do、for、if、repeat、while doforifrepeatwhile,它们排在一个有序表中,其查找概率分别是 p 1 = 0.2 , p 2 = 0.15 , p 3 = 0.1 , p 4 = 0.03 , p 5 = 0.01 p_{1}=0.2,p_{2}=0.15,p_{3}=0.1,p_{4}=0.03,p_{5}=0.01 p1=0.2p2=0.15p3=0.1p4=0.03p5=0.01。而查找它们之间不存在数据的概率分别为 q 0 = 0.2 , q 1 = 0.15 , q 2 = 0.1 , q 3 = 0.03 , q 4 = 0.02 , q 5 = 0.01 q_{0}=0.2,q_{1}=0.15,q_{2}=0.1,q_{3}=0.03,q_{4}=0.02,q_{5}=0.01 q0=0.2q1=0.15q2=0.1q3=0.03q4=0.02q5=0.01,该有序表如下:

  (1)试画出对该有序表分别采用顺序查找和折半查找时的判定树。
  (2)分别计算顺序查找的查找成功和不成功的平均查找长度。
  (3)分别计算折半查找的查找成功和不成功的平均查找长度。

答:(1)对该有序表分别采用顺序查找和折半查找时的判定树分别如 图9.2 和 9.3 所示。
  (2)对于顺序查找,成功查找到第 i i i 个元素需要 i i i 次比较,不成功查找需要比较的次数为对应外部结点的层次减 1:
   A S L 成 功 = ( 1 p 1 + 2 p 2 + 3 p 3 + 4 p 4 + 5 p 5 ) = 0.97 ASL_{成功}=(1p_{1}+2p_{2}+3p_{3}+4p_{4}+5p_{5})=0.97 ASL=(1p1+2p2+3p3+4p4+5p5)=0.97
   A S L 不 成 功 = ( 1 q 0 + 2 q 1 + 3 q 2 + 4 q 3 + 5 q 4 + 5 q 5 ) = 1.07 ASL_{不成功}=(1q_{0}+2q_{1}+3q_{2}+4q_{3}+5q_{4}+5q_{5})=1.07 ASL=(1q0+2q1+3q2+4q3+5q4+5q5)=1.07
  (3)对于折半查找,成功查找需要比较的次数为对应内部结点的层次,不成功查找需要比较的次数为对应外部结点的层次减 1:
   A S L 成 功 = ( 1 p 3 + 2 ( p 1 + p 4 ) + 3 ( p 2 + p 5 ) ) = 1.04 ASL_{成功}=(1p_{3}+2(p_{1}+p_{4})+3(p_{2}+p_{5}))=1.04 ASL=(1p3+2(p1+p4)+3(p2+p5))=1.04
   A S L 不 成 功 = ( 2 q 0 + 3 q 1 + 3 q 2 + 2 q 3 + 3 q 4 + 3 q 5 ) = 1.3 ASL_{不成功}=(2q_{0}+3q_{1}+3q_{2}+2q_{3}+3q_{4}+3q_{5})=1.3 ASL=(2q0+3q1+3q2+2q3+3q4+3q5)=1.3

图 9.2 有序表上顺序查找的判定树
 
图 9.3 有序表上折半查找的判定树

2. 对于 A [ 0..10 ] A[0..10] A[0..10] 有序表,在等概率的情况下,求采用折半查找法时成功和不成功的平均查找长度。对于有序表(12,18,24,35,47,50,62,83,90,115,134),当用折半查找法查找 90 时,需进行多少次查找可确定成功;查找 47 时需进行多少次查找可确定成功;查找 100 时,需进行多少次查找才能确定不成功。

答:对于 A[0…10]有序表构造的判定树如 图 9.4(a)所示。因此有:
A S L 成 功 = 1 × 2 + 2 × 2 + 4 × 3 + 4 × 4 11 = 3 ASL _{成功} = \frac{1 \times 2 + 2 \times 2 + 4 \times 3 + 4 \times 4 }{11} =3 ASL=111×2+2×2+4×3+4×4=3
A S L 不 成 功 = 4 × 3 + 8 × 4 11 = 3.67 ASL _{不成功}= \frac{4 \times 3 + 8 \times 4 }{11}=3.67 ASL=114×3+8×4=3.67
对于题中给定的有序表构造的判定树如 图 9.4(b)所示。查找 90 时,关键字比较次序是 50、90,比较 2 次。查找 47 时,关键字比较次序是 50、24、35、47,比较 4 次。查找 100 时,关键字比较次序是 50、90、115,比较 3 次。

图 9.4 两棵判定树

3. 有以下查找算法:

int fun(int a[],int n,int k)
{	int i;
	for (i=0;i<n;i+=2)
		if (a[i]==k)
			return i;
	for (i=1;i<n;i+=2)
		if (a[i]==k)
			return i;
	return -1;
}

  (1)指出 fun(a,n,k)算法的功能。
  (2)当 a [ ] a[] a[]={2,6,3,8,1,7,4,9}时,执行 fun( a , n , 1 a,n,1 an1) 后的返回结果是什么?一共进行了几次比较。
  (3)当 a [ ] a[] a[]={2,6,3,8,1,7,4,9}时,执行fun( a , n , 5 a,n,5 an5) 后的返回结果是什么?一共进行了几次比较。

答:(1)fun( a , n , k a,n,k ank)算法的功能是在数组 a [ 0.. n − 1 ] a[0..n-1] a[0..n1] 中查找元素值为 k k k 的元素。若找到了返回 k k k 对应元素的下标;否则返回 -1。算法先在奇数序号的元素中查找,如没有找到,再在偶数序号的元素中查找。
  (2)当 a [ ] a[] a[]={2,6,3,8,1,7,4,9} 时,执行 fun( a , n , 1 a,n,1 an1) 后的返回结果是 4,表示查找成功。一共进行了 3 次比较。
  (3)当 a [ ] a[] a[]={2,6,3,8,1,7,4,9} 时,执行 fun( a , n , 5 a,n,5 an5) 后的返回结果是 -1,表示查找不成功。一共进行了 8 次比较。

4. 假设一棵二叉排序树的关键字为单个字母,其后序遍历序列为 ACDBFIJHGE,回答以下问题:

  (1)画出该二叉排序树;
  (2)求在等概率下的查找成功的平均查找长度。
  (3)求在等概率下的查找不成功的平均查找长度。

答:(1)该二叉排序树的后序遍历序列为 A C D B F I J H G E ACDBFIJHGE ACDBFIJHGE,则中序遍历序列为
A B C D E F G H I J ABCDEFGHIJ ABCDEFGHIJ,由后序序列和中序序列构造的二叉排序树如图9.5所示。

图 9.5 一棵二叉排序树

  (2) A S L 成 功 = ( 1 × 1 + 2 × 2 + 4 × 3 + 2 × 4 + 1 × 5 ) / 10 = 3 ASL_{成功}=(1×1+2×2+4×3+2×4+1×5)/10=3 ASL=(1×1+2×2+4×3+2×4+1×5)/10=3
  (3) A S L 不 成 功 = ( 6 × 3 + 3 × 4 + 2 × 5 ) / 11 = 3.64 ASL_{不成功}=(6×3+3×4+2×5)/11=3.64 ASL=(6×3+3×4+2×5)/11=3.64

5. 证明如果一棵非空二叉树(所有结点值均不相同)的中序遍历序列是从小到大有序的,则该二叉树是一棵二叉排序树。

证明:对于关键字为k的任一结点a,由中序遍历过程可知,在中序遍历序列中,它的左子树的所有结点的关键字排在 k k k 的左边,它的右子树的所有结点的关键字排在 k k k 的右边,由于中序序列是从小到大排列的,所以结点 a a a 的左子树中所有结点的关键字小于 k k k,结点 a a a 的右子树中所有结点的关键字大于 k k k,这满足二叉排序树的性质,所以该二叉树是一棵二叉排序树。

6. 由 23、12、45 关键字构成的二叉排序树有多少棵,其中属于平衡二叉树的有多少棵?

答:这里n=3, 构 成 的 二 叉 排 序 树 的 个 数 = 1 n + 1 C 2 n n = 5 构成的二叉排序树的个数= \frac{1}{n+1} C_{2n}^{n} = 5 =n+11C2nn=5,如图9.6所示。
其中的平衡二叉树有1棵,为图中第3棵。

图 9.6 5 棵二叉排序树

7. 将整数序列(4,5,7,2,1,3,6)中的元素依次插入到一棵空的二叉排序树中,试构造相应的二叉排序树,要求用图形给出构造过程。

答:构造一棵二叉排序树过程如图9.7所示。

图 9.7 构造二叉排序树过程

8. 将整数序列(4,5,7,2,1,3,6)中的元素依次插入到一棵空的平衡二叉树中,试构造相应的平衡二叉树,要求用图形给出构造过程。

答:构造一棵平衡二叉树过程如图9.8所示。

图 9.8 构造平衡二叉树过程

9. 已知一棵5阶B-树中有53个关键字,则树的最大高度是多少?

答:当每个结点的关键字个数都最少时,该 B − 树 B-树 B 的高度最大。根结点最少有 1 个关键字、2 棵子树,第 1 层至少有 1 个结点。除根结点外每个结点至少有 [ 5 / 2 ] − 1 = 2 [5/2]-1=2 [5/2]1=2 个关键字、3 棵子树,则第 2 层至少有 2 个结点,共 2×2=4 个关键字。第3层至少有 2×3 个结点,共 2×3×2=12 个关键字。第4层至少有 6×2 个结点,共 6×3×2=36 个关键字。而 1+4+12+36=53,加上外部结点层,该 B − 树 B-树 B 中最大高度是 5 层。

10. 设有一组关键字(19,1,23,14,55,20,84,27,68,11,10,77),其哈希函数为 h ( k e y ) h(key) h(key)= k e y key key % 13。采用开放地址法的线性探测法解决冲突,试在 0~18 的哈希表中对该关键字序列构造哈希表,并求在成功和不成功情况下的平均查找长度。

答:依题意, m m m=19,利用线性探测法计算下一地址的计算公式为:
   d 0 = h ( k e y ) d_{0}=h(key) d0=h(key)
   d j + 1 = ( d j + 1 ) d_{j+1}=(d_{j}+1) dj+1=(dj+1) % m      j j j=0,1,2,…
计算各关键字存储地址的过程如下:
   h ( 19 ) h(19) h(19)=19 % 13=6, h ( 1 ) h(1) h(1)=1 % 13=1, h ( 23 ) h(23) h(23)=23 % 13=10
   h ( 14 ) h(14) h(14)=14 % 13=1(冲突), h ( 14 ) h(14) h(14)=(1+1) % 19 =2
   h ( 55 ) h(55) h(55)=55 % 13=3, h ( 20 ) h(20) h(20)=20 % 13=7
   h ( 84 ) h(84) h(84)=84 % 13=6(冲突), h ( 84 ) h(84) h(84)=(6+1) % 19=7(仍冲突), h ( 84 ) h(84) h(84)=(7+1) % 19=8
   h ( 27 ) h(27) h(27)=27 % 13=1(冲突), h ( 27 ) h(27) h(27)=(1+1) % 19=2(仍冲突), h ( 27 ) h(27) h(27)=(2+1) % 19=3(仍冲突), h ( 27 ) h(27) h(27)=(3+1) % 19=4
   h ( 68 ) h(68) h(68)=68 % 13=3(冲突), h ( 68 ) h(68) h(68)=(3+1) % 19=4(仍冲突), h ( 68 ) h(68) h(68)=(4+1) % 19=5
   h ( 11 ) h(11) h(11)=11 % 13=11
   h ( 10 ) h(10) h(10)=10 % 13=10(冲突), h ( 10 ) h(10) h(10)=(10+1) % 19=11(仍冲突), h ( 10 ) h(10) h(10)=(11+1) % 19=12$
   h ( 77 ) h(77) h(77)=77 % 13=12(冲突), h ( 77 ) h(77) h(77)=(12+1) % 19=13
 
因此,构建的哈希表如表 9.1 所示。

表 9.1 哈希表
下标0123456789101112131415161718
key11455276819208423111077
探测次数121431131132

  表中探测次数即为相应关键字成功查找时所需比较关键字的次数,因此:
   A S L 成 功 = ( 1 + 2 + 1 + 4 + 3 + 1 + 1 + 3 + 1 + 1 + 3 + 2 ) / 12 = 1.92 ASL_{成功}=(1+2+1+4+3+1+1+3+1+1+3+2)/12=1.92 ASL=(1+2+1+4+3+1+1+3+1+1+3+2)/12=1.92
  查找不成功表示在表中未找到指定关键字的记录。以哈希地址是 0 的关键字为例,由于此处关键字为空,只需比较 1 次便可确定本次查找不成功;以哈希地址是 1 的关键字为例,若该关键字不在哈希表中,需要将它与从 1~9 地址的关键字相比较,由于地址 9 的关键字为空,所以不再向后比较,共比较 9 次,其他的依次类推,所以得到如 表 9.2 所示结果。

表 9.2 不成功查找的探测次数
下标0123456789101112131415161718
key11455276819208423111077
探测次数1987654321543211111

  而哈希函数为 h ( k e y ) h(key) h(key)= k e y key key % 13,所以只需考虑 h ( k e y ) h(key) h(key)=0~12 的情况,即:
   A S L 不 成 功 = ( 1 + 9 + 8 + 7 + 6 + 5 + 4 + 3 + 2 + 1 + 5 + 4 + 3 ) / 13 = 58 / 13 = 4.46 ASL_{不成功}=(1+9+8+7+6+5+4+3+2+1+5+4+3)/13=58/13=4.46 ASL=(1+9+8+7+6+5+4+3+2+1+5+4+3)/13=58/13=4.46

11. 设计一个折半查找算法,求查找到关键字为 k k k 的记录所需关键字的比较次数。假设 k k k R [ i ] . k e y R[i].key R[i].key 的比较得到 3 种情况,即 k = = R [ i ] . k e y k==R[i].key k==R[i].key k < R [ i ] . k e y k<R[i].key k<R[i].key 或者 k > R [ i ] . k e y k>R[i].key k>R[i].key,计为 1 次比较(在教材中讨论关键字比较次数时都是这样假设的)。

解:用 c n u m cnum cnum 累计关键字的比较次数,最后返回其值。由于题目中的假设,实际上 c n u m cnum cnum 是求在判定树中比较结束时的结点层次(首先与根结点比较,所以 c n u m cnum cnum 初始化为 1)。对应的算法如下:

int BinSearch1(RecType R[],int n,KeyType k)
{ int low=0,high=n-1,mid;
    int cnum=1; //成功查找需要 1 次比较
    while (low<=high)
    { mid=(low+high)/2;
        if (R[mid].key==k)
            return cnum;
        else if (k<R[mid].key)
            high=mid-1;
        else
            low=mid+1;
        cnum++;
    }
    cnum--; //不成功查找比较次数需要减 1
    return cnum;
}

12. 设计一个算法,判断给定的二叉树是否是二叉排序树。假设二叉树中结点关键字均为正整数且均不相同。

解:对二叉排序树来说,其中序遍历序列为一个递增有序序列。因此,对给定的二叉树进行中序遍历,如果始终能保持前一个值比后一个值小,则说明该二叉树是一棵二叉排序树。对应的算法如下:

KeyType predt=-32768; //predt 为全局变量,保存当前结点中序前驱的值,初值为-∞
bool JudgeBST(BSTNode *bt)
{	bool b1,b2;
    if (bt==NULL)
        return true;
    else
    {	b1=JudgeBST(bt->lchild); //判断左子树
        if (b1==false) //左子树不是 BST,返回假
            return false;
        if (bt->key<predt) //当前结点违反 BST 性质,返回假
            return false;
        predt=bt->key;
        b2=JudgeBST(bt->rchild); //判断右子树
        return b2;
    }
}

13. 设计一个算法,在一棵非空二叉排序树 b t bt bt 中求出指定关键字为 k k k 结点的层次。

解:采用循环语句边查找边累计层次 l v lv lv。当找到关键字为 k k k 的结点时返回 l v lv lv;否则返回 0。对应的算法如下:

int Level(BSTNode *bt, KeyType k) {
    int lv = 1; //层次 lv 置初值 1
    BSTNode *p = bt;
    while (p != NULL && p->key != k) //二叉排序树未找完或未找到则循环
    {
        if (k < p->key)
            p = p->lchild; //在左子树中查找
        else
            p = p->rchild; //在右子树中查找
        lv++; //层次增 1
    }
    if (p != NULL) //找到后返回其层次
        return lv;
    else
        return (0); //表示未找到
}

14. 设计一个哈希表 h a [ 0.. m − 1 ] ha[0..m-1] ha[0..m1] 存放 n n n 个元素,哈希函数采用除留余数法 H ( k e y ) H(key) H(key)= k e y key key % p p p p ⩽ m p \leqslant m pm),解决冲突的方法采用开放定址法中的平方探测法。

(1)设计哈希表的类型。
(2)设计在哈希表中查找指定关键字的算法。

解:哈希表为 h a [ 0.. m − 1 ] ha[0..m-1] ha[0..m1],存放 n n n个元素,哈希函数为 H ( k e y ) H(key) H(key)= k e y key key % p p p p ⩽ m p \leqslant m pm)。平方探测法: H i = ( H ( k e y ) + d i ) m o d m ( 1 ⩽ i ⩽ m − 1 ) H_{i}=(H(key)+d_{i}) mod m(1 \leqslant i \leqslant m-1) Hi=(H(key)+di)modm1im1,其中, d i = 1 2 、 − 1 2 、 2 2 、 − 2 2 、 ⋅ ⋅ ⋅ di=1^2、-1^2、2^2、-2^2、··· di=12122222
  (1)设计哈希表的类型如下:

#define MaxSize 100 //定义最大哈希表长度
#define NULLKEY -1 //定义空关键字值
#define DELKEY -2 //定义被删关键字值
typedef int KeyType; //关键字类型
typedef char *InfoType; //其他数据类型
typedef struct {
    KeyType key; //关键字域
    InfoType data; //其他数据域
    int count; //探测次数域
} HashTable[MaxSize]; //哈希表类型

  (2)对应的算法如下:

int SearchHT1(HashTable ha, int p, int m, KeyType k) //在哈希表中查找关键字 k
{
    int adr, adr1, i = 1, sign;
    adr = adr1 = k % p; //求哈希函数值
    sign = 1;
    while (ha[adr].key != NULLKEY && ha[adr].key != k) //找到的位置不空
    {
        adr = (adr1 + sign * i * i) % m;
        if (sign == 1)
            sign = -1;
        else //sign==-1
        {
            sign = 1;
            i++;
        }
    }
    if (ha[adr].key == k) //查找成功
        return adr;
    else //查找失败
        return -1;
}

如有侵权,请联系作者删除

  • 4
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小饅頭

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值