显示标签为“数据结构习题解答”的博文。显示所有博文
显示标签为“数据结构习题解答”的博文。显示所有博文
8-4 用邻接矩阵表示图,若图中有1000个顶点,1000条边,则形成的邻接矩阵有多少矩阵元素?有多少非零元素?是否稀疏矩阵?


形成的邻接矩阵有10^6个元素;如果是有向图则拥有1000个非零元素,若是无向图则有1000×2=2000个非零元素;是稀疏矩阵。


8-5 用邻接矩阵表示图时,矩阵元素的个数与顶点个数是否相关?与边的条数是否相关?

矩阵元素的个数与顶点个数相关。非零元素与边的条数相关。


8-6 有n个顶点的无向连通图至少有多少条边?有n个顶点的有向强连通图至少有多少条边?试举例说明。

n个顶点连通,如果将n个顶点排成一直线,则很容易知道,至少需要n-1条边才能把它连通。
还是和刚才一样,只不过多一条边,将首尾连起来,使得从每个定点开始都可以回到该顶点。
例子已举。


8-7 对于有n个顶点的无向图,采用邻接矩阵表示,如何判断以下问题:图中有多少条边?任意两个顶点i和j之间是否有边相连?任意一个顶点的度是多少?

图中的边数等于邻接矩阵中非零元素的一半;查看矩阵中E[i][j]或者E[j][i]是否等于零,零的话不联通,反之连通;假设要观察顶点i的度,只要查看i行有多少个非零元素,即为i的度(如果查看i列也一样,因为无向图的邻接矩阵是行列对称的)。


8-9 试扩充深度优先搜索算法,在遍历图的过程中建立生成森林的左子女——右兄弟链表。算法的首部为
Void Graph::DFS(const int v, int visited[], TreeNode< int > *t)
其中,指针t指向生成森林上具有图顶点v信息的根结点。


答案建立生成森林的主函数里的q,定义很奇怪。还是老老实实按照书中DFS的定义来写。
声明:不写模板了。可以直接修改TreeNode的指针.

void Graph::DFS(){ //主过程
int* visited=new int[n]; //n为图中顶点的个数
for(int i=0;i< n;i++)visited[i]=0; //初始化辅助数组
Tree< int > &T; //这里要不要写成 Tree T? 还是迷糊
DFS(0,visited,T.root); //从顶点0开始构建生成森林
delete[] visited;
}

void Graph::DFS(const int v, int visited[], TreeNode< int > *t){
t.SetData(GetValue(v)); //访问该顶点,拷贝值到相应森林结点。
visited[v]=1; //修改为已访问
int w=GetFirstNeighbor(v); //找到顶点v的第一个邻接顶点w
t=t->FirstChild; //t指向t的左子女
while(w!=1){ //有邻接顶点
if(!visited[w]) //如果没有访问过
DFS(w,visited,t); //从该顶点开始建立生成森林
w=GetNextNeighbor(v,w); //找到v的下一个邻接顶点
t=t->NextSibling; //同时把t指向t的右兄弟
}
}



8-10 用邻接表表示图时,顶点个数设为n,边的条数设为e,在邻接表上执行有关图的遍历操作时,时间代价是O(n*e)?还是O(n+e)?或者是O(max(n,e))?

必须要顺邻接表的顶点表挨个检查,并且沿着每条边向下遍历,所以时间代价为O(n+e)。


8-16 试证明在一个有n个顶点的完全图中,生成树的数目至少有2^(n-1)-1

这题有如下疑问
首先生成树的根固定么?如果不固定的话,就相当于将这n个顶点得到一个排序序列,就是生成树了。p(n)显然是答案,当然p(n)永远都大于2^(n-1)-1.这样的解答貌似太简单了。
然后我们假设必须从某个顶点开始,作为生成树的根,那么问题就变成剩下的n-1个顶点,做一个排序序列,那么显然就是比较p(n-1)与2^(n-1)-1。而这个显然必须在n>=5时才能成立,好像又不对。
迷惑中。


8-18 利用Dijkstra算法的思想,设计一个求最小生成树的算法。

计算连通网络的最小生成树的dijkstra算法可描述如下:将连通网络中所有的边以方便的次序逐步加入到初始为空的生成树的边集合T中。每次选择并加入一条边后,需要判断它是否会与先前加入T的边构成回路。如果构成了回路,则从这个回路中将权值最大的边退选。

具体的实现,答案的程序看不太懂。并且从这里开始暂时不把数据结构的答案放到网上了。因为迷糊的东西太多了。
7-10 假定用一个循环链表来实现一个有序表,并让指针head指向具有最小关键码的结点。指针current初始时等于head,每次搜索后指向当前检索的结点,但如果搜索不成功则current重置为head。试编写一个函数search(head,current,key)实现这种搜索。当搜索成功时函数返回被检索的结点地址,若搜索不成功则函数返回空指针0.请说明如何保持指针current以减少搜索时的平均搜索长度。

思路:定义搜索函数为类的公共函数。并且这个有序循环列表按key值从小到大排列。
 ListNode< Type >* Search(ListNode< Type >*head,ListNode< Type > *current,Type key){
if(key < current->data ) //key小,在前半段,递归调用.
return current=Search(current,head,key);
while(key > current->data && current!=head)
current=current->link;
if(current->data != key)
return NULL;
else return current;
}


7-11 考虑用双向链表来实现一个有序表,使得能在这个表中进行正向和反向的搜索。若指针p总是指向最后成功搜索到的结点,搜索可以从p指示的结点出发沿任一方向进行。试根据这种情况编写一个函数search(head,p,key),检索具有关键码key的结点,并相应的修改p。最后请给出搜索成功和搜索不成功时的平均搜索长度。

思路:类似上题
 ListNode< Type >* Search(ListNode< Type > *head,ListNode< Type > *p,Type key){
if(p->data > key)
while(p->data > key&&p !=NULL)p=p->llink;
if(p->data < key)
while(p->data < key&&p !=NULL)p=p->rlink;
if(p->data == key)
return p;
else{ p=head;reutrn NULL;}
}


7-14 试说明如何利用并查集来计算无向连通图中连通子图的个数。

1.以每个顶点为一个集合,初始化
2.边作为等价对,建立并查集
3.最后得到的集合个数就是连通子图的个数。


7-16 有n个结点的二叉搜索树具有多少种不同形态?

这个题和 问 在给定中序序列下的树,可以有多少种前序遍历。
答案是一样的1/(n+1)Cn/2n


7-18 在二叉搜索树上删除一个有两个子女的结点时,可以采用以下三种方法:
(1)用左子树Tl上具有最大关键码的结点X顶替,再递归地删除X。
(2)交替地用左子树Tl上具有最大关键码的结点和右子树Tr上具有最小关键码的结点顶替,再递归地删除适当的结点。
(3)用左子树Tl上具有最大关键码的结点或者用右子树Tr上具有最小关键码的结点顶替,再递归地删除适当的结点。可随机选择其中一个方案。


声明:均作为类函数加入,为减少篇幅,省略模板结构。并且假设所删除的结点一定是含有两个子女的。
(1)利用左子树上最大关键码结点代替。
remove(BstNode< Type > *ptr){
if(no child)
直接删除
BstNode< Type > *temp;
temp=Max(ptr->leftChild);
ptr->data=temp->data;
remove(temp);
}


(2)
设置一个参数来判断什么 顶替方案
7-1 设A={1,2,3},B={3,4,5},求下列结果:
(1) A+B (2)A*B (3)A-B
(4) A.Contains(1) (5)A.AddMember(1)
(6) A.DelMember(1) (7)A.Min()


(1)A={1,2,3,4,5}
(2)A={3}
(3)A={1,2}
(4)1
(5)A={1,2,3}
(6)A={2,3}
(7)1


7-2 试编写一个算法,打印一个有穷集合中的所有成员。要求使用集合抽像数据类型中的基本操作。如果集合中包含有子集合,各个子集合之间没有重复的元素,采用什么结构比较合适。

采用广义表的结构会比较好,就像广义表的遍历操作一样,采用递归。
template< class Type >class Set{
int Contains(const Type x);
int SubSet(Set< Type >& right);
int operator ==(Set< Type >&right);
int Elemtype();
Type GetData();
char GetName();
Set< Type >* GetSubSet();
Set< Type >* GetNext();
int IsEmpty();
};

ostream& operator<< (ostream& out,Set< Type >){
t.traverse(out,t);return out;
}

void traverse(ostream&out, Set< Type >s){
if(s.IsEmpty()==0){
if(s.Elemtype()==0)out<< s.GetName()<< '{';
else if(s.Elemtype()==1){
out<< s.GetData;
if(s.GetNext()!=NULL)out<< ',';
}
else{
traverse(s.GetSubSet());
if(s.GetNext()!=NULL)out<< ',';
}
traverse(s.GetNext());
}
else out<< '}';
}

除了最后一行外,全部照抄的参考答案。题目没意思,懒得做

7-3 当全集合可以映射成1到N之间的整数时,可以用位数组来表示它的任一子集合。当全集和是下列集合时,应当建立什么样的映射?
(1)整数0,1,...,99
(2)从n到m的所有整数,n<=m
(3) 整数n,n+2,n+4,...,n+2k
(4)字母'a','b','c',...,'z'
(5)两字母组成的字符串,其中每个字母取自'a','b','c',...,'z'.


假设不设置监哨点
(1) i={0,1,...,99}一一对应即可
(2) n->0,n+1->1,n+2->2,...,n+(m-n)->m-n 即可
(3) n->0,n+2->1,n+4->2,...,n+2k->k 即可
(4) 'a'->0,'b'->1,...,'z'->25
(5) 'aa'->0,'ab'->1,...,'zz'->675

7-7 给定一个用无序链表表示的集合,需要在其上执行operator+(),operator*(),operator-(),Contains(x),AddMember(x),DelMember(x),Min(),试写出它的类声明,并给出所有这些成员函数的实现。

思路:模仿教材中有序链表表示集合的类结构,写无序链表表示方法,无序链表在删除和加入时无序做过多操作,但是在求最大最小合并等操作里,要扫描全部链表。交并等集合运算时要注意是否有相同元素.
template < class Type >class SetList;
template< class Type >class SetNode{
//这部分参考教材
friend class SetList< Type >;
public:
SetNode(const Type& item)
:data(item),link(NULL);
private:
Type data;
SetNode< Type >* link;
};

template< class Type >class SetList{
//这部分参考教材
public:
SetList();
~SetList();
SetList< Type > &operator+(SetList< Type >& right);
SetList< Type > &operator*(SetList< Type >& right);
SetList< Type > &operator-(SetList< Type >& right);
int Contains(const Type &x);
int AddMember(const Type &x);
int DelMember(const Type &x);
Type & Min();
private:
SetNode< Type > *first,*last;
}

template< class Type >SetList< Type >& SetList< Type >
::operator+(SetList< Type >& right){
SetNode< Type > *pb=right.first->link; //目标链
SetNode< Type > *tmp=pa=first->link; //tmp暂存first
SetNode< Type > *pc=first; //新加入
int diff; //判断元素是否在A集合中出现
while(pb!=NULL){
diff=1; //初始置为 不在A集合中出现
for(pa=tmp;pa=pa->link;pa!=NULL)
if(pb->data==pa->data){
//如果相等,pa直接到链尾
pa=last->link;
diff=0;
}
if(diff==1)
pc=pc->link=pb; //加入pb
pb=pb->link;
}
pc->link=tmp;
//原来的A链加入新链的链尾
return * this;
}

template< class Type >SetList< Type >& SetList< Type >
::operator*(SetList< Type >& right){
SetNode< Type > *pb=right.first->link; //目标链
SetNode< Type > *tmp=pa=first->link; //tmp暂存first
SetNode< Type > *pc=first; //新加入
int diff; //判断元素是否在A集合中出现
while(pb!=NULL){
diff=1; //初始置为 不在A集合中出现
for(pa=tmp;pa=pa->link;pa!=NULL)
if(pb->data==pa->data){
//如果相等,pa直接到链尾
pa=last->link;
diff=0;
}
if(diff==0)
pc=pc->link=pb; //加入pb
pb=pb->link;
}
return * this;
}

template< class Type >SetList< Type >& SetList< Type >
::operator-(SetList< Type >& right){
SetNode< Type > *pb=right.first->link; //目标链
SetNode< Type > *pa=first; //this链
SetNode< Type > *tmp;
while(pb!=NULL){
for(pa=first;pa=pa->link;pa->link!=NULL)
if(pa->link->data==pb->data){
如果B中有元素与A相等,则删去A中的该元素
tmp=pa->link;
pa->link=pa->link-link;
delete tmp;
pa=last;
}
pb=pb->link;
}
return * this;
}

template< class Type >int SetList< Type >
::Contains(const Type &x){
SetNode< Type > *p=first->link;
while(p!=NULL)
if(p->data==x)
return 1;
else p=p->link;
return 0;
}

template< class Type >int SetList< Type >
::AddMember(const Type &x){
int i=Contains(x);
if(i==1)
return 0;
else{
last->link=new SetNode(x);
last=last->link;
return 1;
}
}

template< class Type >int SetList< Type >
::DelMember(const Type &x){
SetNode< Type > *p=first->link,*q=first;
while(p!=NULL){
if(p->data==x){
q->link=p->link;
if(p==last)last=q;
delete p;
return 1;
}
else{
p=p->link;
q=q->link;
}
}
return 0;
}

template< class Type >Type& SetList< Type >::Min(){
//求集合中的最小元素.
SetNode< Type > *p=first->link;
if(p==NULL){
//如果是空表返回错误;
cout<< "空表"<< endl;
return;
}
Type minnum=p->data;
p=p->link;
while(p!=NULL){
if(minnum > p->data)
minnum=p->data
p=p->link;
}
return minnum;
}
6-18 二叉树的双序遍历(Double-order traversal)是指:对于二叉树的每一个结点来说,先访问这个结点,再按双序遍历它的左子树,然后再一次访问这个结点,接下来双序遍历它的右子树。试写出执行这种双序遍历的算法。

声明:类结构按教材的二叉树类定义

双序遍历

template < calss Type >void BinaryTree< Type >::DbOrder(){
DbOrder(root);
}

template< class Type >void BinaryTree< Type >
::DbOrder(BinTreeNode< Type > *current){
if(current!=NULL){
visit();
DbOrder(current->leftChild);
visit();
DbOrder(current->rightChild);
}
}


6-20 已知一棵树的先根次序遍历的结果与其对应二叉树表示(长子-兄弟表示)的前序遍历结果相同,树的后根次序遍历结果与其对应二叉树表示的中序遍历结果相同。试问利用树的先根次序遍历结果和后根次序遍历结果能否唯一确定一棵树?

当然可以. 每一个二叉树可以唯一的转变成一棵树.

6-21 给定权值集合{15 03 14 02 06 09 16 17},构造相应的霍夫曼树,并计算它的带权外部路径长度。

WPL=229
身边没有稿纸,心中建树,+所有的分支结点的权值


6-22 假定用于通讯的电文仅8个字母c1,c2,c3,c4,c5,c6,c7,c8组成,各字母在电文中出现的频率分别为5,25,3,6,10,11,36,4. 试为这8个字母设计不等长Huffman编码,并给出该电文的总码数。

c1:0110 c2:10 c3:0000
c4:0111 c5:001 c6:010
c7:11 c8:0001

总码率为:257


6-23 给定一组权值:23,15,66,07,11,45,33,52,39,26,58,试构造一棵具有最小带权外部路径长度的扩充4叉树,要求该4叉树中所有内部结点的度都是4,所有外部结点度为0.这棵扩充4叉树的带权外部路径长度是多少?

本题关键是11个结点没法构成符合要求的4叉树,需要补充2个节点。补充两个权值为0的节点并不影响最后的外部路径长度。
6-10 一棵高度为h的满k叉树有如下性质;第h层上的结点都是叶结点,其余各层上每个结点都有k课非空子树,如果按层次自顶向下,同层自左向右,顺序从1开始对全部结点进行编号,试问:
(1)各层结点个数是多少?
(2)编号为i的结点的父结点(若存在)的编号是多少?
(3)编号为i的结点的第m个孩子结点(若存在)的编号是多少?
(4)编号为i的结点有右兄弟的条件是什么?其右兄弟结点的编号是多少?


(1)
第0层 1
第1层 k
第2层 k^2
...
第h层 k^h

(2)假设i为第m层元素,那么i层i是第几个 N=i - m层前所有元素个数
而i的父结点在上层的序号位置正好是N/k的上整
那么该父结点的序号是m-1层前所有元素+N/k的上整
上整外取,得到如下公式
上整『(1-k^(m-1))/(1-k)+(i-(1-k^m)/(1-k))/k』
化简得到取上整『(i-1)/k)』


(3)和2相反
(i-1)k+1+m

(4)i结点有右兄弟,条件就是i不是该层子树的最后一个元素。
即(i-1)%k!=0 有兄弟 右兄弟为i+1;

6-11 试用判定树的方法给出在中序线索化二叉树上:
(1) 如何搜索指定结点的在中序下的后继。
(2) 如何搜索指定结点的在前序下的后继。
(3) 如何搜索指定结点的在后序下的后继。


看树的线索化去……
第2小题结果好复杂!!@@

6-12 已知一棵完全二叉树存放于一个一维数组T[n]中,T[n]中存放的是各结点的值。试设计一个算法,从T[0]开始顺序读出各结点的值,建立该二叉树的二叉链表表示.

思路:利用完全二叉树的性质 T[i]应该插入的位置是 下整『(i-1)/2』 (i-1)/2整除则正好是该点的左子女,否则右子女.简化代码,不写类和模板了。

CreatTree(int T[],int n,int i,BinTreeNode *ptr){
if(i >=n)ptr=NULL;
else{
ptr=new BinTreeNode(T[i]);
ConstructTree(T,n,2*i+1,ptr->leftChild);
ConstructTree(T,n,2*i+2,ptr->rightChild);
}
}

程序从CreatTree(t,n,0,root)开始,事先先创建一棵空树.

6-13 试编写一个算法,把一个新结点l作为结点s的左子女插入到一棵线索化二叉树中,s原来的左子女变成l的左子女。
假定是中序线索化二叉树

template< class Type > void ThreadTree< Type >::
InsertRight(ThreadNode< Type > *s,ThreadNode< Type > *l){
l->leftChild=s->leftChild;
l->leftThread=s->leftThread;
l->rightChild=s;L->rightThread=1;
s->leftChild=l;s->leftThread=0;
if(l->leftThread==0){
current=l->leftChild;
ThreadNode< Type > *temp=Last();
temp->rightChild=l;
}
}

