1、链表(Linked List)介绍
1.1、内存结构
内存上来看:链表存储空间不连续(不像数组)假设有一个线性表为(a1,a2,a3,a4,a5,a6),则其对应的带有头结点的链表表示及存储结构如图所示,其中head指针的值为2020H。
1.2 、逻辑结构
逻辑上来看:链表属于线性结构含有n个元素的线性表通过每个结点的指针域链接成一个链表。又由于此链表的每个结点中只有一个指向后继的指针,所以称其为单链表或线性链表。下图的单链表是带有头结点的单链表,头结点的作用是方便单链表的特殊操作,简化代码。其中,头指针head指向链表的头结点,各元素结点的指针指向下一个结点,而最后一个结点的指针为“空”(NULL)。从头指针开始便可沿着链找到链表各个元素,因此可用头指针来表示一个单链表。
2、单链表的实现
2.1、水浒英雄榜
单链表的应用实例使用带head头的单向链表实现 –水浒英雄排行榜管理完成对英雄人物的增删改查操作, 注: 删除和修改,查找可以考虑学员独立完成,也可带学员完成第一种方法在添加英雄时,直接添加到链表的尾部第二种方式在添加英雄时,根据排名将英雄插入到指定位置(如果有这个排名,则添加失败,并给出提示)
2.2、链表节点定义
no :英雄编号name :英雄名字nickName :英雄昵称next :指向下一个 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
+ '\'' +
'}';
}
}
2.3、链表定义
Head :头结点不存放数据,仅仅作为当前链表的入口head 字段的值不能改变,一旦改变,就丢失了整个链表的入口,我们也就无法通过 head 找到链表了
/定义SingleLikedList管理英雄
class SingleLikedList{
private HeroNode head
= new HeroNode(0,"","");
2.4、遍历链表
定义辅助变量 temp ,相当于一个指针,指向当前节点何时遍历完成?temp == null 表明当前节点为 null ,即表示已到链表末尾如何遍历?temp = temp.next ,每次输出当前节点信息之后,temp 指针后移
public void list(){
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
;
}
}
2.5、添加节点
代码思路
定义辅助变量 temp ,相当于一个指针,指向当前节点如何在链表末尾插入节点?首先需要遍历链表,找到链表最后一个节点,当 temp.next == null时,temp 节点指向链表最后一个节点然后在 temp 节点之后插入节点即可:temp.next = heroNode
public void addByOrder(HeroNode heroNode
){
HeroNode temp
= head
;
boolean flag
= false;
while (true){
if (temp
.next
== null
)break;
if (temp
.next
.no
> heroNode
.no
){
break;
}else if (temp
.next
.no
== heroNode
.no
){
flag
= true;
break;
}
temp
= temp
.next
;
}
if (flag
){
System
.out
.printf("准备插入的英雄编号%d已经存在,不能加入\n",heroNode
.no
);
}else {
heroNode
.next
= temp
.next
;
temp
.next
= heroNode
;
}
}
2.6、修改节点
代码思路
定义辅助变量 temp ,相当于一个指针,指向当前节点如何找到指定节点?temp.no = newHeroNode.no
代码实现
/修改节点的信息,根据no编号来修改,即no编号不能修改
public void update(HeroNode newHeroNode
){
if (head
.next
== null
){
System
.out
.println("链表为空");
return;
}
HeroNode temp
= head
.next
;
boolean flag
= false;
while (true){
if (temp
== null
)break;
if (temp
.no
== newHeroNode
.no
){
flag
= true;
break;
}
temp
= temp
.next
;
}
if (flag
){
temp
.name
= newHeroNode
.name
;
temp
.nickname
= newHeroNode
.nickname
;
}else {
System
.out
.printf("没有找到编号%d的节点,不能修改\t",newHeroNode
.no
);
}
}
2.7、删除节点
代码思路
定义辅助变量 temp ,相当于一个指针,指向当前节点如何找到待删除的节点?遍历链表,当 temp.next == no 时,temp.next 节点就是待删除的节点如何删除?temp = temp.next.next 即可删除 temp.next 节点,该节点没有引用指向它,会被垃圾回收机制回收
代码实现
public void del(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
);
}
}
2.9、单链表总结
遍历链表,执行操作时,判断条件有时候是 temp ,有时候是 temp.next ,Why?对于插入、删除节点来说,需要知道当前待操作的节点(heroNode)前一个节点的地址(指针)如果直接定位至当前待操作的节点 heroNode ,那没得玩。。。因为不知道heroNode 前一个节点的地址,无法进行插入、删除操作,所以 while 循环中的条件使用 temp.next 进行判断对于更新、遍历操作来说,我需要的仅仅就只是当前节点的信息,所以 while 循环中的条件使用 temp进行判断头结点与首节点 参考资料:https://blog.csdn.net/WYpersist/article/details/80288056 头结点是为了操作的统一与方便而设立的,放在第一个元素结点之前,其数据域一般无意义(当然有些情况下也可存放链表的长度、用做监视哨等等)。 首元结点也就是第一个元素的结点,它是头结点后边的第一个结点。
3.双链表的实现
3.1 双向链表
以上讨论的链表,结点中只有一个指向其后继结点的指针域next,若已知某个结点,要找其前驱结点,则只能从表头指针出发。也就是说,找后继的时间复杂度为O(1),而找前驱的时间复杂度为O(n),如果希望找前驱的时间复杂度也为O(1),则需付出空间的代价,在每个结点中再设一个指向前驱的指针域,结点结构如图所示,由这种结点组成的链表称为双向链表。
3.2 与单向链表的比较
单向链表, 查找的方向只能是一个方向, 而双向链表可以向前或者向后查找 单向链表不能自我删除, 需要靠辅助节点而双向链表, 则可以自我删除, 所以前面我们单链表删除时节点, 总是找到 temp ,temp 是待删除节点的前一个节点(认真体会)
3.3 链表节点定义
在单向链表节点的基础上,增加 pre ,用于指向前一个节点
class HeroNode {
public int no
;
public String name
;
public String nickname
;
public HeroNode next
;
public HeroNode pre
;
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
+ '\'' +
'}';
}
}
3.4 遍历链表
定义一个头结点head,作为链表的头部入口,不需要存放数据
class DoubleLikedList{
private HeroNode head
= new HeroNode(0,"","");
public void list(){
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
;
}
}
3.5 双链表插入数据
如何插入?temp.next 指向新的尾节点 heroNode :temp.next = heroNode;heroNode .pre 指向旧的尾节点 temp :heroNode.pre = temp;
代码实现
public void add(HeroNode heroNode
){
HeroNode temp
= head
;
while (true){
if (temp
.next
== null
)break;
temp
= temp
.next
;
}
temp
.next
= heroNode
;
heroNode
.pre
= temp
;
}
3.6 双链表删除节点
对于双向链表,可以直接找到需要删除的节点,找到后自我删除即可
如何删除?主要是实现上一节点和下一节点相连,注意尾节点的空指针问题temp 的前一个节点的 next 域指向 temp 的后一个节点:temp.pre.next = temp.next;temp 的后一个节点的 pre 域指向 temp 的前一个节点:temp.next.pre = temp.pre;有个地方需要注意,如果 temp 已经是链表尾节点,temp 已经没有下一个节点这时只需要将 temp 的前一个节点的 next 指向 null 即可所以 temp.next.pre = temp.pre; 执行的前提条件是 temp.next != null
代码实现
public void del(int no
){
if (head
.next
== null
){
System
.out
.println("空链表,无法删除");
}
HeroNode 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
);
}
}
3.7. 双链表的修改
和单链表基本类似
代码思路
定义辅助变量 temp ,相当于一个指针,指向当前节点如何找到指定节点?temp.no = newHeroNode.no
代码实现
/修改节点的信息,根据no编号来修改,即no编号不能修改
public void update(HeroNode newHeroNode
){
if (head
.next
== null
){
System
.out
.println("链表为空");
return;
}
HeroNode temp
= head
.next
;
boolean flag
= false;
while (true){
if (temp
== null
)break;
if (temp
.no
== newHeroNode
.no
){
flag
= true;
break;
}
temp
= temp
.next
;
}
if (flag
){
temp
.name
= newHeroNode
.name
;
temp
.nickname
= newHeroNode
.nickname
;
}else {
System
.out
.printf("没有找到编号%d的节点,不能修改\t",newHeroNode
.no
);
}
}
4.循环链表
循环链表是另一种形式的链表,它的特点是表中最后一个结点的指针域不再为空,而是指向表头结点,整个链表形成一个环。由此,从表中任一结点出发均可找到链表中其他结点,如图2.12所示。
4.1 单向环形链表应用场景
Josephu 问题
Josephu 问题为: 设编号为 1, 2, … n 的 n 个人围坐一圈, 约定编号为 k(1<=k<=n) 的人从 1 开始报数, 数到 m 的那个人出列, 它的下一位又从 1 开始报数, 数到 m 的那个人又出列, 依次类推, 直到所有人出列为止, 由此产生一个出队编号的序列。
用一个不带头结点的循环链表来处理 Josephu 问题: 先构成一个有 n 个结点的单循环链表, 然后由 k 结点起从 1 开始计数, 计到 m 时, 对应结点从链表中删除, 然后再从被删除结点的下一个结点又从 1 开始计数, 直到最后一个结点从链表中删除算法结束。
4.2 环形链表的构建与遍历
Boy 节点就是个普普通通的单向链表节点
class Boy {
private int no
;
private Boy next
;
public Boy(int no
) {
this.no
= no
;
}
public int getNo() {
return no
;
}
public void setNo(int no
) {
this.no
= no
;
}
public Boy
getNext() {
return next
;
}
public void setNext(Boy next
) {
this.next
= next
;
}
}
first 节点为单向循环链表的首节点,是真实存放数据的节点,不是头结点
class CircleSingleLinkedList {
private Boy first
= null
;
4.2.1 构建单向循环链表
代码思路
长度为 1 的情况:新创建的 boy 节点即是首节点:first = boy;
自封闭(自己构成环形链表):first.setNext(first);
此时 first 节点既是首节点,也是尾节点,辅助指针也指向 first :curBoy = first;
长度不为 1 的情况:将 boy 节点添加至环形链表的最后:curBoy.setNext(boy);
curBoy 节点永远是环形链表的尾节点构成环形链表(最):boy.setNext(first);
辅助指针后移,指向环形链表的尾节点:curBoy = boy;
代码实现
public void addBoy(int nums
) {
if (nums
< 1) {
System
.out
.println("nums的值不正确");
return;
}
Boy curBoy
= null
;
for (int i
= 1; i
<= nums
; i
++) {
Boy boy
= new Boy(i
);
if (i
== 1) {
first
= boy
;
first
.setNext(first
);
curBoy
= first
;
} else {
curBoy
.setNext(boy
);
boy
.setNext(first
);
curBoy
= boy
;
}
}
}
4.2.2 遍历单向循环链表
代码思路定义辅助变量 curBoy ,相当于一个指针,指向当前节点何时退出 while 循环?当 curBoy 已经指向环形链表的尾节点:curBoy.getNext() == first
代码实现
public void showBoy() {
if (first
== null
) {
System
.out
.println("没有任何小孩~~");
return;
}
Boy curBoy
= first
;
while (true) {
System
.out
.printf("小孩的编号 %d \n", curBoy
.getNo());
if (curBoy
.getNext() == first
) {
break;
}
curBoy
= curBoy
.getNext();
}
}
4.3 解决 Josephu 问题
代码思路:
1.辅助变量 helper :helper 永都指向环形链表的尾节点,环形链表的尾节点永远都指向首节点,可得出:
helper.getNext() == first
2. 如何将 helper 定位至环形链表的尾节点?
初始化时,让 helper = first ,此时 helper 指向环形链表的首节点
while 循环终止条件?helper.getNext() == first :此时 helper 已经移动至环形链表的尾节点
3. 如何定位至第 startNo 个节点?
如果想要定位至第 2 个节点,那么则需要让 first 和 helper 都移动 1 步,所以让 first 和 helper 都移动 (startNo - 1)步即可
4.如何数 nums 下?
让 first 和 helper 都移动 (nums - 1)步即可
5.如何实现出圈?
我们需要将 first 指向的节点出圈,first 前一个节点的地址在 helper 中存着(环形链表)
先让 first 后移一步:first = first.getNext;
出圈:helper.setNext(first); ,原来的 first 节点由于没有任何引用,便会被垃圾回收机制回收
6.while 循环终止条件?
圈中只剩一人:helper == first
代码实现
public void countBoy(int startNo
, int countNum
, int nums
) {
if (first
== null
|| startNo
< 1 || startNo
> nums
) {
System
.out
.println("参数输入有误, 请重新输入");
return;
}
Boy helper
= first
;
while (true) {
if (helper
.getNext() == first
) {
break;
}
helper
= helper
.getNext();
}
for (int j
= 0; j
< startNo
- 1; j
++) {
first
= first
.getNext();
helper
= helper
.getNext();
}
while (true) {
if (helper
== first
) {
break;
}
for (int j
= 0; j
< countNum
- 1; j
++) {
first
= first
.getNext();
helper
= helper
.getNext();
}
System
.out
.printf("小孩%d出圈\n", first
.getNo());
first
= first
.getNext();
helper
.setNext(first
);
}
System
.out
.printf("最后留在圈中的小孩编号%d \n", first
.getNo());
}
总结
操作单向链表:对于插入、删除操作,只能定位至待操作节点的前一个节点,如果定位至当前节点,那么其上一个节点的信息便会丢失