20200913 第 4 章 链表
第 4 章 链表
4.1 链表(Linked List)介绍
链表是有序的列表, 但是它在内存中是存储如下
- 链表是以节点的方式来存储,是链式存储
- 每个节点包含 data 域, next 域: 指向下一个节点.
- 如图: 发现链表的各个节点不一定是连续存储
- 链表分带头节点的链表和没有头节点的链表, 根据实际的需求来确定
4.2 单链表的应用实例
使用带 head 头的单向链表实现 – 水浒英雄排行榜管理完成对英雄人物的增删改查操作
第一种方法在添加英雄时, 直接添加到链表的尾部
添加(创建):
- 先创建一个head 头节点, 作用就是表示单链表的头
- 后面我们每添加一个节点,就直接加入到 链表的最后
遍历:
- 通过一个辅助变量遍历,帮助遍历整个链表
代码实现
节点实现:
@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;
}
@Override
public String toString() {
return "HeroNode{" + "no=" + no + ", name='" + name + '\'' + ", nickName='" + nickName + '\'' + '}';
}
}
链表实现:
public class HeroLinkedList {
private HeroNode head = new HeroNode(0, "", "");
//添加节点到单向链表
//思路,当不考虑编号顺序时
//1. 找到当前链表的最后节点
//2. 将最后这个节点的next 指向 新的节点
public void add(HeroNode heroNode) {
HeroNode curNode = head;
while (true) {
if (curNode.getNext() == null) {
break;
}
curNode = curNode.getNext();
}
curNode.setNext(heroNode);
}
//显示链表[遍历]
public void list() {
HeroNode curNode = head.getNext();
while (true) {
if (curNode == null) {
break;
}
System.out.println(curNode);
curNode = curNode.getNext();
}
}
}
测试功能:
public class HeroLinkedListTest {
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, "林冲", "豹子头");
HeroLinkedList list = new HeroLinkedList();
list.add(hero1);
list.add(hero2);
list.add(hero3);
list.add(hero4);
list.list();
}
}
第二种方式在添加英雄时, 根据排名将英雄插入到指定位置(如果有这个排名, 则添加失败, 并给出提示)
需要按照编号的顺序添加
- 首先找到新添加的节点的位置, 是通过辅助变量(指针), 通过遍历来搞定
- 新的节点.next = temp.next
- 将temp.next = 新的节点
代码实现
添加方法:
//第二种方式在添加英雄时,根据排名将英雄插入到指定位置
//(如果有这个排名,则添加失败,并给出提示)
public void addByOrder(HeroNode heroNode) {
HeroNode curNode = head;
boolean existFlag = false;
while (true) {
if (curNode.getNext() == null) {
// 已到达链表尾部
break;
}
if (curNode.getNext().getNo() > heroNode.getNo()) {
// 找到应插入位置
break;
}
if (curNode.getNext().getNo() == heroNode.getNo()) {
// 已存在此编号英雄
existFlag = true;
break;
}
curNode = curNode.getNext();
}
if (existFlag) {
System.out.printf("已存在编号为 %d 英雄,不能添加\n", heroNode.getNo());
} else {
// 将节点插入链表
heroNode.setNext(curNode.getNext());
curNode.setNext(heroNode);
}
}
测试功能:
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, "林冲", "豹子头");
HeroLinkedList list = new HeroLinkedList();
// list.add(hero1);
// list.add(hero4);
// list.add(hero3);
// list.add(hero2);
// list.add(hero3); xxx,这里不可以插入重复节点,否则会造成链表节点间循环
list.addByOrder(hero1);
list.addByOrder(hero4);
list.addByOrder(hero3);
list.addByOrder(hero2);
// list.addByOrder(hero4);
list.list();
}
更新节点
更新功能:
public void update(HeroNode heroNode) {
HeroNode curNode = head;
boolean existFlag = false;
while (true) {
if (curNode == null) {
break;
}
if (curNode.getNo() == heroNode.getNo()) {
existFlag = true;
break;
}
curNode = curNode.getNext();
}
if (existFlag) {
curNode.setName(heroNode.getName());
curNode.setNickName(heroNode.getNickName());
} else {
System.out.printf("不存在编号为 %d 的英雄\n", heroNode.getNo());
}
}
测试功能:
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, "林冲", "豹子头");
HeroLinkedList list = new HeroLinkedList();
// list.add(hero1);
// list.add(hero4);
// list.add(hero3);
// list.add(hero2);
// list.add(hero3); xxx,这里不可以插入重复节点,否则会造成链表节点间循环
list.addByOrder(hero1);
list.addByOrder(hero4);
list.addByOrder(hero3);
list.addByOrder(hero2);
// list.addByOrder(hero4);
list.list();
System.out.println("修改英雄信息");
HeroNode hero33 = new HeroNode(2, "吴用2", "智多星3");
list.update(hero33);
list.list();
}
删除节点
删除功能:
public void delete(int no) {
HeroNode temp = head;
boolean existFlag = false;
while (true) {
if (temp.getNext() == null) {
break;
}
if (temp.getNext().getNo() == no) {
existFlag =true;
break;
}
temp = temp.getNext();
}
if (existFlag) {
temp.setNext(temp.getNext().getNext());
} else {
System.out.printf("链表中没有编号为 %d 的英雄\n", no);
}
}
测试功能:
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, "林冲", "豹子头");
HeroLinkedList list = new HeroLinkedList();
// list.add(hero1);
// list.add(hero4);
// list.add(hero3);
// list.add(hero2);
// list.add(hero3); xxx,这里不可以插入重复节点,否则会造成链表节点间循环
list.addByOrder(hero1);
list.addByOrder(hero4);
list.addByOrder(hero3);
list.addByOrder(hero2);
// list.addByOrder(hero4);
list.list();
System.out.println("修改英雄信息");
HeroNode hero33 = new HeroNode(2, "吴用2", "智多星3");
list.update(hero33);
list.list();
System.out.println("删除英雄信息");
list.delete(1);
// list.delete(2);
// list.delete(3);
// list.delete(4);
// list.delete(5);
list.list();
}
4.3 单链表面试题
求单链表中有效节点的个数
// 单链表中有效节点的个数
public int size() {
HeroNode curNode = head.getNext();
int count = 0;
while (true) {
if (curNode == null) {
break;
}
count++;
curNode = curNode.getNext();
}
return count;
}
查找单链表中的倒数第 k 个结点
// 查找单链表中的倒数第 k 个结点
public HeroNode findLastNode(int lastIndex) {
int size = size();
if (lastIndex > size) {
return null;
}
HeroNode curNode = head.getNext();
for (int i = 0; i < size - lastIndex; i++) {
curNode = curNode.getNext();
}
return curNode;
}
单链表的反转
思路:
- 先定义一个节点 reverseHead = new HeroNode();
- 从头到尾遍历原来的链表,每遍历一个节点,就将其取出,并放在新的链表reverseHead 的最前端.
- 原来的链表的head.next = reverseHead.next
/**
* 翻转单链表,改变了原链表
*/
public void reverse() {
/*
* 1. 先定义一个节点 reverseHead = new HeroNode();
2. 从头到尾遍历原来的链表,每遍历一个节点,就将其取出,并放在新的链表reverseHead 的最前端.
3. 原来的链表的head.next = reverseHead.next
*/
HeroNode reverseHead = new HeroNode(0, null, null); // 翻转单链表的头结点
HeroNode curNode = head.getNext(); // 记录遍历原链表的当前节点
HeroNode next = null; // 临时变量,记录遍历原链表的下一个节点
while (true) {
if (curNode == null) {
break;
}
next = curNode.getNext(); // 记录当前节点的下一个节点
// 将当前节点插入翻转单链表
curNode.setNext(reverseHead.getNext());
reverseHead.setNext(curNode);
curNode = next; // 当前节点后移,遍历单链表
}
head.setNext(reverseHead.getNext());
}
/**
* 翻转单链表,不改变原链表
*/
public HeroLinkedList reverseList() {
HeroLinkedList reverseList = new HeroLinkedList();
HeroNode reverseHead = reverseList.head;
HeroNode curNode = head.getNext();
while (true) {
if (curNode == null) {
break;
}
HeroNode heroNode = new HeroNode(curNode);
// 将当前节点插入翻转单链表
heroNode.setNext(reverseHead.getNext());
reverseHead.setNext(heroNode);
curNode = curNode.getNext();
}
return reverseList;
}
合并两个有序的单链表, 合并之后的链表依然有序
/**
* 合并两个有序的单链表, 合并之后的链表依然有序
*/
public void joinList(HeroLinkedList list) {
HeroNode curNode = head;
HeroNode curNode2 = list.head.getNext();
if (curNode.getNext() == null) {
curNode.setNext(curNode2);
return;
}
if (curNode2 == null) {
return;
}
while (true) {
if (curNode.getNext() == null) {
break;
}
if (curNode2 == null) {
// 已完全加入
break;
}
if (curNode.getNext().getNo() > curNode2.getNo()) {
HeroNode heroNode = new HeroNode(curNode2);
heroNode.setNext(curNode.getNext());
curNode.setNext(heroNode);
curNode2 = curNode2.getNext(); // curNode2 后移,遍历
}
curNode = curNode.getNext(); // curNode 后移,遍历
}
// 如果 curNode2 不为空,说明仍有节点没有加入进来
if (curNode2 != null) {
curNode.setNext(curNode2);
}
}
4.4 双向链表
4.4.1双向链表的操作分析和实现
使用带 head 头的双向链表实现 – 水浒英雄排行榜
管理单向链表的缺点分析:
- 单向链表, 查找的方向只能是一个方向, 而双向链表可以向前或者向后查找。
- 单向链表不能自我删除, 需要靠辅助节点 , 而双向链表, 则可以自我删除, 所以前面我们单链表删除时节点, 总是找到 temp,temp 是待删除节点的前一个节点
分析 双向链表的遍历,添加,修改,删除的操作思路===》代码实现
- 遍历 方和 单链表一样,只是可以向前,也可以向后查找
- 添加 (默认添加到双向链表的最后)
- 先找到双向链表的最后这个节点
- temp.next = newHeroNode
- newHeroNode.pre = temp;
- 修改 思路和 原来的单向链表一样.
- 删除
- 因为是双向链表,因此,我们可以实现自我删除某个节点
- 直接找到要删除的这个节点,比如temp
- temp.pre.next = temp.next
- temp.next.pre = temp.pre;
双向链表的代码实现
节点类:
@Data
public class DoubleHeroNode {
private int no;
private String name;
private String nickName;
private DoubleHeroNode next;
private DoubleHeroNode pre;
public DoubleHeroNode(int no, String name, String nickName) {
this.no = no;
this.name = name;
this.nickName = nickName;
}
public DoubleHeroNode(DoubleHeroNode heroNode) {
this.no = heroNode.no;
this.name = heroNode.name;
this.nickName = heroNode.nickName;
}
@Override
public String toString() {
return "HeroNode{" + "no=" + no + ", name='" + name + '\'' + ", nickName='" + nickName + '\'' + '}';
}
}
双向列表实现:
public class DoubleHeroLinkedList {
private DoubleHeroNode head = new DoubleHeroNode(0, "", "");
//添加节点到单向链表
//思路,当不考虑编号顺序时
//1. 找到当前链表的最后节点
//2. 将最后这个节点的next 指向 新的节点
public void add(DoubleHeroNode heroNode) {
DoubleHeroNode curNode = head;
while (true) {
if (curNode.getNext() == null) {
break;
}
curNode = curNode.getNext();
}
curNode.setNext(heroNode);
heroNode.setPre(curNode);
}
public void addByOrder(DoubleHeroNode heroNode) {
DoubleHeroNode curNode = head;
boolean existFlag = false;
while (true) {
if (curNode.getNext() == null) {
// 已到达链表尾部
break;
}
if (curNode.getNext().getNo() > heroNode.getNo()) {
// 找到应插入位置
break;
}
if (curNode.getNext().getNo() == heroNode.getNo()) {
// 已存在此编号英雄
existFlag = true;
break;
}
curNode = curNode.getNext();
}
if (existFlag) {
System.out.printf("已存在编号为 %d 英雄,不能添加\n", heroNode.getNo());
} else {
// 将节点插入链表
if (curNode.getPre() == null) {
// 如果为 head
heroNode.setNext(curNode.getNext());
curNode.setNext(heroNode);
heroNode.setPre(curNode.getPre());
} else {
curNode.getPre().setNext(heroNode);
heroNode.setPre(curNode.getPre());
curNode.setPre(heroNode);
heroNode.setNext(curNode);
}
}
}
public void update(DoubleHeroNode heroNode) {
DoubleHeroNode curNode = head;
boolean existFlag = false;
while (true) {
if (curNode == null) {
break;
}
if (curNode.getNo() == heroNode.getNo()) {
existFlag = true;
break;
}
curNode = curNode.getNext();
}
if (existFlag) {
curNode.setName(heroNode.getName());
curNode.setNickName(heroNode.getNickName());
} else {
System.out.printf("不存在编号为 %d 的英雄\n", heroNode.getNo());
}
}
/**
* 双向链表可以自我删除
*
* @param no
*/
public void delete(int no) {
DoubleHeroNode tempNode = head.getNext();
boolean existFlag = false;
while (true) {
if (tempNode == null) {
break;
}
if (tempNode.getNo() == no) {
existFlag = true;
break;
}
tempNode = tempNode.getNext();
}
if (existFlag) {
tempNode.getPre().setNext(tempNode.getNext());
if (tempNode.getNext() != null) {
tempNode.getNext().setPre(tempNode.getPre());
}
} else {
System.out.printf("链表中没有编号为 %d 的英雄\n", no);
}
}
//显示链表[遍历]
public void list() {
DoubleHeroNode curNode = head.getNext();
while (true) {
if (curNode == null) {
break;
}
System.out.println(curNode);
curNode = curNode.getNext();
}
}
}
测试功能:
public class DoubleHeroLinkedListTest {
public static void main(String[] args) {
// 测试
System.out.println("双向链表的测试");
// 先创建节点
DoubleHeroNode hero1 = new DoubleHeroNode(1, "宋江", "及时雨");
DoubleHeroNode hero2 = new DoubleHeroNode(2, "卢俊义", "玉麒麟");
DoubleHeroNode hero3 = new DoubleHeroNode(3, "吴用", "智多星");
DoubleHeroNode hero4 = new DoubleHeroNode(4, "林冲", "豹子头");
// 创建一个双向链表
DoubleHeroLinkedList doubleLinkedList = new DoubleHeroLinkedList();
doubleLinkedList.add(hero1);
doubleLinkedList.add(hero2);
doubleLinkedList.add(hero3);
doubleLinkedList.add(hero4);
doubleLinkedList.list();
// 修改
DoubleHeroNode newHeroNode = new DoubleHeroNode(4, "公孙胜", "入云龙");
doubleLinkedList.update(newHeroNode);
System.out.println("修改后的链表情况");
doubleLinkedList.list();
// 删除
doubleLinkedList.delete(3);
System.out.println("删除后的链表情况~~");
doubleLinkedList.list();
// 按顺序插入
System.out.println("按顺序插入链表情况~~");
doubleLinkedList = new DoubleHeroLinkedList();
DoubleHeroNode hero11 = new DoubleHeroNode(11, "宋江", "及时雨");
DoubleHeroNode hero21 = new DoubleHeroNode(21, "卢俊义", "玉麒麟");
DoubleHeroNode hero31 = new DoubleHeroNode(31, "吴用", "智多星");
DoubleHeroNode hero41 = new DoubleHeroNode(41, "林冲", "豹子头");
doubleLinkedList.addByOrder(hero11);
doubleLinkedList.addByOrder(hero31);
doubleLinkedList.addByOrder(hero41);
doubleLinkedList.addByOrder(hero21);
doubleLinkedList.list();
}
}
4.7 Josephu(约瑟夫) 问题
问题描述
Josephu 问题为:设编号为1,2,… n的n个人围坐一圈,约定编号为k(1<=k<=n)的人从1开始报数,数到m 的那个人出列,它的下一位又从1开始报数,数到m的那个人又出列,依次类推,直到所有人出列为止,由此产生一个出队编号的序列。
n = 5 , 即有5个人
k = 1, 从第一个人开始报数
m = 2, 数2下
出圈的顺序
2->4->1->5->3
思路分析
用一个不带头结点的循环链表来处理 Josephu 问题: 先构成一个有 n 个结点的单循环链表,然后由 k 结点起从 1 开始计数, 计到 m 时, 对应结点从链表中删除, 然后再从被删除结点的下一个结点又从 1 开始计数, 直到最后一个结点从链表中删除算法结束。
构建一个单向的环形链表思路
- 先创建第一个节点, 让 first 指向该节点,并形成环形
- 后面当我们每创建一个新的节点,就把该节点,加入到已有的环形链表中即可.
遍历环形链表
- 先让一个辅助指针(变量) curBoy,指向first节点
- 然后通过一个while循环遍历 该环形链表即可 curBoy.next == first 结束
代码实现
节点类:
@Data
public class BoyNode {
private int no;
private BoyNode next;
public BoyNode(int no) {
this.no = no;
}
@Override
public String toString() {
return "BoyNode{" + "no=" + no + '}';
}
}
单向环形链表实现:
public class CircleSingleLinkedList {
private BoyNode first; // 单向环形链表的第一个节点,不会移动
/**
* 构建一个单向的环形链表思路
* 1. 先创建第一个节点, 让 first 指向该节点,并形成环形
* 2. 后面当我们每创建一个新的节点,就把该节点,加入到已有的环形链表中即可.
*
* @param boyNum
*/
public void addBoyNodes(int boyNum) {
BoyNode curBoy = null;
for (int i = 1; i <= boyNum; i++) {
BoyNode boyNode = new BoyNode(i);
if (i == 1) {
first = boyNode;
first.setNext(first);
curBoy = boyNode;
} else {
curBoy.setNext(boyNode);
boyNode.setNext(first);
curBoy = boyNode;
}
}
}
/**
* 遍历环形链表
* 1. 先让一个辅助指针(变量) curBoy,指向first节点
* 2. 然后通过一个while循环遍历 该环形链表即可 curBoy.next == first 结束
*/
public void listBoyNodes() {
if (first == null) {
System.out.println("链表为空");
return;
}
BoyNode curBoy = first;
while (true) {
System.out.println(curBoy);
if (curBoy.getNext() == first) {
break;
}
curBoy = curBoy.getNext();
}
}
/**
* 从第k个开始,每数m个,出列一个
* <p>
* 1. 需求创建一个辅助指针(变量) helper , 事先应该指向环形链表的最后这个节点.
* 补充: 小孩报数前,先让 first 和 helper 移动 k - 1次
* 2. 当小孩报数时,让first 和 helper 指针同时 的移动 m - 1 次
* 3. 这时就可以将first 指向的小孩节点 出圈
* first = first .next
* helper.next = first
* 原来first 指向的节点就没有任何引用,就会被回收
*
* @param startNo k
* @param countNum m
*/
public void calcJosephuOrder(int startNo, int countNum) {
BoyNode beforeCurNode = first, curNode = first;
// 将 beforeCurNode 执行 first 前一个节点
while (true) {
if (beforeCurNode.getNext() == first) {
break;
}
beforeCurNode = beforeCurNode.getNext();
}
// 移动 startNo 个位置,然后开始出圈
for (int i = 0; i < startNo - 1; i++) {
beforeCurNode = beforeCurNode.getNext();
curNode = curNode.getNext();
}
// 每移动 countNum,出圈一个
// 当最后剩下一个节点时,停止循环
while (true) {
if (beforeCurNode == curNode) {
break;
}
// 移动 countNum - 1 次,出圈一个
for (int i = 0; i < countNum - 1; i++) {
beforeCurNode = beforeCurNode.getNext();
curNode = curNode.getNext();
}
System.out.printf("出圈的是 %d \n", curNode.getNo());
curNode = curNode.getNext();
beforeCurNode.setNext(curNode);
}
System.out.printf("最后剩下的是 %d \n", curNode.getNo());
}
}
测试功能:
public class CircleSingleLinkedListTest {
public static void main(String[] args) {
CircleSingleLinkedList list = new CircleSingleLinkedList();
list.addBoyNodes(5);
list.listBoyNodes();
list.calcJosephuOrder(1,2);
}
}