不用判断语句,求两个数的最大值
Hash 思想
1. 首先引进一种简单的思路:
假设我们对两个无符号整型进行求最大值的操作:
#define BIT_SIZE (sizeof(int) * 8)
int max_ui32(unsigned int a, unsigned int b)
{
int hash[2] = {a, b} ;
int index = (a - b) >> (BIT_SIZE-1) & 1 ; // a <=> b : index
// >= : 0
// < : 1
return hash[index] ;
}此方法的精髓在于, 两个无符号整型的减法会导致符号位的变化.
当 a >= b 时, 结果为正数, 因此符号位为 0.
当 a < b 时, 结果为负数, 因此符号位为 1.
显然, 此方法只是用于无符号整型, 更准确的说, 应该是只适用于同号的运算. 因为存在溢出问题(例如当一个很大的正数减去一个很小的负数, 如2000000000 和 -2123456789相减, 会导致结果向符号位进位, 从而溢出), 所以不能适应异号的运算.
2. 以下办法我个人认为也是应用了 hash 的思想, 只不过此程序通过枚举形成了一个映射, 从而处理了异号之间的问题. 适用于任何符号.
// 引用他人程序
#define signed_bit(x) (( (x) & 0x80000000) >> 31)
#define value_stuff(x) ( x & 0x7FFFFFFF)
#define value_diff(x, y) signed_bit( value_stuff(x) - value_stuff(y) )
int Max( int x, int y)
{
int nums[2][2][2] =
{
x, //000
y, //001
x, //010
x, //011
y, //100
y, //101
x, //110
y //111
};
int idx0 = signed_bit(x);
int idx1 = signed_bit(y);
int idx2 = value_diff(x, y);
return nums[idx0][idx1][idx2];
}
将一个int分成两部分,最高位为符号位,剩下的31位为负载部分。
符号位通过 signed_bit 得出,为 0(如果>=0) 或者为1(< 0)
负载部分通过 value_stuff 得出,介于0到0x7FFFFFFF之间,总为正数。
value_diff宏将两个数的负载部分进行减法运算(由于负载在0-0x7FFFFFFF之间且总为正数,因此不可能溢出),为0表示计算后的结果为正数,则x的负载大于或者等于y,否则x的负载小于y。
然后预先建立一个3维数组, 建立依据是逻辑运算:
对于的维分别代表:
signed_bit(x), signed_bit(y), value_diff(x, y)
然后给这个数组初始化:
x, //000 x为正数,y为正数,x负载>=y, 那么x为最大值
y, //001 x为正数,y为正数,x负载<y, 那么y为最大值
x, //010 x为正数,y为负数,那么x为最大值 (无需考虑负载部分)
x, //011 x为正数,y为负数,那么x为最大值(无需考虑负载部分)
y, //100 x为负数,y为正数,那么y为最大值(无需考虑负载部分)
y, //101 x为负数,y为正数,那么y为最大值(无需考虑负载部分)
x, //110 x为负数,y为负数,x负载>=y, 那么x为最大值
y //111 x为负数,y为负数,x负载<y, 那么y为最大值
剩下就是分别得到3个维的index,并返回数组中的值。
3. 还是一个应用 hash 的变种, 只不过这次变化在思路上有所不同, 因为有对于异号运算的处理, 所以能够适用于所有符号.
对于函数 max_same 不做过多解释, 请看第一个简单的示例.#include "stdafx.h"
#include <iostream>
using namespace std ;
#define BIT_SIZE (sizeof(int) * 8)
int max_same(int a, int b)
{
int hash[2] = {a, b} ;
int index = (a - b) >> (BIT_SIZE-1) & 1 ; // The 31-th bit is 0 if a >= b,
// is 1 if a < b in 2'comlement.
return hash[index] ;
}
int max_diff(int a, int b)
{
// One is non-gegative while other is negative.
// Of course non-negative is bigger than negative one.
int hash[2] = {a, b} ;
int index = a >> (BIT_SIZE - 1) & 1 ; // sign bit of a : index
// 0 : 0
// 1 : 1
return hash[index] ;
}
int max(int a, int b)
{
int hash[2] = {a, b} ;
int (*fun[2])(int a, int b) = {max_same, max_diff} ;
int index = (a ^ b) >> (BIT_SIZE - 1) & 1 ; // Is 1 if 'a' & 'b' are same sign,
// otherwise is 0.
return fun[index](a, b) ;
}
int _tmain(int argc, _TCHAR* argv[])
{
cout <<"max_same: " <<max_same(2000000000, -2000000000) <<endl ; // 溢出
cout <<"max_diff: " <<max_diff(2000000000, -2000000000) <<endl ;
cout <<"max: " <<max(2000000000, -2000000000) <<endl ; // 根据不同符号情况, 调用正确处理过程
cout <<"max: " <<max(2000000000, 0) <<endl ; // 根据相同符号情况, 调用正确处理过程
return 0;
}
对于函数 max_diff, 在逻辑上也是十分简单的: 一个负数和一个非负数比大小, 结果显然是非负数一定大于负数. 因此, 只要两个变量异号, 则凡是符号位是 1 的变量, 必定小于另一个. 因为没有减法操作, 从而避免了溢出的问题.
对于最终的函数 max, 则是通过异或运算判断两个变量是同号还是异号. 根据不同的情况调用相对应的 max_XXXX 函数, 得到正确的结果.
数学插值
接下来看一个, 这个方法我目前没明白是如何实现的, 待高手指点.
// 引用他人程序
new_max (int x, int y)
{
int xy, yx;
xy = ((x - y) >> 31) & 1;
yx = ((y - x) >> 31) & 1;
return xy * y + yx * x + (1 - xy - yx) * x;
}
这里还有一种更精妙的方法, 正所谓 "棋妙子无多".
对于函数 max_same: 在不考虑溢出的情况下,unsigned int z=((x-y)>>31)&1的值有两种可能,x>=y时为0,x<y时为1,所以当x>=y时(1-z)*x+z*y的值为x,当x<y时其值为y,所以(1-z)*x+z*y就是x,y的最大值。函数如下:
// 引用他人程序
int max_same(int x,int y)
{
unsigned int z;
z=((x-y)>>31)&1;
return (1-z)*x+z*y; // 各人认为, 这种数学方法与 hash 是殊途同归. 但是在使用方便程度上更胜一筹.
}
int max_diff(int x, int y)
{
unsigned int z;
z=(x>>31)&1;
return (1-z)*x + z*y;
}
int max(int x, int y)
{
unsigned int z;
z=((x^y)>>31)&1;
return (1-z)*max_same(x,y) + z*max_diff(x,y); // 这里极大的体现出了使用数学公式而非 hash 映射的简洁性.
// 使用 hash 则必须要使用函数指针.
}
对于函数 max_diff: 在考虑溢出的情况下,unsigned int z=((x^y)>>31)&1的值有两种可能,x、y同号时为0,x、y异号时为1。当x、y同号时x-y不会溢出,可参考1,得出最大值;当x、y异号时,取正的那个就是最大值。函数如下:
其它
此算法核心思想是通过异或运算, 找到最高位的 1 的位置, 利用此位置做除法, 将结果映射到 hash 中. 思想略显复杂, 此程序作者数学功底扎实.
// 引用他人程序
#define SHIFT (sizeof(int) * 8 - 1)
int max(int x, int y)
{
unsigned int m, n;
int ary[2] = {x, y};
m = x ^ y;
m = m & ~(m / 2) & ~(m / 4);
n = m | 0x01;
return ary[ ((x & n) + n / 2) / n ^ !(m >> SHIFT)];
}说明一下吧:
这个算法是企图对x、y按位比较,简单地说,如果不考虑+、-号,则在第一次出现两者的位不相同时,该位是 1 的值比较大,如
00001111111100001111000011110000
00001110111100111111110011111010
左数第8位上,前者是1,后者是0,所以前者比后者大。
对于有符号的情况我们在后面再分析。
1、首先,x^y把x、y中不相同的位置1了,假设x^y=0...01X...X,后面的X表示任意的0或1,这表示在1所在的位置之前的位x、y是相等的,在这一位上或者x为1、y为0;或者x为0、y为1;我们主要是要确定哪一个为1。可惜的是我们无法得到0...010...0这个数以及1所在的位号(不然就要用循环去确定,这不符合题目的要求)
2、设m=x^y,则当x > y时,x在该位上为1,所以x & m = 0...01X...X,所以(x & m) / m 大约为1(注意“大约”,因为有等于0的可能存在,我们等会儿把它排除掉);当x < y时,x & m = 0...00X...X,所以(x & m) / m = 0,现在我们的程序好象可以工作了。。。
3、但是,有一个BUG,如果m == 0呢?(x & m) / m会溢出!m = 0说明x=y,现在的处理是m = m | 0x01,即把最后一位置1,使m >= 1!
这样当m == 0时,我们不管(x & m) / m算出来的是个啥结果,反正哪个都一样(x == y嘛!),而m > 0时,最后一位是不是1不影响我们的计算结果了!
4、前面说过这里还有个BUG,即当x在该位上为1时,我们不能确定(x & m) / m 一定 = 1,因为尽管x & m在该位上也为1,但x & m还是有可能 < m的(还是感叹如果我们能得到0..010...0这个数就好了!)。现在的处理是m = m & ~( m / 2 ) & ~( m / 4),把m这个数变成0...0100X...X,算式也修正为((x & m) + m / 2) / m,这个可以满足我们的要求了
5、最后再来考虑一下有符号的情况
设t=((x & m) + m / 2) / m,f = m的第一位=m >> SHIFT
则当m = 0时,表示x、y同号,这时按上面的方式确定最大值
当m = 1时,x、y一正一负,t表示x首位,为1则x < 0,为0则x >= 0
总的说来:
(1)当f=0,t=0时最大值为y -----1
(2)当f=0,t=1时最大值为x -----0
(3)当f=1,t=0时最大值为x -----0
(4)当f=1,t=1时最大值为y -----1
显然是个同或运算, 可以表示为: t ^ !f
如果不让用!那就是t ^ (~f )
关于 m = m & ~(m / 2) & ~(m / 4);是把m变成:0...0100X...X这样的形式,即1后面有两个0. 至于为什么有两个 0, 是为了让 (x & m) + m/2 的时候, 不产生进位而避免溢出. (如果溢出将导致 ((x & m) + m/2) / m 的结果为 0).
于是当x在该位上为1时:
(x & m) + m / 2 >=0...0100...0 + 0...0010...0 = 0...0110...0 >=m
(x & m) + m / 2) <= 0...01001...1 + 0...001001...1 <= 0...011...10 < 2m
所以((x & m) + m / 2) / m = 1
而当x在该位上为0时:
(x & m) + m / 2 <= 0...00001...1 + 0....001001...1 = 0...0011...1 < m
所以((x & m) + m / 2) / m = 0
一点提升:
a ^ b 的最高位 ---- 判断符号是否相等, 对于异或操作的理解应为: 将不同的位置为 1.
a - b 的最高位 ---- 在符号相同的情况下判断大小
a 的最高位 ---- a 的符号
b 的最高位 ---- b 的符号