數據結構之二叉樹(C語言實現)

Table of Contents

定義

實現定義結構

定義操作構造空二叉樹

創建二叉樹

遞歸先序遍歷

遞歸中序遍歷

非遞歸先序遍歷

非遞歸中序遍歷

非遞歸後序遍歷

層次遍歷

訪問結點


定義

之前四篇博客分別介紹了線性結構中的順序表鏈表隊列。從難度來講,順序表到鏈表是遞增的。從實現來講,棧和隊列基於順序表和鏈表(之前棧採用了順序表的存儲結構,隊列採用了鏈表的存儲結構)。此次介紹的二叉樹雖是非線性結構的樹形結構分支,但在其各個結點遍歷的實現上,使用到了棧和隊列的特性。

二叉樹是一種特殊的線性結構,每個結點最多隻有兩個分支,稱左孩子結點和右孩子結點。更多關於二叉樹的特性,自行查閱資料。接下來只詳細的介紹創建二叉樹以及二叉樹的遍歷。

實現
定義結構

typedef char TreeType;

typedef struct BitNode {
    TreeType key;
    struct BitNode *left;
    struct BitNode *right;
} BitTree;

二叉樹的結構和雙向鏈表的結構一致,只是雙向鏈表的兩個指針構成線性結構,二叉樹的兩個指針構成非線性結構。

定義操作
構造空二叉樹

/**
 * 構造空二叉樹
 */
void initBitTree(BitTree& root) {
    root.left = NULL;
    root.right = NULL;
}

將根結點的左右兩個指針置空。

創建二叉樹

/**
 * 創建二叉樹(按照前序遍歷方式構建二叉樹)
 */
void createBitTree(BitTree** parent) {
    char key = getchar();
    getchar();
    if (key == '#') { //輸入#表示該節點是葉子節點
        *parent = NULL;
        return;
    } else {
        *parent = (BitTree*) malloc(sizeof(BitTree));
        if (*parent == NULL)
            exit(9);
        (*parent)->key = key;
        createBitTree(&((*parent)->left));
        createBitTree(&((*parent)->right));
    }
}

先序遍歷的方式構建二叉樹,輸入#號表示當前結點的左孩子結點或右孩子結點爲空。

遞歸先序遍歷

/**
 * 遞歸先序遍歷
 */
void preOrderTraverse(BitTree* parent) {
    if (parent != NULL) {
        visit(*parent);
        preOrderTraverse(parent->left);//遍歷左子樹
        preOrderTraverse(parent->right);//遍歷右子樹
    }
}



先訪問根結點,再依次遞歸訪問左子樹和右子樹

遞歸中序遍歷

/**
 * 遞歸中序遍歷
 */
void inOrderTraverse(BitTree* parent) {
    if (parent != NULL) {
        inOrderTraverse(parent->left);//遍歷左子樹
        visit(*parent);
        inOrderTraverse(parent->right);//遍歷右子樹
    }
}

先遞歸遍歷左子樹,再訪問根節點,最後遞歸訪問右子樹。

遞歸後序遍歷

/**
 * 遞歸後序遍歷
 */
void postOrderTraverse(BitTree* parent) {
    if (parent != NULL) {
        postOrderTraverse(parent->left);//遍歷左子樹
        postOrderTraverse(parent->right);//遍歷右子樹
        visit(*parent);
    }
}

先依次遞歸訪問左子樹和右子樹,最後訪問根結點。

*三序遍歷的遞歸方式簡單的介紹到這裏,三序遍歷的非遞歸方式一個比一個難,這是本人自行思考寫出的算法,若說閱讀了參考資料那也是很久之前的事情。因此覺得,這三段代碼還是很有閱讀價值。

非遞歸先序遍歷

/**
 * 非遞歸先序遍歷
 */
void preOrderTraverseNormal(BitTree* parent) {
    Stack stack;
    if (initStack(stack) == 0)
        return;
    if (parent == NULL)
        return;
    push(stack, *parent);
    BitTree topNode;
    while (pop(stack, topNode)) { //取棧頂元素,訪問並出棧
        visit (node); //訪問棧頂元素
        if (topNode.right != NULL) { //存在右結點,則先將右結點入棧。因爲左結點先遍歷
            push(stack, *(topNode.right));
        }
        if (topNode.left != NULL) { //存在左結點,左結點入棧
            push(stack, *(topNode.left));
        }
    }
}

非遞歸遍歷的重點是手動構造遞歸棧。首先將根結點入棧,然後在while循環中,先將棧頂結點出棧,並依次將該結點的右孩子結點和左孩子結點入棧(如果存在),知道棧爲空pop函數返回0 結束循環。

非遞歸中序遍歷

/**
 * 非遞歸中序遍歷
 */
void inOrderTraverseNormal(BitTree* parent) {
    Stack stack;
    if (initStack(stack) == 0)
        return;
    if (parent == NULL)
        return;
    push(stack, *parent);
    BitTree topNode;
    while (top(stack, topNode)) { //取棧頂結點
        /**
         * 首先,一直遍歷到最左邊的結點
         */
        while (topNode.left != NULL) { //左孩子結點不爲空,入棧
            push(stack, *(topNode.left));//左孩子結點入棧
            topNode = *(topNode.left);//看下一個左孩子結點
        }
        /**
         * 其次,判斷其是否存在右孩子結點。
         * 不存在右孩子結點,直接將該結點出棧
         */
        int flag = 1;
        while (flag && topNode.right == NULL) {
            pop(stack, topNode);//出棧
            visit(topNode);
            flag = top(stack, topNode);//取棧頂結點,繼續判斷
        }
        /**
         * 存在右孩子結點,當前結點出棧,並將右孩子結點入棧
         */
        if (pop(stack, topNode)) {
            visit(topNode);
            push(stack, *(topNode.right));
        }
    }
}



先將根結點入棧,然後根據根結點一直尋找到該左子樹的最左邊結點,訪問該結點。如果該結點不存在右子樹,直接將該結點出棧,並一直出棧到棧頂的結點存在右子樹。此時將棧頂結點出棧,並將該結點的右孩子結點入棧,並尋找到該結點右子樹的最左邊結點。

非遞歸後序遍歷

/**
 * 非遞歸後序遍歷
 */
void postOrderTraverseNormal(BitTree* parent) {
    /**
     * 聲明兩個棧,遍歷樹的管理棧和備用棧
     * 備用棧的作用:部分結點存在兩次訪問的,備用棧是記錄第一次訪問,然後入棧。
     * 也可以在結點中添加一個標籤記錄訪問次數,備用棧的設計是爲了避免修改結點
     */
    Stack stack, backup;
    if (initStack(stack) == 0)
        return;
    if (initStack(backup) == 0)
        return;
    if (parent == NULL)
        return;
    push(stack, *parent);
    BitTree topNode; //記錄當前棧頂結點
    BitTree backupNode; //記錄備用棧的棧頂結點
    BitTree lastNode; //上次訪問的結點
    while (top(stack, topNode)) { //取棧頂結點
        int flag = top(backup, backupNode); //備用棧的棧頂元素,返回0表示備用棧爲空。
        if (flag == 0 || compareTreeNode(topNode, backupNode) == 0) { //該結點是第一次訪問
            if (topNode.left != NULL
                    && compareTreeNode(*(topNode.left), lastNode) == 0) { //左孩子結點不爲空,且上次訪問的不是左孩子結點
                push(stack, *(topNode.left)); //左孩子結點入棧
                continue;
            }
            push(backup, topNode);
            if (topNode.right != NULL
                    && compareTreeNode(*(topNode.right), lastNode) == 0) { //右孩子結點不爲空,且上次訪問的不是右孩子結點
                push(stack, *(topNode.right)); //右孩子結點入棧
                continue;
            }
        } else { //該節結點是第二次訪問,直接出棧
            pop(backup, backupNode); //備用棧棧頂元素出棧
            pop(stack, topNode); //當前棧棧頂元素出棧
            visit(topNode); //訪問剛出棧的結點
            lastNode = topNode; //記錄剛剛訪問的結點
        }
    }
}

考慮到後續遍歷的特殊性質,根結點會在左孩子結點和右孩子結點出棧時訪問兩次。多數資料上的實現方式都是通過在每個結點中添加一個標誌記錄根節點的訪問次數。爲了維護之前定義好的結構體的完整性。用一個備用棧,完美的解決問題。

