剑指offer思路(31-39)

31. 整数中1出现的次数

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

思路:找规律。链接:https://www.nowcoder.com/questionTerminal/bd7f978302044eee894445e244c7eee6?f=discussion
 

个位

我们知道在个位数上,1会每隔10出现一次,例如1、11、21等等,我们发现以10为一个阶梯的话,每一个完整的阶梯里面都有一个1,例如数字22,按照10为间隔来分三个阶梯,在完整阶梯0-9,10-19之中都有一个1,但是19之后有一个不完整的阶梯,我们需要去判断这个阶梯中会不会出现1,易推断知,如果最后这个露出来的部分小于1,则不可能出现1(这个归纳换做其它数字也成立)。

我们可以归纳个位上1出现的个数为:

n/10 * 1+(n%10!=0 ? 1 : 0)

十位

现在说十位数,十位数上出现1的情况应该是10-19,依然沿用分析个位数时候的阶梯理论,我们知道10-19这组数,每隔100出现一次,这次我们的阶梯是100,例如数字317,分析有阶梯0-99,100-199,200-299三段完整阶梯,每一段阶梯里面都会出现10次1(从10-19),最后分析露出来的那段不完整的阶梯。我们考虑如果露出来的数大于19,那么直接算10个1就行了,因为10-19肯定会出现;如果小于10,那么肯定不会出现十位数的1;如果在10-19之间的,我们计算结果应该是k - 10 + 1。例如我们分析300-317,17个数字,1出现的个数应该是17-10+1=8个。

那么现在可以归纳:十位上1出现的个数为:

  • 设k = n % 100,即为不完整阶梯段的数字
  • 归纳式为:(n / 100) * 10 + (if(k > 19) 10 else if(k < 10) 0 else k - 10 + 1)

百位

现在说百位1,我们知道在百位,100-199都会出现百位1,一共出现100次,阶梯间隔为1000,100-199这组数,每隔1000就会出现一次。这次假设我们的数为2139。跟上述思想一致,先算阶梯数 * 完整阶梯中1在百位出现的个数,即n/1000 * 100得到前两个阶梯中1的个数,那么再算漏出来的部分139,沿用上述思想,不完整阶梯数k199,得到100个百位1,100<=k<=199则得到k - 100 + 1个百位1。

那么继续归纳百位上出现1的个数:

  • 设k = n % 1000
  • 归纳式为:(n / 1000) * 100 + (if(k >199) 100 else if(k < 100) 0 else k - 100 + 1)

后面的依次类推....

  • k = n % (i * 10)
  • count(i) = (n / (i * 10)) * i + (if(k > i * 2 - 1) i else if(k < i) 0 else k - i + 1)

好了,这样从10到10的n次方的归纳就完成了。

  • sum1 = sum(count(i)),i = Math.pow(10, j), 0<=j<=log10(n)

但是有一个地方值得我们注意的,就是代码的简洁性来看,有多个ifelse不太好,能不能进一步简化呢? 我们可以把后半段简化成这样,我们不去计算i * 2 - 1了,我们只需保证k - i + 1在[0, i]区间内就行了,最后后半段可以写成这样

min(max((n mod (i*10))−i+1,0),i)

代码:

public int NumberOf1Between1AndN_Solution(int n) {
        int count = 0;

//        //个位:每10个出现一次(1,11,21...)
//        int count1 = n / 10 * 10 + ((n % 10) > 0 ? 1 : 0);
//        //十位:每100个出现10次(10-19)
//        int count2 = n / 100 * 10 + ((n % 100 >= 19) ? 10 : ((n % 100) < 10) ? 0 : (n % 100 - 10 + 1));
//        //百位:每1000个出现100次(100~199)
//        int count3 = n / 1000 * 100 + ((n % 1000 >= 199) ? 100 : ((n % 1000) < 100) ? 0 : (n % 1000 - 100 + 1));

        //....归纳

        int i = 1;
        for (; i <= n; i *= 10) {
            int deliver = i * 10;
            count += (n / deliver * i) + ((n % deliver >= (2 * i - 1)) ? i : (n % deliver < i) ? 0 : (n % deliver - i + 1));
        }

        return count;
    }

32. 把数组排成最小的数

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

思路:

   关键就是制定排序规则。
*  * 排序规则如下:
*  * 若ab > ba 则 a > b,
*  * 若ab < ba 则 a < b,
*  * 若ab = ba 则 a = b;
*  * 解释说明:
*  * 比如 "3" < "31"但是 "331" > "313",所以要将二者拼接起来进行比较

注意:Collections.sort()函数默认是从小到大排序,要改变排序方式就要重写compare方法,(比如想要从大到小排序,就定义compare中(if o1>o2 return -1,其中参数顺序必须是o1、o2。)

代码:

public String PrintMinNumber(int [] numbers) {
        List<Integer> list = new ArrayList<>();
        int length = numbers.length;

        if (length <= 0) {
            return "";
        }

        StringBuilder sb = new StringBuilder();

        /**拷贝数组*/
        for (int i = 0; i < length; i++) {
            list.add(numbers[i]);
        }



        Collections.sort(list, new Comparator<Integer>() {

            @Override
            public int compare(Integer str1, Integer str2) {
                String s1 = str1 + "" + str2;
                String s2 = str2 + "" + str1;
                /**降序排列,本题中可以得出最大的数*/
                //return s2.compareTo(s1);
                /**升序排列
                 * 此时s1>s2才会返回正数,所以升序*/
                return s1.compareTo(s2);
            }
        });

        for (int i:list) {
            sb.append(i);
        }

        return String.valueOf(sb);
    }

33. 丑数

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

思路:链接:https://www.nowcoder.com/questionTerminal/6aa9e04fc3794f68acf8778237ba065b?f=discussion

我们可以维护三个队列:

(1)丑数数组: 1

乘以2的队列:2

乘以3的队列:3

乘以5的队列:5

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

(2)丑数数组:1,2

乘以2的队列:4

乘以3的队列:3,6

乘以5的队列:5,10

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

(3)丑数数组:1,2,3

乘以2的队列:4,6

乘以3的队列:6,9

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

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

(4)丑数数组:1,2,3,4

乘以2的队列:6,8

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

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

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

(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放入三个队列;我们没有必要维护三个队列,只需要记录三个指针显示到达哪一步;

代码

public int GetUglyNumber_Solution(int index) {
        if (index <= 0) {
            return 0;
        }
        ArrayList<Integer> array = new ArrayList<>();
        array.add(1);

        int count = 1;

        int multi2 = 0;
        int multi3 = 0;
        int multi5 = 0;

        while (count < index) {
            int min = min(array.get(multi2)*2,array.get(multi3)*3,array.get(multi5)*5);
            array.add(min);
//            array[count]其实就是刚刚的min,这一步就是看刚刚这个数是哪个(2,3,5)得来的
            while (array.get(multi2)*2 == array.get(count)) {
                multi2++;
            }
            while (array.get(multi3)*3 == array.get(count)) {
                multi3++;
            }
            while (array.get(multi5)*5 == array.get(count)) {
                multi5++;
            }
            count++;
        }

        return array.get(index-1);
    }

    public int min(int a, int b, int c){
        int min = a < b ? a : b;
        min = min < c ? min : c;
        return min;
    }

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

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

思路:两个map,一个用来存放出现的次数,一个用来存放出现的位置。

注意

1. 要找的是字符的位置,如果只用hashMap来存的话,应该key是字符,value是数字。所以还要有一个保存位置的集合。

2. 为了避免两次遍历(一般是先遍历countMap,找出所有只出现一次的;再在firstMap中查找其出现的位置再找出最小的),可以用LinkedHashMap存储countMap,保证存取顺序一致,这样的话第一个遍历的的value=1的就是第一次出现的,在firstMap中找到对应的位置即可。

代码:

    public int FirstNotRepeatingChar(String str) {
        if (str == null || str.length() <= 0)
            return -1;

        HashMap<Character,Integer> countMap = new LinkedHashMap<>();
        HashMap<Character,Integer> firstMap = new HashMap<>();

        for (int i = 0; i < str.length() ; i++) {
            if (!firstMap.containsKey(str.charAt(i))) {
                firstMap.put(str.charAt(i),i);
                countMap.put(str.charAt(i),1);
            }
            else {
                countMap.put(str.charAt(i),countMap.get(str.charAt(i))+1);
            }
        }

        Iterator it = countMap.entrySet().iterator();
        while (it.hasNext()) {
            Map.Entry entry = (Map.Entry) it.next();
            if (1 == (int)entry.getValue()) {
                return firstMap.get(entry.getKey());
            }
        }

        return -1;
    }

35. ※※数组中的逆序对

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

思路:归并排序的变种

链接:https://www.nowcoder.com/questionTerminal/96bd6684e04a44eb80e6a68efc0ec6c5?f=discussion
 

(a) 把长度为4的数组分解成两个长度为2的子数组;

(b) 把长度为2的数组分解成两个成都为1的子数组;

(c) 把长度为1的子数组 合并、排序并统计逆序对

(d) 把长度为2的子数组合并、排序,并统计逆序对;

在上图(a)和(b)中,我们先把数组分解成两个长度为2的子数组,再把这两个子数组分别拆成两个长度为1的子数组。接下来一边合并相邻的子数组,一边统计逆序对的数目。在第一对长度为1的子数组{7}、{5}中7大于5,因此(7,5)组成一个逆序对。同样在第二对长度为1的子数组{6}、{4}中也有逆序对(6,4)。由于我们已经统计了这两对子数组内部的逆序对,因此需要把这两对子数组 排序 如上图(c)所示, 以免在以后的统计过程中再重复统计。

接下来我们统计两个长度为2的子数组子数组之间的逆序对。合并子数组并统计逆序对的过程如下图如下图所示。

我们先用两个指针分别指向两个子数组的末尾,并每次比较两个指针指向的数字。如果第一个子数组中的数字大于第二个数组中的数字,则构成逆序对,并且逆序对的数目等于第二个子数组中剩余数字的个数,如下图(a)和(c)所示。如果第一个数组的数字小于或等于第二个数组中的数字,则不构成逆序对,如图b所示。每一次比较的时候,我们都把较大的数字从后面往前复制到一个辅助数组中,确保 辅助数组(记为copy) 中的数字是递增排序的。在把较大的数字复制到辅助数组之后,把对应的指针向前移动一位,接下来进行下一轮比较。

过程:先把数组分割成子数组,先统计出子数组内部的逆序对的数目,然后再统计出两个相邻子数组之间的逆序对的数目。在统计逆序对的过程中,还需要对数组进行排序。如果对排序算法很熟悉,我们不难发现这个过程实际上就是归并排序

代码:

public int InversePairs(int [] array) {
        if (array == null || array.length <= 0) {
            return 0;
        }

        //用来存放辅助数组
        int[] copy = new int[array.length];
        int count = InversePairsHelper(array,copy,0,array.length-1);
        return count;
    }

    private int InversePairsHelper(int[] array, int[] copy, int left, int right) {
        // 递归结束
        if (left == right) return 0;
        int mid = (left + right) >>> 1;

//        先从左边只剩下最左边的两个数开始,刚开始都是两个两个
//        分治
        int leftCount = InversePairsHelper(array,copy,left,mid) % 1000000007;
        int rightCount = InversePairsHelper(array,copy,mid+1,right) % 1000000007;

        int count = 0;
        int i = mid;
        int j = right;
        int copyIndex = right;

//        从后往前
        while (i >= left && j > mid) {
            if (array[i] > array[j]) {
                count += j-mid;
                copy[copyIndex--] = array[i--];

                if (count >= 1000000007) {
                    count = count % 1000000007;
                }
            }
            else {
                copy[copyIndex--] = array[j--];
            }
        }

        //将数组中剩余元素复制到copy数组中,排好序
        for(; i >= left; i--){
            copy[copyIndex--] = array[i];
        }
        for(; j > mid; j--){
            copy[copyIndex--] = array[j];
        }
        //将排好序的数组服回给原数组,进行下一步的合并
        for(int s = left;s <= right;s++){
            array[s] = copy[s];
        }
        return (count + leftCount + rightCount) % 1000000007;

    }

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

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

思路:链表相交一定是Y字形,不可能是X字形

找出2个链表的长度,然后让长的先走两个链表的长度差,然后再一起走(因为2个链表用公共的尾部)

public ListNode FindFirstCommonNode(ListNode pHead1, ListNode pHead2) {
        if (pHead1 == null || pHead2 == null)
            return null;

        int len1 = getListLen(pHead1);
        int len2 = getListLen(pHead2);

        if (len1 > len2) {
            pHead1 = walkPre(pHead1,len1-len2);
        }else {
            pHead2 = walkPre(pHead2,len2-len1);
        }

        while (pHead1 != null) {
            if (pHead1 == pHead2) {
                return pHead1;
            }
            pHead1 = pHead1.next;
            pHead2 = pHead2.next;
        }
        return null;
    }

    private ListNode walkPre(ListNode pHead, int len) {
        while (len > 0) {
            pHead = pHead.next;
            len--;
        }

        return pHead;
    }

    private int getListLen(ListNode pHead) {
        if (pHead == null)
            return 0;

        int len = 0;

        while (pHead != null) {
            len++;
            pHead = pHead.next;
        }

        return len;
    }

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

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

思路:看见有序数组,就想二分!

1) 两次二分,一次查第一次出现的下标,一次查最后一次出现的下标

2) 由于都是整数,查找k-0.5和k+0.5在数组中应该插入的位置,两个位置相减即所求

注意

1) 二分模板注意防止死循环,方法是改变mid是取左中位数还是右中位数

2) 二分模板一定要有后处理,以防要找的数根本在数组中不存在的情况;

以及在左右0.5方法中,因为返回的是应该在数组中插入的位置,有可能会插入到最后一个的后面,但是left永远取不到arr.lenth这个值,所以要判断,如果退出循环的时候依旧有k>arr[left],就应该返回left+1

代码:

1. 

public int GetNumberOfK(int [] array , int k) {
        if(array == null || array.length <= 0) {
            return 0;
        }
        int firstIndex = getLow(array,k);
        int lastIndex = getHigh(array,k);

        return lastIndex - firstIndex + 1;
    }

    private int getLow(int[] array, int k) {
        int left = 0;
        int right = array.length-1;

        int mid = (right + left) >>> 1;

        while (left < right) {
            if (array[mid] >= k) {
                right = mid;
            }
            else {
                left = mid + 1;
            }

            mid = (right + left) >>> 1;
        }

        if (array[left] == k)
            return left;
        else
            return 0;
    }

    private int getHigh(int[] array, int k) {
        int left = 0;
        int right = array.length-1;

        int mid = (right + left + 1) >>> 1;

        while (left < right) {
            if (array[mid] <= k) {
                left = mid;
            }
            else {
                right = mid - 1;
            }

            mid = (right + left + 1) >>> 1;
        }

        if (array[left] == k)
            return left;
        else
            return -1;
    }

2. 左右0.5

public int GetNumberOfK(int [] array , int k) {
        if(array == null || array.length <= 0) {
            return 0;
        }

        return biSearch(array,k+0.5) - biSearch(array,k-0.5);
    }

    private int biSearch(int[] array, double k) {
        int left = 0;
        int right = array.length-1;
        int mid = (left + right) >>> 1;

        while (left < right) {
            if (k > array[mid]) {
                left = mid + 1;
            }
            else {
                right = mid;
            }
            mid = (left + right) >>> 1;
        }

        if (k > array[mid])
            return left+1;

        return left;
    }

38. ※二叉树的深度

题目:求二叉树的深度

思路:见二叉树一定是递归。

注意

不同于深度遍历,或者求二叉树中和为某一值的路径(剑指24),这个只用求最大长度不用具体记录路径,所以并不需要回溯,只需要每次求出最大值即可。

代码:

public int TreeDepth(TreeNode root) {
        if (root == null) {
            return 0;
        }

        int depth1 = 1;
        int depth2 = 1;

        if (root.left != null) {
            depth1 = TreeDepth(root.left)+1;
        }

        if (root.right != null) {
            depth2 = TreeDepth(root.right)+1;
        }

        return (depth1>depth2) ? depth1 : depth2;
    }

39. 平衡二叉树

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

平衡二叉树:左右子树的高度差不超过1

由下往上遍历,可以节省时间(如果从上往下,可能会多次遍历底下的节点)

代码:

1. 从上往下:

public boolean IsBalanced_Solution(TreeNode root) {
        if (root == null) return true;

        return Math.abs(maxDepth(root.left) - maxDepth(root.right)) <= 1 &&
                IsBalanced_Solution(root.left) && IsBalanced_Solution(root.right);
    }

    private int maxDepth(TreeNode root) {
        if (root == null) return 0;
        return Math.max(maxDepth(root.left),maxDepth(root.right))+1;
    }

2. 从下往上

public boolean IsBalanced_Solution(TreeNode root) {
        return absDepth(root) != -1 ;
    }

    public int absDepth(TreeNode root) {
        if (root == null) return 0;
        int left = absDepth(root.left);
//        如果不平衡,一路返回
        if (left == -1) return -1;
        int right = absDepth(root.right);
        if (right == -1) return -1;

        return (Math.abs(left - right)) > 1 ? -1 : Math.max(left,right) + 1;
    }
发布了53 篇原创文章 · 获赞 5 · 访问量 1523

猜你喜欢

转载自blog.csdn.net/zhicheshu4749/article/details/103381589