iOS數據結構與算法實戰 二叉樹總結篇

樹的基本概念篇

前言

因爲咱們後面講的一些結構有不少是樹結構實現的好比堆,而後基於堆能夠實現優先級隊列,有界優先級隊列等,因此咱們先講述樹結構,咱們可能常見到的是二叉樹,可是還有一些其餘的樹的概念:好比二叉搜索樹,AVL樹,紅黑樹,B樹,決策樹等。以便於在特定場景下使用。node

樹的一些應用場景

  • CFBinaryHeap 這個類在iOS中你可能會見到,這是一個二叉搜索算法實現的一個二叉堆,後面的priority queues這個結構就是用這個二叉堆實現的。還能夠實現二叉搜索樹。對高效率的搜索和排序有幫助。
  • iOS 中視圖的層級結構就是一個很形象的樹。以下圖所示:添加順序是A,B,C。先添加的在數組中的索引小。

hit-test 邏輯:此方法經過hitTest:withEvent:從最後到第一個向其每一個子視圖發送消息來遍歷接收者的子樹,直到其中一個返回非nil值。ios

代碼:git

- (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,層級不少的狀況下豈不是要遍歷不少節點。以下圖所示:github

好比「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]。bash

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 + 2 2+ 2 3 + ....+ 2 h=2 h+1-1

等比數列求和能夠參考以下連接: zh.wikipedia.org/wiki/等比數列ide

反過來能夠很容易推導出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 = \frac{n-1}{2})。

  • 第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的索引若是有存在小數狀況是向下取整。

下面咱們看怎麼根據這個表示方法判斷是否是complete binary tree。

上三個圖中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:

github.com/renmoqiqi/1…

github.com/renmoqiqi/1…

參考連接:

smnh.me/hit-testing…

zh.wikipedia.org/wiki/等比數列

轉載於:https://juejin.im/post/5cd8c8dbf265da037129bcfa