1、链表介绍
链表是有序的列表,它在内存中是存储如下:
1) 链表是以节点的方式来存储,是链式存储
2) 每个节点都包含 data 域:存储数据; next 域:指向下一个节点
3) 如上图:链表的各个节点不一定是连续存储
4) 链表分带头节点的链表 和 没有头节点的链表,根据实际的需求来确定
单链表(带头结点)逻辑结构示意图如下:
2、单链表的实现
使用带head头的单向链表实现 –水浒英雄排行榜管理 完成对英雄人物的增删改查操作
使用代码模拟:
模型创建:
节点模型:
package LinkedList.SingleLiskedList;
// 定义 HeroNode,每一个 HeroNode 就是一个节点
public class HeroNode {
private Integer no;
private String name;
private String nickname; // 绰号
private HeroNode next; // 指向下一个节点
public HeroNode(Integer no, String name, String nickname) {
this.no = no;
this.name = name;
this.nickname = nickname;
}
public HeroNode() {
}
@Override
public String toString() {
return "HeroNode{" +
"no=" + no +
", name='" + name + '\'' +
", nickname='" + nickname + '\'' +
'}';
}
}
2.1、 第一种方法在添加英雄时,直接添加到链表的尾部
思路分析图:
代码示例:
public class SingLinkedList {
// 创建头节点
private HeroNode head = new HeroNode(0,"","");
// 添加节点
// 思路:当不考虑编号顺序时
// 1、找到当前链表的最后节点
// 2、将最后这个节点的 next 指向 新的节点
public void add(HeroNode hero) {
// 因为 head 节点不能动,一次需要哟啊一个辅助节点 temp
HeroNode temp = head;
// 遍历链表
while (true) {
// 找到 链表的最后一个节点
if (temp.next == null) {
// 将新节点 插入 到最后一个节点的后面,在跳出死循环
temp.next = hero;
break;
}
temp = temp.next;
}
}
}
2.2、第二种方式在添加英雄时,根据排名将英雄插入到指定位置 (如果有这个排名,则添加失败,并给出提示)
思路分析图:
代码示例:
//第二种方式在添加英雄时,根据排名将英雄插入到指定位置 (如果有这个排名,则添加失败,并给出提示)
public void addByOrder(HeroNode hero){
// 因为 head 节点不能动,一次需要创建一个辅助指针 temp
// 因为时单链表,所以我们找到 temp 是位于 添加位置的前一个节点,否则插入不了
HeroNode temp = head;
boolean flag = false;
while (true) {
if (temp.next == null) { // 到链表最后,无法插入,退出循环
break;
}
if (temp.next.no > hero.no) { // 位置找到,退出循环
break;
} else if (temp.next.no == hero.no) { // 说明要插入的节点编号已存在
// 说明编号已存在,退出循环
flag = true;
break;
}
// 将节点后移
temp = temp.next;
}
if (flag) {
// 编号存在,无法添加
System.out.printf("准备插入的英雄编号%d已存在,无法继续插入\n",hero.no);
}else{
hero.next = temp.next;
temp.next = hero;
}
}
2.3 修改链表中的节点
思路:
(1) 先找到该节点,通过遍历,
(2) temp.name =newHeroNode.name ;
temp.nickname=newHeroNode.nickname
// 修改节点信息,根据 no 编号来修改,即 no 编号不能修改
public void update(HeroNode newHero) {
// 判断链表是否为空
if (head.next == null) {
System.out.println("链表为空~~");
return;
}
// 定义一个辅助指针
HeroNode temp = head.next;
boolean flag = false; // 表示是否找到要修改的节点
while (true) {
if (temp == null) {
break; // 链表已遍历完,退出循环
}
if (temp.no == newHero.no) { // 根据 no 找到要修改的节点
// 找到要修改的节点,退出循环
flag = true;
break;
}
// 迭代条件
temp = temp.next;
}
if (flag) {
temp.name = newHero.name;
temp.nickname = newHero.nickname;
} else {
System.out.printf("没有找到编号为%d的节点,不能修改", newHero.no);
}
}
2.4 删除节点
思路分析图:
代码示例:
// 删除节点
// 注意:
// 1、head 不能动,因此需要一个 temp 辅助节点找到待删除节点的前一个节点
// 2、在比较时,是 temp.next.no 和 需要删除的节点的 no 比较
public void delete(Integer 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不存在",no);
}
}
2.5 代码测试
package LinkedList.SingleLiskedList;
class SingLinkedListTest {
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, "宋江", "豹子头");
HeroNode hero5 = new HeroNode(0, "晁盖", "未知");
SingLinkedList singLinkedList = new SingLinkedList();
// singLinkedList.add(hero1);
// singLinkedList.add(hero2);
// singLinkedList.add(hero3);
// singLinkedList.add(hero4);
singLinkedList.addByOrder(hero1);
singLinkedList.addByOrder(hero3);
singLinkedList.addByOrder(hero4);
singLinkedList.addByOrder(hero2);
singLinkedList.addByOrder(hero5);
singLinkedList.list();
singLinkedList.update(new HeroNode(2, "小卢", "玉麒麟~~"));
singLinkedList.update(new HeroNode(5, "小卢", "玉麒麟~~"));
System.out.println("修改后的链表");
singLinkedList.list();
singLinkedList.delete(1);
singLinkedList.delete(4);
singLinkedList.delete(2);
System.out.println("删除后的链表");
singLinkedList.list();
}
}
3、单链表面试题(新浪、百度、腾讯)
单链表的常见面试题有如下:
- 求单链表中有效节点的个数
- 查找单链表中的倒数第k个结点 【新浪面试题】
- 单链表的反转【腾讯面试题,有点难度】
- 从尾到头打印单链表 【百度,要求方式1:反向遍历 。 方式2:Stack栈】
- 合并两个有序的单链表,合并之后的链表依然有序
3.1 求单链表中有效节点的个数
代码如下:
public HeroNode getHead() {
return head;
}
/**
*
* @param head 链表的头节点
* @return 返回有效节点的个数
*/
public int getLength(HeroNode head) {
if (head.next == null) {
return 0;// 空链表
}
int length = 0;
// 定义一个辅助指针
HeroNode temp = head.next;
while (temp != null) {
length++;
temp = temp.next;
}
return length;
}
代码测试:
// 测试:求单链表中有效节点的个数
System.out.println("链表的有效节点为:"
+singLinkedList.getLength(singLinkedList.getHead()));
3.2 查找单链表中的倒数第k个结点 【新浪面试题】
代码示例:
/**
* 查找单链表中的倒数第index个结点
* 思路:
* 1、编写一个方法,接收 head 节点,同时接受一个 index
* 2、index表示是倒数第 index 个节点
* 3、先把链表从头到尾遍历,得到链表的总长度 size
* 4、得到 size 后,我们从链表的的第一个节点开始遍历(size-index)个,找到所需要的位置
* 5、如果找到了,则返回改节点,否则返回null
* @param head 要查询的链表
* @param index 倒数的索引位置
* @return
*/
public HeroNode findLastIndexHero(HeroNode head,int index) {
// 判断链表是否为空
if (head.next == null) {
return null;
}
// 把链表从头到尾遍历,得到链表的总长度 size
int size = getLength(head);
//判断 index 是否超出范围
if (index <= 0 || index > size) {
return null;
}
// 定义一个辅助指针
HeroNode temp = head.next;
// 从链表的的第一个节点开始遍历(size-index)个,找到所需要的位置
for (int i = 0; i < size - index; i++) {
temp = temp.next;
}
return temp;
}
代码测试:
System.out.println(singLinkedList.findLastIndexHero(singLinkedList.getHead(),1));
System.out.println(singLinkedList.findLastIndexHero(singLinkedList.getHead(),2));
System.out.println(singLinkedList.findLastIndexHero(singLinkedList.getHead(),3));
3.3 单链表的反转【腾讯面试题,有点难度】
单链表的反转【腾讯面试题,有点难度】
代码实现:
// 单链表的反转
public void reverseList(HeroNode head) {
// 如果目标链表为空或只有一个节点,不用反转,直接返回
if (head.next == null || head.next.next == null) {
return;
}
// 定义一个辅助指针
HeroNode temp = head.next;
HeroNode next = null; // 指向当前节点的下一个节点
// 创建一个反转链表
HeroNode reverseHead = new HeroNode(0, "", "");
while (temp != null) {
next = temp.next; // 保存当前节点的下一个节点
temp.next = reverseHead.next; // 将当前节点放入反转链表的最前端
reverseHead.next = temp; // 将当前节点放入 反转链表中
temp = next; // 让 temp 后移 ,迭代条件
}
// 将 head.next 指向 reverseHead.next,实现链表的反转
head.next = reverseHead.next;
}
代码测试:
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, "宋江", "豹子头");
SingLinkedList singLinkedList = new SingLinkedList();
singLinkedList.add(hero1);
singLinkedList.add(hero4);
singLinkedList.add(hero2);
singLinkedList.add(hero3);
System.out.println("反转前的链表:");
singLinkedList.list();
singLinkedList.reverseList(singLinkedList.getHead());
System.out.println("反转后的链表:");
singLinkedList.list();
}
结果:
3.4 从尾到头打印单链表 【百度,要求方式1:反向遍历 。 方式2:Stack栈】
思路分析图解:
测试栈(Stack)的使用:
public static void main(String[] args) {
// 栈的特点:先入后出
Stack<String> stack = new Stack<>();
// 入栈
stack.add("张三");
stack.add("李四");
stack.add("王五");
// 出栈
while (stack.size() > 0) {
System.out.println(stack.pop()); // pop就是栈顶的取出
}// 结果:
// 王五
// 李四
// 张三
}
代码实现:
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, "宋江", "豹子头");
HeroNode hero5 = new HeroNode(0, "晁盖", "未知");
SingLinkedList singLinkedList = new SingLinkedList();
singLinkedList.add(hero1);
singLinkedList.add(hero4);
singLinkedList.add(hero2);
singLinkedList.add(hero3);
System.out.println("打印链表的反转,不改变原有链表的结构");
singLinkedList.reversePrint(singLinkedList.getHead());
System.out.println("打印原有的链表");
singLinkedList.list();
}
3.5 合并两个有序的单链表,合并之后的链表依然有序
思路:
① 传入要合并的两个有序单链表的头节点,返回一个合并后的有序链表
② 判断 传入要合并的两个有序单链表 是否都为空,若都为空,抛出异常,不为空,则继续运行
③ 当要合并的两个链表都不为空时,进行节点编号的比较,节点编号较小的节点插入到新链表中
④ 当 比较合并 进行一段后,会出现某个链表为空,最将不为空的链表全部插入到新链表中
代码实现:
/**
* 合并两个有序的单链表,合并之后的链表依然有序
* 思路:
* ① 传入要合并的两个有序单链表的头节点,返回一个合并后的有序链表
* ② 判断 传入要合并的两个有序单链表 是否都为空,若都为空,抛出异常,不为空,则继续运行
* ③ 当要合并的两个链表都不为空时,进行节点编号的比较,节点编号较小的节点插入到新链表中
* ④ 当 比较合并 进行一段后,会出现某个链表为空,最将不为空的链表全部插入到新链表中
* @param head1 传入要合并的链表1
* @param head2 传入要合并的链表2
* @return 返回一个合并后的有序链表
*/
public static SingLinkedList mergeList(HeroNode head1, HeroNode head2) {
if (head1.next == null && head2.next == null) {
throw new RuntimeException("要合并的两个链表都为空,无法合并");
}
// 创建要合并新链表的头节点
HeroNode newHead = new HeroNode(0, "", "");
// 创建一个新链表
SingLinkedList singLinkedList = new SingLinkedList();
singLinkedList.setHead(newHead);
// 创建辅助指针
HeroNode newTemp = newHead;
HeroNode temp1 = head1.next; // 链表1,当前节点
HeroNode temp2 = head2.next; // 链表2,当前节点
// 当要合并的两个链表都不为空时,可以实现比较合并
while (temp1 != null && temp2 != null) {
if (temp1.no <= temp2.no) {
newTemp.next = temp1;
// 将链表 1 往后移
temp1 = temp1.next;
} else {
newTemp.next = temp2;
// 将链表 2 往后移
temp2 = temp2.next;
}
// 将新链表往后移
newTemp = newTemp.next;
}
if (temp1 == null) {
while (temp2 != null) {
newTemp.next = temp2;
// 将链表2往后移
temp2 = temp2.next;
// 将新链表往后移
newTemp = newTemp.next;
}
} else { // 此时 temp2 为空
while (temp1 != null) {
newTemp.next = temp1;
// 将链表 1 往后移
temp1 = temp1.next;
// 将新链表往后移
newTemp = newTemp.next;
}
}
return singLinkedList;
}
代码测试:
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, "林冲", "豹子头");
HeroNode hero7 = new HeroNode(7, "霹雳火", "秦明");
HeroNode hero8 = new HeroNode(13, "花和尚", "鲁智深");
HeroNode hero9 = new HeroNode(14, "行者", "武松");
SingLinkedList singLinkedList1 = new SingLinkedList();
SingLinkedList singLinkedList2 = new SingLinkedList();
singLinkedList1.addByOrder(hero1);
singLinkedList1.addByOrder(hero4);
singLinkedList1.addByOrder(hero8);
singLinkedList2.addByOrder(hero2);
singLinkedList2.addByOrder(hero3);
singLinkedList2.addByOrder(hero7);
singLinkedList2.addByOrder(hero9);
SingLinkedList singLinkedList = SingLinkedList.
mergeList(singLinkedList1.getHead(), singLinkedList2.getHead());
singLinkedList.list();
}
结果:
4、双向链表的实现
使用带head头的双向链表实现 –水浒英雄排行榜 管理单向链表的缺点分析:
1) 单向链表,查找的方向只能是一个方向,而双向链表可以向前或者向后查找。
2) 单向链表不能自我删除,需要靠辅助节点﹐而双向链表,则可以自我删除,所以前面我们单链表删除时节点,总是找到temp,temp是待删除节点的前一个节点.
4.1 模拟双向链表的思路分析
对上图的说明:
分析双向链表的遍历,添加,修改,删除的操作思路===》
1)遍历 方式和单链表一样,只是可以向前,也可以向后查找
2)添加(默认添加到双向链表的最后)
(1)先找到双向链表的最后这个节点(2) temp.next = newHeroNode
(3) newHeroNode.pre = temp;
3) 修改思路和原来的单向链表一样.4)删除
(1)因为是双向链表,因此,我们可以实现自我删除某个节点(2)直接找到要删除的这个节点,比如 temp
(3) temp.pre.next =temp.next;
(4) temp.next.pre = temp.pre; (注:最后一个节点不能使用以下代码,否则会报空指针异常)
4.2 代码实现
4.2.1 结点类
// 定义 HeroNode,每一个 HeroNode 就是一个节点
public class HeroNode {
Integer no;
String name;
String nickname; // 绰号
HeroNode next; // 指向下一个节点
HeroNode pre; // 指向前一个节点
public HeroNode(Integer no, String name, String nickname) {
this.no = no;
this.name = name;
this.nickname = nickname;
}
public HeroNode() {
}
@Override
public String toString() {
return "HeroNode{" +
"no=" + no +
", name='" + name + '\'' +
", nickname='" + nickname + '\'' +
'}';
}
}
4.2.2 链表的遍历功能
public class DoubleLinkedList {
// 创建头节点
HeroNode head = new HeroNode(0, "", "");
public HeroNode getHead() {
return head;
}
public void setHead(HeroNode head) {
this.head = head;
}
// 显示链表
public void list() {
// 判断是否为空
if (head.next == null) {
System.out.println("链表为空!");
return;
}
// 因为 head 节点不能动,所以需要一个辅助指针 temp
HeroNode temp = head.next;
// 遍历链表并输出节点
while (true) {
// 判断 是否到 链表最后
if (temp == null) {
break;
}
// 输出节点信息
System.out.println(temp);
// 将当前节点后移
temp = temp.next;
}
}
}
4.2.3 添加结点(默认添加到链表的最后一个位置)
public void add(HeroNode hero) {
// 因为 head 节点不能动,一次需要哟啊一个辅助节点 temp
HeroNode temp = head;
// 遍历链表
while (true) {
// 找到 链表的最后一个节点
if (temp.next == null) {
// 将新节点 插入 到最后一个节点的后面,在跳出死循环
temp.next = hero;
hero.pre = temp;
break;
}
temp = temp.next;
}
}
4.2.4 结点修改
// 修改节点信息,与单向链表一样
public void update(HeroNode newHero) {
// 判断链表是否为空
if (head.next == null) {
System.out.println("链表为空~~");
return;
}
// 定义一个辅助指针
HeroNode temp = head.next;
boolean flag = false; // 表示是否找到要修改的节点
while (true) {
if (temp == null) {
break; // 链表已遍历完,退出循环
}
if (temp.no == newHero.no) { // 根据 no 找到要修改的节点
// 找到要修改的节点,退出循环
flag = true;
break;
}
// 迭代条件
temp = temp.next;
}
if (flag) {
temp.name = newHero.name;
temp.nickname = newHero.nickname;
} else {
System.out.printf("没有找到编号为%d的节点,不能修改", newHero.no);
}
}
4.2.5 结点删除
/**
* 删除结点,双向链表可以自我删除
* ① 直接找到要删除的这个节点,比如 temp
* ② temp.pre.next =temp.next;
* ③ temp.next.pre = temp.pre;(注:最后一个节点不能使用以下代码,否则会报空指针异常)
*
* @param no 传入要删除的节点编号
*/
public void delete(Integer no) {
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不存在", no);
}
}
4.3 代码测试
测试代码:
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, "林冲", "豹子头");
DoubleLinkedList doubleLinkedList = new DoubleLinkedList();
doubleLinkedList.add(hero1);
doubleLinkedList.add(hero2);
doubleLinkedList.add(hero3);
doubleLinkedList.add(hero4);
System.out.println("显示链表:");
doubleLinkedList.list();
doubleLinkedList.update(new HeroNode(1,"晁盖","托塔天王"));
System.out.println("显示修改后的链表:");
doubleLinkedList.list();
doubleLinkedList.delete(2);
doubleLinkedList.delete(4);
System.out.println("显示删除后的链表:");
doubleLinkedList.list();
}
结果:
5. Josephu问题及循环链表
5.1 Josephu 问题
Josephu 问题为:设编号为1,2,… n的n个人围坐一圈,约定编号为k(1<=k<=n)的人从1开始报数,数到m 的那个人出列,它的下一位又从1开始报数,数到m的那个人又出列,依次类推,直到所有人出列为止,由此产生一个出队编号的序列。
提示: 用一个不带头结点的循环链表来处理Josephu 问题:先构成一个有n个结点的单循环链表,然后由k结点起从1开始计数,计到m时,对应结点从链表中删除,然后再从被删除结点的下一个结点又从1开始计数,直到最后一个结点从链表中删除算法结束。
5.2 思路分析
5.3 代码实现
public 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;
}
}
public class CircleSingleLinkedList {
// 创建一个 first 结点
private Boy first;
// 添加 num 个小孩结点,构成一个环状链表
public void addBoys(int num) {
// 需要做一个校验
if (num < 1) {
System.out.println("传入参数num的值不能小于1");
return;
}
// 创建一个辅助指针
Boy temp = null;
for (int i = 1; i <= num; 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 show() {
if (first == null) {
System.out.println("链表为空~~");
return;
}
// 因为 first 不能动,所以要创建一个辅助指针
Boy temp = first;
// 循环打印
while (true) {
System.out.printf("小孩的编号为:%d\n", temp.getNo());
if (temp.getNext() == first) { // 遍历完毕
return;
}
// 辅助指针后移
temp = temp.getNext();
}
}
/**
* 根据用户的输入,计算小孩出圈的顺序
*
* @param startNo 表示从第几个小孩开始数数
* @param countNum 表示数几下
* @param nums 表示最初有多少小孩在圈中
*/
public static List<Integer> countBoy(int startNo, int countNum, int nums) {
// 1. 检验参数
if (startNo < 1 || countNum > nums) {
System.out.println("输入的参数有误!");
return null;
}
// 2. 根据 nums 创建链表
CircleSingleLinkedList linkedList = new CircleSingleLinkedList();
linkedList.addBoys(nums);
// 3. 创建辅助指针,帮助完成小孩出圈
Boy temp = linkedList.first;
Boy first = linkedList.first;
// 4. 需要创建一个辅助指针(变量)temp,实现应该指向链表的最后结点
while (true) {
if (temp.getNext() == first) {
break;
}
temp = temp.getNext();
}
// 5. 小孩报数前,先让 frist 和 temp 移动 k-1 次.
for (int i = 0; i < startNo - 1; i++) {
first = first.getNext();
temp = temp.getNext();
}
// 6. 创建一个 数组集合,记录出圈小孩的编号
List<Integer> list = new ArrayList<>();
// 7. 当小孩报数时,让 frist 和 temp 指针同时移动 m-1 次,然后出圈
while (true) {
if (first == temp) {
// 当圈中只有一个节点时,结束循环
break;
}
// frist 和 temp 指针同时移动 m-1 次
for (int i = 0; i < countNum - 1; i++) {
first = first.getNext();
temp = temp.getNext();
}
// 记录要出圈小孩的编号
list.add(first.getNo());
// 小孩出圈
first = first.getNext();
temp.setNext(first);
}
// 循环结束后,剩余一个小孩未出圈,记录其编号
list.add(first.getNo());
return list;
}
}
5.4 代码测试
public class Test {
public static void main(String[] args) {
CircleSingleLinkedList linkedList = new CircleSingleLinkedList();
linkedList.addBoys(5);
linkedList.show();
List<Integer> ints = CircleSingleLinkedList.countBoy(1, 2, 5);
System.out.println(ints);
}
}
运行结果: