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;
}