常見4種查找算法詳解以及Java代碼的實現

查找(Searching)就是根據給定的某個值,在查找表中確定一個其關鍵字等於給定值的數據元素(或記錄)。本文詳細介紹了常見的數據查找算法,比如順序查找/線性查找、二分查找/折半查找、插值查找、斐波那契查找等,並且提供了相應的Java代碼實現。

1 查找概述

查找表:
  所有需要被查的數據所在的集合,我們給它一個統稱叫查找表。查找表(Search Table)是由同一類型的數據元素(或記錄)構成的集合。
查找(Searching):
  根據給定的某個值,在查找表中確定一個其關鍵字等於給定值的數據元素(或記錄)。若表中存在這樣的一個記錄,則稱查找是成功的,此時查找的結果給出整個記錄的信息,或指示該記錄在查找表中的位置;若表中不存在關鍵字等於給定值的記錄,則稱查找不成功,此時查找的結果可給出一個“空”記錄或“空”指針。
查找表分類:
  查找表按照操作方式來分有兩大種:靜態查找表和動態查找表。
  靜態查找表(Static Search Table):只作查找操作的查找表。它的主要操作有:

  1. 查詢某個“特定的”數據元素是否在查找表中。
  2. 檢索某個“特定的”數據元素和各種屬性。

  動態查找表(Dynamic Search Table):在查找過程中同時插入查找表中不存在的數據元素,或者從查找表中刪除已經存在的某個數據元素。顯然動態查找表的操作就是兩個:

  1. 查找時插入數據元素。
  2. 查找時刪除數據元素。

查找結構:
  爲了提高查找的效率,我們需要專門爲查找操作設置數據結構,這種面向查找操作的數據結構稱爲查找結構。
  從邏輯上來說,查找所基於的數據結構是集合,集合中的記錄之間沒有本質關係。可是要想獲得較高的查找性能,我們就不能不改變數據元素之間的關係,在存儲時可以將查找集合組織成表、樹等結構。
  例如,對於靜態查找表來說,我們不妨應用線性表結構來組織數據,這樣可以使用順序查找算法,如果再對主關鍵字排序,則可以應用二分查找等技術進行高效的查找。
  如果是需要動態查找,則會複雜一些,可以考慮二叉排序樹的查找技術。
  另外,還可以用散列表結構來解決一些查找問題。

2 順序查找

  順序查找(Sequential Search)又叫線性查找,是最基本的查找技術,它的查找過程是:從表中第一個(或最後一個)記錄開始,逐個進行記錄的關鍵字和給定值比較,若某個記錄的關鍵字和給定值相等,則查找成功,找到所查的記錄;如果直到最後一個(或第一個)記錄,其關鍵字和給定值比較都不等時,則表中沒有所查的記錄,查找不成功。
  順序查找一般是一種在數組中查找數據的算法,是一種靜態查找。
順序查找的實現:
  順序查找非常簡單,就是從頭開始遍歷內部數組,查看有沒有關鍵字(key)。有的話就返回對應的索引。
  設數組元素數量爲n,則順序查找的查找成功最短時間爲O(1),最長爲O(n);查找失敗時間爲O(n)。記作O(n)。

3 二分查找

3.1 二分查找概述

  每次取中間記錄查找的方法叫做二分查找。二分查找也稱折半查找(Binary Search),它是一種效率較高的查找方法。但是,二分查找要求線性表必須採用順序存儲結構,而且表中元素按關鍵字有序排列。
  二分查找的基本思想是:在有序表中,取中間記錄作爲比較對象,若給定值與中間記錄的關鍵字相等,則查找成功;若給定值小於中間記錄的關鍵字,則在中間記錄的左半區繼續查找;若給定值大於中間記錄的關鍵字,則在中間記錄的右半區繼續查找。不斷重複上述過程,直到查找成功,或所有查找區域無記錄,查找失敗爲止。
  設有序數組元素數量爲n,將其長度減半log n 次後,其中便只剩一個數據了,這樣就能百分之百確定元素是否存在,則二分查找的查找成功最短時間爲O(1),最長爲O(logn)。查找失敗最短時間爲時間爲O(1),最長爲O(logn)。記作O(logn)。
  二分查找的時間複雜度爲O(logn),與線性查找的O(n) 相比速度上得到了指數倍提高(x = log2n,則n = 2^x)。但是,二分查找必須建立在數據已經排好序的基礎上才能使用,因此添加數據時必須加到合適的位置,這就需要額外耗費維護數組的時間。 而使用線性查找時,數組中的數據可以是無序的,因此添加數據時也無須顧慮位置,直接把它加在末尾即可,不需要耗費時間。
  綜上,具體使用哪種查找方法,可以根據查找和添加兩個操作哪個更爲頻繁來決定。

