剑指offer(Java版)

剑指offer|31-40题解题思路及代码(Java版)

2019-04-12  本文已影响0人  蓝白绛

剑指offer31到40题总览:

  1. 整数中1出现的次数(从1到n整数中1出现的次数)
  2. 把数组排成最小的数
  3. 丑数
  4. 第一个只出现一次的字符
  5. 数组中的逆序对
  6. 两个链表的第一个公共结点
  7. 数字在排序数组中出现的次数
  8. 二叉树的深度
  9. 平衡二叉树
  10. 数组中只出现一次的数字

31、整数中1出现的次数(从1到n整数中1出现的次数)

题目描述

求出113的整数中1出现的次数,并算出1001300的整数中1出现的次数?为此他特别数了一下1~13中包含1的数字有1、10、11、12、13因此共出现6次,但是对于后面问题他就没辙了。ACMer希望你们帮帮他,并把问题更加普遍化,可以很快的求出任意非负整数区间中1出现的次数(从1 到 n 中1出现的次数)。

解题思路

暴力的方法,将1到n变成字符串类型,遍历每个字符,等于'1'则count+1。或其他方法:
先以n=100为例,可知有1的数有1 10 11 12 13 14 15 16 17 18 19 21 31 41 51 61 71 81 91 100。其中11出现了两次1,要算两次。我们换一种数法。
计算个位数为1的个数:1 11 21 31 41 51 61 71 81 91。有10个。
计算十位数为1的个数:10 11 12 13 14 15 16 17 18 19。有10个。
计算百位数为1的个数:100。有1个
可以看到,这样的数法能巧妙地将11计算两次。由此我们可以推出以下巧妙的解法。

参考牛客网的解法:链接:https://www.nowcoder.com/questionTerminal/bd7f978302044eee894445e244c7eee6
例如我们现在要计算百位数为1的数的个数。n=3141592,我们令i=100,由此利用%100和/100可以将n=3141592分成 31415|92 ,a=31415,b=92。
我们知道100~199有100个数,这些数的百位数都为1。
当百位数>=2时,3141500则经历了3142个完整的从100~199的数(不管其它位),从100~199, 1100~1199, 2100~2199, 3100~3199,...,3141100~3141199一共有0~3141个完整的100~199的数。
当百位数=1时,则经历了3141个完整的从100~199的数,然后还有第二部分的b=92,会经历从100~192的一共93个百位数为1的数。
当百位数=0时,则不用考虑第二部分。
综上,则当i=100时,会经历(a+8)/10个完整的100~199共100个数。再加上如果百位数为1,需要考虑的第二部分为(a%10==1)*(b+1)个百位数为1的数。
这里(a+8)/10的原因是,如果百位数>=2,则a+8会发生进位,可以巧妙地多计算一个完整的从100~199的数,而=0或=1时不会计算。如果我们要计算的是整数1~n中出现2的次数,则为(a+7)/10,当百位数>3=时发生进位。

public class Solution {
    public int NumberOf1Between1AndN_Solution(int n) {
        int count = 0;
        for(int i=1; i<=n; i*=10){
            int a = n/i;//计算前半部分
            int b = n%i;//计算后半部分
            if(a%10 == 1){//对前面部分的末位进行判断
                count = count + (a/10)*i + (b+1);//加上后半段
            }else if(a%10 == 0){
                count = count + (a/10)*i;//不需要加上后半段
            }else{
                count = count + (a/10 + 1)*i;
            }
        }
        return count;
    }
}

32、把数组排成最小的数

题目描述

输入一个正整数数组,把数组里所有数字拼接起来排成一个数,打印能拼接出的所有数字中最小的一个。例如输入数组{3,32,321},则打印出这三个数字能排成的最小数字为321323。

解题思路

总体思路是基于排序,给数字排一个先后顺序。比较的时候,将数字转化为String类型,对两种拼接类型进行比较,选择拼接的数字比较小的那一种,保持它们在list中的相对顺序。

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;

