數據結構9-二叉樹和二叉搜索樹

二叉樹

基本概念

前面實現的全部鏈表結構都有一個共同的特徵,就是每個鏈表的節點都只有一個next指針指向下一個節點,是一條線性的數據結構。二叉樹和線性表的不一樣之處就是二叉樹的每個節點有兩個指針分別指向兩個子節點。node

鏈表:git

二叉樹:github

下面首先了解一下二叉樹的基本概念:數組

  • 節點
    • 構成二叉樹的每個節點。
  • 根節點
    • 二叉樹起始的節點,如上圖中的①。
  • 空樹
    • 節點數量爲0的樹。
  • 子節點
    • 如上圖,④的子節點是⑥和⑦。
  • 父節點
    • 如上圖,④的父節點是②,根節點①沒有父節點。
  • 子樹
    • 如上圖,①兩個子節點分別是②和③。 以②爲根節點還能夠當作一棵二叉樹,這棵二叉樹能夠當作①的子樹。②在左邊,稱爲左子樹。以③爲根節點的子樹稱爲右子樹。
  • 節點的度
    • 節點子樹的個數。如上圖,①的度爲2,②的度爲1。
  • 葉子節點
    • 度爲0的節點,如上圖⑥⑦⑧⑨。

二叉樹的基本結構

經過上面介紹能夠看到,二叉樹是由節點構成的,這點和鏈表很是像。並且是由根節點開始的,就像鏈表是由頭節點起始的同樣。每個節點由父節點指向它,而它又分別指向它的兩個子節點。兩個子節點一個在左邊,一個在右邊。因此二叉樹的節點起碼須要以下的屬性: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

上面雖然實現並知足了二叉樹的基本結構,不過並無實際使用價值。可是當二叉樹知足以下性質就能夠用實用價值了:

  • 任何一個節點的值都大於其左子樹全部節點的值。
  • 任何一個節點的值有小於其右子樹全部節點的值。

而知足如上條件的二叉樹就是一棵二叉搜索樹(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。既然二叉樹開始只能獲取到根節點,咱們搜索也是從根節點開始的:

  • 第一步:12 != 7 && 12 > 7,往下搜索節點7的右子樹。
  • 第二步:12 != 9 && 12 > 9,往下搜索節點9的右子樹。
  • 第三步:12 != 11 && 12 > 11,往下搜索節點11的右子樹。
  • 第四步:12 == 12,找到了12。

能夠發現,只須要4步就可以找到12,經過二叉搜索樹能夠大大提升搜索數據的效率。下面咱們就本身實現基於剛剛封裝的二叉樹的基礎上,實現一個二叉搜索樹。

二叉搜索樹基本結構

首先二叉搜索樹也是二叉樹,咱們的二叉搜索樹直接繼承剛剛封裝的二叉樹就能夠,同時爲了內部方便的存儲和記錄二叉樹的節點數量,同鏈表的設計同樣,咱們須要二叉樹額外添加一個_size屬性記錄當前二叉樹存書元素的個數,這個屬性並不是二叉搜索樹獨有的,而是因此二叉樹共有的,因此放在二叉樹類中。

/// 二叉樹
@interface JKRBinaryTree<ObjectType> : NSObject {
@protected
    NSUInteger _size;
    JKRBinaryTreeNode *_root;
}

@end
複製代碼

同時因爲二叉搜索樹的須要比較節點元素大小的性質,二叉搜索樹添加的元素必定是須要比較大小且可以比較大小的,而基於面向對象的特性,存儲的元素必定是對象,咱們須要告訴二叉搜索樹如何比較存入對象的大小,這裏咱們在二叉搜樹中定一個block:

typedef NSInteger(^jkrbinarytree_compareBlock)(id e1, id e2);
複製代碼

經過block返回的值判斷大小:

  • e1 = e2:返回值爲0
  • e1 > e2:返回值大於0
  • e1 < e2:返回值小於0

二叉搜索樹須要保存一個外部傳入的比較大小的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;
}
複製代碼

比較元素大小

二叉搜索樹的查找離不開比較邏輯,這裏先實現元素比較的私有方法:

比較的邏輯以下:

  • 首先判斷二叉樹是否傳入了自定義對象比較的block,若是有則經過block進行比較。
  • 若是沒有傳入block,判斷添加的對象類型是否實現了咱們自定義的比較方法。
  • 若是沒有傳入block,也沒有實現自定義的比較方法,判斷添加的對象類型是否支持系統的默認比較方法。
  • 上述三種都不知足,則添加的元素沒法比較大小,中斷並報錯。
- (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!");
    }
}
複製代碼

添加元素須要如下的判斷邏輯:

  • 第一步,判斷元素是否爲空,不爲空進行第二步。
  • 第二步,當前二叉樹是不是空樹,若是是空樹則根據傳入元素建立一個新節點,並將二叉樹的根節點指向新節點,_size++。不然進入須要進行遍歷查找比較操做,從根節點開始遍歷。
  • 第三步,比較添加元素和當前遍歷節點元素的大小,若是小於進入第四步,大於進入第五步,相等進入第六步。
  • 第四步,取出該節點的左子節點,若是左子節點存在,則返回第三步比較左子節點。不然根據添加元素建立新節點,將新節點作爲該節點的左子節點,_size++。
  • 第五步,取出該節點的右子節點,若是右子節點存在,則返回第三步比較右子節點。不然根據添加元素建立新節點,將新節點作爲該節點的右子節點,_size++。
  • 第六步,直接將添加的元素替換當前節點的保存的元素。
- (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++;
}
複製代碼

經過元素獲取對應節點

在二叉搜索樹開始的分析時已經模擬一遍查找邏輯,經過元素獲取節點和上面添加元素的比較邏輯很是類似:

  • 第一步,獲取根節點,並從根節點開始遍歷。
  • 第二步,若是當前遍歷的節點爲空,則沒有找到對應的節點返回nil,不然進入第三步。
  • 第三步,比較查找的元素和當前遍歷節點的元素,若是相等,則返回該節點。大於,遍歷右子節點返回第二步。不然遍歷左子節點返回第二步。
- (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)
複製代碼

能夠看到打印的結果和預期一致,知足二叉搜索樹的性質。

二叉搜索樹的其它功能

二叉搜索樹刪除相比添加更加複雜並且須要以下二叉樹概念:

這裏沒法立刻直接實現二叉搜索樹的所有功能,之因此先實現二叉搜索樹的添加功能,是由於須要先建立一個能夠觀察節點元素規律的二叉樹,方便後面實現二叉樹的遍歷和打印。後面完成二叉樹的遍歷和一些其它基本概念的理解後,會繼續實現二叉搜索樹的其它功能。

源碼

點擊查看源碼

相關文章
相關標籤/搜索