算法与数据结构笔记

428 阅读14分钟

复杂度分析

平均情况时间复杂度也就是加权的平均时间复杂度,把每种结果出现的概率考虑进去,综合分析

// 全局变量,大小为 10 的数组 array,长度 len,下标 i。
int array[] = new int[10]; 
int len = 10;
int i = 0;

// 往数组中添加一个元素
void add(int element) {
   if (i >= len) { // 数组空间不够了
     // 重新申请一个 2 倍大小的数组空间
     int new_array[] = new int[len*2];
     // 把原来 array 数组中的数据依次 copy 到 new_array
     for (int j = 0; j < len; ++j) {
       new_array[j] = array[j];
     }
     // new_array 复制给 array,array 现在大小就是 2 倍 len 了
     array = new_array;
     len = 2 * len;
   }
   // 将 element 放到下标为 i 的位置,下标 i 加一
   array[i] = element;
   ++i;
}


时间复杂度是指n次调用add函数的时间复杂度

最好的情况下,这n次的调用每次都是i < len的情况下,所以调用n次add函数,每次都是直接插入,所以最后时间复杂度也就是O(1)

最差的情况就是没次调用add函数,结果每次都是刚好i>=len的时候,所以每次调用都是翻倍,假设第一次调用add函数的时候,数组里边元素为 X个,则第一次调用需要X次,第二次调用add函数需要2X次,第三次调用需要4X次,则形成了一个等差数列,所以最后为O(n)

平均情况,就是前len-1次调用,每次都是1,最后一次则为len,可以看到基本上每次O(len)肯定有len次O(1),所以根据均摊分析,就是O(1)

另外一种可以看下总的次数,第一次是1,1,1 。。。一直len-1,然后是len-1次,然后再次1,1,1... 到2len-1,然后是2len-1, 可以看到一共有n/2len次不是1的操作,最后可以得到2的lgn除以n,所以最后为O(1)

递归

递归需要满足二个条件 1,大问题可以划分为同样的小问题 2,存在初始条件

递归代码的空间复杂度是叠加的。

归并排序

1, 利用哨兵简化代码,

快速排序

快速排序感觉这个名字起的不好,每次都感觉好像是特别快的排序方法,结果时间久了,根本就不知道什么是快排,总想着是什么特别快的排序方法,我看不如叫选择哨兵的归并排序。

问题: 现在你有 10 个接口访问日志文件,每个日志文件大小约 300MB,每个文件里的日志都是按照时间戳从小到大排序的。你你希望将这 10 个较小的日志文件,合并为 1 个日志文件,,合并之后的日志仍然按照时间戳从小到大排列。如果处理上述排序任务的机器内存只有 1GB,你有什么好的解决方法?

利用归并思想,先分割开,在内存中排序,然后再合并。取日志文件的最小值和最大值的中间值。

桶排序

最主要的是可以划分分m个桶,并且划分之后,桶的顺序已经是确定好的了,而不用重新去再去排序桶的顺序。 比如0-10一个桶,11-20一个桶,这就是已经排好序的桶了。

计数排序

最精妙的就是通过数组中的计数,然后计算出数据在数组中的值。

二分查找

问题: 假设我们有1000万个整数数据,每个数据占8个字节,如何设计数据结构和算法,快速判断某个整数是否出现在这1000 万数据中? 我们希望这个功能不要占用太多的内存空间,最多不要超过 100MB内存,如何实现?

这里是1MB = 1024KB = 1024 * 1024B = 1000000B 所以1000万数据,每个占8个字节差不多就是80MB

这里是我比较疑惑的是让设计数据结构,完全没搞明白让设计什么样的数据结构,最后想明白了,原始就是一个数组

其他的就很简单了,使用一个数组,占据内存差不多80MB,然后采用排序,最后采用二分查找。

二分查找的效率是非常高,比如查找42亿条数据,也就只需要32次,也就是说2的32次方等于42亿,效率惊人。

变形的二分查找算法

tip: 算法实现如果看不懂,是别人写的太难,太难懂,还是自己的水平太菜,根本看不出别人这样写的好处呢?

ip地址可以转化为32位的整形数值

许多问题看到的可能不是需要解决的问题,需要通过一定的技巧来转化,比如如果排序ip地址,如果单纯的排序ip地址,肯定是不好排序的,但是如果转成32位整数,就很自然的可以排序了,这就是一种转化。

跳表

跳表就是为链表建立一个索引,当在链表查找某个元素的时候,先遍历索引,缩小范围,然后再到链表中查找。

如何存储用户密码?

1,hash 算法最重要的是保证单向,不能通过hash之后的值,来反推到正确的原始值,也就是在存储密码的时候,不能通过hash后的密码推算出正确的原始密码

使用hash算法,hash密码,如果密码过于简单,可以通过加salt,其实就是增加密码的复杂度,然后再hash,尽量避免可以通过暴力破解。

二叉树

二叉树可以用链式存储,也可以用数组存储,不过最适合用数组存储的是完全二叉树。

二叉树的前序中序和后序遍历是相对于中间的根节点来说的,也就是前序遍历是先访问根节点,再访问左子树,后访问右子树。

前序后序中序的时间复杂度都为O(n)

问题: 给定一组数组,比如1,3, 5, 6, 9 ,10, 可以构建出多少种不同的二叉树。

如果是数组,一般就是完全二叉树,也就是数组的排列有几种,根据排列组合原理,也就是n!

红黑树

红黑树是一种近似平衡的二叉查找树

出现的原因是为了防止查找二叉树,在不断插入的情况下,出现查找性能退化的问题。

使用递归分析时间复杂度

问题: 1 个细胞的生命周期是 3小时,1小时分裂一次,求n个小时后,容器内有多少个细胞,时间复杂度是多少?

这里需要明确的是当一个细胞经过一次分裂之后,这两个细胞还能存活的时间是不一样的,比如刚开始的时候,容器内只有一个细胞的时候,当经过一个小时之后,分裂成两个细胞后,其中一个细胞只能存活2个小时了,另外一个细胞可以存活三个小时。

其他的都比较简单。

堆排序

1, 什么是堆 堆是一个完全二叉树,且每个节点都大于或等于左右子树的值(大顶堆)

堆排序就是特殊完全二叉树的排序,不要被堆迷惑。

2, 堆排序的使用

2.1 合并小文件 假设有100个小文件,每个小文件大小是100M,并且这些小文件是有序的字符串,希望将这些小文件合并成一个有序的大文件。

利用堆排序重排序的时候的时间复杂度是O(lgN)的特性。

2.2 求数据中top K 的数据?

其实就是维护一个大小为k的堆,然后不断插入,不断比较插入的数据和堆顶元素,到最后就得到了一个大小为k的堆,就是需要的数据。

图有两种存储方法,邻接矩阵存储方法和邻接表存储方法

图广度搜索和深度搜索的时间复杂度都是O(E), E 表示边的数量。 图广度搜索和深度搜索的空间复杂度都是O(V), V 表示图中定点的数量。

字符串匹配算法

主串:被匹配的字符串,大小为n 模式串:匹配的字符串,大小为m, m<=n

1, BF 算法

这个算法很简单,就是遍历主串,每次从主串中取出m个字符和模式串进行比较,如果匹配,则返回,如果没有匹配,则继续取下一个大小为m的字符串。

2,RK 算法

RK 算法就是对BF算法的一种改进,改进的地方在于不需要直接对比主串中取到的m个字符和模式串直接对比,而是通过hash算法,来比对两者之间的hash值,来达到减少时间复杂度的目的。

3, BM 算法

性能是没有极限的,需要不断的寻找新的算法,也就是方法去优化性能,找到性能瓶颈,去不断的优化性能。

BM算法其实就是在当发现主串的字符和模式串的字符不匹配的时候,尽量的多排除一些肯定不会匹配的情况,多往前移动几位。

3.1 坏字符规则

逆向,通过对比模式串从后往前的对比主串,当发现不匹配的时候,就是出现坏字符,也就是主串中不匹配的字符被称作坏字符。

可以看到下图中,a就是主串中的坏字符,这时候把a对应模式串中的d对应的下标记为si,同时在模式串中发现坏字符a在模式串中的位置下标为Xi,则这个时候就可以把模式串向前移动Si-Xi位,例如图中移动的位置也就是2-0 = 2。

3.2 好后缀原则

这个原则其实很简单,就是当在主串中找到长度为n1的字符串和模式串中后几位匹配的时候,根据主串中已经匹配的n1的字符串再和模式串中查找前缀和n1重合的最大字符串,然后直接移动到模式串和主串中n1的字符串重合的地方就可以了,

如图:

KMP 算法

这个算法其实原理也是一样的,当主串和模式串进行匹配的时候,当遇到主串的某个字符和模式串的某个字符不一致的时候,想办法尽量把模式串尽量的多往前移动几位,尽量减少匹配的次数。

最长可匹配前缀子串,就是一个字符串中,从0开始的最长的长度为s1的子串可以匹配字符串中最后s1长度的子串 比如字符串absab 则最长可匹配前缀子串为ab

KMP算法中最重要的就是根据模式串构建出一个数组,该数组的下标是字符串的长度,数组的值是最长可匹配前缀子串的最后一个字符在模式串中的下标。

next数组的构建其实也很简单,就是一个不断的迭代的过程,当模式串中最长可匹配前缀子串的下一位字符和主串中的下一位不相等的时候,迭代查找模式串中次长的可匹配前缀子串的下一位和主串中的下一位字符进行比较,如果还不相等,继续迭代。

当然如果当模式串中最长可匹配前缀子串的下一位字符和主串中的下一位相等的时候,就很简单了,没什么好说的。

trie 树

trie树的本质是利用字符串之间的公共前缀,将重复的前缀合并起来,也就是形成一个节点,每个字符串都可以通过搜索树来获得。

字符是和int类型想通的,其实最后所有的东西,在计算机中都会变成1和0

AC自动机算法

多模式匹配,就是在用多个模式串和主串进行匹配,一次扫描,多次匹配。

ac自动机算法就是对trie树模式匹配算法,进行的优化,把可以省略的不必要的步骤给省去,从而提高性能。

ac自动机的算法其实也很简单,首先根据模式串构建出trie树,然后根据trie树构建出fail指针,当遍历的时候,先走完一个树的分支,查找出所有的以当前的字符为结尾的匹配的模式串,当走完一个分支后,开始另外一个分支,或者不用走完当前分支,只有下面的字符不匹配就可以去另外的分支。

算法之贪心思想

适合用贪心思想的一般涉及到期望值和限制值,也就是在问题中涉及到限制值和期望值解决的问题。

假设我们有 n 个区间,区间的起始端点和结束端点分别是结束端点分别是 [l1, r1],[l2, r2],[l3, r3],……,[ln, rn]。我们从这 n 个区中选出一部分区间,这部分区间满足两两不相交(端点相交的情况不算相交),最多能选出多少个区间?

通过转换,转换成贪心算法模型,或者通过抽象,不管怎么样,只要可以通过转化为贪心算法模型的,可以优先尝试贪心算法。

算法之分治思想

分治就是大而化小,分而治之。

算法之回溯思想

就是枚举出所有的类型,最后根据结果选择需要的类型。

动态规划

动态规划,就是把问题拆分为n个状态,然后再n-1状态和n状态之间找到一个通道,也就是一个转移公式,来实现从n-1的状态到n的状态的转移。

  1. 如何量化两个字符串的相似度? 根据编辑距离,也就是一个字符串如果通过增加,删除变成另一个字符串所用的次数。

拓扑排序

就是一个互相依赖的关系,比如一般是要先有车,才能开车,这就是一个依赖关系。

1, kahn 算法

这个算法很简单,首先找到有向图中入度为0的顶点,也就是不依赖其他的顶点,然后删除,同时也更新其他顶点的入度,然后再循环查找入度为0的顶点。

如何去除网络爬虫中的重复url。

1, 位图

就是把对应的数字通过一些转化方法用内存中的一位进行表示。

2,布隆过滤器

布隆过滤器就是通过n个hash函数对数字进行处理,然后通过存储中的一系列位图的位置来确定某个数字是否存在。

垃圾短信过滤和电话拦截

1, 黑名单过滤。

2, 定义一堆垃圾短信的规则,根据规则来过滤。

3,基于概率统计来拦截。

简单的音乐推荐系统

1,找到和你兴趣相同的用户,把他的歌单推荐给你。

2,找到和你你听过的歌特征类似的歌曲推荐给你。

B+树,mysql数据库的索引如何实现的?

1, 思考的过程是最重要的,也就是思想是最重要的,而不是正确的结果是什么。

B+树就是对二叉树的改进,让二叉树支持可以区间查询。

如何实现游戏中的寻路功能?

A* 算法利用在游戏中不需要寻找到最佳的路径,可以通过改进Dijkstra 算法,来找到一条比较合理的路径。

如何在海量数据中查找某个数据?

数据库的索引就是类似于书籍的目录。