CodeGym /Kurslar /C++ SELF /Məlumatların indeksləşdirilməsi: id → obyekt, tag → siyah...

Məlumatların indeksləşdirilməsi: id → obyekt, tag → siyahı

C++ SELF
Səviyyə, Dərs
Mövcuddur

1. Məlumat indeksləri

Tutaq ki, siz tapşırıqları std::vector<Task> daxilində saxlayırsınız və hər dəfə tapşırığı id üzrə tapmaq üçün vektoru dövr edirsiniz. Bu işləyir… elə o vaxtadək ki, tapşırıqlar çoxalmır və sorğular daha da artır. İndeks— açar üzrə tipik sorğunu sürətləndirən məlumat strukturu. Real inkişafda məhz indekslər tətbiqləri "çevik" edir, amma eyni zamanda yeni baş ağrısı gətirir: onları uyğun vəziyyətdə saxlamaq lazımdır.

Gündəlik analoq olaraq sadə bir misal verək. Kitabxanada kitablar rəflərdədir (bunlar "həqiqi obyektlər"), və həmçinin müəlliflər üzrə kataloq və adlar üzrə kataloq var (bunlar "indekslər"). Əgər kitabxanaçı kitabı rəfə qoyub, amma kataloqa daxil etməyi unutsa—oxucu kitabın olmadığını düşünəcək. Əksinə, kataloqda qeyd var, amma rəfdə kitab yoxdursa—oxucu "xəyal" axtaracaq. Bu indekslərin uyğunsuzluğu deməkdir.

Mini məlumat modeli: Task və "həqiqət mənbəyi"

Məsələdən konkret danışmaq üçün kiçik bir model yaradaq. Gəlin bizim sadə tətbiqimiz TaskBook olsun: tapşırıqları saxlayırıq, hər birinin ədədi id, mətn title və tag dəsti var (məsələn, "cpp", "study", "home"). Biz mürəkkəb arxitektura istifadəsinə getmirik: klasslar və "sehr" yox, yalnız struct və STL konteynerləri, olduğumuz kimi.

Əvvəlcədən bir prinsipə əməl etmək vacibdir: bir həqiqət mənbəyi. Bu o deməkdir ki, həqiqi Task obyektini bir yerdə saxlayırıq, indeksdə isə yalnız istinad məlumatı saxlanılır (məsələn, id). Beləliklə, tapşırıq dəyişdikdə iki nüsxəni sinxronlaşdırmağa ehtiyac yoxdur, yalnız ona istinad edən indeksləri yeniləmək kifayətdir.

Modelə baxaq:

#include <string>
#include <vector>

struct Task {
    int id{};
    std::string title;
    std::vector<std::string> tags;
};

Burada tags məhz vector-dur, çünki çap etmək və iterasiya üçün rahatdır. Tagların unikalığını ayrıca təmin edəcəyik (biraz sonra), beləliklə eyni tagı bir neçə dəfə saxlamayacağıq — bəli, bu istifadəçi tagları əl ilə daxil etdikdə baş verə bilər.

2. İndekslər: ilkin və ikinci dərəcəli

İlkin indeks id → Task: obyektə sürətli giriş

İlkin indeks — "həqiqi" obyektlərin yerləşdiyi yerdir. Ən anlaşılır variant: std::unordered_map<int, Task> tasksById; Beləliklə, id üzrə tapşırığı sürətlə və xətti axtarışsız əldə edirik. Biz burada hash-cədvəlin daxili işləməsini və ideal mürəkkəblikləri müzakirə etmirik — vacib odur ki, əməliyyatın mənası "hamısını keçmək"dən "açar üzrə tapmaq"a dəyişir.

Tipler təyin edək (kod oxunması yaxşılaşır və gözlər daha az yorulur):

#include <unordered_map>

using TaskId = int;
using TaskById = std::unordered_map<TaskId, Task>;

İndi oxumaq üçün əsas nümunəni göstərək: find → yoxla → istifadə et. Qeyd edin: bu oxuma yan təsirlərsizdir (heç nə təsadüfən yaratmırıq).

#include <iostream>
#include <unordered_map>

int main() {
    TaskById tasks;
    tasks.emplace(1, Task{1, "Learn C++", {"cpp", "study"}});

    if (auto it = tasks.find(1); it != tasks.end()) {
        std::cout << it->second.title << '\n'; // Learn C++
    }
}

