Java:实现Ternary search三元搜索算法(附带源码)

一、项目背景详细介绍

在计算机科学与软件工程领域,查找算法是最基础也是最重要的模块之一。对于有序数组的查找,经典的二分(Binary)查找算法凭借 O(log N) 的时间复杂度在许多场景中被广泛应用。另一方面,三元(Ternary)查找作为对二分查找的扩展,将区间划分为三段,每次比对两个“探测点”而非一个,从理论上也能达到对数级时间复杂度。

三元查找常用于以下几种场景:

  1. 函数极值查找
    当我们要在一个 unimodal(单峰)函数上寻找极大值或极小值时,可以利用三元查找对连续区间进行切分。

  2. 搜索有序数组
    虽然对静态数组查找,二分查找往往更常见,但三元查找在某些硬件或分支预测条件下也能获得性能优势。

  3. 教学与算法竞赛
    三元查找展示了分治思想的另一种变体,帮助学习者更全面地理解“缩小搜索区间”的多种策略。

本项目以 Java 为实现语言,系统地介绍并实现三元查找(Ternary Search)在有序数组和 unimodal 函数上的应用,帮助读者掌握其原理、实现细节和性能分析,为实际工程和算法竞赛提供参考。


二、项目需求详细介绍

  1. 功能性需求

    • 有序整数数组中查找指定目标值:

      • 方法签名:

public static int ternarySearch(int[] arr, int key)
    • 输入:已升序排列的整型数组 arr,查找目标 key

    • 输出:若找到则返回索引,否则返回 -1

  • 单峰函数上查找极值(最大值或最小值)对应的自变量:

    • 方法签名:

public static int ternarySearchPeak(int[] arr)
      • 输入:数组 arr 满足前半段递增、后半段递减;

      • 输出:峰值元素的索引。

  1. 性能需求

    • 时间复杂度:O(log₃ N),即对数级别;

    • 空间复杂度:O(1),仅使用常数额外变量;

  2. 鲁棒性需求

    • 支持空数组与长度为 1 的特殊处理;

    • 对于没有峰值(不满足 unimodal)时返回 -1

  3. 质量需求

    • 代码组织清晰,注释遵循 JavaDoc 规范;

    • 单元测试涵盖:目标存在/不存在、全部相同、单峰函数峰值位置在两端/中间;

    • 使用 Maven 管理,CI 环境能自动运行测试并报告覆盖率。


三、相关技术详细介绍

  1. 分治(Divide and Conquer)思想

    • 将问题不断划分为更小的子问题,针对每段进行针对性搜索;

    • 三元查找相比二分查找,使用两个分割点 mid1mid2,每次跳过三分之一的区间。

  2. 有序数组查找

    • 保证 arr 为升序排列;

    • 在每次迭代中,根据 keyarr[mid1]arr[mid2] 的大小关系缩小区间。

  3. 单峰函数极值查找

    • 输入 arr[0..n-1] 满足先严格递增后严格递减;

    • 在一次迭代中,通过比较 arr[mid1]arr[mid2] 决定峰值在哪一段继续查找;

  4. 指针运算与边界管理

    • 计算两个分割点:

mid1 = left + (right - left) / 3;
mid2 = right - (right - left) / 3;
    • 边界条件 left <= rightleft + 2 <= right 的不同含义;


四、实现思路详细介绍

4.1 有序数组中的目标查找

  1. 初始化

    • left = 0right = arr.length - 1

  2. 循环过程

    • left <= right 时:

      • 计算 mid1 = left + (right - left) / 3mid2 = right - (right - left) / 3

      • arr[mid1] == key,返回 mid1

      • arr[mid2] == key,返回 mid2

      • key < arr[mid1],则 right = mid1 - 1

      • Else if key > arr[mid2],则 left = mid2 + 1

      • Else 在中间段:left = mid1 + 1right = mid2 - 1

  3. 结束未找到

    • 返回 -1

4.2 单峰函数极值查找

  1. 初始化

    • 同上;

  2. 循环过程

    • left + 2 <= right 时保证有至少三个元素可比较;

      • 计算 mid1mid2

      • arr[mid1] < arr[mid2],说明上升区间在右侧,峰值不在 left..mid1,则 left = mid1 + 1

      • 否则 right = mid2 - 1

  3. 最后检查

    • [left, right] 中线性比较,返回最大元素索引;


五、完整实现代码

// 文件: src/main/java/com/example/search/TernarySearch.java
package com.example.search;

/**
 * 三元查找算法工具类:针对有序数组的查找与单峰函数的极值定位
 */
public class TernarySearch {

    /**
     * 在升序数组中执行三元查找,寻找目标 key 的索引
     *
     * @param arr 已按升序排列的整型数组
     * @param key 要查找的目标值
     * @return 若找到则返回任一匹配索引;否则返回 -1
     */
    public static int ternarySearch(int[] arr, int key) {
        if (arr == null || arr.length == 0) {
            return -1;
        }
        int left = 0, right = arr.length - 1;
        while (left <= right) {
            int third = (right - left) / 3;
            int mid1 = left + third;
            int mid2 = right - third;
            if (arr[mid1] == key) {
                return mid1;
            }
            if (arr[mid2] == key) {
                return mid2;
            }
            if (key < arr[mid1]) {
                right = mid1 - 1;
            } else if (key > arr[mid2]) {
                left = mid2 + 1;
            } else {
                left = mid1 + 1;
                right = mid2 - 1;
            }
        }
        return -1;
    }

