《剑指Offer》第1,2章面试流程与基础知识

第一章

项目介绍

可按照STAR模型。
S:背景
T:自己完成的任务
A:为了完成任务怎么做的(什么工具什么平台什么技术)
R:成果,实现了怎样的效果,最好能够量化

高质量的代码

边界值的处理,特殊输入,负面输入,最好先写单元测试再写实现
遇到复杂问题有三个理解的思路:1.画图 2.举例 3.分解
而且还需要关注时间复杂度,空间复杂度等。

提问环节

问相应的人,相应的问题。

基础知识

第一章可能是基础知识吧,都比较简单,书中给出的一些解决思路还是有点妙妙。突然觉得这书用于入门也挺好的

实现Singleton模式

主要就分为懒汉式和饿汉式,懒汉对应延迟加载,不到必要时,不创建,好处在于减少初始化时的负担

数组中的重复的数字

找出一个数组(数字的范围为0~n-1,其中n为长度)中任意一个重复的数字,重复的数字不知道有几个也不知道重复了几次。
比较常规的想法是遍历数组,如果n的范围在0~231 之间可以使用数组记录数字出现的次数,用索引作为数字的标志。如果超出了这个返回可以使用HashSet,一旦某个数字存在过,就可以输出了,而且这种方法可以求出第一个重复的数字(牛客网上对题目的解读是这样的)

书中的给了一个方法,俺觉得有点妙,不过不能保证是第一个重复的数字。
如果没有重复且有序的数组,那么每个数字与数组的索引应该是相对应的,如果出现不对应的那就是出现了重复。
现在有个问题,数组不是有序的。
只需要顺序遍历数组,如果数字与索引对应,则下一个,如果不对应则把数字放到对应的位置,对应位置上的原数字再放到对应的位置,直到某个数字已经在对应的位置为止,或者某个数字没在对应的位置,但是属于它的位置已经被其他数字匹配上了,这时重复的就出现了,这样做找出的数字可能不是第一个重复的数字,不过这是符合题意的(任意)。

private static int func(int[] arr){
        if(arr==null){
            throw  new RuntimeException("无输入");
        }
        for(int i=0;i<arr.length;i++){
            while(arr[i]!=i){
                int num = arr[i];
                if(arr[num]==num){
                    return num;
                }else{
                    int temp = num;
                    arr[i] = arr[num];
                    arr[num] = num;
                }
            }
        }
        throw  new RuntimeException("不存在重复值");
    }

这种方法会要求修改数组,如果不修改数组呢?(emm时间复杂度有点高…O(nlogN))
可以借助二分查找,因为如果无重复,自然是有0~n-1个数字,那么我们可以算出预设的中间值,遍历数组,算出前半大小的有多少,如果前半多了,说明前半肯定存在重复的数字,那么缩小范围去前半找,反之去后半,直到最后锁定到一个结果,如果数组中存在的该数大于1了,则说明找到啦。

private static int func(int[] arr){
        if(arr==null){
            throw  new RuntimeException("无输入");
        }
        int left = 1,right = arr.length;
        while (left<=right){
            int mid = left+right>>1;
            int count =0;
            for(int i=0;i<arr.length;i++){
                if(arr[i]<=mid&&arr[i]>=left)
                    count++;
            }
            if(left==right){
                if(count>1)
                    return left;
                break;
            }
            if(count>mid-left+1) {
                right = mid;
            }else {
                left = mid+1;
            }
        }
        throw new RuntimeException("无重复数字");

    }

二维数组中的查找

见本文

替换空格

将字符串中的空格替换为%20

这题按理说也很简单,用Java的话,转换为StringBuilder/StringBuffer ,将空格替换就好,如果不允许使用这种结构,只允许使用一个字符数组之类的呢?书中给了这样一个场景,需要在原字符串的基础上进行扩容一定的长度,操作完成。(C语言,移动下指针就好)
数组的定义决定了它不能不能扩容,所以一开始要选定数组的长度。
长度就是 原长度+空格的数量×2
原字符串迁移的话,如果顺着更新,每次遇到空格需要将后续字符串往后迁移2单位,这就会使得时间复杂度为O(n2);
不过逆着更新就不会,每次只用移动对应的。
一个指针指着末尾空闲的位置,另一个指针指着原来字符串的末端,如果是常规字符直接转移即可,遇到空格则用‘%20’进行填充(记得是逆向的哦)

