稀疏数组

  • 前言
  • 稀疏数组
  • 稀疏数组案例
  • 队列
  • 数组模拟队列
  • 数组模拟环形队列


前言

    数据结构和算法的在程序开发中是十分重要的,算法是程序的灵魂,优秀的程序可以在海量数据计算时,依然保持高速计算。
    数据结构和算法的关系
    1、数据结构是一门研究组织数据方式的学科,有了编程语言也就有了数据结构,学好数据结构可以写出更加有效率的代码。
    2、程序=数据结构+算法。
    3、数据结构是算法的基础,学算法之前必须要有扎实的数据结构知识。
    数据结构的分类
    数据结构包括线性结构和非线性结构。
    1、线程结构
    1.1:线性结构作为最常用的数据结构,其特点是数据元素之间存在着“一对一”的线性关系的数据结构,如(a0,a1,a2,…,an),a0为第一个元素,an为最后一个元素,此集合即为一个线性结构的集合。
    1.2:线性结构有两种不同的存储结构,即顺序存储结构和链式存储结构,顺序存储的线性表为顺序表,顺序表中的存储元素是连续的。
    1.3:链式存储的线性表称为链表,链表中存储的元素不一定是连续的,元素节点中存放数据元素以及相邻元素的地址信息。
    1.4:线性结构常见的有:数组、队列、链表和栈。
    非线程结构
    非线性结构包括:二维数组、多维数组、广义表、树结构、图结构。

稀疏数组

    基本介绍

    当一个数组中大部分元素为0或者同一个值的数组时,可以使用稀疏数组来保存该数组。

    稀疏数组的处理方法是:1、记录数组一共有几行几列,有多少个不同的值。2、把具有不同值的元素的行列记录在一个小规模的数组中(它就是稀疏数组),从而缩小数组的规模。

    例如:五子棋的位置保存问题,可以使用二维数组去记录,1表示黑子,2表示白子,记录保存问题可以使用稀疏数组记录。

    二维数组转换称稀疏数组图示

数据结构与算法训练营_数据结构


    上图所示:将一个6行6列的二维数组转换成为一个3行6列的一个稀疏数组,稀疏数组的第一行记录的是原始数组有多少行,多少列,多少个元素。当然数据量变大之后,稀疏数组不再适合使用。

稀疏数组案例

    假定有一个十行十列有一个五子棋棋盘,需要保存起来,然后恢复成该棋盘的样子。

数据结构与算法训练营_数据结构_02

    上图中所示的棋盘转换为二维数组、二维数组再转换为稀疏数组、稀疏数组还原成二维码数组。
    二维数组转稀疏数组的思路
    1、遍历原始的二维数组,获取有效数据的个数 count。
    2、根据 count就可以创建稀疏数组 sparseArr int[count + 1] [3]
    3、将二维数组的有效数据存入到稀疏数组中
    4、稀疏数组中的第一行存储的数据是原始数组有多少行 多少列,多少个有效数据。
    稀疏数组转二维数组的思路
    1、先读取稀疏数组的第一行,根据第一行的数据,创建原始二维数组。
    2、再读取稀疏数组后几行的数据,并赋值给原始数组。
    代码示例

package com.example.data.sparse;

import java.io.*;

/**
 * 稀疏数组
 *
 * @author zjt
 */
public class SparseArr {

