Java面试资料个人整理

408基础

一、数据结构与算法


Q:递归、迭代、分治、回溯、动规、贪心的概念
A:
递归的本质是将原问题拆分成具有相同性质的子问题,递归解法的特点有两个,分别是子问题拆分方程和终止条件。 递归的调用栈会有深度限制。

迭代和递归本质可以说是一样的, 只是我们模拟了递归的调用栈而已,因此迭代有时候会用到栈这样的数据结构。

分治算法的特征一般是先将原问题拆分成若干个子问题(分解),然后求解子问题(终止条件),最后将各个子问题的解合并,形成原问题的解(合并)。(在求解一些问题时,有很多重复的子问题,这样会导致算法非常低效,这个时候就可以加缓存,也就是带备忘录的递归解法)

动态规划的特征:
最优子结构:在自下而上的递推过程中,我们求得的每个子问题一定是全局最优解,既然它分解的子问题是全局最优解,那么依赖于它们解的原问题自然也是全局最优解。
重叠子问题:在求解原问题的时候,我们往往需要依赖其子问题,子问题依赖其子子问题,甚至可能同时依赖多个子问题,因此子问题之间是有重叠关系的。
状态初始化:因为动态规划是依赖子问题的,因此需要先初始化已知状态,从而才能自下而上的递推到原问题得解。
遍历状态集:首先根据需要求解的结果来确定状态集,然后遍历状态集,更新状态 dp
状态转移方程:也就是每一级子问题之间的关系式。

回溯算法是一种通过探索所有可能的候选解来找出所有的解的算法。如果候选解被确认不是一个解的话(或者至少不是最后一个解),回溯算法会通过在上一步进行一些变化抛弃该解,即回溯并且再次尝试。

贪心算法(代表算法有Prim, kruskal, Dijkstra, Huffman )是指,在对问题求解时,总是做出在当前看来是最好的选择。也就是说,不从整体最优上加以考虑,它所做出的仅仅是在某种意义上的局部最优解。
贪心算法的特征:
最优子结构:贪心算法也是将原问题分解成多个性质相同的子问题,每个问题都是局部最优。
贪心选择性质:在做贪心选择时,我们直接做出当前子问题中看起来最优的解,这也是贪心算法和动态规划的不同之处。
遍历状态集:遍历状态集,做出局部最优选择,更新结果。
无后效性: 某个状态以后的过程不会影响以前的状态,只与当前状态有关

Q:DFS 的基本思想和 BFS 的基本思想
A:
DFS (深度优先搜索)类似于二叉树的先序遍历,它的基本思想是首先访问出发点并将其标记为已访问,然后选取与起始点邻接的未被访问的任意一个顶点标记并递归访问,再选择与上一标记顶点邻接的未被访问的顶点标记并递归访问, 以此重复进行。当一个顶点的所有邻接顶点都被访问过时,则依次退回到最近被访问过的顶点,若该顶点还有其他邻接顶点未被访问,则从这些未被访问的顶点中取一个并重复上述递归访问过程, 直到途中所有的顶点都被访问过为止。
BFS(广度优先搜索) 类似于树的层序遍历, 需要使用队列这一数据结构, 它的基本思想是首先访问起始顶点入队并标记为已访问,当队列不空时循环执行访问操作,即出队并依次检查出队顶点的所有邻接顶点,访问没有被访问过的邻接顶点并将其入队。当队列为空时跳出循环,广度优先搜索即完成。

Q:Kruskal 算法的基本思想
A:
首先构造一个只有 n 个顶点没有边的非连通图,给所有的边按值以从小到大的顺序排序,选择一个最小权值边,若该边的两个顶点在不同的连通分量上,加入到有效边中,否则舍去这条边,重新选择权值最小的边,以此重复下去,直到所有顶点在同一连通分量上。

Q:Prim算法的基本思想
A:
从某顶点 u0 出发,选择与它关联的具有最小权值的边(u0, v),将其顶点加入到生成树的顶点集合U中。每一步从U中挑选一个顶点u,而另一个顶点不在U中的各个顶点中选择权值最小的边(u, v),把它的顶点加入到U中,直到所有顶点都加入到生成树顶点集合U中为止。

Q:简述链表和数组的优缺点
A:
数组的优点: 随机访问性强, 查找速度快
数组的缺点: 插入和删除效率低, 可能浪费内存, 内存空间要求高,必须有足够的连续内存空间。数组大小固定,不能动态拓展
链表的优点: 插入删除速度快, 内存利用率高,不会浪费内存, 大小没有固定,拓展灵活。
链表的缺点: 不能随机查找,必须从第一个开始遍历,查找效率低

Q:Heap 和 Stack 的概念及区别
A:
一、堆栈空间分配区别:
1、栈(操作系统):由操作系统自动分配释放 ,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈;
2、堆(操作系统): 一般由程序员分配释放, 若程序员不释放,程序结束时可能由 OS 回收,分配方式倒是类似于链表。
二、堆栈缓存方式区别:
1、栈使用的是一级缓存, 他们通常都是被调用时处于存储空间中,调用完毕立即释放;
2、堆是存放在二级缓存中,生命周期由虚拟机的垃圾回收算法来决定(并不是一旦成为孤儿对象就能被回收)。所以调用这些对象的速度要相对来得低一些。
三、堆栈数据结构区别:
堆(数据结构):堆可以被看成是一棵树,如:堆排序;
栈(数据结构):一种先进后出的数据结构。

Q:哈希表最好最坏情况下复杂度?
A:
O(1)和 O(n), n 为表长。

Q:求二叉树的直径?
A:
两次 DFS,第一次找出距离 root 最远点,第二次以第一次结果为起点找出第二个点,这两点的距离即为直径。

Q:
A:

Q:
A:

二、操作系统


Q:进程和线程的区别
A:
根本区别:进程是资源分配最小单位,线程是程序执行的最小单位。
地址空间:进程有自己独立的地址空间,每启动一个进程,系统都会为其分配地址空间,建立数据表来维护代码段、堆栈段和数据段;线程没有独立的地址空间,同一进程的线程共享本进程的地址空间。
资源拥有:进程之间的资源是独立的;同一进程内的线程共享本进程的资源。
执行过程:每个独立的进程有一个程序运行的入口、顺序执行序列和程序入口。但是线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。线程是处理机调度的基本单位,但是进程不是。由于程序执行的过程其实是执行具体的线程,那么处理机处理的也是程序相应的线程,所以处理机调度的基本单位是线程。
系统开销:进程执行开销大,线程执行开销小

Q:系统调用的定义?
A:
系统调用是 OS 与应用程序之间的接口,它是用户程序取得 OS 服务的唯一途径。 它与一般的过程调用的区别:运行在不同的系统状态, 调用程序运行在用户态,而被调用的程序运行在系统态,通过软中断机制,先由用户态转为系统态,经核心分析后,才转向相应的系统调用处理子程序;一般的过程调用返回后继续执行, 但对系统调用,当调用进程仍具有最高优先权时,才返回到调用进程继续处理,否则只能等被重新调度。

Q:解释一下管程
A:
管程是由一组局部变量、 对局部变量进行操作的一组过程和对局部变量进行初始化的语句序列组成。引入它的目的是因为 Wait/Singal 操作太过分散,对它的维护很麻烦且容易造成死锁。管程的特点是:管程的过程只能访问管程的局部变量,管程的局部变量只能由其过程来访问;任何时刻只能有一个进程进入管程执行;进程只能通管程提供的过程入口进入管程。

Q:在可变分区管理中,需要哪些硬件机制?
A:
采用可变分区方式管理时,一般均采用动态重定位方式装入作业。地址变换要靠硬件支持,主要是两个寄存器:基址寄存器和限长寄存器,限长寄存器存放作业所占分区的长度,基址寄存器则存放作业所占分区的起始地址,这两个值确定了一个分区的位置和大小。
转换时根据逻辑地址与限长值比较,如果不有超过这个值,表示访问地址合法,再加上基址寄存器中的值就得到了绝对地址了,否则形成“ 地址越界” 中断。

Q:中断和陷入有什么异同
A:
外中断时指来自处理机和内存外部的中断,如 I/O 中断、定时器中断、外部信号中断等。狭义上也叫中断;
内中断主要指在处理机和内存内部产生的中断,也称陷入,如校验错、页面失效、溢出、除数为零等。
中断和陷阱的主要区别:
(1) 陷入通常由处理机正在执行的现行指令引起,而中断则是由与现行指令无关的中断源引起的。
(2) 陷阱处理程序提供的服务为当前进程所用,而中断处理程序提供的服务则不是为了当前进程的。
管理时,一般均采用动态重定位方式装入作业。地址变换要靠硬件支持,主要是两个寄存器:基址寄存器和限长寄存器,限长寄存器存放作业所占分区的长度,基址寄存器则存放作业所占分区的起始地址,这两个值确定了一个分区的位置和大小。转换时根据逻辑地址与限长值比较,如果不有超过这个值,表示访问地址合法,再加上基址寄存器中的值就得到了绝对地址了,否则形成“ 地址越界” 中断。

Q:中断和系统调用区别?
A:
中断:解决处理器速度和硬件速度不匹配,是多道程序设计的必要条件。每个中断都有自己的数字标识,当中断发生时,指令计数器 PC 和处理机状态字 PSW中的内容自动压入处理器堆栈,同时新的 PC 和 PSW 的中断向量也装入各自的寄存器中。这时, PC 中包含的是该中断的中断处理程序的入口地址,它控制程序转向相应的处理,当中断处理程序执行完毕,该程序的最后一条 iret(中断返回),它控制着恢复调用程序的环境。
中断和系统调用的区别: 中断是由外设产生,无意的, 被动的 系统调用是由应用程序请求操作系统提供服务产生, 有意的,主动的。要从用户态通过中断进入内核态。
(联系)
中断过程:中断请求 中断响应 断点保护 执行中断服务程序 断点恢复 中断返回
系统调用过程:应用程序在用户态执行时请求系统调用,中断,从用户态进入内核态,在内核态执行相应的内核代码。

Q:操作系统的特点?
A:
共享:资源可被多个并发执行的进程使用
并发:可以在同一时间间隔处理多个进程,需要硬件支持
虚拟:将物理实体映射成为多个虚拟设备
异步:进程执行走走停停,每次进程执行速度可能不同,但 OS 需保证进程每次执行结果相同

Q:进程的三个组成部分?
A:
程序段、数据段、 PCB(Process Control Block)

Q:并发与并行区别?
A:
并发:同一间隔 并行:同一时刻

Q:进程切换的过程?
A:
保持处理机上下文 -> 更新 PCB -> 把 PCB 移入相应队列(就绪、阻塞) ->选择另一个进程并更新其 PCB -> 更新内存管理的数据结构 -> 恢复处理机上下文

Q:进程通信的方式
A:
1、低级通信方式
PV 操作(信号量机制)。
– P: wait(S)原语,申请 S 资源
– V: signal(S)原语,释放 S 资源
2、高级通信方式:以较高效率传输大量数据的通信方式
– 共享存储(使用同步互斥工具操作共享空间)
– 消息传递(进程间以格式化的消息进行数据交换,有中间实体,分为直接和间接两种,底层通过发送消息和接收消息两个原语实现)
– 管道通信(两个进程中间存在一个特殊的管道文件,进程的输入输出都通过管道,半双工通信)

Q:死锁的必要条件?
A:
– 互斥条件:资源在某一时刻只能被一个进程占有
– 不剥夺条件:进程所持有的资源在主动释放前不能被其他进程强行夺走
– 请求和占用条件:死锁进程必然是既持有资源又在申请资源的
– 循环等待条件:存在等待链,互相申请,互不释放

Q:死锁与饥饿的区别?
A:
– 都是资源分配问题
– 死锁是等待永远不会释放的资源,而饥饿申请的资源会被释放,只是永远不会分配给自己
– 一旦产生死锁,则死锁进程必然是多个,而饥饿进程可以只有一个
– 饥饿的进程可能处于就绪状态,而死锁进程一定是阻塞进程

Q:FCB 包含什么?
A:
文件指针:上次读写位置。
文件打开数:多少个进程打开了此文件。
文件磁盘位置。
文件的访问权限:创建、只读、读写等。

Q:页面置换算法?
A:
最佳置换算法 OPT
先进先出置换算法 FIFO
最近最久未使用算法 LRU
时钟算法 LOCK
改进型时钟算法

Q:批处理作业调度算法?
A:
先来先服务 FCFS
最短作业优先 SJF
最高响应比优先 HRN
多级队列调度算法

Q:进程调度算法?
A:
先进先出 FIFO
时间片轮转算法 RR
最高优先级算法 HPF
多级队列反馈算法

Q:磁盘调度算法?
A:
先来先服务 FCFS
最短寻道时间优先 SSTF
扫描算法 SCAN
循环扫描算法 C-SCAN

Q:局部性原理
A:
程序的时间局部性是指程序在运行时呈现出局部性规律,在一段时间间隔内,程序的执行是局限在某个部份,所访问的存储空间也只局限在某个区域。
程序的空间局部性是指若一个存储单元被访问,那么它附近的单元也可能被访问,这是由于程序的顺序执行引起的。

Q:fork 一个进程和生成一个线程有什么区别?
A:
当你 fork 一个进程时,新的进程将执行和父进程相同的代码,只是在不同的内存空间中。
但当你在已有进程中生成一个线程时,它会生成一个新的代码执行路线,但共享同一个内存空间。

Q:
A:

Q:
A:

三、计算机网络


Q:IP 层的协议有哪些?
A:
ICMP 协议: ICMP 协议是指英文全称(Internet Control Message Protocol),就是网际控制信息协议。主要是用于补充 IP 传输数据报的过程中,发送主机无法确定数据报是否到达目标主机。 ICMP 报文分为出错报告报文和查询报文两种。若数据报不能到达目标主机, ICMP 出错报告报文可以以回送信息的方式,向源主机发去信息,并不能纠抄正数据报中的任何出错。除了出错报告, ICMP 还可以诊断出某些网络问题,这就是 ICMP 的查询报文。
IGMP 协议: IGMP 协议是指英文全称(Internet Group Management Protocol),网络组管理协议。主要用于建立和管理多播组,对 IP 分组广播进行控制

Q:简述网卡的功能
A:
1、网卡要进行串行/并行转换:网卡和局域网之间的通信是通过电缆或双绞线以串行传输方式进行的。而网卡和计算机之间的通信则是通过计算机主板上的 I/O 总线以并行传输方式进行。
2、网卡能实现以太网协议:在安装网卡时必须将管理网卡的设备驱动程序安装在计算机的操作系统中。这个驱动程序以后就会告诉网卡,应当从存储器的什么位置上将局域网传送过来的数据块存储下来。
3、网卡能处理正确的帧:当网卡收到一个有差错的帧时,它就将这个帧丢弃而不必通知它所插入的计算机。当网卡收到一个正确的帧时,它就使用中断来通知该计算机并交付给协议栈中的网络层。当计算机要发送一个 IP 数据包时,它就由协议栈向下交给网卡组装成帧后发送到局域网。

Q:TCP 和 UDP 的区别?
A:
TCP 可靠, UDP 不可靠。
TCP 只支持点对点服务, UDP 可以一对一、一对多、多对一和多对多。
TCP 面向连接, UDP 无连接。
UDP 有较好的实时性,工作效率比TCP 高。
TCP 对系统资源要求多, UDP 则无。

Q:UDP 的优点?
A:
发送前无需连接,减少了开销和时延,首部开销小,无拥塞控制,方便实时应用,不保证可靠交付,无需维持连接状态表。 UDP 的可靠性要通过应用层来控制。

Q:RIP 和 OSPF
A:
RIP(Routing Information Protocol)在应用层,最大站点数为 15
OSPF(Open Shortest Path First)网络层,洪泛法,迪杰斯特拉算法

Q:DNS 工作过程?
A:
应用层协议,使用 UDP。分为迭代查询和递归查询。采用分布式集群的工作方式,防止单点故障,增加通信容量。
迭代:主机访问本地域名服务器,若缓存没有 IP 则本地域名服务器进一步向其他根域名服务器查询。
递归:主机分别向多个服务器发出查询请求。

Q:解释 TCP/IP 的三次握手
A:
step1:首先客户机 TCP 向服务器 TCP 发送连接请求报文,报文中 SYN=1,随机发送一个序号 seq=x;
step2:服务器 TCP 接收到连接请求报文后,若同意连接,则返回确认报文,且为该 TCP 连接分配 TCP 变量和资源,确认报文中 SYN=1, ACK(确认位字段) =1,ack
(确认号字段) =x+1, seq=y;
step3:客户机接收到服务器的确认连接报文后,同样返回确认报文,且为该 TCP连接分配 TCP 变量和资源,确认报文中, ACK=1, ack=y+1, seq=x+1。

Q:虚电路和数据报的区别
A:
虚电路和数据报都是分组交换技术。
①数据报是无连接的数据交换,而虚电路是面向连接的数据交换;②数据报的分组都是通过独立的路由选择和转发,而同属于一条虚电路的分组按照同一路由转发;
③数据报不保证数据的可靠交付,虚电路可靠性由网络保证;
④数据报不保证分组的有序到达,虚电路保证分组的有序到达。

1,http和https区别

HTTPS和HTTP的区别主要如下:

Https协议不是一种新的协议,它就是Http协议和SSL(现在叫TLS协议)协议组合而成的协议,所以他们的不同就是SSL的作用。(Hyper Text Transfer Protocol Hyper Text Transfer Protocol over SecureSocket Layer。)

1、相比较比http,https协议加上了加密处理(共享密钥加密(公开密钥加密,私有密钥解密)和对称密码加密(加密和解密使用同样的密钥))和认证(第三方机构提供的证书)以及完整性保护(应用层发送数据的时候会附加MAC消息验证码的报文摘要,能够查到报文是否被篡改)。

这三部分是https相对于http的优点,那现在也仍然有很多网站不使用https,使用http协议,原因就是http也有它的优点

​ 1、http比https要快,因为https使用ssl协议,它的通信慢,CPU和内存的消耗比较大,所以处理速度也会变慢。单位时间里能处理的请求数量就会变少。所以如果不是一些敏感信息,用http更合适。特别对于一些小网站来说负载太多根本承担不了。

​ 2、最后一点就是购买申请证书也是要钱的,用http就可以节约成本。

这就是就是http和https各自的优缺点,也就是他们的区别。

3、http和https用的端口也不一样,前者是80,后者是443

2,三次握手四次挥手

三次握手(Three-way Handshake)就是建立一个TCP连接的时候,需要客户端和服务器端一共发送3个包.

主要作用就是彼此.确定双方的接收能力和发送能力是否正常

为什么要三次握手,两次不行嘛?

不行,三次握手他的这个流程是这样.第一次的握手,客户端向服务器端发包,此时服务器端接受到了的话,服务器端就知道了客户端的发送能力没有问题,自己的接受能力没有问题.第二次握手服务器端向客户端发包,客户端接收到的话,客户端就知道了自己和服务器端的发送和接收能力都没有问题.但这时候服务器端是不知道自己的发送能力和客户端的接收能力有没有问题的,所以需要第三次握手.客户端要再发一次包给服务器端.如果这时候服务器端接收到了.证明双方的发送能力和接收能力都没有问题.

四次挥手

建立一个连接需要三次握手,而终止一个连接要经过四次挥手(也有将四次挥手叫做四次握手的)。这由TCP的半关闭(half-close)造成的。所谓的半关闭,其实就是TCP提供了连接的一端在结束它的发送后还能接收来自另一端数据的能力。

TCP 的连接的拆除需要发送四个包,因此称为四次挥手(Four-way handshake),客户端或服务器均可主动发起挥手动作。

刚开始双方都处于 ESTABLISHED 状态,假如是客户端先发起关闭请求。四次挥手的过程如下:

第一次挥手:客户端发送一个 FIN 报文,报文中会指定一个序列号。此时客户端处于 FIN_WAIT1 状态。

即发出连接释放报文段(FIN=1,序号seq=u),并停止再发送数据,主动关闭TCP连接,进入FIN_WAIT1(终止等待1)状态,等待服务端的确认。

第二次挥手:服务端收到 FIN 之后,会发送 ACK 报文,且把客户端的序列号值 +1 作为 ACK 报文的序列号值,表明已经收到客户端的报文了,此时服务端处于 CLOSE_WAIT 状态。

即服务端收到连接释放报文段后即发出确认报文段(ACK=1,确认号ack=u+1,序号seq=v),服务端进入CLOSE_WAIT(关闭等待)状态,此时的TCP处于半关闭状态,客户端到服务端的连接释放。客户端收到服务端的确认后,进入FIN_WAIT2(终止等待2)状态,等待服务端发出的连接释放报文段。

第三次挥手:如果服务端也想断开连接了,和客户端的第一次挥手一样,发给 FIN 报文,且指定一个序列号。此时服务端处于 LAST_ACK 的状态。

即服务端没有要向客户端发出的数据,服务端发出连接释放报文段(FIN=1,ACK=1,序号seq=w,确认号ack=u+1),服务端进入LAST_ACK(最后确认)状态,等待客户端的确认。

第四次挥手:客户端收到 FIN 之后,一样发送一个 ACK 报文作为应答,且把服务端的序列号值 +1 作为自己 ACK 报文的序列号值,此时客户端处于 TIME_WAIT 状态。需要过一阵子以确保服务端收到自己的 ACK 报文之后才会进入 CLOSED 状态,服务端收到 ACK 报文之后,就处于关闭连接了,处于 CLOSED 状态。

即客户端收到服务端的连接释放报文段后,对此发出确认报文段(ACK=1,seq=u+1,ack=w+1),客户端进入TIME_WAIT(时间等待)状态。此时TCP未释放掉,需要经过时间等待计时器设置的时间2MSL后,客户端才进入CLOSED状态。

收到一个FIN只意味着在这一方向上没有数据流动。客户端执行主动关闭并进入TIME_WAIT是正常的,服务端通常执行被动关闭,不会进入TIME_WAIT状态。

在socket编程中,任何一方执行close()操作即可产生挥手操作。

2.1 挥手为什么需要四次?

因为当服务端收到客户端的SYN连接请求报文后,可以直接发送SYN+ACK报文。其中ACK报文是用来应答的,SYN报文是用来同步的。但是关闭连接时,当服务端收到FIN报文时,很可能并不会立即关闭SOCKET,所以只能先回复一个ACK报文,告诉客户端,“你发的FIN报文我收到了”。只有等到我服务端所有的报文都发送完了,我才能发送FIN报文,因此不能一起发送。故需要四次挥手。 

3,doGet、doPost


get、post是HTTP协议里面的两种方法(此外还有head、delete)

get、post两种方法有本质上没有区别。

因为,Http的底层是或者说是基于TCP/IP协议。所以Get和Post的能做的事情是一样的。你可以给Get方法加上request body,也可以给post方法在url中加上参数。但是约定俗成,浏览器会限制url的长度,而且服务器针对藏在requestbody中的get方法提交的数据,有时候不进行接收和处理,这就是他们的本质。
所以说完本质后,通常情况下有区别:

get只有一个流,提交请求的时候,参数是附在url后面的,大小个数都有限制而且只能是字符串。post方法的参数是使用另外的流进行传递的,并不通过url,所以大小个数都很大,而且可以传递二进制数据,如文件的上传。所以在传输大数据的时候选择post方法。
get方法产生一个TCP数据包,而post方法产生两个产生两个TCP数据包。get只需要发送一次就可以header和data发送出去,而post则需要发送两次才能算一次请求成功。所以post发送耗费的时间比较多。也就是说get方法的效率高一点。
doGet()和doGost()是servlet中处理get、post方法的

可以在把方法写在doGet中,在doPost中去调用执行,这样无论不管你用哪种方法提交,都会执行。

四、计算机组成原理


Q:为了实现重定位需要哪些硬件?
A:
最简单的方式是在系统中增设一个重定位寄存器,用来存放正在执行作业的内存地址,每次访问数据时,由硬件自动将相对地址和重定位寄存器中的起始地址相加,形成实际的物理地址。当然在分页式与分段式系统中,还有地址变换机构,以及块表等方式。

Q:指令和数据放在一起存储的,计算机是如何区分指令和数据的?
A:
方式一:通过不同时间段来区分指令和数据,即在取指令阶段(或取值微指令)取出的为指令,在执行指令阶段(或相应微程序)取出的即为数据。
方式二:通过地址来源区分,由 PC 提供存储单元地址的取出的是指令,由指令地址码部分提供存储单元地址的取出的是操作数。

Q:
A:

Q:
A:

Q:
A:

五、程序设计基础


Q:重载和重写的区别
A:
方法的重载和重写都是实现多态的方式,区别在于前者实现的是编译时的多态性,而后者实现的是运行时的多态性。
重载发生在一个类中, 同名的方法如果有不同的参数列表(参数类型不同、参数个数不同或者二者都不同)则视为重载;重写发生在子类与父类之间,重写要求子类被重写方法与父类被重写方法有相同的参数列表,有兼容的返回类型,比父类被重写方法更好访问,不能比父类被重写方法声明更多的异常(里氏代换原则)。重载对返回类型没有特殊的要求,不能根据返回类型进行区分。

Q:两个栈模仿一个队列?
A:
进队:入 A 栈。
出队:若 B 栈不为空,则 B 栈全部出栈;否则将 A 栈中数据全部入 B 栈,再依次出 B 栈。

Q:两个队列模仿一个栈?
A:
入栈:入 A 队
出栈:将 A 队除队尾元素全部转移到 B 队,出 A 队, 算法思想就是两个队列倒来倒去,只留一个元素时出栈。

Q:如何判断链表是否有环?
A:
设置快慢指针,快指针每次前进两步,当两指针重合则有环,快指针为 null则无环。

Q:如何判断有环链表环的入口?
A:
1、将遍历过的结点都入 set,如果当前结点在 set 里有,则此结点即为入口。
2、快慢指针重合后,重置 fast 指针,此时 fast 每次走一步,再次重合结点即
为入口。

Q:最长公共子序列求解(LCS)?
A:
DP。由最长公共子序列问题的最优子结构性质可知,要找出 X=<x1, x2, …,xm>和 Y=<y1, y2, …, yn>的最长公共子序列,可按以下方式递归地进行: 当xm=yn 时,找出 Xm-1 和 Yn-1 的最长公共子序列,然后在其尾部加上 xm(=yn)即可得 X 和 Y 的一个最长公共子序列。 当 xm≠yn 时,必须解两个子问题,即找出Xm-1 和 Y 的一个最长公共子序列及 X 和 Yn-1 的一个最长公共子序列。这两个公共子序列中较长者即为 X 和 Y 的一个最长公共子序列.


Q:链表能否使用二分查找?
A:
可以。先将链表排序,将各个结点的值记入数组,再二分查找。

Q:给定一颗二叉树的头结点,和这颗二叉树中 2 个节点 n1 和 n2,求这两个节点的最近公共祖先
A:
后序遍历方法

public class Solution {
    public TreeNode lowes tCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
        if(root == null|I root == p|| root == q)
            return root;
        TreeNode left = lowestCommonAncestor(root.left, p, q);
        TreeNode right = lowestCommonAncestor(root.right, p, q);
        if(left != null && right != null) return root;
        return left != null ? left : right ;
}

Q:栈应用括号匹配?
A:
左括号入栈,右括号出栈进行匹配,栈空仍未匹配到则失败

Q:汉诺塔问题?
A:

void Hanoi(char src, char des, char via, int n) {
    Hanoi(src, via, des, n - 1);
    Move(src, des, n); //把第n个盘子直接从src移动到des
    Hanoi(via,des, src, n - 1);
}

Q:二叉树删除节点?
A:
1.被删除的节点是叶子节点,这时候只要把这个节点删除,再把指向这个节点的父节点指针置为空就行。
2.被删除的节点有左子树,或者有右子树,而且只有其中一个,那么只要把当前删除节点的父节点指向被删除节点的左子树或者右子树就行。
3.被删除的节点既有左子树而且又有右子树,这时候需要把左子树的最右边的节点(最大前驱结点)或者右子树最左边的节点(最小后驱结点)提到被删除节点的位置。

Q:三种传参方式?
A:
值传递:传递的是实参的一个拷贝,修改形参不会改变实参值。
地址传递:传递的是实参地址的一个拷贝,修改形参不会改变实参值。
引用传递:传递的是实参的一个别名,修改形参会导致改变实参。被调用函数的形参只有在被调用时才会临时分配存储单元,一旦调用结束则释放内存。

Java基础

1.说说有哪些常见集合?

集合相关类和接口都在java.util中,主要分为3种:List(列表)、Map(映射)、Set(集)。

Java集合主要关系

其中Collection是集合ListSet的父接口,它主要有两个子接口:

  • List:存储的元素有序,可重复。

  • Set:存储的元素不无序,不可重复。

Map是另外的接口,是键值对映射结构的集合。

2.ArrayList和LinkedList有什么区别?

(1)数据结构不同

  • ArrayList基于数组实现

  • LinkedList基于双向链表实现

ArrayList和LinkedList的数据结构

(2) 多数情况下,ArrayList更利于查找,LinkedList更利于增删

  • ArrayList基于数组实现,get(int index)可以直接通过数组下标获取,时间复杂度是O(1);LinkedList基于链表实现,get(int index)需要遍历链表,时间复杂度是O(n);当然,get(E element)这种查找,两种集合都需要遍历,时间复杂度都是O(n)。

  • ArrayList增删如果是数组末尾的位置,直接插入或者删除就可以了,但是如果插入中间的位置,就需要把插入位置后的元素都向前或者向后移动,甚至还有可能触发扩容;双向链表的插入和删除只需要改变前驱节点、后继节点和插入节点的指向就行了,不需要移动元素。

    ArrayList和LinkedList中间插入

ArrayList和LinkedList中间删除

注意,这个地方可能会出陷阱,LinkedList更利于增删更多是体现在平均步长上,不是体现在时间复杂度上,二者增删的时间复杂度都是O(n)

(3)是否支持随机访问

  • ArrayList基于数组,所以它可以根据下标查找,支持随机访问,当然,它也实现了RandmoAccess 接口,这个接口只是用来标识是否支持随机访问。

  • LinkedList基于链表,所以它没法根据序号直接获取元素,它没有实现RandmoAccess 接口,标记不支持随机访问。

(4)内存占用,ArrayList基于数组,是一块连续的内存空间,LinkedList基于链表,内存空间不连续,它们在空间占用上都有一些额外的消耗:

  • ArrayList是预先定义好的数组,可能会有空的内存空间,存在一定空间浪费

  • LinkedList每个节点,需要存储前驱和后继,所以每个节点会占用更多的空间

3.ArrayList的扩容机制了解吗?

ArrayList是基于数组的集合,数组的容量是在定义的时候确定的,如果数组满了,再插入,就会数组溢出。所以在插入时候,会先检查是否需要扩容,如果当前容量+1超过数组长度,就会进行扩容。

ArrayList的扩容是创建一个1.5倍的新数组,然后把原数组的值拷贝过去。

ArrayList扩容

4.ArrayList怎么序列化的知道吗?为什么用transient修饰数组?

ArrayList的序列化不太一样,它使用transient修饰存储元素的elementData的数组,transient关键字的作用是让被修饰的成员属性不被序列化。

为什么最ArrayList不直接序列化元素数组呢?

出于效率的考虑,数组可能长度100,但实际只用了50,剩下的50不用其实不用序列化,这样可以提高序列化和反序列化的效率,还可以节省内存空间。

那ArrayList怎么序列化呢?

ArrayList通过两个方法readObject、writeObject自定义序列化和反序列化策略,实际直接使用两个流ObjectOutputStreamObjectInputStream来进行序列化和反序列化。

ArrayList自定义序列化

5.快速失败(fail-fast)和安全失败(fail-safe)了解吗?

快速失败(fail—fast):快速失败是Java集合的一种错误检测机制

  • 在用迭代器遍历一个集合对象时,如果线程A遍历过程中,线程B对集合对象的内容进行了修改(增加、删除、修改),则会抛出Concurrent Modification Exception。

  • 原理:迭代器在遍历时直接访问集合中的内容,并且在遍历过程中使用一个 modCount  变量。集合在被遍历期间如果内容发生变化,就会改变modCount的值。每当迭代器使用hashNext()/next()遍历下一个元素之前,都会检测modCount变量是否为expectedmodCount值,是的话就返回遍历;否则抛出异常,终止遍历。

  • 注意:这里异常的抛出条件是检测到 modCount!=expectedmodCount  这个条件。如果集合发生变化时修改modCount值刚好又设置为了expectedmodCount值,则异常不会抛出。因此,不能依赖于这个异常是否抛出而进行并发操作的编程,这个异常只建议用于检测并发修改的bug。

  • 场景:java.util包下的集合类都是快速失败的,不能在多线程下发生并发修改(迭代过程中被修改),比如ArrayList 类。

安全失败(fail—safe)

  • 采用安全失败机制的集合容器,在遍历时不是直接在集合内容上访问的,而是先复制原有集合内容,在拷贝的集合上进行遍历。

  • 原理:由于迭代时是对原集合的拷贝进行遍历,所以在遍历过程中对原集合所作的修改并不能被迭代器检测到,所以不会触发Concurrent Modification Exception。

  • 缺点:基于拷贝内容的优点是避免了Concurrent Modification Exception,但同样地,迭代器并不能访问到修改后的内容,即:迭代器遍历的是开始遍历那一刻拿到的集合拷贝,在遍历期间原集合发生的修改迭代器是不知道的。

  • 场景:java.util.concurrent包下的容器都是安全失败,可以在多线程下并发使用,并发修改,比如CopyOnWriteArrayList类。

6.有哪几种实现ArrayList线程安全的方法?

fail-fast是一种可能触发的机制,实际上,ArrayList的线程安全仍然没有保证,一般,保证ArrayList的线程安全可以通过这些方案:

  • 使用 Vector 代替 ArrayList。(不推荐,Vector是一个历史遗留类)

  • 使用 Collections.synchronizedList 包装 ArrayList,然后操作包装后的 list。

  • 使用 CopyOnWriteArrayList 代替 ArrayList。

  • 在使用 ArrayList 时,应用程序通过同步机制去控制 ArrayList 的读写。

