链表数据结构
在计算机科学中,链表是数据元素的线性集合,元素的线性顺序不是由它们在内存中的物理地址给出的。它是由一组节点组成的数据结构,每个元素指向下一个元素,这些节点一起,表示线性序列。
在最简单的链表结构下,每个节点由数据和指针(存放指向下一个节点的指针)两部分组成,这种数据结构允许在迭代时有效地从序列中的任何位置插入或删除元素。
链表的数据结构通过链的连接方式,提供了可以不需要扩容空间就更高效的插入和删除元素的操作,在适合的场景下它是一种非常方便的数据结构。但在一些需要遍历、指定位置操作、或者访问任意元素下,是需要循环遍历的,这将导致时间复杂度的提升。
链表分类
链表的主要表现形式分为;单向链表、双向链表、循环链表,接下来我们分别介绍下。
1. 单向链表
单链表包含具有数据字段的节点以及指向节点行中的下一个节点的 “下一个” 字段。可以对单链表执行的操作包括插入、删除和遍历。
2. 双向链表
在 “双向链表” 中,除了下一个节点链接之外,每个节点还包含指向序列中 “前一个” 节点的第二个链接字段。这两个链接可以称为 'forward('s')和 'backwards',或 'next' 和 'prev'('previous')。
3. 循环链表
在列表的最后一个节点中,链接字段通常包含一个空引用,一个特殊的值用于指示缺少进一步的节点。一个不太常见的约定是让它指向列表的第一个节点。在这种情况下,列表被称为 “循环” 或 “循环链接”;否则,它被称为 “开放” 或 “线性”。它是一个列表,其中最后一个指针指向第一个节点。
实现一个链表
像 Java 的源码中本身就提供了非常多的数据结构,包括我们所学习的链表 LinkedList 在日常的开发使用中也是非常频繁。所以我们在学习的过程中,以使用 Java 程序员本身常用的语言来分析学习,并通过简化结构的方式把 LinkedList 手写实现,让大家更能方便的理解链表。
1. 链表节点
private static class Node<E> { E item; Node<E> next; Node<E> prev; public Node(Node<E> prev, E element, Node<E> next) { this.item = element; this.next = next; this.prev = prev; } }
链表的数据结构核心根基就在于节点对象的使用,并在节点对象中关联当前节点的上一个和下一个节点。通过这样的方式构建出链表结构。但也因为在链表上添加每个元素的时候,都需要创建新的 Node 节点,所以这也是一部分耗时的操作。
2. 头插节点
void linkFirst(E e) { final Node<E> f = first; final Node<E> newNode = new Node<>(null, e, f); first = newNode; if (f == null) last = newNode; else f.prev = newNode; size++; }
头插的操作流程,先把头节点记录下来。之后创建一个新的节点,新的节点构造函数的头节点入参为 null,通过这样的方式构建出一个新的头节点。原来的头结点,设置 f.prev 连接到新的头节点,这样的就可以完成头插的操作了。另外如果原来就没有头节点,设置设置为新的节点即可。最后记录当前链表中节点的数量,也就是你使用 LinkedList 获取 size 时候就是从这个值获取的。
3. 尾插节点
void linkLast(E e) { final Node<E> l = last; final Node<E> newNode = new Node<>(l, e, null); last = newNode; if (l == null) { first = newNode; } else { l.next = newNode; } size++; }
尾差节点与头插节点正好相反,通过记录当前的结尾节点,创建新的节点,并把当前的结尾节点,通过 l.next 关联到新创建的节点上。同时记录 size 节点数量值。
4. 拆链操作
E unlink(Node<E> x) { final E element = x.item; final Node<E> next = x.next; final Node<E> prev = x.prev; if (prev == null) { first = next; } else { prev.next = next; x.prev = null; } if (next == null) { last = prev; } else { next.prev = prev; x.next = null; } x.item = null; size--; return element; }
unlink 是一种拆链操作,只要你给定一个元素,它就可以把当前这个元素的上一个节点和一个节点进行相连,之后把自己拆除。这个方法常用语 remove 移除元素操作,因为整个操作过程不需要遍历,拆除元素后也不需要复制新的空间。
5. 删除节点
public boolean remove(Object o) { if (o == null) { for (Node<E> x = first; x != null; x = x.next) { if (x.item == null) { unlink(x); return true; } } } else { for (Node<E> x = first; x != null; x = x.next) { if (o.equals(x.item)) { unlink(x); return true; } } } return false; }
删除元素的过程需要 for 循环判断比删除元素的值,找到对应的元素,进行删除。循环比对的过程是一个 O (n) 的操作,删除的过程是一个 O (1) 的操作。所以如果这个链表较大,删除的元素又都是贴近结尾,那么这个循环比对的过程也是比较耗时的。