美文网首页
剑指offer刷题记录(C++版本)(之四)

剑指offer刷题记录(C++版本)(之四)

作者: 傑jay | 来源:发表于2019-09-27 22:02 被阅读0次

    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> &copy,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];
        }
    };
    

    相关文章

      网友评论

          本文标题:剑指offer刷题记录(C++版本)(之四)

          本文链接:https://www.haomeiwen.com/subject/hzpgectx.html