7.CopyOnWriteArrayList了解多少?

CopyOnWriteArrayList就是线程安全版本的ArrayList。

它的名字叫CopyOnWrite——写时复制,已经明示了它的原理。

CopyOnWriteArrayList采用了一种读写分离的并发策略。CopyOnWriteArrayList容器允许并发读,读操作是无锁的,性能较高。至于写操作,比如向容器中添加一个元素,则首先将当前容器复制一份,然后在新副本上执行写操作,结束之后再将原容器的引用指向新容器。

Map

Map中,毫无疑问,最重要的就是HashMap,面试基本被盘出包浆了,各种问法,一定要好好准备。

8.能说一下HashMap的数据结构吗?

JDK1.7的数据结构是数组+链表,JDK1.7还有人在用?不会吧……

说一下JDK1.8的数据结构吧:

JDK1.8的数据结构是数组+链表+红黑树

数据结构示意图如下:

jdk1.8 hashmap数据结构示意图

其中,桶数组是用来存储数据元素,链表是用来解决冲突,红黑树是为了提高查询的效率。

  • 数据元素通过映射关系,也就是散列函数,映射到桶数组对应索引的位置

  • 如果发生冲突,从冲突的位置拉一个链表,插入冲突的元素

  • 如果链表长度>8&数组大小>=64,链表转为红黑树

  • 如果红黑树节点个数<6 ,转为链表

9.你对红黑树了解多少?为什么不用二叉树/平衡树呢?

红黑树本质上是一种二叉查找树,为了保持平衡,它又在二叉查找树的基础上增加了一些规则:

  1. 每个节点要么是红色,要么是黑色;

  2. 根节点永远是黑色的;

  3. 所有的叶子节点都是是黑色的(注意这里说叶子节点其实是图中的 NULL 节点);

  4. 每个红色节点的两个子节点一定都是黑色;

  5. 从任一节点到其子树中每个叶子节点的路径都包含相同数量的黑色节点;

红黑树

之所以不用二叉树:

红黑树是一种平衡的二叉树,插入、删除、查找的最坏时间复杂度都为 O(logn),避免了二叉树最坏情况下的O(n)时间复杂度。

之所以不用平衡二叉树:

平衡二叉树是比红黑树更严格的平衡树,为了保持保持平衡,需要旋转的次数更多,也就是说平衡二叉树保持平衡的效率更低,所以平衡二叉树插入和删除的效率比红黑树要低。

10.红黑树怎么保持平衡的知道吗?

红黑树有两种方式保持平衡:旋转染色

  • 旋转:旋转分为两种,左旋和右旋

左旋

右旋

  • 染⾊:

染色

11.HashMap的put流程知道吗?

先上个流程图吧:

HashMap插入数据流程图

  1. 首先进行哈希值的扰动,获取一个新的哈希值。(key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);

  2. 判断tab是否位空或者长度为0,如果是则进行扩容操作。

    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    
  3. 根据哈希值计算下标,如果对应小标正好没有存放数据,则直接插入即可否则需要覆盖。tab[i = (n - 1) & hash])

  4. 判断tab[i]是否为树节点,否则向链表中插入数据,是则向树中插入节点。

  5. 如果链表中插入节点的时候,链表长度大于等于8,则需要把链表转换为红黑树。treeifyBin(tab, hash);

  6. 最后所有元素处理完成后,判断是否超过阈值;threshold,超过则扩容。

12.HashMap怎么查找元素的呢?

先看流程图:

HashMap查找流程图

HashMap的查找就简单很多:

  1. 使用扰动函数,获取新的哈希值

  2. 计算数组下标,获取节点

  3. 当前节点和key匹配,直接返回

  4. 否则,当前节点是否为树节点,查找红黑树

  5. 否则,遍历链表查找

13.HashMap的哈希/扰动函数是怎么设计的?

HashMap的哈希函数是先拿到 key 的hashcode,是一个32位的int类型的数值,然后让hashcode的高16位和低16位进行异或操作。

    static final int hash(Object key) {
        int h;
        // key的hashCode和key的hashCode右移16位做异或运算
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

这么设计是为了降低哈希碰撞的概率。

14.为什么哈希/扰动函数能降hash碰撞?

因为 key.hashCode() 函数调用的是 key 键值类型自带的哈希函数,返回 int 型散列值。int 值范围为 -2147483648~2147483647,加起来大概 40 亿的映射空间。

只要哈希函数映射得比较均匀松散,一般应用是很难出现碰撞的。但问题是一个 40 亿长度的数组,内存是放不下的。

假如 HashMap 数组的初始大小才 16,就需要用之前需要对数组的长度取模运算,得到的余数才能用来访问数组下标。

源码中模运算就是把散列值和数组长度 - 1 做一个 "与&" 操作,位运算比取余 % 运算要快。

bucketIndex = indexFor(hash, table.length);

static int indexFor(int h, int length) {
     return h & (length-1);
}

顺便说一下,这也正好解释了为什么 HashMap 的数组长度要取 2 的整数幂。因为这样(数组长度 - 1)正好相当于一个 “低位掩码”。 操作的结果就是散列值的高位全部归零,只保留低位值,用来做数组下标访问。以初始长度 16 为例,16-1=15。2 进制表示是 0000 0000 0000 0000 0000 0000 0000 1111。和某个散列值做  操作如下,结果就是截取了最低的四位值。

哈希&运算

这样是要快捷一些,但是新的问题来了,就算散列值分布再松散,要是只取最后几位的话,碰撞也会很严重。如果散列本身做得不好,分布上成等差数列的漏洞,如果正好让最后几个低位呈现规律性重复,那就更难搞了。

这时候 扰动函数 的价值就体现出来了,看一下扰动函数的示意图:

扰动函数示意图

右移 16 位,正好是 32bit 的一半,自己的高半区和低半区做异或,就是为了混合原始哈希码的高位和低位,以此来加大低位的随机性。而且混合后的低位掺杂了高位的部分特征,这样高位的信息也被变相保留下来。

15.为什么HashMap的容量是2的倍数呢?

  • 第一个原因是为了方便哈希取余:

将元素放在table数组上面,是用hash值%数组大小定位位置,而HashMap是用hash值&(数组大小-1),却能和前面达到一样的效果,这就得益于HashMap的大小是2的倍数,2的倍数意味着该数的二进制位只有一位为1,而该数-1就可以得到二进制位上1变成0,后面的0变成1,再通过&运算,就可以得到和%一样的效果,并且位运算比%的效率高得多

HashMap的容量是2的n次幂时,(n-1)的2进制也就是1111111***111这样形式的,这样与添加元素的hash值进行位运算时,能够充分的散列,使得添加的元素均匀分布在HashMap的每个位置上,减少hash碰撞。

  • 第二个方面是在扩容时,利用扩容后的大小也是2的倍数,将已经产生hash碰撞的元素完美的转移到新的table中去

我们可以简单看看HashMap的扩容机制,HashMap中的元素在超过负载因子*HashMap大小时就会产生扩容。

put中的扩容

16.如果初始化HashMap,传一个17的值new HashMap<>,它会怎么处理?

简单来说,就是初始化时,传的不是2的倍数时,HashMap会向上寻找离得最近的2的倍数,所以传入17,但HashMap的实际容量是32。

我们来看看详情,在HashMap的初始化中,有这样⼀段⽅法;

public HashMap(int initialCapacity, float loadFactor) {
 ...
 this.loadFactor = loadFactor;
 this.threshold = tableSizeFor(initialCapacity);
}
  • 阀值 threshold ,通过⽅法 tableSizeFor 进⾏计算,是根据初始化传的参数来计算的。

  • 同时,这个⽅法也要要寻找⽐初始值⼤的,最⼩的那个2进制数值。⽐如传了17,我应该找到的是32。

static final int tableSizeFor(int cap) {
 int n = cap - 1;
 n |= n >>> 1;
 n |= n >>> 2;
 n |= n >>> 4;
 n |= n >>> 8;
 n |= n >>> 16;
 return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1; }
  • MAXIMUM_CAPACITY = 1 << 30,这个是临界范围,也就是最⼤的Map集合。

  • 计算过程是向右移位1、2、4、8、16,和原来的数做|运算,这主要是为了把⼆进制的各个位置都填上1,当⼆进制的各个位置都是1以后,就是⼀个标准的2的倍数减1了,最后把结果加1再返回即可。

以17为例,看一下初始化计算table容量的过程:

容量计算

17.你还知道哪些哈希函数的构造方法呢?

HashMap里哈希构造函数的方法叫:

  • 除留取余法:H(key)=key%p(p<=N),关键字除以一个不大于哈希表长度的正整数p,所得余数为地址,当然HashMap里进行了优化改造,效率更高,散列也更均衡。

除此之外,还有这几种常见的哈希函数构造方法:

  • 直接定址法

    直接根据key来映射到对应的数组位置,例如1232放到下标1232的位置。

  • 数字分析法

    key的某些数字(例如十位和百位)作为映射的位置

  • 平方取中法

    key平方的中间几位作为映射的位置

  • 折叠法

    key分割成位数相同的几段,然后把它们的叠加和作为映射的位置

散列函数构造

18.解决哈希冲突有哪些方法呢?

我们到现在已经知道,HashMap使用链表的原因为了处理哈希冲突,这种方法就是所谓的:

  • 链地址法:在冲突的位置拉一个链表,把冲突的元素放进去。

除此之外,还有一些常见的解决冲突的办法:

  • 开放定址法:开放定址法就是从冲突的位置再接着往下找,给冲突元素找个空位。

    找到空闲位置的方法也有很多种:

    • 线行探查法: 从冲突的位置开始,依次判断下一个位置是否空闲,直至找到空闲位置

    • 平方探查法: 从冲突的位置x开始,第一次增加1^2个位置,第二次增加2^2…,直至找到空闲的位置

    • ……

开放定址法

  • 再哈希法:换种哈希函数,重新计算冲突元素的地址。

  • 建立公共溢出区:再建一个数组,把冲突的元素放进去。

19.为什么HashMap链表转红黑树的阈值为8呢?

树化发生在table数组的长度大于64,且链表的长度大于8的时候。

为什么是8呢?源码的注释也给出了答案。

源码注释

红黑树节点的大小大概是普通节点大小的两倍,所以转红黑树,牺牲了空间换时间,更多的是一种兜底的策略,保证极端情况下的查找效率。

阈值为什么要选8呢?和统计学有关。理想情况下,使用随机哈希码,链表里的节点符合泊松分布,出现节点个数的概率是递减的,节点个数为8的情况,发生概率仅为0.00000006

至于红黑树转回链表的阈值为什么是6,而不是8?是因为如果这个阈值也设置成8,假如发生碰撞,节点增减刚好在8附近,会发生链表和红黑树的不断转换,导致资源浪费。

20.扩容在什么时候呢?为什么扩容因子是0.75?

为了减少哈希冲突发生的概率,当当前HashMap的元素个数达到一个临界值的时候,就会触发扩容,把所有元素rehash之后再放在扩容后的容器中,这是一个相当耗时的操作。

put时,扩容

而这个临界值threshold就是由加载因子和当前容器的容量大小来确定的,假如采用默认的构造方法:

临界值(threshold )= 默认容量(DEFAULT_INITIAL_CAPACITY) * 默认扩容因子(DEFAULT_LOAD_FACTOR)

threshold计算

那就是大于16x0.75=12时,就会触发扩容操作。

那么为什么选择了0.75作为HashMap的默认加载因子呢?

简单来说,这是对空间成本和时间成本平衡的考虑。

在HashMap中有这样一段注释:

关于默认负载因子的注释

我们都知道,HashMap的散列构造方式是Hash取余,负载因子决定元素个数达到多少时候扩容。

假如我们设的比较大,元素比较多,空位比较少的时候才扩容,那么发生哈希冲突的概率就增加了,查找的时间成本就增加了。

我们设的比较小的话,元素比较少,空位比较多的时候就扩容了,发生哈希碰撞的概率就降低了,查找时间成本降低,但是就需要更多的空间去存储元素,空间成本就增加了。

21.那扩容机制了解吗?

HashMap是基于数组+链表和红黑树实现的,但用于存放key值的桶数组的长度是固定的,由初始化参数确定。

那么,随着数据的插入数量增加以及负载因子的作用下,就需要扩容来存放更多的数据。而扩容中有一个非常重要的点,就是jdk1.8中的优化操作,可以不需要再重新计算每一个元素的哈希值。

因为HashMap的初始容量是2的次幂,扩容之后的长度是原来的二倍,新的容量也是2的次幂,所以,元素,要么在原位置,要么在原位置再移动2的次幂。

看下这张图,n为table的长度,图a表示扩容前的key1和key2两种key确定索引的位置,图b表示扩容后key1和key2两种key确定索引位置。

扩容之后的索引计算

元素在重新计算hash之后,因为n变为2倍,那么n-1的mask范围在高位多1bit(红色),因此新的index就会发生这样的变化:

扩容位置变化

所以在扩容时,只需要看原来的hash值新增的那一位是0还是1就行了,是0的话索引没变,是1的化变成原索引+oldCap,看看如16扩容为32的示意图:

扩容节点迁移示意图

扩容节点迁移主要逻辑:

扩容主要逻辑

22.jdk1.8对HashMap主要做了哪些优化呢?为什么?

jdk1.8 的HashMap主要有五点优化:

  1. 数据结构:数组 + 链表改成了数组 + 链表或红黑树

    原因:发生 hash 冲突,元素会存入链表,链表过长转为红黑树,将时间复杂度由O(n)降为O(logn)

  2. 链表插入方式:链表的插入方式从头插法改成了尾插法

    简单说就是插入时,如果数组位置上已经有元素,1.7 将新元素放到数组中,原始节点作为新节点的后继节点,1.8 遍历链表,将元素放置到链表的最后。

    原因:因为 1.7 头插法扩容时,头插法会使链表发生反转,多线程环境下会产生环。

  3. 扩容rehash:扩容的时候 1.7 需要对原数组中的元素进行重新 hash 定位在新数组的位置,1.8 采用更简单的判断逻辑,不需要重新通过哈希函数计算位置,新的位置不变或索引 + 新增容量大小。

    原因:提高扩容的效率,更快地扩容。

  4. 扩容时机:在插入时,1.7 先判断是否需要扩容,再插入,1.8 先进行插入,插入完成再判断是否需要扩容;

  5. 散列函数:1.7 做了四次移位和四次异或,jdk1.8只做一次。

    原因:做 4 次的话,边际效用也不大,改为一次,提升效率。

23.你能自己设计实现一个HashMap吗?

这道题快手常考。

不要慌,红黑树版咱们多半是写不出来,但是数组+链表版还是问题不大的,详细可见:手写HashMap,快手面试官直呼内行!

整体的设计:

  • 散列函数:hashCode()+除留余数法

  • 冲突解决:链地址法

  • 扩容:节点重新hash获取位置

自定义HashMap整体结构

完整代码:

完整代码

24.HashMap 是线程安全的吗?多线程下会有什么问题?

HashMap不是线程安全的,可能会发生这些问题:

  • 多线程下扩容死循环。JDK1.7 中的 HashMap 使用头插法插入元素,在多线程的环境下,扩容的时候有可能导致环形链表的出现,形成死循环。因此,JDK1.8 使用尾插法插入元素,在扩容时会保持链表元素原本的顺序,不会出现环形链表的问题。

  • 多线程的 put 可能导致元素的丢失。多线程同时执行 put 操作,如果计算出来的索引位置是相同的,那会造成前一个 key 被后一个 key 覆盖,从而导致元素的丢失。此问题在 JDK 1.7 和 JDK 1.8 中都存在。

  • put 和 get 并发时,可能导致 get 为 null。线程 1 执行 put 时,因为元素个数超出 threshold 而导致 rehash,线程 2 此时执行 get,有可能导致这个问题。这个问题在 JDK 1.7 和 JDK 1.8 中都存在。

25.有什么办法能解决HashMap线程不安全的问题呢?

Java 中有 HashTable、Collections.synchronizedMap、以及 ConcurrentHashMap 可以实现线程安全的 Map。

  • HashTable 是直接在操作方法上加 synchronized 关键字,锁住整个table数组,粒度比较大;

  • Collections.synchronizedMap 是使用 Collections 集合工具的内部类,通过传入 Map 封装出一个 SynchronizedMap 对象,内部定义了一个对象锁,方法内通过对象锁实现;

  • ConcurrentHashMap 在jdk1.7中使用分段锁,在jdk1.8中使用CAS+synchronized。

26.能具体说一下ConcurrentHashmap的实现吗?

ConcurrentHashmap线程安全在jdk1.7版本是基于分段锁实现,在jdk1.8是基于CAS+synchronized实现。

1.7分段锁

从结构上说,1.7版本的ConcurrentHashMap采用分段锁机制,里面包含一个Segment数组,Segment继承于ReentrantLock,Segment则包含HashEntry的数组,HashEntry本身就是一个链表的结构,具有保存key、value的能力能指向下一个节点的指针。

实际上就是相当于每个Segment都是一个HashMap,默认的Segment长度是16,也就是支持16个线程的并发写,Segment之间相互不会受到影响。

1.7ConcurrentHashMap示意图

put流程

整个流程和HashMap非常类似,只不过是先定位到具体的Segment,然后通过ReentrantLock去操作而已,后面的流程,就和HashMap基本上是一样的。

  1. 计算hash,定位到segment,segment如果是空就先初始化

  2. 使用ReentrantLock加锁,如果获取锁失败则尝试自旋,自旋超过次数就阻塞获取,保证一定获取锁成功

  3. 遍历HashEntry,就是和HashMap一样,数组中key和hash一样就直接替换,不存在就再插入链表,链表同样操作

get流程

get也很简单,key通过hash定位到segment,再遍历链表定位到具体的元素上,需要注意的是value是volatile的,所以get是不需要加锁的。

1.8 CAS+synchronized

jdk1.8实现线程安全不是在数据结构上下功夫,它的数据结构和HashMap是一样的,数组+链表+红黑树。它实现线程安全的关键点在于put流程。

put流程

  1. 首先计算hash,遍历node数组,如果node是空的话,就通过CAS+自旋的方式初始化

 tab = initTable();

node数组初始化:

    private final Node<K,V>[] initTable() {
        Node<K,V>[] tab; int sc;
        while ((tab = table) == null || tab.length == 0) {
            //如果正在初始化或者扩容
            if ((sc = sizeCtl) < 0)
                //等待
                Thread.yield(); // lost initialization race; just spin
            else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {   //CAS操作
                try {
                    if ((tab = table) == null || tab.length == 0) {
                        int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
                        @SuppressWarnings("unchecked")
                        Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
                        table = tab = nt;
                        sc = n - (n >>> 2);
                    }
                } finally {
                    sizeCtl = sc;
                }
                break;
            }
        }
        return tab;
    }

2.如果当前数组位置是空则直接通过CAS自旋写入数据

    static final <K,V> boolean casTabAt(Node<K,V>[] tab, int i,
                                        Node<K,V> c, Node<K,V> v) {
        return U.compareAndSwapObject(tab, ((long)i << ASHIFT) + ABASE, c, v);
    }
  1. 如果hash==MOVED,说明需要扩容,执行扩容

else if ((fh = f.hash) == MOVED)
                tab = helpTransfer(tab, f);
    final Node<K,V>[] helpTransfer(Node<K,V>[] tab, Node<K,V> f) {
        Node<K,V>[] nextTab; int sc;
        if (tab != null && (f instanceof ForwardingNode) &&
            (nextTab = ((ForwardingNode<K,V>)f).nextTable) != null) {
            int rs = resizeStamp(tab.length);
            while (nextTab == nextTable && table == tab &&
                   (sc = sizeCtl) < 0) {
                if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
                    sc == rs + MAX_RESIZERS || transferIndex <= 0)
                    break;
                if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1)) {
                    transfer(tab, nextTab);
                    break;
                }
            }
            return nextTab;
        }
        return table;
    }
  1. 如果都不满足,就使用synchronized写入数据,写入数据同样判断链表、红黑树,链表写入和HashMap的方式一样,key hash一样就覆盖,反之就尾插法,链表长度超过8就转换成红黑树

 synchronized (f){
     ……
 }

ConcurrentHashmap jdk1.8put流程

get查询

get很简单,和HashMap基本相同,通过key计算位置,table该位置key相同就返回,如果是红黑树按照红黑树获取,否则就遍历链表获取。

27.HashMap 内部节点是有序的吗?

HashMap是无序的,根据 hash 值随机插入。如果想使用有序的Map,可以使用LinkedHashMap 或者 TreeMap。

28.讲讲 LinkedHashMap 怎么实现有序的?

LinkedHashMap维护了一个双向链表,有头尾节点,同时 LinkedHashMap 节点 Entry 内部除了继承 HashMap 的 Node 属性,还有 before 和 after 用于标识前置节点和后置节点。

Entry节点

可以实现按插入的顺序或访问顺序排序。

LinkedHashMap实现原理

29.讲讲 TreeMap 怎么实现有序的?

TreeMap 是按照 Key 的自然顺序或者 Comprator 的顺序进行排序,内部是通过红黑树来实现。所以要么 key 所属的类实现 Comparable 接口,或者自定义一个实现了 Comparator 接口的比较器,传给 TreeMap 用于 key 的比较。

TreeMap

Set

Set面试没啥好问的,拿HashSet来凑个数。

30.讲讲HashSet的底层实现?

HashSet 底层就是基于 HashMap 实现的。( HashSet 的源码⾮常⾮常少,因为除了 clone() 、 writeObject() 、 readObject() 是 HashSet⾃⼰不得不实现之外,其他⽅法都是直接调⽤ HashMap 中的⽅法。

HashSet的add方法,直接调用HashMap的put方法,将添加的元素作为key,new一个Object作为value,直接调用HashMap的put方法,它会根据返回值是否为空来判断是否插入元素成功。

    public boolean add(E e) {
        return map.put(e, PRESENT)==null;
    }

HashSet套娃

而在HashMap的putVal方法中,进行了一系列判断,最后的结果是,只有在key在table数组中不存在的时候,才会返回插入的值。

            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }

31,hashCode()和equals()方法作用

为什么存在了hashcode()还会有equals()方法,hashcode方法是本地方法(
public native int hashCode();),HashSet<>中先会比较hashcode()方法,若相等的话在比较equals方法,若两者相等则表明两个对象相等,若hashcode相等则两对象不一定相等。==和equals的区别,==常量比值对象比引用,equals是比引用和比值(String先比引用在比值)

32,对象常量池技术

Integer,Character 对象

public static Integer valueOf(int i) {
    if (i >= IntegerCache.low && i <= IntegerCache.high)
        return IntegerCache.cache[i + (-IntegerCache.low)];
    return new Integer(i);
}
private static class IntegerCache {
    static final int low = -128;
    static final int high;
    static {
        // high value may be configured by property
        int h = 127;
    }
}
public static Character valueOf(char c) {
    if (c <= 127) { // must cache
      return CharacterCache.cache[(int)c];
    }
    return new Character(c);
}

private static class CharacterCache {
    private CharacterCache(){}
    static final Character cache[] = new Character[127 + 1];
    static {
        for (int i = 0; i < cache.length; i++)
            cache[i] = new Character((char)i);
    }

}

33,Object类的方法

public final native Class<?> getClass()//native方法,用于返回当前运行时对象的Class对象,使用了final关键字修饰,故不允许子类重写。

public native int hashCode() //native方法,用于返回对象的哈希码,主要使用在哈希表中,比如JDK中的HashMap。
public boolean equals(Object obj)//用于比较2个对象的内存地址是否相等,String类对该方法进行了重写用户比较字符串的值是否相等。

protected native Object clone() throws CloneNotSupportedException//naitive方法,用于创建并返回当前对象的一份拷贝。一般情况下,对于任何对象 x,表达式 x.clone() != x 为true,x.clone().getClass() == x.getClass() 为true。Object本身没有实现Cloneable接口,所以不重写clone方法并且进行调用的话会发生CloneNotSupportedException异常。

public String toString()//返回类的名字@实例的哈希码的16进制的字符串。建议Object所有的子类都重写这个方法。

public final native void notify()//native方法,并且不能重写。唤醒一个在此对象监视器上等待的线程(监视器相当于就是锁的概念)。如果有多个线程在等待只会任意唤醒一个。

public final native void notifyAll()//native方法,并且不能重写。跟notify一样,唯一的区别就是会唤醒在此对象监视器上等待的所有线程,而不是一个线程。

public final native void wait(long timeout) throws InterruptedException//native方法,并且不能重写。暂停线程的执行。注意:sleep方法没有释放锁,而wait方法释放了锁 。timeout是等待时间。

public final void wait(long timeout, int nanos) throws InterruptedException//多了nanos参数,这个参数表示额外时间(以毫微秒为单位,范围是 0-999999)。 所以超时的时间还需要加上nanos毫秒。

public final void wait() throws InterruptedException//跟之前的2个wait方法一样,只不过该方法一直等待,没有超时时间这个概念

protected void finalize() throws Throwable { }//实例被垃圾回收器回收的时候触发的操作

33,String不可修改性,StringBuffer,StringBuilder

String不可修改性

  1. 保存字符串的数组被 final 修饰且为私有的,并且String 类没有提供/暴露修改这个字符串的方法。
  2. String 类被 final 修饰导致其不能被继承,进而避免了子类破坏 String 不可变。

String 中的对象是不可变的,也就可以理解为常量,线程安全。AbstractStringBuilder 是 StringBuilder 与 StringBuffer 的公共父类,定义了一些字符串的基本操作,如 expandCapacityappendinsertindexOf 等公共方法。StringBuffer 对方法加了同步锁或者对调用的方法加了同步锁,所以是线程安全的。StringBuilder 并没有对方法进行加同步锁,所以是非线程安全的 

  1. 操作少量的数据: 适用 String
  2. 单线程操作字符串缓冲区下操作大量数据: 适用 StringBuilder
  3. 多线程操作字符串缓冲区下操作大量数据: 适用 StringBuffer

34,IO

BIO:阻塞IO,用户进程发起读请求之后,一直会等待直到内核读取完数据之后才进行读操作。

NBIO:非阻塞IO,用户进程发起读请求之后,会轮询是否数据准备就绪。

NIO:用户进程发起读请求会用select监控,一旦部分缓冲区数据准备就绪就通知用户进程进行读

AIO:异步IO,用户发起读操作,内核读取完数据之后会通知用户进程进行读数据。

35,ArrayList的扩容机制

通过grow函数进行扩容,初始为1.5倍进行扩容,若不够则会变成minCapacity(最小的实际申请容量)

private void grow(int minCapacity) {
        // oldCapacity为旧容量,newCapacity为新容量
        int oldCapacity = elementData.length;
        //将oldCapacity 右移一位,其效果相当于oldCapacity /2,
        //我们知道位运算的速度远远快于整除运算,整句运算式的结果就是将新容量更新为旧容量的1.5倍,
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        //然后检查新容量是否大于最小需要容量,若还是小于最小需要容量,那么就把最小需要容量当作数组的新容量,
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
        //再检查新容量是否超出了ArrayList所定义的最大容量,
        //若超出了,则调用hugeCapacity()来比较minCapacity和 MAX_ARRAY_SIZE,
        //如果minCapacity大于MAX_ARRAY_SIZE,则新容量则为Interger.MAX_VALUE,否则,新容量大小则为 MAX_ARRAY_SIZE。
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
        // minCapacity is usually close to size, so this is a win:
        elementData = Arrays.copyOf(elementData, newCapacity);
    }

36, volatile 关键字的应用

1.状态标记量

int a = 0;
volatile bool flag = false;

public void write() {
    a = 2;              //1
    flag = true;        //2
}

public void multiply() {
    if (flag) {         //3
        int ret = a * a;//4
    }
}


2.DCL 双重校验锁-单例模式

public class Singleton {

    private volatile static Singleton uniqueInstance;

    private Singleton() {
    }

    public  static Singleton getUniqueInstance() {
       //先判断对象是否已经实例过,没有实例化过才进入加锁代码
        if (uniqueInstance == null) {
            //类对象加锁
            synchronized (Singleton.class) {
                if (uniqueInstance == null) {
                    uniqueInstance = new Singleton();
                }
            }
        }
        return uniqueInstance;
    }
}

需要注意 uniqueInstance 采用 volatile 关键字修饰也是很有必要。

uniqueInstance 采用 volatile 关键字修饰也是很有必要的, uniqueInstance = new Singleton(); 这段代码其实是分为三步执行:

  1. 为 uniqueInstance 分配内存空间
  2. 初始化 uniqueInstance
  3. 将 uniqueInstance 指向分配的内存地址

但是由于 JVM 具有指令重排的特性,执行顺序有可能变成 1->3->2。指令重排在单线程环境下不会出现问题,但是在多线程环境下会导致一个线程获得还没有初始化的实例。例如,线程 T1 执行了 1 和 3,此时 T2 调用 getUniqueInstance() 后发现 uniqueInstance 不为空,因此返回 uniqueInstance,但此时 uniqueInstance 还未被初始化。

使用 volatile 可以禁止 JVM 的指令重排,保证在多线程环境下也能正常运行

JVM

重点:

  1. JVM包含哪些区域以及区域的内容和作用

宏观整体

类加载子系统

重点:

  1. 类加载子系统的组成
  2. 类加载器(引导类,扩展,程序类加载器)
  3. 双亲委派机制
  4. 主动引用和被动引用(被动引用不会进行初始化操作)

 程序计数器

重点:

  1. pc寄存器的特点与作用

虚拟机栈

重点:

  1. 设置栈的大小:-Xss
  2. 虚拟机栈的内容与特点
  3. 栈帧、局部变量表、操作数栈、逃逸方法

 

 本地方法栈

 堆

重点:

  1. 堆的包含
  2. 不同版本的jdk堆的变化
  3. 逃逸分析

方法区

重点:

  1. 栈、堆、方法区之间的关系
  2. 不同版本JDK之间方法区、类变量和字符串常量池的存储位置的变化?
  3. 方法区中包含什么?

Java对象的实例化过程

重点:

  1. 创建对象的步骤
  2. 内存布局
  3. 对象头包含什么?
  4. 如何访问到对象,句柄访问和直接指针?

 

 执行引擎

字符串常量池

重点:

  1. string.intern()在不同的JDK中的不同
  2. 常量的拼接
  3. 不同版本的jdk字符串常量池存在的位置

垃圾回收GC

 

 

Java框架

spring

  1. spring初始化一个bean的过程?

springMVC

  1. springMVC处理流程?

         用户访问url到DispatcherServlet,根据HandleMappering请求对应的Controller,等到controller处理响应的业务逻辑之后放回一个ModelAndView,DispatcherServlet选择相应的视图解析器解析modleandview并返回给用户。

mybatis

 Linux

find

  • find 目录 -name/-iname “文件名” //查找目录下某个特定的文件
  • find 目录 -name/-iname “文件名”  -exec 命令 {} \;  //找到某个文件并执行命令
  • find 目录 -name/-iname “文件名”  -size +1024k //表示找到大于1024k的文件
  • find 目录 -name/-iname “文件名 -mtime +120 //表示查找120天前被修改的文件

ls

  • 以易读的方式显示文件大小(显示为 MB,GB…):ls -lh 。
  • 以最后修改时间升序列出文件:ls -ltr 。
  • 在文件名后面显示文件类型:ls -F 。

pwd

mkdir 命令

  • 在 home 目录下创建一个名为 temp 的目录:mkdir ~/temp 。
  • 使用 -p 选项可以创建一个路径上所有不存在的目录:mkdir -p dir1/dir2/dir3/dir4/ 。

Redis

Redis为什么快呢?

redis的速度非常的快,单机的redis就可以支撑每秒10几万的并发,相对于mysql来说,性能是mysql的几十倍。速度快的原因主要有几点:

  1. 完全基于内存操作
  2. C语言实现,优化过的数据结构,基于几种基础的数据结构,redis做了大量的优化,性能极高
  3. 使用单线程,无上下文的切换成本
  4. 基于非阻塞的IO多路复用机制

那为什么Redis6.0之后又改用多线程呢?

redis使用多线程并非是完全摒弃单线程,redis还是使用单线程模型来处理客户端的请求,只是使用多线程来处理数据的读写和协议解析,执行命令还是使用单线程。

这样做的目的是因为redis的性能瓶颈在于网络IO而非CPU,使用多线程能提升IO读写的效率,从而整体提高redis的性能。

知道什么是热key吗?热key问题怎么解决?

所谓热key问题就是,突然有几十万的请求去访问redis上的某个特定key,那么这样会造成流量过于集中,达到物理网卡上限,从而导致这台redis的服务器宕机引发雪崩。

针对热key的解决方案:

  1. 提前把热key打散到不同的服务器,降低压力
  2. 加入二级缓存,提前加载热key数据到内存中,如果redis宕机,走内存查询

什么是缓存击穿、缓存穿透、缓存雪崩?

缓存击穿

缓存击穿的概念就是单个key并发访问过高,过期时导致所有请求直接打到db上,这个和热key的问题比较类似,只是说的点在于过期导致请求全部打到DB上而已。

解决方案:

  1. 加锁更新,比如请求查询A,发现缓存中没有,对A这个key加锁,同时去数据库查询数据,写入缓存,再返回给用户,这样后面的请求就可以从缓存中拿到数据了。
  2. 将过期时间组合写在value中,通过异步的方式不断的刷新过期时间,防止此类现象。

缓存穿透

缓存穿透是指查询不存在缓存中的数据,每次请求都会打到DB,就像缓存不存在一样。

针对这个问题,加一层布隆过滤器。布隆过滤器的原理是在你存入数据的时候,会通过散列函数将它映射为一个位数组中的K个点,同时把他们置为1。

这样当用户再次来查询A,而A在布隆过滤器值为0,直接返回,就不会产生击穿请求打到DB了。

显然,使用布隆过滤器之后会有一个问题就是误判,因为它本身是一个数组,可能会有多个值落到同一个位置,那么理论上来说只要我们的数组长度够长,误判的概率就会越低,这种问题就根据实际情况来就好了。

