【算法讲解】基础算法——二分查找、二分答案

前言

本文会深入浅出的讲解二分搜索和二分答案
对于还不知道的同学,本文会带你入门,了解其本质原理和应用
对于已经了解,但每次应用还需要模板辅助的同学,本文会让你不再依赖模板,从死记硬背进化到轻松推演
对于已经对二分法滚瓜烂熟的同学,本文会给你带来新的理解,刻入骨子里一辈子都不会忘

二分搜索是一个非常常用的基础算法,应用于在有序数组(增序和减序是一样的,一般我们都用增序为例)中搜索目标值,有三个小分类:

  • 精确查找一个等于目标值的下标
  • 找第一个大于(等于)目标值的下标
  • 找最后一个小于(等于)目标值的下标

二分答案是算法题中的一个利器,可以有效的题目降低难度,将一道难题变成中等题,将中等题变成简单题,将简单题变成水题

二分搜索

精确查找

最朴素的做法

最朴素的做法就是,遍历一遍数组 a,当遍历到元素等于目标值时,就找到了,如果遍历完都没找到,那么意味着数组 a 中没有目标值
代码如下:

int findTarget(vector<int> &a, int target) {
  for (int i = 0; i < a.size(); i ++) {
    if (a[i] == target) {
      return i;
    }
  }
  return -1;
}

这种做法的复杂度是 O ( n ) O(n) O(n),当数组非常大时,是非常耗时的

Tips:
在各种语言中,在各种容器上都有类似 find 的函数,来查找目标值第一个所在下标,用的都上述算法,都是 O ( n ) O(n) O(n) 的复杂度
比如 c++: vector.find, python: List.index, js: Array.indexOf
这种运算由于只有一个语句,对于复杂度不敏感的工程师,往往会忽略它的复杂度,以为是O(1)
这种问题在算法比赛中很少遇到,但是工程上经常会遇到
切记一定要避免

随机取值

因为组数非常大,我们顺序遍历可能会花费很大的代价才能找到目标
那么能不能随机取值呢,如果恰巧随机到目标,那岂不是很快就能结束查找
代码如下:

int randSearch(vector<int> &a, int target) {
  while (true) {
    // 随机到天荒地老
    int index = rand() % a.size();
    if (a[index] == target) {
      return index;
    }
  }
}

显然,上述程序有以下这两个问题:

  1. 没有随机结束条件,如果数组 a 中没有目标值,一直不会结束
  2. 随机是无序的,可能某个下标会被反复随机到

优化一下

上述随机函数没有利用到有序(递增)数组这一特性,我们可以根据这个特性进行优化
我们用另一个数组 comp 来做辅助解释,comp 的规则为:

  • 当 a[i] < Target 时,comp[i] = ‘S’ (smaller)
  • 当 a[i] == Target 时,comp[i] = ‘E’ (equal)
  • 当 a[i] > Target 时,comp[i] = ‘B’ (bigger)

由于 a 是有序数组,所以 comp 数组一定符合以下特征:
S , S , . . . S ⏟ 0   o r   m o r e   S , E , E , . . . E ⏟ 0   o r   m o r e   E , B , B , . . . B ⏟ 0   o r   m o r e   B \underbrace{S, S, ... S}_{0\ or\ more\ S}, \underbrace{E, E, ... E}_{0\ or\ more\ E}, \underbrace{B, B, ... B}_{0\ or\ more\ B} 0 or more S S,S,...S,0 or more E E,E,...E,0 or more B B,B,...B

所以我们可以得出以下几个推论:

  • 当 a[i] == target 既 comp[i] == ‘E’
    • 找到目标
  • 当 a[i] < target 既 comp[i] == ‘S’
    • i 左边的一定都是 S,意味着左边的数据没有继续搜索的价值,我们只需要搜索 i 右边的内容就行
    • 既可以把后续的搜索范围的下边界设置为 i + 1
  • 当 a[i] > target$ 既 comp[i] == ‘B’
    • i 右边的一定都是 B,意味着右边的数据没有继续搜索的价值,我们只需要搜索 index 左边的内容就行
    • 既可以把后续的搜索范围的上边界设置为 i - 1

根据上述推论,我们就可以对之前的随机查找函数进行优化

同时,也确定了搜索结束的时机
因为我们会不断缩小搜索范围,直到为空,如果搜索范围为空了都没找到目标值,那就代表着搜索结束,数组 a 中没有目标值
代码如下:

int randSearch(vector<int> &a, int target) {
  int lo = 0;
  int hi = a.size() - 1; // 首先,设定我们的搜索范围的上下边界 [0, a.size() - 1]
  while (lo <= hi) { // 搜索范围不为空的时,进行搜索,当 left > right 时,意味着搜索范围为空
    int index = lo + rand() % (hi - lo + 1) // 随机一个在搜索范围内的下标
    if (a[index] == target) {
      return index;
    } else if (a[index] < target) {
      lo = index + 1; // 前面的数字一定都小于 target,没有搜索的必要,将搜索的下边界增大
    } else {
      hi = index - 1; // 后续的数字一定都大于 target,没有搜索的必要,将搜索的上边界减少
    }
  }
  return -1; // 搜索范围缩小到空都没找到,意味着数组 a 中没有目标值
}

这个算法其实很难测算复杂度,每次缩小的搜索范围都是随机的,可能只缩小了1个,也有可能一下子就缩小到非常接近目标
我们按最差复杂度来算,每次只缩小1个,所以它的复杂度依然还是 O(n)

二分查找

上述算法因为每次都是随机,每次可能只缩小一点点范围,导致最差复杂度依然很高
为了快速搜索到目标值,最好是每次能稳定的减少一半的搜索范围,而选取搜索范围最中间的那个下标,就可以让搜索范围减半

  • 中间下标的值等于目标值,找到目标
  • 中间下标的值小于目标值,则左半边的不用再搜,范围缩小了一半
  • 中间下标的值大于目标值,则右半边的不用再搜,范围缩小了一半

因为每次范围都缩小了一半,所以最差经过 l o g 2 n log_2n log2n 次查找后,一定能找到目标值或者将范围缩小为空
所以整体的复杂度是 O ( l o g n ) O(logn) O(logn)
代码如下:

int binarySearch(vector<int> &a, int target) {
  int lo = 0;
  int hi = a.size() - 1;
  while (lo <= hi) {
    int mid = (lo + hi) >> 1; // 利用位运算快速计算除2
    if (a[mid] == target) {
      return mid;
    } else if (a[mid] < target) {
      lo = mid + 1;
    } else {
      hi = mid - 1;
    }
  }
  return -1; // 找不到
}

运行示例

a = [0, 2, 4, 6, 8, 10],  target = 6
a      0   2   4   6   8   10
index  0   1   2   3   4   5

第一次  l       m           h  // a[mid] = 4 < 6 => lo = mid + 1
第二次              l   m   h  // a[mid] = 8 > 6 => hi = mid - 1
第三次             l/m  h      // a[mid] = 6 == 6 => findTarget

查找第一个大于(等于)目标值的下标

最朴素的做法(遍历枚举)就不再赘述,这里来介绍下如何将上述精确查找的思路进行修改升级
由于不是精确查找,所以原本的辅助数组 comp 不再使用,我们该用另一个数组 match 来辅助解释,match 的生成规则为:

  • 当 a[i] not 大于(等于)目标值时,match[i] = ‘F’ (false)
  • 当 a[i] 大于(等于)目标值时,match[i] = ‘T’ (true)
    由于 a 是有序数组,所以 match 一定符合以下特征:
    F , F , . . . F ⏟ 0   o r   m o r e   F , T , T , . . . T ⏟ 0   o r   m o r e   T \underbrace{F, F, ... F}_{0\ or\ more\ F}, \underbrace{T, T, ... T}_{0\ or\ more\ T} 0 or more F F,F,...F,0 or more T T,T,...T

我们将这个特征称之为,数组的二分性:既以某个下标为分界线,下标左边都匹配/不匹配某个规则,下标右边都不匹配/匹配某个规则
显然,对于有序数组,规则为【大于(等于)目标值】是符合二分性的

我们同样可以得到以下几个推论:

  • 当 a[i] < target 既 match[i] == ‘F’$
    • i 左边的一定都是 F,意味着左边的数据没有继续搜索的价值,我们只需要搜索 index 右边的内容就行
    • 既可以把后续的搜索范围的下边界设置为 i + 1
  • 如果 a[i] >= target 既 comp[i] == ‘T’
    • 找到匹配【大于(等于)目标】的值,但它并不一定是第一个,但它右边的数据一定不是第一个,也就失去了搜索的价值,我们只需要再尝试搜索 i 左边的内容就行
    • 既可以把后续的搜索范围的上边界设置为 i - 1
    • 同时我们将这个下标暂时标记为答案
      • 如果后续还能找到匹配条件的值,那么会更新这个答案
      • 如果后续找不到匹配条件的值,那么说明这个答案就是第一个

结束条件同样是搜索范围为空,当数组不存在大于(等于)目标值的情况,答案也不会被更新

代码如下:

bool match(int number, int target) {
  return number > target; // number >= target
}
int firstBinarySearch(vector<int> &a, int target) {
  int lo = 0;
  int hi = a.size() - 1;
  int answer = -1;
  while (lo <= hi) {
    int mid = (lo + hi) >> 1;
    if (match(a[mid], target)) {
      hi = mid - 1;
      answer = mid;
    } else {
      lo = mid + 1;
    }
  }
  return answer;
}

运行示例

a = [0, 2, 4, 6, 8, 10],  target = 5
a      0   2   4   6   8   10
match  F   F   F   T   T   T
index  0   1   2   3   4   5

第一次  l       m           h  // a[mid] = 4 > 5 => false => lo = mid + 1
第二次              l   m   h  // a[mid] = 8 > 5 => true => hi = mid - 1
第三次             l/m  h      // a[mid] = 6 > 5 => true => hi = mid - 1
第四次          h   l          // break

answer = hi + 1 = 3

查找最后一个小于(等于)目标值的下标

和查找第一个大于(等于)目标值的下标一样
不过数组的二分性变成了 T , T , . . . T ⏟ 0   o r   m o r e   T , F , F , . . . F ⏟ 0   o r   m o r e   F \underbrace{T, T, ... T}_{0\ or\ more\ T}, \underbrace{F, F, ... F}_{0\ or\ more\ F} 0 or more T T,T,...T,0 or more F F,F,...F
推导过程类似,大家可以自己尝试推导

对应的代码如下:

bool match(int number, int target) {
  return number < target; // number <= target
}
int firstBinarySearch(vector<int> &a, int target) {
  int lo = 0;
  int hi = a.size() - 1;
  while (lo <= hi) {
    int mid = (lo + hi) >> 1;
    if (match(a[mid], target)) {
      lo = mid + 1;
    } else {
      hi = mid - 1;
    }
  }
  return lo - 1;
}

思考:
这里代码中省略了 answer 的赋值,直接用 lo - 1 代替,大家自行可以思考下为什么可以这样子

运行示例

a = [0, 2, 4, 6, 8, 10],  target = 5
a      0   2   4   6   8   10
match  T   T   T   F   F   F
index  0   1   2   3   4   5

第一次  l       m           h  // a[mid] = 4 < 5 => true => lo = mid + 1
第二次              l   m   h  // a[mid] = 8 < 5 => false => hi = mid - 1
第三次            l/m/h        // a[mid] = 6 < 5 => false => hi = mid - 1
第四次          h   l          // break

answer = lo - 1

STL lower_bound, upper_bound

c++ 的 STL 中自带了 lower_boundupper_bound 两个函数可以直接对一个递增数组进行二分搜索
lower_bound 是查找第一个不小于目标值的元素
upper_bound 是查找第一个大于目标值的元素
示例:

vector<int> a = {0, 2, 4, 6, 8};
int firstIndexNotLessThan6 = lower_bound(a.begin(), a.end(), 6) - a.begin(); // 3
int firstIndexGreaterThan6 = upper_bound(a.begin(), a.end(), 6) - a.begin(); // 4

二分答案

上述的二分是都是应用在有序数组中的查找,而在算法题中,二分还有一个常用的方法,就是对答案进行二分
通常是针对那些答案无法直接计算出来,同时答案在枚举区间内符合二分性的算法题

Case

我们以一个实际的 case 来进行讲解:

leetcode 410 难
给定一个非负整数数组 nums 和一个整数 m ,你需要将这个数组分成 m 个非空的连续子数组。
设计一个算法使得这 m 个子数组各自和的最大值最小。
1 <= nums.length <= 1000
0 <= nums[i] <= 10 6 ^6 6
1 <= m <= min(50, nums.length)

思路

直接求这个答案没有任何思路,而完全枚举所有的可能性,有 C n − 1 m C_{n-1}^{m} Cn1m(n 是数组的长度) 种可能,是个天文数字,不可行

我们将题目用另一个方式来描述:

找到一个最小的整数 x,使其匹配以下条件: m 个子数组的各自的最大值小于等于 x。

是不是和上文已经讲解的查找第一个大于(等于)目标值的下标很像?

找到一个最小的下标 index,使其匹配以下条件:a[index] > target

我们非常愉快的发现,整个题目结构是一模一样的,这就意味着可以用二分的模板来解决
但是在套用二分的模板之前,我们还有几个问题需要解决:

  • 答案是否符合二分性
  • 二分搜索的范围是数组的下标,那么二分答案的范围是啥
  • 匹配条件比之前的 a[index] > target 复杂很多,应该如何编写

答案的二分性

先回忆一下二分搜索中提到数组的二分性:以某个下标为分界线,下标左边都匹配/不匹配某个规则,下标右边都不匹配/匹配某个规则
映射到答案的二分性即为:以某个数字为分界线,小于(等于)该数字的都匹配/不匹配某个规则,大于(等于)该数字的都不匹配/匹配某个规则

在此题下,我们需要证明,如果 x 匹配条件,那么 y(y > x) 一定也匹配条件。很容易证明:
我们将 m 个子数组的各自的最大值记为 m a x s u m m max_{sum_m} maxsumm
已经知
(1). x 匹配条件【 m 个子数组的各自的最大值 小于等于 x】,既 m a x s u m m ≤ x max_{sum_m} \leq x maxsummx
(2). x < y
由 (1)(2) 可得: m a x s u m m < y max_{sum_m} < y maxsumm<y,既 m 个子数组的各自的最大值 小于 y,既 y 匹配条件

搜索范围

