赶紧准备春招

算法 | 一周刷完《剑指Offer》 Day1:第1~16题

2018-12-11  本文已影响0人  机盐Johnny

开个新坑,准备校招研发岗面试,基本的算法还是要过关的。


写在前面

下一篇:算法 | 一周刷完《剑指Offer》 Day2:第17~26题


Day1:第1~16题

后面总会遇到难的绕的题,前两天多做几道,总不会亏的。


T1. 二维数组中的查找

题目描述

在一个二维数组中(每个一维数组的长度相同),每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺序排序。请完成一个函数,输入这样的一个二维数组和一个整数,判断数组中是否含有该整数。

解题思路

因为矩阵中的每一个数,左边都比它小,下边都比它大。因此,从右上角开始查找,就可以根据 target 和当前元素的大小关系来缩小查找区间。

    public boolean Find(int target, int[][] array) {
        if(array == null || array.length == 0 || array[0].length == 0) {
            return false;
        }
        
        int m = 0, n = array[0].length - 1;//从右上角开始找,array[0][n-1]
        
        while(m <= array.length - 1 && n >= 0) {
            if(target == array[m][n])
                return true;
            else if(target > array[m][n])
                m ++;
            else
                n --;
        }
        
        return false;
    }

T2. 替换空格

题目描述

请实现一个函数,将一个字符串中的每个空格替换成“%20”。例如,当字符串为We Are Happy,则经过替换之后的字符串为We%20Are%20Happy。

解题思路

由于每个空格替换成了三个字符,首先要扩容字符串长度至替换后的长度,因此当遍历到一个空格时,需要在尾部填充两个任意字符。

然后声明两个下标,一个为原字符串末尾下标 i,一个为现字符串末尾 j,两个下标同步从后往前扫。当 i 指向空格时,j 就倒着依次添加 '0', '2', '%'。
这样做的目的是: j 下标不会超过 i 下标,不会影响原字符串的内容。

    public String replaceSpace(StringBuffer str) {
        int oldLen = str.length();
        
        for(int i = 0; i < oldLen; i ++) {
            if(str.charAt(i) == ' ') {//每出现一个空格,在结尾添加两个任意字符,扩充字符串长度
                str.append("12");
            }
        }
        
        int i = oldLen - 1;
        int j = str.length() - 1;
        
        while(i >= 0 && j > i) {
            char c = str.charAt(i);
            i --;
            
            if(c == ' ') {//每出现一个空格,替换为%20
                str.setCharAt(j, '0');
                j --;
                str.setCharAt(j, '2');
                j --;
                str.setCharAt(j, '%');
                j --;
            } else {//否则照旧
                str.setCharAt(j, c);
                j --;
            }
        }
        
        return str.toString();
    }

T3. 从尾到头打印链表

题目描述

输入一个链表,按链表值从尾到头的顺序返回一个ArrayList。

解题思路

使用栈实现:先入后出。全部入栈,依次出栈,顺序即为从尾到头。

    public ArrayList<Integer> printListFromTailToHead(ListNode listNode) {
        //使用栈实现,先入后出
        Stack<Integer> stack = new Stack<>();
        ArrayList<Integer> arrayList = new ArrayList<>();
        
        while(listNode != null) {
            stack.push(listNode.val);
            listNode = listNode.next;
        }
        
        while(!stack.isEmpty()) {
            arrayList.add(stack.pop());
        }
        
        return arrayList;
    }

使用递归实现:先加入链表后面的结点,再加入当前结点,顺序即为从尾到头。

    public ArrayList<Integer> printListFromTailToHead(ListNode listNode) {
        //使用递归实现,先加入链表后面的结点,再加入当前结点
        ArrayList<Integer> arrayList = new ArrayList<>();
        
        if(listNode != null) {
            arrayList.addAll(printListFromTailToHead(listNode.next));//先递归
            arrayList.add(listNode.val);//再加入当前
        }
        
        return arrayList;
    }

T4. 重建二叉树

题目描述

输入某二叉树的前序遍历和中序遍历的结果,请重建出该二叉树。假设输入的前序遍历和中序遍历的结果中都不含重复的数字。例如输入前序遍历序列{1,2,4,7,3,5,6,8}和中序遍历序列{4,7,2,1,5,3,8,6},则重建二叉树并返回。

解题思路

首先清楚以下性质:
前序遍历:根 -> 左 -> 右,中序遍历:左 -> 根 -> 右。

因此一颗二叉树中,前序遍历第一个结点为根结点,通过它在中序遍历中的位置,可以将中序遍历分为两部分,左半部分是该结点的左子树右半部分是该结点的右子树

根据这个原理,递归执行即可重构出二叉树。

    private Map<Integer, Integer> map = new HashMap<>();//用于查找中序遍历数组中,每个值对应的索引
    
    public TreeNode reConstructBinaryTree(int[] pre, int[] in) {
        for(int i = 0; i < in.length; i ++) {
            map.put(in[i], i);//key: in的值,value: 值在in中位置
        }
        
        return getBiTree(pre, 0, pre.length - 1, 
                            in, 0, in.length - 1);
    }
    
    private TreeNode getBiTree(int[] pre, int preLeft, int preRight, //前序遍历及当前在前序遍历中的区间
                                int[] in, int inLeft, int inRight) { //中序遍历及当前在前序遍历中的区间
        if(preLeft == preRight) { //即根据前序遍历,当前结点无子结点
            return new TreeNode(pre[preLeft]);
        }
        if(preLeft > preRight || inLeft > inRight) {
            return null;
        }
        
        TreeNode root = new TreeNode(pre[preLeft]);
        int inIndex = map.get(root.val);
        int leftTreeSize = inIndex - inLeft;//该结点左半部分的结点数
        
        //递归,获取左子树及右子树
        root.left = getBiTree(pre, preLeft + 1, preLeft + leftTreeSize, //左半部分在前序遍历中的区间
                                in, inLeft, inIndex - 1);//左半部分在中序遍历中的区间
        root.right = getBiTree(pre, preLeft + 1 + leftTreeSize, preRight, //右半部分在前序遍历中的区间
                                in, inIndex + 1, inRight);//右半部分在中序遍历中的区间
        
        return root;
    }

T5. 用两个栈实现队列

题目描述

用两个栈来实现一个队列,完成队列的Push和Pop操作。 队列中的元素为int类型。

解题思路

队列:先进先出,:先进后出

例如,假设 1 ~ n 的数字顺序,入Stack1,出的顺序为 n ~ 1 。

这时,Stack1出栈进入Stack2,进入Stack2的顺序为n ~ 1,那么Stack2出的顺序为 1 ~ n 。

相当于队列 1 ~ n 进, 1 ~ n 出。

    Stack<Integer> stack1 = new Stack<Integer>();//stack1入栈时使用
    Stack<Integer> stack2 = new Stack<Integer>();//stack2出栈时使用,直接出栈即可
    
    public void push(int node) {
        while(!stack2.isEmpty()) {//先将stack2中所有元素压入stack1
            stack1.push(stack2.pop());
        }
        stack1.push(node);//然后将当前要进入队列元素压入stack1
        while(!stack1.isEmpty()) {//最后将stack1所有元素压入stack2
            stack2.push(stack1.pop());//此时stack2中出栈顺序即为队列出栈顺序,相当于先入先出
        }
    }
    
    public int pop() {
        return stack2.pop();
    }

T6. 旋转数组的最小数字

题目描述

把一个数组最开始的若干个元素搬到数组的末尾,我们称之为数组的旋转。 输入一个非减排序的数组的一个旋转,输出旋转数组的最小元素。 例如数组{3,4,5,1,2}为{1,2,3,4,5}的一个旋转,该数组的最小值为1。 NOTE:给出的所有元素都大于0,若数组大小为0,请返回0。

解题思路

我们可以认为数组分成了两部分,前半部分是较大的数,后半部分是较小的数

利用二分查找的思路,观察取中的数在前半部分还是后半部分,以此来找出最小的数(后半部分的第一个数)。

    public int minNumberInRotateArray(int[] array) {
        if(array.length == 0) {
            return 0;
        }
        
        //二分查找
        int low = 0, high = array.length - 1;
        while (array[low] >= array[high]) {
            if(high - low == 1) {
                return array[high];
            }
            int mid = low + (high - low) / 2;//取中
            if(array[mid] >= array[low]) {//此时mid在前半区大的数里
                low = mid;
            } else {//此时mid在后半区小的数里
                high = mid;
            }
        }
        return array[low];
    }

T7. 斐波那契数列

题目描述

大家都知道斐波那契数列,现在要求输入一个整数n,请你输出斐波那契数列的第n项(从0开始,第0项为0)。n<=39

解题思路

斐波那契数列:F[n]=F[n-1]+F[n-2] (n>=3,F[1]=1,F[2]=1)

注意:此题要求F[1]=0。

    public int Fibonacci(int n) {//n<=39
        int[] array = new int[40];
        array[0] = 0;
        array[1] = 1;
        for(int i = 2; i < array.length; i ++) {
            array[i] = array[i - 1] + array[i - 2];
        }
        
        return array[n];
    }

