麻省理工公开课算法导论(一):peak finder

前言

本篇来自于笔者学习MIT的公开课算法导论的学习笔记,仅仅是我个人接受课程教育后,进行的学习笔记,可能理解并不到位,仅供参考。

课程视频地址:
Lecture 1: Introduction and Peak Finding

Peak Finder

Lecture Overview

  • Administrivia
  • Course Overview
  • “Peak finding” problem — 1D and 2D versions

Peak Finder

One-dimensional Version(一维版本)

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?


  • If a[n/2] < a[n/2 − 1] then only look at left half 1 . . . n/2 − − − 1 to look for peak
  • Else if a[n/2] < a[n/2 + 1] then only look at right half n/2 + 1 …n to look for peak
  • Else n/2 position is a peak: WHY?

扫盲区,高手请无视:

上面的公式中出现了大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];
    }
}
Two-dimensional Version(二维版本)

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,则该值就满足二维峰值,返回即可。

假定存在二维矩阵,存在nm列,我们推导该方法的算法时间复杂度:

即最坏情况下的时间复杂度为: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;
    }
}
  • 1
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值