秋招面试题---拼多多

****拼多多是要求手写代码的****

拼多多算法题

1.生活中用到栈和队列的例子

对于队列很好想到关于任何排队的都是,但是栈就不是特别好想,唯一想到的就是火车调度。


2.n个节点的树可能的高度。最高和最低的高度

首先是不是二叉树,如果不是当然最高就是n,最低就是2

是二叉树的话,最高就n,最小的情况是完全二叉树,高度是[logn]+1


3.BST树的高度

BST(二叉查找树/二叉搜索树)


4.问了一个订单数据表(uid,orderId,time,moblie)每天2000w条,要存1年。应该怎么存?

https://www.jianshu.com/p/84da619ce203

(1)订单数据划分

由于是订单表,一般最近一个月的是比较常查询的,因此可以将订单数据划分成两大类型:分别是热数据和冷数据。

  • 热数据:1个月内的订单数据,查询实时性较高;
  • 冷数据A:1个月 ~ 3个月前的订单数据,查询频率不高;
  • 冷数据B:3个月到一年的订单数据,几乎不会查询,只有偶尔的查询需求;

可能这里有个疑惑为什么要将冷数据分成两类,因为根据实际场景需求,用户基本不会去查看3个月以后的数据,如果将这部分数据还存储在db中,那么成本会非常高,而且也不便于维护。另外如果真遇到有个别用户需要查看3个月以后的订单信息,可以让用户走离线数据查看。

对于这三类数据的存储,目前规划如下:

  • 热数据: 使用mysql进行存储,当然需要分库分表;
  • 冷数据A: 对于这类数据可以存储在ES中,利用搜索引擎的特性基本上也可以做到比较快的查询;
  • 冷数据B: 对于这类不经常查询的数据,可以存放到Hive

(2)MySql 如何分库分表

  • 按业务拆分(和本问题无关,但是实际当中需要将数据库按照业务拆分到不同的库中存储)

  • 分库与分表

我们知道每台机器无论配置多么好它都有自身的物理上限,所以当我们应用已经能触及或远远超出单台机器的某个上限的时候,我们惟有寻找别的机器的帮助或者继续升级的我们的硬件,但常见的方案还是通过添加更多的机器来共同承担压力。

(1)分表策略

我们假设预估单个库需要分配100个表满足我们的业务需求,我们可以简单的取模计算出订单在哪个子表中,例如: order_id % 100

(2)分库实现策略

数据库分表能够解决单表数据量很大的时候数据查询的效率问题,但是无法给数据库的并发操作带来效率上的提高,因为分表的实质还是在一个数据库上进行的操作,很容易受数据库IO性能的限制。因此,如何将数据库IO性能的问题平均分配出来,很显然将数据进行分库操作可以很好地解决单台数据库的性能问题。分库策略与分表策略的实现很相似,最简单的都是可以通过取模的方式进行。

例如:order_id % 库容量。

(3)分库分表结合使用策略

数据库分表可以解决单表海量数据的查询性能问题,分库可以解决单台数据库的并发访问压力问题。有时候,我们需要同时考虑这两个问题,因此,我们既需要对单表进行分表操作,还需要进行分库操作,以便同时扩展系统的并发处理能力和提升单表的查询性能,就是我们使用到的分库分表。

如果使用分库分表结合使用的话,不能简单进行order_id 取模操作,需要加一个中间变量用来打散到不同的子表

中间变量 = shard key %(库数量*单个库的表数量);

库序号 = 取整(中间变量/单个库的表数量);

表序号 = 中间变量%单个库的表数量。

例如:数据库有10个,每一个库中有100个数据表,用户的order_id=1001,按照上述的路由策略,可得:

中间变量=1001%(10*100)=1;

库序号=取整(1/100)=0;

表序号=1%100=1

这样的话,对于order_id=1001,将被路由到第1个数据库的第2个表中(索引0 代表1,依次类推)。

(3)整体架构设计

  • 写操作还是很简单的,就通过分区分表策略决定写到哪个库哪个表,但是都是写到Mysql中的。
  • 读操作需要根据订单id先判断出读的数据是热数据还是冷数据,再去相应数据库读取数据。订单id通常使用:商户所在地区号+时间戳+随机数组成,这样就可以根据时间戳选取查询数据库。
  • Mysql中冷数据要定期的迁移到冷数据库中。

5.NIO

(1)IO与NIO区别

  1. IO是面向流的,NIO是面向缓冲的;
  2. IO是阻塞的,NIO是非阻塞的;
  3. IO是单线程的,NIO 是通过选择器来模拟多线程的;

NIO在基础的IO流上发展处新的特点,分别是:内存映射技术,字符及编码,非阻塞I/O和文件锁定

(2)内存映射

因为磁盘读取是很慢的,所以提出了缓存,但是对于大文件,内存是不能存下的,因此提出了内存映射文件:允许我们创建和修改那些因为太大而不能放入内存的文件此时就可以假定整个文件都放在内存中,而且可以完全把它当成非常大的数组来访问(随机访问)。

这个功能主要是为了提高大文件的读写速度而设计的。内存映射文件(memory-mappedfile)能让你创建和修改那些大到无法读入内存的文件。有了内存映射文件,你就可以认为文件已经全部读进了内存,然后把它当成一个非常大的数组来访问了。将文件的一段区域映射到内存中,比传统的文件处理速度要快很多。内存映射文件它虽然最终也是要从磁盘读取数据,但是它并不需要将数据读取到OS内核缓冲区,而是直接将进程的用户私有地址空间中的一部分区域与文件对象建立起映射关系,就好像直接从内存中读、写文件一样,速度当然快了。

实现:

1首先,从文件中获得一个通道(channel)通道是用于磁盘文件的一种抽象,它使我们可以访问诸如内存映射文件加锁机制(下文缓冲区数据结构部分将提到)文件间快速数据传递等操作系统特性。

FileChannel channel = FileChannel.open(filename);

2然后,通过调用FileChannel类的map方法进行内存映射,map方法从这个通道中获得一个MappedByteBuffer对象(ByteBuffer的子类)

MappedByteBuffer buffer = channel.map(FileChannel.MapMode.READ_ONLY, 0, length);

可以指定想要映射的文件区域与映射模式,支持的模式有3种:

  • FileChannel.MapMode.READ_ONLY:产生只读缓冲区,对缓冲区的写入操作将导致ReadOnlyBufferException;
  • FileChannel.MapMode.READ_WRITE:产生可写缓冲区,任何修改将在某个时刻写回到文件中,而这某个时刻是依赖OS的,其他映射同一个文件的程序可能不能立即看到这些修改,多个程序同时进行文件映射的确切行为是依赖于系统的,但是它是线程安全的
  • FileChannel.MapMode.PRIVATE:产生可写缓冲区,但任何修改是缓冲区私有的,不会回到文件中。。。

3、一旦有了缓冲区,就可以使用ByteBuffer类和Buffer超类的方法来读写数据

缓冲区支持顺序随机数据访问:

顺序:有一个可以通过get和put操作来移动的位置

1 while(buffer.hasRemaining()){
2     byte b = buffer.get(); //get当前位置
3     ...
4 }

随机:可以按内存数组索引访问

1 for(int i=0; i<buffer.limit(); i++){
2     byte b = buffer.get(i); //这个get能指定索引
3     ...
4 }

(3)字符及编码

  • 编码方案:

编码方案定义了如何把字符编码的序列表达为字节序列。字符编码的数值不需要与编码字节相同,也不需要是一对一或一对多个的关系。原则上,把字符集编码和解码近似视为对象的序列化和反序列化

大部分的操作系统在I/O与文件存储方面仍是以字节为导向的,所以无论使用何种编码,Unicode或其他编码,在字节序列和字符集编码之间仍需要进行转化。

由java.nio.charset包组成的类满足了这个需求。这不是Java平台第一次处理字符集编码,但是它是最系统、最全面、以及最灵活的解决方式。

(4)非阻塞IO

五种IO模型举例引用levin

1.阻塞I/O模型

老李去火车站买票,排队三天买到一张退票。

耗费:在车站吃喝拉撒睡 3天,其他事一件没干。

2.非阻塞I/O模型

老李去火车站买票,隔12小时去火车站问有没有退票,三天后买到一张票。

耗费:往返车站6次,路上6小时,其他时间做了好多事。

3.I/O复用模型

老李去火车站买票,委托黄牛,然后每隔6小时电话黄牛询问,黄牛三天内买到票,然后老李去火车站交钱领票。

耗费:往返车站2次,路上2小时,黄牛手续费100元,打电话17次

  • epoll

老李去火车站买票,委托黄牛,黄牛买到后即通知老李去领,然后老李去火车站交钱领票。

耗费:往返车站2次,路上2小时,黄牛手续费100元,无需打电话

4.信号驱动I/O模型

老李去火车站买票,给售票员留下电话,有票后,售票员电话通知老李,然后老李去火车站交钱领票。

耗费:往返车站2次,路上2小时,免黄牛费100元,无需打电话

5.异步I/O模型

老李去火车站买票,给售票员留下电话,有票后,售票员电话通知老李并快递送票上门。

耗费:往返车站1次,路上1小时,免黄牛费100元,无需打电话

 

典型的非阻塞IO模型一般如下:

while(true){
    data = socket.read();
    if(data!= error){
        处理数据
        break;
    }
}

但是对于非阻塞IO就有一个非常严重的问题,在while循环中需要不断地去询问内核数据是否就绪,这样会导致CPU占用率非常高,因此一般情况下很少使用while循环这种方式来读取数据。所以这就不得不说到下面这个概念–多路复用IO模型。

如果一个I/O流进来,我们就开启一个进程处理这个I/O流。那么假设现在有一百万个I/O流进来,那我们就需要开启一百万个进程一一对应处理这些I/O流(——这就是传统意义下的多进程并发处理)。思考一下,一百万个进程,你的CPU占有率会多高,这个实现方式及其的不合理。所以人们提出了I/O多路复用这个模型,一个线程,通过记录I/O流的状态来同时管理多个I/O,可以提高服务器的吞吐能力。(相当于黄牛)

(5)文件锁定

NIO中的文件通道(FileChannel)在读写数据的时候主 要使用了阻塞模式,它不能支持非阻塞模式的读写,而且FileChannel的对象是不能够直接实例化的, 他的实例只能通过getChannel()从一个打开的文件对象上边读取(RandomAccessFile、 FileInputStream、FileOutputStream),并且通过调用getChannel()方法返回一个 Channel对象去连接同一个文件,也就是针对同一个文件进行读写操作。
文件锁的出现解决了很多Java应用程序和非Java程序之间共享文件数据的问题,在以前的JDK版本中,没有文件锁机制使得Java应用程序和其他非Java进程程序之间不能够针对同一个文件共享 数据,有可能造成很多问题,JDK1.4里面有了FileChannel,它的锁机制使得文件能够针对很多非 Java应用程序以及其他Java应用程序可见。但是Java里面 的文件锁机制主要是基于共 享锁模型,在不支持共享锁模型的操作系统上,文件锁本身也起不了作用,JDK1.4使用文件通道读写方式可以向一些文件 发送锁请求,FileChannel的 锁模型主要针对的是每一个文件,并不是每一个线程和每一个读写通道,也就是以文件为中心进行共享以及独占,也就是文件锁本身并不适合于同一个JVM的不同 线程之间。


6.java并发之原子性、可见性、有序性

(1)原子性:

在Java中,对基本数据类型的变量的读取和赋值操作是原子性操作,即这些操作是不可被中断的,要么执行,要么不执行。

实现:可以通过synchronizedLock来实现。由于synchronized和Lock能够保证任一时刻只有一个线程执行该代码块,那么自然就不存在原子性问题了,从而保证了原子性。

(2)可见性

指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。

实现:volatile关键字来保证可见性。

通过synchronized和Lock也能够保证可见性,synchronized和Lock能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存当中。因此可以保证可见性。

