数学专题

数学

寻找缺失的整数

需求:在一个无序数组里有99个不重复的正整数,范围是1~100,唯独缺 少1个1~100中的整数。如何找出这个缺失的整数?
解法一:Hash表
- 创建一个哈希表,以1到100这100个整数为Key。然后遍历整个数组,每读到一个整数,就定位到哈希表中对应的Key,然后删除这个Key。
- 由于数组中缺少1个整数,哈希表最终一定会有99个Key被删除,从而剩下1个唯一的Key。这个剩下的Key就是那个缺失的整数。 
- 假设数组长度是n,那么该解法的时间复杂度是O(n),空间复杂度是O(n)。
- 这个解法在时间上是最优的,但额外开辟了内存空间,有没有办法降低空间复杂度呢?
解法二:排序,找相邻元素不连续
- 先把数组元素从小到大进行排序,然后遍历已经有序的数组,如果发现某两个相邻元素并不连续,说明缺少的就是这两个元素之间的整数。
- 假设数组长度是n,如果用时间复杂度为O(nlogn)的排序算法进行排序,那么该解法的时间复杂度是O(nlogn),空间复杂度是O(1)。 
- 这个解法没有开辟额外的空间,但是时间复杂度又太大了。有没有办法对时间复杂度和空间复杂度都进行优化呢?
解法三:
- 这是一个很简单也很高效的方法,先算出1+2+3+…+100的和,然后依次减去数组里的元素,最后得到的差值,就是那个缺失的整数。 
- 假设数组长度是n,那么该解法的时间复杂度是O(n),空间复杂度是O(1)。
- 对于没有重复元素的数组,这个解法在时间和空间上已经最优了。但如果把问题扩展一下

扩展问题1:
- 一个无序数组里有若干个正整数,范围是1~100,其中99个整数都出现了偶数次 ,只有1个整数出现了奇数次 ,如何找到这个出现奇数次的整数?
- 只要把数组里所有元素依次进行异或运算,最后得到的就是那个缺失的整数!
扩展问题2:
- 假设一个无序数组里有若干个正整数,范围是1~100,其中有98个整数出现了偶数次,只有2个 整数出现了奇数次,如何找到这2个出现奇数次的整数?
- 首先把数组元素依次进行异或运算,得到的结果是2个出现了奇数次的整数的异或运算结果,在这个结果中至少有1个二进制位是1。分治算法
- 举个例子,给出一个无序数组{4,1,2,2,5,1,4,3},所有元素进行异或运算的结果是00000110B。 
- 选定该结果中值为1的某一位数字,如00000110B的倒数第2位是1,这说明A和B对应的二进制的倒数第2位是不同的。其中必定有一个整数的倒数第2位是0,另一个整数的倒数第2位是1。
- 根据这个结论,可以把原数组按照二进制的倒数第2位的不同,分成两部分,一部分的倒数第2位是0,另一部分的倒数第2位是1。由于A和B的倒数第2位不同,所以A被分配到其中一部分,B被分配到另一部分,绝不会出现A和B在同一部分,另一部分既没有A,也没有B的情况。

public class FindLostNum {

    public static int[] findLostNum(int[] array) {
        //用于存储两个出现奇数次的整数
        int result[] = new int[2];
        //第一次整体异或
        int xorResult = 0;
        for(int i=0;i<array.length;i++){
            xorResult^=array[i];
        }
        //如果异或结果为0,说明输入数组不符合题目
        if(xorResult == 0){
            return null;
        }
        //确定两个整数的不同位,以此来做分组
        int separator = 1;
        while (0==(xorResult&separator)){
            separator<<=1;
        }
        //第二次分组异或
        for(int i=0;i<array.length;i++){
            if(0==(array[i]&separator)){
                result[0]^=array[i];
            }else {
                result[1]^=array[i];
            }
        }

        return result;
    }

    public static void main(String[] args) {
        int[] array = {4,1,2,2,5,1,4,3};
        int[] result = findLostNum(array);
        System.out.println(result[0] + "," + result[1]);
    }
}

删去k个数字后的最小值

需求:给出一个整数,从该整数中去掉k个数字,要求剩下的数字形成的新整数尽可能小。应该如何选取被去掉的数字?
- 比如1593212  删除3个数  最小1212
- 30020  删去1个数  最小 200
- 10删去2个数  最小0
- 3549 删去1个数  最小349
- 找规律  先讨论删除1个数字的情况,发现删除开始逆序的那个数字
- 验证541270936   删除5  
- 41270936  删除4
- 1270936  删除7
- 如果需要删除多个数字,那么就依次求的局部最优解,最终得到全局最优解,叫贪心算法

解法一:
- 每一次内层循环都需要从头开始遍历所有数字
- subString方法本身性能不高

解法二:
- 通过栈来实现

public class RemoveKDigits {

    /**
     * 删除整数的k个数字,获得删除后的最小值
     * @param num  原整数
     * @param k  删除数量
     */
    public static String removeKDigits(String num, int k) {
        for(int i=0; i<k; i++){
            boolean hasCut = false;
            //从左向右遍历,找到比自己右侧数字大的数字并删除
            for(int j=0; j<num.length()-1;j++){
                if(num.charAt(j) > num.charAt(j+1)){
                    num = num.substring(0, j) + num.substring(j+1,num.length());
                    hasCut = true;
                    break;
                }
            }
            //如果没有找到要删除的数字,则删除最后一个数字
            if(!hasCut){
                num = num.substring(0, num.length()-1);
            }
        }
        //清除整数左侧的数字0
        int start = 0;
        for(int j=0; j<num.length()-1; j++){
            if(num.charAt(j) != '0'){
                break;
            }
            start++;
        }
        num = num.substring(start, num.length()) ;
        //如果整数的所有数字都被删除了,直接返回0
        if(num.length() == 0){
            return "0";
        }
        return num;
    }

    /**
     * 删除整数的k个数字,获得删除后的最小值
     * @param num  原整数
     * @param k  删除数量
     */
    public static String removeKDigitsV2(String num, int k) {
        //新整数的最终长度 = 原整数长度 - k
        int newLength = num.length() - k;
        //创建一个栈,用于接收所有的数字
        char[] stack = new char[num.length()];
        int top = 0;
        for (int i = 0; i < num.length(); ++i) {
            //遍历当前数字
            char c = num.charAt(i);
            //当栈顶数字大于遍历到的当前数字,栈顶数字出栈(相当于删除数字)
            while (top > 0 && stack[top-1] > c && k > 0) {
                top -= 1;
                k -= 1;
            }
            //如果遇到数字0,且栈为空,0不入栈
            if('0' == c && top == 0){
                newLength--;
                if(newLength <= 0){
                    return "0";
                }
                continue;
            }
            //遍历到的当前数字入栈
            stack[top++] = c;
        }
        // 用栈构建新的整数字符串
        return newLength<=0 ? "0" : new String(stack, 0, newLength);
    }

    public static void main(String[] args) {
        System.out.println(removeKDigits("1593212", 3));
        System.out.println(removeKDigits("30200", 1));
        System.out.println(removeKDigits("10", 2));
        System.out.println(removeKDigits("541270936", 3));
        System.out.println(removeKDigits("1593212", 4));
        System.out.println(removeKDigits("1000020000000010", 2));
    }
}

如何实现大整数相加

需求:两个100位的整数相加,long类型都存不下,怎么计算两个数相加的结果。
解决方案一:
- 用数组来代替整数 
- 两个数组相加,遍历

解决方案二:
- 比如有一个50位的大整数,没必要拆分成51长度的数组
- int类型的取值范围是-2 147 483 648~2 147 483 647,最多可以有10位整数,为了防止溢出,可以把大整数的每9位作为数组的一个元素,进行加法运算
- 也可以使用long类型来拆分,按照int类型拆分仅仅是提供一个思路
- 长度为50位的大整数,只需要长度为6的数组,每个数组元素放一个9位数
- java中,工具类BigInteger和BigDecimal的底层实现同样是把大整数拆分成数组进行运算的,和这个思路大体类似

