从1开始学Java数据结构与算法——四种查找算法讲解分析与代码实现

在java中,我们常用的查找算法有四种:
1)顺序(线性)查找
2)二分查找/折半查找
3)插值查找
4)斐波那契查找

顺序查找

顺序查找也叫线性查找非常简单,就是对一组数据,去遍历它,挨个比较是否有自己所需的数据,如果有就将其位置返回,没有就给出提示即可。其实说白了就是遍历逐一比较查找的一个过程,这里就不多说,直接给出代码

/**
	 * 顺序查找
	 * @param arr:从arr这个数组里去查找数据
	 * @param number:需要被查找的数据
	 */
	public static int seqsearch(int[] arr, int number) {
		for(int i = 0; i<arr.length-1; i++) {
			if(arr[i] == number) {
				return i;//返回要查找数据所在数组的下标位置
			}
		}
		return -1;//返回-1表示要查找的数据不存在
	}

这里注意,顺序查找由于只是一个简单的遍历过程,所以在有序和无序数组中都可以进行

二分查找

这里注意:二分查找,只能在有序数组中进行,所以要进行二分查找,就需要先将数组先进行排序,使其成为一个有序数组后才能进行二分查找,至于为什么必须是要有序数组,下面我们来看看二分查找的思路就明白了。

思路分析

对于一个给定数组,先找他的中间值,然后将其与需要被查找的元素进行大小比较,如果被查找的元素小于该中值,就在数据中值以左的位置继续查找,同样的再取左边剩余部分的中值重复上述过程。反之,如果如果被查找的元素大于该中值,就在数据中值以右的位置继续查找,同样的再取右边剩余部分的中值重复上述过程。
那么根据上面的思路描述,就可以发现两个问题(以从小到大排序为例):

  • 如果这个数组不是有序的,那么就不能保证,每次取的中值右边的所有数都大于左边的所有数,那么取该中值比较也就没有意义
  • 2)这是一个重复分割左右两部分呢的过程,所以可以采用递归的方法去实现

下面我们以{3,6,15,27,28,37,46,52}数组中查找3给出图解:
1)先找到该组数据的中间值:(左索引的下标+右索引的下标)/2后取整=3,所以中值为数组中下标为3的元素,27。
在这里插入图片描述
2)接着比较3和27,因为3比27小,所以从中值的左边去查找。同样的在左边继续取一个中值
在这里插入图片描述
3)然后继续将3与中值的6进行比较,发现3任然小于6,所以继续在中值的左边去找。由于6的左边只有一个元素,所以此时左索引和右索引都是该位置,那么中值也就是该位置,那么此时中值的值就是我们需要查找的数据3,所以查找完毕
在这里插入图片描述

  • 根据上述图解,我们又可以得出递归退出的条件:1)当找到数据的时候可以退出;2)当数据不存在,即左索引大于右索引的时候可以退出

代码实现

/**
	 * 二分查找
	 * @param arr:有序数组,从该数组里面查找
	 * @param left:左索引
	 * @param right:右索引
	 * @param number:被查找的数据
	 */
	public static int binarysearch(int[] arr, int left, int right, int number) {
		int middle = (left+right)/2;
		while(left <= right) {
			if(number < arr[middle]) {
				//向左递归查找
				return binarysearch(arr, left, middle-1, number);
			}else if(number > arr[middle]) {
				//向右递归查找
				return binarysearch(arr, middle+1, right, number);
			}else {
				//找到,返回下标位置即可
				return middle;
			}
		}
		return -1;
	}

二分查找功能完善

如果有序数组中,被查找的元素有多个,比如在{2,8,9,9,9,10,15,20}中查找9这个元素,那么该如何返回所有9的下标位置呢?
思路也很简单,因为二分查找是在一个有序数组中进行的,所以当找到一个9之后,我们不要马上将该位置返回回去,继续再在该位置的左右两边取逐个取相同的数据的下标位置即可,最后将他们放到一个ArrayList中一起返回,即达到目的

