算法之字典序问题(java实现,详细剖析)

第一种情况

例如"ab"这个字符串

步骤1:计算"ab"这个字符串的长度len=2,

步骤2:计算所有小于len位即小于2,那就是1位了,就是算出字符串长度为1时的字符串的总个数
因为"ab"是长度为2的字符串,说明长度为1的(a,b,c,d…z)这些字符串它都已经经历过了,
所以把前面的总和先算出来,通过调用g(int k)计算得26

步骤3:把字符串转化为字符数组,然后计算出当前字符串"ab"的首个字符head的编码,
即int head = a[0]-‘a’ + 1,如果head计算得1,就说明这个首个字符刚好是a,

步骤4:计算head(‘a’)字符开头的剩下所有字符的字符串个数,这里"ab",head开头字符就是a,剩下的字符
就是b,说明a后紧接的就是b,所以直接26加上本身的字符串"ab"就可以得到27。


第二种情况

例如"abc"这个字符串

步骤1:计算"abc"这个字符串的长度len=3,

步骤2:计算所有小于len位即小于3,那就是1位和2位了,就是算出字符串长度为1时的字符串的总个数
和计算出字符串长度为2时的字符串的总个数,再把这两个数相加,因为"abc"是长度为3的字符串,
说明长度为1的(a,b,c,d…z)这些字符串它都已经经历过了,(ab,ac,ad,…az)、(bc,bd,be,…bz)、
(de,df,dg,…dz)、…(yz)这些长度为2的字符串的编码它也已经经历过,所以可以通过循环,分别
调用g(int k)函数,计算两种情况的字符串总数在相加,就是26+325=351.

步骤3:把字符串转化为字符数组,然后计算出当前字符串"abc"的首个字符head的编码,
即int head = a[0]-‘a’ + 1,如果head计算得1,就说明这个首个字符刚好是a,
这个和"ab"字符串的情况一样。

步骤4:计算head(‘a’)字符开头的剩下所有字符的字符串个数,

for (int i = 1,temp = head; i < len; i++) {
				int str2 =ch[i]-'a'+1;//获取下一个要计算的首字母
				int len2 =  len-i;//获取新的字符串即当前字符串长度
				for (int j = temp+1; j < str2; j++) {
					sum+=f(j,len2);//计算j位开头的,字符串长度为len2的字符串个数
				}
				temp=str2;
			}

用一个二重循环,int str2 =ch[i]-‘a’+1;就是表示除去"abc"的字符’a’,i=1开始就是要获取’b’字符的编码
然后int len2 = len-i;计算出新的字符串"bc"当前的长度,内循环j= temp+1,我们知道temp存的是head即’a’
的编码值1,temp+1就是从’a’的下一个就是’b’,编码值是2开始循环,然后j < str2就表示循环到当前首字符,这里很不幸,
情况和第一种是一样的,因为Str2刚刚我们算的是2,也就是b,就是算b->b之间的字符个数,但是是到自己所以就不用
内层循环就不用执行,然后把str2的值重新赋给temp,然后i++,int str2 =ch[i]-‘a’+1; 之后str2就变成3,代表的首字符就是c了,
下面同样内循环,从temp+1开始,到c之间的字符数,刚好又是c,又不用循环,这里看不懂没关系,情况2只是和情况1对比理解步骤2
同时引出第三种情况,可以看完第3中情况后,再来看这里的第四步。

步骤5:既然都不用循环进去计算,就在最后在加上本身的"abc"这个字符串咯,就是前面的sum=351+1=352

第三种情况

例如""bug"这个字符串(当然这个不符合规定因为g比前面的小,导致后面处理最后一个g的时候直接跳出循环了,发现的时候已经晚了,道理都是一样的)

步骤1:计算"bug"这个字符串的长度len=3

