完全二叉樹的基本概念
可能你會疑問,為什么我們明明講的是堆排序,怎么又扯上了二叉樹的概念了,答案就是,我們這里的堆就是基于完全二叉樹來的,我們稱之為最大堆,所謂的完全二叉樹其實是相對于滿二叉樹而言的,這里我們不去深究二叉樹之類的這些概念,因為我們主要是討論堆排序,下面我上兩張圖你就能明白滿二叉樹是什么,完全二叉樹是什么了:
滿二叉樹的所有分支結點都既有左子樹又有右子樹,并且所有葉子都在同一層。滿二叉樹就是感覺是滿的沒有殘缺。
而完全二叉樹不一定是滿的,但它自上而下,自左而右來看的話,是連續沒有缺失的。
完全二叉樹的特性
上面已經介紹了完全二叉樹的概念,這里我還需要總結一下完全二叉樹的特性,因為我們后面需要用到這些特性來擼代碼:
這里我們假如使用數組來實現這個最大堆,數據從 1 的位置開始存儲,第0的索引我們不存放東西,那么就有如下性質:
1.父親節點的索引parent = i /2 向下取整例如 這里2是4和5的父親節點,那么2 = 5/2,向下取整等于2;
2.第一個無葉子的節點的索引等于整個堆的元素個數除以2 ,k = count / 2;
3.左孩子的索引等于父節點的索引除以2, k = 2 * i,而右孩子就等于2 * i + 1
最大堆的實現/優先隊列的概念
這里我還是想簡單介紹一個場景,這樣大家理解起來會比較容易,假如我們是一個游戲玩家--例如王者榮耀的玩家,那么每一個英雄都有一個攻擊的范圍,假如這個范圍內出現了多個敵方英雄或者小兵,那么你可以攻擊他們任意一個,但是他會有一個優先級,你可以攻擊英雄 也可以攻擊小兵,假如你得需求是每次你都優先攻擊英雄,那么讓你來實現這個需求,你會怎么做呢?這就是我們的優先隊列或者說最大堆;
最大堆/優先隊列的實現
下面就是我們最大堆或者說優先隊列的實現了,假如我們使用一個類來實現這個最大堆,我們先思考一下我們需要哪些方法?
首先我覺得有最重要的幾個方法,實例化方法、入隊和出隊
/**
實例化最大堆
@param capacity 堆中的最大容量
@return 最大堆實例
*/
+ (instancetype)maxHeapWithCapacity:(NSInteger)capacity;
/**
插入值到最大堆中(入隊)
@param item 元素
*/
- (void)insertItem:(id)item;
/**
從堆中取值(出隊)
@return 優先的元素
*/
- (id)extractMax;
接下來我們可能還需給外界提供一個獲取當前堆中容量和是否是一個空堆的方法
/**
最大堆中的大小
@return 堆容量
*/
- (NSInteger)size;
/**
判斷是否為空堆
@return 是否空堆
*/
- (BOOL)isEmpty;
好了,我們就根據我們提出的這幾個方法來依次實現以下:
1.實例化方法 (MaxHeap類的實例化)
再講實例化方法之前,我們先把存放元素的數組和一些其它需要用到的屬性聲明好,當然這些都是不提供給外界的,所以我們都放在.m中
/** 堆容量 */
@property(nonatomic,assign) NSInteger capacity;
/** 數據個數*/
@property(nonatomic,assign) int count;
/** 容器 */
@property(nonatomic,strong) NSMutableArray *itemsArray;
- (NSMutableArray *)itemsArray {
if (!_itemsArray) {
_itemsArray = [NSMutableArray arrayWithCapacity:self.capacity + 1];
//默認先加入一個元素 也就是第0個元素
[_itemsArray addObject:@(0)];
}
return _itemsArray;
}
有了上面這些,我們的實例化方法就出來了:
+ (instancetype)maxHeapWithCapacity:(NSInteger)capacity {
MaxHeap *maxHeap = [[MaxHeap alloc]init];
maxHeap.capacity = capacity;
maxHeap.count = 0;
return maxHeap;
}
上面的代碼我就不用解釋了吧,capacity是數組最大容量,而count是我們的元素真實的個數,當然我們在懶加載中可能你注意到了一個地方,那就是我在初始化itemArray的時候默認是添加了一個元素的,原因是我們的最大堆是從第一個元素開始的,而第一個元素我們不需要用到,所以這里默認第一個元素就直接用0來代替
2.入隊操作的實現
- (void)insertItem:(id)item {
if (self.count >= self.capacity) {
//這里也可以用斷言
NSLog(@"容量已滿");
return;
}
//添加元素
[self.itemsArray addObject:item];
//元素個數++
self.count++;
//執行shiftUp操作
[self shiftUp:_count];
}
/**
不斷與父親節點比較往上升的過程
@param k 比較的索引
*/
- (void)shiftUp:(int)k {
//當k == 1的時候只有一個元素就可以不用比較了 self.itemsArray[k/2]是父親節點的值
while (k > 1 && self.itemsArray[k] > self.itemsArray[k/2]) {
//交換位置
[self.itemsArray exchangeObjectAtIndex:k/2 withObjectAtIndex:k];
//更新索引為父節點的索引
k /= 2;
}
}
3.出隊列操作的實現
- (id)extractMax {
if (self.count <= 0) {
NSLog(@"無法取出元素---堆中元素已全部取出");
return nil;
}
//將當前最大的數也就是第一個元素取出來
id item = self.itemsArray[1];
//將最后一個元素放到第一位
[self.itemsArray exchangeObjectAtIndex:1 withObjectAtIndex:self.count];
self.count--;
//執行shiftDown操作
[self shiftDown:1];
return item;
}
- (void)shiftDown:(int)k {
while (2*k <= _count) {//判斷是否有孩子 只要判斷的有左孩子就行了
//聲明一個變量初始化為做左孩子的索引
int j = 2 *k;
//判斷是否右孩子防止越界且比較左孩子和右孩子的值
if (j + 1 <= _count && _itemsArray[j] < _itemsArray[j + 1]) {
j += 1;
}
//此時itemsArray[j]中存放的就是兩個孩子中間最大的元素
//比較父節點與孩子中最大的值
if (_itemsArray[j] <= _itemsArray[k]) {
break;
}
//走到這里說明父節點比子節點的值要小 交換位置 更新索引
[_itemsArray exchangeObjectAtIndex:j withObjectAtIndex:k];
k = j;
}
}
4.元素個數以及判空實現
- (NSInteger)size {
return self.count;
}
- (BOOL)isEmpty {
return self.count == 0;
}
第一個版本的堆排序
到這里,我們已經基本實現了一個優先隊列或者說最大堆了,此外我們的第一個版本的堆排序也已經完成了,可能你就會納悶了,你寫了這么多好像也沒看到排序啊,我到底外界怎么使用呢?別急,下面就告訴你怎么用:
/**
第一個版本的堆排序
@param originalArray 待排序數組
*/
- (void)heapSort1:(NSMutableArray *)originalArray {
//1.實例化最大堆
MaxHeap *maxHeap = [MaxHeap maxHeapWithCapacity:originalArray.count];
//2.將所有數組中的元素入隊
[originalArray enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
[maxHeap insertItem:obj];
}];
//3.從堆中依次取出元素賦值給array(出隊)
for (int i = (int)originalArray.count - 1; i >= 0; i--) {
originalArray[i] = maxHeap.extractMax;
}
}
好了,就這么簡單,運行然后打印一下要排序的數組,是不是覺得很神奇?原來排序還可以這樣實現,第一次看到堆排序的時候我也覺得超神奇,所以說還是應該要多學習,你會收獲到很多你意想不到的東西。
第二個版本的堆排序(heapify)
可能你會納悶了,不是已經都實現完了嗎?怎么還有第二個版本,呵呵,我只能說:“騷年,你還太年輕,除了第二個版本,還有第三個”。別急,看完上一個版本的堆排序,可能你會問,第二個版本的堆排序是為了解決什么?答案是減少時間復雜度呀。廢話不多少,下面還是先把圖上了:
前面我們提到了完全二叉樹的特性2:
第一個無葉子的節點的索引等于整個堆的元素個數除以2 ,k = count / 2;
從圖中我們可以看到第一個無葉子節點的索引多少呢?
答案是 5,也就是 10 / 2 = 5,那我們拿到這個有什么用呢?大家注意看,如果我們從5號索引開始依次遞減執行我們在上面實現的shiftDown方法會怎樣呢?,看出什么了嗎,你會發現是不是執行完之后,這個數組內的元素已經是一個最大堆了呢?沒錯就是這么簡單,下面是代碼實現:
.h中
/**
實例化最大堆
@param orginalArray 待排序數組
@return 最大堆
*/
+ (instancetype)maxHeapWithOriginalArray:(NSMutableArray *)orginalArray;
.m中
+ (instancetype)maxHeapWithOriginalArray:(NSMutableArray *)orginalArray {
MaxHeap *maxHeap = [[MaxHeap alloc]init];
//因為索引是從1開始添加數據 所以這里要加1
maxHeap.capacity = (int)orginalArray.count + 1;
maxHeap.count = (int)orginalArray.count;
//依次將數組中的元素添加到itemArray中
[orginalArray enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
[maxHeap.itemsArray addObject:obj];
}];
//從第一個無葉子節點開始依次執行shiftDown操作
for (int i = maxHeap.count/2; i >= 1; i--) {
[maxHeap shiftDown:i];
}
return maxHeap;
}
那么怎么使用呢?
/**
堆排序第二個版本
@param originalArray 待排序數組
*/
- (void)heapSort2:(NSMutableArray *)originalArray {
//實例化優先隊列
MaxHeap *maxHeap = [MaxHeap maxHeapWithOriginalArray:originalArray];
for (int i = (int)originalArray.count - 1; i >= 0; i--) {
//依次出隊列就可以了
originalArray[i] = maxHeap.extractMax;
}
}
看到這里,趕緊去試一下效果吧,你是不是也會驚嘆:"還有這操作",這里可能很多人會說,既然你是為了優化時間復雜度而來的第二個版本,那么你用代碼來測試一下啊,其實我也想在這里就直接來個測試截圖,這樣這篇文章就結束了,當然這是我不愿意看到的,因為如果這樣就結束了,那我第三個版本的堆排序該怎么進行呢?so,別著急,等我第三個版本的堆排序講完了,給大家看最后的測試代碼和結果。
第三個版本的堆排序(原地堆)
我們前面的兩個版本排序都是屬于先通過實現一個最大堆也就是新開辟了空間的基礎上來實現的,那么有沒有什么方式可以直接在原數組上面就實現呢?答案肯定是有的,有了第二個版本的的基礎,其實我們也稱第二個版本叫做heapify的過程,我們就可以直接在原數組上將數組heapify,再通過一定的交換操作來完成原地堆的排序:
第一步:將數組heapify
//1.先將數組heapify(形成最大堆)
for (int i = (int)(originalArray.count - 1)/2; i >= 0; i--) {
//依次執行shiftDown操作來實現最大堆
[self shiftDown:originalArray count:(int)originalArray.count index:i];
}
這里我就不再解釋了,如果沒有看懂怎么將數組heapify,那就往前再回顧一下,這里的shiftDown方法和原來第二個版本的shiftDown方法基本一樣,但是由于我們是直接在原數組的基礎上進行,索引是從0開始的,所以這里就會有一些差異,這些差異我在實現的代碼里都標記了注釋,所以這里我就先不講,下面會給出shiftDown的代碼,看到這里,數組已經是一個優先隊列了,那么我們要排序,假如要將從小到大的排列,我們可以這樣做:
1.將第0個位置的元素和最后一個位置的元素交換位置,此時最后一個元素就是最大的元素了
2.執行完上面的操作之后,你會發現除了最后一個元素之外也就是n - 1這些個元素現在已經不能滿足最大堆的性質了,那么我們可以想辦法讓它繼續滿足最大堆的性質,具體怎么做呢?其實很簡單,我們對數組n - 1個元素中的第0個位置的元素執行shiftDown操作就可以了
下面來看代碼:
/**
原地堆排序--堆排序第三個版本
@param originalArray 待排序數組
*/
- (void)heapSort3:(NSMutableArray *)originalArray {
//1.先將數組heapify(形成最大堆)
for (int i = (int)(originalArray.count - 1)/2; i >= 0; i--) {
[self shiftDown:originalArray count:(int)originalArray.count index:i];
}
//2.將最大堆中的第一個元素也就是最大的元素 放到數組最后
for (int j = (int)originalArray.count - 1; j >=0 ; --j) {
[originalArray exchangeObjectAtIndex:0 withObjectAtIndex:j];
//交換完位置后再執行shiftDown操作讓數組前半部分保持最大堆的性質
[self shiftDown:originalArray count:j index:0];
}
}
/**
shiftDown操作
@param array 待排序數組
@param count shifDown操作界限
@param index shiftDown的位置
*/
- (void)shiftDown:(NSMutableArray *)array count:(int)count index:(int)index{
//因為是從0開始 所以左孩子就應該要偏移1個位置
while (2*index + 1 < count) {//判斷只要有孩子(有左孩子就表示有孩子)
int j = 2*index + 1;//初始化j為左孩子索引
if (j + 1 < count && array[j] < array[j+1]) {//第一個判斷條件為右孩子是否越界,判斷左孩子是否比右孩子的值大
j += 1;//j+1 索引變為右孩子的索引
}
if (array[index] >= array[j]) {//判斷父親節點的值和子孩子的值進行比較
break;
}
//當父親節點的值比孩子節點的值要大舅應該交換位置
[array exchangeObjectAtIndex:index withObjectAtIndex:j];
//更新索引
index = j;
}
}
可能會有很多人對下面這一部分代碼不理解:
//2.將最大堆中的第一個元素也就是最大的元素 放到數組最后
for (int j = (int)originalArray.count - 1; j >=0 ; --j) {
[originalArray exchangeObjectAtIndex:0 withObjectAtIndex:j];
//交換完位置后再執行shiftDown操作讓數組前半部分保持最大堆的性質
[self shiftDown:originalArray count:j index:0];
}
我這里簡單的解釋一下,我們這里的j就是從count - 1開始,這是因為我們交換完成之后讓前半部分沒有交換的元素依然保持最大堆的性質,如果從0開始則沒法控制這個j的索引,而下面的shiftDown操作中count傳入的恰好是j,也就是要維持最大堆性質的個數,當然這里的index是要執行shiftDown操作的索引,這里很簡單就是第一個元素啦,因為交換位置后,最大的元素被移動到最后去啦,說到這里,今天要討論的問題就結束了,但是我前面還說了要給大家提供三個版本堆排序的測試代碼和結果的,所以還不能結束(哈哈_別哭)
測試代碼 & 測試結果
- (void)testSortWithExcuteBlock:(void(^)())excuteBlock{
CFAbsoluteTime startTime =CFAbsoluteTimeGetCurrent();
if (excuteBlock) {
excuteBlock();
}
CFAbsoluteTime linkTime = (CFAbsoluteTimeGetCurrent() - startTime);
NSLog(@"Linked in %f ms", linkTime *1000.0);
}
上面是我在測試排序工具類中的方法,這里抽出來單獨說一下,傳入一個block,在外界調用后就可以拿到測試結果,下面開始測試:
NSMutableArray *array1 = [self.testHelper generateRandomArray:100000 rangeLeft:1 rangeRight:1000];
NSMutableArray * heapSort1 = array1.mutableCopy;
NSMutableArray * heapSort2 = array1.mutableCopy;
NSMutableArray * heapSort3 = array1.mutableCopy;
NSLog(@"============第一個版本堆排序==============");
[self.testHelper testSortWithExcuteBlock:^{
[self heapSort1:insertionSort1];
}];
NSLog(@"============第二個版本堆耗時==============");
[self.testHelper testSortWithExcuteBlock:^{
[self heapSort2:heapSort2];
}];
NSLog(@"============第三個版本堆耗時==============");
[self.testHelper testSortWithExcuteBlock:^{
[self heapSort3:heapSort3];
}];
這里把生成代碼的測試數組的方法貼出來,不然我怕有些朋友又要問這個怎么來的了:
#pragma mark - 隨機獲取一個數組
#pragma mark -
- (NSMutableArray *)generateRandomArray:(int)count rangeLeft:(int)left rangeRight:(int)right{
NSMutableArray * tempArray = [NSMutableArray arrayWithCapacity:count];
for (int i = 0; i < count; i ++) {
int randomNum = arc4random()%(right - left + 1) + left;
[tempArray addObject:@(randomNum)];
}
return tempArray;
}
這里的testHelper就是我的測試工具類,這里你可以忽略,反正就是用來幫助測試代碼執行時長的,這里我也把隨機生成一個數組的方法提供了,這里我用的數據是10萬個數據進行排序,,下面是測試結果:
我們可以明顯的看到三個版本的堆排序時間是依次遞減的,也就是說我們的優化還是挺有成效是不是(哈哈??)
好吧,如果你認真的再看這篇文章的話,可能看到這里你也很累了,說實話,我更累啊 ,碼字、截圖、測試好累好累,希望可以幫到大家,有什么不懂得依舊留言或者私信哦。