链表在物理上/逻辑上

链表是一种物理存储单元非连续、非顺序的存储结构。
数据元素的逻辑顺序是通过链表中的指针链接次序实现的。

链表每个结点的组成

链表由一系列结点(链表中每一个元素称为结点)组成,结点可以在运行时动态生成。
每个结点包括两个部分:一个是存储数据元素的数据域,另一个是存储下一个结点地址的指针域

链表使用过程中的时间复杂度

链表在插入的时候可以达到O(1)的复杂度,比另一种线性表顺序表快得多,但是查找一个节点或者访问特定编号的节点则需要O(n)的时间,而线性表和顺序表相应的时间复杂度分别是O(logn)和O(1)。
简单说,查询慢,增删快。
因为需要加入一个指针域,所以空间开销比较大。

单向链表

Java链式调用 每组链返回参数不一样_java


它存储的数据分散在内存中,每个结点只能也只有它能知道下一个结点的存储位置。由N各节点(Node)组成单向链表,每一个Node记录本Node的数据及下一个Node。向外暴露的只有一个头节点(Head),我们对链表的所有操作,都是直接或者间接地通过其头节点来进行的。

尝试自己写一个简单的

首先准备好结点(class Node)实现一个单向的,所以只需要准备当前结点的数据,和next地址就可以了。
具体的增删改查方法就是new Node并且处理next 和data就可以了。

public class MyLink<E> {

    /**
     * 头结点,声明链表的时候,实例化一个空的头结点
     */
    Node head = null;

    int size = 0;


    /**
     * 链表插入数据 插入的数据指向的对象,是否存在下一个结点,
     * @param d
     */
    private synchronized void addLastNode(E d){
        final Node<E> newNode = new Node(d);
        if(head ==null){
            head = newNode;
            size++;
            return;
        }
        //定义一个临时的结点  判断头结点下面的值,且不改变实际头结点的值
        Node<E> tmp = head;
        //当前结点的下一个结点是否有值  如果有,当前结点变为next结点
        while(tmp.next!=null){
            tmp = tmp.next;
        }
        //直到last Node  给当前结点的下一个结点赋为需要添加的值
        tmp.next = newNode;
        size++;
    }

    /**
     * 给头结点添加元素
     * @param d
     */
    private synchronized  void addHeadNode(E d){
        //声明一个头结点 next为null,需要将原来的头结点 变成newNode的next
        final Node<E> newNode = new Node(d);
        newNode.next = head;
        head = newNode;
        size++;
    }

    /**
     * 给中间结点添加新结点
     * @param d
     */
    private synchronized  void addSizeNode(int index,E d){
        Node<E> newNode = null;
        //插入中间元素
        for (int i = 0; i < index-1; i++){
            //next index次
            newNode = head.next;
        }
        //得到需要插入的结点的位置,往该节点下插入新的
        Node<E> addNode = new Node<>(d);
        addNode.next = newNode.next;
        newNode.next = addNode;
        size++;
    }

    /**
     * 实例一个删除头部元素
     * @param index
     */
    public synchronized void deleteHeadNode(){
        //现有头部元素作为新的头部
        Node newHead = head.next;

        //原有头部data设置为null;
        head.data = null;
        head.next = null;
        head = newHead;
        size--;

    }


    public static void main(String[] args) {
        MyLink<SystemA> myLink = new MyLink<>();
        myLink.addHeadNode(new SystemA("001","张三","true"));
        myLink.addLastNode(new SystemA("002","李四","true"));
        myLink.addLastNode(new SystemA("004","王五","true"));
        myLink.addSizeNode(2,new SystemA("003","李四","true"));
        myLink.deleteHeadNode();
        System.out.println("长度是:"+myLink.size);
        System.out.println("头元素是:"+myLink.head.data.toString());
        System.out.println("头.next元素是:"+myLink.head.next.data.toString());
    }
}
class Node<E>{
    Node next = null;//结点的引用,指向下一个结点
    E data;//结点的对象,内容
    public Node(E data){
        this.data = data;
    }
}

自己实现的过程中,发现删除元素,新增元素很容易实现,但是需要先找到元素之后再做这些操作才简单。数据的二分法就比较的好。
边界值问题需要考虑很多,比如当前链表的长度,新增的时候是否越界、删除的时候是否越界、长度为0的时候等等等。
那些问题大佬们早就考虑到了
一般实现好的链表会有限定好的值,而且有跳跃表来简化链表的查找数据。

单向链表反转

单向链表反转的方式有比较多的,很粗暴的拆了重组。
从头结点一直next,直到node.next==null,取当前结点为新的头结点【head】并且切断该结点。
继续切已经被切过尾巴的链表,得到的数据添加到新的链表中…直到老的链表为null,返回新的链表表头就可以了。时间复杂度O(n)。

双向链表

双向链表也是同样的准备Node

public class MyLinkDou<E> {

    int size = 0;

    Node head = null;
    Node last = null;

    class Node {
        //实际存储的数据
        public E e;
        //下一个结点
        public Node next;
        //上一个结点
        public Node pre;

        /**
         *
         * @param e
         */
        public Node(E e){
            this.e = e;
            next = null;
            pre = null;
        }
    }
}

其他的增删改查与单向的挺像的。

/**
     * 根据下标找到当前结点的上一个结点
     * @param index
     * @return
     */
    public Node findpreNode(int index){
        Node presend = head;
        int nowindex = -1;

        while(presend.next != null){
            if( nowindex== index - 1){
                return presend;
            }
            presend = presend.next;
            nowindex++;
        }
        return null;
    }

    /**
     * 根据下标插入一个数据
     * @param index
     * @param e
     */
    public void addIndex(int index,E e){
        if(index <0||index>=size)
            return;
        Node node = new Node(e);
        Node preNode = this.findpreNode(index);
        node.next = preNode.next;
        preNode.next.pre = node;
        preNode.next = node;
        node.pre = preNode;
        size++;
    }

阅读LinkedList源码文档注释 哇哦,看着好复杂,但是能看到官方的链表写的有多么的强大。
首先多线程下不安全

*<p><strong>Note that this implementation is not synchronized.</strong>
 * If multiple threads access a linked list concurrently, and at least
 * one of the threads modifies the list structurally, it <i>must</i> be
 * synchronized externally.  (A structural modification is any operation
 * that adds or deletes one or more elements; merely setting the value of
 * an element is not a structural modification.)  This is typically
 * accomplished by synchronizing on some object that naturally
 * encapsulates the list.

其次是双线链表

* Doubly-linked list implementation of the {@code List} and {@code Deque}
 * interfaces.  Implements all optional list operations, and permits all
 * elements (including {@code null}).

源码里写到的结点类,只有一个全参构造器

private static class Node<E> {
        E item;
        Node<E> next;
        Node<E> prev;

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

add方法有六个、remove方法有9个、返回迭代器、clone、等方法。原来学习Java数据结构的时候对链表不是很了解,自己手写后,对这种数据结构也有了自己的认识。

跳跃表

上面说到,链表寻找数据很麻烦,比如ABCDEFG,这种顺序如果要得到F,需要遍历ABCDE结点,才可以到达F结点,如果可以ACEF的跳跃式的访问结点,那么链表查找某个值就快很多了;如果可以AEF的,甚至直接AF,那么取F的值,也是比较简单的。