数据结构与算法—线性与非线性结构、稀疏矩阵和队列
尚硅谷韩老师数据结构与算法:线性与非线性结构、稀疏矩阵和队列
文章目录
1. 线性结构与非线性结构
数据结构包括:线性结构和非线性结构。
1.1 线性结构
- 线性结构作为最常用的数据结构,其特点是数据元素之间存在一对一的线性关系
- 线性结构有两种不同的存储结构,即顺序存储结构和链式存储结构。顺序存储的线性表称为顺序表,顺序表中的存储元素是连续的(地址连续)。
- 链式存储的线性表称为链表,链表中的存储元素不一定是连续的,元素节点中存放数据元素以及相邻元素的地址信息。
- 常见的线性结构有:数组、队列、链表和栈
1.2 非线性结构
非线性结构包括:二维数组,多维数组、广义表、树结构、图结构。
2. 稀疏数组
需求引入:对于一个很多值默认值是0的二维数组,记录了很多没有意义的数据(值为0的数据)
这个时候就可以用一个稀疏数组对此二维数组进行压缩
2.1 基本介绍
当一个数组中大部分元素为0,或者为同一个值的数组时,可以使用稀疏数组来保存该数组。
稀疏数组的处理方法是:
- 记录数组一共有几行几列,有多少个不同的值
- 把具有不同值的元素的行列及值记录在一个小规模的数组中,从而缩小程序的规模。
举例说明
如图1所示,左侧的数组为保存了所有数据值的6 * 7的二维数组,右侧是转换为9 * 3的稀疏数组,后者相对于前者少了一定的数据,缩小了数据的规模。其中图1中稀疏数组的第一行的三个元素分别表示原数组的行数、列数、有效数据值的个数,后面每一行都分别保存一个有效数据值所在原数组中第几行、第几列、值。
2.2 二维数组与稀疏数组的相互转化
2.2.1 二维数据转稀疏数组
二维数组转稀疏数组的思路:
- 遍历原始的二维数组,得到有效数据的个数sum
- 根据sum就可以创建稀疏数组大小为:sum+1 * 3
- 将二维数组的有效数据存入到稀疏数组中
2.2.2 稀疏数组转二维数组
稀疏数组转二维数组思路:
- 先读取稀疏数组的第一行,根据第一行的数据,创建原始的二维数组。
- 再读取稀疏数组后几行的数据,并赋给原始的二维数组即可。
2.2.3 代码实现
public class SparseArray {
public static void main(String[] args) {
//先创建一个原始的二维数组 11*11
//0:表示没有棋子 1:表示黑子 2:表示篮子
int[][] chessArr1 = new int[11][11];
chessArr1[1][2] = 1;
chessArr1[2][3] = 2;
//输出原始二维数组
System.out.println("==============原始的二维数组===============");
for (int[] row : chessArr1) {
for (int data : row) {
System.out.print(data + "\t");
}
System.out.println();
}
//将二维数组转稀疏数组
//1. 先遍历二维数组 得到非0数据的个数
int sum = 0;
for (int[] row : chessArr1) {
for (int data : row) {
if (data != 0) {
sum++;
}
}
}
System.out.println("数组不为零的个数为sum:" + sum);
//2. 创建对应的稀疏数组
int[][] sparseArr = new int[sum + 1][3];
//给稀疏数组赋值
sparseArr[0][0] = chessArr1.length;//行数
sparseArr[0][1] = chessArr1[0].length;//列数
sparseArr[0][2] = sum; //有效数据的个数
int k = 1;//用于记录是第一个非零值
//遍历二维数组 将非零的值存放到sparseArr中
for (int i = 0; i < chessArr1.length; i++) {
for (int j = 0; j < chessArr1[0].length; j++) {
if (chessArr1[i][j] != 0) {
sparseArr[k][0] = i;//保存此值的行数
sparseArr[k][1] = j;//保存此值的列数
sparseArr[k++][2] = chessArr1[i][j];//保存值
}
}
}
//输出稀疏数组的形式
System.out.println("==============得到的稀疏数组===============");
for (int[] row : sparseArr) {
for (int data : row) {
System.out.print(data + "\t");
}
System.out.println();
}
//将稀疏数组 -->恢复为原始的二维数组
//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.print(data + "\t");
}
System.out.println();
}
}
}
2.2.4 课后练习
- 在前面的基础上 将稀疏数组保存到磁盘上,比如map.data
- 恢复原来的数组时,读取map.data进行恢复
实现代码如下:
//在前面的基础上 将稀疏数组保存到磁盘上,比如map.data
try {
DataOutputStream dataOutputStream = new DataOutputStream(new FileOutputStream("map.dat"));
// 保存稀疏数组的行和列信息
dataOutputStream.writeInt(sparseArr.length);
dataOutputStream.writeInt(sparseArr[0].length);
for (int i = 0; i < sparseArr.length; i++) {
for (int j = 0; j < sparseArr[0].length; j++) {
if (sparseArr[i][j] != 0) {
// 保存非零元素的行、列和值
dataOutputStream.writeInt(i);
dataOutputStream.writeInt(j);
dataOutputStream.writeInt(sparseArr[i][j]);
}
}
}
dataOutputStream.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
//恢复原来的数组时,读取map.data进行恢复
try {
DataInputStream dataInputStream = new DataInputStream(new FileInputStream("map.dat"));
int rows = dataInputStream.readInt();
int cols = dataInputStream.readInt();
int[][] sparseArr2 = new int[rows][cols];
while (dataInputStream.available() > 0) {
int row = dataInputStream.readInt();
int col = dataInputStream.readInt();
int value = dataInputStream.readInt();
sparseArr2[row][col] = value;
}
dataInputStream.close();
System.out.println("==============由稀疏数组得到的二维数组===============");
for (int[] row : sparseArr2) {
for (int data : row) {
System.out.print(data + "\t");
}
System.out.println();
}
} catch (IOException e) {
throw new RuntimeException(e);
}
或者使用字符包装流
try {
BufferedWriter writer = new BufferedWriter(new FileWriter("src\\map.txt"));
// 保存稀疏数组的行和列信息
writer.write(sparseArr.length + " " + sparseArr[0].length);
writer.newLine();
for (int i = 0; i < sparseArr.length; i++) {
for (int j = 0; j < sparseArr[0].length; j++) {
if (sparseArr[i][j] != 0) {
// 保存非零元素的行、列和值
writer.write(i + " " + j + " " + sparseArr[i][j]);
writer.newLine();
}
}
}
writer.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
try {
BufferedReader reader = new BufferedReader(new FileReader("src\\map.txt"));
String line = reader.readLine();
String[] dimensions = line.split(" ");
int rows = Integer.parseInt(dimensions[0]);
int cols = Integer.parseInt(dimensions[1]);
int[][] sparseArr2 = new int[rows][cols];
while ((line = reader.readLine()) != null) {
String[] elements = line.split(" ");
int row = Integer.parseInt(elements[0]);
int col = Integer.parseInt(elements[1]);
int value = Integer.parseInt(elements[2]);
sparseArr2[row][col] = value;
}
reader.close();
System.out.println("==============由稀疏数组得到的二维数组===============");
for (int[] row : sparseArr2) {
for (int data : row) {
System.out.print(data + "\t");
}
System.out.println();
}
} catch (IOException e) {
throw new RuntimeException(e);
}
3.队列
3.1 队列介绍
队列介绍:
-
队列是一个有序列的表,可以用数组或是链表来实现
-
遵循先入先出的原则,即:先存入队列的数据,要先取出。后存入的要后取出。
-
示意图:(使用数组模拟队列示意图)
图2中,变量rear随着数据的加入而变化,变量front随着数据的取出而变化。由图2可知,先进入的数据先被取走(0、1处的数据比2、3处的数据先放入,最后也是先被取出)。
3.2 数组模拟队列
数组模拟队列:
-
队列本身是有序列表,若使用数组的结构来存储队列的数据,则队列数组的声明如下图,其中maxSize是该队列的最大容量。
-
因为队列的输出、输入是分别从前后端来处理,因此需要两个变量front及rear分别记录队列前后端的下标,front会随着数据输出而改变,而rear则会随着数据输入而改变,如图2所示。
-
当需要将数据存入到队列中时,称为"addQueue",addQueue的处理需要有两个步骤:
- 当front==rear(此时为空),将尾指针往后移:rear++;
- 当rear<maxSize-1(此时队列未满),将尾指针往后移:rear++。否则说明队列满了,无法存入数据。
//使用数组模拟队列 编写一个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;
}
//判断队列是否为空
public boolean isEmpty() {
//因为每次取数据都是取的是front+1时的数据
//但是一旦front==rear 那么说明后面没数据了
//则队列为空了
return rear == front;
}
//添加数据到队列
public void addQueue(int n) {
//判断队列是否满
if (isFull()) {
System.out.println("队列满,不能加入数据~~~");
return;
}
arr[++rear] = n;
}
//获取队列的数据,出队列 为null则失败
public Integer getQueue() {
//判断队列是否为空
if (isEmpty()) {
System.out.println("队列空,取不出来数据~~~");
return null;
// //通过抛出异常处理
// throw new RuntimeException("队列空,不能取数据");
}
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 Integer headQueue() {
//判断
if (isEmpty()) {
System.out.println("队列是空的,没有数据~~");
return null;
// throw new RuntimeException("队列空,没有数据");
}
return arr[front + 1];
}
}
问题分析并优化
- 目前数组使用一次就不能用,没有达到复用的效果
- 将这个数组使用算法,改进成一个环境队列
3.3 使用数组模拟环形队列的思路分析
俺先在上面基础上写了一个环形队列,测试的话还能用,显示队列的功能没有完善(从零显示到尾)
//使用数组模拟队列 编写一个ArrayQueue类
class ArrayQueue {
private int maxSize; //表示数组的最大容量
//队列头 指向的位置 代表已经取出了相应位置的值
private int front; //比如front=2 则说明第2个位置不在栈中了(从0开始计数) front + 1为第一个有效数据的位置
private int rear; //队列尾 指向的位置 代表最新添加的数据所在的位置
private int[] arr; //该数据用于存放数据,模拟队列
private boolean flag; //用来辅助判断是否为空 true则说明上一次执行的是添加数据操作
//创建队列的构造器
public ArrayQueue(int arrMaxSize) {
maxSize = arrMaxSize;
arr = new int[maxSize];
front = -1; //指向队列头部 分析出front是指向队列头的第一个位置
rear = -1; //指向尾,指向队列尾的数据(即就是队列的最后一个数据)
}
//判断队列是否满
public boolean isFull() {
// return rear == maxSize - 1;
//因为每次读入的数据都写在第rear+1个位置
//如果第rear+1个位置正好在第一个有效数据的位置(front+1) 则说明写满了
if(front == -1){//说明还没取出过数据1
return rear == maxSize - 1;
}else {
return (rear + 1 > maxSize - 1 ? 0 : rear + 1) == (front + 1 > maxSize - 1 ? 0 :front + 1);
}
//return ((rear + 1 > maxSize - 1 ? 0 : rear) == front && front != -1) || (front == -1 && rear == maxSize - 1);
}
//判断队列是否为空
public boolean isEmpty() {
if(flag){//如果上一个是添加数据操作 则直接返回false
return false;
}
//否则 如果最新添加的数据的位置是与front相同
//则说明最新添加的数据也被取走了
//则此时没有数据了
return front == rear;
}
//添加数据到队列
public void addQueue(int n) {
//判断队列是否满
if (isFull()) {
System.out.println("队列满,不能加入数据~~~");
return;
}
//++rear 说明添加的数据值的位置放在rear+1处 且rear + 1 代表最新添加的数据的位置
arr[++rear > maxSize - 1 ? rear = 0 : rear] = n;
flag = true;
}
//获取队列的数据 出队列 为null则失败
public Integer getQueue() {
//判断队列是否为空
if (isEmpty()) {
System.out.println("队列空,取不出来数据~~~");
return null;
// //通过抛出异常处理
// throw new RuntimeException("队列空,不能取数据");
}
flag = false;
//++front 说明取出的值为front+1出的位置 且front + 1 代表此位置被取走了
//那么此时一个有效数据的位置为 在得到++front更新后的front值再加1
return arr[++front > maxSize - 1 ? front = 0 : 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 Integer headQueue() {
//判断
if (isEmpty()) {
System.out.println(front);
System.out.println("队列是空的,没有数据~~");
return null;
// throw new RuntimeException("队列空,没有数据");
}else if(front == -1){//此时不为空 但是也没取出过数据
return arr[0];
}
//front + 1 第一个有效数据的位置
return arr[front + 1 > maxSize - 1 ? 0 : front + 1];
}
韩老师将front、rear变量的含义均做了一个调整,思路如下:
-
front就指向队列的第一个元素,即arr[front]就是队列的第一个有效数据
- 所以现在取出来就应该执行return arr[front++];先用front再加1
- 初始值一开始指向位置为零的第1个元素 front = 0;
-
rear指向队列的最后一个元素(新添加元素)的后一个位置。主要是想空出一个空间作为约定
- 所以现在添加元素应该是执行arr[rear++] = n;先用rear再加1
- 初始值一开始指向位置为零的第1个元素 rear = 0;
-
当队列满时,条件是(rear + 1) % maxSize == front 或者 (rear + 1 > maxSize - 1? 0 : rear +1) ==front
- rear + 1得到的就是最后一个元素的后面第二个位置 如果此位置和front队列元素的第一个位置相同 则此时满了
- 说明最后一个元素的后面第一个位置直接被跳过去了 没有利用到 所以这里牺牲了一个空间(最后一个元素的后一个空间)
- 也就是说当最后一个的后面第二个空间为队列第一个有效数据时 那么此时的最后一个元素的后一个空间永远不存数据 因为此时就认为是满了 不存数据了
- 按道理来说应该是最后一个元素的后面第一个位置如果与front队列元素第一个位置相同才说明满了
- 这是为了避免出现我上个代码中写的问题(出现了一个flag辅助判断是否为满)
-
当队列空时,条件是front == rear
- 也就是说当队列的一个元素和队列最后一个元素的后一个位置相同时 说明空了
- 因为队列最后一个元素的后一个位置肯定是没存有效数据的 此时如果队列一个元素还和其位置相同
- 那就说明刚好读空了 有一个元素的条件的front + 1 == rear;第二个元素的位置是空的
-
当我们这样分析 队列中有效数据的个数则为 (maxSize + rear - front ) % maxSize
-
有效个数 其实就是从rear的位置开始 数前面位置(rear-1 rear-2…)的数 直到数到front的位置 总和
-
当rear > front时 直接就是rear - front 此时就是rear与front的长度
-
当rear < front时 如果直接rear-front(=-(front - rear)) 则得到的是 从front开始往前数 数到rear 那么得到的就是无效数据的个数的负数 若加上 maxSize(总长度、总个数) 最后得到的就是有效数据的个数,这才是我们需要求得数据。即应该为rear-front + maxSize
-
接下来将两个条件写成一个等式,注意到两个条件最后得到的都是整数 如果对第一个条件也加上maxSize ,则两个条件得等式相同 但是由于第一个条件会多出一个maxSize 所以再对式子取模maxSize 那么还是得到正确的结果。同样此时第二个条件也还是得到正确的结果
-
综上所示 条件为 (maxSize + rear - front ) % maxSize
韩老师代码如下
-
//使用数组模拟队列 编写一个ArrayQueue类
class ArrayQueue {
private int maxSize; //表示数组的最大容量
//队列头 指向的位置 代表已经取出了相应位置的值
private int front; //比如front=2 则说明第2个位置不在栈中了(从0开始计数) front + 1为第一个有效数据的位置
private int rear; //队列尾 指向的位置 代表最新添加的数据所在的位置
private int[] arr; //该数据用于存放数据,模拟队列
private boolean flag; //用来辅助判断是否为空 true则说明上一次执行的是添加数据操作
//创建队列的构造器
public ArrayQueue(int arrMaxSize) {
maxSize = arrMaxSize;
arr = new int[maxSize];
front = -1; //指向队列头部 分析出front是指向队列头的第一个位置
rear = -1; //指向尾,指向队列尾的数据(即就是队列的最后一个数据)
}
//判断队列是否满
public boolean isFull() {
// return rear == maxSize - 1;
//因为每次读入的数据都写在第rear+1个位置
//如果第rear+1个位置正好在第一个有效数据的位置(front+1) 则说明写满了
if(front == -1){//说明还没取出过数据1
return rear == maxSize - 1;
}else {
return (rear + 1 > maxSize - 1 ? 0 : rear + 1) == (front + 1 > maxSize - 1 ? 0 :front + 1);
}
//return ((rear + 1 > maxSize - 1 ? 0 : rear) == front && front != -1) || (front == -1 && rear == maxSize - 1);
}
//判断队列是否为空
public boolean isEmpty() {
if(flag){//如果上一个是添加数据操作 则直接返回false
return false;
}
//否则 如果最新添加的数据的位置是与front相同
//则说明最新添加的数据也被取走了
//则此时没有数据了
return front == rear;
}
//添加数据到队列
public void addQueue(int n) {
//判断队列是否满
if (isFull()) {
System.out.println("队列满,不能加入数据~~~");
return;
}
//++rear 说明添加的数据值的位置放在rear+1处 且rear + 1 代表最新添加的数据的位置
arr[++rear > maxSize - 1 ? rear = 0 : rear] = n;
flag = true;
}
//获取队列的数据 出队列 为null则失败
public Integer getQueue() {
//判断队列是否为空
if (isEmpty()) {
System.out.println("队列空,取不出来数据~~~");
return null;
// //通过抛出异常处理
// throw new RuntimeException("队列空,不能取数据");
}
flag = false;
//++front 说明取出的值为front+1出的位置 且front + 1 代表此位置被取走了
//那么此时一个有效数据的位置为 在得到++front更新后的front值再加1
return arr[++front > maxSize - 1 ? front = 0 : 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 Integer headQueue() {
//判断
if (isEmpty()) {
System.out.println(front);
System.out.println("队列是空的,没有数据~~");
return null;
// throw new RuntimeException("队列空,没有数据");
}else if(front == -1){//此时不为空 但是也没取出过数据
return arr[0];
}
//front + 1 第一个有效数据的位置
return arr[front + 1 > maxSize - 1 ? 0 : front + 1];
}
}