الگوریتم مبتنی بر Heap با سوئیفت — از صفر تا صد

۱۰۸ بازدید
آخرین به‌روزرسانی: ۰۳ مهر ۱۴۰۲
زمان مطالعه: ۴ دقیقه
الگوریتم مبتنی بر 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 دارای دو فرزند است.

درک الگوریتم مبتنی بر Heap
مثالی از یک آرایه که به صورت یک «درخت تقریباً کامل» بصری‌سازی شده است.

از آنجا که چیدمان مقادیر ترتیبی است، یک الگوی ساده ظاهر می‌شود. در هر گره می‌توانیم موقعیت را به دقت به وسیله استفاده از فرمول‌های زیر پیش‌بینی کنیم:

1//formulas for heap indicies
2
3left = 2i + 1
4right = 2i + 2
5parent = floor(i - 1 / 2)

هیپ‌های مرتب‌سازی

یکی از ویژگی‌های جالب هیپ، توانایی آن برای مرتب‌سازی است.

الگوریتم‌های زیادی برای مرتب‌سازی دنباله‌های داده‌ها طراحی شده‌اند و هنگامی که به مرتب‌سازی هیپ‌ها می‌پردازیم، گره‌ها می‌توانند طوری چیدمان یابند که هر والد مقداری کمتر از فرزندانش باشد. در علوم رایانه این کار به نام «هیپ-کمینه» (min-heap) خوانده می‌شود:

الگوریتم مبتنی بر 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 یک مسیر منفرد را به عنوان پارامتر می‌پذیرد. برخلاف دیگر الگوریتم‌های مرتب‌سازی، هدف ما این نیست که همه آیتم‌ها مرتب‌سازی شوند، بلکه می‌خواهیم کمترین مقدار را پیدا کنیم. این بدان معنی است که می‌توانیم با مقایسه مقادیر زیرمجموعه، کارایی را بهبود ببخشیم.

الگوریتم مبتنی بر Heap
پردازش enqueuer یک مقدار جدیداً اضافه شده را مقایسه می‌کند.
درک الگوریتم مبتنی بر Heap
پردازش تا زمانی که کوچک‌ترین مقدار در ریشه قرار گیرد، ادامه می‌یابد.

از آنجا که متد 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-algorithms-data-structures
نظر شما چیست؟

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *