Java 内功修炼 之 数据结构与算法(四、查找算法)

四、查找算法

1、顺序(线性)查找

(1)什么是 线性查找?
  最简单直接的一种查找方式,基本思想是 对于待查找数据 key, 从数据的第一个记录开始,逐个 与 key 比较,若存在与 key 相同的值则查找成功,若不存在则查找失败。

(2)代码实现

【代码实现:】
package com.lyh.search;

public class LinearSearch {

    public static void main(String[] args) {
        int[] arrays = new int[]{100, 40, 78, 24, 10, 16};
        int key = 10;
        int index = linearSearch(arrays, key);
        if (index != -1) {
            System.out.println("查找成功,下标为: " + index);
        } else {
            System.out.println("查找失败");
        }
    }

    /**
     * 顺序查找,返回元素下标
     * @param arrays 待查找数组
     * @param key 待查找数据
     * @return 查找失败返回 -1,查找成功返回 0 ~ n-1
     */
    public static int linearSearch(int[] arrays, int key) {
        // 遍历数组,挨个匹配
        for (int i = 0; i < arrays.length; i++) {
            if (arrays[i] == key) {
                return i;
            }
        }
        return -1;
    }
}

【输出结果:】
查找成功,下标为: 4

(3)分析
  顺序查找效率是比较低的,n 个数据最坏情况下需要比较 n 次,即时间复杂度为 O(n)。

2、二分(折半)查找

(1)什么是 折半查找?
  是一个效率较高的查找方法。其要求必须采用 顺序存储结构 且 存储数据有序。

【基本实现思路:】
Step1:确定数组的中间下标。 middle = (left + right) / 2。将数据 分为左右两部分。
Step2:将待查找数据 key 与 中间元素 arrays[middle] 比较。
    Step2.1:如果 key > arrays[middle],则说明要查找数据在 middle 下标右侧,需要在右侧数据进行查找(递归)。
    Step2.2:如果 key < arrays[middle],则说明要查找数据在 middle 下标左侧,需要在左侧数据进行查找(递归)。
    Step2.3:如果 key == arrays[middle],则说明查找成功。

 上面递归结束条件:
     查找成功,结束递归。
     查找失败,即 left > right 时,退出递归。 

【举例:】
    在 {13, 27, 38, 49, 65, 76, 97} 中查找 key = 27。
第一次折半:
    left = 0, right = 6, middle = 3
    即 a[left] = 13, a[right] = 97, a[middle] = 49。
    由于待查找数据 key < a[middle],则从左侧剩余数据 {13, 27, 38, 49} 开始查找。

第二次折半:
    left = 0, right = 2, middle = 1
    即 a[left] = 0, a[right] = 38, a[middle] = 27。
    由于待查找数据 key == a[middle],则查找成功。

(2)代码实现

【代码实现:】
package com.lyh.search;

public class BinarySearch {
    public static void main(String[] args) {
        int[] arrays = new int[]{13, 27, 38, 49, 65, 76, 97};
        int key = 27;
        int index = binarySearch(arrays, 0, arrays.length - 1, key);
        if (index != -1) {
            System.out.println("查找成功,下标为: " + index);
        } else {
            System.out.println("查找失败");
        }
    }

    /**
     * 折半查找,返回元素下标
     * @param arrays 待查找数组
     * @param left 最左侧下标
     * @param right 最右侧下标
     * @param key 待查找数据
     * @return 查找失败返回 -1,查找成功返回元素下标 0 ~ n
     */
    public static int binarySearch(int[] arrays, int left, int right, int key) {
        // 若 left > right,则表示查找失败
        if (left <= right) {
            // 获取中间下标
            int middle = (left + right) / 2;
            if (key == arrays[middle]) {
                return middle;
            } else if (key > arrays[middle]) {
                return binarySearch(arrays, middle + 1, right, key);
            } else {
                return binarySearch(arrays, left, middle - 1, key);
            }
        }
        return -1;
    }
}

【输出结果:】
查找成功,下标为: 1

(3)分析:
  每次查找数据均折半,设折半次数为 x,则 2^x = n,即折半次数为 x = logn,时间复杂度为 O(logn)。效率比顺序查找高。

3、插值查找

(1)什么是插值查找?
  插值查找类似于 折半查找,其区别在于 中间节点 是自适应的。
  采用自适应节点是为了 使 middle 值更靠近 key,从而 减少 key 比较次数。

【插值查找、折半查找区别:】
折半查找 求 middle:
    middle = (left + right) / 2 = left + (right - left) / 2.

插值查找 求 middle:
    middle = left + (right - left) * (key - a[left]) / (a[right] - a[left]).

即 使用 (key - a[left]) / (a[right] - a[left]) 去替换 1 / 2,可以在某种情况下提高查找效率。
对于数据量较大、且数据分布较均匀的 数据来说,使用 插值查找 速度较快(较少比较次数)。
注:
    除法可能会遇到异常(java.lang.ArithmeticException: / by zero)。

【举例:】
  对于 0 ~ 99 的数,查找 27,
  若采用 折半查找,需要折半 5 次。
  若采用 插值查找,需要折半 1 次。

(2)代码实现

【代码实现:】
package com.lyh.search;

public class InsertionSearch {
    public static void main(String[] args) {
        int[] arrays = new int[100];
        for(int i = 0; i < arrays.length; i++) {
            arrays[i] = i;
        }
        int key = 27;
        int index = insertionSearch(arrays, 0, arrays.length - 1, key);
        if (index != -1) {
            System.out.println("查找成功,下标为: " + index);
        } else {
            System.out.println("查找失败");
        }
    }

    /**
     * 插值查找,返回元素下标
     * @param arrays 待查找数组
     * @param left 最左侧下标
     * @param right 最右侧下标
     * @param key 待查找数据
     * @return 查找失败返回 -1,查找成功返回元素下标 0 ~ n
     */
    public static int insertionSearch(int[] arrays, int left, int right, int key) {
        // 数据不符合时,退出递归
        if (left > right || key > arrays[right] || key < arrays[left]) {
            return -1;
        }
        // 自适应节点
        int middle = left + (right - left) * (key - arrays[left]) / (arrays[right] - arrays[left]);
        if (key == arrays[middle]) {
            return middle;
        } else if (key > arrays[middle]) {
            return insertionSearch(arrays, middle + 1, right, key);
        } else {
            return insertionSearch(arrays, left, middle + 1, key);
        }
    }
}

【输出结果:】
查找成功,下标为: 27

4、斐波那契(黄金分割)查找

(1)什么是 斐波那契 查找?
  斐波那契查找 与 折半查找、插值查找 类似,都是改变中间节点的位置。
  此时的 中间节点 位于 黄金分割点 附近。

【黄金分割比例:】
    黄金分割比例指 将一个整体分为两个部分,其中 较小部分 : 较大部分 = 较大部分 : 整体,且值约为 0.618,此比例称为黄金分割比例。
比如:
    1 米长绳子,分为 0.618 与 0.382 两部分,则 0.382 :0.618 == 0.618 :1。

【斐波那契数列:】
斐波那契公式:
    f(1) = 1;
    f(2) = 1;
    f(n) = f(n - 1) + f(n - 2);  n > 2
即:数列 {1, 1, 2, 3, 5, 8, 13, 21, 34, 55, ...} 
    斐波那契数列两个相邻数的比例,近似于 0.618。
比如:
    21 : 34 == 0.6176470588235294 : 1
    34 : 55 == 0.6181818181818182 : 1
    
【斐波那契查找算法原理:】
如何求黄金分割点:
    由于斐波那契数列公式为  f(n) = f(n - 1) + f(n - 2), 且 f(n -1 ) : f(n - 2) == 0.618。 
    想要使用 斐波那契处理 数据,即将数据按照 f(n-1) 与 f(n-2) 分成两部分即可。
    比如:f(n) - 1 = (f(n -  1) - 1) + (f(n - 2) - 1 + 1); 分为 (f(n -  1) - 1) 与 (f(n - 2) - 1 + 1) 两部分。
    此时 left + (f(n - 1) - 1) 即为黄金分割点 middle。

