上海交大ACM班C++算法与数据结构——数据结构之队列
1.队列的定义
- 先进先出的线性表
- 基本操作:
- 创建
- 入队
- 出队
- 读取
- 判断是否为空 - 虚构类实现
template <class elemType>
class queue{
public:
virtual bool isEmpty() = 0; //判队空
virtual void enQueue(const elemType &x) = 0; //进队
virtual elemType deQueue() = 0; //出队
virtual elemType getHead() = 0; //读队头元素
virtual ~queue() {} //虚析构函数
};
2.顺序实现
- 循环队列:头指针、尾指针都可以移动,既不会浪费空间(数据固定,出队时头指针移动),也不需要移动大量元素(头指针固定,出队时数据移动)
- 类定义
template <class elemType>
class seqQueue: public queue<elemType> {
private:
elemType *elem;
int maxSize;
// 队头和队尾
int front, rear;
void doubleSpace();
public:
seqQueue(int size = 10);
// 析构函数:收回动态数组
~seqQueue() { delete [] elem ; }
// 判队列是否为空:队头是否等于队尾
bool isEmpty() { return front == rear; }
//进队
void enQueue(const elemType &x);
//出队
elemType deQueue();
// 访问队头元素
elemType getHead() { return elem[(front + 1) % maxSize]; }
};
- 构造函数
template <class elemType>
seqQueue<elemType>::seqQueue(int size) {
elem = new elemType[size];
maxSize = size;
front = rear = 0;
}
- deQueue函数:出队
template <class elemType>
elemType seqQueue<elemType>::deQueue() {
front = (front + 1) % maxSize;
return elem[front];
}
- enQueue函数:入队
template <class elemType>
void seqQueue<elemType>::enQueue(const elemType &x) {
if ((rear + 1) % maxSize == front)
doubleSpace();
rear = (rear + 1) % maxSize;
elem[rear] = x;
}
- doubleSpace函数
template <class elemType>
void seqQueue<elemType>::doubleSpace() {
elemType *tmp =elem;
elem = new elemType[2 * maxSize];
for (int i = 1; i < maxSize; ++i)
elem[i] = tmp[(front + i) % maxSize];//复制到新数组的时候将front调整到数组最前端
front = 0;
rear = maxSize - 1;
maxSize *= 2;
delete [] tmp;
}
-
例题
新学期开始了,小哈是小哼的新同学,小哼向小哈询问QQ号,小哈当然不会直接告诉小哼。所以小哈给了小哼一串加密过的数字,同时小哈也告诉了小哼解密规则。规则是这样的:首先将第1个数删除,紧接着将第2个数放到这串数的末尾,再将第3个数删除并将第4个数再放到这串数的末尾,再将第5个数删除……直到剩下最后一个数,将最后一个数也删除。按照刚才删除的顺序,把这些删除的数连在一起就是小哈的QQ啦。现在你来帮帮小哼吧。小哈给小哼加密过的一串数是6 3 1 7 5 8 9 2 4。解密后小哈的QQ号应该是6 1 5 9 4 7 2 8 3。
输入描述:
只有2行,第1行有一个整数n (1 ≤ n ≤ 10^5)
第2行有n个整数为加密过的QQ号,每个整数之间用空格隔开。每个整数在 1~9 之间。
对于100%的数据满足1 ≤ n ≤ 10^5。
输出描述:
只有一行,输出解密后的QQ号。
示例 1:
输入:
9
6 3 1 7 5 8 9 2 4
输出:
6 1 5 9 4 7 2 8 3
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
#define N 1000005
using namespace std;
int n;
int q[N], l, r;
int main() {
int n;
scanf("%d", &n);
l = r = 0;
for (int i = 1; i <= n; ++i) {
int x;
scanf("%d", &x);
q[r++] = x;
}
int flag = 0;
while (l < r) {
int x = q[l++];
if (!flag) printf("%d ", x);
else q[r++] = x;
flag ^= 1;
}
return 0;
}
3.链接队列
- 不带头结点的单链表(同时记录头尾结点的位置)
- 队头:表头,表头插入和删除都方便,表尾不方便删除
队尾:表尾
- 类定义
template <class elemType>
class linkQueue: public queue<elemType> {
private:
struct node {
elemType data;
node *next;
node(const elemType &x, node *N = NULL){data = x; next = N;}
node():next(NULL) {}
~node() {}
};
node *front, *rear;
public:
linkQueue() { front = rear = NULL; }
// 析构函数,和链表的析构函数类似
~linkQueue();
bool isEmpty() { return front == NULL; }
void enQueue(const elemType &x);
elemType deQueue();
elemType getHead() { return front->data; }
};
- enQueue函数:入队(要单独考虑空队时候入队的情况)
template <class elemType>
void linkQueue<elemType>::enQueue(const elemType &x) {
if (rear == NULL)
front = rear = new node(x);
else
rear = rear->next = new node(x);
}
- deQueue函数:出队(要单独考虑出队后队为空的情况)
template <class elemType>
elemType linkQueue<elemType>::deQueue() {
node *tmp = front;
elemType value = front->data;
front = front->next;
delete tmp;
// 判断是否为空
if (front == NULL) rear = NULL;
return value;
}
- 性能分析:所有操作的时间复杂度都是O(1)
4.列车车厢重排问题
一列货运列车共有n节车厢,每节车厢将被放在不同的车站。假定n个车站的编号分别为1 – n,货运列车按照第n站到第1站的次序经过这些车站。车厢的编号与它们的目的地相同。为了便于从列车上卸掉相应的车厢,必须重新排列这些车厢,将第n节车厢放在最后,第1节车厢放在最前面。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Tfbijr21-1651345240940)(http://jiang-yvhang.gitee.io/boke/C++AlgorithmsAndDataStructures/photo/53列车车厢重拍.png)]
思路:依次取入轨的队头元素,直到队列为空; 进入一条最合适的缓冲轨道(最合适:可进入缓冲轨道中末尾元素最大的轨道);检查每条缓冲轨道的队头元素,将可以放入出轨的元素出队,进入出轨。
// in表示输入轨道中车厢的排列,总共n个车厢,通过k个缓冲轨道
void arrange(int in[], int n, int k) {
linkQueue<int> *buffer = new linkQueue<int>[k];
int last = 0;
// 依次把每节车厢放入缓冲轨道,然后检查能否将缓冲轨道中的车厢移动到输出轨道
for (int i = 0; i < n; ++i) {
// 如果车厢放不进去,表示重排失败,不必再进行下去
if (!putBuffer(buffer, k, in[i])) return;
checkBuffer(buffer, k, last);
}
}
//putBuffer函数:找合适的轨道
//基本要求:轨道上最后一节车厢的编号小于进入的车厢编号,没有满足基本要求的轨道,则启用一条新轨道
//最优要求:多个轨道满足时,选择其中最后一节车厢编号最大的一个
//例如处理车厢8,现有轨道情况
//轨道1: 2、5
//轨道2:3、7
//8应该放入轨道2。如果放入轨道1,接入后面一节车厢是6号,则必须启用一根新的轨道
// 把车厢in,放入缓冲轨道,buffer是存储缓冲轨道队列的数组,size表示缓冲轨道的数量
// 如果车厢能放入合适的缓冲轨道则返回true,否则返回false
bool putBuffer(linkQueue<int> *buffer, int size, int in) {
// 希望把车厢放到编号为avail的缓冲轨道上,max表示所有合适轨道队尾编号最大的车厢
int avail = -1, max = 0;
for (int j = 0 ; j < size; ++j) {
if (buffer[j].isEmpty()) { if (avail == -1) avail = j; }
else if (buffer[j].getTail() < in && buffer[j].getTail() > max) {
avail = j;
max = buffer[j].getTail();
}
}
if (avail != -1) {
buffer[avail].enQueue(in);
cout << in << "移入缓冲区 " << avail << endl;
return true;
} else {
cout << "无可行的方案" << endl;
return false;
}
}
//checkBuffer函数
// 检查能否将缓冲轨道上的车厢移动到输出轨道,last代表输出轨道上最后一列车厢的标号
void checkBuffer( linkQueue<int> *buffer, int size, int &last) {
// 表示需要检查缓冲轨道
bool flag = true;
while (flag) {
flag = false;
for (int j = 0; j < size; ++j) {
if (! buffer[j].isEmpty() && buffer[j].getHead() == last + 1) {
cout << "将" << buffer[j].deQueue() << "从缓冲区" << j << "移到出轨" << endl;
++last;
// 有车厢出轨后再次将flag设置为true
flag = true;
break;
}
}
}
}
- 生成[a,b]间均匀分布的随机数:
rand*(b-a+1)/(RAND_MAX+1)+a
(分成均匀的单位段)
5.例题
小组队列
有m个小组,n个元素,每个元素属于且仅属于一个小组。支持以下操作:
push x:使元素x进队,如果前边有x所属小组的元素,x会排到自己小组最后一个元素的下一个位置,否则x排到整个队列最后的位置。
pop:出队,弹出队头并输出出队元素,出队的方式和普通队列相同,即排在前边的元素先出队。
对于全部测试数据,保证1 ≤ n ≤ 10^5,1 ≤ m ≤ 300,T ≤ 10^5,输入保证操作合法。
输入描述:
第一行有两个正整数n m,分别表示元素个数和小组个数,元素和小组均从0开始编号。
接下来一行n个非负整数Ai,表示元素i所在的小组。
接下来一行一个正整数T,表示操作数。
接下来T行,每行为一个操作。
输出描述:
对于每个出队操作输出一行,为出队的元素。
示例 1:
输入:
4 2
0 0 1 1
6
push 2
push 0
push 3
pop
pop
pop
输出:
2
3
0
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
#define N 100005
#define M 305
using namespace std;
int n, m, t;
int team[N];
int q[M][N], l[M], r[M];
int main() {
scanf("%d%d", &n, &m);
for (int i = 0; i < n; ++i) scanf("%d", &team[i]);
scanf("%d", &t);
char op[10];
int x;
while (t--) {
scanf("%s", op);
if (op[1] == 'u') {
scanf("%d", &x);
int tx = team[x];
if (l[tx] == r[tx]) q[m][r[m]++] = tx;
q[tx][r[tx]++] = x;
} else {
int tx = q[m][l[m]];
printf("%d\n", q[tx][l[tx]++]);
if (l[tx] == r[tx]) ++l[m];
}
}
return 0;
}
6.块状链表
-
概念:链表结点内顺序存储(每个结点存放字符串中一段连续的字符,而不是单个字符)
-
优点:提高了空间的利用率(加大了数据的比例,减小了指针的比例,每个指针要占用4个字节的空间)
-
缺点:插入和删除时会引起数据的大量移动
-
改进方法:允许节点有一定的空闲空间(空间换时间)
-
应用:字符串处理(C中用固定大小的数组处理字符串,常常会因为超出数组大小出现不可预知的错误,C++ 中用大小可变的空间处理字符串)
-
例题
定义字符串的基本操作为三种:删除一个字符、插入一个字符和将一个字符修改成另外一个字符。将字符串S1变成字符串S2的最少操作步数,称为字符串S1到字符串S2的编辑距离。字符串ABCDEFG到字符串BADECG的编辑距离为:
3(首先,删除A得到BCDEFG;接着,替换C为A得到BADEFG;最后,替换F为C得到BADECG。)
墓碑字符
考古学家发现了一座千年古墓,墓碑上有神秘的字符。经过仔细研究,发现原来这是开启古墓入口的方法。墓碑上有两行字符串,其中第一个串的长度为偶数,现在要求把第二个串插入到第一个串的正中央,如此便能开启墓碑进入墓中。
输入描述:
第一行一个整数n,表示测试数据的组数。
接下来n组数据,每组数据两行。每行各一个字符串,且长度满足1 ≤ length ≤ 50,并且第一个串的长度必为偶数。
输出描述:
每组数据输出一个能开启古墓的字符串,每组输出占一行。
示例 1:
输入:
2
CSJI
BI
AB
CMCLU
输出:
CSBIJI
ACMCLUB提示:substr函数:复制子字符串,要求从指定位置开始,并具有指定的长度。没指定长度或指定长度无效,则默认到源字符串的结尾
#include <iostream>
#include <string>
using namespace std;
int main() {
int t, len;
cin >> t;
while (t--) {
string a, b, ans;
cin >> a >> b;
len = a.length();
ans = a.substr(0, len / 2) + b + a.substr(len / 2);
cout << ans << endl;
}
return 0;
}