1. 程式人生 > >【編程珠璣】【第二章】問題C

【編程珠璣】【第二章】問題C

word 保存 隨著 main arc 清晰 詞典 temp 不可

變位詞(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