作者 | 人魔七七

责编 | 胡巍巍

树的基本概念篇

前言

由于我们后面讲的一些结构有很多是树结构实现的比如堆,然后基于堆可以实现优先级队列,有界优先级队列等,所以我们先讲述树结构,我们可能常见到的是二叉树,但是还有一些其他的树的概念:比如二叉搜索树,AVL树,红黑树,B树,决策树等。以便于在特定场景下使用。

树的一些应用场景

  • CFBinaryHeap 这个类在iOS中你可能会见到,这是一个二叉搜索算法实现的一个二叉堆,后面的priority queues这个结构就是用这个二叉堆实现的。还可以实现二叉搜索树。对高效率的搜索和排序有帮助。

  • iOS 中视图的层级结构就是一个很形象的树。如下图所示:添加顺序是A,B,C。先添加的在数组中的索引小。

hit-test 逻辑:此方法通过hitTest:withEvent:从最后到第一个向其每个子视图发送消息来遍历接收者的子树,直到其中一个返回非nil值。

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
    if (!self.isUserInteractionEnabled || self.isHidden || self.alpha <= 0.01) {
        return nil;
    }
    if ([self pointInside:point withEvent:event]) {
        for (UIView *subview in [self.subviews reverseObjectEnumerator]) {
            CGPoint convertedPoint = [subview convertPoint:point fromView:self];
            UIView *hitTestView = [subview hitTest:convertedPoint withEvent:event];
            if (hitTestView) {
                return hitTestView;
            }
        }
        return self;
    }
    return nil;
}

采用reverse pre-order depth-first traversal algorithm遍历。首先访问根节点,然后从较高到较低的索引遍历其子树,这样做是为了快速遍历到我们需要的节点,试想如果从低到高遍历这个View,层级很多的情况下岂不是要遍历很多节点。如下图所示:

比如“View A.2”和“View B.1”都是重叠的。但由于“View B”的子视图索引高于“View A”,因此“View B”及其子视图呈现在“View A”及其子视图上方。因此,当用户的手指在与“视图A.2”重叠的区域中触摸“视图B.1”时,应通过命中测试返回“视图B.1”。

打印当前View下所有子View采用了递归遍历并打印

- (void)listSubviewsOfView:(UIView *)view {

NSArray *subviews = [view subviews];

if ([subviews count] == 0) return;

for (UIView *subview in subviews)
    {

NSLog(@"%@", subview);

[self listSubviewsOfView:subview];
    }
}

  • 其他比如人工智能下国际象棋采用决策树来解决。

  • 数据库中我们需要高效的访问,插入删除等操作。为了降低磁盘IO操作开销,就用到了B树。

  • 用二叉树表示数学表达式我们叫做表达式树。还记得之前我们用栈结构结合后缀表达式来计算数学表达式吗?其实下图可以通过前序中序后序遍历方式得到前后中不同的表达式。当然后缀表达式适合计算表达式,因为它很容易通过栈结构来计算。

上图后序遍历得到后缀表达式:( ((70 10 - )32 / ) (24 13 + ) X )

  • 堆排序,我们利用二叉堆来实现堆排序,堆又是二叉树来实现的,近似于完全二叉树的结构。

  • 霍夫曼编码是数据编码的的一种算法,用于JPEG和zip等压缩图像或者文件。该方法利用霍夫曼树来压缩一组数据,霍夫曼树是一颗二叉树。

二叉树介绍篇

一张图来描述Binary Tree:

二叉树的节点最大分支度是2,也说明每个节点最多拥有2个子节点,范围是[0-2]。

