转载

Java数据结构和算法


Java数据结构和算法

数据结构包括:线性结构和非线性结构。

线性结构非线性结构
特点数据元素存在一对一的线性关系数据元素存在复杂的非线性关系
例子数组,队列,链表,栈多维数组,广义表,树结构,图结构

线性结构的存储结构可分为两类:顺序存储结构(内存中存储的元素是连续的):数组;链式存储结构(内存中存储的元素不一定是连续的,有指针指向下个元素):链表。

稀疏数组

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

稀疏数组的处理方法

  • 记录数组一共有几行几列,有多少个不同的值
  • 把具有不同值的元素的行列及值记录在一个小规模的数组(稀疏数组)中,从而缩小存储的规模。
原始数组:						转换成					稀疏数组:
0	0	0	22	0	0	15							行(row)		列(col)		值(value)
0	11	0	0	0	17	0						[0]		6			7			8
0	0	0	-6	0	0	0						[1]		0			3			22
0	0	0	0	0	39	0		===>>>			[2]		0			6			15
91	0	0	0	0	0	0						[3]		1			1			11
0	0	28	0	0	0	0						[4]		1			5			17
												[5]		2			3			-6
												[6]		3			5			39
												[7]		4			0			91
												[8]		5			2			28

二维数组转换成稀疏数组的思路

  1. 遍历二维数组,计算出有效的数据个数 sum;可在此时存储有效的数据信息。

  2. 创建稀疏数组

    int[] sparseArray = new int[sum + 1][3];
    
  3. 将二维数组中的有效数据存储到稀疏数组中(对应稀疏数组的第一个元素的值是有效数据的所在原始数组的行数,第二个元素的值是有效数据的所在原始数组的列数,第三个元素的值是有效数据的值)。

稀疏数组转换成二维数组(原始数组)思路

  1. 读取稀疏数组的第一行数据,并创建二维数组。

    int[] originalArray = new int[sparseArray[0][0]][sparseArray[0][1]]
    
  2. 遍历稀疏数组的其他元素,将数据赋值给二维数组。

代码实现

public class SparseArray {
    public static void main(String[] args) {
        int[][] chessArray = new int[11][11];
        chessArray[1][2] = 1;
        chessArray[2][3] = 2;
        chessArray[10][10] = 88;

        System.out.println("原始的二维棋盘数组:");
        printArray(chessArray);

        // 将二维数组转换成稀疏数组
        int sum = 0; // 记录有效数据的个数
        for (int i = 0; i < chessArray.length; i++) {
            for (int j = 0; j < chessArray[i].length; j++) {
                if (chessArray[i][j] != 0) {
                    sum++;
                }
            }
        }

        // 创建稀疏数组
        // 总共有 有效数据个数 + 1 行,列固定 3 列
        // 一行中,下标为0 记录有效数据的行,
        // 下标为1 记录有效数据的列,下标为2 记录有效数据的值
        int[][] sparseArray = new int[sum + 1][3];

        // 稀疏数组的第一行数据,存放原始数组有多少行,多少列,有效数据的个数
        sparseArray[0][0] = 11;
        sparseArray[0][1] = 11;
        sparseArray[0][2] = sum;

        int count = 1;
        for (int i = 0; i < chessArray.length; i++) {
            for (int j = 0; j < chessArray[i].length; j++) {
                if (chessArray[i][j] != 0) {
                    sparseArray[count][0] = i;
                    sparseArray[count][1] = j;
                    sparseArray[count][2] = chessArray[i][j];
                    count++;
                }
            }
        }

        System.out.println("转换后的稀疏数组为:");
        printArray(sparseArray);

        System.out.println("将稀疏数组转换成新的原始数组=============");

        int[][] newArray = new int[sparseArray[0][0]][sparseArray[0][1]];

        for (int i = 1; i < sparseArray.length; i++) {
            newArray[sparseArray[i][0]][sparseArray[i][1]] = sparseArray[i][2];
        }

        System.out.println("将稀疏数组转换成新的二维数组:");
        printArray(newArray);
    }

    private static void printArray(int[][] array) {
        for (int[] row : array) {
            for (int i : row) {
                System.out.printf("%d\t", i);
            }
            System.out.println();
        }
    }
}

队列

队列是个有序列表,可用数组或链表来实现,队列遵循先入先出的原则。

数组模拟队列

思路

队列的长度等于数组的长度 - 1;队列的输出,输入分别是从前后端来处理的,因此需要两个变量front和rear分别记录队列的前后端的下标,front会随着数据输出而改变,rear则是随着数据输入而改变

入队:rear指针先后移动,rear+1,当front == rear时队列为空;当rear不大于队列的长度(数组的长度 - 1)时,将数据存放rear(下标)对应的数组元素中,当 rear == array.length - 1时,队列满,不能入队。

注意:rear 指向队列最后的元素(包含),front指向队列最前面的元素(不包含)。

代码实现

/
 * 使用数组模拟队列
 *
 * @author lzlg
 */
public class ArrayQueue {

    private int maxSize; // 数组的最大容量,数据能存放的数量

    private int front; // 队列头,指向队列头的前一个位置

    private int rear; // 队列尾,指向队列尾部的数据(队列最后一个数据)

    private int[] array; // 队列的数据存放

    /
     * 构造方法
     */
    public ArrayQueue(int maxSize) {
        this.maxSize = maxSize;
        array = new int[maxSize];
        front = -1;
        rear = -1;
    }

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

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

    /
     * 入队操作
     */
    public void addQueue(int n) {
        if (isFull()) {
            System.out.println("队列已满,不能入队。。。");
            return;
        }
        rear++;
        array[rear] = n;
    }

    /
     * 出队操作
     */
    public int getQueue() {
        if (isEmpty()) {
            throw new RuntimeException("队列为空,不能出队。。。");
        }
        front++;
        return array[front];
    }

    /
     * 展示队列中的数据
     */
    public void showQueue() {
        for (int i = front + 1; i <= rear; i++) {
            System.out.printf("array[%d] = %d", i, array[i]);
        }
    }

    /
     * 看队头元素
     */
    public int head() {
        if (isEmpty()) {
            throw new RuntimeException("队列为空,不能出队。。。");
        }
        return array[front + 1];
    }
}

测试代码

import java.util.Scanner;

public class Test {
    public static void main(String[] args) {
        Scanner input = new Scanner(System.in);
        ArrayQueue queue = new ArrayQueue(3);
        boolean loop = true;
        while (loop) {
            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):查看队列头的数据");
            String key = input.next();
            switch (key) {
                case "s":
                    queue.showQueue();
                    break;
                case "e":
                    loop = false;
                    input.close();
                    break;
                case "a":
                    System.out.print("请输入一个数:");
                    int n = input.nextInt();
                    queue.addQueue(n);
                    break;
                case "g":
                    try {
                        int r = queue.getQueue();
                        System.out.printf("从队列中取出的数据是%d\n", r);
                    } catch (Exception e) {
                        System.out.println(e.getMessage());
                    }
                    break;
                case "h":
                    try {
                        int h = queue.head();
                        System.out.printf("队列头的数据是%d\n", h);
                    } catch (Exception e) {
                        System.out.println(e.getMessage());
                    }
                    break;
            }
        }
        System.out.println("程序退出~~");
    }
}

存在的问题

队列不能重复使用:当填满队列,然后将所有元素出队后,再次添加,会显示队列为满。

数据模拟环形队列

思路

  1. front含义变化:front指向队列的第一个元素,front的初始值为0;
  2. rear含义变化:rear指向队列的最后一个元素的后一个位置,留一个位置作为约定,rear初始值为0;
  3. 当队列满时,此时 (rear + 1) % maxSize == front; 此时是环形,必须用取模运算;
  4. 当队列为空时,此时 front == rear;
  5. 队列中有效数据的个数为 (rear + maxSize - front) % maxSize;

代码实现

/
 * 使用数组模拟环形队列
 *
 * @author lzlg
 */
public class CircleArrayQueue {

    private int maxSize; // 数组的最大容量,数据能存放的数量

    private int front; // 队列头,指向队列头的第一个元素

    private int rear; // 队列尾,指向队列尾部的后一个位置

    private int[] array; // 队列的数据存放

    /
     * 构造方法
     */
    public CircleArrayQueue(int maxSize) {
        this.maxSize = maxSize;
        array = new int[maxSize];
    }

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

    /
     * 判断队列是否已满
     */
    public boolean isFull() {
        // 注意此时会预留一个位置用来作为约定
        return (rear + 1) % maxSize == front;
    }

    /
     * 入队操作
     */
    public void addQueue(int n) {
        if (isFull()) {
            System.out.println("队列已满,不能入队。。。");
            return;
        }
        // 因为rear指向最后数组元素的最后一个位置,所以可直接赋值新元素
        array[rear] = n;
        // rear后移,必须考虑取模运算
        rear = (rear + 1) % maxSize;
    }

    /
     * 出队操作
     */
    public int getQueue() {
        if (isEmpty()) {
            throw new RuntimeException("队列为空,不能出队。。。");
        }
        // 此时front指向队列头一个元素,取出赋值给临时变量v
        int v = array[front];
        // front后移,需考虑取模
        front = (front + 1) % maxSize;
        return v;
    }

    /
     * 展示队列中的数据
     */
    public void showQueue() {
        if (isEmpty()) {
            System.out.println("队列为空,没有数据~~~");
            return;
        }
        // 计算出队列中有效数据的个数
        int size = size();
        for (int i = front; i < front + size; i++) {
            // 注意这里必须进行取模,因为是环形队列
            // front有可能指向数组的最后(物理存储),但却是队列的开头(逻辑状态)
            int pos = i % maxSize;
            System.out.printf("array[%d] = %d\n", pos, array[pos]);
        }
    }

    /
     * 队列的有效数据个数
     */
    public int size() {
        // 注意此时是环形队列,必须考虑取模
        return (rear + maxSize - front) % maxSize;
    }

    /
     * 看队头元素
     */
    public int head() {
        if (isEmpty()) {
            throw new RuntimeException("队列为空,没有数据。。。");
        }
        // front指向队列头部第一个元素的位置,直接返回
        return array[front];
    }
}

测试代码

// 将上面 数组模拟队列 的ArrayQueue替换
CircleArrayQueue queue = new CircleArrayQueue(4);

链表

单链表是以节点(Node)的方式存储的,每个节点包含data域(存放数据)和next域(指向下一个节点),在内存中的存储不是连续的。链表有带头节点的链表,也有没有代头节点的链表。

单链表(带头节点)的逻辑结构:head --> a1 --> a2 --> a3 ...... --> aN --> null

准备

/
 * 单链表
 *
 * @author lzlg
 */
public class SingleLinkedList {
    // 单链表头节点
    private HeroNode head = new HeroNode(0, null, null);
}

/
 * 英雄节点
 */
class HeroNode {

    int no;             // 排名

    String name;        // 名称

    String nickname;    // 外号

    HeroNode next;      // 指向下个节点

    public HeroNode(int no, String name, String nickname) {
        this.no = no;
        this.name = name;
        this.nickname = nickname;
    }

    @Override
    public String toString() {
        return "HeroNode{" +
                "no=" + no +
                ", name='" + name + '\'' +
                ", nickname='" + nickname + '\'' +
                '}';
    }
}

单链表的添加(创建)

思路

  1. 先创建一个head头节点(无具体数据),作用是表示链表的头。
  2. 每添加一个节点,直接加入到链表的最后。

代码实现

/
 * 添加节点到链表尾部
 */
public void add(HeroNode node) {
    // 遍历链表到最后一个元素,将最后一个元素的next节点指向新节点
    // 使用辅助指针遍历
    HeroNode temp = head;
    while (true) {
        // 如果链表的下个节点无数据,则证明在链表的最后
        if (temp.next == null) {
            break;
        }
        temp = temp.next;
    }
    // 将链表最后一个元素的next节点指向新节点
    temp.next = node;
}

单链表的遍历

使用一个辅助指针,用来遍历单链表。

/
 * 遍历链表,打印链表数据
 */
public void list() {
    // 判断链表是否为空,如果head节点的next域为空,则链表为空
    if (head.next == null) {
        System.out.println("链表为空,没有数据~~~~");
        return;
    }
    HeroNode temp = head.next;
    while (true) {
        if (temp == null) {
            break;
        }
        System.out.println(temp);
        temp = temp.next;
    }
}

排序单链表的添加(创建)

将英雄节点按照no的顺序添加

思路

  1. 找到要插入的节点位置;如果链表为空,则直接添加到链表的尾部;如果链表中有元素,则遍历到当前节点的下个节点的no大于新节点 或者 遍历到链表的最后,此时当前节点就是temp。
  2. 将新节点的next指向temp(辅助指针)的next,newNode.next = temp.next;
  3. 将temp的next指向新节点,temp.next = newNode;

代码实现

/
 * 按照节点的no顺序添加到链表中
 */
public void orderAdd(HeroNode node) {
    // 遍历使用的辅助指针
    HeroNode temp = head;
    boolean flag = false; // 标识是否有重复的no加入
    while (true) {
        if (temp.next == null) { // 说明已经到链表的尾部
            break;
        }
        if (temp.next.no > node.no) {
            // 如果节点的下个节点的no大于新节点的no,则证明找到了添加的位置
            break;
        } else if (temp.next.no == node.no) {
            // 节点的下个节点的no等于新节点的no,证明已经有重复的no
            flag = true;
            break;
        }
        temp = temp.next;
    }
    if (flag) {
        System.out.printf("编号为 %d 的节点已经存在,不能重复添加~~~\n", node.no);
    } else {
        // 将新节点的next指针指向temp的下个节点
        node.next = temp.next;
        // 将temp节点的next指针指向新节点
        temp.next = node;
    }
}

单链表的修改

根据no编号找到对应的节点,然后修改相应节点的信息,注意no不能修改。

/
 * 根据新节点的no,修改节点
 */
public void update(HeroNode newNode) {
    if (head.next == null) {
        System.out.println("链表为空,不能修改~~~");
        return;
    }
    HeroNode temp = head.next;
    boolean flag = false; // 标识是否已经找到要修改的节点
    while (true) {
        if (temp == null) {
            break;// 此时已经遍历完链表
        }
        if (temp.no == newNode.no) { // 找到要修改的节点
            flag = true;
            break;
        }
        temp = temp.next;
    }
    if (flag) { // 修改节点信息
        temp.name = newNode.name;
        temp.nickname = newNode.nickname;
    } else {
        System.out.printf("没有找到编号为 %d 的节点,不能修改~~~\n", newNode.no);
    }
}

单链表的删除

根据编号no删除节点

思路

  1. 根据编号找到待删除的节点的上一个节点位置,记为temp。
  2. 将temp节点的next指向temp节点的下一个节点的next;temp.next = temp.next.next;

代码实现

/
 * 根据no删除节点
 */
public void delete(int no) {
    HeroNode temp = head;
    boolean flag = false; // 标识是否找到要删除的节点
    while (true) {
        if (temp.next == null) {
            break; // 遍历到链表的尾部
        }
        if (temp.next.no == no) {
            flag = true; // 找到要删除的节点的上一个节点
            break;
        }
        temp = temp.next;
    }
    if (flag) {
        temp.next = temp.next.next;
    } else {
        System.out.printf("没有找到编号为 %d 的节点,不能删除~~~\n", no);
    }
}

面试题

反转链表

思路:遍历一个节点cur后,首先保存cur节点的下个节点到temp中;使用空节点pre,将cur的next节点指向pre节点,再将cur节点赋值给pre节点,完成交换。

private static HeroNode reverse(HeroNode head) {
    HeroNode pre = null;
    HeroNode oldTemp = head; // 使用辅助指针进行原头节点的遍历
    while (oldTemp != null) {
        HeroNode newTemp = oldTemp.next; // 注意必须先保存下个节点信息
        oldTemp.next = pre;
        pre = oldTemp;
        oldTemp = newTemp;
    }
    return pre;
}

合并两个有序的链表

思路:创建一个新的头节点head,将head赋值给辅助指针help;遍历两个链表l1,l2;比较两个链表data域的值,如果l1.data > l2.data,则把help的next节点赋值为l2,并将l2移动一位,l2 = l2.next;反之,则把help的next节点赋值为l1,并将l1移动一位,l1 = l1.next;然后将help指针移动一位,help = help.next;最后判断哪个链表先空,并把help的next域指向非空的那个链表。

/
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode(int x) { val = x; }
 * }
 */
public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
    ListNode preHead = new ListNode(-1);
    ListNode pre = preHead;
    while (l1 != null && l2 != null) {
        if (l1.val > l2.val) {
            pre.next = l2;
            l2 = l2.next;
        } else {
            pre.next = l1;
            l1 = l1.next;
        }
        pre = pre.next;
    }
    pre.next = (l1 == null) ? l2 : l1;
    return preHead.next;
}

双向链表的增删改查

双向链表的节点有一个pre(指向前一个节点)指针,next(指向后一个节点)指针。在添加时,和单链表的添加一样,但需要把新节点的pre指向链表的最后一个元素。在删除时,节点可以自我删除,因此遍历到要删除的节点node上,把node的上个节点的next指向node的下个节点,把node的下个节点的pre指向node的上个节点。删除时要注意,如果是删除最后一个节点的话,则需要判断要删除节点的next节点是否为空。

/
 * 双向链表
 *
 * @author lzlg
 */
public class DoubleLinkedList {
    // 头节点
    private TwoNode head = new TwoNode(0, null);

    /
     * 向双向链表中添加节点
     */
    public void add(TwoNode node) {
        TwoNode temp = head;
        while (temp.next != null) {
            temp = temp.next;
        }
        temp.next = node;
        node.pre = temp;
    }

    /
     * 按照no的顺序添加节点
     */
    public void orderAdd(TwoNode node) {
        TwoNode temp = head;
        while (temp.next != null && temp.next.no < node.no) {
            temp = temp.next;
        }
        node.next = temp.next;
        if (temp.next != null) {
            temp.next.pre = node;
        }
        temp.next = node;
        node.pre = temp;
    }