发现和右插入简直就是镜像啊

6-15 判断以下序列是否是堆?如果不是,将它调整为堆。
(1) {100,86,48,73,35,39,42,57,66,21}
(2) {12,70,33,65,24,56,48,92,86,33}
(3) {103,97,56,38,66,23,42,12,30,52,06,20}
(4) {05,56,20,23,40,38,29,61,35,76,28,100}


(1)

100
86 48
73 35 39 42
57 66 21

100
86 48
73 21 39 42
57 66 35

100
86 48
57 21 39 42
73 66 35

100
86 39
57 21 48 42
73 66 35

100
21 39
57 35 48 42
73 66 86

21
35 39
57 86 48 42
73 66 100
就做一题了 {21 35 39 57 86 48 42 73 66 100}

6-17 在森林的二叉树表示中,用llink存储指向结点第一个子女的指针,用rlink存储指向结点下一个兄弟的指针,用data存储节点的值。如果我们采用静态二叉链表作为森林的存储表示,同时按森林的先根次序依次安放森林的所有结点,则可以在它们的结点中用只有一个二进位的标志ltag代替llink,用rtag代替rlink。并设定若rtag!=0,则该结点有下一个兄弟。试给出这种表示的结构定义,并设计一个算法,将这种表示存储的森林转换成用llink-rlink表示的森林。

先在草稿纸上画出这样表示的森林和二叉树结构表,理解相互之间的转换方式。

先定义llink-rlink表示的结点定义
template< class Type >class LchRsibNode{
protected:
Type data;
int llink,rlink;
public:
LchRsibNode():llink(-1),rlink(-1){}
LchRsibNode(Type x):llink(-1),rlink(-1),data(x){}
}

双标记类节点定义
template< class Type >class DoublyTagNode{
protected:
Type data;
int ltag,rtag;
public:
DoublyTagNode():ltag(0),rtag(0){}
DoublyTagNode(Type x):ltag(0),rtag(0),data(x){}
}

