我在上次面试中遇到的一个问题是:

设计函数f,使得:f(f(n))==-n其中n是32位有符号整数;你不能用复数算术。如果你不能为整个数字范围设计这样的函数,那么就尽可能为最大范围设计它。

有什么想法吗?


当前回答

这是一个C/C++解决方案,它不使用任何按位运算符,也不需要任何数学库,尽管这有点作弊。。。

double f(double n)
{
    if (n == (double)(int)n)
        return n + 0.5;
    else
        return -(n - 0.5);
}

这适用于所有32位整数,只有一个异常0x80000000(因为它的相反值不能存储在32位整数系统中)。f(f(n))==-n将始终为真,除非在这种情况下。

不过,我相信有一种更简单、更快的方法来实现它。这只是我第一个想到的。

其他回答

由于C++中的重载:

double f(int var)
{
 return double(var);
} 

int f(double var)
{
 return -int(var);
}

int main(){
int n(42);
std::cout<<f(f(n));
}

我试着打高尔夫,这是罗德里克·查普曼的回答。

无分支:74个字符

int f(int i){return(-((i&1)<<1)|1)*i-(-((i>>>31)<<1)|1)*(((i|-i)>>31)&1);}

带有分支,Java风格:58个字符

int f(int i){return i==0?0:(((i&1)==0?i:-i)+(i>0?-1:1));}

带分支,C样式:52个字符

int f(int i){return i?(((i&1)?-i:i)+(i>0?-1:1)):0;}

经过快速但有效的基准测试后,分支版本在我的机器上的速度提高了33%。(正数和负数的随机数据集,足够的重复,并防止编译器在预热时优化代码。)考虑到非分支版本中的操作数量以及可能的良好分支预测,这并不奇怪,因为函数被调用了两次:f(f(i))。当我将基准更改为度量:f(I)时,分支版本只快28%。我认为这证明了分支预测在第一种情况下确实有一些好处。更多证明:当使用f(f(f)(f(i)))进行测试时,分支版本的速度会快42%。

在PHP中

function f($n) {
    if(is_int($n)) {
        return (string)$n;
    }
    else {
        return (int)$n * (-1);
    }
}

我相信你能理解其他语言的这种方法的精神。为了让不使用弱类型语言的人更清楚,我明确地将其转换回int。对于某些语言,您必须重载该函数。

这个解决方案的妙处在于,无论您是从字符串还是整数开始,它都是有效的,并且在返回f(n)时不会明显改变任何内容。

在我看来,面试官在问,“这位应聘者是否知道如何标记数据以供以后操作”,以及“这位应聘人员是否知道如何在最少更改数据的情况下标记数据?”你可以使用双精度、字符串或任何其他你想使用的数据类型来实现这一点。

这对所有负数都是正确的。

    f(n) = abs(n)

因为两个互补整数的负数比正数多一个,所以f(n)=abs(n)比f(n(n)=n>0-n:n溶液,与f(n)=-abs(n)相同。一个接一个…:D

更新

不,这对一个以上的案例无效,因为我刚从李布的评论中认识到。。。abs(Int.Min)将溢出。。。

我也想过使用mod 2信息,但得出的结论是,它不起作用。。。到早期。如果操作正确,它将适用于除Int.Min之外的所有数字,因为这将溢出。

更新

我玩了一段时间,寻找一个很好的位操作技巧,但我找不到一个很不错的单行线,而mod 2解决方案适合一个。

    f(n) = 2n(abs(n) % 2) - n + sgn(n)

在C#中,这变成了以下内容:

public static Int32 f(Int32 n)
{
    return 2 * n * (Math.Abs(n) % 2) - n + Math.Sign(n);
}

要使其适用于所有值,必须将Math.Abs()替换为(n>0)+n:-n,并将计算包含在未选中的块中。然后,您甚至可以像未检查的否定一样将Int.Min映射到自身。

更新

受另一个答案的启发,我将解释函数是如何工作的,以及如何构造这样的函数。

让我们从头开始。函数f被重复应用于给定值n,产生一系列值。

    n => f(n) => f(f(n)) => f(f(f(n))) => f(f(f(f(n)))) => ...

这个问题要求f(f(n))=-n,即f的两个连续应用否定这个论点。另外两次应用f-总共四次-再次否定论点,再次产生n。

    n => f(n) => -n => f(f(f(n))) => n => f(n) => ...

现在有一个明显的长度为4的循环。代入x=f(n),并注意所获得的方程式f(f(f)n))=f(f f(x))=-x成立,得出以下结果。

    n => x => -n => -x => n => ...

所以我们得到一个长度为4的循环,有两个数字,两个数字被取反。如果将循环想象为矩形,则取反的值位于相反的角落。

构建这样一个循环的许多解决方案之一是从n开始的以下方法。

 n                 => negate and subtract one
-n - 1 = -(n + 1)  => add one
-n                 => negate and add one
 n + 1             => subtract one
 n

一个具体的例子是这样一个循环:+1=>-2=>-1=>+2=>+1。我们快完成了。注意到所构造的循环包含一个奇数正数,它的偶数后继数,并且两个数都是负数,我们可以很容易地将整数划分为许多这样的循环(2^32是四的倍数),并找到了满足条件的函数。

但我们有一个零的问题。循环必须包含0=>x=>0,因为零对自身求反。因为循环状态已经是0=>x,所以它遵循0=>x=>0=>x。这只是一个长度为2的循环,x在两次应用后变为自身,而不是变为-x。幸运的是,有一个案例解决了这个问题。如果X等于零,我们得到一个长度为1的循环,它只包含零,我们解决了这个问题,得出结论,零是f的不动点。

完成?几乎我们有2^32个数字,零是留下2^32-1个数字的固定点,我们必须将这个数字分成四个数字的循环。糟糕的是,2^32-1不是四的倍数-在任何长度为四的循环中都会保留三个数字。

我将使用范围从-4到+3的较小的3位带符号iteger集来解释解决方案的其余部分。我们用零结束了。我们有一个完整的循环+1=>-2=>-1=>+2=>+1。现在让我们构建从+3开始的循环。

    +3 => -4 => -3 => +4 => +3

出现的问题是+4不能表示为3位整数。我们可以通过将-3减为+3来获得+4,这仍然是一个有效的3位整数,但然后将1加上+3(二进制011)得到100个二进制。它被解释为无符号整数,它是+4,但我们必须将它解释为有符号整数-4。因此实际上,本例中的-4或一般情况下的Int.MinValue是整数算术否定的第二个不动点-0和Int.MinValue映射到它们自己。所以循环实际上如下。

    +3 =>    -4 => -3 => -4 => -3

这是一个长度为2的循环,另外+3通过-4进入循环。因此,-4在两个函数应用程序之后正确映射到自身,+3在两个功能应用程序之后被正确映射到-3,但-3在两个应用程序之后错误映射到自身。

所以我们构造了一个函数,它适用于除1以外的所有整数。我们能做得更好吗?不,我们不能。为什么?我们必须构造长度为4的循环,并且能够覆盖多达四个值的整个整数范围。剩下的值是必须映射到自身的两个固定点0和Int.MinValue,以及必须由两个函数应用程序相互映射的两个任意整数x和-x。

为了将x映射到-x,反之亦然,它们必须形成一个四循环,并且必须位于该循环的相对角。因此,0和Int.MinValue也必须位于相反的角落。这将正确映射x和-x,但在两个函数应用程序之后交换两个固定点0和Int.MinValue,并留下两个失败的输入。因此,不可能构造一个适用于所有值的函数,但我们有一个适用所有值(除了一个值)的函数,这是我们所能达到的最佳效果。

const unsigned long Magic = 0x8000000;

unsigned long f(unsigned long n)
{    
    if(n > Magic )
    {
        return Magic - n;
    }

    return n + Magic;
} 

0~2^31