链表(LinkedList) 1

上期内容我们讲述了顺序表,知道了顺序表的底层是一段连续的空间进行存储(数组),在插入元素或者删除元素需要将顺序表中的元素整体移动,时间复杂度是O(n),效率比较低。因此,在Java的集合结构中又引入了链表来解决这一问题。

1、链表

链表是一种物理存储结构上非连续 存储结构,数据元素的 逻辑顺序 是通过链表中的 引用链接 次序实现的 。
下面通过图解来理解链表的结构:

如上图所示是单向链表节点的两个元素:其中value存储着节点的值,next存储着下一个节点的地址。因此,一个单向链表可以表示为下图所示:

注意 :

1、从上图可以看出,链式结构在逻辑上是连续的,但在物理上(即在计算机的内存里面)不一定连续。

2、 现实中的节点一般是从堆上申请出来的。

3、从堆上申请的空间,是按照一定策略来分配的,两次申请的空间可能连续,也可能不连续。

2、链表结构

链表组合起来的结构一共有8种,通过以下情况进行排列组合:

2.1 单向或者双向

单向

双向

双向链表在next域的基础上增加了prev域,使得通过链表的一个节点不仅能访问后继元素,也能访问前驱元素 。

2.2 带头或者不带头

带头

注意:这里的头并没有实际的值,主要用它链接后续的节点,因此,head指向第一个元素的地址。

不带头

2.3 循环或者非循环

循环

非循环

这里我们重点讲单向不带头非循环链表和双向不带头非循环链表。

3、无头单向非循环链表实现

3.1 节点的实现

链表的节点主要通过静态内部类进行实现。代码如下:

  static class ListNode{
        public int val;//节点的值域
        public ListNode next;//下一个节点的地址

        public ListNode() {
        }

        public ListNode(int val) {
            this.val = val;
        }
    }
   public ListNode head;//表示当前链表的头结点

这里可能有人会有疑问:为什么不把头结点的声明放入内部类中呢?

其实,从逻辑上想,不难想明白:头结点是属于整个链表的头结点,而非结点的头结点。

3.2 creatNode方法

本方法用于初始化一个链表。

   public void creatNode(){
        ListNode node1 = new ListNode(12);
        ListNode node2 = new ListNode(23);
        ListNode node3 = new ListNode(34);
        ListNode node4 = new ListNode(45);
        ListNode node5 = new ListNode(56);
        node1.next = node2;
        node2.next = node3;
        node3.next = node4;
        node4.next = node5;
        this.head = node1;
    }

3.3 头插法

故名思义:就是在链表的头部插入。这里有个点需要注意:先插入的数据会最后输出,后插入的最先输出。

在头部插入一个元素,我们需要先绑定元素后面的信息,再让我们的头结点指向我们要插入的元素。代码如下:

  //头插法
    public void addFirst(int data){
        ListNode node = new ListNode(data);
        //一般建议在插入的时候先绑定后面的节点信息
        node.next = head;
        this.head = node;
    }

3.4 尾插法

在尾部插入一个元素,需要先绑定前面的元素的信息,即让最后一个元素的next指向要插入的元素。

注意:如果链表中没有元素,则直接让头结点指向要插入的元素。

//尾插法
    public void addLast(int data){
        ListNode cur = head;
        ListNode node = new ListNode(data);
        if(cur == null){
            //如果链表中没有元素,那么让头结点指向node
            head = node;
            return;
        }
        //当cur.next == null的时候,那么该节点就是尾巴节点
        //当cur == null证明该链表已经被遍历完了
        while (cur.next != null){
            cur = cur.next;
        }
        cur.next = node;
    }

3.5 任意位置插入,第一个数据节点为0号下标

在任意位置插入,首先,我们要判定下标是否合法,若不合法则抛出异常。第二步,如果插入的下标为0,则直接调用前面写的头插法函数,如果下标等于链表的长度,则调用尾插法函数。第三步,如果插入的地方是在中间,则需要先找到要插入结点的前一个结点,绑定后面的信息后再进行插入。

//任意位置插入,第一个数据节点为0号下标
    public void addIndex(int index,int data){
        if(index < 0 || index > size()){
            throw new IndexErrorException("下标不合法");
        }
        if(index == 0){
            addFirst(data);
            return;
        }
        else if (index == size()){
            addLast(data);
            return;
        }
        //1、定义cur走index-1步
        //2、进行插入
        ListNode node = new ListNode(data);
        ListNode cur = head;
        int count = 0;
        while(count != index-1){
            cur = cur.next;
            count++;
        }
        node.next = cur.next;
        cur.next = node;
    }

3.6 查找关键字key是否在单链表当中

查找关键字key只需要遍历链表,看结点的value是否等于key就ok了。

 //查找是否包含关键字key是否在单链表当中
    public boolean contains(int key){
        ListNode cur = head;
        while (cur != null){
            if(cur.val == key){
                return true;
            }
            cur = cur.next;
        }
        return false;
    }

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

思路:删除一个结点(del),我们需要记录被删除结点的前一个结点(cur),再让前一个结点的next域(cur.next)指向被删除结点的下一个结点(del.next)。需要注意的是:在这个过程之前需要单独删除头结点。

//删除第一次出现关键字为key的节点
 public void remove(int key){
        if(head == null){
            return;
        }
        //单独删除头结点
        if(head.val == key){
            head = head.next;
            return;
        }
        //找到删除节点的前一个结点
        ListNode cur = head;
        while (cur.next != null)
        {
            ListNode del = cur.next;
            if(del.val == key){//此时cur指向的节点就是要删除节点的前一个节点
                    //cur.next = cur.next.next
                cur.next = del.next;
                return;//删除之后返回
            }
            cur = cur.next;
        }
        if(cur==null){
            System.out.println("没有你要删除的数字");
        }
    }

3.8 删除所有值为key的节点

删除所有值为key的结点需要用到双指针的思想(ps:因为需要不断地记录被删除结点的前一个结点), prev指针用来找到被删除元素的前一个元素,cur指针则用来遍历链表和找到被删除元素。删除的过程就是直接让prev的next域直接指向cur指针的next域,再让cur指针向后走;若不是被删除的元素则直接让两个指针一起往后走。最后,我们需要单独删除头结点。(注意:这里我们在最后才删除头结点是因为前面的cur和prev指针需要使用到头结点)。

  //删除所有值为key的节点 
public void removeAllKey(int key){
        if(head == null){
            return;
        }
        ListNode prev = head;
        ListNode cur = head.next;
        while (cur != null){
            if(cur.val == key){
                prev.next = cur.next;
                cur = cur.next;
            }else{
                prev = cur;
                cur = cur.next;
            }
        }
        //单独删除头结点
        if(head.val == key){
            head = head.next;
        }
    }

3.9 得到单链表长度

得到单链表的长度,我们需要定义一个计数器,然后遍历链表让count++就行了。

 //得到单链表的长度
    public int size(){
        int count = 0;
        ListNode cur = head;
        while(cur != null){
            count++;
            cur = cur.next;
        }
        return count;
    }

3.10 清空链表

遍历链表将链表的每个结点置空即可。

//清空链表  
public void clear() {
        ListNode cur = head;
        while (cur != null){
            cur = null;
            cur = cur.next;
        }
    }

3.11 打印链表

遍历链表,打印每个结点的value域即可。

//打印链表
    public void display() {
        ListNode cur = head;
        while (cur!= null){//如果cur==null证明把链表遍历完成了
            System.out.print(cur.val + " ");
            cur = cur.next;//cur每次都在向后走一步
}
        System.out.println();
    }
相关推荐
style-h2 小时前
数据结构 算法时间复杂度和空间复杂度
数据结构
南宫生3 小时前
力扣动态规划-26【算法学习day.120】
java·数据结构·算法·leetcode·动态规划
surtr13 小时前
【C++】RBTree(红黑树)模拟实现
数据结构·c++·算法·stl·map·红黑树·rbtree
Dovis(誓平步青云)6 小时前
【数据结构】双向链表(真正的零基础)
数据结构·链表
曙曙学编程9 小时前
进阶数据结构——二叉堆
数据结构
黑夜无路人10 小时前
redis底层数据结构——整数集合
数据结构·redis
Zz_waiting.12 小时前
java数据结构_二叉树_5.4
数据结构·算法
亲爱的老吉先森14 小时前
常见数据结构的C语言定义---《数据结构C语言版》
c语言·开发语言·数据结构
敲上瘾18 小时前
DFS+回溯+剪枝(深度优先搜索)——搜索算法
数据结构·c++·算法·回归·深度优先·剪枝·回归算法
纪伊路上盛名在19 小时前
scRNA-seq scanpy教程1:准备工作+AnnData数据结构理解
数据结构·python·可视化·生物学·单细胞·scanpy