public class BigNumberSum {

    /**
     * 大整数求和
     * @param bigNumberA  大整数A
     * @param bigNumberB  大整数B
     */
    public static String bigNumberSum(String bigNumberA, String bigNumberB) {
        //1.把两个大整数用数组逆序存储,数组长度等于较大整数位数+1
        int maxLength = bigNumberA.length() > bigNumberB.length() ? bigNumberA.length() : bigNumberB.length();
        int[] arrayA = new int[maxLength+1];
        for(int i=0; i< bigNumberA.length(); i++){
            arrayA[i] = bigNumberA.charAt(bigNumberA.length()-1-i) - '0';
        }
        int[] arrayB = new int[maxLength+1];
        for(int i=0; i< bigNumberB.length(); i++){
            arrayB[i] = bigNumberB.charAt(bigNumberB.length()-1-i) - '0';
        }
        //2.构建result数组,数组长度等于较大整数位数+1
        int[] result = new int[maxLength+1];
        //3.遍历数组,按位相加
        for(int i=0; i<result.length; i++){
            int temp = result[i];
            temp += arrayA[i];
            temp += arrayB[i];
            //判断是否进位
            if(temp >= 10){
                temp = temp-10;
                result[i+1] = 1;
            }
            result[i] = temp;
        }
        //4.把result数组再次逆序并转成String
        StringBuilder sb = new StringBuilder();
        //是否找到大整数的最高有效位
        boolean findFirst = false;
        for (int i = result.length - 1; i >= 0; i--) {
            if(!findFirst){
                if(result[i] == 0){
                    continue;
                }
                findFirst = true;
            }
            sb.append(result[i]);
        }
        return sb.toString();
    }

    public static void main(String[] args) {
        System.out.println(bigNumberSum("426709752318", "95481253129"));
    }
}

如何求出最大公约数

需求:求两个整数的最大公约数,要尽量优化算法的性能。
辗转相除法  又叫做欧几里得算法:目的是求出两个正整数的最大公约数。
- 两个正整数a和b(a>b),它们的最大公约数等于a除以b的余数c和b之间的最大公约数。 
- 例如10和25,25除以10商2余5,那么10和25的最大公约数,等同于10和5的最大公约数。
- 当两个整数较大时,做a%b取模运算的性能会比较差

更相减损术:当两个整数较大时,做a%b取模运算的性能会比较差,采用更相减损术,是一种求最大公约 数的算法。
- 两个正整数a和b(a>b),它们的最大公约数等于a-b的差值c和较小数b的最大公约数。
- 例如10和25,25减10的差是15,那么10和25的最大公约数,等同于10和15的最大公约数。
- 更相减损术是不稳定的算法,当两数相差悬殊时,如计算10000和1的最大公约数,就要递归9999次!

最优解:
- 众所周知,移位运算的性能非常好。对于给出的正整数a和b,不难得到如下的结论。(从下文开始,获得最大公约数的方法getGreatestCommonDivisor被简写为gcd。) 
  - 当a和b均为偶数时,gcd(a,b) = 2×gcd(a/2, b/2) = 2×gcd(a>>1,b>>1)。 
  - 当a为偶数,b为奇数时,gcd(a,b) = gcd(a/2,b) = gcd(a>>1,b)。 
  - 当a为奇数,b为偶数时,gcd(a,b) = gcd(a,b/2) = gcd(a,b>>1)。 
  - 当a和b均为奇数时,先利用更相减损术运算一次,gcd(a,b) = gcd(b,a-b),此时a-b必然是偶数,然后又可以继续进行移位运算。 
