经典算法面试题(二)

1 . 大整数乘法

下面先介绍“列表法”:
例如当计算8765*234时,把乘数和被乘数照如下列出,见表:

8 7 6 5 *
16 14 12 10 2
24 21 18 15 3
32 28 24 20 4
16 14 12 10
24 21 18 15
32 28 24 20
16 38 65 56 39 20
16 38 65 56 39 20
2 16+4=20 38+7=45 65+6=71 56+4=60 39+2=41
留2 留0进2 留5进4 留1进7 留0进6 留1进4 留0进2
2 0 5 1 0 1 0

根据以上思路 就可以编写C++程序了,再经分析可得:

1,一个m位的整数与一个n位的整数相乘,乘积为m+n-1位或m+n位。
2,程序中,用三个字符数组分别存储乘数,被乘数与乘积。由第1点分析知,存放乘积的字符数组饿长度应不小于存放乘数与被乘数的两个数组的长度之和。
3,可以把第二步“计算填表”与第三四步“累加进位”放在一起完成,可以节省存储表格2所需的空间。
4,程序关键部分是两层循环,内层循环累计一数组的和,外层循环处理保留的数字和进位。
#include 
#include 

using namespace std;

int  resualt[2048] = { 0 };
string num1, num2;

void multiply(string n1,string n2)
{
    int n1len = n1.length();
    int n2len = n2.length();
    int sum = 0;
    int carry = 0;
    int i, j;
    for (i = n1len + n2len-2; i >=0; i--)
    {
        sum = carry;
        j = i - n1len+1;
        if (j < 0)
            j = 0;
        for (; j <= i&&j <= n2len-1; j++)
            sum += (n1[i - j] - '0')*(n2[j] - '0');
        resualt[i+1] = sum % 10;
        carry = sum / 10;
    }
    if (carry>0)
        resualt[0] = carry ;
}

