題目鏈接在:針對一群范圍對的最快查找算法設計(不要用數組),是我目前遇到的一個較棘手的問題。
描述如下:
假如有一群范圍對,格式為:<范圍表示,該范圍對應的結果值>,設計一個最快查找算法,使得給定一個值,輸出該值所在范圍對的結果值。
注意1:范圍對之間沒有交集,即不可能存在<1, 10>和<2, 11>這樣的兩個范圍對。注意2:各個區間不一定嚴格相鄰,也就是可能只有<1, 3>和<99, 201>這樣兩個區間,所以STL中的lower_bound不適用。
例如有以下幾個范圍對:
<<1, 2>, 20>
<<3, 37>, 27>
<<48, 57>, 28>
<<58, 63>, 27>
<<97, 128>, 122>
<<129, 149>, 12>
<<150, 189>, 13>
<<200, 245>, 14>
<<246, 256>, 129>
<<479, 560>, 12>假如給定一個數100,則根據題意應輸出122,因為100屬於范圍對<97, 128>
要求:不要用范圍對作為下標用數組來存儲,因為范圍對可能非常大。
對於這個問題,思考許久,有了下面幾個思路:
1. 用STL map來存儲這些范圍對(key)及對應的結果集(value),用map進行查找
范圍對定義如下:
class range { public: int from; int to; public: range(): from(-1), to(-1) {} range(int f, int t): from(f), to(t) {} };
map定義為:
typedef map<range*, int> range_map;
但這里有個問題,map的key是自定義類型,一般需要自定義比較函數才能進行查找,一般的自定義比較函數如下:
struct cmp_func { bool operator()(const range* lc,const range* rc) const { return (lc->from < rc->from) || (lc->from == rc->from && lc->to < rc->to); } };
但這樣的比較函數並不適用於我們的需求,因為我們要求查詢的並不是一個范圍對,即並不是查詢map中有沒有<3, 37>這樣的范圍對,而是要求給定一個值,查詢這個值屬於哪個范圍對,那么能不能自定義一個這樣的比較函數呢?以上面那個例子為例,如果我們查找35這個數,我們將35包裝成一個范圍對<35, 35>,然后查找它包含在map中的哪個范圍對,上面的例子是包含在<3, 37>這樣的范圍對,這樣就找到了,也就是兩個key相等,只要它們包含在同一個范圍對即可。這似乎有點奇怪,違背了通常意義上的比較含義(也就是兩個key相等,兩個key的組成部分都應該相同才是)。不管如何,這樣的比較函數還是比較簡單的,如下:
struct cmp_func { bool operator()(const range* lc,const range* rc) const { return lc->to < rc->from; } };
這樣就實現了我們用map的find函數來查找給定的一個數屬於哪個范圍對了。當然,這時我們的map定義就變成了:
typedef map<range*, int, cmp_func> range_map;
用map查找表面上看上去應該挺高效的,至少比一個個順序查找要快吧,但事實卻並非如此。我用未自定義比較函數的map順序查找和自定義上面比較函數的map find查找,結果卻發現用自定義比較函數后的效果並不好,竟然比順序查找還要慢,下面的粗糙的測試程序:
#include<iostream> #include<stdio.h> #include<map> #include<sys/time.h> using namespace std; class range { public: int from; int to; public: range(): from(-1), to(-1) {} range(int f, int t): from(f), to(t) {} }; struct cmp_func { bool operator()(const range* lc,const range* rc) const { return lc->to < rc->from; } }; typedef map<range*, int, cmp_func> range_map; int get_next1(range_map *rm, int c) { for(range_map::iterator it = rm->begin(); it != rm->end(); ++it) { if(c >= it->first->from && c <= it->first->to) return it->second; } return -1; // not found. } int get_next2(range_map *rm, int c) { range_map::iterator iter = rm->find(new range(c, c)); if(iter != rm->end()) return iter->second; return -1; // not found. } int main() { struct timeval t_begin, t_end; range_map *rm = new range_map(); rm->insert(pair<range*, int>(new range(1, 2), 20)); rm->insert(pair<range*, int>(new range(3, 37), 27)); rm->insert(pair<range*, int>(new range(48, 57), 28)); rm->insert(pair<range*, int>(new range(58, 63), 27)); rm->insert(pair<range*, int>(new range(97, 128), 122)); rm->insert(pair<range*, int>(new range(129, 149), 12)); rm->insert(pair<range*, int>(new range(150, 189), 12)); rm->insert(pair<range*, int>(new range(200, 245), 14)); rm->insert(pair<range*, int>(new range(246, 256), 129)); rm->insert(pair<range*, int>(new range(479, 560), 12)); gettimeofday(&t_begin,NULL); int result[256]; for(int c = 0; c < 256; c++) result[c] = get_next1(rm, c); gettimeofday(&t_end,NULL); double timeuse=1000000*(t_end.tv_sec-t_begin.tv_sec)+(t_end.tv_usec-t_begin.tv_usec); timeuse/=1000000; printf("\nget_next1 time use: %.12f\n", timeuse); for(int c = 0; c < 256; c++) cout << result[c] << " "; cout << endl; gettimeofday(&t_begin,NULL); for(int c = 0; c < 256; c++) result[c] = get_next2(rm, c); gettimeofday(&t_end,NULL); timeuse=1000000*(t_end.tv_sec-t_begin.tv_sec)+(t_end.tv_usec-t_begin.tv_usec); timeuse/=1000000; printf("\nget_next2 time use: %.12f\n", timeuse); for(int c = 0; c < 256; c++) cout << result[c] << " "; cout << endl; return 0; }
運行結果為:
get_next1 time use: 0.000124000000 get_next2 time use: 0.000144000000
當然這個例子並不能代表所有情況,且每次運行結果也不一樣,但從每次的運行結果來看,幾乎沒有一次是用自定義比較函數比順序查找情況好的。這至少說明了一點:我們的自定義比較函數讓map在查找時做了一些額外的工作,減慢了速度。比如我們為了使用map的find函數,不得不封裝我們的一個數為一個range對象,在查找的時候還得調用我們自定義的比較函數進行處理。
難道就只能順序查找嗎?在這個不靠譜的思路過后又萌生了另一個不靠譜的思路。
2. 使用二分查找的思想來查找范圍對
我們使用ranges和results這兩個數組來保存范圍對及對應的結果,按序保存,每兩個ranges數對應一個results里的數。
例如上面的例子保存為:
int ranges[] = {1, 2, 3, 37, 48, 57, 58, 63, 97, 128, 129, 149, 150, 189, 200, 245, 246, 256, 479, 560};
int results[] = {20, 27, 28, 27, 122, 12, 13, 14, 129, 12};
使用二分查找來查找某個數屬於哪個范圍對。那么如何查找呢?比如查找35屬於哪個范圍對,首先與最中間的128進行比較,35<128,這時候有兩種可能:
(1)100在128前半部分的數組里,即1, 2, 3, 37, 48, 57, 58, 63, 97;
(2)由於128是范圍對<97, 128>的第二部分,那么也有可能這個數屬於這個范圍對。
由於35不屬於這個范圍對,那么只有在97之前的部分找(不包括97),繼續二分即與37進行比較,35 < 37,與上類似,此時35屬於范圍對<3, 37>,也就是找到了。
再舉個例子,找130屬於哪個范圍對,同樣的先與128比較,130 > 128,這時候130只可能在128的后半部分而不需要判斷是否屬於范圍對<128, 129>,因為<128, 129>不是范圍對。怎么判斷是不是范圍對呢?很簡單,根據當前位置的奇偶性判斷即可。
下面是我寫的二分查找算法,及與map順序查找、數組順序查找的簡單對比試驗:
#include<iostream> #include<stdio.h> #include<map> #include<sys/time.h> using namespace std; class range { public: int from; int to; public: range(): from(-1), to(-1) {} range(int f, int t): from(f), to(t) {} }; typedef map<range*, int> range_map; int get_next1(range_map *rm, int c) { for(range_map::iterator it = rm->begin(); it != rm->end(); ++it) { if(c >= it->first->from && c <= it->first->to) return it->second; } return -1; // not found. }
// binary search int get_next2(int *ranges, int *results, int size, int c) { if(size <= 1) return -1; int start, end, mid; start = 0; end = size - 1; while(start <= end) { if(c < ranges[start] || c > ranges[end]) return -1; mid = start + (end - start) / 2; if(c == ranges[mid]) return results[mid / 2]; if(c < ranges[mid]) { if(mid % 2 == 1) { if(c >= ranges[mid - 1]) return results[mid / 2]; else end = mid - 2; } else end = mid - 1; } else { if(mid % 2 == 0) { if(c <= ranges[mid + 1]) return results[mid / 2]; else start = mid + 2; } else start = mid + 1; } } return -1; // not found. } int get_next3(int *ranges, int *results, int size, int c) { for(int i = 0; i < size;) { if(i % 2 == 0) { if(c >= ranges[i] && c <= ranges[i + 1]) return results[i / 2]; else if(c < ranges[i]) return -1; else i += 2; } } } int main() { struct timeval t_begin, t_end; range_map *rm = new range_map(); rm->insert(pair<range*, int>(new range(1, 2), 20)); rm->insert(pair<range*, int>(new range(3, 37), 27)); rm->insert(pair<range*, int>(new range(48, 57), 28)); rm->insert(pair<range*, int>(new range(58, 63), 27)); rm->insert(pair<range*, int>(new range(97, 128), 122)); rm->insert(pair<range*, int>(new range(129, 149), 12)); rm->insert(pair<range*, int>(new range(150, 189), 13)); rm->insert(pair<range*, int>(new range(200, 245), 14)); rm->insert(pair<range*, int>(new range(246, 256), 129)); rm->insert(pair<range*, int>(new range(479, 560), 12)); int ranges[] = {1, 2, 3, 37, 48, 57, 58, 63, 97, 128, 129, 149, 150, 189, 200, 245, 246, 256, 479, 560}; int results[] = {20, 27, 28, 27, 122, 12, 13, 14, 129, 12}; // int r = get_next2(ranges, results, 20, 65); // cout << r << endl; gettimeofday(&t_begin,NULL); int result[256]; for(int c = 0; c < 256; c++) result[c] = get_next1(rm, c); gettimeofday(&t_end,NULL); double timeuse=1000000*(t_end.tv_sec-t_begin.tv_sec)+(t_end.tv_usec-t_begin.tv_usec); timeuse/=1000000; printf("\nget_next1 time use: %.12f\n", timeuse); // for(int c = 0; c < 256; c++) // cout << result[c] << " "; // cout << endl; gettimeofday(&t_begin,NULL); for(int c = 0; c < 256; c++) result[c] = get_next2(ranges, results, 20, c); gettimeofday(&t_end,NULL); timeuse=1000000*(t_end.tv_sec-t_begin.tv_sec)+(t_end.tv_usec-t_begin.tv_usec); timeuse/=1000000; printf("\nget_next2 time use: %.12f\n", timeuse); // for(int c = 0; c < 256; c++) // cout << result[c] << " "; // cout << endl; gettimeofday(&t_begin,NULL); for(int c = 0; c < 256; c++) result[c] = get_next3(ranges, results, 20, c); gettimeofday(&t_end,NULL); timeuse=1000000*(t_end.tv_sec-t_begin.tv_sec)+(t_end.tv_usec-t_begin.tv_usec); timeuse/=1000000; printf("\nget_next3 time use: %.12f\n", timeuse); // for(int c = 0; c < 256; c++) // cout << result[c] << " "; // cout << endl; return 0; }
運行結果為:
get_next1 time use: 0.000302000000 get_next2 time use: 0.000043000000 get_next3 time use: 0.000165000000
說明二分查找算法還是挺高效的,順序查找也不錯,有時候表現的與二分查找差不多,這里的數據比較少,體現不出准確的對比,但至少可能說明二分查找算法比簡單的順序查找(map順序和數組順序查找)要快不少。
上面是自己的一點拙見,相信二分查找算法肯定不是最高效的算法,但目前實在想不出更好的辦法了。大家有想法的盡管提,不試試不知道算法好不好!