手写单链表

Posted LRcoding

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了手写单链表相关的知识,希望对你有一定的参考价值。

2. 单链表

2.1 单链表技能点

  • 存储空间不连续
  • 每个结点对应一个数据元素,结点由指针域和数据域组成
  • 元素的逻辑关系通过结点间的链接关系体现,逻辑上相邻,物理上不一定相邻
  • 按索引查询慢,存储空间大
  • 插入、删除速度快
  • 有元素才会分配空间,不会有闲置的结点

注:为了对空表非空表的情况以及首元结点进行统一处理,常增加一个头结点

2.2 手写单链表

测试代码

public static void main(String[] args) {
    //创建线性顺序表
    List list = new SingleLinkedList();
    
    //向末尾添加元素
    list.add("11111");
    list.add("aaaaa");
    list.add("bbbbb");
    list.add("33333");
    list.add("22222");
    
    // 添加到指定位置
    list.add(3, "AAAAA");
    
    //进行各种操作验证添加
    System.out.println(list.size());
    System.out.println(list.isEmpty());
    System.out.println(list.get(2));
    System.out.println(list.contains("44444"));
    System.out.println(list.indexOf("22222"));
    System.out.println(list.toString());
}

add 操作及 get 操作的底层思想

List类

public interface List {
    // 返回线性表的大小,即数据元素的个数。
    int size();
    // 返回线性表中序号为 i 的数据元素
    Object get(int i);
    // 如果线性表为空返回 true,否则返回 false。
    boolean isEmpty();
    // 判断线性表是否包含数据元素 e
    boolean contains(Object e);
    // 返回数据元素 e 在线性表中的序号
    int indexOf(Object e);
    // 将数据元素 e 插入到线性表中 i 号位置
    void add(int i, Object e);
    // 将数据元素 e 插入到线性表末尾
    void add(Object e);
    // 将数据元素 e 插入到元素 obj 之前
    boolean addBefore(Object obj, Object e);
    // 将数据元素 e 插入到元素 obj 之后
    boolean addAfter(Object obj, Object e);
    // 删除线性表中序号为 i 的元素,并返回之
    Object remove(int i);
    // 删除线性表中第一个与 e 相同的元素
    boolean remove(Object e);
    // 替换线性表中序号为 i 的数据元素为 e,返回原数据元素
    Object replace(int i, Object e);
}

Node类

public class Node {
    Object data;
    Node next; // 指向的元素也为结点

    public Node() {
    }

    public Node(Object data, Node next) {
        this.data = data;
        this.next = next;
    }

    public Node(Object data) {
        this.data = data;
    }
    // getter / setter / toString方法
}

单链表 SingleLinkedList 类

public class SingleLinkedList implements List {
    private Node head = new Node(); // 事先存在一个头结点
    private int size; // 结点数量,默认是0(不包含头结点)

    @Override
    public int size() {
        return size;
    }

    @Override
    public Node get(int index) {
        if (index < 0 || index >= size) {
            throw new IndexOutOfBoundsException("索引越界:" + index);
        }
        // 定义一个 p :指向头结点
        Node p = head;

        // 循环去找第 index 个结点
        for (int i = 0; i <= index; i++) {
            p = p.next; // !!!指向下一个结点
        }

        return p;
    }

    @Override
    public boolean isEmpty() {
        return size == 0;
    }

    @Override
    public void add(int index, Object e) {
        if (index < 0 || index > size) {
            throw new IndexOutOfBoundsException("索引越界" + index);
        }
        // p 首先指向头结点
        Node p = head;

        if (index > 0) {
            // 让指针移动到第 index 个结点
            p = get(index - 1);
        }
        // 新建一个结点,并加入数据
        Node node = new Node(e);
        // 指定该新结点指向下一个结点的地址,也就是p指向的下一个结点的地址
        node.next = p.next;
        // 修改p指向的下一个结点的地址,为该新结点
        p.next = node;
        // 结点数量++
        size++;
    }

    @Override
    public void add(Object e) {
        add(size, e);
    }

    @Override
    public Object remove(int index) {
        if (index < 0 || index > size) {
            throw new IndexOutOfBoundsException("索引越界" + index);
        }
        Node p = head;
        if (index > 0) {
            p = get(index - 1);
        }
        // 将指向第 index 个结点先存起来
        Node currentNode = p.next;

        // 第 index 的结点指向下一个结点的地址,直接改为第 index + 1 个结点指向下一个结点的地址
        p.next = p.next.next;

        // 存起来的结点指向下一个结点的地址,直接指向 null
        currentNode.next = null;

        size--;
        return null;
    }

    @Override
    public String toString() {
        StringBuilder builder = new StringBuilder("[");
        /** 直接调用get,相当于频繁的去查找 */
        // for (int i = 0; i < size; i++) {
        //     Node node = get(i);
        //     builder.append(node.data + ",");
        // }
        Node p = head;
        for (int i = 0; i < size; i++) {
            // 顺藤摸瓜
            p = p.next; // p 指向第 i个结点
            builder.append(p.data + ",");
        }
        if (size != 0) {
            builder.charAt(builder.length() - 1);
        }
        return builder.toString();
    }
}

3. LinkedList

3.1 底层原理

底层是一个双向链表(添加、删除元素效率高,按索引查询效率低)

同时实现了 List、Deque、Queue接口,所以可以当作线性表、队列、双端队列、栈来用

不存在容量不足的问题

3.2 手写LinkedList

LinkedList类

public class LinkedList implements List {

    transient int size = 0; // 有几个结点
    transient Node first; // 指向链表的第一个结点(类似于C的指针,并不是结点对象)
    transient Node last; // 指向链表的最后一个结点

    // 内部类,Node 结点类
    private static class Node<E> {
        E item; // 数据
        Node<E> next; // 指向下一个结点
        Node<E> prev; // 指向前一个结点

        public Node(Node<E> prev, E item, Node<E> next) {
            this.item = item;
            this.next = next;
            this.prev = prev;
        }

        @Override
        public String toString() {
            return "Node{item=" + item + ", next=" + next + '}';
        }
    }

    @Override
    public int size() {
        return size;
    }

    @Override
    public Object get(int i) {
        Node node = node(i);
        return node.item;
    }

    @Override
    public boolean isEmpty() {
        return size == 0;
    }

    @Override
    public void add(int i, Object e) {
        if (i == size) {
            linkLast(e);
        } else {
            linkBefore(e, node(i));
        }
    }

    /**
     * 查询指定索引的结点
     * @param index
     * @return
     */
    private Node node(int index) {
        if (index < (size >> 1)) { // 前一半结点,从第一个结点开始向后查询
            Node p = first;
            for (int i = 0; i < index; i++)
                p = p.next;
            return p;
        } else { // 后一半结点,从后边向前
            Node l = last;
            for (int i = size - 1; i > index; i--)
                l = l.prev;
            return l;
        }
    }

    @Override
    public void add(Object e) {
        linkLast(e);
    }

    // 在最后面插入数据!!!!
    private void linkLast(Object e) {
        final Node l = last;
        final Node newNode = new Node(l, e, null);
        last = newNode;
        if (l == null) {
            first = newNode;
        } else {
            l.next = newNode;
        }
        size++;
    }

    // 在最前面插入数据
    private void linkBefore(Object e, Node succ) {
        final Node pred = succ.prev;
        final Node newNode = new Node(pred, e, succ);
        succ.prev = newNode;
        if (pred == null) {
            first = newNode;
        } else {
            pred.next = newNode;
        }
        size++;
    }
}

分析linkLast添加结点时的内存分配图

以上是关于手写单链表的主要内容,如果未能解决你的问题,请参考以下文章

《重学数据结构与算法》:请手写一个简易的单链表

《重学数据结构与算法》:请手写一个简易的单链表

《重学数据结构与算法》:请手写一个简易的单链表

手写单链表

JavaLearn#(15)集合提升训练:手写ArrayList单链表LinkedListHashMapHashSet新一代并发集合类

JavaLearn#(15)集合提升训练:手写ArrayList单链表LinkedListHashMapHashSet新一代并发集合类