კომპარატორები და სტაბილურობა

რეალურ კოდში სორტირებას თითქმის არასდროს წერ; წერ წესს. თუ იცი, როგორ მუშაობს კომპარატორები და სტაბილურობა, „დაალაგე თარიღით, მერე სახელით“ ერთ სწორ ხაზად იქცევა.

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

ისწავლე

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

01კომპარატორი თავად არის რიგი

std::sort(v.begin(), v.end(), comp) უშვებს O(n log n) ალგორითმს (introsort) და შენს მონაცემზე ერთადერთი, რაც იცის, შენი ფუნქცია comp(a, b)-ა, რომელიც პასუხობს: a უნდა იყოს b-ზე წინ?

ესე იგი ნებისმიერი წესით შეგიძლია დაალაგო:

  • კლებადობით: return a > b;
  • ველით: return a.score > b.score;
  • რამდენიმე გასაღებით: შეადარე პირველი გასაღები; მხოლოდ ტოლობისას შეადარე შემდეგი

სხვა ენებში სხვანაირად გამოიყურება, მაგრამ ასევე მუშაობს: Python sorted(v, key=lambda p: (-p.score, p.name)), Java Comparator.comparing(...). გასაღების ფუნქცია, რომელიც კორტეჟს აბრუნებს, ხშირად ყველაზე მარტივი გზაა მრავალგასაღებიანი რიგის სწორად მისაღებად.

comp(a, b): if (a.score != b.score)return a.score > b.score; return a.name < b.name;ადრეGio80Ana95Eka80Luka70sort(v, comp)-ის შემდეგAna95Eka80Gio80Luka70

02სტაბილურობა

სორტირება სტაბილურია, თუ ტოლი გასაღების ელემენტები ერთმანეთის მიმართ თავდაპირველ რიგს ინარჩუნებს.

რატომ გვაინტერესებს? ვთქვათ, სია უკვე სახელით არის დალაგებული და ახლა ნიშნით ალაგებ. სტაბილური სორტირებით ერთნაირი ნიშნის მქონე სტუდენტები სახელის რიგში რჩებიან, უფასოდ. არასტაბილურით ნებისმიერ რიგში შეიძლება გამოვიდნენ.

  • std::sort არ არის სტაბილური (სწრაფ სორტირებაზეა აგებული).
  • std::stable_sort სტაბილურია (შერწყმაზეა აგებული), ოდნავ ნელია და შეიძლება O(n) მეხსიერება გამოიყენოს.
  • Python-ის sort და Java-ს ობიექტების სორტირება ყოველთვის სტაბილურია.

აქედან კლასიკური ხრიკი: A გასაღებით და მერე B-თი დასალაგებლად სტაბილურად დაალაგე ჯერ B-თი, მერე A-თი. ან უბრალოდ ერთი კომპარატორი გამოიყენე (A, B)-ზე.

შემავალი მონაცემები (სახელით დალაგებული)AnaBEkaBGioALukaAვალაგებთ ნიშნითსტაბილური: Ana კვლავ Eka-ს წინააGioALukaAAnaBEkaBარასტაბილური: ტოლების რიგი შეიძლება აირიოსLukaAGioAEkaBAnaB?

03წესი, რომელსაც კომპარატორი უნდა იცავდეს

კომპარატორი მკაცრი „ნაკლებია“-სავით უნდა იქცეოდეს (მკაცრი სუსტი რიგი):

  • comp(a, a) არის false: არაფერი დგას საკუთარ თავზე წინ
  • თუ comp(a, b) true-ა, comp(b, a) false უნდა იყოს
  • უნდა იყოს ტრანზიტული: a b-ზე წინ და b c-ზე წინ ნიშნავს a c-ზე წინ

კლასიკური შეცდომაა <-ის ნაცვლად <=-ის დაწერა. მაშინ comp(x, x) true-ა, ალგორითმის დაშვებები ირღვევა და std::sort შეიძლება ნაგავი დააბრუნოს ან მასივის ბოლოს იქით წაიკითხოს და ჩავარდეს. სხვა ხაფანგი: double-ების შედარება, რომლებიც შეიძლება NaN იყოს, ან „შემთხვევითი“ კომპარატორი არევისთვის.

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

