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

原题链接:

第一题:左旋转字符串

题目:

汇编语言中有一种移位指令叫做循环左移(ROL),现在有个简单的任务,就是用字符串模拟这个指令的运算结果。对于一个给定的字符序列S,请你把其循环左移K位后的序列输出。例如,字符序列S=”abcXYZdef”,要求输出循环左移3位后的结果,即“XYZdefabc”。是不是很简单?OK,搞定它!

解析:

最简单的做法是把这个字符串本身加到这个字符串后,手动形成一个首位相接的点,这样,你再按照题目中的要求去左移,注意,只需要移动,不需要把移出去的部分补到字符串的后面,答案就出来了。

class Solution {
public:
    string LeftRotateString(string str, int n) {
        return str.size() == 0 ? "" : (str += str, str.substr(n, str.size() / 2));
    }
};

用字母来表示这个题就是:把字符串 ABBA A B ⇒ B A

我们反过来考虑, BA=((BA)T)T=(ATBT)T B A = ( ( B A ) T ) T = ( A T B T ) T ,现在也就是说 (ATBT)T=BA ( A T B T ) T = B A

那么我们就可以把A部分反转一次,B部分反转一次,最后整个字符串再反转一次,就可以得到答案。

class Solution {
public:
    string LeftRotateString(string str, int n) {
        if (str.size() == 0)
            return "";
        reverse(str.begin(), str.begin() + n);
        reverse(str.begin() + n, str.end());
        reverse(str.begin(), str.end());
        return str;
    }
};

第二题:翻转单词顺序列

题目:

牛客最近来了一个新员工Fish,每天早晨总是会拿着一本英文杂志,写些句子在本子上。同事Cat对Fish写的内容颇感兴趣,有一天他向Fish借来翻看,但却读不懂它的意思。例如,“student. a am I”。后来才意识到,这家伙原来把句子单词的顺序翻转了,正确的句子应该是“I am a student.”。Cat对一一的翻转这些单词顺序可不在行,你能帮助他么?

解析:

先用递归做,把后面的字符串都弄好,最后把当前单词接到后面的字符串的最后。

class Solution {
public:
    string ReverseSentence(string str) {
        int pos = str.find(" ");
        return pos == string::npos ? str : 
                        ReverseSentence(str.substr(pos + 1)) + " " + str.substr(0, pos);
    }
};

非递归就是用栈去消除这个递归,从开头往后扫描单词,遇到一个单词就把该单词入栈;所有的单词都入栈后,这时只需要把所有单词顺序出栈输出就好了。

class Solution {
public:
    string ReverseSentence(string str) {
        stack<string> st;
        int pos, pre;
        for (pre = 0, pos = str.find(" ", pre); pos != string::npos; 
                pre = pos + 1, pos = str.find(" ", pre))
            st.push(str.substr(pre, pos - pre));
        string ret(str.substr(pre));
        for ( ; !st.empty(); ret += " " + st.top(), st.pop()) {}
        return ret;
    }
};

这里还有一种做法,其实上一个题已经讲过了。只不过 AB A B 变成了 ABC A B C ⋯ 而已,思想还是一样的。

class Solution {
public:
    string ReverseSentence(string str) {
        int pos, pre;
        for (pre = 0, pos = str.find(" ", pre); pos != string::npos; 
                pre = pos + 1, pos = str.find(" ", pre))
            reverse(str.begin() + pre, str.begin() + pos);
        reverse(str.begin() + pre, str.end());
        reverse(str.begin(), str.end());
        return str;
    }
};

第三题:扑克牌顺子

题目:

LL今天心情特别好,因为他去买了一副扑克牌,发现里面居然有2个大王,2个小王(一副牌原本是54张^_^)…他随机从中抽出了5张牌,想测测自己的手气,看看能不能抽到顺子,如果抽到的话,他决定去买体育彩票,嘿嘿!!“红心A,黑桃3,小王,大王,方片5”,“Oh My God!”不是顺子…..LL不高兴了,他想了想,决定大小王可以看成任何数字,并且A看作1,J为11,Q为12,K为13。上面的5张牌就可以变成“1,2,3,4,5”(大小王分别看作2和4),“So Lucky!”。LL决定去买体育彩票啦。 现在,要求你使用这幅牌模拟上面的过程,然后告诉我们LL的运气如何。为了方便起见,你可以认为大小王是0。

解析:

首先,顺子必须是五张牌,这个判断一下,其次,大小王可以任意变化,故我们先不考虑大小王,先考虑那些不能变的,看看它们合法与否;

不能变的牌要满足下面的要求:

  • 不能有重复的牌;
  • 最大的牌到最小的牌的距离要小于五。

只有满足这两个条件,不能变的牌才是合法的,缺少的牌用大小王去变,这样就是顺子了。

class Solution {
public:
    bool IsContinuous( vector<int> numbers ) {
        if (numbers.size() != 5)
            return false;
        bool f = true;
        int val_max = -0x3f3f3f, val_min = 0x3f3f3f, used = 0;
        for (auto it = numbers.begin(); it != numbers.end(); used |= (1 << *it++))
            if (*it && (used & (1 << *it))) {
                f = false;
                break;
            }
        if (!f)
            return false;
        for (auto it = numbers.begin(); it != numbers.end();
            val_max = max(val_max, *it ? *it : val_max), val_min = min(val_min, *it ? *it : val_min), ++it) {}
        return val_max - val_min < 5;
    }
};

