数据结构与算法之美要点摘要

写这篇文章是为了总结记录《数据结构与算法》课程中的技术要点。

一、数组

  • 在数组中插入的时候,如果要插入指定的下标,会使数组的其他元素后移,此时插入到指定下标的时间复杂度是O(n),如果不是非必要不一定要插入到指定下标,可以插入末尾,或者直接替换下标数据。
  • 删除数组中元素的时候,会使数组产生复制操作,此时时间复杂度是O(n),为了节省时间,可以批量删除,降低时间复杂度。
  • ArrayList:内部是数组,默认数组容量是10,如果达到数据达到数组容量,扩容为元素组3倍容量,扩容会产生数组复制,是比较耗时的操作。

二、链表

  • 链表做插入操作的时候有可能元素为空,如果每次插入都要判断首节点是否为null,需要多一次逻辑判断,此时可以用哨兵模式,head节点就设置为空,这样每次插入的时候,就插到尾部下一个即可。

三、递归

  • 递归的核心是“写出递推公式,找到终止条件”,引用一段《数据结构与算法之美》的例子说明:

假如这里有 n 个台阶,每次你可以跨 1 个台阶或者 2 个台阶,请问走这 n 个台阶有多少种走法?如果有 7 个台阶,你可以 2,2,2,1 这样子上去,也可以 1,2,1,1,2 这样子上去,总之走法有很多,那如何用编程求得总共有多少种走法呢?

我们仔细想下,实际上,可以根据第一步的走法把所有走法分为两类,第一步走一个台阶,第二类是第一步走了两个台阶,所以n个台阶的走法就相当于走了一个台阶后n-1个台阶的走法,加上走了两个台阶后,n-2个台阶的走法,用公式表示就是:f(n) = f(n-1)+f(n-2)

有了递推公司,递归代码基本就完成了一半。我们再看下终止条件。当有一个台阶时,我们不需要再继续递归,就只有一种走法,所以f(1)=1。这个递归终止条件足够么,我们用比较小的n=2,n=3来试验一下。

n=2时,f(2)=f(1)+f(0),如果递归终止条件只有一个n=1,f(2)就无法求解了,因为没有0个台阶这种情况,所以我们把f(2)=2作为终止条件,表示走两个台阶有两种走法,一步走完或者分两步走。所以递归的终止条件有两个:f(1)=1,f(2)=2。这个时候你可以拿n=3,n=4来验证一下,看是否符合条件。我们把递归终止条件和刚刚得到的递推公式放到一起就是这样的:
f(1) = 1;
f(2) = 2;
f(n) = f(n-1)+f(n-2)

有了这个公式,我们转化成递归代码就简单多了。最终的递归代码是这样的:
int f(int n) {
if (n == 1) return 1;
if (n == 2) return 2;
return f(n-1) + f(n-2);
}

  • 总结:计算机擅长做重复的事情,人脑喜欢做平铺直叙的运算,我们在做递归算法的时候,喜欢脑子中跟着一层层往下调用,其实这是进入了思维误区,人脑其实根本没有办法把“递”和“归”的过程一步步想清楚。
  • 很多时候我们理解起来困难,是因为自己给自己制造了这种思维障碍,正确的思维方式应该是怎样的呢:
    如果问题A可以被分解为问题B、C、D,你可以假设B、C、D问题已经解决,在此基础上思考问题A与B、C、D问题的联系,思考怎么解决,不需要一层层往下思考子问题与子子问题之间的关系,屏蔽掉递归细节,这样理解起来就简单多了。
    因此,编写递归代码的关键是,只要遇到递归,我们就把它抽象成一个递推公式,不用想一层层的调用关系,不要试图用人脑去分解递归的每个步骤。
警惕递归代码重复计算

上面的递归例子,我们用图解分解一下,如下:
数据结构与算法之美要点摘要_第1张图片
图中可以看到f(3)被计算了3次,f(4)被计算了两次。
为了避免重复计算,我们可以把计算过的值存到hashMap中,当遇到之前计算过的值,直接取出即可,改造之前代码如下:

public int f(int n) {
  if (n == 1) return 1;
  if (n == 2) return 2;
  
  // hasSolvedList 可以理解成一个 Map,key 是 n,value 是 f(n)
  if (hasSolvedList.containsKey(n)) {
    return hasSovledList.get(n);
  }
  
  int ret = f(n-1) + f(n-2);
  hasSovledList.put(n, ret);
  return ret;
}
递归调试方式
  • 打印日志发现,递归值。
  • 结合条件断点进行调试。
递归调用细节问题解决
  • 避免递归太深:可以设置一个阈值。

参考文献

专栏:数据结构与算法之美:递归

四、排序

排序算法 时间复杂度 是否基于比较
冒泡、插入、选择 O(n2)
快排、归并 O(nlogn)
桶、计数、基数 O(n)
  • 在java的List和Array的排序算法中,默认使用的是在插入排序上优化过的Tim排序,在32个元素内进行插入排序,32个元素以上进行归并排序,时间复杂度O(n log n),最坏情况下的时间复杂度是O(n²)。

五、HashMap

  • 初始大小是16,扩容为原容量的二倍大小,当元素个数>负载因子*容量,进行扩容,扩容要进行数据移动,也是比较耗时的操作。

六、有序集合

  • redis设计与实现

七、二叉树

  • AVL树:最先被发明的平衡二叉树是AVL树,有一篇很不错的关于AVL树的文章
  • 红黑树:想了解红黑树要在了解二叉树和二三树的基础上,否则很难理解:从2-3树到红黑树

八、堆排序的应用:

  • 堆的排序应用有:优先级队列、利用堆求topK、利用堆求中位数。

九、深度和广度优先搜索

  • 搜索的最基础就是广度与深度优先搜索算法,此处有一篇文章,请收好:深度和广度优先搜索

十、字符串匹配

  • 在java中,字符串匹配是用的最简单的方式,一个个比较。
  • 关于搜索匹配,最高效的算法是BM算法:在linux中的grep命令就是用BM算法实现

十一、AC自动机

当你有上千万的敏感词,需要过滤,你输入的一句话,你能一个个地把敏感词与你输入的语句进行匹配么,此时就需要用到AC自动机。
其实后来楼主想了下,如果不懂AC自动机,用散列其实效率也不会下降非常厉害。

十二、高端的算法思想

  • 分治算法、动态规划
  • 最短路径:地图软件是如何计算出行的最短路径的
  • 位图:实现网页爬虫中的url去重功能
  • 布隆过滤器:基于BitMap,可以在非常大批量的数据中统计哪些被使用过,性能很高

你可能感兴趣的:(算法)