手写简易版链表及原理分析

好多人都觉得为什么要自己写这样的数据结构,变成里面不是有吗?为什么要去写,有这个疑问,其实这个疑问这我的脑海中也存在了很长一段时间,本人是学习java编程的,直接看java的集合框架不行吗?这个时候如果你的水平到了还好。如果没有,你会发现你根本就理解不了编程语言里面数据结构,看了就忘掉了,也理解不了,学习了半个月编程里面的集合发现学不会,还要抱怨怎么可以这样,看了半个月都没有看懂,于是就放弃了。如果让我来分析缘由,那就是市面上已经发布的编程语言里面的数据结构(集合框架)都是王者水平(巅峰王者)的人写的,这样厉害的人写出的东西,你一个没有啥基础的编程人员(按照游戏段位排序:青铜),几天几个月就整的明明白白,如果这样都能会,只有一种可能,你是天才,不是普通人。
所以基于这样的分析,我们学习就是要先从没有进数据结构的大门到变得更够秀(进军白银段位以致更高的水平),所以简易版的练习过程是在必行,脚踏实地,方是走过漫漫长路的捷径之路,于此,便有了一些列的数据结构的简易版,今天分析链表。

手写简易版链表

简单源码及思维分析

public class LinkedListDemo<E> {
    /**
     * 节点类
     */
    private class Node {
        public E e;
        public Node next;
        public Node(E e, Node next) {
            this.e = e;
            this.next = next;
        }
        public Node(E e) {
            this(e, null);
        }
        public Node() {
            this(null, null);
        }
        @Override
        public String toString() {
            return e.toString();
        }
    }

    private Node dummyHead;
    private int size;

    public LinkedListDemo() {
        dummyHead = new Node(null, null);
        size = 0;
    }

    public int getSize() {
        return size;
    }

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

泛型类设计理念创建类,这样有更好的类型高可拓展性,由于链表是引用存储方式,所以还需要设计一个内部类Node(用于存放值以及下一个Node节点的指针)。属性有size、dummyHead,先说一下size,是用来记录存放链表中的数据大小,对应size还有两个方法,getSize获取大小以及isEmpty是否为空;dummyHead属性是整个设计链表类的重头戏,在构造方法中 dummyHead = new Node(null, null);,dummy可以翻译为虚拟之意,目前的链表实现是以单向链表形式实现,单向必然需要一个方向管理,那么我这里就是通过头head进行,那又为什么要添加一个虚拟头(dummyHead)而不是null??这里和链表数据结构增删有一定关系,这样设计有利于增加和修改操作(当然还能有更好的设计方式,目前鉴于本人水平有限)。对此原因进行简要论述:
这样设计有利于1、当想要在第一个位置添加的时候能够迅速定位到。2、当想要在第一个位置删除的时候能够迅速定位到。后面将会使用源码进行进一步分析。
在这里插入图片描述

链表的添加方法实现、通过添加方法衍生出来的方法及原理分析

添加方法以及添加方法衍生出来的方法源码如下:

public void add(E e, int index) {
        if (index < 0 && index > size)
           throw new IllegalArgumentException(" index is not correct");

        Node prev = this.dummyHead;
        for (int i = 0; i < index; i++) {
            prev = prev.next;
        }
        prev.next = new Node(e, prev.next);
        size++;

    }

    public void addFirst(E e) {
        add(e, 0);
    }

    public void addLast(E e) {
        add(e, size);
    }

添加方法参数有传入的值以及要存入的下标位置。当然在添加到相应位置前,需要对index进行校验,防止不在链表范围内;
prev就是链表的虚拟节点为起点。之后就是进行for循环,每次都找node=node.next,这里做了一个处理就是如果下个节点不是最终要的节点,那么就将下个节点引用赋值给当前节点,直到找到要插入坐标的前一个节点index=2的引用。
在这里插入图片描述
当index为2是,分析思路如上图所示(index只是一个常数,2或3其实一样,当然可以设置为k,代表常数),for循环如下:

for (int i = 0; i < 2; i++) {
            prev = prev.next;
}

当找到了index下标的前一个位置node以后,将下一个prev.next(下标为2的位置引用)赋值给node.next(目前要插入的Node节点),这是时候,新插入的node已经和下面的node有了指向,然后就把自己在指向prev.next,也就是prev.next=node,0>>>>>1>>>>666>>>>2>>>>等等就算连接起来了,一下表形式放入成功,size进行加1(size++)。
代码赋值解读:①和②实现的赋值是一样的,不太明白的,去看一下上面的节点内部类,想一下就明白了,这里就不做过多的阐述了。

