算法-知识点总结(持续更新中)

1. 位图法  bimap算法

应用场景:快速在大量的数中查找一个数、去除重复的数

优点:节省内存、查找速度快

  • 假设有1亿个数,要在1亿个数里面找到是否存在666这个数,那么可以构建一个bitmap结构。用一个数组存0、1,将这一亿个数存到这个数组,数组只存0、1,默认数组所有位置的值设置为0,将一亿个数的值作为数组的下标将对应数组的位置赋值为1,比如数字10则将array[10]=1,构建好bitmap结构后查找一个数是否存在只需要o1,比如查找666这个数是否存在这一亿个数里面,只需要判断array[666]是否为1即可。
  • 利用bitmap算法统计在线用户, 将用户id作为对应的位,1表示用户在线,0表示不在线。可以通过redis的setbit、getbit命令实现,例子 : setbit user_login 666 1。通过getbit user_login 666获取到userid为666的值为1即可知道该用户在线,通过bitcount user_login即可获取到在线用户数。

2. 布隆过滤器

应用场景:基于bitmap的扩展,可用于判断某个字符串一定不存在,但不能判断某个字符串一定存在

优点:节省内存空间,查找速度快

基于bitmap思想,由于字符串无法当作数组下标,因此可以通过多种hash算法,计算字符串的hash值,将获取到的hash值作为数组下标,之所以要用多种hash算法是为了提高准确性,避免过多hash碰撞导致准确率过低。假设3个hash算法,首先要构建一个数组,将每个要存放的字符串通过三种hash算法计算得到三个hash值,将hash值作为数组下标,将数组对应下标的值置为1。如果要查找字符串abcd是否一定不存在,则对abcd进行hash,假设算出hash值10、21、34三个值,通过判断数组这三个下标的值是否都为1即可判断一个字符串是否一定不存在。

为什么是只能判断一定不存在,不能判断一定存在?因为不管有几个hash算法,都会存在碰撞的情况,假设abcd字符串算出的三个hash值对应的数组位置之前已经都被置为1了,那么这个时候就不能说这三个位置都为1所以abcd一定存在。而且hash算法个数也不是越多越好,越多说明一个字符串占位越多,占用的空间和碰撞的机率也会提高。

基于以上的点有一条计算公式k表示hash算法个数,m表示数组容量,n表示插入的元素个数,k=m/n * ln2

redis同样也有布隆过滤器相关支持的插件,这里就不扩展,可参考掘金里这篇文章 https://juejin.im/post/5bc7446e5188255c791b3360

3. 区分稳定排序和非稳定排序的作用。

主要是可用于二次排序,在比如基于字段a排序之后,现在想按照b字段排序,如果想让按b排序后相同的还是保持之前按照a字段排序的顺序,就要稳定排序,比如职工排名先按照工资排名,然后再按照级别排名,稳定排序的情况下,同级别工资高的就会排在前面。

4. BF算法,暴风算法  Brute Force

字符串匹配算法,一种暴力匹配,abcdef字符串中判断是否存在子串cde,c和a,b比较不匹配则跳过,c匹配到再陆续判断de是否匹配,不匹配则继续接着遍历abcdef字符串去匹配。

5. 搜索算法

搜索算法有深度优先搜索算法和广度优先搜索算法,深度优先搜索适用查找所有解且空间占用较小,广度优先适用找到最优解(找到最优解的速度更快,最优解可以理解是找到最短路径,最高权重等等)。

  • 深度优先搜索:

  深度优先搜索相当于先一条路搜到死,得到答案或者搜到死胡同回溯(回溯参考下面解释),可以通过递归实现,假设需要从一个树里面找到所有符合条件的节点。如下,递归往一条路径搜,并将符合条件的节点记录下来,继续搜索到底,再函数返回搜其他位置,直到将所有的解找到。

search(Node node){ 
... 
search(node.left); 
search(node.right);
...
}

 

  • 广度优先搜索

前面提到广度优先搜索适用于更快找到最优解,为什么会更快?先看下深度优先是怎么搜的,会一条路径走到尽头,发现走错了,又回到原点往其他方向再搜,假设错误的路径特别深,那么会浪费很多搜索时间在一条错误的路径上,走得很长的步数。

广度优先则是通过多条路径一块搜索,每条路径都先走一步,然后走剩下可行每个路径再走一步,这样就可以更快找到最短路径。相当于一群人在找一个人,遇到分叉口的时候分开搜,会比一群人往一个方向搜到底,再回来再搜其他方向快。

为什么说广度搜索会耗费更多的空间,因为需要额外申请一个队列来存储要搜哪个节点。假设有个树结构,想要找到最短路径符合条件的节点,起始需要搜索左节点a右节点b,ab依次入队,循环遍历队列搜索,依据队列先进先出会先搜索a节点,搜索a节点的时候发现a节点也有左节点c右节点d,cd入队,接下来会轮到b出队,搜索b会有左右ef节点入队,按照这个规律,即可实现按照树一层一层结构往下搜,从而达到找到最短路径符合条件的节点。

 搜索涉及到两个知识点:

回溯:搜索过程中搜到一条错误路径,即无解的路径,回退到原点。实现回溯,可以通过栈或递归实现,栈本身是先进后出,往前走相当于进栈,走一步进一次栈,回退即为出栈。而递归则是搜索到尽头发现是死路后,函数return,会回退到上层调用从而达到回退的目的。递归其实底层原理也是即基于栈实现的,一次调用本方法相当一次进栈,从栈顶执行到栈底,所以如果递归深度太深,可能会触发栈溢出错误。

