赞
踩
数据结构的逻辑结构描述数据元素之间的逻辑关系,与数据存储无关,独立于计算机。每种逻辑结构可以使用不同的存储方式。
java中提供了一些集合的实现,如LinkedList实现了线性表的双向链表存储结构、Stack(Vector的子类)实现了栈的结构,由于Vector的底层就是Object[]数组,因此Stack的底层也是Object[]数组实现的,即使用顺序存储,而非链式存储
还有Java的一些集合底层也用到了这样的逻辑结构。
在此说明:Stack有其体现栈结构的特殊操作,但是用其父类的add()方法也不是不可以!
1、基本概念
总结:若干数据项→数据元素(数据基本单位)–相同性质集合—>数据对象
2、数据结构:是相互之间存在一种或多种特定关系的数据元素的集合。这种数据元素相互之间的关系称为结构。数据结构的三要素:逻辑结构、物理结构、数据的运算。一个算法的设计取决于所选定的逻辑结构,算法的实现依赖于所采用的存储结构。
线性结构:一对一。如线性表、栈、队列、串、数组
集合结构:同属于一个集合,集合之间没有逻辑关系。如各种各样的集合
树形结构:一对多。如树、二叉树
图形结构:多对多。如各种各样的图
数据元素、关系
的表示,用计算机语言实现,依赖于计算机语言节点
信息外,还建立附加的索引表
。索引表的每一项称为索引项,索引的一般形式:关键字、地址。
不支持排序
,占用空间更多,记录的关键字不能重复
3、算法和算法评价
算法:对特定问题求解步骤的一种描述,是指令的有限序列
,其中的每条指令表示一个或多个操作
算法特性:出入可行,确定很穷
算法"好"目标:正确性(求解问题)、可读性、健壮性(非法数据)、效率与低存储量需求
算法效率的度量:
时间复杂度:一个语句的频度是指该语句在算法中被重复执行的次数。算法中所有语句的频度之和T(n)——该算法问题规模n的函数,时间复杂度分析它的数量级。因此采用算法中基本运算的频度f(n)来分析
T(n)=O(f(n))
由于时间复杂度与问题规模、输入数据的性质有关:最好、最坏、平均时间复杂度
空间复杂度:S(n)=O(g(n)),算法所耗费的存储空间,一个程序执行过程中除了需要存储空间存放本身所用的指令、常数、变量、输入数据外,还需要对数据进行操作的工作单元和存储一些为实现计算所需信息的辅助空间。若输入数据所占空间只取决于问题本身,与算法无关,那么只分析除输入和程序之外的额外空间。
算法原地工作:指算法所需的辅助空间为常量,即O(1)。
每种数据结构需要根据它的特点→使用不同的物理结构实现,定义不同的结构体,以及相关的操作
但是Java也提供了一些逻辑结构的物理实现可以直接用:LinkedList、ArrayList、Stack、TreeSet、TreeMap
1、线性表:由n个元素组成的一个有限序列,表中的每一个元素有且只有一个前驱、后继,除根/终结点外
特点:元素个数有限、逻辑上顺序性、数据类型相同、元素具有抽象性(仅考虑元素关系,不考虑是什么内容)、元素都是数据元素,每个元素都是单个元素
(1)顺序存储——顺序表:用一组地址连续的存储单元一次存储线性表中的数据元素,使得逻辑和物理都相邻
下面的特点以数组为例:用来存放同一种数据类型的集合,注意只能存放同一种数据类型。
特点:提供随机访问 并且容量有限。可以利用元素的索引(index)可以计算出该元素对应的存储地址。
假如数组的长度为 n。
访问:O(1)//访问特定位置的元素
插入:O(n )//最坏的情况发生在插入发生在数组的首部并需要移动所有元素时
删除:O(n)//最坏的情况发生在删除数组的开头发生并需要移动第一元素后面所有的元素时
//只声明了类型和长度
数据类型[] 数组名称 = new 数据类型[数组长度];
//声明了类型,初始化赋值,大小由元素个数决定
数据类型[] 数组名称 = {数组元素1,数组元素2,......}
自定义数组Array:底层使用Object[]数组实现
//测试几个功能 @Test public void test1() { Array<String> arr = new Array<>(5); arr.add("123"); arr.add("345"); arr.add("zhang"); arr.delete("123"); arr.update("345","789"); arr.print(); } //数组实现 class Array<E>{ private Object[] elementData;//存储数据的数组 private int size; //从构造器就可以看出Array数组必须要指定初始容量,并没有提供无参构造器 public Array(int capacity){ elementData = new Object[capacity]; } public int size(){//湖区数组的大小 return this.size; } //实现添加元素功能:满了就加不了了 public void add(E value){ if(size >= elementData.length){//由于数组的不可变性,已满就不能添加 throw new RuntimeException("数组已满,不可添加"); } elementData[size] = value; size++; } //查询元素value在数组中的索引位置 public int find(E value){ for (int i = 0; i < size; i++) { if(elementData[i] == value){ return i; } } return -1;//返回-1表示不存在 } //从数组中移除首次出现的value元素 public boolean delete(E value){ int index = find(value); if(index == -1){ return false; } for (int i = index; i < size; i++) { elementData[i] = elementData[i+1]; } elementData[size - 1] = null;//最后一个置为默认值 size--;//别忘了size要减1 return true; } //将数组中首次出现的oldValue元素替换为newValue public boolean update(E oldValue, E newValue){ int index = find(oldValue); if(index == -1){ return false; } elementData[index] = newValue; return true; } //遍历数组中所有的数据 public void print(){ System.out.print("{"); for (int i = 0; i < size; i++) { if(i == size - 1){ System.out.print(elementData[i] + "}"); break; } System.out.print(elementData[i] + ","); } } }
(2)链式存储——链表:通过一组任意的存储单元来存储线性表中的数据元素。为了建立数据元素之间的线性关系,对每个链表节点存放数据域、指针域(存放后继节点的地址)。链表由节点组成。
注意:各种链表的删除、插入操作,指针的修改!
链表的几种形式:单链表、双链表、循环链表。第一个结点叫作头结点,链表通常有一个不保存任何值的 head 节点(头结点),通过头结点我们可以遍历整个链表。尾结点通常指向 null。
插入删除O(1),查找O(n)
假如链表中有n个元素。
访问:O(n)//访问特定位置的元素
插入删除:O(1)//必须要要知道插入元素的位置
自定义节点(数据域、指针域),自定义单链表结构及一些相关的功能——这里定义的单链表,头结点保存数据了
通过一个一个节点新建链表有头插法、尾插法
说明:对于需要操作的数据data,需要包装成节点Node
//构建节点对象:data、next class Node<E>{//使用泛型实现 E data;//存放节点的值 Node next;//存放下一个节点的地址 Node(E data, Node next){//新建节点的时候用 this.data = data; this.next = next; } } //构成单链表 //测试单独的节点的操作:添加,已知头结点的指针 @Test public void test3() { Node node1 = new Node("String",null);//新建节点 Node node2 = new Node("zzz",null);//新建节点 node1.next = node2; } //还可以构建节点:每次新建一个节点,自己赋值data、next class Node2<E>{//使用泛型实现 E data;//存放节点的值 Node next;//存放下一个节点的地址 Node(){//默认的无参构造器 } } //单链表类:实现add() class Link<E>{ Node header;//头结点,初始化为null private int size = 0;//初始化为0 public int size(){ return this.size; } //向链表中添加元素:尾插法 void add(E data){//第一次调用add方法时,header为null Node addNode = new Node(data,null);//new一个节点对象,他是一个待插入当前链表中的节点 if(header == null){//说明还没有节点 header = addNode; }else{//头结点不为空,通过头结点找到当前的末尾节点,让当前末尾节点的next是新结点 Node currentLastNode = findLast(header); currentLastNode.next = addNode; } size++;//每添加一个元素后,将size加1,可以获取到size的大小 } //专门查找尾节点的方法,这个方法可以直接调用 Node findLast(Node node){ //递归出口:如果一个节点的next是null表示他就是尾节点 if(node.next == null){//说明这个节点就是尾节点 return node; } //能到这里说明:node不是尾节点 return findLast(node);//递归算法 } /*// 删除链表中某个数据的方法 public void remove(Object obj){ //略 } // 修改链表中某个数据的方法 public void modify(Object newObj){ //略 }*/ // 按值查找:查找链表中某个元素的方法。——不太准确 public int find(E value){ Node p = header;//头节点 //因为建立单链表是从头结点开始存的 while(true){ if(p.data == value){ return p; } p = p.next; } } }
调用示例
//测试单链表 @Test public void test1() { //新建单链表:初始化seize=0,头结点header为null Link<String> link = new Link<>(); link.add("zhangying"); link.add("shi"); } //测试单独的节点的操作:添加,已知头结点的指针 @Test public void test2() { Node addNode = new Node("String",null);//新建节点 Node p = header;//从头节点开始 //找到尾节点插入addNode节点 while(p.next != null){ p = p.next; } //循环结束找到p.next为null的尾节点了 p.next = addNode;//在尾节点插入addNde节点 }
双向链表实现——java中提供了LinkedList实现双向链表,这相当于他的底层源码
这里的双向链表:头结点就是第一个节点。
注意:二叉树也是这样的结构:有两个指针域
class Node<E>{ Node Prev; E data; Node next; Node(Node prev,E data, Node next){ this.Prev = prev; this.data = data; this.next = next; } } //测试构成双链表的结构 @Test public void test1(){ Node node1 = new Node(null,"AA",null); Node node2 = new Node(node1,"AA",null); Node node3 = new Node(node2,"AA",null); node1.next = node2; node2.next = node3;//构成双向的 } //实现双向链表类 class MyLinked<E> implements Iterable<E> { private Node first;//链表的首元素 private Node last;//链表的尾元素 private int total;//计算链表的长度 //实现增加元素对象的功能:头插法 public void add(E e) { //该节点的前驱指向前一个结点 Node newNode = new Node(last, e, null);//先把节点构建出来,该节点是添加到最后,因此后继节点为null if (first == null) {//判断该链表是否为空 first = newNode;//如果为空就将这个节点作为头结点 }else{ last.next = newNode;//如果不为空就指向新结点 } last = newNode;//改变最后一个节点的位置 } public void delete(E obj){ Node find = findNode(obj); if(find != null){ if(find.prev != null){ find.prev.next = find.next; }else{ first = find.next; } if(find.next != null){ find.next.prev = find.prev; }else{ last = find.prev; } find.prev = null; find.next = null; find.data = null; total--; } } private Node findNode(E obj){ Node node = first; Node find = null; if(obj == null){ while(node != null){ if(node.data == null){ find = node; break; } node = node.next; } }else{ while(node != null){ if(obj.equals(node.data)){ find = node; break; } node = node.next; } } return find; } public boolean contains(Object obj){ return findNode(obj) != null; } public void update(E old, E value){ Node find = findNode(old); if(find != null){ find.data = value; } } @Override public Iterator<E> iterator() { return new Itr(); } private class Itr implements Iterator<E>{ private Node<E> node = first; @Override public boolean hasNext() { return node!=null; } @Override public E next() { E value = node.data; node = node.next; return value; } } }
2、数组和链表对比
数组支持随机访问,而链表不支持。
数组使用的是连续内存空间对 CPU 的缓存机制友好,链表则相反。
数组的大小固定,而链表则天然支持动态扩容。如果声明的数组过小,需要另外申请一个更大的内存空间存放数组元素,然后将原数组拷贝进去,这个操作是比较耗时的!
1、栈:又称为堆栈或堆叠,只允许在有序的线性数据集合的一端(称为栈顶 top)进行加入数据(push)和移除数据(pop)。因而按照 后进先出(LIFO, Last In First Out) 的原理运作。在栈中,push 和 pop 的操作都发生在栈顶。
核心类库中的栈结构有Stack和LinkedList。其中Stack就是顺序栈,它是Vector的子类。LinkedList是链式栈。
顺序存储:顺序栈;链式存储:链式栈
体现栈结构的操作方法:peek()查看栈顶元素、pop()弹出栈、push(E e)压入栈、size()大小、empty()判空、search(E e)查看元素出现位置、clear()清栈
假设堆栈中有n个元素。
访问/搜索:O(n)//最坏情况
插入删除:O(1)//顶端插入和删除元素
2、栈的一些操作:栈顶指针是指向栈顶元素的
栈顶指针:s.top,初始设置为s.top=-1,栈顶元素s.data[s.top](链式取值方式)
进栈push(E e):栈顶指针(索引)先+1,再添加元素
出栈pop():先取栈顶值,再栈顶指针(索引)-1。
判空:s.top==-1,栈满s.top==maxsize-1;栈长:stop+1。
3、关于共享栈:利用栈底位置相对不变的特性,让两个顺序栈共享一个一维数组空间,栈底分别设置在共享空间的两端,两个栈顶向共享空间的中间延伸
栈空:top0=-1时0号空,top1=maxsize时1号空,
栈满:(top1-top0)=1时,为满
进栈:0号栈top0+1再赋值,1号栈top1-1再赋值;出栈相反
4、栈的应用场景:只涉及在一端插入和删除数据,并且满足后进先出
(1)实现浏览器的回退和前进功能
(2)检查符号是否成对出现:
给定一个只包括 ‘(’,‘)’,‘{’,‘}’,‘[’,‘]’ 的字符串,判断该字符串是否有效。有效字符串需满足:左括号必须用相同类型的右括号闭合。左括号必须以正确的顺序闭合。比如 “()”、“()[]{}”、“{[]}” 都是有效字符串,而 “(]”、“([)]” 则不是。
(3)反转字符串
(4)子程序的调用、维护函数调用、递归调用(FIB、Honi)
(5)深度优先遍历DFS
(6)CPU的中断处理
栈的实现:可以通过数组、链表实现,无论哪种方式:入栈/出栈(即插入删除)的复杂度都为O(1)
每次入站之前需要先判断栈的容量是否够用,不够的话就用Arrays.copyOf()扩容
实现功能:push(Object e)、ensureCapacity()、pop()、peek()、peek()、isEmpty()、size()
//自定义栈:顺序栈 class MyStack{ private Object[] elements;//用来存放元素 private int index;//用来记录索引 private int capacity;//记录栈的容量 private static final int GROW_FACTOR = 2;//全局静态常量,每次自动扩容2倍 public MyStack(){ this.capacity = 8; //默认初始化容量是10 this.elements = new Object[8]; index = -1;//表示当前没有元素 } //入栈操作 public void push(Object obj){ if(index == elements.length){ //扩容 ensureCapacity(); } index++;//先移动栈顶指针/索引,再添加元素 elements[index] = obj;//每次栈顶指针指向栈顶元素 } //扩容:只给自己内部使用,外部无需调用,每次扩容2倍 private void ensureCapacity(){ int newCapacity = capacity * GROW_FACTOR; elements = Arrays.copyOf(elements, newCapacity); capacity = newCapacity; } //出栈:每取出一个元素,栈帧向下移动一位 public Object pop() throws Exception{ if(index < 0){ //栈空抛出异常 throw new Exception("弹栈失败,栈已空!"); } Object obj = elements[index];//先取栈顶值,再移动栈顶指针 elements[index] = null; index--; return obj; } }
java中实现栈的集合的操作:LinkedList、Stack
public class TestStack { //测试Stack @Test public void test1(){ Stack<Integer> list = new Stack<>(); list.push(1); list.push(2); list.push(3); System.out.println("list = " + list); System.out.println("list.peek()=" + list.peek()); System.out.println("list.peek()=" + list.peek()); System.out.println("list.peek()=" + list.peek()); System.out.println("list.pop() =" + list.pop()); System.out.println("list.pop() =" + list.pop());//java.util.NoSuchElementException while(!list.empty()){ System.out.println("list.pop() =" + list.pop()); } } //测试LinkedList @Test public void test2(){ LinkedList<Integer> list = new LinkedList<>(); list.push(1); list.push(2); list.push(3); System.out.println("list = " + list); System.out.println("list.peek()=" + list.peek()); System.out.println("list.pop() =" + list.pop()); System.out.println("list.pop() =" + list.pop());//java.util.NoSuchElementException while(!list.isEmpty()){ System.out.println("list.pop() =" + list.pop()); } } }
1、队列(Queue):先进先出 (FIFO,First In, First Out) 的线性表。在具体应用中通常用链表或者数组来实现,用数组实现的队列叫作 顺序队列 ,用链表实现的队列叫作 链式队列 。队列只允许在后端(rear)进行插入操作也就是入队 enqueue,在前端(front)进行删除操作也就是出队 dequeue。
假设队列中有n个元素。
访问:O(n)//最坏情况
插入删除:O(1)//后端插入前端删除元素
2、队列常见的一些操作:队尾指针rear指向队尾元素的下一个位置,队头指向队头元素(不同的地方要求可能不一样,根据实际情况实现入队、出队)→为了避免当只有一个元素时处理麻烦就这样涉及,当rear=font时队列为空。
入队EnQueue(E e):从队尾入,先送值到队尾,再将队尾指针+1
出队DeQueue();从队头出,先取队头值,再将队头指针+1
“假溢出”:Q.rear = = maxsize,即出现上溢出,data数组中任然存在可以存放元素的空位置→解决办法循环队列
3、循环队列:解决顺序队列和假溢出问题,即从头开始,形成头尾相接的循环
解决法1:设计队列时保证数组还有一个空闲的位置(即队尾指针指向队尾元素的下一个位置)
判断队列是否为满:(rear+1) % queueSize = = front;判断队空:front = = rear
解决法2:设置一个标志变量flag
队满:rear==front,flag=1;队空flas=0。
4、双端队列双端队列 (Deque) :一种在队列的两端都可以进行插入和删除操作的队列,相比单队列来说更加灵活。一般来说,我们可以对双端队列进行 addFirst、addLast、removeFirst 和 removeLast 操作。
5、优先队列:从底层结构上来讲并非线性的数据结构,它一般是由堆来实现的。
入队:将新元素其插入堆中并调整堆;队头出队:返回堆顶元素并调整堆。
不论进行什么操作,优先队列都能按照某种排序方式进行一系列堆的相关操作,从而保证整个集合的有序性。虽然优先队列的底层并非严格的线性结构,但是在使用的过程中,是感知不到堆的,从使用者的眼中优先队列可以被认为是一种线性的数据结构:一种会自动排序的线性队列。
6、队列的应用场景
(1)图形的广度优先查找法(BFS)
(2)CPU的作业调度、阻塞队列、线程池中的请求/任务队列、Linux内核进程队列
(3)用于计算机各种事件处理的模拟,如事件队列、消息队列
(4)栈:双端队列天生便可以实现栈的全部功能(push、pop 和 peek),并且在 Deque 接口中已经实现了相关方法。Stack 类已经和 Vector 一样被遗弃,现在在 Java 中普遍使用双端队列(Deque)来实现栈。
(5)层次遍历
实现队列的定义及相关操作——使用数组实现
class Queue { Object[] values; int size;//记录存储的元素的个数 public Queue(int length) { values = new Object[length]; } public void add(Object ele) { //添加 if (size >= values.length) { throw new RuntimeException("队列已满,添加失败"); } values[size] = ele; size++; } public Object get() { //获取 if (size <= 0) { throw new RuntimeException("队列已空,获取失败"); } Object obj = values[0]; //数据前移 for (int i = 0; i < size - 1; i++) { values[i] = values[i + 1]; } //最后一个元素置空 values[size-1]= null; size--; return obj; } }
1、一些概念
结点
:树中的数据元素都称之为结点;
根节点
:最上面的结点称之为根。每个结点都可以认为是其子树的根
父节点
:结点的上层结点;子节点
:节点的下层结点;兄弟节点
:具有相同父节点的结点称为兄弟节点
结点的度数
:每个结点所拥有的子树的个数称之为结点的度
树叶
:度数为0的结点,也叫作终端结点
非终端节点(或分支节点)
:树叶以外的节点,或度数不为0的节点。
树的深度(或高度)
:树中结点的最大层次数
结点的层数
:从根节点到树中某结点所经路径上的分支树称为该结点的层数,根节点的层数规定为1,其余结点的层数等于其父亲结点的层数+1
同代
:在同一棵树中具有相同层数的节点
2、树的性质:
树中节点数等于所有节点的度数之和加1
度为m的树中第i层上最多mi-1 个节点(i>=1)
高为h的m叉树最多有(mh -1)/(m-1)个节点
具有n个结点的m叉树的最小高度为logm (n(m-1)+1)取上界
二叉树节点的构造
//二叉树 class TreeNode{ TreeNode left; Object data; TreeNode right; public TreeNode(Object data){ this.data = data; } public TreeNode(TreeNode left,Object data,TreeNode right){ this.left = left; this.data = data; this.right = right; } } //创建对象 创建对象: TreeNode node1 = new TreeNode(null,"AA",nuLL); TreeNode leftNode = new TreeNode(null,"BB",null); TreeNode rightNode = new TreeNode(null,"Cc",null); nodel.left = leftNode; node1.right = rightNode; //或者:树节点 public class BinaryTree<E>{ private TreeNode root; //二叉树的根结点 private int total;//结点总个数 private class TreeNode{ //至少有以下几个部分 TreeNode parent; TreeNode left; E data; TreeNode right; public TreeNode(TreeNode parent, TreeNode left, E data, TreeNode right) { this.parent = parent; this.left = left; this.data = data; this.right = right; } } } //创建对象 TreeNode leftNode = new TreeNode(nodel,null,"BB",null); TreeNode rightNode = new TreeNode(nodel,null,"Cc",null); nodel.left = leftNode,nodel.right = rightNode;
3、二叉树:每个结点最多只能有两棵子树,且有左右之分。
二叉树的性质:n0 = n2 +1(n =n0 +n1 +n2 ,且n=分支数量+1=n1 +2n2 于是得到这个结论)
非空二叉树上第k层最多有2k-1 个节点(k>=1)
高度为h的二叉树最多有2h -1个节点(h>=1)
具有n个节点的完全二叉树的高度为log2(n+1)取上界,log2 n+1取下界
二叉树的遍历:
- 先序:先输出根结点,再遍历左子树,最后遍历右子树,遍历左子树和右子树的时候,同样遵循先序遍历的规则
- 中序:先递归中序遍历左子树,再输出根结点的值,再递归中序遍历右子树
- 后续:先递归后序遍历左子树,再递归后序遍历右子树,最后输出根结点的值
- 层次遍历。
//先序遍历 public void preOrder(TreeNode root){ //递归出口 if(root == null){//每次都是先遍历根节点 return; } System.out.print(root.data);//先输出根节点的值 preOrder(root.left);//先遍历左子树 preOrder(root.right);//再遍历右子树 } //中序遍历 public void inOrder(TreeNode root){ if(root == null){ return; } inOrder(root.left); system.out.println(root.data); inOrder(root.right); } //后续遍历 public void postOrder(TreeNode root){ if(root == null){ return; } postOrder(root.left); postOrder(root.right); system.out.println(root.data); }
4、几个特殊的二叉树:满二叉树、完全二叉树、二叉排序树(二叉查找树)、平衡二叉树
二叉排序树会退化成单链表的形式→平衡二叉树(LL、RR、LR、RL)
满二叉树
: 除最后一层无任何子节点外,每一层上的所有结点都有两个子结点的二叉树。 第n层的结点数是2n-1,总的结点个数是2n-1完全二叉树
: 叶结点只能出现在最底层的两层,且最底层叶结点均处于次底层叶结点的左侧。recolor
:将某个节点变红或变黑、rotation
:将红黑树某些结点分支进行旋转(左旋或右旋)红黑树定义
public class Node {
public Class<?> clazz;
public Integer value;
public Node parent;
public Node left;
public Node right;
// AVL 树所需属性
public int height;
// 红黑树所需属性
public Color color = Color.RED;
}
TreeMap红黑树的定义
·TreeMap、TreeSet 以及 JDK1.8 的 HashMap 底层都用到了红黑树。·
public class TreeMap<K,V> { private transient Entry<K,V> root; private transient int size = 0; static final class Entry<K,V> implements Map.Entry<K,V> { K key; V value; Entry<K,V> left; Entry<K,V> right; Entry<K,V> parent; boolean color = BLACK; /** * Make a new cell with given key, value, and parent, and with * {@code null} child links, and BLACK color. */ Entry(K key, V value, Entry<K,V> parent) { this.key = key; this.value = value; this.parent = parent; } } }
其六:B树(Balance):B树也称B-树,全称多路平衡查找树。大部分数据库系统及文件系统采用(IO少)。
关于m阶B树:所有节点的孩子个数的最大值称为B树的阶m
其七:B+树:是 B 树的一种变体
B树&B+树对比:
作用:一种来检索元素是否在给定大集合中的数据结构,这种数据结构是高效且性能很好的,但缺点是具有一定的错误识别率(存在会误判,但是不存在不会)和删除难度(比如缓存穿透、海量数据去重)
1、定义:二进制向量(或位bit数组)+ 哈希函数(一系列随机映射函数) = 组成的数据结构。相比于 List、Map、Set 等数据结构,占用空间更少且效率更高,但是其返回的结果是概率性的,不是非常准确,数据不容易删除。
使用位数组存储所有数据,每个元素占1bit(0表false,1表true)。如果申请一个100w个元素的位数组只占用 1000000Bit / 8 = 125000 Byte = 125000/1024 KB ≈ 122KB 的空间。
2、布隆过滤器原理
有几个哈希函数就得到几个哈希值
,根据得到的哈希值,在位数组中把对应下标的值置为1。3、使用场景:
4、布隆过滤器的实现:
(1)自定义布隆过滤器:体验布隆过滤器的原理
具体实现:①合适大小的位数组;②几个不同的哈希函数;③添加元素到位数组(布隆过滤器中);④判断元素是否存在位数组(布隆过滤器中)
import java.util.BitSet; public class MyBloomFilter{ private static final int DEFAULT_SIZE = 2 << 24;//位数组的大小 private static final int[] SEEDS = new int[]{3, 13, 46, 71, 91, 134};//哈希函数的个数 private BitSet bits = new BitSet(DEFAULT_SIZE);//位数组,并指定容量大小 private SimpleHash[] func = new SimpleHash[SEEDS.length];//hash函数类的数组 //初始化多个包含hash函数的类的数组 public MyBloomFilter(){ for (int i = 0; i < SEEDS.length; i++) { func[i] = new SimpleHash(DEFAULT_SIZE,SEEDS[i]); } } //添加元素到位数组 public void add(Object value){ for(SimpleHash f : func){ bits.set(f.hash(value),true);//计算出位置然后在对应的bits数组中将其设置为true即1 } } //判定元素是否存在于为数组中 public boolean contains(Object value){ boolean ret = true; for(SimpleHash f : func){ ret = ret && bits.get(f.hash(value)); } return ret; } //静态内部类:用于hash操作 public static class SimpleHash{ private int cap;//hash数组容量 private int seed; //提供构造器 public SimpleHash(int cap, int seed){ this.cap = cap; this.seed = seed; } //计算hash值 public int hash(Object value){ int h; return (value == null) ? 0 : Math.abs(seed * (cap - 1) & ((h = value.hashCode()) ^ (h >>> 16))); } } }
(2)利用Google开源的Guava中自带的布隆过滤器
缺陷:只能单机使用,不易扩展容量
导入Guava的依赖
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>28.0-jre</version>
</dependency>
创建布隆过滤器,可以容忍的误判的概率:0.01
当 mightContain() 方法返回 true 时,我们可以 99%确定该元素在过滤器中,当过滤器返回 false 时,我们可以 100%确定该元素不存在于过滤器中。
// 创建布隆过滤器对象
BloomFilter<Integer> filter = BloomFilter.create(
Funnels.integerFunnel(),
1500,
0.01);
// 判断指定元素是否存在
System.out.println(filter.mightContain(1));
System.out.println(filter.mightContain(2));
// 将元素添加进布隆过滤器
filter.put(1);
filter.put(2);
System.out.println(filter.mightContain(1));
System.out.println(filter.mightContain(2));
(3)Redis中的布隆过滤器:适用于互联网的分布式场景
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。