3.2 二分查找實現

  對如下數據{0,1,16,24,35,47,59,62,73,88,99}進行二分查找,是否存在73、72這兩個數。

public class Bisearch {
    /**
     * 有序的數組
     */
    public int[] elements = new int[]{1, 16, 24, 35, 47, 59, 62, 73, 88, 99};

    @Test
    public void test1() {
        //7
        System.out.println(bisearch(73));
        //4
        System.out.println(bisearch(47));
        //-1
        System.out.println(bisearch(72));

        //求logn
        System.out.println(Math.log((double) 10) / Math.log((double) 2));
    }

    /**
     * 折半查找的實現
     *
     * @param key 要查找的數據
     * @return 查找到的索引, 或者-1 表示查找到
     */
    public int bisearch(int key) {
        int low = 0;
        int high = elements.length - 1;
        int mid;
        //不在範圍內,直接返回-1
        if (elements[low] > key || elements[high] < key) {
            return -1;
        }
        //開始折半查找
        while (low <= high) {
            //折半
            mid = (low + high) / 2;
            /*若查找值比中值小*/
            if (elements[mid] > key) {
                //最高下標調整到中位下標小一位
                high = mid - 1;
                /*若查找值比中值大*/
            } else if (elements[mid] < key) {
                //最低下標調整到中位下標大一位
                low = mid + 1;
                /*若查找值等於中值*/
            } else {
                //說明mid即爲查找到的位置
                return mid;
            }
        }
        //未查找到,返回-1
        return -1;
    }
}

  上面的數據,採用二分查找之後其查找結構如下圖:
在這裏插入圖片描述
  從上圖可以看出來二分查找等於是把靜態有序查找表分成了兩棵子樹,即查找結果只需要找其中的一半數據記錄即可,等於工作量少了一半,然後繼續二分查找,循環重複執行該操作就可以找到目標數據,或得出目標數據不存在的結論,最高需要查找logn≈4次。效率當然是非常高了。

4 插值查找

4.1 插值查找概述

  插值查找(Interpolation Search),有序表的一種查找方式。 插值查找算法類似於二分查找,不同的是插值查找每次從自適應 mid 處開始查找。
  這裏的自適應,很好解釋,比如要在取值範圍0~10000之間100個元素從小到大均勻分佈的數組中查找5,我們自然會考慮從數組下標較小的開始查找。
  將二分查找中的求 mid 索引的公式,變換一下格式得到:
在這裏插入圖片描述
  也就是mid等於最低下標low加上最高下標high與low的差的一半。算法科學家們考慮的就是將這個1/2進行改進,改進爲下面的計算方案:
在這裏插入圖片描述
  假設a[10]={1,16,24,35,47,59,62,73,88,99},low=0,high=9,則a[low]=1,a[high]=99,如果我們要找的是key=35時,按原來折半的做法,我們需要四次纔可以得到結果,但如果用新辦法,(key-a[low])/(a[high]-a[low])=(16-1)/(99-1)≈0.153,即mid≈0+0.153×(9-0)=1.377,取整得到mid=1,我們只需要一次就查找到結果了,顯然大大提高了查找的效率。
  這就是插值查找和二分查找的不同之處,插值查找是根據要查找的關鍵字key與查找表中最大最小記錄的關鍵字比較後的查找方法,其核心就在於插值的計算公式(key-a[low])/(a[high]-a[low])。從時間複雜度來看,它也是O(logn),但對於表長較大,而關鍵字分佈又比較均勻的查找表來說,插值查找算法的平均性能比折半查找要好得多。反之,由於插值的計算依賴於最大值和最小值,因此數組中如果分佈類似{0,1,2,2000,2001,…,999998,999999}這種極端不均勻的數據,用插值查找效率比二分查找低。因此插值查找應用有限。

4.2 插值查找實現

public class FibonacciSearch {
    /**
     * 有序的數組
     */
    public int[] elements = new int[]{1, 16, 24, 35, 47, 59, 62, 73, 88, 99};

