剑指offer(一)
剑指 Offer 03. 数组中重复的数字
题目要求:
找出数组中重复的数字。
在一个长度为 n 的数组nums
里的所有数字都在0~n-1
的范围内。数组中某些数字是重复的,但不知道有几个数字重复了,也不知道每个数字重复了几次。请找出数组中任意一个重复的数字。
思路一:
比较基本的思路有两种:先排序,再找重复的数字,这么做时间复杂度为\(O(nlog n)\),空间复杂度为\(O(1)\);另一种做法是另开一个数组,用于计数,当发现计数大于2的时候,就直接输出,时间复杂度为\(O(n)\),空间复杂度为\(O(1)\)。这里实现的是第二中做法。
class Solution {
public int findRepeatNumber(int[] nums) {
int n = nums.length;
int[] cnt = new int[n];
for (int i = 0; i < n; i++) {
cnt[nums[i]]++;
if (cnt[nums[i]] > 1){
return nums[i];
}
}
return n;
}
}
思路二:
由于所有数字都在0~n-1
的范围内且长度为n,因此可以借助数组的索引来计数。主站的41是一个思路。当nums[i] != i
时,如果nums[i]
与nums[nums[i]]
相等,则找到了重复的数,直接返回。如果不相等,则将它们交换直至nums[i] == i
。由于每次都把每个数字放到它应该在的地方去,因此每个数字a只要一次就能把它放到索引 a 的位置去。故时间复杂度为\(O(n)\),空间复杂度为\(O(1)\)。
class Solution {
public int findRepeatNumber(int[] nums) {
int n = nums.length;
int tmp;
for (int i = 0; i < n; i++) {
while(nums[i] != i){
if(nums[i] == nums[nums[i]]){
return nums[i];
}
tmp = nums[i];
nums[i] = nums[tmp];
nums[tmp] = tmp;
}
}
return n;
}
}
小拓展
本题还有别的问法,比如主站的287. 寻找重复数。
题目要求:
给定一个包含 n + 1 个整数的数组nums
,其数字都在 1 到 n 之间(包括 1 和 n),可知至少存在一个重复的整数。假设只有一个重复的整数,找出这个重复的数。本题还有另外的要求,不能更改原数组,要求算法原地,时间复杂度不能超过\(O(n^2)\)。
思路:
对数组中整数的范围进行二分查找,如果数组中小于mid
的元素数量大于mid
,则重复元素一定在mid
之前(鸽笼原理)。
class Solution {
public int findDuplicate(int[] nums) {
int n = nums.length;
int left = 1, right = n - 1;
// 每个数都在[left, right]的范围内
while(left < right){
int mid = left + (right - left) / 2;
int cnt = 0;
for(int i = 0; i < n ; i++){
if (nums[i] <= mid){
cnt++;
}
}
if (cnt > mid){
right = mid;
}else{
left = mid + 1;
}
}
return left;
}
}
剑指 Offer 04. 二维数组中的查找
题目要求:
在一个n * m
的二维数组中,每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺序排序。请完成一个高效的函数,输入这样的一个二维数组和一个整数,判断数组中是否含有该整数。
思路:
由于该矩阵从左到右是升序的,从上到下是升序的,因此很容易想到要用二分查找。但是对于矩阵来说,不可能做到一次排除掉一半的元素,只能一次排除一行或者一列。
class Solution {
public boolean findNumberIn2DArray(int[][] matrix, int target) {
int m = matrix.length;
if(m == 0){
return false;
}
int n = matrix[0].length;
int row = 0, col = n - 1;
while( row < m && col > -1){
if (target == matrix[row][col]){
return true;
}
else if(target < matrix[row][col]){
col --;
}
else if(target > matrix[row][col]){
row ++;
}
}
return false;
}
}
剑指 Offer 05. 替换空格
题目要求:
请实现一个函数,把字符串 s 中的每个空格替换成"%20"。
思路:
在Java中,字符串是不可变的。要使用StringBuffer
或StringBuilder
才能修改字符串。
class Solution {
public String replaceSpace(String s) {
StringBuffer sb = new StringBuffer();
for (int i = 0; i < s.length(); i++) {
char ch = s.charAt(i);
if (ch == ' ') {
sb.append("%20");
}else{
sb.append(ch);
}
}
return sb.toString();
}
}
剑指 Offer 06. 从尾到头打印链表
题目要求:
输入一个链表的头节点,从尾到头反过来返回每个节点的值(用数组返回)。
思路:
先给链表加一个头节点,遍历一次单链表,求出其长度,并开辟相应长度的数组。接着再遍历一次单链表,并将其中的元素从尾部开始放到数组中。
class Solution {
public int[] reversePrint(ListNode head) {
ListNode a = new ListNode(0);
a.next = head;
int n = 0;
while(head != null){
head = head.next;
n++;
}
head = a.next;
int[] ans = new int[n];
for(int i = n - 1; i > -1; i --){
ans[i] = head.val;
head = head.next;
}
return ans;
}
}
剑指 Offer 07. 重建二叉树
题目要求:
输入某二叉树的前序遍历和中序遍历的结果,请重建该二叉树。假设输入的前序遍历和中序遍历的结果中都不含重复的数字。
思路:
二叉树的根结点在前序遍历中的第一个位置,而在中序遍历中,根结点在“中间”位置,即其左侧都是左子树的结点,右侧都是右子树的结点。根据这一原理,可以不断地找出子树的根结点、左子树和右子树,最终就可以重构出二叉树。
class Solution {
public TreeNode buildTree(int[] preorder, int[] inorder) {
List<Integer> preorderList = new ArrayList<>();
List<Integer> inorderList = new ArrayList<>();
for (int i = 0; i < preorder.length; i++) {
preorderList.add(preorder[i]);
inorderList.add(inorder[i]);
}
return builder(preorderList, inorderList);
}
private TreeNode builder(List<Integer> preorderList, List<Integer> inorderList) {
if (inorderList.isEmpty())
return null;
//前序遍历的第一个值就是根节点
int rootVal = preorderList.remove(0);
//创建跟结点
TreeNode root = new TreeNode(rootVal);
// 递归构建左右子树
// 先找到根节点在中序遍历中的位置,进行划分
int rootindex = inorderList.indexOf(rootVal);
// 构建左子树,范围 [0:rootindex)
root.left = builder(preorderList, inorderList.subList(0, rootindex));
// 构建右子树,范围 (rootindex:最后的位置]
root.right = builder(preorderList, inorderList.subList(rootindex + 1, inorderList.size()));
// 返回根节点
return root;
}
}
剑指 Offer 09. 用两个栈实现队列
题目要求:
用两个栈实现一个队列。队列的声明如下,请实现它的两个函数 appendTail 和 deleteHead ,分别完成在队列尾部插入整数和在队列头部删除整数的功能。(若队列中没有元素,deleteHead 操作返回 -1 )
思路:
栈是先进后出,队列是先进先出。入队操作只需要直接入栈即可。当需要出队时,就必须返回先进栈的那个元素,就需要第二个栈将第一个栈中的元素倒序。所以第一个栈就负责入队,第二个栈负责倒序即可。
class CQueue {
LinkedList<Integer> stack1,stack2;
public CQueue() {
stack1 = new LinkedList<Integer>();
stack2 = new LinkedList<Integer>();
}
public void appendTail(int value) {
stack1.add(value);
}
public int deleteHead() {
if (!stack2.isEmpty()){
return stack2.removeLast();
}
if (stack1.isEmpty()){
return -1;
}
while(!stack1.isEmpty()){
int val = stack1.removeLast();
stack2.add(val);
}
return stack2.removeLast();
}
}
剑指 Offer 10- I. 斐波那契数列
题目要求:
写一个函数,输入 n ,求斐波那契(Fibonacci)数列的第 n 项。斐波那契数列的定义如下:
斐波那契数列由 0 和 1 开始,之后的斐波那契数就是由之前的两数相加而得出。
答案需要取模 1e9+7(1000000007),如计算初始结果为:1000000008,请返回 1。
思路:
这题n的取值范围是0到100,因此用递归的方法肯定是不行的。所以根据斐波那契数列的定义,每次将两个数相加得到下一个斐波那契数。但是这样很快就会超过int型的范围。而取余运算满足分配律,加之前取余和取余后相加是一样的,因此可以每次将两个数字相加的结果取余,就不会超过整型范围了。
public int fib(int n) {
if (n < 2){
return n;
}
int fib0 = 0, fib1 = 1;
int ans = 0;
for (int i = 1; i < n; i++) {
ans = (fib0 + fib1) % 1000000007;
fib0 = fib1;
fib1 = ans;
}
return ans;
}
剑指 Offer 10- II. 青蛙跳台阶问题
题目要求:
一只青蛙一次可以跳上1级台阶,也可以跳上2级台阶。求该青蛙跳上一个 n 级的台阶总共有多少种跳法。
思路:
实际上这一题跟上一题是一样的,只不过在这题中需要自己想出递推公式,而没有现成的递推公式。依题意,青蛙每次可以跳一级或者两级台阶,因此到达最后一级台阶时,要么是跳的一级,要么是跳的两级。因此
与上题是一样的。
public int numWays(int n) {
if (n < 2){
return 1;
}
if (n == 2){
return 2;
}
int a = 1, b = 2, ans = 0;
for (int i = 3; i <= n; i++) {
ans = (a + b) % 1000000007;
a = b;
b = ans;
}
return ans;
}
剑指 Offer 11. 旋转数组的最小数字
题目要求:
把一个数组最开始的若干个元素搬到数组的末尾,我们称之为数组的旋转。输入一个递增排序的数组的一个旋转,输出旋转数组的最小元素。例如,数组[3,4,5,1,2]
为[1,2,3,4,5]
的一个旋转,该数组的最小值为1。
思路:
二分查找。实际上旋转数组是将一个排序数组分成两个排序数组,且旋转数组中的最小数字即为右边排序数组中的最小值。
- 当
numbers[mid]>numbers[right]
时,说明mid
在左边的排序数组中,此时令left = mid + 1
。 - 当
numbers[mid]<numbers[right]
时,说明mid
在右边的排序数组中,此时令right = mid
。 - 当
numbers[mid]=numbers[right]
时,这种情况是因为数组中存在重复的元素,此时将右指针减一。
这么一来就可以保证右指针一直在右边的排序数组中,而左指针在算法结束时到达右边的排序数组中的第一个元素。
class Solution {
public int minArray(int[] numbers) {
int n = numbers.length;
int left = 0, right = n - 1;
while(left < right){
int 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[left];
}
}
剑指 Offer 12. 矩阵中的路径
题目要求:
请设计一个函数,用来判断在一个矩阵中是否存在一条包含某字符串所有字符的路径。路径可以从矩阵中的任意一格开始,每一步可以在矩阵中向左、右、上、下移动一格。如果一条路径经过了矩阵的某一格,那么该路径不能再次进入该格子。例如,在下面的3×4的矩阵中包含一条字符串“bfce”的路径。
[["a","b","c","e"],
["s","f","c","s"],
["a","d","e","e"]]
但矩阵中不包含字符串“abfb”的路径,因为字符串的第一个字符b占据了矩阵中的第一行第二个格子之后,路径不能再次进入这个格子。
思路:
这题需要用DFS递归地去寻找矩阵中符合条件的路径。使用字符'\0'
作为该位置的元素已被访问过的标记。在使用递归函数时,系统会用栈记住函数的执行状态,因此只要在算法关键步骤完成后将矩阵恢复即可,后续的递归就不会受到影响。矩阵中的每一个元素都要依次作为递归的起点,向上下左右四个方向搜索符合条件的路径。当索引超出数组范围或矩阵元素与字符串对应元素不等或该处的元素已经被访问过,说明当前路径一定不是复合条件的路径,就直接返回false
。只有当上面的条件都不满足且字符串已经遍历到了最后一个元素,才返回true
。
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;
}
board[i][j] = '\0';
boolean ans = 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] = word[k];
return ans;
}
}
剑指 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。请问该机器人能够到达多少个格子?
思路:
仍然使用深度优先算法。方格中的位置分为两种,一种是可达的格子,可达的格子一定是与起点[0,0]
连通的(通过可达的格子连得起来),另一种是就算其数位之和小等于k,但是机器人也无法通过别的格子到达该格子,为不可达的格子。而机器人在网格中只需要向左或者向下就可以到达所有的结点,因此计算时就只需要向下和向右方向搜索即可。而反过来说,只要一个格子上方或左方是可达的且这个格子也满足数位之和小等于k,那么它也是可达的。
class Solution {
public int movingCount(int m, int n, int k) {
boolean[][] visited = new boolean[m][n];
// 默认值为false
return dfs(visited, m, n, k, 0, 0);
}
private int dfs(boolean[][] visited, int m, int n, int k, int i, int j) {
if(i >= m || j >= n || visited[i][j] || bitSum(i) + bitSum(j) > k){
return 0;
}
visited[i][j] = true;
return 1 + dfs(visited, m, n, k, i + 1, j) + dfs(visited, m, n, k, i, j + 1) ;
}
private int bitSum(int n) {
int sum = 0;
while(n > 0) {
sum += n % 10;
n /= 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。
思路一:
动态规划。对于一段长度为i
的绳子,先切一段长度为j
的,如果剩下的绳子不再剪了,总的乘积就是j * (i - j)
,如果要继续剪乘积就是j * dp[i - j]
。只要遍历1 <= j < i
就可以求出最大的乘积。于是状态转移方程可以写为
class Solution {
public int cuttingRope(int n) {
int[] dp = new int[n + 1];
for (int i = 2; i < n + 1; i++) {
for (int j = 1; j < i; j++) {
dp[i] = Math.max(dp[i], Math.max(j * (i - j), j * dp[i - j]));
}
}
return dp[n];
}
}
思路二:
数学方法。根据中学的知识,越接近的数乘积是越大的。在本题中可以证明将绳子分为接近3的小段时,最终的乘积最大。
class Solution {
public int cuttingRope(int n) {
if(n <= 3){
return n - 1;
}
int a = n/3, b = n % 3;
if (b == 0){
return (int)Math.pow(3, a);
}
if (b == 1){
return (int)Math.pow(3, a - 1) * 4;
}
return (int)Math.pow(3, a) * 2;
}
}
剑指 Offer 14- II. 剪绳子 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。
思路:
上一题中 n 的取值范围是 2 到 58,而本题的取值范围是 2 到 1000,因此本题的结果会超过整数的范围。通过快速幂取余,可以解决这一问题。根据取余的性质有
根据以上的公式,可以分为以下两种情况:
根据上一题中的贪心算法,可以先算出\(3^{n/3 - 1}\),再根据\(n%3\)返回结果。
class Solution {
public int cuttingRope(int n) {
if(n <= 3){
return n - 1;
}
int b = n % 3, p = 1000000007;
long rem = 1, x = 3;
for(int a = n / 3 - 1; a > 0; a /= 2) {
if(a % 2 == 1) rem = (rem * x) % p;
x = (x * x) % p;
}
if(b == 0){
return (int)(rem * 3 % p);
}
if(b == 1){
return (int)(rem * 4 % p);
}
return (int)(rem * 6 % p);
}
}
剑指 Offer 15. 二进制中1的个数
题目要求:
请实现一个函数,输入一个整数(以二进制串形式),输出该数二进制表示中 1 的个数。例如,把 9 表示成二进制是 1001,有 2 位是 1。因此,如果输入 9,则该函数输出 2。
思路一:
当n
不为0时,将n
与 1 进行与运算,如果此时n
的最后一位是1,则结果是1,将计数器加一,再将n
向右移一位。
public class Solution {
// you need to treat n as an unsigned value
public int hammingWeight(int n) {
int cnt = 0;
while(n > 0) {
cnt += n & 1;
n >>>= 1;
}
return cnt;
}
}
思路二:
对于32位整数来说,上面的代码需要循环32次。而思路二的代码只需要循环cnt
次,其中cnt
为n
中1的个数。n
减去1时,不妨假设n
最右边的1在第m
位,则此时第m
位左侧的数位不变,第m
位变为0,第m
位右边的数位变为1。当n
不为0时,将n
与n - 1
做与运算,且将计数器加一。做完与运算后,第m
位为0,第m
位左侧的数位不变,第m
位右边的数位变为0,二进制中1的个数恰好少了一个。
public class Solution {
// you need to treat n as an unsigned value
public int hammingWeight(int n) {
int cnt = 0;
while(n != 0) {
cnt++;
n = n & (n - 1);
}
return cnt;
}
}
剑指 Offer 16. 数值的整数次方
题目要求:
实现函数double Power(double base, int exponent),求base的exponent次方。不得使用库函数,同时不需要考虑大数问题。
思路:
对于\(x^n\),我们可以将 n 写为其二进制形式\((i_m, i_{m-1}, ..., i_0)_2\),则有
而我们知道,对于一个二进制的数字来说,每个位上的数不是 0 就是 1 ,也就是说只有对应的二进制表示的位上是 1 的对于求\(x^n\)才有贡献。例如 77 的二进制表示为1001101
,\(x^{77} = x*x^4*x^8*x^{64}\),恰好对应了二进制的每一个 1。
需要注意的是,在Java中还需要担心n的范围超出32位整型的情况,因为本题n的范围是\([-2^{31}, 2^{31} - 1]\),因此将n赋值给long型的N。
class Solution {
public double myPow(double x, int n) {
long N = n;
return N >= 0? quickMul(x, N): 1.0 / quickMul(x, -N);
}
private double quickMul(double x, long n){
double ans = 1.0;
double x_pow = x;
while (n > 0){
if (n % 2 == 1){
ans *= x_pow;
}
x_pow *= x_pow;
n /= 2;
}
return ans;
}
}
剑指 Offer 17. 打印从1到最大的n位数
题目要求:
输入数字 n,按顺序打印出从 1 到最大的 n 位十进制数。比如输入 3,则打印出 1、2、3 一直到最大的 3 位数 999。
思路:
LeetCode中的测试用例没有大数问题。要解决大数问题,就要用字符串来模拟数字,这样就不会受各种数据类型的范围限制。初始的字符串长度为n
,从最后一位开始,每次加1并去除字符串前面的0,直到无法继续向上进位了,即99...99
。
public void printNumbers(int n) {
StringBuilder str = new StringBuilder();
// 将str初始化为n个'0'字符组成的字符串
for (int i = 0; i < n; i++) {
str.append('0');
}
while(!increment(str)){
// 去掉左侧的0
int index = 0;
while (index < str.length() && str.charAt(index) == '0'){
index++;
}
System.out.println(str.toString().substring(index));
}
}
public boolean increment(StringBuilder str) {
boolean isOverflow = false;
for (int i = str.length() - 1; i >= 0; i--) {
char s = (char)(str.charAt(i) + 1);
// 如果s大于'9'则发生进位
if (s > '9') {
str.setCharAt(i, '0');
if (i == 0) {
isOverflow = true;
}
}
// 没发生进位则跳出for循环
else {
str.setCharAt(i, s);
break;
}
}
return isOverflow;
}
剑指 Offer 18. 删除链表的节点
题目要求:
给定单向链表的头指针和一个要删除的节点的值,定义一个函数删除该节点。返回删除后的链表的头节点。
思路:
本题中要删除的结点一定是存在的,因此不用考虑结点不存在的情况。需要考虑的是,如果要删除的结点恰好是第一个结点,那么直接返回head.next
即可。
class Solution {
public ListNode deleteNode(ListNode head, int val) {
if (head.val == val){
return head.next;
}
ListNode p = head;
while (p.next.val != val){
p = p.next;
}
ListNode q = p.next;
p.next = q.next;
return head;
}
}
剑指 Offer 21. 调整数组顺序使奇数位于偶数前面
题目要求:
输入一个整数数组,实现一个函数来调整该数组中数字的顺序,使得所有奇数位于数组的前半部分,所有偶数位于数组的后半部分。
思路:
用两个指针指向数组的两端,奇偶数交换即可。
class Solution {
public int[] exchange(int[] nums) {
int n = nums.length;
int left = 0, right = n - 1;
while(left < right){
while (left < right && nums[left] % 2 == 1){
++left;
}
while (left < right && nums[right] % 2 == 0){
--right;
}
if (nums[left] % 2 == 0 && nums[right] % 2 == 1){
int tmp = nums[left];
nums[left] = nums[right];
nums[right] = tmp;
}
}
return nums;
}
}
剑指 Offer 22. 链表中倒数第k个节点
题目要求:
输入一个链表,输出该链表中倒数第k个节点。为了符合大多数人的习惯,本题从1开始计数,即链表的尾节点是倒数第1个节点。例如,一个链表有6个节点,从头节点开始,它们的值依次是1、2、3、4、5、6。这个链表的倒数第3个节点是值为4的节点。
思路:
用两个指针,一个指针先走k步,接着两个指针一起走,直到快的指针走到空值。
class Solution {
public ListNode getKthFromEnd(ListNode head, int k) {
ListNode L = new ListNode(0);
L.next = head;
ListNode pre = L, q = L;
while(k > 0){
q = q.next;
--k;
}
while (q != null){
pre = pre.next;
q = q.next;
}
return pre;
}
}
剑指 Offer 24. 反转链表
题目要求:
定义一个函数,输入一个链表的头节点,反转该链表并输出反转后链表的头节点。
思路:
新构造一个头结点,将其指针指向原链表的第一个结点。从头到尾遍历这个链表,将遍历到的结点放到头结点后面。
class Solution {
public ListNode reverseList(ListNode head) {
if (head == null){
return head;
}
ListNode l1 = new ListNode(0);
l1.next = head;
ListNode p = head.next;
l1.next.next = null;
while (p != null){
ListNode q = p;
p = p.next;
q.next = l1.next;
l1.next = q;
}
return l1.next;
}
}
剑指 Offer 25. 合并两个排序的链表
题目要求:
输入两个递增排序的链表,合并这两个链表并使新链表中的节点仍然是递增排序的。
思路:
简单的归并。注意把剩下的结点加入最终的链表。
class Solution {
public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
ListNode head = new ListNode(0);
ListNode p = head;
while(l1 != null && l2 != null){
if (l1.val <= l2.val){
p.next = l1;
l1 = l1.next;
}else {
p.next = l2;
l2 = l2.next;
}
p = p.next;
}
p.next = l1 == null ? l2: l1;
return head.next;
}
}