静态链表类定义
template< class Type >class staticlinkList
:public LchRsibNode< Type >,pubic DoublyTagNode< Type >{
private:
LchRsibNode< Type > *V;
DoublyTagNode< Type > *U;
int MaxSize,CurrentSize;
public:
dstaticlinkList(int Maxsz ):MaxSize(Maxsz),CurrentSize(0){
V=new LchRsibNode< Type >[Maxsz];
U=new DoublyTagNode< Type >[Maxsz];
}

森林的双标记先根次序表示 向 左子女-右兄弟链表示先根次序表示的转换
void staticlinkList< Type >::DtagF-LchRsibF(){
Stack< int >st; int k;
for(int i=0;i< CurrentSize; i++){
switch(U[i].ltag){
case 0:switch(U[i].rtag){
case 0:V[i].llink=V[i].link=-1;
if(!st.IsEmpty())
{k=st.GetTop();
st.Pop();
V[k].rlink=i+1;
}break;
case 1:V[i].link=-1;
V[i].rlink=i+1;
break;
}
case 1: V[i].llink=i+1;
if(U[i].rtag==0)
V[i].rlink=-1;
else
st.Push(i);
break;
}
}
}

感觉用switch结构并不好,不过实在太饿了,懒得改写了。
6-4 用嵌套类写出用链表表示的二叉树的类声明。
把书p170~171页改写成嵌套形式
template< class Type >class BinaryTree{
private:
template< calss Type >class BinTreeNode{
public:
BinTreeNode< Type >* leftChild,*rightChild;
Type data;
}
Type RefValue;
BinTreeNode< Type >* root;
......
结点相关的一些递归操作
......
publick:
//一些构造函数
BinaryTree();
~BinaryTree();
BinTreeNode();
~BinTreeNode();
.......
二叉树的公共操作
.......
}


6-5 在结点个数为n(n>1)的各棵树中,高度最小的树的高度是多少?它有多少个叶结点?多少个分支结点?高度最大的树的高度是多少?它有多少个叶结点?多少个分支结点?

最小高度为1,有两层。n-1个页结点,1个分支结点。
最大高度为n-1,有n层,n-1 个分支结点,1个页结点.

6-7 如果一棵树有n1个度为1的结点,有n2个度为2的结点,...,nm个度为m的结点,试问有多少个度为0的结点?试推导之。

设度为0的结点为n0个。可以想象,每个度必定对应一个结点,或者反过来,除去根结点每个结点都消耗一个度。那么我们可以列出如下等式 n1*1+n2*2+n3*3+...+nm*m=n0+n1+n2+...+nm-1
n0=1+n2*1+n3*2+...+nm*(m-1)

6-8 试分别找出满足以下条件的所有二叉树:
(1) 二叉树的前序序列与中序序列相同;
(2) 二叉树的中序序列与后序序列相同;
(3) 二叉树的前序序列与后序序列相同;


(1)空树,或者所有结点均没有左子树的单支树;
(2)空树,或者所有结点均没有右子树的单枝树;
(3)空树,或者只有根结点的二叉树;

6-9 若用二叉链表作为二叉树的存储表示,试针对以下问题编写递归算法:
(1) 统计二叉树中叶结点的个数。
(2) 以二叉树为参数,交换每个结点的左子女和右子女。


声明:类定义参见教科书
(1)思路:如果有左子女或者右子女,返回左子女或者右子女的叶结点个数,如果没有(本身就是叶结点),返回1.方便起见,作为类成员函数加入。

int count(BinaryTree &t){
count(root);
}

int count(BinTreeNode *current){
if(current==NULL) return 0;
else if(current->leftChild==NULL && current->rightChild==NULL)return 1;
else return count(current->leftChild)+count(current->rightChild);
}


(2)思路同上,另一直想把二叉树作为直接参数,函数作为外部函数,但这样写起来要使用很多类函数,很麻烦,每次都要重新构造一棵树,非常麻烦,上题也是这个困惑。看到参考答案直接忽视题目的要求--二叉树作为参数,无语,直接仿效。
void exchange(BinaryTree & t){
exchange(root);
}

void exchange(BinTreeNode *current){
BinTreeNode *temp;
if(current==NULL)return;
//书里的条件有问题,NULL->link不一定=NULL
temp=current->leftChild;
current->leftChild=current->rightChild;
current->rightChild=temp;
exchange(current->leftChild);
exchange(current->rightChild);
}
6-1 写出用广义表表示法表示的树的类声明,并给出如下成员函数的实现:
(1) operator >>() 接收用广义表表示的树作为输入,建立广义表的存储表示;
(2) 复制构造函数 用另一棵树表示为广义表的树初始化一棵树;
(3) operator ==() 测试用广义表表示的两棵树是否相等;
(4) operator<< () 用广义表的形式输出一棵树;
(5) 析构函数 清除一棵用广义表表示的树。


class GenTree;
class GenTreeNode{
friend class GenTree;
private:
int utype; //标志结点
GenTreeNode* nextSibling;

union(
char RootData; //utype=0
char Childdata; //utype=1
GenTreeNode* firstChild; //utype=2
}
public:
GenTreeNode(int tp,char item):utype(tp),nextSibling(NULL)
{if(tp==0) RootData=item;else ChildData=item;}
//构造函数:构造广义表表示的树的数据结点.
GenTreeNode(GenTreeNode* son=NULL)
:utype(2),nextSibling(NULL),firstChild(son){}
//构造函数:广义表表示的树的子树结点
int nodetype(){return utype;} //返回结点数据类型
char GetData(){return data;} //返回结点的值
GenTreeNode* GetFchild(){return firstChild;}
//返回子树结点的地址
GenTreeNode* GetNsibling(){return nextSibling;}
//返回下一个兄弟结点的地址
void setInfo(char item){data=item;}
void setFchild(GenTreeNode* ptr)
{firstChild=ptr;}
void setNsibling(GenTreeNode* ptr)
{nexSibling=ptr;}
};

class GenTree{
private:
GenTreeNode *first; //根指针
char reValue; //建树时的输入停止标志
GenTreeNode *Copy(GenTreeNode * ptr);
//复制一个ptr指示的子树
void Traverse(GenListNode *ptr);
//对ptr为根的子树遍历并输出
void Remove(GenTreeNode *ptr);
//将以ptr为根的广义树结构释放
friend int Equal(GenTreeNode *s,GenTreeNode *t);
//比较以s和t为根的树是否相等.
void ConstructTree(istream&in,char& value)
//从输入流in接受用广义表表示的非空树,建立广义表的存储表示t
public:
GenTree();
GenTree(GenTree& t);
~GenTree();
friend int operator==(GenTree& t1,GenTree& t2);
//判断两棵树t1和t2是否相等
friend istream& operator >>(istream& in,GenTree& t);
//输入
friend istream& operator<< (istream& out,GenTree& t);
//输出
}

(1) oerator >>() 接收用广义表表示的树作为输入,建立广义表的存储表示

istream& operator >>(istream& in,GenTree& t){
//友元函数,从输入流对象in接受用广义表表示的树,建立广义表的存储表示t
t.ConstructTree(in,reValue);
return in;
}

void GenTree::ConstructTree(istream& in,char& value){
Stack< GenTreeNode* >st;
GenTreeNode * p,q,r;
Type ch;
cin >>value;
cin >>ch;first=q=new GenTreeNode(0,ch);
cin >>ch;if(ch=='(')st.Push(q);
cin >>ch;
while(ch!=value){
switch(ch){
case'(':p=new GenTreeNode< Type >(q);
r=st.GetTop();st.Pop();
r->nextSibling=p;
st.Push(p);
st.Push(q);
break;
case')':q->nextSibling=NULL;
st.Pop();
if(st.IsEmpty()==0)
q=st.GetTop();
else return 0;
break;
case',':break;
default:p=q;
if(isupper(ch))
q=new GenTreeNode(0,ch);
else q=new GenTreeNode(1,ch);
p->nextSibling=q;
}
cin>>ch;
}
}

先去吐血了……
5-5 已知f为单链表的表头指针,链表中存储的都是整型数据,试写出实现下列运算的递归算法:
(1)求链表中的最大整数;
(2)求链表的结点个数;
(3)求所有整数的平均值;


和整型数组A[]的算法一样;另外与参考答案同步,假设是不带表头结点的单链表。
(1)思路:把f->data的值和Max(f->link)比较;套用广义表的说法,就是比较表头和表尾即可,顺便递归定义表尾。
int Max(ListNode *f){
if(f->link == NULL) return f->data;
int tmp=Max(f->link);
if(f->data >= tmp)
return f->data;
else return tmp;
}

(2)求链表长度

int Length(ListNode *f){
if(f == NULL) return 0;
else return 1+Length(f->link);
}

(3)求所有整数的平均值

float Avg(ListNode *f){
if(f->link == NULL) return f->data;
else{int n=length(f->link); //时间代价够高的 呵呵
return (f->data+Avg(f->link)*n)/n;
}


5-7 利用广义表的head和tail操作写出函数表达式,把以下各题中的单元素banana从广义表中分离出来:
(1)L1(apple,pear,banana,orange)
(2)L2((apple,pear),(banana,orange))
(3)L3(((apple),(pear),(banana),(orange)))
(4)L4((((apple))),((pear)),(banana),orange)
(5)L5((((apple),pear),banana),orange)
(6)L6(apple,(pear,(banana),orange))


(1) head(tail(tail(L1)))
(2) head(head(tail(L2)))
(3) head(head(tail(tail(head(L3)))))
(4) head(head(tail(tail(L4))))
(5) head(tail(head(L5)))
(6) head(head(tail(head(tail(L6)))))


5-8 广义表具有可共享性,因此在遍历一个广义表时必须为每一个结点增加一个标志域mark,以记录该结点是否访问过。一旦某一个共享的子表结点被作了访问标志,以后就不再访问它。
(1)试定义该广义表的类结构;
(2)采用递归的算法对一个非递归的广义表进行遍历。
(3)试使用一个栈,实现一个非递归算法,对一个非递归广义表进行遍历。


(1)声明:按照参考答案的思路;广义表中假定只存放了一种元素类型char,统称为原子结点 atom。 这样,广义表中结点类型只有3种:表头结点、char原子结点和子表结点.大写的字母表示表,小写表示原子结点 等同于教材。
class GenList;
class GenListNode{
friend class GneList;
private:
int mark,utype;
GenListNode* link;
union{
char listname; //utype=0,表头结点,存放表名.
char atom; //utype=1,存放原子结点的数据.
GenLisNode* hlink; //utype=2,存放指向子表的指针.
}value;
public:
GenListNode(int tp,char info):mark(0),utype(tp),tlink(NULL)
//表头或者原子结点的构造函数.
{if(utype==0)value.listname=info;
else value.atom=info;
}
GenListNode(GenListNode* hp) //子表构造函数.
:mark(0),utype(2),value.hlink(hp){}
char Info(GenListNode* elem) //返回元素elem的值.
{return(utype==0)?elem->value.listname:elem->value.atom;}
};

class GenList{
private:
GenListNode* first; //表头.
void traverse(GenListNode* ls); //广义表遍历.
void remove(GenListNode* ls); //删除.
public:
GenList(char& value); //构造函数,value停止标记
~GenList();
void traverse();
}

(2)广义表遍历的递归算法

void GenList::traverse(){
if(first==NULL){
cout<< "error"< return;
}
traverse(first);
}

void GenList::traverse(GenListNode* ls){
if(ls!=NULL){
ls->mark=1;
if(ls->utype==0) //表头结点;
cout<< ls->value.listname<< '(';
else if(ls->utype==1){ //原子结点;
cout<< ls->value.atom;
if(ls->linke!=NULL)cout<< ',';
}
else if(ls->utype==2){ //子表结点;
if(ls->value.hlink->mark==0)
traverse(ls->value.hlink);
else cout<< ls->value.hlink->value.listname;
if(ls->tlink!=NULL)cout<< ',';
}
traverse(ls->link);
}
else cout<< ')';
}

(3)按照上题的遍历顺序,改写成利用栈的非递归方式。依然方便处理,pop()直接弹出并返回元素结点.
下面这段程序不同于参考答案,感觉参考答案写的有点乱,修改了。
void GenList::traverse(GenListNode *ls){
Stack< GenListNode* >st;
st.Push(ls);
while(!st.IsEmpty()){
ls=st.Pop(); //取出结点
if(ls==NULL){ //空结点,当前层结束
cout<< ')';
return;
}
else st.Push(ls->link); //压入当前层下一个结点
ls->mark=1;
switch(ls->utype){
case 0:cout<< ls->value.listname<< '(';
//表头结点
case 1:cout<< ls->value.atom;
if(ls->link!=NULL)cout<< ',';
//原子结点
case 2:if(ls->value.hlink->mark==0)
st.Push(ls->value.hlink);
else{
cout<< ls->value.hlink->value.listname;
if(ls->link!=NULL)cout<< ',';
}
//子表结点
}
}

多简洁啊,参考答案的栈用的让人觉着不和谐。
5-3 【背包问题】设有一个背包可以放入的物品的重量为s,现有n件物品,重量分别为w[1],w[2],...,w[n]。问能否从这n件物品中选择若干件放入此背包中,使得放入的重量之和正好为s。如果存在一种符合上述要求的选择,则称此背包问题有解;否则无解。

思路:按照题中所给的背包问题递归定义,可以理解为从w[n]开始检查,直到n和s达到0,检查完毕。

Boolean bag(int s,int n){
if(s< 0||s >0&&n< 1) return False;
if(s==0) return Ture;
if(bag(s-w[n],n-1)==True) {cout<< w[n]<< ',';return Ture}
return bag(s,n-1);
}


5-4 【八皇后问题】设在初始状态下在国际象棋棋盘上没有任何棋子(皇后)。然后顺序在第一行,第2行。...,第8行上布放棋子。在每一行中有8个位置可选择,但在任一时刻,棋盘的合法布局都必须满足3个限制条件,即任何两个棋子不得放在棋盘上的同一行、或者同一列、或者同一斜线上。试编写一个递归算法,求解并输出此问题的所有合法布局。

思路:抄参考答案;书里的方法已经很经典了。

col[i]:标志第i列是否放置了皇后,是1,下同.
md[k]:标志第k条主对角线是否安置了皇后。k=7+i-j-1
sd[k]:标志第k条次对角线是否放置了皇后,k=i+j
q[i]:记录第i行皇后在第几列。

void Queen(int i){
for(int j=0;j< 8;j++){
if(col[j]+md[8+i-j-1]+sd[i+j]==0){//没有被攻击
col[j]=md[8+i-j-1]=sd[i+j]=1; //修改攻击范围
q[i]=1; //安置皇后
if(i==8){ //如果已经是第八个皇后了
for(j=0;j< 8;j++) //这个j不影响外层的j么?
cout<< q[j]<< ','<< endl;
}
else Queen(i+1); //安置下一行
col[j]=md[8+i-j-1]=sd[i+j]=0; //撤销,尝试其他位置
q[i]=0;
}
}
}

执行的时候用函数Queen(1) 从第一行开始.
5-1 已知A[n]为整数数组,试写出实现下列运算的递归算法:
(1)求数组A中的最大整数。
(2)求n个整数的和。
(3)求n个整数的平均值。


解:
(1)思路:求A(n)中的最大整数,等价于比较A[n-1]与A(n-1)中的最大值,谁大就返回谁.A(n-1)看起来有点别扭,其实是指前n-1个数,从A[0]~A[n-2]。
于是我们可以这样设计函数
int max(int n){
if(n == 1) //递归结束条件
return A[0];
else{
int tmp=max(n-1); //返回A(n-1)的最大值
return A[n-1]>=tmp?A[n-1]:tmp;
}


(2)思路:和上题一样
int sum(int n){
if(n == 1)
return A[0];
else return A[n-1]+sum(n-1);
}


(3) 用递归求平均值有意义么?

5-2 已知Ackerman函数定义如下:

|n+1 当m=0时.
akm(m,n)= |akm(m-1,1) 当m!=0,n=0时.
|akm(m-1,akm(m,n-1)) 当m!=0,n!=0时

(1)根据定义,写出它的递归求解算法;
(2)利用栈,写出它的非递归求解算法.


(1)依样画葫芦,相当于直接把函数抄下来
int akm(int m,int n){
if(m != 0)
if(n != 0)
return akm(m-1,akm(m,n-1));
else return akm(m-1,1);
else reurn n+1;
}


(2)看了好久的答案才明白这类题的做法,我的理解如下:
a. 作为一个递归函数什么时候需要栈呢?
如果akm(m,n)能够等价于另一个简单的akm(x,y),那么这时候就不需要工作栈,直接变换就行。比如“斐波那契数列”;只有形如akm(..akm(..akm(..)...)这个时候需要栈,因为m,n的变换必须依靠下一步的结果,也就是说你除了把这一步先保存起来,别无他法,你永远不知道它下一步会变成什么。没有一个简单的变化,这时候就需要工作栈.
b. 哪些变量需要进栈?以及如何判断进栈和出栈
那些与其他参数的下一步结果相关的变量需要进栈。这里便是akm函数的第一个参数m。
在我们得到下一组n的值之前,m必须先被保存起来,我们没有办法先运算m。而n的值恰好是尾递归.

进栈与出栈的条件一般已经在函数或者题意中给出,比如这个函数可以直接按照函数结构来写。
int akm(int m,int n){
int vn=n,vm;
stack st; //就不写最大元素数了.
st.Push(m); //栈,保存一系列参数m
while(!st.IsEmpty()){
vm=st.Pop();
if(vm == 0)vn++; //函数的第一个分支条件
else if(vn == 0){ //第二分支条件
st.Push(vm-1);vn=1;
}
else{ //第三个分支条件
st.Push(vm-1);
st.Push(vm);
vn--;
}
}
return vn;
}

这道题做了我足足两天之久。第一次接触 递归和循环 结构的转换,非常努力的试图搞懂它,最后刹那间明了,欣喜若狂,忍不住糊诌几句。
花了很多时间才看懂参考答案的意思,它是通过找出栈的变换规律,然后具体问题具体分析,总结出来,再写成循环结构。不过这样的规律并不一定好找,我一开始之所以看不懂的原因也在于此,因为我总觉得它应该从普遍性入手。
后来我就一直思考,栈为什么要存在,必要性在哪里,是什么原因导致了函数的无规律变换,而需要一串的地址去存储它的自变量或者中间量。哪些元素又是必须依靠栈去保留呢。
当我凝视着窗外烈日底下在风中轻轻舞动的树叶,交织出婆娑的树影和跃动的光斑时,突然想通了。
于是很快写下了算法,完全依照函数的结构,只保留必须要保留的值,相比参考答案,更简洁明了,更具有普遍性.
4-11 若使用循环链表来表示队列,p是链表中的一个指针。试基于此结构给出队列的插入和删除算法,并给出p为何值时队列为空。

思路:我们可以考虑把p作为入口,每次加入的元素从p加入。而p->link就成为出口,每次出队,都读取p->link,并删去这个结点.

按照书p117的基本类声明改写
isEmpty():p==NULL;
template< class Type >void Queue< Type >::EnQueue(const Type& item){
//将新元素item插入到队列的入口.
QueueNode *out;
if(isEmpty()) //如果队空,则p作为第一个结点并且链向自身
p->link=p=new QueueNode(item,NULL);
else
{out=p->link; //保存出口指针
p->link=new QueueNode(item,NULL); //新元素到入口
p=p->link;p->link=out; //修改入口指针,并指向出口
}
}

template< class Type >Type Queue< Type >::DeQueue(){
//从出口取出元素,并返回该元素值.
assert(!IsEmpty());
QueueNode *out=p->link; //从出口取出元素
Type retvalue = out->data; //元素值
p->link=out->link; //修改出口指针
delete out;
return retvalue;
}


4-12 若将一个双端队列顺序表示在一维数组V[m]中,两个端点设为end1和end2,并组织成一个循环队列。试写出双端队列所用指针end1和end2的初始化条件以及队列空和队列满的条件,并编写基于此结构的相应的插入和删除操作。

恩,双端队列,看了参考答案才明白什么叫双端队列。丫就一无赖双头栈~
思路:end1和end2为队列的两头,但不同于普通的循环队列,这两头不分first和rear,既能进,又能出,双向通道,类似于腔肠动物……
为了和参考答案保持一致,定义入队时end1向右,end2向左,简单点理解就好像某些时候数组的中间段空的。其实我更喜欢反过来。
那么很显然和循环队列一样,队空队满都是end1==end2,为了以示区别,可以定义end2永远指向第二个端口实际向前的一个位置,多出一个空位。如此我们有
队满:end1+1==end2 考虑到循环效果,end1可能会从V[0]恰好退到v[m],而此时 end2指向v[0],队列也是满的。所以表打成 (end1+1)%m==end2
队空:end1==end2

初始化,一定是置空队列和数组,所以有end1=end2=0 指向v[0]

同以前的题,我么假设判断队空和队满的函数以及类结构都已完成。
template< class Type >void DblQueue< Type >::EnQueue(Type& item,const int end){
//元素进队,end为1或者2,表示进队的方向.
assert(!IsFull());
if (end == 1){
end1 = (end1+1)%m;
V[end1]=item;
}
else{ //end1和end2的操作有区别,原因在于end2实际指向空
V[end2]=item;
end2=(end2-1+m)%m;
//莫非模不能做负数?前面我好像有地方取负数的模了
}
}

template< class Type >Type DblQueue< Type >::DeQueue(const int end){
//元素出队,end为1或者2,表示出队的方向.
assert(!IsEmpty());
Type temp;
if(end==1){
temp=V[end1];
end1=(end1+m-1)%m;
}
else{
end2=(end2+1)%m;
temp=V[end2]; //从这里可以看出end2指向的位置
}
return temp;
}


4-13 设用链表表示一个双端队列,要求可在表的两端插入,但限制只能在表的一端删除。试编写基于此结构的队列的插入和删除算法,并给出队列空和队列满的条件。

思路:
先描述一个用链表表示的双端队列的数据结构,两端用指针first和last表示,其中first端允许删除。为使链表的操作简化一致,同样使first作为头结点,first->link指向第一个结点。初始化条件first=last;first->link=last->link=NULL;
队满?不是链表么?还考虑队满啊 哎 太麻烦 无视之

感觉参考答案有问题,首先它把这个链设计成了一个循环链,然后end1->link=end1,那么如果一直只是从空表的end2端插入,end2->link指向end1,但是这样的链却没法删除,因为end1->link依然=end1.没有元素,事实上已经从end2端插入了很多元素.并且是从NULL开始的,不知道这样的表有没有意义

enum Direct{firstin,lastin}
EnQueue(const &item,Direct a){
//插入函数,a作为插入的方向,可选firstin或者lastin
QueueNode *p,*newnode;
newnode =new QueueNode(item,NULL); //新加入结点
p= a==firsin?first:last; //p作为指针
newnode->link=p->link; //仿照书p75页,统一各种情况
if(p->link == NULL)last=newnode;
p->link=newnode;
}

DeQueue(){
assert(first!=last); //确定队不空
QueueNode *q=first->link;
Type value=q->data; //懒得写模板了……用Type代替
first->link=q->link;
delete q;
if(first->link == NULL)last=first; //如果删最后一个元素
return value;
}


队空条件 first==last
4-6 根据课文中给出的优先级 ,回答以下问题:
(1)在函数postfix中,如果表达式e含有n个运算符和分界符,那么栈中可能存入元素的最大个数是多少?
(2)如果表达式e含有n个运算符,且括号嵌套的最大深度为6层,那么栈中可能存入元素的最大个数是多少?


(1)我想参考答案应该是错的。
题中的元算符指操作符,分界符在课文中有优先级定义么?
按照优先级表,从+,—开始往右依次进栈时,都不会导致退栈,一直到(,然后可以继续循环,如此可以保证最多的操作符进栈,一次进栈3个操作符和“(”,然后循环,但是(需要“)”对应,设有x个“(”,那么4x+3+x=n。x=(n-3)/5 那么最多进栈的操作符是 4(n-3)/5+3+1 1是指栈底的#

(2)括号深度最大6个,那么如果n足够大的话,最多存入的元素是
4*6+3再进来就是)了,要退栈。加上# 一共是28个。此时的n应该 >=33
如果n& lt;33的话,答案还是4(n-3)/5+3+1=4(n+2)/5

4-7 设表达式的中缀表示为a*x-b/x^2,试利用栈将它改为后缀表示ax*bx2^/-。写出转换过程中栈的变化。

步序 扫描项 类型项 动 作 栈变化 输出
0 '#'进栈 #
1 a 操作数 #  a
2 * 操作符 isp* >icp#,进栈 #* a
3 x 操作数 #* ax
4 - 操作符 isp-< icp*,退栈 # ax*
isp- >icp#,进栈 #- ax*
5 b 操作数 #- ax*b
6 / 操作符 isp/ >icp-,进栈 #-/ ax*b
7 x 操作数 #-/ ax*bx
8 ^ 操作符 isp^ >icp/,进栈 #-/^ ax*bx
9 2 操作数 #-/^ ax*bx2
10 # 操作符 isp#< icp^,退栈, #-/ ax*bx2^
isp#< icp/,退栈, #- ax*bx2^/
isp#< icp-,退栈, # ax*bx2^/-
结束


4-9 假设以数组Q[m]存放循环队列中的元素,同时以rear和length分别指示环形队列中的队尾位置和队列中所含元素的个数。试给出该循环队列的队空条件和队满条件,并写出相应的插入(enqueue)和删除(dlqueue)元素的操作。

队空条件:length==0;
队满条件:length==maxsize;

template< class Type >void Queue< Type >::EnQueue(const Type& item){
//若队列不满,则将元素item插入该队列的队尾,否则出错处理.
assert(!isFull()); //假设isFull()函数已经能够正确判断
rear=(rear+1)%MaxSize; //对为指针加1
elements[rear]=item; //在队尾插入item
length++;
}

template< class Type >Type Queue< Type >::DeQueue(){
//若队列不空则函数返回该队列队头的元素的值,同时将该队头元素删除.
assert(!isEmpty()); //同样假设已经修正isEmpty()函数.
length--; //队列长度减1
return element[(rear-length)%MaxSize]; //返回原队头元素
}


4-10 假设以数组Q[m]存放循环队列中的元素,同时设置一个标志tag,以tag==0和tag==1来区别在队头指针(front)和队尾指针(rear)相等时,队列状态为“空”还是“满”。试编写出此结构相应的插入和删除函数。

加入标志tag,则循环队列结构中不需要空一个元素位置了。front依然指向第一个元素前的队头
改写isEmpty()和isFull()
队空:front==rear&&tag==0
队满:front==rear&&tag==1

template< class Type >void Queue< Type >::EnQueue(const Type& item){
//若队列不满,则将元素item插入该队列的队尾,否则出错处理.
assert(!isFull()); //假设isFull()函数已经能够正确判断
rear=(rear+1)%MaxSize; //对为指针加1
elements[rear]=item; //在队尾插入item
tag=1; //队列不空
}

template< class Type >Type Queue< Type >::DeQueue(){
//若队列不空则函数返回该队列队头的元素的值,同时将该队头元素删除.
assert(!isEmpty()); //同样假设已经修正isEmpty()函数.
front=(front+1)%MaxSize; //队头往前移1
tag=0; //队列不满
return element[front]; //返回原队头元素
}
4-1 在下题中,s是栈的名字,a、b是栈中的元素,操作PUSH返回栈s的栈顶元素地址,POP返回从栈中退出的元素。试说明下列运算的结果:
(1)POP(PUSH(s,a));
(2)PUSH(s,POP(s));
(3)PUSH(s,POP(PUSH(s,b)));


1)返回a;
2)s栈先退出元素,后又将退出的元素压回栈中,返回栈顶元素,栈顶元素无变化;
3)b压入了栈中,并返回栈顶元素地址。

4-2 改写顺序栈的进栈成员函数Push(x),要求当栈满时执行一个stackFull()操作进行栈满处理。其功能是:动态创建一个比原来的栈数组大二倍的新数组,代替原来的栈数组,原来栈数组中的元素占据新数组的前MaxSize位置。

思路:将p105页的Push()函数改成if结构的;调用stackFull函数。创建stackFull函数。
template < class Type >void Stack< Type >::Push(const Type& item){
//若栈满执行stackFull。再执行压栈操作.
if(IsFull())
stackFull();
elements[++top]=item;
}

template < class Type >void Stack< Type >::stackFull(){
maxSize*=3; //最大元素扩大2倍
Type *temp=new Type[maxSize];
for(int i=0;i <= top;i++) //复制数据
temp[i]=elements[i];
delete[]elements;
elements=temp;
}


4-3 铁路进行列车调度时,常把站台设计成栈式结构的站台,如右图所示。试问:
(1)设有编号1,2,3,4,5,6的六辆车,顺序开入栈式结构的站台,则可能的出栈顺序有多少种?
(2)若进站的六辆列车顺序如上述,那么是否能够得到435612,325641,154623,135426的出战顺序,如果不能,请说明为什么不能;如果能,说明如何得到。


1)第一题不会做 答案也看不懂
然后上水木请教了,不过一般大侠都很忙,给了三字锦囊 catalan google之
然后google到了一些东西,h(n)=c(2n-2,n-1)/n
一个c++学习者对于atalan的总结
这个网站上,其他一些学习编程的心得也不错
组合数学中的catalan数
我的理解:
可以对这6辆车的出栈次序计数作如下分析
我们把这6辆车分为如下5种分组:
前1辆车后5辆车,前2后4,前3后3(123 456),前4后2,前5后1(12345 6)
我们总是让前组的车完成出栈后 再让后组车进栈,并且我们人为的规定后组车的第一辆车进栈后不会立刻弹出,要不然会和后一种情形重复,得少算一种情况。最后654321这样的出栈顺序并未包含在上述分组中。
至此,我们将6辆车的出栈次序全部列出,并令其为函数f(k),k=1,2...6
那么f(6)=f(1)*(f(5)-1)+f(2)*(f(4)-1)+f(3)*(f(3)-1)+f(4)*(f(2)-1)+f(5)*f(1)
好像不对
还是很晕,得回去查一下组合数学的书,恩,得好好看。
答案应该是h(n+1)=c(2n,n)/(n+1)
后注:在二叉树计数一章有详细的讲解

2)不可能得到435612和154623这两种出栈序列,问题出在最后的12和23不可能实现。
325641:1进-2进-3进-3出-2出-4进-5进-5出-6进-6出-4出-1出
135426:1进-1出-2进-3进-3出-4进-5进-5出-4出-2出-6进-6出

4-4试证明:若借助栈可由输入序列1,2,3,...,n得到一个输出序列p1,p2,p3...pn(它是输入序列的某种排列),则在输出序列中不可能出现以下情况,即存在i< j< k,使得pj< pk< pi。

假设,存在这样的pj< pk< pi
首先i< j,即pi先于pj出栈,而我们根据题意知道输入序列从小到大进栈,pj< pi,那么pj先于pi是先进栈的。所以pi和pj之间的进出栈关系是pj进栈,pi进栈,pi出栈,pj出栈。---1
然后我们看j< k,同样pj先于pk出栈,而pj< pk,那么pj先于pk入栈,那么pj和pk之间的关系是:pj进栈,pj出栈,pk进栈,pk出栈。---2
再考察i< k,pi先于pk出栈,pk< pi,pk先于pi入栈,那么:pk入栈,pi入栈,pi出栈,pk出栈.--3
把序列3代入2,得到 pj进,pj出,pk进,pi进,pi出,pk出。其结果和序列1矛盾.
故不存在这样的出栈序列

4-5写出下列中缀表达式的后缀形式:
(1)A×B×C
(2)-A+B-C+D
(3)A×-B+C
(4)(A+B)×D+E/(F+A×D)+C
(5)A&&B||!(E >F)
(6)!(A&&!((B< C)||(C >D)))||(C< E)
3-10 如果用循环链表表示一元多项式,试编写一个函数Polynominal::Calc(x),计算多项式在x处的值。
思路:模仿书里用单链表的方式表示一元多项式,ListNode代表多项式中的一项,类型Type实例化为Term。Term用struct定义,包括coef(系数)和exp(指数),他们都是共有数据成员,所有能够使用Term对象的函数都可以存取它们。并且把term对象声明为polynominal的私有成员。

struct Term{ //链表结点data的数据类型
int coef; //系数
int exp; //指数
Term(int c,int e){coef=c;exp=e;}
};

class Polynominal{
//多项式按指数从小到大排列,不声明构造函数等其他函数了
public:
Polynominal(); //构造函数,不声明了
~Polynominal();
double Calc(double x);
private:
CircList poly; //循环链表结构,data部分数据类型为Term
};

//以下实现Cacl函数
double Polynominal::Calc(double x){
Term current; //当前项
double sum=0.0; //存放最后的结果
int len; //多项式的项数
poly.First(); //当前指针指向第一项
len=poly.Length(); //求项数
for(int i=1;i<=len;i++){
current=poly.getData();
sum+=current->coef * pow(x,current->exp)
//pow(x,y)函数,求x的y次幂函数
poly.Next(); //下一项
}
}


3-12 试设计一个实现下述要求的locate运算的函数。设有一个带有表头结点的双向链表L,每个结点有4个数据成员:指向前驱结点的指针prior、指向后继结点的指针next、存放数据的成员data和访问频度freq。所有结点的freq初始时都为0。每当在链表上进行一次Locate(L,x)操作时,令元素值为x的结点的访问频度freq加1,并将该结点前移,链接到与它的访问频度相等的结点后面,时的链表中所有结点保持按访问频度递减的顺序排列,以使访问频繁的结点靠近表头。

思路:Locate函数参数为双向链表L和元素值x,通过双向链表的公共操作实现该函数的功能。
声明:为双向链表结点类加入 freq域,同时在双向链表类中加入两个公共操作
int getfreq();取得当前结点的freq值,返回
addfreq(); 当前结点的freq值增加1
同时原Insert函数,加入新结点时默认将freq置为0

void Locate(DblList< Type > L,Type& x){
if(!Find(x))
{cout << "Not find!\n";return}
int i; Type tmp;
i=getfreq(); //取得原freq值
tmp=getData(); //该结点的值
Remove(); //删去该结点
while(Prior())
/*寻找应该插入的位置,如果到了first,那么prior()返回0,current指向first*/
if(getfreq() > i)
return;
Insert(tmp); //插入
for(int k=0;k<=i;k++)
addfreq(); //设定freq,比原先大1
}
3-4 设计一个算法,通过一趟遍历在单链表中确定值最大的结点。

声明:在单链表类的基础上加入一个操作(公共函数)
思路:设置一个变量max暂存当前找到的最大值的结点,然后从单链表的第一个结点开始比较,如果找到比max值大的点则,替换max值。

template< class Type > ListNode< Type > *List< Type >::Max(){
//在当前的单链表中找出值最大的结点,并返回该结点的地址.
ListNode *p=first->link;
ListNOde *max=p;
while(p != NULL){
if(p->data > max->data)
max=p;
p=p->link; //遍历
}
return max;
}


3-5 设计一个算法,在非递减有序的单链表中删除值相同的多余结点。

声明:在单链表类的基础上加入一个操作(公共函数)。
思路:同上题,依然是一次遍历,因为单链表本身已经按照非递减顺序排好,只需要检查p->link是否和p的值一样,如果一样的话,就删除。

template< class Type > void List< Type >::Zip(){
//将非递减有序链表压缩,删去拥有相同值的多余结点。
ListNode *p=first->link,*q=NULL;
while(p != NuLL&&p->link != NULL){
if(p->data==p->link->data){ //相邻两点是否相等
q=p->link;p->link=q->link; //重新拉链
delete q;
}
eles p=p->link; //指针p进到下一位
}
}


3-6 设有一个表头指针为h的单链表。试设计一个算法,通过遍历一趟链表,将链表中所有结点的链接方向逆转。要求逆转结果链表的表头指针h指向原链表的最后一个结点。

声明:和参考答案的思路有区别。
思路:与3-3类似,p循链检测链表,并将当前指针p所指向的结点插到第一个结点位置,再重置p=pnext。pnext为预先保留的指针,指向原链中p的下一个结点。

void Inverse(ListNode *h){
ListNode *p=h->link,*pnext;
while(p!=NULL){
pnext=p->link; //保留p的下一个指针
p->link=h->link;h->link=p; //插到第一个节点位置
p=pnext; //p再往前走一位
}
}


3-7 从左到右及从右到左遍历一个单链表是可能的,其方法是在从左向右遍历的过程中将连接方向逆转,如p102页图所示。在图中的指针p指向当前正在访问的结点,指针pr指向指针p所指结点的左侧的结点。此时,指针p所指结点左侧的所有结点的链接方向都已逆转。
(1)编写一个算法,从任一给定的位置(pr,p)开始,将指针p右移k个结点。如果p移出链表,则将p置为0,并让pr停留在链表最右边的结点上。
(2)编写一个算法,从任一给定的位置(pr,p)开始,将指针p左移k个结点。如果pr移出链表,则将pr置为0,并让p停留在链表最左边的结点上。
(ps:题目应该把p和pr弄反了)

(1)思路:用tmp暂存p,然后p右移一位,tmp->link指向pr,逆转连,然后pr=tmp。检查p有没有溢出。
void moveright(ListNode *pr,ListNode *p,int k){
ListNode *tmp;
for(int i=1;i <= k;i++){
if(p==NULL||p==0){
p=0;
return;
}
tmp=p;p=p->link; //向右逆转
tmp->link=pr;pr=tmp;
}
}


(2)和上一样 p和pr对换
void moveleft(ListNode *pr,ListNode *p,int k){
ListNode *tmp;
for(int i=1;i <= k;i++){
if(pr==NULL||pr==0){
pr=0;
return;
}
tmp=pr;pr=pr->link; //向左逆转
tmp->link=p;p=tmp;
}
}


3-8 试写出适合循环链表的游标类定义,并编写它的3个成员函数First()、Next()和NextNotNull()的实现程序。这些函数的功能应该和单链表的相应操作相同。

思路:模仿单链表的Iterator类

ListNode前视定义 CLIterator类为友元
CircList前视定义 CLIterator类为友元
……
enum Boolean{False,True};
temple< class Type > class CLIterator{
public:
CLIterator(const CircList< Type > &l):list(l),current(l.first){}
Boolean NextNotNull();
Type *First();
Type *Next();
private:
const CircList< Type > &list;
ListNode< Type > *current;
}

template< class Type > Boolean CLIterator< Type >::NextNotNull(){
//检查链表中下一个元素是否非空.
if(current!=NULL&¤t->link!=NULL)
return Ture;
else return False;
}

template< class Type > Type * CLIerator< Type >::First(){
//返回指向链表中第一个结点的指针.
if(list.first->link!=NULL){
current=list.first;
return & current->data;
}
else{current=NULL;return NULL;}
}

template< class Type > Type * CLIerator< Type >::Next(){
//返回当前指针的下一个结点的指针.
if(current!=NULL){
current=current->link;
return & current->data;
}
else{ current=NULL;return NULL;}
}
3-3 设ha和hb分别是两个带表头结点的非递减有序单链表的表头指针,试设计一个算法,将这两个有序链表合并成一个非递增有序的单链表。要求结果链表仍使用原来两个链表的存储空间,不另外占用其它的存储空间。表中允许有重复的数据。

思路:以ha为表头,比较a和b链的元素,依次将当前的较小值插入到第一个结点位置,正好可以逆序合并。

ListNode * Merge(ListNode *ha,ListNode *hb){     
ListNode *pa, *pb; //a和b的pointer
ListNode *tmp; //暂存待链入的节点
LIstNode *left; //剩余部分
pa=ha->link; //初始化
pb=hb->link;
ha->link=NULL;
while(pa!=NULL && pb!=NULL){
if(pa->data <= pb->data)
{tmp=pa; pa=pa->link;}
else {tmp=pb;pb=pb->link;}
tmp->link=ha->link; //链入ha
ha->link=tmp;
}
left=(pa != NULL)?pa:pb;
while(left != NULL){ //处理剩下的链
tmp=left;left=left->link;
tmp->link=ha->link;
ha->link=tmp;
}
}
2-14 字符串的替换操作replace(String &s, String &t, String &v) 是指:若t是s的子串,则用串v替换t在串s中的所有出现;若t不是s的子串,则串s不变。试利用字符串的基本运算实现这个替换操作。
基本思路:s串中寻找t的匹配,找到後,在t处将t去掉,正好把s串一分为二,上半段接上目标串v,下半段继续寻找t的匹配,然后重复以上操作,并把前半段加入上一次操作的上一段。最后将上下两段合并,即完成所需要的replace操作。
所有操作均利用书上字符串类所给的公共函数实现

void Replace(String &s, String &t, String &v){
String tmp; //建立临时子串,存放上半段
while (int pos=s.Find(t) != -1){
tmp += s(0,pos) += v; //链入上半段和v
s=s(pos+t.length(),s.length()-pos-t.length());//产生s下半段
}
if (tmp.length()){ //如果没有替换声明一下
cout << "未找到可替换的子串" << endl;return;
}
else { s = tmp += s;return;} //整合
}

与参考答案相比,几乎完全不一样,这里的replace操作是一个独立的操作,s也作为一个变量。
另外题目要求利用字符串的基本运算操作,我的理解是利用那些公共函数,而不该去用存储结构部分。在一般情况下,这个replace函数不会放在类里。或者会成为一个新的类的函数。

2-16 设串s为“aaab”,串t为“abcabaa” ,串r 为"abcaabbabcabaacbacba",试分别计算他们的失效函数f(j)的值。
解:
"aaab" f(0)=-1 f(1)=0 f(2)=1 f(3)=-1
"abcabaa" f(0)=-1 f(1)=-1 f(2)=-1 f(3)=0
f(4)=1 f(5)=0 f(6)=0
"abcaabbabcabaacbacba"
f(0)=-1 f(1)=-1 f(2)=-1 f(3)=0 f(4)=0 f(5)=1
f(6)=-1 f(7)=-1 f(8)=1 f(9)=2 f(10)=3 f(11)=-1
f(12)=0 f(13)=0 f(14)=-1 f(15)=-1 f(16)=0 f(17)=-1
f(18)=-1 f(19)=0

ps:题目里有个空格,不过我想空格也只不过相当于个符号,去之。练习。

2-17 没太理解题目的意思,是再抄一遍p66 p67 呢 还是要用纯粹的数学上的方法证明?
感觉这离开模式匹配的环境就没有意义了。
2-9 设有一个n×n的对称矩阵A,如教材p69页a图所示。为了节约存储,可以只存对角线及对角线以上的元素,或者只存对角线或对角线以下的元素。前者称为上三角矩阵,后者称为下三角矩阵。我们把它们按行存放于一个一维数组B中,如图(b)和图(c)所示。并称之为对角矩阵A的压缩存储方式。试问:
(1)存放对称矩阵A上三角部分或下三角部分的一维数组B有多少元素?
(2)若在一维数组B中从0号位置开始存放,则如图a所示的对称矩阵中的任一元素a(ij)在只存上三角部分的情形下(图b)应存于一维数组的什么下标位置?给出计算公式。
(3)若在一维数组B中从0号位置开始存放,则a(ij)只存下三角时,对应在c数组中的下标计算公式?


(1) n是矩阵A对角线元素的个数 那么 B的元素个数就是 (n×n-n)/2+n=n(n+1)/2

(2) 矩阵元素a(ij)的编号是从1开始的,只存上三角的话,j肯定不小于i。
a(ij)前面有i-1行,第一行有n个元素,后依次是n-1,n-2,...,n-(i-2).然后第i行,a(ij)前有j-i个元素。所以
b数组的下标k=(n+n-(i-2))*(i-1)/2+j-i+0=(2n-i)*(i-1)/2+j-1
如果a(ij)不处于上三角位置时,只需将i与j对换即可。

(3) 思考方法同2
i>=j时 k=(1+(i-1))*(i-1)/2+j=i(i-1)/2+j
i < j时 k=j(j-1)/2+i



2-10 设A和B均为n*n的下三角矩阵,二维数组C,有n行n+1列。试设计一个方案,将A的下三角区域中的元素存放于C的下三角区域中,B先转置后存放于C的上三角区域中。给出计算A的矩阵元素a(ij)和B的矩阵元素b(ij)在C中的存放位置下标的公式。

对于A矩阵和B矩阵,只考虑i >= j的情形。
设在C中存放的位置为c[p][q] 因为是二维数组,所以均从0开始。同时为方便起见,矩阵A和B我们也假设是从0号还是计数。

那么对于a(ij)来说
p=i q=j;

对于b(ij)来说
首先是转置,那么i与j对换;b(ji)=b(ij)
然后这个上三角矩阵存入C中时,要右移一个位置,以保证不覆盖C的下三角区域。所以
p=j q=i+1;

2-11 三对角矩阵的定义:在该矩阵中除了主对角线及在主对角线上下最临近的两条对角线上的元素外,所有其他元素均为0.现在要将这样的三对角矩阵A中的非零元素按行存入一维数组B中,且a(11)存放在B[0]。给出a(ij)(1<=i<=n,i-1<=j<=i+1)在一维数组B中的存放位置的计算公式。

问题可以转化为a(ij)前有几个元素。
1) i=1时 j-i;
2) 1 < i < n时 (i-1)*3-1+j+1-i
3) i=n时 (i-1)*3-1+j+1-i
最后可以统一这三种情况
j+2i-3


