线性查找也叫顺序查找,这是最基本的一种查找方法,从给定的值中进行搜索,从一端开始逐一检查每个元素,直到找到所需元素的过程。
元素序列的排列可以有序,也可以无序。
public class Test01 {
public static void main(String[] args) {
//线性查找
int[] arr = {45, 62, 15,62, 78, 30};
int index = sequentialSearch01(arr, 62);
System.out.println("指定元素首次出现的下标位置:" + index);
List<Integer> indexList = sequentialSearch02(arr, 62);
System.out.println("指定元素出现的下标位置的集合:" + Arrays.toString(indexList.toArray()));
}
/**
* 顺序查找
* 返回指定元素首次出现的下标位置
*/
public static int sequentialSearch01(int[] arr,int value){
for (int i = 0; i < arr.length; i++) {
if(arr[i] == value){
return i;
}
}
return -1;
}
/**
* 顺序查找
* 返回指定元素出现的下标位置的集合
*/
public static List<Integer> sequentialSearch02(int[] arr,int value){
List<Integer> list = new ArrayList<>();
for (int i = 0; i < arr.length; i++) {
if(arr[i] == value){
list.adds(i);
}
}
return list;
}
}
二分查找(Binary Search)算法,也叫折半查找算法。
当要从一个序列中查找一个元素的时候,二分查找是一种非常快速的查找算法。
二分查找是针对有序数据集合的查找算法,如果是无序数据集合就遍历查找。
二分查找之所以快速,是因为它在匹配不成功的时候,每次都能排除剩余元素中一半的元素。因此可能包含目标元素的有效范围就收缩得很快,而不像顺序查找那样,每次仅能排除一个元素。
比如有一个有序表数组[1,3,5,7,9,11,13,15,17,19,21],它是按照从小到大的顺序来进行排列的,现在需要在该有序表中查找元素19,步骤如下:
- 首先设置两个指针low和high,分别指向数据集合的第一个数据元素1(位序为0)和最后一个数据元素21(位序为10)。
然后把整个数据集合长度分成两半,并用一个指针指向它们的临界点,所以定义指针mid指向了中间元素11(位序5),也就是说mid=(high+low)/2,其中high和low都代表所指向的元素的位序,如下图:
- 接着,将mid所指向的元素(11)与待查找元素(19)进行比较。
因为19大于11,说明待查找的元素(19)一定位于mid和high之间。所以继续折半,则low = mid+1,而mid = (low+high)/2,结果如下图:
- 最后,又将mid所指向的元素(19)与待查找元素(19)进行比较,结果相等,则查找成功,返回mid指针指向的元素的位序。
如果查找的元素值不是19,而是20,那么在最后一步之前还得继续折半查找,最后出现的情况如下图:
public class Test01 {
public static void main(String[] args) {
//二分法查找
int[] arr = {1,2,3,4,5,6,7,8,9,11,11,11,11,11,11};
int index = binarySearch01(arr, 11);
System.out.println("指定元素出现的下标位置:" + index);
List<Integer> indexList = binarySearch02(arr, 11);
System.out.println("指定元素出现的下标位置的集合:" + Arrays.toString(indexList.toArray()));
index = recursionbinarySearch01(arr, 0, arr.length-1, 11);
System.out.println("递归方式 - 指定元素出现的下标位置:" + index);
indexList = recursionbinarySearch02(arr, 0, arr.length-1, 11);
System.out.println("递归方式 - 指定元素出现的下标位置的集合:" + Arrays.toString(indexList.toArray()));
}
/**
* 有序的数组中查找某个元素出现的下标位置
* 不使用递归的二分查找
* 返回出现的下标位置
*/
public static int binarySearch01(int[] arr,int val){
int low = 0;
int high = arr.length-1;
while(low <= high){
int mid = (low + high)/2;
if(val > arr[mid]){
//目标在右侧
low = mid+1;
}else if(val < arr[mid]){
//目标在左侧
high = mid-1;
}else{
return mid;
}
}
return -1;
}
/**
* 有序的数组中查找某个元素首次出现的下标位置
* 不使用递归的二分查找
* 返回下标集合
*/
public static List<Integer> binarySearch02(int[] arr,int val){
int low = 0;
int high = arr.length-1;
while(low <= high){
int mid = (low + high)/2;
if(val > arr[mid]){
//目标在右侧
low = mid+1;
}else if(val < arr[mid]){
//目标在左侧
high = mid-1;
}else{
// 定义放置索引下标的集合
List<Integer> list = new ArrayList<>();
// 将首次查找的位置放入集合
list.add(mid);
// 判断是否还有重复值
int index = mid + 1;
while(index < arr.length){
if(arr[index] == val){
list.add(index);
}else{
break;
}
index++;
}
index = mid-1;
while(index >= 0){
if(arr[index] == val){
list.add(index);
}else{
break;
}
index--;
}
return list;
}
}
return null;
}
/**
* 有序的数组中查找某个元素出现的下标位置
* 使用递归的二分查找
* 返回出现的下标位置
*/
public static int recursionbinarySearch01(int[] arr,int low,int high,int val){
if(val < arr[low] || val > arr[high] || low > high){
return -1;
}
int mid = (low + high)/2;
if(val > arr[mid]){
//目标在右侧
return recursionbinarySearch01(arr, mid+1, high, val);
}else if(val < arr[mid]){
//目标在左侧
return recursionbinarySearch01(arr, low, mid-1, val);
}else{
return mid;
}
}
/**
* 有序的数组中查找某个元素首次出现的下标位置
* 使用递归的二分查找
* 返回下标集合
*/
public static List<Integer> recursionbinarySearch02(int[] arr,int low,int high,int val){
if(val < arr[low] || val > arr[high] || low > high){
return null;
}
int mid = (low + high)/2;
if(val > arr[mid]){
//目标在右侧
return recursionbinarySearch02(arr, mid+1, high, val);
}else if(val < arr[mid]){
//目标在左侧
return recursionbinarySearch02(arr, low, mid-1, val);
}else{
// 定义放置索引下标的集合
List<Integer> list = new ArrayList<>();
// 将首次查找的位置放入集合
list.add(mid);
// 判断是否还有重复值
int index = mid + 1;
while(index < arr.length){
if(arr[index] == val){
list.add(index);
}else{
break;
}
index++;
}
index = mid-1;
while(index >= 0){
if(arr[index] == val){
list.add(index);
}else{
break;
}
index--;
}
return list;
}
}
}
优点:速度快,不占空间,不开辟新空间
缺点:必须是有序的数组,数据量太小没有意义
二分查找性能
下面分析二分查找算法的性能
时间复杂度:
最坏情况:O(log n)
最好情况:如果待查找元素恰好在数组中央,只需要循环一次 O(1)
空间复杂度:
需要常数个指针 i,j,m,因此额外占用的空间是 O(1)
注意:
遍历数组,最坏情况需要对整个数组进行查找,而二分法最坏情况只是数组的一半。我们可以把排好序的数据存下来,之后直接使用,而线性查找(遍历所有数)需要每次都要每次全部遍历
习题
给定一个 n 个元素有序的(升序)整型数组 nums 和一个目标值 target ,写一个函数搜索 nums 中的 target,如果目标值存在返回下标,否则返回 -1
例如
输入: nums = [-1,0,3,5,9,12], target = 9
输出: 4
解释: 9 出现在 nums 中并且下标为 4
输入: nums = [-1,0,3,5,9,12], target = 2
输出: -1
解释: 2 不存在 nums 中因此返回 -1
参考答案:可以用任意一种二分求解
public class BinarySearch {
public static void main(String[] arg){
int[] nums={2,5,8,9,12,15,18};
int indexs=binarySearch(nums, 8);
System.out.print(indexs);
}
public static int binarySearch(int[] a,int target){
int i=0; int j=a.length-1;
while(i<=j){
int m=(i+j)>>>1;
if (target
}
给定一个排序数组和一个目标值
在数组中找到目标值,并返回其索引
如果目标值不存在于数组中,返回它将会被按顺序插入的位置
例如
输入: nums = [1,3,5,6], target = 5
输出: 2
输入: nums = [1,3,5,6], target = 2
输出: 1
输入: nums = [1,3,5,6], target = 7
输出: 4
参考答案1:用二分查找基础版代码改写,基础版中,找到返回 m,没找到 i 代表插入点,因此有
public int searchInsert(int[] a, int target) {
int i = 0, j = a.length - 1;
while (i <= j) {
int m = (i + j) >>> 1;
if (target < a[m]) {
j = m - 1;
} else if (a[m] < target) {
i = m + 1;
} else {
return m;
}
}
return i; // 原始 return -1
}
如果数组中不存在目标值 target,返回 [-1, -1]。
你必须设计并实现时间复杂度为 O(log n) 的算法解决此问题
例如
输入:nums = [5,7,7,8,8,10], target = 8
输出:[3,4]
输入:nums = [5,7,7,8,8,10], target = 6
输出:[-1,-1]
输入:nums = [], target = 0
输出:[-1,-1]
参考答案
public static int left(int[] a, int target) {
int i = 0, j = a.length - 1;
int candidate = -1;
while (i <= j) {
int m = (i + j) >>> 1;
if (target < a[m]) {
j = m - 1;
} else if (a[m] < target) {
i = m + 1;
} else {
candidate = m;
j = m - 1;
}
}
return candidate;
}
public static int right(int[] a, int target) {
int i = 0, j = a.length - 1;
int candidate = -1;
while (i <= j) {
int m = (i + j) >>> 1;
if (target < a[m]) {
j = m - 1;
} else if (a[m] < target) {
i = m + 1;
} else {
candidate = m;
i = m + 1;
}
}
return candidate;
}
public static int[] searchRange(int[] nums, int target) {
int x = left(nums, target);
if(x == -1) {
return new int[] {-1, -1};
} else {
return new int[] {x, right(nums, target)};
}
}
从折半查找中可以看出,折半查找的查找效率还是不错的。可是为什么要折半呢?为什么不是四分之一、八分之一呢?
打个比方,在牛津词典里要查找“apple”这个单词,会首先翻开字典的中间部分,然后继续折半吗?肯定不会,对于查找单词“apple”,我们肯定是下意识的往字典的最前部分翻去,而查找单词“zero”则相反,我们会下意识的往字典的最后部分翻去。
所以在折半查找法的基础上进行改造就出现了插值查找法,也叫做按比例查找。所以插值查找与折半查找唯一不同的是在于mid的计算方式上,它的计算方式为:
int mid = low + (high - low) * (val- arr[low]) / (arr[high] - arr[low])
这样就能快速定位目标数值所在的索引,比二分查找可以更快速实现查找。
自适应获取mid,也就是自适应查找点。
public class Test01 {
public static void main(String[] args) {
//插值查找
int[] arr = {1,2,3,4,5,6,7,8,9,11,11,11,11,11,11};
int index = insertSearch01(arr, 11);
System.out.println("指定元素出现的下标位置:" + index);
List<Integer> indexList = insertSearch02(arr, 11);
System.out.println("指定元素出现的下标位置的集合:" + Arrays.toString(indexList.toArray()));
index = recursionInsertSearch01(arr, 0, arr.length-1, 11);
System.out.println("递归方式 - 指定元素出现的下标位置:" + index);
indexList = recursionInsertSearch02(arr, 0, arr.length-1, 11);
System.out.println("递归方式 - 指定元素出现的下标位置的集合:" + Arrays.toString(indexList.toArray()));
}
/**
* 有序的数组中查找某个元素出现的下标位置
* 不使用递归的二分查找
* 返回出现的下标位置
*/
public static int insertSearch01(int[] arr,int val){
int low = 0;
int high = arr.length-1;
while(low <= high){
int mid = low + (high - low) * (val - arr[low])/(arr[high] - arr[low]);
if(val > arr[mid]){
//目标在右侧
low = mid+1;
}else if(val < arr[mid]){
//目标在左侧
high = mid-1;
}else{
return mid;
}
}
return -1;
}
/**
* 有序的数组中查找某个元素首次出现的下标位置
* 不使用递归的二分查找
* 返回下标集合
*/
public static List<Integer> insertSearch02(int[] arr,int val){
int low = 0;
int high = arr.length-1;
while(low <= high){
int mid = low + (high - low) * (val - arr[low])/(arr[high] - arr[low]);
if(val > arr[mid]){
//目标在右侧
low = mid+1;
}else if(val < arr[mid]){
//目标在左侧
high = mid-1;
}else{
// 定义放置索引下标的集合
List<Integer> list = new ArrayList<>();
// 将首次查找的位置放入集合
list.add(mid);
// 判断是否还有重复值
int index = mid + 1;
while(index < arr.length){
if(arr[index] == val){
list.add(index);
}else{
break;
}
index++;
}
index = mid-1;
while(index >= 0){
if(arr[index] == val){
list.add(index);
}else{
break;
}
index--;
}
return list;
}
}
return null;
}
/**
* 有序的数组中查找某个元素出现的下标位置
* 使用递归的二分查找
* 返回出现的下标位置
*/
public static int recursionInsertSearch01(int[] arr,int low,int high,int val){
if(val < arr[low] || val > arr[high] || low > high){
return -1;
}
int mid = low + (high - low) * (val - arr[low])/(arr[high] - arr[low]);
if(val > arr[mid]){
//目标在右侧
return recursionInsertSearch01(arr, mid+1, high, val);
}else if(val < arr[mid]){
//目标在左侧
return recursionInsertSearch01(arr, low, mid-1, val);
}else{
return mid;
}
}
/**
* 有序的数组中查找某个元素首次出现的下标位置
* 使用递归的二分查找
* 返回下标集合
*/
public static List<Integer> recursionInsertSearch02(int[] arr,int low,int high,int val){
if(val < arr[low] || val > arr[high] || low > high){
return null;
}
int mid = low + (high - low) * (val - arr[low])/(arr[high] - arr[low]);
if(val > arr[mid]){
//目标在右侧
return recursionInsertSearch02(arr, mid+1, high, val);
}else if(val < arr[mid]){
//目标在左侧
return recursionInsertSearch02(arr, low, mid-1, val);
}else{
// 定义放置索引下标的集合
List<Integer> list = new ArrayList<>();
// 将首次查找的位置放入集合
list.add(mid);
// 判断是否还有重复值
int index = mid + 1;
while(index < arr.length){
if(arr[index] == val){
list.add(index);
}else{
break;
}
index++;
}
index = mid-1;
while(index >= 0){
if(arr[index] == val){
list.add(index);
}else{
break;
}
index--;
}
return list;
}
}
}
斐波那契查找也叫做黄金分割法查找。
斐波那契查找也是二分查找的一种提升算法,通过运用黄金比例的概念在数列中选择查找点进行查找,提高查找效率。同样地,斐波那契查找也属于一种有序查找算法。
对于斐波那契数列:1、1、2、3、5、8、13、21、34、55、89……(也可以从0开始),前后两个数字的比值随着数列的增加,越来越接近黄金比值0.618。
比如元素个数为89的有序列表。89在斐波那契数列中是34和55相加所得。
把元素个数为89的有序列表分成:前55个数据元素组成的前半段和34个数据元素组成的后半段。那么前半段元素个数和整个有序表长度的比值接近黄金比值0.618,而前后两段长度的比值也接近黄金比值0.618。
假如要查找的元素在前半段,那么继续按照斐波那契数列来看,55 = 34 + 21,所以继续把前半段分成前34个数据元素的前半段和后21个元素的后半段,继续查找,如此反复,直到查找成功或失败。这样斐波那契数列就被应用到查找算法中了。
总长度=f[k],
前半段长度=f[k-1],后半段长度=f[k-2]
有序列表的元素个数不是斐波那契数列中的数字时该如何处理呢?
当有序表的元素个数不是斐波那契数列中的某个数字时,需要把有序列表的长度补齐,让它成为斐波那契数列中的一个数值。
如果不是补齐,而是将多余的截掉是否可行?把原有序列表截断肯定是不可行的,因为可能把要查找的目标值截掉。
每次取斐波那契数列中的某个值时(f[k]),都会进行-1操作,这是因为数组下标从0开始。
public class Test01 {
public static void main(String[] args) {
int[] arr = {1,13,25,37,49,51,62,68,70,80,80};
List fiboSearchList = fiboSearchList(arr, 80);
System.out.println(Arrays.toString(fiboSearchList.toArray()));
}
public static List fiboSearchList(int[] arr, int val) {
int low = 0;
int high = arr.length-1;
// 斐波那契的索引下标。数组长度的数值在斐波那契数列中对应的索引下标
int[] fiboArray = getFiboArray(10);//[1, 1, 2, 3, 5, 8, 13, 21, 34, 55]
// 斐波那契的索引下标。数组长度的数值在斐波那契数列中对应的索引下标
int k = 0;
// 斐波那契的索引下标。数组长度的数值在斐波那契数列中对应的索引下标
while(arr.length > fiboArray[k]){
k++;
}
System.out.println("k = " + k);//6
System.out.println("fiboArray = " + Arrays.toString(fiboArray));//[1, 1, 2, 3, 5, 8, 13, 21, 34, 55]
// 利用Java工具类Arrays 构造新数组并指向 数组 arr[]
int[] temp = Arrays.copyOf(arr, fiboArray[k]);
System.out.println("temp=" + Arrays.toString(temp));
//[1, 13, 25, 37, 49, 51, 62, 68, 70, 80, 80, 0, 0]
//对新构造的数组进行元素补充,补充为最高位的数值
for (int i = high+1; i < temp.length; i++) {
temp[i] = arr[high];
}
System.out.println("补充数值的temp=" + Arrays.toString(temp));
//[1, 13, 25, 37, 49, 51, 62, 68, 70, 80, 80, 80, 80]
while(low <= high){
//数列左侧有f[k-1]个元素
int mid = low + fiboArray[k-1] - 1;
if(val < temp[mid]){
// 目标值小于mid所在元素,在左侧查找
high = mid-1;
/*全部元素=前部元素+后部元素
* f[k]=f[k-1]+f[k-2]
* 因为左侧有f[k-1]个元素,所以可以继续拆分f[k-1]=f[k-2]+f[k-3]
* 即在f[k-1]的前部继续查找 所以k-=1
* 即下次循环 mid=f[k-1-1]-1
*/
k-=1;
}else if(val > temp[mid]){
// 目标值大于mid所在元素,在右侧查找
low = mid+1;
/*全部元素=前部元素+后部元素
* f[k]=f[k-1]+f[k-2]
* 因为右侧有f[k-2]个元素,所以可以继续拆分f[k-2]=f[k-3]+f[k-4]
* 即在f[k-2]的前部继续查找 所以k-=2
* 即下次循环 mid=f[k-1-2]-1
*/
k -= 2;
}else{
// 定义放置索引下标的集合
ArrayList list = new ArrayList<>();
list.add(mid);
int index = mid+1;
while(index < arr.length){
if(arr[index] == val){
list.add(index);
index++;
}else{
break;
}
}
index = mid-1;
while(index > 0){
if(arr[index] == val){
list.add(index);
index--;
}else{
break;
}
}
return list;
}
}
return null;
}
public static int[] getFiboArray(int maxSize){
int[] fiboArray = new int[maxSize];
fiboArray[0] = 1;
fiboArray[1] = 1;
for (int i = 2; i < fiboArray.length; i++) {
fiboArray[i] = fiboArray[i-1] + fiboArray[i-2];
}
return fiboArray;
}
}