剑指OfferC++_31-40

目录

31、整数中1出现的次数(从1到n整数中1出现的次数)

32、把数组排成最小的数

33、丑数

34、第一个只出现一次的字符

 35、数组中的逆序对

31-35知识点总结:

36、两个链表的第一个公共结点

   37、数字在排序数组中出现的次数

38、二叉树的深度

39、平衡二叉树

40、数组中只出现一次的数字


31、整数中1出现的次数(从1到n整数中1出现的次数)

很快的求出任意非负整数区间中1出现的次数(从1 到 n 中1出现的次数)。

解题思路:

使用归纳的方法:
对于个位数,我们可以发现,每10个数就会在各位出现一个1.如0-22里所有的数,只有1,11,21三个数的个位有1.所以对于22以内的数,,个位出现1一共3次。计算方法可以为: n/10 * 1+(n%10!=0 ? 1 : 0)
对于十位数,我们发现,每100个数,出现10次,10-19.
设k = n % 100,即为不完整阶梯段的数字   归纳式为:(n / 100) * 10 + (if(k > 19) 10 else if(k < 10) 0 else k - 10 + 1)  每100个数有10个数在十位上有1.余数如果大于19.那么又10个数,否则是k-10+1个(如:17-19+1=18)
对于百位数同理 设k = n % 1000 归纳式为:(n / 1000) * 100 + (if(k >199) 100 else if(k < 100) 0 else k - 100 + 1
对于i位,我们设定i=1/10/100  设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) ,将count求和就是结果sum1 = sum(count(i)),i = Math.pow(10, j), 0<=j<=log10(n)
把后半段简化,我们不去计算i * 2 - 1了,我们只需保证k - i + 1在[0, i]区间内就行了,最后后半段可以写成这样  min(max((n mod (i*10))−i+1,0),i)

int NumberOf1Between1AndN_Solution(int n)
    { int sum=0;
        for(int i=1;i<=n;i=10*i)  //  0<=j<=log10(n)
            sum+=(n/(i*10))*i+min(max(0,(n%(i*10))-i+1),i);
        return sum;
    }

32、把数组排成最小的数

输入一个正整数数组,把数组里所有数字拼接起来排成一个数,打印能拼接出的所有数字中最小的一个。例如输入数组{3,32,321},则打印出这三个数字能排成的最小数字为321323。

解题思路:明显是重载的题,难点在于如何重载,很可能觉得是return a<b 但不是 应该 return a+b<b+a

static bool cmp(int a,int b)//注意!一定要使用static变量!!!!!
{   string aa,bb;
    aa=to_string(a);
    bb=to_string(b);
    return (aa+bb)<(bb+aa);
}

string PrintMinNumber(vector<int> numbers) {
    string jj="";
    sort(numbers.begin(),numbers.end(),cmp);
    for(int i=0;i<numbers.size();i++)
        jj+=to_string(numbers[i]);
    return jj;
    }

33、丑数

把只包含质因子2、3和5的数称作丑数(Ugly Number)。例如6、8都是丑数,但14不是,因为它包含质因子7。 习惯上我们把1当做是第一个丑数。求按从小到大的顺序的第N个丑数。

解题思路:

