线性表是由称为元素的数据项构成的一种有限且有序的序列。请注意,有序并非是元素按照值的大小排序,而是指每一个元素都有一个确定的位置。

一些术语

  • 空表-不含任何元素的线性表,记作<>
  • 长度-当前存储的元素数目。
  • 表头-线性表的开始结点。
  • 表尾-线性表的结尾结点。
  • 有序线性表-元素按照值的递增顺序排列的线性表。
  • 无序线性表-元素的值与位置之间无特殊关系的线性表。

  线性表包含了多种实现方式(数据结构),但是在此之前,应该先确定它的逻辑形式。也就是说,需要首先定义线性表对象的抽象数据类型(ADT)——通过考虑线性表的操作来定义。在C++中使用抽象类表示法定义线性表ADT。

什么是抽象类?

  • 包含纯虚函数的类。

什么是纯虚函数?

  • 纯虚函数无法实例化对象,只是作为基类为派生类服务;
  • 纯虚函数规定了函数所需参数和返回类型;
  • 纯虚函数一般不具有函数体(但是注意,也可以有函数体,但是仍然无法实例化对象),亦即可以让类先具有一个操作名称,而没有操作内容;
  • 纯虚函数在C++中的格式如下:
1
2
3
4
class <类名>{
virtual <类型><函数名>(<参数表>)=0;

};
单击此处查看线性表抽象类声明

  下面是从教材中摘录的线性表抽象类声明。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
template <typename E> class List { // List ADT
private:
void operator =(const List&) {} // Protect assignment
List(const List&) {} // Protect copy constructor
public:
List() {} // Default constructor
virtual ~List() {} // Base destructor

// Clear contents from the list, to make it empty.
virtual void clear() = 0;

// Insert an element at the current location.
// item: The element to be inserted
virtual void insert(const E& item) = 0;

// Append an element at the end of the list.
// item: The element to be appended.
virtual void append(const E& item) = 0;

// Remove and return the current element.
// Return: the element that was removed.
virtual E remove() = 0;

// Set the current position to the start of the list
virtual void moveToStart() = 0;

// Set the current position to the end of the list
virtual void moveToEnd() = 0;

// Move the current position one step left. No change
// if already at beginning.
virtual void prev() = 0;

// Move the current position one step right. No change
// if already at end.
virtual void next() = 0;

// Return: The number of elements in the list.
virtual int length() const = 0;

// Return: The position of the current element.
virtual int currPos() const = 0;

// Set current position.
// pos: The position to make current.
virtual void moveToPos(int pos) = 0;

// Return: The current element.
virtual const E& getValue() const = 0;
};

  使用ADT定义的线性表有一个栅栏(书面描述时使用|表示)和两个分离部分。栅栏指示操作的定位,例如对<10, 12 | 5, 8>,用值30执行函数insert,那么就得到<10, 12 | 30, 5, 8>
  线性表的实现有两种标准方法——顺序表、链表。

一 顺序表

  顺序表是在计算机内存中以数组的形式保存的线性表,它的特点是采用顺序存储——用一组地址连续的存储单元依次存储线性表中的各个元素、使得线性表中在逻辑结构上相邻的数据元素存储在相邻的物理存储单元中。如图F1所示。
F1.顺序表示例

请注意以下细节:

  • 在逻辑结构上相邻-不能理解为将数据按照从小到大的顺序排列,只要每一个元素都有确定的位置即可;
  • 相邻的数据元素存储在相邻的物理存储单元中-这意味着顺序表中不能存在空位,每一元素都应该依次相邻放入表中。

  顺序表既可以是静态的,也可以是动态的。静态顺序表是用固定长度的数组存储元素(需要首先确定数组的长度,直接利用数组定义),动态顺序表是用动态开辟的数组存储元素(可以动态扩容,利用指针定义)。后者采用较多。

单击此处查看顺序表的实现

  下面是从教材中摘录的顺序表实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
// This is the file to include in your code if you want access to the
// complete AList template class

// First, get the declaration for the base list class
#include "list.h"

