[数组, 单链表, 双链表, 优先队列, ByteBuffer(可变读写)]
以下均为伪代码、提供示例。
一 数组
创建一个数组:
Object[] values = new Object[1024];
创建的数据长度为1024 ,这个是指定长度的数组,是一个不可变长的数组。
可以通过index操作数组的数据获得与修改。values[10] = 1000;
二 可变长数组
可变长数组是在基本的数组上做的扩展,预先分配一个固定数组,和一个 writeIndex (写入位置)的变量。
封装一个方法 public void add(Object value);
当调用 add 写入数据的时候,数据写入位置为 this.values[writeIndex] = value;
改变这个index的值后,writeIndex++ (下一次写入位置将会+1)。
如果 writeIndex 的长度 与 this.values.length 相等,则无法继续写入,因为数组越界了,此时重新分配一个比现在的数组还要长一些的新数组 nvalues = new Object[新的长度要比久的长度大]
将原values数据 copy 到 nvalues 后,替换values的引用(this.values=nvalues)
Object[] values = new Object[16];
private int writeIndex = 0;
public void add(Object value) {
if (writeIndex == values.length) {
Object[] nvalues = new Object[values.length + 16];
System.arraycopy(values, 0, nvalues, 0, values.length);
this.values = nvalues;
}
values[writeIndex++] = value;
}
可变数组的优点继承了数组操作的优点,
对数据查询获取速度比较迅速。
但是删除数据性能较低。例如移除 index = 0 的数据,从index = 1 到 index = writeIndex-1的位置都有向前移动数据。
三 单链表
单链表一般可用于栈操作,先进先后出,从头入栈从头出栈。
入栈时 如果栈内有无数据直接入栈,如果栈内有其他节点,则将新的节点的下节点指向目前的头节点。头节点(栈顶)则替换为当前节点。(本文均为伪代码,仅提供思路)
class Node {
Node next;
Object value;
}
class Stack {
Node head;
public void add(Object value) {
Node node = new Node(value);
node.next = head;
head = node;
}
}
单链表对于只操作顶端数据时,移除顶端只需要从新将头节点 指向当前原头节点下一节点。
public void removeFirst() {
head = head.next;
}
四 双链表
双链表对于操作头尾端点,或对某节点移除操作时更为迅速。但查询数据需要遍历节点所以查询数据并不时最优的选择。选择双链表还是可变数组的要关注业务情况,查询与节点修改的多少。
双链表的节点拥有 上节点引用与下节点引用。 两个并列节点相互引用。
插入节点的时候,可以插入头或插入尾部,例如插入尾部的时候,将新节点的上一节点指向当前节点的末尾节点,末尾节点的下一节点指向新节点,则新节点此时作为末尾节点。
删除中间或头节点的时候,不需要向数组那样向前移动下标。
只需要将当前节点的上一节点指向当前节点的下节点,当前节点的下节点指向当前节点的上节点,当前节点上下节点赋NULL即可。
// 节点
class Node {
Node prev; // 上一节点
Node next; // 下一节点
Object value;
}
class LinkedList {
Node head = null;
Node foot = null;
// 添加节点
public void add(Object value) {
Node node = new Node(value);
if (head == null) {
foot = head = node;
} else {
node.prev = foot;
foot.next = node;
foot = node
}
}
// 删除某节点时
public void remove(Node node) {
if (node == head && node == foot) {
head = null;
foot = null;
}
Node prev = node.prev;
Node next = node.next;
node.prev = null;
node.next = null;
if (prev != null ) {
prev.next = next;
}
if (next != null ) {
next.prev = prev;
}
}
}
五 优先队列
优先队列处理的问题,例如千万的数据中查询出前几的操作, 随机对数据优先级最高的先输出。
普通队列时对头尾追加或移除的操作, 先队列中元素拥有优先级,优先级最高的元素将被取出。
创建优先队列时候,定义一个数组来存储每个元素, 假设有 put(num) pop() 两个方法做进出处理。
put 方法将一个数据插入到当前数组的可写入位置末端(用writeIndex 记录)。
此时将一个数组看作一个 平衡二叉树。假设数组内数值越大则优先级越高。put时插入末端的元素实际上为树的末节点,此节点将与父节点做比较,大于父则与父交换位置,并依次向上比较,直到没有比它更大的或者到节点头(index = 0)
数组 : [0][1][2][3][4][5][6], 对应的树结构如下:
0
1 2
3 4 5 6
那么每个节点有一个规律,
节点的 父节点index = (当前节点 - 1) / 2
左子节点 = 当前节点 x 2 + 1
右子节点 = 当前节点 x 2 + 2
int values[] = new int[10000000];
int writeIndex = 0;
void put (int value) {
values[writeIndex ++] = value;
while (index > 0) {
int parentIndex = parentIndex(index);
int parentValue = this.values[parentIndex];
if (value > parentValue) {
this.values[index] = parentValue;
this.values[parentIndex] = value;
index = parentIndex;
} else {
break;
}
}
}
pop取出数据操作的时候,是保证了,优先级越高的优先出队。
每次取出数据实际上是返回 index =0 的数据,要保证每次优先级最高的取出数据,取出数据后将要将空位数据下沉,下沉过程比较左子节点与右子节点的大小,优先级大的与本节点替换,依次下沉到末端,比较次数实际为二叉树的层数量。
int pop () {
int popvalue = this.values[0];
int value = this.values[--writeIndex];
this.values[0] = value;
int currentIndex = 0;
while (true) {
int index = currentIndex * 2 + 1; // left node
if (index >= writeIndex) {
break;
}
int rightIndex = index + 1; // right node = currentIndex * 2 + 2
if (rightIndex < writeIndex && this.values[rightIndex] > this.values[index]) {
index = rightIndex;
}
if (value >= this.values[index]) {
break;
}
this.values[currentIndex] = this.values[index];
this.values[index] = value;
currentIndex = index;
}
return popvalue;
}
六 ByteBuffer 读写 (简介)
这种数据结构的形式是一个 byte[] 的可变数组实现 (参考可变数组),
他有几个常用方法:
write(b), read() 写入一个字节或读取一个字节。他有两个指向下标writeIndex readIndex
markWriteIndex(); 标记目前写入位置,当调用 resetWriteIndex 时写index回归当前标记。
markReadIndex(); 标记目前读取位置,当调用 resetReadIndex 时读index回归当前标记。
resetWriteIndex(); markWriteIndex 默认 -1
resetReadIndex(); markReadIndex 默认 -1
每次调用read都会读取一个新的数据,直到读取不到数据时发生异常。
写入数据会一直向后写入,当写入空间达到 数组.length的值时,则自动扩容。
发生扩容时容量和index的位置变化:
当没有标记 markReadIndex时
新的容量 大于 (writeIndex - readIndex);
当前数组从 index = readIndex 位置, copy长度 = writeIndex - readIndex 数据到 新生成的数组中(0index开始)。然后将新数组替换原引用后,重新赋值read与write index
此时
writeIndex = writeIndex - readIndex
readIndex = 0,
当标记 markReadIndex 时
新的容量 大于 (writeIndex - markReadIndex)
当前数组从 index = markReadIndex 位置, copy长度 = writeIndex - markReadIndex 数据到 新生成的数组中(0index开始)。然后将新数组替换原引用后,重新赋值read与write index
此时:
writeIndex = writeIndex - markReadIndex
readIndex = readIndex - markReadIndex
markReadIndex = 0
假设 markReadIndex = 10 , readIndex = 20 , writeIndex = 30
真实数据大小(从mark-windex) = writeIndex - markReadIndex = 20
可以读取长度(从rindex-windex)= writeIndex - readIndex = 10
扩容后:markReadIndex = 0, readIndex = 10, writeIndex = 20
一般用于处理封装协议的连续数据,当读取的数据不完整不能被解析,需要等待后续内容连接成完整的数据才能解析,可以使用此实现将读取的数据暂时缓存。等待读取的内容足够完整在进行解析。
byte[] values = new byte[capacity];
int writeIndex; // 写位置
int readIndex; // 读位置
int makeReadIndex = -1; // 标记读读位置
int makeWriteIndex = -1;
public void put(int b) {
// 整理并扩容
if (writeIndex == values.length) {
int leftindex = readIndex;
if (makeReadIndex != -1) {
leftindex = makeReadIndex;
this.makeReadIndex = 0;
}
int length = writeIndex - leftindex;
byte[] nvalues = new byte[length + 10];
System.arraycopy(values, leftindex, nvalues, 0, length);
readIndex = readIndex - leftindex;
writeIndex = writeIndex - leftindex;
if (makeWriteIndex != -1) {
makeWriteIndex = makeWriteIndex - leftindex;
}
this.values = nvalues;
}
values[writeIndex ++] = (byte) b;
}
public int read() {
if (readIndex == writeIndex) {
throw new ArrayIndexOutOfBoundsException(“越界了");
}
return values[readIndex++];
}
// 标记读位置
public void markReadIndex() {
makeReadIndex = readIndex;
}
public void markWriteIndex() {
makeWriteIndex = writeIndex;
}
// 还原读位置
public void resetReadIndex() {
if (makeReadIndex != -1) {
readIndex = makeReadIndex;
makeReadIndex = -1;
}
}
public void resetWriteIndex() {
if (makeWriteIndex != -1) {
writeIndex = makeWriteIndex;
makeWriteIndex = -1;
}
}