转载自FlushHip http://blog.csdn.net/flushhip/article/details/79039260
原题链接:
- 第一题:整数中1出现的次数;
- 第二题:把数组排成最小的数;
- 第三题:丑数;
- 第四题:第一个只出现一次的字符位置;
- 第五题:数组中的逆序对;
- 第六题:两个链表的第一个公共结点;
第一题:整数中1出现的次数
题目:
求出
1~13
的整数中1
出现的次数,并算出100~1300
的整数中1
出现的次数?为此他特别数了一下1~13
中包含1
的数字有1、10、11、12、13
因此共出现6
次,但是对于后面问题他就没辙了。ACMer希望你们帮帮他,并把问题更加普遍化,可以很快的求出任意非负整数区间中1
出现的次数。
解析:
这题暴力也能过,数据不是很强,不过这里不介绍暴力,介绍一下数学方法,像这种区间 f(L,R) f ( L , R ) 问题,我们一般利用区间的可加减性转化为 f(1,R)−f(1,L−1) f ( 1 , R ) − f ( 1 , L − 1 ) 来做,这么做可以简化问题;
那么现在问题就转化为了如何求 f(1,x) f ( 1 , x ) 了,但是这个题没有给我们
[L, R]
区间,直接让我们求[1, n]
,这样问题反而简化了。从数位上来分析:
x
可以表示成abcdef....
,这里假设x
可以表示成abcdef
,x
是个6位数,那么我们考虑每一位对答案的贡献,由于每一位独立,因此可以单独计算每一位上的1,然后全部加起来,这就是答案;那么每一位上的1出现的次数该怎么搞呢?这就要分情况讨论了,假设我们现在讨论
c
这一位:
c == 0
,也就是说ab
从0
到ab - 1
变化,cdef
从0000
到9999
变化,c
这一位上都可以出现1
,利用乘法原理,那么对答案的贡献就是(ab - 1 - 0 + 1) * (9999 - 0000 + 1) = ab * 10000
;c == 1
,和上一项c == 0
对答案的贡献一样,只不过要在这个贡献上再加上一点额外的贡献,当ab = ab
时,c = 1
,那么cdef
就只能从0000
到cdef
变化了,故要在原贡献的基础上加上cdef - 0000 + 1
即ab * 10000 + cedf + 1
;c > 1
,和第一项c == 0
对答案的贡献一样,也要加上一点额外的贡献,当ab = ab
时,c = 1
,那么cdef
就能从0000
到9999
变化了,故要在原贡献的基础上加上9999- 0000 + 1
即ab * 10000 + 10000
。通过对
c
这一位分析,我相信你可以看出来公式了,没错,就是这么做的,具体看下代码。
class Solution {
public:
int NumberOf1Between1AndN_Solution(int n)
{
int ret = 0;
for (int power = 1, high = n / 10, low = 0, cur = n % 10; n / power;
ret += high * power + (cur == 0 ? 0 : cur == 1 ? low + 1 : power),
power *= 10, high = n / power / 10, cur = n / power % 10, low = n % power);
return ret;
}
};
这一题的代码还可以这样写,讨论区中很多人是这么写的,不过原理和我上面分析的是一样的,只不过代码实现的形式不一样而已,我先给出代码:
class Solution {
public:
int NumberOf1Between1AndN_Solution(int n)
{
int ret = 0;
for (int i = 1; i <= n; ret += (n / i + 8) / 10 * i + (n / i % 10 == 1) * (n % i + 1), i *= 10);
return ret;
}
};
代码的关键是为什么要加
8
,你自己像上面分类讨论下就知道了,举个栗子吧,n = 205, 215, 225
,现在求十位上1的个数,n / i = 20, 21, 22
,十位上的数字分别是0, 1, 2
,(n / i + 8) / 10 = 28 / 10, 29 / 10, 30 / 10 = 2, 2, 3
,这时候你再看看当十位上是2
时,贡献为3 * 10 = 2 * 10 + 10
这就是上面第三种情况,其余的情况依次类推就好了。
第二题:把数组排成最小的数
题目:
输入一个正整数数组,把数组里所有数字拼接起来排成一个数,打印能拼接出的所有数字中最小的一个。例如输入数组
{3,32,321}
,则打印出这三个数字能排成的最小数字为321323
。
解析:
这题一开始我也是懵的,似乎考虑了大小还要考虑长度,然后两个搞在一起后就无法做了,但是,如果我们限定长度,然后考虑字典序,这样搞就很好理解了;
如何限定长度呢,这个题无非就是数组中的某些元素放前面和放后面的问题,说白了就是排序,如果自己使用相邻元素的字典序排序,答案是不对的,这也没有限制长度,但是,如果把前后两个字符串拼接起来,利用不同拼接顺序生成的字符串的字典序排序就对了;
不同的拼接顺序,不会改变生成字符串的长度,这样就限定了长度这个条件,然后通过字典序来生成最小的排序序列,最后把数组中的字符串顺序拼接起来就行了,排序的方法你可以任选,但是规则要自己制定。
class Solution {
public:
string PrintMinNumber(vector<int> numbers) {
vector<string> arr(numbers.size());
stringstream sin;
for (int i = 0; i < (int)numbers.size(); sin << numbers[i], sin >> arr[i], sin.clear(), ++i);
sort(arr.begin(), arr.end(), cmp);
string ret = "";
for (auto it = arr.begin(); it != arr.end(); ret += *it++);
return ret;
}
static bool cmp(const string &A, const string &B)
{
return A + B < B + A;
}
};
第三题:丑数
题目:
把只包含因子
2、3
和5
的数称作丑数(Ugly Number)。例如6、8
都是丑数,但14
不是,因为它包含因子7
。 习惯上我们把1
当做是第一个丑数。求按从小到大的顺序的第N
个丑数。
解析:
思想就是用已知的丑数去生成丑数,然后把丑数排序就好了。先来一发毫无技巧的代码:生成第
i
个丑数时,都从第一个丑数往后扫描,分别用2, 3, 5
去乘,如果结果大于arr[i - 1]
,就维护一个最小值,扫描完后,这个最小值就是第i
个丑数。时间复杂度: O(n2) O ( n 2 ) 。
class Solution {
public:
int GetUglyNumber_Solution(int index) {
if (index == 0)
return 0;
vector<int> arr(index, 0x3f3f3f3f);
arr[0] = 1;
for (int i = 1; i < index; i++) {
for (int j = 0; j < i; j++) {
if (2 * arr[j] > arr[i - 1])
arr[i] = min(arr[i], 2 * arr[j]);
if (3 * arr[j] > arr[i - 1])
arr[i] = min(arr[i], 3 * arr[j]);
if (5 * arr[j] > arr[i - 1])
arr[i] = min(arr[i], 5 * arr[j]);
}
}
return arr[index - 1];
}
};
那有没有什么地方是可以优化的呢,其实是有的,不知你注意到了没,我们要寻找一个数乘以
2, 3, 5
大于arr[i - 1]
且这个数最小,这个操作是不需要每次都从数组头开始找的,而且也不要每次都找到i - 1
处。现在把问题简化一下,只考虑用2
去乘,那么我们总可以在数组中找到一个位置pos
,这个位置上的数乘2
刚好大于arr[i - 1]
,往后的数字就不需要去计算了,因为越往后数字越大,乘起来的结果也就越大,故不满足该数最小这个条件,因此pos
处的数乘2
就是arr[i]
,现在把pos
记录下来,下一次扫描的时候就要求arr[i + 1]
了,这时候就要寻找一个数乘以2
大于arr[i]
且这个数最小,而arr[pos] * 2 = arr[i]
,所以满足条件的当然就是arr[i + 1] = arr[pos + 1] * 2
,这样子查找的时间复杂度就降到了 O(1) O ( 1 ) ;现在再把那个简化的问题恢复为原问题,用
2, 3, 5
去乘,那么不是就维护三个pos
分别对应2, 3, 5
就行了吗,每次都去比较这三个位置乘出来的结果,取最小的就行了,然后用这个最小的结果取和这三个位置乘出来的结果比较,如果相等就往后移动对应的pos
。时间复杂度: O(n) O ( n ) 。
class Solution {
public:
int GetUglyNumber_Solution(int index) {
if (index == 0)
return 0;
vector<int> arr(index, 1), ptr(3, 0), num({2, 3, 5});
for (int cnt = 1, min_value; cnt < index; arr[cnt++] = min_value) {
vector<int> result(3);
for (int i = 0; i < 3; result[i] = arr[ptr[i]] * num[i], ++i);
min_value = *min_element(result.begin(), result.end());
for (int i = 0; i < 3; ptr[i] += min_value == result[i], ++i);
}
return arr[index - 1];
}
};
第四题:第一个只出现一次的字符位置
题目:
在一个字符串(
1<=字符串长度<=10000
,全部由字母组成)中找到第一个只出现一次的字符,并返回它的位置
解析:
先用
map
把对应字符和出现的次数映射起来,然后从头开始查找,找到第一个只出现一次的字符返回就好了。
class Solution {
public:
int FirstNotRepeatingChar(string str) {
map<char, int> mp;
for (auto it = str.begin(); it != str.end(); mp[*it++]++);
for (auto it = str.begin(); it != str.end(); it++)
if (mp[*it] == 1)
return it - str.begin();
return -1;
}
};
第五题:数组中的逆序对
题目:
在数组中的两个数字,如果前面一个数字大于后面的数字,则这两个数字组成一个逆序对。输入一个数组,求出这个数组中的逆序对的总数
P
。并将P
对1000000007
取模的结果输出。 即输出P%1000000007
。
解析:
这题的做法详细做法在我另一篇博客序列逆序对问题详解有介绍。
第六题:两个链表的第一个公共结点
题目:
输入两个链表,找出它们的第一个公共结点。
解析:
首先,最直接的想法就是把第一个链表的所有结点都加到
set
中,然后遍历第二个链表的结点,如果发现在set
中能找到,那么这个结点就是链表的第一个公共结点。时间复杂度: O(nlogn) O ( n l o g n ) 。
/*
struct ListNode {
int val;
struct ListNode *next;
ListNode(int x) :
val(x), next(NULL) {
}
};*/
class Solution {
public:
ListNode* FindFirstCommonNode( ListNode* pHead1, ListNode* pHead2) {
set<ListNode *> se;
for (auto it = pHead1; it != nullptr; se.insert(it), it = it->next);
for (auto it = pHead2; it != nullptr; it = it->next)
if (se.find(it) != se.end())
return it;
return nullptr;
}
};
假设两个链表一样长,那么从链表头向后以相同的速度走,那么一定可以相遇,相遇点就是两个链表的第一个公共结点,现在问题是两个链表的长度一般都不一样,我们可以让较长的那个链表先走一段距离,这个距离是多少呢,就是两个链表长度的差值,走完之后,两个链表当前的起点到结尾的距离就一样了。
时间复杂度: O(n) O ( n ) 。
/*
struct ListNode {
int val;
struct ListNode *next;
ListNode(int x) :
val(x), next(NULL) {
}
};*/
class Solution {
public:
ListNode* FindFirstCommonNode( ListNode* pHead1, ListNode* pHead2) {
int len1 = 0, len2 = 0;
for (auto it = pHead1; it != nullptr; ++len1, it = it->next);
for (auto it = pHead2; it != nullptr; ++len2, it = it->next);
auto it1 = len1 > len2 ? pHead1 : pHead2;
auto it2 = len1 > len2 ? pHead2 : pHead1;
for (int i = 0; i < abs(len1 - len2); i++, it1 = it1->next);
for ( ; it1 != nullptr; it1 = it1->next, it2 = it2->next)
if (it1 == it2)
return it1;
return nullptr;
}
};
其实 O(n) O ( n ) 的算法已经很好了,但是为什么还会有下面的这个代码呢,原因是它代码简短啊,而且讨论区中就有人是这么实现的,而且大家好像对这个代码不是很理解,所以我在这里就细讲一下,这个代码其实就是两个链表以相同的速度跑圈,而且要注意,这里面把链表尾的空指针也作为一个结点(为了处理没有公共结点的情况),但是为什么这样做,两个链表一定就会相遇,且相遇点一定就是第一个公共结点呢?具体解释看下图和后面的解释:
我们先看上面这两个链表,长度分别为
len1 = 6, len2 = 4
,由于速度一样,相遇时经历的时间一样,故两个链表相遇时走的路程一样,假设相遇时第一个链表走了 circlex c i r c l e x 圈(当然它的路程还要多一点),第二个链表走了 circley c i r c l e y 圈,那么可以建立下列等式circlex∗len1+len1−k=circley∗len2+len2−k c i r c l e x ∗ l e n 1 + l e n 1 − k = c i r c l e y ∗ l e n 2 + l e n 2 − k
整理可得circlex+1circley+1=len2len1 c i r c l e x + 1 c i r c l e y + 1 = l e n 2 l e n 1
这也就是说,相遇时它两走的圈数加一和它们本身的长度成正比,因此可以根据 circlex,circley c i r c l e x , c i r c l e y 均是非负数且第一次相遇它们走的圈数一定最少,可以把 circlex,circley c i r c l e x , c i r c l e y 解出来,那么是否能把 k k 也解出来呢?答案是不能的,因为再也找不到等式来限制了,为此我特意做了下面这两个链表,可以发现down
和up
明显不一样,但是len1, len2
是一样的,所以 circlex,circley c i r c l e x , c i r c l e y 也是一样的, k k 是不相同的,故要在代码中自己测一下;说到这里还没有解释相遇点为什么就是第一次公共结点,其实你仔细想想就很简单了,假设相遇点在
k
之前结点d
之前,这不可能啊,那时候两个链表根本不可能相遇;假设相遇点在d
之后,这也不可能啊,既然在d
之后相遇,那么在d
点也会相遇,所以啊,相遇点一定就是第一次公共结点。通过上面的分析,我们证明了下面这个代码的正确性,不过时间复杂度取决于两个链表长度的最小公倍数。
时间复杂度: O(len1∗len2gcd(len1∗len2)) O ( l e n 1 ∗ l e n 2 g c d ( l e n 1 ∗ l e n 2 ) ) 。
/*
struct ListNode {
int val;
struct ListNode *next;
ListNode(int x) :
val(x), next(NULL) {
}
};*/
class Solution {
public:
ListNode* FindFirstCommonNode( ListNode* pHead1, ListNode* pHead2) {
auto it1 = pHead1, it2 = pHead2;
for (; !(it1 == it2); ) {
it1 = it1 == nullptr ? pHead1 : it1->next;
it2 = it2 == nullptr ? pHead2 : it2->next;
}
return it1;
}
};