Java 内功修炼 之 数据结构与算法(二、基本数据结构以及代码实现)

二、基本数据结构以及代码实现

1、稀疏数组(Sparse Array)

(1)什么是稀疏数组?
  当数组中 值为 0 的元素 大于 非 0 元素 且 非 0 元素 分布无规律时,可以使用 稀疏数组 来表示该数组,其将一个大数组整理、压缩成一个小数组,用于节约磁盘空间。
注:
  不一定必须为 值为 0 的元素,一般 同一元素在数组中过多时即可。
  使用 稀疏数组 的目的是为了 压缩数组结构、节约磁盘空间(比如:一个二维数组 a[10][10] 可以存储 100 个元素,但是其只存储了 3 个元素后,那么将会有 97 个空间被闲置,此时可以将 二维数组 转为 稀疏数组 存储,其最终转换成 b[4][3] 数组进行保存,即从 a[10][10] 的数组 压缩到 b[4][3],从而减少空间浪费)。

【举例:】
定义二维数组 a[4][5],并存储 3 个值如下:
    0 0 0 0 0 
    0 1 0 2 0
    0 0 0 0 0
    0 0 1 0 0
此时,数组中元素为 0 的个数大于 非 0 元素个数,所以可以作为 稀疏数组 处理。

换种方式,比如 将 0 替换成 5 如下,也可以视为 稀疏数组 处理。
    5 5 5 5 5
    5 1 5 2 5
    5 5 5 5 5
    5 5 1 5 5

(2)二维数组转为稀疏数组:

【如何处理:】
    Step1:先记录数组 有几行几列,有多少个不同的值。
    Step2:将不同的值 的元素 的 行、列、值 记录在一个 小规模的 数组中,从而将 大数组 缩减成 小数组。

【举例:】
原二维数组如下:
    0 0 0 0 0 
    0 1 0 2 0
    0 0 0 0 0
    0 0 1 0 0
    
经过处理后变为 稀疏数组 如下:
    行   列    值
    4    5     3       // 首先记录原二维数组 有 几行、几列、几个不同值
    
    1    1     1       // 表示原二维数组中 a[1][1] = 1
    1    3     2       // 表示原二维数组中 a[1][3] = 2
    3    2     1       // 表示原二维数组中 a[3][2] = 1
    
可以看到,原二维数组 a[4][5] 转为 稀疏数组 b[4][3],空间得到利用、压缩。

(3)二维数组、稀疏数组 互相转换实现

【二维数组 转 稀疏数组:】
    Step1:遍历原始二维数组,得到 有效数据 个数 num。
    Step2:根据有效数据个数创建 稀疏数组 a[num + 1][3]。
    Step3:将原二维数组中有效数据存储到 稀疏数组中。
注:
    稀疏数组有 三列:分别为:行、 列、 值。
    稀疏数组 第一行 存储的为 原二维数组的行、列 以及 有效数据个数。其余行存储 有效数据所在的 行、列、值。
    所以数组定义为 [num + 1][3]

【稀疏数组 转 二维数组:】
    Step1:读取 稀疏数组 第一行数据并创建 二维数组 b[行][列]。
    Step2:读取其余行,并赋值到新的二维数组中。
    
【代码实现:】
package com.lyh.array;

import java.util.HashMap;
import java.util.Map;

public class SparseArray {
    public static void main(String[] args) {
        // 创建原始 二维数组,定义为 4 行 10 列,并存储 两个 元素。
        int[][] arrays = new int[4][10];
        arrays[1][5] = 8;
        arrays[2][3] = 7;

        // 遍历输出原始 二维数组
        System.out.println("原始二维数组如下:");
        showArray(arrays);

        // 二维数组 转 稀疏数组
        System.out.println("\n二维数组 转 稀疏数组如下:");
        int[][] sparseArray = arrayToSparseArray(arrays);
        showArray(sparseArray);

        // 稀疏数组 再次 转为 二维数组
        System.out.println("\n稀疏数组 转 二维数组如下:");
        int[][] sparseToArray = sparseToArray(sparseArray);
        showArray(sparseToArray);
    }

    /**
     * 二维数组 转 稀疏数组
     * @param arrays 二维数组
     * @return 稀疏数组
     */
    public static int[][] arrayToSparseArray(int[][] arrays) {
        // count 用于记录有效数据个数
        int count = 0;
        // HashMap 用于保存有效数据(把 行,列 用逗号分隔拼接作为 key,值作为 value)
        Map<String, Integer> map = new HashMap<>();
        // 遍历得到有效数据、以及总个数
        for (int i = 0; i < arrays.length; i++) {
            for (int j = 0; j < arrays[i].length; j++) {
                if (arrays[i][j] != 0) {
                    count++;
                    map.put(i + "," + j, arrays[i][j]);
                }
            }
        }
        // 根据有效数据总个数定义 稀疏数组,并赋值
        int[][] result = new int[count + 1][3];
        result[0][0] = arrays.length;
        result[0][1] = arrays[0].length;
        result[0][2] = count;
        // 把有效数据从 HashMap 中取出 并放到 稀疏数组中
        for(Map.Entry<String, Integer> entry : map.entrySet()) {
            String[] temp = entry.getKey().split(",");
            result[count][0] = Integer.valueOf(temp[0]);
            result[count][1] = Integer.valueOf(temp[1]);
            result[count][2] = entry.getValue();
            --count;
        }
        return result;
    }

    /**
     * 遍历输出 二维数组
     * @param arrays 二维数组
     */
    public static void showArray(int[][] arrays) {
        for (int[] a : arrays) {
            for (int data : a) {
                System.out.print(data + " ");
            }
            System.out.println();
        }
    }

    /**
     * 稀疏数组 转 二维数组
     * @param arrays 稀疏数组
     * @return 二维数组
     */
    public static int[][] sparseToArray(int[][] arrays) {
        int[][] result = new int[arrays[0][0]][arrays[0][1]];
        for (int i = 1; i < arrays.length; i++) {
            result[arrays[i][0]][arrays[i][1]] = arrays[i][2];
        }
        return result;
    }
}    

【输出结果:】
原始二维数组如下:
0 0 0 0 0 0 0 0 0 0 
0 0 0 0 0 8 0 0 0 0 
0 0 0 7 0 0 0 0 0 0 
0 0 0 0 0 0 0 0 0 0 

二维数组 转 稀疏数组如下:
4 10 2 
1 5 8 
2 3 7 

稀疏数组 转 二维数组如下:
0 0 0 0 0 0 0 0 0 0 
0 0 0 0 0 8 0 0 0 0 
0 0 0 7 0 0 0 0 0 0 
0 0 0 0 0 0 0 0 0 0

2、队列(Queue)、环形队列

(1)什么是队列?
  队列指的是一种 受限的、线性的数据结构,其仅允许在 一端进行插入操作(队尾插入,rear),且在另一端进行 删除操作(队首删除,front)。
  队列可以使用 数组 或者 链表 实现(一般采用数组实现,仅在首尾增删,效率比链表高)。
       其遵循 先进先出(First In First Out,FIFO) 原则,即先存入 队列的值 先取出。

【使用 数组实现 队列:】
需要注意三个值:
    maxSize: 表示队列最大容量。
    front:   表示队列头元素下标(指向队列头部的第一个元素的前一个位置),初始值为 -1.
    rear:    表示队列尾元素下标(指向队列尾部的最后一个元素),初始值为 -1。

临界条件:
    front == rear 时,表示队列为 空。
    rear == maxSize - 1 时,表示队列已满。
    rear - front, 表示队列的存储元素的个数。
    
数据进入队列时:
    front 不动,rear++。

数据出队列时:
    rear 不动,front++。

如下图:
  红色表示入队操作,rear 加 1。
  黄色表示出队操作,front 加 1。
  每次入队,向当前实际数组尾部添加元素,每次出队,从当前实际数组头部取出元素,符合 先进先出原则。

  可以很明显的看到,如果按照这种方式实现队列,黄色区域的空间将不会被再次使用,即此时的队列是一次性的。
  那么如何重复利用 黄色区域的空间?可以采用 环形队列实现(看成一个环来实现)。

  环形队列在 上面队列的基础上稍作修改,当成环处理(数据首尾相连,可以通过 % 进行取模运算实现),核心是考虑 队列 什么时候为空,什么时候为满。
  一般采用 牺牲一个 数组空间 作为判断当前队列是否已满的条件。

【使用 数组 实现环形队列:(此处仅供参考)】
需要注意三个值:
    maxSize: 表示队列最大容量。
    front:   表示队列头元素下标(指向队列头部的第一个元素),初始值为 0。
    rear:    表示队列尾元素下标(指向队列尾部的最后一个元素的后一个位置),初始值为 0。

临界条件:
    front == rear 时,表示队列为 空。
    (rear + 1) % maxSize == front 时,表示队列已满。
    (rear - front + maxSize) % maxSize, 表示队列的存储元素的个数。
    
数据进入队列时:
    front 不动,rear = (rear + 1) % maxSize。

数据出队列时:
    rear 不动,front = (front + 1) % maxSize。

(2)使用数组实现队列

【代码实现:】
package com.lyh.queue;

public class ArrayQueue<E> {

    private int maxSize; // 队列最大容量
    private int front; // 队列首元素
    private int rear; // 队列尾元素
    private Object[] queue; // 存储队列

    /**
     * 构造初始队列
     * @param maxSize 队列最大容量
     */
    public ArrayQueue(int maxSize) {
        this.maxSize = maxSize;
        queue = new Object[maxSize];
        front = -1;
        rear = -1;
    }

    /**
     * 添加数据进入队列
     * @param e 待入数据
     */
    public void addQueue(E e) {
        if (isFull()) {
            System.out.println("队列已满");
            return;
        }
        // 队列未满时,添加数据,rear 向后移动一位
        queue[++rear] = e;
    }

    /**
     * 从队列中取出数据
     * @return 待取数据
     */
    public E getQueue() {
        if (isEmpty()) {
            System.out.println("队列已空");
            return null;
        }
        // 队列不空时,取出数据,front 向后移动一位
        return (E)queue[++front];
    }

    /**
     * 输出当前队列所有元素
     */
    public void showQueue() {
        if (isEmpty()) {
            System.out.println("队列已空");
            return;
        }
        System.out.print("当前队列存储元素总个数为:" + getSize() + "  当前队列为:");
        for(int i = front + 1; i <= rear; i++) {
            System.out.print(queue[i] + " ");
        }
        System.out.println();
    }

    /**
     * 获取当前队列实际大小
     * @return 队列实际存储数据数量
     */
    public int getSize() {
        return rear - front;
    }

    /**
     * 判断队列是否为空
     * @return true 为空
     */
    public boolean isEmpty() {
        return front == rear;
    }

