顺序表利用数组元素在物理位置(即数组下标)上的邻接关系来表示线性表中数据元素之间的逻辑关系,这使得顺序表具有以下缺点:
① 插入和删除操作需要移动大量元素。在顺序表上做插入和删除操作(时间复杂度为O(n)),等概率下情况下,平均需要移动表中一半的元素。
② 表的容量难以确定。由于数组的长度必须事先确定(动态分配可解决该问题),因此,当线性表的长度变化较大时,难以确定合适的存储规模。
③ 造成存储空间的“碎片”。数组要求占用连续的存储空间,即使存储单元数超过所需的数目,如果不连续也不能使用,造成存储空间的“碎片”现象。
单链表
template <typename DataType>
struct Node{
DataType data; // 数据域
Node<DataType> *next; // 指针域
};
通常在单链表的开始节点之前附设一个类型相同的节点,称为头节点(head node),加上头节点以后,无论单链表是否为空,头指针始终指向头节点,因此空表和非空表的处理也变得统一。
template <typename DataType>
class LinkList{
public:
LinkList(); // 建立只有头节点的空链表
LinkList(DataType a[],int n); // 建立n个元素的单链表
~LinkList(); // 析构函数
int Length(); // 求单链表的长度
DataType Get(int i); // 按位查找,查找第i个节点的元素值
int Locate(DataType x); // 按值查找,查找值为x的元素序号
void Insert(int i,DataType x); // 插入操作,第i个位置插入值为x的节点
DataType Delete(int i); // 删除操作,删除第i个节点
bool Empty(); // 判断线性表是否为空
void PrintList(); // 遍历操作,按序号依次输出各元素
private:
Node<DataType> *first; //单链表的头指针
};
1.无参构造函数
template <typename DataType>
LinkList<DataType>::LinkList(){
first = new Node<DataType>; // 生成头节点
first->next = nullptr; // 头节点的指针域置空
}
2.有参构造函数
(1) 头插法
如果a[5]={1,2,3,4,5},则经过头插法构造函数之后,遍历操作将打印"5,4,3,2,1"。
template <typename DataType>
LinkList<DataType>::LinkList(DataType a[],int n){
first = new Node<DataType>;
first->next = nullptr;
for (int i=0; i<n; i++){
Node<DataType> *s = nullptr;
s= new Node<DataType>; s->data = a[i];
s->next = first->next; first->next = s; // 将节点s插入头节点后
}
}
(2) 尾插法
如果a[5]={1,2,3,4,5},则经过尾插法构造函数之后,遍历操作将打印"1,2,3,4,5"。
template <typename DataType>
LinkList<DataType>::LinkList(DataType a[],int n){
first = new Node<DataType>;
Node<DataType> *r = first, *s = nullptr; // 尾指针初始化
for (int i=0; i<n; i++){
s = new Node<DataType>; s->data = a[i];
r->next = s; r = s; // 将节点s插入到终端节点之后
}
r->next = nullptr; // 单链表建立完毕,将终端节点的指针域置空
}
3.析构函数
template <typename DataType>
LinkList<DataType>::~LinkList(){
Node<DataType> *p = first;
while (first != nullptr){ // 释放每一个节点的存储空间
first = first->next;
delete p;
p = first;
}
}
4.求单链表的长度
template <typename DataType>
int LinkList<DataType>::Length(){
Node<DataType> *p = first->next; // 工作指针p的初始化
int count = 0;
while (p != nullptr){
p = p->next;
count++;
}
return count;
}
**5.按位查找 **O(n)
template <typename DataType>
DataType LinkList<DataType>::Get(int i){
Node<DataType> *p = first->next; // 工作指针的初始化
int count = 1;
while (p!=nullptr && count<i){
p = p->next;
count++;
}
if (p == nullptr)
throw "illegal search location";
else
return p->data;
}
**6.按值查找 **O(n)
template <typename DataType>
int LinkList<DataType>::Locate(DataType x){
Node<DataType> *p = first->next; // 工作指针的初始化
int count = 1;
while (p != nullptr){
if (p->data == x)
return count;
p = p->next;
count++;
}
return -1; // 退出循环表面查找失败
}
**7.插入操作 **O(n)
template <typename DataType>
void LinkList<DataType>::Insert(int i,DataType x){
Node<DataType> *p = first, *s = nullptr;
int count = 0;
while (p != nullptr && count<i-1){
// 查找第(i-1)个节点
p = p->next;
count++;
}
if(p == nullptr)
throw "illegal insertion location"; // 没有找到第(i-1)个节点
else {
s = new Node<DataType>; s->data = x; // 申请节点s,数据域为x
s->next = p->next; p->next = s; // 将节点s插入到节点p之后
}
}
**8.删除操作 **O(n)
template <typename DataType>
DataType LinkList<DataType>::Delete(int i){
DataType x;
Node<DataType> *p = first, *q = nullptr;
int count = 0;
while (p!=nullptr && count<i-1){
// 查找第(i-1)个节点
p = p->next;
count++;
}
if (p==nullptr || p->next==nullptr)
throw "illegal deleted location";
else {
q = p->next; x = q->data; // 暂存被删节点
p->next = q->next; // 摘链
delete q;
return x;
}
}
关于判断条件p->next==nullptr,是因为如果删除的元素是最后一个元素的下一个(不存在),此时p!=nullptr,但仍然是删除位置错误,故增加此判断条件。
9.判断线性表是否为空
template <typename DataType>
bool LinkList<DataType>::Empty(){
return first->next==nullptr?true:false;
}
10.遍历操作
template <typename DataType>
void LinkList<DataType>::PrintList(){
Node<DataType> *p = first->next; // 工作指针p的初始化
while (p != nullptr){
cout << p->data << "\t";
p = p->next;
}
cout << endl;
}
双链表
如果希望快速确定单链表中任一结点的前驱结点,可以在单链表的每个结点中再设置一个指向其前驱结点的指针域,这样就形成了双链表(double linked list)。和单链表类似,双链表一般也是由头指针唯一确定,增加头结点也能使双链表的某些操作变得方便。
template <typename DataType>
struct Node{
DataType data;
Node<DataType> *prior,*next; // 前驱,后继指针域
};
在双链表中求表长、按位查找、按值查找、遍历等操作的实现与单链表基本相同,下面讨论插入和删除操作。
1. 插入操作
在结点p的后面插入一个新结点s,需要修改4个指针:
① s->prior = p;
② s->next = p->next;
③ p->next->prior = s;
④ p->next = s;
在修改第②和③布的指针时,要用到p->next以找到结点p的后继结点,所以第④步指针的修改要在第②和③的指针修改完成后才能进行。
2. 删除操作
设指针p指向待删除结点,删除操作可通过下述三条语句完成:
① p->prior->next = p->next;
② p->next->prior = p->prior;
③ delete p;
前两句语句的顺序可以颠倒。
循环链表
在单链表中,如果将终端结点的指针由空指针改为指向头结点,就使得整个单链表形成一个环,这种头尾相接的单链表称为循环单链表(circular singly linked list),在用头指针指示的循环单链表中,找到开始结点的时间为O(1),但找到终端结点的时间为O(n),如果改用指向终端结点的尾指针来指示循环单链表,那寻找开始结点以及终端结点的时间都为O(1),它们的存储地址分别为rear->next->next和rear,显然时间都为O(1)。因此,实际应用中多采用尾指针指示的循环单链表。
循环链表没有增加任何存储量,仅仅对链表的链接方式稍作改变,因此,基本操作的实现和链表类似。从循环链表中任一结点出发,可扫描到其他结点,从而提高链表操作的灵活性。但这种方法的危险在于循环链表中没有明显的尾端,可能会使循环链表的处理操作进入死循环,所以,需要格外注意循环条件,通常判断用作循环变量的工作指针是否等于某一指定指针(如头指针或尾指针),以判定工作指针是否扫描了整个循环链表。例如,在循环链表的遍历算法中,用循环条件p!=first判断工作指针p是否扫描了整个链表。
静态链表
静态链表用数组来表示链表,用数组元素的下标来模拟链表的指针。由于利用数组定义的链表,属于静态存储分配,因此叫作静态链表。最常用的时静态单链表,在不致混淆的情况下,将静态单链表简称为静态链表,存储示意图如下图所示,其中avail是空闲链表(全部由空闲数组单元组成的单链表)头指针,first是静态链表的头指针,为了运算方便,静态链表也带头指针。
静态链表的每个数组元素由两个域构成:data域存放数据元素,next域存放该元素的后继元素所在的数组下标。
template <typename DataType>
struct SNode{
DataType data;
int next; // 指针域(也称游标),注意不是指针类型
};
静态链表的实现
const int MaxSize = 100; // 100是示例数据,根据实际问题具体定义
template <typename DataType>
class StaList{
public:
StaList(); // 构造函数,初始化空的静态链表和空闲链表
StaList(DataType a[],int n); // 构造函数,建立长度为n的静态链表
~StaList(); // 析构函数
// 与单链表成员函数相同
private:
SNode SList[MaxSize]; // 静态链表数组
int first,avail; // 游标,链表头指针和空闲头指针
};
静态链表采用静态存储分配,因此析构函数为空。求表长、按位查找、按值查找、遍历等操作的实现与单链表基本相同,下面讨论插入和删除操作。
1、插入操作
在静态链表中进行插入操作,首先从空闲链的最前端摘下一个结点,将该结点插入静态链表中,假设新结点插在结点p的后面,则修改指针的操作为:
s = avail; // 不用申请结点,利用空闲链的第一个结点
avail = SList[avail].next; // 空闲链的头指针后移
SList[s].data = x; // 将x填入下标为s的结点
SList[s].next = SList[p].next; // 将下标为s的结点插入到下标为p的结点后面
SList[p].next = s;
2、删除操作
在静态链表中进行删除操作,首先将被删除结点从静态链表中摘下,再插入到空闲链的最前端,假设要删除结点p的后继结点,则修改指针的操作为:
q = SList[p].next; // 暂存被删结点的下标
SList[p].next = SList[q].next; // 摘链
SList[q].next = avail; // 将结点q插在空闲链avail的最前端
avail = q; // 空闲链头指针avail指向结点q
静态链表总结:
静态链表虽然用数组来存储线性表,但在执行插入和删除操作时,只需移动游标,无须移动表中的元素,从而改进了在顺序表中插入和删除操作需要移动大量元素的缺点。
顺序表和链表的比较
时间性能比较
1、若线性表需要频繁查找却很少进行插入和删除操作,或者操作和“数据元素在线性表中的位置”密切相关时,宜采用**线性表**作为存储结构;
2、若线性表需要频繁进行插入和删除操作,宜采用**链表**作为存储结构。
空间性能比较
1、如果事先知道线性表的大致长度,使用**顺序表**的空间效率会更高;
2、当线性表中元素个数变化较大或者未知时,最好使用**链表**实现。