/**
	 * 二分查找,考虑多个重复的数据
	 * @param arr:有序数组,从该数组里面查找
	 * @param left:左索引
	 * @param right:右索引
	 * @param number:被查找的数据
	 */
	public static ArrayList<Integer> binarysearch2(int[] arr, int left, int right, int number) {
		int middle = 0;
		while(left <= right) {
		middle = (left+right)/2;
			if(number < arr[middle]) {
				//向左递归查找
				return binarysearch2(arr, left, middle-1, number);
			}else if(number > arr[middle]) {
				//向右递归查找
				return binarysearch2(arr, middle+1, right, number);
			}else {
				//找到一个值之后,创建一个ArrayList,继续左右两边查找看是否有相同的数据
				ArrayList<Integer> result_list = new ArrayList<Integer>();
				result_list.add(middle);//将最先查找到的中间值存放
				int temp = middle - 1;
				while(temp > 0 && arr[temp] == number) {
					//向左查找
					result_list.add(temp);
					temp--;
				}
				temp = middle + 1;
				while(temp < arr.length-1 && arr[temp] == number) {
					//向左查找
					result_list.add(temp);
					temp++;
				}
				return result_list;
			}
		}
		//这里注意,因为我们改了返回类型,所以没找到的时候,不饿能再返回-1,我们可以返回一个空的ArrayList
		//最后去判断ArrayList是否为空,来表示该数据是否找到
		return new ArrayList<Integer>();
	}

这里还有一个小问题,就是返回的ArrayList中存放的下标位置不一定是有序的,那么改动的话,大概有两个思路:
1)将Arraylist排序好之后,再返回
2)先从第一个找到的位置开始往左找到最前面一个相同的数据,然后开始往右查找的时候,才放入,那么这样得到的ArrayList中的下标位置就是有序的了

插值查找

上面已经写完了顺序查找和二分查找,我们可以发现,当数据量很大的时候,顺序查找的遍历显然不如二分查找来的快,二分查找每次可以省去一大半的数据不用管。而且顺序查找的时间复杂度为O(n),而二分查找的时间复杂度为O(log2n)。
但是我们对二分查找任然觉得有点小问题,比如下面这个数组
{1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20},如果我们用二分查找去找1的话,经过计算,我们需要折半4次找到,那么我们有没有办法,能让这个中值,更快的定位到1呢?下面我们就来看看插值查找

公式变化

插值查找类似于微分查找,不同的只是他们定位middle,也就是定位中间值得公式不同,如下图左边是二分查找定位中间值得公式,右边是插值查找定位中间值得公式。【low相当于我们上前面说的左索引left,hight相当于我们前面说的右索引right,key相当与我们需要被查找得值】
在这里插入图片描述
那么这样改的好处是什么呢,下面我们来通过两个例子看看:
假如我们有个1到100的连续有序数组,我们需要查找数字1和100的位置
利用上面右边插入查找的公式我们来算算:
mid=0+(1-arr[0])/(arr[99]-arr[0])×(99-0)=0+(1-1)/(100-1)×99=0,而0又刚好是数字1在数组中的下标
mid=0+(100-arr[0])/(arr[99]-arr[0])×(99-0)=0+(100-1)/(100-1)×99=99,而99又刚好是数字100在数组中的下标
那么我们可以看到,只需要一次就能查找出1或100,而我们利用二分查找的话,每次折半下来,需要7次才能找到

值与这个公式为什么会更好呢,其实里面是有一些数学的东西在里面,下面对该公式进行了一个简单的分析:
其实可以通俗的理解为 类似于翻字典:要找的页数跟第一页的差值占总页数的多少
在这里插入图片描述

代码实现

代码实现其实和上面的二分查找很类似,所不同的就是把确定中值的公式该一下

/**
	 * 插值查找
	 * @param arr:数组
	 * @param left:左索引
	 * @param right:右索引
	 * @param number:查找值
	 */
	public static ArrayList<Integer> insertvaluesearch(int[] arr, int left, int right, int number) {
		int middle = 0;
		while(left <= right) {
			middle = left+(number-arr[left])/(arr[right]-arr[left])*(right-left);//插值查找确定中值的公式
			if(number < arr[middle]) {
				//向左递归
				return insertvaluesearch(arr, left, middle-1, number);
			}else if(number > arr[middle]) {
				//向右递归
				return insertvaluesearch(arr, middle+1, right, number);
			}else if(number == arr[middle]) {
				ArrayList<Integer> result_list = new ArrayList<Integer>();
				result_list.add(middle);//先加入最先找到的下标位置
				//开始往左找相同数据
				int temp = middle-1;
				while(temp > 0 && arr[temp] == number) {
					result_list.add(temp);
					temp--;
				}
				//开始往右找相同数据
				temp = middle+1;
				while(temp < arr.length-1 && arr[temp] == number) {
					result_list.add(temp);
					temp++;
				}
				return result_list;
			}
		}
		//如果没找到,返回一个空的ArrayList
		return new ArrayList<Integer>();
	}

