Bootstrap

链表(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();
    }

;