剑指offer Java代码解析
- 剑指 offer 03:数组中重复的数字
- 剑指 offer 04:二维数组中的查找
- 剑指 offer 05:替换空格
- 剑指 offer 06:从尾到头打印链表
- 剑指 offer 07:重建二叉树
- 剑指 offer 08:二叉树的下一个节点
- 剑指 offer 09:用两个栈实现队列
- 剑指 offer 10-I:斐波那契数列
- 剑指 offer 10-II:青蛙跳台阶问题
- 剑指 offer 11:旋转数组的最小数字
- 剑指 offer 12:矩阵中的路径
- 剑指 offer 13:机器人的运动范围
- 剑指 offer 14-I:剪绳子
- 剑指 offer 14-II:剪绳子
- 剑指 offer 15:二进制中1的个数
剑指 offer 03:数组中重复的数字
题目描述
找出数组中重复的数字。(传送门)
在一个长度为 n 的数组 nums 里的所有数字都在 0~n-1 的范围内。数组中某些数字是重复的,但不知道有几个数字重复了,也不知道每个数字重复了几次。请找出数组中任意一个重复的数字。
示例 1:
输入:
[2, 3, 1, 0, 2, 5, 3]
输出:2 或 3
解题思路+代码实现
方法一: 排序+for循环
public int findRepeatNumber(int[] nums) {
int len = nums.length;
Arrays.sort(nums);
for (int i = 0; i < len - 1; i++) {
if (nums[i + 1] - nums[i] == 0) {
return nums[i];
}
}
return -1;
}
方法二:HashSet
这里使用set集合的性质:
- Set集合中的对象不按特定的方式排序,只是简单的将对象加入到集合中,但是Set集合不能包括重复对象
- Set接口继承了Collection接口,因此也包含Collection接口的所有方法
public int findRepeatNumber2(int[] nums) {
Set<Integer> set = new HashSet<>();
for (int num : nums) {
if (!set.add(num)) {
//当重复元时会返回false
return num;
}
}
return -1;
}
方法三:原地交换
首先注意这句话在一个长度为 n 的数组 nums 里的所有数字都在 0~n-1 的范围内。,那么每个数字都可以放在它对应的下标,比如说0放在nums[0] 位置,1放在nums[1]位置。
- 从头到尾依次扫描这个数组的每一个元素
- 看下标为 i 的数字是不是等于 i ,如果它的值为m != i 就和下标为m 的值进行比较,如果相等就返回这个重复的数字,如果不相同那就交换。
- 重复这样的操作,知道遍历结束
public int findRepeatNumber(int[] nums) {
int len = nums.length;
for (int i = 0; i < len; i++) {
while (i != nums[i]) {
// 注意在这里时while 循环可能 交换回来的数也不等于 i
if (nums[i] != nums[nums[i]]) {
// 不等于则进行交换
int temp = nums[i];
nums[i] = nums[temp];//注意这里如果写nums[i] = nums[nums[i]];会超出时间复杂度
nums[temp] = temp;
}else{
return nums[i];
}
}
}
return -1;
}
剑指 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]
]
解题思路+代码实现
方法一: 暴力查找
public boolean findNumberIn2DArray11(int[][] matrix, int target) {
if (matrix == null || matrix.length == 0 || matrix[0].length == 0) {
return false;
}
int ROW = matrix.length;
int COL = matrix[0].length;
for (int i = 0; i < ROW ; i++) {
for (int j = 0; j < COL; j++) {
if (matrix[i][j] == target) {
return true;
}
}
}
return false;
}
方法二:优化方法
我们可以从右上角开始查找,根据矩阵元素的规律,当左上角元素小于目标值则改行一定是都小于目标值的,直接就不需要判断该行的其他元素行++即可;当左上角元素大于该目标值,则改行的下方也就找不到,一定就在该列的前边,进行列–即可。这样就优化了暴力遍历每一个值了。
查找过程:
public boolean findNumberIn2DArray(int[][] matrix, int target) {
if (matrix == null || matrix.length == 0 || matrix[0].length == 0) {
return false;
}
int i = 0;
int j = matrix[0].length-1;
while (i < matrix.length && j >= 0) {
int tmp = matrix[i][j];
if (tmp > target) {
j--;
} else if (tmp < target) {
i++;
} else {
return true;
}
}
return false;
}
剑指 offer 05:替换空格
题目描述
请实现一个函数,把字符串 s 中的每个空格替换成"%20"。
(传送门)
示例 1:
输入:s = "We are happy."
输出:"We%20are%20happy."
解题思路+代码实现
方法一: replaceAll
public String replaceSpace(String s) {
//输入:s = "We are happy."
//输出:"We%20are%20happy."
return s.replaceAll(" ","%20");
}
方法二:字符数组
public String replaceSpace(String s) {
//输入:s = "We are happy."
//输出:"We%20are%20happy."
//return s.replaceAll(" ","%20");
int len = s.length();
char[] array = new char[3 * len]; // 按照最坏打算扩容
int size = 0; // 记录下标位置
for (int i = 0; i < len; i++) {
if (s.charAt(i) != ' ') {
array[size++] = s.charAt(i);
} else {
array[size++] = '%';
array[size++] = '2';
array[size++] = '0';
}
}
String ans = new String(array, 0, size);// 将有效部分转为字符串
return ans;
}
剑指 offer 06:从尾到头打印链表
题目描述
输入一个链表的头节点,从尾到头反过来返回每个节点的(用数组返回)。(传送门)
示例 1:
输入:head = [1,3,2]
输出:[2,3,1]
解题思路+代码实现
方法一:递归
public int[] reversePrint(ListNode head) {
int count = 0;
ListNode tmp = head;
while (tmp != null) {
tmp = tmp.next;
count++;
}
int[] array = new int[count];
reversePrintHelper(head,array,count-1);
return array;
}
private void reversePrintHelper(ListNode head, int[] array,int i) {
if (head == null) {
return;
}
array[i] = head.val;
reversePrintHelper(head.next,array,--i);
}
方法二:辅助栈
public int[] reversePrint(ListNode head) {
Stack<ListNode> stack = new Stack<ListNode>();
ListNode temp = head;
while (temp != null) {
stack.push(temp);
temp = temp.next;
}
int size = stack.size();
int[] ans = new int[size];
for (int i = 0; i < size; i++) {
ans[i] = stack.pop().val;
}
return ans;
}
剑指 offer 07:重建二叉树
题目描述
输入某二叉树的前序遍历和中序遍历的结果,请重建该二叉树。假设输入的前序遍历和中序遍历的结果中都不含重复的数字。(传送门)
例如,给出
前序遍历 preorder = [3,9,20,15,7]
中序遍历 inorder = [9,3,15,20,7]
返回如下的二叉树:
3
/ \
9 20
/ \
15 7
解题思路+代码实现
方法一:递归
public class Interview07 {
//输入某二叉树的前序遍历和中序遍历的结果,请重建该二叉树。假设输入的前序遍历和中序遍历的结果中都不含重复的数字。
//前序遍历 preorder = [3,9,20,15,7]
//中序遍历 inorder = [9,3,15,20,7]
private int index = 0;
public TreeNode buildTree(int[] preorder, int[] inorder) {
index = 0;
return buildTreeHelper(preorder, inorder, 0, inorder.length);//递归
}
//[inorderLeft,inorderRight) 表示当前这个子树的中序遍历区间
private TreeNode buildTreeHelper(int[] preorder, int[] inorder, int inorderLeft, int inorderRight) {
if (inorderLeft >= inorderRight) {
return null;
}
if (index >= preorder.length) {
return null;
}
// 创建一个新的结点
TreeNode newNode = new TreeNode(preorder[index]);
// 在中序结果中查找newNode的位置
int pos = find(inorder, inorderLeft, inorderRight, newNode.val);
index++;
newNode.left = buildTreeHelper(preorder, inorder, inorderLeft, pos);
newNode.right = buildTreeHelper(preorder, inorder, pos + 1, inorderRight);
return newNode;
}
// 在中序中查找 newNode.val位置
private int find(int[] inorder, int inorderLeft, int inorderRight, int val) {
for (int i = inorderLeft; i < inorderRight; i++) {
if (inorder[i] == val) {
return i;
}
}
return -1;
}
}
剑指 offer 08:二叉树的下一个节点
题目描述
给定一棵二叉树和其中的一个节点,如何找到中序遍历的序列的下一个节点?树中的结点不仅包含左右子结点,同时包含指向父结点的指针。
例如:如下的二叉树:
a
/ \
b c
/ \ / \
d e f g
/ \
h i
其中序遍历结果为=={ d, b, h, e, i, a, f, c, g}==
解题思路+代码实现
现在分为两大种情况:
- 1.该节点有右子树,则下一节点为他右子树的中序遍历的第一个值(例如节点b的下一节点为h,节点a的下一个节点是f)
- 2.该节点没有右孩子,则需要去找其父亲节点(反正结果一定是一个父节点或则null)
- 2.1 如果该节点是父亲节点的左孩子,那么下一节点就是该父节点(例如d的下一节点是b)
- 2.2 如果该节点是父亲节点的右孩子,那么需要沿着父节点向上遍历,直到找到那个是父节点的左孩子的节点,那么下一节点是该父节点。(例如b的父节点a就是节点i的下一节点)
- 2.3 如果遍历到root根节点仍然没有找到是父节点左孩子的节点,那么则返回None (例如节点g没有下一节点)
Java代码
public TreeLinkNode getNext(TreeLinkNode pNode){
// 1,首先判断为空的情况
if(pNode == null){
return null;
}
// 2.当前节点有右子节点
TreeLinkNode curr = pNode;
if(null != pNode.right){
//则去找最左边的子节点即可
curr = curr.right;
while (curr.left!=null){
curr = curr.left;
}
return curr;
// 3.当前节点没有由右子节点
}else {
//就去父节点中找,反正得到的结果一定是一个父节点
//下面这个可以考虑i节点的下一节点是a的情况
while(curr.parent !=null && curr == curr.parent.right){
curr = curr.parent;
}
return curr.parent;
}
}
剑指 offer 09:用两个栈实现队列
题目描述
用两个栈实现一个队列。队列的声明如下,请实现它的两个函数 appendTail 和 deleteHead ,分别完成在队列尾部插入整数和在队列头部删除整数的功能。(若队列中没有元素,deleteHead 操作返回 -1 )(传送门)
示例 1:
输入:
["CQueue","appendTail","deleteHead","deleteHead"]
[[],[3],[],[]]
输出:[null,null,3,-1]
示例 2:
输入:
["CQueue","deleteHead","appendTail","appendTail","deleteHead","deleteHead"]
[[],[],[5],[2],[],[]]
输出:[null,-1,null,null,5,2]
解题思路+代码实现
Java代码
public class Interview09 {
class CQueue {
Stack<Integer> stack1;
Stack<Integer> stack2;
public CQueue() {
stack1 = new Stack<Integer>();// 负责入栈
stack2 = new Stack<Integer>();// 负责出栈
}
// 尾插
public void appendTail(int value) {
stack1.add(value);// 直接入栈
}
// 头删
public int deleteHead() {
// stack2 不为空直接出栈,并返回
if (!stack2.isEmpty()) {
return stack2.pop();
}
// stack2为空,从stack1 往2 中压栈(全部)
if(stack2.isEmpty()) {
while (!stack1.isEmpty()) {
stack2.push(stack1.pop());
}
}
// 如果 2 还是空直接返回 -1 ,否则 stack2 出栈即可
return stack2.isEmpty() ? -1 : stack2.pop();
}
}
}
剑指 offer 10-I:斐波那契数列
题目描述
(传送门)写一个函数,输入 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。
示例 1:
输入:n = 2
输出:1
示例 2:
输入:n = 5
输出:5
解题思路+代码实现
Java代码
public class Interview10_1 {
public int fib(int n) {
// F(0) = 0, F(1) = 1
// F(N) = F(N - 1) + F(N - 2)
if (n < 2) {
return n;
}
int f0 = 0;
int f1 = 1;
int f2 = 1;
for (int i = 2; i < n; i++) {
f2 = (f0 + f1)%1000000007;
f0 = f1;
f1 = f2;
}
return f2;
}
}
剑指 offer 10-II:青蛙跳台阶问题
题目描述
一只青蛙一次可以跳上1级台阶,也可以跳上2级台阶。求该青蛙跳上一个 n 级的台阶总共有多少种跳法。
答案需要取模 1e9+7(1000000007),如计算初始结果为:1000000008,请返回 1。(传送门)
示例 1:
输入:n = 2
输出:2
示例 2:
输入:n = 7
输出:21
示例 3:
输入:n = 0
输出:1
解题思路+代码实现
Java代码
public static int numWays(int n) {
//输入:n = 2
//输出:2
if (n < 2) {
return 1;
}
int[] dp = new int[n+1];
dp[0] = 1;
dp[1] = 1;
for (int i = 2; i <= n; i++) {
dp[i] = (dp[i - 1] + dp[i - 2])%1000000007;
}
return dp[n];
}
剑指 offer 11:旋转数组的最小数字
题目描述
把一个数组最开始的若干个元素搬到数组的末尾,我们称之为数组的旋转。输入一个递增排序的数组的一个旋转,输出旋转数组的最小元素。例如,数组 [3,4,5,1,2] 为 [1,2,3,4,5] 的一个旋转,该数组的最小值为1。 (传送门)
示例 1:
输入:[3,4,5,1,2]
输出:1
示例 2:
输入:[2,2,2,0,1]
输出:0
解题思路+代码实现
方法一:暴力
暴力从左往右找,因为他是有序的经过旋转一次,当出现从大到小的转变,那么那个小的数就是找的最小值。
public int minArray111(int[] numbers) {
int len = numbers.length;
for (int i = 0; i < len - 1; i++) {
if (numbers[i] > numbers[i + 1]) {
return numbers[i + 1];
}
}
return numbers[0];
}
方法二:二分
注意为什莫是left = mid+1;(mid大于right,那么mid肯定不是最小,可以直接排除掉)
注意为什莫是right = mid;(mid小于right,mid有可能是最小的就不能排除)
第三种需要考虑到相同时,如下两种情况不能判断最小是在左边区间还是右边区间,所以只能暴力排除一个:
public int minArray(int[] numbers) {
// 1 3 5
int len = numbers.length;
int left = 0;
int right = len - 1;
int mid = 0;
while (left < right) {
mid = left + (right - left) / 2;
if (numbers[mid] > numbers[right]) {
left = mid+1;
} else if (numbers[mid] < numbers[right]) {
right = mid;
} else {
right--;// 因为当出现相同的元素是就不能判断最小值在他的左边还是右边所以需要,--(暴力遍历结合)
}
}
return numbers[right];
}
剑指 offer 12:矩阵中的路径
题目描述
请设计一个函数,用来判断在一个矩阵中是否存在一条包含某字符串所有字符的路径。路径可以从矩阵中的任意一格开始,每一步可以在矩阵中向左、右、上、下移动一格。如果一条路径经过了矩阵的某一格,那么该路径不能再次进入该格子。例如,在下面的3×4的矩阵中包含一条字符串“bfce”的路径(路径中的字母用加粗标出)。
[[“a”,“b”,“c”,“e”],
[“s”,“f”,“c”,“s”],
[“a”,“d”,“e”,“e”]]
但矩阵中不包含字符串“abfb”的路径,因为字符串的第一个字符b占据了矩阵中的第一行第二个格子之后,路径不能再次进入这个格子。(传送门)
示例 1:
输入:board = [["A","B","C","E"],["S","F","C","S"],["A","D","E","E"]], word = "ABCCED"
输出:true
示例 2:
输入:board = [["a","b"],["c","d"]], word = "abcd"
输出:false
解题思路+代码实现
方法一:DFS+剪枝
public class Interview12 {
public boolean exist(char[][] board, String word) {
int len = word.length();
int ROW = board.length;
int COL = board[0].length;
if (ROW * COL < len) {
return false;
}
int index = 0;
// 监视每个位置是否被用过了
boolean[][] visit_board = new boolean[ROW][COL];
for (int i = 0; i < ROW; i++) {
for (int j = 0; j < COL; j++) {
if (existHelper(board, visit_board, ROW, COL, i, j, word, len, index)) {
return true;
}
}
}
return false;
}
// [["a","b"],["c","d"]]
// "abcd"
private boolean existHelper(char[][] board, boolean[][] visit_board, int ROW, int COL, int i, int j, String word, int len, int index) {
if (i < 0 || j < 0 || i >= ROW || j >= COL || visit_board[i][j] || board[i][j] != word.charAt(index)) {
return false;// 有一个为真,就返回flase说明不满足条件,是找不到的
}
if (visit_board[i][j] == false && (board[i][j] == word.charAt(index))) {
visit_board[i][j] = true;
// 找到最后一个,说明找到了
if (index + 1 >= len) {
return true;
}
}
// 去找他的四个方向
if ( existHelper(board, visit_board, ROW, COL, i+1, j, word, len, index+1)||
existHelper(board, visit_board, ROW, COL, i-1, j, word, len, index+1)||
existHelper(board, visit_board, ROW, COL, i, j+1, word, len, index+1)||
existHelper(board, visit_board, ROW, COL, i, j-1, word, len, index+1)) {
return true;
}
// 四个方向都没有找到,那么visit_board[i][j]这个位置去找是找不到的,置为false在往回退
visit_board[i][j] = false;
return false;
}
}
剑指 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。请问该机器人能够到达多少个格子?(传送门)
示例 1:
输入:m = 2, n = 3, k = 1
输出:3
示例 2:
输入:m = 3, n = 1, k = 0
输出:1
解题思路+代码实现
方法一:DFS
/**
* @ClassName Interview13
* @Description :TODO
* @Author Josvin
* @Date 2021/01/16/13:33
*/
public class Interview13 {
public int movingCount(int m, int n, int k) {
// 需要一个监视数组,记录位置是否被访问
boolean[][] visit = new boolean[m][n];
return movingCountHelper(m, n, 0, 0, k, visit);
}
private int movingCountHelper(int m, int n, int i, int j, int k, boolean[][] visit) {
// 递归返回条件:越界,该位置已经被访问,各个位数之和大于k
if (i < 0 || j < 0 || i >= m || j >= n || visit[i][j] == true || digit_num(i, j) > k) {
return 0;
}
// 程序走到这里说明,该位置没有被访问,并且没有越界,符合访问条件,则将其置为true
visit[i][j] = true;
// 递归搜索四个方向
return 1 + movingCountHelper(m, n, i, j - 1, k,visit) + movingCountHelper(m, n, i, j + 1, k,visit) + movingCountHelper(m, n, i + 1, j,k, visit) + movingCountHelper(m, n, i - 1, j, k,visit);
}
// 计算(i,j),i和j各个位数之和
private int digit_num(int i, int j) {
int sum = 0;
while (i != 0) {
sum += i % 10;
i /= 10;
}
while (j != 0) {
sum += j % 10;
j /= 10;
}
return sum;
}
}
剑指 offer 14-I:剪绳子
题目描述
给你一根长度为 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:
输入: 2
输出: 1
解释: 2 = 1 + 1, 1 × 1 = 1
示例 2:
输入: 10
输出: 36
解释: 10 = 3 + 3 + 4, 3 × 3 × 4 = 36
解题思路+代码实现
方法一:动态规划
public int cuttingRope(int n) {
int[] dp = new int[n + 1];// 长度为 n 时最大的乘积
dp[2] = 1;
for (int i = 3; i <= n; i++) {
for (int j = 2; j < i; j++) {
// j来遍历 i 之前的基础上
dp[i] = Math.max( Math.max(j*(i-j),dp[i-j]*j), dp[i]);
}
}
return dp[n];
}
方法二:数学规律
由数学推导得到:尽可能将绳子以长度 为3 等分为多段时,乘积最大。
public int cuttingRope(int n) {
if (n <= 3) {
return n - 1;
}
if (n == 4) {
return 4;
}
int ans = 1;
while (n > 4) {
ans *= 3;
n = n - 3;
}
// 最后n的值只有可能是:2、3、4。而2、3、4能得到的最大乘积恰恰就是自身值
// 因为2、3不需要再剪了(剪了反而变小);4剪成2x2是最大的,2x2恰巧等于4
return ans*n;
}
剑指 offer 14-II:剪绳子
题目描述
给你一根长度为 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。(传送门)
答案需要取模 1e9+7(1000000007),如计算初始结果为:1000000008,请返回 1。
示例 1:
输入: 2
输出: 1
解释: 2 = 1 + 1, 1 × 1 = 1
示例 2:
输入: 10
输出: 36
解释: 10 = 3 + 3 + 4, 3 × 3 × 4 = 36
解题思路+代码实现
方法一:数学规律
由数学推导得到:尽可能将绳子以长度 为3 等分为多段时,乘积最大。
这道题和上道题的区别就是数据范围变大了,在I 的第二种解法上进行取模即可,注意的是要使用long类型。
public int cuttingRope(int n) {
if (n <= 3) {
return n - 1;
}
if (n == 4) {
return 4;
}
long ans = 1;
while (n > 4) {
ans *= 3;
ans %= 1000000007;
n = n - 3;
}
return (int)(ans*n%1000000007);
// 最后n的值只有可能是:2、3、4。而2、3、4能得到的最大乘积恰恰就是自身值
// 因为2、3不需要再剪了(剪了反而变小);4剪成2x2是最大的,2x2恰巧等于4
}
剑指 offer 15:二进制中1的个数
题目描述
请实现一个函数,输入一个整数(以二进制串形式),输出该数二进制表示中 1 的个数。例如,把 9 表示成二进制是 1001,有 2 位是 1。因此,如果输入 9,则该函数输出 2。(传送门)
示例 1:
输入:00000000000000000000000000001011
输出:3
解释:输入的二进制串 00000000000000000000000000001011 中,共有三位为 '1'。
参考链接:位运算-计算十进制数字转为二进制1的个数
解题思路+代码实现
public int hammingWeight(int n) {
int count = 0;
while (n != 0) {
if((n&1) == 1) {
count++;
}
n = n>>>1;
}
return count;
}