数据结构-链表(Java语言实现)、JDK中LinkedList源码剖析

我们刚接触数据结构最基础的数据结构那就是数组,但是数组在创建时大小就要被固定,数组的存储地址要是连续的等等不好的地方,当然数组也有他的好处,查询速度快等等我们这篇博客以数组和链表的不同开始讲解链表,最后再来剖析一下JDK中实现的链表LinkedList的源码,通过学习源码进一步提升。
有兴趣朋友可以看看我的数组的博客。

数组和链表

  • 数组有‘1好2不好’
  1. 好是,数组的内存是绝对连续的,因此数组的随机访问操作非常的快,时间复杂度是O(1) O(1)O(1),为常量时间,例如arr[20]和arr[2000]的访问花费的时间是一样的。
  2. 不好是 :
    a)数组在定义的时候,必须指定其大小,实际应用中,当数组元素满了以后,要进行扩容,扩容的代码虽然简单,如arr = Arrays.copyOf(arr, arr.length*2)就能够达到2倍扩容的效果,但是当数组的元素数量比较大的话,扩容需要经过开辟更大块内存,拷贝数据,GC回收原来的旧内存等步骤,其效率就比较低了,扩容花费的时间也长了。当扩容一段时间后,数组内存空间特别大,但是随着删除操作,最后只有少量的有效元素(如10000个数组元素空间,却只存储了10个有效元素),内存就被浪费了,这时候就得缩容(如ArrayList数组集合的缩容实现),其效率也比较低。
    b)在数组中,插入元素或者删除元素,都需要经过大量数据的移动。插入操作会引起插入点后面的元素都向后进行移动;删除操作会引起删除点后面的元素都向前进行移动,这两个操作的时间花费都是O(n) O(n)O(n),是线性时间,说明随着数组的元素数量越多,插入删除操作的效率越低。
  • 相对于数组,那么链表就有‘1不好2好’
  • 2好是:
    a)链表的每一个元素节点都是独立new出来的,也就是说链表中所有元素节点内存并不是连续的,而是上一个节点记录了下一个节点的地址,这样内存的使用效率就非常的高,在存储数据量比较大的时候,不需要大片连续的内存空间,因此只要当前JVM可用内存足够大,链表就可以无限生成新的节点,不存在大块内存开辟,大量数据拷贝,GC回收旧内存的内存扩容问题,这一点要比数组优秀!

b)在链表中插入数据,只需要生成新的节点接入链表中就可以,不涉及其它数据节点的移动;在链表中删除数据,只需要把待删除节点从原链表中卸载下来就可以,也不涉及其它数据节点的移动,也就是数据插入和删除的时间复杂度是O(1) O(1)O(1),常量时间,速度很快(这个指的是插入删除操作本身花费的时间,往往在链表中做插入和删除操作的时候,首先会遍历链表,找到合适的位置,那么链表搜索的时间是O(n) O(n)O(n),为线性时间)。

  • 1不好是,因为链表中节点的内存不是连续的,所以就不能像数组那样支持元素的随机访问(就是通过指定下标,访问对应的元素的值),当在链表中访问一个元素节点时,总是要从头节点开始,一个个往后遍历,链表元素越多,遍历的时间就越长,因此链表遍历搜索的时间是O(n) O(n)O(n),为线性时间,没有数组随机访问O(1) O(1)O(1)的效率好。

  • 结论:所以一般插入删除操作多,用链表;随机访问多,用数组。当然还需要具体情况具体分析。数组和链表各操作时间。

在这里插入图片描述

链表的实现

单链表

public class SingleLinkedList<T> {

    HeadNode <T> headNode;

    public SingleLinkedList() {
        this.headNode = new HeadNode<>(null,null,0);
    }

    /**
     * 头插法,头结点后插入数据
     * @param val
     */
    public void insertHead(T val){
        Node<T> newNode=new Node<>(val,this.headNode.next);//将新节点的下一个设置为链表第二个节点
        this.headNode.next=newNode;
        this.headNode.count++;
    }

