資料結構系列:Objective-C實現二叉樹

weixin_34050427發表於2018-11-21
2177502-30a682081a100e2d.jpeg
二叉樹

本篇是我在學習二叉樹時做的總結,屬於面向我這種小白的文章

摘自《維基百科》

電腦科學中,二叉樹(英語:Binary tree)是每個節點最多隻有兩個分支(即不存在分支度大於2的節點)的樹結構。通常分支被稱作“左子樹”或“右子樹”。二叉樹的分支具有左右次序,不能隨意顛倒。

與普通樹不同,普通樹的節點個數至少為1,而二叉樹的節點個數可以為0;普通樹節點的最大分支度沒有限制,而二叉樹節點的最大分支度為2;普通樹的節點無左、右次序之分,而二叉樹的節點有左、右次序之分。

摘自《百度百科》

完全二叉樹:對於深度為K的,有n個結點的二叉樹,當且僅當其每一個結點都與深度為K的滿二叉樹中編號從1至n的結點一一對應時稱之為完全二叉樹。

滿二叉樹:一個二叉樹,如果每一個層的結點數都達到最大值,則這個二叉樹就是滿二叉樹。也就是說,如果一個二叉樹的層數為K,且結點總數是(2^k) -1 ,則它就是滿二叉樹。

廢話不多說,直接上原始碼。下面是Objective-C實現的核心程式碼以及呼叫原始碼。

@implementation  BinaryTree

/**
 新增節點

 @param item 節點根元素
 */
- (void)add:(NSInteger)item {
    
    Node *node = [[Node alloc] initWithItem: item];
    
    if (self.root == nil)
    {
        self.root =  node;
        
        return;
    }
    
    NSMutableArray *queue = [NSMutableArray array];
    
    [queue addObject: self.root];
    
    while (queue.count) {
        Node *curNode = queue.firstObject;

        [queue removeObjectAtIndex: 0];
        
        if (curNode.leftChild == nil)
        {
            curNode.leftChild = node;
            return;
        }
        else {
            [queue addObject: curNode.leftChild];
        }
        
        if (curNode.rightChild == nil)
        {
            curNode.rightChild = node;
            return;
        }
        else {
            [queue addObject: curNode.rightChild];
        }
    }
}

/**
 廣度遍歷
 */
- (void)breadthTraversal {
    if (self.root == nil) return;
    
    NSMutableArray *queue = [NSMutableArray array];
    
    [queue addObject: self.root];
    
    while (queue.count) {
        
        Node *curNode = queue.firstObject;
        [queue removeObjectAtIndex: 0];
        
        NSLog(@"%ld", curNode.element);
        
        if (curNode.leftChild != nil) [queue addObject: curNode.leftChild];
        
        if (curNode.rightChild != nil) [queue addObject: curNode.rightChild];
    }
}

/**
 深度遍歷:前序遍歷(先序遍歷)

 @param node 遍歷開始節點
 */
- (void)preorderTraversal:(Node *)node {
    if (node == nil) return;
    NSLog(@"%ld", node.element);
    [self preorderTraversal: node.leftChild];
    [self preorderTraversal: node.rightChild];
}

/**
 深度遍歷:中序遍歷

 @param node 遍歷開始節點
 */
- (void)inorderTraversal:(Node *)node {
    if (node == nil) return;
    [self inorderTraversal: node.leftChild];
    NSLog(@"%ld", node.element);
    [self inorderTraversal: node.rightChild];
}

/**
 深度遍歷:後序遍歷

 @param node 遍歷開始節點
 */
- (void)postorderTraversal:(Node *)node {
    if (node == nil) return;
    [self postorderTraversal: node.leftChild];
    [self postorderTraversal: node.rightChild];
    NSLog(@"%ld", node.element);
}

@end

實際使用案例


- (void)viewDidLoad {
    [super viewDidLoad];
    
    BinaryTree *tree = [[BinaryTree alloc] init];
    
    for (int i = 0; i < 10; ++i)
    {
        [tree add: i];
    }
    
    [tree breadthTraversal];              // 廣度遍歷:0 1 2 3 4 5 6 7 8 9
    
    [tree preorderTraversal: tree.root];  // 前序遍歷:0 1 3 7 8 4 9 2 5 6

    [tree inorderTraversal: tree.root];   // 中序遍歷:7 3 8 1 9 4 0 5 2 6

    [tree postorderTraversal: tree.root]; // 後序遍歷:7 8 3 9 4 1 5 6 2 0
}


寡人的思路

由於我們是為了簡單實現二叉樹,所以節點的元素型別用最基礎的NSInteger

新增方法:- (void)add:(NSInteger)item


新增的原則就是要讓整個二叉樹朝著滿二叉樹發展。

