zoukankan      html  css  js  c++  java
  • [leetcode] 位操作(Ⅱ)

    本文是 leetcode 位操作题库的题目解析。点击每个标题可进入题目页面。

    重复的DNA序列

    题目:所有 DNA 都由一系列缩写为 A,C,G 和 T 的核苷酸组成,例如:“ACGAATTCCG”。在研究 DNA 时,识别 DNA 中的重复序列有时会对研究非常有帮助。编写一个函数来查找 DNA 分子中所有出现超过一次的 10 个字母长的序列(子串)。(是个好题,请耐心看完优化的部分)

    示例:

    输入:s = "AAAAACCCCCAAAAACCCCCCAAAAAGGGTTT"
    输出:["AAAAACCCCC", "CCCCCAAAAA"]
    

    解题思路:显然,这里的序列都是连续的,因此最多只会有 (s.length - 10) 种序列,因此采用数据结构 map<string,int> 进行计数即可。

    class Solution
    {
    public:
        vector<string> findRepeatedDnaSequences(string s)
        {
            unordered_map<string, int> m;
            vector<string> v;
            size_t len = s.length();
            for (int i = 0; (i + 9) < len; i++)
            {
                m[s.substr(i, 10)]++;
            }
    
            for (auto x : m)
            {
                if (x.second > 1)
                    v.push_back(x.first);
            }
            return v;
        }
    };
    

    下面考虑如何优化,因为使用字符串作为 (key) 值,效率低得令人发指(特别是使用的空间较大)。

    从 leetcode 的海外版找到一个十分 geek 的 优化方案中文也有人翻译了一下)。建议阅读一下英文原文的三点优化思路,可以看出原作者的计算机基础十分好(特别是 C++ Runtime 这一块)。

    简单来说,就是优化 map 的 (key, val) 的存储:

    • key :不使用10个字符的子串作为 key 值,而是将其转化为若干比特位。
    • val :不需要完整计算子串出现的次数,只需要记录 3 种状态:出现零次,出现一次,出现多次(编码表示为00, 01, 11, 实则是出现一个1就代表出现一次,因此只需要 2 bit)。

    首先对 ACGT 四种字符编码: 。

    unordered_map<char, int> m = {{'A', 0}, {'C', 1}, {'G', 2}, {'T', 3}};
    

    对于 10 个字符的子串,映射之后的值为:

    for (int i = 0; i < 10; i++)
        val = (val << 2) | m[s[i]];
    

    一个 10 字符的子串 (substr) ,只有 (4^{10}=2^{20}) 种不同的形式,经过映射为一个 20bit 的数值之后,仍然只有 (2^{20}) 种不同的值,那么在这 ([0,2^{20}-1]) 这些数值当中,我们如何记录某个值 (val) 是否出现呢?答案是使用一个 bitset<(1<<20)> bs 去记录,如果某个 val 出现,那么 bs[val]=1 (bs.set(val))

    到这一步解决了 key 的映射和存储,val 的「出现零次」和「出现一次」的问题也解决了,那么如何记录「出现多次」呢?上面已经提到,需要用到 2 bit 去记录,目前,某个 val 是否出现只用了 bs[val] 一个 bit 去记录,因此我们需要再添加一个 bitset<1<<20> bs2

    • 出现 0 次:bs1[val]=0, bs2[val]=0
    • 出现 1 次:bs1[val]=1, bs2[val]=0
    • 出现 2 次:bs1[val]=1, bs2[val]=1
    class Solution
    {
    public:
        unordered_map<char, int> m = {{'A', 0}, {'C', 1}, {'G', 2}, {'T', 3}};
        vector<string> findRepeatedDnaSequences2(string s)
        {
            int len = s.length();
            vector<string> v;
            if (len < 10)
                return v;
            uint32_t val = 0;              //val只需要低20bit
            uint32_t mask = (1 << 20) - 1; //低20位全1
            bitset<1 << 20> s1, s2;
            for (int i = 0; i < 10; i++)
                val = (val << 2) | m[s[i]];
            s1.set(val);
    
            for (int i = 10; i < len; i++)
            {
                val = ((val << 2) & mask) | m[s[i]];
                if (s2[val])
                    continue;
                if (s1[val])
                {
                    v.push_back(s.substr(i - 9, 10));
                    s2.set(val);
                }
                else
                {
                    s1.set(val);
                }
            }
            return v;
        }
    };
    

    时间复杂度 (O(N)) ,对于空间的使用,bitset<1<<20> 看似很大但其实:

    int main()
    {
        bitset<1 << 20> s;
        cout << 2 * sizeof(s) / 1024 << endl;
    }
    

    上面程序的输出是:256 (KB) 。而使用原始字符串作为 key 值,最坏情况下有 (2^{20}) 种不同的字符串,每个字符串 10 个字节:(2^{20} cdot 10 = 10 MB) 的空间开销。

    面试题专栏

    下一个数

    题目:下一个数。给定一个正整数,找出与其二进制表达式中1的个数相同且大小最接近的那两个数(一个略大,一个略小)。

    示例:

     输入:num = 2(或者0b10)
     输出:[4, 1] 或者([0b100, 0b1])
     输入:num = 1
     输出:[2, -1]
    

    解题思路:细节巨多,还没完成。「正面刚」解法如下所述。

    我们先了解一个预备知识:假如某个整数中, bits[i] 为 1 ,把它移动到 bits[i + d] 的位置(bits[i+d] 这个位置为 0 ),那么前后数值的变化为 (Delta = 2^{i+d} - 2^i = 2^{i} cdot (2^d-1))

    对于 01000100111011010101000010111110 ,找比它「最近大」的,肯定是把 1 向高位移动,并遵循以下原则: 把位于尽可能低位的 1 提高到尽可能低位的 0 (并要求两个位置尽可能地接近) 。也就是:

       0100010011101101010100001 0  1 11110
    => 0100010011101101010100001 1  0 11110
    

    但是这是不是就是要找的「最近大」的数值呢?显然不是,经过「放大」之后,还能适当缩小:

       0100010011101101010100001 0  1 11110
    => 0100010011101101010100001 1  0 11110
    => 0100010011101101010100001 1  0 01111
    

    把位于被移动的1后面的那些1全部「挤」到最低位,显然这才是答案。

    下面来看如何找「最近小」。操作必定是把高位的 1 向低位移动。并且还是遵循:把位于尽可能低位的 1 降低到尽可能低位的 0 (并要求两个位置尽可能地接近)。比如:

       00001010110000
    => 00001010101000
    

    这是比较简单的情况,考虑到 ...00001111 这种形式,如果省略号的位置都没有 1 ,那么显然找不到。如果有 1 ,那就是 ...10...0000111 这种形式,比如 0100 0000 1111,那么:

       0100 0000 1111
    => 0010 0000 1111
    

    同理,这里我们是缩小了,但其实还应该适当「放大」才是我们需要的答案:

       0100 0000 1111
    => 0010 0000 1111
    => 0011 1110 0000
    

    被移动的1后面的那些1全部排到后面。

    完整代码:

    class Solution
    {
    public:
        vector<int> findClosedNumbers(int n)
        {
            vector<int> v({-1, -1});
            if (n == 0 || n == 0x7fffffff)
                return v;
            v[0] = bigger(n), v[1] = smaller(n);
            cout << v[0] << ' ' << v[1] << endl;
            return v;
        }
        int bigger(int n)
        {
            bitset<32> bits(n);
            int i = 0;
            while (bits[i] == 0)
                i++;
            int t = i;
            while (i < 31 && bits[i] == 1)
                i++;
            if (i >= 31)
                return -1;
            int ones = i - t;
            bits[i] = 1, bits[i - 1] = 0;
            uint32_t mask = ~((1 << (i - 1)) - 1);
            bits &= mask;
            i = 0;
            while (--ones)
                bits[i++] = 1;
            return (int)bits.to_ulong();
        }
        int smaller(int n)
        {
            bitset<32> bits(n);
            int i = 0;
            while (bits[i] == 0)
                i++;
            int j = i - 1;
            if (j != -1)
            {
                // ...1000 的形式
                bits[i] = 0, bits[j] = 1;
                return (int)bits.to_ulong();
            }
            else
            {
                // 到这里说明 i=0
                // ..0...1 的形式
                // 计算连续 i 个 1
                //肯定有 0, 不必担心越界
                while (bits[i] == 1)
                    i++;
                int ones = i;
    
                while (i < 31 && bits[i] == 0)
                    i++;
                // i=31, 可确定n=1
                if (i == 31)
                    return -1;
                bits[i] = 0, bits[i - 1] = 1;
                uint32_t mask = ~((1 << (i - 1)) - 1);
                bits &= mask;
                while (ones--)
                    bits[i - 2] = 1, i--;
                return (int)bits.to_ulong();
            }
        }
    
        void printBits(int num)
        {
            bitset<32> bits(num);
            cout << bits << endl;
        }
    };
    

    还有一个「正面刚」的方法,遍历,计算 1 的个数,如果与 n 相等就找到了,到 0 说明没找到,则是默认值 -1 。

    class Solution
    {
    public:
        vector<int> findClosedNumbers(int n)
        {
            vector<int> v({-1, -1});
            if (n == 0 || n == 0x7fffffff)
                return v;
            int t = n;
            int bench = hammingWeight(n);
            while (--t)
            {
                if (hammingWeight(t) == bench)
                {
                    v[1] = t;
                    break;
                }
            }
            t = n;
            while (++t)
            {
                if (hammingWeight(t) == bench)
                {
                    v[0] = t;
                    break;
                }
            }
            return v;
        }
    
        int hammingWeight(uint32_t n)
        {
            int t = 0;
            while (n)
                t++, n &= (n - 1);
            return t;
        }
    };
    

    翻转数位

    题目:给定一个32位整数 num,你可以将一个数位从0变为1。请编写一个程序,找出你能够获得的最长的一串1的长度。

    示例

    输入: num = 1775(11011101111)
    输出: 8
    输入: num = 7(0111)
    输出: 4
    

    解题思路:暴力解法,遍历每个0,翻转,计算1的个数,取最大。

    #define bit(n, i) (((n) >> (i)) & 0x1)
    class Solution
    {
    public:
        int reverseBits(int num)
        {
            int len = 0;
            bitset<32> bits(num);
            for (int i = 0; i < 32; i++)
            {
                if (bits[i] == 0)
                {
                    bits[i] = 1;
                    len = max(len, count((uint32_t)bits.to_ulong()));
                    bits[i] = 0;
                }
            }
            return len;
        }
    
        int count(uint32_t n)
        {
            if (((int)n) == -1)
                return 32;
            if (n & (n - 1) == 0)
                return 1;
            int idx = 0;
            int len = 0;
            while (idx < 32 && bit(n, idx) == 0)
                idx++;
    
            for (int i = idx; i < 32; i++)
            {
                if (bit(n, i) == 0)
                {
                    cout << idx << endl;
                    len = max(len, i - idx);
                    while (i < 32 && bit(n, i) == 0)
                        i++;
                    idx = i;
                }
            }
            return len;
        }
    };
    

    交换数字

    题目:编写一个函数,不用临时变量,直接交换numbers = [a, b]ab的值。

    解题思路:异或的性质。

    class Solution {
    public:
        vector<int> swapNumbers(vector<int>& numbers) {
            numbers[0]^=numbers[1]; //n[0]^n[1]
            numbers[1]^=numbers[0]; //n[1]=n[1]^n[0]^n[1]=n[0]
            numbers[0]^=numbers[1]; //n[0]=n[0]^n[1]^[0]=n[1]
            return numbers;
        }
    };
    
  • 相关阅读:
    织梦删除不需要的文件及文件安全设置
    织梦安全设置
    阿里云一个虚拟主机安装多个织梦系统
    显示织梦模板不存在的解决方法
    织梦安装
    织梦修改数据库密码
    CSS实现兼容性的渐变背景(gradient)效果
    css边框样式、边框配色、边框阴影、边框圆角、图片边框
    织梦后台编辑器添加中文字体
    织梦安装百度编辑器
  • 原文地址:https://www.cnblogs.com/sinkinben/p/12343416.html
Copyright © 2011-2022 走看看