查找算法
在 java 中,我们常用的查找有四种
- 顺序(线性)查找 : 时间复杂度是O(n)
- 二分查找/折半查找 : 时间复杂度是O(log2n)
- 插值查找
- 斐波那契查找
线性查找算法
这里我们实现的线性查找是找到一个满足条件的值就返回
public class SeqSearch {
public static void main(String[] args) {
int arr[] = { 1, 9, 11, -1, 34, 89 };// 没有顺序的数组
int index = seqSearch(arr, -11);
if(index == -1) {
System.out.println("没有找到到");
} else {
System.out.println("找到,下标为=" + index);
}
}
public static int seqSearch(int[] arr, int value) {
// 线性查找是逐一比对,发现有相同值,就返回下标
for (int i = 0; i < arr.length; i++) {
if(arr[i] == value) {
return i;
}
}
return -1;
}
}
二分查找算法
二分查找法只适用于从有序的数组中进行查找(比如数字和字母等)
- 数组一旦有重复元素,使用二分查找法返回的元素下标可能不是唯一的 , 二分法适用于有序和数组元素不重复
二分查找法的运行时间为对数时间 O(logn) ,即查找到需要的目标位置最多只需要logn 步
- 假设从[0,99]的 队列(100 个数,即 n=100)中寻到目标数 30,则需要查找步数为log100 , 即最多需要查找 7 次( 2^6 < 100 < 2^7)
写二分法,区间的定义一般为两种,左闭右闭即[left, right],或者左闭右开即[left, right)
- 在二分查找的过程中要保持不变量,就是在while寻找中每一次边界的处理都要坚持根据区间的定义来操作,这就是循环不变量规则
区间定义target在[left, right]区间,所以有如下两点
- while (left <= right) 要使用 <= ,因为left == right是有意义的(二者指向了同一个元素),所以使用 <=
- if (nums[middle] > target) ,因为当前这个nums[middle]一定不是target,那么接下来要查找的左区间结束下标 right就是 middle - 1
使用循环的方式实现二分查找
public class ArrayUtil {
public static void main(String[] args) {
int[] arr = {100,200,230,235,600,1000,2000,9999};
// 找出arr这个数组中200所在的下标
int index = binarySearch(arr, 230);
System.out.println(index == -1 ? "该元素不存在!" : "该元素下标" + index);
}
public static int binarySearch(int[] arr, int dest) {
// 开始下标
int begin = 0;
// 结束下标
int end = arr.length - 1;
// 临界值就是begin和end指向了同一个元素, begin = end
// 如果这个元素还不是的话,再往下去就是begin+1或者end-1 ,总之就是begin>end
while(begin <= end) {
// 中间元素下标
int mid = begin + ((end - begin) / 2);// 防止溢出,等同于(left + right)/2
if (arr[mid] > dest) { // 目标元素在左区间,所以[begin, middle - 1]
end = mid - 1;
} else if (arr[mid] < dest) {// 目标元素在右区间,所以[middle + 1, end]
begin = mid + 1;
} else { // arr[mid] == dest , 目标元素就是中间值 ,直接返回下标
return mid;
}
}
//循环结束说明整个数组遍历完了还是没有找到目标值
return -1;
}
}
使用递归的方式实现二分查找
//注意:使用二分查找的前提是 该数组是有序的.
public class BinarySearch {
public static void main(String[] args) {
int arr[] = { 1, 8, 10, 89,1000, 1234 };
}
public static int binarySearch(int[] arr, int left, int right, int findVal) {
// 当 left > right 时,说明整个数组递归完了但是没有找到
if (left > right) {
return -1;
}
// 确定中间元素的下标
//int mid = (left + right) / 2;
// 防止溢出,移位也更高效
//int mid = left + ((right - left) >> 1);
int mid = (left + right ) >>> 1;
// 目标元素在中间元素的右边
// 开始元素下标需要发生变化(开始元素的下标需要重新赋值)
if (findVal > arr[mid]) { // 向右递归
return binarySearch(arr, mid + 1, right, findVal);
// 目标元素在中间元素的左边边
// 结束元素下标需要发生变化(结束元素的下标需要重新赋值)
} else if (findVal < arr[mid]) { // 向左递归
return binarySearch(arr, left, mid - 1, findVal);
//目标元素就是中间元素
} else {
return mid;
}
}
}
定义 target 是在一个在左闭右开[left, right)的区间里,那么有如下两点
- while (left < right),这里使用 < ,因为left == right在区间[left, right)是没有意义的
- if (nums[middle] > target) ,因为当前nums[middle]不等于target,去左区间继续寻找,而寻找区间是左闭右开区间,所以right更新为middle,即:下一个查询区间不会去比较nums[middle]
class Solution {
public int search(int[] nums, int target) {
int left = 0, right = nums.length;
while (left < right) {
int mid = left + ((right - left) >> 1);
if (nums[mid] == target)
return mid;
else if (nums[mid] < target)
left = mid + 1;
else if (nums[mid] > target)
right = mid;
}
return -1;
}
}
插值查找算法
插值查找原理介绍: 插值查找算法类似于二分查找,不同的是插值查找每次从自适应 mid 处开始查找
将折半查找中的求 mid 索引的公式 , 改成插值索引公式, 因为 findVal 会影响求出的 mid , 其值必须在有序数组范围内 , 否则求出的 mid 可能越界
插值算法的原理
插值查找算法,也要求数组是有序的
- 对于数据量较大,关键字分布比较均匀的查找表来说,采用插值查找, 速度较快.
- 关键字分布不均匀的情况下,该方法不一定比折半查找要好
public class InsertValueSearch {
public static void main(String[] args) {
int arr[] = { 1, 8, 10, 89,1000,1000, 1234 };
int index = insertValueSearch(arr, 0, arr.length - 1, 1234);
System.out.println("index = " + index);
}
public static int insertValueSearch(int[] arr, int left, int right, int findVal) {
//若findVal < arr[0] 和 findVal > arr[arr.length - 1] 时则不再查找 ,否则根据公式求出的 mid 可能越界
if (left > right || findVal < arr[0] || findVal > arr[arr.length - 1]) {
return -1;
}
// 根据公式求出mid
int mid = left + (right - left) * (findVal - arr[left]) / (arr[right] - arr[left]);
int midVal = ;
if (findVal > arr[mid]) { // 向右边递归
return insertValueSearch(arr, mid + 1, right, findVal);
} else if (findVal < arr[mid]) { // 向左递归查找
return insertValueSearch(arr, left, mid - 1, findVal);
} else {
return mid;
}
}
}
斐波那契(黄金分割法)查找算法
斐波那契查找原理与前两种相似,仅仅改变了中间结点(mid)的位置,mid 不再是通过中间或插值得到,而是位于黄金分割点附近 mid=low+F[k - 1]-1
由斐波那契数列 F[k]=F[k-1]+F[k-2] 的性质,可以得到 ( F[k]-1 ) =(F[k-1]-1)+(F[k-2]-1)+1
只要顺序表的长度为 F[k]-1,则可以将该表分成长度为 F[k-1]-1 和 F[k-2]-1 以及 mid 三部分 , 类似的每一子段也可以用相同的方式分割
- 顺序表长度 n 不一定刚好等于 F[k]-1,所以需要将原来的顺序表长度 n 增加至 F[k]-1。这里的 k 值只要能使得 F[k]-1 恰好大于或等于 n 即可
- 顺序表长度增加后,新增的从 n+1 到 F[k]-1 位置的值都赋为 n 位置的值即可
使用非递归的方式编写斐波那契查找算法
- 在求mid时需要使用到斐波那契数列 , 因此我们需要先获取到一个斐波那契数列
- 若 key < temp[mid] , 我们应该向 mid 前面的查找 , 此时重新确定mid值时需要 K-1 , 并且修改结束下标
- 若 key > temp[mid] , 我们应该向 mid 后面的查找 , 此时重新确定mid值时需要 K-2 , 并且修改开始下标
- 因为 temp 是扩充过的 , mid的值可能大于high的值 , 如果大于high , 直接返回high处的值 , 因为新增的值都一样
public class FibonacciSearch {
public static int maxSize = 20;
public static void main(String[] args) {
int [] arr = {1,8, 10, 89, 1000, 1234};
System.out.println("index=" + fibSearch(arr, 189));// 0
}
//使用非递归方法得到一个斐波那契数列
public static int[] fib() {
int[] f = new int[maxSize];
f[0] = 1;
f[1] = 1;
for (int i = 2; i < maxSize; i++) {
f[i] = f[i - 1] + f[i - 2];
}
return f;
}
//使用非递归的方式编写斐波那契查找算法
public static int fibSearch(int[] a, int key) {
int low = 0;
int high = a.length - 1;
//k表示斐波那契分割数值的下标
int k = 0;
int mid = 0;
//获取到斐波那契数列
int f[] = fib();
//获取到斐波那契分割数值的下标 ,只要 f[k] - 1 大于等于数组的长度即可
while(high > f[k] - 1) {
k++;
}
//因为 f[k] 值 可能大于数组的长度,使用数组最后的数代替新增加位置默认的0
int[] temp = Arrays.copyOf(a, f[k]);
for(int i = high + 1; i < temp.length; i++) {
temp[i] = a[high];
}
// 使用while来循环处理,找到我们的数 key
while (low <= high) { // 只要这个条件满足,就可以找
mid = low + f[k - 1] - 1;
if(key < temp[mid]) { //我们应该继续向数组的前面查找(左边)
high = mid - 1;
k--;
} else if ( key > temp[mid]) { // 我们应该继续向数组的后面查找(右边)
low = mid + 1;
k -= 2;
} else {
//因为数组扩充过了找到的 mid 可能越界 ,所以需要确定返回的是哪个下标
if(mid <= high) {
return mid;
} else {
return high;
//return a.length-1
}
}
}
//数组遍历完都没有找到
return -1;
}
}
查找算法例题
线性查找的应用
请求出一个数组 int[]的最大值 {4,-1,9, 10,23},并得到对应的下标
- 定义一个 int 数组 int[] arr = {4,-1,9, 10,23};
- 假定 max = arr[0] 是最大值 , maxIndex=0;
- 从下标 1 开始遍历 arr, 如果 max < 当前元素,说明 max 不是真正的最大值, 我们就 max=当前元素; maxIndex=当前元素下标
- 当我们遍历完这个数组 arr 后 , max 就是真正的最大值,maxIndex 最大值对应的下标
int[] arr = {4,-1,9,10,23};
//假定第一个元素就是最大值
int max = arr[0];
int maxIndex = 0;
//因为假定第一个元素是最大值 , 所以从第二个元素开始遍历 arr
for(int i = 1; i < arr.length; i++) {
//如果 max < 当前元素把 max 设置成 当前元素
if(max < arr[i]) {
max = arr[i];
maxIndex = i;
}
}
//当我们遍历这个数组 arr 后 , max 就是真正的最大值,maxIndex 最大值下标
System.out.println("max=" + max + " maxIndex=" + maxIndex);
搜索目标元素所有下标
当一个有序数组中有多个相同的数值时,将所有的数值的下标都查找到
- 在找到 mid 索引值,不要马上返回
- 先向 mid 索引值的左边扫描 , 再向 mid 索引值的右边扫描 , 将满足条件元素的下标放到ArrayList集合中 , 最后返回该集合
public static List<Integer> binarySearch2(int[] arr, int left, int right, int findVal) {
// 当 left > right 时,说明整个数组递归完了但是没有找到
if (left > right || findVal < arr[0] || findVal > arr[arr.length - 1]) {
return -1;
}
// 确定中间元素的下标
int mid = (left + right) / 2;
// 目标元素在中间元素的右边
// 开始元素下标需要发生变化(开始元素的下标需要重新赋值)
if (findVal > arr[mid]) { // 向右递归
return binarySearch(arr, mid + 1, right, findVal);
// 目标元素在中间元素的左边
// 结束元素下标需要发生变化(结束元素的下标需要重新赋值)
} else if (findVal < arr[mid]) { // 向左递归
return binarySearch(arr, left, mid - 1, findVal);
//目标元素就是中间元素
} else {
//找到了 mid 但不要立马返回 , 先左右扫描看看其他是否也满足条件
List<Integer> resIndexlist = new ArrayList<Integer>();
//向 mid 索引值的左边扫描
int temp = mid - 1;
while(true) {
if (temp < 0 || arr[temp] != findVal) {//退出
break;
}
//找到将 temp 放入到集合中 , 并让temp左移
resIndexlist.add(temp);
temp -= 1;
}
//将中间的 mid 放入集合
resIndexlist.add(mid);
//向 mid 索引值的右边扫描
temp = mid + 1;
while(true) {
if (temp > arr.length - 1 || arr[temp] != findVal) {//退出
break;
}
//找到将 temp 放入到集合中 , 并让temp右移
resIndexlist.add(temp);
temp += 1;
}
//最后找完后返回集合
return resIndexlist;
}
}
搜索目标元素插入位置
给定一个排序数组(数组中无重复元素)和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置
要在数组中插入目标值,无非是这四种情况
- 目标值在数组所有元素之前
- 目标值等于数组中某一个元素
- 目标值插入数组中的位置
- 目标值在数组所有元素之后
暴力解法
//时间复杂度:O(n)
//空间复杂度:O(1)
int searchInsert(vector<int>& nums, int target) {
for (int i = 0; i < nums.size(); i++) {
// 分别处理如下三种情况
// 目标值在数组所有元素之前
// 目标值等于数组中某一个元素
// 目标值插入数组中的位置
if (nums[i] >= target) { // 一旦发现大于或者等于target的num[i],那么i就是我们要的结果
return i;
}
}
// 目标值在数组所有元素之后的情况
return nums.size(); // 如果target是最大的,或者 nums为空,则返回nums的长度
}
二分法
//时间复杂度:O(log2 n)
//空间复杂度:O(1)
//定义target在左闭右闭的区间,[low, high]
public int searchInsert(int[] nums, int target) {
int n = nums.length;
int low = 0;
int high = n - 1;
while (low <= high) { // 当low==high,区间[low, high]依然有效
int mid = low + (high - low) / 2; // 防止溢出
if (nums[mid] > target) {
high = mid - 1; // target 在左区间,所以[low, mid - 1]
} else if (nums[mid] < target) {
low = mid + 1; // target 在右区间,所以[mid + 1, high]
} else {
// 1. 目标值等于数组中某一个元素
return mid;
}
}
// 2.目标值在数组所有元素之前 3.目标值插入数组中 4.目标值在数组所有元素之后
return high + 1;
}
// 定义target在左闭右开的区间,[left, right)
public int searchInsert(int[] nums, int target) {
int left = 0;
int right = nums.length;
while (left < right) { //左闭右开 [left, right)
int middle = left + ((right - left) >> 1);
if (nums[middle] > target) {
right = middle; // target 在左区间,在[left, middle)中
} else if (nums[middle] < target) {
left = middle + 1; // target 在右区间,在 [middle+1, right)中
} else { // nums[middle] == target
return middle; // 数组中找到目标值的情况,直接返回下标
}
}
// 目标值在数组所有元素之前 [0,0)
// 目标值插入数组中的位置 [left, right) ,return right 即可
// 目标值在数组所有元素之后的情况 [left, right),因为是右开区间,所以 return right
return right;
}
搜索目标元素的起点和终点
给定一个按照升序排列的整数数组 nums,和一个目标值 target。找出给定目标值在数组中的开始位置和结束位置
- 如果数组中不存在目标值 target,返回 [-1, -1]
寻找target在数组里的左右边界
- 情况一:target 在数组范围的右边或者左边,例如数组{3, 4, 5},target为2或者数组{3, 4, 5},target为6,此时应该返回{-1, -1}
- 情况二:target 在数组范围中,且数组中不存在target,例如数组{3,6,7},target为5,此时应该返回{-1, -1}
- 情况三:target 在数组范围中,且数组中存在target,例如数组{3,6,7},target为6,此时应该返回{1, 1}
解法一
- 这份代码在简洁性很有大的优化空间,例如把寻找左右区间函数合并一起。但拆开更清晰一些,而且把三种情况以及对应的处理逻辑完整的展现出来了
class Solution {
int[] searchRange(int[] nums, int target) {
int leftBorder = getLeftBorder(nums, target);
int rightBorder = getRightBorder(nums, target);
// 情况一
if (leftBorder == -2 || rightBorder == -2) return new int[]{-1, -1};
// 情况三
if (rightBorder - leftBorder > 1) return new int[]{leftBorder + 1, rightBorder - 1};
// 情况二
return new int[]{-1, -1};
}
// 寻找target的右边界(不包括target)
// 如果rightBorder为没有被赋值(即target在数组范围的左边,例如数组[3,3],target为2),为了处理情况一
int getRightBorder(int[] nums, int target) {
// 定义target在左闭右闭的区间里,[left, right]
int left = 0;
int right = nums.length - 1;
// 记录一下rightBorder没有被赋值的情况
int rightBorder = -2;
while (left <= right) {// 当left==right,区间[left, right]依然有效
int middle = left + ((right - left) / 2);
if (nums[middle] > target) {
right = middle - 1;
} else { // 寻找右边界,nums[middle] == target的时候更新left
left = middle + 1;
rightBorder = left;
}
}
return rightBorder;
}
// 寻找target的左边界leftBorder(不包括target)
// 如果leftBorder没有被赋值(即target在数组范围的右边,例如数组[3,3],target为4),为了处理情况一
int getLeftBorder(int[] nums, int target) {
int left = 0;
int right = nums.length - 1;
// 记录一下leftBorder没有被赋值的情况
int leftBorder = -2;
while (left <= right) {
int middle = left + ((right - left) / 2);
if (nums[middle] >= target) { // 寻找左边界,nums[middle] == target的时候更新right
right = middle - 1;
leftBorder = right;
} else {
left = middle + 1;
}
}
return leftBorder;
}
}
在 nums 数组中二分查找 target的下标 , 找到下标后通过左右滑动指针,来找到符合题意的区间
- 如果二分查找失败,则 binarySearch 返回 -1,表明 nums 中没有 target。 直接返回 {-1, -1}
- 如果二分查找成功,则 binarySearch 返回 nums 中值为 target 的一个下标, 然后通过左右滑动指针,来找到符合题意的区间
class Solution {
public int[] searchRange(int[] nums, int target) {
int index = binarySearch(nums, target); // 二分查找
if (index == -1) { // nums 中不存在 target,直接返回 {-1, -1}
return new int[] {-1, -1}; // 匿名数组
}
// nums 中存在 targe,则左右滑动指针,来找到符合题意的区间
int left = index;
int right = index;
// 向左滑动,找左边界
while (left - 1 >= 0 && nums[left - 1] == nums[index]) { // 防止数组越界。逻辑短路,两个条件顺序不能换
left--;
}
// 向左滑动,找右边界
while (right + 1 < nums.length && nums[right + 1] == nums[index]) { // 防止数组越界
right++;
}
return new int[] {left, right};
}
//二分查找
public int binarySearch(int[] nums, int target) {
int left = 0;
int right = nums.length - 1; // 不变量:左闭右闭区间
while (left <= right) { // 不变量:左闭右闭区间
int mid = left + (right - left) / 2;
if (nums[mid] == target) {
return mid;
} else if (nums[mid] < target) {
left = mid + 1;
} else {
right = mid - 1; // 不变量:左闭右闭区间
}
}
return -1; // 不存在
}
}
解法三
class Solution {
public int[] searchRange(int[] nums, int target) {
int left = searchLeft(nums,target);
int right = searchRight(nums,target);
return new int[]{left,right};
}
public int searchLeft(int[] nums,int target){
// 寻找元素第一次出现的地方
int left = 0;
int right = nums.length-1;
while(left<=right){
int mid = left+(right-left)/2;
// >= 的都要缩小 因为要找第一个元素
if(nums[mid]>=target){
right = mid - 1;
}else{
left = mid + 1;
}
}
// right = left - 1
// 如果存在答案 right是首选
if(right>=0&&right<nums.length&&nums[right]==target){
return right;
}
if(left>=0&&left<nums.length&&nums[left]==target){
return left;
}
return -1;
}
public int searchRight(int[] nums,int target){
// 找最后一次出现
int left = 0;
int right = nums.length-1;
while(left<=right){
int mid = left + (right-left)/2;
// <= 的都要更新 因为我们要找最后一个元素
if(nums[mid]<=target){
left = mid + 1;
}else{
right = mid - 1;
}
}
// left = right + 1
// 要找最后一次出现 如果有答案 优先找left
if(left>=0&&left<nums.length&&nums[left]==target){
return left;
}
if(right>=0&&right<=nums.length&&nums[right]==target){
return right;
}
return -1;
}
}
找到给定字符串的索引
有个排序后的字符串数组,其中散布着一些空字符串,编写一个方法,找出给定字符串(肯定不是空字符串)的索引
- 在 {a ,“”, ac, “”, ad, b, “”, ba} 中找到 b
实际还是二分法的思想,先是找到最中间的元素,如果是空字符串得到的索引加1就是ad,之后用compareto()方法比较字符串
- 返回参与比较的前后两个字符串的ASCII码的差值,如果两个字符串首字母不同,则该方法返回首字母的ASCII码的差值
- 参与比较的两个字符串如果首字符相同,则比较下一个字符,直到有不同的为止,返回该不同的字符的asc码差值
- 返回为正数表示a1>a2, 返回为负数表示a1<a2, 返回为0表示a1==a2
public class Main {
public static void main(String[] args) {
// TODO Auto-generated method stub
String [] arr= {"a","","ac","","ad","b","","ba"};
int res=indexOf(arr,"b");
System.out.println(res);
}
private static int indexOf(String []arr,String p) {
int begin =0;
int end =arr.length-1;
while(begin<=end) {
int indexOfMid=begin+((end-begin)>>1);
while(arr[indexOfMid].equals("")) {
indexOfMid++;
//虽然确定的是空字符串,但是必须保证indexOfMid++后不能大于end
if(indexOfMid>end)
return -1;
}
if(arr[indexOfMid].compareTo(p)>0) {
end=indexOfMid-1;
}
else if(arr[indexOfMid].compareTo(p)<0) {
begin=indexOfMid+1;
}
else {
return indexOfMid;
}
}
return -1;
}
}
其他
69.x 的平方根
367.有效的完全平方数