剑指offer整理(待更新)
Note:根据 牛客网剑指offer 整理的算法题,这里仅收录一些有意思的题目。
- 算法在执行时是线性的过程,但是在思考时是非线性,是带分支的过程;
- 算法有好坏之分
- 时间复杂度低的算法好
- 算法归纳性强,能涵盖大多测试用例,且整体思路简单的算法好。
- 需要细分多种不同类型的测试用例,不停地
debug
写分支条件的算法不好。 - 在
Java
中,使用包装类集合的算法时间开销会比较大,即使计算的算法时间复杂度低,但是实际下拆包和装箱带来时间复杂度会比较高,在做题时,尽量减少包装类的拆包和装箱的操作。参考在二叉树中找到两个节点的最近公共祖先
一、数据结构
1、链表
- JZ6 从尾到头打印链表,参考题解
Note:- 注意链表的头指针不带空值
- 先截断第一个节点,再使用p,q指针反转链表
- JZ24 反转链表(答案同JZ6一模一样)
- JZ25 合并两个排序的链表,参考题解
Note:- 创建一个带头指针的新链表,每次选两链表最小值插入新链表尾部
- 分别采用三个指针对list1,list2和插入链表进行遍历。
- JZ52 两个链表的第一个公共结点(无环),参考题解
Note:- 计算两个链表长度,让长的链表先执行
len2 - len1
步,之后两链表同步遍历; - 输入分为是3段,注意后台会将这3个参数组装为两个链表,并将这两个链表对应的头节点传入到函数。
- 计算两个链表长度,让长的链表先执行
- JZ23 链表中环的入口结点(有环),参考题解
Note:- 设置快慢指针,两指针同时从头指针出发,慢指针步长为1,快指针步长为2,找到第一次相遇的节点(第一次相遇的节点并不一定是入口节点)
- 由于慢指针走了
X+Y
步,快指针走了2(X+Y)
步,而快指针在环路内重复走了两次Y
,因此可以设置两个指针,分别从第一次相遇的节点和头指针同步出发,再次相遇的节点为入口节点。 - 注意考虑链表无环的用例。
- JZ22 链表中倒数最后k个结点,参考题解
Note:- 如果要让时间复杂度为
O
(
n
)
O(n)
O(n)(求解链表长度方法为
O
(
2
n
)
O(2n)
O(2n)),则可以设置快慢指针,快指针先走
k
步,接着快慢指针同步,直到快指针走到链尾,输出慢指针即可(慢指针走了len-k
步)。
- 如果要让时间复杂度为
O
(
n
)
O(n)
O(n)(求解链表长度方法为
O
(
2
n
)
O(2n)
O(2n)),则可以设置快慢指针,快指针先走
2、树
- JZ55 二叉树的深度(递归实现分治思想),参考题解
- JZ77 按之字形顺序打印二叉树,参考题解
Note:- 使用队列依次存储第
i
层的节点; - 在插入第
i
层某节点的左右孩子节点之前,先将队列中的节点转移至栈中。 - 弹出栈顶节点,根据
height = i + 1
,决定是先插入左子树还是右子树。 - 遍历结束条件为队列为空。
- 使用队列依次存储第
- JZ54 二叉搜索树的第k个节点,参考题解
Note:- 使用递归或非递归(栈)对二叉搜索树进行中序遍历
- 建议不要把tree转化成list再排序
- JZ7 重建二叉树(前序找根节点,中序左右子树分治),参考题解
- JZ26 树的子结构,参考题解
Note:- 先从
A
树中找到B
子树根结点值相同的节点,再从A
该节点出发,同步判断B
是否为A
子树。
- 先从
- JZ27 二叉树的镜像(先将左子树和右子树分别变成镜像结构,再交换根节点的左右子树的位置,参考题解
- JZ32 从上往下打印二叉树(用队列
offer
和poll
逐层遍历),参考题解 - JZ33 二叉搜索树的后序遍历序列(分治思想),参考题解
Note:- 先从后序遍历序列中找到根节点
root
(都在区间末尾),接着找到该区间下分隔root
的左右子树的索引值mid
,接着对[left,mid]
和[mid + 1,right - 1]
进行分治。 - 需要考虑
root
只有左子树或者右子树的情况,如果只有右子树(比如[5,4,3,2,1]
),还需要重新遍历子序列[left, right - 1]
。 - 需要利用一些特殊的测试用例,通过
if-else
完善整个算法逻辑。
- 先从后序遍历序列中找到根节点
- JZ82 二叉树中和为某一值的路径(一),参考题解
Note:- 题目要求是判断是否有从根节点到叶子节点的节点值之和等于
sum
的路径,也就是说该路径的起点为根,终点为叶子节点; - 该问题是回溯问题,即到了叶子节点就回溯;不是分治,并没有要求当前节点具有 有选 和 不选 两种选择
- 节点为叶子节点的判断条件为:
root.left == null && root.right == null
。
- 题目要求是判断是否有从根节点到叶子节点的节点值之和等于
- JZ34 二叉树中和为某一值的路径(二),参考题解,Java List的remove()方法踩坑
Note:- 解题思路同 JZ82 二叉树中和为某一值的路径(一);在使用递归进行搜索时,要用
templist
暂存当前访问的节点。 - 到了叶子节点,用用
result_list
深拷贝templist
,再插入到返回集合中。
- 解题思路同 JZ82 二叉树中和为某一值的路径(一);在使用递归进行搜索时,要用
- JZ28 对称的二叉树(可以从根的左右子树出发,使用两个队列同步比较;也可以通过
root1
、root2
,使用递归分治,按不同遍历顺序同步比较两节点值),可参考题解 - JZ68 二叉搜索树的最近公共祖先,可参考题解
Note:- 一种方式是将
TreeNode
类型的二叉树转化成以ParentNode
类型的带双亲节点指针的二叉树,接着通过频数统计,最先遍历两次的节点即为p
,q
两节点的最近共同祖先。
也可以使用Map
实现双亲节点表示法。 - 另一种方式是寻找二叉树中两个节点的路径,接着分别从头开始遍历两条路径,当第一次出现两值相等时,则该值为
p
,q
两节点的最近共同祖先。
- 一种方式是将
- JZ86 在二叉树中找到两个节点的最近公共祖先,可参考题解
Note:- 可以采用递归实现分治思想,获取从根结点到每个节点
o1
和o2
的路径,接着从两路径中寻找第一个公共点,参考上面 寻找二叉搜索树最近公共祖先 的解决方法。 - 也可以采用递归实现分治思想,直接找两个节点的共同祖先:
- 从
root
作为二叉树的入口,先判断root.val
是否为o1
或o2
,如果是,则直接返回; - 如果不是,则
root
要求左右子树返回给它个结果 - 是否包含o1
,o2
节点。 - 接着
root
自行判断,这两个节点o1
和o2
是在都在root
的左子树上,还是右子树上,还是分别在两子树上。
- 从
- 可以采用递归实现分治思想,获取从根结点到每个节点
3、队列 & 栈
- JZ9 用两个栈实现队列(在
pop
时,只有当stack2
为空,才将stack1
中的元素全部压入stack2
中),参考题解 - JZ30 包含min函数的栈(维护两个栈:一个原始栈,一个最小值栈;注意在
pop
时维护min
值),参考题解 - JZ31 栈的压入、弹出序列,参考题解
Note:- 栈顶元素与当前
popA
元素不同,如果pushA
元素未全部压入,则继续压入pushA
元素; - 如果已全部压入
pushA
,但栈顶元素不等于popA
当前元素,则popA
不满足出栈要求。 - 注意考虑匹配过程中栈空的情况。
- 栈顶元素与当前
- JZ59 滑动窗口的最大值,可参考题解(4种方法,逐步优化)
Note:- 这道题先用暴力法2:每移动一次窗口,将加入的值和上一个窗口的最大值比较,如果大于等于则直接替换;在输出最大值时需要判断当前维护的最大值是否在该窗口内。
- 接着使用堆(优先队列) 来优化 窗口内重复计算带来的开销;
- 最后使用单调队列(双端队列) 来进一步优化 堆维护带来的开销;
二、算法
1、搜索算法
2、动态规划
3、回溯
4、排序
- JZ51 数组中的逆序对,可参考题解
Note:- 如果利用2层
for
暴力求解会超时。 - 可以利用归并排序计算元素的交换次数,即为逆序对数目,注意在归并时需要额外开辟一个新的子数组
new arr[right - left +1]
,用于复制原来子区间的数组origin[left,right]
- 接着利用双指针进行归并,并将结果直接写入原来
origin
中。
- 如果利用2层