空間換時間,查表法的經典例子
前言
上一篇分享了:C語言精華知識:表驅動法程式設計實踐
這一篇再分享一個查表法經典的例子。
我們怎麼衡量一個函式/程式碼塊/演算法的優劣呢?這需要從多個角度看待。本篇筆記我們先不考慮程式碼可讀性、規範性、可移植性那些角度。
在我們嵌入式中,我們需要根據實際資源的情況來設計我們的程式碼。比如當我們能用的儲存器空間極其有限的情況,我之前就有遇到這樣子的情況,我能用的flash空間只有4KB,但是要實現的功能很多,稍微不注意就會超了,這種情況下我們就得多考慮程式佔用方面的問題。如果我們的儲存器空間很足,有時候可以犧牲一些儲存器空間來換取我們程式的執行速度。查表法就是以空間換取時間
的典型例子。下面看一個經典的例子:
基礎例子
編寫程式統計一個4bit資料(0x0~0x0F)中1的個數。這裡提供兩種方法:
1、方法一:常規法
常規法就是依次判斷這個4bit的資料的每一位是否為1,並用一個計數變數把1的個數記錄下來:
#include <stdio.h> /* 測試結果 */ struct test_res { unsigned int data; /* 資料 */ unsigned int count; /* 資料中1的個數 */ }; struct test_res get_test_res(unsigned int data) { /* 儲存測試結果 */ struct test_res res; /* 保證資料總會在0~0xf之間 */ unsigned int temp = data & 0xf; res.count = 0; res.data = temp; /* 迴圈判斷每一位 */ for (int i = 0; i < 4; i++) { if (temp & 0x01) { res.count++; } temp >>= 1; } return res; } int main(void) { struct test_res res = {0}; for (int i = 0; i < 32; i++) { res = get_test_res(i); printf("%2d中二進位制位為1的個數有%d\n", res.data, res.count); } return 0; }
執行結果:
unsigned int temp = data & 0xf;
語句就是為了保證資料都是在0x00xf之間,即015為一個週期,如果輸入的資料為16,則當做0來看待,輸入的資料為17,則當做1來看待……
2、方法二:查表法
這個例子也可以用查表法來做,把0x0~0xF中的所有資料中每個資料的1的個數都記錄下來,存放到一個表中。這樣一來,資料
與資料中1的個數
就建立起了一一對應關係,我們就可以通過陣列索引來獲取我們想要的結果:
int table[16] = {0, 1, 1, 2, 1, 2, 2, 3, 1, 2, 2, 3, 2, 3, 3, 4}; struct test_res get_test_res(unsigned int data) { /* 儲存測試結果 */ struct test_res res; /* 保證資料總會在0~0xf之間 */ unsigned int temp = data & 0xf; /* 獲取結果 */ res.data = temp; res.count = table[temp]; return res; }
常規法使用for迴圈的方式來實現,缺點是佔用了不少處理器的時間;查表法的優點彌補了常規法的不足,但是額外佔用了一些靜態空間。這裡針對這個應用而言處理的資料還是比較簡單的,資料範圍只是0x0~0xF之間,所以這兩種方式可能也都差不多。
那如果以上題目稍微改一下:編寫程式統計一個8bit、16bit資料中1的個數。查表法換取的時間就比較明顯了。
延伸例子
下面我們先來看一下編寫程式統計一個8bit(0x0~0xFF)資料中1的個數
的情況。
1、常規法
把以上程式碼稍微改一下就可以:
struct test_res get_test_res(unsigned int data)
{
/* 儲存測試結果 */
struct test_res res;
/* 保證資料總會在0~0xf之間 */
unsigned int temp = data & 0xff;
res.count = 0;
res.data = temp;
/* 迴圈判斷每一位 */
for (int i = 0; i < 16; i++)
{
if (temp & 0x01)
{
res.count++;
}
temp >>= 1;
}
return res;
}
執行結果:
2、查表法
上面的資料範圍僅僅是0x0~0xF
,資料量比較少,建立資料表也比較容易。這裡的資料量範圍變成了0x0~0xFF
,比原來多了兩百多個數據,這也還可以接受,也還可以全都列出來。
但是針對這裡的這個問題有更好的方法:
在這個問題中,8bit的資料可以看做兩個4bit資料,這樣就可以共用上面4bit資料的資料表。所以我們只要把2個4bit資料的1的個數相加,就是最後的結果。
獲取8bit資料1的個數:
struct test_res get_test_res(unsigned int data)
{
/* 儲存測試結果 */
struct test_res res;
/* 保證資料總會在0~0xf之間 */
unsigned int temp = data & 0xff;
/* 獲取低4位中1的個數 */
unsigned int low_data = temp & 0xf;
unsigned int low_cnt = table[low_data];
/* 獲取高4位中1的個數 */
unsigned int high_data = (temp >> 4) & 0xf;
unsigned int high_cnt = table[high_data];
/* 結果 */
res.count = low_cnt + high_cnt;
res.data = temp;
return res;
}
同樣的,獲取16bit資料也是類似的,把16bit資料當做4個4bit資料。
針對以上這個查表法的例子我們可以總結出:
1、資料表的確定要合適。像上面8bit的情況再重新建立一個數據表把表元素列出來也還可以接受。但是如果是16bit這樣子大資料的情況,建立這麼大的資料表也不太現實。所以需要考慮如何建立一個合適的資料表。
2、需要權衡空間換取時間是否值得。像16bit這樣子大資料的情況,全部列出來的話會大幅度的增加我們的儲存開銷,這種以空間換時間的情況可能會得不償失。
以上就是本次的分享。