1.简介
线性结构和非线性结构
数据结构包括:线性结构和非线性结构
线性结构
- 线性结构作为最常用的数据结构,其特点是数据元素之间存在一对一的线性关系(如数组:a[0]只有一个元素)
- 线性结构有两种不同的存储结构,即顺序存储结构(数组)和链式存储结构(链表)。顺序存储的线性表称为顺序表,顺序表中的存储元素是连续的(如数组,在内存中创建的是连续的内存块)
- 链式存储的线性表称之为链表,列表中存储的素不一定是连续的,元素节点中存放数据元素以及相邻元素的地址信息(内存中不是连续的内存块)
- 线性结构常见的有:数组、队列、链表和栈
非线性结构
非线性结构包括:二维数组,多维数组,广义表,树结构,图结构
1.稀疏数组和队列
1.1 稀疏数组sparsearray
1.1.1 稀疏数组基本介绍
当一个数组中大部分元素为0时,可以使用稀疏数组来保存该数组
1.1.2 稀疏数组的处理方法是:
-
- 记录数组一共有几行几列,有多少个不同的值
- 把具有不同值的元素的行和列记录在一个小规模的数组中(稀疏数组),从而缩小程序的规模
1.1.3 应用实例
- 使用稀疏数组,来保存类似前面的二维数组(棋盘地图等等)
- 把稀疏数组存盘,并且可以从新恢复原来的二维数组
- 整体思路分析:
二维数组转稀疏数组的思维
1.遍历原始的二维数组,得到有效的数据个数(sum)
2.根据sum就可以创建稀疏数组sparseArr int[sum+1][3]
sun+1行(第一行是原始数组的描述:多少行,多少列,有效数字有几个)
3列:后续有效数字的描述(原始数组中第几行,第几列,有效值是多少)
3.将二维数组的有效数据存入到稀疏数组中
数组转原始的二维数组思路
1.先读取稀疏数组的第一行,根据第一行的数据,创建原始的二维数组,比如chessArr=int[11][11]
2.再读取稀疏数组的后几行数据,并赋给原始的二维数组即可。
1.1.4 代码实现
package cn.com.sparsearray;
/**
* 稀疏数组演示
*/
public class SparseArray {
public static void main(String[] args) {
/*
重点1:创建一个原始的二维数组棋盘
1:黑子
2:白子
*/
int chessAr1[][] = new int[11][11];
chessAr1[1][2] = 1;
chessAr1[2][3] = 2;
System.out.println("原始数组===>");
for (int[] row : chessAr1) {
for (int data : row) {
System.out.print(data + " ");
}
System.out.println();
}
/*
重点2:将二维数组转为稀疏数组
*/
int sum = 0;
//遍历二维数组,得到有效数据个数
for (int[] row : chessAr1) {
for (int data : row) {
if (data != 0) {
sum++;
}
}
}
//创建对应的稀疏数组:sum+1行(第一行是描述原始二维数组有多少行,多少列,有效数据有几个)
int sparseArr[][] = new int[sum + 1][3];
//赋值稀疏数组第一行:描述原始二维数组:多少行,多少列,有效数字个数
sparseArr[0][0] = 11;
sparseArr[0][1] = 11;
sparseArr[0][2] = sum;
//计数器:因为稀疏数组的行是递增的,这个用来计数
int count = 0;
//遍历原始二维数组,并将其转为稀疏数组
for (int i = 0; i < chessAr1.length; i++) {
for (int j = 0; j < chessAr1[i].length; j++) {
if (chessAr1[i][j] != 0) {
count++;
sparseArr[count][0] = i;
sparseArr[count][1] = j;
sparseArr[count][2] = chessAr1[i][j];
}
}
}
//遍历稀疏数组
System.out.println("稀疏数组=====>");
for (int[] row : sparseArr) {
for (int data : row) {
System.out.print(data + " ");
}
System.out.println();
}
/*
重点3:稀疏数组还原到原始二维数组
*/
//稀疏数组的第一行第一列=原始数组的行
int row = sparseArr[0][0];
//稀疏数组的第一行第二列=原始数组的列
int cloum = sparseArr[0][1];
int[][] chessArr2 = new int[row][cloum];
for (int i = 1; i < sparseArr.length; i++) {
chessArr2[sparseArr[i][0]][sparseArr[i][1]] = sparseArr[i][2];
}
//遍历转换后的二维数组
System.out.println("稀疏数组转换二维数组=====>");
for (int[] rows :chessArr2){
for (int data :rows){
System.out.print(data+" ");
}
System.out.println();
}
}
}
问题:
因为数组的类型只能有一个,在定义稀疏数组时,必须创建为int型的
int sparseArr[][] = new int[sum + 1][3];
为什么只能定义为int型呢?
因为稀疏数组的第一行:第一列=原始二维数组的行数(int),第二列=原始二维数组的列数(int),第三列(原始二维数组的有效数个数int)
所以,原始二维数组的内容只能是int类型,才能转为稀疏数组,假如是String类型时,我们在稀疏数组赋值时:稀疏数组[i][2]="",和数组的格式不统一,导致出错!这是个坑
2.队列
2.1 简介
队列介绍
- 队列是一个有序列表,可以用数组或者链表来实现.
- 遵循先入先出的原则。即:先存入队列的数据,要先取出。后存入的要后取出
- 示意图:(使用数组模拟队列示意图)
1.数组模拟队列
- 队列本身是有序列表,为使用数组的结构来存储队列的数据,则队列数组的声明如下图,其中maxSize是该队列的最大容量
- 因为队列的输出、输入是分别从前后单端来处理,因此需要两个变量front和rear分别记录队列的前后端的下标,front会随着数据的输出而变化,而rear则会随着数据的输入而变化。
1.rear是队列的最后【含】
2.front是队列最前的元素【不包含】
- 当我们将数据存入队列时称为"addQueue",addQueue的处理需要两个步骤
- 将尾指针往后移:rear+1,当ront==rear【空】,初始队列没有值时
- 若尾指针rear小于队列的最大下标maxSize-1,则将数据存入rear所值的数组元素中,否则无法存入数据。rear==maxSize-1【队列满】
代码实现:
package cn.com.query;
import java.util.Scanner;
public class ArrayQueryDemo {
public static void main(String[] args) {
//创建容量为3的队列
ArrayQuery query = new ArrayQuery(3);
boolean loop = true;
Scanner scanner = new Scanner(System.in);
while (loop) {
System.out.println("输入a:添加元素");
System.out.println("输入g:获取元素");
System.out.println("输入h:输出头部元素");
System.out.println("输入s:遍历元素");
System.out.println("输入e:退出程序");
char key = scanner.next().charAt(0);
switch (key) {
case 'a':
try {
System.out.println("输入添加元素...");
int data = scanner.nextInt();
query.addQuery(data);
} catch (Exception e) {
System.out.println(e.getMessage());
}
break;
case 'g':
try {
int getData = query.getQuery();
System.out.println("获取数据:" + getData);
} catch (Exception e) {
System.out.println(e.getMessage());
}
break;
case 'h':
try {
int headData = query.headQuery();
System.out.println("头部数据:" + headData);
} catch (Exception e) {
System.out.println(e.getMessage());
}
break;
case 's':
System.out.println("遍历列表..");
query.show();
break;
case 'e':
System.out.println("退出程序~~");
loop = false;
scanner.close();
}
}
}
}
/**
* 使用数组模拟队列
*/
class ArrayQuery {
//队列的最大长度
private int maxSize;
//队列头:
private int front;
//队列尾
private int rear;
//模拟队列
private int[] arr;
public ArrayQuery(int maxSize) {
this.maxSize = maxSize;
//指向队列头:并且注意front一直执行队列第一个元素的前一个位置
this.front = -1;
//执行队列尾:指向的是队列的最后一个数据(即就是队列的最后一个数据)
this.rear = -1;
this.arr = new int[maxSize];
}
/**
* 1:判断队列是否已满
*
* @return 是否已满
*/
public boolean isFull() {
//判断队列尾是否==最大值-1
return rear == maxSize - 1;
}
/**
* 2:添加数据
*
* @param data 数据
*/
public void addQuery(int data) {
if (isFull()) {
throw new RuntimeException("队列已满,不能添加数据");
}
//尾指针后移一位
rear++;
arr[rear] = data;
}
/**
* 3:判断对垒是否为空
*
* @return 队列是否为空
*/
public boolean isEmpty() {
//front指向队列第一个元素的前一个位置,rear指向队列的最后一个元素,当两值相等时,即队列没有元素
return rear == front;
}
/**
* 4:获取数据
*
* @return 返回的元素
*/
public int getQuery() {
if (isEmpty()) {
throw new RuntimeException("队列为空,不能获取数据");
}
front++;
return arr[front];
}
/**
* 5:遍历输出数据
*/
public void show() {
if (isEmpty()) {
System.out.println("队列为空,没有需要遍历的数据");
return;
}
for (int i = 0; i < arr.length; i++) {
System.out.println(i + ":" + arr[i]);
}
}
/**
* 6:展示队列的头元素,只是展示
*
* @return 头元素
*/
public int headQuery() {
if (isEmpty()) {
throw new RuntimeException("队列为空,没有头元素");
}
//只是展示,这里不能用front++,因为使用这个方法,front本身值发生变化
return arr[front + 1];
}
}
测试:
1.新增3个元素:10,20,30
2.输入s遍历列表
s
遍历列表..
0:10
1:20
2:30
3.输入h获取列表头部元素:
多次输入发现返回一致,因为获取头部元素只是获取,不移动内部front指针位置,
内部采用的是front+1的下标获取方式,并没有采用front++
h
头部数据:10
h
头部数据:10
4.3个元素已满,再添加
a
输入添加元素...
40
队列已满,不能添加数据
5.获取元素
g
获取数据:10
6.获取后查看头部元素:发现头部元素发生变化
h
头部数据:20
7.取完元素后,再取
g
队列为空,不能获取数据
问题分析优化:
1.目前数组只能使用一次,没有达到复用效果,如:新增3个数据后再取完,是不能再往里放数据了,因为rear已经到达最大容量-1位置,isfull判断返回true
改进:将这个数组使用算法,改进为环形队列,使用取模的方法:%
2.虽然数据已经取完,但是遍历发现数据依然存储在数组中,因为底层移动的只是front指针,并没有对原始位置置空或处理
2. 使用数组模拟环形队列解决上述问题:
思路如下:(该算法是老师分析的算法,最重要的一点是rear变量指向队尾元素的后一位,因为希望多出一个空间作为约定,即当创建5个容量的元素,实际只能存储4个元素)
算法有和多种,这只是展示的一种
1.front变量含义做一个调整:front就指向队列的第一个数据,也就是说arr[front]就是队列的第一个数据【初始值是0】
2.rear变量的含义做一个调整:rear执行队列的最后一个元素,因为希望多出一个空间作为约定【初始值是0】
3.当队列满时,如下图:条件是(rear+1)%maxSize=front【满】
4.队列为空的条件:front==rear【空】
5.有效数字个数:(rear+maxSize-front)%maxSize
代码示例:
package cn.com.queue;
import java.util.Scanner;
public class CircleArrayQueueDemo {
public static void main(String[] args) {
//创建容量为5的队列,只能放4个元素
CircleArray query = new CircleArray(5);
boolean loop = true;
Scanner scanner = new Scanner(System.in);
while (loop) {
System.out.println("输入a:添加元素");
System.out.println("输入g:获取元素");
System.out.println("输入h:输出头部元素");
System.out.println("输入s:遍历元素");
System.out.println("输入e:退出程序");
char key = scanner.next().charAt(0);
switch (key) {
case 'a':
try {
System.out.println("输入添加元素...");
int data = scanner.nextInt();
query.addQuery(data);
} catch (Exception e) {
System.out.println(e.getMessage());
}
break;
case 'g':
try {
int getData = query.getQuery();
System.out.println("获取数据:" + getData);
} catch (Exception e) {
System.out.println(e.getMessage());
}
break;
case 'h':
try {
int headData = query.headQuery();
System.out.println("头部数据:" + headData);
} catch (Exception e) {
System.out.println(e.getMessage());
}
break;
case 's':
System.out.println("遍历列表..");
query.show();
break;
case 'e':
System.out.println("退出程序~~");
loop = false;
scanner.close();
}
}
}
}
/**
* 使用数组模拟队列
*/
class CircleArray {
//队列的最大长度
private int maxSize;
//队列头:
private int front;
//队列尾
private int rear;
//模拟队列
private int[] arr;
public CircleArray(int maxSize) {
this.maxSize = maxSize;
//指向队列头:并且注意front一直指向队列的第一个元素,front的初始值是0
//也就是说arr[front]就是队列的第一个数据
this.front = 0;
//指向队列尾:指向的是队列的最后一个数据的后一个位置,rear初始值为0
//因为希望多出一个空间作为约定
this.rear = 0;
this.arr = new int[maxSize];
}
/**
* 判断是否满了
*/
public boolean isFull() {
return (rear + 1) % maxSize == front;
}
/**
* 判断是否为空
*/
public boolean isEmpty() {
return rear == front;
}
/**
* 添加数据
*
* @param data 数据
*/
public void addQuery(int data) {
if (isFull()) {
throw new RuntimeException("队列已满,不能添加数据");
}
//直接将数据放入
arr[rear] = data;
//将rear后移,这里必须考虑取模
rear = (rear + 1) % maxSize;
}
/**
* 获取数据
*
* @return 返回的元素
*/
public int getQuery() {
if (isEmpty()) {
throw new RuntimeException("队列为空,不能获取数据");
}
//这里需要分析出front是指向队列的第一个元素
//1.将front对应的值保留到一个临时变量
int value = arr[front];
//2.将front后移
front = (front + 1) % maxSize;
//3.将临时变量返回
return value;
}
/**
* 遍历输出数据
*/
public void show() {
if (isEmpty()) {
System.out.println("队列为空,没有需要遍历的数据");
return;
}
//从front开始便利,便利多少个元素(有效数字)
for (int i = front; i < front+size(); i++) {
System.out.println(i + ":" + arr[i]);
}
}
/**
* 返回当前队列有效数据大小
*/
public int size() {
return (rear + maxSize - front) % maxSize;
}
/**
* 查看头元素
*/
public int headQuery() {
return arr[front];
}
}
结果演示:
1.往队列里添加数据,容量为5,最后一个空间是约定空间,不存储数据,所以只能存储4个(10,20,30,40)当添加50时
50
队列已满,不能添加数据
2.查看队列
s
遍历列表..
0:10
1:20
2:30
3:40
3.查看头元素,并不移动底层指针,所以不影响列表
h
头部数据:10
4.取出数据
g
获取数据:10
5.再次便利列表时:发现取出的数据不会再被遍历出,解决了上述问题
s
遍历列表..
1:20
2:30
3:40
重点总结:
1.这只是一种算法,有很多种算法
2.按照这个算法思路,会留出最后一个空间作为约定,即5个容量只能容纳4个数据
3.该算法中最有意思的是有效数字个数的计算:
(rear + maxSize - front) % maxSize;