【剑指offer】——二分查找相关例题总结

一、旋转数组的最小值

题目要求:

把一个数组最开始的若干个元素搬到数组的末尾,我们称之为数组的旋转。
输入一个非递减排序的数组的一个旋转,输出旋转数组的最小元素。
例如数组{3,4,5,1,2}为{1,2,3,4,5}的一个旋转,该数组的最小值为1。
NOTE:给出的所有元素都大于0,若数组大小为0,请返回0。

题目分析:

首先,我们可以明确的是该旋转数组的规律
旋转之后的数组实际上可以划分为两个排序的子数组,而且前面子数组的元素都大于或者等于后面子数组的元素
这样,我们就可以依照这个规律来探索问题的答案了。我们还是以简单通俗易懂的方式,以具体的列子来分析。

情况一:
以数组{3,4,5,1,2}为列子。首先定义两个指针,分别指向数组的第一个元素和最后一个元素。有阴影的部分就是第二个子数组
在这里插入图片描述
计算此时的中间元素下标=(0+4)/2=2。下标为2的数组元素值为5.由于5>p1所指向的数字3,所以中间的数值在第一个子数组中。把p1指向中间的数字。
在这里插入图片描述
此时p1,p2中间的数字1小于p2指向的数字。中间的数字在第二个子数组中。把p2指向中间的数字。
在这里插入图片描述
此时,p1和p2指向两个相邻的数字,则
p2
指向的是数组中的最小数字。
为什么p2为最小数字?
因为当两个指针相距只有一个位置的时候表明p1已经指向了第一个递增数组的最后一个元素,p2指向了第二个递增数组的第一个元素。所以p2指向的即为数组中的最小元素。

情况二:
当遇到p1,p2和中间的指针指向的数字都相同的情况下时,如数组{0,1,1,1,1}。{1,0,1,1,1}和{1,1,1,0,1}都为该递增数组的旋转
在这里插入图片描述
如上图所示,这种情况就不明白中间元素到底是属于哪一个递增数组的,所以就不得不采取顺序查找的方式进行。

情况三:
如果把排序的数组的前面0个元素搬到最后面,即数组本身,这仍然是数组的一个旋转。
这种情况就直接返回p1说指向的元素即可。这也是把indexMid赋值给p1的原因。

代码实现

int MinInOrder(int* numbers, int p1, int p2)
{
    int result = numbers[p1];
    for (int i = p1 + 1; i <= p2; i++)
    {
        if (result > numbers[i])
            result = numbers[i];
    }
    return result;
}
int Min(int* numbers, int length)
{
	if (numbers == nullptr || length <= 0)
		throw new::std::exception("Invalid parameters");

	int p1 = 0;
	int p2 = length - 1;
	int indexmid = p1;
	while (numbers[p1] >= numbers[p2])
	{
		//情况一
		if (p2 - p1 == 1)
		{
			indexmid = p2;
			break;
		}
		indexmid = (p1 + p2) / 2;

		//情况二
		if (numbers[p1] == numbers[p2] == numbers[indexmid])
		{
			return MinInOrder(numbers, p1, p2);
		}
		//缩小范围
		if (numbers[indexmid] >= numbers[p1])
			p1 = indexmid;
		else if(numbers[indexmid] <= numbers[p2])
			p2 = indexmid;
	}
	return numbers[indexmid];
}

二、在排序数组中查找数字

题目一:数字在排序数组中出现的次数

统计一个数字在排序数组中出现的次数。比如说输入排序数组{1,2,3,3,3,3,4,5}和数字3,由于3在这个数组中出现了4次,因此输出4。

分析:
由于该数组是有序的,所以我们自然会联想到使用二分查找。首选利用二分查找找到一个3,再在3的左右两边顺序扫描,分别找出第一个3和最后一个3即可。

查找该数组中的第一个3:
基于上面二分查找找到了数字3,但是现在还不明确这个3是否是该数组中的第一个3。所以,我们还得判断3前面的一个数字
在这里插入图片描述
如果图中的X,如果X不为3,则该数组的第一个3为刚才查找到的3.
如果图中的X是3,则第一个3肯定在数组的前半段,下一轮我们仍然需要在数组的前半段查找。
代码实现如下:

