阿里面试-2019

还有一篇可以看看:随笔分类 - 备战阿里

 他妈 辛辛苦苦准备了2个月,真正的去阿里面试的时候,简直就是打脸大会啊,面试官的问题都听不明白,回答更是天方夜谭。。。。。。

 以下是本人 2019年面试阿里的题目:

自我介绍环节:

 问题1:上来就讲细节,其实对方根本听不懂,应该先介绍项目背景,如果对方感兴趣的话,会多问几句,不要自己上来就说细节。除非对方明确要求问。

 问题2:对于自己项目的架构图一定要牢记,面试一般都会让你画一下架构图,最好漂亮点,去网上查查名词,吹一吹,唬唬人,多做点功课,因为这个可以说是送分题,哪个面试官都会问,准备一定用的上的。

问题3:对于项目建议美团的项目要重点写,其他的简单点写,这样让面试官问起来好问点,不然只能问自我评价的东西了。

最重要的源码一定要看点,

这两天阿里面试官问了我几个问题。 

 

 

 

3、MYSQL数据库隔离级别:

Mysql的隔离级别默认是 可重复读,REPEAT-READ

Oracle和SQL Server的默认隔离级别是:read-commit;

MySQL数据库为我们提供的四种隔离级别:

数据库事务的四大特性以及4种事务的隔离级别-以及对应的5种JDBC事务隔离级别

4、IO复用,什么是IO复用,以及应用

 这个问题也比较复杂:

另一个面试官还问到IO与NIO的区别:

参考: IO多路复用,同步,异步,阻塞和非阻塞 区别

Java NIO:IO与NIO的区别 -阿里面试题

 

5、数据库遇到性能问题如何处理,

(1) 硬件提升:容量不足,磁盘不足,使用share memory

(2) 软件提升:性能不够,可以使用分库,分表,读写分离

(3)分布式部署:一个事务,两个数据库联动。使用分布式事务。 

 

参考:FULL GC分析过程分享

 

 

 

 分布式部署问题:

1、(a)一致性Hash算法解释

答:这是dubbo 的负载均衡的算法:ConsistentHash LoadBalance:一致性Hash策略,具体配置方法可以参考Dubbo文档。相同调用参数的请求会发送到同一个服务提供方节点上,如果某个节点发生故障无法提供服务,则会基于一致性Hash算法映射到虚拟节点上(其他服务提供方)

(b) 阿里还问 如果有7台服务器给你提供接口,服务器如何找到这个接口的,其实考察的是负载均衡算法:

Dubbo框架内置提供了4种负载均衡策略,如下所示:

(1)Random LoadBalance:随机策略,配置值为random。可以设置权重,有利于充分利用服务器的资源,高配的可以设置权重大一些,低配的可以稍微小一些

(2)RoundRobin LoadBalance:轮询策略,配置值为roundrobin

(3)LeastActive LoadBalance:配置值为leastactive。根据请求调用的次数计数,处理请求更慢的节点会受到更少的请求

(4)ConsistentHash LoadBalance:一致性Hash策略,具体配置方法可以参考Dubbo文档。相同调用参数的请求会发送到同一个服务提供方节点上,如果某个节点发生故障无法提供服务,则会基于一致性Hash算法映射到虚拟节点上(其他服务提供方)

具体参考:Dubbo学习(二) Dubbo 集群容错模式-负载均衡模式

2、CAP理论

 参考:CAP原则 (阿里)

3、原子操作 AtomicInteger  

对CAS的理解,CAS是一种无锁算法,CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。 

4、分布式如何解决一致性问题

参考:分布式系统一致性问题解决实战(阿里)

6、高并发综合策略

参考:高并发&高可用系统的常见应对策略 秒杀等-(阿里)

 

  7、我说用过docker ,他问 给你5台服务器 如何部署docker,自动化 如何写脚本 使用 Swarm ,compose;

参考:docker swarm和compose 的使用(阿里)

 

   8 docker网墙原理;

9、如果数据量很大,如何在web层面处理这些大的数据。

10、当处理上亿级别的数据的时候,在web层如何搭建架构:

参考:【系统架构】亿级Web系统搭建(1):Web负载均衡(阿里)

 

二、数据库问题:

1、redis

     (a)Redis的内存废弃策略

  参考:Redis的内存回收策略和内存上限

   (b)redis  的网络架构,单线程还能实现并发量这么高,如何实现的,为何redis使用了跳表,没有使用B+数,因为B+树是为了减少IO,而redis 是在内存里面,所以不用B+树

具体参考:redis为何单线程 效率还这么高 为何使用跳表不使用B+树做索引(阿里)

2、redis高并发的key怎么处理

参考:高并发架构系列:Redis并发竞争key的解决方案详解

3、redis 缓存雪崩(大量的key同时失效) 缓存击穿如何处理:

参考:Redis缓存穿透、缓存雪崩、redis并发问题 并发竞争key的解决方案 (阿里)

4、redis 是如何找到其中的key的; mysql是如何通过索引找到key 并删除的

这其实考察的是索引的原理

Mysql 使用了B+Tree 具体参考:一步步分析为什么B+树适合作为索引的结构

redis 使用了跳表 复杂度O(logn) 参考:聊聊Mysql索引和redis跳表 ---redis的跳表原理 时间复杂度O(logn)(阿里)

5、mysql 的innoDB使用的是B+Tree索引,mysiam呢使用了什么索引(fulltext索引);

举例来说,比如我在orderId上面 添加了索引,如果 我执行 delete tb_order where orderId='12';

mysql 如何通过index定位到这条数据,并删除的。

答: fulltext索引(全文索引) 仅可用于 MyISAM 表 ,

在MySQL中,主要有四种类型的索引,分别为:B-Tree索引,Hash索引,Fulltext索引和R-Tree索引

具体参考:一步步分析为什么B+树适合作为索引的结构 

区别参考:MySql的多存储引擎架构, 默认的引擎InnoDB与 MYISAM的区别(滴滴 阿里)

5、HIVE sql 调优

参考:Hive之——Hive SQL优化

参考:hivesql优化的深入解析

 

三、Sring MVC 的请求过程,一个Controller是单例还是多实例(答案:默认是单例的)

参考:spring的controller默认是单例还是多例

参考:Spring学习 6- Spring MVC (Spring MVC原理及配置详解)

四、IO 通讯

  1、netty原理 

  参考:新手入门:目前为止最透彻的的Netty高性能原理和框架架构解析

     2、BIO,NIO,AIO的原理

     参考:IO复用,AIO,BIO,NIO,同步,异步,阻塞和非阻塞 区别(百度)

    3、Websocket连接池 原理,如何实现一个端口 实现多个并发请求的。用了哪一种连接池。如何你来设计连接池,你会考虑哪些参数,比如 过期时间等

我们使用了 GenericObjectPool 一般对象池技术 

 @Override
    @RhinoBreaker
    @ApiResult
    public CityResponse execRequest(CityRequest cityRequest) {
        GenericObjectPool<WSClient> wsClientPool = null;
        WSClient wsClient = null;
        URI uri;
        Transaction transaction = Cat.newTransaction(CatConstant.TRANSACTION_WEBSOCKET_REQUEST.getKey(), CatConstant.TRANSACTION_WEBSOCKET_REQUEST.getValue());

        try {
            uri = getURI(cityRequest);
            wsClientPool = wsClientPoolCache.get(uri.toString());
            wsClient = wsClientPool.borrowObject();
            CityResponse cityResponse = send(wsClient, Collections.singletonList(cityRequest));
            if (cityResponse.isOk()) {
                transaction.setSuccessStatus();
            } else {
                transaction.setStatus("上报失败");
            }
            return cityResponse;
        } catch (Exception e) {
            transaction.setStatus(e);
            /*网络IO 异常 & 可重试异常 & 连接池获取异常*/
            return new FailureResponse(e);
        } finally {
            if (wsClientPool != null && wsClient != null) {
                ((WebSocketImpl) wsClient.getConnection()).updateLastPong();
                wsClientPool.returnObject(wsClient);
            }
            transaction.complete();
        }
    }

 

    举例说一下 连接池丢弃的参数写法。关键字  

连接池丢弃的写法:

  .removalListener((RemovalListener<String, GenericObjectPool<WSClient>>) removalNotification -> {
                    /* 连接池销毁 */
                    if (removalNotification.getValue() != null) {
                        removalNotification.getValue().close();
                        LOGGER.warn("url:{} 的WebSocket连接池已销毁,池数目:{}", removalNotification.getKey(), wsClientPoolCache.size());
                    }
                })

线程池丢弃最开始的数据的写法

这是定义:

    public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler) {
        if (corePoolSize < 0 ||
            maximumPoolSize <= 0 ||
            maximumPoolSize < corePoolSize ||
            keepAliveTime < 0)
            throw new IllegalArgumentException();
        if (workQueue == null || threadFactory == null || handler == null)
            throw new NullPointerException();
        this.acc = System.getSecurityManager() == null ?
                null :
                AccessController.getContext();
        this.corePoolSize = corePoolSize;
        this.maximumPoolSize = maximumPoolSize;
        this.workQueue = workQueue;
        this.keepAliveTime = unit.toNanos(keepAliveTime);
        this.threadFactory = threadFactory;
        this.handler = handler;
    }

下面是重写:

   }, (r, executor) -> {
        if (!executor.isShutdown()) {
            /* 丢弃队列最老的数据 */
            if (executor.getQueue().poll() != null) {
                Cat.logMetricForCount(CatConstant.METRIC_DISCARD_FILE_TASK_COUNT);
            }
            executor.execute(r);
        }
    });

 

   4、网络中断 报错 如何处理


    5、HTTP协议的架构,http1.0,http1.1,http2.0协议的区别和联系。

参考:HTTP/1.0和HTTP/1.1 http2.0的区别,HTTP怎么处理长连接(阿里)

五、MQ

 1、kafka 如何处理高并发的,比如一条数据进入partition之后,如何找到这条数据的。怎么消费的,消费数据的,

参考:kafka如何实现高并发存储-如何找到一条需要消费的数据(阿里)

六、数据结构:

   1、hashmap的时间复杂度O(1),数组的是 O(n),链表也是 O(n)

       参考:HashMap, HashTable,HashSet,TreeMap 的时间复杂度 注意数组链表 增删改查的时间复杂度都不相同(阿里)

   2、Hashtable继承自Dictionary类,而HashMap继承自AbstractMap类。但二者都实现了Map接口。hashtable的初始大小

   hashmap的初始大小,为什么扩容因子是0.75; 以及resize的操作原理  entry 扩容完了之后,顺序会颠倒吗?

    3、冒泡排序的时间复杂度是多少,空间复杂度又是多少?

 答:空间复杂度O(1)

参考:各种排序算法的时间复杂度和空间复杂度(阿里) 

 

   4、blockQueue的长度是多少。 

  5、hashMap的初始大小 16 扩容因子0.75, 原因?

参考:HashMap默认加载因子为什么选择0.75?(阿里)

(a) ArrayList初始化n=10个空间扩容(n3)/2 + 1,如果不够设置传入的值
(b) HashMap初始化n=16空间扩容2n,在并发环境下,可能会形成环状链表(扩容时可能造成)
(c) Hashtable初始化n=11空间扩容2n+1
jdk1.6ConcurrentHashMap初始化segments=16个空间每个segments是初始化一个HashEntry 扩容segments=n2
jdk1.7ConcurrentHashMap初始化segments=16个空间每个segments是初始化两个HashEntry 扩容segments=n*2

6、 concurrentHashMap 是否是线程安全的,如何实现的?

我做了总结:使用了分段锁的技术

 这个问题比较复杂,可以参考:

hashmap,hashTable concurrentHashMap 是否为线程安全,区别,如何实现的

而且Java1.7与Java1.8实现方式并不相同,阿里的面试官3个都问道这个问题,他们好这口,

我整理了一下:ConcurrentHashMap原理分析(1.7与1.8)

7、hashmap 如果多线程操作的话,线程不安全,为什么会不安全:

答案:多线程put的时候,会引起死循环,好好看看这个例子,有图有解释,很管用

具体原因参考:HashMap多线程并发问题分析-正常和异常的rehash1(阿里)


 

   7、红黑树的插入和删除操作,红黑树的树高,左旋右旋,用红黑树实现currentHashMap,红黑树与B+树的区别

