从浅到深的算法技巧链表

3.链表

链表是在集合类的抽象数据类型实现中表示数据的合适选择。

定义:链表是一种递归的数据结构,它或者为空(null),或者是指向一个结点( node )的引用,该结点合有一个泛型的元素和一个指向另一条链表的引用。

在这个定义中,结点是一个可能含有任意类型数据的抽象实体,它所包含的指向结点的应用显示了它在构造链表之中的作用。和递归程序一样。

3.1 结点记录

在面向对象编程中,实现链表并不困难。我们首先用一个嵌套类来定义结点的抽象数据类型:

private class Node{

   Item item;

   Node next;

}

一个Node对象含有两个实例变量,类型分别为Item (参数类型)和Node。我们会在需要使用Node类的类中定义它并将它标记为private,因为它不是为用例准备的。和任意数据类型一样,我们通过new Node()触发(无参数的)构造函数来创建一个Node类型的对象。调用的结果是一个指向Node对象的引用,它的实例变量均被初始化为null。Item是一个占位符,表示我们希望用链表处理的任意数据类型(我们将会使用Java 的泛型使之表示任意引用类型):Node类型的实例变量显示了这种数据结构的链式本质。为了强调我们在组织数据时只使用了Node类,我们没有定义任何方法且会在代码中直接引用实例变量:如果first是一个指向某个Node对象的变量,我们可以使用first.item和first.node访问它的实例变量。这种类型的类有时也被称为记录。它们实现的不是抽象数据类型因为我们会直接使用其实例变量。但是在我们的实现中,Node 和它的用例代码都会被封装在相同的类中且无法被该类的用例访问。

3.2 构造链表

现在,根据递归定义,我们只需要一个 Node类型的变量就能表示- 条链表,只要保证它的值是null或者指向另一个Node对象且该对象的next域指向了另一条链表即可。例如,要构造yi一条含有元素to、be和or的链表,我们首先为每个元素创造一个结点:

Node first = new Node();

Node second = new Node();

Node. third = new Node();

并将每个结点的item城设为所需的值(简单起见,我们假设在这些例子中Item为string):

first.item = "to"

second.item = "be"

third.item = "or"

然后设置next域来构造链表:

first.next = second;

second.next = third;

(注意: third.next 仍然是null,即对象创建时它被初始化的值。) 结果是,third 是一条链表(它是一个结点的引用, 该结点指向null,即一个空链表),second 也是一条链表(它是一个结点的引用,且该结点含有一个指向third的引用,而third是一条链表),也是一条链表(它是一个结点的引用,且该结点含有一个指向second的引用,而second是一条链表)。

链表表示的是一列元素。在我们刚刚考察过Node third = new Node()的例子中,first 表示的序列是to, be、or。我们也可以用一个数组来表示列元素。例如,可以用以下数组表示同一列字符串:

String[] s = { "to",“be","or” );

不同之处在于,在链表中向序列插入元素或是从序列中删除元素都更方便。

在追踪使用链表和其他链式结构的代码时,我们会使用可视化表示方法:

1.用长方形表示对象;

2.将实例变量的值写在长方形中;

3.用指向被引用对象的箭头表示引用关系。

这种表示方式抓住了链表的关键特性。方便起见,我们用术语链接表示对结点的引用。简单起见,当元素的值为字符串时,我们会将字符串写在长方形之内,这种可视化的表示方式使我们能够将注意力集中在链表上。

3.3 在表头插入结点

首先,假设你希望向一条链表中插人一个新的结点。最容易做到这点的地方就是链表的开头。例如,要在首结点为first的给定链表开头插人字符串not,我们先将first保存在oldfirst中,然后将一个新结点赋予first,并将它的item域设为not, next 域设为oldfirst。这段在链表开头插入一个结点的代码只需要几行赋值语句,所以它所需的时间和链表的长度无关。

3.4 从表头删除结点

接下来,假设你删除一条链表的首结点。这个操作更简单:只需将first指向first.next即可。一般来说你可能会希望在赋值之前得到该元素的值,因为一旦改变了first的值,就再也无法访问它曾经指向的结点了。曾经的结点对象变成了一个孤儿,Java 的内存管理系统最终将回收它所占用的内存。和以前一样, 这个操作只含有一条赋值语句,因此它的运行时间和链表的长度无关。

3.5 在表尾插入结点