一个丑数一定由另一个丑数乘以2或者乘以3或者乘以5得到。那么我们从1开始乘以2,3,5,就得到2,3,5三个丑数,在从这三个丑数出发乘以2,3,5就得到4,6,10,6,9,15,10,15,25九个丑数,我们发现这种方法会得到重复的丑数,而且我们题目要求第N个丑数,这样的方法得到的丑数也是无序的。那么我们可以维护4个队列,其中3个序列是第一个序列的子序列,用索引来表示:
一个队列存当前的丑数序列;另外三序列存的是分别是2的倍数的丑数,3的倍数的丑数,5的倍数的丑数。这三个序列之间存在重复。但是三个序列单独是递增的。x,y,z相当于存的是在2/3/5队列里,现在第N个丑数是乘了几个。
而要判断当前ret[i]是多少,其实就是找3个队列里的最小值。而最小值,其实就是ret[x]*2因为上一个从2这里面拿走的是ret[x],而再从2这里面拿走一定是ret[x+1]=ret[x]*2.而当前ret[i]*2也在2这个队列里,可是不是最小的。也就是序列1是递增顺序的。每次Push进序列1里的数我们会把他的2/3/5倍数分别放进对应序列里。

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放入三个队列;

    int GetUglyNumber_Solution(int index) {
        if(index<7)  return index; //1-6都是丑数
        vector<int> res;
        int x=0,y=0,z=0;
        res.push_back(1);
        for(int i=1;i<index;i++)
        {
            res.push_back(min(res[x]*2,min(res[y]*3,res[z]*5)));
            if(res[i]==res[x]*2) x++;
            if(res[i]==res[y]*3) y++;
            if(res[i]==res[z]*5) z++;
//这三行是为了防止重复。这个数可能同时是,某另一个数的另一个倍数,如果不++,会重复。并且一定是下一个就是这个重复的数的时候才++
        }
        return res[index-1];
    }

34、第一个只出现一次的字符

在一个字符串中找到第一个只出现一次的字符,并返回它的位置, 如果没有则返回 -1(需要区分大小写)

考查:map内部排序的,所以要记录进来的顺序。对于map里的键值,会自动赋初值0;
LinkedHashMap  java里有 是一个根据某种规则有序的hashmap。根据名字,这个集合是有hash散列的功能的同时也有顺序。
hashmap是无法根据某种顺序来访问数据的,例如放入集合的元素先后的顺序。list都有这个功能,可以根据放入集合的先后来访问具体的数据。C++没有这样的数据结构。他是两种结构的组合。一种是我们熟悉的hashmap。另外一种就是链表。数据存入集合的时候,先根据hashmap的流程存放入数组中。然后再根据链表的原则,进行链接。


int FirstNotRepeatingChar(string str) {
    map<char , int> p;
    for(int i=0;i<str.length();i++) p[str[i]]++;
//因为map是以红黑树实现的,map后,不能以map的迭代器遍历,必需用str[i]来遍历。
    for(int i=0;i<str.length();i++)
        if(p[str[i]]==1)   return i;
     return -1;
    }

 35、数组中的逆序对

在数组中的两个数字,如果前面一个数字大于后面的数字,则这两个数字组成一个逆序对。输入一个数组,求出这个数组中的逆序对的总数P。并将P对1000000007取模的结果输出。 即输出P%1000000007

考查:大数的取余,归并排序

其实就是看这个数字后面有几个数比自己小。相当于排序。排序后自己的位置比现在的位置往后移了几个就是有几个比自己小的
归并排序的方法:
我们以数组{7,5,6,4}为例来分析统计逆序对的过程。
每次扫描到一个数字的时候,我们不拿ta和后面的每一个数字作比较,否则时间复杂度就是O(n^2),因此我们可以考虑先比较两个相邻的数字。
(a) 把长度为4的数组分解成两个长度为2的子数组;
(b) 把长度为2的数组分解成两个成都为1的子数组;
(c) 把长度为1的子数组  合并、排序并统计逆序对  ;       分别得到 7/5  6/4
(d) 把长度为2的子数组合并、排序,并统计逆序对;         得到5,7 /4,6 并且得到两个逆序树。
具体:在第一对长度为1的子数组{7}、{5}中7大于5,因此(7,5)组成一个逆序对。同样在第二对长度为1的子数组{6}、{4}中也有逆序对(6,4)。之后对其进行排序。
(e)我们统计两个长度为2的子数组子数组之间的逆序对,并且合并子数组。
我们先用两个指针分别指向两个子数组的末尾,并每次比较两个指针指向的数字。
如果第一个子数组中的数字大于第二个数组中的数字,则构成逆序对,并且逆序对的数目等于第二个子数组中剩余数字的个数。
如果第一个数组的数字小于或等于第二个数组中的数字,则不构成逆序对。
每一次比较的时候,我们都把较大的数字从后面往前复制到一个辅助数组中,确保 辅助数组(记为copy) 中的数字是递增排序的。
在把较大的数字复制到辅助数组之后,把对应的指针向前移动一位,接下来进行下一轮比较。
也就是对于5 7 / 4 6,指针分别指向7和6.先比较7>6 构成逆序列,逆序列个数为右边序列个数。则+2.并且把比较后的大的值7放入copy队列
之后指针指向5和6.5<6 不构成逆序列,将大的数6放进copy队列。之后指向5和4.5>4,逆序列+1.并把5放进copy队列。之后指向NULL和4.此时逆序列个数加上4左边的个数,这里为0.并且将4左边的所有数放入copy序列中
这就得到下一个合并结果。并且得到逆序列个数为5

