数据结构与算法(二):单链表
一、什么是链表#
链表是一种数据结构,跟数组不同,链表不需要连续的内存空间,而是通过指针将零散的内存块连接起来。
因此,链表的查找需要通过节点按顺序遍历,而增加与删除通过只需要操作指针指向,这也造成了相比数组,链表的查找性能消耗大而增加和删除消耗小的特点。
链表由节点组成,一般每个节点最少包含用于储存数据的data区和用于指向下一个节点的指针next,有的链表可能会有头结点或尾结点用于存储链表长度等信息。
二、链表的简单实现#
先来实现一个简单的节点类:
/**
* @Author:huang
* @Date:2020-06-20 10:19
* @Description:节点类
*/
public class Node {
//节点序号
int num;
//数据
Object data;
//下一个节点
Node next;
public Node(int num, Object data) {
this.num = num;
this.data = data;
}
public int getNum() {
return num;
}
public Object getData() {
return data;
}
public void setNum(int num) {
this.num = num;
}
public void setData(Object data) {
this.data = data;
}
@Override
public String toString() {
return "Node{" +
"num=" + num +
", data=" + data +
'}';
}
}
1.插入节点#
1.1不按照排序的插入#
/**
* @Author:huang
* @Date:2020-06-20 10:19
* @Description:单链表
*/
public class SingleLinkList {
//默认初始化一个头节点
private Node head = new Node(0, "我是头结点");
/**
* 添加节点到链表
* @param node
*/
public void add(Node node) {
Node temp = head;
//遍历链表
while (true) {
if (temp.next == null) {
break;
}
//不是尾节点就继续遍历下一个节点
temp = temp.next;
}
//将尾节点指向即将插入的新节点
temp.next = node;
}
}
1.2按照排序插入#
要想按顺序插入节点,需要确定节点的位置,也就是确定新节点的大小,以图为例:
遍历链表,我们把遍历到的节点叫A,A之前的节点叫A-1,A之后的节点叫A+1,要插入的节点叫B,有以下几种情况:
- 当A的序号大于B时,就把B插到A前,也就是原本的A和A-1之间
- 当A的序号等于B时,抛出异常禁止插入
- 当A的序号小于B时,就跳过这个节点,继续遍历,然后对比A+1和B的大小
- 当A就是链表最后一个节点时,A还是小于B,那就直接让B插到A后,变成链表的尾节点
按这个思路,我们在原来的类里新增一个方法:
/**
* 按顺序添加节点到链表
* @param node 要插入的节点
*/
public void addByOrder(Node node) {
Node temp = head;
//遍历链表
while (true) {
//如果链表到底了就直接插入
if (temp.next == null) {
temp.next = node;
return;
}else if (temp.next.num > node.num){
//如果当前节点比当要插入的节点大,就把新节点插到当前节点之前
node.next = temp.next;
temp.next = node;
return;
}else if (temp.next.num == node.num){
//如果有节点序号和要插入的节点重复就抛异常
throw new RuntimeException("插入节点与已有节点序号重复!");
}
//如果当前节点比当要插入的节点小,就继续遍历
temp = temp.next;
}
}
2.查找节点#
/**
* 展示链表全部节点
*/
public void show() {
//判断链表是否为空
if (head.next == null) {
throw new RuntimeException("链表为空!");
}
Node temp = head.next;
//遍历链表
while (true) {
if (temp == null) {
break;
}
System.out.println(temp.toString());
temp = temp.next;
}
}
/**
* 根据序号获取节点
* @param num 要获取的节点序号
* @return
*/
public Node get(int num){
//判断链表是否为空
if (head.next == null) {
throw new RuntimeException("链表为空!");
}
Node temp = head.next;
//遍历链表
while (true) {
if (temp == null) {
throw new RuntimeException("编号为" + num + "的节点不存在!");
}
//如果是要获取的节点
if (temp.num == num) {
return temp;
}
temp = temp.next;
}
}
3.修改节点#
修改节点与按顺序插入逻辑相似,需要先遍历并找到要修改的节点,然后用新节点去替换旧节点,或者干脆直接修改节点内的信息
/**
* 修改节点
* @param node 要更新的节点
*/
public void update(Node node) {
Node temp = head;
//判断链表是否为空
if (temp.next == null) {
throw new RuntimeException("链表为空!");
}
//获取要更新的节点序号
int nodeNum = node.num;
//遍历链表
while (true) {
//如果已经遍历完链表
if (temp == null) {
throw new RuntimeException("编号为" + temp.num + "的节点不存在!");
}
//如果找到了该节点
if (temp.num == nodeNum) {
temp.data = node.data;
return;
}
//继续遍历下一节点
temp = temp.next;
}
}
4.删除节点#
因为在单向链表中,节点无法知道自己前一个节点的情况,以图为例:
如果我们想要删除节点A,那么就要找到A的前一个节点A-1,根据是否存在A后的节点A+1有以下几种情况:
-
A节点就是链表尾节点,此时只需让A-1节点的next指向null即可
nodeA-1.next = null
-
A节点后有A+1,此时让A-1节点指向A+1节点
nodeA-1.next = nodeA-1.next.next
按这个思路,在类里新加一个方法:
/**
* 删除节点
* @param num 要删除的节点编号
*/
public void delete(int num) {
Node temp = head;
//判断链表是否为空
if (temp.next == null) {
throw new RuntimeException("链表为空!");
}
//遍历链表
while (true) {
//如果链表到底了
if (temp.next == null) {
throw new RuntimeException("编号为" + num + "的节点不存在!");
}
//如果找到了待删除节点的前一个节点
if (temp.next.num == num) {
//判断待删除节点是否为尾节点
if (temp.next.next == null){
temp.next = null;
}else {
temp.next = temp.next.next;
}
return;
}
//继续遍历下一节点
temp = temp.next;
}
}
三、完整实现代码#
/**
* @Author:huang
* @Date:2020-06-20 10:19
* @Description:单链表
*/
public class SingleLinkList {
private Node head = new Node(0, "我是头结点");
/**
* 添加节点到链表
* @param node 要插入的节点
*/
public void add(Node node) {
Node temp = head;
//遍历链表
while (true) {
if (temp.next == null) {
break;
}
//不是尾节点就继续遍历下一个节点
temp = temp.next;
}
//将尾节点指向即将插入的新节点
temp.next = node;
}
/**
* 展示链表
*/
public void show() {
//判断链表是否为空
if (head.next == null) {
throw new RuntimeException("链表为空!");
}
Node temp = head.next;
//遍历链表
while (true) {
if (temp == null) {
break;
}
System.out.println(temp.toString());
temp = temp.next;
}
}
/**
* 根据序号获取节点
* @param num 要获取的节点序号
* @return
*/
public Node get(int num){
//判断链表是否为空
if (head.next == null) {
throw new RuntimeException("链表为空!");
}
Node temp = head.next;
//遍历链表
while (true) {
if (temp == null) {
throw new RuntimeException("编号为" + num + "的节点不存在!");
}
if (temp.num == num) {
return temp;
}
temp = temp.next;
}
}
/**
* 按顺序添加节点到链表
* @param node 要插入的节点
*/
public void addByOrder(Node node) {
Node temp = head;
//遍历链表
while (true) {
//如果链表到底了就直接插入
if (temp.next == null) {
temp.next = node;
return;
}
else if (temp.next.num > node.num){
//如果后一节点比当新节点大,就插入当前节点
node.next = temp.next;
temp.next = node;
return;
}else if (temp.next.num == node.num){
//如果后一节点等于新节点,抛异常
throw new RuntimeException("插入节点与已有节点序号重复!");
}
//如果后一节点比当前节点小,就继续遍历
temp = temp.next;
}
}
/**
* 修改节点
* @param node 要更新的节点
*/
public void update(Node node) {
Node temp = head;
//判断链表是否为空
if (temp.next == null) {
throw new RuntimeException("链表为空!");
}
//获取要更新的节点序号
int nodeNum = node.num;
//遍历链表
while (true) {
//如果已经遍历完链表
if (temp == null) {
throw new RuntimeException("编号为" + temp.num + "的节点不存在!");
}
//如果找到了该节点
if (temp.num == nodeNum) {
temp.data = node.data;
return;
}
//继续遍历下一节点
temp = temp.next;
}
}
/**
* 删除节点
* @param num 要删除的节点编号
*/
public void delete(int num) {
Node temp = head;
//判断链表是否为空
if (temp.next == null) {
throw new RuntimeException("链表为空!");
}
//遍历链表
while (true) {
//如果链表到底了
if (temp.next == null) {
throw new RuntimeException("编号为" + num + "的节点不存在!");
}
//如果找到了待删除节点的前一个节点
if (temp.next.num == num) {
//判断待删除节点是否为尾节点
if (temp.next.next == null){
temp.next = null;
}else {
temp.next = temp.next.next;
}
return;
}
//继续遍历下一节点
temp = temp.next;
}
}
}
四、环形链表#
循环链表是一种特殊单链表,他跟单链表的区别在于尾节点的尾指针指向了链表的头结点,也就是相当于将链表连接成了一个环形。
环形链表可以用于处理能描述为环形的数据,典型的比如约瑟夫问题。
作者:Createsequence
出处:https://www.cnblogs.com/Createsequence/p/13168613.html
版权:本作品采用「署名-非商业性使用-相同方式共享 4.0 国际」许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 【译】Visual Studio 中新的强大生产力特性
· 【设计模式】告别冗长if-else语句:使用策略模式优化代码结构
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义