这贴子我看到的时候,脑子第一反应就是:“这老板是不是前一天晚上喝多了?”十几个人的小公司,就敢在群里问“谁能开发一门编程语言”?说实话,别说会不会,就这语气,就已经让人满头问号。
我觉得,老板可能想要的是“结合业务的DSL”,而不是啥真正意义上的编程语言。这事儿吧,说难也难,说简单也简单。
要真是搞个类SQL的东西,配合公司业务场景生成代码,倒也不是不可能。但如果他真以为自己公司能搞出下一个Python,那就只能说,梦做得挺好。
而且要做这事,不是你随便叫一个人牵头就能搞定的。这玩意不仅要懂语法设计、解释器实现,还得配合业务逻辑、团队协作,最重要的,还得有人用,能推广。这难度,绝对不是“有程序员就能上”的活(备注:文末可领最新资料)。
算法题:至少有 1 位重复的数字
这个题目其实是个典型的“补集问题”:我们要找的是那些“至少有一个数字重复”的数,其实等价于:用总数减去“所有数字都不重复”的数。
我一开始没这么想,脑袋里先过了一遍暴力法,想着是不是每个数都拆开判断一下有没有重复数字。但你要是认真点一想,n可能是10^9,这玩意儿暴力跑根本跑不动,老老实实用数学建模来搞。
正经地来一套数学推导
先来看看总共有多少个数?n个。那我们要干的是:计算出[1, n]之间有多少个数字没有重复的数字,再用n减去这个数。
重点来了,这个“没有重复数字”的数怎么算?我们得分两种情况讨论。
第一种是:位数小于n的数,比如n是4321,那我们就先看1位、2位、3位数里有多少个不重复的。
这个其实可以组合数学搞定:
1位数里,1-9,有9个(0不能算)
2位数,第一位可以选1-9中的任何一个,第二位要从0-9去掉第一位那个数里选,共9*9=81
3位数,第一位还是9种,第二位是9种(除去第一位),第三位是8种(除去前两个),所以998=648
4位数,第一位是9,第二位9,第三位8,第四位7:998*7=4536
这一部分叫“低位全排列”,用的是P(9, d-1)
但有些n是非整十,比如4321,那我们不能只靠排列。得处理一下“最高位卡着”的情况,比如4xxx里面,必须小于4321,这就得做DFS。
接下来是正片:DFS逐位判断
我写的时候是从最高位往下递归,判断每一位的取值范围。比如说n=4321,我们要先看千位:
千位可以从1到4(不能从0开始)
假设我们取了3,那后面三位我们可以自由地取数字,但不能重复,也得小于321这个范围
这其实就是一个典型的“前缀树剪枝”的操作,得维护一个used数组,记录用过的数字,下一位不能再用。
每一位我们都有两种选择:
小于当前位:直接暴力枚举所有没用过的数字
等于当前位:那就进入下一位继续递归
一旦某位比目标位大了,就不能继续了;如果某位选的数已经用过了,也不能继续。
Java代码怎么写?我用了递归函数dfs(pos, usedMask, isLimit, isNum),分别代表:
当前是第几位
当前哪些数字已经用过(用一个int的bit位表示)
当前位是否受上层限制(比如前面位都是紧贴n)
当前是否已经选了数字(为了处理前导0)
这个思路虽然不太像普通回溯,但它的效率是真高,因为是状态压缩的,最多也就处理10个bit的used。
Java实现也贴一下,核心部分是DFS
public class Solution {
public int numDupDigitsAtMostN(int n) {
String s = String.valueOf(n);
return n - dfs(0, 0, true, false, s);
}
private int dfs(int pos, int used, boolean isLimit, boolean isNum, String s) {
if (pos == s.length()) return isNum ? 1 : 0;
int res = 0;
if (!isNum) {
res += dfs(pos + 1, used, false, false, s);
}
int low = isNum ? 0 : 1;
int high = isLimit ? s.charAt(pos) - '0' : 9;
for (int d = low; d <= high; d++) {
if ((used >> d & 1) == 0) {
res += dfs(pos + 1, used | (1 << d), isLimit && d == high, true, s);
}
}
return res;
}
}
这段代码逻辑不长,但藏着不少门道。像used >> d & 1就是个经典的位运算技巧,用来检测数字d有没有用过。
补充个实战经验:
我在项目中还真用过类似思路。某次做手机号筛选,客户要求“不能有重复数字”,我第一反应就是这个题。不同的是,那次我还得加规则“不能有两个奇数”,规则多了,状态就更复杂,但套路一样:位数DFS+状态压缩。
再讲点“坑”:
有个坑是前导0的问题。像“00123”这种不应该算数,但我们的DFS逻辑会在没选任何数的情况下跳过当前位,这就可以规避这个坑。
还有一个坑就是n本身可能有重复,比如n=1000,我们得算清楚从1到999中不重复的,再处理1000这个数。
我觉得这个题有点启发意义:
很多题不是直接求目标,而是“先求补集”,这个思维在排列组合问题里特别常见。就像有时候我们求概率,直接求1-不满足条件的概率反而简单。
而这题也是数字DP的典型入门之一,下一步你可以试试“所有数字都不含4”“数字不连续递增”这种扩展题。
讲真,做完这题再回头看什么“多少个不含某个子串的数”都会轻松点。就看你愿不愿意多动脑子,把暴力优化成结构化搜索。你会发现,不止是代码,思维也干净了很多。