Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update 12~15.0.md #77

Merged
merged 1 commit into from
Jan 1, 2014
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 29 additions & 31 deletions ebook/zh/12~15.0.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,9 @@

###题目如下

7、腾讯面试题:给40亿个不重复的unsignedint的整数,没排过序的,然后再给一个数,如何快速判断这个数是否在那40亿个数当中?
7、腾讯面试题:给40亿个不重复的unsigned int的整数,没排过序的,然后再给一个数,如何快速判断这个数是否在那40亿个数当中?

__分析__:1个unsigned int占用4字节,40亿大约是4G个数不到,那么一共大约要用16G的内存空间,如果内存不够大,反复和硬盘交换数据的话,后果不堪设想。
__分析__:1个unsigned int占用4字节,40亿大约是4G个数,那么一共大约要用16G的内存空间,如果内存不够大,反复和硬盘交换数据的话,后果不堪设想。

  那么怎么储存这么多的数据呢?还记得伴随数组么?还是那种思想,利用内存地址代替下标。

Expand All @@ -42,11 +42,11 @@ __分析__:1个unsigned int占用4字节,40亿大约是4G个数不到,那

  那么69可以表示0.2.6三个数存在,其余的7以下的数不存在,0表示0-7都不存在,255表示0-7都存在,这就是位图算法:通过全部置0,存在置1,这样一种模式来通过连续的地址存贮数据,和检验数据的方法。

  那么1个 unsigned int代表多少个数呢?1个unsigned int 是一个2^32以内的数,那么也就是这样的1个数,可以表示32个数是否存在。同理申请一个unsigned int的数组a[n]则可以表示连续的(n+1)*32的数。也就是a[0]表示0-31的数是否存在,a[1]表示32-63的数是否存在,依次类推。
  那么1个 unsigned int代表多少个数呢?1个unsigned int 是一个2^32以内的数,那么也就是这样的1个数,可以表示32个数是否存在。同理申请一个unsigned int的数组a[n]则可以表示连续的 n*32的数。也就是a[0]表示0-31的数是否存在,a[1]表示32-63的数是否存在,依次类推。

  这时候需要用多大的内存呢?

    16g/32=512M
    16G/32=512M

  512M和16G之间的区别,却是是否一个32位寻址的CPU能否办得到的事儿了,众所周知,32位CPU最大寻址不超过4G,固然,你会说,现在都是64位的CPU之类的云云,但是,对于底层的设计者来说,寻址范围越小越好操控的事实是不争的。

Expand All @@ -57,11 +57,11 @@ __当数据超出可接受范围之后…__

  当然,下面就要开始说一说,当数据超出了可以接受的范围之后的事情了。比如, 2^66范围的数据检索,也会是一个问题

  4倍于64位CPU寻址范围,如果加上CPU本身的偏移寄存器占用的资源,可能应该是6-8个64位U的寻址范围,如果反复从内存到硬盘的读写,过程本身就是可怕的。
  4倍于64位CPU寻址范围,如果加上CPU本身的偏移寄存器占用的资源,可能应该是6-8个64位CPU的寻址范围,如果反复从内存到硬盘的读写,过程本身就是可怕的。

  算法,更多的是用来解决瓶颈的,就想现在,根本不用考虑内存超出8M的问题,但是20年前,8086的年代,内存4M,或者内存8M,你怎么处理?固然做软件的不需要完全考虑摩尔定律,但是摩尔定律绝对是影响软件和算法编写者得想法的。

  再比如,乌克兰俄罗斯的一批压缩高手,比如国内有名的R大,为什么压缩会出现?就是因为,要么存不下,要么传输时间过长。网络再好,64G的高清怎么的也得下载个一段时间吧。海量数据处理,永远是考虑超过了当前硬件条件的时候,该怎么办?!
  再比如,乌克兰俄罗斯的一批压缩高手,比如国内有名的R大,为什么压缩会出现?就是因为,要么存不下,要么传输时间过长。网络再好,64G的高清怎么的也得下遍历n个元素取出等概率载个一段时间吧。海量数据处理,永远是考虑超过了当前硬件条件的时候,该怎么办?!

  那么我们可以发现一个更加有趣的问题,如果存不下,但是还要存,怎么办!

Expand Down Expand Up @@ -94,11 +94,11 @@ __LZW算法的适用范围__
END of WHILE
output the code for STRING

  看过上面的适用范围在联想本题,数据有多少种,根据同余模的原理,可以惊人的发现,其实真的非常适合压缩,但是压缩之后,尽管存下了,在查找的时候,势必又需要解码,那么又回到了我们当初学习算法时候,的那句经典话,算法本身,就是为了解决时间和空间的均衡问题,要么时间换空间,要么空间换时间。
  看过上面的适用范围再联想本题,数据有多少种,根据同余模的原理,可以惊人的发现,其实真的非常适合压缩,但是压缩之后,尽管存下了,在查找的时候,势必又需要解码,那么又回到了我们当初学习算法时的那句经典话,算法本身,就是为了解决时间和空间的均衡问题,要么时间换空间,要么空间换时间。

  更多的,请读者自行思考,因为,压缩本身只是想引起读者思考,已经是题外话了~本部分完--__上善若水.qinyu__。

##第二部分、遍历n个元素取出等概率随机取出其中之一元素
##第二部分、随机取出其中之一元素

###问题描述

Expand All @@ -114,7 +114,7 @@ __LZW算法的适用范围__

  第二个人中签的情况只能在第一个人未中时才有可能,所以他中的概率是4/5 X 1/4 = 1/5(4/5表示第一个人未中,1/4表示在剩下的4个签里中签的概率),所以,第二个人最终的中签概率也是1/5,

  同理,第三个人中签的概率为:第一个人未中的概率X 第二个人未中的概率X第三个人中的概率,即为:4/5 X 3/4 X 1/3 = 1/5,
  同理,第三个人中签的概率为:第一个人未中的概率 * 第二个人未中的概率 * 第三个人中的概率,即为:4/5 * 3/4 * 1/3 = 1/5,

  一样的可以求出第四和第五个人的概率都为1/5,也就是说先后顺序不影响公平性。

Expand All @@ -140,22 +140,22 @@ __LZW算法的适用范围__

  在遍历到第1个元素的时候,即L为1,那么r%L必然为0,所以e为第一个元素,p=100%,

  遍历到第2个元素时,L为2,r%L==0的概率为1/2, 这个时候,第1个元素不被替换的概率为1X(1-1/2)=1/2,
  遍历到第2个元素时,L为2,r%L==0的概率为1/2, 这个时候,第1个元素不被替换的概率为1*(1-1/2)=1/2,

  第1个元素被替换,也就是第2个元素被选中的概率为1/2 = 1/2,你可以看到,只有2时,这两个元素是等概率的机会被选中的。
  第1个元素被替换,也就是第2个元素被选中的概率为1/2,你可以看到,只有2时,这两个元素是等概率的机会被选中的。

  继续,遍历到第3个元素的时候,r%L==0的概率为1/3,前面被选中的元素不被替换的概率为1/2 X (1-1/3)=1/3,前面被选中的元素被替换的概率,即第3个元素被选中的概率为1/3
  继续,遍历到第3个元素的时候,r%L==0的概率为1/3,前面被选中的元素不被替换的概率为1/2*(1-1/3)=1/3,前面被选中的元素被替换的概率,即第3个元素被选中的概率为1/3

  归纳法证明,这样走到第L个元素时,这L个元素中任一被选中的概率都是1/L,那么走到L+1时,第L+1个元素选中的概率为1/(L+1), 之前选中的元素不被替换,即继续被选中的概率为1/L X ( 1-1/(L+1) ) = 1/(L+1)。证毕。
  归纳法证明,这样走到第L个元素时,这L个元素中任一被选中的概率都是1/L,那么走到L+1时,第L+1个元素选中的概率为1/(L+1), 之前选中的元素不被替换,即继续被选中的概率为1/L*(1-1/(L+1)) = 1/(L+1)。证毕。

  也就是说,走到文件最后,每一个元素最终被选出的概率为1/n, n为文件中元素的总数。好歹我们是一个技术博客,看不到一丁点代码多少有点遗憾,给出一个选取策略的伪代码,如下:

__伪代码__

Element RandomPick(file):
Int length = 1;
While( length <= file.size )
If( rand() % length == 0)
While (length <= file.size)
If (rand() % length == 0)
Picked = File[length];
Length++;
Return picked
Expand All @@ -169,32 +169,30 @@ __伪代码__

方法: 计数法

&emsp;&emsp;假设一天之内某个IP访问百度的次数不超过40亿次,则访问次数可以用unsigned表示.用数组统计出每个IP地址出现的次数, 即可得到访问次数最大的IP地址.
&emsp;&emsp;假设一天之内某个IP访问百度的次数不超过40亿次,则访问次数可以用unsigned int表示。用数组统计出每个IP地址出现的 次数, 即可得到访问次数最大的IP地址

&emsp;&emsp;IP地址是32位的二进制数,所以共有N=2^32=4G个不同的IP地址, 创建一个unsigned count[N];的数组,即可统计出每个IP的访问次数,而sizeof(count) == 4G*4=16G, 远远超过了32位计算机所支持的内存大小,因此不能直接创建这个数组.下面采用划分法解决这个问题.
&emsp;&emsp;IP地址是32位的二进制数,所以共有N=2^32个不同的IP地址,创建一个大小为N的的数组count,即可统计出每个IP的访问次数,而sizeof(count) == 4G*4 = 16G,远远超过了32位计算机所支持的内存大小,因此不能直接创建这个数组.下面采用划分法解决这个问题

&emsp;&emsp;假设允许使用的内存是512M, 512M/4=128M 即512M内存可以统计128M个不同的IP地址的访问次数.而N/128M =4G/128M = 32 ,所以只要把IP地址划分成32个不同的区间,分别统计出每个区间中访问次数最大的IP, 然后就可以计算出所有IP地址中访问次数最大的IP了.
&emsp;&emsp;假设允许使用的内存是512M, 512M/4=128M 即512M内存可以统计128M个不同的IP地址的访问次数而N/128M = 4G/128M = 32 ,所以只要把IP地址划分成32个不同的区间,分别统计出每个区间中访问次数最大的IP,然后就可以计算出所有IP地址中访问次数最大的IP了.

&emsp;&emsp;因为2^5=32, 所以可以把IP地址的最高5位作为区间编号, 剩下的27为作为区间内的值,建立32个临时文件,代表32个区间,把相同区间的IP地址保存到同一的临时文件中.
&emsp;&emsp;因为2^5=32, 所以可以把IP地址的最高5位作为区间编号,剩下的27为作为区间内的值,建立32个临时文件,代表32个区间,把相同区间的IP地址保存到同一临时文件中.

例如:

ip1=0x1f4e2342
IP1=0x1f4e2342

ip1的高5位是id1 = ip1 >>27 = 0x11 = 3
IP1的高5位是id1 = IP1 >> 27 = 0x11 = 3

ip1的其余27位是value1 = ip1 &0x07ffffff = 0x074e2342
IP1的其余27位是value1 = IP1 & 0x07ffffff = 0x074e2342

所以把 value1 保存在tmp3文件中.

由id1和value1可以还原成ip1, 即 ip1 =(id1<<27)|value1
由id1和value1可以还原成IP1, 即 IP1 =(id1 << 27) | value1

&emsp;&emsp;按照上面的方法可以得到32个临时文件,每个临时文件中的IP地址的取值范围属于[0-128M),因此可以统计出每个IP地址的访问次数.从而找到访问次数最大的IP地址

程序源码:

test.cpp是c++源码.

```cpp
#include <fstream>
#include <iostream>
Expand Down Expand Up @@ -307,13 +305,13 @@ int main()

&emsp;&emsp;回文判断是一类典型的问题,尤其是与字符串结合后呈现出多姿多彩,在实际中使用也比较广泛,而且也是面试题中的常客,所以本文就结合几个典型的例子来体味下回文之趣。

&emsp;&emsp;回文,英文palindrome,指一个顺着读和反过来读都一样的字符串,比如 madam、我爱我,这样的短句在智力性、趣味性和艺术性上都颇有特色,中国历史上还有很多有趣的回文诗呢 :)
&emsp;&emsp;回文,英文palindrome,指一个顺着读和反过来读都一样的字符串,比如madam、我爱我,这样的短句在智力性、趣味性和艺术性上都颇有特色,中国历史上还有很多有趣的回文诗呢 :)

###一、回文判断

&emsp;&emsp;那么,我们的第一个问题就是:__判断一个字串是否是回文__

&emsp;&emsp;通过对回文字串的考察,最直接的方法显然是将字符串逆转,存入另外一个字符串,然后比较原字符串和逆转后的字符串是否一样,一样就是回文,这个方法的时空复杂度都是 O(n)。
&emsp;&emsp;通过对回文字串的考察,最直接的方法显然是将字符串逆转,存入另外一个字符串,然后比较原字符串和逆转后的字符串是否一样,一样就是回文,这个方法的时空复杂度都是O(n)。

&emsp;&emsp;我们还很容易想到只要从两头开始同时向中间扫描字串,如果直到相遇两端的字符都一样,那么这个字串就是一个回文。我们只需要维护头部和尾部两个扫描指针即可,代码如下:

Expand All @@ -324,7 +322,7 @@ int main()
*/
bool IsPalindrome(const char *s, int n)
{
if (s == 0 || n < 1) return false; // invalid string
if (s == NULL || n < 1) return false; // invalid string
char *front, *back;
front = s; back = s + n - 1; // set front and back to the begin and endof the string
while (front < back) {
Expand All @@ -346,12 +344,12 @@ bool IsPalindrome(const char *s, int n)
*/
bool IsPalindrome2(const char *s, int n)
{
if (s == 0 || n < 1) return false; // invalid string
if (s == NULL || n < 1) return false; // invalid string
char *first, *second;
int m = ((n>>1) - 1) >= 0 ? (n>>1) - 1 : 0; // m is themiddle point of s
int m = ((n >> 1) - 1) >= 0 ? (n >> 1) - 1 : 0; // m is themiddle point of s
first = s + m; second = s + n - 1 - m;
while (first >= s)
if (s[first--] !=s[second++]) return false; // not equal, so it's not apalindrome
if (s[first--] != s[second++]) return false; // not equal, so it's not apalindrome
return true; // check over, it's a palindrome
}
```
Expand Down