public class Solution {
    public String PrintMinNumber(int [] numbers) {
        if(numbers.length == 0){return "";}
        ArrayList<Integer> list = new ArrayList<>();//新建一个list,可用Collections.sort进行排序。
        for(int i=0; i<numbers.length; i++){//将数组中的数加入list
            list.add(numbers[i]);
        }
        Collections.sort(list, new Comparator<Integer>(){
            @Override
            public int compare(Integer a, Integer b){//重写compare函数
                String s1 = String.valueOf(a);
                String s2 = String.valueOf(b);
                return (s1+s2).compareTo(s2+s1);//s1+s2比较小则返回-1,升序排列
            }
        });
        String result = "";//将list转化为输出结果
        for(int i=0; i<list.size(); i++){
            result = result+list.get(i);
        }
        return result;
    }
}

33、丑数

题目描述

把只包含质因子2、3和5的数称作丑数(Ugly Number)。例如6、8都是丑数,但14不是,因为它包含质因子7。 习惯上我们把1当做是第一个丑数。求按从小到大的顺序的第N个丑数。

解题思路

建立三个队列,对应2、3、5的队列,每次将三个队列的队头元素最小的数出队列,如果有多个相同的最小值,则全出列,这样可以避免重复。出队列之后将其2、3、5的倍数入相应队列。

import java.util.LinkedList;

public class Solution {
    public int GetUglyNumber_Solution(int index) {
        if(index==0){return 0;}
        if(index==1){return 1;}
        LinkedList<Integer> q2 = new LinkedList<>();
        LinkedList<Integer> q3 = new LinkedList<>();
        LinkedList<Integer> q5 = new LinkedList<>();
        q2.offer(2);//将1*2, 1*3, 1*5入各自的队列
        q3.offer(3);
        q5.offer(5);
        for(int i=2; i<index; i++){
            int min = Math.min(Math.min(q2.peek(), q3.peek()), q5.peek());
            if(min == q2.peek()){q2.poll();}//这里要是连续的if,而不是if else语法,因为可能发现队头元素一样大的情况,这种要将一样的数全部出队列。
            if(min == q3.peek()){q3.poll();}
            if(min == q5.peek()){q5.poll();}
            q2.offer(min*2);//出了队列之后,将其2,3,5的倍数入相应队列
            q3.offer(min*3);
            q5.offer(min*5);
        }
        return Math.min(Math.min(q2.peek(), q3.peek()), q5.peek());
    }
}

34、第一个只出现一次的字符

题目描述

在一个字符串(0<=字符串长度<=10000,全部由字母组成)中找到第一个只出现一次的字符,并返回它的位置, 如果没有则返回 -1(需要区分大小写).

解题思路

用HashMap记录出现次数

import java.util.HashMap;

public class Solution {
    public int FirstNotRepeatingChar(String str) {
        if(str.length() == 0){return -1;}
        if(str.length() == 1){return 0;}
        HashMap<Character, Integer> map = new HashMap<>();
        for(int i=0; i<str.length(); i++){//遍历字符串,记录每个字符出现的次数
            if(map.containsKey(str.charAt(i))){
                int count = map.get(str.charAt(i));
                map.put(str.charAt(i), count+1);
            }else{
                map.put(str.charAt(i), 1);
            }
        }
        for(int i=0; i<str.length(); i++){//遍历字符串,看那一个字符出现次数为1,第一个发现次数为1的返回
            if(map.get(str.charAt(i)) == 1){
                return i;
            }
        }
        return -1;
    }
}

35、数组中的逆序对

题目描述

在数组中的两个数字,如果前面一个数字大于后面的数字,则这两个数字组成一个逆序对。输入一个数组,求出这个数组中的逆序对的总数P。并将P对1000000007取模的结果输出。 即输出P%1000000007
输入描述:
题目保证输入的数组中没有的相同的数字
数据范围:
对于%50的数据,size<=10^4
对于%75的数据,size<=10^5
对于%100的数据,size<=2*10^5

解题思路

此题需要一个稳定的排序算法,并记录每个数前移多少位,这样才能计算逆序对。稳定的有:冒泡排序、插入排序、归并排序。冒泡和插入排序都是O(n^2),试了冒泡排序,无法通过。

注意事项

  1. 本题有一个数组长度特别大的用例,算法应该尽量快。
public class Solution {
    long count = 0;
    public int InversePairs(int [] array) {
        mergeSort(array, 0, array.length);
        return (int)(count % 1000000007);
    }