int bian(vector<int>& kk,vector<int>& copyy,int start,int endd)
{
    if(start==endd)
    {
        copyy[start]=kk[start];
        return 0;
    }
    int  mid=(endd-start)/2;
    int endd1=start+mid;
    int endd2=endd;
    int now=endd;
    int l=bian(kk,copyy,start,endd1);//先遍历到最小,只会开始归并。所以先拆再合
    int r=bian(kk,copyy,endd1+1,endd2);
    int cou=0;
    while(endd1>=start && endd2>=(start+mid+1))//开始合。合的结果存在copyy里
    {
        if(kk[endd1]>kk[endd2]) {
            copyy[now--]=kk[endd1--];
            cou+=endd2-start-mid;//代表endd2前面所有的都小,所以cou加上endd2前面的个数
            if(cou>=1000000007)
                cou%=1000000007;
        }
        else copyy[now--]=kk[endd2--];
    }
    while(endd1>=start)
        copyy[now--]=kk[endd1--];
    while(endd2>=(start+mid+1))
        copyy[now--]=kk[endd2--];
    for(int i=start;i<=endd;i++)
        kk[i]=copyy[i];
    return (l+r+cou)%1000000007;
}

int InversePairs(vector<int> kk)
{
    if(kk.size()==0||kk.size()==1) return 0;
    vector<int> copyy(kk.size());
    return bian(kk,copyy,0,kk.size()-1);
}

31-35知识点总结:

1、vector<int> k(10); 开辟10个int的空间

vector清空的方法:
a.clear();  和 循环a.erase()
vector <int>::iterator iter=vecInt.begin();
    for ( ;iter!=vecInt.end();)
    {
        iter=vecInt.erase(iter);
    }
但是这两种方法都不清除空间,仅仅删除内容
j= vecInt.capacity();      //j=512
i = vecInt.size();         //i=0
使用swap来清空  vector <int>().swap(vecInt);

36、两个链表的第一个公共结点

题目:输入两个链表,找出它们的第一个公共结点。

理解:

公共节点的意思是两个链表。里面某一个节点是两个链表共有的。意思是两个链表中第一个在同一个地址上的点。而因为这个点是同一个地址,那么他们后面的next一定就一样了。所以从公共点开始两个链表后面一定一样。

