线性表的基本概念
- 线性表特点:数据元素之间仅具有单一前驱和后继关系,一个线性表中的数据元素类型必须相同。
- 线性表的存储结构:顺序存储和链式存储。
顺序存储结构
- 定义:在内存中用地址连续的一块存储空间顺序存放线性表的各元素。
- 地址计算:设顺序表的第一个元素的存储地址(首地址)为Loc(a),每个数据元素占d个存储单元,则第i个数据元素的地址为Loc(n)=Loc(a)+(i-1)*d (1<=i<=n)
- 顺序表可以随机存储(顺序表是一种数据结构,用于存储一系列元素,并且这些元素在内存中是按照一定的顺序排列的。在顺序表中,每个元素占据一定的内存空间,并且它们在内存中是连续存储的,也就是说相邻的元素在内存中的地址是相邻的。通俗地说,可以把顺序表类比成一排排的抽屉,每个抽屉里放着一个元素。这些抽屉是按照顺序排列的,你可以根据抽屉的位置(索引)来快速找到对应的元素。例如,如果你想要找到第三个抽屉里的元素,你可以直接打开第三个抽屉,而不需要打开其他抽屉。由于顺序表中的元素在内存中是连续存储的,所以可以通过计算元素的地址来快速访问它们。假设顺序表的起始地址是A,每个元素占据的内存空间是固定的,例如每个元素占据4个字节,那么第一个元素的地址就是A,第二个元素的地址就是A+4,第三个元素的地址就是A+8,依此类推。这种连续存储的方式使得顺序表可以实现随机存储,也就是说可以直接通过元素的索引来访问或修改元素的值,而不需要像链表那样从头开始逐个查找。假设你知道某个元素的索引是n,那么你可以通过计算地址A+n*4来直接找到该元素在内存中的位置。总结起来,顺序表是一种按照顺序排列并连续存储在内存中的数据结构,类似于一排排的抽屉。它可以通过元素的索引快速访问和修改元素的值,实现了随机存储的功能)。
顺序表的类模板
#include<iostream>
using namespace std;
template<class T,int Maxsize>
class SeqList {
private:
T data[Maxsize];
int length;
public:
SeqList();
SeqList(T a[], int n);
int Getlength();
void PrintSeqList();
T Get(int pos);
int Locate(T item);
void Insert(int i, T item);
void Delete(int i);
};
初始化操作——构造函数
1.顺序表的无参构造函数
template<class T, int Maxsize>
SeqList<T,Maxsize>::SeqList()
{
length = 0;
}
2.顺序表的有参构造函数
template<class T, int Maxsize>
SeqList<T, Maxsize>::SeqList(T a[],int n)
{
if (n > Maxsize)
{
cerr << "操作非法" << endl;
exit(1);
}
for (int i = 0; i < n; i++)
{
data[i] = a[i];
}
length = n;
}
注:cout是标准输出流,而cerr是标准错误流。
求顺序表的长度
template<class T, int Maxsize>
int SeqList<T, Maxsize>::Getlength()
{
return length;
}
按位查找
注意:顺序表中第pos个元素存储在数组data中下标为pos-1的位置。
template<class T, int Maxsize>
T SeqList<T, Maxsize>::Get(int pos)
{
if (pos<0 || pos>MaxSize)
{
cerr << "操作非法" << endl;
exit(1);
}
return data[pos - 1];
}
按值查找
按值查找时将待查找的值item与顺序表中的元素依次进行比较,如果查到具有item值的元素时,则返回该元素的序号;否则返回值0,表明查找失败。
template<class T, int Maxsize>
int SeqList<T, Maxsize>::Locate(T item)
{
for (int i = 0; i < length; i++)
{
if (data[i] == item)
{
return i + 1;
}
return 0;
}
}
遍历顺序表
template<class T, int Maxsize>
void SeqList<T, Maxsize>::PrintSeqList()
{
for (int i = 0; i < length; i++)
{
cout << data[i] << endl;
}
}
顺序表的插入
插入算法具体描述:
- 检查顺序表的存储空间是否已到最大值(被占满),若是则停止插入,上溢异常,否则执行第二步。
- 检查插入位置i是否合法,若不合法,则停止插入,插入位置异常,否则执行第三步。
- 从最后一个元素向前直至第i个元素(下标为i-1)为止,将每一个元素均后移一个存储单元,将第i个元素的存储位置空出来。
- 将新元素item写入到第i个元素处,即下标为i-1的位置。
- 将顺序表的长度加1。
template<class T, int Maxsize>
void SeqList<T, Maxsize>::Insert(int i, T item)
{
if (length >= Maxsize)
{
cerr << "上溢";
exit(1);
}
if (i<0 || i>length)
{
cerr << "操作非法";
exit(1);
}
for (int j = length - 1; j > =i - 1; j--)
{
data[j + 1] = data[j];
}
data[i-1] = item;
length++;
}
注:插入算法的时间复杂度为O(n)
顺序表的删除
删除算法具体描述:
- 检查顺序表是否为空,若是则下溢异常,否则执行第二步。
- 检查删除位置i是否合法,如果不合法,操作异常,否则执行第三步。
- 取出被删除元素。
- 从第i+1个元素(下标为i)向后直至最后一个元素为止,将每一个元素均前移一个存储位置。
- 将顺序表的长度减一。
- 返回被删除的元素的值。
template<class T, int Maxsize>
T SeqList<T, Maxsize>::Delete(int i)
{
if (length == 0)
{
cerr << "下溢";
exit(1);
}
if (i<0 || i>length)
{
cerr << "操作非法";
exit(1);
}
T x = data[i - 1];
for (int j = i; j < length; j++)
{
data[j - 1] = data[j];
}
length--;
return x;
}
注:删除算法的时间复杂度为O(n)。
链式存储结构
- 定义:链表用一组物理上不一定相邻的存储单元来存储线性表中的数据元素。
- 链表由数据元素本身和指针组成。根据指针的不同,分单链表,双向链表,循环链表。
- 本章具体讲述单链表的内容,使用的是带头结点的单链表。头结点的加入使得单链表无论是否为空,头指针始终指向头结点,从而使空表和非空表的处理一致。
单链表的类模板
#include<iostream>
using namespace std;
template<class T>
struct Node {
T data;
Node<T>* next;
};
template<class T>
class LinkList {
Node<T>* head;
public:
LinkList();
LinkList(T a[], int n);
~LinkList();
int ListLength();
T Get(int pos);
int Locate(T item);
void PrintLinkList();
void Insert(int i, T item);
T Delete(int i);
};
单链表的初始化——构造函数
1.单链表的无参构造函数
template<class T>
LinkList<T>::LinkList()
{
head = new Node<T>;
head->next = NULL;
}
2.单链表的含参构造函数
template<class T>
LinkList<T>::LinkList(T a[],int n)
{
head = new Node<T>;
Node<T>* rear = head;
for (int i = 0; i < n; i++)
{
Node<T>* s = new Node<T>;
s->data = a[i];
rear->next = s;
rear = s;
}
rear->next = NULL;
}
求单链表的长度
template<class T>
int LinkList<T>::ListLength()
{
Node<T>* p = head->next;
int num = 0;
while (p)
{
p = p->next;
num++;
}
return num;
}
按位查找
单链表按位查找的算法描述:
- 初始化指针p和计数器j。
- 当p不为空或者j不等于pos时,p向后移指向下一个结点,同时j加一。
- 若p为空,则抛出查找位置异常,否则p指向需查找元素,返回p所指向结点的数据。
template<class T>
T LinkList<T>::Get(int pos) {
Node<T>* p = head->next;
int j = 1;
while (p && j < pos) {
p = p->next;
j++;
}
if (!p || j > pos) {
cerr << "查找位置非法";
exit(1);
}
else {
return p->data;
}
}
按值查找
单链表按值查找的算法描述:
- 按值查找将待查找的值item与单链表中的每个结点元素依次进行比较。
- 如果查找到具有item值的元素时,则返回该元素的序号,否则返回0,表明查找失败。
template<class T>
int LinkList<T>::Locate(T item) {
Node<T>* p = head->next;
int j = 1;
while (p && p->data != item) {
p = p->next;
j++;
}
if (p) {
return j;
}
else {
return 0;
}
}
遍历单链表
template<class T>
void LinkList<T>::PrintLinkList()
{
Node<T>* p = head->next;
while (p)
{
cout << p->data << endl;
p = p->next;
}
}
单链表的插入(此处为尾差法,头插法后续介绍)
template<class T>
void LinkList<T>::Insert(int i, T item) {
Node<T>* p = head;
int j = 0;
while (p && j < i - 1) {
p = p->next;
j++;
}
if (!p) {
cerr << "插入位置非法";
}
else {
Node<T>* s = new Node<T>;
s->data = item;
s->next = p->next;
p->next = s;
}
}
单链表的删除
删除第i个结点的算法的描述:
- 工作指针p初始化,累加器j清零。
- 查找第i-1个结点,并将指针p指向该结点。
- 若p为空或p不存在后继节点,则删除位置非法异常,否则删除结点p的后一个结点。
template<class T>
T LinkList<T>::Delete(int i) {
Node<T>* p = head;
int j = 0;
while (p && j < i - 1) {
p = p->next;
j++;
}
if (!p || !p->next) {
cerr << "删除位置非法";
exit(1);
}
else {
Node<T>* q = p->next;
T x = q->data;
p->next = q->next;
delete q;
return x;
}
}
单链表的析构函数
析构时要保证链表未处理的部分不断开。
template<class T>
LinkList<T>::~LinkList()
{
Node<T>* p = head;
while (p)
{
Node<T>* q = p;
p = p->next;
delete q;
}
head = NULL;
}
单链表的其他操作举例(逆置单链表,此处应用头插法)
将一个单链表按逆序链接,即若原单链表中存储元素的次序为a1,a2,a3,···,an,则逆序链接后变为an,an-1,···,a1。
注:链表的操作中,数据不动,只动指针,此处使用头插法。
template<class T>
void LinkList<T>::Invert()
{
Node<T>* p = head->next;
head->next = NULL;//将逆置后的单链表初始化为空表
while (p != NULL)//遍历原单链表中的结点
{
Node<T>* q = p;
p = p->next;
q->next = head->next;//将结点插入到逆置后单链表的表头
head->next = q;
}
}
循环链表简单介绍
循环链表和单链表很相似,区别仅在于判断单链表结束的条件是指针是否为空,而判断循环链表结束的条件是指针是否指向头结点。
双向链表简单介绍
双向链表相对单链表,是以空间换时间,单链表找后继的时间复杂度O(1),找前驱的时间复杂度是O(n),而双向链表则把找前驱的时间复杂度变为O(1)。
本章小结
作为数据结构的基础存储结构,线性表格外重要,是数据结构这门课的必须掌握的基础核心要点。
顺序表 | 链表 | |
读取 | O(1) | O(n) |
插入 | O(n) | O(1) |
删除 | O(n) | O(1)[仅当能够立即访问要删除的元素时] |
顺序表和单链表的比较
-
存储结构
顺序表采用顺序存储结构,即用一段地址连续的存储单元依次存储线性表的数据元素,数据元素之间的逻辑关系通过存储位置来实现。
单链表采用链式存储结构,即用一组任意的存储单元存放线性表的元素。用指针来反映数据元素之间的逻辑关系。
-
时间复杂度:
按位查找:
顺序表的时间为O(1),是随机存取;
单链表的时间为O(n),是顺序存取。
插入和删除:
顺序表需移动表长一半的元素,时间为O(n);
单链表不需要移动元素,在给出某个合适位置的指针后,插入和删除操作所需的时间仅为O(1)。
-
顺序表的优点:
⑴ 无需为表示表中元素之间的逻辑关系而增加额外的存储空间;
⑵ 随机存取:可以快速地存取表中任一位置的元素。
-
顺序表的缺点:
⑴ 插入和删除操作需要移动大量元素;
⑵ 表的容量难以确定,表的容量难以扩充; (表长固定)
⑶ 造成存储空间的“碎片”。
-
链表的优点:插入删除无需移动元素
-
链表的缺点:结构性开销、只能从前依次向后扫描
习题部分
上机题第二章上机题
2021年期中考试题:从键盘输入n个整数,将其建立一个单链表
Node* fun()
{
Node<T>* head = new Node<T>;
head->next = nullptr;
for(int i=0;i<n;i++){
T x;
cin>>x;
Node<T>* p =new Node<T>;
p->data = x;
p->next = head->next;
head->next = p;
}
return head;
}
链表倒数第k个
输入一个单向链表,输出该链表中倒数第k个结点的元素。fast、slow,先fast前移k,再fast、slow一起移动。
//考虑k=0的特殊情况
int findKthFromEnd(ListNode* head, int k) {
if (!head || k <= 0) {
return -1;
}
ListNode* fast = head;
ListNode* slow = head;
// 将fast指针向前移动k个节点for (int i = 0; i < k; i++) {
if (!fast) {
return -1;
}
fast = fast->next;
}
// 同时移动fast和slow指针while (fast) {
fast = fast->next;
slow = slow->next;
}
return slow->val;
}