剑指offer_edition2刷题记录
- 写在前面:此博客记录刷剑指offer题中遇到的困难和总结,以及过程中难以理解的地方,其中*代表需要过段时间回过头再看的题
-
- Q7 重建二叉树*(20210421)
- Q8 二叉树的下一个节点(原书涉及到指针,暂时跳过)
- Q9 两个栈实现一个队列
- 附加题 两个队列实现一个栈
- Q10 斐波那契数列
- 附加题:青蛙跳台阶
- 附加题:快速排序
- Q11 旋转数组的最小数字*(20210424)
- Q12 矩阵中的路径*(20210426)
- Q13 机器人的运动范围*(20210428)
- Q14-1 剪绳子
- Q14-2 剪绳子
- Q15 二进制中1的个数
- Q16 数值的整数次方*(20210518)
- Q17打印从1到最大的n位数
- Q18 删除链表的节点
- Q19正则表达式匹配
- Q20 表示数值的字符串
- Q21 调整数组顺序使奇数位于偶数前面
- Q22 链表中倒数第k个节点
- Q24 反转链表
- Q25 合并两个排序的链表
- Q26 树的子结构**20210609
- Q27 二叉树的镜像***20210610
- Q28 对称的二叉树
- Q32-I 从上到下打印二叉树
- Q32 - II. 从上到下打印二叉树 II**20210610
- Q32-III 从上到下打印二叉树III
- Q33 二叉搜索树的后序遍历序列****20210714
- Q34 二叉树中和为某一值的路径
- Q35 复杂链表的复制
- Q36二叉搜索树与双向链表***20210714
- Q39 数组中出现次数超过一半的数字
- Q38字符串的排列20210702****
- Q40 最小的k个数
- Q42 连续子数组的最大和20210702****
- Q44 数字序列中某一位的数字
- Q45 把数组排成最小的数****0703
- Q46 把数字翻译成字符串
- Q47 礼物的最大价值
- Q48最长不含重复字符的子字符串
- Q49丑数
- Q50 第一个只出现一次的字符
- Q52 两个链表的第一个公共节点
- Q53-1 在排序数组中查找数字-1
- Q53-2 0~n-1中缺失的数字
- Q54 二叉搜索树的第k大节点
- Q55-1 二叉树的深度
- Q55-2 平衡二叉树***0722
- Q56-1 数组中数字出现的次数
- Q56-2 数组中数字出现的次数 2
- Q57 和为s的两个数字
- Q57-2 和为s的连续正数序列
- Q58-1 翻转单词顺序
- Q58-2 左旋转字符串
- Q59-1 滑动窗口的最大值
- Q59-2 队列的最大值
- Q60 n个骰子的点数
- Q61 扑克牌中的顺子
- Q62 圆圈中最后剩下的数字(20230222回过头来看这道题,发现并不简单,可递归可dp,记住公式即可,浪费太多时间想细节没得必要,dp[n]=(dp[n-1]+m)/n)
- Q63 股票的最大利润
- Q64 求1+2+3+...+n
- Q65 不用加减乘除做加法
- Q66 构建乘积数组
- Q67 把字符串转换成整数
- Q68-1 二叉搜索树的最近公共祖先
- Q68-2 二叉树的最近公共祖先*****202107312233
写在前面:此博客记录刷剑指offer题中遇到的困难和总结,以及过程中难以理解的地方,其中*代表需要过段时间回过头再看的题
Q7 重建二叉树*(20210421)
此题属于力扣中等难度题,初次做此题实在有难度,还是自己太菜了555555。不看题解实在是写不出来
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
class Solution {
HashMap<Integer, Integer> map = new HashMap<>();
public TreeNode buildTree(int[] preorder, int[] inorder) {
for(int i = 0; i < inorder.length; i++)
map.put(inorder[i], i);
return recur(preorder,0, 0, inorder.length - 1);
}
TreeNode recur(int [] preorder,int root, int left, int right) {
if(left > right) return null; // 递归终止
TreeNode node = new TreeNode(preorder[root]); // 建立根节点
int i = map.get(preorder[root]); // 划分根节点、左子树、右子树
node.left = recur(preorder,root + 1, left, i - 1); // 开启左子树递归
node.right = recur(preorder,root + i - left + 1, i + 1, right); // 开启右子树递归
return node; // 回溯返回根节点
}
}
代码思路:
首先需要了解中序遍历和前序遍历的基本原理。
然后根据前序遍历的数组的第一个值即为根节点,然后通过此根节点查找中序遍历数组中对应的索引,这里为了减少时间复杂度,引入了哈希map,也就是先利用中序数组new一个hashmap,再得到中序数组中的根节点索引。
再定义一个递归调用的函数,首先确定递归终止条件:
即中序数组的左边界大于右边界。
再建立根节点,确定中序数组中的根索引,从而确定划分左右子树。然后开始递归调用,即确定左子树:node.left = recur()
右子树:node.right = recur(),主要是要搞清楚里面的参数代表的意思,如:前序数组,前序数组的根索引,中序数组的左边界,中序数组的右边界
Q8 二叉树的下一个节点(原书涉及到指针,暂时跳过)
Q9 两个栈实现一个队列
先放代码:
class CQueue {
Stack<Integer> stackA;
Stack<Integer> stackB;
public CQueue() {
stackA = new Stack<Integer>();
stackB = new Stack<Integer>();
}
public void appendTail(int value) {
stackA.push(value);
}
public int deleteHead() {
if (stackB.isEmpty()){
if(stackA.isEmpty()){
return -1;
}
while(!stackA.isEmpty()){
stackB.push(stackA.pop());
}
}
return stackB.pop();
}
}
/**
* Your CQueue object will be instantiated and called as such:
* CQueue obj = new CQueue();
* obj.appendTail(value);
* int param_2 = obj.deleteHead();
*/
此题需要注意的就是分清楚两个栈的各自作用,其中A的作用就是入栈即入队操作,B的作用就是出队的操作,不过此处需要注意的就是,在出队操作时,需要首先判断B是否为空,若为空,则再进行判断A,若也为空,则返回-1;若A不为空,则将A中的元素弹出到B,顺序刚好就反转回来了,然后再弹出B的元素;若B不为空,则弹出B的元素即为出队元素。
附加题 两个队列实现一个栈
还是先上代码把
class MyStack {
Queue<Integer> queue1;
Queue<Integer> queue2;
public MyStack() {
queue1 = new LinkedList<Integer>();
queue2 = new LinkedList<Integer>();
}
public void push(int x) {
queue2.offer(x);
while (!queue1.isEmpty()) {
queue2.offer(queue1.poll());
}
Queue<Integer> temp = queue1;
queue1 = queue2;
queue2 = temp;
}
public int pop() {
return queue1.poll();
}
public int top() {
return queue1.peek();
}
public boolean empty() {
return queue1.isEmpty();
}
}
队列1作为主队列,队列2作为辅助队列。当入栈时,首先将元素放入队列2,然后将队列1中的元素全部出队放入队列2中,这样子就满足了后进的在最底部,即后进先出的原则,然后将队列1和2互换,再将队列1进行出队操作,就属于后进先出的栈弹出操作了。
Q10 斐波那契数列
老规矩先上代码:
class Solution {
public int fib(int n) {
if(n == 0) return 0;
int[] dp = new int[n + 1];
dp[0] = 0;
dp[1] = 1;
for(int i = 2; i <= n; i++){
dp[i] = dp[i-1] + dp[i-2];
dp[i] %= 1000000007;
}
return dp[n];
}
}
此问题虽然是典型的递归教科书讲解题,但是如果直接用递归来写的话,时间效率太低,当次数太高时,会有很多重复计算,因此,可以使用动态规划思想,将问题化为各个子问题,然后根据自底向上的循环,依次计算求值,注意到这儿使用了一个数组保存之前的值,提高了效率。
附加题:青蛙跳台阶
代码:
class Solution {
public int numWays(int n) {
if(n<=1){
return 1;
}
if(n==2){
return 2;
}
int []s = new int [n+1];
s[0] = 1;
s[1] = 1;
s[2] = 2;
for(int i = 3;i<=n;i++){
s[i] = (s[i-1] +s[i-2]) % 1000000007;
}
return s[n];
}
}
这个可是双百beats哦,还是不错了。
解题思路:
其实此题就是求斐波拉契数列的封装。最关键的就是要明白青蛙跳最后一步的时候,要么跳一级台阶,要么跳两级台阶,因此可化为跳n级台阶的时候f(n) = f(n-1)+f(n-2)。借鉴动态规划的思想,从下往上循环,避免使用递归重复计算,提高效率,同时使用一个数组进行保存之前的值,减少多个变量的定义,提高效率。还有就是初始值需要单独列出来,跟斐波拉契的初始值有一丢丢区别。
附加题:快速排序
快排是20世纪十大最伟大的算法之一,可见其重要性了。因此很多公司面试时,会经常考察这个题。其中最需要注意的就是,此算法涉及到递归和指针两个知识点。在交换数据顺序的时候,可以采用单边循环法或者双边循环法,而因为单边循环法只需要一个指针作指引,且代码简单得多。因此,这里着重记录单边循环法。主要看partition函数,只有一个for循环,用于寻找比基准元素小的元素,然后移动mark索引,并交换两个元素。循环完后再将基准元素和mark索引对应的元素进行交换,并返回基准元素索引mark。代码如下:
class Solution{
public static void quickSort(int[] arr, int startIndex,
int endIndex) {
// 递归结束条件:startIndex大于或等于endIndex时
if (startIndex >= endIndex) {
return;
}
// 得到基准元素位置
int pivotIndex = partition(arr, startIndex, endIndex);
// 根据基准元素,分成两部分进行递归排序
quickSort(arr, startIndex, pivotIndex - 1);
quickSort(arr, pivotIndex + 1, endIndex);
}
/**
* 分治(单边循环法)
* @param arr 待交换的数组
* @param startIndex 起始下标
* @param endIndex 结束下标
*/
private static int partition(int[] arr, int startIndex,
int endIndex) {
// 取第1个位置(也可以选择随机位置)的元素作为基准元素
int pivot = arr[startIndex];
int mark = startIndex;
for(int i=startIndex+1; i<=endIndex; i++){
if(arr[i]<pivot){
mark ++;
int p = arr[mark];
arr[mark] = arr[i];
arr[i] = p;
}
}
arr[startIndex] = arr[mark];
arr[mark] = pivot;
return mark;
}
}
Q11 旋转数组的最小数字*(20210424)
此题最简单最直接的做法,可能就是直接来个遍历,找出最小的值,在力扣上也试过这种做法,奇怪的是,本来时间复杂度为O(n),效率不算高,但是结果确实击败了100%,估计是测试的例子刚好没遇到极端的情况吧。此题虽然是简单题,但想要优化时间复杂度,降为logN,需要用到二分查找法,涉及到对两个指针的操作,还是有一点难度的,至少不看题解,我是想不到的。先放代码:
class Solution {
public int minArray(int[] numbers) {
int i = 0, j = numbers.length - 1;
while (i < j) {
int m = (i + j) / 2;
if (numbers[m] > numbers[j]) i = m + 1;
else if (numbers[m] < numbers[j]) j = m;
else j--;
}
return numbers[i];
}
}
其中两个指针为i,j,用来寻找旋转分界点。这道题要利用旋转数组的特性,也就是左边数组值肯定大于右边数组值。因此,判断二分中界点m的位置很关键。当nums[m]>nums[j]时,m肯定在左边数组,因此可将i缩小范围,令i=m+1;当nums[m]<nums[j]时,m肯定位于右边数组,可将j的范围缩小,令j=m;
当nums[m]==nums[j]时,特别注意,这时候无法判断m处在那个位置,因此只能保守的将j缩小一个位置,即令j=j-1;当i=j时,跳出循环,并将nums[i]返回即为最小值。
还有一种特殊情况:当nums[m]==nums[j],可以证明左边数组或者右边数组都相等。**此时可跳出二分查找,直接使用线性查找,即遍历,更快。**代码如下:
class Solution {
public int minArray(int[] numbers) {
int i = 0, j = numbers.length - 1;
while (i < j) {
int m = (i + j) / 2;
if (numbers[m] > numbers[j]) i = m + 1;
else if (numbers[m] < numbers[j]) j = m;
else {
int x = i;
for(int k = i + 1; k < j; k++) {
if(numbers[k] < numbers[x]) x = k;
}
return numbers[x];
}
}
return numbers[i];
}
}
Q12 矩阵中的路径*(20210426)
阿西,此题好难,看题解都要看好半天才能理解。害。。。。。
言归正传,本题主要涉及到递归。看题解说此题涉及到–深度优先搜索(DFS)+剪枝。这啥玩意儿,戴个这么高深的术语帽子,来吓人,是生怕我们看懂嘛,哼╭(╯^╰)╮
还是先放代码把,结合代码来看容易多了:
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 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] = word[k];
return res;
}
}
说白了,对于一个矩阵,先弄两个for循环,以遍历所有的元素,也就是看所有的字符路径是否有符合要求的路径。然后对于每一次搜索,需要知道四个参数:原矩阵,目标字符数组,矩阵中的元素索引i和j,也就是第几行第几列,和目标字符索引k。对于DFS搜索函数,其实是个递归调用函数,首先我们需要知道递归调用终止条件:数组越界,即i和j<0或者>=数组长度或者不等于目标字符,则return false;当继续执行下一步时,说明当前访问字符与目标字符相等,于是需要判断k是否等于目标字符数组的最后一个字符索引,若等于的话,则立即返回return true;然后为了避免对访问后的字符重复进行访问,我们将其修改为board[i][j] = ‘\0’,然后再以下、上、右、左的顺序进行下一步搜索,也就是递归调用搜索函数,只要搜索到一条可行路径即可,所以用或||连接,然后将访问过的字符进行恢复,**即board[i][j] = word[k];这一步也很关键,不然原矩阵会被改变,影响下一次的搜索。**函数中数组作为参数的 时候,是传递的 引用,所有一改会跟着改。
Q13 机器人的运动范围*(20210428)
先上代码:
class Solution {
public int movingCount(int m, int n, int k) {
boolean[][] visited = new boolean[m][n];
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;
if (n<10) sum= n;
else if(n==100) sum =1;
else sum = n%10+n/10 ;
return sum;
}
}
同样是不看题解不会做系列,啊啊啊啊啊啊啊啊,烦
此题属于路径搜索问题,同样可以使用递归的方法,采用深度优先搜索(DFS),其中关键的就是需要知道递归的参数,终止条件,这里递归参数包括辅助矩阵visit(用于保存已经访问了的路径),当前格子的i,j索引和数位之和限制k,数位之和计算比较简单,不再赘述。递归终止条件为超出索引界限、visit[][]为true、数位之和大于k,这些条件之间用或连接,然后标记访问数组为true,然后返回值为1+向左和向右搜索的结果,其中1代表当前起始格子,。至于为什么返回值为1+向左向右搜索的结果,可以结合leetcode题解的图来形象理解。
class Solution {
public int movingCount(int m, int n, int k) {
boolean[][] visited = new boolean[m][n];
return dfs(visited, m, n, k, 0, 0);
}
//递归函数首先要确定其定义代表什么,也就是返回值。
//然后根据返回值含义先递归遍历。
//再确定合适的边界条件,即终止递归条件。
//其中还需要注意细节问题,也就是是否需要标记已遍历的路径或者恢复现场。
//此题dfs返回的就是从当前坐标开始,一共有多少个格子可达,其总数=1+右边下+下边还有几个格子可达。
//因此,1+dfs(下边)+dfs(右边)即为格子数。
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;
}
}
Q14-1 剪绳子
此题主要涉及到一些基本的数学推导,也是分为动态规划和贪心算法两种方式。最容易想到的就是贪心算法,虽然我也是看了题解才明白的。。。。。害,真的是感叹每日一菜。
贪心算法中的解题要点就是每次剪绳子的长度最好为3,如果剩下长度是4的话,则剪为2*2=4。然后程序里面首先用if定义绳子长度小于5的情况,然后一个循环,实现maxProduct的计算,每次n为n-3,直到n<5,则跳出循环,再将maxProduct*n即为所求。代码如下:
class Solution {
public int cuttingRope(int n) {
int maxProduct = 1;
if(n==2){
return 1;
}
if(n==3) return 2;
if(n==4) return 4;
while(n>=5){
maxProduct *= 3;
n -= 3;
}
return maxProduct*n;
}
}
Q14-2 剪绳子
此题主要涉及到防止大数越界的问题,需要进行求模,整体思路无太大变化。
需要注意的就是注意到类型强制转换,不然会报错。
class Solution {
public int cuttingRope(int n) {
long maxProduct = 1L;
if(n==2){
return 1;
}
if(n==3) return 2;
if(n==4) return 4;
int p = (int)1e9+7;
while(n>=5){
maxProduct = (maxProduct*3)%p;
n -= 3;
}
return (int)(maxProduct*n%p);
}
}
Q15 二进制中1的个数
此题主要涉及到位运算,通过与1进行位运算,判断该位是否为1。由于采用右移的话,当遇到最高位为1的时候,会出现死循环,FFFFFFF的情况,因此,这里通过将1与原数字的从低到高位进行与运算,判断是否为0,再进行计数即可得到1的个数。
public class Solution {
// you need to treat n as an unsigned value
public int hammingWeight(int n) {
int m =1;
int count=0;
for(int i=0;i<32;i++){
if((m & n) != 0){
count++;
}
m= m <<1;
}
return count;
}
}
此题最逆天的解法就是知道n&(n-1)即代表将n最右边的1化为0,这个规律很有用,很多二进制的题都有涉及。
Q16 数值的整数次方*(20210518)
前段时间论文返修意见回来了,然后就一直忙着改论文,都没怎么刷题了,感觉又落下了好多诶。今天把论文改完了,然后又开始接着刷题了。言归正传,这道题最直接的方法就是用循环的方式计算数值的整数次方,如下所示:
class Solution {
public double myPow(double x, int n) {
double result =1;
if (x==0){
if(n!=0)
return result =0;
else {
result = -1;}
}
if(n<0){
n=-n;
for(int i=0;i<n;i++){
result *= x;
}
result= 1/result;
}
else if(n==0){
result=1;}
else{
for(int i=0;i<n;i++){
result *= x;
}
}
return result;
}
}
这样做的结果就是计算时间过长,无法通过测试。还是只有看题解,题解中提到两种解法,一种涉及到数学推导,直接略过。故直接看第二种没那么反人类的解法。但还是需要用到递归的手段。代码如下:
class Solution {
public double myPow(double x, int n) {
long m = Math.abs((long)n);
double num =calculate(x,m);
if(n < 0) return 1 / num;
return num;
}
public double calculate(double x,long n){
if(n == 0) return 1;
if(n == 1) return x;
double a = calculate(x,n>>1);
if(n % 2 == 0){
return a * a;
}
return a * a * x;
}
}```
![双百击杀](https://img-blog.csdnimg.cn/20210518210603301.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2ppYW5kYW5kaWFuXw==,size_16,color_FFFFFF,t_70)
## Q17 打印从1到最大的n位数
此题因为看力扣归为简单题,因此一上来就想到定义一个数组,然后for循环添加。代码如下:
```java
class Solution {
public int[] printNumbers(int n) {
int arrLen = 0;
arrLen =(int)Math.pow(10,n)-1;
int []res =new int[arrLen];
for(int i=0;i<arrLen;i++){
res[i]=i+1;
}
return res;
}
}
虽然顺利通过了测试,但是剑指offer上面主要考察大数问题,要将其转化为字符串的形式。当然这也是不看题解所想不出来的。阿西吧~~~~~
Q17打印从1到最大的n位数
这道题乍一看,以为很简单,直接定义一个一维数组,长度为10^n-1,然后循环打印出来即可。可事实上没那么简答,哎。。。。
要考虑到大数问题,也就是当n很大时,会超出 int的表示范围。因此,需要用字符串来辅助。这里面又涉及到递归的知识,需要先固定高位,再固定低位。通过循环实现,这里递归终止的条件为最低位被固定。目前考虑到的问题尚能理解,结果一看题解,还要考虑删除多余的0以及转为int型。。。。。实在是脑力不够了,,,,卒。先放在这儿吧,以后再回过头来看。