    public void mergeSort(int[] arr, int start, int end){
        if(end-start > 1){
            int mid = (end+start+1)/2;
            mergeSort(arr, start, mid);//对左半部分递归
            mergeSort(arr, mid, end);//对右半部分递归

            int i = start;
            int j = mid;
            int[] temp = new int[end-start];
            int index = 0;
            while(j != end && i != mid){//合并两个部分,比较i和j指向的数,将更小的数写入temp数组,起到排序作用
                if(arr[j] < arr[i]){
                    temp[index] = arr[j];
                    count = count + j - start - index;
                    ++j;
                }else{
                    temp[index] = arr[i];
                    ++i;
                }
                ++index;
            }
            if(j!=end){//如果是后半部分没有读完,则可以不用把后半部分复制到temp数组中,直接向前将前面的数覆盖到原数组中
                for(--index; index>-1;index--){
                    arr[index+start] = temp[index];
                }
            }else{
                for(; index<temp.length; index++){//前半部分没有读完,将前半部分的数复制到temp中
                    temp[index] = arr[i];
                    ++i;
                }
                for(int k=0; k<temp.length; k++){//将temp数组整个覆盖到原数组中
                    arr[k+start] = temp[k];
                }
            }
        }
    }
}

36、两个链表的第一个公共结点

题目描述

输入两个链表,找出它们的第一个公共结点。

解题思路

一个方法是先计算两个链表的长度,如果链表1比链表2长k个结点,则链表1先走k个结点,然后与链表2一起走,每走一步判断是否是同一个结点,第一个相同的结点即是返回结果。
另一个方法是用hashmap,先将链表1中的所有结点加入hashmap,然后对链表2中的每个结点,判断是否在hashmap中存在,第一个在hashmap中已经存在的结点,即是返回结果。

import java.util.HashMap;
public class Solution {
    public ListNode FindFirstCommonNode(ListNode pHead1, ListNode pHead2) {
        if(pHead1==null || pHead2==null){return null;}
        HashMap<ListNode, Integer> map = new HashMap<>();
        do{//将链表1中的结点全部入hashmap
            map.put(pHead1, 1);
            pHead1 = pHead1.next;
        }while(pHead1!=null);
        do{//对链表2中的每个结点,查看是否已经在hashmap中存在
            if(map.containsKey(pHead2)){
                return pHead2;
            }else{
                pHead2 = pHead2.next;
            }
        }while(pHead2!=null);
        return null;
    }
}

37、数字在排序数组中出现的次数

题目描述

统计一个数字在排序数组中出现的次数。

解题思路

数组有序,用二分法查找该数组,然后左右扩展看看有多少个。

public class Solution {
    public int GetNumberOfK(int [] array , int k) {
        if(array.length == 0){return 0;}
        if(array.length == 1){
            if(array[0] == k){return 1;}
            else{return 0;}
        }
        int i = 0;
        int j = array.length-1;
        int index = -1;//index保存要寻找的数的下标
        while(i<=j){//当j小于i时循环停止
            index = (i+j)/2;
            if(array[index] == k){break;}//在该i,j段的中间找到目标值,跳出循环
            else if(array[index] < k){//如果中间值小于目标值,则在右侧寻找
                i = index+1;
            }else{//如果中间值大于目标值,则在左侧寻找
                j = index-1;
            }
        }
        int count = 0;
        if(array[index]==k){//如果是中间值等于目标值跳出循环,则可左右扩展寻找目标值的个数;如果是因为j<i而跳出,则返回0。
            count++;//先将index算上一个
            for(int t=index-1; t>=0; t--){//向左寻找个数
                if(array[t] < k){
                    break;
                }else{
                    count++;
                    System.out.println("+1");
                }
            }
            for(int t=index+1; t<array.length; t++){//向右寻找个数
                if(array[t] > k){
                    break;
                }else{
                    count++;
                    System.out.println("+1");
                }
            }
        }
        return count;
    }
}

38、二叉树的深度

题目描述

输入一棵二叉树,求该树的深度。从根结点到叶结点依次经过的结点(含根、叶结点)形成树的一条路径,最长路径的长度为树的深度。

解题思路

递归的方法最简洁

public class Solution {
    public int TreeDepth(TreeNode root) {
        if(root==null){return 0;}
        int left = TreeDepth(root.left);//求左子树深度
        int right = TreeDepth(root.right);//求右子树深度
        return left>right? left+1:right+1;//返回更深的子树+1
    }
}

39、平衡二叉树

题目描述

输入一棵二叉树,判断该二叉树是否是平衡二叉树。

解题思路

用递归的方法计算树的深度,如果子树是平衡的,则返回子树深度,如果子树不平衡,则返回-1,然后-1可一路返回到根节点上。

注意事项

  1. 判断树是否平衡,需要对每个结点计算其左右子树的深度,并判断左右子树的深度差的绝对值是否小于等于1。如果采用从根节点到叶子节点的方法依次计算深度,进而判断是否平衡,则会产生很多重复计算深度的情况。
public class Solution {
    public boolean IsBalanced_Solution(TreeNode root) {
        if(root==null){return true;}
        if(getSubtreeDiff(root) == -1){//如果子树差返回了-1,则返回false,树不平衡
            return false;
        }else{return true;}//子树差返回不是-1,则返回true,树平衡
    }

    public int getSubtreeDiff(TreeNode root){
        if(root==null){return 0;}//如果结点为空,则当前树平衡
        int left = getSubtreeDiff(root.left);//计算root左子树是否平衡,如果平衡则返回左子树的高度
        if(left == -1){return -1;}//如果左子树不平衡,则可一路返回-1
        int right = getSubtreeDiff(root.right);//计算root右子树是否平衡,如果平衡则返回右子树高度
        if(right == -1){return -1;}//如果右子树不平衡,则可一路返回-1
        return Math.abs(left-right)<=1? 1+Math.max(left, right): -1;//左右子树都是平衡的,那么计算root节点是否平衡,如果左右高度差绝对值小于等于1,则平衡,返回root树的深度;如果不平衡,则返回-1,可一路返回-1
    }
}

40、数组中只出现一次的数字

题目描述

一个整型数组里除了两个数字之外,其他的数字都出现了偶数次。请写程序找出这两个只出现一次的数字。

解题思路

一种方法是用hashmap,另一种方法是用异或的方法。
我们将数组中的所有数都异或起来得到n,则其他相同的数都会被抵消,最后异或的结果,就是两个只出现了一次的数的异或n
得到了两个数的异或,将它们分开的方法如下:我们利用异或的性质,0^1=1、1^0=1、0^0=0、1^1=0,可以得到当两位不相同时,可以得到1,也就是说,我们可以找到n二进制从右至左的第一个1(first1_index),则num1和num2关于这一位一定相反,一个该位是1,一个该位为0。
我们根据这一个性质,将数组中的数可以分为两部分。一部分是该位为1的数,另一部分是该位为0的数。每组相同的数会被抵消,只剩下一个只出现一次的数。

注意事项

  1. java中的:与&、或|、非~、异或^、向左位移<<、向右位移>>。
public class Solution {
    public void FindNumsAppearOnce(int [] array,int num1[] , int num2[]) {
        if(array.length == 2){
            num1[0] = array[0];
            num2[0] = array[1];
        }
        int n = array[0];
        for(int i=1; i<array.length; i++){//将数组中所有数异或起来,得到n
            n = n ^ array[i];
        }
        int first1_index = 0;//查找n的二进制中从右至左的第一个1,得到该位的下标first1_index
        while((n&1)!=1 && first1_index<32){
            n >>= 1;
            first1_index++;
        }
        for(int i=0; i<array.length; i++){
            if(((array[i]>>first1_index) & 1)==0){//根据array[i]二进制该位的值是0还是1,进行分组
                num1[0]^=array[i];//该位为0的与num1[0]进行异或
            }else{
                num2[0]^=array[i];//该位为1的与num2[0]进行异或
            }
        }
    }
}

结尾

如果您发现我的文章有任何错误,或对我的文章有什么好的建议,请联系我!如果您喜欢我的文章,请点喜欢~*我是蓝白绛,感谢你的阅读!

上一篇下一篇

猜你喜欢

热点阅读