文章目录


1.稀疏数组的应用场景

先看一个实际的需求

  • 编写的五子棋程序中,有​​存盘退出​​​和​​续上盘​​的功能。

数据结构之稀疏数组与环形队列_queue

  • ​分析问题​​:
  • 因为该二维数组的很多值是默认值0,因此记录了很多没有意义的数据 ->​​稀疏数组​
  • 稀疏数组的基本介绍
  • 当一个数组中大部分元素为0,或者为同一个值的数组时,可以使用稀疏数组来保存该数组。
  • 稀疏数组的处理方法是:
  • 记录数组一共有​​几行几列​​,有多少个不同的值
  • 把具有不同值的元素的行列及值记录在一个小规模的数组中,从而缩小程序的规模
  • 稀疏数组举例说明:

数据结构之稀疏数组与环形队列_数据结构_02

2.稀疏数组转换的思路分析

  • 应用实例
  • 使用稀疏数组,来保留类似前面的二维数组(棋盘、地图等等)
  • 把稀疏数组存盘,并且可以从新恢复原来的二维数组数

数据结构之稀疏数组与环形队列_数据结构_03

思路分析

二维数组  稀疏数组的思路
1. 遍历原始的二维数组,得到有效数据的个数 sum
2. 根据 sum 就可以创建稀疏数组 sparseArr int[sum + 1] [3],表示"sum+1"行3列
3. 将二维数组的有效数据数据存入到稀疏数组中

稀疏数组 原始的二维数组的思路
1. 先读取稀疏数组的第一行,根据第一行的数据,创建原始的二维数组,比如上面的 chessArr2 = int [11][11]
2. 再读取稀疏数组后几行的数据,并赋给 原始的二维数组 即可.

3.稀疏数组的代码实现

/**
* @author xiexu
* @create 2020-11-02 11:04 上午
*/
public class SparceArray {

public static void main(String[] args) {
//创建一个原始的二维数组 11*11
//0表示没有棋子,1表示黑子,2表示白子
int chessArr[][] = new int[11][11];
chessArr[1][3] = 5;
chessArr[2][1] = 17;
chessArr[5][5] = 21;
//输出原始的二维数组
System.out.println("原始的二维数组:");
for (int[] row : chessArr) { //遍历出来的是一维数组
for (int data : row) { //这一层遍历出来的才是对应的值
System.out.printf("%d\t",data);
}
System.out.println();
}

//二维数组 转 稀疏数组
//1.先遍历二维数组,得到非0数据的个数
int sum = 0;
for (int i = 0;i < chessArr.length;i++) {
for (int j = 0;j < chessArr.length;j++) {
if (chessArr[i][j] != 0) {
sum++;
}
}
}

//2.创建对应的稀疏数组
int sparseArr[][] = new int[sum+1][3];
//给稀疏数组赋值
sparseArr[0][0] = chessArr.length;
sparseArr[0][1] = chessArr.length;
sparseArr[0][2] = sum; //有效数据的个数,即非0的值

//遍历二维数组,将非0的值存入到稀疏数组saprseArr中
int count = 0; //用于记录是第几个非0数据
for (int i = 0;i < chessArr.length;i++) {
for (int j = 0;j < chessArr.length;j++) {
if (chessArr[i][j] != 0) {
count++;
sparseArr[count][0] = i; //第一列
sparseArr[count][1] = j; //第二列
sparseArr[count][2] = chessArr[i][j]; //第三列
}
}
}

//输出稀疏数组的形式
System.out.println();
System.out.println("得到的稀疏数组:");
for (int i = 0; i < sparseArr.length; i++) {
System.out.printf("%d\t%d\t%d\n",sparseArr[i][0],sparseArr[i][1],sparseArr[i][2]);
}
System.out.println();

//将稀疏数组恢复为原始的二维数组
/*
* 1.先读取稀疏数组的第一行,根据第一行的数据,创建原始的二维数组,比如上面的 chessArr2 = int [11][11]
* 2.在读取稀疏数组后几行的数据,并赋给原始的 二维数组 即可.
*/
//1.先读取稀疏数组的第一行,根据第一行的数据,创建原始的二维数组
int chessArr2[][] = new int[sparseArr[0][0]][sparseArr[0][1]];
//2.在读取稀疏数组后几行的数据(从第二行开始),并赋给 原始的二维数组
for (int i = 1; i < sparseArr.length; i++) {
chessArr2[sparseArr[i][0]][sparseArr[i][1]] = sparseArr[i][2];
}

//输出恢复后的二维数组
System.out.println("恢复后的二维数组:");
for (int[] row : chessArr2) { //行
for (int data : row) { //列
System.out.printf("%d\t",data);
}
System.out.println();
}

}

}

