1) 以一個數組作為存儲,但數據結構實際是一個二叉樹
2) 二叉樹的某個節點大於等於它的所有子節點,但同一個層次的左右(子)節點大小未知,所以該堆(數組)的第一個元素必然是最大值(當然最大值有可能會出現在多個元素)
3) 存儲方式為,如果數組的序號從1開始,那麼v(i)>=v(2i)以及v(2i+1),即序號為i的節點如果存在直接子節點(最多2個),那麼直接子節點是2i 和 2i+1(如果都存在的話)
比起普通的堆,Faiss的堆還附帶了另一個id數組
template <class C> inline void heap_heapify ( size_t k, typename C::T * bh_val, typename C::TI * bh_ids, const typename C::T * x = nullptr, const typename C::TI * ids = nullptr, size_t k0 = 0) { if (k0 > 0) assert (x);
if (ids) { for (size_t i = 0; i < k0; i++) heap_push<C> (i+1, bh_val, bh_ids, x[i], ids[i]); } else { for (size_t i = 0; i < k0; i++) heap_push<C> (i+1, bh_val, bh_ids, x[i], i); }
for (size_t i = k0; i < k; i++) { bh_val[i] = C::neutral(); bh_ids[i] = -1; }
}
輸入
1) k 是需要初始化的堆的元素個數
2) bh_val 待初始化的堆所在數組,在faiss中指返回距離數組
3) bh_ids 一個附加返回,對應返回距離對應的id(id指來自與查詢比較的一個目標索引集合)
4) x 有可能需要定製的距離初始值,個數為k0, 默認為空
5) ids 有可能需要定製的距離x對應的元素id,個樹為k0,默認為空
6) k0 需要定製的距離的個數,默認為0
執行
1) 如果有需要定製的距離數組x和ids, 那麼先用定製數組初始化的堆bh_val以及對應的bh_ids ,默認是不需要的
2) 既然已經初始了k0個元素,那麼後面只需要初始化k-k0就可以了,默認k0 為0
3) 默認的初始化是指bh_val堆中所有元素值為std::numeric_limits<float>::max(),即3.40282347e+38,bh_ids ,默認全為-1
std::numeric_limits<float>::max()
初始化完畢後變成了
template <class C> inline void heap_pop (size_t k, typename C::T * bh_val, typename C::TI * bh_ids) { bh_val--; /* Use 1-based indexing for easier node->child translation */ bh_ids--; typename C::T val = bh_val[k]; size_t i = 1, i1, i2; while (1) { i1 = i << 1; i2 = i1 + 1; if (i1 > k) break; if (i2 == k + 1 || C::cmp(bh_val[i1], bh_val[i2])) { if (C::cmp(val, bh_val[i1])) break; bh_val[i] = bh_val[i1]; bh_ids[i] = bh_ids[i1]; i = i1; } else { if (C::cmp(val, bh_val[i2])) break; bh_val[i] = bh_val[i2]; bh_ids[i] = bh_ids[i2]; i = i2; } } bh_val[i] = bh_val[k]; bh_ids[i] = bh_ids[k]; }
1) 堆的大小,即數組的個數
2) bh_val 即需要處理的堆
3) bh_ids 堆中元素對應的id(id解釋見上文)
處理
1) 由於數組的實際存儲序號是從0開始,為了方便計算(原因見基本概念部分),首先將指針為前挪了一位
2) 此時bh_val[1]是最大的根節點,左子節點和右子節點分別為i<<1和i<<+1, 比較左子節點、右子節點、還有最後一個元素3者之間的大小
2.1) 如果左節點節點較大,那麼進入左子樹,否則進入右子樹
2.2) 通常情況下,根據最大堆的概念,最後一個元素< 較大的子樹,此時是需要一個騰挪的,所謂騰挪比如較大左子點分別是元素2,元素4,元素9,元素18,元素18是最後一個元素,那麼此時就需要元素2挪到根節點,元素4挪到元素2,元素9挪到元素4,元素18挪到元素9,所以通常情況下整個程序會不斷的沿著樹的較大子節點下探
2.3) 不知道為啥有2個break,通常情況下最後一個葉子節點肯定不會大於某個較大的葉子節點,所以這個 break不大會執行,除非數據問題
3) bh_ids 本身也不是堆,只是bh_vals對應的一個id數組,隨著bh_vals的變化就好
總之
功能是彈出堆的第一個元素,即堆中最大值,然後將堆中第二大值填充到第一個元素,再將其他元素逐個騰挪補位,直到最後一元素也騰挪完畢,此時最後一個元素已失去意義,為接下來的push做準備
template <class C> inline void heap_push (size_t k, typename C::T * bh_val, typename C::TI * bh_ids, typename C::T val, typename C::TI ids) { bh_val--; /* Use 1-based indexing for easier node->child translation */ bh_ids--; size_t i = k, i_father; while (i > 1) { i_father = i >> 1; if (!C::cmp (val, bh_val[i_father])) /* the heap structure is ok */ break; bh_val[i] = bh_val[i_father]; bh_ids[i] = bh_ids[i_father]; i = i_father; } bh_val[i] = val; bh_ids[i] = ids; }
4) val 待push的那個距離
5) id val對應的id
1) 堆指針前挪一位的解釋見上文
2) 由於最後一個值是無效的,所以push的那個值就需要和最後那個元素的對應的父節點,父節點的父節點......,直到根節點,看看比較適合放在哪個位置,比如還是以堆的大小是18,那麼最後一個元素是18, 依次往上的父子關係是1(v1),2(v2),4(v4),9(v9),18(無效值), 如果此時待插入的元素x在v2,v4之間,那麼插入x元素之後,會變成1(v1),2(v2),4(x),9(v4),18(v9), 堆中其他位置的元素不變
3) bh_ids的處理push
heap 初始值
{3.40282347e+38, 3.40282347e+38, 3.40282347e+38, 3.40282347e+38} {-1, -1, -1, -1}
1.輸入10.5144215
pop: {3.40282347e+38, 3.40282347e+38, 3.40282347e+38, 3.40282347e+38} {-1, -1, -1, -1}
push {3.40282347e+38, 3.40282347e+38, 3.40282347e+38, 10.5144215} {-1, -1, -1, 0}
2. 輸入0
pop: {3.40282347e+38, 3.40282347e+38, 10.5144215, 10.5144215} {-1, -1, 0, 0}
push: {3.40282347e+38, 3.40282347e+38, 10.5144215, 0} {-1, -1, 0, 1}
3.輸入10.2842665
pop: {3.40282347e+38, 0, 10.5144215, 0} {-1, 1, 0, 1}
push: {3.40282347e+38, 10.2842665, 10.5144215, 0} {-1, 2, 0, 1}
4.輸入15.4879131
pop: {10.5144215, 10.2842665, 0, 0} {0, 2, 1, 1}
push {15.4879131, 10.5144215, 0, 10.2842665} {3, 0, 1, 2}
5 .輸入12.4123116
pop: {10.5144215, 10.2842665, 0, 10.2842665} {0, 2, 1, 2}
push {12.4123116, 10.5144215, 0, 10.2842665} {4, 0, 1, 2}
6 輸入9.3830843
push {10.5144215, 10.2842665, 0, 9.3830843} {0, 2, 1, 5}
template <typename C> inline size_t heap_reorder (size_t k, typename C::T * bh_val, typename C::TI * bh_ids) { size_t i, ii;
for (i = 0, ii = 0; i < k; i++) { /* top element should be put at the end of the list */ typename C::T val = bh_val[0]; typename C::TI id = bh_ids[0];
/* boundary case: we will over-ride this value if not a true element */ heap_pop<C> (k-i, bh_val, bh_ids); bh_val[k-ii-1] = val; bh_ids[k-ii-1] = id; if (id != -1) ii++; } /* Count the number of elements which are effectively returned */ size_t nel = ii;
memmove (bh_val, bh_val+k-ii, ii * sizeof(*bh_val)); memmove (bh_ids, bh_ids+k-ii, ii * sizeof(*bh_ids));
for (; ii < k; ii++) { bh_val[ii] = C::neutral(); bh_ids[ii] = -1; } return nel; }
1) 核心部分是第一個for循環
2) pop出一個最大元素,並將最大元素到當前堆的最後一個位置(如上文提到由於pop之後,這個最後一個位置失去意義)
3) 每次pop都會縮小堆元素的範圍,不斷重複執行上一步
4) 第一個for循環是為了處理一些邊界條件,為了防止一些無效的id,一旦出現這種無效id,最後統一補齊
推薦閱讀: