Showing posts with label functional. Show all posts
Showing posts with label functional. Show all posts

Thursday, March 20, 2014

C++11: Տեքստի տրոհումը բառերի՝ istream-ի միջոցով

Իմ բլոգի գրառումներում արդեն երկու անգամ անդրադարձել եմ տրված տեքստի բառերի հաճախության աղյուսակի կառուցման խնդրին։ Մի անգամ Tcl լեզվով և մի անգամ էլ Python լեզվով։ Այս անգամ որոշել էի նույն խնդիրը գրել C++11 լեզվով՝ այդ լեզվի հնարավորություններն ուսումնասիրելու համար։

Խնդիրը բաղկացած է երկու մասից ա) տրված տեքստը տրոհել բառերի, և բ) հաշվել տեքստում ամեն մի բառի հանդիպելու հաճախությունը։

Ֆայլից սիմվոլ առ սիմվոլ տեքստը կարդալու և բառեր կազմելու հավես ես չունեի։ C լեզվի գրադարանային strtok ֆունկցիան էլ տարբեր պատճառներով չէի ուզում օգտագործել։ Նպատակ ունեի օգտագործել C++ լեզվի istream դասը և այդ դասի համար սահմանված operator>> գործողությունը։ Ավելի պարզ ասած, ուզում էի, որ հետևյալ read_file ֆունկցիան filename ֆայլից կկարդա բառերը և կլցնի words վեկտորի մեջ.
void read_file( char* filename, std::vector<std::string>& words )
{
  std::string w{ "" };
  std::ifstream fin{ filename };
  while( !fin.eof() ) {
    fin >> w;
    words.push_back( w );
  }    
  fin.close();
}
Բայց պարզ է, որ words վեկտորը պարզապես լցվելու է տեքստի՝ բացատներով բաժանված հատվածներով, որովհետև լռելությամբ operator>> գործողությունը բաժանիչ (delimiter) է համարում միայն բացատները։
     Իմ խնդրի համար բաժանիչ պետք է համարել այբուբենի մեծատառերից ու փոքրատառերից տարբերովող բոլոր նիշերը։ Եվ istream դասի օբյեկտը պետք է մի որևէ եղանակով կարգավորել այնպես, որ բաժանիչ համարվեն և անտեսվեն բոլոր ոչ պետքական նիշերը։      Ինտերնետում քչփորելուց հետո հասկացա, որ իմ ուզած բաժանիչները սահմանելու համար պետք է ստեղծեմ նոր locale օբյեկտ։ Հետո այդ locale-ի համար էլ սահմանեմ այնպիսի ctype ֆասետ (facet - սրա անունը այդպես էլ չհասկացա), որում արդեն այբուբենի տառերից տարբերվող նիշերին տրված է space դիմակը (mask)։ Ահա այդ նոր սահմանված դասը, որ ժառանգած է std::ctype<char>-ից։
class word_ctype : public std::ctype<char> {
private:
  static const mask* custom_table()
  {
    mask* wcs = new mask[table_size];
    std::copy_n(classic_table(), table_size, wcs);
    for( int c = 32; c < table_size; ++c ) {
      if( isalpha(c) ) continue;
      wcs[c] = (mask)space;
    }
    return wcs;
  }
public:
  word_ctype( std::size_t refs = 0 )
    : ctype(custom_table(), true, refs)
  {}
};
Հետո արդեն ավելի հետաքրքիր մասն է։ Սահմանեցի create_dictionary ֆունկցիան, որի առաջին արգումենտը վերլուծվող ֆայլի անունն է, իսկ երկրորդը՝ կառուցվելիք բառարանի ֆայլի անունն է։ Ստացվեց համարյա ֆունկցիոնալ կոդ։
void create_dictionary( char* infile, char* outfile )
{
  // բառարան է, որը հաշվում է ամեն մի բառի քանակը
  std::map<std::string,int> dict;
  // ընթերցման հոսքի ստեղծում՝ տրված ֆայլի անունով
  std::ifstream fin{infile};
  // ընթերցման հոսքում ներդնել նոր locale օբյեկտ՝ վերը սահմանված facet-ով
  fin.imbue(std::locale{std::locale::classic(), new word_ctype});
  // հոսքից կարդալու երկու իտերատորներ
  std::istream_iterator sbegin(fin), send;
  // կարդալ հոսքը սկզբից մինչև վերջ և բառերն ավելացնել բառարանում
  std::for_each( sbegin, send, [&dict](std::string w){ ++dict[downcase(w)]; } );
  fin.close();
  
  // ստեղծել արտածման հոսք՝ բառարանը գրելու համար
  std::ofstream fout{outfile};
  // բառարանի ամեն մի գրառման համար ...
  for( auto w : dict )
    // ֆայլում գրել բառը և նրա քանակը
    fout << w.first << ',' << w.second << std::endl;
  fout.close();
}
Այս ֆունկցիայում հոսքից կարդացած բառի բոլոր տառերը փոքրատառ դարձնելու համար օգտագործված downcase ֆունկցիան սահմանված է հետևյալ կերպ։
std::string downcase( std::string sr )
{
  std::transform( sr.begin(), sr.end(), sr.begin(), ::tolower );
  return sr;
}

Sunday, January 20, 2013

Common Lisp: Պարզ ու կատարյալ թվերի մասին

«N թիվը կոչվում է պարզ, եթե այն բացի մեկից և իրենից այլ բաժանարարներ չունի։» Եթե թվի պարզությունը որոշող ֆունկցիան գրենք ըստ այս սահմանման, ապա պետք է որ ստանանք մոտավորապես հետևյալը․
(defun is-prime-a (num)
  (loop for k from 2 to (1- num)
        never (zerop (mod num k))))
Սա իմպերատիվ լուծում է, որտեղ ցիկլի կազմակերպմամբ բացահայտորեն նկարագրված է, թե ինչ գործողություններ պետք է անել թվի պարզությունը ստուգելու համար։ (Այս ալգորիթմի քայլերի քանակը կարելի է կրճատել, եթե ցիկլի հաշվիչի վերին սահմանը փոխարինենք (ceiling (sqrt num)) արտահայտությամբ։ Բայց սա ոճական առումով ոչ մի լավացում չի տալիս։)

Փորձենք տալ պարզ թվի մեկ այլ սահմանում՝ հիմնված առաջինի վրա․ «N թիվը կոչվում է պարզ, եթե այն ունի միայն երկու բաժանարար։» Բայց այս սահմանումը պահանջում է, որ ստուգվեն 1..N միջակայքի մոլոր թվերը։ Այս անգամ էլ միջակայքի վերին սահմանը փոխարինելով (ceiling (sqrt N)) թվով, կստանանք մի նոր սահմանում։ «N թիվը կոչվում է պարզ, եթե այն 1..(sqrt N) միջակայքում ունի միայն մեկ բաժանարար։» (Բնականաբար այդ բաժանարարը 1 թին է։)

Այս վերջին սահմանումը բառացիորեն ծրագրավորելու դեպքում կունենանք հետևյալ ֆունկցիոնալ լուծումը․
(defun is-prime-b (num)
  (= 1 (list-length
 (remove-if 
  #'(lambda (e)
      (/= 0 (mod num e)))
  (loop for i from 1 to (ceiling (sqrt num))
        collect i)))))
Այս ֆունկցիան, չնայած որ կառուցվածքով բավականին հետաքրքիր է, արտաքնապես այնքան էլ գրավիչ չէ։ Փորձենք այն տրոհել ավելի պարզ ֆունկցիաների։

Վերջին սահմանումը հուշում է երեք գործողություն․ ա) 1..(sqrt N) միջակայքի ամբողջ թվերի ցուցակի կառուցում, բ) այդ ցուցակից N թվի բաժանարարների ֆիլտրում, գ) ֆիլտրված ցուցակի երկարության համեմատում 1 թվի հետ։

[a..b] միջակայքը, որտեղ a>b, կարող ենք կառուցել և՛ ռեկուրսիվ, և՛ իտերատիվ եղանակներով։ Ռեկուրսիվ տարբերակն ահա այսպիսինն է․
(defun range (lower upper &optional (delta 1))
  (if (> lower upper)
      '()
      (cons lower (range (+ lower delta) upper))))
Իտերատիվ տարբերակը էլ ավելի պարզ կառուցվածք ունի․
(defun range (lower upper &optional (delta 1))
  (loop for i from lower to upper by delta
        collect i))
Միջակայքը կազմող թվերի ցուցակը ֆիլտրելու և միայն տրված թվի բաժանարաները թողնելու համար սահմանենք divisors ֆունկցիան։ Ֆիլտրելու համար օգտագործված է Common Lisp լեզվի remove-if ֆունկցիան, որը տրված ցուցակից հեռացնում է տրված պրեդիկատին բավարարող տարրերը։
(defun divisors-a (num)
  (remove-if #'(lambda (e) (/= 0 (mod num e)))
             (range 1 (ceiling (sqrt num)))))
Եվ վերջապես, is-prime պրեդիկատը բաժանարարների ցուցակի երկարությունը համեմատում է 1-ի հետ։
(defun is-prime (num)
  (= 1 (list-length (divisors-a num))))

* * *
«N թիվը կոչվում է կատարյալ, եթե այն հավասար է իր բաժանարարների (բացի իրենից) գումարին։» Եթե թվի պարզ լինելը ստուգելու համար բաժանարարները որոնում էինք 1..(sqrt N) միջակայքում, ապա այս դեպքում պետք է ընտրել 1..N/2 միջակայքը։ Սահմանենք մի նոր divisors ֆունկցիա․
(defun divisors (num)
  (remove-if #'(lambda (e) (/= 0 (mod num e)))
             (range 1 (ceiling num 2))))
Թվի կատարյալ լինելն էլ ստուգելու համար սահմանենք is-perfect ֆունկցիան, որը գումարում է ընտրված բաժանարարները և ամեմատում թվի հետ։
(defun is-perfect (num)
  (= num (apply #'+ (divisors num))))
Այն դեպքում, երբ պետք է որոնել տրված M թվին չգերազանցող բոլոր կատարյալ թվերի ցուցակը, կարող ենք սահմանել perfects-in-range ֆունկցիան։
(defun perfects-in-range (upper)
  (remove-if (complement #'is-perfect) (range 2 upper)))