链表(Linked List)数据结构概览
链表(Linked List)是线性表的一种(线性表包含顺序表与链表),通过指针将一系列位于不连续的内存空间中的元素连接起来,链表结构可以充分利用计算机内存空间,实现灵活的内存动态管理,但也失去了快速随机存取的优点,同时增大了内存开销(存储指针域)。
链表数据结构由一连串节点(Node)组成,每个节点包含数据域(Data Fields)和一或两个用来指向上一个/或下一个节点位置的指针域(Pointer Fields)。链表可以方便地插入或移除表中任意位置的节点,但是随机存取效率不高。链表有很多种不同的类型:单向链表,双向链表以及循环链表。
链表(Linked List)数据结构操作接口
我们将要使用 Java 语言手写一枚链表数据结构。首先,明确链表数据结构所具有的操作接口方法。
接口方法 | 解释说明 |
---|---|
void addFirst(E element) | 向链表头部添加一个新的元素。 |
void addLast(E element) | 向链表尾部添加一个新的元素。 |
E removeFirst() | 移除链表头部第一个元素。 |
E removeLast() | 移除链表尾部最后一个元素。 |
E getFirst() | 返回链表头部第一个元素。 |
E getLast() | 返回链表尾部最后一个元素。 |
boolean contains(E element) | 检查链表中是否包含指定元素。 |
E insert(int index, E element) | 向链表指定索引位置插入新元素。 |
E get(int index) | 返回链表中指定索引的元素。 |
E set(int index, E element) | 为链表中指定索引的元素设新值。 |
E remove(int index) | 移除链表中指定索引的元素。 |
boolean remove(E element) | 移除链表中指定的元素。 |
int indexOf(E element) | 返回指定元素所在链表的索引,元素不存在则返回-1 ,若存在多个相同元素,则返回第一次出现的索引下标。 |
int size() | 返回链表存储元素数量。 |
boolean isEmpty() | 检查链表是否为空。 |
void clear() | 清空链表。 |
String toString() | 返回链表的字符串形式。 |
表:链表(Linked List)数据结构接口方法表
单向链表(Single Linked List)
我们使用 Java 语言实现一枚简单的单向链表。顾名思义,单向链表只能做单向遍历(头节点 -> 尾节点),因为单向链表的节点(Node)只包含数据域和一个指针域(指向下一个节点)。
我们来定义出单链表中节点(Node)的数据结构,使用泛型类提高节点存储数据的灵活性。节点数据结构包含构造方法,两个私有变量:数据域和指针域,及其对应的Getter/Setter
公开方法。
class Node<E> {
/**
* 数据域
*/
private E elem;
/**
* 指针域
*/
private Node<E> next;
/**
* 构造方法
*/
public Node(E element, Node<E> next) {
this.elem = element;
this.next = next;
}
/**
* 无参构造方法
*/
public Node() {
this(null, null);
}
/* Setter & Getter */
public void setElem(E element) {
this.elem = element;
}
public E getElem() {
return this.elem;
}
public void setNext(Node<E> next) {
this.next = next;
}
public Node<E> getNext() {
return this.next;
}
}
代码清单:单链表节点(Node)
我们考虑单链表数据结构:单链表包含一枚头节点(Head),头结点不存储数据,而是指向第一个实际存储数据的节点;尾节点可以被定义为指针域为null
的最后一枚节点。
单链表的构造方法即为单链表初始化:构造一枚数据域和指针域均为空的头节点。
public class SingleLinkedList<E> {
/**
* 链表头节点
*/
private Node<E> head;
/**
* 构造方法:创建空链表
* @param void
*/
public SingleLinkedList() {
this.head = new Node<E>();
}
}
代码清单:单链表(Single Linked List)结构
单链表addFirst()
方法向链表头部添加一个新的元素,插入的新元素总是位于链表头部(头节点指向的节点),这种插入元素的方式称为头插法,通过以下3步,即可完成向链表头部插入元素。
根据新元素构建一枚新节点
将新节点指针域置为头节点指向的节点
头节点指向新节点
...
/**
* 向链表头部添加一个新的元素(头插法)。
* @param element
* @return void
*/
public void addFirst(E element) {
Node<E> node = new Node<E>(element, null);
node.setNext(head.getNext());
head.setNext(node);
}
...
代码清单:单链表
addFirst()
头插法
单链表addLast()
方法向链表尾部添加一个新的元素,插入的新元素总是位于链表尾部(指针域为空的尾节点),这种插入元素的方式称为尾插法,通过以下4步,即可完成向链表尾部插入元素。
根据新元素构建一枚新节点
将新节点指针域置空
遍历链表找到尾节点(指针域为空的节点)
尾节点指向新节点
...
/**
* 向链表尾部添加一个新的元素(尾插法)。
* @param element
* @return void
*/
public void addLast(E element) {
Node<E> node = new Node<E>(element, null);
Node<E> tail = head;
while (tail.getNext() != null) {
tail = tail.getNext();
}
tail.setNext(node);
}
...
代码清单:单链表
addLast()
尾插法
理解了addFirst()
与addLast()
方法实现后,实现getFirst()
与getLast()
方法就非常简单了,返回头/尾节点数据域中存储的数据即可,但是需要考虑到链表为空的情况:直接抛出NoSuchElementException
异常。
...
/**
* 取得链表头部第一个元素,链表为空则抛出异常。
* @param void
* @return First element of {@code LinkedList}.
* @throws NoSuchElementException if this {@code LinkedList} is empty.
*/
public E getFirst() throws NoSuchElementException {
if (head.getNext() == null) {
throw new NoSuchElementException();
}
return head.getNext().getElem();
}
/**
* 取得链表尾部最后一个元素,链表为空则抛出异常。
* @param void
* @return Last element of {@code LinkedList}.
* @throws NoSuchElementException if this {@code LinkedList} is empty.
*/
public E getLast() throws NoSuchElementException {
Node<E> tail = head;
while (tail.getNext() != null) {
tail = tail.getNext();
}
if (tail == head) {
throw new NoSuchElementException();
}
return tail.getElem();
}
...
代码清单:单链表
getFirst()
与getLast()
方法实现
上述实现代码段其实已经涉及到了isEmpty()
与size()
接口方法,现在我们来实现这两个方法。
size()
:遍历链表元素并计数,计算链表存储元素数量。isEmpty()
:判断链表是否为空,可以借用size()
方法(链表存储元素数量为0
则表示链表为空),也可以直接判断头结点指针域是否为空。
...
/**
* 计算链表存储元素数量。
* @param void
* @return Size of elements in {@code LinkedList}.
*/
public int size() {
int cnt = 0;
for (Node<E> n = head; n.getNext() != null; n = n.getNext()) {
++cnt;
}
return cnt;
}
/**
* 检查链表是否为空。
* @param void
* @return Boolean {@code true} or {@code false}.
*/
public boolean isEmpty() {
return head.getNext() == null ? true : false;
}
/**
* 检查链表是否为空。
* @param void
* @return Boolean {@code true} or {@code false}.
*/
public boolean _isEmpty() {
return this.size() > 0 ? false : true;
}
...
代码清单:单链表
isEmpty()
与size()
方法实现
单链表removeFirst()
方法返回并移除链表第一个元素,通过以下步骤完成。
检查链表是否为空
获取链表首元素节点
头节点指向第二元素节点(首元素节点的下一个节点)
返回首元素节点数据域
...
/**
* 返回并移除链表头部第一个元素。
* @param void
* @return First element of this {@code Linked List}.
* @throws NoSuchElementException
*/
public E removeFirst() throws NoSuchElementException {
if (this.isEmpty()) {
throw new NoSuchElementException();
}
Node<E> first = head.getNext();
head.setNext(
first.getNext()
);
return first.getElem();
}
...
代码清单:单链表
removeFirst()
方法实现
单链表removeLast()
方法返回并移除链表最后一个元素,通过以下步骤完成。
检查链表是否为空
获取链表倒数第二元素节点(尾元素前一节点)
获取链表尾元素节点
将链表倒数第二元素节点指针域置空
返回尾元素节点数据域
...
/**
* 返回并移除链表尾部最后一个元素。
* @param void
* @return Last element of this {@code Linked List}.
* @throws NoSuchElementException
*/
public E removeLast() throws NoSuchElementException {
if (this.isEmpty()) {
throw new NoSuchElementException();
}
Node<E> prev = head;
while (prev.getNext().getNext() != null) {
prev = prev.getNext();
}
Node<E> last = prev.getNext();
prev.setNext(null);
return last.getElem();
}
...
代码清单:单链表
removeLast()
方法实现
我们来考虑单链表的contains(E e)
方法,检查链表中是否包含指定元素。我们使用equals()
比较方法判断两个元素是否相等,因此,存入链表的数据类型必须实现equals()
比较方法。
contains(E e)
方法具体实现为:遍历链表,比较每个元素,找到即返回true
,找不到则返回false
。
...
/**
* 检查链表中是否包含目标元素,
* 元素相等使用 {@code o.equals(obj)} 判断。
* @param element
* @return Boolean
*/
public boolean contains(E element) {
for (Node<E> current = head.getNext();
current != null;
current = current.getNext()) {
if (element.equals(current.getElem())) {
return true;
}
}
return false;
}
...
代码清单:单链表
contains()
方法实现
链表使用链式存储结构,存储的数据在内存空间中不连续,不能做到像数组一般高效的直接随即访问。我们来实现indexOf(E e)
方法,返回指定元素所在链表的索引,元素不存在则返回-1
,若存在多个相同元素,则返回第一次出现的索引。注意,我们将链表索引下标从0
计起,与数组保持一致。indexOf()
方法实现与contains()
方法相似,加入一枚索引下标计数器即可。
...
/**
* 返回指定元素所在链表的索引。
* @param element
* @return The index of element in {@code LinkedList},
* return {@code -1} if element does not found.
*/
public int indexOf(E element) {
int index = 0;
for (Node<E> current = head.getNext();
current != null;
current = current.getNext()) {
if (element.equals(current.getElem())) {
return index;
}
++index;
}
return -1;
}
...
代码清单:单链表
indexOf()
方法实现
我们使用get(int index)
方法获取链表中指定索引的元素,如果索引越界,抛出IndexOutOfBoundsException
异常。
...
/**
* 获取链表指定索引的元素。
* @param index
* @return element
* @throws IndexOutOfBoundsException
*/
public E get(int index) throws IndexOutOfBoundsException {
if (index < 0 || index >= size()) {
throw new IndexOutOfBoundsException();
}
Node<E> n = head.getNext();
while (index > 0) {
n = n.getNext();
--index;
}
return n.getElem();
}
...
代码清单:单链表
get()
方法实现
我们使用set(int index, E element)
方法为链表中指定索引位置的元素设新值,如果索引越界,抛出IndexOutOfBoundsException
异常。
...
/**
* 为链表指定索引位置的元素设新值。
* @param index
* @param element
* @return Previous element in the index.
* @throws IndexOutOfBoundsException
*/
public E set(int index, E element) throws IndexOutOfBoundsException {
if (index < 0 || index >= size()) {
throw new IndexOutOfBoundsException();
}
Node<E> n = head.getNext();
while (index > 0) {
n = n.getNext();
--index;
}
E oldValue = n.getElem();
n.setElem(element);
return oldValue;
}
...
代码清单:单链表
set()
方法实现
我们使用remove(int index)
方法移除链表中指定索引下标位置的元素,具体步骤如下。如果索引下标越界,则抛出IndexOutOfBoundsException
异常。
找到链表中指定索引下标的待移除节点及其前驱、后继节点
将指定索引下标节点的前后节点使用指针连接起来
返回移除节点数据域
...
/**
* 移除链表指定索引下标元素。
* @param index
* @return Removed element
* @throws IndexOutOfBoundsException
*/
public E remove(int index) throws IndexOutOfBoundsException {
if (index < 0 || index >= size()) {
throw new IndexOutOfBoundsException();
}
index -= 1;
Node<E> prev = head;
while (index >= 0) {
prev = prev.getNext();
--index;
}
Node<E> current = prev.getNext();
Node<E> next = current.getNext();
prev.setNext(next);
return current.getElem();
}
...
代码清单:单链表
remove(int index)
方法实现
移除元素remove()
方法还有另一种形式:boolean remove(E element)
,移除链表中的指定元素。我们可以使用indexOf(E element)
配合remove(int index)
实现,先获取指定元素在链表中的索引下标,再移除掉,操作成功返回true
,如果不存在目标元素,则返回false
。
...
/**
* 移除链表指定元素,
* 操作成功返回{@code true},不存在目标元素则返回{@code false}。
* @param element
* @return Boolean
*/
public boolean remove(E element) {
int index = indexOf(element);
return index == -1 ?
false : element.equals(remove(index));
}
...
代码清单:单链表
remove(E element)
方法实现
链表数据结构的优势在于其插入元素的开销比起数组要小很多,我们来实现链表插入元素insert()
方法,具体步骤如下所示。如果索引下标越界,则抛出IndexOutOfBoundsException
异常。
找到链表中指定索引下标节点(当前节点)及其前驱节点
创建一枚新节点
新节点指向当前节点
前驱节点指向新节点
返回当前节点数据域
...
/**
* 向列表指定位置插入一个新的元素。
* @param index
* @param element
* @return Previous element
* @throws IndexOutOfBoundsException
*/
public E insert(int index, E element)
throws IndexOutOfBoundsException {
if (index < 0 || index > size()) {
throw new IndexOutOfBoundsException();
}
index -= 1;
Node<E> prev = head;
while (index >= 0) {
prev = prev.getNext();
--index;
}
Node<E> current = prev.getNext();
Node<E> node = new Node<E>(element, null);
node.setNext(current);
prev.setNext(node);
return current == null ? null : current.getElem();
}
...
代码清单:单链表
insert()
方法实现
我们为链表提供一枚clear()
方法,用于清空链表元素。由于 Java 语言的自动垃圾回收机制,直接将头节点(Head)置空即可表示清空链表,不用担心内存泄露问题,但是为了帮助垃圾收集器更好地做内存回收工作,这里我们选择显式清空每一个节点。
...
/**
* 清空链表。
* @param void
* @return void
*/
public void clear() {
while (head != null) {
Node<E> n = head;
head = head.getNext();
n.setElem(null);
n.setNext(null);
}
head = new Node<E>();
}
...
代码清单:单链表
clear()
方法实现
最后,我们来覆写链表toString()
方法,更加方便地查看链表元素。
...
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append('[');
for (Node<E> current = head.getNext();
current != null;
current = current.getNext()) {
sb.append(current.getElem().toString());
sb.append(", ");
}
sb.append(']');
return sb.toString();
}
...
代码清单:单链表
toString()
方法实现
双向链表(Double Linked List)
双向链表(Double Linked List)可以做到双向遍历(头节点 <--> 尾节点),因为双向链表的节点(DLNode)包含数据域和两个指针域(前驱与后继)。
</div>