剑指offer_edition2刷题记录

剑指offer_edition2刷题记录

写在前面:此博客记录刷剑指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型。。。。。实在是脑力不够了,,,,卒。先放在这儿吧,以后再回过头来看。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值