    /**
     * 判断队列是否已满
     * @return true 已满
     */
    public boolean isFull() {
        return rear == maxSize - 1;
    }

    public static void main(String[] args) {
        // 创建队列
        ArrayQueue<Integer> arrayQueue = new ArrayQueue<>(6);
        // 添加数据
        arrayQueue.addQueue(10);
        arrayQueue.addQueue(8);
        arrayQueue.addQueue(9);
        arrayQueue.showQueue();

        // 取数据
        System.out.println(arrayQueue.getQueue());
        System.out.println(arrayQueue.getQueue());
        arrayQueue.showQueue();
    }
}

【输出结果:】
当前队列存储元素总个数为:3  当前队列为:10 8 9 
10
8
当前队列存储元素总个数为:1  当前队列为:9

 (3)使用数组实现环形队列

【代码实现:】
package com.lyh.queue;

public class ArrayCircleQueue<E> {

    private int maxSize; // 队列最大容量
    private int front; // 队列首元素
    private int rear; // 队列尾元素
    private Object[] queue; // 存储队列

    /**
     * 构造初始队列
     * @param maxSize 队列最大容量
     */
    public ArrayCircleQueue(int maxSize) {
        this.maxSize = maxSize;
        queue = new Object[maxSize];
        front = 0;
        rear = 0;
    }

    /**
     * 添加数据进入队列
     * @param e 待入数据
     */
    public void addQueue(E e) {
        if (isFull()) {
            System.out.println("队列已满");
            return;
        }
        // 队列未满时,添加数据,rear 向后移动一位
        queue[rear] = e;
        rear = (rear + 1) % maxSize;
    }

    /**
     * 从队列中取出数据
     * @return 待取数据
     */
    public E getQueue() {
        if (isEmpty()) {
            System.out.println("队列已空");
            return null;
        }
        // 队列不空时,取出数据,front 向后移动一位
        E result = (E)queue[front];
        front = (front + 1) % maxSize;
        return result;
    }

    /**
     * 输出当前队列所有元素
     */
    public void showQueue() {
        if (isEmpty()) {
            System.out.println("队列已空");
            return;
        }
        System.out.print("当前队列存储元素总个数为:" + getSize() + "  当前队列为:");
        for(int i = front; i < front + getSize(); i++) {
            System.out.print(queue[i] + " ");
        }
        System.out.println();
    }

    /**
     * 获取当前队列实际大小
     * @return 队列实际存储数据数量
     */
    public int getSize() {
        return (rear - front + maxSize) % maxSize;
    }

    /**
     * 判断队列是否为空
     * @return true 为空
     */
    public boolean isEmpty() {
        return front == rear;
    }

    /**
     * 判断队列是否已满
     * @return true 已满
     */
    public boolean isFull() {
        return (rear + 1) % maxSize == front;
    }

    public static void main(String[] args) {
        // 创建队列
        ArrayCircleQueue<Integer> arrayQueue = new ArrayCircleQueue<>(3);
        // 添加数据
        arrayQueue.addQueue(10);
        arrayQueue.addQueue(8);
        arrayQueue.addQueue(9);
        arrayQueue.showQueue();

        // 取数据
        System.out.println(arrayQueue.getQueue());
        System.out.println(arrayQueue.getQueue());
        arrayQueue.showQueue();
    }
}

【输出结果:】
队列已满
当前队列存储元素总个数为:2  当前队列为:10 8 
10
8
队列已空

3、链表(Linked list)-- 单链表 以及 常见笔试题

(1)什么是链表?
  链表指的是 物理上非连续、非顺序,但是 逻辑上 有序 的 线性的数据结构。
  链表 由 一系列节点 组成,节点之间通过指针相连,每个节点只有一个前驱节点、只有一个后续节点。节点包含两部分:存储数据元素的数据域 (data)、存储下一个节点的指针域 (next)。
  可以使用 数组、指针 实现。比如:Java 中 ArrayList 以及 LinkedList。

(2)单链表实现?
  单链表 指的是 单向链表,首节点没有前驱节点,尾节点没有后续节点。只能沿着一个方向进行 遍历、获取数据的操作(即某个节点无法获取上一个节点的数据)。

注:
  头节点(非必须):仅用于作为链表起点,放在链表第一个节点前,无实际意义。
  首节点:指链表第一个节点,即头节点后面的第一个节点。
  头节点是非必须的,使用头节点是方便操作链表而设立的。如下代码实现采用 头节点 方式实现。

【模拟 指针形式 实现 单链表:】
模拟节点:
    节点包括 数据域(保存数据) 以及 指针域(指向下一个节点)。
    class Node<E> {
        E data; // 数据域,存储节点数据
        Node next; // 指针域,指向下一个节点
    
        public Node(E data) {
            this.data = data;
        }
    
        public Node(E data, Node<E> next) {
            this.data = data;
            this.next = next;
        }
    }
    
【增删节点:】
直接添加节点 A 到链表末尾:
    先得遍历得到最后一个节点 B 所在位置,条件为: B.next == null,
    然后将最后一个节点 B 的 next 指向该节点, 即 B.next = A。
    
向指定位置插入节点:
    比如: A->B 中插入 C, 即 A->C->B,此时,先让 C 指向 B,再让 A 指向 C。
    即 
        C.next = A.next;   // 此时 A.next = B
        A.next = C;

直接删除链表末尾节点:
    先遍历到倒数第二个节点 C 位置,条件为:C.next.next == null;
    然后将其指向的下一个节点置为 null 即可,即 C.next = null。

删除指定位置的节点:
    比如: A->C->B 中删除 C,此时,直接让 A 指向 B。
    即:
        A.next = C.next;

【代码实现:】
package com.lyh.com.lyh.linkedlist;

public class SingleLinkedList<E> {

    private int size; // 用于保存链表实际长度
    private Node<E> header; // 用于保存链表头节点,仅用作 起点,不存储数据。

    public SingleLinkedList(Node<E> header) {
        this.header = header;
    }

    /**
     * 在链表末尾添加节点
     * @param data 节点数据
     */
    public void addLastNode(E data) {
        Node<E> newNode = new Node<>(data); // 根据数据创建一个 新节点
        Node<E> temp = header; // 使用临时变量保存头节点,用于辅助遍历链表
        // 遍历链表
        while(temp.next != null) {
            temp = temp.next;
        }
        // 在链表末尾添加节点,链表长度加 1
        temp.next = newNode;
        size++;
    }

    /**
     * 在链表末尾添加节点
     * @param newNode 节点
     */
    public void addLastNode(Node<E> newNode) {
        Node<E> temp = header; // 使用临时变量保存头节点,用于辅助遍历链表
        // 遍历链表
        while(temp.next != null) {
            temp = temp.next;
        }
        // 在链表末尾添加节点,链表长度加 1
        temp.next = newNode;
        size++;
    }

    /**
     * 在链表指定位置 插入节点
     * @param node 待插入节点
     * @param index 指定位置(1 ~ n, 1 表示第一个节点位置)
     */
    public void insert(Node<E> node, int index) {
        Node<E> temp = header; // 使用临时变量保存头节点,用于辅助遍历链表
        // 节点越界则抛出异常
        if (index < 1 || index > size) {
            throw new IndexOutOfBoundsException("Index: " + index + ", Size: " + size);
        }
        // 若节点为链表末尾,则调用 末尾添加 节点的方法
        if (index == size) {
            addLastNode(node);
            return;
        }
        // 若节点不是链表末尾,则遍历找到插入位置
        while(index != 1) {
            temp = temp.next;
            index--;
        }
        // A -> B 变为 A -> C -> B, 即 A.next = B 变为 C.next = A.next, A.next = C,即 A 指向 C,C 指向 B。
        node.next = temp.next;
        temp.next = node;
        size++;
    }

    /**
     * 返回链表长度
     * @return 链表长度
     */
    public int size() {
        return size;
    }

    /**
     * 输出链表
     */
    public void showList() {
        Node<E> temp = header.next; // 使用临时变量保存第一个节点,用于辅助遍历链表
        if (size == 0) {
            System.out.println("当前链表为空");
            return;
        }
        // 链表不为空时遍历链表
        System.out.print("当前链表长度为: " + size + " 当前链表为: ");
        while(temp != null) {
            System.out.print(temp + " ===> ");
            temp = temp.next;
        }
        System.out.println();
    }

    /**
     * 删除最后一个节点
     */
    public void deleteLastNode() {
        Node<E> temp = header; // 使用临时变量保存头节点,用于遍历链表
        if (size == 0) {
            System.out.println("当前链表为空,无需删除");
            return;
        }
        while(temp.next.next != null) {
            temp = temp.next;
        }
        temp.next = null;
        size--;
    }

    /**
     * 删除指定位置的元素
     * @param index 指定位置(1 ~ n, 1 表示第一个节点位置)
     */
    public void delete(int index) {
        Node<E> temp = header; // 使用临时变量保存头节点,用于辅助遍历链表
        // 节点越界则抛出异常
        if (index < 1 || index > size) {
            throw new IndexOutOfBoundsException("Index: " + index + ", Size: " + size);
        }
        // 若节点为链表末尾,则调用 末尾删除 节点的方法
        if (index == size) {
            deleteLastNode();
            return;
        }
        // 遍历链表,找到删除位置
        while(index != 1) {
            index--;
            temp = temp.next;
        }
        // A -> C -> B 变为 A -> B,即 A.next = C, C.next = B 变为 A.next = C.next,即 A 直接指向 B
        temp.next = temp.next.next;
        size--;
    }

    public static void main(String[] args) {
        // 创建一个单链表
        SingleLinkedList<String> singleLinkedList = new SingleLinkedList(new Node("Header"));
        // 输出,此时链表为空
        singleLinkedList.showList();
        System.out.println("=======================================");

        // 给链表添加数据
        singleLinkedList.addLastNode("Java");
        singleLinkedList.addLastNode(new Node<>("JavaScript"));
        singleLinkedList.insert(new Node<>("Phthon"), 1);
        singleLinkedList.insert(new Node<>("C"), 3);
        // 输出链表
        singleLinkedList.showList();
        System.out.println("=======================================");

        // 删除链表数据
        singleLinkedList.deleteLastNode();
        singleLinkedList.delete(2);
        // 输出链表
        singleLinkedList.showList();
        System.out.println("=======================================");
    }
}

class Node<E> {
    E data; // 数据域,存储节点数据
    Node<E> next; // 指针域,指向下一个节点

    public Node(E data) {
        this.data = data;
    }

    public Node(E data, Node<E> next) {
        this.data = data;
        this.next = next;
    }

    @Override
    public String toString() {
        return "Node{ data = " + data + " }";
    }
}    

【输出结果:】    
当前链表为空
=======================================
当前链表长度为: 4 当前链表为: Node{ data = Phthon } ===> Node{ data = Java } ===> Node{ data = JavaScript } ===> Node{ data = C } ===> 
=======================================
当前链表长度为: 2 当前链表为: Node{ data = Phthon } ===> Node{ data = JavaScript } ===> 
=======================================

 (3)常见的单链表笔试题

【笔试题一:】
    找到当前链表中倒数 第 K 个节点。
    
【笔试题一解决思路:】
 思路一:
     链表长度 size 可知时,则可以遍历 size - k 个节点,从而找到倒数第 K 个节点。
     当然 size 可以通过遍历一遍链表得到,这会消耗时间。
     
思路二:
    链表长度 size 未知时,可使用 快慢指针 解决。
    使用两个指针 A、B 同时遍历,且指针 B 始终比指针 A 快 K 个节点,
    当 指针 B 遍历到链表末尾时,此时 指针 A 指向的下一个节点即为倒数第 K 个节点。
    
【核心代码如下:】
/**
 * 获取倒数第 K 个节点。
 * 方式一:
 *  size 可知,遍历 size - k 个节点即可
 * @param k K 值,(1 ~ n,1 表示倒数第一个节点)
 * @return 倒数第 K 个节点
 */
public Node<E> getLastKNode(int k) {
    Node<E> temp = header.next; // 使用临时变量存储第一个节点,用于辅助链表遍历
    // 判断节点是否越界
    if (k < 1 || k > size) {
        throw new IndexOutOfBoundsException("Index: " + k + ", Size: " + size);
    }
    // 遍历 size - k 个节点,即可找到倒数第 K 个节点
    for (int i = 0; i < size - k; i++) {
        temp = temp.next;
    }
    return temp;
}

/**
 * 获取倒数第 K 个节点。
 * 方式二:
 *  size 未知时,使用快慢节点,
 *  节点 A 比节点 B 始终快 k 个节点,A,B 同时向后遍历,当 A 遍历完成后,B 遍历的位置下一个位置即为倒数第 K 个节点。
 * @param k K 值,(1 ~ n,1 表示倒数第一个节点)
 * @return 倒数第 K 个节点
 */
public Node<E> getLastKNode2(int k) {
    Node<E> tempA = header; // 使用临时变量存储头节点,用于辅助链表遍历
    Node<E> tempB = header; // 使用临时变量存储头节点,用于辅助链表遍历
    // 节点越界判断
    if (k < 1) {
        throw new IndexOutOfBoundsException("Index: " + k);
    }
    // A 比 B 快 K 个节点
    while(tempA.next != null && k != 0) {
        tempA = tempA.next;
        k--;
    }
    // 节点越界判断
    if (k != 0) {
        throw new IndexOutOfBoundsException("K 值大于链表长度");
    }
    // 遍历,当 A 到链表末尾时,B 所处位置下一个位置即为倒数第 K 个节点
    while(tempA.next != null) {
        tempA = tempA.next;
        tempB = tempB.next;
    }
    return tempB.next;
}

【笔试题二:】
    找到当前链表的中间节点(链表长度未知)。

【笔试题二解决思路:】
    链表长度未知,可以采用 快慢指针 方式解决。
    此处与解决 上题 倒数第 K 个节点类似,只是此时节点 B 比 节点 A 每次都快 1 个节点(即 A 每次遍历移动一个节点,B 会遍历移动两个节点)。    

【核心代码如下:】
/**
 * 链表长度未知时,获取链表中间节点
 * @return 链表中间节点
 */
public Node<E> getHalfNode() {
    Node<E> tempA = header.next; // 使用临时变量保存第一个节点,用于辅助遍历链表
    Node<E> tempB = header.next; // 使用临时变量保存第一个节点,用于辅助遍历链表
    // 循环遍历 B 节点,B 节点每次都比 A 节点快一个节点(每次多走一个节点),所以当 B 遍历完成后,A 节点所处位置即为中间节点。
    while(tempB.next != null && tempB.next.next != null) {
        tempA = tempA.next;
        tempB = tempB.next.next;
    }
    return tempA;
}

 

【笔试题三:】
    反转链表。
    
【笔试题三解决思路:】
思路一:
    头插法,新建一个链表,遍历原始链表,将每个节点通过头插法插入新链表。
    头插法,即每次均在第一个节点位置处进行插入操作。
    
思路二:    
    直接反转。
    通过三个指针来辅助,beforeNode、currentNode、afterNode,此时 beforeNode -> currentNode -> afterNode。
    其中:
         beforeNode 为当前节点上一个节点。
         currentNode 为当前节点。
         afterNode 为当前节点下一个节点。
    遍历链表,使 currentNode -> beforeNode。

【核心代码如下:】
/**
 * 链表反转。
 * 方式一:
 *  头插法,新建一个链表,遍历原始链表,将每个节点通过头插法插入新链表。
 * @return
 */
public SingleLinkedList<E> reverseList() {
    Node<E> temp = header.next; // 使用临时变量存储第一个节点,用于辅助遍历原链表
    SingleLinkedList singleLinkedList = new SingleLinkedList(new Node("newHeader")); // 新建一个链表
    // 若原链表为空,则直接返回 空的 新链表
    if (temp == null) {
        return singleLinkedList;
    }
    // 遍历原链表,并调用新链表的 头插法添加节点
    while(temp != null) {
        singleLinkedList.addFirstNode(new Node(temp.data));
        temp = temp.next;
    }
    return singleLinkedList;
}

/**
 * 头插法插入节点,每次均在第一个节点位置处进行插入
 * @param node 待插入节点
 */
public void addFirstNode(Node<E> node) {
    Node<E> temp = header.next; // 使用临时变量保存第一个节点,用于辅助遍历链表
    // 若链表为空,则直接赋值即可
    if (temp == null) {
        header.next = node;
        size++;
        return;
    }
    // 若链表不为空,则在第一个节点位置进行插入
    node.next = temp;
    header.next = node;
    size++;
}

/**
 * 链表反转。
 * 方式二:
 *  直接反转,通过三个指针进行辅助。此方式会直接变化当前链表。
 */
public void reverseList2() {
    // 链表为空直接返回
    if (header.next == null) {
        System.out.println("当前链表为空");
        return;
    }
    Node<E> beforeNode = null; // 指向当前节点的上个节点
    Node<E> currentNode = header.next; // 指向当前节点
    Node<E> afterNode = null; // 指向当前节点的下一个节点
    // 遍历节点
    while(currentNode != null) {
        afterNode = currentNode.next; // 获取当前节点的下一个节点 
        currentNode.next = beforeNode; // 将当前节点指向上一个节点
        beforeNode = currentNode; // 上一个节点后移
        currentNode = afterNode; // 当前节点后移,为了下一个遍历
    }
    header.next = beforeNode; // 遍历结束后,beforeNode 为最后一个节点,使用 头节点 指向该节点,即可完成链表反转
}

 

【笔试题四:】
    打印输出反转链表,不能反转原链表。

【笔试题四解决思路:】
思路一(此处不重复演示,详见上例代码):
    由于不能反转原链表,可以与上例头插法相同,
    新建一个链表并使用头插法添加节点,最后遍历输出新链表。

思路二:
    使用栈进行辅助。栈属于先进后出结构。
    可以先遍历链表并存入栈中,然后依次取出栈顶元素即可。
    
思路三:
    使用数组进行辅助(有序结构存储一般均可,比如 TreeMap 存储,根据 key 倒序输出亦可)。
    遍历链表并存入数组,然后反序输出数组即可(注:若是反序存入数组,可以顺序输出)。    

【核心代码如下:】
/**
 * 不改变当前链表下,反序输出链表。
 * 方式一:
 *  借用栈结构进行辅助。栈是先进后出结构。
 *  先遍历链表并依次存入栈,然后从栈顶挨个取出数据,即可得到反序链表。
 */
public void printReverseList() {
    Node<E> temp = header.next; // 使用临时变量保存第一个节点,用于辅助链表遍历
    Stack<Node<E>> stack = new Stack(); // 使用栈存储节点
    // 判断链表是否为空
    if (temp == null) {
        System.out.println("当前链表为空");
        return;
    }
    // 遍历节点,使用栈存储链表各节点。
    while(temp != null) {
        stack.push(temp);
        temp = temp.next;
    }
    // 遍历输出栈
    while(stack.size() > 0) {
        System.out.print(stack.pop() + "==>");
    }
    System.out.println();
}

/**
 * 不改变当前链表下,反序输出链表。
 * 方式二:
 *  采用数组辅助。
 *  遍历链表存入数组,最后反序输出数组即可(注:若是反序存入数组,可以顺序输出)。
 */
public void printReverseList2() {
    Node<E> temp = header.next; // 使用临时变量保存第一个节点,用于辅助链表遍历
    int length = size();
    Node<E>[] nodes = new Node[length]; // 使用数组存储链表节点
    // 判断链表是否为空
    if(temp == null) {
        System.out.println("当前链表为空");
        return;
    }
    // 遍历链表,存入数组,此处反序存入数组,后面顺序输出即可
    while(temp != null) {
        nodes[--length] = temp;
        temp = temp.next;
    }
    System.out.println(Arrays.toString(nodes));
}

上述所有单链表相关代码完整版如下(有部分地方还需修改,仅供参考):

 单链表相关代码完整版

4、链表(Linked list)-- 双向链表、环形链表(约瑟夫环)

(1)双向链表
  通过上面单链表相关操作,可以知道 单链表的 查找方向唯一。
  而双向链表在 单链表的 基础上在 添加一个指针域(pre),这个指针域用来指向 当前节点的上一个节点,从而实现 链表 双向查找(某种程度上提高查找效率)。

【使用指针 模拟实现 双向链表:】
模拟节点:
    在单链表的基础上,增加了一个 指向上一个节点的 指针域。
    class Node2<E> {
        Node<E> pre; // 指针域,指向当前节点的上一个节点
        Node<E> next; // 指针域,指向当前节点的下一个节点
        E data; // 数据域,存储节点数据
    
        public Node2(E data) {
            this.data = data;
        }
        
        public Node2(E data, Node<E> pre, Node<E> next) {
            this.data = data;
            this.pre = pre;
            this.next = next;
        }
    }

【增删节点:】
直接添加节点 A 到链表末尾:
    首先得遍历到链表最后一个节点 B 的位置,条件: B.next = null。
    然后将 B 下一个节点指向 A, A 上一个节点指向 B。即 B.next = A;  A.pre = B。
    
指定位置添加节点 C:
    比如: A -> B 变为 A -> C -> B。
    即 A.next = B; B.pre = A; 变为 C.next = B; C.pre = B.pre; B.pre.next = C; B.pre = C;
    
直接删除链表末尾节点 A:
    遍历到链表最后一个节点 B 的位置,然后将其下一个节点指向 null 即可,即 B.next = null;        

删除指定位置的节点 C:
    比如: A -> C -> B 变为 A -> B。
    C.pre.next = C.next; C.next.pre = C.pre;

 