    /**
     * 插值查找的實現
     *
     * @param key 要查找的數據
     * @return 查找到的索引, 或者-1 表示查找到
     */
    public int interpolationSearch(int key) {
        int low = 0;
        int high = elements.length - 1;
        int mid;
        //不在範圍內,直接返回-1
        if (elements[low] > key || elements[high] < key) {
            return -1;
        }
        //開始插值查找
        while (low <= high) {
            //插值
            mid = low + (high - low) * (key - elements[low]) / (elements[high] - elements[low]);
            /*若查找值比中值小*/
            if (elements[mid] > key) {
                //最高下標調整到中位下標小一位
                high = mid - 1;
                /*若查找值比中值大*/
            } else if (elements[mid] < key) {
                //最低下標調整到中位下標大一位
                low = mid + 1;
                /*若查找值等於中值*/
            } else {
                //說明mid即爲查找到的位置
                return mid;
            }
        }
        //未查找到,返回-1
        return -1;
    }

    @Test
    public void test1() {
        System.out.println(interpolationSearch(315));
    }
}

5 斐波那契查找

5.1 斐波那契查找概述

  斐波那契查找(Fibonacci Search)也是有序表的一種查找方式,同樣屬於二分查找的一個優化,它是利用了黃金分割原理(斐波那契數列)來實現的。改變了中間結點(mid)的位置,mid不再是中間或插值得到,而是位於黃金分割點附近,即mid=low+F(k-1)-1(F代表斐波那契數列)。
  斐波那契數列:即1,1,2,3,5,8,13…,從第三個數開始,後面的數都等於前兩個數之和,而斐波那契查找就是利用的斐波那契數列來實現查找的。初始化的斐波那契數列最後一位要大於等於數組元素的size-1。

查找步驟:
  假設表中有 n 個元素,查找過程爲獲取區間的下標 mid=low + fibonacci[k - 1] - 1 ,對 mid 的關鍵字與給定值的關鍵字比較:

  1. 如果與給定關鍵字相同,則查找成功,返回mid和high的最小值;
  2. 如果給定關鍵字大,向右查找並減小2個斐波那契區間;
  3. 如果給定關鍵字小,向左查找並減小1個斐波那契區間;
  4. 重複過程,直到找到關鍵字(成功)或區間爲空集(失敗)。

5.2 斐波那契查找實現

public class FibonacciSearch {
    /**
     * 有序的數組
     */
    public int[] elements = new int[]{1, 16, 24, 35, 47, 59, 62, 73, 88, 99};
    /**
     * 對應的斐波拉契數組,數組的最大值>=elements.length-1
     */
    public int[] fibonacci = new int[]{1, 1, 2, 3, 5, 8, 13};

    @Test
    public void test1() {
        //3
        System.out.println(fibonacciSearch2(35));
    }

    /**
     * 斐波那契查找的實現
     *
     * @param key 要查找的數據
     * @return 查找到的索引, 或者-1 表示查找到
     */
    public int fibonacciSearch2(int key) {
        //最小索引
        int low = 0;
        //最大索引
        int high = elements.length - 1;
        //不在範圍內,直接返回-1
        if (elements[low] > key || elements[high] < key) {
            return -1;
        }

        int k = 0, i, mid;
        /*計算high位於斐波那契數列的位置 */
        //這裏high爲9,fibonacci[5]<9<fibonacci[6] ,取大的,即k=6
        while (high > fibonacci[k]) {
            k++;
        }

        /*擴展數組*/
        //擴展原數組,長度擴展爲fibonacci[k]=13,即多加了三個位置elements[10],elements[11],elements[12]
        elements = Arrays.copyOf(elements, fibonacci[k]);
        //爲了保證數組的順序,把擴展的值都設置爲原始數組的最大值
        for (i = high + 1; i < elements.length; i++) {
            elements[i] = elements[high];
        }
        /*開始斐波那契查找*/
        while (low <= high) {
            /* 計算當前分隔的下標索引,取的是黃金分割點 7-4-2-1-0  7-10*/
            mid = low + fibonacci[k - 1] - 1;
            /* 若查找記錄小於當前分隔記錄 */
            if (key < elements[mid]) {
                /* 最高下標調整到分隔下標mid-1處 */
                high = mid - 1;
                /* 斐波那契數列下標減一位 */
                k = k - 1;
            }
            /* 若查找記錄大於當前分隔記錄 */
            else if (key > elements[mid]) {
                /* 最低下標調整到分隔下標mid+1處 */
                low = mid + 1;
                /* 斐波那契數列下標減兩位 */
                k = k - 2;
            } else {
                /* 若mid <= high則說明mid即爲查找到的位置,返回mid */
                /* 若mid>high說明是補全數值,返回high */
                return Math.min(mid, high);
            }
        }
        return -1;
    }

}