剪枝:搜索过程中将一些无用的路径进行排除,避免无效搜索。比如剪掉已经走过的路径,或者跟据实际场景和已搜索的路径情况,可以判断出某个路径已经无需再走则可以剪掉(比如经过某个点后,得知这个点之后无论怎么走都无法得到结果,那么经过这个点的所有路径都可以剪掉)。涉及到权重统计找权重最高之类比较常用,比如两个方向ab会汇聚到一个点c,之后都能到达d点,如果方向a这条路径到达c的权重累加没有b路径的高,那么a方向到达c点之后的所有路径都没必要走了,因为肯定没有b方向到达c点之后的路径分数高。(当然找最优解还是用广度搜索,但是如果有空间限制,那就用深度搜索并剪枝优化)。

6 马拉车算法 manacher

应用场景: 用于寻找字符串中的最长回文串,价值在于把时间复杂度优化到线性查找O(n)

对于一个字符串abcdcbn,先在字符中间加个特殊字符,如a#b#c#d#c#b#n,因为需要记录回文中心,如果回文中心是空隙没办法记录,字符间统一加上一个字符,不影响原有回文串。常规判断一个回文的方式是遍历字符串,然后往两边扩散比较字符,从而得到最长的回文串,而manacher算法则是通过回文串的特性,避免了一些没必要的扩散比较。

以字符串abcdcbn为例子,遍历到第5位的时候,第4位已知是长度为5的回文串中心,这个回文串的右边界为第6位,由于第5位小于回文串的右边界,根据回文特性,可得知第3位与第5位对称且第三位两边的字符是一致的,而第3位为中心的回文串长度为1,因此第5位无需两边扩散判断便可得知第5位的回文长度也为1。通过这样可以避免一些扩散判断,提供了查找速度。

需要注意的点:  1. 如果刚好处于已知回文串的右边界上是直接需要扩散判断的,因为右边界的右边的字符是未知的。

                        2. 以上面的例子为例,假设第三位的回文串长度是3,那么第5位的回文长度至少也是3及3以上,因为第6位就是边界,边界外的字符是未知的,这种情况也要再进行扩散判断。

7. 字典树 trie树

应用场景: 用于快速查找词库中是否有某个词,且支持前缀匹配搜索,自动补全控件就可以通过字典树实现。

字典树,即存储词库的词时,将词拆分成一个个字母组成树,一个字母一个节点,查找单词时,只要按照单词每个字母往节点搜索即可。如下所示

算法-知识点总结(持续更新中)_第1张图片

字典树的压缩存储,由于每个字母占用一个节点,会浪费空间,因此可以通过拆分单词,做为节点存储。如下,apple、apollo单词的存储

算法-知识点总结(持续更新中)_第2张图片

压缩存储的几个注意点:(1)拆分单词会导致拆出不是单词的前缀词,如上会拆出ap,需要在节点中标记是前缀不是单词。

                                          (2)  有新的单词进来,比如append,需要进行节点裂变,上面的ple节点要裂变成p节点和le节点。

                                          (3) 两个独立的树可以用一个空节点作为顶节点,连接成一个树。

                                          (4) 如果需要统计某个前缀有多少单词,可以在节点中存储子节点个数,空间换时间,就不用去遍历子节点统计。

8.分治法

分而治之将一个问题拆成若干个子问题,最终合并得出结果。

比如经典的top N问题,在10w个数中寻找top 1000,可以随机找一个数,将大于这个数的移到右边,小于这个数移动到左边,判断如果右边的数小于1000,则在左边随机选个数按照此法找出剩下的top 1000的数,最后将结果汇总。(top N问题更好的解决方法是通过构建堆来实现)。

如果有多台机器,数据可以分开存储,各自算出top 1000后汇总,再根据汇总的数据计算top 1000的数据,这也是分治法的思想,elasticSearch的排序搜索、得分计算按分数返回数据也是这种思想。

快排也是基于分治法的思想,将数据拆成一个个小块,让块与块之间有序,达到排序的效果。

9. 动态规划

应用场景:斐波那契数列

动态规划和分治法类似,都是将一个问题拆成若干个子问题处理,不同的是动态规划拆分的子问题互相不独立,一个子问题的计算依赖前一个子问题计算的结果,通过记住求过的解来节省时间。

斐波那契数列就可以通过动态规划实现,众所周知斐波那契数列是为0 1 1 2 3 5 8 13 21........,即fn=f(n-1)+f(n-2),常规做法我们可以通过递归实现获取第n位对应的数,如下

int fun(int n){
  if (n<=1){
    return n;  
  }
  return fn(n-1)+fn(n-2);
}

递归求法n值越大,栈的深度越深,可能会导致栈溢出。

先看这种求法是怎么处理的,求

f(4)=f(3)+fn(2)=fun(2)+fun(1)+fn(1)+fn(0)=fn(1)+fn(0)+f(1)+f(1)+fn(0)

算法中需要重复fn(2) f(1)的值,如果把这些值暂存起来,就可以用于下次计算,不用重复拆分计算。如下将每次计算的结果暂存起来用于下次计算。

int fun(int n) {
  int preNum=1;
  int prePreNum=0;
  int result=0;
  if(n<=1){
    return n;
  }          
  for(int i=2;i<=n;i++){
    result=preNum+prePreNum;
    prePreNum=preNum;
    preNum=result;
  }
  return result; 
}

可以简化成

int fun(n){
  int perNum=0;
  int nextNum=1;  
  while (n-- > 0){
    nextNum += perNum;
    perNum = nextNum-perNum;
  }
  return perNum;
}

10 并查集

并查集是一种树型的数据结构,用于处理一些不相交集合的合并及查询问题。

 

 

 

你可能感兴趣的:(数据结构和算法)