数据结构与算法

1. 概述

1.1 数据结构与算法的重要性

算法是程序的灵魂,优秀的程序可以在海量数据计算的时候,依然保持高速计算。

一般来讲,程序使用了内存计算框架(比如Spark)和缓存技术(比如Redis)来优化程序,再深入思考一下,这些计算框架和缓存技术,

它的核心功能就是数据结构与算法。 拿实际工作经历说,在Unix下开发服务器程序,功能是要支持上千万人同时在线,在上线前,进行

测试都OK,可是上线后,服务器就撑不住了,公司的CTO对代码进行优化,再次上线,就坚如磐石。这是程序的灵魂——算法。

1.2 数据结构和算法的关系

  • 数据(data)结构(structure)是一门研究组织数据方式的学科,有了编程语言也就有了数据结构,学号数据结构可以编写出更加漂亮,更加有效率的代码。
  • 要学好数据结构就要多多考虑如何将生活中遇到的问题,用程序去解决。
  • 程序 = 数据结构 + 算法
  • 数据结构是算法的基础,换言之,想要学好算法,就要把数据结构学到位

1.3 线性结构与非线性结构

1.3.1 线性结构

  • 线性结构作为最常用的数据结构,其特点是数据元素之间存在一对一的线性关系。
  • 线性结构有两种不同的存储结构,即顺序存储结构链式存储结构。顺序存储结构的线性表称为顺序表,顺序表中的存储元素是连续的。
  • 链式存储的线性表称为链表,链表中的存储元素不一定是连续的,元素节点中存放数据元素以及相邻元素的地址信息。
  • 线性结构常见的有:数组、队列、链表和栈,后面我会相信讲解。

1.3.2 非线性结构

非线性结构包括:二维数组,多维数组,广义表,树结构,图结构

2. 稀疏数组

2.1 基本概述

2.1.1 基本介绍

我们首先先看一个基本需求

编写的五子棋程序中,有存盘退出和续上盘的功能。

java数据结构与算法电子书 java版数据结构与算法_算法

分析问题:因为该二维数组的很多值是默认值0, 因此记录了很多没有意义的数据 —> 稀疏数组。

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

2.1.2 稀疏数组的处理方法

  • 记录数组一共有几行几列,有多少个不同的值
  • 把具有不同值的元素的行列及值记录在一个小规模数组中,从而缩小程序的规模

java数据结构与算法电子书 java版数据结构与算法_数据结构_02

比如上述原始数组中的1,就是第1行索引,第2列索引,值为1。

2.1.3 二维数组转稀疏数组的思路

  • 便利原始的二维数组,得到有效数据的个数sum
  • 根据sum就可以创建稀疏数组Integer sparseArr[][] = new Integer[sum+1][3]

2.1.4 稀疏数组转原始的二维数组的思路

  • 先读取稀疏数组的第一行,根据第一行的数据,创建原始的二维数组,比如Integer[11][11] chessArr2
  • 再读取稀疏数组后几行的数据,并赋值给原始的二维数组即可

java数据结构与算法电子书 java版数据结构与算法_链表_03

2.2 代码实现

public class SparseArray {
    public static void main(String[] args) {
        // 创建一个原始的二维数组 11 * 11
        // 0:表示没有棋子,1表示黑子,2表示篮子
        int[][] chessArr1 = new int[11][11];
        chessArr1[1][2] = 1;
        chessArr1[2][4] = 2;

        // 输出原始的二维数组
        System.out.println("原始的二维数组是:");
        printArray(chessArr1);

        // 原始二维数组转换为稀疏数组
        // 1. 首先得到二维数组中非零的个数
        int row = chessArr1[0].length;
        int col = chessArr1.length;
        int count = 0;
        for (int i = 0; i < row; i++) {
            for (int j = 0; j < col; j++) {
                if (chessArr1[i][j] != 0) {
                    count++;
                }
            }
        }

        // 2. 创建对应的稀疏数组
        int[][] sparseArr = new int[count + 1][3];
        // 3. 给稀疏数组的第一行赋值
        sparseArr[0][0] = row;
        sparseArr[0][1] = col;
        sparseArr[0][2] = count;

        // 4. 对稀疏数组内部进行赋值
        // 进行计录第几个非零数据
        int times = 0;
        for (int i = 0; i < row; i++) {
            for (int j = 0; j < col; j++) {
                if (chessArr1[i][j] != 0) {
                    times++;
                    sparseArr[times][0] = i;
                    sparseArr[times][1] = j;
                    sparseArr[times][2] = chessArr1[i][j];
                }
            }
        }

        // 5. 输出稀疏数组的形式
        System.out.println("\n得到的稀疏数组");
        for (int[] ints : sparseArr) {
            System.out.printf("%d\t%d\t%d\t\n", ints[0], ints[1], ints[2]);
        }
        System.out.println();

        // 将稀疏数组恢复成原始数组
        // 1.先读取稀疏数组的第一行,根据第一行的数据,创建原始的二维数组
        int[][] chessArr2 = new int[sparseArr[0][0]][sparseArr[0][1]];

        // 2. 读取稀疏数组的后几行数据(从第二行开始),并复制给原始的二维数组即可
        for (int i = 1; i < sparseArr[0].length; i++) {
            chessArr2[sparseArr[i][0]][sparseArr[i][1]] = sparseArr[i][2];
        }

        // 3.输出恢复后的二维数组
        System.out.println("恢复后的二维数组~~");
        printArray(chessArr2);
    }

    /**
     * 打印棋盘的方法
     *
     * @param array 原始二维数组
     */
    public static void printArray(int[][] array) {
        for (int[] row : array) {
            for (int data : row) {
                System.out.printf("%d\t", data);
            }
            System.out.println();
        }
    }
}

3. 链表

3.1 链表分类

java数据结构与算法电子书 java版数据结构与算法_算法_04

3.2 单链表

链表是有序的列表,但是它在内存中的存储如下:

java数据结构与算法电子书 java版数据结构与算法_数组_05

  • 链表是以节点的方式来存储, 是链式存储
  • 每个节点包含 data 域(存储数据),next 域(指向下一个节点)
  • 链表的各个节点不一定是连续存储
  • 链表分为带头节点的链表和没有头节点的链表,根据实际的需求来确定

3.2.1 单链表的应用实例

使用带 head 头的单向链表实现 –水浒英雄排行榜管理完成对英雄人物的增删改查操作

  1. 第一种方法在添加英雄时,直接添加到链表的尾部

java数据结构与算法电子书 java版数据结构与算法_java_06

  1. 第二种方式在添加英雄时, 根据排名将英雄插入到指定位置(如果有这个排名,则添加失败,并给出提示)

java数据结构与算法电子书 java版数据结构与算法_数据结构_07

3.2.2 代码实现

@Data
public class HeroNode {
    /**
     * 英雄编号
     */
    private int no;

    /**
     * 英雄名称
     */
    private String name;

    /**
     * 英雄昵称
     */
    private String nickname;

    /**
     * 指向下一个节点
     */
    private HeroNode next;

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

    /**
     * 初始化头节点---不存放任何数据
     */
    private HeroNode head = new HeroNode(0, "", "");

    /**
     * 初始化一个尾节点,指向最后一个元素,默认等于head
     */
    private HeroNode tail = head;

    /**
     * 添加节点到单向链表
     * 不考虑编号顺序时
     * 1. 找到当前链表的最后节点
     * 2. 将最后这个节点的next指向新的节点
     *
     * @param heroNode 节点
     */
    public void add(HeroNode heroNode) {
        tail.setNext(heroNode);
        tail = heroNode;
    }

