3-1 编写打印出一个单链表的所有元素的程序。
void PrintList(List L)
{
Position p;
p = L->Next;
while (p != NULL)
{
printf("%d ", p->Element);
p = p->Next;
}
printf("\n");
}
3-2 给你一个链表L和另一个链表P,它们包含以升序排列的整数。操作PrintLots(L, P)将打印L中那些由P所指定的位置上的元素。例如,如果p = 1, 3, 4, 6,那么,L中的第1、第3、第4和第6个元素被打印出来。写出过程PrintLots(L, P)。你应该只使用基本的表操作。该过程的运行时间是多少?
void PrintLots(List L, List P)
{
int count = 1;
Position L_loc, P_loc;
L_loc = L->Next;
P_loc = P->Next;
while (P_loc != NULL && L_loc != NULL)
{
if (P_loc->Element == count++)
{
printf("%d ", L_loc->Element);
P_loc = P_loc->Next;
}
L_loc = L_loc->Next;
}
printf("\n");
}
运行时间是。
3-3 通过只调整指针(而不是数据)来交换两个相邻的元素,使用
a. 单链表。
b. 双链表。
a. 用单链表实现
/* BeforeP is the cell before the two adjacent cells that are to be swapped. */
/* Error checks are omitted for clarity. */
void SwapWithNext(List L, Position BeforeP)
{
Position P, AfterP;
P = BeforeP->Next;
AfterP = P->Next;
P->Next = AfterP->Next;
BeforeP->Next = AfterP;
AfterP->Next = P;
}
b. 用双链表实现
/* P and AfterP are cells to be switched. Error checks as before. */
void SwapWithNext( Position P, List L )
{
Position BeforeP, AfterP;
BeforeP = P->Prev;
AfterP = P->Next;
P->Next = AfterP->Next;
BeforeP->Next = AfterP;
AfterP->Next = P;
P->Next->Prev = P;
P->Prev = AfterP;
AfterP->Prev = BeforeP;
}
分析:使用双链表来交换两个元素,不仅要重构元素之间的后继关系,还要重构其前驱关系,所以链表操作代价翻倍。
3.4 给定两个已排序的表L1和L2,只使用基本的表操作编写计算的过程。
void Intersect(List L1, List L2, List L3)
{
Position p1, p2, p3;
p1 = L1->Next;
p2 = L2->Next;
p3 = L3;
while (p1 != NULL && p2 != NULL)
{
if (p1->Element < p2->Element)
p1 = p1->Next;
else if (p1->Element == p2->Element)
{
p3->Next = p1;
p3 = p1;
p1 = p1->Next;
p2 = p2->Next;
}
else
p2 = p2->Next;
}
p3->Next = NULL;
}
3.5 给定两个已排序的表L1和L2,只使用基本的表操作编写计算的过程。
void Union(List L1, List L2, List L3)
{
Position p1, p2, p3;
p1 = L1->Next;
p2 = L2->Next;
p3 = L3;
while (p1 != NULL && p2 != NULL)
{
if (p1->Element < p2->Element)
{
p3->Next = p1;
p3 = p1;
p1 = p1->Next;
}
else if (p1->Element == p2->Element)
{
p3->Next = p1;
p3 = p1;
p1 = p1->Next;
p2 = p2->Next;
}
else
{
p3->Next = p2;
p3 = p2;
p2 = p2->Next;
}
}
if (p1 != NULL)
p3->Next = p1;
else
p3->Next = p2;
}
3.6 编写将两个多项式相加的函数。不要毁坏输入数据。用一个链表实现。如果这两个多项式分别有M项和N项,那么你的程序的时间复杂度是多少?
void AddPoly(const Polynomial Poly1, const Polynomial Poly2, Polynomial PolySum)
{
int sum;
Polynomial p1, p2, p3, TmpCell;
p1 = Poly1->Next;
p2 = Poly2->Next;
p3 = PolySum;
while (p1 != NULL && p2 != NULL)
{
if (p1->Exponent > p2->Exponent)
{
p3->Next = p1;
p3 = p1;
p1 = p1->Next;
}
else if (p1->Exponent < p2->Exponent)
{
p3->Next = p2;
p3 = p2;
p2 = p2->Next;
}
else
{
sum = p1->Coefficient + p2->Coefficient;
if (sum)
{
TmpCell = malloc(sizeof(struct Node));
TmpCell->Coefficient = sum;
TmpCell->Exponent = p1->Exponent;
p3->Next = TmpCell;
p3 = TmpCell;
}
p1 = p1->Next;
p2 = p2->Next;
}
}
if (p1 != NULL)
p3->Next = p1;
else
p3->Next = p2;
}
时间复杂度为。
3.7 编写一个函数将两个多项式相乘,用一个链表实现。你必须保证输出的多项式按幂次排列并且最多有一项为任意幂。
a. 给出以时间求解该问题的算法。
b. 编写一个以时间执行乘法的程序,其中M是具有较少项数的多项式的项数。
c. 编写一个以时间执行乘法的程序。
d. 上面哪个的时间界最好?
分析:
(a) 一种算法是将相乘后的结果多项式保存在按幂次排列的链表中。个乘积每个都要遍历一次链表以合并同类项。由于链表的大小为
,所以总的时间复杂度为
。
(b) 时间界可以通过将一个多项式的一项和另一个多项式的各项相乘来降低,然后利用类似练习3.2的函数插入相乘后的整个序列到项数较少的多项式中。每个序列耗时,但是一共只有M个序列,所以时间界为
。
(c) 可以算出所有的项乘积,然后利用
的排序算法,将结果按幂次排列,最后合并同类项所花的时间很小。
(d) 该问题选择哪种算法取决于M和N的相对大小。如果两者相近,则选择算法(c)更好。如果其中有一个多项式的项数很小,那么选择算法(b)更好。
3.8 编写一个程序,输入一个多项式,计算出
。你的程序的时间复杂度是多少?至少再提出一种对
和
的某些可能的选择具有竞争性的解法。
分析:可以利用类似第2章的幂运算函数来解决这个问题。如果P较小,花费而不是
的算法可能更好,这是因为
的项数可能很多而
很小,那么这种情形用3.7中的算法(b)更好。
3.10 Josephus问题是下面的游戏:N个人从1到N编号,围坐成一个圆圈。从1号开始传递一个热土豆。经过M次传递后拿着热土豆的人被清除离座,围坐的圆圈缩进,由坐在被清除的人后面的人拿起热土豆继续进行游戏。最后剩下的人获胜。因此,如果和
,则依次被清除后,5号获胜。如果
和
,那么被清除的人的顺序是2,4,1,5.
a. 编写一个程序解决M和N在一般数值县的Josephus问题,应使你的程序尽可能地高效,要确保能够清楚单元。
b. 你的程序的运行时间是多少?
c. 如果,你的程序的运行时间是多少?对于大的N(
),其free例程是如何影响实际速度的?
代码:
void Josephus(List L, int m, int n)
{
Position p, tmp;
p = L->Next;
int i, count = 0;
while (n > 1)
{
count = m % n;
for (i = 0; i < count; i++)
p = p->Next;
tmp = p->Next;
p->Previous->Next = tmp;
tmp->Previous = p->Previous;
free(p);
p = tmp;
--n;
}
printf("%d\n", p->Element);
}
分析:这是一个典型的规划问题。可以通过设
3.12
a. 编写一个非递归过程以时间反转单链表。
*b. 使用常数附加空间编写一个过程以时间反转单链表。
代码:
void Reverse(List L)
{
Position p, tmp;
p = L->Next;
L->Next = NULL;
while (p != NULL)
{
tmp = p->Next;
p->Next = L->Next;
L->Next = p;
p = tmp;
}
}
分析:该算法类似创建链表的头插法。
3.15
a. 写出自调整表的数组实现。自调整表如同一个规则的表,但是所有的插入都在表头进行,当一个元素被Find访问时,它就被移到表头而并不改变其余的项的相对顺序。
b. 写出自调整表的链表实现。
*c. 设每个元素都有其被访问的固定概率。证明那些具有最高访问概率的元素都靠近表头。
a.
Position Find(ElementType X, List L)
{
int i, Where;
Where = 0;
for (i = 1; i < L.SizeOfList; i++)
{
if (X == L[i].Element)
{
Where = i;
break;
}
}
if (Where)
{
for (i = Where; i > 1; i--)
L[i].Element = L[i-1].Element;
L[1].Element = X;
return 1;
}
else
return 0;
}
b.
Position Find(ElementType X, List L)
{
Position PrevPos, XPos;
PrevPos = FindPrevious(X, L);
if (PrevPos->Next != NULL)
{
XPos = PrevPos->Next;
PrevPos->Next = XPos->Next;
XPos->Next = L->Next;
L->Next = XPos;
return XPos;
}
else
return NULL;
}
3.16 假设我们有一个基于数组的表A[0..N-1],并且我们想要删除所有相同的元素。LastPosition初始值为N-1,但该值随着相同元素被删除而变得越来越小。考虑如下伪码程序段。过程Delete删除位置j上的元素并使表破坏。
for (i = 0;i < LastPosition; i++)
{
j = i + 1;
while (j < LastPosition)
{
if (A[i] == A[j])
Delete(j);
else
j++;
}
}
a. 解释该过程是如何进行工作的。
b. 利用一般的表操作重写这个过程。
*c. 如用标准的数组实现,则这个过程的运行时间是多少?
d. 使用链表实现的运行时间是多少?
*e. 给出一个算法以时间解决该问题。
**f. 证明:如果只使用比较,那么解决该问题的任何算法都需要次比较。
*g. 证明:如果我们允许除比较之外的其他操作,并且这些关键字都是实数,那么我们不用元素间的比较就可以解决该问题。
分析:
(c) 因为Delete过程需要的时间,而该过程用嵌套在两重循环中,其中每个循环的大小都是N,所以总的时间复杂度为
。
(d) 因为链表删除元素的代价是常数级的,所以,总的时间复杂度为。
(e) 以的时间排序链表中的元素,这样使得重复的元素肯定是相邻的,然后只需遍历一遍链表就可以去除重复的元素。
3.17 不同于我们已经给出的删除方法,另一种是使用懒惰删除的方法。为了删除一个元素,我们只标记上该元素被删除(使用一个附加的位域)。表中被删除和非被删除元素的个数作为数据结构的一部分被保留。如果被删除元素和非被删除元素一样多,我们遍历整个表,对所有被标记的节点执行标准的删除算法。
a. 列出懒惰删除的优点和缺点。
b. 编写实现使用懒惰删除的标准链表操作的例程。
a.
分析: 优点在于该方法更容易编码,而且如果要删除的元素有重复,那么设置标志位会降低时间复杂度。缺点是空间复杂度代价较大,需要给每个元素设置附加的位域,并且一些没有用到元素也没有被释放。
3.18 编写检测下列语言平衡符号的程序:
a. Pascal(begin/end, ( ), [ ], { })。
b. C(/* */, ( ), [ ], { })。
*c. 解释如何打印出错信息
代码:
int Balance(char *Filename)
{
Stack S = CreateStack(10);
char ch;
FILE *fp;
fp = fopen(Filename, "r");
if (fp == NULL)
printf("cannot open the file!");
ch = fgetc(fp);
while (ch != EOF)
{
if (ch == '(' || ch == '[' || ch == '{')
Push(ch, S);
if (IsEmpty(S))
return 0;
else
{
if (ch == ')' && TopAndPop(S) != '('
|| ch == ']' && TopAndPop(S) != '['
|| ch == '}' && TopAndPop(S) != '{')
{
return 0;
}
}
ch = fgetc(fp);
}
fclose(fp);
if (IsEmpty(S))
return 1;
else
return 0;
}
3.19 编写一个程序计算后缀表达式的值。
代码:
ElementType PostCalculate(char *Filename)
{
Stack S = CreateStack(10);
int operand1, operand2;
char ch, temp[2];
temp[1] = '\0';
FILE *fp;
fp = fopen(Filename, "r");
if (fp == NULL)
printf("cannot open the file!");
ch = fgetc(fp);
while (ch != EOF)
{
if (ch >= 48 && ch <= 57)
{
temp[0] = ch;
Push(atoi(temp), S);
}
else
{
operand2 = TopAndPop(S);
operand1 = TopAndPop(S);
switch(ch)
{
case '+': Push(operand1 + operand2, S);break;
case '-': Push(operand1 - operand2, S);break;
case '*': Push(operand1 * operand2, S);break;
case '/': Push(operand1 / operand2, S);break;
}
}
ch = fgetc(fp);
}
return TopAndPop(S);
}
3.20
a. 编写一个程序将中缀表达式转换成后缀表达式,该中缀表达式包含“(”、“)”、“+”、“-”、“*”和“/”。
b. 把幂操作符添加到你的指令系统中去。
c. 编写一个程序将后缀表达式转换成中缀表达式。
a.
char Operator[6] = {'(', ')', '+', '-', '*', '/'};
char Priority[6][6] = {
'<', '=', '>', '>', '>', '>',
' ', ' ', ' ', ' ', ' ', ' ',
'<', ' ', '=', '=', '<', '<',
'<', ' ', '=', '=', '<', '<',
'<', ' ', '>', '>', '=', '=',
'<', ' ', '>', '>', '=', '='
};
int Compare(char operator1, char operator2)
{
int i, j;
for (i = 0; i < 6; i++)
{
if(Operator[i] == operator1)
break;
}
for (j = 0; j < 6; j++)
{
if(Operator[j] == operator2)
break;
}
switch(Priority[i][j])
{
case '=': return 0;
case '>': return 1;
case '<': return -1;
}
}
void PostfixToInfix(char *Filename)
{
Stack S = CreateStack(10);
char ch, top;
FILE *fp;
if ((fp = fopen(Filename, "r")) == NULL)
printf("cannot open the file!\n");
ch = fgetc(fp);
while (ch != EOF)
{
if (ch >= 65 && ch <= 90 || ch >= 97 && ch <= 122)
putchar(ch);
else
{
if (ch != ')')
{
while (!IsEmpty(S) && (top = Top(S)) != '(' && Compare(top, ch) != -1)
{
putchar(top);
Pop(S);
}
Push(ch, S);
}
else
{
while ((top = Top(S)) != '(')
{
putchar(top);
Pop(S);
}
Pop(S);
}
}
ch = fgetc(fp);
}
fclose(fp);
while (!IsEmpty(S))
putchar(TopAndPop(S));
}
b.
分析:将^操作符及其优先级加入OPerator数组和Priority数组,注意当相邻的两个操作符都是幂操作时,表达式是从右到左结合的。
c.
分析:后缀转中缀的程序与计算后缀表达式值的程序类似,只不过栈存的不再是每一步计算的结果,而是表达式。
3.21 编写仅用一个数组而实现两个栈的例程。除非数组的每一个单元都被使用,否则你的栈例程不能有溢出声明。
分析:一个栈从低端到高端向上增长,而另一个栈从高端到低端向下增长。
3.22
*a. 提出支持栈的Push和Pop操作以及第三种操作FindMin的数据结构,其中FindMin返回该数据结构的最小元素,所有操作在最坏的情况下的运行时间都是。
*b. 证明,如果我们加入第四种操作DeleteMin,那么至少有一种操作必须花费时间,其中,DeleteMin找出并删除最小的元素。(本题需要阅读第7章)
(a) 假设扩展后的栈为E。我们将用两个子栈来实现E。其中S栈用来记录Push和Pop操作,而另一个M栈用来记录最小元素。我们利用Push(X, S)来实现Push(X, E)。如果X小于或等于M栈的栈顶元素,我们将X同时也压入M栈。我们利用Pop(S)来实现Pop(E)。如果X等于M栈的栈顶元素,我们同时弹出M栈的栈顶元素。FindMin(E)可以通过取M栈的栈顶元素实现。以上所有的操作都明显是常数级的。
*3.23 说明如何用一个数组实现三个栈。
分析:一个栈从低端到高端向上增长,第二个栈从高端到低端向下增长,而第三个栈可以从数组中间向某一方向增长。如果第三个栈和其他两个栈中的任意一个栈有冲突,那么需要移动这个栈。一个合理的策略是移动栈使得栈的中心在另外两个栈的栈顶之间。
3.24 在2.4节中用于计算斐波那契数的递归例程如果在N=50下运行,栈空间有可能用完吗?为什么?
分析:不会用完。因为只有49次调用记录会存放在栈中。然而,如第2章所示,运行时间是指数级的,因此这个例程不会在一个合理数量级时间内终止。
3.25 编写实现队列的例程,使用
a. 链表
b. 数组
a.
分析:队列的链表实现的缺点和栈的类似,即对于malloc和free的调用的开销是昂贵的。所以队列也一般用数组实现。
b.
struct QueueRecord;
typedef struct QueueRecord *Queue;
typedef int ElementType;
#define MinQueueSize (5)
struct QueueRecord
{
int Capacity;
int Front;
int Rear;
int Size;
ElementType *Array;
};
void MakeEmpty(Queue Q)
{
Q->Size = 0;
Q->Front = 1;
Q->Rear = 0;
}
Queue CreateQueue(int MaxElements)
{
Queue Q;
if (MaxElements < MinQueueSize)
printf("Queue size is too small");
Q = malloc(sizeof(struct QueueRecord));
if (Q == NULL)
printf("Out of space!!!");
Q->Array = malloc(sizeof(ElementType) * MaxElements);
if (Q->Array == NULL)
printf("Out of space!!!");
Q->Capacity = MaxElements;
MakeEmpty(Q);
return Q;
}
void DisposeQueue(Queue Q)
{
if (Q != NULL)
{
free(Q->Array);
free(Q);
}
}
int IsEmpty(Queue Q)
{
return Q->Size == 0;
}
int IsFull(Queue Q)
{
return Q->Size == Q->Capacity;
}
static int Succ(int Value, Queue Q)
{
if (++Value == Q->Capacity)
Value = 0;
return Value;
}
void Enqueue(ElementType X, Queue Q)
{
if (IsFull(Q))
printf("Full queue");
else
{
Q->Size++;
Q->Rear = Succ(Q->Rear, Q);
Q->Array[Q->Rear] = X;
}
}
ElementType Front(Queue Q)
{
if (!IsEmpty(Q))
return Q->Array[Q->Front];
printf("Empty queue");
return 0;
}
void Dequeue(Queue Q)
{
if (IsEmpty(Q))
printf("Empty queue");
else
{
Q->Size--;
Q->Front = Succ(Q->Front, Q);
}
}
ElementType FrontAndDequeue(Queue Q)
{
ElementType Tmp;
if (!IsEmpty(Q))
{
Tmp = Q->Array[Q->Front];
Dequeue(Q);
return Tmp;
}
printf("Empty queue");
return 0;
}
3.26 双端队列(deque)是由一些项的表组成的数据结构,对该数据结构可以进行下列操作:
Push(X, D):将项X插入到双端队列D的前端。
Pop()D:从双端队列D中删除前端项并将其返回。
Inject(X, D):将项X插入到双端队列D的尾端。
Eject(D):从双端队列D中删除尾端项并将其返回。
编写支持双端队列的例程,每种操作均花费时间。
代码:
附1:链表的指针实现
typedef struct Node* PtrToNode;
typedef PtrToNode List;
typedef PtrToNode Position;
struct Node
{
int Element;
Position Next;
};
void CreateListHead(List L, int n)
{
Position p;
int i;
//L = malloc( sizeof(struct Node) );
L -> Next = NULL;
for (i = n; i > 0; i--)
{
p = malloc( sizeof(struct Node) );
p -> Element = pow(2, i - 1) - 1;
p -> Next = L -> Next;
L -> Next = p;
}
}
void CreateListTail(List L, int n)
{
Position p, r;
int i;
//L = malloc( sizeof(struct Node) );
r = L;
for (i = 0; i < n; i++)
{
p = malloc( sizeof(struct Node) );
p -> Element = i + 1;
r -> Next = p;
r = p;
}
r -> Next = NULL;
}
void PrintList(List L)
{
Position p;
p = L -> Next;
while (p != NULL)
{
printf("%d ", p -> Element);
p = p -> Next;
}
printf("\n");
}
int IsEmpty(List L)
{
return L -> Next == NULL;
}
int IsLast(List L, Position p)
{
return p -> Next == NULL;
}
Position Find(List L, int x)
{
Position p;
p = L -> Next;
while (p != NULL && p -> Element != x)
p = p -> Next;
return p;
}
Position FindPrevious(List L, int x)
{
Position p;
p = L -> Next;
while (p != NULL && p -> Next -> Element != x)
p = p -> Next;
return p;
}
void Delete(List L, int x)
{
Position p, TmpCell;
p = FindPrevious(L, x);
if ( !IsLast(L, p))
{
TmpCell = p -> Next;
p -> Next = TmpCell -> Next;
free(TmpCell);
}
}
void DeleteList(List L)
{
Position p, Tmp;
p = L -> Next;
L -> Next = NULL;
while (p != NULL)
{
Tmp = p -> Next;
free(p);
p = Tmp;
}
}
void Insert(List L, Position p, int x)
{
Position TmpCell;
TmpCell = malloc(sizeof(struct Node));
if (TmpCell == NULL)
{
printf("out of space!\n");
exit(0);
}
TmpCell -> Element = x;
TmpCell -> Next = p -> Next;
p -> Next = TmpCell;
}
附2:栈的数组实现
struct StackRecord;
typedef struct StackRecord *Stack;
typedef char ElementType;
#define EmptyTOS (-1)
#define MinStackSize (5)
struct StackRecord
{
int Capacity;
int TopOfStack;
ElementType *Array;
};
void MakeEmpty(Stack S)
{
S->TopOfStack = EmptyTOS;
}
Stack CreateStack(int MaxElements)
{
Stack S;
if (MaxElements < MinStackSize)
printf("Stack size is too small");
S = malloc(sizeof(struct StackRecord));
if (S == NULL)
printf("Out of space!!!");
S->Array = malloc(sizeof(ElementType) * MaxElements);
if (S->Array == NULL)
printf("Out of space!!!");
S->Capacity = MaxElements;
MakeEmpty(S);
return S;
}
void DisposeStack(Stack S)
{
if (S != NULL)
{
free(S->Array);
free(S);
}
}
int IsEmpty(Stack S)
{
return S->TopOfStack == EmptyTOS;
}
int IsFull(Stack S)
{
return S->TopOfStack == S->Capacity - 1;
}
void Push(ElementType X, Stack S)
{
if (IsFull(S))
printf("Full stack");
else
S->Array[++S->TopOfStack] = X;
}
ElementType Top(Stack S)
{
if (!IsEmpty(S))
return S->Array[S->TopOfStack];
printf("Empty stack");
return 0;
}
void Pop(Stack S)
{
if (IsEmpty(S))
printf("Empty stack");
else
S->TopOfStack--;
}
ElementType TopAndPop(Stack S)
{
if (!IsEmpty(S))
return S->Array[S->TopOfStack--];
printf("Empty stack");
return 0;
}