2-12 设带状矩阵是n×n阶的方阵,其中所有的非零元素都在有主对角线及主对角线上下各b条对角线构成的带装区域内,其他元素都为零。试问:
(1)该带状矩阵中有多少个非零元素?
(2)设a11存放在B[0]中,试给出所有非零元素a(ij)在一维数组B中的存放位置。



1)只需求出下三角区域内有多少个零元素即可。(1+(n-1-b))*(n-1-b)/2=(n-b)(n-b-1)/2
这样的区域有2个 (n-b)(n-b-1)
非零元素个数为 n*n-(n-b)(n-b-1)=(2b+1)n-b(b-1)

2)同上题 必定有 1 <=i<=n i-b<=j<=i+b
感觉应该存在可以用通式把各种情况下的结果都写出来,但是找不到这样的通式,继续思考中。
答案给的好复杂。
2-5 顺序表的插入和删除要求仍然保持各个元素原来的次序。设在等概率情形下,对有127个元素的顺序表进行插入,平均需要移动多少个元素?删除一个元素,又平均需要移动多少个元素?

插入:一个有128个位置可以插入,平均移动元素个数为
n=(127+126+125+...+2+1+0)/128=(127+0)*128/2/128=63.5个
删除:一共127个元素可以删除,平均移动元素个数为
n=(126+125+...+2+1+0)/127=63个