int main()
{
    cin >> num1 >> num2;
    multiply(num1, num2);
    for (int y = 0; y cout << resualt[y];
    return 0;
}

算法改进:
8216547*96785 将两数从个位起,每3位分为节,列出乘法表,将斜线间的数字相加:

8 216 547
96 785
8 216 547 *
768 20736 52512 96
6250 169560 429395 785
768 20736 52512
6250 169560 429395
768 27016 222072 429395

将表中最后一行进行如下处理:从个位数开始,每一个方格里只保留三个数字,超出1000的部分进位到前一个方格里:

768 27016 222072 429395
768+27=795 27016+222=27238 222072+429=222501 留395进429
795 238 501 395

所以8216547*96785 = 795238501395

也就是说我们在计算生成这个二维表时,不必一位一位的乘,而可以三位三位的乘;在累加时也是满1000进位。这样,我们计算m位整数乘以n位整数,只需要进行m*n/9次乘法运算,再进行约(m+n)/3次加法运算和(m+n)/3次去摸运算。总体看来,效率是前一种算法的9倍。

2 . 哈夫曼树

定义哈夫曼树之前先说明几个与哈夫曼树有关的概念:

路径: 树中一个结点到另一个结点之间的分支构成这两个结点之间的路径。
路径长度:路径上的分枝数目称作路径长度。
树的路径长度:从树根到每一个结点的路径长度之和。
结点的带权路径长度:在一棵树中,如果其结点上附带有一个权值,通常把该结点的路径长度与该结点上的权值 之积称为该结点的带权路径长度(weighted path length)
树的带权路径长度:如果树中每个叶子上都带有一个权值,则把树中所有叶子的带权路径长度之和称为树的带
                               权路径长度。

设某二叉树有n个带权值的叶子结点,则该二叉树的带权路径长度记为:
经典算法面试题(二)_第1张图片
公式中,Wk为第k个叶子结点的权值;Lk为该结点的路径长度。
示例:
经典算法面试题(二)_第2张图片
根据哈弗曼树的定义,一棵二叉树要使其WPL值最小,必须使权值越大的叶子结点越靠近根结点,而权值越小的叶子结点
越远离根结点。
哈弗曼依据这一特点提出了一种构造最优二叉树的方法,其基本思想如下:
经典算法面试题(二)_第3张图片
下面演示了用Huffman算法构造一棵Huffman树的过程:
经典算法面试题(二)_第4张图片

3 . 由一个等概率的随机函数生成另一个等概率的随机函数

知有个rand7()的函数,可以生成等概率的[1,7]范围内的随机整数,让利用这个rand7()构造rand10()函数,生成等概率的[1,10]范围内的随机整数。

已知有个rand7()的函数,可以生成等概率的[1,7]范围内的随机整数,让利用这个rand7()构造rand10()函数,生成等概率的[1,10]范围内的随机整数。

分析:要保证rand10()在整数1-10的均匀分布,可以构造一个1-10*n的均匀分布的随机整数区间(n为任何正整数)。假设x是这个1-10*n区间上的一个随机整数,那么x%10+1就是均匀分布在1-10区间上的整数。由于(rand7()-1)*7+rand7()可以构造出均匀分布在1-49的随机数(原因见下面的说明),可以将41~49这样的随机数剔除掉,得到的数1-40仍然是均匀分布在1-40的,这是因为每个数都可以看成一个独立事件。
下面说明为什么(rand7()-1)*7+rand7()可以构造出均匀分布在1-49的随机数:
首先rand7()-1得到一个离散整数集合{0,1,2,3,4,5,6},其中每个整数的出现概率都是1/7。那么(rand7()-1)*7得到一个离散整数集合A={0,7,14,21,28,35,42},其中每个整数的出现概率也都是1/7。而rand7()得到的集合B={1,2,3,4,5,6,7}中每个整数出现的概率也是1/7。显然集合A和B中任何两个元素组合可以与1-49之间的一个整数一一对应,也就是说1-49之间的任何一个数,可以唯一确定A和B中两个元素的一种组合方式,反过来也成立。由于A和B中元素可以看成是独立事件,根据独立事件的概率公式P(AB)=P(A)P(B),得到每个组合的概率是1/7*1/7=1/49。因此(rand7()-1)*7+rand7()生成的整数均匀分布在1-49之间,每个数的概率都是1/49。

int rand_10()    
{    
    int x = 0;    
    do    
    {    
        x = 7 * (rand7() - 1) + rand7();    
    }while(x > 40);    
    return x % 10 + 1;    
}

另一种思路:第一个数由rand7()产生,第二个由rand7()+1产生,,,第10个由(rand7()+9)%10产生,由于是等概率的,以此循环,也生成的等概率的1-10之间的随机数。

4 . 字符串匹配算法

字符串匹配算法一般有朴素的字符串匹配算法,Rabin-Karp算法,优先自动机的算法以及KMP算法,其中最经典的就是KMP算法。
首先看下KMP算法的执行过程:
举例说明,如下是使用上例的模式串对目标串执行匹配的步骤
经典算法面试题(二)_第5张图片
通过模式串的5次移动,完成了对目标串的模式匹配。这里以匹配的第3步为例
此时pattern串的第1个字母与target[6]对齐,从6向后依次匹配目标串,到target[13]时发现target[13]=’a’,而pattern[8]=’c’,匹配失败,此时next[8]=5,所以将模式串向后移动8-next[8] = 3个字符,将pattern[5]与target[13]对齐,然后由target[13]依次向后执行匹配操作。在整个匹配过程中,无论模式串如何向后滑动,目标串的输入字符都在不会回溯,直到找到模式串,或者遍历整个目标串都没有发现匹配模式为止。
next跳转表,在进行模式匹配,实现模式串向后移动的过程中,发挥了重要作用。这个表看似神奇,实际从原理上讲并不复杂,对于模式串而言,其前缀字符串,有可能也是模式串中的非前缀子串,这个问题我称之为前缀包含问题。以模式串abcabcacab为例,其前缀4 abca,正好也是模式串的一个子串abc(abca)cab,所以当目标串与模式串执行匹配的过程中,如果直到第8个字符才匹配失败,同时也意味着目标串当前字符之前的4个字符,与模式串的前4个字符是相同的,所以当模式串向后移动的时候,可以直接将模式串的第5个字符与当前字符对齐,执行比较,这样就实现了模式串一次性向前跳跃多个字符。所以next表的关键就是解决模式串的前缀包含。
下面给出该算法的伪码:

KMP-MATCHER(T,P)
n<-length(T)
m<-length(P)
next<-COMPUTE-PREFIX-FUNCTION(P)
q<-0
for i<-1 to n
    do while q>0 and P[q+1]!=T[i]
        do q<-next[q]
       if P[q+1]=T[i]
           then q<-q+1
       if q=m
           then print "Pattern occurs with shift" i<-m
           q<-next[q]

COMPUTE-PREFIX-FUNCTION(P)
m<-length(P)
next[1]<-0
k<-0
for q<-2 to m
    do while k>0 and P[k+1]!=P[q]
        do k<-next[k]
    if P[k+1]=P[q]
        then k<-k+1
    next[q]<-k
return next

KMP算法的时间复杂度为O(m+n).

你可能感兴趣的:(算法)