目录
在介绍二分算法之前,我们先来介绍一下时间复杂度的概念!
时间复杂度
- 一个程序或算法的时间效率,也称“时间复杂度”,有时简称“复杂度”。
- 复杂度常用大的字母O和小写字母n来表示,比如O(n),O(n2 )等。n代表问题的规模,也理解为需要处理数据的量。
- 时间复杂度是用算法运行过程中,某种时间固定的操作需要被执行的次数和n的关系来度量的。在无序数列中查找某个数,复杂度是O(n)。
- 计算复杂度的时候,只统计执行次数最多的(n足够大时)那种固定操作的次数。比如某个算法需要执行加法n^2次,除法n次,那么就记其复杂度是O(n2 )的。而且时间复杂度也忽略系数,比如O(n/2),也记作O(n)的时间复杂度。
- 如果复杂度是多个n的函数之和,则只关心随n的增长增长得最快的那个函数。
- 复杂度有“平均复杂度”和“最坏复杂度”两种。两者可能一致,也可能不一致。
比如快速排序,平均复杂度为 O(log(n)),但最坏复杂度却为O(n^2)。
种类
接下来我们来看一下时间复杂度都有哪些种类吧!
- 常数复杂度:O(1) 时间(操作次数)和问题的规模无关
- 对数复杂度:O(log(n))
- 线性复杂度:O(n)
- 多项式复杂度:O(n^k ) K为恒定实数
- 指数复杂度:O(a^n )
- 阶乘复杂度:O(n! )
二分算法
说起二分算法,大家可能第一时间想到的是猜数字游戏:就是猜1到100之间的一个数字 ,我们的最优解法是每次都猜中间,来做到不断缩小数字的准确范围,我们的二分算法也是类似的思想:在一个有序数组找特定的数,每次都缩小区间,直到找到需要找到的数。
模板
我们在此先给出二分算法的模板,基本所有的二分算法都是在模板上做出修改的,所以我们先来了解最基础的二分算法。
写一个函数BinarySeach,在包含size个元素的、从小到大排序的int数组a里查找元素 p,如果找到,则返回元素下标,如果找不到,则返回-1。要求复杂度O(log(n))
int BinarySearch(int a[],int size,int p)
{
int L = 0; // 查找区间的左端点
int R = size - 1; // 查找区间的右端点
while( L <= R) { // 如果查找区间不为空就继续查找
int mid = L+(R-L) >> 1; // 取查找区间正中元素的下标,防止溢出(相当于 L+(R-L)/2)
if( p == a[mid] ) // 如果找到了就返回
return mid;
else if( p > a[mid])
L = mid + 1; //设置新的查找区间的左端点
else
R = mid - 1; //设置新的查找区间的右端点
}
return -1;// 没找到就返回-1
} //复杂度O(log(n))
写一个函数LowerBound,在包含size个元素的、从小到大排序的int数组a里查找比给 定整数p小的,下标最大的元素。找到则返回其下标,找不到则返回-1。
int LowerBound(int a[],int size,int p) //复杂度O(log(n))
{
int L = 0; //查找区间的左端点
int R = size - 1; //查找区间的右端点
int lastPos = -1; //到目前为止找到的最优解
while( L <= R) { //如果查找区间不为空就继续查找
int mid = L+(R-L)/ >> 1; //取查找区间正中元素的下标
if(a[mid]>= p)
R = mid - 1;// 设置右端点
else {
lastPos = mid; // 设置左端点
L = mid+1;
}
}
return lastPos;
}
例题
1.求方程的根
- 题目
求下面方程的一个根:f(x) = x3 -5x2+10x-80 = 0 若求出的根是a,则要求 |f(a)| <= 10^-6
- 输入
无
- 输出
方程的一个根
解题思路
思路当然是二分法啦!对f(x)求导,得f'(x)=3x2-10x+10。由一元二次方程求根公式知方程 f'(x)= 0 无解,因此f'(x)恒大于0。故f(x)是单调递增的。易知 f(0) < 0且 f(100)>0,所以区间[0,100]内必然有且只有一个根。由于f(x)在[0,100]内是单 调的,所以可以用二分的办法在区间[0,100]中寻找根。
代码实现如下所示:
#include<stdio.h>
#include<math.h>
#define EPS 1e-6
double f(double x) // 定义函数
{
return x*x*x - 5*x*x + 10*x - 80;
}
int main() {
double root, x1 = 0, x2 = 100,y;
root = x1+(x2-x1) / 2;
y = f(root);
while( fabs(y) > EPS) {
if( y > 0 ) x2 = root;// 右端点左移
else x1 = root;// 左端点右移
root = x1+(x2 - x1)/2;
y = f(root);
}
printf("%.8f\n",root);
return 0;
}
运行结果如下所示
总结
一个简单的二分法,不是很蓝的啦!
2.寻找指定和的整数对
- 题目
输入n ( n<= 100,000)个整数,找出其中的两个数,它们之和等于整数m(假定肯定有解)。题中所有整数都能用 int 表示。
解题思路
一开始当然是万能的for循环啦,两重循环,遍历所有的可能,但本题数据太大了,可能过不了。
// 两层for循环,失败!
哎,说到底,这道题目就是找值嘛!我们可以使用二分查找来找啊!所以我们先排序,然后找值,但是呢,排序得用快速排序哦,这样才能降低复杂度,好了,我们来康康代码吧!
#include<stdio.h>
int cmp(const void* a, const void* b);
int main(void)
{
int n, i ,min = 1000,m;
// 输入
scanf("%d %d", &n,&m);
int a[n];
for(i = 0;i < n; i++){
scanf("%d", &a[i]);
}
//排序
qsort(a, n, sizeof(a[0]), cmp);
//遍历
for(i = 0;i < n - 1; i++){
if(BinarySearch(a,n,m-a[i]) != -1){
printf("%d %d\n",a[i],m-a[i]);
}
}
return 0;
}
// 升序排序
int cmp(const void* a, const void* b)
{
return *(int *)a - *(int *)b;
}
// 查找
int BinarySearch(int a[],int size,int p)
{
int L = 0; // 查找区间的左端点
int R = size - 1; // 查找区间的右端点
while( L <= R) { // 如果查找区间不为空就继续查找
int mid = L+(R-L) / 2; // 取查找区间正中元素的下标,防止溢出(相当于 L+(R-L)/2)
if( p == a[mid] ) // 如果找到了就返回
return mid;
else if( p > a[mid])
L = mid + 1; //设置新的查找区间的左端点
else
R = mid - 1; //设置新的查找区间的右端点
}
return -1;// 没找到就返回-1
} //复杂度O(log(n))
运行结果如下所示
嘿哈,既然是找值,当然离不开双指针了,这个代码就是用双指针实现的,其实和上面差不多,大概思路如下。
- 将数组排序,复杂度是O(n×log(n))
- 查找的时候,设置两个变量i和j,i初值是0,j初值是n-1.看a[i]+a[j],如果大于m,就让j 减1,如果小于m,就让i加1,直至a[i]+a[j]=m。
#include<stdio.h>
int cmp(const void* a, const void* b);
void removeElement(int* nums, int numsSize, int val);
int main(void)
{
int n, i ,min = 1000,m;
// 输入
scanf("%d %d", &n,&m);
int a[n];
for(i = 0;i < n; i++){
scanf("%d", &a[i]);
}
//排序
qsort(a, n, sizeof(a[0]), cmp);
//遍历
removeElement(a, n-1, m);
return 0;
}
// 升序排序
int cmp(const void* a, const void* b)
{
return *(int *)a - *(int *)b;
}
// 查找
void removeElement(int* nums, int numsSize, int val){
int left = 0, right = numsSize;
while (left < right) { // 循环判断条件,左闭右开
if (nums[left]+nums[right] > val) { // 如果两指针大于目标值
right--; // 右指针左移
} else {
left++; // 否则左指针右移
}
if(nums[left]+nums[right] == val )
printf("%d %d\n",nums[left],nums[right]);
}
}
运行结果如下所示
其实这道题目有点像两数之和,于是,有一个更为厉害的解法,只是目前没有用C语言实现,就先看一下Python的解法吧!用到了哈希表哦!
class Solution:
def twoSum(self, nums: List[int], target: int) -> List[int]:
hash = dict() # 声明一个字典
for i, num in enumerate(nums): # 使用enumerate()函数来遍历同时返回索引
if target - num in hash: # 如果在哈希表里找到,就返回下标
return [hash[target - num], i]
hash[nums[i]] = i # 如果没找到,就放入哈希表中
return []
总结
这道题目也不是很蓝啦!不过解法多样,还是蛮好玩的!
3. 搜索插入排序
- 题目
给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。
请必须使用时间复杂度为 O(log n) 的算法。
示例 1:
输入: nums = [1,3,5,6], target = 5
输出: 2
示例 2:输入: nums = [1,3,5,6], target = 2
输出: 1
示例 3:输入: nums = [1,3,5,6], target = 7
输出: 4
思路
首先, 此题为有序数组,无重复,容易想到用二分查找来实现,但是与普通的二分查找所不同的是,此题可能存在不存在的情况,要我们返回它将会被按顺序插入的位置。但是本质上还是二分查找,稍加改变,即可得到相应的解法。
首先明确可能遇到的几种情况:
- 目标值在数组中
- 目标值在所有元素之前
- 目标值在所有元素之后
- 目标值应该插入在数组的某个位置
之后再对这些情况一一分析即可
代码如下所示
Python
class Solution:
def searchInsert(self, nums: List[int], target: int) -> int:
left, right = 0, len(nums)-1 # 确定区间为闭区间
while left <= right: # 之所以是小于等于,是因为我们选择的区间是闭区间,如果是开区间,则不需要等号
middle = (right + left) >> 1
# 将中间值设置好,这个地方选择用位运算,不过对于python来说速度提升不大,还有一点,就是要防止溢出所以会选择 middle = left + ((right - left) >> 1)这种写法
if nums[middle] < target: # target 在右区间,所以在[middle + 1, right]这个区间
left = middle + 1
elif nums[middle] > target: # target 在左区间,所以在[left, middle - 1]这个区间
right = middle - 1
else:
return middle
return right + 1 # 经过模拟计算,发现需要返回right + 1
C语言
int searchInsert(int* nums, int numsSize, int target) {
int left = 0, right = numsSize - 1;
while (left <= right) {
int mid = ((right - left) >> 1) + left;
if (target <= nums[mid]) {
right = mid - 1;
} else {
left = mid + 1;
}
}
return right + 1;
}
运行结果就不贴了,这道是以前写力扣遇到的,也是二分,就放到一起来了!