这是系列文章,每篇文章末尾均附有源代码地址。目的是通过模拟集合框架的简单实现,从而对常用的数据结构和java集合有个大概的了解。当然实现没有java集合的实现那么复杂,功能也没有那么强大,但是可以通过这些简单的实现窥探到底层的一些共性原理。

一. 什么叫带头结点的单链表?

带头结点的单链表和普通的单链表差不多,只是在普通单链表的第一个结点之前增加一个特殊的结点,这个结点就叫做头结点。在带头结点的单链表中,head指向头结点。虽然头结点不是用来存储数据的,但是会占用一个存储单元,相当于牺牲一个结点的存储空间来简化单链表的操作。由于增加了头结点,使得所有单链表(包括空表)的头结点均非空,所以对单链表的插入,删除操作不需要区分该单链表是否为空表或是在第一个位置进行,能与在其他位置的插入,删除操作保持一致,这就是带头结点单链表带来的方便之处如下所示:

(a) 空链表

java多节点部署方案 java结点类_java带头结点的单链表

(b) 头插入,不改变head,不用区分插入位置,头结点后面插入元素C如下图所示:

java多节点部署方案 java结点类_java带头结点的单链表_02

(c) 头删除,不改变head,不用区分删除位置,删除元素c如下图所示

java多节点部署方案 java结点类_结点_03

二. 带头结点单链表的实现

1.定义带头结点的单链表类

带头结点的单链表类HeadSinglyLinkedList声明如下,实现方式和普通单链表SinglyLinkedList差别不大,也使用单链表节点类Node。

package org.light4j.dataStructure.linearList.linkList.head;
import org.light4j.dataStructure.linearList.LList;
import org.light4j.dataStructure.linearList.linkList.Node;
/**
* 带头结点的单链表类
*
* @author longjiazuo
*/
public class HeadSinglyLinkedList implements LList {
protected Node head;// 单链表的头结点,指向单链表的头结点
protected Node rear;// 单链表的尾结点,指向单链表的最后一个结点
protected int n;// 单链表的长度
public HeadSinglyLinkedList() {// 构造空单链表
this.head = new Node(null);// 构造头结点,元素值为空
this.rear = this.head;// 构造尾结点,初始化的时候头结点和尾结点都指向头结点
this.n = 0;// 初始化链表长度为0
}
}

代码解释:

① 单链表HeadSinglyLinkedList类实现接口LList,所以必须实现该接口的相关方法,和普通单链表的方法类似的方法下面不再列举,请查看前面的单链表的实现的文章,下面会对二者不一样的方法做些说明。

② 在构造函数里面指定了头结点和尾结点,头结点的数据为空,初始化的时候,头结点和尾结点都指向头结点。

3. 判断带头结点单链表是否为空 /**

* 判断带头结点的单链表是否为空
*/
@Override
public boolean isEmpty() {
return this.head.next == null;
}

代码解释:

① 由于增加了头结点,所以判断带头结点单链表是否为空的条件是根据头结点的下一个结点是否为空作为依据。

4. 求带头结点单链表的长度 /**

* 返回带头结点的单链表长度,时间复杂度为O(1)
*/
@Override
public int length() {
return this.n;
}

代码解释:

① 由于增加了表示单链表长度的成员变量n,所以获取单链表长度直接返回n即可,操作的时间复杂度为O(1)。

5. 带头结点单链表的插入 /**

* 在指定位置插入非空的指针对象
*/
@Override
public boolean add(int index, E element) {
if (element == null) {// 不允许插入非空元素
return false;
}
if (index >= this.n) {// 尾插入,插入在最后
this.add(element);
} else {
Node p = this.head;
int i = 0;
while (p.next != null && i < index) {
i++;
p = p.next;
}
// 下面操作可以包含头插入和中间插入
Node q = new Node(element);
q.next = p.next;
p.next = q;// 将q结点插入到p结点之后
this.n++;
return true;
}
return false;
}
/**
* 在单链表的最后插入元素对象,时间复杂度是O(1)
*/
@Override
public boolean add(E element) {
if (element == null) {// 不允许插入非空元素
return false;
}
this.rear.next = new Node(element);// 尾插入
this.rear = this.rear.next;// 移动尾指针
this.n++;// 链表长度增加
return true;
}

代码解释:

① 带头结点的单链表在进行插入操作的时候不需要区分插入位置。

② 插入操作会维护单链表的长度n,插入一个元素之后n加1。

6. 带头结点单链表的删除 /**

* 移除索引index处的结点,操作成功返回被移除的对象,失败则返回null
*/
@Override
public E remove(int index) {
E old = null;
if (index >= 0) {// 头删除,中间删除,尾删除
Node p = this.head;
int i = 0;
while (p.next != null && i < index) {// 从头结点开始遍历,定位到待删除结点的前驱结点
i++;
p = p.next;
}
if (p.next != null) {
old = p.next.data;
if (p.next == this.rear) {// 如果p结点的后一个结点是尾结点,则移除之后尾结点指针前移
this.rear = p;
}
p.next = p.next.next;// 删除p结点的后继结点
this.n--;// 链表长度减少
return old;
}
}
return old;
}

代码解释

① 带头结点的单链表在进行删除操作的时候不需要区分删除位置。

② 删除操作会维护单链表的长度n,删除一个元素之后n减1。

7. 清空带头结点的单链表 /**

* 清空单链表
*/
@Override
public void clear() {
this.head.next = null;
this.rear = this.head;
this.n = 0;
}

代码解释

① 清空带头结点的单链表需要把头结点的下一个结点置为空。

② 把尾结点指向头结点。

③ 把单链表的长度置为0。

8. 重写toString()方法 @Override

public String toString() {// 返回所有元素值对应的字符串
String str = "(";
Node p = this.head.next;//不是从头结点开始,而是从头结点的下一个结点开始
while (p != null) {
str += p.data.toString();
p = p.next;
if (p != null) {
str += ", ";
}
}
return str + ")";
}

三.测试

测试代码如下所示:

package org.light4j.dataStructure.linearList.linkList.head;
import org.light4j.dataStructure.linearList.LList;
public class Test {
public static void main(String[] args) {
LList list = new HeadSinglyLinkedList();
for (int i = 0; i < 10; i++) {
list.add(0, new String((char) ('A' + i) + ""));
}
System.out.println(list.toString());
list.remove(0);//移除第一个元素
System.out.println(list.toString());
}
}

运行结果如下图所示:

java多节点部署方案 java结点类_头结点_04

四.带头结点的单链表操作效率分析

由于增加成员变量n来维护单链表的长度,所以length()操作的时间复杂度为O(1)。由于增加了尾结点rear指向单链表的最后一个结点,所以在尾结点进行插入操作的时间复杂度是O(1)。别的操作的时间复杂度和普通单链表一样,请参见之前文章单链表的实现的分析。