    /**
     * 尾插法,链表尾部插入数据
     * @param val
     */
    public void insertTail(T val){
        Node node=this.headNode;
        while (node.next != null){
            node=node.next;
        }
        node.next=new Node(val,null);
        this.headNode.count++;
    }

    /**
     * 展示链表中所有的节点
     */
    public void show(){
        Node node=this.headNode.next;
        while (node!=null){
            System.out.println(node.data);
            node=node.next;
        }
    }

    /**
     * 删除指定位置的节点
     * @param val
     */
    public void remove(T val){
        Node<T> pre = headNode;
        Node<T> cur = headNode.next;

        while(cur != null){
            if(cur.data == val){
                // val节点的删除
                pre.next = cur.next;
                cur = pre.next; // 重置cur,继续向后删除链表中所有值为val的节点
                this.headNode.count -= 1; // 更新头节点中链表节点的个数
            } else {
                pre = cur;
                cur = cur.next;
            }
        }

    }

    /**
     * 单链表的节点类型
     * @param <T>
     */
    static class Node<T>{
        T data;//链表中存储的数据
        Node<T> next;//下一个节点

        public Node(T data, Node<T> next) {
            this.data = data;
            this.next = next;
        }
    }

    /**
     * 链表的头结点
     * @param <T>
     */
    static class HeadNode<T> extends Node<T>{
        int count;//链表中数据的个数

        public HeadNode(T data, Node<T> next,int count) {
            super(data, next);
            this.count=count;
        }
    }

}

单向循环链表

在这里插入图片描述

public class CircleLinkList<T> {
    int size;//链表长度
    Node<T> headNode;//头结点
    Node<T> tailNode;//尾节点

    public CircleLinkList() {
        this.size = 0;
        this.headNode = null;
        this.tailNode = null;
    }

    /**
     * 头部插入数据
     * @param val
     */
    public void addHead(T val){
        Node node=new Node(val,null);
        if (size<=0){//链表无元素
            node.next=node;
            tailNode=headNode=node;
            size++;
        }else {
            tailNode.next=node;
            node.next=headNode;
            headNode=node;
            size++;
        }
    }

    /**
     * 尾部插入数据
     * @param val
     */
    public void addTail(T val){
        Node node=new Node(val,null);
        if (size<=0){
            node.next=node;
            tailNode=headNode=node;
            size++;
        }else {
            tailNode.next=node;
            node.next=headNode;
            tailNode=node;
            size++;
        }
    }

    public void disPlay(){
        Node node=headNode;
        while (node.next!=headNode){
            System.out.println(node.data);
            node=node.next;
        }
        System.out.println(node.data);
    }


    //节点类型
    static class Node<T>{
        T data;
        Node<T> next;

        public Node(T data, Node<T> next) {
            this.data = data;
            this.next = next;
        }
    }
}

双向链表

我们这里先自己来实现双向链表,在JDK中LinkedList实现就是双向链表,之后我们会进行源码剖析。
在这里插入图片描述

public class TwoWayLinkList<T> {
    int size;//节点的个数
    Node headnode;//头结点

    public TwoWayLinkList() {
        this.size = 0;
        this.headnode = new Node(null, null, null);
    }

    /**
     * 头插数据
     *
     * @param val
     */
    public void addHead(T val) {
        Node<T> node = new Node<>(val, headnode, headnode.nextnode);
        this.headnode.nextnode = node;
        if (node.nextnode != null) {
            node.nextnode.prenode = node;
        }
        size++;
    }

    public void addTail(T val) {
        Node tmpnode = this.headnode;
        while (tmpnode.nextnode != null) {
            tmpnode = tmpnode.nextnode;
        }
        tmpnode.nextnode = new Node<>(val, tmpnode, null);
        size++;
    }

    /**
     * 打印整个链表
     */
    public void disPlayList() {
        Node node = this.headnode.nextnode;
        while (node != null) {
            System.out.println(node.data);
            node = node.nextnode;
        }
    }

