稀疏数组(sparse array)
看一个实际的需求
编写的五子棋程序中, 有存盘退出, 继续上盘的功能
如果是默认的话, 我们可以采用二维数组来记录五子棋的位置
分析问题
因为该二位数组的很多值默认都是0,因此记录了很多没有意义的数据
那该则么解决呢? 没错 使用稀疏数组解决
基本介绍
当一个数组中大部分元素为0, 或者为同一个值的数组时, 可以采用稀疏数组来保存该数组
稀疏数组的处理方法是:
0 | 0 | 0 | 22 | 0 | 0 | 15 |
---|---|---|---|---|---|---|
0 | 1 | 0 | 0 | 0 | 17 | 0 |
0 | 0 | 0 | -6 | 0 | 0 | 0 |
0 | 0 | 0 | 0 | 0 | 39 | 0 |
91 | 0 | 0 | 0 | 0 | 0 | 0 |
0 | 0 | 28 | 0 | 0 | 0 | 0 |
将一个6x7的二位数组转化为一个3x9的稀疏数组,从42缩小到27, 缩小了15个单位
行 | 列 | 值 | 描述 | |
---|---|---|---|---|
0 | 6 | 7 | 8 | 原二位数组为6行7列,8个位置有值 |
1 | 0 | 3 | 22 | 第1行第3列,值为22, 因为索引从0开始 |
2 | 0 | 6 | 15 | … |
3 | 1 | 1 | 1 | … |
4 | 1 | 5 | 17 | … |
5 | 2 | 3 | -6 | … |
6 | 3 | 5 | 39 | … |
7 | 4 | 0 | 91 | … |
8 | 5 | 2 | 28 | … |
应用实例
- 使用稀疏数组, 来保留类似前面的二维数组(棋盘, 地图等等)
- 把稀疏数组存盘, 并可以重新恢复成原始的二位数组
- 整体思路分析
二维数组转稀疏数组的思路
- 遍历原始的二维数组, 得到有效数据的个数 sum, 在这里我们的无效数据就默认为0, 如果不是, 需要先定义无效数据的值
- 根据有效数据的个数, 创建稀疏数组sparse array = new int[sum + 1][3]
- sum + 1代表需要多一行来存放 行数,列数,和值的个数
- 3代表稀疏数组固定3列
将二维数组的有效数据存入到 稀疏数组中
先读取稀疏数组的第一行, 根据第一行的数据, 创建原始的二维数组, 比如上面的new int[11][11]
- 在读取稀疏数组的第二行到第N行的数据, 并根据横坐标+纵坐标+值, 设置到原始二维数组中
代码实现
创建项目
IDEA创建一个mavend的项目就可以,视频中是Eclipse,但是我用IDEA编写代码
```java package com.dance.sparearray;
import java.util.ArrayList; import java.util.Arrays; import java.util.List;
/**
稀疏数组 */ public class SpareArray {
public static void main(String[] args) {
int[][] chessArr = createSourceArray();
// 原始数组转为稀疏素组
int[][] sparseArray = convertSparseArray(chessArr);
// 稀疏数组转为原始数组
int[][] sourceArray = convertSourceArray(sparseArray);
}
public static int[][] convertSourceArray(int[][] sparseArray){
int[][] sourceArray = sparseArrayToArray(sparseArray);
System.out.println("稀疏数组转为原始数组:");
printf(sourceArray);
return sourceArray;
}
public static int[][] convertSparseArray(int[][] chessArr){
System.out.println("原始数组:");
printf(chessArr);
// 原始数组转为稀疏数组
int[][] sparseArray = arrayToSparseArray(chessArr);
System.out.println("原始数组转为稀疏数组:");
printf(sparseArray);
return sparseArray;
}
public static int[][] createSourceArray(){
// 创建原始二维数组
int[][] chessArr = new int[11][11];
// 1表示黑子, 2表示蓝子
chessArr[1][2] = 1;
chessArr[2][3] = 2;
return chessArr;
}
public static int[][] sparseArrayToArray(int[][] sparseArray){
// 根据第一行创建原始数组
int x = sparseArray[0][0];
int y = sparseArray[0][1];
int sum = sparseArray[0][2];
int[][] sourceArray = new int[x][y];
// 根据sum还原数据
for (int i = 1; i < sparseArray.length; i++) {
sourceArray[sparseArray[i][0]][sparseArray[i][1]] = sparseArray[i][2];
}
return sourceArray;
}
/**
- 原始数组转为稀疏数组
- @param chessArr 原始数组
@return 稀疏数组 */ public static int[][] arrayToSparseArray(int[][] chessArr){ // 将二维数组转为稀疏数组 int sum = 0; // 记录数据位置, 下面赋值可以不用二次循环 List
valueIndices = new ArrayList<>(); // 获取非0值的个数 for (int x = 0; x < chessArr.length; x++) {
for (int y = 0; y < chessArr[x].length; y++) {
int value = chessArr[x][y];
if (value != 0) {
ValueIndex valueIndex = new ValueIndex();
valueIndex.x = x;
valueIndex.y = y;
valueIndex.value = value;
valueIndices.add(valueIndex);
sum++;
}
}
} System.out.println(“数值个数: “ + sum);
// 根据数值个数创建稀疏数组 int[][] sparseArray = new int[sum + 1][3]; // 给稀疏数字赋值 // 行 sparseArray[0][0] = chessArr.length; // 列 sparseArray[0][1] = chessArr[0].length; // 数值个数 sparseArray[0][2] = sum;
// 稀疏数组赋值 for (int i = 1; i < sparseArray.length; i++) {
// 获取一个存入
ValueIndex valueIndex = valueIndices.remove(0);
sparseArray[i][0] = valueIndex.x;
sparseArray[i][1] = valueIndex.y;
sparseArray[i][2] = valueIndex.value;
} return sparseArray; }
/**
- 输出二维数组
@param chessArr 二维数组 */ public static void printf(int[][] chessArr) { Arrays.stream(chessArr).forEach(x -> {
Arrays.stream(x).forEach(y -> System.out.printf("%d\t", y));
System.out.println();
}); }
static class ValueIndex { int x; int y; int value; }
}
<a name="q5s7j"></a>
## 测试执行
执行结果
```java
原始数组:
0 0 0 0 0 0 0 0 0 0 0
0 0 1 0 0 0 0 0 0 0 0
0 0 0 2 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0
数值个数: 2
原始数组转为稀疏数组:
11 11 2
1 2 1
2 3 2
稀疏数组转为原始数组:
0 0 0 0 0 0 0 0 0 0 0
0 0 1 0 0 0 0 0 0 0 0
0 0 0 2 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0
ok, 我们成功的实现了将原始数组转换为稀疏数组,再将稀疏数组恢复成了原始数组
队列
队列的使用场景
队列介绍
- 队列是一个有序列表, 可以使用数组或者链表来实现
- 遵循先入先出的原则, 即: 先存入队列的数据, 要先取出, 后存入的要后取出
- 示意图: (使用数组模拟队列的示意图)
数组模拟队列
思路
- 队列本身是有序列表, 若使用数组的结构来存储队列的数组, 则队列数组的声明如上图, 其中MaxSize是该队列的最大容量
- 因为队列的输出, 输入是分别从前后端来处理的, 因此需要两个变量front, 及rear, 分别记录队列的前后端的下标, front会随着数据的输出而改变, 而rear则是随着数据输入而改变, 如图所示
- 当我们将数据存入队列时称为入队(“addQueue”)
import java.util.Scanner;
public class ArrayQueueDemo {
public static void main(String[] args) {
ArrayQueue queue = new ArrayQueue(3);
menuMain(queue);
}
public static void menuMain(ArrayQueue queue) {
Scanner scanner = new Scanner(System.in);
char key = ' ';
while (key != 'e') {
printMenu();
// 获取用户输入
key = scanner.next().charAt(0);
executeMenu(key, scanner, queue);
}
}
public static void printMenu() {
System.out.println("a(addQueue) : 添加队列数据");
System.out.println("g(getQueue) : 获取队列数据");
System.out.println("s(showQueue) : 展示全部数据");
System.out.println("h(headQueue) : 获取头部数据");
System.out.println("e(exit) : 退出程序");
}
public static void executeMenu(char key, Scanner scanner, ArrayQueue queue) {
switch (key) {
case 'a':
System.out.println("请输入要添加的数据:");
int i = scanner.nextInt();
System.out.println("添加数据到队列: " + i);
queue.addQueue(i);
break;
case 'g':
try {
System.out.println("获取到的数据为: "+queue.getQueue());
} catch (Exception e) {
System.out.println(e.getMessage());
}
break;
case 's':
queue.showQueue();
break;
case 'h':
try {
System.out.println("队列头部数据为: " + queue.headQueue());
} catch (Exception e) {
System.out.println(e.getMessage());
}
break;
case 'e':
scanner.close();
System.out.println("程序退出~~~");
}
}
}
/**
使用数组模拟-队列 */ class ArrayQueue {
/**
数组最大值 */ private int maxSize;
/**
队列首指针 */ private int front;
/**
队列尾指针 */ private int rear;
/**
数据存放数组 */ private int[] array;
/**
- 创建队列的构造器
- front = -1 , 指向队列头部, 分析出front是指向队列头的前一个位置
- rear = -1 , 指向队列尾部, 指向队列尾部的数据(即就是队列的最后一个数据) *
@param maxSize 队列最大容量 */ public ArrayQueue(int maxSize) { this(maxSize, -1, -1); }
private ArrayQueue(int maxSize, int front, int rear) { this.maxSize = maxSize; this.front = front; this.rear = rear; this.array = new int[maxSize]; }
/**
- 队列是否有可用空间 *
@return true: 没有 , false: 有 */ public boolean arrayIsFull() { return rear == maxSize - 1; }
/**
- 队列是否为空 *
@return true: 为空, false: 不为空 */ public boolean arrayIsEmpty() { return front == rear; }
/**
- 添加数据到队列中 *
@param newValue 要入队的值 */ public void addQueue(int newValue) { if (arrayIsFull()) {
System.out.println("队列已经存满, 不能存入: " + newValue); return;
} array[++rear] = newValue; }
/**
- 获取元素 *
@return 返回最先进入的数据, 保证队列的先入先出 */ public int getQueue() { if (arrayIsEmpty()) {
throw new ArrayIndexOutOfBoundsException("数组为空");
} // 开始位置后移 return array[++front]; }
/**
展示队列的全部数据 */ public void showQueue() { if (arrayIsEmpty()) {
System.out.println("数组为空"); return;
} for (int i = 0; i < array.length; i++) {
System.out.printf("arr[%d]=%d\n", i, array[i]);
} }
/**
- 获取队列的头节点 *
- @return 队列头部的数据
*/
public int headQueue() {
if (arrayIsEmpty()) {
} // 不能使用++ 因为会改变自身的值, 这里并不取数据只是 查看头部 return array[front + 1]; } }throw new ArrayIndexOutOfBoundsException("数组为空");
<a name="FxpnF"></a> ### 执行测试 ```java a(addQueue) : 添加队列数据 g(getQueue) : 获取队列数据 s(showQueue) : 展示全部数据 h(headQueue) : 获取头部数据 e(exit) : 退出程序 a 请输入要添加的数据: 10 添加数据到队列: 10 a(addQueue) : 添加队列数据 g(getQueue) : 获取队列数据 s(showQueue) : 展示全部数据 h(headQueue) : 获取头部数据 e(exit) : 退出程序 s arr[0]=10 arr[1]=0 arr[2]=0 a(addQueue) : 添加队列数据 g(getQueue) : 获取队列数据 s(showQueue) : 展示全部数据 h(headQueue) : 获取头部数据 e(exit) : 退出程序 g 获取到的数据为: 10 a(addQueue) : 添加队列数据 g(getQueue) : 获取队列数据 s(showQueue) : 展示全部数据 h(headQueue) : 获取头部数据 e(exit) : 退出程序 g 数组为空 a(addQueue) : 添加队列数据 g(getQueue) : 获取队列数据 s(showQueue) : 展示全部数据 h(headQueue) : 获取头部数据 e(exit) : 退出程序 s 数组为空 a(addQueue) : 添加队列数据 g(getQueue) : 获取队列数据 s(showQueue) : 展示全部数据 h(headQueue) : 获取头部数据 e(exit) : 退出程序 a 请输入要添加的数据: 20 添加数据到队列: 20 a(addQueue) : 添加队列数据 g(getQueue) : 获取队列数据 s(showQueue) : 展示全部数据 h(headQueue) : 获取头部数据 e(exit) : 退出程序 s arr[0]=10 arr[1]=20 arr[2]=0 a(addQueue) : 添加队列数据 g(getQueue) : 获取队列数据 s(showQueue) : 展示全部数据 h(headQueue) : 获取头部数据 e(exit) : 退出程序 h 队列头部数据为: 20 a(addQueue) : 添加队列数据 g(getQueue) : 获取队列数据 s(showQueue) : 展示全部数据 h(headQueue) : 获取头部数据 e(exit) : 退出程序 g 获取到的数据为: 20 a(addQueue) : 添加队列数据 g(getQueue) : 获取队列数据 s(showQueue) : 展示全部数据 h(headQueue) : 获取头部数据 e(exit) : 退出程序 g 数组为空 a(addQueue) : 添加队列数据 g(getQueue) : 获取队列数据 s(showQueue) : 展示全部数据 h(headQueue) : 获取头部数据 e(exit) : 退出程序 e 程序退出~~~
问题分析并优化
数组使用一次就不能用了, 没有达到内存复用效果
a(addQueue) : 添加队列数据 g(getQueue) : 获取队列数据 s(showQueue) : 展示全部数据 h(headQueue) : 获取头部数据 e(exit) : 退出程序 a 请输入要添加的数据: 11 添加数据到队列: 11 a(addQueue) : 添加队列数据 g(getQueue) : 获取队列数据 s(showQueue) : 展示全部数据 h(headQueue) : 获取头部数据 e(exit) : 退出程序 a 请输入要添加的数据: 12 添加数据到队列: 12 a(addQueue) : 添加队列数据 g(getQueue) : 获取队列数据 s(showQueue) : 展示全部数据 h(headQueue) : 获取头部数据 e(exit) : 退出程序 a 请输入要添加的数据: 13 添加数据到队列: 13 a(addQueue) : 添加队列数据 g(getQueue) : 获取队列数据 s(showQueue) : 展示全部数据 h(headQueue) : 获取头部数据 e(exit) : 退出程序 a 请输入要添加的数据: 14 添加数据到队列: 14 队列已经存满, 不能存入: 14 a(addQueue) : 添加队列数据 g(getQueue) : 获取队列数据 s(showQueue) : 展示全部数据 h(headQueue) : 获取头部数据 e(exit) : 退出程序 s arr[0]=11 arr[1]=12 arr[2]=13 a(addQueue) : 添加队列数据 g(getQueue) : 获取队列数据 s(showQueue) : 展示全部数据 h(headQueue) : 获取头部数据 e(exit) : 退出程序
-
使用数组模拟环形队列-优化
对前面的数组模拟队列的优化, 充分利用数组,因此将数组看做是一个环形的, (通过取模的方式实现即可)
分析说明
尾部索引的下一个为头部索引时表示队列满, 即将队列容量空出一个作为约定, 这个在做判断队列满的时候需要注意: (rear + 1) % maxSize == front , 表达式成立代表队列满
- rear == front 代表队列为空
- 分析示意图
思路如下
- front变量的含义做一个调整: front就指向队列的第一个元素, 也就是说arr[front]就是队列的第一个元素, front的初始值为0
- rear变量的含义做一个调整: rear指向队列的最后一个元素的后一个位置,因为希望空出一个空间作为约定,rear的初始值为0
- 当队列满时, 条件是 (rear + 1) % maxSize = front
- 当队列为空时, 条件是 rear == front
- 当我们这样分析. 队列中有效的数据的个数 (rear + maxSize - front ) % maxSize // rear=1 front = 0
- 我们就可以在原来的队列上修改得到一个环形队列
代码实现
```java package com.dance.queue;
import java.util.Scanner;
public class CircleArrayQueueDemo {
public static void main(String[] args) {
CircleArrayQueue queue = new CircleArrayQueue(3);
menuMain(queue);
}
public static void menuMain(CircleArrayQueue queue) {
Scanner scanner = new Scanner(System.in);
char key = ' ';
while (key != 'e') {
printMenu();
// 获取用户输入
key = scanner.next().charAt(0);
executeMenu(key, scanner, queue);
}
}
public static void printMenu() {
System.out.println("a(addQueue) : 添加队列数据");
System.out.println("g(getQueue) : 获取队列数据");
System.out.println("s(showQueue) : 展示全部数据");
System.out.println("h(headQueue) : 获取头部数据");
System.out.println("e(exit) : 退出程序");
}
public static void executeMenu(char key, Scanner scanner, CircleArrayQueue queue) {
switch (key) {
case 'a':
System.out.println("请输入要添加的数据:");
int i = scanner.nextInt();
System.out.println("添加数据到队列: " + i);
queue.addQueue(i);
break;
case 'g':
try {
System.out.println("获取到的数据为: " + queue.getQueue());
} catch (Exception e) {
System.out.println(e.getMessage());
}
break;
case 's':
queue.showQueue();
break;
case 'h':
try {
System.out.println("队列头部数据为: " + queue.headQueue());
} catch (Exception e) {
System.out.println(e.getMessage());
}
break;
case 'e':
scanner.close();
System.out.println("程序退出~~~");
}
}
}
/**
使用数组模拟-队列 */ class CircleArrayQueue {
/**
数组最大值 */ private int maxSize;
/**
队列首指针 */ private int front;
/**
队列尾指针 */ private int rear;
/**
数据存放数组 */ private int[] array;
/**
- 创建队列的构造器
- front = -1 , 指向队列头部, 分析出front是指向队列头的前一个位置
- rear = -1 , 指向队列尾部, 指向队列尾部的数据(即就是队列的最后一个数据) *
@param maxSize 队列最大容量 */ public CircleArrayQueue(int maxSize) { this(maxSize, 0, 0); }
private CircleArrayQueue(int maxSize, int front, int rear) { this.maxSize = maxSize; this.front = front; this.rear = rear; this.array = new int[maxSize]; }
/**
- 队列是否有可用空间 *
@return true: 没有 , false: 有 */ public boolean arrayIsFull() { return (rear + 1) % maxSize == front; }
/**
- 队列是否为空 *
@return true: 为空, false: 不为空 */ public boolean arrayIsEmpty() { return front == rear; }
/**
- 添加数据到队列中 *
@param newValue 要入队的值 */ public void addQueue(int newValue) { if (arrayIsFull()) {
System.out.println("队列已经存满, 不能存入: " + newValue); return;
} array[rear] = newValue; // 将rear后移 这里必须考虑取模 rear = (rear + 1) % maxSize; }
/**
- 获取元素 *
@return 返回最先进入的数据, 保证队列的先入先出 */ public int getQueue() { if (arrayIsEmpty()) {
throw new ArrayIndexOutOfBoundsException("数组为空");
} int resultIndex = front;
// 这里需要分析出 front是指向队列的第一个元素 // 先把 front 保存到临时的变量 // 将front 后移 // 使用临时变量返回 // 后移时考虑取模, 防止移动到最后 front = (front + 1) % maxSize;
return array[resultIndex]; }
/**
展示队列的全部数据 */ public void showQueue() { if (arrayIsEmpty()) {
System.out.println("数组为空"); return;
} // 从front 开始遍历 // 获取有效数据的个数 int length = (rear + maxSize - front) % maxSize; for (int i = front; i < front + length; i++) {
System.out.printf("arr[%d]=%d\n", i % maxSize, array[i % maxSize]);
} }
/**
- 获取队列的头节点 *
- @return 队列头部的数据
*/
public int headQueue() {
if (arrayIsEmpty()) {
} // 不能使用++ 因为会改变自身的值, 这里并不取数据只是 查看头部 return array[front]; } } ```throw new ArrayIndexOutOfBoundsException("数组为空");
测试执行
```java a(addQueue) : 添加队列数据 g(getQueue) : 获取队列数据 s(showQueue) : 展示全部数据 h(headQueue) : 获取头部数据 e(exit) : 退出程序 a 请输入要添加的数据: 10 添加数据到队列: 10 a(addQueue) : 添加队列数据 g(getQueue) : 获取队列数据 s(showQueue) : 展示全部数据 h(headQueue) : 获取头部数据 e(exit) : 退出程序 a 请输入要添加的数据: 20 添加数据到队列: 20 a(addQueue) : 添加队列数据 g(getQueue) : 获取队列数据 s(showQueue) : 展示全部数据 h(headQueue) : 获取头部数据 e(exit) : 退出程序 a 请输入要添加的数据: 30 添加数据到队列: 30 队列已经存满, 不能存入: 30 a(addQueue) : 添加队列数据 g(getQueue) : 获取队列数据 s(showQueue) : 展示全部数据 h(headQueue) : 获取头部数据 e(exit) : 退出程序 g 获取到的数据为: 10 a(addQueue) : 添加队列数据 g(getQueue) : 获取队列数据 s(showQueue) : 展示全部数据 h(headQueue) : 获取头部数据 e(exit) : 退出程序 g 获取到的数据为: 20 a(addQueue) : 添加队列数据 g(getQueue) : 获取队列数据 s(showQueue) : 展示全部数据 h(headQueue) : 获取头部数据 e(exit) : 退出程序 a 请输入要添加的数据: 10 添加数据到队列: 10 a(addQueue) : 添加队列数据 g(getQueue) : 获取队列数据 s(showQueue) : 展示全部数据 h(headQueue) : 获取头部数据 e(exit) : 退出程序 a 请输入要添加的数据: 50 添加数据到队列: 50 a(addQueue) : 添加队列数据 g(getQueue) : 获取队列数据 s(showQueue) : 展示全部数据 h(headQueue) : 获取头部数据 e(exit) : 退出程序 a 请输入要添加的数据: 60 添加数据到队列: 60 队列已经存满, 不能存入: 60 a(addQueue) : 添加队列数据 g(getQueue) : 获取队列数据 s(showQueue) : 展示全部数据 h(headQueue) : 获取头部数据 e(exit) : 退出程序 s arr[2]=10 arr[0]=50 a(addQueue) : 添加队列数据 g(getQueue) : 获取队列数据 s(showQueue) : 展示全部数据 h(headQueue) : 获取头部数据 e(exit) : 退出程序 g 获取到的数据为: 10 a(addQueue) : 添加队列数据 g(getQueue) : 获取队列数据 s(showQueue) : 展示全部数据 h(headQueue) : 获取头部数据 e(exit) : 退出程序 g 获取到的数据为: 50 a(addQueue) : 添加队列数据 g(getQueue) : 获取队列数据 s(showQueue) : 展示全部数据 h(headQueue) : 获取头部数据 e(exit) : 退出程序 s 数组为空 a(addQueue) : 添加队列数据 g(getQueue) : 获取队列数据 s(showQueue) : 展示全部数据 h(headQueue) : 获取头部数据 e(exit) : 退出程序 e 程序退出~~~
成功解决了,数据存放的问题, 但是里面还是放弃了一个数据位置用来做约定
<a name="sSIHv"></a>
### 图解代码
因为我对算法也不懂, 看弹幕上说的是都懂了,但是我~,图解一下吧<br />先说一下取模是什么吧, 我也不太懂, 感觉应该是取余数<br />在控制台执行了一下, 就是这样的结果
```java
1 % 3
1 剩余1
2 % 3
2 剩余2
3 % 3
0 剩余0
4 % 3
1 剩余1 后面应该是循环了