数据结构与算法
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 基本介绍
我们首先先看一个基本需求
编写的五子棋程序中,有存盘退出和续上盘的功能。
分析问题:因为该二维数组的很多值是默认值0, 因此记录了很多没有意义的数据 —> 稀疏数组。
当一个数组中大部分元素为0,或者为同一个值的数组时,可以用稀疏数组来保存该数组。
2.1.2 稀疏数组的处理方法
- 记录数组一共有几行几列,有多少个不同的值
- 把具有不同值的元素的行列及值记录在一个小规模数组中,从而缩小程序的规模
比如上述原始数组中的1,就是第1行索引,第2列索引,值为1。
2.1.3 二维数组转稀疏数组的思路
- 便利原始的二维数组,得到有效数据的个数sum
- 根据sum就可以创建稀疏数组
Integer sparseArr[][] = new Integer[sum+1][3]
2.1.4 稀疏数组转原始的二维数组的思路
- 先读取稀疏数组的第一行,根据第一行的数据,创建原始的二维数组,比如
Integer[11][11] chessArr2
- 再读取稀疏数组后几行的数据,并赋值给原始的二维数组即可
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 链表分类
3.2 单链表
链表是有序的列表,但是它在内存中的存储如下:
- 链表是以节点的方式来存储, 是链式存储
- 每个节点包含 data 域(存储数据),next 域(指向下一个节点)
- 链表的各个节点不一定是连续存储
- 链表分为带头节点的链表和没有头节点的链表,根据实际的需求来确定
3.2.1 单链表的应用实例
使用带 head 头的单向链表实现 –水浒英雄排行榜管理完成对英雄人物的增删改查操作
- 第一种方法在添加英雄时,直接添加到链表的尾部
- 第二种方式在添加英雄时, 根据排名将英雄插入到指定位置(如果有这个排名,则添加失败,并给出提示)
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 单链表的反转
/**
* 将单链表反转
*
* @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 双向链表分析
分析 双向链表的遍历,添加,修改,删除的操作思路
- 遍历方法和单链表一样,只是可以向前,也可以向后查找
- 添加(默认添加到双向链表的最后)
- 先找到双向链表的最后这个节点
- 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 的那个人又出列,依次类推,直到所有人出列为止,由 此产生一个出
队编号的序列。
提示:用一个不带头结点的循环链表来处理 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);
}
}