1. 程式人生 > >分塊查詢演算法完全攻略(原理、實現及時間複雜度)

分塊查詢演算法完全攻略(原理、實現及時間複雜度)

一般對於需要查詢的待查資料元素列表來說,如果很少變化或者幾乎不變,則我們完全可以通過排序把這個列表排好序以便我們以後查詢。但是對於經常增加資料元素的列表來說,要是每次增加資料都排序的話,那真的是有點太累人了。

所以之前我們分析過,對於幾乎不變的資料列表來說,排序之後使用二分查詢是很不錯的,但是對於經常變動的資料元素列表來說,每次排序後再使用二分查詢則不是很好的選擇。

什麼是分塊查詢

由於上面所提到的,對於需要經常增加或減少資料的資料元素列表,我們每次增加或減少資料之後排序,或者每次查詢前排序都不是很好的選擇,這樣無疑會增加查詢的複雜度,在這種情況下可以採用分塊查詢。

分塊查詢是結合二分查詢和順序查詢的一種改進方法。在分塊查詢裡有索引表和分塊的概念。索引表就是幫助分塊查詢的一個分塊依據,其實就是一個數組,用來儲存每塊的最大儲存值,也就是範圍上限;分塊就是通過索引表把資料分為幾塊。

在每需要增加一個元素的時候,我們就需要首先根據索引表,知道這個資料應該在哪一塊,然後直接把這個資料加到相應的塊裡面,而塊內的元素之間本身不需要有序。因為塊內無須有序,所以分塊查詢特別適合元素經常動態變化的情況。

分塊查詢只需要索引表有序,當索引表比較大的時候,可以對索引表進行二分查詢,鎖定塊的位置,然後對塊內的元素使用順序查詢。這樣的總體效能雖然不會比二分查詢好,卻比順序查詢好很多,最重要的是不需要數列完全有序。

分塊查詢的原理及實現

分塊查詢要求把一個數據分為若干塊,每一塊裡面的元素可以是無序的,但是塊與塊之間的元素需要是有序的。對於一個非遞減的數列來說,第i塊中的每個元素一定比第i-1塊中的任意元素大。同時,分塊查詢需要一個索引表,用來限定每一塊的範圍。在增加、刪除、查詢元素時都需要用到。

如圖 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 日的記錄表。在做儲存和查詢的時候就可以按照時間去做一個表的分塊,再查詢詳細的記錄了。其實這裡用到的就是分塊的思想。