步骤2:计算所有小于len位即小于3,那就是1位和2位了,就是算出字符串长度为1时的字符串的总个数
和计算出字符串长度为2时的字符串的总个数,再把这两个数相加,因为"bug"是长度为3的字符串,
说明长度为1的(a,b,c,d…z)这些字符串它都已经经历过了,(ab,ac,ad,…az)、(bc,bd,be,…bz)、
(de,df,dg,…dz)、…(yz)这些长度为2的字符串的编码它也已经经历过,所以可以通过循环,分别
调用g(int k)函数,计算两种情况的字符串总数在相加,就是sum=26+325=351.

步骤3把字符串转化为字符数组,然后计算出当前字符串"bug"的首个字符head的编码,
即int head = a[0]-‘a’ + 1,这里的话计算head=2,然后通过下面这个循环

for (int i = 1; i < head; i++) {
			//3.计算以head字符之前的字符  打头长度为len的字符串个数并加到sum中
					//"ab"的话,a就是最前面的了,不需要累加了
					sum +=f(i,len);
				}

就是就算’b’字符之前的字符,‘b’字符前面好像就只有’a’这一个了,所以就循环一次就好了,如果是’c’,前面有’b’和’a’,那么就要循环两次,调用f(int i int k)函数计算从i开始即从1开始,也是’a’开始,长度为3的字符串个数,在和前面的351累加就算sum=351+300=651,

步骤4:计算head(‘b’)字符开头的剩下所有字符的字符串个数,

for (int i = 1,temp = head; i < len; i++) {
				int str2 =ch[i]-'a'+1;//获取下一个要计算的首字母
				int len2 =  len-i;//获取新的字符串即当前字符串长度
				for (int j = temp+1; j < str2; j++) {
					sum+=f(j,len2);//计算j位开头的,字符串长度为len2的字符串个数
				}
				temp=str2;
			}

用一个二重循环,int str2 =ch[i]-‘a’+1;就是表示除去"bug"的字符’b’,i=1开始就是要获取’u’字符的编码然后int len2 = len-i;
计算出新的字符串"ug"当前的长度,内循环j= temp+1,我们知道temp存的是head即’b’的编码值2,temp+1就是从’b’的下一个就是’c’,编码值是3开始循环,
然后j < str2就表示循环到当前首字符’u’,编码为21,就是从3循环到21,计算字符’c’->‘u’,之间的字符串个数,内层循环第一次就是f(3,2)就是c开头
的长度为2的字符串个数,接着第二次f(4,2),第三次f(5,2)…直到f(20,2),在累加和
sum=651+23+22+21+20+19+18+17+16+15+14+13+12+11+10+9+8+7+6=912,接着把str2的值重新赋给temp即把21赋给了temp,然后i++变成2,len是3,仍然
成立,这次的首字符是’g’,编码值为7,所以str2=7,新的字符串"g"长度len=3-2=1了,此时的temp+1的值是21+1=22,不满足j < tr2 的条件,然后跳出内
层循环,执行temp=str2;把str2的值重新赋给temp即把7赋给了temp,i++变成了3,就跳出外层循环结束

步骤5:最后在加上本身的"bug"这个字符串咯,就是前面的sum=912+1=913

以上例子最好结合下面的代码慢慢理解,每个方法都有注释,下面是实现代码

package experiment1;

import java.util.Scanner;

/**
 * 设以第i个字符打头的长度不超过k的升序字符串个数为f(i,k):如ab,ab,ac,ad...az,abc...abz..acd...acz..
 * 长度不超过k的升序字符串总个数为g(k),则 g(k)=i=1到i=26f(i,k)的和
 * @remakeTODO
 * @author Conquer丶ZF
 * @date 2019年9月21日上午9:52:03
 */
public class ZiDianXu {

	public static void main(String[] args) {
		
//		int sum1 = f(1,2);
//		System.out.println(sum1);
		
//		int sum2 = g(2);
//		System.out.println(sum2);
		Scanner input = new  Scanner(System.in);
		System.out.print("请输入k的值:");
		int k = input.nextInt();
		
		String str;
		for (int i = 0; i < k; i++) {
			System.out.print("请输入第"+ (i+1) +"个字符串:");
			str = input.next();
			
			int num = getNum(str);
			System.out.println(str +"的编码是:"+num);
		}
		

	}
	
