今天继续学习,先做两题算法题练练手,在继续整理八股文,深入理解,才能在面试的时候有更好地表现,一起加油吧,希望秋招多拿几个令人心动的offer,冲吧。
目录
1、算法题:判断链表中是否有环
2、算法题:合并二叉树
3.什么是跳表?
4.Spring的特性?
5.mysql事务特性?mysql隔离级别?mysql怎么实现可重复读的呢?
6.讲一下B树,B+树,哈希表,AVL树,红黑树?
7.怎么保证HashMap的线程安全?
8. CAS(Compare And Swap) 、synchronized、volatile?
9.ThreadLocal是否可保证资源的同步?它会导致内存泄漏?
10.jvm 垃圾回收是什么时候触发?
11.线程池有哪几种类型?
12.TCP三次握手四次挥手?
13.OSI七层和TCP/IP四层?OSI五层?
14.算法题:两个有序链表合并?
15.算法题:反转链表
1、算法题:判断链表中是否有环
题目链接:判断链表中是否有环_牛客题霸_牛客网
判断链表是否有环,用快慢指针即可,快指针每次走两步,慢指针每次走一步,如果慢指针不为空,快慢指针能相遇,则链表有环,否则,链表无环。
注意:边界值的处理,防止空指针异常。
/**
* Definition for singly-linked list.
* class ListNode {
* int val;
* ListNode next;
* ListNode(int x) {
* val = x;
* next = null;
* }
* }
*/
public class Solution {
public boolean hasCycle(ListNode head) {
if(head == null || head.next == null){
return false ;
}
ListNode fast = head;
ListNode slow = head ;
while(fast != null && fast.next != null){
fast = fast.next.next ;
slow = slow.next ;
if(slow == fast){
return true ;
}
}
return false ;
}
}
2、算法题:合并二叉树
可以使用递归法和迭代法合并,递归法如下,递归合并到树t1上。
import java.util.*;
/*
* public class TreeNode {
* int val = 0;
* TreeNode left = null;
* TreeNode right = null;
* }
*/
public class Solution {
/**
*
* @param t1 TreeNode类
* @param t2 TreeNode类
* @return TreeNode类
*/
public TreeNode mergeTrees (TreeNode t1, TreeNode t2) {
// write code here
//合并到t1上
if(t1 == null && t2 == null){
return null ;
}
if(t1 == null){
return t2 ;
}
if(t2 == null){
return t1 ;
}
t1.val = t1.val + t2.val ;
t1.left = mergeTrees(t1.left,t2.left) ;
t1.right = mergeTrees(t1.right,t2.right) ;
return t1 ;
}
}
迭代法如下:
建立一个队列,层次遍历的方式,累加相应的节点值。
import java.util.*;
/*
* public class TreeNode {
* int val = 0;
* TreeNode left = null;
* TreeNode right = null;
* }
*/
public class Solution {
/**
*
* @param t1 TreeNode类
* @param t2 TreeNode类
* @return TreeNode类
*/
public TreeNode mergeTrees (TreeNode t1, TreeNode t2) {
// write code here
//合并到t1上
if(t1 == null && t2 == null){
return null ;
}
if(t1 == null){
return t2 ;
}
if(t2 == null){
return t1 ;
}
Queue<TreeNode> queue = new LinkedList() ;
queue.offer(t1) ;
queue.offer(t2) ;
while(!queue.isEmpty()){
TreeNode node1 = queue.poll() ;
TreeNode node2 = queue.poll() ;
node1.val = node1.val + node2.val ;
if(node1.left != null && node2.left != null){
queue.offer(node1.left) ;
queue.offer(node2.left) ;
}
if(node1.right != null && node2.right != null){
queue.offer(node1.right) ;
queue.offer(node2.right) ;
}
if(node1.left == null && node2.left != null){
node1.left = node2.left ;
}
if(node1.right == null && node2.right != null){
node1.right = node2.right ;
}
}
return t1 ;
}
}
3.什么是跳表?
1)跳表,又叫做跳跃表、跳跃列表,在有序链表的基础上增加了“跳跃”的功能。
2)跳表在原来的有序链表上加上了多级索引,通过索引来快速查找;可以支持快速的删除、插入和查找操作。
3)跳表实际上是一种增加了前向指针的链表,是一种随机化的数据结构。
4)Redis中 的 SortedSet、LevelDB 中的 MemTable 都用到了跳表。
5)对比平衡树, 跳表的实现和维护会更加简单, 跳表的搜索、删除、添加的平均时间复杂度是 O(logn)。
参考这篇博客:跳表(Skip List)_wink22的博客-CSDN博客_跳表
4.Spring的特性?
三大特性:依赖注入(DI),控制反转(IOC),面向切面编程(AOP).
可以参考这篇博客:【Spring篇】Spring的三大特性_我是不贪嘴吖的博客-CSDN博客_spring特性
5.mysql事务特性?mysql隔离级别?mysql怎么实现可重复读的呢?
ACID:原子性,一致性,隔离性,持久性
读未提交,读已提交,可重复读,串行化。
可重复读是Mysql的默认隔离级别,就是指在同一事务中多次读取的数据是一致的。
实现方法是 Mysql通过多版本并发控制(MVCC):我们常说的MVCC是由MySQL数据库InnoDB存储引擎实现的,并非是由MySQL本身实现的,不同的存储引擎,对MVCC都有不同的实现。就是在InnoDB引擎中,InnoDB在每行记录后面保存两个隐藏的列,分别保存了这个行的创建时间(版本号)和行的删除时间(版本号)。这里存储的并不是实际的时间值,而是系统版本号,当数据被修改时,版本号加1。在读取事务开始时,系统会给当前读事务一个版本号,然后,事务会读取版本号<=当前版本号的数据。此时如果其他写事务修改了这条数据(增删改),那么这条数据的版本号就会加1,从而比当前事务的版本号高,所以,当前事务读取的并不是更新后的数据,而是当前事务对应版本下的数据。
参考链接:mysql 可重复读的实现原理_Vance.的博客-CSDN博客_mysql可重复读实现原理
6.讲一下B树,B+树,哈希表,AVL树,红黑树?
这2篇介绍了红黑树,B树,B+树,讲的还可以:为什么要使用红黑树,B树和B+树_-停泊的博客-CSDN博客_索引 为什么用红黑树
B树、B+树、红黑树_prefect_start的博客-CSDN博客_b树 b+树 红黑树
这篇文章看完了,写的比较全面:面试题
7.怎么保证HashMap的线程安全?
通过ConcurrentHashMap线程安全类实现:JDK1.7通过对segment加锁,片段锁。JDK1.8降低了锁的粒度,通过对头结点加锁来保证线程安全的。
看完理论这篇博客,讲的还可以:ConcurrentHashMap是如何保证线程安全的?_Tom弹架构的博客-CSDN博客
8. CAS(Compare And Swap) 、synchronized、volatile?
1、 synchronized 是悲观锁,属于抢占式,会引起其他线程阻塞。
2、 volatile 提供多线程共享变量可见性和禁止指令重排序优化。
3、 CAS 是基于冲突检测的乐观锁(非阻塞)。
参考这篇,写的很好:基础篇:详解锁原理,synchronized、volatile+cas底层实现 - 知乎
9.ThreadLocal是否可保证资源的同步?它会导致内存泄漏?
- 当使用ThreadLocal声明变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本
- 从上面的概念可知,ThreadLocal其实并不能保证变量的同步性,只是给每一个线程分配一个变量副本
hreadLocalMap
是使用ThreadLocal
的弱引用作为Key
的,弱引用的对象在 GC 时会被回收。ThreadLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal没有外部强引用来引用它,那么系统 GC 的时候,这个ThreadLocal势必会被回收,这样一来,ThreadLocalMap中就会出现key为null的Entry,就没有办法访问这些key为null的Entry的value,如果当前线程再迟迟不结束的话,这些key为null的Entry的value就会一直存在一条强引用链:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value永远无法回收,造成内存泄漏。- 每次使用完
ThreadLocal
,都调用它的remove()
方法,清除数据,防止内存泄漏。
参考这篇博文:ThreadLocal使用注意:线程不安全,可能会发生内存泄漏_深山猿的博客-CSDN博客_threadlocal有什么隐患
10.jvm 垃圾回收是什么时候触发?
参考这篇博客:jvm :垃圾回收是什么时候触发? 垃圾回收算法? 有哪些垃圾回收器?_花和尚也有春天的博客-CSDN博客
11.线程池有哪几种类型?
1、newCachedThreadPool:创建一个可缓存线程池,如果线程池长度超过处理所需,可灵活回收空闲线程,若线程数不够,则新建线程。
2、newFixedThreadPool:创建一个固定大小的线程池,即指定线程数的线程。可控制并发的线程数量,如果工作线程数量达到线程池初始的最大数,则将提交的任务存入到池队列中。
3、newSingleThreadExecutor:创建一个单线程的线程池,即只创建唯一的工作者线程来执行任务,,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
4、newScheduleThreadPool:创建一个定长的线程池,支持定时及周期性任务执行。
推荐通过 new ThreadPoolExecutor() 的写法创建线程池,这样写线程数量更灵活,开发中多数用这个类创建线程。
12.TCP三次握手四次挥手?
TCP的三次握手和四次挥手实质就是TCP通信的连接和断开。
三次握手:为了对每次发送的数据量进行跟踪与协商,确保数据段的发送和接收同步,根据所接收到的数据量而确认数据发送、接收完毕后何时撤消联系,并建立虚连接。
四次挥手:即终止TCP连接,就是指断开一个TCP连接时,需要客户端和服务端总共发送4个包以确认连接的断开。
三次握手过程详细说明:
1、客户端发送建立TCP连接的请求报文,其中报文中包含seq序列号,是由发送端随机生成的,并且将报文中的SYN字段置为1,表示需要建立TCP连接。(SYN=1,seq=x,x为随机生成数值);
2、服务端回复客户端发送的TCP连接请求报文,其中包含seq序列号,是由回复端随机生成的,并且将SYN置为1,而且会产生ACK字段,ACK字段数值是在客户端发送过来的序列号seq的基础上加1进行回复,以便客户端收到信息时,知晓自己的TCP建立请求已得到验证。(SYN=1,ACK=x+1,seq=y,y为随机生成数值)这里的ack加1可以理解为是确认和谁建立连接;
3、客户端收到服务端发送的TCP建立验证请求后,会使自己的序列号加1表示,并且再次回复ACK验证请求,在服务端发过来的seq上加1进行回复。(SYN=1,ACK=y+1,seq=x+1)。
四次挥手过程详细说明:
1、客户端发送断开TCP连接请求的报文,其中报文中包含seq序列号,是由发送端随机生成的,并且还将报文中的FIN字段置为1,表示需要断开TCP连接。(FIN=1,seq=x,x由客户端随机生成);
2、服务端会回复客户端发送的TCP断开请求报文,其包含seq序列号,是由回复端随机生成的,而且会产生ACK字段,ACK字段数值是在客户端发过来的seq序列号基础上加1进行回复,以便客户端收到信息时,知晓自己的TCP断开请求已经得到验证。(FIN=1,ACK=x+1,seq=y,y由服务端随机生成);
3、服务端在回复完客户端的TCP断开请求后,不会马上进行TCP连接的断开,服务端会先确保断开前,所有传输到A的数据是否已经传输完毕,一旦确认传输数据完毕,就会将回复报文的FIN字段置1,并且产生随机seq序列号。(FIN=1,ACK=x+1,seq=z,z由服务端随机生成);
4、客户端收到服务端的TCP断开请求后,会回复服务端的断开请求,包含随机生成的seq字段和ACK字段,ACK字段会在服务端的TCP断开请求的seq基础上加1,从而完成服务端请求的验证回复。(FIN=1,ACK=z+1,seq=h,h为客户端随机生成)
至此TCP断开的4次挥手过程完毕。
参考这篇博客:一文搞懂TCP的三次握手和四次挥手_不脱发的程序猿的博客-CSDN博客_三次握手和四次挥手
注意:TCP两次握手行不行?
因为TCP协议是双向的,第三次握手是为了使得sever知道客户答应了连接的请求。其中两次握手只能确定从客户端到服务端的网络是可达的,但却无法保证从服务端到客户端的网络是可达的。所以我们一定要保证双向的可达。
还有一个重要的原因:seq(序号)的初始化,要互相确定两方互相开始发送的序号,便于之后的发送、控制、纠错等等。
假如我们使用两次握手,则我们可以设想这么一种情况:客户向服务器进行请求,服务器同意并分配资源,向客户也发送信息,但此时这条信息因为网络原因丢失,客户便不了解服务器收没收到它发出的信息,则客户会继续向服务器发出请求,服务器若接收到,会认为是别的机器进行请求,继续分配资源,且也依然无法建立从服务器到客户的连接。
13.OSI七层和TCP/IP四层?OSI五层?
看下面一个图就可以大概了解了,如下:应用层、表示层、会话层可以归为应用层,数据链路层和物理层可以归结为网络接口层。
参照这篇博客:OSI七层和TCP/IP四层、五层协议 - 一切随风
14.算法题:两个有序链表合并?
思路1:迭代法,定义一个头节点为0的,依次遍历合并即可,最后再把头节点去掉。
/*
public class ListNode {
int val;
ListNode next = null;
ListNode(int val) {
this.val = val;
}
}*/
public class Solution {
public ListNode Merge(ListNode list1,ListNode list2) {
//迭代法
if(list1 == null){
return list2 ;
}
if(list2 == null){
return list1 ;
}
ListNode pHead = new ListNode(0) ;
ListNode cur = pHead ;
while(list1 != null && list2 != null){
if(list1.val <= list2.val){
cur.next = list1 ;
list1 = list1.next ;
}else{
cur.next = list2 ;
list2 = list2.next ;
}
cur = cur.next ;
}
//哪个链表还未结束,接在后面
cur.next = list1 != null ? list1 : list2 ;
//去掉头节点的0
return pHead.next ;
}
}
思路2:递归法。递归合并。
/*
public class ListNode {
int val;
ListNode next = null;
ListNode(int val) {
this.val = val;
}
}*/
public class Solution {
public ListNode Merge(ListNode list1,ListNode list2) {
//递归法
if(list1 == null){
return list2 ;
}
if(list2 == null){
return list1 ;
}
//若list1的值更小,则递归合并后接到list1上
if(list1.val <= list2.val){
list1.next = Merge(list1.next,list2) ;
return list1 ;
}else{//反之,递归合并接到list2上
list2.next = Merge(list1,list2.next) ;
return list2 ;
}
}
}
15.算法题:反转链表
题目链接:反转链表_牛客题霸_牛客网
思路1:在遍历链表时,将当前节点的next 指针改为指向前一个节点。由于节点没有引用其前一个节点,因此必须事先存储其前一个节点。在更改引用之前,还需要存储后一个节点。最后返回新的头引用。
/*
public class ListNode {
int val;
ListNode next = null;
ListNode(int val) {
this.val = val;
}
}*/
public class Solution {
public ListNode ReverseList(ListNode head) {
ListNode pre = null ;
while(head != null){
//先定义下一个指针,防止被覆盖
ListNode head_next = head.next ;
//让当前元素指向pre
head.next = pre ;
//更新pre
pre = head ;
//更新当前元素
head = head_next ;
}
return pre ;
}
}
递归法:每次递归反转两个,如下:
/*
public class ListNode {
int val;
ListNode next = null;
ListNode(int val) {
this.val = val;
}
}*/
public class Solution {
public ListNode ReverseList(ListNode head) {
//递归法
if(head == null || head.next == null){
return head ;
}
ListNode ans = ReverseList(head.next) ;
head.next.next = head ;
head.next = null ;
return ans ;
}
}