数据存储——单向链表

    科技2022-08-11  103

    数据存储——单向链表

    所有的代码都存放在了gitee上,该知识点上所有的源代码见:源代码

    首先我们需要了解什么叫链表,通过百度百科的链表可以知道链表的定义:链表是一种物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。链表由一系列结点(链表中每一个元素称为结点)组成,结点可以在运行时动态生成。每个结点包括两个部分:一个是存储数据元素的数据域,另一个是存储下一个结点地址的指针域。 相比于线性表顺序结构,操作复杂。由于不必须按顺序存储,链表在插入的时候可以达到O(1)的复杂度,比另一种线性表顺序表快得多,但是查找一个节点或者访问特定编号的节点则需要O(n)的时间,而线性表和顺序表相应的时间复杂度分别是O(logn)和O(1)。

    从这段话我们可以知道的几个知识点是:

    非连续非顺序通过指针连接查找时间复杂度高插入时间复杂度低

    单向链表

    单链表源码位于(源代码/src/cn/tansanqinger/linked/SingleLinkedDemo.java)

    增加数据

    首先我们来看看单链表,所谓的单链表就是只能从头往尾读取数据,或者说叫只能在尾部添加数据,所有的数据都是连接成一条直线。

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0R2x1qhU-1601877874394)(C:\Users\C3H2\AppData\Roaming\Typora\typora-user-images\image-20201005095613183.png)]

    这就是两个结点,他们之间是通过地址来连接的,意思就是将下一个结点的内存地址存储到上一个结点上,这样上一个结点就可以通过结点来获取到下一个结点了,这就实现了多个数据之间的连接。但是需要注意的一点就是这些连接并不是连续的,因为每一个结点在开辟内存空间的时候都是随机的,因此他们并不是连续的。同时因为没有进行数据排序处理,所以顺序也无法展示。上述说的指针,可能有些朋友没有接触过C、C++,因此我用内存地址来描述可能会更加通俗易懂。

    那么这个如何来实现呢?

    首先来看看如何保存数据

    interface ISingleLink<T>{ void add(T data);//添加数据 } class SingleLinkImpl<T> implements ISingleLink<T>{ private Node root;//链表的起始地址 private Node end;//链表最后的位置 private int size;//链表的长度 public void add(T data){ Node node = new Node(data); if(this.root==null){ this.root = node;//将第一个结点交给链表起始头 this.end = node;//尾结点也为第一个 } else { this.end.node = node;//尾结点的Node存储下一个结点 this.end = node;//尾结点后移 } this.size++;//链表长度+1 } private class Node<T>{ private T data;//保存数据 private Node node;//存储下一个结点的地址 private Node(T data){ this.data = data; } } }

    那么接下来就是将数据读取出来,如何读取呢?首先我们需要通过root这个根结点,通过它去将所有的数据进行遍历,然后通过数组的方式将之返回。

    首先我们需要在ISingleLink接口中添加两个方法:

    interface ISingleLink<T>{ void add(T data);//添加数据 int size();//返回链表长度 T[] toArray();//返回数据 }

    最后实现的结果为:

    public int size() { return this.size;//返回链表长度 } public T[] toArray() { if (this.size == 0) {//没有数据 return null; } else { Object result[] = new Object[this.size];//开辟数组长度 Node list = root; int i = 0; while (list != null) {//循环遍历 result[i++] = list.data; list = list.node; } return (T[]) result;//返回结果 } }

    接下来就是来检验一下该链表是否可用

    public class SimgleLinkDemo{ public static void main(String[]args){ SingleLinkImpl<String> singleLink=new SingleLinkImpl<>(); singleLink.add("A"); singleLink.add("B"); singleLink.add("C"); System.out.println(singleLink.size()); System.out.println(Arrays.toString(singleLink.toArray())); } }

    执行结果为:[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KD3Oti22-1601877874407)(C:\Users\C3H2\AppData\Roaming\Typora\typora-user-images\image-20201005113159442.png)]

    删除数据

    一个完整的的链表应该有增删改查四项,刚刚完成了增,接下来看看删,首先还是来进行原理分析

    首先我们想要删除一个数据,我们首先需要做的就是找到它。但是我们需要注意的一个事情就是,由于我们使用的是链表,两个结点之间是通过地址关联起来的,如果我们想要删除一个链表那么应该找到它上一个结点,如果直接找要删除的结点的话,那就还要往上退一步。但是我们这个是单向链表,是没有办法通过一个结点来查找它的上一级结点的。我们找到了之后又要如何操作将这个数据删除呢?其实我们我们只需要知道如何添加数据的就可以完成这个问题。因为我们删除一个链表,相对于我们将链表中的一个结点去除,那么使用的方法无非就是上一个结点的地址存的是被删除结点的下一个结点的地址。通俗来说就是跳过了它。

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LxmDP7QW-1601877874414)(C:\Users\C3H2\AppData\Roaming\Typora\typora-user-images\image-20201005114425686.png)]

    知道了原理,那接下来就是实现它。首先在接口中添加T remove(int index)这个方法,该方法返回被删除的数据,当然也可以不返回。

    public T remove(int index) { if(this.size==0 || this.size<=index || index<0){//排除干扰 return null; } else if(index == 0){ Object data = this.root.data; this.root = this.root.node;//根结点后移 this.size--;//链表长度-1 return (T)data; } else { Node list = root;//获取链表 while(index>1){ //这里通过对index的判断来进行循环,减少变量 list = list.node;//指针后移 index --; } Node dataNode = list; //用于获取要删除的值 Object data = dataNode.node.data; list.node = list.node.node; this.size--; return (T)data; } } //测试 public static void main(String[]args){ SingleLinkImpl<String> singleLink=new SingleLinkImpl<>(); singleLink.add("A"); singleLink.add("B"); singleLink.add("C"); System.out.println(singleLink.remove(0)); System.out.println(singleLink.size()); System.out.println(Arrays.toString(singleLink.toArray())); }

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YvARtNZe-1601877874421)(C:\Users\C3H2\AppData\Roaming\Typora\typora-user-images\image-20201005121351211.png)]

    修改数据

    链表的修改和上述的操作类似,都是先找到要修改的数据,然后将数据进行替换,总体保持链表的结构不发生改变。现在我们先在接口中添加T set(int index, T data)方法。整体代码为:

    public T set(int index, T data) { if(this.size==0 || this.size<=index || index<0){//排除干扰 return null; } else { Node list = root;//获取链表 while(index>0){ list.node = list.node.node; index--; } Object dataNode = list.data; list.data = data; return (T)dataNode; } } //测试代码 public static void main(String[]args){ SingleLinkImpl<String> singleLink=new SingleLinkImpl<>(); singleLink.add("A"); singleLink.add("B"); singleLink.add("C"); System.out.println(singleLink.set(0,"D")); System.out.println(singleLink.size()); System.out.println(Arrays.toString(singleLink.toArray())); }

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uHehCrBH-1601877874424)(C:\Users\C3H2\AppData\Roaming\Typora\typora-user-images\image-20201005134454218.png)]

    查询数据

    查询数据再添加数据的时候就已经写出来了,但是那个是整体查询,现在要做的是单一数据的查询。首先还是再接口中添加T put(int index)方法。

    整体代码的实现和修改数据类似,为:

    public T put(int index) { if(this.size==0 || this.size<=index || index<0){//排除干扰 return null; } else { Node list = root;//获取链表 while(index>0){ list.node = list.node.node; index--; } return (T)list.data; } }

    总结

    到现在为止,单链表的增删改查已经全部写完了,通过代码的编写也可以发现,其实相比于数组来说,代码量更大,查询数据速度更慢,但是再删除数据的时候的速度是要比数组更快的,所以如何选择链表和数组就需要开发者自己考虑了。 如果对双向链表感兴趣的可以点击前往数据存储——双向链表 下面是我将代码优化后的实例,也可以直接到gitee上去下载源码。

    package cn.tansanqinger.linked; import java.util.Arrays; interface ISingleLink<T> { void add(T data);//添加数据 int size();//返回链表长度 T[] toArray();//返回数据 T remove(int index);//删除数据 T set(int index, T data);//修改数据 T put(int index);//查询数据 } class SingleLinkImpl<T> implements ISingleLink<T> { private Node root;//链表的起始地址 private Node end;//链表最后的位置 private int size;//链表的长度 public void add(T data) { Node node = new Node(data); if (this.root == null) { this.root = node;//将第一个结点交给链表起始头 this.end = node;//尾结点也为第一个 } else { this.end.node = node;//尾结点的Node存储下一个结点 this.end = node;//尾结点后移 } this.size++;//链表长度+1 } public int size() { return this.size;//返回链表长度 } public T[] toArray() { if (this.size == 0) {//没有数据 return null; } else { Object result[] = new Object[this.size];//开辟数组长度 Node list = root; int i = 0; while (list != null) {//循环遍历 result[i++] = list.data; list = list.node; } return (T[]) result;//返回结果 } } @Override public T remove(int index) { if(index == 0){ Object data = this.root.data; this.root = this.root.node;//根结点后移 this.size--;//链表长度-1 return (T)data; } else{ Node list = value(index-1); if(list!=null){ Node dataNode = list; //用于获取要删除的值 Object data = dataNode.node.data; list.node = list.node.node; this.size--; return (T)data; } } return null; } @Override public T set(int index, T data) { Node list = value(index); if(list!=null){ Object dataNode = list.data; list.data = data; return (T)dataNode; } return null; } @Override public T put(int index) { Node list = value(index); if (list!=null){ return (T)list.data; } return null; } private Node value(int index){ if(this.size==0 || this.size<=index || index<0){//排除干扰 return null; } else { Node list = root;//获取链表 while(index>0){ list.node = list.node.node; index--; } return list; } } private class Node<T> { private T data;//保存数据 private Node node;//存储下一个结点的地址 private Node(T data) { this.data = data; } } } public class SimgleLinkDemo{ public static void main(String[]args){ SingleLinkImpl<String> singleLink=new SingleLinkImpl<>(); singleLink.add("A"); singleLink.add("B"); singleLink.add("C"); System.out.println(singleLink.remove(1)); System.out.println(singleLink.size()); System.out.println(Arrays.toString(singleLink.toArray())); } }
    Processed: 0.009, SQL: 8