1. 什么是链表
链表: 链表是由一组不必相连(可相连可不相连)的内存结构(节点)按特定的顺序链接在一起的抽象数据类型。
链表是离散存储线性结构
离散: 链表的每个节点可能存于不相连的内存块中,即两个或多个节点所处的内存块的内存地址不相连,所以说链表是离散的。
线性: 通过链表的头结点可以找到下一个节点,直至找到尾结点,就像多个有顺序的点可以连成一条线,所以说链表是线性的。
链表由节点组成,链表的节点离散分配,节点彼此通过指针相连,每个节点只有一个前驱结点和一个后续节点,其中,头节点没有前驱结点,尾结点没有后续节点。
链表的结构举例:
/**
*@ClassName: Link
*@Description: 单链表
*
*/
public class Link {
/**
*@ClassName: Node
*@Description: 单链表的单个节点类
*
*/
private class Node {
// 节点数据,类型自定义,数量自定义
private Object data;
// 下一个节点指针
private Node next;
}
// 单链表的头结点
private Node head;
// 单链表的节点个数
private int size;
}
2. 链表的种类
链表有三种:
- 单链表
- 双链表
- 循环链表
其中循环链表可分为单向循环链表和双响循环链表。
这里主要介绍单链表。
3. 链表与数组的区别
链表的优点:
- 存储空间没有限制
- 插入和删除节点很快
链表的缺点:
- 读取节点速度很慢
数组是一种连续存储线性结构,数组每个元素的类型相同,大小相同。
数组的优点:
- 读取元素速度很快
数组的缺点:
- 存储空间有限制,需要事先知道数组的长度
- 需要连续的内存块
- 插入和删除元素很慢
可见数组和链表的优缺点基本相反,当主要对数据进行读取时优先考虑使用数组,当插入删除数据操作频繁时优先考虑使用链表、
4. 链表的使用
链表操作一般为,插入节点,删除节点,查找节点,遍历链表。
节点示例图:
链表示例图:
插入节点示例图:
删除节点示例图:
使用案例:
package work.java.xzk10301002.link;
/**
*@ClassName: Link
*@Description: 单链表
*
*/
public class Link {
/**
*@ClassName: Node
*@Description: 单链表的单个节点类
*
*/
private class Node {
// 节点数据
private int data;
// 下一个节点指针
private Node next;
public Node(int data, Node next) {
this.data = data;
this.next = next;
}
public Node(int data) {
this(data, null);
}
}
// 单链表的头结点
private Node head;
// 单链表的节点个数
private int size;
public Link() {
}
/**
* @Description //TODO 对单链表进行初始化,方便演示遍历
* @Param []
* @return void
**/
public void initLink() {
Node node1 = new Node(1);
this.head = node1;
Node node2 = new Node(4);
node1.next = node2;
Node node3 = new Node(3);
node2.next = node3;
Node node4 = new Node(4);
node3.next = node4;
this.size = 4;
}
/**
* @Description //TODO 单链表进行遍历,打印每一个节点的数据
* @Param []
* @return void
**/
public void printLink() {
// curNode为当前操作节点,初始为head节点
Node curNode = head;
// 当前节点不为空则打印节点数据data
while (curNode != null) {
System.out.print(curNode.data);
System.out.print(" -> ");
//打印完当前节点数据则指针后移,使下一个节点为当前节点进入下一次循环
curNode = curNode.next;
}
// 退出循环则表示单链表已经遍历完毕
System.out.println("null");
}
/**
* @Description //TODO 在单链表指定位置节点后面插入新的节点
* @Param [data, index] date为新节点的数据,index表示在单链表中第index个节点后插入节点
* @return void
**/
public void addNode(int data, int index) {
// 检查节点的插入点是否合理,合理则继续执行,不合理则给出提示并停止操作
if (index < 0 || index > this.size) {
System.out.println("单链表中不存在第" + index + "个节点,插入节点失败!");
return;
}
// 可以插入新节点,创建新的节点对象并初始化
Node node = new Node(data);
// 当index为0时表示要在单链表的头结点前插入新节点
if (index == 0) {
// 新节点指向头结点
node.next = this.head;
// 单链表头指针指向新节点
this.head = node;
// 节点插入完毕后,单链表节点数加1
this.size++;
// 给出插入节点的反馈
System.out.println("成功插入新的头结点!节点数据:" + data);
} else {
// 遍历单链表,找到要链接新节点的目标节点curNode
Node curNode = this.head;
for (int i = 1; i < index; i++) {
curNode = curNode.next;
}
// 找到后使新节点指向curNode的下一个节点
node.next = curNode.next;
// 使curNode节点指向新节点
curNode.next = node;
// 节点插入完毕后,单链表节点数加1
this.size++;
// 给出插入节点的反馈
// 这里使用占位符进行格式化输出,printf继承了C语言中printf的一些特性
System.out.printf("成功在第%d个结点后插入新节点!节点数据:%d%n", index, data);
}
// 执行添加节点操作后打印单链表
printLink();
}
/**
* @Description //TODO 在单链表的末尾插入新节点
* @Param [data] 新节点的数据
* @return void
**/
public void addNode(int data) {
// 只有一个参数表示要在单链表的末尾插入节点,实际上可视为插入位置是节点总数后面
addNode(data, this.size);
}
/**
* @Description //TODO 根据传入的参数删除指定的节点
* @Param [index] 索引,表示单链表中要删除的第index个节点
* @return void
**/
public void deleteNodeByIndex(int index) {
// 通过索引指定删除第index个节点,先判断index是否合理
if (index < 1 || index > this.size) {
// index不合理给出提示并停止操作
System.out.println("单链表中不存在第" + index + "个节点,删除失败!");
return;
} else if (index == 1) {
// index=1表示删除头节点,此时将头指针指向头结点的下一个节点即可
this.head = head.next;
// 单链表节点总数减1
this.size--;
} else {
// 遍历单链表,找到要删除的节点的前一个节点preNode
Node preNode = this.head;
for (int i = 1; i < index - 1; i++) {
preNode = preNode.next;
}
// 找到前一个节点preNode后,判断删除节点是不是尾节点
if (preNode.next.next == null) {
// 是尾部节点则直接使preNode指针指向null即可
preNode.next = null;
} else {
// 不是尾部节点则使preNode指针指向删除节点的下一个节点
preNode.next = preNode.next.next;
}
// 单链表节点总数减1
this.size--;
}
// 给出删除反馈
System.out.println("成功删除第" + index + "个节点!");
// 执行删除节点操作后打印单链表
printLink();
}
/**
* @Description //TODO 根据需要删除的数据删除单链表的节点
* @Param [data] 需要删除的数据
* @return void
**/
public void deleteNodeByData(int data) {
// 删除一个未知节点需要两个信息
// 当前节点,用于对比数据data,可能成为要删除的节点,初始为头结点
Node curNode = this.head;
// 当前节点curNode的前一个节点,用于在确定要删除的节点后执行删除操作,初始为头结点
Node preNode = this.head;
// 记录下遍历单链表前的节点总数,留有后用
int allNodes = this.size;
// 当前节点不是null则一直循环,退出循环表示单链表遍历完毕
while (curNode != null) {
// 判断当前节点数据是否为data
if (curNode.data == data) {
// 判断当前节点是不是头结点
if (curNode == this.head) {
// 是头结点则使当前节点的下一个节点成为头结点即可完成删除
this.head = head.next;
// 使curNode和preNode回到初始状态
curNode = this.head;
preNode = this.head;
} else {
// 不是头结点则删除当前节点,使preNode指向curNode指向的节点即可
preNode.next = curNode.next;
// preNode不用改变,更新一下新的curNode即可
curNode = preNode.next;
}
// 走到这说明删除了一个节点,不管是不是头结点
// 单链表节点总数减1
this.size--;
// 给出删除反馈
System.out.println("成功删除一个数据为" + data + "的节点!");
} else {
// 当前节点不是要删除的节点
// 当前节点成为新的preNode
preNode = curNode;
// 当前节点后移一个
curNode = curNode.next;
}
}
// 遍历完毕,根据节点总数的变化给出删除操作的反馈
if (allNodes == this.size) {
// 节点总数无变化,说明没有找到数据为data的节点
System.out.println("单链表中不存在数据为" + data + "的节点,删除失败!");
} else {
System.out.printf("删除完毕,一共删除了%d个数据为%d的节点!%n", allNodes - this.size, data);
}
// 执行删除节点操作后打印单链表
printLink();
}
/**
* @Description //TODO 在单链表中查找数据为data的节点并给出节点信息
* @Param [data] 要找的节点的信息
* @return void
**/
public void searchNode(int data) {
// 使用curNode遍历单链表
Node curNode = this.head;
// 计数,用于记录找到的节点位置
int count = 0;
// 总数,找到的数据的data的节点总数
int sum = 0;
// 用于存储找到的节点信息
StringBuffer result = new StringBuffer();
// 当前节点不为null则一直循环查找
while (curNode != null) {
// 计数加1
count++;
// 判断当前节点信息是否为data
if (curNode.data == data) {
// 找到一个节点,总数加1
sum++;
// 迭代信息
result.append(",第");
result.append(count);
result.append("个节点");
}
// 当前节点核对完毕,指针后移
curNode = curNode.next;
}
// 查找完毕,给出反馈
if (sum == 0) {
System.out.println("没找到数据为" + data + "的节点!");
} else {
System.out.printf("一共找到%d个数据为%d的节点,位置为:%s%n", sum, data, result.substring(1));
}
}
}
package work.java.xzk10301002.link;
/**
*@ClassName: LinkTest
*@Description: 单链表功能演示的启动类
*
*/
public class LinkTest {
public static void main(String[] args) {
Link link = new Link();
link.initLink();
link.printLink();
link.addNode(6, 9);
link.addNode(6, 0);
link.addNode(8);
link.addNode(9, 3);
link.deleteNodeByIndex(2);
link.deleteNodeByIndex(0);
link.deleteNodeByIndex(1);
link.deleteNodeByData(1);
link.searchNode(5);
link.searchNode(4);
link.deleteNodeByData(4);
}
}
运行结果: