LeetCode刷题 - 基础算法小结

一.扫描线

什么是扫描线?

拿数飞机举例

思路一:暴力扫描

遍历每个时刻,检测每个时刻有多少个飞机

思路二:扫描线

不需要检测每一时刻,只需要检测起点或者终点的位置(交点变化的位置只有起点或者终点)

扫描线一般运用在图形上面,它和它的字面意思十分相似,就是一条线在整个图上扫来扫去,它一般被用来解决图形面积,周长等问题

总结

一般两种解法:使用 pq(heap) 或者将 start/end 分开进行扫描线

考点:sort常用comparator的写法,判断interval merge的边界条件

Arrays.sort(intervals,(a,b) -> a[0] - b[0]);
Arrays.sort(intervals,new Comparator<Interval>() {
  @override
  public int compare(Interval a,Interval b) {
    return a.start - b.start;
  }
}

PriorityQueue<int[]> heap = new PriorityQueue<>((a,b) -> a[1] - b[1]);
PriorityQueue<Interval> heap = new PriorityQueue<Interval> (intervals.length,new Comparator<Interval>() {
  @override
  public int compare(Interval a,Interval b) {
    return a.end - b.end;
  }
}

二.滑动窗口

什么是滑动窗口?

滑动窗口算法可以用以解决数组/字符串的子元素问题,它可以将嵌套的循环问题,转换为单循环问题,降低时间复杂度

如何识别滑动窗口?

1.连续的元素,比如string,subarray,LinkedList

2.min,max,longest,shortest,key word

滑动窗口基本类型

1.Easy,size fixed

窗口长度确定,比如max sum of size = k

2.Median,size可变,单限制条件

比如找到subarray sum比目标值大一点点

3.Median,size可变,双限制条件

比如longest substring with distinct character

4.Hard,size fixed,单限制条件

比如sliding window maximum,考察单调队列

套路

滑动窗口几乎都长得一样:给你一个数组,要求找到一个连续的子数组,满足某个条件

设左右两个指针为L和R,题目求的是最长,那么每一步都是:求L不动的情况下,R能移动到多远,满足条件越远越好,实在不行我们试试下一个L

每道题不一样的地方基本只在于怎么维护是否满足条件,可以分为3类

①维护子数组的和

②维护set / map / bool 数组

③维护一个有序数组 / 堆

模板

//本质仍然是two pointer,左边left,右边iterator(i)
public in lengthOfLongestSubstringKDistinct(String s,int k) {
  Map<Character,Integer> map = new HashMap<>();
  int left = 0,res = 0;
  for (int i = 0; i < s.length(); i++) {
    char cur = s.charAt(i);
    map.put(cur,map.getOrDefault(cur,0) + 1);  //当前遍历的 i 进入窗口
    while (map.size() > k) {
      char c = s.charAt(left);
      map.put(c,map.get(c) - 1);
      if (map.get(c) == 0) map.remove(c);
      left++;  //当窗口不符合条件时 left 持续退出窗口
    }
    res = Math.max(res,i - left + 1);  //现在窗口valid了,我们计算结果
  }
  return res;
}

总结 

1.滑动窗口套路模板时间复杂度一般为O(n)

2.一般string使用map作为window,如果说明了只有小写字母也可以用int[26]

3.多重限制条件的压轴题需要考虑是否为单调队列

4.字母类还可以暴力尝试26个字母,比如1个unique,2个unique,然后内部模板

5.Exact(k)可以转换为atMost(k) - atMost(k - 1)

三.前缀和

前缀和是算法题中比较实用的一种技巧,当算法题的背景是整数型数组且出现 “ 子数组和 ” 或者 “ 连续的子数组 ” 既可以考虑使用前缀和来求解会得到不错的效果

Prifix sum相关考题从频率最高的17题可以发现常考的知识点有:

1.2sum系列

2.rangeSum

3.sliding window

4.monotonic queue

5.少量扫描线

6.rolling hash内部也使用了prefix sum

2sum系列

2sum系列在整个leetcode中大概有50多道,基本思路是求两数字之和等于target,两数之差,两数的余数相同,或者两数和为0(如果只有1,-1则为二者数量相同)

public int[] twoSum(int[] nums, int target) {
  Map<Integer,Integer> map = new HashMap<>();
  for (int i = 0; i < nums.length; i++) {
    int diff = target - nums[i];
    if (map.containsKey(diff)) return new int[]{map.get(diff),i};
    map.put(nums[i], i);
  }
  return null;
}

总结

1. 2sum系列

2. rangeSum

3. sliding window

4. monotonic queue

考察最多的prefix还是two sum (和,差,余数,0),如果是range sum的话,大部分情况会提升到2维。对于sliding window还是单调队列,取决于是否有负数。滑动窗口的左缩进是不论subarray sum大小直接缩进,对于全正数是ok的,因为缩进一定会让sum减小,有负数的情况就不可以这样,需要根据subarray sum减小来改变左缩进,也就是单调队列保持最小起点,因为差值sum[i] - sum[queue.peekFirst()] 就是subarray的和,左缩进不一定是一步一步走的,是根据总window sum的减小来走的,会走到下一个最小的起点

很多题目包括greedy比如gas station类似的题目,也使用了prefix sum的思路来对一路上的gas求和,这里的prefix sum就比较广义了,题目过多不再详述

四.二分查找法

二分查找法主要是解决在“一堆数中找出指定的数”这类问题

想要用二分查找法,这“一堆数”必须有以下特征:

存储在数组中

有序排列

所以如果是链表存储的,就无法在其上应用二分查找法了

模板

start = 0;end = len - 1;
while(start + 1 < end){
mid = start + (end - start)/2;
if(nums[mid]<target)
  start = mid;
else
  end = mid;
}
 
 
start = 0;end = len;
while(start < end){
mid = start + (end - start)/2;
if(nums[mid]<target)
  start = mid + 1;
else
  end = mid;
}
 
 
start = 0;end = len - 1;
while(start <= end){
mid = start + (end - start)/2;
if(nums[mid] < target)
  start = mid + 1;
else
  end = mid - 1;
}

五.分治法

设计思想:将一个难以直接解决的大问题,分割成一些规模较小的相同问题,以便各个击破,分而治之

分治策略:对于一个规模为n的问题,若该问题可以容易地解决(比如说规模n较小)则直接解决,否则将其分解为k个规模较小的子问题,这些子问题互相独立且与原问题形式相同,递归地解这些子问题,然后将各子问题的解合并得到原问题的解

1.把大问题分成2个或者2个以上的小问题

2.解决小问题并且把结果合并

3.hybrid algorithm 实际代码中我们不会等base case小到1或者0,当relative small的时候我们就会去计算,比如quick sort,size < 10我们就会去用insertion sort做而不是继续divide

4.redundant call需要避免,Fibnacci就是一个例子,虽然是divide and conquer但是有很多的重复call,所以这里memorization

步骤

1.分解原问题为若干子问题,这些子问题是原问题的规模最小的实例

2.解决这些子问题,递归地求解这些子问题,当子问题的规模足够小,就可以直接求解

3.合并这些子问题的解成原问题的解

优势在可以很效率地解决一些困难问题,一般分解为2或4个问题,最后的时间复杂度和logn有关

总结

这是一个很万能的解法,但是很多时候并不一定好用(比如有pq的情况下,面试可能pq就给过了,dc的话知识follow up要小心第一时间把自己给绕进去,给一个workable的solution是最重要的)。另外常常用于nlogn的优化,一般给avg(N)并且可以根据pivot优化,quickSelect

六.贪心算法

基本概念

所谓贪心算法是指,在对问题求解时,总是做出在当前看来最好的选择。也就是说,不从整体最优加以考虑,他所做出的仅是在某种意义上的局部最优解

贪心算法没有固定的算法框架,算法设计的关键是贪心策略的选择。必须注意的是,贪心算法不是对所有问题都能得到整体最优解,选择的贪心策略必须具备无后效性,即某个状态以后的过程不会影响以前的状态,至于当前状态有关

所以对所采用的贪心策略一定要仔细分析其是否满足无后效性(无后效性是指如果在某个阶段上过程的状态已知,则从此阶段以后过程的发展变化仅与此阶段的状态有关,而与过程在此阶段以前的阶段所经历过的状态无关。利用动态规划方法求解多阶段决策过程问题,过程的状态必须具备无后效性)

基本思路

1.建立数学模型来描述问题

2.把求解的问题分成若干个子问题

3.对每一子问题求解,得到子问题的局部最优解

4.把子问题的解局部最优解合成原来解问题的一个解

适用的问题

贪心策略适用的前提是:局部最优策略能导致产生全局最优解

实际上,贪心算法适用的情况很少。一般,对一个问题分析是否适用于贪心算法,可以先选择该问题下的几个实际数据进行分析,就可做出判断

与动态规划对比

1.基本思想

贪心算法:并不从整体最优上加以考虑,它所做的选择只是在某种意义上的局部最优解

动态规划:将待求解的问题分解成若干个子问题,先求解子问题,然后从这些子问题的解得到原问题的解

2.基本要素

贪心算法:最优子结构性质和贪心选择性质

动态规划:最优子结构性质和重叠子问题性质

3.区别

共同点:两者都具有最优子结构的性质

不同点:动态规划算法中,每步所做的选择往往依赖于相关子问题的解,因而只有在解出相关子问题时才能做出选择。而贪心算法,仅在当前状态下做出最好选择,即局部最优选择,然后再去解做出这个选择后产生的相应的子问题

七.回溯算法

result = []
def backtrack(路径,选择列表):
  if 满足结束条件:
    result.add(路径)
    return

  for 选择 in 选择列表:
    做选择
    backtrack(路径,选择列表)
    撤销选择

八.双指针

所谓双指针,指的是在遍历对象的过程中,不是普通的使用单个指针进行访问,而是使用两个相同方向或者相反方向的指针进行扫描,从而达到相应的目的

换言之,双指针法充分使用了数组有序这一特征,从而在某些情况下能够简化一些运算

注:这里的指针,并非专指c中指针的概念,而是指索引,游标或指针,可迭代对象等

给定一个有序递增数组,在数组中找到满足条件的两个数,使得这两个数的和为某一给定的值。如果有多对数,只输出一对即可

常见的题型有哪些?

①快慢指针(两个指针步长不同)

②左右端点指针(两个指针分别指向头尾,并往中间移动,步长不确定)

③固定间距指针(两个指针间距相同,步长相同)

双指针一直是程序员面试中的一个必须准备的主题,面试中双指针出现的次数比较多,主要由于在工作中指针经常用到,指针问题能够直接反映面试者的基础知识,代码能力和思维逻辑,因此双指针的问题必须掌握

解决双指针问题的三种常用思想:

①左右指针:需要两个指针,一个指向开头,一个指向末尾,然后向中间遍历,直到满足条件或者两个指针相遇

②快慢指针:需要两个指针,开始都指向开头,根据条件不同,快指针走得快,慢指针走的慢,直到满足条件或者快指针走到结尾

③后序指针:常规指针操作是从前向后遍历,对于合并和替换类型题,防止之前的数据被覆盖,双指针需从后向前遍历

记忆口诀:左右指针中间夹,快慢指针走到头,后序指针往回走

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值