剑指offer -- 在排序数组中查找数字(三道题目应用二分查找思路)

题目一:数字在排序数组中出现的次数。如排序数组[1,2,3,3,3,3,4,5] ,其中3出现了4次,则返回4。
分析:方法一
输入的数组是排序的,那我们很容易想到用二分查找先找到一个3,但由于3可能出现多次,因此我们找到的3左右两边可能都有3,于是在找到的3左右两边顺序扫描分别找到第一个3和最后一个3。因为要查找的数字在长度为n数组里面有可能出现O(n)次,所以顺序扫描的时间复杂度是O(n)。这种算法的效率和从头开始顺序扫描到尾统计3的次数效率是相同的,所以我们来思考一下如何更好的利用二分查找算法。

int GetKCount(int* data, int length, int k){
    
    
	int start = 0;
	int end = length - 1;
	int middle = 0;
	int count = 0;

	while(start <= end){
    
    
		middle = (start + end)/2;
		if(data[middle] == target){
    
    
			int a = middle+1;
			while(middle >= 0 && data[middle] == k){
    
    
				count++;
				middle--;
			}
			while(a <= length-1 && data[a] == k){
    
    
				count++;
				a++;
			}
			return count;
		}
		else if(data[middle] > k)
			end = middle - 1;
		else
			start = middle +1;
	}
	return count;
}

方法二
我们前面利用二分算法是找到其中一个3,然后时间主要浪费在去扫描寻找第一个3和最后一个3,我们能否直接寻找到第一个和最后一个3呢?
我们先分析如何利用二分查找在数组中找到第一个k,先拿数组中间的数和k比较,如果数组中间的数大于k,那么k就只可能出现在数组的前半段,下一轮我们就只在数组前半段查找就可以了,反之,下一轮就只在数组的后半段查找。如果中间的数字和k相等,我们先判断这个是不是第一个k,如果中间的数字的前一个数字不是k,那么此时这个中间数字就是第一个k,如果中间数字的前一个数字是k,那么第一个肯定在数组的前半段,下一轮我们仍然需要在数组的前半段查找。在GetFirstK中,如果数组中不包含数字k将返回-1,如果数组中置少包含一个k,那么返回第一个k在数组中的下标。我们可以用同样的思路找到数组中最后一个k的位置。

int GetKCount(int *data,int length,int k){
    
    
	if(data == NULL || length<=0)
		return 0;

	int first = GetFirstK(data,length,k,0,length-1);
	int last = GetLastK(data,length,k,0,length-1);

	if(first > -1 && last > -1 )
		return last - first +1;
	else
		return 0;
}

int GetFirstK(int *data,int length,int k,int start,int end){
    
    
	if(start > end)
		return -1;

	int middleindex  = (end + start)/2;
	int middledata = data[middleindex];

	if(middledata == k){
    
    
		if(middleindex > 0 && data[middleindex-1] != k || middleindex == 0)
			return middleindex;
		else
			end = middleindex - 1;
	}
	else if(middledata > k)
		end = middleindex - 1;
	else
		start = middleindex +1;

	return GetFirstK(data,length,k,start,end);
}

int GetLastK(int *data ,int length ,int k,int start,int end){
    
    
	if(start > end)
		return -1;
	int middleindex  = (end + start)/2;
	int middledata = data[middleindex];

	if(middledata == k){
    
    
		if(middleindex < length-1 && data[middleindex+1] != k || middleindex == length-1)
			return middleindex;
		else
			start = middleindex + 1;
	}
	else if(middledata > k)
		end = middleindex - 1;
	else
		start = middleindex +1;

	return GetLastK(data,length,k,start,end);
}

在方法二的两段代码中GetFirstK和GetLastK都是用二分查找算法在数组中查找数字,时间复杂度都为O(logn),因此GetKCount的时间复杂度也只有O(logn)。

题目二:0 ~ n - 1中缺失的数字
一个长度为n - 1的递增排序数组中所有数字都是唯一的,且每个数字都在范围0 ~ n - 1之内。在范围0 ~ n - 1内的n个数字有且只有只有一个数字不在该数组内,请找出这个数字。如0~2范围内,数字有0、1和2,长度为2,如果为1、2则缺失0,如果为0、1则缺失2。

这道题有一个显然的解法我们可以利用等差数列求和公式,求出这n个数的总和sum1。再顺序遍历数组,求出数组内所有数字的总和sum2,sum1减去sum2得到的结果就是缺失的数字。

但显然我们没有利用到数字的特点递增排序。因为0~n-1这些数字在数组中是排序的,因此数组一开始的数字与他们的下标相同,如数组中缺失数字m,那么在m以前数组内的数据与下标相同,nums[m-2]=m-2,nums[m-1]=m-1,由于m不在数组中,在m以后的数组内的数据与下标不相同。则nums[m]=m+1;m+1在下标为m处,m+2在下标m+1处。因此这个问题转化成在排序数组中找到第一个值与下标不相等的元素。

我们可以基于二分查找,如果中间元素的值与下标相同,那么下一轮查找只需要查找右半边。如果中间元素的值与下标不相同,且中间元素前面的一个元素的值与下标相等,这意味着这个中间元素就是我们需要的第一个数字和下标不等的元素,返回下标即可。如果中间元素的值与下标不相同,且中间元素前面的一个元素的值与下标也不相同,则下一轮只需要查找左半边。

int missingNumber(int* nums, int numsSize){
    
    
	int start = 0;
	int end = numsSize - 1;
	int middle = 0;

	while(start <= end){
    
    
		middle = (start + end)/2;
		if(nums[middle] != middle){
    
    
			if(middle == 0 || nums[middle-1] == middle-1)
				return middle;
			end = middle - 1;
		}
		else
			start = middle+1;
		if(start == numsSize)
			return numsSize;
		
	}
	return -1;
}

题目三:
数组中数值和下标相等的元素。假设一个单调递增的数组里面的每个元素都是整数,且是唯一的,请找出数组里存在的任意一个数值等于其下标的元素。如在数组[-3,-1,1,3,5]中数字3和它的下标相等。

我们很容易想到也是最直观的解法就是从头到尾扫描数组寻找这个元素,显然这种算法的时间复杂度为O(n),由于数组是单调递增的,我们可以尝试用二分来优化。

我们扫描到一个元素的时候,如果它的值比下标大,由于数组内元素的值是递增的,那么它右边的值都比它大,最少也会依次递增1,下标就是依次递增1,而由于刚开始这个元素就比下标大,那么以后的所有元素也都会比对应的下标大,则下一轮应该在这个值的左边进行。 值比下标小的同理。

int GetNumberSameAsIndex(int *nums,int length){
    
    
	if(nums == NULL || length <= 0)
		return -1;
	int start = 0;
	int end  = length - 1;
	int middle = 0;

	while(start <= end){
    
    
		middle = (start + end)/2;

		if(nums[middle] == middle)
			return middle;
		if(nums[middle] > middle)
			end = middle - 1;
		else
			start = middle + 1;
	}
	return -1;
}

这是一个典型的二分查找过程。

Guess you like

Origin blog.csdn.net/scarificed/article/details/120634990