Və ayrıca vacib məqam: əgər oxumaq istəyirsinizsə — find/contains istifadə edin. operator[]-dən yalnız yaratmaq və ya yeniləmək istədiyiniz zaman istifadə edin.

İkinci dərəcəli indeks tag → siyahı id: kateqoriya üzrə sürətli axtarış

İndi ikinci növ sorğunu edək: "cpp" taglı bütün tapşırıqları göstər. Yalnız tasksById olsa, bütün tapşırıqları yoxlayıb Task::tags daxilində lazımlı tagın olub-olmadığını yoxlamalıyıq. Bu yenə bütün tapşırıqlarda xətti gedişdir və böyük məlumatlarda pis performans verəcək.

Buna görə ikinci dərəcəli indeks təqdim edirik: tag → id siyahısı. Niyə tag → Task deyil? Çünki bir həqiqət mənbəyi prinsipinə əməl etdik: Task tasksById-də yaşayır, indeks isə yalnız ona istinad olaraq id saxlayır.

"id siyahısı" üçün konteyner seçimi tələblərə bağlıdır. Tədris nümunəsində std::unordered_set<int> götürmək rahatdır: o təkrarlananları avtomatik aradan qaldırır və "id dəsti daxilindədirmi" yoxlaması sürətlidir. Əgər sıra və ya təkrarlara ehtiyac olsa idi — std::vector<int> seçmək olardı, amma onda təkrarlardan əl ilə təmizləmək lazım olacaq. Başlanğıc üçün "etibarlı" variant götürürük.

#include <string>
#include <unordered_map>
#include <unordered_set>

using Tag = std::string;
using IdSet = std::unordered_set<TaskId>;
using TagIndex = std::unordered_map<Tag, IdSet>;

İndi tətbiqimizin məlumat strukturları konseptual olaraq belədir:

flowchart LR
    subgraph Primary["İlkin indeks (həqiqət mənbəyi)"]
        A["tasksById: id → Task"]
    end

    subgraph Secondary["İkinci dərəcəli indeks (istinadlar)"]
        B["tagToIds: tag → {id, id, id}"]
    end

    B -->|"id"| A

Psixoloji olaraq faydalıdır ki, ikinci indeksdə biz tapşırıq saxlamırıq, ona yalnız id ilə "istisna" edirik. Bu kataloq kartı kimidir, kitab yox.

4. Uyğunsuzluq olmadan əməliyyatlar

İkinci indeks yaranınca, hər "obyekt yarat" əməliyyatı "obyekt yarat + indeksləri yenilə" olur. Və bu vaxt tipik səhvlər başlayır: tələbə tapşırığı tasksById-ə əlavə edir, amma id-ni tagToIds-ə əlavə etməyi unudur və sonra niyə tag üzrə axtarış heç nə tapmır deyə təəccüblənir.

Belə səhvlərin yayılmaması üçün sadə intizam tətbiq edirik: bütün məlumat dəyişiklikləri bir funksiya vasitəsilə keçir. Gəlin bu funksiyanı AddTask(...) adlandıraq. Bəli, bu darıxdırıcı səslənir. Amma darıxdırıcı kod adətən işləyir, əyləncəli kod isə çox vaxt yalnız kompilyatora əyləncə verir.

İlk olaraq tag təkrarlılığını aradan qaldıran kiçik funksiya hazırlayaq. Bunu müvəqqəti olaraq unordered_set-lə edərik, sonra vector-a qaytararıq. Kod kiçikdir, amma məqsəd əhəmiyyətlidir: Task daxilində taglar nizamlı olacaq.

#include <string>
#include <unordered_set>
#include <vector>

std::vector<std::string> MakeUniqueTags(const std::vector<std::string>& tags) {
    std::unordered_set<std::string> uniq(tags.begin(), tags.end());
    return std::vector<std::string>(uniq.begin(), uniq.end());
}

Uyğun əlavə: hər iki indeksi yeniləyirik

Qeyd edin: əvvəlcə tasksById-ə daxil etməyə çalışırıq. Əgər id artıq işğal olunubsa — çıxırıq, heç nə dəyişmirik. Əks halda—daxiletmə uğurlu olarsa—tagToIds-i yeniləyirik. Burada operator[] bilincli şəkildə istifadə olunur: əgər tag hələ yoxdursa, onun üçün boş dəst yaratmaq istəyirik.