(2)双向链表代码实现如下:

【代码实现:】
package com.lyh.com.lyh.linkedlist;

public class DoubleLinkedList<E> {

    private int size = 0; // 用于保存链表实际长度
    private Node2<E> header; // 用于保存链表头节点,仅用作 起点,不存储数据。

    public DoubleLinkedList(Node2<E> header) {
        this.header = header;
    }

    /**
     * 直接在链表末尾添加节点
     * @param node 待添加节点
     */
    public void addLastNode(Node2<E> node) {
        Node2<E> temp = header; // 使用临时变量保存头节点,用于辅助链表遍历
        // 遍历链表至链表末尾
        while(temp.next != null) {
            temp = temp.next;
        }
        // 添加节点
        temp.next = node;
        node.pre = temp;
        size++;
    }

    /**
     * 直接在链表末尾添加节点
     * @param data 待添加数据
     */
    public void addLastNode2(E data) {
        Node2<E> temp = header; // 使用临时节点保存头节点,用于辅助链表遍历
        Node2<E> newNode = new Node2<>(data); // 创建新节点
        // 遍历链表至链表末尾
        while(temp.next != null) {
            temp = temp.next;
        }
        // 添加节点
        temp.next = newNode;
        newNode.pre = temp;
        size++;
    }

    /**
     * 遍历输出链表
     */
    public void showList() {
        Node2<E> temp = header.next; // 使用临时变量保存第一个节点,用于辅助遍历链表
        // 判断链表是否为空
        if(temp == null) {
            System.out.println("当前链表为空");
            return;
        }
        // 遍历输出链表
        System.out.print("当前链表长度为: " + size() + " == 当前链表为: ");
        while(temp != null) {
            System.out.print(temp + " ==> ");
            temp = temp.next;
        }
        System.out.println();
    }

    /**
     * 返回链表长度
     * @return 链表长度
     */
    public int size() {
        return this.size;
    }

    /**
     * 在指定位置添加节点
     * @param index 1 ~ n(1 表示 第一个节点)
     */
    public void insert(int index, Node2<E> newNode) {
        Node2<E> temp = header; // 使用临时变量保存头节点,用于辅助链表遍历
        // 遍历找到指定位置
        while(index != 0 && temp.next != null) {
            temp = temp.next;
            index--;
        }
        if (index != 0) {
            throw new IndexOutOfBoundsException("指定位置有误: " + index);
        }
        newNode.next = temp;
        newNode.pre = temp.pre;
        temp.pre.next = newNode;
        temp.pre = newNode;
        size++;
    }

    /**
     * 删除指定位置的节点
     * @param index 1 ~ n(1 表示第一个节点)
     */
    public void delete(int index) {
        Node2<E> temp = header; // 使用临时变量保存头节点,用于辅助链表遍历
        // 遍历找到待删除节点位置
        while(index != 0 && temp.next != null) {
            index--;
            temp = temp.next;
        }
        // 判断节点是否存在
        if (index != 0) {
            throw new IndexOutOfBoundsException("指定节点位置不存在");
        }
        temp.pre.next = temp.next;
        // 若节点为最后一个节点,则无需对下一个节点进行赋值操作
        if (temp.next != null) {
            temp.next.pre = temp.pre;
        }
        size--;
    }

    /**
     * 直接删除链表末尾节点
     */
    public void deleteLastNode() {
        Node2<E> temp = header; // 使用临时变量保存头节点,用于辅助链表遍历
        // 判断链表是否为空
        if (temp.next == null) {
            System.out.println("当前链表为空");
            return;
        }
        // 遍历链表至最后一个节点
        while(temp.next != null) {
            temp = temp.next;
        }
        temp.pre.next = null;
        size--;
    }

    public static void main(String[] args) {
        // 创建双向链表
        DoubleLinkedList<String> doubleLinkedList = new DoubleLinkedList<>(new Node2<>("header"));
        // 输出链表
        doubleLinkedList.showList();
        System.out.println("==========================");

        // 添加节点
        doubleLinkedList.addLastNode(new Node2<>("Java"));
        doubleLinkedList.addLastNode2("JavaScript");
        doubleLinkedList.insert(2, new Node2<>("E"));
        doubleLinkedList.insert(1, new Node2<>("F"));
        // 输出链表
        doubleLinkedList.showList();
        System.out.println("==========================");

        doubleLinkedList.delete(1);
        doubleLinkedList.deleteLastNode();
        // 输出链表
        doubleLinkedList.showList();
        System.out.println("==========================");
    }

}

class Node2<E> {
    Node2<E> pre; // 指针域,指向当前节点的上一个节点
    Node2<E> next; // 指针域,指向当前节点的下一个节点
    E data; // 数据域,存储节点数据

    public Node2(E data) {
        this.data = data;
    }

    public Node2(E data, Node2<E> pre, Node2<E> next) {
        this.data = data;
        this.pre = pre;
        this.next = next;
    }

    @Override
    public String toString() {
        return "Node2{ pre= " + (pre != null ? pre.data : null)  + ", next= " + (next != null ? next.data : null) + ", data= " + data + '}';
    }
}

【输出结果:】
当前链表为空
==========================
当前链表长度为: 4 == 当前链表为: Node2{ pre= header, next= Java, data= F} ==> Node2{ pre= F, next= E, data= Java} ==> Node2{ pre= Java, next= JavaScript, data= E} ==> Node2{ pre= E, next= null, data= JavaScript} ==> 
==========================
当前链表长度为: 2 == 当前链表为: Node2{ pre= header, next= E, data= Java} ==> Node2{ pre= Java, next= null, data= E} ==> 
==========================

(3)单向环形链表
  单向循环链表 指的是 在单链表基础上,将 最后一个节点的指针域 指向第一个节点,从而使链表变成一个环状结构。
  其最常见的应用场景就是 约瑟夫环 问题。

【约瑟夫(josephu)环问题:】
    已知 n 个人围成一圈,编号由 1 ~ n,从编号为 k (1 <= k <= n)的人开始从 1 报数,数到 m 的那个人出列。
    并从下一个人开始重新报数,再次数到 m 的人出列,依次类推,直至所有人出列,问 n 个人的出队编号(或者最后一个出队的是谁)。

【解决思路:】
    使用一个不带头节点的单向循环链表处理。
    先构成一个有 n 个节点的单向循环链表(构建一个单链表,并另最后一个节点 last 指向 第一个节点,即 last.next = first),
    由 k 节点开始从 1 计数,移动 m 个节点后将对应的节点从链表中删除。并从下一个节点开始计数,直至最后一个节点。 
    
    使用 两个节点指针来辅助链表遍历 -- first(指向当前第一个节点、且用于表示待移除的节点)、last(指向当前最后一个节点)。
    先遍历到 k 点(即 first 指向 k 点,last 指向 k 点上一个节点),计数(包括自身,所以 first、last 移动 m - 1 个节点),
    此时 first 指向的即为待输出的节点,输出后,将其移除。即 first = first.next; last.next = first;
    同理,从移除节点的下一个节点开始操作,当链表只剩最后一个节点,即 first == last 时,遍历结束,输出最后一个节点 即可。
注:
    last.next == first 表示环满
    last == first 表示环空,即环里只有一个节点        
    
【代码实现:】
package com.lyh.com.lyh.linkedlist;

public class CircleSingleLinkedList<E> {
    private Node3<E> first; // 保存第一个节点

    /**
     * 构成单向环形链表
     * @param num 链表节点个数 (1 ~ n, 1 表示 1 个节点)
     */
    public void addNode(int num) {
        // 判断 num 是否合适
        if (num < 1) {
            throw new IndexOutOfBoundsException("数据不能构成环");
        }
        Node3 temp = null; // 辅助指针,用于记录尾节点
        // 添加节点,构成循环链表
        for (int i = 1; i <= num; i++) {
            Node3 node = new Node3(i); // 构建新节点
            if (i == 1) {
                // 只有一个节点时,即为首节点
                first = node;
                temp = first;
            } else {
                // 添加尾节点
                temp.next = node;
                temp = node;
            }
        }
        // 尾节点指向首节点,构成环
        temp.next = first;
    }

    /**
     * 遍历输出当前环形链表
     */
    public void showList() {
        Node3<E> temp = first; // 使用临时变量存储第一个节点,用于辅助链表遍历
        if (temp == null) {
            System.out.println("当前链表为空");
            return;
        }
        System.out.print("当前链表为: ");
        while(temp.next != first) {
            System.out.print(temp + " ==> ");
            temp = temp.next;
        }
        System.out.println(temp);
    }

    /**
     * 按要求输出 移除节点 顺序
     * @param num 节点总数(n)
     * @param start 开始节点编号(1 ~ n)
     * @param count 计数(1 ~ m)
     */
    public void printList(int num, int start, int count) {
        Node3<E> last = first; // 用于记录当前链表最后一个节点
        if (last == null || start < 1 || start > num) {
            throw new RuntimeException("参数不合法");
        }
        // 遍历,得到最后一个节点
        while(last.next != first) {
            last = last.next;
        }
        // 找到开始节点, first 表示开始节点,last 表示最后一个节点(即开始节点的上一个节点)
        while(start != 1) {
            last = last.next;
            first = first.next;
            start--;
        }
        // 遍历输出节点(开始节点、最后节点重合时 即链表只存在一个节点)
        while(last != first) {
            // 找到待移除节点,由于当前节点会被计算,所以只需移动 count - 1 个节点。
            for (int i = 1; i < count; i++) {
                first = first.next;
                last = last.next;
            }
            System.out.print(first + " ==> ");
            // 移除节点(first 为被移除节点, 即 last -> first -> A 变为 fisrt = A 且 last -> A)
            first = first.next;
            last.next = first;
        }
        System.out.println(last);
    }

    public static void main(String[] args) {
        // 构建一个空的循环链表
        CircleSingleLinkedList<Integer> circleSingleLinkedList = new CircleSingleLinkedList<>();
        circleSingleLinkedList.showList();
        System.out.println("========================");

        // 添加节点
        int num = 5; // 节点个数
        circleSingleLinkedList.addNode(num);
        circleSingleLinkedList.showList();
        System.out.println("========================");

        // 输出节点 出链表顺序
        int start = 2; // 开始编号 K
        int count = 2; // 计数
        circleSingleLinkedList.printList(num, start, count);
    }
}

class Node3<E> {
    Node3<E> next; // 指针域,存储下一个节点
    E data; // 数据域,存储节点数据

    public Node3(E data) {
        this.data = data;
    }

    public Node3(Node3<E> next, E data) {
        this.next = next;
        this.data = data;
    }

    @Override
    public String toString() {
        return "Node3{ data= " + data + '}';
    }
}

