线性表是最基本、最简单、也是最常用的一种数据结构。线性表(linear list)是数据结构的一种,线性表是n个数据元素构成的有限序列,表中元素的数量为表的长度。长度为零的表为空表。
特点:
-
集合中必存在唯一的一个“第一元素”。
-
集合中必存在唯一的一个 “最后元素” 。
-
除最后一个元素之外,均有唯一的后继(后件)。
-
除第一个元素之外,均有唯一的前驱(前件)。
逻辑结构图:
a1————a2————a3————a4--------------an
存储结构:
一、顺序存储
1.顺序表
使用数组实现,一组地址连续的存储单元,数组大小有两种方式指定,一是静态分配,二是动态扩展。
注:线性表从1开始,而数组从0开始。
优点:
- 有随机访问特性,查找O(1)时间
- 无需额外空间,存储密度高
缺点:
- 插入和删除操作需移动大量元素
- 表的容量难以确定
顺序表的实现
用C++的类来实现顺序表,由于数据类型不确定,所以采用模板机制。
const int MAXSIZE = 1e5;
template<typename T>
class SeqList {
T data[MAXSIZE];
int length;
public:
SeqList();
SeqList(T a[], int n);
~SeqList();
int GetLength();//求线性表的长度
T Get(int index);//按索引查找
int Locate(T x);//按值查找
void Insert(T x, int index);//插入
T Delete(int index);//按位置删除
bool Empty();//判断是否为空
void printSetList();//遍历输出
};
无参构造函数—初始化顺序表
建立一个空的顺序表,将length初始化为0即可。
template<typename T>
SeqList<T>::SeqList()
{
length = 0;
}
有参构造函数—建立顺序表
将数据元素传入顺序表中,数据元素个数即为顺序表长度。如果顺序表的存储空间小于给定的元素个数,此时无法建立顺序表。
template<typename T>
SeqList<T>::SeqList(T a[], int n)
{
if (n > MAXSIZE)throw"非法参数!";
for (int i = 0; i < n; i++)
data[i] = a[i];
length = n;
}
析构函数—销毁顺序表
顺序表是静态储存分配,自动释放内存。所以析构函数为空。
template<typename T>
SeqList<T>::~SeqList()
{
//空
}
求线性表的长度
返回length的值即可
template<typename T>
int SeqList<T>::GetLength()
{
return length;
}
按位查找
顺序表下表为i的元素储存在数组中下标为i-1的位置。时间复杂度为O(1)。
template<typename T>
T SeqList<T>::Get(int index)
{
if (index < 0 || index >= length)
throw"非法输入!";
return data[index - 1];
}
按值查找
需要对顺序表中的元素进行比较,查找成功返回序号(下标+1),否则返回0。时间复杂的为O(n)。
template<typename T>
int SeqList<T>::Locate(T x)
{
for (int i = 0; i < length; i++)
if (data[i] == x)
return i + 1;
return 0;
}
插入操作
输人:插入位置i,待插入的元素值x
输出:如果插人成功,返回新的顺序表,否则返回插入失败信息
1.如果表满了,则输出上溢错误信息,插入失败;
2.如果元素的插人位置不合理,则输出位置错误信息,插人失败;
3.将最后一个元素直至第1个元素分别向后移动一个位置;
4.将元素x填人位置i处;
5.表长加1;
时间复杂度为O(n)
template<typename T>
void SeqList<T>::Insert(T x, int index)
{
if (length = MAXSIZE)throw("上溢!");
if (index < 0 || index >= length)throw("非法输入!");
for (int i = length; i >= index; i--)
data[i] = data[i - 1];
data[index - 1] = x;
length++;
}
删除操作
从删除位置开始,将数据元素前移一位即可,注意下标的表示。时间复杂度为O(n)
template<typename T>
T SeqList<T>::Delete(int index)`在这里插入代码片`
{
if (length = 0)throw("下溢!");
if (index < 0 || index >= length)throw("删除位置错误!");
T x = data[index - 1];
for (int i = index; i < length; i++)
data[i - 1] = data[i];
length--;
return x;
}
判断顺序表是否为空
判断length的值即可
template<typename T>
bool SeqList<T>::Empty()
{
if (length > 0)
return true;
return false;
}
遍历输出
template<typename T>
void SeqList<T>::printSetList()
{
for (int i = 0; i < length; i++)
{
cout << data[i] << " ";
if (i % 5 == 0)
cout << endl;
}
}
二、链式存储
1.单链表
单链表是用一组任意的存储单元存放线性表的元素,这组存储单元可以连续也可以不连续,甚至可以零散分布在内存中的任意位置。
每个存储单元在存储数据元素的同时,还必须存储其后继元素所在的地址信息,这个地址信息称为指针。这两部分组成了数据元素的存储映象,称为结点。
每个结点的存储地址存放在其前驱结点的next域中,而第一个元素无前驱,所以设头指针指向第一个元素所在结点(称为开始结点),整个单链表的存取必须从头指针开始进行,因而头指针具有标识一个单链表的作用。最后一个元素无后继,故最后一个元素所在结点(称为终端结点)的指针域为空,这个空指针称为尾标志。
通常在单链表的开始结点之前附设一个类型相同的结点,称为头结点。也有不带头结点的单链表,不过其操作更为麻烦,所以不经常使用,我们下面会简单介绍。如不特殊说明,使用的链表都是默认带头结点的。
单链表的实现
结点的定义
data为数据区,存放数据
next为指针域,存放下一个结点的地址
template<typename T>
struct Node
{
T data;
Node<T>* next;
};
声明:
template<typename T>
class LinkList {//有首结点
Node<T>* first;//头指针
public:
LinkList();
LinkList(T a[], int n);//头插法
LinkList(int n,T a[])//尾插法
T Get(int index);//按位查找
int Locate(T x);//按值查找
void Insert(int index, T x);//插入
T Delete(int index);//删除
void Delete_Data(T x);//按值删除
void PrintList();//遍历输出
int Length();//链表长度
int Empty();//判断链表是否为空
~LinkList();//析构函数
};
无参构造
带头结点
创建一个带首节点的空链表
给first指针申请一块内存,将其指针域赋空值(代表无后继结点),即空链表
template<typename T>
LinkList<T>::LinkList()
{
first = new Node<T>;
first->next = NULL;
}
不带头结点
直接把头指针赋空值
template<typename T>
LinkList<T>::LinkList()
{
first = NULL;
}
有参构造(头插法)
带头结点
头插法就是把结点插入到首节点之后
先建立结点s储存数据
将s的指针域指向原来的第一个结点,即first->next
现在s成为了第一个节点
将first->next指向s
template<typename T>
LinkList<T>::LinkList(T a[], int n)
{
first = new Node<T>;
first->next = NULL;
Node<T>* s = NULL;
for (int i=0; i < n; i++)
{
s = new Node<T>;
s->data = a[i];
s->next = first->next;
first->next = s;
}
}
不带头节点
把插入的节点作为首结点
template<typename T>
LinkList<T>::LinkList(T a[], int n)
{
first = NULL;
Node<T>* s = new Node<T>;
for (int i = 0; i < n; i++)
{
s = new Node<T>;
s->data = a[i];
s->next = first;
first = s;
}
}
有参构造(尾插法)
带头结点
尾插法就是在链表的尾部插入节点
为了操作方便,这里需要增加一个尾指针last
定义一个结点s储存信息
将尾指针的指针域指向新插入的s
现在结点s才是真正的队尾
然后再将尾指针指向s
所有元素都插入后,将尾指针的next赋空值
template<typename T>
LinkList<T>::LinkList(int n, T a[])
{
first = new Node<T>;
first->next = NULL;
Node<T>* s = NULL;
Node<T>* last = first;
for (int i = 0; i < n; i++)
{
s = new Node<T>;
s->data = a[i];
last->next = s;
last = s;
}
last->next = NULL;
}
不带头节点
操作相对来说较复杂一些,第一个节点和后续结点的操作不同
需要先插入第一个结点,再循环插入后续结点
template<typename T>
LinkList<T>::LinkList(T a[],int n)
{
Node<T>* s = new Node<T>;
s->data = a[0];
first = s;
Node<T>* last = first;
for (int i = 1; i < n; i++)
{
s = new Node<T>;
s->data = a[i];
last->next=s;
last = s;
}
last->next = NULL;
}
按位置查找值
这里需要一个工作指针p和一个计数器count来计数
当count==index-1时,此时的p就是要查找的结点
输出p->data
template<typename T>
T LinkList<T>::Get(int index)
{
int count = 0;//计数器
Node<T>* p = first->next;//工作指针
while (p && count < index-1)
{
count++;
p = p->next;//工作指针后移
}
if (!p)throw"查找失败";
return p->data;
}
按值查找位置
还是需要计数器count来计数,当p->next==x时,输出此时的count
template<typename T>
int LinkList<T>::Locate(T x)
{
Node<T>* p = first->next;
int count = 1;
while (p)
{
if (p->data == x)return count;//查找成功
count++;
p = p->next;
}
if (!p)throw"查找失败";
}
插入操作
有首节点
使用结点s来记录数据
插入需要查找到第i-1个结点,然后再修改指针变量的值
将s->next指向下一个结点,即第i-1个结点储存的地址(next)
然后将第i-1个结点的next指向s
template<typename T>
void LinkList<T>::Insert(int index, T x)
{
Node<T>* p = first;
int count = 0;
while (p && count < index - 1)
{
p = p->next;
count++;
}
if (p)
{
Node<T>* s = new Node<T>;
s->data = x;
s->next = p->next;
p->next = s;
}
else
throw"插入失败";
}
无首结点
需要判断插入的位置,插入在第一个元素的操作不同
template<typename T>
void LinkList<T>::Insert(int index, T x)
{
Node* p=NULL;
Node* s = NULL;
int count;
if (index <= 0) throw"位置非法";
if (index == 1)
{
s = new Node<T>;
s->data = x;
s->next = first;
first = s;
return;
}
p = first;
count = 1;
while (p && count < i - 1)
{
p = p->next;
count++;
}
if (!p)throw"位置非法";
else
{
s = new Node<T>;
s->data = x;
s->next = p->next;
p->next = s;
}
}
按位置删除
删除操作也是需要找到第i-1个结点
然后将第i-1个结点的next指向第i+1个结点,即第i个结点的next
然后释放第i个结点的内存
template<typename T>
T LinkList<T>::Delete(int index)
{
T x;
Node<T>* p = first;
Node<T>* q = NULL;
int count = 0;
while (p && count < index - 1)
{
p = p->next;
count++;
}
if (!p || !p->next)"删除失败";
else
{
q = p->next;
x = q->data;
p->next = q->next;
delete q;
return x;
}
}
按值删除
先查找到和数据相等的值所在的结点p
再修改其前驱结点p的的指针域
void LinkList::Delete_Data(int x)//按值删除
{
Node* p = first;//前驱
Node* q = NULL;//后继
Node* temp = NULL;
bool flag = false;
while (p->next)
{
if (p->next->data == x)
{
q = p->next;
p->next = q->next;
delete q;
flag = true;
}
p = p->next;
}
if (!flag)throw"删除失败";
}
求链表的长度
遍历计数即可
template<typename T>
int LinkList<T>::Length()
{
Node<T>* p = first->next;
int count = 1;
while (p)
{
count++;
p = p->next;
}
return count;
}
判断链表是否为空
判断first->next是否为空
template<typename T>
int LinkList<T>::Empty()
{
if (first->next == NULL)
return 1;
return 0;
}
输出链表
遍历输出即可
template<typename T>
void LinkList<T>::PrintList()
{
Node<T>* p = first->next;
while (p)
{
cout << p->data <<" ";
p = p -> next;
}
cout << endl;
}
析构函数
释放内存
template<typename T>
LinkList<T>::~LinkList()
{
Node<T>* p = NULL;
while (first)
{
p = first->next;
delete first;
first = p;
}
}
2.循环链表
将单链表的头尾结点连接起来,就是一个循环链表。没有增加额外的开销,为链表的操作增添了不少便利。
特点:从链表的任意一点出发,都能访问到表中其他节点。
整体的方法和单链表的差异不大,只要稍作修改即可。
构造空表
首尾相连
template<typename T>
CycleLinkList<T>::CycleLinkList()
{
first = new Node<T>;
first->next = first;
}
有参构造(头插法)
带头结点
和单链表构造的唯一差距就是将其中一条语句修改
first->next=first
template<typename T>
LinkList<T>::LinkList(T a[], int n)
{
first = new Node<T>;
first->next = first;//修改
Node<T>* s = NULL;
for (int i=0; i < n; i++)
{
s = new Node<T>;
s->data = a[i];
s->next = first->next;
first->next = s;
}
}
有参构造(尾插法)
带头结点
last->next=first
template<typename T>
LinkList<T>::LinkList(int n, T a[])
{
first = new Node<T>;
first->next = NULL;
Node<T>* s = NULL;
Node<T>* last = first;
for (int i = 0; i < n; i++)
{
s = new Node<T>;
s->data = a[i];
last->next = s;
last = s;
}
last->next = first;
}
将单链表变为循环链表
Node*p=first;
while(p->next)
{
p=p->next;
}
p->next=first;
其他的操作与单链表的操作基本相同们这里不再赘述。
3.双链表
在双链表中,每个结点不仅记录了数据元素,还储存了前驱结点和后继结点的地址信息。能方便的找到前驱节点。
结点构成
template<typename T>
struct Node
{
T data;
Node<T>* pre;
Node<T>* next;
};
声明
template<typename T>
class DoubleLink {
Node<T>* first;
public:
DoubleLink();
DoubleLink(T a[], int n);
T Get(int index);
int Get_data(T x);
T Delete(int index);
T Delete_data(T x);
void Insert(int index, T x);
int Length();
void Print();
~DoubleLink();
};
无参构造
template<typename T>
DoubleLink<T>::DoubleLink()
{
first = new Node<T>;
first->pre = NULL;
first->next = NULL;
}
头插法构造
先正向修改指针,再反向修改指针,同时注意判断p->next是否存在
template<typename T>
DoubleLink<T>::DoubleLink(T a[],int n)
{
first = new Node<T>;
first->pre = NULL;
first->next = NULL;
Node<T>* s = NULL;
for (int i = 0; i < n; i++)
{
s = new Node<T>;
s->data = a[i];
s->next = first->next;
first->next = s;
s->pre = first;
if(s->next)
s->next->pre = s;
}
}
尾插法构造
利用尾指针进行操作
template<typename T>
DoubleLink<T>::DoubleLink(T a[], int n)
{
first = new Node<T>;
first->pre = NULL;
first->next = NULL;
Node<T>* s = NULL;
Node<T>* last = first;
for (int i = 0; i < n; i++)
{
s = new Node<T>;
s->data = a[i];
last->next = s;
s->pre = last;
last = s;
}
last->next = NULL;
}
按位置查找值
和单链表操作相同
template<typename T>
T LinkList<T>::Get(int index)
{
int count = 0;//计数器
Node<T>* p = first->next;//工作指针
while (p && count < index-1)
{
count++;
p = p->next;//工作指针后移
}
if (!p)throw"查找失败";
return p->data;
}
按值查找位置
和单链表操作相同
template<typename T>
int LinkList<T>::Locate(T x)
{
Node<T>* p = first->next;
int count = 1;
while (p)
{
if (p->data == x)return count;//查找成功
count++;
p = p->next;
}
if (!p)throw"查找失败";
}
插入操作
和单链表基本相同,就只有修改指针操作不同
template<typename T>
void DoubleLink<T>::Insert(int index, T x)
{
if (index <= 0)throw"位置非法";
Node<T>* p = first;
int count = 0;
while (p&&count<index-1)
{
p = p->next;
count++;
}
if (p)
{
Node<T>* s = new Node<T>;
s->data = x;
s->next = p->next;
p->next = s;
s->pre = p;
if (s->next)
s->next->pre = s;
}
else
throw"位置非法"
}
按位置删除
和单链表稍微有些不同
不需要找前驱节点,直接找到第index个结点就可以
template<typename T>
T DoubleLink<T>::Delete(int index)
{
T x;
Node<T>* p = first->next;
int count = 0;
while (p && count < index - 1)
{
p = p->next;
count++;
}
if (!p || !p->next)"删除失败";
else
{
p->pre->next = p->next;
if(p->next)
p->next->pre = p->pre;
delete p;
return x;
}
}
按值删除
思路和单链表基本上相同,知识细微部分不同
需要注意判断p->next是否为空
void List::Delete(int x)
{
Node* p=NULL, * q=NULL;
p = first->next;
while (p)
{
if (p->data == x)
{
q = p;
p->pre->next = p->next;
if (p->next != NULL)
p->next->pre = p->pre;
delete q;
}
p = p->next;
}
}
遍历输出
和单链表操作相同
template<typename T>
void DoubleLink<T>::Print()
{
Node<T>* p = first->next;
while(p)
{
cout << p->data << " ";
p = p->next;
}
cout << endl;
return;
}
求链表的长度
遍历计数,和单链表相同
template<typename T>
int LinkList<T>::Length()
{
Node<T>* p = first->next;
int count = 1;
while (p)
{
count++;
p = p->next;
}
return count;
}
析构函数
和单链表相同
template<typename T>
DoubleLink<T>::~DoubleLink()
{
Node<T>* p = NULL;
while (first)
{
p = first->next;
delete first;
first = p;
}
}
总的来说,双链表的操作的实现思路和单链表相同,操作上大同小异。
三、顺序表和链表的比较
时间性能:
若线性表的操作主要是查找,很少进行删除、插入时,可以选择顺序表来储存。
若进行频繁的插入和删除操作,适合选择链表来储存。
空间性能:
当线性表的长度变化不大,且事先容易确定其大小时,适合采用顺序表来存储,否则使用链表。
如果你觉得看完这篇文章有收获,就动动小手点个赞吧!