#include <unordered_map>
#include <unordered_set>
#include <string>
#include <vector>

bool AddTask(TaskById& tasksById, TagIndex& tagToIds, Task task) {
    task.tags = MakeUniqueTags(task.tags);

    auto [it, inserted] = tasksById.emplace(task.id, task);
    if (!inserted) return false;

    for (const std::string& tag : it->second.tags) {
        tagToIds[tag].insert(it->first);
    }
    return true;
}

Gözəl nəticə budur: çağıran kodun heç nəyə fikir verməsinə ehtiyac yoxdur ki, "iki indeksimiz var". O sadəcə AddTask-i çağırır, funksiya isə bütün darıxdırıcı işi görür. Bu böyük sözlərə ehtiyac olmadan yaxşı arxitekturaya kiçik addımdır.

Uyğun silmə: "xəyal" qalmamaq üçün silirik

Silmə əlavə etmədən daha təhlükəlidir. Çünki səhv adətən belə görünür: tapşırığı tasksById-dən sildiniz, amma onun id-ni tagToIds-dən çıxarmağı unutdunuz. Sonra tag üzrə göstərdiyiniz zaman id alırsınız, amma ilkin indeksi görəndə tapşırıq yoxdur. Bu zaman "xəyal tapşırıq" ilə qarşılaşırsınız: indeks deyir var, reallıq isə yox.

Silmə iki addımdan ibarət olmalıdır: əvvəlcə ikinci indekslərdən istinadları çıxarın, sonra özü obyekti silin. Sıra məhz belədir: əvvəl kataloqları təmizləyin, sonra kitabı rəfdən çıxarın. Yenə də bu əməliyyatı bir funksiyaya yığırıq ki, kodun müxtəlif yerlərində təkrarlanmasın.

#include <string>
#include <unordered_map>
#include <unordered_set>

bool RemoveTask(TaskById& tasksById, TagIndex& tagToIds, TaskId id) {
    auto it = tasksById.find(id);
    if (it == tasksById.end()) return false;

    for (const std::string& tag : it->second.tags) {
        if (auto jt = tagToIds.find(tag); jt != tagToIds.end()) {
            jt->second.erase(id);
            if (jt->second.empty()) tagToIds.erase(jt);
        }
    }
    tasksById.erase(it);
    return true;
}

Burada bir estetik nüans var: əgər id silindikdən sonra dəst boşdursa, biz həmin tagı indeksdən çıxarırıq. Bu mütləq deyil, amma strukturu daha təmiz saxlayır: "tapşırığı olmayan taglar" olmaz.

Məlumatların dəyişdirilməsi: tag və başlığı necə təhlükəsiz dəyişmək

Dəyişdirmə indekslərin xüsusilə asinxronlaşmağa meylli olduğu hissədir. Başlıq (title) problemsiz dəyişdirilə bilər: o indeskdə yer almır (hələ). Amma taglar ikinci dərəcəli indeksin əsasıdır. Əgər Task::tags-ı dəyişdirirsinizsə, amma tagToIds-i yeniləmirsinizsə, ikinci indeks dərhal "yanlış" olacaq.

Tagların düzgün yenilənməsi belədir: əvvəlcə id-ni köhnə taglardan çıxarmalıyıq, sonra id-ni yeni taglara əlavə etməliyik və yalnız sonra yeni tagları Task-a yazmalıyıq. Əslində bu biri dəstdən digərinə "köçdürmə"dir. Yenə də bu məntiqi ayrılmış funksiyaya yığıb tətbiq boyu səpələnməsinin qarşısını alırıq.

#include <string>
#include <vector>

bool UpdateTaskTags(TaskById& tasksById, TagIndex& tagToIds,
                    TaskId id, std::vector<std::string> newTags) {
    auto it = tasksById.find(id);
    if (it == tasksById.end()) return false;

    for (const std::string& tag : it->second.tags) {
        if (auto jt = tagToIds.find(tag); jt != tagToIds.end()) {
            jt->second.erase(id);
            if (jt->second.empty()) tagToIds.erase(jt);
        }
    }

    newTags = MakeUniqueTags(newTags);
    it->second.tags = newTags;

    for (const std::string& tag : it->second.tags) {
        tagToIds[tag].insert(id);
    }
    return true;
}

Əgər indi düşündünüzsə: "Vay, taglar üçün bu qədər kod..." — bu normal reaksiya. İndekslər sorğu sürətini verir, amma dəyişiklik zamanı nizam tələb edir. Böyük sistemlərdə bu nizam arxitektura və intizamla təmin edilir; bizdə isə funksiyalar bu işi görür.

5. Sorğular və yoxlama

İndekslər gözə görə deyil, sorğular üçün qurulur. Gəlin iki kiçik çap funksiyası yaradaq: biri id üzrə tək tapşırığı çap edir, digəri tag üzrə bütün tapşırıqları. Razılaşaq: biz mürəkkəb "gözəl çıxış" qurmuruq, məqsəd indekslərin necə istifadə olunduğunu göstərməkdir.

Sorğular: "id üzrə göstər" və "tag üzrə göstər"

Tək tapşırığın çapı:

#include <iostream>

void PrintTask(const Task& t) {
    std::cout << "#" << t.id << " " << t.title << " [";
    for (std::size_t i = 0; i < t.tags.size(); ++i) {
        std::cout << t.tags[i] << (i + 1 == t.tags.size() ? "" : ", ");
    }
    std::cout << "]\n";
}

"id üzrə göstər" sorğusu sadəcə ilkin indeksdə find etməkdir:

#include <iostream>

void PrintById(const TaskById& tasksById, TaskId id) {
    if (auto it = tasksById.find(id); it != tasksById.end()) {
        PrintTask(it->second);
    } else {
        std::cout << "no such task\n"; // no such task
    }
}

"Tag üzrə göstər" sorğusu isə ikinci indeksdən istifadə edir. Qeyd edin: ikinci indeks bizə id dəstini verir, sonra hər id üçün tasksById-ə müraciət edib obyekt götürürük. Bu "bir həqiqət mənbəyi" prinsiplərini göstərir.

#include <iostream>
#include <string>

void PrintByTag(const TaskById& tasksById, const TagIndex& tagToIds,
                const std::string& tag) {
    auto it = tagToIds.find(tag);
    if (it == tagToIds.end()) {
        std::cout << "no such tag\n"; // no such tag
        return;
    }

    for (TaskId id : it->second) {
        if (auto jt = tasksById.find(id); jt != tasksById.end()) {
            PrintTask(jt->second);
        }
    }
}

"Paranoya"ya diqqət yetirin: indeks uyğun olmalıdır desək də, biz yenə də tasksById.find(id)-i yoxlayırıq. İdeal dünyada buna ehtiyac olmaya bilər, amma tədris kodunda bu yoxlama sinxronizasiya səhvini aşkar etməyə və proqramın qəfil çökməsinin qarşısını almağa kömək edir.

Uyğunluğun yoxlanması: invariantlar və sadə validasiya

İndekslər bir neçə olduqda "dəyişməz qaydalar"ı müəyyənləşdirmək faydalıdır. Proqramlaşdırmada bu cür qaydalar tez-tez invariant adlanır. Rəsmi riyaziyyat olmadan, sadə dildə:

Əgər tagToIds["cpp"]-də 42 varsa, onda tasksById-də id-si 42 olan tapşırıq mütləq olmalıdır. Və əksinə: əgər tapşırıq 42 tag "cpp" saxlayırsa, tagToIds["cpp"] də 42-ni içərməlidir.

Tədris layihələrində əməliyyatlardan sonra çağırıla bilən sadə bir validator çox kömək edir və indekslərin "qaçıb-qaçmadığını" yoxlamağa imkan verir. Bəli, bu əlavə xəttdir, amma lampa kimidir: gedişi sürətləndirmir, amma ağaca dəyib yıxılmağı önləyir.

İkinci dərəcədən ilkinə yoxlama (praktikada əksər səhvləri tutur) nümunəsi:

#include <iostream>
#include <string>

bool ValidateTagIndex(const TaskById& tasksById, const TagIndex& tagToIds) {
    for (const auto& [tag, ids] : tagToIds) {
        for (TaskId id : ids) {
            if (!tasksById.contains(id)) {
                std::cout << "Broken index: tag '" << tag
                          << "' refers to missing id=" << id << '\n';
                return false;
            }
        }
    }
    return true;
}