    /**
     * 第二种方式在添加英雄时, 根据排名将英雄插入到指定位置(如果有这个排名,则添加失败,并给出提示)
     *
     * @param heroNode 节点
     */
    public void addByOrder(HeroNode heroNode) {
        HeroNode currentNode = head;
        boolean flag = false;

        // 寻找插入的位置
        while (true) {
            if (currentNode.getNext() == null) {
                // 说明已经指向链表的最后
                break;
            }
            if (currentNode.getNext().getNo() > heroNode.getNo()) {
                // 说明已经找到位置
                break;
            } else if (currentNode.getNext().getNo() == heroNode.getNo()) {
                // 有该编号 表名重复
                flag = true;
                break;
            }
            // 上述情况都不满足 后移
            currentNode = currentNode.getNext();
        }

        // 插入
        if (flag) {
            // 表名重复 不能重复添加
            System.out.printf("准备插入的英雄编号%d已经存在了\n", heroNode.getNo());
        } else {
            heroNode.setNext(currentNode.getNext());
            currentNode.setNext(heroNode);
        }
    }

    /**
     * 修改节点的信息,根据编号来修改
     *
     * @param heroNode 节点
     */
    public void update(HeroNode heroNode) {
        // 判断是否为空
        if (isEmpty()) {
            System.out.println("链表为空!!!");
            return;
        }
        // 寻找该节点
        HeroNode currentNode = head.getNext();
        boolean flag = false;
        while (true) {
            if (currentNode == null) {
                // 遍历完毕 没有找到
                break;
            }
            if (currentNode.getNo() == heroNode.getNo()) {
                flag = true;
                break;
            }
            currentNode = currentNode.getNext();
        }
        // 进行修改
        if (flag) {
            // 找到该节点 进行修改
            currentNode.setName(heroNode.getName());
            currentNode.setNickname(heroNode.getNickname());
        } else {
            // 没有找到
            System.out.printf("没有找到编号为 %d 的节点,不能修改\n", heroNode.getNo());
        }
    }

    private boolean isEmpty() {
        return head.getNext() == null;
    }

    /**
     * 根据编号 删除节点
     *
     * @param no 英雄节点编号
     */
    public void delete(int no) {
        // 判断是否为空
        if (isEmpty()) {
            System.out.println("链表为空!!!");
            return;
        }
        // 寻找该节点的前一个节点
        HeroNode currentNode = head;
        boolean flag = false;
        while (true) {
            if (currentNode.getNext() == null) {
                break;
            }
            if (currentNode.getNext().getNo() == no) {
                // 找到待删除节点的前一个节点temp
                flag = true;
                break;
            }
            currentNode = currentNode.getNext();
        }

        // 判断是否找到
        if (flag) {
            // 找到 进行删除
            currentNode.setNext(currentNode.getNext().getNext());
        } else {
            System.out.printf("要删除的 %d 节点不存在\n", no);
        }
    }

    /**
     * 显示链表
     */
    public void list() {
        if (isEmpty()) {
            System.out.println("链表为空");
            return;
        }
        HeroNode currentNode = head.getNext();
        while (currentNode != null) {
            System.out.println(currentNode);
            currentNode = currentNode.getNext();
        }
    }
}

测试代码

public class SingleLinkedListDemo {
    public static void main(String[] args) {
        // 先创建节点
        HeroNode her1 = new HeroNode(1, "宋江", "及时雨");
        HeroNode her2 = new HeroNode(2, "卢俊义", "玉麒麟");
        HeroNode her3 = new HeroNode(3, "吴用", "智多星");
        HeroNode her4 = new HeroNode(4, "林冲", "豹子头");

        // 创建一个链表
        SingleLinkedList singleLinkedList = new SingleLinkedList();
        // 无顺序加入节点
//        singleLinkedList.add(her1);
//        singleLinkedList.add(her2);
//        singleLinkedList.add(her3);
//        singleLinkedList.add(her4);
        // singleLinkedList.add(her4); (不考虑排序)如果重复添加一个对象就会死循环,因为第一次添加到队尾的时候next还为空,再次添加next就为自己本身就死循环了

        // 按照编号顺序加入节点
        singleLinkedList.addByOrder(her1);
        singleLinkedList.addByOrder(her4);
        singleLinkedList.addByOrder(her2);
        singleLinkedList.addByOrder(her3);
        singleLinkedList.addByOrder(her3); // 不能重复插入

        // 显示
        singleLinkedList.list();

        // 测试修改节点
        HeroNode newHeroNode = new HeroNode(2, "小卢", "玉麒麟~~");
        singleLinkedList.update(newHeroNode);
        System.out.println("修改之后的链表存储情况:");
        singleLinkedList.list();

        // 删除节点
        singleLinkedList.delete(1);
        singleLinkedList.delete(4);
        singleLinkedList.delete(3);
        singleLinkedList.delete(2);
        System.out.println("删除之后的链表存储情况:");
        singleLinkedList.list();
    }
}

3.3 单链表面试题

3.3.1 求单链表中有效节点的个数

/**
* 方法:获取单链表的节点个数(如果是带头结点的链表,需求不统计头节点)
*
* @param head 链表的头节点
* @return 返回的就是有效节点的个数
*/
public static int getLength(HeroNode head) {
    if (head.getNext() == null) {
        return 0;
    }
    int count = 0;
    HeroNode currentNode = head.getNext();
    while (currentNode != null) {
        count++;
        currentNode = currentNode.getNext();
    }
    return count;
}

3.3.2 查找单链表的倒数第k个节点

/**
* 查找单链表的倒数第k个节点【新浪面试题】
* 两次遍历方法
*
* @param head  链表的头节点
* @param index 倒数位置索引
* @return 倒数index的节点
*/
public static HeroNode findLastIndexNode1(HeroNode head, int index) {
    if (head.getNext() == null) {
        return null;
    }
    // 首先得到链表的长度
    HeroNode currentNode = head.getNext();
    int length = 0;
    while (currentNode != null) {
        length++;
        currentNode = currentNode.getNext();
    }
    if (index <= 0 || index > length) {
        return null;
    }
    currentNode = head.getNext();
    // 进行寻找
    for (int i = 0; i < length - index; i++) {
        currentNode = currentNode.getNext();
    }
    return currentNode;
}

/**
* 查找单链表的倒数第k个节点
* 快慢指针方法
*
* @param head  链表的头节点
* @param index 倒数位置索引
* @return 倒数index的节点
*/
public static HeroNode findLastIndexNode2(HeroNode head, int index) {
    // 调用方法 找到正数第index个节点
    HeroNode quick = findFirstIndexNode(head, index);
    if (quick == null) {
        // 如果为空,说明链表长度太短,直接返回
        return null;
    }
    HeroNode slow = head.getNext();
    while (quick.getNext() != null) {
        quick = quick.getNext();
        slow = slow.getNext();
    }
    return slow;
}

/**
* @param head  链表的头节点
* @param index 正数位置索引
* @return 正数index的节点
*/
public static HeroNode findFirstIndexNode(HeroNode head, int index) {
    if (index <= 0) {
        return null;
    }
    if (head.getNext() == null) {
        return null;
    }
    // 这里需要获得正数第index节点,所以这里遍历从head.getNext()起,与findFirstIndexNode从head起不一致
    HeroNode temp = head;
    for (int i = 0; i < index; i++) {
        if (temp == null) {
            return null;
        }
        temp = temp.getNext();
    }
    return temp;
}

3.3.3 单链表的反转

java数据结构与算法电子书 java版数据结构与算法_算法_08

/**
* 将单链表反转
*
* @param head 头节点
*/
public static void reverseList1(HeroNode head) {
    // 如果当前链表为空,或者只有一个节点,则无需反转,直接返回
    if (head.getNext() == null || head.getNext().getNext() == null) {
        return;
    }
    // 定义一个辅助的指针(变量),帮助我们遍历原来的链表
    HeroNode currentNode = head.getNext();
    // 定义当前节点currentNode的下一个节点
    HeroNode next = null;
    HeroNode reverseHead = new HeroNode(0, "", "");
    while (currentNode != null) {
        // 先暂时保存当前节点的下一个节点,方便后面使用
        next = currentNode.getNext();
        // 将currentNode的下一个节点指向新链表的最前端
        currentNode.setNext(reverseHead.getNext());
        // 将currentNode插入到新链表的最前端
        reverseHead.setNext(currentNode);
        // 然后currentNode后移
        currentNode = next;
    }
    // 将head的next指向reverseHead的next
    head.setNext(reverseHead.getNext());
}

public static HeroNode reverseList2(HeroNode head) {
    if (head.getNext() == null || head.getNext().getNext() == null) {
        return head;
    }
    HeroNode currentNode = head.getNext();
    // 记录前驱节点
    HeroNode prevNode = null;
    HeroNode newHeroNode = new HeroNode(0, "", "");
    while (currentNode != null) {
        // 保存currentNode下一个节点的位置
        HeroNode temp = currentNode.getNext();
        currentNode.setNext(prevNode);
        prevNode = currentNode;
        currentNode = temp;
    }
    newHeroNode.setNext(prevNode);
    return newHeroNode;
}

3.3.4 从尾到头打印单链表【方式1:反向遍历,方式2:Stack栈】

public static void reversePrint(HeroNode head) {
    if (head.getNext() == null) {
        System.out.println("链表为空,无法反转");
        return;
    }
    HeroNode currentNode = head.getNext();
    Deque<HeroNode> stack = new ArrayDeque<>();
    while (currentNode != null) {
        stack.push(currentNode);
        currentNode = currentNode.getNext();
    }
    while (!stack.isEmpty()) {
        System.out.println(stack.pop());
    }
}

3.3.5 合并两个有序的单链表,合并之后的链表依然是有序的

/**
* 合并两个有序链表,合并后,链表仍是有序的
* 方法1:以其中一个链表为主,依次向这个链表中插入另一个链表的元素
*
* @param head1 头节点1
* @param head2 头节点2
* @return 合并后的有序链表
*/
public static HeroNode mergeList1(HeroNode head1, HeroNode head2) {
    // 以head1 为主
    HeroNode newHead = head1;
    HeroNode currentNode = head2.getNext();
    while (currentNode != null) {
        HeroNode temp = currentNode.getNext();
        // 首先将下个节点为null 然后进行比较插入
        currentNode.setNext(null);
        addByOrder(newHead, currentNode);
        currentNode = temp;
    }
    return newHead;
}

/**
* 将节点按照顺序新增到链表之中
*
* @param head     链表的头节点
* @param heroNode 代新增的头节点
*/
private static void addByOrder(HeroNode head, HeroNode heroNode) {
    HeroNode currentNode = head;
    boolean flag = false;
    while (true) {
        if (currentNode.getNext() == null) {
            // 说明已经是链表的最后
            break;
        }
        if (currentNode.getNext().getNo() > heroNode.getNo()) {
            // 已经找到了
            break;
        }
        if (currentNode.getNext().getNo() == heroNode.getNo()) {
            // 不能重复插入
            flag = true;
            break;
        }
        currentNode = currentNode.getNext();
    }

    if (flag) {
        System.out.printf("准备插入的英雄编号【%d】已经存在了\n", heroNode.getNo());
    } else {
        heroNode.setNext(currentNode.getNext());
        currentNode.setNext(heroNode);
    }
}

