数据结构之单链表

前戏

以下内容是基于不带头单向非循环的单链表.希望能清楚这一点.

大家都写过顺序表, 不难发现顺序表的删除和增加一个元素很麻烦,每次删除和增加一个元素都需要挪动一部分数据.那么问题来了,有没有一个数据结构删除和增加元素是不用挪动数据的?

答案是肯定的,单链表就来了. 说起single 不由让人想起一首歌, single dog, single dog, single dog the way~

上面说到不带头单向非循环,可能有些兄弟不懂什么意思,里面涵盖三个条件.下面依次来解释.

不带头

链表中每个节点都可以当头, 头节点是不确定的.0x12也可以,后面的节点都可以当头节点.假如头节点是确定的,就确定了整个单链表.后面涉及了代码就可以理解头节点的重要性了.

单向

所谓单向,指的是单指向,每个节点用next连接且只有前一个的next指向下个节点,上图就是单向的.

非循环

最后一个节点的next指向了第一个节点的地址,形成了一个闭环.这是循环,反之则是非循环.

创建链表

我采用的是穷举法,将每个节点定义出来,然后每个节点通过next连接,最后将note1视为头节点.注意只是定义一个变量当作头,这个头随时都可以改变的.

static class Note{
        public int val;
        public Note next;
        public Note(int val){
            this.val = val;
        }
    }
    Note head;
    public void createNote(){
        Note note1 = new Note(21);
        Note note2 = new Note(21);
        Note note3 = new Note(28);
        Note note4 = new Note(21);
        note1.next = note2;
        note2.next = note3;
        note3.next = note4;
        head = note1;
    }

打印链表

首先定义一个变量cur指向head,为什么我要这么写呢? 因为我们研究的是不带头的链表,假如用head去遍历,到最后head就不知道指向哪里了,找不到head,所以我们这里就伪造一个head,让它去遍历整个链表.

那么问题来了,什么时候整个链表走完了?当cur.next == null时,链表走完了吗? 

并没有走完,当前cur的val也要打印出来.cur.next == null 当做while的条件,还有一个问题,就是当head为空时,就会报空指针异常.所以以下代码中while的条件就改成了cur != null.这样就可以遍历完整个链表了.

public void display(){
        Note cur = head;
        while(cur != null){
            System.out.print(cur.val + " ");
            cur = cur.next;
        }
        System.out.println();
    }

链表头插

这里要注意的一点就是先绑定后面的节点,然后head指向note.

public void addFirst(int data){
        Note note = new Note(data);
        note.next = head;
        head = note;
    }

链表尾插

抛开上面的代码不说,while的条件为什么是cur.next != null ?而头插法while的条件是cur != null,我们做事情不要太死了,要学会变通,看下图,当循环结束时,cur是在最后一个节点上面的,这个时候就找到了尾节点,然后将尾节点连接新增的节点就行了. 

当利用cur.next 作为条件时,势必要考虑cur是否为空的问题,也就是head是否为空,也就是while循环上面的代码.当head为空时,也就是说明当前链表是空链表.新增的节点既是头也是尾,head直接更新为新增节点就行了.

public void addLast(int data){
        Note note = new Note(data);
        if(this.head == null){
            this.head = note;
            return;
        }
        Note cur = head;
        while(cur.next !=null){
            cur = cur.next;
        }
        cur.next = note;
    }

任意位置插入

我们一上一个数据结构做铺垫,涉及元素的位置,首先就要考虑该位置是否合法.也就是第一个if语句.

需要注意的是,数据结构中,不能隔着位置插入元素,每种数据结构都一样.

在任意位置新增节点,无非就是三种情况,第一点: 在第一个位置插入,第二点: 在链表的最后插入,第三点: 在链表中插入. 第一点和第二点就是上面的头插和尾插,我们要插入一个元素,首先要找到要插入的位置,然后将节点插入,通过findCur函数找到要插入的位置,然后进行插入节点操作.

需要注意的点还是先绑定后面的节点,然后绑定前面的节点,假如位置替换,cur.next = note, note.next= cur.next. 乍一看没什么问题,推理一下就能发现问题了,cur.next的指向已经发生改变了,指向了note,而cur之前的next找不到了,就是这么神奇,所以说要先绑定后面的节点,然后绑定前面的节点.

还有就是这个方法需要考虑head 为空的情况吗? 答案是不需要,当head为空时,链表的长度为0, 链表为0的情况,上面的if语句就已经处理过了,所以不用考虑head为null的情况了.

public boolean addIndex(int index,int data) throws InputException{
        if(index < 0 || index > size()){
            throw new InputException("输入的位置非法");
        }
        if(index == 0){
            addFirst(data);
            return true;
        }else if(size() == index){
            addLast(data);
            return true;
        }
        Note note = new Note(data);
        Note cur = findCur(index);
        note.next = cur.next;
        cur.next = note;
        return true;
    }
    public Note findCur(int index){
        Note cur = this.head;
        while(index - 1 != 0){
            cur = cur.next;
            index--;
        }
        return cur;
    }

查找是否包含关键字key

这个没啥好说的,就是通过cur去遍历整个链表,找到了返回true,没找到就返回false.方法的开头判断一下链表是否为空就行了.

public boolean contains(int key){
        if(this.head == null){
            return false;
        }
        Note cur = this.head;
        while(cur != null){
            if(cur.val == key){
                return true;
            }
            cur = cur.next;
        }
        return false;
    }

删除第一次出现的关键字key

注意:这里是叫我们删除第一次出现的关键字,假如链表是这样的:1,2,3,3假如要删除的是三,最后删除之后的链表是1, 2, 3.只是删除了第一次出现的3, 而后面的3并没有删除.

要注意的第二点: 所谓的节点删除,并不是被内存回收了,而是没有被当前链表中的节点指向了,但是我们在创建链表的时候,还有一个变量指向被删除的节点,那有人就要问了,有变量指向的节点会被jvm中的垃圾回收机制回收吗?会被回收.

当程序结束时或者节点长时间不被使用时,都会被回收.

这里的删除节点的思路也很简单,首先要找到要删除的节点,然后将节点删除.可能有一行代码,比较难理解就是 cur.next = cur.next.next. 如下图,假如要删除val为三的这个节点的,cur.next指向的下一个节点的地址,下一个节点的地址的next指向的又是下一个节点的地址,当cur.next指向cur.next.next时,其实就是cur下一个节点指向由原来的0x02变成了0x03了.

public void remove(int key) throws EmptyException{
        if(this.head == null){
            throw new EmptyException("链表为空");
        }
        if(this.head.val == key){
            this.head = head.next;
            return;
        }
        Note cur = findCurOfKey(key);
        if(cur == null){
            System.out.println("链表中没有要删除的元素");
        }
        cur.next = cur.next.next;
    }
    public Note findCurOfKey(int key){
        Note cur = this.head;
        while(cur.next != null){
            if(cur.next.val == key){
                return cur;
            }
            cur = cur.next;
        }
        return null;
    }

 删除所有值为key的节点

这里用到了一个常用的算法来解决这个问题.前后指针法, 我利用动态图像来表达删除的整个过程.

假如我们要删除的是33这个节点,cur为空循环结束.

可以看到下面的动态图,当要删除的节点是head节点时,利用如下while循环是无法删除的,head节点要单独删除,那为什么要放在while之后?

假如放在while之前删除的话,万一head后面还有一个要删除的节点,还是删除不了.因为head又到后面去了,所以将head之后的节点删除完了之后,然后单独删除head节点.

public void removeAllKey(int key){
        if(this.head == null){
            throw new EmptyException("链表为空");
        }
        Note cur = this.head.next;
        Note prev = this.head;
        while(cur != null){
            if(cur.val == key){
                prev.next = cur.next;
                cur = cur.next;
            }else {
                prev = cur;
                cur = cur.next;
            }

        }
        if(this.head.val == key){
            head = head.next;
            return;
        }
    }

得到单链表的长度

这个代码没什么好说的,比较容易.我第一次写的时候,还判断链表是否为空,其实不需要判断.

当head = null时,while循环进不去,最后返回conter,这时候的conter的值还是初始化值 0 .

public int size(){
        int conter = 0;
        Note cur = this.head;
        while(cur != null){
            cur = cur.next;
            conter++;
        }
        return conter;
    }

总结

有一个经验丰富的程序员跟我说,写链表的时候,先绑定后面的节点,然后绑定当前节点.

以上就是非循环不带头单链表的所有内容啦,如果对你有帮助的话,不要忘记点赞关注收藏哦!!!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值