    /**
     * 节点
     *
     * @param <T>
     */
    static class Node<T> {
        T data;
        Node<T> prenode;
        Node<T> nextnode;

        public Node(T data, Node<T> prenode, Node<T> nextnode) {
            this.data = data;
            this.prenode = prenode;
            this.nextnode = nextnode;
        }
    }

    
}

反转链表的三种方法

  • 下面是基于单链表的的逆置方法
  1. 反转链表方法一:将这个链表每一个节点的next改为前一个节点,最后把headnode.next重置
/**
     * 反转链表方法一:将这个链表每一个节点的next改为前一个节点,最后把headnode.next重置
     */
    public void reverse2(){
        Node pre=null;
        Node curr=headNode.next;
        Node tmp;
        while (curr!=null){
            tmp=curr.next;
            //当前节点的下一个节点,改为前一个节点
            curr.next=pre;
            //移动pre和curr
            pre=curr;
            curr=tmp;
        }
        headNode.next=pre;
    }
  1. 把当前链表的下一个节点pCur插入到头结点dummy的下一个节点中,就地反转。
    把当前链表的下一个节点pCur插入到头结点dummy的下一个节点中,就地反转。
    dummy->1->2->3->4->5的就地反转过程:

dummy->2->1->3->4->5
dummy->3->2->1->4->5
dummy->4>-3->2->1->5
dummy->5->4->3->2->1

在这里插入图片描述

public ListNode reverseList1(ListNode head) {
 3         if (head == null)
 4             return head;
 5         ListNode dummy = new ListNode(-1);
 6         dummy.next = head;
 7         ListNode prev = dummy.next;
 8         ListNode pCur = prev.next;
 9         while (pCur != null) {
10             prev.next = pCur.next;
11             pCur.next = dummy.next;
12             dummy.next = pCur;
13             pCur = prev.next;
14         }
15         return dummy.next;
16     }
  1. 创建一个新链表不断使用头插法

判断链表是否有环

/**
 * 判断链表是否有环,如果有,返回入环节点的值,没有环,返回null
 * @return
 */
public T getLinkCircleVal(){
    Entry<T> slow = this.head.next;
    Entry<T> fast = this.head.next;

    // 使用快慢指针解决该问题
    while(fast != null && fast.next != null){
        slow = slow.next;
        fast = fast.next.next;
        if(slow == fast){
            break;
        }
    }

    if(fast == null){
        return null;
    } else {
        /**
         * fast从第一个节点开始走,slow从快慢指针相交的地方开始走,
         * 它们相遇的时候,就是环的入口节点
         */
        fast = this.head.next;
        while(fast != slow){
            fast = fast.next;
            slow = slow.next;
        }
        return slow.data;
    }
}


合并两个单链表

/**
 * 合并两个有序的单链表
 * @param link
 */
public void merge(Link<T> link){
    Entry<T> p = this.head;
    Entry<T> p1 = this.head.next;
    Entry<T> p2 = link.head.next;

    // 比较p1和p2节点的值,把值小的节点挂在p的后面
    while(p1 != null && p2 != null){
        if(p1.data.compareTo(p2.data) >= 0){
            p.next = p2;
            p2 = p2.next;
        } else {
            p.next = p1;
            p1 = p1.next;
        }
        p = p.next;
    }

    if(p1 != null){ // 链表1还有剩余节点
        p.next = p1;
    }

    if(p2 != null){ // 链表2还有剩余节点
        p.next = p2;
    }
}


LinkedList源码剖析

我们看LinkedList几个需要注意的点,其他方面它的实现和我上面的基本相似。

  1. Node节点,我们能够看到LinkedList是双向链表。
    在这里插入图片描述
  2. 当我们调add方法添加元素默认是尾插法
    在这里插入图片描述
    在这里插入图片描述
  3. get方法获取指定位置的元素,我们看到它是用了二分查找的思想,
    在这里插入图片描述
    在这里插入图片描述
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值