2-6 若矩阵A(m×n)中的某一元素A[i][j]是第i行中的最小值,同时又是第j列中的最大值,则称此元素为该矩阵的一个鞍点。假设以二维数组存放矩阵,试编写一个函数,确定鞍点在数组中的位置(若鞍点存在时),并分析该函数的时间复杂度。

如果写成 z=A[x][y] 那么就可以在空间坐标中,画出这些点,并用光滑的曲面连接这些点,就可以得到一个曲面。比较容易想像鞍点的样子。同时这个鞍点只能有一个,这和连续曲面有所不同。
感觉没有什么特别的办法,只能一行一行的扫描,然扫描这些行最小值的列号。用s[i]来存储。

int saddle(int A[][],const int m,const int n){
int *s=new int[m]; //s[i]用来存储行最小值的列号
int *c=new int[n]; //c[j]用来存储列最大值的行号
int i, j,k;
for (i=0;i < m;i++;){
s[i]=0; //用来存j
for (j=1;j < n;j++) // 扫描各行,将最小值的j存入s[i]
if (A[i][j] < A[i][s[i]]) s[i]=j;
}
for (j=0;j < n;j++) c[j]=-1; //-1标记没有访问过
for (i=0;i < m;i++){ //循s[i]
tmp=0;
if (c[s[i]] == -1){
for (k=0;k < m;k++) //扫描s[i]列,找出最大的行号
if (A[k][s[i]] > A[tmp][s[i]]) tmp=k;
c[s[i]]=tmp;
}
if (c[s[i]]==i){ //扫描了s[i]列如果A[i][s[i]]是鞍点
cout << "The saddle point is :(" << i << "," << s[i] << ")" << endl;
return 1; //找到鞍点
}
}
return 0; //未找到鞍点
}