缓存雪崩

当某一时刻发生大规模的缓存失效的情况,比如你的缓存服务宕机了,会有大量的请求进来直接打到DB上,这样可能导致整个系统的崩溃,称为雪崩。雪崩和击穿、热key的问题不太一样的是,他是指大规模的缓存都过期失效了。

针对雪崩几个解决方案:

  1. 针对不同key设置不同的过期时间,避免同时过期
  2. 限流,如果redis宕机,可以限流,避免同时刻大量请求打崩DB
  3. 二级缓存,同热key的方案。

持久化方式,rdb和aof的区别

百度安全验证https://baijiahao.baidu.com/s?id=1654694618189745916&wfr=spider&for=pc

那么定期+惰性都没有删除过期的key怎么办?

假设redis每次定期随机查询key的时候没有删掉,这些key也没有做查询的话,就会导致这些key一直保存在redis里面无法被删除,这时候就会走到redis的内存淘汰机制。

  1. volatile-lru:从已设置过期时间的key中,移除最近最少使用的key进行淘汰
  2. volatile-ttl:从已设置过期时间的key中,移除将要过期的key
  3. volatile-random:从已设置过期时间的key中随机选择key淘汰
  4. allkeys-lru:从key中选择最近最少使用的进行淘汰
  5. allkeys-random:从key中随机选择key进行淘汰
  6. noeviction:当内存达到阈值的时候,新写入操作报错

作者:阿里云云栖号
链接:https://zhuanlan.zhihu.com/p/368770382
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
 

Redis的过期策略有哪些?

redis主要有2种过期删除策略

惰性删除

惰性删除指的是当我们查询key的时候才对key进行检测,如果已经达到过期时间,则删除。显然,他有一个缺点就是如果这些过期的key没有被访问,那么他就一直无法被删除,而且一直占用内存。

定期删除

定期删除指的是redis每隔一段时间对数据库做一次检查,删除里面的过期key。由于不可能对所有key去做轮询来删除,所以redis会每次随机取一些key去做检查和删除。

怎么实现Redis的高可用?

要想实现高可用,一台机器肯定是不够的,而redis要保证高可用,有2个可选方案。

主从架构

主从模式是最简单的实现高可用的方案,核心就是主从同步。主从同步的原理如下:

  1. slave发送sync命令到master
  2. master收到sync之后,执行bgsave,生成RDB全量文件
  3. master把slave的写命令记录到缓存
  4. bgsave执行完毕之后,发送RDB文件到slave,slave执行
  5. master发送缓存中的写命令到slave,slave执行

这里我写的这个命令是sync,但是在redis2.8版本之后已经使用psync来替代sync了,原因是sync命令非常消耗系统资源,而psync的效率更高。

哨兵

基于主从方案的缺点还是很明显的,假设master宕机,那么就不能写入数据,那么slave也就失去了作用,整个架构就不可用了,除非你手动切换,主要原因就是因为没有自动故障转移机制。而哨兵(sentinel)的功能比单纯的主从架构全面的多了,它具备自动故障转移、集群监控、消息通知等功能。

哨兵可以同时监视多个主从服务器,并且在被监视的master下线时,自动将某个slave提升为master,然后由新的master继续接收命令。整个过程如下:

  1. 初始化sentinel,将普通的redis代码替换成sentinel专用代码
  2. 初始化masters字典和服务器信息,服务器信息主要保存ip:port,并记录实例的地址和ID
  3. 创建和master的两个连接,命令连接和订阅连接,并且订阅sentinel:hello频道
  4. 每隔10秒向master发送info命令,获取master和它下面所有slave的当前信息
  5. 当发现master有新的slave之后,sentinel和新的slave同样建立两个连接,同时每个10秒发送info命令,更新master信息
  6. sentinel每隔1秒向所有服务器发送ping命令,如果某台服务器在配置的响应时间内连续返回无效回复,将会被标记为下线状态
  7. 选举出领头sentinel,领头sentinel需要半数以上的sentinel同意
  8. 领头sentinel从已下线的的master所有slave中挑选一个,将其转换为master
  9. 让所有的slave改为从新的master复制数据
  10. 将原来的master设置为新的master的从服务器,当原来master重新回复连接时,就变成了新master的从服务器

sentinel会每隔1秒向所有实例(包括主从服务器和其他sentinel)发送ping命令,并且根据回复判断是否已经下线,这种方式叫做主观下线。当判断为主观下线时,就会向其他监视的sentinel询问,如果超过半数的投票认为已经是下线状态,则会标记为客观下线状态,同时触发故障转移。

Redis哨兵(Sentinel)模式 - 简书

Mysql

1. 数据库的三范式是什么?

  • 第一范式:强调的是列的原子性,即数据库表的每一列都是不可分割的原子数据项。
  • 第二范式:要求实体的属性完全依赖于主关键字。所谓完全 依赖是指不能存在仅依赖主关键字一部分的属性。
  • 第三范式:任何非主属性不依赖于其它非主属性。

2. MySQL 支持哪些存储引擎?

MySQL 支持多种存储引擎,比如 InnoDB,MyISAM,Memory,Archive 等等.在大多数的情况下,直接选择使用 InnoDB 引擎都是最合适的,InnoDB 也是 MySQL 的默认存储引擎。

MyISAM 和 InnoDB 的区别有哪些:

  • InnoDB 支持事务,MyISAM 不支持
  • InnoDB 支持外键,而 MyISAM 不支持
  • InnoDB 是聚集索引,数据文件是和索引绑在一起的,必须要有主键,通过主键索引效率很高;MyISAM 是非聚集索引,数据文件是分离的,索引保存的是数据文件的指针,主键索引和辅助索引是独立的。
  • Innodb 不支持全文索引,而 MyISAM 支持全文索引,查询效率上 MyISAM 要高;
  • InnoDB 不保存表的具体行数,MyISAM 用一个变量保存了整个表的行数。
  • MyISAM 采用表级锁(table-level locking);InnoDB 支持行级锁(row-level locking)和表级锁,默认为行级锁。

3. 超键、候选键、主键、外键分别是什么?

  • 超键:在关系中能唯一标识元组的属性集称为关系模式的超键。一个属性可以为作为一个超键,多个属性组合在一起也可以作为一个超键。超键包含候选键和主键。
  • 候选键:是最小超键,即没有冗余元素的超键。
  • 主键:数据库表中对储存数据对象予以唯一和完整标识的数据列或属性的组合。一个数据列只能有一个主键,且主键的取值不能缺失,即不能为空值(Null)。
  • 外键:在一个表中存在的另一个表的主键称此表的外键。

4. SQL 约束有哪几种?

  • NOT NULL: 用于控制字段的内容一定不能为空(NULL)。
  • UNIQUE: 控件字段内容不能重复,一个表允许有多个 Unique 约束。
  • PRIMARY KEY: 也是用于控件字段内容不能重复,但它在一个表只允许出现一个。
  • FOREIGN KEY: 用于预防破坏表之间连接的动作,也能防止非法数据插入外键列,因为它必须是它指向的那个表中的值之一。
  • CHECK: 用于控制字段的值范围。

5. MySQL 中的 varchar 和 char 有什么区别?

char 是一个定长字段,假如申请了char(10)的空间,那么无论实际存储多少内容.该字段都占用 10 个字符,而 varchar 是变长的,也就是说申请的只是最大长度,占用的空间为实际字符长度+1,最后一个字符存储使用了多长的空间.

在检索效率上来讲,char > varchar,因此在使用中,如果确定某个字段的值的长度,可以使用 char,否则应该尽量使用 varchar.例如存储用户 MD5 加密后的密码,则应该使用 char。

6. MySQL中 in 和 exists 区别

in是内外表进行连接,相当于多个or操作,适合小内表,exists是外表一直loop循环,对内表进行查询,适合小外表。

MySQL中的in语句是把外表和内表作hash 连接,而exists语句是对外表作loop循环,每次loop循环再对内表进行查询。一直大家都认为exists比in语句的效率要高,这种说法其实是不准确的。这个是要区分环境的。

如果查询的两个表大小相当,那么用in和exists差别不大。 如果两个表中一个较小,一个是大表,则子查询表大的用exists,子查询表小的用in。 not in 和not exists:如果查询语句使用了not in,那么内外表都进行全表扫描,没有用到索引;而not extsts的子查询依然能用到表上的索引。所以无论那个表大,用not exists都比not in要快。

7. drop、delete与truncate的区别

三者都表示删除,但是三者有一些差别:

  当表被TRUNCATE 后,这个表和索引所占用的空间会恢复到初始大小

  DELETE操作不会减少表或索引所占用的空间。

  drop语句将表所占用的空间全释放掉。

 一般而言,drop(删除表) > truncate(清空表) > delete(删除某条数据),

应用范围。

   TRUNCATE 只能对TABLE;         

   DELETE可以是table和view,一般需要和where一起,具体删除某条数据

   TRUNCATE 和DELETE只删除数据, DROP则删除整个表(结构和数据)。

8. 什么是存储过程?有哪些优缺点?

存储过程是一些预编译的 SQL 语句。

1、更加直白的理解:存储过程可以说是一个记录集,它是由一些 T-SQL 语句组成的代码块,这些 T-SQL 语句代码像一个方法一样实现一些功能(对单表或多表的增删改查),然后再给这个代码块取一个名字,在用到这个功能的时候调用他就行了。

2、存储过程是一个预编译的代码块,执行效率比较高,一个存储过程替代大量 T_SQL 语句 ,可以降低网络通信量,提高通信速率,可以一定程度上确保数据安全

但是,在互联网项目中,其实是不太推荐存储过程的,比较出名的就是阿里的《Java 开发手册》中禁止使用存储过程,我个人的理解是,在互联网项目中,迭代太快,项目的生命周期也比较短,人员流动相比于传统的项目也更加频繁,在这样的情况下,存储过程的管理确实是没有那么方便,同时,复用性也没有写在服务层那么好。

9. MySQL 执行查询的过程

  1. 客户端通过 TCP 连接发送连接请求到 MySQL 连接器,连接器会对该请求进行权限验证及连接资源分配
  2. 查缓存。(当判断缓存是否命中时,MySQL 不会进行解析查询语句,而是直接使用 SQL 语句和客户端发送过来的其他原始信息。所以,任何字符上的不同,例如空格、注解等都会导致缓存的不命中。)
  3. 语法分析(SQL 语法是否写错了)。 如何把语句给到预处理器,检查数据表和数据列是否存在,解析别名看是否存在歧义。
  4. 优化。是否使用索引,生成执行计划。
  5. 交给执行器,将数据保存到结果集中,同时会逐步将数据缓存到查询缓存中,最终将结果集返回给客户端。

更新语句执行会复杂一点。需要检查表是否有排它锁,写 binlog,刷盘,是否执行 commit。

10、视图和表之间的区别和联系

  区别:

1.数据都是存储在表里面的,而不是视图

2.表是创建好的,真实存在的,而视图只是一段已经编译好的sql语句

3.视图只是窗口,而表则是里面的数据

4.表占有真实的物理空间,而视图只是一个逻辑概念,就像钱是表,爱情是视图

5.表是内模式,视图是外模式(内模式又称存储模式,对应于物理级,它是数据库中全体数据的内部表示或底层描述,是数据库最低一级的逻辑描述,它描述了数据在存储介质上的存储方式和物理结构,对应着实际存储在外存储介质上的数据库。内模式由内模式描述语言来描述、定义,它是数据库的存储观。外模式又称子模式,对应于用户级。它是某个或某几个用户所看到的数据库的数据视图,是与某一应用有关的数据的逻辑表示。外模式是从模式导出的一个子集,包含模式中允许特定用户使用的那部分数据。)

6.视图是查看数据表的一种方法,可以查询到表中用户想知道的数据,并且还不知道表结构,比较安全

7.表可以增删改查,视图只能查

8.视图的创建和删除不影响表

联系:

   视图一般都是建立在表的基础之上的,它的所有内容全部都来自于表,可能是一个表,也可能是多个表,视图是基本表的抽象和在逻辑意义上建立的新关系。 

11、limit关键字的使用

mysql> SELECT * FROM table LIMIT 5,10; // 检索记录行 6-15   
  
//为了检索从某一个偏移量到记录集的结束所有的记录行,可以指定第二个参数为 -1:    
mysql> SELECT * FROM table LIMIT 95,-1; // 检索记录行 96-last.   
  
//如果只给定一个参数,它表示返回最大的记录行数目:    
mysql> SELECT * FROM table LIMIT 5; //检索前 5 个记录行   
  
//换句话说,LIMIT n 等价于 LIMIT 0,n。  

事务

1. 什么是数据库事务?

事务是一个不可分割的数据库操作序列,也是数据库并发控制的基本单位,其执行的结果必须使数据库从一种一致性状态变到另一种一致性状态。事务是逻辑上的一组操作,要么都执行,要么都不执行。

事务最经典也经常被拿出来说例子就是转账了。

假如小明要给小红转账1000元,这个转账会涉及到两个关键操作就是:将小明的余额减少1000元,将小红的余额增加1000元。万一在这两个操作之间突然出现错误比如银行系统崩溃,导致小明余额减少而小红的余额没有增加,这样就不对了。事务就是保证这两个关键操作要么都成功,要么都要失败。

2. 介绍一下事务具有的四个特征

事务就是一组原子性的操作,这些操作要么全部发生,要么全部不发生。事务把数据库从一种一致性状态转换成另一种一致性状态。

  • 原子性。事务是数据库的逻辑工作单位,事务中包含的各操作要么都做,要么都不做
  • 一致性。事 务执行的结果必须是使数据库从一个一致性状态变到另一个一致性状态。因此当数据库只包含成功事务提交的结果时,就说数据库处于一致性状态。如果数据库系统 运行中发生故障,有些事务尚未完成就被迫中断,这些未完成事务对数据库所做的修改有一部分已写入物理数据库,这时数据库就处于一种不正确的状态,或者说是 不一致的状态。
  • 隔离性。一个事务的执行不能其它事务干扰。即一个事务内部的//操作及使用的数据对其它并发事务是隔离的,并发执行的各个事务之间不能互相干扰。
  • 持续性。也称永久性,指一个事务一旦提交,它对数据库中的数据的改变就应该是永久性的。接下来的其它操作或故障不应该对其执行结果有任何影响。

3. 说一下MySQL 的四种隔离级别

  • Read Uncommitted(读取未提交内容)

在该隔离级别,所有事务都可以看到其他未提交事务的执行结果。本隔离级别很少用于实际应用,因为它的性能也不比其他级别好多少。读取未提交的数据,也被称之为脏读(Dirty Read)。

  • Read Committed(读取提交内容)

这是大多数数据库系统的默认隔离级别(但不是 MySQL 默认的)。它满足了隔离的简单定义:一个事务只能看见已经提交事务所做的改变。这种隔离级别 也支持所谓 的 不可重复读(Nonrepeatable Read),因为同一事务的其他实例在该实例处理其间可能会有新的 commit,所以同一 select 可能返回不同结果。

  • Repeatable Read(可重读)

这是 MySQL 的默认事务隔离级别,它确保同一事务的多个实例在并发读取数据时,会看到同样的数据行。不过理论上,这会导致另一个棘手的问题:幻读 (Phantom Read)。

  • Serializable(可串行化)

通过强制事务排序,使之不可能相互冲突,从而解决幻读问题。简言之,它是在每个读的数据行上加上共享锁。在这个级别,可能导致大量的超时现象和锁竞争。

MySQL 默认采用的 REPEATABLE_READ隔离级别 Oracle 默认采用的 READ_COMMITTED隔离级别

事务隔离机制的实现基于锁机制和并发调度。其中并发调度使用的是MVVC(多版本并发控制),通过保存修改的旧版本信息来支持并发一致性读和回滚等特性。

因为隔离级别越低,事务请求的锁越少,所以大部分数据库系统的隔离级别都是READ-COMMITTED(读取提交内容):,但是你要知道的是InnoDB 存储引擎默认使用 REPEATABLE-READ(可重读)并不会有任何性能损失。

InnoDB 存储引擎在 分布式事务 的情况下一般会用到SERIALIZABLE(可串行化)隔离级别。

4. 什么是脏读?幻读?不可重复读?

1、脏读:事务 A 读取了事务 B 更新的数据,然后 B 回滚操作,那么 A 读取到的数据是脏数据

2、不可重复读:事务 A 多次读取同一数据,事务 B 在事务 A 多次读取的过程中,对数据作了更新并提交,导致事务 A 多次读取同一数据时,结果 不一致。

3、幻读:系统管理员 A 将数据库中所有学生的成绩从具体分数改为 ABCDE 等级,但是系统管理员 B 就在这个时候插入了一条具体分数的记录,当系统管理员 A 改结束后发现还有一条记录没有改过来,就好像发生了幻觉一样,这就叫幻读。

不可重复读侧重于修改,幻读侧重于新增或删除(多了或少量行),脏读是一个事务回滚影响另外一个事务。

5. 事务的实现原理

事务是基于重做日志文件(redo log)和回滚日志(undo log)实现的。

每提交一个事务必须先将该事务的所有日志写入到重做日志文件进行持久化,数据库就可以通过重做日志来保证事务的原子性和持久性。

每当有修改事务时,还会产生 undo log,如果需要回滚,则根据 undo log 的反向语句进行逻辑操作,比如 insert 一条记录就 delete 一条记录。undo log 主要实现数据库的一致性。

6. MySQL事务日志介绍下?

innodb 事务日志包括 redo log 和 undo log。

undo log 指事务开始之前,在操作任何数据之前,首先将需操作的数据备份到一个地方。redo log 指事务中操作的任何数据,将最新的数据备份到一个地方。

事务日志的目的:实例或者介质失败,事务日志文件就能派上用场。

redo log

redo log 不是随着事务的提交才写入的,而是在事务的执行过程中,便开始写入 redo 中。具体的落盘策略可以进行配置 。防止在发生故障的时间点,尚有脏页未写入磁盘,在重启 MySQL 服务的时候,根据 redo log 进行重做,从而达到事务的未入磁盘数据进行持久化这一特性。RedoLog 是为了实现事务的持久性而出现的产物。

undo log

undo log 用来回滚行记录到某个版本。事务未提交之前,Undo 保存了未提交之前的版本数据,Undo 中的数据可作为数据旧版本快照供其他并发事务进行快照读。是为了实现事务的原子性而出现的产物,在 MySQL innodb 存储引擎中用来实现多版本并发控制。

7. 什么是MySQL的 binlog?

MySQL的 binlog 是记录所有数据库表结构变更(例如 CREATE、ALTER TABLE)以及表数据修改(INSERT、UPDATE、DELETE)的二进制日志。binlog 不会记录 SELECT 和 SHOW 这类操作,因为这类操作对数据本身并没有修改,但你可以通过查询通用日志来查看 MySQL 执行过的所有语句。

MySQL binlog 以事件形式记录,还包含语句所执行的消耗的时间,MySQL 的二进制日志是事务安全型的。binlog 的主要目的是复制和恢复。

binlog 有三种格式,各有优缺点:

  • statement: 基于 SQL 语句的模式,某些语句和函数如 UUID, LOAD DATA INFILE 等在复制过程可能导致数据不一致甚至出错。
  • row: 基于行的模式,记录的是行的变化,很安全。但是 binlog 会比其他两种模式大很多,在一些大表中清除大量数据时在 binlog 中会生成很多条语句,可能导致从库延迟变大。
  • mixed: 混合模式,根据语句来选用是 statement 还是 row 模式。

8. 在事务中可以混合使用存储引擎吗?

尽量不要在同一个事务中使用多种存储引擎,MySQL服务器层不管理事务,事务是由下层的存储引擎实现的。

如果在事务中混合使用了事务型和非事务型的表(例如InnoDB和MyISAM表),在正常提交的情况下不会有什么问题。

但如果该事务需要回滚,非事务型的表上的变更就无法撤销,这会导致数据库处于不一致的状态,这种情况很难修复,事务的最终结果将无法确定。所以,为每张表选择合适的存储引擎非常重要。

9. MySQL中是如何实现事务隔离的?

读未提交和串行化基本上是不需要考虑的隔离级别,前者不加锁限制,后者相当于单线程执行,效率太差。

MySQL 在可重复读级别解决了幻读问题,是通过行锁和间隙锁的组合 Next-Key 锁实现的。

详细原理看这篇文章:https://haicoder.net/note/MySQL-interview/MySQL-interview-MySQL-trans-level.html

10. 什么是 MVCC?

MVCC, 即多版本并发控制。MVCC 的实现,是通过保存数据在某个时间点的快照来实现的。根据事务开始的时间不同,每个事务对同一张表,同一时刻看到的数据可能是不一样的。

11. MVCC 的实现原理

对于 InnoDB ,聚簇索引记录中包含 3 个隐藏的列:

  • ROW ID:隐藏的自增 ID,如果表没有主键,InnoDB 会自动按 ROW ID 产生一个聚集索引树。
  • 事务 ID:记录最后一次修改该记录的事务 ID。
  • 回滚指针:指向这条记录的上一个版本。

我们拿上面的例子,对应解释下 MVCC 的实现原理,如下图:

如图,首先 insert 语句向表 t1 中插入了一条数据,a 字段为 1,b 字段为 1, ROW ID 也为 1 ,事务 ID 假设为 1,回滚指针假设为 null。当执行 update t1 set b=666 where a=1 时,大致步骤如下:

  • 数据库会先对满足 a=1 的行加排他锁;
  • 然后将原记录复制到 undo 表空间中;
  • 修改 b 字段的值为 666,修改事务 ID 为 2;
  • 并通过隐藏的回滚指针指向 undo log 中的历史记录;
  • 事务提交,释放前面对满足 a=1 的行所加的排他锁。

在前面实验的第 6 步中,session2 查询的结果是 session1 修改之前的记录,这个记录就是来自 undolog 中。

因此可以总结出 MVCC 实现的原理大致是:

InnoDB 每一行数据都有一个隐藏的回滚指针,用于指向该行修改前的最后一个历史版本,这个历史版本存放在 undo log 中。如果要执行更新操作,会将原记录放入 undo log 中,并通过隐藏的回滚指针指向 undo log 中的原记录。其它事务此时需要查询时,就是查询 undo log 中这行数据的最后一个历史版本。

MVCC 最大的好处是读不加锁,读写不冲突,极大地增加了 MySQL 的并发性。通过 MVCC,保证了事务 ACID 中的 I(隔离性)特性。

1. 为什么要加锁?

当多个用户并发地存取数据时,在数据库中就会产生多个事务同时存取同一数据的情况。若对并发操作不加控制就可能会读取和存储不正确的数据,破坏数据库的一致性。

保证多用户环境下保证数据库完整性和一致性。

2. 按照锁的粒度分数据库锁有哪些?

在关系型数据库中,可以按照锁的粒度把数据库锁分为行级锁(INNODB引擎)、表级锁(MYISAM引擎)和页级锁(BDB引擎 )。

行级锁

  • 行级锁是MySQL中锁定粒度最细的一种锁,表示只针对当前操作的行进行加锁。行级锁能大大减少数据库操作的冲突。其加锁粒度最小,但加锁的开销也最大。行级锁分为共享锁 和 排他锁。
  • 开销大,加锁慢;会出现死锁;锁定粒度最小,发生锁冲突的概率最低,并发度也最高。

表级锁

  • 表级锁是MySQL中锁定粒度最大的一种锁,表示对当前操作的整张表加锁,它实现简单,资源消耗较少,被大部分MySQL引擎支持。最常使用的MYISAM与INNODB都支持表级锁定。表级锁定分为表共享读锁(共享锁)与表独占写锁(排他锁)。
  • 开销小,加锁快;不会出现死锁;锁定粒度大,发出锁冲突的概率最高,并发度最低。

页级锁

  • 页级锁是MySQL中锁定粒度介于行级锁和表级锁中间的一种锁。表级锁速度快,但冲突多,行级冲突少,但速度慢。所以取了折衷的页级,一次锁定相邻的一组记录。BDB支持页级锁
  • 开销和加锁时间界于表锁和行锁之间;会出现死锁;锁定粒度界于表锁和行锁之间,并发度一般

MyISAM和InnoDB存储引擎使用的锁:

  • MyISAM采用表级锁(table-level locking)。
  • InnoDB支持行级锁(row-level locking)和表级锁,默认为行级锁

3. 从锁的类别上分MySQL都有哪些锁呢?

从锁的类别上来讲,有共享锁和排他锁。

  • 共享锁: 又叫做读锁。 当用户要进行数据的读取时,对数据加上共享锁。共享锁可以同时加上多个。
  • 排他锁: 又叫做写锁。 当用户要进行数据的写入时,对数据加上排他锁。排他锁只可以加一个,他和其他的排他锁,共享锁都相斥。

用上面的例子来说就是用户的行为有两种,一种是来看房,多个用户一起看房是可以接受的。 一种是真正的入住一晚,在这期间,无论是想入住的还是想看房的都不可以。

锁的粒度取决于具体的存储引擎,InnoDB实现了行级锁,页级锁,表级锁。

他们的加锁开销从大到小,并发能力也是从大到小。

4. 数据库的乐观锁和悲观锁是什么?怎么实现的?

数据库管理系统(DBMS)中的并发控制的任务是确保在多个事务同时存取数据库中同一数据时不破坏事务的隔离性和统一性以及数据库的统一性。乐观并发控制(乐观锁)和悲观并发控制(悲观锁)是并发控制主要采用的技术手段。

  • 悲观锁:假定会发生并发冲突,屏蔽一切可能违反数据完整性的操作。在查询完数据的时候就把事务锁起来,直到提交事务。实现方式:使用数据库中的锁机制
  • 乐观锁:假设不会发生并发冲突,只在提交操作时检查是否违反数据完整性。在修改数据的时候把事务锁起来,通过version的方式来进行锁定。实现方式:乐一般会使用版本号机制或CAS算法实现。

两种锁的使用场景

从上面对两种锁的介绍,我们知道两种锁各有优缺点,不可认为一种好于另一种,像乐观锁适用于写比较少的情况下(多读场景),即冲突真的很少发生的时候,这样可以省去了锁的开销,加大了系统的整个吞吐量。

但如果是多写的情况,一般会经常产生冲突,这就会导致上层应用会不断的进行retry,这样反倒是降低了性能,所以一般多写的场景下用悲观锁就比较合适。

5. InnoDB引擎的行锁是怎么实现的?

InnoDB是基于索引来完成行锁

例: select * from tab_with_index where id = 1 for update;

for update 可以根据条件来完成行锁锁定,并且 id 是有索引键的列,如果 id 不是索引键那么InnoDB将完成表锁,并发将无从谈起

6. 什么是死锁?怎么解决?

死锁是指两个或多个事务在同一资源上相互占用,并请求锁定对方的资源,从而导致恶性循环的现象。

常见的解决死锁的方法

1、如果不同程序会并发存取多个表,尽量约定以相同的顺序访问表,可以大大降低死锁机会。

2、在同一个事务中,尽可能做到一次锁定所需要的所有资源,减少死锁产生概率;

3、对于非常容易产生死锁的业务部分,可以尝试使用升级锁定颗粒度,通过表级锁定来减少死锁产生的概率;

如果业务处理不好可以用分布式事务锁或者使用乐观锁

7. 隔离级别与锁的关系

在Read Uncommitted级别下,读取数据不需要加共享锁,这样就不会跟被修改的数据上的排他锁冲突

在Read Committed级别下,读操作需要加共享锁,但是在语句执行完以后释放共享锁;

在Repeatable Read级别下,读操作需要加共享锁,但是在事务提交之前并不释放共享锁,也就是必须等待事务执行完毕以后才释放共享锁。

SERIALIZABLE 是限制性最强的隔离级别,因为该级别锁定整个范围的键,并一直持有锁,直到事务完成。

8. 优化锁方面的意见?

  • 使用较低的隔离级别
  • 设计索引,尽量使用索引去访问数据,加锁更加精确,从而减少锁冲突
  • 选择合理的事务大小,给记录显示加锁时,最好一次性请求足够级别的锁。列如,修改数据的话,最好申请排他锁,而不是先申请共享锁,修改时在申请排他锁,这样会导致死锁
  • 不同的程序访问一组表的时候,应尽量约定一个相同的顺序访问各表,对于一个表而言,尽可能的固定顺序的获取表中的行。这样大大的减少死锁的机会。
  • 尽量使用相等条件访问数据,这样可以避免间隙锁对并发插入的影响
  • 不要申请超过实际需要的锁级别
  • 数据查询的时候不是必要,不要使用加锁。MySQL的MVCC可以实现事务中的查询不用加锁,优化事务性能:MVCC只在committed read(读提交)和 repeatable read (可重复读)两种隔离级别
  • 对于特定的事务,可以使用表锁来提高处理速度活着减少死锁的可能。

分库分表

1. 为什么要分库分表?

分表

比如你单表都几千万数据了,你确定你能扛住么?绝对不行,单表数据量太大,会极大影响你的 sql执行的性能,到了后面你的 sql 可能就跑的很慢了。一般来说,就以我的经验来看,单表到几百万的时候,性能就会相对差一些了,你就得分表了。

分表就是把一个表的数据放到多个表中,然后查询的时候你就查一个表。比如按照用户 id 来分表,将一个用户的数据就放在一个表中。然后操作的时候你对一个用户就操作那个表就好了。这样可以控制每个表的数据量在可控的范围内,比如每个表就固定在 200 万以内。

分库

分库就是你一个库一般我们经验而言,最多支撑到并发 2000,一定要扩容了,而且一个健康的单库并发值你最好保持在每秒 1000 左右,不要太大。那么你可以将一个库的数据拆分到多个库中,访问的时候就访问一个库好了。

这就是所谓的分库分表。

2. 用过哪些分库分表中间件?不同的分库分表中间件都有什么优点和缺点?

这个其实就是看看你了解哪些分库分表的中间件,各个中间件的优缺点是啥?然后你用过哪些分库分表的中间件。

比较常见的包括:

  • cobar
  • TDDL
  • atlas
  • sharding-jdbc
  • mycat

cobar

阿里 b2b 团队开发和开源的,属于 proxy 层方案。早些年还可以用,但是最近几年都没更新了,基本没啥人用,差不多算是被抛弃的状态吧。而且不支持读写分离、存储过程、跨库 join 和分页等操作。

TDDL

淘宝团队开发的,属于 client 层方案。支持基本的 crud 语法和读写分离,但不支持 join、多表查询等语法。目前使用的也不多,因为还依赖淘宝的 diamond 配置管理系统。

atlas

360 开源的,属于 proxy 层方案,以前是有一些公司在用的,但是确实有一个很大的问题就是社区最新的维护都在 5 年前了。所以,现在用的公司基本也很少了。

sharding-jdbc

当当开源的,属于 client 层方案。确实之前用的还比较多一些,因为 SQL 语法支持也比较多,没有太多限制,而且目前推出到了 2.0 版本,支持分库分表、读写分离、分布式 id 生成、柔性事务(最大努力送达型事务、TCC 事务)。而且确实之前使用的公司会比较多一些(这个在官网有登记使用的公司,可以看到从 2017 年一直到现在,是有不少公司在用的),目前社区也还一直在开发和维护,还算是比较活跃,个人认为算是一个现在也可以选择的方案

mycat

基于 cobar 改造的,属于 proxy 层方案,支持的功能非常完善,而且目前应该是非常火的而且不断流行的数据库中间件,社区很活跃,也有一些公司开始在用了。但是确实相比于 sharding jdbc 来说,年轻一些,经历的锤炼少一些。

3. 如何对数据库如何进行垂直拆分或水平拆分的?

水平拆分的意思,就是把一个表的数据给弄到多个库的多个表里去,但是每个库的表结构都一样,只不过每个库表放的数据是不同的,所有库表的数据加起来就是全部数据。水平拆分的意义,就是将数据均匀放更多的库里,然后用多个库来抗更高的并发,还有就是用多个库的存储容量来进行扩容。

垂直拆分的意思,就是把一个有很多字段的表给拆分成多个表或者是多个库上去。每个库表的结构都不一样,每个库表都包含部分字段。一般来说,会将较少的访问频率很高的字段放到一个表里去,然后将较多的访问频率很低的字段放到另外一个表里去。因为数据库是有缓存的,你访问频率高的行字段越少,就可以在缓存里缓存更多的行,性能就越好。这个一般在表层面做的较多一些。

两种分库分表的方式

  • 一种是按照 range 来分,就是每个库一段连续的数据,这个一般是按比如时间范围来的,但是这种一般较少用,因为很容易产生热点问题,大量的流量都打在最新的数据上了。
  • 或者是按照某个字段hash一下均匀分散,这个较为常用。

range 来分,好处在于说,扩容的时候很简单,因为你只要预备好,给每个月都准备一个库就可以了,到了一个新的月份的时候,自然而然,就会写新的库了;缺点,但是大部分的请求,都是访问最新的数据。实际生产用 range,要看场景。

hash 分发,好处在于说,可以平均分配每个库的数据量和请求压力;坏处在于说扩容起来比较麻烦,会有一个数据迁移的过程,之前的数据需要重新计算 hash 值重新分配到不同的库或表

读写分离、主从同步(复制)

1. 什么是MySQL主从同步?

主从同步使得数据可以从一个数据库服务器复制到其他服务器上,在复制数据时,一个服务器充当主服务器(master),其余的服务器充当从服务器(slave)。

因为复制是异步进行的,所以从服务器不需要一直连接着主服务器,从服务器甚至可以通过拨号断断续续地连接主服务器。通过配置文件,可以指定复制所有的数据库,某个数据库,甚至是某个数据库上的某个表。

2. MySQL主从同步的目的?为什么要做主从同步?

  1. 通过增加从服务器来提高数据库的性能,在主服务器上执行写入和更新,在从服务器上向外提供读功能,可以动态地调整从服务器的数量,从而调整整个数据库的性能。
  2. 提高数据安全-因为数据已复制到从服务器,从服务器可以终止复制进程,所以,可以在从服务器上备份而不破坏主服务器相应数据
  3. 在主服务器上生成实时数据,而在从服务器上分析这些数据,从而提高主服务器的性能
  4. 数据备份。一般我们都会做数据备份,可能是写定时任务,一些特殊行业可能还需要手动备份,有些行业要求备份和原数据不能在同一个地方,所以主从就能很好的解决这个问题,不仅备份及时,而且还可以多地备份,保证数据的安全

3. 如何实现MySQL的读写分离?

其实很简单,就是基于主从复制架构,简单来说,就搞一个主库,挂多个从库,然后我们就单单只是写主库,然后主库会自动把数据给同步到从库上去。

4. MySQL主从复制流程和原理?

基本原理流程,是3个线程以及之间的关联

主:binlog线程——记录下所有改变了数据库数据的语句,放进master上的binlog中;

从:io线程——在使用start slave 之后,负责从master上拉取 binlog 内容,放进自己的relay log中;

从:sql执行线程——执行relay log中的语句;

复制过程如下

Binary log:主数据库的二进制日志

Relay log:从服务器的中继日志

第一步:master在每个事务更新数据完成之前,将该操作记录串行地写入到binlog文件中。

第二步:salve开启一个I/O Thread,该线程在master打开一个普通连接,主要工作是binlog dump process。如果读取的进度已经跟上了master,就进入睡眠状态并等待master产生新的事件。I/O线程最终的目的是将这些事件写入到中继日志中。

第三步:SQL Thread会读取中继日志,并顺序执行该日志中的SQL事件,从而与主数据库中的数据保持一致。

5. MySQL主从同步延时问题如何解决?

MySQL 实际上在有两个同步机制,一个是半同步复制,用来 解决主库数据丢失问题;一个是并行复制,用来 解决主从同步延时问题。

  • 半同步复制,也叫 semi-sync 复制,指的就是主库写入 binlog 日志之后,就会将强制此时立即将数据同步到从库,从库将日志写入自己本地的 relay log 之后,接着会返回一个 ack 给主库,主库接收到至少一个从库的 ack 之后才会认为写操作完成了。
  • 并行复制,指的是从库开启多个线程,并行读取 relay log 中不同库的日志,然后并行重放不同库的日志,这是库级别的并行。

MySQL优化

1. 如何定位及优化SQL语句的性能问题?

对于低性能的SQL语句的定位,最重要也是最有效的方法就是使用执行计划,MySQL提供了explain命令来查看语句的执行计划。 我们知道,不管是哪种数据库,或者是哪种数据库引擎,在对一条SQL语句进行执行的过程中都会做很多相关的优化,对于查询语句,最重要的优化方式就是使用索引。

而执行计划,就是显示数据库引擎对于SQL语句的执行的详细情况,其中包含了是否使用索引,使用什么索引,使用的索引的相关信息等。

2. 大表数据查询,怎么优化

  • 优化shema、sql语句+索引;
  • 第二加缓存,memcached, redis;
  • 主从复制,读写分离;
  • 垂直拆分,根据你模块的耦合度,将一个大的系统分为多个小的系统,也就是分布式系统;
  • 水平切分,针对数据量大的表,这一步最麻烦,最能考验技术水平,要选择一个合理的sharding key, 为了有好的查询效率,表结构也要改动,做一定的冗余,应用也要改,sql中尽量带sharding key,将数据定位到限定的表上去查,而不是扫描全部的表;

3. 超大分页怎么处理?

数据库层面,这也是我们主要集中关注的(虽然收效没那么大),类似于select * from table where age > 20 limit 1000000,10 这种查询其实也是有可以优化的余地的. 这条语句需要 load1000000 数据然后基本上全部丢弃,只取 10 条当然比较慢. 当时我们可以修改为select * from table where id in (select id from table where age > 20 limit 1000000,10).这样虽然也 load 了一百万的数据,但是由于索引覆盖,要查询的所有字段都在索引中,所以速度会很快。

解决超大分页,其实主要是靠缓存,可预测性的提前查到内容,缓存至redis等k-V数据库中,直接返回即可.

在阿里巴巴《Java开发手册》中,对超大分页的解决办法是类似于上面提到的第一种.

【推荐】利用延迟关联或者子查询优化超多分页场景。
说明:MySQL并不是跳过offset行,而是取offset+N行,然后返回放弃前offset行,返回N行,那当offset特别大的时候,效率就非常的低下,要么控制返回的总页数,要么对超过特定阈值的页数进行SQL改写。
正例:先快速定位需要获取的id段,然后再关联:
SELECT a.* FROM 表1 a, (select id from 表1 where 条件 LIMIT 100000,20 ) b where a.id=b.id

4. 统计过慢查询吗?对慢查询都怎么优化过?

在业务系统中,除了使用主键进行的查询,其他的我都会在测试库上测试其耗时,慢查询的统计主要由运维在做,会定期将业务中的慢查询反馈给我们。

慢查询的优化首先要搞明白慢的原因是什么? 是查询条件没有命中索引?是load了不需要的数据列?还是数据量太大?

所以优化也是针对这三个方向来的,

  • 首先分析语句,看看是否load了额外的数据,可能是查询了多余的行并且抛弃掉了,可能是加载了许多结果中并不需要的列,对语句进行分析以及重写。
  • 分析语句的执行计划,然后获得其使用索引的情况,之后修改语句或者修改索引,使得语句可以尽可能的命中索引。
  • 如果对语句的优化已经无法进行,可以考虑表中的数据量是否太大,如果是的话可以进行横向或者纵向的分表。

5. 如何优化查询过程中的数据访问

  • 访问数据太多导致查询性能下降
  • 确定应用程序是否在检索大量超过需要的数据,可能是太多行或列
  • 确认MySQL服务器是否在分析大量不必要的数据行
  • 查询不需要的数据。解决办法:使用limit解决
  • 多表关联返回全部列。解决办法:指定列名
  • 总是返回全部列。解决办法:避免使用SELECT *
  • 重复查询相同的数据。解决办法:可以缓存数据,下次直接读取缓存
  • 是否在扫描额外的记录。解决办法: 使用explain进行分析,如果发现查询需要扫描大量的数据,但只返回少数的行,可以通过如下技巧去优化: 使用索引覆盖扫描,把所有的列都放到索引中,这样存储引擎不需要回表获取对应行就可以返回结果。
  • 改变数据库和表的结构,修改数据表范式
  • 重写SQL语句,让优化器可以以更优的方式执行查询。

6. 如何优化关联查询

  • 确定ON或者USING子句中是否有索引。
  • 确保GROUP BY和ORDER BY只有一个表中的列,这样MySQL才有可能使用索引。

7. 数据库结构优化

一个好的数据库设计方案对于数据库的性能往往会起到事半功倍的效果。

需要考虑数据冗余、查询和更新的速度、字段的数据类型是否合理等多方面的内容。

  1. 将字段很多的表分解成多个表

对于字段较多的表,如果有些字段的使用频率很低,可以将这些字段分离出来形成新表。

因为当一个表的数据量很大时,会由于使用频率低的字段的存在而变慢。

  1. 增加中间表

对于需要经常联合查询的表,可以建立中间表以提高查询效率。

通过建立中间表,将需要通过联合查询的数据插入到中间表中,然后将原来的联合查询改为对中间表的查询。

  1. 增加冗余字段

设计数据表时应尽量遵循范式理论的规约,尽可能的减少冗余字段,让数据库设计看起来精致、优雅。但是,合理的加入冗余字段可以提高查询速度。

表的规范化程度越高,表和表之间的关系越多,需要连接查询的情况也就越多,性能也就越差。

注意:

冗余字段的值在一个表中修改了,就要想办法在其他表中更新,否则就会导致数据不一致的问题。

8. MySQL数据库cpu飙升到500%的话他怎么处理?

当 cpu 飙升到 500%时,先用操作系统命令 top 命令观察是不是 MySQLd 占用导致的,如果不是,找出占用高的进程,并进行相关处理。

如果是 MySQLd 造成的, show processlist,看看里面跑的 session 情况,是不是有消耗资源的 sql 在运行。找出消耗高的 sql,看看执行计划是否准确, index 是否缺失,或者实在是数据量太大造成。

一般来说,肯定要 kill 掉这些线程(同时观察 cpu 使用率是否下降),等进行相应的调整(比如说加索引、改 sql、改内存参数)之后,再重新跑这些 SQL。

也有可能是每个 sql 消耗资源并不多,但是突然之间,有大量的 session 连进来导致 cpu 飙升,这种情况就需要跟应用一起来分析为何连接数会激增,再做出相应的调整,比如说限制连接数等。

9. 大表怎么优化?

类似的问题:某个表有近千万数据,CRUD比较慢,如何优化?分库分表了是怎么做的?分表分库了有什么问题?有用到中间件么?他们的原理知道么?

当MySQL单表记录数过大时,数据库的CRUD性能会明显下降,一些常见的优化措施如下:

  • 限定数据的范围: 务必禁止不带任何限制数据范围条件的查询语句。比如:我们当用户在查询订单历史的时候,我们可以控制在一个月的范围内;
  • 读/写分离: 经典的数据库拆分方案,主库负责写,从库负责读;
  • 缓存: 使用MySQL的缓存,另外对重量级、更新少的数据可以考虑;
  • 通过分库分表的方式进行优化,主要有垂直分表和水平分表。

刷题

Java刷题技巧

Java输入输出问题

Scanner sc=new Scanner(System.in);

输入一行字符串:sc.nextLine()

输入一个基本类型:sc.nextInt、nextLong、nextDouble

输入一个字符:scanner.next().charAt(0);

输出

System.out.printf("%d",value);

力扣刷题常见的特殊情况

 1,常见的&操作便捷的含义

n&(-n)表示去lowbits,取二进制最低位,树形数组中应用。

n&(n-1)表示将二进制最低位进行取反。

2,常见的list转换成[]int、[]String方法

list-->[]int:list.stream().mapToInt(Integer::valueOf).toArray();

list-->[]String:list.toArray(new String[0]);

3,list转换成[][]int方法

ArrayList<int []> res=new ArrayList<>();

res.toArray(new int [0][]);

4,堆数据结构

PriorityQueue<Integer> q=new PriorityQueue<>(new Comparator<Integer>(

int compare(Integer o1,Integer o2) {return 0;}

));

5.Java中Long转换成int

(int)Long,可能出现溢出,且java不支持。

Long.intValue(),Long对象中包含此转换方法。

Integer.parseInt(String.valueOf(long)),先转成字符串,在转成int。

6、快速幂

思想:

88 * 0110 0011(2) = 88 * 0 * 2^7 
                  + 88 * 1 * 2^6 
                  + 88 * 1 * 2^5 
                  + 88 * 0 * 2^4 
                  + 88 * 0 * 2^3 
                  + 88 * 0 * 2^2 
                  + 88 * 1 * 2^1
                  + 88 * 1 * 2^0
代码:

int quickMulti(int A, int B) {
    int ans = 0;
    for ( ; B; B >>= 1) {
        if (B & 1) {
            ans += A;
        }
        A <<= 1;
    }
    return ans;

7、摩尔投票法

应用:找出数组中出现次数超过一半的数

具体步骤:相同的数,则保留记录,不同的数则删除,,直到末尾。

8、树状数组

应用:求逆序数

class BIT{
    int []tree;
    int n;
    public BIT(int n){
         tree=new int[n+1];
         this.n=n;
    }
    public int lowbit(int n){
        return (n)&(-n);
    }
    public int query(int index){
        int res=0;
        while(index!=0){
            res+=tree[index];
            index-=lowbit(index);
        }
        return res;
    }
    public void add(int index){
        while(index<=n){
            tree[index]++;
            index-=tree[index];
        }
    }
}

9、质数的判断方法

1,常见方法,直接通过遍历到n的开平法进行整除判断,效率不高。

2,通过标志方法,设置一个bool数组,先进行初始化,奇数设置为true,偶数设置为false,只需将前面为true表示为质数的倍数设置为flase即可,效率较上面高。

3,质数分布的规律:大于等于5的质数一定和6的倍数相邻。例如5和7,11和13,17和19等等;

bool isPrime( int num )
{
    //两个较小数另外处理
    if(num == 2||num == 3 )
        return 1;
    //不在6的倍数两侧的一定不是质数
    if(num % 6 != 1&&num % 6 != 5)
        return 0;
    int tmp = sqrt(num);
    //在6的倍数两侧的也可能不是质数
    for(int i = 5;i <= tmp;i += 6)
        if(num %i == 0||num % (i+ 2) == 0)
            return 0;
    //排除所有,剩余的是质数
    return 1;

10、博弈-NIm游戏

n个石子,两个人取,每次可以取1-m个石子,谁取到最后一个石子就赢得比赛,取的局面为(m+1)的时候必输,且为(m+1)的倍数时候也必输。

11,new Comparator<>(){}

int compare(Integer o1,Integer o2)方法中的简单写法为:return o1-o2;

一般最简单写法:

  • Collections.sort(arr,(o1,o2)->o1.val-o2.val);// 升序
  • Collection.sort(arr,(o1,o2)->{return o1.val-o2.val;}
  • new Comparator<>(){}中的int compare(Integer o1,Integer o2)
  • Arrays.sort(T[],new Comparator<>(){});

12,Arrays.copyOf(arr[],len)和System.arraycopy(src, srcPos, dest, destPos, length)

前者一般是数组的扩容,产生一个新的对象,后者是数组的复制,会对源数组进行赋值,不会产生新的数组。

13,快速排序

思想:随机取一个值作为基准(一般去做下标),对数组的值分为大于和小于基准两部分,然后采用递归的方式全部使得数组有序。

  public static void quickSort(int []nums,int l,int r){
        if(l<r){
            int index=partition(nums,l,r);
            //分治递归
            quickSort(nums,l,index-1);
            quickSort(nums,index+1,r);
        }
    }
    // partition就是让按照基准划分两部分
    public static int partition(int []nums,int l,int r){
        int flag=l;//标识
        int index=l+1;//标识右部分的初始位置
        for(int i=index;i<=r;i++){
            if(nums[i]<nums[flag]){
                // 交换
                swap(nums,i,index);
                index++;
            }
        }
        //将flag和前半部分最后一个进行交换
        swap(nums,index-1,flag);
        // index-1是标识的下标
        return index-1;
    }
    public static  void swap(int [] nums,int i,int j){
        int t=nums[i];
        nums[i]=nums[j];
        nums[j]=t;
    }

14,N皇后问题-回朔法

NxN的期盼,放置n个皇后,要求行列对角线不能重复。

  1. 思路一:一行一行进行试探,每次试探一步进行标记,然后求出所有的可能。
  2. 思路二:用arr[n]记录每次放皇后的列行,arr[i]表示第i行的皇后位置放在arr[i]位置上面,满足列不相等的情况只要arr[i]!=arr[j](j<i),对角线不相等的情况是i+arr[i]!=j+arr[j],进行递归即可。
 class Main {
    static int resultCount = 0;

    private static boolean place(int[] arr, int s) {
        for(int i = 0; i < s; i++) {
            if((arr[i] == arr[s]) || (Math.abs(i-s) == Math.abs(arr[i]-arr[s]))) {
                return false;
            }
        }

        return true;
    }

    public static void tria(int[] arr, int i, int n) {
        if(i >= n) {
            // 打印出来
            for(int j=0;j<8;j++){
                for(int k=0;k<8;k++){
                    if(arr[j]==k){
                        System.out.print("*");
                    }else{
                        System.out.print(".");
                    }
                }
                System.out.println();

            }
            System.out.println("------------------------");
            ++resultCount;
        } else {
            for(int j = 0; j < n; j++) {
                arr[i] = j;
                if(place(arr, i)) {
                    tria(arr, i+1, n);
                }
            }
        }
    }

    public static void main(String[] args) {
        int[] queen = new int[8];
        tria(queen, 0, 8);

        System.out.println(resultCount);
    }
}

15,Collections常用的工具方法

排序操作

void reverse(List list)//反转
void shuffle(List list)//随机排序
void sort(List list)//按自然排序的升序排序
void sort(List list, Comparator c)//定制排序,由Comparator控制排序逻辑
void swap(List list, int i , int j)//交换两个索引位置的元素
void rotate(List list, int distance)//旋转。当distance为正数时,将list后distance个元素整体移到前面。当distance为负数时,将 list的前distance个元素整体移到后面

 查找替换

int binarySearch(List list, Object key)//对List进行二分查找,返回索引,注意List必须是有序的
int max(Collection coll)//根据元素的自然顺序,返回最大的元素。 类比int min(Collection coll)
int max(Collection coll, Comparator c)//根据定制排序,返回最大元素,排序规则由Comparatator类控制。类比int min(Collection coll, Comparator c)
void fill(List list, Object obj)//用指定的元素代替指定list中的所有元素
int frequency(Collection c, Object o)//统计元素出现次数
int indexOfSubList(List list, List target)//统计target在list中第一次出现的索引,找不到则返回-1,类比int lastIndexOfSubList(List source, list target)
boolean replaceAll(List list, Object oldVal, Object newVal)//用新元素替换旧元素

 安全(Collections可以将不安全的容易变为安全的)

synchronizedCollection(Collection<T>  c) //返回指定 collection 支持的同步(线程安全的)collection。
synchronizedList(List<T> list)//返回指定列表支持的同步(线程安全的)List。
synchronizedMap(Map<K,V> m) //返回由指定映射支持的同步(线程安全的)Map。
synchronizedSet(Set<T> s) //返回指定 set 支持的同步(线程安全的)set。

 16,Arrays.asList(new int[]{}

数组转换成集合,直接转换成的是Array&ArrayList,并不是真正的ArrayList,在在一个new ArrayList<>(Arrays.asList(new int[]{}));

17,LinkedHashMap

应用:LRU

构造方法:public LinkedHashMap(int initialCapacity, float loadFactor, boolean accessOrder)

accessOrder=false表示插入顺序,true表示访问顺序,且HashMap并不能保持插入顺序,LinkedHashMap的子类从写removeEldestEntry()方法可以实现LRU固定缓冲区大小置换的功能。

18,拓扑排序

多任务存在先后,找出合法的任务执行序列,应用(课程表中的先修问题-解决存在环的问题)

思想:可以将入度为0的结点加入队列,然后进行出队为key,将其余入度为key的结点入度数减一,并将入度为0的加入队列,总的出队数等于总的结点数的话则表明存在拓扑排序。

19,数组中第k大的数-面试题

排序+找下标

小根堆,将数组一步一步加入根堆,节点数量超出k范围则剔除,直到末尾。

快速标记法-基于快速排序实,partition不变,增加quickSelect方法

quickSelect(int l,int r,int k,int []nums){
    if(l>r)return;
    while(l<r){
        int p=partition(l,r,nums);
        if(p==k)return nums[p];
        else if(p>k) r=p-1;
        else l=p+1;
}
}

剑指offer(第二版)

剑指 Offer 03 数组中重复的数字

找出数组中重复的数字。
在一个长度为 n 的数组 nums 里的所有数字都在 0~n-1 的范围内。数组中某些数字是重复的,但不知道有几个数字重复了,也不知道每个数字重复了几次。请找出数组中任意一个重复的数字。

方法:

  1. 排序+遍历
  2. 用Hashset容器进行去重
  3. 临时数组tep,用于记录出现的次数
class Solution {
    public int findRepeatNumber(int[] nums) {
        int tep[]=new int[nums.length];
        int n=nums.length;
        for (int i=0;i<nums.length;i++)
        {
           tep[nums[i]]+=n;
        }
        for (int i=0;i<nums.length;i++)
        {
            if(tep[nums[i]]>n)
            {
                return nums[i];
            }
        }
        return 0;
    }
}

剑指 offer 04 二维数组中查找

在一个 n * m 的二维数组中,每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺序排序。请完成一个高效的函数,输入这样的一个二维数组和一个整数,判断数组中是否含有该整数。

示例:

现有矩阵 matrix 如下:

[
  [1,   4,  7, 11, 15],
  [2,   5,  8, 12, 19],
  [3,   6,  9, 16, 22],
  [10, 13, 14, 17, 24],
  [18, 21, 23, 26, 30]
]

给定 target = 5,返回 true

给定 target = 20,返回 false

 方法:

  1. 二分查找,对每行进行二分查找。
class Solution {
    public boolean findNumberIn2DArray(int[][] matrix, int target) {
        for (int i=0;i<matrix.length;i++)
        {
           if(binSerach(0,matrix[0].length-1,target,i,matrix))
           {
               return true;
           }
        }
        return false;
    }
    public boolean binSerach(int l,int r,int tar,int index,int [][]matrix){
        if (l>r){
            return false;
        }
        int m=(l+r)/2;
        if(matrix[index][m]==tar)
        {
            return true;
        }else if(matrix[index][m]>tar)
        {
             return binSerach(l,m-1,tar,index,matrix);
        }else{
             return binSerach(m+1,r,tar,index,matrix);
        }
      
    }
}

剑指 Offer 05 替换空格

难度简单224

请实现一个函数,把字符串 s 中的每个空格替换成"%20"。

示例 1:

输入:s = "We are happy."
输出:"We%20are%20happy."
class Solution {
    public String replaceSpace(String s) {
        return s.replace(" ","%20");
    }
}

剑指 Offer 06 从尾到头打印链表

难度简单247

输入一个链表的头节点,从尾到头反过来返回每个节点的值(用数组返回)。

示例 1:

输入:head = [1,3,2]
输出:[2,3,1]

限制:

0 <= 链表长度 <= 10000

 方法:

将list转换成int []

  1. 先存入list在添加到int[]
  2. 直接通过list.stream().mapToInt(Integer::valueOf).toArray();进行转换
/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode(int x) { val = x; }
 * }
 */
class Solution {
    public int[] reversePrint(ListNode head) {
        LinkedList<Integer> res=new LinkedList<>();
        while(head!=null)
        {
            res.addFirst(head.val);
            head=head.next;
        }
        // int [] res1=new int[res.size()];
        // for(int i=0;i<res.size();i++)
        // {
        //     res1[i]=res.get(i);
        // }
        // return res1;
        return res.stream().mapToInt(Integer::valueOf).toArray();
    }
}

 剑指 Offer 07 重建二叉树

难度中等685

输入某二叉树的前序遍历和中序遍历的结果,请构建该二叉树并返回其根节点。

假设输入的前序遍历和中序遍历的结果中都不含重复的数字。

 方法:

  1. 以前序遍历为模板,找出前序遍历的根、左、右进行划开,对应中序遍历,找出前序遍历除根节点外左子树的长度,和右子树的常数,递归进行创造建树。
class Solution {
    Map<Integer,Integer> mymap;
    public TreeNode buildTree(int[] preorder, int[] inorder) {
        mymap=new HashMap<>();
        int len=preorder.length;
        // 将中序遍历按照键(节点值)值(索引位置)
        for(int i=0;i<len;i++)
        {
            mymap.put(inorder[i],i);
        }
        return  creatTree(preorder,inorder,0,len-1,0,len-1);
    }
    public TreeNode creatTree(int[] preorder,int [] inorder,int preorder_left,int preorder_right,int inorder_left,int inorder_right){
        if(preorder_left>preorder_right)return null;
        //创建根节点
        TreeNode root=new TreeNode(preorder[preorder_left]);
        // 查找根节点在中序遍历的位置
        int root_index=mymap.get(preorder[preorder_left]);
        int left_length=root_index-inorder_left;
        //得到根的左子树
        root.left=creatTree(preorder,inorder,preorder_left+1,preorder_left+left_length,inorder_left,root_index-1);
        //得到根的右子树
        root.right=creatTree(preorder,inorder,preorder_left+left_length+1,preorder_right,root_index+1,inorder_right);
        return root;
    }
}

剑指 Offer 09 用两个栈实现队列

难度简单457

用两个栈实现一个队列。队列的声明如下,请实现它的两个函数 appendTail 和 deleteHead ,分别完成在队列尾部插入整数和在队列头部删除整数的功能。(若队列中没有元素,deleteHead 操作返回 -1 )

方法:

  1. 直接用LinkedList中的方法
  2. 用两个栈的方法
方法一
class CQueue {

    LinkedList<Integer> res;
    public CQueue() {
        res=new LinkedList<>();
    }
    
    public void appendTail(int value) {
        res.addLast(value);
    }
    
    public int deleteHead() {
        if(res.size()==0){
            return -1;
        }
        int t=res.getFirst();
        res.removeFirst();
        return t;
    }
}
方法二

class CQueue {

    Stack <Integer> a=new Stack<>();
    Stack <Integer> b=new Stack<>();
    public CQueue() {
       
    }
    
    public void appendTail(int value) {
        a.push(value);
    }
    
    public int deleteHead() {
        if(a.size()==0){
            return -1;
        }

        while(!a.isEmpty()){
            b.push(a.peek());
            a.pop();
        }

        int res=b.peek();
        b.pop();

        while(!b.isEmpty()){
            a.push(b.peek());
            b.pop();
        }

        return res;
    }
}

/**
 * Your CQueue object will be instantiated and called as such:
 * CQueue obj = new CQueue();
 * obj.appendTail(value);
 * int param_2 = obj.deleteHead();
 */

 剑指 Offer 10 斐波那契数列

难度简单307

写一个函数,输入 n ,求斐波那契(Fibonacci)数列的第 n 项(即 F(N))。斐波那契数列的定义如下:

F(0) = 0,   F(1) = 1
F(N) = F(N - 1) + F(N - 2), 其中 N > 1.

斐波那契数列由 0 和 1 开始,之后的斐波那契数就是由之前的两数相加而得出。

答案需要取模 1e9+7(1000000007),如计算初始结果为:1000000008,请返回 1。

简单的迭代思想

class Solution {
    public int fib(int n) {
     if(n==0)return 0;
     if (n==1)return 1;
      int t1=0;
      int t2=1;
      int t3=0;
       for(int i=2;i<=n;i++)
       {
           t3=t1+t2;
           if(t3>1000000007){
               t3=t3%1000000007;
           }
           t1=t2;
           t2=t3;
       }
       return t3;
    }
}

剑指 Offer 10 青蛙跳台阶问题

难度简单246

一只青蛙一次可以跳上1级台阶,也可以跳上2级台阶。求该青蛙跳上一个 n 级的台阶总共有多少种跳法。

答案需要取模 1e9+7(1000000007),如计算初始结果为:1000000008,请返回 1。

 方法:

  1. 动态规划:// dp[i]表示青蛙跳到第i个台阶共需要多少种跳法
class Solution {
    public int numWays(int n) {
        // dp[i]表示青蛙跳到第i个台阶共需要多少种跳法
        int dp[]=new int[101];
        dp[0]=1;
        dp[1]=1;
        for(int i=2;i<=n;i++){
            dp[i]=dp[i-2]+dp[i-1];
            if (dp[i]>1000000007){
                dp[i]%=1000000007;
            }
        }
        return dp[n];
    }
}

剑指 Offer 11 旋转数组的最小数字

难度简单542

把一个数组最开始的若干个元素搬到数组的末尾,我们称之为数组的旋转。

给你一个可能存在 重复 元素值的数组 numbers ,它原来是一个升序排列的数组,并按上述情形进行了一次旋转。请返回旋转数组的最小元素。例如,数组 [3,4,5,1,2] 为 [1,2,3,4,5] 的一次旋转,该数组的最小值为1。  

class Solution {
    public int minArray(int[] numbers) {
        Arrays.sort(numbers);
        return numbers[0];
    }
}

 剑指 Offer 12 矩阵中的路径

难度中等529

给定一个 m x n 二维字符网格 board 和一个字符串单词 word 。如果 word 存在于网格中,返回 true ;否则,返回 false 。

单词必须按照字母顺序,通过相邻的单元格内的字母构成,其中“相邻”单元格是那些水平相邻或垂直相邻的单元格。同一个单元格内的字母不允许被重复使用。

例如,在下面的 3×4 的矩阵中包含单词 "ABCCED"(单词中的字母已标出)。

 方法

  1. 采用DFS方法对路径进行遍历剪枝。
class Solution {
    private boolean res=false;
    public boolean exist(char[][] board, String word) {
        boolean [][]visited=new boolean[board.length][board[0].length];
        for(int i=0;i<board.length;i++)
        {
            for(int j=0;j<board[0].length;j++){
                if(board[i][j]==word.charAt(0))
                {
                    visited[i][j]=true;
                    dfs(i,j,board,1,word,visited);
                    visited[i][j]=false;
                }
            }
        }
        return res;
    }
    public void dfs(int x,int y,char[][] board,int index,String word,boolean [][]visited){
        int m=board.length;
        int n=board[0].length;
        if(index==word.length()){
            res=true;
            return;
        }
        for(int i=0;i<4;i++)
        {
             if(res){
                return;
            }
            switch(i){
                case 0:
                if(x-1>=0&&visited[x-1][y]==false&&word.charAt(index)==board[x-1][y])
                {
                    visited[x-1][y]=true;
                    dfs(x-1,y,board,index+1,word,visited);
                    visited[x-1][y]=false;
                }
                break;
                case 1:
                if(x+1<m&&visited[x+1][y]==false&&word.charAt(index)==board[x+1][y])
                {
                    visited[x+1][y]=true;
                    dfs(x+1,y,board,index+1,word,visited);
                    visited[x+1][y]=false;
                }
                break;

                case 2:
                if(y+1<n&&visited[x][y+1]==false&&word.charAt(index)==board[x][y+1])
                {
                    visited[x][y+1]=true;
                    dfs(x,y+1,board,index+1,word,visited);
                    visited[x][y+1]=false;
                }
                 case 3:
                if(y-1>=0&&visited[x][y-1]==false&&word.charAt(index)==board[x][y-1])
                {
                    visited[x][y-1]=true;
                    dfs(x,y-1,board,index+1,word,visited);
                    visited[x][y-1]=false;
                }
                break;
            }
        }

    }
}

 剑指 Offer 13 机器人的运动范围

难度中等452

地上有一个m行n列的方格,从坐标 [0,0] 到坐标 [m-1,n-1] 。一个机器人从坐标 [0, 0] 的格子开始移动,它每次可以向左、右、上、下移动一格(不能移动到方格外),也不能进入行坐标和列坐标的数位之和大于k的格子。例如,当k为18时,机器人能够进入方格 [35, 37] ,因为3+5+3+7=18。但它不能进入方格 [35, 38],因为3+5+3+8=19。请问该机器人能够到达多少个格子?

方法:

  1. dfs思路
class Solution {
    int res=0;
    public int movingCount(int m, int n, int k) {
        boolean [][] visited=new boolean[m][n];
        dfs(0,0,m,n,k,visited);
        return res;
    }
    public void dfs(int x,int y,int m,int n,int k,boolean[][] visited)
    {
        // 判断
        if(getSum(x)+getSum(y)<=k){
            visited[x][y]=true;
            res++;
        }else{
            return;
        }
        for(int i=0;i<4;i++){
            switch(i){
                case 0:
                    if(x-1>=0&&visited[x-1][y]==false){
                        dfs(x-1,y,m,n,k,visited);
                    }
                break;
                 case 1:
                    if(x+1<m&&visited[x+1][y]==false){
                        dfs(x+1,y,m,n,k,visited);
                    }
                break;
                case 2:
                    if(y-1>=0&&visited[x][y-1]==false){
                        dfs(x,y-1,m,n,k,visited);
                    }
                break;
                case 3:
                    if(y+1<n&&visited[x][y+1]==false){
                        dfs(x,y+1,m,n,k,visited);
                    }
                break;
            }
        }
    }
    public int getSum(int x){
        int r=0;
        while(x!=0){
            r+=(x%10);
            x/=10;
        }
        return r;
    }
}

 剑指 Offer 14-1 剪绳子

难度中等374

给你一根长度为 n 的绳子,请把绳子剪成整数长度的 m 段(m、n都是整数,n>1并且m>1),每段绳子的长度记为 k[0],k[1]...k[m-1] 。请问 k[0]*k[1]*...*k[m-1] 可能的最大乘积是多少?例如,当绳子的长度是8时,我们把它剪成长度分别为2、3、3的三段,此时得到的最大乘积是18。

思路:

  1. 对绳子进行分段的关键在于在满足分割了一次的前提下面,不能出现长度为1的绳子,尽量为长度为3的绳子,可以为长度为2的绳子,其余为长度为4的绳子。
class Solution {
    public int cuttingRope(int n) {
        int res=1;
        boolean j=true;
        // 从3开始
        while(n>=2){
           if(n>=5){
               res*=3;
               n-=3;
               j=false;
           }else{
               if(j){//长度为2-4,表示最少要一次分割
                   j=false;
                   //必须进行划分
                   int tep=n/2;
                   res=(n-tep)*tep;
                   n=0;
               }else{//长度为4,表示为末尾不分割
                 res*=n;
                 n=0;
               }
           }
        }
        return res;
    }
}

 剑指 Offer 15 二进制中1的个数

难度简单222

编写一个函数,输入是一个无符号整数(以二进制串的形式),返回其二进制表达式中数字位数为 '1' 的个数(也被称为 汉明重量).)。

思想:

  1. int为32位,遍历进行判断统计。
  2. 采用n&(n-1)可以将二进制最右边的1转换成0。

常见的&操作的快捷方法

  • n&(-n)表示求出二进制最边的1位起始的值的大小。
  • n&(n-1)可以将二进制最右边的1转换成0.
public class Solution {
    // you need to treat n as an unsigned value
    public int hammingWeight(int n) {
        int res=0;
        for(int i=0;i<32;i++)
        {
            int tep=n>>i;
            if((tep&1)==1){
                res++;
            }
        }
        return res;
    }
}

剑指 Offer 16 数值的整数次方 

难度中等261

实现 pow(x, n) ,即计算 x 的 n 次幂函数(即,xn)。不得使用库函数,同时不需要考虑大数问题。

提示:

  • -100.0 < x < 100.0
  • -231 <= n <= 231-1
  • -104 <= xn <= 104

思路:

  1. 很容易漏掉几种特殊情况,如果没有测试用例的提示,很难判断出来,需要看到x和n的特殊情况,x=1或者-1的时候,n为-23-1和0的时候,这都不要进行判断。
class Solution {
    public double myPow(double x, int n) {
      if(n==0||x==1)return 1;
      if(x==-1){
          if(n%2==0)return 1;
          return -1;
      }
      if(n==-2147483648){
          return 0;
      }
      double res=1;
      if(n>0){
          for(int i=1;i<=n;i++){
              res*=x;
              if(res==0.0)return 0;
          }
          return res;
      }else{
          for(int i=1;i<=-n;i++){
              res*=(1/x);
              if(res==0.0)return 0;
          }
          return res;
      }
    }
}

 剑指 Offer 17 打印从1到最大的n位数

难度简单194

输入数字 n,按顺序打印出从 1 到最大的 n 位十进制数。比如输入 3,则打印出 1、2、3 一直到最大的 3 位数 999。

示例 1:

输入: n = 1
输出: [1,2,3,4,5,6,7,8,9]
class Solution {
    public int[] printNumbers(int n) {
        int max=0;
        for(int i=0;i<n;i++){
            max=(max*10)+9;
        }
        LinkedList<Integer> res=new LinkedList<>();
        for(int i=1;i<=max;i++){
            res.addLast(i);
        }
        return res.stream().mapToInt(Integer::valueOf).toArray();
    }
}

 剑指 Offer 18 删除链表的节点

难度简单196

给定单向链表的头指针和一个要删除的节点的值,定义一个函数删除该节点。

返回删除后的链表的头节点。

注意:此题对比原题有改动

/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode(int x) { val = x; }
 * }
 */
class Solution {
    public ListNode deleteNode(ListNode head, int val) {
        ListNode h1=new ListNode();h1.next=head;
        ListNode p1=h1,p2=head;
        while(p2!=null&&p2.val!=val){
            p1=p1.next;
            p2=p2.next;
        }
        p1.next=p2.next;
        return h1.next;
    }
}

 剑指Offer 20 表示数值的字符串

难度中等302

请实现一个函数用来判断字符串是否表示数值(包括整数和小数)。

数值(按顺序)可以分成以下几个部分:

  1. 若干空格
  2. 一个 小数 或者 整数
  3. (可选)一个 'e' 或 'E' ,后面跟着一个 整数
  4. 若干空格

小数(按顺序)可以分成以下几个部分:

  1. (可选)一个符号字符('+' 或 '-'
  2. 下述格式之一:
    1. 至少一位数字,后面跟着一个点 '.'
    2. 至少一位数字,后面跟着一个点 '.' ,后面再跟着至少一位数字
    3. 一个点 '.' ,后面跟着至少一位数字

整数(按顺序)可以分成以下几个部分:

  1. (可选)一个符号字符('+' 或 '-'
  2. 至少一位数字

部分数值列举如下:

  • ["+100", "5e2", "-123", "3.1416", "-1E-16", "0123"]

部分非数值列举如下:

  • ["12e", "1a3.14", "1.2.3", "+-5", "12e+5.4"]

思想:

  1. 用Double.parseDouble()进行内置转换,对不能进行转换的进行捕获,在进行排除特殊情况。
class Solution {
    public boolean isNumber(String s) {
        s=s.trim();
        if(s.length()==0)return false;//去掉空格之后可能会空串
        try{
            Double.parseDouble(s);
        }catch(Exception e){
            return false;
        }
        // 排除八进制和十六进制的可能性,d、D,f、F等
        char flag=s.charAt(s.length()-1);
        if((flag>='0'&&flag<='9')||flag=='.'){
            return true;
        }
        return false;
    }
}

 剑指 Offer 21 调整数组顺序使奇数位于偶数前面

难度简单201

输入一个整数数组,实现一个函数来调整该数组中数字的顺序,使得所有奇数在数组的前半部分,所有偶数在数组的后半部分。

示例:

输入:nums = [1,2,3,4]
输出:[1,3,2,4] 
注:[3,1,2,4] 也是正确的答案之一。

提示:

  1. 0 <= nums.length <= 50000
  2. 0 <= nums[i] <= 10000
class Solution {
    public int[] exchange(int[] nums) {
        LinkedList<Integer> res=new LinkedList<>();
        for(int i:nums){
            if(i%2==0){
                res.addLast(i);
            }else{
                res.addFirst(i);
            }
        }
        return res.stream().mapToInt(Integer::valueOf).toArray();
    }
}

 剑指 Offer 22 链表中倒数第k个节点

难度简单330

输入一个链表,输出该链表中倒数第k个节点。为了符合大多数人的习惯,本题从1开始计数,即链表的尾节点是倒数第1个节点。

例如,一个链表有 6 个节点,从头节点开始,它们的值依次是 1、2、3、4、5、6。这个链表的倒数第 3 个节点是值为 4 的节点。

/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode(int x) { val = x; }
 * }
 */
class Solution {
    public ListNode getKthFromEnd(ListNode head, int k) {
        ListNode h1=head;
        if(head==null)return null;
        for(int i=0;i<k;i++){
            h1=h1.next;
        }
        while(h1!=null){
            h1=h1.next;
            head=head.next;
        }
        return head;
    }
}

剑指 Offer 24 反转链表

难度简单385

定义一个函数,输入一个链表的头节点,反转该链表并输出反转后链表的头节点。

示例:

输入: 1->2->3->4->5->NULL
输出: 5->4->3->2->1->NULL
/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode(int x) { val = x; }
 * }
 */
class Solution {
    public ListNode reverseList(ListNode head) {
        if(head==null)return head;
        ListNode h1=new ListNode();
        h1.next=null;
        while(head!=null){
            ListNode tep=head;
            head=head.next;
            tep.next=h1.next;
            h1.next=tep;
        }
        return h1.next;
    }
}

 剑指 Offer 25 合并两个升序的链表

难度简单217

输入两个递增排序的链表,合并这两个链表并使新链表中的节点仍然是递增排序的。

示例1:

输入:1->2->4, 1->3->4
输出:1->1->2->3->4->4
/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode(int x) { val = x; }
 * }
 */
