欢迎浏览我的博客:https://Mr00wang.github.io
学习笔记 Java算法与数据结构之链表
链表
一、链表介绍
- 链表是以节点的方式来存储,是链式存储
- 每个节点包含 data 域, next 域:指向下一个节点.
- 如图:发现链表的各个节点不一定是连续存储.
- 链表分带头节点的链表和没有头节点的链表,根据实际的需求来确定
单链表
链表通过指针将一组零散的内存块串联在一起,内存块称为链表的“结点”。每个链表的结点除了存储数据之外,还需要记录链上的下一个结点的地址,叫作后继指针 next。链表有两个特殊的结点,分别是第一个结点(头结点)和最后一个结点(尾结点)。头结点用来记录链表的基地址,用它可以遍历得到整条链表。尾结点指向一个空地址 NULL,表示这是链表上最后一个结点。
双向链表
双向链表支持两个方向,每个结点同时有后继指针 next 指向后面的结点,还有一个前驱指针 pre 指向前面的结点。双向链表需要额外的两个空间来存储后继结点和前驱结点的地址,存储同样的数据,双向链表要比单链表占用更多的内存空间。
循环链表
循环链表跟单链表的区在尾结点指针是指向链表的头结点。和单链表相比,循环链表的优点是从链尾到链头比较方便。当要处理的数据具有环型结构特点时,采用循环链表实现代码会简洁很多。
内存结构图
单链表(带头结点) 逻辑结构图
二、单链表应用实例
使用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(hero4);
singleLinkedList.addByOrder(hero3);
singleLinkedList.addByOrder(hero2);
singleLinkedList.list();
//测试修改节点的代码
System.out.println("修改2号:");
HeroNode newHeroNode = new HeroNode(2, "小卢", "玉麒麟。。。");
singleLinkedList.update(newHeroNode);
//显示
System.out.println("修改后情况!");
singleLinkedList.list();
//删除一个节点
System.out.println("删除1号:");
singleLinkedList.delete(1);
System.out.println("删除后的链表情况");
singleLinkedList.list();
}
}
(1)添加节点
第一种方式,直接添加到链表的尾部
第二种方式,插入节点,可以根据排名插入到指定位置
思路:
- 需要按照编号的顺序添加
- 首先找到新添加的节点的位置, 是通过辅助变量(指针), 通过遍历来搞定
- newNode.next = temp.next
- 将temp.next = newNode
(2)删除节点
思路:
- 我们先找到 需要删除的这个节点的前一个节点 temp
- temp.next = temp.next.next
- 被删除的节点,将不会有其它引用指向,会被垃圾回收机制回收
(3)修改节点
思路:
- 通过遍历找到该节点,然后直接重新赋值
- temp.data= newNode.data
(4)查询节点
链表的随机访问第 k 个元素,必须根据指针一个结点一个结点地依次遍历,直到找到相应的结点。
详细代码如下:
//定义一个SingleLinkedList 管理我们的英雄
class SingleLinkedList {
//先初始化一个头节点, 头节点不要动, 不存放具体数据
private HeroNode head = new HeroNode(0, "", "");
//添加节点到单项链表
//思路,当不考虑编号顺序时
//1.找到当前链表的最后节点
//2.将最后这个节点的next 指向 新的节点
public void add(HeroNode heroNode) {
//head节点不能动,因此需要一个辅助遍历temp
HeroNode temp = head;
//遍历链表,找到最后
while (true) {
//找到链表最后
if (temp.next == null) {
break;
}
//如果没有找到就把temp后移
temp = temp.next;
}
//当退出while循环时,temp就指向了链表的最后
//将最后这个节点的next 指向 新的节点
temp.next = heroNode;
}
//第二种方式在添加英雄时,根据排名将英雄插入到指定位置(如果有这个排名,则添加失败,并给出提示)
public void addByOrder (HeroNode heroNode) {
//通过一个辅助指针来帮助找到添加的位置
//因为单链表,因为我们找的temp是位于添加位置的前一个节点,否则插入不了
HeroNode temp = head;
boolean flag = false;//标志添加的编号是否存在
while (true) {
if (temp.next == null) { //说明temp已经在链表最后
break;
}
if (temp.next.no > heroNode.no) { //位置找到,就在temp的后面插入
break;
} else if (temp.next.no == heroNode.no) { //添加的编号存在
flag = true; //编号存在
break;
}
temp = temp.next;
}
//判断flag的值
if (flag) {
//编号存在,不能添加
System.out.println("准备插入的编号" + heroNode.no +"已经存在了");
} else {
//插入到链表中,temp的后面
heroNode.next = temp.next;
temp.next = heroNode;
}
}
//修改节点的信息,根据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.println("没有找到编号为" +newHeroNode.no + "的节点");
}
}
//删除节点
//思路
//1.head 节点不动,需要一个temp辅助节点,找到待删除节点的前一个节点
//2.说明我们在比较时,是temp.next.no和待删除的节点的no比较
public void delete(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.println("要删除的"+ no +"这个节点不存在");
}
}
//显示链表
public void list() {
//判断链表是否为空
if (head.next == null) {
System.out.println("链表为空");
return;
}
//因为头节点不能动,所以需要一个辅助遍历temp
HeroNode temp = head.next;
while (true) {
if (temp == null) {
break;
}
//输出节点信息
System.out.println(temp);
//将next后移
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;
}
//重写toString
@Override
public String toString() {
return "HeroNode [no = "+ no +", name = "+ name +", nickname = "+ nickname +"]";
}
}
效果展示:
HeroNode [no = 1, name = 宋江, nickname = 及时雨]
HeroNode [no = 2, name = 卢俊义, nickname = 玉麒麟]
HeroNode [no = 3, name = 吴用, nickname = 智多星]
HeroNode [no = 4, name = 林冲, nickname = 豹子头]
修改2号:
修改后情况!
HeroNode [no = 1, name = 宋江, nickname = 及时雨]
HeroNode [no = 2, name = 卢, nickname = 玉麒麟]
HeroNode [no = 3, name = 吴用, nickname = 智多星]
HeroNode [no = 4, name = 林冲, nickname = 豹子头]
删除1号
删除后的链表情况
HeroNode [no = 2, name = 卢, nickname = 玉麒麟]
HeroNode [no = 3, name = 吴用, nickname = 智多星]
HeroNode [no = 4, name = 林冲, nickname = 豹子头]
三、双链表应用实例
(1)添加节点
第一种方式,添加到尾部
思路:
- 先找到双向链表的最后这个节点temp
- temp.next = newNode
- newNode.pre = temp;
第二种方式,插入节点,可以根据排名插入到指定位置
思路:
-
需要按照编号的顺序添加
-
首先找到新添加的节点的位置, 是通过辅助变量(指针), 通过遍历来搞定
👉需要注意下面代码的先后顺序
newNode.next = temp.next;
if (temp.next != null) {
temp.next.pre = newNode;
}
temp.next = newNode;
newNode.pre = temp;
(2)删除节点
思路:
- 因为是双向链表,因此,我们可以实现自我删除某个节点
- 直接找到要删除的这个节点,比如temp
- temp.pre.next = temp.next
- temp.next.pre = temp.pre;
(3)修改节点
👉双链表修改节点思路和单链表思路一样
(4)遍历节点
👉遍历方式和单链表一样,可以向前遍历也可以向后遍历
代码实现如下:
public class DoubleLinkedListDemo {
public static void main(String[] args) {
System.out.println("双向链表");
//创建节点
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(hero3);
// doubleLinkedList.add(hero4);
//根据编号顺序添加
doubleLinkedList.addByOrder(hero1);
doubleLinkedList.addByOrder(hero4);
doubleLinkedList.addByOrder(hero3);
doubleLinkedList.addByOrder(hero2);
doubleLinkedList.list();
System.out.println("修改4号");
HeroNode2 newHeroNode = new HeroNode2(4, "林冲...", "豹子...");
doubleLinkedList.update(newHeroNode);
System.out.println("修改后:");
doubleLinkedList.list();
System.out.println("删除吴用");
doubleLinkedList.delete(3);
System.out.println("删除后:");
doubleLinkedList.list();
}
}
class DoubleLinkedList {
//先初始化一个头节点, 头节点不要动, 不存放具体数据
private HeroNode2 head = new HeroNode2(0, "", "");
public HeroNode2 getHead() {
return head;
}
//添加一个节点到双向链表的最后
public void add(HeroNode2 heroNode) {
//head节点不能动,因此需要一个辅助遍历temp
HeroNode2 temp = head;
//遍历链表,找到最后
while (true) {
//找到链表最后
if (temp.next == null) {
break;
}
//如果没有找到就把temp后移
temp = temp.next;
}
//当退出while循环时,temp就指向了链表的最后
//形成一个双向链表
temp.next = heroNode;
heroNode.pre = temp;
}
//第二种方式在添加英雄时,根据排名将英雄插入到指定位置(如果有这个排名,则添加失败,并给出提示)
public void addByOrder (HeroNode2 heroNode) {
//通过一个辅助指针来帮助找到添加的位置
//因为单链表,因为我们找的temp是位于添加位置的前一个节点,否则插入不了
HeroNode2 temp = head;
boolean flag = false;//标志添加的编号是否存在
while (true) {
if (temp.next == null) { //说明temp已经在链表最后
break;
}
if (temp.next.no > heroNode.no) { //位置找到,就在temp的后面插入
break;
} else if (temp.next.no == heroNode.no) { //添加的编号存在
flag = true; //编号存在
break;
}
temp = temp.next;
}
//判断flag的值
if (flag) {
//编号存在,不能添加
System.out.println("准备插入的编号" + heroNode.no +"已经存在了");
} else {
//插入到链表中,temp的后面
//这里格外要注意下面的先后顺序
heroNode.next = temp.next;
if (temp.next != null) {
temp.next.pre = heroNode;
}
temp.next = heroNode;
heroNode.pre = temp;
}
}
//修改节点的信息,根据no编号来修改,和单项链表一样
public void update(HeroNode2 newHeroNode) {
//判断链表是否为空
if (head.next == null) {
System.out.println("链表为空");
return;
}
//找到需要修改的节点
HeroNode2 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.println("没有找到编号为" +newHeroNode.no + "的节点");
}
}
//删除节点
//思路
// 1. 因为是双向链表,因此,我们可以实现自我删除某个节点
//2. 直接找到要删除的这个节点,比如temp
//3. temp.pre.next = temp.next
//4. temp.next.pre = temp.pre;
public void delete(int no) {
if (head.next == null) {
System.out.println("链表为空,无法删除");
return;
}
HeroNode2 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.println("要删除的"+ no +"这个节点不存在");
}
}
//显示链表,遍历链表
public void list() {
//判断链表是否为空
if (head.next == null) {
System.out.println("链表为空");
return;
}
//因为头节点不能动,所以需要一个辅助遍历temp
HeroNode2 temp = head.next;
while (true) {
if (temp == null) {
break;
}
//输出节点信息
System.out.println(temp);
//将next后移
temp = temp.next;
}
}
}
//定义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;
}
//重写toString
@Override
public String toString() {
return "HeroNode [no = "+ no +", name = "+ name +", nickname = "+ nickname +"]";
}
}
实现效果
双向链表
HeroNode [no = 1, name = 宋江, nickname = 及时雨]
HeroNode [no = 2, name = 卢俊义, nickname = 玉麒麟]
HeroNode [no = 3, name = 吴用, nickname = 智多星]
HeroNode [no = 4, name = 林冲, nickname = 豹子头]
修改4号
修改后:
HeroNode [no = 1, name = 宋江, nickname = 及时雨]
HeroNode [no = 2, name = 卢俊义, nickname = 玉麒麟]
HeroNode [no = 3, name = 吴用, nickname = 智多星]
HeroNode [no = 4, name = 林冲..., nickname = 豹子...]
删除吴用
删除后:
HeroNode [no = 1, name = 宋江, nickname = 及时雨]
HeroNode [no = 2, name = 卢俊义, nickname = 玉麒麟]
HeroNode [no = 4, name = 林冲..., nickname = 豹子...]
四、单项环形链表应用场景
Josephu(约瑟夫、约瑟夫环)问题
Josephu问题
N个人坐成一个圆环(编号为1 - N),从第1个人开始报数✋,数到K的人出列,后面的人重新从1开始报数。问最后剩下的人的编号。
👉思路:
用一个不带头结点的循环链表来处理Josephu 问题:先构成一个有n个结点的单循环链表,然后由k结点起从1开始计数,计到m时,对应结点从链表中删除,然后再从被删除结点的下一个结点又从1开始计数,直到最后一个结点从链表中删除算法结束。
约瑟夫环 51Nod - 1073
例如:N = 3,K = 2。2号先出列,然后是1号,最后剩下的是3号。
Input
2个数N和K,表示N个人,数到K出列。(2 <= N, K <= 10^6)
Output
最后剩下的人的编号
Sample Input
3 2
Sample Output
3
对于这道OJ题可以通过递归的方法,找到其中的规律便可AC,代码如下:
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
int n = scanner.nextInt();
int k = scanner.nextInt();
int ans = 0;
for (int i = 2; i <= n; i++) {
ans = (ans + k) % i;
}
System.out.println(ans + 1);
}
}
👉以此题为例,使用单项循环链表的方式实现,但是不能作为此题AC的代码,会超时!
思路如下:
首先构建一个单向的环形链表
- 先创建第一个节点, 让 first 指向该节点,并形成环形⚪
- 后面当我们每创建一个新的节点,就把该节点,加入到已有的环形链表中即可.f
然后进行报数出圈
- 需要创建一个辅助指针(变量) helper , 首先让其指向环形链表的最后这个节点
- 当开始报数时,让first 和 helper 指针同时 的移动 k - 1 次 🐌🐌
- 这时就可以将first 指向的节点 出圈
first = first .next
helper.next = first - 然后直到圈内只有最后一个人,即最后一个节点的时候(first == helper)结束循环🔄
- 最后的first中的编号即为最后一个人的编号
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
int n = scanner.nextInt();
int k = scanner.nextInt();
addNode(n);
countNode(n, k);
}
public static Node first = null;
public static void addNode(int nums) {
Node current = null; // 辅助指针
//创建一个循环链表
for (int i = 1; i <= nums; i++) {
Node node = new Node(i);
if (i == 1) { //first指向第一个孩子
first = node;
first.next = first;
current = first; //
} else {
current.next = node;
node.next = first;
current = node;
}
}
}
public static void countNode(int nums, int k) {
//创建一个辅助指针helper
Node helper = first;
//需要创建一个辅助指针helper,事先应该指向环形链表的最后这个节点
while (true) {
if (helper.next == first) { //说明helper指向最后一个节点
break;
}
helper = helper.next;
}
//当报数时,让first 和 helper指针同时移动 k- 1次,然后出圈
//这是一个循环操作,直到圈中只有一个节点
while (true) {
if (first == helper) {//说明圈中只有一个节点
break;
}
for (int i = 0; i < k - 1; i++) {
first = first.next;
helper = helper.next;
}
//这是first指向的节点,就是要出圈的节点
first = first.next;
helper.next = first;
}
System.out.println(first.no);
}
}
//创建一个Node节点类
class Node {
public int no;
public Node next;
public Node(int no) {
this.no = no;
}
}
加油!!!😘😘