方法中的陣列 queue 定義成可變陣列,我們把它當做佇列使用,新增和刪除待處理的節點將按照先進先出的原則。在每次想要新增一個節點之前,都需要先把二叉樹的根節點新增到陣列的最前面,方便從頭查詢。

每次都從陣列中取出第一個元素,然後將這個元素從陣列中刪除,即處理一個就出佇列一個。如果取出的這個節點的左子樹不為空,說明這個節點有左子樹,那就把其左子樹放到待處理佇列(queue陣列)中。如果這個節點左子樹為空,說明這個節點沒有左子樹,那就把要新增的這個 node 賦值上去,新增動作至此結束。右子樹同理。

廣度遍歷:- (void)breadthTraversal

逐層列印元素的值

前序遍歷:- (void)preorderTraversal:(Node *)node

先看一下前序遍歷的原理圖:


2177502-7091e242e8f25d65.png

前序遍歷的原則是“根→左→右”。要把整個二叉樹看做一個整體,先去處理根,就是列印根元素。然後處理整個二叉樹的“左”,等到整個左子樹全部處理列印完,再去處理整個二叉樹的“右”。

處理整個二叉樹的“左”和整個二叉樹的“右”的時候依然要按照“根→左→右”的原則。這時就要把整個二叉樹的“左”和“右”分別重新看做一個整體。先處理這個整體的根,然後處理左子樹,最後處理右子樹。同理逐層向下。

圖中的紅色箭頭就是先序遍歷的處理順序。
根據“根→左→右”的原則:

  • 整個二叉樹的“根”是 節點:0第一個列印 0。這時“根”處理完,該處理“左”。

  • 整個二叉樹的“左”是以 節點:1 為根的二叉樹,包括 節點:3、4、7、8、9 。到了“左”這一部分,仍然要按照“根→左→右”的原則去處理。這部分的“根”是 節點:1第二個列印 1

    • “根”處理完之後,要處理這部分的“左”,就是以 節點:3 為根的二叉樹,包括 節點:7、8 。這部分還是按照“根→左→右”的原則,先處理這部分的“根”,也就是說第三個列印 3
    • “根”處理完之後,處理“左”。這個“左”就是 節點:7 ,到了 節點:7 這裡就再無子樹了,所以第四個列印 7之後就說明“左”處理完了。接下來應該處理“右”,同樣這個“右”即 節點:8 也再無子樹,在第五個列印 8之後就算處理完“右”。
    • 至此列印了 0、1、3、7、8 之後,我們剛剛處理好以 節點 3 為根的二叉樹也就是以 節點 1 為根的二叉樹的“左”。
    • 接下來就要處理以 節點 1 為根的二叉樹的“右”,這個“右”是一個以 節點:4 為根的二叉樹。先處理這個“右”的“根”,第六個列印 4,接下來處理以 節點: 4 為根的二叉樹的“左”,第七個列印 9
    • 到這裡我們列印了 0、1、3、7、8、4、9 ,我們處理完了以 節點: 4 為根的二叉樹也就是以 節點: 1 為根的二叉樹的“右”,那麼以 節點: 1 為根的二叉樹也已全部處理完。同時我們也已處理完以 節點: 0 為根的二叉樹的“左”。接下來同樣按照這個方式去處理以 節點: 1 為根的二叉樹的“右”。
  • 整個二叉樹的“右”是以 節點:2 為根的二叉樹,包括 節點:5、6 。道理一樣,就不再贅述了。

說實話,這麼分析是真的累

中序遍歷:- (void)inorderTraversal:(Node *)node

下面是中序遍歷的原理圖:

2177502-cb184b4ad46349b5.png

圖中灰色箭頭是用來幫助查詢應該處理的“真凶”的幕後路線,紅色箭頭就是表面的查詢路線。

中序遍歷的原則是“左→根→右”,所以我們要先找到“左”。

先整體再區域性。整體是以 節點:0 為根的二叉樹,逐層向下查詢“左”依次是:以 節點:1 為根的二叉樹、以 節點:3 為根的二叉樹、節點:7。所以到 節點:7 這裡才真正找到我們要最先處理的“左”,即第一個列印 7

列印了 7 之後,我們就處理完了以 節點:3 為根的二叉樹的“左”。接下來要處理它的“根”,所以第二個列印 3。然後要處理它的“右”,第三個列印 8

列印了 7、3、8 之後就處理完了以 節點:1 為根的二叉樹的“左”。接下來無限迴圈的找下去。。。 我不想再繼續下去了,快要迷糊了


後序遍歷:- (void)postorderTraversal:(Node *)node

下面是後序遍歷的原理圖:

2177502-38ea0efb18021774.png

各位這個後序遍歷我就不BB了,要不然顯得我在湊字數了,雖然這段話也是在湊字數


Github完整原始碼

GCBinaryTree


參考資料

二叉樹-維基百科,自由的百科全書
完全二叉樹_百度百科
滿二叉樹_百度百科

相關文章