الگوریتم مبتنی بر Heap با سوئیفت — از صفر تا صد
در یکی از مطالب قبلی مجله فرادرس به بررسی الگوریتم «دایجسترا» (Dijkstra) برای جستجوی یک گراف پرداختیم. این الگوریتم که در سال 1959 انتشار یافته است، یک تکنیک رایج برای یافتن کوتاهترین مسیر در مسائل پیچیده محسوب میشود. در آن مقاله به بررسی جنبههای گوناگون از قبیل پیمایش گراف، ساختمانهای داده سفارشی و «رویکرد حریصانه» (greedy approach) پرداختیم. زمانی که برنامهها را طراحی میکنیم، تماشای کار کردن آنها حس خوبی ایجاد میکند. استفاده از الگوریتم دایجسترا به ما امکان میدهد که کوتاهترین مسیر بین رأس مبدأ و مقصد را بیابیم. با این حال این رویکرد را میتوان بهبود بخشید تا کارآمدتر شود. در این مقاله قصد داریم با استفاده از الگوریتم مبتنی بر Heap این بهینهسازی را اجرا کنیم. برای مطالعه مقاله قبلی میتوانید روی لینک زیر کلیک کنید:
الگوریتم کوتاهترین مسیر دایجسترا در سوئیفت — به زبان ساده
طرز کار الگوریتم مبتنی بر Heap
ساحتمان داده Heap اساساً فقط از یک آرایه تشکیل یافته است.
با این حال برخلاف آرایه آن را به صورت درخت بصریسازی میکنیم. کلمه «بصریسازی» بدین معنی است که از تکنیکهای پردازشی استفاده میکنیم که به طور معمول با ساختمانهای داده بازگشتی مرتبط هستند. این تغییر در طرز تفکر، مزیتهای زیادی دارد. به مثال زیر توجه کنید:
1//a simple array of unsorted integers
2let numberList: Array<Int> = [8, 2, 10, 9, 11, 7]
چنان که میبینید، numberList را میتوان به سادگی به صورت یک Heap تصور کرد. با آغاز از اندیس 0، آیتمها یک نقطه متناظر را به صورت گره والد یا فرزند پر میکنند. هر والد نیز به استثنای اندیس شماره 2 دارای دو فرزند است.
از آنجا که چیدمان مقادیر ترتیبی است، یک الگوی ساده ظاهر میشود. در هر گره میتوانیم موقعیت را به دقت به وسیله استفاده از فرمولهای زیر پیشبینی کنیم:
1//formulas for heap indicies
2
3left = 2i + 1
4right = 2i + 2
5parent = floor(i - 1 / 2)
هیپهای مرتبسازی
یکی از ویژگیهای جالب هیپ، توانایی آن برای مرتبسازی است.
الگوریتمهای زیادی برای مرتبسازی دنبالههای دادهها طراحی شدهاند و هنگامی که به مرتبسازی هیپها میپردازیم، گرهها میتوانند طوری چیدمان یابند که هر والد مقداری کمتر از فرزندانش باشد. در علوم رایانه این کار به نام «هیپ-کمینه» (min-heap) خوانده میشود:
در الگوریتم دایجسترا از مفهومی به نام frontier استفاده کردیم. Frontier به صورت یک آرایه ساده کدنویسی شده که در آن وزن کلی هر مسیر را برای یافتن کوتاهترین مسیر مورد مقایسه قرار دادیم:
1...
2
3 //construct the best path
4 var bestPath: Path = Path()
5
6 while frontier.count != 0 {
7
8 //support path changes using the greedy approach
9 var pathIndex: Int = 0
10
11 for x in 0..<frontier.count {
12
13 let itemPath: Path = frontier[x]
14
15 if (bestPath.total == 0) || (itemPath.total < bestPath.total) {
16 bestPath = itemPath
17 pathIndex = x
18 }
19 }
20...
با این که این روش پاسخگو بود، اما در واقع ما از تکنیک brute force استفاده کردیم. به بیان دیگر ما همه مسیرهای بالقوه را برای یافتن کوتاهترین مسیر مورد بررسی قرار دادهایم. این قطعه کد در یک زمان خطی O(n) اجرا میشود. اگر frontier شامل یک میلیون ردیف باشد، تأثیر آن بر عملکرد کلی الگوریتم چگونه خواهد بود؟
ساختار هیپ
در این بخش یک frontier کارآمدتر میسازیم. نام این ساختمان داده جدید که کارکرد یک آرایه را بسط میدهد PathHeap میگذاریم:
1public class PathHeap {
2
3 private var heap: Array<Path>
4
5 init() {
6 heap = Array<Path>()
7 }
8
9 //the number of frontier items
10 var count: Int {
11 return self.heap.count
12 }
13}
کلاس PathHeap شامل دو مشخصه است. هیپ برای پشتیبانی از طراحی خوب، یک مشخصه خصوصی اعلان کرده است. count برای ردگیری تعداد آیتمها، به صورت یک «مشخصه محاسبهشده» (computed property) اعلان یافته است.
ساخت صف
برای جستجوی frontier به روشی کارآمدتر از O(n) باید طرز فکر جدیدی پیش بگیریم. میتوانیم با استفاده از هیپ، عملکردمان را تا حد O(1) بهبود بخشیم. بدین ترتیب با بهرهگیری از فرمولهای heapsort، رویکرد جدید به این صورت است که مقادیر اندیس را طوری بچینیم که کوچکترین آیتم در ریشه قرار گیرد:
1//sort shortest paths into a min-heap (heapify)
2func enQueue(_ key: Path) {
3
4 heap.append(key)
5
6 var childIndex: Float = Float(heap.count) - 1
7 var parentIndex: Int = 0
8
9
10 //calculate parent index
11 if childIndex != 0 {
12 parentIndex = Int(floorf((childIndex - 1) / 2))
13 }
14var childToUse: Path
15 var parentToUse: Path
16//use the bottom-up approach
17 while childIndex != 0 {
18
19 childToUse = heap[Int(childIndex)]
20 parentToUse = heap[parentIndex]
21
22
23 //swap child and parent positions
24 if childToUse.total < parentToUse.total {
25 heap.swapAt(parentIndex, Int(childIndex))
26 }
27
28 //reset indices
29 childIndex = Float(parentIndex)
30if childIndex != 0 {
31 parentIndex = Int(floorf((childIndex - 1) / 2))
32 }
33
34
35 } //end while
36
37
38 } //end function
متد enqueuer یک مسیر منفرد را به عنوان پارامتر میپذیرد. برخلاف دیگر الگوریتمهای مرتبسازی، هدف ما این نیست که همه آیتمها مرتبسازی شوند، بلکه میخواهیم کمترین مقدار را پیدا کنیم. این بدان معنی است که میتوانیم با مقایسه مقادیر زیرمجموعه، کارایی را بهبود ببخشیم.
از آنجا که متد enqueuer مشخصه min-heap را نگهداری میکند، وظیفه اضافی یافتن کوتاهترین مسیر را حذف میکنیم. در نتیجه، کارایی الگوریتم تا حد زمان ثابت O(1) افزایش مییابد. در اینجا یک متد ابتدایی peek برای بازیابی آیتم در سطح ریشه پیادهسازی میکنیم:
1//obtain the minimum path
2func peek() -> Path? {
3
4 if heap.count > 0 {
5 return heap[0] //the shortest path
6 }
7 else {
8 return nil
9 }
10 }
هیپ و ژنریک
استفاده از هیپ میتواند به بهبود پیچیدگی زمانی راهحلهای مختلف کمک کند. برای این که مدل خود را برای کاربردهای عمومی بسط دهیم، میتوانیم الگوریتم تعمیمیافتهتری نیز ایجاد کنیم:
1//a generic heap structure
2class Heap<T: Comparable> {
3
4 private var items: Array<T>
5 private var heapType: HeapType
6
7 //min-heap default initialization
8 init(type: HeapType = .Min) {
9
10 items = Array<T>()
11 heapType = type
12 }
13
14
15 //the min or max value
16 func peek() -> T? {
17
18 if items.count > 0 {
19 return items[0] //the min or max value
20 }
21 else {
22 return nil
23 }
24 }
25
26
27 //addition of new items
28 func enQueue(_ key: T) {
29
30 items.append(key)
31
32 var childIndex: Float = Float(items.count) - 1
33 var parentIndex: Int = 0
34
35
36 //calculate parent index
37 if childIndex != 0 {
38 parentIndex = Int(floorf((childIndex - 1) / 2))
39 }
40
41
42 var childToUse: T
43 var parentToUse: T
44
45
46 //use the bottom-up approach
47 while childIndex != 0 {
48
49
50 childToUse = items[Int(childIndex)]
51 parentToUse = items[parentIndex]
52
53
54 //heapify depending on type
55 switch heapType {
56
57 case .Min:
58
59 //swap child and parent positions
60 if childToUse <= parentToUse {
61 items.swapAt(parentIndex, Int(childIndex))
62 }
63
64 case .Max:
65
66 //swap child and parent positions
67 if childToUse >= parentToUse {
68 items.swapAt(parentIndex, Int(childIndex))
69 }
70 }
71
72 //reset indices
73 childIndex = Float(parentIndex)
74
75 if childIndex != 0 {
76 parentIndex = Int(floorf((childIndex - 1) / 2))
77 }
78
79 } //end while
80} //end function
81}
82//used for generic heap data structure processing
83enum HeapType {
84
85 case Min
86 case Max
87}
بدین ترتیب به پایان این مقاله میرسیم.
اگر این مطلب برای شما مفید بوده است، آموزشهای زیر نیز به شما پیشنهاد میشوند:
- مجموعه آموزشهای برنامهنویسی
- آموزش برنامه نویسی Swift (سوئیفت) برای برنامه نویسی iOS
- مجموعه آموزشهای دروس علوم و مهندسی کامپیوتر
- آموزش سوئیفت (Swift) — مجموعه مقالات مجله فرادرس
- عملگرهای سفارشی در سوئیفت — از صفر تا صد
==