什么是线性表?
线性表是由n个相同类型的数据元素组成的有限序列,它是最基本的一种线性结构。顾名思义,线性表就像是一条线,不会分叉。线性表有唯一的开始和结束,除了第一个元素外,每个元素都有唯一的直接前驱,除了最后一个元素外,每个元素都有唯一的直接后继。线性表有两种存储方式,分别是顺序存储和链式存储。采用顺序存储的线性表称为顺序表,采用链式存储的线性表称为链表。链表又分为单链表、双向链表和循环链表。
顺序表:顺序表采用顺序存储方式,元素存储是连续的,中间不允许有空,因此顺序表可以快速定位是第几个元素,但是插入和删除需要移动大量的元素。
顺序表的基本操作:
(1)取值:顺序表中的任何一个元素都可以立即找到,称为随机存取方式。例如,要取第i个元素,只要i值合理,可以找到该元素,由于下标是从0开始的,因此第i个元素,其下标为i-1。线性表取值的时间复杂度为O(1)。
(2)查找:在顺序表中查找一个元素e,可以从第一个元素开始顺序查找,依次比较每一个元素值,如果相等,则返回元素位置(位序,即第几个元素),如果查找整个顺序表都没找到,则返回-1。线性表取值的时间复杂度为O(n)。
(3)插入:在顺序表中第i个位置之前插入一个元素e,需要从最后一个元素开始,后移一位,…,直到把第i个元素也后移一位,然后把e放入第i个位置。线性表取值的时间复杂度为O(n)。
(4)删除:在顺序表中删除第i个元素,需要把该元素暂存到变量e,然后从i+1个元素开始前移,…,直到把第n个元素也前移一位,即可完成删除操作。线性表取值的时间复杂度为O(n)。
顺序表的优点:操作简单,存储密度高,可以随机存取,只需要O(1)的时间就可以取出第i个元素。
顺序表的缺点:需要预先分配最大空间,最大空间估计过大或过小会造成空间浪费或者溢出。插入和删除操作需要移动大量的元素。
在实际问题中,如果经常需要插入和删除操作,则顺序表的效率很低。为了克服该缺点,可以采取链式存储。我们首先来看看最简单的单链表。
单链表:可以给每个元素附加一个指针域(或者数组),指向下一个元素的存储位置。如下图所示,每个节点包含两个域,数据域和指针域。数据域存储数据元素,指针域存储下一个节点的地址。每个指针都指向下一个节点,都是朝着一个方向的,这样的链表称为单链表。
取值:单链表的取值不像顺序表那样可以随机访问任何一个元素,必须从第一个结点开始按顺序向后找,一直找到第i个结点。单链表取值的时间复杂度为O(n)。
查找:在一个单链表中查找是否存在元素e,可以定义一个p 指针,指向第一个元素结点,比较p指向结点的数据域是否等于e。单链表查找的时间复杂度为O(n)。
插入:如果要在第i个结点之前插入一个元素,则必须先找到第i-1个结点,想一想:为什么?单链表插入的时间复杂度为O(1)。
删除:删除一个结点,实际上是把这个结点跳过去。根据单向链表向后操作的特性,要想跳过第i个结点,就必须先找到第i-1个结点。单链表插入的时间复杂度为O(1)。
在单链表中,每个节点除了存储自身的数据之外还存在了下一个节点的地址,因此可以轻松的访问下一个节点,以及后面的所有的后继节点。但是,如果想访问前面的节点就不行了,再也回不去了。单链表只能向后操作,不可以向前操作,该怎么办呢?因此,还有另外一种链表——双向链表。
双向链表:为了向前、向后操作方便,可以给每个元素附加两个指针域,一个存储前一个元素的地址,另一个存储下一个元素的地址。
那么除了双向链表之外,循环链表也可以解决单链表只能向后操作,不可以向前操作。所谓的循环链表就是让单链表的最后一个节点的指针指向头节点,这样就形成了一个环,这样就可以从任何一个节点出发,访问所有的节点了。循环链表和普通链表的区别就是最后一个节点的后继指向了头节点。
链表的优点:链表是动态存储的,不需要预先分配最大空间,插入和删除不需要移动元素。
链表的缺点:每次动态分配一个节点,每个节点的地址是不连续的,需要有指针域记录下一个节点的地址,指针域需要占用一个int的空间,因此存储密度低。其次存取的时间复杂度较大。说完了什么是线性表之后,我们再来看看什么是栈。
什么是栈?
小李攒钱买了车,可是他家住在胡同的尽头。胡同很窄,只能通过一辆车,而且是死胡同。小李每天都为停车发愁,如果回家早了停在里面,早上上班就要让所有人挪车,先让最外面的车出去,然后挨着一辆一辆的出去,这样小李才能去上班。胡同很窄,只能通过一辆车,而且是死胡同,所以只能从胡同口进出。小汽车呈线性排列,只能从一端进出,后进的汽车先出去,如下图所示:
这种后进先出的线性序列,称为栈。栈是一种线性表,只不过是它是操作受限的线性表,只能在一端进行进出操作。进出的一端称为栈顶(top),另一端称为栈底(base)。栈可以用顺序存储,也可以用链式存储,分别称为顺序栈和链栈。这里我们只侧重讲顺序栈,链栈了解即可。
假设栈为∶s=(a1,a2,…,an),那么a1为栈底元素,而an是栈顶元素。栈中的元素是按照a1、a2、…、an的顺序进栈,退栈的第一个元素应为栈顶元素。由于其先进后后出的特点,栈又称为先进后出(First In Last Out, FILO)的线性表。
入栈和出栈
入栈:入栈前要判断是否栈满,如果栈已满,则入栈失败;否则将元素放入栈顶,栈顶指针向上移动一个位置(top++)。
出栈:出栈前要判断是否栈空,如果栈是空的,则出栈失败;否则将栈顶元素暂存给一个变量,栈顶指针向下移动一个空间(top--)。
stack简介
栈的英文名就是stack,在C++的STL标准库中提供了一个容器stack来实现栈的基本操作,可以方便的进行出入栈的操作。
使用标准库的栈和队列时,先包含相关的头文件#include<stack>
定义栈如下:stack<int> s
;s.empty()
如果栈为空返回true,否则返回falses.size()
返回栈中元素的个数s.pop()
删除栈顶元素s.top()
返回栈顶的元素s.push()
在栈顶压入新元素**
stack没有提供遍历所有元素的方法,不支持下标操作。因此入栈和出栈都需要使用循环实现。下面的代码是利用stack模拟实现1-10个数字的入栈和出栈操作。
#include<iostream>
#include<stack> //栈的头文件
using namespace std;
int main()
{
stack<int> s; //定义栈
for(int i=1; i<=10; i++) //将1-10入栈
{
s.push(i);
}
while(!s.empty()) //首先输出栈顶元素,然后将栈顶元素出栈,循环执行,直到栈为空。
{
cout <<s.top()<<" "; //输出首元素
s.pop(); //栈顶元素出栈
}
return 0;
}
Copy
栈相关训练:其实栈的定义以及基本操作都很好理解,但是在一道题目里面要不要用栈,该如何用栈其实还是有一定难度的。我们不妨通过下面几个习题来体会下栈的美妙之处。
训练1:括号匹配
给定一个只包含左右括号的合法括号序列,按右括号从左到右的顺序输出每一对配对的括号出现的位置(括号序列以0开始编号)。括号序列长度不超过100。
输入格式:仅一行,表示一个合法的括号序列。
输出格式:设括号序列有n个右括号。则输出包括n行,每行两个整数l,r,表示配对的括号左括号出现在第l位,右括号出现在第r位。
输入输出样列:
输入样例:
(())()
输出样例:
1 2
0 3
4 5
Copy
解题思路:因为本题出现在栈的训练里面,大家首选用栈去解决,那么如果不是呢?你能看出来这是栈的应用吗?从样例中其实不容易看出规律,我们举一个特殊的例子,例如有这样一个括号((((())))),如果我们想找第一个匹配的括号,其实是找第一个右括号出现的位置,找到右括号的位置之后就是找离它最近的左括号,离他最近的左括号就是在左括号序列里面的最后一个,说到这里,大家是不是感觉有点像栈了,最后进去的左括号先出来,这不就是后进先出的栈吗?想到这里,题目就变得很简单了。我们定义一个栈,然后输入括号字符串,如果是左括号就先保存在栈里面,如果不是(即为右括号)则找离它最近的左括号的位置即栈顶,s.top()表示左括号位置,i表示右括号位置,需要注意的是,当这个左括号完成匹配之后需要出栈。完整的代码如下所示:
#include <iostream>
#include <stack>
#include <string>
using namespace std;
string a; //括号序列
stack<int> s; //定义栈
int main()
{
cin >> a;
for(int i=0; i<a.size(); i++)
{
if(a[i] == '(') s.push(i); //如果是左括号将其位置入栈
else //如果是右括号,则这个时候离它最近的左括号一定在栈顶
{
cout << s.top() << ' ' << i << endl; //s.top()表示最近左括号的位置,i代表右括号的位置
s.pop(); //该左括号匹配完之后需要出栈
}
}
return 0;
}
Copy
训练2:括号匹配升级(主题库1466)
假设表达式中允许包含圆括号和方括号两种括号,其嵌套的顺序随意,如或[([ ][])]等为正确的匹配,[( ])或(或(( )))均为错误的匹配。本题的任务是检验一个给定表达式中的括号是否正确匹配。输人一个只包含圆括号和方括号的字符串,判断字符串中的括号是否匹配,匹配就输出“0K” ,不匹配就输出“Wrong“。
输入:一行字符,只含有圆括号和方括号,个数小于255。
输出:匹配就输出一行文本“0K",不匹配就输出一行文本“Wrong“。
样例输入:
[(])
样例输出:
Wrong
解题思路:有了上一题的经验之后,以后在遇到类似括号匹配的问题大家自然而然想到用栈的思想去解决问题,这也就是为什么要多刷题的原因。完整的代码如下:
#include<bits/stdc++.h>
using namespace std;
stack<char> a; //字符型栈
char t;
int main() {
while(cin>>t) {
if(t=='(') a.push(t); //左小括号入a栈
if(t=='[') a.push(t); //左中括号入a栈
if(t==')') { //如果是右小括号
if(a.empty()==1||a.top()!='(') { //如果这个时候a栈是空的,或者a栈的栈顶不是小括号,表示这个时候匹配不上,输出Wrong
cout<<"Wrong";
return 0;
}
a.pop(); //如果匹配上了,则出栈
}
if(t==']') { //同理
if(a.empty()==1||a.top()!='[') {
cout<<"Wrong";
return 0;
}
a.pop();
}
}
if(a.empty()==1) cout<<"OK"; //右边的找完了,这个时候a栈也应该空了,因此空了就是ok,不空就是wrong,该判断易忽略
else cout<<"Wrong";
return 0;
}
Copy
训练3:车站问题
PopPush城市有一座著名的火车站。这个国家到处都是丘陵。而这个火车站是建于上一个世纪。不幸的是,那时的资金有限。所以只能建立起一条路面铁轨。而且,这导致这个火车站在同一个时刻只能一个轨道投入使用,因为它缺少空间,两列火车将无路可走。具体看下图。
当地的惯例是每一列火车从A方向驶向B方向时候,会用某种方式将车厢重组。假设火车将要到达A方向,拥有N个车厢(N<=1000),这些车厢按照递增顺序标记为1到N。
为了重组车厢,要求按照特定的顺序进入B方向的铁轨,驶出车站。你可以借助中转站C,C是一个可以停放任意多节车厢的车站,但是由末端封顶,驶入C车站的车厢必须按照相反的顺序驶出C(后进先出),对于每个车厢,一旦从A进入C,就不能再回到A,一旦从C驶入B就不能再回到C。
现在你需要判断火车是否能够按照某种特定的顺序进入B方向的铁轨驶出车站。例如上图,出站顺序(5 4 1 2 3)是不可能的,但(5 4 3 2 1)是可能的。
输入格式:输入由多个数据集组成,
每一个数据集的第一行是一个整数N,表示车厢的数量,中间有多行,每一行表示一个被要求重组后的车厢序列,每一个数据集的最后一行是一个数字0,表示此数据集结束。最后一个数据集只有一个0,表示测试数据结束。
输出格式:针对每组测试数据,输出多行,与输入数据中的多行重组车厢序列一一对应,如果这行车厢序列能够实现,则输出Yes,否则输出No。针对多组测试数据的输出,以一个空行隔开。
输入样例:
5
1 2 3 4 5
5 4 1 2 3
0
6
6 5 4 3 2 1
0
0
Copy
输出样例:
Yes
No
Yes
Copy
解题思路:同样还是一道栈的模拟题,是不是又无从下手,就连这道题的输入输出格式都有点难度。对于比较复杂的题目,分析样例是非常重要的。那我们就一起来分析一下样例。首先输入1,2,3,4,5,输出yes,为什么呢?显然,a车站的数字1经过车站c之后直接去了车站b,接着a车站的数字2经过车站c之后又直接去了车站b,以此类推,车站b的编号就是1,2,3,4,5了。那么为什么输入5,4,1,2,3,输出则为no呢?统一b车站第一个数字是5,那么5是怎么来的呢?稍微思考一下发现,首先a车站把编号1,2,3,4,5的货物存在车站c里面,这时候c就好比一个栈,栈顶元素为5,把c的栈顶元素给b,则b的第一个元素就是5了。接着b车站的第二个数字4就是剩下栈c的栈顶,接着b车站的第三个数字1,而这个是c的栈顶为3,因此该组车厢序列是无法实现的。聪明的同学到这里,其实已经看出了本题的要害了。其实对于b车站中车厢,它只有两种方式获取,一种是直接从a车站拿(其实是先从a到c,再从c直接到b,中间不停留),另外一种就是从c的栈顶获取。因此对于b车站的车厢我们可以分成两种情况讨论。第一种情况,b车站里面的数正好是a站的第一个数字,则这个时候,该数字从a经过c的栈顶直接进入 b,这个时候a和b的位置都后移一个;第二种情况,即b车站里面的数正好是c栈顶的数。最后,如果两种情况都不符合,则a车站的数进入c车站,即入栈。完整的代码如下:
#include <iostream>
#include <stack>
using namespace std;
const int N = 1010;
stack<int> c; //定义栈c
int n, b[N]; //b数组存放 B 车站的车厢顺序
int main()
{
while(cin >> n && n!=0){ //输入n(n!=0)执行循环
while(cin >> b[1] && b[1]!=0){ //读取 B 车站的车厢顺序(可能为0,因此需要单独对第一个元素进行判断)
for(int i=2; i<=n; i++)
{
cin >> b[i];
}
int a = 1, t = 1; //a 和 t 分别记录 A、B 车站当前最前面的车厢。
while(t <= n) { //当t <= n的时候,分为三种情况(想一想为什么?)
if(b[t]==a) {a++, t++;} //第一种情况
else if (!c.empty() && c.top() == b[t]) { //第二种情况,
c.pop(); t++; //c栈顶出栈,b位置后移一个
}
else if (a<=n) c.push(a++); //以上两种情况都不符合,a入c栈
else break; //不满足条件,结束循环
}
if(t <= n) cout << "No" << endl; //这个if语句是什么意思?
else cout << "Yes" <<endl;
}
cout << endl;
}
return 0;
}
Copy
栈相关习题训练:参考主题库数据结构——栈章节。
队列:
还记得我们之前讲过的买了新车的小李吗?为了解决胡同停车问题,小李跑了无数次居委会,终于将挡在胡同口的建筑清除了。这样住在胡同尽头的小李就可以早早回家把车停在最里面,并且每天都第一个开车去上班了。现在胡同被打通了,但是胡同还是很窄,只能通过一辆小车。小汽车呈线性排列,只能从一端进,另一端出,如下图所示,先进先出。
这种先进先出(First In First Out,FIFO) 的线性序列,称为“队列”。队列也是一种线性表,只不过它是操作受限的线性表,只能在两端操作:一端进,一端出。进的一端称为队尾(rear),出的一端称为队头(front)。 队列可以用顺序存储,也可以用链式存储。这里我们只侧重讲队列的顺序存储,队列的链式存储了解即可。
假设队列为∶q=(a1,az,…,an),那么a1就是队头元素,而an是队尾元素。队列中的元素是按照 a1、a2、…、an的顺序进入的,退出队列时也只能按照这个顺序依次退出,也就是说只有在a1,a2,…,an-1都离开队列以后,an才能退出队列。
queue简介
队列的英文名就是queue,在C++的STL标准库中提供了一个容器queue来实现队列的基本操作,可以方便的进行队列的相关操作。
使用STL标准库的队列时,先包含相关的头文件:#include<queue>
定义队列如下:queue<int> q
;q.empty()
如果队列为空返回true,否则返回falseq.size()
返回队列中元素的个数q.pop()
删除队列首元素q.front()
返回队首元素的值q.push()
在队尾压入新元素q.back()
返回队列尾元素的值**
注意∶跟stack类似,queue没有提供遍历所有元素的方法,不支持下标操作。如果需要遍历queue中所有元素,需要一遍调用front(),一边调用pop()。
队列相关训练:其实队列的定义以及基本操作都很好理解,但是在一道题目面前要不要用队列,该如何用队列其实还是有一定难度的。我们不妨通过下面几个习题来体会下栈的美妙之处。
例题1:队列的使用
使用STL中的queue,实现输入n个数,之后依次出队,输出他们的头元素、尾元素、当前队列中元素个数、当前队列是否为空。
输入格式:第一行输入n,第二行输入n个不同的数
输出格式:共n+1行,每一行输出如题所示,中间用空格隔开
样例输入:
5
4 8 7 2 6
Copy
样例输出:
4 6 5 0
8 6 4 0
7 6 3 0
2 6 2 0
6 6 1 0
0 6 0 1
Copy
解题思路:一开始的队列是4,8,7,2,6,因此这个时候的头元素是4、尾元素是6、当前队列中元素个数是5、当前队列不为空是0;当i=2是,第一个元素4出队列,则队列中还剩下8,7,2,6,因此这个时候的头元素是8、尾元素是6、当前队列中元素个数是4、当前队列不为空是0......
#include<bits/stdc++.h> //万能头文件包含#include<queue>
using namespace std;
int main()
{
queue<int> a; //定义一个int类型的队列a
int n,i,t;
cin>>n;
for(i=1;i<=n;i++)
{
cin>>t;
a.push(t); //把输入的数字存到队列里面,注意存放的顺序
}
for(i=1;i<=n+1;i++)
{
cout<<a.front()<<" "; //输出队列的第一个元素
cout<<a.back()<<" "; //输出队列的最后一个元素
cout<<a.size()<<" "; //输出队列有多少个元素
cout<<a.empty()<<endl; //判断队列是否为空,是为1,否为0
if(a.empty()==0)
{ a.pop(); }
}
}
Copy
例题2:拓尔思舞会2(主题库2627)
和上题一样,男士们和女士们进入舞厅时,各自排成一队配舞伴。跳完一轮后的男女接到队伍尾部继续排队。不同的是,匹配的过程中,如果发生男女的舞力值相差超过10,那么舞力值低的那一位会离开舞厅,舞力值高的留在原地继续等待匹配下一个舞伴。如果在k支舞曲结束前,有一只队伍全员离开了,舞会就结束了,输出“Over”。
输入格式:第1行两个正整数,表示男士人数m和女士人数n (1≤m,n≤1000)
第2行m个正整数,表示每位男士的舞力值,第3行n个正整数,表示每位女士的舞力值,第4行一个正整数,表示舞曲的数目k (k≤1000)。
输出格式:不超过k行,每行为两个数,用一个空格隔开表示配对舞伴的序号,男士在前,女士在后。如果匹配截止输出Over
样例输入:
2 3
1 15
2 6 11
7
Copy
样例输出:
1 2
15 6
1 11
15 6
1 11
15 6
1 11
Copy
解题思路:请自行补充。
#include<bits/stdc++.h>
using namespace std;
int main()
{
int m,n,i,j,t,k;
cin>>m>>n;
queue<int>q1,q2;
for(i=1;i<=m;i++){ cin>>t; q1.push(t); }
for(i=1;i<=n;i++){ cin>>t; q2.push(t); }
cin>>k;
while(k--)
{ int t1=q1.front(),t2=q2.front();
if(abs(t1-t2)<=10) //如果舞力值相差小于10更上一题思想一致
{ cout<<t1<<" "<<t2<<endl;
q1.pop();q2.pop();
q1.push(t1);q2.push(t2);
}
else //如果舞力值相差大于10
{ if(t1<t2) q1.pop();
else q2.pop(); //较小的出队列
if(q1.size()==0||q2.size()==0)
{ cout<<"Over";return 0; } //如果没人了就输出“Over”
k++;
}
}
}
Copy
例题3: 取牌游戏(主题库1455)
小明正在使用一堆共K张纸牌与N-1个朋友玩取牌游戏。其中,N≤K<100000,2≤N≤100,K是N的倍数。纸牌中包含M=K/N张“good”牌和K-M张“bad”牌。
小明负责发牌,他当然想自己获得所有"good”牌。他的朋友怀疑他会欺骗,所以他们给出以下一些限制,以防小明耍诈:
1)游戏开始时,将最上面的牌发给小明右手边的人。
2)每发完一张牌 ,他必须将接下来的P张牌(1≤P≤10)一张一张地依次移到最后,放在牌堆的底部。
3)以逆时针方向,连续给每位玩家发牌。
小明迫切想赢,请你帮助他算出所有“good”牌放置的位置,以便他得到所有“good”牌。牌从上往下依次标注为1、2、3 ...
输入输出格式
输入格式:第1行,3个用一个空格间隔的正整数N、K和P。
输出格式:M行,从顶部按升序依次输出“good"牌的位置。
样例输入:
3 9 2
Copy
样例输出:
3
7
8
Copy
解题思路:请自行补充。
#include<bits/stdc++.h>
using namespace std;
int main()
{
int n,k,p,i,j=0,s=0,t=0,b[50001];
cin>>n>>k>>p;
queue<int>q; //声明int类型队列q
for(i=1;i<=k;i++) { q.push(i); } //把k张牌存到队列里面去
while(1)
{ s++; //s表示发牌的编号
if(s%n==0) //因为是逆序发牌,所以第n张是发给小明自己的
{
j++; b[j]=q.front(); //把这个时候的牌的编号存在b数组里面
t++; if(t==(k/n)) { break; } //一共只有k/n张good牌,找到了,循环也就结束了
}
q.pop(); //每发一张,出队一张
for(i=1;i<=p;i++) //循环移动p张牌
{
q.push(q.front()); //每次把队首放到队尾的位置
q.pop();
}
}
sort(b+1,b+1+j); //从小到大排序,根据题意
for(i=1;i<=j;i++)
{
cout<<b[i]<<endl;
}
return 0;
}
Copy
例题4:Blah数集(主题库1456)
该题比较难,有兴趣的同学可以去拓拓学编程看看讲解视频。
数学家高斯小时候偶然间发现一种有趣的自然数集合Blah。对于以a为基的集合Blah 定义如下:
1)a是集合Blah的基,且a是Blah的第一个元素:
2)如果x在集合Blah中,则2x+1和3x+1也都在集合Blah中:
3)没有其他元素在集合Blah中了。
现在小高斯想知道如果将集合Blah中元素按照升序排列,第n个元素会是多少?注意:集合中没有重复的元素。
输入格式:一行两个正 整数,分别表示集合的基a以及所求元素序号n,1≤a≤50,1≤n≤1000000。
输出格式:一行一个正整数,表示集合Blah的第n个元素值。
样例输入:
1 100
Copy
样例输出:
418
Copy
解题思路:请自行补充。
int a,n,s=1;
cin>>a>>n;
queue<int>q1,q2;
while(1)
{
q1.push(2*a+1);
q2.push(3*a+1);
if(q1.front()<q2.front())
{ a=q1.front(); q1.pop(); }
else
{ if(q1.front()>q2.front())
{ a=q2.front(); q2.pop(); }
else
{ a=q1.front(); q1.pop();q2.pop(); }
}
s++;
if(s==n)
{ cout<<a;return 0; }
}