(8)《數據結構與算法》之查找算法

在java中,咱們常見的查找有四種java

  1. 順序查找,也叫線性查找
  2. 二分查找,也叫折半查找
  3. 插值查找
  4. 斐波那契查找

咱們將一一介紹着四種查找方式的思想以及程序的實現。算法


1.順序查找

順序查找 的查找過程爲:從數組的第一個元素開始,逐個將要查找的關鍵字和數組中的元素進行比較,若存在相等,則返回對應的下標。反之,若至最後一個元素,其關鍵字和元素都不相等,則代表數組中不存在要查找的數,查找不成功數組

舉例說明:
有一個數列: {1,8, 10, 89, 1000, 1234} ,判斷數列中是否包含數字8;要求: 若是找到了,就提示找到,並給出下標值。優化

public class seqSearch {
    public static void main(String[] args) {
        //定義數組arr
        int[] arr = {1,8, 10, 89, 1000, 1234};
        //定義下標
        int index = seqSearch(arr, 8);
        //當下標爲-1時,說明要查找的數不存在
        if (index == -1) {
            System.out.println("不存在");
        } else {
            System.out.println("下標爲:" + index);
        }
    }
    public static int seqSearch(int[] arr, int value) {
        for (int i = 0; i < arr.length; i++) {
            if (arr[i] == value ) {
                return i;
            }
        }
        //由於下標數值老是>= 0,因此,當要查找的數不存在時,返回-1便可
        return -1;
    }
}

擴展 : 假如咱們要查的數在數組中存在兩個及兩個以上,咱們怎麼把全部知足條件的元素下標輸出呢?
思考: 這個問題的難點在於咱們並不知道數組有多大,以及要查的數到底有多少個,因此沒法新建一個新的數組來保存知足條件的下標。因此咱們使用ArrayList 來存儲保存知足條件的下標。設計

import java.util.ArrayList;

public class seqSearch {
    public static void main(String[] args) {
        int[] arr = {1, 9, 11, 11, 52, 65, -4};
        ArrayList<Integer> arrayList = seqSearch(arr, 11);
       //當沒有查找到時,則arrayList中則沒有下標值
        System.out.println("arrayList:" + arrayList);
    }

    public static ArrayList<Integer> seqSearch(int[] arr, int value) {
        ArrayList<Integer> arrayList = new ArrayList<Integer>();
        
        for (int i = 0; i < arr.length; i++) {
            if (arr[i] == value ) {
            //將知足條件的下標放入arrayList中
                arrayList.add(i);
            }
        }
        return arrayList;
    }
}

2.二分查找

二分查找的查找過程:先肯定待查找記錄所在的範圍(區間),而後逐步縮小範圍直到找到或找不到該記錄爲止。
接下來咱們將對這個查找過程進行分析和代碼實現:
思路3d

  1. 咱們要新建一個有序數組arr
  2. 肯定該數組中間值的下標 mid = (left + right) / 2;
  3. 讓須要查找的數findVal 和中間值 arr[mid] 比較
    3.1 findVal > arr[mid], 說明你要查找的數在mid的右邊,所以須要 遞歸的向查找
    3.2 findVal < arr[mid], 說明你要查找的數在mid的左邊,所以須要 遞歸的向查找
    3.3 findVal = arr[mid], 說明你找到了,就返回mid
  4. 既然咱們使用了 遞歸,那麼何時咱們結束遞歸呢?
    4.1 找到就結束遞歸
    4.2 遞歸完整個數組,仍然沒有找到findVal,也須要結束遞歸。當 left > right 則說明整個數組遞歸完了,退出。

舉例說明: 有一個數列{1, 2, 3, 4, 5, 6, 7, 8, 9},判斷數列中是否包含數字8;要求: 若是找到了,就提示找到,並給出下標值。code

public class BinarySearch {
    public static void main(String[] args) {
        int[] arr = {1, 2, 3, 4, 5, 6, 7, 8, 9};
        //注意: right 最大值 取不到 arr.length, 最大是arr.length - 1
        int resIndex = binarySearch(arr,0, arr.length - 1,8);
        System.out.println("resIndex: " + resIndex);
    }

    //二分查找
    /**
     * @param arr        有序數組            
     * @param left       序列左邊
     * @param right      序列右邊                
     * @param findVal    要找的數                  
     * @return           若是找到就返回下標,不然返回 -1                 
     */
    public static int binarySearch(int[] arr, int left, int right, int findVal) {
        //當left > right 時, 說明遞歸整個數組, 可是沒有找到
        if (left > right) {
            return -1;
        }
        int mid = (left + right) / 2;
        int midVal = arr[mid];

        //向右遞歸
        if (findVal > midVal) {
            return binarySearch(arr, mid + 1, right, findVal);
        } else if (findVal < midVal) {
            //向左遞歸
            return binarySearch(arr, left,mid - 1, findVal);
        } else {
            return mid;
        }
    }
}

擴展 : 假如咱們要查的數在數組中存在兩個及兩個以上,咱們怎麼把全部知足條件的元素下標輸出呢?
思考: 這個問題的難點在於咱們並不知道數組有多大,以及要查的數到底有多少個,因此沒法新建一個新的數組來保存知足條件的下標。因此咱們使用ArrayList 來存儲保存知足條件的下標。blog

import java.util.ArrayList;

/**
 *    問題:
 *          {1, 2, 3, 4, 5, 6, 7, 8, 8, 9}  當一個有序數組中,有多個相同的數值時,
 *    如何將全部的數值都查找到,好比這裏的 8.
 */
public class BinarySearch2 {
    public static void main(String[] args) {
       int[] arr = {1, 2, 3, 4, 5, 6, 7, 8, 8, 9};
       ArrayList<Integer> resIndexList = binarySearch2(arr, 0, arr.length - 1, 8);
       System.out.println("resIndexList = " + resIndexList);
    }

    public static ArrayList binarySearch2(int[] arr, int left, int right, int findval) {
        //若是left > right , 說明遞歸了整個數組,退出循環
        if (left > right) {
            return new ArrayList<Integer>();
        }

        int mid = (left + right) / 2;
        int midVal = arr[mid];

        //向右循環
        if (findval > midVal) {
            return binarySearch2(arr, mid + 1, right, findval);
        } else if (findval < midVal) {
            //向左循環
            return binarySearch2(arr, left, mid - 1, findval);
        } else {
//         思路:
//              1. 在找到mid 索引值後,不要馬上返回
//              2. 向mid 索引值的左邊掃描,將全部等於 8 的元素的下標,加入到集合ArrayList
//              3. 向mid 索引值的右邊掃描,將全部等於 8 的元素的下標,加入到集合ArrayList
//              4. 將ArrayList 返回

            ArrayList<Integer> resIndexList = new ArrayList<Integer>();

            //向mid 索引值的左邊掃描,將全部等於 8 的元素的下標,加入到集合ArrayList
            int temp = mid - 1;
            while (true) {
                if (temp < 0 || arr[temp] != findval) {
                    break;
                }
                //不然,就temp 放入resIndexList
                resIndexList.add(temp);
                //temp 左移
                temp--;
            }
            //將中間的mid 下標放入
            resIndexList.add(mid);

           //向mid 索引值的右邊掃描,將全部等於 8 的元素的下標,加入到集合ArrayList
            temp = mid + 1;
            while (true) {
                if (temp > arr.length - 1 || arr[temp] != findval) {
                    break;
                }
                //不然,就temp 放入resIndexList
                resIndexList.add(temp);
                //temp 右移
                temp++;
            }
            return resIndexList;
        }
    }
}

3. 插入查找

插入查找實際上是從二分查找 優化而來,改變了查找的規則,從而實現快速查找。插值查找算法的mid 是自適應的,而二分查找的 mid 老是 序列的中間值,插入查找的mid = low + (high - low) * (key - arr[low]) / (arr[high] - arr[low]) 。這裏的low對應二分查找的left,right對應於二分查找的right,key就是咱們前面說的findVal
在這裏插入圖片描述在這裏插入圖片描述在這裏插入圖片描述遞歸

思路同二分查找相同
舉例說明: 插值查找算法 查找數組中數值爲8的元素下標索引

import java.util.ArrayList;
import java.util.Arrays;

public class InsertValueSearch {
    //count 表示遞歸次數
    private static int count = 0;

    public static void main(String[] args) {
        //定義一個有100個元素的數組
        int arr[] = new int[100];
        for (int i = 0; i < arr.length; i++) {
            arr[i] = i;
        }
        //將數組轉成字符串輸出
        System.out.println(Arrays.toString(arr));

        ArrayList<Integer> resArrayList = insertValueSearch(arr, 0, arr.length - 1, 8);
        System.out.println("resArrayList = " + resArrayList);
        System.out.println("遞歸次數:" + count);

    }

    //插值查找
    public static ArrayList insertValueSearch(int[] arr, int left, int right, int findVal) {
        count++;
        //若是left > right,則說明已經遞歸完數組中全部的數字, 退出
        //findval < arr[0] || findval > arr[arr.length - 1] 必需要有,由於當findVal很是大時,會致使mid越界
        if (left > right || findVal < arr[0] || findVal > arr[arr.length  - 1]) {
            return new ArrayList<Integer>();
        }

        //找到中間值
        int mid = left + (right - left) * (findVal - arr[left]) / (arr[right] - arr[left]);
        int midVal = arr[mid];

        //向左遞歸
        if (findVal < midVal) {
            return insertValueSearch(arr, left, mid - 1, findVal);
        } else if (findVal > midVal) {
            //向右遞歸
            return insertValueSearch(arr, mid + 1, right, findVal);
        } else {

            ArrayList<Integer> resArrayList = new ArrayList<Integer>();
            //往mid 左邊掃描,將全部等於 8 的元素的下標,加入到集合ArrayList
            int temp = mid - 1;
            while (true) {
                //temp < 0 則意味着,mid在數組的最左邊
                if (temp < 0 || arr[temp] != findVal) {
                    break;
                }
                resArrayList.add(temp);
                //左移一位
                temp--;
            }
            //將中間的mid 下標放入
            resArrayList.add(mid);

            //往mid 右邊掃描,將全部等於 8 的元素的下標,加入到集合ArrayList
            temp = mid + 1;
            while (true) {
                //temp > arr.length - 1 則意味着,mid在數組的最右邊
                if (temp > arr.length - 1 || arr[temp] != findVal) {
                    break;
                }
                resArrayList.add(temp);
                //右移
                temp++;
            }
            return resArrayList;
        }
    }
}

插值算法的注意事項:

  1. 對於數據量較大,關鍵字分佈比較均勻的查找表來講,採用插值查找,速度較快。
  2. 關鍵字分佈不均勻的狀況下,該方法不必定比折半查找好。

PS:關於插值查找算法的mid值爲何要這麼求,我也不是很清楚。不過我認爲若是你不是專門作算法的,那我們知道應該在什麼時候何地使用這種算法就能夠了。

4.斐波那契查找算法

  • 斐波那契(黃金分割法)查找基本介紹:
  1. 黃金分割點是指把一條線段分割爲兩部分,使其中一部分與全長之比等於另外一部分與這部分之比。取其前三位數字的近似值是0.618。因爲按此比例設計的造型十分美麗,所以稱爲黃金分割,也稱爲中外比。這是一個神奇的數字,會帶來意向不大的效果。
  2. 斐波那契數列 {1, 1, 2, 3, 5, 8, 13, 21, 34, 55 } 發現斐波那契數列的兩個相鄰數 的比例,無限接近 黃金分割值0.618
  • 斐波那契(黃金分割法)查找算法:
    1. 斐波那契查找原理與前兩種類似,僅僅 改變了中間結點(mid)的位置,mid不 再是中間或插值獲得,而是位於黃金分 割點附近,即mid=low+F(k-1)-1

    在這裏插入圖片描述

    1. 對F(k-1)-1的理解:
      • 由斐波那契數列 F[k]=F[k-1]+F[k-2] 的性質,能夠獲得 (F[k]-1)=(F[k-1]-1)+(F[k-2]-1)+1 。該式說明:只要順序表的長度爲F[k]-1,則能夠將該表分紅長度爲F[k-1]-1和F[k-2]-1的兩段,即如上圖所示。從而中間位置爲mid=low+F(k-1)-1
      • 相似的,每一子段也能夠用相同的方式分割
      • 但順序表長度n不必定恰好等於F[k]-1,因此須要將原來的順序表長度n增長至F[k]-1。這裏的k值只要能使得F[k]-1剛好大於或等於n便可,由如下代碼獲得,順序表長度增長後,新增的位置(從n+1到F[k]-1位置),都賦爲n位置的值便可。
        在這裏插入圖片描述
    • 舉例說明: 請對一個有序數組進行斐波那契查找 {1,8, 10, 89, 1000, 1234} ,輸入一個數看看該數組是否存在此數,而且求出下標,若是沒有就提示"沒有這個數"。
import java.util.Arrays;

public class FibonacciSearch {
    //斐波那契數組元素的個數,
    public static int maxSize = 20;
    public static void main(String[] args) {
        int[] arr = {1, 8, 10, 89, 1000, 1234};
        System.out.println(fibSearch(arr,89));

    }

    //由於後面咱們mid = low + F(k-1) - 1,須要使用到斐波拉契數列,所以咱們須要先獲取一個斐波那契數列
    //非遞歸方法獲得斐波那契數列
    public static int[] fib() {
        int[] f = new int[maxSize];
        f[0] = 1;
        f[1] = 1;
        for (int i = 2; i < maxSize; i++) {
            f[i] = f[i - 1] + f[i - 2];
        }
        return f;
    }

    //編寫斐波那契查找算法
    //非遞歸的方式編寫斐波那契查找算法
    /**
     *
     * @param a     數組
     * @param key   咱們須要查找的關鍵碼
     * @return      返回對應的下標,若是沒有就返回 -1
     */
    public static int fibSearch(int[] a, int key) {
        int low = 0;
        int high = a.length - 1;
        //表示斐波那契分割數值的下標
        int k = 0;
        int mid = 0;

        //獲取到斐波那契數列
        int f[] = fib();
        //獲取到斐波那契分割數值的下標
        while (high > f[k] - 1) {
            k++;
        }
        //由於f[k] 值可能大於咱們數組a 的長度,所以咱們須要使用Array類,構造一個新的數組,並指向a
        //不足的部分會使用0填充
        int[] temp =  Arrays.copyOf(a, f[k]);
        //實際上須要使用a數組最後的數填充temp
        /**
         *  舉例說明:
         *      int[] arr = {1, 8, 10, 89, 1000, 1234};
         *      int[] temp =  Arrays.copyOf(a, f[k]);
         *      當 數組a 的長度 小於 f[k] 的長度, 咱們要將a 的值賦值給數組temp ,可是
         *      數組temp 的長度是和f[k] 相同的,因此 數組temp 中沒有被賦值的部分 填充爲0;
         *      又由於,若是數組temp 中沒有被賦值的部分
         *
         */
        for (int i = high + 1; i < temp.length; i++) {
            temp[i] = a[high];
        }

        //使用while來循環處理,找到咱們的數key
        while (low <= high) {
            //只要這個條件知足,就能夠繼續尋找
            //獲得mid 中間值 的規則 不同
            mid = low + f[k - 1] - 1;

            //向左邊查找
            if (key < temp[mid]) {
                high = mid - 1;
                /*
                    爲何是k--
                    1.  所有元素 = 前面的元素 + 後面元素
                    2.  f[k] = f[k - 1] + f[k - 2]
                    由於 前面f[k - 1] 個元素,因此能夠繼續拆分 f[k-1] = f[k-2] + f[k-3]
                    即在f[k-1] 的前面繼續查找 k--
                    即,下次循環 mid = f[k-1-1] -1
                 */
                //重點!!!!難點!!!
                k--;
            } else if (key > temp[mid]) {
                //右邊查找
                low = mid + 1;
                /*
                    爲何是 k -= 2
                    1. 所有元素 = 前面的元素 + 後面元素
                    2. f[k] = f[k - 1] + f[k - 2]
                    3. 由於後面咱們有f[k - 2] 因此能夠繼續拆分 f[k-2] = f[k-3] + f[k-4]
                    4. 即在f[k-2] 的前面 進行查找 k -= 2
                    5. 即下次循環 mid = f[k - 1 - 2] - 1
                 */
                //重點!!!!難點!!!
                k -=2;
            } else {
                //找到了
                //須要肯定返回的是哪一個下標
                if (mid <= high) {
                    return mid;
                } else {
                    return high;
                }
            }
        }
        return  -1;
    }
}

以上全部程序都在IDEA中運行過,沒有任何問題。謝謝你們!共勉!

相關文章
相關標籤/搜索