本篇文章内容如下,请耐心观看,将持续更新。
-
简单数组
-
简单栈
-
简单队列
-
简单链表
-
简单二叉树
-
简单集合
-
图的基本概念
-
二叉堆
-
线段树
-
树状数组与字典树
-
线段树进阶
简单数组:
-
STL可变数组 vector
" 我们首先要知道这个容器有什么特性,然后它是咋创建的、然后要知道这个东西最常见的功能,访问,查找,删除,修改,添加……是如何实现的。再接着,我们尽可能了解一些这个容器的常见函数的使用,还要知道它的时间复杂度。那么这个容器,你就算大概了解了。"
vector 是一个“ 可变长度 ” 数组。 一般是数组,定义的时候需要同时定义长度。
有些时候,我们不知道应该定义多长,或者定义过长会出现浪费的情况
那么我们就希望 有一个弹性的数组,需要用多少就有多长。
vector 就是一个这样的东西
“ 建立一个可变长度数组v,内部元素类型是int , 该可变数组最开始有 n 个元素, 每个元素被初始化为 m 。如果省略m,那么这个可变数组有n个元素,每个元素初始化为0.”
vector<int> v(n,m)
"我们也可以省略上面的 (n,m),此时的 v 长度就是0。 “
vector<int > v
“并且,v容器内的元素还可以是其他的数据类型。”
vector<string> v;
vector<char> v;
对于vector 数组的 访问 或 编辑 ,我们可以像普通数组一样使用方括号进行索引(也就是说下标这个概念是存在于vector中的),比如 v [1 0 ] ,就是访问下标为10的元素。
不过需要注意的是: 如果使用下标进行访问,那么需要注意vector 的长度是不是足够,否则就会造成越界。
其实,因为vector 是可变长的,所以如果空间可能会不够的话,我们可以实时地给它增长长度。
就可以用 push_back ( ) , 或者 resize ( ) 函数,进行增长。
然后要想知道这个数组的长度的话,我们就可以用 size()函数。
vector <int > v ;1、v.push_back(a);
2、v.resize(n,m);
3、v.size();
1、v.push_back(a) 指的是 向容器里添入一个元素,添入成功后,自然v的长度也就变长了 。
2、v.resize(n,m)指的是 “重新调整数组长度为n” ,如果当前vector 的长度小于n ,那么他的长度在原来的基础上就会增加到n,而新增加的元素,就会被初始化为m。
倘若vector 的长度 大于n ,那么就会删除多余的部分。
3、v.size() 指的是 " 返回v数组的长度"。
来一道简单的例题,上手实践一下这个新容器吧?
P3156 【深基15.例1】询问学号 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)https://www.luogu.com.cn/problem/P3156
#include<iostream>
#include<vector>
using namespace std;
int main()
{vector<int> arr;int n,m;cin >> n>>m;for (int i = 0; i < n; i++){int temp;cin >> temp;arr.push_back(temp);}for (int i = 0; i < m; i++){int temp;cin >> temp;cout << arr[temp-1];if(i!=m-1)cout<<endl;}
}
看到这里,我们想想还有什么没讲?
- 上面说了增删改查的第一种形式,就是使用数组下标,那么还有没有另一种方式呢?
- 上面介绍了一维vector , 我们都知道数组也有二维,那么vector 有没有二维呢?
- vector 还有其他有用的函数吗? 这些函数对比常规的静态数组有什么优势呢?
接下来的内容我们就围绕这些问题来讲述:
增删改查的第二种形式: 迭代器。
vector <int > v;
vector <int > :: iterator it;
vector <char > :: iterator ii;
定义一个迭代器 i t,这个迭代器只能指向 vector<int >。
定义一个迭代器 i i ,这个迭代器只能指向 vector<char >。
由上面的代码,我们足以看出一些东西了。
首先 迭代器一定是有对象的(悲,我没有 ),并且这个迭代器 是为一类容器服务的,不是为了某一个具体的容器。所以我们后续可能会有一些方法,使得这个迭代器变成某个具体容器的专门迭代器。
其次,迭代器的定义也太特么长了吧!! (说不定存在某些东西可以让我们简写)
最后,迭代器肯定和一般的int,char 这种数据类型不一样,它肯定是一种特殊的数据类型,比如指针这种。所以使用它的时候,我们不能以看待常见的数据类型的眼光去看它……。
相信,再给一段代码,这段代码代表,遍历容器内所有的元素,你一定能解决上面的问题吧!喵
vector<int > v;
vector<int > :: iterator it;
for( it = v.begin() ; it != v.end() ; it++ ){cout << *it<<' ' ;
}
” 如果你是第一次看见, 你可能会说, 卧槽,这一坨东西是啥,为什么我每个字母都看得懂,连起来我就不知道是啥了 啊 呜呜呜 “ 。
别急别急,我们细细分析, 在上面我们说到,it 迭代器是为一类容器服务的(比如容器内部是int的元素算一类 ) 但是为啥 it 能用来遍历 v容器(一个具体的容器)啊? 噢!
因为我们让 it 等于了 v.begin() 这个东西。 所以it 就暂时为 v容器服务了。
我们还能知道,因为it是迭代器类型, 一个东西能直接给它赋值, 那么这俩玩意,一定是互通的,或者说,这俩是同一种类型 。
哎哟卧槽,这岂不是说
v.begin( ) 应该就是 专属于 v的东西(因为它开头已经被打上了标记v
并且,v.begin( ) (我们可以看出他应该是个函数) 这个函数的返回值必然是 一个 迭代器类型的东西!!!。
bingo !!恭喜你少年,你猜对了!!。
那么 v.end () 肯定跟 v.begin ( ) 是差不多的东西辣。
结合他俩的英文单词,再加上我说这串代码是遍历容器v,所以所以所以!!!
v.begin ( ) 返回的是一个指向 v容器开头的迭代器,
v.end( ) 返回的是指向v容器末尾的迭代器。
我们的临时工 it ,从v容器的开头,遍历到v容器的结尾, 这特么可不就是遍历吗!!!!
但是,你可能还有一个疑问,这和我们常规的数组遍历不一样啊,咋回事啊?
for ( int i=0 ;i< v.size() ;i ++ ) {cout << v[i]<<' ';
}vector<int > :: iterator it;for ( it = v.begin() ;it!= v.end(); it++ ) cout<< *it<<' ';
}
(为啥一个 是 i<v.size() , 一个是 it != v.end( ) )
(为啥一个是 cout<< i , 一个是cout<< *it )
等等,华生,你发现了盲点!!!cout后面的内容,居然是输出 *it , 那么岂不是说明,这个迭代器,在某些层面上和指针是一样的!!!
嗷嗷嗷嗷嗷,你彻底懂了, 所以 it 不能写成 it< v.end() ,为啥,这特么是地址啊,只有等于或不等于的情况,没有大于小于的情况。
那么也就明白了, it ++ 是什么意思, 也就是指向下一个元素 地址。总之,这一方面,就是指针的内容。
好好好 , 你已经彻底懂了迭代器是啥,也知道begin,end 这俩函数是啥,你已经掌握一大半了。
你也许会问,为啥,it != v.end() , end 难道不是最后一个元素吗?
其实。。 end不是指最后一个元素,它指的是最后一个元素的再后面一个位置,这个位置是未知的,所以千万要小心,不能输出 *end。
然后,就剩最后一个问题了,二维的vector是咋样的呢?
首先,我先不告诉你二维的vector 是啥样的。我们不妨来想象一下吧!
对于一个这样的数组:
int a[10][10] ;
我们都知道它可以表示成:
{ ………………} ,
{………………},
…………
{………………},
一共有是10 层,每一层都有 10个元素。
也就是说,如果这个数组是二维的,那么它的一维下标代表有多少层,二维下标代表这一层有多少个数字。(等等,我们为啥说是数字呢?因为,这个数组的数据类型是 int )
好,那么我们参照二维普通数组的概念。
那么可变二维vector 我们可以猜测: 它的第一维表示有多少层,这个层数是不定长的,二维表示每一层有多少元素,这个元素也是不定个数的。
不等层数,每一层不定元素的初始化:
vector< vector<int > > v;
定层数,但是每一层元素不固定的初始化:(最常用)
vector< vector<int > > v(5);
定层数,定元素的初始化:
vector< vector<int > > v (5,vector<int>(4,0) );
那么我们由彼及此,由于二维vector 的 第二维也是一个vector ,那么 v是一个vector , v[i] 自然也是一个vector咯。
v[ 1 ].push_back(b), 就代表在第一层,添加一个元素;
看到这里你可能会想:那第一种不定层数,不定元素的 二维vector 应该怎么加入层数呢?
我们观察它的初始化,不难发现,最外层的vector 的元素的数据类型 也是 vector,所以要想增加层数,则我们只要push_back( ) vector类型就行了。
现在你已经知道 了二维vector , 那么就来做一道题巩固一下吧!!!
传送门:P3613 【深基15.例2】寄包柜 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)https://www.luogu.com.cn/problem/P3613
题解:
#define _CRT_SECURE_NO_WARNINGS #include<iostream> #include<cstdio> #include<cmath> #include<string> #include<cstring> #include<string> #include<algorithm> #include<vector> #include<cctype> #include<map> #include<set> #include<queue> #include<numeric> #include<iomanip> using namespace std;int n, q, i, j, k, ops; int main() {cin >> n >> q;vector< vector<int> > v(n+10);while (q--) {cin >> ops;if (ops == 1) {cin >> i >> j>>k;if (v[i].size() <= j) { v[i].resize(j + 1);}v[i][j]=k;}else {cin >> i >> j;cout << v[i][j] << endl;}} }
现在你已经大致了解了 简单vector 的用法。下面我们来介绍一些好用的vector 函数,这些函数可以给我们解题带来极大的遍历。(待更新中……)
简单栈:
“ 啥是栈啊? ”
你可以把它想象成一个深坑。 嗯。。没了,这就是栈。
你任意在这个坑里面放一些元素(元素不能融合!)
你能想到的任何对这些元素的操作,都是栈操作。(不要杠我
栈的最常见的操作就是,压入,取出。
你可以想象,我们将一些物品用1~n标号,逐个放入这个坑中。我们想将1取出,那么必然要将1上面的物品取出,要想取出1上面的物品,那就要取出1上面的上面的物品…………
直到这个物品上方为空(即没有任何物体)
我们可以发现,这样的坑,有个明显的特点” 后进先出,先进后出 “,”只能从一端进行增删的操作“。
于是,聪明的数学家们,将满足上面性质的东西,称作栈结构。
所以啊,我们便不必局限于是不是一个坑了。任何满足这些性质的东西,我们都可以将它抽象成一个栈结构。
对于一个栈结构,我们一般会对其进行以下的操作:
stack < int > s;s. push(a) 将a压入栈中s.pop () 弹出栈顶元素s.top ()查询栈顶元素s.size ()查询栈内元素个数s.empty ()查询栈是否为空
我们再用一个例子来描述一下栈的简单操作流程:
假设现在有 1,2,3 三个数 , 我们依次将其压入栈,然后压入之后,再一个一个取出来。
stack <int > s;
s.push(1);
s.push(2);
s.push(3);cout<<s.top()<<endl;
s.pop();cout<<s.top()<<endl;
s.pop();cout<<s.top()<<endl;
s.pop()输出结果: 3 2 1
在我们初步了解了栈的基本操作之后,我们就应该试图手写一个栈。
ps:(栈虽然直接用stl方便,但是如果我们不打开 O2优化的话,就会有一点慢。
在一些非常需要追求运行速度的情况下,往往需要自己手写栈。)
手写栈:
#include<iostream>
using namespace std;const int MAXN = 1e5;
int stack[MAXN];
int p = 0; //栈顶指针void push(int x) {if (p >= MAXN) {cout << "Stack overflow" << endl;return;}p++;stack[p] = x;return;
}void pop() {if (p == 0) {cout << "Stack is empty";return;}p--;
}int top() {if (p == 0) {cout << "Stack is empty";return 0;}return stack[p];
}int size() {if (p == 0) {cout << "Stack is empty";return 0;}return p;
}bool empty() {if (p == 0)return true;else return false;
}
好啦,现在你已经大概知道了栈是如何工作的。
那么我们就做几道题来巩固一下吧。
例题1、
Parentheses Balance - UVA 673 - Virtual Judgehttps://vjudge.net/problem/UVA-673
输入一个包含“()”和“[]”的括号序列,判断是否合法。 具体规则:
- 空串合法;
- 如果A和B合法,那么AB合法;
- 如果A合法(A)和[A]都合法
输入输出样例:
输入 #1复制3 ([]) (([()]))) ([()[]()])()输出 #1复制
Yes No Yes
#include<bits/stdc++.h>
using namespace std;
char reverse(char ch){if(ch==')')return '(';if(ch==']')return'[';else return ' ';
}int main(){stack<char> s;int sum;cin>>sum;cin.ignore();string t;while(sum--){while(!s.empty())s.pop();getline(cin,t);for(int i=0;i<t.size();i++){if(s.empty()){s.push(t[i]);continue;}if(reverse(t[i])==s.top())s.pop();else s.push(t[i]);}if(s.empty())cout<<"Yes"<<endl;else cout<<"No"<<endl;}
}
例题2、后缀表达式 - 洛谷1449https://www.luogu.com.cn/problem/P1449
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
#include<cstdio>
#include<cmath>
#include<string>
#include<cstring>
#include<string>
#include<algorithm>
#include<vector>
#include<cctype>
#include<map>
#include<set>
#include<queue>
#include<numeric>
#include<iomanip>
#include<stack>
using namespace std;stack<int > S;
void todo(char ch,int first,int second) {if (ch == '*')S.push(first * second);else if (ch == '+')S.push(first + second);else if (ch == '-')S.push(second - first);else if (ch == '/')S.push(second / first);return;
}
int main() {string s;getline(cin, s);int first, second;int t = 0;for (int i = 0; i < s.size(); i++) {while (isdigit(s[i])) {t = t * 10 + s[i] - '0';i++;}if (s[i] == '.') {S.push(t);t = 0;continue;}else if (s[i] == '@')break;else {first = S.top();S.pop();second = S.top();S.pop();todo(s[i], first, second);}}cout << S.top();
}
简单队列:
这玩意,我们需要解释吗?啥是队列,超市买东西排队这就是队列。一端付完钱直接走人,叫队头。
一端挑完东西,加入队列,叫队尾。
所以,一端进入一端出去,先进先出就是队列。
如果我们看到一道题目满足这样的性质,我们就可以用队列来模拟。
那么,对于这样的队列,我们有啥操作呢?其实和栈是差不多了。
- queue< int > q ;
- 入队 (加入队尾) q.push()
- 出队 (从队首出队)q.pop()
- 查询队首 q. front()
- 查询队尾 q.back()
- 查询元素个数 q. size()
- 是否为空 q.empty ()
这样光说,肯定是枯燥的,那么我们就列举一个生活中常见的例子吧。
超市排队:(编写一个程序模拟收银过程)
超市排队模拟器:int queue[MAXN]; // 用数组模拟队列,MAXN 表示队伍一次性能加入的人数
int head=0; // 队头指针
int tail=0; // 队尾指针有人挑完东西,加入正在付款的队伍:
void push(int x){if( tail>MAXN ) cout<<" Queue overflow "<<endl;else {queue[tail]=x;tail++;}
}队伍最前面的人已经买完了东西,现在要走人
void pop(){if(head==tail){cout<<"Queue is empty"<<endl;}else{head++;}
}查询队伍最前面的人是谁
int front(){if(head==tail){cout<<"Queue is empty"<<endl;}else{return queue[head];}查询队伍最后面的人是谁
int back(){if(head=tail){cout<<"Queue is empty"<<endl;}else{return queue[tail-1];}查询队伍人数:
int size(){return tail-head;}
查询队伍是否为空:
bool empty(){
if(head==tail)return 1;
else return 0;
}
现在,你应该对队列的操作熟悉了吧?那下面来写一道经典的习题吧!!
P1996 约瑟夫问题 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)https://www.luogu.com.cn/problem/P1996ps(这道题可以优化成一个队列的写法,就是每次数到的数字整除m,先输出,再删除,,否则,先将数字加入队尾,然后再删除。
退出循环的条件就是这个队列为空
#include<bits/stdc++.h>
using namespace std;queue<int > id;
queue<int > temp;
queue<int > out;
//先将所有的人加入到id队列
// 将id队列的人一个一个出队,出到 temp临时队列
// 每次当数到的数字能够整除m,此时的人出队,进入到out队列。
// 然后循环完了一遍,将temp队列重新赋值到id队列,再将temp队列清空
// 结束条件就是out队列的数量大于等于 n
int main() {int n, m;cin >> n >> m;for (int i = 1; i <= n; i++)id.push(i);int count = 1;while (out.size()<=n) {if (out.size() >= n)break;int len = id.size()+count-1;for (; count <= len; count++) {if (count % m == 0)out.push(id.front());else temp.push(id.front());id.pop();}while (!temp.empty()) {id.push(temp.front());temp.pop();}
}while (!out.empty()) {cout << out.front() << ' ';out.pop();}
}
简单链表 :
啥是链表?链表是一种和上面我们讲过的栈,队列,数组 相似的线性的,储存元素排列顺序的表。
让我们再接触链表之前,先和之前一样从生活实际开始模拟。
假设n名同学排成一排,解散后,现在要求每个人重新排成原来的样子,但是没有人知道原来是怎么排的,好在每个同学都记得自己后面的第一个人是谁。利用这些信息,你能还原出初始的队列吗?
假如有4名同学编号从1~4,他们后面的同学分别是:4,3,0,2 (0代表后面无人)
(已经知道1号同学站在第一位)
不难写出代码:
int Next[MAXN];
for(int i=1;i<=MAXN;i++){cin>>Next[i];
}
for(int i=1;i!=0;i=Next[i]){cout<<i<<' ' ;
}
从这个问题,我们不难知道链表具有怎样的性质:
如果你知道每个元素的前面后面是谁,那么你就可以恢复整个表的顺序。
也就是说,链表,其实就是一种储存了每个元素前驱和后继的表。
如此,我们就得到了链表的重要特性,利用这个特性,我们可以做什么呢?
下面通过另一个问题来回答这个问题。
有n名同学正在排队,但是来了一位恶霸(y号)插队到了x号的后面,其余的同学顺序不变,求,插队之后,队伍是什么样的顺序?
int Next[MAXN];
void insert(int x,int y) {int temp = Next[x]; // 先储存x的后继Next[x] = y; // x的后继为yNext[y] = temp; // y的后继是原来x 的后继
}
通过这样的方式,我们就可以快速地实现插入这一步骤了,即时数据量再大也不怕捏。
那么再来思考一下这个:
假如,x同学的后面一个同学忍不住,先离开了,那么现在的队列应该是什么顺序呢?
void restore(int x) {Next[x] = Next[Next[x]];
}
通过以上的两种方法,我们可以只要耗费O(1)的时间复杂度就完成一次维护。
这是单链表的使用方法,但其实上,我们有很多类型的链表需要学习:(目前只了解单双链表即可)
- 单链表(每一个结点记录自己的后继,只能单向移动)
- 双链表(每一个结点记录自己的前驱和后继,可以双向移动)
- 循环单链表(尾部的后继是头部)
- 循环双链表
- 块状链表
- 跳表
老规矩下面进行一个排队的模拟(假设队伍里最开始只有编号1这一个人):
这是排队的程序应该有的功能
- ins_back(x,y); 将元素y插入到x的后面
- ins_bcak(x,y); 将元素y插入到x的前面
- ask_back(x); 询问x的后继是谁
- ask_front(x); 询问x的前驱是谁
- del(x); 从列表中删除元素,不改变其他元素的先后顺序。
这题,我们就可以用双向链表来维护:
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
#include<cstdio>
#include<cmath>
#include<string>
#include<cstring>
#include<string>
#include<algorithm>
#include<vector>
#include<cctype>
#include<map>
#include<set>
#include<queue>
#include<numeric>
#include<iomanip>
#include<stack>
using namespace std;
const int MAXN = 1e6 + 7;
struct node {int pre,nex,val; // 前驱,后继,值
}a[MAXN] = {0};
int n, pls;//相对位置
int flag[MAXN]; //标记数组,标记是否存在
int index1[MAXN];//用来记录每个结点编号void ins_pre(int x, int y) {//首先找到x的结点编号int now = pls++; //新增一个编号给y用a[pls].nex = now; // y的后缀等于 x的结点编号nowa[pls].val = y; // y的值等于y。a[pls].pre = a[now].pre; // y的前驱是x原来的前驱a[a[now].pre].nex = pls; // x原来的前驱的后继变成y的编号 plsa[now].pre = pls; // x的前驱是pls。index1[y] = pls;
}//将y元素插在x 的后面
void ins_back(int x, int y) {int now = index1[x];pls++;a[pls].pre = now; // 先把y的前驱改成xa[pls].nex = a[now].nex;//再把y的后继改成x的后继a[pls].val = y;//再把y的值附上a[a[now].nex].pre = pls;//然后改变x的后继的前驱a[now].nex = pls;//最后改变x的后继index1[y] = pls;
}//询问x的后继的值
int ask_back(int x) {return a[a[index1[x]].nex].val;// 先查询x的结点编号,然后返回x的后继的val,这里跳步了直接return
}
//查询x 的前驱的值
int ask_pre(int x) {return a[a[index1[x]].pre].val;
}//从链表中删除元素
void del(int x) {int now = index1[x]; // 找到x的结点编号a[a[now].pre].nex = a[now].nex;// 将x前驱的后继改成x的后继a[a[now].nex].pre = a[now].pre;// 将x后继的前驱改成x的前驱
}int main() {a[0].val = 0;a[0].pre = 0;a[0].nex = 0;index1[0] = 0;ins_back(0,1);
}
(如果数据过大,index1可能存不下那么多,那么可以用hash或map来优化)
相信看完前面,你对链表也已经有了一些理解,下面来几道简单的例题练练手吧!
例题1:
P1160 队列安排 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)https://www.luogu.com.cn/problem/P1160
#include<bits/stdc++.h>
using namespace std;
const int MAXN = 1e6 + 7;
struct node {int pre,nex,val; // 前驱,后继,值
}a[MAXN] = {0};
int n, pls;//相对位置
int flag[MAXN]; //标记数组,标记是否存在
int index1[MAXN];//用来记录每个结点编号void ins_left(int x, int y) {//把y插入到x的左边int now =index1[x];pls++;a[pls].val = y;a[pls].nex = now;a[pls].pre = a[now].pre;a[a[now].pre].nex=pls;a[now].pre = pls;index1[y] = pls;}
void ins_right(int x, int y) {int now = index1[x];pls++;a[pls].val = y;a[pls].pre = now;a[pls].nex = a[now].nex;a[a[now].nex].pre = pls;a[now].nex = pls;index1[y] = pls;}
void del(int x) {int now = index1[x];int Nex = a[now].nex;int Pre = a[now].pre;a[Nex].pre = Pre;a[Pre].nex = Nex;index1[x] = 0;
}
int main() {cin >> n;a[0].val = 0;a[0].pre = 0;a[0].nex = 0;ins_right(0, 1);for (int i = 2; i <= n; i++) {int k, p;cin >> k >> p;if (p == 0) {ins_left(k, i);}else ins_right(k, i);}int m;cin >> m;while (m--) {int temp;cin >> temp;if (index1[temp])del(temp);}int now = a[0].nex;while (now) {cout << a[now].val<<' ';now = a[now].nex;}
}
ps(虽然我没写注释,但是这道题和我们上面的模拟排队是几乎一样的,所以看懂一个就行 了。)
例题2:
P1996 约瑟夫问题 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)https://www.luogu.com.cn/problem/P1996
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
#include<cstdio>
#include<cmath>
#include<string>
#include<cstring>
#include<string>
#include<algorithm>
#include<vector>
#include<cctype>
#include<map>
#include<set>
#include<queue>
#include<numeric>
#include<iomanip>
#include<stack>
using namespace std;
const int MAXN = 1e6 + 7;
struct node {int pre,nex,val; // 前驱,后继,值node(int _pre=0, int _nex=0 , int _val=0 ) {pre = _pre, nex = _nex, val = _val;}
}a[MAXN] ;
int n, pls;//相对位置
int flag[MAXN]; //标记数组,标记是否存在
int index1[MAXN];//用来记录每个结点编号void ins_pre(int x, int y) {//首先找到x的结点编号int now = index1[x];pls++; //新增一个编号给y用a[pls] = node(a[now].pre, now, y);a[a[now].pre].nex = pls; // x原来的前驱的后继变成y的编号 plsa[now].pre = pls; // x的前驱是pls。index1[y] = pls;
}//将y元素插在x 的后面
void ins_back(int x, int y) {int now = index1[x];pls++;a[pls] = node(a[now].nex, now, y);a[a[now].nex].pre = pls;//然后改变x的后继的前驱a[now].nex = pls;//最后改变x的后继index1[y] = pls;
}//询问x的后继的值
int ask_back(int x) {return a[a[index1[x]].nex].val;// 先查询x的结点编号,然后返回x的后继的val,这里跳步了直接return
}
//查询x 的前驱的值
int ask_pre(int x) {return a[a[index1[x]].pre].val;
}//从链表中删除元素
void del(int x) {int now = index1[x]; // 找到x的结点编号int Nex = a[now].nex;int Pre = a[now].pre;a[Nex].pre = Pre;a[Pre].nex = Nex;index1[x] = 0;
}int main() {int n;cin >> n;for (int i = 1; i <= n; i++) {index1[i] = i;a[i] = node(i - 1, i + 1, i);}
//搞一个循环双链表。然后循环的次数大一点,一定是可以删除干净的。
//其实循环单链表好像也可以。a[1] = node(n, 2, 1);a[n] = node(n - 1, 1, n);int m;cin >> m;int now = index1[1];while (n--) {for (int i = 1; i <= m; i++) {if (i == m) {cout << now << ' ';del(now);}now = a[now].nex;}}}
好啦,上面是手写链表的过程,那么接下来我就介绍一下链表(stl):list
下面给出链表的常用函数:
定义一个int类型的链表
list<int > a;
我们可以用这样的方式来给链表初始化。
int arr[5] = {1,2,3} ; list<int > a(arr.arr+3 ) ;
返回链表的节点数量:
a.size();
定义一个迭代器:
list<int >:: iterator it ;
链表的开头,和末尾( 返回的是迭代器)
a.begin() , a.end() ;
在链表的开头或者末尾插入元素x
a.push_front (x)
a.push_back(x) ;
在链表某一位置的前面插入元素x:
a.insert( it,x ) ; it 表示这个位置的迭代器
在链表开头或结尾删除元素
a.pop_front () , a.pop_bcak();
删除链表某一位置的元素
a.erase(it) it表示这一位置的迭代器
遍历整个链表
for( it=a.begin() ; it!=a.end() ; it++)
有了上面的那些函数,我们就可以实现如下功能
- ins_back(x,y); 将元素y插入到x的后面
- ins_bcak(x,y); 将元素y插入到x的前面
- ask_back(x); 询问x的后继是谁
- ask_front(x); 询问x的前驱是谁
- del(x); 从列表中删除元素,不改变其他元素的先后顺序。
const int MAXN = 1e6 + 7; list<int > a; list<int >::iterator index1[MAXN]; // 迭代器数组,用来代替findvoid ins_front(int x, int y) { //y插入x的前面auto it = index1[x];a.insert(it, y);index1[y] = --it; // y的迭代器就是it的前一个位置 } void ins_back(int x, int y) { //yc插入 x的后面auto it = index1[x];it++;a.insert(it, y);index1[y] = --it; } void del(int x) { //删除xif (index1[x] == a.end())return;auto it = index1[x];a.erase(it);index1[x] = a.end(); } int ask_front(int x){auto it=index1[x];return *(--it); } int ask_back(int x){auto it=index1[x];return *(++it); } void print() {for (auto it = a.begin(); it != a.end(); it++) {cout << *it<<' ';} }
P1996 约瑟夫问题 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)https://www.luogu.com.cn/problem/P1996
#include<bit/stdc++.h>
using namespace std;
const int MAXN = 1e6 + 7;
list<int > a;
int main() {int n, m;cin >> n >> m;for (int i = 1; i <= n; i++) {a.push_back(i);}list<int >::iterator it, now;it = a.begin();int count = 0;while (!a.empty()) {count++;now=it;if (++it == a.end())it = a.begin();if (count % m == 0) {cout << *now << ' ';a.erase(now);}}
}
P1160 队列安排 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)https://www.luogu.com.cn/problem/P1160
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
#include<cstdio>
#include<cmath>
#include<string>
#include<cstring>
#include<string>
#include<algorithm>
#include<vector>
#include<cctype>
#include<map>
#include<set>
#include<queue>
#include<numeric>
#include<iomanip>
#include<stack>
#include<list>
using namespace std;
const int MAXN = 1e6 + 7;
list<int > a;
list<int >::iterator index1[MAXN];void ins_front(int x, int y) {auto it = index1[x];a.insert(it, y);index1[y] = --it; // y的迭代器就是it的前一个位置
}
void ins_back(int x, int y) {auto it = index1[x];it++;a.insert(it, y);index1[y] = --it;
}
void del(int x) {if (index1[x] == a.end())return;auto it = index1[x];a.erase(it);index1[x] = a.end();
}
void print() {for (auto it = a.begin(); it != a.end(); it++) {cout << *it<<' ';}
}
int main() {int n;cin >> n;a.push_back(1);index1[1] = a.begin();for (int i = 2; i <= n; i++) {int k, p;cin >> k >> p;if (p == 0)ins_front(k, i);else ins_back(k,i);}int m;cin >> m;while (m--) {int temp;cin >> temp;del(temp);}print();
}
简单二叉树:
二叉树,顾名思义是一种树。不过,我们在数据结构中研究的树,一般都是“倒过来的”
就像这样:
所以,越上面越接近根部,越下面越接近叶子。
那么最上面的点,就叫做根结点,其他的点就是普通结点,最下面的点(就是没有任何分支的点)
叫做叶子节点。
那么二叉树是什么呢?就是每一个结点的分支不超过两个的树。
如上面那张图片,每个结点都有两个分支,我们管一个结点的左右分支叫(左右子树)
这样的结构就是完美二叉树。
那么对于完美二叉树而言:
如果从根部开始标号1,然后下一层从左到右开始往下标号。
我们会发现这样的规律:
假设某一结点的编号为x ,那么其左子树的根节点编号为:2*x,右子树的根节点为:2*x+1
若有n层 ,那么总结点的数量就是:2^n-1
下面给出一个思考问题:
假如有 2^n (n<=7 )个国家参加淘汰赛,要角逐出冠军。现在,他们两两进行pk,赢的国家进入下一环节的淘汰赛,如此往复,直到出现冠军。现在给你每个国家的编号,和能力值,
对于编号:1号和2号pk 、3号和4号pk……如此以往。
对于能力值:能力值高的一方获胜。
那么我们对于这个问题,很明显画出来是一个满二叉树的结构,先把能力值放在叶子节点,然后从叶子结点开始比较,能力值大的将信息传递给上一层的根节点,直到得到根节点。
我们不难发现这是一个有规律的行为(其实就是递归
并且我们如果得到了根节点的话,必然是把整棵树都扫描了一遍。
我们把这种行为叫做二叉树的遍历(就是用dfs
ok , 现在我们已经得到了一种简单的性质,我们需要将它运用在实战之中:
例题1:
P4715 【深基16.例1】淘汰赛 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)https://www.luogu.com.cn/problem/P4715题解1:
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
#include<cstdio>
#include<cmath>
#include<string>
#include<cstring>
#include<string>
#include<algorithm>
#include<vector>
#include<cctype>
#include<map>
#include<set>
#include<queue>
#include<numeric>
#include<iomanip>
#include<stack>
#include<list>
using namespace std;
const int N = 1e6 + 7;
struct tree {int winner, power, id;
}tree1[N];
int n;void dfs(int x) {if(x>=(1<<n))return ;dfs(2 * x);dfs(2 * x + 1);int left = 2*x , right = 2*x+1;if (tree1[left].power > tree1[right].power) {tree1[x].winner = tree1[left].winner;tree1[x].power = tree1[left].power;}else {tree1[x].winner = tree1[right].winner;tree1[x].power = tree1[right].power;}return;}
int main() {cin >> n;int ans = 1;for (int i = 1<<n; i < 1<<(n+1); i++) {cin >> tree1[i].power;tree1[i].winner = ans;ans++;}dfs(1);//从根节点开始搜索tree1[2].power > tree1[3].power ? cout << tree1[3].winner : cout << tree1[2].winner;
}