İstəsəniz, əks yoxlama (ilkin dərəcədən ikinci dərəcəliyə) də əlavə etmək olar, amma tək yoxlama da intizam yaradır: indeksləri saxlamağın vacib olduğunu hiss edirsiniz, onlar "sehrli optimizasiya" deyil.

6. Bir neçə indekslə işləyərkən tipik səhvlər

Bu mövzuda səhvlərin çoxu "sintaktik" deyil, arxitekturadır: kod kompilyasiya olunur, bəzən işləyir, amma məlumatlar yavaş-yavaş detektiv hekayəsinə çevrilir. Buna görə də səhvləri simptomlar və vərdişlər üzrə tanımaq daha faydalıdır.

Səhv №1: iki həqiqət mənbəyi (obyektlərin indekslərdə dublikasiyası).
Tez-tez rahatlıq üçün tag → vector<Task> etmək istəyirlər ki, "çap etmək rahat olsun". İlk yarım saat həqiqətən rahatdır. Sonra siz title-ı tasksById-də dəyişirsiniz, ikinci yerdəki nüsxəni yeniləməyi unutursunuz — və birdən iki fərqli reallıq yaranır. Obyekti bir yerdə saxlamaq və indekslərdə yalnız id saxlamaq daha etibarlıdır.

Səhv №2: bir indeksi yeniləmisiniz, ikinciyi unutmusunuz.
Bu janrın klassikası: tapşırığı tasksById-ə əlavə etdiniz, amma id-ni tagToIds-ə daxil etmədiniz; ya da tapşırığı tasksById-dən sildiniz, amma tagToIds-dən təmizləmədiniz. Simptomlar sadədir: "id üzrə tapılır, tag üzrə tapılmır" və ya "tag üzrə id var, amma id üzrə tapşırıq yoxdur". Çözüm intizamdır: bütün əməliyyatlar AddTask/RemoveTask/UpdateTaskTags kimi funksiyalar vasitəsilə həyata keçirilsin.

Səhv №3: oxuma zamanı operator[]-dən istifadə edib təsadüfən boş qeydlər yaradırsınız.
Məsələn, tagToIds[tag] oxu rejimində yeni tag üçün boş dəst yaradacaq və sonra bu qəribə boş tagların haradan gəldiyini düşünəcəksiniz. Oxuyursunuzsa — find/contains-dən istifadə edin. operator[]-i yalnız yoxdursa yaratmaq istədiyiniz hallarda saxlayın (məsələn, AddTask).

Səhv №4: ikinci dərəcəli indeksdə "boş" açarları təmizləmirsiniz.
Əgər tagToIds[tag]-dən id çıxarılandan sonra dəst boşalır və siz boş qeydi saxlayırsınızsa—loqika ümumiyyətlə pozulmur, amma məlumat strukturu "çirklənir": istifadəçiyə göstərilən tag siyahısında tapşırığı olmayan taglar görünəcək. Boş açarların təmizlənməsi davranışı daha təmiz və proqnozlaşdırılan edir.

Səhv №5: Task içindəki taglar təkrarlanır və indeks qəribə işləyir.
Əgər tapşırığın tagları {"cpp", "cpp"} kimi olsa, indeksləri yeniləyərkən artıq iş görmək olar, və çap zamanı istifadəçi təkrarlananları görüb şübhələnə bilər. Tagların normallaşdırılması (ən azı təkrarlardan azad etmə) kiçik bir məqamdır, amma məlumat keyfiyyətini yüksəldir və "sehrli" səhvlərin sayını azaltmağa kömək edir.

1
Tapşırıq
C++ SELF, səviyyə, dərs
Bağlanıb
Tapşırıqlar indeksi
Tapşırıqlar indeksi
1
Tapşırıq
C++ SELF, səviyyə, dərs
Bağlanıb
Tag üzrə axtarış
Tag üzrə axtarış
1
Tapşırıq
C++ SELF, səviyyə, dərs
Bağlanıb
Iki indeks
Iki indeks
1
Tapşırıq
C++ SELF, səviyyə, dərs
Bağlanıb
Baza xidməti
Baza xidməti
1
Sorğu/viktorina
, səviyyə, dərs
Əlçatan deyil
map/unordered_map/set
map/unordered_map/set
Şərhlər
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION