【王道数据结构】《王道数据结构》课后代码题汇总
《王道数据结构》算法设计题
整理出《王道数据结构》一书中所有(不确定)的课后代码题
二、线性表
(2.1) 顺序表
2.1.1 顺序表查找最小值并返回被删元素的值
/*
搜索整个顺序表,查找最小值元素并记住其位置,搜索结束后用最后一个元素填补空出的原最小值元素的位置。
*/
bool Del_Min(sqList &L,int &value)
{
if(L.length ==0)
return false;
value = L.data[0];
int pos = 0;
for(int i=1;i<L.length;i++)
{
if(L.data[i]<value)
{
value = L.data[i];
pos = i; //循环找最小值的元素
}
//空出的位置由最后一个填补
L.data[pos] = L.data[L.length -1];
L.length--;
returm true;
}
}
2.1.2 顺序表L所有元素逆置,空间复杂度为O(1)
void Reverse(Sqlist &L)
{
int temp;
for(i=0;i<L.length/2;i++)
{
temp = L.data[i]; //交换变量
L.data[i] = L.data[L.length - i - 1];
L.data[L.length - i -1] = temp;
}
}
2.1.3 顺序表删除所有值为x的数据元素,时间复杂度O(n),空间复杂度O(1)
/*
用 k 记录顺序表L中不等于x的元素个数(即需要保存的元素个数),边扫描L边统计k,并将不等于x的元素向前移动 k 个位置,最后修改L的长度。
*/
void del_x1(Sqlist &L,int x)
{
int k=0; //记录值不等于x的元素个数
for(i=0;i<L.length;i++)
if(L.data[i]!=x)
{
L.data[k] = L.data[i];
k++; //不等于x的元素增1
}
L.length = k; //顺序表L的长度等于k
}
2.1.4 有序顺序表中删除给定值 s 与 t 之间的所有元素
/*
本题与上题的区别,因为是有序表,所有删除的元素必须是相连的整体。从前向后扫描顺序表L,用k记录下元素值在s到t之间的元素的个数。若其值不在s到t之间,则前移k个位置;否则执行k++.
*/
bool Del_s_t(SqList &L,int s,int t)
{
int i,k=0;
if(L.length==0||s>=t)
return false;
for(i=0;i<L.length;i++)
{
if(L.data[i]>=s && L.data[i]<=t)
k++;
else
L.data[i-k] = L.data[i];//当前元素前移k个位置
}
L.length -= k; //长度减小
return true;
}
2.1.5 有序顺序表中删除所有值重复的元素,使所有元素的值均不同
/*
有序顺序表,值相同的元素在连续的位置上,用类似直接插入排序的思想,初始时将第一个元素视为非重复的有序表。之后依次判断后面的元素是否与前面非重复有序表的最后一个元素相同,若相同则继续向后判断,若不同则插入到前面的非重复有序表的最后,直到判断到表尾为止。
*/
bool Delete_Same(SeqList &L)
{
if(L.length == 0)
return false;
int i,j;
for(i=0;j=1; j<L.length;j++)
if(L.data[i]!=L.data[j])
L.data[++i] = L.data[j];
L.length = i+1;
return true;
}
2.1.6 两个有序顺序表合并为一个新的有序顺序表,由函数返回结果顺序表
/*
按顺序不断取下两个顺序表表头较小的结点存入新的顺序表中。然后,看哪个表还有剩余,将剩余的部分加到新的顺序表后面。
*/
bool Merge(SeqList A,SeqList B,Seqlist &C)
{
if(A.length + B.length > C.maxSize)
return false;
int i=0,j=0,k=0;
while(i<A.length && j<B.length) //循环,两两比较,小者存入结果表
{
if(A.data[i] <= B.data[j])
C.data[k++] = A.data[i++];
else
C.data[k++] = B.data[j++];
}
while(i<A.length)
C.data[k++] = A.data[i++];
while(j<B.length)
C.data[k++] = B.data[j++];
C.length = k;
return true;
}
2.1.7 一维数组 A[m+n] 中将两个顺序表m和顺序表n 的位置互换
/*
首先将A[m+n]中的全部元素原地逆置,再对前n个元素和后m个元素分别使用逆置算法,即可得到题意。
*/
void Reverse(int A[],int left,int right,ing arraysize)
{
if(left>=right || right>=arraySize)
return;
int mid = (left+right)/2;
for(int i=0;i<=mid - left;i++)
{
int temp = A[left + i];
A[left + i] = A[right -i];
A[right-i] = temp;
}
}
void Exchange(int A[],int m,int n,int arraySize)
{
Reverse(A,0,m+n-1,arraySize);
Reverse(A,0,n-1,arraySize);
Reverse(A,n,m+n-1,arraySize);
}
2.1.8 递增有序顺序表,查找数值为 x 的元素。若找到则与其后继元素交换位置,找不到则将其插入表中。
/*
题目要求“用最少的时间在表中查找数值为x的元素”,这里应使用折半查找法。
*/
void SearchExchangeInsert(int A[],int x)
{
int low=0,high = n-1,mid;
while(low <= high)
{
mid = (low+high)/2; //找中间位置
if(A[mid] == x) break;//找到x,退出while循环
else if(A[mid]<x) low = mid+1;//到中点mid的右半部去查
else high = mid -1; //到中点mid的左半部分去查
}
if(A[mid] == x && mid != n-1) //若最后一个元素与x相等,则不存在与其后继交换的操作
{
t=A[mid];
A[mid] = A[mid+1];
A[mid+1] = t;
}
if(low>high) //查找失败,插入数据元素x
{
for(i = n-1;i>high;i--)
A[i+1] = A[i]; //后移元素
A[i+1] = x; //插入x
}
}
(2.2) 顺序表真题
2.2.1 一维数组R的序列循环左移p个位置,空间和时间上都尽量高效。
/*
将问题视为数组ab转换成数组ba.(a代表数组的前p个元素,b代表数组中余下的n-p个元素),先将a逆置得到 (a的逆置+b),再将b逆置得到(a的逆置+b的逆置),最后将整个(a的逆置+b的逆置)逆置得到ba。
*/
void Revserse(int R[],int from,int to)
{
int i,temp;
for(i=0;i<(to-from+1)/2;i++)
{
temp = R[from+i];
R[from+i] = R[to-i];
R[to-i] = temp;
}
}
void Converse(int R[],int n,int p)
{
Revserse(R,0,p-1); //时间复杂度为O(p/2)
Revserse(R,p,n-1); //时间复杂度为O((n-p)/2)
Revserse(R,0,n-1); //时间复杂度为O(n/2)
}
//时间复杂度O(n),空间复杂度O(1)
2.2.2 找出数组中未出现的最小正整数,时间上尽可能高效。
/*
要求时间上尽可能高效,因此采用空间换时间的办法。分配一个用于标记的数组B[n],用来记录A中是否出现了1~n的正整数。B[0]对应正整数1,B[n-1]对应正整数n,初始化B中全部为0。由于A中含有n个整数,因此可能返回的值是1~n+1,当A中n个数恰好为1~n时返回n+1。当数组A中出现了小于等于0或大于n的值可以不采取任何操作。
算法流程:
从A[0]开始遍历,若0<A[i]<=n,则令B[A[i]-1] = 1;否则不做操作。对A遍历结束后,开始遍历数组B,若能查找第一个满足B[i]全部不为0,返回i+1, (跳出循环时i=n,i+1等于n+1),此时说明A中未出现的最小正整数是n+1。
*/
int findMissMin(int A[],int n)
{
int i,*B; //标记数组
B = (int *)malloc(sizeof(int) *n); //分配空间
memset(B,0,sizeof(int)*n); //赋初值为0
for(i=0;i<n;i++)
{
if(A[i]>0 && A[i]<n) //若A[i]的值介于1~n,则标记数组B
B[A[i]-1] = 1;
for(i=0;i<n;i++) //扫描数组B,找到目标值
if(B[i] ==0) break;
return i+1; //返回结果
}
}
//例子:{-5,3,2,3} ===> return 1
(2.3) 单链表
2.3.1 头插法建立单链表
/*
采用头插法建立单链表时,读入数据的顺序与生成的链表中的元素是相反的。每个结点插入的时间为O(1),设单链表长为n,则总时间复杂度为O(n)。
*/
LinkList List_HeadInsert(LinkList &L)
{
LNode *s;
int x;
L = (LinkList)malloc(sizeof(LNode));
L->next = NULL;
scanf("%d",&x);
while(x!=999)
{
s = (LNode*)malloc(sizeof(LNode));//创建新节点
s->data = x;
s->next = L->next;
L->next = s;//将新结点插入表中,L为头指针
scanf("%d",&x);
}
}
2.3.2 采用尾插法建立单链表
LinkList List_TailInsert(LinkList &L)
{
int x;
L = (LinkList)malloc(sizeof(LNode));
LNode *s,*r = L; //r为表尾指针
scanf("%d",&x);
while(x!=999)
{
s = (LNode*)malloc(sizeof(LNode));
s->data = x;
r->next = s;
r=s; //r指向新的表尾结点
scanf("%d",&x);
}
}
2.3.3 不带头结点的单链表L删除所有值为x的结点
/*
算法需要借助一个递归工作栈,深度为O(n),时间复杂度为O(n)。
*/
void Del_X_3(LinkList &L,int x)
{
LNode *p;
if(L==NULL) //p指向待删除结点
return; //递归出口
if(L->data==x)
{
p=L; //删除*L,并让L指向下一结点
L=L->next;
free(p);
Del_x_3(L,x);//递归调用
}
else //若L所指结点的值不为x
Del_X_3(L->next,x)
}
2.3.4 带头结点的单链表L删除所有值为 x 的结点,释放其空间,假设值 x 的结点不唯一
/*
用p从头至尾扫描单链表,pre指向*p结点的前驱。若p所指结点的值为x,则删除,并让p移向下一个结点,否则让pre、p指针同步后移一个结点。
该段代码可以一直使用,if条件可以更改。时间复杂度为O(n),空间复杂度为O(1).
*/
void Del_X_1(LinkList &L,int x)
{
LNode *p = L->next,*pre = L,*q;
while(p!=NULL)
{
if(p->data ==x)
{
q = p; //q指向该结点
p = p->next;
pre->next = p; //删除*q结点
free(q);
}
else //否则,pre和p同步后移
{
pre= p;
p = p->next;
}
}//while
}
2.3.5 带头结点的单链表L逆向输出每个结点的值
void R_Print(LinkList &L)
{
if(L->next != NULL)
{
R_Print(L->next);
}
print(L->data);
}
2.3.6 带头结点的单链表L中删除最小值结点(假设最小值结点唯一),时间高效
LinkList Delete_Min(LinkList &L)
{
LNode *pre = L,*P = pre->next; //p为工作指针,pre指向其前驱
LNode *minpre = pre,*minp = p; //保存最小值结点及其前驱
while(p!=NULL)
{
if(p->data < minp->data)
{
minp = p; //找到比之前找到的最小值结点更小的结点
minpre = pre;
}
pre = p; //继续扫描下一个结点
p = p->next;
}
minpre->next = minp->next; //删除最小值结点
free(minp);
return L;
}
//若本题改为不带头结点的单链表,则实现上会有所不同,请读者自行思考。
2.3.7 带头结点的单链表就地逆置,即辅助空间复杂度为O(1)。
/*
将头结点摘下,然后从第一结点开始,依次插入到头结点的后面,直到最后一个结点为止,这样就实现了链表的逆置。
*/
LinkList Reverse_1(LinkList L)
{
LNode *p,*r; //p为工作指针,r为p的后继,以防断链
p = L->next; //从第一个元素结点开始
L->next = NULL; //先将头结点L的next域置为NULL
while(p!=NULL) //依次将元素结点摘下
{
r = p->next; //暂存p的后继
p->next = L->next; //将p结点插入到头结点之后
L->next = p;
p = r;
}
return L;
}
2.3.8 带头结点的单链表L使其元素递增有序
/*
采用直接插入排序算法的思想,先构成只含一个数据结点的有序单链表,然后依次扫描单链表中剩下的结点*p (直至p==NULL为止),在有序表中通过比较查找 *p 的前驱结点*pre,然后*p插入到*pre之后。该算法时间复杂度为O(n^2).
*/
void Sort(LinkList &L)
{
LNode *p = L->next,*pre;
LNode *r = p->next; //r保持*p后继结点指针,以保证不断链
p->next = NULL; //构造只含一个数据结点的有序表
p = r;
while(p!=NULL)
{
r = p->next; //保存*p 的后继结点指针
pre = L;
while(pre->next != NULL && pre->next->data < p->data)
pre = pre->next; //在有序表中查找插入*p的前驱结点*pre
p->next = pre->next; //将*p插入到*pre之后
pre->next = p;
p = r; //扫描单链表中剩下的结点
}
}
2.3.9 带头结点的单链表L无序,删除表中所有介于给定的两个值之间的元素(若存在)
/*
因为链表是无序的,所以只能逐个结点进行检查,进行删除。
*/
void RangeDelete(LinkList &L,int min,int max)
{
LNode *pr = L,
*p = L->link; //p是检测指针,pr是其前驱
while(p!=NULL)
{
if(p->data>min && p->data<max) //寻找到被删结点,删除
{
pr->link = p->link;
free(p);
p = pr->link;
}
else //否则继续寻找被删结点
{
pr = p;
p = p->link;
}
}
}
2.3.10 找出两个链表的公共结点
/*
两个链表有公共结点,则两条链表的拓扑形状像一条平放的Y。
算法思路: 首先分别遍历两个链表得到它们的长度,并求出两个长度之差。在长的链表上先遍历长度之差个结点之后,再同步遍历两个链表,直到找到相同的结点,或者一直到链表结束。此时,该方法的时间复杂度为O(len1+len2)。
*/
LinkList Search_lst_Common(LinkList L1,LinkList L2)
{
int len1 = Length(L1),len2 = Length(L2);
LinkList longList,shortList;
if(len1>len2)
{
longList = L1->next;
shortList = L2->next;
dist = len1 - len2;
}
else
{
longList = L2->next;
shortList = L1->next;
dist = len2-len1;
}
while(dist--)
longList = longList->next;
while(longList != NULL)
{
if(longList == shortList)
return longList;
else
{
longList = longList->next;
shortList = shortList->next;
}
}//while
return NULL;
}
2.3.11 带头结点的单链表,head为头指针,递增输出数据元素,并释放结点存储空间。(不允许使用数组作为辅助空间)
/*
对链表遍历,在每次遍历中找出整个链表的最小值元素,输出并释放结点所占空间;再查找次小值元素,输出并释放空间,如此下去,直至链表为空,最后释放头结点所占存储空间。该算法的时间复杂度为O(n^2)。
*/
void Min_Delete(LinkList &head)
{
while(head->next !=NULL) //循环到仅剩头结点
{
pre = head; //pre为元素最小值结点的前驱结点的指针
p = pre->next; //p为工作指针
while(p->next != NULL)
{
if(p->next->data < pre->next->data);
pre = p; //记住当前最小值结点的前驱
p = p->next;
}
print(pre->next->data); //输出最小值
u = pre->next; //删除元素值最小的结点,释放结点空间
pre->next = u->next;
free(u);
}//while
free(head); //释放头结点
}
2.3.12 带头结点的单链表A分解为使A表中含有原表中序号为奇数的元素,B表中含有原表中序号为偶数的元素,相对顺序保持不变。
/*
设置一个访问变量(初值为0),每访问一个结点序号自动加1,然后根据序号的奇偶性将结点插入到A表或B表中,重复以上操作直到表尾。
为了保持原来结点的顺序,采用尾插法建立单链表。此外,本算法完全可以不用设置序号变量。while循环中的代码改为将结点插入到表A中并将下一结点插入到B中,这样while中第一处理的结点就是奇数号结点,第二处理的就是偶数号结点。
*/
LinkList DisCreat_1(LinkList &A)
{
int i=0;
B = (LinkList)malloc(sizeof(LNode));
B->next = NULL;
LNode *ra = A,*rb = B;
p = A->next;
A->next = NULL;
while(p!=NULL)
{
i++;
if(i%2==0)
{
rb->next = p;
rb = p;
}
else
{
ra-next = p;
ra = p;
}
p = p->next;
}
ra->next = NULL;
rb-next = NULL;
return B;
}
2.3.13 带头结点的单链表C={a1,b1,a2,b2······am,bn}拆解为两个线性表,使得A={a1,a2···an},B =
/*
采用上题的思路,不设序号变量,二者的的区别在于对B表的建立采用头插法。这里需要注意的是,采用头插法插入结点后,*p的指针域已改变,若不设变量保存其后继结点则会引起断链,从而导致算法出错。
*/
LinkList DisCreat_2(LinkList &A)
{
LinkList B = (LinkList)malloc(sizeof(LNode)); //创建B表表头
B->next = NULL; // B表的初始化
LNode *p = A->next,*q; //p为工作指针
LNode *ra = A; //ra始终指向A的尾结点
while(p!=NULL)
{
ra->next = p; //将*p链到A的表尾
ra = p;
p = p->next;
q = p->next; // 头插后,*p将断链,因此用q记忆*p的后继
p->next = B->next; //将*p插入到B的前端
B->next = p;
p = q;
}
ra->next = NULL; //A尾结点的next域置空
return B;
}
2.3.14 递增的单链表,去掉表中重复的元素,只保留一个数值
/*
由于是有序表,所有相同值域的结点都是相邻的。用p扫描递增单链表L,若*p结点的值域等于其后继结点的值域,则删除后者,否则p移向下一个结点。实际时间复杂度为O(n),空间复杂度为O(1)。
*/
void Del_Same(LinkList &L)
{
LNode *P = L->next,*q;
if(p==NULL)
return;
while(p->next !=NULL)
{
q = p->next; //q指向*p的后继结点
if(p->data == q->data) //找到重复的结点
{
p->next = q->next; //释放*q结点
free(q);
}
else
p = p->next;
}
}
2.3.15 两个递增的单链表归并为一个递减的单链表,并要求利用原来两个单链表的结点存放归并后的单链表
/*
合并时,均从第一个结点起开始比较,将较小的结点链入链表中,同时后移工作指针。该问题要求结果链表按元素值递减次序排列,故新链表的建立应该采用头插法。比较结束后,可能会有一个链表非空,此时用头插法将剩下的结点依次插入新链表中即可。
*/
void MergeList(LinkList &La,LinkList &Lb)
{
LNode *r,*pa = La->next, *pb = Lb->next;
La->next = NULL; //La作为结构链表的头指针,先将结果链表初始化为空
while(pa && pb) //当两链表不空时,循环
{
if(pa->data <= pb->next)
{
r = pa->next;
pa->next = La->next;
La->next = pa;
pa = r;
}
else
{
r = pb->next; //r暂存pb的后继结点指针
pb->next = La->next;
La->next = pb; //将pb结点链于结果表中,同时逆置
pb =r; //恢复pb为当前待比较结点
}
if(pa)
pb = pa; //通常情况下会剩一个链表非空,处理剩余的部分。
while(pb) //处理剩下的一个非空链表
{
r = pb->next; //依次插入到La中
pb->next = La->next;
La->next = pb;
pb =r;
}
free(Lb);
}
}
2.3.16 两个带头结点并且递增的单链表A和B中产生单链表C,C中包含它们的公共元素,要求不破坏A、B的结点
/*
表A、B都有序,可从第一个元素起依次比较A、B两表的元素,若元素值不等,则值小的指针往后移。若元素值相等,则创建一个值等于两结点的元素值的新结点,使用尾插法插入到新的链表中,并将两个原表指针后移一位,直到其中一个链表遍历到表尾。
*/
void Get_Common(LinkList A,LinkList B)
{
LNode *p = A->next,*q = B->next. *r,*s;
LinkList C = (LinkList)malloc(sizeof(LNode)); //建立表C
r = C; //r始终指向C的尾结点
while(p!=NULL && q!=NULL) //循环跳出条件
{
if(p->data < q->data)
p = p->next; //若A的当前元素较小,后移指针
else if(p->data > q->data)
q = q->next; //若B的当前元素较小,后移指针
else //找到公共元素结点
{
s = (LNode*)malloc(sizeof(LNode));
s->data = p->data; //复制产生结点*s
r-next = s; //将*s链接到C上
r = s;
p = p->next; //表A和B继续向后扫描
q = q->next;
}
r ->next = NULL; //置C尾结点指针为空
}
}
2.3.17 两个递增的单链表A和B,将A和B的公共元素存放于A链表中
/*
采用归并的思想,设置两个工作指针pa和pb,对两个链表进行归并扫描,只有同时出现在两集合中的元素才链接到结果表中且仅保留一个,其他的结点全部释放。当一个链表遍历完毕后,释放另一个表中剩下的全部结点。时间复杂度为O(len1+len2),空间复杂度为O(1).
*/
LinkList Union(LinkList &la,LinkList &lb)
{
pa = la->next;
pb = lb->next;
pc = la; //结果表中当前合并结点的前驱指针
while(pa && pb)
{
if(pa->data == pb->data) //交集并入结果表中
{
pc->next = pa; //A中结点链表到结果表
pc =pa;
pa = pa->next;
u = pb;
pb = pb->next;
free(u);
}
else if(pa->data < pb->data) //若A中当前结点值小于B中当前结点值
{
u=pa;
pa = pa->next;
free(u);
}
else //若B中当前结点值小于A中当前结点值
{
u = pb;
pb = pb->next;
free(u); //释放B中剩余结点
}
}//while结束
while(pa) //B已遍历完,A未完
{
u = pa;
pa = pa->next;
free(u); //释放A中剩余结点
}
while(pb){ //A已遍历完,B未完
u = pb;
pb = pb->next;
free(u); //释放B中剩余结点
}
pc->next = NULL; //置结果链表尾指针为NULL
free(lb); //释放B表的头结点
return la;
}
2.3.18 判断单链表B中的序列是否是单链表A中序列的连续子序列
/*
操作从两个链表的第一个结点开始,若对应数据相等,则后移指针;若对应数据不等,则A链表从上次开始比较结点的后继开始,B链表仍从第一个结点开始比较,直到B链表表到尾表示匹配成功。A链表到尾而B链表未到尾表示失败。操作中应记住A链表每次的开始结点,以便下次匹配时好从其后继开始。
*/
int Pattern(LinkList A,LinkList B)
{
LNode *p = A; //p为A链表的工作指针,本题假定A和B均为结点
LNode *pre =p; //pre记住每趟比较中A链表的开始结点
LNode *q = B; //q是B链表的工作指针
while(p && q)
{
if(p->data == q->data) //结点值相同
{
p = p->next;
q = q->next;
}
else
{
pre = pre->next;
p = pre; //A链表新的开始比较结点
q = B; //q从链表第一个结点开始
}
if(q==NULL) //B已经比较结束
return 1; //说明B是A的子列
else
return 0; //B不是A的子列
}
}
2.3.19 判断带头结点的循环双链表是否对称
/*
让p从左到右扫描,q从右向左扫描,直到它们指向同一结点(p==q,当循环双链表中结点个数为奇数时)或相邻(p->next=q 或q->prior =p,当循环双链表中结点个数为偶数时)为止若它们所指结点值相同,则继续进行下去,否则返回0。若比较全部相等,则返回1。
*/
int Symmetry(DLinkList L)
{
DNode *p = L->next,*q = L->prior; //两头工作指针
while(p!=q && p->next!=q)
{
if(p->data == q->data) //所指结点值相同则继续比较
{
p = p->next;
q = q->prior;
}
else //否则,返回0
return 0;
return 1; //比较结束后返回1
}
}
2.3.20 将循环单链表h2链接到h1之后,要求链接后的链表仍保持循环链表形式
/*
先找到两个链表的尾指针,将第一个链表的尾指针与第二个链表的头结点链接起来,再使之成为循环的。
*/
LinkList Link(LinkList &h1,LinkList &h2)
{
LNode *p,*q; //分别指向两个链表的尾结点
p = h1;
while(p->next != h1) //寻找h1的尾结点
p = p->next;
q = h2;
while(q->next != h2) //寻找h2的尾结点
q = q->next;
p->next = h2; //将h2链接到h1之后
q->next = h1; //令h2的尾结点指向h1
return h1;
}
2.3.21 带头结点的循环单链表,结点值均为正整数,反复找出结点值最小的结点输出并删除,直到单链表为空,再删除表头结点。
/*
对于循环单链表L,在不空时循环,每循环一次查找一个最小结点(由minp指向最小值结点,minpre指向其前驱结点)并删除它。最后释放头结点。
*/
void Del_All(LinkList &L)
{
LNode *p,*pre,*minp,*minpre;
while(L->next != L)
{
P = l->next; pre = L; //p为工作指针,pre指向其前驱
min = p;
minpre = pre; //minp指向最小值结点
while(p != L) //循环一遍,查找最小值结点
{
if(p->data < minp->data)
{
minp = p; //找到值更小的结点
minpre = pre;
}
pre = p; //=查找下一个结点
p = p->next;
}
prinf("%d",minp->data);
minpre->next = minp->next; //将最小值结点从表中"断开"
free(minp);
}
free(L); //释放头结点
}
(2.4) 单链表真题
2.4.1 查找链表倒数第 k 个位置上的结点 (k为正整数)。若查找成功,输出该结点的data域的值,并返回1;否则只返回0。
/*
描述算法的基本思想:定义两个指针变量p和q,初始时均指向头结点的下一个结点(链表的第一个结点),p指针沿链表移动;当p指针移动到第k个结点时,q指针开始于p指针同步移动;当p指针移动到最后一个结点时,q指针所指示结点为倒数第k个结点。以上过程对链表仅进行一遍扫描。
描述算法的详细实现步骤:
① count=0, p和q指向链表表头结点的下一个结点。
② 若p为空,转⑤
③ 若count等于k,则q指向下一个结点;否则,count = count +1。
④ p指向下一个结点,转②
⑤ 若count等于k,则查找成功,输出该结点的data域的值,返回1;否则,说明超过了线性表的长度,查找长度,返回0。
⑥ 算法结束。
程序设计语言描述算法:
*/
typedef struct LNode
{
int data;
struct LNode *link;
}LNode,*LinkList
int Search_k(LinkList list,int k)
{
LNode*p = list->link,*q = list->link; //指针p、q指示第一个结点
int count =0;
while(p!=NULL) //遍历链表直到第一个结点
{
if(count < k) count++; //计数,若count<k只移动p
else q = q->link;
p = p->link; //之后让p、q同步移动
}//while
if(count < k) //查找失败返回0
return 0; //否则打印并返回1
else
{
printf("%d",q->data);
}
}
2.4.2 带头结点的单链表保存保存单词,当两个单词有相同的后缀时,可共享相同的后缀存储空间,找出由 str1 和 str2所指向两个链表共同后缀的起始位置。
/*
描述算法的基本设计思想
采用程序设计语言描述算法
说明时间复杂度为O(len1+len2)
*/
typedef struct Node
{
char data;
struct Node *next;
}SNode;
/*求链表长度的函数*/
int listlen(SNode *head)
{
int len = 0;
while(head->next != NULL)
{
len++;
head = head->next;
}
return len;
}
/*找出共同后缀的起始地址*/
SNode* find_addr(SNode *str1,SNode *str2)
{
int m,n;
SNode *p,*q;
m = listlen(str1); //求str1的长度
n = listen(str2); //求str2的长度
for(p = str1; m>n;m--) //若m>n,使q指向链表中的第 n-m+1 个结点
p = p->next;
for(q = str2; m<n;n--) //若m<n,使q指向链表中的第 n-m+1 个结点
q = q->next;
while(p->next!= NULL && p->next!= q->next) //将指针p和q同步向后移动
{
p = p->next;
q = q->next;
}//while
return p->next; //返回共同后缀的起始地址
}
2.4.3 带头结点的单链表保存 m 个整数,且 |data|<=n,(n为正整数), 对于链表中绝对值相等的结点,仅保留第一次出现的结点而删除其余绝对值相等的结点,时间上尽可能高效。
/*
描述算法的基本设计思想是用空间换时间,使用辅助数组记录链表中已出现的数值,从而只需对链表进行一趟扫描。
因为|data|<=n,故辅助数组q的大小为n+1,各元素的初值均为0。依次扫描链表中的各结点,同时检查q[|data|]的值,若为0则保留该结点,并令q[|data|] = 1,否则将该结点从链表中删除。
时间复杂度为O(m),空间复杂度为O(n)
*/
/*使用C或C++给出单链表结点的数据类型定义*/
typedef int node
{
int *data;
struct node *link;
}NODE;
Typedef NODE *PNODE;
void func(PNODE h,int n)
{
PNODE p=h,r;
int *q,m;
q = (int*)malloc(sizeof(int)*n+1));
for(int i=0; i<n+1;i++)
*(q+i) = 0;
while(p->link != NULL)
{
m = p->link->data>0 ? p->link->data: -p->link->data;
if(*(q+m)==0)
{
*(q+m)=1;
p = p->link;
}
else
{
r = p->link;
p->link = r->link;
free(r);
}
}
free(q);
}
三、栈和队列
(3.1) 栈
3.1.1 单链表头指针为L,data域为字符型。判断链表的全部 n 个字符是否中心对称。例如 xyx 是中心对称。
/*
让链的前一半元素依次进栈,在处理链表的最后一半元素时,当访问到链表的一个元素后,就从栈中弹出一个元素,两个元素比较,若相等,则将链表中的下一个元素与栈中弹出的元素比较,直至链表到尾。这时若栈是空栈,则得出链表中心对称的结论;否则,当链表中的一个元素与弹出元素不等时,结论为非中心对称。
*/
int dc(LinkList L,int n)
{
int i;
char s[n/2]; //s字符栈
p = L->next; //p是链表的工作指针,指向待处理的当前元素
for(i=0;i<n/2;i++) //链表前一半元素进栈
{
s[i] = p->data;
p = p->next;
}
i--; //恢复最后的i值
if(n%2 == 1) //若n是奇数,后移过中心结点
p = p->next;
while(p!=NULL && s[i]==p->data) //检测是否中心对称
{
i--; //i充当栈顶指针
p = p->next;
}
if(i==-1) //栈为空栈
return i; //链表中心对称
else
return 0; //链表不中心对称
}
(3.2) 队列
3.2.1 循环队列设置一个标志域tag,并以 tag 的值为 0 或 1 来区分队头指针 front 和 队尾指针 rear 相同时的队列状态是 " 空" 还是 “满”,试编写此结构相应的入队和出队算法。
/*
进队时置tag为1,出队时置tag为0。置tag =0、front =rear =0,这样队列的4要素如下:
队空条件: Q.front == Q.rear且Q.tag ==0
队满条件: Q.front == Q.rear且Q.tag ==1
进队操作: Q.data[Q.rear] =x; Q.rear = (Q.rear +1)% MaxSize; Q.tag =1
出队操作: x = Q.data[Q.front]; Q.front = (Q.front+1) % MaxSize; Q.tag =0
*/
/*
设"tag"法循环入队算法
*/
int EnQueue1(SqQueue &Q,int x)
{
if(Q.front==Q.rear && Q.tag==1)
return 0; //两个条件都满足时则队满
Q.data[Q.rear] = x;
Q.rear = (Q.rear +1) % MaxSize;
Q.tag = 0; //可能队满
return 1;
}
/*
设"tag"法循环出队算法
*/
int DeQueue1(SeQueue &Q,int x)
{
if(Q.front==Q.rear && Q.tag == 0)
return 0; //两个条件都满足时则队空
x = Q.data[Q.front];
Q.front = (Q.front +1) % MaxSize;
Q.tag = 0; //可能队空
return 1;
}
3.2.2 Q是一个队列,S是一个空栈,实现将队列中的元素逆置的算法。
/*
让队列中的元素逐个出队列,入栈;
全部入栈后再逐个出栈,入队列。
*/
void Inverser(Stack S,Queue Q)
{
while(!QueueEmpty(Q))
{
x = DeQueue(Q); //栈中全部元素依次出队
Push(S,x); //元素依次入栈
}
while(!StackEmpty(S))
{
Pop(S,x); //栈中全部元素依次出栈
EnQueue(Q,x); //再入队
}
}
3.2.3 利用两个栈 S1、S2 来模拟一个队列,已知栈的4个运算来实现该队列的 3 个运算。
/*
4个运算定义如下:
Push(S,x);
pop(S,x);
StackEmpty(S);
StackOverflow(S);
队列的3个运算分别是 Enqueue、Dequeue、QueueEmpty
*/
//进队算法
int EnQueue(Stack &S1,Stack &S2,int e)
{
if(!StackOverflow(S1))
{
Push(S1,e);
return 1;
}
if(StackOverflow(S1) && !StackEmpty(S2))
{
while(!StackEmpty(S1))
{
Pop(S1,x);
Push(S2,x);
}
}
Push(S1,e);
return 1;
}
//出队算法
void DeQueue(Stack &S1,Stack &S2,int &x)
{
if(!StackEmpty(S2))
{
Pop(S2,x);
}
else if(StackEmpty(S1))
{
printf("队列为空");
}
else
{
while(!StackEmpty(S1))
{
Pop(S1,x);
Push(S2,x);
}
Pop(S2,x);
}
}
//队列为空
int QueueEmpty(Stack S1,Stack S2)
{
if(StackEmpty(S1) && StackEmpty(S2))
return 1;
else
return 0;
}
(3.3) 栈和队列的应用
3.3.1 判别算法表达式的括号是否配对,以字符"\0"作为算术表达式的结束符。
bool BracketCheck(char *str)
{
InitStack(S); //初始化栈
int i =0;
while(str[i] !='\0')
{
switch(str[i])
{ //左括号入栈
case '(':
Push(S,'(');
break;
case '[':
Push(S,'(');
break;
case '{':
Push(S,'(');
break;
//遇到右括号,检测栈顶
case ')':
Pop(S,e);
if(e!='(')
return false;
break;
case ']':
if(e!='[')
return false;
Pop(S,e);
break;
case '}':
Pop(S,e);
if(e!='{')
return false;
break;
default:
break;
}//switch
}//while
if(!IsEmpty(S))
{
printf("括号不匹配\n");
return false;
}
else
{
printf("括号匹配\n");
return true;
}
}
3.3.2 过江渡船每次只能载 10 辆车过江,过江车辆分为客车类和货车类,上渡船有如下规定:同类车先到先上船;客车先于货车上船,且每上4辆客车,才允许放上 1 辆货;若等待客车不足 4 辆,则以货车代替;若无货车等待,允许客车都上船,试设计一个算法模拟渡口管理。
/*
假设数组q的最大下标为10,,恰好是每次载渡的最大量。假设客车的队列为q1,货车的队列为q2。若q1充足,则每取4个q1元素后再取一个q2元素,直到q的长度为10。若q1不充足,则直接用q2补齐。
*/
Queue q; //过江渡船载渡队列
Queue q1; //客车队列
Queue q2; //货车队列
void manager()
{
int i=0,j=0; //j表示渡船上的总车辆数
while(j<10) //不足10辆时
{
if(!QueueEmpty(q1) && i<4) //客车队列不为空,则未上足4辆
{
DeQueue(q1,x); //从客车队列出列
EnQueue(q,x); //客车上渡船
i++; //客车数加1
j++; //渡船上的总车辆数加1
}
else if(i==4 && !QueueEmpty(q2)) //客车已上足4辆
{
DeQueue(q2,x); //从货车队列出列
EnQueue(q,x); //货车上渡船
j++; //渡船上的总车辆数加1
i = 0; //每上一辆车,i重新计数
}
else
{
while(j<10 && i<4 &&!QueueEmpty(q2)) //客车队列空
{
DeQueue(q2,x); //从货车队列出队
EnQueue(q,x); //货车上渡船
i++;
j++; //渡船上的总车辆加1
}
i = 0;
}
if(QueueEmpty(q1) && QueueEmpty(q2))
j = 11; //若货车和客车加起来不足10辆
}
}
四、树和二叉树
(4.1) 二叉树的遍历
4.1.1 编写后序遍历二叉树的非递归算法
/*
算法思想:后序非递归遍历二叉树的顺序是先访问左子树,再访问右子树,最后访问根结点。当用堆栈来存储结点时,必须分清返回根结点时是从左子树返回的还是从右子树返回的。所以,使用辅助指针r,其指向最近访问过的结点。也可在结点中增加一个标志域,记录是否已被访问。
PS:访问一个结点*p时,栈中结点恰好是*p结点的所有祖先。从栈底到栈顶结点再加上*p结点,刚好构成从根结点到*p结点的一条路径。在很多算法设计中都利用了这一特性求解,如求根结点到某结点的路径、求两个结点的最近公共祖先等,都可以利用这个思路来实现。
*/
void postOrder(BiTree T)
{
InitStack(S);
p = T;
r = NULL;
while(p|| !IsEmpty(S))
{
if(p){ //走到最左边
push(S,p);
p = p->lchild;
}
else{ //向右
GetTop(S,p); //取栈顶结点
if(p->rchild && p->rchild != r) //若右子树存在,且未被访问过
{
p = p->rchild; //转向右
push(S,p); //压入栈
p = p->lchild; //再走到最左
}
else{ //否则,弹出结点并访问
pop(S,p); //将结点弹出
visit(p->data); //访问该结点
r = p; //记录最近访问过的结点
p = NULL; //结点访问完后,重置该指针
}
}//else
}//while
}
4.1.2 编写二叉树的自下而上、自右到左的层次遍历算法
/*
一般的二又树层次遍历是自上而下、从左到右,这里的遍历顺序恰好相反。算法思想:利用原有的层次遍历算法,出队的同时将各结点指针入栈在所有结点入栈后再从栈顶开始依次访问即为所求的算法。具体实现如下:
1)把根结点入队列。
2)把一个元素出队列,遍历这个元素
3)依次把这个元素的右孩子、左孩子入队列。
4)若队列不空,则跳到(2),否则结束。
*/
void InvertLevel(BiTree bt){
Stack s;
Queue Q;
if(bt!=NULL)
{
InitStack(s); //栈初始化,栈中存放二叉树结点的指针
InitQueue(Q); //队列初始化,队列中存放二叉树结点的指针
EnQueue(Q,bt);
while(IsEmpty(Q)==false) //从上而下层次遍历
{
DeQueue(Q,p);
Push(s,p); //出队,入栈
if(p->lchild)
EnQueue(Q,p->lchild); //若左子女不空,则入队列
if(p->rchild)
EnQueue(Q,p->rchild); //若右子女不空,则入队列
}
while(IsEmpty(s) == false){
Pop(s,p);
visit(p->data);
} //自下而上、自右到左的层次遍历
}//if结束
}
4.1.3 非递归算法求二叉树的高度
/*
采用层次遍历的算法,设置变量1eve1记录当前结点所在的层数,设置变量last指向当前层的最右结点,每次层次遍历出队时与last 指针比较,若两者相等,则层数加1,并让last指向下一层的最右结点,直到遍历完成。1eve1的值即为二叉树的高度。
*/
int Btdepth(BiTree T)
{
if(!T)
return 0; //树空,高度为0
int front= -1, rear = -1;
int last = 0,level = 0; //last指向下一层第一个结点的位置
BiTree Q[MaxSize]; //设置队列Q,元素是二叉树结点指针且容量足够
Q[++rear] = T; //将根结点入队
BiTree p;
while(front < rear) //队不空,则循环
{
p = Q[++front]; //队列元素出队,即正在访问的结点
if(p->lchild)
Q[++rear] = p->lchild; //左孩子入队
if(p->rchild)
Q[++rear] = p->rchild; //右孩子入队
if(front == last){ //处理该层的最右结点
level++; //层数增1
last = rear; //last指向下层
}
}
return level;
}
/*
求某层的结点个数、每层的结点个数、树的最大宽度等,都采用与此题类似的思想。当然,此题可编写递归算法,其实现如下
*/
int Btdepth2(BiTree T)
{
if(T==NULL)
return 0; //空树,高度为0
ldep = Btdepth(T->lchild); //左子树高度
rdep = Btdepth(T->rchild); //右子树高度
if(ldep > rdep)
return ldep+1; //树的高度为子树最大高度加根节点
else
return rdep+1;
}
4.1.4 二叉树各结点的值互不相同,其先序遍历和中序遍历序列分别存于两个一维数组A[1···n]和B[1···n]中,试编写算法建立该二叉树的二叉链表。
/*
由先序序列和中序序列可以唯一确定一棵二叉树。算法的实现步骤如下:
1) 根据先序序列确定树的根结点。
2) 根据根结点在中序序列中划分出二叉树的左、右子树包含哪些结点,然后根据左、右子树结点在先序序列中的次序确定子树的根结点,即回到步骤1)。
3) 如此重复上述步骤,直到每棵子树仅有一个结点(该子树的根结点)为止。
*/
BiTree PreInCreat(int A[],int B[],int l1,int h1,int l2,int h2)
{
//11,h1为先序的第一和最后一个结点下标,12,h2为中序的第一和最后一个结点下标
//初始调用时,l1=l2=1, h1=h2=n
root = (BiTNode*)malloc(sizeof(BiTNode));
root->data = A[l1];
for(i = l2;B[i]!=root->data;i++);
llen = i - l2;
rlen = h2 - i;
if(llen)
root->lchild = PreInCreat(A,B,l1+1,l1+llen,l2,l2+llen-1);
else
root->lchild = NULL;
return root;
}
4.1.5 二叉树按二叉链表形式存储,写一个判别给定二叉树是否是完全二叉树的算法。
/*
根据完全二叉树的定义,具有n个结点的完全二叉树与满二又树中编号从1~n的结点一一对应。
算法思想:采用层次遍历算法,将所有结点加入队列(包括空结点)。遇到空结点时,查看其后是否有非空结点。若有,则二又树不是完全了叉树。
*/
bool IsComplete(BiTree T)
{
InitQueue(Q);
if(!T)
return 1; //空树为满二叉树
EnQueue(Q,T);
while(!IsEmpty(Q))
{
DeQueue(Q,p);
if(p) //结点非空,将其左、右子树入队列
{
EnQueue(Q,p->lchild);
EnQueue(Q,p->rchild);
}
else //结点为空,检查其后是否有非空结点
while(!IsEmpty(Q)){
DeQueue(Q,p);
if(p) //结点非空,则二叉树为非完全二叉树
return 0;
}
}
return 1;
}
4.1.6 二叉树按二叉链表形式存储,计算一棵给定二叉树的所有双分支结点个数。
/*
计算一棵二叉树b中所有双分支结点个数的递归模型f(b)如下:
f(b)=0 若b=NULL
f(b)=f(b->1chi1d) + f(b->rchild) + 1 若*b为双分支结点
f(b)=f(b->1chi1d) + f(b->rchild) 其他情况(*b为单分支结点或叶子结点)
*/
int DsonNodes(BiTree b)
{
if(b==NULL)
return 0;
else if(b->lchild!=NULL && b->rchild!=NULL)
return DsonNodes(b->lchild) + DsonNodes(b->rchild)+1;
else
return DsonNodes(b->lchild) + DsonNodes(b->rchild);
}
4.1.7 二叉树B按二叉链表形式存储,编写一个树B中所有结点的左、右子树进行交换的函数。
/*
采用递归算法实现交换二叉树的左、右子树,首先交换b结点的左孩子的左、右子树,然后交换b结点的右孩子的左、右子树,最后交换b结点的左、右孩子,当结点为空时递归结束(后序遍历的思想)。
*/
void swap(BiTree b)
{
if(b){
swap(b->lchild); //递归地交换左子树
swap(b->rchild); //递归地交换右子树
temp = b->lchild; //交换左、右孩子结点
b->lchild = b->rchild;
b->rchild = temp;
}
}
4.1.8 二叉树按二叉链表形式存储,求先序遍历序列中第 k (1<=k<=二叉树中结点个数) 个结点的值。
/*
设置一个全局变量i记录已访问过的结点的序号,其初值是根结点在先序序列中的序号,即1.当二叉树b为空时返回特殊字符,,当i==k时,表示找到了满足条件的结点,返回b->data;当ik时,递归地在左子树中查找,若找到则返回该值,否则继续递归地在右子树中查找,并返回其结果。
本题实质上就是一个遍历算法的实现,只不过用一个全局变量来记录访问的序号,求其他遍历序列的第k个结点也采用相似的方法。二叉树的遍历算法可以引申出大量的算法题,因此考生务必要熟练掌握二又树的遍历算法。
*/
int i=1; //遍历序号的全局变量
int PreNode(BiTree b,int k)
{
if(b==NULL) //空结点,则返回特殊字符
return '#'; //相等,则当前结点即为第k个结点
if(i==k)
return b->data;
i++; //下一个结点
ch = PreNode(b->lchild,k); //左子树中递归寻找
if(ch !='#') //在左子树中,则返回该值
return ch;
ch = PreNode(b->rchild,k); //在右子树中递归寻找
return ch;
}
4.1.9 二叉树按二叉链表形式存储,对于树中每个元素值为 x 的结点,删去以它为根的子树,并释放相应的空间。
/*
删除以元素值x为根的子树,只要能删除其左、右子树,就可以释放值为x的根结点,因此宜采用后序遍历。
算法思想:删除值为x的结点,意味着应将其父结点的左(右)子女指针置空,用层次遍历易于找到某结点的父结点。
本题要求删除树中每个元素值为x的结点的子树,因此要遍历完整棵二叉树。
*/
void DeleteXTree(BiTree bt) //删除以bt为根的子树
{
if(bt){
DeleteTree(bt->lchild);
DeleteTree(bt->rchild); //删除bt的左子树、右子树
free(bt); //释放被删结点所占的存储空间
}
}
//在二叉树上查找所有以x为元素值的结点,并删除以其为根的子树
void Search(BiTree bt,int x)
{
BiTree Q[]; //Q是存放二叉树结点指针的队列,容量足够大
if(bt){
if(bt->data == x){ //若根结点值为x,则删除整棵树
DeleteXTree(p->lchild);
exit(0);
}
Init Queue(Q);
EnQueue(Q,bt);
while(!IsEmpty(Q)){
DeQueue(Q,p);
if(p->lchild) //若左子女非空
if(p->lchild->data == x){ //左子树符合则删除左子树
DeleteXTree(p->lchild);
p->lchild = NULL;
} //父结点的左子女置空
else
EnQueue(Q,p->lchild); //左子树入队列
if(p->rchild) //若右子女非空
if(p->rchild->data ==x){ //右子女符合则删除右子树
DeleteXTree(p->rchild);
p->rchild = NULL; //父结点的右子女置空
}
else
EnQueue(Q,p->rchild); //右子女入队列
}
}
}
4.1.10 二叉树中查找值为 x 的结点,打印值为 x 的结点的所有祖先,假设值为x的结点不多于一个。
/*
算法思想:采用非递归后序遍历,最后访问根结点,访问到值为x的结点时,栈中所有元素均为该结点的祖先,依次出栈打印即可。因为查找的过程就是后序遍历的过程,因此使用的栈的深度不超过树的深度。
*/
typedef struct{
BiTree t;
int tag; //tag=0表示左子女已被访问,tag=1表示右子女已被访问
}stack;
void Search(BiTree bt,int x)
{
stack s[]; //栈容量足够大
top = 0;
while(bt!=NULL || top>0)
{
while(bt!=NULL && bt->data !=x) //结点入栈
{
s[++top].t = bt;
s[top].tag = 0;
bt = bt->lchild; //沿左分支向下
}
if(bt->data ==x)
{
printf("所查结点的所有祖先结点的值为:\n"); //找到x
for(i=1;i<=top;i++)
printf("%d",s[i].t->data); //输出祖先值后结束
exit(1);
}
while(top!=0 && s[top].tag==1)
top--; //退栈(空遍历)
if(top!=0)
{
s[top].tag =1;
bt = s[top].t->rchild; //沿右分支向下遍历
}
}//while
}
4.1.11 二叉树中 p 和 q分别为指向该二叉树中任意两个结点的指针,试编写算法找到 p 和 q的最近公共祖先结点 r 。
/*
后序遍历最后访问根结点,即在递归算法中,根是压在栈底的。
本题要找p和q的最近公共祖先结点r,不失一般性,设p在q的左边。
算法思想:采用后序非递归算法,栈中存放二又树结点的指针,当访问到某结点时,栈中所有元素均为该结点的祖先。后序遍历必然先遍历到结点p,栈中元素均为p的祖先。先将栈复制到另一辅助栈中。继续遍历到结点q时,将栈中元素从栈顶开始逐个到辅助栈中去匹配,第一个匹配(即相等)的元素就是结点p和q的最近公共祖先。
*/
typedef struct{
BiTree t;
int tag; //tag=0表示左子女已被访问,tag=1表示右子女已被访问
}stack;
stack s[],s1[]; //栈,容量足够大
BiTree Ancester(BiTree ROOT,BiTNode *p,BiTNode *q){
top = 0;
bt = ROOT;
while(bt!=NULL && bt!=p && bt!=q){ //结点入栈
while(bt != NULL){
S[++top].t = bt;
s[top].tag = 0;
bt= bt->lchild;
} //沿左分支向下
while(top!=0 && s[top].tag ==1){
//假定p在q的左侧,遇到p时,栈中元素均为p的祖先
if(s[top].t ==p){
for(i=1;i<=top;i++){
s1[i] = s[i];
top1 = top;
} //将栈s的元素转入辅助栈s1保存
if(s[top].t ==q) //找到q结点
for(i=top;i>0;i--){ //将栈中元素的树结点到s1中去匹配
for(j=top1;j>0;j--)
if(s1[j].t == s[i].t)
return s[i].t; //p和q的最近公共祖先已找到
}
top--; //退栈
}//while
if(top!=0){
s[top].tag = 1;
bt = s[top].t->rchild;
} //沿右分支向下遍历
}//while
return NULL; //p和q无公共祖先
}
4.1.12 二叉树按二叉链表形式存储,试求非空二叉树b的宽度 (即具有结点数最多的那一层的结点个数)。
/*
采用层次遍历的方法求出所有结点的层次,并将所有结点和对应的层次放在一个队列中。然后通过扫描队列求出各层的结点总数,最大的层结点总数即为二又树的宽度。
注意:本题队列中的结点,在出队后仍需要保留在队列中,以便求二又树的宽度,所以设置的队列采用非环形队列,否则在出队后可能被其他结点覆盖,无法再求二又树的宽度。
*/
typedef struct(
BiTree data[MaxSize]; //i保存队列中的结点指针
int level[MaxSize]; //保存data中相同下标结点的层次
int front,rear;
}Qu;
int BTWidth(BiTree b){
BiTree p;
int k,max,i,n;
Qu.front=Qu.rear= -1; //队列为空
Qu.rear++;
Qu.data[Qu.rear]=b; //根结点指针入队
Qu.level[Qu.rear]=1; //根结点层次为1
while(Qu.front<Qu.rear){
Qu.front++; //出队
p=Qu.data[Qu.front]; //出队结点
k=Qu.level[Qu.front]; //出队结点的层次
if(p->lchild!=NULL){ //左孩子进队列
Qu.rear++;
Qu.data[Qu.rear]=p->lchild;
Qu.level[Qu.rear]=k+1;
}
if(p->rchild!=NULL){ //右孩子进队列
Qu.rear++;
Qu.data [Qu.rear]=p->rchild;
Qu.level[Qu.rear]=k+1;
}
}//while
max=0;i=0; //max保存同一层最多的结点个数
k = 1; //k表示从第一层开始查找
while(i<=Qu.rear){ //i扫描队中所有元素
n=0; //n统计第k层的结点个数
while(i<=Qu.rear&6Qu.level[i]==k){
n++;
i++;
}
k=Qu.level[i];
if(n>max) max=n; //保存最大的n
}
return max;
}
4.1.13 设有一棵满二叉树(所有结点值均不同),已知其先序序列为pre,设计一个算法求其后序序列post.
/*
对一般二叉树,仅根据先序或后序序列,不能确定另一个遍历序列。但对满二叉树,任意一个结点的左、右子树均含有相等的结点数,同时,先序序列的第一个结点作为后序序列的最后一个结点,由此得到将先序序列pre[11..h1]转换为后序序列post[12..h2]的递归模型如下:
f(pre,l1,h1,post,l2,h2) =不做任何事情 h1<l1时
f(pre,l1,h1,post,l2,h2) = post[h2] = pre[11] 其他情况
取中间位置half=(h1-12)/2:
将pre[l1+1,l1+ha1f]左子树转换为post[l2,l2+half-1],
即f(pre,l1+1,l1+half,post,12,l2+half-1);
将pre[11+half+1,h1]右子树转换为post[12+half,h2-1],
即f(pre,l1+half+1,h1,post,l2+half,h2-1)。
其中,post[h2]=pre[l1]表示后序序列的最后一个结点(根结点)等于先序序列的第一个结点(根结点)。
*/
void PreToPost(int pre[],int l1,int h1,int post[],int l2,int h2){
int half;
if(h1>=l1){
post[h2]=pre[l1];
half = (h1-l1)/2;
PreroPost(pre,l1+1,l1+half,post,l2,l2+half-1);//转换左子树
PreroPost(pre,l1+half+1,h1,post,l2+half,h2-1;//转换右子树
}
}
//测试
char *pre="ABCDEFG";
char post[MaxSize];
PreToPost(pre,0,6,post,0,6);
printf("后序序列:");
for(int i=0;i<=6;i++)
printf("%c",post[i]);
printf("\n");
4.1.14 二叉树按二叉链表形式存储,设计算法将二叉树的叶结点按从左到右的顺序连成一个单链表,表头指针为head,链接时用叶结点的右指针域来存放单链表指针。
/*
算法思想:设置前驱结点指针pre,初始为空。第一个叶结点由指针head指向,遍历到叶结点时,就将它前驱的rchi1d指针指向它,最后一个叶结点的rchi1d为空。算法的时间复杂度为O(n),辅助变量使用head和pre,栈空间复杂度为O(n)。
*/
LinkedList head,pre=NULL; //全局变量
LinkedList InOrder(BiTree bt){
if(bt){
Inorder(bt->lchild); //中序遍历左子树
if(bt->lchild==NULL && bt->rchild==NULL)//叶结点
if(pre==NULL){
head=bt;
pre=bt;
} //处理第一个叶结点
else{
pre->rchild=bt;
pre=bt;
} //将叶结点链入链表
Inorder(bt->rchild); //中序遍历右子树
pre->rchi1d=NULL; //设置链表尾
return head;
}
4.1.15 判断两棵二叉树是否相似的算法,所谓二叉树T1和T2相似,指的是T1和T2都是空的二叉树或都只有一个根节点;或T1的左子树和T2的左子树是相似的,且T1的右子树和T2的右子树是相似的。
/*
本题采用递归的思想求解,若T1和T2都是空树,则相似;若有一个为空另一个不空则必然不相似;否则递归地比较它们的左、右子树是否相似。递归函数的定义如下:
1) 若T1=T2==NULL,则f(T1,T2) =1;
2) 若T1和T2之一为NULL,另一个不为NULL,则f(T1,T2) =0;。
3) 若T1和T2均不为NULL,则f(T1,T2) = f(T1->1child,T2->1child) && f(T1->rchild,T2->rchild);
*/
int similar(BiTree T1,BiTree T2){
//采用递归的算法判断两个二叉树是否相似
int leftS,rightS;
if(T1==NULL && T2==NULL)//两树皆空
return 1;
else if(T1==NULL||T2==NULL)//只有一树为空
return 0;
else{ //递归判断
leftS = similar(T1->lchild,T2->lchild);
rightS = similar(T1->rchild,T2->rchild);
return leftS && rightS;
}
}
4.1.16 写出在中序线索二叉树里查找指定结点在后序的前驱结点的算法。
/*
算法思想:在后序序列中,若结点p有右子女,则右子女是其前驱,若无右子女而有左子女,则左子女是其前驱。若结点p左、右子女均无,设其中序左线索指向某祖先结点f(p是f右子树中按中序遍历的第一个结点),若f有左子女,则其左子女是结点p在后序下的前驱;若f无左子女,则顺其前驱找双亲的双亲,一直找到双亲有左子女(这时左子女是p的前驱)。还有一种情况,若p是中序遍历的第一个结点,则结点p在中序和后序下均无前驱。
*/
BiThrTree InPostPre(BiThrTree t,BiThrTree p){
BiThrTree q;
if(p->rtag == 0) //若p有右子女,则右子女是其后序前驱
q = p->rchild;
else if(p->ltag ==0) //若p只有左子女,左子女是其后序前驱
q = p->lchild;
else if(p->lchild ==NULL)
q = NULL; //p是中序序列第一结点,无后序前驱
else //顺左线索向上找p的祖先,若存在,再找祖先的左子女
{
while(p->ltag ==1 && p->lchild!=NULL)
p = p->lchild;
if(p->ltag ==0)
q = p->lchild; //p结点的祖先的左子女是其后序前驱
else
q = NULL; //仅有单支树(p是叶子),已到根结点,p无后序前驱
}
return q;
}
(4.2) 二叉树的遍历--真题
4.2.1 二叉树按二叉链表形式存储,设计求二叉树T的WPL的算法
/*
(1) 给出算法的基本设计思想
(2) 给出二叉树结点的数据类型定义
(3) C++语言描述算法,关键之处给出注释
*/
/*
考查二叉树的带权路径长度,二叉树的带权路径长度为每个叶结点的深度与权值之积的总和,可以使用先序遍历解决问题。
1)算法的基本设计思想。
基于先序递归遍历的算法思想是用一个static变量记录wpl,把每个结点的深度作为递归函数的一个参数传递。
算法步骤如下:
① 若该结点是叶结点,则变量wpl加上该结点的深度与权值之积。
② 若该结点是非叶结点,则左子树不为空时,对左子树调用递归算法,右子树不为空,对右子树调用递归算法,深度参数均为本结点的深度参数加1。
③最后返回计算出的wpl即可。
PS:当static关键字用于代码块内部的变量的声明时,用于修改变量的存储类型,即从自动变量修改为静态变量,但变量的链接属性和作用域不受影响。用这种方式声明的变量在程序执行之前创建,并在程序的整个执行期间一直存在,而不是每次在代码块开始执行时创建,在代码块执行完毕后销毁。也就是说,它保持局部变量内容的持久。静态局部变量的生存期虽然为整个源程序,但其作用域仍与局部变量相同,即只能在定义该变量的函数内使用该变量。退出该函数后,尽管该变量还继续存在,但不能使用它。
*/
// 二叉树结点的数据类型定义如下:
typedef struct BiTNode{
int weight;
struct BiTNode *lchild,*rchild;
}BiTNode,*BiTree;
int WPL(BiTree root){
return wpl_PreOrder(root,0);
}
int wpl_Preorder(BiTree root,int deep){
static int wpl=0; //定义一个static变量存储wp1
if(root->lchild==NULL && root->rchild==NULL) //若为叶结点,累积wp1
wpl += deep*root->weight;
if(root->lchild !=NULL) //若左子树不空,对左子树递归遍历
wp1_PreOrder(root->1child,deep+1);
if(root->rchild !=NULL) //若右子树不空,对右子树递归遍历
wp1_PreOrder(root->rchild,deep+1);
return wpl;
4.2.2 将给定的表达式(二叉树)转换为等价的中缀表达式(通过括号反映操作符的计算次序)并输出。
/*
题目给定条件:二叉树结点定义如下:
typedef struct node{
char data[10];
struct node *left,*right;
}
(1) 给出算法的基本设计思想
(2) C++语言描述算法,关键之处给出注释
*/
/*
1)算法的基本设计思想:
表达式树的中序序列加上必要的括号即为等价的中缀表达式。可以基于二叉树的中序遍历策略得到所需的表达式。
表达式树中分支结点所对应的子表达式的计算次序,由该分支结点所处的位置决定。为得到正确的中缀表达式,需要在生成遍历序列的同时,在适当位置增加必要的括号。显然,表达式的最外层(对应根结点)和操作数(对应叶结点)不需要添加括号。
2)算法实现:
将二又树的中序遍历递归算法稍加改造即可得本题的答案。除根结点和叶结点外,遍历到其他结点时在遍历其左子树之前加上左括号,遍历完右子树后加上右括号。
*/
void BtreeToE(BTree *root){
BtreeToExp(root,1); //根的高度为1
}
void BtreeToExp(BTree*root,int deep)
{
if(root==NULL) return; //空结点返回
else if(root->left==NULL && root->right==NULL) //若为叶结点
printf("%s",root->data);
else{
if(deep>1) printf("("); //输出操作数,不加括号
BtreeToExp(root->left,deep+1); //若有子表达式则加1层括号
printf("%s",root->data); //输出操作符
BtreeToExp(root->right,deep+1);
if(deep>1) printf(")"); //若有子表达式则加1层括号
}
}
(4.3) 树和二叉树
4.3.1 编程以孩子兄弟表示法存储的森林的叶子结点数
/*
当森林(树)以孩子兄弟表示法存储时,若结点没有孩(fch=nul1),则它必是叶子,总的叶子结点个数是孩子子树(fch)上的叶子数和兄弟子树(nsib)上的叶结点个数之和。
*/
typedef struct node
{
int data; //数据域
int node *fch,*nsib;//孩子与兄弟域
}*Tree;
int Leaves(Tree t){ //计算以孩子兄弟表示法存储的森林的叶子数
if(t==NULL)
return 0; //树空返回0
if(t->fch==NULL) //若结点无孩子,则该结点必是叶子
return 1+Leaves(t->nsib);//返回叶子结点和其兄弟子树中的叶子结点数
else //孩子子树和兄弟子树中叶子数之和
return Leaves(t->fch)+Leaves(t->nsib);
}
4.3.2 以孩子兄弟链表为存储结构,请设计递归算法求树的深度
/*
由孩子兄弟链表表示的树,求高度的算法思想如下:采用递归算法,若树为空,高度为零;否则,高度为第一子女树高度加1和兄弟子树高度的大者。其非递归算法使用队列,逐层遍历树,取得树的高度。
*/
int Height(CSTree bt){
//递归求以孩子兄弟链表表示的树的深度
int hc,hs;
if(bt==NULL)
return 0;
else{ //否则,高度取子女高度+1和兄弟子树高度的大者
hc = height(bt->firstchild); //第一子女树高
hs = height(bt->nextsibling); //兄弟树高
if(hc+1>hs)
return hc+1;
else
return hs;
}
}
4.3.3 已知一棵树的层次序列及每个结点的度,编写算法构造此时的孩子-兄弟链接
/*
本题与树的层次序列有关。可设立一个辅助数组pointer[]存储新建树的各结点的地址,再根据层次序列与每个结点的度,逐个链接结点。
*/
#define maxNodes 15
void createcSTree_Degree(Csfree&T,int e[],int degree[],int n){
//根据树结点的层次序列e[]和各结点的度degree[]构造树的孩子-兄弟链表
//参数n是树结点个数
CSNode *pointer = new CSNode[maxNodes];//判断pointer[i]为空的语句未写
int i,j,d,k=0;
for(i=0;i<n;i++){ //初始化
pointer[i]=new csNode;//判断pointer[i]为空的语句未写
pointer[i]->data=e[i];
pointer[i]->lchild=pointer[i]->rsibling=NULL;
}
for(i=0;i<n;i++){
d=degree[i];//结点i的度数
if(d){
k++;//k为子女结点序号
pointer[i]->1child=pointer[k];//建立i与子女k间的链接
for(j=2;j<=d;j++){
k++;
pointer[k-1]->rsibling = pointer[k];
}
}
T = pointer[0];
delete [] pointer;
}
(4.4) 树和二叉树的应用
4.4.1 判断给定的二叉树是否是二叉排序树
/*
对二叉排序树来说,其中序遍历序列为一个递增有序序列。因此,对给定的二叉树进行中序遍历,若始终能保持前一个值比后一个值小,则说明该二又树是一棵二又排序树。
*/
int predt=-32767;//predt为全局变量,保存当前结点中序前驱的值,初值为-无穷。
int JudgeBST(BiTree bt){
int b1,b2;
if(bt==NULL)//空树
return 1;
else{
b1=JudgeBST(bt->1child);//判断左子树是否是二又排序树
if(b1==0 || predt>=bt->data)//以若左子树返回值为0或前驱大于等于当前结点
return 0; //不是二叉排序树
predt=bt->data;//保存当前结点的关键字
b2=JudgeBST(bt->rchild);//判断右子树
return b2; //返回右子树的结果
}
}
4.4.2 设计一个算法,求出指定结点在给定二叉排序树中的层次
/*
算法思想:设二又树采用二又链表存储结构。在二叉排序树中,查找一次就下降一层。因此,查找该结点所用的次数就是该结点在二又排序树中的层次。采用二叉排序树非递归查找算法,用n保存查找层次,每查找一次,n就加1,直到找到相应的结点。
*/
int level(BiTree bt,BSTNode *p){
//本算法计算给定结点在二叉排序树中的层次
int n=0;//统计查找次数
BiTree t=bt;
if(bt!=NULL){
n++;
while(t->data!=p->data){
if(t->data < p->data)//在左子树中查找
t = t->lchild;
else //在右子树中查找
t = t->rchild;
n++;//层次加1
}
}
return n;
}
4.4.2 利用二叉树遍历的思想编写一个判断二叉树是否平衡二叉树的算法
/*
设置二叉树的平衡标记balance,标记返回二又树bt是否为平衡二叉树,若为平衡二叉树,
则返回1,否则返回0:h为二又树bt的高度。采用后序遍历的递归算法:
1)若bt为空,则高度为0,balance=1。
2)若bt仅有根结点,则高度为1,balance=1。
3)否则,对bt的左、右子树执行递归运算,返回左、右子树的高度和平衡标记,bt的高度
为最高子树的高度加1。若左、右子树的高度差大于1,则balance=0;若左、右子树的
高度差小于等于1,且左、右子树都平衡时,balance=1,否则balance=0。
*/
void Judge AVL(BiTree bt,int &balance,int sh){
//本算法判断一个给定的二叉树是否为平衡二叉树
int b1=0,br=0,hl=0,hr=0;//左、右子树的平衡标记和高度
if(bt==NULL){//空树,高度为0
h=0;
balance=1;
else if(bt->1child==NULL&6bt->xchild==NULL){//仅有根结点,则高度为1
h=1;
balance=1;
}
Judge _AVL(bt->1child,bl,h1);//递归判断左子树
Judge_AVL(bts>rchild,br,hr);//递归判断右子树
h=(h1>hr?hl:hr)+1;
if(abs(hl-hr)<2)//若子树高度差的绝对值<2,则看左、右子树是否都平衡
balance=bl&&br;//66为逻辑与,即左、右子树都平衡时,二叉树平衡
else
balance=0;
4.4.3 设计一个算法,求出给定二又排序树中最小和最大的关键字。
/*
在一棵二又排序树中,最左下结点即为关键字最小的结点,最右下结点即为关键字最大的结点,本算法只要找出这两个结点即可,而不需要比较关键字。
*/
int MinKey(BSTNode *bt){
//求出二叉排序树中最小关键字结点
while(bt->lchild != NULL)
bt=bt->lchild;
return bt->data;
}
int MaxKey(BSTNode *bt){
//求出二叉排序树中最大关键字结点
while(bt->rchild != NULL)
bt = bt->rchild;
return bt->data;
}
4.4.4 设计一个算法,从小到大输出二叉排序树中所有值小于 k 的关键字
/*
由二叉排序树的性质可知,右子树中所有的结点值均大于根结点值,左子树中所有的结点值均小于根结点值。为了从大到小输出,先遍历右子树,再访问根结点,后遍历左子树。
*/
void OutPut(BSTNode *bt, int k)
{
//本算法从大到小输出二叉排序树中所有值不小于k的关键字
if(bt==NULL)
return;
if(bt->rchild != NULL)
OutPut(bt->rchild,k); //递归输出右子树结点
if(bt->data >= k)
printf("%d",bt->data); //只输出大于等于k的结点值
if(bt->lchild !=NULL)
OutPut(bt->lchild,k);//递归输出左子树的结点
}
4.4.5 编写一个递归算法,在一棵有n个结点的、随机建立起来的二又排序树上查找第k (1<k<n) 小的元素,并返回指向该结点的指针。要求算法的平均时间复杂度为O(log2n),二又排序树的每个结点中除data、lchild、rchild等数据成员外,增加一个count成员,保存以该结点为根的子树上的结点个数。
/*
在以t为根的子树上寻找第k小的元素,返回其所在结点的指针。k从11开始计算,在树结点中增加一个count数据成员,存储以该结点为根的子树的结点个数。
最大查找长度取决于树的高度。由于二叉排序树是随机生成的,其高度应是O(log2n),时间复杂度为O(log2n)。
*/
BSTNode *Search_Small(BSTNode*t,int k){
if(k<1 || k>t->count) return NULL;
if(t->lchild==NULL){
if(k==1) return;
else return Search_Small(t->rchild,k-1);
}
else{
if(t->lchild->count == k-1)
return t;
if(t->lchild->count > k-1)
return Search_Small(t->lchild,k);
if(t->lchild->count < k-1)
return Search_Small(t->rchild,k-(t->lchild->count+1));
}
五、图
(5.1) 图的存储结构和基本操作
5.1.1 写出从图的邻接表表示转换成邻接矩阵表示的算法
/*
算法的基本思想:设图的顶点分别存储在数组v[n]中。首先初始化邻接矩阵。遍历邻接表,在依次遍历顶点v[i]的边链表时,修改邻接矩阵的第i行的元素值。若链表边结点的值为,则置arcs[i][j]=1。遍历完邻接表时,整个转换过程结束。此算法对于无向图、有向图均适用。
*/
void Convert(ALGraph &G,int arcs[M][N]){
//此算法将邻接表方式表示的图G转换为邻接矩阵arcs
for(i=0; i<n; i++){ //依次遍历各顶点表结点为头的边链表
p =(G->v[i]).firstarc; //取出顶点i的第一条出边
while(p!=NULL){ //遍历边链表
arcs[i][p->data]=1;
p=p->nextarc; //取下一条出边
}
}
}
(5.2) 图的遍历
5.2.1 写出从图的邻接表表示转换成邻接矩阵表示的算法
/*
算法的基本思想:设图的顶点分别存储在数组v[n]中。首先初始化邻接矩阵。遍历邻接表,在依次遍历顶点v[i]的边链表时,修改邻接矩阵的第i行的元素值。若链表边结点的值为,则置arcs[i][j]=1。遍历完邻接表时,整个转换过程结束。此算法对于无向图、有向图均适用。
*/
void Convert(ALGraph &G,int arcs[M][N]){
//此算法将邻接表方式表示的图G转换为邻接矩阵arcs
for(i=0; i<n; i++){ //依次遍历各顶点表结点为头的边链表
p =(G->v[i]).firstarc; //取出顶点i的第一条出边
while(p!=NULL){ //遍历边链表
arcs[i][p->data]=1;
p=p->nextarc; //取下一条出边
}
}
}
(5.2) 图的存储结构和基本操作
5.2.1 试设计一个算法,判断一个无向图G是否为一棵树。若是一棵树,则算法返回true,否则返回false。
/*
一个无向图G是一棵树的条件是,G必须是无回路的连通图或有n-1条边的连通图。这里采用后者作为判断条件。对连通的判定,可用能否遍历全部顶点来实现。可以采用深度优先搜索算法在遍历图的过程中统计可能访问到的顶点个数和边的条数,若一次遍历就能访问到n个顶点和n-1条边,则可断定此图是一棵树。
*/
bool isTree(Graph& G){
for(i=1;i<=G.vexnum;i++)
visited[i]=FALSE; //访问标记visited[]初始化
int Vnum=0,Enum=0; //记录顶点数和边数
DFS(G,1,Vnum,Enum,visited);
if(Vnum==G.vexnum && Enum==2*(G.vexnum-1))
return true; //符合树的条件
else
return false; //不符合树的条件
void DFS(Graph& G,int v,int& Vnum,int& Enum,int visited[]){
//深度优先遍历图G,统计访问过的顶点数和边数,通过Vnum和Enum返回
visited[v]=TRUE;
Vnum++; //作访问标记,顶点计数
int w=FirstNeighbor(G,v);//取v的第一个邻接顶点
while(w !=-1){ //当邻接顶点存在
Enum++; //边存在,边计数
if(!visited[w]) //当该邻接顶点未访问过
DFS(G,w,Vnum,Enum,visited);
w=NextNeighbor(G,v,w);
}
}
5.2.2 写出图的深度优先搜索DFS算法的非递归算法(图采用邻接表形式)。 3
/*
在深度优先搜索的非递归算法中使用了一个栈s来记忆下一步可能访问的顶点,同时使用了一个访问标记数组visited[i]来记忆第i个顶点是否在栈内或曾经在栈内,若是则它以后不能再进栈。图采用邻接表形式。
注意:由于使用了栈,使得遍历的方式从右端到左端进行,不同于常规的从左端到右端,但仍然是深度优先遍历。
*/
void DFS_Non_RC(AGraph& G,int v){
//从顶点v开始进行深度优先搜索,一次遍历一个连通分量的所有顶点
int w; // 顶点序号
InitStack(S); // 初始化栈s
for(i=0; i<G.vexnum; i++)
visited[i]=FALSE; //初始化visited
Push(S,v); visited[v]=TRUE; // v入栈并置Qisited[v]
while(!IsEmpty(S)){
k=Pop(S); // 栈中退出一个顶点
visit(k); // 先饰问,再将其子结点入栈
for(w=FirstNeighbor(G,k); w>=0; w=NextNeighor(G,k,w))
// k所有邻接点
if(!visited[w]){ // 未进过栈的顶点进栈
Push(S,w);
visited[w]=true; // 作标记,以免再次入栈
}//if
}//while
}
5.2.3 分别采用基于深度优先遍历和广度优先遍历算法判别以邻接表方式存储的有向图中是否存在由顶点 vi 到顶点 vj 的路径 (i≠j) 。注意,算法中涉及的图的基本操作必须在此存储结构上实现。
/*
两个不同的遍历算法都采用从顶点v,出发,依次遍历图中每个顶点,直到搜索到顶点vj,若能够搜索到vj,则说明存在由顶点vi到顶点j的路径。
*/
//深度优先遍历算法的实现如下
int visited[MAXSIZE]={0}; //访问标记数组
int Exist_Path_DFS(ALGraph G,int i,int j){
int p; //顶点序号
if(i==j)
return 1; //i就是j
else{
visited[i]=1; //置访问标记
for (p=FirstNeighbor(G,i);p>=0;p=NextNeighbor(G,i,p)){
k = p.adjvex;
if(!visited[p] && Exist_Path_DFS(G,p,j))
return 1;
}//for
}//else
return 0;
}
//广度优先遍历算法的实现如下
int visited[MAXSI2E]={0}; //访问标记数组
int Exist_Path_BFS(ALGraph G,int i,int j){
//广度优先判断有同图G中顶点vi到顶点vj是否有路径,是则返回1,否则返回0
InitQueue(Q);
EnQueue(Q,i); //顶点i入队
while(!isEmpty(Q)){ //非空循环
DeQueue(Q,u); //队头顶点出队
visited[u]=1; //置访问标记
for(p=FirstNeighbor(G,i);p;p=NextNeighbor(G,i,p)){
//检查所有邻接点
k=p.adjvex;
if(k==j) //若k==j,则查找成功
return 1;
if(!visited[k]) //否则,顶点k入队
EnQueue(Q,k);
}//for
}//while
return 0;
}
5.2.4 假设图用邻接表表示,设计一个算法,输出从顶点 Vi 到顶点Vj 的所有简单路径。
/*
本题采用基于递归的深度优先遍历算法,从结点u出发,递归深度优先遍历图中结点,若访问到结点v,则输出该搜索路径上的结点。为此,设置一个path数组来存放路径上的结点(初始为空),d表示路径长度(初始为-1)。
*/
void FindPath(AGraph *G,int u,int v,int path[],int d){
int w,i;
ArcNode *p;
d++; //路径长度增1
path[d]=u; //将当前顶点添加到路径中
visited[u]=1; //置已访问标记
if(u==V) //找到一条路径则输出
print(path[]); //输出路径上的结点
p=G->adjlist[u].firstarc;//p指向v的第一个相邻点
while(p!=NULL){
w=p->adjvex; //若顶点w未访问,递归访问它
if(visited[w]==0)
FindPath(G,w,V,path,d);
p = p->nextarc; //p指向v的下一个相邻点
}
visited[u] = 0; //恢复环境,使该顶点可重新使用
}
六、查找
(6.1) 顺序查找和折半查找
6.1.1 写出折半查找的递归算法。初始调用时,low为1,high为 ST.length。
/*
算法的基本思想:根据查找的起始位置和终止位置,将查找序列一分为二,判断所查找的关键字在哪一部分,然后用新的序列的起始位置和终止位置递归求解。
算法把规模为n的复杂问题经过多次递归调用转化为规模减半的子问题求解。时间复杂度为O(log2n),算法中用到了一个递归工作栈,其规模与递归深度有关,也是O(log2n)。
*/
typedef struct{ //查找表的数据结构
int *elem; //存储空间基址,建表时按实际长度分配,0号留空
int length; //表的长度
}SSTable;
int BinSearchRec(SSTable ST,int key,int low,int high){
//在有序表中递归折半查找其关键字为key的元素,返回其在表中序号
if(low>high)
return 0;
mid=(low+high)/2; //取中间位置
if(key>ST.elem[mid]) //向后半部分查找
Search(ST,key,mid+1,high);
else if(key<ST.elem[mid]) //向前半部分查找
Search(ST,key,low,mid-1);
else //查找成功
return mid;
}
6.1.2 线性表中各结点的检索概率不等时,可用如下策略提高顺序检索的效率:若找到指定的结点,则将该结点和其前驱结点(若存在)交换,使得经常被检索的结点尽量位于表的前端。试设计在顺序结构和链式结构的线性表上实现上述策略的顺序检索算法。
/*
算法的基本思想:检索时可先从表头开始向后顺序扫描,若找到指定的结点,则将该结点和其前趋结点(若存在)交换。采用顺序表存储结构的算法实现如下:
*/
int SeqSrch(int R[],int k){
//顺序查找线性表,找到后和其前面的元素交换
int i=0;
while((R[i].key!=k) &&(i<n))
i++; //从前向后顺序查找指定结点
if(i<n && i>0){ //若找到,则交换
temp=R[i];
R[i]=R[i-1];
R[i-1]=temp;
return --i; //交换成功,返回交换后的位置
else
return -1; //交换失败
}
七、排序
(7.1) 快速排序
7.1.1 试重新编写考点精析中的快速排序的划分算法,使之每次选取的枢轴值都是随机地从当前子表中选择的。
/*
这类题目比较简单,为方便起见,可直接先随机地求出枢轴的下标,然后将枢轴值与A[low]交换,而后的思想就与前面的划分算法一样。
*/
int Partition2(int A[],int low,int high){
int rand_Index=low+rand() %(high-low+1);
Swap(A[rand Index],A[low]);//将枢轴值交换到第一个元素
int pivot=A[low]; //置当前表中的第一个元素为枢轴值
int i=low; //使得表A[low…i]中的所有元素小于pivot,初始为空表
for(int j=low+1;j<=high;j++)//从第毫个元素开始寻找小于基准的元素
if(A[j]<pivot) //找到后,交换到前面
swap(A[++i],A[j]);
swap(A[i],A[low]); //将基准元素插入到最终位置
return i; //返回基准元素的位置
}
7.1.2 试编写一个算法,使之能够在数组L[1..n]中找出第 k 小的元素(即从小到大排序后处于第k个位置的元素)。
/*
本题最直接的做法是用排序算法对数组先进行从小到大的排序,然后直接提取L(k)便得到了第k小元素,但其平均时间复杂度将达O(nlog2n)以上。此外,还可采用小顶堆的方法,每次堆顶元素都是最小值元素,时间复杂度为O(n+klog2n)。下面介绍一个更精彩的算法,它基于快速排序的划分操作。
*/
int kth_elem(int a[],int low,int high,ing k)
{
int pivot = a[low];
int low temp=low;//由于下面会修改low与high,在递归时又要用到它们
int high_temp=high;
while(low<high){
while(low<high && a[high]>=pivot)
--high;
a[low] =a[high];
while(low<high && a[low]<=pivot)
++low;
a[high]=a[low];
}
a[low]=pivot;
//上面即为快速排序中的划分算法
//以下就是本算法思想中所述的内容
if(low==k) //由于与k相同,直接返回pivot元素
return a[low];
else if(low>k) //在前一部分表中递归寻找
return kth_elem(a,low_temp,low-1,k);
else //在后一部分表中递归寻找
return kth_elem(a,low+1,high_temp,k-low);
}
7.1.3 【2016统考真题】已知由n(n>2)个正整数构成的集合A={ak l 0<=k<n},将其划分为两个不相交的子集A1和A2,元素个数分别是n1和n2,A1和A₂中的元素之和分别为S1和S₂。设计一个尽可能高效的划分算法,满足|n1-n2l最小且|S1-S2|最大。要求:6
1)给出算法的基本设计思想。
2)根据设计思想,采用C或C++语言描述算法,关键之处给出注释。
3)说明你所设计算法的平均时间复杂度和空间复杂度。6
/*
1)算法的基本设计思想
由题意知,将最小的L[n/2]个元素放在A1中,其余的元素放在A2中,分组结果即可满足题目要求。仿照快速排序的思想,基于枢轴将n个整数划分为两个子集。根据划分后枢轴所处的位置i分别处理:
① 若i = L[n/2],则分组完成,算法结束。
② 若i < L[n/2],则枢轴及之前的所有元素均属于A1,继续对i之后的元素进行划分。
③ 若i > L[n/2],则枢轴及之后的所有元素均属于A2,继续对i之前的元素进行划分。基于该设计思想实现的算法,无须对全部元素进行全排序。其平均时间复杂度是O(n),空间复杂度是O(1)。
*/
int setPartition(int a[],int n){
int pivotkey,low=0,low0=0,high=n-1,high0=n-1,flag=1,k=n/2,i;
int s1=0,s2=0;
while(flag){
piovtkey = a[low]; //选择枢轴
while(low<high){ //基于枢轴对数据进行划分
while(low<high && a[high]>=pivotkey)-high;
if(low!=high) a[low]=a[high];
while(low<high && a[low]<=pivotkey)
++low;
if(low!=high)
a[high]=a[low];
}
a[low]=pivotkey;
if(low==k-1)//若枢轴是第n/2小元素,划分成功
flag=0;
else{ //是否继续划分
if(low<k-1){
low0 = ++low;
high = high0;
}
else{
high0 = --high;
low=low0;
}
}
}
for(i=0;i<k;i++) s1 += a[i];
for(i=k;i<n;i++) s2 += a[i];
return s2-s1;
}
7.1.4 荷兰国旗问题:设有一个仅由红、白、蓝三种颜色的条块组成的条块序列,请编写一个时间复杂度为O(n)的算法,使得这些条块按红、白、蓝的顺序排好,即排成荷兰国旗图案。
/*
算法思想:顺序扫描线性表,将红色条块交换到线性表的最前面,蓝色条块交换到线性表的最后面。为此,设立三个指针,其中,j为工作指针,表示当前扫描的元素,i以前的元素全部为红色,k以后的元素全部为蓝色。根据所指示元素的颜色,决定将其交换到序列的前部或尾部。
初始时i=0,k=n-1,算法的实现如下。
*/
typedef enum(RED,WHITE,BLUE} color; //设置枚举数组
void Flag_Arrange(color a[],int n){
int i=0,j=0,k=n-1;
while(j<=k)
switch(a[j]){ //判断条块的颜色
case RED:Swap(a[i],a[j]);i++; j++; break;
//红色,则和i交换
case WHITE:j++;break;
case BLUE:Swap(a[j],a[k]); k--;
//蓝色,则和k交换
//这里没有j++语句以防止交换后a[j]仍为蓝色的情况
}
}
(7.2) 冒泡排序
7.2.1 编写双向冒泡排序算法,在正反两个方向交替进行扫描,即第一趟把关键字最大的元素放在序列的最后面,第二趟把关键字最小的元素放在序列的最前面,如此反复进行。
/*
这种排序方法又称双向起泡。奇数趟时,从前向后比较相邻元素的关键字,遇到逆序即交换,直到把序列中关键字最大的元素移动到序列尾部偶数趟时,从后往前比较相邻元素的关键字,遇到逆序即交换,直到把序列中关键字最小的元素移动到序列前端。程序代码如下:
*/
void BubbleSort(int A,int n){
//双向起泡排序,交替进行正反两个方向的起泡过程
int low = 0,high = n-1;
bool flag = true;//一趟冒泡后记录元素是否交换标志
while(low<high && flag){//循环跳出条件,当flag为false 说明已没有逆序
flag=false;//每趟初始置flag为false
for(i=low;i<high;i++)//从前向后起泡
if(a[i]>a[i+1]){ //发生逆序
swap(a[i],a[i+1]);//交换
flag=true; //置flag
}
high--; //更新上界
for(i=high;i>low;i--)//从后往前起泡
if(a[i]<a[i-1]){ //发生逆序
swap(a[i],a[i-1]); //交换
flag=true; //置flag
}
low++; //修改下界
}
7.2.2已知线性表按顺序存储,且每个元素都是不相同的整数型元素,设计把所有奇数移动到所有偶数前边的算法(要求时间最少,辅助空间最少)。
/*
本题可采用基于快速排序的划分思想来设计算法,只需遍历一次即可,其时间复杂度为O(n),空间复杂度为O(1)。假设表为L[1..n],基本思想是:先从前向后找到一个偶数元素L(i),再从后向前找到一个奇数元素L(j),将二者交换;重复上述过程直到i大于。
*/
void move(int A[],int len){
//对表A按奇偶进行一趟划分
int i=0,j=len-1;//i表示左端偶数元素的下标;j表示右端奇数元素的下标
while(i<j){
while(i<j && A[i]%2!=0) i++;//从前向后找到一个偶数元素
while(i<j && A[j]%2!=1) j--;//从后向前找到一个奇数元素
if(i<j){
Swap(A[i],A[j]);//交换这两个元素.
i++;
j--;
}
}
}
(7.3) 简单选择排序
7.3.1 编写一个算法,在基于单链表表示的待排序关键字序列上进行简单选择排序。
/*
算法的思想是:每趟在原始链表中摘下关键字最大的结点,把它插入到结果链表的最前端。
由于在原始链表中摘下的关键字越来越小,在结果链表前端插入的关键字也越来越小,因此最后形成的结果链表中的结点将按关键字非递减的顺序有序链接。
假设单链表不带表头结点。
*/
void selectSort(LinkedList& L) {
//对不带表头结点的单链表工执行简单选择排序
LinkNode *h=L,*p,*q,*r,*s;
L=NULL;
while(h!=NULL){ //持续扫描原链表
p = s = h;
q = r = NULL;
//指针s和r记忆最大结点和其前驱;p为工作指针,q为其前驱
while(p !=NULL){ //扫描原链表寻找最大结点s
if(p->data>s->data){
s = p;
r = q;
} //找到更大的,记忆它和它的前驱
q = p;
p = p->1ink; //继续寻找
}
if(s==h)
h = h->1ink; //最大结点在原链表前端
else
r->1ink = s->1ink; //最大结点在原链表表内
s->1ink=L;
L=s; //结点s插入到结果链前端
}
}
(7.4) 堆排序
7.4.1 试设计一个算法,判断一个数据序列是否构成一个小根堆。
/*
将顺序表L[1..n]视为一个完全二叉树,扫描所有分支结点,遇到孩子结点的关键字小于根结点的关键字时返回false,扫描完后返回true。
*/
bool IsMinHeap(int A[],int len){
if(len%2 == 0){ //len为偶数,有一个单分支结点
if(A[len/2]>A[len]) //判断单分支结点
return false;
for(i=len/2-1; i>=1; i--) //判断所有双分支结点
if(A[i]>A[2*i] || A[i]>A[2*i+1])
return false;
else{ //1en为奇数时,没有单分支结点
for(i=len/2; i>=1; i--) //判断所有双分支结点
if(A[i]>A[2*i] || A[i]>A[2*i+1])
return false;
}
return true;
}
(7.5) 插入排序
7.5.1 设顺序表用数组A[]表示,表中元素存储在数组下标1~m+n的范围内,前m个元素递增有序,后n个元素递增有序,设计一个算法,使得整个顺序表有序。
/*
(1) 给出算法的基本设计思想
(2) 根据设计思想,采用C/C++描述算法,关键之处给出注释。
(3) 说明你所设计算法的时间复杂度与空间复杂度。
*/
/*
算法的基本设计思想如下:将数组A[1..m+n]视为一个已经过m趟插入排序的表,则从m+l趟开始,将后n个元素依次插入前面的有序表中。
时间复杂度由m和n共同决定,从上面的算法不难看出,在最坏情况下元素的比较次数为O(mn),而元素移动的次数为O(mn),所以时间复杂度为O(mn),由于算法只用到了常数个辅助空间,所以空间复杂度为O(1)。
*/
void Insert_Sort(int A[],int m,int n){
int i,j;
for(i = m+1;i<=m+n;i++){ //依次将A[m+1.m+n]插入有序表
A[0]=A[i]; // 复制为哨兵
for(j=i-1;A[j]>A[0];j--) // 从后往前插入
A[j+1] = A[j]; // 元素后移
A[j+1] = A[0]; // 插入
}
}