【C/C++】386. 字典序排数

411 阅读3分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第18天,点击查看活动详情


题目链接:386. 字典序排数

题目描述

给你一个整数 n ,按字典序返回范围 [1, n] 内所有整数。

你必须设计一个时间复杂度为 O(n) 且使用 O(1) 额外空间的算法。

提示:

  • 1n51041 \leqslant n \leqslant 5 * 10^4

示例 1:

输入: n = 13
输出: [1,10,11,12,13,2,3,4,5,6,7,8,9]

示例 2:

输入: n = 2
输出: [1,2]

题意整理

题目非常简短,让我们返回 [1, n] 内所有整数按照字典序排序后的结果,并且有复杂度要求。

解题思路分析

习惯性动作:首先确定题目数据范围 51045 * 10^4 ,这个数据大小是不能接受 O(n2)O(n^2) 的时间复杂度,可以接受的是 O(nlog2n)O(n \log_2 n)O(n)O(n) 的时间复杂度。

我们很容易想到的做法是 O(n)O(n) 遍历一遍 n 个数字,将每个数字转换成字符串的形式,然后对其进行 O(nlog2n)O(n\log_2 n) 的字符串排序。

显然这样做是可以通过所有测试的,但是不符合题目的要求,题目要求设计一个时间复杂度为 O(n)O(n) 且使用 O(1)O(1) 额外空间的算法,因此我们不能使用直接排序的方法。排序的方法使用了 O(nlog2n)O(n\log_2 n) 的时间复杂度,O(n)O(n) 的空间复杂度。

那我们考虑一下对于一个整数 num,它的下一个字典序整数应该怎么求:

  1. 首先可以想到的是在 num 后面直接添加一个 0 这肯定是 num 的下一个字典序整数,但是我们需要考虑当 num 后面添加一个 0 后也就是 num * 10 的大小是否超过了 n,因为题目要求我们对 n 以内的数字进行字典序排序,所以我们可以得到求 num 的下一个字典序整数方法为:如果 num * 10 <= n,那么说明 num * 10num 的下一个字典序整数
  2. 其次我们考虑剩余情况,也就是如果 num * 10 > n 的情况:那么我们可以对 num 进行 +1 操作,同样我们需要考虑当 num + 1 后是否超过了 n,并且考虑边界情况:当 num % 10 == 9 时我们对 num 进行 +1 操作其实就相当于对 num 进行 (num / 10) + 1 操作,那不就是对 num 的上一层进行 +1 操作,所以当 num * 10 > n 时,我们可以求得 num 的下一个字典序整数方法为:如果num * 10 > n,那么 num + 1 是下一个字典序整数,但是需要判断 num 末尾的数位已经搜索完成的情况:如果 num % 10 == 9num + 1 > n,表示 num 末尾的数位已经搜索完成,我们需要一直回退至 num % 10 != 9num + 1 <= n 的那一位进行 num + 1 操作。这里回退上一位也就是 num=num10\textit{num} = \Big \lfloor \dfrac{\textit{num}}{10} \Big \rfloor,即 num = num / 10;

具体实现

  1. 字典序最小的整数为 num = 1,我们从它开始,然后依次获取下一个字典序整数,加入结果中,结束条件为已经获取到 n 个整数。
  2. 对于一个整数 num 的下一个字典序整数获取方法:
    • num * 10 <= n 时我们直接 num *= 10
    • 否则对 num 进行 +1 操作,但是在进行 +1 操作前需要判断 num 末尾的数位已经搜索完成的情况,回退至 num % 10 != 9num + 1 <= n 的那一位进行 num + 1 操作。

复杂度分析

  • 时间复杂度:O(n)O(n),其中 n 为整数的数目。获取下一个字典序整数的最坏时间复杂度为 O(logn)O(\log n),但 while 循环的迭代次数与 num 的末尾连续 9 的数目有关,在整数区间 [1,n][1, n] 中,末尾连续的 9 的数目为 k 的整数不超过 n10k\Big \lceil \dfrac{n}{10^k} \Big \rceil 个,其中 1klog10n1 \le k \le \lceil \log_{10} n \rceil ,因此总迭代次数不超过 kkn10k2n\sum_k k \Big \lceil \dfrac{n}{10^k} \Big \rceil \le 2n,总时间复杂度为 O(n)O(n)
  • 空间复杂度:O(1)O(1)。返回值不计入空间复杂度。

需要注意的是严格来说递归实现的 DFS 空间复杂度是 O(log10n)O(\log_{10} n) 不满足题目要求的 O(1)O(1) ,而用迭代实现的空间复杂度满足 O(1)O(1)

代码实现

class Solution {
public:
    vector<int> lexicalOrder(int n) {
        //答案数组res
        vector<int> res;
        res.clear();
        //初始化字典序最小的整数为1
        int num = 1;
        for(int i = 1; i <= n; i++){
            //从第1个到第n个按照字典序压入答案数组res
            res.push_back(num);
            //获取num的下一个字典序整数
            if(num * 10 <= n){
                num *= 10;
            }
            else{
                //考虑 +1 后大于 n 和 num % 10 = 9 的边界情况
                while(num + 1 > n || num % 10 == 9){
                    //回退上一层
                    num /= 10;
                }
                num++;
            }
        }
        return res;
    }
};

总结

该题本质上是对一颗节点数量为 10(节点从 09),形态类似字典树的多阶树进行遍历。此外还有一道类似于这题的进阶版本:440.字典序的第K小数字


结束语

我们总在仰望和羡慕别人,一回头,却发现自己正被别人仰望和羡慕着。其实每个人都有属于自己的幸福,只是你的幸福,常常在别人眼里。与其总是与别人攀比,不如珍惜此刻所拥有的。把握当下的生活,才能通往明天的幸福。