当m远小于n时,这段程序的执行效率略高于答案。
算法的基础是考虑,首先找出各行的最小列号。然后,我们要比较的列,只有这几个出现的列号,而不需寻找所有的列(答案就搜索了所有列)。同时会出现不同行的最小列号有可能相等,用c[j]数组去标记该列有没有被查找过,如果查过就存入最大行号。答案似乎没有认识到鞍点只有一个

该算法的总时间复杂度还是O(m×n)

2-7 设有一个二维数组A[m][n],假设A[0][0]存放在位置644(十进制,后同),A[2][2]存放位置在676,每个元素占一个空间,问A[3][3]存放在什么位置?

如果是行优先的话,那么数组的存储方式一定是
A[0][0] A[0][1] A[0][2]...A[1][0]A[1][1]A[1][2]...A[2][0]A[2][1]...
所以loc(2,2)-loc(0,0)=2*m+2 m=15
A[3][3]=3*15+3+644=692
列优先与行优先的情况时一样的。

2-8 计算多项式Pn(x)=a0x^n+a1x^(n-1)+a2x^(n-2)+...+a(n-1)x+an的值。可以用递归的方法求解Pn(x)=x*P(n-1)(x)+an.试写出这个递归函数。

系数存储在double coef[n+1]中,不另作声明