    public static void main(String[] args) throws Exception {
        // 先创建一个原始的二维数组 10 * 10
        // 0 表示没有棋子 1 表示黑色 2表示 白色
        int[][] chessArr = new int[10][10];
        chessArr[1][2] = 1;
        chessArr[2][3] = 2;
        chessArr[3][3] = 1;
        // 输出原始的二维数组
        System.out.println("输出原始的二维数组~~");
        for (int[] row : chessArr) {
            for (int data : row) {
                System.out.print(String.format("%d\t", data));
            }
            System.out.println();
        }
        // 将二维数组转成稀疏数组
        // 1、遍历二维数组 得到非0数据的个数
        int sum = 0;
        for (int[] row : chessArr) {
            for (int data : row) {
                if (data > 0) {
                    sum++;
                }
            }
        }

        System.out.println("-------------------------------");
        System.out.println("------二维数组转稀疏数组---------");

        // 2、创建对应的稀疏数组
        int[][] sparseArr = new int[sum + 1][3];
        // 给稀疏数组赋值
        sparseArr[0][0] = 10;
        sparseArr[0][1] = 10;
        sparseArr[0][2] = sum;
        // 遍历二维数组将非0的值赋值到稀疏数组中
        // count 用于记录是第几个非0数据
        int count = 0;
        for (int i = 0; i < chessArr.length; i++) {
            int[] data = chessArr[i];
            for (int j = 0; j < data.length; j++) {
                if (data[j] != 0) {
                    count++;
                    sparseArr[count][0] = i;
                    sparseArr[count][1] = j;
                    sparseArr[count][2] = data[j];
                }
            }
        }
        // 得到的稀疏数组的形式
        System.out.println();
        System.out.println("得到的稀疏数组的形式");
        for (int[] data : sparseArr) {
            System.out.println(String.format("%d\t%d\t%d\t", data[0], data[1], data[2]));
        }

        System.out.println("-------------------------------");
        System.out.println("------稀疏数组转二维数组---------");
        // 稀疏数组的第一行存储的是 行数 列数 有效数据总个数
        int[][] chessArr1 = new int[sparseArr[0][0]][sparseArr[0][1]];
        for (int i = 1; i < sparseArr.length; i++) {
            int[] data = sparseArr[i];
            chessArr1[data[0]][data[1]] = data[2];
        }
        System.out.println("输出转换后的二维数组~~");
        for (int[] row : chessArr1) {
            for (int data : row) {
                System.out.print(String.format("%d\t", data));
            }
            System.out.println();
        }

        // 将获取到的稀疏数组写出到磁盘
        // 起到一个保存的作用
        System.out.println("-------------------------------");
        System.out.println("将获取到的稀疏数组写出到磁盘");
        File file = new File("/Users/zjt/Desktop/arr.txt");
        if (!file.exists()) {
            file.createNewFile();
        }
        BufferedWriter bw = new BufferedWriter(new FileWriter(file));
        StringBuilder sb = new StringBuilder();
        for (int[] data : sparseArr) { // 数组转成字符串存储
            for (int datum : data) {
                sb.append(datum).append("\t");
            }
            sb.append("\n");
        }
        bw.write(String.valueOf(sb));
        bw.flush();
        bw.close();

        System.out.println("-------------------------------");
        System.out.println("从磁盘读取数组");
        // 从磁盘读取数组
        int[][] sArr;
        BufferedReader br = new BufferedReader(new FileReader("/Users/zjt/Desktop/arr.txt"));
        // 第一行数据 存储的是 原始二维数组的 行列数 有效值的个数
        String line = br.readLine();
        String[] arr = line.split("\t");
        sArr = new int[Integer.parseInt(arr[0])][Integer.parseInt(arr[1])];
        while ((line = br.readLine()) != null) {
            String[] s2 = line.split("\t");
            sArr[Integer.parseInt(s2[0])][Integer.parseInt(s2[1])] = Integer.parseInt(s2[2]);
        }
        br.close();
        for (int[] row : sArr) {
            for (int data : row) {
                System.out.print(String.format("%d\t", data));
            }
            System.out.println();
        }

    }

}

队列

    基本介绍

    1、队列是一个有序列表,可以用数组或者链表来实现。

    2、遵循先入先出原则,即先存入的数据要先取出,后存入的数据要后取出。

    3、示意图:(使用数组模拟队列)

数据结构与算法训练营_稀疏数组_03


    上图简要说明,声明了两个指针,rear和front两个变量,初始值是-1,rear是队首,front是队尾。当有数组加入时rear增加front不变,取出数据时rear不变。

数组模拟队列

    1、队列本身是有序列表,若使用数组的结构来存储队列的数据,则队列数组的声明如上图,其中maxSize是该队列的最大容量。
    2、因为队列的输出、输入是分别从前后端来处理,因此需要两个变量front及rear分别记录队列前后端的下标,front会随着数据的输出而改变,而rear则是根据数据的输入而改变。
    思路分析
    当数据存入队列是使用addQueue()方法,addQueue()的处理需要两个步骤:
    1、将尾指针往后移,rear+1,当 front == rear 表示空队列。
    2、若尾指针rear小于队列的最大下标 MaxSize-1,则存入rear所指的数组无法存入数据,rear == MaxSize-1 表示满队列。
    代码实现

package com.example.data.sparse.queue;

import java.util.Scanner;

/**
 * 数组模拟队列
 * 当前队列存在的问题 队列是一次性的 也就是即是空的也是满的
 * 空间不能重复使用
 *
 * @author zjt
 */
public class ArrayQueue {

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

    private int front; // 队列的头部

    private int rear; // 队列的尾部

    private int[] arr; // 数组用于存放数据

    public ArrayQueue(int arrMaxSize) {
        if (arrMaxSize < 3) { //数组最小长度是 3
            arrMaxSize = 3;
        }
        maxSize = arrMaxSize;
        arr = new int[arrMaxSize];
        front = -1;// 指向队列的头部, 指向队列头的前一个位置
        rear = -1; // 指向队列尾部,指向队列尾的数据,也就是队列的最后一个数据
    }

    private boolean isFull() { // 判断队列是否已经满了
        return rear == maxSize - 1;
    }

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

    // 添加数据到队列
    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++;
        return arr[front];
    }

    // 显示队列的所有数据
    public void showQueue() {
        if (isEmpty()) {
            System.out.println("队列为空");
            return;
        }
        for (int i = 0; i <= arr.length; i++) {
            System.out.println(String.format("arr[%d] = %d", i, arr[i]));
        }
    }

    // 显示队列的头数据 不是取出数据
    public int getHead() {
        if (isEmpty()) {
            throw new RuntimeException("队列为空");
        }
        return arr[front + 1];
    }

}

