其他分享
首页 > 其他分享> > 输入输出优化

输入输出优化

作者:互联网

一般来说,使用

cin >> a;
cout << a;

会比

scanf("%d", &a);
printf("%d", a);

要慢
因为前者有很多参数需要配置,检查,所以会慢很多。而后者

_CRTIMP int __cdecl __MINGW_NOTHROW printf (const char*, ...);
_CRTIMP int __cdecl __MINGW_NOTHROW	scanf (const char*, ...);

第一个参数在某些编译器里面叫做format,也就是在这里你已经提供了他的格式。注意这里一定要匹配。如果

int i = -1;
printf("%u", i);

输出的就不是-1。这是因为在printf函数里面,他按照unsigned的方式输出-1,所以得到的结果就不一样


但是这样还是很慢,因为这两个函数都要检查format里面提供的参数。
所以我们开始考虑自己写快读快写
我们目前知道比较简单的输入输出操作就是

_CRTIMP int __cdecl __MINGW_NOTHROW	getchar (void);
_CRTIMP int __cdecl __MINGW_NOTHROW	putchar (int);

那么我们能不能用这两个函数来写快读快写呢?
肯定可以的
每次读入一个字符,如果不是数字字符就跳过,是数字字符就把当前的数乘以10再加上。
简单说来就是这样

int read()
{
  int x = 0, flag = 1;
  char ch = getchar();
  while(ch < '0' or ch > '9')
  {
    if(ch == '-') f = -1;
    else f = 1;
    ch = getchar();
  }
  while(ch >= '0' and ch <= '9')
  {
    // x = (x * 2) + (x * 8) + (ch - 48);
    x = (x << 1) + (x << 3) + (ch ^ 48);
    ch = getchar();
  }
  return x * flag;
}

同样的,我们也能写出输出。注意这里逆序输出推荐使用递归

void write(int x)
{
  if(x < 0) putchar('-');
  if(x > 9) write(x / 10);
  putchar(x % 10 + 48);
}

但是这样还不够快!
频繁调用

_CRTIMP int __cdecl __MINGW_NOTHROW	getchar (void);
_CRTIMP int __cdecl __MINGW_NOTHROW	putchar (int);

会多次打开缓存输入,造成效率低下
我们就想
能不能把输入数据一次性读入进来,保存在一个数组里面,直接访问数组元素呢?
比如说

char pI[], *Ip1, *Ip2;
char getchar()
{
  if(Ip1 == Ip2)
  {
    pass();
    Ip1 = pI;
  }
  if(Ip1 == Ip2) return -1;
  return (*Ip1++)
}

这里pI数组是读入的数据,Ip1是当前读入到的位置(指针),Ip2是整个pI的尾指针,如果Ip1==Ip2就说明当前的读入到头了,要不重新读入,要不返回EOF(-1)
pass()函数是用来处理读入数据的

思路讲清楚了,现在看看具体怎么实现
现在告诉你,stdio.h库里面有

_CRTIMP size_t __cdecl __MINGW_NOTHROW	fread (void*, size_t, size_t, FILE*);

的作用是从FILE* 里面读入size_t个大小为size_t的字符保存到void*中,返回size_t作为成功读入的长度
比如说我用

int a = fread(pI, 1, 128, stdin);

读入"123\26",这里'\26'是结束的意思,相当于Ctrl+Z,这样就读入了4个字符,分别是'1', '2', '3', ''(我也不知道为什么是这个)。
注意使用这个读入的话'\n'也算一个字符
库里面还有

_CRTIMP size_t __cdecl __MINGW_NOTHROW	fwrite (const void*, size_t, size_t, FILE*);

是把const void里面的size_t个大小为size_t的内容写入到FILE里面
比如

char a[10] = {'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'h', 'h'};
fwrite(a, 1, 10, stdout);

输出的就是abcdefghhh
然后回到我们之前的代码

char pI[], *Ip1, *Ip2;
char getchar()
{
  if(Ip1 == Ip2)
  {
    pass();
    Ip1 = pI;
  }
  if(Ip1 == Ip2) return -1;
  return (*Ip1++)
}

怎么实现pass()函数呢?
初始化的时候,Ip1=Ip2=0, 使用fread()读入,更新Ip2的位置,每次返回*Ip1并且Ip1++
然后我们又知道C++里面指针是可以加减整数的
所以我们就可以

#define fastIO
#ifdef fastIO
#define IOMxn 1001
char pI[IOMxn], *Ip1, *Ip2;
#define getchar() ((Ip1 == Ip2) && (Ip2 = (Ip1 = pI) + fread(pI, 1, IOMxn, stdin), Ip1 == Ip2)? -1 : *(Ip1++))
#undef IOMXn
#endif

其实最主要就是这一行

#define getchar() ((Ip1 == Ip2) && (Ip2 = (Ip1 = pI) + fread(pI, 1, IOMxn, stdin), Ip1 == Ip2)? -1 : *(Ip1++))

每次用getchar()都会变成((Ip1 == Ip2) && (Ip2 = (Ip1 = pI) + fread(pI, 1, IOMxn, stdin), Ip1 == Ip2)? -1 : *(Ip1++))
那就让我们看看他是怎么工作的
首先我们要知道程序是很懒的,当你用&&, ||连接两个布尔变量的时候

bool A, B, C, D;
C = A && B;
D = A || B;

如果A为假,那么B无论真还是假C都为假,所以C就是假,程序根本不看B是啥
同理,如果A为真,那么D肯定为真,所以程序根本不看B是啥
那么你会说:这不是很正常嘛
但是如果

bool fun1()
{
  cout << "三千世间疾," << endl;
  return false;
}
bool fun2()
{
  cout << "相思最难医。" << endl;
}
void main()
{
  if(fun1() && fun2());
  return ;
}

这个时候虽然两个函数都返回bool类型,但是这两个函数还有其他事情要干啊
这个时候系统调用fun1()发现是假,那么他就不会调用fun2(),所以fun2()的返回值我都懒得写了(我和程序一样懒)
不信的话去试试?程序只会输出第一句。
虽然有时候这个很麻烦,但是我们也不妨利用他的这个性质。
在讲正文之前,还有一个前置知识要告诉你们的
你们写某些题,是不是说读入以0结束
然后是怎么读入的?

while(scanf("%d", &n), n);

当时可能有很多小伙伴就在问,这个逗号是来干嘛的。
现在我告诉你,语句

int n = (fun1(), fun2());

的作用就是先执行fun1(),然后把fun2()的返回值赋给n
可以试试这段代码

int fun1(int & a)
{
  a = 20;
  return 100;
}
void main()
{
  int n = 10;
  cout << (fun1(n), n) << endl;
  return ;
}

就会发现我讲的是对的

回到正文

((Ip1 == Ip2) && (Ip2 = (Ip1 = pI) + fread(pI, 1, IOMxn, stdin), Ip1 == Ip2)? -1 : *(Ip1++))

这里用的是三目运算符 ?:, 我在这里写清楚一点

(
  (Ip1 == Ip2) 
  && 
  (Ip2 = (Ip1 = pI) + fread(pI, 1, IOMxn, stdin), Ip1 == Ip2)
  ?

 -1 : *(Ip1++))

这里有很多个括号。如果担心的话其实还可以多加一点,毕竟#define是直接替换,如果和外面某些东西发生了冲突就不好了
如果检测发现Ip1 == Ip2为假,也就是现在pI数组还没读入完,所以不管&&后面发生了什么,整个?前面的表达式就是假,三目表达式的值就是:后面的(Ip1++),就是返回Ip1然后Ip1++
但是如果Ip1 == Ip2,也就是说现在pI数组读入完了。我们也知道有些数据很大,不能全部搬到程序当中,每次只能搬IOMxn个,具体多少个根据实际自行定义。
如果搬完了Mxn个还有数据,就等待下一次搬运吧……
现在发现pI数组读入完了,那么就重新读入,首先Ip1=pI重新指向整个数组的开头,然后fread()读入并返回个数
然后C++指针可以加减整数,如果

int a[];
cout << *(a + i);
cout << a[i];

第二行和第三行输出结果一样。因为第二行得到了a[i]的地址,然后使用转换为整形,就是a[i]
所以我们得到一个惊天动地的结论:(a + i) == & a[i] \(|i \in N^*\), 计算机访问a[i]实际上就是访问
(a + i), 有时候指针飘走了,才会发生运行错误
所以我们Ip2就可以通过pI + fread(pI, 1, IOMxn, stdin)得到
如果得到了Ip2之后,也就是重新读入之后,Ip1还是等于Ip2,就说明遇到了文件末尾EOF,那么我们也返回EOF好了。
否则的话,和前面一样返回*(Ip1++)
然后有人就要问了:如果我是文件输入输出怎么办?
别急,还记不记得fread的函数原型

_CRTIMP size_t __cdecl __MINGW_NOTHROW	fread (void*, size_t, size_t, FILE*);

里面有一个FILE参数,当时我们给的是stdin,也就是控制台输入输出。
现在如果要文件输入输出,只需要使用fopen新建一个FILE
参数即可。

FILE* const Ifile = fopen("my.in", "r");
fread(a, 1, IOMxn, Ifile);

这里注意FILE* const中const 的位置。定义一般的常量const随便放,但是指针常量const的位置不同会有不同的意思
比如

int a, b;
const int* pa = &a;
int* const pb = &b;
pa = &b;
*pb = 100;

经常性指针指向的变量是会更改的。如果在*前面加上const只是表明指针指向的是常量,不能通过指针来改变这个量。如下操作是非法的:

*pa = 100;

如果在*后面加上const就是说明这个指针指向的量可以通过指针改变,但是这个指针一生都只能指向这个变量,矢志不渝。如下操作是非法的:

pb = &a;

又因为我们的FILE* Ifile指向输入文件,而且只能(会)指向输入文件。但是输入/输出文件由于读入/写入就会改变,所以需要通过指针改变这个量。最后定义就是

FILE* const Ifile;

现在就来讲一下缓存优化快写
其实和快读差不多,把要写入的字符先储存到数组里面,等数组满了再一次性输出。注意主程序结束之前要输出一次,保证未满的数组中的数据也能全部输出。
这里提醒一下,对于数组中的((char)Op1)==0,虽说这个字符的Ascii码是0,但是使用fwrite输出会把size_t个字符全部输出,(char)0也会当做一个空格输出(迷)

char pO[IOMxn];
char *Op = pO;
#define putchar(a) (((Op - pO == IOMxn) && (fwrite(pO, 1, IOMxn, stdout), Op = pO), *(Op++)) = a)

由于网上没有快写宏定义的代码,这是手敲的,肯定慢(逃)
解释一下,由于快写不是快读,所以只需要一个数组一个指针就够了。指针说明现在填充到数组的第几位。
又因为C++指针可以加减,所以Op - pO == IOMxn,当然也可以写成Op - pO == sizeof(pO),就说明数组现在已经满了,需要执行后面的代码,输出之后重新把指针指向数组开头。
然后就向*Op填充数据,然后Op++


总结下来,快读快写的代码就下面这几行:

#define fastIO
#ifdef fastIO
#define IOMxn 1 << 20
char pI[IOMxn], *Ip1, *Ip2;
#define getchar() ((Ip1 == Ip2) && (Ip2 = (Ip1 = pI) + fread(pI, 1, IOMxn, stdin), Ip1 == Ip2)? -1 : *(Ip1++))
char pO[IOMxn];
char *Op = pO;
#define putchar(a) (((Op - pO == IOMxn) && (fwrite(pO, 1, IOMxn, stdout), Op = pO), *(Op++)) = a)
#undef IOMXn
#endif

int main()
{
  ///...
  fwrite(pO, 1, Op - pO, stdout);
  return 0;
}

P.S.:如果发现不需要输入直接结束程序的情况,不妨调小IOMxn的值试试。

让我们先去Luogu试一下效果
时间上根据网上的资料肯定是快了的
看看正确性
好家伙
525ms变成了479ms
都看到这里了

好文要顶,点个赞再走呗~

标签:__,int,输入输出,Ip2,Ip1,读入,pI,优化
来源: https://www.cnblogs.com/LASS/p/14523469.html