算法学习

189 阅读11分钟

1.删除数组中的重复元素(原地删除,不能额外开辟空间)

如果数组是有序的,那么利用快慢指针操作。如果数组没有序,那么先排序,再利用快慢指针操作。

2.快排的空间复杂度是?

3.去除字符串中出现次数最少的字符,不改变原字符串的顺序。

4.给你一个大文件(40亿个字符串),统计文件中中出现次数最多的字符串

答:每一个字符串做哈希算法,得到一个哈希值,之后再%100。等到0 - 99 之间的数据,这样就把40亿个字符串划分成100份(100个小文件),相同的字符串必定在同一个小文件(根据哈希函数的性质,划分后小文件大小都差不多)。之后再用哈希map,key 为 字符串, value 为出现的次数,统计每个小文件,出现最多的字符串是哪个。综合100个小文件的统计数据,出现最多的字符串就为大文件出现最多的。

5.哈希表为什么增删改查的复杂度都是O(1)

常见的哈希表是桶挂链表的结构
答:
1、删,改,查可以认为是O(1),主要是利用哈希函数的性质,类似问题2的文件分块,虽然理论上时间复杂度是O(n),但是可以固定链表的大小为K,当加入的数据大于K的时候,就触发扩容
2、扩容的理论时间复杂度还是O(n)但是,利用离线扩容技术,可以在实际应用中,把时间复杂度降低至O(1),所谓离线扩容,就是在用户不知情的情况下,当发现用户使用的这个哈希表将要满的时候,就静默扩容。 扩容完成之后,再把之前使用的哈希表copy 过去。(这个离线扩容是在其他线程进行的吗?)

6.设计randomPool 结构

设计一种结构,该结构有如下三种功能
1.insert(key) 将某个key加入该结构,不能重复
2.delete(key) 将原本在结构中的key 删除
3.getRandom() 等概率返回结构中的任一key
提示:利用两个哈希表实现。以字符串为例
insert实现:首先,用一个int整形size 记录插入的个数,每插入一个字符串(str),size++,在Map1 中,以str 为key,size为value,在map2中,以size为key,str为value。
delete实现:每一次删除,比如删除"abc",通过map1找到"abc"的位置,通过map2和size(最后一个数)找到最后一个字符串假如是"fgh",之后利用"fgh"替换原来的"abc"(map1和map2同时替换),接着size--。这样的在getRandom的时候,就能避免过多空判断
getRandom()实现:利用系统的random函数,随机生成size范围内的数据,之后从map2中取。

7.布隆过滤器

用于处理超级大量的黑白名单问题。
比如,爬虫爬数据,已经爬过的网站进入白名单,无需再爬。如果已经爬过来100亿个网站,那么,如何快速设计一种数据结构,判断下一个需要爬的网站是已经爬过了的?
如果直接把100亿个URL放入内存,建立哈希表,这是样需要大概640G内存,是非常不现实的。
我们可以设计一个位图(可以用C语言的位域),通过位信息来表示是否是在黑名单,这样节省内存。我们创建一个具有m位信息的位图(数组),之后利用url1,通过多个(K个)哈希函数算出的结果再%m得到K的数,再把这K个数对应的位图信息设置为1。这样就完成了url1的存储。接下来的100亿个url同理,一同放在刚刚创建的位图中。
如果需要查询urlx是否在黑名单中,通过刚刚的操作获取到K个值,如果K个值全部为1,那么证明urlx是黑名单,如果有一个值不为1,那么就不是黑名单。这里m和k的值的大小设计很重要。
这种做法有小概率误判。但是只会发生原本是白名单的,结果是黑名单。不会发生原本是黑名单的,结果被放出来,变成白名单,而且这个方案不能有删除行为,就是已经是黑名单了就没法T出黑名单。
假设这个方案的失误率为p,那么p的值和m,k有关系,而且不是线性的。有具体的推导公式,这里略

8.一致性哈希(被誉为谷歌改变世界的三大技术之一??)

问题:假设有m台数据服务器,一般情况下,服务器分流都是通过哈希值取模(m)得到数据具体需要存储的服务器编号,但是这种做法会导致新增存储服务器比较麻烦(因为取模数变了,变成m+1),需要把所有的数据都重新计算一次。
针对这个问题,我们可以不取模,而是把数据服务器通过哈希算法,得到唯一的标识符,之后插入一个哈希环(0 --- 2^64 -1)。每当有数据需要存储的时候,计算数据的哈希值之后和服务器的哈希值对比,存储在最近的服务器即可。如果需要新增服务器,只需要在环上插入一台服务器,之后把离他最近的服务器数据迁移。这样就不需要每一台服务器迁移,只需要迁移一台服务器的数据。大大地降低扩容成本。但是这样或导致负载不均衡
如果引入虚拟节点技术,解决负载均衡问题,但是不又回到之前所有的,新增一个台服务器N,由于虚拟节点技术的存在,导致N会向之前所有的服务器要数据,新增一台服务器的成本又变大了。

哈希key的选择,一般要选择种类比较多的类型作为key。这样才能保证根据Key去查询的时候,高频key,中频key和低频的key 能够均匀低分配到每一台服务器。这样每一台服务器就能负载均衡。
比如选取人名为key,因为人名种类多,合适。再比如选取国家为key,但是"中国"和"美国"为高频key,高频key 过少,会导致负载不均衡。
服务器的逻辑端和数据端要分开对待。

9. 并查集

过于复杂

10.KMP

KMP算法解决的问题:
字符串str1 是否包含字符串str2,如果包含,返回str2在str1的位置,请用时间复杂度为O(n)的算法完成 和暴力对比方法的流程其实是一样的,只不过,KMP算法是对这一过程的加速。 针对str2,计算出最长前缀和最长后缀的匹配长度(假设为K)。当str1匹配到第一个和str2不相同的字符串的时候,常规解法是从i+1的位置重新匹配,而运用了KMP加速的话,可以从i+K的位置开始匹配。从而节省匹配时间。

11.工程上对排序的改进

1、小规模用插入排序改进(针对快排和归并) 2、

12.链表

技巧
1.快慢指针
2.额外数据结构记录(哈希表,栈)
题目
1、打印有序链表的公共部分(注意是有序)
答:双指针,谁小谁移动,相等同时移动。相等就是公共部分
2.逆序打印链表
答:用栈
3.判断一个链表是否是回文链表
答:用栈,对比。或者用快慢指针(无需额外空间),把后半部分翻转之后在对比。
4.将单向链表按照某个值划分成左边小,中间等,右边大的形式
答:1.类似于快排,把链表放到一个数组之后把排好之后再串回去。2.利用6个指针(以数10为例,分别是小于10的指针头和尾,等于10的指针头和尾,大于10的指针头和尾),把链表归类之后再串回去。
5.复制包含随机指针的单向链表
答:利用哈希表或者在原链表操作。

有环链表

A.判断单向链表是否有环,并返回第一个入环节点

额外空间方法: 利用哈希表,遍历链表,逐个放入哈希表,发现第一个哈希表中已经存在的数据,那么链表有环并且是第一个入环点。
不适用额外空间方法: 利用快慢指针,快指针每次走两步,慢指针每次走一步。如果有环,快慢指针肯定相遇。
当快快慢指针相遇的时候,快指针回到链表头,之后快指针每次都一步,慢指针也每次走一步,他们再次相遇的节点就是第一个入环点。(这个结论很魔性!!!)

B.判断无环单向链表是否相交,并返回第一个交点

答:先找到链表L1的长度len1,和链表L2的长度len2。如果尾部节点不相等,那么两个链表必定不相交,如果尾部节点相等,那么必定相交。
如果相交,如果len1 == len2 那么同时遍历L1和L2,第一个相等的节点就是链表L1,L2第一个交点,如len1 != len2 假设len1 - len2 = k(k>0,第一个链表较长),那么L1就从第k个节点开始遍历,L2从头节点开始遍历,第一个相等的节点就是他们的交点。(这个方法比LeetCode的简单多了)

C、给定两个可能有环或者无环的单向链表,如果两个链表相交,请返回相交的第一个节点,如果不相交,返回NULL

1.第一个情况是两个都无环,和上题B判断逻辑一样
2.第二种情况是一个有环,一个无环,那么这两个链表不可能相交
3.第三种情况,两个都有环:
利用"A"求得L1的入环点a1,和L2的入环点a2。如果a1 == a2 那么两个链表必定相交。并且交点就是入环点。
如果a1 != a2 ,那么L1继续往下遍历。直到再次遍历a1,这个期间,如果没有经过a2,那么这两个链表就是独立的链表,互不相交,如果经过a2节点,那么这两个链表就是相交链表。三种情况如下图:

WechatIMG169.jpeg

二叉树

遍历

前序,中序和后序遍历都是从二叉树的递归序中等到

void f(node * n){
    if (n == null) return;
    //1.前序遍历在这里打印
    f(n->left);
    //2.中序遍历在这里打印
    f(n-right);
    //3.后序遍历在这里打印
}

也可以用栈来实现非递归遍历二叉树,其中后序遍历需要用到两个栈结构。稍微复杂点。 针对前序遍历:
1.头节点进栈
2.节点出栈,打印节点
3.节点右子树进栈,节点左子树进栈。
4.重复2.
中序和后续的用栈的遍历方案,请自行研究

求二叉树的最大宽度

在通过队列实现二叉的层序遍历中,每次加入队列的时候,通过一个Map,统计加入的叶子节点是在哪一层的。之后遍历的时候,就能根据这个Map 来获取得到遍历时候,这个节点属于哪一层,这样就能统计每一层的节点数,从而求得哪一层拥有最大的节点。需要注意的是,头结点入队列的时候,默认就是第一层,之后他的左右子树入队列的时候,就第二层,这样以此类推的。 下面这个方法通过层次遍历,生成一个节点,层数(key,value)的哈希表,用来统计二叉树的层数和每一层拥有节点的数量

levelMap.put(head, 1);

int curLevel = 1;

int curLevelNodes =0;

int max = Integer.MIN VALUE:

while(!queue.isEmpty()) {

    Node cur = queue.poll();

    int curNodeLevel = levelMap.get(cur);

    if( curNodeLevel == curLevel) {

        curLevelNodest+;

    }else {

        max = Math.max(max, curLevelNodes);

        curLevelt++;

        curLevelNodes = 1;

}

if(cur.left !=null) {

    leve1Map.put(cur.left, curNodeLevel+1);

    queue.add(cur.left);

}

if(cur.right !=null) {

    levelMap.put(cur.left, curNodeLevel+1):

    queue.add(cur.right);

}
判断一棵树是否是搜索二叉树

前序遍历,遍历出来的结果递增

判断一棵树是否是完全二叉树

1.层序遍历,如果发现一个节点只有右子树,没有左子树,那么这棵树就不是完全二叉树,直接返回false
2.在1的基础下如果发现,一个节点,只有左子树,那么接下来的所有节点,都必须是叶子节点,如果不是,直接返回false
完全二叉树的定义:
所谓完全二叉树,就是一颗二叉树,从左往右是依次变满的过程,即任何节点左右子尽量是双全的,不行就让左子全,右子不全,否则自己就是叶节点

树形DP

大量的二叉树问题,都可以用树形递归解决。