資料結構和演算法面試題系列—二叉樹基礎

ssjhust發表於2018-09-20

這個系列是我多年前找工作時對資料結構和演算法總結,其中有基礎部分,也有各大公司的經典的面試題,最早釋出在CSDN。現整理為一個系列給需要的朋友參考,如有錯誤,歡迎指正。本系列完整程式碼地址在 這裡

0 概述

在說二叉樹前,先來看看什麼是樹。樹中基本單位是結點,結點之間的連結,稱為分支。一棵樹最上面的結點稱之為根節點,而下面的結點為子結點。一個結點可以有0個或多個子結點,沒有子結點的結點我們稱之為葉結點。

二叉樹是指子結點數目不超過2個的樹,它是一種很經典的資料結構。而二叉搜尋樹(BST)是有序的二叉樹,BST需要滿足如下條件:

  • 若任意結點的左子樹不空,則左子樹上所有節點的值均小於它的根節點的值;
  • 若任意結點的右子樹不空,則右子樹上所有節點的值均大於或等於它的根節點的值;(有些書裡面定義為BST不能有相同值結點,本文將相同值結點插入到右子樹)
  • 任意結點的左、右子樹也分別為二叉查詢樹;

本文接下來會從定義,二叉搜尋樹的增刪查以及二叉樹的遞迴和非遞迴遍歷進行整理。 下一篇文章會對二叉樹相關的經典面試題進行全面解析,本文程式碼在 這裡

1 定義

我們先定義一個二叉樹的結點,如下:

typedef struct BTNode {
    int value;
    struct BTNode *left;
    struct BTNode *right;
} BTNode;
複製程式碼

其中 value 儲存值,leftright 指標分別指向左右子結點。二叉搜尋樹跟二叉樹可以使用同一個結構,只是在插入或者查詢時會有不同。

2 基本操作

接下來看看二叉樹和二叉查詢樹的一些基本操作,包括BST插入結點,BST查詢結點,BST最大值和最小值,二叉樹結點數目和高度等。二叉查詢樹(BST)特有的操作都在函式前加了 bst 字首區分,其他函式則是二叉樹通用的。

1) 建立結點

分配記憶體,初始化值即可。


/**
 * 建立BTNode
 */
BTNode *newNode(int value)
{
    BTNode *node = (BTNode *)malloc(sizeof(BTNode));
    node->value = value;
    node->left = node->right = NULL;
    return node;
}
複製程式碼

2) BST 插入結點

插入結點可以用遞迴或者非遞迴實現,如果待插入值比根節點值大,則插入到右子樹中,否則插入到左子樹中。如下圖所示(圖來自參考資料1,2,3):

BST插入結點

/**
 * BST中插入值,遞迴方法
 */
/**
 * BST中插入結點,遞迴方法
 */
BTNode *bstInsert(BTNode *root, int value)
{
    if (!root)
        return newNode(value);

    if (root->value > value) {
        root->left = bstInsert(root->left, value);
    } else {
        root->right = bstInsert(root->right, value);
    }
    return root;
}

/**
 * BST中插入結點,非遞迴方法
 */
BTNode *bstInsertIter(BTNode *root, int value)
{
    BTNode *node = newNode(value);

    if (!root)
        return node;

    BTNode *current = root, *parent = NULL;

    while (current) {
        parent = current;
        if (current->value > value)
            current = current->left;
        else
            current = current->right;
    }

    if (parent->value >= value)
        parent->left = node;
    else
        parent->right = node;

    return root;
}
複製程式碼

3) BST 刪除結點

刪除結點稍微複雜一點,要考慮3種情況:

  • 刪除的是葉子結點,好辦,移除該結點並將該葉子結點的父結點的 left 或者 right 指標置空即可。

BST刪除結點-葉子結點

  • 刪除的結點有兩個子結點,則需要找到該結點左子樹的最大結點(使用後面的bstSearchIter 函式),並將其值替換到待刪除結點中,然後遞迴呼叫刪除函式刪除該結點左子樹最大結點即可。

BST刪除結點-有兩個子結點

  • 刪除的結點只有一個子結點,則移除該結點並將其子結點的值填充到該刪除結點即可(需要判斷是左孩子還是右孩子結點)。

