线性表
- 线性表就像是一条线,不会分叉。线性表有唯一的开始和结束,除了第1个元素,每个元素都有唯一的直接前驱;除了最后一个元素,每个元素都有唯一的直接后继。
- 线性表有两种存储方式:顺序存储和链式存储。采用顺序存储的线性表被称为顺序表,采用链式存储的线性表被称为链表。
顺序表
- 顺序表是顺序存储方式,即逻辑上相邻的数据在计算机内的存储位置也是相邻的。在顺序存储方式中,元素存储是连续的,中间不允许有空。
- 顺序表可以分为静态分配和动态分配两种。
- 用定长数组存储的方法被称为静态分配:
#define Maxsize 100//最大空间
typedef struct{
int data[Maxsize];
int length;//顺序表的长度
}SqList;//用typedef将结构体等价于类型名
- 动态分配结构体定义:
typedef struct{
int *pr;//基地址,前面加*表示取地址中的内容
int length;//顺序表的长度
}SqList;//用typedef将结构体等价于类型名
插入
步骤
- 判断插入位置是否合法,可以在第1个元素之间插入,也可以在第个元素之前插入。
- 判断顺序表的存储空间是否已满。
- 将第至第个元素依次向后移动一个位置,空出第个位置。
- 将要插入的新元素放入第个位置。
- 表长加1。
实现
#include <iostream>
using namespace std;
#define Maxsize 100//最大空间
typedef struct{
int *elem;
int length;//顺序表的长度
}SqList;//用typedef将结构体等价于类型名
bool ListInsert_Sq(SqList &L,int i,int e);//插入数值
void InitList(SqList &L);//初始化顺序表
void AssginList(SqList &L,int x[],int size);//为顺序表赋值
void show(const SqList &L);//打印数值元素
int main() {
SqList cs;
InitList(cs);
int num[8] = {3,5,6,7,2,8,10,1};
AssginList(cs,num,8);
show(cs);
ListInsert_Sq(cs,4,9);
show(cs);
}
bool ListInsert_Sq(SqList &L,int i,int e){
if (i<1 || i>L.length+1){//判断i值是否合法
return false;
}
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;
}
void InitList(SqList &L) {
L.elem = new int[10];
L.length = 0;
}
void AssginList(SqList &L,int x[],int size) {
for (int i = 0; i < size; i++) {
L.elem[i] = x[i];
L.length++;
}
}
void show(const SqList &L){
for (int i = 0; i < L.length; ++i) {
cout << L.elem[i] << " ";
}
cout <<endl;
}
输出:
3 5 6 7 2 8 10 1
3 5 6 9 7 2 8 10 1
删除
步骤
- 判断插入位置是否合法。
- 将欲删除的元素保留在中。
- 将第至第个元素依次向前移动一个位置。
- 表长减1,若删除成功则返回
true
实现
#include <iostream>
using namespace std;
typedef struct{
int *elem;
int length;//顺序表的长度
}SqList;//用typedef将结构体等价于类型名
bool ListDelete_Sq(SqList &L,int i,int &e);//删除元素
void InitList(SqList &L);//初始化顺序表
void AssginList(SqList &L,int x[],int size);//为顺序表赋值
void show(const SqList &L);//打印数值元素
int main() {
SqList cs;
InitList(cs);
int num[8] = {3,5,6,7,2,8,10,1};
AssginList(cs,num,8);
show(cs);
int bl;//保留删除元素
ListDelete_Sq(cs,4,bl);
show(cs);
cout << bl;
}
bool ListDelete_Sq(SqList &L,int i,int &e){
if (i<1 || i>L.length){//判断i值是否合法
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--;
return true;
}
void InitList(SqList &L) {
L.elem = new int[10];
L.length = 0;
}
void AssginList(SqList &L,int x[],int size) {
for (int i = 0; i < size; i++) {
L.elem[i] = x[i];
L.length++;
}
}
void show(const SqList &L){
for (int i = 0; i < L.length; ++i) {
cout << L.elem[i] << " ";
}
cout <<endl;
}
输出:
3 5 6 7 2 8 10 1
3 5 6 2 8 10 1
7
单链表
- 链表是线性表的链式存储方式,可以给每个元素都附加一个指针于,指向下一个元素的存储位置。
- 每个节点都包含两个域:数据域和指针域。数据域存储数据元素,指针域存储下一个节点的地址,因此指针指向的类型也是节点类型。
- 在单链表中找第i个元素,必须从头开始,按顺序一个一个地找,一直数到第i个元素,被称为顺序存取。
- 单链表节点结构体定义:
typedef struct Lnode{
int data;
struct Lnode *next;//指向下一个节点的指针
}Lnode,*Linklist;
//用typedef将结构体等价于类型名Lnode,指针Linklist
插入
在第i
个结点之前插入元素e
,相当于在第i-1
个节点之后插入元素e
。假设已找到第i-1
个节点,并用p
指针指向该节点,s
指向待插入的新节点。
#include <iostream>
using namespace std;
typedef struct Lnode{
int data;
struct Lnode *next;
}Lnode,*LinkList;
bool ListInstert_L(LinkList &L,int i,int e);//元素插入函数
void show( const LinkList L);//打印链表内容函数
int main(){
Lnode s1,s2;
LinkList p = &s1;
s1.data = 10;
s1.next = &s2;
s2.data = 13;
s2.next = NULL;
show(p);
ListInstert_L(p,1,15);//将15插入链表中
show(p);
ListDelete_L(p,2);
show(p);
}
bool ListInstert_L(LinkList &L,int i,int e){
int j;
LinkList p, s;
p = L;
j = 0;
while (p && j<i-1){//查找第i-1个节点,p指向该节点
p = p->next;
j++;
}
if(!p || j>i-1){
return false;
}
s = new Lnode;//生成新节点
s->data = e;//将数据元素e放入新节点的数据域中
s->next = p->next;//将新节点的指针域指向第i个节点
p->next = s;//将节点p的指针域指向节点s
}
void show(const LinkList L){
LinkList cs = L;
while(cs!=NULL)
{
cout<< cs->data <<" ";
cs = cs->next;
}
cout << endl;
}
输出:
10 13
10 15 13
删除
删除一个节点实际上是把这个结点跳过去。根据单向链表向后操作的特性,想要跳过第i
个节点,就必须先找到第i-1
个节点,否则无法跳过。
#include <iostream>
using namespace std;
typedef struct Lnode{
int data;
struct Lnode *next;
}Lnode,*LinkList;
bool ListInstert_L(LinkList &L,int i,int e);//元素插入函数
bool ListDelete_L(LinkList &L,int i);//元素删除函数
void show( const LinkList L);//打印链表内容函数
int main(){
Lnode s1,s2;
LinkList p = &s1;
s1.data = 10;
s1.next = &s2;
s2.data = 13;
s2.next = NULL;
show(p);
ListInstert_L(p,1,15);//将15插入链表中
show(p);
ListDelete_L(p,2);
show(p);
}
bool ListInstert_L(LinkList &L,int i,int e){
int j;
LinkList p, s;
p = L;
j = 0;
while (p && j<i-1){//查找第i-1个节点,p指向该节点
p = p->next;
j++;
}
if(!p || j>i-1){
return false;
}
s = new Lnode;//生成新节点
s->data = e;//将数据元素e放入新节点的数据域中
s->next = p->next;//将新节点的指针域指向第i个节点
p->next = s;//将节点p的指针域指向节点s
}
bool ListDelete_L(LinkList &L,int i){
LinkList p,q;
int j;
p = L;
j = 0;
while((p->next) && (j<j-1)){//查找第i-1个节点,p指向该节点
p = p->next;
j++;
}
if(!(p->next) || (j>i-1)){ //当i>n或i<1时,删除位置不合理
return false;
}
q = p -> next;//临时保存被删除节点的地址以备释放空间
p->next = q->next;//将节点q的下一个结点地址赋值给节点p的指针域
delete q;//释放被删除节点的空间
return true;
}
void show(const LinkList L){
LinkList cs = L;
while(cs!=NULL)
{
cout<< cs->data <<" ";
cs = cs->next;
}
cout << endl;
}
输出:
10 13
10 15 13
10 13
双向链表
- 在双向链表中,每个元素都附加了两个指针域,分别指向前驱结点和后继结点,一个存储前一个元素的地址,一个存储下一个元素的地址,方便向前、向后操作方便。
- 双向链表的节点结构体定义如下:
typedef struct DuLnode{
int data;
struct DuLnode *prior,*next; //指向前驱节点与后继节点的指针
}DuLnode,*DuLinklist;
插入:
#include<bits/stdc++.h>
using namespace std;
typedef struct DuLnode{
int data;
struct DuLnode *prior,*next; //指向前驱节点与后继节点的指针
}DuLnode,*DuLinklist;
void CreateTailList(DuLinklist &L, int n); //创建双向链表
bool ListInsert_L(DuLinklist &L,int i,int e); //插入元素
void show(DuLinklist L); //遍历链表并打印
int main()
{
DuLinklist c;
CreateTailList(c,4);
show(c);
ListInsert_L(c,2,8);
show(c);
}
bool ListInsert_L(DuLinklist &L,int i,int e){
int j;
DuLinklist p,s;
p = L;
j = 0;
while(p && j<i){ //查找第i个节点,p指向该节点
p = p -> next;
j++;
}
if(!p || j>i){ //i>n+1 或者 i<1
return false;
}
s = new DuLnode; //生成新节点
s->data = e; //将新节点的数据域置为e
p->prior->next = s;
s->prior = p->prior;
s->next = p;
p->prior = s;
return true;
}
void show(DuLinklist L){
DuLinklist cs = L;
while(cs -> next != NULL){
cout<< cs->next->data <<" ";
cs = cs->next;
}
cout << endl;
}
void CreateTailList(DuLinklist &L, int n){//创建大小长度为n的链表
DuLinklist p, r;
L = new DuLnode;
L->next = NULL;
L->prior = NULL;
L->data = n;
r = L;
int i = 0;
for(i = 0; i < n; i ++){
p = new DuLnode;
p->data = i;
r->next = p;
p->prior = r;
r = p;
}
r->next = NULL;
}
输出:
0 1 2 3
0 8 1 2 3
删除:
#include<bits/stdc++.h>
using namespace std;
typedef struct DuLnode{
int data;
struct DuLnode *prior,*next; //指向前驱节点与后继节点的指针
}DuLnode,*DuLinklist;
void CreateTailList(DuLinklist &L, int n); //创建双向链表
bool ListInsert_L(DuLinklist &L,int i,int e); //插入元素
bool ListDelete_L(DuLinklist &L,int i); //删除元素
void show(DuLinklist L); //遍历链表并打印
int main()
{
DuLinklist c;
CreateTailList(c,4);
show(c);
ListInsert_L(c,2,8);
show(c);
ListDelete_L(c,1);
show(c);
}
void CreateTailList(DuLinklist &L, int n){//创建大小长度为n的链表
DuLinklist p, r;
L = new DuLnode;
L->next = NULL;
L->prior = NULL;
L->data = n;
r = L;
int i = 0;
for(i = 0; i < n; i ++){
p = new DuLnode;
p->data = i;
r->next = p;
p->prior = r;
r = p;
}
r->next = NULL;
}
bool ListInsert_L(DuLinklist &L,int i,int e){
int j;
DuLinklist p,s;
p = L;
j = 0;
while(p && j<i){ //查找第i个节点,p指向该节点
p = p -> next;
j++;
}
if(!p || j>i){ //i>n+1 或者 i<1
return false;
}
s = new DuLnode; //生成新节点
s->data = e; //将新节点的数据域置为e
p->prior->next = s;
s->prior = p->prior;
s->next = p;
p->prior = s;
return true;
}
bool ListDelete_L(DuLinklist &L,int i){
DuLinklist p;
int j;
p = L;
j = 0;
while(p && (j<i)){//查找第i个节点,p指向该节点
p = p->next;
j++;
}
if(!p || (j>i)){//当i>n或i<1时,删除位置不合理
return false;
}
if(p->next){ //如果p的后继节点存在
p->next->prior = p->prior;
}
p->prior->next = p->next;
delete p; //释放被删除节点的空间
return true;
}
void show(DuLinklist L){
DuLinklist cs = L;
while(cs -> next != NULL){
cout<< cs->next->data <<" ";
cs = cs->next;
}
cout << endl;
}
输出:
0 1 2 3
0 8 1 2 3
8 1 2 3
训练1:区块世界
在早期的人工智能规划和机器人研究中使用了一个区块世界,在这个世界中,机器人手臂执行设计区块操作的任务。问题是要解析一系列命令,这些命令指导机器人手臂如何操作平板上的块。最初,有个区块(编号为),对于所有的情况,区块与区块相邻。
用于操纵块的有效命令如下
-
move a onto b
:把a
和b
上方的块全部放回初始位置,然后把a
放到b
上方。 -
move a over b
:把a
上方的块全部放回初始位置,然后把a
放到b
所在块堆的最上方。 -
pile a onto b
:把b
上方的块全部放回初始位置,然后把a
和a
上方所有的块整体放到b
上方。 -
pile a over b
:把a
和a
上方所有的块整体放到b所在块堆的最上方。 -
quit
:结束标志。
任何a = b
或a
和b
在同一块堆中的命令都是非法命令。所有非法命令都应被忽略。
输入:输入的第1行为整数,表示区块时间中的块数。后面是一系列块命令,每行一个命令。在遇到quit
命令之前,程序应该处理所有命令。所有命令都将采用上面指定的格式,不会有语法错误的命令。
输出:输出应该包含区块世界的最终状态。每一个区块i(0 \leq i <n)后面都有一个冒号。如果上面至少有一个块,则冒号后面必须跟一个空格,后面跟一个显式在该位置的块列表,每个块号与其他块号之间用空格隔开,不要在行末加空格。
#include <iostream>
#include <vector>
using namespace std;
vector<int> block[30];
int n;
void init();//输入函数
void loc(int x, int &p, int &h);//找位置
void goback(int p, int h);//将p块堆高度大于h的所有块归位
void moveall(int p, int h, int q);//将p块堆高度大于或等于h的所有块都移动到q块堆的上方
void solve();//判断块操作函数
int main() {
init();
solve();
for (int i = 0; i < n; ++i) {
cout << i << ":";
for (int j = 0; j < block[i].size(); ++j) {
cout << " "<<block[i][j];
}
cout << endl;
}
}
void init(){
cin >> n;
for (int i = 0; i < n; ++i) {
block[i].push_back(i);
}
}
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;//高度
}
}
}
}
void goback(int p, int h){
for (int i = h+1; i < block[p].size(); ++i) {
int k = block[p][i];
block[k].push_back(k);//归位
}
block[p].resize(h+1);//重置大小
//resize()传递两个参数,分别是大小和初始值,初始值默认为0
}
void moveall(int p, int h, int q){
for (int i = h; i < block[p].size(); ++i) {
int k = block[p][i];
block[q].push_back(k);
}
block[p].resize(h);//重置大小
}
void solve(){
int a,b;
string s1,s2;
while(cin >> s1){
if(s1 == "quit"){
break;
}
cin >> a >> s2 >> b;
int ap = 0,ah = 0,bp = 0,bh = 0;
loc(a,ap,ah);//
loc(b,bp,bh);//
if(ap == bp){
continue;
}
if(s1 == "move"){//a归位
goback(ap,ah);
}
if(s2 == "onto"){//b归位
goback(bp,bh);
}
moveall(ap,ah,bp);//将a块堆高度大于h的所有块都移动到b块堆上。
}
}
输入:
10
move 9 onto 1
move 8 over 1
move 7 over 1
move 6 over 1
pile 8 over 6
pile 8 over 5
move 2 over 1
move 4 over 9
quit
输出:
0: 0
1: 1 9 2 4
2:
3: 3
4:
5: 5 8 7 6
6:
7:
8:
9:
训练2 悲剧文本
假设你在用坏键盘键入一个长文本。键盘的唯一问题是有时Home
或End
键会自动按下(内部)。你没有一时到这个问题,因为你只关注文本,甚至没有打开显示器!输入完毕后,你才发现屏幕上显示的是一段悲剧文本。你的任务是找到悲剧文本。
输入:有几个测试用例。每个测试用例各占一行,包含至少一个且最多100000个字母、下画线和两个特殊字符[
和]
。[
表示内部按了Home
键,]
表示内部按下了End
键。输入由文件结尾EOF
终止
输出:对于每种情况,都在屏幕上输出悲剧文本
算法设计
本题一直在头部和尾部进行操作,使用顺序存储时需要移动大量的元素,因此可以考虑双向链表。不需要移动元素,直接进行插入操作。在C++的STL
中,list
是一个双向链表,可以快速在头尾进行操作。
- 定义一个字符类型的
list
,链表名为text
- 定义一个迭代器
it
,指向链表的开头。 - 检查字符串,如果遇到
[
,则指向链表开头,即it = text.begin()
;如果遇到]
,则指向链表尾部,即it = text.end()
。 - 如果是正常文本,则执行插入操作。
#include <iostream>
#include <list>;
using namespace std;
void solve(string s);
int main(){
string temp;
while(getline(cin,temp) && !cin.eof()){
solve(temp);
}
}
void solve(string s){
int len = s.size();
list<char> text;
auto it = text.begin();
for(int i = 0; i<len; 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;
}
s.clear();
cout << endl;
}
输入:
Tis_is_a_[Beiju]_text
[[]][][]Happy_Birthday_to_Tsinghua_University
输出:
BeijuTis_is_a__text
Happy_Birthday_to_Tsinghua_University
训练3 移动盒子
一行有个盒子,从左到右编号为。模拟以下4种命令。
- 1 :将盒子移动到的左侧(如果已经在的左侧,则忽略此项)
- 2 :将盒子移动到的右侧(如果已经在的右侧,则忽略此项)
- 3 :交换盒子和的位置。
- 4:翻转整行盒子序列
以上命令保证有效,即不等于
输入:最多有10个测试用例。每个测试用例的第1行都包含两个整数和(),下面的行,每行都包含一个命令
输出:对于每个测试用例,都单行输出奇数索引位置的数字总和。
算法设计
本题涉及大量移动元素,使用链表比较合适,但查找是链表不擅长的,多次查找会超时,所以不能使用list
链表实现,应选择静态双向链表。
- 初始化双向静态链表(前驱数组为l[],后继数组为r[]),翻转标记flag = false
- 读入操作指令a。
- 如果a = 4,则标记翻转,flag = !flag,否则读入x、y。
- 如果a!=3&&flag,则a = 3-a。因为如果翻转标记为真,则左右是倒置的,1、2指令正好相反,即1号指令(将x移到y左侧)相当于2号指令(将x移到y右侧)。因此如果a = 1则转换为2;如果a = 2则转换为1。
- 对于1、2指令,如果本来位置就是对的,则说明都不做。
- 如果a = 1,则删除x,将x插入y左侧
- 如果a = 2,则删除x,将x插入y右侧
- 如果a = 3,则考虑相邻和不相邻两种情况进行处理。
#include<bits/stdc++.h>
using namespace std;
int r[100000+5],l[100000+5];
void init(int n); //链接函数
void link(int L,int R); //删除函数
int main()
{
int n,m,a,x,y,k=0;
bool flag;
while(cin>>n>>m)
{
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&&r[y]==x) swap(x,y);
if(a!=3&&flag)
a=3-a;
if(a==1&&x==l[y])
continue;
if(a==2&&x==r[y])
continue;
int Lx=l[x],Rx=r[x],Ly=l[y],Ry=r[y];
if(a==1)
{
link(Lx,Rx);//删除x
link(Ly,x);
link(x,y);//x插入y左
}
else if(a==2)
{
link(Lx,Rx);//删除x
link(y,x);
link(x,Ry);//x插入y右
}
else if(a==3)
{
if(r[x]==y)
{
link(Lx,y);
link(y,x);
link(x,Ry);
}
else
{
link(Lx,y);//交换位置
link(y,Rx);
link(Ly,x);
link(x,Ry);
}
}
}
}
int t=0;
long long sum=0;
for(int i=1;i<=n;i++)
{
t=r[t];
if(i%2==1)
sum+=t;
}
if(flag&&n%2==0)
sum=(long long)n*(n+1)/2-sum;
cout<<"Case "<<++k<<": "<<sum<<endl;
}
return 0;
}
void init(int n)
{
for(int i=1;i<=n;i++)
{
l[i]=i-1;
r[i]=(i+1)%(n+1);
}
r[0]=1;
l[0]=n;
}
void link(int L,int R) //将L和R链接起来
{
r[L]=R;
l[R]=L;
}
输入:
6 4
1 1 4
2 3 5
3 1 6
4
6 3
1 1 4
2 3 5
3 1 6
100000 1
4
输出:
Case 1: 12
Case 2: 9
Case 3: 2500050000