zl程序教程

您现在的位置是:首页 >  Java

当前栏目

(四)算法基础——二分算法

2023-02-18 15:50:00 时间

目录

时间复杂度

种类

二分算法

模板

例题

1.求方程的根

2.寻找指定和的整数对

3.搜索插入排序 


        在介绍二分算法之前,我们先来介绍一下时间复杂度的概念!

时间复杂度

  •  一个程序或算法的时间效率,也称“时间复杂度”,有时简称“复杂度”。
  • 复杂度常用大的字母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;
}

        运行结果就不贴了,这道是以前写力扣遇到的,也是二分,就放到一起来了!