第四题:孩子们的游戏(圆圈中最后剩下的数)

题目:

每年六一儿童节,牛客都会准备一些小礼物去看望孤儿院的小朋友,今年亦是如此。HF作为牛客的资深元老,自然也准备了一些小游戏。其中,有个游戏是这样的:首先,让小朋友们围成一个大圈。然后,他随机指定一个数m,让编号为0的小朋友开始报数。每次喊到m-1的那个小朋友要出列唱首歌,然后可以在礼品箱中任意的挑选礼物,并且不再回到圈中,从他的下一个小朋友开始,继续0...m-1报数….这样下去….直到剩下最后一个小朋友,可以不用表演,并且拿到牛客名贵的“名侦探柯南”典藏版(名额有限哦!!^_^)。请你试着想下,哪个小朋友会得到这份礼品呢?(注:小朋友的编号是从0n-1)

解析:

经典的约瑟夫环问题。网上很多博客写了这个问题的解法,但是我推荐牛客网左神的讲解,真的讲得很好,大家就去找找他的这个视频看看。

这里给出的是这个问题的 O(n) O ( n ) 的解法,其余的用链表去模拟的就不写了。

class Solution {
public:
    int LastRemaining_Solution(int n, int m)
    {
        if (!n)
            return -1;
        int ret = 0;
        for (int i = 2; i <= n; ret = (ret + m) % i++) {}
        return ret;
    }
};

第五题:求1+2+3+…+n

题目:

求1+2+3+…+n,要求不能使用乘除法、for、while、if、else、switch、case等关键字及条件判断语句(A?B:C)。

解析:

这些关键字都不能用,那么只能用递归去模拟循环,逻辑表达式的短路原则去模拟判断了。

class Solution {
public:
    int Sum_Solution(int n) {
        int ret = 0;
        n && (ret = n + Sum_Solution(n - 1));
        return ret;
    }
};

这是个等差数列求和,那么公式就是 S=n(1+n)2 S = n ( 1 + n ) 2

这个公式中的除2倒是好解决,用右移运算就行了;问题是如何实现这个乘法?所以下面的几种解法都是围绕这个实现乘法的问题来阐述的。

这几种方法我觉得最脑洞的就是利用C语言中sizeof这个运算符了。

开一个二维字符数组, 第一维大小为n,第二维大小为n + 1,这样用sizeof去计算这个字符数组的字节数的时候,就会自动执行n * (n + 1)了。

class Solution {
public:
    int Sum_Solution(int n) {
        char tmp[n][n + 1];
        return sizeof(tmp) >> 1;
    }
};

最投机的方法我觉得就是用库函数pow了,n * (n + 1) = pow(n, 2) + n这样子就没有乘法了。

class Solution {
public:
    int Sum_Solution(int n) {
        return (int)(pow(n * 1.0, 2.0) + n) >> 1;
    }
};

最后这种方法我觉得也很好, a×b=(1010111)2×b=b2n+b2n2+ a × b = ( 1010111 ⋯ ) 2 × b = b ∗ 2 n + b ∗ 2 n − 2 + ⋯

就是把 a a 化成二进制,然后利用乘法的分配率进行计算,由于b都是乘以2的幂次,故可以通过左移来代替乘法运算;之后再通过递归来累加这些乘积。

class Solution {
public:
    int Sum_Solution(int n) {
        return get(n, n + 1) >> 1;
    }

    int get(int a, int b) {
        int ret = 0;
        a && (((a & 1) && (ret = (a & 1) * b + get(a >> 1, b << 1))) || (ret = get(a >> 1, b << 1)));
        return ret;
    }
};

可能你们会说了,这个代码和第一个代码是一样的啊,为什么还要多此一举写这个代码呢?原因就是时间复杂度!

第一个代码的时间复杂度为 O(n) O ( n ) ,这个代码的时间复杂度为 O(logn) O ( l o g n )

第六题:不用加减乘除做加法

题目:

写一个函数,求两个整数之和,要求在函数体内不得使用+、-、*、/四则运算符号。

解析:

想想自己是怎么手算两个十进制数相加的,这里的思想只不过是把十进制相加的步骤改一下,然后用到二进制上,因为二进制手算加减法不需要四则运算,只需要位运算就可以搞定。

二进制中,加减可以用异或来代替,而进位则可以用与运算(两个1才能为1)来代替。

我先用十进制来演示一下这个算法步骤。

十进制中的进位都是累加到下一位计算的,我们这里不累加到下一位,相当于把进位单独拿出来相加,举个例子, 245+199=334+10+100=334+110=444 245 + 199 = 334 + 10 + 100 = 334 + 110 = 444

先把各位上的数字相加取个位数,然后再把各位上的进位累加起来就好了。

用二进制计算同样是这个思想,只不过二进制中相加取个位,累加进位可以用位运算来表示而已。

  • 相加取个位用异或来解决;
  • 累加进位用与运算的结果然后左移一起来解决;
  • 然后把这两步的结果加起来,由于这两步的结果加起来也可能有进位,因此,如果有进位就重复第一步和第二步,直到没有进位为止,这个时候相加起来的结果才是答案。
class Solution {
public:
    int Add(int num1, int num2)
    {
        int ret = num1, w = num2;
        for (; w; ) {
            ret = num1 ^ num2;
            w = (num1 & num2) << 1;
            num1 = ret;
            num2 = w;
        }
        return ret;
    }
};
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值