// This is the declaration for AList. It is split into two parts
// because it is too big to fit on one book page
template <typename E> // Array-based list implementation
class AList : public List<E> {
private:
int maxSize; // Maximum size of list
int listSize; // Number of list items now
int curr; // Position of current element
E* listArray; // Array holding list elements

public:
AList(int size=defaultSize) { // Constructor
maxSize = size;
listSize = curr = 0;
listArray = new E[maxSize];
}

~AList() { delete [] listArray; } // Destructor

void clear() { // Reinitialize the list
delete [] listArray; // Remove the array
listSize = curr = 0; // Reset the size
listArray = new E[maxSize]; // Recreate array
}

// Insert "it" at current position
void insert(const E& it) {
Assert(listSize < maxSize, "List capacity exceeded");
for(int i=listSize; i>curr; i--) // Shift elements up
listArray[i] = listArray[i-1]; // to make room
listArray[curr] = it;
listSize++; // Increment list size
}

void append(const E& it) { // Append "it"
Assert(listSize < maxSize, "List capacity exceeded");
listArray[listSize++] = it;
}

// Remove and return the current element.
E remove() {
Assert((curr>=0) && (curr < listSize), "No element");
E it = listArray[curr]; // Copy the element
for(int i=curr; i<listSize-1; i++) // Shift them down
listArray[i] = listArray[i+1];
listSize--; // Decrement size
return it;
}
void moveToStart() { curr = 0; } // Reset position
void moveToEnd() { curr = listSize; } // Set at end
void prev() { if (curr != 0) curr--; } // Back up
void next() { if (curr < listSize) curr++; } // Next

// Return list size
int length() const { return listSize; }

// Return current position
int currPos() const { return curr; }

// Set current list position to "pos"
void moveToPos(int pos) {
Assert ((pos>=0)&&(pos<=listSize), "Pos out of range");
curr = pos;
}

const E& getValue() const { // Return current element
Assert((curr>=0)&&(curr<listSize),"No current element");
return listArray[curr];
}
};

二 链表

  链表是一种物理存储单元上非连续非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。它是由一系列称为表的结点的对象组成的。结点是一个独立的对象(而数组中的元素不是),它包含data域和next域。
  链表也分为静态动态两种,静态/动态特征与顺序表相同。后者采用较多。

(一)单链表

1.定义

  教材指出,单链表中的结点类定义如下:

1
2
3
4
5
6
7
8
9
10
// Singly linked list node
template <typename E> class Link {
public:
E element; // Value for this node
Link *next; // Pointer to next node in list
// Constructors
Link(const E& elemval, Link* nextval =NULL)
{ element = elemval; next = nextval; }
Link(Link* nextval =NULL) { next = nextval; }
};

  注意到,在该类中,结点只有一个指向表中下一结点的指针,所以称为单链表(或称单向表),如图F2所示。
F2.单链表示例
  图F2中:

  • 链表中的第一个结点通过名为head的指针访问;
  • 最后一个结点通过名为tail的指针访问;
  • 栅栏的位置由指针curr指示,并且指针curr指向逻辑上当前节点的前驱结点
  • 穿过指针变量方框的对角线表示NULL指针,它是不指向任何地方的指针值。

为什么指针curr不能指向逻辑上的当前结点?

  需要明确的是,插入的位置即为栅栏的位置。如果指针curr指向逻辑上的当前结点(如图F3所示),那么所插入结点(在图F3中的蓝色结点)的next域应当代之以curr的指针。注意到,所插入结点的前驱结点的next域应更新为指向所插入结点data域的指针,进行该操作需要首先找到该前驱结点的位置——由于在已知的指针中不存在满足这样要求的指针,这样就不能直接找到它。而如果改为指针curr指向逻辑上当前节点的前驱结点(如图F4所示),那么由于指针curr已知,因此修改它的next域极其容易,修改方法如下面的insert函数所示。

1
2
3
4
5
6
// Insert "it" at current position
void insert(const E& it) {
curr->next = new Link<E>(it, curr->next);
if (tail == curr) tail = curr->next; // New tail
cnt++;
}

F3.指针curr指向逻辑上的当前结点示例
F4.指针curr指向逻辑上当前节点的前驱结点示例

  此外,该类的数据成员都是公开的,这是因为它还要用于栈和队列的实现。

单击此处查看链表的实现

  下面是从教材中摘录的链表实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
// This is the file to include in your code if you want access to the
// complete LList template class

// First, get the declaration for the base list class
#include "list.h"
#define defaultSize 100
// This is the declaration for LList. It is split into two parts
// because it is too big to fit on one book page
// Linked list implementation
template <typename E> class LList: public List<E> {
private:
Link<E>* head; // Pointer to list header
Link<E>* tail; // Pointer to last element
Link<E>* curr; // Access to current element
int cnt; // Size of list

void init() { // Intialization helper method
curr = tail = head = new Link<E>;
cnt = 0;
}

void removeall() { // Return link nodes to free store
while(head != NULL) {
curr = head;
head = head->next;
delete curr;
}
}

public:
LList(int size=defaultSize) { init(); } // Constructor
~LList() { removeall(); } // Destructor
void print() const; // Print list contents
void clear() { removeall(); init(); } // Clear list

// Insert "it" at current position
void insert(const E& it) {
curr->next = new Link<E>(it, curr->next);
if (tail == curr) tail = curr->next; // New tail
cnt++;
}

void append(const E& it) { // Append "it" to list
tail = tail->next = new Link<E>(it, NULL);
cnt++;
}

// Remove and return current element
E remove() {
//Assert(curr->next != NULL, "No element");
E it = curr->next->element; // Remember value
Link<E>* ltemp = curr->next; // Remember link node(这一步非常重要,不能“丢失”结点的内存,而是应该先创建临时指针ltemp,它的next域指向被删除的结点)
if (tail == curr->next) tail = curr; // Reset tail
curr->next = curr->next->next; // Remove from list
delete ltemp; // Reclaim space(然后再使用delete函数释放被删除节点占用的内存)
cnt--; // Decrement the count
return it;
}

void moveToStart() // Place curr at list start
{ curr = head; }

void moveToEnd() // Place curr at list end
{ curr = tail; }

// Move curr one step left; no change if already at front
void prev() {
if (curr == head) return; // No previous element
Link<E>* temp = head;
// March down list until we find the previous element
while (temp->next!=curr) temp=temp->next;
curr = temp;
}

// Move curr one step right; no change if already at end
void next()
{ if (curr != tail) curr = curr->next; }

int length() const { return cnt; } // Return length

// Return the position of the current element
int currPos() const {
Link<E>* temp = head;
int i;
for (i=0; curr != temp; i++)
temp = temp->next;
return i;
}

// Move down list to "pos" position
void moveToPos(int pos) {
//Assert ((pos>=0)&&(pos<=cnt), "Position out of range");
curr = head;
for(int i=0; i<pos; i++) curr = curr->next;
}

const E& getValue() const { // Return current element
//Assert(curr->next != NULL, "No value");
return curr->next->element;
}
};

2.特殊的结点——表头结点

  注意到当链表为空时,没有元素可供指针head curr tail指向;栅栏左边部分为空时,指针curr也不能指向任何元素。这时可以增加表头结点来解决这些问题。
  表头结点时表中的第一个结点,它与表中其它元素一样,只是值被忽略,不被当成表中的实际元素。使用表头结点来初始化一个空链表如图F5所示。
F5.使用表头结点初始化的空链表示例

3.可利用空间表

  注意到在前面的描述中,存储的分配和回收使用newdelete来完成——这样的过程造成了大量的内存负担。在这里,通过使用Link类管理其可利用空间表,以取代反复调用newdelete
  可利用空间表存放当前那些不用的线性表结点,从一个链表中删除的结点就可以放到可利用空间表的首端。当需要把一个新元素增加到链表中时,先检查可利用空间表,看看是否有可用的线性表结点。如果有空结点,则从可利用空间表中取走一个结点。只有当可利用空间表为空时,才会调用标准操作符new。一个简单的实现办法是重载操作符来替代newdelete

单击此处查看包含可利用空间表的Link类实现

  下面是从教材中摘录的包含可利用空间表的Link类实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// Singly linked list node with freelist support
template <typename E> class Link {
private:
static Link<E>* freelist; // Reference to freelist head
public:
E element; // Value for this node
Link* next; // Point to next node in list

// Constructors
Link(const E& elemval, Link* nextval =NULL)
{ element = elemval; next = nextval; }
Link(Link* nextval =NULL) { next = nextval; }

void* operator new(size_t) { // Overloaded new operator
if (freelist == NULL) return ::new Link; // Create space
Link<E>* temp = freelist; // Can take from freelist
freelist = freelist->next;
return temp; // Return the link
}

// Overloaded delete operator
void operator delete(void* ptr) {
((Link<E>*)ptr)->next = freelist; // Put on freelist
freelist = (Link<E>*)ptr;
}
};

// The freelist head pointer is actually created here
template <typename E>
Link<E>* Link<E>::freelist = NULL;

  可利用空间表的初始状态下为。如果需要成千上万个链表结点,那么就可以重载new操作符,使其调用一次系统new操作符时便分配很多个链表结点。

  注意到freelist变量使用了static关键字——对于同一类型的线性表(或元素类型相同的多个链表),只需要一个可利用空间表。不同数据类型的Link类会使用不同的可利用空间表

