資料結構與演算法:查詢演算法

小高飛發表於2020-09-29

查詢演算法

查詢( Search)是指從一批記錄中找出滿足指定條件的某一記錄的過程,查詢又稱為檢索。查詢演算法廣泛應用於各類應用程式中。因此,一個有效的查詢演算法往往可以大大提高程式的執行效率。在實際應用中,資料的型別千變萬化,每條資料項往往包含多個資料域。但是,在執行查詢操作時,往往只是指定一個或幾個域的值,這些作為查詢條件的域稱為關鍵字(Key),關鍵字分為兩類。

在實際應用中,針對不同的情況往往可以選擇不同的查詢演算法。對於無順序的資料,只有逐個比較資料,才能找到需要的內容,這種方法稱為順序查詢。對於有順序的資料,也可以採用順序查詢法逐個比較,但也可以採取其他更快速的方法找到所需資料。另外,對於一些特殊的資料結構,例如連結串列、樹結構和圖結構等,也都有相對應的合適的查詢演算法。

常見七大查詢演算法

  1. 順序查詢 2. 二分查詢 3. 插值查詢 4. 斐波那契查詢 5. 分塊查詢

  1. 雜湊查詢(涉及雜湊表結構) 7. 樹表查詢(涉及樹表結構)

 

1. 順序查詢

線性查詢又稱順序查詢,是一種最簡單的查詢方法,它的基本思想是從第一個記錄開始,逐個比較記錄的關鍵字,直到和給定的K值相等,則查詢成功;若比較結果與檔案中n個記錄的關鍵字都不等,則查詢失敗。

程式碼實現

/**
 * 順序查詢
 * @param array 查詢的陣列
 * @param value 查詢的值
 * @return 陣列內有和查詢值匹配的值則返回陣列內匹配值的下標,反之則返回-1
 */
public static int orderSearch(int[] array, int value){
    //遍歷陣列中的所有資料,當有能和value匹配的資料時,返回該資料的陣列下標
    for (int i=0; i<array.length; i++){
        if (array[i] == value){
            return i;
        }
    }
    //當陣列中沒有能和value匹配的資料時,返回-1
    return -1;
}

 

2. 二分查詢

二分查詢也稱折半查詢(Binary Search),它是一種效率較高的查詢方法。但是,折半查詢要求線性表必須採用順序儲存結構,而且表中元素按關鍵字有序排列

二分查詢的操作就像它的名字一樣,根據中間值將一個儲存結構一分為二,分為前段和後段,因為規定了儲存結構必須按關鍵字有序排序,所以只需將傳入的查詢值與中間值進行比較,根據比較結果再判斷是往前段查詢還是往後段查詢,反覆執行前面的操作,直到查詢到傳入的值或查詢完整個儲存結構。

資料結構與演算法:查詢演算法

程式碼實現

/**
 * 二分查詢(查詢的陣列必須是有序陣列)
 * @param array 查詢的陣列
 * @param left 中間值的左標(該次遞迴運算元組段的最左端下標)
 * @param right 中間值的右標(該次遞迴運算元組段的最右端下標)
 * @param findValue 查詢的值
 * @return 陣列內有和查詢值匹配的值則返回陣列內匹配值的下標,反之則返回-1
 */
public static int binarySearch(int[] array, int left, int right, int findValue){
    if (left > right){//判斷是否已經查詢到盡頭
        return -1;
    }

    int mid = (left + right)/2;//取中間值下標
    if (findValue > array[mid]){//如果findValue>array[mid] 則從中間值向右遞迴
        return binarySearch(array, findValue, mid+1, right);
    }else if (findValue < array[mid]){//如果findValue<array[mid] 則從中間值向左遞迴
        return binarySearch(array, findValue, left, mid);
    }else {//findValue==array[mid]時,則匹配成功,返回mid下標
        return mid;
    }
}

 

3. 插值查詢

插值查詢,有序表的一種查詢方式。插值查詢是根據查詢關鍵字與查詢表中最大最小記錄關鍵字比較後的查詢方法。插值查詢基於二分查詢,將查詢點的選擇改進為自適應選擇,提高查詢效率。插值類似於平常查英文字典的方法,在查一個以字母C開頭的英文單詞時,決不會用二分查詢,從字典的中間一頁開始,因為知道它的大概位置是在字典的較前面的部分,因此可以從前面的某處查起,這就是插值查詢的基本思想。

插值查詢除要求查詢表是順序儲存的有序表外,還要求資料元素的關鍵字在查詢表中均勻分佈,這樣,就可以按比例插值。

資料結構與演算法:查詢演算法

mid:查詢陣列段內的中間值

key:查詢的值

low:查詢陣列段內最小值的下標,即查詢陣列段內最左下標left

high:查詢陣列段內最大值的下標,即查詢陣列段內最右下標right

程式碼實現

/**
 * 插值查詢(查詢的陣列必須是有序陣列)
 * @param array 查詢的陣列
 * @param left 查詢段的最小值索引下標,即查詢段內最左下標
 * @param right 查詢段的最大值索引下標,即查詢段內最右下標
 * @param findValue 查詢的值
 * @return 陣列內有和查詢值匹配的值則返回陣列內匹配值的下標,反之則返回-1
 */
public static int insertSearch(int array[], int left, int right, int findValue){
    //當左標left大於右標right時,即查詢已經遍歷到陣列的盡頭,則返回-1
    //當查詢值findValue小於陣列內最小值或大於陣列內最大值時,即陣列內沒有能匹配的值,則返回-1
    if (left>right || findValue<array[0] || findValue>array[array.length-1]){
        return -1;
    }

    //使用插值公式求出中間值位置下標
    int mid = left + (right-left) * (findValue-array[left]) / (array[right]-array[left]);
    if (findValue < array[mid]){//當查詢值小於中間中間值時,向左遞迴查詢
        return insertSearch(array, left, mid-1, findValue);
    }else if (findValue > array[mid]){//當查詢值大於中間值時,向右遞迴查詢
        return insertSearch(array, mid+1, right, findValue);
    }else {//當查詢值等於中間值時,則返回中間值下標
        return mid;
    }
}

 

4. 斐波那契查詢

斐波那契查詢就是在二分查詢的基礎上根據斐波那契數列進行分割的。在斐波那契數列找一個等於略大於查詢表中元素個數的數f[n],將原查詢表擴充套件為長度為f[n],完成後進行斐波那契分割,即f[n]個元素分割為前半部分f[n-1]個元素,後半部分f[n-2]個元素,找出要查詢的元素在那一部分並遞迴,直到找到。

黃金分割:在介紹斐波那契查詢演算法之前,先了解根它緊密相連的一個概念——黃金分割。黃金比例又稱黃金分割,是指事物各部分間一定的數學比例關係,即將整體一分為二,較大部分與較小部分之比等於整體與較大部分之比,其比值約為1:0.618或1.618:1。0.618被公認為最具有審美意義的比例數字,這個數值的作用不僅僅體現在諸如繪畫、雕塑、音樂、建築等藝術領域,而且在管理、工程設計等方面也有著不可忽視的作用。因此被稱為黃金分割。斐波那契數列:1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89…….(從第三個數開始,後邊每一個數都是前兩個數的和)。然後我們會發現,隨著斐波那契數列的遞增,前後兩個數的比值會越來越接近0.618,利用這個特性,我們就可以將黃金比例運用到查詢技術中。

程式碼實現

/**
 * 斐波那契查詢(查詢陣列必須是有序陣列)
 * @param array 查詢的陣列
 * @param findValue 查詢的值
 * @return 陣列內有和查詢值匹配的值則返回陣列內匹配值的下標,反之則返回-1
 */
public static int fibonacciSearch(int array[], int findValue){
    int low = 0;//查詢段的最小值索引下標,即查詢段內最左下標
    int high = array.length-1;//查詢段的最大值索引下標,即查詢段內最右下標
    int mid = 0;//儲存中間值的下標
    int k = 0;//斐波那契陣列的下標索引
    int fib[] = fib();//斐波那契數列陣列

    /*
    獲取和查詢陣列最接近且大於查詢陣列最右下標的斐波那契陣列的值,以作為新陣列的長度
    因為斐波那契的值fib[k]對應的是陣列的長度,而右標high對應的是陣列下標,
    所以比較時,fib[k]的值需要-1
     */
    while (high > fib[k]-1){
        k++;
    }

    //將查詢的陣列複製到新的陣列,長度為前面求出的斐波那契陣列值fib[k]
    int temp[] = Arrays.copyOf(array, fib[k]);
    /*
    因為fib[k]不一定等於查詢陣列的長度,所以在最右下標high右邊,
    即超出查詢陣列的部分,把它們的值都設為最右下標的值temp[high]
     */
    for (int i=high+1; i<temp.length-1; i++){
        temp[i] = temp[high];
    }

    /*
    在斐波那契查詢中是按照斐波那契數列的黃金分割點來劃分查詢陣列的,
    而在斐波那契數列中 fib[k] = fib[k-1]+fib[k-2],
    所以可以把查詢陣列分成倆部分fib[k-1]和fib[k-2],因此
    黃金分割點,即中間值的下標mid = low+fib[k-1]-1,
    向左遞迴部分 = fib[k-1],向右遞迴部 = fib[k-2]
     */
    while (low <= high){//判斷是否查詢到盡頭
        /*
        low:隨著向右遞迴而發生變化,使得中間值可以跟著適應
        fib[k-1]-1:因為斐波那契數列陣列中的值對應的是陣列長度,所以需-1
         */
        mid = low + fib[k-1] -1;

        if (findValue < temp[mid]){//查詢值小於中間值,向左遞迴
            high = mid-1;//重新設定查詢陣列右標,設定新的查詢段
            k -=1; //因為是向左遞迴,對應的是fib[k-1],所以斐波那契陣列的下標k要-1
        }else if (findValue > temp[mid]){//查詢值大於於中間值,向右遞迴
            low = mid+1;//重新設定查詢陣列的左標,設定新的查詢段
            k -=2;//因為是向右遞迴,對應的是fib[k-2],所以斐波那契陣列的下標k要-2
        }else {//當查詢值等於中間值時,即匹配成功
            /*
            因為臨時陣列temp的長度可能超過原先的查詢陣列,而超出部分的值都等於右標位置的值,
            所以當匹配成功值的下標大於右標時,要返回右標high,反之則之間返回中間值下標mid即可
             */
            if (mid <= high){
                return mid;
            }else {
                return high;
            }
        }
    }
    //陣列內無匹配成功的值,則返回-1
    return -1;
}

//獲取斐波那契數列
public static int[] fib(){
    int fib[] = new int[20];
    fib[0] = 1;
    fib[1] = 1;
    for (int i=2; i<fib.length; i++){
        fib[i] = fib[i-1] + fib[i-2];
    }
    return fib;
}

 

5. 分塊查詢

分塊查詢是二分查詢和順序查詢的一種改進方法,二分查詢雖然具有很好的效能,但其前提條件時線性表順序儲存而且按照關鍵碼排序,這一前提條件在結點樹很大且表元素動態變化時是難以滿足的。而順序查詢可以解決表元素動態變化的要求,但查詢效率很低。如果既要保持對線性表的查詢具有較快的速度,又要能夠滿足表元素動態變化的要求,則可採用分塊查詢的方法。

分塊查詢的速度雖然不如折半查詢演算法,但比順序查詢演算法快得多,同時又不需要對全部節點進行排序。當節點很多且塊數很大時,對索引表可以採用折半查詢,這樣能夠進一步提高查詢的速度。

分塊查詢由於只要求索引表是有序的,對塊內節點沒有排序要求,因此特別適合於節點動態變化的情況。當增加或減少節以及節點的關鍵碼改變時,只需將該節點調整到所在的塊即可。在空間複雜性上,分塊查詢的主要代價是增加了一個輔助陣列。

分塊查詢操作:分塊,即把一個儲存結構分成多塊,每一塊都有一個關鍵字——該塊資料池中的最大值,和一個塊在原始儲存結構中的起始位置。將分後的各塊組合成一個索引表,儲存各塊的最大值和起始位置。通過查詢值和塊中的最大值進行比較,從而縮短需要查詢的原始儲存結構長度。

資料結構與演算法:查詢演算法

程式碼實現

public class BlockSearch {
    public static void main(String[] args) {
        int[] array = new int[50];
        for (int i=0; i<array.length; i++){
            array[i] = (int)(Math.random() * 100);
        }
        System.out.println(Arrays.toString(array));

        List<Block> blockTable = createBlockTable(array);
        System.out.println(blockSearch(array, 10, blockTable));
    }

    /**
     * 將陣列分成多塊,並建立塊索引表儲存各塊的最大值和起始位置
     * @param array 查詢的陣列
     * @return 返回塊索引表
     */
    public static List<Block> createBlockTable(int[] array){
        int nums = 10;//每塊儲存的資料數量
        int start = 0;//塊的起始位置,初始為0
        int maxValue = 0;//塊中的最大值
        List<Block> blockTable = new ArrayList<>();//塊索引表
        
        while (start < array.length){//判斷起始起始位置是否大於查詢陣列的長度
            maxValue = array[start];//假定塊中的最大值為起始位置的值
            //判斷剩餘的陣列長度是否小於每塊儲存的初始資料量,
            //如果大於則的塊的長度為初始資料量,如果大於則最後一塊的長度就是剩餘的陣列長度
            int maxLength = (start+nums) < array.length ? (start+nums) : array.length;
            //遍歷塊中的元素,選出塊中的最大值
            for (int i=start; i<maxLength; i++){
                if (maxValue < array[i]){
                    maxValue = array[i];
                }
            }
            //將塊中的最大值和起始位置新增進塊索引表
            //注意:塊中的資料與塊的索引對於查詢陣列來說是有序的,即是按照查詢陣列的原始順序來排序的
            blockTable.add(new Block(start, maxValue));
            //移動到下一塊的起始位置
            start += nums;
        }
        return blockTable;
    }

    /**
     * 分塊查詢
     * @param array 查詢陣列
     * @param findValue 查詢的值
     * @param blockTable 塊索引表
     * @return 陣列內有和查詢值匹配的值則返回陣列內匹配值的下標,反之則返回-1
     */
    public static int blockSearch(int[] array, int findValue, List<Block> blockTable){
        int blockIndex = 0;//塊索引表中塊的索引,按順序遍歷,初始值為0
        //判斷查詢值是否大於塊中的最大值,如果大於則移動到下一塊
        while (blockIndex < blockTable.size() && findValue > blockTable.get(blockIndex).maxValue){
            blockIndex++;
        }
        //當塊的索引大於塊索引表的大小時,即說明查詢值大於查詢陣列中的最大值,陣列中沒有查詢值
        if (blockIndex > blockTable.size()-1){
            return -1;
        }
        //從塊的起始位置開始在查詢陣列中進行查詢,這時查詢長度比順序查詢的長度縮短了start+1
        for (int i=blockTable.get(blockIndex).start; i<array.length; i++){
            //當從陣列中匹配到查詢值時,返回該匹配值在陣列中的下標
            if (array[i] == findValue){
                return i;
            }
        }
        //當查詢陣列中無與查詢值匹配的值時,返回-1
        return -1;

    }
}

//
class Block{
    int start;//起始位置
    int maxValue;//塊中的最大值

    public Block(int start, int maxValue) {
        this.start = start;
        this.maxValue = maxValue;
    }
}

 

相關文章