Binary Tree的几个常见类型:

  • A degenerate (or pathological) tree。(树的每个节点只有一个子节点或者是右孩子或者是左孩子,这时候这个树就和链表性能差不多了。)

  • Full Binary Tree (树的任何一个节点都有0或者2个孩子节点。或者这样定义树的任何一个非叶子节点都有两个孩子节点)

  • Complete Binary Tree(可能除了树的最后一层其它层级的每个节点都有左右孩子节点,最后一层要么是满的要么节点都靠左边)

  • Perfect Binary Tree (它是一个这样的二叉树,他所有的非叶子节点都有左右子节点,并且所有的叶子节点都在同一层级)

和Binary Tree有关的一些公式:

  • 节点数和二叉树树Height的关系,假如h是树的Height,n是树节点个数。那么Min Nodes(n = h+1),Max Nodes(2h+1-1)。看下图例子,很容易推导出Min Nodes(n = h+1)。

  • 下面我们推导下Max Nodes。上图第三种情况h = 3,Max Nodes = 1 +2 + 22+ 23 = 15,也就是Max Nodes = 1 +2 + 22+ 23 + ….+ 2h= ,也就是等比数列求和,如下图:

代入求和:Max Nodes = 1 +2 + 22+ 23 + ….+ 2h=2h+1-1

反过来可以很容易推导出Min Height (h = Log2(n+1)-1),Max Height(h = n- 1)。

  • 如果是full binary tree那么节点数和树Height的关系又是什么呢?
    推导过程可以参考上面的步骤,Min Nodes(n = 2h+1),Max Nodes(2h+1-1),反过来可以很容易推导出Min Height (h = Log2(n+1)-1),Max Height(h =)。

  • 第i层至多拥有2i-1个节点,最少有1个节点。从下图可以很容易看出来。

  • 度为0的节点数n1和度为2节点数n2的关系。n1 = n2 + 1。看下图

二叉树的存储方式:

  • Array Representation

  • Linked Representation

Array Representation:

二叉树可以被以广度优先的顺序作为隐式数据结构存储在数组中。注意的是如果这个二叉树是complete binary tree,这些不会浪费空间,但是如果对于A degenerate (or pathological) tree这种高度很大的树就很浪费空间,可以参考后面根据这个存储方式判断这个树是不是complete binary tree的介绍。这种存储方法通常也用在binary heaps。

举例:找E的父节点,E的索引是5,那么Parent = i/2 = 5/2 = 2.5,向下取整就是2,对应的就是B。反之假如找A的左右孩子,A的索引是1,那么左孩子索引就是2对应B,右孩子索引就是3对应C。

注意:Parent的索引如果有存在小数情况是向下取整。

上三个图中1,2元素之间没有空白的空间是complete binary tree,图3元素之间有空白的空间说明不是complete binary tree。

Linked Representation:

@interface DSTreeNode : NSObject

@property (nonatomic, strong) NSObject   *object;
@property (nonatomic, strong) DSTreeNode *leftChild;
@property (nonatomic, strong) DSTreeNode *rightChild;
@property (nonatomic, strong) DSTreeNode *parent;
@property (nonatomic, assign) SEL         compareSelector;

- (void)printDescription;
//是否是左还是结点
- (BOOL)isLeftChildOfParent;

@end

这种存储二叉树方法浪费了不少内存,由于那些节点的左右指针(为null或者指向某些节点)。

二叉树的周游算法篇

  • 前序遍历:visit(node),preorder(left Subtree), preorder(right Subtree)。

  • 中序遍历:in-order(left Subtree),visit(node),in-order(right Subtree)。

  • 后序遍历:post-order(left Subtree),post-order(right Subtree),visit(node)。

  • 层级遍历:一层层访问每个节点。

通过上述四种方式遍历二叉树的每个节点。

练习周游算法的技巧 1:

思路:一般我们习惯 ,根节点-左节点-右节点,这样的模型,我们就把例如上图A的左子树当做一个块,类似一个大节点用括号圈起来,同样的右子树也这样做。然后每个块里做前中后遍历。

  • 前序遍历。A,(B,D,E),(C,F,G)。得到结果是 A,B,D,E,C,F,G 。

  • 中序遍历。(D,B,E),A,(F,C,G)。得到的结果是 D,B,E,A,F,C,G 。

  • 后序遍历。(D,E,B),(F,G,C),A。得到的结果是 D,E,B,F,G,C,A 。

  • 层级遍历。 A,B,C,D,E,F,G 。

练习周游算法的技巧 2:

前序遍历思路:每个节点从左边画线一直到底部这个线,然后按照从左到右的顺序读取节点。 结果是:A,B,D,E,C,F,G 。

中序遍历思路:每个节点从中间画线到底部这个线,然后按照从左到右的顺序读取节点。 结果是 D,B,E,A,F,C,G 。

后序遍历思路:每个节点从右边画线到底部这条线,然后从左到右的顺序读取节点。 结果是 D,E,B,F,G,C,A 。

练习周游算法的技巧 3:

前序遍历思路:从每个节点左边画出一个线,然后从根结点开始转一圈,经过每个节点和树的分支,包裹这个树。经过这些短线的顺序就是结果。A,B,D,E,C,F,G 。

中序遍历思路:从每个节点底部边画出一个线,然后从根结点开始转一圈,经过每个节点和树的分支,包裹这个树。经过这些短线的顺序就是结果。D,B,E,A,F,C,G 。

后序遍历思路:从每个节点右边画出一个线,然后从根结点开始转一圈,经过每个节点和树的分支,包裹这个树。经过这些短线的顺序就是结果。D,E,B,F,G,C,A 。

周游算法延伸:

  • 前序遍历,中序遍历,后续遍历的思想是按照深度优先的顺序遍历的。层级遍历的思想是按照广度优先的顺序遍历的。

  • 由于要遍历树的每个节点因此时间复杂度是O(n)。

  • 广度优先遍历思想的层级遍历需要的额外的空间是O(w),w是这个二叉树的最大的宽,比如Perfect Binary Tree这种情况下最大节点在最后一层,第i层至多拥有2i-1个节点,因此需要额外空间O(Ceil(n/2));深度优先遍历思想的其他三种方式需要额外空间是O(h),这个h是二叉树的最大高度,比如一个平衡树h是Log2(n) ,但是对于极不平衡的左倾斜或者右倾斜树来说h就是n 。所以在最坏的情况下,两者所需的额外空间是O(n)。但最坏的情况发生在不同类型的树木上,因此针对不同种类不同性质的树需要的额外空间有不尽相同。从以上可以明显看出,当树更平衡时,广度优先遍历思想的层级遍历所需的额外空间可能更多,并且当树不太平衡时,深度优先遍历思想的其他三种遍历方式的额外空间可能更多。

二叉树的实现篇

这节主要介绍二叉树的代码实现,我们讲述Linked Representation的实现,主要包含下面几个操作。

  • 构建

  • 插入

  • 查找

  • 前序,中序,后续,层级遍历

节点类:

从上图可以看出,每个节点除了本身以外,还得有一个父子以及左右孩子节点信息。因此需要一个节点类。主要代码实现如下:

@interface DSTreeNode : NSObject

@property (nonatomic, strong) NSObject   *object;
@property (nonatomic, strong) DSTreeNode *leftChild;
@property (nonatomic, strong) DSTreeNode *rightChild;
@property (nonatomic, strong) DSTreeNode *parent;
@property (nonatomic, assign) SEL         compareSelector;

- (void)printDescription;
//是否是左还是结点
- (BOOL)isLeftChildOfParent;

@end

构建:

对于二叉树的创建我们初始化一个根节点的方式创建,如下代码实现:

- (instancetype)initWithObject:(NSObject *)object
{
    if (self = [super init]) {
        _root            = [[DSTreeNode alloc] init];
        self.root.object = object;
    }

return self;
}

插入:

以插入节点的方式构建整个二叉树如下代码:

