像往常一样,有一个简单的答案和一个复杂的答案。
Clifford在另一个答案中已经描述了简单的答案:将数字序列构造为字符串,然后i
从中选择第'个字符。
复杂的答案要有趣得多。该字符串实际上是OEIS序列A007376。有已知的公式(通过相关的序列A033307),但它们要么适合于生成整个序列,要么包含令人讨厌的内容,例如Lambert W函数的本金。由于序列非常简单,因此我们可以构建自己的算法。
让我们看一下序列本身:
前9位数字是数字本身:1、2、3,..,8、9
接下来的2 * 9 * 10 = 180个数字来自数字10、11,..,98、99
接下来的3 * 9 * 10 * 10 = 2700个数字来自数字100、101,..,998、999
接下来的4 * 9 * 10 * 10 * 10 = 36000个数字来自数字1000、1001,..,9998、9999
我们将序列的一部分称为具有相同位数的数字,即region。第一个区域包含9个数字,下一个包含数字10至99的180个数字,接下来的2700个数字100至999的数字,依此类推。
关键是为我们感兴趣的索引找到正确的区域。事实证明很容易:我们只需减去每个区域的位数,直到“剩余索引”在当前区域之内!
假设n
是正确区域中的位数,即i
“剩余索引”,即从序列索引中减去的较小区域中的位数。
为了获得目标号码-整数值序列索引从挑选位- ,我们需要10加N-1来(i-1)/n
。这是因为第一个(所以i = 1)两位数字是10,而不是11。数字中的位置是(i-1)%n
(除法(i-1)%n的余数),其中0表示最左边。另一种计算位置的方法是使用(n-1)-(i-1)%n
,其中0表示最右边的数字(“ ones”)。
(与往常一样,如果将索引较早地减一,并使用基于零的计算,则数学会变得简单一些。)
无需信守我的诺言。考虑以下实现:
#include <stdint.h>
const uint64_t pow_10[20] = {
UINT64_C(1),
UINT64_C(10),
UINT64_C(100),
UINT64_C(1000),
UINT64_C(10000),
UINT64_C(100000),
UINT64_C(1000000),
UINT64_C(10000000),
UINT64_C(100000000),
UINT64_C(1000000000),
UINT64_C(10000000000),
UINT64_C(100000000000),
UINT64_C(1000000000000),
UINT64_C(10000000000000),
UINT64_C(100000000000000),
UINT64_C(1000000000000000),
UINT64_C(10000000000000000),
UINT64_C(100000000000000000),
UINT64_C(1000000000000000000),
UINT64_C(10000000000000000000)
};
int A007376(uint64_t i)
{
uint64_t digits = 1U;
uint64_t value = 1U;
uint64_t limit = 9U;
unsigned int tens;
/* The sequence starts at index 1. Shift to zero. */
if (!i--)
return 0;
/* Find the number of digits in each number
in the region of index i. */
while (i/limit >= digits) {
const uint64_t old_limit = limit;
i -= digits * limit;
digits++;
value *= 10U;
limit *= 10U;
/* If limit overflows, we are in the correct, final region. */
if (limit <= old_limit)
break;
}
/* We know the desired digit is i'th digit
in a string formed by concatenating the
9*10^(digits-1) numbers starting at 10^(digits-1).
The value of this number is pow10(digits-1) + i/digits: */
value += i / digits;
#ifdef DEBUG_OUTPUT
fprintf(stderr, "Digit %d of %" PRIu64 " ", 1 + (int)(i % digits), value);
#endif
/* Move the desired digit into the ones position. */
value /= pow_10[(digits - 1) - (i % digits)];
#ifdef DEBUG_OUTPUT
fprintf(stderr, "= %d\n", (int)(value % 10U));
#endif
return value % 10U;
}
Run Code Online (Sandbox Code Playgroud)
上面的函数提供了对数时间复杂度(O(log N))所需的数字。一些有趣的结果:
i A007376(i)
1 1
9 9
10 1
11 0
12 1
13 1
186 9
187 8
188 9
189 9
190 1
191 0
192 0
2884 9
2885 9
2886 8
2890 1
2891 0
2892 0
2893 0
4294967284 4
4294967285 8
4294967286 9
4294967287 5
4294967288 6
4294967289 4
4294967290 2
4294967291 6
4294967292 6
4294967295 9
18446744073709551580 1
18446744073709551581 0
18446744073709551582 2
18446744073709551583 9
18446744073709551584 3
18446744073709551585 6
18446744073709551586 0
18446744073709551587 7
18446744073709551588 9
18446744073709551589 9
18446744073709551590 2
18446744073709551591 0
18446744073709551592 1
18446744073709551593 0
18446744073709551594 8
18446744073709551595 7
18446744073709551596 5
18446744073709551597 1
18446744073709551598 0
18446744073709551615 5
Run Code Online (Sandbox Code Playgroud)
当然,“简单”(数字顺序为字符串,i
从中选择第一个字符)与“复杂”(上面的函数)之间有许多可能的中间实现方式,具体取决于对行为的深入了解。您研究的顺序。
就个人而言,我想说对于这个特定的序列,区域概念或类似概念是一个分水岭。(例如,您可以只在缓冲区中打印64位目标值,然后通过从缓冲区中挑选出来返回数字。我仍将这种实现方式归类为“复杂”类别。)
简单方法可用作字符串操作(甚至甚至在动态内存管理中)的练习,但组合方法和与序列相关的问题的实际解决方案应尽可能使用“复杂”方法:简单方法无法扩展。