《剑指Offer》Java版上篇(面试题1-22,多种解题思路)

        《剑指Offer》前22道题的Java版多个解题思路和代码示例。Github的地址为:https://github.com/hzka/Sword2OfferJava

面试题3:查找数组中重复的数字
问题描述:
       在一个长度为n的数组里的所有数字都在0到n-1的范围内。 数组中某些数字是重复的,但不知道有几个数字是重复的。也不知道每个数字重复几次。请找出数组中任意一个重复的数字。 例如,如果输入长度为7的数组{2,3,1,0,2,5,3},那么对应的输出是第一个重复的数字2。
解题思路:
      (1)较优:使用HashSet来解决。一次循环,使用contains来判断有没有重复元素,若有进行赋值并返回true,否则false。注意必须要import包。Mine

    public boolean duplicate(int numbers[],int length,int [] duplication) {
        HashSet hashset = new HashSet<>();
        for(int i= 0;i

        运行时间:20ms;占用空间:9580k
      (2)先排序后(Arrays.sort(),快速排序算法)查找相邻元素。如果相邻元素相同,则返回true,否则false。注意:假设数组只有零个或者一个元素,那么返回flase,无需进行排序和后面的操作。Mine

public boolean duplicate(int numbers[],int length,int [] duplication) {
        if(length == 0 || length == 1)
            return false;
        Arrays.sort(numbers);       
        for(int i = 0;i < length-1;i++){
            if(numbers[i] == numbers[i+1]){
                duplication[0] = numbers[i];
                return true;
            }
        }
        return false;
}

          时间:38ms,空间:9420k
       (3)充分利用已知条件,boolean只占一位,所以还是比较省的。

    public boolean duplicate(int numbers[], int length, int[] duplication) {
        boolean[] k = new boolean[length];
        for (int i = 0; i < k.length; i++) {
            if (k[numbers[i]] == true) {
                duplication[0] = numbers[i];
                return true;
            }
            k[numbers[i]] = true;
        }
        return false;
    }

          时间:31ms,内存:9700k
知识点:
HashTable、HashSet、HashMap的区别。
参考链接:https://blog.csdn.net/u011109589/article/details/80535412
(1)HashTable与HashMap
       HashTable线程安全,添加synchronized关键字确保同步,后者不安全;前者不可使用null作为key,后者可以;初始容量不同,前者容量16,后者11;计算hash方式不同。一般使用HashMap。
(2)HashMap与HashSet
       除开HashMap和Hashtable外,还有一个hash集合HashSet,有所区别的是HashSet不是key value结构,仅仅是存储不重复的元素,相当于简化版的HashMap,只是包含HashMap中的key而已。通过查看源码也证实了这一点,HashSet内部就是使用HashMap实现,只不过HashSet里面的HashMap所有的value都是同一个Object而已,因此HashSet也是非线程安全的。

面试题4:二维数组中的查找
问题描述:在一个二维数组中(每个一维数组的长度相同),每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺序排序。请完成一个函数,输入这样的一个二维数组和一个整数,判断数组中是否含有该整数。
解题思路:
      (1)自己考虑的比较简单,利用二维数组由上到下,由左到右递增的规律,若target元素小于array[i][0],则元素肯定在i行之前,设置标志位flag,记录i行,然后在前i-1行利用暴力搜索的方法,若找到有,否则没有。注意:需要考虑到array长度为0或array[0]的长度为零的情况。Mine

    public static boolean Find(int target, int[][] array) {
        if (array.length == 0 || array[0].length == 0) return false;
        int flag = array.length;
        for (int i = 0; i < array.length; i++) {
            if (target == array[i][0]) {
                return true;
            } else if (target < array[i][0]) {
                flag = i;
                break;
            }
        }
        for (int i = 0; i < flag; i++) {
            for (int j = 0; j < array[0].length; j++) {
                if (target == array[i][j]) {
                    return true;
                }
            }
        }
        return false;
    }

         时间:173ms,空间:17468k
      (2)把每一行看成有序递增的数组,利用二分查找,通过遍历每一行得到答案,时间复杂度是nlogn。

 public static boolean Find(int target, int[][] array) {
        for (int i = 0; i < array.length; i++) {
            for (int j = 0; j < array[0].length; j++) {
                int low = 0;
                int high = array[0].length - 1;
                while (low <= high) {
                    int mid = (low + high) / 2;
                    if (target < array[i][mid]) {
                        high = mid - 1;
                    } else if (target > array[i][mid]) {
                        low = mid + 1;
                    } else {
                        return true;
                    }
                }
            }
        }
        return false;
}

        时间:186ms;占用内存:17516k
     (3)较优:利用二维数组由上到下,由左到右递增的规律,那么选取右上角或者左下角的元素a[row][col]与target进行比较,当target小于元素a[row][col]时,那么target必定在元素a所在行的左边,即col--;当target大于元素a[row][col]时,那么target必定在元素a所在列的下边,即row++;

public static boolean Find(int target, int[][] array) {
        int row=0;
        int col=array[0].length-1;
        while(row<=array.length-1&&col>=0){
            if(target==array[row][col])
                return true;
            else if(target>array[row][col])
                row++;
            else
                col--;
        }
        return false;
}

         时间:198ms,空间:17308k。

面试题5:替换空格
问题描述:请实现一个函数,将一个字符串中的每个空格替换成“%20”。例如,当字符串为We Are Happy.则经过替换之后的字符串为We%20Are%20Happy。
解题思路:
     (1)较优:查找元素为空格的地方,将其用replace方法进行替换,然后不遍历这些,使用i=i+2。最后toString方法。注意:牛客网没有代码调试和代码补全功能,需要记住有些API,譬如:stringbuffer.length;str.charat(i);str.replace(begin,end,”%20”);等。Mine

public static String replaceSpace(StringBuffer str) {
        for(int i = 0;i

         运行时间:26ms;占用内存:9376k
     (2)不考虑java里现有的replace方法。从前往后替换,后面的字符要不断往后移动,要多次移动,所以效率低下。从后往前,先计算需要多少空间,然后从后往前移动,则每个字符只为移动一次,这样效率更高一点。

    public static String replaceSpace(StringBuffer str) {
        int spacenum = 0;//spacenum为计算空格数
        for(int i=0;i=0 && indexold

         运行时间:20ms;占用内存:9524k
      (3)一行搞定:

return str.toString().replaceAll(" " , "%20");

        运行时间:15ms,占用内存:9668k
知识点:
String、StringBuffer和StringBuilder的相似点与不同点:
参考链接:https://www.cnblogs.com/su-feng/p/6659064.html
       1.运行速度,或者说是执行速度,在这方面运行速度快慢为:StringBuilder > StringBuffer > String;String为字符串常量,而StringBuilder和StringBuffer均为字符串变量,即String对象一旦创建之后该对象是不可更改的,但后两者的对象是变量,是可以更改的。
       2.线程安全;在线程安全上,StringBuilder是线程不安全的,而StringBuffer是线程安全的;如果一个StringBuffer对象在字符串缓冲区被多个线程使用时,StringBuffer中很多方法可以带有synchronized关键字,所以可以保证线程是安全的,但StringBuilder的方法则没有该关键字,所以不能保证线程安全,有可能会出现一些错误的操作。
       3.String:适用于少量的字符串操作的情况;StringBuilder:适用于单线程下在字符缓冲区进行大量操作的情况;StringBuffer:适用多线程下在字符缓冲区进行大量操作的情况

面试题6:从尾到头打印链表
题目描述:
       输入一个链表,按链表值从尾到头的顺序返回一个ArrayList。
解题思路:
     (1)先将链表中的每一个元素遍历拿到,将其存储至Arraylist数组中。然后再实现就地逆置。小于一半长度的元素就可以了,不用小于等于。注意:1.Arraylist存储时的判定条件2.逆置时的条件3.Arraylist使用set和get方法来存储和读取元素。Mine

public ArrayList printListFromTailToHead(ListNode listNode) {
        ArrayList returnlist = new ArrayList();
        if(listNode==null) return returnlist;
        for (; listNode != null; listNode = listNode.next) {
            returnlist.add(listNode.val);
        }
        for(int i = 0;i

          运行时间:21ms;占用内存:9312k
       (2)借助递归实现(递归的本质还是使用了堆栈结构)

    ArrayList returnlist = new ArrayList();
    public ArrayList printListFromTailToHead(ListNode listNode) {
        if (listNode != null) {
            printListFromTailToHead(listNode.next);
            returnlist.add(listNode.val);
        }
        return returnlist;
    }

           运行时间:20ms;占用内存:9260k
      (3)较优:借用堆栈的先进后出

  public ArrayList printListFromTailToHead(ListNode listNode) {
        //借用堆栈的先进后出
        ArrayList returnlist = new ArrayList();
        Stack  stack = new Stack();
        while (listNode!=null){
            //堆栈压栈
            stack.push(listNode.val);
            //更新当前节点
            listNode = listNode.next;
        }
        while (!stack.isEmpty()){
            returnlist.add(stack.pop());
        }
        return returnlist;
}

         运行时间:15ms;占用内存:9164k

**(第一遍不会)
面试题7:重建二叉树

问题描述:输入某二叉树的前序遍历和中序遍历的结果,请重建出该二叉树。假设输入的前序遍历和中序遍历的结果中都不含重复的数字。例如输入前序遍历序列{1,2,4,7,3,5,6,8}和中序遍历序列{4,7,2,1,5,3,8,6},则重建二叉树并返回。
解决思路:肯定用递归,没话说。手动可以构建二叉树,但写的话思路不多,主要在对于递归不熟,重点在计算遍历左右子树的两个数组的开始位置和结束位置,记得进行输入合法性判断和判断溢出条件。

public class Solution {
    public TreeNode reConstructBinaryTree(int [] pre,int [] in) {
        /**
         * 输入合法性判断
         */
        if (pre == null || in == null || pre.length != in.length) {
            return null;
        }
        return construct(pre, 0, pre.length - 1, in, 0, in.length - 1);
    }
    private TreeNode construct(int[] pre, int pre_start, int pre_end, int[] in, int in_start, int in_end) {
        if (pre_start > pre_end) return null;
        //取前序遍历的第一个数字为根节点
        int value = pre[pre_start];
        //在中序中遍历寻找该根节点
        int index = in_start;
        while (index <= in_end && value != in[index]) {
            index++;
        }
        //判断溢出条件
        if (index > in_end) throw new RuntimeException("Invalid Input");
        //创建当前根节点,并为节点赋值
        TreeNode treeNode = new TreeNode(value);
        //递归调用构建当前节点的左子树
        treeNode.left = construct(pre, pre_start + 1, pre_start + index - in_start, in, in_start, index - 1);
        //先序遍历而言,左子树开始位置是pre_start+1,结束位置是pre_start+index-in_start;
        //中序遍历而言,左子树开始位置是in_start,结束位置是middle -1;
        //递归调用当前节点的右子树
        treeNode.right = construct(pre, pre_start + index - in_start + 1, pre_end, in, index + 1, in_end);
        return treeNode;
    }
}

知识点:
      递归:https://mp.weixin.qq.com/s/kFaJ_aYV7o-_8Ql3w4o1GA      https://mp.weixin.qq.com/s/dSZH2VT8i8rVqUEPpyrHwQ
      从未知到已知,再从已知到为止。1.一个问题的解可以分解为几个子问题的解;2. 这个问题与分解之后的子问题,除了数据规模不同,求解思路完全一样;3. 存在递归终止条件,即存在递归出口。

**(第一遍卡了蛮久,也没做出来,其实真的就差后面的一步)
面试题8:二叉树的下一个结点

问题描述:给定一个二叉树和其中的一个结点,请找出中序遍历顺序的下一个结点并且返回。注意,树中的结点不仅包含左右子结点,同时包含指向父结点的指针。
算法思路:
      1、如果该结点有右子树,则该结点的下一个结点为该结点的右子树的最左结点。
      2、如果该结点没有右子树,则又分两种情况讨论:
      情况一:如果该结点为该结点的父结点的左孩子,则该结点的父结点pNode.next则为下一个结点。
      情况二:如果该结点为该结点的父结点的右孩子,则该结点的父结点的父结点的父结点,直到其中的一个父结点是这个父结点的左孩子,则该父结点的父结点为下一个结点。
代码示例:

public TreeLinkNode GetNext(TreeLinkNode pNode)
{
if(pNode == null)  return null;
         //1.如果有右子树,那么下一个结点就是右子树最左边的节点。
        if(pNode.right!=null){
           pNode = pNode.right;
           while (pNode.left != null) pNode = pNode.left;
           return pNode;
        }
        //2.如果没有右子树,分两种情况,(1)如果该结点的为父结点的左孩子,则该结点的父节点pNode.next则为该结点的下一个结点。
        //(2)如果该结点的为父节点的右孩子,则向上找父节点,直到父节点为该父节点的左孩子,则该父节点的父节点为下一个结点。
        while(pNode.next!=null){
            if(pNode.next.left == pNode)  return pNode.next;
            pNode = pNode.next;
        }
        //3.如果遍历到根节点,说明是从右边上来的,返回null。
        return null;
}

      运行时间:21ms,占用内存:9820k
*(第一遍时有一个点没想到,如何利用第二个堆栈达到pop的效果没想明白;但第二点不停更新第一个堆栈的值想到了)
面试题9:用两个栈实现队列

题目描述:
       用两个栈来实现一个队列,完成队列的Push和Pop操作。 队列中的元素为int类型。
解决思路:首先队列先进先出,尾部插入,头部删除。堆栈后进先出。思路如下:1.将队列中的元素“abcd”压入stack1中,此时stack2为空;2.将stack1中的元素逐个pop并Push进stack2中,此时pop一下stack2中的元素,就可以达到和队列删除数据一样的顺序了;3.如果此时整体有元素入队列,参考步骤2,先将stack2的元素整体pop并push进stack1;再将新元素插入,达到一致的顺序。(这个问题其实不难,为什么第一次没想到)
代码示例:

import java.util.ArrayList;
import java.util.Stack;
public class Solution{
    Stack stack1 = new Stack();
    Stack stack2 = new Stack();
    public void push(int node) {
        while (!stack2.isEmpty()){
            stack1.push(stack2.pop());
        }
        stack1.push(node);
    }
    public int pop() {
        while (!stack1.isEmpty()){
            stack2.push(stack1.pop());
        }
        return stack2.pop();
    }
}

       运行时间:14ms;占用内存:9188k。

面试题10:斐波那契数列
题目描述:
        大家都知道斐波那契数列,现在要求输入一个整数n,请你输出斐波那契数列的第n项(从0开始,第0项为0)。n<=39
解决思路:
       第一种:递归法:在数学上,斐波纳契数列以如下被以递推的方法定义:F(1)=1,F(2)=1, F(n)=F(n-1)+F(n-2)(n>=3,n∈N*)O(2^n)Mine
       第二种:(较优)直接求解法(动态编程计算斐波那契数列):  新的时间复杂度为O(n)。动态编程通过解决子问题,将子问题的结果结合来获得整个问题的解得过程。递归效率不高,子问题重叠。动态编程的思想是只解决子问题一次,将子问题解决存储以备后用。Mine
代码示例:

public static int Fibonacci01(int n) {
        if(n==1)  return 1;
        if(n==0)  return 0;
        return Fibonacci01(n-1)+Fibonacci01(n-2);
    }
运行时间:1215ms;占用内存:9428k
    public static int Fibonacci(int n) {
        if(n==1)  return 1;
        if(n==0)  return 0;
        int f_n2 = 0,f_n1 = 1;
        for(int i = 1;i

       运行时间:18ms;占用内存:9164k

面试题11:旋转数组的最小数字
题目描述:
       把一个数组最开始的若干个元素搬到数组的末尾,我们称之为数组的旋转。 输入一个非减排序的数组的一个旋转,输出旋转数组的最小元素。 例如数组{3,4,5,1,2}为{1,2,3,4,5}的一个旋转,该数组的最小值为1。 NOTE:给出的所有元素都大于0,若数组大小为0,请返回0。
解题思路:
       第一种:顺序遍历,O(n)复杂度,找到最小元素;或者Arrays.sort进行快速排序,然后return第一个元素。Mine
       第二种:剑指offer上的:
       Step1.和二分查找法一样,我们用两个指针分别指向数组的第一个元素和最后一个元素。
       Step2.接着我们可以找到数组中间的元素:
    如果该中间元素位于前面的递增子数组,那么它应该大于或者等于第一个指针指向的元素。此时数组中最小的元素应该位于该中间元素的后面。我们可以把第一个指针指向该中间元素,这样可以缩小寻找的范围。移动之后的第一个指针仍然位于前面的递增子数组之中。如果中间元素位于后面的递增子数组,那么它应该小于或者等于第二个指针指向的元素。此时该数组中最小的元素应该位于该中间元素的前面。
      Step3.接下来我们再用更新之后的两个指针,重复做新一轮的查找。
代码示例:

public static int minNumberInRotateArray(int[] array) {
        int min = Integer.MAX_VALUE;
        for (int i = 0; i < array.length; i++) {
            if (array[i] < min) {
                min = array[i];
            }
        }
        return min;
}

        运行时间:333ms;占用内存:27944k

  public static int minNumberInRotateArray(int[] array) {
        if (array.length == 0) return Integer.MIN_VALUE;
        int index_first = 0, index_last = array.length - 1, index_mid = 0;
        while (array[index_first] >= array[index_last]) {
            if (index_last - index_first == 1) {
                index_mid = index_last;
                break;
            }
            index_mid = (index_first + index_last) / 2;
            if (array[index_first] == array[index_mid] && array[index_mid] == array[index_last]) {
                return GetMinInorder(array, index_first, index_last);
            }
            if (array[index_mid] >= array[index_first]) {
                index_first = index_mid;
            } else if (array[index_mid] <= array[index_first]) {
                index_last = index_mid;
            }
        }
        return  array[index_mid];
    }

    private static int GetMinInorder(int[] array, int index_first, int index_last) {
        int result = array[index_first];
        for(int i = index_first+1;i<=index_last;i++){
            if(result>array[i]){
                result  = array[i];
            }
        }
        return result;
    }

        运行时间:244ms;占用内存:28460k

(**)面试题12:矩阵中的路径
题目描述
       请设计一个函数,用来判断在一个矩阵中是否存在一条包含某字符串所有字符的路径。路径可以从矩阵中的任意一个格子开始,每一步可以在矩阵中向左,向右,向上,向下移动一个格子。如果一条路径经过了矩阵中的某一个格子,则之后不能再次进入这个格子。 例如 a b c e s f c s a d e e 这样的3 X 4 矩阵中包含一条字符串"bcced"的路径,但是矩阵中不包含"abcb"路径,因为字符串的第一个字符b占据了矩阵中的第一行第二个格子之后,路径不能再次进入该格子。
算法思路:(道理我都懂,可就是写不出来,气死了)
   (1)根据给定数组,初始化一个标志位数组,初始化为false,表示未走过,true表示已经走过,不能走第二次
   (2)根据行数和列数,遍历数组,先找到一个与str字符串的第一个元素相匹配的矩阵元素,进入judge
   (3)根据i和j先确定一维数组的位置,因为给定的matrix是一个一维数组
   (4)确定递归终止条件:越界,当前找到的矩阵值不等于数组对应位置的值,已经走过的,这三类情况,都直接false,说明这条路不通
   (5)若k,就是待判定的字符串str的索引已经判断到了最后一位,此时说明是匹配成功的
   (6)下面就是本题的精髓,递归不断地寻找周围四个格子是否符合条件,只要有一个格子符合条件,就继续再找这个符合条件的格子的四周是否存在符合条件的格子,直到k到达末尾或者不满足递归条件就停止。
   (7)走到这一步,说明本次是不成功的,我们要还原一下标志位数组index处的标志位,进入下一轮的判断。
代码示例:

public static boolean hasPath(char[] matrix, int rows, int cols, char[] str) {
        boolean[] flag = new boolean[matrix.length];
        for (int i = 0; i < rows; i++) {
            for (int j = 0; j < cols; j++) {
                if (judge(matrix, i, j, rows, cols, str, flag, 0)) {
                    return true;
                }
            }
        }
        return false;
}
   //字符串索引初始为0即先判断字符串的第一位
private static boolean judge(char[] matrix, int i, int j, int rows, int cols, char[] str, boolean[] flag, int k) {
        //转换一维数组位置
        int index = i * cols + j;
        //递归的终止条件
        if (i < 0 || j < 0 || j >= cols || i >= rows || matrix[index] != str[k] || flag[index] == true) {
            return false;
        }
        //若k已经到str末尾了,说明之前匹配都成功了,直接返回ture就可以了。
        if (k == str.length - 1) return true;
        flag[index] = true;

        //回溯,递归寻找,每次找到了就给k加一,找不到,还原
        if (judge(matrix, i - 1, j, rows, cols, str, flag, k + 1) ||
                judge(matrix, i + 1, j, rows, cols, str, flag, k + 1) ||
                judge(matrix, i, j - 1, rows, cols, str, flag, k + 1) ||
                judge(matrix, i, j + 1, rows, cols, str, flag, k + 1)) {
            return true;
        }
        //走到这,说明走不通,还原,再试试其他路径
        flag[index] = false;
        return false;
}

        运行时间:26ms;占用内存:9440k
知识点:
       回溯法(探索与回溯法)是一种选优搜索法,又称为试探法,按选优条件向前搜索,以达到目标。但当探索到某一步时,发现原先选择并不优或达不到目标,就退回一步重新选择,这种走不通就退回再走的技术为回溯法,而满足回溯条件的某个状态的点称为“回溯点”。
       回溯法对任一解的生成,一般都采用逐步扩大解的方式。每前进一步,都试图在当前部分解的基础上扩大该部分解。它在问题的状态空间树中,从开始结点(根结点)出发,以深度优先搜索整个状态空间。这个开始结点成为活结点,同时也成为当前的扩展结点。在当前扩展结点处,搜索向纵深方向移至一个新结点。这个新结点成为新的活结点,并成为当前扩展结点。如果在当前扩展结点处不能再向纵深方向移动,则当前扩展结点就成为死结点。此时,应往回移动(回溯)至最近的活结点处,并使这个活结点成为当前扩展结点。回溯法以这种工作方式递归地在状态空间中搜索,直到找到所要求的解或解空间中已无活结点为止。
(*)面试题13:机器人的运动范围
题目描述
      地上有一个m行和n列的方格。一个机器人从坐标0,0的格子开始移动,每一次只能向左,右,上,下四个方向移动一格,但是不能进入行坐标和列坐标的数位之和大于k的格子。 例如,当k为18时,机器人能够进入方格(35,37),因为3+5+3+7 = 18。但是,它不能进入方格(35,38),因为3+5+3+8 = 19。请问该机器人能够达到多少个格子?
算法思路一:(思路清楚,可是写出来竟然花费了一个半小时,而且代码真的写的很烂)
     1.建立标志数组,用来记录是否遍历过。
     2.判断各种越界情况(判断该点是都在矩阵中)。
     3.计算位置并判断该位置是否满足要求,计数器加一并将该位置的flags标志位置为true。
     4.四向遍历,不往回回溯,譬如往右走一个单位,考虑该节点是否遍历过,该节点是否在矩阵中,该节点的越界情况,该节点是否之前遍历过。Mine
代码示例:

public class Main {
    private static int counting = 0;
    public static void main(String[] args) {
        System.out.println(movingCount(10, 1, 100));
    }
    public static int movingCount(int threshold, int rows, int cols) {
        //第一个参数是数组是数字k,第二个是行,第三个是列。
        boolean[] flags = new boolean[rows * cols];
        howmanyblocks(threshold, rows, cols, 0, 0, flags);
        return counting;
    }
    private static boolean howmanyblocks(int threshold, int rows, int cols, int i, int j, boolean[] flags) {
        int index = i * cols + j;
        //第一种判断各种越界情况
        if (i < 0 || j < 0 || i >= rows || j >= cols) {
            return false;
        }
        //第二种判断该位置是否满足要求
        if (judgetruorfalse(i, j, threshold)) {
            return false;
        }
        counting++;
        flags[index] = true;
        if (!judgetruorfalse(i - 1, j, threshold) && (((i - 1) * cols + j) >= 0) && (((i - 1) * cols + j) < rows * cols) && !flags[(i - 1) * cols + j]
        ) {
            howmanyblocks(threshold, rows, cols, i - 1, j, flags);
        }
        if (!judgetruorfalse(i + 1, j, threshold) && ((i + 1) * cols + j) >= 0 && ((i + 1) * cols + j) < rows * cols && !flags[(i + 1) * cols + j]
        ) {
            howmanyblocks(threshold, rows, cols, i + 1, j, flags);
        }
        if (!judgetruorfalse(i, j - 1, threshold) && (i * cols + j - 1) >= 0 && (i * cols + j - 1) < rows * cols && !flags[i * cols + j - 1]
        ) {
            howmanyblocks(threshold, rows, cols, i, j - 1, flags);
        }
        if (!judgetruorfalse(i, j + 1, threshold) && (i * cols + j + 1) >= 0 && (i * cols + j + 1) < rows * cols && !flags[i * cols + j + 1]
        ) {
            howmanyblocks(threshold, rows, cols, i, j + 1, flags);
        }
        return true;
    }

    private static boolean judgetruorfalse(int i, int j, int threshold) {
        int rows_Num = String.valueOf(i).length();
        int cols_Num = String.valueOf(j).length();
        int add_result = 0;
        for (int k = 0; k < rows_Num; k++) {
            add_result += (String.valueOf(i).charAt(k) - '0');
        }
        for (int k = 0; k < cols_Num; k++) {
            add_result += (String.valueOf(j).charAt(k) - '0');
        }
        if (add_result > threshold) return true;
        else return false;
    }
}

算法思路二:
      1.从(0,0)开始走,每成功走一步标记当前位置为true,然后从当前位置往四个方向探索,返回1 + 4 个方向的探索值之和。
      2.探索时,判断当前节点是否可达的标准为:
          1)当前节点在矩阵内;
          2)当前节点未被访问过;
          3)当前节点满足limit限制。
代码示例:

    public int movingCount(int threshold, int rows, int cols) {
        boolean[][] visited = new boolean[rows][cols];
        return countingSteps(threshold, rows, cols, 0, 0, visited);
    }
    private int countingSteps(int threshold, int rows, int cols, int i, int j, boolean[][] visited) {
        if (i < 0 || i >= rows || j < 0 || j >= cols || visited[i][j] || (bitSum(i) + bitSum(j) > threshold))
            return 0;
        visited[i][j] = true;
        return countingSteps(threshold, rows, cols, i - 1, j, visited) +
                countingSteps(threshold, rows, cols, i + 1, j, visited) +
                countingSteps(threshold, rows, cols, i, j - 1, visited) +
                countingSteps(threshold, rows, cols, i, j + 1, visited) + 1;
}

private static int bitSum(int i) {
        int count = 0;
        while (i != 0) {
            count += i % 10;
            i /= 10;
        }
        return count;
    }

       运行时间:14ms;占用内存:9560k

面试题15:二进制中1的个数
题目描述:
       输入一个整数,输出该数二进制表示中1的个数。其中负数用补码表示。
算法思路一:
    1.对于正整数,除以2,看余数是否等于1,若是,计数器加一。2.对于负整数,使用Integer.toBinaryString(n)这一API投机取巧将其转为补码形式的字符串。然后再行计算等于1的情况。Mine
代码示例一:

    public int NumberOf1(int n){
        int count = 0;
        if (n > 0) {
            while (n / 2 != 0) {
                if (n % 2 == 1) count++;
                n = n / 2;
            }
            if (n % 2 == 1) count++;
        } else {
            String s1 = Integer.toBinaryString(n);
            for (int i = 0; i < s1.length(); i++) {
                if (s1.charAt(i) == '1') count++;
            }
        }
        return count;
    }

        运行时间:13ms;占用内存:9372k
算法思路二:(较优)由于除法的效率要比移位运算要差很多,1的二进制是 前面都是0,最后一位为1,也就是只有一个1,每次向左移位一下,使得flag的二进制表示中始终只有一个位为1,每次与n做位与操作,这样就相当于逐个检测n的每一位是否是1了。
代码示例二:

    public static int NumberOf1(int n) {
        int count = 0;
        int flag = 1;
        while (flag != 0) {
            if((flag & n)!=0) count++;
            flag = flag << 1;
        }
        return count;
    }

        运行时间:14ms;占用内存:9228k
算法思路三:记住结论即可:把一个整数减去1,再和原整数做与运算,会把该整数最右边的1变为0,那么一个整数中二进制表示有多少个1,就会进行多少次这样的操作。
代码示例三:

    public static int NumberOf1(int n) {
        int count = 0;
        while (n != 0) {
            count++;
            n = (n-1)&n;
        }
        return count;
    }

        运行时间:13ms;占用内存:9376k

面试题16:数值的整数次方
题目描述:
       给定一个double类型的浮点数base和int类型的整数exponent。求base的exponent次方。
算法思路一:
       使用Math.pow这一API直接计算;(较优)或者考虑指数为正负的情况,实现类乘操作。
代码示例一:

    public static double Power(double base, int exponent) {
        return Math.pow(base, exponent);
    }
运行时间:63ms;占用内存:10384k
    public static double Power(double base, int exponent) {
        double result = 1.0;
        if (exponent == 0) return 1;
        for (int i = 0; i < Math.abs(exponent); i++) {
            result *= base;
        }
        if (exponent > 0) {
            return result;
        } else {
            return 1.0 / result;
        }
    }

       运行时间:39ms;占用内存:10544k
算法思路二:
      1.全面考察指数的正负、底数是否为零等情况。
      2.写出指数的二进制表达,例如13表达为二进制1101。
      3.举例:10^1101 = 10^0001*10^0100*10^1000。
      4.通过&1和>>1来逐位读取1101,为1时将该位代表的乘数累乘到最终结果。
代码示例:

public static double Power(double base, int n) {
        double res = 1,curr = base;
        int exponent;
        if(n>0){
            exponent = n;
        }else if(n<0){
            if(base==0)
                throw new RuntimeException("分母不能为0");
            exponent = -n;
        }else{// n==0
            return 1;// 0的0次方
        }
        while(exponent!=0){
            if((exponent&1)==1)
                res*=curr;
            curr*=curr;// 翻倍
            exponent>>=1;// 右移一位
        }
        return n>=0?res:(1/res);
}

         运行时间:69ms;占用内存:10396k

(*)面试题18:删除链表中重复的结点
题目描述:
      在一个排序的链表中,存在重复的结点,请删除该链表中重复的结点,重复的结点不保留,返回链表头指针。 例如,链表1->2->3->3->4->4->5 处理后为 1->2->5
算法思路:
    1.假设该链表只有一个或零个节点,返回pHead即可;
    2.若当前节点不重复,则保留当前结点,从下一个结点开始递归;
    3.若当前节点重复,则跳过值与当前结点相同的全部结点,找到第一个与当前结点不同的结点, 从第一个与当前结点不同的结点开始递归。(注意下意识跳过第一个,自己是没有跳,所以报错。后面一直与第一个相对比)
代码示例:

   public ListNode deleteDuplication(ListNode pHead)
    {
        //新建一个头结点
        ListNode first = new ListNode(-1);
        //这个头结点的下一个结点为pHead
        first.next = pHead;
        //p结点为pHead
        ListNode p = pHead;
        //last结点为新建的头结点
        ListNode last = first;
        while (p != null && p.next != null) {
            if (p.val == p.next.val) {
                int val = p.val;
                while (p!= null&&p.val == val)
                    p = p.next;
                last.next = p;
            } else {
                last = p;
                p = p.next;
            }
        }
        return first.next;
    }

         运行时间:34ms;占用内存:9532k。
(**)面试题19:正则表达式匹配
题目描述
       请实现一个函数用来匹配包括'.'和'*'的正则表达式。模式中的字符'.'表示任意一个字符,而'*'表示它前面的字符可以出现任意次(包含0次)。 在本题中,匹配是指字符串的所有字符匹配整个模式。例如,字符串"aaa"与模式"a.a"和"ab*ac*a"匹配,但是与"aa.a"和"ab*a"均不匹配。
代码思路一:
       直接调用API即可。
代码示例一:

    public static boolean match(char[] str, char[] pattern) {
        String strs = String.valueOf(str);
        String patterns = String.valueOf(pattern);
        return strs.matches(patterns);
    }

代码思路二:
       当模式中的第二个字符不是“*”时:
       1、如果字符串第一个字符和模式中的第一个字符相匹配,那么字符串和模式都后移一个字符,然后匹配剩余的。 
       2、如果 字符串第一个字符和模式中的第一个字符相不匹配,直接返回false。
       而当模式中的第二个字符是“*”时:
       如果字符串第一个字符跟模式第一个字符不匹配,则模式后移2个字符,继续匹配。如果字符串第一个字符跟模式第一个字符匹配,可以有3种匹配方式:
      1、模式后移2字符,相当于x*被忽略;
      2、字符串后移1字符,模式后移2字符;
      3、字符串后移1字符,模式不变,即继续匹配字符下一位,因为*可以匹配多位;
代码示例二:

    public static boolean match(char[] str, char[] pattern) {
        if (str == null || pattern == null) {
            return false;
        }
        int strIndex = 0;
        int patternIndex = 0;
        return matchCore(str, strIndex, pattern, patternIndex);
    }
    private static boolean matchCore(char[] str, int strIndex, char[] pattern, int patternIndex) {
        //有效性检查,str到尾,patter到尾,此时匹配成功。
        if (strIndex == str.length && patternIndex == pattern.length) return true;
        //pattern先到尾,匹配失败
        if (strIndex != str.length && patternIndex == pattern.length) return false;
        //模式第2个是*,且字符串第1个跟模式第1个匹配,分3种匹配模式;如不匹配,模式后移2位
        if (patternIndex + 1 < pattern.length && pattern[patternIndex + 1] == '*') {
            if (strIndex != str.length && pattern[patternIndex] == str[strIndex] || (pattern[patternIndex] == '.' && strIndex != str.length)) {
                return matchCore(str, strIndex, pattern, patternIndex + 2) ||//模式后移2,视为x*匹配0个字符
                        matchCore(str, strIndex + 1, pattern, patternIndex + 2) ||//视为模式匹配1个字符
                        matchCore(str, strIndex + 1, pattern, patternIndex);//*匹配1个,再匹配str中的下一个
            } else {
                return matchCore(str, strIndex, pattern, patternIndex + 2);
            }
        }
        //模式第2个不是*,且字符串第1个跟模式第1个匹配,则都后移1位,否则直接返回false
        if((strIndex!=str.length && pattern[patternIndex]==str[strIndex])||(pattern[patternIndex]=='.' && strIndex!=str.length)){
            return  matchCore(str,strIndex+1,pattern,patternIndex+1);
        }
        return false;
}

(*)面试题20:表示数值的字符串
题目描述
       请实现一个函数用来判断字符串是否表示数值(包括整数和小数)。例如,字符串"+100","5e2","-123","3.1416"和"-1E-16"都表示数值。 但是"12e","1a3.14","1.2.3","+-5"和"12e+4.3"都不是。
算法思路一:
       使用JVM的强制类型转换Double.parseDouble的API,若报错,则返回false,若成功,则返回true。(又投机取巧,服了自己了)Mine
代码示例一:

    public static boolean isNumeric(char[] str) {
        String s1 = String.valueOf(str);
        try {
            Double var = Double.parseDouble(s1);
        } catch (Exception e) {
            return false;
        }
        return true;
     }

       运行时间:17ms;占用内存:9412k
算法思路二:(较优,虽然可以想明白,但自己写的话还是有点凌乱)
       1.对字符串中的每个字符进行判断分析
       2.e(E)后面只能接数字,并且不能出现2次
       3. 对于+、-号,只能出现在第一个字符或者是e的后一位
       4.对于小数点,不能出现2次,e后面不能出现小数点
代码示例二:

    public static boolean isNumeric(char[] str) {
        //标记符号、小数点、e是否出现过。
        boolean sign = false, decimal = false, hasE = false;
        for (int i = 0; i < str.length; i++) {
            //1.确保e后面必须有数字,2.确保不能同时存在两个e。
            if (str[i] == 'e' || str[i] == 'E') {
                if (i == str.length - 1) return false;
                if (hasE) return false;
                hasE = true;
            } else if (str[i] == '+' || str[i] == '-') {
                //1.第二次出现+-号,则其必须紧接在e之后。2.第一次出现+-时,且不是在字符串开头,
                // 则必须接在e之后
                if (sign && str[i - 1] != 'e' && str[i - 1] != 'E') return false;
                if (!sign && i > 0 && str[i - 1] != 'e' && str[i - 1] != 'E') return false;
                sign = true;
            } else if (str[i] == '.') {
                //e后面不能接小数点,小数点不能超过两次
                if (hasE || decimal) return false;
                decimal = true;
            } else if (str[i] < '0' || str[i] > '9') {
                //排除掉不合法字符
                return false;
            }
        }
        return true;
    }

         运行时间:14ms;占用内存:9356k
(*)面试题21:调整数组顺序使奇数位于偶数前面
题目描述
       输入一个整数数组,实现一个函数来调整该数组中数字的顺序,使得所有的奇数位于数组的前半部分,所有的偶数位于数组的后半部分,并保证奇数和奇数,偶数和偶数之间的相对位置不变。
算法思路一:
       建立一个Arraylist数组,第一遍遍历Array数组将偶数依次添加进动态数组中;第二遍遍历依次添加进动态数组中。最后将Arraylist数组挨个赋值给Array数组。因为题目对空间的限制比较宽泛,因此可以这么干。Mine
代码示例一:

    public static void reOrderArray(int[] array) {
        ArrayList arraylist = new ArrayList();
        for (int i = 0; i < array.length; i++) {
            if (array[i] % 2 != 0) {
                arraylist.add(array[i]);
            }
        }
        for (int i = 0; i < array.length; i++) {
            if (array[i] % 2 == 0) {
                arraylist.add(array[i]);
            }
        }
        for(int i = 0;i

       运行时间:23ms;占用内存:9280k
算法思路二:
       使用Arraylist存偶数,使用交换法将所有奇数按照次序排好队(第二个指针遇到偶数往前走,直至奇数;第一个指针遇到奇数往前走,直到偶数;假如第一个指针是偶数,第二个指针是奇数且第一个指针的位置小于第二个位置。则交换位置;否则,第二个指针走得慢,让它再往前走一格。),再进行后半部分偶数的赋值Mine
代码示例二:

    public static void reOrderArray(int[] array) {
        ArrayList arraylist = new ArrayList();
        for (int i = 0; i < array.length; i++) {
            if (array[i] % 2 == 0) {
                arraylist.add(array[i]);
            }
        }
        int first_ele = 0, second_ele = 0;
        while (second_ele < array.length && first_ele 

       运行时间:22ms;占用内存:9412k
算法思路三:(较优)为了保持相对位置的不变(稳定性),采用冒泡排序的思想,记录已经排好位置的奇数的个数,若后续存在奇数,则将其挨个冒泡交换。很巧。
代码示例三:

    public static void reOrderArray(int [] array) {
        //相对位置不变,稳定性
        //冒泡排序的思想
        int m = array.length;
        int k = 0;//记录已经摆好位置的奇数的个数
        for (int i = 0; i < m; i++) {
            if (array[i] % 2 == 1) {
                int j = i;
                while (j > k) {//j >= k+1
                    int tmp = array[j];
                    array[j] = array[j-1];
                    array[j-1] = tmp;
                    j--;
                }
                k++;
            }
        }
   }

       运行时间:15ms;占用内存:9360k 

面试题22:链表中倒数第k个结点
题目描述
       输入一个链表,输出该链表中倒数第k个结点。
算法思路一:遍历第一遍链表,获得链表长度,如果k大于链表长度,则返回null,否则计算真实位置(总长度-k)。然后再顺序查找到总长度-k的位置,返回该ListNode。Mine
代码示例一:

    public static ListNode FindKthToTail(ListNode head,int k) {
        //计算listnode的总长度
        ListNode store_point = head;
        int listnode_length = 0;
        while(head!=null){
            listnode_length++;
            head = head.next;
        }
        if(k > listnode_length)  return null;
        int real_pos = listnode_length - k;
        for(int i = 0;i

       运行时间:22ms;占用内存:9516k
代码思路二:
       两个指针,先让第一个指针和第二个指针都指向头结点,然后再让第一个指正走(k-1)步,到达第k个节点。然后两个指针同时往后移动,当第一个结点到达末尾的时候,第二个结点所在位置就是倒数第k个节点了。与此同时,防止链表越界。(相当于制造了一个K长度的尺子,把尺子从头往后移动,当尺子的右端与链表的末尾对齐的时候,尺子左端所在的结点就是倒数第k个结点)
代码示例二:

    public static ListNode FindKthToTail(ListNode head, int k) {
        if (head == null || k <= 0) return null;
        ListNode tail = head;
        for (int i = 0; i < k; i++) {
            if (tail != null) {
                tail = tail.next;
            } else {
                return null;
            }
        }
        while (tail != null) {
            head = head.next;
            tail = tail.next;
        }
        return head;
    }

(**)面试题23:链表中环的入口结点
题目描述
       给一个链表,若其中包含环,请找出该链表的环的入口结点,否则,输出null。
算法思路一:
       自己想的很low。先将已经遍历过的节点存放在Arraylist数组中,判断当前节点的下一个节点是否在Arraylist中(使用contains的API)。若在则返回该节点,若不在,则继续遍历,知道pHead为Null,返回null。感觉代码复杂度应该在O(n^3)。
代码示例一:

    public ListNode EntryNodeOfLoop(ListNode pHead) {
        ArrayList arraylist = new ArrayList();
        arraylist.add(pHead);
        pHead = pHead.next;
        while (pHead != null) {
            if (arraylist.contains(pHead)) {
                return pHead;
            }
            arraylist.add(pHead);
            pHead = pHead.next;

        }
        return null;
    }

       运行时间:24ms;占用内存:9652k   
算法思路二:(较优,感觉记住咋操作就行了,没必要记原因)
       第一步,用两个快慢指针找环中相汇点。分别用slow, fast指向链表头部,slow每次走一步,fast每次走二步,直到fast == slow找到在环中的相汇点。
       第二步,找环的入口。当fast == slow时,假设slow走过x个节点,则fast走过2x个节点。设环中有n个节点,因为fast比slow多走一圈(n个节点),所以有等式2x = n + x,可以推出n = x,两者同时减去蓝色部分,算出来相等,都为n-x。这时,我们让fast重新指向链表头部pHead,slow的位置不变,然后slow和fast一起向前每次走一步,直到fast == slow,此时两个指针相遇的节点就是环的入口。
      参考链接:https://www.jianshu.com/p/092d14d13216
      证明链接:https://www.nowcoder.com/questionTerminal/253d2c59ec3e4bc68da16833f79a38e4
代码示例二:

    public ListNode EntryNodeOfLoop(ListNode pHead){
        if(pHead==null|| pHead.next==null|| pHead.next.next==null)return null;
        ListNode fast=pHead.next.next;
        ListNode slow=pHead.next;
        //先判断有没有环
        while(fast!=slow){
            if(fast.next!=null&& fast.next.next!=null){
                fast=fast.next.next;
                slow=slow.next;
            }else{
                //没有环,返回
                 return null;
            }
        }
        //循环出来的话就是有环,且此时fast==slow.
        fast=pHead;
        while(fast!=slow){
            fast=fast.next;
            slow=slow.next;
        }
        return slow;
    }

        运行时间:20ms;占用内存:9632k

(*)面试题24:反转链表
题目描述
       输入一个链表,反转链表后,输出新链表的表头。
算法思路一:
      利用栈的后进先出,遍历链表并逐个push进堆栈中,循环条件为head!=null,逐个pop构建新链表,但存在时间复杂度或者空间复杂度过高的问题。Mine
代码示例一:

    public static ListNode ReverseList01(ListNode head) {
        if(head==null || head.next ==null)  return head;
        Stack stack = new Stack();
        while (head != null) {
            stack.push(head);
            head = head.next;
        }
        ListNode reverstnode = stack.pop();
        ListNode returnnode = reverstnode;
        System.out.println(reverstnode.val);
        while (!stack.isEmpty()) {
            reverstnode.next = stack.pop();
            reverstnode = reverstnode.next;
            System.out.println(reverstnode.val);
        }
        return returnnode;
    }

       时间还是空间超过限制。
算法思路二:
     (较优)使用三个指针保存当前节点head、当前节点的下一个节点next以及当前节点的上一个节点pre,循环条件是head不等于空,(1)先使用next = head.next保存head的下一个节点的信息;(2)再使用head.next=pre实现断链和反转链表的效果。(3)pre=head,head=next;让pre,head,next依次往后移动一个节点,继续下一次指针的反转。
代码思路二:

    public static ListNode ReverseList(ListNode head) {
        if(head==null || head.next ==null)  return head;
        ListNode pre = null;
        ListNode next =null;
        while (head!=null){
            next = head.next;//暂存下一个节点
            head.next = pre;//断链并进行反转
            pre = head;//将两个指针都往前移动一格
            head = next;
        }
        return next;
    }

        运行时间:20ms;占用内存:9536k
(*)面试题25:合并两个排序的链表
题目描述
       输入两个单调递增的链表,输出两个链表合成后的链表,当然我们需要合成后的链表满足单调不减规则。
算法思路一:
       我想的是插入,以list1为合成后的链表,进行比较插入,但由于是插入且维护的指针较多,所以时间复杂度较高,故而舍弃掉。新建一个链表,进行逐个比较,若list1小于list2,则将list1的节点插入,并将list1往前走一格,否则list2向前走一格。如果一个走完另一个没有走完,直接将没走完的放在合并后链表的后面,挺简单。Mine
代码示例一:

    public static ListNode Merge(ListNode list1, ListNode list2) {
        ListNode mergeResult = new ListNode(-1);
        mergeResult.next = null;
        ListNode head = mergeResult;
        while (list1 != null && list2 != null) {
            if (list1.val < list2.val) {
                mergeResult.next = list1;
                mergeResult = mergeResult.next;
                list1 = list1.next;
            } else {
                mergeResult.next = list2;
                mergeResult = mergeResult.next;
                list2 = list2.next;
            }
        }
        if (list1 != null) {
           mergeResult.next = list1;
        }
        if(list2!=null){
            mergeResult.next = list2;
        }
        return head.next;
   }

       运行时间:26ms;占用内存:9464k
算法思路二:
    (较优)采用递归的方法进行合并。

                           《剑指Offer》Java版上篇(面试题1-22,多种解题思路)_第1张图片
代码示例二:

    public static ListNode Merge(ListNode list1,ListNode list2) {
        if(list1 == null){
            return list2;
        }
        if(list2 == null){
            return list1;
        }
        if(list1.val <= list2.val){
            list1.next = Merge(list1.next, list2);
            return list1;
        }else{
            list2.next = Merge(list1, list2.next);
            return list2;
        }
    }   

       运行时间:26ms;占用内存:9464k

(**)面试题26:树的子结构
题目描述
        输入两棵二叉树A,B,判断B是不是A的子结构。(ps:我们约定空树不是任意一个树的子结构)
算法思路一:
     (1)先根遍历二叉树A,不动二叉树B;(2)假设找到A和B相等的第一个节点,递归进行判断两个二叉树的左孩子和右孩子是否都相等,递归结束条件是1.二叉树B遍历结束则返回false;2.二叉树B未遍历结束但二叉树A结束了,返回false;3.两者不相等,返回false。我自己第一步考虑到了,但第二步想的是层次遍历,未考虑递归这块,但层次遍历自己又实现不了,好吧。。
代码示例一:

    public static boolean HasSubtree(TreeNode root1, TreeNode root2) {
        boolean result = false;
        if (root1 != null && root2 != null) {
            if (root1.val == root2.val) {
                result = doesTree1HaveTree2(root1, root2);
            }
            if (!result) result = HasSubtree(root1.left, root2);
            if (!result) result = HasSubtree(root1.right, root2);
        }
        return result;
    }
    private static boolean doesTree1HaveTree2(TreeNode root1, TreeNode root2) {
        if (root1 == null) return false;
        if (root2 == null) return true;
        if (root1.val != root2.val) return false;
        return doesTree1HaveTree2(root1.left, root2.left) && doesTree1HaveTree2(root1.right, root2.right);
    }

运行时间:16ms占用内存:9508k

你可能感兴趣的:(Java开发)