【编程珠玑】【第二章】问题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);
     }
}

原文地址:https://www.cnblogs.com/blastbao/p/8306654.html

时间: 2024-10-13 00:51:55

【编程珠玑】【第二章】问题C的相关文章

一维向量旋转算法 编程珠玑 第二章

看了编程珠玑第二章,这里面讲了三道题目,这里说一下第二题,一维向量旋转算法. 题目:将一个n元一维向量(例数组)向左旋转i个位置. 解决方法:书上讲解了5种方法,自己只想起来2种最简单方法(下面讲的前两种). 1.原始方法. 从左向右依次移动一位,对所有数据平移:这样循环i次,算法最坏时间复杂度达n^2.耗时不推荐. 2.空间换时间. 顾名思义,申请一个i长度的空间,把前i半部分放到申请空间中,再把后面的所有数据向左移动i个位置,最后把申请的空间中的数据放到后半部分.浪费空间,不推荐. 3.杂技

编程珠玑第二章

编程珠玑第二章 A题 给定一个最多包含40亿个随机排列的32位整数的顺序文件,找出一个不在文件中一32位整数. 1.在文件中至少存在这样一个数? 2.如果有足够的内存,如何处理? 3.如果内存不足,仅可以用文件来进行处理,如何处理? 答案: 1.32位整数,包括-2146473648~~2146473647,约42亿个整数,而文件中只有40亿个,必然有整数少了. 2.如果采用位数思想来存放,则32位整数最多需要占用43亿个位.约512MB的内存空间. 可以采用前一章的位处理方法.然后判断每个in

集体智慧编程_第二章(提供推荐)_1

前言:最近正在拜读Toby Segaran先生写的集体智慧编程,首先感谢Toby Segaran先生将知识以书本的方式传播给大家,同时也感谢莫映和王开福先生对此书的翻译,谢谢各位的不辞辛苦.首先在写随笔之前,跟各位分享一下我的编程环境:win7系统,python版本是2.7.10,开发环境我选择的是pycharm程序.本书的第一章为集体智慧导言,主要介绍的何为集体智慧和机器学习的相关概念和其局限性,以及与机器学习相关的例子和应用场景.下面开始机器学习第二章--提供推荐的相关内容. 本章主要内容:

java编程思想 第二章

这篇时间较之前篇章时间靠后,是由于,某一天晚上看完Java编程思想文献之后来不及做笔记了. 以下笔记基本为转载,不是原创 第二章   一切都是对象 目录: 2.1 用引用操纵对象 2.2 必须由你创建所有对象 2.3 永远不需要销毁对象 2.4 创建新的数据类型:类 2.5 方法.参数和返回值 2.6 构建一个Java程序 2.7 你的第一个Java程序 2.8 注释和嵌入式文档 2.9 编码风格 2.1 用引用操纵对象 一切都看作对象,操纵的标识符实际上是对象的一个“引用”,遥控器(引用)操纵

[书籍翻译] 《JavaScript并发编程》 第二章 JavaScript运行模型

本文是我翻译<JavaScript Concurrency>书籍的第二章 JavaScript运行模型,该书主要以Promises.Generator.Web workers等技术来讲解JavaScript并发编程方面的实践. 完整书籍翻译地址:https://github.com/yzsunlei/javascript_concurrency_translation .由于能力有限,肯定存在翻译不清楚甚至翻译错误的地方,欢迎朋友们提issue指出,感谢. 本书第一章我们探讨了JavaScri

对一千万条数据进行排序---编程珠玑第二版 第一章

本书第一章提出了一个看似简单的问题,有最多1000万条不同的整型数据存在于硬盘的文件中,如何在1M内存的情况下对其进行尽可能快的排序. 每个数字用4byte,1M即可存储250 000个数据,显然,只要每次对250 000个数据排序,写入到文件中即可,重复40次. 那么如何选出每次遍历的二十五万条数据呢?有如下两个策略: 1.对一千万条数据遍历40次,第i次遍历时,判断数是否属于[i*250000,i*250000+249999),如果是,则读入内存,当第i次遍历完成时,内 存中有了二十五万条数

编程珠玑第一章习题答案

习题 1.1      如果不缺内存,如何使用一个具有库的语言来实现一种排序算法? 因为C++有sort,JAVA也有,这里以C++为例给出,记住如果用set集合来排序时,是不可以有元素重复的 代码: #include <iostream> #include <cstring> #include <cmath> #include <queue> #include <stack> #include <list> #include <

java编程思想--第二章 一切都是对象

如果不做引申的话,本章没有多少可总结的内容.有些以前没有注意的细节可以写下来. 1.数据存储的位置 (1).寄存器.程序控制不了. (2).堆栈.对象的引用.基本类型. (3).堆.各种对象. (4).常量存储.和代码在一起. (5).非RAM存储.主要指流对象和持久化对象,前者准备网络传输,后者被丢到了硬盘上. 2.java中的数组会被自动初始化: (1).基本类型数组元素会被初始化为0 (2).引用类型数组元素会被初始化为null 3.变量作用域 (1).基本类型的跟C一样 (2).引用类型

Python核心编程_第二章课后习题

以下是自己在学习Python核心编程时,做的课后练习题.现在把它们贴出来,以记录自己的学习过程.小弟是机械出身,很多练习题目写的很是机械.虽然写出来的脚本都能满足题目要求,但效率可能不是最好的,所以,小弟还是厚着脸皮把它们给贴出来,一来可以让高手指点,二来可以与我一样在学习Python的兄弟共同学习. 以下的程序均以题目标号命名,如2-3这个题目,程序名就为2_3.py. 习题2_3.py #!/usr/bin/env python A = 10 B = 4 print "A plus B is

python核心编程2第二章课后练习

2-1 print来显示变量的内容,仅用变量名时,输出的字符串使用单引号括起来的,这是为了让非字符串对象也能以字符串的方式显示在屏幕上,print语句使用str()函数显示对象,交互解释器调用repr()函数来显示对象 2-2 (a)运算1+2*4 (b)只会做运算不会输出 (c)运算未显示结果 (d)交互解释器输入一段语句后会返回语句结果 (e)print ‘1+2*4’   2-3   2-4 (a) #!/usr/etc/env python string =raw_input("plea