剑指 offer 03 数组中重复的数字
题目
在一个长度为 n 的数组 nums 里的所有数字都在 0~n-1 的范围内。数组中某些数字是重复的,但不知道有几个数字重复了,也不知道每个数字重复了几次。请找出数组中任意一个重复的数字。
思路
法一:利用hashset的不重复性
查找重复的数字,利用哈希集合(HashSet)的特性不可重复性,如果set中没有添加成功,说明有重复,即可解决,类似查重的都可以利用hashset,而对应的HashMap可以重复
法二:利用动态数组
剑指 offer 04 二维数组中的查找
题目
在一个 n * m 的二维数组中,每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺序排序。请完成一个高效的函数,输入这样的一个二维数组和一个整数,判断数组中是否含有该整数。
思路
如下图,将该数组倒过来,选取下顶点(i=length-1,j=0)的值,若比该值比目标值小,则应该减小行数,若比目标大,则增加列数,即可最快找到目标值
class Solution {
public boolean findNumberIn2DArray(int[][] matrix, int target) {
int i = matrix.length - 1, j = 0;//选取第一列最后一个数 while(i >= 0 && j < matrix[0].length)
{
if(matrix[i][j] > target) i--;//大于目标值,减少行数 else if(matrix[i][j] < target) j++;//小于目标值,增加列数 else return true;//等于返回true }
return false;
}
}
剑指 offer 05 替换空格
题目
请实现一个函数,把字符串 s 中的每个空格替换成"%20"。
思路
法一:利用replaceAll( )方法,但是不适用于面试和笔试
法二:建立新的String利用If判断进行添加,一般使用StringBuilder(效率高)或者StringBuffer(线程安全)
法三:对数组进行扩容,进行逐个添加,特定字符替换,arr[size++]体现了动态的将数据加入到数组中,同时String(arr,offset,count)方法注意一下
剑指 offer 06 从尾到头打印链表
题目
输入一个链表的头节点,从尾到头反过来返回每个节点的值(用数组返回)。
思路
法一:属于逆序的应用,应该首先想到栈,先进后出,先入栈
法二:还可以创建一个同大小新的数组,从数组的尾部进行添加数据,也可以得到逆序
剑指 offer 07 重建二叉树
题目
输入某二叉树的前序遍历和中序遍历的结果,请重建该二叉树。假设输入的前序遍历和中序遍历的结果中都不含重复的数字。
思路
知识点:前序遍历列表:第一个元素永远是 【根节点 (root)】
中序遍历列表:根节点 (root)【左边】的所有元素都在根节点的【左分支】,【右边】的所有元素都在根节点的【右分支】
递推参数: 根节点在前序遍历的索引 root 、子树在中序遍历的左边界 left 、子树在中序遍历的右边界 right ;
终止条件: 当 left > right ,代表已经越过叶节点,此时返回 null;
递推工作:
建立根节点 node : 节点值为 preorder[root] ; 划分左右子树: 查找根节点在中序遍历 inorder 中的索引 i ; 为了提升效率,使用哈希表 dic 存储中序遍历的值与索引的映射,查找操作的时间复杂度为 O(1)O(1)
构建左右子树: 开启左右子树递归;
/*class TreeNode{int val;TreeNode left;TreeNode right;TreeNode(int x){ val = x;}}*/
public TreeNode buildTree(int[] preorder, int[] inorder) {
//保存中序遍历的下标和数据在map中,便于提高查找效率,注意要反过来保存 HashMap map = new HashMap<>();
for (int i=0;i
map.put(inorder[i],i);
}
TreeNode root = TreeBuild(preorder, map, 0, 0, map.size() - 1);
return root;
}
/*** @describe: 恢复二叉树* @param preOrder 前序遍历* @param map 保存中序遍历的哈希表* @param rootIndex 根节点下标(前序遍历中* @param leftIndex 左边界* @param rightIndex 右边界* @return: jianzhioffer.TreeNode* @author Eric:* @date: 2020/11/3 23:58*/
public TreeNode TreeBuild(int[] preOrder,HashMap map,int rootIndex,int leftIndex,int rightIndex){
//递归退出条件 if (leftIndex>rightIndex){
return null;
}
//建立新的根节点,根节点下标为前序遍历中根节点下标 TreeNode root = new TreeNode(preOrder[rootIndex]);
//得出根节点在中序遍历中的下标 int idx = map.get(root.val);
//进行左子树递归,对于左子树(中序遍历中),其根节点(前序遍历)即为前序遍历中右子树的第一个节点,即为rootIndex+1 //其左边界(中序遍历)为原边界leftIndex,右边界为左子树(中序遍历)的边界,即为idx-1 root.left = TreeBuild(preOrder,map,rootIndex+1,leftIndex,idx-1);
//进行右子树递归,对于右子树(中序遍历中),其根节点(前序遍历中)当前根节点+左子树的个数+1rootIndex+idx-leftIndex+1 //左边界(中序遍历)为右子树(中序遍历中)的第一个节点idx+1,右节点不变rightIndex root.right = TreeBuild(preOrder,map,(rootIndex+idx-leftIndex+1),idx+1,rightIndex);
return root;
}
剑指 offer 09 用两个栈实现队列
题目
用两个栈实现一个队列。队列的声明如下,请实现它的两个函数 appendTail 和 deleteHead ,分别完成在队列尾部插入整数和在队列头部删除整数的功能。(若队列中没有元素,deleteHead 操作返回 -1 )
思路分析:
栈stack1用来添加数据,另一个栈stack2用来删除数据,当删除数据时,注意,先判断stack2是否为空,因为只有stack2为空,才能把stack1的数据添加进去,不会扰乱顺序 ,stack2中的数据具有删除优先级,相当于队列头部数据
(1)stack2空,stack1空,删除后剩余数据为0,添加的数据为0,所以返回-1
(2)stack2空,stack1不空,则将stack1的所有数据压入stack2,再返回stack2.pop()的数据
(3)stack2不空,则不管stack1是否有没有数据,只能删除stack2的数据删除完,返回stack2.pop()的数据
优化
如果你使用Stack的方式来做这道题,会造成速度较慢; 原因的话是Stack继承了Vector接口,而Vector底层是一个Object[]数组,那么就要考虑空间扩容和移位的问题了。 可以使用LinkedList来做Stack的容器,因为LinkedList实现了Deque接口,所以Stack能做的事LinkedList都能做,其本身结构是个双向链表,扩容消耗少。
LinkedList stack1 = new LinkedList;
LinkedList stack2 = new LinkedList;
或
Deque stack1 = new LinkedList;
Deque stack2 = new LinkedList;
剑指 offer 10-1 斐波那契数列
题目
写一个函数,输入 n ,求斐波那契(Fibonacci)数列的第 n 项。斐波那契数列的定义如下:
F(0) = 0, F(1) = 1
F(N) = F(N - 1) + F(N - 2), 其中 N > 1.
斐波那契数列由 0 和 1 开始,之后的斐波那契数就是由之前的两数相加而得出。
答案需要取模 1e9+7(1000000007),如计算初始结果为:1000000008,请返回 1。
思路:
定义一元数组当作斐波那契数列,分为n<2和n>2,按照公式进行计算即可
注意在n>2进行循环进行计算时,数字会越来越大,超越32位甚至64位,所以在循环内就需要边计算边取模,防止出界
法一:动态规划
原理: 以斐波那契数列性质 f(n + 1) = f(n) + f(n - 1)为转移方程。 从计算效率、空间复杂度上看,动态规划是本题的最佳解法。
法二:递归
原理: 把 f(n) 问题的计算拆分成 f(n-1) 和 f(n−2) 两个子问题的计算,并递归,以 f(0) 和 f(1) 为终止条件。 缺点: 大量重复的递归计算,例如 f(n)和 f(n - 1)两者向下递归需要 各自计算 f(n - 2)的值。
剑指 offer 10-2 青蛙跳台阶问题
题目
一只青蛙一次可以跳上1级台阶,也可以跳上2级台阶。求该青蛙跳上一个 n 级的台阶总共有多少种跳法。
答案需要取模 1e9+7(1000000007),如计算初始结果为:1000000008,请返回 1。
思路
设跳上 n 级台阶有 f(n)种跳法。在所有跳法中,青蛙的最后一步只有两种情况: 跳上 1级或 2级台阶。
当为 1 级台阶: 剩 n-1个台阶,此情况共有 f(n-1) 种跳法; 当为 2级台阶: 剩 n-2个台阶,此情况共有 f(n-2)种跳法。
f(n) 为以上两种情况之和,即 f(n)=f(n-1)+f(n-2),以上递推性质为斐波那契数列。
剑指 offer 11 旋转数组的最小数字
题目
把一个数组最开始的若干个元素搬到数组的末尾,我们称之为数组的旋转。输入一个递增排序的数组的一个旋转,输出旋转数组的最小元素。例如,数组 [3,4,5,1,2] 为 [1,2,3,4,5] 的一个旋转,该数组的最小值为1。
思路
法一:常规思路,复杂度为O(n),遍历数组,当找到当前数小于前一个数,最小数为当前数,如果遍历完没有找到,就返回第一个元素(说明数组没有旋转,顺序为从小到大)
法二:利用二分查找法,复杂度为log(n),推荐使用,左边界为Low,右边界为high,中间为mid=(low+high)/2
class Solution {
public:
int minArray(vector& numbers) {
int low = 0;
int high = numbers.size() - 1;
while (low < high) {//当low==high说明已经找到 int pivot = low + (high - low) / 2;
if (numbers[pivot] < numbers[high]) {
high = pivot;
}
else if (numbers[pivot] > numbers[high]) {
low = pivot + 1;
}
else {//相等的情况,high左移 high -= 1;
}
}
return numbers[low];
}
};
剑指 offer 12 矩阵中的路径
题目
请设计一个函数,用来判断在一个矩阵中是否存在一条包含某字符串所有字符的路径。路径可以从矩阵中的任意一格开始,每一步可以在矩阵中向左、右、上、下移动一格。如果一条路径经过了矩阵的某一格,那么该路径不能再次进入该格子。例如,在下面的3×4的矩阵中包含一条字符串“bfce”的路径(路径中的字母用加粗标出)。
[["a","b","c","e"], ["s","f","c","s"], ["a","d","e","e"]]
思路
本问题是典型的矩阵搜索问题,可使用 深度优先搜索(DFS)+ 剪枝 解决。
每个点有4个方向可以走,可以想象成一棵4叉树,树的深度优先遍历很容易想到用递归,当一个方法执行完毕,或者遇到return,就会返回,遵守谁调用,就将结果返回给谁,同时当方法执行完毕或者返回时,该方法也就执行完毕。
算法原理
深度优先搜索: 可以理解为暴力法遍历矩阵中所有字符串可能性。DFS 通过递归,先朝一个方向搜到底,再回溯至上个节点,沿另一个方向搜索,以此类推。 剪枝: 在搜索中,遇到 这条路不可能和目标字符串匹配成功 的情况(例如:此矩阵元素和目标字符不同、此元素已被访问),则应立即返回,称之为 可行性剪枝 。
递归参数: 当前元素在矩阵 board 中的行列索引 i 和 j ,当前目标字符在 word 中的索引 k 。 终止条件: 返回 false : ① 行或列索引越界 或 ② 当前矩阵元素与目标字符不同 或 ③ 当前矩阵元素已访问过 (③ 可合并至 ② ) 。 返回 true: 字符串 word 已全部匹配,即 k = len(word) - 1 。 递推工作: 标记当前矩阵元素: 将 board[i] [j]值暂存于变量 tmp ,并修改为字符 '/' ,代表此元素已访问过,防止之后搜索时重复访问。 搜索下一单元格: 朝当前元素的 上、下、左、右 四个方向开启下层递归,使用 或 连接 (代表只需一条可行路径) ,并记录结果至 res 还原当前矩阵元素: 将 tmp 暂存值还原至 board[i] [j] 元素。 回溯返回值: 返回 res ,代表是否搜索到目标字符串。
class Solution {
public boolean exist(char[][] board, String word) {
char[] words = word.toCharArray();
for(int i = 0; i < board.length; i++) {
for(int j = 0; j < board[0].length; j++) {
if(dfs(board, words, i, j, 0)) return true;
}
}
return false;
}
boolean dfs(char[][] board, char[] word, int i, int j, int k) {
if(i >= board.length || i < 0 || j >= board[0].length || j < 0 || board[i][j] != word[k]) return false;
if(k == word.length - 1) return true;
char tmp = board[i][j];
board[i][j] = '/';
boolean res = dfs(board, word, i + 1, j, k + 1) || dfs(board, word, i - 1, j, k + 1) ||
dfs(board, word, i, j + 1, k + 1) || dfs(board, word, i , j - 1, k + 1);
board[i][j] = tmp;//回溯 return res;
}
}
剑指 offer 13 机器人的运动范围
题目
地上有一个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。请问该机器人能够到达多少个格子?
思路
很明显本题采用dfs遍历,同时要注意判断是否已经被访问过
class Solution {
public int movingCount(int m, int n, int k) {
int[][] arr = new int[m][n];
boolean[][] isVisited = new boolean[m][n];
return dfs(arr,0,0,isVisited,k);
}
public int dfs(int[][] arr,int i,int j,boolean[][] isVisited,int k){
//判断当前位置是否符合访问的规则 if (i<0||j<0||i>=arr.length||j>=arr[0].length||isVisited[i][j]){
return 0;
}
int sum = i/10+i%10+j/10+j%10;//得到当前位置的sum和 if (sum>k){
return 0;
}
isVisited[i][j] = true;//设置当前位置访问过 //最后的结果+1是因为当前位置也可以访问到 int res = dfs(arr,i-1,j,isVisited,k)+dfs(arr,i,j-1,isVisited,k)+
dfs(arr,i+1,j,isVisited,k)+dfs(arr,i,j+1,isVisited,k)+1;
return res;
}
}