- 例如计算10和25的最大公约数的步骤如下。 
  - 整数10通过移位,可以转换成求5和25的最大公约数。 
  - 利用更相减损术,计算出25-5=20,转换成求5和20的最大公约数。
  - 整数20通过移位,可以转换成求5和10的最大公约数。 
  - 整数10通过移位,可以转换成求5和5的最大公约数。 
  - 利用更相减损术,因为两数相等,所以最大公约数是5。 
  - 这种方式在两数都比较小时,可能看不出计算次数的优势;当两数越大时,计算次数的减少就会越明显。

时间复杂度:
1. 暴力枚举法: 时间复杂度是O(min(a, b))。 
2. 辗转相除法: 时间复杂度不太好计算,可以近似为O(log(max(a, b))),但是取模运算性能较差。 
3. 更相减损术: 避免了取模运算,但是算法性能不稳定,最坏时 间复杂度为O(max(a, b))。 
4. 更相减损术与移位相结合: 不但避免了取模运算,而且算法性 能稳定,时间复杂度为O(log(max(a, b)))。 

public class GreatestCommonDivisor {

    //暴力枚举
    public static int getGreatestCommonDivisor(int a, int b){
        int big = a>b ? a:b;
        int small = a<b ? a:b;
        if(big%small == 0){
            return small;
        }
        for(int i= small/2; i>1; i--){
            if(small%i==0 && big%i==0){
                return i;
            }
        }
        return  1;
    }

    //辗转相除法
    public static int getGreatestCommonDivisorV2(int a, int b){
        int big = a>b ? a:b;
        int small = a<b ? a:b;
        if(big%small == 0){
            return small;
        }
        return getGreatestCommonDivisorV2(big%small, small);
    }

    //更相减损术
    public static int getGreatestCommonDivisorV3(int a, int b){
        if(a == b){
            return a;
        }
        int big = a>b ? a:b;
        int small = a<b ? a:b;
        return getGreatestCommonDivisorV3(big - small, small);
    }

    //辗转相除法+更相减损术  在更相减损术的基础上使用位移运算
    public static int gcd(int a, int b){
        if(a == b){
            return a;
        }
        if((a&1)==0 && (b&1)==0){
            return gcd(a >> 1, b >> 1)<<1;
        } else if((a&1)==0 && (b&1)!=0){
            return gcd(a >> 1, b);
        } else if((a&1)!=0 && (b&1)==0){
            return gcd(a, b >> 1);
        } else {
            int big = a>b ? a:b;
            int small = a<b ? a:b;
            return gcd(big - small, small);
        }
    }

    public static void main(String[] args) {
        System.out.println(gcd(25, 5));
        System.out.println(gcd(100, 80));
        System.out.println(gcd(27, 14));
    }
}

正则表达式匹配

//正则表达式匹配
//请实现一个函数用来匹配包括'.'和'*'的正则表达式。模式中的字符'.'表示任意一个字符,而'*'表示它前面的字符可以出现任意次(包含0次)。 在本题中,匹配是指字符串的所有字符匹配整个模式。例如,字符串"aaa"与模式"a.a"和"ab*ac*a"匹配,但是与"aa.a"和"ab*a"均不匹配

--------------------------------------------------------------------------------
      public boolean match(char[] str, char[] pattern) {
         return matchTwo(str, 0, str.length, pattern, 0, pattern.length);
      }

      private boolean matchTwo(char[] str, int i, int length1, char[] pattern,
                               int j, int length2) {
         if (i == length1 && j == length2) {
            return true;
         }
         if (i == length1 && j != length2) {
            while (j != length2) {
               if (pattern[j] != '*' && (j + 1 >= length2 || pattern[j + 1] != '*')) {
                  return false;
               }
               j++;
            }
            return true;
         }
         if (i != length1 && j == length2) {
            return false;
         }
         if (j + 1 == length2) {
            if (str[i] == pattern[j] || pattern[j] == '.')
               return matchTwo(str, i + 1, length1, pattern, j + 1, length2);
            else {
               return false;
            }
         }
         if ((str[i] == pattern[j] || pattern[j] == '.') && pattern[j + 1] != '*')
            return matchTwo(str, i + 1, length1, pattern, j + 1, length2);
         if ((str[i] == pattern[j] || pattern[j] == '.') && pattern[j + 1] == '*')
            return matchTwo(str, i, length1, pattern, j + 2, length2) || matchTwo(str, i + 1, length1, pattern, j, length2);
         if (pattern[j + 1] == '*')
            return matchTwo(str, i, length1, pattern, j + 2, length2);
         return false;
      }

