分塊查詢演算法完全攻略(原理、實現及時間複雜度)
阿新 • • 發佈:2018-12-23
一般對於需要查詢的待查資料元素列表來說,如果很少變化或者幾乎不變,則我們完全可以通過排序把這個列表排好序以便我們以後查詢。但是對於經常增加資料元素的列表來說,要是每次增加資料都排序的話,那真的是有點太累人了。
所以之前我們分析過,對於幾乎不變的資料列表來說,排序之後使用二分查詢是很不錯的,但是對於經常變動的資料元素列表來說,每次排序後再使用二分查詢則不是很好的選擇。
分塊查詢是結合二分查詢和順序查詢的一種改進方法。在分塊查詢裡有索引表和分塊的概念。索引表就是幫助分塊查詢的一個分塊依據,其實就是一個數組,用來儲存每塊的最大儲存值,也就是範圍上限;分塊就是通過索引表把資料分為幾塊。
在每需要增加一個元素的時候,我們就需要首先根據索引表,知道這個資料應該在哪一塊,然後直接把這個資料加到相應的塊裡面,而塊內的元素之間本身不需要有序。因為塊內無須有序,所以分塊查詢特別適合元素經常動態變化的情況。
分塊查詢只需要索引表有序,當索引表比較大的時候,可以對索引表進行二分查詢,鎖定塊的位置,然後對塊內的元素使用順序查詢。這樣的總體效能雖然不會比二分查詢好,卻比順序查詢好很多,最重要的是不需要數列完全有序。
分塊查詢要求把一個數據分為若干塊,每一塊裡面的元素可以是無序的,但是塊與塊之間的元素需要是有序的。對於一個非遞減的數列來說,第i塊中的每個元素一定比第i-1塊中的任意元素大。同時,分塊查詢需要一個索引表,用來限定每一塊的範圍。在增加、刪除、查詢元素時都需要用到。
如圖 1 所示是一個已經分好塊的資料,同時有個索引表,現在我們要在資料中插入一個元素,該怎麼做呢?
圖 1 分塊的插入
首先,我們看到索引表是 10、20、30,對於元素 15 來說,應該將其放在分塊 2 中。於是,分塊 2 的資料變為 12、18、15、12、15,直接把 15 插入分塊 2 的最後就好了。
接下來是查詢操作。如果要查詢圖 1 中的 27 這個數,則首先該怎麼做呢?通過二分查詢索引表,我們發現 27 在分塊 3 裡,然後在分塊 3 中順序查詢,得到 27 存在於數列中。
現在分塊查詢的操作步驟很明瞭,我們接下來寫一下分塊查詢的程式碼實現:
下面是測試程式碼,用來檢驗查詢的正確性:
分塊查詢由於只需要索引表有序,所以特別適合用於在動態變化的資料元素序列中查詢。但是如何分塊比較複雜。如果分塊過於稀疏,則可能導致每一塊的內容過多,在順序查詢時效率很低;如果分塊過密,則又會導致塊數很多,無論是插入還是刪除資料,都會頻繁地進行二分查詢;如果塊數特別多,則基本上和直接二分查詢的動態插入資料類似,這樣分塊查詢就沒有意義了。
所以對於分塊查詢來說,可以根據資料量的大小及資料的區間來進行對分塊的選擇。二分查詢的平均查詢長度近似 log2(n+1)-1,這裡的n是塊數;順序查詢的平均查詢長度為 (n+1)/2,這裡的 n 是每塊的個數。
儘量等分為固定的塊,假設塊數為 a,每個塊內的元素數量為 b,則 b=n/a,那麼接下來就好辦了,如果給定一個數據量 n 進行分塊,則總的平均查詢長度為 (b+1)/2+log2(a+1)-1,這樣就可以解出 a 和 b 分別為多少了。
分塊查詢有點類似於雜湊表,但又不如散列表好用,其實很多時候我們在程式設計中並不會直接用到這個演算法,但是分塊的思想在很多時候還是很有用的。
但是我們經常用到彙總表。舉個例子,我們要統計一個視訊網站中每個使用者的觀看行為,即每個使用者分別觀看每個視訊多長時間。這個記錄量很大。怎麼處理呢?如果把每一個這樣的記錄都記錄到一個表裡,那就真的太恐怖了,一天可能有幾千萬的量,統計的時間長一些,通過資料庫就查不出來了。
於是我們一般會根據具體的業務量做分表。可能一天一個表,具體的表名可能是 t_user_watch_xxx_20160211,表示這張表是 2016 年 2 月 11 日的記錄表。在做儲存和查詢的時候就可以按照時間去做一個表的分塊,再查詢詳細的記錄了。其實這裡用到的就是分塊的思想。
所以之前我們分析過,對於幾乎不變的資料列表來說,排序之後使用二分查詢是很不錯的,但是對於經常變動的資料元素列表來說,每次排序後再使用二分查詢則不是很好的選擇。
什麼是分塊查詢
由於上面所提到的,對於需要經常增加或減少資料的資料元素列表,我們每次增加或減少資料之後排序,或者每次查詢前排序都不是很好的選擇,這樣無疑會增加查詢的複雜度,在這種情況下可以採用分塊查詢。分塊查詢是結合二分查詢和順序查詢的一種改進方法。在分塊查詢裡有索引表和分塊的概念。索引表就是幫助分塊查詢的一個分塊依據,其實就是一個數組,用來儲存每塊的最大儲存值,也就是範圍上限;分塊就是通過索引表把資料分為幾塊。
在每需要增加一個元素的時候,我們就需要首先根據索引表,知道這個資料應該在哪一塊,然後直接把這個資料加到相應的塊裡面,而塊內的元素之間本身不需要有序。因為塊內無須有序,所以分塊查詢特別適合元素經常動態變化的情況。
分塊查詢只需要索引表有序,當索引表比較大的時候,可以對索引表進行二分查詢,鎖定塊的位置,然後對塊內的元素使用順序查詢。這樣的總體效能雖然不會比二分查詢好,卻比順序查詢好很多,最重要的是不需要數列完全有序。
分塊查詢的原理及實現
如圖 1 所示是一個已經分好塊的資料,同時有個索引表,現在我們要在資料中插入一個元素,該怎麼做呢?
圖 1 分塊的插入
首先,我們看到索引表是 10、20、30,對於元素 15 來說,應該將其放在分塊 2 中。於是,分塊 2 的資料變為 12、18、15、12、15,直接把 15 插入分塊 2 的最後就好了。
接下來是查詢操作。如果要查詢圖 1 中的 27 這個數,則首先該怎麼做呢?通過二分查詢索引表,我們發現 27 在分塊 3 裡,然後在分塊 3 中順序查詢,得到 27 存在於數列中。
現在分塊查詢的操作步驟很明瞭,我們接下來寫一下分塊查詢的程式碼實現:
import me.irfen.algorithm.ch01.ArrayList; public class BlockSearch { private int[] index; private ArrayList[] list; /** * 初始化索引表 * @param index */ public BlockSearch(int[] index) { if (index != null && index.length != 0) { this.index = index; this.list = new ArrayList[index.length]; for (int i = 0; i < list.length; i++) { list[i] = new ArrayList(); } } else { throw new Error("index cannot be null or empty."); } } /** * 插入元素 * @param value */ public void insert(int value) { int i = binarySearch(value); list[i].add(value); } /** * 查詢元素 * @param data * @return */ public boolean search(int data) { int i = binarySearch(data); for (int j = 0; j < list[i].size(); j++) { if (data == list[i].get(j)) { return true; } } return false; } /** * 列印每塊元素 */ public void printAll() { for (int i = 0; i < list.length; i++) { ArrayList l = list[i]; System.out.println("ArrayList " + i + ":"); for (int j = 0; j < l.size(); j++) { System.out.println(l.get(j)); } } } /** * 二分查詢定位索引位置 * @param data 要插入的值 * @return */ private int binarySearch(int data) { int start = 0; int end = index.length; int mid = -1; while (start <= end) { mid = (start + end) / 2; if (index[mid] > data) { end = mid - 1; } else { // 如果相等,也插入在後面 start = mid + 1; } } return start; } }
public class BlockSearchTest { public static void main(String[] args) { int[] index = {10, 20, 30}; BlockSearch blockSearch = new BlockSearch(index); blockSearch.insert(1); blockSearch.insert(12); blockSearch.insert(22); blockSearch.insert(9); blockSearch.insert(18); blockSearch.insert(23); blockSearch.insert(5); blockSearch.insert(15); blockSearch.insert(27); blockSearch.printAll(); System.out.println(blockSearch.search(18)); System.out.println(blockSearch.search(29)); } }
分塊查詢的特點與效能分析
分塊查詢的特點其實顯而易見,那就是分塊查詢擁有順序查詢和二分查詢的雙重優勢,即順序查詢不需要有序,二分查詢的速度快。分塊查詢由於只需要索引表有序,所以特別適合用於在動態變化的資料元素序列中查詢。但是如何分塊比較複雜。如果分塊過於稀疏,則可能導致每一塊的內容過多,在順序查詢時效率很低;如果分塊過密,則又會導致塊數很多,無論是插入還是刪除資料,都會頻繁地進行二分查詢;如果塊數特別多,則基本上和直接二分查詢的動態插入資料類似,這樣分塊查詢就沒有意義了。
所以對於分塊查詢來說,可以根據資料量的大小及資料的區間來進行對分塊的選擇。二分查詢的平均查詢長度近似 log2(n+1)-1,這裡的n是塊數;順序查詢的平均查詢長度為 (n+1)/2,這裡的 n 是每塊的個數。
儘量等分為固定的塊,假設塊數為 a,每個塊內的元素數量為 b,則 b=n/a,那麼接下來就好辦了,如果給定一個數據量 n 進行分塊,則總的平均查詢長度為 (b+1)/2+log2(a+1)-1,這樣就可以解出 a 和 b 分別為多少了。
分塊查詢的適用場景
其實分塊查詢有很多可用之處。一個現實場景就是,有些年級在有大型考試的時候,都是隨機分配每個人的考試座位的,交考試卷的時候試卷上的名字、班級是有封條的,一個年級的所有試卷最終是一些老師一起改的。在得出最終分數之後,才拆開所有的封條,按照班級來分配試卷。這裡的“按照班級”也就是分塊。分塊查詢有點類似於雜湊表,但又不如散列表好用,其實很多時候我們在程式設計中並不會直接用到這個演算法,但是分塊的思想在很多時候還是很有用的。
但是我們經常用到彙總表。舉個例子,我們要統計一個視訊網站中每個使用者的觀看行為,即每個使用者分別觀看每個視訊多長時間。這個記錄量很大。怎麼處理呢?如果把每一個這樣的記錄都記錄到一個表裡,那就真的太恐怖了,一天可能有幾千萬的量,統計的時間長一些,通過資料庫就查不出來了。
於是我們一般會根據具體的業務量做分表。可能一天一個表,具體的表名可能是 t_user_watch_xxx_20160211,表示這張表是 2016 年 2 月 11 日的記錄表。在做儲存和查詢的時候就可以按照時間去做一個表的分塊,再查詢詳細的記錄了。其實這裡用到的就是分塊的思想。