	/*
	 * f(i,k):第i位(共26位对应a-z)字符开头,字符串长度为k的升序
	 * 字符串个数(注意是升序字符串,是串的个数)
	 * 举个例子就是以a开头,长度为2即f(1,2)就是ab,ac,ad....az这样的字符串一共25个
	 * 以b开头,长度为2即f(2,2)就是bc,bd,be...bz这样的字符串一共24个
	 */
	public static int f(int i, int k){
		int sum = 0;
		if(k==1){  //如果字符串长度是1,那不管是什么开头的都是只有本身这一种字符串
			return 1;
		}else{
			for (int j = i+1; j <= 26; j++) {
			//就比如f(1,2)ab-az就是从后面b算到z的个数,因为把a抛弃了所以长度k-1,再次递归,
		    //结果发现抛弃了a,长度就变1了,那么长度为1,b开头的字符串就只有本身-->b
		    //接着j++来到c开头,也是同理...
				sum +=f(j,k-1);
				
			//如果是(1,3)abc-abz、acd-acz、ade-adz....ayz很多种,也是通过递归,第一次递归
			//长度变成2.第二次递归长度变成1,就可以相应累加计算了
			}
			return sum;
		}
	}
	
	/*
	 * 此方法用于字符串长度为k的所有字符串的总个数
	 * 比如字符串长度为1时,即(a,b,c,d....z)所有的字符总个数为26个,
	 * 即f(1,1)+f(2,1)+f(3,1)+f(4,1)+...f(26,1)的和算得是26
	 * 
	 * 字符串长度为2时,即(ab,ac,ad,ae....az)、(bc,bd,be...bz)、(cd,ce...cz)、(yz)
	 * 即f(1,2)+f(2,2)+f(3,2)+f(3,3)+f(4,3)+....f(26,2)的和
	 * 即调用上面的递归 25+24+23+22+...1=325  ((1+25)*25)/2=325
	 * 
	 * 字符串长度为3时也类似.....
	 */
	public static int g(int k){
		int sum = 0;
		for (int i=1 ; i <=26; i++) {
			
			sum+=f(i,k);
		}
		return sum;
	}
	
	/*
	 * 获取编码值,即求最后的字典序
	 */
	public static int getNum(String str){

		int len = str.length();//计算字符串的长度,比如传入字符串"ab",则len=2
		int sum = 0;
		for (int i = 1; i < len; i++) {
		//1.计算所有小于len位的各个位的升序字符串的总和
			//比如"ab",len=2,则会计算出字符串长度为1时字符串的总个数即26
			//"abc",则会计算那么len为3,小于len的有1和2所以个数是26+325=351个
			sum +=g(i);
		}
		//2.把字符串转换为字符数组
			char[] ch=str.toCharArray();
			int head = ch[0]-'a'+1; //比如传入"ab",ch[0]='a',则head='a'-'a'+1=1
			//head=1说明输入的是打头元素a,前面没有
			for (int i = 1; i < head; i++) {
		//3.计算以head字符之前的字符  打头长度为len的字符串个数并加到sum中
				//"ab"的话,a就是最前面的了,不需要累加了
				sum +=f(i,len);
			}
		//4.计算s字符开头即,剩下的所有字符的字符串个数
			//若"ab",就是计算a字符开头剩下的所有字符串个数
			for (int i = 1,temp = head; i < len; i++) {
				int str2 =ch[i]-'a'+1;//获取下一个要计算的首字母如果是"ab",就算得值是2,即b
				int len2 =  len-i;//获取新的字符串即当前字符串长度
				for (int j = temp+1; j < str2; j++) {
					sum+=f(j,len2);
				}
				temp=str2;//要放在内循环外,外循环内,放在里面循环一次temp就会往下跳一次
			}
		
		
		return sum+1;//最后加上原字符串
	}

在这里插入图片描述

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值