文章目录
- 链表
- 1、基本介绍
- 2、单向链表
- 2.1 带头节点的单向链表
- 测试类:
- 链表实现类:
- 2.2 不带头节点的单向链表
- 2.3 练习
- 测试类:
- 链表实现类:
- 3、双向链表
- 测试类:
- 双向链表实现类:
- 4、单向环形链表
- **测试类**:
- **实现类**:
链表
1、基本介绍
-
什么是链表?
链表(Linked List)是用链式存储结构实现的线性表。链表示意图:
-
链表的组成:
数据域
+引用域
(数据域和引用域合称结点或元素)- 数据域存放数据元素自身的数据
- 引用域存放相邻结点的地址
-
链表的特点:
-
链表中元素的联系依靠引用域
-
具有线性结构的特点,链表所使用的逻辑结构是线性结构
-
具有链式存储结构的特点,所使用的物理存储结构是链式存储
-
-
链表的分类:
-
单向链表:单链表是一种最简的链表,只有一个引用域1next
特点:通过next可以访问到后继结点,终端结点的引用域指向null
-
双向链表:具有两个引用域prev和next,prev用来保存前驱结点的地址,next用来保存后继结点的地址
特点:通过next可以访问后继结点,终端结点的next指向null;通过prev可以访问到前驱节点,起始结点的prev指向null
-
循环链表:循环链表本质是一种特殊的单向链表,只是它的终端结点指向了开始结点(也就是next存放了开始结点的地址)
特点:所有结点都能具有前驱节点和后继结点
-
-
链表的使用场景:对查找速度要求不高,但对插入和删除速度要求高时,可以使用链表。常见的比如:
2、单向链表
单向链表(简称单链表)有带头结点的单链表,也有不带头链表的单链表。
-
单链表的基本操作:
- 非空判断:判断链表中是否含有元素
- 求表长度:获取链表中所有元素的个数
- 插入结点:在单链表中添加一个新的结点
- 删除结点:删除单链表中的结点
- 取表元素:更具所给索引,确定该索引所在链表的结点
- 定位元素:根据所给值,确定该元素所在链表的索引号
- 修改元素:根据所给索引,修改对应的结点
- 清空链表:清空链表中所有的元素
2.1 带头节点的单向链表
带头结点就是先固定一个头节点,用来标识链表的初始位置,它的data域不存任何东西,它的next域用来第一个结点的地址,每次遍历链表或定位结点都需要借助一个辅助变量temp来实现。
插入结点示意图:
删除结点示意图:
修改结点示意图:
遍历经验总结:当我们想要进行的操作的结点依赖于前一个结点时,比如插入、删除、修改等操作操作,就必须从head结点开始遍历,否则会出现空指针异常;当我们想要进行的操作不依赖前一个结点时,就无须从head结点开始遍历,比如根据id获取结点,非空判断、获取链表长度、展示链表等操作。
测试类:
package com.hhxy.linkedlist;
import java.util.Scanner;
import com.hhxy.queue.ArrayQueue2;
/**
* 单向链表测试类
* @author ghp
* 测试数据:
* 1 宋江 及时雨
* 2 林冲 豹子头
* 3 鲁智深 花和尚
* 4 吴用 智多星
*/
public class SingleLinkedListTest {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
SingleLinkedListDemo1 sll = new SingleLinkedListDemo1();//创建链表
OUT:
while(true) {
System.out.println("-------------------单向链表操作界面-----------------");
System.out.println("请输入操作指令:");
System.out.println("0 : 退出程序");
System.out.println("1 : 在链尾添加结点");
System.out.println("2 : 按id从小到大的顺序添加结点");
System.out.println("3 : 根据id获取结点");
System.out.println("4 : 根据id删除结点");
System.out.println("5 : 获取链表中元素的个数");
System.out.println("6 : 展示链表中所有的元素");
System.out.println("7 : 根据id修改结点");
System.out.println("8 : 清空链表");
//用于接收用户输入
int id;
String name="";
String alias="";
Student student = null;
switch(sc.next()) {
case "0": //退出程序
System.out.println("正在退出程序~~~");
break OUT;
case "1": //在链尾添加结点
System.out.println("请按照 id name alias 的格式输入要添加的元素:");
id = sc.nextInt();
name = sc.next();
alias = sc.next();
student = new Student(id,name,alias);
if(sll.add(student)) System.out.println("结点:"+student+"添加成功!");
break;
case "2"://按id从小到大的顺序添加结点
System.out.println("请按照 id name alias 的格式输入要添加的元素:");
id = sc.nextInt();
name = sc.next();
alias = sc.next();
student = new Student(id,name,alias);
if(sll.addById(student)) System.out.println("结点:"+student+"添加成功!");
break;
case "3"://根据id获取结点
System.out.println("请输入要获取结点的id号:");
id = sc.nextInt();
try {
student = sll.get(id);
System.out.println(id+"号结点为:"+student);
}catch(Exception e){
System.out.println(e.getMessage());
}
break;
case "4"://根据id删除结点
System.out.println("请输入要删除结点的id号:");
id = sc.nextInt();
try {
if(sll.remove(id)) System.out.println("结点删除成功!");
}catch(Exception e) {
System.out.println(e.getMessage());
}
break;
case "5"://获取链表中结点的个数(不包括头节点)
System.out.println("链表中的结点个数为:"+sll.size());
break;
case "6"://展示链表中所有的结点(不包括头节点)
sll.show();
break;
case "7"://根据id修改结点
System.out.println("请按照 id name alias 的格式输入要修改的元素:");
student = new Student(sc.nextInt(),sc.next(),sc.next());
try {
if(sll.update(student)) System.out.println("修改成功");
}catch(Exception e) {
System.out.println(e.getMessage());
}
break;
case "8"://清空链表
if(sll.clear()) System.out.println("链表已成功清空!");
break;
default:
System.out.println("请输入有效指令!");
break;
}
}
System.out.println("程序已退出");
}
}
链表实现类:
package com.hhxy.linkedlist;
//结点类
class Student{
//数据域(将成员变量设置为public方便外部访问)
public int id;
public String name;
public String alias;
//引用域
public Student next;
public Student(int id, String name, String alias) {
this.id = id;
this.name = name;
this.alias = alias;
}
@Override
public String toString() {
return "[id=" + id + ", name=" + name + ", alias=" + alias + "]";
}
}
//链表类
public class SingleLinkedListDemo1 {
//初始化头结点
Student head = new Student(-99,"","");
/**
* 判断链表是否为空
* @return true表示链表为空
*/
public boolean isEmpty() {
//因为头节点是链表位置的标识,不能动,所以使用一个辅助引用来遍历链表
Student temp = head.next;
if(temp!=null) {
//head后面存在至少一个元素,所以链表不为空
return false;
}
return true;
}
/**
* 在链尾添加结点
* @param student 待添加的结点
* @return true表示添加成功
*/
public boolean add(Student student) {
//同理,因为链表头节点不能动。
//注意:需要是从头节点开始遍历,因为链表可能为空,如果从头节点后一个遍历,当链表为空时会报空指针异常
Student temp = head;
//遍历寻找尾结点。因为temp=head,所以是从头节点开始遍历
while(temp.next != null) {
temp = temp.next;
}
//已找到链表尾结点,进行指向
temp.next = student;
return true;
}
/**
* 按照id从小到大的顺序添加结点
* @param student 待添加的结点
* @return true表示添加成功
*/
public boolean addById(Student student) {
Student temp = head;
boolean flag = true;//用于判断链表是加在尾结点,还是加在结点之间
while(temp.next != null) {
if(student.id < temp.next.id) {
//说明是添加在结点之间
flag = false;
break;
}
temp = temp.next;
}
if(flag) {
//如果添加的结点是在尾结点
temp.next = student;
}else {
//添加的结点是在结点之间
student.next = temp.next;//切记:先改变后一个指向,再改变前一个指向
temp.next = student;
}
return true;
}
/**
* 根据id获取结点
* @param id
* @return 返回对应id的结点
*/
public Student get(int id) {
if(isEmpty()) {
throw new RuntimeException("该链表为空!");
}
Student temp = head.next;
//从head结点后面开始遍历
boolean flag = false;//判断链表中是否存在待获取的结点
while(temp != null) {
if(temp.id == id) {
//找到id对应的结点
flag = true;
break;
}
temp = temp.next;
}
if(flag) {
//如果找到id对应结点
return temp;
}else {
//如果没有找到id对应的结点
throw new RuntimeException("待获取的结点不存在!");
}
}
/**
* 根据id删除结点
* @param id 待删除结点的id
* @return true表示删除成功
*/
public boolean remove(int id) {
if(isEmpty()) {
throw new RuntimeException("链表为空!");
}
Student temp = head;//删除结点需要依赖前一个结点,所以从头节点开始遍历
boolean flag = false;//判断链表中是否存在待删除的结点
while(temp.next != null) {
if(temp.next.id == id) {
//找到该结点
flag = true;
break;
}
temp = temp.next;
}
if(flag) {
//如果找到了要删除的结点
temp.next = temp.next.next;
}else {
//如果没有找到id对应的结点
throw new RuntimeException("待删除的结点不存在!");
}
return true;
}
/**
* 获取链表中结点的个数(不包括头节点)
*/
public int size() {
Student temp = head;
int count = 0;
//这里虽然遍历了头节点,但是没有遍历尾结点
while(temp.next != null) {
count++;
temp = temp.next;
}
return count;
}
/**
* 展示链表中所有的结点(不包括头节点)
*/
public void show() {
if(isEmpty()) {
System.out.println("链表为空!");
return;
}
//注意:不需要展示头节点!
Student temp = head;
while(temp.next != null) {
System.out.println(temp.next);
temp = temp.next;
}
}
/**
* 根据id修改结点
* @param student 待修改的结点
* @return true表示修改成功
*/
public boolean update(Student student) {
if(isEmpty()) {
throw new RuntimeException("链表为空!");
}
Student temp = head;
boolean flag = false;//判断链表是否修改成功
while(temp.next != null) {
if(temp.next.id == student.id) {
//找到要修改的链表
flag = true;
student.next = temp.next.next;
temp.next = student;
break;
}
temp = temp.next;
}
if(flag) {
//如果修改成功
return true;
}else {
//如果链表中没有找到待删除的结点
throw new RuntimeException("链表中不存在该结点");
}
}
/**
* 清空链表
* @return true表示清空成功
*/
public boolean clear() {
/*方式一:直接将头节点指向空(节约时间,但占内存)
* head.next = null;
* 除头节点以外其它结点存在引用,JVM不会回收结点内存,仍然会占据内存
* 这种清楚方法很费内存!
*/
//方式二:将所有结点占据的内存都进行释放(耗时,但不占内存)
Student2 temp = head;//这里需要从后往前遍历,逐步去掉所有结点的引用,否则无法遍历下取
while(head.next != null) {
for (int i = size(); i > 1; i--) {
temp = temp.next;
}
temp.next = null;
}
return true;
}
}
2.2 不带头节点的单向链表
略……逻辑思路都差不多,只是将头节点换成一个头指针
不带头节点和带头结点的主要区别:带头结点遍历的时候、不能将头节点进行计数;而不带结点能够直接进行遍历
本质上两者并没有什么太大区别,带头节点的链表没有指针,头结点就相当于头指针,而不带头节点的链表是由头指针的
注意:这里所谓的指针和结点其实都是结点对象,只是指针初始值为null,结点要进行初始化
2.3 练习
- 反转链表示意图:
- 合并链表示意图:
测试类:
/**
* 练习题1:获取链表倒数第k个结点
* 练习题2:将链表反转
* 练习题3:从尾到头打印链表的结点
* 练习题4:合并两个有序链表,合并后仍然有序
*/
//测试类:
public class SingleLinkedListDemo3{
public static void main(String[] args) {
Node node1 = new Node(1);
Node node2 = new Node(2);
Node node3 = new Node(3);
SingleLinkedList sll = new SingleLinkedList();
sll.add(node1);
sll.add(node2);
sll.add(node3);
System.out.println("链表原始状态:");
sll.show();
System.out.println("------------------------");
//测试1:测试获取链表倒数第k个结点
Node t = sll.findLastIndexNode(sll, 1);
System.out.println(sll.findLastIndexNode(sll,1));
System.out.println(sll.findLastIndexNode(sll,2));
System.out.println(sll.findLastIndexNode(sll,3));
System.out.println("-------------------------");
//测试2:测试将链表反转
sll.reverset(sll);
System.out.println("反转后的链表:");
sll.show();
System.out.println("-------------------------");
//测试3:从头到位打印链表
System.out.println("反向打印链表:");
sll.reversetPrint(sll);
System.out.println("-------------------------");
//测试4:将两个有序链表合并,合并后仍然有序
SingleLinkedList list1 = new SingleLinkedList();
SingleLinkedList list2 = new SingleLinkedList();
Node node4 = new Node(4);
Node node5 = new Node(5);
Node node6 = new Node(6);
Node node7 = new Node(7);
Node node8 = new Node(8);
Node node9 = new Node(9);
Node node10 = new Node(10);
Node node11 = new Node(11);
list1.add(node4);
list1.add(node7);
list1.add(node8);
list1.add(node10);
list1.add(node11);
System.out.println("链表1:");
list1.show();
System.out.println("链表2:");
list2.add(node5);
list2.add(node6);
list2.add(node9);
list2.show();
SingleLinkedList list = new SingleLinkedList();
list = list.combine(list1, list2);
System.out.println("合并后的链表:");
list.show();
}
}
链表实现类:
package com.hhxy.linkedlist;
import java.util.Stack;
//结点类
class Node {
int n;
Node next;
public Node(int n) {
this.n = n;
}
@Override
public String toString() {
return "[n=" + n + "]";
}
}
//链表类:
public class SingleLinkedList {
//初始化头节点
public Node head = new Node(-99);
/**
* 添加结点
*/
public void add(Node node) {
Node current = head;
while(current.next != null) {
current = current.next;
}
current.next = node;
}
/**
* 获取链表的长度
* @return
*/
public int size() {
Node current = head.next;
int count = 0;
while(current != null) {
count++;
current = current.next;
}
return count;
}
/**
* 展示
*/
public void show() {
Node current = head.next;
while(current != null) {
System.out.println(current);
current = current.next;
}
}
/*--------------------核心方法-------------------------*/
/**
* 寻找链表倒数第k个结点
* @param index
* @return
*/
public Node findLastIndexNode(SingleLinkedList sll,int index) {
Node head = sll.head;
if(index <0 || index>sll.size() || head.next == null) {
return null;
}
Node current = head.next;
//将指针从第二个结点开始往后移动index位
for (int i = 0; i < size()-index; i++) {
current = current.next;
}
return current;
}
/**
* 将链表反转
* @param sll 待反转的链表
*/
public void reverset(SingleLinkedList sll) {
Node head = sll.head;
if(head.next == null || head.next.next == null) {
//当前链表为空,或者只有一个结点,直接返回
return;
}
SingleLinkedList sllTemp = new SingleLinkedList();//创建一个新链表
Node headTemp = sllTemp.head;
Node temp = null;//用来存旧链表的引用,方便遍历旧链表
Node current = head.next;//辅助遍历旧链表
while(current != null) {
temp = current.next;//不保存,新链表就会断开,就无法进行遍历了
current.next = headTemp.next;//指向新创建的头结点的后面的结点
headTemp.next = current;//新创建的头结点,指向插入的结点
current = temp;//指针往后移
}
head.next = headTemp.next;
}
/**
* 反向打印链表
* @param sll
*/
public void reversetPrint(SingleLinkedList sll) {
Node head = sll.head;
if(head.next == null) {
return;
}
/*
//方式一:使用findLastIndexNode方法(要先实现findLastIndexNode方法,不值得推荐)
for(int i=1;i<=sll.size();i++) {
System.out.println(sll.findLastIndexNode(sll, i));
}
//方式二:使用reverset方法(要先实现reverset方法,并且改变了链表的结构,不值得推荐)
reverset(sll);
sll.show();
*/
//方式三:使用栈(强烈推荐)
Stack<Node> stack = new Stack<>();
Node current = head.next;
while(current != null) {
stack.push(current);
current = current.next;
}
while(stack.size()>0) {
System.out.println(stack.pop());
}
}
/**
* 合并两个有序链表,合并后仍然有序(这里我是默认按从小到大排序的)
*/
public SingleLinkedList combine(SingleLinkedList sll1,SingleLinkedList sll2) {
Node head1 = sll1.head.next;//用于遍历sll1链表
Node head2 = sll2.head.next;
if(head1 == null || head2 == null) {
//只要有一个链表为空就直接返回
return head1 != null ? sll1 : sll2;
}
SingleLinkedList sll = new SingleLinkedList();//合并后的链表
Node temp=sll.head;//用来给sll链表添加结点的
while (head1 != null && head2 != null){
if (head1.n < head2.n){
//链表1的结点是当前最小结点
temp.next = head1;//新链表连接最小结点
temp = temp.next;//每新增一个结点,temp就往后移一位,保证他在尾结点方便连接新结点
head1 = head1.next;//链表1的指针也往后移一位
}else{
//链表2的结点是当前最小结点
temp.next = head2;
temp = temp.next;
head2 = head2.next;
}
}
if (head1 != null && head2 == null){
//经过一一段时间的合并后,sll2的链表为空了,直接就将sll1链表后面的结点拼接上去
temp.next = head1;
}
if (head1 == null && head2 != null){
temp.next = head2;
}
return sll;
}
/*------------------------------------------------*/
}
3、双向链表
双向链表相对单向链表就较为简单了,因为每个结点既能往后遍历,又能往前遍历 ,对于插入、删除、修改都无需像单链表一样依靠前一个结点。
与单链表的主要区别:
-
遍历不仅可以往前遍历,还可以往后遍历
-
插入、删除、修改不需要依赖前一个结点(在链尾插入需要依赖尾结点)
-
添加的时候,需要进行双向绑定!
-
双向链表插入示意图:
-
双向链表删除示意图:
测试类:
和单向链表的测试方法相同
示意图:
双向链表实现类:
其实只要理解了单向链表,再来看双向链表就会觉得so easy😄单向链表的方法双向链表都能使用,只是添加和修改的时候,需要多修改下prev的的指向。
package com.hhxy.linkedlist.doubles;
//结点类
class Student2{
public int id;
public String name;
public String alias;
public Student2 prev;//指向前一个结点
public Student2 next;//指向后一个结点
public Student2(int id, String name, String alias) {
super();
this.id = id;
this.name = name;
this.alias = alias;
}
@Override
public String toString() {
return "Student [n=" + id + ", name=" + name + ", alias=" + alias + "]";
}
}
//链表类
public class DoubleLinkedListDemo1 {
//初始化头节点
public Student2 head = new Student2(-99,"","");
/**
* 判断链表是否为空
* @return true表示链表为空
*/
public boolean isEmpty() {
//因为头节点是链表位置的标识,不能动,所以使用一个辅助引用来遍历链表
Student2 temp = head.next;
if(temp!=null) {
//head后面存在至少一个元素,所以链表不为空
return false;
}
return true;
}
/**
* 在链尾添加结点
* @param student2 待添加的结点
* @return true表示添加成功
*/
public boolean add(Student2 student2) {
//同理,因为链表头节点不能动。
//注意:需要是从头节点开始遍历,因为链表可能为空,如果从头节点后一个遍历,当链表为空时会报空指针异常
Student2 temp = head;
//遍历寻找尾结点。因为temp=head,所以是从头节点开始遍历
while(temp.next != null) {
temp = temp.next;
}
//形成双向链表
temp.next = student2;
student2.prev = temp;
return true;
}
/**
* 按照id从小到大的顺序添加结点
* @param student2 待添加的结点
* @return true表示添加成功
*/
public boolean addById(Student2 student2) {
Student2 temp = head;
boolean flag = true;//用于判断链表是加在尾结点,还是加在结点之间
while(temp.next != null) {
if(student2.id < temp.next.id) {
//说明是添加在结点之间
flag = false;
break;
}
temp = temp.next;
}
if(flag) {
//如果添加的结点是在尾结点
//形成双向链表
temp.next = student2;
student2.prev = temp;
}else {
//添加的结点是在结点之间,注意要形成双向指向
student2.next = temp.next;//切记:先改变后一个指向,再改变前一个指向
temp.next.prev = student2;
//前面一根线
temp.next = student2;
student2.prev = temp;
}
return true;
}
/**
* 根据id获取结点
* @param id
* @return 返回对应id的结点
*/
public Student2 get(int id) {
if(isEmpty()) {
throw new RuntimeException("该链表为空!");
}
Student2 temp = head.next;
//从head结点后面开始遍历
boolean flag = false;//判断链表中是否存在待获取的结点
while(temp != null) {
if(temp.id == id) {
//找到id对应的结点
flag = true;
break;
}
temp = temp.next;
}
if(flag) {
//如果找到id对应结点
return temp;
}else {
//如果没有找到id对应的结点
throw new RuntimeException("待获取的结点不存在!");
}
}
/**
* 根据id删除结点
* @param id 待删除结点的id
* @return true表示删除成功
*/
public boolean remove(int id) {
if(isEmpty()) {
throw new RuntimeException("链表为空!");
}
//使用双向链表可以进行自我删除
Student2 temp = head.next;//删除结点需要依赖前一个结点,所以从头节点开始遍历
boolean flag = false;//判断链表中是否存在待删除的结点
while(temp != null) {
if(temp.id == id) {
//找到该结点
flag = true;
break;
}
temp = temp.next;
}
if(flag) {
//如果找到了要删除的结点
temp.prev.next = temp.next;//前一根线
if(temp.next != null) {//要排除最后一个结点的可能,否则会出现空指针异常
temp.next.prev = temp.prev;//后一根线
}
}else {
//如果没有找到id对应的结点
throw new RuntimeException("待删除的结点不存在!");
}
return true;
}
/**
* 获取链表中结点的个数
* @return
*/
public int size() {
Student2 temp = head;
int count = 0;
while(temp.next != null) {
count++;
temp = temp.next;
}
return count;
}
/**
* 展示链表中所有的结点
*/
public void show() {
if(isEmpty()) {
System.out.println("链表为空!");
return;
}
Student2 temp = head;
while(temp.next != null) {
System.out.println(temp.next);
temp = temp.next;
}
}
/**
* 根据id修改结点
* @param student2 待修改的结点
* @return true表示修改成功
*/
public boolean update(Student2 student2) {
if(isEmpty()) {
throw new RuntimeException("链表为空!");
}
Student2 temp = head.next;
boolean flag = false;//判断链表是否修改成功
while(temp != null) {
if(temp.id == student2.id) {
//找到要修改的链表
flag = true;
//后一根线
student2.next = temp.next;
if(temp.next!=null) {
//排除最后一个节点
temp.next.prev = student2;
}
//前一根线
temp.prev.next = student2;
student2.prev = temp.prev;
break;
}
temp = temp.next;
}
if(flag) {
//如果修改成功
return true;
}else {
//如果链表中没有找到待删除的结点
throw new RuntimeException("链表中不存在该结点");
}
}
/**
* 清空链表
* @return
*/
public boolean clear() {
/*方式一:直接将头节点指向空(节约时间,但占内存)
* head.next = null;
* 除头节点以外其它结点存在引用,JVM不会回收结点内存,仍然会占据内存
* 这种清楚方法很费内存!
* return true;
*/
//方式二:将所有结点占据的内存都进行释放(耗时,但不占内存)
Student2 temp = head;//这里需要从后往前遍历,逐步去掉所有结点的引用,否则无法遍历下取
Student2 current = head;
while(current.next != null) {
current = temp;
current.next = null;
current.prev = null;
temp = temp.next;
}
return true;
}
}
4、单向环形链表
基本和单向链表类似,也可以分为带头节点,和不带头结点点,这里演示的是不带头结点的单向环形链表,单向环形链表和单向链表唯一的区别:尾结点的next不指向空,而是指向开始节点。
主要思想还是在单链表那一节😄只要掌握单向链表,这些双向链表还有单向循环链表就是弟弟(′д` )…彡…彡直接套用第一节的接口,实现所有的方法
测试类:
和2.1一样,换个对象就行了(这个测试类真渣😆)
实现类:
这里主要记录以下按顺序插入结点的思路,怕以后忘记了。其实主要思想还是和单向链表的
addById
方法的逻辑是一致的,主要是要考虑循环!思路主要如下:
- 先将链表添加分为两大类,首结点的添加 和 非首结点的添加,因为首结点的添加需要自动成环
- 再将非首结点的添加又分为在 添加在首结点之前 和 之后,之前需要移动
first
指针,之后不需要移动示意图:
删除结点:
- 先将删除分为两大类,删除头结点 和 删除普通结点
- 删除头结点又可以分为两类,链表只有一个头结点 和 除了头结点还有其它结点
- 删除普通结点时需要注意链表是单向的,删除操作需要依赖待删除结点的前一个结点
修改结点的逻辑思路和删除类似,不在赘述,示意图:
清空链表:链表的清空有两种方法,一种是直接让
first=null
,这种清空简单省事,但是是假清空,链表仍然存在内存中!第二种方法是让每个结点的next指向空,然后将
first=null
,这种费脑子但是省空间
package com.hhxy.linkedlist.circular;
//结点类
class Student{
public int id;
public String name;
public String alias;
public Student next;
public Student(int id, String name, String alias) {
super();
this.id = id;
this.name = name;
this.alias = alias;
}
@Override
public String toString() {
return "[id=" + id + ", name=" + name + ", alias=" + alias + "]";
}
}
//链表类
public class CircularLinkedListDemo1 {
private Student first = null;//注意这是头指针,不是头结点!
/**
* 非空判断
* @return true表示为空
*/
public boolean isEmpty() {
//因为是没有头结点,所以直接判断first
if(first != null) {
return false;
}else {
return true;
}
}
/**
* 在尾结点添加结点
* @param student
* @return true表示结点添加成功
*/
public boolean add(Student student) {
if(first == null) {
//对第一个结点进行单独考虑
first = student;
first.next = first;//构成环状
return true;
}else {
Student current = first;
//找到最后一个结点
while(current.next != first) {
current = current.next;
}
//找到后将节点加入环形链表中
current.next = student;
student.next = first;
return true;
}
}
/**
* 根据id从小到大的顺序添加结点
* @return true表示添加成功
*/
public boolean addById(Student student) {
if(first == null) {
//第一个结点单独成环
first = student;
first.next = first;
}else {
Student current = first;
if(student.id < current.id && current == first) {
//说明这个结点比头结点还要小,要将头指针移动位置
current.next = student;
student.next = current;
first = student;//将first移动到student上
return true;
}
while(current.next != first) {
//寻早结点添加的位置
if(student.id < current.next.id) {
//已找到添加的位置
break;
}
current = current.next;
}
student.next = current.next;//切记:一定要先改变后一根线,不然链表会断裂!
current.next = student;
}
return true;
}
/**
* 根据id获取结点
*/
public Student get(int id) {
if(isEmpty()) {
throw new RuntimeException("链表为空!");
}
Student current = first;
boolean flag = false;
while(true) {
if(current.id == id) {
//找到就直接结束遍历
flag = true;
break;
}
if(current.next == first) {
//如果是最后一个结点,就表明还没有找到,直接结束遍历
break;
}
current = current.next;//辅助指针后移,遍历链表
}
if(flag) {
//找到就返回这个结点
return current;
}else {
//没有找到,打印提示信息
throw new RuntimeException("链表中不存在该结点!");
}
}
/**
* 根据id删除结点
*/
public boolean remove(int id) {
if(isEmpty()) {
throw new RuntimeException("链表为空!");
}
if(first.id == id) {
//当头结点就是要删除的结点时,分类讨论
if(first.next == first) {
//如果链表只有头结点时
first = null;
return true;
}else {
//如果链表除了头结点还有其它结点时,需要移动first指针
Student current = first;
//找到尾结点
while(current.next != first) {
current = current.next;
}
current.next = current.next.next;
first = current.next;//移动头指针
return true;
}
}
//删除普通结点
Student current = first;
boolean flag = false;//判断链表中是否存在该结点
while(true) {
if(current.next.id == id) {//这里使用current.next判断是为了使用前一个结点
//找到结点直接退出
flag = true;
break;
}
if(current.next == first) {
//遍历完成,直接退出
break;
}
current = current.next;
}
if(flag) {
//找到待删除的结点,利用前一个结点将其删除(思路和单项链表是一样的)
current.next = current.next.next;
return true;
}else {
throw new RuntimeException("该结点不存在!");
}
}
/**
* 获取链表长度
*/
public int size() {
Student current = first;
if(isEmpty()) {
return 0;//这里要不要都无所谓,只是习惯了~~~
}
int count = 0;
while(true) {
count++;
if(current.next == first) {
break;
}
current = current.next;
}
return count;
}
/**
* 展示链表
*/
public void show() {
Student current = first;
if(first == null) {
//排除空链表
throw new RuntimeException("链表为空!");
}
while(true) {
System.out.println(current);
if(current.next == first) {
//当发现结点是最后一个结点直接退出打印
break;
}
current = current.next;
}
}
/**
* 根据id修改结点
*/
public boolean update(Student student) {
if(isEmpty()) {
throw new RuntimeException("链表为空!");
}
if(student.id == first.id) {
//修改的结点是头节点
if(first.next == first) {
//只有一个头节点
first = student;
first.next = student;
return true;
}else {
//除了头节点还有其它结点
Student current = first;
//找到尾结点
while(current.next != first) {
current = current.next;
}
student.next = current.next.next;//修改后一根线
current.next = student;//修改前一根线
first = student;//修改头指针
return true;
}
}
//修改的结点是普通结点
Student current = first;
boolean flag = false;//判断链表中是否存在该结点
while(current.next != first) {
if(current.next.id == student.id) {
//找到结点,直接退出
flag = true;
break;
}
current = current.next;
}
if(flag) {
student.next = current.next.next;//修改后一根线,防止链表断裂
current.next = student;
return true;
}else {
throw new RuntimeException("不存在该结点!");
}
}
/**
* 清空链表
*/
public boolean clear() {
/*方式一:直接将头节点指向空(节约时间,但占内存)
* first.next = null;
* 除头节点以外其它结点存在引用,JVM不会回收结点内存,仍然会占据内存
* 这种清楚方法很费内存!
* return true;
*/
//方式二:将所有结点占据的内存都进行释放(耗时,但不占内存)
if(isEmpty()) {
return true;
}
if(first == first.next) {
//只有一个结点
first = null;
return true;
}
Student temp = first;//临时存储current的引用,辅助遍历
Student current = first;
//将链表每个结点的引用都断开,这样每个结点都没有被引用,就能被JVM给回收
while(current.next != first) {
temp = temp.next;
current.next = null;
current = temp;
}
//将最后一个结点的引用 和头指针 设为空
current.next = null;//不断开最后一个接待你的引用,头节点就不会被回收
first = null;
return true;
}
}
引用域:是链表结点中一片很小的空间,用来存放后继结点的地址 ↩︎