/**
* 方法二---直接将两个有序链表合并成一个新的有序链表
*
* @param head1 有序链表1
* @param head2 有序链表2
* @return 合并之后的链表
*/
public static HeroNode mergeList2(HeroNode head1, HeroNode head2) {
    HeroNode newHead = new HeroNode(0, "", "");
    HeroNode tail = newHead;
    newHead.setNext(tail);
    HeroNode temp1 = head1.getNext();
    HeroNode temp2 = head2.getNext();
    while (temp1 != null && temp2 != null) {
        Integer flag = compareNode(temp1, temp2);
        if (flag.equals(1)) { // 说明节点1大于节点2
            HeroNode next = temp2.getNext(); // 暂时存储temp2的下一个节点
            temp2.setNext(null); // 将temp2的next设为null
            tail.setNext(temp2); // 将temp2加入新的链表中
            tail = temp2; // tail变成了新增的节点temp2
            temp2 = next; // temp2后移
        } else if (flag.equals(0)) { // 说明节点1小于节点2
            HeroNode next = temp1.getNext(); // 暂时存储temp1的下一个节点
            temp1.setNext(null); // 将temp1的next设为null
            tail.setNext(temp1); // 将temp1加入到新的链表中
            tail = temp1; // tail变成了新增的节点temp1
            temp1 = next; // temp1后移
        } else { // 说明节点1和节点2的no相等,新增其中一个就行了
            HeroNode next1 = temp1.getNext();
            HeroNode next2 = temp2.getNext();
            tail.setNext(temp1);
            tail = temp1;
            temp1 = next1;
            temp2 = next2;
        }
    }
    if (temp1 == null) { // 说明是链表1先遍历到最后一个
        tail.setNext(temp2); // 不用考虑tail的值会和temp2的值相等,上面相等的话同时后移
    } else { // 说明是链表2先遍历到最后一个
        tail.setNext(temp1); // 不用考虑tail的值会和temp2的值相等,上面相等的话同时后移
    }
    return newHead;
}

/**
* 比较两个节点的no值,-1表示相等,1表示节点1大于节点2,0表示节点1小于节点2
*
* @param node1 节点1
* @param node2 节点2 
* @return result
*/
public static Integer compareNode(HeroNode node1, HeroNode node2) {
    if (node1.getNo() == node2.getNo()) {
        return -1;
    }
    return node1.getNo() > node2.getNo() ? 1 : 0;
}

3.4 双链表

单向链表的缺点分析

  • 单向链表,查找的方向只能是一个方向,而双向链表可以向前或者向后查找
  • 单向链表不能自我删除,需要靠辅助节点 ,而双向 链表,则可以自我删除,所以前面我们单链表删除时节点,总是找到temp,temp
    是待删除节点的前一 个节点

3.4.1 双向链表分析

java数据结构与算法电子书 java版数据结构与算法_数组_09

分析 双向链表的遍历,添加,修改,删除的操作思路

  • 遍历方法和单链表一样,只是可以向前,也可以向后查找
  • 添加(默认添加到双向链表的最后)
  • 先找到双向链表的最后这个节点
  • temp.next = newHeroNode
  • newHeroNode.pre = temp
  • 修改思路和原来的单向链表一样
  • 删除
  • 因为是双向链表,因此可以实现自我删除某个节点
  • 直接找到要删除的这个节点,比如 temp
  • temp.pre.next = temp.next
  • temp.next.pre = temp.pre

3.4.2 代码实现

@Data
public class HeroNode {
    private int no;
    private String name;
    private String nickname;

    /**
     * 后继节点
     */
    private HeroNode next;

    /**
     * 前驱节点
     */
    private HeroNode prev;

    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 + "]";
    }
}
public class DoubleLinkedList {

    /**
     * 初始化一个头节点,头节点不动,不存放具体的数据
     */
    private HeroNode head = new HeroNode(0, "", "");

    /**
     * 初始化一个尾节点,指向最后一个元素,默认等于head
     */
    private HeroNode tail = head;

    public HeroNode getHead() {
        return head;
    }

    /**
     * 链表是否为空
     *
     * @return true or false
     */
    public boolean isEmpty() {
        return head.getNext() == null;
    }