5.2.1 具體步驟

如果查找的key等於35:

  1. 程序開始運行,數組elements={1,16,24,35,47,59,62,73,88,99},high =9,要查找的關鍵字key=35。注意此時我們已經有了事先計算好的全局變量數組fibonacci的具體數據,它是斐波那契數列,fibonacci ={1,1,2,3,5,8,13},計算原則是斐波那契數列的最大值大於等於elements.length-1。
  2. 計算high=9位於斐波那契數列的索引位置,可能是位於某兩個索引位置之間,那麼取最大的索引位置。fibonacci[5]<9<fibonacci[6] ,取大的,即k=6。
  3. fibonacci[6]=13,計算時數組長度應該爲13,因此我們需要對原數組elements擴展長度10至13,擴展後後面的索引位置均沒有賦值,爲了保證數組的有序,賦值爲原素組elements的最大值elements[10]= elements[11]= elements[12]= elements[9]。
  4. 然後開始斐波那契查找:
    a) 尋找mid下標,由於low=0且k=6,我們第一個要對比的數值是從下標爲mid=0 + fibonacci[6 - 1] – 1 = 7開始的。
    在這裏插入圖片描述
    b) 此時elements[7]=73>key=35,因此查找記錄小於當前分隔記錄;得到high=7-1=6,k=6-1=5。再次循環,計算mid=0+F[5-1]-1=4。
    在這裏插入圖片描述c) 此時elements[4]=47>key=35,因此查找記錄小於當前分隔記錄;得到high=4-1=3,k=5-1=4。再次循環,mid=0+F[4-1]-1=2。
    在這裏插入圖片描述
    d) 此時elements[2]=24<key=35,因此查找記錄大於當前分隔記錄;得到low=2+1=3,k=4-2=2。再次循環,mid=3+F[2-1]-1=3。
    在這裏插入圖片描述
    e) 此時elements[3]=35<key=35,因此查找記錄等於當前分隔記錄;返回此時mid=3和high=3的最小值,斐波那契查找結束。即返回3

如果查找的key等於99:
  前幾步都是一樣的,主要是斐波那契查找不一樣:

  1. 尋找mid下標,由於low=0且k=6,我們第一個要對比的數值是從下標爲mid=0 + fibonacci[6 - 1] – 1 = 7開始的。
    在這裏插入圖片描述

  2. 此時elements[7]=73<key=99,因此查找記錄大於當前分隔記錄;得到low=7+1=8,k=6-2=4。再次循環,計算mid=8+F[4-1]-1=10。注意此時已經到了擴展的三個元素中了。
    在這裏插入圖片描述

  3. 此時elements[10]=99=key=99,因此查找記錄等於當前分隔記錄;返回此時mid=10和high=9的最小值,斐波那契查找結束,即返回9。這裏可以看出來擴展數組的用意,因爲mid有可能算出超出原數組索引長度的索引;同時也可以看出來最後還要比較取最小值的用意,因爲擴展的元素只是我們比較時添加的,實際上原數組並不存在這個索引,因此要取high,即原數組存在的索引;同時這也是爲擴展的元素賦值的用意,要保證mid有值-但是不超過最大值,因此就取最大值。

5.3 總結

在這裏插入圖片描述
  如上圖,斐波那契查找的特點就是左側半區範圍大於右側半區範圍。如果要查找的記錄在mid右側,則左側的數據都不用再判斷了,不斷反覆進行下去,對處於右側當中的大部分數據,其工作效率要高一些。 所以儘管斐波那契查找的時間複雜也爲O(logn),但就平均性能來說,斐波那契查找要優於二分查找。可惜如果是最壞情況,比如這裏key=1,那麼始終都處於左側長半區在查找,則查找效率要低於二分查找。
  還有比較關鍵的一點,二分查找是進行加法與除法運算(mid=(low+high)/2),插值查找進行復雜的四則運算(mid=low+(highlow)*(key-a[low])/(a[high]-a[low])),而斐波那契查找只是最簡單加減法運算(mid=low+F[k-1]-1),在海量數據的查找過程中,這種細微的差別可能會影響最終的查找效率。

參考
《算法》
《大話數據結構》

如果有什麼不懂或者需要交流,可以留言。另外希望點贊、收藏、關注,我將不間斷更新各種Java學習博客!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章