红黑树的树高度<=2log(n+1) 时间复杂度:O(lgn)

红黑树:    R-B Tree,全称是Red-Black Tree,又称为“红黑树”,它一种特殊的二叉查找树。红黑树的每个节点上都有存储位表示节点的颜色,可以是红(Red)或黑(Black)。

(4)红黑树的特性
(1)每个节点或者是黑色,或者是红色。
(2)根节点是黑色。
(3)每个叶子节点(NIL)是黑色。 [注意:这里叶子节点,是指为空(NIL或NULL)的叶子节点!]
(4)如果一个节点是红色的,则它的子节点必须是黑色的。
(5)从一个节点到该节点的子孙节点的所有路径上包含相同数目的黑节点。

红黑树特征,以及新增删除 参考:红黑树之 原理和算法详细介绍(阿里面试-treemap使用了红黑树) 红黑树的时间复杂度是O(lgn) 高度<=2log(n+1)1、X节点左旋-将X右边的子节点变成 父节点 2、X节点右旋-将X左边的子节点变成父节点

 

8、如果一个数据结构,需要进行深度优先的遍历,或者进行广度优先的遍历,应该首先考虑什么数据结构来处理

答案:

深度优先-栈

广度优先-队列

因为深度优先需要无路可走时按照来路往回退,正好是后进先出 就是栈
广度优先则需要保证先访问顶点的未访问邻接点先访问,恰好就是先进先出那就是 队列

 

 六、Lock 锁问题;

1、synchronized 如何实现 线程安全的

参考:啃碎并发(七):深入分析Synchronized原理

2、膨胀锁的原理。 

参考:JAVA锁的膨胀过程和优化(阿里)

参考:JAVA锁的优化和膨胀过程

 

 七、JVM内存问题:

1、强引用 和软引用,弱引用(比如threadlocal),虚引用,区别,可达性算法的定义;

 在JDK 1.2之后,Java对引用的概念进行了扩充,将引用分为强引用(Strong Reference)、软引用 依次逐渐减弱。

强引用
  在程序代码中普遍存在的,类似 Object obj = new Object() 这类引用,只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象。

软引用
  用来描述一些还有用但并非必须的对象。对于软引用关联着的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中进行第二次回收。如果这次回收后还没有足够的内存,才会抛出内存溢出异常。

弱引用
  也是用来描述非必需对象的,但是它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。在JDK 1.2之后,提供了WeakReference类来实现弱引用。比如 threadlocal

虚引用
  也叫幽灵引用或幻影引用(名字真会取,很魔幻的样子),是最弱的一种引用 关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。它的作用是能在这个对象被收集器回收时收到一个系统通知。。在JDK 1.2之后,提供了PhantomReference类来实现虚引用。

 2、垃圾回收算法:

首先问一下目前你们用的垃圾回收机制是什么样的:

回答:使用命令:

java -XX:+PrintFlagsFinal -version | grep :

就能看到:

  uintx InitialHeapSize                          := 128981632       {product}           
    uintx MaxHeapSize                              := 2065694720      {product}           
     bool PrintFlagsFinal                          := true            {product}           
     bool UseCompressedOops                        := true            {lp64_product}      
     bool UseParallelGC                            := true            {product}           
java version "1.7.0_76"
Java(TM) SE Runtime Environment (build 1.7.0_76-b13)
Java HotSpot(TM) 64-Bit Server VM (build 24.76-b04, mixed mode)

里面能看到我的来及回收机制是 ParallelGC的垃圾回收机制;具体参考:JVM的垃圾回收机制 总结(垃圾收集、回收算法、垃圾回收器)

能否说几个G1的配置参数:参考:

回答:比如用户期望的最大停顿时间

-XX:MaxGCPauseMillis=200

 或者使用 G1的命令:-XX:+UseG1GC

具体参考:G1相关参数

(1)CMS与G1的区别和联系

主要的区别:G1只有的并发标记的时候才不会stop-the-world 其他步骤都会stop-the-world; 

不要以为掌握到这个程度就行了,看看阿里下面的问题:

(2)G1垃圾回收算法的架构,跟分代收集很不一样,架构是怎样的,还有就是G1如何做到时间可控的

  (a) G1虽然也把内存分成了这三大类,Eden(E), Suvivor(S)和Old(O), 但是在G1里面这三大类不是泾渭分明的三大块内存,G1把内存划分成很多小块, 每个小块会被标记为E/S/O中的一个,可以前面一个是Eden后面一个就变成Survivor了。

  (b) G1其实是Garbage First的意思,垃圾优先? 不是,是优先处理那些垃圾多的内存块的意思

  (c)G1的另一个显著特点他能够让用户设置应用的暂停时间,为什么G1能做到这一点呢?也许你已经注意到了,G1回收的第4步,它是“选择一些内存块”,而不是整代内存来回收,这是G1跟其它GC非常不同的一点,其它GC每次回收都会回收整个Generation的内存(Eden, Old), 而回收内存所需的时间就取决于内存的大小,以及实际垃圾的多少,所以垃圾回收时间是不可控的;而G1每次并不会回收整代内存,到底回收多少内存就看用户配置的暂停时间,配置的时间短就少回收点,配置的时间长就多回收点,伸缩自如。 

更具体的参考:G1 垃圾收集器架构和如何做到可预测的停顿(阿里)

G1与CMS的区别参考:CMS收集器和G1收集器 他们的优缺点对比 G1只有并发标记才不会stop-the-world 其他都会停下来(阿里多次问到)


3、当new 一个对象的时候,JVM做了那些事情。还有我们在堆分配了空间,那么会不会线程不安全,因为堆是共享的?(阿里面试的高频问题,问了至少3次了)
回答:首先一定是安全的,至于原因:为了保证Java对象的内存分配的安全性,同时提升效率,一般有两种解决方案:

  • 1、对分配内存空间的动作做同步处理,采用CAS机制,配合失败重试的方式保证更新操作的线程安全性。
  • 2、每个线程在Java堆中预先分配一小块内存,然后再给对象分配内存的时候,直接在自己这块"私有"内存中分配,当这部分区域用完之后,再分配新的"私有"内存。
  • 方案1在每次分配时都需要进行同步控制,这种是比较低效的。

    方案2是HotSpot虚拟机中采用的,这种方案被称之为TLAB分配,即Thread Local Allocation Buffer。这部分Buffer是从堆中划分出来的,但是是本地线程独享的。

    这里值得注意的是,我们说TLAB时线程独享的,但是只是在“分配”这个动作上是线程独占的,至于在读取、垃圾回收等动作上都是线程共享的。而且在使用上也没有什么区别

  • 不知道大家有没有想过,我们使用了TLAB之后,在TLAB上给对象分配内存时线程独享的了,这就没有冲突了,但是,TLAB这块内存自身从堆中划分出来的过程也可能存在内存安全问题啊。所以,在对于TLAB的分配过程,还是需要进行同步控制的。但是这种开销相比于每次为单个对象划分内存时候对进行同步控制的要低的多。

参考:灵魂拷问:Java对象的内存分配过程是如何保证线程安全的?(阿里面试)

 

 

4、 如果遇到了Full GC如何处理,

首先搞明白什么事Full GC ,JVM回收主要是堆的回收,而堆分为 新生代和老年代,新生代如果满了就会执行Minor GC,回收一次新生代的内存,

新生代执行垃圾回收很频繁,因此使用了复制法(Coping)

存活的对象会放在老年代,如果老年代满了,会执行Full GC,回收老年代的垃圾不是很频繁,因此使用了标记整理法(Mark-Compact)

由此看来:Full GC的生成条件:

(a)调用System.gc时,系统建议执行Full GC,但是不必然执行

(b)老年代空间不足

(c)方法区空间不足

(d)通过Minor GC后进入老年代的平均大小大于老年代的可用内存

(e)由Eden区、From Space区向To Space区复制时,对象大小大于To Space可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小 

阿里大神面试官回答:

(a)看一下有没有大对象

(b)大的静态对象

(c) Dump 内存快照,进行分析 

 

八、线上问题排查:

   1、如果一台服务器负载很高,如何处理 ,假如他cpu使用率很低,IO也很低呢?

不要上来就说 看一下 是CPU高还是IO高,因为有时候,CPU不高,IO也不高,但服务器负载就是高,看下面的博客

参考:服务器负载过高问题分析-不是cpu高负载也不是IO负载如何处理(阿里)

九、算法题目

1、给你1000条数据,每条数据都带有起始结束时间的任务,如何把时间重叠的数据取出来;

2、给你1亿个数据,里面有重复的,如何把前100最小的数取出来

参考:关于“100g文件全是数组,取最大的100个数”解决方法汇总

 

 

十、Cache 缓存

1、guava如何删除过期数据,guava的架构,如何实现的删除,都要看源码,别只看博客解说。就是应付也要看点,不然怎么说,Caffeine缓存

参考:Guava cacha 机制及源码分析   

2、在GuavaCache中,并不存在任何线程!它实现机制是在写操作时顺带做少量的维护工作(如清除),偶尔在读操作时做(如果写操作实在太少的话),也就是说在使用的是调用线程 

参考:GuavaCache简介(一)是轻量级的框架 少量数据,并且 过期时间相同 可以用 GuavaCache

十一、设计模式,生产者消费者设计模式,以及wait,notifiy的运用,阿里问了一个面试、终极目的是用wait和notify实现一个可重入锁来保证顺序;

问题:使用“生产者-消费者模式”编写代码实现:线程A随机间隔(10~200ms)按顺序生成1到100的数字(共100个),
放到某个队列中.3个线程B、C、D即时消费这些数据,线程B打印(向控制台)所有被2整除的数,
线程C打印被3整除的数,线程D打印其它数据,要求数字的打印是有序的(从1到100)
限时40分钟,可以使用IDE及第三方类库

参考:阿里笔试-生产者消费者模式

这个类似的问题美团也问过,不过美团直接说能否用synchronized 实现一个可重入锁,本质上也是使用了wait和notify()函数

参考:使用synchronized 实现ReentrantLock(美团面试题目)

 

 十二、加解密算法 每次向对方的服务器发送数据,都需要传递token 这是什么机制?

这是非对称加密 参考:聊聊对称/非对称加密在HTTPS中的使用

 

 

最后一百、架构设计知识

1、是否了解DDD 架构设计 

 

 

 

分为四层,(1)基础设施层(2)领域服务层(3)应用服务层(4)用户接口层

用户界面层:负责向用户展示信息或解释用户指令,如:天网的前端界面,调用天网服务的外卖应用程序等。

应用层:定义软件要完成的任务,指挥领域层的对象或调用其他应用服务来解决问题 ,不包含业务规则和知识,要求尽量的简单。

领域层:负责表达业务概念,业务状态信息以及业务规则。 核心

基础设施层:为上面各层提供通用的技术能力:比如 以上各层交互的Thrfit,为领域层提供持久化机制的zebra,公司为UI层提供的通用组件 等等。

 

以天网量化分级导入功能为例:

用户界面层:天网前端界面

应用层:负责参数校验、excel解析、调用mdc接口验证门店是否存在、调用领域层进行检查 和存储。

领域层:量化分级模块 领域对象(Entity、Value Objext)、以及 操作服务(Service)。

基础设施层:Thrift、MCC、zebra等。

 

 
1、电面:重点在过往主导技术项目的方案阐述/计算机理论知识/Java基础知识的考核:

(1)方案有思考、有对比选型 (2)沟通表达顺畅、能抓住重点;(3)计算机(计算机网络/数据结构/操作系统等),Java(集合类/并发/多线程/JVM等)基础扎实 (4)有业务sense加分
2、现场1面:重点考察落地以及编码能力(1)深入履历核心方案深挖 (2)给一个陌生场景,考察候选人面对新问题的应变能力 (3)实际编码考察

3、数据结构的现场面试题:

链表翻转的图文讲解

参考:https://www.cnblogs.com/aspirant/p/9199608.html

给定一个字符串,请你找出其中不含有重复字符的 最长子串 的长度。

参考:https://www.jianshu.com/p/f1f74aa59dd4

 

参考:DDD(Domain Driven Design) 架构设计

 

 

参考:阿里P6-P7面试准备

posted @ 2018-04-08 17:43  aspirant  阅读(2707)  评论(0编辑  收藏  举报