链表超详细讲解(单向链表实现)

上一篇我们认识了链表的基础概念和哨兵节点的优势。本文将聚焦单向链表,先实现普通版本,再通过哨兵节点优化,对比两者的实现差异,理解哨兵如何简化代码。

一、单向链表的实现(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无需判断空链表(哨兵始终存在)
代码复杂度高(特殊逻辑多)低(逻辑统一)

单向链表通过哨兵节点大幅简化了代码。下一篇我们将进阶到双向链表,看看如何利用哨兵节点实现双向遍历和更灵活的操作。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值