(3)有序性

即程序执行的顺序按照代码的先后顺序执行

实现:可以通过volatile关键字来保证一定的“有序性”(具体原理在下一节讲述)。另外可以通过synchronized和Lock来保证有序性,很显然,synchronized和Lock保证每个时刻是有一个线程执行同步代码,相当于是让线程顺序执行同步代码,自然就保证了有序性。


7.nat是什么?属于哪一层?

NAT是网络地址转换,是一种在IP数据包通过路由器或防火墙时重写来源IP地址或目的IP地址的技术。

在第三层,网络层。


8.网络分层的模型和协议


9.布隆过滤器

https://www.jianshu.com/p/2104d11ee0a2

(1)介绍

本质上布隆过滤器是一种数据结构,比较巧妙的概率型数据结构(probabilistic data structure),特点是高效地插入和查询,可以用来告诉你 “某样东西一定不存在或者可能存在”。

相比于传统的 List、Set、Map 等数据结构,它更高效、占用空间更少,但是缺点是其返回的结果是概率性的,而不是确切的。

(2)原理

讲述布隆过滤器的原理之前,我们先思考一下,通常你判断某个元素是否存在用的是什么?应该蛮多人回答 HashMap 吧,确实可以将值映射到 HashMap 的 Key,然后可以在 O(1) 的时间复杂度内返回结果,效率奇高。但是 HashMap 的实现也有缺点,例如存储容量占比高,考虑到负载因子的存在,通常空间是不能被用满的,而一旦你的值很多例如上亿的时候,那 HashMap 占据的内存大小就变得很可观了。

还比如说你的数据集存储在远程服务器上,本地服务接受输入,而数据集非常大不可能一次性读进内存构建 HashMap 的时候,也会存在问题。

布隆过滤器数据结构

布隆过滤器是一个 bit 向量或者说 bit 数组,长这样:

如果我们要映射一个值到布隆过滤器中,我们需要使用多个不同的哈希函数生成多个哈希值,并对每个生成的哈希值指向的 bit 位置 1,例如针对值 “baidu” 和三个不同的哈希函数分别生成了哈希值 1、4、7,则上图转变为:

Ok,我们现在再存一个值 “tencent”,如果哈希函数返回 3、4、8 的话,图继续变为:

当查询一个key是否存在时,将这个key的所有hash函数结果得出,如果存在有为0的,这个key一定不存在,但如果没有,说明这个key可能存在的。是有一定概率的,不能完全确保存在。

(3)其他问题

  • 支持删除么

基本的是值支持添加和查询操作,不支持删除的,原因显而易见。但如果想支持删除可以采用计数法,但会占用内存的。即添加数据时,map的相应位value++;删除的时候value--,如果value==0,查询不存在。

  • 如何选择哈希函数个数和布隆过滤器长度


10.HTTP缓存机制

https://www.cnblogs.com/chenqf/p/6386163.html

Http 缓存机制作为 web 性能优化的重要手段,对于从事 Web 开发的同学们来说,应该是知识体系库中的一个基础环节

(1)在介绍HTTP缓存之前,作为知识铺垫,先简单介绍一下HTTP报文

HTTP报文就是浏览器和服务器间通信时发送及响应的数据块。
浏览器向服务器请求数据,发送请求(request)报文;服务器向浏览器返回数据,返回响应(response)报文。
报文信息主要分为两部分
1.包含属性的首部(header)--------------------------附加信息(cookie,缓存信息等)与缓存相关的规则信息,均包含在header中
2.包含数据的主体部分(body)-----------------------HTTP请求真正想要传输的部分

(2)缓存规则解析

在客户端第一次请求数据时,此时缓存数据库中没有对应的缓存数据,需要请求服务器,服务器返回后,将数据存储至缓存数据库中。

(3)分类

  • 强制缓存

在缓存数据未失效的情况下,可以直接使用缓存数据,那么浏览器是如何判断缓存数据是否失效呢?
我们知道,在没有缓存数据的时候,浏览器向服务器请求数据时,服务器会将数据和缓存规则一并返回,缓存规则信息包含在响应header中。对于强制缓存来说,响应header中会有两个字段来标明失效规则(Expires/Cache-Control

Expires的值为服务端返回的到期时间,即下一次请求时,请求时间小于服务端返回的到期时间,直接使用缓存数据。
到期时间是由服务端生成的,但是客户端时间可能跟服务端时间有误差,这就会导致缓存命中的误差。
所以HTTP 1.1 的版本,使用Cache-Control替代。

图中Cache-Control仅指定了max-age,所以默认为private,缓存时间为31536000秒(365天)
也就是说,在365天内再次请求这条数据,都会直接获取缓存数据库中的数据,直接使用。

  • 对比缓存

基于对比缓存的流程下,不管是否使用缓存,都需要向服务器发送请求,那么还用缓存干什么?

简单的说就是缓存的数据可能有被修改过,

Last-Modified:服务器在响应请求时,告诉浏览器资源的最后修改时间。

If-Modified-Since:
再次请求服务器时,通过此字段通知服务器上次请求时,服务器返回的资源最后修改时间。

服务器收到请求后发现有头If-Modified-Since 则与被请求资源的最后修改时间进行比对。

若资源的最后修改时间大于If-Modified-Since,说明资源又被改动过,则响应整片资源内容,返回状态码200;

若资源的最后修改时间小于或等于If-Modified-Since,说明资源无新修改,则响应HTTP 304,告知浏览器继续使用所保存的cache。

不不用修改时,返回影响时间是很快的,因为不需要穿数据,因此并不浪费时间的。


11.hashMap产生死锁的原因

多线程,在扩容的时候产生死锁


12.Hash索引和B+树索引的区别

(1)如果是等值查询,那么hash索引明显有绝对优势:但如果查询健不是唯一的话,因为hash碰撞问题,需要在同一个entry下面的链表中查询,这样查询速度就不是很快了;不是很稳定的。B+索引稳定。

(2)范围查找,排序:hash不支持的。B+树支持的;

(3)哈希索引也不支持多列联合索引的最左匹配规则


13.Redis数据类型的实现原理

(1)数据类型:字符串,链表,set集合,hash,有序集合

(2)实现

  • 字符串

int 编码:保存的是可以用 long 类型表示的整数值。

raw 编码:保存长度大于44字节的字符串(redis3.2版本之前是39字节,之后是44字节)。

embstr 编码:保存长度小于44字节的字符串(redis3.2版本之前是39字节,之后是44字节)。

  • list对象

ziplist(压缩列表) 和 linkedlist(双端链表)

  • hash对象

ziplist(压缩列表) 或者 hashtable(字典)

  • set对象

intset(整数集合)或者 hashtable(字典)

  • sorted_set对象

ziplist (压缩列表) 或者 skiplist(跳跃表


14.跳跃表的原理

基于并联的链表,其效率可比拟于二叉查找树(对于大多数操作需要O(log n)平均时间)。基本上,跳跃列表是对有序的链表增加上附加的前进链接,增加是以随机化的方式进行的,所以在列表中的查找可以快速的跳过部分列表(因此得名)。所有操作都以对数随机化的时间进行。Skip List可以很好解决有序链表查找特定值的困难。

跳表具有如下性质:

(1) 由很多层结构组成

(2) 每一层都是一个有序的链表

(3) 最底层(Level 1)的链表包含所有元素

(4) 如果一个元素出现在 Level i 的链表中,则它在 Level i 之下的链表也都会出现。

(5) 每个节点包含两个指针,一个指向同一链表中的下一个元素,一个指向下面一层的元素。


15. Comparator和Comparable

(1)Comparable:与compareTo一起使用

Comparable可以认为是一个内比较器,实现了Comparable接口的类有一个特点,就是这些类是可以和自己比较的。(同一个类的两个对象进行比较)

至于具体和另一个实现了Comparable接口的类如何比较,则依赖compareTo方法的实现,compareTo方法也被称为自然比较方法

如果开发者add进入一个Collection的对象想要Collections的sort方法帮你自动进行排序的话,那么这个对象必须实现Comparable接口。

compareTo方法的返回值是int,有三种情况:

1、比较者大于被比较者(也就是compareTo方法里面的对象),那么返回正整数

2、比较者等于被比较者,那么返回0

3、比较者小于被比较者,那么返回负整数

例子:

public class Domain implements Comparable<Domain>{
    private String str;

    public Domain(String str)
    {
        this.str = str;
    }
    //重写compareTo
    public int compareTo(Domain domain)
    {
        if (this.str.compareTo(domain.str) > 0)
            return 1;
        else if (this.str.compareTo(domain.str) == 0)
            return 0;
        else 
            return -1;
    }
    
    public String getStr()
    {
        return str;
    }
}
public class test {
	public static void main(String[] args)
    {
        Domain d1 = new Domain("c");
        Domain d2 = new Domain("c");
        Domain d3 = new Domain("b");
        Domain d4 = new Domain("d");
        System.out.println(d1.compareTo(d2));
        System.out.println(d1.compareTo(d3));
        System.out.println(d1.compareTo(d4));
        //加入集合中进行排序
        List<Domain>ld=new ArrayList<>();
        ld.add(d1);
        ld.add(d2);
        ld.add(d3);
        ld.add(d4);
        System.out.println("排序前:");
        for(Domain d:ld) {
        	System.out.print(d.getStr()+" ");
        }
        System.out.println();
        System.out.println("排序后:");
        Collections.sort(ld);
        for(Domain d:ld) {
        	System.out.print(d.getStr()+" ");
        }
    }
}

结果:

 

(2)Comparator:与compare()一起使用

Comparator可以认为是是一个外比较器,个人认为有两种情况可以使用实现Comparator接口的方式:

1、一个对象不支持自己和自己比较(没有实现Comparable接口),但是又想对两个对象进行比较

2、一个对象实现了Comparable接口,但是开发者认为compareTo方法中的比较方式并不是自己想要的那种比较方式

Comparator接口里面有一个compare方法,方法有两个参数T o1和T o2,是泛型的表示方式,分别表示待比较的两个对象,方法返回值和Comparable接口一样是int,有三种情况:

1、o1大于o2,返回正整数

2、o1等于o2,返回0

3、o1小于o2,返回负整数

例子:上面例子Domian类不用变

DomainComparator

public class DomainComparator implements Comparator<Domain>
{
    public int compare(Domain domain1, Domain domain2)
    {
        if (domain1.getStr().compareTo(domain2.getStr()) > 0)
            return 1;
        else if (domain1.getStr().compareTo(domain2.getStr()) == 0)
            return 0;
        else 
            return -1;
    }
}
public class test {
	public static void main(String[] args)
	{
	    Domain d1 = new Domain("c");
	    Domain d2 = new Domain("c");
	    Domain d3 = new Domain("b");
	    Domain d4 = new Domain("d");
	    DomainComparator dc = new DomainComparator();
	    System.out.println(dc.compare(d1, d2));
	    System.out.println(dc.compare(d1, d3));
	    System.out.println(dc.compare(d1, d4));
	}
}

结果:

(3)总结

1.使用:Comparable与compareTo()结合使用,Comparator与compare()结合使用;

2.作用:Comparable是内比较器,用于同一个类的比较,当然可也有用于两个类,但这两个类需要重写compareTo();Comparator是外部比较器,通过实现Comparator,重写compare()比较两个类。

3.优缺点:实现Comparable接口的方式比实现Comparator接口的耦合性要强一些,如果要修改比较算法,要修改Comparable接口的实现类,而实现Comparator的类是在外部进行比较的,不需要对实现类有任何修改。从这个角度说,其实有些不太好,尤其在我们将实现类的.class文件打成一个.jar文件提供给开发者使用的时候。实际上实现Comparator 接口的方式后面会写到就是一种典型的策略模式

当然,这不是鼓励用Comparator,意思是开发者还是要在具体场景下选择最合适的那种比较器而已。

 

 

 

 


 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值