线性表ADT设计
线性表ADT中包含的关键设计是对当前位置(current position)的支持,当前位置是指线性表操作(如插入和删除)将会作用的位置。
template <typename E> class List
{
private:
void operator =(const List&) {}
List(const List&) {}
public:
List() {}
virtual ~List() {}
//清空
virtual void clear() = 0;
//增删
virtual void insert(const E& item) = 0;
virtual void append(const E& item) = 0;
virtual E remove() = 0;
//移动
virtual void moveToStart() = 0;
virtual void moveToStart() = 0;
virtual void moveToPos(int pos) = 0;
virtual void prev() = 0;
virtual void next() = 0;
//表的属性及内容
virtual int length() const = 0;
virtual const E& getValue() const = 0;
//被const修饰的常成员函数,在其中不得修改类中任何数据成员的值
};
使用线性表模板实现的查找函数
bool find(List<int>& L, int K)
{
int it;
for (L.moveToStart(); L.currPos() < L.length(); L.next())
{
it = L.getvalue();
if (K == it) return true;
}
return false;
}
注:尽管这个find函数可以改成模板的形式,从而可以处理不同的数据类型,但是依然会收到限制。具体来说,只有当线性表中元素的数据类型和待查元素的数据类型相同时,并且在该数据类型重载了等价运算符 “ == ” 的情况下,该函数才能工作。
顺序表
template <typename E>
class Alist : public List<E>
{
private:
int maxSize;
int listSize;
int curr;
E* listArray; //首地址
public:
AList(int size = defaultSize)
{
maxSize = size;
listSize = curr = 0;
listArray = new E[maxSize]; //初始化并分配内存空间
}
}
~AList() { delete[] listArray; }
void clear()
{
delete[] listArray;
listSize = curr = 0;
listArray = new E[maxSize];
void insert(const E& it)
{
Assert(listSize < maxSize, "List capacity exceeded");
for (int i = listSize; i > curr; i--)
listArray[i] = listArray[i - 1];
listArray[curr] = it;
listSize++;
}
...
};
AList继承了List类,需要实现其所有虚函数。从insert函数可以看出,实现AList的关键点在于“当前存储量”和“当前位置”的维护。
单链表
相对于顺序表的物理上的强制顺序存储,链表是由一个个自定义的结点组织而成,不强制物理上的连续性,因此在插入、删除操作上更加灵活,效率更高
单链表结点类Link定义:
template <typename E> class Link
{
public:
E element; //数据域
Link *next; //指针域
// 表中结点创建
Link(const E& elemval, Link* nextval = NULL)
{
element = elemval;
next = nextval;
}
// 表头结点创建
Link(Link* nextval = NULL)
{
next = nextval;
}
};
注:这个Link结点的定义十分重要,我们在以后介绍其他线性结构时都会遇到它!
单链表实现:
template <typename E> class LList : public List<E>
{
private:
Link<E>* head;
Link<E>* tail;
Link<E>* curr;
int cnt;
void init()
{
curr = tail = head = new Link<E>;
cnt = 0;
}
void removeall()
{
while (head != NULL)
{
curr = head;
head = head->next;
delete curr; //一切操作都是对当前位置的操作
}
}
public:
LList(int size = defaultSize) { init(); }
~LList() { removeall(); }
void clear() { removeall(); init(); }
// 关键函数实现
void insert(const E& it)
{
}
void append(const E& it)
{
}
E remove()
{
}
void moveToStart()
{ }
void moveToEnd()
{ }
void prev() //前驱
{
}
void next() //后继
{
}
void moveToPos(int pos)
{
}
// 链表当前相关参数
int length() const { return cnt; }
int currPost() const
{
Link<E>* temp = head;
int i;
for (int i = 0; curr != temp; i++)
temp = temp->next;
return i;
}
const E& getValue() const //Return current element
{
Assert(curr->next != NULL; "No value");
return curr->next->element;
}
};
注
- 为了
操作的便利性
以及考虑对特殊情况的一致性处理
,本例的设计考虑了如下两个策略:
i) 让curr指针指向当前元素
的前一个元素
ii) 增加特殊的表头结点
- 单链表的实现关键在于对于
head
,tail
,curr
指针的维护 - 在关键函数的实现上需要考虑特殊情况的处理,本文一下内容将对于这些函数的具体实现做一个探讨
insert函数实现
由图可知,insert的操作分为三步:
- 创建新结点
- 将新结点的指针指向当前位置
- 断开当前位置的前驱与当前位置结点的指针并将当前位置的前驱结点的指针指向新结点
下面来考虑代码:
首先创建结点
new Link<E>(it)
然后将新结点指针指向当前位置
(此处由于有前文Link结点类提供的函数我们可以便利的将1,2步合并为一步)
new Link<E>(it, curr->next)
最后将前驱的指针指向新结点
curr->next = new Link<E>(it, curr->next)
然后将cnt++即完成了insert函数的基本实现
但是此处应思考一个问题,若是当前位置是表尾,这时完成插入操作后表尾指针tail指向了表中最后第二个元素。
所以应在插入完成后加入如下语句:
if (tail == curr) tail = curr->next; // New tail
insert完整实现
void insert(const E& it)
{
curr->next = new Link<E>(it, curr->next);
if (tail == curr) tail = curr->next; // New tail
cnt++;
}
注 remove(), prev()等函数的实现思考方式类似,本文不做实现,留给读者练习。
对于链表更详尽的讨论,可以参见博主的另一篇博文线性表(三) 链表