    /
     * 修改双向链表中的节点信息
     */
    public void update(TwoNode node) {
        TwoNode temp = head.next;
        boolean flag = false; // 标识是否找到要修改的节点
        while (true) {
            if (temp == null) {
                break;
            }
            if (temp.no == node.no) {
                flag = true;
                break;
            }
            temp = temp.next;
        }
        if (flag) {
            temp.name = node.name;
        } else {
            System.out.printf("没有找到编号为 %d 的节点,不能修改~~~\n", node.no);
        }

    }

    /
     * 删除no对应的节点
     */
    public void delete(int no) {
        if (head.next == null) {
            System.out.println("链表为空,不能删除~~~");
            return;
        }
        TwoNode temp = head.next;
        boolean flag = false; // 标识是否找到要删除的节点
        while (true) {
            if (temp == null) {
                break;
            }
            if (temp.no == no) {
                flag = true;
                break;
            }
            temp = temp.next;
        }
        if (flag) {
            temp.pre.next = temp.next;
            if (temp.next != null) {
                temp.next.pre = temp.pre;
            }
        } else {
            System.out.printf("没有找到编号为 %d 的节点,不能删除~~~\n", no);
        }
    }

    /
     * 展示所有节点信息
     */
    public void list() {
        if (head.next == null) {
            System.out.println("链表为空,不能遍历~~~");
            return;
        }
        TwoNode temp = head.next;
        while (temp != null) {
            System.out.println(temp);
            temp = temp.next;
        }
    }
}

class TwoNode {

    int no;

    String name;

    TwoNode pre; // 指向前一个节点

    TwoNode next; // 指向下一个节点

    public TwoNode(int no, String name) {
        this.no = no;
        this.name = name;
    }

    @Override
    public String toString() {
        return "TwoNode{" +
                "no=" + no +
                ", name='" + name + '\'' +
                '}';
    }
}

单向环形链表

应用场景:约瑟夫问题。设编号为1,2,... n的几个人围坐一圈,约定编号为k(1<=k<=n)的人从1开始报数,数到m的那个人出列,m的下一位从1开始数,数到m的那个人又出列,依次类推,直到所有人出列位置,由此产生一个出队编号的序列。

链表的构建和遍历

链表有一个指向首节点的first指针,当添加第一个节点时,把该节点赋值给first,并让first的next指向first,形成环状,并让辅助指针cur指向first;当添加第二个节点时,把辅助指针cur的next指向新节点,并把新节点的next指向first,然后移动辅助指针到新节点上。遍历时,当辅助指针的next为first时,证明已经遍历完成。

/
 * 单向环形链表
 *
 * @author lzlg
 */
public class SingleCircleLinkedList {
    // 首节点
    private BoyNode first;

    /
     * 添加数量为nums的节点
     */
    public void add(int nums) {
        if (nums < 1) {
            System.out.printf("输入的 %d 不合理,无法构建环形链表", nums);
        }
        BoyNode cur = null;
        for (int i = 1; i <= nums; i++) {
            BoyNode boy = new BoyNode(i);
            if (i == 1) {
                first = boy;
                first.next = first;
                cur = first;
            } else {
                cur.next = boy;
                cur = cur.next;
                cur.next = first;
            }
        }
    }

    /
     * 遍历环形链表
     */
    public void show() {
        if (first == null) {
            System.out.println("单向环形链表为空,不能遍历~~~");
            return;
        }
        BoyNode cur = first;
        while (true) {
            System.out.println(cur);
            if (cur.next == first) {
                break;
            }
            cur = cur.next;
        }
    }
}

class BoyNode {

    int no;

    BoyNode next;

    public BoyNode(int no) {
        this.no = no;
    }

    @Override
    public String toString() {
        return "BoyNode{" +
                "no=" + no +
                '}';
    }
}

约瑟夫问题分析

  1. 使用辅助节点help,help是first节点的上一个节点,即指向环形链表的最后节点。
  2. 先移动到报数的人的节点上,即help和first指针移动 k - 1 次,first指向要报数的人的节点,help指向该节点的前一个节点。
  3. 开始出圈,help和first指针移动 m - 1 次(报数从1开始),first指向要出圈的人。此时将first移动到要出圈的人的节点的下个节点,将help指针的next指向移动好的first节点。
  4. 什么时候截至呢?当help和first指针相等的时候,此时只剩一个人(节点)。
/
 * 出圈操作
 *
 * @param startNo 从第几个人开始数数
 * @param count   报数的最大值
 */
public void run(int startNo, int count) {
    if (first == null || startNo < 1 || count > this.nums) {
        System.out.println("输入的数据不合理,不能执行操作~~~");
        return;
    }
    // 1.找到first的前一个指针help
    BoyNode help = first;
    while (help.next != first) {
        help = help.next;
    }
    // 2.first 和 help指针移动到要报数的人的节点上
    for (int i = 0; i < startNo - 1; i++) {
        help = help.next;
        first = first.next;
    }

    while (help != first) {
        // 移动到第一个报数的人
        for (int i = 0; i < count - 1; i++) {
            help = help.next;
            first = first.next;
        }
        System.out.printf("要出圈的人的编号为:%d\n", first.no);
        // 出圈操作,将first 移动到要出圈的人的节点的下个节点上
        first = first.next;
        // 将help的next指向first
        help.next = first;
    }
    System.out.printf("最后出圈的人编号为:%d\n", help.no);
}

栈:stack,是一个先入后出(FILO--First In Last Out)的有序列表。栈是限制线性表中元素的插入和删除只能在线性表的同一端进行的特殊线性表。允许插入和删除的一端(变化的一端)称为栈顶(Top),另一端为固定的一端,称为栈底(Bottom)

特性:最先放入栈中的元素在栈低,最后放入的元素在栈顶;删除元素时,最后放入的元素(栈顶)先删除。

出栈(Pop):把栈顶的元素取出。入栈(push):把元素放入栈中。

应用场景:栈帧:子程序的调用,处理递归调用;表达式的转换和求值;二叉树的遍历;图的深度优先搜索等。

数组模拟栈

思路

  1. 使用变量top,记录栈顶的元素位置,初始化为-1;
  2. 入栈时,将top++;把数据data放入数组中,array[top] = data;
  3. 出栈时,使用临时变量v存储栈顶的元素,v = array[top];然后top--;最后把v返回。return v;

代码实现

/
 * 使用数组模拟栈
 *
 * @author lzlg
 */
public class ArrayStack {
    private int maxSize; // 栈深度

    private int[] stack; // 栈中数据的存放

    private int top = -1; // 栈顶位置

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

    /
     * 栈是否已满
     */
    public boolean isFull() {
        return top == maxSize - 1;
    }

    /
     * 栈是否已空
     */
    public boolean isEmpty() {
        return top == -1;
    }

    /
     * 入栈
     */
    public void push(int value) {
        if (isFull()) {
            System.out.println("栈满,不能入栈~~~");
            return;
        }
        stack[++top] = value;
    }

    /
     * 出栈
     */
    public int pop() {
        if (isEmpty()) {
            throw new RuntimeException("栈空,不能出栈~~~");
        }
        return stack[top--];
    }

    /
     * 显示所有栈中的元素,注意从栈顶到栈底
     */
    public void list() {
        if (isEmpty()) {
            System.out.println("栈空,没有数据~~~");
        }
        for (int i = top; i >= 0; i--) {
            System.out.printf("stack[%d] = %d \n", i, stack[i]);
        }
    }
}

单链表模拟栈

/
 * 使用链表模拟栈
 *
 * @author lzlg
 */
public class LinkedListStack {

    private StackNode head; // 栈的首节点

    private static class StackNode {

        int value;

        StackNode next;

        public StackNode(int value, StackNode next) {
            this.value = value;
            this.next = next;
        }

        @Override
        public String toString() {
            StringBuilder sb = new StringBuilder();
            sb.append(value).append("_");
            sb.append(next == null ? "null" : next.value);
            return sb.toString();
        }
    }

    /
     * 栈是否为空
     */
    public boolean isEmpty() {
        return head == null;
    }

    /
     * 入栈
     */
    public void push(int value) {
        StackNode pre = head;
        head = new StackNode(value, pre);

    }

    /
     * 出栈
     */
    public int pop() {
        if (isEmpty()) {
            throw new RuntimeException("栈空,不能出栈~~~");
        }
        StackNode node = head;
        head = head.next;
        return node.value;
    }

    /
     * 显示栈中所有元素
     */
    public void list() {
        if (isEmpty()) {
            System.out.println("栈空,没有数据~~~");
            return;
        }
        StackNode temp = head;
        while (temp != null) {
            System.out.println("data : " + temp.value);
            temp = temp.next;
        }
    }
}

栈实现计算表达式

思路

  1. 使用一个指针index用来扫描表达式的一个个字符。
  2. 准备两个栈,一个是数栈numberStack(只存放数),一个是运算符栈signStack(只放运算符)。
  3. index扫描到数,就入数栈。
  4. index扫描到运算符s,如果signStack为空,直接入栈。如果signStack不为空,当前栈顶的操作符c的优先级大于运算符s,则从数栈中pop出两个数,从signStack中pop出c,进行运算,并将结果r压入数栈;c的优先级小于等于s,则s入栈。
  5. 扫描完表达式后,则从数栈中pop出两个数,从signStack中pop出运算符,进行运算,并将结果压入数栈中,依上进行,直到运算符栈为空,数栈只剩一个数result,此时这个result就是计算结果。

代码实现

/
 * 使用栈计算表达式的值
 *
 * @author lzlg
 */
public class Calculator {
    private Calculator() {
    }

    public static void main(String[] args) {
        String expression = "30+2*8-40";
        // 数栈,存放数字
        LinkedListStack numberStack = new LinkedListStack();
        // 符号栈,存放运算符
        LinkedListStack signStack = new LinkedListStack();

        char[] chars = expression.toCharArray();

        String num = "";
        for (int i = 0; i < chars.length; i++) {
            char c = chars[i];
            if (isNumber(c)) { // 数字之间入数栈
                num += c;
                // 如果已经是最后一位了,直接把num转成int入栈
                if (i == chars.length - 1) {
                    numberStack.push(toInt(num));
                } else { // 否则看下一个字符是否是运算符,如果是运算符,则入栈
                    if (!isNumber(chars[i + 1])) {
                        numberStack.push(toInt(num));
                        num = "";
                    }
                }
            } else {
                // 运算符
                if (signStack.isEmpty()) { // 符号栈为空,之间入栈
                    signStack.push(c);
                } else {
                    // 如果当前字符优先级大于栈顶的字符的优先级,则之间入栈
                    if (priority(c, signStack.peek())) {
                        signStack.push(c);
                    } else { // 反之,从数栈中pop出两个元素,
                        // 从符号栈pop出运算符,进行运算,把运算的结果入栈
                        int result = calculate(numberStack.pop(), numberStack.pop(), signStack.pop());
                        numberStack.push(result);
                        // 把当前字符入栈
                        signStack.push(c);
                    }
                }
            }
        }

        // 计算结果
        while (!signStack.isEmpty()) {
            int result = calculate(numberStack.pop(), numberStack.pop(), signStack.pop());
            numberStack.push(result);
        }
        System.out.printf("表达式 %s 的结果为 %d\n", expression, numberStack.pop());
    }

    /
     * 判断一个字符是否是数字
     */
    private static boolean isNumber(char c) {
        return Character.isDigit(c);
    }

    /
     * 自定义操作符优先级,优先级越高,数值越大
     */
    private static int priority(char c) {
        if (c == '+' || c == '-') {
            return 0;
        } else if (c == '*' || c == '/') {
            return 1;
        } else {
            return -1;
        }
    }

    /
     * 比较a操作符的优先级是否大于b
     */
    private static boolean priority(int a, int b) {
        return priority((char) a) > priority((char) b);
    }

    /
     * 计算值,a,b是数字,s是操作符
     */
    private static int calculate(int a, int b, int s) {
        switch ((char) s) {
            case '+':
                return a + b;
            case '-':
                return b - a; // 注意顺序
            case '*':
                return a * b;
            case '/':
                return b / a; // 注意顺序
        }
        throw new RuntimeException("不是合法的操作符");
    }

    /
     * 数字字符转对应的int整数
     */
    private static int toInt(char c) {
        return c - 48;
    }

    /
     * 字符串转int
     */
    private static int toInt(String s) {
        return Integer.parseInt(s);
    }
}

前缀表达式

前缀表达式又叫波兰式,前缀表达式的运算符位于操作数之前。

比如:(3 + 4) x 5 - 6 对应的前缀表达式为:- x + 3 4 5 6;

计算过程:从右向左扫描表达式,遇到数字时,将数字压入栈中;遇到运算符时,弹出栈顶的两个数,并用运算符计算结果,并将结果入栈。重复上诉过程直到扫描完毕。注意:减法的顺序是弹出栈顶的第一个元素减去第二个。

上诉表达式的运算过程是:将6,5,4,3入栈,遇到 + 号,弹出栈顶的 3 和 4,计算为 7,将7压入栈顶;遇到 x 号,弹出栈顶的 7 和 5,计算为 35,将35压入栈顶;遇到 - 号,弹出栈顶的 35 和 6,计算为 29,将29入栈顶。

中缀表达式

中缀表达式就是常见的运算表达式,比如:(3 + 4) x 5 - 6 。但是对计算机来说不好操作。

后缀表达式

后缀表达式又称逆波兰表达式,与前缀表达式类似,只是运算符位于操作数之后。

比如:(3 + 4) x 5 - 6 对应的后缀表达式为:3 4 + 5 x 6 - ;

计算过程:从左至右扫描表达式,遇到数字时,将数字入栈。遇到运算符时,弹出栈顶的两个元素,并用运算符计算结果,将结果压入栈;重复上诉过程直到扫描完毕。注意:减法的顺序是弹出栈顶的第二个元素减去第一个。

上诉表达式的运算过程是:将3,4入栈,遇到 + 号,弹出栈顶的 3 和 4,计算为 7,将7压入栈顶;将5入栈;遇到 x 号,弹出栈顶的 5 和 7,计算为 35,将35压入栈顶;将 6 入栈;遇到 - 号,弹出栈顶的 6 和 35,计算为 29,将29入栈顶。

逆波兰表达式计算器

输入一个逆波兰表达式,使用栈计算结果;支持小括号和多位整数。

/
 * 逆波兰表达式计算器
 *
 * @author lzlg
 */
public class RebelPolandComputer {
    public static void main(String[] args) {
        String expression = "30 4 + 5 * 6 -";
        int result = compute(expression);
        System.out.println(result);
    }

   /
     * 计算逆波兰表达式的值
     */
    private static int compute(String expression) {
        String[] array = expression.split(" ");
        return compute(Arrays.asList(array));
    }

    /
     * 真正的执行计算的方法
     */
    public static int compute(List<String> list) {
        Stack<Integer> stack = new Stack<>();
        for (String s : list) {
            if (Tools.isNumber(s)) { //遇到数字之间入栈
                stack.push(Integer.parseInt(s));
            } else { // 遇到运算符,则弹出两个元素,并把计算结果入栈
                stack.push(compute(stack.pop(), stack.pop(), s));
            }
        }
        return stack.pop();
    }

    private static int compute(int b, int a, String sign) {
        switch (sign) {
            case "+":
                return a + b;
            case "-":
                return a - b;
            case "*":
                return a * b;
            case "/":
                return a / b;
        }
        throw new RuntimeException("运算符不正确~~~");
    }
}

中缀转后缀表达式

