內插搜尋 Interpolation search 為二元搜尋的變種,差別在於二分點的選擇方法,二元搜尋選擇中間的元素作為二分點,而內插搜尋則名副其實,以內插法找尋二分點。內插法有許多種類,本次搜尋演算法選擇使用常見的線性內插(linear interpolation)實作。
內插搜尋的特色如下:
- 資料需要是可計算內插(interpolation)的數值資料。
- 對資料分佈敏感,資料均勻分佈時,效能勝過二元搜尋。
- 資料分佈不均勻時,最差複雜度高達
$O(n)$ 。
- 確認資料已經排好序。
- 利用第一個元素 a 與最後的元素 b,以及搜尋上下界 hi 與 lo 位置,作為兩個端點。
- 利用上述兩點 (lo, a) 與 (hi, b),對搜尋目標計算內插,得到可能的位置。
- 若該位置上元素較小,則令其為新搜尋下界 a',重複步驟二到三,繼續求內插。
- 若該位置上元素較大,則令其為新搜尋上界 b',重複步驟二到三,繼續求內插。
- 若相等,則完成搜尋。
- 搜尋停止在 a'、b' 兩元素搜尋位置重疊,以及目標值比下界 a' 小或比上界 b' 大。
迅速說明線性內插法。線性內插法是中學必修的數學概念,給定兩點
接下來就是小學解方程式的事兒了。
回到正題,以下用文字解釋內插搜尋。
這裡有一個已排序有 14 個元素的序列,我們需要從中找出 27。
[1, 9, 10, 15, 17, 17, 18, 23, 27, 28, 29, 30, 31, 34]
我們將序列索引當作 x 軸,元素值作為 y 軸。可得已知兩點為
首先,透過斜率公式,計算出在
查看 arr[10]
為 29,比搜尋目標 27 來得大。將 29 當作我們新的上界,搜尋範變成第 [0, 9] 個元素(29 不需列入搜尋),繼續計算內插
查看 arr[9]
為 28,比搜尋目標 27 來得大。將 28 當作我們新的上界,搜尋範變成第 [0, 8] 個元素(28 不需列入搜尋),繼續計算內插
查看 arr[8]
為 27,恰恰是搜尋目標 27,搜尋到此結束。
Complexity | |
---|---|
Worst | |
Best | |
Average | |
Average |
|
Worst space |
$n$ :資料筆數
線性內差搜尋的最差時間複雜度為
線性內插搜尋對資料的期望是均勻機率分佈(uniform probability distribution)。想求平均時間複雜度
PDF of uniform distribution by IkamusumeFan - CC BY-SA 3.0
內插搜尋的實作共分為幾部分:
- 處理空序列狀況。
- 建立迴圈疊代共用的變數。
- 計算線性插值的主要迴圈。
- 將內插值映射到結果的
Result
。
首先是函式宣告。
pub fn interpolation_search(
arr: &[i32],
target: &i32,
) -> Result<usize, usize>
映入眼簾的是 i32
,而非泛型參數,為什麼呢?是因為內插搜尋為了計算線性內插,資料僅限定在「數值資料」,而 Rust 並沒有特別一類 Numeric 的型別,自己透過 trait 實作又異常繁瑣,因此先以 i32
代替。而回傳值的部分,與指數搜尋/二元搜尋一模一樣,回傳的 Result
- 若為
Ok
,其值代表目標值在序列內的索引位置; - 若為
Err
,則是可以將目標值插入序列內又不會破壞排序的位置。
延續數值型別的話題,Rust 社群提供 num crate,定義了各種數值型別與 trait,大整數、複數、虛數、有理數都囊括其中,非常有趣。
再來就是第一第二部分,處理空序列與建立共用變數,非常直觀。
if arr.is_empty() {
return Err(0)
}
let mut hi = arr.len() - 1;
let mut lo = 0_usize;
let mut interpolant = 0_usize;
hi
、lo
兩個變數劃定的搜尋範圍上下界。interpolant
儲存線性插值,代表每次疊代的搜尋位置。
接下來就是主要的迴圈,負責疊代計算內插值。分為三個部分,直接看程式碼先。
loop {
let lo_val = arr[lo];
let hi_val = arr[hi];
// 1.
if hi <= lo || *target < lo_val || *target > hi_val {
break
}
// 2. The linear interpolation part
let offset = (*target - lo_val) * (hi - lo) as i32 / (hi_val - lo_val);
interpolant = lo + offset as usize;
let mid_val = arr[interpolant];
// 3.
if mid_val > *target {
hi = interpolant - 1;
} else if mid_val < *target {
lo = interpolant + 1;
} else {
break
}
}
- 迴圈的三個終止條件,分別為:
hi
、lo
兩個變數劃定的搜尋範圍重疊,長度為零。- 搜尋目標值比上界還大。
- 搜尋目標值比下界還小。
- 線性內插的計算方程式,要注意我們是寫 Rust 不是 JavaScript,
i32
與usize
不能混用,要手動轉型。 - 比較插值與目標值。相等則跳出迴圈;若目標大於小於插值,則縮小搜尋範圍。注意,範圍需手動加減一,排除上下界,以免無限迴圈產生。
最後一部分則是決定線性插值所得的索引位置是否為目標值,並將該值映射到 Result
上。
if *target > arr[hi] {
Err(hi + 1)
} else if *target < arr[lo] {
Err(lo)
} else {
Ok(interpolant)
}
完整的程式碼如下。
pub fn interpolation_search(
arr: &[i32],
target: &i32,
) -> Result<usize, usize> {
// 1. Handle empty sequence.
if arr.is_empty() {
return Err(0)
}
// 2. Setup variable storing iteration informaion.
let mut hi = arr.len() - 1;
let mut lo = 0_usize;
let mut interpolant = 0_usize;
// 3. Main loop to calculate the interpolant.
loop {
let lo_val = arr[lo];
let hi_val = arr[hi];
// 3.1. Three condition to exit the loop
if hi <= lo || *target < lo_val || *target > hi_val {
break
}
// 3.2. The linear interpolation part
let offset = (*target - lo_val) * (hi - lo) as i32 / (hi_val - lo_val);
interpolant = lo + offset as usize;
let mid_val = arr[interpolant];
// 3.3. Comparison between the interpolant and targert value.
if mid_val > *target {
hi = interpolant - 1;
} else if mid_val < *target {
lo = interpolant + 1;
} else {
break
}
}
// 4. Determine whether the returning interpolant are equal to target value.
if *target > arr[hi] {
Err(hi + 1)
} else if *target < arr[lo] {
Err(lo)
} else {
Ok(interpolant)
}
}
Interpolation search tree(IST),姑且稱它「內插搜尋樹」,是一個將內插搜尋結合樹的資料結構。如上述提及,內插搜尋達到
-
$O(n)$ 空間複雜度。 - 預期有
$O(\log \log n)$ 的平攤增減節點操作時間,最差有$(O \log n)$ 。 - 在規律分佈的資料中,預期搜尋時間為
$O(\log \log n)$ ,最差時間複雜度則為$O((\log n)^2)$ - 線性時間的循序存取,而取得前後節點或最小值都是常數時間。
更多詳細證明可以閱讀參考資料「Andersson, A. (1996, October). Faster deterministic sorting and searching in linear space」。
- Wiki: Interpolation search
- Perl, Y., Itai, A., & Avni, H. (1978). Interpolation search—a log log N search. Communications of the ACM, 21(7), 550-553.
- Andersson, A. (1996, October). Faster deterministic sorting and searching in linear space. In Foundations of Computer Science, 1996. Proceedings., 37th Annual Symposium on (pp. 135-141). IEEE.
- Linear interpolation visualisation SVG By Cmglee CC BY-SA 3.0, via Wikimedia Commons.
- Probability density function of uniform distribution SVG By IkamusumeFan CC BY-SA 3.0, via Wikimedia Commons.