【数据结构】3000字剖析链表及双向链表

文章目录

  • 链表的概念与结构
    • 链表的介绍
      • 链表的模拟实现
    • 双向链表
      • 双向链表的模拟实现
    • 链表常用的方法
    • 链表及顺序表的遍历
    • ArrayList和LinkedList的差异

链表的概念与结构

前面讲解了 ArrayList 实现的顺序表,下面讲解线性表的另一种存储结构 链表

在顺序表中,所有的元素都是在一段地址连续的存储单元上保存的,每个元素挨着每一个元素,通过下标也可以进行访问;但缺点是在非首尾的插入和删除时,需要移动大量的元素,时间复杂度较高;
【数据结构】3000字剖析链表及双向链表_第1张图片

所以,为了解决这个问题,就衍生出了另一种数据结构——链表

链表链表是用一段物理地址不连续的存储单元依次存储数据的线性结构;

有n个节点组成,每个节点里面除了存储数值以外,还保存了下一个节点的地址,这样就可以通过地址依次找到对应的下一个节点;
【数据结构】3000字剖析链表及双向链表_第2张图片
【数据结构】3000字剖析链表及双向链表_第3张图片

链表的介绍

【数据结构】3000字剖析链表及双向链表_第4张图片

2、有头或者无头链表
【数据结构】3000字剖析链表及双向链表_第5张图片
3、循环或非循环链表
【数据结构】3000字剖析链表及双向链表_第6张图片

根据以上三种链表结构又可以组合成一下八种结构:
【数据结构】3000字剖析链表及双向链表_第7张图片
而最常用的是以上无头单向非循环链表无头双向链表,下面就针对于两种链表进行详细的剖析!!!

链表的模拟实现

要想深刻的理解链表,就要亲手实现一下,这样才能深切的领会到链表他是怎样进行操作的,如何使用的!!!

首先,先思考,如果要实现一个链表的话,都要有哪些功能呢?下面我实现了一个链表功能,我将针对实现的链表每一部分进行一个剖析:

要实现一个链表,首先实现以下几个基础的功能:

1、节点的增加

2、节点的删除

3、数据的查找

4、节点的插入

//无头单向非循环链表实现
public class LinkedList {
    
    static class listNode {
        private int value;//结点中的值
        private listNode next;//下一个结点的地址
        public listNode(int value) {
            this.value = value;
        }
    }
        //创建一个链表
    public void createList() {
        ListNode listNode1 = new ListNode(20);
        ListNode listNode2 = new ListNode(30);
        ListNode listNode3 = new ListNode(40);
        ListNode listNode4 = new ListNode(50);
        ListNode listNode5 = new ListNode(60);

        listNode1.next = listNode2;
        listNode2.next = listNode3;
        listNode3.next = listNode4;
        listNode4.next = listNode5;

        this.head = listNode1.next;
    }
    
    private listNode head;//第一个结点的地址

    //头插法
    public void addHead(int val) {
        ListNode listNode = new ListNode(val);
        listNode.next = head;
        head = listNode;
        return;      
    }
    //尾插法
    public void addLast() {
          ListNode listNode = new ListNode(val);
        ListNode cur = head;
        while(cur.next != null) {
            cur = cur.next;
        }
        cur.next = listNode;
    }
    //任意位置插入
    public void addIndex() {
         ListNode listNode = new ListNode(value);
        if(index == 0) {
            addHead(value);
            return;
        }
        if(index >figureSize() || index<0) {
           throw new IndexException("输入下标下标有误");
        }

        ListNode cur = head;
        ListNode pos = checkIndex(cur, index);//定义一个查找前一个节点的方法
        listNode.next = pos.next;
        pos.next = listNode;
    }
    //查找前一个节点方法
      public  static  ListNode checkIndex(ListNode cur, int index) {
        for(int i =0; i<index-1; i++) {
            cur = cur.next;
        }
        return cur;
    }
    //查找链表中是否包含查找的值
    public void contains() {
         ListNode cur = head;
        for(int i =0; i<figureSize(); i++) {
            if(cur.value == key) {
                System.out.println("链表中包含查找的值");
                return;
            }
            cur = cur.next;
        }
        System.out.println("链表中不包含查找的值");
    }
    //删除第一次出现的指定的值的结点
    public void delFirst(int key) {
        if(head == null) {
            return;
        }
        if(key == head.value) {
            head = head.next;
            return;
        }
        ListNode cur =  searchVal(key);
        if(cur == null) {
            System.out.println("没有您要删除的值");
        }

        assert cur != null;
        ListNode del = cur.next;
        cur.next = del.next;
    }
    //负责查找前一个节点
    public ListNode searchVal(int key) {
        ListNode cur = head;
        for(int i =0; i<figureSize(); i++) {
            if(key == cur.next.value) {
                return cur;
            }
            cur = cur.next;
        }
        return null;
    }
    //删除所有值为key的结点
    public void allPoint(int key) {
         if(head == null) {
            return;
        }
        ListNode prev = head;
        ListNode cur = head.next;
        while(cur != null){
            if(cur.value == key) {
                prev.next = cur.next;
                cur = cur.next;
            }else {
                prev = cur;
                cur = cur.next;
            }
            if(head.value == key) {
                head = head.next;
            }

        }
    }
       //打印链表
    public void disPlay() {
        ListNode cur = head;
        while(cur != null) {
            System.out.println(cur.value +" ");
            cur = cur.next;
        }
    }
    //计算单链表的长度
    public void figureSize() {
        int count = 0;
        ListNode cur = head;
        while(cur!=null) {
            count++;
            cur = cur.next;
        }
        return count;
    }
    //清空节点
    public void clear() {
        head = null;
    }
    
}

下面对这些功能进行一一的实现:

1、如何创建一个结点

在实现这些功能前,需要先知道如何去写这个结点的代码,下图讲解:
【数据结构】3000字剖析链表及双向链表_第8张图片
【数据结构】3000字剖析链表及双向链表_第9张图片
2、创建一个链表

在已经创建好一个结点的基础上,如何创建链表呢,下面来实现一下:
【数据结构】3000字剖析链表及双向链表_第10张图片

1、头插法
【数据结构】3000字剖析链表及双向链表_第11张图片

2、尾插法
【数据结构】3000字剖析链表及双向链表_第12张图片
3、任意位置插入
【数据结构】3000字剖析链表及双向链表_第13张图片
【数据结构】3000字剖析链表及双向链表_第14张图片

4、查找链表中是否包含查找的值
【数据结构】3000字剖析链表及双向链表_第15张图片

5、删除第一次出现的指定的值的结点

【数据结构】3000字剖析链表及双向链表_第16张图片
【数据结构】3000字剖析链表及双向链表_第17张图片

6、删除所有值为key的节点

【数据结构】3000字剖析链表及双向链表_第18张图片
【数据结构】3000字剖析链表及双向链表_第19张图片

6、清除节点

直接将头节点置为空,因为只要头节点中的地址为空了,那么后面的结点就没有被引用指向了,所以就会被内存回收
【数据结构】3000字剖析链表及双向链表_第20张图片

双向链表

单向链表在使用上有以下几个缺点:

1、在删除和尾插时,时间复杂度较高

2、只能从前向后访问,无法从后向前访问,限制较大

而下面,介绍的双向链表是非常牛掰的,包括以后在实现栈、队列、双向队列等方面,使用双向链表会方便很多,下面先简单介绍一下双向链表:
【数据结构】3000字剖析链表及双向链表_第21张图片

双向链表的模拟实现

如果想要彻底的理解链表的话,我的建议是,自己多模拟实现几遍这个链表,这样可以更加有利于你灵活的去使用,而不是只记住它的方法的功能。

//无头双向链表的实现
public class LinkedList {

    static class ListNode{
        private int val;
        private ListNode next;
        private ListNode prev;

        public ListNode(int val) {
            this.val = val;
        }
    }

    private ListNode head;
    private ListNode last;
    //头插法
    public void addFirst(int val) {
        ListNode node = new ListNode(val);
        //情况一:链表中无节点
        if(head == null) {
            head = node;
            last = node;
            return;
        }
        //情况二:链表中无节点
        node.next = head;
        head.prev = node;
        head = node;
    }
    //尾插法
    public void addLast(int val) {
        ListNode node = new ListNode(val);
        //情况一:链表中无节点
        if(head == null && last == null) {
            head = node;
            last = node;
        }
        //情况二:链表中有节点
        last.next = node;
        node.prev = last;
        last = last.next;
    }
    //任意位置插入
    public void addIndex(int pos,int val) {
        ListNode node = new ListNode(val);
        if(pos == 0) {
            addFirst(val);
            return;
        }
        if(pos == size()) {
            addLast(val);
            return;
        }
        //寻找要插入节点的位置
        ListNode cur = searchIndex(pos);
        cur.prev.next = node;
        node.next = cur;
        node.prev = cur.prev;
        cur.prev = node;
    }
    public ListNode searchIndex(int pos) {
        ListNode cur = head;
        for(int i =0; i<pos; i++) {
            cur = cur.next;
        }
        return cur;
    }
    //查找关键字key是否包含在链表中
    public void contains(int key) {
        if(searchKey(key)) {
            System.out.println("找到了!!!");
        }else{
            System.out.println("链表中不包含此数据!!!");
        }
    }
    public boolean searchKey(int key) {
        ListNode cur = head;
        while(cur != null && cur.val != key) {
            cur = cur.next;
        }
        if(cur != null) return true;
        return false;
    }
    //删除第一次出现关键字为key的节点
    public void remove(int key) {
        ListNode cur = searchFirst(key);
        if(cur == null) {
            System.out.println("链表中不包含此数据!!!");
        }
        //情况一:头节点为key时
        if(cur == head) {
            //只有一个节点时
            if(head == last) {
                head = null;
                last = null;
            }else {
                //多个节点时
                head = head.next;
                head.prev = null;
            }
        }else if(cur == last) {
            //情况二:尾结点为key时
            last = last.prev;
            last.next = null;
        }else {
            cur.prev.next = cur.next;
            cur.next.prev = cur.prev;
        }

    }
    public ListNode searchFirst(int key) {
        ListNode cur = head;
        while(cur != null && cur.val != key) {
            cur = cur.next;
        }
        if(cur != null) {
            return cur;
        }
        return null;
    }
    //删除所有值为key的节点
    public void removeAll(int key) {

        ListNode cur = head;
        while(cur != null) {
            cur = searchFirst(key);
            if(cur == null) {
                return;
            }
            //为头节点时
            //情况一:头节点为key时
            if(cur == head) {
                //只有一个节点时
                if(head == last) {
                    head = null;
                    last = null;
                }else {
                    //多个节点时
                    head = head.next;
                    head.prev = null;
                }
            }else if(cur == last) {
                //情况二:尾结点为key时
                cur.prev.next = null;
                last = null;
            }else {
                cur.prev.next = cur.next;
                cur.next.prev = cur.prev;
            }
        }


    }
    //链表的长度
    public int size() {
        ListNode cur = head;
        int size = 0;
        while(cur != null) {
            size++;
            cur = cur.next;
        }
        return size;
    }
    //打印单链表
    public void display() {
        ListNode cur = head;
        while(cur != null) {
            System.out.print(cur.val+" ");
            cur = cur.next;
        }
    }
    //清空链表
    public void clear() {
        head = null;
        last = null;
    }
}