//插入结点
- (BOOL)insertNode:(NSObject *)node parent:(NSObject *)parent isLeftChild:(BOOL)value
{
    DSTreeNode *treeNode = [[DSTreeNode alloc] init];
    treeNode.object = node;
    DSTreeNode *parentNode = [self find:parent];
    //1
    if (value == true && parentNode.leftChild == nil) {
        //2
        treeNode.parent = parentNode;
        //3
        parentNode.leftChild = treeNode;
    }
    //4
    else if (parentNode.rightChild == nil) {
        treeNode.parent = parentNode;
        parentNode.rightChild = treeNode;
    }
    //5
    else {
        NSAssert(parentNode.leftChild != nil || parentNode.rightChild != nil, @"Can't insert into parent node!");
        return false;
    }
    return true;
}

代码解释:

  1. 如果插入的位置是当前节点的左孩子并且左孩子结点不存在可以插入。

  2. 被插入的节点的parent指针指向当前节点,此处是必须的,不然这个树分支就断了,也就不能构成完整的树。

  3. 当前节点左孩子指针指向被插入的节点,此处是必须的,和第二步原因一样。

  4. 否则插入的是右孩子节点。

  5. 如果某个节点的左右孩子节点都存在则提示不能插入的信息。

查找:

查找某个节点

- (DSTreeNode *)find:(NSObject *)object
{
    //1 
    DSQueue*queue = [[DSQueue alloc] init];
    [queue enqueue:self.root];
    DSTreeNode *node;
    //2
    while (![queue isEmpty]) {
        node = [queue dequeue];
        if ([node.object isEqualTo:object]) {
            return node;
        }
        if (node.leftChild) {
            [queue enqueue:node.leftChild];
        }
        if (node.rightChild) {
            [queue enqueue:node.rightChild];
        }
    }
    return nil;
}

  • 利用队列先进先出特性遍历每个结点

  • 注意这个遍历的顺序是层级遍历顺序

前序,中序,后续,层级遍历:

层级遍历的思路和上述查找的思路类似。前中后序遍历的思路利用递归的思路实现,然后按照之前介绍二叉树遍历算法的思路就可以实现了。前序遍历的代码如下:

//如果当前根结点存在则前序遍历这个树
- (void)preOrderTraversal
{
    if (self.root) {
        [DSBinaryTree preOrderTraversalRecursive:self.root];
    }
}

//递归的遍历并打印树 顺序是根 左 右
+ (void)preOrderTraversalRecursive:(DSTreeNode *)node
{
    if (node) {
        NSLog(@"%@",node.object);
        [DSBinaryTree preOrderTraversalRecursive:node.leftChild];
        [DSBinaryTree preOrderTraversalRecursive:node.rightChild];
    }
}

二叉树算法实战篇

题目大意:

Given a binary tree, return all root-to-leaf paths.

For example, given the following binary tree:

1
 /   \
2     3
 \
  5

All root-to-leaf paths are:

["1->2->5", "1->3"]

灵感思路:

给我们一个二叉树,让我们返回所有根到叶节点的路径。我们可以采用递归的思路,不停的DFS到叶结点,如果遇到叶结点的时候,那么此时一条完整的路径已经形成,我们加上当前的叶结点后变成的完整路径放到数组中。

需要注意的是对空节点的判断,以及递归函数回溯时候对一些对象的影响。

主要代码:

- (void)printPathsRecurTreeNode:(DSTreeNode *)treeNode path:(NSString *)path results:(NSMutableArray <NSString *>*)results
{

//1
    if (treeNode == nil) {
        return;
    }
    //2
    if (treeNode.leftChild == nil && treeNode.rightChild == nil)
    {
        NSString *resultsStr = [NSString stringWithFormat:@"%@%@",path,treeNode.object];
        [results addObject:resultsStr];

}
    else
    {
        //3
        if (treeNode.leftChild != nil)
        {
            NSString *resultsStr = [NSString stringWithFormat:@"%@%@",path,[NSString stringWithFormat:@"%@->",treeNode.object]];

[self printPathsRecurTreeNode:treeNode.leftChild path:resultsStr results:results];
        }
        //4
        if (treeNode.rightChild != nil )
        {
            NSString *resultsStr = [NSString stringWithFormat:@"%@%@",path,[NSString stringWithFormat:@"%@->",treeNode.object]];
            [self printPathsRecurTreeNode:treeNode.rightChild path:resultsStr results:results];
        }
    }

}

代码解释:

  1. 如果节点是空则返回。

  2. 如果当前节点是叶子节点则把这个完整路径加到数组里。

  3. 如果当前节点存在左孩子节点,则继续DFS直到叶子节点。

  4. 如果当前节点存在右孩子节点,则继续DFS直到叶子节点。

GitHubDemo:

https://github.com/renmoqiqi/100-Days-Of-iOS-DataStructure-Algorithm

https://github.com/renmoqiqi/100-Days-Of-iOS-DataStructure-Algorithm/tree/master/Day16

参考链接:

  • http://smnh.me/hit-testing-in-ios/

  • https://zh.wikipedia.org/wiki/%E7%AD%89%E6%AF%94%E6%95%B0%E5%88%97

作者简介:人魔七七,移动端开发工程师。热爱技术学习总结分享,对技术以及产品体验有追求。业余时间喜欢产品和设计。目前就职于知名互联网公司。

本文系作者投稿,版权归作者所有。

程序员转行学什么语言?

https://edu.csdn.net/topic/ai30?utm_source=csdn_bw

【END】

作为码一代,想教码二代却无从下手:

听说少儿编程很火,可它有哪些好处呢?

孩子多大开始学习比较好呢?又该如何学习呢?

最新的编程教育政策又有哪些呢?

下面给大家介绍CSDN新成员:极客宝宝(ID:geek_baby)

戳他了解更多↓↓↓

 热 文 推 荐 

☞ 英特尔将开源进行到底!

☞ 这家公司的 IoT ,你可千万别低估!

Windows 多个系统版本惊现大漏洞,攻击者可随意操作程序!

☞ 普通家庭走出信息学才子,抱病参赛夺世界信奥亚军 | 人物志

☞ 程序员专属小情话,哎呦,不错哦!| 程序员有话说

Rust今天4岁啦, 为什么越来越多的知名项目用Rust来开发?

腾讯面试:一条SQL语句执行得很慢的原因有哪些?

☞ 商汤“变法”:推中小学AI教材,mini自驾车,要打造AI时代的「清明上河图」

☞ 刺激!华为程序员年薪200万 ?真相让人心酸!

你点的每个“在看”,我都认真当成了喜欢

万字长文详解二叉树算法,再也不怕面试了!| 技术头条相关推荐

  1. 【技术综述】万字长文详解Faster RCNN源代码

    文章首发于微信公众号<有三AI> [技术综述]万字长文详解Faster RCNN源代码 作为深度学习算法工程师,如果你想提升C++水平,就去研究caffe源代码,如果你想提升python水 ...

  2. 边缘计算:万字长文详解高通SNPE inception_v3安卓端DSP推理加速实战

    本文是在以下文章的基础上编写,关于SNPE环境部署和服务器端推理可以参考上一篇文章: 边缘计算:万字长文详解高通SNPE inception_v3推理实战_seaside2003的博客-CSDN博客 ...

  3. 万字长文详解文本抽取:从算法理论到实践

    导读:"达观杯"文本智能信息抽取挑战赛已吸引来自中.美.英.法.德等26个国家和地区的2400余名选手参赛,目前仍在火热进行中(点击阅读原文进入比赛页面,QQ群见上图或文末二维码) ...

  4. ​万字长文详解文本抽取:从算法理论到实践(附“达观杯”官方baseline实现解析及答疑)...

    [ 导读 ]"达观杯"文本智能信息抽取挑战赛已吸引来自中.美.英.法.德等26个国家和地区的2400余名选手参赛,目前仍在火热进行中(点击"阅读原文"进入比赛页 ...

  5. 万字长文详解如何用Python玩转OpenGL | CSDN 博文精选

    作者 | 天元浪子 来源 | CSDN博文精选 [编者按]OpenGL(开放式图形库),用于渲染 2D.3D 矢量图形的跨语言.跨平台的应用程序编程接口,C.C++.Python.Java等语言都能支 ...

  6. 万字长文详解如何用 Python 玩转 OpenGL | CSDN 博文精选

    作者 | 天元浪子 责编 | 伍杏玲 出品 | CSDN 博客 [CSDN 编者按]OpenGL(开放式图形库),用于渲染 2D.3D 矢量图形的跨语言.跨平台的应用程序编程接口,C.C++.Pyth ...

  7. 万字长文详解 Go 程序是怎样跑起来的?| CSDN 博文精选

    作者 | qcrao 责编 | 屠敏 出品 | CSDN博客 刚开始写这篇文章的时候,目标非常大,想要探索 Go 程序的一生:编码.编译.汇编.链接.运行.退出.它的每一步具体如何进行,力图弄清 Go ...

  8. 万字长文详解​场景金融风险识别与管控

    本文围绕场景金融的"三层风险金字塔"风险识别与管控,先对场景金融进行多视角分析,然后从场景"四要素"(流量.数据.交易.信用)入手,进行场景分类(泛场景.浅场景 ...

  9. 万字长文详解:2023年手机银行MAU和AUM双增实操宝典

    随着金融科技不断发展,银行向数字化转型加速推进.近几年,手机银行应用(Application,简称App)逐渐成为商业银行大零售板块各展风采的重要阵地. 截至2022年12月手机银行服务应用活跃人数已 ...

最新文章

  1. BZOJ2631tree——LCT
  2. 网络分布式软件bonic清除
  3. 最好用的在线思维导图软件
  4. oracle用户创建及权限设置
  5. python 网络爬虫介绍
  6. SAP 电商云 Spartacus UI Component 级别的延迟加载实现(Lazy Load)
  7. 【数据泵】EXPDP导出表结构
  8. mysql 之 sql管理数据 二
  9. css将空的div撑开,如何使用css将空的浮动div伸展到可用的全高度?
  10. 自动驾驶感知-车道线系列(二)——Canny边缘检测
  11. 使用K近邻对iris数据集进行分类
  12. python制作热力图_python绘制热力图
  13. s5p4418的uboot网络无法使用问题解决
  14. 360安全卫士企业版本 跳过卸载保护密码
  15. JSR 168 翻译03
  16. filevault(电子仓库)自动切换文件夹以及文档最大值设置
  17. “河南旦”的四个坟墓的故事·《晚唱·贾平凹》
  18. 简单的复习下箭头函式
  19. SpringCloud的实用篇
  20. 如何做好自己的职业规划?

热门文章

  1. sobel,canny(可滑动调节阈值)边缘检测算法opencv-python实战
  2. mysql执行计划explain介绍_Mysql执行计划EXPLAIN详解
  3. mysql 5.5主从同步_MySQL 5.5主从同步
  4. 人脸关键点标注工具_谈谈人脸关键点的江湖
  5. 【图像融合】主成分分析PCA
  6. 从入门到入土:python爬虫|SCU每日打卡自动填写|测试训练|
  7. 被隐藏的或许才是金子
  8. 华为今年不发布Mate系列新机;一加宣布与OPPO合并:将成为OPPO旗下独立品牌;Gradle 7.1 发布|极客头条...
  9. 基于Flink CDC打通数据实时入湖
  10. 一个合格的ACMer的代码当中,都藏着哪些秘密?