九章算法高级班笔记4.二分法深入和扫描线
- 二分法的深入理解
- 二维二分
- 按照值域进行二分
- Stack
- 扫描线
3.双端队列
Find Peak Element II 算法视频QQ_1603159172
There is an integer matrix which has the following features:
The numbers in adjacent positions are different.
The matrix has n rows and m columns.
For all i < m, A[0][i] < A[1][i] && A[n – 2][i] > A[n – 1][i].
For all j < n, A[j][0] < A[j][1] && A[j][m – 2] > A[j][m – 1].
We define a position P is a peek if:
A[j][i] > A[j+1][i] && A[j][i] > A[j-1][i] && A[j][i] > A[j][i+1] && A[j][i] > A[j][i-1]
Find a peak element in this matrix. Return the index of the peak.
Notice
The matrix may contains multiple peeks, find any of them.
Have you met this question in a real interview? Yes
Example
Given a matrix:
[
[1 ,2 ,3 ,6 ,5],
[16,41,23,22,6],
[15,17,24,21,7],
[14,18,19,20,10],
[13,14,11,10,9]
]
return index of 41 (which is [1,1]) or index of 24 (which is [2,2])
这道题我想到的是随便找个位置当起点,然后往高处爬.
我开始觉得这种算法的时间复杂度为m+n, 后来觉得自己有点脑残...
这种做法最坏情况下的时间复杂度为0(n^2).
情况如下:
XXXXXXXXXXXXXXXXXXXXXXXXXXXXX
X 1 2 3 4 5 6 7 8 9 10 11XXXX
X 19 18 17 16 15 14 13 12XXXX
X 20 21 22 23 24 25 26 27XXXX
......
呈蛇形排列的情况.
然后呢我们想想能不能类似于find peak I 里面的二分去掉一半?
我们首先改一下栗子...改成如下,方便理解:
[ 1, 2, 3, 6, 5],
[16, 41, 23, 22, 6],
[15, 17, 16, 21, 7],
[14, 18, 19, 20, 10],
[13, 14, 11, 10, 9]
我们先假设取中间的一行:
[15, 17, 16, 21, 7]
中间一行有两个极大值:17和21, 我们目标是:看看找到极值,然后极值往高了爬,能不能甩掉身后小的那半边矩阵, 并保证在剩下的一般里仍然有要找的答案.
假设我们先选择17,然后上下找比它大的,意图删掉带有17的那半边:
如果走到41,是ok的...
但是如果走到18呢,18->19->20->21,又走回原来的第3行了...阿西吧,行不通.
然后我们试试看看最大值,21呢?因为21是第三行的最大值, 所以我们如果找个一个比21大的, 就是第二行第四列的22. 我们发现, 如果从21走到22, 在从22找比它大的往高爬, 永远都不会爬回21所在的第三行, 所以我成功甩掉了第四五行…下面我们来确定在剩下的一半里面, 一定有我们要找的答案:
题目中的条件:
For all i < m, A[0][i] < A[1][i] && A[n – 2][i] > A[n – 1][i].
For all j < n, A[j][0] < A[j][1] && A[j][m – 2] > A[j][m – 1].
可以保证四周的一圈是山脚,比中间的要小, 这样才能确定中间一定有个极值峰, 如pic4.1图左, 当删掉21所在的第三行下面的第四五行, 图变成了pic4.1的右图形式,保证了峰值依然存在:
class Solution {
/**
* @param A: An integer matrix
* @return: The index of the peak
*/
public List find(int x1, int x2, int y1, int y2,
int[][] A, boolean flag) {
if (flag) {
int mid = x1 + (x2 - x1) / 2;
int index = y1;
for (int i = y1; i A[mid][index])
index = i;
if (A[mid - 1][index] > A[mid][index])
return find(x1, mid - 1, y1, y2, A, !flag);
else if (A[mid + 1][index] > A[mid][index])
return find(mid + 1, x2, y1, y2, A, !flag);
else
return new ArrayList(Arrays.asList(mid, index));
} else {
int mid = y1 + (y2 - y1) / 2;
int index = x1;
for (int i = x1; i A[index][mid])
index = i;
if (A[index][mid - 1] > A[index][mid])
return find(x1, x2, y1, mid - 1, A, !flag);
else if (A[index][mid + 1] > A[index][mid])
return find(x1, x2, mid + 1, y2, A, !flag);
else
return new ArrayList(Arrays.asList(index, mid));
}
}
public List findPeakII(int[][] A) {
// write your code here
int n = A.length;
int m = A[0].length;
return find(1, n - 2, 1, m - 2, A, true);
}
}
这道题的时间复杂度是:
T(n) = O(n) + O(n/2) + T(n/2)
最后的时间还是O(3n), 即为O(n).
二分法之二分答案 Binary Search on Result
算法视频QQ_1603159172
往往没有给你一个数组让你二分, 而是找到满足某个条件的最大或者最小值
因为找答案有两种思路:
- 比较常见的是直接求, 我们一般都是会想到这个方法
- 答案如果有二分性的话, 我们可以猜答案, 然后验证.
而神马是有二分性呢:
就是一个区间, 中间某个点画条线, 线一边的全部满足条件, 另一边的全部不满足:
-> 满足 ->|
|____________|____________|
解题方法:
通过猜值判断是否满足题意不对去搜索可能解
- 找到可行解范围
- 猜答案
- 检验条件
- 调整搜索范围
模板如下:
Sqrt(x)
算法视频QQ_1603159172
Implement int sqrt(int x).
Compute and return the square root of x.
Have you met this question in a real interview? Yes
Example
sqrt(3) = 1
sqrt(4) = 2
sqrt(5) = 2
sqrt(10) = 3
class Solution {
/**
* @param x: An integer
* @return: The sqrt of x
*/
public int sqrt(int x) {
// find the last number which square of it <= x
long start = 1, end = x;
while (start + 1 < end) {
long mid = start + (end - start) / 2;
if (mid * mid <= x) {
start = mid;
} else {
end = mid;
}
}
if (end * end <= x) {
return (int) end;
}
return (int) start;
}
}
Sqrt(x) II
算法视频QQ_1603159172
Implement double sqrt(double x) and x >= 0.
Compute and return the square root of x.
Notice
You do not care about the accuracy of the result, we will help you to output results.
Have you met this question in a real interview? Yes
Example
Given n = 2 return 1.41421356
这道题有个浮点精度问题, 在计算机里面, 两个树如果不相等,但是因为精度问题, 计算机会判断他们相等. 计算机如何判断两个浮点数y和z相等呢?
if (y ==z) 对于计算机来说是 if (abs(y-z)<eps), 这个eps是计算机设定的很小的值. 那么对于计算机来说:
y = 1.0
z = 1.000000000000000000000000000001
是相等的, 虽然他们本来是不相等的.
这道题的eps肿么确定呢?
答案就是: 和面试官商量…
public class Solution {
/**
* @param x a double
* @return the square root of x
*/
public double sqrt(double x) {
// Write your code here
double left = 0.0;
double right = x;
double eps = 1e-12;
if(right eps) {
// 二分浮点数 和二分整数不同
// 一般都有一个精度的要求 譬如这题就是要求小数点后八位
// 也就是只要我们二分的结果达到了这个精度的要求就可以
// 所以 需要让 right 和 left 小于一个我们事先设定好的精度值 eps
// 一般eps的设定1e-8,因为这题的要求是到1e-8,所以我把精度调到了1e-12
// 最后 选择 left 或 right 作为一个结果即可
double mid = (right + left) / 2;
if(mid * mid < x) {
left = mid;
}
else {
right = mid;
}
}
return left;
}
}
Wood Cut
算法视频QQ_1603159172
Given n pieces of wood with length L[i] (integer array). Cut them into small pieces to guarantee you could have equal or more than k pieces with the same length. What is the longest length you can get from the n pieces of wood? Given L & k, return the maximum length of the small pieces.
Notice
You couldn’t cut wood into float length.
If you couldn’t get >= k pieces, return 0.
Have you met this question in a real interview? Yes
Example
For L=[232, 124, 456], k=7, return 114.
这道题可以用heap来做, 同时呢, 也可以二分试答案:
public class Solution {
/**
*@param L: Given n pieces of wood with length L[i]
*@param k: An integer
*return: The maximum length of the small pieces.
*/
public int woodCut(int[] L, int k) {
int max = 0;
for (int i = 0; i = k) {
return end;
}
if (count(L, start) >= k) {
return start;
}
return 0;
}
private int count(int[] L, int length) {
int sum = 0;
for (int i = 0; i < L.length; i++) {
sum += L[i] / length;
}
return sum;
}
}
有个小问题:
这道题如果范围是[1, INT_MAX]的话, 最多我们能找多少次呢?
就是log(max -min)次. 那么这个数大约是多大呢?
31, 因为int是四个byte,就是最多32位, 32位的最大的整数是2^31-1
,所以需要找log(2^31-1-1)次, 就是大约31次. 那么时间复杂度是多少呢?
log(max-min) * O(n)
Find the Duplicate Number
算法视频QQ_1603159172
Given an array nums containing n + 1 integers where each integer is between 1 and n (inclusive), prove that at least one duplicate number must exist. Assume that there is only one duplicate number, find the duplicate one.
Notice
You must not modify the array (assume the array is read only).
You must use only constant, O(1) extra space.
Your runtime complexity should be less than O(n^2).
There is only one duplicate number in the array, but it could be repeated more than once.
Have you met this question in a real interview? Yes
Example
Given nums = [5,5,4,3,2,1] return 5
Given nums = [5,4,4,3,2,1] return 4
因为一共有n+1个数,所以如果一个一个找的话,对于
[5,5,4,3,2,1] 来说呢?我们一个一个试:
1的时候 小于等于1的 1个
2的时候 小于等于2的 2个
3的时候 小于等于3的 3个
4的时候 小于等于4的 4个
5的时候 小于等于5的 6个
我们发现, 对于数字n来说,如果小于等于它的数字个数小于等于n, 那么它和它之前木有重复的. 反之有重复.
对于这种for的查找, 答案有二分性, 我们要想到神马?
!!!二分法
public class Solution {
/**
* @param nums an array containing n + 1 integers which is between 1 and n
* @return the duplicate one
*/
public int findDuplicate(int[] nums) {
// Write your code here
int start = 1;
int end = nums.length - 1;
while(start + 1 < end) {
int mid = start + (end - start) / 2;
if (check_smaller_num(mid, nums) <= mid) {
start = mid;
} else {
end = mid;
}
}
if (check_smaller_num(start, nums) <= start) {
return end;
}
return start;
}
public int check_smaller_num(int mid, int[] nums) {
int cnt = 0;
for(int i = 0; i < nums.length; i++){
if(nums[i] <= mid){
cnt++;
}
}
return cnt;
}
}
// 映射法
public class Solution {
/**
* @param nums an array containing n + 1 integers which is between 1 and n
* @return the duplicate one
*/
public int findDuplicate(int[] nums) {
// Write your code here
if (nums.length <= 1)
return -1;
int slow = nums[0];
int fast = nums[nums[0]];
while (slow != fast) {
slow = nums[slow];
fast = nums[nums[fast]];
}
fast = 0;
while (fast != slow) {
fast = nums[fast];
slow = nums[slow];
}
return slow;
}
}
神马情况不用二分呢?
满足条件和不满足条件的混合:
-> 不满足->满足
|____________|____________|
Sweep-Line
算法视频QQ_1603159172
Number of Airplanes in the Sky
Given an interval list which are flying and landing time of the flight. How many airplanes are on the sky at most?
Notice
If landing and flying happens at the same time, we consider landing should happen at first.
Have you met this question in a real interview? Yes
Example
For interval list
[
[1,10],
[2,3],
[5,8],
[4,7]
]
这道题呢, 和起点终点的大小有关.
但是有个问题, 如果按起点排序呢, 终点没法处理.
如果按终点排序呢, 起点又没法处理.
区间类的问题, 我们要想扫描线.
这道题我们可以怎么做?
for一遍1 2 3 4 5 6 7 8 9 10的时间段, 找当时天空中一共有几架飞机, 然后取最大
本质是这样的, 如图4.2所示:
我们有个时间轴, 每个时间我们画一条铅笔的总线, 看各个时间的这条总线和横线的区间交点最多是几个.
现在我们把起点和终点拆开记录,T代表起飞, F代表降落:
1 T 10 F
2 T 3 F
5 T 8 F
4 T 7 F
排序然后依次遍历这些时间节点, 记录count:
1 count++
2 count++
3 count--
......
class Point{
int time;
int flag;
Point(int t, int s){
this.time = t;
this.flag = s;
}
public static Comparator PointComparator = new Comparator(){
public int compare(Point p1, Point p2){
if(p1.time == p2.time) return p1.flag - p2.flag;
else return p1.time - p2.time;
}
};
}
class Solution {
/**
* @param intervals: An interval array
* @return: Count of airplanes are in the sky.
*/
public int countOfAirplanes(List airplanes) {
List list = new ArrayList(airplanes.size()*2);
for(Interval i : airplanes){
list.add(new Point(i.start, 1));
list.add(new Point(i.end, 0));
}
Collections.sort(list,Point.PointComparator );
int count = 0, ans = 0;
for(Point p : list){
if(p.flag == 1) count++;
else count--;
ans = Math.max(ans, count);
}
return ans;
}
}
Building Outline
算法视频QQ_1603159172
Given N buildings in a x-axis,each building is a rectangle and can be represented by a triple (start, end, height),where start is the start position on x-axis, end is the end position on x-axis and height is the height of the building. Buildings may overlap if you see them from far away,find the outline of them。
An outline can be represented by a triple, (start, end, height), where start is the start position on x-axis of the outline, end is the end position on x-axis and height is the height of the outline.
Notice
Please merge the adjacent outlines if they have the same height and make sure different outlines cant overlap on x-axis.
Example
Given 3 buildings:
[
[1, 3, 3],
[2, 4, 4],
[5, 6, 1]
]
The outlines are:
[
[1, 2, 3],
[2, 4, 4],
[5, 6, 1]
]
图示参考:
https://briangordon.github.io/2014/08/the-skyline-problem.html
这是一道扫描线和其它结合的题:
区间问题, 如果按起点和终点排序解决不了, 那我们要想到扫描线. 而扫描线因为需要排序, 所以时间复杂度一般是O(nlogn).
这道题举个栗子:
[
[1, 3, 2],
[2, 4, 3],
[5, 6, 2]
]
就要拆成:
1 T 2
3 F 2
2 T 3
4 F 3
5 T 2
6 F 2
sweep line + heap 因为要删除, 所以c++要用heap
扫描问题的思路
- 事件往往是以区间的形式存在
- 区间两端代表事件的开始和结束
- 需要排序
import java.util.*;
public class Solution {
class HashHeap {
ArrayList heap;
String mode;
int size_t;
HashMap hash;
class Node {
public Integer id;
public Integer num;
Node(Node now) {
id = now.id;
num = now.num;
}
Node(Integer first, Integer second) {
this.id = first;
this.num = second;
}
}
public HashHeap(String mod) {
// TODO Auto-generated constructor stub
heap = new ArrayList();
mode = mod;
hash = new HashMap();
size_t = 0;
}
public int peek() {
return heap.get(0);
}
public int size() {
return size_t;
}
public Boolean isEmpty() {
return (heap.size() == 0);
}
int parent(int id) {
if (id == 0) {
return -1;
}
return (id - 1) / 2;
}
int lson(int id) {
return id * 2 + 1;
}
int rson(int id) {
return id * 2 + 2;
}
boolean comparesmall(int a, int b) {
if (a 0) {
siftdown(0);
}
} else {
hash.put(now, new Node(0, hashnow.num - 1));
}
return now;
}
public void add(int now) {
size_t++;
if (hash.containsKey(now)) {
Node hashnow = hash.get(now);
hash.put(now, new Node(hashnow.id, hashnow.num + 1));
} else {
heap.add(now);
hash.put(now, new Node(heap.size() - 1, 1));
}
siftup(heap.size() - 1);
}
public void delete(int now) {
size_t--;
Node hashnow = hash.get(now);
int id = hashnow.id;
int num = hashnow.num;
if (hashnow.num == 1) {
swap(id, heap.size() - 1);
hash.remove(now);
heap.remove(heap.size() - 1);
if (heap.size() > id) {
siftup(id);
siftdown(id);
}
} else {
hash.put(now, new Node(id, num - 1));
}
}
void siftup(int id) {
while (parent(id) > -1) {
int parentId = parent(id);
if (comparesmall(heap.get(parentId), heap.get(id)) == true) {
break;
} else {
swap(id, parentId);
}
id = parentId;
}
}
void siftdown(int id) {
while (lson(id) = heap.size()
|| (comparesmall(heap.get(leftId), heap.get(rightId)) == true)) {
son = leftId;
} else {
son = rightId;
}
if (comparesmall(heap.get(id), heap.get(son)) == true) {
break;
} else {
swap(id, son);
}
id = son;
}
}
}
class Edge {
int pos;
int height;
boolean isStart;
public Edge(int pos, int height, boolean isStart) {
this.pos = pos;
this.height = height;
this.isStart = isStart;
}
}
class EdgeComparator implements Comparator {
@Override
public int compare(Edge arg1, Edge arg2) {
Edge l1 = (Edge) arg1;
Edge l2 = (Edge) arg2;
if (l1.pos != l2.pos)
return compareInteger(l1.pos, l2.pos);
if (l1.isStart && l2.isStart) {
return compareInteger(l2.height, l1.height);
}
if (!l1.isStart && !l2.isStart) {
return compareInteger(l1.height, l2.height);
}
return l1.isStart ? -1 : 1;
}
int compareInteger(int a, int b) {
return a <= b ? -1 : 1;
}
}
List output(List res) {
List ans = new ArrayList();
if (res.size() > 0) {
int pre = res.get(0).get(0);
int height = res.get(0).get(1);
for (int i = 1; i 0) {
now.add(pre);
now.add(id);
now.add(height);
ans.add(now);
}
pre = id;
height = res.get(i).get(1);
}
}
return ans;
}
public List buildingOutline(int[][] buildings) {
// write your code here
List res = new ArrayList();
if (buildings == null || buildings.length == 0
|| buildings[0].length == 0) {
return res;
}
ArrayList edges = new ArrayList();
for (int[] building : buildings) {
Edge startEdge = new Edge(building[0], building[2], true);
edges.add(startEdge);
Edge endEdge = new Edge(building[1], building[2], false);
edges.add(endEdge);
}
Collections.sort(edges, new EdgeComparator());
HashHeap heap = new HashHeap("max");
List now = null;
for (Edge edge : edges) {
if (edge.isStart) {
if (heap.isEmpty() || edge.height > heap.peek()) {
now = new ArrayList(Arrays.asList(edge.pos,
edge.height));
res.add(now);
}
heap.add(edge.height);
} else {
heap.delete(edge.height);
if (heap.isEmpty() || edge.height > heap.peek()) {
if (heap.isEmpty()) {
now = new ArrayList(Arrays.asList(edge.pos, 0));
} else {
now = new ArrayList(Arrays.asList(edge.pos,
heap.peek()));
}
res.add(now);
}
}
}
return output(res);
}
}
Sliding Window Maximum
Given an array of n integer with duplicate number, and a moving window(size k), move the window at each iteration from the start of the array, find the maximum number inside the window at each moving.
Have you met this question in a real interview? Yes
Example
For array [1, 2, 7, 7, 8], moving window size k = 3. return [7, 7, 8]
At first the window is at the start of the array like this
[|1, 2, 7| ,7, 8] , return the maximum 7;
then the window move one step forward.
[1, |2, 7 ,7|, 8], return the maximum 7;
then the window move one step forward again.
[1, 2, |7, 7, 8|], return the maximum 8;
打个比方:
算法视频QQ_1603159172
queue: 是吃饭拉屎
stack: 是吃饭有毒吐出来
queue: 是上下都吃 上下都排…
这道题如果用heap做呢, 就是O(nlogk)的时间. 如果优化的话呢?
只能想线性数据结构啦
线性数据结构有啥呢? queue stack 和deque
那我们先试试stack?
比如: [ 1 2 7 5 8] 这组数据:
我们想找最大值, 那么就是如果从左到右, 如果遇到比一个数a大的, 就把a踢出去:
先:
1 1加入
1 2 2加入
2 1踢掉
2 7 7加入
7 2踢掉
7 5 5加入
这时候注意了, 5比7 小并不能说明5不会成为最大值, 因为stack先进后出, 所以有保留有效信息的能力, 所以我们先把5 存着, 然后:
7 5 8 8加入
7 8 5踢掉
8 7踢掉
单调栈升级为单调双端队列:
public class Solution {
/**
* @param nums: A list of integers.
* @return: The maximum number inside the window at each moving.
*/
void inQueue(Deque deque, int num) {
while (!deque.isEmpty() && deque.peekLast() < num) {
deque.pollLast();
}
deque.offer(num);
}
void outQueue(Deque deque, int num) {
if (deque.peekFirst() == num) {
deque.pollFirst();
}
}
public ArrayList maxSlidingWindow(int[] nums, int k) {
// write your code here
ArrayList ans = new ArrayList();
Deque deque = new ArrayDeque();
if (nums.length == 0) {
return ans;
}
for (int i = 0; i < k - 1; i++) {
inQueue(deque, nums[i]);
}
for(int i = k - 1; i < nums.length; i++) {
inQueue(deque, nums[i]);
ans.add(deque.peekFirst());
outQueue(deque, nums[i - k + 1]);
}
return ans;
}
}
- 二分法
- 按值二分,需要怎么二分性
- 扫描线
- 见到区间需要排序就可以考虑扫描线
- 双端队列
- 只用掌握sliding windows maximum这一道题目
- 维护一个候选可能的最大值集合
Debug的基本步骤
算法视频QQ_1603159172
为什么Debug一定要靠自己?
原因有四:
- 如果是别人给你指出你的程序哪儿错了,你自己不会有任何收获,你下一次依旧会犯同样的错误。
- 经过长时间努力Debug 获得的错误,印象更深刻。
- debug 能力是面试的考察范围。
- 锻炼Debug 能力能够提高自己的Bug Free的能力。
Debug的基本步骤
- 重新读一遍程序。按照自己当初想的思路,走一遍程序,看看程序是不是按照自己的思路在走。(因为很多时候,你写着写着就忘了很多事儿)这种方式是最有效最快速的 Debug 方式。
- 找到一个非常小非常小的可以让你的程序出错的数据。比如空数组,空串,1-5个数的数组,一个字符的字符串。
- 在程序的若干位置输出一些中间结果。比如排序之后输出一下,看看是不是真的按照你所想的顺序排序的。这样可以定位到程序出错的部分。
- 定位了出错的部分之后,查看自己的程序该部分的逻辑是否有错。
在第4步中,如果无法通过肉眼看出错误的部分,就一步步“模拟执行”程序,找出错误。
实在Debug 不出来怎么办?
如果你已经 Debug 了一整天,可以考虑向他人求助。