区间查找算法
有一个需求,业务里需要判断一个数组的数值变化趋势,所以定义了一套门限,但是现在觉得这个门限太全局了,需要缩小范围, 最好一段一段的,趋势的判断也会改变原来的逻辑,每次获取门限的时候都带上数组的索引,因此需要设计一个算法,接受到一个 数组索引,可以快速查询出对应的门限区间,进而拿到该门限。
分析
需要满足以下几个条件:
- 区间有可能会重合,需要做区间的去重操作;
- 区间的查询速度一定要快,因为原始的数组非常大,每个数组元素都会访问一遍;
性能考虑
因为有可能分段会非常多,所以提升速度,选用二叉搜索来提升效率,但是有一点需要注意,二叉查询前必须要确保区间没有重叠, 这个可以最后做一下数据的预处理。
可行性分析
有序的区间序列有特点:当前区间的起始一定小于前一个区间的结束;因此如果给定一个值想知道在哪个区间,二分是可行的。
搜索实现
以下是保证区间没有重复后所实现的搜索方法。因为我的代码还没有用作实际的生产,所以只是定义接口进行测试.
伪代码
double value; // 给定的值
IRange wantedRange; // 结果区间
IRange[] range; // 区间端信息
int start=0, end=range.length; // 区间的默认始末
// 二分查找
while(start <= end)
{
mid = (start + end) / 2; //折半
if(value <= range[mid].start)
{
end = mid - 1;
continue; // 修正区间继续
}
if(value > range[mid].end)
{
start = mid + 1;
continue; // 修正区间继续
}
wantedRange = range[mid]; // 找到了
break;
}
类型说明
因为没有具体类型的设计,所以定义接口(IRagion.java
)类型供算法使用:
package com.moss.arithmetic;
/**
* 类型: 区域, 左开右闭
*/
public interface IRagion {
/**
* 获取区间起始
*
* @return 区间起始
*/
double getStart();
/**
* 获取区间结束
*
* @return 区间结束
*/
double getEnd();
/**
* 修正区间的起始
*
* @param start 新的区间起始
*/
void setStart(double start);
/**
* 修正区间结束
*
* @param end 新的区间结束
*/
void setEnd(double end);
}
抽象成策略
因为考虑到可能还会有更加完美的算法,所以搜素定义成策略接口(IRagionStrategy.java
),做到与具体实现分离:
package com.moss.arithmetic;
/**
* 区间搜索策略接口
*/
public interface IRagionStrategy {
/** 根据给定的值搜索所在的区间, 如果没有找到, 返回null */
IRagion search(double value);
}
具体实现
目前我能想到的好方法还只是使用二叉来提高搜索效率RagionStrategy.java
:
package com.moss.arithmetic;
import java.util.List;
/**
* 分区查找策略
*/
public class RagionStrategy implements IRagionStrategy {
/**
* 保存分段信息
*/
private List<IRagion> ragionList;
/**
* 当前的位置
*/
private double value;
/**
* 入参务必要保证区间不能重合
*
* @param ragionList 区间列表
*/
public RagionStrategy(List<IRagion> ragionList) {
this.ragionList = ragionList;
}
/**
* 搜索距离所在的区间
*
* @param value 输入的位置
* @return 位置所处的区间, 如果找不到返回null
*/
@Override
public IRagion search(double value) {
return binarySearch(value);
}
/**
* 二分查找
*
* @param value 需要查询的值
* @return 找到的值所在区间
*/
private IRagion binarySearch(double value) {
// 默认找不到区间
IRagion ret = null;
// 初始化区间的始末
int start = 0;
int end = ragionList.size() - 1;
// 只要区间合法, 就会一直搜索
while (start <= end) {
// 取出中点计算
int mid = (start + end) / 2;
IRagion ragion = ragionList.get(mid);
// 如果要找的值比区间起始还要小,则只有区间左边的才有可能是
if (Double.compare(value, ragion.getStart()) <= 0) {
end = mid - 1;
continue;
}
// 如果要找的值比区间的结束还要大,则只有区间右边的才有可能是
if (value > ragion.getEnd()) {
start = mid + 1;
continue;
}
// 如果改值落在区间内(start, end],则说明找到了
ret = ragion;
break;
}
return ret;
}
}
处理区间,去除重叠部分
假定我们的规则是,区间重叠部分认为是无效的部分,那么处理区间的逻辑简化为修正区间的起始和结束。
伪代码
- 按照区间的起始位置排序, 这样区间至少起始是有序的;
- 如果区间存在重叠,那么一定是当前区间的结束位置要后于下一个区间的起始,则去除重复后, 当前区间的结束就是下个区间的开始,下个区间的开始就是当前区间的结束,相当于交换前后当前区间的结束和下一个区间的开始
代码实现
因为是数据的特殊预处理,所以用一个简单的适配器RagionAdapter.java
:
package com.moss.arithmetic;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
/**
* 区间适配器,在进入算法前,对数据进行预处理
*/
public class RagionAdapter {
/**
* 区间比较,按照区间的起始位置比较
*/
private class RagionComparator implements Comparator<IRagion> {
@Override
public int compare(IRagion o1, IRagion o2) {
return Double.compare(o1.getStart(), o2.getStart());
}
}
/**
* 处理区间为后面的算法做准备,去除区间的重合部分
*
* @param ragionList 原始的区间列表
*/
public void handleRagion(List<IRagion> ragionList) {
if (ragionList == null) {
return;
}
// 先排序区间
Collections.sort(ragionList, new RagionComparator());
for (int i = 0, end = ragionList.size() - 1; i < end; i++) {
IRagion currentRagion = ragionList.get(i);
IRagion nextRagion = ragionList.get(i + 1);
double currentEnd = currentRagion.getEnd();
double nextStart = nextRagion.getStart();
// 如果发现区间重合,直接去掉重合部分
if (nextStart > currentEnd) {
currentRagion.setEnd(nextStart);
nextRagion.setStart(currentEnd);
}
}
}
}
Loading Disqus comments...