完全二叉樹實現優先佇列與堆排序

囧叔發表於2017-12-22

本文的目標是要做出優先佇列和堆排序兩個Demo。

  • 完全二叉樹
  • 優先佇列
  • 堆排序

完全二叉樹

完全二叉樹的定義是建立在滿二叉樹定義的基礎上的,而滿二叉樹又是建立在二叉樹的基礎上的。

大致瞭解一下概念

1、是一對多的資料結構,從一個根結點開始,生長出它的子結點,而每一個子結點又生長出各自的子結點,成為子樹。如果某個結點不再生長出子結點了,它就成為葉子。 2、二叉樹每個結點最多隻有兩棵子樹,而且左右子樹是有順序的,不可顛倒。

滿二叉樹

3、滿二叉樹的所有分支結點都既有左子樹又有右子樹,並且所有葉子都在同一層。滿二叉樹看起來的感覺很完美,沒有任何缺失。

完全二叉樹

4、完全二叉樹不一定是滿的,但它自上而下,自左而右來看的話,是連續沒有缺失的。

  • 對一棵具有n個結點的二叉樹按層序編號,如果編號為i (1 <= i <= n)的結點與同樣深度的滿二叉樹中編號為i的結點在二叉樹中位置完全相同,則這棵二叉樹稱為完全二叉樹。

性質

完全二叉樹有好幾條性質,其中有一條在本文中需要用到:

二叉樹性質.png

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

  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

相關文章