定义
在数学和计算机科学领域,算法是一系列有限的严谨指令,通常用于解决一类特定问题或执行计算
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 AA 内,查找值 t a r g e t targettarget
算法描述
前提 | 给定一个内含 n 个元素的有序数组 A,满足 A0≤A1≤A2≤⋯≤An−1,一个待查target |
1 | 设置 i=0,j=n−1 |
2 | 如果 i>j,结束查找,没找到 |
3 | 设置 m = m=floor(i+j/2) ,m 为中间索引,floor 是向下取整(≤2i+j 的最小整数) |
4 | 如果 target<Am 设置 j=m−1,跳到第2步 |
5 | 如果 Am<target 设置 i=m+1,跳到第2步 |
6 | 如果 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 + 1 次i++
受数组元素个数 n 的影响,自增 n 次a[i] == k
受元素个数 n 的影响,比较 n 次return -1
,执行一次粗略认为每行代码执行时间是 t tt,假设 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;
}
结果:
注意:左侧未找到和右侧未找到结果不一样,这里不做分析
两个算法比较,可以看到 n 在较小的时候,二者花费的次数差不多
但随着 n 越来越大,比如说 n=1000 时,用二分查找算法(红色)也就是 54t ,而蓝色算法则需要 3003t
画图采用的是 Desmos | 图形计算器
计算机科学中,时间复杂度是用来衡量:一个算法的执行,随数据规模增大,而增长的时间成本
如何表示时间复杂度呢?
大 O 表示法[^4]
其中
渐进上界
渐进上界(asymptotic upper bound):从某个常数 n0开始,c∗g(n) 总是位于 f(n) 上方,那么记作O(g(n))
例1
例2
已知 f ( n ) f(n)f(n) 来说,求g(n)
常见大 O OO 表示法
按时间复杂度从低到高
渐进下界
渐进下界(asymptotic lower bound):从某个常数n0开始,c∗g(n) 总是位于 f(n) 下方,那么记作)Ω(g(n))
渐进紧界
渐进紧界(asymptotic tight bounds):从某个常数n0开始,f(n) 总是在 c1∗g(n) 和 c2∗g(n) 之间,那么记作 Θ(g(n))
空间复杂度
与时间复杂度类似,一般也使用大 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 二分查找
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;
}
几个名词
求最近邻居:
阅读量:615
点赞量:2
收藏量:0
左闭右开的区间,i 指向的可能是目标,而 j 指向的不是目标 不奢望循环内通过 m 找出目标, 缩小区间直至剩 1 个, 剩下的这个可能就是要找的(通过i) 改变 i 边界时,它指向的可能是目标,因此不能 m+1 循环内的平均比较次数减少了 时间复杂度 Θ(log(n))