剑指offer 刷题记录(31~40题)

转眼间就到了年末,前些天一直忙着论文投稿和开题报告。刷题都怠慢了,现在赶紧补上!
让我们用刷题来告别2019,用刷题来迎接2020!废话不多说,直接开始!

31.求出1-13的整数中1出现的次数,并算出100-1300的整数中1出现的次数?为此他特别数了一下1~13中包含1的数字有1、10、11、12、13因此共出现6次,但是对于后面问题他就没辙了。ACMer希望你们帮帮他,并把问题更加普遍化,可以很快的求出任意非负整数区间中1出现的次数(从1 到 n 中1出现的次数)。

最简单最直观的做法:一个个去判断,然后外层循环到n就行了。但是这样的做法效率很低。

//运行时间:6ms 占用内存:500k
class Solution {
public:
    int NumberOf1Between1AndN_Solution(int n) {
        int number=0;
        if(n<1) 
            return 0;
        for(int i=1;i<=n;++i) {
            int temp=i;
            while(temp) {
                if(temp%10==1)
                    ++number;
                temp/=10;
            }
        }
        return number;
    }
};

还有这个方法,给跪了orz!

//运行时间:6ms 占用内存:476k
    int ones = 0;
    for (long long m = 1; m <= n; m *= 10) {
        int a = n/m, b = n%m; //m表示当前分析的是哪一个数位
        ones += (a + 8) / 10 * m + (a % 10 == 1) * (b + 1);
    }
    return ones;

主要思路:设定整数点(如1、10、100等等)作为位置点i(对应n的各位、十位、百位等等),分别对每个数位上有多少包含1的点进行分析:

  1. 根据设定的整数位置,对n进行分割,分为两部分,高位n/i,低位n%i
  2. 当i表示百位,且百位对应的数>=2,如n=31456,i=100,则a=314,b=56,此时百位为1的次数有
  3. a/10+1=32(最高两位0~31),每一次都包含100个连续的点,即共有(a%10+1)*100个点的百位为1
  4. 当i表示百位,且百位对应的数为1,如n=31156,i=100,则a=311,b=56,此时百位对应的就是1,则共有a%10(最高两位0-30)次是包含100个连续点,当最高两位为31(即a=311),本次只对应局部点00~56,共b+1次,所有点加起来共有(a%10*100)+(b+1),这些点百位对应为1
  5. 当i表示百位,且百位对应的数为0,如n=31056,i=100,则a=310,b=56,此时百位为1的次数有a/10=31(最高两位0~30)
  6. 综合以上三种情况,当百位对应0或>=2时,有(a+8)/10次包含所有100个点,还有当百位为1(a%10==1),需要增加局部点b+1
  7. 之所以补8,是因为当百位为0,则a/10==(a+8)/10,当百位>=2,补8会产生进位位,效果等同于(a/10+1)

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

**解题思路:先将整型数组转换成String数组,然后将String数组排序,最后将排好序的字符串数组拼接出来。关键就是制定排序规则。 排序规则如下:

  • 若ab > ba 则 a > b,
  • 若ab < ba 则 a < b,
  • 若ab = ba 则 a = b;
    **解释说明:**比如 “3” < "31"但是 “331” > “313”,所以要将二者拼接起来进行比较

对vector容器内的数据进行排序,按照 将a和b转为string后, 若 a+b<b+a a排在在前 的规则排序, 如 2 21 因为 212 < 221 所以 排序后为 21 2

