目录
前言
“实战面经”是本专栏的第二个部分,本篇博文是第六篇博文,如有需要,可:
一、如何在N个数中1前K小个数?
1. 利用快排
利用快排中的划分Partition()函数,找到第k小的数a[k-1],则a[0…k-1]是前k小的数(无序)
即选择一个关键字,通过一趟快排,划分出比关键字小的m个数,如果m > k,则再对前m个数做,否则,对关键字后的一位再做一趟快排,直到选出最小的k个数。
时间复杂度为O(n)
2. 利用堆
建立一个元素个数为k的大根堆,然后用剩下的n – k个元素与堆顶进行比较,如果小于堆顶,就用该元素替换堆顶,然后进行堆调整。
时间复杂度为O(n + k)
3. 利用hash散列
先利用hash散列进行降维,分为m份,然后求出每份的前k小,然后再根据堆方法,求出这mk个数中的前k小。
二、TCP断开连接时为什么需要四次挥手?
为了确保数据能够完整传输。
断开TCP连接需要四次挥手的原因是因为TCP是全双工协议,即双方都可以向对方发送数据。在断开连接时,双方都要发送确认信息,以确保对方已经收到自己的断开请求。因此,需要四次挥手来完成断开连接的过程。
三、虚拟地址到物理地址的映射过程
1. 分页内存管理
将虚拟内存空间和物理内存空间皆划分成大小相同的页面,例如4KB、8KB和16KB等。并将页作为内存空间的最小分配单位,一个程序的一个页面(虚拟页面)可以存放在任何一个物理页面中。
2. 映射过程
先从内存中读取页表,然后根据虚拟地址查表得到物理地址,如果内存中没有当前所需的页,就会发生缺页中断,将所需的页调入内存中,并更新页表。
四、传输用户账号密码时如何加密?
在前后端之间传输用户帐号密码时,为确保信息的安全性和隐私,应采取以下措施对数据进行加密:
1. HTTPS(安全超文本传输协议): 在客户端与服务器之间使用 HTTPS 而非 HTTP 进行通信。HTTPS 协议使用 SSL/TLS 加密,能确保传输数据的机密性、完整性和身份验证。在浏览器中输入账号密码时,启用 HTTPS 可防止他人拦截和窃取数据。
2. 客户端加密: 在传输之前,可以使用客户端 JavaScript 对用户密码进行加密。加密可以使用哈希函数(例如,SHA-256 或 SHA-512)或基于公钥的加密方式(如 RSA)。对于哈希函数,请注意添加随机的“盐”(Salt)以使生成的哈希更具安全性。
3. 服务器端加密: 当服务器接收到加密过的账号和密码后,在存储数据至数据库前,应再次进行加密。常见的做法是使用哈希+盐的方式对密码进行安全哈希。服务器应该存储最终的哈希值以及盐,而非明文密码。对于用户鉴权,应将收到的待验证密码作同样的加密操作并与存储的哈希值进行比较。
下面是一些关键点:
- 确保域名已经获取了 SSL/TLS 证书,并已在 Web 服务器上进行了安装和配置,以确保 HTTPS 的正常使用。
- 使用现代安全的加密库和算法。避免使用已知的缺陷和不安全的加密算法。
保持代码库和所依赖的加密库的更新,确保补丁及时修复。 - 以上方法概括了前后端传输时有关加密的一些关键点。注意到只有通过 HTTPS 保护数据传输过程,客户端和服务端加密确保了存储数据的安全。建议始终跟踪当前的安全实践和加密策略,以确保用户数据安全。
五、JSON为什么不存在数据库里?
JSON 数据并非不能存储在数据库中。实际上,许多现代数据库(特别是 NoSQL 数据库,如 MongoDB、Couchbase 和 Cassandra, 或一些支持 JSON 数据类型的关系型数据库如 PostgreSQL 和 MySQL)支持 JSON 格式的数据存储。
但在一些数据存储场景下,存储原始的 JSON 数据可能不是最优选择。原因可能有以下几点:
1. 结构化查询: 关系型数据库如 SQL Server、Oracle 和 MySQL 是为高效处理结构化数据而设计的。如果直接将 JSON 数据存储在这些关系型数据库中,可能导致低效的查询性能。另一方面,针对 JSON 数据格式的 NoSQL 数据库在这方面表现得更好,因为它们支持更灵活的数据存储和查询。
2. 数据规范化: 在关系型数据库中,通常偏向于采用规范化的数据模型(即通过使用连接去掉重复数据),以减少数据冗余、节省存储空间以及实现数据一致性。JSON 数据是一种自包含的、非规范化的数据结构,可能不适用于具有规范化需求的关系型数据库。
3. 数据类型和验证: 关系型数据库通常支持严格的数据类型和数据约束,从而可以确保数据的完整性。JSON 数据格式没有这种功能,因此在这些应用场景下,需要额外地实现这些验证和完整性保证。
4. 在原始JSON格式下存储至关系型数据库时,如果需要对JSON格式内的字段进行建立索引和查询,将带来额外的性能和计算负担。因为处理包含在JSON内的查询将更复杂和耗时。
需要强调的是: 选择存储 JSON 数据或将其转换为特定数据模型取决于具体的需求和使用场景。在一些现代数据库中, 对 JSON 数据的存储和查询具有很好的支持,因此可以根据实际需求进行判断。
六、Redis是什么?
Redis是REmote DIctionary Server
的缩写,即远程字典服务器。它以字典结构存储数据,并允许其他应用通过TCP协议读写字典中的内容。同大多数脚本语言中的字典一样,Redis字典中的键值除了可以是字符串,还可以是其他数据类型。
七、Redis的过期策略
1. 定时删除: 在设置key的过期时间的同时,为该key创建一个定时器,让定时器在key的过期时间来临时,对key进行删除。
2. 惰性删除: key过期的时候不删除,每次从数据库获取key的时候去检查是否过期,若过期,则删除,返回null。
3. 定期删除: 每隔一定时间就随机抽取一些设置了过期时间的 key,检查其是否过期,如果过期就删除。
缺点:
1) 定时删除: 定时器会消耗CPU资源,降低系统吞吐量;
2) 惰性删除: 如果过期的key很久没有被再次访问,那么会被无用的数据占用大量的内存空间。
3) 定期删除: 因为是随机的,所以可能过期的key很长时间没有被随机到。
都有缺点,所以需要淘汰策略。
八、Redis的淘汰策略
淘汰策略起作用于内存不足以容纳新写入的数据时:
1. noeviction
: 新写入操作会报错。
2. allkeys-lru
: 在键空间中,移除最近最少使用的key。
3. allkeys-random
: 在键空间中,随机移除某个key。
4. volatile-lru
: 在设置了过期时间的键空间中,移除最近最少使用的key。
5. volatile-random
: 在设置了过期时间的键空间中,随机移除某个key。
6. volatile-ttl
: 在设置了过期时间的键空间中,移除最早过期时间的key。
九、写一个LRUCache
十、ThreadLocal适合的场景、实现、注意事项
1. 适合的场景: ThreadLocal适合用来保存线程生命周期内的上下文信息。
2. 实现: Thread类里面有一个threadLocals对象,指向这个线程的ThreadLocalMap对象,里面存储了这个线程中所有的ThreadLocal对象。
3. 注意事项: 由于ThreadLocalMap的Entry中Key是弱引用,可以被回收,回收之后键值对变成<null, Value>。为了避免内存泄漏,用完之后要手动调用remove方法通过Key,把整个Entry释放掉。
十一、CountDownLatch、Semaphore和CyclicBarrier
1. CountDownLatch: 是一个计数器闭锁,通过它可以完成类似于阻塞当前线程的功能,即:一个线程或多个线程一直等待,直到其他线程执行的操作完成。CountDownLatch用一个给定的计数器来初始化,该计数器的操作是原子操作,即同时只能有一个线程去操作该计数器。调用该类await方法的线程会一直处于阻塞状态,直到其他线程调用countDown
方法使当前计数器的值变为零,每次调用countDown计数器的值减1。当计数器值减至0时,所有因调用await()方法而处于等待状态的线程就会继续往下执行。
2. Semaphore: 与CountDownLatch相似,不同的地方在于Semaphore的值被获取到后是可以释放的,并不像CountDownLatch那样一直减到底。它也被更多地用来限制流量,类似阀门的 功能。如果限定某些资源最多有N个线程可以访问,那么超过N个主不允许再有线程来访问,同时当现有线程结束后,就会释放,然后允许新的线程进来。类似于锁的lock与 unlock过程(acquire
和release
)。
3. CyclicBarrier: 它允许一组线程相互等待,直到到达某个公共屏障点(common barrier point)。通过它可以完成多个线程之间相互等待,只有当每个线程都准备就绪后,才能各自继续往下执行后面的操作。类似于CountDownLatch,它也是通过计数器来实现的。当某个线程调用await方法时,该线程进入等待状态,且计数器加1,当计数器的值达到设置的初始值时,所有因调用await进入等待状态的线程被唤醒,继续执行后续操作。因为CycliBarrier在释放等待线程后可以重用,所以称为循环barrier。
十二、生产者/消费者的例子
用LinkedBlockDeque
实现
1. 生产者
public class Producer implements Runnable {
private final LinkedBlockingDeque<String> deque;
private int productCount;
public Producer(LinkedBlockingDeque<String> deque) {
this.deque = deque;
productCount = 0;
}
@Override
public void run() {
try {
while (true) {
Thread.sleep(1500);
System.out.println(produce() + " left.");
}
} catch (InterruptedException ex) {
Logger.getLogger(Producer.class.getName()).log(Level.SEVERE, null, ex);
}
}
public int produce() {
String productName = "p" + ++productCount;
deque.offerFirst(productName);
System.out.print("Producer: " + productName + " produced. ");
return deque.size();
}
}
2. 消费者
public class Consumer implements Runnable {
private final LinkedBlockingDeque<String> deque;
public Consumer(LinkedBlockingDeque<String> deque) {
this.deque = deque;
}
@Override
public void run() {
try {
while (true) {
Thread.sleep(2000);
System.out.println(consume() + " left.");
}
} catch (InterruptedException ex) {
Logger.getLogger(Consumer.class.getName()).log(Level.SEVERE, null, ex);
}
}
public int consume() {
String product = deque.pollLast();
System.out.print("Consumer: product " + product + " consumed. ");
return deque.size();
}
}
3. 主类
public class Starter {
private static final LinkedBlockingDeque<String> DEQUE = new LinkedBlockingDeque<>();
public static void main(String[] args) {
Executor threadPool = Executors.newFixedThreadPool(2);
threadPool.execute(new Producer(DEQUE));
threadPool.execute(new Consumer(DEQUE));
}
}
十三、JDK1.7和JDK1.8的HashMap有什么区别?
1. 链表和红黑树的结构调整: 在 JDK 1.8 中的HashMap
实现中,引入了红黑树数据结构。在 JDK 1.7 中,HashMap
在解决哈希冲突时只使用链表。而在 JDK 1.8 中,为减少哈希冲突导致的查询性能下降,当链表长度超过阈值(默认为 8)时,会把链表转换为红黑树。当红黑树节点减少到一定数量(小于 6 个)时,会将红黑树转换回链表。这个优化有助于提高哈希表在处理冲突严重情况下的查询性能。
2. 节点类结构调整: 在 JDK 1.8 中的HashMap
实现里,相较于 JDK 1.7,在节点类中加入了红黑树节点所需要的属性,如色彩属性(红色或黑色)和指向父节点、左子节点、右子节点的引用。
3. 哈希计算优化: 在 JDK 1.8 的HashMap
中,为了降低哈希冲突的概率,引入了一个新的哈希函数。通过将哈希码的高位与低位进行异或运算,使不同哈希码的差异更加明显,减少哈希冲突的概率。这有助于提高哈希表的整体性能。
总之,引入红黑树
和哈希计算优化
是 JDK 1.8 中HashMap
的两个主要改进。这些改进使得HashMap
在处理哈希冲突严重情况下具有更好的性能。
十四、各种排序算法的比较
类别 | 排序方法 | 时间复杂度 | 空间复杂度 | 稳定性 | ||
---|---|---|---|---|---|---|
平均 | 最好 | 最坏 | 辅助存储 | |||
插入排序 | 直接插入 | O(n2) | O(n) | O(n2) | O(1) | 稳定 |
Shell排序 | O(n1.3) | O(n) | O(n2) | O(1) | 不稳定 | |
选择排序 | 直接选择 | O(n2) | O(n2) | O(n2) | O(1) | 不稳定 |
堆排序 | O(nlog2n) | O(nlog2n) | O(nlog2n) | O(1) | 不稳定 | |
交换排序 | 冒泡排序 | O(n2) | O(n) | O(n2) | O(1) | 稳定 |
快速排序 | O(nlog2n) | O(nlog2n) | O(n2) | O(nlog2n) | 不稳定 | |
归并排序 | O(nlog2n) | O(nlog2n) | O(nlog2n) | O(n) | 稳定 | |
基数排序 | O(d(r+n)) | O(d(rd+n)) | O(d(r+n)) | O(rd+n) | 稳定 |
十五、各种HTTP协议的method
方法 | 说明 |
---|---|
GET | 获取资源 |
POST | 传输实体主体 |
PUT | 传输文件 |
HEAD | 获得报文首部 |
DELETE | 删除文件 |
OPTIONS | 询问支持的方法 |
TRACE | 追踪路径 |
CONNECT | 要求用隧道协议连接代理 |
十六、各个容器类之间的关系
在 Java 里,容器类主要可以分为两大类:Collection
接口下的相关容器和 Map
接口下的相关容器。这两类和它们的子类在结构、能力和使用场景上有所区别,我将为你简要介绍这些关系。
1. Collection 接口系列
Collection
接口是 Java 中最基本的容器类,它的主要子接口包括 List
、Set
和 Queue
。
1) List
接口: List
接口的实现提供了一个有序的容器,可以包含重复的元素。用户可以使用索引值访问容器中的元素。常用的 List
实现类有:
ArrayList
:基于动态数组实现,访问效率高,插入、删除效率较低。
LinkedList
:基于双向链表实现,插入、删除效率高,访问效率较低。
2) Set
接口: Set
接口的实现提供了一个无序的容器,不允许出现重复的元素。主要的 Set
实现类包括:
HashSet
:基于哈希表实现,保证元素唯一性,查询、插入和删除速度较快。
LinkedHashSet
:基于 HashSet
实现,并维护一个双向链表,保留了元素的插入顺序。
TreeSet
:基于红黑树实现,元素按自然排序或自定义排序规则进行存储。
3) Queue
接口: Queue
接口的实现提供了基于先进先出(FIFO)策略的队列。典型的 Queue
实现类有:
ArrayDeque
:基于数组实现,具有双端队列特性,可作为栈和队列使用。
LinkedList
:除了实现了 List
接口外,还实现了 Queue
接口,并具有双端队列特性。
PriorityQueue
:基于堆结构实现,元素按自然排序或自定义排序规则进行存储。
2. Map 接口系列
Map
接口是一个键值对映射关系的容器。根据不同的实现方式,Map
接口有以下子类:
1) HashMap
: 基于哈希表实现的 Map,保证键的唯一性,查询、插入和删除速度较快。HashMap
是线程不安全的。
2) LinkedHashMap
: 基于 HashMap
和双向链表实现的 Map,键唯一,保留键值对的插入顺序。
3) TreeMap
: 基于红黑树实现的 Map,键按自然顺序或自定义顺序进行存储。
4) Hashtable
: 与 HashMap
类似,基于哈希表实现的 Map,但在大部分操作上采用同步方法,因此是线程安全的。在 Java 中现在不太推荐使用,ConcurrentHashMap
是一个更好的线程安全的哈希表实现。
5) ConcurrentHashMap
: 基于哈希表实现的 Map,它在多线程修改时采用分段锁机制,较 Hashtable
有更好的性能。线程安全。
这些容器类各自有不同的应用场景和性能特点,根据使用场景和需求选择合适的容器类来满足数据存储和操作的需求。在选择容器类时,通常需要考虑操作性能要求、数据集的大小、线程安
后记
这份面经题目较之前的少些,但补充了很多之前不全的知识点。