1. 程式人生 > >你真的瞭解模運算嗎?

你真的瞭解模運算嗎?

問題

假設我們需要編寫一個字母表右移對映的程式(可能用於實現某種加密演算法),說起來似乎有些抽象,舉個例子便清晰了:

譬如字母表為 { ‘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,下次再見吧~