非开发科班生遇到喜欢问场景的面试官可太难了,基本秒跪。来临时抱抱佛脚,总结下常见场景面试题,部分是面试中遇到的,部分是网上搜集的。
TOP K问题
10亿个数据中找出最大的10000个?——最小堆
最小堆法
- 先拿10000个数建堆
- 然后逐个添加剩余元素
- 如果大于堆顶的数(10000中最小的),将这个数替换堆顶,并调整结构使之仍然是一个最小堆
- 遍历完后,堆中的10000个数就是所需的最大的10000个。
- 复杂度分析:时间复杂度是O(mlogm),算法的时间复杂度为O(nmlogm)(n为10亿,m为10000)。
优化方法
- 如果内存受限:可以直接在内存总使用Hash方法将数据划分成n个partition,每个partition交给一个线程处理,线程的处理逻辑可以采用最小堆,最后一个线程将结果归并。
进一步优化:
该方法存在一个瓶颈会明显影响效率,即数据倾斜。每个线程的处理速度可能不同,快的线程需要等待慢的线程,最终的处理速度取决于慢的线程。而针对此问题
解决的方法是,将数据划分成c×n个partition(c>1),每个线程处理完当前partition后主动取下一个partition继续处理,直到所有数据处理完毕,最后由一个线程进行归并。 - 如果含较多重复值:先用hash / 依图法去重,可大大节省运算量
有几台机器存储着几亿淘宝搜索日志,你只有一台 2g 的电脑,怎么选出搜索热度最高的十个?
针对top k类文本问题,通常比较好的方案是【分治+trie树/hash+小顶堆】,即先将数据集按照hash方法分解成多个小数据集,然后使用trie树或者hash统计每个小数据集中的query词频,之后用小顶堆求出每个数据集中出频率最高的前K个数,最后在所有top K中求出最终的top K。
-
拆分成n多个文件:以首字母区分,不同首字母放在不同文件,长度仍过长的继续按照次首字母进行拆分。这样一来,每个文件的每个数据长度相同且首字母尾字母也相同,就能保证数据被独立的分为了n个文件,且各个文件中不存在关键词的交集。
-
分别词频统计:对于每个文件,使用
hash或者Trie树
进行进行词频统计 -
小顶堆排序:依次处理每个文件,并逐渐更新最大的十个词
提取某日访问次数最多的IP地址
将去进行 %1024
取模散列到1024个文件中**(划分子文件方法之一)**
采用hashmap对其进行次数统计 最后用排序算法进行排序。
数据太多,内存不够,找到最热门的K个数据
首先对数据进行预处理,采用hash表统计次数 然后再排序
海量数据排序、压缩问题?
重要方法——位图法 Bitmap
- 位图的基本概念:用一个位(bit)来标记某个数据的存放状态。例如,有
{2, 4, 5, 6, 67, 5}
这么几个整数,我维护一个00…0000(共67位)
的0/1
字符串,1表示该索引(=数据值)处存在数,0则表示不存在。 - 应用:位图法可以用于海量数据排序,海量数据去重,海量数据压缩
- 优点:针对于稠密的数据集可以很好体现出位图法的优势,内存消耗少,速度较快
- 缺点:不适用于稀疏数据集,比如我们有一个长度为10的序列,最大值为20亿,则构造位串的内存消耗将相当大250M,而实际却只需要40个字节,此外位图法还存在可读性差等缺点。
非重复排序
假设我们有一个不重复的整型序列{n1, n2, ... ,nn}
,假设最大值为nmax
,则我们可以维护一个长度为nmax
的位串。主要过程就是2步:
- 第一遍遍历整个序列,将出现的数字在位串(java中可以用数组实现)中对应的位置置为1;
- 第二遍遍历位图,依次输出值为1的位对应的数字,这些1所在的位串中的位置的索引代表序列数据,1出现的先后位置则代表序列的大小关系。
重复排序——保留 / 不保留重复值
- 保留重复值:同上,只是子串中不只存在
0/1
,实际数量为多少,则值为多少.输出时,值为多少则输出多少遍 - 不保留:略。
数据压缩
前提:数据中存在大量的冗余值
基本思路就是使用某个子串存储原数据中的海量值
如何用redis存储统计1亿用户一年的登陆情况,并快速检索任意时间窗口内的活跃用户数量
在redis 2.2.0版本之后,新增了一个位图数据。redis单独对bitmap提供了一套命令。可以对任意一位进行设置和读取。所以可以在位图中使用1表示活跃。
bitmap的核心命令:
-
SETBIT
:设置某位为1
语法:SETBIT key offset value
例如:setbit abc 5 1 ----> 00001 setbit abc 2 1 ----> 00101
-
GETBIT
:获取某位的值
语法:GETBIT key offset
例如:getbit abc 5 ----> 1 getbit abc 1 ----> 0
bitmap的其他命令还有bitcount,bitpos,bitop
等命令。都是对位的操作。
- 获取某一天id为88000的用户是否活跃:
getbit 2020-01-01 88000 [时间复杂度为O(1)]
- 统计某一天的所有的活跃用户数:
bitcount 2019-01-01 [时间复杂度为O(N)]
-统计某一段时间内的活跃用户数,需要用到bitop命令。这个命令提供四种位运算,AND(与),(OR)或,XOR(亦或),NOT(非)。我们可以对某一段时间内的所有key进行OR(或)操作,或操作出来的位图是0的就代表这段时间内一次都没有登陆的用户。那只要我们求出1的个数就可以了
以下例子求出了2019-01-01到2019-01-05这段时间内的活跃用户数。
bitop or result 2019-01-01 2019-01-02 2019-01-03 2019-01-04 2019-01-05 [时间复杂度为O(N)]
bitcount result
从时间复杂度上说,无论是统计某一天,还是统计一段时间。在实际测试时,基本上都是秒出的。符合我们的预期。
海量文本去重——simhash法
资源 vs 请求问题
如果一个外卖配送单子要发布,现在有200个骑手都想要接这一单,如何保证只有一个骑手接到单子?
-
如果只是单机,采用volatile关键字修饰该订单采用CAS操作对其进行乐观锁操作。
- volatile保证可见性
- CAS保证原子性
-
采用redis,zookeeper分布式锁加锁。
-
消息队列 实现幂等接口
如果只是单机,采用volatile关键字修饰该订单采用CAS操作对其进行乐观锁操作。
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicInteger;
public class sub1 {
static int USER_NUM=10000;//模拟并发数,也就是有这么多个用户一起抢单
static CountDownLatch countDownLatch=new CountDownLatch(USER_NUM);//用这个来实现所有线程在同一起跑线上
// AtomicInteger.getAndDecrement底层就是CAS
static volatile AtomicInteger number=new AtomicInteger(10);//有限的资源数
static Ticket ticket=new Ticket();
public static void main(String[] args) throws InterruptedException{
Buy();
}
public static void Buy() throws InterruptedException {
for (int i = 0; i < USER_NUM; i++) {
new Thread(new MyThread()).start();
if(i==USER_NUM){
Thread.currentThread().sleep(1000);
}
countDownLatch.countDown();
}
Thread.currentThread().sleep(2000);
}
public static class MyThread implements Runnable{
@Override
public void run() {
try{
countDownLatch.await();
}catch (InterruptedException e){
e.printStackTrace();
}
ticket.sale();
}
}
public static class Ticket {
public void sale(){
int n=number.getAndDecrement();
if(n>0){
System.out.println(Thread.currentThread().getName() + "抢到了第 " + n + " 张票");
}
}
}
}
多个微信用户抢红包
类似于秒杀系统
- 数据库加乐观锁、悲观锁
- 在逻辑处理界面加分布式锁
- 消息队列
❓1000个任务分给10个人做
全局队列,每一个人都从一个队列中取
分成10个队列对应每一个人
设计类问题
你如何设计一个消息队列?
回答一:开放式回答,比较粗糙
- 首先这个 mq 得支持可伸缩性吧
就是需要的时候快速扩容,就可以增加吞吐量和容量,那怎么搞?设计个分布式的系统呗,参照一下 kafka 的设计理念,broker -> topic -> partition,每个 partition 放一个机器,就存一部分数据。如果现在资源不够了,简单啊,给 topic 增加 partition,然后做数据迁移,增加机器,不就可以存放更多数据,提供更高的吞吐量了?- 其次你得考虑一下这个 mq 的数据要不要落地磁盘吧?
那肯定要了,落磁盘才能保证别进程挂了数据就丢了。那落磁盘的时候怎么落啊?顺序写,这样就没有磁盘随机读写的寻址开销,磁盘顺序读写的性能是很高的,这就是 kafka 的思路。- 其次你考虑一下你的 mq 的可用性啊?
这个事儿,具体参考之前可用性那个环节讲解的 kafka 的高可用保障机制。多副本 -> leader & follower -> broker 挂了重新选举 leader 即可对外服务。- 能不能支持数据 0 丢失啊?
可以的,参考我们之前说的那个 kafka 数据零丢失方案。
回答二:流程式回答,比较详细
参考文章
其他
怎么判别淘宝刷单?怎么检验手段效果?
商家角度:
- 营业额远高于历史平均数据
- 营业额远高于行业平均数据
- 下单的用户很多均为存在异常行为被监控的账户
顾客角度(账号维度):判断是否存在异常行为
- 单次交易行为:是否精准搜索、货比三家、页面停留时间等
- 近期购物成功率:远高于历史时期平均购物成功率
- 是否可能为垫付:短时间支付宝账户内收到与下单金额相同的金额
- 下单的店铺很多均为存在异常行为被监控的账户
商家与顾客:
- 存在相近或者共同的网络环境:如ip、wifi等
如何把一个文件快速下发到 100w 个服务器?
采用p2p网络形式,比如树状形式,网状形式,单个节点既可以从其他节点接收服务又可以向其他节点提供服务。
对于树状传递,在100W台服务器这种量级上,可能存在两个问题:
- 如果树上的某一个节点坏掉了,那么从这个节点往下的所有服务器全部宕机。
- 如果树中的某条路径,传递时间太长了(网络中,两个节点间的传递速度受很多因素的影响,可能相差成百上千倍),使得传递效率退化。
改进:100W台服务器相当于有100W个节点的连通图。那么我们可以在图里生成多颗不同的生成树,在进行数据下发时,同时按照多颗不同的树去传递数据。这样就可以避免某个中间节点宕机,影响到后续的节点。同时这种传递方法实际上是一种依据时间的广度优先遍历,可以避免某条路径过长造成的效率低下。
如何实现两个线程交替打印
第一种方法:加锁,并使用volatile boolean类型的标志来控制线程执行
public class sub2{
static volatile int num=1;
static volatile boolean flag=true;
public static void main(String[] args) {
Object obj=new Object();
new Thread(()->{
while(num<=100){
if(flag){
System.out.println(Thread.currentThread().getName() + ":" + num);
num++;
flag=false;
}
}
},"A").start();
new Thread(()->{
while(num<=100){
if(!flag){
System.out.println(Thread.currentThread().getName() + ":" + num);
num++;
flag=true;
}
}
},"B").start();
}
}
第二种方法:加锁,一个线程执行完就等待并唤醒另一个线程
public class sub2{
static volatile int num=1;
public static void main(String[] args) {
Object obj=new Object();
new Thread(()->{
while(num<=100){
synchronized (obj){
obj.notifyAll();
System.out.println(Thread.currentThread().getName() + ":" + num);
num++;
try {
obj.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
},"A").start();
new Thread(()->{
while(num<=100) {
synchronized (obj){
obj.notifyAll();
System.out.println(Thread.currentThread().getName() + ":" + num);
num++;
try {
obj.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
},"B").start();
}
}