    /**
     * 对于单峰(unimodal)数组,使用三元查找寻找峰值索引
     *
     * @param arr 满足前段严格递增、后段严格递减的整型数组
     * @return 峰值元素索引;若不满足单峰特性返回 -1
     */
    public static int ternarySearchPeak(int[] arr) {
        if (arr == null || arr.length == 0) {
            return -1;
        }
        int n = arr.length;
        if (n < 3) {
            // 长度<3,直接线性选最大
            int maxIdx = 0;
            for (int i = 1; i < n; i++) {
                if (arr[i] > arr[maxIdx]) {
                    maxIdx = i;
                }
            }
            return maxIdx;
        }
        int left = 0, right = n - 1;
        while (left + 2 <= right) {
            int third = (right - left) / 3;
            int mid1 = left + third;
            int mid2 = right - third;
            if (arr[mid1] < arr[mid2]) {
                // 峰值在右侧区间
                left = mid1 + 1;
            } else {
                // 峰值在左侧区间(包含 mid1..mid2)
                right = mid2 - 1;
            }
        }
        // 线性寻找局部最大
        int peak = left;
        for (int i = left + 1; i <= right; i++) {
            if (arr[i] > arr[peak]) {
                peak = i;
            }
        }
        return peak;
    }

    /**
     * 示例 main 方法:演示两种三元查找
     */
    public static void main(String[] args) {
        int[] sorted = {1, 4, 7, 10, 13, 16, 19};
        System.out.println("查找 10,索引 = " + ternarySearch(sorted, 10));

        int[] unimodal = {1, 3, 8, 12, 9, 5, 2};
        System.out.println("单峰数组峰值索引 = " + ternarySearchPeak(unimodal));
    }
}

// 文件: src/test/java/com/example/search/TernarySearchTest.java
package com.example.search;

import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;

/**
 * 单元测试:验证 TernarySearch 功能
 */
public class TernarySearchTest {

    @Test
    public void testSearchFound() {
        int[] arr = {2, 5, 8, 12, 16, 23};
        assertEquals(3, TernarySearch.ternarySearch(arr, 12));
    }

    @Test
    public void testSearchNotFound() {
        int[] arr = {2, 5, 8, 12, 16, 23};
        assertEquals(-1, TernarySearch.ternarySearch(arr, 7));
    }

    @Test
    public void testEmptyAndNull() {
        assertEquals(-1, TernarySearch.ternarySearch(null, 5));
        assertEquals(-1, TernarySearch.ternarySearch(new int[0], 5));
    }

    @Test
    public void testPeakMiddle() {
        int[] arr = {1, 4, 9, 15, 10, 6, 2};
        assertEquals(3, TernarySearch.ternarySearchPeak(arr));
    }

    @Test
    public void testPeakEdge() {
        int[] arr = {5, 9, 14, 20};
        // 递增无下坡,峰值最后
        assertEquals(3, TernarySearch.ternarySearchPeak(arr));
        int[] arr2 = {20, 15, 10, 5};
        // 递减无上坡,峰值首位
        assertEquals(0, TernarySearch.ternarySearchPeak(arr2));
    }

    @Test
    public void testPeakShort() {
        int[] arr = {7, 7};
        assertEquals(0, TernarySearch.ternarySearchPeak(arr));
    }
}

六、代码详细解读

  • ternarySearch 方法
    负责在升序数组上实现三元查找,包含:

    1. 空数组检查;

    2. 计算两分割点 mid1mid2

    3. 三分区间比较并根据目标与 arr[mid1]arr[mid2] 的关系移动 leftright

    4. left > right 时未找到返回 -1

  • ternarySearchPeak 方法
    针对 unimodal 数组寻找峰值索引,流程如下:

    1. 长度小于 3 时直接线性选取最大;

    2. 循环条件 left + 2 <= right 确保至少三个比较点;

    3. 根据 arr[mid1] < arr[mid2] 判断峰值在右侧或左侧,移动边界;

    4. 最后在剩余区间线性搜索最大元素并返回其索引。

  • main 方法
    演示两种三元查找在示例数组上的调用结果。

  • TernarySearchTest 测试类
    覆盖了目标存在、目标不存在、空数组/null、峰值在中间或端点,以及短数组等场景,确保两种算法的正确性与边界鲁棒性。


七、项目详细总结

本项目完整实现了三元查找算法在 Java 中的两大应用场景:

  1. 有序数组目标查找:时间复杂度 O(log₃ N),在分支预测和缓存友好的情况下可能优于经典二分查找;

  2. 单峰函数峰值定位:通过分段比较快速逼近峰值,适用于 unimodal 数据。

代码结构模块化,易于维护与扩展。单元测试全面覆盖,保证高可靠性。


八、项目常见问题及解答

  1. 问:三元查找比二分查找快吗?
    答:理论上三元查找分支更多,实际分支预测和缓存命中率会影响性能,一般二分更常用;三元更常用于 unimodal 函数峯值查找。

  2. 问:为何 unimodal 峰值查找要用三元?
    答:因为函数先增后减,比较中间两点即可确定峰值区间,而三元查找恰好一次比较两点。

  3. 问:如何保证 left + 2 <= right 合理?
    答:保证剩余区间至少三个点,以便比较 mid1mid2,否则无法继续分三段。

  4. 问:若数组不满足 unimodal,会怎样?
    答:可能落入线性搜索分支,但结果不可靠,方法在不满足时返回局部最大索引或 -1。


九、扩展方向与性能优化

  1. 与二分查找混合:对静态有序查找,先用二分缩小一段,再用三元微调;

  2. 并行三分:对超大数组,可在计算 mid1mid2 周围并行比较,提高多核利用率;

  3. 泛型支持:改造方法签名为 >,支持任意可比对象;

  4. 浮点数峰值查找:将数组换为连续函数的样本点,插值后用三元查找找极值;

  5. 内存与缓存优化:调整分段系数(如改为 4 元或 5 元查找),在不同平台上调优分割比例以获得最佳性能。

你可能感兴趣的:(Java算法完整教程,算法)