1 线性结构和非线性结构
数据结构包括:线性结构和非线性结构。
1.1 线性结构
- 线性结构作为最常用的数据结构,其特点是数据元素之间存在一对一的线性关系
- 线性结构有两种不同的存储结构,即顺序存储结构和链式存储结构。顺序存储的线性表称为顺序表,顺序表中的存储元素是连续的
- 链式存储的线性表称为链表,链表中的存储元素不一定是连续的,元素节点中存放数据元素以及相邻元素的地址信息
- 线性结构常见的有:数组、队列、链表和栈,后面我们会详细讲解.
1.2 非线性结构
非线性结构包括:二维数组,多维数组,广义表,树结构,图结构
2 稀疏数组和队列
2.1 稀疏sparsearray数组
2.1.1 先看一个实际的需求
- 编写的五子棋程序中,有存盘退出和续上盘的功能
- 分析问题:因为该二维数组的很多值是默认值0, 因此记录了很多没有意义的数据.->稀疏数组。
2.1.2 基本介绍
当一个数组中大部分元素为0,或者为同一个值的数组时,可以使用稀疏数组来保存该数组。
稀疏数组的处理方法是:
- 稀疏数组第一行记录原数组数组一共有几行几列,有多少个不同的值
- 把具有不同值的元素的行列及值记录在一个小规模的数组中,从而缩小程序的规模
2.1.3 应用实例
- 使用稀疏数组,来保留类似前面的二维数组(棋盘、地图等等)
- 把稀疏数组存盘,并且可以从新恢复原来的二维数组数
- 整体思路分析
代码实现:
/**
* @author Wnlife
* @create 2019-10-17 15:00
*
* 二维数组 -> 稀疏数组
* 稀疏数组 -> 二维数组
*
*/
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][3]=2;
chessArr1[4][5]=2;
//打印原始二维数组
System.out.println("二维数组~~");
Arrays.stream(chessArr1).forEach((n)->{
Arrays.stream(n).forEach((m)->System.out.printf("%d\t",m));
System.out.println();
});
/**
* 1.将二维数组转成稀疏数组
* ①遍历原始的二维数组,计算二维数组的有效数据个数sum
* ②根据得到的sum创建稀疏数组sparseArr int[sum+1][3]
* ③将二维数组的有效数据存储到稀疏数组
*/
//①遍历原始的二维数组,计算二维数组的有效数据个数sum
int sum=0;
for (int[] n : chessArr1) {
long count = Arrays.stream(n).filter((m) -> {
if (m != 0)
return true;
else
return false;
}).count();
sum += (int)count;
}
//②根据得到的sum创建稀疏数组sparseArr int[sum+1][3]
int[][] sparseArr = new int[sum + 1][3];
//③将二维数组的有效数据存储到稀疏数组
sparseArr[0][0]=11;
sparseArr[0][1]=11;
sparseArr[0][2]=sum;
int k=0;
for (int i = 0; i < chessArr1.length; i++) {
for (int j = 0; j < chessArr1.length; j++) {
if(chessArr1[i][j]!=0){
k++;
sparseArr[k][0]=i;
sparseArr[k][1]=j;
sparseArr[k][2]=chessArr1[i][j];
}
}
}
//输出稀疏数组的形式
System.out.println("稀疏数组~~");
Arrays.stream(sparseArr).forEach((n)->{
Arrays.stream(n).forEach((m)->System.out.printf("%d\t",m));
System.out.println();
});
/**
* 2.稀疏数组恢复为原来的二维数组
* ①先读取数组的第一行,根据第一行的数据,创建原始的二维数组,比如上面的chessArr2=new int[11][11];
* ②在读取稀疏数组的后面几行数组,并幅值给原始的二维数组即可。
*/
//①先读取数组的第一行,根据第一行的数据,创建原始的二维数组,比如上面的chessArr2=new int[11][11];
int[][] chessArr2 = new int[sparseArr[0][0]][sparseArr[0][1]];
for (int i = 1; i < sparseArr.length; i++) {
chessArr2[sparseArr[i][0]][sparseArr[i][1]]=sparseArr[i][2];
}
//数组恢复后的二维数组
System.out.println("数组恢复后的二维数组~~");
Arrays.stream(chessArr2).forEach((n)->{
Arrays.stream(n).forEach((m)->System.out.printf("%d\t",m));
System.out.println();
});
}
}
2.2 队列
2.2.1 队列的一个使用场景
银行排队的案例:
2.2.2 队列介绍
- 队列是一个有序列表,可以用数组或是链表来实现。
- 遵循先入先出的原则。即:先存入队列的数据,要先取出。后存入的要后取出
- 示意图:(使用数组模拟队列示意图)
2.2.3 数组模拟队列
- 队列本身是有序列表,若使用数组的结构来存储队列的数据,则队列数组的声明如下图, 其中 maxSize 是该队列的最大容量。
- 因为队列的输出、输入是分别从前后端来处理,因此需要两个变量 front及 rear分别记录队列前后端的下标,front 会随着数据输出而改变,而 rear则是随着数据输入而改变,如图所示:
- 当我们将数据存入队列时称为”addQueue”,addQueue 的处理需要有两个步骤:思路分析
- 将尾指针往后移:rear+1 , 当front == rear 【空】
- 若尾指针 rear 小于队列的最大下标 maxSize-1,则将数据存入 rear所指的数组元素中,否则无法存入数据。 rear == maxSize - 1[队列满]
代码实现:
由数组实现的简单队列
/**
* @author Wnlife
* @create 2019-10-17 18:05
*
* 数组模拟队列
*/
public class ArrayQueue {
private int maxSize;//表示数组的最大容量
private int front;//队列头
private int rear;//队列尾
private int[]arr;//该数据用于存放数据,模拟队列
//创建构造器
public ArrayQueue(int maxSize) {
this.maxSize = maxSize;
this.front=-1;//指向队列的头部,分析出front是指向队列头的前一个位置
this.rear=-1;//指向队列的尾部,指向队列的最后一个数据
arr=new int[maxSize];
}
//判断队列是否已满
public boolean isFull(){
return rear==maxSize-1;
}
//判断队列是否为空
public boolean isEmpty(){
return rear==front;
}
//添加数据到队列
public void addQueue(int n){
if (isFull()) {
System.out.println("队列已满~~");
return;
}
arr[++rear]=n;
}
//获取队列里面的数据
public int getQueue(){
if (isEmpty()) {
throw new RuntimeException("队列为空,不能取数据~~");
}
return arr[++front];
}
//显示对列里面的所有数据
public void showQueue(){
if (isEmpty()) {
System.out.println("队列空的,没有数据~~");
return;
}
for (int i = 0; i < arr.length; i++) {
System.out.printf("arr[%d]=%d\n",i,arr[i]);
}
}
//显示队列的头数据,注意不是取数据
public int headQueue(){
if (isEmpty()) {
throw new RuntimeException("队列为空,没有数据");
}
return arr[front+1];
}
}
测试由数组实现的队列:
/**
* @author Wnlife
* @create 2019-10-17 17:44
*
* 测试一:用数组 模拟 队列
*/
public class ArrayQueueTest {
public static void main(String[] args) {
//先创建一个队列
ArrayQueue queue=new ArrayQueue(3);
char key=' ';//接受用户输入
Scanner in=new Scanner(System.in);
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): 查看队列头的数据");
key=in.next().charAt(0);
switch (key){
case 's':
queue.showQueue();
break;
case 'e':
in.close();
loop=false;
break;
case 'a':
System.out.print("请输入一个数:");
int t = in.nextInt();
queue.addQueue(t);
break;
case 'g':
try {
int val = queue.getQueue();
System.out.printf("取出的数据为:%d\n",val);
} catch (Exception e) {
System.out.println(e.getMessage());
}
break;
case 'h':
try {
int head = queue.headQueue();
System.out.printf("队列头的数据是%d\n",head);
} catch (Exception e) {
System.out.println(e.getMessage());
}
break;
default:
break;
}
}
System.out.println("程序退出~~");
}
}
- 问题分析优化
1)目前数组使用一次就不能用,没有达到复用的效果
2)将这个数组使用算法,改进成一个环形的队列,取模:%
2.2.4 环形队列
对前面的数组模拟队列的优化,充分利用数组. 因此将数组看做是一个环形的。(通过取模的方式来实现即可)
分析说明:
1)尾索引的下一个为头索引时表示队列满,即将队列容量空出一个作为约定,
这个在做判断队列满的时候需要注意 (rear + 1) % maxSize == front 满]
2)rear == front [空]
3)分析示意图
代码实现:
由数组实现的环形队列:
/**
* @author Wnlife
* @create 2019-10-17 20:49
*
* 数组模拟环形队列
*/
public class CircleArrayQueue {
private int maxSize;//表示数组的最大容量
//front 变量的含义做一个调整: front 就指向队列的第一个元素, 也就是说 arr[front] 就是队列的第一个元素
//front 的初始值 = 0
private int front;
//rear 变量的含义做一个调整:rear 指向队列的最后一个元素的后一个位置. 因为希望空出一个空间做为约定.
//rear 的初始值 = 0
private int rear;
private int[]arr;//该数据用于存放数据,模拟队列
public CircleArrayQueue(int maxSize) {
this.maxSize = maxSize;
arr=new int[maxSize];
}
//判断队列是否已满
public boolean isFull(){
return (rear+1)%maxSize==front;
}
//判断队列是否为空
public boolean isEmpty(){
return rear==front;
}
//添加数据到队列
public void addQueue(int n){
if (isFull()) {
System.out.println("队列已满~~");
return;
}
//直接将数据加入
arr[rear]=n;
//将 rear 后移, 这里必须考虑取模
rear=(rear+1)%maxSize;
}
//获取队列里面的数据
public int getQueue(){
if (isEmpty()) {
throw new RuntimeException("队列为空,不能取数据~~");
}
// 这里需要分析出 front是指向队列的第一个元素
// 1. 先把 front 对应的值保留到一个临时变量
// 2. 将 front 后移, 考虑取模
// 3. 将临时保存的变量返回
int t=arr[front];
front=(front+1)%maxSize;
return t;
}
//显示对列里面的所有数据
public void showQueue(){
if (isEmpty()) {
System.out.println("队列空的,没有数据~~");
return;
}
for (int i = front; i < front+size(); i++) {
System.out.printf("arr[%d]=%d\n",i%maxSize,arr[i%maxSize]);
}
}
//计算当前队列有效数据的个数
public int size(){
return (rear+maxSize-front)%maxSize;
}
//显示队列的头数据,注意不是取数据
public int headQueue(){
if (isEmpty()) {
throw new RuntimeException("队列为空,没有数据");
}
return arr[front];
}
}
测试代码:
/**
* @author Wnlife
*
* 测试2:环形队列
*/
public class CircleArrayQueueTest {
public static void main(String[] args) {
//先创建一个队列
CircleArrayQueue queue=new CircleArrayQueue(4);
char key=' ';//接受用户输入
Scanner in=new Scanner(System.in);
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): 查看队列头的数据");
key=in.next().charAt(0);
switch (key){
case 's':
queue.showQueue();
break;
case 'e':
in.close();
loop=false;
break;
case 'a':
System.out.print("请输入一个数:");
int t = in.nextInt();
queue.addQueue(t);
break;
case 'g':
try {
int val = queue.getQueue();
System.out.printf("取出的数据为:%d\n",val);
} catch (Exception e) {
System.out.println(e.getMessage());
}
break;
case 'h':
try {
int head = queue.headQueue();
System.out.printf("队列头的数据是%d\n",head);
} catch (Exception e) {
System.out.println(e.getMessage());
}
break;
default:
break;
}
}
System.out.println("程序退出~~");
}
}
3 链表
3.1 链表(Linked List)介绍
链表是有序的列表,但是它在内存中是存储如下:
单链表(带头结点) 逻辑结构示意图如下:
3.2 单链表的应用示例
使用带head头的单向链表实现-水浒英雄排行榜管理对人物的增删改查操作
1)第一种方法在添加英雄时,直接添加到链表的尾部
思路分析示意图:
2)第二种方式在添加英雄时,根据排名将英雄插入到指定位置(如果有这个排名,则添加失败,并给出提示)
思路分析示意图:
3)修改节点功能
思路
①先找到该节点,通过遍历
②temp.name=newHeroNode.name; temp.nickname=newHeroNode.nickname
4)删除节点
思路分析的示意图:
代码实现:
/**
* @author Wnlife
* @create 2019-10-19 10:48
* <p>
* 单链表实例演示:使用带head头的单向链表实现-水浒英雄排行榜管理对人物的增删改查操作
*/
public class SingleLinkedListDemo {
public static void main(String[] args) {
HeroNode hero1 = new HeroNode(1, "宋江", "及时雨");
HeroNode hero2 = new HeroNode(2, "卢俊义", "玉麒麟");
HeroNode hero3 = new HeroNode(3, "吴用", "智多星");
HeroNode hero4 = new HeroNode(4, "林冲", "豹子头");
SingleLinkedList singleLinkedList = new SingleLinkedList();
//无序添加
/*singleLinkedList.add(hero1);
singleLinkedList.add(hero2);
singleLinkedList.add(hero3);
singleLinkedList.add(hero4);*/
//按照编号的顺序添加
singleLinkedList.addByOrder(hero1);
singleLinkedList.addByOrder(hero1);
singleLinkedList.addByOrder(hero2);
singleLinkedList.addByOrder(hero4);
singleLinkedList.addByOrder(hero3);
singleLinkedList.show();
System.out.println("修改后的链表~~");
singleLinkedList.update(new HeroNode(3, "小卢", "玉麒麟@@"));
singleLinkedList.show();
System.out.println("删除后的节点~~");
singleLinkedList.delete(4);
singleLinkedList.delete(3);
singleLinkedList.delete(2);
singleLinkedList.delete(1);
singleLinkedList.show();
}
}
/**
* 定义SingleLinkedList 管理英雄
*/
class SingleLinkedList {
private HeroNode head = new HeroNode(0, "", "");
/**
* 添加节点到单向链表
* 思路,当不考虑编号顺序时
* 1. 找到当前链表的最后节点
* 2. 将最后这个节点的next 指向 新的节点
*/
public void add(HeroNode heroNode) {
HeroNode temp = head;
while (temp.next != null) {
temp = temp.next;
}
temp.next = heroNode;
}
/**
* 第二种方式在添加英雄时,根据排名将英雄插入到指定位置
* (如果有这个排名,则添加失败,并给出提示)
*/
public void addByOrder(HeroNode heroNode) {
//因为头节点不能动,因此我们仍然通过一个辅助指针(变量)来帮助找到添加的位置
//因为单链表,因为我们找的temp 是位于 添加位置的前一个节点,否则插入不了
HeroNode temp=head;
while(temp.next!=null){
if(temp.next.no>heroNode.no)//找到位置插入
break;
else if (temp.next.no==heroNode.no){//要插入的编号存在
System.out.printf("准备插入的英雄的编号 %d 已经存在了, 不能加入\n", heroNode.no);
return;
}
temp=temp.next;
}
heroNode.next=temp.next;
temp.next=heroNode;
}
/**
* 修改链表:
* 修改节点的信息, 根据no编号来修改,即no编号不能改.
* 说明:根据 newHeroNode 的 no 来修改即可
*/
public void update(HeroNode newHeroNode){
//判断是否为空
if (head.next == null) {
System.out.println("链表为空~~");
return;
}
//找到要修改的节点,根据no编号
HeroNode temp=head.next;
while(temp!=null){
if(temp.no==newHeroNode.no){//找到要修改的节点
temp.name=newHeroNode.name;
temp.nickname=newHeroNode.nickname;
return;
}
temp=temp.next;
}
//没有找导要修改的节点
System.out.printf("没有找到 编号 %d 的节点,不能修改\n", newHeroNode.no);
}
/**
* 删除节点:
* 思路:
* 1. head 不能动,因此我们需要一个temp辅助节点找到待删除节点的前一个节点
* 2. 说明我们在比较时,是temp.next.no 和 需要删除的节点的no比较
*/
public void delete(int no){
HeroNode temp=head;
while(temp.next!=null){
if(temp.next.no==no){
HeroNode tt=temp.next;
temp.next=tt.next;
tt=null;
return;
}
temp=temp.next;
}
System.out.printf("要删除的 %d 节点不存在\n", no);
}
//输出链表的方法
public void show() {
if (head.next == null) {
System.out.println("链表为空~~");
return;
}
HeroNode temp = head.next;//从第二个节点开始输出
while (temp != null) {
System.out.println(temp);
temp = temp.next;
}
}
}
/**
* 定义HeroNode,每个HeroNode对象就是一个节点
*/
class HeroNode {
public int no;
public String name;
public String nickname;
public 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.求单链表中有效节点的个数(如果是带头结点的链表,需求不统计头结点)
public int getLength(HeroNode head) {
if (head.next == null) {
System.out.println("链表为空~~");
return 0;
}
HeroNode cur = head.next;
int length = 0;
while (cur != null) {
length++;
cur = cur.next;
}
return length;
}
//2.查找单链表中的倒数第k个结点
public HeroNode findKthToTail(HeroNode head, int k) {
HeroNode pAhead=head;
HeroNode pBehind=null;
for (int i = 0; i < k-1; i++) {
pAhead=pBehind.next;
}
pBehind=head;
while(pAhead!=null){
pAhead=pAhead.next;
pBehind=pBehind.next;
}
return pBehind;
}
//3.单链表的反转
public HeroNode reverse(HeroNode head){
HeroNode newHead=null;
HeroNode pre=null;
HeroNode cur=head;
while(cur!=null){
HeroNode next = cur.next;
if(next==null)
newHead=cur;
cur.next=pre;
pre=cur;
cur=next;
}
return newHead;
}
//4.从尾到头打印单链表
public void printLinkListByEnd(HeroNode head){
if(head!=null){
if(head.next!=null)
printLinkListByEnd(head.next);
System.out.println(head);
}
}
//5.合并两个有序的单链表,合并之后的链表依然有序
public HeroNode combinLinkedList(HeroNode head1,HeroNode head2){
//空指针判断
if (head1 == null) {
return head2;
}else if (head2 == null) {
return head1;
}
//递归合并
HeroNode combinHead=null;
if(head1.no>head2.no){
combinHead=head2;
combinHead.next=combinLinkedList(head1,head2.next);
}else{
combinHead.next=head1;
combinHead.next=combinLinkedList(head1.next,head2);
}
return combinHead;
}
3.3 双链表应用示例
3.3.1 双向链表的操作分析和实现
使用带head头的双向链表实现 –水浒英雄排行榜
管理单向链表的缺点分析:
- 单向链表,查找的方向只能是一个方向,而双向链表可以向前或者向后查找。
- 单向链表不能自我删除,需要靠辅助节点 ,而双向链表,则可以自我删除,所以前面我们单链表删除时节点,总是找到temp,temp是待删除节点的前一个节点(认真体会).
- 分析了双向链表如何完成遍历,添加,修改和删除的思路
对上图的说明:
双向链表的代码实现:
/**
* @author Wnlife
* @create 2019-10-20 16:16
* <p>
* 双向链表示例
*/
public class DoubleLinkedListDemo {
public static void main(String[] args) {
// 测试
System.out.println("双向链表的测试");
// 先创建节点
HeroNode2 hero1 = new HeroNode2(1, "宋江", "及时雨");
HeroNode2 hero3 = new HeroNode2(3, "吴用", "智多星");
HeroNode2 hero4 = new HeroNode2(4, "林冲", "豹子头");
HeroNode2 hero2 = new HeroNode2(2, "卢俊义", "玉麒麟");
// 创建一个双向链表
DoubleLinkedList doubleLinkedList = new DoubleLinkedList();
doubleLinkedList.add(hero1);
doubleLinkedList.add(hero2);
doubleLinkedList.add(hero3);
doubleLinkedList.add(hero4);
doubleLinkedList.show();
HeroNode2 newHeroNode = new HeroNode2(4, "公孙胜", "入云龙");
doubleLinkedList.update(newHeroNode);
System.out.println("修改后的链表情况");
doubleLinkedList.show();
// 删除
doubleLinkedList.delete(4);
System.out.println("删除后的链表情况~~");
doubleLinkedList.show();
}
}
/**
* 创建双向链表
*/
class DoubleLinkedList {
// 先初始化一个头节点, 头节点不要动, 不存放具体的数据
private HeroNode2 head = new HeroNode2(0, "", "");
// 返回头节点
public HeroNode2 getHead() {
return head;
}
//输出链表的方法
public void show() {
if (head.next == null) {
System.out.println("链表为空~~");
return;
}
HeroNode2 temp = head.next;//从第二个节点开始输出
while (temp != null) {
System.out.println(temp);
temp = temp.next;
}
}
// 添加一个节点到双向链表的最后.
public void add(HeroNode2 heroNode) {
HeroNode2 temp = head;
while (temp.next != null) {
temp = temp.next;
}
temp.next = heroNode;
heroNode.pre=temp;
}
/**
* 修改链表:
* 修改节点的信息, 根据no编号来修改,即no编号不能改.
* 说明:根据 newHeroNode 的 no 来修改即可
*/
public void update(HeroNode2 newHeroNode) {
//判断是否为空
if (head.next == null) {
System.out.println("链表为空~~");
return;
}
//找到要修改的节点,根据no编号
HeroNode2 temp = head.next;
while (temp != null) {
if (temp.no == newHeroNode.no) {//找到要修改的节点
temp.name = newHeroNode.name;
temp.nickname = newHeroNode.nickname;
return;
}
temp = temp.next;
}
//没有找导要修改的节点
System.out.printf("没有找到 编号 %d 的节点,不能修改\n", newHeroNode.no);
}
/**
* 删除节点:
* 思路:
* 1. head 不能动,因此我们需要一个temp辅助节点找到待删除节点的前一个节点
* 2. 说明我们在比较时,是temp.next.no 和 需要删除的节点的no比较
*/
public void delete(int no) {
// 判断当前链表是否为空
if (head.next == null) {// 空链表
System.out.println("链表为空,无法删除");
return;
}
HeroNode2 temp = head.next;
while (temp != null) {
if (temp.no == no) {
HeroNode2 tt = temp;
temp.pre.next = temp.next;
if (temp.next != null)
temp.next.pre = temp.pre;
tt = null;
return;
}
temp = temp.next;
}
System.out.printf("要删除的 %d 节点不存在\n", no);
}
/**
* 第二种方式在添加英雄时,根据排名将英雄插入到指定位置
* (如果有这个排名,则添加失败,并给出提示)
*/
public void addByOrder(HeroNode2 heroNode) {
//因为头节点不能动,因此我们仍然通过一个辅助指针(变量)来帮助找到添加的位置
HeroNode2 temp = head.next;
while (temp != null) {
if (temp.no > heroNode.no)//找到位置插入
break;
else if (temp.no == heroNode.no) {//要插入的编号存在
System.out.printf("准备插入的英雄的编号 %d 已经存在了, 不能加入\n", heroNode.no);
return;
}
temp = temp.next;
}
temp.pre.next=heroNode;
heroNode.pre=temp.pre;
heroNode.next=temp;
temp.pre=heroNode;
}
}
/**
* 定义HeroNode2,每个HeroNode对象就是一个节点
*/
class HeroNode2 {
public int no;
public String name;
public String nickname;
public HeroNode2 next;
public HeroNode2 pre;
public HeroNode2(int no, String name, String nickname) {
this.no = no;
this.name = name;
this.nickname = nickname;
}
@Override
public String toString() {
return "HeroNode2{" +
"no=" + no +
", name='" + name + '\'' +
", nickname='" + nickname + '\'' +
'}';
}
}
3.4单向环形链表的应用示例
Josephu(约瑟夫、约瑟夫环) 问题
Josephu 问题为:设编号为1,2,… n的n个人围坐一圈,约定编号为k(1<=k<=n)的人从1开始报数,数到m 的那个人出列,它的下一位又从1开始报数,数到m的那个人又出列,依次类推,直到所有人出列为止,由此产生一个出队编号的序列。
提示:用一个不带头结点的循环链表来处理Josephu 问题:先构成一个有n个结点的单循环链表,然后由k结点起从1开始计数,计到m时,对应结点从链表中删除,然后再从被删除结点的下一个结点又从1开始计数,直到最后一个结点从链表中删除算法结束。
3.4.1 单向环形链表简介
6.4.2 Josephu问题
Josephu 问题示意图:
- Josephu 问题
Josephu 问题为:设编号为1,2,… n的n个人围坐一圈,约定编号为k(1<=k<=n)的人从1开始报数,数到m 的那个人出列,它的下一位又从1开始报数,数到m的那个人又出列,依次类推,直到所有人出列为止,由此产生一个出队编号的序列。
- 提示
用一个不带头结点的循环链表来处理Josephu 问题:先构成一个有n个结点的单循环链表,然后由k结点起从1开始计数,计到m时,对应结点从链表中删除,然后再从被删除结点的下一个结点又从1开始计数,直到最后一个结点从链表中删除算法结束。
- 约瑟夫问题-创建环形链表的思路图解
- 约瑟夫问题-小孩出圈的思路分析
代码实现:
/**
* @author Wnlife
* @create 2019-10-20 18:07
*/
public class Josepfu {
public static void main(String[] args) {
CircleSingletonLinkedList circleSingletonLinkedList = new CircleSingletonLinkedList();
circleSingletonLinkedList.addBoy(5);
circleSingletonLinkedList.showBoy();
circleSingletonLinkedList.countBoy(1,2,5);
}
}
/**
* 创建一个环形的单向链表
*/
class CircleSingletonLinkedList {
// 创建一个first节点,当前没有编号
private Boy first = null;
// 添加小孩节点,构建成一个环形的链表
public void addBoy(int nums) {
// nums 做一个数据校验
if (nums < 1) {
System.out.println("nums的值不正确~~");
return;
}
Boy cur = null;// 辅助指针,帮助构建环形链表
// 使用for来创建我们的环形链表
for (int i = 1; i <= nums; i++) {
Boy boy = new Boy(i);
if (i == 1) {
first = boy;
first.next = first;
cur = boy;
} else {
cur.next = boy;
boy.next = first;
cur = boy;
}
}
}
/**
* @param startNo 表示从第几个小孩开始数数
* @param countNum 表示数几下
* @param nums 表示最初有多少小孩在圈中
*/
public void countBoy(int startNo, int countNum, int nums) {
//对数据进行校验
if (first == null || startNo < 1 || startNo > nums) {
System.out.println("参数输入有误, 请重新输入~~");
return;
}
//创建要给辅助指针,帮助小孩出圈
Boy helper = first;
while (helper.next != first) {//将helper指针移动到链表最后一个节点
helper = helper.next;
}
//让helper和first指向起始点
for (int i = 0; i < startNo-1; i++) {
helper=helper.next;
first=first.next;
}
//当小孩报数时,让first 和 helper 指针同时 的移动 m - 1 次, 然后出圈
//这里是一个循环操作,知道圈中只有一个节点
while (helper != first) {
for (int i = 0; i < countNum - 1; i++) {
helper=helper.next;
first=first.next;
}
//这时first指向的节点,就是要出圈的小孩节点
System.out.printf("小孩%d出圈\n", first.no);
Boy t=first;
//这时将first指向的小孩节点出圈
first=first.next;
helper.next=first;
t=null;
}
System.out.printf("最后留在圈中的小孩编号%d \n", first.no);
}
// 遍历当前的环形链表
public void showBoy() {
// 判断链表是否为空
if (first == null) {
System.out.println("没有小孩~~");
return;
}
// 因为first不能动,因此我们仍然使用一个辅助指针完成遍历
Boy cur = first;
do {
System.out.printf("小孩的编号 %d \n", cur.no);
cur = cur.next;
} while (cur != first);
}
}
/**
* 创建一个Boy类,表示一个链表的节点
*/
class Boy {
public int no;//编号
public Boy next;//下一个节点的指针
public Boy(int no) {
this.no = no;
}
@Override
public String toString() {
return "Boy{" +
"no=" + no +
'}';
}
}