数据结构-链表

570 阅读8分钟

链表

一个节点连接下一个节点构成一个链表。逻辑上是一个线性结构,实际上是由多个不同地址的空间通过指针连接起来。插入和删除速度快、随机访问速度慢。链表的几种实现:单向链表、双向链表、环形链表。

实现接口

public class DoublyLinkedListSentinel implements Iterable<Integer>

目的:实现遍历,可以使用增强for循环遍历节点

节点结构

通过定义一个内部类Node实现

单向链表的Node类

private static class Node{
    int value;
    Node next;
    // 使用内部类Node,目的:对外隐藏实现细节(没必要让类的使用者关心Node结构),
    // 定义为 static 内部类,是因为 Node 不需要与 SinglyLinkedList 实例相关,
    // 多个 SinglyLinkedList实例能共用 Node 类定义,即
    public Node(int value,Node next){
        this.value = value;
        this.next = next;
    }
}

双向链表的Node类

static class Node {
    Node prev;
    int value;
    Node next;

    public Node(Node prev, int value, Node next) {
        this.prev = prev;
        this.value = value;
        this.next = next;
    }
}

单向链表

  • 节点.next指向下一个节点 image-20230803160241062.png

  • 添加元素 image-20230803160925067.png

  • 删除节点

    • 直接通过覆盖节点来实现
    • p1.next= p1.next.next

java实现

不带头结点

package linklist;

import java.util.Iterator;

/**
 * 单向链表
 * @author hly
 * @version 1.0
 */
public class SinglyLinkList implements Iterable<Integer>{
    private Node head;// 头结点
    // 静态内部类Node
    private static class Node{
        int value;
        Node next;
        // 使用内部类Node,目的:对外隐藏实现细节(没必要让类的使用者关心Node结构),
        // 定义为 static 内部类,是因为 Node 不需要与 SinglyLinkedList 实例相关,
        // 多个 SinglyLinkedList实例能共用 Node 类定义,即
        public Node(int value,Node next){
            this.value = value;
            this.next = next;
        }
    }

    /**
     * 头插法
     * @param value
     */
    public void addFirst(int value){
        this.head = new Node(value,this.head);
    }
    // while遍历
    public void loopByWhile(){
        Node curr = this.head;
        while (curr!=null){
            // 实现一些功能
            System.out.println(curr.value);
            curr = curr.next;
        }
    }
    // for遍历
    public void loopByFor(){
        for (Node curr = this.head;curr!=null;curr=curr.next){
            // 实现一些功能
            System.out.println(curr.value);
        }
    }
    // 迭代器遍历
    // NodeIterator 要定义为非 static 内部类,显式定义一个内部类为了方便查看
    // 因为SinglyLinkList遍历其实是Node内部类的遍历。
    // 可以在实现iterator()方法时,直接返回一个匿名类
    /*
        return new Iterator<Integer>() {
                Node curr = head;
                @Override
                public boolean hasNext() {
                    return curr!=null;
                }

                @Override
                public Integer next() {
                    int value= curr.value;
                    curr = curr.next;
                    return value;
                }
            }
      */
    // 是因为它与 SinglyLinkedList 实例相关,
    // 是对某个 SinglyLinkedList 实例的迭代
    private class NodeIterator implements Iterator<Integer>{
        Node curr= head;
        public boolean hasNext(){
            return curr!=null;
        }
        public Integer next(){
            int value = curr.value;
            curr = curr.next;
            return value;
        }
    }

    /**
     * 迭代器遍历,实现Iterable接口
     * @return 返回一个iterator实现类,重写hasNext和next方法。
     * hasNext判断迭代的条件
     * next->对迭代元素进行的操作
     */
    @Override
    public Iterator<Integer> iterator() {
        return new NodeIterator();
    }

    // 递归循环
    public void loopByRecursion(){
        recursion(this.head);
    }

    /**
     * 递归函数
     * @param curr
     */
    private void recursion(Node curr) {
        if (curr==null){
            // 遍历到最后一个Node
            return;
        }
        // 理解成中间件的调用执行顺序?
        /*
            先执行的,最后再执行?
            相当于栈的调用,先进后出的调用顺序
         */
        // 此次遍历做的事
        System.out.println("我是先执行的value值"+curr.value);
        recursion(curr.next);
        // 后面做的事
        System.out.println("我是后执行的value值"+curr.value);
    }

    /**
     * 查找最后一个Node节点
     * @return
     */
    private Node findLast(){
        if (this.head==null){
            return null;
        }
        Node curr =this.head;
        //找到最后一个Node.next为空的Node
        while (curr.next!=null){
            curr=curr.next;
        }
        return curr;
    }

    /**
     * 尾部添加
     * @param value 节点的value
     */
    public void addLast(int value){
        Node last = this.findLast();
        if (last==null){
            // 说明此时链表是null,相当于头插法
            this.addFirst(value);
            return;
        }
        // 将新结点插入
        last.next = new Node(value,null);
    }

    /**
     * 尾部添加多个
     * 步骤:先将rest拼成一个长串,再添加到末尾
     * @param first 第一个元素
     * @param rest 不定长参数
     */
    public void addLast(int first,int... rest){
        Node sublist= new Node(first,null);
        Node curr = sublist;
        for (int value:rest){
            curr.next = new Node(value,null);
            curr = curr.next;
        }
        Node last = this.findLast();
        if (last==null){
            // 直接赋值给头结点
            this.head = sublist;
            return;
        }
        last.next = sublist;
    }

    /**
     * 尾插法
     * @param values value数组
     */
    public void addLast(int[] values){
        Node sublist= new Node(values[0],null);
        Node curr = sublist;
        for (int i = 1; i < values.length; i++) {
            curr.next = new Node(values[i],null);
            curr =curr.next;
        }
        Node last = this.findLast();
        if (last==null){
            // 直接赋值给头结点
            this.head = sublist;
            return;
        }
        last.next = sublist;
    }

    /**
     * 根据索引获取
     * @param index
     * @return
     */
    private Node findNode(int index){
        int i = 0;
        Node curr= this.head;
        while (curr!=null){
            if (i==index){
                return curr;
            }
            curr = curr.next;
            i++;
        }
        return null;
    }

    /**
     * 封装异常类(index索引问题)
     * 因为这个SinglyLinkList类查找时,没有index越界问题,只有Node为Null的情况
     * @param index
     * @return
     */
    private IllegalArgumentException illegalIndex(int index){
        return new
                IllegalArgumentException(String.format("index [%d] 不合法%n",index));
    }

    /**
     * 根据索引值获取value
     * @param index
     * @return
     */
    public int get(int index){
        Node node =this.findNode(index);
        if (node!=null){
            return node.value;
        }
        throw this.illegalIndex(index);
    }

    /**
     * 根据Index插入数据
     * @param index
     * @param value
     */
    public void insert(int index,int value){
        if (index==0){
            //说明头插法
            this.addLast(value);
        }
        //查找应为上一个Node
        Node node = this.findNode(index-1);
        //判断
        if (node==null){
            throw illegalIndex(index);
        }
       // 赋值
        node.next = new Node(value,node.next);

    }

    /**
     * 删除
     * @param index
     * @return
     */
    public void remove(int index){
        if (index == 0){
            if (this.head!=null) {
                this.head = this.head.next;
                return;
            }else {
                throw illegalIndex(index);
            }
        }
        Node node = findNode(index-1);
        if (node==null||node.next==null){
            throw illegalIndex(index);
        }
        node.next = node.next.next;
    }
}

带头结点

  • 链表长度至少为1。
  • 优点
    • 添加元素时,不需要判断head是否为null
    • 很多操作就没有必要判断head是否为null,减少不必要的操作
package linklist;

import java.util.Iterator;

/**
 * 单向链表(带哨兵)
 * 因为Head节点一直在,所以就没有判断head为空的必要
 * 此时,很多操作就没有必要判断head是否为null,减少不必要的操作
 * @author hly
 * @version 1.0
 */
public class SinglyLinkListSentinel implements Iterable<Integer>{
    private Node head = new Node(Integer.MIN_VALUE,null);// 头结点
    // 静态内部类Node
    private static class Node{
        int value;
        Node next;
        // 使用内部类Node,目的:对外隐藏实现细节(没必要让类的使用者关心Node结构),
        // 定义为 static 内部类,是因为 Node 不需要与 SinglyLinkedList 实例相关,
        // 多个 SinglyLinkedList实例能共用 Node 类定义,即
        public Node(int value,Node next){
            this.value = value;
            this.next = next;
        }
    }

    /**
     * 头插法
     * @param value
     */
    public void addFirst(int value){
        this.head.next = new Node(value,this.head.next);
    }
    // while遍历
    public void loopByWhile(){
        Node curr = this.head.next;
        while (curr!=null){
            // 实现一些功能
            System.out.println(curr.value);
            curr = curr.next;
        }
    }
    // for遍历
    public void loopByFor(){
        for (Node curr = this.head.next;curr!=null;curr=curr.next){
            // 实现一些功能
            System.out.println(curr.value);
        }
    }
    // 迭代器遍历
    // NodeIterator 要定义为非 static 内部类,显式定义一个内部类为了方便查看
    // 因为SinglyLinkList遍历其实是Node内部类的遍历。
    // 可以在实现iterator()方法时,直接返回一个匿名类
    /*
        return new Iterator<Integer>() {
                Node curr = head;
                @Override
                public boolean hasNext() {
                    return curr!=null;
                }

                @Override
                public Integer next() {
                    int value= curr.value;
                    curr = curr.next;
                    return value;
                }
            }
      */
    // 是因为它与 SinglyLinkedList 实例相关,
    // 是对某个 SinglyLinkedList 实例的迭代
    private class NodeIterator implements Iterator<Integer>{
        Node curr= head.next;
        public boolean hasNext(){
            return curr!=null;
        }
        public Integer next(){
            int value = curr.value;
            curr = curr.next;
            return value;
        }
    }

    /**
     * 迭代器遍历,实现Iterable接口
     * @return 返回一个iterator实现类,重写hasNext和next方法。
     * hasNext判断迭代的条件
     * next->对迭代元素进行的操作
     */
    @Override
    public Iterator<Integer> iterator() {
        return new NodeIterator();
    }

    // 递归循环
    public void loopByRecursion(){
        recursion(this.head.next);
    }

    /**
     * 递归函数
     * @param curr
     */
    private void recursion(Node curr) {
        if (curr==null){
            // 遍历到最后一个Node
            return;
        }
        // 理解成中间件的调用执行顺序?
        /*
            先执行的,最后再执行?
            相当于栈的调用,先进后出的调用顺序
         */
        // 此次遍历做的事
        System.out.println("我是先执行的value值"+curr.value);
        recursion(curr.next);
        // 后面做的事
        System.out.println("我是后执行的value值"+curr.value);
    }

    /**
     * 查找最后一个Node节点
     * @return
     */
    private Node findLast(){
        Node curr =this.head;
        //找到最后一个Node.next为空的Node
        while (curr.next!=null){
            curr=curr.next;
        }
        return curr;
    }

    /**
     * 尾部添加
     * @param value 节点的value
     */
    public void addLast(int value){
        Node last = this.findLast();
        // 将新结点插入
        last.next = new Node(value,null);
    }

    /**
     * 尾部添加多个
     * 步骤:先将rest拼成一个长串,再添加到末尾
     * @param first 第一个元素
     * @param rest 不定长参数
     */
    public void addLast(int first,int... rest){
        Node sublist= new Node(first,null);
        Node curr = sublist;
        for (int value:rest){
            curr.next = new Node(value,null);
            curr = curr.next;
        }
        Node last = this.findLast();
        last.next = sublist;
    }

    /**
     * 尾插法
     * @param values value数组
     */
    public void addLast(int[] values){
        Node sublist= new Node(values[0],null);
        Node curr = sublist;
        for (int i = 1; i < values.length; i++) {
            curr.next = new Node(values[i],null);
            curr =curr.next;
        }
        Node last = this.findLast();
        last.next = sublist;
    }

    /**
     * 根据索引获取
     * @param index
     * @return
     */
    private Node findNode(int index){
        // 代表的是head节点,当插入的index为0时,
        // 即FindNode()接收到是参数是-1,即为head节点
        int i = -1;
        Node curr= this.head;
        while (curr!=null){
            if (i==index){
                return curr;
            }
            curr = curr.next;
            i++;
        }
        return null;
    }

    /**
     * 封装异常类(index索引问题)
     * 因为这个SinglyLinkList类查找时,没有index越界问题,只有Node为Null的情况
     * @param index
     * @return
     */
    private IllegalArgumentException illegalIndex(int index){
        return new
                IllegalArgumentException(String.format("index [%d] 不合法%n",index));
    }

    /**
     * 根据索引值获取value
     * @param index
     * @return
     */
    public int get(int index){
        Node node =this.findNode(index);
        if (node!=null){
            return node.value;
        }
        throw this.illegalIndex(index);
    }

    /**
     * 根据Index插入数据
     * index的取值为[-1,.] -1代表head节点
     * @param index
     * @param value
     */
    public void insert(int index,int value){
        //查找应为上一个Node
        // 传入0时,返回的是哨兵
        Node node = this.findNode(index-1);
        //判断
        if (node==null){
            throw illegalIndex(index);
        }
       // 赋值
        node.next = new Node(value,node.next);

    }

    /**
     * 删除
     * @param index
     * @return
     */
    public void remove(int index){
        Node node = findNode(index-1);
        if (node==null||node.next==null){
            throw illegalIndex(index);
        }
        node.next = node.next.next;
    }
}

双向链表

  1. head、tail两个节点
  2. 链表为空时
    1. head.next= tail;tail.prev=head;

image-20230803162016995.png

  1. 优点
    1. 可以双向遍历:由于每个节点都有指向前一个节点的指针,因此可以从任意一个节点开始,向前或向后遍历整个链表。这使得双向链表在某些场景下更加方便,比如需要逆向遍历链表或者需要双向查找某个节点。
    2. 删除和插入操作更高效:在单向链表中,如果要删除或插入某个节点,需要知道其前驱节点的位置,而双向链表通过指针的前后关系,不需要再查找前驱节点。删除和插入操作可以在常数时间内完成。
    3. 灵活性更强:双向链表相比单向链表可以更灵活地操作节点。例如,可以通过前后指针直接交换两个相邻节点的位置,而无需修改其他节点的指针。
    4. 更容易实现某些功能:由于双向链表具有双向遍历的特性,因此在实现一些功能时更加便捷。例如,实现LRU(最近最少使用)缓存算法时,可以使用双向链表来快速删除最久未使用的节点。
  • 添加与删除跟单向链表类似,多一步骤:前驱指针的指向。

java实现

package linklist;

import java.util.Iterator;

/**双向链表(带哨兵)
 * @author hly
 * @version 1.0
 */
public class DoublyLinkedListSentinel implements Iterable<Integer>{
    // 头结点
    private final Node head;
    // 尾节点
    private final Node tail;



    //Node内部类
    private static class Node{
        //上一个节点
         Node prev;
         int value;
        // 下一个节点
         Node next;

        public Node(Node prev, int value, Node next) {
            this.prev = prev;
            this.value = value;
            this.next = next;
        }
    }

    //初始化
    public DoublyLinkedListSentinel(){
        head = new Node(null,666,null);
        tail = new Node(null,888,null);
        head.next=tail;
        tail.prev=head;
    }

    /**
     * 根据Index获取节点
     * @param index
     * @return
     */
    private Node findNode(int index){
        int i = -1;
        for (Node p = this.head; p!=tail; p= p.next,i++) {
            if (i==index){
                return p;
            }
        }
        return null;
    }

    /**
     * 插入第一个位置
     * @param value
     */
    public void addFirst(int value){
        this.insert(0,value);
    }

    /**
     * 末尾插入元素
     * @param value
     */
    public void addLast(int value){
        Node prev = tail.prev;
        Node added = new Node(prev,value,tail);
        prev.next=added;
        tail.prev=added;
    }
    /**
     * 删除第一个元素
     */
    public void removeFirst(){
        this.remove(0);
    }
    public void removeLast(){
        Node removed = tail.prev;
        if (removed==head){
            //null
            throw illegalIndex(0);
        }
        Node prev = removed.prev;
        prev.next=tail;
        tail.prev=prev;
    }


    /**
     * 根据Index删除Node
     * @param index
     */
    public void remove(int index) {
        Node prev = this.findNode(index - 1);
        if (prev==null){
            throw illegalIndex(index);
        }
        Node removed= prev.next;
        if (removed==tail){
            // 此时双向链表没有任何一个节点(除头尾指针外)
            throw illegalIndex(index);
        }
        /**
         * 简易版
         * Node next = removed.next;
         *         prev.next = next;
         *         next.prev = prev;
         */
        prev.next=removed.next;
        removed.next.prev=prev;
    }

