学习完部分大数据知识之后, 大数据阶段的学习就暂时告一段落了. 为了能够有机会进入大厂修习, 因此特别在这段时间里通过学习韩顺平老师的数据结构与算法来复习下数据结构与算法. 与其说是复习不如说是预习,嘿嘿.我将不同于以往的写博方式, 重新和大家一起认识下数据结构. 去发现其中的奥秘~~~
本期关键词: 线性结构, 非线性结构, 二维数据, 稀疏数组, 队列, 循环队列
在学习数据结构与算法之前我们需要了解数据结构与算法的重要性
然后我们可以了解一下数据结构与算法的关系, 对其有一个初步的认识
首先我们去了解数据结构, 然后以数据结构为基础, 再去了解算法吧
数据结构是有分类的, 它分为线性结构和非线性结构
线性结构最为最常用的数据结构, 是一个有序的数据元素集合, 特点是元素间具有一对一的线性关系.
另外, 线性结构有两种存储: 线性存储结构和链式存储结构
基于顺序存储结构存储的线性表称为顺序表, 其元素存储顺序是连续的;
基于链式存储结构存储的线性表称为链表, 其元素存储顺序不一定是连续的, 且元素中存放数据元素以及相邻元素的地址信息
我们常见的线性结构: 数组, 链表,栈和队列
那么, 我想我们应该好奇非线性结构是什么东东了,非线性结构就是当前元素可能有 多个 直接前驱 和 直接后继元素
常见的非线性结构有: 数组(二维or多维), 广义表, 树结构, 图结构
需求
如何保存五子棋记录? 我们能够比较容易的想到使用二维数组
问题
可以看到二维数组中很多数据都是默认值0, 因此可以采用稀疏数组的方式存储数据
稀疏数组( SparseArray )
当一个数组大部分数据元素为0 or 同一个值时, 采取稀疏数组
稀疏数组的处理方法
因此五子棋存盘问题可以转换成如下问题
代码实现
根据思路,一步步实现即可
/**
* 稀疏数组与二维数组的转换
*
* @author TimePause
* @create 2020-01-08 16:08
*/
public class SparseArray {
public static void main(String[] args) {
/**
* 二维数组转稀疏数组
* 1. 遍历原始二维数据, 得到有效数据个数sum
* 2. 根据sum创建稀疏数组 sparseArray[sum+1][3], 3代表列数且为固定
* 3. 将二维数组中的数据存放到稀疏数组
*/
// 表示棋子的二维数组
int[][] chessArray1 = new int[11][11];
chessArray1[1][2] = 1;
chessArray1[2][3] = 2;
//遍历二维数组
int sum = 0;
for (int[] row : chessArray1) {
for (int data : row) {
if (data != 0) {
sum++;
}
System.out.print(data + "\t");
}
System.out.println();
}
System.out.println("二维数组的有效元素个数" + sum);
//System.out.println(chessArray1.length); 取得二维数组行数
//System.out.println(chessArray1[0].length); 取得二维数组列数
// 创建稀疏数组, 本质仍是二维数据
int[][] sparseArray = new int[sum + 1][3];
sparseArray[0][0] = chessArray1.length;
sparseArray[0][1] = chessArray1[0].length;
sparseArray[0][2] = sum;
// 将二维数据的有效值赋值给稀疏数组(第二行以后)
int count = 1; //对行计数
for (int i = 0; i < chessArray1.length; i++) {
for (int j = 0; j < chessArray1[i].length; j++) {
if (chessArray1[i][j] != 0) {
// 第一列有:效数据元素所在行.第二列: 有效数据元素所在列,第三列: 有效数据元素的值
sparseArray[count][0] = i;
sparseArray[count][1] = j;
sparseArray[count][2] = chessArray1[i][j];
count++;
}
}
}
System.out.println("==============稀疏数组遍历结果====================");
for (int[] row : sparseArray) {
for (int data : row) {
System.out.print(data + "\t");
}
System.out.println();
}
/**
* 稀疏数组转二维数组
* 1. 读取稀疏数组第一行数据, 根据第一行数据创建原始的第二行数据, 比如chess2=int[11][11]
* 2. 在去读稀疏数组后几行数据, 并赋值给原始的二维数组即可
*/
int[][] chessArray2 = new int[sparseArray[0][0]][sparseArray[0][1]];
// 遍历稀疏数组第二列数据, 注意i从1开始!!!
System.out.println("==============稀疏数组转二维数组====================");
for (int i = 1; i < sparseArray.length; i++) {
chessArray2[sparseArray[i][0]][sparseArray[i][1]] = sparseArray[i][2];
}
for (int[] row: chessArray2) {
for (int data : row) {
System.out.print(data + "\t");
}
System.out.println();
}
}
}
通过上面的代码学习, 我们可以总结出二维数组遍历的两种方式
int[][] chessArray1 = new int[11][11];
//...赋值省略
//方式一: 增强for循环
for (int[] row : chessArray1) {
for (int data : row) {
if (data != 0) {
sum++;
}
System.out.print(data + "\t");
}
System.out.println();
}
//方式二
int count = 1; //对行计数
for (int i = 0; i < chessArray1.length; i++) {
for (int j = 0; j < chessArray1[i].length; j++) {
System.out.print(chessArray1[i][j] + "\t");
}
System.out.println();
}
在生活中, 我们在银行排队中需要去取票机取票, 按照取票的顺序(由小到大)进行排队,
银行有n个窗口, 每次每个窗口处理完业务处理完业务后, 都会叫下一个大的码号, 而且会不断的有人加入这个队列, 如何模拟?
下面我们来学习线性结构的一种数据结构: 队列
队列是一个有序表, 编程上可以通过数组和链表来实现
利用数组模拟队列
队列本身是有序列表,若使用数组的结构来存储队列的数据,则队列数组的声明如下图, 其中 maxSize 是该队列的最大容量。
因为队列的输出、输入是分别从前后端来处理,因此需要两个变量 front及 rear分别记录队列前后端的下标,front 会随着数据输出而改变,而 rear则是随着数据输入而改变,如图所示:
数组模拟队列代码
rear==front,
队满条件 rear==maxSize-1
,不符合条件需要抛出异常throw new RuntimeException()
可以在运行时抛出我们指定的异常, 配合try()…catch()可以实现异常的定制显示而且不会中断程序的执行scanner.next().charAt(0)
用于接收下一行输出的第一个字符package ah.sz.tp.algorithm1;
import java.util.Scanner;
/**
* 数组模拟队列
*
* @author TimePause
* @create 2020-01-08 20:15
*/
public class AarryQueueDemo {
public static void main(String[] args) {
// 创建一个队列实体, 且元素个数为3
ArrayQueue queue=new ArrayQueue(3);
// 定义键盘输入的数值
char key=' ';
Scanner scanner = new Scanner(System.in);
// 定义一个循环是否终止的变量
boolean flag=true;
while (flag){
//显示菜单
System.out.println("s(show): 显示当前队列数据");
System.out.println("a(add): 向当前队列添加数据");
System.out.println("g(get): 向当前队列取出数据");
System.out.println("h(head): 显示当前队列头数据");
System.out.println("e(exit): 执行退出操作");
//读取我们输入的第一个字符
key = scanner.next().charAt(0);
switch (key) {
case 's':
try {
queue.showQueueData();
} catch (Exception e) {
// 这里使用try..catch...的作用是,不会中断程序的运行,手动打印异常信息
System.out.println("队空,无法显示所有数据~~~");
}
break;
case 'h':
try {
queue.showHeadData();
} catch (Exception e) {
System.out.println("队空,无法显示头数据~~~");
}
break;
case 'a':
try {
System.out.println("请输入入队的数据");
int n = scanner.nextInt();
queue.addQueue(n);
} catch (Exception e) {
System.out.println("队满,无法添加数据~~~");
}
break;
case 'g':
try {
int res = queue.getQueue();
System.out.println("取出的数据为:"+res);
} catch (Exception e) {
System.out.println("队空,无法获取数据~~~");
}
break;
case 'e':
flag = false;
break;
default:
break;
}
}
System.out.println("已退出, 欢迎下次使用~~~");
}
}
class ArrayQueue {
private int maxSize; //数组最大容量
private int front; //队列头
private int rear; //队列尾
private int[] arr; //该数组用于模拟队列, 存放队列元素
public ArrayQueue(int maxSize) {// 构造函数无需定义返回值!!!
this.maxSize = maxSize;
this.front = -1; //指向队列的头部, 实际上指向队列头的前一个位置.-1实际上是因为数组下标从0开始
this.rear = -1; //指定队列的尾部, 最后一个数据
this.arr = new int[maxSize];
}
//判断队列是否为空, 条件是判断front==rear
public boolean isNull() {
return front == rear;//首先对进行是否等值的判断, 然后将结果输出
}
//判断队列是否为满, 条件是判断rear==maxSize-1
public boolean isFull() {
return rear == maxSize - 1;
}
// 入队操作, 需要判断是否队满
public void addQueue(int n) {
if (isFull()) {
throw new RuntimeException("队满, 无法进行入队操作");
// System.out.println("队满, 无法进行入队操作");
}
rear++;
arr[rear] = n;
}
//出队操作, 需要判断是否队空
public int getQueue() {
if (isNull()) {
throw new RuntimeException("队空,无法进行入队操作");
// System.out.println("队空,无法进行入队操作");
}
front++;
return arr[front];//因为rear在上, front在下,front+1后,便会自动跳过上一条数据
}
// 显示头数据, 仍需要判空
public int showHeadData() {
if (isNull()) {
throw new RuntimeException("队空,无法进行显示头数据操作");
// System.out.println("队空,无法进行显示头数据操作");
}
return arr[front + 1];
}
// 显示所有数据
public void showQueueData() {
if (isNull()) {
throw new RuntimeException("队空, 无法显示所有数据");
//System.out.println("队空, 无法显示所有数据");
}
for (int i = 0; i < arr.length; i++) {
System.out.println("arr[" + i + "]=" + arr[i]);
}
}
}
结果展示
问题分析及优化
问题: 数组使用一次便不可用, 不能复用
优化: 改进成一个环形队列, 取模: %
循环队列实现代码
rear==front
, 队满 (rear + 1)% maxSize=front
,队列中有效数据的个数 (rear+maxSize-front)%maxSize
arr[rear] = n
, 然后rear指针后移(取模)rear = (rear + 1) % maxSize
front = (front + 1) % maxSize
3. 将临时保存的变量返回for (int i = front; i < front+circleQueueSize(); i++)
, 注意i的初始值和结束值package ah.sz.tp.algorithm1;
import java.util.Scanner;
/**
* 数组模拟队列
*
* @author TimePause
* @create 2020-01-09 11:30
*/
public class CircleQueueDemo {
public static void main(String[] args) {
// 创建一个循环队列实体, 且有效数据长度为 maxSize-1
CircleQueue queue = new CircleQueue(4);
// 定义键盘输入的数值
char key = ' ';
Scanner scanner = new Scanner(System.in);
// 定义一个循环是否终止的变量
boolean flag = true;
while (flag) {
//显示菜单
System.out.println("s(show): 显示当前队列数据");
System.out.println("a(add): 向当前队列添加数据");
System.out.println("g(get): 向当前队列取出数据");
System.out.println("h(head): 显示当前队列头数据");
System.out.println("e(exit): 执行退出操作");
//读取我们输入的第一个字符
key = scanner.next().charAt(0);
switch (key) {
case 's':
try {
queue.showQueueData();
} catch (Exception e) {
// 这里使用try..catch...的作用是,不会中断程序的运行,手动打印异常信息
System.out.println("队空,无法显示所有数据~~~");
}
break;
case 'h':
try {
queue.showHeadData();
} catch (Exception e) {
System.out.println("队空,无法显示头数据~~~");
}
break;
case 'a':
try {
System.out.println("请输入入队的数据");
int n = scanner.nextInt();
queue.addQueue(n);
} catch (Exception e) {
System.out.println("队满,无法添加数据~~~");
}
break;
case 'g':
try {
int res = queue.getQueue();
System.out.println("取出的数据为:" + res);
} catch (Exception e) {
System.out.println("队空,无法获取数据~~~");
}
break;
case 'e':
flag = false;
break;
default:
break;
}
}
System.out.println("已退出, 欢迎下次使用~~~");
}
}
class CircleQueue {
private int maxSize; //数组最大容量
private int front; //队列头 front=0, 指向第一个元素
private int rear; //队列尾 rear=0,指向最后一个元素的最后一个元素
private int[] arr; //该数组用于模拟队列, 存放队列元素
public CircleQueue(int maxSize) {// 构造函数无需定义返回值!!!
this.maxSize = maxSize;
this.arr = new int[maxSize];
// front=rear=0.为默认值, 因此可以省略
}
//判断队列是否为空
public boolean isNull() {
return front == rear;//首先对进行是否等值的判断, 然后将结果输出
}
//判断队列是否为满
public boolean isFull() {
return (rear + 1) % maxSize == front;
}
// 入队操作, 需要判断是否队满
public void addQueue(int n) {
if (isFull()) {
throw new RuntimeException("队满, 无法进行入队操作");
}
// 因为rear指向的最后一个元素的后一个元素,所以在入队是直接加入
arr[rear] = n;
// 后移, 必须考虑取模!!!
rear = (rear + 1) % maxSize;
}
//出队操作, 需要判断是否队空
public int getQueue() {
if (isNull()) {
throw new RuntimeException("队空,无法进行入队操作");
}
//出队时, 需要分析front指向队列的第一个元素
// 步骤: 1. 将front保存到一个临时变量 2. front后移(取模) 3. 将临时保存的变量返回
int temp = arr[front];
front = (front + 1) % maxSize;
return temp;
}
// 显示头数据, 仍需要判空
public int showHeadData() {
if (isNull()) {
throw new RuntimeException("队空,无法进行显示头数据操作");
}
return arr[front];
}
// 显示所有数据, 需要求出有效数据的长度!!!
public void showQueueData() {
if (isNull()) {
throw new RuntimeException("队空, 无法显示所有数据");
}
// 这里注意i=front, 而不是front+circleQueueSize()!!!
for (int i = front; i < front+circleQueueSize(); i++) {
System.out.println("arr[" + i % maxSize + "]=" + arr[i % maxSize]);
}
}
// 循环队列有效数据的长度
public int circleQueueSize() {
return (rear + maxSize - front) % maxSize;
}
}
结果演示
查看队列是否为空->添加元素至队满->查看元素->取出所有元素至队空->查看是否能够重新加入该元素->查看这些元素