       //①
        Node a=new Node(e);
        a.next=prev.next;
        prev.next=a;
        //②
        prev.next = new Node(e, prev.next);

插入链表的思维图:
在这里插入图片描述
在这里插入图片描述
由add衍生出来的addFirst、add方法分别下标是0和size(其实一下标来论,目前只有size-1个,但是由于是添加到size-1个的后面,肯定是size,添加之前是没有的),即可完成方法封装。

移除remove、衍生出的方法及原理剖析

手写源码如下:

    public E reomve(int index){
        if (index<0||index>size)
            throw new IllegalArgumentException("");
        Node prev = this.dummyHead;
        for (int i = 0; i < index; i++) {
            prev=prev.next;
        }
        Node retNode=prev.next;
        prev.next=retNode.next;
        retNode.next=null;
        size--;
        return retNode.e;
    }
    public E reomveFirst(){
        return reomve(0);
    }
    public E reomveLast(){
        return reomve(size-1);
    }

remove方法:1、首先校验index是否在链表的大小范围内;2、以虚拟头结点为开始位置( Node prev = this.dummyHead),for循环index=0获取到的位置是链表的第一个位置引用,通过循环找到待删除的前一个节点(这里就是找到1下标引用的位置),找到以后将delNode(retNode)要删除节点的next引用赋值到1(prev)为节点的next,即

   Node retNode=prev.next;
   prev.next=retNode.next;

将链表重新连接起来,当然要删除的delNode(retNode)的next指向的3位置的引用需要断开连接,所以赋值为null(retNode.next=null),被删掉节点等待jvm垃圾回收;3、删除掉一个node后size减一(size–),返回删除的值retNode.e
在这里插入图片描述
删除方法衍生出来的删除第一个和最后一个的方法分别是下标为0和size-1,对于removejinx重用。

其他方法(get、set、contains、find、toString)

  //获得链表的第index(0-based)的元素
    //在链表中不是一个常用的操作
    public E get(int index){
          if (index<0||index>size)
              throw new IllegalArgumentException("");
        Node cur = dummyHead.next;
        for (int i = 0; i < index; i++)
            cur=cur.next;
        return cur.e;
    }

    //获得链表的第一个元素
    public E getFirst(){
        return  get(0);
    }
    //获得链表的第一个元素
    public E getLast(){
        return  get(size-1);
    }
    public void set(int index,E e){
        if (index<0||index>size)
            throw new IllegalArgumentException("");
        Node cur = dummyHead.next;
        for (int i = 0; i < index; i++)
            cur=cur.next;
        cur.e=e;
    }
    //查找链表中是否存在元素e
    public boolean contains(E e){
        Node cur = dummyHead.next;
        while (cur!=null){
            if (cur.e.equals(e))
                return true;
            cur=cur.next;
        }
        return false;
    }

    @Override
    public String toString() {
        StringBuffer res = new StringBuffer();
        for ( Node cur = dummyHead.next;cur!=null;cur=cur.next)
            res.append(cur+"--->");
        res.append("null");
        return res.toString();
    }
get方法分析:

1、判断index合法性;2、节点就要从链表的第一个开始,也就是dummyHead.next虚拟节点的下一个。当下标传入的是0时,for不符合循环条件直接返回,不需要进行循环查找,直接返回cur.e;当下标为3时,for循环代码:

   for (int i = 0; i < 3; i++)
            cur=cur.next;

如图思维图所示
在这里插入图片描述
当然是实现get的衍生方法有getFirst和getLast分别是下标为0和size-1的元素。

set分析

set方法原理其实就是在get方法找到node的时候进行对node.e进行赋值,思维逻辑与get一直;

contains分析

基本的思路就是将链表循环一遍,查看穿入值是否与链表中节点的值一致,当然这样比较存在一些问题的(待完善)

写博不易,关注博客了解更多最新内容

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值