算法挑战记录——Java

算法挑战记录——Java

  • 最近感觉需要提高一下自己的算法水平,因此开始进行算法挑战训练专题,希望自己能够坚持下来。开始于2020-11-08.

  • 最近同时也在学习python,打算同步做一份Python实现的算法,flag先立在这里,之后会把链接附在上面。于2020-11-10.

  • 执行效率还蛮高,可能是现在只做到6个,当天就搞定了Python的版本,这里是Python实现的链接:
    算法挑战记录——Python

  • 本文篇幅已经超过600行,不便继续维护,因此接下来的内容在其他文章维护。于2020-11-18
    第二篇地址 算法挑战记录Ⅱ——Java


1.旋转字符串挑战

描述

  • 给定一个字符串(以字符数组的形式给出)和一个偏移量,根据偏移量原地旋转字符串(从左向右旋转)。

    在数组上原地旋转,使用O(1)的额外空间

思路

  • 原地旋转并使用O(1)的额外空间,意味着只能在字符数组上进行操作,通过交换索引位置来进行交换。
  • 交换次数为数组长度-1,代码如下:
public class Solution {
    /**
     * @param str: An array of char
     * @param offset: An integer
     * @return: nothing
     */
    public void rotateString(char[] str, int offset) {
        // write your code here
        if (str.length < 2 || offset == 0 ) return;//字符数组长度小于2时无需交换
        int len = str.length;
        offset %= len;//超过长度取余数
        if (offset == 0) return;//交换位置为0时无需操作
        char temp; //临时字符位置
        int pos =0;//需要进行替换的元素位置
        int check = 0;//如果字符数组长度不是素数,会出现循环
        for ( int i =1;i<str.length ;i++ ){//只需要进行length-1次
            int newPos = (pos+offset)%len;
            temp = str[newPos];
            str[newPos] = str[check];
            str[check] = temp;
            pos+= offset;
            if (pos%len == check){//check最多为offset-1,循环就可结束
                check += 1;
                pos =check;
            }
            
        } 
    }
}

2.尾随零

描述

  • 给定一个整数n,返回n!(n的阶乘)的尾随零的个数。

    您的解法时间复杂度应为对数级别。

思路

  • 尾随0以为着计算结果有多少个10,10 =2×5。也就是说寻找阶乘中因数2和5的个数。由于阶乘中含有因数2的数量将远多于5的个数,因此只需要统计因数为5的个数。
  • 如果针对5的数量直接进行统计,时间复杂度将不是对数级别。因此如何统计5的个数就是解决问题的关键。
  • 我们不妨针对所给的n进行考虑,思考5 25 125 情况下包含5的个数规律,不难发现,每隔5倍,因子中包含5的数量都为n/5个。
  • 综合上述分析,循环次数仅需次数为log5n,代码如下。
public class Solution {
    /**
     * @param n: a integer
     * @return: return a integer
     */
    public int trailingZeroes(int n) {
        // write your code here
        int num5 = 0;
        for (int i =n;i>=5;i/=5){
            num5 += i/5;
        }
        return num5;
    }
}

3.落单的数Ⅰ

描述

  • 给出 2 * n + 1个数字,除其中一个数字之外其他每个数字均出现两次,找到这个数字。(n≤100)

    挑战: 一次遍历,常数级的额外空间复杂度

思路

  • 最为直观的思路是利用set的特性来进行去重操作,但无法做到常数级别的空间复杂度,代码如下。
public class Solution {
    /**
     * @param A: An integer array
     * @return: An integer
     */
    public int singleNumber(int[] A) {
        // write your code here
        Set hs = new HashSet();
        for (int i = 0;i<A.length ;i++ ){
            if (hs.contains(A[i])){
                hs.remove(A[i]);
            }else{
                hs.add(A[i]);
            }
        }
        return (int)hs.iterator().next();
    }
}
  • 利用位操作异或的特性,即a ^ a ^ b = b,可以实现空间复杂度为常数的目标。
public class Solution {
    /**
     * @param A: An integer array
     * @return: An integer
     */
    public int singleNumber(int[] A) {
        // write your code here
        int res = A[0];
        for (int i = 1;i<A.length ;i++ ){
            res ^= A[i];
        }
        return res;
    }
}

4.统计数字

描述

  • 计算数字 k 在 0 到 n 中的出现的次数,k 可能是 0~9 的一个值。

