编程珠玑第二章问题C

Posted 小僧尤里

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了编程珠玑第二章问题C相关的知识,希望对你有一定的参考价值。

变位词(anagrams):指的是组成两个单词的字符相同,但位置不同的单词。比如说,abbcd和abcdb就是一对变位词。在介绍问题c之前,我们先看看如何判断两个字符串是否是变位词。

分析:求解题目C有两种思路:

思路一

由于变位词只是字母的顺序改变,字符长度,字符种类没有改变,所以根据此我们只要重新根据字典序排序一下,两个字符串也就一样了。如abcbd和acdbb是一对变位词,按照字典序排序之后他们都变成了abbcd。这种方法的时间效率根据你使用的排序算法不同而不同,基于比较的排序的时间复杂度下界为O(nlogn),两字符串按位比较的复杂度为O(n)。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int cmpfunc( const void *a , const void *b ){
    return *(char *)a - *(char *)b;//升序排序
    //return *(char *)b - *(char *)a;  //降序排序
}
void anagrams(char * s1,char *s2){
    int len1 = strlen(s1);
    printf("length of s1 : %d\\n",len1);
    int len2 = strlen(s2);
    printf("length of s1 : %d\\n",len1);
    //注意短路运算,优先顺序从左到右
    if(len1 != len2 || len1==0 || len2 ==0){
        printf("they are not anagrams ");
        return;
    }
    qsort(s1,len1, sizeof(char), cmpfunc);
    qsort(s2,len2, sizeof(char), cmpfunc);
    /*测试代码
    int i;
    for(i=0;s1[i]!=\'\\0\';i++){
        printf("%c ",s1[i]);
    }
    printf("\\n");
    for(i=0;s2[i]!=\'\\0\';i++){
        printf("%c ",s2[i]);
    }
    printf("\\n");
    */
    if(strcmp(s1,s2)==0){       //strcmp的返回值正,0,负
             printf("they are anagrams ");
    }else{
            printf("they are not anagrams ");
    }
}
int main(int argc, char* argv[]){
    char s1[] = "abbcb!de?";
    char s2[] = "ac?edbbb!";
    anagrams(s1,s2);
    return 0;
}    

注意,本代码使用了库函数qsort,需要自己定义比较函数cmpfun,我们这里使用常规的比较函数,基于ASCII码值的比较。并且排序之后字符串的比较我们使用了标准函数strcmp,是区分字符串大小写的。如果想忽略大小写,需要使用自定义的strcmp

思路二

由于组成变位词的字符是一模一样的,数目也一样,因此我们可以先统计每个字符串中各个字符出现的次数, 然后看这两个字符串中各字符出现次数是否一样。如果是,则它们是一对变位词。

这需要开一个辅助数组来保存各字符的出现次数。我们可以开一个大小是26的整数数组用于记录字符串中每个字符(不区分大小写的话,最多26个英文字母)出现的次数。遍历第一个字符串时,将相应字符出现的次数加1;遍历第二个字符串时, 将相应字符出现的次数减1。最后如果数组中所有元素值都为0,说明两个字符串是一对变位词。 (第1个字符串中出现的字符都被第2个字符串出现的字符抵消了),如果数组中有一个不为0,说明它们不是一对变位词。

代码一:

