剑指offer刷题记录(C++版本)(之四)
31.整数中1出现的次数(从1到n整数中1出现的次数)
- 题目:求出113的整数中1出现的次数,并算出1001300的整数中1出现的次数?为此他特别数了一下1~13中包含1的数字有1、10、11、12、13因此共出现6次,但是对于后面问题他就没辙了。ACMer希望你们帮帮他,并把问题更加普遍化,可以很快的求出任意非负整数区间中1出现的次数(从1 到 n 中1出现的次数)。
- 思路:以下来自牛客[Duqcuid]的分析,相当详细
像类似这样的问题,我们可以通过归纳总结来获取相关的东西。
首先可以先分类:
个位
我们知道在个位数上,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)
后面的依次类推....
再次回顾个位
我们把个位数上算1的个数的式子也纳入归纳式中
- k = n % 10
- 个位数上1的个数为:n / 10 * 1 + (if(k > 1) 1 else if(k < 1) 0 else k - 1 + 1)
完美!归纳式看起来已经很规整了。 来一个更抽象的归纳,设i为计算1所在的位数,i=1表示计算个位数的1的个数,10表示计算十位数的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)
下面是代码
#define max(a,b) (((a) > (b)) ? (a) : (b)) //牛客直接调用max,min函数存在问题
#define min(a,b) (((a) < (b)) ? (a) : (b))
class Solution {
public:
int NumberOf1Between1AndN_Solution(int n)
{
if(n <= 0)
return 0;
int count = 0;
for(long i = 1; i <= n; i *= 10){
long diviver = i * 10;
count = count+(n / diviver) * i + min (max (n % diviver - i + 1, 0), i);
}
return count;
}
};
32.把数组排成最小的数
- 题目:输入一个正整数数组,把数组里所有数字拼接起来排成一个数,打印能拼接出的所有数字中最小的一个。例如输入数组{3,32,321},则打印出这三个数字能排成的最小数字为321323。
- 思路:最简单的方法是全排列找最小值,但全排列的种数有n!个,时间太长。另外组合之后的数组可能超出int的数值范围
std::sort(first,last,cmp); 使用的范围是[first,last)
省略 cmp,使用 sort(first,last), 则默认从 小到大排序。
使用 sort(first,last, greater<T>() ), 则 从 大到小排序。
如果是结构体或者自定义排序规则,则需要自定义cmp 函数。
相等最好返回 false。
cmp函数的含义,如果返回值是 True,表示 要把 序列 (X,Y),X放Y前。
class Solution {
public:
static bool cmp(int a,int b){
string A="";
string B="";
A+=to_string(a);
A+=to_string(b);
B+=to_string(b);
B+=to_string(a);
return A<B;
}
string PrintMinNumber(vector<int> numbers) {
string answer="";
sort(numbers.begin(),numbers.end(),cmp);
for(int i=0;i<numbers.size();i++){
answer+=to_string(numbers[i]);
}
return answer;
}
};
33.丑数
- 题目:把只包含质因子2、3和5的数称作丑数(Ugly Number)。例如6、8都是丑数,但14不是,因为它包含质因子7。 习惯上我们把1当做是第一个丑数。求按从小到大的顺序的第N个丑数。
- 思路:动态规划,对于第i个数,它一定是之前已存在数的2倍,3倍或5倍
class Solution {
public:
int GetUglyNumber_Solution(int index) {
if (index < 7)return index;
vector<int> res(index);
res[0] = 1;
int t2 = 0, t3 = 0, t5 = 0, i;
for (i = 1; i < index; ++i)
{
res[i] = min(res[t2] * 2, min(res[t3] * 3, res[t5] * 5));
if (res[i] == res[t2] * 2)t2++;
if (res[i] == res[t3] * 3)t3++;
if (res[i] == res[t5] * 5)t5++;
}
return res[index - 1];
}
};
34.第一个只出现一次的字符
- 题目:在一个字符串(0<=字符串长度<=10000,全部由字母组成)中找到第一个只出现一次的字符,并返回它的位置, 如果没有则返回 -1(需要区分大小写)
- 思路:
Map是键-值对的集合,map中的所有元素都是pair,可以使用键作为下标来获取一个值。Map中所有元素都会根据元素的值自动被排序,同时拥有实值value和键值key,pair的第一元素被视为键值,第二元素被视为实值,同时map不允许两个元素有相同的键值。
在单词的第一次出现时,会在word_count中创建并插入一个以该单词为索引的新元素,同时将它的值初始化为0。然后其值立即加1,所以每次在map中添加新元素时,所统计的次数正好从1开始。需要注意的是,使用map创建的哈希表已经按键值进行了排序,所以序列的顺序已经不再是原始的输入顺序了。
class Solution {
public:
int FirstNotRepeatingChar(string str) {
map<char, int> mp;
for(int i = 0; i < str.size(); ++i)
mp[str[i]]++;
for(int i = 0; i < str.size(); ++i){
if(mp[str[i]]==1)
return i;
}
return -1;
}
};
35.数组中的逆序对???
- 题目:在数组中的两个数字,如果前面一个数字大于后面的数字,则这两个数字组成一个逆序对。输入一个数组,求出这个数组中的逆序对的总数P。并将P对1000000007取模的结果输出。 即输出P%1000000007
输入描述:
题目保证输入的数组中没有的相同的数字
数据范围:
对于%50的数据,size<=10^4
对于%75的数据,size<=10^5
对于%100的数据,size<=2*10^5
示例1
输入
1,2,3,4,5,6,7,0
输出
7
-
思路:
我们以数组{7,5,6,4}为例来分析统计逆序对的过程。每次扫描到一个数字的时候,我们不拿ta和后面的每一个数字作比较,否则时间复杂度就是O(n^2),因此我们可以考虑先比较两个相邻的数字。
(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) 中的数字是递增排序的。在把较大的数字复制到辅助数组之后,把对应的指针向前移动一位,接下来进行下一轮比较。
过程:先把数组分割成子数组,先统计出子数组内部的逆序对的数目,然后再统计出两个相邻子数组之间的逆序对的数目。在统计逆序对的过程中,还需要对数组进行排序。如果对排序算法很熟悉,我们不难发现这个过程实际上就是归并排序。
class Solution {
public:
int InversePairs(vector<int> data) {
int length=data.size();
if(length<=0)
return 0;
//vector<int> copy=new vector<int>[length];
vector<int> copy;
for(int i=0;i<length;i++)
copy.push_back(data[i]);
long long count=InversePairsCore(data,copy,0,length-1);
//delete[]copy;
return count%1000000007;
}
long long InversePairsCore(vector<int> &data,vector<int> ©,int start,int end)
{
if(start==end)
{
copy[start]=data[start];
return 0;
}
int length=(end-start)/2;
long long left=InversePairsCore(copy,data,start,start+length);
long long right=InversePairsCore(copy,data,start+length+1,end);
int i=start+length;
int j=end;
int indexcopy=end;
long long count=0;
while(i>=start&&j>=start+length+1)
{
if(data[i]>data[j])
{
copy[indexcopy--]=data[i--];
count=count+j-start-length; //count=count+j-(start+length+1)+1;
}
else
{
copy[indexcopy--]=data[j--];
}
}
for(;i>=start;i--)
copy[indexcopy--]=data[i];
for(;j>=start+length+1;j--)
copy[indexcopy--]=data[j];
return left+right+count;
}
};
36.两个链表的第一个公共结点
- 题目:输入两个链表,找出它们的第一个公共结点。
- 思路:两个单链表如果存在第一个公共结点,则后续结点一定都公共,因为结点里包含next指针,如果第一个公共结点相同,则next必然相同,所以第一个公共结点后链表合并。
找出2个链表的长度,然后让长的先走两个链表的长度差,然后再一起走
(因为2个链表用公共的尾部)
class Solution {
public:
ListNode* FindFirstCommonNode( ListNode* pHead1, ListNode* pHead2) {
int len1 = findListLenth(pHead1);
int len2 = findListLenth(pHead2);
if(len1 > len2){
pHead1 = walkStep(pHead1,len1 - len2);
}else{
pHead2 = walkStep(pHead2,len2 - len1);
}
while(pHead1 != NULL){
if(pHead1 == pHead2) return pHead1;
pHead1 = pHead1->next;
pHead2 = pHead2->next;
}
return NULL;
}
int findListLenth(ListNode *pHead1){
if(pHead1 == NULL) return 0;
int sum = 1;
while(pHead1 = pHead1->next) sum++;
return sum;
}
ListNode* walkStep(ListNode *pHead1, int step){
while(step--){
pHead1 = pHead1->next;
}
return pHead1;
}
};
37.数字在排序数组中出现的次数
-
题目:统计一个数字在排序数组中出现的次数。
例如输入排序数组{1,2,3,3,3,3,4,5}和数字3,由于3在数组中出现了4才,因此输出4。 - 思路:使用二分法找到排序数组中所求数字的第一个和最后一个位置,即可知道所求数字的个数。
因为data中都是整数,所以可以稍微变一下,不是搜索k的两个位置,而是搜索k-0.5和k+0.5
这两个数应该插入的位置,然后相减即可。
class Solution {
public:
int GetNumberOfK(vector<int> data ,int k) {
return biSearch(data, k+0.5) - biSearch(data, k-0.5) ;
}
private:
int biSearch(const vector<int> & data, double num){
int s = 0, e = data.size()-1;
while(s <= e){
int mid = (e - s)/2 + s;
if(data[mid] < num)
s = mid + 1;
else if(data[mid] > num)
e = mid - 1;
}
return s;
}
};
38.二叉树的深度
- 题目:输入一棵二叉树,求该树的深度。从根结点到叶结点依次经过的结点(含根、叶结点)形成树的一条路径,最长路径的长度为树的深度。
- 思路:递归判断当前是否为深度最大值(递归NB)
class Solution {
public:
int TreeDepth(TreeNode* pRoot)
{
if(!pRoot) return 0 ;
return max(1+TreeDepth(pRoot->left), 1+TreeDepth(pRoot->right));
}
};
39.平衡二叉树
- 题目:输入一棵二叉树,判断该二叉树是否是平衡二叉树。
- 知识点:平衡二叉树(Balanced Binary Tree)具有以下性质:它是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。
-
思路:同上题思路,依然可以使用递归判断,加上子树深度差小于1这个条件即可。
这种做法有很明显的问题,在判断上层结点的时候,会多次重复遍历下层结点,增加了不必要的开销。如果改为从下往上遍历,如果子树是平衡二叉树,则返回子树的高度;如果发现子树不是平衡二叉树,则直接停止遍历,这样至多只对每个结点访问一次。
class Solution {
public:
bool IsBalanced(TreeNode *root, int & dep){
if(root == NULL){
return true;
}
int left = 0;
int right = 0;
if(IsBalanced(root->left,left) && IsBalanced(root->right, right)){
int dif = left - right;
if(dif<-1 || dif >1)
return false;
dep = (left > right ? left : right) + 1;
return true;
}
return false;
}
bool IsBalanced_Solution(TreeNode* pRoot) {
int dep = 0;
return IsBalanced(pRoot, dep);
}
};
40.数组中只出现一次的数字
- 题目:一个整型数组里除了两个数字之外,其他的数字都出现了两次。请写程序找出这两个只出现一次的数字。
- 思路:
此题考察的是异或运算的特点:即两个相同的数异或结果为0。
此题用了两次异或运算特点:
(1)第一次使用异或运算,得到了两个只出现一次的数相异或的结果。
(2)因为两个只出现一次的数肯定不同,即他们的异或结果一定不为0,一定有一个位上有1。另外一个此位上没有1,我们可以根据此位上是否有1,将整个数组重新划分成两部分,一部分此位上一定有1,另一部分此位上一定没有1,然后分别对每部分求异或,因为划分后的两部分有这样的特点:其他数都出现两次,只有一个数只出现一次。因此,我们又可以运用异或运算,分别得到两部分只出现一次的数。
异或:
class Solution {
public:
void FindNumsAppearOnce(vector<int> data,int* num1,int *num2) {
int diff=accumulate(data.begin(),data.end(),0,bit_xor<int>());
diff&=-diff; //即找到最右边1-bit
*num1=0;*num2=0;
for (auto c:data) {
if ((c&diff)==0) *num1^=c;
else *num2^=c;
}
}
};
哈希表:
class Solution {
public:
void FindNumsAppearOnce(vector<int> data, int* num1, int *num2) {
unordered_map<int, int> map;
for (int i = 0; i < data.size(); i++)
map[data[i]]++;
vector<int>v;
for (int i = 0; i < data.size(); i++)
if (map[data[i]]== 1)
v.push_back(data[i]);
*num1 = v[0]; *num2 = v[1];
}
};