九章算法系列(#2 Binary Search)-课堂笔记

前言

先说一些题外的东西吧。受到春跃大神的影响和启发,推荐了这个算法公开课给我,晚上睡觉前点开一看发现课还有两天要开始,本着要好好系统地学习一下算法,于是就爬起来拉上两个小伙伴组团报名了。今天听了第一节课,说真的很实用,特别是对于我这种算法不扎实,并且又想找工作,提高自己的情况。 那就不多说废话了,以后每周都写个总结吧,就趁着这一个月好好把算法提高一下。具体就从:课堂笔记、leetcode和lintcode相关习题、hdu和poj相关习题三个方面来写吧。希望自己能够坚持下来,给大家分享一些好的东西。

 outline:

  • 第一境界:会写程序

    • Find First Position of Target
    • Find Last Position of Target
  • 第二境界:找到第一个/最后一个满足某个条件的位置/值
    • Search a 2D Matrix
    • Find Minimum in Rotated Sorted Array
  • 第三境界:保留有解的那一半
    • Find Peak Element

课堂笔记

二分查找这类题以前接触的也算是比较多的了,所以还算相对熟悉,但今天听老师讲过以后,还是觉得有了很多新的认识,最有印象的就是令狐老师讲的三个境界:



1. 第一境界:会写程序

这个境界我自认为在刷了那么多leetcode之后算是没有问题的了,套了不少模版,虽然还有一些边界问题考虑不周全,但是经过调试,应该没有什么问题,经过几次面试,也面到过Binary Seach。想必大家也有很好的基础。 正如老师说的,这个境界还是存在一些问题,比如解决二分程序的三大痛点、权衡递归与非递归。 对于第一个问题,其实就是start和end的位置选取,比如容易进入死循环,或者容易分不清楚到底应该是start = mid还是start = mid+ 1等。以下给出一个代码模版,这个也是我之前写二分问题经常会写的样子:

int start = 0, end = nums.size() - 1;
while (start < end){
    int mid = (start + end)/2;
    if (...) {...}
    else if (...) {...}
    else {...}
}

想必大家都会把循环条件写成start < end或者start <= end这样的,这样在一些情况下也确实没有问题(这里直接上一个题):

  Find First Position of Target

  http://www.lintcode.com/zh-cn/problem/first-position-of-target/

给定一个排序的整数数组(升序)和一个要查找的整数target,用O(logn)的时间查找到target第一次出现的下标(从0开始),如果target不存在于数组中,返回-1

样例

在数组 [1, 2, 3, 3, 4, 5, 10] 中二分查找3,返回2

这个题应该是Binary Search最基础的题,直接套用模版就可以,以下是这个题的代码(Bug Free):

    int binarySearch(vector<int> &array, int target) {
        if (!array.size()) return -1;
        int start = 0, end = array.size() - 1;
        while (start < end) {
            int mid = (start + end) >> 1;
            if (array[mid] < target) {
                start = mid + 1;
            } else if (array[mid] > target) {
                end = mid -1;
            } else {
                end = mid;
            }
        }
        if (array[start] == target) return start;
        return -1;
    }

因为比较简单,就不再多说了,这里需要注意的几个点是,int mid = (start + end) >>1;其实就是int mid = (start + end)/1;因为在面试中如果会位运算的话,还是能够给面试官留下很好的印象。有的人说直接加起来除以2会溢出,其实start和end不会大到超过int的最大值的,因为一个vector也不会去开辟那么大的空间,但是写成`int mid = (end - start)/2 + start;`也能显得你比较不错。综上,两种方法都可以。 在这个题中因为是找第一个与target相等的值,所以用这种方法不会出问题,但是在考虑下面的题,就会出现问题:

  Find Last Position of Target

  http://www.lintcode.com/zh-cn/problem/last-position-of-target/

给一个升序数组,找到target最后一次出现的位置,如果没出现过返回-1

样例

给出 [1, 2, 2, 4, 5, 5].

target = 2, 返回 2.

target = 5, 返回 5.

target = 6, 返回 -1.

错误代码如下:

while (start < end) {
        int mid = ( start + end ) >>1;
        if (A[mid] < target) {       start = mid + 1;     } else if ( A[mid] > target) {       end = mid -1;     } else {       start = mid;     }
    }

这里如果这样写的话,代码就会进入死循环,因为在求mid的时候是向左边取整的。考虑这样的一个情况[...,5,5],假设target为5,那么start就会一直向右靠近,最后到n-2的位置,而end此时为n-1,再次进入循环mid等于n-2,所以就进入了死循环。 根据课上老师所说的,建议大家写成start + 1 < end,最后再判断start和end(按照所需先后判断)即可,这种写法适用于所有的情况,不容易出现问题。 ps. 这里把条件写成如下也可行:

while (start + 1 < end) {
    int mid = (start + end)>>1;
    if (A[mid] > target) {    end = mid;
    } else {     start = mid;  }
}

因为start和end不管是否包括mid值都不影响最后的结果。 这个境界需要理解一个重点: 二分法实际上就是把区间变小的问题,把一个长度为n的区间变为n/2,然后再变小,即:

T(n) = T(n/2) + O(1) = O(logn)

通过O(1)的时间,把规模为n的问题变为n/2 当面试的时候,有O(n)的解,如果面试官需要你进一步优化,那么很大可能就是需要用二分O(logn)的方法来做。 实际上的步骤:

**区间缩小-> 剩下两个下标->判断两个下表**

**注:不要把缩小区间和得到答案放在一个循环里面,容易出问题,增加难度**



 2. 第二境界:找到第一个/最后一个满足某个条件的位置/值

这个境界就是第一个境界的进阶版本,就是能够把一些实际的应用问题转换为二分的核心问题:把具体的问题转变为找到数组中的额第一个/最后一个满足某个条件的位置/值。

就不多说了,直接上题吧:

  Seach a 2D Matrix

  http://www.lintcode.com/zh-cn/problem/search-a-2d-matrix/

写出一个高效的算法来搜索 m × n矩阵中的值。

这个矩阵具有以下特性:

  • 每行中的整数从左到右是排序的。
  • 每行的第一个数大于上一行的最后一个整数。

样例

考虑下列矩阵:

[
  [1, 3, 5, 7],
  [10, 11, 16, 20],
  [23, 30, 34, 50]
]

给出 target = 3,返回 true

这道题最简单的方式就是对每一行进行一次二分查找,第一行没有找到就找第二行,以此类推,那么时间复杂度为0(nlogn)。 如果需要再进行优化,那么可以这样考虑:因为条件中有每行的第一个数大于上一行的最后一个整数。所以我们可以先对每行的第一个数来进行一个二分查找,找到最后一个不大于target的数(注意这里是二分查找的核心思想)然后再对这一行进行二分查找即可,这样首先对每行的第一个数查找复杂度为0(logn),再对某一行进行查找,复杂度为O(logn),所以为O(logn)。代码如下(Bug Free):

bool searchMatrix(vector<vector<int> > &matrix, int target) {
    if (!matrix.size()||!matrix[0].size()) return false;
    int start = 0, end = matrix.size() - 1;
    while (start + 1 < end) {
        int mid = (end - start)/2 + start;
        if (matrix[mid][0] < target) start = mid;
        else end = mid;
    }
    int new_start = 0,new_end = matrix[0].size()-1;
    int index = matrix[end][0] <= target ?end:start;
    while (new_start + 1 < new_end) {
        int mid = (new_end - new_start)/2 + new_start;
        if (matrix[index][mid] > target) new_end = mid;
        else if (matrix[index][mid] < target)
            new_start = mid;
        else return true;
    }
    if (matrix[index][new_end] == target) return true;
    if (matrix[index][new_start] == target) return true;
    return false;
}

这个题关键在于要对两个端点的把握,还是二分查找的基本流程:先缩小区间,然后对两个剩余的端点进行判断。

当然这个题也有不需要进行两次计算的方法,因为当前行的所有元素严格大于第一行,所以可以把矩阵考虑为一维的数组,只需要在切换的时候进行一个行和列的转换即可,具体代码如下(Bug Free):

    bool searchMatrix(vector<vector<int> > &matrix, int target) {
        if (!matrix.size()||!matrix[0].size()) return false;
        int m = matrix.size();
        int n = matrix[0].size();
        int start = 0, end = n * m - 1;
        while (start + 1 < end) {
            int mid = (end - start)/2 + start;
            int x = mid / n;
            int y = mid % n;
            if (matrix[x][y] > target) {
                end = mid;
            } else {
                start = mid;
            }
        }
        int x = start / n;
        int y = start % n;
        if (matrix[x][y] == target) {
            return true;
        }
        x = end / n;
        y = end % n;
        if (matrix[x][y] == target) {
            return true;
        }
        return false;
    }

再来一个题吧:

  Find Minimum in Rotated Sorted Array

  http://www.lintcode.com/zh-cn/problem/find-minimum-in-rotated-sorted-array/

假设一个旋转排序的数组其起始位置是未知的(比如0 1 2 4 5 6 7 可能变成是4 5 6 7 0 1 2)。

你需要找到其中最小的元素。

你可以假设数组中不存在重复的元素。

样例

给出[4,5,6,7,0,1,2]  返回 0

这个题如果按照我以前的想法,就是最直观的办法:直接从头遍历,发现某一个值小于前一个值,并且小于后一个值,那么这个值就是最小的。这样的复杂度就是O(n),我记得有一次面试的时候就是这么回答了,然后遭到了面试官无情的鄙视。 这里使用二分的话,是非常有技巧的,这个技巧也是对于之后难一些的题来说需要掌握的,我们要时刻不能忘记二分的宗旨:把具体的问题转变为找到数组中的额第一个/最后一个满足某个条件的位置/值。这题其实可以这么考虑:由最小值为中心把两边分开,两边都是递增的,而后一部分的最大值也严格小于前一部分的所有值,显然,最后一部分的最大值就是num[n-1]那么我们只需要找到比这个值小的第一个值即可。这里是不是又回到了最原始的问题。代码如下(Bug Free):

    int findMin(vector<int> &num) {
        if (!num.size()) return 0;
        int start = 0, end = num.size() - 1;
        int target = num[end];
        while (start + 1 < end) {
            int mid = (end - start)/2 + start;
            if (num[mid] <= target) {
                end = mid;
            }
            else {
                start = mid;
            }
        }
        if (num[start] <= target) {
            return num[start];
        }
        else {
            return num[end];
        }
    }

这题稍微有一些和之前不一样的地方,就是在两个部分之间进行了一个权衡,在start和end的变换的地方,需要大家注意。


3. 第三境界:保留有解的那一半

有了前面两个阶段的铺垫,达到一定的训练以后,应该也差不多能够熟练掌握中等和简单的题了,其实足够深刻地理解了二分的精髓以后,可以把几个习题都做一遍,尽量还是达到Bug Free的级别吧(这里所说的Bug Free是指能够不用编译器的情况下,直接空手写代码,用眼睛和笔来调试,最后提交后Accepted)。

第三个阶段呢,其实就是回到了二分本身的定义,我的理解就是:二分法其实就是把问题不断缩小为原来的n/2,然后再找到相应的位置进行处理。那么二分法的最高境界就是学会去保留有答案的那一半,也许你心里会想:这个我本来就知道啊,但是真正到了实际操作的时候,还是会有搞不清楚的时候。贴出一道题:

  Find Peak Element

  http://www.lintcode.com/zh-cn/problem/find-peak-element/

给出一个整数数组(size为n),其具有以下特点:

  • 相邻位置的数字是不同的
  • A[0] < A[1] 并且 A[n - 2] > A[n - 1]

假定P是峰值的位置则满足A[P] > A[P-1]A[P] > A[P+1],返回数组中任意一个峰值的位置。

样例

给出数组[1, 2, 1, 3, 4, 5, 7, 6]返回1, 即数值 2 所在位置, 或者6, 即数值 7 所在位置.

这个题算是比较简单的题,但是重要的是理解其中的思想,还是回到二分法第三个阶段的核心:保留有答案的那一半。这道题只是要求找到其中一个峰值即可。峰值满足的条件就是左边的部分是单调递增的,而右边的部分是单调递减的(如果该点可导,并且导数为0,那么这个点就是峰值),我们很容易在纸上画出来某个点的四种情况(如下图所示):

第一种情况:当前点就是峰值,直接返回当前值。

第二种情况:当前点是谷点,不论往那边走都可以找到峰值。

第三种情况:当前点处于下降的中间,往左边走可以到达峰值。

第四种情况:当前点处于上升的中间,往右边走可以达到峰值。

分析了四种情况,那么就容易把有答案的一半保留下来了,接下来就判断是否能够找到峰值即可。代码如下(Bug Free):

  int findPeak(vector<int> A) {
      if (!A.size()) return 0;
      int start = 0;
      int end = A.size() -1;
      while (start + 1 < end) {
          int mid = (end - start)/2 + start;
          if (A[mid] > A[mid - 1] && A[mid] > A[mid + 1]) {
              return mid;
          } else if (A[mid] <= A[mid+1] && A[mid] >= A[mid -1]) {
              start = mid;
          } else if (A[mid] >= A[mid+1] && A[mid] <= A[mid -1]) {
              end = mid;
          } else {
              start = mid;
          }
      }
      if (start >= 1 && A[start] > A[start - 1] && A[start] > A[start + 1]) return start;      if (end <= A.size()-2 && A[end] > A[end-1] && A[end] > A[end+1]) return end;
  }

这道题的难点其实就是把各种情况考虑一下,然后把有答案的部分保留下来,基本上就没有问题了。



总结

本文只是挑选了一些比较好的课上的题进行了讲解,还有部分题没有写出来,也会在后续的博客中。

对于我个人而言,二分法算是比较熟悉的一个方法,之前在做微软校招第一题的时候用的就是二分的方法。在面试中也是比较常用到的一种方法,因为总有那么一种说法嘛:比0(n)还要快的算法复杂度,那必须就是0(logn)了(这里说的是在一般的面试情况下)那么O(logn)就必然要考虑二分的方法来做了。一般都会与一些排序的序列、在一段有规则的序列等情况中找到符合某个条件的位置/值。这个模块还是需要多练习,然后就能够很好上手了,如果想要能够在算法面试中有更好的突破,还是需要去解决一些难一点的题,诸如poj或者hdu这样的应用场景的题。

这也是本人第一次认真写一个技术长文,虽然也没有什么特别深奥的东西,读到这里说明你也是很给我面子的了,之后还会继续更新一些自己的想法和一些好的题目,希望大家多多支持!

时间: 2024-10-10 02:46:45

九章算法系列(#2 Binary Search)-课堂笔记的相关文章

九章算法系列(#5 Linked List)-课堂笔记

前言 又是很长时间才回来发一篇博客,前一个月确实因为杂七杂八的事情影响了很多,现在还是到了大火燃眉毛的时候了,也应该开始继续整理一下算法的思路了.Linked List大家应该是特别熟悉不过的了,因为这个算是数据结构了里面基本上最开始讲的结构吧.这块内容也没有太多需要琢磨的技巧,可以考量的东西也不多,所以考的就是一些小的trick来完成,面试中链表考得特别多,算是面试官对面试者的基础的考查,所以我建议大家在Linked List这一章,一定要实现Bug Free.这个也是我练的比较多的,有些想法

九章算法系列(#3 Dynamic Programming)-课堂笔记

前言 时隔这么久才发了这篇早在三周前就应该发出来的课堂笔记,由于懒癌犯了,加上各种原因,实在是应该反思.好多课堂上老师说的重要的东西可能细节上有一些急记不住了,但是幸好做了一些笔记,还能够让自己回想起来.动态规划算是我的一道大坎了,本科的时候就基本没有学过,研一的时候老师上课也是吃力的跟上了老师的步伐,其实那个时候老师总结的还是挺好的:把动态规划的题目都分成了一维动规.二维遍历.二维不遍历等一系列的问题.这次听了老师的课程,觉得还是需要更加集中的去把各种题进行一个分类吧,然后有针对的去准备,虽然

九章算法

九章算法 前言 第一天的算法都还没有缓过来,直接就进入了第二天的算法学习.前一天一直在整理Binary Search的笔记,也没有提前预习一下,好在Binary Tree算是自己最熟的地方了吧(LeetCode上面Binary Tree的题刷了4遍,目前95%以上能够Bug Free)所以还能跟得上,今天听了一下,觉得学习到最多的,就是把Traverse和Divide Conquer分开来讨论,觉得开启了一片新的天地!今天写这个博客我就尽量把两种方式都写一写吧. Outline: 二叉树的遍历

分治习题--九章算法培训课第三章笔记

1.Maximum Depth of Binary Tree 这是道简单的分治习题了 分: 左子树最大深度 右子树最大深度 治: 最大深度等于max(左子树,右子树)+1 public class Solution { public int maxDepth(TreeNode root) { if (root == null) { return 0; } int left = maxDepth(root.left); int right = maxDepth(root.right); retur

九章算法 基础算法 强化算法 系统设计 大数据 安卓 leetcode 高清视频

leetcode 直播视频讲座录像 九章算法视频录像,PPT 算法班,算法强化班,Java入门与基础算法班,big data项目实战班,Andriod项目实战班 九章算法下载 九章算法面试 九章算法leetcode 九章算法答案 九章算法mitbbs 九章算法班 九章算法ppt 九章算法录像 九章算法培训 九章算法微博 leetcode 视频 九章算法偷录 算法培训 算法班课程大纲: 1 从strStr谈面试技巧与Coding Style(免费试听) 2 二分搜索与旋转排序数组 Binary S

九章算法 课程 视频 录制 免费下载 cs3k.com

cs3k.com [ 九章算法强化班 ] 课程 完整 视频 录制 免费下载 最新一期 << 九章算法强化班 >> 课程完整视频录制, 课件打包下载, 百度云盘 地址 ! 本站长期提供九章算法免费课程视频 与 课件打包下载 Big Table 教学TodoList 探讨System Design 分析Rate Limiter 视频Binary Tree 研究Dribbble 设计Big Data 研究PageRank 面试Google Suggestion 设计Location Ba

CS3K.com 九章算法 课程 视频 录制 免费下载

[ 九章算法班 ] 课程 完整 视频 录制 免费下载最新一期 << 九章算法班 >> 课程完整视频录制, 课件打包下载, 百度云盘 地址 ! 本站长期提供九章算法免费课程视频 与 课件打包下载 Dribbble 视频System Design 探讨Distributed File System 研究MiniLinkedin 探讨Map Reduce 分析Array 视频Design Tiny Url 教学Android 设计Divide Conquer 研究PageRank 视频

【九章算法免费公开课】从 strStr 谈面试技巧与 Coding Style

刷题到底刷到什么程度才够? 题目不会直接说不会么? 为什么题目都做出来还是老是挂? 觉得面试官总在为难你? 从来就搞不懂动态规划是什么? 如何正确的骑驴找马? 什么是正确的Coding Style? 面试中该与面试官如何沟通? 本周日,九章算法<算法班>金牌讲师-段誉 为您倾情奉献,同时提供实时在线问答环节,解答您最想知道的面试"内幕"! 讲座时间: 北京时间6月14日 09:30-11:30 (周日) 美西时间6月13日 18:30-20:30(周六) 报名链接: htt

九章算法--分配抄书员

思路: (1)最常见的思路就是dp:状态表示为dp[i][j],表示前j个人抄i本书最少时间:dp[i][j] = min(max(dp[k][j-1],sum(k+1,i))) (j<=k<i): 解释一下min,max操作,由于抄书是并行,所以就是取所有人时间里的最大值 (2)二分+贪心:我自己一开始的思路是总和除以人数,然后贪心,但是这样可能不是最优解.但是我们可以计算每个人最大和最小抄书的时间,然后二分搜索这个时间即可. 下面代码是DP:但是没有考虑边界情况只是写来熟悉算法流程. 1