private static String func(String s){
        if(s==null||s.length()==0){
            return "";
        }
        char[] cs = s.toCharArray();
        int numBlank = 0;
        for(int i=0;i<cs.length;i++){
            if(cs[i]==' '){
                numBlank++;
            }
        }
        int newLen = cs.length+(numBlank<<1);
        char[] newC = new char[newLen];
        int left = cs.length-1,right = newC.length-1;
        while(left>=0){
            if(cs[left]==' '){
                newC[right--] = '0';
                newC[right--] = '2';
                newC[right--] = '%';
            }else {
                newC[right--] = cs[left];
            }
            left--;
        }
        return new String(newC);
    }

如果合并数组啊字符串啊(那种以一个字符串为基础,然后插着顺序来的),也可以考虑从后往前的来。

从尾到头打印链表

链表对比数组的好处在于,插入和删除的时间复杂度为O(1),但是相应的查询无法根据下标来查询,需要遍历全部,时间复杂度为O(n).

private static void func(ListNode node){
        if(node==null){
            return;
        }
        func(node.next);
        System.out.println(node.val);
    }

重建二叉树

从中序遍历和前序遍历的数组中重组二叉树。

这题还是不错的,实现后,以后编二叉树相关的测试样例方便了很多。
二叉树一般有三种遍历方式(如果层次遍历也算就是四种),前中后。
在这里插入图片描述
(图源百度百科,懒得画了)
前序遍历: 优先遍历根节点,然后遍历左节点,最后遍历右节点。比如上图就是FCADBEHGM
中序遍历: 左中右,ACBDFHEMG
后序遍历: 左右中,ABDCHMGEF
这题中根据前序和中序还原,根据前序,可以知道某一层的根节点,然后在中序遍历中,它之左的都是左子树的节点,之右的都是右子树的节点。
也就是说,用前序节点区分树的左右,再以该节点在中序的位置来限定下一个右子树根节点的位置。
假设现在中序的限定范围为left~right,前序对应的位置在其中的inx位置,inx在中序的位置为mid,那么left~mid-1都是左子树,mid+1 ~ right都是右子树。
左子树的总大小为mid-1-left+1,那么前序里右子树的根节点位置在inx+1+mid-1-left+1.左子树倒是简单,就是inx+1.
好了这些都理解了就可以写代码了。

private static TreeNode func(int[] pre,int[] in,int inx,int left,int right){
        if(left>right||left<0||right<0||left>=pre.length||right>=pre.length)
            return null;
        if(inx>=in.length){
            throw new RuntimeException("数据不匹配");
        }
        int mid = find(pre[inx],in);
        if(mid==-1)
            throw new RuntimeException("数据不匹配");
        TreeNode root = new TreeNode(pre[inx]);
        if(mid<=right||mid>=left){
            root.left =func(pre,in,inx+1,left,mid-1);
            root.right = func(pre,in,inx+mid-left+1,mid+1,right);
        }
        return root;
    }
    private static int find(int pre,int[] in){
       return   IntStream.range(0,in.length).filter(i->pre==in[i]).findAny().orElse(-1);
    }

二叉树的下一个节点

给定一棵二叉树,和其中的一个节点,中序遍历下,该节点的下一个节点是啥。这个二叉树除了左右指针还多了一个指向父节点的指针。

还是从中序的定义出发,中序是左中右。
一个节点有右儿子,则下一个就是它的右边子树的最左节点。
一个节点莫得右儿子,1.自己是属于左子树那就找它的直接父节点,2.如果自己属于右儿子,这种情况麻烦,需要找到一个至少为左子树的祖先,再取该祖先的父节点;如果找不到那就是没得了.

    public TreeLinkNode GetNext(TreeLinkNode pNode)
    {
        if(pNode==null)
            return null;
        TreeLinkNode ans;
        if(pNode.right!=null){
            TreeLinkNode tmpR = pNode.right;
            while(tmpR.left!=null){
                tmpR = tmpR.left;
            }
            ans = tmpR;
        }else if(pNode.next!=null&&pNode.next.right == pNode){
            TreeLinkNode father = pNode.next;
            while(father!=null&&father.next!=null&&father.next.right==father){
                father = father.next;
            }
            ans = father==null?null:father.next;
        }else
            ans = pNode.next;
        return ans;
    }

用两个栈实现队列

栈的特征是后进先出,队列是先进先出。
直到了这个特征,用一个栈用于入队,一个用于出队,第一次出队时,如果两个栈都为空抛出异常,如果不为空,如果出队的栈是空的,则将入队的栈里的元素压入出队的栈,这一就实现“逆序”,与队列原本顺序一致了,如果出队的栈不是空的,直接出队(栈)即可。

static private class Queue<T>{
        java.util.Stack<T> sk1 = new java.util.Stack<>();
        java.util.Stack<T> sk2 = new java.util.Stack<>();
        int size;
        public void offer(T i){
            sk1.push(i);
            size++;
        }
        public boolean isEmpty(){
            return size==0;
        }
        public T poll(){
            if(isEmpty())
                throw new RuntimeException("队列为空");
            if(sk2.isEmpty()){
                while(!sk1.isEmpty()){
                    sk2.push(sk1.pop());
                }
            }
            size--;
            return sk2.pop();
        }
    }

那两个栈实现队列呢?
这个可就比俩栈实现队列麻烦了,只能用一个队列用于临时队列了,当需要出栈时,将队列里除了最后一个全部入另一个队,只剩一个,再将这个出队,然后再交换这俩队列的关系。

static private class Stack<T>{
        java.util.Queue<T> qu1 = new LinkedList<>();
        java.util.Queue<T> qu2 = new LinkedList<>();
        int size;
        public void push(T t){
            qu1.offer(t);
            size++;
        }
        public boolean isEmpty(){
            return size==0;
        }
        public T pop(){
            if(isEmpty())
                throw new RuntimeException("栈为空");
            while(qu1.size()>1){
                qu2.offer(qu1.poll());
            }
            T ans = qu1.poll();
            size--;
            java.util.Queue<T> temp = qu1;
            qu1 = qu2;
            qu2 = temp;
            return ans;
        }
    }

斐波那契数列

见本文

跳楼梯与变态跳楼梯

见本文

旋转数组的最小数字

一个排序数组旋转了,就是原本1,2,3,4,5->4,5,1,2,3…之类的,不存在重复的数字,找出其中的最小数字。

使用二分法,左指针指向最左端,右指针指向右端,取他俩中间的那个值,
如果那个值大于了左指针,
有两种可能,要么处于旋转的那段,要么数组根本没有旋转,第一种最小值肯定再右端,第二种为0号位置,但是由于如果多加判断的话会显得逻辑比较繁琐(也没多大繁琐的,加个左端和右端的判断即可,不过如果不是那种唯一的顺序情况,其他情况下,进行都不必要的额外判断就会浪费些时间)。
如果中间值小于左指针则说明在最小值可能是当前值也可能在左边。

public static int findMin(int[] array){
            if(array==null||array.length<1)
                return -1;
            int left = 0,right = array.length-1;
            while (left<right){
                int mid = left+right>>1;
                if(array[mid]<array[0]){
                    right = mid;
                }else {
                    left = mid+1;
                }
            }
            return Math.min(array[left],array[0]);
        }
    }

矩阵中的路径

给定一个字符矩阵,再给定一个字符串,在矩阵中是否存在满足字符串顺序与字符的路径,路径无环,合法的方向为上下左右。
这是一道回溯的题,不知道存不存在,以所有点作为可能的开始,然后从四个分别探索,如果从某个点开始匹配不上则回退一步从另一个方向探索,直到四个方向都不对,再回退一步,直到退完…或者找到了。
由于不能重复走,所以需要用一个标志来标记在当前路径下某个点是否走过。
遇到这种题可以画一画递归树。

private static boolean find(char[][] m,String target){
        if(m==null||m.length==0)
            return target.length()==0;
        for(int i=0;i<m.length;i++){
            for(int j=0;j<m[0].length;j++){
                if(find(m,target,i,j,0,new boolean[m.length][m[0].length]))
                    return true;
            }
        }
        return false;
    }
    private static boolean find(char[][] m, String target,int x,int y,int inx,boolean[][] flag) {
        if(x<0||x>=m.length||y<0||y>=m[0].length||flag[x][y]||inx==target.length())
            return inx==target.length();
        if(m[x][y]!=target.charAt(inx))
            return false;
        flag[x][y] = true;
        inx++;
        return find(m,target,x+1,y,inx,flag)||find(m,target,x-1,y,inx,flag)||find(m,target,x,y-1,inx,flag)||find(m,target,x,y+1,inx,flag);
    }

机器人的运动范围

地上有m×n的方格,机器人从(0,0)出发,可以沿着上下左右四个方向运动,但是它不能去格子坐标的数位之和大于k的位置。问它能够到达多少个格子。
还是上一题那么回事呗,遇到不能走的回退,遇到能走的,走,然后格子数+1,不过不能走重复的位置。

public int movingCount(int threshold, int rows, int cols) {
        if(threshold<=0)
            return 0;
        return moveCount(rows,cols,0,0,threshold,new boolean[rows*cols]);
    }
    private static int moveCount(int x, int y, int i, int j, int limit,boolean[] flag) {
        if(limit<sum(i)+sum(j)||i<0||i>=x||j<0||j>=y||flag[i*y+j])
            return 0;
        flag[i*y+j] = true;
        return 1+moveCount(x,y,i-1,j,limit,flag)+moveCount(x,y,i+1,j,limit,flag)+moveCount(x,y,i,j+1,limit,flag)+moveCount(x,y,i,j-1,limit,flag);
    }
    private static int sum(int i){
        int count =0;
        while (i>0){
            count+=i%10;
            i/=10;
        }
        return count;
    }

剪绳子

有一段绳子,长度为n可剪成任意段,求段之间的乘积最大的值。

这题能用动态规划解,也可用贪心解决,动态规划本质我觉得是普适版的贪心。
动态规划能否使用有俩条件,1.划分子问题,子问题的累积就是最终结果,2.这些重叠子问题的最终结果的来源是否会影响到后面的结果。
那么贪心就是在能够使用动态规划的基础上,每次采用的策略都是一样的,就可以使用贪心,贪心会比动态规划了来得快。
贴一下俺与大佬的对话,俺觉得对俺很有启示
在这里插入图片描述
回到本题,如果使用动态规划,重叠子问题就是,一段绳子可以拆分为(1~n-1)的长度仍选,然后另外两段又能够拆分,拆分到为1的时候就不拆了。再选择最大的情况保存,上一次拆分再选择能选择的最大的那个保存。

private static int max(int target) {
        if(target<2)
            return 0;
        int[] dp = new int[target];
        dp[0] = 1;
        dp[1] = 2;
        dp[2] = 3;
        for(int i=4;i<=target;i++){
            for(int j=1;j<=i>>1;j++){
                dp[i-1] =Math.max(dp[i-1],dp[i-j-1]*dp[j-1]);
            }
        }
        return dp[target-1];
    }

贪心的话,尽可能的多拆分为3的段数,如果剩下为1,则与3合并,如果剩下为2,则不作处理,最后相乘就行了。

private static int maxT(int target){
        if(target<2)
            return 0;
        int n = target/3;
        if(target-n*3==1)
            n-=1;
        int n2 = target-n*3>>1;
        return myPow(3,n)*myPow(2,n2);
    }

    private static int myPow(int d,int e){
        if(e==0)
            return 1;
        if(e==1)
            return d;
        int tmp = 1;
        while (e>0){
            if((e&1)==1)
                tmp *= d;
            d*=d;
            e>>=1;
        }
        return tmp;
    }

如果拆分的段数限定的话也同贪心的道理,尽可能拆分得平均,如果段数大于每份拆分为3份的段数,则等同于上面那种。

二进制中的1

给定一个int类型的数字,算出其中包含的1的数量。
注意这题数字是可为负数的,负数也就是32位为1,然后对整数的情况下取补码(取反+1)。
那么可以这样用1,一直向右位移,与目标数字求与,如果为1则保存,那个数字位移到第32位位置(该数字就小于0了),如果原数字是负数,则再在结果上+1;

public class Solution {
    public int NumberOf1(int n) {
       int f =1,ans =0;
        while (f>0){
            if((n&f)!=0)
                ans++;
            f=f<<1;
        }
        return n<0?ans+1:ans;
    }
}

也可以用另一种方法,一个数字自己与自己求与肯定是不会变的,那么它-1,就相当于减去了最低位,再求与最低位就被抵消掉,如果此时该数不为0,则说明至少还有一个位为1,直到结果为1为止。

private static int count(int n){
        int ans =0;
        while (n!=0){
            ans++;
            n = (n-1)&n;
        }
        return ans;
    }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值