检查拼写数字是否在C++范围内
Check if a spelled number is in a range in C++
我想在输入部分输入时根据范围列表(min,max)检查(数字)输入;换句话说,我需要一个优雅的算法来检查数字的前缀与范围(不使用正则表达式)。
示例测试用例:
1 is in ( 5, 9) -> false
6 is in ( 5, 9) -> true
1 is in ( 5, 11) -> true (as 10 and 11 are in the range)
1 is in ( 5, 200) -> true (as e.g. 12 and 135 are in the range)
11 is in ( 5, 12) -> true
13 is in ( 5, 12) -> false
13 is in ( 5, 22) -> true
13 is in ( 5, 200) -> true (as 130 is in the range)
2 is in (100, 300) -> true (as 200 is in the range)
你有什么想法吗?
我相信输入是可以接受的,当且仅当
:- 它是转换为字符串的下限的前缀子字符串
或
- 输入后跟任意数量的附加零(可能没有)落入该范围
第一条规则是必需的,例如13 is in range (135, 140)
. 第二条规则是必需的,例如2 is in range (1000, 3000)
.
第二条规则可以通过一系列乘以 10 来有效地测试,直到缩放后的输入超过上限。
迭代公式:
bool in_range(int input, int min, int max)
{
if (input <= 0)
return true; // FIXME handle negative and zero-prefixed numbers
int multiplier = 1;
while ((input + 1) * multiplier - 1 < min) // min <= [input]999
multiplier *= 10; // TODO consider overflow
return input * multiplier <= max; // [input]000 <= max
}
更简单的[编辑:更有效;见下文]方法是使用截断整数除法:
bool in_range(int input, int min, int max)
{
if (input <= 0)
return true;
while (input < min) {
min /= 10;
max /= 10;
}
return input <= max;
}
测试和剖析:
#include <iostream>
#include <chrono>
bool ecatmur_in_range_mul(int input, int min, int max)
{
int multiplier = 1;
while ((input + 1) * multiplier - 1 < min) // min <= [input]999
multiplier *= 10; // TODO consider overflow
return input * multiplier <= max; // [input]000 <= max
}
bool ecatmur_in_range_div(int input, int min, int max)
{
while (input < min) {
min /= 10;
max /= 10;
}
return input <= max;
}
bool t12_isInRange(int input, int min, int max)
{
int multiplier = 1;
while(input*multiplier <= max)
{
if(input >= min / multiplier) return true;
multiplier *= 10;
}
return false;
}
struct algo { bool (*fn)(int, int, int); const char *name; } algos[] = {
{ ecatmur_in_range_mul, "ecatmur_in_range_mul"},
{ ecatmur_in_range_div, "ecatmur_in_range_div"},
{ t12_isInRange, "t12_isInRange"},
};
struct test { int input, min, max; bool result; } tests[] = {
{ 1, 5, 9, false },
{ 6, 5, 9, true },
{ 1, 5, 11, true }, // as 10 and 11 are in the range
{ 1, 5, 200, true }, // as e.g. 12 and 135 are in the range
{ 11, 5, 12, true },
{ 13, 5, 12, false },
{ 13, 5, 22, true },
{ 13, 5, 200, true }, // as 130 is in the range
{ 2, 100, 300, true }, // as 200 is in the range
{ 13, 135, 140, true }, // Ben Voigt
{ 13, 136, 138, true }, // MSalters
};
int main() {
for (auto a: algos)
for (auto t: tests)
if (a.fn(t.input, t.min, t.max) != t.result)
std::cout << a.name << "(" << t.input << ", " << t.min << ", " << t.max << ") != "
<< t.result << "n";
for (auto a: algos) {
std::chrono::time_point<std::chrono::system_clock> start = std::chrono::system_clock::now();
for (auto t: tests)
for (int i = 1; i < t.max * 2; ++i)
for (volatile int j = 0; j < 1000; ++j) {
volatile bool r = a.fn(i, t.min, t.max);
(void) r;
}
std::chrono::time_point<std::chrono::system_clock> end = std::chrono::system_clock::now();
std::cout << a.name << ": "
<< std::chrono::duration_cast<std::chrono::nanoseconds>(end - start).count() << 'n';
}
}
令人惊讶的是(至少对我来说)迭代除法出现得最快:
ecatmur_in_range_mul: 17331000
ecatmur_in_range_div: 14711000
t12_isInRange: 15646000
bool isInRange(int input, int min, int max)
{
int multiplier = 1;
while(input*multiplier <= max)
{
if(input >= min / multiplier) return true;
multiplier *= 10;
}
return false;
}
它通过了您的所有测试用例。
一个简单的解决方案是生成范围内的所有 N 位前缀。因此,对于11 is in ( 5, 12)
,您需要 5 到 12 之间的所有数字的两位数前缀。显然,这只是10,11和12。
通常,对于数字 X 到 Y,可以通过以下算法获得可能的 N 位前缀:
X = MIN(X, 10^(N-1) ) ' 9 has no 2-digit prefix so start at 10
Y = Y - (Y MOD 10^N) ' 1421 has the same 2 digit prefix as 1400
WHILE (X < Y)
LIST_PREFIX += PREFIX(N, X) ' Add the prefix of X to the list.
X += 10^(TRUNCATE(LOG10(X)) - N+1) ' For N=2, go from 1200 to 1300
给定一个值n,从半开范围 [n,n+ 1) 开始,然后按数量级进行:
- [10N, 10(n+ 1))
- [100N, 100(n+ 1))
- [1000N, 1000(n+ 1))
- 。
继续,直到迭代范围与目标范围重叠,或者两个范围不再重叠。
#include <iostream>
bool overlaps(int a, int b, int c, int d) {
return a < c && c < b || c < a && a < d;
}
bool intersects(int first, int begin, int end) {
int last = first + 1;
++end;
while (first <= end) {
if (overlaps(first, last, begin, end))
return true;
first *= 10;
last *= 10;
}
return false;
}
int main(int argc, char** argv) {
std::cout << std::boolalpha
<< intersects( 1, 5, 9) << 'n'
<< intersects( 6, 5, 9) << 'n'
<< intersects( 1, 5, 11) << 'n'
<< intersects( 1, 5, 200) << 'n'
<< intersects(11, 5, 12) << 'n'
<< intersects(13, 5, 12) << 'n'
<< intersects(13, 5, 22) << 'n'
<< intersects(13, 5, 200) << 'n'
<< intersects( 2, 100, 300) << 'n'
<< intersects(13, 135, 140) << 'n';
}
使用范围对于防止遗漏案例是必要的。考虑n= 2 和目标范围 [21, 199]。2 不在范围内,所以我们乘以 10;20 不在范围内,所以我们再次乘以 10;200 不在范围内,也没有任何更高的数字,因此朴素算法以假阴性终止。
我更喜欢使用已经实现的算法的方法。虽然许多其他解决方案使用递归除以10
,但我认为最好使用具有O(1)
复杂度的 10 基对数,这样整个解决方案的复杂度是O(1)
的。
让我们把问题分成两部分。
第一部分将处理number * 10^n
在min
和max
之间至少一个n
的情况。这将让我们检查例如number = 12
和min,max = 11225,13355
,x = 12000 = 12*10^3
介于min
和max
之间。如果此测试签出,则表示结果True
。
第二部分将处理number
min
或max
开始时的情况。例如,如果number = 12
和min,max = 12325,14555
,则第一个测试将失败,因为12000
不在min
和max
之间(并且对于任何n
,12*10^n
所有其他数字都将失败)。但第二次测试会发现,12
是12325
的开始,True
回报。
第一
让我们检查一下,第一个x = number*10^n
是否等于或大于min
小于或等于max
(所以min <= x <= max, where x is number*10^n for any integer n
)。如果它比max
大,则比所有其他x
es都会更大,因为我们选择了最小的。
log(number*10^n) > log(min)
log(number) + log(10^n) > log(min)
log(number) + n > log(min)
n > log(min) - log(number)
n > log(min/number)
为了得到要比较的数字,我们只需计算第一个令人满意的n
:
n = ceil(log(min/number))
然后计算数字x
:
x = number*10^n
第二
我们应该检查我们的数字是否是任一边界的文字开头。
我们只是计算x
以与number
相同的数字开头,并在末尾填充0
s,长度与min
相同:
magnitude = 10**(floor(log10(min)) - floor(log10(number)))
x = num*magnitude
然后检查min
和x
的差异(在量级尺度上)是否小于1
并且大于或等于0
:
0 <= (min-x)/magnitude < 1
所以,如果number
是121
而min
是132125
,那么magnitude
是1000
,x = number*magnitude
将是121000
。min - x
给出132125-121000 = 11125
,它应该小于1000
(否则min
开头将大于121
),所以我们通过除以它的值并与1
进行比较来将其与magnitude
进行比较。如果min
是121000
也可以,但如果min
是122000
就不行了,这就是为什么0 <=
和< 1
。
相同的算法适用于max
.
伪代码
将其全部合并到伪代码中给出了以下算法:
def check(num,min,max):
# num*10^n is between min and max
#-------------------------------
x = num*10**(ceil(log10(min/num)))
if x>=min and x<=max:
return True
# if num is prefix substring of min
#-------------------------------
magnitude = 10**(floor(log10(min)) - floor(log10(num)))
if 0 <= (min-num*magnitude)/magnitude < 1:
return True
# if num is prefix substring of max
#-------------------------------
magnitude = 10**(floor(log10(max)) - floor(log10(num)))
if 0 <= (max-num*magnitude)/magnitude < 1:
return True
return False
可以通过避免重复计算log10(num)
来优化此代码。此外,在最终解决方案中,我会从浮点数变为整数范围(magnitude = 10**int(floor(log10(max)) - floor(log10(num)))
),然后执行所有不除法的比较,即0 <= (max-num*magnitude)/magnitude < 1
->0 <= max-num*magnitude < magnitude
.这将减少舍入误差的可能性。
此外,也可以用magnitude = 10**(floor(log10(min/num)))
替换magnitude = 10**(floor(log10(min)) - floor(log10(num)))
,其中log10
只计算一次。但我不能证明它总会带来正确的结果,也不能反驳它。如果有人能证明这一点,我将不胜感激。
测试(在 Python 中):http://ideone.com/N5R2j(您可以编辑输入以添加另一个测试)。
我在思考@Ben Voigt的美丽解决方案的证明时得出了这个新的简单解决方案:
让我们回到我们做数字比较的小学。 问题会像:检查数字">A"是否在数字">B"和数字">C"的范围内
解决方案:在数字的左侧添加必要的零(因此我们在所有数字中的位数相等) 我们从最左边的数字开始。 将其与其他两个数字中的等效数字进行比较。
如果 A 中的数字小于B中的数字或大于C中的数字,则A不在范围内。
如果没有,我们用A的下一个数字以及B和C的等价物重复该过程。
重要问题:我们为什么不就此止步?我们为什么要检查下一个数字?
重要答案:因为A的数字介于B和C的等价物之间,到目前为止是可以的,但还没有足够的理由做出决定!(很明显吧?
反过来,这意味着可能有一组数字可以将A排除在范围之外。
而且,同样
可能有一组数字可以将A放在范围内
这是另一种说法A可能是该范围内数字的前缀。
这不是我们要找的吗?! :D
该算法的主干基本上是每个输入事件的简单比较:
- 在 min 的左侧添加一些零(如有必要),以便min和max的长度相等。
- 将输入A与最小值和最大值的等效数字进行比较(从左边而不是右边切掉最小值和最大值的相应数字)
- 输入A是 <=最大值的对应部分,>=最小值的对应部分吗?(否:返回假,是:返回真)
假和真在这里表达"到目前为止"的情况,正如问题所要求的那样。
(input >= lower_bound) && input <= upper_bound
OR
(f(input) >= lower_bound) && (f(input) <= upper_bound)
OR
(lower_bound - f(input) < pow(10, n_digits_upper_bound - n_digits_input)) &&
(lower_bound - f(input) > 0)
where
f(input) == (input * pow(10, n_digits_upper_bound - n_digits_input))
1 is in ( 5, 9) -> 1 * pow(10,0) -> same -> false
6 is in ( 5, 9) -> true
1 is in ( 5, 11) -> 1 * pow(10,1) -> 10 is in (5,11) -> true
1 is in ( 5, 200) -> 1 * pow(10,2) -> 100 is in (5, 200) -> true
11 is in ( 5, 12) -> true
13 is in ( 5, 12) -> 13 * pow(10,0) -> same -> false
13 is in ( 5, 22) -> true
13 is in ( 5, 200) -> true
2 is in (100, 300) -> 2 * pow(10,2) -> 200 is in (100,300) -> true
4 is in (100, 300) -> 4 * pow(10,2) -> 400 is in (100,300) -> false
13 is in (135, 140) -> 135 - 130 -> true
14 is in (135, 139) -> 135 - 140 -> false
所有困难的情况都是下限的数字少于上限的情况。只需将范围分成两个(或三个)。如果 AB 是集合 A 和 B 的并集,则x in AB
意味着x in A
或x in B
。所以:
13 is in (5, 12)
=>13 is in (5, 9)
或13 is in (10, 12)
13 is in (5, 120)
=>13 is in (5, 9)
或13 is in (10, 99)
或13 is in (100, 120)
然后,截断以匹配长度。
即13 is in (5, 120)
=>13 is in (5, 9)
或13 is in (10, 99)
或13 is in (10
0, 12
0)
第二次重写后,它变成了一个简单的数字检查。请注意,如果您有显示的范围10,99
那么您拥有所有可能的 2 位前缀,实际上不需要检查,但这是一种优化。(我假设我们忽略前缀 00-09)
是的,另一个答案。对于输入 X 和边界最小值和最大值
WHILE (X < MIN)
IF X is a prefix of MIN
x = 10*x + next digit of MIN
ELSE
x = 10*x
RETURN (x>= MIN && x<=MAX)
这通过"键入"下一个最低数字来工作。因此,硬情况13 in (135, 140)
加 5 以产生 135,而不是零。
无论您选择哪种实现方法,都应该考虑构建大量单元测试。因为您提出这个问题就像您为测试驱动开发 (TDD) 编写测试一样。所以我建议,当你在等待一个合适的算法从堆栈溢出中弹出时,编写你的单元测试:
如果您给出的示例未产生示例中的结果,则使测试失败。编写其他几个极限测试用例以确保万无一失。然后,如果您碰巧使用了错误或错误的算法,您很快就会知道它。测试通过后,您就会知道自己已经达到了目标。
此外,它可以保护您免受未来任何回归的影响
也许我想得太少了,但假设整数的最小-最大范围都是正数(即大于或等于零),这个代码块应该可以很好地解决问题:
bool CheckRange(int InputValue, int MinValue, int MaxValue)
{
// Assumes that:
// 1. InputValue >= MinValue
// 2. MinValue >= 0
// 3. MinValue <= MaxValue
//
if (InputValue < 0) // The input value is less than zero
return false;
//
if (InputValue > MaxValue) // The input value is greater than max value
return false;
//
if (InputValue == 0 && InputValue < MinValue)
return false; // The input value is zero and less than a non-zero min value
//
int WorkValue = InputValue; // Seed a working variable
//
while (WorkValue <= MaxValue)
{
if (WorkValue >= MinValue && WorkValue <= MaxValue)
return true; // The input value (or a stem) is within range
else
WorkValue *= 10; // Not in range, multiply by 10 to check stem again
}
//
return false;
}
好吧,派对有点晚了,但在这里...
请注意,我们在这里讨论的是用户输入,因此仅// TODO: consider overflow
是不够的。验证用户输入是一场战争,偷工减料最终将导致简易爆炸装置的爆炸。(好吧,好吧,也许没有那么戏剧化,但仍然...事实上,ecatmur 有用的测试工具中只有一种算法可以正确处理极端情况({23, 2147483644, 2147483646, false}
),如果t.max
太大,测试工具本身就会进入无限循环。
唯一通过的是ecatmur_in_range_div,我认为这很好。但是,可以通过添加一些检查来使其(稍微)更快:
bool in_range_div(int input, int min, int max)
{
if (input > max) return false;
if (input >= min) return true;
if (max / 10 >= min) return true;
while (input < min) {
min /= 10;
max /= 10;
}
return input <= max;
}
"取决于"快多少;如果 min 和 max 是编译时常量,那将特别有用。我认为,前两个测试是显而易见的;第三个可以通过多种方式证明,但最简单的方法是观察 Ecatmur 循环的行为:当循环结束时,输入为>= min,但<10*min,因此如果 10*min
用除法而不是乘法来表达算术应该是一种习惯;我知道,我们大多数人从小就认为分裂是愚蠢的,必须避免。但是,与乘法不同,除法不会溢出。事实上,每当你发现自己在写:
if (a * k < b) ...
或
for (..., a < b, a *= k)
或这些主题的其他变体,您应该立即将其标记为整数溢出,并将其更改为等效的除法。
实际上,除了一种重要(但常见)的情况外,加法也是如此:
if (a + k < b) ...
或
a += k; if (a < b) ...
也是不安全的,除非k 是 1,并且您知道 a 在加法之前
// enumerate every kth element of the range [a, b)
assert(a < b);
for (; a < b; a += k) { ... }
可悲的是,很少有候选人得到它。
我现在要删除这个答案,除了它显示了失败的方法。
检查
Str(min).StartWith(input)
后,您需要从数字上检查是否有任何10^n*Val(input)
在该范围内,正如Ben Voight当前的答案所说。
失败的尝试
由于Ben Voigt的评论而编辑:(我错过了他当前答案中的第一点:前缀匹配到最低限度是可以的。
根据@Ben Voigt的见解,我的解决方案是检查当前输入是否Min
StartsWith
。如果没有,则将当前输入0
PadRight
为字符串,Max
的长度。然后,如果此修改后的输入在Min
和Max
之间在词法上(即被视为字符串),则可以。
伪代码:
Confirm input has only digits, striping leading 0s
(most easily done by converting it to an integer and back to a string)
check = Str(min).StartsWith(input)
If Not check Then
testInput = input.PadRight(Len(Str(max)), '0')
check = Str(min) <= testInput && testInput <= Str(max)
End If
int input = 15;
int lower_bound = 1561;
int upper_bound = 1567;
int ipow = 0;
while (lower_bound > 0) {
if (lower_bound > input) {
++ipow;
lower_bound = lower_bound / 10;
} else {
int i = pow(10, ipow) * input;
if (i < upper_bound) {
return true;
}
return false;
}
}
return false;
- 为什么在全局范围内使用"extern int a"似乎不行?
- 错误:未在此范围内声明'reverse'
- 并行用于C++17中数组索引范围内的循环
- 求出有多少个数字是完美平方,而sqrt()是L,R范围内的素数
- 不计算一个范围内的完美数
- 错误:"imread"未在此范围内声明
- 我在范围内未声明的错误类有问题
- 如何在cpp中使用地图显示给定日期范围内(在下面的问题中)的费率?
- 我有一个数组,我想输入一个范围,然后找到范围内所有偶数的总和?
- 未在此范围内声明错误 'xy'
- 在C++中使用变量而不是"#define"来指定数组大小是不是一种糟糕的做法?(C错误:在文件范围内
- 命名空间范围内的外部 - GCC vs clang vs msvc
- 如何改进一堆在已知值范围内评估变量的 else-if 条件?
- 如何仅使用 While 循环在给定范围内找到可被 7 整除的计数整数
- Socklen_t未在此范围内声明
- 错误:'[' 之前预期的非限定 id 和错误:'users'未在此范围内声明
- 查找给定范围内最长连续 1 的频率
- "Main"已在当前范围内声明
- c++ 变量在宏的扩展中没有在这个范围内声明
- 变量不在 lambda 的范围内