西工大NOJ数据结构理论——014.求广义表深度(严5.30)

33 篇文章 8 订阅

第一下拿到这道题,我的脑袋轰一下就大了。如果说用”括号匹配“来做的话,这其实很简单。但是如果要用广义表来做的话,难度哗哗哗的就涨上来了。为什么呢?首先,要把读入的字符串存到广义表里面去,这里就需要第一次用到递归;然后,对已经构造好了的广义表,还需要进行求深度的递归,这就用到了第二次递归。上面这两次递归就已经足够让人头疼的了,更不用说处理输入中的逗号了。

当然,在这片文章的最后会有完整的代码实现,但是我还是更想去和大家分享一下,在这道题中,应该怎么去构建递归函数,当然这是我今天下午摸索出的一套构建的方法,也有了一些自己的思考,总结了一些好用的技巧。下面跟大家分享一下:

老师曾经教过说读代码可以使用”走读“的方式,也就是说不用电脑,而是只在纸上写写画画。同样的道理,我们在写程序中的算法时,也可以采用这种方法。以两个递归函数中的”字符串存到广义表“为例,我们可以先拿出来一个综合性很强的例子,比如说“(a,(b,c)),(c,(d,e)))",然后沉下心来一步一步地画它的广义表,在画的时候,你就会情不自禁的想把你发现的东西给写下来,没错,你的感觉是对的!把你发现的东西写到纸上(比如,你会发现”如果表头是a、b、c之类的字母,那么表头上要新创一个tag为0并且变量值为a、b、c这些字母的结点“)。等到你画完了一个完整的广义表,你也会在纸上写下了很多东西。没错,下一步就是编制程序。把你在纸上写下来的东西转换成为具体的代码(这时我发现最有效的方法),你就会发现写程序明显没有之前那么痛苦了。为什么这很有效呢?因为你要知道,电脑是根据人脑制造出来的,所以当你走到程序进程中的某一个地方时,你对于下一步的下意识想法或者行动,很有可能就是你程序中的那个困扰你的那个步骤。

此外,很多情况下,我们写一个成熟的程序经常需要很长时间,经常是一不小心一个上午就过去了,而且临近中午的时候,脑子几乎就是不动的,效率会慢慢变得越来越低。为了应对这种困境,我发现了一种特别好用的方法,就是把一个问题拆分成为几个小问题,然后分时间段完成这几个小问题,最后就能解决这个大问题。以之前的实验”高精度pi值得求解“为例,我是怎么拆分的呢?看下面:

1.搭建用来存放高精度小数的双链表;

2.完成双链表表示的小数与一个整数的乘法(没有发生进位的那种);

3.完成双链表表示的小数与一个整数的乘法(有发生进位的那种);

4.完成双链表表示的小数与一个整数的除法;

5.构建主函数。

通过这5步,我总共用了4个小时的时间,从无到有得做完了这个实验。有人会说,可其他不这样做的人,他们也是用的这种方法,思路和你一样,而且用的时间也没有比你慢多少,你能有多少优势?对,没错,优势确实很小,但这是一个很有效的思维工具。当面对简单问题的时候,这种分解复杂问题的思维会拖累你的思维,让问题复杂化,做的还不如其它人快,在考试时如果你用这种方法,那你十有八九会做不完卷子,最后拿一个不及格,因为考试题都是极简单不需要过多思考的题,这是它的局限性。但是当问题很复杂,或者问题综合性特别强,抑或者说这是一个你从来没有见过的工程问题时,这种思维工具就会变得特别有用。当别人束手无策不知从何下手时,你已经明确了自己需要完成几个小任务,每个小任务又需要完成什么,而且因为这些小任务往往比较简单,不需要很多的思考,往往都是可以完成的,这也给了你完成任务之后的成就感。这样子成就感给了你积极的反馈,你就会有更大的信心去完成下一个小任务。这样子相比其他人,你就有了更大更多的优势,最后你就是第一。

话题回到这道题的递归上,就我个人来说,咱们初学者没必要也不应该追求简洁的递归实现,也不应该执着于”直接调用“上,相反,”间接调用“是学习递归的一个很好的桥梁,它能够很好地帮你完成程序的编制,最后很快很好的写出行之有效的程序来。

最后,大一作为一个过渡阶段,纸和笔还是有着它无可替代的作用,不要眼高手低,低下头来踏踏实实做事情,在纸上写写画画,往往能提供给你写程序的关键思路。

最后还是真心求赞啦,你们的鼓励是对我创作的最大支持!

