剑指Offer66题之每日6题 - 第六天

原题链接:

第一题:整数中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,L1) f ( 1 , R ) − f ( 1 , L − 1 ) 来做,这么做可以简化问题;

那么现在问题就转化为了如何求 f(1,x) f ( 1 , x ) 了,但是这个题没有给我们[L, R]区间,直接让我们求[1, n],这样问题反而简化了。

从数位上来分析:x可以表示成abcdef....,这里假设x可以表示成abcdefx是个6位数,那么我们考虑每一位对答案的贡献,由于每一位独立,因此可以单独计算每一位上的1,然后全部加起来,这就是答案;

那么每一位上的1出现的次数该怎么搞呢?这就要分情况讨论了,假设我们现在讨论c这一位:

  • c == 0 ,也就是说ab0ab - 1变化,cdef00009999变化,c这一位上都可以出现1,利用乘法原理,那么对答案的贡献就是(ab - 1 - 0 + 1) * (9999 - 0000 + 1) = ab * 10000
  • c == 1,和上一项c == 0对答案的贡献一样,只不过要在这个贡献上再加上一点额外的贡献,当ab = ab时,c = 1,那么cdef就只能从0000cdef变化了,故要在原贡献的基础上加上cdef - 0000 + 1ab * 10000 + cedf + 1
  • c > 1,和第一项c == 0对答案的贡献一样,也要加上一点额外的贡献,当ab = ab时,c = 1,那么cdef就能从00009999变化了,故要在原贡献的基础上加上9999- 0000 + 1ab * 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、35的数称作丑数(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。并将P1000000007取模的结果输出。 即输出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 圈,那么可以建立下列等式

circlexlen1+len1k=circleylen2+len2k 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 也解出来呢?答案是不能的,因为再也找不到等式来限制k了,为此我特意做了下面这两个链表,可以发现 downup明显不一样,但是 len1, len2是一样的,所以 circlex,circley c i r c l e x , c i r c l e y 也是一样的, k k 是不相同的,故k要在代码中自己测一下;

说到这里还没有解释相遇点为什么就是第一次公共结点,其实你仔细想想就很简单了,假设相遇点在k之前结点d之前,这不可能啊,那时候两个链表根本不可能相遇;假设相遇点在d之后,这也不可能啊,既然在d之后相遇,那么在d点也会相遇,所以啊,相遇点一定就是第一次公共结点。

通过上面的分析,我们证明了下面这个代码的正确性,不过时间复杂度取决于两个链表长度的最小公倍数。

时间复杂度: O(len1len2gcd(len1len2)) 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;
    }
};
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值