3.1 课后练习

/**
* 练习:
* 1.在前面的基础上,将稀疏数组保存到磁盘上,比如 map.data
* 2.恢复原来的数组时,读取 map.data 进行恢复
*
* @author xiexu
* @create 2020-11-02 20:00 下午
*/
public class SparceArray {

public static void main(String[] args) {
//创建一个原始的二维数组 11*11
//0表示没有棋子,1表示黑子,2表示白子
int chessArr[][] = new int[11][11];
chessArr[1][3] = 5;
chessArr[2][1] = 17;
chessArr[5][5] = 21;
//输出原始的二维数组
System.out.println("原始的二维数组:");
for (int[] row : chessArr) { //遍历出来的是一维数组
for (int data : row) { //这一层遍历出来的才是对应的值
System.out.printf("%d\t",data);
}
System.out.println();
}

//二维数组 转 稀疏数组
//1.先遍历二维数组,得到非0数据的个数
int sum = 0;
for (int i = 0;i < chessArr.length;i++) {
for (int j = 0;j < chessArr.length;j++) {
if (chessArr[i][j] != 0) {
sum++;
}
}
}

//2.创建对应的稀疏数组
int sparseArr[][] = new int[sum+1][3];
//给稀疏数组赋值
sparseArr[0][0] = chessArr.length;
sparseArr[0][1] = chessArr.length;
sparseArr[0][2] = sum; //有效数据的个数,即非0的值

//遍历二维数组,将非0的值存入到稀疏数组saprseArr中
int count = 0; //用于记录是第几个非0数据
for (int i = 0;i < chessArr.length;i++) {
for (int j = 0;j < chessArr.length;j++) {
if (chessArr[i][j] != 0) {
count++;
sparseArr[count][0] = i; //第一列
sparseArr[count][1] = j; //第二列
sparseArr[count][2] = chessArr[i][j]; //第三列
}
}
}

//保存稀疏数组到文件中
System.out.println();
System.out.println("写入文件...");
System.out.println("得到的稀疏数组为: ");
ObjectOutputStream oos = null;
try {
FileOutputStream fos = new FileOutputStream("/Users/xiexu/sgg-workspace_idea/DataStructures/sparsearray/map.data");
oos = new ObjectOutputStream(fos);
for (int i = 0; i < sparseArr.length; i++) {
System.out.printf("%d\t%d\t%d\n",sparseArr[i][0],sparseArr[i][1],sparseArr[i][2]);
oos.writeObject(sparseArr[i][0]);
oos.flush();
oos.writeObject(sparseArr[i][1]);
oos.flush();
oos.writeObject(sparseArr[i][2]);
oos.flush();
}
//在最后添加一个null值,目的是为了后面的循环判断
oos.writeObject(null);
oos.flush();
System.out.println("写入成功...");
} catch (IOException e) {
e.printStackTrace();
} finally {
if (oos != null) {
try {
oos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}

//将读取到的文件 恢复成 原来的二维数组
Object obj = null;
ObjectInputStream ois = null;
//创建一个list集合,用于存储读取到的文件内容
ArrayList<Integer> list = new ArrayList<>();
int[][] arr = null;
try {
FileInputStream fis = new FileInputStream("/Users/xiexu/sgg-workspace_idea/DataStructures/sparsearray/map.data");
ois = new ObjectInputStream(fis);
Object object = ois.readObject();
while (object != null) {
list.add((Integer) object);
object = ois.readObject();
}
//将list集合 还原为 稀疏数组arr
int counts = 0;
arr = new int[list.size()/3][3];
for (int i = 0; i < arr.length; i++) {
for (int j = 0; j < 3; j++) {
arr[i][j] = list.get(counts);
counts++;
}
}
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
} finally {
if (ois != null) {
try {
ois.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}

//将稀疏数组恢复为原始的二维数组
//1.先读取稀疏数组的第一行,根据第一行的数据,创建原始的二维数组
System.out.println();
int chessArr2[][] = new int[arr[0][0]][arr[0][1]];
//2.在读取稀疏数组后几行的数据(从第二行开始),并赋给 原始的二维数组
for (int i = 1; i < arr.length; i++) {
chessArr2[arr[i][0]][arr[i][1]] = arr[i][2];
}
//输出恢复后的二维数组
System.out.println("恢复后的二维数组:");
for (int[] row : chessArr2) { //行
for (int data : row) { //列
System.out.printf("%d\t",data);
}
System.out.println();
}

}

}

4.队列的应用场景和介绍

数据结构之稀疏数组与环形队列_queue_04

  • 队列是一个有序列表,可以用​​数组​​​或是​​链表​​来实现
  • 遵循​​先入先出​​的原则。即:先存入队列的数据,要先取出。后存入的要后取出
  • 示意图:( 使用​​数组模拟队列​​示意图 )

数据结构之稀疏数组与环形队列_数据结构_05

5.数组模拟队列的思路分析

  • 队列本身是​​有序列表​​​,若使用数组的结构来存储队列的数据,则队列数组的声明如下图, 其中​​maxSize​​ 是该队列的最大容量。
  • 因为队列的输出、输入是分别从前后端来处理,因此需要两个变量​​front​​​及​​rear​​​分别记录队列​​前端​​​和​​后端​​的下标,front 会随着数据输出而改变,而 rear则是随着数据输入而改变,如图所示:

数据结构之稀疏数组与环形队列_数组_06

  • 当我们将数据存入队列时称为​​addQueue​​​,addQueue 的处理需要有两个步骤:​​思路分析​
  • 将尾指针往后移:rear+1 , 判断条件是当队列为空或者队列还没满
  • 若尾指针 rear​​小于​​​队列的最大下标 maxSize-1,则将数据存入 rear所指的数组元素中,否则无法存入数据。 rear == maxSize - 1​​[队列满]​

6.数组模拟队列的代码实现

/**
* 用数组模拟队列
* @author xiexu
* @create 2020-11-02 8:55 下午
*/
public class ArrayQueueDemo {

public static void main(String[] args) {
//测试代码
//创建一个队列
ArrayQueue queue = new ArrayQueue(3);
char key = ' '; //接收用户的输入
Scanner scanner = new Scanner(System.in);
boolean loop = true;
//输出一个菜单
while (loop) {
System.out.println("s(show):显示队列");
System.out.println("e(exit):退出程序");
System.out.println("a(add):添加数据到队列");
System.out.println("g(get):从队列中取出数据");
System.out.println("h(head):查看队列头的数据");
key = scanner.next().charAt(0); //接收一个字符
switch (key) {
case 's': //显示队列
queue.ShowQueue();
break;
case 'a': //添加数据到队列
System.out.println("输入一个值:");
int value = scanner.nextInt();
queue.addQueue(value);
break;
case 'h': //查看队列头的数据
try {
int res = queue.headQueue();
System.out.printf("队列头的数据是%d\n",res);
} catch (Exception e) {
System.out.println(e.getMessage());
}
break;
case 'g': //从队列中取出数据
try {
int res = queue.getQueue();
System.out.printf("取出的数据是%d\n",res);
} catch (Exception e) {
System.out.println(e.getMessage());
}
break;
case 'e': //退出
scanner.close();
loop = false;
System.out.println("程序退出...");
break;
default:
break;
}
}
}

}

//使用数组模拟队列 --> 编写一个ArrayQueue类
class ArrayQueue {
private int maxSize; //表示数组的最大容量
private int front; //队列头
private int rear; //队列尾
private int[] arr; //该数组用于存放数据,模拟队列

//创建队列的构造器
public ArrayQueue(int arrMaxSize) {
maxSize = arrMaxSize;
arr = new int[maxSize];
front = -1; //指向队列头部,分析出front是指向队列头的"前一个位置"
rear = -1; //指向队列尾部,指向队列尾部的数据(即队列尾部的"最后一个数据")
}

//判断队列是否满
public boolean isFull() {
return rear == maxSize - 1; //数组的下标从0开始,满为true,不满为false
}

//判断队列是否为空
public boolean isEmpty() {
return rear == front;
}

//添加数据到队列
public void addQueue(int n) {
//判断队列是否满了,满了不加入数据,不满可以加入数据
if (isFull()) { //队列满了
System.out.println("队列满了,不能再加入数据了...");
return;
}
//队列未满
rear++; //rear后移
arr[rear] = n; //添加数据
}

//获取队列的数据,出队列
public int getQueue() {
//判断队列是否为空
if (isEmpty()) {
throw new RuntimeException("队列已经空了,不能取数据...");
}
//如果队列不空,则返回数据
front++; //front后移
return arr[front]; //出队列
}

//显示队列的所有数据
public void ShowQueue() {
//遍历
if (isEmpty()) { //队列为空
System.out.println("队列是空的,没有数据...");
return;
}
//队列不空
for (int i = 0; i < arr.length; i++) {
System.out.printf("arr[%d]=%d\n",i,arr[i]);
}
}

//显示队列的头数据。注意:是显示头数据,不是取出数据
public int headQueue() {
//判断
if (isEmpty()) { //队列为空
throw new RuntimeException("队列空的,没有头数据...");
}
return arr[front+1];
}

}

问题分析并优化

  • 目前数组​​使用一次就不能再使用了​​​,无法达到​​复用​​的效果
  • 将这个数组使用​​算法​​​,改进成一个环形的队列取模:​​%​

7.环形队列的思路分析

数据结构之稀疏数组与环形队列_队列_07

1. front 变量的含义做一个调整:front 就指向队列的第一个元素, 也就是说 arr[front] 就是队列的第一个元素 
front 的初始值 = 0
2. rear 变量的含义做一个调整:rear 指向队列的最后一个元素的后一个位置,因为我希望空出一个空间做为约定.
rear 的初始值 = 0
3. 当队列满时,条件是:(rear + 1) % maxSize == front []
4. 对队列为空的条件:rear == front []
5. 当我们这样分析,队列中有效的数据的个数:(rear + maxSize - front) % maxSize // rear = 1 front = 0
6.

8.环形队列的代码实现

/**
* 数组模拟环形队列
* @author xiexu
* @create 2020-11-03 9:08 上午
*/
public class CircleArrayQueueDemo {

public static void main(String[] args) {
//测试代码
System.out.println("测试数组模拟环形队列的案例");
//创建一个队列
CircleArray queue = new CircleArray(4); //这里设置的4,其队列的有效数据最大是3
char key = ' '; //接收用户的输入
Scanner scanner = new Scanner(System.in);
boolean loop = true;
//输出一个菜单
while (loop) {
System.out.println("s(show):显示队列");
System.out.println("e(exit):退出程序");
System.out.println("a(add):添加数据到队列");
System.out.println("g(get):从队列中取出数据");
System.out.println("h(head):查看队列头的数据");
key = scanner.next().charAt(0); //接收一个字符
switch (key) {
case 's': //显示队列
queue.ShowQueue();
break;
case 'a': //添加数据到队列
System.out.println("输入一个值:");
int value = scanner.nextInt();
queue.addQueue(value);
break;
case 'h': //查看队列头的数据
try {
int res = queue.headQueue();
System.out.printf("队列头的数据是%d\n", res);
} catch (Exception e) {
System.out.println(e.getMessage());
}
break;
case 'g': //从队列中取出数据
try {
int res = queue.getQueue();
System.out.printf("取出的数据是%d\n", res);
} catch (Exception e) {
System.out.println(e.getMessage());
}
break;
case 'e': //退出
scanner.close();
loop = false;
System.out.println("程序退出...");
break;
default:
break;
}
}
}

}

//使用数组模拟环形队列
class CircleArray {

private int maxSize; //表示数组的最大容量

// front 队列头,front 就指向队列的第一个元素, 也就是说 arr[front] 就是队列的第一个元素
// front 的初始值为0
private int front;

// rear 队列尾,rear 指向队列的最后一个元素的后一个位置. 因为希望空出一个空间做为约定.
// rear 的初始值 = 0
private int rear;
private int[] arr; //该数组用于存放数据,模拟队列

//创建队列的构造器
public CircleArray(int maxSize) {
this.maxSize = maxSize;
arr = new int[maxSize];
}

//判断队列是否为满
public boolean isFull() {
return ( (rear+1) % maxSize ) == front;
}

//判断队列是否为空
public boolean isEmpty() {
return rear == front;
}

//添加数据到队列
public void addQueue(int n) {
//判断队列是否满了,满了就不加入,不满则加入数据
if(isFull()) {
System.out.println("队列满了,不能再加了...");
return;
}
//队列未满
arr[rear] = n;
rear = (rear + 1) % maxSize; //将rear后移,必须考虑取模
}

// 获取队列的数据,出队列
public int getQueue() {
// 判断队列是否空
if (isEmpty()) { // 队列为空
// 通过抛出异常来处理
throw new RuntimeException("队列空了,不能再取数据了...");
}
// 队列不空,返回数据
// 1. 先把 front 对应的值保留到一个临时变量
// 2. 将 front 后移, 考虑取模
// 3. 将临时保存的变量返回
int value = arr[front];
front = (front + 1) % maxSize;
return value;
}

// 求出当前队列的长度(即有效个数)
public int size() {
return (rear + maxSize - front) % maxSize;
}

// 显示队列的所有数据
public void ShowQueue() {
// 遍历
if (isEmpty()) { // 队列为空
System.out.println("队列空的,没有数据...");
return;
}
// 队列不空
// 思路:从front开始遍历,遍历多少个元素
for (int i = front; i < front + size(); i++) {
System.out.printf("arr[%d]=%d\n", (i % maxSize), arr[i % maxSize]);
}
}

// 显示队列的头数据。注意:显示头数据,不是取出数据
public int headQueue() {
// 判断
if (isEmpty()) {
throw new RuntimeException("队列空了,没有数据...");
}
return arr[front];
}

}