定义
在数学和计算机科学领域,算法是一系列有限的严谨指令,通常用于解决一类特定问题或执行计算
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 Algorithm[^2]
不正式的说,算法就是任何定义优良的计算过程:接收一些值作为输入,在有限的时间内,产生一些值作为输出。
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.
定义
在计算机科学领域,数据结构是一种数据组织、管理和存储格式,通常被选择用来高效访问数据
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 Algorithm[^2]
数据结构是一种存储和组织数据的方式,旨在便于访问和修改
A data structure is a way to store and organize data in order to facilitate access and modifications
可以说,程序 = 数据结构 + 算法,它们是每一位程序员的基本功,下来我们通过对一个非常著名的二分查找算法的讲解来认识一下算法
二分查找算法也称折半查找,是一种非常高效的工作于有序数组的查找算法。后续的课程中还会学习更多的查找算法,但在此之前,不妨用它作为入门。
需求:在有序数组 A A A 内,查找值 t a r g e t target target
算法描述
前提 | 给定一个内含 n n n 个元素的有序数组 A A A,满足 A 0 ≤ A 1 ≤ A 2 ≤ ⋯ ≤ A n − 1 A_{0}\leq A_{1}\leq A_{2}\leq \cdots \leq A_{n-1} A0≤A1≤A2≤⋯≤An−1,一个待查值 t a r g e t target target |
1 | 设置 i = 0 i=0 i=0, j = n − 1 j=n-1 j=n−1 |
2 | 如果 i > j i \gt j i>j,结束查找,没找到 |
3 | 设置 m = f l o o r ( i + j 2 ) m = floor(\frac {i+j}{2}) m=floor(2i+j) , m m m 为中间索引, f l o o r floor floor 是向下取整( ≤ i + j 2 \leq \frac {i+j}{2} ≤2i+j 的最小整数) |
4 | 如果 t a r g e t < A m target < A_{m} target<Am 设置 j = m − 1 j = m - 1 j=m−1,跳到第2步 |
5 | 如果 A m < t a r g e t A_{m} < target Am<target 设置 i = m + 1 i = m + 1 i=m+1,跳到第2步 |
6 | 如果 A m = t a r g e t A_{m} = target Am=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;
}
另一种写法
public static int binarySearch(int[] a, int target) {
int i = 0, j = a.length;
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;
}
时间复杂度
下面的查找算法也能得出与之前二分查找一样的结果,那你能说出它差在哪里吗?
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,3,4]
查找 5
int i = 0
只执行一次i < a.length
受数组元素个数 n n n 的影响,比较 n + 1 n+1 n+1 次i++
受数组元素个数 n n n 的影响,自增 n n n 次a[i] == k
受元素个数 n n n 的影响,比较 n n n 次return -1
,执行一次粗略认为每行代码执行时间是 t t t,假设 n = 4 n=4 n=4 那么
如果套用二分查找算法,还是 [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;
}
int i = 0, j = a.length - 1
各执行 1 次
i <= j
比较 f l o o r ( log 2 ( n ) + 1 ) floor(\log_{2}(n)+1) floor(log2(n)+1) 再加 1 次
(i + j) >>> 1
计算 f l o o r ( log 2 ( n ) + 1 ) floor(\log_{2}(n)+1) floor(log2(n)+1) 次
接下来 if() else if() else
会执行 3 ∗ f l o o r ( log 2 ( n ) + 1 ) 3* floor(\log_{2}(n)+1) 3∗floor(log2(n)+1) 次,分别为
return -1
,执行一次
结果:
注意:
左侧未找到和右侧未找到结果不一样,这里不做分析
两个算法比较,可以看到 n n n 在较小的时候,二者花费的次数差不多
但随着 n n n 越来越大,比如说 n = 1000 n=1000 n=1000 时,用二分查找算法(红色)也就是 54 t 54t 54t,而蓝色算法则需要 3003 t 3003t 3003t
画图采用的是 Desmos | 图形计算器
计算机科学中,时间复杂度是用来衡量:一个算法的执行,随数据规模增大,而增长的时间成本
如何表示时间复杂度呢?
假设算法要处理的数据规模是 n n n,代码总的执行行数用函数 f ( n ) f(n) f(n) 来表示,例如:
为了对 f ( n ) f(n) f(n) 进行化简,应当抓住主要矛盾,找到一个变化趋势与之相近的表示法
大 O O O 表示法[^4]
其中
渐进上界
渐进上界(asymptotic upper bound):从某个常数 n 0 n_0 n0开始, c ∗ g ( n ) c*g(n) c∗g(n) 总是位于 f ( n ) f(n) f(n) 上方,那么记作 O ( g ( n ) ) O(g(n)) O(g(n))
例1
例2
已知 f ( n ) f(n) f(n) 来说,求 g ( n ) g(n) g(n)
常见大 O O O 表示法
按时间复杂度从低到高
渐进下界
渐进下界(asymptotic lower bound):从某个常数 n 0 n_0 n0开始, c ∗ g ( n ) c*g(n) c∗g(n) 总是位于 f ( n ) f(n) f(n) 下方,那么记作 Ω ( g ( n ) ) \Omega(g(n)) Ω(g(n))
渐进紧界
渐进紧界(asymptotic tight bounds):从某个常数 n 0 n_0 n0开始, f ( n ) f(n) f(n) 总是在 c 1 ∗ g ( n ) c_1*g(n) c1∗g(n) 和 c 2 ∗ g ( n ) c_2*g(n) c2∗g(n) 之间,那么记作 Θ ( g ( n ) ) \Theta(g(n)) Θ(g(n))
空间复杂度
与时间复杂度类似,一般也使用大 O O O 表示法来衡量:一个算法执行随数据规模增大,而增长的额外空间成本
public static int binarySearchBasic(int[] a, int target) {
int i = 0, j = a.length - 1; // 设置指针和初值
while (i <= j) { // 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;
}
二分查找性能
下面分析二分查找算法的性能
时间复杂度
空间复杂度
public static int binarySearchBalance(int[] a, int target) {
int i = 0, j = a.length;
while (1 < j - i) {
int m = (i + j) >>> 1;
if (target < a[m]) {
j = m;
} else {
i = m;
}
}
return (a[i] == target) ? i : -1;
}
思想:
private static int binarySearch0(long[] a, int fromIndex, int toIndex,
long key) {
int low = fromIndex;
int high = toIndex - 1;
while (low <= high) {
int mid = (low + high) >>> 1;
long midVal = a[mid];
if (midVal < key)
low = mid + 1;
else if (midVal > key)
high = mid - 1;
else
return mid; // key found
}
return -(low + 1); // key not found.
}
有时我们希望返回的是最左侧的重复元素,如果用 Basic 二分查找
对于数组 [ 1 , 2 , 3 , 4 , 4 , 5 , 6 , 7 ] [1, 2, 3, 4, 4, 5, 6, 7] [1,2,3,4,4,5,6,7],查找元素4,结果是索引3
对于数组 [ 1 , 2 , 4 , 4 , 4 , 5 , 6 , 7 ] [1, 2, 4, 4, 4, 5, 6, 7] [1,2,4,4,4,5,6,7],查找元素4,结果也是索引3,并不是最左侧的元素
public static int binarySearchLeftmost1(int[] a, int target) {
int i = 0, j = a.length - 1;
int candidate = -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 {
candidate = m; // 记录候选位置
j = m - 1; // 继续向左
}
}
return candidate;
}
如果希望返回的是最右侧元素
public static int binarySearchRightmost1(int[] a, int target) {
int i = 0, j = a.length - 1;
int candidate = -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 {
candidate = m; // 记录候选位置
i = m + 1; // 继续向右
}
}
return candidate;
}
应用
对于 Leftmost 与 Rightmost,可以返回一个比 -1 更有用的值
Leftmost 改为
public static int binarySearchLeftmost(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 {
i = m + 1;
}
}
return i;
}
Rightmost 改为
public static int binarySearchRightmost(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 {
i = m + 1;
}
}
return i - 1;
}
几个名词
范围查询:
求排名: l e f t m o s t ( t a r g e t ) + 1 leftmost(target) + 1 leftmost(target)+1
求前任(predecessor): l e f t m o s t ( t a r g e t ) − 1 leftmost(target) - 1 leftmost(target)−1
求后任(successor): r i g h t m o s t ( t a r g e t ) + 1 rightmost(target)+1 rightmost(target)+1
求最近邻居: