徹底二叉樹實現優先隊列與堆排序

本文的目標是要作出優先隊列和堆排序兩個Demo。node

  • 徹底二叉樹
  • 優先隊列
  • 堆排序

徹底二叉樹

徹底二叉樹的定義是創建在滿二叉樹定義的基礎上的,而滿二叉樹又是創建在二叉樹的基礎上的。git

大體瞭解一下概念github

一、是一對多的數據結構,從一個根結點開始,生長出它的子結點,而每個子結點又生長出各自的子結點,成爲子樹。若是某個結點再也不生長出子結點了,它就成爲葉子。 二、二叉樹每一個結點最多隻有兩棵子樹,並且左右子樹是有順序的,不可顛倒。算法

滿二叉樹

三、滿二叉樹的全部分支結點都既有左子樹又有右子樹,而且全部葉子都在同一層。滿二叉樹看起來的感受很完美,沒有任何缺失。數組

徹底二叉樹

四、徹底二叉樹不必定是滿的,但它自上而下,自左而右來看的話,是連續沒有缺失的。數據結構

  • 對一棵具備n個結點的二叉樹按層序編號,若是編號爲i (1 <= i <= n)的結點與一樣深度的滿二叉樹中編號爲i的結點在二叉樹中位置徹底相同,則這棵二叉樹稱爲徹底二叉樹。

性質ui

徹底二叉樹有好幾條性質,其中有一條在本文中須要用到:atom

二叉樹性質.png

對於一棵有n個結點的徹底二叉樹,對其結點按層序編號,從上到下,從左到右,對任一結點i (1 = i <= n)有:spa

  1. 若是i = 1,則結點i是二叉樹的根,無父結點;若是i > 1,則其父結點的位置是⌊i / 2⌋(向下取整)。
  2. 若是2i > n,則結點i無左孩子(結點i爲葉子結點);不然其左孩子是結點2i
  3. 若是2i + 1 > n,則結點i無右孩子;不然其右孩子是結點2i + 1

存儲結構線程

得益於二叉樹的嚴格定義,咱們只須要把徹底二叉樹按層序遍歷依次把結點存入一維數組中,其數組下標就可以體現出父子結點關係來(數組第0位不使用)。

存儲進一維數組

優先隊列 (Priority Queue)

徹底二叉樹的概念大體瞭解後,下面進入正題,看看如何把它用起來。

  • 普通的隊列是一種先進先出的數據結構,元素在隊列尾追加,而從隊列頭刪除。
  • 在優先隊列中,元素被賦予優先級。當訪問元素時,具備最高優先級的元素最早刪除。優先隊列具備最高級先出的行爲特徵。

爲何使用徹底二叉樹來實現優先隊列?

優先隊列按數據結構的不一樣有幾種實現方式:

優先隊列時間複雜度

  1. 有序數組。每次入隊操做時都對數組從新排序,把入隊元素放在合適位置,維持數組有序;每次出隊操做只需刪除數組第一位便可,因第一位老是優先值最大的元素,應被最早刪除(降序排列的狀況下)。
  2. 無序數組。每次入隊操做直接追加在數組末尾;每次出隊操做須要遍歷整個數組來尋找最大優先值。
  3. 徹底二叉樹(堆)。假如咱們能保證二叉樹的每個父結點的優先值都大於或者等於它的兩個子結點,那麼在整棵樹看來,頂部根結點一定就是優先值最大的。這樣的樹結構能夠稱爲堆有序,而且由於最大值在根部,也稱爲大頂堆。 在每次出隊操做時,只須要把根結點出隊便可,而後從新調整二叉樹恢復堆有序;在每次入隊操做時把元素追加到末尾,一樣調整二叉樹恢復堆有序。

堆有序,大頂堆

綜合了入隊和出隊兩個操做來看,使用徹底二叉樹來實現的優先隊列在時間效率上是最高的。

隊列的操做

一個隊列應該提供兩個關鍵的方法:入隊列和出隊列。

typedef NSComparisonResult(^JXPriorityQueueComparator)(id obj1, id obj2);

@interface JXPriorityQueue : NSObject

/// 定義元素的比較邏輯
@property (nonatomic, copy) JXPriorityQueueComparator comparator;

/// 入列
- (void)enQueue:(id)element;

/// 出列
- (id)deQueue;

@end
複製代碼

入列

入列總的來講分爲兩步:

  1. 把元素加入進來
  2. 而後上游元素到合適的位置

EnQueue

- (void)enQueue:(id)element {
    // 添加到末尾
    [self.data addObject:element];
    
    // 上游元素以維持堆有序
    [self swimIndex:self.tailIndex];
}

/// 上游,傳入須要上游的元素位置,以及容許上游的最頂位置
- (void)swimIndex:(NSInteger)index {
    // 暫存須要上游的元素
    id temp = self.data[index];
    
    // parent的位置爲本元素位置的1/2
    for (NSInteger parentIndex = index / 2; parentIndex >= 1; parentIndex /= 2) {
        // 上游條件是本元素大於parent,不然不上游
        if (self.comparator(temp, self.data[parentIndex]) != NSOrderedDescending) {
            break;
        }
        // 把parent拉下來
        self.data[index] = self.data[parentIndex];
        // 上游本元素
        index = parentIndex;
    }
    // 本元素進入目標位置
    self.data[index] = temp;
}
複製代碼

這裏關鍵在於如何上游元素:

  1. 在元素加入進來後,與其父結點比較,假如大於父結點,則把元素上游到父結點的位置。
  2. 在上游到父結點位置後,再和當前所處結點的父結點比較,若是大於父結點,繼續上游。
  3. 重複,直到整棵樹調整成爲大頂堆。

出列

把優先值最大的元素(根結點)出列,可分爲以下步驟:

  1. 交換首尾兩個元素的位置,這樣尾元素將會成爲根結點,堆有序被打破。
  2. 剪掉被交換到末尾的原根元素
  3. 把交換到根結點的元素下沉到合適位置,從新調整爲大頂堆
  4. 返回被剪出的元素,即爲須要出列的最大優先值元素

DeQueue

- (id)deQueue {
    if (self.count == 0) {
        return nil;
    }
    // 取根元素
    id element = self.data[1];
    // 交換隊首和隊尾元素
    [self swapIndexA:1 indexB:self.tailIndex];
    [self.data removeLastObject];
    
    if (self.data.count > 1) {
        // 下沉剛剛交換上來的隊尾元素,維持堆有序狀態
        [self sinkIndex:1];
    }
    return element;
}

/// 交換元素
- (void)swapIndexA:(NSInteger)indexA indexB:(NSInteger)indexB {
    id temp = self.data[indexA];
    self.data[indexA] = self.data[indexB];
    self.data[indexB] = temp;
}

/// 下沉,傳入須要下沉的元素位置,以及容許下沉的最底位置
- (void)sinkIndex:(NSInteger)index {
    // 暫存須要下沉的元素
    id temp = self.data[index];
    
    // maxChildIndex指向最大的子結點,默認指向左子結點,左子結點的位置爲本結點位置*2
    for (NSInteger maxChildIndex = index * 2; maxChildIndex <= self.tailIndex; maxChildIndex *= 2) {
        // 若是存在右子結點,而且左子結點比右子結點小
        if (maxChildIndex < self.tailIndex && (self.comparator(self.data[maxChildIndex], self.data[maxChildIndex + 1]) == NSOrderedAscending)) {
            // 指向右子結點
            ++ maxChildIndex;
        }
        // 下沉條件是本元素小於child,不然不下沉
        if (self.comparator(temp, self.data[maxChildIndex]) != NSOrderedAscending) {
            break;
        }
        // 不然
        // 把最大子結點元素上游到本元素位置
        self.data[index] = self.data[maxChildIndex];
        // 標記本元素須要下沉的目標位置,爲最大子結點原位置
        index = maxChildIndex;
    }
    // 本元素進入目標位置
    self.data[index] = temp;
}
複製代碼

這裏關鍵在於如何把剪枝後的樹從新調整爲大頂堆,在下沉方法中:

  1. 將其左右兩個子結點比較一下,找出值最大的那個子結點。
  2. 與最大子結點比較,若是本身比最大子結點還要大,或者等於最大子結點,則無須下沉;若是比子結點小,則爲了調整爲大頂堆,本身就須要下沉到子結點的位置。
  3. 在進入到子結點位置後,再和當前所處結點的子結點比較,若是小於子結點,繼續下沉。
  4. 重複,直到整棵樹調整成爲大頂堆。

堆排序 (Heap Sort)

堆排序是對簡單選擇排序的一種改進,改進後的效果很是明顯。選擇排序的時間複雜度是,堆排序是nlog₂n

堆排序效率

堆排序總的來講分爲兩個步驟:

  1. 構造大頂堆。從下往上、從右到左,把每一個非終結點(即葉子結點)看成根結點,將其和其子樹調整成大頂堆。
  2. 對大頂堆進行排序。這一步驟和優先隊列的出列操做是很是類似的,都是不斷地把大頂堆根結點交換到末尾位置,而後剪掉,再把這樣剪枝後的樹從新調整成大頂堆以找出下一個最大值,放在根結點,繼續進行新一輪剪枝。 這是一個不斷選擇最大值,依次排列起來的過程。

NSMutableArray+JXHeapSort.h

typedef NSComparisonResult(^JXSortComparator)(id obj1, id obj2);
typedef void(^JXSortExchangeCallback)(id obj1, id obj2);
typedef void(^JXSortCutCallback)(id obj, NSInteger index);

@interface NSMutableArray (JXHeapSort)

// 堆排序
- (void)jx_heapSortUsingComparator:(JXSortComparator)comparator didExchange:(JXSortExchangeCallback)exchangeCallback didCut:(JXSortCutCallback)cutCallback;

@end
複製代碼

NSMutableArray+JXHeapSort.m

@implementation NSMutableArray (JXHeapSort)

/// 堆排序
- (void)jx_heapSortUsingComparator:(JXSortComparator)comparator didExchange:(JXSortExchangeCallback)exchangeCallback didCut:(JXSortCutCallback)cutCallback {
    // 排序過程當中不使用第0位
    [self insertObject:[NSNull null] atIndex:0];
    
    // 構造大頂堆
    // 遍歷全部非終結點,把以它們爲根結點的子樹調整成大頂堆
    // 最後一個非終結點位置在本隊列長度的一半處
    for (NSInteger index = self.count / 2; index > 0; index --) {
        // 根結點下沉到合適位置
        [self sinkIndex:index bottomIndex:self.count - 1 usingComparator:comparator didExchange:exchangeCallback];
    }
    
    // 徹底排序
    // 從整棵二叉樹開始,逐漸剪枝
    for (NSInteger index = self.count - 1; index > 1; index --) {
        // 每次把根結點放在列尾,下一次循環時將會剪掉
        [self jx_exchangeWithIndexA:1 indexB:index didExchange:exchangeCallback];
        if (cutCallback) {
            cutCallback(self[index], index - 1);
        }
        // 下沉根結點,從新調整爲大頂堆
        [self sinkIndex:1 bottomIndex:index - 1 usingComparator:comparator didExchange:exchangeCallback];
    }
    
    // 排序完成後刪除佔位元素
    [self removeObjectAtIndex:0];
}

/// 下沉,傳入須要下沉的元素位置,以及容許下沉的最底位置
- (void)sinkIndex:(NSInteger)index bottomIndex:(NSInteger)bottomIndex usingComparator:(JXSortComparator)comparator didExchange:(JXSortExchangeCallback)exchangeCallback {
    for (NSInteger maxChildIndex = index * 2; maxChildIndex <= bottomIndex; maxChildIndex *= 2) {
        // 若是存在右子結點,而且左子結點比右子結點小
        if (maxChildIndex < bottomIndex && (comparator(self[maxChildIndex], self[maxChildIndex + 1]) == NSOrderedAscending)) {
            // 指向右子結點
            ++ maxChildIndex;
        }
        // 若是最大的子結點元素小於本元素,則本元素沒必要下沉了
        if (comparator(self[maxChildIndex], self[index]) == NSOrderedAscending) {
            break;
        }
        // 不然
        // 把最大子結點元素上游到本元素位置
        [self jx_exchangeWithIndexA:index indexB:maxChildIndex didExchange:exchangeCallback];
        // 標記本元素須要下沉的目標位置,爲最大子結點原位置
        index = maxChildIndex;
    }
}

/// 交換兩個元素
- (void)jx_exchangeWithIndexA:(NSInteger)indexA indexB:(NSInteger)indexB didExchange:(JXSortExchangeCallback)exchangeCallback {
    id temp = self[indexA];
    self[indexA] = self[indexB];
    self[indexB] = temp;
    
    if (exchangeCallback) {
        exchangeCallback(temp, self[indexA]);
    }
}

@end
複製代碼

堆排序

在Demo中,nodeArray是一個UILabel數組:

@property (nonatomic, strong) NSMutableArray<UILabel *> *nodeArray;
複製代碼

對這個數組進行排序,並藉助信號量在線程間通訊,控制排序速度:

dispatch_semaphore_t sema = dispatch_semaphore_create(0);
    
// 定時發出信號,以容許繼續交換
NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:0.6 repeats:YES block:^(NSTimer * _Nonnull timer) {
    dispatch_semaphore_signal(sema);
}];

[self.nodeArray jx_heapSortUsingComparator:^NSComparisonResult(id obj1, id obj2) {
    // 比較兩個結點
    return [self compareWithNodeA:obj1 nodeB:obj2];
} didExchange:^(id obj1, id obj2) {
    // 交換兩結點
    dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
    [self exchangeNodeA:obj1 nodeB:obj2];
} didCut:^(id obj, NSInteger index) {
    // 剪枝
    dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
    [self cutNode:obj index:index];
}];
複製代碼

源碼

優先隊列:https://github.com/JiongXing/JXPriorityQueue 堆排序:https://github.com/JiongXing/JXHeapSort 排序算法比較 : https://github.com/JiongXing/JXSort

相關文章
相關標籤/搜索