剑指offer(31-40)

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

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

思路:定义一个计数变量sum,将整数1-n转换为字符串,然后for循环遍历字符串中的每个字符,当字符满足判断条件,即字符等于1时,sum++。

public class Solution {
    public int NumberOf1Between1AndN_Solution(int n) {
    int sum=0;
        for(int i=1;i<=n;i++){
            String str=String.valueOf(i);
            for(int j=0;j<str.length();j++){
                if(str.charAt(j)=='1'){
                    sum++;
                }
            }
        }
        return sum;
    }
}

32、把数组排成最小的数

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

思路:将数组中所有的数字的类型全部转为String类型,用数组str存起来。接下来为数组中的字符串排序,排序规则为:将两个字符串拼接,s1+s2和s2+s1,比较拼接的两个字符串的大小(按字典序),如"3"和"32",拼接后为"332"和"323",而"323"小于"332",此时"32"将会被排到"3"前面,两个数比较之后,我们得到了这两个数排列顺序的局部最优,如果所有数都按照这种方法排列,那么数组的排序就按照我们想要的方式达到了全局最优。排列完毕后只需要按顺序将所有的字符串拼接返回即可。

代码

import java.util.*;
import java.util.ArrayList;
public class Solution {
    public String PrintMinNumber(int [] numbers) {
        if(numbers.length==0||numbers==null){
            return "";
        }
        List<Integer> list=new ArrayList<Integer>();
        for(int i=0;i<numbers.length;i++){
            list.add(numbers[i]);
        }
        list.sort((a,b)->(a+""+b).compareTo(b+""+a));
        StringBuffer stringBuffer=new StringBuffer();
        for(int a:list){
            stringBuffer.append(a);
        }
        return stringBuffer.toString();
    }
}

33、丑数

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

思路:来源于牛客网事无巨细,悉究本末的思路,根据丑数的定义,链接:https://www.nowcoder.com/questionTerminal/6aa9e04fc3794f68acf8778237ba065b

一个丑数的因子只有2,3,5,那么丑数p = 2 ^ x * 3 ^ y * 5 ^ z,换句话说一个丑数一定由另一个丑数乘以2或者乘以3或者乘以5得到,那么我们从1开始乘以2,3,5,就得到2,3,5三个丑数,在从这三个丑数出发乘以2,3,5就得到4,6,10,6,9,15,10,15,25九个丑数,我们发现这种方得到重复的丑数,而且我们题目要求第N个丑数,这样的方法得到的丑数也是无序的。那么我们可以维护三个队列:

  • 丑数数组:1

乘以2的队列:2

乘以3的队列:3

乘以5的队列:5

选择三个队列头最小的数2加入丑数数组,同时将该最小的数乘以2,3,5放入三个队列;

  • 丑数数组:1,2

乘以2的队列:4

乘以3的队列:3,6

乘以5的队列:5,10

选择三个队列头最小的数3加入丑数数组,同时将该最小的数乘以2,3,5放入三个队列

  • 丑数数组:1,2,3

乘以2的队列:4,6

乘以3的队列:6,9

乘以5的队列:5,10,15

选择三个队列头里最小的数4加入丑数数组,同时将该最小的数乘以**2,3,5放入三个队列;

  • 丑数数组:1,2,3,4

乘以2的队列:6,8

乘以3的队列:6,9,12

乘以5的队列:5,10,15,20

选择三个队列头里最小的数5加入丑数数组,同时将该最小的数乘以2,3,5放入三个队列;

  • 丑数数组:1,2,3,4,5

乘以2的队列:6,8,10,

乘以3的队列:6,9,12,15

乘以5的队列:10,15,20,25

选择三个队列头里最小的数6加入丑数数组,但我们发现,有两个队列头都为6,所以我们弹出两个队列头,同时将12,18,30放入三个队列

……………………

import java.lang.Math;
public class Solution {
    public int GetUglyNumber_Solution(int index) {
        if(index<=0){
            return 0;
        }
        int p2=0,p3=0,p5=0;
        int[] result=new int[index];
        result[0]=1;
        for(int i=1;i<index;i++){
            result[i]=Math.min(result[p2]*2,Math.min(result[p3]*3,result[p5]*5));
            if(result[i]==result[p2]*2) p2++;
            if(result[i]==result[p3]*3) p3++;
            if(result[i]==result[p5]*5) p5++;
        }
        return result[index-1];
    }
}

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

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