class Solution {
    public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
        ListNode h1=new ListNode(),tail;
        h1.next=null;
        tail=h1;
        while(l1!=null&&l2!=null)
        {
            ListNode tep;
            if(l1.val>l2.val)
            {
                tep=l2;
                l2=l2.next;
                tep.next=null;
                tail.next=tep;
                tail=tep;
            }else{
                tep=l1;
                l1=l1.next;
                tep.next=null;
                tail.next=tep;
                tail=tep;
            }
        }
        if(l1!=null){
            tail.next=l1;
        }
         if(l2!=null){
            tail.next=l2;
        }
        return h1.next;
    }
}

 剑指 Offer 26 树的子结构

难度中等476

输入两棵二叉树A和B,判断B是不是A的子结构。(约定空树不是任意一个树的子结构)

B是A的子结构, 即 A中有出现和B相同的结构和节点值。

例如:
给定的树 A:

     3
    / \
   4   5
  / \
 1   2

给定的树 B:

   4 
  /
 1

返回 true,因为 B 与 A 的一个子树拥有相同的结构和节点值。

思想:

可以根据先序遍历,然后对每个子结构进行匹配验证。

/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode(int x) { val = x; }
 * }
 */
class Solution {
    private boolean flag=false;
    public boolean isSubStructure(TreeNode A, TreeNode B) {
        if(B==null)return false;
        dfs1(A,B);
        return flag;
    }
    void dfs1(TreeNode A,TreeNode B){
        if(A==null){
            return;
        }
        flag=dfs(A,B);
        if(flag)return;
        dfs1(A.left,B);
        if(flag)return;
        dfs1(A.right,B);
    }
    boolean dfs(TreeNode A,TreeNode B)
    {
        if(B==null){
            // 表示为真
            return true;
        }
        if(A==null&&B!=null){
            return false;
        }
        if(A.val!=B.val){
            return false;
        }
        boolean cleft=dfs(A.left,B.left);
        boolean cright=dfs(A.right,B.right);
        return cleft&&cright;
    }
}

 剑指 Offer 27 二叉树的镜像

难度简单218

请完成一个函数,输入一个二叉树,该函数输出它的镜像。

例如输入:

     4
   /   \
  2     7
 / \   / \
1   3 6   9

镜像输出:

     4
   /   \
  7     2
 / \   / \

/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode(int x) { val = x; }
 * }
 */
class Solution {
    public TreeNode mirrorTree(TreeNode root) {
        dfs(root);
        return root;
    }
    void dfs(TreeNode root){
        if(root==null){
            return;
        }
        TreeNode tep=root.left;
        root.left=root.right;
        root.right=tep;

        dfs(root.left);
        dfs(root.right);
    }
}

剑指 Offer 28 对称的二叉树

难度简单288

请实现一个函数,用来判断一棵二叉树是不是对称的。如果一棵二叉树和它的镜像一样,那么它是对称的。

例如,二叉树 [1,2,2,3,4,4,3] 是对称的。

    1
   / \
  2   2
 / \ / \
3  4 4  3

但是下面这个 [1,2,2,null,3,null,3] 则不是镜像对称的:

    1
   / \
  2   2
   \   \
   3    3

/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode(int x) { val = x; }
 * }
 */
class Solution {
    public boolean isSymmetric(TreeNode root) {
        return dfs(root,root);
    }
    boolean dfs(TreeNode r1,TreeNode r2){
        if(r1==null&&r2==null){
            return true;
        }
        if(r1==null||r2==null){
            return false;
        }
        if(r1.val!=r2.val){
            return false;
        }
        return dfs(r1.right,r2.left)&&dfs(r1.left,r2.right);
    }
}

 剑指 Offer 29 顺时针打印矩阵

难度简单368

输入一个矩阵,按照从外向里以顺时针的顺序依次打印出每一个数字。

示例 1:

输入:matrix = [[1,2,3],[4,5,6],[7,8,9]]
输出:[1,2,3,6,9,8,7,4,5]

示例 2:

输入:matrix = [[1,2,3,4],[5,6,7,8],[9,10,11,12]]
输出:[1,2,3,4,8,12,11,10,9,5,6,7]

思路:

  1. 采用DFS进行遍历打印,注意边界值
class Solution {
    private ArrayList<Integer> res=new ArrayList<>();
    public int[] spiralOrder(int[][] matrix) {
        if (matrix.length==0){
            return res.stream().mapToInt(Integer::valueOf).toArray();
        }
        boolean [][]visited=new boolean[matrix.length][matrix[0].length];
        visited[0][0]=true;
        res.add(matrix[0][0]);
        dfs(0,0,matrix.length,matrix[0].length,0,matrix,visited);
        return res.stream().mapToInt(Integer::valueOf).toArray();
    }
    void dfs(int x,int y,int m,int n,int dir,int[][] matrix,boolean[][] visited){
        switch(dir){
            case 0:
            //右走
            int i=y;
            for(i++;i<n;i++){
                if(visited[x][i]){
                    break;
                }else{
                    visited[x][i]=true;
                    res.add(matrix[x][i]);
                }
            }
           if(i!=y+1)
            dfs(x,i-1,m,n,1,matrix,visited);
            if(y+1==1){// 表示为单列
                 dfs(x,i-1,m,n,1,matrix,visited);
            }
            break;
            case 1:
            //下走
             i=x;
            for(i++;i<m;i++){
                if(visited[i][y]){
                    break;
                }else{
                    visited[i][y]=true;
                    res.add(matrix[i][y]);
                }
            } 
            if(i!=x+1)
            dfs(i-1,y,m,n,2,matrix,visited);
            break;
            case 2:
             i=y;
            for(i--;i>=0;i--){
                 if(visited[x][i]){
                    break;
                }else{
                    visited[x][i]=true;
                    res.add(matrix[x][i]);
                }
            }

            if(i!=y-1)
            dfs(x,i+1,m,n,3,matrix,visited);
            break;

            case 3:
             i=x;
            for(i--;i>=0;i--){
                if(visited[i][y]){
                    break;
                }else{
                    visited[i][y]=true;
                    res.add(matrix[i][y]);
                }
            }
            if(i!=x-1)
            dfs(i+1,y,m,n,0,matrix,visited);
            break;
        }
    }
}

 剑指 Offer 30 包含min函数的栈

难度简单293

定义栈的数据结构,请在该类型中实现一个能够得到栈的最小元素的 min 函数在该栈中,调用 min、push 及 pop 的时间复杂度都是 O(1)。

示例:

MinStack minStack = new MinStack();
minStack.push(-2);
minStack.push(0);
minStack.push(-3);
minStack.min();   --> 返回 -3.
minStack.pop();
minStack.top();      --> 返回 0.
minStack.min();   --> 返回 -2.
class MinStack {

    /** initialize your data structure here. */
    Stack<Integer> s=new Stack<>();
    TreeMap<Integer,Integer> t=new TreeMap<>();

    public MinStack() {
        
    }
    
    public void push(int x) {
        s.push(x);
        Integer v=t.get(x);
        if(v==null){
            t.put(x,1);
        }else{
            t.put(x,v+1);
        }
    }
    
    public void pop() {
        int x=s.peek();
        s.pop();
        Integer v=t.get(x);
        if(v==1){
            t.remove(x);
        }else{
            t.put(x,v-1);
        }
    }
    
    public int top() {
        return s.peek();
    }
    
    public int min() {
        return t.firstKey();
    }
}

/**
 * Your MinStack object will be instantiated and called as such:
 * MinStack obj = new MinStack();
 * obj.push(x);
 * obj.pop();
 * int param_3 = obj.top();
 * int param_4 = obj.min();
 */

剑指 Offer 31 栈的压入、弹出序列

难度中等288

输入两个整数序列,第一个序列表示栈的压入顺序,请判断第二个序列是否为该栈的弹出顺序。假设压入栈的所有数字均不相等。例如,序列 {1,2,3,4,5} 是某栈的压栈序列,序列 {4,5,3,2,1} 是该压栈序列对应的一个弹出序列,但 {4,3,5,1,2} 就不可能是该压栈序列的弹出序列。

class Solution {
    public boolean validateStackSequences(int[] pushed, int[] popped) {
        Stack<Integer> s=new Stack<>();
        int index=0;
        for(int i=0;i<pushed.length;i++){
            if(pushed[i]==popped[index])
            {
                index++;
                //是否可以继续弹栈
                while(!s.isEmpty()&&s.peek()==popped[index])
                {
                    index++;
                    s.pop();
                }
            }else{
                s.push(pushed[i]);
            }
        }
        while(!s.isEmpty()&&s.peek()==popped[index])
        {
            index++;
            s.pop();
        }
        if(index==pushed.length){
            return true;
        }
        return false;
    }
}

剑指 Offer 32 从上到下打印二叉树

难度简单184

从上到下按层打印二叉树,同一层的节点按从左到右的顺序打印,每一层打印到一行。

/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode(int x) { val = x; }
 * }
 */
class Solution {
    private List<List<Integer>> res=new ArrayList<>();
    public List<List<Integer>> levelOrder(TreeNode root) {
        LinkedList<TreeNode> q=new LinkedList<>();
        if(root==null)return res;
        LinkedList<Integer> c=new LinkedList<>();
        int clen=1;
        int cindex=0;
        int nextlen=0;
        q.addLast(root);
        while(!q.isEmpty()){
            TreeNode cval=q.getFirst();
            c.add(cval.val);
            cindex++;
            q.pop();

            if(cval.left!=null){
                nextlen++;
                q.addLast(cval.left);
            }
            if(cval.right!=null){
                nextlen++;
                q.addLast(cval.right);
            }

            if(cindex==clen){
                cindex=0;
                clen=nextlen;
                nextlen=0;
                res.add(new ArrayList<>(c));
                c.clear();
            }
        }
        return res;
    }
}

剑指 Offer 33 二叉搜索树的后序遍历序列

难度中等445

输入一个整数数组,判断该数组是不是某二叉搜索树的后序遍历结果。如果是则返回 true,否则返回 false。假设输入的数组的任意两个数字都互不相同。

参考以下这颗二叉搜索树:

     5
    / \
   2   6
  / \
 1   3

示例 1:

输入: [1,6,3,2,5]
输出: false

思想:

  1. 二叉搜索树的性质:左<根<右,后序遍历的性质:左,右,根,最右边的是根,根据根可以判断出左子树和右子树,对左子树和右子树继续递归进行判断,直至判断所有的子树都满足要求。
class Solution {
    public boolean verifyPostorder(int[] postorder) {
        return judge(postorder,0,postorder.length-1);
    }
    boolean judge(int []postorder,int left,int right){
        if(left>=right)return true;
        // 找到以postorder[right]为根右子树最边上的index
        int tep=left;
        while(left<=right&&postorder[left]<postorder[right]){
            left++;
        }
        left--;
        // 对右子树进行判断
        for(int i=left+1;i<right;i++){
            if(postorder[i]<postorder[right]){
                return false;
            }
        }
        return judge(postorder,tep,left)&&judge(postorder,left+1,right-1);
    }
}

剑指 Offer 34 二叉树中和为某一值的路径

难度中等297

给你二叉树的根节点 root 和一个整数目标和 targetSum ,找出所有 从根节点到叶子节点 路径总和等于给定目标和的路径。

叶子节点 是指没有子节点的节点。

思路:

  1. 用DFS的思想去统计从根节点到叶子节点的路径之和,可以用ArrayList存储某一条路径,用add和remove(new Integer())会出现结点值相同,而删除错误位置的结点,所以用LinkedList去处理边界山的值,addLast(),removeLast()。
/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode() {}
 *     TreeNode(int val) { this.val = val; }
 *     TreeNode(int val, TreeNode left, TreeNode right) {
 *         this.val = val;
 *         this.left = left;
 *         this.right = right;
 *     }
 * }
 */
class Solution {
    private List<List<Integer>> res=new ArrayList<>();
    public List<List<Integer>> pathSum(TreeNode root, int target) {
        // 根节点表示左右根节点都为null
        dfs(root,0,target,new LinkedList<>());
        return res;
    }
    void dfs(TreeNode root,int csum,int target,LinkedList<Integer> cval){
        if(root==null)return;

        cval.addLast(root.val);
        csum+=root.val;

        if(root.left==null&&root.right==null){
            if(csum==target)
            res.add(new ArrayList<>(cval));
            cval.removeLast();
            return;
        }

        dfs(root.left,csum,target,cval);
        dfs(root.right,csum,target,cval);

        cval.removeLast();
    }
}

剑指 Offer 35 复杂链表的复制

难度中等444

请实现 copyRandomList 函数,复制一个复杂链表。在复杂链表中,每个节点除了有一个 next 指针指向下一个节点,还有一个 random 指针指向链表中的任意节点或者 null

示例 1:

输入:head = [[7,null],[13,0],[11,4],[10,2],[1,0]]
输出:[[7,null],[13,0],[11,4],[10,2],[1,0]]
/*
// Definition for a Node.
class Node {
    int val;
    Node next;
    Node random;

    public Node(int val) {
        this.val = val;
        this.next = null;
        this.random = null;
    }
}
*/
class Solution {
    public Node copyRandomList(Node head) {
        Node h1=new Node(-1);
        Node tail=h1;
        Node h2=head;
        HashMap<Node,Node> LToL=new HashMap<>();
        while(h2!=null){
            Node tep=new Node(h2.val);
            tep.next=null;
            tep.random=null;
            tail.next=tep;
            tail=tep;
            LToL.put(h2,tep);
            h2=h2.next;
        }
        h2=h1.next;
        while(h2!=null&head!=null){
            h2.random=LToL.get(head.random);
            head=head.next;
            h2=h2.next;
        }
        return h1.next;      
    }
}

剑指 Offer 36 二叉搜索树与双链表

难度中等416

输入一棵二叉搜索树,将该二叉搜索树转换成一个排序的循环双向链表。要求不能创建任何新的节点,只能调整树中节点指针的指向。

为了让您更好地理解问题,以下面的二叉搜索树为例:

我们希望将这个二叉搜索树转化为双向循环链表。链表中的每个节点都有一个前驱和后继指针。对于双向循环链表,第一个节点的前驱是最后一个节点,最后一个节点的后继是第一个节点。

下图展示了上面的二叉搜索树转化成的链表。“head” 表示指向链表中有最小元素的节点。

特别地,我们希望可以就地完成转换操作。当转化完成以后,树中节点的左指针需要指向前驱,树中节点的右指针需要指向后继。还需要返回链表中的第一个节点的指针。

class Solution {
    LinkedList<Node> q=new LinkedList<>();
    public Node treeToDoublyList(Node root) {
       if(root==null)return root;
       dfs(root);
       Node h1=q.getFirst();
       q.removeFirst();
       root=h1;
        h1.right=h1;
        h1.left=h1;
      while(!q.isEmpty()){
        Node tt=q.getFirst();
        q.removeFirst();
        h1.right=tt;
        tt.left=h1;
        h1=tt;
      }
      h1.right=root;
      root.left=h1;
      return root; 
    }
    void dfs(Node root){
        if(root==null){
            return;
        }
        dfs(root.left);
        q.addLast(root);
        dfs(root.right);
    }
}

剑指 Offer 37 序列化二叉树

难度困难269

请实现两个函数,分别用来序列化和反序列化二叉树。

你需要设计一个算法来实现二叉树的序列化与反序列化。这里不限定你的序列 / 反序列化算法执行逻辑,你只需要保证一个二叉树可以被序列化为一个字符串并且将这个字符串反序列化为原始的树结构。

提示:输入输出格式与 LeetCode 目前使用的方式一致,详情请参阅 LeetCode 序列化二叉树的格式。你并非必须采取这种方式,你也可以采用其他的方法解决这个问题。

/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode(int x) { val = x; }
 * }
 */
public class Codec {

    private TreeNode res1;
    // Encodes a tree to a single string.
    public String serialize(TreeNode root) {
       res1=root;
       return null;
    }

    // Decodes your encoded data to tree.
    public TreeNode deserialize(String data) {
       return res1;
    }
}

// Your Codec object will be instantiated and called as such:
// Codec codec = new Codec();
// codec.deserialize(codec.serialize(root));

剑指 Offer 38 字符串的排列

难度中等506

输入一个字符串,打印出该字符串中字符的所有排列。

你可以以任意顺序返回这个字符串数组,但里面不能有重复元素。

示例:

输入:s = "abc"
输出:["abc","acb","bac","bca","cab","cba"]

限制:

1 <= s 的长度 <= 8

通过次数186,068提交次数320,465

思路:

  1. dfs进行全排列,但是去重是一个老大难的问题,设置一个HashSet[]进行去重复判断,但是时间超出了限制,于是想到了HashSet也存在toArray()方法,可以直接进行转化为String[],因为对返回的全排列的顺序没有要求,因此可以将全排列直接全部插入set中,会自动去重复。
class Solution {
    HashSet<String> ss=new HashSet<>();
    public String[] permutation(String s) {
        dfs(0,"",s,new boolean[s.length()]);
        return ss.toArray(new String[0]);
    }
    void dfs(int index,String cv,String s,boolean []visited){
        if(index==s.length()){
            ss.add(cv);
            return ;
        }
        for(int i=0;i<s.length();i++){
            if(!visited[i]){
                visited[i]=true;
                dfs(index+1,cv+s.charAt(i),s,visited);
                visited[i]=false;
            }
        }
    }
}

剑指 Offer 39 数组中出现次数超过一半的数字

难度简单251

数组中有一个数字出现的次数超过数组长度的一半,请找出这个数字。

你可以假设数组是非空的,并且给定的数组总是存在多数元素。

示例 1:

输入: [1, 2, 3, 2, 2, 2, 5, 4, 2]
输出: 2
class Solution {
    public int majorityElement(int[] nums) {
        int res=nums[0];
        int n=1;
        for(int i=1;i<nums.length;i++){
            if(n>=1){
                if(nums[i]==res){
                    n++;
                }else{
                    n--;
                }
            }else{
                res=nums[i];
                n=1;
            }
        }
        return res;
    }
}

 剑指 Offer 40 最小的k个数

难度简单387

输入整数数组 arr ,找出其中最小的 k 个数。例如,输入4、5、1、6、2、7、3、8这8个数字,则最小的4个数字是1、2、3、4。

class Solution {
    public int[] getLeastNumbers(int[] arr, int k) {
        Arrays.sort(arr);
        int []res=new int[k];
        res=Arrays.copyOf(arr,k);
        return res;
    }
}

 剑指 Offer 41 数据流中的中位数

难度困难263

如何得到一个数据流中的中位数?如果从数据流中读出奇数个数值,那么中位数就是所有数值排序之后位于中间的数值。如果从数据流中读出偶数个数值,那么中位数就是所有数值排序之后中间两个数的平均值。

例如,

[2,3,4] 的中位数是 3

[2,3] 的中位数是 (2 + 3) / 2 = 2.5

设计一个支持以下两种操作的数据结构:

  • void addNum(int num) - 从数据流中添加一个整数到数据结构中。
  • double findMedian() - 返回目前所有元素的中位数。

思想:

 comparator比较器的写法:

1,new Comparator<T>(){

int comare(T o1,T o2){

return 1;

}

}

2,o1,o2->o1>o2?-1:1

3,(o1,o2)->{return o1>o2?-1:1;}

还有另一种解法时间复杂度较小,可以采用大小根堆区避免每次都要进行排序。

class MedianFinder {

    /** initialize your data structure here. */
    private ArrayList<Double> res;
    public MedianFinder() {
        res=new ArrayList<>();
    }
    
    public void addNum(int num) {
        res.add((double)num);
        // res.sort(new Comparator<Double>(){
        //     @Override
        //     public int compare(Double o1, Double o2) {
        //         if(o1>o2){
        //             return -1;
        //         }else 
        //         return 1;
        //     }
        // });
        // res.sort((x,y)-> {return x-y>0? -1:1;});
        res.sort((x,y)-> x-y>0? -1:1);
    }
    
    public double findMedian() {
        if(res.size()%2==0){
            return (res.get(res.size()/2)+res.get(res.size()/2-1))/2;
        }else{
            return res.get(res.size()/2);
        }
    }
}

// 大小根堆的方式求解
// 建设数组排序后,后半部分较大的放在小根堆,前半部分放在大根堆,这就满足小根堆的顶部和大根堆的顶部都是靠中间的。
class MedianFinder {
    Queue<Integer> A, B;
    public MedianFinder() {
        A = new PriorityQueue<>(); // 小顶堆,保存较大的一半
        B = new PriorityQueue<>((x, y) -> (y - x)); // 大顶堆,保存较小的一半
    }
    public void addNum(int num) {
        if(A.size() != B.size()) {
            A.add(num);
            B.add(A.poll());
        } else {
            B.add(num);
            A.add(B.poll());
        }
    }
    public double findMedian() {
        return A.size() != B.size() ? A.peek() : (A.peek() + B.peek()) / 2.0;
    }
}

剑指 Offer 42 连续子数组的最大和

难度简单474

输入一个整型数组,数组中的一个或连续多个整数组成一个子数组。求所有子数组的和的最大值。

要求时间复杂度为O(n)。

class Solution {
    public int maxSubArray(int[] nums) {
        int res=Integer.MIN_VALUE;
        int cv=0;

        for(int i:nums){
            // 进行比较 另开一个 还是连续的干
            if(cv+i<0||cv+i<i){
                cv=i;
                res=Integer.max(cv,res);
            }else{
                cv+=i;
                res=Integer.max(cv,res);
            }
        }
        return res;
    }
}

剑指 Offer 43 1-n整数中1出现的次数

难度困难288

输入一个整数 n ,求1~n这n个整数的十进制表示中1出现的次数。

例如,输入12,1~12这些整数中包含1 的数字有1、10、11和12,1一共出现了5次。

示例 1:

输入:n = 12
输出:5

示例 2:

输入:n = 13
输出:6

思路:

进行逐位进行统计,对于一个数字n,先统计最低位置,个位,然后在看十位和以上的高位,个位为1,高位则可以随便变换,个位可能为0-9,当为0的时候,

class Solution {
    public int countDigitOne(int n) {
        int res=0;
        String s = String.valueOf(n);
        int len=s.length();
        for(int i=len-1;i>=0;i--)//按照十进制展开 
        {
            int digit= (int) Math.pow(10,len-1-i);
           // 从右向左 i表示位置,digit也表示位置,digit=1表示个位,digit=10表示十位
            // 
            if(s.charAt(i)=='0')
            {
                int temp=0;
                for(int j=0;j<i;j++)
                {
                    temp*=10;
                    temp+=s.charAt(j)-'0';
                }
                // 因为i位置不为1,所以00-temp-1总共有temp,再有位数为digit,所以
                res+=(digit*temp);
            }
            else if(s.charAt(i)=='1')
            {
                // 高位
                int temp=0;
                for(int j=0;j<i;j++)
                {
                    temp*=10;
                    temp+=s.charAt(j)-'0';
                }
                res+=(digit*temp);
                // 低位
                temp=0;
                for(int j=i+1;j<len;j++)
                {
                    temp*=10;
                    temp+=s.charAt(j)-'0';
                }
                temp++;
                res+=temp;
            }
            else{// 当前位为2-9
                int temp=0;
                for(int j=0;j<i;j++)
                {
                    temp*=10;
                    temp+=s.charAt(j)-'0';
                }
                temp++;
                res+=(digit*temp);
            }
        }
        return res;
    }
    }

剑指 Offer 44 数字序列中某一位的数字

难度中等207

数字以0123456789101112131415…的格式序列化到一个字符序列中。在这个序列中,第5位(从下标0开始计数)是5,第13位是1,第19位是4,等等。

请写一个函数,求任意第n位对应的数字。

示例 1:

输入:n = 3
输出:3

思路:

  1. 对1-n进行逐位添加,存在规律,首先是1-9,其次是10-99,在100-99,,,可以按照这个规律将n位置找出来。
class Solution {
    public int findNthDigit(int n) {
        int digit = 1;
        long start = 1;
        long count = 9;
        while (n > count) { // 1.
            n -= count;
            digit += 1;
            start *= 10;
            count = digit * start * 9;
        }
        long num = start + (n - 1) / digit; // 2.
        return Long.toString(num).charAt((n - 1) % digit) - '0'; // 3.
    }
}

剑指 Offer 45 把数组排成最小的数

难度中等390

输入一个非负整数数组,把数组里所有数字拼接起来排成一个数,打印能拼接出的所有数字中最小的一个。

思路:

  1. 对于一个数组进行全排列得到最小的数,可考虑用Arrays.sort()中的比较器comparator(),将两个数转换成字符串进行拼接,a+b和b+a的形式,然后进行判断即可得出目标要求。
class Solution {
    public String minNumber(int[] nums) {
         String res="";
        Integer [] temp=new Integer[nums.length];
        for(int i=0;i<nums.length;i++)
            temp[i]=nums[i];
        Arrays.sort(temp,(one,two)->{
            return (one.toString()+two.toString()).compareTo(two.toString()+one.toString());
        });
        for(Integer i:temp)
            res=res+i.toString();
        return res;
    }
}

剑指 Offer 46 把数字翻译成字符串

难度中等382

给定一个数字,我们按照如下规则把它翻译为字符串:0 翻译成 “a” ,1 翻译成 “b”,……,11 翻译成 “l”,……,25 翻译成 “z”。一个数字可能有多个翻译。请编程实现一个函数,用来计算一个数字有多少种不同的翻译方法。

示例 1:

输入: 12258
输出: 5
解释: 12258有5种不同的翻译,分别是"bccfi", "bwfi", "bczi", "mcfi"和"mzi"

思想:

  1. 采用DFS思想进行遍历,对满足进行set去重即可。
class Solution {
    private HashSet<String> res;
    public int translateNum(int num) {
        res=new HashSet<>();
        String s=String.valueOf(num);
        dfs(0,"",s);
        return res.size();
    }
    void dfs(int index,String cv,String s){
        if(index==s.length()){
            res.add(cv);
            return ;
        }
        int num=s.charAt(index)-'0';
        dfs(index+1,cv+String.valueOf('a'+num),s);
        // 如果index index+1合法的话
        if(index+1==s.length())return;
        if(num==0||num>2)return;
        if(num==1){
            num=10+s.charAt(index+1)-'0';
            dfs(index+2,cv+String.valueOf('a'+num),s);
        }
        if(num==2){
            num=20+s.charAt(index+1)-'0';
            if(num>=26)return;
            dfs(index+2,cv+String.valueOf('a'+num),s);
        }
    }
}

剑指 Offer 47 礼物的最大价值

难度中等255

在一个 m*n 的棋盘的每一格都放有一个礼物,每个礼物都有一定的价值(价值大于 0)。你可以从棋盘的左上角开始拿格子里的礼物,并每次向右或者向下移动一格、直到到达棋盘的右下角。给定一个棋盘及其上面的礼物的价值,请计算你最多能拿到多少价值的礼物?

示例 1:

输入: 
[
  [1,3,1],
  [1,5,1],
  [4,2,1]
]
输出: 12
解释: 路径 1→3→5→2→1 可以拿到最多价值的礼物
class Solution {
    public int maxValue(int[][] grid) {
        // 很明显的dp题目,dp[i][j]表示
        if(grid.length==0)return 0;
        int [][]dp=new int[grid.length][grid[0].length];
        int csum=0;
        for(int i=0;i<grid.length;i++){
            csum+=grid[i][0];
            dp[i][0]=csum;
        }
        csum=0;
        for(int i=0;i<grid[0].length;i++){
            csum+=grid[0][i];
            dp[0][i]=csum;
        }
        for(int i=1;i<grid.length;i++){
            for(int j=1;j<grid[0].length;j++){
                dp[i][j]=Integer.max(dp[i-1][j],dp[i][j-1])+grid[i][j];
            }
        }
        return dp[grid.length-1][grid[0].length-1];
    }
}

剑指 Offer 48 最长不含重复字符的子串字符串

难度中等386

请从字符串中找出一个最长的不包含重复字符的子字符串,计算该最长子字符串的长度。

示例 1:

输入: "abcabcbb"
输出: 3 
解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。
class Solution {
    public int lengthOfLongestSubstring(String s) {
        int res=0;
        String tep="";
        for(int i=0;i<s.length();i++){
            String t=""+s.charAt(i);
            int index=tep.indexOf(t);
            if(index==-1){
                tep=tep+t;
            }else{
                tep=tep.substring(index+1,tep.length());
                tep=tep+t;
            }
            res=Integer.max(res,tep.length());
        }
        return res;
    }
}

剑指 Offer 49 丑数

难度中等292

我们把只包含质因子 2、3 和 5 的数称作丑数(Ugly Number)。求按从小到大的顺序的第 n 个丑数。

示例:

输入: n = 10
输出: 12
解释: 1, 2, 3, 4, 5, 6, 8, 9, 10, 12 是前 10 个丑数。

方法一,超时

class Solution {
    public int nthUglyNumber(int n) {
        int index=0;
        int count=1;
        while(true){
            if(isUgly(count)){
                index++;
                if(index==n){
                    return count;
                }
            }
            count++;
        }
        
    }
    boolean isUgly(int n){
        while(n%5==0)n/=5;
        while(n%3==0)n/=3;
        while(n%2==0)n/=2;
        return n==1?true:false;
    }
}

方法二:

思想:

丑数为2,3,5的倍数,故此,可以对丑数x进行2x、3x、5x进行相乘且也为丑数,去重并排序后,在慢慢进行下去,直至到达n即可。

class Solution {
    public int nthUglyNumber(int n) {
        int[] factors = {2, 3, 5};
        Set<Long> seen = new HashSet<Long>();
        PriorityQueue<Long> heap = new PriorityQueue<Long>();
        seen.add(1L);
        heap.offer(1L);
        int ugly = 0;
        for (int i = 0; i < n; i++) {
            long curr = heap.poll();
            ugly = (int) curr;
            for (int factor : factors) {
                long next = curr * factor;
                if (seen.add(next)) {
                    heap.offer(next);
                }
            }
        }
        return ugly;
    }
}

面试题 50 第一只出现一次的字符

难度简单190

在字符串 s 中找出第一个只出现一次的字符。如果没有,返回一个单空格。 s 只包含小写字母。

示例 1:

输入:s = "abaccdeff"
输出:'b'

思路:

LinkedHashMap,保持顺序的map,且map的遍历,Map.Entry<T,T> entry:map.entrySet();,然后通过entry.getKey()和entry.getValue();

class Solution {
    public char firstUniqChar(String s) {
        if(s.length()==0)
        return ' ';
        LinkedHashMap<Character,Integer> map=new LinkedHashMap<>();
        for(int j=0;j<s.length();j++){
            char i=s.charAt(j);
            Integer f= map.get(i);
            if(f==null){
                map.put(i,1);
            }else{
                map.put(i,f+1);
            }
        }
        for(Map.Entry<Character,Integer> entry:map.entrySet()){
            if(entry.getValue()==1){
                return entry.getKey();
            }
        }
       return ' ';
    }
}

剑指 Offer 51 数组中的逆序对

难度困难638

在数组中的两个数字,如果前面一个数字大于后面的数字,则这两个数字组成一个逆序对。输入一个数组,求出这个数组中的逆序对的总数。

示例 1:

输入: [7,5,6,4]
输出: 5
class Solution {
    public int reversePairs(int[] nums) {
        // 离散化
        int temp[]=Arrays.copyOf(nums,nums.length);
        Arrays.sort(temp);
        for(int i=0;i<nums.length;i++){
            nums[i]=Arrays.binarySearch(temp,nums[i])+1;
        }
        BIT t=new BIT(nums.length);
        int res=0;
        for(int i=0;i<nums.length;i++){
            t.add(nums[i]);
            res=res+i-t.query(nums[i])+1;
            // i表示当前加入bit中的数的总数,query(index)表示小于等于index数的总数
        }
        return res;
    }
}
class BIT{
    int tree[];
    int n;
    public BIT(int n){
        tree=new int[n+1];
        this.n=n;
    }
    int lowbit(int x){
        return x&(-x);
    }
    void add(int index){
        while(index<=n){
            tree[index]++;
            index+=lowbit(index);
        }
    }
    int query(int index){
        int res=0;
        while(index!=0){
            res+=tree[index];
            index-=lowbit(index);
        }
        return res;
    }
}

剑指 Offer 52 两个链表的第一个公共节点

难度简单451

输入两个链表,找出它们的第一个公共节点。

如下面的两个链表

/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode(int x) {
 *         val = x;
 *         next = null;
 *     }
 * }
 */
public class Solution {
    public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
        int len1=0;
        int len2=0;
        ListNode h1=headA,h2=headB;
        while(h1!=null){len1++;h1=h1.next;}
        while(h2!=null){len2++;h2=h2.next;}
        int t=len1-len2;
        if(t>0){
            while(t!=0){t--;headA=headA.next;}
        }else{
            t=len2-len1;
            while(t!=0){t--;headB=headB.next;}
        }
        while(headA!=headB){
            headA=headA.next;
            headB=headB.next;
        }
        return headA;
    }
}

剑指 Offer 53 在排序数组中查找数字

难度简单278

统计一个数字在排序数组中出现的次数。

示例 1:

输入: nums = [5,7,7,8,8,10], target = 8
输出: 2
class Solution {
    public int search(int[] nums, int target) {
        int res=0;
        for(int i:nums){
            if(res!=0&&target!=i)return res;
            if(i==target)res++;
        }
        return res;
    }
}

 剑指 Offer 53 0-n-1中缺失的数字

难度简单240

一个长度为n-1的递增排序数组中的所有数字都是唯一的,并且每个数字都在范围0~n-1之内。在范围0~n-1内的n个数字中有且只有一个数字不在该数组中,请找出这个数字。

示例 1:

输入: [0,1,3]
输出: 2
class Solution {
    public int missingNumber(int[] nums) {
        int sum=0;
       for(int i:nums)sum+=i;
       return (nums.length+0)*(nums.length+1)/2-sum;
    }
}

剑指 Offer 54 二叉搜索数的第k大节点

难度简单261

给定一棵二叉搜索树,请找出其中第 k 大的节点的值。

示例 1:

输入: root = [3,1,4,null,2], k = 1
   3
  / \
 1   4
  \
   2
输出: 4
/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode(int x) { val = x; }
 * }
 */
class Solution {
    private int res=0;
    int count=0;
    public int kthLargest(TreeNode root, int k) {
        dfs(root,k);
        return res;
    }
    void dfs(TreeNode root,int k){
        if(root==null)return;
        
        dfs(root.right,k);
        count++;
        if(count==k){
            res=root.val;
            return;
        }
        dfs(root.left,k);
        
    }
}

剑指 Offer 55 二叉树的深度

难度简单167

输入一棵二叉树的根节点,求该树的深度。从根节点到叶节点依次经过的节点(含根、叶节点)形成树的一条路径,最长路径的长度为树的深度。

例如:

给定二叉树 [3,9,20,null,null,15,7]

    3
   / \
  9  20
    /  \
   15   7
/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode(int x) { val = x; }
 * }
 */
class Solution {
    int res=0;
    public int maxDepth(TreeNode root) {
        dfs(root,0);
        return res;
    }
    void dfs(TreeNode root,int d){
        if(root==null)return;

        res=Integer.max(res,d+1);

        dfs(root.left,d+1);

        dfs(root.right,d+1);
    }
}

剑指Offer 55 平衡二叉树

难度简单237

输入一棵二叉树的根节点,判断该树是不是平衡二叉树。如果某二叉树中任意节点的左右子树的深度相差不超过1,那么它就是一棵平衡二叉树。

示例 1:

给定二叉树 [3,9,20,null,null,15,7]

    3
   / \
  9  20
    /  \
   15   7
/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode(int x) { val = x; }
 * }
 */
class Solution {
    private boolean flag=true;
    public boolean isBalanced(TreeNode root) {
        deepth(root);
        return flag;
    }
    int deepth(TreeNode root){
        if(root==null)return 0;
        int cl=deepth(root.left);
        int cr=deepth(root.right);
        // 进行左右对比
        if(Math.abs(cl-cr)>=2){
            flag=false;
        }
        return Integer.max(cl,cr)+1;
    }
}

剑指 Offer 56 数组中数字出现的次数-1

难度中等557

一个整型数组 nums 里除两个数字之外,其他数字都出现了两次。请写程序找出这两个只出现一次的数字。要求时间复杂度是O(n),空间复杂度是O(1)。

示例 1:

输入:nums = [4,1,4,6]
输出:[1,6] 或 [6,1]

思路:

  1. 两个数字出现一次,其余出现两次,采用分组异或即可满足条件,那怎样进行分组呢???先可以让nms[]数组全部进行异或,得到的是目标a、b的异或,然后得到a^b的异或二进制位置为1的位置进行分组,然后遍历nums[]进行分组异或即可得到a和b。
class Solution {
    public int[] singleNumbers(int[] nums) {
        int ret = 0;
        for (int n : nums) {
            ret ^= n;
        }
        int div = 1;
        while ((div & ret) == 0) {
            div <<= 1;
        }
        int a = 0, b = 0;
        for (int n : nums) {
            if ((div & n) != 0) {
                a ^= n;
            } else {
                b ^= n;
            }
        }
        return new int[]{a, b};
    }
}

剑指 Offer 56 数组中出现的次数-2

难度中等292

在一个数组 nums 中除一个数字只出现一次之外,其他数字都出现了三次。请找出那个只出现一次的数字。

示例 1:

输入:nums = [3,4,3,3]
class Solution {
    public int singleNumber(int[] nums) {
        HashMap<Integer,Integer> r=new HashMap<>();
        for(int i:nums){
            Integer t=r.get(i);
            if(t==null){
                r.put(i,1);
            }else{
                r.put(i,t+1);
            }
        }
        for(Map.Entry<Integer,Integer> entry:r.entrySet()){
            if(entry.getValue()==1){
                return entry.getKey();
            }
        }
        return 0;
    }
}

剑指 Offer 57 和为s的两个数字

难度简单168

输入一个递增排序的数组和一个数字s,在数组中查找两个数,使得它们的和正好是s。如果有多对数字的和等于s,则输出任意一对即可。

示例 1:

输入:nums = [2,7,11,15], target = 9
输出:[2,7] 或者 [7,2]
class Solution {
    public int[] twoSum(int[] nums, int target) {

       ArrayList<Integer> res=new ArrayList<>();
        for(int i=0;i<nums.length;i++){
            int t=target-nums[i];
            int flag=Arrays.binarySearch(nums,i+1,nums.length,t);
            if(flag>=0){
                res.add(t);
                res.add(target-t);
                break;
            }
        }
        return res.stream().mapToInt(Integer::valueOf).toArray();
    }
}

剑指 Offer 57 和为s的连续正数序列

难度简单389

输入一个正整数 target ,输出所有和为 target 的连续正整数序列(至少含有两个数)。

序列内的数字由小到大排列,不同序列按照首个数字从小到大排列。

示例 1:

输入:target = 9
输出:[[2,3,4],[4,5]]

思想:

  1. 滑动窗口
class Solution {
    public int[][] findContinuousSequence(int target) {
        ArrayList<int []> res=new ArrayList<>();

        int l=1;
        int r=2;
        while(l<=target/2){
            if(sum(l,r)==target){
                int []t=new int[r-l+1];
                int v=l;
                for(int i=0;i<r-l+1;i++){
                    t[i]=v;
                    v++;
                }
                res.add(t);
                l++;
                r++;
            }else if(sum(l,r)>target){
                l++;
            }else{
                r++;
            }
        }
        return res.toArray(new int[0][]);
    }
    int sum(int l,int r){
        return (l+r)*(r-l+1)/2;
    }
}

剑指 Offer 58 翻转单词顺序

难度简单178

输入一个英文句子,翻转句子中单词的顺序,但单词内字符的顺序不变。为简单起见,标点符号和普通字母一样处理。例如输入字符串"I am a student. ",则输出"student. a am I"。

示例 1:

输入: "the sky is blue"
输出: "blue is sky the"
class Solution {
    public String reverseWords(String s) {
       String res="";
       String t="";
       for(int i=0;i<s.length();i++){
           if(s.charAt(i)==' '){
               if(t.length()!=0){
                   if(res.length()==0){
                       res=t+"";
                   }else{
                       res=t+" "+res;
                   }
                   t="";
               }
           }else{
               t=t+String.valueOf(s.charAt(i));
               if(i==s.length()-1){
                    if(res.length()==0){
                       res=t+"";
                   }else{
                       res=t+" "+res;
                   }
               }
           }
       }
        
        return res;
    }
}

剑指 Offer 58 左旋转字符串

难度简单203

字符串的左旋转操作是把字符串前面的若干个字符转移到字符串的尾部。请定义一个函数实现字符串左旋转操作的功能。比如,输入字符串"abcdefg"和数字2,该函数将返回左旋转两位得到的结果"cdefgab"。

class Solution {
    public String reverseLeftWords(String s, int n) {
        return s.substring(n,s.length())+s.substring(0,n);
    }
}

 剑指 Offer 59 滑动窗口的最大值

难度困难399

给定一个数组 nums 和滑动窗口的大小 k,请找出所有滑动窗口里的最大值。

示例:

输入: nums = [1,3,-1,-3,5,3,6,7], 和 k = 3
输出: [3,3,5,5,6,7] 
解释: 

  滑动窗口的位置                最大值
---------------               -----
[1  3  -1] -3  5  3  6  7       3
 1 [3  -1  -3] 5  3  6  7       3
 1  3 [-1  -3  5] 3  6  7       5
 1  3  -1 [-3  5  3] 6  7       5
 1  3  -1  -3 [5  3  6] 7       6
 1  3  -1  -3  5 [3  6  7]      7
class Solution {
    public int[] maxSlidingWindow(int[] nums, int k) {

        ArrayList<Integer> res=new ArrayList<>();
        if(nums.length==0)return res.stream().mapToInt(Integer::valueOf).toArray();
        PriorityQueue<Integer> q=new PriorityQueue<>(new Comparator<Integer>(){
            public int compare(Integer a1,Integer a2){
                if(a1>a2)return -1;
                else return 1;
            }
        });
        int l=0;
        int r=k-1;
        for(int i=0;i<k-1;i++){
            q.add(nums[i]);
        }
        while(r<nums.length){
            q.add(nums[r]);
            res.add(q.peek());
            
            // res.add(sum[r]-sum[l-1]);
            q.remove(new Integer(nums[l]));
            r++;
            l++;
            
        }
        return res.stream().mapToInt(Integer::valueOf).toArray();
    }
}

剑指 Offer 59 队列中的最大值

难度中等327

请定义一个队列并实现函数 max_value 得到队列里的最大值,要求函数max_valuepush_back 和 pop_front 的均摊时间复杂度都是O(1)。

若队列为空,pop_front 和 max_value 需要返回 -1

示例 1:

输入: 
["MaxQueue","push_back","push_back","max_value","pop_front","max_value"]
[[],[1],[2],[],[],[]]
输出: [null,null,null,2,1,2]
class MaxQueue {
    PriorityQueue<Integer> q=new PriorityQueue<>(new Comparator<Integer>(){
        public int compare(Integer a1,Integer a2){
            if(a1>a2)return -1;
            else return 1;
        }
    });
    LinkedList<Integer> l=new LinkedList<>();

    public MaxQueue() {

    }
    
    public int max_value() {
        if(l.size()==0)return -1;
        return q.peek();
    }
    
    public void push_back(int value) {
        l.addLast(value);
        q.add(value);
    }
    
    public int pop_front() {
        if(l.size()==0)return -1;
        int res=l.getFirst();
        l.removeFirst();
        q.remove(new Integer(res));
        return res;
    }
}

/**
 * Your MaxQueue object will be instantiated and called as such:
 * MaxQueue obj = new MaxQueue();
 * int param_1 = obj.max_value();
 * obj.push_back(value);
 * int param_3 = obj.pop_front();
 */

剑指 Offer 60 n个骰子的点数

难度中等374

把n个骰子扔在地上,所有骰子朝上一面的点数之和为s。输入n,打印出s的所有可能的值出现的概率。

你需要用一个浮点数数组返回答案,其中第 i 个元素代表这 n 个骰子所能掷出的点数集合中第 i 小的那个的概率。

思路:

  1. 根据骰子的个数,由少到多并进行统计,根据上一层的点数去求得下一层的点数。
class Solution {
    public double[] dicesProbability(int n) {
        double[] dp = new double[6];
        Arrays.fill(dp, 1.0 / 6.0);
        for (int i = 2; i <= n; i++) {// 骰子数
            double[] tmp = new double[5 * i + 1];// 下一层的骰子
            for (int j = 0; j < dp.length; j++) {// 当前层的骰子数
                for (int k = 0; k < 6; k++) {//从当前层跳到下一层有六种情况
                    tmp[j + k] += dp[j] / 6.0;// 每种情况进行累加
                }
            }
            dp = tmp;
        }
        return dp;
    }
}

剑指 Offer 61 扑克牌中的顺子

难度简单211

若干副扑克牌中随机抽 5 张牌,判断是不是一个顺子,即这5张牌是不是连续的。2~10为数字本身,A为1,J为11,Q为12,K为13,而大、小王为 0 ,可以看成任意数字。A 不能视为 14。

class Solution {
    public boolean isStraight(int[] nums) {
        Arrays.sort(nums);
        int count_0=0;
        int l=-1;
        for(int i:nums){
            if(i==0){
                count_0++;
            }else{
                if(l==-1){
                    l=i;
                }else{
                    if(i-l==1){
                        l=i;
                    }else if(i-l==0){
                        return false;
                    }else{
                        int tep=i-l-1;
                        if(tep>count_0){
                            return false;
                        }else{
                            l=i;
                            count_0-=tep;
                        }
                    }
                }
            }
        }
        return true;
    }
}

剑指 Offer 62 圆圈中最后剩下的数字

难度简单554

0,1,···,n-1这n个数字排成一个圆圈,从数字0开始,每次从这个圆圈里删除第m个数字(删除后从下一个数字开始计数)。求出这个圆圈里剩下的最后一个数字。

例如,0、1、2、3、4这5个数字组成一个圆圈,从数字0开始每次删除第3个数字,则删除的前4个数字依次是2、0、4、1,因此最后剩下的数字是3。

思路:

约瑟夫环问题,可以采用list去模拟删除操作,index=(index+m-1)%n,n为变化的。

class Solution {
    public int lastRemaining(int n, int m) {
        ArrayList<Integer> list = new ArrayList<>(n);
        for (int i = 0; i < n; i++) {
            list.add(i);
        }
        int idx = 0;
        while (n > 1) {
            idx = (idx + m - 1) % n;
            list.remove(idx);
            n--;
        }
        return list.get(0);
    }
}

剑指 Offer 63 股票的最大利润

难度中等223

假设把某股票的价格按照时间先后顺序存储在数组中,请问买卖该股票一次可能获得的最大利润是多少?

示例 1:

输入: [7,1,5,3,6,4]
输出: 5
解释: 在第 2 天(股票价格 = 1)的时候买入,在第 5 天(股票价格 = 6)的时候卖出,最大利润 = 6-1 = 5 。
     注意利润不能是 7-1 = 6, 因为卖出价格需要大于买入价格

思路:

  1. 要找到最低点在前,最高点在后的两点,采用单调栈的方式记录栈低就能满足要求。
class Solution {
    public int maxProfit(int[] prices) {
       
        int res=0;
        int low=0;
        Stack<Integer> s=new Stack<>();
        for(int i:prices){
            if(s.isEmpty()){
                s.push(i);
                low=i;
            }else if(s.peek()<i){
                s.push(i);
                res=Integer.max(res,i-low);
            }else{
                while(!s.isEmpty()&&s.peek()>i){
                    s.pop();
                }
                // if(s.isEmpty())low=0;
                s.push(i);
                low=Integer.min(low,i);
                if(s.size()>=2){
                    res=Integer.max(res,i-low);
                }


            }

        }
        return res;
    }
}

剑指 Offer 64 求1+2+...+n

难度中等448

求 1+2+...+n ,要求不能使用乘除法、for、while、if、else、switch、case等关键字及条件判断语句(A?B:C)。

class Solution {
    public int sumNums(int n) {
        if(n==1)return 1;
        return n+sumNums(n-1);
    }
}

剑指 Offer 65 不用加减乘除做加法

难度简单269

写一个函数,求两个整数之和,要求在函数体内不得使用 “+”、“-”、“*”、“/” 四则运算符号。

class Solution {
    public int add(int a, int b) {
        int res=0;
        int jin=0;
        for(int i=0;i<32;i++){
            
            int a1=(a>>i)&1;
            int b1=(b>>i)&1;
            if(jin==1){
                if(a1==1&&b1==1){
                    jin=1;
                    res+=(1<<i);
                }else if(a1==1||b1==1){
                    jin=1;

                }else{
                    jin=0;
                    res+=(1<<i);
                }
            }else{
                if(a1==1&&b1==1){
                    jin=1;
                }else if(a1==1||b1==1){
                    jin=0;
                    res+=(1<<i);
                }else{
            }
        }
    }
    return res;
    }
}

剑指 Offer 66 构建乘积数组

难度中等205

给定一个数组 A[0,1,…,n-1],请构建一个数组 B[0,1,…,n-1],其中 B[i] 的值是数组 A 中除了下标 i 以外的元素的积, 即 B[i]=A[0]×A[1]×…×A[i-1]×A[i+1]×…×A[n-1]。不能使用除法。

示例:

输入: [1,2,3,4,5]
输出: [120,60,40,30,24]
class Solution {
    public int[] constructArr(int[] a) {
        if(a.length<=1)return a;
        int []t1=new int[a.length];
        int []t2=new int[a.length];
        Arrays.fill(t1,1);
        Arrays.fill(t2,1);
        int temp=1;
        for(int i=0;i<a.length;i++){
            temp*=a[i];
            t1[i]=temp;
        }
        temp=1;
        for(int i=a.length-1;i>=0;i--){
            temp*=a[i];
            t2[i]=temp;
        }
        int []res=new int[a.length];
        for(int i=0;i<res.length;i++){
            if(i==0){
                res[i]=t2[1];
            }else if(i==res.length-1){
                res[i]=t1[res.length-2];
            }else{
                res[i]=t1[i-1]*t2[i+1];
            }
        }
        return res;
    }
}

剑指 Offer 67 把字符串转换成整数

难度中等146

写一个函数 StrToInt,实现把字符串转换成整数这个功能。不能使用 atoi 或者其他类似的库函数。

首先,该函数会根据需要丢弃无用的开头空格字符,直到寻找到第一个非空格的字符为止。

当我们寻找到的第一个非空字符为正或者负号时,则将该符号与之后面尽可能多的连续数字组合起来,作为该整数的正负号;假如第一个非空字符是数字,则直接将其与之后连续的数字字符组合起来,形成整数。

该字符串除了有效的整数部分之后也可能会存在多余的字符,这些字符可以被忽略,它们对于函数不应该造成影响。

注意:假如该字符串中的第一个非空格字符不是一个有效整数字符、字符串为空或字符串仅包含空白字符时,则你的函数不需要进行转换。

在任何情况下,若函数不能进行有效的转换时,请返回 0。

说明:

假设我们的环境只能存储 32 位大小的有符号整数,那么其数值范围为 [−231,  231 − 1]。如果数值超过这个范围,请返回  INT_MAX (231 − 1) 或 INT_MIN (−231) 。

class Solution {
         public int strToInt(String str) {
             str=str.trim();
             if(str.length()==0)return 0;
             if(str.charAt(0)!='+'&&str.charAt(0)!='-'&&str.charAt(0)<'0'&&str.charAt(0)>'9')return 0;
             Long res=0L;
             boolean flag=true;
             for(int i=0;i<str.length();i++){
                 if(i==0&&(str.charAt(i)=='-'))
                 {
                     flag=false;
                     continue;
                 }
                 if(i==0&&(str.charAt(i)=='+'))
                 {
                     continue;
                 }
                 if(str.charAt(i)<'0'||str.charAt(i)>'9'){
                     break;
                 }else{
                     res=res*10+(str.charAt(i)-'0');
                 }
                 if(!flag){
                     if(-res<=Integer.MIN_VALUE){
                         return Integer.MIN_VALUE;
                     }
                 }
                 else
                 if(res>=Integer.MAX_VALUE){
                     return Integer.MAX_VALUE;
                 }
             }
             if(!flag){
                 if(-res<=Integer.MIN_VALUE){
                     return Integer.MIN_VALUE;
                 }else{
                     return -res.intValue();
                 }
             }
             if(res>=Integer.MAX_VALUE){
                 return Integer.MAX_VALUE;
             }else{
                 return res.intValue();
             }
             // return Integer.parseInt(String.valueOf(res));
         }
     }

剑指 Offer 68 二叉树的最近公共祖先

难度简单388

给定一个二叉树, 找到该树中两个指定节点的最近公共祖先。

百度百科中最近公共祖先的定义为:“对于有根树 T 的两个结点 p、q,最近公共祖先表示为一个结点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)。”

例如,给定如下二叉树:

思路:

  1. 可以用dfs去寻找单个目标值,找到返回true,当左右子树都找到目标值的时候,此时的根就为目标节点。
class Solution {
public:
    TreeNode* ans;
    bool dfs(TreeNode* root, TreeNode* p, TreeNode* q) {
        if (root == nullptr) return false;
        bool lson = dfs(root->left, p, q);
        bool rson = dfs(root->right, p, q);
        if ((lson && rson) || ((root->val == p->val || root->val == q->val) && (lson || rson)))// 左右子树存在目标值
        {
            ans = root;
        }
        return lson || rson || (root->val == p->val || root->val == q->val);
    }
    TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
        dfs(root, p, q);
        return ans;
    }
};

LeetCode 热题 HOT 100

1,两数之和

难度简单13695

给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出 和为目标值 target  的那 两个 整数,并返回它们的数组下标。

你可以假设每种输入只会对应一个答案。但是,数组中同一个元素在答案里不能重复出现。

你可以按任意顺序返回答案。

思路:

  • 采用两层遍历法,代码比较简单,单复杂度高、
  • 为了减少时间复杂度,因为题干中说明了,只有一种情况的答案,顾可以采用map<value,index>去存储nums数组,达到减少时间复杂度。
class Solution {
    public int[] twoSum(int[] nums, int target) {   
        for(int i=0;i<nums.length-1;i++){
            for(int j=i+1;j<nums.length;j++){
                if(nums[i]+nums[j]==target)
                return new int[]{i,j};
            }
        }
        return new int[]{0,0};
    }
}
class Solution {
    public int[] twoSum(int[] nums, int target) {   
        HashMap<Integer,Integer> m=new HashMap<>();
        for(int i=0;i<nums.length;i++){   
          m.put(nums[i],i);
        }
        for(int i=0;i<nums.length;i++){
            int t=target-nums[i];
            if(m.containsKey(t)&&m.get(t)!=i){// 相同的位置的值不能出现在答案中
                return new int[]{i,m.get(t)};
            }
        }
        return new int[]{0,0};
    }
}

2,两数相加(两链表相加)

难度中等7667

给你两个 非空 的链表,表示两个非负的整数。它们每位数字都是按照 逆序 的方式存储的,并且每个节点只能存储 一位 数字。

请你将两个数相加,并以相同形式返回一个表示和的链表。

你可以假设除了数字 0 之外,这两个数都不会以 0 开头。

/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode() {}
 *     ListNode(int val) { this.val = val; }
 *     ListNode(int val, ListNode next) { this.val = val; this.next = next; }
 * }
 */
class Solution {
    public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
        ListNode res=new ListNode();
        ListNode tail=res;
        res.next=null;
        int jin=0;
        while(l1!=null&&l2!=null){
            int t=l1.val+l2.val+jin;
            if(t>=10){
                jin=1;
                t-=10;
            }else{
                jin=0;
            }
            ListNode temp=new ListNode(t);
            temp.next=null;
            tail.next=temp;
            tail=temp;
            l1=l1.next;
            l2=l2.next;
        }
        while(l1!=null){
            int t=l1.val+jin;
            if(t>=10){
                jin=1;
                t-=10;
            }else{
                jin=0;
            }
           ListNode temp=new ListNode(t);
            temp.next=null;
            tail.next=temp;
            tail=temp;
            l1=l1.next;
        }
         while(l2!=null){
            int t=l2.val+jin;
            if(t>=10){
                jin=1;
                t-=10;
            }else{
                jin=0;
            }
            ListNode temp=new ListNode(t);
            temp.next=null;
            tail.next=temp;
            tail=temp;
            l2=l2.next;
        }
        if(jin==1){
             ListNode temp=new ListNode(1);
            temp.next=null;
            tail.next=temp;
            tail=temp;
        }
       return res.next;
    }
}

3,无重复字符的最长子串(for+indexOf)

难度中等7090

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

class Solution {
    public int lengthOfLongestSubstring(String s) {
        int res=0;
        String temp="";
        for(int i=0;i<s.length();i++){
            int flag=temp.indexOf(s.charAt(i));
            if(flag>=0){
                temp=temp.substring(flag+1,temp.length());
            }
            temp=temp+s.charAt(i);
            res=Integer.max(res,temp.length());
        }
        return res;
    }
}

4,寻找两个正序数组的中位数(大小根堆)

难度困难5124

给定两个大小分别为 m 和 n 的正序(从小到大)数组 nums1 和 nums2。请你找出并返回这两个正序数组的 中位数 。

算法的时间复杂度应该为 O(log (m+n)) 。

class Solution {
    public double findMedianSortedArrays(int[] nums1, int[] nums2) {
        ArrayList<Integer> res=new ArrayList<>();
        for(int i:nums1){
            res.add(i);
        }
         for(int i:nums2){
            res.add(i);
        }
        Collections.sort(res);
        int f=nums1.length+nums2.length;
        if(f%2==0){
            return (double)(res.get(f/2-1)+res.get(f/2))/2;
        }else{
            return res.get(f/2);
        }
    }
}

5,最长回文子串(dp)

难度中等4842

给你一个字符串 s,找到 s 中最长的回文子串。

示例 1:

输入:s = "babad"
输出:"bab"
解释:"aba" 同样是符合题意的答案。
class Solution {
    public String longestPalindrome(String s) {
        // 典型的dp问题 dp[i][j] 表示下标i-j的子字符串为回文串
        // dp[i][j]=true or flase
        String res="";
        res=res+s.charAt(0);
        boolean [][]dp=new boolean[s.length()][s.length()];
        for(int i=0;i<s.length();i++){
            dp[i][i]=true;
        }
        for(int len=2;len<=s.length();len++){
            for(int i=0;i<s.length();i++){
                int j=i+len-1;
                 if(j>=s.length())break;
                 if(s.charAt(i)==s.charAt(j)){
                     if(j-i<=2){
                         dp[i][j]=true;
                     }else{
                         dp[i][j]=dp[i+1][j-1];
                     }
                 }
                 if(dp[i][j]==true&&j-i+1>res.length()){
                     res=s.substring(i,j+1);
                 }
            }
        }
        return res;
    }
}

11,盛最多水的容器(贪心)

难度中等3280

给定一个长度为 n 的整数数组 height 。有 n 条垂线,第 i 条线的两个端点是 (i, 0) 和 (i, height[i]) 。

找出其中的两条线,使得它们与 x 轴共同构成的容器可以容纳最多的水。

返回容器可以储存的最大水量。

说明:你不能倾斜容器。

class Solution {
    public int maxArea(int[] height) {
        int res=0;
        int left=0;
        int right=height.length-1;
        while(left<right){
            res=Math.max(res,(right-left)*Math.min(height[left],height[right]));
            if(height[left]>height[right]){
                right--;
            }else if(height[left]<height[right]){
                left++;
            }else{
                if(right-left<=2){
                    left++;
                }else{
                    if(height[left+1]>height[right-1]){
                        left++;
                    }else {
                        right--;
                    }
                }
            }
        }
        return res;
    }
}

15、三数之和(三层循环)

难度中等4464

给你一个包含 n 个整数的数组 nums,判断 nums 中是否存在三个元素 a,b,c ,使得 a + b + c = 0 ?请你找出所有和为 0 且不重复的三元组。

注意:答案中不可以包含重复的三元组。

示例 1:

输入:nums = [-1,0,1,2,-1,-4]
输出:[[-1,-1,2],[-1,0,1]]

思路:

  1. 三数之和,先排序,三层遍历,固定两个索引,从后向前遍历,去重步骤是开头两固定的索引(满足nums[i]!=nums[i-1])即可达到去重复的目的。
class Solution {
    public List<List<Integer>> threeSum(int[] nums) {
        int n = nums.length;
        Arrays.sort(nums);
        List<List<Integer>> ans = new ArrayList<List<Integer>>();
        // 枚举 a
        for (int first = 0; first < n; ++first) {
            // 需要和上一次枚举的数不相同
            if (first > 0 && nums[first] == nums[first - 1]) {
                continue;
            }
            // c 对应的指针初始指向数组的最右端
            int third = n - 1;
            int target = -nums[first];
            // 枚举 b
            for (int second = first + 1; second < n; ++second) {
                // 需要和上一次枚举的数不相同
                if (second > first + 1 && nums[second] == nums[second - 1]) {
                    continue;
                }
                // 需要保证 b 的指针在 c 的指针的左侧
                while (second < third && nums[second] + nums[third] > target) {
                    --third;
                }
                // 如果指针重合,随着 b 后续的增加
                // 就不会有满足 a+b+c=0 并且 b<c 的 c 了,可以退出循环
                if (second == third) {
                    break;
                }
                if (nums[second] + nums[third] == target) {
                    List<Integer> list = new ArrayList<Integer>();
                    list.add(nums[first]);
                    list.add(nums[second]);
                    list.add(nums[third]);
                    ans.add(list);
                }
            }
        }
        return ans;
    }
}

17、电话号码的字母组合(dfs)

难度中等1770

给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。答案可以按 任意顺序 返回。

给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。

示例 1:

输入:digits = "23"
输出:["ad","ae","af","bd","be","bf","cd","ce","cf"]
class Solution {
    List<String> res=new ArrayList<>();
    public List<String> letterCombinations(String digits) {
        if(digits.length()==0)return res;
        String[] map=new String[]{"","","abc","def","ghi","jkl","mno","pqrs","tuv","wxyz"};
        dfs(0,digits,"",map);
        return res;
    }
    public void dfs(int index,String s,String cv,String []map){
        if(index==s.length()){
            res.add(new String(cv));
            return;
        }
        for(int i=0;i<map[s.charAt(index)-'0'].length();i++){
            dfs(index+1,s,cv+map[s.charAt(index)-'0'].charAt(i),map);
        }
    }
}

19,删除链表的倒数第N个结点

难度中等1861

给你一个链表,删除链表的倒数第 n 个结点,并且返回链表的头结点。

示例 1:

输入:head = [1,2,3,4,5], n = 2
输出:[1,2,3,5]
/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode() {}
 *     ListNode(int val) { this.val = val; }
 *     ListNode(int val, ListNode next) { this.val = val; this.next = next; }
 * }
 */
class Solution {
    public ListNode removeNthFromEnd(ListNode head, int n) {
        ListNode res=new ListNode(-1);
        res.next=head;
        while(n!=0){
            head=head.next;
            n--;
        }

        ListNode p1=res,p2=res.next;
        while(head!=null){
            p1=p1.next;
            p2=p2.next;
            head=head.next;
        }
        p1.next=p2.next;
        return res.next;
    }
}

20,有效的括号(栈)

难度简单3073

给定一个只包括 '('')''{''}''['']' 的字符串 s ,判断字符串是否有效。

有效字符串需满足:

  1. 左括号必须用相同类型的右括号闭合。
  2. 左括号必须以正确的顺序闭合。
class Solution {
    public boolean isValid(String s) {
        Stack<Character> stack=new Stack<>();
        for(int i=0;i<s.length();i++){
            if(s.charAt(i)=='('||s.charAt(i)=='{'||s.charAt(i)=='['){
               stack.push(s.charAt(i));
            }else{
               if(stack.isEmpty())return false;
               switch(s.charAt(i)){
                   case ')': 
                   if(stack.peek()=='('){
                       stack.pop();
                   }else return false;
                   break;
                   case '}': 
                   if(stack.peek()=='{'){
                       stack.pop();
                   }else return false;
                   break;
                   case ']': 
                   if(stack.peek()=='['){
                       stack.pop();
                   }else return false;
                   break;
               }
            }
            
        }
        return stack.isEmpty();
    }
}

21、合并两个有序链表

难度简单2264

将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。 

示例 1:

输入:l1 = [1,2,4], l2 = [1,3,4]
输出:[1,1,2,3,4,4]
/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode() {}
 *     ListNode(int val) { this.val = val; }
 *     ListNode(int val, ListNode next) { this.val = val; this.next = next; }
 * }
 */
class Solution {
    public ListNode mergeTwoLists(ListNode list1, ListNode list2) {
        ListNode res=new ListNode(-1);
        res.next=null;
        ListNode tail=res;

        while(list1!=null&&list2!=null){
            ListNode temp;
            if(list1.val>list2.val){
                temp=list2;
                list2=list2.next;
            }else{
                temp=list1;
                list1=list1.next;
            }
            temp.next=null;
            tail.next=temp;
            tail=temp;
        }
        if(list1!=null){
            tail.next=list1;
        }
        if(list2!=null){
            tail.next=list2;
        }
        return res.next;
    }
}

22、括号生成(DFS)

难度中等2442

数字 n 代表生成括号的对数,请你设计一个函数,用于能够生成所有可能的并且 有效的 括号组合。

示例 1:

输入:n = 3
输出:["((()))","(()())","(())()","()(())","()()()"]

思路:

  1. 深度遍历只需要管住(,然后对)的数量小于等于(的数量。
class Solution {
    List<String> res=new ArrayList<>();
    public List<String> generateParenthesis(int n) {
        dfs(0,"",n);
        return res;
    }
    void dfs(int index,String cv,int n){
        if(cv.length()==2*n){
            res.add(new String(cv));
            return;
        }
        if(index<n)
        dfs(index+1,cv+"(",n);

        if(2*index>cv.length()){
            dfs(index,cv+")",n);
        }
    }
}

23、合并k个升序链表

难度困难1806

给你一个链表数组,每个链表都已经按升序排列。

请你将所有链表合并到一个升序链表中,返回合并后的链表。

示例 1:

输入:lists = [[1,4,5],[1,3,4],[2,6]]
输出:[1,1,2,3,4,4,5,6]
解释:链表数组如下:
[
  1->4->5,
  1->3->4,
  2->6
]
将它们合并到一个有序链表中得到。
1->1->2->3->4->4->5->6

注意:

  • throw new IllegalArgumentException("Comparison method violates its general contract!");
  • google了一下:JDK7中的Collections.Sort方法实现中,如果两个值是相等的,那么compare方法需要返回0,否则可能会在排序时抛错,而JDK6是没有这个限制的。
/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode() {}
 *     ListNode(int val) { this.val = val; }
 *     ListNode(int val, ListNode next) { this.val = val; this.next = next; }
 * }
 */
class Solution {
    public ListNode mergeKLists(ListNode[] lists) {
        ListNode res=new ListNode(-1);
        ListNode tail=res;
        res.next=null;
        ArrayList<ListNode> arr=new ArrayList<>();
        for(ListNode temp:lists){
            ListNode t=temp;
            while(t!=null){
                ListNode tt=t;
                t=t.next;
                tt.next=null;
                arr.add(tt);
            }
        }
        Collections.sort(arr,new Comparator<ListNode>(){
            public int compare(ListNode o1,ListNode o2){
                // if(o1.val>o2.val)return 1;
                // else if(o1.val<o2.val) return -1;
                // else return 0;
                return o1.val-o2.val;
            }
        });
        for(int i=0;i<arr.size();i++){
            ListNode temp=arr.get(i);
            tail.next=temp;
            tail=temp;
        }
        return res.next;
    }
}

31,下一个排列

难度中等1586

整数数组的一个 排列  就是将其所有成员以序列或线性顺序排列。

  • 例如,arr = [1,2,3] ,以下这些都可以视作 arr 的排列:[1,2,3][1,3,2][3,1,2][2,3,1] 。

整数数组的 下一个排列 是指其整数的下一个字典序更大的排列。更正式地,如果数组的所有排列根据其字典顺序从小到大排列在一个容器中,那么数组的 下一个排列 就是在这个有序容器中排在它后面的那个排列。如果不存在下一个更大的排列,那么这个数组必须重排为字典序最小的排列(即,其元素按升序排列)。

  • 例如,arr = [1,2,3] 的下一个排列是 [1,3,2] 。
  • 类似地,arr = [2,3,1] 的下一个排列是 [3,1,2] 。
  • 而 arr = [3,2,1] 的下一个排列是 [1,2,3] ,因为 [3,2,1] 不存在一个字典序更大的排列。

给你一个整数数组 nums ,找出 nums 的下一个排列。

必须 原地 修改,只允许使用额外常数空间。

思路:

  1. 从后向前遍历,索引i之后的数字只要满足一个大于nums[i]的就可以跳出,将i之后的最小大于nums[i]的数和nums[i]进行交换,在进行Arrays.sort(nums,i+1,nums.length);
class Solution {
    ArrayList<String> list=new ArrayList<>();
    public void nextPermutation(int[] nums) {
        int i=nums.length-1;
        PriorityQueue<Integer> q=new PriorityQueue<>((x,y)->y-x);
        for(;i>=0;i--){
            if(q.isEmpty()){
                q.add(nums[i]);
            }else{
                if(nums[i]<q.peek()){
                    break;
                }else{
                    q.add(nums[i]);
                }
            }
        }
        if(q.size()==nums.length){
            Arrays.sort(nums,0,nums.length);
            return;
        }
        int last=-1;
        while(!q.isEmpty()&&q.peek()>nums[i]){
            last=q.peek();
            q.poll();
        }
        int j=i+1;
        while(j<nums.length){
            if(nums[j]==last){
                break;
            }
            j++;
        }
        nums[i]^=nums[j];
        nums[j]^=nums[i];
        nums[i]^=nums[j];
        Arrays.sort(nums,i+1,nums.length);
    }
}

33,搜索旋转排序数组

难度中等1915

整数数组 nums 按升序排列,数组中的值 互不相同 。

在传递给函数之前,nums 在预先未知的某个下标 k0 <= k < nums.length)上进行了 旋转,使数组变为 [nums[k], nums[k+1], ..., nums[n-1], nums[0], nums[1], ..., nums[k-1]](下标 从 0 开始 计数)。例如, [0,1,2,4,5,6,7] 在下标 3 处经旋转后可能变为 [4,5,6,7,0,1,2] 。

给你 旋转后 的数组 nums 和一个整数 target ,如果 nums 中存在这个目标值 target ,则返回它的下标,否则返回 -1 。

class Solution {
    public int search(int[] nums, int target) {
        // 左边从左到右是递增的
        // 右边从右到左是递减的
        int len=nums.length;
        int i=0;
        for(;i<len;i++)
        {
            if(nums[i]==target)return i;
            if(i>0&&nums[i]<nums[i-1]){
                break;
            }
        } 
        int j=len-1;
        while(i<=j){
            int mid=(i+j)/2;
            if(target==nums[mid]){
                return mid;
            }else if(nums[mid]>target){
                j=mid-1;
            }else{
                i=mid+1;
            }
        }
        return -1;
    }
}

34,在排序数组中查找元素的第一个和最后一个位置

难度中等1533

给定一个按照升序排列的整数数组 nums,和一个目标值 target。找出给定目标值在数组中的开始位置和结束位置。

如果数组中不存在目标值 target,返回 [-1, -1]

进阶:

  • 你可以设计并实现时间复杂度为 O(log n) 的算法解决此问题吗?
class Solution {
    public int[] searchRange(int[] nums, int target) {
        
        int index=Arrays.binarySearch(nums,target);
        if(index<0)return new int[]{-1,-1};
        int l=index,r=index;
        while(l>=0&&nums[l]==target){
            l--;
        }
        
        l++;
       
        while(r<nums.length&&nums[r]==target){
            r++;
        }
        r--;
        return new int[]{l,r};

    }
}

39,组合总和(dfs)注意和139单词拆分的区别

难度中等1824

给你一个 无重复元素 的整数数组 candidates 和一个目标整数 target ,找出 candidates 中可以使数字和为目标数 target 的 所有 不同组合 ,并以列表形式返回。你可以按 任意顺序 返回这些组合。

candidates 中的 同一个 数字可以 无限制重复被选取 。如果至少一个数字的被选数量不同,则两种组合是不同的。 

对于给定的输入,保证和为 target 的不同组合数少于 150 个。

class Solution {
    List<List<Integer>> res=new ArrayList<>();
    public List<List<Integer>> combinationSum(int[] candidates, int target) {
        dfs(candidates,target,0,new ArrayList<>());
        return res;
    }

    void dfs(int[] candidates,int target,int index,ArrayList<Integer> tep){
        if(index==candidates.length||target<0){
            return;
        }
        if(target==0){
            res.add(new ArrayList<>(tep));
            return;
        }
        dfs(candidates,target,index+1,tep);

        if(target-candidates[index]>=0){
            tep.add(candidates[index]);
            dfs(candidates,target-candidates[index],index,tep);
            tep.remove(new Integer(candidates[index]));
        }
    }
}

42,接雨水

难度困难3227

给定 n 个非负整数表示每个宽度为 1 的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。

示例 1:

输入:height = [0,1,0,2,1,0,1,3,2,1,2,1]
class Solution {
    public int trap(int[] height) {
        int left[]=new int[height.length];
        int right[]=new int[height.length];
        int min=-1;
        for(int i=0;i<height.length;i++){
            if(height[i]>min){
                min=height[i];
            }
            left[i]=min;
        }
        min=-1;
        for(int i=height.length-1;i>=0;i--){
            if(height[i]>min){
                min=height[i];
            }
            right[i]=min;
        }
        int res=0;
        for(int i=0;i<height.length;i++){
            int cmin=Integer.min(left[i],right[i]);
            if(cmin>height[i]){
                res+=(cmin-height[i]);
            }
        }
        return res;
    }
}

46,全排列(DFS

难度中等1846

给定一个不含重复数字的数组 nums ,返回其 所有可能的全排列 。你可以 按任意顺序 返回答案。

class Solution {
    List<List<Integer>> res=new ArrayList<>();
    public List<List<Integer>> permute(int[] nums) {
        dfs(0,nums,new boolean[nums.length],new ArrayList<Integer>());
        return res;
    }
    void dfs(int index,int []nums,boolean []visited,ArrayList<Integer> temp){
        if(index==nums.length){
            res.add(new ArrayList<>(temp));
            return;
        }
        for(int i=0;i<visited.length;i++){
            if(!visited[i]){
                temp.add(nums[i]);
                visited[i]=true;
                dfs(index+1,nums,visited,temp);
                temp.remove(new Integer(nums[i]));
                visited[i]=false;
            }
        }
    }
}

48,旋转图像

难度中等1211

给定一个 × n 的二维矩阵 matrix 表示一个图像。请你将图像顺时针旋转 90 度。

你必须在 原地 旋转图像,这意味着你需要直接修改输入的二维矩阵。请不要 使用另一个矩阵来旋转图像。

思路:

  1. 逆时针旋转90°等效于左上角和右下角对换+上交对换。
  2. System.arraycopy(),可以保证对原数组进行从新赋值。
class Solution {
    public void rotate(int[][] matrix) {
        int res[][]=new int[matrix.length][matrix[0].length];
        int index=0;
        for(int i=0;i<matrix.length;i++){
            int tep[]=new int[matrix.length];
            int cv=0;
            for(int j=matrix.length-1;j>=0;j--){
                tep[cv]=matrix[j][i];
                cv++;
            }
            res[index]=Arrays.copyOf(tep,tep.length);
            index++;
        }
        // matrix=res;
        for(int i=0;i<matrix.length;i++)
        System.arraycopy(res[i],0,matrix[i],0,matrix.length);
    }
}//

49,字母异或位词分组(DFS+Map<String,List<String>>)

难度中等1054

给你一个字符串数组,请你将 字母异位词 组合在一起。可以按任意顺序返回结果列表。

字母异位词 是由重新排列源单词的字母得到的一个新单词,所有源单词中的字母通常恰好只用一次。

思路:

  1. map<String,value>,String的比较方法被从写,是比较值。
class Solution {
    List<List<String>> res=new LinkedList<>();
    public List<List<String>> groupAnagrams(String[] strs) {
        HashMap<String,List<String>> mymap=new HashMap<>();
        int len=strs.length;
        for(int i=0;i<len;i++)
        {
            char[] chars = strs[i].toCharArray();
            Arrays.sort(chars);
            String tep=new String(chars);
            List<String> strings = mymap.get(tep);
            if(strings==null)
            {
                strings=new LinkedList<>();
                strings.add(strs[i]);
                mymap.put(tep,strings);
            }
            else{
                strings.add(strs[i]);
            }
        }
        // 对mymap进行遍历
        mymap.forEach((x,y)->{
            res.add(new LinkedList<>(y));
        });
        return res;
    }
}

53,最大子数组和(贪心)

难度简单4543

给你一个整数数组 nums ,请你找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。

子数组 是数组中的一个连续部分。

class Solution {
    public int maxSubArray(int[] nums) {
        int res=Integer.MIN_VALUE;
        int cv=0;
        for(int i:nums){
            if(cv+i<=i){
                cv=i;
            }else{
                cv+=i;
            }
            res=Integer.max(res,cv);
        }
        return res;
    }
}

55,跳跃游戏(贪心)

难度中等1715

给定一个非负整数数组 nums ,你最初位于数组的 第一个下标 。

数组中的每个元素代表你在该位置可以跳跃的最大长度。

判断你是否能够到达最后一个下标。

思路:

  1. 从后向前跳跃,贪心思想。
class Solution {
    public boolean canJump(int[] nums) {
        int res=1;
        for(int i=nums.length-2;i>=0;i--){
            if(nums[i]>=res){
                res=1;
            }else{
                res++;
            }
        }
        return res==1?true:false;
    }
}

56,合并区间

难度中等1375

以数组 intervals 表示若干个区间的集合,其中单个区间为 intervals[i] = [starti, endi] 。请你合并所有重叠的区间,并返回 一个不重叠的区间数组,该数组需恰好覆盖输入中的所有区间 。

class Solution {
    public int[][] merge(int[][] intervals) {
        Arrays.sort(intervals, new Comparator<int[]>() {
            @Override
            public int compare(int[] o1, int[] o2) {
                if(o1[0]>o2[0]){
                    return 1;
                }else if(o1[0]==o2[0]){
                    if(o1[1]>o2[1]) {
                        return 1;
                    }
                    else if(o1[1]==o2[1]) {
                        return 0;
                    }else{
                        return -1;
                    }

                }else{
                    return -1;
                }
            }
        });
        List<int[]> res=new ArrayList<>();
        int index=0;
        int l=intervals[0][0];
        int r=intervals[0][1];
        index++;
        while(index<intervals.length){
            // 判断是否相交
            if(l<=intervals[index][0]&&intervals[index][0]<=r){
                r=Integer.max(r,intervals[index][1]);
            }else{
                res.add(new int[]{l,r});
                l=intervals[index][0];
                r=intervals[index][1];
            }
            index++;
        }
        res.add(new int[]{l,r});
        return res.toArray(new int [0][]);
    }
}

62,不同路径(DP)

难度中等1326

一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为 “Start” )。

机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish” )。

问总共有多少条不同的路径?

示例 1:

class Solution {
    public int uniquePaths(int m, int n) {
        int dp[][]=new int[m][n];
        for(int i=0;i<m;i++)dp[i][0]=1;
        for(int i=0;i<n;i++)dp[0][i]=1;

        for(int i=1;i<m;i++){
            for(int j=1;j<n;j++){
                dp[i][j]=dp[i-1][j]+dp[i][j-1];
            }
        }
        return dp[m-1][n-1];
    }
}

64,最小路径和(DP)

难度中等1179

给定一个包含非负整数的 m x n 网格 grid ,请找出一条从左上角到右下角的路径,使得路径上的数字总和为最小。

说明:每次只能向下或者向右移动一步。

示例 1:

class Solution {
    public int minPathSum(int[][] grid) {
        int dp[][]=new int[grid.length][grid[0].length];
        int csum=0;
        for(int i=0;i<grid.length;i++){
            csum+=grid[i][0];
            dp[i][0]=csum;
        }
        csum=0;
         for(int i=0;i<grid[0].length;i++){
            csum+=grid[0][i];
            dp[0][i]=csum;
        }
        for(int i=1;i<grid.length;i++){
            for(int j=1;j<grid[0].length;j++){
                dp[i][j]=Integer.min(dp[i-1][j],dp[i][j-1])+grid[i][j];
            }
        }
        return dp[grid.length-1][grid[0].length-1];
    }
}

70,爬楼梯(DFS,迭代)

难度简单2264

假设你正在爬楼梯。需要 n 阶你才能到达楼顶。

每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?

示例 1:

输入:n = 2
输出:2
解释:有两种方法可以爬到楼顶。
1. 1 阶 + 1 阶
2. 2 阶
class Solution {
    public int climbStairs(int n) {
        if(n==1)return 1;
        if(n==2)return 2;
        int l1=1,l2=2;
        int res=0;
        for(int i=3;i<=n;i++){
            res=l1+l2;
            l1=l2;
            l2=res;
        }
        return res;
    }
}

75,颜色分类

难度中等1193

给定一个包含红色、白色和蓝色、共 n 个元素的数组 nums ,原地对它们进行排序,使得相同颜色的元素相邻,并按照红色、白色、蓝色顺序排列。

我们使用整数 0、 1 和 2 分别表示红色、白色和蓝色。

必须在不使用库的sort函数的情况下解决这个问题。

示例 1:

输入:nums = [2,0,2,1,1,0]
输出:[0,0,1,1,2,2]
class Solution {
    public void sortColors(int[] nums) {
        int index0=0;
        int index2=0;
        int index1=0;
        for(int i=0;i<nums.length;i++){
          if(nums[i]==0)index0++;
          else if(nums[i]==1)index1++;
          else index2++;
        }
        Arrays.fill(nums,0,index0,0);
        Arrays.fill(nums,index0,index0+index1,1);
        Arrays.fill(nums,index0+index1,index0+index1+index2,2);
    }
}

78,子集(DFS)

难度中等1528

给你一个整数数组 nums ,数组中的元素 互不相同 。返回该数组所有可能的子集(幂集)。

解集 不能 包含重复的子集。你可以按 任意顺序 返回解集。

class Solution {
    List<List<Integer>> res=new ArrayList<>();
    public List<List<Integer>> subsets(int[] nums) {
        dfs(0,nums,new ArrayList());
        return res;
    }
    void dfs(int index,int []nums,ArrayList<Integer> cv){
        if(index==nums.length){
            res.add(new ArrayList<>(cv));
            return;
        }
        //
        dfs(index+1,nums,cv);
        
        cv.add(nums[index]);
        dfs(index+1,nums,cv);
        cv.remove(new Integer(nums[index]));
    }
}

79,单词搜索(DFS)

难度中等1224

给定一个 m x n 二维字符网格 board 和一个字符串单词 word 。如果 word 存在于网格中,返回 true ;否则,返回 false 。

单词必须按照字母顺序,通过相邻的单元格内的字母构成,其中“相邻”单元格是那些水平相邻或垂直相邻的单元格。同一个单元格内的字母不允许被重复使用。

示例 1:

输入:board = [["A","B","C","E"],["S","F","C","S"],["A","D","E","E"]], word = "ABCCED"
输出:true
class Solution {
    private boolean res=false;
    public boolean exist(char[][] board, String word) {
        int m=board.length;
        int n=board[0].length;
        boolean [][]visited=new boolean[m][n];
        for(int i=0;i<m;i++){
            if(res)break;
            for(int j=0;j<n;j++){
                if(res)break;
                if(board[i][j]==word.charAt(0)){
                    visited[i][j]=true;
                    dfs(i,j,m,n,1,visited,board,word);
                    visited[i][j]=false;
                }
            }
        }
        return res;
       
    }
    void dfs(int x,int y,int m,int n,int clen,boolean [][]visited,char[][] board,String word){
        if(clen==word.length()){
            res=true;
            return;
        }
        if(res)return;
        for(int i=0;i<4;i++){
            switch(i){
                case 0: 
                    if(x-1>=0&&!visited[x-1][y]&&board[x-1][y]==word.charAt(clen)){
                        visited[x-1][y]=true;
                        dfs(x-1,y,m,n,clen+1,visited,board,word);
                        visited[x-1][y]=false;
                    }
                
                break;
                  case 1: 
                    if(x+1<m&&!visited[x+1][y]&&board[x+1][y]==word.charAt(clen)){
                        visited[x+1][y]=true;
                        dfs(x+1,y,m,n,clen+1,visited,board,word);
                        visited[x+1][y]=false;
                    }
                    break;
                        case 2: 
                    if(y-1>=0&&!visited[x][y-1]&&board[x][y-1]==word.charAt(clen)){
                        visited[x][y-1]=true;
                        dfs(x,y-1,m,n,clen+1,visited,board,word);
                        visited[x][y-1]=false;
                    }
                    break;
                  case 3: 
                    if(y+1<n&&!visited[x][y+1]&&board[x][y+1]==word.charAt(clen)){
                        visited[x][y+1]=true;
                        dfs(x,y+1,m,n,clen+1,visited,board,word);
                        visited[x][y+1]=false;
                    }
                break;
            }
        }
    }

}

94,二叉树的中序遍历

难度简单1323

给定一个二叉树的根节点 root ,返回它的 中序 遍历。

示例 1:

class Solution {
    private List<Integer> res=new ArrayList<>();
    public List<Integer> inorderTraversal(TreeNode root) {
        inorder(root);
        return res;
    }

    void inorder(TreeNode root){
        if(root==null){
            return;
        }
        inorder(root.left);
        res.add(root.val);
        inorder(root.right);
    }
}

96,不同的二叉搜索树(DP)

卡特兰数-动态规划

难度中等1624

给你一个整数 n ,求恰由 n 个节点组成且节点值从 1 到 n 互不相同的 二叉搜索树 有多少种?返回满足题意的二叉搜索树的种数。

解题思路:
假设 n 个节点存在二叉排序树的个数是 G (n),令 f(i) 为以 i 为根的二叉搜索树的个数,则
G(n) = f(1) + f(2) + f(3) + f(4) + ... + f(n)

当 i 为根节点时,其左子树节点个数为 i-1 个,右子树节点为 n-i,则
f(i) = G(i-1)*G(n-i)

综合两个公式可以得到 卡特兰数 公式
G(n) = G(0)*G(n-1)+G(1)*(n-2)+...+G(n-1)*G(0)G(n)=G(0)∗G(n−1)+G(1)∗(n−2)+...+G(n−1)∗G(0)

class Solution {
    public int numTrees(int n) {
        int[] dp = new int[n+1];
        dp[0] = 1;// 表示树长度为0
        dp[1] = 1;// 表示树长度为1
        // dp[i]表示长度为i的搜索二叉树的种数,dp[i]=dp[0]*dp[i]+dp[1]*dp[i-1]+dp[2]*dp[i-2]..
        for(int i = 2; i < n + 1; i++)// 树长
            for(int j = 1; j < i + 1; j++) // 表示以j为根节点
                dp[i] += dp[j-1] * dp[i-j];
        
        return dp[n];
    }
}

98,验证二叉搜索树

难度中等1470

给你一个二叉树的根节点 root ,判断其是否是一个有效的二叉搜索树。

有效 二叉搜索树定义如下:

  • 节点的左子树只包含 小于 当前节点的数。
  • 节点的右子树只包含 大于 当前节点的数。
  • 所有左子树和右子树自身必须也是二叉搜索树。
/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode() {}
 *     TreeNode(int val) { this.val = val; }
 *     TreeNode(int val, TreeNode left, TreeNode right) {
 *         this.val = val;
 *         this.left = left;
 *         this.right = right;
 *     }
 * }
 */
class Solution {
    private long last=Integer.MAX_VALUE+1L;
    private boolean res=true;
    public boolean isValidBST(TreeNode root) {
        inorder(root);
        return res;
    }
    void inorder(TreeNode root){
        if(root==null)return;
        if(!res)return;
        inorder(root.left);
        if(last==Integer.MAX_VALUE+1L){
            last=root.val;
        }else{
            if(root.val<=last){
                res=false;
            }else{
                last=root.val;
            }
        }
        inorder(root.right);
    }
}

101,对称二叉树

难度简单1809

给你一个二叉树的根节点 root , 检查它是否轴对称。

class Solution {
    private boolean res=true;
    public boolean isSymmetric(TreeNode root) {
        // 根 左右  根 右左
        dfs(root,root);
        return res;
    }
    void dfs(TreeNode root1,TreeNode root2){
        if(root1==null&&root2==null)return;
        if(!res)return;
        if((root1==null||root2==null)||(root1.val!=root2.val)){
            res=false;
            return;
        }
        dfs(root1.left,root2.right);
        dfs(root1.right,root2.left);
    }
}

102,二叉树的层次遍历(队列)

难度中等1229

给你二叉树的根节点 root ,返回其节点值的 层序遍历 。 (即逐层地,从左到右访问所有节点)。

/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode() {}
 *     TreeNode(int val) { this.val = val; }
 *     TreeNode(int val, TreeNode left, TreeNode right) {
 *         this.val = val;
 *         this.left = left;
 *         this.right = right;
 *     }
 * }
 */
class Solution {
    private List<List<Integer>> res=new ArrayList<>();
    public List<List<Integer>> levelOrder(TreeNode root) {
        if(root==null)return res;
        LinkedList<TreeNode> q=new LinkedList<>();
        q.addLast(root);
        int cv=0;
        int clen=1;
        int next=0;
        ArrayList<Integer> list=new ArrayList<>();
        while(!q.isEmpty()){
            TreeNode t= q.getFirst();
            q.removeFirst();
            cv++;

            list.add(t.val);
        
            if(t.left!=null){
                next++;
                q.addLast(t.left);
            }
            if(t.right!=null){
                next++;
                q.addLast(t.right);
            }

            if(cv==clen){
                cv=0;
                clen=next;
                next=0;
                res.add(new ArrayList<>(list));
                list.clear();
            }
        }
        return res;
    }
}

104,二叉树的最大深度

难度简单1152

给定一个二叉树,找出其最大深度。

二叉树的深度为根节点到最远叶子节点的最长路径上的节点数。

说明: 叶子节点是指没有子节点的节点。

class Solution {
    private int res=0;
    public int maxDepth(TreeNode root) {
        dfs(root,0);
        return res;
    }
    void dfs(TreeNode root,int cv){
        if(root==null){
            res=Integer.max(res,cv);
            return;
        }
        dfs(root.left,cv+1);
        dfs(root.right,cv+1);
    }
}

105,从前序与中序遍历构造二叉树

难度中等1489

给定两个整数数组 preorder 和 inorder ,其中 preorder 是二叉树的先序遍历, inorder 是同一棵树的中序遍历,请构造二叉树并返回其根节点。

/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode() {}
 *     TreeNode(int val) { this.val = val; }
 *     TreeNode(int val, TreeNode left, TreeNode right) {
 *         this.val = val;
 *         this.left = left;
 *         this.right = right;
 *     }
 * }
 */
class Solution {
    public TreeNode buildTree(int[] preorder, int[] inorder) {
          HashMap<Integer,Integer> map=new HashMap<>();
          for(int i=0;i<inorder.length;i++){
              map.put(inorder[i],i);
          }  
          return creatTree(0,preorder.length-1,0,inorder.length-1,preorder,inorder,map);
    }
    TreeNode creatTree(int preorder_l,int preorder_r,int inorder_l,int inorder_r,int[] preorder,int inorder[], HashMap<Integer,Integer> map)
    {
        if(inorder_l>inorder_r)return null;
        //根
        TreeNode root=new TreeNode(preorder[preorder_l]);

        // 获取中序遍历中根的下标
        int root_index=map.get(preorder[preorder_l]);

        // 左子树的长度
        int left_len=root_index-inorder_l;

        root.left=creatTree(preorder_l+1,preorder_l+left_len,inorder_l,root_index-1,preorder,inorder, map);
        root.right=creatTree(preorder_l+left_len+1,preorder_r,root_index+1,inorder_r,preorder,inorder, map);

        return root;
    }
}

114,二叉树展开为链表

难度中等1104

给你二叉树的根结点 root ,请你将它展开为一个单链表:

  • 展开后的单链表应该同样使用 TreeNode ,其中 right 子指针指向链表中下一个结点,而左子指针始终为 null 。
  • 展开后的单链表应该与二叉树 先序遍历 顺序相同。
class Solution {
    ArrayList<TreeNode> temp=new ArrayList<>();
    public void flatten(TreeNode root) {
        if(root==null)return;
        preorder(root);
        TreeNode tail=temp.get(0);
        for(int i=1;i<temp.size();i++){
            tail.left=null;
            tail.right=temp.get(i);
            tail=temp.get(i);
        }
        tail.left=null;
        tail.right=null;
    }
    void preorder(TreeNode root){
        if(root==null)return;

        temp.add(root);

        preorder(root.left);

        preorder(root.right);
    }
}

121,买卖股票的最佳时机(单调栈)

难度简单2208

给定一个数组 prices ,它的第 i 个元素 prices[i] 表示一支给定股票第 i 天的价格。

你只能选择 某一天 买入这只股票,并选择在 未来的某一个不同的日子 卖出该股票。设计一个算法来计算你所能获取的最大利润。

返回你可以从这笔交易中获取的最大利润。如果你不能获取任何利润,返回 0 。

思路:

  1. 单调栈
class Solution {
    public int maxProfit(int[] prices) {
        Stack<Integer> s=new Stack<>();
        int min=10001;
        int res=0;
        for(int i:prices){
            if(s.isEmpty()){
                s.push(i);
                min=i;
            }else{
                while(!s.isEmpty()&&s.peek()>=i){
                    s.pop();
                }
                s.push(i);
                // 更新min
                if(s.size()==1){
                    min=i;
                }
            }
            res=Integer.max(res,i-min);
        }
        return res;
    }
}

128,最长连续序列

难度中等1163

给定一个未排序的整数数组 nums ,找出数字连续的最长序列(不要求序列元素在原数组中连续)的长度。

请你设计并实现时间复杂度为 O(n) 的算法解决此问题。

// 考虑时间复杂度为o(n),不考虑空间复杂度
class Solution {
    public int longestConsecutive(int[] nums) {
        int len=nums.length;
        if(len==0)return 0;
        HashSet<Integer> myset=new HashSet<>();
        for(int i=0;i<len;i++)
            myset.add(nums[i]);
        int res=1;
        for(Integer i:myset)
        {
            if(!myset.contains(i-1)) {
                int tep = i;
                int cv = 0;
                while (myset.contains(tep)) {
                    tep++;
                    cv++;
                }
                res = Integer.max(res, cv);
            }
        }
        return res;
    }
}
class Solution {
    public int longestConsecutive(int[] nums) {
        Arrays.sort(nums);
        int len=0;
        int last=0;
        int res=0;
        for(int i:nums){
            if(len==0){
                len++;
                last=i;
            }else{
                if(last+1==i){
                    len++;
                }else if(last==i){
                    
                }else{
                    len=1;
                }
                last=i;
            }
            res=Integer.max(res,len);
        }
        return res;
    }
}

136,只出现一次的数字(二进制)

难度简单2315

给定一个非空整数数组,除了某个元素只出现一次以外,其余每个元素均出现两次。找出那个只出现了一次的元素。

说明:

你的算法应该具有线性时间复杂度。 你可以不使用额外空间来实现

class Solution {
    public int singleNumber(int[] nums) {
        int res=0;
        for(int i:nums)res^=i;
        return res;
    }
}

139,单词拆分(DP)

难度中等1485

给你一个字符串 s 和一个字符串列表 wordDict 作为字典。请你判断是否可以利用字典中出现的单词拼接出 s 。

注意:不要求字典中出现的单词全部都使用,并且字典中的单词可以重复使用。

提示:

1 <= s.length <= 300
1 <= wordDict.length <= 1000
1 <= wordDict[i].length <= 20
s 和 wordDict[i] 仅有小写英文字母组

 思想:

  1. 采用dfs思想,在配合String.startsWith()进行判断,结果部分用例超时。
  2. 采用dp的思想去解决问题,因为s.length()不算太长,dp[i]表示长度为i的子串(下标为0-i-1)可以被表示出来,dp[i]=dp[j]&&s.substring(j,i)是否为能表示出来。
class Solution {
    public boolean wordBreak(String s, List<String> wordDict) {
        Set<String> wordDictSet = new HashSet(wordDict);
        boolean[] dp = new boolean[s.length() + 1];
        // dp[i]表示以0-i-1的字符串能被字典表示
        dp[0] = true;
        for (int i = 1; i <= s.length(); i++) {// 长度
            for (int j = 0; j < i; j++) {
                if (dp[j] && wordDictSet.contains(s.substring(j, i))) {
                    dp[i] = true;
                    break;
                }
            }
        }
        return dp[s.length()];
    }
}

141,环形链表

难度简单1414

给你一个链表的头节点 head ,判断链表中是否有环。

如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。注意:pos 不作为参数进行传递 。仅仅是为了标识链表的实际情况。

如果链表中存在环 ,则返回 true 。 否则,返回 false 。

public class Solution {
    public boolean hasCycle(ListNode head) {
        if(head==null)return false;
        ListNode p1=head;
        ListNode p2=head;
        while(p2!=null&&p1!=null){
            

            p1=p1.next;
            if(p1==null)return false;
            p1=p1.next;

            p2=p2.next;

            if(p1==p2)return true;
        }
        return false;
    }
}

142,环形链表2

难度中等1464

给定一个链表的头节点  head ,返回链表开始入环的第一个节点。 如果链表无环,则返回 null

如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。如果 pos 是 -1,则在该链表中没有环。注意:pos 不作为参数进行传递,仅仅是为了标识链表的实际情况。

不允许修改 链表。

 思路:

  1. set记录节点。
  2. 判圈法:女生走一步,男生走两步,直到相遇,在男生从初始点和女生从相遇点一步一步走就能找到环的入口。
public class Solution {
    public ListNode detectCycle(ListNode head) {
       if(head==null)return null;
       ListNode p1=head,p2=head;

       // 相遇点,一个走一步,一个走两步
        while(p1!=null&&p2!=null){
            p1=p1.next;
            if(p1==null)return null;
            p1=p1.next;

            p2=p2.next;

            if(p1==p2){
                break;
            }
        }
        // 一个为空代表木环
        if(p1==null||p2==null)return null;

        // 从头结点和相遇点一步一步走会在此相遇
        while(head!=p1){
            head=head.next;
            p1=p1.next;
        }
        return head;

    }
}
public class Solution {
    public ListNode detectCycle(ListNode head) {
        HashSet<ListNode> set=new HashSet<>();
        while(head!=null){
            if(!set.contains(head)){
                set.add(head);
            }else return head;
            head=head.next;
        }
        return null;
    }
}

146,LRU缓存(linkedHashMap)

难度中等2031

请你设计并实现一个满足  LRU (最近最少使用) 缓存 约束的数据结构。

实现 LRUCache 类:

  • LRUCache(int capacity) 以 正整数 作为容量 capacity 初始化 LRU 缓存
  • int get(int key) 如果关键字 key 存在于缓存中,则返回关键字的值,否则返回 -1 。
  • void put(int key, int value) 如果关键字 key 已经存在,则变更其数据值 value ;如果不存在,则向缓存中插入该组 key-value 。如果插入操作导致关键字数量超过 capacity ,则应该 逐出 最久未使用的关键字。

函数 get 和 put 必须以 O(1) 的平均时间复杂度运行。

思想:

LinkedHashMap中存在removeEldestEntry()方法就能自动开启LRU缓存功能,要写LinkedHashMap子类重写removeEldestEntry方法。

public LinkedHashMap(int initialCapacity,
                     float loadFactor,
                     boolean accessOrder) 

// 构造方法,accessOrder表示读取方式,false表示链表为插入顺序,true表示链表保持读取顺序。

class LRUCache extends LinkedHashMap<Integer, Integer>{
    private int capacity;
    
    public LRUCache(int capacity) {
        super(capacity, 0.75F, true);
        this.capacity = capacity;
    }

    public int get(int key) {
        return super.getOrDefault(key, -1);
    }

    public void put(int key, int value) {
        super.put(key, value);
    }

    @Override
    protected boolean removeEldestEntry(Map.Entry<Integer, Integer> eldest) {
        return size() > capacity; 
    }
}

148,排序链表(排序)

难度中等1522

给你链表的头结点 head ,请将其按 升序 排列并返回 排序后的链表 。

class Solution {
    public ListNode sortList(ListNode head) {
        if(head==null||head.next==null)return head;
        ArrayList<ListNode> c=new ArrayList<>();
        while(head!=null){
            c.add(head);
            head=head.next;
        }
        Collections.sort(c,new Comparator<ListNode>(){
            public int compare(ListNode o1,ListNode o2){
                return o1.val-o2.val;
            }
        });
        ListNode res=new ListNode(-1),tail;
        res.next=null;
        tail=res;
        for(int i=0;i<c.size();i++){
            ListNode temp= c.get(i);
            temp.next=null;
            tail.next=temp;
            tail=temp;
        }
        return res.next;
    }
}

152,乘积最大子数组(DP)

难度中等1562

给你一个整数数组 nums ,请你找出数组中乘积最大的非空连续子数组(该子数组中至少包含一个数字),并返回该子数组所对应的乘积。

测试用例的答案是一个 32-位 整数。

子数组 是数组的连续子序列。

class Solution {
    public int maxProduct(int[] nums) {
        int dpmin[]=new int[nums.length];
        int dpmax[]=new int[nums.length];
        dpmax[0]=nums[0];
        dpmin[0]=nums[0];
        int res=nums[0];
        for(int i=1;i<nums.length;i++){
            dpmax[i]=Integer.max(dpmax[i-1]*nums[i],dpmin[i-1]*nums[i]);
            dpmax[i]=Integer.max(dpmax[i],nums[i]);

            dpmin[i]=Integer.min(dpmax[i-1]*nums[i],dpmin[i-1]*nums[i]);
            dpmin[i]=Integer.min(dpmin[i],nums[i]);

            res=Integer.max(res,dpmax[i]);
        }
        return res;
    }
}

 155,最小栈(priorityQueue)

难度简单1233

设计一个支持 push ,pop ,top 操作,并能在常数时间内检索到最小元素的栈。

实现 MinStack 类:

  • MinStack() 初始化堆栈对象。
  • void push(int val) 将元素val推入堆栈。
  • void pop() 删除堆栈顶部的元素。
  • int top() 获取堆栈顶部的元素。
  • int getMin() 获取堆栈中的最小元素。
class MinStack {

    Stack<Integer> s=new Stack<>();
    PriorityQueue<Integer> que=new PriorityQueue<>();

    public MinStack() {

    }
    
    public void push(int val) {
        que.add(val);
        s.push(val);
    }
    
    public void pop() {
        que.remove(s.peek());
        s.pop();
    }
    
    public int top() {
        return s.peek();
    }
    
    public int getMin() {
        return que.peek();
    }
}

/**
 * Your MinStack object will be instantiated and called as such:
 * MinStack obj = new MinStack();
 * obj.push(val);
 * obj.pop();
 * int param_3 = obj.top();
 * int param_4 = obj.getMin();
 */

160,相交链表

难度简单1611

给你两个单链表的头节点 headA 和 headB ,请你找出并返回两个单链表相交的起始节点。如果两个链表不存在相交节点,返回 null 。

图示两个链表在节点 c1 开始相交

public class Solution {
    public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
        int len1=0,len2=0;
        ListNode h1=headA,h2=headB;
        while(h1!=null){len1++;h1=h1.next;}
        while(h2!=null){h2=h2.next;len2++;}

        if(len1<len2){
           int flag=len2-len1;
            while(flag!=0){
                headB=headB.next;
                flag--;
            }
        }else{
             int flag=len1-len2;
            while(flag!=0){
                headA=headA.next;
                flag--;
            }
        }
        
        while(headA!=null&&headB!=null){
            if(headB==headA){
                return  headB;
            }
            headB=headB.next;
            headA=headA.next;
        }
        
        return null;
    }
}

 169,多数元素

难度简单1365

给定一个大小为 的数组,找到其中的多数元素。多数元素是指在数组中出现次数 大于 ⌊ n/2 ⌋ 的元素。

你可以假设数组是非空的,并且给定的数组总是存在多数元素。

class Solution {
    public int majorityElement(int[] nums) {
        int val=0;
        int len=0;
        for(int i:nums){
            if(len==0){
                len=1;
                val=i;
            }else{
                if(val==i){
                    len++;
                }else{
                    len--;
                }
            }
        }
        return val;
    }
}

198,打家劫舍(DP)

难度中等1983

你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警

给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。

class Solution {
    public int rob(int[] nums) {
        int n=nums.length;
        int [][]dp=new int[n][2];
        dp[0][0]=0;//不偷
        dp[0][1]=nums[0];//偷
        for(int i=1;i<n;i++){
            dp[i][0]=Integer.max(dp[i-1][1],dp[i-1][0]);
            dp[i][1]=Integer.max(dp[i-1][1],dp[i-1][0]+nums[i]);
        }
        return Integer.max(dp[n-1][0],dp[n-1][1]);
    }
}

 200,岛屿数量(DFS)

难度中等1611

给你一个由 '1'(陆地)和 '0'(水)组成的的二维网格,请你计算网格中岛屿的数量。

岛屿总是被水包围,并且每座岛屿只能由水平方向和/或竖直方向上相邻的陆地连接形成。

此外,你可以假设该网格的四条边均被水包围。

class Solution {
    private int res=0;
    public int numIslands(char[][] grid) {
        int m=grid.length, n=grid[0].length;
        boolean [][]visited=new boolean[m][n];
        for(int i=0;i<m;i++){
            for(int j=0;j<n;j++){
                if(grid[i][j]=='1'&&!visited[i][j]){
                    res++;
                    visited[i][j]=true;
                    dfs(i,j,m,n,visited,grid);
                }
            }
        }
        return res;
    }
    void dfs(int x,int y,int m,int n,boolean [][]visited,char[][] grid){
        for(int i=0;i<4;i++){
            switch(i){
                case 0: 
                    if(x-1>=0&&grid[x-1][y]=='1'&&!visited[x-1][y]){
                        visited[x-1][y]=true;
                        dfs(x-1,y,m,n,visited,grid);
                    }
                break;
                case 1: 
                    if(x+1<m&&grid[x+1][y]=='1'&&!visited[x+1][y]){
                        visited[x+1][y]=true;
                        dfs(x+1,y,m,n,visited,grid);
                    }
                break;
                case 2:
                    if(y-1>=0&&grid[x][y-1]=='1'&&!visited[x][y-1]){
                        visited[x][y-1]=true;
                        dfs(x,y-1,m,n,visited,grid);
                    }
                break;
                 case 3:
                    if(y+1<n&&grid[x][y+1]=='1'&&!visited[x][y+1]){
                        visited[x][y+1]=true;
                        dfs(x,y+1,m,n,visited,grid);
                    }
                break;
            }
        }
    }
}

 206,反转链表

难度简单2379

给你单链表的头节点 head ,请你反转链表,并返回反转后的链表。

class Solution {
    public ListNode reverseList(ListNode head) {
        ListNode res=new ListNode(-1);
        res.next=null;
        while(head!=null){
            ListNode t=head;
            head=head.next;
            t.next=res.next;
            res.next=t;
        }
        return res.next;
    }
}

207,课程表(拓扑排序)

难度中等1177

你这个学期必须选修 numCourses 门课程,记为 0 到 numCourses - 1 。

在选修某些课程之前需要一些先修课程。 先修课程按数组 prerequisites 给出,其中 prerequisites[i] = [ai, bi] ,表示如果要学习课程 ai 则 必须 先学习课程  bi 。

  • 例如,先修课程对 [0, 1] 表示:想要学习课程 0 ,你需要先完成课程 1 。

请你判断是否可能完成所有课程的学习?如果可以,返回 true ;否则,返回 false 。

思想:

  1. 拓扑排序,任务有先后,求出一种没有冲突的任务执行方式,将入度为0的结点加入队列,每次出队为key,将其余结点的入度为key的进行删除并且当一节点入度为0时候进行入队,当出队数等于总结点数则表明可以进行拓扑排序。
class Solution {
    List<List<Integer>> edges;
    int[] indeg;

    public boolean canFinish(int numCourses, int[][] prerequisites) {
        edges = new ArrayList<List<Integer>>();
        for (int i = 0; i < numCourses; ++i) {
            edges.add(new ArrayList<Integer>());
        }
        indeg = new int[numCourses];
        for (int[] info : prerequisites) {
            edges.get(info[1]).add(info[0]);
            ++indeg[info[0]];
        }

        Queue<Integer> queue = new LinkedList<Integer>();
        for (int i = 0; i < numCourses; ++i) {
            if (indeg[i] == 0) {
                queue.offer(i);
            }
        }

        int visited = 0;
        while (!queue.isEmpty()) {
            ++visited;
            int u = queue.poll();
            for (int v: edges.get(u)) {
                --indeg[v];
                if (indeg[v] == 0) {
                    queue.offer(v);
                }
            }
        }

        return visited == numCourses;
    }
}

208,实现trie前缀树

难度中等1115

Trie(发音类似 "try")或者说 前缀树 是一种树形数据结构,用于高效地存储和检索字符串数据集中的键。这一数据结构有相当多的应用情景,例如自动补完和拼写检查。

请你实现 Trie 类:

  • Trie() 初始化前缀树对象。
  • void insert(String word) 向前缀树中插入字符串 word 。
  • boolean search(String word) 如果字符串 word 在前缀树中,返回 true(即,在检索之前已经插入);否则,返回 false 。
  • boolean startsWith(String prefix) 如果之前已经插入的字符串 word 的前缀之一为 prefix ,返回 true ;否则,返回 false 。
class Trie {

    private HashSet<String> set=new HashSet<>();
    public Trie() {

    }
    
    public void insert(String word) {
        set.add(word);
    }
    
    public boolean search(String word) {
        return set.contains(word);
    }
    
    public boolean startsWith(String prefix) {
        for(String s:set){
            if(s.startsWith(prefix)){
                return true;
            }
        }
        return false;
    }
}

215,数组中的第k个最大元素

难度中等1546

给定整数数组 nums 和整数 k,请返回数组中第 k 个最大的元素。

请注意,你需要找的是数组排序后的第 k 个最大的元素,而不是第 k 个不同的元素。

class Solution {
    PriorityQueue<Integer> q=new PriorityQueue<>();
    public int findKthLargest(int[] nums, int k) {
        for(int i:nums){
            add(i,k);
        }
        return q.peek();
    }
    void add(int val,int k){
        q.add(val);
        if(q.size()>k){
            q.poll();
        }
    }
}

221,最大正方形

难度中等1081

在一个由 '0' 和 '1' 组成的二维矩阵内,找到只包含 '1' 的最大正方形,并返回其面积。

示例 1:

class Solution {
    public int maximalSquare(char[][] matrix) {
        int m=matrix.length,n=matrix[0].length;
        int [][]dp=new int[m][n];
        int res=0;
        for(int i=0;i<m;i++){
            dp[i][0]=matrix[i][0]-'0';
            res=Integer.max(dp[i][0],res);
        }
        for(int i=0;i<n;i++)
        {
            dp[0][i]=matrix[0][i]-'0';
            res=Integer.max(dp[0][i],res);
        }
        for(int i=1;i<m;i++){
            for(int j=1;j<n;j++){
                if(matrix[i][j]=='1'){
                    dp[i][j]=Integer.min(Integer.min(dp[i-1][j],dp[i][j-1]),dp[i-1][j-1])+1;
                    res=Integer.max(dp[i][j],res);
                }
            }
        }
        return res*res;
    }
}

226,翻转二叉树

难度简单1208

给你一棵二叉树的根节点 root ,翻转这棵二叉树,并返回其根节点。

class Solution {
    public TreeNode invertTree(TreeNode root) {
        dfs(root);
        return root;
    }
    void dfs(TreeNode root){
        if(root==null)return;

        TreeNode t=root.left;
        root.left=root.right;
        root.right=t;

        dfs(root.left);
        dfs(root.right);
    }
}

234,回文链表

难度简单1309

给你一个单链表的头节点 head ,请你判断该链表是否为回文链表。如果是,返回 true ;否则,返回 false 。

示例 1:

class Solution {
    public boolean isPalindrome(ListNode head) {
        StringBuffer a=new StringBuffer();
        while(head!=null){
            a.append(String.valueOf(head.val));
            head=head.next;
        }
        if(a.length()==1)return true;
        String c=a.substring(0,a.length()/2);
        String b=a.reverse().substring(0,a.length()/2);
        
        if(b.equals(c))return true;
        return false;
    }
}

236,二叉树的最近公共祖先

难度中等1633

给定一个二叉树, 找到该树中两个指定节点的最近公共祖先。

百度百科中最近公共祖先的定义为:“对于有根树 T 的两个节点 p、q,最近公共祖先表示为一个节点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)。”

class Solution {
    private TreeNode res=null;
    public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
        dfs(root,p,q);
        return res;
    }

    boolean dfs(TreeNode root,TreeNode p,TreeNode q){
        if(root==null)return false;
        boolean l=dfs(root.left,p,q);
        boolean r=dfs(root.right,p,q);
        if((l&&r)||((r||l)&&(root==p||root==q)))
        {
            res=root;
            return true;
        }
        return l||r||(root==p)||(root==q);
    }
}

238,除自身以外组的乘积(两边记性标志-类似于接雨水)

难度中等1097

给你一个整数数组 nums,返回 数组 answer ,其中 answer[i] 等于 nums 中除 nums[i] 之外其余各元素的乘积 。

题目数据 保证 数组 nums之中任意元素的全部前缀元素和后缀的乘积都在  32 位 整数范围内。

不要使用除法,且在 O(n) 时间复杂度内完成此题。

思路:

  1. 双向标记数组。
class Solution {
    public int[] productExceptSelf(int[] nums) {
        int []f=new int[nums.length];
        int []l=new int[nums.length];

        int cv=1;
        for(int i=0;i<nums.length;i++){
            cv*=nums[i];
            f[i]=cv;
        }
         cv=1;
        for(int i=nums.length-1;i>=0;i--){
            cv*=nums[i];
            l[i]=cv;
        }
        int res[]=new int[nums.length];
        for(int i=0;i<nums.length;i++){
            if(i==0){
                res[i]=l[1];
            }else if(i==nums.length-1){
                res[i]=f[nums.length-2];
            }else{
                 res[i]=l[i+1]*f[i-1];
            }
        }
        return res;
    }
}

240,搜索二维矩阵

难度中等962

编写一个高效的算法来搜索 m x n 矩阵 matrix 中的一个目标值 target 。该矩阵具有以下特性:

  • 每行的元素从左到右升序排列。
  • 每列的元素从上到下升序排列。
class Solution {
    public boolean searchMatrix(int[][] matrix, int target) {
        for(int i=0;i<matrix.length;i++){
            if(Arrays.binarySearch(matrix[i],target)>=0){
                return true;
            }
        }
        return false;
    }
}

279,完全平方数(DP)

难度中等1289

给你一个整数 n ,返回 和为 n 的完全平方数的最少数量 。

完全平方数 是一个整数,其值等于另一个整数的平方;换句话说,其值等于一个整数自乘的积。例如,149 和 16 都是完全平方数,而 3 和 11 不是。

思路:

  1. 局部性原路。
class Solution {
    public int numSquares(int n) {
       int []dp=new int[n+1];
       // dp[i]表示数i需要表示为完全平方数之和的最小步数
       Arrays.fill(dp,n+1);
       dp[0]=0;
       for(int i=1;i<=n;i++)
       {
           for(int j=1;j*j<=i;j++)
           {
               dp[i]=Math.min(dp[i],dp[i-j*j]+1);
           }
       }
       return dp[n];
    }

}

283,移动零

难度简单1501

给定一个数组 nums,编写一个函数将所有 0 移动到数组的末尾,同时保持非零元素的相对顺序。

请注意 ,必须在不复制数组的情况下原地对数组进行操作。

class Solution {
    public void moveZeroes(int[] nums) {
       int zero=0;
       ArrayList<Integer> arr=new ArrayList<>();
       for(int i:nums){
           if(i==0){
               zero++;
           }else{
               arr.add(i);
           }
       }
       int index=0;
       for(;index<arr.size();index++){
           nums[index]=arr.get(index);
       }
       for(int i=index;i<nums.length;i++){
           nums[i]=0;
       }
    }
}

287,寻找重复数

难度中等1641

给定一个包含 n + 1 个整数的数组 nums ,其数字都在 [1, n] 范围内(包括 1 和 n),可知至少存在一个重复的整数。

假设 nums 只有 一个重复的整数 ,返回 这个重复的数 。

你设计的解决方案必须 不修改 数组 nums 且只用常量级 O(1) 的额外空间。

思路:

  1. 因为数堵在[1,n]之间,我可以对nums[nums[i]%n]加n,遍历完一遍只要nums[i]>2*n的证明至少有两个相同的数存在了。
class Solution {
    public int findDuplicate(int[] nums) {
        int n=nums.length-1;
       for(int i=0;i<n+1;i++){
           nums[nums[i]%n]=nums[nums[i]%n]+n;
       }
       for(int i=0;i<n+1;i++){
           if(nums[i]>2*n){
               return i==0?n:i;
           }
       }
       return 1;
    }
}

300,最长递增子序列(DP)

难度中等2341

给你一个整数数组 nums ,找到其中最长严格递增子序列的长度。

子序列 是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7] 是数组 [0,3,1,6,2,2,7] 的子序列。

class Solution {
    public int lengthOfLIS(int[] nums) {
        int n=nums.length;
        int []dp=new int[n];
        Arrays.fill(dp,1);
        int res=1;
        for(int i=1;i<n;i++){
            for(int j=i-1;j>=0;j--){
                if(nums[i]>nums[j]){
                    dp[i]=Integer.max(dp[i],dp[j]+1);
                }
            }
            res=Integer.max(res,dp[i]);
        }
        return res;
    }
}

301,删除无效的括号(DFS+剪枝)

难度困难689

给你一个由若干括号和字母组成的字符串 s ,删除最小数量的无效括号,使得输入的字符串有效。

返回所有可能的结果。答案可以按 任意顺序 返回。

class Solution {
    HashSet<String> res=new HashSet<>();
    int step_MIN=Integer.MAX_VALUE;
    public List<String> removeInvalidParentheses(String s) {
       
        dfs(0,0,0,0,"",s);
        return new ArrayList<>(res);

    }
    void dfs(int l,int r,int index,int step,String cv,String s){
        if(index==s.length())
        {
            if(l==r){
                if(step<step_MIN){
                    step_MIN=step;
                    res.clear();
                    res.add(new String(cv));
                }else if(step==step_MIN){
                     res.add(new String(cv));
                }
            }
            return;
        }
        char flag=s.charAt(index);
        if(flag=='('){
            //可要可不要
            dfs(l+1,r,index+1,step,cv+s.charAt(index),s); 
            dfs(l,r,index+1,step+1,cv,s); 
        }else if(flag==')'){
            if(r+1<=l){
                //可要可不要
                 dfs(l,r+1,index+1,step,cv+s.charAt(index),s);   
            }
            dfs(l,r,index+1,step+1,cv,s); 
        }else{
            dfs(l,r,index+1,step,cv+s.charAt(index),s);
        }
    }
}

309,最佳买卖股票时机含冷冻期(DP问题)

难度中等1130

给定一个整数数组prices,其中第  prices[i] 表示第 i 天的股票价格 。​

设计一个算法计算出最大利润。在满足以下约束条件下,你可以尽可能地完成更多的交易(多次买卖一支股票):

  • 卖出股票后,你无法在第二天买入股票 (即冷冻期为 1 天)。

注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)

思路:

本题的难点在于处理冷冻期,冷冻期是表示当天卖出后一天则不可以买入,关注点不是下一天是否可以买入,而是关注什么是否卖出(必须表示出来)。

一天有两种状态,持股和不持股(不持股也有两种状态,当天卖出不持股,当天没有股票不持股)dp[i][0] :不持股(当天不卖出),dp[i][1]:持股, dp[i][2]:不持股(当天卖出)

转移方程:

dp[i][0]=max(dp[i-1][0],dp[i-1][2])

dp[i][1]=max(dp[i-1][1],dp[i-1][0]-price[i])// 这里考虑到如果i-1卖出的话就不考虑,考虑了冷冻期

dp[i][2]=dp[i-1][1]+price[i]

class Solution {
    public int maxProfit(int[] prices) {
        int n=prices.length;
        if(n<=1) return 0;
        int [][] dp=new int[n][3];
        dp[0][0]=0;
        dp[0][1]=-1*prices[0];
        dp[0][2]=0;
        for(int i=1;i<n;i++){//从[1]...[n-1]
          dp[i][0]=Math.max(dp[i-1][0],dp[i-1][2]);
            dp[i][1]=Math.max(dp[i-1][1],dp[i-1][0]-prices[i]);
            dp[i][2]=dp[i-1][1]+prices[i];

        }
        return Math.max(dp[n-1][0],dp[n-1][2]);
    }
}

322,零钱兑换(DP-和单词拆分有着相同的思想)

难度中等1810

给你一个整数数组 coins ,表示不同面额的硬币;以及一个整数 amount ,表示总金额。

计算并返回可以凑成总金额所需的 最少的硬币个数 。如果没有任何一种硬币组合能组成总金额,返回 -1 。

你可以认为每种硬币的数量是无限的。

class Solution {
    public int coinChange(int[] coins, int amount) {
        int []dp=new int[amount+1];
        Arrays.sort(coins);
        Arrays.fill(dp,Integer.MAX_VALUE);
        dp[0]=0;
        for(int i=1;i<=amount;i++){
            int cmin=Integer.MAX_VALUE;
            for(int j=0;j<coins.length;j++){
                if(coins[j]>i){
                    break;
                }
                if(dp[i-coins[j]]!=Integer.MAX_VALUE){
                    cmin=Integer.min(cmin,dp[i-coins[j]]+1);
                }
            }
            dp[i]=cmin;
        }
        return dp[amount]==Integer.MAX_VALUE?-1:dp[amount];
    }
}

337,打家劫舍(DP+二叉树)

难度中等1204

小偷又发现了一个新的可行窃的地区。这个地区只有一个入口,我们称之为 root 。

除了 root 之外,每栋房子有且只有一个“父“房子与之相连。一番侦察之后,聪明的小偷意识到“这个地方的所有房屋的排列类似于一棵二叉树”。 如果 两个直接相连的房子在同一天晚上被打劫 ,房屋将自动报警。

给定二叉树的 root 。返回 在不触动警报的情况下 ,小偷能够盗取的最高金额 。

思路:

  1. 存在子问题结构,f表示该点选择了最大价值,g表示该点不选择后的最大价值。
class Solution {
    Map<TreeNode, Integer> f = new HashMap<TreeNode, Integer>();
    Map<TreeNode, Integer> g = new HashMap<TreeNode, Integer>();

    public int rob(TreeNode root) {
        dfs(root);
        return Math.max(f.getOrDefault(root, 0), g.getOrDefault(root, 0));
    }
    // 后序遍历的思想+DP思想
    public void dfs(TreeNode node) {
        if (node == null) {
            return;
        }
        dfs(node.left);
        dfs(node.right);
        f.put(node, node.val + g.getOrDefault(node.left, 0) + g.getOrDefault(node.right, 0));
        g.put(node, Math.max(f.getOrDefault(node.left, 0), g.getOrDefault(node.left, 0)) + Math.max(f.getOrDefault(node.right, 0), g.getOrDefault(node.right, 0)));
    }
}

338,比特位计数

难度简单939

给你一个整数 n ,对于 0 <= i <= n 中的每个 i ,计算其二进制表示中 1 的个数 ,返回一个长度为 n + 1 的数组 ans 作为答案。

class Solution {
    public int[] countBits(int n) {
        int res[]=new int[n+1];
        for(int i=0;i<32;i++){
            for(int j=1;j<=n;j++){
                if(((j>>i)&1)==1){
                    res[j]++;
                }
            }
        }
        return res;
    }
}

347,前k个高频元素

难度中等1077

给你一个整数数组 nums 和一个整数 k ,请你返回其中出现频率前 k 高的元素。你可以按 任意顺序 返回答案。

class Solution {
    public int[] topKFrequent(int[] nums, int k) {

        HashMap<Integer,Integer> map=new HashMap<>();
        for(int i:nums){
            Integer t= map.get(i);
            if(t==null){
                map.put(i,1);
            }else{
                map.put(i,t+1);
            }
        }
        int [][] val=new int[map.size()][2];
        int index=0;
        for(Map.Entry<Integer,Integer> entry:map.entrySet()){
            val[index][0]=entry.getKey();
            val[index][1]=entry.getValue();
            index++;
        }
        Arrays.sort(val, new Comparator<int[]>() {
            @Override
            public int compare(int[] o1, int[] o2) {
                return o2[1]-o1[1];
            }
        });
        int []res=new int[k];
        for(int i=0;i<k;i++){
            res[i]=val[i][0];
        }
        return res;
    }
}

394,字符串解码(栈)

难度中等1074

给定一个经过编码的字符串,返回它解码后的字符串。

编码规则为: k[encoded_string],表示其中方括号内部的 encoded_string 正好重复 k 次。注意 k 保证为正整数。

你可以认为输入字符串总是有效的;输入字符串中没有额外的空格,且输入的方括号总是符合格式要求的。

此外,你可以认为原始数据不包含数字,所有的数字只表示重复的次数 k ,例如不会出现像 3a 或 2[4] 的输入。

class Solution {
    public String decodeString(String s) {
        Stack<Integer> n=new Stack<>();
        Stack<Character> chars=new Stack<>();
        String res="";
        boolean last=false;
        for(int i=0;i<s.length();i++){
            if(s.charAt(i)>='0'&&s.charAt(i)<='9'){
                if(last){
                    int tt=n.peek();
                    n.pop();
                    n.push(tt*10+s.charAt(i)-'0');
                }else{
                    n.push(s.charAt(i)-'0');
                }
                last=true;
            }else if(s.charAt(i)==']'){
                last=false;
                // 进行出栈
                String t="";
                while(chars.peek()!='['){
                    t=chars.peek()+t;
                    chars.pop();
                }
                chars.pop();

                if(chars.isEmpty()){
                    //
                    int flag=n.peek();
                    n.pop();
                    for(int j=0;j<flag;j++){
                        res=res+t;
                    }
                }else{
                    int flag=n.peek();
                    n.pop();
                    String tep="";
                    for(int j=0;j<flag;j++){
                        tep=tep+t;
                    }
                    for(int j=0;j<tep.length();j++){
                        chars.push(tep.charAt(j));
                    }
                }

            }else{
                last=false;
                chars.push(s.charAt(i));
            }
        }
        String temp="";
        while(!chars.isEmpty()){
            temp=chars.peek()+temp;
            chars.pop();
        }
        return res+temp;
    }
}

437,路径总和三(先序遍历)

难度中等1276

给定一个二叉树的根节点 root ,和一个整数 targetSum ,求该二叉树里节点值之和等于 targetSum 的 路径 的数目。

路径 不需要从根节点开始,也不需要在叶子节点结束,但是路径方向必须是向下的(只能从父节点到子节点)。

class Solution {
    public int pathSum(TreeNode root, int targetSum) {
        if (root == null) {
            return 0;
        }

        int ret = rootSum(root, targetSum);
        ret += pathSum(root.left, targetSum);
        ret += pathSum(root.right, targetSum);
        return ret;
    }

    public int rootSum(TreeNode root, int targetSum) {
        int ret = 0;

        if (root == null) {
            return 0;
        }
        int val = root.val;
        if (val == targetSum) {
            ret++;
        } 

        ret += rootSum(root.left, targetSum - val);
        ret += rootSum(root.right, targetSum - val);
        return ret;
    }
}

438,找到字符串中所有字母异位词

难度中等820

给定两个字符串 s 和 p,找到 s 中所有 p 的 异位词 的子串,返回这些子串的起始索引。不考虑答案输出的顺序。

异位词 指由相同字母重排列形成的字符串(包括相同的字符串)。

class Solution {
    public List<Integer> findAnagrams(String s, String p) {
        int []flag=new int[26];
        for(int i=0;i<p.length();i++){
            flag[p.charAt(i)-'a']++;
        }
        int []t=new int[26];
        int len=p.length();
        List<Integer> res=new ArrayList<>();
        for(int i=0;i<s.length();i++){
            t[s.charAt(i)-'a']++;
            if(i>=len){
                 t[s.charAt(i-len)-'a']--;
            }
            if(Arrays.equals(t,flag)){
                res.add(i-len+1);
            }
        }
        // abcd
        return res;
    }
}

448,找到所有数组中消失的数字

难度简单937

给你一个含 n 个整数的数组 nums ,其中 nums[i] 在区间 [1, n] 内。请你找出所有在 [1, n] 范围内但没有出现在 nums 中的数字,并以数组的形式返回结果。

class Solution {
    public List<Integer> findDisappearedNumbers(int[] nums) {
        for(int i=0,len=nums.length;i<len;i++){
            nums[nums[i]%len]=nums[nums[i]%len]+len;
        }
        List<Integer> res=new ArrayList<>();
        for(int i=0,len=nums.length;i<len;i++){
            if(nums[i]<=len){
                if(i==0)
                res.add(len);
                else res.add(i);
            }
        }
        return res;
    }
}

461,汉明距离

难度简单578

两个整数之间的 汉明距离 指的是这两个数字对应二进制位不同的位置的数目。

给你两个整数 x 和 y,计算并返回它们之间的汉明距离。

class Solution {
    public int hammingDistance(int x, int y) {
        int res=0;
        for(int i=0;i<32;i++){
            if(((x>>i)&1^(y>>i)&1)==1){
                res++;
            }
        }
        return res;
    }
}

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

阿联爱学习

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值