注意事项:

  • 对于数据量比较大,关键字部分比较均匀的查找表来说,采用插值查找速度较快
  • 关键字部分不均匀的情况下,该方法不一定比二分查找好

斐波那契(黄金分割法)查找

思路分析

斐波那契(黄金分割法)原理与前两种相似,仅仅改变了中间节点(middle)的位置,middle不再是中间或者插值找到,而是位于黄金分割点附近,即middle = left+F(k-1)-1F代表斐波那契数列
简单来说就是在该数组中以黄金分割点的位置来确定中值middle的位置
那么问题又来了,如何确定黄金分割点呢?这里我们就用到了斐波那契数列。给出一段斐波那契数列{1,1,2,3,5,8,13,21,34,55},我们可以发现,从第三个元素开始,越往后的相邻两个元素的比值越接近黄金分割点0.618。

由于斐波那契数列中从第三个元素开始可以表示为F(k) = F(k-1) + F(k-2),所以可以得到F(k) - 1= [F(k-1) -1] + [F(k-2) - 1] +1这样一个公式。 那么整个数组就以middle这个中值,被分为了前面的F(k-1)-1和后面的F(k-2)-1两段,而分好的这两段又刚好符合斐波那契数列,说一middle这个中值点就是黄金分割点。如下图所示
在这里插入图片描述
但是这里要注意一个问题:由于斐波那契数列中的值不连续,所以我们的数组长度并不一定刚好的等于斐波那契数列中的某个值-1,所以这里我们就需要在算法开始的时候,先进性一个数组长度和斐波那契数列中的值进行比较的过程,找到一个斐波那契值-1>=数组长度,如果恰好相等,那正好,如果大了,那我们将数组扩大,扩大的部分中没有数据的部分,填上原数组中最后的一个值。
如下图解:
在这里插入图片描述
那么这时候我们需要找到该数组的黄金分割点,利用斐波那契数列的特性可以得到F(5) - 1 = 8 - 1 = 7 = [F(4) - 1] + [F(3) - 1] + 1>6(数组长度),所以我们要将数组长度补到7,并以原数组中最后一个值去填充
在这里插入图片描述
接下来的过程就是和二分查找和插入查找一样得,将被查找的数与中值数进行比较,然后再去左右两边继续查找即可

代码实现

/**
	 * 斐波那契(黄金分割)查找
	 * @param arr:数组
	 * @param number:被查找的数
	 */
	public static int fibonaccisearch(int[] arr, int number) {
		
		int k = 0;//斐波那契数列的索引
		int[] f = F();//获取斐波那契数列
		int right = arr.length-1;//右索引
		int left = 0;//左索引
		
		//先找到斐波那契数列中与数组的长度最相近的一个数值
		while(right+1 > f[k]-1) {
			k++;
		}
		//因为f[k]的值可能会会大于数组长度,所以我们需要做个数组扩容和填充
		int[] temp = Arrays.copyOf(arr, f[k]);//数组扩容
		for(int i = right+1; i<temp.length; i++) {//数据填充
			temp[i] = arr[right];
		}
		
		int middle = 0;
		//下面开始进行黄金分割查找
		while(left <= right) {
			 middle = left + f[k - 1] - 1;//确定中值
			 System.out.println(middle);
			//被查找的数与中值数判断
			if(temp[middle] > number) {
				//向左继续分割查找,将右索引往左移动
				right = middle - 1;
				//这里为什么要进行k--,因为全部元素=前面部分+后面部分 => f[k] = f[k-1] + f[k-2]
				//因为中值的前面有f[k-1]个元素,所以继续拆分即为f[k-1] = f[k-1-1] + f[k-2-1]
				//即在f[k-1]的前面继续查找k--,即下次循环middle = f[k-1-1]-1
				k--;
			}else if(temp[middle] < number){
				//向右继续分割查找,将左索引往右移动
				left = middle + 1;
				//这里为什么要 k = k-2,和上面k--的道理一样
				k -= 2;
			}else if(temp[middle] == number) {
				//找到了
				//但是这里我们需要判断一下,因为找到的有可能是在填充的数据位上
				if(middle < right) {
					return middle;
				}else {
					return right;
				}
				
			}
		}
		//没找到
		return -1;
	}

下一篇: 从1开始学Java数据结构与算法——哈希表的内存布局与代码实现.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Java大魔王

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值