取遞歸棧中的棧頂元素和備用棧中的棧頂元素(如果存在)對比,如果相同,就是第二次遍歷到該結點。分別將遞歸棧和備用棧中的棧頂元素出棧,訪問並記錄當前出棧的結點。如果不相同,就是第一次訪問該結點,此時需要考慮當前遞歸棧中棧頂結點是否存在左子樹或右子樹以及上次出棧的結點是否是該結點的左孩子結點或右孩子結點。
當該結點不存在左子樹或上次出棧的結點是該結點的左孩子結點,則標記當前遞歸棧中的棧頂結點已經訪問過一次,將該結點添加到備用棧中。

說了這麼多,有點繞。經過長時間的思考以及兩次優化之後的成果。越是難懂的算法不一定就是最高級的算法,此處 ,不對我的代碼做任何評價。

層次遍歷

/**
 * 層次遍歷
 */
void levelOrderTraverse(BitTree* parent) {
    Queue queue = createQueue();//創建隊列
    enterQueue(queue, *parent);//根結點入隊
    BitTree node;
    while (exitQueue(queue, node)) {//隊列中結點出隊,隊列爲空,返回0,while循環結束
        visit(child);//訪問隊列中第一個結點
        if (node.left != 0)//判斷是否存在左孩子結點,將左孩子結點入隊
            enterQueue(queue, *node.left);
        if (node.right != 0)//判斷是否存在右孩子結點,將右孩子結點入隊
            enterQueue(queue, *node.right);
    }
}

看完前面三個非遞歸的遍歷算法,也許都暈了。層次遍歷,沒有遞歸或非遞歸而言,自頂向下、從左到右訪問二叉樹中所有的結點。先訪問的結點子樹上的全部結點一定比後訪問結點子樹上的全部結點先訪問,所以層次遍歷用到了隊的特性。

訪問結點

/**
 * 訪問
 */
void visit(BitTree node) {
    printf("%c", node.key);
}


這只是模擬訪問結點的操作,可根據需要自定定義該函數的功能。

上述代碼中用到的棧和隊列中的函數,都是複用了之前博客中介紹的棧和隊列的操作函數。只是修改下每個元素的結點類型。

#ifndef STACK_H_
#define STACK_H_

#include "tree.h"

#define LIST_INIT_SIZE 10
#define LISTINCREMENT 2

typedef BitTree StackType; //棧中每個元素的結點類型是二叉樹

typedef struct stackNode {
    StackType *elem; //存儲空間基地址
    int length; //當前長度
    int listsize; //當前分配的存儲容量(以sizeof(ElemType)爲單位)
}Stack;

……

#ifndef QUEUE_H_
#define QUEUE_H_

#include "tree.h"

typedef BitTree QueueType;//隊列中每個元素的結點類型是二叉樹

struct LinkQueue {
    QueueType key;
    struct LinkQueue *next;
};

typedef struct queueNode {
    struct LinkQueue *head; //隊列的頭指針
    struct LinkQueue *end; //隊列的尾指針
} Queue;

……


最後,附上頭文件的定義,部分方法未實現。

/*
 * tree.h
 *
 *  Created on: 2016年9月25日
 *      Author: flueky
 */
#include "stack.h"
#include "queue.h"

#ifndef TREE_H_
#define TREE_H_

typedef char TreeType;

typedef struct BitNode {
    TreeType key;
    struct BitNode *left;
    struct BitNode *right;
} BitTree;

/**
 * 構造空二叉樹
 */
void initBitTree(BitTree&);
/**
 * 銷燬二叉樹
 */
void destoryBitTree(BitTree&);
/**
 * 創建二叉樹
 */
void createBitTree(BitTree**);
/**
 * 將二叉樹清爲空樹
 */
void clearBitTree(BitTree&);
/**
 * 遞歸先序遍歷
 */
void preOrderTraverse(BitTree*);
/**
 * 非遞歸先序遍歷
 */
void preOrderTraverseNormal(BitTree*);
/**
 * 遞歸中序遍歷
 */
void inOrderTraverse(BitTree*);
/**
 * 非遞歸中序遍歷
 */
void inOrderTraverseNormal(BitTree*);
/**
 * 遞歸後序遍歷
 */
void postOrderTraverse(BitTree*);
/**
 * 非遞歸後序遍歷
 */
void postOrderTraverseNormal(BitTree*);

/**
 * 層次遍歷
 */
void levelOrderTraverse(BitTree*);
/**
 * 比較兩個結點,相同返回1,不同返回0
 */
int compareTreeNode(BitTree, BitTree);
/**
 * 訪問
 */
void visit(BitTree);

#endif /* TREE_H_ */

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章