刘旭的个人网站

问题7 整数反转 ★☆☆☆☆

https://leetcode.cn/problems/reverse-integer/

按十进制位反转一个整数的思路是比较直接的,由低位到高位,我们可以很方便地遍历一个整数的十进制位,在遍历过程中,也很容易维护一个反转后的整数值。唯一的难点是如何处理overflow的问题。如果可以使用长整型,那么overflow的问题也很容易解决。但是题目明确要求不要使用长整型。稍作思考,只能在可能overflow前去判断。

	public int reverse(int x) {
		if (x == Integer.MIN_VALUE) {
			return 0;
		}

		int maxIntDivide10 = Integer.MAX_VALUE / 10;
		int maxIntDivide10Remain = Integer.MAX_VALUE % 10;

		boolean negative = x < 0;
		int absX = x >= 0 ? x : -x;

		int reverse = 0;
		while (absX > 0) {
			int remain = absX % 10;

			if (reverse > maxIntDivide10
				|| (reverse == maxIntDivide10 && remain > maxIntDivide10Remain)) {
				return 0;
			}

			reverse = reverse * 10 + remain;

			absX = absX / 10;
		}

		return negative ? -reverse : reverse;
	}

问题8 字符串转换整数 ★☆☆☆☆

https://leetcode-cn.com/problems/string-to-integer-atoi/

这个题目是经典的字符串转整数,从解题思路上来说,并不复杂,根据题目要求按顺序处理字符串就可以,需要注意的是如何处理overflow的问题。相比问题7,这个题目并没有要求不能使用长整型,所以偷懒的做法就是直接用长整型。如果题目要求不能使用长整型,那么就需要使用和问题7中类似的方法,在overflow之前去判断,特别的,需要考虑到32位整数取值范围的非对称性。

    public int myAtoi(String s) {
        char[] chars = s.toCharArray();

        int i = 0;
        while (i < chars.length && Character.isSpaceChar(chars[i])) {
            i++;
        }

        if (i == chars.length
            || (chars[i] != '+' && chars[i] != '-' && !Character.isDigit(chars[i]))) {
            return 0;
        }

        boolean negative = false;
        if (chars[i] == '+' || chars[i] == '-') {
            negative = chars[i] == '-';
            i++;
        }

        long ret = 0;
        while (i < chars.length && Character.isDigit(chars[i])) {
            ret = ret * 10 + (chars[i]-'0');
            if (ret > Integer.MAX_VALUE) {
                break;
            }
            i++;
        }
        ret = ret * (negative ? -1 : 1);

        if (ret > Integer.MAX_VALUE) {
            return Integer.MAX_VALUE;
        } else if (ret < Integer.MIN_VALUE) {
            return Integer.MIN_VALUE;
        } else {
            return (int) ret;
        }
    }

问题12 整数转罗马数字 ★☆☆☆☆

https://leetcode-cn.com/problems/integer-to-roman/

题目乍一看有点唬人,简单来说,就是将阿拉伯数字转换为罗马数字。那么很直接的,按照罗马数字的进制规则,由大到小的处理基数就可以了,即按照1000、900、500、400、100、90、50、40、10、9、5、4、1的顺序,分别做如下计算:用当前值和基数做整数除法,计算得出包含的基数个数,添加到罗马数字表达法中,然后用余数和下一个基数重复此过程。

	private static int[] val = {1000, 900, 500, 400, 100, 90, 50, 40, 10, 9, 5, 4, 1};
	private static String[] symbol = {"M", "CM", "D", "CD", "C", "XC", "L", "XL", "X", "IX", "V", "IV", "I"};

	public String intToRoman(int num) {
		StringBuilder builder = new StringBuilder();

		int i = 0;
		while (num != 0) {
			int quotient = num / val[i];
			if (quotient != 0) {
				for (int j = 0; j < quotient; j++) {
					builder.append(symbol[i]);
				}
			}
			num = num % val[i];
			i++;
		}

		return builder.toString();
	}

问题1362 最接近的因数 ★☆☆☆☆

https://leetcode-cn.com/problems/closest-divisors/

这个题目我想到的就是暴力解法,抽象出一个计算某个数绝对值差最小的乘数对的方法,然后分别计算num+1和num+2的绝对值差最小的乘数对,然后比较它们哪个绝对值差更小。提交以后是没有问题的,不过效率是差了点。额,看了下官方题解,基本是一样的思路。

	public int[] closestDivisors(int num) {
		int target1 = num + 1;
		int target2 = num + 2;

		int[] divisors1 = minDiffDivisors(target1);
		int[] divisors2 = minDiffDivisors(target2);

		if (Math.abs(divisors1[0] - divisors1[1]) < Math.abs(divisors2[0] - divisors2[1])) {
			return divisors1;
		} else {
			return divisors2;
		}
	}

	private int[] minDiffDivisors(int num) {
		int max = (int) Math.floor(Math.sqrt(num));

		int minDiff = Integer.MAX_VALUE;
		int[] ret = new int[2];
		for (int i = 1; i <= max; i++) {
			if (num % i == 0) {
				int j = num / i;
				int curDiff = Math.abs(i - j);
				if (curDiff < minDiff) {
					minDiff = curDiff;
					ret[0] = i;
					ret[1] = j;
				}
			}
		}

		return ret;
	}

问题2481 分割圆的最少切割次数 ★☆☆☆☆

https://leetcode.cn/problems/minimum-cuts-to-divide-a-circle/

这个题目有点意思,我觉得是一个不错的思维题。

那么按照题目要求的将一个圆n等分,至少需要的cut数是多少呢?将一个圆n等分的结果是以圆心为顶点的n个角,每个角大小一样。每个角两条边,但是相邻的两个角共用一条边,所以将一个圆n等分,n条边肯定是够了,也就是n个cut。这个题目的关键就在于,这n个半长的cut中如果有2条可以连成1条全长的cut,就等于减小了一个cut,也就是说可能不需要n条边那么多就能将一个圆n等分。

那么什么情况下可以2条半长的cut连成1条全长的cut,从而减小cut数呢?这个地方我一开始陷入了一个错误的思路,我的直觉是需要考虑n等分后1个角的大小是否能整除180度,比如,将一个圆6等分,6条半长的cut肯定够了,但是180/60=3,所以只需要3条全长的cut就够了。然后我就提交了一版答案,先判断n是否能整除360,能整除的,再计算一个角的大小,再判断一个角的大小能否整除180。结果题目判定失败,错误示例,14等分需要的最小cut数是7,而不是14。

然后我就陷入了迷惑,14不能整除360,14等分后的一个角的大小也不能整除180,怎么会有能连成全长cut的呢?再稍作思考,才发现问题所在。关键不在于是不是能整除180,即便14等分的一个角不能整除180,但无论如何,14等分的一半是不是半圆?既然是半圆,那么显然任意连续的七个14等分角就都是半圆,所以最小cut数是7,而不是14。由此可推,是否能将2个半长的cut连成1个全长的cut,只取决于n是否能被2整除,只要能被2整除,那么就可以减半cut数。所以,答案其实很简单,判断n是否能被2整除,能整除的需要n/2个cut,不能整除的需要n个cut。

为什么我一开始会陷入认为需要考虑单角能否整除180的错误里呢?归根结底我觉得是对无理数、对数的连续性缺乏认知。由此我还想到数学史上的一桩轶事,就是如何只用直尺和圆规将一个圆17等分。据说这是伟大的数学家高斯错拿了老师的研究题目,而花了一夜时间解决了这个问题,第二天和老师抱怨昨天的题目太难了,老师看了以后惊掉下巴……

	public int numberOfCuts(int n) {
		if (n == 1) {
			return 0;
		}

		if (n % 2 == 0) {
			return n / 2;
		} else {
			return n;
		}
	}

问题1015 可被K整除的最小整数 ★★☆☆☆

https://leetcode.cn/problems/smallest-integer-divisible-by-k/

这个题目算是比较纯粹的数学题目了。题目本身不难理解,给定一个整数k,求完全由数字1组成的、能被k整除的、最小的那个整数包含的数字1的个数。

从简单直接的想法入手,我们可以由小到大挨个遍历1、11、111、…,直到遍历到能被k整数的那一个。但是这里面有两个问题:

对于第一个问题,某些语言可能能支持大数,比如Java中使用BigInteger,但这样未免过于取巧。那么应当如何避免溢出问题呢?对于题目而言,其实我们关心的是当前遍历到的完全由数字1组成的整数对k取余的结果,当取余为0时,说明我们找到了满足题目要求的整数。换句话说,我们不需要真的由小到大遍历每个完全由数字1组成的整数,而只需要维护这样的整数对k取余的结果即可。而根据数学运算,x1 % k == (x * 10 + 1) % k == (x % k) * 10 + 1

对于第二个问题,直接说答案吧,如果对于给定的k,不存在满足题目要求的整数,那么由小到大遍历完全由数字1组成的整数,并对k取余,这个余数一定是1到k-1中的一个,并且会按一定顺序挨个出现,直到重复。额,具体的数学推导我是不记得了,但这个结论还是有印象的,应该是和费马大定理有关。

另外,对于尾数是偶数或者是5的,可以直接返回,肯定不会存在满足题目要求的整数。

	public int smallestRepunitDivByK(int k) {
		if (k == 1) {
			return 1;
		}

		if (k % 2 == 0) {
			return -1;
		}

		if (k % 5 == 0) {
			return -1;
		}

		int cnt = 1;
		int val = 1;
		while (true) {
			val = val*10 + 1;
			cnt++;

			int remainder = val % k;
			if (remainder % k == 0) {
				return cnt;
			}

			if (remainder == 1) {
				break;
			}

			val = remainder;
		}

		return -1;
	}