所以在两个链表长度相同的情况下。两个链表的结点分别后移,如果指针相等,就是公共链表。!一定要注意在两个链表长度不一样的情况下。遍历是0(MN),直接后移不行。所以要找到短的那个链表的长度x。之后长的从倒数x位置开始一起后移比较。
方法1:对于两个链表,先找到两者分别的长度。如果不等。长的那个从后往前变为相等。之后对比两个长度相等的链表。如果都到NULL了,就说明没有
方法2:类似,但是不需要求长度。p1和p2分别指向链表的头结点。如果两个链表长度不等。那么短的那个先到终点。先等于NULL,这个时候他的指针首先变成另一个链表的头结点。这个时候两个结点的位置差刚好是短链表的长度。
而长的链表往后走到NULL的时候,原来的短链表正好走到与真正的短链表相差得位置上。这个时候原来得长链表得指针指向短链表得头。此时。两个指针分别指向短链表得头和长链表与短链表相同长度得位置上。就可以找公共子节点了。

 ListNode* FindFirstCommonNode( ListNode* pHead1, ListNode* pHead2) {
        if(pHead1==NULL || pHead2==NULL)    return NULL;
        ListNode* p1=pHead1;
        ListNode* p2=pHead2;
//假设pHead2长度为(x1+x2),pHead1长度为(x2)短的那个链表先到NULL之后等于另一个链表的头假如是p1=pHead2
//此时另一个链表所在位置与该链表差的就是短链表的长度及x2。之后当p2也为NULL之后等于pHead1。此时对于pHead2所在链表上p1节点距离末尾就是x2长度。这个时候,遍历的时候两个链表的长度就相同了。
        while(p1!=p2)
        {
            p1=(p1==NULL?pHead2:p1->next);
            p2=(p2==NULL?pHead1:p2->next);
        }
        return p1;
    }

   37、数字在排序数组中出现的次数

题目描述:统计一个数字在排序数组中出现的次数。
题目理解:一共有序数组找一个数字,首先想到的就是二分法。
方法1. 看如果要插入这个数-0.5得位置p1。和要插入这个数+0.5得位置p2。p2-p1就是最终得结果
方法2:找第一个数的位置和最后一个数的位置

int erfen(vector<int> data,double k)
//如果插进来的是3.5那么b,e可能都指向3.这个时候3<3.5,b=4, e=3。 b,e也可能都指向4.这个时候4>3.5,所以e=3。所以无论怎样都是插在b的位置,就是在原来的第4个位置处放上3.5。原来第4个位置的数依次后移
{
    int b=0,e=data.size()-1;
    while(b<=e)
    {//b-1那个位置的数永远小于插进来的数。而e+1那个位置的数大于等于插进来的数。所以返回的b是这个数第一次出现的位置
        if(data[b+(e-b)/2]<k)   b=b+(e-b)/2+1;
        else                    e=b+(e-b)/2-1;
    }
    return b;
}

int erfen_up(vector<int> data,double k)
//如果插进来的是3.5那么b,e可能都指向3.这个时候3<3.5,b=4, e=3。 b,e也可能都指向4.这个时候4>3.5,所以e=3。所以无论怎样都是插在b的位置,就是在原来的第4个位置处放上3.5。原来第4个位置的数依次后移。
{   int b=0,e=data.size()-1;
    while(b<=e)
    {
//e+1的位置的数永远大于插进来的数,所以返回的e永远是最后一次出现的位置
        if(data[b+(e-b)/2]<=k) b=b+(e-b)/2+1;
        else                   e=b+(e-b)/2-1;
    }
    return e;
}

int GetNumberOfK_2(vector<int> data ,int k) 
{ return erfen_up(data,k)-erfen(data,k)+1; }

int GetNumberOfK_1(vector<int> data ,int k)
 { return erfen(data,k+0.5)-erfen(data,k-0.5); }

38、二叉树的深度

 int TreeDepth(TreeNode* pRoot)
    {
        if(pRoot==NULL) return 0;
        int depthl=TreeDepth(pRoot->left);
        int depth2=TreeDepth(pRoot->right);
        return depth2>depthl?depth2+1:depthl+1;
    }

39、平衡二叉树

left是左子树的深度,right是右子树的深度。如果有一个返回的深度是-1,那么直接返回不再遍历,因为肯定不是平衡二叉树。否则,看两个深度差是不是小于1.大于1,返回-1.否则返回的是两个深度中大的那个+1,作为这个根节点引出的子树的深度

  int getDepth(TreeNode* root) {
        if (root == NULL) return 0;
         int left = getDepth(root->left);
         if (left == -1) return -1; //一旦发现左右子树高度差不满足平衡二叉树,则返回-1.之后就不再看高度了
         int right = getDepth(root->right);
         if (right == -1) return -1;
         return abs(left - right) > 1 ? -1 : 1 + max(left, right);  }
 bool IsBalanced_Solution(TreeNode* root) {
    return getDepth(root) != -1; }

40、数组中只出现一次的数字

一个整型数组里除了两个数字之外,其他的数字都出现了两次。请写程序找出这两个只出现一次的数字。

很像零件配套的题。两两配套,有一套不配套。找出这两个异常点

异或a⊕b = (¬a ∧ b) ∨ (a ∧¬b)的性质:相同为假,不同为真   1^1=0 0^0=0  1^0=1 0^1=1  ,一共一个数与0异或得到的是本身 ,与1异或,奇数减一偶数加一
   int a=2,b=5;  //a和b不通过第三个变量来交换值,使用异或
a=a^b;
b=a^b;  //b=a^b^b b^b=0 b=a^0=a
a=a^b;  //a=a^b^a=b
cout<<a<<b;
a=a>>1是除2,注意一定要赋值!移位之后本身不变
所以对于本题来说:我们可以用异或,如果两个数相同,那么他们异或得到的是0.所以最终将所有数都做一次异或,得到的结果就是这两个数异或的结果。
而这个异或的结果不为0,肯定有一位一个数是1,一个数不是1.找到这位就看异或的结果,哪位是1
找到这位的方法,可以用1与 之后1<<1;
找到那位我们按照第n位是不是1对其进行分类。两类再次做异或得到的结果分别就是这两个数的和。而看第n位是不是1,可以看与0010000做与运算,得到的结果是不是0.

void FindNumsAppearOnce(vector<int> data,int* num1,int *num2) {
    int y=0;
    for(int i=0;i<data.size();i++) y^=data[i];//y是两个数异或得结果
    int u=1;
    while(1)
    {
        if((y&u)!=0)   break;
        u=u<<1;  //注意一定要赋值!不同数位里一定有1不一样的。u是找到第一个一个数为1,一个为0的那位。
    }
    vector<int> d1,d2;//根据不同的那位找到这些数中那位是1的一组数。和不为1的一组数
    for(int i=0;i<data.size();i++)
    {
      if((data[i]&u)!=0) d1.push_back(data[i]);
      if((data[i]&u)==0) d2.push_back(data[i]);
    }//因为除了这两个数其余都一定有两个,所以再次异或其余数就没了
    for(int i=0;i<d1.size();i++)  *num1^=d1[i];
    for(int i=0;i<d2.size();i++)  *num2^=d2[i];
    }
#唯一一个数字的python的神奇实现
import numpy as np
a=[1,2,3,4,5,4,3,2,1]
(np.sum(a)-2*np.sum(list(set(list(a)))))*-1 #方法1:使用set,之后不能直接减,需要求和
from functools import reduce
reduce(lambda c,b:c^b , a) #方法2:使用reduce,将a看作一个迭代器,reduce将按照func对参数序列中元素进行累积。
import operator
reduce(operator.xor, a)  #方法2:对于异或操作,Python里有内置运算符operator.xor,所以不用自己写lambda

知识点!!

异或a⊕b = (¬a ∧ b) ∨ (a ∧¬b)的性质:相同为假,不同为真   1^1=0 0^0=0  1^0=1 0^1=1  ,一共一个数与0异或得到的是本身 ,与1异或,奇数减一偶数加一
   int a=2,b=5;  //a和b不通过第三个变量来交换值,使用异或
a=a^b;
b=a^b;  //b=a^b^b b^b=0 b=a^0=a
a=a^b;  //a=a^b^a=b
cout<<a<<b;
a=a>>1是除2,注意一定要赋值!移位之后本身不变

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值