每日算法 — 使用java实现斐波那契搜索:黄金分割与有序数组查找优化

引言

斐波那契搜索(Fibonacci Search)是一种基于黄金分割原理的有序数组查找算法,与二分搜索相比,它仅使用加法和减法进行中间点计算,在特定硬件环境下具有性能优势。本文将用Java完整实现斐波那契搜索,并深入分析其与二分搜索、插值搜索的差异。读者将理解黄金分割比例 $φ \approx 1.618$ 如何自然地融入算法设计,以及为什么这种”不完美分割”在某些场景下反而更优。

算法原理

斐波那契数列与黄金分割

斐波那契数列定义为:$F_0 = 0, F_1 = 1, F_n = F_{n-1} + F_{n-2}$。相邻两项之比收敛于黄金分割比:

$$\lim_{n \to \infty} \frac{F_{n+1}}{F_n} = φ = \frac{1 + \sqrt{5}}{2} \approx 1.618$$

搜索策略

对于长度为 $n$ 的有序数组,找到最小的 $F_k \geq n+1$。将数组划分为两部分,长度比约为 $F_{k-1} : F_{k-2}$,即接近黄金分割比。比较后舍弃约 $1/φ \approx 61.8\%$ 或 $1/φ^2 \approx 38.2\%$ 的元素。

与二分搜索的对半分($50\%:50\%$)不同,斐波那契搜索的不对称分割在某些场景下能减少平均比较次数。

核心思想

设当前搜索区间长度为 $F_k – 1$,则中间点位于 $F_{k-1}$ 处:
– 若目标值等于中间点,查找成功
– 若目标值小于中间点,在左区间 $[0, F_{k-1} – 1]$ 继续搜索(长度 $F_{k-1} – 1$)
– 若目标值大于中间点,在右区间 $[F_{k-1} + 1, F_k – 1]$ 继续搜索(长度 $F_{k-2} – 1$)

Java实现

import java.util.*;

/**
 * 斐波那契搜索 - 黄金分割与有序数组查找优化
 */
public class FibonacciSearch {

    /**
     * 生成斐波那契数列直到超过上限
     */
    public static List<Integer> generateFibonacci(int max) {
        List<Integer> fib = new ArrayList<>();
        fib.add(0);
        fib.add(1);
        while (true) {
            int next = fib.get(fib.size() - 1) + fib.get(fib.size() - 2);
            if (next > max) break;
            fib.add(next);
        }
        return fib;
    }

    /**
     * 斐波那契搜索主算法
     * @param arr 已排序数组(升序)
     * @param target 目标值
     * @return 目标值索引,未找到返回-1
     */
    public static int search(int[] arr, int target) {
        int n = arr.length;
        if (n == 0) return -1;

        // 生成足够大的斐波那契数列
        List<Integer> fib = generateFibonacci(n + 1);
        int k = fib.size() - 1;

        // 找到最小的 F_k >= n + 1
        while (fib.get(k) < n + 1) {
            k++;
            if (k >= fib.size()) {
                fib.add(fib.get(k - 1) + fib.get(k - 2));
            }
        }

        // 扩展数组(用最后一个元素填充)至长度 F_k - 1
        int[] extended = new int[fib.get(k) - 1];
        System.arraycopy(arr, 0, extended, 0, n);
        for (int i = n; i < extended.length; i++) {
            extended[i] = arr[n - 1]; // 用最大值填充
        }

        int offset = -1; // 已排除元素的数量
        int fk = fib.get(k);
        int fk1 = fib.get(k - 1);
        int fk2 = fib.get(k - 2);

        while (k > 1) {
            // 计算当前检查位置
            int i = Math.min(offset + fk1, n - 1);

            if (target < extended[i]) {
                // 目标在左侧,舍弃右侧 F_{k-2} 个元素
                k = k - 1;
                fk = fib.get(k);
                fk1 = fib.get(k - 1);
                fk2 = fib.get(k - 2);
            } else if (target > extended[i]) {
                // 目标在右侧,舍弃左侧 F_{k-1} 个元素
                offset = i;
                k = k - 2;
                if (k >= 0) {
                    fk = fib.get(k);
                    fk1 = fib.get(k - 1);
                    fk2 = (k >= 2) ? fib.get(k - 2) : 0;
                }
            } else {
                // 找到目标
                return (i < n) ? i : -1;
            }
        }

        // 最后检查剩余元素
        if (k == 1 && offset + 1 < n && arr[offset + 1] == target) {
            return offset + 1;
        }

        return -1;
    }

    /**
     * 迭代版斐波那契搜索(无需预生成数列)
     */
    public static int searchIterative(int[] arr, int target) {
        int n = arr.length;
        if (n == 0) return -1;

        int fm2 = 0;  // F_{k-2}
        int fm1 = 1;  // F_{k-1}
        int fm = fm1 + fm2; // F_k

        // 找到最小的 F_k >= n + 1
        while (fm < n + 1) {
            fm2 = fm1;
            fm1 = fm;
            fm = fm1 + fm2;
        }

        int offset = -1;

        while (fm > 1) {
            int i = Math.min(offset + fm2, n - 1);

            if (target < arr[i]) {
                // 向左
                fm = fm2;
                fm1 = fm1 - fm2;
                fm2 = fm - fm1;
            } else if (target > arr[i]) {
                // 向右
                offset = i;
                fm = fm1 - fm2;
                fm1 = fm2;
                fm2 = fm - fm1;
                if (fm2 < 0) fm2 = 0;
            } else {
                return i;
            }
        }

        // 检查最后一个元素
        if (fm1 == 1 && offset + 1 < n && arr[offset + 1] == target) {
            return offset + 1;
        }

        return -1;
    }

    /**
     * 二分搜索(用于对比)
     */
    public static int binarySearch(int[] arr, int target) {
        int left = 0, right = arr.length - 1;
        while (left <= right) {
            int mid = left + (right - left) / 2;
            if (arr[mid] == target) return mid;
            if (arr[mid] < target) left = mid + 1;
            else right = mid - 1;
        }
        return -1;
    }

    /**
     * 性能对比测试
     */
    public static void benchmark() {
        int[] sizes = {1000, 10000, 100000, 1000000};
        Random rand = new Random(42);

        System.out.println("=== 性能对比(平均比较次数) ===");
        System.out.printf("%-12s %-15s %-15s %-15s%n", "数组大小", "斐波那契搜索", "二分搜索", "插值搜索");

        for (int size : sizes) {
            int[] arr = new int[size];
            for (int i = 0; i < size; i++) arr[i] = i * 2;

            int queries = 1000;
            long fibComps = 0, binComps = 0;

            for (int q = 0; q < queries; q++) {
                int target = rand.nextInt(size * 2);
                fibComps += searchWithComparisons(arr, target);
                binComps += binarySearchWithComparisons(arr, target);
            }

            System.out.printf("%-12d %-15.2f %-15.2f %-15s%n", 
                size, (double)fibComps/queries, (double)binComps/queries, "N/A");
        }
    }

    private static int searchWithComparisons(int[] arr, int target) {
        int comps = 0;
        int n = arr.length;
        int fm2 = 0, fm1 = 1, fm = fm1 + fm2;
        while (fm < n + 1) { fm2 = fm1; fm1 = fm; fm = fm1 + fm2; }
        int offset = -1;
        while (fm > 1) {
            comps++;
            int i = Math.min(offset + fm2, n - 1);
            if (target < arr[i]) { fm = fm2; fm1 = fm1 - fm2; fm2 = fm - fm1; }
            else if (target > arr[i]) { offset = i; fm = fm1 - fm2; fm1 = fm2; fm2 = fm - fm1; if (fm2 < 0) fm2 = 0; }
            else return comps;
        }
        if (fm1 == 1 && offset + 1 < n) comps++;
        return comps;
    }

    private static int binarySearchWithComparisons(int[] arr, int target) {
        int comps = 0;
        int left = 0, right = arr.length - 1;
        while (left <= right) {
            comps++;
            int mid = left + (right - left) / 2;
            if (arr[mid] == target) return comps;
            comps++;
            if (arr[mid] < target) left = mid + 1;
            else right = mid - 1;
        }
        return comps;
    }

    /**
     * 查找所有匹配元素(处理重复值)
     */
    public static List<Integer> searchAll(int[] arr, int target) {
        List<Integer> results = new ArrayList<>();
        int idx = searchIterative(arr, target);
        if (idx == -1) return results;

        // 向左右扩展找所有重复值
        results.add(idx);
        int left = idx - 1;
        while (left >= 0 && arr[left] == target) {
            results.add(left);
            left--;
        }
        int right = idx + 1;
        while (right < arr.length && arr[right] == target) {
            results.add(right);
            right++;
        }
        Collections.sort(results);
        return results;
    }

    public static void main(String[] args) {
        // 基础测试
        int[] arr = {10, 22, 35, 40, 45, 50, 80, 82, 85, 90, 100, 120};
        int target = 85;

        System.out.println("数组: " + Arrays.toString(arr));
        System.out.println("查找目标: " + target);
        System.out.println("斐波那契搜索结果: " + search(arr, target));
        System.out.println("迭代版搜索结果: " + searchIterative(arr, target));
        System.out.println("二分搜索结果: " + binarySearch(arr, target));

        // 边界测试
        System.out.println("\n边界测试:");
        System.out.println("查找10: " + searchIterative(arr, 10));
        System.out.println("查找120: " + searchIterative(arr, 120));
        System.out.println("查找999: " + searchIterative(arr, 999));

        // 性能测试
        System.out.println("\n");
        benchmark();
    }
}

复杂度分析

指标 斐波那契搜索 二分搜索 插值搜索
时间复杂度(最坏) $O(\log n)$ $O(\log n)$ $O(n)$
时间复杂度(平均) $O(\log n)$ $O(\log n)$ $O(\log \log n)$(均匀分布)
空间复杂度 $O(1)$ $O(1)$ $O(1)$
除法/乘法 无 有(mid计算) 有(插值计算)
比较次数(平均) $\approx 1.44 \log_2 n$ $\approx \log_2 n$ 更低(均匀分布)

斐波那契搜索的关键优势在于仅使用整数加减法,在以下场景表现优异:
1. 硬件不支持高效除法(如嵌入式系统)
2. 数据存储介质访问代价高(如磁带),需要最小化比较次数
3. 需要与黄金分割相关的分治策略时

扩展思考

  1. 黄金分割搜索(优化领域):与斐波那契搜索名称相似但用途不同,用于单峰函数极值搜索,通过黄金分割比例缩小区间
  2. 自适应斐波那契搜索:根据数据分布动态调整分割比例,非均匀分布时可接近插值搜索的效率
  3. k-ary搜索推广:将二分/斐波那契搜索推广到k路分割,在分支预测友好的现代CPU上可能获得更好的缓存性能

总结

本文通过Java实现了斐波那契搜索算法,揭示了黄金分割比例 $φ$ 在算法设计中的自然出现。虽然在现代通用计算机上,二分搜索因分支预测友好和实现简洁而更常用,但斐波那契搜索在特定约束环境(无除法指令、高访问代价存储)中仍具实用价值。理解这一算法有助于建立对分治策略多样性的深刻认识——最优分割不一定是均分,而应根据问题特性选择最合适的分割比例。