დათვლითი სორტირება

შედარებაზე დაფუძნებული სორტირებები n log n-ს ვერასდროს აჯობებს. დათვლითი სორტირება კანონიერად „ატყუებს“: როცა გასაღებები პატარა მთელი რიცხვებია, საერთოდ არ ადარებს და წრფივ დროში ალაგებს.

★ ლექცია: sorting_count · ზაზა გამეზარდაშვილი▶ ვიდეო საშუალო⏱ 8 წთ
01

ისწავლე

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

01დათვალე, ნუ შეადარებ

დავუშვათ, ყოველი მნიშვნელობა მთელი რიცხვია 0-დან k − 1-მდე, მაგალითად გამოცდის ქულები ან ასაკები. შექმენი k ზომის მასივი count, ყველა ნული. ერთხელ გაიარე შემავალი მონაცემები და ყოველი v-სთვის გააკეთე count[v]++.

[3, 6, 1, 3, 4, 1, 6, 3]-სთვის ეს იძლევა count = [0, 2, 0, 3, 1, 0, 2]: ორი 1, სამი 3, ერთი 4, ორი 6.

თუ მხოლოდ დალაგებული რიცხვები გჭირდება, უკვე მზად ხარ: თითო v ამოწერე count[v]-ჯერ. ორი მარტივი გავლა, O(n + k), და ელემენტებს შორის არცერთი შედარება.

შემავალიმონაცემები36134163count[v]: რამდენჯერ გვხვდება v00122033415062

02პრეფიქსული ჯამები პოზიციებს იძლევა (და სტაბილურობას)

ჩვეულებრივ ჩანაწერებს ალაგებ გასაღებით (ადამიანებს ასაკით), ამიტომ პოზიციები გჭირდება და არა მხოლოდ რაოდენობები.

count პრეფიქსულ ჯამად აქციე: count[v] ხდება „რამდენი გასაღებია ≤ v“. მაშინ v გასაღების ელემენტები გამოსავლის უჯრებს იკავებს count[v−1]-დან count[v] − 1-მდე. მაგალითში 3-ები 2, 3 და 4 უჯრებს იკავებს.

ახლა შემავალი მონაცემები მარჯვნიდან მარცხნივ გაიარე: v გასაღების ყოველი ელემენტისთვის შეამცირე count[v] და ელემენტი ჩასვი out[count[v]]-ში. მარჯვნიდან მარცხნივ გავლა ბოლო 3-ს ბოლო 3-ის უჯრაში სვამს, ამიტომ ტოლი გასაღებები შემავალი მონაცემების რიგს ინარჩუნებს: დათვლითი სორტირება სტაბილურია.

სწორედ ეს სტაბილურობა ამუშავებს თანრიგებით სორტირებას (radix sort): რიცხვები ჯერ ბოლო ციფრით დაალაგე, მერე წინა ციფრით და ა.შ., ყოველ ჯერზე სტაბილური დათვლითი სორტირებით.

count00210233140526პრეფიქსული ჯამი: რამდენია ≤ v0225668გამოსავალი: 3-ები იკავებს უჯრებს 2..41011323334456667

03როგორ ჯობნის n log n-ს და როდის არა

ნებისმიერ სორტირებას, რომელიც რიგის შესახებ მხოლოდ ორი ელემენტის შედარებით იგებს, უარეს შემთხვევაში დაახლოებით n log n შედარება სჭირდება. შესაძლო რიგი n!-ია, და ყოველი კი/არა შედარება კანდიდატებს საუკეთესო შემთხვევაში ანახევრებს, ამიტომ საჭიროა log₂(n!) ≈ n log n კითხვა.

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

ფასი არის k წევრი O(n + k) დროსა და მეხსიერებაში. ქულები 0..100 ან ასაკები 0..120 იდეალურია. 10⁹-მდე მნიშვნელობები, ტელეფონის ნომრები ან სტრიქონები არა: count მასივი უზარმაზარი იქნებოდა. მაშინ გამოიყენე ჩვეულებრივი O(n log n) სორტირება, ან radix sort, თუ გასაღებები ფიქსირებული ზომის მთელებია. სასარგებლო ხრიკი: თუ მნიშვნელობები დიდია, მაგრამ ცოტაა, ჯერ შეკუმშე ისინი (დაალაგე განსხვავებული მნიშვნელობები და თითო მისი რანგით შეცვალე).

კარგიაასაკები 0..120ქულები 0..100k პატარაა → O(n + k)ცუდიატელეფონები 0..10¹⁰ნამდვილი რიცხვებიk დიდია → O(n log n)

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

დროO(n + k)
დამატებითი მეხსიერებაO(n + k)
Radix sort, d ციფრიO(d · (n + base))
შედარებითი სორტირების ქვედა ზღვარიΩ(n log n)

დაიმახსოვრე

  1. დათვლითი სორტირება ყოველ გასაღებს count[]-ში ითვლის, ამიტომ პატარა მთელ გასაღებებს შედარების გარეშე O(n + k)-ში ალაგებს.
  2. რაოდენობების პრეფიქსული ჯამები ყოველ გასაღებს გამოსავლის უჯრებს აძლევს, მარჯვნიდან მარცხნივ გავლა კი სტაბილურობას ინარჩუნებს.
  3. ის მხოლოდ მაშინ ამართლებს, როცა გასაღებების დიაპაზონი k n-ზე ბევრად დიდი არ არის.
02

უყურე

ლექცია ვიდეოს სახით.

▶

ლექცია, ანიმაციით

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

03

ითამაშე

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

👀 რას უყურო: სამი ფაზა: დათვლა, პრეფიქსული ჯამი, განთავსება. ბოლო ფაზაში შენიშნე, რომ სკანირება მარჯვნიდან მარცხნივ მიდის და ყოველი count ერთით მცირდება.

ინტერაქტიული ვიზუალიზატორიაქ დააწკაპე, მერე space ← →
✎ შენი მონაცემებიმაქს. 12 რიცხვი, თითო 0–9. გამეორებებია საინტერესო.

შემავალი მასივი

3
6
1
3
4
1
6
3

count[] (მნიშვნელობა 0..6)

v0123456
count0000000

გამომავალი

········
დათვლითი სორტირება 8 მნიშვნელობისა შუალედში 0..6. ის არასდროს ადარებს ორ ელემენტს, ის მათ ითვლის.

ფსევდოკოდი

 1 count occurrences of each value 0..k-1 2 prefix-sum count[] → count[v] = #(≤ v) 3 scan input right→left: out[--count[v]] = v 4 // zero comparisons between elements
1 / 1
04

შეამოწმე

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

№1

შემავალი მონაცემები: [2, 0, 2, 1, 0, 2]. როგორია count[] (მნიშვნელობები 0..2) დათვლის გავლის შემდეგ?

№2

უნდა დაალაგო 10⁶ მომხმარებლის id, თითო 10¹⁸-მდე. კარგი იდეაა დათვლითი სორტირება?

№3

რატომ მიდის განთავსების გავლა მარჯვნიდან მარცხნივ?

05

ივარჯიშე

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