1. 线性表
基本介绍
线性表:零个或多个数据元素的有限序列。
个人理解:线性表是数据结构中最简单的一种结构,也是后续其他数据结构的基础。元素与元素之间如同用一根线串起来。通过基础学习知道,任何一种线性数据结构在内存中都有两种物理结构,即在计算机中的存储形式,线性表的物理结构:顺序存储结构-数组形式实现、链式存储结构-链表形式实现。而在后续的数据结构学习中,都会用两种物理结构去表示该数据结构。因此数组和链表都是最基础的数据结构,他们是线性表的不同存储形式的体现,同时数组和链表也是其他列表(栈、队列等)的实现方式。
线性表的顺序存储结构:指的是用一段地址连续的存储单元,依次存储线性表的数据元素。可以用一维数组来实现顺序存储结构。
1.1 数组(Array)
理论基础
数组是存放在连续内存空间上的相同类型数据的集合,如图所示。
线性表长度和数组长度不同。数组长度是存放线性表的存储空间的长度,存储分配后一般是不变的。线性表的长度是指线性表中的数据个数,随着线性表的插入和删除操作进行,该值是变化的。因此在任意时刻,线性表的长度都应该小于等于数组的长度。
正是因为数组的在内存空间的地址是连续的,所以我们在删除或者增添元素的时候,就难免要移动其他元素的地址。
优点:无需为了表示表中元素之间的逻辑关系而增加额外的存储空间;可以快速的存取表中任一位置的元素。
缺点:插入和删除操作需要移动大量元素;当线性表长度变化较大时,难以确定存储空间的容量;造成存储空间的“碎片”。
![](https://img-blog.csdnimg.cn/img_convert/5b7d190d28193c0c283898aebba8336e.png)
数组的理论部分相对较少,但是数组的操作却非常的灵活,目前这里的数组仅仅是一维。个人认为数组的操作其实就是对指针(索引)的操作,无论是"快慢指针"、“双指针”、“滑动窗口问题”等,大同小异。
稀疏数组
应用场景
在编写的五子棋程序中,我们可以使用二维数组来记录棋局。假设1代表黑棋,2代表白棋,0代表无棋子。因为该二维数组的很多值都是默认值0,因此记录了很多没有意义的数据 -------引出 稀疏数组。
基本介绍
当一个数组中的大部分元素为0,或者为同一个值的数组时,可以使用稀疏数组来保存该数组。
稀疏数组的处理方法是:(1)记录数组一共有几行几列,有多少个不同的值;(2)把具有不同值的元素的行列及值记录在一个小规模的数组中,从而缩小程序的规模。
案例
![](https://img-blog.csdnimg.cn/img_convert/495879f8438f9d5c439dd8e8881138a3.png)
应用分析
使用稀疏数组,来保留类似前面的二维数组(棋盘、地图等等)
把稀疏数组存盘,并且可以重新恢复原来的二维数组
整体思路分析
二维数组 转 稀疏数组的思路
遍历 原始的二维数组,得到有效数据的个数 sum
根据 sum 就可以创建稀疏数组 sparseArr int[ sum+1 ] [ 3 ] 注:3是固定的,包括 行、列、值
将二维数组的有效数据存入到 稀疏数组中
稀疏数组 转 原始的二维数组的思路
先读取稀疏数组的第一行,根据第一行的数据,创建原始的二维数组,比如上面的 chessArr2 = int[ 11 ] [ 11 ]
在读取稀疏数组后几行的数据,并赋给 原始的二维数组即可。
代码实现
代码功能:完成了二维数组到稀疏数组的转换,又实现了稀疏数组到二维数组的转换,并将稀疏数组保存在电脑中(IO流)
import java.io.*;
public class ChessArr {
public static void main(String[] args) throws IOException {
//创建一个原始的二维数组 11*11
//0:表示没有棋子,1表示黑子 2表示白字
int chessArr1[][] = new int[11][11];
chessArr1[1][2] = 1;
chessArr1[2][3] = 2;
chessArr1[4][5] = 2;
//输出原始的二维数组
for (int[] row : chessArr1) {
for (int data : row) {
// System.out.print(data +" ");
System.out.printf("%d\t",data);//格式化输出
}
System.out.println();
}
//输出稀疏数组的形式
System.out.println("得到稀疏数组为~~");
int[][] sparseArray = returnSparseArray(chessArr1);
for (int i = 0; i < sparseArray.length; i++) {
for (int j = 0; j < sparseArray[i].length; j++) {
System.out.printf("%d\t",sparseArray[i][j]);
}
System.out.println();
}
//输出原二维数组
System.out.println("输出原来的二维数组~~");
int[][] chessArr = returnChessArr(sparseArray);
for (int i = 0; i < chessArr.length; i++) {
for (int j = 0; j < chessArr[i].length; j++) {
System.out.printf("%d\t",chessArr[i][j]);
}
System.out.println();
}
/**
* 从文件中存储 和 读取 数组
*/
String filePath = "D:\\map.data";
DataOutputStream dos = new DataOutputStream(new FileOutputStream(filePath));
dos.writeInt(sparseArray.length);
dos.writeInt(sparseArray[0].length);
for (int i = 0; i < sparseArray.length; i++) {
for (int j = 0; j < sparseArray[i].length; j++) {
dos.writeInt(sparseArray[i][j]);
}
}
dos.close();
DataInputStream dis = new DataInputStream(new FileInputStream(filePath));
int[][]spar = new int[dis.readInt()][dis.readInt()];
for (int i = 0; i < spar.length; i++) {
for (int j = 0; j < spar[i].length; j++) {
spar[i][j] = dis.readInt();
}
}
dis.close();
System.out.println("从文件中读取二维数组");
for (int i = 0; i < spar.length; i++) {
for (int j = 0; j < spar[i].length; j++) {
System.out.print(spar[i][j]+" ");
}
System.out.println();
}
}
/**
* 将二维数组 转 稀疏数组
*/
public static int[][] returnSparseArray(int[][]arr){
//1.先遍历二维数组,得到非0数据的个数
int sum = 0;
for (int i = 0; i < arr.length; i++) {
for (int j = 0; j < arr[i].length; j++) {
if(arr[i][j]!=0){
sum++;
}
}
}
//2.创建对应的稀疏数组
int[][] sparseArr = new int[sum+1][3];
// 给稀疏数组赋值:
sparseArr[0][0] = 11;
sparseArr[0][1] = 11;
sparseArr[0][2] = sum;
//3.遍历二维数组,将非0的值存放到稀疏数组中
int index = 0;//用于确定稀疏数组放非0值的行
for (int i = 0; i < arr.length; i++) {
for (int j = 0; j < arr[i].length; j++) {
if(arr[i][j]!=0){
index++;
sparseArr[index][0] = i;
sparseArr[index][1] = j;
sparseArr[index][2] = arr[i][j];
}
}
}
return sparseArr;
}
/**
* 将稀疏数组 转 二维原始数组
* 1. 先读取稀疏数组的第一行,根据第一行的数据,创建原始的二维数组,比如上面的 chessArr2 = int[ 11 ] [ 11 ]
* 2. 在读取稀疏数组后几行的数据,并赋给 原始的二维数组即可。
*/
public static int[][] returnChessArr(int[][] arr){
int row = arr[0][0];//行
int col = arr[0][1];//列
int[][] chessArr = new int[row][col];
for (int i = 1; i < arr.length; i++) {
chessArr[arr[i][0]][arr[i][1]] = arr[i][2];
}
return chessArr;
}
}
1.2 链表(Linked List)
基本介绍
链表是一种物理存储单元上非连续、非顺序的存储结构;数据元素的逻辑顺序是通过链表中的指针链接次序实现的。
链表由一系列结点(链表中每一个元素称为结点 Node)组成,结点可以在运行时动态生成。每个结点包括两个部分:一个是存储数据元素的数据域,另一个是存储下一个结点地址的指针域(单链表)。
链表在插入的时间复杂度 O(1);查找一个节点则时间复杂度 O(n)
链表的几种类型:单链表、双链表、循环链表等
单链表
理论基础
n 个节点链接成一个链表,即线性表(a1,a2,a3.....an)的链式存储结构,因为此链表的每个结点中只包含一个指针域,所以叫做单链表。
头指针:链表中第一个结点的存储位置,整个链表的存取就必须是从头指针开始进行
线性链表的最后一个结点指针为 空
有时候为了能更加方便对链表进行操作,会在单链表的第一个结点前附设一个结点,称为头结点。头结点的数据域可以不存储任何信息,也可以存储如线性长度等附加信息,头结点的指针域指向第一个结点的指针。此时头指针就是指向头结点的指针,而不是头结点里的指针域,头结点的指针域存放的是后继指针地址。
头指针和头结点的异同点:
头指针:头指针是指链表指向第一个结点的指针,若链表有头结点,则是指向头结点的指针。
头指针具有标识作用,所以常用头指针冠以链表的名字。
无论链表是否为空,头指针均不为空。头指针是链表的必要元素。
头结点:头结点是为了操作的统一和方便而设立的,放在第一元素的结点之前,其数据域一般无意义。
有了头结点,对第一元素结点前插入结点和删除第一结点,其操作与其他结点的操作就统一了。
头结点不一定是链表必须要素。
代码实现
java一切皆对象!!创建一个链表就是一个对象!!核心:工作指针(辅助指针)的后移
添加一:(创建)
先创建一个 head 头结点,作用就是表示单链表的头
后面我们每添加一个结点,就直接加入到链表的最后
//添加节点到单向链表
//思路:直接添加(即不考虑英雄编号的顺序)
//1.找到当前链表的最后节点
//2.将最后这个节点的next 指向新的节点。
public void add(HeroNode heroNode){
//因为 head 节点不能动,因此我们需要一个辅助指针(辅助变量)指向最后一个结点
HeroNode temp = head;
//遍历链表,找到最后
while(true){
//temp.next == null 找到链表的最后
if(temp.next == null){
break;
}
//如果没有找到最后,就将 temp 后移
temp = temp.next;
}
//当退出循环时,意味着 temp 指向了链表的最后结点
temp.next = heroNode;
}
添加二(按照数据域里面的编号添加,即单链表的插入)
首先找到新添加的结点(s)的位置,是通过辅助指针(辅助变量)【通过遍历来实现】
s.next = p.next
p.next = s 注意:顺序不能颠倒
//单链表的插入
//(如果有这个排名,则添加失败,并给出提示)
public void addByOrder(HeroNode heroNode){
//因为 head 头结点不能动,因此我们仍然需要一个辅助指针(辅助变量)
//因为是单链表,因此我们需要找的temp 是位于添加位置的前一个结点,否则加入不了!
HeroNode temp = head;
boolean flag = false;// 表示添加的编号是否存在,默认为 false。true表示存在
//循环的目的就是为了找到新结点插入的位置,等同于确定 temp 的位置
while(true){
if(temp.next == null){//说明temp 已经到链表的最后,此时 temp 确定
//不改变 flag 的值意味着可以插入
break;
}
if(temp.next.no > heroNode.no){//位置找到,就在temp 的后面
break;
}else if(temp.next.no == heroNode.no){//说明希望添加的编号已经存在
flag = true;//不可添加
break;
}
temp = temp.next;//后移,遍历当前链表
}
//当退出 while 循环时,意味着已经可以确定是否能添加或者添加的位置已确定
//判断 flag 的值 ----> 是否能添加
if(flag){//不能添加,说明编号存在
System.out.printf("准备插入的英雄编号%d 已经存在,不能添加\n",heroNode.no);
}else{
//插入到链表中,temp 的后面
heroNode.next = temp.next;
temp.next = heroNode;
}
}
遍历:
通过一个辅助指针(辅助变量),帮助遍历整个链表。
//显示链表【遍历】
public void list(){
//因为 head 不能动,因此同样需要一个辅助指针(辅助变量)帮助遍历结点
//先判断链表是否为空
if(head.next == null){
System.out.println("链表为空");
return;
}
HeroNode temp = head.next;
while(true){
//判断退出条件:指针指向链表的最后
if(temp == null){
break;
}
//输出结点的信息
System.out.println(temp);
//将 temp 后移
temp = temp.next;
}
}
修改
先找到该节点 temp【通过遍历】temp.no == newHeroNode.no
直接修改数据域。注意:不能修改 no。
//修改结点的信息,根据编号来修改,即 no 编号不能改。
//说明:根据 newHeroNode 的no来修改即可
public void update(HeroNode newHeroNode){
//判断是否空
if(head.next == null){
System.out.println("链表为空~");
return;
}
//找到需要修改的结点,根据 no 编号
//先定义一个辅助指针(辅助变量),对链表进行遍历
HeroNode temp = head.next;
boolean flag = false;//表示是否找到该结点,true表示找到
while(true){
if(temp == null){
break;//已经遍历结束
}
if(temp.no == newHeroNode.no){
//找到该结点
flag = true;
break;
}
temp = temp.next;
}
//退出循环,表示两种可能:
//1.遍历整个链表,但是没有找到该结点 --- flag = false
//2.找到了该结点 --- flag = true
//根据 flag 判断
if(flag){
temp.name = newHeroNode.name;
temp.nickName = newHeroNode.nickName;
}else{//没有找到
System.out.printf("没有找到编号%d 的结点,不能修改\n",newHeroNode.no);
}
}
删除
先找到需要删除的结点的前一个结点 temp
temp.next = temp.next.next
被删除的结点,将不会有其他引用指向,会被垃圾回收机制回收
//删除结点
//思路:head 头结点不能动,因此需要 temp 辅助结点 找到待删除结点的前一个结点
//说明:在比较时,是temp.next.no 和 需要删除的结点的no 比较
public void delete(int no){
//注意:可以仿照 修改方法中的,先判断是否为空,再 HeroNode temp = head.next;
HeroNode temp = head;
boolean flag = false;//表示是否找到待删除的点。true表示找到
while(true){
if(temp.next == null){//表示已到最后一个结点
break;
}
if(temp.next.no == no){
//找到了待删除结点的前一个结点
flag = true;
break;
}
temp = temp.next;//temp 后移,遍历
}
//判断 flag
if(flag){
//可以删除
temp.next = temp.next.next;
}else{
System.out.printf("要删除的%d 结点不存在\n",no);
}
}
整表删除思路
首先声明两个辅助变量(辅助指针)p 和 q。p用来循环删除,q用来记录下一个结点
将 head 头结点 赋给 p
循环:
将下一个结点赋给 q
删除 q
将 q 的值赋给 p。
//整表删除
public void clearList(){
HeroNode p = head.next;
HeroNode q = null;
while(true){
if(p == null){
break;
}
q = p;
delete(p.no);
p = q;
p = p.next;
}
head = null;
}
}
完整代码
//定义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;
}
//为了显示方便,重写 toString 方法
@Override
public String toString() {
return "HeroNode{" +
"no=" + no +
", name='" + name + '\'' +
", nickName='" + nickName + '\'' +
'}';
}
}
小结
单链表结构和顺序存储结构对比:
存储分配方式:顺序存储结构用一段连续的存储单元依次存储线性表的数据元素;单链表采用链式存储结构,用一组任意的存储单元存放线性表的元素。
时间性能:
查找:顺序存储结构 O(1);单链表 O(n)
插入和删除:顺序存储结构需要平均移动表长一半的元素,时间为 O(n);单链表再找出某位置的指针后,插入和删除的时间仅为 O(1)
空间性能:顺序存储结构需要预分配存储空间,分太大浪费,分小了易发生上溢;单链表不需要分配存储空间,只要有就可以分配,元素的个数不受到限制。
若线性表需要频繁查找,很少进行插入和删除操作时,宜采用顺序存储结构。反之宜采用单链表结构
当线性表中的元素个数变化较大时或者根本不知道元素的个数是多少时,采用单链表结构。
合并两个有序的单链表,合并之后的链表依然有序
基本思路:因为两个链表本身有序,因此通过两个单链表的头结点遍历两个有序单链表l1和l2,比较节点的值,将较小的节点添加到新链表中。最后将剩余的节点添加到新链表中,返回新链表的头结点即可。
public static HeroNode Merge(HeroNode head1,HeroNode head2){
//如果有一个链表为空,则直接返回另一个链表
if(head1.next == null){
return head2;
}else if(head2.next == null){
return head1;
}
HeroNode mergeHead = new HeroNode(0,"","");
HeroNode temp1 = head1.next;
HeroNode temp2 = head2.next;
HeroNode cur = mergeHead;
//遍历两个链表,比较每个结点的值,将较小的结点添加到新链表中
while(temp1 != null && temp2 != null){
if(temp1.no > temp2.no){
cur.next = temp2;
temp2 = temp2.next;
}else{
cur.next = temp1;
temp1 = temp1.next;
}
cur = cur.next;
}
//将剩余的结点添加到新链表中
if(temp1 != null){
cur.next = temp1;
}else if(temp2 != null){
cur.next = temp2;
}
return mergeHead;
}
双向链表
理论基础
单向链表的不足:查找方向只能是一个方向;双向链表可以实现向前或向后查找。单向链表不能自我删除,需要依靠辅助结点,而双向链表可以自我删除。
定义:双向链表是在单链表的每个结点中,再设置一个指向其前驱结点的指针域。所以双向链表中的结点都有两个指针域,一个指向直接后继,一个指向直接前驱。
代码实现
遍历:方法和单链表一样,只是可以向前,也可以向后查找
添加(默认添加至双向链表的最后):
先通过遍历,找到双向链表的最后一个结点(temp)
temp.next = newNode;
newNode.pre = temp;
修改:和原来的单链表一样
通过数据域找到该结点(判断该节点是否存在)
修改结点的内容
删除:
先遍历,找到待删除的结点 cur -----> 可以直接找到待删除的结点,实现自我删除
cur.pre.next = cur.next;
cur.next.pre = cur.pre;//注意:需要判断一下后面的结点是否为 null,如果为空则不执行该句 temp.next != null
插入:按照数据域的id的值添加
先找到待插入位置的前一个结点 p;新节点为 s
s.pre = p;
s.next = p.next;
p.next.pre = s;
p.next = s;
注意:该顺序不能反,可以理解为先搞定 s 的前驱和后继,再搞定后结点的前驱,最后解决前结点的后继。个人理解:先将新节点串入双向链表中,然后将后一个结点与新节点连接好,最后改变 p.next !! 因为如果 后面那个结点没有先和新节点连接好,此时改动 p.next,则后一个结点就找不到了。
/**
* 双向链表应用实例
*/
public class DoubleLinkedListDemo {
public static void main(String[] args) {
//进行测试
//先创建结点
HeroNode2 hero1 = new HeroNode2(1, "宋江", "及时雨");
HeroNode2 hero2 = new HeroNode2(2, "卢俊义", "玉麒麟");
HeroNode2 hero3 = new HeroNode2(3, "吴用", "智多星");
HeroNode2 hero4 = new HeroNode2(4, "林冲", "豹子头");
DoubleLinkedList doubleLinkedList = new DoubleLinkedList();
doubleLinkedList.add(hero1);
doubleLinkedList.add(hero2);
doubleLinkedList.add(hero4);
doubleLinkedList.add(hero3);
doubleLinkedList.list();
System.out.println("-------------------------------------------------------");
doubleLinkedList.delete(4);
doubleLinkedList.update(new HeroNode2(1, "小宋", "及时雨"));
doubleLinkedList.list();
HeroNode2 hero7 = new HeroNode2(7, "java7", "豹子头");
HeroNode2 hero8 = new HeroNode2(8, "java8", "豹子头");
HeroNode2 hero9 = new HeroNode2(9, "java9", "豹子头");
System.out.println("-------------------------------------------------------");
doubleLinkedList.addByOrder(hero7);
doubleLinkedList.addByOrder(hero9);
doubleLinkedList.addByOrder(hero8);
doubleLinkedList.list();
}
}
//创建一个双向链表的类
class DoubleLinkedList{
//先初始化一个头结点,头结点不要动,不存放具体的数据
private HeroNode2 head = new HeroNode2(0,"","");
//返回头结点
public HeroNode2 getHead() {
return head;
}
//遍历双向链表的方法
public void list(){
//因为 head 不能动,因此同样需要一个辅助指针(辅助变量)帮助遍历结点
//先判断链表是否为空
if(head.next == null){
System.out.println("链表为空");
return;
}
HeroNode2 temp = head.next;
while(true){
//判断退出条件:指针指向链表的最后
if(temp == null){
break;
}
//输出结点的信息
System.out.println(temp);
//将 temp 后移
temp = temp.next;
}
}
//添加节点到双向链表
//思路:直接添加(即不考虑英雄编号的顺序)
//1. 先通过遍历,找到双向链表的最后一个结点(temp)
//2. temp.next = newNode;
//3. newNode.pre = temp;
public void add(HeroNode2 heroNode){
//因为 head 节点不能动,因此我们需要一个辅助指针(辅助变量)指向最后一个结点
HeroNode2 temp = head;
//遍历链表,找到最后
while(true){
//temp.next == null 找到链表的最后
if(temp.next == null){
break;
}
//如果没有找到最后,就将 temp 后移
temp = temp.next;
}
//当退出循环时,意味着 temp 指向了链表的最后结点
temp.next = heroNode;
heroNode.pre = temp;
}
//修改结点的信息,根据编号来修改,即 no 编号不能改。
//说明:根据 newHeroNode 的no来修改即可
public void update(HeroNode2 newHeroNode){
//判断是否空
if(head.next == null){
System.out.println("链表为空~");
return;
}
//找到需要修改的结点,根据 no 编号
//先定义一个辅助指针(辅助变量),对链表进行遍历
HeroNode2 temp = head.next;
boolean flag = false;//表示是否找到该结点,true表示找到
while(true){
if(temp == null){
break;//已经遍历结束
}
if(temp.no == newHeroNode.no){
//找到该结点
flag = true;
break;
}
temp = temp.next;
}
//退出循环,表示两种可能:
//1.遍历整个链表,但是没有找到该结点 --- flag = false
//2.找到了该结点 --- flag = true
//根据 flag 判断
if(flag){
temp.name = newHeroNode.name;
temp.nickName = newHeroNode.nickName;
}else{//没有找到
System.out.printf("没有找到编号%d 的结点,不能修改\n",newHeroNode.no);
}
}
//删除结点
//思路:head 头结点不能动,因此需要 temp 辅助结点 找到待删除结点
//说明:
//1. 先遍历,找到待删除的结点 temp --->可以直接找到待删除的结点,实现自我删除
//2. temp.pre.next = temp.next;
//3. temp.next.pre = temp.pre
public void delete(int no){
//注意:可以仿照 修改方法中的,先判断是否为空,再 HeroNode temp = head.next;
HeroNode2 temp = head;
boolean flag = false;//表示是否找到待删除的点。true表示找到
while(true){
if(temp.next == null){//表示已到最后一个结点
break;
}
if(temp.no == no){
//找到了待删除结点的前一个结点
flag = true;
break;
}
temp = temp.next;//temp 后移,遍历
}
//判断 flag
if(flag){
//可以删除
temp.pre.next = temp.next;
//这里我们的代码有些问题?
//如果是最后一个结点,就不需要执行下面的语句,否则会出现空指针
if(temp.next != null){
temp.next.pre = temp.pre;
}
}else{
System.out.printf("要删除的%d 结点不存在\n",no);
}
}
//第二种方式添加英雄结点,根据英雄结点排名插入到指定位置
//双链表的插入
//1. 先找到待插入位置的前一个结点 p;新节点为 s
//2. s.pre = p;
//3. s.next = p.next;
//4. p.next.pre = s;
//5. p.next = s;
public void addByOrder(HeroNode2 heroNode){
//因为 head 头结点不能动,因此我们仍然需要一个辅助指针(辅助变量)
HeroNode2 temp = head;
boolean flag = false;// 表示添加的编号是否存在,默认为 false。true表示存在
//循环的目的就是为了找到新结点插入的位置,等同于确定 temp 的位置
while(true){
if(temp.next == null){//说明temp 已经到链表的最后,此时 temp 确定
//不改变 flag 的值意味着可以插入
break;
}
if(temp.next.no > heroNode.no){//位置找到,就在temp 的后面
break;
}else if(temp.next.no == heroNode.no){//说明希望添加的编号已经存在
flag = true;//不可添加
break;
}
temp = temp.next;//后移,遍历当前链表
}
//当退出 while 循环时,意味着已经可以确定是否能添加或者添加的位置已确定
//判断 flag 的值 ----> 是否能添加
if(flag){//不能添加,说明编号存在
System.out.printf("准备插入的英雄编号%d 已经存在,不能添加\n",heroNode.no);
}else{
//插入到链表中,temp 的后面
heroNode.pre = temp;
if(temp.next != null){//必须得有这个判断,因为若是最后一个结点,会报空指针异常。
heroNode.next = temp.next;
temp.next.pre = heroNode;
}
temp.next = 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;
}
//为了显示方便,重写 toString 方法
@Override
public String toString() {
return "HeroNode2{" +
"no=" + no +
", name='" + name + '\'' +
", nickName='" + nickName + '\'' +
'}';
}
}
循环链表
单循环链表
基本介绍
将单链表中终端结点的指针端由空指针改向头结点,就使整个单链表形成一个环,这种头尾相接的单链表称为单循环链表,简称循环链表(circular linked list)
循环链表和单链表的主要差异就在于循环条件的判断上,原来是 p.next == null ,现在是判断 p.next 是否等于头结点。
单循环链表的应用
Josephu(约瑟夫)问题
设编号为 1,2,。。。n 的 n 个人围坐一圈,约定编号为 k(1<=k<=n)的人从1开始报数,数到 m 的那个人出列,它的下一位又从 1 开始报数,数到m 的那个人又出列,依次类推,直到所有人出列为止,由此产生一个出队编号的序列。
思路:用一个不带头结点的循环链表来处理 Josephu 问题,先构成一个有 n 个结点的单循环链表,然后由 k 结点起从1开始计数,计到 m 时,对应结点从链表中删除,然后再从被删除结点的下一个结点又从 1 开始计数,直到最后一个结点从链表中删除,算法结束。
构建一个单向的环形链表思路:
1.先创建先创建第一个结点,让 first 指向该结点,并形成环形
2.后面当我们每创建一个新的结点,就把该节点加入到已有的环形链表中即可。
遍历环形链表
1.先让一个辅助指针(变量)cur,指向 first 结点
2.然后通过一个 while 循环遍历该环形链即可 cur.next == first 判断是否结束。
出圈
1.先创建一个 辅助指针,指向 循环链表的结尾。
2.将 first 和 helper 指针移动 m-1 次 ,first 指向初始结点
3.输出 first 结点的信息,first 后移 first = first.getNext();
4. helper.setNext(first);
/**
* 单循环链表
*/
public class CircularSimpleLinkedListDemo {
public static void main(String[] args) {
CircularSimpleLinkedList circularSimpleLinkedList = new CircularSimpleLinkedList();
circularSimpleLinkedList.add(5);
circularSimpleLinkedList.list();
circularSimpleLinkedList.countNode(1,2,5);
}
}
//创建一个环形的单向链表
class CircularSimpleLinkedList{
//创建一个 first 结点,当前没有编号
private Node first = null;
//添加结点,构成一个环形链表
public void add(int nums){
// 对 nums 做一个数据校验
if(nums < 1){
System.out.println("nums 的值不正确");
return;
}
Node cur = null;//辅助指针,帮助构建环形链表
//用 for 循环创建我们的环形链表
for (int i = 1; i <= nums; i++) {
//根据编号创建结点
Node newNode = new Node(i);
if(i == 1){
first = newNode;
first.setNext(first);//构成环
cur = first;//cur 指向第一个结点
}else{
cur.setNext(newNode);
newNode.setNext(first);
cur = newNode;
}
}
}
//遍历当前环形链表
public void list(){
if(first == null){
System.out.println("空链表");
return;
}
Node temp = first;
while(true){//说明已经遍历完毕
System.out.printf("编号%d\n",temp.getNo());
if(temp.getNext() == first){
break;
}
temp = temp.getNext();//后移
}
}
//根据用户的输入,计算出出圈的顺序
/**
*
* @param startNode 表示从第几个Node 开始数数
* @param countNum 表示数几下
* @param nums 表示最初的 Node 的个数
*/
public void countNode(int startNode, int countNum,int nums){
//先对数据进行校验
if(first == null || startNode < 1 || startNode > nums){
System.out.println("参数输入有误,请重新输入:");
return;
}
//创建辅助指针,完成 出圈
Node helper = first;
//先定位好 first 和 helper 的位置,因为是单链表,所以需要通过遍历的方式
while(true){
if(helper.getNext() == first){//helper 指向最后一个结点
break;
}
helper = helper.getNext();
}
//让 first 定位到指定的初始 Node 的位置,helper 随动
for (int i = 0; i < startNode - 1; i++) {
first = first.getNext();
helper = helper.getNext();
}
//循环出圈
while(true){
if(helper == first){
break;
}
//让 first 和 helper 同时移动countNum次,出圈
for (int i = 0; i < countNum - 1; i++) {
first = first.getNext();
helper = helper.getNext();
}
//这时 first 指向的结点是要出圈的结点
System.out.printf("%d 出圈\n",first.getNo());
//这时将结点出圈
first = first.getNext();
helper.setNext(first);
}
System.out.printf("最后留在圈中的编号%d\n",first.getNo());
}
}
class Node{
private int no;//编号
private Node next;//指向下一个结点。默认 null
public Node(int no) {
this.no = no;
}
public int getNo() {
return no;
}
public void setNo(int no) {
this.no = no;
}
public Node getNext() {
return next;
}
public void setNext(Node next) {
this.next = next;
}
@Override
public String toString() {
return "Node{" +
"no=" + no +
'}';
}
}