    /**
     * 修改一个节点的内容
     *
     * @param heroNode 修改的节点
     */
    public void update(HeroNode heroNode) {
        if (isEmpty()) {
            System.out.println("链表为空");
            return;
        }
        HeroNode currentNode = head.getNext();
        boolean flag = false;
        while (true) {
            if (currentNode == null) {
                break;
            }
            if (currentNode.getNo() == heroNode.getNo()) {
                // 找到节点
                flag = true;
                break;
            }
            currentNode = currentNode.getNext();
        }
        if (flag) {
            // 找到 进行修改
            currentNode.setName(heroNode.getName());
            currentNode.setNickname(heroNode.getNickname());
        } else {
            System.out.printf("没有找到编号为 %d 的节点,不能修改\n", heroNode.getNo());
        }
    }

    /**
     * 双向链表删除节点
     *
     * @param no 节点编号
     */
    public void delete(int no) {
        if (isEmpty()) {
            System.out.println("链表为空,无法删除!!!");
            return;
        }
        HeroNode currentNode = head.getNext();
        boolean flag = false;
        while (true) {
            if (currentNode == null) {
                // 没有该节点
                break;
            }
            if (currentNode.getNo() == no) {
                // 找到
                flag = true;
                break;
            }
            currentNode = currentNode.getNext();
        }
        if (flag) {
            // 找到 进行删除
            currentNode.getPrev().setNext(currentNode.getNext());
            // 如果是最后一个节点,就不需要指向下面这句话,否则会出现空指针 temp.getNext().setPre(null.getPre())
            if (currentNode.getNext() != null) {
                currentNode.getNext().setPrev(currentNode.getPrev());
            }
        }
    }

    /**
     * 直接新增节点
     *
     * @param heroNode 节点
     */
    public void add(HeroNode heroNode) {
        tail.setNext(heroNode);
        heroNode.setPrev(tail);
        tail = heroNode;
    }

    /**
     * 有序新增节点
     *
     * @param heroNode 节点
     */
    public void addByOrder(HeroNode heroNode) {
        HeroNode currentNode = head;
        boolean flag = false;   // 代表是否有重复
        while (true) {
            if (currentNode.getNext() == null) {
                break;
            }
            // 位置已经找到,应该在currentNode和currentNode.getNext()之间
            if (currentNode.getNext().getNo() > heroNode.getNo()) {
                break;
            }
            if (currentNode.getNext().getNo() == heroNode.getNo()) {
                flag = true;
                break;
            }
            currentNode = currentNode.getNext();
        }
        if (flag) {
            System.out.printf("准备插入的英雄编号【%d】已经存在了\n", heroNode.getNo());
        } else {
            heroNode.setNext(currentNode.getNext());
            // 判断链表是否在最后
            if (currentNode.getNext() != null) {
                currentNode.getNext().setPrev(heroNode);
            }
            currentNode.setNext(heroNode);
            heroNode.setPrev(currentNode);
        }
    }

    /**
     * 遍历打印双向链表的方法
     */
    public void list() {
        if (isEmpty()) {
            System.out.println("链表为空!!!");
            return;
        }
        HeroNode temp = head.getNext();
        while (temp != null) {
            System.out.println(temp);
            temp = temp.getNext();
        }
    }
}

3.5 单向循环链表

3.5.1 应用场景

Josephu(约瑟夫、约瑟夫环) 问题 Josephu 问题为:设编号为 1,2,… n 的 n 个人围坐一圈,约定编号为 k(1<=k<=n)的人从 1 开始报

数,数 到 m 的那个人出列,它的下一位又从 1 开始报数,数到 m 的那个人又出列,依次类推,直到所有人出列为止,由 此产生一个出

队编号的序列。

java数据结构与算法电子书 java版数据结构与算法_java_10

提示:用一个不带头结点的循环链表来处理 Josephu 问题:先构成一个有 n 个结点的单循环链表,然后由 k 结 点起从 1 开始计数,计到

m 时,对应结点从链表中删除,然后再从被删除结点的下一个结点又从 1 开始计数,直 到最后一个结点从链表中删除算法结束

