前面实现的全部链表结构都有一个共同的特征,就是每个链表的节点都只有一个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)
复制代码
能够看到打印的结果和预期一致,知足二叉搜索树的性质。
二叉搜索树删除相比添加更加复杂并且须要以下二叉树概念:
这里没法立刻直接实现二叉搜索树的所有功能,之因此先实现二叉搜索树的添加功能,是由于须要先建立一个能够观察节点元素规律的二叉树,方便后面实现二叉树的遍历和打印。后面完成二叉树的遍历和一些其它基本概念的理解后,会继续实现二叉搜索树的其它功能。