1 简介与实例
在有序数组中查找某一数据所在的下标,若存在该数据,返回下标,否则返回-1。
#include <cstdio>
int a[10]={1,3,5,7,9,11,13,15,17,19};
int value; //待查找元素的值
int index; //查找到的下标,如果没有查到,返回-1
int search_index(int a[],int n)
{
int l=0;
int r=n-1;
while(l<=r)
{
int mid=l+((r-l)>>2);
if(a[mid]==value)
return mid;
else if(a[mid]>value)
r=mid-1;
else
l=mid+1;
}
return -1;
}
int main()
{
printf("请输入您要查询的数据:\n");
while(scanf("%d",&value)!=EOF)
{
index=search_index(a,10);
printf("您所查询的数据%d所在的下标为:%d\n",value,index);
}
return 0;
}
以上是左闭右闭的写法,所谓左开右闭就是在[0,n-1]的区间查找,区间的左右边界都可以取到。
#include <cstdio>
int a[10]={1,3,5,7,9,11,13,15,17,19};
int value; //待查找元素的值
int index; //查找到的下标,如果没有查到,返回-1
int search_index(int a[],int n)
{
int l=0;
int r=n;
while(l<r)
{
int mid=l+((r-l)>>2);
if(a[mid]==value)
return mid;
else if(a[mid]>value)
r=mid;
else
l=mid+1;
}
return -1;
}
int main()
{
printf("请输入您要查询的数据:\n");
while(scanf("%d",&value)!=EOF)
{
index=search_index(a,10);
printf("您所查询的数据%d所在的下标为:%d\n",value,index);
}
return 0;
}
以上是左闭右开的写法,即在[0,n)的范围查找,右边的数字取不到。注意这种写法与左闭右闭写法的边界调整的区别。
2 二分的应用,变种与案例
二分查找常见的应用场景:
1 在有序序列中查找一个数,比如上边的例子。
2 并不一定非要在有序序列中得到应用,任意一个序列,只要在二分之后可以淘汰掉一半,那么就可以采用二分法。比如腾讯秋招笔试题目-贪吃的小Q
二分查找常见的考察点:
1 对于边界条件的考察以及代码实现的能力。
2 二分搜索常见题目的变化:
(1)给定处理或查找的对象不同。比如在有重复数值的数组中查找和在无重复数值的数组中查找时,处理情况的细节稍显差异。
(2)判断条件不同。一般需要查找一个数组中等于X的位置,我们也可以查找数组中大于或小于X的位置。
(3)要求返回的内容不同。返回任意一个等于X的位置,返回最早等于X的位置,或者返回等于X的个数等。
3 在有序循环数组中进行二分查找:
比如数组12345循环之后可以是:12345 23451 34512 45123 51234
二分搜索的重要提醒:
在选择中间节点时,选择的公式为mid=left+(right-left)/2, 防止数据过大时数据溢出。
案例一
本题可以用二分法解决。
1 如果数组为空或长度为0,返回-1,表示局部最小位置不存在。
2 如果数组长度为1,返回0,因为0便是局部最小位置。
3 如果数组长度大于1,先检查2头的位置。即:
(1)如果a[0]<a[1],返回0
(2)如果a[n-1]<a[n-2],返回n-1
4 如果2头不满足,说明a[0]>a[1],a[n-1]>a[n-2],所以从最左的位置向右看,有减小的趋势,从最右的位置向左看,也是减小的趋势。此时查看mid位置的数据:
(1)如果Mid位置的数据既比左边小,又比右边小,则为局部最小值。
(2)如果Mid的数据比右边小,比左边大,则mid往左是减小的趋势。取左部分继续查找。
(3)如果Mid的数据比左边小,比右边大,则Mid往右是减小的趋势,取右部分继续查找。
(4)如果mid的数据比左右都大,任选一侧都可以。
#include <cstdio>
const int maxn=20;
int n;
int a[maxn];
int index;
int find_part_min_index(int a[],int n)
{
if(n<=0)
return -1;
else if(n==1)
return 0;
else if(a[0]<a[1])
return 0;
else if(a[n-1]<a[n-2])
return n-1;
int left=0;
int right=n-1;
while(left<=right)
{
int mid=left+(right-left)>>1;
if(a[mid]<a[mid-1]&&a[mid]<a[mid+1])
return mid;
else if(a[mid]>a[mid-1]&&a[mid]<a[mid+1])
right=mid-1;
else if(a[mid]>a[mid+1]&&a[mid]<a[mid-1])
left=mid+1;
else
left=mid+1;
}
return -1;
}
int main()
{
while(true)
{
printf("请输入数组的元素个数:\n");
scanf("%d",&n);
printf("请输入数组的元素:\n");
for(int i=0;i<n;i++)
{
scanf("%d",&a[i]);
}
index=find_part_min_index(a,n);
printf("数组的局部最小位置下标为:%d\n",index);
printf("\n");
}
return 0;
}
案例二
给定一个有序数组arr,再给定一个整数num,请在数组中找到数字num这个数出现的最左边的位置。
思路:定义一个全局变量ans,用变量记录数据num最后一次出现的位置。如果最后没有找到,就返回-1.
#include <cstdio>
const int maxn=20;
int ans;
int a[maxn];
int n;
int num=3;
int find_num_of_min_index(int a[],int n)
{
if(n<=0)
return -1;
int left=0;
int right=n-1;
while(left<=right)
{
int mid=left+(right-left)/2;
if(a[mid]==num)
{
ans=mid;
right=mid-1;
continue;
}
else if(a[mid]>num)
{
right=mid-1;
}
else
left=mid+1;
}
if(a[ans]==num)
return ans;
else
return -1;
}
int main()
{
while(true)
{
printf("请输入数组的元素个数:\n");
scanf("%d",&n);
printf("请输入数组的元素:\n");
for(int i=0;i<n;i++)
{
scanf("%d",&a[i]);
}
int index=find_num_of_min_index(a,n);
printf("数组的关于数据%d的最小位置下标为:%d\n",num,index);
printf("\n");
}
return 0;
}
案例三
给定一个有序循环数组,返回数组中的最小值。
首先判断a[0]是否小于a[n-1],如果小于,说明数列已经有序,直接返回a[0]即可。
否则,a[0]>=a[n-1]:说明是逆序的:
(1)如果a[0]>a[M],则在0-m的范围找;(注意不是m-1)
(2)如果a[n-1]<a[m] ,则在m+1----n-1的范围去找
(3)否则,必有a[0]<=a[m],a[m]<=a[n-1],又a[0]>=a[n-1],则a[0]=a[n-1]=a[m]。这个时候的情况就如:2222222221222。只可以通过遍历来确定了。
#include <cstdio>
const int maxn=20;
int min_data;
int a[maxn];
int n;
int find_min_data(int a[],int n)
{
if(n<=0)
return -1;
else if(n==1)
return a[0];
else if(a[0]<a[n-1])
return a[0];
else
{
int left=0;
int right=n-1;
while(left<=right)
{
int mid=left+(right-left)/2;
if(a[0]>a[mid])
{
right=mid;
continue;
}
else if(a[n-1]<a[mid])
{
left=mid+1;
continue;
}
else
{
int ans=a[0];
for(int i=1;i<n;i++)
{
if(ans>a[i])
ans=a[i];
}
return ans;
}
}
return -1;
}
}
int main()
{
while(true)
{
printf("请输入数组的元素个数:\n");
scanf("%d",&n);
printf("请输入数组的元素:\n");
for(int i=0;i<n;i++)
{
scanf("%d",&a[i]);
}
min_data=find_min_data(a,n);
printf("数组的最小值:%d\n",min_data);
printf("\n");
}
return 0;
}
案例四
给定一个有序数组arr,其中不含有重复元素,请找出满足arr[i]==i的条件最左的位置,如果所有位置上的数都不满足,返回-1。
思路如下:
(1)首先判断a[0]>=N-1,一定不存在。
(2)再判断a[N-1]<=0,同样不存在。
(3)观察中间元素:如果a[mid]>mid,right=mid-1;如果a[mid]<mid,left=mid+1;否则,记录该位置,继续right=mid-1;
#include <cstdio>
int res=-1; //记录最后出现的位置
const int maxn=100;
int a[maxn];
int find_index(int a[],int n)
{
if(n<=0)
return -1;
if(n==1)
{
if(a[0]==0)
return 0;
else
return -1;
}
if(a[0]>=n-1)
return -1;
if(a[n-1]<=0)
return -1;
int left=0;
int right=n-1;
while(left<=right)
{
int mid=left+(right-left)/2;
if(a[mid]>mid)
{
right=mid-1;
continue;
}
else if(a[mid]>mid)
{
left=mid+1;
continue;
}
else
{
res=mid;
right=mid-1;
}
}
return res;
}
int main()
{
int n;
while(scanf("%d",&n)!=EOF)
{
for(int i=0;i<n;i++)
{
scanf("%d",&a[i]);
}
res=find_index(a,n);
printf("%d\n\n",res);
}
return 0;
}
案例五
给定一颗完全二叉树头节点head,返回这棵树节点的个数,若完全二叉树节点数为N,请实现时间复杂度低于O(N)的解法。
首先找到二叉树最左边的节点,目的是统计出二叉树的高度/深度。
接着,找到二叉树头节点的右子树的最左边节点,如果该节点可以到达最后一层,说明头节点左子树是一个满二叉树。
这时,求出左子树节点个数加上根节点,右子树节点个数可以通过递归求取。
如果该节点不可以到达最后一层,说明头节点的右子树是一颗完全二叉树。这时候求右子树节点个数加上根节点,左子树节点个数可以通过递归求得。
其时间复杂度为O(logN^2)
案例六
如何更快地求取一个整数k的N次方。若2个整数相乘并得到结果的时间复杂度为O(1),得到整数k的N次方的过程请实现时间复杂度为O(logN)的方法。
法一:
N为偶数
N为奇数
递归法:
#include <cstdio>
int base,exp;
int PowerWithExponent(int base,unsigned int exp)
{
if(exp==0)
return 1;
if(exp==1)
return base;
int result=PowerWithExponent(base,exp>>1);
result*=result;
if(exp&0x1==1)
result*=base;
return result;
}
int main()
{
printf("请输入底数和指数:\n");
scanf("%d%d",&base,&exp);
int result=PowerWithExponent(base,exp);
printf("%d\n",result);
return 0;
}
法二:
例如求,先把75写作二进制形式:1001011
即
那么我们可以先求出。再根据二进制相应位置是否为1选择性地进行乘法。