这是前几天做的一道笔试题,当时实现复杂了,导致最后超时,现在记录一下思路与实现方法。
题目
有一个长度为n并包含1~n的整数序列。在最初,序列为升序排列,即1在序列首部,n在序列末尾。
对序列进行m次操作,第i次操作会把 x i x_i xi从序列中取出并放到序列首部。经过m次操作后,序列变成什么样子?
输入描述
输入包含m+1行。
第一行包含两个正整数n,m(1<=n,m<=100000)。
接下来m行,每行一个正整数 x i x_i xi,表示每次操作的数。
输出描述
输出一行,即m次操作之后的序列,以空格分割,行末无空格。
示例
输入
5 4
4
3
1
4
输出
4 1 3 2 5
先来说一下解决这道题的几种思路:
思路1 哈希表
这也是我在笔试时使用的方法。
先通过一个遍历生成一个哈希表,key为具体数字,value为每个数字在序列中的索引。这样做的好处是每次搜索要操作数x时,搜索时间复杂度为O(1)。找到数x后,记录它当前的索引index,然后将x的索引值变为-1。接着遍历整个哈希表,将所有数的索引值小于x最初索引index的索引数+1,这样就得到每次操作之后,每个数的新索引。经过m次操作之后,再次遍历整个哈希表,根据每个数的索引值构建一个序列,最后将其输出即可。
可以发现这个思路多次使用了遍历,从而导致我最后提交只有70%通过,其余都超时了。时间复杂度为O(m*n+3n),空间复杂度至少为O(2n)。代码如下:
#include<iostream>
#include<vector>
#include<unordered_map>
using namespace std;
int main() {
int n, m = 0;
scanf_s("%d", &n);
scanf_s("%d", &m);
unordered_map<int, int> hashmap;
for (int i = 0; i<n; i++) {
hashmap.insert(pair<int, int>(i + 1, i));
}
for (int i = 0; i<m; i++) {
int j = 0;
scanf_s("%d", &j);
int temp_index = hashmap[j];
hashmap[j] = -1;
for (auto it = hashmap.begin(); it != hashmap.end(); ++it) {
if (it->second<temp_index) {
hashmap[it->first] = it->second + 1;
}
}
}
vector<int> result;
result.resize(n);
for (auto it = hashmap.begin(); it != hashmap.end(); ++it) {
result[it->second] = it->first;
}
for (int i=0; i<result.size(); i++){
cout << result[i];
if (i != n - 1) {
cout << " ";
}
}
return 0;
}
思路2(移位与查找操作合并)相对最优
这是我和他人经过讨论后,得出的一种更高效的方法。
首先通过一个遍历将所有数存放如vector中,每次操作一个数x时,直接将x与vector[0]中的值替换,取出的值放入temp中,接着temp依次与后面的数进行替换,直到取出的数与x相等,则结束替换。最后将其输出即可。
优点:某种程度上将移位与查找操作合并在了一起,节省了时间。
时间复杂度最坏情况为O(m*n + 2n),空间复杂度为O(n)。代码如下:
#include<iostream>
#include<vector>
using namespace std;
int main() {
int n, m = 0;
scanf_s("%d", &n);
scanf_s("%d", &m);
vector<int> result(n);
for (int i = 0; i<n; i++) {
result[i] = i + 1;
}
for (int i = 0; i<m; i++) {
int x = 0;
scanf_s("%d", &x);
if (x == result[0]) {
continue;
}
int pre = result[0];
result[0] = x;
for (int i = 1; pre != x; i++) {
int temp = result[i];
result[i] = pre;
pre = temp;
}
}
for (int i=0; i<n; i++){
cout << result[i];
if (i != n - 1) {
cout << " ";
}
}
system("pause");
return 0;
}
思路3 链表
使用链表,用链表存储每个数。
优点:数字位置可以高效移动,无需改变其他元素位置。
缺点:1.生成链表会占用大量内存。2.链表只能顺序查找,会花费较多时间。
代码略。
思路4(放弃索引移位,直接判断输出)
这是我写这篇文章的时候突然想到的(/ω\),这种方法时间/空间复杂度可能很高-_-||
既然我们知道了有m个数,那么我们直接反转m个数,并排除掉重复的数,存入vector,先把这几个数输出。其余的数每次输出前判断是否在vector,不在则按顺序输出即可。
时间复杂度:O( m + ( m − 1 ) ∗ ( m − 2 ) 2 + n ∗ m = m 2 + m 2 + n ∗ m m+\frac{(m-1)*(m-2)}{2}+n*m=\frac{m^2+m}{2}+n*m m+2(m−1)∗(m−2)+n∗m=2m2+m+n∗m )
空间复杂度:O(m)
#include<iostream>
#include<vector>
using namespace std;
int main() {
int n, m;
scanf_s("%d", &n);
scanf_s("%d", &m);
vector<int> m_list;
vector<int> m_one;
int x = 0;
for (int i = 0; i<m-1; i++) {
scanf_s("%d", &x);
m_list.push_back(x);
}
scanf_s("%d", &x);
m_one.push_back(x);
cout << x << " ";
vector<int>::iterator it;
for (int i = m-2; i>-1; --i) {
int y = m_list[i];
it = find(m_one.begin(), m_one.end(), y);
if (it != m_one.end()) {
continue;
} else {
m_one.push_back(y);
cout << y << " ";
}
}
int t = 0;
for (int i=0; i<n; i++){
int c = i + 1;
if (t < m_one.size()) {
it = find(m_one.begin(), m_one.end(), c);
//如果找到c,则计数并进行下一次循环
if (it != m_one.end()) {
t += 1;
continue;
}
}
cout << c;
if (i != n - 1) {
cout << " ";
}
}
system("pause");
return 0;
}
总的来说,还是思路2最好,其余几种都会存在超时的情况,尤其是思路4-_-||。
如果有更好的方法,欢迎大家留言交流。