#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include<math.h>
typedef enum{ATOM,LIST}ElemTag;
//ATOM==0,原子,LIST==1,子表
typedef char AtomType;
typedef struct _GLNode{
	ElemTag tag;
	union{
		AtomType atom;
		struct{
			struct _GLNode* head;
			struct _GLNode* tail;
		}ptr;
	};
}*GList;

GList InitGList(){//创建空的广义表L
	GList L=(GList)malloc(sizeof(struct _GLNode));
	if(L==NULL){
		printf("Out of space!\n");
		return NULL;
	}else{
		L->ptr.head=NULL;
		L->ptr.tail=NULL;
		return L;
	}
}

void StringDividing(GList L,char* ori_str,char* s_head,char* s_tail);
void Head_Recursion(GList L,char* s_head){
	if(*s_head=='('){
		char sub_s_head[50]={0};
		char sub_s_tail[50]={0};
		StringDividing(L,s_head,sub_s_head,sub_s_tail);
	}
}

void Tail_Recursion(GList L,char* s_tail){
	if(*s_tail!='0'){
		char sub_s_head[50]={0};
		char sub_s_tail[50]={0};
		StringDividing(L,s_tail,sub_s_head,sub_s_tail);
	}
}

void StringDividing(GList L,char* ori_str,char* s_head,char* s_tail){
	char* temp=ori_str+1;
	//将字符串分成“表头字符串”和“表尾字符串”
	//然后用"s_head"存放表头字符串
	//再用"s_tail"存放表尾字符串 
	//下面先处理"s_head": 
	//如果s_head中只有一个字符,那么: 
	if(*temp!='('){
		*s_head=*temp;
		//并且创建一个原子结点 
		GList Atom=InitGList();
		Atom->tag=ATOM;
		Atom->atom=*temp;
		//然后让当前结点的表头指针指向它
		L->ptr.head=Atom;
	}
	//但如果s_head中不止一个字符,就应该: 
	else{
		//首先,确定"s_head"中要放多少字符: 
		int fre=0;
		while(1){
			temp++;
			if((*temp!='(')&&(*temp!=')'));
			else if(*temp=='(') {fre++;}
			else if(*temp==')') {
				if(0!=fre) fre--;
				else break;
			}
		}
		//确定好了之后,就可以往"s_head"中放字符了: 
		char* record=ori_str+1;
		//记录s_head的初始位置,如果不这样做,那么递归传过去的数组指针就有问题
		for(char *p=ori_str+1;p<=temp;){
			*s_head=*p;
			p++;s_head++;
		}
		//最后不要忘记末尾置0 
		*s_head='0';
		//并且新建一个子表结点
		GList list=InitGList();
		list->tag=LIST;
		//然后让当前结点的表头指针指向它 
		L->ptr.head=list;
		//再然后,还需要把当前的"s_head"送去递归分割 
		Head_Recursion(list,record);
		//record记录的是s_head的初始位置 
	}
	//然后再处理"s_tail":
	//分两种情况来讨论:
	//1.如果表尾是空表的话,那么:
	if(*(temp+1)==')'){
		//当前结点的表尾指针置为NULL即可
		L->ptr.tail=NULL; 
	}
	//2.如果表尾不是空表的话,那么: 
	else{
		//因为表尾一定是表,所以还需要用"()",把它再给包起来 
		//先在数组头儿上放"(":
		char* record=s_tail; 
		//记录s_tail的初始位置,如果不这样做,那么递归传过去的数组指针就有问题 
		*s_tail='(';
		s_tail++;
		//然后放中间,因为很容易确定"s_tail"中要放多少字符,所以直接往里面放就可以:
		for(char *p=temp+1;*p!='\0';){
			*s_tail=*p;
			p++;s_tail++;
		}
		//最后需要在数组尾儿上放")",但是因为ori_str最后自带一个")",所以不用多费心 
		*s_tail='0';
		//并且新建一个子表结点
		GList list=InitGList();
		list->tag=LIST;
		//然后让当前结点的表尾指针指向它
		L->ptr.tail=list; 
		//再然后,还需要把当前的"s_tail"送去递归分割 
		Head_Recursion(list,record);
		//record记录的是s_tail的初始位置 
	}
}

int GListDepth(GList L){//递归获取广义表深度
	if(L==NULL) return 1;//空表深度为1
	else if(L->tag==ATOM) return 0;
	int max=0;
	GList pp=L;
	for(;pp!=NULL;pp=pp->ptr.tail){
		int dep;
		dep=GListDepth(pp->ptr.head);
		if(dep>max) max=dep;
	}
	return (max+1);
}

int main(){
	//创建最初的广义表 
	GList L=InitGList();
	L->tag=LIST;
	//从键盘中录入原始字符串 
	char ori[250]={0};
	scanf("%s",ori);
	//对原始字符串进行处理,去除里面的逗号
	char ori_str[250]={0};
	char* p1=ori;char* p2=ori_str;
	while(*p1!='\0'){
		if(*p1==','){
			p1++;
		}else{
			*p2=*p1;
			p1++;p2++;
		}
	}
	char s_head[50]={0};
	char s_tail[50]={0};
	StringDividing(L,ori_str,s_head,s_tail);
	
	int depth=0;
	depth=GListDepth(L);//获取广义表深度 
	printf("%d\n",depth);
	printf("%d\n",depth);
	return 0;
}

 

  • 38
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
哈夫曼编码是一种常用的数据压缩算法,可以将原始数据转换为更短的编码,从而减少存储空间。它的基本思想是:根据字符出现的频率,构建一颗二叉树,使得出现频率高的字符离根节点近,出现频率低的字符离根节点远。然后,对于每个字符,从根节点出发,沿着对应的路径到达该字符所在的叶子节点,记录下路径,作为该字符的编码。 哈夫曼编码的具体实现步骤如下: 1. 统计每个字符在原始数据中出现的频率。 2. 根据字符的频率构建哈夫曼树。构建方法可以采用贪心策略,每次选择出现频率最低的两个字符,将它们作为左右子节点,父节点的权值为两个子节点的权值之和。重复这个过程,直到只剩下一个根节点。 3. 对哈夫曼树进行遍历,记录下每个字符的编码,为了避免编码产生歧义,通常规定左子节点为0,右子节点为1。 4. 将原始数据中的每个字符,用它对应的编码来代替。这一步可以通过哈夫曼树来实现。 5. 将编码后的数据存储起来。此时,由于每个字符的编码长度不同,所以压缩后的数据长度也不同,但总体上来说,压缩效果通常是比较好的。 实现哈夫曼编码的关键在于构建哈夫曼树和计算每个字符的编码。构建哈夫曼树可以采用优先队列来实现,每次从队列中取出两个权值最小的节点,合并成一个节点,再将合并后的节点插入队列中。计算每个字符的编码可以采用递归遍历哈夫曼树的方式,从根节点出发,如果走到了左子节点,则将0添加到编码中,如果走到了右子节点,则将1添加到编码中,直到走到叶子节点为止。 以下是基于C++的代码实现,供参考: ```c++ #include <iostream> #include <queue> #include <string> #include <unordered_map> using namespace std; // 定义哈夫曼树节点的结构体 struct Node { char ch; // 字符 int freq; // 出现频率 Node* left; // 左子节点 Node* right; // 右子节点 Node(char c, int f) : ch(c), freq(f), left(nullptr), right(nullptr) {} }; // 定义哈夫曼树节点的比较函数,用于优先队列的排序 struct cmp { bool operator() (Node* a, Node* b) { return a->freq > b->freq; } }; // 构建哈夫曼树的函数 Node* buildHuffmanTree(unordered_map<char, int> freq) { priority_queue<Node*, vector<Node*>, cmp> pq; for (auto p : freq) { pq.push(new Node(p.first, p.second)); } while (pq.size() > 1) { Node* left = pq.top(); pq.pop(); Node* right = pq.top(); pq.pop(); Node* parent = new Node('$', left->freq + right->freq); parent->left = left; parent->right = right; pq.push(parent); } return pq.top(); } // 遍历哈夫曼树,计算每个字符的编码 void calcHuffmanCode(Node* root, unordered_map<char, string>& code, string cur) { if (!root) return; if (root->ch != '$') { code[root->ch] = cur; } calcHuffmanCode(root->left, code, cur + "0"); calcHuffmanCode(root->right, code, cur + "1"); } // 将原始数据编码成哈夫曼编码 string encode(string s, unordered_map<char, string> code) { string res; for (char c : s) { res += code[c]; } return res; } // 将哈夫曼编码解码成原始数据 string decode(string s, Node* root) { string res; Node* cur = root; for (char c : s) { if (c == '0') { cur = cur->left; } else { cur = cur->right; } if (!cur->left && !cur->right) { res += cur->ch; cur = root; } } return res; } int main() { string s = "abacabad"; unordered_map<char, int> freq; for (char c : s) { freq[c]++; } Node* root = buildHuffmanTree(freq); unordered_map<char, string> code; calcHuffmanCode(root, code, ""); string encoded = encode(s, code); string decoded = decode(encoded, root); cout << "Original string: " << s << endl; cout << "Encoded string: " << encoded << endl; cout << "Decoded string: " << decoded << endl; return 0; } ```

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值