BST刪除結點-一個子結點

/**
 * BST中刪除結點
 */
BTNode *bstDelete(BTNode *root, int value)
{
    BTNode *parent = NULL, *current = root;
    BTNode *node = bstSearchIter(root, &parent, value);
    if (!node) {
        printf("Value not found\n");
        return root;
    }

    if (!node->left && !node->right) {
        // 情況1:待刪除結點是葉子結點
        if (node != root) {
            if (parent->left == node) {
                parent->left = NULL;
            } else {
                parent->right = NULL;
            }
        } else {
            root = NULL;
        }
        free(node);
    } else if (node->left && node->right) {
        // 情況2:待刪除結點有兩個子結點
        BTNode *predecessor = bstMax(node->left);
        bstDelete(root, predecessor->value);
        node->value = predecessor->value;
    } else {
        // 情況3:待刪除結點只有一個子結點
        BTNode *child = (node->left) ? node->left : node->right;
        if (node != root) {
            if (node == parent->left)
                parent->left = child;
            else
                parent->right = child;
        } else {
            root = child;
        }
        free(node);
    }
    return root;
}
複製程式碼

4) BST 查詢結點

注意在非遞迴查詢中會將父結點也記錄下來。

BST查詢結點

/**
 * BST查詢結點-遞迴
 */
BTNode *bstSearch(BTNode *root, int value)
{
    if (!root) return NULL; 

    if (root->value == value) {
        return root;
    } else if (root->value > value) {
        return bstSearch(root->left, value);
    } else {
        return bstSearch(root->left, value);
    }
}

/**
 * BST查詢結點-非遞迴
 */
BTNode *bstSearchIter(BTNode *root, BTNode **parent, int value)
{
    if (!root) return NULL;

    BTNode *current = root;

    while (current && current->value != value) {
        *parent = current;
        if (current->value > value)
            current = current->left;
        else
            current = current->right;
    }

    return current;
}
複製程式碼

5)BST 最小值結點和最大值結點

最小值結點從左子樹遞迴查詢,最大值結點從右子樹遞迴找。

/**
 * BST最小值結點
 */
BTNode *bstMin(BTNode *root)
{
    if (!root->left)
        return root;

    return bstMin(root->left);
}

/**
 * BST最大值結點
 */
BTNode *bstMax(BTNode *root)
{
    if (!root->right)
        return root;

    return bstMax(root->right);
}

複製程式碼

6)二叉樹結點數目和高度

/**
 * 二叉樹結點數目
 */
int btSize(BTNode *root)
{
    if (!root) return 0;
    
    return btSize(root->left) + btSize(root->right) + 1;
}

/**
 * 二叉樹高度
 */
int btHeight(BTNode *root)
{
    if (!root) return 0;

    int leftHeight = btHeight(root->left);
    int rightHeight = btHeight(root->right);
    int maxHeight = leftHeight > rightHeight ? leftHeight+1 : rightHeight+1;
    return maxHeight;
}
複製程式碼

3 二叉樹遍歷

遞迴遍歷-先序、中序、後序、層序

二叉樹遍歷的遞迴實現比較簡單,直接給出程式碼。這裡值得一提的是層序遍歷,先是計算了二叉樹的高度,然後呼叫的輔助函式依次遍歷每一層的結點,這種方式比較容易理解,雖然在時間複雜度上會高一些。

/**
 * 二叉樹先序遍歷
 */
void preOrder(BTNode *root)
{
    if (!root) return;

    printf("%d ", root->value);
    preOrder(root->left);
    preOrder(root->right);
}

/**
 * 二叉樹中序遍歷
 */
void inOrder(BTNode *root)
{
    if (!root) return;

    inOrder(root->left);
    printf("%d ", root->value);
    inOrder(root->right);
}

/**
 * 二叉樹後序遍歷
 */
void postOrder(BTNode *root)
{
    if (!root) return;

    postOrder(root->left);
    postOrder(root->right);
    printf("%d ", root->value);
}

/**
 * 二叉樹層序遍歷
 */
void levelOrder(BTNode *root)
{
    int btHeight = height(root);    
    int level;
    for (level = 1; level <= btHeight; level++) {
        levelOrderInLevel(root, level);
    }
}

/**
 * 二叉樹層序遍歷輔助函式-列印第level層的結點
 */
void levelOrderInLevel(BTNode *root, int level)
{
    if (!root) return;

    if (level == 1) {
        printf("%d ", root->value);
        return;
    }
    levelOrderInLevel(root->left, level-1);
    levelOrderInLevel(root->right, level-1);
}
複製程式碼

非遞迴遍歷-先序、中序、後序、層序

  • 非遞迴遍歷裡面先序遍歷最簡單,使用一個棧來儲存結點,先訪問根結點,然後將右孩子和左孩子依次壓棧,然後迴圈這個過程。中序遍歷稍微複雜一點,需要先遍歷完左子樹,然後才是根結點,最後才是右子樹。
  • 後序遍歷使用一個棧的方法postOrderIter()會有點繞,也易錯。所以在面試時推薦用兩個棧的版本postOrderIterWith2Stack(),容易理解,也比較好寫。
  • 層序遍歷用了佇列來輔助儲存結點,還算簡單。
  • 這裡我另外實現了一個佇列 BTNodeQueue 和棧 BTNodeStack,用於二叉樹非遞迴遍歷。

/*********************/
/** 二叉樹遍歷-非遞迴 **/
/*********************/
/**
 * 先序遍歷-非遞迴
 */
void preOrderIter(BTNode *root)
{
    if (!root) return;

    int size = btSize(root);
    BTNodeStack *stack = stackNew(size);

    push(stack, root);
    while (!IS_EMPTY(stack)) {
        BTNode *node = pop(stack);
        printf("%d ", node->value);

        if (node->right)
            push(stack, node->right);

        if (node->left)
            push(stack, node->left);
    }
    free(stack);
}

/**
 * 中序遍歷-非遞迴
 */
void inOrderIter(BTNode *root)
{
    if (!root) return;

    BTNodeStack *stack = stackNew(btSize(root));

    BTNode *current = root;
    while (current || !IS_EMPTY(stack)) {
        if (current) {
            push(stack, current);
            current = current->left;
        } else {
            BTNode *node = pop(stack);
            printf("%d ", node->value);
            current = node->right;
        }
    }
    free(stack);
}

/**
 * 後續遍歷-使用一個棧非遞迴
 */
void postOrderIter(BTNode *root)
{
    BTNodeStack *stack = stackNew(btSize(root));
    BTNode *current = root;
    do { 
        // 移動至最左邊結點
        while (current) { 
            // 將該結點右孩子和自己入棧
            if (current->right) 
                push(stack, current->right); 
            push(stack, current); 
  
            // 往左子樹遍歷
            current = current->left; 
        } 
  
        current = pop(stack); 
  
        if (current->right && peek(stack) == current->right) { 
            pop(stack);
            push(stack, current);
            current = current->right;
        } else { 
            printf("%d ", current->value); 
            current = NULL; 
        } 
    } while (!IS_EMPTY(stack)); 
}

/**
 * 後續遍歷-使用兩個棧,更好理解一點。
 */
void postOrderIterWith2Stack(BTNode *root)
{
    if (!root) return;

    BTNodeStack *stack = stackNew(btSize(root));
    BTNodeStack *output = stackNew(btSize(root));

    push(stack, root);
    BTNode *node;

    while (!IS_EMPTY(stack)) {
        node = pop(stack);
        push(output, node);

        if (node->left)
            push(stack, node->left);

        if (node->right)
            push(stack, node->right);
    }

    while (!IS_EMPTY(output)) {
        node = pop(output);
        printf("%d ", node->value);
    }
}

/**
 * 層序遍歷-非遞迴
 */
void levelOrderIter(BTNode *root)
{
    if (!root) return;

    BTNodeQueue *queue = queueNew(btSize(root));
    enqueue(queue, root);

    while (1) {
        int nodeCount = QUEUE_SIZE(queue);
        if (nodeCount == 0)
            break;
btHeight
        while (nodeCount > 0) {
            BTNode *node = dequeue(queue);
            printf("%d ", node->value);

            if (node->left)
                enqueue(queue, node->left);

            if (node->right)
                enqueue(queue, node->right);

            nodeCount--;
        }
        printf("\n");
    }
}
複製程式碼

參考資料

相關文章