前言:以前剑指offer单独刷题,但没按题型分类,总有一种线牵不起来的感觉。按数据结构类、具体算法类分类。
提示:剑指offer动态规划的题比较少,因此需要在Leetcode上专项训练
面试必刷-《剑指offer》刷题小结
牛客网《剑指offer》题解–Java版
003-JZ3-从尾到头打印链表
014-JZ14-链表中倒数第k个结点
015-JZ15-反转链表
注:使用结点前插方法或递归
016-JZ16-合并两个排序的链表
ps:到底是合并两个或k个有序链表.??这题能不能新建结点(不破坏原链表)?使用递归方法更好且不用创建新结点(在原链表上操作)
025-JZ25-复杂链表的复制
ps:两种思路,利用两种映射关系(复制一次结点)或者复制后分离(拆分时要注意后面是不是有结点)
036-JZ36-两个链表的第一个公共结点
注:找出最简写法
055-链表中环的入口结点
注:这题目还需要隔段时间想一下。两种方法,set或快慢指针
056-JZ56-删除链表中重复的结点
注:做出来了以前没想到的方法,使用hashMap去重,反而更简洁。以前采用的是两前后指针确定位置删除
import java.util.*;
public class Solution {
public ListNode deleteDuplication(ListNode pHead)
{
if(pHead == null)
return null;
ListNode p = pHead, node = null;
Map<Integer, ListNode> map = new LinkedHashMap<>();
while(p != null){
if(map.containsKey(p.val)){
map.put(p.val, null); // 重复就不考虑这个值
}else{
map.put(p.val, p);
}
p = p.next;
}
ListNode head = new ListNode(-1);
ListNode pre = head;
for(Integer i : map.keySet()){
node = map.get(i);
if(node != null){
node.next = null; // 这里需要先去除结点后一个的连接
pre.next = node;
pre = node;
}
}
return head.next;
}
}
004-JZ4-重建二叉树
注:Arrays.copyOfRange很有用
017-JZ17-树的子结构
注:这道题有难度,关键在于理解子结构就是两棵树部分重合,首先需要拿右树与左树的每棵子树比较,其次树的深度(可以帮助提前退出部分比较),最后部分重合判断
/**
public class TreeNode {
int val = 0;
TreeNode left = null;
TreeNode right = null;
public TreeNode(int val) {
this.val = val;
}
}
*/
public class Solution {
public boolean HasSubtree(TreeNode root1,TreeNode root2) {
if(root2 == null) // root2是空树则直接不是任意树的子结构
return false;
if(depth(root1) < depth(root2)) // 若root2是root1的子结构,那么树root1一定不能比root2低
return false;
if(partEquals(root1, root2))// 若root2是root1的子结构,那么树root2与root1的部分完全重合
return true;
return HasSubtree(root1.left, root2) || HasSubtree(root1.right, root2); // root2只要与root1的下一层去比较
}
public boolean partEquals(TreeNode root1, TreeNode root2){
if(root2 == null){
return true; // 若root2为空树,则完全重合,之前HasSubtree就排除了root2为空树的情况
}
if(root1 == null && root2 != null) // 若root2还存在,而root1就没了,那肯定无法重合
return false;
if(root1.val != root2.val){
return false;// 若两棵树的每一层存在一个结点不相等,则直接不重合
}
return partEquals(root1.left, root2.left) && partEquals(root1.right, root2.right);
}
public int depth(TreeNode root){
if(root == null)
return 0;
return Math.max(depth(root.left), depth(root.right)) + 1;
}
}
018-JZ18-二叉树的镜像
022-从上往下打印二叉树
注:在Java中Queue接口中的两类函数,ArrayDeque的使用
容量不够或队列为空时不会抛异常:
offer(添加队尾元素)、peek(访问队头元素)、poll(访问队头元素并移除)
容量不够或队列为空时抛异常:
add、element(访问队列元素)、remove(访问队头元素并移除)
023-二叉搜索树的后序遍历序列
注:首先数组为空不是后序遍历,但后面递归切分数组时会碰到切分的为空需要额外考虑
import java.util.*;
public class Solution {
public boolean VerifySquenceOfBST(int [] sequence) {
if(sequence == null || sequence.length == 0)
return false;
if(sequence.length == 1)
return true;
int rootVal = sequence[sequence.length-1];
int i = 0;
for(; i < sequence.length-1; i++){
if(sequence[i] > rootVal){
for(int j = i; j < sequence.length-1; j++){
if(sequence[j] < rootVal)
return false;
}
break;
}
}
if(i == 0){
// 没有左子树
return VerifySquenceOfBST(Arrays.copyOfRange(sequence, i, sequence.length-1));
}
if(i == sequence.length-1){
// 没有右子树
return VerifySquenceOfBST(Arrays.copyOfRange(sequence, 0, sequence.length-1));
}
return VerifySquenceOfBST(Arrays.copyOfRange(sequence, 0, i))
&& VerifySquenceOfBST(Arrays.copyOfRange(sequence, i, sequence.length-1));
}
}
024-二叉树中和为某一值的路径
注:首先,这篇最大的教训分析不清,盲目写代码。其次,数组的复制与链表的复制不熟悉。
new ArrayList<>(list)这就是链表复制(下面的太累赘,但需要掌握),Arrays.copyOfRange( , , )数组复制
ArrayList<Integer> copy(ArrayList list){
if(list == null)
return null;
ArrayList<Integer> listCopy = new ArrayList<>(Arrays.asList(new Integer[list.size()])); // 必须先开辟空间
Collections.copy(listCopy, list);
return listCopy;
}
教训深刻
import java.util.ArrayList;
import java.util.*;
public class Solution {
public ArrayList<ArrayList<Integer>> FindPath(TreeNode root,int target) {
ArrayList<ArrayList<Integer>> ret = new ArrayList<ArrayList<Integer>>();
if(root == null)
return ret;
ArrayList<Integer> list = new ArrayList<>();
preOrder(root, ret, list, target);
return ret;
}
public void preOrder(TreeNode root, ArrayList<ArrayList<Integer>> ret, ArrayList<Integer> list, int target){
if(root == null)
return;
list.add(root.val);
target -= root.val;
if(target < 0)
return;
if(target == 0 ){
if(root.left == null && root.right == null)
ret.add(list);
return;
}
//preOrder(root.left, ret, copy(list), target);
//preOrder(root.right, ret, copy(list), target);
preOrder(root.left, ret, new ArrayList(list), target);
preOrder(root.right, ret, new ArrayList(list), target);
}
ArrayList<Integer> copy(ArrayList list){
if(list == null)
return null;
ArrayList<Integer> listCopy = new ArrayList<>(Arrays.asList(new Integer[list.size()]));
Collections.copy(listCopy, list);
return listCopy;
}
}
026-二叉搜索树与双向链表
注:这一题要好好看讨论区
中序遍历若先左子树后右子树,那么最后一次落在最右;若先右子树后左子树,那么最后一次落在最左。
最简单方法是先中序遍历按顺序保存结点后再考虑连接。
中序遍历逻辑处理都放在中间。
递归引用复制后传递只能保证递归前后层次不影响,并不能传递给递归每层参数调用。
类的变量可以普通方法乃至递归方法都可以访问到。
038-二叉树的深度
039-平衡二叉树
057-二叉树的下一个结点
注:费了时间还需要复习,其实结果就只有3种,找右子树的最左或上溯一个结点或上溯直到当前结点为父结点的左结点,后两种存在找不到为null的情况。最简单的思路还是直接中序找出所有结点,然后遍历找到下一个结点。
/*
public class TreeLinkNode {
int val;
TreeLinkNode left = null;
TreeLinkNode right = null;
TreeLinkNode next = null;
TreeLinkNode(int val) {
this.val = val;
}
}
*/
public class Solution {
public TreeLinkNode GetNext(TreeLinkNode pNode)
{
if(pNode == null)
return null;
TreeLinkNode node = pNode;
if(node.right == null){
// 没有右子节点,需要上溯
if(node.next == null){
// 没有父结点
return null;
}
if(node == node.next.left){
// 存在父结点,且是父结点的左子结点
return node.next;
}
// 存在父结点,且是父结点的右子结点,需要上溯
while(node != null ){
if(node.next != null && node == node.next.left){
// 在上溯过程中,若当前结点属于父结点的左子树
return node.next;
}
node = node.next;
}
return null;
}
// 存在右子节点,找出右子树的最左
return findLeft(node.right);
}
public TreeLinkNode findLeft(TreeLinkNode root){
if(root == null)
return null;
while(root.left != null){
root = root.left;
}
return root;
}
}
058-对称的二叉树
注:空树也算对称,还需要复习。如果一个二叉树同此二叉树的镜像是同样的,定义其为对称的。也就是说二叉树关于根节点左右对称
059-按之字形顺序打印二叉树
注:之字形,先左到右,再右到左,循环。
两层循环或一层循环。列表前插或统一反转。
一层循环中利用两个栈的来回使用更是惊奇。
reverseFlag = ! reverseFlag;
060-把二叉树打印成多行
061-序列化二叉树
注:这题很难,隔段时间还需要复习
public class Solution {
int index = -1;
String Serialize(TreeNode root) {
if(root == null){
return "#";
}else{
return root.val + "," + Serialize(root.left) + "," + Serialize(root.right);
}
}
TreeNode Deserialize(String str) {
String[] s = str.split(",");
index++;
if(index > str.length())
return null;
TreeNode node = null;
if(!s[index].equals("#")){
// 说明不是空节点
node = new TreeNode(Integer.parseInt(s[index]));
node.left = Deserialize(str);
node.right = Deserialize(str);
}
return node;
}
}
062-二叉搜索树的第k个结点
005-用两个栈实现队列
020-包含min函数的栈
021-栈的压入、弹出序列
044-翻转单词顺序列(栈)
注:这题有个巨大的陷阱,就是字符串只有空格且可能有多个,只能直接返回原字符串。那怎么识别?用空格去切分,若得到数组长度为0,说明只有空格。
064-滑动窗口的最大值(双端队列)
注:双端队列就是双端都能删除、查看、添加的结构,如链表或队列。
LinkedList前后都能移除或添加或查看,ArrayDeque同样具备功能。
ArrayList虽然也具备类似功能,但普遍要借助索引,显得有些麻烦。
这题使用treeMap也行(利用排序,最后一个则为窗口中的最大值),键为元素,值为出现次数
029-最小的K个数
注:直接利用了treeSet
当然堆排序,或者其他排序后再查也行。
034-第一个只出现一次的字符
注:LinkedHashMap,保留遍历顺序,还能去重。
字符串indexOf与lastIndexOf再配合hashSet。
不用LinkedHashMap,改用hashMap或简单256位字符数组同样可以,因为字符串中就暗含遍历的顺序。
065-矩阵中的路径(BFS)
注:小心递归退出后哪些状态要恢复
066-机器人的运动范围(DFS)
注意:Solution类名和Main类名,不要改它的原本类名,否则会报"请检查是否存在数组越界等非法访问情况"
小总结:
深度优先遍历有回溯的思想(比如栈退出会返回上一层状态,其他访问数组是否要回溯看需要),广度则没有。
在DFS中我们说关键点是递归以及回溯,在BFS中,关键点则是状态的选取和标记。
bfs需要借助队列+循环(或其他一端进一端出的结构都可以,如数组),dfs递归一路到底,借助栈。
dfs类似暴力,而bfs有点头脑,适合找出最短,最少,最快等问题。
007-斐波拉契数列
008-跳台阶
009-变态跳台阶
注:倒着想
010-矩形覆盖
001-二维数组中的查找
注:尽力消除唯一性
006-旋转数组的最小数字(二分查找)
注:二分查找,也要考虑特殊情况二分查找解决不了,只能遍历
037-数字在排序数组中出现的次数(二分查找)
注:二分查找再两边延伸。但要注意,二分查找移动位置要正确,否则就可能死循环
027-字符串的排列
注:需要再次复习
递归
这还是存在问题的,需要去重。排序后遍历是可以解决的。
ArrayList是可以判断重复的(或去重)contains方法
StringBuilder更新字符方法是setCharAt()
非常惊艳的一道解法
以后全排列都可以尝试这种思路
import java.util.ArrayList;
public class Solution {
public ArrayList<String> Permutation(String str) {
ArrayList<String> result = new ArrayList<>();
if(str.length() == 0){
return result;
}
recur(str,"",result);
return result;
}
public void recur(String str,String cur,ArrayList<String> result){
if(str.length() == 0){
if(!result.contains(cur)){
result.add(cur);
}
}
for(int i = 0; i < str.length();i++){
recur(str.substring(0,i)+str.substring(i+1,str.length()),
cur+str.charAt(i),
result);
}
}
}
030-连续子数组的最大和
注:Integer的最小值为Integer.MIN_VALUE
052-正则表达式匹配(我用的暴力)
注:难
注:暴力破解太麻烦了,需要注意匹配次数问题,采取动态规划或递归思路
掌握递归,了解动态规划就行
public class Solution {
public boolean match(char[] str, char[] pattern) {
if (str == null || pattern == null)
return false;
return matchCore(str, 0, pattern, 0);
}
private boolean matchCore(char[] str, int s, char[] pattern, int p) {
//下面4行是递归结束标志,两个指针都指到了最后,才是匹配,否则不匹配
if (s == str.length && p == pattern.length)
return true;
if (s < str.length && p == pattern.length)
return false;
//虽然比的是P位置的,但是P后面出现*时,规则需要改变。
if (p + 1 < pattern.length && pattern[p + 1] == '*') {
//出现了*,并且s和P指向的相同,3种情况并列
if (s < str.length && (pattern[p] == '.') || ( pattern[p] == str[s]) ) {
// 匹配0次,匹配1次或匹配2次 // a 与 a* .*
return matchCore(str, s, pattern, p + 2)
|| matchCore(str, s + 1, pattern, p)
|| matchCore(str, s + 1, pattern, p + 2);
} else {//出现了*,并且s和p指向的不同,那就把*前面的字符理解出现了0次,p+2
// a 与 b* ,匹配0次
return matchCore(str, s, pattern, p + 2);
}
}
//说明P后面不是*,那么就进行常规判断。相同就分别给指针+1
if (s < str.length && (pattern[p] == str[s] || pattern[p] == '.'))
// a与 a . ,匹配1次
return matchCore(str, s + 1, pattern, p + 1);
//p后面又不是*,也没有.给你撑腰,你还敢出现不同,那必然false
return false;
}
}
public class Solution {
public boolean match(char[] str, char[] pattern)
{
if(str == null || pattern == null)
return false;
boolean[][] dp = new boolean[str.length+1][pattern.length+1];
dp[0][0] = true;
for (int j = 1; j <= pattern.length; j++) {
if (pattern[j - 1] == '*' && dp[0][j - 2]) {
// 解决str为空,pattern为.*问题,需要返回true
dp[0][j] = true;
}
}
for(int i = 1; i <= str.length; i++){
for(int j = 1; j <= pattern.length; j++){
if(str[i-1] == pattern[j-1] || pattern[j-1] == '.'){
dp[i][j] = dp[i-1][j-1];
}else if(pattern[j-1] == '*'){
if(pattern[j-2] != '.' && str[i-1] != pattern[j-2] ){
dp[i][j] = dp[i][j-2];
}else{
dp[i][j] = dp[i][j-2] || dp[i][j-1] || dp[i-1][j];
}
}
}
}
return dp[str.length][pattern.length];
}
}
065-矩阵中的路径(BFS)
066-机器人的运动范围(DFS)
035-数组中的逆序对(归并排序)
注:+=复合运算符带来的问题
public class Solution {
private int reverseCnt = 0;
public int InversePairs(int [] array) {
if(array == null || array.length < 2)
return 0;
mergeSort(array, 0, array.length-1);
return reverseCnt;
}
public void mergeSort(int[] array, int low, int high){
if(low >= high)
return;
int mid = (low+high)/2;
mergeSort(array, low, mid);
mergeSort(array, mid+1, high);
sort(array, low, mid, high);
}
public void sort(int[] array, int low, int mid, int high){
int[] tmp = new int[high-low+1];
int i = low, j = mid+1, pos = 0;
while(i <= mid && j <= high){
if(array[i] > array[j]){
tmp[pos++] = array[j++];
//reverseCnt++; 错误思路,漏算
//reverseCnt += (mid-i+1);
//reverseCnt = reverseCnt + (mid-i+1);
//reverseCnt += (mid-i+1)%1000000007;
reverseCnt = (reverseCnt+ (mid-i+1) )%1000000007; // 只有这一种对,数太大了还有复合运算符和四则运算有区别的
}else{
tmp[pos++] = array[i++];
}
}
while(i <= mid){
tmp[pos++] = array[i++];
}
while(j <= high){
tmp[pos++] = array[j++];
}
for(int n = 0; n < tmp.length; n++){
array[low+n] = tmp[n];
}
}
}
029-最小的K个数(堆排序)
注:前面已经写了,利用TreeSet的键排序特性
029-最小的K个数(快速排序)
注:前面已经写了
011-二进制中1的个数
注:不带进位位位移,二进制最低位是否为0,非0的数二进制一定存在1但最低位不一定就为0
012-数值的整数次方
public class Solution {
public double Power(double base, int exponent) {
if(Math.abs(base) < 1e-6)
return 0;
if(Math.abs(base-1) < 1e-6 || exponent == 0)
return 1.0;
boolean positive = true;
int absExp = exponent;
if(exponent < 0){
positive = false;
absExp = - exponent;
}
if(!positive){
return 1 / power(base, absExp);
}
return power(base, absExp);
}
public double power(double base, int absExp){
if(absExp == 1)
return base;
if((absExp & 0x1) == 0){
double tmp = power(base, absExp/2);
return tmp*tmp;
}else{
double tmp = power(base, absExp/2);
return tmp*tmp*base;
}
}
}
040-数组中只出现一次的数字
002-替换空格
注:String.replace()替换字符串方法要会
StringBuilder的append()方法
013-调整数组顺序使奇数位于偶数前面
注:反而比较难想到,按理不应该啊
插入排序是最容易想到
冒泡左偶右奇就交换
归并排序
import java.util.*;
public class Solution {
public void reOrderArray(int [] array) {
if(array == null || array.length <= 1)
return;
mergeSort(array, 0, array.length-1);
}
public void mergeSort(int[] array, int low, int high){
if(low >= high)
return;
int mid = (low+high)/2;
mergeSort(array, low, mid);
mergeSort(array, mid+1, high);
merge(array, low, mid, high);
}
public void merge(int[] array, int low, int mid, int high){
int i = 0, j = 0;
int[] L = Arrays.copyOfRange(array, low, mid+1);
int[] R = Arrays.copyOfRange(array, mid+1, high+1);
int pos = low;
// 核心在这,以前归并排序是按顺序复制一遍数组,而这里是左奇右偶或左偶右偶(不破坏偶数的相对顺序)左添加
// 左偶右奇右添加,一边添加完直接把另一边后续添加
while (pos <= high){
if(i == L.length){
array[pos++] = R[j++];
}else if(j == R.length){
array[pos++] = L[i++];
}else if(L[i] % 2 == 1 || (L[i] % 2 == 0 && R[j]%2 == 0)){
array[pos++] = L[i++];
}else{
array[pos++] = R[j++];
}
}
}
}
028-数组中出现次数超过一半的数字
031-整数中1出现的次数(从1到n整数中1出现的次数)
fuck,看了一个解法和代码对不上,浪费我时间
牢记于心,这种从个位到十位到百位观察
leetcode递归更好
public class Solution {
public int NumberOf1Between1AndN_Solution(int n) {
if(n <= 0)return 0;
int count = 0;
for(int i=1; i <= n; i*=10){
//计算在第i位上总共有多少个1
count += (n/(10*i))*i;
//不足i的部分有可能存在1
int mod = n%(10*i);
//如果超出2*i -1,则多出的1的个数是固定的
if(mod > 2*i -1){
count += i; // 如十位数,27 或任意37 > 19,存在固定10个
}else if(mod >= i){
//只有大于i的时候才能会存在1,如十位数,10<17<19,存在17-10+1=8个
count += (mod -i)+1;
}
// 如若十位数,即07 < 10 ,那么就十位数上就不存在1的情况
}
return count;
}
}
// 这道题大神解法太精彩了
public class Solution {
public int NumberOf1Between1AndN_Solution(int n) {
return f(n);
}
private int f(int n){
if(n <= 0)
return 0;
String nStr = String.valueOf(n);
int high = nStr.charAt(0) - '0';
int pow = (int)Math.pow(10, nStr.length()-1);
int last = n - high * pow;
if(high == 1){
return f(pow-1) + last + 1 + f(last);
}else{
return high * f(pow-1) + f(last) + pow;
}
}
}
032-把数组排成最小的数
注:冒泡排序就可以
全排列也可以,但是麻烦些,需要找出所有组合后排序,如下
System.arraycopy复制数组(分段复制进一个数组),相对于Arrays.copyOfRange()就是单纯复制,不方便截取拼接
列表的toArray方法转化为数组,需要开辟一段空间
Arrays.asList()方法,将数组转换为列表,里面数组为包装类数组
import java.util.*;
public class Solution {
public String PrintMinNumber(int [] numbers) {
if(numbers == null || numbers.length == 0)
return "";
ArrayList<String> list = new ArrayList<String>();
int[] curNum = new int[0];
permute(numbers, curNum, list);
Collections.sort(list);
if(!list.isEmpty()){
return list.get(0);
}
return "";
}
public void permute(int[] numbers, int[] curNums, ArrayList<String> list){
if(numbers.length == 0){
String str = "";
for(int i = 0; i < curNums.length; i++){
str += curNums[i];
}
list.add(str);
return;
}
for(int i = 0; i < numbers.length; i++){
//todo:这里太过累赘了
int[] newNum = new int[numbers.length-1];
// 分段复制numbers数组
System.arraycopy(numbers, 0, newNum, 0, i);
System.arraycopy(numbers, i+1, newNum, i, numbers.length-i-1);
int[] newCur = new int[curNums.length+1];
// curNums添加一个元素
System.arraycopy(curNums,0, newCur,0, curNums.length);
newCur[curNums.length] = numbers[i];
//todo
permute(newNum, newCur, list);
}
}
}
033-丑数
注:3个指针加一个临时数组,动态规划
041-和为S的连续正数序列(滑动窗口思想)
注:判断右边界
042-和为S的两个数字(双指针思想)
注:最先想到的是二分查找再两端向外,直接双指针向内更好
043-左旋转字符串(矩阵翻转)
注:最先想到的是放入双端队列,截取字符串拼接无疑更好
046-孩子们的游戏-圆圈中最后剩下的数(约瑟夫环)
递归没理解(到时也推不出来),下面普通方法较容易理解
环形链表模拟圆圈也挺好的,但是要注意结点停留位置
public class Solution {
public int LastRemaining_Solution(int n, int m) {
if(n <= 0)
return -1;
if(n < 2 || m < 0)
return 0;
int[] visit = new int[n]; // 访问数组
int pos = 0;// 访问位置
int cnt = n; // 剩余未领礼物小朋友人数
while(cnt > 1){
int circle = m;
while(circle > 1){
pos++; // 开始报数
pos = pos % n;
if(visit[pos] != 1){
// 此位置小朋友未领礼物,报数有效
circle--;
}
// 反之,此位置小朋友已领礼物,不报数
}
while(visit[pos] == 1){
// 报数已经到了m-1,当前位置下朋友若已领礼物,反复跳过,直到下一个未领礼物的小朋友
pos++;
pos = pos % n;
}
visit[pos] = 1; // 设置当前位置小朋友领礼物
while(visit[pos] == 1){
pos++; // 从下一个未领礼物小朋友开始报数
pos = pos % n;
}
cnt--; // 未领礼物小朋友个数减一
}
// 未领礼物小朋友个数为1,返回位置
return pos;
}
}
051-构建乘积数组
注:使用上下三角
import java.util.ArrayList;
public class Solution {
public int[] multiply(int[] A) {
if(A == null || A.length == 0)
return A;
int len = A.length;
int[] B = new int[len];
B[0] = 1;
for(int i = 1; i < len; i++){
B[i] = B[i-1] * A[i-1]; // 左边已经构造了下三角
}
int tmp = 1; // 构造上三角辅助变量
for(int i = len-2; i >= 0; i--){
tmp *= A[i+1];
B[i] *= tmp;
}
return B;
}
}