T8. 跳台阶

题目描述

一只青蛙一次可以跳上1级台阶,也可以跳上2级。求该青蛙跳上一个n级的台阶总共有多少种跳法(先后次序不同算不同的结果)。

解题思路

动态规划的思想。这里采用倒着往前推的方法递减target,最后一次跳1或最后一次跳2,倒着往前递归。

    public int JumpFloor(int target) {
        if(target == 0) {
            return 0;
        }
        if(target == 1) {
            return 1;
        }
        if(target == 2) {
            return 2;
        }
        if(target > 2) {//递归求可能出现的情况总数(最后一次跳1或最后一次跳2,倒着往前推)
            return JumpFloor(target - 1) + JumpFloor(target - 2);
        }
        
        return 0;
    }

或者,其本质上是斐波那契数列的原理。

    public int JumpFloor(int target) {
        if(target == 0) {
            return 0;
        }
        if(target == 1) {
            return 1;
        }

        int[] array = new int[target];
        array[0] = 1;
        array[1] = 2;
        for (int i = 2; i < target; i++) {
            array[i] = array[i - 1] + array[i - 2];
        }
        return array[target - 1];
    }

T9. 变态跳台阶

题目描述

一只青蛙一次可以跳上1级台阶,也可以跳上2级……它也可以跳上n级。求该青蛙跳上一个n级的台阶总共有多少种跳法。

解题思路

同理,动态规划的思想,递归求解。(这次情况有点多,用到循环了)

    public int JumpFloorII(int target) {
        if(target == 0 || target == 1) {
            return 1;
        }
        if(target == 2) {
            return 2;
        }
        
        int sum = 0;
        for(int i = 0; i < target; i ++) {//本次跳0次到跳target-1次
            sum += JumpFloorII(i);//对于本次的跳跃又有多少种跳法?递归获取结果
        }
        
        return sum;
    }

T10. 矩阵覆盖

题目描述

我们可以用 2 * 1 的小矩形横着或者竖着去覆盖更大的矩形。请问用n个 2 * 1 的小矩形无重叠地覆盖一个 2 * n 的大矩形,总共有多少种方法?

解题思路

原理同T8,详见图片。

    public int RectCover(int target) {
        if(target == 0) {
            return 0;
        }
        if(target == 1) {
            return 1;
        }

        int[] array = new int[target];
        array[0] = 1;
        array[1] = 2;
        for (int i = 2; i < target; i++) {
            array[i] = array[i - 1] + array[i - 2];
        }
        return array[target - 1];
    }

T11. 二进制中 1 的个数

题目描述

输入一个整数,输出该数二进制表示中1的个数。其中负数用补码表示。

解题思路

Integer.bitCount(n):技术整型的二进制表示中1的个数。。。

    public int NumberOf1(int n) {//(???)
        return Integer.bitCount(n);
    }

正常算法:&按位与。每次将 n 和 n-1 进行 & 运算,从右往左去掉二进制最右边的一个1。例如:

n:           100100
n - 1:       100011
n & (n-1):   100000

一次运算后,n由100100变为100000,去除了一个1。所有1去完时,n为0。

    public int NumberOf1(int n) {//正常算法
        int sum = 0;
        
        while(n != 0) {
            sum ++;
            n = n & (n-1);//&按位与
        }
        
        return sum;
    }

T12. 数值的整数次方

题目描述

给定一个double类型的浮点数base和int类型的整数exponent。求base的exponent次方。

解题思路

由于xn = (x*x)n/2,通过递归求解可减小时间复杂度,每递归一次,n 减一半。时间复杂度O(logn)。

注意:需要小心 n 的正负、奇偶。

    public double Power(double base, int exponent) {
        if(exponent == 0) return 1;
        if(exponent == 1) return base;
        
        boolean isPositive = true;//正负次方以此判断
        if(exponent < 0) {
            exponent = -exponent;
            isPositive = false;
        }
        
        double pow = Power(base * base, exponent / 2);//递归,每次递归n减小一半,即 (x*x)^(n/2)
        if(exponent % 2 != 0) pow *= base;//奇次幂的话,/2会少算一次,在这补回来
        
        return isPositive ? pow : (1 / pow);//三元表达式,正次幂为pow,负次幂为1/pow
    }

T13. 调整数组顺序使奇数位于偶数前面

题目描述

输入一个整数数组,实现一个函数来调整该数组中数字的顺序,使得所有的奇数位于数组的前半部分,所有的偶数位于数组的后半部分,并保证奇数和奇数,偶数和偶数之间的相对位置不变。