样例

输入:
k = 1, n = 12
输出:
5
解释:
在 [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12] 中,我们发现 1 出现了 5 次 (1, 10, 11, 12)(注意11中有两个1)。

思路

在不进行分析的情况下,使用二维for循环暴力计算显然会耗费大量的时间。

因此我们需要对数字出现次数分布进行分析,寻找更好的方法。
  • 寻找规律

    • 我们很容易想到数字本身就是从0-9循环累加,每一位都是从0到9之后进位。

    • 不妨对数字首位进行补零。补零后全部个位数、十位数、百位数…的总位数和分别为10 2×102 3×103…。

    • 设位数为n,n位数以内的全部数字出现的次数为n×10n

    • 因此每个数字出现的次数为n×10n/10=n×10n-1

    • 由于除个位外最高位数字不能为零,需要去掉除个位0之外的多余的零,此时需要分别统计k=0和k!=0两种情况。

    • k!=0时:
      在n位以内的全部数字中k出现的次数为: s u m k = n × 1 0 n − 1 sum_k =n×10^{n-1} sumk=n×10n1

    • k=0时:
      在n位以内的全部数字中k出现的次数为: s u m k = n × 1 0 n − 1 − ∑ 1 n 1 0 n − 1 + 1 sum_k =n×10^{n-1} - \sum_1^n 10^{n-1}+1 sumk=n×10n11n10n1+1最末尾的+1是指个位数时0可以为最高位数字

  • 总结上述规律之后,我们在统计时可以将统计数字简化为
    1.统计当前位所含数字k数量
    2.统计之前位所含数字k数量

  • 统计当前位数字k数量时需要对比当前位数字和k之间的关系,分类进行统计。

  • 综合上述分析,采用n/10>0循环来作为判断条件,当n为零时需要单独考虑,该算法循环次数仅需次数为log10n,空间复杂度为O(1),代码如下。

public class Solution {
    /**
     * @param k: An integer
     * @param n: An integer
     * @return: An integer denote the count of digit k in 1..n
     */
    public int digitCounts(int k, int n) {
        // write your code here
        if (n == 0) {
            if(k == 0 ) return 1;
            else return 0;
        }
        int unit = 1;//当前位的单位1、10、100、
        int place = 0;//当前位所在的前一位
        
        int sum =0;//统计结果
        int temp;//存储n每一位的数值
        int count=0;//统计次一位数的总数
        int tail = 0;//当k为0时统计过多计算的0
        while (n>0){
            temp =n%10;
            //统计当前位所含数字k的数量
            if(temp>k) {
                sum += unit;
            }else if (temp == k){
                sum += count+1;
            }
            //统计之前位所含数字k的数量
            if(k == 0){
                sum += temp*place*unit/10 -tail;//注意第一次个位时tail = 0
            }else{
                sum += temp*place*unit/10;
            }
            
            count += temp * unit;
            place+=1;
            unit *= 10;
            n/=10;
            tail = unit;
        }
        return sum;
    }
}

5.移除9

描述

  • 从整数1开始,删除任意整数包含9,像是9, 19, 29…
    现在,我们有一串新的整数序列: 1, 2, 3, 4, 5, 6, 7, 8, 10, 11, …
    给定正整数n,你需要返回删除之后序列的第n个整数。注意1将会是第一个整数。

    n 不会超过 9 x 108.

样例

输入:88
输出:107
解释:
移除包含9的数字,第88个数字为107

思路

  • 实质上是进行了进制转换,将十进制的数转化为了9进制
  • 该算法循环次数仅需次数为log9n,空间复杂度为O(1)具体代码如下:
public class Solution {
    /**
     * @param n: an integer
     * @return: return an long integer
     */
    public long newInteger(int n) {
        // write your code here
        long res = 0;
        int times = 0;
        
        while (n >0){
            res += Math.pow(10,times)*(n%9);
            n = n / 9;
            times+=1;
        }
        return res;
    }
    
}

6.丑数 II

描述

  • 设计一个算法,找出只含素因子2,3,5 的第 n 小的数。
    符合条件的数如:1, 2, 3, 4, 5, 6, 8, 9, 10, 12…

      我们可以认为 1 也是一个丑数。
    

    挑战: 要求时间复杂度为 O(nlogn) 或者 O(n)。

样例

输入:9
输出:10

