<Linked List Basics> 链表基础

Abstract

This document introduces the basic structures and techniques for building linked lists
with a mixture of explanations, drawings, sample code, and exercises. The material is
useful if you want to understand linked lists or if you want to see a realistic, applied
example of pointer-intensive code. A separate document, Linked List Problems
(http://cslibrary.stanford.edu/105/), presents 18 practice problems covering a wide range
of difficulty.
Linked lists are useful to study for two reasons. Most obviously, linked lists are a data
structure which you may want to use in real programs. Seeing the strengths and
weaknesses of linked lists will give you an appreciation of the some of the time, space,
and code issues which are useful to thinking about any data structures in general.
Somewhat less obviously, linked lists are great way to learn about pointers. In fact, you
may never use a linked list in a real program, but you are certain to use lots of pointers.
Linked list problems are a nice combination of algorithms and pointer manipulation.
Traditionally, linked lists have been the domain where beginning programmers get the
practice to really understand pointers.

Contents

Section 1 — Basic List Structures and Code 基础List的结构和代码
Section 2 — Basic List Building 构建基础的列表
Section 3 — Linked List Code Techniques 链表编程技巧
Section 3 — Code Examples 代码基础

由于这部分比较简单,就不翻译了

这句话比较重要:
Seeing the strengths and weaknesses of linked lists will give you an appreciation of the some of the time, space,and code issues which are useful to thinking about any data structures in general.
Somewhat less obviously, linked lists are great way to learn about pointers.
了解链表的优缺点可以有助于我们对其他的数据结构的时间空间代码问题的理解。
并且可以加强对指针的学习。

链表是学习树和图的基础,而且在数据结构与算法中,指针真的非常重要啊!
哪怕是Java中不存在真正的指针,但也大量用到“指针引用”的概念,都是同一道理的。

Linked List Code Techniques 链表编程基础

This section summarizes, in list form, the main techniques for linked list code. These
techniques are all demonstrated in the examples in the next section.

1) Iterate Down a List 循环List

The head pointer is copied into a local variable current which then iterates down the list.
将头指针复制到本地变量current中,然后用current循环列表

//while方法
int Length(struct node* head) {
	int count = 0;
	struct node* current = head;
	while (current != NULL) {
		count++;
		current = current->next
	}
	return(count);
}
//for方法
for (current = head; current != NULL; current = current->next) {
2) Changing a Pointer With A Reference Pointer 通过引用指针修改指针

Many list functions need to change the caller’s head pointer. To do this in the C language,
pass a pointer to the head pointer. Such a pointer to a pointer is sometimes called a
“reference pointer”. The main steps for this technique are…
• Design the function to take a pointer to the head pointer. This is the
standard technique in C — pass a pointer to the “value of interest” that
needs to be changed. To change a struct node*, pass a struct
node**.
• Use ‘&’ in the caller to compute and pass a pointer to the value of interest.
• Use ‘*’ on the parameter in the callee function to access and change the
value of interest.
The following simple function sets a head pointer to NULL by using a reference
parameter…
这部分是只调用方法传入的参数

// Change the passed in head pointer to be NULL
// Uses a reference pointer to access the caller's memory
void ChangeToNull(struct node** headRef) { // Takes a pointer to // the value of interest
	*headRef = NULL; // use '*' to access the value of interest
}
void ChangeCaller() {
	struct node* head1;
	struct node* head2;
	ChangeToNull(&head1); // use '&' to compute and pass a pointer to
	ChangeToNull(&head2); // the value of interest
	// head1 and head2 are NULL at this point
}

这段不翻译,对C不是特别熟悉

3) Build — At Head With Push() 通过Push()方法添加到头部

The easiest way to build up a list is by adding nodes at its “head end” with Push(). The
code is short and it runs fast — lists naturally support operations at their head end. The
disadvantage is that the elements will appear in the list in the reverse order that they are
added. If you don’t care about order, then the head end is the best.
往链表中添加节点最简单的方法是,通过push()方法添加到头部。代码又简单又快。但缺点是列表也因此有了顺序(添加的顺序)

struct node* AddAtHead() {
	struct node* head = NULL;
	int i;
	for (i=1; i<6; i++) {
		Push(&head, i);
	}
	// head == {5, 4, 3, 2, 1};
	return(head);
}
4) Build — With Tail Pointer 使用尾指针

