数据结构三大查找算法及二分查找的应用

查找算法

在 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},并得到对应的下标

  1. 定义一个 int 数组 int[] arr = {4,-1,9, 10,23};
  2. 假定 max = arr[0] 是最大值 , maxIndex=0;
  3. 从下标 1 开始遍历 arr, 如果 max < 当前元素,说明 max 不是真正的最大值, 我们就 max=当前元素; maxIndex=当前元素下标
  4. 当我们遍历完这个数组 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.有效的完全平方数

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值