前言

先说一些题外的东西吧。受到春跃大神的影响和启发,推荐了这个算法公开课给我,晚上睡觉前点开一看发现课还有两天要开始,本着要好好系统地学习一下算法,于是就爬起来拉上两个小伙伴组团报名了。今天听了第一节课,说真的很实用,特别是对于我这种算法不扎实,并且又想找工作,提高自己的情况。 那就不多说废话了,以后每周都写个总结吧,就趁着这一个月好好把算法提高一下。具体就从:课堂笔记、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 = , end = nums.size() - ;
while (start < end){
int mid = (start + end)/;
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 -;
int start = , end = array.size() - ;
while (start < end) {
int mid = (start + end) >> ;
if (array[mid] < target) {
start = mid + ;
} else if (array[mid] > target) {
end = mid -;
} else {
end = mid;
}
}
if (array[start] == target) return start;
return -;
}

因为比较简单,就不再多说了,这里需要注意的几个点是,int mid = (start + end) >>1;其实就是int mid = (start + end)/2;因为在面试中如果会位运算的话,还是能够给面试官留下很好的印象。有的人说直接加起来除以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 ) >>;
if (A[mid] < target) {
       start = mid + ;
     } else if ( A[mid] > target) {
       end = mid -;
     } 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 +  < end) {
int mid = (start + end)>>;
if (A[mid] > target) {
    end = mid;
} else {
    start = mid;
  }
}

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

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

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

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

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


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

这个境界就是第一个境界的进阶版本,就是能够把一些实际的应用问题转换为二分的核心问题:把具体的问题转变为找到数组中的第一个/最后一个满足某个条件的位置/值。 
就不多说了,直接上题吧:
  Seach 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[].size()) return false;
