单向链表实现(普通与带哨兵)
上一篇我们认识了链表的基础概念和哨兵节点的优势。本文将聚焦单向链表,先实现普通版本,再通过哨兵节点优化,对比两者的实现差异,理解哨兵如何简化代码。
一、单向链表的实现(Java)
单向链表的节点仅含value(数据)和next(后继指针),核心操作包括初始化、插入、删除、查找和遍历。
1、初始化链表
- 初始化链表结构,创建一个空链表。
- 链表由一系列节点组成,每个节点包含一个整数值和一个指向下一个节点的引用。
- 初始状态下,链表的头节点为null,表示链表为空。
public class LinkedList implements Iterable<Integer> {
//创建一个初始节点
private Node head;
//建立内部类,更少的让用户看到内部的运行
private static class Node{
private int value;
private Node next=null;
public Node(int value, Node next) {
this.value = value;
this.next = next;
}
public Node() {
}
}
}
2、插入节点
(1)插入节点—在头部插入新节点
- 在链表头部插入一个新节点。
- 新节点的值为传入的参数value,插入后该节点成为链表的新头节点。
- 插入操作的时间复杂度为O(1),因为只需要修改头节点的引用
//在头部插入新节点
public void addFirst(int value)
{
//头部没有节点的情况下插入节点的下一个节点为空(头节点也为空),头部有节点的情况下,插入节点的下一节点为头节点
//所以无论头节点有没有值,添加头节点时,指向的下一节点都为头节点
head=new Node(value,head);
}
(2)插入节点—在尾部插入新节点
- 寻找链表中的最后一个节点。
- 从链表头节点开始遍历,直到找到一个节点的next引用为null,该节点即为最后一个节点。
- 如果链表为空(头节点为null),则返回null。
//寻找最后一个节点,将方法抽取出来以便复用
private Node findLast()
{
if (head==null)
{
return null;
}
Node lastPointer=head;
//遍历到next为空时,就找到了最后一个节点
while(lastPointer.next!=null)
{
lastPointer=lastPointer.next;
}
return lastPointer;
}
- 在链表尾部插入一个新节点。
- 新节点的值为传入的参数value。
- 如果链表为空,则调用addFirst方法插入新节点;否则找到最后一个节点并修改其next引用指向新节点。
- 插入操作的时间复杂度为O(n),因为需要遍历链表找到最后一个节点。
//在尾部插入新节点
public void addLast(int value)
{
Node lastPointer=findLast();
//lastPointer==null时表示没有节点存在,头节点也是尾节点,执行addFirst即可
if (lastPointer==null)
{
addFirst(value);
return;
}
lastPointer.next=new Node(value,null);
}
(3)插入节点—在指定位置插入新节点
- 查找链表中指定索引位置的节点。
- 索引从0开始,从头节点开始遍历链表,移动index次后到达目标节点。
- 如果在遍历过程中链表结束(指针为null),则返回null,表示索引超出范围。
//抽取的寻找指定Node的方法,以便复用
private Node findNode(int index)
{
Node pointer=head;
while(index>0)
{
pointer=pointer.next;
//pointer为空时抛出错误
if (pointer==null){
return null;
}
index--;
}
return pointer;
}
- 在链表的指定索引位置插入一个新节点。
- 新节点的值为传入的参数value。
- 如果索引为0,则调用addFirst方法在头部插入;否则找到索引位置的前一个节点,修改其next引用指向新节点。
- 插入操作的平均时间复杂度为O(n),因为可能需要遍历链表找到插入位置。
//将节点插入指定位置
public void insert(int index,int value)
{
if (index==0)
{
addFirst(value);
return;
}
Node insertPointer=findNode(index-1);
if (insertPointer==null){
throw new IllegalArgumentException(String.format("index值不合法"));
}
insertPointer.next=new Node(value,insertPointer.next);
}
3、删除节点
(1)删除节点—删除头部节点
- 删除链表的第一个节点。
- 如果链表为空(头节点为null),则抛出异常提示用户没有节点可删除。
- 删除操作的时间复杂度为O(1),因为只需要修改头节点的引用。
//删除第一个节点
public void removeFirst()
{
//当head==null时抛出错误,提示用户节点不存在
if (head==null)
{
throw new IllegalArgumentException(String.format("没有节点"));
}
// 保存原头节点引用
Node oldHead = head;
// head指向下一个节点
head = head.next;
// 断开原头节点的next引用
oldHead.next = null;
}
(2)删除节点—删除指定节点
- 删除链表中指定索引位置的节点。
- 如果索引为0,则调用removeFirst方法删除头节点;否则找到索引位置的前一个节点,修改其next引用跳过要删除的节点。
- 如果索引超出范围或链表为空,则抛出异常提示节点不存在。
- 删除操作的平均时间复杂度为O(n),因为可能需要遍历链表找到删除位置。
//删除指定节点
public void remove(int index)
{
//判断index是否为0,如果为0则beforePointer一定为null
if (index==0){
removeFirst();
return;
}
Node beforePointer=findNode(index-1);
//判断beforePointer是否为null,如果为null则被删除的节点的前继不存在,那么被删除节点也不存在
if (beforePointer==null){
throw new IllegalArgumentException(String.format("节点不存在"));
}
Node removePointer=beforePointer.next;
//判断removePointer是否为null,如果为null则被删除的节点不存在
if (removePointer==null){
throw new IllegalArgumentException(String.format("节点不存在"));
}
//将删除节点后节点作为删除节点前节点的后续节点,实现删除节点
beforePointer.next=removePointer.next;
}
4、查找链表中的指定节点
- 获取链表中指定索引位置节点的值。
- 使用findNode方法查找指定索引的节点,如果节点存在则返回其值,否则抛出异常提示索引不合法。
- 查找操作的平均时间复杂度为O(n),因为可能需要遍历链表找到目标节点。
//获取链表中的某个值
public int get(int index)
{
//使用抽取出来的findNode方法
Node pointer=findNode(index);
//pointer==null时则用户找的index不存在,抛出错误提示用户
if (pointer==null){
throw new IllegalArgumentException(String.format("index值不合法"));
}
//如果存在返回该节点的值
return pointer.value;
}
5、遍历链表
(1)链表的遍历—while循环
- 使用while循环遍历链表中的所有节点。
- 从链表头节点开始,依次访问每个节点,并将节点的值传递给Consumer函数进行处理,直到遍历完所有节点。
//遍历链表:用while遍历
public void foreachListWhile(Consumer<Integer> consumer)
{
Node pointer=head;
//遍历至node的next为空即为空节点
while(pointer!=null)
{
consumer.accept(pointer.value);
pointer=pointer.next;
}
}
(2)链表的遍历—for循环
- 使用for循环遍历链表中的所有节点。
- 从链表头节点开始,依次访问每个节点,并将节点的值传递给Consumer函数进行处理,直到遍历完所有节点。
//遍历链表:用for遍历
public void foreachListFor(Consumer<Integer> consumer)
{
Node pointer=head;
//遍历至node的next为空即为空节点
for (Node node=head;node!=null;node=node.next)
{
consumer.accept(pointer.value);
pointer=pointer.next;
}
}
(3)链表的遍历—迭代器
- 通过实现Iterable接口,提供链表的迭代器实现。
- 迭代器从链表头节点开始,依次返回每个节点的值,直到链表结束。
- 支持使用增强for循环遍历链表
//遍历链表:用迭代器遍历
@Override
public Iterator<Integer> iterator() {
return new Iterator<Integer>() {
Node pointer=head;
@Override
public boolean hasNext() {
return pointer!=null;
}
@Override
public Integer next() {
int value=pointer.value;
pointer=pointer.next;
return value;
}
};
}
6、单向链表的使用示例
以下是单向链表的完整使用示例
public class TextLinkedList {
public static void main(String[] args) {
LinkedList linkedList=new LinkedList();
//链表初始化
linkedList.addFirst(1);
linkedList.addFirst(2);
linkedList.addFirst(3);
linkedList.addFirst(4);
System.out.println("----while循环遍历----");
linkedList.foreachListWhile((value)->{
System.out.print(value+" ");
});
System.out.println();
//在最后添加节点
linkedList.addLast(5);
System.out.println("----for循环遍历----");
linkedList.foreachListFor((value)->{
System.out.print(value+" ");
});
System.out.println();
//在中间添加节点
linkedList.insert(4,0);
System.out.println("----迭代器遍历-----");
for (int value:linkedList) {
System.out.print(value+" ");
}
System.out.println();
//删除第一个节点
linkedList.removeFirst();
System.out.println("----get-----");
System.out.print(linkedList.get(0));
System.out.println();
System.out.println("----remove-----");
linkedList.remove(4);
for (int value:linkedList) {
System.out.print(value+" ");
}
}
}
运行结果如下图所示:
二、带哨兵的单向链表的实现
带哨兵的双向循环链表特别适合需要循环访问数据的场景(如约瑟夫问题、环形缓冲区),其闭环特性和统一的边界处理使代码简洁高效,同时保留了双向链表的双向操作优势。
1、初始化带哨兵节点的单向链表
带哨兵的单向链表在初始化时便创建一个哨兵节点(不存储实际数据),作为链表的固定头部。后续所有节点操作都围绕哨兵节点展开,无需单独处理空链表场景。
public class SentinelLinkedList implements Iterable<Integer> {
// 创建哨兵节点,作为链表的固定头部(不存储实际数据)
private Node head = new Node(666, null);
// 节点内部类,包含数据域和指向下一节点的指针
private static class Node {
private int value;
private Node next = null;
public Node(int value, Node next) {
this.value = value;
this.next = next;
}
public Node() {
}
}
}
2、查找节点
(1)查找链表中最后一个节点
从哨兵节点开始遍历,直到找到next指针为null的节点,即为最后一个节点。由于哨兵节点存在,遍历起始条件始终有效。
private Node findLast()
{
Node lastPointer=head;
while(lastPointer.next!=null)
{
lastPointer=lastPointer.next;
}
return lastPointer;
}
(2)查找指定索引位置的节点
从哨兵节点开始遍历,通过索引计数定位目标节点。哨兵节点索引视为-1,有效节点从索引0开始,统一了节点查找的逻辑。
//抽取的寻找指定Node的方法,以便复用
private Node findNode(int index)
{
int i=-1;
for (Node pointer=head;pointer!=null;pointer=pointer.next,i++) {
if (i == index)
{
return pointer;
}
}
// 索引超出范围时返回null
return null;
}
(3)获取指定索引位置节点的值
通过findNode方法找到目标节点,返回其数据值。若节点不存在则抛出异常。
//获取链表中的某个值
public int get(int index)
{
Node pointer=findNode(index);
if (pointer==null){
throw new IllegalArgumentException(String.format("index值不合法"));
}
return pointer.value;
}
3、插入节点
(1)在尾部插入新节点
通过遍历找到链表的最后一个节点(哨兵节点保证链表永远非空),将新节点插入到最后一个节点之后。无需判断链表是否为空,简化了逻辑。
//在尾部插入新节点
public void addLast(int value)
{
Node lastPointer=findLast();
lastPointer.next=new Node(value,null);
}
(2)在指定位置插入新节点
通过查找目标位置的前一个节点(利用findNode方法),将新节点插入到其后方。由于哨兵节点的存在,头部插入与中间插入逻辑完全一致,无需特殊处理。
//将节点插入指定位置
public void insert(int index,int value)
{
Node insertPointer=findNode(index-1);
if (insertPointer==null){
throw new IllegalArgumentException(String.format("index值不合法"));
}
insertPointer.next=new Node(value,insertPointer.next);
}
(3)在头部插入新节点
直接复用insert方法,在索引0位置插入节点,等价于在哨兵节点后方插入新节点,成为链表的第一个有效节点。
//在头部插入新节点
public void addFirst(int value) {
insert(0, value); // 头部插入即索引0位置插入,复用insert方法
}
4、删除节点
(1)删除指定索引位置的节点
找到目标节点的前一个节点,通过修改其next指针跳过目标节点实现删除。哨兵节点保证了头部删除与中间删除逻辑一致,无需额外判断头节点是否为空。
// 删除指定索引位置的节点
public void remove(int index) {
Node beforePointer = findNode(index - 1); // 找到目标节点的前一个节点
// 若前节点不存在或前节点的next为空(目标节点不存在),则抛出异常
if (beforePointer == null || beforePointer.next == null) {
throw new IllegalArgumentException("节点不存在");
}
// 前节点的next指向目标节点的next,跳过目标节点实现删除
beforePointer.next = beforePointer.next.next;
}
(2)删除头部节点
直接复用remove方法,删除索引0位置的节点,即哨兵节点后方的第一个有效节点。
//删除第一个节点
public void removeFirst()
{
remove(0);
}
5、遍历链表
注:从哨兵节点的下一个节点(第一个有效节点)开始
(1)遍历链表—while 循环
通过while循环依次访问所有节点,将节点值传递给Consumer函数处理。
//遍历链表:用while遍历
public void foreachListWhile(Consumer<Integer> consumer)
{
Node pointer=head.next;
//遍历至node的next为空即为空节点
while(pointer!=null)
{
consumer.accept(pointer.value);
pointer=pointer.next;
}
}
(2)遍历链表—for循环
从哨兵节点的下一个节点开始,通过for循环依次访问所有节点,逻辑与while循环一致,仅语法形式不同。
//遍历链表:用for遍历
public void foreachListFor(Consumer<Integer> consumer)
{
Node pointer=head.next;
//遍历至node的next为空即为空节点
for (Node node=head.next;node!=null;node=node.next)
{
consumer.accept(pointer.value);
pointer=pointer.next;
}
}
(3)遍历链表—迭代器遍历
实现Iterable接口的iterator方法,返回自定义迭代器。迭代器从第一个有效节点开始,支持通过增强for循环遍历链表。
//遍历链表:用迭代器遍历
@Override
public Iterator<Integer> iterator() {
return new Iterator<Integer>() {
Node pointer=head.next;
@Override
public boolean hasNext() {
return pointer!=null;
}
@Override
public Integer next() {
int value=pointer.value;
pointer=pointer.next;
return value;
}
};
}
6、带哨兵的单向链表使用示例
以下是带哨兵节点的单向链表的完整使用示例,涵盖节点插入、删除、查找和遍历等操作:
public class TestSentinelLinkedList {
public static void main(String[] args) {
SentinelLinkedList linkedList = new SentinelLinkedList();
// 在头部插入节点
linkedList.addFirst(1);
linkedList.addFirst(2);
linkedList.addFirst(3);
linkedList.addFirst(4);
System.out.println("----while循环遍历(头部插入后)----");
linkedList.foreachListWhile(value -> System.out.print(value + " "));
System.out.println(); // 输出:4 3 2 1
// 在尾部插入节点
linkedList.addLast(5);
System.out.println("----for循环遍历(尾部插入后)----");
linkedList.foreachListFor(value -> System.out.print(value + " "));
System.out.println(); // 输出:4 3 2 1 5
// 在指定位置插入节点
linkedList.insert(4, 0); // 在索引4位置插入0(原尾部5之前)
System.out.println("----迭代器遍历(中间插入后)----");
for (int value : linkedList) {
System.out.print(value + " ");
}
System.out.println(); // 输出:4 3 2 1 0 5
// 删除头部节点
linkedList.removeFirst();
System.out.println("----get获取索引0的值(删除头部后)----");
System.out.println(linkedList.get(0)); // 输出:3
// 删除指定位置节点
linkedList.remove(4); // 删除索引4位置的节点(值为5)
System.out.println("----增强for循环遍历(删除指定节点后)----");
for (int value : linkedList) {
System.out.print(value + " ");
} // 输出:3 2 1 0
}
}
运行结果如下:
三、两种实现对比
操作 | 普通链表 | 带哨兵链表 |
---|---|---|
头部插入 | 需单独判断空链表 | 统一调用 insert (0) |
尾部插入 | 空链表需调用头部插入 | 直接找尾节点(哨兵可作为起点) |
边界判断 | 频繁判断head == null | 无需判断空链表(哨兵始终存在) |
代码复杂度 | 高(特殊逻辑多) | 低(逻辑统一) |
单向链表通过哨兵节点大幅简化了代码。下一篇我们将进阶到双向链表,看看如何利用哨兵节点实现双向遍历和更灵活的操作。