数据结构
**数据结构指数据的存储、组织方式。**有人认为“程序=数据结构+算法”。因此良好的数据结构对于程序的运行至关重要,尤其是在复杂的系统中,设计优秀的数据结构能够提高系统的灵活性和性能。
![](https://i-blog.csdnimg.cn/blog_migrate/f9ea5c3b6e88252b1c5d7a30d45795e6.png)
栈及其Java实现
栈(Stack)又名堆栈,是允许在同一端进行插入和删除操作的特殊线性表。其中,允许进行插入和删除操作的一端叫作栈顶(Top),另一端叫作栈底(Bottom),栈底固定,栈顶浮动。栈中的元素个数为零时,该栈叫作空栈。插入一般叫作进栈(Push),删除叫作退栈(Pop)。栈也叫作后进先出(FILO-First In Last Out)的线性表。
要实现一个栈,需要先实现以下核心方法。
◎ push():向栈中压入一个数据,先入栈的数据在最下边。
◎ pop():弹出栈顶数据,即移除栈顶数据。
◎ peek():返回当前的栈顶数据。
栈的数据结构,Stack的类,用来存储栈的数据结构;定义了一个数组data,用来存储栈中的数据;定义了maxSize,表示栈的最大容量;定义了top,表示栈顶数据的指针;定义了两个栈的构造函数,在构造函数没有参数时默认构造一个大小为10的栈。
/**
* 基于数组实现的顺序栈
* @param <E>
*/
public class Stack<E> {
private Object[] data = null;
private int maxSize=0; //栈的容量
private int top =-1; //栈顶的指针
//构造函数:根据指定的size初始化栈
Stack(){
this(10); //默认的栈大小为10
}
Stack(int initialSize){
if(initialSize >=0){
this.maxSize = initialSize;
data = new Object[initialSize];
top = -1;
}else{
throw new RuntimeException("初始化大小不能小于0:" + initialSize);
}
}
}
数据入栈push、数据出栈pop、数据查询peek
//push进栈,第1个元素top=0;
public boolean push(E e){
if(top == maxSize -1){
throw new RuntimeException("栈已满,无法将元素入栈!")
}else{
data[++top]=e;
return true;
}
}
//pop弹出栈顶的元素
public E pop(){
if(top == -1){
throw new RuntimeException("栈为空!");
}else{
return (E)data[top--];
}
}
//peek
查看栈顶元素但不移除
public E peek(){
if(top == -1){
throw new RuntimeException("栈为空!");
}else{
return (E)data[top];
}
}
队列及其Java实现
队列是一种只允许在表的前端进行删除操作且在表的后端进行插入操作的线性表。其中,执行插入操作的端叫作队尾,执行删除操作的端叫作队头。没有元素的队列叫作空队列,在队列中插入一个队列元素叫作入队,从队列中删除一个队列元素叫作出队。因为队列只允许在队头插入,在队尾删除,所以最早进入队列的元素将最先从队列中删除,所以队列又叫作先进先出(FIFO-first in first out)线性表。
要实现一个队列,需要先实现以下核心方法,前者不会抛出异常。
◎ offer()/add():向队列的尾部加入一个元素(入队),先入队列的元素在最前边。
◎ poll()/remove():删除队列头部的元素(出队)。
◎ peek()/element():取出队列头部的元素。
队列的数据结构,Queue的队列数据结构,并定义了用于存储队列数据的data数组、队列头位置标记front、队列尾位置标记rear、队列的容量maxSize。队列的默认长度为10,在初始化时,front的位置等于rear的位置,都为0;在有新的数据加入队列时,front的值加1。(感觉书上错了应该是rear加1,而且用linkedlist应该更好,poll头部以后,不能在添加了)
public class Queue<E> {
private Object[] data=null;
private int maxSize; //队列的容量
private int front; //队列头,允许删除
private int rear; //队列尾,允许插入
//构造函数,默认的队列大小为10
public Queue(){
this(10);
}
public Queue(int initialSize){
if(initialSize >=0){
this.maxSize = initialSize;
data = new Object[initialSize];
front = rear =0;
}else{
throw new RuntimeException("初始化大小不能小于0:" + initialSize);
}
}
}
插入add数据,删除数据poll,取走peek
//add,在队列的尾部插入数据
public boolean add(E e){
if(rear== maxSize){
throw new RuntimeException("队列已满,无法插入新的元素!");
}else{
data[rear++]=e;
return true;
}
}
//poll删除队列头部的元素:出队
public E poll(){
if(empty()){
throw new RuntimeException("空队列异常!");
}else{
E value = (E) data[front]; //临时保存队列front端的元素的值
data[front++] = null; //释放队列front端的元素
return value;
}
}
//peek取出队列头部的元素,但不删除
public E peek(){
if(empty()){
throw new RuntimeException("空队列异常!");
}else{
return (E) data[front];
}
}
链表
链表是由一系列节点(链表中的每一个元素都叫作一个节点)组成的数据结构,节点可以在运行过程中动态生成。每个节点都包括两部分内容:存储数据的数据域;存储下一个节点地址的指针域。由于链表是随机存储数据的,因此在链表中插入数据的时间复杂度为O(1),比在线性表和顺序表中插入的效率要高;但在链表中查找一个节点时需要遍历链表中所有元素,因此时间复杂度为O(n),而在线性表和顺序表中查找一个节点的时间复杂度分别为O(logn)和O(1)。链表数据结构的优点是插入快,缺点是数据查询需要遍历整个链表,效率慢。链表有3种不同的类型:单向链表、双向链表及循环链表。
单向链表
单向链表(又称单链表)是链表的一种,其特点是链表的链接方向是单向的,访问链表时要从头部开始顺序读取。单向链表是链表中结构最简单的。一个单向链表的节点(Node)可分为两部分:第1部分为数据区(data),用于保存节点的数据信息;第2部分为指针区(next),用于存储下一个节点的地址,最后一个节点的指针指向null。
单向链表的操作:
查找:单向链表只可向一个方向遍历,一般在查找一个节点时需要从单向链表的第1个节点开始依次访问下一个节点,一直访问到需要的位置。
插入:对于单向链表的插入,只需将当前插入的节点设置为头节点,将Next指针指向原来的头节点即可。(头插、尾插)
删除:对于单向链表的删除,我们只需将该节点的上一个节点的Next指针指向该节点的下一个节点,然后删除该节点即可。
定义了名为SingleLinkedList的单向链表,并定义了length表示链表的大小;head,表示链表的头部;名为Node的内部类,表示链表的节点数据结构,在Node中有data和next两个属性,分别表示该链表节点的数据和下一个节点的连接。
public class SingleLinkedList {
private int length; //链表节点的个数
private Node head; //头节点
public SingleLinkedList(){
size = 0;
head = null;
}
//链表的每个节点的数据结构描述类
private class Node{
private Object data; //每个节点的数据
private Node next; //每个节点指向下一个节点的连接
public Node(Object data){
this.data = data;
}
}
}
插入数据、删除数据、查询数据
//出入数据在链表头添加元素
public Object addHead(Object obj){
Node newHead = new Node(obj); //step 1 :定义新节点
if(length== 0){ //step 2 :如果链表为空,则将该节点设置为头部节点
head = newHead;
}else{//step 3: 设置当前节点为头部节点,并将当前节点的下一个节点指向原来的头部节点
newHead.next = head;
head = newHead;
//书上应该错,应该先指向原头结点,然后在设置结点为头部结点
}
length ++; //step 4:链表长度+1
return obj;
}
//删除指定的元素,删除成功则返回true
public boolean delete(Object value){
if(length == 0){
return false;
}
Node current = head;
Node previous = head;
while(current.data ! = value){
if(current.next == null){
return false;
}else{
previous = current;
current = current.next;
}
}
//如果删除的节点是头节点
if(current == head){
head = current.next;
length--;
}else{//删除的节点不是头节点
previous.next = current.next;
length--;
}
return true;
}
//查找指定的元素,若找到了则返回节点Node,找不到则返回null
public Node find(Object obj){
Node current = head;
int tempSize = length;
while(tempSize > 0){
if(obj.equals(current.data)){
return current;
}else{
current = current.next;
}
tempSize--;
}
return null;
}
双向链表
在双向链表的每个数据节点中都有两个指针,分别指向其直接后继和直接前驱节点。所以,从双向链表中的任意一个节点开始,都可以很方便地访问它的直接前驱节点和直接后继节点。双向链表和单向链表的不同之处在于,单向链表除数据项外只定义了一个Next指针指向下一个节点,而双向链表定义了Prev和Next两个指针分别指向上一个节点和下一个节点。
TwoWayLinkedList的双向链表的数据结构,其中定义了:head,表示链表头;tail,表示链表尾;length,表示链表长度;Node,表示链表的节点,链表的节点包含data、prev、next,分别表示节点数据、上一个节点和下一个节点。
public class TwoWayLinkedList {
private Node head; //表示链表头
private Node tail; //表示链表尾
private int length; //表示链表的长度
private class Node{
private Object data;
private Node next;
private Node prev;
public Node(Object data){
this.data = data;
}
}
public TwoWayLinkedList(){
size = 0;
head = null;
tail = null;
}
}
链表头部增加节点、尾部增加节点、删除头部结点、删除尾部
//在链表头部增加节点
public void addHead(Object value){
Node newNode = new Node(value);
if(length == 0){
head = newNode;
tail = newNode;
length++;
}else{
head.prev = newNode;
newNode.next = head;
head = newNode;
length++;
}
}
//在链表尾部增加节点
public void addTail(Object value){
Node newNode = new Node(value);
if(length == 0){
head = newNode;
tail = newNode;
length++;
}else{
newNode.prev = tail;
tail.next = newNode;
tail = newNode;
length++;
}
}
//删除链表的头部节点
public Node deleteHead(){
Node temp = head;
if(length ! = 0){
head = head.next;
head.prev = null;
length--;
return temp;
}else{ return null }
}
//删除链表的尾部节点
public Node deleteTail(){
Node temp = tail;
if(length ! = 0){
tail = tail.prev;
tail.next = null;
length--;
return temp;
}else{ return null }
}
循环链表
表中最后一个节点的指针域指向头节点,整个链表形成一个环。循环节点的实现和单向链表十分相似,只是在链表中,尾部元素的Next指针不再是null,而是指向头部节点,其他实现和单向链表相同。
散列表
散列表(Hash Table,也叫作哈希表)是根据数据的关键码值(Key-Value对)对数据进行存取的数据结构。散列表通过映射函数把关键码值映射到表中的一个位置来加快查找。这个映射函数叫作散列函数,存放记录的数组叫作散列表。给定表M,存在函数f(key),对任意给定的关键字key,代入函数后若能得到包含该关键字的记录在表中的地址,则称表M为散列表,称函数f(key)为散列函数。
常用的构造散列函数如下。
◎ 直接定址法:取关键字或关键字的某个线性函数值为散列地址,即h(key) = key或h(key) = a×key + b,其中a和b为常数。
◎ 平方取值法:取关键字平方后的中间几位为散列地址。
◎ 折叠法:将关键字分割成位数相同的几部分,然后取这几部分的叠加和作为散列地址。
◎ 除留余数法:取关键字被某个不大于散列表长度m的数p除后所得的余数为散列地址,即h(key) = key /p (p≤m)。
◎ 随机数法:选择一个随机函数,取关键字的随机函数值作为其散列地址,即h(key)=random(key)。
◎ Java HashCode实现:在Java中计算HashCode的公式为f(key) = s[0]× 31n-1+s[1]× 31n-2+…+s[n-1]。就是以31为权,每一位字符的ASCII只进行计算,用自然溢出来等效取模。主要原因是因为31是一个奇素数,所以31i=32i-i=(i<<5)-i,这种位移与减法结合的计算相比一般的运算快很多。
// String中hashCode方法的实现源码
public int hashCode() {
int h = hash; // hash表示该串的哈希值
if (h == 0 && value.length > 0) {
char val[] = value;
for (int i = 0; i < value.length; i++) {
h = 31 * h + val[i];
}
hash = h;
}
return h;
}
Hash主要用于用信息安全加密和快速查询的应用场景。
◎ 信息安全:Hash主要被用于信息安全领域的加密算法中,它把一些不同长度的信息转化成杂乱的128位编码,这些编码的值叫作Hash值。也可以说,Hash就是找到一种数据内容和数据存放地址之间的映射关系。
◎ 快速查找:散列表,又叫作散列,是一种更加快捷的查找技术。基于列表集合查找的一般做法是从集合中拿出一个元素,看它是否与当前数据相等,如果不相等,则缩小范围,继续查找。而散列表是完全另外一种思路,在知道key值以后,就可以直接计算这个元素在集合中的位置,不需要一次又一次的遍历查找。
二叉排序树
二叉排序树(Binary Sort Tree),又称二叉查找树(Binary Search Tree)或二叉搜索树。二叉排序树为满足以下条件的树:
◎ 若左子树不空,则左子树上所有节点的值均小于它的根节点的值;
◎ 若右子树不空,则右子树上所有节点的值均大于它的根节点的值;
◎ 左、右子树也分别为二叉排序树。
插入操作
(1)将待插入的新节点与当前节点进行比较,如果两个节点的值相同,则表示新节点已经存在于二叉排序树中,直接返回false。
(2)如果待插入的新节点的值小于当前节点的值,则在当前节点的左子树中寻找,直到左子树为空,则当前节点为要找的父节点,将新节点插入当前节点的左子树即可。
(3)如果待插入的新节点的值大于当前节点的值,则在当前节点的右子树中寻找,直到右子树为空,则当前节点为要找的父节点,将新节点插入当前节点的右子树即可。
删除操作
二叉排序树的删除操作主要分为三种情况:
(1)待删除的节点没有子节点:直接删除该节点。
(2)待删除的节点只有一个子节点:使用子节点替换当前节点,然后删除该节点即可。
(3)待删除的节点有两个子节点:首先查找该节点的替换节点(替换节点为左子树中的最大节点或者右子树中的最小节点),然后替换待删除的节点为替换节点,最后删除替换节点。(会有两种结果)
查找操作
二叉排序树的查找方式和效率接近二分查找法,因此可以很容易获取最大(最右最深子节点)值和最小(最左最深子节点)值,具体的查找流程为:将要查找的数据与根节点的值进行比较,如果相等就返回,如果小于就到左子树中递归查找,如果大于就到右子树中递归查找。
public class Node {
private int value;
private Node left;
private Node right;
public Node(){
}
public Node(Node left, Node right, int value){
this.left = left;
this.right = right;
this.value = value;
}
public Node(int value){
this(null, null, value);
}
}
红黑树
红黑树(Red-Black Tree, R-B Tree)是一种自平衡的二叉查找树。在红黑树的每个节点上都多出一个存储位表示节点的颜色,颜色只能是红(Red)或者黑(Black)。
红黑树的特性
◎ 每个节点或者是黑色的,或者是红色的。
◎ 根节点是黑色的。
◎ 每个叶子节点(NIL)都是黑色的。
◎ 如果一个节点是红色的,则它的子节点必须是黑色的。
◎ 从一个节点到该节点的子孙节点的所有路径上都包含相同数量的黑色节点。
红黑树的左旋、右旋、插入、删除和查找再网上找资料看,本书写的一般。
红黑树的添加分为3步:①将红黑树看作一颗二叉查找树,并以二叉树的插入规则插入新节点;②将插入的节点涂为“红色”或“黑色”; ③通过左旋、右旋或着色操作,使之重新成为一颗红黑树。
根据被插入的节点的父节点的情况,可以将具体的插入分为3种情况来处理。
(1)如果被插入的节点是根节点,则直接把此节点涂为黑色的。
(2)如果被插入的节点的父节点是黑色的,则什么也不需要做,在节点插入后,仍然是红黑树。
(3)如果被插入的节点的父节点是红色的,则在被插入节点的父节点是红色的时,被插入节点一定存在非空祖父节点,即被插入节点也一定存在叔叔节点,即使叔叔节点(叔叔节点指当前节点的祖父节点的另一个子节点)为空,我们也视之为存在,空节点本身就是黑色节点。然后根据叔叔节点的颜色,在被插入节点的父节点是红色的时,进一步分为3种情况来处理。
◎ 如果当前节点的父节点是红色的,当前节点的叔叔节点是红色的,则将父节点设为黑色的,将叔叔节点设为黑色的,将祖父节点设为红色的,将祖父节点设为当前节点。
◎ 如果当前节点的父节点是红色的,当前节点的叔叔节点是黑色的且当前节点是右节点,则将父节点设为当前节点,以新节点为支点左旋。
◎ 如果当前节点的父节点是红色的,当前节点的叔叔节点是黑色的且当前节点是左节点,则将父节点设为黑色的,将祖父节点设为红色的,以祖父节点为支点右旋。
红黑树的删除分为两步:①将红黑树看作一颗二叉查找树,根据二叉查找树的删除规则删除节点;②通过左旋、旋转、重新着色操作进行树修正,使之重新成为一棵红黑树,具体操作如下。
(1)将红黑树看作一颗二叉查找树,将节点删除。
◎ 如果被删除的节点没有子节点,那么直接将该节点删除。
◎ 如果被删除的节点只有一个子节点,那么直接删除该节点,并用该节点的唯一子节点替换该节点的位置。
◎ 如果被删除的节点有两个子节点,那么先找出该节点的替换节点,然后把替换节点的数据复制给该节点的数据,之后删除替换节点。
(2)通过左旋、旋转、重新着色操作进行树修正,使之重新成为一棵红黑树,因为红黑树在删除节点后可能会违背红黑树的特性,所以需要通过旋转和重新着色来修正该树,使之重新成为一棵红黑树:①如果当前节点的子节点是“红+黑”节点,则直接把该节点设为黑色的;②如果当前节点的子节点是“黑+黑”节点,且当前节点是根节点,则什么都不做;③如果当前节点的子节点是“黑+黑”节点,且当前节点不是根节点,则又可以分为以下几种情况进行处理。
◎ 如果当前节点的子节点是“黑+黑”节点,且当前节点的兄弟节点是红色的,则将当前节点的兄弟节点设置为黑色的,将父节点设置为红色的,对父节点进行左旋,重新设置当前节点的兄弟节点。
◎ 如果当前节点的子节点是“黑+黑”节点,且当前节点的兄弟节点是黑色的,兄弟节点的两个子节点也都是黑色的,则将当前节点的兄弟节点设置为红色的,设置当前节点的父节点为新节点。
◎ 如果当前节点的子节点是“黑+黑”节点,且当前节点的兄弟节点是黑色的,兄弟节点的左子节点是红色的且右子节点是黑色的,则将当前节点的左子节点设置为黑色的,将兄弟节点设置为红色的,对兄弟节点进行右旋,重新设置当前节点的兄弟节点。
◎ 如果当前节点的子节点是“黑+黑”节点,且当前节点的兄弟节点是黑色的,兄弟节点的右子节点是红色的且左子节点是任意颜色的,则将当前节点的父节点的颜色赋值给兄弟基点,将父节点设置为黑色的,将兄弟节点的右子节点设置为黑色的,对父节点进行左旋,设置当前节点为根节点。
图
图是由有穷非空集合的顶点和顶点之间的边组成的集合,通常表示为G(V, E),其中G表示一个图,V是图G中顶点的集合,E是图G中边的集合。在线性结构中,每个元素都只有一个直接前驱和直接后继,主要用来表示一对一的数据结构;在树形结构中,数据之间有着明显的父子关系,每个数据和其子节点的多个数据相关,主要用来表示一对多的数据结构;在图形结构中,数据之间具有任意关系,图中任意两个数据元素之间都可能相关,可用来表示多对多的数据结构。图根据边的属性可分为无向图和有向图。
有向图
若从顶点Vi到Vj的边有方向,则称这条边为有向边,也叫作弧,用有序偶<Vi, Vj>来表示有向边,Vi叫作弧尾,Vj叫作弧头。由顶点和有向边组成的图叫作有向图。如图4-19所示,G=(V2, {E2}),其中顶点集合V2={A, B, C, D},弧集合E2={<A, D>, <B, A>, <C, A>, <B, C>}。连接顶点A到D的有向边就是弧,A是弧尾,D是弧头,<A, D>表示弧,注意弧是有方向的,不能写成<D, A>。
图的存储结构:邻接矩阵
无向图:在无向图的邻接矩阵中,如果<Vi, Vj>的交点为1,则表示两个顶点连通,为0则不连通。在无向图的邻接矩阵中,主对角元素都为0,也就是说顶点自身没有连通关系
![](https://i-blog.csdnimg.cn/blog_migrate/02687ce384c884acc91c21896cc48c49.png)
有向图:
在有向图的邻接矩阵中,如果<Vi, Vj>的交点为1,则表示从Vi到Vj存在弧(但从Vj到Vi是否存在弧不确定),为0则表示从Vi到Vj不存在弧;同样,在有向图的邻接矩阵中主对角元素都为0,也就是说从顶点到自身没有弧。需要注意的是,有向图的连接是有方向的,V1的出度为2(从V1出发的边有两条),表示从V1顶点出发的边有两条,V3的出度为0,表示没有从V3出发的边。
![](https://i-blog.csdnimg.cn/blog_migrate/549ca684dd91cac3ec8656eac40747aa.png)
带权重:有些图的每条边上都带有权重,如果要将这些权值保存下来,则可以采用权值代替矩阵中的0、1,在权值不存在的元素之间用∞表示。
![](https://i-blog.csdnimg.cn/blog_migrate/98e33fc5c5e7c2bf66afc0a53964127e.png)
图的存储结构:邻接表
数组与链表相结合的存储方法叫作邻接表。邻接表是图的一种链式存储结构,主要用于解决邻接矩阵中顶点多边少时空间浪费的问题。
(1)将图中的顶点信息存储在一个一维数组中,同时在顶点信息中存储用于指向第1个邻接点的指针,以便查找该顶点的边信息。
(2)图中每个顶点Vi的所有邻接点构成一个线性表,由于邻接点的个数不定,所以用单向链表存储,如果是无向图,则称链表为顶点Vi的边表,如果是有向图,则称链表为以顶点Vi为弧尾的出边表。
无向图与有向图:顶点是通过一个头节点类型的一维数组保存的,其中每个头节点的第1个弧都指向第1条依附在该顶点上的边的信息,邻接域表示该边的另一个顶点在顶点数组中的下标,下一个弧指向下一条依附在该顶点上的边的信息,有向图的邻接表和无向图类似。
带权重:对于带权值的图,在节点定义中再增加一个权重值weight的数据域,存储权值信息即可。
图的遍历-广度优先搜索和深度优先搜索
图的遍历指从图中某一顶点出发访遍图中的每个顶点,且使每一个顶点仅被访问一次。图的遍历分为广度优先遍历和深度优先遍历,且对无向图和有向图都适用。
广度优先搜索(Breadth First Search),类似于树的分层遍历算法,其定义为:假设从图中某个顶点V出发,在访问了V之后依次访问V的各个未曾访问过的邻接点,然后分别从这些邻接点出发依次访问它们的邻接点,并使先被访问的顶点的邻接点先于后被访问的顶点的邻接点被访问,直到图中所有已被访问的顶点的邻接点都被访问;若此时图中尚有顶点未被访问,则另选图中未曾被访问的一个顶点作为起始点重复上述过程,直至图中所有顶点均被访问。
深度优先搜索(Depth First Search),类似于树的先根遍历(先访问树的根节点)。其定义如下:假设从图中的某个顶点V出发,在访问V节点后依次从V未被访问的邻接点出发以深度优先的原则遍历图,直到图中所有和V节点路径连通的顶点都被访问;若此时图中尚有顶点未被访问,则另选一个未曾访问的顶点作为起始点重复上述过程,直至图中所有节点都被访问。
位图
**位图(Bitmap)**通常基于数组实现,我们可以将数组中的每个元素都看作一系列二进制数,所有元素一起组成更大的二进制集合,这样就可以大大节省空间。位图通常是用来判断某个数据存不存在的,常用于在Bloom Filter中判断数据是否存在,还可用于无重复整数的排序等,在大数据行业中使用广泛。
位图的数据结构
位图在内部维护了一个M×N维的数组char[M][N],在这个数组里面每个字节占8位,因此可以存储M×N×8个数据。假如要存储的数据范围为0~15,则只需使用M=1, N=2的数据进行存储。在我们要存储的数据为{1,3,6,10,15}时,只需将有数据的位设置为1,表示该位存在数据,将其他位设置为0。
位图的Java实现
在Java中使用byte[]字节数组来存储bit,1Byte = 8bit。对于bit中的第i位,该bit为1则表示true,即数据存在;为0则表示false,即数据不存在。其具体实现分为数据结构的定义、查询方法的实现和修改方法的实现。
数据结构的定义
定义了一个名为Bitmap的类用于位图数据结构的存储,其中byte[]数组用于存储具体的数据,length用于记录数据的长度
//以bit为存储单位的数据结构,对于给定的第i位,1表示true,0表示false
public class Bitmap {
private byte[] bytes;
//length为位图的长度,实际可操作的下标为[0, length)
private int length;
public Bitmap(int length){
this.length = length;
bytes = new byte[length%8==0 ? length/8 : length/8+1];
}
}
查询方法的实现
位图的查询操作为在拿到目标bit所在的Byte后,将其向右位移(并将高位置0),使目标bit在第1位,这样结果值就是目标bit值,方法如下。
(1)通过byte[index >> 3](等价于byte[index/8])取到目标bit所在的Byte。
(2)令i = index&7(等价于index%8),得到目标bit在该Byte中的位置。
(3)为了将目标bit前面的高位置0(这样位移后的值才等于目标bit本身),需要构建到目标bit为止的低位掩码,即01111111 >>>(7 - i),再与原Byte做&运算。
(4)将结果向右位移i位,使目标bit处于第1位,结果值即为所求。
//获取指定位的值
public boolean get(int index){
int i = index & 7;
//构建到index结束的低位掩码并做&运算(为了将高位置0),
然后将结果一直右移,直到目标位(index位)移到第1位,然后根据其值返回结果
if((bytes[index >> 3] & (01111111>>>(7-i))) >> i == 0)
return false;
else
return true;
}
修改方法的实现
对位图的修改操作根据设定值true或false的不同,分为两种情况。
(1)如果value为true,则表示数据存在,将目标位与1做或运算,需要构建目标位为1、其他位为0的操作数。
(2)如果value为false,则表示数据不存在,将目标位与0做与运算,需要构建目标位为0、其他位为1的操作数。构建目标位为1且其他位为0的操作数的做法为:1 <<(index & 7)。
//设置指定位的值
public void set(int index, boolean value){
if(value)
//通过给定位index,先定位到对应的Byte,并根据value值进行不同位的操作:
//1.如果value为true,则目标位应该做或运算,构建“目标位为1,
//其他位为0”的操作数,为了只合理操作目标位,而不影响其他位
//2.如果value为false,则目标位应该做与运算,构建“目标位为0,
//其他位为1”的操作数
bytes[index >> 3] |= 1 << (index & 7);
//bytes[index/8] = bytes[index/8] | (0b0001 << (index%8))
else
bytes[index >> 3] &= ~(1 << (index & 7));
}