![9b1cbdd43ba91868fece649c2acf0dc3.png](https://i-blog.csdnimg.cn/blog_migrate/7cab5dd10beb1ac4a9455d8215436e91.jpeg)
首先祝各位看官五一快乐啊!临时起意想对数据结构做的一些练习(当然是新手向的)进行总结,于是这篇文章就这么产生了。
1. 在链接存储结构下,将线性表,以 m 位置为分界点的前后两部分元素换位成
。
注:这里的线性表下标从1开始
这题是相对简单的一道题,思路也很容易想到:我们先找到要换位的节点,分别是
![equation?tex=m](https://i-blog.csdnimg.cn/blog_migrate/f79ab5c40ded11a5ecf9c3162bbd89d5.png)
/* 链表定义如下:
typedef struct node{
int data;
struct node *link;
}NODE;
*/
void transposition(NODE **hd, int m) // hd 链表指针的地址,m 为分界点。
{
int i=0;
NODE *p=*hd,*q;
for(i=1;i<m;i++)
{
p=p->link;
}//找到m结点前一个的位置
q=p;
while(q->link!=NULL)
{
q=q->link;
}//找到最后一个结点的位置
q->link=*hd;
*hd=p->link;
p->link=NULL;//结点换位
}
其中需要注意的是,由于我们需要改变头指针的值,所以传入函数的是头指针的地址,这样在函数内改变头指针的值就能传出函数外。
有人可能就认为:就这?这也太简单了吧?那行我们加点难度
1改. 在链接存储结构下,将链表,以倒数第
个位置为分界点的前后两部分元素换位成
。
虽然加大了点难度,但是想想还是能解决问题的。如果我们把链表遍历一遍,求出链表的长度,然后再用1中的方法不就解决问题了吗?具体代码请读者自行完成(逃
要求当然不会这么简单(众:你想怎样?),由于原题是用一次遍历就把问题解决了,所以这里我们也作同样的要求:
1改':在1改的条件下,用一次遍历解决问题。
经过了漫长的思考,你可能想出了这样的解法:首先我们先遍历链表,将每个链表结点存储到一个数组中,这样我们可以直接读出链表的长度,从而能直接得到倒数第
![equation?tex=m](https://i-blog.csdnimg.cn/blog_migrate/f79ab5c40ded11a5ecf9c3162bbd89d5.png)
![equation?tex=O%28n%29](https://i-blog.csdnimg.cn/blog_migrate/b1e1fe9d316f093d33735a5259d8e5e5.png)
![equation?tex=O%281%29](https://i-blog.csdnimg.cn/blog_migrate/89d5b096d58fa794c11b5f410c6a3378.png)
有同学就问了:我都想到这一步了,不会还有新要求吧?你说对了,的确会有新要求,由于原来算法的空间复杂度是
![equation?tex=O%281%29](https://i-blog.csdnimg.cn/blog_migrate/89d5b096d58fa794c11b5f410c6a3378.png)
1改'':在1改'的条件下,要求空间复杂度为![]()
终于来到了本题的重头戏,即使给了这么多稀奇古怪的要求,解法还是存在的。我们可以用小学二年级就学过的双指针解法来解决这道题,我们先定义两个指针都为头结点,分别记为快慢指针,然后让快指针先走
![equation?tex=m](https://i-blog.csdnimg.cn/blog_migrate/f79ab5c40ded11a5ecf9c3162bbd89d5.png)
![equation?tex=m](https://i-blog.csdnimg.cn/blog_migrate/f79ab5c40ded11a5ecf9c3162bbd89d5.png)
//链表定义同上
void transposition(NODE **hd, int m) // hd 链表指针的地址,m 为分界点。
int i=0;
NODE *fast=*hd,*slow=*hd;
for(i=1;i<=m;i++)
{
fast=fast->link;
}
while(fast->link!=NULL)
{
fast=fast->link;
slow=slow->link;
}
fast->link=*hd;
*hd=slow->link;
slow->link=NULL;
}
2.检验一个表达式中括号是否匹配,如:[([]())]是正确的,[(])或(()]是不正确的
这题也是个经典的栈问题了。首先先解决一个:为什么需要用栈?我们需要知道一个事实:最里层的右括号只和离它最近的左括号匹配。(众:这不废话吗?)且听我说完,这句话是不是说明了,如果我们用一个数组存放所有的左括号,那么最先遇到的右括号不就是和最近进入数组的左括号匹配吗?也就是说,我们需要一个能够实现后进先出的数组,那不就是栈吗?
接下来我们需要搞清楚(1)什么东西进栈(2)什么时候进栈(3)什么时候出栈。
进栈的元素可以有:字符的值(左括号)/字符的位置等等。这里我们方便起见,直接让字符的值进栈,这样就省去了再通过位置读取字符。
接下来回答第二个问题:什么时候进栈?由于我们要让左括号和右括号匹配,当我们遇到左括号时,就把左括号的值存入栈中。
那么什么时候出栈?一个比较直接的想法是当右括号和左括号匹配的时候,让左括号出栈。那如果不匹配呢?不匹配就直接return false了嘛。
代码如下:
int Bracket_match (char *s) /* 检验字符串 s 中的括号是否匹配。
如果匹配返回1, 否则返回0; */
{
char stack[100], c; // 定义堆栈
int i=0, top=0; // 初始化堆栈及字符位置
if(strlen(s)==0) return 1;
c=s[i]; // 取一字符为当前处理字符
while (c)
{
switch (c)
{
case '(': stack[top++]=c;break; // 为'('
case ')':
if(top!=0 && stack[top-1]=='(')
{
top--;
break;
}
else return 0;
case '[': stack[top++]=c;break; // 为'['
case ']':
if(top!=0 && stack[top-1]=='[')
{
top--;
break;
}
else return 0;
case '{': stack[top++]=c;break; // 为'{'
case '}':
if(top!=0 && stack[top-1]=='{')
{
top--;
break;
}
else return 0;
}
c=s[++i];
}
if (top==0) return 1;// 匹配成功, 返回 1
else return (0); // 匹配失败, 返回 0
}
值得注意的是,我们在遍历字符串的时候,可能会出现左括号和右括号的数目不一样多的情况,如果左括号比右括号多,那么有可能出现这样的情况:(1)遇到右括号但是栈空(2)在执行完程序后栈非空。在遇到这两种情况就需要特殊处理了。当然还有一种特殊情况:传进来的会不会是个空字符串?(众:还有这种操作?)当然本题输入数据没有这样的,但为了代码的鲁棒性考虑,在遍历数组之前我们最好对空串进行特殊处理,直接返回真值即可。
3. 在一个串中搜索所有匹配的子串的位置,包括重叠和不重叠两种情况。如:主串bcaabbaabdaabbaabbaabc和子串aabbaab,则在主串中可以找到1个不重叠和两个重叠的匹配子串,其位置分别为3、11和15。要求使用API:模式匹配函数 S_index(char * t, char*p),其中 t 是主串,p 是模式子串,返回值为模式子串在 t 中的位置,若匹配失败则返回0
注:与第一题的链表相同,这里的函数内输出位置和 S_index( ) 的输出位置都是从1开始
有人可能会想了:如果我把字符串的每个元素都匹配一遍,那是不是就是结果?当然不是了!按照这样的思路做下去,很可能会重复输出元素的。以题目给的例子而言,前三个输出分别会是3、3、3,这并不符合我们的要求。那有人就想了:如果我把已经匹配过的位置用一个数组存放起来,在下次匹配的时候,如果出现的是新值,就输出,如果不是,就不输出任何数。这固然是一种方法,但是还不够精炼(众:终于不是请读者自行完成代码了)原因在于,我们重复匹配了一些元素。
既然这样我们考虑这么一种想法:当我们通过 S_index( ) 得到一个位置(记为loc)的时候,那么下一个匹配的位置是不是就在这个位置之后?我们简单地证明一下:如果这个位置(记为l)在 loc 之前,那么 S_index( l )==loc 矛盾。所以我们以 loc 为起点,对字符串进行模式匹配,直至 S_index( ) 的返回值为0。有了这么个想法,代码也就能想出来了:
void prn_all_index(char *t, char* s) // t 为主串, s 为要搜索子串。 // 函数功能: 输出所有匹配的位置
{
int i=0,j=0;
while(i=S_index(t+j,s))
{
printf("%d ",i+j);
j+=i;
}
printf("nn");
}
虽然主程序代码也就短短三行,但里面还是有一些细节的。首先,为什么可以直接代入 t+j, 首先 t 是个字符型指针,指向着主串的第一个字母,那么一个指针和一个整型数相加意味着什么?意味着 t 指针移动了 j 位,再进一步翻译,就是 &t[j]。 接下来,为什么这个搜索可行?第一次搜索得到的结果是 i 那么根据上面的算法,第二次搜索的起点就应该是 t+i(仔细想想为什么)那么 j 呢?j 记录的是原来那个字符串指针需要移动的总位数。以题目的样例为例,第一次搜索的结果是3,那么第二次搜索由于头指针改变了位置,所以返回的值的7,那么我们下次搜索要从位置10开始,怎么办?用 j 存储移动的位置和就行了。
4. 已经创建按标准形式存储的m次树:
(1) 用非递归方法统计m次树结点总数;
(2) 用递归方法求树的高度;
首先先分析一下问题,不管是(1)问还是(2)问都需要我们对树进行遍历。树的遍历大家都很熟悉了,那么这个题到底是怎么一回事呢?(众:怎么一股营销号的味道)
就(1)问而言,我们需要统计的是 m 次树的结点总数,遍历方法有两种:
第一种方法是以栈为辅助的深度优先搜索(DFS),先访问树的最深层,然后回溯。由于我们可以让所有结点都进栈,所以我们既可以统计进栈次数,也可以统计出栈次数,以获得结点总数,当然这段代码也请读者自行完成啦(诶诶诶,你们拿着刀干嘛,很危险的)。
第二种方法是以队列为辅助的层次优先搜索(BFS),逐层访问树,将结点全部进入队列,那么在非循环队列的情况下,队尾元素在队列内的位置坐标就正好是结点的数量(想想为什么)。
关于树的DFS和BFS的解法这里就不多展开了,具体看看(1)的代码吧:
/*树的结点定义
typedef struct node {
char data;
struct node *child[M]; // M为事先给定
}NODE;
*/
int node_num (NODE *T, int m )
{
int n=0,i=0;
int head=0,tail=1;
NODE *p=T, *queue[100]; //这里设定100是测试数据较小
queue[0]=T;
if(T==NULL)return 0;
while(head<tail)
{
p=queue[head++];
for(i=0;i<m;i++)
{
if(p->child[i]!=NULL)
{
queue[tail++]=p->child[i];
}
else continue;
}
}
n=tail;
return n;
}
可能有同学会问了:如果我用到循环队列不就不能直接读了?虽然不能直接读出来,但是在每次进队的时候可以增加一个判定,用一个变量存储循环的次数。这样结点总数=循环次数*队列长度+队尾位置
这里再作一个拓展:如何用最简洁的代码算出一个二叉树的结点总数。平时都是看着python一行解决问题,这里c语言也能一行吗?接下来就是见证奇迹的时刻:
/* 二叉树的结点定义
typedef struct node{
char data;
struct node *lchild;
struct node *rchild;
}NODE;
*/
int node_num(NODE *T)
{
return T==NULL?0:(1+node_num(T->lchild)+node_num(T->rchild));
}
接下来我们看到(2)问,用递归方法计算树的深度。递归方法的主要特点就是把大问题分解成小问题,然后递归求解小问题,最后得到大问题的答案。按这样考虑,如果我们要求一棵树的最大深度,就相当于求根结点的所有子树的最大深度+1,那么根节点的所有子树的最大深度的怎么求呢?诶,对了,就把子树的根结点当作新的根结点,按照上面的方法再求完后取最大值就得到答案了。
int tree_depth(NODE *t, int m)
{
int i,max=0,temp;
if(t==NULL) return 0;
for(i=0;i<m;i++)
{
if(t->child[i]!=NULL)
{
temp=tree_depth(t->child[i],m);
max=(max>temp?max:temp);
}
else break;
}
return max+1;
}
这段代码有几个细节可以注意一下:首先,为什么使用temp?这是为了减少次数,如果不把递归结果保存下来的话,那下面一句就会多次调用递归,增加无谓的运算时间。再来,为什么当 t->child[i] == NULL 时可以退出循环?注意到我们的 m 次树是标准形式的,也就是说树的结点都存储在数组的前面部分,而数组的后面部分都是空指针,提前退出循环还能节省运算次数,对吧?
5. 已经创建按标准形式存储的二叉树:
(1) 用递归方法交换所有“双分支结点”左右子树的位置。(即把左子树变成的右子树,右子树变成左子树,注意:单分支的不用对调)
(2) 使用队列删除当前树中“所有的叶结点”(非递归方法)。
(1)问的想法是很自然的,给定一个结点,判断是不是双分支结点,如果是,交换左右结点的指针,如果否,访问非空的孩子结点,重复上述步骤。注意这里可能会陷入一个误区:认为如果是单分支结点就不需要访问其孩子结点了。这里有个反例长这样:
![1f0ba42bd9aa1c5e8c7388f41361bb85.png](https://i-blog.csdnimg.cn/blog_migrate/88e0a89e21e921e1fce80391b9b35661.jpeg)
按照上述思路,写代码的时候可能需要将四种情况都考虑,这里提供一个较为简洁(但是会多占用栈空间)的代码以供参考:
/* 二叉树的结点定义
typedef struct node{
char data;
struct node *lchild;
struct node *rchild;
}NODE;
*/
void exchange(NODE *t)
{
NODE *temp;
if(t==NULL)return;
if(t->lchild!=NULL && t->rchild!=NULL)
{
temp=t->lchild;
t->lchild=t->rchild;
t->rchild=temp;
}
exchange(t->lchild);
exchange(t->rchild);
}
这里由于判断孩子结点前有着空树检测,即使孩子结点为空也可以运行。
(2)问的思路和4(1)问有些类似,都是需要遍历结点,然后对结点进行操作,由于题目要求队列解法,这里就简要分析一下思路:我们需要解决的有三个问题(1)谁进队?(2)什么时候进队?(3)什么时候出队?第一个问题我想以大家小学二年级的水平就可以猜出来进队的是结点。
那么什么时候进队呢?进队可以认为是待处理的,也就是说如果孩子结点时叶结点,就直接 free 并置空,反之如果孩子结点不是叶结点,那就进队吧,过会儿再处理它。那么什么时候出队也就很显然了,处理完队首就让他出队好了。代码如下:
void DEL_ALL_lev(NODE *T)
{
NODE *queue[100],*p;
int head=0,tail=1;
if(T==NULL) return;
queue[0]=T;
while(head<tail)
{
p=queue[head++];
if(p->lchild!=NULL)
{
if(p->lchild->lchild==NULL && p->lchild->rchild==NULL)
{
free(p->lchild);
p->lchild=NULL;
}
else queue[tail++]=p->lchild;
}
if(p->rchild!=NULL)
{
if(p->rchild->lchild==NULL && p->rchild->rchild==NULL)
{
free(p->rchild);
p->rchild=NULL;
}
else queue[tail++]=p->rchild;
}
}
}
需要提醒的一点的是,为了节省内存空间,置空的同时记得把指针 free 掉。
感谢您能看到最后!如果对题目有什么疑问或者见解的话,欢迎在评论区留言哦~