标签: 递归

LeetCode刷题【902】最大为 N 的数字组合

给定一个按 非递减顺序 排列的数字数组 digits 。你可以用任意次数 digits[i] 来写的数字。例如,如果 digits = ['1','3','5'],我们可以写数字,如 '13''551', 和 '1351315'

返回 可以生成的小于或等于给定整数 n 的正整数的个数 。

 

示例 1:

输入:digits = ["1","3","5","7"], n = 100
输出:20
解释:
可写出的 20 个数字是:
1, 3, 5, 7, 11, 13, 15, 17, 31, 33, 35, 37, 51, 53, 55, 57, 71, 73, 75, 77.

示例 2:

输入:digits = ["1","4","9"], n = 1000000000
输出:29523
解释:
我们可以写 3 个一位数字,9 个两位数字,27 个三位数字,
81 个四位数字,243 个五位数字,729 个六位数字,
2187 个七位数字,6561 个八位数字和 19683 个九位数字。
总共,可以使用D中的数字写出 29523 个整数。

示例 3:

输入:digits = ["7"], n = 8
输出:1

 

提示:

  • 1 <= digits.length <= 9
  • digits[i].length == 1
  • digits[i] 是从 '1' 到 '9' 的数
  • digits 中的所有值都 不同 
  • digits 按 非递减顺序 排列
  • 1 <= n <= 109

数学方法+递归,10月18日,今天的每日一题,有空偷个闲撸一把

看到题目第一反应是动态规划,然后扫了下题目给了示例,看到这么一段

输入:digits = ["1","4","9"], n = 1000000000
输出:29523
解释:
我们可以写 3 个一位数字,9 个两位数字,27 个三位数字,
81 个四位数字,243 个五位数字,729 个六位数字,
2187 个七位数字,6561 个八位数字和 19683 个九位数字。
总共,可以使用D中的数字写出 29523 个整数。

突然灵光一闪,可以直接用数学方法来解

拿其中的一个示例稍微改下来分解运算试试看,粗略的分解如下

输入:digits = ["1","3","5","7"], n = 1571
 1 5 7 1

 4
 16   4*4
 64   4*4*4
 45   0*4*4*4 + 1 * (2*4*4 + 1 * (3*4 + 1*( 0 + 1*(1))))

最终验证得到 4 + 16 + 64 + 45 = 129 确定正确
如果你能看懂上面最后一行 4位数的组合数量计算公式的话,那么说明你就已经GET到了

数字1571一共有4位,那么我们将结果分为两部分,不满足4位的和满足4位的结果分开计算

不满足4位的部分

  1. 如果只列举一位,那么就有String[] digits种方案,每一个数字都可以列举一次
  2. 如果要列举两位数字的情况,那么每一位都可以列举String[] digits种方案,在本示例中即为4 * 4
  3. 如果要列举三位数字的话,那么同理有4 * 4 * 4中,因为位数小于目标数字n = 1571的4位,所以可以任意组合,不要考虑结果是否会大于1571

接下来第二部分,需要组合4位数字的情况

  1. 我们先取出目标数字1571的第一位数字1,拿到digits数组中比较,发现小于1的没有,但是有等于1的情况
  2. 如果digits中存在小于当前位数字的lessCnt个,那么后面的排列组合可以不考虑。即有lessCnt * digits.length * ...... * digits.length种组合情况
  3. 如果digits中没有当前位的数字,在处理了上一位小于的情况之后,说明后面无论怎么样排序都不会有满足结果小于目标值的情况
  4. 如果存在有等于当前位的数字,那么,我们需要再往后看一位,此时回到了和步骤1相同的处理逻辑部分
  5. 那么第二位是5,digits数组中有[1,3]是小于5的,即可以有两种情况,后面无论如何排列都是满足的,那么得到2 * 4 * 4
  6. digits数组中也包含了5,我们继续看下一位数字7
  7. digits数组中有[1,3,5]小于7,于是有新的可能组合数量 3 * 4,且数组中有数字7,那么继续看下一位数字
  8. 最后一位数字1,同样的,数组中小于1的数字不存在,那么即有0种情况,后面没有4要乘了
  9. 同时数组中包含了1,所以继续看下一位
  10. 最后已经没有下一位了,这里需要返回个1,根据前一步的结果返回0的话显然会造成结果不正确

代码

class Solution {
    public int atMostNGivenDigitSet(String[] digits, int n) {
        int size = getSize(n);
        int ans = 0;
        int t = digits.length;
        //低位数的直接相加
        while (--size>0){
            ans += t;
            t *= digits.length;
        }
        int[] intDigtis = getIntDigtis(digits);
        int[] nArr = getNArr(n);
        //最高位数的分区段统计数量
        ans += splitCnt(intDigtis,nArr, 0);
        return ans;
    }

    int splitCnt(int[] digits, int[] arr, int idx){
        if (idx == arr.length){
            return 1;
        }
        //比arr[idx]小的数字有个lessCnt个,相等的有equalCnt个
        int lessCnt = 0;
        int equalCnt = 0;
        for (int i = 0; i < digits.length; i++) {
            if (digits[i] > arr[idx]){
                break;
            }
            if (digits[i] < arr[idx]){
                lessCnt++;
            }
            if (digits[i] == arr[idx]){
                equalCnt++;
            }
        }
        for (int i = idx+1; i < arr.length; i++) {
            lessCnt *= digits.length;
        }
        return lessCnt + (equalCnt * splitCnt(digits,arr,idx+1));
    }

    final static int[] mod = {
            0,
            1,
            10,
            100,
            1000,
            10000,
            100000,
            1000000,
            10000000,
            100000000,
            1000000000
    };

    final static int [] sizeTable = {
            9,
            99,
            999,
            9999,
            99999,
            999999,
            9999999,
            99999999,
            999999999,
            Integer.MAX_VALUE
    };

    static int getSize(int x) {
        for (int i=0; ; i++)
            if (x <= sizeTable[i])
                return i+1;
    }

    int[] getIntDigtis(String[] digtis) {
        int[] intDigtis = new int[digtis.length];
        for (int i = 0; i < digtis.length; i++) {
            intDigtis[i] = digtis[i].charAt(0)-'0';
        }
        return intDigtis;
    }

    int[] getNArr(int n){
        int size = getSize(n);
        int[] nArr = new int[size];
        int i = size;
        while (n > 0){
            nArr[size-i] = n/mod[i];
            n %= mod[i];
            i--;
        }
        return nArr;
    }
}

思路想明白了的话,还是不算太复杂的,主要是全位数部分计算的方法要递归起来实现的话,略微有点点显得绕

LeetCode刷题【60】排列序列

给出集合 [1,2,3,...,n],其所有元素共有 n! 种排列。

按大小顺序列出所有排列情况,并一一标记,当 n = 3 时, 所有排列如下:

  1. "123"
  2. "132"
  3. "213"
  4. "231"
  5. "312"
  6. "321"

给定 n 和 k,返回第 k 个排列。

 

示例 1:

输入:n = 3, k = 3
输出:"213"

示例 2:

输入:n = 4, k = 9
输出:"2314"

示例 3:

输入:n = 3, k = 1
输出:"123"

 

提示:

  • 1 <= n <= 9
  • 1 <= k <= n!
Related Topics
  • 递归
  • 数学

  • 👍 682
  • 👎 0
  • 逆康托展开 与 康托展开【Cantor Expansion】
    关于本题原型可以参见下康托展开【Cantor Expansion】 ,就是原题了,具体内容不做更多赘述

    思路

    1. 已知现有数字n,那么按字典序排序组合后我们知道。初步的根据第1位的字符可以分成n段,每段长度为(n-1)!
    2. 继而我们这当中的某一段中,我又可以如上分成(n-1)段,因为前面已经用了一位数字了,而这(n-1)段的每段长度为(n-2)!
    3. 那么按照题意,我们可以按照从大区间到小区间逐步缩小范围的方法来处理
    4. 首先除以(n-1)!,得到第一位的数字,记下余数
    5. 用上一步的余数除以(n-2)!,知道了第二位数字,继续记下余数用来处理第三位数字
    6. 每次除以(n-?)!得到的结果是确定的未使用的数字的顺序,即在确定这个数字的时候需要跳过前面已经使用过的数字,见方法findNthNum(int nth)
    7. 最终还剩下一个数字,就是最后一位了

    容易掉的坑,k--,上来记得要减下,被这步坑了好久,减一之后直接除了便于取整取余划分区间

    代码

    class Solution {
    
        boolean[] usedNums;
    
        public String getPermutation(int n, int k) {
            k--;
            usedNums = new boolean[n+1];
            int[] factorial = new int[n];
            factorial[n-1] = 1;
            for (int i = n-2; i >= 0; i--) {
                factorial[i] = factorial[i+1] * (n-i);
            }
            char[] ans = new char[n];
            int idx = 0;
            while (++idx < n){
                int i = k / factorial[idx];
                k %= factorial[idx];
                int num = findNthNum(i+1);
                ans[idx-1] = (char)('0'+ num);
            }
            ans[idx-1] = (char)('0'+ findNthNum(1));
            return new String(ans);
        }
    
        int findNthNum(int nth){
            for (int i = 1; i < usedNums.length; i++) {
                if(!usedNums[i]){
                    nth--;
                    if (nth==0){
                        usedNums[i] = true;
                        return i;
                    }
                }
            }
            return -1;
        }
    }

    以上其实是逆康托展开的过程,根据第k的数值,确定这个数值是什么

    根据定义,与这一过程相对的是康托展开的过程,根据数值确定这个数值的排序

    那么相应思路为

    1. 从头开始确定当前数字在未使用数字中的排序i(从0下标开始或者你认为从正数1开始排序i'那么前面有i'-1组),那么前面有i * (n-1)!
    2. 第二位同样假设第二位数字在未使用数字中的排序i_1(下标),第二位则对应为i_1 * (n-2)!
    3. 后面依次叠加
    4. 最终得到的结果即为对应的排序

    LeetCode刷题【剑指 Offer 43】1~n 整数中 1 出现的次数

    输入一个整数 n ,求1~n这n个整数的十进制表示中1出现的次数。

    例如,输入12,1~12这些整数中包含1 的数字有1、10、11和12,1一共出现了5次。

     

    示例 1:

    输入:n = 12
    输出:5
    

    示例 2:

    输入:n = 13
    输出:6

     

    限制:

    • 1 <= n < 2^31

    注意:本题与主站 233 题相同:https://leetcode-cn.com/problems/number-of-digit-one/

    Related Topics
    • 递归
    • 数学
    • 动态规划

  • 👍 343
  • 👎 0
  • 按位计算统计,(【左侧数值】当前位1【右侧数值】)
    直接拿一个数字31261分析下,过程如下

        3      1      2      6      1
                                    3126 + 1
                             3130
                      3200
                3000 + 261 + 1
        10000
    1. 最低位当前数字是1,这个位置上1粗线的次数由他前面的数字决定,所以我们可以非常明白的知道,这个时候这一位一共出现了3127次字符1
      • 分别为有前缀的3126 次
      • 和无前缀的 当数字n = 1的时候的1次
    2. 倒数第二位数字6,这个数字大于1,根据上面算最低位的时候的情况分析依据,我们可以知道这个位置一共出现了3130次,这个结果可以也可以分为两部分
      • 有前缀的时候3120种情况,数字xxx1x的情况,当前位前面有312种情况,而又因为当前位大于1,后面一位的0-9的10种情况,即为3120种
      • 无前缀的时候1019的10种情况
      • 这样,我们暂时把这部分换为另外一种算法,即为( 312 + 1 ) * 10 种。
    3. 再往前一位数字2,同前面的数字6的情况,因为大于1所以结果为(31 + 1) * 100
    4. 再次往前一位,数字1,虽然前面为1的情况我们已经分析过了,但是那个是不完整的,在这里,我们将完整分析下为1的时候的情况
      • 因为后面还有其他数字,整个31xxx区段是还没完全结束的,所以我们不能直接得到结果为4000
      • 那么前面部分的情况为012的3000种情况,也可以认为如果左侧数字是k,那么现在就有k000
      • 又因为之前说了这个31xxx区段是还没完全结束,所以应当还有当前位后面部分的261种情况,外加如果后面都为0的情况
      • 那么这个位置数字1出现的次数就是3000 + 261 + 1 = 3262
    5. 最高位为3,大于1,可以直白的知道有10000次出现了字符1,那么按照前面的规律,我们假定最高位为0,按照之前的方法可以得到( 0 + 1 ) * 10000
    6. 举例数据中没有出现的当前位为0的情况,这个就比较简单了,直接就是左侧数据出现的次数了
      • 如 30261 中0的位置,
      • 就是数字1xxx,11xxx,21xxx的3000种情况

    计算中间的问题

    我们在计算中使用了一个辅助变量10,100,100010000,这个数值是比入参数字大一位的

    又因为题目给的入参条件1 <= n < 2^31

    所以中间计算过程我就偷了个懒给转成了long型数据

    另外,葱低往高取每一位的数字的时候可以直接用余10,之后再除10的方法,我这边一开始是准备转成数组直接根据数组计算左右侧数据影响的当前位置1出现次数的,最后就还是没改了

    代码

    class Solution {
        /*
        3      1      2      6      1
                                    3127
                             3130
                      3200
                3000 + 261 + 1
         10000
         */
    
    
        final static int [] sizeTable = { 9, 99, 999, 9999, 99999, 999999, 9999999,
                99999999, 999999999, Integer.MAX_VALUE };
    
        public int countDigitOne(int n) {
            long ans = 0;
            long num = (long) n;
            int size = numSize(n);
            int[] arr = new int[size];
            int idx = size-1;
            while (n != 0){
                arr[idx] = n%10;
                n /= 10;
                idx--;
            }
            idx = size-1;
            long tmp = 10;
            while (idx >= 0){
                long r = 0;
                if (arr[idx] == 0){
                    r = ( num / tmp ) * ( tmp / 10 );
                }
                if (arr[idx] == 1){
                    r = ( num / tmp ) * ( tmp / 10 ) + ( num % (tmp / 10) ) + 1;
                }
                if (arr[idx] > 1){
                    r = ( (num / tmp)+1 ) * ( tmp / 10 );
                }
                ans += r;
                tmp *= 10;
                idx--;
            }
    
            return (int)ans;
        }
    
    
    
        static int numSize(int x) {
            for (int i=0; ; i++)
                if (x <= sizeTable[i])
                    return i+1;
        }
    }

    LeetCode刷题【剑指 Offer 33】二叉搜索树的后序遍历序列

    输入一个整数数组,判断该数组是不是某二叉搜索树的后序遍历结果。如果是则返回 true,否则返回 false。假设输入的数组的任意两个数字都互不相同。

     

    参考以下这颗二叉搜索树:

         5
        / \
       2   6
      / \
     1   3

    示例 1:

    输入: [1,6,3,2,5]
    输出: false

    示例 2:

    输入: [1,3,2,6,5]
    输出: true

     

    提示:

    1. 数组长度 <= 1000
    Related Topics
    • 二叉搜索树
    • 递归
    • 二叉树
    • 单调栈

  • 👍 541
  • 👎 0
  • 分段验证

    解题思路

    分段验证

    代码

    class Solution {
        public boolean verifyPostorder(int[] postorder) {
            return validate(postorder,0,postorder.length-1);
        }
    
    
        private boolean validate(int[] postorder, int left ,int right){
            //如果左指针大于等于右指针了,说明当前节点以及没有子节点了,自然是符合条件的】
            if (left>=right || left < 0 || right < 0) return true;
            //找到这段数组对应的根节点,根据后序遍历的特性,即为这段数组的最后一位
            int rootNum = postorder[right];
            //初始赋值
            int leftEnd = -1;
            int rightStart = -1;
            //开始遍历
            for (int i = left; i < right; i++) {
                if (postorder[i] < rootNum){
                    //如果这个值小于根节点的值,说明这个节点应该是在左子树中
                    leftEnd = i;
                }
                if (postorder[i] > rootNum && rightStart == -1){
                    //如果这个值大于根节点的值,说明这个节点应该是右子树上的
                    //且rightStart == -1 表示是第一次碰到的
                    rightStart = i;
                }
            }
            //此时如果符合条件的话,应该是 leftEnd 在 rightStart 的左边一位
            //或者  没有左子树:leftEnd == -1 且rightStart == left
            //或者  没有右子树:rightStart == -1 且leftEnd == right-1
            boolean validateResult = (leftEnd>-1 &&  rightStart> -1 && leftEnd+1== rightStart)
                    || ( leftEnd == -1 && rightStart == left )
                    || ( rightStart == -1 && leftEnd == right-1);
            //自身验证完了,还要对分割好了的子序列的有效性判断
            if (validateResult){
                return validate( postorder, left, leftEnd ) && validate( postorder, rightStart, right-1 );
            }
            return false;
        }
    }

    LeetCode刷题【21】合并两个有序链表

    将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。 

     

    示例 1:

    输入:l1 = [1,2,4], l2 = [1,3,4]
    输出:[1,1,2,3,4,4]
    

    示例 2:

    输入:l1 = [], l2 = []
    输出:[]
    

    示例 3:

    输入:l1 = [], l2 = [0]
    输出:[0]
    

     

    提示:

    • 两个链表的节点数目范围是 [0, 50]
    • -100 <= Node.val <= 100
    • l1l2 均按 非递减顺序 排列
    Related Topics
  • 递归
  • 链表

  • 👍 2490
  • 👎 0
  • 双指针,串珠子

    剑指Offer25
    这个问题之前给人家形象的比喻讲解过。

    1. 你拿两串珠子,分别按照题意标上有序递增的数字
    2. 另外你再拿一根线,把这两串珠子合并成一串,这时候你会怎么做呢?
      当然是两个原串头上挑小的串起来呀!哪个小串哪个,另外一串没有了就整个串上就完了
    class Solution {
        public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
            ListNode head = new ListNode();
            ListNode cur = head;
            while ( l1 != null || l2 != null ){
                if (l1 == null || l2 == null){
                    cur.next = l1==null?l2:l1;
                    break;
                }
                if (l1.val < l2.val){
                    cur.next = l1;
                    l1 = l1.next;
                }else{
                    cur.next = l2;
                    l2 = l2.next;
                }
                cur = cur.next;
            }
            return head.next;
        }
    }
    

    LeetCode刷题【剑指 Offer 24】反转链表

    定义一个函数,输入一个链表的头节点,反转该链表并输出反转后链表的头节点。

     

    示例:

    输入: 1->2->3->4->5->NULL
    输出: 5->4->3->2->1->NULL

     

    限制:

    0 <= 节点个数 <= 5000

     

    注意:本题与主站 206 题相同:https://leetcode-cn.com/problems/reverse-linked-list/

    Related Topics
  • 递归
  • 链表

  • 👍 448
  • 👎 0
  • 遍历修改指针

    修改指针

    操作简单

    1. 把当前节点的next指向之前遍历的节点pre,这是核心
    2. 因为进行了上一步操作之后下一个节点就找不到了,所以需要一个变量暂存一下修改之前的next指向
    3. 同时pre指向跟随遍历窗口移动到当前节点
    4. 同时需要把当前移动到原链表的下一个节点,也就是步骤(2)中记下的那个
    5. 到结束时遍历到原链表的结尾null节点,此时pre指向为原链表的结尾非空那个节点,也就是翻转后的头结点

    代码

    class Solution {
        public ListNode reverseList(ListNode head) {
            if (null == head || head.next == null){
                return head;
            }
            ListNode pre = null;
            ListNode cur = head;
            while (cur!=null){
                ListNode next = cur.next;
                cur.next = pre;
                pre = cur;
                cur = next;
            }
            return pre;
        }
    }

    LeetCode刷题【剑指 Offer 25】合并两个排序的链表

    输入两个递增排序的链表,合并这两个链表并使新链表中的节点仍然是递增排序的。

    示例1:

    输入:1->2->4, 1->3->4
    输出:1->1->2->3->4->4

    限制:

    0 <= 链表长度 <= 1000

    注意:本题与主站 21 题相同:https://leetcode-cn.com/problems/merge-two-sorted-lists/

    Related Topics
  • 递归
  • 链表

  • 👍 254
  • 👎 0
  • 双指针合并,不如找两串珠子自己比划比划

    这个问题之前给人家形象的比喻讲解过。

    1. 你拿两串珠子,分别按照题意标上有序递增的数字
    2. 另外你再拿一根线,把这两串珠子合并成一串,这时候你会怎么做呢?
    3. 当然是两个原串头上挑小的串起来呀!哪个小串哪个,另外一串没有了就整个串上就完了

    代码

    class Solution {
        public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
            ListNode head = new ListNode();
            ListNode cur = head;
            while ( l1 != null || l2 != null ){
                if (l1 == null || l2 == null){
                    cur.next = l1==null?l2:l1;
                    break;
                }
                if (l1.val < l2.val){
                    cur.next = l1;
                    l1 = l1.next;
                }else{
                    cur.next = l2;
                    l2 = l2.next;
                }
                cur = cur.next;
            }
            return head.next;
        }
    }

    如果本题能够很好理解了不妨再去试试23. 合并K个升序链表
    题解JAVA 分治归并 其实挺简单的