int GetFirstK(int* data, int length, int k, int start, int end)
{
    if (start > end)
        return -1;
    int mid = (start + end) / 2;
    int middleData = data[mid];

    if (middleData == k)
    {
        if ((mid > 0 && data[mid - 1] != k) || mid == 0)
            return mid;
        else
            end = mid - 1;
    }
    else if (middleData > k)
    {
        end = mid - 1;
    }
    else
    start = mid + 1;
    return GetFirstK(data, length, k, start, end);
}

没有找到3返回-1,找到了数组中开始的3返回其下标

查找该数组中的最后一个3:
查找最后一个3的方法与上述类似,基于递归写出的代码如下:

int GetLastK(int* data, int length, int k, int start, int end)
{
    if (start > end)
        return -1;
    int mid = (start + end) / 2;
    int middleData = data[mid];
    if (middleData == k)
    {
        if ((mid < length-1 && data[mid + 1] != k) || mid == length-1)
            return mid;
        else
            start = mid + 1;
    }
    else if (middleData < k)
    {
        start = mid + 1;
    }
    else
      end = mid - 1;
    return GetLastK(data, length, k, start, end);
}

找出了数组中第一个3和最后一个3的下标之后,就能计算出数组中出现的次数,代码如下:

int GetNumberOfK(int* data, int length, int k)
{
    int count = 0;
    if (data != nullptr && length > 0)
    {
        int first = GetFirstK(data, length, k, 0, length - 1);
        int last = GetLastK(data, length, k, 0, length - 1);

        if (first > -1 && last > -1)
            count = last - first + 1;
    }
    return count;
}

题目二:0~n-1中缺失的数字

一个长度为n-1的递增数组中所有的数字都是唯一的,并且每个数字都在范围0~n-1之内,在范围内的n个数字中有且只有一个数字不在该数组中,请找出这个数字

题目分析:
方法一:
我们可以先计算数字0到n-1的所有数字之和为s1,再计算数组中所有数字之和为s2,用s1-s2所得的差值即为0~n-1中缺失的数字。
但是这种方法不好,因为没有利用到题目中给出的数组递增的规律,再者他的时间复杂度是O(n)

方法二:
我们可以利用二分查找的思想来求解
因为0~n-1这些数字在数组中是排序的,因此这些数字开始的部分与数组下标相同,假设数字m为数组中缺失的那个数字,则m以后的数字和下标不是对应的关系
在这里插入图片描述
因此这个问题就转换成了在排序数组中寻找第一个值和下标不相等的元素

1、如果中间元素的值和下标相等,那么下一轮只需要在右边部分查找
2、如果中间的元素值和下标不等

  1. 并且他的前面一个元素和他的下标不相等,则该元素则是丢失的元素
  2. 并且他的前面一个元素和他的下标不相等,就意味着下一轮查找我们只需要在右边查找即可

综上所述,代码实现如下:

int GetMissingNumbers(const int* numbers, int length)
{
    if (numbers == nullptr || length <= 0)
        return -1;

    int left = 0;
    int right = length - 1;
    
    while (left <= right)
    {
        int mid = (left + length) / 2;
        if (numbers[mid] != mid)
        {
            if (numbers[mid - 1] == mid - 1 || mid == 0)
                return mid;
            else
                right = mid - 1;
        }
        else
            left = mid + 1;
    }
    //缺失的是最后一个数字
    if (left == length)
        return length;

    return -1;
}

题目三:数组中数值和下标相等的元素

假设一个单调递增的数组里的每个元素都是整数并且是唯一的。请编程实现一个函数找出数组中任意一个数值等于其下标的元素。例如,在数组{-3, -1, 1, 3, 5}中,数字3和它的下标相等。

题目分析:
还是充分利用数组递增的规律,假设第i个数字的值大于下标i,那么他右边的数字都大于对应的下标。下一轮的查找只需要在她的左边的数字中查找即可
假设第i个数字的值小于下标i,那么他的左边的的数字都小于对应的小编。下一轮的查找只需要在她的右边的数字中查找即可。
基于上述说法,代码实现如下:

int GetNumberSameAsIndex( int* numbers, int length)
{
    if (numbers == nullptr || length <= 0)
        return -1;

    int left = 0;
    int right = length - 1;

    while (left <= right)
    {
        int mid = (right - left ) / 2 + left;
        if (numbers[mid] = mid)
            return mid;
        if (numbers[mid] < mid)
            left = mid + 1;
        else
            right = mid + 1;
    }
    return -1;
}
发布了98 篇原创文章 · 获赞 9 · 访问量 3654

猜你喜欢

转载自blog.csdn.net/qq_43412060/article/details/105338437
今日推荐