剑指Offer/31-40


31. 整数中1出现的次数

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

解题思路:
方法1:递归
f(n)为1-n范围内,这n个数中1出现的次数
首先将n分为两种情况:最高位为1(如1234)和最高位不为1(如3234)
情况一:当最高位为1时(以1234为例):
①:将n分为两部分,最高位单独拿出来为1,定义为high;剩下的部分为234,定义为last;
②:获取到最高位的分位,为千分位,即定义pow=1000;
③将数据n定义两个范围:
第一个范围:1-999范围内,1的个数为f(pow-1);
第二个范围:1000-1234范围内:
1)首先只考虑千分位是1的个数:也就是1000、1001、1002…1234,即234+1,转换下为: last+1
2)其次考虑其他位数上的1的个数为:f(last)。
综上所述:情况一中1出现的次数为: f(pow-1)+last+1+f(last)
情况二:当最高为不为1时(以3234为例):
①:1-999范围内,1的个数依然为:f(pow-1);
②:1000-1999范围内:
1)只考虑千分位是1的个数为:1000、1001、1002…1999即1000个也就是pow;
2)其他位数上1的数量为:f(pow-1)
③:2000-2999范围内1的个数:f(pow-1),注意这里的千分位是2,所以不要考虑分两种情况
④:3000-3234范围内1的个数:f(last)
综上所述:情况二中1出现的次数为:pow + high*f(pow-1) + f(last);

方法2: 利用StringBuilder,将每个数字追加到字符串中,进而统计1出现的次数。

public class Solution {
    public int NumberOf1Between1AndN_Solution(int n){
        //方法1:递归  划分成不同的段
        if(n <= 0){
            return 0;
        }
        String s = String.valueOf(n);
        int high = s.charAt(0) - '0';
        int pow = (int)Math.pow(10, s.length()-1);
        int last = n - high*pow;
        if(high == 1){
            return NumberOf1Between1AndN_Solution(pow-1) + last + 1 
                + NumberOf1Between1AndN_Solution(last);
        }else{
            return high * NumberOf1Between1AndN_Solution(pow-1) + pow 
                + NumberOf1Between1AndN_Solution(last);
        }
//         //方法2:将数字转换成字符串
//         StringBuilder sb = new StringBuilder();
//         for(int i = 1; i < n+1; i++){
//             sb.append(i);
//         }
//         int count = 0;
//         for(int i = 0; i < sb.length(); i++){
//             if(sb.charAt(i) == '1')
//                 count++;
//         }
//         return count;
    }
}

32. 把数组排成最小的数

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

解题思路:
比较两个字符串s1, s2大小的时候,先将它们拼接起来,比较s1+s2 和s2+s1哪个大,如果s1+s2大,那说明s2应该放前面,所以按这个规则,s2就应该排在s1前面。

import java.util.ArrayList;
public class Solution {
    public String PrintMinNumber(int [] numbers) {
        if(numbers == null || numbers.length == 0)
            return "";
        //数字字符串a+b>b+a说明b是小于a的,b应该位于a前
        for(int i = 0; i < numbers.length; i++){
            for(int j = i+1; j < numbers.length; j++){
                String str1 = numbers[i] + String.valueOf(numbers[j]);
                String str2 = numbers[j] + String.valueOf(numbers[i]);
                if(str1.compareTo(str2) > 0){
                    int temp = numbers[j];
                    numbers[j] = numbers[i];
                    numbers[i] = temp;
                }
            }
        }
        String str = new String("");
        for(int i = 0; i < numbers.length; i++){
            str = str + numbers[i];
        }
        return str;
    }
}

33. 丑数

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

解题思路:
丑数=2^x × 3^y × 5^z ,所以只需要把得到的丑数不断地乘以2、3、5之后并放入他们应该放置的位置即可,而此题的难点就在于如何有序的放在合适的位置。核心思想也是丑数乘以丑数得到丑数,只不过在找寻下一个丑数的时候并不是暴力的,而是确定了下一个丑数出现的范围,然后再在这个范围中取最小的即可。

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;
        //一个丑数乘以2或者3或者5还是丑数
        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++;
            }else if(result[i] == result[p3]*3){
                p3++;
            }else if(result[i] == result[p5]*5){
                p5++;
            }
            //下面语句容易忽略
            if(result[i] == result[i-1])
                i--;
        }
        return result[index-1];
    }
}

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

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

解题思路:
方法1:类Hash方法,采用哈希思想。
遍历一遍字符串,统计每个字符出现的次数。然后再遍历一遍字符串,找出答案
方法2:: 利用HashMap
先创建一个HashMap,然后遍历整个字符串一遍,记录下每个字符出现的次数;
再次遍历整个字符串,根据我们前面存储的hashmap找哪个字符只出现过一次,直接返回他的位置

import java.util.HashMap;
public class Solution {
    public int FirstNotRepeatingChar(String str) {
        if(str == null || str.length() == 0)
            return -1;
//         //方法1
//         //用一个类似哈希的东西来存储字符出现的次数
//         int[] count = new int[256];
//         for(int i=0; i<str.length(); i++){
//             count[str.charAt(i)]++;
//         }
//         for(int i = 0; i < str.length(); i++){
//             if(count[str.charAt(i)] == 1){
//                 return i;
//             }
//         }
//         return -1;
        //方法2:利用HashMap
        HashMap<Character, Integer> map = new HashMap<>();
        for(int i = 0; i < str.length(); i++){
            if(map.get(str.charAt(i)) == null){
                map.put(str.charAt(i), 1);
            }else{
                map.put(str.charAt(i), map.get(str.charAt(i))+1);
            }
        }
        for(int i = 0; i < str.length(); i++){
            if(map.get(str.charAt(i)) == 1){
                return i;
            }
        }
        return -1;
    }
}

35. 数组中的逆序对

题目描述
在数组中的两个数字,如果前面一个数字大于后面的数字,则这两个数字组成一个逆序对。输入一个数组,求出这个数组中的逆序对的总数P。并将P对1000000007取模的结果输出。 即输出P%1000000007

示例1
输入
[1,2,3,4,5,6,7,0]
返回值
7

解题思路:利用归并排序的思想
分治的思想,将数组不断一分为二,直到数组中只有两个元素,统计逆序对个数。然后对相邻的两个子数组进行合并,由于已经统计了这两对子数组内部的逆序对,因此需要把这两对子数组进行排序,避免在之后的统计过程中重复统计。在合并的时候也要计算组间的逆序对个数。
逆序对的总数 = 左边数组中的逆序对的数量 + 右边数组中逆序对的数量 + 左右结合成新的顺序数组时中出现的逆序对的数量
整个过程是一个归并排序的算法。

在这里插入图片描述

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);
    }
    //二路归并 合并
    public void MergeOne(int[] array, int start, int mid, int end){
        int i = start;
        int j = mid + 1;
        int k = 0;
        int[] temp = new int[end-start+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++];
        }
        //把排好序的temp移动到array
        for(int t = 0; t < k; t++){
            array[start+t] = temp[t];
        }
    }
    public int InversePairs(int [] array) {
        int[] temp = new int[array.length];
        MergeSort(array, 0, array.length-1);
        return cnt;
    }
}

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

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

解题思路:双指针法
创建两个指针p1和p2,分别指向两个链表的头结点,然后依次往后遍历。如果某个指针到达末尾,则将该指针指向另一个链表的头结点;如果两个指针所指的节点相同,则循环结束,返回当前指针指向的节点。比如两个链表分别为:1->3->4->5->6和2->7->8->9->5->6。短链表的指针p1会先到达尾部,然后重新指向长链表头部,当长链表的指针p2到达尾部时,重新指向短链表头部,此时p1在长链表中已经多走了k步(k为两个链表的长度差值),p1和p2位于同一起跑线,往后遍历找到相同节点即可。其实该方法主要就是用链表循环的方式替代了长链表指针先走k步这一步骤。

/*
public class ListNode {
    int val;
    ListNode next = null;

    ListNode(int val) {
        this.val = val;
    }
}*/
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 == null ? pHead2 : p1.next;
            p2 = p2 == null ? pHead1 : p2.next;
        }
        return p1;
    }
}

37. 数组在排序数组中出现的次数

题目描述
统计一个数字在升序数组中出现的次数。

解题思路:二分查找
一种投机取巧的思想:因为 array 中都是整数,利用二分法搜索 k-0.5 和 k+0.5 这两个数应该插入的位置,然后相减即可。注意是start <= end

public class Solution {
    public int GetNumberOfK(int [] array , int k) {
        if(array.length == 0 || array == null){
            return 0;
        }
        int first = binarySearch(array, k-0.5);
        int last = binarySearch(array, k+0.5);
        return last - first;
    }
    private int binarySearch(int[] array, double k){
        int low = 0;
        int high = array.length - 1;
        while(low <= high){
            int mid = (low + high)/2;
            if(array[mid] < k){
                low = mid + 1;
            }else if (array[mid] > k){
                high = mid - 1;
            }else{
                return low;
            }
        }
        return low;//return low或者high都可以
    }
}

38. 二叉树的深度

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

解题思路:
方法1:递归
传入某节点,调用该方法,返回的应该是以传入节点为根节点的树的深度,而树的深度,肯定和左右子树深度有关,所以进入这个方法后,就包含了左右子树的深度(而要得到左右子树的深度,肯定又是以左右子节点为根节点,再次调用该方法深度获取的,因此此时进行递归),并且还有由一个左右深度比较的过程,然后取较大值,这个较大值就是该节点左右子树深度较深的值,以该值+1作为返回值,就是该节点的深度。

方法2:层次遍历
求最大深度,可用队列。因为要满足先进先出的特性。
借助队列,对二叉树进行层次遍历;
在层次遍历的过程中,每次当队列中某一层的节点出队完成后,高度+1;
关键点:判别队列中某一层节点出队完成的标准是什么?
在出队之前,此时队列中记录的只有某一层节点,所以队列的大小就是某一层节点的个数。当此个数减到0的时候,则说明该层节点全部出队完成

/**
public class TreeNode {
    int val = 0;
    TreeNode left = null;
    TreeNode right = null;
    public TreeNode(int val) {
        this.val = val;
    }
}
*/
import java.util.*;
public class Solution {
    public int TreeDepth(TreeNode root) {
//         //递归
//         if(root == null){
//             return 0;
//         }
//         int left = TreeDepth(root.left);
//         int right = TreeDepth(root.right);
//         return Math.max(left, right)+1;
        //非递归——层次遍历
        if(root == null){
            return 0;
        }
        Queue<TreeNode> queue = new LinkedList();
        queue.add(root);
        int high = 0;
        int size;//用size控制high增长的次数和时机(同一层的元素没有完全退出队列的时候high不可以增加)
        TreeNode node;
        while(queue.size() != 0){
            size = queue.size();//队列长度
            while(size != 0){
                node = queue.poll();//从队列头部删除一个元素
                if(node.left != null)
                    queue.add(node.left);
                if(node.right != null)
                    queue.add(node.right);
                size--;//当size==0时说明同一层的元素遍历完成
            }
            high++;
        }
        return high;
    }
}

39. 平衡二叉树

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

在这里,我们只需要考虑其平衡性,不需要考虑其是不是排序二叉树。

解题思路:
平衡二叉树定义:它是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。
根据定义,我们只需要后序遍历此树,从树的叶子节点开始计算高度,只要有一个子树不满足定义就返回-1,如果满足继续向上计算高度。

public class Solution {
    public boolean IsBalanced_Solution(TreeNode root) {
        return depth(root) != -1;
    }
    public int depth(TreeNode root){
        if(root == null)
            return 0;
        int ldept = depth(root.left);
        if(ldept == -1)
            return -1;
        int rdept = depth(root.right);
        if(rdept == -1)
            return -1;
        int sub = Math.abs(ldept - rdept);
        if(sub > 1)
            return -1;
        return Math.max(ldept, rdept)+1;
    }
}

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

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

解题思路:哈希法
遍历一遍数组,用map记录出现的次数,然后再遍历一遍数组,找出出现1次的数字。

//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[]) {
        HashMap<Integer, Integer> map = new HashMap<>();
        for(int i = 0; i < array.length; i++){
            if(map.get(array[i]) == null){
                map.put(array[i], 1);
            }else{
                map.put(array[i], map.get(array[i])+1);
            }
        }
        int count = 1;
        Iterator<Map.Entry<Integer, Integer>> iterator = map.entrySet().iterator();
        while(iterator.hasNext()){
            Map.Entry<Integer, Integer> entry = iterator.next();
            if(entry.getValue() == 1){
                if(count == 1){
                    num1[0] = entry.getKey();
                    count++;
                }else{
                    num2[0] = entry.getKey();
                    break;
                }
            }
        }
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值