一. 初识算法
1.1 什么是算法?
定义
在数学和计算机科学领域,算法是一系列有限的严谨指令,通常用于解决一类特定问题或执行计算
In mathematics and computer science, an algorithm (/ˈælɡərɪðəm/) is a finite sequence of rigorous instructions, typically used to solve a class of specific problems or to perform a computation.1
Introduction to Algorithm2
不正式的说,算法就是任何定义优良的计算过程:接收一些值作为输入,在有限的时间内,产生一些值作为输出。
Informally, an algorithm is any well-defined computational procedure that takes some value, or set of values, as input and produces some value, or set of values, as output in a finite amount of time.
1.2 什么是数据结构?
定义
在计算机科学领域,数据结构是一种数据组织、管理和存储格式,通常被选择用来高效访问数据
In computer science, a data structure is a data organization, management, and storage format that is usually chosen for efficient access to data
Introduction to Algorithm2
数据结构是一种存储和组织数据的方式,旨在便于访问和修改
A data structure is a way to store and organize data in order to facilitate access and modifications
可以说,程序 = 数据结构 + 算法,它们是每一位程序员的基本功,下来我们通过对一个非常著名的二分查找算法的讲解来认识一下算法
1.3 二分查找 3
二分查找算法也称折半查找,是一种非常高效的工作于有序数组的查找算法。后续的课程中还会学习更多的查找算法,但在此之前,不妨用它作为入门。
1) 基础版
需求:在有序数组 A 内,查找值 target
- 如果找到返回索引
- 如果找不到返回
-1
算法描述
| 前提 | 给定一个内含n 个元素的有序数组 A,满足 A_{0}\leq A_{1}\leq A_{2}\leq \cdots \leq A_{n-1},一个待查值 target |
| 1 | 设置i=0,j=n-1 |
| 2 | 如果i \gt j,结束查找,没找到 |
| 3 | 设置m = floor(\frac {i+j}{2}) ,m 为中间索引,floor 是向下取整(\leq \frac {i+j}{2} 的最小整数) |
| 4 | 如果target < A_{m} 设置 j = m - 1,跳到第2步 |
| 5 | 如果A_{m} < target 设置 i = m + 1,跳到第2步 |
| 6 | 如果A_{m} = target,结束查找,找到了 |
P.S.
- 对于一个算法来讲,都有较为严谨的描述,上面是一个例子
- 后续讲解时,以简明直白为目标,不会总以上面的方式来描述算法
java 实现
public static int binarySearch(int[] a, int target) {
int i = 0, j = a.length - 1;
while (i <= j) {
int m = (i + j) >>> 1;
if (target < a[m]) { // 在左边
j = m - 1;
} else if (a[m] < target) { // 在右边
i = m + 1;
} else {
return m;
}
}
return -1;
}
问题1:为什么是 i <= j ,而不是 i < j ?
意味着区间内有未比较的元素,i, j 他们指向的元素也会参与比较。
问题2:(i + j) / 2 有没有问题?
问题3:都写小于符号有啥好处?
i,j对应着搜索区间[0,a.length-1](注意是闭合的区间),i<=j意味着搜索区间内还有未比较的元素,i,j指向的元素也可能是比较的目标- 思考:如果不加
i==j行不行? - 回答:不行,因为这意味着
i,j指向的元素会漏过比较
- 思考:如果不加
m对应着中间位置,中间位置左边和右边的元素可能不相等(差一个),不会影响结果- 如果某次未找到,那么缩小后的区间内不包含
m
测试用例:
package test;
import com.sun.scenario.effect.impl.sw.java.JSWBlend_SRC_OUTPeer;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import static main.BinarySearch.binarySearchBasic;
import static org.junit.jupiter.api.Assertions.*;
class BinarySearchTest {
@Test
@DisplayName("binarySearchBasic 找到")
public void test1() {
int[] a = {7, 13, 21, 30, 38, 44, 52, 53};
assertEquals(0, binarySearchBasic(a, 7));
assertEquals(1, binarySearchBasic(a, 13));
assertEquals(2, binarySearchBasic(a, 21));
assertEquals(3, binarySearchBasic(a, 30));
assertEquals(4, binarySearchBasic(a, 38));
assertEquals(5, binarySearchBasic(a, 44));
assertEquals(6, binarySearchBasic(a, 52));
assertEquals(7, binarySearchBasic(a, 53));
}
@Test
@DisplayName("binarySearchBasic 没找到")
public void test2() {
int[] a = {7, 13, 21, 30, 38, 44, 52, 53};
assertEquals(-1, binarySearchBasic(a, 0));
assertEquals(-1, binarySearchBasic(a, 15));
assertEquals(-1, binarySearchBasic(a, 60));
}
public static void main(String[] args){
int i = 0;
int j = Integer.MAX_VALUE - 1;
int m = (i + j) / 2;
System.out.println(m);
i = m + 1;
m = (i + j)/2;
System.out.println(m);
i = m + 1;
System.out.println(i);
System.out.println(j);
System.out.println(i+j);
/**
* 同一个二进制数
* 1011_1111_1111_1111_1111_1111_1111_1110
*
* 不把最高位视为符号位,代表3221225470
* 把最高位视为符号位,代表-1073741823
*/
m = (i+j)/2;
System.out.println(m);
}
}
改变版
另一种写法
public static int binarySearch(int[] a, int target) {
int i = 0, j = a.length; //i指向第一个元素,j指向边界
while (i < j) {
int m = (i + j) >>> 1; //无符号右移运算
if (target < a[m]) { // 在左边
j = m;
} else if (a[m] < target) { // 在右边
i = m + 1;
} else {
return m;
}
}
return -1;
}
i,j对应着搜索区间[0,a.length)(注意是左闭右开的区间),i<j意味着搜索区间内还有未比较的元素,j指向的一定不是查找目标- 思考:为啥这次不加
i==j的条件了? - 回答:这回
j指向的不是查找目标,如果还加i==j条件,就意味着j指向的还会再次比较,找不到时,会死循环
- 思考:为啥这次不加
- 如果某次要缩小右边界,那么
j=m,因为此时的m已经不是查找目标 了
1.4 衡量算法好坏
时间复杂度
下面的查找算法也能得出与之前二分查找一样的结果,那你能说出它差在哪里吗?
public static int search(int[] a, int k) {
for (
int i = 0;
i < a.length;
i++
) {
if (a[i] == k) {
return i;
}
}
return -1;
}
思考角度:
//1.最差的执行情况。
//2.假设每行语句执行时间一样。
/*
数据元素个数 n
int i = 0; 1
i < a.length; n+1
i++ n
a[i] == target n
return - 1; 1
算法运行语句总次数:3*n + 3
*/
分析思路:
考虑最坏情况下(没找到)例如 [1,2,3,4] 查找 5
int i = 0只执行一次i < a.length受数组元素个数n的影响,比较n+1次i++受数组元素个数n的影响,自增n次a[i] == k受元素个数n的影响,比较n次return -1,执行一次
粗略认为每行代码执行时间是 t,假设 n=4 那么
- 总执行时间是
(1+4+1+4+4+1)*t = 15t - 可以推导出更一般地公式为,
T = (3*n+3)t
如果套用二分查找算法,还是 [1,2,3,4] 查找 5
public static int binarySearch(int[] a, int target) {
int i = 0, j = a.length - 1;
while (i <= j) {
int m = (i + j) >>> 1;
if (target < a[m]) { // 在左边
j = m - 1;
} else if (a[m] < target) { // 在右边
i = m + 1;
} else {
return m;
}
}
return -1;
}
/*
1 [2, 3, 4, 5] 右侧没找到更差
int i = 0, j = a.length - 1; 2
return - 1; 1
元素个数 循环次数 公式
4-7 3 floor(log_2(4)) = 2+1
8-15 4 floor(log_2(5)) = 3+1
16-21 5 floor(log_2(6)) = 4+1
32-63 6 floor(log_2(7)) = 5+1
......
循环次数:floor(log_2(n)) + 1
i <= j L+1
int m = (i + j) >>>1; L
target <a[m] L
a[m] < target L
i = m + 1; L
循环次数:(floor(log_2(n)) + 1)*5 + 4
*/
-
int i = 0, j = a.length - 1各执行 1 次 -
i <= j比较floor(\log_{2}(n)+1)再加 1 次 -
(i + j) >>> 1计算floor(\log_{2}(n)+1)次 -
接下来
if() else if() else会执行3* floor(\log_{2}(n)+1)次,分别为- if 比较
- else if 比较
- else if 比较成立后的赋值语句
-
return -1,执行一次
结果:
- 总执行时间为
(2 + (1+3) + 3 + 3 * 3 +1)*t = 19t - 更一般地公式为
(4 + 5 * floor(\log_{2}(n)+1))*t
注意:
左侧未找到和右侧未找到结果不一样,这里不做分析
两个算法比较,可以看到 n 在较小的时候,二者花费的次数差不多
但随着 n 越来越大,比如说 n=1000 时,用二分查找算法(红色)也就是 54t,而蓝色算法则需要 3003t
画图采用的是 Desmos | 图形计算器
计算机科学中,时间复杂度是用来衡量:一个算法的执行,随数据规模增大,而增长的时间成本
- 不依赖于环境因素
如何表示时间复杂度呢?
-
假设算法要处理的数据规模是
n,代码总的执行行数用函数f(n)来表示,例如:- 线 性查找算法的函数
f(n) = 3*n + 3 - 二分查找算法的函数
f(n) = (floor(log_2(n)) + 1) * 5 + 4
- 线 性查找算法的函数
-
为了对
f(n)进行化简,应当抓住主要矛盾,找到一个变化趋势与之相近的表示法
大 O 表示法4