(二)双链表

  单链表只允许从一个表结点直接访问它的后继结点,而双链表可以从一个表结点直接访问它的前驱结点(除头结点外)和后继结点(除尾结点外)(如图F6所示)。
F6.双链表示例
  注意到与单链表的区别:

  • 双链表存储了两个指针以访问前驱结点和后继结点;
  • 双链表不仅使用了头结点,还使用了尾结点(与头结点类似,不包含任何数据信息)。在双链表初始化时,头结点和尾结点都会被创建。

  注意到双链表中的结点(除头结点外)可以访问到前驱结点,这意味着指针curr可以指向逻辑上的当前结点。但是为了与单链表统一,指针curr仍指向逻辑上当前结点的前驱结点。

单击此处查看双链表的实现

  下面是从教材中摘录的双链表实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
// Doubly linked list link node with freelist support
template <typename E> class Link {
private:
static Link<E>* freelist; // Reference to freelist head

public:
E element; // Value for this node
Link* next; // Pointer to next node in list
Link* prev; // Pointer to previous node

// Constructors
Link(const E& it, Link* prevp, Link* nextp) {
element = it;
prev = prevp;
next = nextp;
}
Link(Link* prevp =NULL, Link* nextp =NULL) {
prev = prevp;
next = nextp;
}

void* operator new(size_t) { // Overloaded new operator
if (freelist == NULL) return ::new Link; // Create space
Link<E>* temp = freelist; // Can take from freelist
freelist = freelist->next;
return temp; // Return the link
}

// Overloaded delete operator
void operator delete(void* ptr) {
((Link<E>*)ptr)->next = freelist; // Put on freelist
freelist = (Link<E>*)ptr;
}

// Insert "it" at current position
void insert(const E& it) {
curr->next = curr->next->prev =
new Link<E>(it, curr, curr->next);
cnt++;
}

// Append "it" to the end of the list.
void append(const E& it) {
tail->prev = tail->prev->next =
new Link<E>(it, tail->prev, tail);
cnt++;
}

// Remove and return current element
E remove() {
if (curr->next == tail) // Nothing to remove
return NULL;
E it = curr->next->element; // Remember value
Link<E>* ltemp = curr->next; // Remember link node
curr->next->next->prev = curr;
curr->next = curr->next->next; // Remove from list
delete ltemp; // Reclaim space
cnt--; // Decrement cnt
return it;
}

// Move fence one step left; no change if left is empty
void prev() {
if (curr != head) // Can't back up from list head
curr = curr->prev;
}
};

// The freelist head pointer is actually created here
template <typename E>
Link<E>* Link<E>::freelist = NULL;

三 线性表实现方法的比较

  下面比较了顺序表和链表这两种线性表的实现方法,如表T1所示。

顺序表 链表
空间分配 顺序表的空间一般事先固定,这就导致元素较少时造成空间浪费(动态扩容也一般成倍扩容,仍然造成空间浪费)。 只有实际需要时才进行空间分配。
结构性开销 无。 链表所附加的指针就是结构性开销。如果datadata域占据的空间较小,那么结构性开销就占去了整个存储空间的一大部分。
插入/删除操作 需要移动大量元素。 利用指针更新。

T1.顺序表和链表的比较

  进一步地,如果设线性表中当前元素的数目为nn,指针的存储单元大小(通常为4字节)为PP,数据元素的存储单元大小为EE,数组中存储的线性表元素的最大数目为DD,则顺序表的空间需求为DEDE,链表的空间需求(不考虑任何给定时刻链表中实际存储的元素数目,例如不考虑可利用空间表)为n(P+E)n(P+E)。那么,对于给定的nn值,当

n>DEP+En>\frac{DE}{P+E}

  时,顺序表的空间效率更高,否则链表的空间效率更高。特别地,当P=EP=E(如数据域为一个4字节的int类型或指针类型且指针域为正常指针时)时,

n>D2n>\frac{D}{2}

  即当数组超过半满时,且在链表的链域和元素域的大小相同时,顺序表效率更高

  一般地,当线性表元素数目变化较大或者未知时,最好使用链表实现;如果事先知道线性表的大致长度,使用顺序表的空间效率更高。


四 元素的副本 or 指向元素的指针

  这是在需要在线性表中添加重复元素时需要考虑的问题,选择何种方式依赖于实际应用。一般来说,元素越大且重复越多,使用指向元素的指针更好,这样可以防止浪费空间来创建大量副本(多个表元素可以通过一个指针指向同一条记录,大大节省了空间)。