斐波那契查找:
    其将一组数据长度 看成是 斐波那契数列 进行处理,若当前数据长度 不满足 斐波那契数列,则使用 最后一个元素将其补齐。
    长度符合后,记新数组为 temp,根据 middle 计算出中间节点,并进行判断。
    若 key > temp[middle],则需要在右侧进行递归判断,而此时右侧属于 f(n - 2) 部分,即 k = k - 2;
    若 key < temp[middle],则需要在左侧进行递归判断,而此时左侧属于 f(n - 1) 部分,即 k = k - 1;
    若 key == temp[middle],则查找成功。
    
【举例:】
    在 {13, 27, 38, 49, 65, 76, 97} 中查找 key = 27。
Step1:补齐数据。    
    当前数据 arrays 长度为 7,而与之相近的斐波那契数列值为 8(f(n) = 8, n = 5),需要将其补齐。
    即数据变为 temp = {13, 27, 38, 49, 65, 76, 97, 97}.
Step2:开始第一次查找操作,中间节点 middle = left + (f(n - 1) - 1)
    left = 0,right = 7,n = 5,middle = 4,
    key < temp[middle],即下次在左侧 {13, 27, 38, 49} 进行查找(right = middle - 1 = 3)。 
    左侧部分等同于 f(n - 1) 区,所以 n 减 1, 即 n = 4。
Step3:开始第二次查找,
    left = 0, right = 3,n = 4, middle = 2
    key < temp[middle],即下次在左侧 {13, 27} 进行查找(right = middle - 1 = 1)。
    左侧部分等同于 f(n - 1) 区,所以 n 减 1, 即 n = 3。
Step3:开始第三次查找,
    left = 0, right = 1,n = 3, middle = 1
    key == temp[middle],查找成功。

(2)代码实现

【代码实现:】
package com.lyh.search;

import java.util.Arrays;

public class FibonacciSearch {
    public static void main(String[] args) {
        int[] arrays = new int[]{13, 27, 38, 49, 65, 76, 97};
        int key = 27;
        int index = fibonacciSearch(arrays, key);
        if (index != -1) {
            System.out.println("查找成功,当前下标为: " + index);
        } else {
            System.out.println("查找失败");
        }
    }

    /**
     * 返回斐波那契数组(使用 迭代 实现)
     * @return 斐波那契数组
     */
    public static int[] fibonacci(int length) {
        if (length < 0) {
            return null;
        }
        int[] fib = new int[length];
        if (length >= 1) {
            fib[0] = 1;
        }
        if (length >= 2) {
            fib[1] = 1;
        }
        for (int i = 2; i < length; i++) {
            fib[i] = fib[i - 1] + fib[i - 2];
        }
        return fib;
    }

    /**
     * 斐波那契查找,返回对应数据下标
     * 将数据长度看成 斐波那契数列,将数据分为 f(n - 1)、 f(n - 2) 两部分
     * @param arrays 待查找数组
     * @param key 待查找数据
     * @return 查找失败返回 -1,查找成功返回相应的下标
     */
    public static int fibonacciSearch(int[] arrays, int key) {
        int n = 0; // 用于记录当前 分隔点 下标
        int[] fibs = fibonacci(arrays.length); // 用于记录斐波那契数列
        // 获取第一次分割点下标
        while (arrays.length > fibs[n]) {
            n++;
        }

        // 若当前数组长度 不满足 斐波那契数列,则使用最后一个元素 去填充新数组,使新长度满足 斐波那契数列
        int[] temp = Arrays.copyOf(arrays, fibs[n]);
        for (int i = arrays.length; i < fibs[n]; i++) {
            temp[i] = arrays[arrays.length - 1];
        }

        // 开始查找
        int left = 0;
        int right = temp.length - 1;
        while(left <= right) {
            // 获取中间节点,将数据区域分为 f(n - 1), f(n - 2) 两部分
            int middle = left + fibs[n - 1] - 1;
            if (key == temp[middle]) {
                // 查找成功
                return middle;
            } else if (key > temp[middle]) {
                // 当前查找失败,下次在右侧 f(n - 2) 区域进行查找
                left = middle + 1;
                n -= 2;
            } else {
                // 当前查找失败,下次在左侧 f(n - 1) 区域进行查找
                right = middle - 1;
                n -= 1;
            }
        }
        // 查找失败,即 left > right,返回 -1
        return -1;
    }
}

【输出结果:】
查找成功,当前下标为: 1

猜你喜欢

转载自blog.csdn.net/zx309519477/article/details/108874678