搜索的范围就是答案可能的范围,不用特别的精确,一般我们通过构造极限 case 来限定
本题中

  • 当 m = 1 时,只能分成一个子串,那么答案最大可能是 ∑ i = 0 n − 1 a i \sum_{i=0}^{n-1}a_i i=0n1ai
  • 当 m = n 时,每个元素各自成为一个子串,那么答案最小可能是 m a x ( a i ) max(a_i) max(ai)

在二分算法中,我们并不会特别在意搜索范围的大小,因为就算搜索范围扩大了1000倍,实际log之后,也就仅仅多了10次搜索
所以不管 nums、m 如何变化,我们都可以采用相同的搜索范围,既:
lo = 0 (对应 nums[i] = 0, n = 1, m = 1 的情况)
hi = 10 9 ^9 9 (对应 nums[i] = 10 6 ^6 6, n = 1000, m = 1的情况)

匹配函数

我们可以将匹配条件【 m 个子数组的各自的最大值 小于等于 x】写成一个匹配函数
函数参数为二分的答案x(可能还需要配合其他辅助计算的参数),函数的主体就是判断 x 是否符合条件
本题中匹配函数代码见下面代码的 match 函数(这不是二分答案的重点,推导过程略过)

代码

class Solution {
  bool match(int x, vector<int>& nums, int k) {
    int cnt = 1;
    int sum = 0;
    for (int item : nums) {
      if (item > x) {
        return false;
      }
      if (sum + item > x) {
        cnt++;
        sum = item;
      } else {
        sum += item;
      }
    }
    return cnt <= k;
  }
public:
  int splitArray(vector<int>& nums, int k) {
    int lo = 0;
    int hi = 1e9;
    int answer = -1;
    while (lo <= hi) {
      int mid = (lo + hi) >> 1;
      if (match(mid, nums, k)) {
        answer = mid;
        hi = mid - 1;
      } else {
        lo = mid + 1;
      }
    }
    return answer;
  }
};

总结

对于求一个最大/最小值的题,如果没有直接的计算思路,可以先尝试将题目描述转化成

寻找一个最大/最小值 x,使 x 匹配 xxx 条件

并且证明其二分性,那么就可以使用二分答案的套路

二分答案可以非常有效的降低题目难度,因为将原本求解的题变成判断题(只需要编写匹配函数)
以 codeforces 为例,二分答案这个思路通常就值 800 分,原题 2400 难度的题,用二分答案讲选择题转变为判断题后,判断题部分其实只有 1600 难度
以 leetcode 为例,二分答案后直接将题目难度降低一个等级(难 -> 中等,中等 -> 简单)

tips:
一般题目要求计算最小值最大最大值最小 的都可以尝试用二分答案,但并不绝对,不要盲目套用
严谨的还是以很难直接计算得到结果答案符合二分性这两个判断为准

练习

(持续更新中)
leetcode 410 【难】
codeforces 1832 D 【2400】

  • 5
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 5
    评论
2积分福利。2积分福利。2积分福利。2积分福利。2积分福利。2积分福利。2积分福利。2积分福利。2积分福利。2积分福利。2积分福利。 涵盖广泛 精炼的理论讲述搭配大量经典算法示例,学习查询兼而有之。 阐述到位 算法思想、算法实现和完整示例合理搭配,相辅相成。 示例完善 示例分析精准,代码注释精确,每段代码皆可通过编译执行。 计算机技术的发展和普及不仅改变了人们的生活和娱乐方式,也改变了人们的工作方式,这其中最为重要的便是计算机编程技术。现代的设计任务大多通过代码编程交给计算机来完成,其中算法起到了至关重要的作用。可以毫不夸张地说,算法是一切程序设计的灵魂和基础。 《C/C++常用算法手册》分3篇,共13章,“第1篇算法基础篇”介绍了算法概述,重点分析了数据结构和基本算法思想;“第2篇算法基本应用篇”详细讲解算法在排序、查找、数值计算、数论、经典趣题和游戏中的应用;“第3篇算法高级应用篇”讲解算法的一些高级应用技术,包括在密码学和数据压缩/解压缩中的应用。 《C/C++常用算法手册》知识点覆盖全面、结构安排紧凑、讲解详细、示例丰富。《C/C++常用算法手册》对每一个知识点都给出了相应的算法及应用示例。虽然这些例子都是以C语言来编写的,但是算法并不局限于C语言。如果读者采用其他编程语言,例如C++、C#、VB、Java等,根据其语法格式进行适当的修改即可。 《C/C++常用算法手册 》主要定位于有一定C/C++语言编程基础、想通过学习算法与数据结构提升编程水平的读者,也可作为具有一定编程经验的程序员以及大中专院校学生学习数据结构和算法的参考书。 第1篇 算法基础篇 1 第1章 算法概述 2 1.1 什么是算法 2 1.2 算法的发展历史 3 1.3 算法的分类 4 1.4 算法相关概念的区别 4 1.5 算法的表示 5 1.5.1 自然语言表示 5 1.5.2 流程图表示 6 1.5.3 N-S图表示 7 1.5.4 伪代码表示 7 1.6 算法的性能评价 8 1.7 算法实例 9 1.7.1 查找数字 9 1.7.2 创建项目 11 1.7.3 编译执行 12 1.8 算法的新进展 13 1.9 小结 14 第2章 数据结构 15 2.1 数据结构概述 15 2.1.1 什么是数据结构 15 2.1.2 数据结构中的基本概念 16 2.1.3 数据结构的内容 16 2.1.4 数据结构的分类 18 2.1.5 数据结构的几种存储方式 18 2.1.6 数据类型 19 2.1.7 常用的数据结构 20 2.1.8 选择合适的数据结构解决实际问题 21 2.2 线性表 21 2.2.1 什么是线性表 21 2.2.2 线性表的基本运算 22 2.3 顺序表结构 23 2.3.1 准备数据 23 2.3.2 初始化顺序表 24 2.3.3 计算顺序表长度 24 2.3.4 插入结点 24 2.3.5 追加结点 25 2.3.6 删除结点 25 2.3.7 查找结点 25 2.3.8 显示所有结点 26 2.3.9 顺序表操作示例 26 2.4 链表结构 30 2.4.1 什么是链表结构 30 2.4.2 准备数据 31 2.4.3 追加结点 31 2.4.4 插入头结点 33 2.4.5 查找结点 33 2.4.6 插入结点 34 2.4.7 删除结点 35 2.4.8 计算链表长度 36 2.4.9 显示所有结点 36 2.4.10 链表操作示例 37 2.5 栈结构 41 2.5.1 什么是栈结构 41 2.5.2 准备数据 42 2.5.3 初始化栈结构 42 2.5.4 判断空栈 43 2.5.5 判断满栈 43 2.5.6 清空栈 43 2.5.7 释放空间 44 2.5.8 入栈 44 2.5.9 出栈 44 2.5.10 读结点数据 45 2.5.11 栈结构操作示例 45 2.6 队列结构 48 2.6.1 什么是队列结构 48 2.6.2 准备数据 49 2.6.3 初始化队列结构 49 2.6.4 判断空队列 50 2.6.5 判断满队列 50 2.6.6 清空队列 50 2.6.7 释放空间 51 2.6.8 入队列 51 2.6.9 出队列 51 2.6.10 读结点数据 52 2.6.11 计算队列长度 52 2.6.12 队列结构操作示例 53 2.7 树结构 56 2.7.1 什么是树结构 56 2.7.2 树的基本概念 56 2.7.3 二叉树 57 2.7.4 准备数据 61 2.7.5 初始化二叉树 61 2.7.6 添加结点 62 2.7.7 查找结点 63 2.7.8 获取左子树 64 2.7.9 获取右子树 64 2.7.10 判断空树 65 2.7.11 计算二叉树深度 65 2.7.12 清空二叉树 65 2.7.13 显示结点数据 66 2.7.14 遍历二叉树 66 2.7.15 树结构操作示例 68 2.8 图结构 71 2.8.1 什么是图结构 71 2.8.2 图的基本概念 72 2.8.3 准备数据 76 2.8.4 创建图 78 2.8.5 清空图 79 2.8.6 显示图 79 2.8.7 遍历图 80 2.8.8 图结构操作示例 81 2.9 小结 84 第3章 基本算法思想 85 3.1 常用算法思想概述 85 3.2 穷举算法思想 85 3.2.1 穷举算法基本思想 86 3.2.2 穷举算法示例 86 3.3 递推算法思想 88 3.3.1 递推算法基本思想 88 3.3.2 递推算法示例 88 3.4 递归算法思想 90 3.4.1 递归算法基本思想 90 3.4.2 递归算法示例 90 3.5 分治算法思想 92 3.5.1 分治算法基本思想 92 3.5.2 分治算法示例 92 3.6 概率算法思想 96 3.6.1 概率算法基本思想 96 3.6.2 概率算法示例 97 3.7 小结 98 第2篇 算法基本应用篇 99 第4章 排序算法 100 4.1 排序算法概述 100 4.2 冒泡排序法 101 4.2.1 冒泡排序算法 101 4.2.2 冒泡排序算法示例 102 4.3 选择排序法 104 4.3.1 选择排序算法 104 4.3.2 选择排序算法示例 105 4.4 插入排序法 107 4.4.1 插入排序算法 107 4.4.2 插入排序算法示例 108 4.5 Shell排序法 110 4.5.1 Shell排序算法 110 4.5.2 Shell排序算法示例 111 4.6 快速排序法 113 4.6.1 快速排序算法 113 4.6.2 快速排序算法示例 114 4.7 堆排序法 116 4.7.1 堆排序算法 116 4.7.2 堆排序算法示例 121 4.8 合并排序法 123 4.8.1 合并排序算法 123 4.8.2 合并排序算法示例 126 4.9 排序算法的效率 129 4.10 排序算法的其他应用 130 4.10.1 反序排序 130 4.10.2 字符串数组的排序 132 4.10.3 字符串的排序 135 4.11 小结 137 第5章 查找算法 138 5.1 查找算法概述 138 5.2 顺序查找 138 5.2.1 顺序查找算法 139 5.2.2 顺序查找操作示例 139 5.3 折半查找 141 5.3.1 折半查找算法 141 5.3.2 折半查找操作示例 142 5.4 数据结构中的查找算法 145 5.4.1 顺序表结构中的查找算法 145 5.4.2 链表结构中的查找算法 148 5.4.3 树结构中的查找算法 151 5.4.4 图结构中的查找算法 152 5.5 小结 153 第6章 基本数学问题 154 6.1 判断闰年 154 6.2 多项式计算 156 6.2.1 —维多项式求值 156 6.2.2 二维多项式求值 158 6.2.3 多项式乘法 160 6.2.4 多项式除法 161 6.3 随机数生成算法 164 6.4 复数运算 171 6.4.1 简单的复数运算 172 6.4.2 复数的幂运算 174 6.4.3 复指数运算 176 6.4.4 复对数运算 177 6.4.5 复正弦运算 178 6.4.6 复余弦运算 179 6.5 阶乘 180 6.6 计算π的近似值 183 6.6.1 割圆术 183 6.6.2 蒙特卡罗算法 185 6.6.3 级数公式 187 6.7 矩阵运算 190 6.7.1 矩阵加法 190 6.7.2 矩阵减法 191 6.7.3 矩阵乘法 193 6.8 方程求解 195 6.8.1 线性方程求解——高斯消元法 195 6.8.2 非线性方程求解——二分法 200 6.8.3 非线性方程求解——牛顿迭代法 202 6.9 小结 205 第7章 复杂的数值计算算法 206 7.1 拉格朗日插值 206 7.1.1 拉格朗日插值算法 206 7.1.2 拉格朗日插值示例 207 7.2 数值积分 210 7.2.1 数值积分算法 210 7.2.2 数值积分示例 211 7.3 开平方 213 7.3.1 开平方算法 213 7.3.2 开平方示例 213 7.4 极值问题的求解算法 215 7.4.1 极值求解算法 215 7.4.2 极值求解示例 217 7.5 特殊函数的计算算法 221 7.5.1 伽玛函数 221 7.5.2 贝塔函数 224 7.5.3 正弦积分函数 228 7.5.4 余弦积分函数 231 7.5.5 指数积分函数 235 7.6 小结 239 第8章 经典数据结构问題 240 8.1 动态数组排序 240 8.1.1 动态数组的存储和排序 240 8.1.2 动态数组排序示例 241 8.2 约瑟夫环 243 8.2.1 简单约瑟夫环算法 243 8.2.2 简单约瑟夫环求解 245 8.2.3 复杂约瑟夫环算法 247 8.2.4 复杂约瑟夫环求解 248 8.3 城市之间的最短总距离 250 8.3.1 最短总距离算法 250 8.3.2 最短总距离求解 253 8.4 最短路径 257 8.4.1 最短路径算法 258 8.4.2 最短路径求解 260 8.5 括号匹配 265 8.5.1 括号匹配算法 265 8.5.2 括号匹配求解 267 8.6 小结 270 第9章 数论问题 271 9.1 数论 271 9.1.1 数论概述 271 9.1.2 数论的分类 272 9.1.3 初等数论 273 9.1.4 基本概念 273 9.2 完全数 274 9.2.1 完全数概述 274 9.2.2 计算完全数算法 275 9.3 亲密数 277 9.3.1 亲密数概述 277 9.3.2 计算亲密数算法 277 9.4 水仙花数 280 9.4.1 水仙花数概述 280 9.4.2 计算水仙花数算法 281 9.5 自守数 283 9.5.1 自守数概述 283 9.5.2 计算自守数算法 284 9.6 最大公约数 287 9.6.1 计算最大公约数算法——搌转相除法 287 9.6.2 计算最大公约数算法一一Stein算法 288 9.6.3 计算最大公约数示例 289 9.7 最小公倍数 290 9.8 素数 292 9.8.1 素数概述 292 9.8.2 计算素数算法 292 9.9 回文素数 294 9.9.1 回文素数概述 294 9.9.2 计算回文素数算法 294 9.10 平方回文数 297 9.10.1 平方回文数概述 297 9.10.2 计算平方回文数算法 297 9.11 分解质因数 299 9.12 小结 301 第10 章算法经典趣题 302 0. .l 百钱买百鸡 302 10.1.1 百钱买百鸡算法 302 10.1.2 百钱买百鸡求解 303 10.2 五家共井 304 10.2.1 五家共井算法 304 10.2.2 五家共井求解 305 10.3 鸡兔同笼 307 10.3.1 鸡兔同笼算法 307 10.3.2 鸡兔同笼求解 308 10.4 猴子吃桃 308 10.4.1 猴子吃桃算法 308 10.4.2 猴子吃桃求解 309 10.5 舍罕王赏麦 310 10.5.1 舍罕王赏麦问题 310 10.5.2 舍罕王赏麦求解 311 10.6 汉诺塔 312 10.6.1 汉诺塔算法 312 10.6.2 汉诺塔求解 314 10.7 窃贼问题 315 10.7.1 窃贼问题算法 315 10.7.2 窃贼问题求解 317 10.8 马踏棋盘 320 10.8.1 马踏棋盘算法 320 10.8.2 马踏棋盘求解 321 10.9 八皇后问题 323 10.9.1 八皇后问题算法 324 10.9.2 八皇后问题求解 325 10.10 寻找假银币 327 10.10.1 寻找假银币算法 327 10.10.2 寻找假银币求解 329 10.11 青蛙过河 331 10.11.1 青蛙过河算法 331 10.11.2 青蛙过河求解 333 10.12 三色旗 335 10.12.1 三色旗算法 335 10.12.2 三色旗求解 337 10.13 渔夫捕鱼 339 10.13.1 渔夫捕鱼算法 339 10.13.2 渔夫捕魚求解 340 10.14 爱因斯坦的阶梯 341 10.14.1 爱因斯坦的阶梯算法 341 10.14.2 爱因斯坦的阶梯求解 342 10.15 兔子产仔 342 10.15.1 兔子产仔算法 343 10.15.2 兔子产仔求解 343 10.16 常胜将军 344 10.16.1 常胜将军算法 344 10.16.2 常胜将军求解 345 10.17 新郎和新娘 346 10.17.1 新郎和新娘算法 347 10.17.2 新郎和新娘求解 348 10.18 三色球 349 10.18.1 三色球算法 349 10.18.2 三色球求解 350 10.19 小结 351 第11章 游戏中的算法 352 11.1 洗扑克牌 352 11.1.1 洗扑克牌算法 352 11.1.2 洗扑克牌示例 353 11.2 取火柴游戏 356 11.2.1 取火柴游戏算法 356 11.2.2 取火柴游戏示例 357 11.3 10点半 358 11.3.1 10点半算法 358 11.3.2 10点半游戏示例 363 11.4 生命游戏 368 11.4.1 生命游戏的原理 368 11.4.2 生命游戏的算法 369 11.4.3 生命游戏示例 371 11.5 小结 376 第3篇 算法高级应用篇 377 第12章 密码学算法 378 12.1 密码学概述 378 12.1.1 密码学的发展 378 12.1.2 密码学的基本概念 379 12.1.3 柯克霍夫斯原则 379 12.1.4 经典密码学算法 380 12.2 换位加密解密 381 12.2.1 换位加密解密算法 381 12.2.2 换位加密解密算法示例 383 12.3 替换加密解密 386 12.3.1 替换加密解密算法 386 12.3.2 替换加密解密算法示例 388 12.4 位加密解密 389 12.4.1 位加密解密算法 390 12.4.2 位加密解密算法示例 391 12.5 一次一密加密解密算法 392 12.5.1 一次一密加密解密算法 392 12.5.2 一次一密加密解密算法示例 394 12.6 小结 396 第13章 压缩与解压缩算法 397 13.1 压缩与解压缩概述 397 13.1.1 压缩与解压缩分类 397 13.1.2 典型的压缩解压缩算法 397 13.2 压缩算法 398 13.3 解压缩算法 401 13.4 压缩/解压缩示例 404 13.5 小结 406
1. 算法的基本概念 利用计算机算法为计算机解题的过程实际上是在实施某种算法。 (1)算法的基本特征 算法一般具有4个基本特征:可行性、确定性、有穷性、拥有足够的情报。 (2)算法的基本运算和操作 算法的基本运算和操作包括:算术运算、逻辑运算、关系运算、数据传输。 (3)算法的3种基本控制结构 算法的3种基本控制结构是:顺序结构、选择结构、循环结构。 (4)算法基本设计方法 算法基本设计方法:列举法、归纳法、递推、递归、减半递推技术、回溯法。 (5)指令系统 所谓指令系统指的是一个计算机系统能执行的所有指令的集合。 (2)数据结构研究的3个方面 ① 数据集合中各数据元素之间所固有的逻辑关系,即数据的逻辑结构; ② 在对数据进行处理时,各数据元素在计算机中的存储关系,即数据的存储结构; ③ 对各种数据结构进行的运算。 2. 逻辑结构 数据的逻辑结构是对数据元素之间的逻辑关系的描述,它可以用一个数据元素的集合和定义在此集合中的若干关系来表示。数据的逻辑结构有两个要素:一是数据元素的集合,通常记为D;二是D上的关系,它反映了数据元素之间的前后件关系,通常记为R。一个数据结构可以表示成:B=(D,R) 其中,B表示数据结构。为了反映D中各数据元素之间的前后件关系,一般用二元组来表示。 例如,如果把一年四季看作一个数据结构,则可表示成:B =(D,R) D ={春季,夏季,秋季,冬季} R ={(春季,夏季),(夏季,秋季),(秋季,冬季)} 3. 存储结构 数据的逻辑结构在计算机存储空间中的存放形式称为数据的存储结构(也称数据的物理结构)。 由于数据元素在计算机存储空间中的位置关系可能与逻辑关系不同,因此,为了表示存放在计算机存储空间中的各数据元素之间的逻辑关系(即前后件关系),在数据的存储结构中,不仅要存放各数据元素的信息,还需要存放各数据元素之间的前后件关系的信息。 一种数据的逻辑结构根据需要可以表示成多种存储结构,常用的存储结构有顺序、链接等存储结构。 顺序存储方式主要用于线性的数据结构,它把逻辑上相邻的数据元素存储在物理上相邻的存储单元里,结点之间的关系由存储单元的邻接关系来体现。 链式存储结构就是在每个结点中至少包含一个指针域,用指针来体现数据元素之间逻辑上的联系。 1.2.2 线性结构和非线性结构 根据数据结构中各数据元素之间前后件关系的复杂程度,一般将数据结构分为两大类型:线性结构与非线性结构。 (1)如果一个非空的数据结构满足下列两个条件: ① 有且只有一个根结点; ② 每一个结点最多有一个前件,也最多有一个后件。 则称该数据结构为线性结构。线性结构又称线性表。在一个线性结构中插入或删除任何一个结点后还应是线性结构。栈、队列、串等都为线性结构。 如果一个数据结构不是线性结构,则称之为非线性结构。数组、广义表、树和图等数据结构都是非线性结构。 (2)线性表的顺序存储结构具有以下两个基本特点: ① 线性表中所有元素所占的存储空间是连续的; ② 线性表中各数据元素在存储空间中是按逻辑顺序依次存放的。 元素ai的存储地址为:ADR(ai)=ADR(a1)+(i-1)k,ADR(a1)为第一个元素的地址,k代表每个元素占的字节数。 (3)顺序表的运算有查找、插入、删除3种。 1.3 栈 1. 栈的基本概念 栈(stack)是一种特殊的线性表,是限定只在一端进行插入与删除的线性表。 在栈中,一端是封闭的,既不允许进行插入元素,也不允许删除元素;另一端是开口的,允许插入和删除元素。通常称插入、删除的这一端为栈顶,另一端为栈底。当表中没有元素时称为空栈。栈顶元素总是最后被插入的元素,从而也是最先被删除的元素;栈底元素总是最先被插入的元素,从而也是最后才能被删除的元素。 栈是按照“先进后出”或“后进先出”的原则组织数据的。例如,枪械的子弹匣就可以用来形象的表示栈结构。子弹匣的一端是完全封闭的,最后被压入弹匣的子弹总是最先被弹出,而最先被压入的子弹最后才能被弹出。 二级公共基础知识速学教程 2. 栈的顺序存储及其运算 栈的基本运算有3种:入栈、退栈与读栈顶元素。 ① 入栈运算:在栈顶位置插入一个新元素; ② 退栈运算:取出栈顶元素并赋给一个指定的变量; ③ 读栈顶元素:将栈顶元素赋给一个指定的变量。 1.4 队列 1. 队列的基本概念 队列是只允许在一端进行删除,在另一端进行插入的顺序表,通常将允许删除的这一端称为队头,允许插入的这一端称为队尾。当表中没有元素时称为空队列。 队列的修改是依照先进先出的原则进行的,因此队列也称为先进先出的线性表,或者后进后出的线性表。例如:火车进遂道,最先进遂道的是火车头,最后是火车尾,而火车出遂道的时候也是火车头先出,最后出的是火车尾。若有队列: Q =(q1,q2,…,qn) 那么,q1为队头元素(排头
二分查找二分答案是两个不同的概念。 二分查找(Binary Search)是一种常用的查找算法,适用于有序的数组或列表。它通过将目标值与数组中间元素进行比较,从而确定目标值在数组的左半部分还是右半部分,然后再在相应的半部分中继续查找,直到找到目标值或者确定目标值不存在。二分查找的时间复杂度是 O(logn)。 以下是一个简单的 C++ 实现示例: ```cpp int binarySearch(int arr[], int target, int left, int right) { while (left <= right) { int mid = left + (right - left) / 2; if (arr[mid] == target) return mid; else if (arr[mid] < target) left = mid + 1; else right = mid - 1; } return -1; // 目标值不存在 } ``` 二分答案(Binary Search for Answer)是一种在某个范围内寻找满足某个条件的最优解的方法。它通过在一个给定范围内进行二分搜索,每次确定一个中间值,然后判断该中间值是否满足条件。如果满足条件,则将搜索范围缩小到左半部分;如果不满足条件,则将搜索范围缩小到右半部分。通过不断缩小搜索范围,最终找到满足条件的最优解。 这种方法在一些问题中很常见,比如在一个有序数组中找到满足某个条件的最小/最大值,或者确定一个函数的最大/最小值等。 具体的实现方式会根据具体的问题而有所不同,但基本的思路是相似的。你可以根据具体的问题来实现相应的二分答案算法

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值