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

重复的DNA序列

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

示例:

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

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

  1. class Solution
  2. {
  3. public:
  4. vector<string> findRepeatedDnaSequences(string s)
  5. {
  6. unordered_map<string, int> m;
  7. vector<string> v;
  8. size_t len = s.length();
  9. for (int i = 0; (i + 9) < len; i++)
  10. {
  11. m[s.substr(i, 10)]++;
  12. }
  13. for (auto x : m)
  14. {
  15. if (x.second > 1)
  16. v.push_back(x.first);
  17. }
  18. return v;
  19. }
  20. };

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

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

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

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

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

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

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

  1. for (int i = 0; i < 10; i++)
  2. 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
  1. class Solution
  2. {
  3. public:
  4. unordered_map<char, int> m = {{'A', 0}, {'C', 1}, {'G', 2}, {'T', 3}};
  5. vector<string> findRepeatedDnaSequences2(string s)
  6. {
  7. int len = s.length();
  8. vector<string> v;
  9. if (len < 10)
  10. return v;
  11. uint32_t val = 0; //val只需要低20bit
  12. uint32_t mask = (1 << 20) - 1; //低20位全1
  13. bitset<1 << 20> s1, s2;
  14. for (int i = 0; i < 10; i++)
  15. val = (val << 2) | m[s[i]];
  16. s1.set(val);
  17. for (int i = 10; i < len; i++)
  18. {
  19. val = ((val << 2) & mask) | m[s[i]];
  20. if (s2[val])
  21. continue;
  22. if (s1[val])
  23. {
  24. v.push_back(s.substr(i - 9, 10));
  25. s2.set(val);
  26. }
  27. else
  28. {
  29. s1.set(val);
  30. }
  31. }
  32. return v;
  33. }
  34. };

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

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

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

面试题专栏

下一个数

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

示例:

  1. 输入:num = 2(或者0b10
  2. 输出:[4, 1] 或者([0b100, 0b1])
  3. 输入:num = 1
  4. 输出:[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 (并要求两个位置尽可能地接近) 。也就是:

  1. 0100010011101101010100001 0 1 11110
  2. => 0100010011101101010100001 1 0 11110

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

  1. 0100010011101101010100001 0 1 11110
  2. => 0100010011101101010100001 1 0 11110
  3. => 0100010011101101010100001 1 0 01111

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

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

  1. 00001010110000
  2. => 00001010101000

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

  1. 0100 0000 1111
  2. => 0010 0000 1111

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

  1. 0100 0000 1111
  2. => 0010 0000 1111
  3. => 0011 1110 0000

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

完整代码:

  1. class Solution
  2. {
  3. public:
  4. vector<int> findClosedNumbers(int n)
  5. {
  6. vector<int> v({-1, -1});
  7. if (n == 0 || n == 0x7fffffff)
  8. return v;
  9. v[0] = bigger(n), v[1] = smaller(n);
  10. cout << v[0] << ' ' << v[1] << endl;
  11. return v;
  12. }
  13. int bigger(int n)
  14. {
  15. bitset<32> bits(n);
  16. int i = 0;
  17. while (bits[i] == 0)
  18. i++;
  19. int t = i;
  20. while (i < 31 && bits[i] == 1)
  21. i++;
  22. if (i >= 31)
  23. return -1;
  24. int ones = i - t;
  25. bits[i] = 1, bits[i - 1] = 0;
  26. uint32_t mask = ~((1 << (i - 1)) - 1);
  27. bits &= mask;
  28. i = 0;
  29. while (--ones)
  30. bits[i++] = 1;
  31. return (int)bits.to_ulong();
  32. }
  33. int smaller(int n)
  34. {
  35. bitset<32> bits(n);
  36. int i = 0;
  37. while (bits[i] == 0)
  38. i++;
  39. int j = i - 1;
  40. if (j != -1)
  41. {
  42. // ...1000 的形式
  43. bits[i] = 0, bits[j] = 1;
  44. return (int)bits.to_ulong();
  45. }
  46. else
  47. {
  48. // 到这里说明 i=0
  49. // ..0...1 的形式
  50. // 计算连续 i 个 1
  51. //肯定有 0, 不必担心越界
  52. while (bits[i] == 1)
  53. i++;
  54. int ones = i;
  55. while (i < 31 && bits[i] == 0)
  56. i++;
  57. // i=31, 可确定n=1
  58. if (i == 31)
  59. return -1;
  60. bits[i] = 0, bits[i - 1] = 1;
  61. uint32_t mask = ~((1 << (i - 1)) - 1);
  62. bits &= mask;
  63. while (ones--)
  64. bits[i - 2] = 1, i--;
  65. return (int)bits.to_ulong();
  66. }
  67. }
  68. void printBits(int num)
  69. {
  70. bitset<32> bits(num);
  71. cout << bits << endl;
  72. }
  73. };

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

  1. class Solution
  2. {
  3. public:
  4. vector<int> findClosedNumbers(int n)
  5. {
  6. vector<int> v({-1, -1});
  7. if (n == 0 || n == 0x7fffffff)
  8. return v;
  9. int t = n;
  10. int bench = hammingWeight(n);
  11. while (--t)
  12. {
  13. if (hammingWeight(t) == bench)
  14. {
  15. v[1] = t;
  16. break;
  17. }
  18. }
  19. t = n;
  20. while (++t)
  21. {
  22. if (hammingWeight(t) == bench)
  23. {
  24. v[0] = t;
  25. break;
  26. }
  27. }
  28. return v;
  29. }
  30. int hammingWeight(uint32_t n)
  31. {
  32. int t = 0;
  33. while (n)
  34. t++, n &= (n - 1);
  35. return t;
  36. }
  37. };

翻转数位

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

示例

  1. 输入: num = 1775(11011101111)
  2. 输出: 8
  3. 输入: num = 7(0111)
  4. 输出: 4

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

  1. #define bit(n, i) (((n) >> (i)) & 0x1)
  2. class Solution
  3. {
  4. public:
  5. int reverseBits(int num)
  6. {
  7. int len = 0;
  8. bitset<32> bits(num);
  9. for (int i = 0; i < 32; i++)
  10. {
  11. if (bits[i] == 0)
  12. {
  13. bits[i] = 1;
  14. len = max(len, count((uint32_t)bits.to_ulong()));
  15. bits[i] = 0;
  16. }
  17. }
  18. return len;
  19. }
  20. int count(uint32_t n)
  21. {
  22. if (((int)n) == -1)
  23. return 32;
  24. if (n & (n - 1) == 0)
  25. return 1;
  26. int idx = 0;
  27. int len = 0;
  28. while (idx < 32 && bit(n, idx) == 0)
  29. idx++;
  30. for (int i = idx; i < 32; i++)
  31. {
  32. if (bit(n, i) == 0)
  33. {
  34. cout << idx << endl;
  35. len = max(len, i - idx);
  36. while (i < 32 && bit(n, i) == 0)
  37. i++;
  38. idx = i;
  39. }
  40. }
  41. return len;
  42. }
  43. };

交换数字

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

解题思路:异或的性质。

  1. class Solution {
  2. public:
  3. vector<int> swapNumbers(vector<int>& numbers) {
  4. numbers[0]^=numbers[1]; //n[0]^n[1]
  5. numbers[1]^=numbers[0]; //n[1]=n[1]^n[0]^n[1]=n[0]
  6. numbers[0]^=numbers[1]; //n[0]=n[0]^n[1]^[0]=n[1]
  7. return numbers;
  8. }
  9. };

[leetcode] 位操作题解-2的更多相关文章

  1. [leetcode] 位操作题解

    子集 题目[78]:给定一组不含重复元素的整数数组 nums,返回该数组所有可能的子集(幂集). 示例: 输入: nums = [1,2,3] 输出: [ [3],   [1],   [2],   [ ...

  2. LeetCode OJ 题解

    博客搬至blog.csgrandeur.com,cnblogs不再更新. 新的题解会更新在新博客:http://blog.csgrandeur.com/2014/01/15/LeetCode-OJ-S ...

  3. Leetcode 简略题解 - 共567题

    Leetcode 简略题解 - 共567题     写在开头:我作为一个老实人,一向非常反感骗赞.收智商税两种行为.前几天看到不止两三位用户说自己辛苦写了干货,结果收藏数是点赞数的三倍有余,感觉自己的 ...

  4. LeetCode 算法题解 js 版 (001 Two Sum)

    LeetCode 算法题解 js 版 (001 Two Sum) 两数之和 https://leetcode.com/problems/two-sum/submissions/ https://lee ...

  5. leetcode & lintcode 题解

    刷题备忘录,for bug-free 招行面试题--求无序数组最长连续序列的长度,这里连续指的是值连续--间隔为1,并不是数值的位置连续 问题: 给出一个未排序的整数数组,找出最长的连续元素序列的长度 ...

  6. LeetCode一句话题解

    深度优先搜索 人生经验 1. 需要输出所有解.并由于元素集有重复元素,要求返回的结果需要去重的情况,可考虑使用值对应数量的map,然后分别考虑依次取不同数量该值的可能. LeetCode39 题目:给 ...

  7. LeetCode 中等题解(3)

    34 在排序数组中查找元素的第一个和最后一个位置 Question 给定一个按照升序排列的整数数组 nums,和一个目标值 target.找出给定目标值在数组中的开始位置和结束位置. 你的算法时间复杂 ...

  8. LeetCode 中等题解(1)

    16 最接近的三数之和 Question 给定一个包括 n 个整数的数组 nums 和 一个目标值 target.找出 nums 中的三个整数,使得它们的和与 target 最接近.返回这三个数的和. ...

  9. leetcode个人题解——two sum

    这是leetcode第一题,通过较为简单. 第一题用来测试的,用的c,直接暴力法过, /** * Note: The returned array must be malloced, assume c ...

随机推荐

  1. .Net Core调用oracle存储过程

    一 前言 实战踩坑系列,调用第三方Oracle存储,各种血泪史,现记录如下. 二 入坑 首先,调用Oracle需要安装客户端驱动才行,但是在程序开发中下载客户端驱动是一个不明智的选择.于是,不管是微软 ...

  2. MVC09

    1.委托(delegate)调用静态方法 委托类似于C++中的函数指针. 某方法仅仅在执行的时候才能确定是否被调用. 是实现事件和回调函数的基础. 面向对象,安全性高. using System; u ...

  3. 解决微信小程序视频组件层级过高的问题

    本文首发于我的个人博客:http://www.fogcrane.org 前言 在微信小程序的开发中,总有一些"VIP"组件,他们的层级,高得让人抓狂,总是凌驾于很多其他低层级组件之 ...

  4. spring boot 整合elasticsearch

    1.导入jar包 <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncodi ...

  5. JDK8内存模型—消失的PermGen

    一.JVM 内存模型 根据 JVM 规范,JVM 内存共分为虚拟机栈.堆.方法区.程序计数器.本地方法栈五个部分. 1.虚拟机栈:每个线程有一个私有的栈,随着线程的创建而创建.栈里面存着的是一种叫“栈 ...

  6. CSS的四种样式

    行内式CSS样式 在标签内部使用的样式 <div id="one" style="width:50p"></div> 内嵌式CSS样式 ...

  7. 使用jquery实现动态时钟

    先导入jquery-1.7.2.min.js或其他版本文件 js部分 <script> $(function () { showTime(); //文档加载后.就开始显示时间 var se ...

  8. 使用 Github Action 进行前端自动化发布

    前言 说起自动化,无论是在公司还是我们个人的项目中,都会用到或者编写一些工具来帮助我们去处理琐碎重复的工作,以节约时间提升效率,尤其是我们做前端开发会涉及诸如构建.部署.单元测试等这些开发工作流中重复 ...

  9. html5 拖放购物车

    1.本例中模仿了购物车添加的功能 主要运用了ondragstart / ondragover/ ondrag 功能 功能比较简单 遗留问题:火狐下图片拖进会被打开 <!doctype html& ...

  10. 记Android R(SDK=30)系统执行UiAutomator1.0异常

    最近Android发布了AndroidStudio 3.6稳定版,升级后明显能体验到好多细节的提升,最大的提升莫过于可以创建Android R预览版的模拟器了,并且模拟器可以设置多个尺寸的屏幕.And ...