【输出结果:】
当前链表为空
========================
当前链表为: Node3{ data= 1} ==> Node3{ data= 2} ==> Node3{ data= 3} ==> Node3{ data= 4} ==> Node3{ data= 5}
========================
Node3{ data= 3} ==> Node3{ data= 5} ==> Node3{ data= 2} ==> Node3{ data= 1} ==> Node3{ data= 4}

5、栈(Stack)

(1)什么是栈?
  栈指的是一种 受限、线性的数据结构,其仅允许在 一端 进行插入(栈顶插入 push)、删除操作(栈顶删除 pop)。其允许插入、删除的一端为 栈顶(Top),另一端为栈底(Bottom)。
  栈可以使用 数组 或者 链表 实现(一般采用数组实现,仅在首或尾增删,效率比链表高)。其遵循 先进后出(First In Last Out,FILO) 原则,即先存入 栈的值 后取出。

(2)常用场景:
  二叉树遍历(迭代法)。
  图的深度优先搜索法。
  表达式转换与求值(比如:中缀表达式 转 后缀表达式)。
  堆栈,比如:JVM 虚拟机栈 处理递归、子程序调用时,存储下一个指令地址 或者 参数、变量。

(3)使用数组模拟栈操作

【使用数组模拟栈操作:】
    定义 top 用于记录当前栈顶指向,初始值为 -1。
    数据 data 进栈时,top 先加 1 再赋值,即 stack[++top] = data。
    数据 data 出栈时,先保存出栈的值,top 再减 1,即 data = stack[top--]

【代码实现:】
package com.lyh.stack;

public class ArrayStack {

    private int maxSize; // 记录栈的大小(最大容量)
    private String[] stack; // 用于记录
    private int top = -1; // 用于初始化栈顶位置

    public ArrayStack(int maxSize) {
        this.maxSize = maxSize;
        this.stack = new String[maxSize];
    }

    /**
     * 判断栈是否为空
     * @return true 为空
     */
    public boolean isEmpty() {
        return top == -1;
    }

    /**
     * 判断栈是否已满
     * @return true 表示已满
     */
    public boolean isFull() {
        return top == maxSize - 1;
    }

    /**
     * 数据入栈
     * @param data 待入栈数据
     */
    public void push(String data) {
        // 判断栈是够已满,已满则不能再添加数据
        if (isFull()) {
            System.out.println("栈满,无法添加");
            return;
        }
        // top 加 1,并存值
        this.stack[++top] = data;
    }

    /**
     * 数据出栈
     * @return 出栈数据
     */
    public String pop() {
        // 判断栈是否为空,为空则无法返回数据
        if(isEmpty()) {
            System.out.println("栈空,无数据");
            return null;
        }
        // 取值,top 减 1
        return this.stack[top--];
    }

    /**
     * 遍历输出栈元素
     */
    public void showList() {
        // 判断栈是否为空
        if (isEmpty()) {
            System.out.println("栈空");
            return;
        }
        System.out.print("当前栈存储数据个数为: " + (top + 1) + " 当前栈输出为: ");
        for(int i = top; i >= 0; i--) {
            System.out.print(this.stack[i] + " == ");
        }
        System.out.println();
    }

    public static void main(String[] args) {
        // 实例化栈
        ArrayStack arrayStack = new ArrayStack(10);
        // 遍历栈
        arrayStack.showList();
        System.out.println("========================");

        // 数据入栈
        arrayStack.push("Java");
        arrayStack.push("Python");
        arrayStack.push("JavaScript");
        // 遍历栈
        arrayStack.showList();
        System.out.println("========================");

        // 数据出栈
        System.out.println(arrayStack.pop());
        System.out.println("========================");

        // 遍历栈
        arrayStack.showList();
        System.out.println("========================");
    }
}

【输出结果:】
栈空
========================
当前栈存储数据个数为: 3 当前栈输出为: JavaScript == Python == Java == 
========================
JavaScript
========================
当前栈存储数据个数为: 2 当前栈输出为: Python == Java == 
========================

6、使用栈计算 前缀(波兰)、中缀、逆波兰(后缀)表达式

(1)表达式的三种表示形式:
表达式可以分为三种表示形式:
  前缀(波兰)表达式。其运算符在操作数之前。
  中缀表达式。常见的算术公式(运算符在操作数中间),其括号不可省。
  后缀(逆波兰)表达式。其运算符在操作数之后。

举例(以下为表达式的三种表示形式):
  前缀表达式:+ 3 4
  中缀表达式:3 + 4
  后缀表达式:4 3 +

注:
  中缀表达式虽然易读,但是计算机处理起来稍微有点麻烦(比如:括号的处理),不如 前缀、后缀 处理方便(消除了括号)。
  所以一般处理表达式时 会对表达式进行转换,比如:中缀表达式转为后缀表达式,然后再对 后缀表达式进行处理。
中缀转后缀、中缀转前缀 过程类似,需要注意的是(详细可见下面转换步骤):
  中缀转前缀时,从右至左扫描字符串,且遇到右括号 ")" 直接入栈。
  中缀转后缀时,从左至右扫描字符串,且遇到左括号 "(" 直接入栈。

(2)前缀表达式 以及 中缀 转 前缀

【前缀(波兰)表达式:】
基本概念:
    前缀表达式又称为 波兰表达式,其运算符(+、-、*、/)位于操作数前。

举例:    
    一个表达式为:(3 + 4) * 5 - 6,其 对应的前缀表达式为 - * + 3 4 5 6。

如何处理前缀表达式:
    Step1:需要一个栈来存储操作数,从右至左扫描表达式。
        Step1.1:如果扫描的是数字,那么就将数字入栈,
        Step1.2:如果扫描的是字符(+、-、*、/),就弹出栈顶值两次,并通过运算符进行计算,最后将结果再次入栈。
   Step2:重复 Step1 过程直至 表达式扫描完成,最后栈中的值即为 表达式结果。     

如何处理前缀表达式(- * + 3 4 5 6):
    Step1:从右至左扫描,依次将 6 5 4 3 入栈。此时栈元素为 6 5 4 3。
        Step1.1:扫描到 +,弹出栈顶值 3、4,相加(4 + 3 = 7)并入栈,即 此时栈元素为 6 5 7。
        Step1.2:扫描到 *,弹出栈顶值 7、5,相乘(5 * 7 = 35)并入栈,即 此时栈元素为 6 35。
        Step1.3:扫描到 -,弹出栈顶值 6、35,相减(35 - 6 = 29)并入栈,即 此时栈元素为 29。
        
        
【中缀表达式 转换为 前缀表达式:】
中缀表达式转前缀表达式步骤:
    Step1:初始化两个栈 A、B,A 用于记录 运算符、B 用于记录 中间结果。
    Step2:从右至左扫描 中缀表达式。
        Step2.1:如果扫描的是数字,直接将其压入栈 B。
        Step2.2:如果扫描的是运算符(+、-、*、/),则比较当前运算符 与 A 栈顶运算符 的优先级。
            Step2.2.1:若 A 为空 或者 栈顶运算符为右括号 ")",则当前运算符 直接入栈。
            Step2.2.2:若上面条件不满足,则比较优先级,若当前运算符优先级 比 A 栈顶运算符 优先级高,则当前运算符 也入栈。
            Step2.2.3:若上面条件不满足,即当前运算符优先级低,则将 A 栈顶运算符弹出并压入 B 栈。重新执行 Step2.2 进行运算符比较。
        Step2.3:如果扫描的是括号
            Step2.3.1:如果为右括号 ")",则直接压入 A 栈。
            Step2.3.2:如果为左括号 "(",则依次弹出 A 栈顶元素并压入 B 栈,直至遇到 右括号 ")",此时 这对括号可以 舍弃。
        Step2.4:重复上面扫描步骤,直至表达式扫描完成。
    Step3:将 A 栈中剩余元素依次取出并压入 B 栈。
    Step4:此时 B 栈顺序取出结果即为前缀表达式。

中缀表达式 "(3 + 4) * 5 - 6" 如何 转为前缀表达式 "- * + 3 4 5 6":     
    Step1:初始化两个栈 A、B。A 存储运算符,B 存储中间结果。从右到左扫描中缀表达式。
       Step1.1:扫描到 6,直接存入 B 栈,此时 A 栈元素为 空,B 栈元素为 6。
       Step1.2:扫描到 -,此时 A 栈为空,直接存入 A 栈,此时 A 栈元素为 -,B 栈元素为 6。
       Step1.3:扫描到 5,直接存入 B 栈,此时 A 栈元素为 -,B 栈元素为 6 5。
       Step1.4:扫描到 *,当前运算符 * 比 A 栈顶运算符 优先级高,直接入栈,即此时 A 栈元素为 - *,B 栈元素为 6 5。
       Step1.5:扫描到 ),直接入 A 栈,此时 A 栈元素为 - * ),B 栈元素为 6 5。
       Step1.6:扫描到 4,直接存入 B 栈,此时 A 栈元素为 - * ),B 栈元素为 6 5 4。
       Step1.7:扫描到 +,由于栈顶元素为右括号 ")",直接入 A 栈,此时 A 栈元素为 - * ) +,B 栈元素为 6 5 4。
       Step1.8:扫描到 3,直接入 B 栈,此时 A 栈元素为 - * ) +,B 栈元素为 6 5 4 3。
       Step1.9:扫描到左括号 "(",A 栈顶元素出栈并压入 B 栈直至遇到 右括号 ")",且移除括号,此时 A 栈元素为 - *, B 栈元素为 6 5 4 3 +。
    Step2:将 A 栈剩余元素依次取出并压入 B 栈。此时 A 栈为空,B 栈元素为 6 5 4 3 + * -。
    Step3:将 B 依次取出即为前缀表达式 "- * + 3 4 5 6"。

(3)中缀表达式

【中缀表达式:】
基本概念:
    中缀表达式就是最常见的运算表达式,其运算符在操作数中间。
注:
    中缀表达式括号不可省,其用于表示运算的优先顺序。    
    
举例:    
    一个表达式为:(3 + 4) * 5 - 6,这就是中缀表达式。

如何处理中缀表达式:
    Step1:需要两个栈 A、B,A 用于存放 操作数,B 用于存放 符号(运算符、括号)。
    Step2:从左到右扫描 中缀表达式。
        Step2.1:如果扫描的是 数字,则直接压入 A 栈。
        Step2.2:如果扫描的是 运算符(+、-、*、/),则比较当前运算符 与 B 栈顶运算符 的优先级。
            Step2.2.1:若 B 为空 或者 栈顶元素为左括号 "(",则当前运算符直接入栈。
            Step2.2.2:若上面条件不满足,则比较优先级,若当前运算符 比 B 栈顶运算符 优先级高,则当前运算符 入 B 栈。
            Step2.2.3:若上面条件不满足,即当前运算符优先级低,则将 B 栈顶运算符弹出,并弹出 A 栈顶两个数据进行 计算,最后将计算结果存入 A 栈。重新执行 Step2.2 进行运算符比较。
        Step2.3:如果扫描的是括号:
            Step2.3.1:若为左括号 "(",则直接压入 B 栈。
            Step2.3.2:若为右括号 ")",则依次弹出 B 栈运算符直至遇到左括号 "(",B 栈每取一个元素,A 栈取两个元素,计算后将结果重新压入 A 栈。
        Step2.4:重复上面扫描步骤,直至表达式扫描完成。
     Step3:依次取出 B 栈顶运算符 以及 A 栈顶元素 计算,最后结果即为 表达式结果。 
注:
    直接处理中缀表达式,在于其会直接通过 运算符 进行运算。

如何处理中缀表达式 "(3 + 4) * 5 - 6":
    Step1:初始化两个栈 A、B。A 用于记录 操作数, B 用于记录 运算符。
    Step2:从左至右扫描 中缀表达式。
        Step2.1:扫描到左括号 "(",直接入 B 栈,此时 A 栈元素为空,B 栈元素为 (。
        Step2.2:扫描到 3,直接入 A 栈,此时 A 栈元素为 3,B 栈元素为 (。
        Step2.3:扫描到 +,此时 B 栈顶元素为左括号 "(",直接入 B 栈,此时 A 栈元素为 3,B 栈元素为 ( +。
        Step2.4:扫描到 4,直接入 A 栈,此时 A 栈元素为 4,B 栈元素为 ( +。
        Step2.5:扫描到右括号 ")",B 栈顶元素 + 出栈,A 栈弹出 4、 3,计算后重新压入 A 栈, 
                B 继续弹出栈顶元素为左括号 "(",直接将其出栈。此时 A 栈元素为 7,B 栈元素为空。
        Step2.6:扫描到 *,B 栈元素为空,直接入 B 栈,此时 A 栈元素为 7,B 栈元素为 *。
        Step2.7:扫描到 5,直接入 A 栈,此时 A 栈元素为 7 5,B 栈元素为 *。
        Step2.8:扫描到 -,当前运算符 - 比 B 栈顶运算符优先级 低,B 栈顶运算符出栈,A 栈弹出 5、7,计算后压入 A 栈,
                此时 B 栈为空,当前运算符直接压入 B 栈,即此时 A 栈元素为 35,B 栈元素为 -。   
        Step2.9:扫描到 6,直接入 A 栈,此时 A 栈元素为 35 6, B 栈元素为 -。
    Step3:取出 B 栈顶元素 -,A 栈弹出元素 6、35,计算后压入 A 栈,此时 B 栈为空,即表达式计算结束,A 栈最终结果即为表达式结果,即 29。

(4)后缀表达式 以及 中缀 转 后缀

【后缀(逆波兰)表达式:】
基本概念:
    后缀表达式又称为 逆波兰表达式,其运算符位于操作数之后。
    
举例:
    一个表达式为:(3 + 4) * 5 - 6,其 对应的前缀表达式为:3 4 + 5 * 6 -
     
如何处理后缀表达式:    
    Step1:需要一个栈来存储操作数,从左至右扫描表达式。
        Step1.1:如果扫描的是数字,那么就将数字入栈,
        Step1.2:如果扫描的是字符(+、-、*、/),就弹出栈顶值两次,并通过运算符进行计算,最后将结果再次入栈。
    Step2:重复 Step1 过程直至 表达式扫描完成,最后栈中的值即为 表达式结果。

如何处理后缀表达式(3 4 + 5 * 6 -):
    Step1:从左至右扫描,依次将 3 4 入栈。此时栈元素为 3 4。
        Step1.1:扫描到 +,弹出栈顶值 3、4,相加并入栈,即 此时栈元素为 7。
        Step1.2:扫描到 5,入栈,即 此时栈元素为 7 5。
        Step1.3:扫描到 *,弹出栈顶值 5、7,相乘并入栈,即 此时栈元素为 35。
        Step1.4:扫描到 6,入栈,即 此时栈元素为 35 6。
        Step1.5:扫描到 -,弹出栈顶值 6、35,相减并入栈,即 此时栈元素为 29。
        
        
【中缀表达式 转换为 后缀表达式:】
中缀表达式转后缀表达式步骤:
    Step1:初始化两个栈 A、B,A 用于记录 运算符、B 用于记录 中间结果。
    Step2:从左至右扫描 中缀表达式。
        Step2.1:如果扫描的是数字,直接将其压入栈 B。
        Step2.2:如果扫描的是运算符(+、-、*、/),则比较当前运算符 与 A 栈顶运算符 的优先级。
            Step2.2.1:若 A 为空 或者 栈顶运算符为左括号 "(",则当前运算符 直接入栈。
            Step2.2.2:若上面条件不满足,则比较优先级,若当前运算符优先级 比 A 栈顶运算符 优先级高,则当前运算符 也入栈。
            Step2.2.3:若上面条件不满足,即当前运算符优先级低,则将 A 栈顶运算符弹出并压入 B 栈。重新执行 Step2.2 进行运算符比较。
        Step2.3:如果扫描的是括号
            Step2.3.1:如果为左括号 "(",则直接压入 A 栈。
            Step2.3.2:如果为右括号 ")",则依次弹出 A 栈顶元素并压入 B 栈,直至遇到 左括号 "(",此时 这对括号可以 舍弃。
        Step2.4:重复上面扫描步骤,直至表达式扫描完成。
    Step3:将 A 栈中剩余元素依次取出并压入 B 栈。
    Step4:此时 B 栈逆序结果即为后缀表达式。
注:
    实际写代码时,由于 B 栈自始至终不会进行弹出操作,且其结果的 逆序 才是 后缀表达式。
    所以为了减少一次 逆序 的过程,可以直接使用 数组 或者 链表 进行存储,然后 顺序读取即可。


中缀表达式 "(3 + 4) * 5 - 6" 如何 转为后缀表达式 "3 4 + 5 * 6 -":     
    Step1:初始化两个栈 A、B。A 存储运算符,B 存储中间结果。从左至右扫描中缀表达式。
        Step1.1:扫描到左括号 "(",压入 A 栈,此时 A 栈元素为 (,B 栈元素为空。
        Step1.2:扫描到 3,压入 B 栈,此时 A 栈元素为 (,B 栈元素为 3。
        Step1.3:扫描到 +,由于 A 栈顶元素为左括号 "(",所以直接入栈。此时 A 栈元素为 ( +,B 栈元素为 3。
        Step1.4:扫描到 4,压入 B 栈,此时 A 栈元素为 ( +,B 栈元素为 3 4。
        Step1.5:扫描到右括号 ),A 栈元素依次出栈压入 B 直至遇到左括号 "(",并移除括号。此时 A 栈元素为 空,B 栈元素为 3 4 +。
        Step1.6:扫描到 *,由于 A 栈为空直接入栈,此时 A 栈元素为 *,B 栈元素为 3 4 +。
        Step1.7:扫描到 5,压入 B 栈,A 栈元素为 *,B 栈元素为 3 4 + 5。
        Step1.8:扫描到 -,当前运算符 - 优先级低于 A 优先级,所以 A 栈顶元素弹出并压入 B 栈,此时 A 栈为空,当前运算符直接存入。此时 A 栈元素为 -,B 栈元素为 3 4 + 5 *。
        Step1.9:扫描到 6,压入 B 栈,此时 A 栈元素为 -,B 栈元素为 3 4 + 5 * 6。
    Step2:将 A 剩余元素出栈并压入 B。此时 A 栈为空,B 栈元素为 3 4 + 5 * 6 -。
    Step3:将 B 栈元素依次取出并倒序输出,即为 后缀表达式 "3 4 + 5 * 6 -"。

(5)中缀表达式、前缀表达式、后缀表达式代码实现
  如下代码,实现 基本表达式(多位数且带括号)的 +、-、*、/。
  此处直接使用 Stack 类作为 栈 使用,不使用自定义栈结构。

【代码实现:】
package com.lyh.stack;

import java.util.ArrayList;
import java.util.List;
import java.util.Stack;

public class Expression {

    public static void main(String[] args) {
        Expression expressionDemo = new Expression();
        // 定义一个表达式(默认格式正确,此处不做过多的格式校验)
//         String expression = ("2+3*(7-4)+8/4").trim();
        String expression = ("(13-6)*5-6").trim();
        System.out.println("当前表达式为: " + expression);
        System.out.println("================================");

        List<String> infixExpressionList = expressionDemo.transfor(expression);
        System.out.println("表达式转换后为中缀表达式: " + infixExpressionList);
        System.out.println("================================");

        System.out.println("中缀表达式求值为: " + expressionDemo.infixExpression(infixExpressionList));
        System.out.println("================================");

        List<String> prefixExpressionList = expressionDemo.infixToPrefix(infixExpressionList);
        System.out.println("中缀表达式: " + infixExpressionList + "  转为 前缀表达式: " + prefixExpressionList);
        System.out.println("前缀表达式求值为: " + expressionDemo.prefixExpression(prefixExpressionList));
        System.out.println("================================");

        List<String> suffixExpressionList = expressionDemo.infixToSuffix(infixExpressionList);
        System.out.println("中缀表达式: " + infixExpressionList + "  转为 后缀表达式: " + suffixExpressionList);
        System.out.println("后缀表达式求值为: " + expressionDemo.suffixExpression(suffixExpressionList));
        System.out.println("================================");
    }

    /**
     * 字符串转换成集合保存,便于操作
     * @param expression 待转换的表达式
     * @return 转换完成的表达式
     */
    public List<String> transfor(String expression) {
        // 用于保存最终结果
        List<String> result = new ArrayList<>();
        // 用于转换多位数
        String temp = "";
        // 遍历字符串,将其 数据取出(可能存在多位数) 挨个存入集合
        for(int i = 0; i < expression.length(); i++) {
            // 遇到多位数,就使用 temp 拼接
            while(i < expression.length() && expression.charAt(i) >= '0' && expression.charAt(i) <= '9') {
                temp += expression.charAt(i);
                i++;
            }
            // 将多位数存放到集合中
            if (temp != "") {
                result.add(temp);
                temp = "";
            }
            // 存放符号(+、-、*、/、括号)
            if (i < expression.length()) {
                result.add(String.valueOf(expression.charAt(i)));
            }
        }
        return result;
    }

    /**
     * 中缀表达式求值(从左到右扫描表达式)
     * @param expression 表达式
     * @return 计算结果
     */
    public String infixExpression(List<String> expression) {
        Stack<String> stackA = new Stack<>(); // 用于存放操作数,简称 A 栈
        Stack<String> stackB = new Stack<>(); // 用于存放运算符,简称 B 栈
        // 遍历集合,取出表达式中 数据 以及 运算符 存入栈中并计算
        expression.forEach(x -> {
            // 如果取出的是数据,直接存放进 A 栈
            if (x.matches("\\d+")) {
                stackA.push(x);
            } else {
                // 如果当前运算符为右括号 ")"
                if (")".equals(x)) {
                    // 依次取出 B 栈顶运算符 以及 A 栈顶两个元素进行计算,计算结果再存入 A 栈,直至遇到左括号 "("
                    while(stackB.size() > 0 && !"(".equals(stackB.peek())) {
                        stackA.push(calculate(stackA.pop(), stackA.pop(), stackB.pop()));
                    }
                    // 移除左括号 "(" 与 当前运算符右括号 ")",即此次比较结束。
                    stackB.pop();
                } else {
                    // 比较运算符优先级,判断当前运算符是直接进入 B 栈,还是先取出优先级高的运算符计算后、再将当前运算符入栈。
                    while(true) {
                        // 如果 当前运算符为左括号 "(" 或者 B 栈为空 或者 B 栈顶元素为 左括号 "(" 或者 当前运算符优先级 高于 B 栈顶元素优先级,则当前运算符直接入栈
                        if ("(".equals(x) || stackB.size() == 0 || "(".equals(stackB.peek()) || priority(x) > priority(stackB.peek())) {
                            stackB.push(x);
                            break;
                        }
                        // 以上条件均不满足,即当前运算符优先级 小于等于 B 栈顶元素优先级
                        // if (priority(x) <= priority(stackB.peek())) {
                        // 依次取出 B 栈顶运算符 以及 A 栈顶两个元素进行计算,计算结果再存入 A 栈
                        stackA.push(calculate(stackA.pop(), stackA.pop(), stackB.pop()));
                        // }
                    }
                }
            }
        });
        // 依次取出 B 栈顶运算符 以及 A 栈顶两个元素进行计算,计算结果再存入 A 栈
        while(stackB.size() > 0) {
            stackA.push(calculate(stackA.pop(), stackA.pop(), stackB.pop()));
        }
        return stackA.pop();
    }

    /**
     * 返回运算符优先级
     * @param operator 运算符
     * @return 优先级(0 ~ n, 0 为最小优先级)
     */
    public int priority(String operator) {
        switch (operator) {
            case "+": return 1;
            case "-": return 1;
            case "*": return 2;
            case "/": return 2;
            default: return 0;
        }
    }

    /**
     * 根据运算符 计算 两数据,并返回计算结果
     * @param num 数据 A
     * @param num2 数据 B
     * @param operator 运算符
     * @return 计算结果
     */
    public String calculate(String num, String num2, String operator) {
        String result = "";
        switch (operator) {
            case "+": result = String.valueOf(Integer.valueOf(num2) + Integer.valueOf(num)); break;
            case "-": result = String.valueOf(Integer.valueOf(num2) - Integer.valueOf(num)); break;
            case "*": result = String.valueOf(Integer.valueOf(num2) * Integer.valueOf(num)); break;
            case "/": result = String.valueOf(Integer.valueOf(num2) / Integer.valueOf(num)); break;
            default: result = ""; break;
        }
        return result;
    }

    /**
     * 前缀表达式求值(从右到左扫描表达式)
     * @param expression 前缀表达式
     * @return 计算结果
     */
    public String prefixExpression(List<String> expression) {
        Stack<String> stackA = new Stack<>(); // 用于存储操作数,简称 A 栈
        // 从右到左扫描表达式
        for (int i = expression.size() - 1; i >= 0; i--) {
            // 用于保存当前表达式数据(操作数 或者 运算符)
            String temp = expression.get(i);
            // 如果当前数据为 操作数,则直接存入 A 栈
            if (temp.matches("\\d+")) {
                stackA.push(temp);
            } else {
                // 若为运算符,则依次弹出 A 栈顶两个数据,并根据运算符进行计算,计算结果重新存入 A 栈
                // 此处顺序要注意,与后缀有区别
                String num2 = stackA.pop();
                String num = stackA.pop();
                stackA.push(calculate(num, num2, temp));
            }
        }
        // 扫描结束后,A 栈最终结果即为 表达式结果
        return stackA.pop();
    }

    /**
     * 中缀表达式转前缀表达式(从右到左扫描表达式)
     * @param expression 中缀表达式
     * @return 前缀表达式
     */
    public List<String> infixToPrefix(List<String> expression) {
        Stack<String> stackA = new Stack<>(); // 用于保存 操作符(运算符),简称 A 栈
        Stack<String> stackB = new Stack<>(); // 用于保存 中间结果(存储数据以及运算符,存储过程中不会有出栈操作),简称 B 栈
        List<String> result = new ArrayList<>(); // 用于记录最终结果
        // 从右到左扫描表达式,取出数据、运算符 并计算
        for (int i = expression.size() - 1; i >= 0; i--) {
            // 用于表示集合当前取出的数据
            String temp = expression.get(i);
            // 如果取出的为 操作数,直接存入 B 栈
            if (temp.matches("\\d+")) {
                stackB.push(temp);
            } else {
                // 如果取出的是左括号
                if ("(".equals(temp)) {
                    // 依次弹出 A 栈顶元素并压入 B 栈,直至遇到 右括号 ")"
                    while(stackA.size() > 0 && !")".equals(stackA.peek())) {
                        stackB.push(stackA.pop());
                    }
                    // 移除 A 栈顶右括号 ")"
                    stackA.pop();
                } else {
                    // 比较运算符优先级,判断运算符直接进入 A 栈 还是 先弹出 A 栈顶元素并压入 B 栈后、再将当前运算符入 A 栈
                    while(true) {
                        // 如果当前运算符为右括号 ")" 或者 A 栈为空 或者 A 栈顶元素为右括号 ")" 或者 当前运算符优先级 高于 A 栈顶运算符,则直接入 A 栈
                        if (")".equals(temp) || stackA.size() == 0 || ")".equals(stackA.peek()) || priority(temp) > priority(stackA.peek())) {
                            stackA.push(temp);
                            break;
                        }
                        // 若上面条件均不满足,即当前运算符优先级小于等于 A 栈顶运算符,则弹出 A 栈顶运算符并压入 B 栈
                        stackB.push(stackA.pop());
                    }
                }
            }
        }
        // 依次将 A 栈剩余元素弹出并压入到 B 栈
        while(stackA.size() > 0) {
            stackB.push(stackA.pop());
        }
        // 依次取出 B 栈元素,即为 前缀表达式
        while(stackB.size() > 0) {
            result.add(stackB.pop());
        }
        return result;
    }

    /**
     * 中缀表达式转后缀表达式(从左到右扫描表达式)
     * @param expression 中缀表达式
     * @return 后缀表达式
     */
    public List<String> infixToSuffix(List<String> expression) {
        Stack<String> stackA = new Stack<>(); // 用于保存 操作符(运算符),简称 A 栈
        // Stack<String> stackB = new Stack<>(); // 用于保存 中间结果,简称 B 栈
        // 由于 B 栈反序输出才是后缀表达式,此处可以直接存放在 集合中,顺序读取即为 后缀表达式。
        List<String> result = new ArrayList<>(); // 用于保存 最终结果,此处用来替代 B 栈,后面简称 B 栈。
        // 从左到右扫描后缀表达式
        expression.forEach(x -> {
            // 如果取出的是 操作数,直接存入 B 栈
            if (x.matches("\\d+")) {
                result.add(x);
            } else {
                // 如果操作符是右括号 ")"
                if (")".equals(x)) {
                    // 依次将 A 栈顶运算符弹出 并压入 B 栈,直至遇到左括号 "("
                    while(stackA.size() > 0 && !"(".equals(stackA.peek())) {
                        result.add(stackA.pop());
                    }
                    // 移除 A 栈顶左括号 "("
                    stackA.pop();
                } else {
                    // 比较运算符优先级,判断运算符直接进入 A 栈 还是 先弹出 A 栈顶元素并压入 B 栈后、再将当前运算符入 A 栈
                    while(true) {
                        // 如果当前运算符为左括号 "(" 或者 A 栈为空 或者 A 栈顶运算符为左括号 "(" 或者 当前运算符优先级 高于 A 栈顶运算符,则直接入 A 栈
                        if ("(".equals(x) || stackA.size() == 0 || "(".equals(stackA.peek()) || priority(x) > priority(stackA.peek())) {
                            stackA.push(x);
                            break;
                        }
                        // 如果上面条件均不满足,即当前运算符 优先级 小于或等于 A 栈顶运算符
                        // 则将 A 栈顶运算符取出并 放入 B 栈
                        result.add(stackA.pop());
                    }
                }
            }
        });
        // 依次将 A 栈顶运算符取出放入 B 栈
        while(stackA.size() > 0) {
            result.add(stackA.pop());
        }
        return result;
    }

    /**
     * 后缀表达式求值(从左到右扫描表达式)
     * @param expression 后缀表达式
     * @return 计算结果
     */
    public String suffixExpression(List<String> expression) {
        Stack<String> stackA = new Stack<>(); // 用于保存 操作数,简称 A 栈
        // 从左到右扫描表达式
        expression.forEach(x -> {
            // 如果是 数字,直接进 A 栈
            if (x.matches("\\d+")) {
                stackA.push(x);
            } else {
               // 是运算符,则取出 A 栈顶两元素,并计算,将计算结果重新压入 A 栈
               stackA.push(calculate(stackA.pop(), stackA.pop(), x));
            }
        });
        // 扫描结束后,A 栈最终结果即为 表达式结果
        return stackA.pop();
    }
}

【输出结果:】
当前表达式为: (13-6)*5-6
================================
表达式转换后为中缀表达式: [(, 13, -, 6, ), *, 5, -, 6]
================================
中缀表达式求值为: 29
================================
中缀表达式: [(, 13, -, 6, ), *, 5, -, 6]  转为 前缀表达式: [-, *, -, 13, 6, 5, 6]
前缀表达式求值为: 29
================================
中缀表达式: [(, 13, -, 6, ), *, 5, -, 6]  转为 后缀表达式: [13, 6, -, 5, *, 6, -]
后缀表达式求值为: 29
================================

7、递归与回溯、八皇后问题

(1)递归:
  递归指的是 方法调用自身方法去解决问题的过程。
  其目的是 将一个复杂的大问题 转换为 与原问题类似的小问题去求解。递归必须得有结束条件,否则将会陷入无限递归(导致栈溢出异常)。
  常用场景:快排、归并排序、二分查找、汉诺塔、八皇后 等问题。

(2)回溯:
  回溯指的是 类似枚举的选优搜索过程,当条件不符合时,返回上一层(即回溯)重新判断。
  其解决的是 某种场景下有许多个解,依次判断每个解是否合适,如果不合适就回退到上一层,重新判断下一个解是否合适。
  常见场景:八皇后 问题。

(3)八皇后问题分析

【八皇后问题介绍:】
    在一个 8 * 8 的国际象棋棋盘上,摆放 八个皇后,且皇后之间不能相互攻击,总共有多少种摆法。
   不能相互攻击 即: 任意两个皇后 不能同时 处在 同一行、同一列、同一斜线上。
    
【思路分析:】
采用 回溯 方法解决。
  每次放置皇后时,均从每行的第一列开始尝试,并校验该皇后位置是否与其他皇后位置发生冲突,如果不冲突则递归调用下一个皇后进行放置,
  如果冲突则尝试当前皇后位置的下一个位置是否能够放置,若当前皇后在当前行的所有列均放置失败,则回溯到上一个皇后所处位置,使上一个皇后放置在其下一列 并重新判断该位置是否冲突。

即:
    Step1:第一个皇后放在第一行第一列。
    Step2:第二个皇后放在第二行第一列,判断是否会攻击,如果会攻击,则将 第二个皇后放在第二行第二列 进行判断。
        若仍会攻击,则依次放置下去,直至第二行第八列。若仍会攻击,则后续不用执行(此时第二个皇后 8 个位置均放置失败),回溯到 上一行 并再次枚举。
    Step3:第二个皇后放好后,同理放置第三个皇后 直至 放置第八个 皇后,若均不冲突则 为一个解。

【判断皇后之间是否攻击:】
使用一维数组 a[8] 存储可行的 八皇后 放置位置(二维数组亦可)。
每一个数组元素存储范围为 0~7,分别表示第 1 ~ 8 位置。

判断皇后之间是否攻击:设当前为第 n 个皇后,记为 a[n]。
    同一行:不需要考虑,每次都是不同行。
    同一列:遍历一维数组,如果 a[i] == a[n],则表示当前存在攻击。
        for(int i = 0; i < n; i++) {
            if (a[i] == a[n]) {
                return false;
            }
        }

    同一斜线:遍历一维数组,若 Math.abs(n - i) == Math.abs(a[n] - a[i]),则存在攻击。
        for(int i = 0; i < n; i++) {
            if (Math.abs(n - i) == Math.abs(a[n] - a[i])) {
                return false;
            }
        }

注:
    i 指的是第 i+1 个皇后,a[i] 指的是第 i+1 个皇后所占据的位置(0~7)。
    所以 a[i] == a[n] 时表示同一列。
    Math.abs(n - i) == Math.abs(a[n] - a[i]) 表示同一斜线(看成等腰直角三角形)。

【八皇后代码实现:】
package com.lyh.recursion;

import java.util.Arrays;

public class EightQueens {

    private int maxsize = 8; // 定义最大为 8 皇后
    private int count = 0; // 用于记录皇后放置总解法数
    private int[] arrays = new int[maxsize]; // 用于存储 8 皇后的解法,范围为 0 ~ 7,表示第 1 ~ 8 位置

    public EightQueens() {
    }

    public EightQueens(int maxsize) {
        this.maxsize = maxsize;
        arrays = new int[this.maxsize];
    }

    public static void main(String[] args) {
        EightQueens eightQueens = new EightQueens();
        eightQueens.putQueen(0);
        System.out.println("总解法: " + eightQueens.count);
    }

    /**
     * 检查当前皇后的放置位置 是否 与其他皇后位置冲突
     * @param n 当前为第 n+1 皇后
     * @return true 表示不冲突
     */
    public boolean check(int n) {
        // 遍历当前所有皇后,已放置 0 ~ n-1 个皇后,即 第 1 ~ n 皇后位置
        for(int i = 0; i < n; i++) {
            // arrays[i] == arrays[n] 表示两皇后在同一列
            // Math.abs(n - i) == Math.abs(arrays[n] - arrays[i]) 表示两皇后在同一斜线上(看成等腰直角三角形处理)
            if (arrays[i] == arrays[n] || Math.abs(n - i) == Math.abs(arrays[n] - arrays[i])) {
                return false;
            }
        }
        return true;
    }

    /**
     * 递归 + 回溯 放置皇后
     * @param n 第 n+1 个皇后
     */
    public void putQueen(int n) {
        // 所有皇后放置完成,打印皇后放置方法
        // 此处为第一个出口,即 8 个皇后全部放置完成时。
        if (n == maxsize) {
            System.out.println(Arrays.toString(arrays));
            count++;
            return;
        }
        
        // 枚举依次求解,遍历 0 ~ maxsize - 1,表示当前皇后放置在第 1 ~ maxsize 个位置。
        // 此处为第二个出口,若遍历完成,n 仍不为 8,即 第 n-1 个皇后 8 个位置均放置失败,后续无需再做,回溯到上一个皇后放置位置的下一个位置
        for (int i = 0; i < maxsize; i++) {
            // 放置皇后
            arrays[n] = i;
            // 当前皇后放置不冲突,则放置下一个皇后,若冲突则结束当前循环并判断下一个位置是否冲突
            if (check(n)) {
                putQueen(n + 1);
            }
        }
    }
}

【输出结果:】
[0, 4, 7, 5, 2, 6, 1, 3]
[0, 5, 7, 2, 6, 3, 1, 4]
[0, 6, 3, 5, 7, 1, 4, 2]
[0, 6, 4, 7, 1, 3, 5, 2]
[1, 3, 5, 7, 2, 0, 6, 4]
[1, 4, 6, 0, 2, 7, 5, 3]
[1, 4, 6, 3, 0, 7, 5, 2]
[1, 5, 0, 6, 3, 7, 2, 4]
[1, 5, 7, 2, 0, 3, 6, 4]
[1, 6, 2, 5, 7, 4, 0, 3]
[1, 6, 4, 7, 0, 3, 5, 2]
[1, 7, 5, 0, 2, 4, 6, 3]
[2, 0, 6, 4, 7, 1, 3, 5]
[2, 4, 1, 7, 0, 6, 3, 5]
[2, 4, 1, 7, 5, 3, 6, 0]
[2, 4, 6, 0, 3, 1, 7, 5]
[2, 4, 7, 3, 0, 6, 1, 5]
[2, 5, 1, 4, 7, 0, 6, 3]
[2, 5, 1, 6, 0, 3, 7, 4]
[2, 5, 1, 6, 4, 0, 7, 3]
[2, 5, 3, 0, 7, 4, 6, 1]
[2, 5, 3, 1, 7, 4, 6, 0]
[2, 5, 7, 0, 3, 6, 4, 1]
[2, 5, 7, 0, 4, 6, 1, 3]
[2, 5, 7, 1, 3, 0, 6, 4]
[2, 6, 1, 7, 4, 0, 3, 5]
[2, 6, 1, 7, 5, 3, 0, 4]
[2, 7, 3, 6, 0, 5, 1, 4]
[3, 0, 4, 7, 1, 6, 2, 5]
[3, 0, 4, 7, 5, 2, 6, 1]
[3, 1, 4, 7, 5, 0, 2, 6]
[3, 1, 6, 2, 5, 7, 0, 4]
[3, 1, 6, 2, 5, 7, 4, 0]
[3, 1, 6, 4, 0, 7, 5, 2]
[3, 1, 7, 4, 6, 0, 2, 5]
[3, 1, 7, 5, 0, 2, 4, 6]
[3, 5, 0, 4, 1, 7, 2, 6]
[3, 5, 7, 1, 6, 0, 2, 4]
[3, 5, 7, 2, 0, 6, 4, 1]
[3, 6, 0, 7, 4, 1, 5, 2]
[3, 6, 2, 7, 1, 4, 0, 5]
[3, 6, 4, 1, 5, 0, 2, 7]
[3, 6, 4, 2, 0, 5, 7, 1]
[3, 7, 0, 2, 5, 1, 6, 4]
[3, 7, 0, 4, 6, 1, 5, 2]
[3, 7, 4, 2, 0, 6, 1, 5]
[4, 0, 3, 5, 7, 1, 6, 2]
[4, 0, 7, 3, 1, 6, 2, 5]
[4, 0, 7, 5, 2, 6, 1, 3]
[4, 1, 3, 5, 7, 2, 0, 6]
[4, 1, 3, 6, 2, 7, 5, 0]
[4, 1, 5, 0, 6, 3, 7, 2]
[4, 1, 7, 0, 3, 6, 2, 5]
[4, 2, 0, 5, 7, 1, 3, 6]
[4, 2, 0, 6, 1, 7, 5, 3]
[4, 2, 7, 3, 6, 0, 5, 1]
[4, 6, 0, 2, 7, 5, 3, 1]
[4, 6, 0, 3, 1, 7, 5, 2]
[4, 6, 1, 3, 7, 0, 2, 5]
[4, 6, 1, 5, 2, 0, 3, 7]
[4, 6, 1, 5, 2, 0, 7, 3]
[4, 6, 3, 0, 2, 7, 5, 1]
[4, 7, 3, 0, 2, 5, 1, 6]
[4, 7, 3, 0, 6, 1, 5, 2]
[5, 0, 4, 1, 7, 2, 6, 3]
[5, 1, 6, 0, 2, 4, 7, 3]
[5, 1, 6, 0, 3, 7, 4, 2]
[5, 2, 0, 6, 4, 7, 1, 3]
[5, 2, 0, 7, 3, 1, 6, 4]
[5, 2, 0, 7, 4, 1, 3, 6]
[5, 2, 4, 6, 0, 3, 1, 7]
[5, 2, 4, 7, 0, 3, 1, 6]
[5, 2, 6, 1, 3, 7, 0, 4]
[5, 2, 6, 1, 7, 4, 0, 3]
[5, 2, 6, 3, 0, 7, 1, 4]
[5, 3, 0, 4, 7, 1, 6, 2]
[5, 3, 1, 7, 4, 6, 0, 2]
[5, 3, 6, 0, 2, 4, 1, 7]
[5, 3, 6, 0, 7, 1, 4, 2]
[5, 7, 1, 3, 0, 6, 4, 2]
[6, 0, 2, 7, 5, 3, 1, 4]
[6, 1, 3, 0, 7, 4, 2, 5]
[6, 1, 5, 2, 0, 3, 7, 4]
[6, 2, 0, 5, 7, 4, 1, 3]
[6, 2, 7, 1, 4, 0, 5, 3]
[6, 3, 1, 4, 7, 0, 2, 5]
[6, 3, 1, 7, 5, 0, 2, 4]
[6, 4, 2, 0, 5, 7, 1, 3]
[7, 1, 3, 0, 6, 4, 2, 5]
[7, 1, 4, 2, 0, 6, 3, 5]
[7, 2, 0, 5, 1, 4, 6, 3]
[7, 3, 0, 2, 5, 1, 6, 4]
总解法: 92

 

猜你喜欢

转载自blog.csdn.net/zx309519477/article/details/108874459