在计算机科学与软件工程领域,查找算法是最基础也是最重要的模块之一。对于有序数组的查找,经典的二分(Binary)查找算法凭借 O(log N) 的时间复杂度在许多场景中被广泛应用。另一方面,三元(Ternary)查找作为对二分查找的扩展,将区间划分为三段,每次比对两个“探测点”而非一个,从理论上也能达到对数级时间复杂度。
三元查找常用于以下几种场景:
函数极值查找
当我们要在一个 unimodal(单峰)函数上寻找极大值或极小值时,可以利用三元查找对连续区间进行切分。
搜索有序数组
虽然对静态数组查找,二分查找往往更常见,但三元查找在某些硬件或分支预测条件下也能获得性能优势。
教学与算法竞赛
三元查找展示了分治思想的另一种变体,帮助学习者更全面地理解“缩小搜索区间”的多种策略。
本项目以 Java 为实现语言,系统地介绍并实现三元查找(Ternary Search)在有序数组和 unimodal 函数上的应用,帮助读者掌握其原理、实现细节和性能分析,为实际工程和算法竞赛提供参考。
功能性需求
在有序整数数组中查找指定目标值:
方法签名:
public static int ternarySearch(int[] arr, int key)
输入:已升序排列的整型数组 arr
,查找目标 key
;
输出:若找到则返回索引,否则返回 -1
;
在单峰函数上查找极值(最大值或最小值)对应的自变量:
方法签名:
public static int ternarySearchPeak(int[] arr)
输入:数组 arr
满足前半段递增、后半段递减;
输出:峰值元素的索引。
性能需求
时间复杂度:O(log₃ N),即对数级别;
空间复杂度:O(1),仅使用常数额外变量;
鲁棒性需求
支持空数组与长度为 1 的特殊处理;
对于没有峰值(不满足 unimodal)时返回 -1
。
质量需求
代码组织清晰,注释遵循 JavaDoc 规范;
单元测试涵盖:目标存在/不存在、全部相同、单峰函数峰值位置在两端/中间;
使用 Maven 管理,CI 环境能自动运行测试并报告覆盖率。
分治(Divide and Conquer)思想
将问题不断划分为更小的子问题,针对每段进行针对性搜索;
三元查找相比二分查找,使用两个分割点 mid1
和 mid2
,每次跳过三分之一的区间。
有序数组查找
保证 arr
为升序排列;
在每次迭代中,根据 key
与 arr[mid1]
、arr[mid2]
的大小关系缩小区间。
单峰函数极值查找
输入 arr[0..n-1]
满足先严格递增后严格递减;
在一次迭代中,通过比较 arr[mid1]
与 arr[mid2]
决定峰值在哪一段继续查找;
指针运算与边界管理
计算两个分割点:
mid1 = left + (right - left) / 3;
mid2 = right - (right - left) / 3;
边界条件 left <= right
或 left + 2 <= right
的不同含义;
初始化
left = 0
,right = arr.length - 1
;
循环过程
当 left <= right
时:
计算 mid1 = left + (right - left) / 3
,mid2 = 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 + 1
,right = mid2 - 1
;
结束未找到
返回 -1
;
初始化
同上;
循环过程
当 left + 2 <= right
时保证有至少三个元素可比较;
计算 mid1
、mid2
;
若 arr[mid1] < arr[mid2]
,说明上升区间在右侧,峰值不在 left..mid1
,则 left = mid1 + 1
;
否则 right = mid2 - 1
;
最后检查
在 [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 方法:
负责在升序数组上实现三元查找,包含:
空数组检查;
计算两分割点 mid1
、mid2
;
三分区间比较并根据目标与 arr[mid1]
、arr[mid2]
的关系移动 left
和 right
;
待 left > right
时未找到返回 -1
。
ternarySearchPeak 方法:
针对 unimodal 数组寻找峰值索引,流程如下:
长度小于 3 时直接线性选取最大;
循环条件 left + 2 <= right
确保至少三个比较点;
根据 arr[mid1] < arr[mid2]
判断峰值在右侧或左侧,移动边界;
最后在剩余区间线性搜索最大元素并返回其索引。
main 方法:
演示两种三元查找在示例数组上的调用结果。
TernarySearchTest 测试类:
覆盖了目标存在、目标不存在、空数组/null、峰值在中间或端点,以及短数组等场景,确保两种算法的正确性与边界鲁棒性。
本项目完整实现了三元查找算法在 Java 中的两大应用场景:
有序数组目标查找:时间复杂度 O(log₃ N),在分支预测和缓存友好的情况下可能优于经典二分查找;
单峰函数峰值定位:通过分段比较快速逼近峰值,适用于 unimodal 数据。
代码结构模块化,易于维护与扩展。单元测试全面覆盖,保证高可靠性。
问:三元查找比二分查找快吗?
答:理论上三元查找分支更多,实际分支预测和缓存命中率会影响性能,一般二分更常用;三元更常用于 unimodal 函数峯值查找。
问:为何 unimodal 峰值查找要用三元?
答:因为函数先增后减,比较中间两点即可确定峰值区间,而三元查找恰好一次比较两点。
问:如何保证 left + 2 <= right
合理?
答:保证剩余区间至少三个点,以便比较 mid1
、mid2
,否则无法继续分三段。
问:若数组不满足 unimodal,会怎样?
答:可能落入线性搜索分支,但结果不可靠,方法在不满足时返回局部最大索引或 -1。
与二分查找混合:对静态有序查找,先用二分缩小一段,再用三元微调;
并行三分:对超大数组,可在计算 mid1
、mid2
周围并行比较,提高多核利用率;
泛型支持:改造方法签名为
,支持任意可比对象;
浮点数峰值查找:将数组换为连续函数的样本点,插值后用三元查找找极值;
内存与缓存优化:调整分段系数(如改为 4 元或 5 元查找),在不同平台上调优分割比例以获得最佳性能。