✗ შეცდომა: ტოლებზე truereturn a.x <= b.x;✓ სწორი: მკაცრი „ნაკლებია“return a.x < b.x;comp(x, x) ყოველთვის false უნდა იყოს, თორემ sort შეიძლება ჩავარდეს

04სორტირება, როგორც პირველი ბიჯი

ბევრი ამოცანა სწორი კომპარატორით დალაგებისთანავე მარტივდება:

  • ხარბი არჩევანი: დაალაგე მოვლენები დასრულების დროით და ყოველ ჯერზე აიღე ყველაზე ადრე დამთავრებული თავსებადი (აქტივობების შერჩევა); ზურგჩანთის უწყვეტი ამოცანისთვის ნივთები დაალაგე კილოგრამზე ღირებულებით.
  • შუალედები: დაალაგე დასაწყისით, მერე გადაფარვები ერთი გავლით გააერთიანე.
  • მოცურავი ხაზი (sweep line): მოსვლები და წასვლები მოვლენებად აქციე, დაალაგე და გაიარე მთვლელით.
  • ორობით ძებნას და ორ მაჩვენებელს ორივეს დალაგებული მონაცემი სჭირდება.

ნიმუში ღირს O(n log n) სორტირებისთვის და ჩვეულებრივ O(n) გავლისთვის. როცა ამოცანა ყველა წყვილს ითხოვს, ჯერ იკითხე: რომელიმე გასაღებით დალაგება პასუხს რიგზე ხომ არ გამოაჩენს?

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

std::sortO(n log n)
std::stable_sortO(n log n)
კომპარატორის გამოძახებები≈ n log n
სორტირება + ერთი გავლაO(n log n)

დაიმახსოვრე

  1. კომპარატორი პასუხობს „a უნდა იყოს b-ზე წინ?“; რამდენიმე ველით დასალაგებლად გასაღებები პრიორიტეტის რიგით შეადარე.
  2. სტაბილური სორტირება ტოლ გასაღებებს შემავალი მონაცემების რიგში ინარჩუნებს; std::sort არ არის სტაბილური, std::stable_sort არის.
  3. კომპარატორში მკაცრი < გამოიყენე: comp(x, x) false უნდა იყოს, თორემ სორტირება შეიძლება გაფუჭდეს.
02

ითამაშე

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

👀 რას უყურო: ყოველი „შედარება“ კომპარატორის ერთი გამოძახებაა. სცადე შენი ნივთები (ღირებულება/წონა) და დაკვრამდე გამოიცანი საბოლოო რიგი.

ინტერაქტიული ვიზუალიზატორიაქ დააწკაპე, მერე space ← →
✎ შენი მონაცემებიმაქს. 7 ნივთი ფორმატით ღირებულება/წონა, მაგ. 60/10 40/5.

std::sort: დალაგება კომპარატორით (ღირებულება/წონა კლებადობით)

$60 / 10kg
6.0 $/kg
$100 / 20kg
5.0 $/kg
$120 / 30kg
4.0 $/kg
$40 / 5kg
8.0 $/kg
$45 / 15kg
3.0 $/kg
std::sort იღებს ნებისმიერ კომპარატორს, რომელიც მკაცრ სუსტ რიგს განსაზღვრავს. ვალაგებთ 5 ნივთს ღირებულება-კილოგრამზე, ყველაზე მაღალით.

ფსევდოკოდი

 1 bool comp(a, b) { return a.ratio > b.ratio; }  // strict weak order 2 compute each ratio = value / weight 3 sort(items, items+n, comp)  // O(n log n) 4 // items now ordered best-value-per-kg first
1 / 1
03

შეამოწმე

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

№1

რომელი კომპარატორი ალაგებს წყვილებს პირველი მნიშვნელობით კლებადად, მერე მეორით ზრდადად?

№2

ჩანაწერები სახელით არის დალაგებული. მათ სტაბილურად ალაგებ ქალაქით. როგორ არიან დალაგებული ერთი ქალაქის ადამიანები?

№3

შენი std::sort მხოლოდ ბევრი ტოლი ელემენტის მქონე შემავალ მონაცემებზე ვარდება. კომპარატორია return a.v <= b.v;. რატომ?

04

ივარჯიშე

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