👂 多年后再见你 - 乔洋/周林枫 - 单曲 - 网易云音乐
目录
🌼例题2 Broken Keyboard (a.k.a. Beiju Text)
🌳3.1 顺序表
(1)插入
位置 i 插入元素e,需要将第L.length至第 i 个元素,依次后移一个位置,空出第 i 个位置
比如第5个位置插入元素9
注意,i 表示的是实际的第几个元素(从1开始),而 [] 中的是索引(从0开始)
bool ListInsert_Sq(SqList &L, int i, int e)
{
if(i < 1 || i > L.length + 1) return false; //插入为止i不合法
if(L.length == Maxsize) return false; //存储空间满了
for(int j = L.length - 1; j >= i - 1; j--)
L.elem[j + 1] = L.elem[j]; //从最后一个元素开始后移, 直到第i个元素
L.elem[i - 1] = e; //新元素e放入第i个位置
L.length++; //表长+1
return true;
}
顺序表插入元素算法,平均时间复杂度为O(n)
分析
上下范围没法打出来,求和上限是n + 1,下限是 i = 1
思路
可以在第1个位置插入,第2个...第n + 1个位置插入(这里的位置从1开始)
共有n + 1个可以插入的位置
每种情况移动的元素个数都是 n - i + 1
将每种情况移动次数 * 概率pi,求和,就是平均时间复杂度
所以...每个位置插入概率位 1 / (n + 1),该位置移动次数为 n - i + 1
(2)删除
删除第 i 个元素,需要将该元素暂存到变量e中(后续可能用上),然后从第 i + 1个元素开始前移...直到把第 n 个元素也前移 1 位
比如,删除第5个元素
注意,i 表示的是实际的第几个元素(从1开始),而 [] 中的是索引(从0开始)
bool ListDelete_Sq(SqList &L, int i, int &e)
{
if(i < 1 || i > L.length) return false; //插入位置不合法
e = L.elem[i - 1]; //保存要删除的元素
for(int j = i; j <= L.length - 1; j++)
L.elem[j - 1] = L.elem[j]; //被删除元素后的元素前移
L.length--; //表长-1
return true;
}
顺序表删除元素算法,平均时间复杂度O(n)
分析
删除元素有 n 种情况,所以删除某个元素的概率 pi 为 1 / n,每种情况移动次数都是 n - i
(3)总结
顺序表
优点:存储密度高,只需O(1)时间就可以取出第 i 个元素
缺点:需预先分配最大空间,大了浪费,小了溢出;且插入删除时,需要移动大量元素
需要插入,删除时,往往采用链式存储
🌳3.2 单链表
提高效率,不准备敲理论内容,看这一篇就够了👇
最详细的C++单向链表实现_c++实现单链表_思泽Elly的博客-优快云博客
再给大家个福利,新加坡国立大学数据结构可视化网站👇
链表(单向,双向),堆栈,队列,双端队列 - VisuAlgo
可视化比如这样👇








下面是结合gpt注释的书中代码
(1)插入
bool ListInsert_L(LinkList &L, int i, int e) //单链表插入, 在第i个节点插入元素e
{
//在带头节点的单链表L中第i个位置插入值为e的新节点
int j; //定义计数器
LinkList p, s; //定义两个链表指针
p = L; //将p指向链表的头结点,即第一个元素之前的结点,也就是头指针
j = 0; //初始化计数器为0
while(p && j < i - 1) { //查找第i-1个节点, p指向该节点
p = p->next; //指向下一个节点
j++; //计数器加1
}
if(!p || j > i - 1) //i > n+1 或 i < 1
return false; //返回false
s = new Lnode; //生成新节点
s->data = e; //将数据元素e放入新节点的数据域中
s->next = p->next; //将新节点的指针域指向第i个节点
p->next = s;//将第i-1个节点的指针域指向新节点
return true; //返回true
}
(2)删除
bool ListDelete_L(LinkList &L, int i) //在带头节点的单链表L中删除第i个元素
{
LinkList p, q; //定义两个链表指针
int j; //定义计数器
p = L; //将p指向链表的头结点,即第一个元素之前的结点,也就是头指针
j = 0; //初始化计数器为0
while((p->next) && (j < i - 1)) { //查找第i-1个节点, p指向该节点
p = p->next; //指向下一个节点
j++; //计数器加1
}
if(!(p->next) || (j > i - 1)) //i > n 或 i < 1
return false; //返回false
q = p->next; //保存要删除的节点的地址
p->next = q->next; //将p的指针域指向q的下一个节点
delete q; //释放q所占用的空间
return true; //返回true
}
🌳3.3 双向链表
双向链表,每个节点,包含三个域:数据域和两个指针域
(1)插入
代码中设计3个节点
1,节点p
2,节点p的前驱节点p->prior(p的上一个节点)
3,节点s(要插入的节点)
//在双向链表L的第i个位置插入元素e
bool ListInsert_L(DuLinkList &L, int i, int e)
{
int j;
DuLinkList p, s;
p = L; //p指向链表L
j = 0;
//查找第i个节点,p指向该节点
while(p && j < i) {
p = p->next; //p后移一个节点
j++;
}
//insert位置不合法,返回false
if(!p || j > i)
return false;
//生成新节点s
s = new DuLnode;
s->data = e; //新节点数据为e
//更新指针域,将s插入到p和p前一个节点之间
p->prior->next = s; //p前一个节点的next指针域指向节点s的地址
s->prior = p->prior; //p前一个节点的地址赋值给节点s的prior指针域
s->next = p; //节点p的地址赋值给节点s的next指针域
p->prior = s; //节点s的地址赋值给p的prior指针域
return true;
}
//其中DuLnode是双向链表的节点结构体,结构如下:
typedef struct DuLnode {
ElemType data; //数据域
struct DuLnode *prior, *next; //前驱和后继指针
} DuLnode, *DuLinkList;
从左至右,依次连接
(2)删除
删除一个节点,就是跳过这个节点
p->prior->next = p->next;
p->next->prior = p->prior;
注意跳过节点p后,要delete p,释放被删除节点的空间
还有篇博客,我没细看,感觉双向链表挺简单的,可能有书的原因(6条消息) 双向链表(Double Linked List)_Hacker_徐的博客-优快云博客
//从双向链表L中删除第i个元素
bool ListDelete_L(DuLinkList &L, int i)
{
DuLinkList p;
p = L; //p指向链表L
int j = 0;
//查找第i个节点,p指向该节点
while(p && j < i) {
p = p->next; //p后移一个节点
j++;
}
//i > n 或 i < 1时, 删除位置不合理,返回false
if(!p || j > i)
return false;
//更新指针域,将p从双向链表中删除
if(p->next) //如果p后继节点存在
p->next->prior = p->prior; //将p的后继节点前驱指针指向p的前驱节点
p->prior->next = p->next; //将p的前驱节点后继指针指向p的后继节点
delete p; //释放p的内存
return true;
}
//其中DuLnode是双向链表的节点结构体,结构如下:
typedef struct DuLnode {
ElemType data; //数据域
struct DuLnode *prior, *next; //前驱和后继指针
} DuLnode, *DuLinkList;
注意,赋值给p->next前,需要先判断p->next是否存在,避免访问到空指针
🌳3.4 双向链表
循环链表及其基本操作 - 随手一只风 - 博客园 (cnblogs.com)
单向循环链表最后一个节点的next域不为空,而是指向了头节点
注意,单链表和单向循环链表判断空表的条件也变化了
1,单链表为空时,L->next = NULL;
2,单向循环链表为空时,L->next = L;
3,双向循环链表尾空时,L->next = L->prior = L;
链表优点:动态,不需分配最大空间,插入删除不需移动元素
缺点
动态分配节点,节点地址不连续,需要指针域记录下一个节点地址
而指针域占用一个int空间,存储密度低(数据所占空间 / 节点所占空间)
存取元素需要头到尾顺序查找,属于顺序存取
🌳3.5 静态链表
静态链表位置0不存储数据,为头节点
Google,百度了下,讲的稀巴烂,还不如自己写(主要是,讲的不够直观,看了5秒看不出个所以然)
静态链表,就是数组模拟链表,通过增加1个或2个数组,代替结构体和指针的使用
单链表中,right[]记录每个元素的后继下标
双向链表中,right[]记录后继下标,left[]记录前驱下标
动态链表,就是结构体包含data和*prior,*next三个成员(数据域,前驱,后继指针)
(1)单链表
(1)插入
比如,25插入到第6个元素的位置,只需将25放入data尾部,再修改后继数组👇
data[9] = 25; //插入元素放data[]尾部
right[5] = 9;
right[9] = 6;
(2)删除
比如,删除第3个元素
right[2] = 4;
注意,第3个元素并没有真正删除,只是不在链表中,被跳过了
(2)双向链表
(1)插入
比如,25插入到第6个元素的位置
data[9] = 25; //插入元素放data[]尾部
right[5] = 9; //后面4行依次修改
left[9] = 5;
right[9] = 6;
left[6] = 9;
(2)删除
比如,删除第3个元素
right[2] = 4;
left[4] = 2;
🌼例题1 The Blocks Problem
The Blocks Problem - UVA 101 - Virtual Judge (vjudge.net)
英文题目中,前2段都是故事背景,没必要看,从第3段开始看就行(第3段有 n, i, bi, bi+1等变量)
没有使用链表,用的vector代替二维数组,也利用了栈后进先出的特性(新元素存放在栈顶)
本题在读懂题意基础上,我们需要通过2个变量,来定位一个元素,1是高度h,2是下标
坑 1
solve()函数中读入一行时,👇下面是错误的
这样当quit输入后,还需要多输入3个变量才能输出
for(int i = 0; i < n; ++i) {
cin>>s1>>a>>s2>>b; //输入
if(s1 == "quit") break;
...
}
坑 2
The input begins with an integer n on a line by itself representing the number of blocks in the block
world. You may assume that 0 < n < 25.翻译
输入以一个整数n开始,它本身在一行中代表块中的块数
世界中的块数。你可以假设0 < n < 25
意味着,n只是表示init()初始化时,一共有0~n-1共n个不同位置,而不是说它会输入n行
Your program should process all commands until the quit command is encountered.
👆这里说的很清楚了,知道遇到"quit"命令
AC 代码
#include<iostream>
#include<vector>
using namespace std;
vector<int>block[30]; //2维vector
int n;
void init()
{
cin>>n;
for(int i = 0; i < n; ++i)
block[i].push_back(i);
}
//归位, 位置和高度
void goback(int p, int h) //p堆 > h 的所有元素归位
{
for(int i = h + 1; i < block[p].size(); ++i) { //高度h以上的归位
int k = block[p][i]; //具体元素
block[k].push_back(k); //下标k插入k
}
block[p].resize(h + 1); //0 ~ h共h + 1个元素, 重置大小
}
//查找元素x所在位置
void loc(int x, int &p, int &h) //将位置和高度传回去
{
for(int i = 0; i < n; ++i)
for(int j = 0; j < block[i].size(); ++j)
if(block[i][j] == x) {
p = i, h = j;
return;
}
}
void move_all(int h, int p, int q) //p下标高度 >= h 的部分移动到 下标q上
{
for(int i = h; i < block[p].size(); ++i) {
int k = block[p][i];
block[q].push_back(k);
}
block[p].resize(h); //重置block[p]大小
}
void solve()
{
int a, b; string s1, s2;
while(cin>>s1) {
if(s1 == "quit") return;
cin>>a>>s2>>b;
int p1 = 0, h1 = 0, p2 = 0, h2 = 0;
loc(a, p1, h1); //得到元素a位置和高度
loc(b, p2, h2);
if(p1 == p2) continue; //非法命名, 不予执行
if(s1 == "move") goback(p1, h1);
if(s2 == "onto") goback(p2, h2); //归位
//归位后整体移动
move_all(h1, p1, p2);
}
}
void print()
{
for(int i = 0; i < n; ++i) {
cout<<i<<":";
for(int j = 0; j < block[i].size(); ++j)
cout<<" "<<block[i][j];
cout<<endl;
}
}
int main()
{
init(); //初始化
solve(); //输入和处理
print(); //输出
return 0;
}
🌼例题2 Broken Keyboard (a.k.a. Beiju Text)
Broken Keyboard (a.k.a. Beiju Text) - UVA 11988 - Virtual Judge (vjudge.net)
翻译
Each test case is a single line containing at least one and at most 100,000 letters, underscores and two special characters ‘[’ and ‘]’. ‘[’ means the “Home” key is pressed internally, and ‘]’ means the “End” key is pressed internally. The input is terminated by end-of-file (EOF)
每个测试用例是一个单行,至少1个最多100,000个,字母、下划线或特殊字符'[' ,']'
']'意味着内部按了 "Home "键,而']'意味着内部按了 "End "键。输入由文件末尾的
(EOF)
解释
遇到 '[' 光标会跳到开头,遇到 ']' 光标会跳到结尾
需要在头尾进行操作,用顺序表,需要移动大量元素,所以我们用STL的list双向链表,速度较快
涉及了
1,list<char>text;的声明,即每个元素是char型的
2,迭代器以及begin(), end()两个指针的使用
3,insert()函数
坑
1,每次插入后,不要漏it++; 同时it++;应该放在else的大括号内
2,迭代器 it 记得初始化(常犯错误)
困惑
搞不懂为什么,后面已经有it++了,前面还一定要用 it 接收插入的位置呢....
it = text.insert(it, s[i]);
it++;
gpt4.0的解释
在C++中,
list是一个双向链表容器,每个元素都是通过链表节点连接在一起。迭代器 (iterator) 是用于访问和操作容器元素的对象。当我们调用
text.insert(it, s[i])时,它会将s[i]插入到it所指向的位置之前,并返回一个指向插入的元素的迭代器。在这种情况下,为了保持迭代器的正确性,我们将返回的迭代器赋值给it。由于插入操作可能导致链表结构的改变,因此原始的迭代器
it可能会失效或者指向错误的位置。通过将返回的迭代器赋值给it,我们确保it始终指向当前正确的位置。然后,通过
it++,我们将迭代器it向后移动一个位置,使其指向下一个元素。这样,在下一次迭代时,我们可以继续将新的元素插入到正确的位置。所以,
text.insert(it, s[i])等价于it = text.insert(it, s[i]); it++;,这样可以确保插入操作的正确性,并更新迭代器的位置。
记忆
text.insert(it, s[i]);
//等价于
it = text.insert(it, s[i]);
it++;
AC 代码
#include<iostream>
using namespace std;
#include<list>
void solve(string s)
{
list<char>text;
list<char>::iterator it = text.begin();
for(int i = 0; i < s.length(); ++i) { //读入
if(s[i] == '[') it = text.begin();
else if(s[i] == ']') it = text.end();
else {
it = text.insert(it, s[i]);
it++; //这里漏了, 插入位置会错
}
}
//输出
for(it = text.begin(); it != text.end(); ++it)
cout<<*it;
cout<<endl;
s.clear();
}
int main()
{
string s;
while(cin>>s)
solve(s);
return 0;
}
🌼例题3 Boxes in a Line
Boxes in a Line - UVA 12657 - Virtual Judge (vjudge.net)
翻译
the sum of numbers at odd-indexed positions. Positions are numbered 1 to n from left to right.
奇数索引位置的数字之和。从左到右为1~n的数字
本题,就双向静态链表来说,除了建2个数组外(由于存储的数据按1~n,所以不需要存储数组的数组,只需要前驱和后继),只有2个封装好的函数,一个是init()初始化,一个是link()链接,一个链接就能处理删除,插入,交换,逆序4种操作
当然,需要考虑(分类讨论)较为全面
思路
由题目,移动需要删除和插入2个操作,还需要查找元素的位置,但是查找是链表不擅长的,链表查找的时间复杂度为O(n),而由题,n <= 1e5,多次查找会超时
所以我们采取静态链表实现(快速插入删除 + 快速查找),因为题目包含向后和向前操作,我们采取静态双向链表。
同时,由于链表中含大量元素,翻转操作复杂度高,只需做标记即可,不用真的翻转
算法设计
(1)初始化双向静态链表(前驱数组l[],后继数组r[],翻转标记flag = false)
(2)读入操作指令a
(3)a = 4则标记翻转,flag = !flag,否则读入x, y
(4)如果 a != 3 && flag,则a = 3 - a,因为此时左右是倒置的,2表示1,1表示2
(5)对于1,2指令,本来位置对的话,不予操作
(6)a = 1,删除x,且x插入y左侧
(7)a = 2,删除x,且x插入y右侧
(8)a = 3,分为相邻和不相邻两种情况
操作代码
1,链接
void link(int L, int R)
{
r[L] = R; //L的后继为R
l[R] = L; //R的前驱为L
} //L和R链接起来, 类似 L -- R
2,删除
//删除x, 只需将x跳过去
link(Lx, Rx); //将x的前驱和后继链接
3,插入
x插入y左侧:先删除x,再将x插入
//删除需要1次链接
//插入需要2次链接
link(Lx, Rx); //删除x
link(Ly, x); //y前驱节点与x链接
link(x, y); //x和y链接
4,插入
x插入y右侧
link(Lx, Rx);
link(y, x); //y和x --
link(x, Ry); //x与y后继节点 --
上面的是相邻,下面的是不相邻
相邻只需要link三次,不相邻需要link四次,都不需要删除
5,交换(相邻)
//做题时建议快速画出草图, 便于理解
link(Lx, y);
link(y, x);
link(x, Ry);
相邻需要考虑X,Y前后的分类讨论
6,交换(不相邻)
link(Lx, y);
link(y, Rx);
link(Ly, x);
link(x, Ry);
不相邻不需要分类讨论(看上图👆)
7,翻转
如果标记了翻转,flag == true
1,长度n为奇数,正向与反向一样,不用操作
2,长度n为偶数,反向奇数位的和 = 所有元素和 - 正向奇数位的和
所以,只需统计正向奇数位的和,再判断1,flag == true 2,长度 n 是否为偶数
👇需要注意的是,静态双向链表初始化时,习惯性初始化为循环链表,头节点和尾节点连在一起
且头节点不存储数据
第2个需要注意的点:统计和的时候,要根据l[]和r[],而不是从头到尾按顺序
坑
因为一个没保存初始值,花了2小时找bug
int lx = l[x], ly = l[y], rx = r[x], ry = r[y]; //link操作会改变同一个操作中的初始值
一开始觉得不用保存初始值,是因为,觉得同一步操作不会影响,实际上,在3步链接或者4步链接中,当y的左端变成lx后,ly就不是ly了,而是lx,自然影响到下一行代码的执行
AC 代码
#include<iostream>
using namespace std;
int l[100010], r[100010]; //前驱 后继数组
void init(int n)
{
for(int i = 1; i < n; ++i) {
l[i] = i - 1;
r[i] = i + 1;
}
l[0] = n, r[0] = 1; //循环
l[n] = n - 1, r[n] = 0;
}
void link(int L, int R) //L与R连接
{
r[L] = R;
l[R] = L;
}
int main()
{
int n, m, a, x, y, number = 1; //Case number
while(cin>>n>>m) { //n个数据 m行命令
bool flag = false; //未翻转
init(n); //初始化前驱 后继数组
for(int i = 0; i < m; ++i) {
cin>>a;
if(a == 4) flag = !flag; //标记要翻转
else {
cin>>x>>y;
if(a != 3 && flag)
a = 3 - a; //命令1和2交换
if(a == 1 && l[y] == x)
continue; //不予操作
if(a == 2 && r[y] == x)
continue;
//再对1 2 3分类讨论
int lx = l[x], ly = l[y], rx = r[x], ry = r[y]; //link操作会改变同一个操作中的初始值
if(a == 1) {
link(lx, rx); //删除x
link(ly, x);
link(x, y);
}
if(a == 2) {
link(lx, rx);
link(y, x);
link(x, ry);
}
if(a == 3) {
//相邻
if(ry == x) { //3次链接
link(ly, x);
link(x, y);
link(y, rx);
}
else if(r[x] == y) { //3次链接
link(lx, y);
link(y, x);
link(x, ry);
}
//不相邻
else { //4次链接
link(lx, y);
link(y, rx);
link(ly, x);
link(x, ry);
}
}
}
}
long long sum = 0, t = 0; //总和, 下标
for(int i = 1; i <= n; ++i) {
t = r[t];
if(i % 2 == 1)
sum += t; //奇数位的和
}
if(flag && n % 2 == 0) //长度n为偶数 且 需要翻转
sum = (long long)n * (n + 1) / 2 - sum; //总和 - 奇数位的和
cout<<"Case "<<number++<<": "<<sum<<endl;
}
return 0;
}
🥇总结
初步接触了单双链表,循环链表,静态链表
链表的优势在于,插入和删除
顺序表的话,只有一个优点,取元素比较快,也就是查找,O(1)复杂度
静态链表的优势除了插入,删除,还有查找(下标)--> 综合了链表和顺序表的优点
链表由结构体中的指针域和数据域构成
静态链表由多个数组代替数据域和指针域
什么叫入门?学完知识,刷上10题叫入门,现在才3题,入门还算不上...慢慢来




英文题目中,前2段都是故事背景,没必要看,从第3段开始看就行(第3段有 n, i, bi, bi+1等变量)


748

被折叠的 条评论
为什么被折叠?