思路:这道题可以Map方法来解决,我们首次对字符串数组中的字符进行遍历,以<元素值,次数>的形式存储在map中,存储的过程如下

遍历字符串数组中的每一个元素

判断map集合中是否存在该元素,若不存在则新添加<元素,1>存放在键值对中

若存在,则新添加<元素,map.get(array[i])+1>存放在map中

最后,我们只需循环判断map.get(array[i]) == 1就可获得第一个只出现一次的字符位置

代码

import java.util.HashMap;
import java.util.Map;
public class Solution {
   public int FirstNotRepeatingChar(String str){
       if(str.length()==0||str==null){
           return -1;
       }
       Map<Character,Integer> map = new HashMap<Character,Integer>();
        char[] array = str.toCharArray();
        for(int i=0; i < str.length(); i++ ){
            if(!map.containsKey(array[i])){
                map.put(array[i],1);
            }else {
                map.put(array[i], map.get(array[i])+1);
            }
        }
        //System.out.println(map.toString());
        for(int i = 0; i<str.length();i++){
            if(map.get(array[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

示例1

输入

1,2,3,4,5,6,7,0

输出

7

思路:这道题有两种方法,一种方法是循环匹配法,一种方法是归并排序法

  • 循环匹配法

这种方法是扫描整个数组,逐个比较和它后面的数字大小,如果后面的数字比它小,则这两个数字就组成了一个逆序对,假设数组中含有n个数字。由于每个数字都要和O(n)这个数字比较,因此这个算法的时间复杂度为O(n^2)。但是这方法不能在有限的时间内完成代码的运行,故舍弃

  • 归并排序法

参考牛客网一叶浮沉的思路,使用归并排序算法,在归并排序的过程中,后一个数组的数如小于前一个数组的数,则一定能够构成逆序对且逆序对的数目可计算,因为待归并的两个数组提前已经归并排序过,所以不会出现像前面那样少统计或者多统计的情况出现。

循环匹配法代码,不能在有限的时间内完成

public class Solution {
    public int InversePairs(int [] array) {
        if(array.length<=1){
            return 0;
        }
        int p=0;
        for(int i=0;i<array.length-1;i++){
            for(int j=i+1;j<array.length;j++){
                if(array[i]>array[j]){
                    p++;
                }
            }
        }
        if(p>0){
            return p%1000000007;
        }
        return 0;
        
    }
}

归并排序

public class Solution {
private int cnt;
    private void MergeSort(int[] array, int start, int end){
        if(start>=end)return;
        int mid = (start+end)/2;
        MergeSort(array, start, mid);
        MergeSort(array, mid+1, end);
        MergeOne(array, start, mid, end);
    }
    private void MergeOne(int[] array, int start, int mid, int end){
        int[] temp = new int[end-start+1];
        int k=0,i=start,j=mid+1;
        while(i<=mid && j<= end){
//如果前面的元素小于后面的不能构成逆序对
            if(array[i] <= array[j])
                temp[k++] = array[i++];
            else{
//如果前面的元素大于后面的,那么在前面元素之后的元素都能和后面的元素构成逆序对
                temp[k++] = array[j++];
                cnt = (cnt + (mid-i+1))%1000000007;
            }
        }
        while(i<= mid)
            temp[k++] = array[i++];
        while(j<=end)
            temp[k++] = array[j++];
        for(int l=0; l<k; l++){
            array[start+l] = temp[l];
        }
    }
    public int InversePairs(int [] array) {
        MergeSort(array, 0, array.length-1);
        return cnt;
    }
}

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

题目:输入两个链表,找出它们的第一个公共结点。(注意因为传入数据是链表,所以错误测试数据的提示是用其他方式显示的,保证传入数据是正确的)

秒杀剑指offer(31-40)_java

思路:这道题有两种思路解决

  • 补全链表的长度

补全链表的长度,使得两个链表长度相等,这样就可以使用双向指针分别遍历两个链表,直到找到两个链表的第一个公共结点。

秒杀剑指offer(31-40)_java_02


  • 循环遍历

带以两个指针p1、p2分别指向两个链表的头结点,为了保证长度一致,当一个链表遍历结束之后就从另一个链表的头结点开始遍历,这两个指针会总会在某一个时刻相遇。


秒杀剑指offer(31-40)_java_03

循环遍历代码

public class Solution {
    public ListNode FindFirstCommonNode(ListNode pHead1, ListNode pHead2) {
        if(pHead1==null||pHead2==null){
            return null;
        }
        ListNode p1=pHead1;
        ListNode p2=pHead2;
        while(p1!=p2){
            p1=p1.next;
            p2=p2.next;
            
            if(p1!=p2){
            if(p1==null)  p1=pHead2;
            if(p2==null)  p2=pHead1;
            }
        }
        return p1;
    }
}

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

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

思路:两种思路,①直接遍历查找②考虑到数组有序,可以使用二分查找

普通遍历查找

public class Solution {
    public int GetNumberOfK(int [] array , int k) {
        if(array.length==0){
            return 0;
        }
       int count=0;
        for(int i=0;i<array.length;i++){
            if(array[i]==k){
                count++;
            }
        }
        return count;
    }
}

二分查找

import java.util.Arrays;
public class Solution {
    public int GetNumberOfK(int [] array , int k) {
     int index=Arrays.binarySearch(array,k);
        if(index<0){
           return 0;
        }
       int count=1;
        for(int i=index+1;i<array.length;i++){
            if(array[i]==k){
                count++;
            }
        }
        for(int i=0;i<index;i++){
            if(array[i]==k){
                count++;
            }
        }
        return count;
    }
}

38、二叉树的深度

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

思路:这道题直接使用递归,返回左/右子树中最大的深度。

秒杀剑指offer(31-40)_java_04
  • F,左右子结点均为null,返回1
  • D,左结点+1=2
  • B,右子结点1,选取,左子结点+1=3,返回给A
  • A,右子结点为1,选取,左子结点+1=4,返回

代码

/**
public class TreeNode {
    int val = 0;
    TreeNode left = null;
    TreeNode right = null;

    public TreeNode(int val) {
        this.val = val;

    }

*/

public class Solution {
    public int TreeDepth(TreeNode root) {
        if(root==null){
            return 0;
        }
      int nleft=TreeDepth(root.left);
      int nright=TreeDepth(root.right);
        return 1+(nleft>nright?nleft:nright);
    }
}

39、平衡二叉树

题目: 输入一棵二叉树,判断该二叉树是否是平衡二叉树。在这里,我们只需要考虑其平衡性,不需要考虑其是不是排序二叉树

思路:关于树的算法题,一般都可以使用递归来实现,平衡二叉树的条件是**|左子树高度-右子树高度|<1**,我们利用这个条件可以分别计算出左右子树的高度。

代码

import java.lang.Math;
public class Solution {
    private boolean isBalanced=true;
    public boolean IsBalanced_Solution(TreeNode root) {
        //平衡二叉树的左右子树深度<=1
        getTreeDepth(root);
        return isBalanced;
    }
    public int getTreeDepth(TreeNode node){
        if(node==null){
            return 0;
        }
        int nleft=getTreeDepth(node.left);
        int nright=getTreeDepth(node.right);
        if(Math.abs(nleft-nright)>1){
            isBalanced=false;
        }
        return nright>nleft?nright+1:nleft+1;
    }
}

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

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

思路:这道题最简单的思路就是用哈希查找

  • 哈希查找

遍历数组,以数组中的值为key、出现次数为value构建Map,构建完成后,查询map获取出现次数为1为1的key值分别放入num1[0]和num2[0]

代码实现

//num1,num2分别为长度为1的数组。传出参数
//将num1[0],num2[0]设置为返回结果
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
public class Solution {
    public void FindNumsAppearOnce(int [] array,int num1[] , int num2[]) {
        Map<Integer,Integer> map=new HashMap<>();
        for(int i=0;i<array.length;i++){
            if(map.containsKey(array[i])){
                map.put(array[i],2);
            }else{
                map.put(array[i],1);
            }
            
        }
        //遍历map数组,查询
        for(int i=0;i<array.length;i++){
            if(map.get(array[i])==1){
                if(num1[0]==0){
                    num1[0]=array[i];
                }else{
                    num2[0]=array[i];
                }
            }
        }
    }
}