int start = , end = matrix.size() - ;
while (start + < end) {
int mid = (end - start)/ + start;
if (matrix[mid][] < target) start = mid;
else end = mid;
}
int new_start = ,new_end = matrix[].size()-;
int index = matrix[end][] <= target ?end:start;
while (new_start + < new_end) {
int mid = (new_end - new_start)/ + 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[].size()) return false;
int m = matrix.size();
int n = matrix[].size();
int start = , end = n * m - ;
while (start + < end) {
int mid = (end - start)/ + 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

假设一个旋转排序的数组其起始位置是未知的(比如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 ;
int start = , end = num.size() - ;
int target = num[end];
while (start + < end) {
int mid = (end - start)/ + 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

给出一个整数数组(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 ;
int start = ;
int end = A.size() -;
while (start + < end) {
int mid = (end - start)/ + start;
if (A[mid] > A[mid - ] && A[mid] > A[mid + ]) {
return mid;
} else if (A[mid] <= A[mid+] && A[mid] >= A[mid -]) {
start = mid;
} else if (A[mid] >= A[mid+] && A[mid] <= A[mid -]) {
end = mid;
} else {
start = mid;
}
}
if (start >= && A[start] > A[start - ] && A[start] > A[start + ]) return start;
if (end <= A.size()- && A[end] > A[end-] && A[end] > A[end+]) return end;
}

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


总结

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

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

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

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

  1. 九章算法系列(#3 Binary Tree & Divide Conquer)-课堂笔记

    前言 第一天的算法都还没有缓过来,直接就进入了第二天的算法学习.前一天一直在整理Binary Search的笔记,也没有提前预习一下,好在Binary Tree算是自己最熟的地方了吧(LeetCode ...

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

    前言 又是很长时间才回来发一篇博客,前一个月确实因为杂七杂八的事情影响了很多,现在还是到了大火燃眉毛的时候了,也应该开始继续整理一下算法的思路了.Linked List大家应该是特别熟悉不过的了,因为 ...

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

    前言 时隔这么久才发了这篇早在三周前就应该发出来的课堂笔记,由于懒癌犯了,加上各种原因,实在是应该反思.好多课堂上老师说的重要的东西可能细节上有一些急记不住了,但是幸好做了一些笔记,还能够让自己回想起 ...

  4. (lintcode全部题目解答之)九章算法之算法班题目全解(附容易犯的错误)

    --------------------------------------------------------------- 本文使用方法:所有题目,只需要把标题输入lintcode就能找到.主要是 ...

  5. 7九章算法强化班全解--------Hadoop跃爷Spark

    ------------------------------------------------------------第七周:Follow up question 1,寻找峰值 寻找峰值 描述 笔记 ...

  6. [Data Structure] 二叉搜索树(Binary Search Tree) - 笔记

    1. 二叉搜索树,可以用作字典,或者优先队列. 2. 根节点 root 是树结构里面唯一一个其父节点为空的节点. 3. 二叉树搜索树的属性: 假设 x 是二叉搜索树的一个节点.如果 y 是 x 左子树 ...

  7. 算法导论学习-binary search tree

    1. 概念: Binary-search tree(BST)是一颗二叉树,每个树上的节点都有<=1个父亲节点,ROOT节点没有父亲节点.同时每个树上的节点都有[0,2]个孩子节点(left ch ...

  8. 九章算法:BAT国内班 - 课程大纲

    第1章 国内笔试面试风格及准备方法 --- 分享面试经验,通过例题分析国内面试的风格及准备方法 · 1) C/C++部分: 实现 memcpy 函数 STL 中 vector 的实现原理 · 2)概率 ...

  9. 【算法模板】Binary Search 二分查找

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

随机推荐

  1. 第八章I/O

    一.File的使用 ①.new File(String fileName);的意义 ②.获取当前文件夹下的所有文件 ③.获取当前文件夹时候过滤掉不许要的文件夹 ④.创建File文件,了解mkDir() ...

  2. Python爬虫学习:二、爬虫的初步尝试

    我使用的编辑器是IDLE,版本为Python2.7.11,Windows平台. 本文是博主原创随笔,转载时请注明出处Maple2cat|Python爬虫学习:二.爬虫的初步尝试 1.尝试抓取指定网页 ...

  3. SQL查询 addScalar()或addEntity()

    Hibernate除了支持HQL查询外,还支持原生SQL查询.   对原生SQL查询执行的控制是通过SQLQuery接口进行的,通过执行Session.createSQLQuery()获取这个接口.该 ...

  4. Fedora安装qt总结四种方法

    在fedora上安装qt有四种方法,本人由于初次接触fedora,所以还是耐心的把三个方法都测试了一遍. 1.  下载源码,手动编译,选择路径安装,请参考<fedora15下搭建QT开发环境及编 ...

  5. Fedora15下搭建QT开发环境及编译QT

    看了不少linux上编译qt的文章,实际上直接通过yum 安装qt是最方便的,请参考<yum安装qt> 不过初步接触fedora,为了了解一下如何在linux上编译.安装开源代码,所以必须 ...

  6. 学DSP(一):开始

    DSP有digital signal process 和 digital signal processor 2个意思,数字信号处理和数字信号处理器,我这里就是学数字信号处理器了. 我为什么要学DSP, ...

  7. MongoDBAuth

    1,mogoDB 认证登陆

  8. 实现action的三种方法

    1.一个普通的类 public class testAction1 { public String execute(){ return "success"; } } 2.实现Act ...

  9. Trie树|字典树(字符串排序)

    有时,我们会碰到对字符串的排序,若采用一些经典的排序算法,则时间复杂度一般为O(n*lgn),但若采用Trie树,则时间复杂度仅为O(n). Trie树又名字典树,从字面意思即可理解,这种树的结构像英 ...

  10. 网络编程API-下 (I/O复用函数)

    IO复用是Linux中的IO模型之中的一个,IO复用就是进程预先告诉内核须要监视的IO条件,使得内核一旦发现进程指定的一个或多个IO条件就绪,就通过进程进程处理.从而不会在单个IO上堵塞了. Linu ...