本篇来自于笔者学习MIT的公开课算法导论的学习笔记,仅仅是我个人接受课程教育后,进行的学习笔记,可能理解并不到位,仅供参考。
课程视频地址:
Lecture 1: Introduction and Peak Finding
Position 2 is a peak if and only if b ≥ a and b ≥ c. Position 9 is a peak if i ≥ h.
峰值查找算法,该算法的含义为:给定一个数组,查找一个元素,满足其左边的元素与右边的元素均小于其值,那么该元素就是峰值元素。
即给定数组a,当在a中满足a[n - 1] <= a[n] >= a[n+1]
,那么n
所在位置的元素,就是峰值元素。
首先最基本的思路,我们有两种方式:
O(n)
,如果我们的数组特别大,其中存在100000个元素的话,那么它的性能会非常的低下。我们来换一种思路:
What if we start in the middle? For the configuration below, we would look at n/2 elements.
Would we have to ever look at more than n/2 elements if we start in the middle, and choose
a direction based on which neighboring element is larger that the middle element?
扫盲区,高手请无视:
上面的公式中出现了大T,我们一般常见的时间复杂度和教科书中最常出现的是大O,那这个大T是个啥呢?
A、大O的定义:
如果存在正数c和N,对于所有的n>=N,有f(n)<=c*g(n),则f(n)=O(g(n))
B、Big Omega的定义
如果存在正数c和N,对于所有的n>=N,有f(n)>=c*g(n),则f(n)=Omega(g(n))
C、Big Theta的定义
如果存在正数c1,c2和N,对于所有的n>=N,有c1*g(n)<=f(n)<=c2*g(n),则f(n)=Theta(g(n))
(友情提示:大O和大Omega主要是>=和<=的区别)
好吧,好像还是有点抽象哈,那么再简单点说:
1、O是一个算法最坏情况的度量(悲观估计)
2、Big Omega是最好情况的度量(乐观估计)
3、Big Theta表达了一个算法的区间,不会好于A,不会坏于B(中性估计)
更加详细的解释,建议参考这篇:
科普一下算法的度量——Big O, Big Omega, Big Theta以及Udi Manber的大OO
前两种解法都是从边界开始破解,那么我们换一种思路,如果从中间的位置开始查找呢?假定一个数组中存在N
个元素,那么中间位置的元素为N/2
。
第三种:
这里我们采用二分法(Divide & Conquer)的思想,对于给定数组a,其中存在N
个元素,其中间元素为a[N/2]
,如果存在:
a[N/2 - 1] > a[N/2]
那么则在a[N/2]的最左侧开始寻找峰值元素,查找范围为1....N/2
,而如果存在:
a[N/2 + 1] > a[N/2]
那么则在a[N/2]的最右侧开始寻找峰值元素,查找范围为N/2 + 1
。
如果这两种情况均未满足,那么则可以得到结论,N/2
就是峰值元素。
根据上面的推导公式,该这种方式下的算法时间复杂度为O( log 2 ( n ) \log_2 (n) log2(n))。
当N
等于1000000
时,两种算法的运行速度差距极大,在python中,O(n)
的执行时间为13秒,而O( log 2 ( n ) \log_2 (n) log2(n))的执行时间为0.001秒。
下面给出Java版代码:
public class PeakFinder {
public static void main(String[] args) {
int[] arr1D = {1, 3, 2, 1, 5, 6, 9, 2};
System.out.println(peakFinding1D(arr1D));
}
/**
* 一维的峰值查找,核心思路:
* 1、首先查找数组的中间值a[mid];
* 2、判断a[mid]是否大于a[mid+1],大于的话对于数组前mid项继续1的步骤;
* 3、否则的话,判断a[mid]是否大于a[mid-1],大于的话对于数组mid项后面的数组继续1的步骤;
* 4、不满足2和3的话,则证明元素a[mid]就是峰值,返回即可.
* 时间复杂度:O(logn)
* @param arr
* @return
*/
private static int peakFinding1D(int[] arr) {
int length = arr.length;
int mid = (int)Math.floor(length / 2);
if (length == 1) {
return arr[0];
} else if (length == 2) {
return arr[0] > arr[1] ? arr[0] : arr[1];
}
if (arr[mid + 1] > arr[mid]) {
return peakFinding1D(Arrays.copyOfRange(arr, mid, length));
}
if (arr[mid - 1] > arr[mid]) {
return peakFinding1D(Arrays.copyOfRange(arr, 0, mid));
}
return arr[mid];
}
}
OK,上面我们讨论了一维数组的情况,那么下面我们再来看一下二维数组的情况。
假定存在一个n * m
的二维矩阵,那么在矩阵中,我们又该如何去界定峰值元素呢?
可以看到上图的二维矩阵,其中存在了5个元素,当元素a满足了:
a >=b, a >=d, a>=c, a>=e
时,元素a即为峰值元素。
那么,我们如何去寻找二维矩阵中的峰值元素,最暴力的思路,是循环寻找,即所谓的贪心算法(Greedy Ascent Algorithm)。
在矩阵中找到一个元素,指定一个方向,进行循环查找,直到找到峰值元素:
该种方式可以达成我们的诉求,但是在最糟糕的情况下,其时间复杂度度为:O(mn)
,当m = n
时,时间复杂度为O( n 2 n ^ 2 n2)。(这里教授没有对贪心算法进行详细的介绍,暂且接受这个结论)
显而易见,这个时间复杂度并不是可以接受的,当n
值特别大时,执行时间是非常冗长的。
OK,让我们再来换一种思路,刚刚在一维的情况下,已经找到了较好的解决方案,那么我们是否可以将问题简化,将二维矩阵转化为一维数组呢?
答案是可以的。
我们在二维矩阵中选定一行i
,并找到列的中心点j = m / 2
,像这样:
由此,我们将二维矩阵转化为一维数组,我们可以在i
行中寻找峰值元素,时间复杂度为O( log 2 ( n ) \log_2 (n) log2(n))。
但是这并没有结束,因为我们只是找到了第i
行中的峰值元素,我们无法确认它是否是真正的峰值元素。
可以参考上图所示,按照上面的寻找方式,我们可以发现14元素是其所在行的峰值元素,但是它并不是它所在列的峰值元素。
顺着这个思路,我们再进一步:
1、选定一个中间列j = m / 2
,寻找该列中最大的元素,那么该元素的坐标为(i, j)
;
2、比较该最大值再行进行比较(i, j − 1),(i, j),(i, j + 1)
;
3、如果存在(i, j − 1) > (i, j)
, 对前面的列进行递归1;
4、如果存在(i, j) < (i, j + 1)
,对后面的列进行递归1;
5、不满足3和4,则该值就满足二维峰值,返回即可。
假定存在二维矩阵,存在n
行m
列,我们推导该方法的算法时间复杂度:
即最坏情况下的时间复杂度为:O(n log 2 ( m ) \log_2 (m) log2(m))。
下面给出Java版代码:
public class PeakFinder {
public static void main(String[] args) {
int[][] arr2D = {
{12, 11, 4, 7}, {14, 13, 22, 21}, {15, 9, 11, 17}, {16, 17, 19, 20}};
System.out.println(peakFinding2D(arr2D, 0, arr2D.length));
}
/**
* 二维的峰值查找,核心思路是将二维转化为一维,然后求解:
* 1、查找中间列的最大值(i, j);
* 2、比较该最大值在行进行比较(i, j − 1),(i, j),(i, j + 1);
* 3、如果(i, j − 1) > (i, j), 对前面的列进行递归1;
* 4、如果(i, j) < (i, j + 1),对后面的列进行递归1;
* 5、不满足3和4,则该值就满足二维峰值,返回即可.
* @param arr
* @return
*/
private static int peakFinding2D(int[][] arr, int start, int end) {
int mid = (int)Math.ceil((start + end) / 2);
//查找中间列中的最大值元素所在的行
int maxIndex = findMaxIndex(arr, mid);
int maxIndexValue = arr[maxIndex][mid];
if (mid == 0 || mid == arr.length - 1) {
return maxIndexValue;
}
if (arr[maxIndex][mid + 1] > maxIndexValue) {
return peakFinding2D(arr, mid, end);
}
if (arr[maxIndex][mid - 1] > maxIndexValue) {
return peakFinding2D(arr, start, mid);
}
return arr[maxIndex][mid];
}
/**
* 查找某一列的最大值
* @param arr
* @param columnNum
* @return
*/
private static int findMaxIndex(int[][] arr, int columnNum) {
int length = arr.length;
int index = 0;
int max = arr[index][columnNum];
for (int i = 0; i < length; i++) {
if (arr[i][columnNum] > max) {
max = arr[i][columnNum];
index = i;
}
}
return index;
}
}