数据结构【五】- 链表【链表的介绍和实现:增删改查】

一. 什么是链表

(一)线性数据结构

   我们之前已经学习了三种线性数据结构。他们的底层依然是“依托静态数组”,靠resize解决固定容量问题。

动态是相对用户来说的。但是链表是真正的动态数据结构

  • 动态数组
  • 队列
  • 链表     --   真正的动态数据结构

(二)为什么说链表很重要

  • 是真正的 / 最简单的动态数据结构。如果对于链表学习有基础,有利于学习更加复杂的动态数据结构。
  • 链表涉及到计算机领域一个非常重要的概念,“引用(或者指针)”,这个概念和内存相关。对链表更加深入的理解,可以帮助我么对引用 / 指针 / 计算机系统中和内存管理的相关话题有更加深刻的认识。
  • 链表本身有非常请晰的递归结构。只不过由于链表本身是一种线性的数据结构,所以我们可以非常容易的'使用循环的方式对链表进行操作。但是链表本身具有递归结构的性质,所以它可以帮助深入理解递归机制。
  • 链表本身具有功能性,可以用来辅助组成更加复杂的数据结构,例如:图结构 / 哈希表 。同时对于 栈 / 队列 ,也可以用链表实现。

二. 链表结构的介绍

1. 数据存储在“节点”(Node)中

class Node{
    E e;
    Node next;
}

(1)当前节点存储两个内容:

  • 1. 当前节点的值
  • 2. 指向当前节点的下一个节点

(2)例如

一个链表就像一节火车,每一个节点就像一节车厢,我们在车厢内存储真正的数据。车厢之间要进行连接,使得我们的数据是整合在一起的,用户可以方便在所有的数据中进行操作。数据的连接使用next完成的。

链表存储的数据是有限的,最后一个节点存储的next就是NULL。所以如果一个节点的next是NULL,这就说明它存储的是最后一个节点。

比如一个火车有10节车厢,车厢头是 "1",车厢尾是 "10"

对于车厢"8"来说,它存储的两个内容是:

当前值“章”,它的下一个车厢 "9",其中车厢“9”中包含了车厢“10”.

对于车厢"1"来说,它存储的两个内容是:

当前值“填”,它的下一个车厢 "2",其中车厢“2”包含了后面所有的车厢。

                                            1  --->  2 --->  3  ---> 4  --->  5  --->  6  --->  7   -->  8  ---> 9 --->  10 --> NULL

 2. 优点:真正的动态,不需要处理固定容量的问题

对于链表来说,你需要多少个数据,就可以生成多少个节点,把他们挂接起来。

 3. 缺点:丧失了随机访问的能力

相比较数组来说,数组可以给个索引,直接从数组中拿到对应的值。这是因为从底层机制来说,数组开辟的空间从内存上来说,是连续分布的,所以我们可以直接寻找这个索引对应的偏移,直接计算出相应的数据所存储的内存地址。

但是链表不同,由于链表是靠next一层一层连接的,所以在计算机的底层,每一个节点所在的内存的位置是不同的。我们必须依靠next一点一点找到我们想要找的元素。

三。准备代码

public class LinkdeList<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 haed;
    private int size;
    public LinkdeList(){
        head = null;
        size = 0;
    }
    //获取链表中的元素个数
    public int getSize(){
        return size;
    }
    //判断链表是否为空
    public boolean isEmpty(){
        return size==0;
    }
}

三. 在链表头添加元素 

1. 添加的过程

    需求  如果我们想将“666”这个节点添加入链表头。

    解决:

       (1)将“666”的next指向head. 然后让head指向“666”节点

      (2)这样,节点“666”就被插入到了这个链表的头部。由于这个过程在函数中执行,对于node变量,在函数结束之后,它的快作用域就结束了,这个变量就没用了。

2. 代码

public class LinkdeList<E> {

    private Node head;
    private int size;
    public LinkdeList(){
        head = null;
        size = 0;
    }

