TODO:太晚了,明日调试
引言:
本文以线性表为例,介绍数据结构
目录
文章目录
正文:
2.线性表
线性表的概念
- 线性表是处理线性结构的数据结构。
- 线性结构的数据指的是:
- 除了A_0A0和A_{n-1}A**n−1外,每个元素都有唯一的前趋和后继
- 对于每个A_iA**i,它的前驱是A_{i-1}A**i−1,它的后继是A_{i+1}A**i+1
- A_0A0只有后继没有前驱,A_{n-1}A**n−1只有前驱没有后继。
线性表的基本操作
- 创建空的线性表
- 清除所有元素
- 求线性表的长度
- 在第i个位置插入一个元素
- 删除第i个位置的元素
- 搜索某个元素在线性表中是否出现
- 访问线性表的第i个元素
- 遍历线性表
线性表的抽象类
因此,我们可以这样定义线性表的抽象类,类中的每个函数都是纯虚函数,在实现数据结构时需要去完成每一个纯虚函数。
- 和线性表的操作相比,少了创建操作,因为这可以用每个类的构造函数来完成。
- 多了虚析构函数,是为了防止内存泄漏,把申请的动态内存析构掉。
template <class elemType>
class list {
public:
// 清除
virtual void clear() = 0;
// 长度
virtual int length() const = 0;
// 插入
virtual void insert(int i, const elemType &x) = 0;
// 删除
virtual void remove(int i) = 0;
// 搜索
virtual int search(const elemType &x) const = 0;
// 访问
virtual elemType visit(int i) const = 0;
// 遍历
virtual void traverse() const = 0;
// 析构
virtual ~list() {};
};
顺序表:
顺序表的概念和设计
顺序表指的是线性表的顺序存储,也就是说线性表中结点存放在存储器上一块连续的空间中,即一个数组。
- 数据成员
- 表元素类型的指针
data
- 数组规模
maxSize
- 数组中的元素个数
currentLength
- 表元素类型的指针
顺序表类定义
template <class elemType>
class seqList: public list<elemType> {
private:
elemType *data;
int currentLength;
int maxSize;
// 为了解决插入元素时,数组容量不够的问题
void doubleSpace();
public:
seqList( int initSize = 10 );
// 析构函数:还掉动态空间
~seqList( ) { delete [] data; }
// 清空数组元素:只要把表长设为0
void clear( ) { currentLength = 0; }
// 数组长度:只需要返回这个私有成员变量currentLength
int length( ) const { return currentLength; }
void insert( int i, const elemType &x);
void remove( int i );
int search( const elemType &x ) const ;
// 访问数组元素:返回下标为i的数组元素
elemType visit( int i) const { return data[i] ; }
void traverse( ) const ;
};
顺序表成员函数的实现
- 构造函数:申请一个空表
template <class elemType>
seqList<elemType>::seqList(int initSize) {
data = new elemType[initSize];
maxSize = initSize;
currentLength = 0;
}
- 搜索函数:用for循环进行查找
template <class elemType>
int seqList<elemType>::search (const elemType &x) const {
int i;
// 空循环,不满足循环条件时跳出
for (i = 0; i < currentLength && data[i] != x; ++i) ;
if (i == currentLength)
return -1;
else return i;
}
- 遍历函数
template <class elemType>
void seqList<elemType>::traverse() const {
cout << endl;
for (int i = 0; i < currentLength; ++i)
cout << data[i] << ' ';
}
- 插入函数
template <class elemType>
void seqList<elemType>::insert(int i, const elemType &x) {
// 如果数组放满了,先扩大数组空间
if (currentLength == maxSize)
doubleSpace();
// 从后往前,元素后移
for ( int j = currentLength; j > i; j--)
data[j] = data[j-1];
// 空出空间,插入元素,表长加1
data[i] = x;
++currentLength;
}
- doubleSpace函数:扩大数组空间
template <class elemType>
void seqList<elemType>::doubleSpace() {
// 保存指向原来空间的指针
elemType *tmp = data;
// 重新申请空间
maxSize *= 2;
data = new elemType[maxSize];
// 拷贝原有数据
for (int i = 0; i < currentLength; ++i)
data[i] = tmp[i];
// 清除原来申请的空间
delete [] tmp;
}
- 删除函数:
template <class elemType>
void seqList<elemType>::remove(int i) {
// 后面所有元素前移,表长减1
for (int j = i; j < currentLength - 1; j++)
data[j] = data[j+1] ;
--currentLength;
}
总结
- 由于要保持逻辑次序和物理次序的一致性,顺序表在插入删除时需要移动大量的数据,性能不太理想;
- 由于逻辑次序和物理次序的一致性使得定位访问的性能很好;
- 顺序表比较适合静态的、经常做线性访问的线性表。
编程实践:
练习4·代码题
序列操作1
现有一个空的整数序列,需要你对其进行如下操作:
- 若命令为
1 x
,表示需要在这个序列末尾新插入整数x
。 - 若命令为
2 x
,表示需要删除从前向后第x
个数字,数据保证x
一定小于等于当前序列中整数的个数。 - 若命令为
3 x
,表示需要输出从前向后第x
个数字,数据保证x
一定小于等于当前序列中整数的个数。
输入描述:
第一行一个整数m
,表示操作的次数。数据保证m ≤ 10^3
。
接下来m
行,每行两个整数,第一个数字表示该次操作的类型,第二个数字x
表示该此操作的参数,具体如题目所表述。
输出描述:
一行,若干个整数,为命令3输出的数字,用空格隔开。
示例 1:
输入:
6
1 1
1 2
1 3
2 2
3 1
3 2
输出:
1 3
代码:
#include <iostream>
using namespace std;
int a[1010], cnt;
int main() {
int m, t, x;
cin >> m;
while (m--) {
cin >> t >> x;
if (t == 1)
a[++cnt] = x;
else if (t == 2) {
for (int i = x; i < cnt; i++)
a[i] = a[i + 1];
cnt--;
}
else
cout << a[x] << " ";
}
return 0;
}
单链表
单链表的概念和设计
- 单链表的概念:在单链表中,每个结点由数据元素和指针构成。该指针指向它的直接后继结点,最后一个结点的后继指针为空,即没有后继节点。
- 单链表类的设计:
- 存储设计:需要定义一个结点类,单链表类的数据成员包括头指针和链表长度
currentLength
。 - 工具函数设计:
- 线性表的抽象函数都需要实现;
- 链表的插入、删除操作都需要将指针移到被操作结点的前一结点。通过设计函数
move
实现找到某一个结点的位置的功能。
- 存储设计:需要定义一个结点类,单链表类的数据成员包括头指针和链表长度
单链表类定义
template <class elemType>
class sLinkList: public list<elemType> {
private:
// 定义一个结点的结构体,里面的所有成员都是公有的
struct node {
elemType data;
node *next;
node(const elemType &x, node *n = NULL) { data = x; next = n; }
node( ):next(NULL) { }
~node() {};
};
node *head;
int currentLength;
node *move(int i) const;
public:
// 构造函数:创建空的单链表
sLinkList() {
head = new node;
currentLength = 0;
}
// 析构函数:调用clear把单链表的所有结点都还掉,再把头结点还掉
~sLinkList() { clear(); delete head; }
void clear() ;
// 表的长度:返回私有的成员变量
int length() const { return currentLength; }
void insert(int i, const elemType &x);
void remove(int i);
int search(const elemType &x) const ;
elemType visit(int i) const;
void traverse() const ;
};
单链表成员函数实现
- clear():把单链表变成一个空表。注意需要把所有结点的空间还给系统。
template <class elemType>
void sLinkList<elemType>::clear() {
node *p = head->next, *q;
head->next = NULL;
// 只要p不是空的,把p的下一个结点记下来,删掉p,再把q赋给p
while (p != NULL) {
q = p->next;
delete p;
p=q;
}
currentLength = 0;
}
- insert(i,x):在第i个位置插入元素x。先让指针指向第i-1个元素,之后执行插入过程,即申请一个存放x的结点,让该结点的后继指针指向结点i,让第i-1个结点的后继指针指向这个新结点。
template <class elemType>
void sLinkList<elemType>::insert(int i, const elemType &x) {
node *pos;
// 通过move函数找到第i-1个元素的地址
pos = move(i - 1);
pos->next = new node(x, pos->next);
++currentLength;
}
- move(i): 返回指向第i个元素的指针。
template <class elemType>
sLinkList<elemType>::node * sLinkList<elemType>::move(int i) const {
node *p = head;
while (i-- >= 0) p = p->next;
return p;
}
- remove(i):删除第i个位置的元素。先找到第i-1个结点,让该结点的后继指针指向第i+1个结点,释放被删结点空间。
template <class elemType>
void sLinkList<elemType>::remove(int i) {
node *pos, *delp;
// 通过move函数找到第i-1个元素的地址
pos = move(i - 1);
// 找到被删结点的位置,让第i-1个元素的后继指针指向第i+1个结点
delp = pos->next;
pos->next = delp->next;
// 释放被删结点的空间
delete delp;
--currentLength;
}
- search(x):搜索某个元素在线性表中是否出现。从头指针的后继结点开始往后检查链表的结点直到找到x或查找到表尾。
template <class elemType>
int sLinkList<elemType>::search(const elemType &x) const {
// 指针指向第一个元素A_0
node *p = head->next;
int i = 0;
while (p != NULL && p->data != x) {
p = p->next;
++i;
}
// p为NULL表示找到表尾都没有找到
if (p == NULL)
return -1;
else
return i;
}
- visit(i):访问线性表的第i个元素。通过move(i)找到第i个结点,返回该结点的数据部分
template <class elemType>
elemType sLinkList<elemType>::visit(int i) const {
return move(i)->data;
}
- traverse():遍历运算。从头结点的直接后继开始重复:输出当前结点值,将后继结点设为当前结点,直到当前节点为空。
template <class elemType>
void sLinkList<elemType>::traverse() const {
node *p = head->next;
cout << endl;
while (p != NULL) {
cout << p->data << " ";
p=p->next;
}
cout << endl;
}
编程实践
序列操作2
现有若干个空的整数序列,需要你对其进行如下操作:
- 若命令为
1 x y
,表示需要在第x
个序列末尾新插入整数y
。 - 若命令为
2 x
,表示需要在第x
个序列末尾删除一个数字,删除前保证该序列不为空。 - 若命令为
3
,表示需要查询当前所有序列中的所包含的元素,每个序列单独一行,对于每个序列按照输入的顺序输出。
数据保证命令3总数不超过2条。
输入描述:
第一行两个整数n m
,表示序列的的数量和操作的次数。数据保证n ≤ 10^5 m ≤ 5 * 10^5
。
接下来m
行,每行表示一条命令,具体如题目所表述。
输出描述:
每次执行命令3输出n
行,每行包含若干整数并用空格隔开,若该序列为空,则此行输出none
。
示例 1:
输入:
2 7
1 1 3
3
1 1 4
1 2 1
1 2 2
2 1
3
输出:
3
none
3
1 2
#include <iostream>
#define N 500010
using namespace std;
int head[100010], nxt[N], val[N], out[N], cnt;
int main() {
int n, m, t, x, y;
cin >> n >> m;
while (m--) {
cin >> t;
if (t == 1) {
cin >> x >> y;
val[++cnt] = y;
nxt[cnt] = head[x];
head[x] = cnt;
}
else if (t == 2) {
cin >> x;
head[x] = nxt[head[x]];
}
else {
for (int i = 1; i <= n; i++) {
int oc = 0, pos = head[i];
while (pos) {
out[++oc] = val[pos];
pos = nxt[pos];
}
if (oc == 0)
cout << "none";
while (oc)
cout << out[oc--] << " ";
cout << endl;
}
}
}
return 0;
}
双链表
双链表的概念和设计
- 双链表概念:在连接实现中,如果每个结点既保存直接后继结点的地址,也保存直接前驱结点的地址,则该链表被称为双链表。
- 双链表类的设计:
- 链表类的数据成员:头指针、尾指针,链表长度
currentLength
。 - 必须定义一个结点类。
- 链表类必须实现线性表的所有操作,故链表类从线性表的抽象类继承。
- 链表的插入、删除操作都需要将指针移到被操作结点的前一结点。通过设计函数
move
实现。
- 链表类的数据成员:头指针、尾指针,链表长度
双链表的类定义
template <class elemType>
class dLinkList: public list<elemType> {
private:
struct node {
elemType data;
node *prev, *next;
node(const elemType &x, node *p = NULL, node *n = NULL) { data = x; next = n; prev = p; }
node( ):next(NULL), prev(NULL) {}
~node() {}
};
node *head, *tail;
int currentLength;
node *move(int i) const;
public:
dLinkList();
// 析构函数:先调用clear删除所有元素,再删除头尾结点
~dLinkList() {
clear();
delete head;
delete tail;
}
void clear();
// 返回元素数量
int length() const { return currentLength; }
void insert(int i, const elemType &x);
void remove(int i);
int search(const elemType &x) const;
elemType visit(int i) const;
void traverse() const;
};
双链表成员函数实现
- 构造函数:创建一个双链表就是创建一个只有头尾结点的链表,其中头结点的前驱为空,尾结点的后继为空。
template <class elemType>
dLinkList<elemType>::dLinkList() {
head = new node;
head->next = tail = new node;
tail->prev = head;
currentLength = 0;
}
- 插入:让指针指向第i个元素,执行插入过程,具体方式基本与单链表一致。
template <class elemType>
void dLinkList<elemType>::insert(int i, const elemType &x) {
node *pos, *tmp;
pos = move(i);
tmp = new node(x, pos->prev, pos);
pos->prev->next = tmp;
pos->prev = tmp;
++currentLength;
}
- 删除:让指针指向第i个元素,执行删除过程,具体方式基本与单链表一致。
template <class elemType>
void dLinkList<elemType>::remove(int i) {
node *pos;
pos = move(i);
pos->prev->next = pos->next;
pos->next->prev = pos->prev
delete pos;
--currentLength;
}
- 双链表其他操作和单链表基本相同。
编程实践
队列安排
一个学校里老师要将班上N
个同学排成一列,同学被编号为1∼N
,他采取如下的方法:
- 先将1号同学安排进队列,这时队列中只有他一个人;
2−N
号同学依次入列,编号为i的同学入列方式为:老师指定编号为i
的同学站在编号为1 ~ (i-1)
中某位同学(即之前已经入列的同学)的左边或右边;- 从队列中去掉
M (M<N)
个同学,其他同学位置顺序不变。
在所有同学按照上述方法队列排列完毕后,老师想知道从左到右所有同学的编号。
输入描述:
第1
行为一个正整数N
,表示了有N
个同学。
第2−N
行,第i
行包含两个整数k p
,其中k
为小于i
的正整数,p
为0
或者1
。若p
为0
,则表示将i
号同学插入到k
号同学的左边,p
为1
则表示插入到右边。
第N+1
行为一个正整数M
,表示去掉的同学数目。
接下来M
行,每行一个正整数x
,表示将x
号同学从队列中移去,如果x
号同学已经不在队列中则忽略这一条指令。
输出描述:
一行,包含最多N
个空格隔开的正整数,表示了队列从左到右所有同学的编号,行末换行且无空格。
示例 1:
输入:
4
1 0
2 1
1 0
2
3
3
输出:
2 4 1
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
#define N 100005
using namespace std;
struct node {
int l, r;
}nod[N];
int head, tail;
void add(int a, int b, int c) {
nod[a].r = b;
nod[b].r = c;
nod[c].l = b;
nod[b].l = a;
}
void del(int a, int b, int c) {
nod[a].r = c;
nod[c].l = a;
nod[b].r = nod[b].l = 0;
}
int n, m;
int main() {
scanf("%d", &n);
head = n + 1;
tail = n + 2;
nod[head].r = tail;
nod[tail].l = head;
add(head, 1, tail);
for (int i = 2; i <= n; ++i) {
int x, y;
scanf("%d%d", &x, &y);
if (!y) add(nod[x].l, i, x);
else add(x, i, nod[x].r);
}
scanf("%d", &m);
for (int i = 0; i < m; ++i) {
int x;
scanf("%d", &x);
if (!nod[x].l) continue;
del(nod[x].l, x, nod[x].r);
}
for (int i = nod[head].r; i != tail; i = nod[i].r) printf("%d ", i);
return 0;
}
视频知识点
P1知识点
数据结构研究什么?
- 一组有特定关系的数据的存储与处理
- 抽象的方法
数据的逻辑结构
- 集合结构
- 线性结构
- 树形结构
- 图形结构
数据结构的操作
- 创建
- 清除
- 插入
- 删除
- 搜索
- 更新
- 访问
- 遍历
P2知识点
- 存储实现
- 需要储存的信息
- 一组数据元素
- 数据元素之间的关系
- 物理结构
- 存储结点
- 数据元素之间的关系的存储
- 附加信息
- 关系的存储
- 顺序存储
- 链接存储
- 哈希存储
- 索引存储
- 需要储存的信息
- 运算实现
- 操作怎么实现
- 每个运算对应一个算法
- 每个算法用一个函数表示
- 每个数据结构有一组函数
- 操作怎么实现
P3知识点
- 时间性能
- 时间性能的衡量
- 标准操作
- 时间复杂度
- 最好情况的时间复杂度
- 最坏情况的时间复杂度
- 平均情况的时间复杂度
- 大O表示法(上界)
- 两个定理
- 求和定理
- 求积定理
- 空间性能
- 空间复杂度
- 算法处理过程中所需的额外工作量
- 一般按最坏情况处理
- 用大O表示法
- 空间复杂度
p4知识点
- 算法优化问题提出
- 慢慢分析,逐步优化
- 最大连续子序列和问题
- O(N^3)O(N3)的算法
- 枚举法
使用朴素的枚举法求最大连续子序列和的时间复杂度?
O(N^3)
P5知识点
知识点
-
O(N^2)O(N2)
的算法
- 枚举子序列
使用枚举子序列法求最大连续子序列和的时间复杂度?
P6知识点
- 集合结构
- 线性结构
- 树形结构
- 图形结构
数据结构的操作
- 创建
- 清除
- 插入
- 删除
- 搜索
- 更新
- 访问
- 遍历
P2知识点
- 存储实现
- 需要储存的信息
- 一组数据元素
- 数据元素之间的关系
- 物理结构
- 存储结点
- 数据元素之间的关系的存储
- 附加信息
- 关系的存储
- 顺序存储
- 链接存储
- 哈希存储
- 索引存储
- 需要储存的信息
- 运算实现
- 操作怎么实现
- 每个运算对应一个算法
- 每个算法用一个函数表示
- 每个数据结构有一组函数
- 操作怎么实现
P3知识点
- 时间性能
- 时间性能的衡量
- 标准操作
- 时间复杂度
- 最好情况的时间复杂度
- 最坏情况的时间复杂度
- 平均情况的时间复杂度
- 大O表示法(上界)
- 两个定理
- 求和定理
- 求积定理
- 空间性能
- 空间复杂度
- 算法处理过程中所需的额外工作量
- 一般按最坏情况处理
- 用大O表示法
- 空间复杂度
p4知识点
- 算法优化问题提出
- 慢慢分析,逐步优化
- 最大连续子序列和问题
- O(N^3)O(N3)的算法
- 枚举法
使用朴素的枚举法求最大连续子序列和的时间复杂度?
O(N^3)
P5知识点
知识点
-
O(N^2)O(N2)
的算法
- 枚举子序列
使用枚举子序列法求最大连续子序列和的时间复杂度?