//运行时间:4ms 占用内存:488k
 class Solution {
 public:
     static bool cmp(int a,int b){
         string A="";
         string B="";
         A+=to_string(a);//to_string() 可以将int 转化为string
         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个丑数。

首先从丑数的定义我们知道,一个丑数的因子只有2,3,5,那么丑数p = 2 ^ x * 3 ^ y * 5 ^ z,换句话说一个丑数一定由另一个丑数乘以2或者乘以3或者乘以5得到,那么我们从1开始乘以2,3,5,就得到2,3,5三个丑数,在从这三个丑数出发乘以2,3,5就得到4,6,10,6,9,15,10,15,25九个丑数,我们发现这种方***得到重复的丑数,而且我们题目要求第N个丑数,这样的方法得到的丑数也是无序的。那么我们可以维护三个队列:
(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放入三个队列;
……………………
疑问:
1.为什么分三个队列?
丑数数组里的数一定是有序的,因为我们是从丑数数组里的数乘以2,3,5选出的最小数,一定比以前未乘以2,3,5大,同时对于三个队列内部,按先后顺序乘以2,3,5分别放入,所以同一个队列内部也是有序的;
2.为什么比较三个队列头部最小的数放入丑数数组?
因为三个队列是有序的,所以取出三个头中最小的,等同于找到了三个队列所有数中最小的。
实现思路:
我们没有必要维护三个队列,只需要记录三个指针显示到达哪一步;“|”表示指针,arr表示丑数数组;
(1)1
|2
|3
|5
目前指针指向0,0,0,队列头arr[0] * 2 = 2, arr[0] * 3 = 3, arr[0] * 5 = 5
(2)1 2
2 |4
|3 6
|5 10
目前指针指向1,0,0,队列头arr[1] * 2 = 4, arr[0] * 3 = 3, arr[0] * 5 = 5
(3)1 2 3
2| 4 6
3 |6 9
|5 10 15
目前指针指向1,1,0,队列头arr[1] * 2 = 4, arr[1] * 3 = 6, arr[0] * 5 = 5
………………

//运行时间:4ms 占用内存:468k
class Solution {
public:
    int GetUglyNumber_Solution(int index) {
    // 0-6的丑数分别为0-6
        if(index < 7) return index;
        //p2,p3,p5分别为三个队列的指针,newNum为从队列头选出来的最小数
        int p2 = 0, p3 = 0, p5 = 0, newNum = 1;
        vector<int> arr;
        arr.push_back(newNum);
        while(arr.size() < index) {
            //选出三个队列头最小的数
            newNum = min(arr[p2] * 2, min(arr[p3] * 3, arr[p5] * 5));
            //这三个if有可能进入一个或者多个,进入多个是三个队列头最小的数有多个的情况
            if(arr[p2] * 2 == newNum) p2++;
            if(arr[p3] * 3 == newNum) p3++;
            if(arr[p5] * 5 == newNum) p5++;
            arr.push_back(newNum);
        }
        return newNum;
    }
};

34.在一个字符串(0<=字符串长度<=10000,全部由字母组成)中找到第一个只出现一次的字符,并返回它的位置, 如果没有则返回 -1(需要区分大小写).

先在hash表中统计各字母出现次数,第二次扫描直接访问hash表获得次数。

//运行时间:4ms 占用内存:476k
class Solution {
public:
    int FirstNotRepeatingChar(string str) {
        if (str.length() == 0)
            return -1;
        
        const int tableSize = 256;
        unsigned int hashTable[tableSize]={0};
        
        for(int i =0;i<str.length();++i)
            hashTable[str[i]]++;
        
        for(int i =0;i<str.length();++i) {
            if( hashTable[str[i]] == 1)
                return i;
        }
        return -1;
    }
};

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

这道题是真的挺难的啊!
思路一:
暴力解,两个循环,时间复杂度O(N2 ),但是会超出运行时间,导致失败。
思路二:
分治的思想,将数组不断一分为二,直到数组中只有两个元素,统计逆序对个数。然后对相邻的两个子数组进行合并,由于已经统计了这两对子数组内部的逆序对,因此需要把这两对子数组进行排序,避免在之后的统计过程中重复统计。在合并的时候也要计算组间的逆序对个数。
逆序对的总数 = 左边数组中的逆序对的数量 + 右边数组中逆序对的数量 + 左右结合成新的顺序数组时中出现的逆序对的数量
整个过程是一个归并排序的算法。
归并排序的性能不受输入数据的影响,时间复杂度始终都是O(nlogn)O(nlogn)。代价是需要额外的内存空间。
参考代码:

static const auto io_sync_off = []() {
    ios_base::sync_with_stdio(false);
    cin.tie(nullptr);
    cout.tie(nullptr);
    return nullptr;
}( );
class Solution {
public:
    static constexpr int P = 1000000007;
    vector<int>::iterator it;
    int InversePairs(vector<int>& data) {
        it = data.begin();
        if (data.empty())return 0;
        vector<int> dup(data);
        return merge_sort(data.begin(), data.end(), dup.begin());
    }
    //template<class RanIt>
    using RanIt = vector<int>::iterator;
    int merge_sort(const RanIt& begin1, const RanIt& end1, const RanIt& begin2) {
        int len = end1 - begin1;
        if (len < 2)return 0;
        int mid = ( len + 1 ) >> 1;
        auto m1 = begin1 + mid, m2 = begin2 + mid;
        auto i = m1, j = end1, k = begin2 + len;
        int ans = ( merge_sort(begin2, m2, begin1) + merge_sort(m2, k, m1) ) % P;
        for (--i, --j, --k; i >= begin1 && j >= m1; --k) {
            if (*i > *j) {
                *k = *i, --i;
                ( ans += j - m1 + 1 ) %= P;
            } else *k = *j, --j;
        }
        if (i >= begin1)copy(begin1, i + 1, begin2);
        else copy(m1, j + 1, begin2);
        return ans;
    }     
};

36.输入两个链表,找出它们的第一个公共结点。(注意因为传入数据是链表,所以错误测试数据的提示是用其他方式显示的,保证传入数据是正确的)

/*
struct ListNode {
	int val;
	struct ListNode *next;
	ListNode(int x) :
			val(x), next(NULL) {
	}
};*/
class Solution {
public:
    ListNode* FindFirstCommonNode( ListNode* pHead1, ListNode* pHead2) {
        ListNode *p1=pHead1;
        ListNode *p2=pHead2;
        int len1=0,len2=0,diff=0;
        while(p1!=NULL){
            p1=p1->next;
            len1++;
        }
        while(p2!=NULL){
            p2=p2->next;
            len2++;
        }
        if(len1>len2){
            diff=len1-len2;
            p1=pHead1;
            p2=pHead2;
        }
        else{
            diff=len2-len1;
            p1=pHead2;
            p2=pHead1;
        }
        for(int i=0;i<diff;i++){
            p1=p1->next;
        }
        while(p1!=NULL && p2!=NULL){
            if(p1==p2)
                break;
            p1=p1->next;
            p2=p2->next;
        }
        return p1;
    }
};

37.统计一个数字在排序数组中出现的次数。

看见有序,肯定就是二分查找了,算法比较简单,不多说,值得一提的是,循环和递归都要熟练的写出来才行。

class Solution {
public:
    int GetNumberOfK(vector<int> data ,int k) {
        int start,end,mid,count=0,i;
        unsigned int len = data.size();
        if(!len)
            return 0;
        start =0;
        end = len-1;
        mid = start;
        while(start<end)
        {
            mid = (start+end)>>1;
            if(k >data[mid])
                start = mid+1;
            if(k<data[mid])
                end = mid-1;
            if(k==data[mid])
                break;
        }
        i=mid;
        while( (i>=0) && (k==data[i]))
        {
            i--;
            count++;
        }
        i=mid+1;
        while((i<len)&& (k==data[i]) )
        {
            i++;
            count++;
        }
        return count;
    }
};

38.输入一棵二叉树,求该树的深度。从根结点到叶结点依次经过的结点(含根、叶结点)形成树的一条路径,最长路径的长度为树的深度。

递归的写法,代码会非常简洁优美:

/*
struct TreeNode {
	int val;
	struct TreeNode *left;
	struct TreeNode *right;
	TreeNode(int x) :
			val(x), left(NULL), right(NULL) {
	}
};*/
class Solution {
public:
    int TreeDepth(TreeNode* pRoot)
    {
         if(pRoot==nullptr) 
             return 0 ;
         return max(1+TreeDepth(pRoot->left), 1+TreeDepth(pRoot->right));
    }
};

39.输入一棵二叉树,判断该二叉树是否是平衡二叉树。

平衡二叉树定义:它是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。换句话说,平衡二叉树的左右子树也是平衡二叉树,那么所谓平衡就是左右子树的高度差不超过1。
思路:根据定义,我们只需要后序遍历此树,从树的叶子节点开始计算高度,只要有一个子树不满足定义就返回-1,如果满足继续向上计算高度。

class Solution {
public:
    bool IsBalanced_Solution(TreeNode* pRoot) {
        if(pRoot == nullptr)
            return true;
        int leftDepth = getDepth(pRoot -> left);
        int rightDepth = getDepth(pRoot -> right);
        if(leftDepth > rightDepth + 1 || leftDepth + 1 < rightDepth)
            return false;
        else
            return IsBalanced_Solution(pRoot -> left) && IsBalanced_Solution(pRoot -> right);
    }
     
    int getDepth(TreeNode* pRoot){
        if(pRoot == nullptr)
            return 0;
        int leftDepth = getDepth(pRoot -> left);
        int rightDepth = getDepth(pRoot -> right);
        return leftDepth > rightDepth ? leftDepth + 1 : rightDepth + 1;
    }
};

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

思路:

  1. 记数组中存在的唯一的两个数字分别为a,b 首先以二进制的角度去看所有的数字,每一位均为0,1组成
  2. 对所有数字进行个位上的异或,成对相同的数字结果为0,每一位上都这样异或依次,所以最终每一位上存在1的则必然是因为a,b在这一位上不同
  3. 根据最终结果上存在‘1’的这一位,将原数组分为两组,一组‘1’,一组‘0‘, 两组数字再分别异或,最终两个结果就是a,b;

参考代码:

class Solution {
public:
    void FindNumsAppearOnce(vector<int> data,int* num1,int *num2) {
        int num=0; 
        for(int i=0;i < data.size();i++){
            num^=data[i];//所有数异或,结果为不同的两个数字的异或
        }
 
        int count=0;//标志位,记录num中的第一个1出现的位置
        for(;count < data.size();count++){
            if((num&(1<<count))!=0){
                break;
            }
        }
 
        int num_1 = 0;
        int num_2 = 0;
 
        for(int i=0; i < data.size(); i++){ 
            if((data[i]&(1<<count))==0){//标志位为0的为一组,异或后必得到一个数字(这里注意==的优先级高于&,需在前面加())
                num_1^=data[i];
            }else{
                num_2^=data[i];//标志位为1的为一组
            }
        }
        *num1 = num_1;
        *num2 = num_2;
    }
};
发布了36 篇原创文章 · 获赞 8 · 访问量 1569

猜你喜欢

转载自blog.csdn.net/weixin_43619346/article/details/103770138