    //为链表头添加元素(在链表头添加元素很方便,是因为我们有个变量head来跟踪链表头)
    public void addFist(E e){
        //写法一
        Node node = new Node(e);
        node.next = head;
        head = node;
        //写法二
        head = new Node(e, head);

        size++;
    }
}

三. 在链表的中间添加新的元素

1. 添加的过程

    需求 在索引为2的地方添加元素666

    解决:

            (1) 知道要添加的节点“666”之前的节点,称为prev节点。这里也就是索引为1的节点。

            (2) 找到prev之后,将“666”.next指向pre.next。也就是: node.next = prev.next.

            (3) 然后将prve.next指向“666”.也就是 prev.next = node。

            【注意:如果先执行prev.next = node,此时prex.next已经等于node.next了,后面的条件node.next = prev.next就不成立了】

 

          (4) 然后就完成了添加过程。

      (5) 这个过程的关键:找到要添加的节点的前一个节点。       

      (6) 但是对于添加链表的头部而言,头部是没有前一个节点的,所以对此要特殊处理

 

2. 代码

    //为链表的Index位置添加元素(Index位置从0开始记)。但是这个操作不是一个常用的操作,仅作练习
    public void add(int index, E e){
        if(index < 0 || index > size){
            throw new IllegalArgumentException("Add failed. Illegal index.");
        }
        if(index == 0){
            addFist(e);
        }else{
            //1.找到待插入节点的前一个节点
            Node prev = head;
            for(int i=0; i<index-1; i++){
                prev = prev.next;
            }
            //2.开始添加--写法一
            Node node = new Node(e);
            node.next = prev.next;
            prev.next = node;
            //2.开始添加--写法二
   //         prev.next = new Node(e, prev.next);
            size++;
        }
    }

四. 添加新的元素--使用链表的虚拟头节点

1. 原理

    需求 在我们上一个中,我们写了向链表中间添加元素的代码。但是当时我们对在【链表的开头添加元素】做了特殊处理。究其原因,是我们的上一节中在向链表添加元素的时候,要找到【待添加位置相应之前的节点】,但是对于在链表头添加元素来说没有之前的节点。所以我们就需要另外一种方法来代替上一节的操作: 【为链表设立虚拟头节点】

    解决:

       (1)我们在链表头之前给一个节点,这个节点不存储任何元素,所以值写为null. 我们叫它dummyHead. 对于这个节点,实际是不存在的,对于用户来说看不到也没有意义,是我们自己虚构的节点。

       (2)因为定义了dummyHead,那么以前定义的【head】变量就可以被代替,那么就将之前的代码变为:

public class LinkdeList<E> {
    private Node dummyHead;
    private int size;
    public LinkdeList(){
        dummyHead = new Node(null,null);
        size = 0;
    }
}

       (3)然后逻辑是一样的,我们需要找到这个节点的前一个位置的节点。

                 1. 对于上一个add()方法,我们找到前一个位置节点的代码如下.这里的for循环中的判断条件是【i<index-1】。

        Node prev = haed;
        //1.找到待插入节点的前一个节点
        for(int i=0; i<index-1; i++){
            prev = prev.next;
        }

                 2. 但是对于虚拟头节点来说,它前面相当于多了一个节点,这里的循环条件就要变化:

        Node prev = dummyHead;
        //1.找到待插入节点的前一个节点
        for(int i=0; i<index; i++){
            prev = prev.next;
        }

 

2. 代码

public class LinkdeList<E> {
    private Node dummyHead;
    private int size;
    public LinkdeList(){
        dummyHead = new Node(null,null);
        size = 0;
    }
   //使用虚拟头节点添加元素
    public void add(int index, E e){
        if(index < 0 || index > size){
            throw new IllegalArgumentException("Add failed. Illegal index.");
        }
        Node prev = dummyHead;
        //1.找到待插入节点的前一个节点
        for(int i=0; i<index; i++){
            prev = prev.next;
        }
        //2.开始添加
        prev.next = new Node(e, prev.next);//如果这句代码看不懂的话,去上一节看有解释
        size++;
    }
    public void addFist(E e){
        add(0,e);
    }
}

 

