比特位计数
题意
给定一个非负整数 num
. 对于 0 ≤ i ≤ num
范围中的每个数字 i
, 计算其二进制数中的 1
的数目并将它们作为数组返回.
示例 1:
1 | 输入: 2 |
示例 2:
1 | 输入: 5 |
编写一个函数,输入是一个无符号整数,返回其二进制表达式中数字位数为 1
的个数.
示例 1:
1 | 输入:00000000000000000000000000001011 |
示例 2:
1 | 输入:00000000000000000000000010000000 |
示例 3:
1 | 输入:11111111111111111111111111111101 |
数据库的索引是一个要点, 无论是面试还是在工作中, 这个知识点都很常会用到, 你可能只是用过索引, 知道加了索引可以提高查询的性能, 但不知道为什么这样, 今天我们一起来详细了解下吧.
索引有很多种存储结构, 称之为索引模型, 不同类型的模型分别对应不同的适用场景.
哈希表是一种以键值对存储数据的结构 KEY - VALUE
. 查找时输入 key
来查找对应点 value
.
哈希表的思路很简单, 将值放置到数组中. 如定义一个长度为 16 的数组, 输入 key
: user1
, 对 user1
做哈希运算 (利用哈希函数), 返回一个整数, 如 2156648
, 用这个数对 16 取余, 返回值为 8. 那么就将他放到数组的第 8 个位置.
你可能会有下面的疑惑:
HashMap
的存储方式.总结: 适合用于等值查询, 不适用于范围查询. 出现大量哈希冲突的情况后, 查询效率会很低.
这个就更简单了, 将所有值从小到大排序, 这样查找时, 可以采用二分法, 时间复杂度只有 O(logN)
. 而且对范围查询的支持也非常好, 先根据二分法, 找到范围查询的左值, 然后依次遍历数组到范围查询的右值即可.
但这仅仅是查询效率很好, 但向数组中心插入值就麻烦了, 如现有数据 [1, 5, 8, 10, 11, 13]
, 现在要插入数据值 3
, 那么就要将 5, 8, 10, 11, 13
这些值都向后移动一位, 插入操作的效率为 O(N)
, 这并不是一个理想的效率.
适用于范围查询. 等值查询的效率也较高, 但插入操作效率较低.
二叉搜索树的特点是: 每个节点的左儿子小于父节点, 父节点又小于右儿子. 如:
1 | 5 |
这是如果你找值 7, 先从根节点 5 开始找, 比 5 大, 那肯定在右边, 所以找到第二层的 6, 比 6 还大, 找到第三次的 8, 比 8 小, 最后找到第四层的 7, 这样最坏的情况也就数据在树的最后一层, 即平均时间复杂度为 O(logN)
.
但是二叉搜索树还可能会出现一个问题是树不平衡, 如:
1 | 1 |
上面我们提高二叉搜索树的查询性能取决于树的高度, 现在这个数查询的性能变成了 O(N), 性能大打折扣. 为了解决这个问题, 提出了 红黑树
的概念. 他是一种自平衡的二叉搜索树, 即在插入节点时, 判断整个树是否是平衡的, 如果不平衡, 通过一系列旋转操作来达到平衡的目的, 在更新时维持树的平衡需要的时间复杂度也是 O(logN)
.
红黑树的篇幅有点长, 不太适合放到这里, 需要详细了解的朋友, 可自行查阅相关知识.
同样的, 平衡二叉树也有一个问题, 如当数据有 100 条时, 此时树的高度为 20, 那么一次查询可能需要访问 20 个数据块, 也就是访问 20 次磁盘, 这是不能被接受的, 尤其是在机械硬盘下, 为了让一个查询更少的读取磁盘, 我们就不应该使用二叉树, 而是 N 叉树, 这个 N 取决于数据块的大小.
以 InnoDB 的一个整数字段索引为例, 这个 N 差不多是 1200, 当树高为 4 时, 可以存储 1200 的三次方个值, 大约为 17 亿, 那么这样访问磁盘的次数就大大减少了.
InnoDB 中的 B+ 树, 就是 N 叉树的一种实现.
在 InnoDB 中, 表是根据主键顺序以索引的形式存放的, InnoDB 存储模型采用了 B+ 树索引模型, 在 InnoDB 中每一个索引都对应着一颗 B+ 树, 每棵非主键索引树的叶子节点存储的是主键的值, 每棵主键树的叶子节点存储的是整行数据值.
假如, 我们有一个主键列为 ID 的表, 表中有字段 K, 并且在 K 上有索引, 建表语句如下:
1 | create table T |
然后插入数据:
1 | insert into T(id, k, name) values(100, 1, "A"); |
两个树的示意图如下:
那么对于 主键索引和普通索引的查询有什么区别呢?
如查询语句 select * from T where ID = 500
, 即根据主键进行查询, 则只需要搜索 ID 索引树.
如查询语句 select * from T where K = 5
, 即根据普通索引进行查询, 则需要先搜索 K 索引树, 得到 ID 值 500, 再到 ID 索引树搜索. 这个过程称之为回表.
由于普通索引的叶子节点存储的是主键, 那么很显然, 主键长度越小越好, 所以自增主键是一个很好的选择.
当然, 如果你自己有业务字段是唯一的, 且不需要其他索引, 那么使用业务字段来做主键会适合.
还是上面那个查询语句: select * from T where K = 5
, 上面我们他会进行 回表 操作.
那么有没有可能经过索引优化, 不回表呢?
如果执行的语句是: select ID from T where K = 5
, 这时只需要查找 ID 值, 而 ID 值已经在 k 索引树的叶子节点中了, 这样已经得到了需要的数据, 就不用进行 回表 操作了.
在这个查询中, 索引 k 覆盖了我们需要查询的字段, 我们称之为 覆盖索引.
由于覆盖索引可以减少树的搜索次数,显著提升查询性能,所以使用覆盖索引是一个常用的性能优化手段。
当然, 我们不能为所有需要查询的字段都建立上 索引, 那索引就太多了, 并且索引的维护成本也很大, 其实 B+ 树 这种索引结构, 支持最左前缀匹配, 来定位记录.
如现在我们有 (name, age)
这个联合索引 :
可以看到, 索引已经按照定义中的顺序排序好了, name 在前, age 在后, 如果 name 一致, 按照 age 排序.
此时我们需要查找所有姓名等于 “张三” 的人, 可以快速定位到 ID4, 然后向后遍历直到姓名不等于张三.
那么如果你要查找的是所有姓名的第一个字等于 “张” 的人, 也可以快速定位到 ID3, 然后向后遍历直到不符合条件.
同理, 如现在有联合索引 (name, age, score)
, 那么我们查询 where name = '%张' and age = 10
也是用到了索引的, 但查询 where name = '%张' and score = 60
, 就没有完全用到这个索引.
由此可知, 我们只要满足索引的最左前缀, 就可以用索引来加速检索, 这个最左前缀可以是联合索引的最左 N
个字段, 也可以是字符串索引的前 M
个字符.
上一段我们提到了最左前缀可以用来在索引中定位记录, 但如果不符合最左前缀的部分, 应该怎么办呢?
还是以 (name, age)
联合索引为例. 现在需要查询 “名字第一个字是张, 年龄为 10 的男孩”, sql 示意如下:
1 | select * from tuser where name like '张%' and age = 10 and ismale = 1; |
再次附上索引结构图:
这个索引只能用到 name 的前缀索引, 找到第一个满足条件的记录 ID3, 然后, 如何判断后面两个条件是否满足呢?
在 MySQL 5.6 之前, 只能从 ID3 开始一个一个的回表, 到主键索引上找出数据行, 再比对字段值.
而在 MySQL 5.6 引入了索引下推优化, 即在索引遍历过程中, 对索引中包含的字段先做判断, 先过滤到不符合条件的记录, 避免回表:
无索引下推执行流程:
有索引下推执行流程:
每个虚线表示回表一次, 在无索引下推图中, 我特意去掉了 age 值, 因为他不会看 age 的值, 只是按顺序把 name 第一个字是 “张” 的所有数据一个一个回表, 因此回表了 4 次.
而在有索引下推时, InnoDB 在 (name, age)
索引内部就判断了 age 是否等于 10, 对于不等于的, 直接跳过, 所以这里只回表了 2 次.
InnoDB
存储引擎是以页为单位来管理存储空间的, 我们的增删改查操作本质上都是在访问页面, 如读取一条数据, 会把这个数据所在的页加载到内存中, 而不仅仅是这条数据本身, 这个页的默认大小是 16KB
.
在事务中, 我们有一个特性: 持久性, 指对于一个已提交的事务, 在事务提交后, 即使系统崩溃, 也要保证这个事务对数据库做的更改不会丢失, 那么我们如何保证这一点呢, 有一个简单粗暴的做法就是: 在事务提交之前, 将事务所修改的所有页面都刷新到磁盘, 但这种做法有几个问题:
InnoDB
所有操作都是基于页面的, 我们只修改一个字节就要刷新一个 16KB 的页到磁盘上台浪费了.那么如何解决这个问题呢, InnoDB
采用了 redo log 机制来解决:
MySQL 拿到一个查询请求后,会先看看之前有没有执行过这条语句,如果执行过,则直接从查询缓存中取之前查询的结果即可,但大多情况不建议使用 MySQL 的查询缓存,因为弊大于利。
因为查询缓存的失效非常频繁,只要对一个表进行更新,那么这个表的所有查询缓存将会全部被清除,所以命中率并不会很好,除非你有一张静态的表,不会改变他的数据,或者很久才会更新一次。比如系统配置表,才适合使用这个查询缓存。
还有一个原因是因为,现在有 Redis
, MemoryCache
等专门用来做缓存的应用,他们对缓存的处理会更优,而且 MySQL 服务器的资源通常都比较宝贵,所以不推荐使用 MySQL 的查询缓存。
查看查询缓存状态:
1 | show variables like '%query_cache_type%'; |
显式指定使用查询缓存:
1 | select SQL_CACHE * FROM user where ID = 10; |