ჰეშ-ცხრილები: set და map

„ეს უკვე მინახავს?“ და „რამდენჯერ?“ რეალურ კოდში ყველაზე ხშირი კითხვებია. ჰეშ-ცხრილი ორივეს საშუალოდ O(1)-ში პასუხობს, ამიტომაც აქვს ის ყველა ენას ჩაშენებული.

დამწყები⏱ 14 წთ
01

ისწავლე

იდეა, მექანიზმი და ფასი.

01სიმრავლეები და ასახვები

ორი ყოველდღიური ინსტრუმენტი ჰეშ-ცხრილზეა აგებული:

  • სიმრავლე (set) უნიკალურ გასაღებებს ინახავს და პასუხობს კითხვას „x მასშია?“. C++ unordered_set, Python set, Java HashSet.
  • ასახვა (map, ლექსიკონი) ინახავს წყვილებს გასაღები → მნიშვნელობა: სიტყვა → რაოდენობა, მომხმარებლის id → პროფილი. C++ unordered_map, Python dict, Java HashMap.

იდეა: ჰეშ-ფუნქცია h(k) ნებისმიერ გასაღებს, რიცხვს თუ სტრიქონს, კალათების მასივის ინდექსად აქცევს. ჩასასმელად გამოთვალე h(k) და გასაღები იქ ჩადე. საძებნელად ისევ გამოთვალე h(k) და მხოლოდ იმ ერთ კალათაში ჩაიხედე. არც სკანირება, არც სორტირება: გასაღები თავად გეუბნება, სად ცხოვრობს.

გასაღებიჰეშ-ფუნქციაკალათებიh(k)"apple""kiwi""plum"01"apple": 3234"kiwi": 756"plum": 2

02კოლიზიები და ჯაჭვები

შესაძლო გასაღებები კალათებზე ბევრად მეტია, ამიტომ ორი გასაღები ზოგჯერ ერთ კალათაში მოხვდება. ეს კოლიზიაა, და ის ჩვეულებრივი მოვლენაა და არა შეცდომა.

უმარტივესი გამოსავალია ჯაჭვები (chaining): ყოველი კალათა პატარა სიას ინახავს. h(k) = k mod 7-ით გასაღებები 15, 8 და 22 სამივე 1-ს იძლევა, ამიტომ კალათა 1-ში სამელემენტიანი ჯაჭვია. ძებნა კალათას ჰეშით პოულობს და მერე გასაღებებს მხოლოდ ამ ჯაჭვის გასწვრივ ადარებს.

მეორე კლასიკური მიდგომაა ღია მისამართება (open addressing): თუ კალათა დაკავებულია, შეამოწმე შემდეგი (და შემდეგი), სანამ თავისუფალს არ იპოვი. ორივე შემთხვევაში ოპერაციის სამუშაო დაახლოებით ერთი ჯაჭვის სიგრძეა, ამიტომ გვინდა, რომ ჯაჭვები მოკლე დარჩეს.

h(k) = k mod 70∅1158222∅3∅4115∅627კოლიზია15, 8, 22 → 1

03დატვირთვის ფაქტორი და რეჰეშირება

დატვირთვის ფაქტორი α = (გასაღებების რაოდენობა) / (კალათების რაოდენობა) ჯაჭვის საშუალო სიგრძეა. კარგი ჰეშ-ფუნქციით, რომელიც გასაღებებს თანაბრად ანაწილებს, ყოველი ოპერაცია დაახლოებით O(1 + α) ღირს.

ამიტომ ცხრილი α-ს შემოსაზღვრულს ინარჩუნებს. როცა ის ზღვარს გადასცდება (unordered_map-ისთვის დაახლოებით 1, Python-ის dict-ისთვის 2/3), ხდება რეჰეშირება: გამოიყოფა დაახლოებით ორჯერ მეტი კალათა და ყველა გასაღები თავიდან ჩაისმება, რადგან h(k) კალათების რაოდენობაზეა დამოკიდებული.

ერთი რეჰეშირება O(n)-ია, მაგრამ ის მხოლოდ ცხრილის გაორმაგების შემდეგ ხდება, ზუსტად ისე, როგორც მზარდ ვექტორში. შედეგი: ჩასმა, ძებნა და წაშლა მოსალოდნელი O(1)-ია, ყველა ოპერაციაზე ამორტიზებით.

α = 6 / 4 = 1.5გრძელი ჯაჭვები04812112337რეჰეშირებაα = 6 / 8 = 0.75მოკლე ჯაჭვები081123344125677

04როდის გამოვიყენოთ და სად არის ხაფანგები

ჰეშ-სიმრავლე ან ასახვა გამოიყენე, როცა გჭირდება კუთვნილება, დათვლა ან დაჯგუფება: დუბლიკატების აღმოჩენა, სიტყვების სიხშირის დათვლა, მოცემული ჯამის მქონე წყვილის პოვნა (შეინახე, რაც უკვე ნახე), ანაგრამების დაჯგუფება დალაგებული გასაღებით.

იცოდე საზღვრები:

  • უარესი შემთხვევა O(n)-ია: თუ ბევრი გასაღები კოლიზირდება, ჯაჭვი ჩვეულებრივ სიად იქცევა. ოლიმპიადების საიტებზე სპეციალური ტესტები unordered_map-ს ამას განზრახ უკეთებს; რანდომიზებული ჰეში იცავს ამისგან.
  • რიგი არ არსებობს: გავლის რიგი ნებისმიერია. თუ დალაგებული გასაღებები, მინ/მაქს ან „შემდეგი დიდი გასაღები“ გჭირდება, გამოიყენე დალაგებული map/set (დაბალანსებული ხე, O(log n)).
  • გასაღები ჰეშირებადი უნდა იყოს და არ უნდა შეიცვალოს შენახვის დროს. სიმრავლეში მყოფი გასაღების შეცვლა მას კარგავს.

ფასი ერთი შეხედვით

ჩასმა / ძებნა / წაშლა (მოსალოდნელი)O(1)
უარესი (ყველა კოლიზიაში)O(n)
რეჰეშირებაO(n), rare
სანაცვლოდ დალაგებული map / setO(log n)

დაიმახსოვრე

  1. ჰეშ-ფუნქცია ყოველ გასაღებს პირდაპირ კალათაში აგზავნის, ამიტომ ძებნას სკანირება არ სჭირდება.
  2. კოლიზიები ჩვეულებრივია; ჯაჭვები მათ სწორად ამუშავებს, შემოსაზღვრული დატვირთვის ფაქტორი კი მოკლედ ინარჩუნებს.
  3. ჰეშ-ცხრილი მოსალოდნელ O(1)-ს იძლევა, მაგრამ რიგს არა; როცა რიგი მნიშვნელოვანია, ხეზე დაფუძნებული map გამოიყენე.
02

ითამაშე

გაიარე ალგორითმი ბიჯ-ბიჯ, მერე სცადე შენს მონაცემებზე.

👀 რას უყურო: უყურე, როგორ იზრდება ჯაჭვი კალათა 1-ში, მერე შეადარე ძებნა, რომელიც ჯაჭვს გაივლის, და ძებნა, რომელიც ცარიელ კალათას ხვდება. სცადე 2 კალათა გრძელი ჯაჭვებისთვის.

ინტერაქტიული ვიზუალიზატორიაქ დააწკაპე, მერე space ← →
✎ შენი მონაცემებიმაქს. 10 გასაღები (0–999), 2–11 კალათა. ცოტა კალათა = გრძელი ჯაჭვები.

ჰეშ-ცხრილი: h(k) = k mod 7

[0]∅
[1]∅
[2]∅
[3]∅
[4]∅
[5]∅
[6]∅
ჰეშ-ცხრილი გასაღებებს 7 კალათაზე ასახავს h(k) = k mod 7-ით. იდეალურად ყოველი ძებნა მხოლოდ ერთ კალათას ეხება.

ფსევდოკოდი

 1 table of B buckets; h(k) = k mod B 2 insert k: append to bucket h(k) (chaining on collision) 3 find k: hash to bucket h(k), then scan its chain 4   compare each key until found or chain ends 5 // expected O(1) while the load factor stays bounded
1 / 1
03

შეამოწმე

სამი კითხვა. აირჩიე პასუხი და ნახე ახსნა.

№1

7 კალათითა და h(k) = k mod 7-ით, რომელ კალათაში მოხვდება გასაღები 30?

№2

უნდა შეამოწმო, n რიცხვიდან რომელიმე ორის ჯამი უდრის თუ არა მიზანს. რომელია ტიპურად ყველაზე სწრაფი მიდგომა?

№3

ბევრჯერ გჭირდება x-ზე მეტი უმცირესი გასაღები. რომელი სტრუქტურა ჯდება?

04

ივარჯიშე

რეალური ამოცანები გასამყარებლად, მარტივიდან რთულისკენ.