这里双链表与单链表的模拟非常相似,只要多多注意几种情况即可;

链表常用的方法

1、LinkedList的构造方法

方法 解释
LinkedList() 无参构造
public LinkedList(Collection< ? extends E > c) 使用其他集合容器中元素构造List
    public static void main(String[] args) {
        //使用无参构造方法
        List<Integer> list = new LinkedList<>();
        list.add(1);
        list.add(2);
        list.add(3);
        System.out.println(list);

        //使用list构造LinkedList
        List<Integer> list2 = new LinkedList<>(list);
        list2.add(4);
        System.out.println(list2);
    }

在这里插入图片描述

2、LinkedList的其他方法

方法 解释
boolean add(E e) 尾插 e
void add(int index, E element) 将 e 插入到 index位置
boolean addAll(Collection c) 尾插 c 中的元素
E remove(int index) 删除 index 位置的元素
boolean remove(Object o) 删除遇见的第一个o
E get(int index) 返回 index 位置的元素
E set(int index, E element) 将下标index 位置元素设置为 element
void clear() 清空链表
boolean contains(Object o) 判断 o 是否在链表中
int indexOf(Object o) 返回第一个 o 所在位置的下标
int lastIndexOf(Object o) 返回最后一个 o 位置的下标

以上方法与ArrayList中的使用都是一样的,这里就不再一一列举!

链表及顺序表的遍历

1、for循环的方式遍历

2、foreach的方式遍历

3、Iterator迭代器遍历

 public static void main(String[] args) {
       LinkedList<Integer> list = new LinkedList<>();
        list.add(1);
        list.add(2);
        list.add(3);
        list.add(4);
        //foreach方式遍历
        for(int x: list) {
            System.out.print(x+" ");
        }
        System.out.println();
        //Iterator迭代器遍历--正向遍历
        Iterator<Integer> it = list.iterator();
        while(it.hasNext()) {
            System.out.print(it.next()+" ");
        }
        System.out.println();
        //Iterator迭代器反向遍历
        //因为反向遍历是从后向前遍历,所以需要知道元素个数
        ListIterator<Integer> it2 = list.listIterator(list.size());
        while(it2.hasPrevious()) {
            System.out.print(it2.previous()+" ");
        }
        System.out.println();
    }

【数据结构】3000字剖析链表及双向链表_第22张图片

ArrayList和LinkedList的差异

1、数据结构实现:

​ ArrayList是动态数组的数据结构实现,而LinkedList是双向链表的数据结构实现;

2、随机访问效率:

​ ArrayList比LinkedList在随机访问的时候效率要高,因为LinkedList是线性的数据存储方式,所以需要移 动指针指针从前向后依次查找;

3、增加和删除效率:

​ 在非首尾的增加和删除操作,LinkedList要比ArrayList效率要高,因为ArrayList增删操作时,会导致其他下标的元素进行移动,时间复杂度高;

4、内存空间占用:

​ LinkedList比ArrayList更占用空间,因为LinkedList的节点处了存储数据,还存储了两个引用,一个指向前一个元素,一个指向后一个元素;

5、线程安全:

​ ArrayList 和 LinkedList都是不同步的,也就是不保证线程安全;

6、综合来说:

​ 在需要频繁读取聚合中的元素时,更推荐使用ArrayList,而在插入和删除操作较多时,推荐使用LinkedList;

**7、LinkedList的双向链表:**它的每一个节点中都有两个指针,分别指向前驱和后继,所以,从双向链表的任意一个节点开始,都可以很方便的访问它的前驱节点和后继节点;

你可能感兴趣的:(链表,数据结构)