表示数值的字符串

//表示数值的字符串
//请实现一个函数用来判断字符串是否表示数值(包括整数和小数)。例如,字符串"+100","5e2","-123","3.1416"和"-1E-16"都表示数值。 但是"12e","1a3.14","1.2.3","+-5"和"12e+4.3"都不是。
--------------------------------------------------------------------------------
      public boolean isNumeric(char[] str) {
         String s = String.valueOf(str);
         //return s.matches("[+-]?[0-9]*(.[0-9]*)?([eE][+-]?[0-9]+)?");
         return s.matches("[+-]?[0-9]*(\\.[0-9]*)?([eE][+-]?[0-9]+)?");
      }

二进制中1的个数

//输入一个整数,输出该数二进制表示中1的个数。其中负数用补码表示。
//机灵1
      public int NumberOf1(int n) {
         //1010 1000   10100111  1010000  1001111   1000000  0111111       
         int count = 0;
         while (n != 0) {
            count++;
            n = n & (n - 1);
         }
         return count;
      }
//机灵2
      public int NumberOf1(int n) {
         int count = 0;
         while (n != 0) {
            if ((n & 1) == 1) {
               count++;
            }
            n = n >>> 1;//无符号右移
         }
         return count;
      }

从1到n整数中1出现的次数

//求出1-13的整数中1出现的次数,并算出100-1300的整数中1出现的次数?
// 为此他特别数了一下1~13中包含1的数字有1、10、11、12、13因此共出现6次,但是对于后面问题他就没辙了。
// ACMer希望你们帮帮他,并把问题更加普遍化,可以很快的求出任意非负整数区间中1出现的次数(从1 到 n 中1出现的次数)。
//机灵1
      public int NumberOf1Between1AndN_Solution(int n) {
         int count = 0;
         while (n > 0) {
            String str = String.valueOf(n);
            char[] chars = str.toCharArray();
            for (int i = 0; i < chars.length; i++) {
               if (chars[i] == '1')
                  count++;
            }
            n--;
         }
         return count;
      }
//递归版
    public int NumberOf1Between1AndN_Solution(int n) {
        if (n <= 0) {
            return 0;
        }
        return NumberOf1Between1AndN_Solution(n - 1) + countNumberOf1(n);
    }
//递推版
      public int NumberOf1Between1AndN_Solution(int n) {
         if (n <= 0) {
            return 0;
         }
         int count = 0;
         for (int i = 1; i <= n; i++) {
            count += countNumberOf1(i);
         }
         return count;
      }

      public int countNumberOf1(int num) {
         int count = 0;
         while (num != 0) {
            if (num % 10 == 1) {
               count++;
            }
            num /= 10;
         }
         return count;
      }

丑数

//把只包含质因子2、3和5的数称作丑数(Ugly Number)。例如6、8都是丑数,但14不是,因为它包含质因子7。 习惯上我们把1当做是第一个丑数。求按从小到大的顺序的第N个丑数。
    public int GetUglyNumber_Solution(int index) {
        if (index <= 0) return 0;
		int[] result = new int[index];
		result[0] = 1;
		int i2 = 0;
		int i3 = 0;
		int i5 = 0;
		for (int i = 1; i < result.length; i++) {
			int temp = Math.min(Math.min(result[i2] * 2, result[i3] * 3), result[i5] * 5);
			if (result[i2] * 2 == temp) i2++;
			if (result[i3] * 3 == temp) i3++;
			if (result[i5] * 5 == temp) i5++;
			result[i] = temp;
		}
		return result[index - 1];
    }