如何才能在链表的尾部添加一个新结点? 我们需要一个指向链表最后一个结点的链接,因为该结点的链接必须被修改并指向一个含有新元素的新结点。我们不能在链表代码中草率地决定维护一个额外的链接,因为每个修改链表的操作都需要添加检查是否要修改该变量(以及作出相应修改)的代码。例如,我们刚刚的删除链表首结点的代码就可能改变指向链表的尾结点的引用,因为当链表中只有一个结点时,它既是首结点又是尾结点!另外,这段代码也无法处理链表为空的情况(它会使用空链接)。

3.6 其他位置的插入和删除操作

我们已经展示了在链表中如何通过若干指令实现以下操作,其中我们可以通过first链接访问链表的首结点并通过last链接访问链表的尾结点:

1.在表头插入结点:

2.从表头删除结点:

3.在表尾插入结点。

其他操作,例如以下几种,就不那么容易实现了:

1.删除指定的结点;

2.在指定结点前插入一个新结点。

例如,我们怎样才能删除链表的尾结点呢? last 链接帮不上忙,因为我们需要将链表尾结点的前一个结点中的链接(它指向的正是last)值改为null。在缺少其他信息的情况下,唯一的解决办法就是遍 历整条链表并找出指向last的结点。这种解决方案并不是我们想要的,因为它所需的时间和链表的长度成正比。实现任意插人和删除操作的标准解决方案是使用双向链表,其中每个结点都含有两个链接,分别指向不同的方向。我们的所有实现都不需要双向链表。

要访问一个数组中的所有元素,我们会使用如下代码来循环处理a[]中的所有元素:

for(inti=0;1<N;i++){

   //处理a[i]
}

访问链表中的所有元素也有一个对应的方式:将循环的索引变量x初始化为链表的首结点,然后通过x.item访问和x相关联的元素,并将x设为x. next来访问链表中的下一个结点,如此反复直到x为null为止(这说明我们已经到达了链表的结尾)。这个过程被称为链表的遍历,可以用以下循环处理链表的每个结点的代码简洁表达,其中first指向链表的首结点:

for (Node x = first; x != nu1l; x = x.next){

    //处理x.item

}

这种方式和迭代遍历一个数组中的所有元素的标准方式一样。在我们的实现中,它是迭代器使用的基本方式, 它使用例能够迭代访问链表的所有元素而无需知道链表的实现细节。

3.7 栈的实现

有了这些知识,给出我们的Stack API的实现就很简单了,如94页的算法1.2所示。它将栈保存为一条链表,栈的顶部即为表头。实例亦量first指向栈顶,这样当使用push()压入一个元索时,我们会将该元素添加在表头;当使用pop()删除一个元素时,我们会将该元索从表头删除。要实现size()方法,我们用实例变量N保存元素的个数,在压人元素时将N加1,在弹出元素时将N减1。要实现isEmpty()方法,只需检查first是否为null (或者可以检查N是否为0)。该实现使用了泛型的Item你可以认为类名后的表示的是实现中所出现的所有Item都会替换为用例所提供的任意数据类型的名称。

链表的使用达到了我们的最优设计目标:

1.它可以处理任意类型的数据:

2.所需的空间总是和集合的大小成正比;

3.操作所需的时间总是和集合的大小无关。

Stack的测试用例
public static void main(String[] args) {
	// 创建一个栈并根据StdIn中的指示压入成弹出字符串

	Stack<String> s = new Stack<String>;

	while (! StdIn. isEmpty()){

	String item = StdIn.readString();

	if (litem. equas("-")     s.push(item);

	else if (! s.isEmpty())   System.out.println(s. pop());

	}

	System.out.println("C" + s.size() +”left on stack)");
}

它定义了链表数据结构并实现了供用例使用的方法push()和pop(,仅用了少量代码就取得了所期望的效果。算法和数据结构是相辅相成的。

算法下压堆栈 (链表实现)
public class Stack<Item>{

		private Node first; // 栈顶(最近添加的元素)

		private int N;  //元素数量

		public class Node{

			//定义了结点的嵌套类

			Item item;

			Node next;

		}

		public boolean isEmpty() { return first = null; } // 或:N == 0

		public int size() { return N; }

		public void push(Item item){

			Node oldfirst = first;

			first = new Node();

			first.item = item;

			first.next = oldfirst;

			N++;

		}

		public Item pop(){

			Item item = first. item;

			first = first. next;

			N--;

			return item;

		}

	}

这份泛型的Stack实现的基础是链表数据结构。它可以用于创建任意数据类型的栈。