往链表尾部添加节点也可以,添加后移动tail指针

5) Build — Special Case + Tail Pointer 特殊情况 + 尾指针
struct node* BuildWithSpecialCase() {
	struct node* head = NULL;
	struct node* tail;
	int i;
	// Deal with the head node here, and set the tail pointer
	Push(&head, 1);
	tail = head;
	// Do all the other nodes using 'tail'
	for (i=2; i<6; i++) {
		Push(&(tail->next), i); // add node at tail->next
		tail = tail->next; // advance tail to point to last node
	}
	return(head); // head == {1, 2, 3, 4, 5};
}
6) Build — Dummy Node 虚拟节点

Another solution is to use a temporary dummy node at the head of the list during the
computation. The trick is that with the dummy, every node appear to be added after the
.next field of a node. That way the code for the first node is the same as for the other
nodes. The tail pointer plays the same role as in the previous example. The difference is
that it now also handles the first node.
使用dummyNode可以不用特殊化head节点,简化操作

struct node* BuildWithDummyNode() {
	struct node dummy; // Dummy node is temporarily the first node
	struct node* tail = &dummy; // Start the tail at the dummy.
	// Build the list on dummy.next (aka tail->next)
	int i;
	dummy.next = NULL;
	for (i=1; i<6; i++) {
		Push(&(tail->next), i);
		tail = tail->next;
	}
	// The real result list is now in dummy.next
	// dummy.next == {1, 2, 3, 4, 5};
	return(dummy.next);
}
7) Build — Local References 本地参考

??? 这个没有明白,不懂C语言语法
Finally, here is a tricky way to unifying all the node cases without using a dummy node.
The trick is to use a local “reference pointer” which always points to the last pointer in
the list instead of to the last node. All additions to the list are made by following the
reference pointer. The reference pointer starts off pointing to the head pointer. Later, it
points to the .next field inside the last node in the list. (A detailed explanation follows.)

This technique is short, but the inside of the loop is scary. This technique is rarely used.
(Actually, I’m the only person I’ve known to promote it. I think it has a sort of compact
charm.) Here’s how it works…

  1. At the top of the loop, lastPtrRef points to the last pointer in the list.
    Initially it points to the head pointer itself. Later it points to the .next
    field inside the last node in the list.
  2. Push(lastPtrRef, i); adds a new node at the last pointer. The
    new node becaomes the last node in the list.
  3. lastPtrRef= &((*lastPtrRef)->next); Advance the
    lastPtrRef to now point to the .next field inside the new last node
    — that .next field is now the last pointer in the list.
    Here is a drawing showing the state of memory for the above code just before the third
    node is added. The previous values of lastPtrRef are shown in gray…

This technique is never required to solve a linked list problem, but it will be one of the
alternative solutions presented for some of the advanced problems.
Both the temporary-dummy strategy and the reference-pointer strategy are a little
unusual. They are good ways to make sure that you really understand pointers, since they
use pointers in unusual ways.

struct node* BuildWithLocalRef() {
	struct node* head = NULL;
	struct node** lastPtrRef= &head; // Start out pointing to the head pointer
	int i;
	for (i=1; i<6; i++) {
		Push(lastPtrRef, i); // Add node at the last pointer in the list
		lastPtrRef= &((*lastPtrRef)->next); // Advance to point to the
		// new last pointer
	}
	// head == {1, 2, 3, 4, 5};
	return(head);
}

Examples 例子

CopyList() Example
CopyList() With Push()
// Variant of CopyList() that uses Push()
struct node* CopyList2(struct node* head) {
	struct node* current = head; // used to iterate over the original list
	struct node* newList = NULL; // head of the new list
	struct node* tail = NULL; // kept pointing to the last node in the new list
	while (current != NULL) {
		if (newList == NULL) { // special case for the first new node
		Push(&newList, current->data);
		tail = newList;
	}
	else {
		Push(&(tail->next), current->data); // add each node at the tail
		tail = tail->next; // advance the tail to the new last node
	}
	current = current->next;
	}
	return(newList);
}
CopyList() With Dummy Node
// Dummy node variant
struct node* CopyList(struct node* head) {
	struct node* current = head; // used to iterate over the original list
	struct node* tail; // kept pointing to the last node in the new list
	struct node dummy; // build the new list off this dummy node
	dummy.next = NULL;
	tail = &dummy; // start the tail pointing at the dummy
	while (current != NULL) {
		Push(&(tail->next), current->data); // add each node at the tail
		tail = tail->next; // advance the tail to the new last node
	}
	current = current->next;
	}
	return(dummy.next);
}
CopyList() With Local References