和为S的连续正数序列

//小明很喜欢数学,有一天他在做数学作业时,要求计算出9~16的和,他马上就写出了正确答案是100。
// 但是他并不满足于此,他在想究竟有多少种连续的正数序列的和为100(至少包括两个数)。没多久,他就得到另一组连续正数和为100的序列:18,19,20,21,22。
// 现在把问题交给你,你能不能也很快的找出所有和为S的连续正数序列? Good Luck!
public ArrayList<ArrayList<Integer>> FindContinuousSequence(int sum) {
   ArrayList<ArrayList<Integer>> result = new ArrayList<>();
   int left = 1;
   int right = 2;
   while (left < right) {
      int temp = (left + right) * (right - left + 1) / 2;
      if (temp == sum) {
         ArrayList<Integer> tempresult = new ArrayList<>();
         for (int i = left; i <= right; i++) {
            tempresult.add(i);
         }
         result.add(tempresult);
         left++;
      } else if (temp < sum) {
         right++;
      } else {
         left++;
      }
   }
   return result;
}

判断是否是2的幂

//机灵1
public boolean isPowerOfTwo(int n) {
   if (n <= 0) return false;
   int result = n & (n - 1);
   if (result == 0) {
      return true;
   } else {
      return false;
   }
}
//机灵2
public boolean isPowerOfTwo(int n) {
   while (true) {
      if (n == 1) {
         return true;
      }
      if (n % 2 == 0) {
         n = n / 2;
      } else {
         return false;
      }
   }
}

二进制求和

//输入: a = "1010", b = "1011"
//输出: "10101" 
public String addBinary(String a, String b) {
   char[] arra = a.toCharArray();
   char[] arrb = b.toCharArray();
   int lena = arra.length - 1;
   int lenb = arrb.length - 1;
   StringBuilder result = new StringBuilder();
   int temp = 0;
   int sum = 0;
   while (lena > 0 || lenb > 0) {
      if (lena > 0) {
         temp = sum + arra[lena] - '0';
         lena--;
      }
      if (lenb > 0) {
         temp = sum + arrb[lenb] - '0';
         lenb--;
      }
      result.append(temp % 2);
      sum = temp / 2;
   }
   if (sum == 1) {
      result.append(1);
   }
   return result.reverse().toString();
}

比特位计数

方式一循环计算
方式二count[i] = count[i&(i-1)]+1
public int[] countBits(int num) {
        int[] ans = new int[num + 1];
        for (int i = 0; i <= num; ++i)
            ans[i] = popcount(i);
        return ans;
    }
    private int popcount(int x) {
        int count;
        for (count = 0; x != 0; ++count)
          x &= x - 1; //zeroing out the least significant nonzero bit
        return count;
    }

public int[] countBits(int num) {
        int[] ans = new int[num + 1];
        int i = 0, b = 1;
        // [0, b) is calculated
        while (b <= num) {
            // generate [b, 2b) or [b, num) from [0, b)
            while(i < b && i + b <= num){
                ans[i + b] = ans[i] + 1;
                ++i;
            }
            i = 0;   // reset i
            b <<= 1; // b = 2b
        }
        return ans;
    }

public int[] countBits(int num) {
      int[] ans = new int[num + 1];
      for (int i = 1; i <= num; ++i)
        ans[i] = ans[i >> 1] + (i & 1); // x / 2 is x >> 1 and x % 2 is x & 1
      return ans;
  }

public int[] countBits(int num) {
      int[] ans = new int[num + 1];
      for (int i = 1; i <= num; ++i)
        ans[i] = ans[i & (i - 1)] + 1;
      return ans;
  }
已标记关键词 清除标记
©️2020 CSDN 皮肤主题: 点我我会动 设计师:上身试试 返回首页