“《编程珠玑》(第2版)第2章”:A题(二分搜索)
A题是这样子的:
给定一个最多包含40亿个随机排列的32位整数的顺序文件,找出一个不在文件中的32位整数(在文件中至少缺失一个这样的数据——为什么?)。在具有足够内存的情况下,如何解决该问题?如果有几个外部的“临时”文件可用,但是仅有几个字节的内存,又该如何解决该问题?
因为2^32>40亿,所以,在文件中至少会缺少一个这样的数据。在有足够内存的情况下,我们可以使用上一章提到的位图数据结构来解决该问题。但利用位图数据结构来解决该问题需要利用到的内存为:40亿/8/1024/1024=476MB,所以当内存不足时是不能够用这种方法的。
前面一篇博文提到了可以用数列求和方法来求得缺失的数。但就是这种方法有一个要求就是,任一个数与其前一个数的差值是一个定值,即要求该数列是一个等差数列。所以,如果我们能够确定,这个需要找出缺失的数的顺序文件里边的数是一个等差数列,那我们就可以利用该方法简单求出缺失的数了,而且占用内存极少,效率当然也就更高。
作者对该问题的解决方法是利用“二分搜索”的方法。具体如下:
算法的第一趟(最多)读取40亿个输入整数,并把起始位为0的整数写入一个顺序文件,把起始位为1的整数写入另一个顺序文件。如下图所示。
这两个文件中,有一个文件最多包含20亿个整数,我们接下来将该文件用作当前输入并重复探测过程,但这次探测的是第2个位。如果原始的输入文件中包含n个元素,那么第1趟将读取n个整数,第2趟最多读取n/2个整数,第3趟最多读取n/4个整数,依此类推,所以总的运行时间正比于n。通过排序文件并扫描,我们也能够找到缺失整数,但是这样做会导致运行时间正比于nlogn。
下边我们就来看看,用“二分搜索”怎么解决这个问题:
1. 文件读写
首先我们来看怎么进行文件的读写。有一篇博文总结的很不错,值得参考。贴出具体代码如下:
- #include <iostream>
- #include <fstream>
- #include <string>
- #include <sstream>
- #include <math.h>
- using namespace std;
- int main()
- {
- // *************************** Write File *************************** //
- // Open file
- ofstream wfile("file.txt", ios::out);
- if (!wfile)
- {
- cout << "The file can not be opened!" << endl;
- exit();
- }
- for (int i = ; i < pow(, ); i++)
- {
- stringstream ss;
- ss << i;
- string str = ss.str();
- wfile << str;
- wfile << endl;
- }
- // Close the file
- wfile.close();
- // *************************** Read File *************************** //
- // Open file
- ifstream rfile("file.txt", ios::in);
- if (!rfile)
- {
- cout << "The file can not be opened!" << endl;
- exit();
- }
- string line;
- int num;
- while (getline(rfile, line))
- {
- // cout << line << endl;
- stringstream ss;
- ss << line;
- ss >> num;
- cout << num << endl;
- }
- // Close the file
- rfile.close();
- return ;
- }
2. 实际操作
生成40亿个数据实在是太庞大了,因此在这里我选择了生成0~65535个顺序数列,然后随意去掉一个数,检验程序能否找出缺失的数。
- #include <iostream>
- #include <fstream>
- #include <string>
- #include <sstream>
- #include <vector>
- #include <math.h>
- using namespace std;
- typedef unsigned int uint;
- int main()
- {
- //// *************************** Write File *************************** //
- //// Open file
- //ofstream wfile("file.txt", ios::out);
- //if (!wfile)
- //{
- // cout << "The file can not be opened!" << endl;
- // exit(1);
- //}
- //for (int i = 0; i < pow(2, 16); i++)
- //{
- // stringstream ss;
- // ss << i;
- // string str = ss.str();
- // wfile << str;
- // wfile << endl;
- //}
- //// Close the file
- //wfile.close();
- // *************************** Read File *************************** //
- // Open file
- ifstream rfile("file.txt", ios::in);
- if (!rfile)
- {
- cout << "The file can not be opened!" << endl;
- exit();
- }
- vector<uint> right, left;
- string line;
- uint num;
- while (getline(rfile, line))
- {
- // cout << line << endl;
- stringstream ss;
- ss << line;
- if (line != "")
- {
- ss >> num;
- if (num & ( << ))
- right.push_back(num);
- else
- left.push_back(num);
- }
- }
- // Close the file
- rfile.close();
- uint count = ;
- uint miss = ;
- uint szLeft = ;
- uint szRight = ;
- while (count != )
- {
- vector<uint> temp;
- szLeft = left.size();
- szRight = right.size();
- if (szLeft < szRight)
- {
- right.clear();
- for (uint i = ; i < szLeft; i++)
- {
- if (left[i] & ( << - count))
- right.push_back(left[i]);
- else
- temp.push_back(left[i]);
- }
- left.clear();
- left = temp;
- }
- else
- {
- left.clear();
- for (uint i = ; i < szRight; i++)
- {
- if (right[i] & ( << - count))
- temp.push_back(right[i]);
- else
- left.push_back(right[i]);
- }
- right.clear();
- right = temp;
- }
- count++;
- }
- szLeft = left.size();
- szRight = right.size();
- if (szLeft > szRight)
- {
- miss = left[] + ;
- }
- else if (szLeft < szRight)
- {
- miss = right[] - ;
- }
- else
- {
- // no elements is missed
- miss = ;
- }
- cout << "The missed one is: " << miss << endl;
- return ;
- }
注:该程序实际所占内存还是挺大的(针对存有40亿个数据的文件),主要是在前期刚从文件读入数据的时候。作者提到的是把这些中间数据写到文件中去的方法,但我在这里为了便于处理,就没有将中间数据写出然后再读进来。当然,我这样做只是权宜之计,要实际解决内存不足的话还是得将中间数据写出文件然后再读取。
通过不断地二分,因为缺少一个整数,所以程序中left和right的大小至少一个为0,又因为,最后的left和right(都已经只有一个数)如果在没有缺失数的情况下,有left[0] = right[0] - 1,这样就很简单就能够判断出缺少的是哪一个数了。
当然程序是可以找出缺失的数的。因为只是验证一下算法,程序的健壮性是没有太多考虑的。
我们可以看到,“二分法”能让程序对时间和空间的需求呈指数级下降,效果是特别明显的。
注:后来又看到其他人的想法,此程序的效率还是可以继续提升的!
我们每次读入总数1/10的数据量的时候,就比较一下最后一个读入的数据D_last跟读入的数据量大小L_read:
- 如果D_last+1=L_read,则表明目前读入的数据中并没有缺失的数,也因此可以在后边将二分搜索起点设定为L_read;
- 如果D_last=L_read,则表明目前已经读入的数据就已经存在缺失的数了,也就是说不用再继续读入数据了。
3. 课后习题2
给定包含4 300 000 000个32位整数的顺序文件,如何找出一个出现至少两次的整数?
二分搜索通过递归搜索包含半数以上整数的子区间来查找至少出现两次的单词。因为4300000000 > 2^32,所以必定存在重复的整数,搜索范围从[0, 2^32)开始,中间值mid为2^31,若区间[0, 2^31)内的整数个数大于2^31个,则调整搜索区间为[0, 2^31),反之则调整搜索区间为[2^31, 2^32),然后再对整个文件再遍历一遍,直到得到最后的结果。
在这里我选择了生成0~65535个顺序数列,然后随意增加N个 重复的数,检验程序能否找出重复的数。程序如下:
- #include <iostream>
- #include <fstream>
- #include <string>
- #include <sstream>
- #include <vector>
- #include <math.h>
- using namespace std;
- typedef unsigned int uint;
- int main()
- {
- //// *************************** Write File *************************** //
- //// Open file
- //ofstream wfile("file.txt", ios::out);
- //if (!wfile)
- //{
- // cout << "The file can not be opened!" << endl;
- // exit(1);
- //}
- //for (int i = 0; i < pow(2, 16); i++)
- //{
- // stringstream ss;
- // ss << i;
- // string str = ss.str();
- // wfile << str;
- // wfile << endl;
- //}
- //// Close the file
- //wfile.close();
- // *************************** Read File *************************** //
- // Open file
- ifstream rfile("file.txt", ios::in);
- if (!rfile)
- {
- cout << "The file can not be opened!" << endl;
- exit();
- }
- vector<uint> right, left;
- string line;
- uint num;
- while (getline(rfile, line))
- {
- // cout << line << endl;
- stringstream ss;
- ss << line;
- if (line != "")
- {
- ss >> num;
- if (num & ( << ))
- right.push_back(num);
- else
- left.push_back(num);
- }
- }
- // Close the file
- rfile.close();
- uint count = ;
- uint repeat = ;
- uint szLeft = ;
- uint szRight = ;
- while (count != )
- {
- vector<uint> temp;
- szLeft = left.size();
- szRight = right.size();
- if (szLeft > szRight)
- {
- right.clear();
- for (uint i = ; i < szLeft; i++)
- {
- if (left[i] & ( << - count))
- right.push_back(left[i]);
- else
- temp.push_back(left[i]);
- }
- left.clear();
- left = temp;
- }
- else
- {
- left.clear();
- for (uint i = ; i < szRight; i++)
- {
- if (right[i] & ( << - count))
- temp.push_back(right[i]);
- else
- left.push_back(right[i]);
- }
- right.clear();
- right = temp;
- }
- count++;
- }
- szLeft = left.size();
- szRight = right.size();
- if (szLeft > szRight)
- {
- repeat = left[];
- }
- else if (szLeft < szRight)
- {
- repeat = right[];
- }
- else
- {
- // no elements is repeated
- repeat = ;
- }
- cout << "The repeated one is: " << repeat << endl;
- return ;
- }
通过不断地二分,程序中left和right的大小至少一个为1,另一个,因为有重复的数,所以大小必然大于1。又因为,最后的left和right在没有重复整数的情况下理应只有一个数,这样就很简单根据left和right的大小就能够判断出重复的是哪一个数了。
程序可以正常工作。
注:后来又看到其他人的想法,此程序的效率还是可以继续提升的!
我们每次读入总数1/10的数据量的时候,就比较一下最后一个读入的数据D_last跟读入的数据量大小L_read:
- 如果D_last+1=L_read,则表明目前读入的数据中并没有重复的数,也因此可以在后边将二分搜索起点设定为L_read;
- 如果D_last+1<L_read,则表明目前已经读入的数据就已经存在重复的数了,也就是说不用再继续读入数据了。
“《编程珠玑》(第2版)第2章”:A题(二分搜索)的更多相关文章
- 《编程珠玑(第2版)》【PDF】下载
<编程珠玑(第2版)>[PDF]下载链接: https://u253469.pipipan.com/fs/253469-230382225 内容简介 书的内容围绕程序设计人员面对的一系列实 ...
- java编程思想第四版第十一章习题
第一题 package net.mindview.holding.test1; import java.util.ArrayList; import java.util.List; /** * 沙鼠 ...
- java编程思想第四版第六章总结
1. 代码重构 为什么f要代码重构 第一次代码不一定是完美的, 总会发现更优雅的写法. 代码重构需要考虑的问题 类库的修改不会破坏客户端程序员的代码. 源程序方便扩展和优化 2. 包 创建一个独一无二 ...
- Java编程思想第四版*第七章*个人练习
欢迎加群:239063848 成团的笔记:该组仅用于技术共享和交流,问题和答案公布 潘基聊天.禁止广告.禁止招聘-- 练习1:(2)创建一个简单的类.第二个类中,将一个引用定义为第一个类的对象.运用惰 ...
- java编程思想第四版第五章习题
创建一个类, 它包含一个未初始化的String引用.验证该引用被Java初始化成了null package net.mindview.initialization; public class Test ...
- java编程思想第四版第六章习题
(略) (略) 创建两个包:debug和debugoff,他们都包含一个相同的类,该类有一个debug()方法,第一个版本显示发送给控制台的String参数,而第二版本什么也不做,使用静态import ...
- java编程思想 第四版 第六章 个人练习
欢迎加群:239063848 进群须知:本群仅用于技术分享与交流.问题公布与解答 禁止闲聊.非诚勿扰 练习1:(1)在某个包中创建一个类,在这个类所处的包的外部创建该类的一个实例. import mi ...
- java编程思想第四版第9章
练习3: public class MainTest { public static void main(String args[]){ Bcycle b=new Bcycle(); b.print( ...
- Java编程思想第四版 *第五章 个人练习
练习3:(1)创建一个带默认构造器(即无參构造器)的类.在构造器中打印一条消息.为这个类创建一个对象.P116 public class Test{ public Test(){ System.out ...
- java编程思想第四版第十三章字符串 总结
1. String和StringBulider的使用 通过书中介绍, 我们得知如下结论: 当使用+连接符将字符串进行拼接的时候, 编译器会进行自动优化为使用StringBuilder连接字符串. 当在 ...
随机推荐
- java自动装箱拆箱总结
对于java1.5引入的自动装箱拆箱,之前只是知道一点点,最近在看一篇博客时发现自己对自动装箱拆箱这个特性了解的太少了,所以今天研究了下这个特性.以下是结合测试代码进行的总结. 测试代码: int a ...
- 使用maven执行单元测试总结
maven本身没有单元测试框架,但是maven的default生命周期的test阶段绑定了maven-surefire-plugin插件,该插件可以调用Junit3.Junit4.TestNG等Jav ...
- EBS并发程序监控
SELECT s.* FROM fnd_concurrent_requests r, v$session v, v$sql s WHERE r.oracle_session_id = v.audsid ...
- android 网络连接 HttpGet HttpPost方法
1.本文主要介绍利用HttpGet和HtppPost方法来获取网络json数据. 代码如下: public HttpData(String Url,HttpGetDataListener listen ...
- 02_3中方式的反射,通过Class.forName获得Class对象,通过类.class获得字节码对象,通过类实例.getClass()的方式获得Class对象
反射中加载类: Java中有一个Class类用于代表某一个类的字节码 .class文件 对应Class //1 加载类 // java中Class代表一个类,但是到底代表哪个类要明确指出 ...
- Python 键盘鼠标监听
异想天开的想记录一下自己每天的键盘键位走向,于是就在网上搜索了一下相关的实现,然后就发现了一个第三方的库pyHook.封装的很好,我们只需要傻瓜式的调用里面的API就可以了. 下面是我在使用pyHoo ...
- Java开发各层对象含义 PO,VO,DAO,BO,POJO
java的几种对象(PO,VO,DAO,BO,POJO)解释 一.PO:persistant object 持久对象,可以看成是与数据库中的表相映射的java对象.最简单的PO就是对应数据库中 ...
- xml特殊字符处理 如&
写了个request2XML的方法,每当数据中有'<'.'&'符号时,封装的XML就无法解析.发现了XML里的CDATA属性,问题迎刃而解!在XML文档中的所有文本都会被解析器解析 ...
- 07_NoSQL数据库之Redis数据库:Redis的高级应用之事务处理、持久化操作、pub_sub、虚拟内存
事务处理 Redis对事务的支持目前还比较简单.Redis只能保证一个client发起的事务中的命令可以连续的执行,而中间不会插入其他client的命令.当一个client在一个连接中发出mul ...
- Mybatis简单入门
MyBatis是一个支持普通SQL查询,存储过程和高级映射的优秀持久层框架.MyBatis消除了几乎所有的JDBC代码和参数的手工设置以及对结果集的检索封装.MyBatis可以使用简单的XML或注解用 ...