Loading

22-查找算法

1. 顺序(线性) 查找

额,没啥要说的吧。优点的话,就是待查找集合数据可以无序

2. 二分/折半查找

2.1 思路分析

  • 确定有序数组中间元素的索引 mid = (left + right) / 2
  • 让待查找数 findVal 和 arr[mid] 比较
    • findVal > arr[mid],说明待查找数在 mid 的右边,因此递归向右进行二分查找
    • findVal < arr[mid],说明待查找数在 mid 的左边,因此递归向左进行二分查找
    • findVal = arr[mid],说明找到,直接返回
  • 递归结束的条件
    • 找到待查找数,就结束递归
    • 由上图可知,当递归到 left = right 了,都还没找到待查找数,就说明是真的找不到了;也就是说,当 left > right 时,退出

2.2 性能分析

二分查找法的运行时间为对数时间 O(㏒₂n),即查找到需要的目标位置最多只需要 ㏒₂n 步。

2.3 代码实现

public class BinarySearch {
    public static void main(String[] args) {
        // 注意, 使用二分查找的前提是数据有序
        int[] arr = {6, 12, 18, 18, 18, 25, 31, 37, 43, 50, 56, 62, 68, 75, 82, 88, 96};
        int index;

        index = binarySearch(arr, 88, 0, arr.length-1);
        System.out.println(index);

        // List<Integer> list = binarySearch2(arr, 18, 0, arr.length-1);
        // System.out.println(list);

        index = binarySearch(arr, 31);
        System.out.println(index);
    }

    /**
     * 二分查找 (迭代)
     * @param arr 待查找数组
     * @param findVal 需要查找的数
     * @return 返回target在数组中的索引; 找不到返回-1
     */
    public static int binarySearch(int[] arr, int findVal) {
        int left = 0;
        int right = arr.length - 1;
        int mid;

        while (left <= right) {
            mid = (right + left) / 2;
            if (arr[mid] == findVal) {
                return mid;
            } else if (findVal < arr[mid]) {
                right = mid - 1;
            } else if (findVal > arr[mid]) {
                left = mid + 1;
            }
        }
        return -1;
    }

    /**
     * 二分查找 (递归)
     * @param arr 待查数组
     * @param findVal 待查找数
     * @param left 最左边的索引
     * @param right 最右边的索引
     * @return
     */
    public static int binarySearch(int[] arr, int findVal, int left, int right) {
        if (left > right) return -1;

        int mid = (left + right) / 2;
        if (findVal > arr[mid])
            return binarySearch(arr, findVal, mid + 1, right);
        else if (findVal < arr[mid])
            return binarySearch(arr, findVal, left, mid - 1);
        else return mid;
    }

    // 数组中有多个与findVal相同的数值
    public static List<Integer> binarySearch2(int[] arr, int findVal, int left, int right) {
        if (left > right) return new ArrayList<Integer>();
        int mid = (left + right) / 2;
        if (findVal > arr[mid])
            return binarySearch2(arr, findVal, mid + 1, right);
        else if (findVal < arr[mid])
            return binarySearch2(arr, findVal, left, mid - 1);
        else {
            // 找到待查找元素在数组中的索引, 先不急着返回
            // 向 mid 左右分别扫描, 看是否还存在相同的数值
            List<Integer> list = new ArrayList<>();
            list.add(mid);
            // 向左边扫描
            int i = mid - 1;
            while (i >= 0 && arr[i] == findVal) {
                list.add(i);
                i--;
            }
            // 向右边扫描
            i = mid + 1;
            while (i < arr.length && arr[i] == findVal) {
                list.add(i);
                i++;
            }
            return list;
    }
}

3. 插值查找

3.1 引入

  • 以查字典为例,在查 "apple" 时,下意识的会翻开前面的书页,当查 "zoo" 时,下意识的翻开一定是后面的书页,显然,此时还绝对不是从中间开始查起,而且有一定目的地从前或从后查找。
  • 同样的,以取值范围在 1~10000 间的 100 个元素从小到大均匀分布的数组中查找 5,那么自然会考虑从数组下标较小的开始查找。
  • 可以看出,二分查找这种查找方式,并不是自适应的。因此,基于二分查找,就有了插值查找,其将查找点的选择改进为自适应选择,从而提高查找效率。

3.2 原理

  • 上面的 1/2 代表区间长度每次缩减一半,也就是控制区间缩减幅度的因子。二分查找并没有考虑数据中数值的情况,仅仅使用了数值是有序的这一信息
  • 若数组有序且分布均匀数据,arr =
    • 搜索的目标是 2,那么区间长度每次都缩减为一半合理吗?显然是不合理的,因为2这个值比较小,明显比较偏向于数据的起始位置
    • 如果搜索的目标是 99,区间减半的方式同样不合理,数值明显比较偏向于数据的末尾
  • 所以,可以改变二分查找的区间缩减策略,根据搜索的值来确定区间缩减幅度,使其不再是固定的 1/2,这种想法就是"插值查找",其中间位置计算方式如图
    • 如果 key 和 左边界值 arr[left] 差的少,则 中间位置mid 应设置的更靠左
    • 如果 key 和 左边界值 arr[left] 差的多,则 中间位置mid 应设置的更靠右
  • 也就是说,插值查找算法的中间位置 mid 不是真的在中间,而是根据目标值和边界值的关系动态的确定
  • 插值查找算法和二分查找算法的区别主要就在于中间位置 mid 的确定,它们在终止条件和判断条件上都是相同。

3.3 Tips

  • 复杂度分析
    • 时间复杂度为 O(log logn),最坏情况为 O(n)。
    • 空间复杂度为 O(1)
  • 注意事项
    • 对于数据量较大,关键字分布比较均匀的查找表来说,采用插值查找,速度较快。
    • 关键字分布不均匀的情况下,该方法不一定比折半查找要好。

3.4 代码

public class InsertSearch {
    public static void main(String[] args) {
        /*
        int[] arr = new int[100];
        for(int i = 0; i<100; i++)
        arr[i] = i+1;
         */
        int[] arr = {6, 12, 18, 18, 18, 25, 31, 37, 43, 50, 56, 62, 68, 75, 82, 88, 96};
        int index = insertSearch(arr, 88, 0, arr.length - 1);
        System.out.println(index);
    }

    // 插值查找
    public static int insertSearch(int[] arr, int value, int left, int right) {
        // 条件2和条件3 必须有, 否则 mid 得到的计算结果反应到数组下标可能越界
        if (left > right || value < arr[left] || value > arr[right])
            return -1;
        int mid = left + (right - left) * (value - arr[left]) / (arr[right] - arr[left]);
        if (value < arr[mid]) {
            return insertSearch(arr, value, left, mid - 1);
        } else if (value > arr[mid]) {
            return insertSearch(arr, value, mid + 1, right);
        } else {
            return mid;
        }
    }
}

4. 斐波那契查找

4.1 斐波那契数列

  • 斐波那契数列
  • 性质:F[k] = F[k-1] + F[k-2]
  • 斐波那契数列的两个相邻数的比例,无限接近黄金分割值 0.618

4.2 把 F 用到查找

  • 斐波那契查找就是在二分查找的基础上根据斐波那契数列进行分割的
    • F[k-1] 是前半段
    • F[k-2] 是后半段
  • 对于斐波那契数列:1、1、2、3、5、8、13、21、34、55、89 ... ,前后两个数字的比值随着数列的增加,越来越接近黄金比值 0.618
  • 比如这里的 89,把它想象成整个有序表的元素个数,而 89 是由前面的两个斐波那契数 34 和 55 相加之后的和,也就是说把元素个数为 89 的有序表分成:
    • 由前 55 个数据元素组成的前半段
    • 由后 34 个数据元素组成的后半段
  • 假如要查找的元素还在前半段,那么继续按照斐波那契数列来看,55 = 34 + 21,所以又把前半段分成:
    • 前 34 个数据元素的前半段
    • 后 21 个数据元素的后半段
  • 继续查找,如此反复下去,直到查找成功或失败,这样就把斐波那契数列应用到查找算法中啦~

4.3 查找原理

  • 斐波那契查找就是在二分查找的基础上根据斐波那契数列进行分割的
    • F[k] = F[k-1] + F[k-2]
    • F[k] - 1 = (F[k-1] - 1) + (F[k-2]-1) + 1
  • 在斐波那契数列找一个数 F[k] - 1 >= arr.length
    • while (fib[k] - 1 < high) k++;
    • 但由于 arr.length 不一定刚好等于 F[k] - 1
      • 所以需要将原来的 数组长度 arr.length 增加至 F[k] - 1
      • 新增的位置上,都赋为数组最后一个元素值
  • 完成后进行斐波那契分割,即对 F[k] - 1 个元素进行黄金分割:
    • 前半部分有 F[k-1] -1 个元素
    • 后半部分有 F[k-2] -1 个元素
    • mid = low + F(k-1) - 1
  • 找出要查找的元素在哪一部分并递归 // 类似的,每一子段也可以用相同的方式分割,直到找到

4.4 为什么是 F(k) - 1 ?

4.5 代码实现

public class FibonacciSearch {
    public static int maxSize = 20;

    public static void main(String[] args) {
        int[] arr = {6, 12, 18, 25, 31, 37, 43, 50, 56, 62, 68, 75, 82, 88, 96};
        int index = fibSearch(arr, 96); // mid = 17, high = 14
        System.out.println(index);      // 14
    }

    public static int[] fib() {
        int[] fibArr = new int[maxSize];
        fibArr[0] = 1;
        fibArr[1] = 1;
        for (int i = 2; i < maxSize; i++)
            fibArr[i] = fibArr[i-1] + fibArr[i-2];
        return fibArr;
    }

    /**
     * 使用非递归方式实现斐波那契查找算法
     * @param arr 要查找的数组
     * @param value 待查找的关键码
     * @return value 在 arr中的索引; 找不到返回-1
     */
    public static int fibSearch(int[] arr, int value) {
        int low = 0;
        int high = arr.length - 1;
        int mid;
        int k = 0;
        int[] fibs = fib();
        // 在斐波那契数列找一个数 F[k] - 1 >= arr.length
        while (fibs[k] - 1 < arr.length) k++;
        // 但由于退出 while 时,F[k] - 1 不一定刚好等于 arr.length
        int[] temp = Arrays.copyOf(arr, fibs[k] - 1);
        // 新增的位置上,都赋为数组最后一个元素值
        for (int i = arr.length; i < fibs[k] - 1; i++)
            temp[i] = arr[high];

        while (low <= high) {
            mid = low + fibs[k-1] - 1;
            if (value < temp[mid]) { // 向前半部分继续迭代
                high = mid - 1;
                k -= 1; // F[k-1] 是 前半段 ~ 0.618那部分
            } else if (value > temp[mid]) { // 向后半部分继续迭代
                low = mid + 1;
                k -= 2; // F[k-2] 是 后半段 ~ 0.382那部分
            } else { // 找到了
                // if 比较时, value 是和 temp[mid] 比较的,  mid 指向的可能是扩展的数组元素
                // 但返回的应该是 value 在 arr[] 中的索引, 而不该是在 temp[] 中的索引
                System.out.printf("mid = %d, high = %d\n", mid, high);
                if (mid < arr.length) { // → value 是 arr[] 尾元素之前的元素
                    return mid;
                } else { // → value 是 arr[] 尾元素
                    return arr.length - 1;
                }
            }
        }
        return -1;
    }
}

5. 二分查找变形问题

  1. 查找第一个值等于给定值的元素
  2. 查找最后一个值等于给定值的元素
  3. 查找第一个大于等于给定值的元素
  4. 查找最后一个小于等于给定值的元素
public class BinarySearchTest {
    public static void main(String[] args) {
        int[] arr = {1, 3, 4, 5, 6, 8, 8, 8, 11, 18};
        System.out.println(findFirstEqualsValue(arr, 8)); // 5
        System.out.println(findLastEqualsValue(arr, 8));  // 7
        System.out.println(findFirstGtValue(arr, 8));     // 5
        System.out.println(findLastLtValue(arr, 8));      // 7
    }

    // 查找第一个值等于给定值的元素
    public static int findFirstEqualsValue(int[] arr, int value) {
        int low = 0;
        int high = arr.length-1;
        // while (low < high) { ×
        while (low <= high) {
            int mid = low + ((high - low) >> 1);
            if (arr[mid] > value) {
                high = mid - 1;
            } else if (arr[mid] < value) {
                low = mid + 1;
            } else { // arr[mid] = value
                if (mid == 0 || arr[mid-1] != value) return mid;
                else high = mid - 1;
            }
        }
        // return mid; ×
        return -1;
    }

    // 查找最后一个值等于给定值的元素
    public static int findLastEqualsValue(int[] arr, int value) {
        int low = 0;
        int high = arr.length-1;
        while (low <= high) {
            int mid = low + ((high - low) >> 1);
            if (arr[mid] < value) {
                low = mid + 1;
            } else if (arr[mid] > value) {
                high = mid - 1;
            } else {
                if (mid == arr.length - 1 || arr[mid+1] != value) return mid;
                else low = mid + 1;
            }
        }
        return -1;
    }

    // 查找第一个大于等于给定值的元素
    public static int findFirstGtValue(int[] arr, int value) {
        int low = 0;
        int high = arr.length-1;
        while (low <= high) {
            int mid = low + ((high - low) >> 1);
            if (arr[mid] < value) {
                low = mid + 1;
            } else {
                if (mid == 0 || arr[mid-1] < value) return mid;
                else high = mid - 1;
            }
        }
        return -1;
    }

    // 查找最后一个小于等于给定值的元素
    public static int findLastLtValue(int[] arr, int value) {
        int low = 0;
        int high = arr.length-1;
        while (low <= high) {
            int mid = low + ((high - low) >> 1);
            if (arr[mid] > value) {
                high = mid - 1;
            } else {
                if (mid == arr.length-1 || arr[mid+1] > value) return mid;
                else low = mid + 1;
            }
        }
        return -1;
    }
}
posted @ 2020-02-18 20:26  tree6x7  阅读(131)  评论(0编辑  收藏  举报