其中
c, c_1, c_2都为一个常数f(n)是实际执行代码行数与 n 的函数g(n)是经过化简,变化趋势与f(n)一致的 n 的函数
渐进上界
渐进上界(asymptotic upper bound):从某个常数 n_0开始,c*g(n) 总是位于 f(n) 上方,那么记作 O(g(n))
- 代表算法执行的最差情况
例1
f(n) = 3*n+3g(n) = n- 取
c=4,在n_0=3之后,g(n)可以作为f(n)的渐进上界,因此表示法写作O(n)
例2
f(n) = 5*floor(log_2(n)) + 9g(n) = log_2(n)O(log_2(n))
已知 f(n) 来说,求 g(n)
- 表达式中相乘的常量,可以省略,如
f(n) = 100*n^2中的100
- 多项式中数量规模更小(低次项)的表达式,如
f(n)=n^2+n中的nf(n) = n^3 + n^2中的n^2
- 不同底数的对数,渐进上界可以用一个对数函数
\log n表示- 例如:
log_2(n)可以替换为log_{10}(n),因为log_2(n) = \frac{log_{10}(n)}{log_{10}(2)},相乘的常量\frac{1}{log_{10}(2)}可以省略
- 例如:
- 类似的,对数的常数次幂可省略
- 如:
log(n^c) = c * log(n)
- 如:
常见大 O 表示法

按时间复杂度从低到高