3.5.2 算法分析

  • 根据用户的输入,生成一个小孩出圈的顺序
  • n=7,即有7个人
  • k=1,从第1个人开始报数
  • m=3,每次数3下
  • 需求创建一个辅助指针(变量)helper,事先应该指向环形链表的最后这一个节点
  • 小孩报数前,先让first和helper指针同时移动m-1此
  • 当这个小孩报数的时候,让first和hepler指针同时的移动m-1次
  • first = first.getNext()
  • helper.setNext(first)
  • 原来的first指向的节点就没有了任何引用(计数无法解决循环引用的问题,现在JVM都不使用引用计数算法了,不过这样确实会通过可
    达性分析算法被回收,是不可达对象)

3.5.3 代码实现

@Data
public class Boy {
    private int no;

    private Boy next;

    public Boy(int no) {
        this.no = no;
    }
}
public class CircleSingleLinkedList {

    /**
     * 创建一个first节点,当前没有编号
     */
    private Boy first = null;

    /**
     * 添加小孩节点,构建一个环形链表
     *
     * @param nums 共多少个节点
     */
    public void addBoy(int nums) {
        if (nums < 1) {
            System.out.println("nums的值不正确");
        }
        Boy temp = null;
        for (int i = 1; i <= nums; i++) {
            Boy boy = new Boy(i);
            // 如果是第一个小孩
            if (i == 1) {
                first = boy;
                // 构成环
                first.setNext(first);
                temp = first;
            } else {
                temp.setNext(boy);
                boy.setNext(first);
                temp = boy;
            }
        }
    }

    /**
     * 遍历当前的环形链表
     */
    public void showBoy() {
        if (first == null) {
            System.out.println("没有任何小孩~");
            return;
        }
        // 因为first不能动,所以我们还需要使用一个辅助指针完成遍历
        Boy temp = first;
        while (true) {
            System.out.printf("小孩的编号%d \n", temp.getNo());
            // 说明遍历完毕
            if (temp.getNext() == first) {
                break;
            }
            // temp后移
            temp = temp.getNext();
        }
    }

    /**
     * 根据用户的输入,计算出小孩出圈的顺序
     *
     * @param firstNo  表示小孩从第几个小孩开始报数
     * @param countNum 表示一次数几下
     * @param nums     表示最初有多少个小孩在圈中
     */
    public void countBoy(int firstNo, int countNum, int nums) {
        // 先对数据进行校验
        if (first == null || firstNo < 1 || firstNo > nums) {
            System.out.println("参数输入有误,请重新输入");
            return;
        }
        // 1. 创建一个前驱指针 用来指向环形链表的first的前一个节点
        Boy prev = first;
        while (prev.getNext() != first) {
            prev = prev.getNext();
        }
        // 2. 事先先让prev 和 first 移动firstNo-1次
        for (int i = 0; i < firstNo - 1; i++) {
            first = first.getNext();
            prev = prev.getNext();
        }
        // 开始循环报数出圈
        while (prev != first) {
            // 如果剩下一个节点 结束循环
            // 3. 小孩报数 让first和prev分别移动countNum-1次
            for (int i = 0; i < countNum - 1; i++) {
                prev = prev.getNext();
                first = first.getNext();
            }
            // 这时出圈的是first节点
            System.out.printf("小孩%d出圈\n", first.getNo());
            first = first.getNext();
            prev.setNext(first);
        }
        System.out.printf("最后留在圈中的小孩编号%d\n", first.getNo());
    }
    
    public static void main(String[] args) {
        // 测试构建环形链表和遍历
        CircleSingleLinkedList circleSingleLinkedList = new CircleSingleLinkedList();
        // 加入7个小孩
        circleSingleLinkedList.addBoy(7);
        circleSingleLinkedList.showBoy();

        // 测试小孩出圈 3->6->2->7->5->1->4
        circleSingleLinkedList.countBoy(1, 3, 7);
    }
}