double poly(int n,double x){
if (n == 0) return coef[n];
else return x*poly(n-1,x)+coef[n];
}
2-1 设n个人围坐在一个圆桌的周围,现在从第s个人开始报数,数到第m个人,就让他出局;然后从出局的下一个人重新开始报数,数到第m个人,再让他出局,……,如此反复直到所有的人全部出局为止。下面要解决的Josephus问题是:对于任意给定的n,s和m,求出这n个人的出局序列。请以n=9,s=1,m=5为例,人工模拟Josephus的求解过程以求得问题的解。

解:1,2,3,4,5,6,7,8,9
第一轮5 1.2.3.4.'6'.7.8.9
第二轮1 '2'.3.4.6.7.8.9
第三轮7 2.3.4.6.'8'.9
第四轮4 2.3.'6'.8.9
第五轮3 2.'6'.8.9
第六轮6 2.'8'.9
第七轮9 '2'.8
第八轮2 '8'
第九轮8

2-2 试编写一个求解Josephus问题的函数。用整数序列1,2,3……,n表示顺序围坐在圆桌周围的人,并采用数组表示作为求解过程中使用的数据结构。然后使用n=9,s=1,m=5,以及n=9,s=1,m=0,或者n=9,s=1,m=10作为输入数据,检查你的程序的正确性和健壮性。最后分析所完成算法的时间复杂度。
其实参考答案已经很好了,只是想给出一种不同的方法练习一下。
利用table[1]=2,table[2]=3,table[3]=4,...,将table表串起来,每次报数淘汰后,比如3号淘汰,那么修改table[2]=4,继续报数。
同时引入table[0]=3,表示第一个淘汰的人,然后再将淘汰的人存入table[3],则最后在输出出局顺序时可以比较方便。当然也可以让table[3]=1,表示第一轮被淘汰的,可以让数组直观的表示该人是第几局出局的。视程序需要而定。这两种方法在输出结果和程序的直观性方面要比参考答案的好一点,而且不需要用%运算.
接下来的程序里使用table[0]的方法。

void jparray(int n,int s,int m){
if(m==0||s>n||s==0||n==0){ //参数检查
cerr << "参数无效!要求m,n,s为自然数,且开始人序号s要小于等于n!" << endl;
return;
}
int i,k,j=0;
int table[n+1];
for (i=0;i < n;i++)table[i]=i+1; //把人链起来
table[n]=1; //让尾回到第一个人
int pre=n; //指向前一个人
for (i=s;){ //开始报数的人
for(k=1;k < m;k++){
pre=i;
i=table[i]; //向后走m-1步,报数从1到m.i为报数到m的人。
}
j=table[j]=i; //j为从table[0]开始的出局顺序表。
i=table[pre]=table[i]; //删掉原i,并重建下一轮开始报数人i。
if (i == pre) { //剩下最后一个人了。
table[j]=i; //链上最后一个人
table[i]=0; //结束标志。
return;
}
//以下输出出局顺序。
cout << "圆桌" << n << "个人,从第" << s << "个人开始报数" << m << "的出局顺序是:/n" << endl;
for (i=0;table[i]!=0;i=table[i])cout << table[i] << " " << endl;
}


算法的时间复杂度,n×m+n 为O(nm),这个方法比较适合在n大,m小的时候,此时比参考答案的方法要小很多。