思路

  1. 使用指针index扫描表达式,并创建两个栈s1(辅助栈)和s2(结果存放栈)。
  2. 遇到数字时,直接入栈s2;注意:( 和 ) 不是运算符。
  3. 遇到运算符p时,当s1为空时,直接入栈;当s1栈顶的元素是 ( 的话,直接入栈;当s1栈顶的元素是运算符q,且q的优先级大于等于p时,q出栈s1,入栈s2,将p压入栈s1;如果q的优先级小于p,则p直接入栈s1。
  4. 遇到符号 ) 时,将s1栈中直到 ( 的运算符依次弹出,放入栈s2。
  5. 扫描完毕后,将s1栈中的元素依次弹出放入到s2中。栈s2中元素的逆序就是后缀表达式。

代码实现

/
 * 把中缀表达式转成后缀表达式
 *
 * @author lzlg
 */
public class InfixToSuffixTool {
    private InfixToSuffixTool() {
    }

    public static void main(String[] args) {
        String expression = "11+((2+3)*4)-56";
        // 先把表达式转换成list --> [1,+,(,(,2,+,3,),*,4,),-,5] 方便以后处理
        List<String> infix = toList(expression);
        System.out.println(infix);
        // 将中缀转换成后缀
        List<String> suffix = toSuffix(infix);
        System.out.println(suffix);
        // 使用逆波兰计算器计算结果
        int result = RebelPolandComputer.compute(suffix);
        System.out.println(result);
    }

    /
     * 把表达式转换成list
     */
    private static List<String> toList(String expression) {
        char[] chars = expression.toCharArray();
        int len = chars.length;
        List<String> list = new ArrayList<>();
        String num;
        for (int i = 0; i < len; ) {
            if (Tools.isNumber(chars[i])) {
                num = "";
                while (i < len && Tools.isNumber(chars[i])) {
                    num += chars[i];
                    i++;
                }
                list.add(num);
            } else {
                list.add("" + chars[i]);
                i++;
            }
        }
        return list;
    }

    /
     * 中缀表达式转换成后缀表达式
     */
    public static List<String> toSuffix(List<String> infix) {
        List<String> suffix = new ArrayList<>();
        Stack<String> stack = new Stack<>();
        // 遍历infix
        for (String s : infix) {
            if (Tools.isNumber(s)) { // 遇到数字直接添加到suffix中
                suffix.add(s);
            } else {
                if (s.equals("(")) { // 如果是左括号直接入栈
                    stack.push(s);
                } else if (s.equals(")")) { // 如果是右括号,
                    // 则把栈中直到不是 ( 的元素弹出,并添加到suffix中
                    while (!stack.peek().equals("(")) {
                        suffix.add(stack.pop());
                    }
                    // 注意要把 ) 也从栈中弹出
                    stack.pop();
                } else { // 如果是运算符
                    if (stack.isEmpty()) { // 1.栈空,直接入栈
                        stack.push(s);
                    } else {
                        if (Tools.priority(stack.peek(), s)) {
                            // 2.栈顶的运算符的优先级大,则弹出栈顶的运算符并添加到suffix中
                            // 将当前扫描到的运算符入栈
                            suffix.add(stack.pop());
                            stack.push(s);
                        } else { // 反之,直接入栈
                            stack.push(s);
                        }
                    }
                }
            }
        }

        // 将栈中的元素依次弹出,添加到suffix中
        while (!stack.isEmpty()) {
            suffix.add(stack.pop());
        }
        return suffix;
    }
}
/
 * 工具类
 *
 * @author lzlg
 */
public class Tools {

    private Tools() {
    }

    private static final String ADD = "+";

    private static final String SUB = "-";

    private static final String MUL = "*";

    private static final String DIV = "/";

    /
     * 判断字符串是否是数字
     */
    public static boolean isNumber(String s) {
        return s != null && s.matches("\\d+");
    }

    /
     * 判断一个字符是否是数字
     */
    public static boolean isNumber(char c) {
        return Character.isDigit(c);
    }

    /
     * 自定义操作符优先级,优先级越高,数值越大
     */
    private static int priority(String s) {
        if (s.equals(ADD) || s.equals(SUB)) {
            return 0;
        } else if (s.equals(MUL) || s.equals(DIV)) {
            return 1;
        } else {
            return -1;
        }
    }

    /
     * 比较a操作符的优先级是否大于b
     */
    public static boolean priority(String a, String b) {
        return priority(a) > priority(b);
    }
}

递归

递归简单来说就是方法自己调用自己,有助于编程解决复杂问题,代码可变得更简洁。当程序执行一个方法的时候,就会开辟独立的空间(栈);每个空间的数据(局部变量)都是独立的。

简单的递归例子

/
 * 递归调用简单例子
 *
 * @author lzlg
 */
public class Example {
    public static void main(String[] args) {
//        print(4);
//        test(4);
        int factorial = factorial(4);
        System.out.println(factorial);
    }

    private static void print(int n) {
        if (n > 2) {
            print(n - 1);
        }
        System.out.println("n = " + n);
    }

    private static void test(int n) {
        if (n > 2) {
            test(n - 1);
        } else {
            System.out.println("n = " + n);
        }
    }

    private static int factorial(int n) {
        if (n == 1) return 1;
        return factorial(n - 1) * n;
    }
}

递归应用场景

各种数学问题:8皇后问题,汉诺塔问题,迷宫问题,阶乘问题;排序算法:快速排序,归并排序,二分查找,分治算法等。

递归要遵守的重要规则

  1. 执行一个方法,就创建一个新的受保护的独立空间。
  2. 方法的局部变量是独立的,不会互相影响。
  3. 如果方法中使用的是引用类型变量,就会共享该引用类型的数据。
  4. 递归必须向退出递归的条件逼近,否则会java.lang.StackOverflowError异常。
  5. 当一个方法执行完毕,或者遇到return,就会返回,遵守谁调用,就将结果返回给谁。

迷宫问题

/
 * 递归中的迷宫问题
 * 约定:使用数字 1 表示墙,2 表示已经走过,3 表示死路
 *
 * @author lzlg
 */
public class Maze {

    public static void main(String[] args) {
        int[][] map = getMap(8, 7);
        walk(map, 1, 1);
    }

    /
     * 走迷宫方法,目前的策略是 下 -> 右 -> 上 -> 左
     */
    private static boolean walk(int[][] map, int i, int j) {
        if (map[map.length - 2][map[0].length - 2] == 2) {
            print(map);
            return true;
        }
        if (map[i][j] == 0) { // 0表示没有走过
            map[i][j] = 2; // 假设可以在此位置可以走通
            if (walk(map, i + 1, j)) {// i + 1 向下走
                return true;
            } else if (walk(map, i, j + 1)) {// j + 1 向右走
                return true;
            } else if (walk(map, i - 1, j)) {// i - 1 向上走
                return true;
            } else if (walk(map, i, j - 1)) {// j - 1 向左走
                return true;
            } else {
                map[i][j] = 3; // 如果都走不通,则路堵死了,置为3
                return false;
            }
        } else { // 其他情况都是false
            return false;
        }
    }

    /
     * 建造迷宫
     */
    private static int[][] getMap(int row, int col) {
        int[][] map = new int[row][col];
        // 将最顶和最底两行初始化为墙(数字1)
        for (int i = 0; i < col; i++) {
            map[0][i] = 1;
            map[row - 1][i] = 1;
        }
        // 将最左和最右两行初始化为墙(数字1)
        for (int i = 0; i < row; i++) {
            map[i][0] = 1;
            map[i][col - 1] = 1;
        }
        // 设置路障
        map[3][1] = 1;
        map[3][2] = 1;
        return map;
    }

    /
     * 打印迷宫
     */
    private static void print(int[][] map) {
        for (int i = 0; i < map.length; i++) {
            for (int j = 0; j < map[i].length; j++) {
                System.out.print(map[i][j] + " ");
            }
            System.out.println();
        }
    }
}

八皇后问题

在8x8的国际象棋上摆放8个皇后,使任意两个皇后不能处于同一行,同一列,同一条斜线上。

思路

把第一个皇后放在第一行的第一列,然后放置第二个皇后到第二列的第一个位置上,判断是否可以,如果不可以,把第二个皇后放在第二列的第二个位置,再次判断,直到第二个皇后满足,然后按照上面的方法再放置其他的皇后,直到得到一个正确的解。然后,开始回溯,把第一个皇后放在第一行的第二列,按照上面的方法再次进行,直到把第一皇后放置到第一行的最后一列完成。

代码实现

/
 * 8皇后问题
 *
 * @author lzlg
 */
public class EightQueen {
    public static void main(String[] args) {
        set(0);
        System.out.println("共有 " + count + " 种解法。");
    }

    private static final int max = 8;
    /
     * 使用chess记录一个正确的结果
     * chess[i] = v;
     * 表示:第 i + 1 个皇后 在 i + 1 行 放置在 v + 1 列
     */
    private static int[] chess = new int[max];

    private static int count = 0;

    /
     * 放置第 n + 1 个皇后
     */
    private static void set(int n) {
        if (n == max) { // 当 n = 8 时,证明已经将8个皇后放置完毕
            print();
            return;
        }
        for (int i = 0; i < max; i++) {
            chess[n] = i;// 把皇后依次放置在一行的每个位置上
            if (judge(n)) { // 如果能够放置,则继续下一个
                set(n + 1);
            }
        }
    }

    /
     * 判断第 n + 1 个皇后是否可以放置
     */
    private static boolean judge(int n) {
        // 和前面的皇后逐个对比
        for (int i = 0; i < n; i++) {
            if (judge(i, n)) {
                return false;
            }
        }
        return true;
    }

    /
     * 之前的皇后和现在的皇后位置比对
     */
    private static boolean judge(int before, int now) {
        // chess[before] == chess[now]是否在同一列上
        // Math.abs(before - now) == Math.abs(chess[before] - chess[now]) 是否在斜线上
        return chess[before] == chess[now]
                || Math.abs(before - now) == Math.abs(chess[before] - chess[now]);
    }

    /
     * 打印一次正确的结果
     */
    private static void print() {
        count++;
        for (int i = 0; i < chess.length; i++) {
            System.out.print(chess[i] + " ");
        }
        System.out.println();
    }
}

汉诺塔问题

大梵天创造世界的时候做了三根金刚石柱子,在一根柱子上从下往上按照大小顺序摞着64片黄金圆盘。大梵天命令婆罗门把圆盘从下面开始按大小顺序重新摆放在另一根柱子上。并且规定,在小圆盘上不能放大圆盘,在三根柱子之间一次只能移动一个圆盘。

思路

假设有四个圆盘在A柱上。假设已经把上面的3个圆盘挪动到B柱子上,则最后的放在C柱子上,就完成了。那如何把3个圆盘放到B柱子上呢?那就假设已经把2个圆盘挪动到A柱子上,那把第三个圆盘放置到B柱子上,就完成。

代码实现

/
 * 汉诺塔问题
 *
 * @author lzlg
 */
public class HanoiTower {
    public static void main(String[] args) {
        move(3, "A", "B", "C");
    }

    /
     * 把a柱子上的n个圆盘,借助柱子b,挪动到C柱子上
     */
    private static void move(int n, String a, String b, String c) {
        if (n == 1) {
            System.out.println(a + " -> " + c);
        } else {
            move(n - 1, a, c, b); // 把 n - 1 个圆盘从 A 移动到 B
            System.out.println(a + " -> " + c); // 把最后一个圆盘 从 A 移动到 C
            move(n - 1, b, a, c); // 把 n - 1 个圆盘从 B 移动到 C
        }
    }
}

排序算法

排序算法是将一组数据,按照指定的顺序进行排列的过程。分为:内部排序和外部排序。常见的排序算法有:插入排序(直接插入,希尔排序),选择排序(简单选择排序,堆排序),交换排序(冒泡排序,快速排序),归并排序,基数排序(桶排序)。以上都是内部排序。

算法的时间复杂度

算法的时间频度:一个算法花费的时间与算法中语句的执行次数成正比。一个算法中的语句执行次数称为语句频度或时间频度 T(n)。

算法的时间复杂度:使用大O标记法。常数项,系数,低次阶都可忽略。

计算方法:用常数1代替所有加法常数,只保留最高阶项,去除最高阶的系数。

常见的时间复杂度:常数阶O(1),对数阶O(logn),线性阶O(n),线性对数阶O(nlogn),平方阶O(n^2),立方阶O(n^3),K次方阶O(n^k),指数阶O(2^n),阶乘O(n!)。时间复杂度从小到大。

平均时间复杂度:所有可能的输入实例均以等概率出现的情况下,该算法的运行时间。

最坏时间复杂度:最好情况下算法的运行时间,是算法在输入任何实例上运行时间的界限。

稳定度:相同元素在未排序时的顺序,是否和排序后的顺序保持一致。

排序法平均时间最差情况稳定度额外空间备注
冒泡O(n^2)O(n^2)稳定O(1)n小时较好
交换O(n^2)O(n^2)不稳定O(1)n小时较好
选择O(n^2)O(n^2)不稳定O(1)n小时较好
插入O(n^2)O(n^2)稳定O(1)大部分已排序时较好
基数O(logrB)O(logrB)稳定O(n)B是真数,r是基数
希尔O(nlogn)O(n^s) (1<s<2)不稳定O(1)s是所选分组
快速O(nlogn)O(n^2)不稳定O(nlogn)n大时较好
归并O(nlogn)O(nlogn)稳定O(1)n大时较好
O(nlogn)O(nlogn)不稳定O(1)n大时较好

算法的空间复杂度

算法的空间复杂度:该算法所耗费的存储空间。在做算法分析时,主要关注算法的时间复杂度,更看重算法的执行速度,一些缓存产品和算法的本质就是用空间换时间。

算法执行时间测试工具类

import java.util.Arrays;
import java.util.Random;
import java.util.function.Consumer;
/
 * 工具类
 */
public class Tools {
    private Tools() {
    }
    /
     * 测试结果
     */
    public static void test(int[] array, Consumer<int[]> consumer) {
        System.out.println("排序前的数组为:");
        System.out.println(Arrays.toString(array));
        consumer.accept(array);
        System.out.println("排序后的数组为:");
        System.out.println(Arrays.toString(array));
    }
    
    private static Random random = new Random();
    /
     * 获取随机数组
     */
    private static int[] randomArray(int count) {
        int[] array = new int[count];
        for (int i = 0; i < count; i++) {
            array[i] = random.nextInt();
        }
        return array;
    }
    /
     * 获取排序算法花费的时间
     */
    public static void time(int count, Consumer<int[]> consumer) {
        int[] array = randomArray(count);
        long start = System.currentTimeMillis();
        consumer.accept(array);
        long end = System.currentTimeMillis();
        System.out.printf("排序花费的时间为:%d秒。", (end - start) / 1000);
    }
}

冒泡排序

基本思路:对排序序列从前往后,依次比较相邻的元素,若发现逆序则交换,使最小(或最大)的值逐渐从后移动到前。优化:如果一趟下来,没有发生过交换,则证明序列有序。

import com.lzlg.study.Tools;
import java.util.Arrays;
/
 * 冒泡排序
 *
 * @author lzlg
 */
public class BubbleSort {
    public static void main(String[] args) {
        // 64秒
        Tools.time(200000, BubbleSort::decreaseSort);
    }

    private static void test() {
        // int[] array = {3, 9, -1, 10, 20, -2};
        int[] array = {1, 2, 3, 4, 5, 6};
        Tools.test(array, BubbleSort::increaseSort);
    }

    /
     * 递增排序
     */
    private static void increaseSort(int[] array) {
        boolean flag = false;
        for (int i = 0; i < array.length - 1; i++) {
            for (int j = 0; j < array.length - i - 1; j++) {
                if (array[j] > array[j + 1]) {
                    flag = true;
                    int temp = array[j];
                    array[j] = array[j + 1];
                    array[j + 1] = temp;
                }
            }
//            System.out.printf("第%d趟排序后的数组为:\n", i + 1);
//            System.out.println(Arrays.toString(array));
            if (flag) { // 如果没有进行一次交换,则证明数组已经有序
                flag = false;
            } else {
                break;
            }
        }
    }

    /
     * 递减排序
     */
    private static void decreaseSort(int[] array) {
        boolean flag = false;
        for (int i = 0; i < array.length - 1; i++) {
            for (int j = 0; j < array.length - i - 1; j++) {
                if (array[j] < array[j + 1]) {
                    flag = true;
                    int temp = array[j];
                    array[j] = array[j + 1];
                    array[j + 1] = temp;
                }
            }
//            System.out.printf("第%d趟排序后的数组为:\n", i + 1);
//            System.out.println(Arrays.toString(array));
            if (flag) { // 如果没有进行一次交换,则证明数组已经有序
                flag = false;
            } else {
                break;
            }
        }
    }
}

选择排序

思路:每次遍历,找到最小(或最大)的元素与第一个位置进行交换。再次遍历,找到除第一个位置的其他元素的最小(或最大)的元素,与第二个位置进行交互。

import com.lzlg.study.Tools;
import java.util.Arrays;
/
 * 选择排序
 *
 * @author lzlg
 */
public class SelectSort {
    public static void main(String[] args) {
        // 16秒
        Tools.time(200000, SelectSort::sort);
    }

    private static void test() {
        int[] array = {100, 21, 230, 1};
        Tools.test(array, SelectSort::sort);
    }

    private static void sort(int[] array) {
        for (int i = 0; i < array.length - 1; i++) {
            int min = array[i]; // 存放最小元素
            int minIndex = i;
            for (int j = i + 1; j < array.length; j++) {
                if (array[j] < min) { // 依次查找直到找到最小值
                    minIndex = j;
                    min = array[j];
                }
            }
            if (minIndex != i) {
                array[minIndex] = array[i]; // 进行交换
                array[i] = min;
            }
//            System.out.printf("第%d趟排序后的数组为:\n", i + 1);
//            System.out.println(Arrays.toString(array));
        }
    }
}

插入排序

思路:只举例第一趟排序过程;假定数组中的第一个元素a是有序的,取出数组的第二个元素b,和前面的有序元素a依次进行比较,如果小于前面的有序元素a,则将有序元素所在位置变为第二个元素b,把有序元素放置第二个元素b原来的位置上。

import com.lzlg.study.Tools;
/
 * 插入排序
 *
 * @author lzlg
 */
public class InsertSort {
    public static void main(String[] args) {
        // 3秒
        Tools.time(200000, InsertSort::sort);
    }

    private static void test() {
        int[] array = {100, 34, 31, 2};
        Tools.test(array, InsertSort::sort);
    }

    private static void sort(int[] array) {
        for (int i = 1; i < array.length; i++) {
            int insertVal = array[i];
            int insertIndex = i - 1; // array[1]前面的第一个元素的下标
            // 如果要插入的元素的值小于前面的有序元素,则把有序元素进行后移
            while (insertIndex >= 0 && insertVal < array[insertIndex]) {
                array[insertIndex + 1] = array[insertIndex];
                insertIndex--;
            }
            // 循环过后,找到了插入的位置 insertIndex + 1
            array[insertIndex + 1] = insertVal;
//            System.out.printf("第%d趟排序后的数组为:\n", i);
//            System.out.println(Arrays.toString(array));
        }
    }
}

希尔排序

思路:将数组(长度为10)进行分组,第一次分为两两一组(共10/2 = 5组),然后将这5组分别进行比较。第二次分为2组(5 / 2 = 2)组,然后将这两组分别进行排序。第三次分为1组(2 / 2 = 1)组,然后进行排序。

import com.lzlg.study.Tools;
/
 * 希尔排序
 *
 * @author lzlg
 */
public class ShellSort {
    public static void main(String[] args) {
        // 交换法 19秒到30秒之间
        Tools.time(ShellSort::sort);

        // 移位法 1千万级数据只需要2秒
//        Tools.time(ShellSort::sort2);
    }

    private static void test() {
        int[] array = {8, 9, 1, 7, 2, 3};
        Tools.test(array, ShellSort::sort2);
    }

    /
     * 移位法进行希尔排序
     */
    private static void sort2(int[] array) {
        for (int g = array.length / 2; g > 0; g /= 2) {// 进行分组
            for (int i = g; i < array.length; i++) {
                int insertVal = array[i];
                int insertIndex = i - g;
                while (insertIndex >= 0 && insertVal < array[insertIndex]) {
                    array[insertIndex + g] = array[insertIndex];
                    insertIndex -= g;
                }
                array[insertIndex + g] = insertVal;
            }
        }
    }

    /
     * 交换法进行希尔排序
     */
    private static void sort(int[] array) {
        int temp = 0;
//       int count = 0;// 计数
        for (int g = array.length / 2; g > 0; g /= 2) {// 进行分组
            for (int i = g; i < array.length; i++) { // 对每个分组进行操作
                for (int j = i - g; j >= 0; j -= g) { // 遍历每个分组
                    if (array[j] > array[j + g]) { // 进行比较
                        temp = array[j];
                        array[j] = array[j + g];
                        array[j + g] = temp;
                    }
                }
            }
//            count++;
//            System.out.println("第" + count + "次希尔排序后:" + Arrays.toString(array));
        }
    }
}

快速排序

思路:从中间(也可在两端)去一个基准元素a。让a左边的元素都小于a,让a右边到的元素都大于a。然后分别从左边和右边做同样的事。从左边遍历,找到一个小于a的元素b;从右边遍历,找到一个大于a的元素c,然后交换,直到遍历完成。然后从左边和右边按照上面步骤再次进行。

import com.lzlg.study.Tools;
/
 * 快速排序
 *
 * @author lzlg
 */
public class QuickSort {
    public static void main(String[] args) {
        // 5千万6秒
//        Tools.time(QuickSort::sort);
        test();
    }

    public static void test() {
        int[] array = {5, 1, 1, 2, 0, 0};
        Tools.test(array, QuickSort::sort);
    }

    private static void sort(int[] array) {
        sort(array, 0, array.length - 1);
    }

    private static void sort(int[] array, int left, int right) {
        int l = left;
        int r = right;
        int b = array[(left + right) / 2];
        while (l < r) {// 让b的左边变成比b小的,让b的右边变成比b大的。
            while (array[l] < b) { // 在左边找到一个比b大的元素
                l++;
            }
            while (array[r] > b) { // 在右边找到一个比b小的元素
                r--;
            }
            if (l >= r) { // 如果没有找到,则已经符合条件了,就退出循环
                break;
            }
            // 进行交换
            int temp = array[l];
            array[l] = array[r];
            array[r] = temp;

            if (array[l] == b) { // 防止数组出现和b相同的元素,死循环
                r--;
            }
            if (array[r] == b) {
                l++;
            }
        }

        if (l == r) { // 防止重复进行
            l++;
            r--;
        }

        if (left < r) { // 向左递归
            sort(array, left, r);
        }

        if (l < right) { // 向右递归
            sort(array, l, right);
        }
    }
}

归并排序

思路:算法使用经典的分治策略。分:把数据分割成到只有一个元素。治:使用辅助数组,把两个元素排序后合并,再把四个元素通过左右指针遍历合并到辅助数组中,直到分割的所有元素都合并完成。

import com.lzlg.study.Tools;
/
 * 归并排序
 *
 * @author lzlg
 */
public class MergeSort {
    public static void main(String[] args) {
        // 5千万数据8秒
        Tools.time(MergeSort::sort);
    }

    private static void test() {
        int[] array = {8, 9, 1, 5, 6, 3, 4, 0, 2, 7};
        Tools.test(array, MergeSort::sort);
    }

    private static void sort(int[] array) {
        int[] temp = new int[array.length];
        split(array, 0, array.length - 1, temp);
    }

    private static void split(int[] array, int left, int right, int[] temp) {
        if (left < right) {
            int mid = (left + right) / 2;
            split(array, left, mid, temp); // 向左分割
            split(array, mid + 1, right, temp); // 向右分割

            merge(array, left, mid, right, temp); // 进行合并
        }
    }

    private static void merge(int[] array, int left, int mid, int right, int[] temp) {
        int l = left;
        int r = mid + 1;
        int t = 0;
        while (l <= mid && r <= right) {// 同时从两个分割的数组中遍历
            // 比较,并将较小的值放置在temp数组中
            if (array[l] <= array[r]) {
                temp[t] = array[l];
                l++;
            } else {
                temp[t] = array[r];
                r++;
            }
            t++;
        }

        while (l <= mid) { // 左边数据有可能没有遍历完成
            temp[t] = array[l];
            t++;
            l++;
        }

        while (r <= right) {// 右边数据有可能没有遍历完成
            temp[t] = array[r];
            t++;
            r++;
        }

        // 把临时数组的数据复制给原数组中,用于下次合并时排序
        t = 0;
        int tempLeft = left;
        while (tempLeft <= right) {
            array[tempLeft] = temp[t];
            t++;
            tempLeft++;
        }
    }
}

基数排序

思路:使用10个桶,遍历一般数组中的元素,将元素的个位取出,并按照个位的数字放置到相应的桶中(桶的下标对应个位的数字),放置完成后,再按照桶的顺序遍历,把元素依次取出,放入到原数组中。然后再遍历数组中的元素,把元素的十位数取出,并按照十位的数字放置到相应的桶中,放置完成后,再按照桶的顺序遍历,把元素依次取出,放入到原数组中。按照上述方法直到把每个元素的最高位遍历完成。

桶排序有自己的局限性:比如有负数的情况(处理比较复杂);如果排序的是对象,则就不能使用。

import com.lzlg.study.Tools;
/
 * 基数排序
 *
 * @author lzlg
 */
public class RadixSort {
    public static void main(String[] args) {
        // 5千万6秒
        Tools.time(RadixSort::sort);
    }

    private static void test() {
        int[] array = {124, 56, 89, 1, 77, 60, 11, 28, 399, 12, 99, 2};
        Tools.test(array, RadixSort::sort);
    }

    private static void sort(int[] array) {
        // 找到最大元素的长度
        int max = array[0];
        for (int i = 1; i < array.length; i++) {
            if (array[i] > max) {
                max = array[i];
            }
        }
        int len = (max + "").length();
        // 创建桶,为了防止极端情况,每个桶的长度都是原数组的长度
        int[][] bucket = new int[10][array.length];
        // 用于记录每个桶的元素的数量,下标对应桶,数组里的元素对应元素数量
        int[] count = new int[10];

        for (int t = 0, n = 1; t < len; t++, n *= 10) {
            // 将元素按照个位放入桶中,并记录元素数量
            for (int i = 0; i < array.length; i++) {
                int digit = array[i] / n % 10;
                bucket[digit][count[digit]++] = array[i];
            }
            // 取出桶的元素放入原数组中
            int index = 0;
            for (int i = 0; i < count.length; i++) {
                if (count[i] != 0) {
                    for (int j = 0; j < count[i]; j++) {
                        array[index++] = bucket[i][j];
                    }
                    count[i] = 0; // 取出后要将数量置为0
                }
            }
        }
    }
}

查找算法

常用的查找算法有:线性(顺序)查找,二分(折半)查找,插值查找,斐波那契查找。

线性查找

/
 * 线性查找
 *
 * @author lzlg
 */
public class LinearSearch {
    public static void main(String[] args) {
        int[] array = {1, 9, -10, 5, 6, 3, -8};
        int v = 1;
        int search = search(array, 1);
        if (search == -1) {
            System.out.printf("没有找到元素%d\n", v);
        } else {
            System.out.printf("查找到元素%d的下标为:%d\n", v, search);
        }
    }

    private static int search(int[] array, int v) {
        for (int i = 0; i < array.length; i++) {
            if (array[i] == v) {
                return i;
            }
        }
        return -1;
    }
}

二分查找

二分查找只能用于已经排序的集合中。将升序集合从中间分为两份,判断要查找的元素是否大于中间的值,如果大于,则向右查找,否则向左查找,直到找到元素为止。

import java.util.ArrayList;
import java.util.List;
/
 * 二分查找
 *
 * @author lzlg
 */
public class BinarySearch {
    public static void main(String[] args) {
        int[] array = {1, 2, 6, 8, 10, 10, 10, 20, 20, 40, 41};
//        int r = search(array, 60);
//        System.out.println(r);

        List<Integer> list = searchMore(array, 20);
        System.out.println(list);

//        int r = searchWhile(array, 41);
//        System.out.println(r);

//        List<Integer> list = searchWhileMore(array, 10);
//        System.out.println(list);
    }

    private static int search(int[] array, int v) {
        return search(array, 0, array.length - 1, v);
    }

    private static List<Integer> searchMore(int[] array, int v) {
        return searchMore(array, 0, array.length - 1, v);
    }

    /
     * 使用循环实现二分查找
     */
    private static List<Integer> searchWhileMore(int[] array, int v) {
        int l = 0;
        int r = array.length - 1;
        int mid = (l + r) / 2;
        while (l <= r) {
            if (v > array[mid]) {
                l = mid + 1;
                mid = (r + l + 1) / 2;
            } else if (v < array[mid]) {
                r = mid - 1;
                mid = (r - l) / 2;
            } else {
                return more(array, mid, v);
            }
        }
        return new ArrayList<>();
    }

    /
     * 使用循环实现二分查找
     */
    private static int searchWhile(int[] array, int v) {
        int l = 0;
        int r = array.length - 1;
        int mid = (l + r) / 2;
        while (l <= r) {
            if (v > array[mid]) {
                l = mid + 1;
                mid = (r + l + 1) / 2;
            } else if (v < array[mid]) {
                r = mid - 1;
                mid = (r - l) / 2;
            } else {
                return mid;
            }
        }
        return -1;
    }

    /
     * 二分查找,只返回找到的第一个元素的下标
     */
    private static int search(int[] array, int left, int right, int v) {
        if (left > right) {
            return -1;
        }
        int mid = (left + right) / 2;
        int midValue = array[mid];
        if (v > midValue) {
            return search(array, mid + 1, right, v);
        } else if (v < midValue) {
            return search(array, mid - 1, left, v);
        } else {
            return mid;
        }
    }

    /
     * 二分查找,只返回找到的多个相同元素的下标
     */
    private static List<Integer> searchMore(int[] array, int left, int right, int v) {
        if (left > right) {
            return new ArrayList<>();
        }
        int mid = (left + right) / 2;
        int midValue = array[mid];
        if (v > midValue) {
            return searchMore(array, mid + 1, right, v);
        } else if (v < midValue) {
            return searchMore(array, mid - 1, left, v);
        } else {
            return more(array, mid, v);
        }
    }

    private static List<Integer> more(int[] array, int mid, int v) {
        List<Integer> list = new ArrayList<>();
        int temp = mid - 1;
        while (temp >= 0 && array[temp] == v) {
            list.add(temp);
            temp--;
        }
        list.add(mid); // 注意不要忘了把mid加入
        temp = mid + 1;
        while (temp < array.length && array[temp] == v) {
            list.add(temp);
            temp++;
        }
        return list;
    }
}

差值查找

二分查找的局限性:如果要查找的数在数组的两端,还是等从中间开始查找,这样查找次数较多。使用一个算法将mid的值改为自适应的值,则能减少查找的次数。算法为:mid = left + (right - left) * (findVal - arr[left]) / (arr[right] - arr[left])。findVal是要查找的值。注意:在查找前必须判断findVal的值是否小于最小值,或大于最大值,否则会造成mid的值过大或过小,造成数组越界异常。

/
 * 差值查找
 *
 * @author lzlg
 * 2020/1/8 21:47
 */
public class DifferenceSearch {
    public static void main(String[] args) {
        int[] array = {1, 2, 6, 7, 8, 19, 1000, 2189, 44444};
        System.out.println(search(array, 2189));
    }
    // 比较适用于,差值不是特别大的数组,如果差值较大,查找次数也会增加
    private static int search(int[] array, int v) {
        return search(array, 0, array.length - 1, v);
    }

    private static int search(int[] array, int left, int right, int v) {
        System.out.println("查找了一次~~~");
        if (left > right || v < array[0] || v > array[array.length - 1]) {
            return -1;
        }
        int mid = left + (right - left) * (v - array[left]) / (array[right] - array[left]);
        if (v > array[mid]) {
            return search(array, mid + 1, right, v);
        } else if (v < array[mid]) {
            return search(array, left, mid - 1, v);
        } else {
            return mid;
        }
    }
}

斐波那契查找

使用黄金分割来查找值。将数组转变成长度为斐波那契数(大于原数组长度),并将大于原数组长度的元素(初始化为0)转变为原数组的最后一个元素的值;此时mid = low + F[k - 1] - 1,如果array[mid] 大于要查找的值,则向左查找,此时k = k - 1;反之向右查找,此时 k = k - 2;(因为斐波那契函数F[k] = F[k-1] + F[k-2])。

import java.util.Arrays;
/
 * 斐波那契查找
 *
 * @author lzlg
 * 2020/1/8 23:00
 */
public class FibonacciSearch {
    public static void main(String[] args) {
        int[] array = {1, 2, 6, 7, 8, 19};
        System.out.println("index = " + search(array, 1000));
    }

    private static final int MAX_SIZE = 20;

    /
     * 生成斐波那契数组
     */
    private static int[] fibArray() {
        int[] array = new int[MAX_SIZE];
        array[0] = 1;
        array[1] = 1;
        for (int i = 2; i < MAX_SIZE; i++) {
            array[i] = array[i - 1] + array[i - 2];
        }
        return array;
    }

    private static int search(int[] array, int v) {
        int[] f = fibArray();
        int l = 0;
        int r = array.length - 1;
        int k = 0;
        while (r > f[k] - 1) { // 确定最接近数组长度并大于该长度的斐波那契数
            k++;
        }
        // 将原数组的元素拷贝到新数组中
        int[] temp = Arrays.copyOf(array, f[k]);
        // 将新数组下标大于原数组长度的元素变为数组的最后一个元素
        for (int i = r; i < temp.length; i++) {
            temp[i] = array[r];
        }

        // 开始查找
        while (l <= r) {
            int mid = l + f[k - 1] - 1;
            if (temp[mid] > v) {
                r = mid - 1;
                k -= 1;
            } else if (temp[mid] < v) {
                l = mid + 1;
                k -= 2;
            } else {
                // 返回最小的下标,因为拷贝了最后一个相同的元素
                return mid <= r ? mid : r;
            }
        }
        return -1; // 找不到,返回-1
    }
}

哈希表

哈希表(Hash Table)又叫散列表,根据关键码值而直接进行访问的数据结构。把关键码值用映射函数(散列函数)映射到表中的位置来加快查找的速度。

jdk1.8之前是通过数组加链表实现的,jdk1.8之后是数组加红黑树实现的。数组存储的是链表的头节点。

/
 * hash表的实现
 *
 * @author lzlg
 * 2020/1/10 18:30
 */
public class MyHashTable<E> {

    private TableLinkedList<E>[] tableListArray;

    private int size;

    private static final int DEFAULT_SIZE = 8;

    public MyHashTable() {
        this.size = DEFAULT_SIZE;
        tableListArray = new TableLinkedList[size];
        // 这里要初始化链表数组
        for (int i = 0; i < tableListArray.length; i++) {
            tableListArray[i] = new TableLinkedList<>();
        }
    }

    // 添加
    public void add(int id, E data) {
        int no = hash(id);
        tableListArray[no].add(id, data);
    }

    // hash 函数,使用简单的取模运算
    private int hash(int id) {
        return id % DEFAULT_SIZE;
    }

    // 遍历
    public void list() {
        for (int i = 0; i < size; i++) {
            System.out.printf("第 %d 条链表的数据为:", i + 1);
            tableListArray[i].list();
            System.out.println();
        }
    }

    // 根据id查找
    public E search(int id) {
        int no = hash(id);
        E data = tableListArray[no].search(id);
        if (data == null) {
            System.out.printf("没有找到id为%d的数据\n", id);
        } else {
            System.out.printf("找到了,在第%d条链表上,数据为:%s\n", no, data.toString());
        }
        return data;
    }

    public E delete(int id) {
        int no = hash(id);
        E data = tableListArray[no].remove(id);
        if (data == null) {
            System.out.printf("没有找到id为%d的数据\n", id);
        } else {
            System.out.printf("找到了,在第%d条链表上,数据为:%s,成功删除\n",
                    no, data.toString());
        }
        return data;
    }

    // hash表使用的链表
    private static class TableLinkedList<E> {
        private Node<E> root; // 根节点

        boolean isEmpty() {
            return root == null;
        }

        // 添加
        void add(int id, E data) {
            if (isEmpty()) {
                root = new Node<>(id, data);
            } else {
                Node<E> temp = root;
                while (temp.next != null) {
                    temp = temp.next;
                }
                temp.next = new Node<>(id, data);
            }
        }

        // 遍历
        void list() {
            if (isEmpty()) {
                System.out.print("链表为空");
            } else {
                Node<E> temp = root;
                while (temp != null) {
                    System.out.print(temp.data + " ");
                    temp = temp.next;
                }
            }
        }

        E search(int id) {
            if (isEmpty()) {
                return null;
            } else {
                Node<E> temp = root;
                E data = null;
                while (temp != null) {
                    if (temp.id == id) {
                        data = temp.data;
                        break;
                    }
                    temp = temp.next;
                }
                return data;
            }
        }

        E remove(int id) {
            if (isEmpty()) {
                return null;
            } else {
                E data = null;
                if (root.id == id) {// 如果删除的是根节点
                    data = root.data;
                    root = root.next;
                } else {
                    boolean find = false;
                    Node<E> temp = root;// 找到待删除节点的上个节点
                    while (temp.next != null) {
                        if (temp.next.id == id) {
                            find = true;
                            break;
                        }
                        temp = temp.next;
                    }
                    if (find) { // 找到进行删除
                        // 注意此时找到的temp是要删除节点的上个节点
                        data = temp.next.data;
                        temp.next = temp.next.next;
                    }
                }
                return data;
            }
        }

        // 节点node
        private static class Node<E> {
            int id;
            E data;
            Node<E> next;

            Node(int id, E data) {
                this.id = id;
                this.data = data;
            }
        }
    }
}
/
 * 实体类雇员
 *
 * @author lzlg
 * 2020/1/10 19:44
 */
public class Employee {
    private int id;

    private String name;

    public Employee(int id, String name) {
        this.id = id;
        this.name = name;
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return "Employee{" +
                "id=" + id +
                ", name='" + name + '\'' +
                '}';
    }
}
/
 * 测试
 * @author lzlg
 * 2020/1/10 18:32
 */
public class Test {
    public static void main(String[] args) {
        MyHashTable<Employee> hashTable = new MyHashTable<>();
        System.out.println("===========测试添加==========");
        hashTable.add(1, new Employee(1, "Song"));
        hashTable.add(10, new Employee(10, "Parade"));
        hashTable.add(5, new Employee(5, "Time"));
        hashTable.add(8, new Employee(8, "Hello"));
        hashTable.add(2, new Employee(2, "World"));
        hashTable.add(6, new Employee(6, "Jayson"));
        hashTable.add(7, new Employee(7, "Byte"));
        hashTable.add(3, new Employee(3, "Now"));
        hashTable.list();
        System.out.println("===========测试查找==========");
        hashTable.search(2);
        System.out.println("===========测试删除==========");
        hashTable.delete(2);
        hashTable.delete(10);
        hashTable.list();
    }
}

数组易查找,删除耗时(要移动数据);链表易删除,查找慢(需遍历)。有种数据结构叫做树,实现了查找和删除都比较高效。

树的常用术语:节点,根节点,父节点,子节点,叶子节点(无子节点的节点),节点的权值,路径(从根节点到节点的路线),层(描述树的高度),子树(每个树可以看成是子树组成的),树的高度,森林(多棵子树)。

二叉树

每个节点最多只能有两个子节点的树称为二叉树。

满二叉树:所有叶子节点都在最后一层,且节点总数等于2^n - 1个(n为树的最大高度)。

完全二叉树: 设二叉树的深度为h,除第 h 层外,其它各层 (1~h-1) 的结点数都达到最大个数,第 h 层所有的结点都连续集中在最左边,这就是完全二叉树。

(1)所有的叶结点都出现在第h层或h-1层(层次最大的两层)

(2)对任一结点,如果其右子树的最大层次为L,则其左子树的最大层次为L或L+l。

二叉树的遍历

二叉树的遍历有三种方式:前序遍历,中序遍历,后序遍历。

前序遍历:先输出父节点,再遍历左子树和右子树。

中序遍历:先遍历左子树,再输出父节点,最后遍历右子树。

后序遍历:先遍历左子树和右子树,再输出父节点。

/
 * 二叉树
 *
 * @author lzlg
 * 2020/1/10 20:46
 */
public class BinaryTree {

    private TreeNode root; // 根节点

    public BinaryTree(TreeNode root) {
        this.root = root;
    }

    // 前序遍历
    public void preList() {
        if (root != null) {
            root.preOrder();
        }
    }

    // 中序遍历
    public void midList() {
        if (root != null) {
            root.midOrder();
        }
    }

    // 后序遍历
    public void postList() {
        if (root != null) {
            root.postOrder();
        }
    }

    public static class TreeNode {
        int id;
        String name;
        TreeNode left;
        TreeNode right;

        public TreeNode(int id, String name) {
            this.id = id;
            this.name = name;
        }

        // 前序遍历
        void preOrder() {
            System.out.println(this);
            if (this.left != null) {
                this.left.preOrder();
            }
            if (this.right != null) {
                this.right.preOrder();
            }
        }

        // 中序遍历
        void midOrder() {
            if (this.left != null) {
                this.left.midOrder();
            }
            System.out.println(this);
            if (this.right != null) {
                this.right.midOrder();
            }
        }

        // 后序遍历
        void postOrder() {
            if (this.left != null) {
                this.left.postOrder();
            }
            if (this.right != null) {
                this.right.postOrder();
            }
            System.out.println(this);
        }

        @Override
        public String toString() {
            return "Node{" +
                    "id=" + id +
                    ", name='" + name + '\'' +
                    '}';
        }
    }
}

二叉树的查找

// 前序搜索
public TreeNode preSearch(int id) {
    TreeNode node = null;
    if (root != null) {
        node = root.preSearch(id);
    }
    System.out.println("node = " + node);
    return node;
}

// 中序搜索
public TreeNode midSearch(int id) {
    TreeNode node = null;
    if (root != null) {
        node = root.midSearch(id);
    }
    System.out.println("node = " + node);
    return node;
}

// 后序搜索
public TreeNode postSearch(int id) {
    TreeNode node = null;
    if (root != null) {
        node = root.postSearch(id);
    }
    System.out.println("node = " + node);
    return node;
}

// 前序查找
TreeNode preSearch(int id) {
    System.out.println("前序查找~~~");
    if (this.id == id) {
        return this;
    }
    TreeNode node = null;
    if (this.left != null) {
        node = this.left.preSearch(id);
    }
    if (node != null) {
        return node;
    }
    if (this.right != null) {
        node = this.right.preSearch(id);
    }
    return node;
}

// 中序查找
TreeNode midSearch(int id) {
    TreeNode node = null;
    if (this.left != null) {
        node = this.left.midSearch(id);
    }
    if (node != null) {
        return node;
    }
    System.out.println("中序查找~~~");
    if (this.id == id) {
        return this;
    }
    if (this.right != null) {
        node = this.right.midSearch(id);
    }
    return node;
}

// 后序查找
TreeNode postSearch(int id) {
    TreeNode node = null;
    if (this.left != null) {
        node = this.left.postSearch(id);
    }
    if (node != null) {
        return node;
    }
    if (this.right != null) {
        node = this.right.postSearch(id);
    }
    if (node != null) {
        return node;
    }
    System.out.println("后序查找~~~");
    return this.id == id ? this : node;
}

顺序存储二叉树

可以使用数组来存储完全二叉树(这里只考虑完全二叉树)。数组是物理结构,而二叉树是逻辑结构。

特点:只考虑完全二叉树;第n个元素的左子节点为:2 * n + 1; 第n个元素的右子节点为:2 * n + 2; 第n个元素的父节点为:(n - 1) / 2;注意:从0开始编号。

/
 * 使用数组实现二叉树
 *
 * @author lzlg
 * 2020/1/10 23:47
 */
public class ArrayBinaryTree {

    private int[] array; // 存放数据的数组

    public ArrayBinaryTree(int[] array) {
        this.array = array;
    }

    private int getLeft(int index) {
        return 2 * index + 1;
    }

    // 前序遍历
    public void preList() {
        if (array == null || array.length == 0) {
            System.out.println("树为空,不能遍历");
        } else {
            preList(0); // 0是根节点
        }
    }

    private void preList(int index) {
        System.out.println(array[index]);
        int left = getLeft(index);
        if (left < array.length) {
            preList(left);
        }
        if (left + 1 < array.length) {
            preList(left + 1);
        }
    }

    // 中序遍历
    public void midList() {
        if (array == null || array.length == 0) {
            System.out.println("树为空,不能遍历");
        } else {
            midList(0); // 0是根节点
        }
    }

    private void midList(int index) {
        int left = getLeft(index);
        if (left < array.length) {
            midList(left);
        }
        System.out.println(array[index]);
        if (left + 1 < array.length) {
            midList(left + 1);
        }

    }

    // 后序遍历
    public void postList() {
        if (array == null || array.length == 0) {
            System.out.println("树为空,不能遍历");
        } else {
            postList(0); // 0是根节点
        }
    }

    private void postList(int index) {
        int left = getLeft(index);
        if (left < array.length) {
            postList(left);
        }
        if (left + 1 < array.length) {
            postList(left + 1);
        }
        System.out.println(array[index]);
    }
}

线索化二叉树

二叉树没有有效地利用节点的左右节点,叶子节点的左右节点都为空,希望有效利用,于是有线索化二叉树。

n个节点的二叉树中有 n + 1(2*n - (n - 1))个空指针域,存放指向该节点在某种遍历次序下的前驱和后继节点的指针。这种附加的指针称为线索,加上了线索的二叉树称为线索二叉树,根据遍历次序的不同可分为前序线索二叉树,中序线索二叉树,后序线索二叉树。

/
 * 中序线索化二叉树
 *
 * @author lzlg
 * 2020/1/11 1:10
 */
public class MidClueBinaryTree {

    private ClueNode root; // 根节点

    private ClueNode pre; // 辅助前置节点

    public MidClueBinaryTree(ClueNode root) {
        this.root = root;
    }

    public void clue() {
        if (root == null) {
            System.out.println("二叉树为空,不能线索化");
        } else {
            clue(root);
        }
    }

    private void clue(ClueNode node) {
        if (node == null) {
            return;
        }
        // 1.先线索化左子树
        clue(node.left);
        // 2.线索化当前节点
        if (node.left == null) { // node的left为空
            node.leftType = 1; // node的左节点为前驱节点
            node.left = pre;
        }
        // 在下次递归过程中处理右节点
        if (pre != null && pre.right == null) { // 注意此时pre指向下次节点
            pre.rightType = 1;// node的left为空
            pre.right = node;
        }

        pre = node; // 让pre始终保持递归过的上次节点

        // 3.后线索化右子树
        clue(node.right);
    }

    // 线索化后二叉树的遍历
    public void list() {
        if (root == null) {
            System.out.println("二叉树为空,不能遍历");
        } else {
            ClueNode temp = root;
            while (temp != null) {
                while (temp.leftType == 0) {// 找到起始节点
                    temp = temp.left;
                }
                System.out.println(temp);// 输出起始节点

                while (temp.rightType == 1) { // 找到后继节点
                    temp = temp.right;
                    System.out.println(temp);
                }
                temp = temp.right;// 跳转到右节点
            }
        }
    }
}

树的应用

堆排序

堆排序是利用堆这种数据结构而设计的排序算法,是一种选择排序。时间复杂度:O(nlogn),不稳定。

堆是具有以下性质的完全二叉树;每个节点的值都大于或等于其左右子节点的值,称为大顶堆;注意:没有要求节点的左右子节点的值的大小关系;每个节点的值都小于或等于左右子节点的值,称为小顶堆。一般升序使用大顶堆,降序使用小顶堆。

基本思路:将无序序列构建成一个堆,并按照升序或降序选择大顶堆或小顶堆;将堆顶元素和末尾元素进行交换,将最大(小)元素沉到数组末端;重新调整结构,再次变长大顶堆或小顶堆,反复执行,直到变成有序序列。

import com.lzlg.study.Tools;
/
 * 堆排序
 *
 * @author lzlg
 * 2020/1/11 18:02
 */
public class HeapSort {
    public static void main(String[] args) {
//        int[] array = {4, 6, 8, 9, 5, 1, -10, -5, 3};
//        Tools.test(array, HeapSort::sort);
        // 1千万5秒
        Tools.time(HeapSort::sort);
    }

    private static void sort(int[] array) {
        // 从下到上 把非叶子节点调整为大顶堆
        for (int i = array.length / 2 - 1; i >= 0; i--) {
            sort(array, i, array.length);
        }
        // 把堆顶元素和末尾元素交换,并再次调整
        int temp;
        for (int i = array.length - 1; i > 0; i--) {
            temp = array[i];
            array[i] = array[0];
            array[0] = temp;
            sort(array, 0, i);// 此时调整忽略已经交换过的末尾元素
        }
    }

    /
     * @param array  要排序的数组
     * @param i      当前要调整的非叶子节点
     * @param length 当前要调整的数组元素的个数
     */
    private static void sort(int[] array, int i, int length) {
        int temp = array[i]; // 保留当前节点
        // 调整数组形成大顶堆,按照顺序二叉树的性质进行调整
        for (int j = i * 2 + 1; j < length; j = i * 2 + 1) {
            if (j + 1 < length && array[j] < array[j + 1]) {// 比较当前节点的左右子节点的大小
                // 取当前最大的子节点
                j++;
            }
            if (array[j] > temp) { // 如果子节点A大于当前节点
                array[i] = array[j]; // 将子节点A的值赋给当前节点
                i = j; // 并将当前节点换为子节点A,并调整子节点A
            } else {
                break; // 注意必须break,第一次循环调整是自下向上的,
                // 除当前节点,其余非叶子节点都以调整完毕
            }
        }
        array[i] = temp; // 注意此时array[i]已经是最大元素,并已经赋值给当前节点,
        // 所以把原来的当前节点赋值给要交换的节点
    }
}

降序堆排序代码:

/
 * 降序堆排序
 */
private static void decreaseSort(int[] array) {
    for (int i = array.length / 2 - 1; i >= 0; i--) {
        decreaseSort(array, i, array.length);
    }
    int temp;
    for (int i = array.length - 1; i > 0; i--) {
        temp = array[i];
        array[i] = array[0];
        array[0] = temp;
        decreaseSort(array, 0, i);
    }
}

private static void decreaseSort(int[] array, int i, int length) {
    int temp = array[i];
    for (int j = i * 2 + 1; j < length; j = i * 2 + 1) {
        if (j + 1 < length && array[j] > array[j + 1]) {
            j++;
        }
        if (array[j] < temp) {
            array[i] = array[j];
            i = j;
        } else {
            break;
        }
    }
    array[i] = temp;
    System.out.println("===========" + Arrays.toString(array));
}

赫夫曼树

给定n个权值作为n个叶子节点,构造一棵二叉树,若该树的带权路径长度(wpl)达到最小,称这样的二叉树为最优二叉树,也称为赫夫曼树(哈夫曼树,霍夫曼树);

路径:从根节点到叶子节点的通路;路径长度:通路中分支的数目称为长度,如果根节点的层数为1,那到达第L层节点的路径长度为 L - 1;

节点的权:节点赋值了一个有意义的值,这个值就称为该节点的权;节点的带权路径长度:节点的路径长度与节点的权值的乘积。

树的带权路径长度:所有叶子节点的带权路径长度之和,称为树的带权路径长度(weighted path length);权值越大的节点离根节点越近的二叉树是最优二叉树。

构建思路:将数组转换成节点集合;将集合进行排序。取出值最小的权值的两个节点A,B,将节点的权值相加,构建新的父节点P,并将父节点的左右节点置为A,B;然后从集合中删除A,B节点对应的值,将父节点的值加入到集合中;再次进行排序,同样的取出最小的数转为A,B,重复以上步骤,直到集合中只有一个节点。

import java.util.*;
/
 * 赫夫曼树
 *
 * @author lzlg
 * 2020/1/11 19:28
 */
public class HuffmanTree {
    public static void main(String[] args) {
        int[] array = {1, 8, 3, 5, 12, 29};
        show(array);
    }

    private static void show(int[] array) {
        Node root = create(array);
        if (root == null) {
            System.out.println("树为空,不能遍历");
        } else {
            root.preOrder();
        }
    }

    private static Node create(int[] array) {
        List<Node> list = toList(array); // 将数组转为节点集合

        while (list.size() > 1) {
            // 将集合排序
            Collections.sort(list); // node需实现Comparable接口
            // 取出权值最小的两个节点
            Node left = list.remove(0);
            Node right = list.remove(0);
            // 构建父节点,并添加到集合中
            Node parent = new Node(left, left.value + right.value, right);
            list.add(parent);
        }
        // 集合中的最后一个元素就是霍夫曼树的根节点
        return list.get(0);
    }

    private static List<Node> toList(int[] array) {
        List<Node> list = new ArrayList<>();
        for (int i : array) {
            list.add(new Node(i));
        }
        return list;
    }

    private static class Node implements Comparable<Node> {
        int value;
        Node left;
        Node right;

        public Node(int value) {
            this.value = value;
        }

        public Node(Node left, int value, Node right) {
            this.value = value;
            this.left = left;
            this.right = right;
        }

        public void preOrder() {
            System.out.println(this);
            if (this.left != null) {
                this.left.preOrder();
            }
            if (this.right != null) {
                this.right.preOrder();
            }
        }

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

        @Override
        public int compareTo(Node o) {
            return this.value - o.value;
        }
    }
}

赫夫曼编码

赫夫曼编码是一种编码方式,是赫夫曼树在通信中的经典应用之一。广泛用于数据文件压缩,压缩率通常在20%~90%之间。赫夫曼编码是可变字长编码的一种,称之为最佳编码。

编码原理:举例传输的字符串为:i like like like java do you like a java

1)统计各个字符出现的次数:d : 1,y : 1,u : 1,j : 2,v : 2,o : 2,l : 4,k : 4,e : 4,i : 5,a : 5,空格 : 9

2)按照上面字符出现的次数构建一棵赫夫曼树。次数作为权值。

3)根据赫夫曼树,给各个字符规定编码(前缀编码),向左的路径为0,向右的为1;则编码为:

o:1000,u:10010,d:100110,y:100111,i:101,a:110,k:1110,e:1111,j:0000,v:0001

l:001,空格:01;并且不会出现多义性。

4)于是原字符串经过编码后变为:

101 01 001 101 1110 1111 01 001 101 1110 1111 01 001 101 1110 1111 01 0000 110 0001 110 01

100110 1000 100111 10010 01 001 101 1110 1111 01 110 0000 110 0001 110

5)原来使用定长编码长度为359,而使用赫夫曼编码为133,压缩率为62.9%。此编码满足前缀编码,即字符的编码都不能是其他字符编码的前缀,不会造成匹配的多义性。

6)注意:如果有相同权值的数据,进行排序(不同的排序方法)时,会造成赫夫曼树的不同,但WPL都一致。

/
 * 编码节点
 *
 * @author lzlg
 * 2020/1/11 20:58
 */
public class CodeNode<E> implements Comparable<CodeNode<E>> {
    E data;         // 数据
    int weight;     // 权值
    CodeNode<E> left;
    CodeNode<E> right;

    public CodeNode(E data, int weight) {
        this.data = data;
        this.weight = weight;
    }

    public CodeNode(CodeNode<E> left, E data, int weight, CodeNode<E> right) {
        this.data = data;
        this.weight = weight;
        this.left = left;
        this.right = right;
    }

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

    // 前序遍历
    public void preOrder() {
        System.out.println(this);
        if (this.left != null) {
            this.left.preOrder();
        }
        if (this.right != null) {
            this.right.preOrder();
        }
    }

    @Override
    public int compareTo(CodeNode<E> o) {
        return this.weight - o.weight;
    }
}

import java.util.*;
import java.util.stream.Collectors;

/
 * 赫夫曼编码
 *
 * @author lzlg
 * 2020/1/11 20:58
 */
public class HuffmanCode {
    /
     * 编码方法
     */
    public static byte[] code(String content) {
        // 获取字符串的byte数组
        byte[] bytes = content.getBytes();
        // 转成集合
        List<CodeNode<Byte>> nodes = toNodes(bytes);
        // 转换成赫夫曼树
        CodeNode<Byte> huffmanTree = toTree(nodes);
        // 生成赫夫曼编码规则map
        Map<Byte, String> codeRuleMap = RuleMapData.codeRuleMap(huffmanTree);
        // 按照编码规则将原来byte数组转成新的二进制字符串
        StringBuilder huffmanStr = toHuffmanString(bytes, codeRuleMap);
        System.out.println("编码后的二进制字符串为:" + huffmanStr);
        // 将新的二进制字符串转换为byte数组,每8位一个
        return toCodeBytes(huffmanStr);
    }