class Client {

    public static void main(String[] args) {
        // 创建一个队列
        ArrayQueue queue = new ArrayQueue(4);
        char key = ' ';//用于接收用户输入
        Scanner scanner = new Scanner(System.in);
        boolean flag = true;
        while (flag) {
            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 'e':
                    scanner.close();
                    flag = false;
                    break;
                case 'a':
                    System.out.println("请输入一个数");
                    int i = scanner.nextInt();
                    queue.addQueue(i);
                    break;
                case 'g':
                    try {
                        int result = queue.getQueue(); // 处理一下异常 避免程序直接结束
                        System.out.println("取出的数据为:" + result);
                    } catch (Exception e) {
                        System.out.println(e.getMessage());
                    }
                    break;
                case 'h':
                    try {
                        int result = queue.getHead();
                        System.out.println("查看队列头的数据:" + result);
                    } catch (Exception e) {
                        System.out.println(e.getMessage());
                    }
                    break;
                default:
                    break;
            }
        }
        System.out.println("程序退出");
    }

}

    目前数组仅利用一次,没有达到空间复用的效果。

数组模拟环形队列

    思路分析

数据结构与算法训练营_稀疏数组_04


    1、front指针的含义调整为->front指向队列的第一个元素,也就是arr[front]就是队列的第一个元素。

    2、real指针的含义调整为->rear指向队列的对后一个元素的后一个位置,原因是希望空出一个空间做约定。

    3、当队列满时,条件是 rear == maxSize-1 调整为 (rear +1) % maxSize == front 表示队列满了。

    4、 队列为空的条件是 rear == front 是空。

    5、rear 和 front 的初始值都为0。

    6、队列中有效的数据的个数 为 (rear + maxSize - front)%maxSize <==> Math.abs(rear-front) % maxSize

    7、可以将原来的队列修改转换成为环形队列

    代码实现

class Client {

    public static void main(String[] args) {
        // 创建一个队列
        RingArrayQueue queue = new RingArrayQueue(4);
        char key = ' ';//用于接收用户输入
        Scanner scanner = new Scanner(System.in);
        boolean flag = true;
        while (flag) {
            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 'e':
                    scanner.close();
                    flag = false;
                    break;
                case 'a':
                    System.out.println("请输入一个数");
                    int i = scanner.nextInt();
                    queue.addQueue(i);
                    break;
                case 'g':
                    try {
                        int result = queue.getQueue(); // 处理一下异常 避免程序直接结束
                        System.out.println("取出的数据为:" + result);
                    } catch (Exception e) {
                        System.out.println(e.getMessage());
                    }
                    break;
                case 'h':
                    try {
                        int result = queue.getHead();
                        System.out.println("查看队列头的数据:" + result);
                    } catch (Exception e) {
                        System.out.println(e.getMessage());
                    }
                    break;
                default:
                    break;
            }
        }
        System.out.println("程序退出");
    }

}

class RingArrayQueue {

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

    // front指针的含义调整为->front指向队列的第一个元素,也就是arr[front]就是队列的第一个元素。
    private int front; // 队列的头部 初始值为0

    // real指针的含义调整为->rear指向队列的对后一个元素的后一个位置,原因是希望空出一个空间做约定。
    private int rear; // 队列的尾部  初始值为0

    private int[] arr; // 数组用于存放数据 这里需要预留一个空的空间去应对环形的问题
                       // 实际存放的元素个数 是 arrMaxSize-1 需要约定一个空的位置 而这个空的位置是会移动的。

    public RingArrayQueue(int arrMaxSize) {
        maxSize = arrMaxSize;
        arr = new int[arrMaxSize];
    }

    private boolean isFull() { // 判断队列是否已经满了
        return (rear + 1) % maxSize == front; // 预留出一个空的空间 需要 +1
    }

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

    // 添加数据到队列
    public void addQueue(int n) {
        if (isFull()) {
            System.out.println("队列已经满了");
            return;
        }
        // 直接将数据加入
        arr[rear] = n;
        // 将rear后移 但是需要考虑环形 也就是取模
        rear = (rear + 1) % maxSize;
    }

    // 取出数据
    public int getQueue() {
        if (isEmpty()) {
            throw new RuntimeException("队列是空的");
        }
        // 这里需要分析出 front 是指向队列的第一个元素
        // 先把front 对应的值保留到一个临时变量 将front后移 然后将临时变量返回
        int value = arr[front];
        front = (front + 1) % maxSize;
        return value;
    }

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

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

    // 显示队列的头数据 不是取出数据
    public int getHead() {
        if (isEmpty()) {
            throw new RuntimeException("队列为空");
        }
        return arr[front];
    }
}