声明
- 面试问题来源:牛客
- 个人总结,可能有的地方有错误或者不充分,欢迎大家评论指正!
- 有需要解决的面试问题可以推荐,我会抽时间去写
- 面经里过滤掉了实习问题和项目问题,只涉及八股
- 整理比较花费时间,如果觉得有用,大家多点赞关注!
Java集合
1.JDK1.7和1.8中HashMap的区别
- 扩容后数据存储的位置计算不同:JDK1.7直接用哈希值与扩容后的size-1进行
&
操作(其实就是hash & newSize
),JDK1.8则根据e.hash&oldSize==0
做判断,如果等于0则原地不动,否则计算出来的大小加上oldSize
- 哈希值的计算方式不同:在JDK1.8中是将哈希值右移16位并且再与哈希值做异或操作,而JDK1.7则进行了多次位运算和异或运算的扰动处理。
// JDK 1.7
static int hash(int h) {
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
// JDK 1.8
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
- 底层数据结构不一样:在JDK1.7中,HashMap的底层数据结构是数组和链表的结合。当发生哈希冲突的时候,冲突的元素会被存储在链表中;JDK1.8在数组和链表的基础上引入了红黑树,当链表中的元素数量超过一定阈值(默认是 8)时,链表会转换为红黑树,从而将查找时间从 O(n) 优化为 O(log n)
这里其实转换成红黑树的条件还有一个是数组的长度大于64
- 插入顺序的不同:JDK1.7及以前插入元素的时候使用的是头插法,在并发的条件下会导致扩容死循环;JDK1.8改用了尾插法。
为什么会产生死循环可以参考下面的博客,讲的比较清楚:
https://www.51cto.com/article/699495.html
2.ArrayList和LinkedList区别介绍一下
- 底层数据结构:ArrayList是动态数组数据结构实现,LinkedList是双向链表的数据结构实现的
- 操作效率:
- 查找:ArrayList按照下标查询的时间复杂度O(1),LinkedList不支持下标查询;如果查找未知索引, ArrayList需要遍历,链表也需要遍历链表,时间复杂度都是O(n)
- 插入和删除:ArrayList尾部插入和删除,时间复杂度是O(1);其他部分增删需要挪动数组,时间复杂度是O(n);LinkedList头尾节点增删时间复杂度是O(1),其他都需要遍历链表,时间复杂度是O(n)。(这里链表插入和删除为O(n)的原因是需要先遍历链表找到具体位置,然后再执行操作,若单纯说在指定位置插入删除则为O(1))
- 线程安全:ArrayList和LinkedList都不是线程安全的,
- 占用内存大小:ArrayList底层是数组,内存连续,节省内存,LinkedList 是双向链表需要存储数
据,和两个指针,更占用内存
JUC
1. volatile关键字介绍一下
- 保证线程变量的可见性:对于有volatile关键字修饰的变量,对于值的修改会立即写入到主存中,每次使用它都到主存中进行读取。这样保证了不同线程对这个变量进行操作的可见性
- 禁止指令重排序:
- 当程序执行到volatile变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行,且结果已经对后面的操作可见;在其后面的操作肯定还没有进行
- 在进行指令优化时,不能将在对volatile变量的读操作或者写操作的语句放在其后面执行,也不能把volatile变量后面的语句放到其前面执
2. Synchronized和ReentrantLock区别
- 相同点:Synchronized和ReentrantLock都是可重入的锁
- 不同点:
- 底层实现不同:Synchronized底层是基于JVM的Monitor来实现的,而ReentrantLock的底层是基于JDK的API层面来实现的,依赖于AQS
- 锁类型不同:ReentrantLock可以指定是公平锁还是非公平锁。而synchronized只能是非公平锁。所谓的公平锁就是先等待的线程先获得锁。ReentrantLock默认情况是非公平的,可以通过 ReentrantLock类的
ReentrantLock(boolean fair)
构造方法来指定是否是公平的。 - 获取锁和释放锁的方式不同:Synchronized在进入代码块之前会自动加锁,在离开代码块之后会自动释放锁。在发生异常时,会自动释放锁,因此不会导致死锁。ReentrantLock需要手动加锁和释放锁,更加灵活一点。发生异常,如果没有使用unlock去释放锁,则很可能导致死锁现象。因此使用该锁需要在finally里释放锁。
- 锁等待时是否和中断不同:ReentrantLock提供了一种能够中断等待锁的线程的机制,通过
lock.lockInterruptibly()
来实现这个机制。也就是说正在等待的线程可以选择放弃等待,改为处理其他事情。Synchronized锁等待是不可中断的 - 灵活性:ReentrantLock加锁和释放锁的时候更加灵活,并且可以设置尝试获取锁的超时时间,同时可以通过
Condition
绑定多个条件 - 性能方面:JDK1.6之后Synchronized有了锁升级优化,两者的性能相差不大,一般锁竞争不激烈的时候使用Synchronized,激烈的时候ReentrantLock的性能更好
个人感觉没太多必要都答上:
可以答自己熟悉的
1.比如底层实现引导面试官去问你synchronized的底层Monitor,ReentrantLock依赖的AQS
2. 锁类型不同引导面试官问你reentrylock如何实现的公平锁和非公平锁
像最后一点如果你熟悉synchronized的锁升级,可以引导面试官,如果不熟悉最后一点可以忽略掉,不然会给自己挖坑
3. 公平锁和非公平锁介绍一下
- 非公平锁:
- 多个线程不按照申请锁的顺序去获取锁,而是直接去尝试获取锁,获取不到,再进入队列等待,如果能够获取到,则直接获取锁。
- 可以减少CPU唤醒线程的开销,整体吞吐量会高一些。但是队列中的线程可能会出现一直获取不到或者长时间获取不到锁,活活饿死的情况。
- 公平锁:
- 多个线程按照申请锁的顺序去获取锁,所有线程都在队列里排队,这样就保证了队列中的第一个先获取到锁。
- 所有线程都能得到资源,不会出现饿死的情况,但是cpu唤醒阻塞线程的开销比较大。
JVM
1. 介绍一下JVM的垃圾回收
个人感觉该问题比较宽泛,可以从怎么定位垃圾、垃圾回收算法、垃圾回收器三个角度去回答问题
- 如何定位垃圾
- 引用计数法:一个对象被引用了一次,就会在对象头上递增一次引用次数,如果这个对象的引用次数为0,则代表可回收
- 可达性分析:通过一系列称为
GC Roots
的对象作为起点,从这些节点开始向下搜索,节点所走过的路径成为引用链,当一个对象到GC Roots
没有任何引用链的话,则证明该对象是没用的,需要被回收
这里涉及到一个问题就是,哪些对象可以作为GC Roots?
《深入理解Java虚拟机》这本书有关于这个问题的回答:
- 在虚拟机栈中引用的对象,譬如各个线程被调用的方法堆栈中使用到的参数、局部变量、临时变量等
- 在方法区中类静态属性引用的对象,譬如Java类的引用类型静态变量
- 在方法区常量引用的对象,譬如字符串常量池里的引用
- 在本地方法栈JNI(通常所说的Native方法)引用的对象
- Java虚拟机内部的引用,如基本数据类型对应的Class对象,一些常驻的异常对象,还有系统类加载器
- 所有被同步锁(Synchronized关键字)持有的对象
- 反应Java虚拟机内部情况的JMXBean、JVMTI中注册的毁掉、本地代码缓存等。
面试能说出来几个就行,至少说一下两栈两方法区
-
垃圾回收算法:
- 标记清除算法:当JVM 识别出内存中的垃圾以后,直接将其清除,但是这样有一个很明显的缺点,就是会导致内存空间的不连续,也就是会产生很多的内存碎片。
- 标记复制算法:
- 首先将内存划分成两个区域。新创建的对象都放在其中一块内存上面,当快满的时候, 就将标记出来的存活的对象复制到另一块内存区域中(注意:这些对象在在复制的时候其内存空间上是 严格排序且连续的),这样就腾出来一那一半就又变成了空闲空间了。依次循环运行。
- 内存区域减半了,并且复制时开销较大,因此适合新生代,不适合老年代(老年代的话会复制很多,开销很大)
- 标记整理算法:标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象回收,而是让所有存活的对象向一端移动,然后直接清理掉端边界以外的内存。
-
垃圾回收器:
- Serial收集器:
- 最基础、历史最悠久的收集器,大家看名字就知道这个收集器是一个单线程收集器了。它的 “单线程” 的意义不仅仅意味着它只会使用一条垃圾收集线程去完成垃圾收集工作,更重要的是它在进行垃圾收集工作的时候必须暂停其他所有的工作线程( “Stop The World” ),直到它收集结束,采用的是标记复制算法。
- 简单而高效(与其他收集器的单线程相比)。Serial 收集器由于没有线程交互的开销,自然可以获得很高的单线程收集效率。Serial 收集器对于运行在 Client 模式下的虚拟机来说是个不错的选择。
- ParNew收集器:
- ParNew 收集器其实就是 Serial 收集器的多线程版本,除了使用多线程进行垃圾收集外,其余行为(控制参数、收集算法、回收策略等等)和 Serial 收集器完全一样。
- 除了Serial收集器外,目前只有它能够和CMS收集器配合工作。
- Parallel Scavenge收集器:
- Parallel Scavenge也是一个基于标志-复制算法实现的收集器,也是能够并行收集的多线程收集器,和ParNew很相似,但是有他自己的特别之处。
- Parallel Scavenge 收集器关注点是吞吐量(高效率的利用 CPU)。CMS 等垃圾收集器的关注点更多的是用户线程的停顿时间(提高用户体验)。
吞吐量=运行用户代码时间/(运行用户代码时间+运行垃圾收集时间)
,Parallel Scavenge提供了两个参数用于精确控制吞吐量大小,一个是控制最大垃圾收集停顿时间,一个是直接控制吞吐量(为了实现低停顿时间,垃圾收集器可能会更频繁地进行小规模的垃圾收集,这样单次停顿时间较短,但总的垃圾收集时间可能增加,影响整体吞吐量)
- Serial Old收集器:
- Serial 收集器的老年代版本,它同样是一个单线程收集器,采用标记整理算法。它主要有两大用途:一种用途是在 JDK1.5 以及以前的版本中与 Parallel Scavenge 收集器搭配使用,另一种用途是作为 CMS 收集器的后备方案。
- Parallel Old收集器:
- Parallel Scavenge 收集器的老年代版本。使用多线程和标记-整理算法。在注重吞吐量以及 CPU 资源的场合,都可以优先考虑 Parallel Scavenge 收集器和 Parallel Old 收集器。
- CMS收集器:CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。它非常符合在注重用户体验的应用上使用。从名字中的Mark Sweep这两个词可以看出,CMS 收集器是一种 标记-清除算法实现的,它的运作过程相比于前面几种垃圾收集器来说更加复杂一些。整个过程分为四个步骤:
- 初始标记: 暂停所有的其他线程,并记录下直接与 root 相连的对象,速度很快
- 并发标记: 同时开启 GC 和用户线程,用一个闭包结构去记录可达对象。但在这个阶段结束,这个闭包结构并不能保证包含当前所有的可达对象。因为用户线程可能会不断的更新引用域,所以 GC 线程无法保证可达性分析的实时性。所以这个算法里会跟踪记录这些发生引用更新的地方。
- 重新标记: 重新标记阶段就是为了修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段的时间稍长,远远比并发标记阶段时间短
- 并发清除: 开启用户线程,同时 GC 线程开始对未标记的区域做清扫。
它还有两个缺点就是会产生浮动垃圾和内存碎片
- G1收集器(有点草率后续会补充):
- G1 收集器把Java堆分为多个大小相等的Region,在后台维护了一个优先列表,每次根据允许的收集时间,优先选择回收价值最大的 Region(这也就是它的名字 Garbage-First 的由来) 。这种使用 Region 划分内存空间以及有优先级的区域回收方式,保证了 G1 收集器在有限时间内可以尽可能高的收集效率(把内存化整为零)。
- Serial收集器:
G1介绍的有点草率,没太具体看过,可以参考其他博客
2. 类加载过程了解吗?
- 加载阶段:将类的
class
文件中的二进制数据读取到内存中,将其放在运行时数据区域的方法区内,然后在堆内存创建一个java.lang.Class
对象,用来封装类在方法区内的数据结构。类的加载的最终产品是是位于堆区中的class
对象,class
封装了类在方法区内的数据结构,并作为程序中每个类的数据访问接口。 - 链接阶段:
- 验证:校验类的正确性(文件格式、元数据、字节码、二进制兼容性),保证符合JVM类规范
- 准备:正式为类变量分配内存并设置初始值
- static变量是
final
的基本类型,以及字符串常量的时候,值已确定,赋值在准备阶段完成 - static变量是
final
的引用类型,赋值在准备阶段完成
- static变量是
- 解析:把类中的符号引用转换为直接引用
- 初始化阶段:对类的静态变量赋值,静态代码块执行初始化
- 假如这个类还没有被加载和链接,则程序先加载并链接该类
- 如果该类的直接父类还没有初始化,则先初始化其父类
- 如果同时包含多个静态变量和静态代码块,则按照自上而下的顺序依次执行
1.如果问的是类的生命周期,需要再说一下使用、卸载两个阶段
2.关于直接引用与符号引用
直接引用是在类加载完成后,指向内存中对象、字段或方法的实际地址。
符号引用是在编译期间生成的一种引用,它以字符串、名称或其他形式表示目标。在编译后的 .class 文件中,符号引用用于标识类、字段、方法等。符号引用是对实际内存地址的一种抽象,并且与具体的内存布局无关。编译时,类还未被加载到内存中,因此无法确定确切的内存地址。
3. 介绍一下双亲委派模型
该问题也是比较广泛,可以考虑从什么是双亲委派、好处是什么、如何破坏?当然还可以印出来类加载器,本次面试过程并没有引出来类加载器
- 介绍:双亲委派模型的工作过程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载。
- 好处:
- **避免类的多次加载:**通过双亲委派机制可以避免某一个类被重复加载,当父类已经加载后则无需重复加载,保证唯一性。
- 保证安全性:如果用户自定义的类加载器加载了核心API中的关键类,可能会引发安全问题。双亲委派模型可以防止核心库的类被随意篡改
- 如何破坏双亲委派:
- 实现双亲委派的代码都集中在
ClassLoader
类的loadClass()
方法中,因此只要自定义类加载器并且重写其中的loadClass()
方法,就可以破坏双亲委派模型
- 实现双亲委派的代码都集中在
MySQL
1. MySQL为什么采用B+树,为什么?有什么优势
- B+ 树的非叶子节点不存放实际的记录数据,仅存放索引,因此数据量相同的情况下,相比存储即存索引又存记录的B 树,B+树的非叶子节点可以存放更多的索引,因此B+树可以比B树更矮胖,查询底层节点的磁盘 I/O次数会更少。
- B+树有大量的冗余节点(所有非叶子节点都是冗余索引),这些冗余索引让 B+ 树在插入、删除的效率都更高,比如删除根节点的时候,不会像 B 树那样会发生复杂的树的变化。
- B+ 树叶子节点之间用双向链表连接了起来,有利于范围查询,而B树要实现范围查询,因此只能通过树的遍历来完成范围查询,这会涉及多个节点的磁盘 I/O 操作,范围查询效率不如 B+ 树。
选Mysql索引数据结构主要考虑的两点
1.尽可能减少磁盘I/O操作完成查询
2.高效查询某个记录,并且能够支持范围查询
为什么不使用二叉查找树?平均情况下在二叉查找树里查找一个记录的时间复杂度为O(logn),但最坏情况下为O(n),会退化成线性时间。随着插入的元素越多,二叉查找树的高度会越来越大,查询性能也会越来越差,而且不支持范围查询。
为什么不使用自平衡二叉树?比如红黑树就是一种自平衡二叉树。自平衡二叉树虽然能保持查询操作的时间复杂度在O(logn),但是因为它本质上是一个二叉树,每个节点只能有 2 个子节点,那么当节点个数越多的时候,树的高度也会相应变高,这样就会增加磁盘的 I/O 次数,从而影响数据查询的效率。
为什么不使用B树?为了解决降低树高度的问题,引入B树。B树虽然查询效率相比自平衡二叉树有了提升,但是由于B树的每个节点都包含索引+记录数据,用户的记录数据的大小很有可能远远超过了索引数据,这就需要花费更多的磁盘 I/O 操作次数来读到有用的索引数据。
2. 三层树高能存储多少数据
- 3层B+树大概可以存:
- 主键为bigint:约
2000w
- 主键为int:约
4000w
- 主键为bigint:约
https://blog.csdn.net/HD243608836/article/details/129180833 具体计算可见这篇博客
3. 事务隔离级别有哪些?分别为了解决什么
- 读未提交(read uncommitted)指一个事务还没提交时,它做的变更就能被其他事务看到;无法解决脏读、不可重复读和幻读
- 读已提交(read committed)指一个事务提交之后,它做的变更才能被其他事务看到;能够解决脏读
- 可重复读 (repeatable read)指一个事务执行过程中看到的数据,一直跟这个事务启动时看到的数据是一致的,MySQL InnoDB 引擎的默认隔离级别;能够解决脏读、不可重复读
- 可串行化(serializable)会对记录加上读写锁,在多个事务对这条记录进行读写操作时,如果发生了读写冲突的时候,后访问的事务必须等前一个事务执行完成,才能继续执行;可以解决解决脏读、不可重复读和幻读
4. undolog、redolog、binlog分别什么作用?哪些属于存储引擎?哪些属于Server?
- undolog(存储引擎):
- 保证事务原子性:
- undo log 是一种用于撤销回退的日志。在事务没提交之前,MySQL 会先记录更新前的数据到 undo log 日志文件里面,当事务回滚时,可以利用 undo log 来进行回滚。
- 每当 InnoDB 引擎对一条记录进行操作(修改、删除、新增)时,要把回滚时需要的信息都记录到 undo log 里
- 在插入一条记录时,要把这条记录的主键值记下来,这样之后回滚时只需要把这个主键值对应的记录删掉就好了
- 在删除一条记录时,要把这条记录中的内容都记下来,这样之后回滚时再把由这些内容组成的记录插入到表中就好了;
- 在更新一条记录时,要把被更新的列的旧值记下来,这样之后回滚时再把这些列更新为旧值就好了。
- 在发生回滚时,就读取 undo log 里的数据,然后做原先相反操作。
- ReadView + undo log 实现 MVCC:
- 一条记录的每一次更新操作产生的 undo log 格式,都有一个
roll_pointer
指针和一个trx_id
事务id:- 通过
trx_id
可以知道该记录是被哪个事务修改的; - 通过
roll_pointer
指针可以将这些 undo log 串成一个链表,这个链表就被称为版本链;
- 通过
- undo log 为每条记录保存多份历史数据,MySQL 在执行快照读(普通 select 语句)的时候,会根据事务的 Read View 里的信息,顺着 undo log 的版本链找到满足其可见性的记录。
- 一条记录的每一次更新操作产生的 undo log 格式,都有一个
- 保证事务原子性:
- redolog(存储引擎):
- 保证事务的持久性:
- redo log 是物理日志,记录了某个数据页做了什么修改,比如对 XXX 表空间中的 YYY 数据页 ZZZ 偏移量的地方做了AAA 更新,每当执行一个事务就会产生这样的一条或者多条物理日志。
- 在事务提交时,只要先将 redo log 持久化到磁盘即可,可以不需要等到将缓存在 Buffer Pool 里的脏页数据持久化到磁盘。
- 当系统崩溃时,虽然脏页数据没有持久化,但是 redo log 已经持久化,接着 MySQL 重启后,可以根据 redo log 的内容,将所有数据恢复到最新的状态。
- 保证事务的持久性:
- binlog(Server):
- 备份恢复、主从复制:
- binlog 文件保存的是全量的日志,也就是保存了所有数据变更的情况
- 主从复制流程:
- MySQL 主库在收到客户端提交事务的请求之后,会先写入 binlog,再提交事务,更新存储引擎中的数据,事务提交完成后,返回给客户端“操作成功”的响应。
- 从库会创建一个专门的 I/O 线程,连接主库的 log dump 线程,来接收主库的 binlog 日志,再把 binlog 信息写入 relay log 的中继日志里,再返回给主库“复制成功”的响应。
- 从库会创建一个用于回放 binlog 的线程,去读 relay log 中继日志,然后回放 binlog 更新存储引擎中的数据,最终实现主从的数据一致性。
- 备份恢复、主从复制:
参考https://xiaolincoding.com/
Redis
1. Redis数据结构有哪些?
- String:
- 最基本的数据类型,它是一个动态字符串,可以包含任何类型的数据,如文本或二进制数据。
INCR
命令可以用来做计数器,同样也可以存储一些简单信息,比如token
,还可以实现分布式锁
- List:
- 双向链表,类似于Java中的
LinkedList
- 可以存储一些活动列表、评论列表、活动列表等等,通过
LRANGE
取出一页,按顺序显示
- 双向链表,类似于Java中的
- Hash:
- String 类型的
field-value(键值对)
的映射表,特别适合用于存储对象,类似于Java中的HashMap
- 可以将用户的购物车信息存储在Hash中,其中用户的ID作为键,Hash中的字段可以是商品ID,值可以是商品数量
- String 类型的
- Set:
- 无序集合,集合中的元素没有先后顺序但都唯一,有点类似于 Java 中的
HashSet
。当你需要存储一个列表数据,又不希望出现重复数据时,Set 是一个很好的选择。 - 基于 Set 轻易实现交集、并集、差集的操作,比如你可以将一个用户所有的关注人存在一个集合中,将其所有粉丝存在一个集合。这样的话,Set 可以非常方便的实现如共同关注、共同粉丝、共同喜好等功能。这个过程也就是求交集的过程。
- 无序集合,集合中的元素没有先后顺序但都唯一,有点类似于 Java 中的
- SortedSet:
- 类似于Set,但和Set相比,Sorted Set 增加了一个权重参数
score
,使得集合中的元素能够按score
进行有序排列,还可以通过 score 的范围来获取元素的列表。 - 可以用作排行榜场景,或者可以实现延迟队列
- 类似于Set,但和Set相比,Sorted Set 增加了一个权重参数
- BitMap:
- 一种利用位来存储信息的方式,可以将 Bitmap 看作是一个存储二进制数字(0 和 1)的数组,数组中每个元素的下标叫做
offset
(偏移量)。 - 在签到打卡的场景中,只用记录签到(1)或未签到(0),所以它就是非常典型的二值状态
- 一种利用位来存储信息的方式,可以将 Bitmap 看作是一个存储二进制数字(0 和 1)的数组,数组中每个元素的下标叫做
- HyperLogLog:
- Redis中的HyperLogLog是一种概率数据结构,用于高效地估算大数据集合的基数(即集合中不同元素的数量)。HyperLogLog的主要优势在于其对内存的高效使用,无论集合中包含多少元素,其内存占用量几乎是固定的,并且相对较小(最多12KB),同时提供了非常快速的计算性能。其标准误差率大约是0.81%,对于大多数应用来说,这个误差是可以接受的。
- 数量量巨大(百万、千万级别以上)的计数场景,热门网站每日/每周/每月访问 ip 数统计、热门帖子 uv 统计
- GEO:
- 用于处理和存储地理位置信息的功能。它允许用户存储地理位置的坐标,并执行各种基于地理位置的查询操作,如查找最近的地点、计算两点之间的距离、查询给定半径内的所有地点等。
- 用于查找附近的人、周边服务搜索
面试的回答前5个即可,如果问你还有什么的话,把后3个答出来即可
2. zset底层原理
- 有序集合的编码可以是
ziplist
或者skiplist
。当有序集合保存的元素个数小于128
个,且所有元素成员长度都小于64
字节时,使用ziplist
编码,否则,使用skiplist
编码 ziplist
编码的有序集合使用压缩列表作为底层实现,每个集合元素使用两个紧挨着一起的两个压
缩列表节点表示,第一个节点保存元素的成员(member),第二个节点保存元素的分值(score)。skiplist
编码的有序集合对象使用 zset 结构作为底层实现,一个 zset 结构同时包含一个字典和一个跳跃表。zset结构中的zs1跳跃表按分值从小到大保存了所有集合元素,每个跳跃表节点都保存了一个集合元素。通过跳跃表,可以对有序集合进行基于score的快速范围查找。zset结构中的dict字典为有序集合创建了从成员到分值的映射,字典的键保存了成员,字典的值保存了分值。通过字典,可以用O(1)复杂度查找给定成员的分值。
这里没有图可能比较难理解,后边我会找一些容易理解的图添加上
Spring框架
1. 什么是IOC?有哪些好处
- 介绍:
- 控制即指的是对象创建(实例化、管理)的权力,反转即控制权交给外部环境(Spring 框架、IoC 容器)
- 因此IOC就是在程序中手动创建对象的控制权,交由 Spring 框架来管理。即原来这个对象的控制权在我们的代码中,我们自己new的对象,在Spring,应用程序不再控制对象的创建,而是被动地接受由容器注入的对象。
- 好处:
- 使用者不需要关心引用Bean的实现细节
- 不用创建多个相同的Baan导致浪费
- Bean的修改适用方无需感知
算法:
1. 求字符串中的最大回文字符串
应该是Hot 100原题,题号:5