内容来源于自己的刷题笔记,对一些题目进行方法总结,用 java 语言实现。
6.剑指 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]
-
思路:
主要实现从队尾插入整数,从队头删除整数,没有元素返回-1
- 定义两个 Linkedlist,实现 Queue 接口,Stack 能做的事情它能做,且扩容消耗少
- 栈为先进先出,则第一个栈 stack1 栈顶为队尾,插入操作即为第一个栈的入栈操作;第二个栈 stack2 栈顶为队头,删除操作即为第二个栈的出栈操作
-
代码实现:
/** * 如果使用Stack的方式来做这道题,会造成速度较慢; * 原因的话是Stack继承了Vector接口,而Vector底层是一个Object[]数组,那么就要考虑空间扩容和移位的问题了。 * 可以使用LinkedList来做Stack的容器,因为LinkedList实现了Deque接口, * 所以Stack能做的事LinkedList都能做,其本身结构是个双向链表,扩容消耗少。 */ private LinkedList<Integer> stack1; private LinkedList<Integer> stack2; /** * 根据栈先进后出的特性,我们每次往第一个栈里插入元素后, * 第一个栈的顶部元素是最后插入的元素,第一个栈的底部元素是下一个待删除的元素。 * 为了维护队列先进先出的特性,我们引入第二个栈,用第二个栈维护待删除的元素, * 在执行删除操作的时候我们首先看下第二个栈是否为空。如果为空, * 我们将第一个栈里的元素一个个弹出插入到第二个栈里,这样第二个栈里元素的顺序就是待删除的元素的顺序, * 要执行删除操作的时候我们直接弹出第二个栈的元素返回即可。 * */ public CQueue() { stack1 = new LinkedList<>(); stack2 = new LinkedList<>(); } public void appendTail(int value) { stack1.push(value); } public int deleteHead() { if (stack2.isEmpty()){ while (!stack1.isEmpty()){ stack2.push(stack1.pop()); } } if (stack2.isEmpty()){ return -1; }else{ return stack2.pop(); } }
7.剑指 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
-
思路:
- 递归法:fun(n) = fun(n - 1) + fun(n - 2),以 f(0)、f(n) 为终止条件,存在大量重复计算
- 记忆化递归法:在递归法的基础上,新建一个长度为 n 的数组,用于存储 fun(0) 至 fun(n) 的数字值,但需要 O(N) 的存储空间
- 动态规划:以 fun(n + 1) = fun(n) + fun(n - 1) 为转移方程,循环使用三个变量进行存储访问,逻辑与第二个类似。
-
代码展示:
public int fib(int n) { return fib(n, new HashMap()); } /** * 递归计算 * @param n * @param map * @return */ public int fib(int n, Map<Integer, Integer> map) { if (n < 2) return n; if (map.containsKey(n)) return map.get(n); int first = fib(n - 1, map)%1000000007; map.put(n - 1, first); int second = fib(n - 2, map)%1000000007; map.put(n - 2, second); int res = (first + second)%1000000007; map.put(n, res); return res; } /** * 递归记忆法 * @param n * @return */ public int fib1(int n) { int[] result = {0,1}; if(n < 2) return result[n]; int[] sum = new int[n]; sum[0] = 1; sum[1] = 1; for (int i = 2;i < n;i++){ sum[i] = (sum[i-1] + sum[i-2])%1000000007; } return sum[n - 1]; } /** * 动态规划 * @param n * @return */ public int fib2(int n){ int a = 0,b = 1,sum; for (int i = 0;i < n;i ++){ sum = a + b; a = b; b = sum; } return a; }
8.剑指 Offer 10- II. 青蛙跳台阶问题
-
问题描述:
一只青蛙一次可以跳上1级台阶,也可以跳上2级台阶。求该青蛙跳上一个 n 级的台阶总共有多少种跳法。
答案需要取模 1e9+7(1000000007),如计算初始结果为:1000000008,请返回 1。
示例 1:
输入:n = 2 输出:2
示例 2:
输入:n = 7 输出:21
示例 3:
输入:n = 0 输出:1
-
思路:
青蛙的最后一步只有一级,两级两种情况。
- 当为一级台阶时,此情况共有 fun(n - 1) 种跳法
- 当为二级台阶时,此情况共有 fun(n - 2) 种跳法
与上一提类似,只是初始值不一样,fun(1) = 1,fun(2) = 1
-
代码实现:
/** * 把n级台阶时的跳法看成n的函数,记为f(n),当n>2, * 第一次跳就有两种不同的选择: * 一是第一次只跳一级,此时跳法数目等于后面剩下的n-1级台阶的跳法数目,即为f(n-1)。 * 二是第一次跳2级,此时跳法数目等于后面剩下的n-2级台阶的跳法数目,即为f(n-2)。 * 因此,n级台阶的不同跳法的总数f(n)=f(n-1)+f(n-2),其实就是斐波那契数列 * * @param n * @return */ public int numWays(int n) { return numWays(n, new HashMap()); } /** * 递归法 * @param n * @param map * @return */ private int numWays(int n, Map<Integer, Integer> map){ if (n < 2) return 1; if (map.containsKey(n)) return map.get(n); int first = numWays(n - 1, map)%1000000007; map.put(n - 1, first); int second = numWays(n - 2, map)%1000000007; map.put(n - 2, second); int res = (first + second)%1000000007; map.put(n, res); return res; } /** * 递归记忆法 * @param n * @return */ public int numWays1(int n){ int[] result = {1,1}; if(n < 2) return result[n]; int[] sum = new int[n]; sum[0] = 1; sum[1] = 2; for (int i = 2;i < n;i++){ sum[i] = (sum[i-1] + sum[i-2])%1000000007; } return sum[n - 1]; } /** * 动态规划 * @param n * @return */ public int numWays2(int n){ int a = 1,b = 1,sum; for (int i = 0;i < n;i ++){ sum = (a + b)%1000000007 ; a = b%1000000007; b = sum%1000000007; } return a; }
9. 剑指 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
-
思路:
-
线性查找:循环过程中出现后一个元素小于前一个,即为最小元素
-
二分查找:
声明 i,j 指向数组两端,设 m = (1 + j)/2 为二分的中点
- nums[m] > nums[j] 时,应在数组右边查找
- nums[m] < nums[j] 时,应在数组左边查找
- nums[m] = nums[j] 时,无法判断,执行 j = j - 1缩小范围,或直接采用线性查找,因为此时,数组有一半相等甚至更多
-
-
代码实现:
/** * 线性查找 * @param numbers * @return */ public int minArray(int[] numbers) { int N = numbers.length; boolean flag = false; int i; for (i = 0;i < N;i++){ if (i < N-1 && less(numbers[i],numbers[i+1])){ flag = true; break; } } if (flag) { return numbers[i + 1]; }else { return numbers[0]; } } private boolean less(int left,int right){ return left > right; } /** * 二分查找 * @param numbers * @return */ public int minArray1(int[] numbers) { int low = 0; int high = numbers.length - 1; while (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 { /** * 第三种情况是 numbers[pivot] == numbers[high] * 由于重复元素的存在,我们并不能确定 numbers[pivot] 究竟在最小值的左侧还是右侧, * 因此我们不能莽撞地忽略某一部分的元素。我们唯一可以知道的是,由于它们的值相同, * 所以无论 numbers[high] 是不是最小值,都有一个它的「替代品」numbers[pivot], * 因此我们可以忽略二分查找区间的右端点。 */ high -= 1; } } return numbers[low]; }
10.剑指 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 解析:
- 终止参数:当前元素在矩阵 board 中的行列索引 i 和 j,当前目标字符在 word 中的索引 k
- 终止条件:
- 返回 false:行或列索引越界或当前矩阵元素与目标字符不同或当前矩阵元素已访问过
- 返回 true:k = len(word) - 1,即字符串 word 已经全部匹配
- 递推工作:
- 标记当前矩阵元素:将 board[i][j] 修改为空字符 ‘\0’ ,代表此元素已经被访问过,防止搜索后重复访问
- 搜索下一单元格:朝当前元素的上、下、左、右四个方向递归,使用或连接
- 还原当前矩阵元素:将 board[i][j] 元素还原至初始值,即 word[k]
- 返回值:
- 返回布尔值 res,代表是否搜索到目标字符串
-
代码实现:
/** * 递归参数: 当前元素在矩阵 board 中的行列索引 i 和 j ,当前目标字符在 word 中的索引 k 。 * * 终止条件: * 返回 false : 行或列索引越界 或 当前矩阵元素与目标字符不同 或 当前矩阵元素已访问过 。 * 返回 true : 字符串 word 已全部匹配,即 k = len(word) - 1 。 * * 递推工作: * 标记当前矩阵元素: 将 board[i][j] 值暂存于变量 tmp ,并修改为字符 '/' ,代表此元素已访问过,防止之后搜索时重复访问。 * 搜索下一单元格: 朝当前元素的 上、下、左、右 四个方向开启下层递归,使用 或 连接 (代表只需一条可行路径) ,并记录结果至 res 。 * 还原当前矩阵元素: 将 tmp 暂存值还原至 board[i][j] 元素。 * 回溯返回值: 返回 res ,代表是否搜索到目标字符串。 * * @param board * @param word * @return */ 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++) { //从第一个元素开始进行索引,索引0 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] = '/' 来防止 “走回头路”。 //当匹配字符串不成功时,会回溯返回,此时需要board[i][j] = tmp 来”取消对此单元格的标记”。 //在DFS过程中,每个单元格会多次被访问的, board[i][j] = '/'只是要保证在当前匹配方案中不要走回头路。 board[i][j] = tmp; return res; }