    /**
     * 根据Index添加Node
     * @param index
     * @param value
     */
    public void insert(int index, int value) {
        Node prev = this.findNode(index - 1);
        if (prev==null){
            throw illegalIndex(index);
        }
        Node next = prev.next;
        Node inserted = new Node(prev,value,next);
        prev.next =inserted;
        // 将插入前的那个位置Nodeprev指针指向上一个
        next.prev=inserted;
    }

    private IllegalArgumentException illegalIndex(int index) {

        return new IllegalArgumentException(String.format("index[%d]不合法%n",index));

    }
    @Override
    public Iterator<Integer> iterator() {
        return new Iterator<Integer>() {
            Node p =head.next;
            @Override
            public boolean hasNext() {
                return p.next!=tail;
            }
            @Override
            public Integer next() {
                int value = p.value;
                p = p.next;
                return value;
            }
        };
    }
}

环形链表

  1. 链表前驱和后驱指针互相指向形成闭环,头->尾,尾->头。
  2. 可以循环遍历,这种特性在某些场景下非常有用,例如模拟循环队列、轮询等
  3. 快速判断环:环形链表具有环形结构,使用快慢指针法可以快速判断链表是否存在环。快慢指针以不同的速度遍历链表,如果存在环,两个指针最终会相遇;如果不存在环,则快指针会先到达链表末尾。
  4. 节省存储空间:在某些情况下,环形链表可以节省存储空间。例如,在循环队列中,使用环形链表可以避免对队列大小进行动态调整和数据迁移。
  5. 实现特定算法和数据结构:环形链表在实现某些特定算法和数据结构时非常方便。例如,约瑟夫问题(Josephus problem)可以使用环形链表解决,也可以使用环形链表实现循环缓冲区等。

java实现

package linklist;

import java.util.Iterator;

/**
 * 环形链表
 * @author hly
 * @version 1.0
 */
public class CircularLinkedList implements Iterable<Integer>{
    @Override
    public Iterator<Integer> iterator() {
        return new Iterator<Integer>() {
            Node pre = sentinel.next;
            @Override
            public boolean hasNext() {
                return pre!=sentinel;
            }

            @Override
            public Integer next() {
                int value = pre.value;
                pre=pre.next;
                return value;
            }
        };
    }

    static class Node {
        Node prev;
        int value;
        Node next;

        public Node(Node prev, int value, Node next) {
            this.prev = prev;
            this.value = value;
            this.next = next;
        }
    }
    // 哨兵
    private final Node sentinel =new Node(null,-1,null);
    public CircularLinkedList() {
        sentinel.next = sentinel;
        sentinel.prev = sentinel;
    }

    /**
     * 添加到第一个元素
     * @param value 待添加值
     */
    public void addFirst(int value){
        Node next  =sentinel.next;
        Node prev = sentinel;
        Node added = new Node(prev,value,next);
        prev.next= added;
        next.prev=added;
    }

    /**
     * 添加末尾
     * @param value
     */
    public void addLast(int value){
        Node prev = sentinel.prev;
        Node next = sentinel;
        Node added = new Node(prev,value,next);
        next.prev = added;
        prev.next = added;
    }

    /**
     * 删除第一个元素
     */
    public void removeFirst(){
        Node removed = sentinel.next;
        if (removed==sentinel){
            throw illegalIndex(0);
        }
//        Node a = sentinel;
        Node node = removed.next;
//        a.next =node;
//        node.prev=a;
        node.prev = removed.prev;
        removed.prev.next =node;

    }

    /**
     * 删除末尾元素
     */
    public void removeLast(){
        Node removed = sentinel.prev;
        if (removed == sentinel){
            throw new IllegalArgumentException("非法");
        }
        Node node = removed.next;
        removed.prev.next = node;
        node.prev = removed.prev;
    }

    /**
     * 根据值删除节点
     * 假定 value 在链表中作为 key, 有唯一性
     * @param value
     */
    public void removeByValue(int value){
        Node removed = this.findNodeByValue(value);
        if (removed ==null){
            throw new IllegalArgumentException("value值不存在!");
        }
        Node prev = removed.prev;
        prev.next = removed.next;
        removed.next.prev = prev;
    }

    private Node findNodeByValue(int value) {
        Node node = this.sentinel.next;
        while (node!=sentinel){
            if (node.value==value){
                return node;
            }
            node = node.next;
        }
        return null;
    }

    public IllegalArgumentException illegalIndex(int index){
        return new IllegalArgumentException(String.format("inex [%d] 不合法%n",index));
    }
}