常见算法题

392 阅读14分钟

致敬左神

算法常见的计算公式

//num是不是2的某次方
(num&(num-1))==02的某次方

n << 1 | 1     //2n+1


eor&(~eor+1); //找到对最后一个1


top 几的问题

肯定是能想到用堆结构的,用小更推去做一个门槛,会极大的缩小空间复杂度, 比他大的才放进去,如果是实时要改的堆结构 八成是要自己手写的 加一个index表

最大公共子串问题

动态规划 只和自己斜上方的数值有关 +1就行 每个都会比较

用数组实现队列和栈

用数组实现栈比较简单,只需要一个size指针,一致放数据,push就下移,pop就上移,不用删数据。

用数组实现队列就比较又难度了,需要size和limit+两个指针控制,解耦,pullindex和putindex的关系, size比limit小,就可以随便加,如果等于了,就返回满了

实现最小栈

就是用两个栈来实现,push的时候把数字和最小站上小的放入最小栈上,pop的时候一起弹出就行了。

用队列实现栈

思路:队列是先进先出的 但是栈是后进后出的,所以用两个队列,把最后入队的元素导到队列前面去,加元素一律再空队列上加,加完合并,先来的导入到后来的后面。

image.png

用栈实现队列

思路,和上面的大同小异,就是把先来的放入到栈顶上,需要两个栈 注意两点,help栈为空且data栈不为空的情况下,可以倒数据,data栈一次新倒完所有数据

用递归求解数组上的最大值

一般过程就是线画图,解决三件事

  • 怎么划分,需要哪些参数
  • 什么时候条件不满足或者终止了
  • 怎么合并结果
public static int getMaxValue(int[] array,int l,int r){
		if (l==r){
			return array[l];
		}
		int mid=l+((r-l)>>1);
		int left = getMaxValue(array, l, mid);
		int right = getMaxValue(array, mid + 1, r);
		return Math.max(left,right);
		
	}

求递归时间复杂度

当子问题的规模是一致的时候,T(n)=aT(N/b)+O(N^d)

a是子递归调用了几次 b是子问题的规模 d是出来递归外的复杂度

这种类型的是把子问题都分成N/b的规模 然后调用了a次 +别的代码的复杂度

image.png

hashmap里面

基础类型都是值传递 非基础类型都是引用传递

image.png

treemap 是有序 如果不是基础类型,需要实现比较器

归并排序和随机快排

可以搞定小和 大和 左边有几个比他大的数,逆序对等

快排时间复杂度收敛于o(n*log(n)) 空间复杂度是根据高度 o(log(n))

延申用法

分区

快速排序的基础

大根堆 push操作是:只要数组没满,就可以加,但是要while循环比较改节点和父节点的大小,如果比父节点大,就交换,然后再比较,直到比较完成。 heapify操作 就是删除并有序操作,拿最后一个节点,顶替,第一个节点,如果去和左右子节点中大的比较,heapsize--

就是三步 1.0 如果是插入 就比较自己和父节点,那个大,如果当前大,就把指针上移, 如果是heapify操作:比较自己和左右两个子节点的大小 ,当然他们得事先存在,如果小于,就把他们中间大的那个拿上来,指针下移。

从下到上的前提是你需要有全部的数组,若只要大根堆,可以从后面向前面近发,heapify就可以了,复杂度比较低的原因是,再节点多的最后一排,反而是下沉最小的,所以比一个一个加要快

image.png

前缀树

代价很低,插入是n的 删除也是n的,查询也是n的 和前缀有关的都可以

image.png

桶排序

是有前提的。和样本数据大小有关的,, 把数据放入已经排好序的容器中,放完就有序了,一个萝卜一个坑

总结

image.png

image.png

image.png

在低样本的情况下 插入排序的时间和快速排序差不多

链表遍历

强调if 条件的时候使用 node.next!=null,但是你想要让全部节点都做一遍操作的话,就必须用node!=null, 可以不用知道头节点,就能删除已给节点,只要把后面节点的值赋给当前节点,然后指针跳到下一个就好了。

image.png

递归遍历二叉树

第一步: 假设左数。和右数都能返回你要的信息, 第二步: 分析可能性 以X为头节点的二叉树如何返回数据, 以和X无关; 和X有关两大类 ,需要哪些信息,如果左右两树,的条件不一样,取并集,保证左右两树返回的是一样的, 第三步: 然后具体讨论写代码的细节, 讨论节点为空的时候怎么返回这些数据,实在不好确定的时候,可以返回空,不过这样的话,在处理合并数据的时候每次都要讨论左右节点是否为空值, 接下来就好写了。 只要利用左右节点是数据把当前节点的数据也写好,就可以了。

先从容易的信息出发,再从与X无关的地方开始讨论,如果条件成立会怎么样

并查集 o1的复杂度

解决连通性问题的好手 必须掌握

贪心算法

每一道算法题都要当下最合适的思路 大概率是通过排序和堆完成的

图的算法难就难再表示图的结构题很多

需要转化成一种你会的就行了

暴力递归

就是从左向右写或者别的模式, 把该传的参数向里面传 比如index之类的这种模式是必须的,然后讨论在最后或者不符合条件的情况下怎么办, 如果是左右的,当完成当前index的处理 跳到下一个前 记得注意判断不符合或者越界等问题, 其实这类问题都是从后往前来的

如果想出现象递归链这种,后面的问题先处理 ,记得把递归放在处理前 和处理后的中间,还可以还原现场,

rest先于index,讨论, 第一步 basecase 第二步 枚举当前情况 可能for循环 可能一两种情况 第三步 递归 下一步 看if条件是否成立 。若要返回什么的 看if 特别是i+2什么的 是否越界 第四步 合并 返回值

动态规划

题型1:从左到右 内在思想0位置的值我要 于结果有啥联系 字符串都是这样的

递归依赖有限的子问题 记忆化搜索和动态规划没区别

自顶向下的动态规划 傻缓存

我有我就给你返回 我没用你在返回前给我加数据 如果下标越界白分支100 解决就是下移条件 别把basecase覆盖了 可以会越界的

经典动态规划

  • 把递归中的参数限制条件直接抄过来
  • 写dp数组 长度来自与遍历的取值范围,没发看出,就去题目里面找,如果不行就别写了
  • 根据递归的base case来赋值,哪里赋值0,哪里赋值1 但是0不用赋值
  • 根据可能性来写for循环 如果是两层 上下位置来更具条件来,里面的取值,和是否能= 看的是这个值你是否赋值过,或者别的地方去赋值
  • 接口看的是递归时的值。

看递归结构 确定方向 总下往上 或者左右 然后就算dp【x】【y】 for循环看仔细了 起步值不一定 填过的值不用填了。能不能等于就看有几层要填,这种凡是能减到0的 象rest步数这种大概率要等 直接把暴力递归的抄过来

process改 ---就是return的直接改赋值 去掉process里面的不变的参数 改成括号

image.png

滑动窗口和单调栈

基于滑动窗口,要把问题和范围简历单调性 ,比如 超过范围就失败 ,范围里面可以找出解

linklist里面存数组下标,不然的画 删数据就是根据下标来的

什么是滑动窗口? 一个数组 L到R 例如获取最大值 利用linkist 双端队列 尾巴进入 头出 且从头到尾巴是大到小的 遍历数组加入 如果尾巴的值比你小或者相等弹出 知道比你大的数出现,因为优先级index大的好, L向右移,查看队列的头部的index是不是过期的index 是就删了

套路:做题目是时候一定要规定好L和R的定义,R大概率是扩展到违规了,R要加到违规为止,更具题目的范围信息计算当前答案。 如果L移动变了 一定要把头删了

小技巧 查询数组l到r上的累加和

最小栈

可能出现的问题是 只要你用了!stack.isEmpty()就是要做循环处理,直到条件不满足,要么break , 倘若单单出现了if(!stack.isEmpty())一般后面要加条件 不然手动break


public static int[][] getNearLess(int[] arr) {
   int[][] res = new int[arr.length][2];
   Stack<List<Integer>> stack = new Stack<>();
   for (int i = 0; i < arr.length; i++) { // i -> arr[i] 进栈
      while (!stack.isEmpty() && arr[stack.peek().get(0)] > arr[i]) {
         List<Integer> popIs = stack.pop();
         int leftLessIndex = stack.isEmpty() ? -1 : stack.peek().get(stack.peek().size() - 1);
         for (Integer popi : popIs) {
            res[popi][0] = leftLessIndex;
            res[popi][1] = i;
         }
      }
      if (!stack.isEmpty() && arr[stack.peek().get(0)] == arr[i]) {
         stack.peek().add(Integer.valueOf(i));
      } else {
         ArrayList<Integer> list = new ArrayList<>();
         list.add(i);
         stack.push(list);
      }
   }
   while (!stack.isEmpty()) {
      List<Integer> popIs = stack.pop();
      int leftLessIndex = stack.isEmpty() ? -1 : stack.peek().get(stack.peek().size() - 1);
      for (Integer popi : popIs) {
         res[popi][0] = leftLessIndex;
         res[popi][1] = -1;
      }
   }
   return res;
}

image.png

这道题求的是 这个数到两侧最小值的范围,反正最大值可以直接求,让每一个数都当最小值试试,那么两侧的数就不能比他更小。

凡是题目中有求子数组的啥的,看看能不能从每一个数固定下来,去求答案,这样只需要遍历一遍, while结束后--- 当遍历完后栈中还有值,就用数组 长度当索引值

类似斐波那契数列 有严格递推的

适用于除了basecase以外的没有条件转移的象字符串转化问题就不行, 碰到这种条件不转移的,倒着象 fn=....+...

最强解法:设函数f 长度n 当第一种选择时候,还能变化的是谁 例如 当第一种选择确定是n-1也确定了,那么就是n-2 如果n-1不确定 那就是n-1 说白了就是哪个数可以带入f函数

小人爬楼梯问题 fn=f n-1+ f n-2 母牛生小牛问题 fn=f n-1+f n-3 -f n-10 (母牛可能会死)

零左必一问题和 贴瓷砖都是fn=f n-1+ f n-2

套的时候还是要看具体情况的 可能 f1 和f2 不同就要换系数 a x res[0][0]+b x res[1][0] a 和b是系数 也就是 1 和2 的数字 题目中有

image.png

蓄水池算法

有一个袋子会一直吐出球 按顺序 1,2... 有一个袋子只有10个位子怎么保证从1到K个球 入袋子都是等概率的? 需要一个随机函数 f(i) 输入i返回1到i等概率的数字 从1到10号球不管直接入 ,11号球 进入函数f(11) 如果返回的数小于等于10 进袋子,然后袋子中的10个球 f(10)决定谁出袋子。

等概率问题

问题 给你一个函数 f(x) 等概率的返回1~7 求等概率返回1到10 怎么搞呢 很简单嘛 新建函数 g(x)利用f(x)如果返回1到3 就返回0 4到6 返回1 7 重写来一次 且1到10 等于返回0到9然后加1 怎么返回0到9 可以返回的0和1 来4次可以来到0到15 9以外的就重写来

kmp 字符串子串问题

可以解决字符串旋转问题 在拼接一个字符串 然后去看是否为子串

bfprt 就是面试装逼用的

笔试不用刻意去求天选之子m

就是利用分区 找到第k个最小的值 如果k必这个大 就在右边分区 只分区一半 复杂度 on

以5个数一组 排好序 取中位数数组中的中位数,每次可以固定排除30%的数 因为他们是必定大于等于这个中位数或者小于他

Morris

用有限几个遍历完成数的遍历,真是牛逼 当然要循环必记录

image.png

manatrue

对回文的改变

线段树

当你要执对一批数统一进行操作的时候可以考虑的 时间为log n 级别的 形成类似数的数据结构,实现懒更新,就不用每次都跟新所有数,除法又有跟新来了.这样才能实现logn 都不用遍历完数组

子数组问题

  • 第一种想法 我以每一位置开头 答案是什么
  • 第二种想法 我有没一组位置结尾 答案是什么 所有答案都会杯包括

布隆过滤器

大概率有失误 ,如果系统不允许失误就不能用

需要知道样本量的范围和允许失误率 N和P 先然m就是数组的范围长度 越大,失误越少,为了控制空间 找一个最合适的 就用第一个公式

hash哈数的个数K 少了样本失误率高 多了也高,就这么大点空间,多样本也容易重复,用第二个公式来算 第三个是实际的M和K 因为很可能有小数

image.png

image.png

hash表的底层实现

先算hashcode 然后取模上系统预设值好的数组长度,当index一样的时候 用链表向后加,也可以用有序表 logn级别的 不用向单链表一样 on级别的 ,当超过一定的长度后,就会扩容 然后每个数在模上新的数组长度,一个N的数 resize的次数应该是 log2 n ,加上从新模的时间 平均一下应该是 (o(n)*log n)/n 是logn级别的 但是在使用上可以理解为o1 因为可以优化

资源优化技巧

image.png

image.png 第一种 比较简单 可以用位图 毕竟一个整形32位呢 我用一个位来记录0到2 32次方-1个数 看哪个没出现 第二种 3kb等于 3000*8位 约等于 240000位 /32 750个整数 取整512个整数数组 为了能让2 32次方除尽, 然后划分范围 第一个整数表示0到80000多个数 累加 看这个范围是否到了,然后去范围不到的地方缩小范围 第三种就是二分法 m=2的32次方/2 遍历文件 L去统计小于m的词频 R去统计大于m的词频 如果小于2的32次方/2 就去那一测统计

image.png 就是用位图 不过是用二个数字来表示 00表示一个数没来过 01 来过一次 10 来过2次 11来过好多次 然后就不变了

特别说明一下的是hash的一致性

比如前端nginx 计算请求的hashcode 模上一个数字 去哪个程序里面调,这样非常好,但是又一个很严重的问题 比如我要加一个机器的时候,所有的值都要重新去模上新的数字,这样我们是不能接收的,比如一两天的高分期 我们要多买好多的机器 过两天就没有了,但是可以利用hash函数的一致性算法,利用环的结构,给三台机器每台分配1000个字符串去散列到环上去瓜分自己的区域 且他们一定的负载均衡的,然后无论是加一台机器还是删除一台机器都及其的方便,例如删除一台机器,只要把上面的区交给下一个就好了,加一台机器就是在加等量 字符串去瓜分自己了领域这样必是均衡的,

文件太大怎么办

可以利用hash分到不同的机器再分到不同的文件

怎么找到大文件中的top100

用上面的方法不同的值会分到不同的文件 然后统计每个文件的词频 用大根堆 先把每个文件的第一个数放上去,弹出一个,然后对于文件的下面一个值补上去,是一个二维堆

怎么找到无序数组中的中位数? 40忆个数怎么搞,先用他给定的内存大小把2的32次方-1个数给划分完毕了,然后统计这里面的词频,到累加和超过20忆,那么这个数必是再这个范围里面 然后往里面找

搜索二叉树

image.png

有序表

定义: key能排序且 查询时间位log n级别的都是 且各种有序表性能差别不大

例如avl数和sb数 红黑树他们都是搜索二叉树的子集

avl树就是每个子树的左右节点的高度差<2 sb树 是每个叔叔节点下面的个数都要大于等于 他的侄子的结点数 ,其实是想保证两边最大不超过2倍+1 红黑树 最长链是红黑红黑交替 最短的是黑黑黑 所有也是为了不超过两倍