`
frank-liu
  • 浏览: 1684679 次
  • 性别: Icon_minigender_1
  • 来自: 北京
社区版块
存档分类
最新评论

binary search及扩展出来的几个问题讨论

 
阅读更多

简介

    binary search是一个几乎大家耳熟能详的问题,只要一提到这个问题,似乎立马就有人把代码都浮现在头脑里了。它本质上就是通过不断的折半查找来缩小范围,这样可以达到一个很理想的运行效率。这个方法本身有几个小的地方值得注意。另外,通过binary search引申出来的一些问题也是各种各样,这里也针对两个比较有意思的问题做了一点分析。

binary search的思路和实现

    如果用一个比较简单直白的语言来介绍binary search的话,无非就是在一个已经排好序的数组里,通过每次将元素的中间值和目标元素来比较,如果中间值大的话,则在前面部分范围内继续查找,否则在后面的部分继续查找。每次求的这个中位数就将数组分成了两半。这样后面每次查找的范围也就缩小了。假设一个数组的元素有n个,每次执行一个折半元素范围就减少一半。从直观上来看,顶多lgN次的折半也就得到一个结果了。所以,binary search方法的时间复杂度为lgn。

    下面是binary search的两个实现:

public static int search(int[] a, int val)
{
	int start = 0;
	int end = a.length - 1;
	int middle;
	while(start <= end)
	{
		middle = start + (end - start) / 2;
		if(val > a[middle])
		{
			start = middle + 1;
		}
		else if(val < a[middle])
		{
			end = middle - 1;
		}
		else
			return middle;
	}

	return -1;
}

public static int recSearch(int[] a, int start, int end, int val)
{
	if(start > end)
		return -1;

	int middle = start + (end - start) / 2;
	if(val == a[middle])
		return middle;
	else if(val > a[middle])
		return recSearch(a, middle + 1, end, val);
	else
		return recSearch(a, start, middle - 1, val);
}

    看这些具体的代码,实际上有几个细节是很值得注意的。1. 我们在while循环里面比较的条件是start <= end。 这里一定要把等于的情况也考虑进去。因为有可能在搜索到start == end这一步,我们还是需要判断一下是否和结果相等,否则会漏掉这么一种特殊的情况。2. 在求两个数start, end的中位数的时候,一般我们会笼统的用(start + end) / 2这种方式。但是,在数字比较大的情况下,如果start + end > Integer.MAX_VALUE的话,则会使得首先括号里的运算产生溢出,进而产生不正确的结果。所以为了避免这种情况需要使用start + (end - start) / 2。

 

引申出来的几个问题

    可以说,前面讨论的binary search没什么新奇的,最不济细心看看也就都弄明白了。可是如果在这个问题的基础上结合一些其他情况引申出来的问题,事情就会复杂很多:

问题一:

    前面我们讨论的binary search是基于所有的元素都是排好序的情况,而且一般是从小到大的顺序。现在考虑一种特殊情况,如果我们有一部分元素循环移位了k个位置,那么和原来的顺序比较可能会有如下的结果:

     现在,在这种情况下,如果我们也期望达到logn的元素查找效果的话,该怎么去做呢?根据上图的描述,如果我们将一个递增的数组循环移位若干位的时候,它将会变成一个由两个递增的序列组成的数组。在这两个序列中间是由一个最大值和最小值作为间隔隔开,好像突然间冒出来的一个坎。由于这么个移位,我们如果取数组中间值mid的时候,它可能落在前面那个递增的串上也可能落在后面那个递增的串上。

 经过前面这部分的讨论,我们可以得到重要的一点就是,我们对数组取中值,它只可能落在两个递增的串上面。现在,我们再对这两种情况进行讨论:

1. 假定是落在前面的递增串上

    按照前面的理解,如果取的中值点能落在前面的串上,则前面这个串至少要涵盖到n/2这么长的长度。也就是说整体的数组移位要超过数组长度的一半。比如下图:

    这个时候,如果我们要来查找需要的目标值,可能就需要进行一些比较。由于mid是在前面的递增串中,我们需要看目标值是否正好落在这个递增的范围内,也就是说判断是否val <= a[mid] && val >= a[start],如果是的,则可以在这个范围内按照二分法的思路继续缩小范围。

    同时,我们还要考虑,如果目标值不在这个区域内,那么可能目标值比a[mid]大,也可能目标值比a[start]小。因为mid是落在前面的递增串,它后面的值是可能存在比它更大的。比a[start]小的值也有可能落在后面那个递增的串上。所以,在这种情况下,也就是val < a[start] || val > a[mid],我们需要在数组的后面部分来搜索目标值。

 

2. 落在后面的递增串上

    落在后面的递增串典型情景如下图:

    和前面的讨论类似,我们需要比较目标值是否在后面的递增串内,如果在,则满足val >= a[mid] && val <= a[end]。我们继续在mid和end之间查找。否则,则有val < a[mid] || val > a[end],对于这种情况,则在start和mid之间查找。

    好了,经过前面的这么些讨论,我们知道在这两种情况下的选择。那么,还有一个问题就是,你怎么知道它这个mid值是落在前面的递增串还是后面的递增串呢?通过仔细观察移位后的情况,我们发现,如果a[mid]  >= a[start],则我们可以说明它落在前面的递增串;如果a[mid] <= a[end],则说明它落在后面的递增串。

    有了前面的讨论,我们就可以很容易的写出一个二分搜索方法的变体:

public static int genericSearch(int[] a, int value) {
	int start = 0, end = a.length - 1;
	int mid;
	while(start <= end) {
		mid = start + (end - start) / 2;
		if(a[mid] == value)
			return mid;
		if(a[mid] <= a[end]) {
			if(value >= a[mid] && value <= a[end])
				start = mid + 1;
			else end = mid - 1;
		} else {
			if(value >= a[start] && value <= a[mid])
				end = mid - 1;
			else start = mid + 1;
		}
	}
	return -1;
}

     这里除了上面讨论的地方,还有一个要注意的点就是当里面的元素有相等的情况该如何考虑和处理。实际运行程序的时候,很可能会出现这个时候start == mid或者end == mid的情况。所以,我们在比较的地方判断的时候要加上等号。

    还有一个有意思的地方就是,虽然我们是针对数组元素移位的情况进行的讨论,如果我们用一个已经排序的数组运行这一段程序,也可以有效的把元素查找出来。为什么呢?你猜:)

 

问题二: 

    给定两个已经排序的数组,求它们的中位数。对于这个中位数,我们的理解可能会存在一定的偏差。一般来说会把这个数字当成一个数组中间的那个元素。比如说[1, 2, 3, 4, 5],对于它来说,中位数是3。可是对于有偶数个元素的数组呢?它的n/2和n/2 + 1是最中间的两个元素。

    实际上,中位数指的是如下的关系:

median = a[n/2 + 1] (如果n为奇数)

median = (a[n/2] + a[n/2 + 1]) /2 (如果n为偶数) 

    对于奇数个元素来说,它最中间的那个元素确实是使得它两边的元素一样多。而对于偶数个的元素来说,它最中间的是两个元素,所以要取这两个元素的平均值。

    基于上述的讨论,假设我们有两个数组[1, 2, 3] [5, 6, 7],那么它们的中间值则为(3 + 5) / 2 = 4。现在,这个问题就归结为求给定两个数组里的第k个值。我们这里的中间值正好是求第k个值的一种特例。

    现在,我们就从一个一般的情况来分析取第k个元素的场景。假设我们要取第k个元素的时候,在数组a中间选择的元素点是i,这个i表示的是a中间的第i个,不是索引i。那么,对应的在另外一个数组b中间对应位置的k-i个是我们需要比较的对象。它们的关系如下图:

    如果这个时候位置为i的元素,也就是数组a里a[i - 1]的元素大于对应比较的元素,即b[k - i - 1]的元素,这表示至少k-i个元素和ii-1个元素是比我们目前第i个元素小的。如果这个时候,a[i-1]比b[k-i-1]后面那个元素小的话,则表示刚好它就是我们要找的元素。否则我们就要再缩小范围来比较。

    我们该怎么来调整这个搜索范围呢?针对这两种情况来讨论,假如a[i - 1] > b[k - i],这个时候表示a串中第i个元素它在对应的数组b中排到比期望的还要靠后才满足a[i-1] < a[m], m > k - i。所以这个时候我们要找的元素肯定在a[i - 1]的左边范围内。因为在a[i-1]右边的只会更大。这种情况如下:

    对于另外一种情况,就是如果a[i-1] < b[k - i - 1],那么表示a[i-1]这个元素所大于的元素还不到k个,这个时候需要去看a[i-1]后面的元素,如下图:

      按照这边的讨论,我们相当于找到了一个逐步递归的步骤。但是递归的结束条件是什么呢?假设我们已经访问到数组a最小的元素,也就是a[0]了。这个时候,我们只需要直接去看b[k-1]。这里的k-1其实是相对于b数组的起始位置来说的。因为前面的每次递归我们都采用类似二分法的方式将搜索的范围减少一半。另外,如果我们要找的元素为第1个了,这个时候只要比较a[0], b[0]中间两者最小的那个就可以了。这里的a[0], b[0]也是相对位置。

    还要一个前面没有澄清的问题,就是我们假设是在数组a中间取到第i个元素。那么最开始我们该设定到哪个位置比较合适呢?随便乱取一个?从二分法的角度来说,我们可以先取k/2个元素,假设a[k/2-1]这个元素也满足> b[k/2-1]并且<b[k/2]的话不就是正好吗?如果没有则按照前面给定的递归关系进行调整。

    这个问题最磨人的地方在于它的各种边界条件非常多。这还没完。我们前面是说假设要在a[]里面去取第k/2个元素。可是如果k/2比数组的长度还要大呢?这不是折腾死个人吗?对于这种情况,我们就需要去选择k/2和a[]长度中的最小值。然后再去计算b中间对应位置的元素。所以这部分的逻辑伪代码应该是这样:

pa = Math.min(k / 2, m);  // m为a[]的长度
pb = k - pa;

  

    有了前面的这些讨论,我们可以得出如下的代码:

 

public class Solution {
    public double findMedianSortedArrays(int A[], int B[]) {
        int m = A.length;
        int n = B.length;
        int total = m + n;
        if(total % 2 == 1) 
            return findMedian(A, 0, m, B, 0, n, total / 2 + 1);
        else 
            return (findMedian(A, 0, m, B, 0, n, total / 2) + 
                findMedian(A, 0, m, B, 0, n, total / 2 + 1)) / 2.0;
    }
    
    private double findMedian(int[] a, int al, int ar, int[] b, int bl, int br, int k) {
        if(ar > br) return findMedian(b, bl, br, a, al, ar, k);
        if(ar == 0) return b[bl + k - 1];
        if(k == 1) return Math.min(a[al], b[bl]);
        int pa = Math.min(k / 2, ar);
        int pb = k - pa;
        if(a[al + pa - 1] <= b[bl + pb - 1]) return findMedian(a, al + pa, ar - pa, b, bl, br, k - pa);
        return findMedian(a, al, ar, b, bl + pb, br - pb, k - pb);
    }
}

     代码中间有一些细节需要注意。因为每次我们是去计算k/2, m的最小值。然后去比较。如果两个节点的位置不对,要么就是在a[]数组上去看另外一段的位置,这样对应的参数ar, br需要相应的减少pa或者pb。同时,对应的位置查找参数k也要减去相应的pa, pb。

 

总结

    binary search本身的思想比较简单,可是由它引申出来的问题可以有很多。对于第二个问题,目前的理解和表示还是不够深刻,后续会继续完善对它的表述。

参考资料

http://community.topcoder.com/tc?module=Static&d1=tutorials&d2=binarySearch

  • 大小: 3.9 KB
  • 大小: 4 KB
  • 大小: 4.1 KB
  • 大小: 1.9 KB
  • 大小: 2 KB
  • 大小: 3.9 KB
  • 大小: 8 KB
  • 大小: 6.1 KB
分享到:
评论

相关推荐

    二分查找算法BinarySearch.rar

    实验可能包括以下几个方面: 1. **算法实现**:使用MATLAB编程语言实现二分查找算法。MATLAB是一款强大的数值计算软件,适合进行算法的快速原型设计和测试。二分查找的MATLAB代码通常会包含一个函数,输入为有序...

    binary tree C语言算法

    在C语言中实现二叉树,我们需要关注几个关键的概念和操作: 1. **节点定义**: 在C语言中,首先我们需要定义二叉树的节点结构。一个简单的二叉树节点结构可能如下所示: ```c typedef struct Node { int data; ...

    阿里云Elasticsearch7.x jar包.zip

    在Elasticsearch 7.4.0版本中,我们看到了几个关键组件和依赖项的更新。首先,`elasticsearch-7.4.0.jar`是核心的Elasticsearch库,包含了所有必需的功能,如索引、搜索、集群管理和数据处理。这个版本引入了对Java ...

    leetcode卡-leetcode_practices_learncard_binarysearch:我的leetcode实践二分搜索学习卡

    在这个`leetcode_practices_learncard_binarysearch`项目中,我们可以看到作者用Java8语言实践了二分搜索算法,并且在LeetCode平台上完成了所有相关题目,达到了100%的完成度。 二分搜索的基本步骤如下: 1. **...

    es的accounts.json文档及使用.docx

    在深入探讨Elasticsearch(简称ES)的accounts.json文档及其使用之前,首先了解Elasticsearch的基本概念是非常重要的。Elasticsearch是一种开源的全文搜索引擎,基于Lucene构建,它提供了分布式、实时、可扩展的搜索...

    《C语言程序设计》课程设计报告-学生信息管理系统.doc

    在本篇《C语言程序设计》课程设计报告中,学生设计了一个学生信息管理系统,该系统主要包含以下几个核心知识点: 一、结构体与数组 在C语言中,结构体是一种自定义的数据类型,允许我们将不同类型的数据组合在一起...

    数据结构各章实验 图的遍历 各种排序 二叉排序树

    本资源包聚焦于几个关键概念:图的遍历、各种排序算法以及二叉排序树,这些都是数据结构学习中的重要章节。 首先,让我们深入探讨图的遍历。图是由顶点(或节点)和边构成的数据结构,广泛应用于网络、地图、关系...

    Java 算法源码集合

    2. **查找算法**:例如二分查找(Binary Search)、哈希查找(Hash Search)以及顺序查找(Sequential Search)。查找算法用于在数据结构中寻找特定元素,它们的效率往往取决于数据结构的特性。 3. **图算法**:如...

    面试常考算法模板 Cheat Sheet V4.31

    int binarySearch(int[] nums, int target) { // 角落情况处理 if (nums == null || nums.length == 0) return -1; int start = 0, end = nums.length - 1; while (start + 1 ) { int mid = start + (end - ...

    常用的几种简单算法,很简单的

    4. **二分查找**(BinarySearch): 适用于有序列表的搜索算法,其时间复杂度为O(log n)。通过不断将查找区间减半,直到找到目标元素或确定不存在为止。 5. **欧几里得算法**(Gcd): 计算两个整数的最大公约数...

    《算法》(第四版)的实现源代码,algs4.zip

    这些类主要可以分为以下几个类别: 1. **基础数据结构**:包括ArrayBag、ArrayStack、ArrayQueue、BinaryHeap、ResizableArray、Deque等。ArrayBag实现了可变大小的集合,ArrayStack和ArrayQueue分别实现了基于数组...

    算法题目是计算机科学和软件工程师面试中常见的考察点.docx

    根据给定文件的信息,我们可以总结出以下几个重要的知识点: ## 常见的计算机科学与软件工程师面试算法题目 ### 1. 快速排序(Quick Sort) 快速排序是一种高效的排序算法,采用分治策略来把一个序列分为较小和较...

    java算法大全源码包.zip

    桶排序将数据分到几个有序的桶里,每个桶再分别排序,最后把所有桶中的数据合并成一个有序序列。 2. **ins_sort**: 指的是插入排序(Insertion Sort),一种简单直观的排序算法,它的工作原理是通过构建有序序列,...

    VC++2012编程演练数据结构《25》线索二叉树

    在提供的文件中,可以看到几个关键的源代码文件,例如`TBSTree.cpp`、`ITBSTree.h`等,这些文件可能包含了线索二叉树的实现和接口定义。 `TBSTree.cpp`:这个文件很可能是实现线索二叉树的具体操作,如插入、删除、...

    麻省理工学院-算法导论

    从哈希表(Hash Tables)到二叉搜索树(Binary Search Trees),再到红黑树(Red-Black Trees)和增强数据结构(Augmenting Data Structures),这几章深入探讨了各种高级数据结构的设计和应用,这些都是实现高效...

    tree_left_rotate.rar_Different

    根据压缩包内的文件名称,我们可以推测以下几个可能的知识点: 1. **spl_inl.c**: 这个文件名可能代表"split inline",可能包含一些关于内联函数或分割操作的代码,内联函数在C语言中用于提高程序执行效率,通过将...

    算法导论课后习题与思考题答案合集

    7. 第12章:二叉搜索树(Binary Search Trees) 二叉搜索树是有序数据集合的有效表示,它能够快速执行查找、插入和删除操作。本章主要讨论了二叉搜索树的性质和操作,以及树的平衡问题和AVL树。 8. 第13章:红黑树...

    5.2_1_二叉树的定义和基本术语1

    二叉树有几种特殊形态,包括空二叉树、只有一个根节点的二叉树、只有左子树或只有右子树的二叉树,以及左右子树都存在的二叉树。 **满二叉树**是一种特殊的二叉树,它的特点是: 1. 只有最后一层有叶子节点。 2. 不...

    清华大学 数据结构 C++描述

    本资料包含以下几个方面的重要知识点: 1. **链表**:链表是一种动态数据结构,节点在内存中不是连续存储的。包括单链表、双向链表和环形链表,以及它们的插入、删除和遍历操作。C++中可以通过定义结构体或类来实现...

Global site tag (gtag.js) - Google Analytics