链表,和数组一样,也是一种线性的数据结构。但链表在存储数据的时候,却不像数组把所有的数据都存储在一片连续的内存空间中,而是数据分散在内存中,数据和数据之间相互链接。数据和数据怎么才能相互链接?比如,5和10怎么才能进行链接?很显然,仅仅依靠数据本身是无法链接起来的,还需要地址。存储数据的时候,同时存储一个地址,当再存储一个数据的时候,把该数据的地址赋值前一个数据中的地址,只要知道地址,就能找到元素,也就相当于,数据和数据链接起来了,所以链表在存储数据的时候,不仅要存数据本身,还要存地址。也正因为如此,链表存储的是一个对象。对象的一个属性是数据,一个属性是地址。链表是对象与对象之间的链接。但地址怎么获取呢?只要链表存储的是对象,地址就很好获取了。在Java 和JavaScript中,当创建一个对象,并把这个对象赋给一个变量的时候,这个变量保存的是就这个对象的地址。当把这个变量复制给另外一个变量的时候,复制的是对象的地址,也就是说,两个变量同时指向了一个对象。也就是说,链表中的每一个对象中,它的地址属性只要声明成对象类型的变量就可以了。链表是对象与对象的链接,这也引出了另外一个问题,链表是存储在堆内存中的(因为对象都是存储在堆内存中的),怎么才能操作它呢?只能把链表中的第一个对象赋值给一个变量,操作链表的时候,通过这个变量去找到链表中的第一个对象,进而找到整个链表进行操作。也就是说,链表中始终都要存在一个指向链表第一个对象的变量,用来找到链表,进而操作链表。链表中的每一个对象也称为节点, 指向链表第一个对象的变量称为链表头。一个简单的链表如下:

链表(1) --- 单链表_插入节点

  链表的分类有三种,单向链表(单链表),双向链表,循环链表。

  单向链表:链接方向是单向的,一个节点只能链接它的后一个节点,而不能链接它的前一个节点,链表的最后一个节点链向null。对链表的访问只能从头部开始,依次向后顺序访问,像上图一样。

  双向链表,链接方向是双向的,就是它的每一个节点中都有两个地址(假设是prev和next),prev指向它的前一个节点, next指向后一个节点,第一个节点的prev提向null,最后一个节点next指向null

 链表(1) --- 单链表_链表_02 

  循环链表就是,链表的最后一个元素不是指向null,而是指向第一个结点。不管是单向链表还是双向链表,它都能变成循环列表

 链表(1) --- 单链表_插入节点_03  

 链表的实现方式也有两种方式,一种是不带头结点,链表头指向的第一个元素就是真实的数据节点

链表(1) --- 单链表_单向链表_04

  一种是带一个头节点,头节点并不保存任何有效数据,链表头指向头节点,头节点指向链表中真正有意义的第一个节点。

链表(1) --- 单链表_单向链表_05

   先看单向链表,由于链表中的每一个节点都是对象,所以要声明一个类来表示这个对象,由于这个类仅在单向链表中使用,可以声明为私有内部类,类的属性分为数据和地址,为了简单起见,数据使用整数,地址就是节点类的变量。为了能够操作链表,还需要一个变量,让它指向链表中的第一个节点,所以一个单向链表要有两个属性,一个是内部节点类,一个是指向链表的第一个元素。有时还要计算链表中节点的个数,再加上一个属性size

public class SingleLinkedList {

    private class Node { // 链表的节点类型
        int data; // 数据
        Node next; // 地址

        Node(int data){
            this.data = data;
        }
    }

    private Node head; // 指向链表的第一个元素,用来找到链表,操作链表
    private int size; // 记录节点的个数
}

  可以想到,这样的实现方式是不带头节点的,因为head默认初始化为null,它并没有指向一个节点。

  插入节点,可以从链表的头部插入,可以从链表的尾部插入,还可以从链表的中间插入,还可以在某个节点后面插入,相应的,你可以单独提供方法,也可以提供通用的方法。头部插入最为简单,就是创建新的节点,让新的节点next指向head(把head赋值给新节点的next属性),然后再让head指向新的接点(把新节点赋值给head),最后不要忘记size++;链表(1) --- 单链表_插入节点_06

public void insertFirst(int data){
    // 创建新的节点
    Node newNode = new Node(data);// 新的节点的next指向head
    newNode.next = head;
    // 让head指向新的节点
    head = newNode;

size++; }

  从尾部插入节点,就有点麻烦了,因为要先找到尾节点,然后再让尾节点的next 指向新的节点。怎样才能找到尾节点?尾节点的next 是null,只要找到一个节点的next 属性是null, 它就是尾节点。如果head.next 是null,则head是尾节点,如果head.next.next 是null,那head.next 就是尾节点。找一个临时变量temp, 把head赋值给它, 那么temp 也就是指向了第一个节点,判断temp.next 是不是null, 如果是,它就是尾节点,如果不是,那就把temp.next 再赋值给temp, 这时temp就指向第二个节点。如果temp.next 还不是null, 那就再把temp.next 赋值给temp, 这时temp指向第三个节点。 可以发现,就是不停地把temp.next 赋值给temp,让temp 指向下一个节点,直到temp.next 是null,那temp 就是最后一个节点,这时把新的节点赋值给temp.next。赋值成功后,不要忘记size++。尾部插入节点要注意的是,先判断head是否为null,如果head是null,head赋值给temp,temp.next就会报错。head为null,链表为空,也没有必要循环链表。

 链表(1) --- 单链表_链表_07

public void insertLast(int data) {
    // 新node的next默认是null
    Node newNode = new Node(data);

    if (head == null){
        head = newNode;
    } else{
        Node temp = head;
        // 只有node的next的节点是null, 才是最后一个节点。
        while (temp.next != null) {
            temp = temp.next;
        }
        // temp是最后一个节点
        temp.next = newNode;
    }
size++; }

  在链表的中间位置或任意位置插入节点,那就要先定位到这个位置,找到这个位置的前一个节点,因为单链表只能链接到下一个节点,要在某个位置插入节点,肯定要让这个位置之前的那个节点的next指向新的节点,同时让新的节点的next指向原来前一个节点指向的后一个节点。定位位置,找到前一个节点,肯定是要循环遍历了,遍历到指定的位置。插入到第n个位置,遍历n-1次,得到的节点,就是前一个节点。

链表(1) --- 单链表_单向链表_08

  理论上是这样,但实现上还要复杂,因为有好多情况要判断,链表是否为空,如果为空,那就只能插入第一个节点。插入的位置是否符合要求,这里要确定一下,链表的第 一个节点是位置0还是位置1,如果按照数组的规则,链表中的第一个节点就是位置0。那么要插入的位置范围就是[0, size]. 如果链表中只有一个节点,是可以插入到位置1的,位置0的节点next指向这个节点就可以了。

public void insert(int position, int data) {
        // 创建新节点
        Node newNode = new Node(data);

        // 判断链表是否为空
        if(head == null) {
            // 如果为空,那只允许插入到第一个节点,也就是位置0处,否则报错
            if (position != 0){
                throw new RuntimeException("链表为空");
            } else {
                head = newNode;
            }
        } else { // 至少有一个节点
            // 判断位置是否符合要求
            if(position < 0 || position > size) {
                throw new RuntimeException("超出范围");
            } else {
                // 插入头节点
                if(position == 0) {
                    insertFirst(data);
                } else if (position == size) { // 插入尾节点
                    insertLast(data);
                } else {
                    Node prevNode = head;
                    // 注意是position -1,如果插入到1位置,那head就是前一个节点,无需循环,position-1 -> 0, 不循环
                    for (int i = 0; i < position -1; i++) {
                        prevNode = prevNode.next;
                    }
                    // prevNode就是前一个节点。
                    // nextNode是后一 个节点
                    Node nextNode = prevNode.next;
                    // 新节点的链接:前一个节点的next指向新节点,新节点的next指向后一个节点
                    prevNode.next = newNode;
                    newNode.next = nextNode;
                }
            }
        }
     size++; }

  插入功能完成后,就要验证实现的对不对,简单的办法就是把链表中的数据打印出来,遍历整个链表。找一个临时变量curNode, 把head赋值给它, 那么curNode也就指向了第一个节点,此时判断curNode是不是null, 如果是,链表为空,什么都不用做了。如果不是,把curNode的data打印出来,再把curNode.next 赋值给curNode, 这时curNode就指向第二个节点。再判断curNode是不是null, 如果不是,继续打印它的data,把curNode.next 赋值给curNode, 这时curNode指向第三个节点。 可以发现,就是不停地判断curNode是否为null, 如果不是,那就打印data,并curNode.next 赋值给curNode,让curNode指向下一个节点,直到curNode 为null。这里要注意它和插入尾节点遍历的不同,插入尾节点,是判断.next是否为null,这里只判断节点是否为null

链表(1) --- 单链表_数据_09

public void display() {
        Node curNode = head;

        while (curNode != null) {
            System.out.print(curNode.data);
            curNode = curNode.next;
        }
    }

  删除节点,删除节点也有很多情况,删除头节点,删除某个位置节点,删除某个指定的节点。删除头节点比较简单,直接让head指向head.next 就可以了,删除方法通常会返回要删除的节点

链表(1) --- 单链表_插入节点_10

 

public Node deleteFirst() {
    if (head == null) {
        throw new RuntimeException("链表为空");
    }
    Node firstDeleted = head;
    head = head.next;
    return firstDeleted;
}

  删除某个指定的节点,分为三种情况,这个节点正好是头节点,这个节点在中间或尾节点,这个节点没有找到。当删除某个节点时,不仅要找到这个节点,还要找到这个节点的上一个节点,还是因为链表是单向的,删除某个节点,就要让某个节点的上一个节点的next指向这个节点的next指向的下一个节点,那遍历查找某个节点时,还要记录前一个节点。

链表(1) --- 单链表_单向链表_11

public Node deleteKey(int key){
    if (head == null) {
        throw new RuntimeException("链表为空");
    }

    Node curNode = head;
    Node prevNode = null;

    // 删除的某个key正好位于头节点中
    if(curNode.data == key){
        head = curNode.next;
        return curNode;
    }

    // 删除的某个key位于中间节点或尾节点
    while (curNode != null && curNode.data != key) {
        prevNode = curNode; //记录前一个节点
        curNode = curNode.next;
    }

    // 如果找到了
    if(curNode != null){
        prevNode.next = curNode.next;
        return curNode;
    }

    // 如果没有找 curNode == null
    System.out.println("没有找到");
    return null;
}

  删除某个位置的节点,要先找到这个节点,同时记录上一个节点。怎么才能找到这个节点呢?遍历到该位置。

public Node deleteByPostion(int postion) {
    if (head == null) {
        throw new RuntimeException("链表为空");
    }
    
    if(postion < 0 || postion >= size) {
        throw new RuntimeException("位置不对");
    }
    
    if(postion == 0){
        Node first = head;
        head = head.next;
        return first;
    }

    Node curNode = head;
    Node prevNode = null;
    for (int i = 0; i < postion; i++) {
        prevNode = curNode;
        curNode = curNode.next;
    }
    
    prevNode.next = curNode.next;
    
    return; curNode;
}

  查找

public int find(int data){
    Node curNode = head;
    int index = 0;
    boolean isFind = false;

    while (curNode != null){
        if(curNode.data == data){
            isFind = true;
            break;
        } else {
            index++;
            curNode = curNode.next;
        }
    }

    if(isFind) {
        return index;
    } else {
        return -1;
    }
}