#include <stdio.h>
#include <string.h>
void  anagrams(char * s1,char *s2){
    int i,count[26];
    memset(count,0,sizeof(count));
    int len1 = strlen(s1);
    int len2 = strlen(s2);
    //注意短路运算,优先顺序从左到右
    if(len1 != len2 || len1==0 || len2 ==0){
        printf("they are not anagrams ");
        return;
    }
    for(i=0;s1[i]!=\'\\0\';i++){
        if(s1[i]>=\'a\'&&s1[i]<=\'z\') 
            count[s1[i]-\'a\']++;
        if(s1[i]>=\'A\'&&s1[i]<=\'Z\')
            count[s1[i]-\'A\']++;
    }
    for(i=0;s2[i]!=\'\\0\';i++){
        if(s2[i]>=\'a\'&&s2[i]<=\'z\')
            count[s2[i]-\'a\']--;
        if(s2[i]>=\'A\'&&s2[i]<=\'Z\')
            count[s2[i]-\'A\']--;
    }
    for(i=0;i<26;i++){
        if(count[i]!=0){
            printf("they are not anagrams ");
            return;
        }    
    }
    printf("they are anagrams ");
}
int main(int argc, char* argv[]){
    char *s1 = "aaABBddd";
    char *s2 = "abaBafff";
    anagrams(s1,s2);
    return 0;
}    

注意,这里我们把大写字母和小写字母等同对待,Aab和aab被视作变位词,所以a和A的共同总数由count[0]记录,因此count数组只需要26个元素即可。如果区分大小写,那么一个单词中可能包含的字母达到52种,因此count需要52个空间,此时Aab和abA是变位词而和aba不是变位词。把大写字母的计数存储在count数组的高位,则相应代码需要改成count[s1[i]-\'A\'+26]++和count[s2[i]-\'A\'+26]--。

代码二:因为变位词的概念不仅针对于英文字符串(只有26个小写+26个大写英文字母),也有可能包含其他符号。此时,大小写字母理所当然的被认为是不同的,所以这个时候我们需要统计一个符号串中出现的所有可能的符号的计数,ASCII码表示的符号数目为256个,所以开辟的count数组大小为256。我们不需要通过形如“s1[i]-\'A\'+26”的代码进行下标的控制,因为ASCII符号编码本身就是从0到255的,因此我们直接存储即可,只是编译器可能会报“字符向整形进行码制转换”的警告,忽略即可。

#include <stdio.h>
#include <string.h>
void  anagrams(char * s1,char *s2){
    int i,count[256];
    memset(count,0,sizeof(count));
    int len1 = strlen(s1);
    int len2 = strlen(s2);
    //注意短路运算,优先顺序从左到右
    if(len1 != len2 || len1==0 || len2 ==0){
        printf("they are not anagrams ");
        return;
    }
    for(i=0;s1[i]!=\'\\0\';i++){
//两个循环合二为一,与原来等价,更简洁。
        count[s1[i]]++;
        count[s2[i]]--;
    }
    for(i=0;i<256;i++){
        if(count[i]!=0){
            printf("they are not anagrams ");
            return;
        }    
    }
    printf("they are anagrams ");
}
int main(int argc, char* argv[]){
    char *s1 = "ab?Aa,B";
    char *s2 = "A,Baba?";
    anagrams(s1,s2);
    return 0;
}

 

问题C

    给定一个英语词典,找出其中的所有变位词的集合。例如,“pots”、“stop”和“tops”互为变位词,因为每一个单词都可以通过改变其他单词中的字母的顺序来得到。

解答:

解法一、最容易想到解决方法——穷举

从英语词典中逐一读出单词,对每个单词把该单词所包含的的字母进行全排列,获得所有可能的字母排列,便得到该单词所有可能的变位词的集合。而后,对于该集合中每一种字母排列去遍历字典,如果某字母排列位于词典中意味着该排列是一个合法的单词即为当前单词的变位词。

因为一个单词的全排列数目往往是很多的,而其中与其他单词是变位词的可能很少甚至没有,这样会浪费许多无意义的查询。尤其是当单词变的很长,求其各个排列的时间、以及进行检索的时间将会超级长,这种情况下,该方法不可取。例如一个单词有22个不同字母构成,如果考虑其字母全排列为22! 大约是10的21次方种排列,作者的字典单词数目是 2300000个单词条目,可见每种排列都要去比较约2300000次才能确定其是否为变位词,即使计算速度非常快,也需要花费超级超级长的时间。

综上,本方法不仅代码编写复杂度高,算法执行复杂度更高,而且查找结果的重复度太高造成去重复杂且开销大,所以一点都不可取。

解法二、穷举法的改进

我们在前面介绍了比较两个单词是否为变位词的方法,从英语词典中逐一读出单词,然后针对该单词跟字典中的其他单词进行比较,这样便能够查找出所有同属于一组的变位词。现在分析一下复杂度,假设一次比较至少花费1微秒的时间,每个单词需要和20万(字典大小)个单词进行比较:20万个单词*每个单词平均比较20万次*每次比较消耗1微秒==11小时(200000单词 x 200000比较/单词 x 1微秒/比较 = 40000x10^6秒 = 40000秒 ≈ 11.1小时)。虽然实际上不需要这么多次比较,但是开销仍然在同一个数量级上,可见比较的次数还是太多,导致效率低下,我们需要找出效率更高的方法。

但是,这种方法并不是一无是处,当当单词长度不是很长,且词典不是很大的时候,效率还是比较好的。

解法三、来自编程珠玑的解法

标识字典中的每一个单词,使得在相同变位词类中的单词具有相同的的标识,然后集中具有相同标识的单词。将每个单词按照字母表排序,排序后得到的字符串作为该单词的标识。那么对于该问题的解题过程可以分为三步:第一步,读入字典文件,对单词进行排序得到标识;第二步,将所有的单词按照其标识的顺序排序;第三步,将同一个变位词类中的各个单词放到同一行中。

代码一本代码摘自网上,是一个基于内存而非文件的算法,主要用于展示算法的思想,代码简单易懂,风格清晰明了,是十分值得参考和学习的代码。赞!

#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include<ctype.h>
#define MAX_SIZE 100
int partition(char words[][MAX_SIZE], char sig[][MAX_SIZE], int left, int right);
/*用于把单词字符转化为小写字母。*/
void str_to_lower(char *str) {
    int i;
    int len = strlen(str);
    for(i = 0; i < len; i++) {
       str[i]  = tolower(str[i]);
    }
}
/*比较字符大小。*/
int char_compare(const void *c1, const void *c2){
    return *(char *)c1 - *(char *)c2;
}

/**
 * 字典中的第i个(下标从0开始)单词存储在word[i][]中,该单词的标识存储
 * 在sig[i][]中。本函数就是为word数组中每个单词求它的标识,并存储在sig
 * 数组中对应的位置上。
 * 单词的标识实际上就是该单词字符串按字典序排序之后的结果。
 */
void sign(char words[][MAX_SIZE], int length, char sig[][MAX_SIZE]) {
    int i;
    for(i = 0; i < length; i++) {
        strcpy(sig[i], words[i]);
        qsort(sig[i], strlen(sig[i]), sizeof(char), char_compare); 
    }
}
/**
 * 比较两个字符串的大小。因为qsort要求cmpFunc函数参数为const void*类型,
 * 对strcmp进行封装之后传递给qsort函数便可以对一组字符串进行排序。
 */
int str_compare(const void *s1, const void *s2) {
    return strcmp((char *)s1, (char *)s2);
}
void qsort_str(char words[][MAX_SIZE], char sig[][MAX_SIZE], int left, int right) {
    int part;
    if(left >= right) 
        return;
    part = partition(words, sig, left, right);
    qsort_str(words, sig, left, part - 1);
    qsort_str(words, sig, part + 1, right);
}
/**
 * 使用快速排序对字符串数组进行排序
 */
void sort(char words[][MAX_SIZE], char sig[][MAX_SIZE], int length) {
    qsort_str(words, sig, 0, length - 1);    
}
int partition(char words[][MAX_SIZE], char sig[][MAX_SIZE], int left, int right) {
    char temp_word[MAX_SIZE];
    char temp_sig[MAX_SIZE];  
    strcpy(temp_sig, sig[left]);
    strcpy(temp_word, words[left]);      
    while(left < right) {
        while(str_compare(temp_sig, sig[right]) < 0 && left < right) right--;
        strcpy(sig[left], sig[right]);
        strcpy(words[left], words[right]);
    
        while(str_compare(temp_sig, sig[left]) >= 0 && left < right) left++;
        strcpy(sig[right], sig[left]);
        strcpy(words[right], words[left]);
    }
    strcpy(words[right], temp_word);
    strcpy(sig[right], temp_sig);
    return right;
}
/* 汇总变位词。
 * 此时sig数组中的字符串都是按字典序排好序的,sig中相同的串都处于相邻位置,
 * 相同sig对应的word属于变位词,应该在同一行输出。
 * 实际上就是根据sig排序后的结果,逐行的输出一组一组的变位词。
 * */
void squash(char words[][MAX_SIZE], char sig[][MAX_SIZE], int length) {
    int i;
    char oldsig[MAX_SIZE];
    strcpy(oldsig, sig[0]);
    printf("%-6s:", oldsig);
    for(i = 0; i < length; i++) {
        //每遇到一个新sig,就另起一行输出新的一组变位词。
        if(strcmp(oldsig, sig[i]) != 0) {
            strcpy(oldsig, sig[i]);
            printf("\\n");
            printf("%-6s:", oldsig);
        }
        //同一组变位词在同一行逐一输出。
        printf("%-6s", words[i]);
    }
    printf("\\n");

}
void print(char words[][MAX_SIZE], char sig[][MAX_SIZE], int length) {
    int i;
    for(i = 0; i < length; i++) {
        printf("%s %s\\n", sig[i], words[i]);
    }

}
int main() {
    char words[][100] = {"pans", "pots", "opt", "snap", "stop", "tops"};
    char sig[6][100];
    sign(words, 6, sig);//为单词添加标识
    print(words, sig, 6);
    sort(words, sig, 6);//使用标识排序
    printf("****************************************\\n");
    print(words, sig, 6);
    printf("----------------------------------------\\n");
    squash(words, sig, 6);//汇总变位词
    return 0;
}

代码来自互联网,原作者说:考虑为每个单词增加一个标识,然后再以标识对单词进行排序,这样排序后,相同标识的单词就分在一起,这样就找出了所有单词的变位词集合。可以将该方法分为以下三个步骤:

(1)为每个单词增加一个标识,这个步骤的关键是怎么找每个单词的标识,使得一个单词的所有的变位词都又相同的标识,相当于找到一个Hash函数,使得一个单词的所有变位词都有相同的Hash值,合适的方式是对单词中的字母进行排序,如“pots”,“stop”和“tops”这三个单词是变位词,他们的标识是“opst”,即对单词中的字母按照字母顺序进行排序,最后得到标识为“opst”。

(2)以单词的标识对字典中的单词排序(对字符char排序可以使用c标准库中的函数qsort实现),经过上面的处理,就得到了一个单词与标识的二元组,将这个二元组视为一个整体,可以使用一个结构体(或对象)来理解,比如结构体可以定义为:

struct pair{
    char identity[MAX_SIZE];
    char word[MAX_SIZE];
}

所有的单词经过步骤1后就得到了一个包含标识和单词的结构体数组,这样使用快速排序对这个结构体数组按照标识identity的字典序进行排序。

(3)汇总单词的变位词,经过步骤2,将有相同标识的单词汇集在一起了,然后再进行汇总。

值得注意的是,在编码实现中,对于步骤2并没有定义结构体,直接使用一个单词数组和一个标识数组来实现,如单词数组中第i个单词的标识保存在标识数组的第i个元素中。此外,因为涉及到对字符串数组排序(按照字典序对标识数组中的字符串进行排序,排序过程中伴随着word数组中对应元素的移动),所以并不能够直接使用c标准库中的qsort函数,需要自行实现快速排序,排序的思想与一般快排并无二致,只是在其内部元素移动时注意要两个数组的元素同时移动,以确保word和sig数组中元素的一一对应。

代码二:基于文件的方法。从文件中读取数据,生成标识后存储在中间文件,以及排序生成最终文件...有两种思路,一种是一次性把数据读入内存,处理完毕后再输出到文件,http://www.cnblogs.com/seebro/archive/2012/03/01/2375644.html另一种思路就是在内存有限的情况下采用基于文件的排序策略http://www.oschina.net/code/snippet_2277123_48318。代码涉及文件操作的库函数,这里暂时不去实现,以后有时间再说,只是展出一小部分代码作为示意:

void sign(char *input_file_name, char *output_file_name) {
    FILE *fp_input, *fp_output;    
    if ((fp_input=fopen(input_file_name,"r"))== NULL ||(fp_output=(output_file_name,"w"))==NULL){
        printf("cannot access the file!");
        exit(0);
    }
    char word[WORDMAX];//这块定义成指针行不行?感觉应该不行。
    char sig[WORDMAX];
    while(fscanf(fp_input,"%s",word)!=EOF){
        //printf("%s  \\n",word);
        str_to_lower(word);
        strcpy(sig,word);
        qsort(sig,strlen(sig),sizeof(char),char_compare);//生成签名
        //printf("%s %s \\n",sig,word);
        fprintf(fp1,"%s %s \\n",sig,word);
    }
    if(fclose(fp_input)!=0 || fclose(fp_output)!=0 ){
         exit(1);
     }
}

 

以上是关于编程珠玑第二章问题C的主要内容,如果未能解决你的问题,请参考以下文章

编程珠玑第二章问题A

编程珠玑第二章问题B

第六周读书笔记——《编程珠玑(第二版)》

<编程珠玑>笔记 三个算法

《编程珠玑》高清 pdf版 第二版

编程珠玑第二章编程求解最大公约数和最小公倍数