思路

  • 分析题目之后,需要明确这道题的关键在于找出只含2,3,5因子的数字。

  • 看到这道题时我很自然想到的是筛法,但筛法无法剔除掉不满足条件的因子。如果采取暴力方式,在筛掉数据之后,显然需要去逐个筛查每个字符是否满足只含2、3、5的因子。这样做无疑会严重增加算法的时间复杂度。

  • 因此我们这里需要将思路从剔除不满足条件的因子 转换到如何利用因子逐个生成满足条件的数字

  • 从数学的角度讲,这道题就是找出由集合{2i3j5k: i,j,k ∈ N}以及乘法运算构成的半群,按从小到大顺序的第n个元素。

  • 可以发现:从1开始,
    当1分别乘2,3,5之后,1就无需使用到
    当2分别乘2,3,5之后,2就无需使用到
    当某一个数字乘2过后,那么只需要考虑该数字乘3和5的情况。

  • 最好的方法就是采用动态规划的方式,逐个生成未使用的最小数字

      使用长度为n的数组存放依次生成的数字。
      
      3个指针指向数组中未被使用到的最小的数字的位置。
      
      从数组第二个位置开始到数组最后位置结束:
      
      	找出三个指针指向的数字生成的最小的数字,存放在数组的空位。
      	该数字对应的指针右移一位。
      	
      数组的最后一位数字即为满足条件的结果。
    
  • 该算法时间复杂度为O(n),空间复杂度为O(n),具体代码如下:

public class Solution {
    /**
     * @param n: An integer
     * @return: return a  integer as description.
     */
    int[] temp = new int[3];//存放乘2、3、5的指针
     
    public int nthUglyNumber(int n) {
        // write your code here
        if (n <= 6) return n;//小于6返回本身即可
        int[] res = new int[n];//存放生成的结果
        res[0] = 1;//初始化种子为1
        for (int i =1;i<n ;i++ ){
            res[i] = getMin(res);//获取最小的满足条件结果
        } 
        return res[n-1];
    }
    
    public int getMin(int[] res){
        int a =res[temp[0]]*2;
        int b =res[temp[1]]*3;
        int c =res[temp[2]]*5;
        
        int min = (min = a<b?a:b) <c? min:c;
        if (a == min) temp[0]++;
        if (b == min) temp[1]++;
        if (c == min) temp[2]++;
        
        return min;
    }
}

7.落单的数Ⅱ

描述

  • 给出3*n + 1 个非负整数,除其中一个数字之外其他每个数字均出现三次,找到这个数字。

样例

输入:  [1,1,2,3,3,3,2,2,4,1]
输出:  4

挑战: 一次遍历,常数级的额外空间复杂度

思路

  • 最为直观的思路是对数组进行排序,然后每隔3个数间隔对比之后一个数。但这样做显然无法做到一次遍历,因为在排序时就已经进行过遍历。

  • 仔细观察题目要求,可以试图将思路转变为如何将三个相同的数之和变为0

    由于非负整数,无需考虑补码存储问题,这里我们可以使用一个长度为32的int数组统计每一位1的数量,然后对3取余,然后再将数组转换为int即可。

  • 这样能够满足只遍历一次的要求,并且额外的空间复杂度只有常数级。算法时间复杂度为O(n)。

public class Solution {
    /**
     * @param A: An integer array
     * @return: An integer
     */
    public int singleNumberII(int[] A) {
        // write your code here
        int[] temp =new int[32];
        int pos =0;
        int res = 0;
        for(int i = 0;i< A.length;i++){
            while(A[i] >0){
                if (A[i]%2 == 1)
                    temp[pos] +=1;
                pos ++;
                A[i] /= 2;
            }
            pos = 0;
        }
        for(int i =31; i>=0;i--){
            res *= 2;
            res += temp[i]%3;
        }
        return res;
    }
}
  • 上述算法需要每次都进行额外的最差32次的循环,而且还需要将数组重组为int类型。如果能够使用位运算来处理,可以有效提高算法的速度,为此回顾了有限状态机相关的内容。

     构造一个三进制的归零的进制规则 即 00->01->10->00
     这样每三个相同的数字运算之后,都会回到0
     由于对于int来说额外增加一位共需64位的空间,因此这里使用两个int类型的变量分别存储高位和低位。最终高位将都是0,因此结果为低位值。
     前一步的低位^A[i]  & ~高位     得到当前低位值
     前一步的高位^A[i] & ~当前低位   得到当前高位值
    
  • 这样能够满足只遍历一次的要求,并且额外的空间只有2个int大小。算法时间复杂度为O(n),运行时间将小于上面的算法。

public class Solution {
    /**
     * @param A: An integer array
     * @return: An integer
     */
    public int singleNumberII(int[] A) {
        // write your code here
        int high = 0;
        int low = 0;
        for(int i = 0;i< A.length;i++){
            low = low ^ A[i] & ~high;
            high = high ^A[i] & ~low;
        }
        return low;
    }
}

8.落单的数Ⅲ

描述

  • 给出2*n + 2个的数字,除其中两个数字之外其他每个数字均出现两次,找到这两个数字。

样例

样例 1:
输入:  [1,2,2,3,4,4,5,3]
输出:  [1,5]

样例 2:
输入: [1,1,2,3,4,4]
输出:  [2,3]

挑战: O(n)时间复杂度,O(1)的额外空间复杂度

思路

  • 最为直观的思路是统计每个数字出现的次数,显然额外空间复杂度时O(n),想要完成挑战目标,还是要从位运算考虑。

  • 很容易发现这道题和Ⅰ相比多了一个数,对数组中的数字按位异或后的数为a^b,我们这里需要对a和b进行区分,区分之后便可以区分出这两个数。

     假设数组为A
     考察a^b
     由于a、b两数不同,那么必然存在1位k为1
     并且该位必定属于a、b其中之一
     那么也就是说我们以这一位k作为判断
     也就是对A[i] & k  ==0 判断 ,同时分别进行按位异或,
     就可以区分出来这两个数。
    
  • 找到k位为1数时可以逐位%2取余判断,也可以按位&1来判断。
    这里可以利用补码的特性,非零数 n & -n 可以得到 n 最后一位非零位。

      以8 bit为例 
      3: 0000 0011   -3: 1111 1101   3&-3:  0000 0001
      6: 0000 0110   -3: 1111 1011   3&-3:  0000 0010
    
  • 这样能够满足O(1)额外的空间复杂度,需要遍历两次算法时间复杂度为O(n)。

public class Solution {
    /**
     * @param A: An integer array
     * @return: An integer array
     */
    public List<Integer> singleNumberIII(int[] A) {
        // write your code here
        int temp = 0;
        int[] res =new int[2];
        for (int i : A){
            temp ^=i;
        }
        temp &= -temp;//找到一位不为0的数
        for (int i : A){
            if((i & temp) == 0){
                res[0] ^=i;
            }else{
                res[1] ^=i;
            }
        }
        return Arrays.asList(res[0],res[1]);
    }
}

9.骰子求和

描述

  • 扔 n 个骰子,向上面的数字之和为 S。给定 n,请列出所有可能的 S 值及其相应的概率。

样例

输入:n = 2
输出:[[2,0.03],[3,0.06],[4,0.08],[5,0.11],[6,0.14],[7,0.17],[8,0.14],[9,0.11],[10,0.08],	[11,0.06],[12,0.03]]

思路

  • 这道题实际上是求掷骰子结果的概率分布,总的投掷可能性是6n,可能的结果范围为 n~6n

     当然我们可以模拟掷骰子的全部可能性,统计各个结果的总数
     但无疑这样做的时间复杂度和空间复杂度都会相当大
     这里应该致力于减少算法的空间复杂度和时间复杂度
     
     通常的解法是使用动态规划来进行处理,这里我并不想这样处理,可以参见 LintCode 20题评论下的解答方法。
    
  • 这次我们从数学的角度考虑,由于每一次投掷的结果都会依赖上一次的掷骰子的结果,实际上就是一种离散卷积。

  • 概率分布如下:
    F ( n ) = ∑ 1 6 F ( n − 1 ) k F(n) = \sum_{1}^6F(n-1)k F(n)=16F(n1)k 边 界 值 F ( 1 ) = k = [ 1 6 , 1 6 , 1 6 , 1 6 , 1 6 , 1 6 ] 边界值F(1)=k=[\frac1 6,\frac1 6,\frac1 6,\frac1 6,\frac1 6,\frac1 6] F1=k=[61,61,61,61,61,61]

  • 或者也可以理解为下式的展开项的系数: ( 1 + x + x 2 + x 3 + x 4 + x 5 ) n 6 n \frac{(1+x+x^2+x^3+x^4+x^5)^n}{6^n} 6n(1+x+x2+x3+x4+x5)n

  • 下面是具体算法,空间复杂度O(n),时间复杂度为O(n2)。

public class Solution {
    /**
     * @param n an integer
     * @return a list of Map.Entry<sum, probability>
     */
    public List<Map.Entry<Integer, Double>> dicesSum(int n) {
        // Write your code here
        // Ps. new AbstractMap.SimpleEntry<Integer, Double>(sum, pro)
        // to create the pair
        HashMap<Integer, Double> map = new HashMap<Integer, Double>();
        if (n == 1){//为1时直接返回结果
            for (int i = 1;i< 7 ;i++){
                map.put(i,1.0/6.0);
            }
            
        } else{//计算骰子个数为n时的结果概率分布
            int all = 5*n+1;
            long[] temp = new long[all];
            for(int i=0; i<6;i++){//初始化起始迭代
                temp[i] = 1L;
            }
            int current = 5;
            int next;
            for (int s=1;s< n;s++){//统计频数,(利用技巧降低空间复杂度)
                next = current + 5;
                for(int i = 0;i< next/2+1;i++){
                    temp[next-i] += temp[next-i-1]+temp[next-i-2]+temp[next-i-3]+temp[next-i-4]+temp[next-i-5];
                }
                for(int i = 0;i< next/2+1;i++){//减少计算次数
                    temp[i] = temp[next -i];
                }
                
                current = next;
            }
            double sum = Math.pow(6,n);//计算总数
            
            for(int i=0;i<all;i++){
                map.put(i+n,temp[i]/sum);
            }
        }
        return new ArrayList<Map.Entry<Integer, Double>>(map.entrySet());//输出符合条件结果。
    }
}
  • 若使用FFT时间复杂度会降到O(nlogn),有兴趣可以尝试以下。

10 两数之和

描述

  • 给一个整数数组,找到两个数使得他们的和等于一个给定的数 target。
    你需要实现的函数twoSum需要返回这两个数的下标, 并且第一个下标小于第二个下标。注意这里下标的范围是 0 到 n-1。

样例

样例1:
给出 numbers = [2, 7, 11, 15], target = 9, 返回 [0, 1].
样例2:
给出 numbers = [15, 2, 7, 11], target = 9, 返回 [1, 2]。

挑战

  • O(n) 空间复杂度,O(nlogn)时间复杂度,
    O(n) 空间复杂度,O(n)时间复杂度,

思路

  • 很明显这道题想要降低时间复杂度需要使用额外的空间
  • 注意到当一个数a在数组中,那么必然target - a也在数组中
  • 因此我们构造一个存储target - number[i] 的另一个容器类,然后判断是否number[i]在该容器中,即可查找到两个目标数的位置。
  • 这里选用set可以使时间复杂度降为O(n),因为set.contains()方法为O(1)时间复杂度。
  • 下面是具体算法,时间复杂度O(n),空间复杂度O(n).
public class Solution {
    /**
     * @param numbers: An array of Integer
     * @param target: target = numbers[index1] + numbers[index2]
     * @return: [index1 + 1, index2 + 1] (index1 < index2)
     */
    public int[] twoSum(int[] numbers, int target) {
        // write your code here
        Set<Integer> set = new HashSet<Integer>();
        int check = 0;
        int[] res = new int[]{-1,-1};
        int find1 = 0;
        for (int i = 0;i<numbers.length ; i++){
            set.add(target - numbers[i]); 
        }
        
        for (int i = 0; i<numbers.length;i++){
            if(set.contains(numbers[i])){
                if(target == 0){
                    if (check == 0){
                        res[0] = i;
                        check +=1;
                    }else if(numbers[res[0]] + numbers[i] == target){
                        res[1] = i;
                        return res;
                    }else if(check == 1){
                        res[1] = i;
                        check +=1;
                    }else{
                        if(numbers[res[0]] + numbers[i] == target){
                            res[1] = i;
                            return res;
                        }else{
                            res[0] = res[1];
                            res[1] = i;
                            return res;
                        }
                    }
                }else{
                    if (check == 0){
                        res[0] = i;
                        check +=1;
                    }else{
                        res[1] = i;
                        return res;
                    }
                }
                
            }
        }
        return res;
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值