解题思路

先统计奇偶数的个数。在所要求的数组中,奇数的个数即为数组中偶数应该开始储存的位置。

clone一个数组,按顺序往原数组里存,奇数存前面,偶数存后面。

    public void reOrderArray(int[] array) {
        int oddNum = 0;
        for(int value: array) {
            if(value % 2 == 1) {
                oddNum++;
            }
        }
        
        int[] copyArray = array.clone();//克隆数组,对原数组赋值
        int i = 0, j = oddNum;//j为偶数开始存储的位置
        
        for(int num : copyArray) {
            if(num % 2 == 1) {
                array[i] = num;
                i ++;
            } else {
                array[j++] = num;
                j ++;
            }
        }
    }

或者,声明两个ArrayList存奇数和偶数,然后合并。

    public void reOrderArray(int[] array) {
        ArrayList<Integer> odd = new ArrayList<>();
        ArrayList<Integer> even = new ArrayList<>();
        
        for(int i = 0; i < array.length; i ++) {
            if(array[i] % 2 == 0) {
                even.add(array[i]);
            } else {
                odd.add(array[i]);
            }
        }
        odd.addAll(even);

        for(int i = 0; i < array.length; i ++) {
            array[i] = odd.get(i);
        }
    }

T14. 链表中倒数第 K 个结点

题目描述

输入一个链表,输出该链表中倒数第k个结点。

解题思路

假设,共 n 个结点,倒数第k个结点即为第 n-k+1 个结点。

定义两个指针node1和node2,使他们都指向第一个结点。到第 n-k+1 个结点则需要移动 n-k次。

此时,node1移动n次会指向空。

先让node1移动 k 次,还剩 n-k 次指向空。

这时,node2与node1同步移动,当node1指空时,node2移动了 n-k 次,刚好到第 n-k+1 个结点。

    public ListNode FindKthToTail(ListNode head, int k) {
        if(head == null) return null;
        
        ListNode node1 = head;
        ListNode node2 = head;
        
        while(node1 != null && k > 0) {//node1移动k次,还有n-k次会指空
            node1 = node1.next;
            k --;
        }
        
        if(k > 0) return null;
        
        while(node1 != null) {//移动n-k次,此时node2到n-k+1个结点,即倒数第k个结点
            node1 = node1.next;
            node2 = node2.next;
        }
        
        return node2;
    }

T15. 反转链表

题目描述

输入一个链表,反转链表后,输出新链表的表头。

解题思路

关键在于声明一个结点preNode用来记录前一个结点,使下一次循环时的结点的next指向它,反转顺序。

    public ListNode ReverseList(ListNode head) {
        if(head == null || head.next == null) return head;
        
        ListNode preNode = null;
        
        while(head != null) {
            ListNode tmp = head.next;//tmp记录【下一个结点】
            head.next = preNode;//【当前结点】的next指向【前一个结点】,反转链表顺序
            preNode = head;//preNode记录【当前结点】,即【下一个结点】的【前一个结点】
            head = tmp;//将【当前结点】更改为【下一个结点】,进入下一次循环
        }
        //当head为null时跳出了循环,此时它的前一个结点preNode即为原链表的最后一个结点,链表顺序已反转
        return preNode;
    }

T16. 合并两个排序的链表

题目描述

输入两个单调递增的链表,输出两个链表合成后的链表,当然我们需要合成后的链表满足单调不减规则。

解题思路

声明一个新链表,不断比较原来两个链表的 val 值,小的插入新链表即可。

注意:要求返回的表头是声明的链表的next结点。

    public ListNode Merge(ListNode list1, ListNode list2) {
        ListNode head = new ListNode(-9999);
        ListNode tmp = head;
        
        while(list1 != null && list2 != null) {
            if(list1.val < list2.val) {//比较两个链表当前结点的值,小的先插入新链表
                tmp.next = list1;
                list1 = list1.next;
            } else {
                tmp.next = list2;
                list2 = list2.next;
            }
            tmp = tmp.next;
        }
        
        //容错,将剩余链表补到新链表结尾,此时能保持单调不减
        if(list1 != null) tmp.next = list1;
        if(list2 != null) tmp.next = list2;
        
        return head.next;//记得head为声明的无意义表头,head.next才是新链表的头
    }

项目地址https://github.com/JohnnyJYWu/offer-Java

下一篇算法 | 一周刷完《剑指Offer》 Day2:第17~26题

希望这篇文章对你有帮助~

上一篇下一篇

猜你喜欢

热点阅读