数据结构与算法学习⑨
数据结构与算法学习⑨
B树
概念
B树(B-tree,B-树):它是一种平衡的多路搜索树,多用于文件系统,数据库的实现
m阶B树的含义:B树中每个节点最多有m个子节点
B树的特点:
1、B树每个节点可以存储超过2个元素,可以拥有超过2个子节点,即多路,多叉
2、B树拥有二叉搜索树的一些性质
3、B树非常平衡,每个节点所有子树的高度均一样
4、B树的高度非常低,即B树很矮 m
5、B树的叶子节点都在同一层(有的定义中叶子节点指的是不包含任何信息的外部空节点,指向它们的指针为null)
m阶B树的性质(m≥2)
B树的搜索
搜索和二叉搜索树的搜索类似
1.先再节点内部从小到大搜索元素
2.如果命中,搜索结束
3.如果未命中,再去对应的子节点中搜索元素,回到步骤一
B树的添加
明确一点,新元素的添加必定再最后一层的叶子节点中
根节点:1≤x≤m-1
非根节点 :⌈ m/2 ⌉ - 1 ≤ x ≤ m-1
由于现在是4阶B树,每个节点内部元素个数最多为3个 35加入后会导致20右子节点内部元素为4,不符合B树 的定义
这种节点内元素个数超过上限的现象叫做:上溢(overflow)
添加-上溢的解决
假设现在是5阶B树,某个节点发生了上溢,那么该节点的元素个数必然是5个
解决方案
1、假设上溢节点最中间元素的位置为k,将k位置的元素向上与父节点合并
2、将[0,k-1]和[k+1,m-1]位置的元素分裂成2个子节点,两个子节点的元素个数也必然不会低于最低限制:⌈ m/2 ⌉ - 1
3、一次分裂完成后,可能导致父节点上溢,只需按照上述方法继续解决,最极端情况是一直分裂到根节点, 根节点上溢分裂会导致B树长高一层
上溢节点是溢出的节点
B树的删除
删除的元素再叶子节点中:直接删除即可
删除的元素在非叶子节中
1、找到前驱或后继元素,覆盖要被删除的元素
2、再把用以覆盖的前驱或后继元素删除 p 非叶子节点的前驱或后继,必定在叶子节点中 所以删除前驱或后继就回到了删除的元素在叶子中 的情况
40的前驱是35后继是50
由于是一棵5阶B树,非根节点中元素个数x的范围是:2 ≤ x ≤ 4,删除15之后破坏了这个性质
这种节点内元素个数低于下限的现象叫做:下溢(underflow)
删除-下溢的解决
明确一点:下溢节点的元素数量必然等于:⌈ m/2 ⌉ - 2
解决方案:看临近的兄弟节点是否能借一个元素,兄弟节点元素个数如果≥⌈ m/2 ⌉ ,则可以向外借一个元素
1、因此将父节点中的b元素插入到下溢节点的0位置(最小位置), 2、用兄弟节点中的元素a(最大元素)替代父节点中的元素b,如果a原本有右边的子节点则将它作为b左边的子节点 p
这种操作其实就是旋转
(因为b比a大,借的话需要把b下来,a上去,d放在原来的另一边)
解决方案:看临近的兄弟节点是否能借一个元素,兄弟节点元素个数如果为⌈ m/2 ⌉ -1,则无法向外借一个元素
1、将父节点中的元素b挪下来和左右两个子节点合并(此时父节点只是少了个元素)
2、合并后的节点元素个数等于:⌈ m/2 ⌉ -1 + 1 + ⌈ m/2 ⌉ -2 ,不超过m -1
3、这个操作可能会导致父节点下溢,此时依然按照上述方案进行操作,下溢现象可能会一直往上传播,极端情况 会传播到根节点,由于根节点既没有父节点也没有兄弟节点,所以最坏的情况原本的根节点下去跟它的子节点合并 成新的根节点,此时整个B树会变矮
红黑树和4阶B树的等价变换
把黑色当作一个层,链接红色,兄弟节点在同一层
位运算
入门
计算机中的数在内存中都是以二进制形式进行存储的,用位运算就是直接对整数在内存中的二进制位进行操作, 因此其执行效率非常高,在程序中尽量使用位运算进行操作,这会大大提高程序的性能。
常见的位运算符
有符号数&无符号数
场景:假设用1字节8位二进制表示一个数,那么8可以表示为:0000 1000,请问-8应该如何表示?
日常书写表示:负数其实就是正数前面加了一个负号
计算机内部表示:将二进制数的最高位(第一位)规定为符号位,用0代表正数,1代表负数,就能够表示负数了 这样八位二进制数中就分成了第一位符号位和后七位数值位
结论:有符号数中最高位用于表示正负,无符号数中,所有的位都用于直接表示该值的大小
计算机如何存储负数
负数用补码表示:补码=反码+1
知识小贴士:已知一个数的补码,求原码的操作分两种情况:
(1)如果补码的符号位为“0”,表示是一个正数,所以补码就是该数的原码。
(2)如果补码的符号位为“1”,表示是一个负数,求原码的操作可以是:符号位为1, 其余各位取反,然后再整个数加1。
以-1为例,原码10000001,反码11111110,补码11111111。
常见位运算的骚操作
面试题
191. 位1的个数
解法一:
位移+计数 n & 1 ==1:最低位是1,计数器+1
n>>1,继续判断
移位32次结束(java中int用4字节表示)
public class Solution {
// you need to treat n as an unsigned value
public int hammingWeight(int n) {
int count=0;
for(int i=0;i<32;i++){
if((n&1)==1){
count++;
}
n>>=1;
}
return count;
}
}
解法二:
n & (n−1)消去最低位的1 n & (n−1),可消去n的二进制表示中最后一位1, 消去1次,计数器+1
当n==0时,所有的1都被置为了0,结束
public class Solution {
// you need to treat n as an unsigned value
public int hammingWeight(int n) {
int count=0;
while(n!=0){
count++;
n=n&(n-1);
}
return count;
}
}
231. 2的幂
知识点:如果是2的幂次方则只会有一个二进制位为1,则可以借助 n & (n-1) 的结果是否为0
231. 2的幂
class Solution {
public boolean isPowerOfTwo(int n) {
if(n<=0){
return false;
}
return (n&(n-1))==0;
}
}
190. 颠倒二进制位
解法一:取模求和
十进制:ans = ans * 10 + n % 10; n = n / 10;
二进制:ans = ans * 2 + n % 2; n = n / 2;
public class Solution {
// you need treat n as an unsigned value
public int reverseBits(int n) {
int ret=0;
for(int i=0;i<32;i++){
ret=(ret<<1)+(n&1);
n=n>>1;
}
return ret;
}
}
解法二:按位翻转
public class Solution {
// you need treat n as an unsigned value
public int reverseBits(int n) {
int res=0;
for(int i=0;i<32;i++){
res^=(n&(1<<i))!=0?(1<<(31-i)):0;
}
return res;
}
}
52. N皇后 II
N皇后 II
位运算解法:此类问题的终极解法
class Solution {
int count=0;
public int totalNQueens(int n) {
dfs(n,0,0,0,0);
return count;
}
public void dfs(int n,int row,int col,int pie,int na){
if(n==row){
count++;
return;
}
//算出棋盘当前行还有哪些位置可以放置皇后由 0 变成 1,以便进行后续的位遍历 i
int bits=~(col|pie|na)&((1<<n)-1);
while(bits>0){
//取第一个1
int mark=bits&(-bits);
dfs(n,row+1,col|mark,(pie|mark)>>1,(na|mark)<<1);
bits&=bits-1;
}
}
}
布隆过滤器
为什么要使用布隆过滤器
1、在 FBI,如何判断一个嫌疑人的名字是否已经在嫌疑名单上?
2、在网络爬虫里,如何判断一个网址是否被访问过?
3、反垃圾邮件功能中,如何从数十亿个垃圾邮件列表中判断某邮件是否垃圾邮件?
4、…
共同的特点: 如何判断一个元素(关键字)是否存在一个集合中?
用什么数据结构存储关键字?
数组 链表 BST Trie map(红黑树) 哈希表 …
思考1:数组,链表,树等数据结构配合常见的排序、二分搜索 可以解决大部分需求 但是当集合里面的元素数量足够大,如果有500万条记录甚至1亿条记录呢?
思考2:哈希表效率很高,查询效率可以达到O(1),判断关键字是否在集合中非常适合 数组、链表、树等数据结构会存储元素的内容,一旦数据量过大,消耗的内存也会呈现线性增长,最终达到瓶颈
但是哈希表需要消耗的内存依然很高。使用哈希表判断一亿个垃圾邮件中某邮件是否垃圾邮件,它毕竟需要将这 一亿条数据存储起来,耗费的空间资源是比较多的。
总之:希望找一种判断效率既高且耗费存储少的方案
什么是布隆过滤器?
布隆过滤器(Bloom Filter)是1970年由布隆提出的。它实际上是一个很长的二进制向量和一系列随机映射函数。 布隆过滤器可以用于检索一个元素是否在一个集合中。它的优点是空间效率和查询时间都远远超过一般的算法, 缺点是有一定的误识别率和删除困难。
很长的二进制向量:二进制数组 不存储关键字数据,只用二进制位表明在还是不在集合中
优势:非常节省存储空间,而且基于数组访问效率高
哈希表中的哈希函数
一系列随机函数:哈希函数
布隆过滤器中的哈希函数
为什么布隆过滤器删除困难?
应用场景举例
1、比特币网络:它是分布式系统,比如要判断某地址是否在某节点
2、分布式系统:Map-Reduce,Hadoop,Search Engine;判断数据是否 在某节点中
3、Redis缓存:解决缓存穿透
4、垃圾邮件,评论过滤等
5.作为Redis的防弹衣,防止缓存穿透,先去布隆过滤器查没有直接返回,有去redis查,再没有才会去数据库查
Cache缓存
缓存三要素
缓存的三个要素
大小(体积) 什么数据放缓存:
空间大:可以将所需要的数据都放缓存
空间小:放一些热数据
替换策略 分级架构的缓存,某级空间不够存储了,用什么策略决定哪些数据被替换(降级)
缓存空间不够了,用什么策略决定应该淘汰哪些数据
工业级缓存替换策略:https://en.wikipedia.org/wiki/Cache_replacement_policies
复杂度要求快速高效,查询尽量是O(1)
LRU
LRU(Least Recently Used):该算法策略首先丢弃最近最少使用的数据
底层数据结构:Hash Table + Double LinkedList
1、双向链表按照被使用的顺序存储了这些键值对,靠近头部的是最近使用的,靠近尾部的是最久未使用的。
2、哈希表即为普通的哈希映射(比如Java中的HashMap),通过缓存数据的键映射到其在双向链表中的位置。
LRU-GET
通过哈希表定位到该节点在双向链表中的位置,并将其移动到双向链表的头部,最后返回该节点的值
LRT-PUT
LRU复杂度分析
时间复杂度
访问哈希表的时间复杂度为 O(1)
在双向链表的头部添加节点、在双向链表的尾部删除节点的复杂度也为 O(1)
将一个节点移到双向链表的头部,可以分成「删除该节点」和「在双向链表的头部添加节点」两步操作, 都可以在 O(1)时间内完成。
结论:查询,添加,修改等操作的时间复杂度均为O(1)
面试题
class LRUCache {
HashMap<Integer,Node>map;
int capacity;
int size;
Node head;
Node tail;
public LRUCache(int capacity) {
map=new HashMap<Integer,Node>();
this.capacity=capacity;
this.size=0;
head=new Node();
tail=new Node();
head.next=tail;
tail.prev=head;
}
public int get(int key) {
Node node=map.get(key);
if(node==null){
return -1;
}
//如果有的话,需要把节点放到开头
removeToHead(node);
return node.value;
}
public void removeToHead(Node node){
removeNode(node);
addToHead(node);
}
public void removeNode(Node node){
Node p=node.prev;
Node t=node.next;
p.next=t;
t.prev=p;
node.prev=null;
node.next=null;
}
public void addToHead(Node node){
Node firstNode=head.next;
head.next=node;
node.next=firstNode;
firstNode.prev=node;
node.prev=head;
}
public void put(int key, int value) {
Node node=map.get(key);
if(node!=null){
node.value=value;
removeToHead(node);
return;
}else{
node=new Node(key,value);
map.put(key,node);
addToHead(node);
size++;
}
if(this.size>this.capacity){
Node n=removeTail();
map.remove(n.key);
size--;
}
}
public Node removeTail(){
Node node=tail.prev;
Node last=node.prev;
last.next=tail;
tail.prev=last;
node.next=null;
node.prev=null;
return node;
}
class Node{
int key;
int value;
Node prev;
Node next;
public Node(){};
public Node(int key,int value){
this.key=key;
this.value=value;
}
}
}
/**
* Your LRUCache object will be instantiated and called as such:
* LRUCache obj = new LRUCache(capacity);
* int param_1 = obj.get(key);
* obj.put(key,value);
*/
class LFUCache {
Map<Integer, Node> cache; // 存储缓存的内容
Map<Integer, LinkedHashSet<Node>> freqMap; // 存储每个频次对应的双向链表
int size;
int capacity;
int min; // 存储当前最小频次
public LFUCache(int capacity) {
cache = new HashMap<> (capacity);
freqMap = new HashMap<>();
this.capacity = capacity;
}
public int get(int key) {
Node node = cache.get(key);
if (node == null) {
return -1;
}
freqInc(node);
return node.value;
}
public void put(int key, int value) {
if (capacity == 0) {
return;
}
Node node = cache.get(key);
if (node != null) {
node.value = value;
freqInc(node);
} else {
if (size == capacity) {
Node deadNode = removeNode();
cache.remove(deadNode.key);
size--;
}
Node newNode = new Node(key, value);
cache.put(key, newNode);
addNode(newNode);
size++;
}
}
void freqInc(Node node) {
// 从原freq对应的链表里移除, 并更新min
int freq = node.freq;
LinkedHashSet<Node> set = freqMap.get(freq);
set.remove(node);
if (freq == min && set.size() == 0) {
min = freq + 1;
}
// 加入新freq对应的链表
node.freq++;
LinkedHashSet<Node> newSet = freqMap.get(freq + 1);
if (newSet == null) {
newSet = new LinkedHashSet<>();
freqMap.put(freq + 1, newSet);
}
newSet.add(node);
}
void addNode(Node node) {
LinkedHashSet<Node> set = freqMap.get(1);
if (set == null) {
set = new LinkedHashSet<>();
freqMap.put(1, set);
}
set.add(node);
min = 1;
}
Node removeNode() {
LinkedHashSet<Node> set = freqMap.get(min);
Node deadNode = set.iterator().next();
set.remove(deadNode);
return deadNode;
}
}
class Node {
int key;
int value;
int freq = 1;
public Node() {}
public Node(int key, int value) {
this.key = key;
this.value = value;
}
}