给你一个整数 n
,按字典序返回范围 [1, n]
内所有整数。你必须设计一个时间复杂度为 O(n)
且使用 O(1)
额外空间的算法。相关标签:深度优先搜索,字典树;提示:1 <= n <= 5 * 10^4
。
Tire树(又称单词查找树)简介:是一种树形结构,是一种哈希树的变种。典型应用于统计,排序和保存大量的字符串(但不仅限于字符串),所以经常被搜索引擎系统用于文本词频统计。它的优点是利用字符串的公共前缀来减少查询时间,最大限度地减少无谓的字符串比较,查询效率比哈希树高,缺点是空间复杂度比较大。优化:我们可以用链表来动态开辟空间,达到空间上利用率的最大化。
Tire树的性质:根结点不包含字符,其他的每一个节点只包含一个字符;从根结点到某一节点,路径上经过的字符连接起来,为该节点对应的字符串(假如某个节点为一个字符串的结尾,对其打个标记即可)。每个节点的所有子节点包含的字符都不相同。
请注意在这题我们用一个trie树来表示所有的数,如果当前数是在1−n范围内的,就插入到答案中。因为表示了所有数,所以没必要模拟一个trie数并且插入。所以我们只需要模拟一个在trie上树搜索即可。时间复杂度O(N)。tire树还不懂的自己随便在网上找张tire树的图看看就理解了。
这题的话就是给我们n个数,让我们把n个数按照字典序排一下,注意不是按数的大小来排而是按字典序的大小来排。力扣这题原本的数据范围很大有5,000,000(五百万)然而现在只剩下50,000(五万)了。
字典序的含义比较简单,这里就不放长概念了粗略的说明一下。比如说88和123,按照数字大小来比较的话88肯定是小于123的,但是按照字典序的话88就大于123,因为是比较每一位的字符大小,简单粗暴的理解就是这个意思,想知道更多的话就自己去百度吧。
接下来我们讨论解题方法,首先最暴力的做法就是把1~n个数先转化为string看成字符串,然后你对string排序就可以了。那么这么做的时间复杂度是多少呢?首先每一个数的长度是log n是吧,然后快排的时间复杂度是nlog n,所以总共的时间复杂度就是一乘就是nlog n^2,这是暴力做法。然后比较优秀的做法是可以用tire来做,注意此tire非彼tire,tire啥意思呢。
tire的意思我们上文已经讲过了,一般字符串排序用tire排序的话时间复杂度能少一个log n,tire其实就类似于桶排序。那么这个tire是怎么样的一个思路呢,就是这样我们先将所有数看成一个字符串。比如123、124还有12都是一个字符串,然后我们把他们插到这个树里面,然后我们可以想象一下将1~n插入到这个串里面,而不是我们在程序设计中真的插入这些数。
想象完把上面数插入到这个串里面,然后插完之后怎么去按照字典序从小到大输出呢?这个其实就是一个树的遍历,如果我们想找到tire里面所有数的话,其实就是一个树的遍历,那么为了能够从小到大的去输出所有数,就比如我们有几个分支是吧,为了保证我们的字典序最小,那么我们再搜的时候第一个数应该是越小越好是吧,所以我们在搜的时候要从小到大搜索每一个分支,那这样只要你按照我们这个思路来搜的话,那么你搜到的结果一定是按照字典序的结果。
因为我们会先搜1再搜2再搜3是吧,那么以1开头的子树肯定会比右边的树都要小,因为它第一个已经小了是吧。所以说我们只要按照字典序去搜这棵树,我们就可以把所有数按照字典序搜出来。好吧,那么这个就是tire树的一个大概的思路,那么这个思路的话首先遍历整个树的时间复杂度是n logn是吧,因为整棵树里面的结点的个数就是n log n是吧,然后我们找出来所有数的时间复杂度也是nlog n,所以整个整个算法时间复杂度就是nlog n,比我们之前的暴力做法要小一个log n。注意我们在写的时候不需要真的把我们所有的数插入到我们这个tire树里面。它插入到时候很有规律是1~n连续插入,所以我们就不需要插入了。然后我们在搜的时候我们假设每个分支都有,只不过我们去判断一下如果当前这个分支的数大于n了,那么这个后面的数也一定大于n是吧,就直接停止就可以了就不用继续往下搜了,所以这题我们是不需要真正的插入的,我们只需要在查询的时候限定一下范围就可以了。
然后是代码怎么写:
class Solution {
public:
vector res;
vector lexicalOrder(int n) {
for(int i=1;i<=9;i++) dfs(i,n);
return res;
}
void dfs(int cur,int n) {
if(cur <= n) res.push_back(cur);
else return;
for(int i=0;i<=9;i++) dfs(cur*10+i,n);
}
};