这个方法不懂。。。。

// Local reference variant
struct node* CopyList(struct node* head) {
	struct node* current = head; // used to iterate over the original list
	struct node* newList = NULL;
	struct node** lastPtr;
	lastPtr = &newList; // start off pointing to the head itself
	while (current != NULL) {
		Push(lastPtr, current->data); // add each node at the lastPtr
		lastPtr = &((*lastPtr)->next); // advance lastPtr
		current = current->next;
	}
	return(newList);
}
CopyList() Recursive

Finally, for completeness, here is the recursive version of CopyList(). It has the pleasing
shortness that recursive code often has. However, it is probably not good for production
code since it uses stack space proportional to the length of its list.

// Recursive variant
struct node* CopyList(struct node* head) {
	if (head == NULL) return NULL;
	else {
		struct node* newList = malloc(sizeof(struct node)); // make the one node
    	newList->data = current->data;
		newList->next = CopyList(current->next); // recur for the rest
		return(newList);
	}
}

Appendix —Other Implementations 附录—其他的实现(待翻译)

There are a many variations on the basic linked list which have individual advantages
over the basic linked list. It is probably best to have a firm grasp of the basic linked list
and its code before worrying about the variations too much.
• Dummy Header Forbid the case where the head pointer is NULL.
Instead, choose as a representation of the empty list a single “dummy”
node whose .data field is unused. The advantage of this technique is that
the pointer-to-pointer (reference parameter) case does not come up for
operations such as Push(). Also, some of the iterations are now a little
simpler since they can always assume the existence of the dummy header
node. The disadvantage is that allocating an “empty” list now requires
allocating (and wasting) memory. Some of the algorithms have an ugliness
to them since they have to realize that the dummy node “doesn’t count.”
(editorial) Mainly the dummy header is for programmers to avoid the ugly
reference parameter issues in functions such as Push(). Languages which
don’t allow reference parameters, such as Java, may require the dummy
header as a workaround. (See the “temporary dummy” variant below.)
• Circular Instead of setting the .next field of the last node to NULL,
set it to point back around to the first node. Instead of needing a fixed head
end, any pointer into the list will do.
• Tail Pointer The list is not represented by a single head pointer. Instead
the list is represented by a head pointer which points to the first node and a
tail pointer which points to the last node. The tail pointer allows operations
at the end of the list such as adding an end element or appending two lists
to work efficiently.
• Head struct A variant I like better than the dummy header is to have a
special “header” struct (a different type from the node type) which
contains a head pointer, a tail pointer, and possibly a length to make many
operations more efficient. Many of the reference parameter problems go
away since most functions can deal with pointers to the head struct
(whether it is heap allocated or not). This is probably the best approach to
use in a language without reference parameters, such as Java.
• Doubly-Linked Instead of just a single .next field, each node
incudes both .next and .previous pointers. Insertion and deletion now
require more operations. but other operations are simplified. Given a
pointer to a node, insertion and deletion can be performed directly whereas
in the singly linked case, the iteration typically needs to locate the point
just before the point of change in the list so the .next pointers can be
followed downstream.
• Chunk List Instead of storing a single client element in each node, store
a little constant size array of client elements in each node. Tuning the
number of elements per node can provide different performance
characteristics: many elements per node has performance more like an
array, few elements per node has performance more like a linked list. The
Chunk List is a good way to build a linked list with good performance.
• Dynamic Array Instead of using a linked list, elements may be
stored in an array block allocated in the heap. It is possible to grow and
shrink the size of the block as needed with calls to the system function
realloc(). Managing a heap block in this way is a fairly complex, but can
have excellent efficiency for storage and iteration., especially because
modern memory systems are tuned for the access of contiguous areas of
memory. In contrast, linked list can actually be a little inefficient, since
they tend to iterate through memory areas that are not adjacent.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值