五. 链表的遍历/ 查询和修改

(一) 获得链表中的第index个位置的元素

1. 原理

    需求 获得index位置的节点的值

2. 代码

    //获得链表的第index(0-based)个位置的元素
    public E get(int index) {
        //1. 对合法性进行判断
        if (index < 0 || index > size) {
            throw new IllegalArgumentException("Add failed. Illegal index.");
        }
        //2.遍历链表--这里是找到index位置的元素,也就是当前元素
        Node curr = dummyHead.next;
        for(int i=0; i< index; i++){
            curr = curr.next;
        }
        return curr.e;
    }

(二) 修改链表的第index位置的元素

1. 原理

    需求 修改第index位置的元素

2. 代码

    //修改链表的第index(0-based)个位置的元素
    public void set(int index, E e) {
        //1. 对合法性进行判断
        if (index < 0 || index > size) {
            throw new IllegalArgumentException("Add failed. Illegal index.");
        }
        //2.遍历链表--这里是找到index位置的元素,也就是当前元素
        Node curr = dummyHead.next;
        for(int i=0; i< index; i++){
            curr = curr.next;
        }
        curr.e = e;
    }

(三) 查找链表中是否有元素e

    //查找链表中是否有元素e
    public boolean contians(E e) {
        Node curr = dummyHead.next;
        while(curr != null){
            if(curr.e.equals(e)){
                return true;
            }
            curr = curr.next;
        }
        return false;
    }

(四) 测试元素

1.写一个toString()来方便打印

public class LinkdeList<E> {
    @Override
    public String toString(){
        StringBuilder res = new StringBuilder();
        Node curr = dummyHead.next;
        //循环写法一
        while(curr != null){
            res.append(curr + "->");
            curr = curr.next;
        }
        //循环写法二
//        for(Node cur = dummyHead.next; curr!=null; curr=curr.next){
//            res.append(curr + "->");
//            curr = curr.next;
//        }
        res.append("NULL");
        return res.toString();
    }
}

2. 写测试代码

   public static void main(String[] args){
        LinkdeList<Integer> linkdedList = new LinkdeList<>();
        for(int i=0; i<5; i++){
            linkdedList.addFist(i);
        }
        linkdedList.add(2,666);
        System.out.println(linkdedList);
    }

3.测试结果

4->3->666->2->1->0->NULL

六. 从链表中删除元素

1. 原理

    需求 使用用虚拟头节点的链表来删除索引为2位置的元素

    解决:

       步骤一:对于删除元素,我们依然要找到链表的头一个位置的节点。

       步骤二:我们要做的就是: prev.next  = delNode.next; prev节点跳过了原本的delNode节点,指向了原本的delNode.next;把2位置的节点跳过去了。这里我的理解是这样的:【prev中由两个属性,其中e保存的是值,next保存的是下一个的地址。我们这里的prev.next  = delNode.next改变的就是prev的下一个地址的指向,所以可以改变链表的结构。】(仅供参考)

       步骤三:为了让java能够回收空间,我们应该手动的让2位置的节点的next和链表脱离。也就是让delNode.next指向一个NULL;这个时候才真正的把2位置的节点从链表中删除了。

         【问题1】:一些人会觉得我既然我是删除delNode,那我直接令delNode=null,不就好了。

                     【解答1】:这里的delNode是一个临时引用类型变量,它是内存实际存储值的引用,你令delNode=null,只是改变了delNode这个引用的指向,等于它不再指向222了。但是222本身和333之间的联系仍然存在,也就是待删除节点的next仍然指向原先他所指向的位置

         【问题2】:如果我们令delNode.next=null.那delNode不用手动清除吗?

                    【解答2】:在delNode和333脱离关系后,我们不用手动给delNode设置为空,因为它本身就是这个函数种创建的临时变量,函数声明周期结束之后,这个变量的生命周期也结束了。至于他所指向的空间,由于不再和其他内存空间有任何联系,会由java的垃圾回收自动处理。

       步骤四:疑惑问题

       关于链表的删除,有一部分意见认为可以这样找到待删除位置的元素delNode,标记为delNode。这个时候,只要让 : delNode =delNode.next ,这个时候就会delNode这个元素就会被删除。这个想法是错误的。就如如下做法:

        1)   Node delNode= prev.delNode;

        2)   delNode= delNode.next;

我的理解(仅供参考)

1. 这两句话句话做的操作是:创建一个临时的引用变量delNode,这个引用变量原来存储的是222的内存地址,但是经过第二句代码之后,现在delNode的引用变量指向delNode.next所在的内存空间。但是自始至终都是delNode这个临时变量在变化,只是将dleNode的指向位置从原来的一个位置变到了另外一个位置。和我们链表本身没有任何关系。这个涉及到引用数据类型的概念,可以参考我另外一篇文章java基础【一】基本类型变量和引用类型变量】。

 2. 对于delNode=delNode.next这句话,我们可以这样理解:比如链表的toString中,循环里就一直在使用cur = cur.next的方式,从头到尾遍历我们的整个链表,每次调用cur = cur.next,cur就像后移动了一个节点

3.  如果我们想让整个链表发生改变,我们必须把链表中的节点中相应的next指向发生变化。所以prev这个节点怎么变化并不重要,重要的是我们必须要prev这个节点指正确的位置。

       步骤五:实践delNode= delNode.next

          举个例子,现在有一个链表 ,在2位置设置为cur,值是222。                         

           

         1) 如果代码是cur= cur.next.在执行执行代码前面,我们已经遍历到了2位置,目前的2位置的元素的值是222.它的下一个节点,也就是curr.next的值,是333,再下一个,也就是curr.next.next的值,是444。

                  2) 如果在代码cur= cur.next之后. 查看链表,本身没有任何改变。只是改变了curr的值。

        

    2. 代码

   //从链表中删除index(0-based)位置的元素,返回删除的元素
    public E remove(int index){
        if(index<0 || index>=size) {
            throw new IllegalArgumentException("Remove failed. Index is illegal.");
        }
        //1.找到待删除节点的之前的一个节点
        Node prev = dummyHead;
        for(int i=0; i<index; i++){
            prev = prev.next;
        }
        //2.找到待删除的节点
        Node delNode = prev.next;
        //3.改变prev.next的指向
        prev.next = delNode.next;//对于delNode实质只是一个临时存放当前节点引用的变量,对于delNode的操作只是修改它的指向,并没有对链表造成实质的副作用。而维护链表链接关系的是Node节点的next域。
        //4.让delNode彻底和当前链表脱离了关系
        delNode.next = null;
        size--;
        return delNode.e;
    }
    public E removeFirst(int index){
        return remove(0);
    }
    public E removeLast(int index){
        return remove(size-1);
    }

    3. 测试代码

    public static void main(String[] args){
        LinkdeList<Integer> linkdedList = new LinkdeList<>();
        linkdedList.addFist(000);
        linkdedList.add(1,111);
        linkdedList.add(2,222);
        linkdedList.add(3,333);
        linkdedList.add(4,444);
        System.out.println(linkdedList);
        linkdedList.remove(2);
        System.out.println(linkdedList);
    }

测试结果

0->111->222->333->444->NULL
0->111->333->444->NULL

七.所有的代码

public class LinkdeList<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 LinkdeList(){
        dummyHead = new Node(null,null);
        size = 0;
    }
    //获取链表中的元素个数
    public int getSize(){
        return size;
    }
    //判断链表是否为空
    public boolean isEmpty(){
        return size==0;
    }
    //为链表末尾添加元素
    public void addLast(E e){
        add(size, e);
    }
    //使用虚拟头节点添加元素
    public void add(int index, E e){
        if(index < 0 || index > size){
            throw new IllegalArgumentException("Add failed. Illegal index.");
        }
        Node prev = dummyHead;
        //1.找到待插入节点的前一个节点
        for(int i=0; i<index; i++){
            prev = prev.next;
        }
        //2.开始添加
        prev.next = new Node(e, prev.next);//如果这句代码看不懂的话,去上一节看有解释
        size++;
    }
    public void addFist(E e){
        add(0,e);
    }
    //获得链表的第index(0-based)个位置的元素
    public E get(int index) {
        //1. 对合法性进行判断
        if (index < 0 || index > size) {
            throw new IllegalArgumentException("Add failed. Illegal index.");
        }
        //2.遍历链表--这里是找到index位置的元素,也就是当前元素
        Node curr = dummyHead.next;
        for(int i=0; i< index; i++){
            curr = curr.next;
        }
        return curr.e;
    }
    //修改链表的第index(0-based)个位置的元素
    public void set(int index, E e) {
        //1. 对合法性进行判断
        if (index < 0 || index > size) {
            throw new IllegalArgumentException("Add failed. Illegal index.");
        }
        //2.遍历链表--这里是找到index位置的元素,也就是当前元素
        Node curr = dummyHead.next;
        for(int i=0; i< index; i++){
            curr = curr.next;
        }
        curr.e = e;
    }
    //查找链表中是否有元素e
    public boolean contians(E e) {
        Node curr = dummyHead.next;
        while(curr != null){
            if(curr.e.equals(e)){
                return true;
            }
            curr = curr.next;
        }
        return false;
    }
    //从链表中删除index(0-based)位置的元素,返回删除的元素
    public E remove(int index){
        if(index<0 || index>=size) {
            throw new IllegalArgumentException("Remove failed. Index is illegal.");
        }
        //1.找到待删除节点的之前的一个节点
        Node prev = dummyHead;
        for(int i=0; i<index; i++){
            prev = prev.next;
        }
        //2.找到待删除的节点
        Node delNode = prev.next;
        //3.改变prev.next的指向
        prev.next = delNode.next;
        //4.让delNode彻底和当前链表脱离了关系
        delNode.next = null;
        size--;
        return delNode.e;
    }
    public E removeFirst(){
        return remove(0);
    }
    public E removeLast(){
        return remove(size-1);
    }
    @Override
    public String toString(){
        StringBuilder res = new StringBuilder();
        Node curr = dummyHead.next;
        //循环写法一
        while(curr != null){
            res.append(curr + "->");
            curr = curr.next;
        }
        //循环写法二
//        for(Node cur = dummyHead.next; curr!=null; curr=curr.next){
//            res.append(curr + "->");
//            curr = curr.next;
//        }
        res.append("NULL");
        return res.toString();
    }

    public static void main(String[] args){
        LinkdeList<Integer> linkdedList = new LinkdeList<>();
        linkdedList.addFist(000);
        linkdedList.add(1,111);
        linkdedList.add(2,222);
        linkdedList.add(3,333);
        linkdedList.add(4,444);
        System.out.println(linkdedList);
        linkdedList.remove(2);
//        System.out.println(linkdedList);
//        linkdedList.removeFirst();
//        System.out.println(linkdedList);
//        linkdedList.removeLast();
        System.out.println(linkdedList);
    }
}

八. 链表的时间复杂度分析

(一)添加操作 add()

  • addLast(e)   O(n)
  • addFirst(e)   O(1)
  • add(index, e)   O(n/2) = O(n)

(二)删除操作 remove()

  • removeLast(e)   O(n)
  • removeFirst(e)   O(1)
  • remove(index, e)   O(n/2) = O(n)

(三)修改操作 set(index, e)   O(n)

(四)查找操作

  • get(index)   O(n)
  • contains(index)   O(n)

 

 

 

                                    以上所有内容都是通过"慕课网"听"liuyubobobo"的《玩转数据结构》课程后总结

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值