数据结构与算法(Java实现)
我的学习资料:
视频:尚硅谷Java数据结构与java算法(Java数据结构与算法)
书籍:《大话数据结构》
笔记中包括学习的内容,代码,同时自己总结了知识点速记(部分会带页内跳转,可点击跳转)供快速回顾和记忆学到的知识点。
0.线性结构和非线性结构
- 数据结构的底层存储方式只有两种:数组(顺序存储)和链表(链式存储)。
0.1 线性结构
特点:数据元素之间存在一对一的线性关系
顺序存储的线性表叫做顺序表,顺序表中的存储元素是连续的
链式存储的线性表叫做链表,链表中的存储元素不一定是连续的
线性结构常见的有:数组,队列,列表和栈。
0.2 非线性结构
常见的有:二维数组,多维数组,广义表,树,图
一、稀疏( sparsearray)数组
1.1 实际需求
对于一个五子棋盘上棋子的情况,可以使用二维数组记录,0表示无棋子,1和2分别表示两种颜色的棋子,因为该二维数组的很多值是默认值 0 ,因此记录了很多没有意义的数据,我们将其转为稀疏数组进行存储。
1.2 介绍
1.2.1 基本介绍
当一个数组中大部分元素为0,或者为同一个值的数组时,可以使用稀疏数组来保存该数组。
1.2.2 处理方法及举例
稀疏数组的处理方法是:
- 记录数组一共有几行几列,有多少个不同的值
- 把具有不同值的元素的行列及值记录在一个小规模的数组中,从而缩小程序的规模
- 具体操作:
第1行:记录原始二维数组的行数,列数,非0元素(或者说有效数据)的总个数
- 第1列:原始二维数组的行数
- 第2列:原始二维数组的列数
- 第3列:原始二维数组的非0元素的个数
剩余几行:记录原始二维数组非0元素(或者说有效数据)在原始二维数组的下标(横纵坐标)及它的值
-
第1列:非0元素(有效数据)的横坐标
-
第2列:非0元素(有效数据)的纵坐标
-
第3列:非0元素(有效数据)的值
-
二维数组——>稀疏数组举例:
在下面的棋盘中,原始二维数组规模是6×7,转化为稀疏数组规模变为9×3(注意数组的坐标是从0开始的,但我们可能习惯描述第几行是从第1行开始的)
1. 搞定第一行
因为原始规模6*7,即【6行7列8个非零值】,因此在稀疏数组第1行(注意行坐标是0)依次填入【6,7,8】
2. 剩余的元素按照“行”的方式遍历,依次填入。
比如第一个非零元素是22,横纵坐标【0,3】因此在稀疏数组下一行依次填入它的坐标及它的值【0,3,22】
接下来按照行遍历,应该依次填入的元素是15,11,17,-6,39,91,28,完成稀疏数组。
1.3 应用实例
1.3.1实例说明
- 使用稀疏数组,来保留类似前面的二维数组(棋盘、地图等等)
- 把稀疏数组存盘,并且可以重新恢复原来的二维数组数
二维数组——>稀疏数组的思路
- 遍历 原始的二维数组,得到有效数据的个数 sum
- 根据sum 就可以创建 稀疏数组 sparseArr int[sum + 1 ] [ 3 ]
- 将二维数组的有效数据数据存入到 稀疏数组
稀疏数组——>原始的二维数组的思路
- 先读取稀疏数组的第一行,根据第一行的数据,创建原始的二维数组,比如上面的 chessArr2 = int [11][11]
- 在读取稀疏数组后几行的数据,并赋给 原始的二维数组即可.
1.3.2 代码实现
package dataStructures;
import java.util.Arrays;
public class GoBang {
public static int BTsize=11;
int[][] realCheckBoard = new int[BTsize][BTsize];
int[][] sparseArray;
int count=0;
public static void main(String[] args) {
GoBang gb = new GoBang();
gb.realCheckBoard[1][2]=1;
gb.realCheckBoard[2][3]=2;
System.out.println();
for (int[]a1:gb.realCheckBoard
) {
System.out.println();
for (int a2:a1
) {
System.out.print(a2+" ");
}
}
int[][] array1 =gb.realCheckBToSparseArray();
int[][] array2 = gb.sparseArrayToRealCheckBT();
}
public GoBang(){
}
public GoBang(int[][] realCheckBoard) {
this.realCheckBoard = realCheckBoard;
}
public int[][] realCheckBToSparseArray(){
System.out.println();
System.out.println("得到稀疏数组为:");
//遍历二维数组找到非0元素值的个数,用count表示
for (int i=0;i<realCheckBoard.length;i++){
for (int j=1; j<realCheckBoard[i].length;j++){
if (realCheckBoard[i][j]!=0){
count++;
}
}
}
sparseArray = new int[count + 1][3];
sparseArray[0][0] = BTsize;
sparseArray[0][1] = BTsize;
sparseArray[0][2] = count;
if (count!=0) {
//遍历二维数组
for (int i = 0; i < realCheckBoard.length; i++) {
for (int j = 1; j < realCheckBoard[i].length; j++) {
if (realCheckBoard[i][j] != 0) {
sparseArray[i + 1][0] = i;
sparseArray[i + 1][1] = j;
sparseArray[i + 1][2] = realCheckBoard[i][j];
}
}
}
}
for (int i=0;i<sparseArray.length;i++){
System.out.printf("%d\t%d\t%d\t\n",sparseArray[i][0],sparseArray[i][1],sparseArray[i][2]);
}
return sparseArray;
}
public static void printArray(int[][] array){
for (int i=0; i<array.length;i++){
System.out.println("=====");
for (int j =0;j<array[i].length;j++){
System.out.print(array[i][j]+" ");
}
}
}
public int[][] sparseArrayToRealCheckBT(){
System.out.println();
System.out.println("根据稀疏矩阵转换的二维数组");
for (int i=1; i<this.sparseArray.length;i++){
this.realCheckBoard[ this.sparseArray[i][0] ][ this.sparseArray[i][1] ]= this.sparseArray[i][2];
}
//for each结构遍历二维数组
for (int[] a1:realCheckBoard
) {
System.out.println();
for (int a2:a1
) {
System.out.print(a2+" ");
}
}
return this.realCheckBoard;
}
}
1.4稀疏数组知识点速记
- 稀疏数组:当一个数组中大部分元素为0,或者为同一个值的数组时,可以使用稀疏数组来保存该数组。稀疏数组N行3列。第一行记录行、列、有效数据个数,剩下的行记录:行遍历顺序下有效数据的横纵坐标和它的具体值。
- 编程思路:
二维——>稀疏:遍历二维找非0值个数,根据个数确定稀疏数组行数(列数固定为3),再次遍历二维数组将非0的横纵坐标记录入稀疏数组。
稀疏——>二维:根据第一行初始化二维矩阵的维数,然后根据剩下行的坐标和具体值还原二维数组。 - 技巧:
双重for each遍历二维数组,printf方法(加入\t,\n)规范化输出形式。
二、队列
2.1 实际需求
银行排队的叫号系统
2.2 介绍
2.2.1 基本介绍
-
队列是一个有序列表,可以用数组或是链表来实现。
-
遵循先入先出(FIFO,first in first out) 的原则。即:先存入队列的数据,要先取出。后存入的要后取出。
-
示意图:(使用数组模拟队列示意图)
图1表示Queue类,maxSize 是该队列的最大容量,rear表示队尾,初始化值为-1,front表示队首的前一个位置**,初始化值为-1。
图2表示,当向队列放4个数据时,front没有变化,rear增加变为3。
图3表示,当从队列取2个数据时,front增加变为2,rear保持不变。
放入数据,rear变化,取出数据,front变化
2.2.2 处理方法及举例
用数组模拟队列:
- 队列本身是有序列表,若使用数组的结构来存储队列的数据,则队列数组的声明如图2.2.1-1,其中 maxSize 是该队列的最大容量。
- 因为队列的输出、输入是分别从前后端来处理,因此需要两个变量 front及 rear分别记录队列前后端的下标,front 会随着数据输出而改变,而 rear则是随着数据输入而改变。
举例(思路分析):
成员变量:
maxSize :该队列的最大容量
rear:表示队尾,初始值为-1
front:表示队首的前一个位置,初始值为-1
arr[][]:用于模拟队列的数组
成员方法:
判断为空:isFull
判断为满:isEmpty
加入元素:addQueue
出队列操作:getQueue
显示(遍历)队列的情况:showQueue
查看队列头元素:headQueue
基本操作:
判断队列为空: front==rear
判断队列满:rear=MaxSize-1
队列元素个数:rear-front
队列入队:先判状态,队列不满才能入,arr[++rear] = value
队列出队:先判状态,队列不空才能出,return arr[++front]
2.2.3 代码实现
package dataStructures.Queue;
import java.util.Scanner;
public class ArrayQueueDemo {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
ArrayQueue a =new ArrayQueue(4);
boolean loop =true;
char key =' ';//接收用户输入
while(loop){
System.out.println("请选择你的操作:");
System.out.println("a(add):添加元素");
System.out.println("g(get):取出元素");
System.out.println("s(show):显示队列元素");
System.out.println("h(head):显示头元素");
System.out.println("e(exit):退出");
key=scanner.next().charAt(0);
switch (key){
case 'a':
System.out.println("请输入进入队列的数:");
try {
a.addQueue(scanner.nextInt());
} catch (Exception e) {
System.out.println(e.getMessage());
}
break;
case 'g':
try {
int get=a.getQueue();
System.out.printf("取出的数是:%d",get);
} catch (Exception e) {
System.out.println(e.getMessage());
}
break;
case 's':
try {
a.showQueue();
} catch (Exception e) {
System.out.println(e.getMessage());
}
break;
case 'h':
try {
int head = a.headQueue();
System.out.printf("队首元素是:%d",head);
} catch (Exception e) {
System.out.println(e.getMessage());
}
break;
case 'e':
loop=false;
break;
}
}
System.out.println("感谢使用,退出程序");
}
}
class ArrayQueue{
private int MaxSize;
private int front;
private int rear;
private int[] arr;
public ArrayQueue(int maxSize) {
MaxSize = maxSize;
front = -1;
rear = -1;
arr = new int[MaxSize];
}
public boolean isFull(){
return rear == MaxSize-1;
}
public boolean isEmpty(){
return rear == front;
}
//添加数据
public void addQueue(int n){
if(isFull()){
throw new RuntimeException("队列满,不能加数据");
}
arr[++rear]=n;
}
//取出数据
public int getQueue(){
if(isEmpty()){
throw new RuntimeException("队列空,不能取数据");
}
return arr[++front];
}
//显示队列元素
public void showQueue(){
if (!isEmpty()){
System.out.println("队列元素为:");
for (int i=0;i<arr.length;i++){
System.out.printf("arr[%d]为:%d\n",i,arr[i]);
}
}else{
throw new RuntimeException("对列为空");
}
}
//查看头元素
public int headQueue(){
if(!isEmpty()){
return arr[front+1];
}else{
throw new RuntimeException("对列为空");
}
}
}
存在的问题:
目前数组使用一次就不能用, 没有达到复用的效果。
理解:
(注意:这里front指向队首,rear指向队尾的下一个位置)
将这个数组使用算法,改进成一个列 环形的队列 取模:%,改进见2.3节
2.3 改进:使用数组模拟环形队列(循环队列)
2.3.1 改进思路
解决前面的假溢出的问题,就是后面满了,就再从头开始,也就是头尾相接的循环,将数组看做是一个环形的,我们把队列的这种头尾相接的顺序存储结构称为循环队列(通过 取模的方式来实现即可,取模运算相当于取余数)。
改进思路理解:
为了避免当只有一个元素时,队头和队尾重合使处理变得麻烦,所以引入两个指针,front 指针指向队头元素,rear 指针指向队尾元素的下一个位置,这样当front = =rear时,此队列不是还剩一个元素,而是空队列。
刚才的例子继续,图4-12-5的 rear可以改为指向下标为0的位置,这样就不会造成指针指向不明的问题了,如图4-12-6所示。
此时问题又出来了,我们刚才说,空队列时,front等于rear,现在当队列满时,也是 front等于rear,那么如何判断此时的队列究竟是空还是满呢?
办法一是设置一个标志变量flag,当front = = rear,且 flag =0时为队列空,当front = = rear,且 flag=1时为队列满。
(重点)办法二是当队列空时,条件就是 front = rear,当队列满时,我们修改其条件,保留一个元素空间。也就是说,队列满时,数组中还有一个空闲单元。
例如图4-12-8所示,我们就认为此队列已经满了,也就是说,我们不允许图4-12-7的右图情况出现。
-
我们重点来讨论第二种方法,由于rear可能比front大,也可能比front小,所以尽管它们只相差一个位置时就是满的情况,但也可能是相差整整一圈。所以若队列的最大尺寸为 maxSize ,那么队列满的条件是(rear+1)%maxSize = = front(取模“%”的目的就是为了整合rear与front的大小为一个问题)。
-
比如上面这个例子,maxSize = 5,图4-12-8的左图中 front=0,而rear=4,(4+1)%5=0,所以此时队列满。
-
再比如图4-12-8中的右图,front = 2而rear = 1。(1 +1) %5=2,所以此时队列也是满的。而对于图4-12-6,front=2而rear =0,(0+1)%5=1,1≠2,所以此时队列并没有满。
然后我们来考虑队列的长度(队列中有效的数据的个数):
- 当rear > front时,此时队列的长度为rear-front (比如图4-12-4的右图和4-12-5的左图,)。
- 当rear < front时,队列长度分为两段,一段是 maxSize -front,另一段是0 + rear,加在一起,队列长度为rear-front + maxSize (如图4-12-6和图4-12-7的左图)。
- 因此通用的计算队列长度公式为:(rear- front + maxSize )%maxSize
2.3.2 循环队列(环形队列)代码实现
相比前面增加了size()方法,注意
package dataStructures.Queue;
import java.util.Scanner;
public class CiecleArrayQueue {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
CircleQueue a =new CircleQueue(4);
boolean loop =true;
char key =' ';//接收用户输入
while(loop){
System.out.println("请选择你的操作:");
System.out.println("a(add):添加元素");
System.out.println("g(get):取出元素");
System.out.println("s(show):显示队列元素");
System.out.println("h(head):显示头元素");
System.out.println("e(exit):退出");
key=scanner.next().charAt(0);
switch (key){
case 'a':
System.out.println("请输入进入队列的数:");
try {
a.addQueue(scanner.nextInt());
} catch (Exception e) {
System.out.println(e.getMessage());
}
break;
case 'g':
try {
int get=a.getQueue();
System.out.printf("取出的数是:%d",get);
} catch (Exception e) {
System.out.println(e.getMessage());
}
break;
case 's':
try {
a.showQueue();
} catch (Exception e) {
System.out.println(e.getMessage());
}
break;
case 'h':
try {
int head = a.headQueue();
System.out.printf("队首元素是:%d",head);
} catch (Exception e) {
System.out.println(e.getMessage());
}
break;
case 'e':
loop=false;
break;
}
}
System.out.println("感谢使用,退出程序");
}
}
class CircleQueue{
private int MaxSize;//数组最大容量
private int front;//指向队首
private int rear;//指向最后一个元素的后一个位置
private int[] arr;//存放队列
public CircleQueue(int maxSize) {
MaxSize = maxSize;
front = 0;
rear = 0;
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()){
throw new RuntimeException("队列满,不能加数据");
}
arr[rear]=n;
rear=(rear+1)% MaxSize;
}
//取出数据
public int getQueue(){
if(isEmpty()){
throw new RuntimeException("队列空,不能取数据");
}
int key = arr[front];
front=(front+1)%MaxSize;
return key;
}
//获取队列中元素个数
public int size(){
return (rear-front+MaxSize)%MaxSize;
}
//显示队列元素
public void showQueue(){
if (!isEmpty()){
System.out.println("队列元素为:");
for (int i=front;i<front+this.size();i++){
System.out.printf("arr[%d]为:%d\n",i%MaxSize,arr[i%MaxSize]);
}
}else{
throw new RuntimeException("队列为空");
}
}
//查看头元素
public int headQueue(){
if(!isEmpty()){
return arr[front];
}else{
throw new RuntimeException("队列为空");
}
}
}
2.3.3 循环队列知识点速记
- 队列(queue):只允许在一端进行插入操作,而在另一端进行删除操作的线性表。
- 编程思路及技巧:
- front指针 : front 指向队首,即队列的第一个元素, 也就是说 arr[front] 就是队列的第一个元素,front 的初始值 = 0
- rear指针 :rear 指向队列的最后一个元素的后一个位置. 因为希望空出一个空间做为约定。rear 的初始值 = 0
- 队列满: (rear + 1) % maxSize == front 【满】
- 队列空: rear == front 【空】
- 队列中有效的数据的个数size() :(rear + maxSize - front) % maxSize 点此跳转
- 指针后移操作不是简单地累加,而是:(front+1)%maxSize或者(rear+1)%maxSize点此跳转
- 显示队列元素,使用for循环实现,条件不能直接粗暴按照数组length来打印,循环中i范围是front——>front+size(),对应数组下标的范围 front%maxSize——>( front+size() )%maxSize
- 操作:存元素(注意指针操作),取元素(注意指针操作),元素个数,显示队列元素(注意for循环里的取模技巧),显示头元素。
- 小tips:
读取输入的字符调用.next().charAt()