你真的瞭解模運算嗎?
問題
假設我們需要編寫一個字母表右移對映的程式(可能用於實現某種加密演算法),說起來似乎有些抽象,舉個例子便清晰了:
譬如字母表為 { ‘a’, ‘b’, ‘c’, ‘d’, ‘e’ }, 右移3位的話, 字母表便被對映為 { ‘d’, ‘e’, ‘a’, ‘b’, ‘c’ }
使用Lua,我們簡單編寫了以下程式碼
local code_table = { "a", "b", "c", "d", "e" }
local function get_map_code(code_index, shift_count)
local code_table_count = #code_table
local map_index = (code_index + shift_count - 1) % code_table_count + 1
return code_table[map_index]
end
for i = 1, #code_table do
print(get_map_code(i, 3))
end
現在我們需要擴充套件程式以支援字母表左移對映的需求,考慮到左移僅是右移的逆操作,我們只要改變shift_count的符號即可~
for i = 1, #code_table do
print(get_map_code(i, -3))
end
執行測試目測沒有問題,nice~
現在我們為了某些考慮(譬如效能),需要將程式碼移植至C/C++,移植完成後的程式碼如下:
const char codeTable[] = { 'a', 'b', 'c', 'd', 'e' };
const int codeTableCount = sizeof(codeTable) / sizeof(char);
auto getMapCode =
[&codeTable, codeTableCount](int codeIndex, int shiftCount)
{
auto mapIndex = (codeIndex + shiftCount) % codeTableCount;
return codeTable[mapIndex];
};
for (auto i = 0; i < codeTableCount; ++i)
{
cout << getMapCode(i, 3) << "\n";
}
程式的執行結果與Lua是一致的,但是當我們簡單的移植左移的時候,程式的結果卻出錯了……
for (auto i = 0; i < codeTableCount; ++i)
{
// error !!!
cout << getMapCode(i, -3) << "\n";
}
問題其實就出在模運算(%)上:
左移操作由於使用了負數的偏移,導致了負數取模運算,而對於負數取模,Lua和C/C++的結果是不一致的,進而導致了結果的偏差……
那麼到底Lua和C/C++中的負數取模有什麼不一樣呢?我們先從模運算的定義說起~
r = a - I(a / b) * b
其中a為除數,b為被除數,r即為模運算的結果,即餘數,而I(…)代表的是取整函式,取整函式不同,取模結果自然也就不同
對於Lua,I(…)使用的是向下取整(Floor)的方式,所以如果我們在Lua中計算-1 % 5 的話, 有:
r = -1 - Floor(-1 / 5) * 5 = -1 - (-1) * 5 = 4
而對於C/C++而言,I(…)使用的是截斷(Truncate)的方式,所以如果我們在C/C++中計算-1 % 5 的話, 有:
r = -1 - Truncate(-1 / 5) * 5 = -1 - (0) * 5 = -1
由於模運算的結果為負導致索引出界,自然程式的結果也就不會正常了~
知道了程式出錯的原因,“修復”起來也就有了對策,方法很簡單,自己實現一個使用Floor的取模運算即可~
const char codeTable[] = { 'a', 'b', 'c', 'd', 'e' };
const int codeTableCount = sizeof(codeTable) / sizeof(char);
auto module =
[](int dividend, int divisor) -> int
{
return dividend - floor((float)dividend / (float)divisor) * divisor;
};
auto getMapCode =
[&codeTable, codeTableCount, &module](int codeIndex, int shiftCount)
{
auto mapIndex = module(codeIndex + shiftCount, codeTableCount);
return codeTable[mapIndex];
};
for (auto i = 0; i < codeTableCount; ++i)
{
cout << getMapCode(i, -3) << "\n";
}
值得一提的是如果你使用Lua中math.fmod來計算 -1 % 5 的話,結果和C/C++中是一致的,為 -1
總結
模運算看似簡單,但其實大家不一定真正瞭解,這裡有一段Python中關於模運算怎麼實現(同Lua一樣,也使用了Floor取整)的討論,有興趣的朋友可以看下~
OK,下次再見吧~