前面實現的全部鏈表結構都有一個共同的特徵,就是每個鏈表的節點都只有一個next指針指向下一個節點,是一條線性的數據結構。二叉樹和線性表的不一樣之處就是二叉樹的每個節點有兩個指針分別指向兩個子節點。node
鏈表:git
二叉樹:github
下面首先了解一下二叉樹的基本概念:數組
經過上面介紹能夠看到,二叉樹是由節點構成的,這點和鏈表很是像。並且是由根節點開始的,就像鏈表是由頭節點起始的同樣。每個節點由父節點指向它,而它又分別指向它的兩個子節點。兩個子節點一個在左邊,一個在右邊。因此二叉樹的節點起碼須要以下的屬性:bash
經過上面的分析,咱們如今先抽象出一個二叉樹節點的類:數據結構
@interface JKRBinaryTreeNode : NSObject
@property (nonatomic, strong, nonnull) id object;
@property (nonatomic, strong, nullable) JKRBinaryTreeNode *left;
@property (nonatomic, strong, nullable) JKRBinaryTreeNode *right;
@property (nonatomic, weak, nullable) JKRBinaryTreeNode *parent;
@end
複製代碼
而二叉樹只須要保存根節點就能夠了;工具
@interface JKRBinaryTree<ObjectType> : NSObject {
JKRBinaryTreeNode *_root;
}
@end
複製代碼
如今咱們已經建立好了一個自定義的二叉樹結構,下面咱們就用剛剛建立好的二叉樹手動構造一組二叉樹的數據:post
JKRBinaryTree *tree = [JKRBinaryTree new];
JKRBinaryTreeNode *rootNode = [JKRBinaryTreeNode new];
rootNode.object = @1;
tree->_root = rootNode;
JKRBinaryTreeNode *leftChildNode = [JKRBinaryTreeNode new];
leftChildNode.object = @2;
leftChildNode.parent = rootNode;
rootNode.left = leftChildNode;
JKRBinaryTreeNode *rightChildNode = [JKRBinaryTreeNode new];
rightChildNode.object = @3;
rightChildNode.parent = rootNode;
rootNode.right = rightChildNode;
NSLog(@"%@", tree);
複製代碼
建立好的二叉樹內存結構應該以下圖,實線和虛線和用來區分iOS中的強引用和弱引用,其它語言能夠無視這個區別。測試
上面建立一個有三個節點構成二叉樹,根節點存放着數字1,根節點的兩個子節點分別存放這數組2和3。打印二叉樹的結構爲:ui
┌-1 (p: (null))-┐
│ │
2 (p: 1) 3 (p: 1)
複製代碼
上面的打印是封裝好的二叉樹打印工具打印的,這個工具邏輯很是複雜並且和數據結構並無關係,能夠直接下載工具源碼,這裏只介紹如何使用:
#import "JKRBinaryTree.h"
/// 引用打印工具
#import "LevelOrderPrinter.h"
/// 自定義的二叉樹類實現改協議
@interface JKRBinaryTree ()<LevelOrderPrinterDelegate>
@end
/// 實現LevelOrderPrinterDelegate的代理方法
@implementation JKRBinaryTree
#pragma mark - LevelOrderPrinterDelegate
/// 返回二叉樹的根節點
- (id)print_root {
return _root;
}
/// 返回一個節點對象的左子節點
- (id)print_left:(id)node {
JKRBinaryTreeNode *n = (JKRBinaryTreeNode *)node;
return n.left;
}
/// 返回一個節點對象的右子節點
- (id)print_right:(id)node {
JKRBinaryTreeNode *n = (JKRBinaryTreeNode *)node;
return n.right;
}
/// 返回一個節點輸出什麼樣的文字
- (id)print_string:(id)node {
return [NSString stringWithFormat:@"%@", node];
}
#pragma mark - 格式化輸出
/// 重寫二叉樹的打印方法
- (NSString *)description {
return [LevelOrderPrinter printStringWithTree:self];
}
@end
@implementation JKRBinaryTreeNode
/// 重寫二叉樹節點的打印方法
- (NSString *)description {
// 打印格式: 節點存儲的元素 (p: 父節點存儲的元素)
return [NSString stringWithFormat:@"%@ (p: %@)", self.object, self.parent.object];
}
@end
複製代碼
上面雖然實現並知足了二叉樹的基本結構,不過並無實際使用價值。可是當二叉樹知足以下性質就能夠用實用價值了:
而知足如上條件的二叉樹就是一棵二叉搜索樹(Binary Search Tree),也稱二叉查找樹、二叉排序樹。
以下就是一棵二叉搜索樹:
┌---7 (p: (null))---┐
│ │
┌-4 (p: 7)-┐ ┌-9 (p: 7)-┐
│ │ │ │
┌-2 (p: 4)-┐ 5 (p: 4) 8 (p: 9) ┌-11 (p: 9)-┐
│ │ │ │
1 (p: 2) 3 (p: 2) 10 (p: 11) 12 (p: 11)
複製代碼
仔細觀察就能夠發現,根節點7的左子樹全部節點都小於7,右子樹全部節點都大於7。一樣的,其它的全部節點也都知足如上的兩個條件。
那麼這樣的二叉樹有什麼優勢和好處呢,這裏經過查找就能夠知道了,假如須要從二叉搜索樹中查找12。既然二叉樹開始只能獲取到根節點,咱們搜索也是從根節點開始的:
能夠發現,只須要4步就可以找到12,經過二叉搜索樹能夠大大提升搜索數據的效率。下面咱們就本身實現基於剛剛封裝的二叉樹的基礎上,實現一個二叉搜索樹。
首先二叉搜索樹也是二叉樹,咱們的二叉搜索樹直接繼承剛剛封裝的二叉樹就能夠,同時爲了內部方便的存儲和記錄二叉樹的節點數量,同鏈表的設計同樣,咱們須要二叉樹額外添加一個_size屬性記錄當前二叉樹存書元素的個數,這個屬性並不是二叉搜索樹獨有的,而是因此二叉樹共有的,因此放在二叉樹類中。
/// 二叉樹
@interface JKRBinaryTree<ObjectType> : NSObject {
@protected
NSUInteger _size;
JKRBinaryTreeNode *_root;
}
@end
複製代碼
同時因爲二叉搜索樹的須要比較節點元素大小的性質,二叉搜索樹添加的元素必定是須要比較大小且可以比較大小的,而基於面向對象的特性,存儲的元素必定是對象,咱們須要告訴二叉搜索樹如何比較存入對象的大小,這裏咱們在二叉搜樹中定一個block:
typedef NSInteger(^jkrbinarytree_compareBlock)(id e1, id e2);
複製代碼
經過block返回的值判斷大小:
二叉搜索樹須要保存一個外部傳入的比較大小的block來進行內部元素的大小比對:
/// 二叉搜索樹繼承自二叉樹
@interface JKRBinarySearchTree<ObjectType> : JKRBinaryTree {
@protected
jkrbinarytree_compareBlock _compareBlock;
}
@end
複製代碼
爲了實現實際使用的基本功能,二叉搜索樹須要定義並實現以下接口:
/*
二叉搜索樹添加的元素必須具有可比較性
1,經過初始化方法傳入比較的代碼塊
2,加入的對象是系統默認的帶有compare:方法的類的實例,例如:NSNumber、NSString類的實例對象
3,加入的對象實現binaryTreeCompare:方法
*/
- (instancetype)initWithCompare:(_Nullable jkrbinarytree_compareBlock)compare;
/// 添加元素
- (void)addObject:(nonnull ObjectType)object;
/// 刪除元素
- (void)removeObject:(nonnull ObjectType)object;
/// 是否包含元素
- (BOOL)containsObject:(nonnull ObjectType)object;
/// 經過元素獲取對應節點
- (JKRBinaryTreeNode *)nodeWithObject:(nonnull ObjectType)object;
/// 刪除節點
- (void)removeWithNode:(JKRBinaryTreeNode *)node;
複製代碼
二叉搜索樹比較邏輯的block不是必傳的,由於一些系統默認類型是有默認的比較功能的,好比NSNumber。
同時咱們還能夠再次支持另外一種比較元素大小的方式,就是聲明一個協議並定義一個比較大小的方法,若是添加的元素類實現了自定義的比較大小的方法,能夠經過自定義比較方法來比較大小:
@protocol JKRBinarySearchTreeCompare <NSObject>
- (NSInteger)binaryTreeCompare:(id)object;
@end
複製代碼
- (instancetype)initWithCompare:(jkrbinarytree_compareBlock)compare {
self = [super init];
_compareBlock = compare;
return self;
}
複製代碼
二叉搜索樹的查找離不開比較邏輯,這裏先實現元素比較的私有方法:
比較的邏輯以下:
- (NSInteger)compareWithValue1:(id)value1 value2:(id)value2 {
NSInteger result = 0;
if (_compareBlock) { // 有比較器
result = _compareBlock(value1, value2);
} else if ([value1 respondsToSelector:@selector(binaryTreeCompare:)]) { // 實現了自定義比較方法
result = [value1 binaryTreeCompare:value2];
} else if ([value1 respondsToSelector:@selector(compare:)]){ // 系統自帶的可比較對象
result = [value1 compare:value2];
} else {
NSAssert(NO, @"object can not compare!");
}
return result;
}
複製代碼
既然二叉樹的添加的元素必須可以比較大小,那麼傳入的元素不能爲空,咱們首先建立一個判斷元素不爲空的方法:
- (void)objectNotNullCheck:(id)object {
if (!object) {
NSAssert(NO, @"object must not be null!");
}
}
複製代碼
添加元素須要如下的判斷邏輯:
- (void)addObject:(id)object {
[self objectNotNullCheck:object];
if (!_root) {
JKRBinaryTreeNode *newNode = [[JKRBinaryTreeNode alloc] initWithObject:object parent:nil];
_root = newNode;
_size++;
return;
}
JKRBinaryTreeNode *parent = _root;
JKRBinaryTreeNode *node = _root;
NSInteger cmp = 0;
while (node) {
cmp = [self compareWithValue1:object value2:node.object];
parent = node;
if (cmp < 0) {
node = node.left;
} else if (cmp > 0) {
node = node.right;
} else {
node.object = object;
return;
}
}
JKRBinaryTreeNode *newNode = [[JKRBinaryTreeNode alloc] initWithObject:object parent:parent];;
if (cmp < 0) {
parent.left = newNode;
} else {
parent.right = newNode;
}
_size++;
}
複製代碼
在二叉搜索樹開始的分析時已經模擬一遍查找邏輯,經過元素獲取節點和上面添加元素的比較邏輯很是類似:
- (JKRBinaryTreeNode *)nodeWithObject:(id)object {
JKRBinaryTreeNode *node = _root;
while (node) {
NSInteger cmp = [self compareWithValue1:object value2:node.object];
if (!cmp) {
return node;
} else if (cmp > 0) {
node = node.right;
} else {
node = node.left;
}
}
return nil;
}
複製代碼
是否包含某元素即經過元素查找對應的節點是否爲空:
return [self nodeWithObject:object] != nil;
複製代碼
依次添加 {7,4,2,1,3,5,9,8,11,10,12} 到二叉搜索樹中並打印:
JKRBinarySearchTree<NSNumber *> *tree = [[JKRBinarySearchTree alloc] initWithCompare:^NSInteger(NSNumber * _Nonnull e1, NSNumber * _Nonnull e2) {
return e1.intValue - e2.intValue;
}];
int nums[] = {7,4,2,1,3,5,9,8,11,10,12};
NSMutableArray *numbers = [NSMutableArray array];
for (int i = 0; i < sizeof(nums)/sizeof(nums[0]); i++) {
printf("%d ", nums[i]);
[numbers addObject:[NSNumber numberWithInt:nums[i]]];
}
printf("\n");
for (NSNumber *number in numbers) {
[tree addObject:number];
}
/// 打印二叉樹
NSLog(@"%@", tree);
複製代碼
打印結果:
┌---7 (p: (null))---┐
│ │
┌-4 (p: 7)-┐ ┌-9 (p: 7)-┐
│ │ │ │
┌-2 (p: 4)-┐ 5 (p: 4) 8 (p: 9) ┌-11 (p: 9)-┐
│ │ │ │
1 (p: 2) 3 (p: 2) 10 (p: 11) 12 (p: 11)
複製代碼
能夠看到打印的結果和預期一致,知足二叉搜索樹的性質。
二叉搜索樹刪除相比添加更加複雜並且須要以下二叉樹概念:
這裏沒法立刻直接實現二叉搜索樹的所有功能,之因此先實現二叉搜索樹的添加功能,是由於須要先建立一個能夠觀察節點元素規律的二叉樹,方便後面實現二叉樹的遍歷和打印。後面完成二叉樹的遍歷和一些其它基本概念的理解後,會繼續實現二叉搜索樹的其它功能。