    /
     * 转成node集合
     */
    private static List<CodeNode<Byte>> toNodes(byte[] bytes) {
        // 获取字符的出现次数,然后构建node集合
        Map<Byte, Integer> map = new HashMap<>();
        for (byte b : bytes) {
            map.merge(b, map.getOrDefault(b, 1), (k, v) -> v + 1);
        }

        // 转成集合
        return map.entrySet().stream()
                .map(v -> new CodeNode<>(v.getKey(), v.getValue()))
                .collect(Collectors.toList());
    }

    /
     * 将新的二进制字符串转换为byte数组,每8位一个
     */
    private static byte[] toCodeBytes(StringBuilder sb) {
        int strLen = sb.length();
        int len = (strLen + 7) / 8;
        byte[] codeBytes = new byte[len];
        int index = 0;
        for (int i = 0; i < strLen; i += 8) {// 每8位截取一个
            String str;// 要截取的字符串
            if (i + 8 > strLen) {// 不足8位,则直接截取到最后
                str = sb.substring(i);
            } else {
                str = sb.substring(i, i + 8);
            }
            codeBytes[index] = (byte) Integer.parseInt(str, 2);// 转成byte
            index++;// 新数组的下标后移
        }
        return codeBytes;
    }

    /
     * 按照编码规则将原来byte数组转成新的二进制字符串
     */
    private static StringBuilder toHuffmanString(byte[] bytes, Map<Byte, String> codeRuleMap) {
        StringBuilder sb = new StringBuilder();
        for (byte b : bytes) {
            sb.append(codeRuleMap.get(b));
        }
        return sb;
    }

    /
     * 将list转换成赫夫曼树
     */
    private static CodeNode<Byte> toTree(List<CodeNode<Byte>> nodes) {
        while (nodes.size() > 1) {
            // 排序
            Collections.sort(nodes);
            // 取出两个节点
            CodeNode<Byte> left = nodes.remove(0);
            CodeNode<Byte> right = nodes.remove(0);
            // 组成父节点
            CodeNode<Byte> parent = new CodeNode<>(left, null,
                    left.weight + right.weight, right);
            // 将父节点添加到集合中
            nodes.add(parent);
        }
        return nodes.get(0);
    }
}

import java.util.HashMap;
import java.util.Map;
/
 * 编码,解码规则map数据
 *
 * @author lzlg
 * 2020/1/11 23:46
 */
public class RuleMapData {

    private static Map<Byte, String> codeRuleMap;

    /
     * 生成赫夫曼编码规则map
     * 规定,左边路径为0,右边路径为1
     */
    public static Map<Byte, String> codeRuleMap(CodeNode<Byte> huffmanTree) {
        if (huffmanTree == null) {
            return null;
        } else {
            codeRuleMap = new HashMap<>();
            codeRuleMap(huffmanTree.left, "0", new StringBuilder(), codeRuleMap);
            codeRuleMap(huffmanTree.right, "1", new StringBuilder(), codeRuleMap);
            return codeRuleMap;
        }
    }

    private static void codeRuleMap(CodeNode<Byte> node, String code, StringBuilder sb, Map<Byte, String> ruleMap) {
        StringBuilder newBuilder = new StringBuilder(sb);
        newBuilder.append(code);
        if (node != null) {
            if (node.data == null) { // 没有数据的节点不是叶子节点
                codeRuleMap(node.left, "0", newBuilder, ruleMap);
                codeRuleMap(node.right, "1", newBuilder, ruleMap);
            } else {
                ruleMap.put(node.data, newBuilder.toString());
            }
        }
    }

    /
     * 获取解码规则数据
     */
    public static Map<String, Byte> decodeRuleMap() {
        if (codeRuleMap == null) {
            return null;
        } else {
            Map<String, Byte> decodeRuleMap = new HashMap<>();
            for (Map.Entry<Byte, String> entry : codeRuleMap.entrySet()) {
                decodeRuleMap.put(entry.getValue(), entry.getKey());
            }
            return decodeRuleMap;
        }
    }
}

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/
 * 赫夫曼解码
 *
 * @author lzlg
 * 2020/1/11 21:28
 */
public class HuffmanDecode {
    /
     * 解码方法
     */
    public static byte[] decode(byte[] codeBytes) {
        // 把编码后的byte数组转换成二进制字符串
        String binaryString = toBinaryString(codeBytes);
        System.out.println("解码后的二进制字符串为:" + binaryString);
        // 将二进制字符串根据解码规则map进行解码
        return decode(binaryString);
    }

    private static byte[] decode(String binaryString) {
        // 获取解码规则map
        Map<String, Byte> decodeRuleMap = RuleMapData.decodeRuleMap();
        // 记录byte数据list
        List<Byte> list = new ArrayList<>();
        int len = binaryString.length();
        for (int i = 0; i < len; ) {
            int count = 1;
            Byte b;
            while (true) { // 每次从string中取出字符,看是否能从解码规则中找到对应的byte
                String s = binaryString.substring(i, i + count);
                b = decodeRuleMap.get(s);
                if (b == null) {
                    count++;
                } else {
                    break;
                }
            }
            list.add(b);
            i += count;
        }
        return listToArray(list);
    }

    /
     * list转换成byte数组
     */
    private static byte[] listToArray(List<Byte> list) {
        byte[] sourceBytes = new byte[list.size()];
        for (int i = 0; i < sourceBytes.length; i++) {
            sourceBytes[i] = list.get(i);
        }
        return sourceBytes;
    }

    /
     * 把编码后的byte数组转换成二进制字符串
     */
    private static String toBinaryString(byte[] bytes) {
        int len = bytes.length;
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < bytes.length; i++) {
            sb.append(toBinaryString(i < len - 1, bytes[i]));
        }
        return sb.toString();
    }

    /
     * 将byte转成字符串,flag表示是否补高位
     */
    private static String toBinaryString(boolean flag, byte b) {
        int temp = b;
        if (flag) {// 编码后的byte数组最后一位是直接截取的,不需要补高位
            temp |= 256;// 不补高位 b = 1 时,str = 1
        }
        String str = Integer.toBinaryString(temp);// int 4字节会转换成32位字符串
        if (flag) {
            return str.substring(str.length() - 8);
        } else {
            return str;
        }
    }
}

赫夫曼压缩

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectOutputStream;
import java.util.Map;
/
 * 压缩文件
 *
 * @author lzlg
 * 2020/1/12 1:04
 */
public class ZipFile {
    /
     * 压缩文件
     */
    public static void zip(String filePath, String destPath) {
        // 读取文件到byte数组
        try (FileInputStream fis = new FileInputStream(filePath);
             ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(destPath))) {
            byte[] bytes = new byte[fis.available()];
            fis.read(bytes);
            // 进行赫夫曼编码
            byte[] codeBytes = HuffmanCode.code(bytes);
            Map<String, Byte> decodeRuleMap = RuleMapData.getDecodeRuleMap();
            oos.writeObject(codeBytes);
            oos.writeObject(decodeRuleMap);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        String filePath = "D:\\test_zip.png";
        String destPath = "D:\\test.zip";
        zip(filePath, destPath);
    }
}

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.util.Map;
/
 * 解压文件
 *
 * @author lzlg
 * 2020/1/12 1:04
 */
public class UnZipFile {
    public static void main(String[] args) {
        String filePath = "D:\\test.zip";
        String destPath = "D:\\test_hhah.png";
        unzip(filePath, destPath);
    }

    public static void unzip(String filePath, String destPath) {
        try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(filePath));
             FileOutputStream fos = new FileOutputStream(destPath)) {
            byte[] codeBytes = (byte[]) ois.readObject();
            Map<String, Byte> decodeRuleMap = (Map<String, Byte>) ois.readObject();
            RuleMapData.setDecodeRuleMap(decodeRuleMap);
            byte[] bytes = HuffmanDecode.decode(codeBytes);
            fos.write(bytes);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

二叉排序树

二叉树的每个非叶子节点的左子节点的权值小于或等于该节点的权值,右子节点的权值大于或等于该节点的值的树,称为二叉排序树。

/
 * 二叉排序树
 *
 * @author lzlg
 * 2020/1/13 14:02
 */
public class BinarySortTree {

    private Node root;

    /
     * 二叉排序树的添加
     */
    public void add(int value) {
        if (root == null) {
            root = new Node(value);
        } else {
            root.add(new Node(value));
        }
    }

    /
     * 二叉排序树的中序遍历
     */
    public void infixList() {
        if (root == null) {
            System.out.println("二叉排序树为空,不能遍历");
        } else {
            root.infixOrder();
        }
    }

    /
     * 二叉排序树删除节点
     */
    public void delete(int value) {
        if (root == null) {
            System.out.println("二叉树为空,不能删除");
        } else {
            // 查找value的节点
            Node node = root.node(value);
            if (node == null) {
                System.out.println("没有查找到指定的节点,无法删除");
                return;
            }
            // 查找value节点的父节点
            Node parent = root.parent(value);

            // 情况有三种,1:当前节点是叶子节点,直接删除
            if (node.left == null && node.right == null) {
                if (parent == null) {// 如果只剩一个根节点
                    root = null;
                } else {
                    if (parent.left.value == value) {
                        parent.left = null;
                    } else {
                        parent.right = null;
                    }
                }
            } else if (node.left != null && node.right != null) {
                // 2.当前节点有左右子节点
                // 从左子树找到最大的值或从右子树查找最小的值(同时删除该值对应的节点)
                node.value = min(node.right);
            } else { // 3.当前节点只有一个子节点
                if (node.left != null) {
                    if (parent == null) { //防止只剩一个根节点和一个子节点的情况
                        root = node.left;
                    } else {
                        if (parent.left.value == value) {
                            parent.left = node.left;
                        } else {
                            parent.right = node.left;
                        }
                    }
                } else {
                    if (parent == null) {
                        root = node.right;
                    } else {
                        if (parent.left.value == value) {
                            parent.left = node.right;
                        } else {
                            parent.right = node.right;
                        }
                    }
                }
            }
        }
    }

    // 找到从当前节点下的最小节点的值,并删除
    private int min(Node node) {
        Node temp = node;
        while (temp.left != null) {
            temp = temp.left;
        }
        int value = temp.value;// 找到该节点的值
        delete(value);// 删除最小节点的值
        return value;
    }

    private static class Node {
        int value;
        Node left;
        Node right;

        private Node(int value) {
            this.value = value;
        }

        // 添加新节点
        private void add(Node node) {
            if (node == null) return;
            if (node.value < this.value) { // 新添加的节点值小于当前节点
                if (this.left == null) {// 添加到左子节点上
                    this.left = node;
                } else {
                    this.left.add(node);
                }
            } else {
                if (this.right == null) {
                    this.right = node;
                } else {
                    this.right.add(node);
                }
            }
        }

        // 中序遍历
        private void infixOrder() {
            if (this.left != null) {
                this.left.infixOrder();
            }
            System.out.println(this);
            if (this.right != null) {
                this.right.infixOrder();
            }
        }

        // 查找节点值是value的节点
        private Node node(int value) {
            if (this.value == value) {
                return this;
            }
            Node node = null;
            if (this.left != null && value < this.value) {
                // value小于当前节点,从左子节点查找
                node = this.left.node(value);
            }
            if (node != null) {// 如果查找到,直接返回,否则从右子节点查找
                return node;
            }
            if (this.right != null) {
                node = this.right.node(value);
            }
            return node;
        }

        // 查找节点的父节点
        private Node parent(int value) {
            if ((this.left != null && this.left.value == value) ||
                    (this.right != null && this.right.value == value)) {
                return this;
            }
            Node parent = null;
            if (this.left != null) {
                parent = this.left.parent(value);
            }
            if (parent != null) {
                return parent;
            }
            if (this.right != null) {
                parent = this.right.parent(value);
            }
            return parent;
        }

        @Override
        public String toString() {
            StringBuilder sb = new StringBuilder();
            sb.append("Node = ");
            if (this.left == null) {
                sb.append("null");
            } else {
                sb.append(this.left.value);
            }
            sb.append("_").append(this.value).append("_");
            if (this.right == null) {
                sb.append("null");
            } else {
                sb.append(this.right.value);
            }
            return sb.toString();
        }
    }
}

AVL树

AVL树:平衡二叉树,为了防止二叉排序树退化成链表的情况。它是一棵空树或它的左右两个子树的高度差的绝对值不 超过1,并且左右子树都是一棵平衡二叉树的二叉树。AVL树常见的实现方法为:红黑树,AVL树等。

// 左子树的高度
private int leftHeight() {
    return left == null ? 0 : left.height();
}

// 右子树的高度
private int rightHeight() {
    return right == null ? 0 : right.height();
}

// 计算树的高度
private int height() {
    return Math.max(left == null ? 0 : left.height(), right == null ? 0 : right.height()) + 1;
}

// 左旋转
private void leftRotate() {
    // 1.创建和当前节点值一样的新节点
    Node newNode = new Node(value);
    // 2.新节点的左子节点指向当前节点的左子节点
    newNode.left = left;
    // 3.新节点的右子节点指向当前节点的右子节点的左子节点
    newNode.right = right.left;
    // 4.将当前节点的右子节点的值赋值给当前节点
    value = right.value;
    // 5.当前节点的右子节点指向当前节点的右子节点的右子节点
    right = right.right;
    // 6.当前节点的左子节点指向新节点
    left = newNode;
}

// 右旋转
private void rightRotate() {
    // 1.创建和当前节点值一样的新节点
    Node newNode = new Node(value);
    // 2.新节点的右子节点指向当前节点的右子节点
    newNode.right = right;
    // 3.新节点的左子节点指向当前节点的左子节点的右子节点
    newNode.left = left.right;
    // 4.将当前节点的左子节点的值赋值给当前节点
    value = left.value;
    // 5.当前节点的左子节点指向当前节点的左子节点的左子节点
    left = left.left;
    // 6.当前节点的右子节点指向新节点
    right = newNode;
}

// 添加新节点
private void add(Node node) {
    if (node == null) return;
    if (node.value < this.value) { // 新添加的节点值小于当前节点
        if (this.left == null) {// 添加到左子节点上
            this.left = node;
        } else {
            this.left.add(node);
        }
    } else {
        if (this.right == null) {
            this.right = node;
        } else {
            this.right.add(node);
        }
    }

    // 添加时进行选择
    int leftHeight = leftHeight();
    int rightHeight = rightHeight();
    if (leftHeight + 1 < rightHeight) {// 如果左子树的高度 + 1 小于右子树的高度,进行左旋
        // 如果右子树的左子树高度大于右子树的高度,则先进行右旋
        if (right != null && right.leftHeight() > right.rightHeight()) {
            right.rightRotate();
        }
        leftRotate();
        return;
    }

    if (rightHeight + 1 < leftHeight) {// 如果右子树的高度 + 1 小于左子树的高度,进行右旋
        // 如果左子树的右子树高度大于左子树的高度,则先进行右旋
        if (left != null && left.rightHeight() > left.leftHeight()) {
            left.leftRotate();
        }
        rightRotate();
    }
}
// 测试方法
public static void main(String[] args) {
    BalanceBinaryTree tree = new BalanceBinaryTree();
    // 测试左旋
    //        int[] array = {4, 3, 6, 5, 7, 8};
    // 测试右旋
    //        int[] array = {10, 5, 11, 4, 6, 3};
    // 测试双旋转
    int[] array = {10, 5, 11, 4, 6, 7};
    for (int i = 0; i < array.length; i++) {
        tree.add(array[i]);
    }
    System.out.println("进行旋转的二叉树为:");
    tree.infixList();
    System.out.println("根节点为:" + tree.getRoot());
}

多路查找树

如果允许每个节点可以有更多的数据项和更多的子节点,就是多叉树。多叉树通过重新组织节点,减少了树的高度,能对二叉树进行优化。

B树:通过重新组织节点,降低树的高度,并且减少I/O的读写次数来提升效率。降低了树的高度;文件系统及数据库系统的设计利用了磁盘预读原理,将一个节点的大小设为等于一个页的大小(通常为4K),这样每个节点只需要一次I/O就可以完全载入。树的度:树种每个节点允许的最大子节点的数量称为度。

2-3树:是最简单的B树;特点:2-3树的所有叶子节点都在同一层(B树都满足);有两个子节点的节点叫二节点,二节点要么没有子节点,要么有两个子节点;有三个子节点的节点叫做三节点,三节点要么没有子节点,要么有三个子节点;2-3树是由二节点和三节点构成的树。

B树:Balanced Tree;B树的阶:节点的最多子节点个数;B树的搜索:从根节点开始,对节点内的关键字(有序)序列进行二分查找,如果命中则结束,否则进入查询关键字所属范围的儿子节点,重复,直到所对应的儿子指针为空,或已经是叶子节点;关键字集合分布在整棵树种,叶子节点和非叶子节点都存放数据;搜索有可能在非叶子节点结束;其搜索性能等价于在关键字全集内做一次二分查找。

B+树:是B树的变体。说明:B+树的搜索和B树基本相同,区别是B+树只有达到叶子节点才命中,其搜索性能等价于在关键字全集内做一次二分查找;所有关键字都出现在叶子节点的链表中(即数据只能在叶子节点:稠密索引),且链表中的关键字(数据)恰好是有序的;不可能在非叶子节点命中;非叶子节点相当于叶子节点的索引(稀疏索引),叶子节点相当于存储(关键字)数据的数据层;更适合文件索引系统。

B*树:是B+树的变体,在B+树的非根和非叶子节点再增加指向兄弟节点的指针。说明:定义了非叶子节点关键字个数至少为(2/3)x M,即块的最低使用率为2/3,而B+树的块的最低使用率为1/2;分配新节点的概率要比B+树低,空间使用率更高。

需要表示多对多关系时,使用图这种数据结构。节点可以有零个或多个相邻元素,两个节点之间的连接称为(edge),节点也可称为顶点(vertex)。无向图(顶点之间无方向),有向图(顶点之间有方向),带权图(图的边有权值)。

图的表示方式:二维数组(邻接矩阵),链表(邻接表)。邻接矩阵:表示图中顶点之间相邻关系的矩阵。邻接表:数组加链表组成,只关心存在的边。

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

/
 * 图的简单实现
 *
 * @author lzlg
 * 2020/1/17 16:24
 */
public class SimpleGraph {
    // 存放顶点
    private List<String> vertexList;
    // 存放边
    private int[][] edges;
    // 边的数目
    private int edgeCount;

    public SimpleGraph(int n) {
        vertexList = new ArrayList<>(n);
        edges = new int[n][n];
    }

    // 添加顶点
    public void addVertex(String vertex) {
        if (vertexList.size() == edges.length) {
            throw new IllegalArgumentException("can not add more vertex, is full");
        }
        vertexList.add(vertex);
    }

    // 添加一条边,v1是第一个顶点的下标,v2是第二个顶点的下标,weight是边的权值
    public void addEdge(int v1, int v2, int weight) {
        outOfBoundCheck(v1, v2);
        edges[v1][v2] = weight;
        edges[v2][v1] = weight;
        edgeCount++;
    }

    // 返回顶点的数量
    public int vertexCount() {
        return vertexList.size();
    }

    // 返回边的数量
    public int edgeCount() {
        return edgeCount;
    }

    // 根据下标获取顶点信息
    public String getVertex(int index) {
        return vertexList.get(index);
    }

    // 获取边的权值,v1是第一个顶点的下标,v2是第二个顶点的下标
    public int getWeight(int v1, int v2) {
        outOfBoundCheck(v1, v2);
        return edges[v1][v2];
    }

    private void outOfBoundCheck(int v1, int v2) {
        if (v1 < 0 || v1 > edges.length || v2 < 0 || v2 > edges.length) {
            throw new IndexOutOfBoundsException("out of max length");
        }
    }

    // 展示图的信息
    public void show() {
        int len = edges.length;
        for (String v : vertexList) {
            System.out.print("\t" + v);
        }
        System.out.println();
        for (int i = 0; i < len; i++) {
            System.out.print(vertexList.get(i));
            for (int j = 0; j < edges[i].length; j++) {
                System.out.print("\t" + edges[i][j]);
            }
            System.out.println();
        }
    }
}

/
 * @author lzlg
 * 2020/1/17 16:25
 */
public class Test {
    public static void main(String[] args) {
        SimpleGraph graph = createGraph();
    }

    private static SimpleGraph createGraph() {
        int n = 5;
        String[] vertexArray = {"A", "B", "C", "D", "E"};
        // 创建图
        SimpleGraph graph = new SimpleGraph(n);
        // 添加顶点信息
        for (String v : vertexArray) {
            graph.addVertex(v);
        }
        // 添加边信息
        // A
        graph.addEdge(0, 1, 1);
        graph.addEdge(0, 2, 1);
        // B
        graph.addEdge(1, 0, 1);
        graph.addEdge(1, 2, 1);
        graph.addEdge(1, 3, 1);
        graph.addEdge(1, 4, 1);
        // C
        graph.addEdge(2, 0, 1);
        graph.addEdge(2, 1, 1);
        // D
        graph.addEdge(3, 1, 1);
        // E
        graph.addEdge(4, 1, 1);

        graph.show();
        return graph;
    }
}

深度优先遍历

策略是:首先访问第一个邻接节点,然后再以这个被访问的邻接节点作为初始节点,访问它的第一个邻接节点。特点:优先往纵向挖掘深入,而不是对一个节点的所有邻接节点进行横向访问;是个递归的过程。

/
 * 获取下一个未访问过的节点
 * preVisited上一个已经访问过的节点下标
 * lastVisited最后一个已经访问过的节点下标
 */
private int getNext(int preVisited, int lastVisited) {
    int count = edges.length;
    for (int j = lastVisited + 1; j < count; j++) {
        if (edges[preVisited][j] > 0) {
            return j;
        }
    }
    return -1;
}

// 深度优先遍历
public void dfs() {
    int count = edges.length;
    // 用来标记顶点是否已访问过
    boolean[] visited = new boolean[count];
    for (int i = 0; i < count; i++) {
        int next = i;
        while (next != -1) {
            if (!visited[next]) {// 如果没有访问过
                System.out.print("-->" + getVertex(next));// 则进行访问
                visited[next] = true;// 标记已经访问过
                // 则寻找next节点后的第一个节点,深度优先遍历
                next = getNext(next, next);
            } else {// 如果已经访问过,则next为节点i对应的第二个节点
                next = getNext(i, next);
            }
        }
    }
}

广度优先遍历

public void bfs() {
    int count = edges.length;
    // 用来标记顶点是否已访问过
    boolean[] visited = new boolean[count];
    // 使用队列保存上一次遍历的节点信息
    Queue<Integer> queue = new LinkedList<>();
    for (int i = 0; i < count; i++) {
        queue.offer(i);// 将当前节点的下标添加到队列中
        while (!queue.isEmpty()) {// 队列不为空,进行遍历
            int p = queue.poll(); // 取出要遍历的当前节点
            int next = p; // 记录要遍历的下个节点
            while (next != -1) {
                if (!visited[next]) {// 如果没有访问过,则进行访问
                    System.out.print("==>" + getVertex(next));
                    visited[next] = true;// 标记已访问过
                }
                // 获取下一个要访问的节点,广度优先遍历
                next = getNext(p, next);
                if (next != -1) {// 将把要访问的节点加入到队列中
                    queue.offer(next);
                }
            }
        }
    }
}

常用算法

二分查找算法

二分查找算法在有序的数组中进行查找

/
 * 二分查找算法
 *
 * @author lzlg
 * 2020/1/17 19:41
 */
public class BinarySearch {
    public static void main(String[] args) {
        int[] array = {1, 3, 8, 9, 11, 16};
        int index = search(array, 10);
        System.out.println("index = " + index);
    }

    private static int search(int[] array, int target) {
        int l = 0;
        int r = array.length - 1;
        while (l < r) {
            int mid = (l + r) / 2;
            if (target > array[mid]) {
                l = mid + 1;
            } else if (target < array[mid]) {
                r = mid - 1;
            } else {
                return mid;
            }
        }
        return -1;
    }
}

分治算法

将问题分解为能解决的最小问题,然后将这些小问题的解进行合并,求出最终结果的算法。

动态规划算法

背包问题,有一个背包,容量为4磅,现有如下物品:

物品重量价格
吉他G11500
音响S43000
电脑L32000

要求:1)装入背包的总价值最大,且重量不超出。2)要求转入的物品不能重复。

算法介绍:将大问题分解为小问题进行解决,从而一步步取得最优解的处理算法;下一个子阶段的求解是建立在上一个子阶段的解的基础上,进行进一步的求解。可通过填表的方式逐步推进,得到最优解。

背包问题的填表过程

物品0磅1磅2磅3磅4磅
00000
吉他G01500150015001500
音响S01500150015003000
电脑L01500150020003500
/
 * 动态规划算法,背包问题
 *
 * @author lzlg
 * 2020/1/17 21:04
 */
public class KnapsackProblem {
    public static void main(String[] args) {
        int[] weight = {0, 1, 4, 3};// 物品的重量
        int[] value = {0, 1500, 3000, 2000};// 物品的价值
        int maxWeight = 4; // 背包的最大容量
        // 最大价值记录
        int[][] maxValue = new int[weight.length][maxWeight + 1];
        // 记录商品存放情况
        int[][] path = new int[weight.length][maxWeight + 1];

        for (int i = 1; i < weight.length; i++) {
            for (int j = 1; j < maxWeight + 1; j++) {
                if (weight[i] > j) {// 如果物品的重量大于当前容量
                    // 直接照搬上次的情况
                    maxValue[i][j] = maxValue[i - 1][j];
                } else {// 如果物品的重量不大于当前容量
                    // 上次的容量 和 最新的容量(物品当前的价值 + 减去当前物品重量的上一次最优)进行比较
                    int preValue = maxValue[i - 1][j];
                    int newValue = value[i] + maxValue[i - 1][j - weight[i]];
                    if (preValue > newValue) {
                        maxValue[i][j] = preValue;
                    } else {
                        path[i][j] = 1; // 记录最优解
                        maxValue[i][j] = newValue;
                    }
                }
            }
        }
        print(maxValue);
        System.out.println("++++++++++++++++++++++");
        System.out.println("找出最优解:");
        int i = path.length - 1;
        int j = path[0].length - 1;
        while (i > 0 && j > 0) {
            if (path[i][j] == 1) {
                System.out.printf("第%d的物品放入背包\n", i);
                j -= weight[i];
            }
            i--;
        }
    }

    private static void print(int[][] array) {
        for (int[] ints : array) {
            for (int i : ints) {
                System.out.printf("%10d", i);
            }
            System.out.println();
        }
    }
}

KMP算法

KMP算法(字符串匹配算法)是常用于一个文本串s内查找一个模式串p的出现位置的算法。利用之前判断过的信息,通过一个next数组保存模式串中前后最长公共子序列的长度,每次回溯时通过next数组找到前面匹配过的位置,省去了大量的计算时间。

KMP算法的核心,是一个被称为部分匹配表(Partial Match Table)的数组。对于字符串“abababca”,它的PMT如下表所示:

char:abababca
index:01234567
value:00123401

PMT中的值是字符串的前缀集合与后缀集合的交集中最长元素的长度。例如,对于”aba”,它的前缀集合为{”a”, ”ab”},后缀 集合为{”ba”, ”a”}。两个集合的交集为{”a”},那么长度最长的元素就是字符串”a”了,长度为1,所以对于”aba”而言,它在PMT表中对应的值就是1。再比如,对于字符串”ababa”,它的前缀集合为{”a”, ”ab”, ”aba”, ”abab”},它的后缀集合为{”baba”, ”aba”, ”ba”, ”a”}, 两个集合的交集为{”a”, ”aba”},其中最长的元素为”aba”,长度为3。

要在主字符串"ababababca"中查找模式字符串"abababca"。如果在 j 处字符不匹配,那么由于前边所说的模式字符串 PMT 的性质,主字符串中 i 指针之前的 PMT[j −1] 位就一定与模式字符串的第 0 位至第 PMT[j−1] 位是相同的。这是因为主字符串在 i 位失配,也就意味着主字符串从 i−j 到 i 这一段是与模式字符串的 0 到 j 这一段是完全相同的。而我们上面也解释了,模式字符串从 0 到 j−1 ,在这个例子中就是”ababab”,其前缀集合与后缀集合的交集的最长元素为”abab”, 长度为4。所以就可以断言,主字符串中i指针之前的 4 位一定与模式字符串的第0位至第 4 位是相同的,即长度为 4 的后缀与前缀相同。这样一来,我们就可以将这些字符段的比较省略掉。具体的做法是,保持i指针不动,然后将j指针指向模式字符串的PMT[j −1]位即可。

我们再看一下如何编程快速求得next数组。其实,求next数组的过程完全可以看成字符串匹配的过程,即以模式字符串为主字符串,以模式字符串的前缀为目标字符串,一旦字符串匹配成功,那么当前的next值就是匹配成功的字符串的长度。

具体参照文章: https://www.zhihu.com/question/21923021

BM算法,Sunday算法参考: https://blog.csdn.net/v_JULY_v/article/details/7041827

import java.util.Arrays;

/
 * 字符串匹配算法:KMP算法
 *
 * @author lzlg
 * 2020/1/28 14:08
 */
public class KMPMatch {
    public static void main(String[] args) {
        String str1 = "BBC ABCDAB ABCDABCDABDE";
        String str2 = "ABCDABD";
        int[] next = next(str2);
        System.out.println(Arrays.toString(next));
        System.out.println(match(str1, str2));
    }

    /
     * kmp算法
     */
    private static int match(String str1, String str2) {
        // 获取模式串的部分匹配表
        int[] next = next(str2);
        int str1Len = str1.length();
        int str2Len = str2.length();
        int i = 0, j = 0;
        while (i < str1Len && j < str2Len) {
            if (j == -1 || str1.charAt(i) == str2.charAt(j)) {
                i++;
                j++;
            } else {
                j = next[j];
            }
        }
        return j == str2Len ? i - j : -1;
    }

    /
     * 获取模式串的next数组
     */
    private static int[] next(String str) {
        char[] chars = str.toCharArray();
        int[] next = new int[chars.length];
        next[0] = -1;
        int i = 0, j = -1;
        int len = chars.length - 1;
        while (i < len) {
            if (j == -1 || chars[i] == chars[j]) {
                j++;
                i++;
                next[i] = j;
            } else {
                j = next[j];
            }
        }
        return next;
    }
}

贪心算法

贪心算法是指对问题求解时,在每一步选择中都采取最好或最优的选择,从而希望导致结果是最好或最优。算法所得到的结果不一定是最优解,但都是相对近似最优解的结果。

最佳应用--集合覆盖

存在如下表的广播台,以及广播台信号可以覆盖的地区,如何选择最少的广播台让所有的地区都能接收到信号。

广播台覆盖地区
K1北京,上海,天津
K2广州,北京,深圳
K3成都,上海,杭州
K4上海,天津
K5杭州,大连
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/
 * 贪心算法:集合覆盖问题
 *
 * @author lzlg
 * 2020/1/28 17:30
 */
public class GreedyAlgorithm {
    public static void main(String[] args) {
        // 创建广播电台,放入到Map中
        Map<String, Set<String>> map = new HashMap<>();
        map.put("K1", Stream.of("北京", "上海", "天津").collect(Collectors.toSet()));
        map.put("K2", Stream.of("广州", "北京", "深圳").collect(Collectors.toSet()));
        map.put("K3", Stream.of("成都", "上海", "杭州").collect(Collectors.toSet()));
        map.put("K4", Stream.of("上海", "天津").collect(Collectors.toSet()));
        map.put("K5", Stream.of("杭州", "大连").collect(Collectors.toSet()));

        Set<String> allAreas = map.values().stream()
                .flatMap(Collection::stream).collect(Collectors.toSet());
        System.out.println("所有要覆盖的区域有:");
        System.out.println(allAreas);

        // selected存储选择的电台结果
        List<String> selected = new ArrayList<>();
        // 存储当前要覆盖的电台区域
        Set<String> temp = new HashSet<>();
        while (!allAreas.isEmpty()) {
            String maxKey = null; // 要存储的最大未覆盖区域的电台key
            for (Map.Entry<String, Set<String>> entry : map.entrySet()) {
                temp.clear();
                temp.addAll(entry.getValue());
                temp.retainAll(allAreas);
                if (temp.size() > 0 && (maxKey == null || temp.size() > map.get(maxKey).size())) {
                    maxKey = entry.getKey();
                }
            }
            // maxKey不为空,则证明找到了,则添加到selected中,并删除maxKey对应的电台覆盖区域(此时已经覆盖)
            if (maxKey != null) {
                selected.add(maxKey);
                allAreas.removeAll(map.get(maxKey));
            }
        }

        System.out.println("被选择的电台有:");
        System.out.println(selected);
    }
}

普利姆算法

普利姆算法是求最小生成树的一种算法。最小生成树:给定一个带权的 无向连通图,如何选取一棵生成树,使树上所有边上权的总和为最小。特征:N个顶点,N-1条边;包含所有顶点;N-1条边都在图中。

算法流程:从第一个顶点A开始,访问从A能直接访问到的顶点结合,并宣传权值最小的边对应的节点B,并标记B已访问过;从A,B顶点开始,将A和B顶点和它们相邻的没有访问过的顶点进行处理,直到所有节点处理完毕。

/
 * 普利姆算法
 *
 * @author lzlg
 * 2020/2/11 14:59
 */
public class PrimAlgorithm {

    private static final int MAX = 10000;

    public static void main(String[] args) {
        char[] data = {'A', 'B', 'C', 'D', 'E', 'F', 'G'};
        int[][] weight = {{MAX, 5, 7, MAX, MAX, MAX, 2},
                {5, MAX, MAX, 9, MAX, MAX, 3},
                {7, MAX, MAX, MAX, 8, MAX, MAX},
                {MAX, 9, MAX, MAX, MAX, 4, MAX},
                {MAX, MAX, 8, MAX, MAX, 5, 4},
                {MAX, MAX, MAX, 4, 5, MAX, 6},
                {2, 3, MAX, MAX, 4, 6, MAX}};
        Graph graph = new Graph(data, weight);
        graph.show();
        prim(graph);
    }

    /
     * 普利姆算法
     */
    private static void prim(Graph graph) {
        // 记录顶点 是否访问过
        boolean[] visited = new boolean[graph.nums];
        // 从第一个顶点开始访问,可从任意一个节点访问
        visited[0] = true;
        // v1,v2用来记录访问的两个顶点
        int v1 = -1;
        int v2 = -1;

        int max = MAX;

        for (int i = 1; i < graph.nums; i++) { // 循环次数为图的顶点数-1
            // j用来标记已访问的顶点信息
            for (int j = 0; j < graph.nums; j++) {
                // k用来标记未访问的顶点信息
                for (int k = 0; k < graph.nums; k++) {
                    if (visited[j] && !visited[k] && graph.weight[j][k] < max) {
                        max = graph.weight[j][k];
                        v1 = j;
                        v2 = k;
                    }
                }
            }
            System.out.println("边<" + graph.data[v1] + ", " + graph.data[v2]
                    + ">,权值:" + graph.weight[v1][v2]);
            // 下次循环重置max的值
            max = MAX;
            // 顶点v2标记已访问过
            visited[v2] = true;
        }
    }

    private static class Graph {
        int nums; // 顶点数量
        char[] data; // 顶点数据
        int[][] weight; // 邻接矩阵

        public Graph(char[] data, int[][] weight) {
            nums = data.length;
            this.data = new char[nums];
            for (int i = 0; i < nums; i++) {
                this.data[i] = data[i];
            }
            this.weight = new int[nums][nums];
            for (int i = 0; i < nums; i++) {
                for (int j = 0; j < nums; j++) {
                    this.weight[i][j] = weight[i][j];
                }
            }
        }

        public void show() {
            for (char v : data) {
                System.out.printf("%10s", v);
            }
            System.out.println();
            for (int i = 0; i < nums; i++) {
                System.out.print(data[i]);
                for (int j = 0; j < nums; j++) {
                    System.out.printf("%10d", weight[i][j]);
                }
                System.out.println();
            }
        }
    }
}

克鲁斯卡尔算法

克鲁斯卡尔算法也是一种生成最小生成树的算法。算法流程:先对边按照权值进行排序,然后依次选择最小的权值边,然后判断是否构成回路,如果构成回路则不选择。如何判断是否构成回路:顶点的终点是否相同;终点:将所有顶点按照从大到小的顺序排好后,某个顶点的终点是与它连通的最大顶点。

import java.util.Arrays;
/
 * 克鲁斯卡尔算法
 *
 * @author lzlg
 * 2020/2/11 15:42
 */
public class KruskalAlgorithm {
    // 表示顶点不能连通
    private static final int INF = Integer.MAX_VALUE;

    public static void main(String[] args) {
        char[] data = {'A', 'B', 'C', 'D', 'E', 'F', 'G'};
        int[][] weight = {{INF, 12, INF, INF, INF, 16, 14},
                {12, INF, 10, INF, INF, 7, INF},
                {INF, 10, INF, 3, 5, 6, INF},
                {INF, INF, 3, INF, 4, INF, INF},
                {INF, INF, 5, 4, INF, 2, 8},
                {16, 7, 6, INF, 2, INF, 9},
                {14, INF, INF, INF, 8, 9, INF}};

        Graph graph = new Graph(data, weight);
        graph.show();
        kruskal(graph);
    }

    /
     * 克鲁斯卡尔算法
     */
    private static void kruskal(Graph graph) {
        Edge[] result = new Edge[graph.nums - 1]; // 结果数组
        int index = 0; // 结果数组下标

        int[] ends = new int[graph.nums]; // 记录下标为i的顶点的终点下标

        Edge[] edges = graph.getEdges();
        Arrays.sort(edges); // 对边进行排序

        for (int i = 0; i < graph.edgeNums; i++) {
            Edge edge = edges[i];
            // 取出当前边的两个顶点的下标
            int p1 = graph.index(edge.v1);
            int p2 = graph.index(edge.v2);
            // 查询这两个顶点的终点
            int m = end(ends, p1);
            int n = end(ends, p2);
            // m != n 则不构成回路
            if (m != n) {
                result[index++] = edge;
                // 此时更新ends数组,让下标为m顶点的终点指向n
                ends[m] = n;
            }
        }

        System.out.println("最小生成树为:");
        for (Edge edge : result) {
            System.out.println(edge);
        }
    }

    /
     * 返回下标为i的顶点的终点的下标
     *
     * @param ends 记录顶点的终点的下标
     * @param i    下标为i的顶点
     */
    private static int end(int[] ends, int i) {
        while (ends[i] != 0) {
            i = ends[i];
        }
        return i;
    }

    /
     * 边对象,并按照权值排序
     */
    private static class Edge implements Comparable<Edge> {
        char v1; // 边的一个顶点
        char v2; // 边的另一个顶点
        int weight; // 权值

        public Edge(char v1, char v2, int weight) {
            this.v1 = v1;
            this.v2 = v2;
            this.weight = weight;
        }

        @Override
        public String toString() {
            return "Edge{" +
                    "v1=" + v1 +
                    ", v2=" + v2 +
                    ", weight=" + weight +
                    '}';
        }

        @Override
        public int compareTo(Edge o) {
            return this.weight - o.weight;
        }
    }

    private static class Graph {
        int nums; // 顶点数量
        int edgeNums; // 边的数量
        char[] data; // 顶点数据
        int[][] weight; // 邻接矩阵

        public Graph(char[] data, int[][] weight) {
            nums = data.length;
            this.data = new char[nums];
            for (int i = 0; i < nums; i++) {
                this.data[i] = data[i];
            }
            this.weight = new int[nums][nums];
            for (int i = 0; i < nums; i++) {
                for (int j = 0; j < nums; j++) {
                    this.weight[i][j] = weight[i][j];
                }
            }
            // 统计边的数量
            for (int i = 0; i < nums; i++) {
                for (int j = i + 1; j < nums; j++) {
                    if (weight[i][j] != INF) {
                        edgeNums++;
                    }
                }
            }
        }

        // 顶点对应的下标
        public int index(char c) {
            for (int i = 0; i < nums; i++) {
                if (c == data[i]) return i;
            }
            return -1;
        }

        // 获取图对应的边的数组
        public Edge[] getEdges() {
            Edge[] edges = new Edge[edgeNums];
            int index = 0;
            for (int i = 0; i < nums; i++) {
                for (int j = i + 1; j < nums; j++) {
                    if (weight[i][j] != INF) {
                        edges[index++] = new Edge(data[i], data[j], weight[i][j]);
                    }
                }
            }
            return edges;
        }

        public void show() {
            for (char v : data) {
                System.out.printf("%15s", v);
            }
            System.out.println();
            for (int i = 0; i < nums; i++) {
                System.out.print(data[i]);
                for (int j = 0; j < nums; j++) {
                    System.out.printf("%15d", weight[i][j]);
                }
                System.out.println();
            }
        }
    }
}

迪克斯特拉算法

迪克斯特拉算法是求最短路径的一种算法,用于计算一个节点到其他节点的最短路径,主要特点是已起始点为中心向外层层扩展(广度优先搜索),直到扩展到终点为止。

import java.util.Arrays;
/
 * 迪杰斯特拉算法--最短路径算法
 *
 * @author lzlg
 * 2020/2/11 17:26
 */
public class DijkstraAlgorithm {

    private static final int MAX = 65535;

    public static void main(String[] args) {
        char[] data = {'A', 'B', 'C', 'D', 'E', 'F', 'G'};
        int[][] weight = {{MAX, 5, 7, MAX, MAX, MAX, 2},
                {5, MAX, MAX, 9, MAX, MAX, 3},
                {7, MAX, MAX, MAX, 8, MAX, MAX},
                {MAX, 9, MAX, MAX, MAX, 4, MAX},
                {MAX, MAX, 8, MAX, MAX, 5, 4},
                {MAX, MAX, MAX, 4, 5, MAX, 6},
                {2, 3, MAX, MAX, 4, 6, MAX}};
        Graph graph = new Graph(data, weight);
        graph.show();
        graph.dijkstra(6);
    }

    private static class Graph {
        int nums;
        char[] data;
        int[][] weight;
        // 记录顶点是否访问过
        boolean[] visited;
        // 记录起始顶点到其他顶点的距离
        int[] dis;
        // 记录顶点的前驱节点
        int[] pre;

        public Graph(char[] data, int[][] weight) {
            this.nums = data.length;
            this.data = data;
            this.weight = weight;

            this.visited = new boolean[nums];
            this.dis = new int[nums];
            Arrays.fill(dis, MAX);
            this.pre = new int[nums];
        }

        /
         * 迪杰斯特拉算法
         *
         * @param index 出发顶点的下标
         */
        public void dijkstra(int index) {
            if (index < 0 || index >= nums) {
                throw new IndexOutOfBoundsException();
            }
            // 初始化起始顶点的数据
            visited[index] = true;
            dis[index] = 0;
            update(index);

            for (int i = 1; i < nums; i++) {
                index = next();
                update(index);
            }
            // 显示结果
            showResult();
        }

        // 更新下标为index的节点访问后的数据
        public void update(int index) {
            for (int i = 0; i < nums; i++) {
                // 从起始顶点到出发顶点的权值和出发顶点到新节点的权值之和
                // 是否大于从起始顶点到新节点的权值之和,且没访问过
                int len = dis[index] + weight[index][i];
                if (!visited[i] && len < dis[i]) {
                    dis[i] = len;
                    pre[i] = index;
                }
            }
        }

        // 获取下一个可访问路径最短的顶点
        public int next() {
            int min = MAX, index = 0;
            for (int i = 0; i < nums; i++) {
                if (!visited[i] && dis[i] < min) {
                    min = dis[i];
                    index = i;
                }
            }
            // 注意更新该节点已经被访问过
            visited[index] = true;
            return index;
        }

        private void showResult() {
            for (int i = 0; i < nums; i++) {
                System.out.print(data[i] + "[visited=" + visited[i] + "] ");
            }
            System.out.println();
            for (int i = 0; i < nums; i++) {
                System.out.print(data[i] + "[pre=" + pre[i] + "] ");
            }
            System.out.println();
            for (int i = 0; i < nums; i++) {
                System.out.print(data[i] + "[dis=" + dis[i] + "] ");
            }
        }

        public void show() {
            for (char v : data) {
                System.out.printf("%10s", v);
            }
            System.out.println();
            for (int i = 0; i < nums; i++) {
                System.out.print(data[i]);
                for (int j = 0; j < nums; j++) {
                    System.out.printf("%10d", weight[i][j]);
                }
                System.out.println();
            }
        }
    }
}

弗洛伊德算法

弗洛伊德算法也是一种求最短路径的算法。和迪杰斯特拉算法的比较:迪杰斯特拉算法通过选定的被访问顶点,求出从出发访问顶点到其他顶点的最短路径;弗洛伊德算法算法中每一个顶点都是出发访问点,需将每一个顶点看做被访问顶点,求出每一个顶点到其他顶点的最短路径。

算法流程:经过3层for循环,把顶点作为中间顶点,出发顶点,终点顶点的情况全部考虑,然后更新邻接矩阵(最短距离)和前驱关系表(中间顶点)。

/
 * 弗洛伊德算法--求出所有顶点最短距离
 *
 * @author lzlg
 * 2020/2/11 19:38
 */
public class FloydAlgorithm {
    private static final int MAX = 65535;

    public static void main(String[] args) {
        char[] data = {'A', 'B', 'C', 'D', 'E', 'F', 'G'};
        int[][] weight = {{0, 5, 7, MAX, MAX, MAX, 2},
                {5, 0, MAX, 9, MAX, MAX, 3},
                {7, MAX, 0, MAX, 8, MAX, MAX},
                {MAX, 9, MAX, 0, MAX, 4, MAX},
                {MAX, MAX, 8, MAX, 0, 5, 4},
                {MAX, MAX, MAX, 4, 5, 0, 6},
                {2, 3, MAX, MAX, 4, 6, 0}};
        Graph graph = new Graph(data, weight);
        graph.floyd();
        graph.show();

    }

    private static class Graph {
        int nums;
        char[] data;
        int[][] weight;
        // 前驱节点信息
        char[][] preChar;

        public Graph(char[] data, int[][] weight) {
            this.nums = data.length;
            this.data = data;
            this.weight = weight;
            this.preChar = new char[nums][nums];
            for (int i = 0; i < nums; i++) {
                for (int j = 0; j < nums; j++) {
                    preChar[i][j] = data[i];
                }
            }
        }

        public void floyd() {
            // k:中间节点
            for (int k = 0; k < nums; k++) {
                // i:出发节点
                for (int i = 0; i < nums; i++) {
                    // j:终点节点
                    for (int j = 0; j < nums; j++) {
                        int len = weight[i][k] + weight[k][j];
                        if (len < weight[i][j]) {
                            weight[i][j] = len;
                            preChar[i][j] = preChar[k][j];
                        }
                    }
                }
            }
        }

        public void show() {
            for (int i = 0; i < nums; i++) {
                for (int j = 0; j < nums; j++) {
                    System.out.print(preChar[i][j] + " ");
                }
                System.out.println();
                for (int j = 0; j < nums; j++) {
                    System.out.print(data[i] + "到" + data[j] + "的最短距离为:" + weight[i][j] + "  ");
                }
                System.out.println();
                System.out.println();
            }
        }
    }
}

马踏棋盘算法

算法对应小游戏:http://www.4399.com/flash/146267_2.htm

算法思路:如果走通,则继续,走不通,则回溯。

import java.awt.*;
import java.util.ArrayList;
import java.util.List;

/
 * 马踏棋盘算法--骑士周游问题
 *
 * @author lzlg
 * 2020/2/11 20:22
 */
public class HorseChessboard {
    private static final int X = 8;// 棋盘的列数
    private static final int Y = 8;// 棋盘的行数
    private static boolean[] visited = new boolean[X * Y]; // 记录棋盘的各个位置是否已经访问过
    private static boolean finished = false; // 记录是否已经完成

    public static void main(String[] args) {
        horseTread(new Point(0, 0));
    }

    public static void horseTread(Point point) {
        System.out.println("start~~~~~~~~~~~~");
        int[][] checkerboard = new int[X][Y];
        long start = System.currentTimeMillis();
        horseTread(checkerboard, point.y, point.x, 1);
        long end = System.currentTimeMillis();
        System.out.println("共耗时:" + (end - start) + "毫秒");
        showCheckerboard(checkerboard);
    }

    private static void showCheckerboard(int[][] checkerboard) {
        for (int i = 0; i < X; i++) {
            for (int j = 0; j < Y; j++) {
                System.out.printf("%5d", checkerboard[i][j]);
            }
            System.out.println();
        }
    }

    /
     * 马踏棋盘
     *
     * @param checkerboard
     * @param step
     */
    private static void horseTread(int[][] checkerboard, int row, int col, int step) {
        // 记录棋盘的步数
        checkerboard[row][col] = step;
        // 该棋盘的位置已经访问过
        visited[row * X + col] = true;

        List<Point> nextPoints = getNextSteps(new Point(col, row));
        sortNextSteps(nextPoints);

        while (!nextPoints.isEmpty()) {
            Point p = nextPoints.remove(0);
            if (!visited[p.y * X + p.x]) {// 如果没有访问过,则递归调用该方法
                horseTread(checkerboard, p.y, p.x, step + 1);
            }
        }
        // 如果没有走通,则重置棋盘,和已访问数组信息
        if (step < X * Y && !finished) {
            checkerboard[row][col] = 0;
            visited[row * X + col] = false;
        } else {
            finished = true;
        }

    }

    /
     * 根据当前马的位置,计算出马能走的位置的集合
     *
     * @param point 注意 此时x 的表示列 y表示行
     * @return
     */
    private static List<Point> getNextSteps(Point point) {
        List<Point> list = new ArrayList<>();
        if ((point.x - 2) >= 0 && (point.y - 1) >= 0) {
            list.add(new Point(point.x - 2, point.y - 1));
        }

        if ((point.x - 1) >= 0 && (point.y - 2) >= 0) {
            list.add(new Point(point.x - 1, point.y - 2));
        }

        if ((point.x + 1) < X && (point.y - 2) >= 0) {
            list.add(new Point(point.x + 1, point.y - 2));
        }

        if ((point.x + 2) < X && (point.y - 1) >= 0) {
            list.add(new Point(point.x + 2, point.y - 1));
        }

        if ((point.x - 1) >= 0 && (point.y + 2) < Y) {
            list.add(new Point(point.x - 1, point.y + 2));
        }

        if ((point.x - 2) >= 0 && (point.y + 1) < Y) {
            list.add(new Point(point.x - 2, point.y + 1));
        }

        if ((point.x + 1) < X && (point.y + 2) < Y) {
            list.add(new Point(point.x + 1, point.y + 2));
        }

        if ((point.x + 2) < X && (point.y + 1) < Y) {
            list.add(new Point(point.x + 2, point.y + 1));
        }

        return list;
    }

    /
     * 将下一次要遍历的棋盘位置排序
     * 规则是:让他们按照下一次的能走的位置的数量 递增排序
     * 这样能使回溯的次数减少
     *
     * @param list
     */
    private static void sortNextSteps(List<Point> list) {
        list.sort((p1, p2) -> {
            int next1 = getNextSteps(p1).size();
            int next2 = getNextSteps(p2).size();
            if (next1 < next2) {
                return -1;
            } else if (next1 == next2) {
                return 0;
            } else {
                return 1;
            }
        });
    }
}
数据结构
算法
  • 作者:lzlg520
  • 发表时间:2020-01-16 01:46
  • 版权声明:自由转载-非商用-非衍生-保持署名
  • 公众号转载:请在文末添加作者公众号二维码