看了也不会系列-链表

153 阅读5分钟

数组

通过使用数组,可以存储一连串的数据

在java中,可以存储所有的基本类型以及对象类型。

但是如果在代码中,需要频繁对数组中的元素进行添加和删除,特别是从数组的头部进行添加和删除,就会有如下的问题:

  • 从头部添加时需要把所有的元素往后移,以腾出第一个位置插入新元素
  • 从头部删除时,需要把所有的元素往前移。
//初始化一个数组
int[] a=new int[10];
a[0]=0;
a[1]=1;
a[2]=2;
//为了方便,使用currentSize来表示数组实际使用了多少
index=3;

//从头部插入一个元素-1,会使用如下方法
if(currentSize<10){
	for(int i=currentSize;i>0;i--){
		a[i]=a[i-1];
	}
  a[0]=-1;
}
//以上操作就是先把原先存在的数据往后移动,然后再插入头部

//当从头部删除一个元素时,使用方法如下
if(currentSize>0){
  for(int i=0;i<currentSize;i++){
    a[i]=a[i+1];
  }
}
//以上操作就是把后面的元素往前移动

从代码可以看出,对数组的插入和删除操作麻烦,如果数组的容量非常大的话,那么循环次数将非常多。

还有一个问题就是,当数组已存储的元素个数达到初始化的容量,若要继续添加元素,就需要对数组进行扩容。如下代码

int[] a=new int[2];
a[0]=0;
a[1]=1;
//此时数组已经达到最大数量,需要进行扩容。
int[] b=new int[a.length*2];
for(int i=0;i<a.length;i++){
  b[i]=a[i];
}
a=b;
//此时数组a的容量就扩大到原来的两倍

链表

为了避免使用数组带来的插入和删除的开销,需要保证元素可以不在内存地址上进行连续存储,否则添加或删除一个元素就可能需要整体移动。这是我们就以使用链表来存储数据。

结构

链表由一系列节点组成,这些节点在内存中不一定相连。在最简单的链表中,每一个节点均包含数据元素和该数据元素的后继的指针,将其称之为next。最后一个元素的next指向null。

如图:

代码实现:

//链表节点实现
class Node {
    //使用Object类型,让节点能够保存任意类型的数据
    private Object data;
    private Node next;

    public Node(Object data) {
        this.data = data;
    }
  
    public Object getData() {
        return data;
    }

    public void setData(Object data) {
        this.data = data;
    }

    public Node getNext() {
        return next;
    }

    public void setNext(Node next) {
        this.next = next;
    }
}

//链表使用
public class Test {
    public static void main(String[] args) {
        Node node1 = new Node(1);
        Node node2 = new Node(2);
        Node node3 = new Node(3);
        node1.setNext(node2);
        node2.setNext(node3);
        node3.setNext(null);
      //这样的链表就和数组 int[] a=new int[]{1,2,3}差不多,由于node3的后继不存在,所以node3指向null
    }
}

我们可以通过intellij编辑器的断点进行查看:

关系就是node1->node2->node3->null

一般的,我们会为链表设置一个表头,它不存储数据,但是指向第一个元素

package list;

class Node {
    //使用Object类型,让节点能够保存任意类型的数据
    private Object data;
    private Node next;

    public Node() {
    }

    public Node(Object data) {
        this.data = data;
    }

    public Object getData() {
        return data;
    }

    public void setData(Object data) {
        this.data = data;
    }

    public Node getNext() {
        return next;
    }

    public void setNext(Node next) {
        this.next = next;
    }
}

public class Test {
    public static void main(String[] args) {
    		//声明表头
        Node head=new Node();
        Node node1 = new Node(1);
        Node node2 = new Node(2);
        Node node3 = new Node(3);
        //直接指向node1,里面没有存储数据
        head.setNext(node1);
        node1.setNext(node2);
        node2.setNext(node3);
        node3.setNext(null);
    }
}

链表的插入和删除

插入

链表的插入很简单,比如要想n节点插入到s节点的后面,首先对链表进行遍历,找到s节点后,先让n.next=s.next,然后让s.next=n

如图,插入新节点A4,就是先让A4指向A3,再让A2指向A4

public class Test {
    public static void main(String[] args) {
        Node head = new Node();
        Node node1 = new Node(1);
        Node node2 = new Node(2);
        Node node3 = new Node(3);
        head.setNext(node1);
        node1.setNext(node2);
        node2.setNext(node3);
        node3.setNext(null);
        //声明一个node4
        Node node4 = new Node(4);
        //要想得到某个指定的节点,就得对链表进行遍历,如果到了某个节点,其next指向为null,说明整个链表都遍历完毕,不存在指定的节点元素,所有可以使用while循环进行遍历
        Node node = head;
        while ((node = node.getNext()) != null) {
            //等于2的时候,就是找到了节点node2
            if (node.getData().equals(2)) {
                //先让新节点node4指向node2的下一节点,也就是node3
                node4.setNext(node2.getNext());
                //然后让node2指向新节点node4
                node2.setNext(node4);
                //跳出循环
                break;
            }
        }
    }
}

使用debug工具进行查看:

删除

删除和插入类似,比如要想删除节点n,首先对链表进行遍历,找到s节点,满足s.next=n,然后让s.next=n.next ,n.ndex=null,节点n会由于虚拟机的垃圾回收机制被自动回收

public class Test {
    public static void main(String[] args) {
        Node head = new Node();
        Node node1 = new Node(1);
        Node node2 = new Node(2);
        Node node3 = new Node(3);
        head.setNext(node1);
        node1.setNext(node2);
        node2.setNext(node3);
        node3.setNext(null);
        //要想得到某个指定的节点,就得对链表进行遍历,如果到了某个节点,其next指向为null,说明整个链表都遍历完毕,不存在指定的节点元素,所有可以使用while循环进行遍历
        Node node = head;
        while ((node = node.getNext()) != null) {
            //节点的next.data等于2的时候,就找到了需要被删除节点的前驱
            if (node.getNext().getData().equals(2)) {
                Node deleteNode = node.getNext();
                //先让前驱指向后继
                node.setNext(deleteNode.getNext());
                //然后把删除节点的后继指向null
                deleteNode.setNext(null);
                //跳出循环
                break;
            }
        }
    }
}

debug查看

可以看到,该链表中不再包含2

总结

总的来说,数组和链表使用频率都大,下面总结下各自的优点和缺点

数组

优点

查询快,因为有下标

缺点
  • 插入和删除需要移动大量元素
  • 容量固定,当容量不够的时候,需要进行扩容

链表

优点
  • 容量大,其大小和内存有关,因为节点都是不连续的
  • 插入和删除方便

缺点

因为链表中的节点不是连续的,所以想要获取某个元素的时候需要对链表进行遍历和判断

使用

需要经常添加和删除元素时,用链表。元素不会经常变动,或者查询频率较高,用数组