实现线性表的另一种方法是链式存储,即用指针将存储线性表中数据元素的那些单元依次串联在一起。这种方法避免了在数组中用连续的单元存储元素的缺点,因而在执行插入或删除运算时,不再需要移动元素来腾出空间或填补空缺。然而我们为此付出的代价是,需要在每个单元中设置指针来表示元素之间的逻辑关系,增加了额外的存储空间的开销。
所谓链式存储其实就是通过链表来实现线性表,而链表有不同的形式,比如单链表、循环链表、双向链表等。那么我们首先来介绍一下链表。
1.链表
1.1 单链表
链表是一系列的存储数据元素的单元通过指针串联起来形成的,因此每个单元至少有两个域,一个域用于数据元素的存储,另一个域是指向其他单元的指针。这里,具有一个数据域和多个指针域的存储单元通常称为结点(Node)。
最简单的链表就是单链表,它有一个数据域与一个指针域,见下图,数据域用来存储数据元素,指针域用来指向下一个具有相同结构的结点。
Java中并没有显示的指针,而实际上对象的访问就是使用指针来实现的,即在Java中使用对象的引用来代替指针。因此使用Java来实现链表的结点结构时,一个结点本身就是一个对象。结点的数据域可以使用Object类型的对象,而指针域就是指向一个结点的引用。下面就是一个单链表结点的定义:
public class Node {
private Object data;
private Node next;
public Node() {
this(null,null);
}
public Node(Object data, Node next) {
super();
this.data = data;
this.next = next;
}
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;
}
}
单链表就是通过上述定义的结点使用next域依次串联而成,结构如下:
链表的第一个结点和最后一个结点,分别称为首结点和尾结点。尾结点的特征是其next域为null。链表中的每一个结点的next引用都相当于一个指针,指向另一个结点,借助这些next引用,我们可以从链表的首结点移动到尾结点。
在单链表中通常使用head引用来指向链表的首结点,由head引用可以完成对整个链表中所有结点的访问。有时也可以根据需要使用指向尾结点的tail引用来方便某些操作的实现。
与数组类似,单链表的结点也具有一个线性次序,即如果结点P的next引用指向结点S,则P就是S的直接前驱,而S是P的直接后继。单链表的一个重要特性就是只能通过前驱结点找到后继结点,而无法从后继结点找到前驱结点。
单链表的查找操作
在单链表中进行查找操作,只能从链表的首结点开始,通过每个结点的next引用来依次访问链表中的每个结点以完成相应的查找操作。如下图:
在单链表中查找操作的时间复杂度与在数组中一样,也是O(n)。
单链表的插入操作
在单链表中数据元素的插入,是通过在链表中插入数据元素所属的结点来完成的。对于链表的不同位置,比如表头、表的中间位置和表尾,插入的过程会有区别,如下图,a,b,c分别为在这三个位置的插入过程:
在已知单链表中某个结点引用的基础上,完成结点的插入操作的需要O(1)时间,这要比数组的插入操作快很多。但是,通常我们的需求是在第i个结点之前插入一个新结点,这样的话,我们就必须先找到第i-1个结点,所以在插入之前也需要一个查找操作,故它的时间复杂度是O(n)。
单链表的删除操作
单链表的删除操作也是通过删除结点来完成的。同样的,在链表的表头、表的中间位置和表尾删除结点,过程也是不一样的,如下:
在单链表中删除一个结点时,除首结点外都必须知道该结点的直接前驱结点的引用。也就是说,如果在已知单链表中某个结点引用的基础上,完成其后续结点的删除操作需要的时间是O(1),这比在数组中的删除操作要快的多。但是在不知道结点引用的情况下,我们删除第i个结点,需要先找到第i-1个结点,故它的时间复杂度也为O(n)。
1.2 循环链表
循环链表(circular linked list)是另一种形式的链式存储结构。它的特点是表中的最后一个结点的指针域指向首结点,整个链表形成一个环。由此,从表中任一结点出发均可找到表中其他结点。
循环链表的操作和单链表基本一致,差别仅在于算法中的循环条件不是next域为空,而是next域是否等于首结点。
1.3 双向链表
上述讨论的链式存储结构的结点中只有一个指示直接后继的指针域,由此,从某个结点出发只能顺时针往后寻找其他结点。若要寻找结点的直接前驱,,则需从表头出发,这需要O(n)的时间。
为此,我们可以扩展单链表的结点结构,使得通过一个结点的引用,不但能够访问其后继结点,也可以方便地访问其前驱结点。扩展单链表结点结构的方法是,在单链表结点结构中新增加一个域,该域用于指向结点的直接前驱结点,如下图所示结点结构:
同样的,我们也可以在Java中定义双向链表的结点:
public class DuLNode {
private Object data;
private DuLNode pre;
private DuLNode next;
public DuLNode() {
this(null, null, null);
}
public DuLNode(Object data, DuLNode pre, DuLNode next) {
super();
this.data = data;
this.pre = pre;
this.next = next;
}
public Object getData() {
return data;
}
public void setData(Object data) {
this.data = data;
}
public DuLNode getPre() {
return pre;
}
public void setPre(DuLNode pre) {
this.pre = pre;
}
public DuLNode getNext() {
return next;
}
public void setNext(DuLNode next) {
this.next = next;
}
}
双向链表是通过上述定义的结点使用pre以及next域依次串联在一起而形成的。一个双向链表的结构如下:
在双向链表中同样需要完成数据元素的查找、插入、删除等操作。在双向链表中进行查找与在单链表中类似,只不过在双向链表中查找操作可以从链表的首结点开始,也可以从链表的尾结点开始,但是需要的时间和在单链表中一样,在平均情况下,需要比较大约一半的数据元素,即T(n)= n/2,时间复杂度也为O(n)。
单链表的插入操作,除了首结点以外必须在某个已知结点后面进行,而在双向链表中插入操作在一个已知结点之前或之后都可以进行。例如在某个结点p之前插入一个新结点的过程如下:
在结点p之后插入一个新结点的操作与上述类似。
单链表的删除操作,除了首结点之外必须在知道待删结点的前驱结点的基础上才能进行,而在双向链表中在已知某个结点引用的前提下,可以完成该结点自身的删除,如下所示:
2.线性表的单链表实现
在使用链表实现线性表时,既可以使用单链表,也可以使用双向链表。在这里我们首先使用单链表来实现线性表。
在使用单链表实现线性表时,线性表中的每个数据元素对应单链表中的一个结点,而线性表元素之间的逻辑关系是通过单链表中元素在结点之间的指向来表示的。
通常,在使用单链表实现线性表的时候,为了使程序更加简洁,我们通常在单链表的最前面添加一个哑元结点,也称为头结点。在头结点中不存储任何实质的数据对象,其next域指向线性表中0号元素所在的结点。头结点的引入可以使线性表运算中的一些边界条件更容易处理。一个带头结点的单链表实现线性表的结构图如下:
通过上图我们可以发现,在带头结点的单链表中,任何基于序号的插入和删除,都可以转化为在某个特定结点之后完成结点的插入、删除,而不需要考虑插入和删除的位置是在链表的首部、中间还是尾部。这给我们带来了很大的方便。
下述代码通过单链表实现了一个线性表:
public class MySingleLinkedList<T> {
/**
* 内部的结点类
*
* @author Gavin
*
*/
private class Node {
private T data;
private Node next;
public Node() {
this(null, null);
}
public Node(T data, Node next) {
super();
this.data = data;
this.next = next;
}
public Object getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
public Node getNext() {
return next;
}
public void setNext(Node next) {
this.next = next;
}
}
private Node head; // 单链表头结点的引用,头结点数据域为空
private int size; // 线性表中数据元素的个数
public MySingleLinkedList() {
head = new Node();
size = 0;
}
/**
* 返回线性表的大小,即线性表中数据元素的个数
*
* @return
*/
public int size() {
return size;
}
/**
* 如果线性表为空,则返回true,否则返回false
*
* @return
*/
public boolean isEmpty() {
return size() == 0;
}
/**
* 判断元素是否存在于线性表中,存在则返回true,否则返回false
*
* @param e
* @return
*/
public boolean contains(T e) {
return indexOf(e) >= 0;
}
/**
* 返回元素e在线性表中的序号,即下标。 要注意e为null的情况。如果不存在元素e,则返回-1。
*
* @param e
* @return
*/
public int indexOf(T e) {
Node p = head.getNext();
int index = 0;
if (e == null) {
while (p != null) {
if (p.getData() == null) {
return index;
} else {
p = p.getNext();
index += 1;
}
}
} else {
while (p != null) {
if (e.equals(p.getData())) {
return index;
} else {
p = p.getNext();
index += 1;
}
}
}
return -1;
}
/**
* 辅助方法,获取元素e所在结点的前驱结点。考虑元素e为null的情况
*
* @param e
* @return
*/
private Node getPreNode(T e) {
Node p = head;
if(e == null){
while(p.getNext() != null){
if(p.getNext().getData() == null){
return p;
}
}
}else{
while(p.getNext() != null){
if(e.equals(p.getNext().getData())){
return p;
}
}
}
return null;
}
/**
* 辅助方法,获取序号为i(0<=i<=size)的元素所在结点的前驱结点。
* i=0时,获取到的前驱结点即为头结点。i=size时,获取到的前驱结点即为最后一个结点
*
* @param i
* @return
*/
private Node getPreNode(int i) {
Node p = head;
for (; i > 0; i--) {
p = p.getNext();
}
return p;
}
/**
* 辅助方法,获取序号为i(0<=i<size)的元素所在的结点
*
* @param i
* @return
*/
private Node getNode(int i) {
Node p = head;
for (; i >= 0; i--) {
p = p.getNext();
}
return p;
}
/**
* 将数据元素e插入到序号为i的位置
*
* @param i
* @param e
*/
public void add(int i, T e) {
// 下标越界的情况
if (i < 0 || i > size) {
throw new IndexOutOfBoundsException();
}
// 前驱结点
Node preNode = getPreNode(i);
// 当前结点
Node newNode = new Node();
newNode.setData(e);
// 插入结点
newNode.setNext(preNode.getNext());
preNode.setNext(newNode);
// 数据大小增加1
size += 1;
}
/**
* 默认将数据元素插入到最后,即序号为size的位置
*
* @param e
*/
public void add(T e) {
add(size(), e);
}
/**
* 删除序号为i的数据元素
*
* @param i
* @return
*/
public T remove(int i) {
// 下标越界的情况
if (i < 0 || i >= size) {
throw new IndexOutOfBoundsException();
}
// 前驱结点
Node preNode = getPreNode(i);
T oldData = (T) preNode.getNext().getData();
preNode.setNext(preNode.getNext().getNext());
size -= 1;
return oldData;
}
/**
* 删除线性表中第一个与e相同的元素
*
* @param e
* @return
*/
public boolean remove(T e) {
// 前驱结点
Node preNode = getPreNode(e);
if (preNode == null) {
return false;
}
preNode.setNext(preNode.getNext().getNext());
size -= 1;
return true;
}
/**
* 获取序号为i的数据元素
*
* @param i
* @return
*/
public T get(int i) {
// 下标越界的情况
if (i < 0 || i >= size) {
throw new IndexOutOfBoundsException();
}
Node node = getNode(i);
return (T) node.getData();
}
/**
* 替换线性表中序号为i的数据元素为e,返回原数据元素
*
* @param i
* @param e
* @return
*/
public T set(int i, T e) {
// 下标越界的情况
if (i < 0 || i >= size) {
throw new IndexOutOfBoundsException();
}
Node node = getNode(i);
T oldData = (T) node.getData();
node.setData(e);
return oldData;
}
@Override
public String toString() {
StringBuilder stringBuilder = new StringBuilder("[");
Node p = head.next;
int index = 0;
while (p != null) {
stringBuilder.append(p.getData());
if (index == size - 1) {
stringBuilder.append("]");
} else {
stringBuilder.append(", ");
}
p = p.getNext();
index += 1;
}
return stringBuilder.toString();
}
}
说明:在MySingleLinkedList类中有2个成员变量,其中head是带头结点的单链表的头结点的引用;而size是线性表的大小,也就是数据元素的个数。
方法size(),isEmpty()的时间复杂度均为O(1),通过成员变量size即可以直接判断出线性表的大小以及线性表是否为空。
由于链表中每个结点在内存中的地址不是连续的,所以链表不具有随机存取的特性。因此,线性表中的一些基于数据元素或序号的插入、删除操作均依赖于对应元素在单链表中的前驱结点的引用。故它们的时间复杂度都是O(n)。
3.线性表的双向链表实现
另外,我们可以选择使用双向链表来实现线性表。
在使用双向链表实现线性表时,为了更加简洁,我们可以使用带两个哑元结点的双向链表,即head头结点,和tail尾结点,它们的数据域data为null,头结点的pre为null,而尾结点的next为空。如下结构所示:
下述代码实现了利用双向链表实现线性表:
public class MyDoubleLinkedList<T> {
/**
* 内部结点类
*
* @author Gavin
*
*/
private class Node {
private T data;
private Node pre;
private Node next;
public Node() {
this(null, null, null);
}
public Node(T data, Node pre, Node next) {
super();
this.data = data;
this.pre = pre;
this.next = next;
}
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
public Node getPre() {
return pre;
}
public void setPre(Node pre) {
this.pre = pre;
}
public Node getNext() {
return next;
}
public void setNext(Node next) {
this.next = next;
}
}
private int size;// 线性表的长度,即数据元素的个数
private Node head;// 头结点
private Node tail;// 尾结点
public MyDoubleLinkedList() {
// 初始化
size = 0;
head = new Node();
tail = new Node();
head.setNext(tail);
tail.setPre(head);
}
/**
* 获取线性表的大小,即数据元素的个数
*
* @return
*/
public int size() {
return size;
}
/**
* 判断线性表是否为空,为空则返回true,否则返回false
*
* @return
*/
public boolean isEmpty() {
return size() == 0;
}
/**
* 返回元素e在线性表中的第一个序号。没有元素e则返回-1
*
* @param e
* @return
*/
public int indexOf(T e) {
// 从头结点开始往后找
Node p = head.getNext();
int index = 0;
if (e == null) {
while (p != null) {
if (p.getData() == null) {
return index;
} else {
p = p.getNext();
index += 1;
}
}
} else {
while (p != null) {
if (e.equals(p.getData())) {
return index;
} else {
p = p.getNext();
index += 1;
}
}
}
return -1;
}
/**
* 返回元素e在线性表中的最后一个序号。没有元素e则返回-1
*
* @param e
* @return
*/
public int lastIndexOf(T e) {
// 从尾结点开始往前找
Node p = tail.getPre();
int index = size-1;
if (e == null) {
while (p != null) {
if (p.getData() == null) {
return index;
} else {
p = p.getPre();
index -= 1;
}
}
} else {
while (p != null) {
if (e.equals(p.getData())) {
return index;
} else {
p = p.getPre();
index -= 1;
}
}
}
return -1;
}
/**
* 判断某个元素是否在线性表中,是则返回true,否则返回false
*
* @param e
* @return
*/
public boolean contains(T e) {
return indexOf(e) >= 0;
}
/**
* 辅助方法,获取序号为i(0<=i<=size)的元素所在的结点的前驱结点
* @param i
* @return
*/
private Node getPreNode(int i) {
Node p = null;
if (i < size() / 2) {
// 如果在前半段,就从头结点开始查找
p = head;
for (; i > 0; i--) {
p = p.getNext();
}
} else {
// 如果在后半段,就从尾结点开始查找
p = tail;
for (; i <= size; i++) {
p = p.getPre();
}
}
return p;
}
/**
* 辅助方法,获取元素e所在结点的前驱结点。考虑元素e为null的情况
*
* @param e
* @return
*/
private Node getPreNode(T e) {
Node p = head;
if(e == null){
while(p.getNext() != null){
if(p.getNext().getData() == null){
return p;
}
}
}else{
while(p.getNext() != null){
if(e.equals(p.getNext().getData())){
return p;
}
}
}
return null;
}
/**
* 在序号为i的位置上插入结点
* @param i
* @param e
*/
public void add(int i, T e) {
// 下标越界
if(i < 0 || i > size){
throw new IndexOutOfBoundsException();
}
Node preNode = getPreNode(i);
Node newNode = new Node(e,preNode,preNode.getNext());
// 改变前后两个结点的指针
preNode.getNext().setPre(newNode);
preNode.setNext(newNode);
// 大小加1
size += 1;
}
/**
* 默认在线性表的末尾添加元素
* @param e
*/
public void add(T e){
add(size(), e);
}
/**
* 删除序号为i的元素
* @param i
* @return
*/
public T remove(int i){
// 下标越界
if(i < 0 || i >= size){
throw new IndexOutOfBoundsException();
}
Node preNode = getPreNode(i);
T oldData = preNode.getNext().getData();
preNode.setNext(preNode.getNext().getNext());
preNode.getNext().setPre(preNode);
size -= 1;
return oldData;
}
/**
* 删除线性表中第一个与e相同的元素
*
* @param e
* @return
*/
public boolean remove(T e) {
// 前驱结点
Node preNode = getPreNode(e);
if (preNode == null) {
return false;
}
preNode.setNext(preNode.getNext().getNext());
preNode.getNext().setPre(preNode);
size -= 1;
return true;
}
/**
* 获取序号为i的数据元素
*
* @param i
* @return
*/
public T get(int i){
// 下标越界
if(i < 0 || i >= size){
throw new IndexOutOfBoundsException();
}
return getPreNode(i).getNext().getData();
}
/**
* 替换线性表中序号为i的数据元素为e,返回原数据元素
*
* @param i
* @param e
* @return
*/
public T set(int i, T e) {
// 下标越界的情况
if (i < 0 || i >= size) {
throw new IndexOutOfBoundsException();
}
Node node = getPreNode(i).getNext();
T oldData = (T) node.getData();
node.setData(e);
return oldData;
}
@Override
public String toString() {
StringBuilder stringBuilder = new StringBuilder("[");
Node p = head.next;
int index = 0;
while (p.getNext()!= null) {
stringBuilder.append(p.getData());
if (index == size - 1) {
stringBuilder.append("]");
} else {
stringBuilder.append(", ");
}
p = p.getNext();
index += 1;
}
return stringBuilder.toString();
}
}
说明:MyDoubleLinkedList中有3个成员变量,其中size是线性表的大小,head是头结点,tail是尾结点。
利用双向链表实现线性表与单向链表实现的线性表基本上是类似的,只是在增加和删除的时候修改的指针不一样。
另外,利用双向链表,在对元素进行随机访问的时候,可以根据序号选择从头结点或者尾结点进行遍历,相对于单链表只能从头结点进行遍历来说,速度上也会快很多。
在时间复杂度上,与单链表实现的线性表是一样的。size()和isEmpty()方法需要O(1)时间,而其他的查找,增加和删除等,都需要O(n)时间。
627

被折叠的 条评论
为什么被折叠?



